kafka实战 - 数据可靠性
概述
常见的存储高可用方案的根本原理就是把数据复制到多个存储设备,通过数据冗余的方式来实现数据的可靠性。比如同一份数据,一份在城市A,一份在城市B。如果城市A发生自然灾害导致机房瘫痪,那么业务就可以直接切到城市B进行服务,从而保障业务的高可用。但是这是理想情况,一旦数据被复制并且分开存储,就涉及到了网络传输,即使在同机房,而且网络状况良好的情况下,也会有10ms以上的延迟,而这种延迟导致各种各样的问题。假设A和B是主备模式,B的数据是A的备份,A对外服务,如果A挂了,B直接替换掉A对外服务。假如A挂了,但是有一小部分数据没来得及同步到B。这时候CAP的场景就出现了,是选择一致性还是高可用?如果选择高可用,直接让B替换掉A成为master对外服务,当A恢复成为slave后,势必需要截断自己多余的数据然后从B同步数据,那么A挂掉之前写入A但是没有同步B的这些数据就算是丢了,导致数据不一致。如果选择一致性,那么B就不能对外服务,虽然数据一致了,但是高可用无法保证。
kafka的数据可靠性是通过副本机制实现的,同样也会存在上述的问题。但是它提供了很多可配置的参数来帮用户定制网络分区发生后的策略。我们的业务有个原则是不管发生什么,数据一定不能丢。所以下面从数据一致性的角度来讨论这些参数的配置,保证数据的可靠性。
数据的可靠性需要producer和broker二者的配置一起来保证。
producer端
producer负责给kafka生产数据,一共需要注意以下2个问题:
<1> 现在的大部分kafka的producer client都提供了异步生产数据的接口。一方面使用异步接口可以同时给多个broker生产数据,另一方面异步接口在把数据写入本地缓存,还没有开始发送就会直接返回。所以异步接口比同步接口要快的多,大部分场景下(比如php的client)produce方法默认使用的也是异步接口。producer的数据一定是从上游通过某种方式传过来的,但是调用produce方法后,数据其实并没有发出去,而是缓存在内存中。如果在数据发出去之前,producer这个进程挂掉,那么没有发出去的数据就会丢失,但是上游的数据已经被消费掉了。所以上游和producer之间,一定需要一种确认机制,确认producer这边成功后,上游才把数据删掉。不能调用完produce发现没有返回错误就继续去上游消费或者向上游报告produce成功。因为是异步接口,produce方法返回的只是是否成功写入内存,而不是发送结果。这种发送结果一般是通过回调函数的方式进行返回的,例如php就是在初始化producer实例时,调用setDrMsgCb()方法设置回调的。在回调函数中,你可以获得发送失败的错误码,错误字符串,和partition,topic等有用的信息。通过这些信息即可判断是否发送成功。
<2> request.required.acks参数。这个参数会和要生产的数据一块发送给broker。告诉broker写入多少个ISR副本后返回。一共可以设置3种类型的值:
a. 0,broker不会给producer返回response,producer也不会去等,这种情况下生产数据的速度是最快的。但是只适用于对数据是否丢失根本不在意的场景下。网络丢包,leader partition 挂掉,都会导致数据丢失。
b. 1, broker会保证写入leader副本之后再返回response(producer端的回调函数会响应这个response)。这种方式在写入leader副本,但是还没有来得及同步到其他副本之前,leader挂了,就会丢失数据。
c. all或者-1。broker会保证把数据写入所有的ISR副本后返回。这种方式是数据最高可靠性的设置。只要不是这个partition的所有副本所在的broker一块死掉,数据就不会丢失。但是同时生产效率也是最慢的。比如有3个副本,ABC,A为leader,A和B的延时是50ms, A和C的延时是100ms, 那么producer的耗时最起码是 50 + 100 + 3台机器数据写入时间 + A和client的通讯时间。这个具体的性能下降程度和场景有关。笔者做的测试是,一台机器,起3个broker,每个topic有2个副本,localhost的producer发送的数据大小随机从1M到150M。在acks=1时,每发送1MB位25ms, acks=-1时,没发送1MB需要的时间为30ms,性能下降17%左右。
一般情况下,如果想要数据有比较好的保证,最好是设置成-1。另外注意一点,这个参数是topic-level,应该设置在topic-level-conf中,在php的client中亲测设置到global-level-conf中没有生效,也没有报错。
broker端
<1> unclean.leader.election.enable参数
kafka的副本机制的原理就是一个partition有多个副本,leader副本负责接受client的读写,其他follower副本负责从leader副本同步数据。和leader副本相差的数据在一定阈值内的副本叫做ISR副本(包括leader)。ISR是动态的,如果某个follower挂了,长时间不从leader拉取数据,就会被踢出ISR。而如果某个follower挂掉之后恢复,很快又追上leader的数据,那么又会被放入ISR中。如果leader挂了,选一个follower作为新的leader。unclean.leader.election.enable参数控制的是非ISR副本是否可以被选为leader。
想把这种非ISR的副本被选为leader造成的后果讲清楚,会涉及到很多其他的概念,比如HW和LEO。网上已经有不少好文解释了这些概念:https://www.cnblogs.com/huxi2b/p/7453543.html,大家可以参考一下。
kafka会保证只要有一个ISR还活着,commited的数据就不会丢失。什么是commited的数据?简单来说就是已经保存在所有ISR中的数据。这些数据都是已经给producer明确返回生产成功response的数据。consumer也只能消费到commited的数据。所以如果leader挂了,选举的新leader是ISR中的另一个副本,新的leader会把自己的LEO做位HW,LEO比新leader小的follower会从新的leader的LEO处开始同步数据,LEO比新leader高的,会把自己的日志截断到新leader的LEO处,然后开始同步。所以这种情况下,对producer来说,已经成功生产的数据没有丢失。对于consumer来说,仍然不会消费到对producer来说没有produce成功的数据。partition的failover看起来是透明的。
但是如果unclean.leader.election.enable为True,允许选举非ISR中的副本为新leader,这时候就会发生commited的数据也会丢失的情况。对于producer来说,已经生产成功的数据就会丢失。consumer也可能因为发送的需要消费的offset比新leader的LEO还要大,所以从头开始消费(这在消费数据不能重复或者已经保存了很多数据的情况下是非常严重的事情,对于实时性的业务,不但会影响数据的实时性,而且可能把下游写爆)。这篇博客讲的很好,可以参考一下:https://blog.csdn.net/u013256816/article/details/80790185。
<2> min.insync.replicas
这个参数是和producer端的request.required.acks配合使用的,只有把request.required.acks设置为-1或者all时,这个参数才会生效。这个参数设置的是broker在给producer返回response前,最少写入ISR的个数。想向一种场景,某个partition,leader还在,但是其他副本全挂了。设置acks为-1,那么broker把数据写入所有ISR(仅仅是leader)之后,返回。副本挂的就剩leader一个了,我们不知道。数据只写入leader,并没有复制到其他副本,我们也不知道。leader一挂,数据就完蛋。所以把这个参数配置成和partition个数一样或者比partition个数稍小一点。这样当broker无法写入这么多ISR之后,会给producer返回特殊的response,producer也会通过回调的错误码,或者抛出NotEnoughReplicas让开发人员指导情况,该修集群修集群,该重新发送重新发送。这个参数可以设置为topic-level的,因为不同的topic副本个数可能不一样,很难使用全局的min.insync.replicas满足所有topic。
其他参考资料:
kafka controller相关协议: https://cwiki.apache.org/confluence/display/KAFKA/Kafka+Controller+Internals
kafka replica相关协议: https://cwiki.apache.org/confluence/display/KAFKA/Kafka+Replication
kafka各种failover场景:https://www.cnblogs.com/fxjwind/p/4972244.html