读《Kafka权威指南》笔记
Kafka是一个高吞吐量的MQ,具有高可用、低延迟、容灾等特性,Kafka保证了消息至少被消费1次,也就是说可能重复消费
持久化
按照一定顺序持久化保存,可按需读取
消息:类似数据行
批次:一组消息,批次数据会被压缩
主题和分区:一个主题可以在多个分区上,每个分区上FIFO保持顺序,但无法保持整个主题的有序性
把主题的数据看作一个流,不管它有多少个分区,流是一组从生产者移动到消费者的数据
消费者对分区拥有所有权,消费者失效后,其他消费者可以接管这个分区
同步消息的偏移量,记录消费者上次消费到什么地方
框架可以通过分区id + 偏移量来排重
Broker
Kafka服务器被称为broker
接受生产者的消息,为消息生成偏移量,保存到磁盘中
为消费者提供服务-返回提交到磁盘上的消息。硬件厉害时,一个broker可轻松处理数千个分区、以及每秒百万级的消息量
集群
每个集群都有一个broker同时充当了集群控制器的角色
一个分区从属于一个broker,该broker被称为分区的首领。一个分区可以分配给多个broker,提供冗余,如果broker失效,其他broker可以接管分区,相关的生产者和消费者可以连接到新的首领上去;首领的维度是分区,而不是主题
保留策略
主题可以配置保留策略,默认的策略是要么7天,要么达到一定的大小,如1GB,可以根据业务进行配置,如1个月,或者1天,或者保留最后一条数据
复制策略
提供了MirrorMaker,通过一个队列将生产者消费者相连,类似于将两个kafka,通过其中一个kafka的主题连接起来
保证亚秒级别的延迟
使用场景
- 活动跟踪:用户的操作记录
- 传递消息
- 度量指标和日志记录,如把日志路由到ElasticSearch上,让其他程序可以搜索
- 提交日志,如将数据库的更新发布到Kafka上,可以把更新复制到远程系统上,数据持久化为变更日志提供了缓冲区
- 流处理
为什么没有用ActiveMQ
当时ActiveMQ对水平扩展支持不够强大(水平扩展:加机器,自动增加能力)(垂直扩展:使单机的性能提升,比如提升硬件)
Zookeeper
Kafka使用Zookeeper保存集群的元数据信息和消费者信息
主题默认配置
num.partitions ,默认1,主题的分区个数需要综合考虑:生产者、消费者吞吐量,分区方式,如按键来分区,分区越多完成首领选举时间越长
log.retention.ms ,消息保留时间,默认为1周
log.retention.bytes, 单个分区保留的消息最大字节数,多出来的部分将被删除
log.segment.bytes, 默认1GB,单个日志片段的大小,超出后就会生成一个新的日志片段,当前片段就开始等待过期。当前片段是否能被删除,是按最后一条消息的时间戳来的。如果设置了1周过期时间,则要等到最后一条消息后的一周才能删除
message.max.bytes 单条消息的大小限制,默认1000 000即1MB,超过这个大小,broker将返回给生成者错误信息
建议
磁盘性能越好,生成消息的延迟就越低,生产者会等待broker告诉它消息是否发送成功
预估磁盘容量
内存影响消费者的速率,Kafka使用系统内存作为页面缓存,可以快速响应消费者的请求
对CPU略有要求,主要是为了存储时对客户端消息批量解压缩,设置偏移量,再批量压缩,再保存到磁盘上,但并非主要因素
需要多少个broker
- 单个broker有多少磁盘空间,计数总的空间 + 复制所需的空间 来确定broker数量
- 集群处理能力,与网络接口处理客户端流量的能力有关
调优
- 避免内存交换(虚拟内存使用文件系统模拟内存,但文件系统的性能始终不能与RAM内存媲美)
- 文件系统noatime配置(不要文件创建时间)
- 网络socket读写缓冲区 128K-2MB、Tcp socket读写缓冲区配置4K 64K 2MB
- 使用G1垃圾回收器,kafka会频繁的生成垃圾,5GB内存时,MaxGCPauseMills 可设置为20ms,InitiatingHeapOccupancyPercent 35,即老年代+新生代使用超过 35%就进行垃圾回收,提前回收垃圾
- broker放到不同的机架上
- kafka集群单独使用一个zookeeper,而非多个kafka集群共用一个zookeeper集群,因为kafka对zookeeper的延迟非常敏感
生产者
- 指定broker清单,两个就行,自动发现其他broker
- key.serialization
- value.serialization
- acks=0,1,all 服务端有多少个分区副本收到消息,生产者才认为消息是成功的
- buffer.memory 生产端本地消息缓冲区大小
- compression.type 消息发送到broker前进行压缩
- retries 生产者重试次数
- batch.size 当有多少消息需要被发送到同一个分区时,生产者会把他们放到一个批次,将这些消息发送出去(半满或一条消息也会被发送,设置太小会导致频繁的发送消息)
- linger.time 默认为0,等待其他消息组成批次的时间,和参数batch.size相关
- client.id 生产者的id
- max.in.flight.requests.per.connection 生产者在接受到服务器响应前可以发多少条消息,为1时,消息顺序写入服务器
想达到严格顺序怎么做:
不建议retries设置为0,建议max.in.flight.requests.per.connection设为1
- 发送并忘记 发送失败的消息将丢失
- 同步发送
- 异步发送
send方法仅把消息放到缓冲区,由一个单独线程来发送到服务器
Avro
通过语言无关的schema来定义,通过JSON来描述
如
{
"namespace":customerManagement.avro",
"type":"record",
"name":"Customer",
"fields":[
{"name":"id","type":"int"},
{"name":"name","type":"string"},
{"name":"email","type":["null","string"],"default":"null"}
]
}
使用schema注册表来保存数据的schema,而不再需要在数据文件中包含schema,仅保存schema id即可
自定义分区器
实现Partitioner接口,依据示例来看,kafka生成者会将分区列表拉到本地,然后在内存中判断需要把数据放到哪个分区,如将最后一个分区n分配给其中一种特殊信息,其余类型的数据共享n-1个分区
消费者、消费者群组
我们有必要为主题创建大量的分区,在负载增长时可以加入更多的消费者。但要注意,消费者多余分区个数时,消费者将闲置
增加一个消费者群组,将会消费所有该主题的消息,有点类似于不同的应用都订阅了这个主题
消费者群组中的消费者只会获取主题其中一个分区的数据
分区的所有权从一个消费者转移到另一个消费者,这样的行为被称为再平衡
第一个加入群组的消费者会成为群主,从协调器获得群组成员列表,并负责给每一个消费者分配分区,再把分配情况列表发送给群组协调器,协调器再把这些信息发送给所有消费者,有两种策略进行分配:将连续的分区给同一个消费者、轮询挨个分配
在poll()时会查找GroupCoordinator,然后加入群组,发送JoinGroup请求,接受分配的分区,再平衡也是这样
0.10.1版本后,消费者和broker间的心跳已改为一个单独的线程来发送心跳
处理分区消息读取偏移量
- 自动提交 基于时间间隔,默认5s 可能会发生重复消费,在轮询里面发生
- 提交当前偏移量,而不是基于时间间隔,auto.commit.offset设置为false,让程序决定何时提交偏移量(手动consumer.commitSync()),提交成功后马上返回,失败就抛出异常
- 异步提交 consumer.commitAsync() 不等待broker返回,注册回调函数处理结果,可以在回调函数里面判断是否失败,失败后可以选择再提交,也可以判断是否已经有更新的offset被提交,如果有就不重新提交
在consumer close之前,最好触发一次同步的提交consumer.commitSync();再close掉consumer
-
可以提交批次中间数据的偏移量
-
可以把分区的偏移量存储到数据库中,发生rebalance的时候,seek分区的offset,避免重复读取
-
优雅的退出,最安全的方法,调用consumer.wakeup();下次调用consumer.poll时将抛出WakeupException异常.可以使用ShutdownHook来注册wakeup事件
-
可以为自己分配分区,调用consumer.assign(partitions)让消费者订阅当前所有的主题分区,如果主题新增了分区,不会自动感知,需要定时调用consumer.partitionsFor("topic");来刷新
-
通过ConsumerRebalanceListener,在subscribe()方法绑定操作,在再均衡开始前和消费者停止消费当前分区后做了一些操作,如提交当前偏移量到数据库中,还有方法用于处理重新分配分区后和消费者开始读取消息前做一些操作
-
可以将处理数据和保存偏移量变成一步,方案是将数据和偏移量都放在一个数据库中,利用事务包裹,保证原子成功或失败
-
消费者往一个叫_consumer_offset的特殊主题里提交偏移量
问题
协调器等待每个消费者真正停止消费并响应后,才告诉他们重新开始消费吗?
集群成员关系
启动时broker向zookeeper注册临时节点,路径/broker/ids,将自己的id放到路径下
控制器:
其实就是一个broker,启动时尝试创建一个临时节点/controller,如果失败就监听它,如果成功,当前broker就成为集群的控制器,负责主题分区管理,每次有新的broker成为controller后,epoch会更新为更大的epoch,使用zookeeper的顺序临时节点即可自动获取epoch
当新加入broker或broker失效后,控制器将收到watcher事件,检查所有的分区,哪些分区失去了首领,找一个副本分区作为新的首领,并告知对应的broker跟踪新的首领,随后,跟随者从新首领那里复制消息
分区副本
- 首领副本 首领副本需要搞清楚哪个跟随者和当前首领数据是同步的(ISR),这将用于新首领选举,所有生产者和消费者都会经过这个副本,ISR的数据将写入zk及本地cache
- 跟随者副本 只有同步的副本才有可能被选举为新首领,不处理来自客户端的请求,跟随者就像一个客户端一样来同步数据,首领可以知道其偏移量
首领
- 首选首领 创建主题时选择的分区首领,如果检查首选首领不是当前首领,且首选首领是同步的,就将它设置为当前首领 第一个副本一般就是首选首领
Kafka要求客户端自己负责把生产请求和获取请求发送到正确的broker上去,客户端通过发送元数据请求,获取自己感兴趣的主题,从响应中获取首领分区的位置,元数据请求可以发送给任意broker,因为所有broker都缓存了这些信息,客户端可定时(metadata.max.age.ms)获取元数据信息或收到非首领错误后重新获取元数据信息
生产请求
- 首领副本收到请求,判断权限
- 检查acks参数
- 根据acks值判断何时响应,在linux系统中消息将被写到文件系统缓存中,而不是磁盘中,并不保证他们何时会被刷新到磁盘上。kafka不会等待数据被写到磁盘上-它依赖复制功能来保证持久性
获取请求
- 客户端可以指定borker最多从分区里返回多少数据,这有利于资源管理,防止客户端内存资源被耗尽
- 检查请求合法性,当前是否为请求分区的首领副本,如果不是,报错
- 读取消息,返回给客户端。kafka使用零复制技术向客户端发送消息:直接把消息从文件系统缓存发送到网络通道,而不需要任何中间缓冲区;避免了字节复制,也不需要管理内存缓冲区。
并不是所有保存到分区首领的数据都可以被获取。大部分客户端只能读取到已经被写入到所有同步副本的消息。这样做是为了避免数据不一致的问题。消息没有写入到所有同步副本的消息,是不会被发送的。一旦首领崩溃,另一个副本成为首领后,消息就丢失了,这将通过同步复制来解决。
其他请求
broker之间也存在着同样的通讯协议,例如当一个新的首领被选举出来后,控制器会发送LeaderAndIsr请求给新首领,这样新首领就可以开始接受新的请求了。
分区分配
- 在broker间均匀的分配副本
- 确保每个分区的副本在不同的broker上
- 如果指定了机架,尽量使不同的副本分配到不同的机架的broker下
在一个大文件下查找和删除消息是非常费时的,所以将数据文件按默认1GB大小或1周分成若干个片段。如果达到上限,就关闭当前文件,打开一个新的文件。
当前正在写入的片段称为活跃片段。
我们把kafka的消息和偏移量保存在文件中。
消息包含了:键、值、偏移量、消息大小、校验和、消息格式版本号、压缩算法、时间戳(可配置,生成者发生的时间戳|broker接受到的时间戳)
建议生产者端使用压缩功能。
索引
kafka为每个分区维护了一个索引,索引把偏移量映射到片段文件和偏移量在文件的位置。索引也被分成片段,这样在删除消息的时候,索引也可以删除,当索引被破坏或删除后,kafka会自动重新生成这些索引。
清理
应用程序从崩溃中恢复时,它从kafka读取消息来恢复状态,它只关心最新的状态,此时可以用清理功能
一个片段分为:干净部分和浑浊部分,干净部分之前已经运行过清理
kafka可以保留某一个key最新的值(如果指定key的话)
通过<键的散列值,消息偏移量>来记录消息,生成一个map,100w个消息仅需要消耗24MB的内存来存放这个map,这是非常高效的。
清理也是基于片段的,先使用浑浊部分创建好map,再从干净部分的最旧的记录开始,把它们内容和map对比,如果消息的键不在map中,则说明消息是最新的,把消息复制到替换片段上。最后将替换片段和原始片段进行交换,然后开始清理一下一个片段。
问题
使用散列值作为key,就意味着可能存在hash冲突,如何规避这种冲突?
可靠性保证
- 可以保证分区消息的顺序性
- 只有当消息被写入分区的所有同步副本时,才被认为是已提交的
- 只要有一个副本是活跃的,就可以保证已经提交的消息不会丢失
- 消费者只能读取已提交的信息
- 复制系数
- 不完全选举 unclean.leader.election.enable = false,不允许不完全同步的副本成为新的首领,那就需要等待旧首领恢复,不过这段时间,服务将不可用,即便如此,例如银行,也宁可多等待一段时间,也不想出现数据丢失,对可用率要求较高的,如实时点击流,会启用该配置,允许不完全同步的副本成为新的首领
- 最少同步副本数 min.insync.replicas 设为2,则至少需要两个同步副本,才能向集群中写入数据,使用acks参数和min.insync.replicas来保证消息不丢失
小结
Kafka支持多种可靠级别的数据存储、传输,如效率至上的点击流,可以不那么保证数据的可靠性,可配置acks:1,即首领获取到消息就任务发送成功,虽然首领还未同步给所有追随者,偶然崩溃,导致数据丢失,但系统不需要如此精确。
如果要求非常严格的可靠性,则acks:all,min.insync.replicas设置为N-1或N(假如分区有N个副本,且N>2),同时不允许不完全选举unclen.leader.election.enable=false,则可以尽量保证可靠性,如果将不同的broker放置到不同的机架,则更加可靠
建议
- 根据可靠性需求配置恰当的acks值
- broker响应异常时,合理的处理未成功发送的消息
幂等行写入:使用主题、分区、偏移量生成一个唯一键
构建数据管道
- 及时性 大型数据缓冲区
- 可靠性 kafka本身支持“至少传递一次”,再利用其它事务型数据库,可以实现“仅一次传递”
- 高吞吐量和动态吞吐量
- 数据格式
- 转换 提取-转换-加载ETL、ELT
Connect API
...
跨集群数据镜像
- 1个中心,多个本地
- 双活、多活
- 主备
失效备援
- 数据丢失和不一致性 主数据中心崩溃后,不立即切换到备,先关闭主,等数据都同步到备了,再切换到备,同时应用程序提前想好该怎么处理关联数据仅到达一部分的情况
- 偏移量的问题,主备间偏移量不一致,这个无法完全解决,要靠程序上的支持
- 基于时间的失效备援,采用时间戳,根据时间戳查找偏移量,如2:05时集群崩溃,则可以倒推两分钟,处理从2:03开始的数据
远程读取比远程发送更安全,因为远程读取最多读不到数据,消息还在kafka中,而远程发送则会变成读取已成功,但发送失败,此时kafka的偏移量可能已经修改。当然如果有加密后发送到远程的需求,还是在MirroMaker中先加密,再发送,因为在broker上加密对性能影响比较大,此时要确保偏移量的提交是在broker已收到有效副本后的情况下。
监控
非同步分区数量指标,如果集群里面多个broker的非同步分区一直保持不变,很有可能集群中的某个broker已经离线了。
系统负载是对cpu使用情况的度量。在单cpu系统里,负载1表示cpu使用率100%,在多cpu系统里面,如果有24核,则负载100%表示负载数值为24.
监控磁盘不会被用光。
不会用到交换内存。
网络使用情况-和复制系数有关。
什么是流式处理
首先,数据流是无边界的数据,意外着无限和持续的增长。
事件是有序的
不可变的数据记录
事件流是可重播的
流式处理:实时的处理一个或多个事件流,是一个持续的事件而非定时的任务。
时间
- 事件时间,发生事件的时间和记录的创建时间
- 日志追加时间,事件保存到broker的时间
- 处理时间,应用程序收到事件后处理的时间