深入浅出
- broker:每个Kafka实例叫一个broker,每个broker可以轻松处理数千个分区和每秒数百万的消息量
- 分区:一个分区属于一个broker,这个分区叫做主分区,同时分区可以分布在多个broker上面,这些分区叫做分区复制,如果主分区失效,其他分区接管主导权。分区复制只能发生在单个集群中。
- 保留策略:保留一段时间(比如7天),如果超时一定大小就删除旧消息,每个topic可以配置单独的保留策略
- 分组订阅:每条消息保证在分组中只会被消费一次
- 默认会自动创建topic的情况:1、生产者写入消息;2、消费者消费消息;3、客户端获取topic元信息
- Kafka客户端要自己负责把生产和消费请求发到正确的broker上面,否则会收到一个“非首领”错误,这是客户端会从任意一个broker获取并更新自己的元信息
服务器配置
- 使用1024以下端口,需要使用root权限
- 默认端口号9092
auto.create.topics.enable=false #关闭自动创建topic
log.retention.bytes=xxx #是设置每个分区最大保留大小,而不是topic维度
操作系统配置
# 页面缓存配置
vm.swappiness=1
vm.dirty_ratio=60~80
vm.dirty_background_ratio=5
# 网络配置
net.core.wmem_default=128K #socket写缓冲大小
net.core.rmem_default=128K #socket读缓冲大小
net.core.wmem_max=2M #socket写缓冲最大值
net.core.rmem_max=2M #socket读缓冲最大值
net.ipv4.tcp_wmem=4k 64K 2M #tcp socket配置
net.ipv4.tcp_rmem=4k 64K 2M
net.ipv4.tcp_window_scaling=1 #启动tcp时间窗口
net.ipv4.tcp_max_syn_backlog=1024 #设置并发连接数
net.core.netdev_max_backlog=1000 #设置最大排队等待数
生产者
发送消息的三种方式:
- 发送不管结果,直接返回
- 同步发送,send方法返回Future,调用get阻塞
- 异步发送,send方法指定回调函数
如果发送嫌慢,可以采用多线程
如何保证发送不丢失消息:
就是不能不管send的结果就直接返回,要通过get或者添加回调函数来处理失败的情况,失败后记录日志或保存到数据库
配置
client.id
acks=[0 | 1 | all] #0不等待直接返回,1首领节点成功就返回,all全部成功才返回
retries=n #收到服务器的错误,重试次数
retry.backoff.ms=n #重试时间间隔
buffer.memory=n #内存缓冲大小
compression.type=[snappy | gzip | lz4] #snappy占用较少cpu来提高可观的压缩比,gzip占用内存大压缩比高
batch.size=n #一批可发送的最大数量
linger.ms #发送前的等待时间,增加吞吐量,增加延迟
max.in.flight.requests.per.connection=n #生成者在收到服务器响应前可以发送多少条消息,设置为1可以保证顺序写入
max.block.ms #调用send和partitionsFor的最大阻塞时间
max.request.size #一批次发送消息的最大值
send.buffer.bytes=n #设置tcp缓冲大小,-1则使用操作系统的默认值
高性能-写入
Kafka的特性之一就是高吞吐率,但是Kafka的消息是保存或缓存在磁盘上的,一般在磁盘上读写数据性能是不高的,但kafka可以轻松支持每秒百万级的写入请求,主要是采用了顺序写入和MMFile(memory mapped files, 内存映射文件, 简称mmap)
顺序写
因硬盘寻址相对读写耗时, 每次读写都会寻址->写入, 随机I/O(RDB很多都是随机I/O)要比顺序I/O慢很多. 为了提高读写硬盘的速度, kafka就是使用顺序I/O. 这样省去了大量的内存开销(减少jvm垃圾回收)以及节省了IO寻址的时间.
但是单纯的使用顺序写入, kafka的写入性能也不可能和内存(寻址读写速度远远高于硬盘)进行对比, 因此Kafka的数据并不是实时的写入硬盘中
MMAP
kafka充分利用了操作系统分页存储来利用内存提高I/O效率. mmap称为内存映射文件, 在64位操作系统中一般可以表示20G的数据文件
它的工作原理是直接利用操作系统的pagecache实现磁盘文件到物理内存的直接映射. 完成MMP映射后, 用户对内存的所有操作会被操作系统自动的刷新到磁盘上,极大地降低了IO使用率. (用户将不直接把数据写入磁盘, 而是写入内存(pagecache)中, pagecache数据会被系统定时(间隔由系统参数配置)刷写进磁盘)
直接写入硬盘, 速度慢, 但安全; 使用pagecache, 速度快, 但操作系统宕机(断电)会丢失数据(应用(用户态)挂掉不影响, pagecache是内核态, 数据依然会被操作系统刷入硬盘)
消费者
- 同一个消费组里面,消费者大于分区数会导致一些消费者被闲置,不能收到消息,因为一个分区只能由消费组中一个消费者消费,因此分区数量决定了并发消费的上限
- 当消费者增加和删除消费者时,会发生再均衡导致一小段时间不会消费消息(再均衡:在同一个消费组当中,分区所有权从一个消费者转移到另一个消费者)
- 订阅消息可以使用正则表达式匹配topic
- 唯一一次消费解决方案:使用数据库保存offset,处理逻辑和保存offset在同一个事物里面。offset通过ConsumerRebalanceListener监听和获取偏移量并设置要读取的开始偏移量
- consumer.wakeup()退出poll(),是consumer唯一一个线程安全的方法
Runtime.getRuntime().addShutdownHook(new Thread() {
public void run() {
consumer.wakeup();
}
});
- 独立消费者:只有一个消费者消费数据,因为消费组有再均衡的问题,为了避免这个问题,可以使用如下API:
List<PartitionInfo> partitionInfoList = consumer.partitionsFor(ProducerTest.topic);
List<TopicPartition> topicPartitionList = partitionInfoList.stream()
.map(item -> new TopicPartition(item.topic(), item.partition()))
.collect(Collectors.toList());
consumer.assign(topicPartitionList);
while(true) {
ConsumerRecords<String, String> poll = consumer.poll(Duration.ofMillis(100));
for (ConsumerRecord<String, String> record : poll) {
System.out.printf("topic:%s,partition:%d,key:%s,content:%s\n",
record.topic(),
record.partition(),
record.key(),
record.value());
}
consumer.commitSync();
}
- Kafka使用零复制技术,直接把消息从系统内核态发送到网络通道,而不需要经过内核态到用户态的拷贝
- 在消息还未复制完成时,消息者不会消费到这些消息,所以消费速度受到副本复制快慢的影响
- 如果中间有些消息想先不处理,可以调用pause保存到缓冲区,调用resume()开始从轮询里面获取新数据
- 基于键的分区,在之后调整分区数量是很困难,所以基于键的分区最好一开始就计算好分区数量
消费者配置
fetch.min.bytes=x #返回的数据大小至少xx,否则阻塞等待到有这么多数据
fetch.max.wait.ms=x #指定最大阻塞等待时间,默认500ms
max.partition.fetch.bytes=x #一次poll,从一个分区能拉取的最大大小
session.timeout.ms=x #会话过期时间,如果过期就被认定为消费者挂掉了
auto.offset.reset=[latest | earliest] #在没有偏移量的情况,指定消费其实位置(最前或者最后)
enable.auto.commit=true #是否自动提交偏移量
auto.commit.interval.ms=100 #设置自动提交偏移量的频率
max.poll.records=n #单次poll最大的返回最大记录数
receive.buffer.bytes=n #设置tcp缓冲大小,-1则使用操作系统的默认值
高性能-读取
kafka服务器在响应客户端读取时, 使用ZeroCopy技术, 直接将需要读取的数据从内核空间的磁盘中传递输出, 而无需将数据读取并拷贝到用户空间, 再进行传输
传统IO操作
- 用户进程调用read等系统调用向操作系统发出IO请求,请求读取数据到自己的内存缓冲区中.自己进入阻塞状态
- 操作系统收到请求后, 进一步将IO请求发送磁盘
- 磁盘驱动器收到内核的IO请求,把数据从磁盘读取到驱动器的缓冲中.此时不占用CPU.当驱动器的缓冲区被读满后,向内核发起中断信号告知自己缓冲区已满
- 内核收到中断,使用CPU时间将磁盘驱动器的缓存中的数据拷贝到内核缓冲区中
- 如果内核缓冲区的数据少于用户申请的读的数据, 重复步骤3跟步骤4, 直到内核缓冲区的数据足够多为止
- 将数据从内核缓冲区拷贝到用户缓冲区, 同时从系统调用中返回. 完成任务
引入DMA(协处理器)后
- 用户进程调用read等系统调用向操作系统发出IO请求,请求读取数据到自己的内存缓冲区中。自己进入阻塞状态。
- 操作系统收到请求后,进一步将IO请求发送DMA。然后让CPU干别的活去。
- DMA进一步将IO请求发送给磁盘。
- 磁盘驱动器收到DMA的IO请求,把数据从磁盘读取到驱动器的缓冲中。当驱动器的缓冲区被读满后,向DMA发起中断信号告知自己缓冲区已满。
- DMA收到磁盘驱动器的信号,将磁盘驱动器的缓存中的数据拷贝到内核缓冲区中。此时不占用CPU。这个时候只要内核缓冲区的数据少于用户申请的读的数据,内核就会一直重复步骤3跟步骤4,直到内核缓冲区的数据足够多为止。
- 当DMA读取了足够多的数据,就会发送中断信号给CPU。
- CPU收到DMA的信号,知道数据已经准备好,于是将数据从内核拷贝到用户空间,系统调用返回。
zero copy
- 文件在磁盘中数据被copy到内核缓冲区
- 从内核缓冲区copy到内核与socket相关的缓冲区
- 数据从socket缓冲区copy到相关协议引擎发送出去
Kafka原理
分区
何为分区:就是一个topic的数据,分散存储在多个broker中,这样就可以避免一个topic把broker给占满的情况,同时分区可以提高吞吐量,比如一个客户端的消费速度是50M/S,要达到每秒1G/S的吞吐量,就需要1000/50=20个分区
分区策略
默认采用轮询broker(round-robin)的方式,先分主分区,再分副本分区;分区数=2为例: 第1条去0分区, 第2条去1分区, 第3条去0分区, 仅仅达到负载均衡
hash, 每条record根据key的hash值对分区数取模(去哪个分区= hash(record的key) % 分区数 ), 决定该record存放哪个分区
分段
- 每个分区又分为多个片段,每个片段默认1G或者1周的数据量,每个片段是一个文件
- 正在写入的片段叫活跃片段,永远不会被删除
数据管道Connect
- 适用于以Kafka为中心,把Kafka的数据转存或者其他地方存到Kafka的场景,比如mysql发到Kafka,Kafka发到ES
- 提供仅一次语义
- 支持数据池Data Sink
- Kafa支持数据加密(SASL)
管理Kafka
主题管理
创建分区
kafka-topics.sh --zookeeper zkStr --create --topic test --replication-factor 2 --partitions 8 #如果放置存在报错,可以加--if-not-exists
增加分区
只能增加分区,不能减少分区,因为减少分区会出现消息乱序,如果一定要减少分区,只能删除整个topic
kafka-topics.sh --zookeeper zkStr --alter --topic test --partition 16
删除topic
删除topic必须broker的delete.topic.enable=true,否则删除请求将被忽略
kafka-topics.sh --zookeeper zkStr --delete --topic test
列出所有topic(简要信息)
kafka-topics.sh --zookeeper zkStr --list
列出所有topic(详细信息)
kafka-topics.sh --zookeeper zkStr --describe
--topics-with-overrides #只列出有不同于默认配置的topic
--under-replicated-partitions #列出包含不同步副本分区
--unavailable-partitions #列出没有leader的分区
消费者群组
在旧版本中,消费者群组信息保存在zk上面,用–zookeeper指定;
在新版本中,消费者群组信息保存在broker上面,用–bootstrap-server指定;
在旧版本可以删除消费者群组和偏移量信息,新的不行。
列出所有消费者群组
kafka-consumer-groups.sh --zookeeper zkStr --list
列出某个消费者群组详细信息
kafka-consumer-groups.sh --zookeeper zkStr --describe --group testGroup
字段 | 描述 |
---|---|
group | 分组名字 |
topic | 分区名字 |
partition | 分区名字 |
current-offset | 这个群组读取的位置 |
log-end-offset | 高水位位置 |
log | current-offset和log-end-offset的差距 |
owner | 消费者id |
动态配置
覆盖配置
kafka-configs.sh --zookeeper zkStr --alter --entity-type [topics | clients] --entity-name xxxx --add-config key=value[,key=value...]
列出被覆盖的配置
kafka-configs.sh --zookeeper zkStr --describe --entity-type [topics | clients] --entity-name xxxx
移除被覆盖的配置
kafka-configs.sh --zookeeper zkStr --alter --entity-type [topics | clients] --entity-name xxxx --delete-config key[,key...]
分区管理
首领选举
修改分区副本
修改复制系数
转储日志片段
副本验证
消费和生产
控制台消费者
kafka-console-consumer.sh --zookeeper zkStr --topic topicName [--from-begin] [--max-messages] [--partition] [--formatter] [--property]
控制台生产者
kafka-console-producer
--broker-list ip:port[,ip:port]
--topic topicName [--key-serializer]
[--value-serializer]
[--compression-codec [none | gzip | snappy | lz4]] #压缩方式
[--sync] #是否同步发送
Kafka Streams流式处理
什么是流式处理
请求与相应:一个请求对应一个响应,响应时长在亚秒和毫秒级
批处理:处理周期为分钟、小时、天、周、月、年等等
流式处理:流式处理范式介于上面两种范式中间,不要求在亚秒级响应,但也不能容忍第二天才返回结果。流式处理是持续的,输入的数据一直持续进行,返回的结果也是持续进行。有持续性和非阻塞特性。
流式处理概念
时间
事件时间:表示追踪时间的发生时间或者创建时间
日志追加时间:事件保存到broker的时间,这个时间一般和流式计算没有关系,除非事件没有记录事件时间,这时可以使用日志追加时间模糊代表事件时间
处理时间:指应用程序在收到事件之后开始对其进行处理的时间。因为从产生事件到处理事件中间时间不确定,所以这个时间更不可靠,尽量避免使用它
状态
本地状态:只有当前应用程序能访问,一般使用内嵌的数据库保存,优点访问速度快,不受内存大小限制
外部状态:使用外部的数据存储系统,一般是NoSQL,优势是没有容量限制,但是延迟高。大部分流式处理应该避免使用外部存储
流和表的二元性
表只关注于数据的当前状态,而流表示了数据的整个变化过程,比如,mysql的binlog就是一个事件流。大部分的数据库都提供了CDC方案(Change Data Capture)
时间窗口
窗口的大小:是统计5分钟还是10分钟内的数据
窗口的移动间隔:统计5分钟的数据,可以每1秒钟统计一次,也可以每1分钟统计一次。如果窗口大小和移动间隔相等叫做“滚动窗口”,如果窗口随每一条数据移动,这种叫做“滑动窗口”
窗口的可更新时长:有些数据可能由于网络或者服务器重启导致数据姗姗来迟,这些数据在允许的时间范围可以更新统计结果,否则忽略他们
Kafka Streams在流式计算领域应用的很少,主要在于只能处理Kafka的数据,对于其他数据源和Sink的数据只能干瞪眼。而且部署上也没有优势。