This commit is contained in:
louzefeng
2024-07-09 18:38:56 +00:00
parent 8bafaef34d
commit bf99793fd0
6071 changed files with 1017944 additions and 0 deletions

View File

@@ -0,0 +1,191 @@
<audio id="audio" title="28 | 主题管理知多少?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/ac/e6/ac2a265909e744e323cab53eca58eee6.mp3"></audio>
你好我是胡夕。今天我想和你讨论一下Kafka中的主题管理包括日常的主题管理、特殊主题的管理与运维以及常见的主题错误处理。
## 主题日常管理
所谓的日常管理无非就是主题的增删改查。你可能会觉得这有什么好讨论的官网上不都有命令吗这部分内容的确比较简单但它是我们讨论后面内容的基础。而且在讨论的过程中我还会向你分享一些小技巧。另外我们今天讨论的管理手段都是借助于Kafka自带的命令。事实上在专栏后面我们还会专门讨论如何使用Java API的方式来运维Kafka集群。
我们先来学习一下如何使用命令创建Kafka主题。**Kafka提供了自带的kafka-topics脚本用于帮助用户创建主题**。该脚本文件位于Kafka安装目录的bin子目录下。如果你是在Windows上使用Kafka那么该脚本位于bin路径的windows子目录下。一个典型的创建命令如下
```
bin/kafka-topics.sh --bootstrap-server broker_host:port --create --topic my_topic_name --partitions 1 --replication-factor 1
```
create表明我们要创建主题而partitions和replication factor分别设置了主题的分区数以及每个分区下的副本数。如果你之前使用过这个命令你可能会感到奇怪难道不是指定 --zookeeper参数吗为什么现在变成 --bootstrap-server了呢我来给出答案从Kafka 2.2版本开始,社区推荐用 --bootstrap-server参数替换 --zookeeper参数并且显式地将后者标记为“已过期”因此如果你已经在使用2.2版本了,那么创建主题请指定 --bootstrap-server参数。
社区推荐使用 --bootstrap-server而非 --zookeeper的原因主要有两个。
1. 使用 --zookeeper会绕过Kafka的安全体系。这就是说即使你为Kafka集群设置了安全认证限制了主题的创建如果你使用 --zookeeper的命令依然能成功创建任意主题不受认证体系的约束。这显然是Kafka集群的运维人员不希望看到的。
1. 使用 --bootstrap-server与集群进行交互越来越成为使用Kafka的标准姿势。换句话说以后会有越来越少的命令和API需要与ZooKeeper进行连接。这样我们只需要一套连接信息就能与Kafka进行全方位的交互不用像以前一样必须同时维护ZooKeeper和Broker的连接信息。
创建好主题之后Kafka允许我们使用相同的脚本查询主题。你可以使用下面的命令查询所有主题的列表。
```
bin/kafka-topics.sh --bootstrap-server broker_host:port --list
```
如果要查询单个主题的详细数据,你可以使用下面的命令。
```
bin/kafka-topics.sh --bootstrap-server broker_host:port --describe --topic &lt;topic_name&gt;
```
如果describe命令不指定具体的主题名称那么Kafka默认会返回所有“可见”主题的详细数据给你。
**这里的“可见”是指发起这个命令的用户能够看到的Kafka主题**。这和前面说到主题创建时,使用 --zookeeper和 --bootstrap-server的区别是一样的。如果指定了 --bootstrap-server那么这条命令就会受到安全认证体系的约束即对命令发起者进行权限验证然后返回它能看到的主题。否则如果指定 --zookeeper参数那么默认会返回集群中所有的主题详细数据。基于这些原因我建议你最好统一使用 --bootstrap-server连接参数。
说完了主题的“增”和“查”我们说说如何“改”。Kafka中涉及到主题变更的地方有5处。
**1.修改主题分区。**
其实就是增加分区目前Kafka不允许减少某个主题的分区数。你可以使用kafka-topics脚本结合 --alter参数来增加某个主题的分区数命令如下
```
bin/kafka-topics.sh --bootstrap-server broker_host:port --alter --topic &lt;topic_name&gt; --partitions &lt;新分区数&gt;
```
这里要注意的是你指定的分区数一定要比原有分区数大否则Kafka会抛出InvalidPartitionsException异常。
**2.修改主题级别参数**
在主题创建之后我们可以使用kafka-configs脚本修改对应的参数。
这个用法我们在专栏[第8讲](https://time.geekbang.org/column/article/101763)中讨论过现在先来复习一下。假设我们要设置主题级别参数max.message.bytes那么命令如下
```
bin/kafka-configs.sh --zookeeper zookeeper_host:port --entity-type topics --entity-name &lt;topic_name&gt; --alter --add-config max.message.bytes=10485760
```
也许你会觉得奇怪,为什么这个脚本就要指定 --zookeeper而不是 --bootstrap-server呢其实这个脚本也能指定 --bootstrap-server参数只是它是用来设置动态参数的。在专栏后面我会详细介绍什么是动态参数以及动态参数都有哪些。现在你只需要了解设置常规的主题级别参数还是使用 --zookeeper。
**3.变更副本数。**
使用自带的kafka-reassign-partitions脚本帮助我们增加主题的副本数。这里先留个悬念稍后我会拿Kafka内部主题__consumer_offsets来演示如何增加主题副本数。
**4.修改主题限速。**
这里主要是指设置Leader副本和Follower副本使用的带宽。有时候我们想要让某个主题的副本在执行副本同步机制时不要消耗过多的带宽。Kafka提供了这样的功能。我来举个例子。假设我有个主题名为test我想让该主题各个分区的Leader副本和Follower副本在处理副本同步时不得占用超过100MBps的带宽。注意是大写B即每秒不超过100MB。那么我们应该怎么设置呢
要达到这个目的我们必须先设置Broker端参数leader.replication.throttled.rate和follower.replication.throttled.rate命令如下
```
bin/kafka-configs.sh --zookeeper zookeeper_host:port --alter --add-config 'leader.replication.throttled.rate=104857600,follower.replication.throttled.rate=104857600' --entity-type brokers --entity-name 0
```
这条命令结尾处的 --entity-name就是Broker ID。倘若该主题的副本分别在0、1、2、3多个Broker上那么你还要依次为Broker 1、2、3执行这条命令。
设置好这个参数之后,我们还需要为该主题设置要限速的副本。在这个例子中,我们想要为所有副本都设置限速,因此统一使用通配符*来表示,命令如下:
```
bin/kafka-configs.sh --zookeeper zookeeper_host:port --alter --add-config 'leader.replication.throttled.replicas=*,follower.replication.throttled.replicas=*' --entity-type topics --entity-name test
```
**5.主题分区迁移。**
同样是使用kafka-reassign-partitions脚本对主题各个分区的副本进行“手术”般的调整比如把某些分区批量迁移到其他Broker上。这种变更比较复杂我会在专栏后面专门和你分享如何做主题的分区迁移。
最后,我们来聊聊如何删除主题。命令很简单,我直接分享给你。
```
bin/kafka-topics.sh --bootstrap-server broker_host:port --delete --topic &lt;topic_name&gt;
```
删除主题的命令并不复杂关键是删除操作是异步的执行完这条命令不代表主题立即就被删除了。它仅仅是被标记成“已删除”状态而已。Kafka会在后台默默地开启主题删除操作。因此通常情况下你都需要耐心地等待一段时间。
## 特殊主题的管理与运维
说完了日常的主题管理操作我们来聊聊Kafka内部主题__consumer_offsets和__transaction_state。前者你可能已经很熟悉了后者是Kafka支持事务新引入的。如果在你的生产环境中你看到很多带有__consumer_offsets和__transaction_state前缀的子目录不用惊慌这是正常的。这两个内部主题默认都有50个分区因此分区子目录会非常得多。
关于这两个内部主题我的建议是不要手动创建或修改它们还是让Kafka自动帮我们创建好了。不过这里有个比较隐晦的问题那就是__consumer_offsets的副本数问题。
在Kafka 0.11之前当Kafka自动创建该主题时它会综合考虑当前运行的Broker台数和Broker端参数offsets.topic.replication.factor值然后取两者的较小值作为该主题的副本数但这就违背了用户设置offsets.topic.replication.factor的初衷。这正是很多用户感到困扰的地方我的集群中有100台Brokeroffsets.topic.replication.factor也设成了3为什么我的__consumer_offsets主题只有1个副本其实这就是因为这个主题是在只有一台Broker启动时被创建的。
在0.11版本之后社区修正了这个问题。也就是说0.11之后Kafka会严格遵守offsets.topic.replication.factor值。如果当前运行的Broker数量小于offsets.topic.replication.factor值Kafka会创建主题失败并显式抛出异常。
那么如果该主题的副本值已经是1了我们能否把它增加到3呢当然可以。我们来看一下具体的方法。
第1步是创建一个json文件显式提供50个分区对应的副本数。注意replicas中的3台Broker排列顺序不同目的是将Leader副本均匀地分散在Broker上。该文件具体格式如下
```
{&quot;version&quot;:1, &quot;partitions&quot;:[
{&quot;topic&quot;:&quot;__consumer_offsets&quot;,&quot;partition&quot;:0,&quot;replicas&quot;:[0,1,2]},
{&quot;topic&quot;:&quot;__consumer_offsets&quot;,&quot;partition&quot;:1,&quot;replicas&quot;:[0,2,1]},
{&quot;topic&quot;:&quot;__consumer_offsets&quot;,&quot;partition&quot;:2,&quot;replicas&quot;:[1,0,2]},
{&quot;topic&quot;:&quot;__consumer_offsets&quot;,&quot;partition&quot;:3,&quot;replicas&quot;:[1,2,0]},
...
{&quot;topic&quot;:&quot;__consumer_offsets&quot;,&quot;partition&quot;:49,&quot;replicas&quot;:[0,1,2]}
]}`
```
第2步是执行kafka-reassign-partitions脚本命令如下
```
bin/kafka-reassign-partitions.sh --zookeeper zookeeper_host:port --reassignment-json-file reassign.json --execute
```
除了修改内部主题我们可能还想查看这些内部主题的消息内容。特别是对于__consumer_offsets而言由于它保存了消费者组的位移数据有时候直接查看该主题消息是很方便的事情。下面的命令可以帮助我们直接查看消费者组提交的位移数据。
```
bin/kafka-console-consumer.sh --bootstrap-server kafka_host:port --topic __consumer_offsets --formatter &quot;kafka.coordinator.group.GroupMetadataManager\$OffsetsMessageFormatter&quot; --from-beginning
```
除了查看位移提交数据,我们还可以直接读取该主题消息,查看消费者组的状态信息。
```
bin/kafka-console-consumer.sh --bootstrap-server kafka_host:port --topic __consumer_offsets --formatter &quot;kafka.coordinator.group.GroupMetadataManager\$GroupMetadataMessageFormatter&quot; --from-beginning
```
对于内部主题__transaction_state而言方法是相同的。你只需要指定kafka.coordinator.transaction.TransactionLog\$TransactionLogMessageFormatter即可。
## 常见主题错误处理
最后,我们来说说与主题相关的常见错误,以及相应的处理方法。
**常见错误1主题删除失败。**
当运行完上面的删除命令后,很多人发现已删除主题的分区数据依然“躺在”硬盘上,没有被清除。这时该怎么办呢?
**实际上造成主题删除失败的原因有很多最常见的原因有两个副本所在的Broker宕机了待删除主题的部分分区依然在执行迁移过程。**
如果是因为前者通常你重启对应的Broker之后删除操作就能自动恢复如果是因为后者那就麻烦了很可能两个操作会相互干扰。
不管什么原因,一旦你碰到主题无法删除的问题,可以采用这样的方法:
第1步手动删除ZooKeeper节点/admin/delete_topics下以待删除主题为名的znode。
第2步手动删除该主题在磁盘上的分区目录。
第3步在ZooKeeper中执行rmr /controller触发Controller重选举刷新Controller缓存。
在执行最后一步时你一定要谨慎因为它可能造成大面积的分区Leader重选举。事实上仅仅执行前两步也是可以的只是Controller缓存中没有清空待删除主题罢了也不影响使用。
**常见错误2__consumer_offsets占用太多的磁盘。**
一旦你发现这个主题消耗了过多的磁盘空间,那么,你一定要显式地用**jstack命令**查看一下kafka-log-cleaner-thread前缀的线程状态。通常情况下这都是因为该线程挂掉了无法及时清理此内部主题。倘若真是这个原因导致的那我们就只能重启相应的Broker了。另外请你注意保留出错日志因为这通常都是Bug导致的最好提交到社区看一下。
## 小结
我们来小结一下。今天我们着重讨论了Kafka的主题管理包括日常的运维操作以及如何对Kafka内部主题进行相应的管理。最后我给出了两个最常见问题的解决思路。这里面涉及到了大量的命令希望你能够在自己的环境中对照着实现一遍。另外我也鼓励你去学习这些命令的其他用法这会极大地丰富你的Kafka工具库。
<img src="https://static001.geekbang.org/resource/image/ca/54/ca8dff8284b9b17c985fbd74bc3d6454.jpg" alt="">
## 开放讨论
请思考一下为什么Kafka不允许减少分区数如果减少分区数可能会有什么样的问题
欢迎写下你的思考和答案,我们一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@@ -0,0 +1,176 @@
<audio id="audio" title="29 | Kafka动态配置了解下" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/73/51/73ef41517a156952de4049d0ff088d51.mp3"></audio>
你好我是胡夕。今天我要和你讨论的主题是Kafka的动态Broker参数配置。
## 什么是动态Broker参数配置
在开始今天的分享之前我们先来复习一下设置Kafka参数特别是Broker端参数的方法。
在Kafka安装目录的config路径下有个server.properties文件。通常情况下我们会指定这个文件的路径来启动Broker。如果要设置Broker端的任何参数我们必须在这个文件中显式地增加一行对应的配置之后启动Broker进程令参数生效。我们常见的做法是一次性设置好所有参数之后再启动Broker。当后面需要变更任何参数时我们必须重启Broker。但生产环境中的服务器怎么能随意重启呢所以目前修改Broker端参数是非常痛苦的过程。
基于这个痛点社区于1.1.0版本中正式引入了动态Broker参数Dynamic Broker Configs。所谓动态就是指修改参数值后无需重启Broker就能立即生效而之前在server.properties中配置的参数则称为静态参数Static Configs。显然动态调整参数值而无需重启服务是非常实用的功能。如果你想体验动态Broker参数的话那就赶快升级到1.1版本吧。
当然了当前最新的2.3版本中的Broker端参数有200多个社区并没有将每个参数都升级成动态参数它仅仅是把一部分参数变成了可动态调整。那么我们应该如何分辨哪些参数是动态参数呢
如果你打开1.1版本之后含1.1的Kafka官网你会发现[Broker Configs](https://kafka.apache.org/documentation/#brokerconfigs)表中增加了Dynamic Update Mode列。该列有3类值分别是read-only、per-broker和cluster-wide。我来解释一下它们的含义。
- read-only。被标记为read-only的参数和原来的参数行为一样只有重启Broker才能令修改生效。
- per-broker。被标记为per-broker的参数属于动态参数修改它之后只会在对应的Broker上生效。
- cluster-wide。被标记为cluster-wide的参数也属于动态参数修改它之后会在整个集群范围内生效也就是说对所有Broker都生效。你也可以为具体的Broker修改cluster-wide参数。
我来举个例子说明一下per-broker和cluster-wide的区别。Broker端参数listeners想必你应该不陌生吧。它是一个per-broker参数这表示你只能为单个Broker动态调整listeners而不能直接调整一批Broker的listeners。log.retention.ms参数是cluster-wide级别的Kafka允许为集群内所有Broker统一设置一个日志留存时间值。当然了你也可以为单个Broker修改此值。
## 使用场景
你可能会问动态Broker参数的使用场景都有哪些呢实际上因为不必重启Broker动态Broker参数的使用场景非常广泛通常包括但不限于以下几种
- 动态调整Broker端各种线程池大小实时应对突发流量。
- 动态调整Broker端连接信息或安全配置信息。
- 动态更新SSL Keystore有效期。
- 动态调整Broker端Compact操作性能。
- 实时变更JMX指标收集器(JMX Metrics Reporter)。
在这些使用场景中动态调整线程池大小应该算是最实用的功能了。很多时候当Kafka Broker入站流量inbound data激增时会造成Broker端请求积压Backlog。有了动态参数我们就能够动态增加网络线程数和I/O线程数快速消耗一些积压。当突发流量过去后我们也能将线程数调整回来减少对资源的浪费。整个过程都不需要重启Broker。你甚至可以将这套调整线程数的动作封装进定时任务中以实现自动扩缩容。
## 如何保存?
由于动态配置的特殊性它必然有和普通只读参数不同的保存机制。下面我来介绍一下Kafka是如何保存动态配置的。
首先Kafka将动态Broker参数保存在ZooKeeper中具体的znode路径如下图所示。
<img src="https://static001.geekbang.org/resource/image/67/59/67125a22a27028e18bab9f03a6664859.png" alt="">
我来解释一下图中的内容。changes是用来实时监测动态参数变更的不会保存参数值topics是用来保存Kafka主题级别参数的。虽然它们不属于动态Broker端参数但其实它们也是能够动态变更的。
users和clients则是用于动态调整客户端配额Quota的znode节点。所谓配额是指Kafka运维人员限制连入集群的客户端的吞吐量或者是限定它们使用的CPU资源。
分析到这里,我们就会发现,/config/brokers znode才是真正保存动态Broker参数的地方。该znode下有两大类子节点。第一类子节点就只有一个它有个固定的名字叫&lt; default &gt;保存的是前面说过的cluster-wide范围的动态参数另一类则以broker.id为名保存的是特定Broker的per-broker范围参数。由于是per-broker范围因此这类子节点可能存在多个。
我们一起来看一张图片它展示的是我的一个Kafka集群环境上的动态Broker端参数。
<img src="https://static001.geekbang.org/resource/image/c4/7c/c4154989775023afb619f2fdb07f6f7c.png" alt="">
在这张图中,我首先查看了/config/brokers下的子节点我们可以看到这里面有&lt; default &gt;节点和名为0、1的子节点。&lt; default &gt;节点中保存了我设置的cluster-wide范围参数0和1节点中分别保存了我为Broker 0和Broker 1设置的per-broker参数。
接下来我分别展示了cluster-wide范围和per-broker范围的参数设置。拿num.io.threads参数为例其cluster-wide值被动态调整为12而在Broker 0上被设置成16在Broker 1上被设置成8。我为Broker 0和Broker 1单独设置的值会覆盖掉cluster-wide值但在其他Broker上该参数默认值还是按12计算。
如果我们再把静态参数加进来一起讨论的话cluster-wide、per-broker和static参数的优先级是这样的per-broker参数 &gt; cluster-wide参数 &gt; static参数 &gt; Kafka默认值。
另外,如果你仔细查看上图中的**ephemeralOwner字段**你会发现它们的值都是0x0。这表示这些znode都是持久化节点它们将一直存在。即使ZooKeeper集群重启这些数据也不会丢失这样就能保证这些动态参数的值会一直生效。
## 如何配置?
讲完了保存原理我们来说说如何配置动态Broker参数。目前设置动态参数的工具行命令只有一个那就是Kafka自带的kafka-configs脚本。接下来我来以unclean.leader.election.enable参数为例演示一下如何动态调整。
下面这条命令展示了如何在集群层面设置全局值即设置cluster-wide范围值。
```
$ bin/kafka-configs.sh --bootstrap-server kafka-host:port --entity-type brokers --entity-default --alter --add-config unclean.leader.election.enable=true
Completed updating default config for brokers in the cluster,
```
总体来说命令很简单,但有一点需要注意。**如果要设置cluster-wide范围的动态参数需要显式指定entity-default**。现在,我们使用下面的命令来查看一下刚才的配置是否成功。
```
$ bin/kafka-configs.sh --bootstrap-server kafka-host:port --entity-type brokers --entity-default --describe
Default config for brokers in the cluster are:
unclean.leader.election.enable=true sensitive=false synonyms={DYNAMIC_DEFAULT_BROKER_CONFIG:unclean.leader.election.enable=true}
```
从输出来看我们成功地在全局层面上设置该参数值为true。注意sensitive=false的字眼它表明我们要调整的参数不是敏感数据。如果我们调整的是类似于密码这样的参数时该字段就会为true表示这属于敏感数据。
好了调整完cluster-wide范围的参数我来演示下如何设置per-broker范围参数。我们还是以unclean.leader.election.enable参数为例我现在为ID为1的Broker设置一个不同的值。命令如下
```
$ bin/kafka-configs.sh --bootstrap-server kafka-host:port --entity-type brokers --entity-name 1 --alter --add-config unclean.leader.election.enable=false
Completed updating config for broker: 1.
```
同样,我们使用下列命令,来查看一下刚刚的设置是否生效了。
```
$ bin/kafka-configs.sh --bootstrap-server kafka-host:port --entity-type brokers --entity-name 1 --describe
Configs for broker 1 are:
unclean.leader.election.enable=false sensitive=false synonyms={DYNAMIC_BROKER_CONFIG:unclean.leader.election.enable=false, DYNAMIC_DEFAULT_BROKER_CONFIG:unclean.leader.election.enable=true, DEFAULT_CONFIG:unclean.leader.election.enable=false}
```
这条命令的输出信息很多。我们关注两点即可。
1. 在Broker 1层面上该参数被设置成了false这表明命令运行成功了。
1. 从倒数第二行可以看出在全局层面上该参数值依然是true。这表明我们之前设置的cluster-wide范围参数值依然有效。
如果我们要删除cluster-wide范围参数或per-broker范围参数也非常简单分别执行下面的命令就可以了。
```
# 删除cluster-wide范围参数
$ bin/kafka-configs.sh --bootstrap-server kafka-host:port --entity-type brokers --entity-default --alter --delete-config unclean.leader.election.enable
Completed updating default config for brokers in the cluster,
```
```
# 删除per-broker范围参数
$ bin/kafka-configs.sh --bootstrap-server kafka-host:port --entity-type brokers --entity-name 1 --alter --delete-config unclean.leader.election.enable
Completed updating config for broker: 1.
```
**删除动态参数要指定delete-config**。当我们删除完动态参数配置后,再次运行查看命令,结果如下:
```
# 查看cluster-wide范围参数
$ bin/kafka-configs.sh --bootstrap-server kafka-host:port --entity-type brokers --entity-default --describe
Default config for brokers in the cluster are:
```
```
# 查看Broker 1上的动态参数配置
$ bin/kafka-configs.sh --bootstrap-server kafka-host:port --entity-type brokers --entity-name 1 --describe
Configs for broker 1 are:
```
此时,刚才配置的所有动态参数都已经被成功移除了。
刚刚我只是举了一个参数的例子如果你想要知道动态Broker参数都有哪些一种方式是在Kafka官网中查看Broker端参数列表另一种方式是直接运行无参数的kafka-configs脚本该脚本的说明文档会告诉你当前动态Broker参数都有哪些。我们可以先来看看下面这两张图。
<img src="https://static001.geekbang.org/resource/image/40/f1/400532989c3214f00a2f764fe7983cf1.png" alt="">
<img src="https://static001.geekbang.org/resource/image/eb/67/eb99853bca3d91ad3a53db5dbe3e3b67.png" alt="">
看到有这么多动态Broker参数你可能会问这些我都需要调整吗你能告诉我最常用的几个吗根据我的实际使用经验我来跟你分享一些有较大几率被动态调整值的参数。
**1.log.retention.ms。**
修改日志留存时间应该算是一个比较高频的操作,毕竟,我们不可能完美地预估所有业务的消息留存时长。虽然该参数有对应的主题级别参数可以设置,但拥有在全局层面上动态变更的能力,依然是一个很好的功能亮点。
**2.num.io.threads和num.network.threads。**
这是我们在前面提到的两组线程池。就我个人而言我觉得这是动态Broker参数最实用的场景了。毕竟在实际生产环境中Broker端请求处理能力经常要按需扩容。如果没有动态Broker参数我们是无法做到这一点的。
**3.与SSL相关的参数。**
主要是4个参数ssl.keystore.type、ssl.keystore.location、ssl.keystore.password和ssl.key.password。允许动态实时调整它们之后我们就能创建那些过期时间很短的SSL证书。每当我们调整时Kafka底层会重新配置Socket连接通道并更新Keystore。新的连接会使用新的Keystore阶段性地调整这组参数有利于增加安全性。
**4.num.replica.fetchers。**
这也是我认为的最实用的动态Broker参数之一。Follower副本拉取速度慢在线上Kafka环境中一直是一个老大难的问题。针对这个问题常见的做法是增加该参数值确保有充足的线程可以执行Follower副本向Leader副本的拉取。现在有了动态参数你不需要再重启Broker就能立即在Follower端生效因此我说这是很实用的应用场景。
## 小结
好了我们来小结一下。今天我们重点讨论了Kafka 1.1.0版本引入的动态Broker参数。这类参数最大的好处在于无需重启Broker就可以令变更生效因此能够极大地降低运维成本。除此之外我还给出了动态参数的保存机制和设置方法。在专栏的后面我还会给出动态参数设置的另一种方法敬请期待。
<img src="https://static001.geekbang.org/resource/image/22/74/22ef35547f9bddb8d56bdb350fd52f74.jpg" alt="">
## 开放讨论
目前社区只是将一部分Broker参数升级为动态参数在实际使用过程中你觉得还有哪些参数也应该变为可动态修改
欢迎写下你的思考和答案,我们一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@@ -0,0 +1,245 @@
<audio id="audio" title="30 | 怎么重设消费者组位移?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/3e/80/3e8e22ac6fc3f1038bbb44199f7f6d80.mp3"></audio>
你好,我是胡夕。今天我要跟你分享的主题是:如何重设消费者组位移。
## 为什么要重设消费者组位移?
我们知道Kafka和传统的消息引擎在设计上是有很大区别的其中一个比较显著的区别就是Kafka的消费者读取消息是可以重演的replayable
像RabbitMQ或ActiveMQ这样的传统消息中间件它们处理和响应消息的方式是破坏性的destructive即一旦消息被成功处理就会被从Broker上删除。
反观Kafka由于它是基于日志结构log-based的消息引擎消费者在消费消息时仅仅是从磁盘文件上读取数据而已是只读的操作因此消费者不会删除消息数据。同时由于位移数据是由消费者控制的因此它能够很容易地修改位移的值实现重复消费历史数据的功能。
对了之前有很多同学在专栏的留言区提问在实际使用场景中我该如何确定是使用传统的消息中间件还是使用Kafka呢我在这里统一回答一下。如果在你的场景中消息处理逻辑非常复杂处理代价很高同时你又不关心消息之间的顺序那么传统的消息中间件是比较合适的反之如果你的场景需要较高的吞吐量但每条消息的处理时间很短同时你又很在意消息的顺序此时Kafka就是你的首选。
## 重设位移策略
不论是哪种设置方式,重设位移大致可以从两个维度来进行。
1. 位移维度。这是指根据位移值来重设。也就是说,直接把消费者的位移值重设成我们给定的位移值。
1. 时间维度。我们可以给定一个时间让消费者把位移调整成大于该时间的最小位移也可以给出一段时间间隔比如30分钟前然后让消费者直接将位移调回30分钟之前的位移值。
下面的这张表格罗列了7种重设策略。接下来我来详细解释下这些策略。
<img src="https://static001.geekbang.org/resource/image/ac/6a/ac093597e8dbef3b1f832f24c125fc6a.jpg" alt="">
Earliest策略表示将位移调整到主题当前最早位移处。这个最早位移不一定就是0因为在生产环境中很久远的消息会被Kafka自动删除所以当前最早位移很可能是一个大于0的值。**如果你想要重新消费主题的所有消息那么可以使用Earliest策略**。
Latest策略表示把位移重设成最新末端位移。如果你总共向某个主题发送了15条消息那么最新末端位移就是15。**如果你想跳过所有历史消息打算从最新的消息处开始消费的话可以使用Latest策略。**
Current策略表示将位移调整成消费者当前提交的最新位移。有时候你可能会碰到这样的场景你修改了消费者程序代码并重启了消费者结果发现代码有问题你需要回滚之前的代码变更同时也要把位移重设到消费者重启时的位置那么Current策略就可以帮你实现这个功能。
表中第4行的Specified-Offset策略则是比较通用的策略表示消费者把位移值调整到你指定的位移处。**这个策略的典型使用场景是,消费者程序在处理某条错误消息时,你可以手动地“跳过”此消息的处理**。在实际使用过程中可能会出现corrupted消息无法被消费的情形此时消费者程序会抛出异常无法继续工作。一旦碰到这个问题你就可以尝试使用Specified-Offset策略来规避。
如果说Specified-Offset策略要求你指定位移的**绝对数值**的话那么Shift-By-N策略指定的就是位移的**相对数值**即你给出要跳过的一段消息的距离即可。这里的“跳”是双向的你既可以向前“跳”也可以向后“跳”。比如你想把位移重设成当前位移的前100条位移处此时你需要指定N为-100。
刚刚讲到的这几种策略都是位移维度的下面我们来聊聊从时间维度重设位移的DateTime和Duration策略。
DateTime允许你指定一个时间然后将位移重置到该时间之后的最早位移处。常见的使用场景是你想重新消费昨天的数据那么你可以使用该策略重设位移到昨天0点。
Duration策略则是指给定相对的时间间隔然后将位移调整到距离当前给定时间间隔的位移处具体格式是PnDTnHnMnS。如果你熟悉Java 8引入的Duration类的话你应该不会对这个格式感到陌生。它就是一个符合ISO-8601规范的Duration格式以字母P开头后面由4部分组成即D、H、M和S分别表示天、小时、分钟和秒。举个例子如果你想将位移调回到15分钟前那么你就可以指定PT0H15M0S。
我会在后面分别给出这7种重设策略的实现方式。不过在此之前我先来说一下重设位移的方法。目前重设消费者组位移的方式有两种。
- 通过消费者API来实现。
- 通过kafka-consumer-groups命令行脚本来实现。
## 消费者API方式设置
首先我们来看看如何通过API的方式来重设位移。我主要以Java API为例进行演示。如果你使用的是其他语言方法应该是类似的不过你要参考具体的API文档。
通过Java API的方式来重设位移你需要调用KafkaConsumer的seek方法或者是它的变种方法seekToBeginning和seekToEnd。我们来看下它们的方法签名。
```
void seek(TopicPartition partition, long offset);
void seek(TopicPartition partition, OffsetAndMetadata offsetAndMetadata);
void seekToBeginning(Collection&lt;TopicPartition&gt; partitions);
void seekToEnd(Collection&lt;TopicPartition&gt; partitions);
```
根据方法的定义我们可以知道每次调用seek方法只能重设一个分区的位移。OffsetAndMetadata类是一个封装了Long型的位移和自定义元数据的复合类只是一般情况下自定义元数据为空因此你基本上可以认为这个类表征的主要是消息的位移值。seek的变种方法seekToBeginning和seekToEnd则拥有一次重设多个分区的能力。我们在调用它们时可以一次性传入多个主题分区。
好了有了这些方法我们就可以逐一地实现上面提到的7种策略了。我们先来看Earliest策略的实现方式代码如下
```
Properties consumerProperties = new Properties();
consumerProperties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
consumerProperties.put(ConsumerConfig.GROUP_ID_CONFIG, groupID);
consumerProperties.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, &quot;earliest&quot;);
consumerProperties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
consumerProperties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
consumerProperties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, brokerList);
String topic = &quot;test&quot;; // 要重设位移的Kafka主题
try (final KafkaConsumer&lt;String, String&gt; consumer =
new KafkaConsumer&lt;&gt;(consumerProperties)) {
consumer.subscribe(Collections.singleton(topic));
consumer.poll(0);
consumer.seekToBeginning(
consumer.partitionsFor(topic).stream().map(partitionInfo -&gt;
new TopicPartition(topic, partitionInfo.partition()))
.collect(Collectors.toList()));
}
```
这段代码中有几个比较关键的部分,你需要注意一下。
1. 你要创建的消费者程序,要禁止自动提交位移。
1. 组ID要设置成你要重设的消费者组的组ID。
1. 调用seekToBeginning方法时需要一次性构造主题的所有分区对象。
1. 最重要的是一定要调用带长整型的poll方法而不要调用consumer.poll(Duration.ofSecond(0))。
虽然社区已经不推荐使用poll(long)了,但短期内应该不会移除它,所以你可以放心使用。另外,为了避免重复,在后面的实例中,我只给出最关键的代码。
Latest策略和Earliest是类似的我们只需要使用seekToEnd方法即可如下面的代码所示
```
consumer.seekToEnd(
consumer.partitionsFor(topic).stream().map(partitionInfo -&gt;
new TopicPartition(topic, partitionInfo.partition()))
.collect(Collectors.toList()));
```
实现Current策略的方法很简单我们需要借助KafkaConsumer的committed方法来获取当前提交的最新位移代码如下
```
consumer.partitionsFor(topic).stream().map(info -&gt;
new TopicPartition(topic, info.partition()))
.forEach(tp -&gt; {
long committedOffset = consumer.committed(tp).offset();
consumer.seek(tp, committedOffset);
});
```
这段代码首先调用partitionsFor方法获取给定主题的所有分区然后依次获取对应分区上的已提交位移最后通过seek方法重设位移到已提交位移处。
如果要实现Specified-Offset策略直接调用seek方法即可如下所示
```
long targetOffset = 1234L;
for (PartitionInfo info : consumer.partitionsFor(topic)) {
TopicPartition tp = new TopicPartition(topic, info.partition());
consumer.seek(tp, targetOffset);
}
```
这次我没有使用Java 8 Streams的写法如果你不熟悉Lambda表达式以及Java 8的Streams这种写法可能更加符合你的习惯。
接下来我们来实现Shift-By-N策略主体代码逻辑如下
```
for (PartitionInfo info : consumer.partitionsFor(topic)) {
TopicPartition tp = new TopicPartition(topic, info.partition());
// 假设向前跳123条消息
long targetOffset = consumer.committed(tp).offset() + 123L;
consumer.seek(tp, targetOffset);
}
```
如果要实现DateTime策略我们需要借助另一个方法**KafkaConsumer.** **offsetsForTimes方法**。假设我们要重设位移到2019年6月20日晚上8点那么具体代码如下
```
long ts = LocalDateTime.of(
2019, 6, 20, 20, 0).toInstant(ZoneOffset.ofHours(8)).toEpochMilli();
Map&lt;TopicPartition, Long&gt; timeToSearch =
consumer.partitionsFor(topic).stream().map(info -&gt;
new TopicPartition(topic, info.partition()))
.collect(Collectors.toMap(Function.identity(), tp -&gt; ts));
for (Map.Entry&lt;TopicPartition, OffsetAndTimestamp&gt; entry :
consumer.offsetsForTimes(timeToSearch).entrySet()) {
consumer.seek(entry.getKey(), entry.getValue().offset());
}
```
这段代码构造了LocalDateTime实例然后利用它去查找对应的位移值最后调用seek实现了重设位移。
最后我来给出实现Duration策略的代码。假设我们要将位移调回30分钟前那么代码如下
```
Map&lt;TopicPartition, Long&gt; timeToSearch = consumer.partitionsFor(topic).stream()
.map(info -&gt; new TopicPartition(topic, info.partition()))
.collect(Collectors.toMap(Function.identity(), tp -&gt; System.currentTimeMillis() - 30 * 1000 * 60));
for (Map.Entry&lt;TopicPartition, OffsetAndTimestamp&gt; entry :
consumer.offsetsForTimes(timeToSearch).entrySet()) {
consumer.seek(entry.getKey(), entry.getValue().offset());
}
```
**总之使用Java API的方式来实现重设策略的主要入口方法就是seek方法**
## 命令行方式设置
位移重设还有另一个重要的途径:**通过kafka-consumer-groups脚本**。需要注意的是这个功能是在Kafka 0.11版本中新引入的。这就是说如果你使用的Kafka是0.11版本之前的那么你只能使用API的方式来重设位移。
比起API的方式用命令行重设位移要简单得多。针对我们刚刚讲过的7种策略有7个对应的参数。下面我来一一给出实例。
Earliest策略直接指定**--to-earliest**。
```
bin/kafka-consumer-groups.sh --bootstrap-server kafka-host:port --group test-group --reset-offsets --all-topics --to-earliest execute
```
Latest策略直接指定**--to-latest**。
```
bin/kafka-consumer-groups.sh --bootstrap-server kafka-host:port --group test-group --reset-offsets --all-topics --to-latest --execute
```
Current策略直接指定**--to-current**。
```
bin/kafka-consumer-groups.sh --bootstrap-server kafka-host:port --group test-group --reset-offsets --all-topics --to-current --execute
```
Specified-Offset策略直接指定**--to-offset**。
```
bin/kafka-consumer-groups.sh --bootstrap-server kafka-host:port --group test-group --reset-offsets --all-topics --to-offset &lt;offset&gt; --execute
```
Shift-By-N策略直接指定**--shift-by N**。
```
bin/kafka-consumer-groups.sh --bootstrap-server kafka-host:port --group test-group --reset-offsets --shift-by &lt;offset_N&gt; --execute
```
DateTime策略直接指定**--to-datetime**。
```
bin/kafka-consumer-groups.sh --bootstrap-server kafka-host:port --group test-group --reset-offsets --to-datetime 2019-06-20T20:00:00.000 --execute
```
最后是实现Duration策略我们直接指定**--by-duration**。
```
bin/kafka-consumer-groups.sh --bootstrap-server kafka-host:port --group test-group --reset-offsets --by-duration PT0H30M0S --execute
```
## 小结
至此重设消费者组位移的2种方式我都讲完了。我们来小结一下。今天我们主要讨论了在Kafka中为什么要重设位移以及如何重设消费者组位移。重设位移主要是为了实现消息的重演。目前Kafka支持7种重设策略和2种重设方法。在实际使用过程中我推荐你使用第2种方法即用命令行的方式来重设位移。毕竟执行命令要比写程序容易得多。但是需要注意的是0.11及0.11版本之后的Kafka才提供了用命令行调整位移的方法。如果你使用的是之前的版本那么就只能依靠API的方式了。
<img src="https://static001.geekbang.org/resource/image/98/f3/98a7d3f9b0d3050947772d8cd2c4caf3.jpg" alt="">
## 开放讨论
你在实际使用过程中,是否遇到过要重设位移的场景,你是怎么实现的?
欢迎写下你的思考和答案,我们一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@@ -0,0 +1,229 @@
<audio id="audio" title="31 | 常见工具脚本大汇总" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/e9/e0/e92f0ea729951471f402d2c1a3909ce0.mp3"></audio>
你好我是胡夕。今天我要跟你分享的主题是Kafka常见的脚本汇总。
## 命令行脚本概览
Kafka默认提供了很多个命令行脚本用于实现各种各样的功能和运维管理。今天我以2.2版本为例详细地盘点下这些命令行工具。下图展示了2.2版本提供的所有命令行脚本。
<img src="https://static001.geekbang.org/resource/image/f7/ee/f74fe9a3aef75f8db6533e1011571eee.png" alt="">
从图中我们可以知道2.2版本总共提供了30个SHELL脚本。图中的windows实际上是个子目录里面保存了Windows平台下的BAT批处理文件。其他的.sh文件则是Linux平台下的标准SHELL脚本。
默认情况下,不加任何参数或携带 --help运行SHELL文件会得到该脚本的使用方法说明。下面这张图片展示了kafka-log-dirs脚本的调用方法。
<img src="https://static001.geekbang.org/resource/image/04/cc/04953999a40ea439fce05a57b29714cc.png" alt="">
有了这些基础的了解,我来逐一地说明这些脚本的用途,然后再给你详细地介绍一些常见的脚本。
我们先来说说connect-standalone和connect-distributed两个脚本。这两个脚本是Kafka Connect组件的启动脚本。在专栏[第4讲](https://time.geekbang.org/column/article/100285)谈到Kafka生态时我曾说过社区提供了Kafka Connect组件用于实现Kafka与外部世界系统之间的数据传输。Kafka Connect支持单节点的Standalone模式也支持多节点的Distributed模式。这两个脚本分别是这两种模式下的启动脚本。鉴于Kafka Connect不在我们的讨论范围之内我就不展开讲了。
接下来是kafka-acls脚本。它是用于设置Kafka权限的比如设置哪些用户可以访问Kafka的哪些主题之类的权限。在专栏后面我会专门来讲Kafka安全设置的内容到时候我们再细聊这个脚本。
下面是kafka-broker-api-versions脚本。**这个脚本的主要目的是验证不同Kafka版本之间服务器和客户端的适配性**。我来举个例子下面这两张图分别展示了2.2版本Server端与2.2版本Client端和1.1.1版本Client端的适配性。
<img src="https://static001.geekbang.org/resource/image/59/39/594776ae03ca045b3203f873b75c3139.png" alt="">
<img src="https://static001.geekbang.org/resource/image/a3/84/a3604bb34011c372d7f63912ffdf0584.png" alt="">
我截取了部分输出内容,现在我稍微解释一下这些输出的含义。我们以第一行为例:
>
**Produce(0): 0 to 7 [usable: 7]**
“Produce”表示Produce请求生产者生产消息本质上就是向Broker端发送Produce请求。该请求是Kafka所有请求类型中的第一号请求因此序号是0。后面的“0 to 7”表示Produce请求在Kafka 2.2中总共有8个版本序号分别是0到7。“usable7”表示当前连入这个Broker的客户端API能够使用的版本号是7即最新的版本。
请注意这两张图中红线部分的差异。在第一张图中我们使用2.2版本的脚本连接2.2版本的Brokerusable自然是7表示能使用最新版本。在第二张图中我们使用1.1版本的脚本连接2.2版本的Brokerusable是5这表示1.1版本的客户端API只能发送版本号是5的Produce请求。
如果你想了解你的客户端版本与服务器端版本的兼容性,那么最好使用这个命令来检验一下。值得注意的是,**在0.10.2.0之前Kafka是单向兼容的即高版本的Broker能够处理低版本Client发送的请求反过来则不行。自0.10.2.0版本开始Kafka正式支持双向兼容也就是说低版本的Broker也能处理高版本Client的请求了**。
接下来是kafka-configs脚本。对于这个脚本我想你应该已经很熟悉了我们在讨论参数配置和动态Broker参数时都提到过它的用法这里我就不再赘述了。
下面的两个脚本是重量级的工具行脚本kafka-console-consumer和kafka-console-producer。在某种程度上说它们是最常用的脚本也不为过。这里我们暂时先跳过后面我会重点介绍它们。
关于producer和consumer成组出现的还有另外一组脚本kafka-producer-perf-test和kafka-consumer-perf-test。它们分别是生产者和消费者的性能测试工具非常实用稍后我会重点介绍。
接下来的kafka-consumer-groups命令我在介绍重设消费者组位移时稍有涉及后面我们来聊聊该脚本的其他用法。
kafka-delegation-tokens脚本可能不太为人所知它是管理Delegation Token的。基于Delegation Token的认证是一种轻量级的认证机制补充了现有的SASL认证机制。
kafka-delete-records脚本用于删除Kafka的分区消息。鉴于Kafka本身有自己的自动消息删除策略这个脚本的实际出场率并不高。
kafka-dump-log脚本可谓是非常实用的脚本。它能查看Kafka消息文件的内容包括消息的各种元数据信息甚至是消息体本身。
kafka-log-dirs脚本是比较新的脚本可以帮助查询各个Broker上的各个日志路径的磁盘占用情况。
kafka-mirror-maker脚本是帮助你实现Kafka集群间的消息同步的。在专栏后面我会单独用一讲的内容来讨论它的用法。
kafka-preferred-replica-election脚本是执行Preferred Leader选举的。它可以为指定的主题执行“换Leader”的操作。
kafka-reassign-partitions脚本用于执行分区副本迁移以及副本文件路径迁移。
kafka-topics脚本你应该很熟悉了所有的主题管理操作都是由该脚本来实现的。
kafka-run-class脚本则颇为神秘你可以用这个脚本执行任何带main方法的Kafka类。在Kafka早期的发展阶段很多工具类都没有自己专属的SHELL脚本比如刚才提到的kafka-dump-log你只能通过运行kafka-run-class kafka.tools.DumpLogSegments的方式来间接实现。如果你用文本编辑器打开kafka-dump-log.sh你会发现它实际上调用的就是这条命令。后来社区逐渐为这些重要的工具类都添加了专属的命令行脚本现在kafka-run-class脚本的出场率大大降低了。在实际工作中你几乎遇不上要直接使用这个脚本的场景了。
对于kafka-server-start和kafka-server-stop脚本你应该不会感到陌生它们是用于启动和停止Kafka Broker进程的。
kafka-streams-application-reset脚本用来给Kafka Streams应用程序重设位移以便重新消费数据。如果你没有用到Kafka Streams组件这个脚本对你来说是没有用的。
kafka-verifiable-producer和kafka-verifiable-consumer脚本是用来测试生产者和消费者功能的。它们是很“古老”的脚本了你几乎用不到它们。另外前面提到的Console Producer和Console Consumer完全可以替代它们。
剩下的zookeeper开头的脚本是用来管理和运维ZooKeeper的这里我就不做过多介绍了。
最后说一下trogdor脚本。这是个很神秘的家伙官网上也不曾出现它的名字。据社区内部资料显示它是Kafka的测试框架用于执行各种基准测试和负载测试。一般的Kafka用户应该用不到这个脚本。
好了Kafka自带的所有脚本我全部梳理了一遍。虽然这些描述看起来有点流水账但是有了这些基础的认知我们才能更好地利用这些脚本。下面我就来详细介绍一下重点的脚本操作。
## 重点脚本操作
### 生产消息
生产消息使用kafka-console-producer脚本即可一个典型的命令如下所示
```
$ bin/kafka-console-producer.sh --broker-list kafka-host:port --topic test-topic --request-required-acks -1 --producer-property compression.type=lz4
&gt;
```
在这段命令中我们指定生产者参数acks为-1同时启用了LZ4的压缩算法。这个脚本可以很方便地让我们使用控制台来向Kafka的指定主题发送消息。
### 消费消息
下面再来说说数据消费。如果要快速地消费主题中的数据来验证消息是否存在运行kafka-console-consumer脚本应该算是最便捷的方法了。常用的命令用法如下
```
$ bin/kafka-console-consumer.sh --bootstrap-server kafka-host:port --topic test-topic --group test-group --from-beginning --consumer-property enable.auto.commit=false
```
注意在这段命令中我们指定了group信息。如果没有指定的话每次运行Console Consumer它都会自动生成一个新的消费者组来消费。久而久之你会发现你的集群中有大量的以console-consumer开头的消费者组。通常情况下你最好还是加上group。
另外from-beginning等同于将Consumer端参数auto.offset.reset设置成earliest表明我想从头开始消费主题。如果不指定的话它会默认从最新位移读取消息。如果此时没有任何新消息那么该命令的输出为空你什么都看不到。
最后我在命令中禁掉了自动提交位移。通常情况下让Console Consumer提交位移是没有意义的毕竟我们只是用它做一些简单的测试。
### 测试生产者性能
如果你想要对Kafka做一些简单的性能测试。那么不妨试试下面这一组工具。它们分别用于测试生产者和消费者的性能。
我们先说测试生产者的脚本:**kafka-producer-perf-test**。它的参数有不少,但典型的命令调用方式是这样的。
```
$ bin/kafka-producer-perf-test.sh --topic test-topic --num-records 10000000 --throughput -1 --record-size 1024 --producer-props bootstrap.servers=kafka-host:port acks=-1 linger.ms=2000 compression.type=lz4
2175479 records sent, 435095.8 records/sec (424.90 MB/sec), 131.1 ms avg latency, 681.0 ms max latency.
4190124 records sent, 838024.8 records/sec (818.38 MB/sec), 4.4 ms avg latency, 73.0 ms max latency.
10000000 records sent, 737463.126844 records/sec (720.18 MB/sec), 31.81 ms avg latency, 681.00 ms max latency, 4 ms 50th, 126 ms 95th, 604 ms 99th, 672 ms 99.9th.
```
上述命令向指定主题发送了1千万条消息每条消息大小是1KB。该命令允许你在producer-props后面指定要设置的生产者参数比如本例中的压缩算法、延时时间等。
该命令的输出值得好好说一下。它会打印出测试生产者的吞吐量(MB/s)、消息发送延时以及各种分位数下的延时。一般情况下,消息延时不是一个简单的数字,而是一组分布。或者说,**我们应该关心延时的概率分布情况,仅仅知道一个平均值是没有意义的**。这就是这里计算分位数的原因。通常我们关注到**99th分位**就可以了。比如在上面的输出中99th值是604ms这表明测试生产者生产的消息中有99%消息的延时都在604ms以内。你完全可以把这个数据当作这个生产者对外承诺的SLA。
### 测试消费者性能
测试消费者也是类似的原理,只不过我们使用的是**kafka-consumer-perf-test**脚本,命令如下:
```
$ bin/kafka-consumer-perf-test.sh --broker-list kafka-host:port --messages 10000000 --topic test-topic
start.time, end.time, data.consumed.in.MB, MB.sec, data.consumed.in.nMsg, nMsg.sec, rebalance.time.ms, fetch.time.ms, fetch.MB.sec, fetch.nMsg.sec
2019-06-26 15:24:18:138, 2019-06-26 15:24:23:805, 9765.6202, 1723.2434, 10000000, 1764602.0822, 16, 5651, 1728.1225, 1769598.3012
```
虽然输出格式有所差别但该脚本也会打印出消费者的吞吐量数据。比如本例中的1723MB/s。有点令人遗憾的是它没有计算不同分位数下的分布情况。因此在实际使用过程中这个脚本的使用率要比生产者性能测试脚本的使用率低。
### 查看主题消息总数
很多时候我们都想查看某个主题当前的消息总数。令人惊讶的是Kafka自带的命令竟然没有提供这样的功能我们只能“绕道”获取了。所谓的绕道是指我们必须要调用一个未被记录在官网上的命令。命令如下
```
$ bin/kafka-run-class.sh kafka.tools.GetOffsetShell --broker-list kafka-host:port --time -2 --topic test-topic
test-topic:0:0
test-topic:1:0
$ bin/kafka-run-class.sh kafka.tools.GetOffsetShell --broker-list kafka-host:port --time -1 --topic test-topic
test-topic:0:5500000
test-topic:1:5500000
```
我们要使用Kafka提供的工具类**GetOffsetShell**来计算给定主题特定分区当前的最早位移和最新位移将两者的差值累加起来就能得到该主题当前总的消息数。对于本例来说test-topic总的消息数为5500000 + 5500000等于1100万条。
### 查看消息文件数据
作为Kafka使用者你是不是对Kafka底层文件里面保存的内容很感兴趣? 如果是的话你可以使用kafka-dump-log脚本来查看具体的内容。
```
$ bin/kafka-dump-log.sh --files ../data_dir/kafka_1/test-topic-1/00000000000000000000.log
Dumping ../data_dir/kafka_1/test-topic-1/00000000000000000000.log
Starting offset: 0
baseOffset: 0 lastOffset: 14 count: 15 baseSequence: -1 lastSequence: -1 producerId: -1 producerEpoch: -1 partitionLeaderEpoch: 0 isTransactional: false isControl: false position: 0 CreateTime: 1561597044933 size: 1237 magic: 2 compresscodec: LZ4 crc: 646766737 isvalid: true
baseOffset: 15 lastOffset: 29 count: 15 baseSequence: -1 lastSequence: -1 producerId: -1 producerEpoch: -1 partitionLeaderEpoch: 0 isTransactional: false isControl: false position: 1237 CreateTime: 1561597044934 size: 1237 magic: 2 compresscodec: LZ4 crc: 3751986433 isvalid: true
......
```
如果只是指定 --files那么该命令显示的是消息批次RecordBatch或消息集合MessageSet的元数据信息比如创建时间、使用的压缩算法、CRC校验值等。
**如果我们想深入看一下每条具体的消息,那么就需要显式指定 --deep-iteration参数**,如下所示:
```
$ bin/kafka-dump-log.sh --files ../data_dir/kafka_1/test-topic-1/00000000000000000000.log --deep-iteration
Dumping ../data_dir/kafka_1/test-topic-1/00000000000000000000.log
Starting offset: 0
baseOffset: 0 lastOffset: 14 count: 15 baseSequence: -1 lastSequence: -1 producerId: -1 producerEpoch: -1 partitionLeaderEpoch: 0 isTransactional: false isControl: false position: 0 CreateTime: 1561597044933 size: 1237 magic: 2 compresscodec: LZ4 crc: 646766737 isvalid: true
| offset: 0 CreateTime: 1561597044911 keysize: -1 valuesize: 1024 sequence: -1 headerKeys: []
| offset: 1 CreateTime: 1561597044932 keysize: -1 valuesize: 1024 sequence: -1 headerKeys: []
| offset: 2 CreateTime: 1561597044932 keysize: -1 valuesize: 1024 sequence: -1 headerKeys: []
| offset: 3 CreateTime: 1561597044932 keysize: -1 valuesize: 1024 sequence: -1 headerKeys: []
| offset: 4 CreateTime: 1561597044932 keysize: -1 valuesize: 1024 sequence: -1 headerKeys: []
| offset: 5 CreateTime: 1561597044932 keysize: -1 valuesize: 1024 sequence: -1 headerKeys: []
| offset: 6 CreateTime: 1561597044932 keysize: -1 valuesize: 1024 sequence: -1 headerKeys: []
| offset: 7 CreateTime: 1561597044932 keysize: -1 valuesize: 1024 sequence: -1 headerKeys: []
| offset: 8 CreateTime: 1561597044932 keysize: -1 valuesize: 1024 sequence: -1 headerKeys: []
| offset: 9 CreateTime: 1561597044932 keysize: -1 valuesize: 1024 sequence: -1 headerKeys: []
| offset: 10 CreateTime: 1561597044932 keysize: -1 valuesize: 1024 sequence: -1 headerKeys: []
| offset: 11 CreateTime: 1561597044932 keysize: -1 valuesize: 1024 sequence: -1 headerKeys: []
| offset: 12 CreateTime: 1561597044932 keysize: -1 valuesize: 1024 sequence: -1 headerKeys: []
| offset: 13 CreateTime: 1561597044933 keysize: -1 valuesize: 1024 sequence: -1 headerKeys: []
| offset: 14 CreateTime: 1561597044933 keysize: -1 valuesize: 1024 sequence: -1 headerKeys: []
baseOffset: 15 lastOffset: 29 count: 15 baseSequence: -1 lastSequence: -1 producerId: -1 producerEpoch: -1 partitionLeaderEpoch: 0 isTransactional: false isControl: false position: 1237 CreateTime: 1561597044934 size: 1237 magic: 2 compresscodec: LZ4 crc: 3751986433 isvalid: true
......
```
在上面的输出中,以竖线开头的就是消息批次下的消息信息。如果你还想看消息里面的实际数据,那么还需要指定 ** --print-data-log参数**,如下所示:
```
$ bin/kafka-dump-log.sh --files ../data_dir/kafka_1/test-topic-1/00000000000000000000.log --deep-iteration --print-data-log
```
### 查询消费者组位移
接下来我们来看如何使用kafka-consumer-groups脚本查看消费者组位移。在上一讲讨论重设消费者组位移的时候我们使用的也是这个命令。当时我们用的是 ** --reset-offsets参数**,今天我们使用的是 ** --describe参数**。假设我们要查询Group ID是test-group的消费者的位移那么命令如图所示
<img src="https://static001.geekbang.org/resource/image/f4/ee/f4b7d92cdebff84998506afece1f61ee.png" alt="">
图中的CURRENT-OFFSET表示该消费者当前消费的最新位移LOG-END-OFFSET表示对应分区最新生产消息的位移LAG列是两者的差值。CONSUMER-ID是Kafka消费者程序自动生成的一个ID。截止到2.2版本你都无法干预这个ID的生成过程。如果运行该命令时这个消费者程序已经终止了那么此列的值为空。
## 小结
好了我们小结一下。今天我们一起梳理了Kafka 2.2版本自带的所有脚本我给出了常见的运维操作的工具行命令。希望这些命令对你操作和管理Kafka集群有所帮助。另外我想强调的是由于Kafka依然在不断演进我们今天提到的命令的用法很可能会随着版本的变迁而发生变化。在具体使用这些命令时你最好详细地阅读一下它们的Usage说明。
<img src="https://static001.geekbang.org/resource/image/5b/32/5b0daf262cde0c853a5cf9fbc9dfa332.jpg" alt="">
## 开放讨论
你在使用Kafka命令的过程中曾经踩过哪些“坑”或者说有哪些惨痛的经历呢
欢迎写下你的思考和答案,我们一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@@ -0,0 +1,177 @@
<audio id="audio" title="32 | KafkaAdminClientKafka的运维利器" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/bc/8c/bcd8560eaf99793a712e26c9e100118c.mp3"></audio>
你好我是胡夕。今天我要和你分享的主题是Kafka的运维利器KafkaAdminClient。
## 引入原因
在上一讲中我向你介绍了Kafka自带的各种命令行脚本这些脚本使用起来虽然方便却有一些弊端。
首先不论是Windows平台还是Linux平台命令行的脚本都只能运行在控制台上。如果你想要在应用程序、运维框架或是监控平台中集成它们会非常得困难。
其次这些命令行脚本很多都是通过连接ZooKeeper来提供服务的。目前社区已经越来越不推荐任何工具直连ZooKeeper了因为这会带来一些潜在的问题比如这可能会绕过Kafka的安全设置。在专栏前面我说过kafka-topics脚本连接ZooKeeper时不会考虑Kafka设置的用户认证机制。也就是说任何使用该脚本的用户不论是否具有创建主题的权限都能成功“跳过”权限检查强行创建主题。这显然和Kafka运维人员配置权限的初衷背道而驰。
最后运行这些脚本需要使用Kafka内部的类实现也就是Kafka**服务器端**的代码。实际上社区还是希望用户只使用Kafka**客户端**代码,通过现有的请求机制来运维管理集群。这样的话,所有运维操作都能纳入到统一的处理机制下,方便后面的功能演进。
基于这些原因社区于0.11版本正式推出了Java客户端版的AdminClient并不断地在后续的版本中对它进行完善。我粗略地计算了一下有关AdminClient的优化和更新的各种提案社区中有十几个之多而且贯穿各个大的版本足见社区对AdminClient的重视。
值得注意的是,**服务器端也有一个AdminClient**包路径是kafka.admin。这是之前的老运维工具类提供的功能也比较有限社区已经不再推荐使用它了。所以我们最好统一使用客户端的AdminClient。
## 如何使用?
下面我们来看一下如何在应用程序中使用AdminClient。我们在前面说过它是Java客户端提供的工具。想要使用它的话你需要在你的工程中显式地增加依赖。我以最新的2.3版本为例来进行一下展示。
如果你使用的是Maven需要增加以下依赖项
```
&lt;dependency&gt;
&lt;groupId&gt;org.apache.kafka&lt;/groupId&gt;
&lt;artifactId&gt;kafka-clients&lt;/artifactId&gt;
&lt;version&gt;2.3.0&lt;/version&gt;
&lt;/dependency&gt;
```
如果你使用的是Gradle那么添加方法如下
```
compile group: 'org.apache.kafka', name: 'kafka-clients', version: '2.3.0'
```
## 功能
鉴于社区还在不断地完善AdminClient的功能所以你需要时刻关注不同版本的发布说明Release Notes看看是否有新的运维操作被加入进来。在最新的2.3版本中AdminClient提供的功能有9大类。
1. 主题管理:包括主题的创建、删除和查询。
1. 权限管理:包括具体权限的配置与删除。
1. 配置参数管理包括Kafka各种资源的参数设置、详情查询。所谓的Kafka资源主要有Broker、主题、用户、Client-id等。
1. 副本日志管理:包括副本底层日志路径的变更和详情查询。
1. 分区管理:即创建额外的主题分区。
1. 消息删除:即删除指定位移之前的分区消息。
1. Delegation Token管理包括Delegation Token的创建、更新、过期和详情查询。
1. 消费者组管理:包括消费者组的查询、位移查询和删除。
1. Preferred领导者选举推选指定主题分区的Preferred Broker为领导者。
## 工作原理
在详细介绍AdminClient的主要功能之前我们先简单了解一下AdminClient的工作原理。**从设计上来看AdminClient是一个双线程的设计前端主线程和后端I/O线程**。前端线程负责将用户要执行的操作转换成对应的请求然后再将请求发送到后端I/O线程的队列中而后端I/O线程从队列中读取相应的请求然后发送到对应的Broker节点上之后把执行结果保存起来以便等待前端线程的获取。
值得一提的是AdminClient在内部大量使用生产者-消费者模式将请求生成与处理解耦。我在下面这张图中大致描述了它的工作原理。
<img src="https://static001.geekbang.org/resource/image/bd/88/bd820c9c2a5fb554561a9f78d6543c88.jpg" alt="">
如图所示前端主线程会创建名为Call的请求对象实例。该实例有两个主要的任务。
1. **构建对应的请求对象**。比如如果要创建主题那么就创建CreateTopicsRequest如果是查询消费者组位移就创建OffsetFetchRequest。
1. **指定响应的回调逻辑**。比如从Broker端接收到CreateTopicsResponse之后要执行的动作。一旦创建好Call实例前端主线程会将其放入到新请求队列New Call Queue此时前端主线程的任务就算完成了。它只需要等待结果返回即可。
剩下的所有事情就都是后端I/O线程的工作了。就像图中所展示的那样该线程使用了3个队列来承载不同时期的请求对象它们分别是新请求队列、待发送请求队列和处理中请求队列。为什么要使用3个呢原因是目前新请求队列的线程安全是由Java的monitor锁来保证的。**为了确保前端主线程不会因为monitor锁被阻塞后端I/O线程会定期地将新请求队列中的所有Call实例全部搬移到待发送请求队列中进行处理**。图中的待发送请求队列和处理中请求队列只由后端I/O线程处理因此无需任何锁机制来保证线程安全。
当I/O线程在处理某个请求时它会显式地将该请求保存在处理中请求队列。一旦处理完成I/O线程会自动地调用Call对象中的回调逻辑完成最后的处理。把这些都做完之后I/O线程会通知前端主线程说结果已经准备完毕这样前端主线程能够及时获取到执行操作的结果。AdminClient是使用Java Object对象的wait和notify实现的这种通知机制。
严格来说AdminClient并没有使用Java已有的队列去实现上面的请求队列它是使用ArrayList和HashMap这样的简单容器类再配以monitor锁来保证线程安全的。不过鉴于它们充当的角色就是请求队列这样的主体我还是坚持使用队列来指代它们了。
了解AdminClient工作原理的一个好处在于**它能够帮助我们有针对性地对调用AdminClient的程序进行调试**。
我们刚刚提到的后端I/O线程其实是有名字的名字的前缀是kafka-admin-client-thread。有时候我们会发现AdminClient程序貌似在正常工作但执行的操作没有返回结果或者hang住了现在你应该知道这可能是因为I/O线程出现问题导致的。如果你碰到了类似的问题不妨使用**jstack命令**去查看一下你的AdminClient程序确认下I/O线程是否在正常工作。
这可不是我杜撰出来的好处实际上这是实实在在的社区bug。出现这个问题的根本原因就是I/O线程未捕获某些异常导致意外“挂”掉。由于AdminClient是双线程的设计前端主线程不受任何影响依然可以正常接收用户发送的命令请求但此时程序已经不能正常工作了。
## 构造和销毁AdminClient实例
如果你正确地引入了kafka-clients依赖那么你应该可以在编写Java程序时看到AdminClient对象。**切记它的完整类路径是org.apache.kafka.clients.admin.AdminClient而不是kafka.admin.AdminClient**。后者就是我们刚才说的服务器端的AdminClient它已经不被推荐使用了。
创建AdminClient实例和创建KafkaProducer或KafkaConsumer实例的方法是类似的你需要手动构造一个Properties对象或Map对象然后传给对应的方法。社区专门为AdminClient提供了几十个专属参数最常见而且必须要指定的参数是我们熟知的**bootstrap.servers参数**。如果你想了解完整的参数列表,可以去[官网](https://kafka.apache.org/documentation/#adminclientconfigs)查询一下。如果要销毁AdminClient实例需要显式调用AdminClient的close方法。
你可以简单使用下面的代码同时实现AdminClient实例的创建与销毁。
```
Properties props = new Properties();
props.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, &quot;kafka-host:port&quot;);
props.put(&quot;request.timeout.ms&quot;, 600000);
try (AdminClient client = AdminClient.create(props)) {
// 执行你要做的操作……
}
```
这段代码使用Java 7的try-with-resource语法特性创建了AdminClient实例并在使用之后自动关闭。你可以在try代码块中加入你想要执行的操作逻辑。
## 常见的AdminClient应用实例
讲完了AdminClient的工作原理和构造方法接下来我举几个实际的代码程序来说明一下如何应用它。这几个例子都是我们最常见的。
### 创建主题
首先,我们来看看如何创建主题,代码如下:
```
String newTopicName = &quot;test-topic&quot;;
try (AdminClient client = AdminClient.create(props)) {
NewTopic newTopic = new NewTopic(newTopicName, 10, (short) 3);
CreateTopicsResult result = client.createTopics(Arrays.asList(newTopic));
result.all().get(10, TimeUnit.SECONDS);
}
```
这段代码调用AdminClient的createTopics方法创建对应的主题。构造主题的类是NewTopic类它接收主题名称、分区数和副本数三个字段。
注意这段代码倒数第二行获取结果的方法。目前AdminClient各个方法的返回类型都是名为***Result的对象。这类对象会将结果以Java Future的形式封装起来。如果要获取运行结果你需要调用相应的方法来获取对应的Future对象然后再调用相应的get方法来取得执行结果。
当然,对于创建主题而言,一旦主题被成功创建,任务也就完成了,它返回的结果也就不重要了,只要没有抛出异常就行。
### 查询消费者组位移
接下来,我来演示一下如何查询指定消费者组的位移信息,代码如下:
```
String groupID = &quot;test-group&quot;;
try (AdminClient client = AdminClient.create(props)) {
ListConsumerGroupOffsetsResult result = client.listConsumerGroupOffsets(groupID);
Map&lt;TopicPartition, OffsetAndMetadata&gt; offsets =
result.partitionsToOffsetAndMetadata().get(10, TimeUnit.SECONDS);
System.out.println(offsets);
}
```
和创建主题的风格一样,**我们调用AdminClient的listConsumerGroupOffsets方法去获取指定消费者组的位移数据**。
不过,对于这次返回的结果,我们不能再丢弃不管了,**因为它返回的Map对象中保存着按照分区分组的位移数据**。你可以调用OffsetAndMetadata对象的offset()方法拿到实际的位移数据。
### 获取Broker磁盘占用
现在我们来使用AdminClient实现一个稍微高级一点的功能获取某台Broker上Kafka主题占用的磁盘空间量。有些遗憾的是目前Kafka的JMX监控指标没有提供这样的功能而磁盘占用这件事是很多Kafka运维人员要实时监控并且极为重视的。
幸运的是我们可以使用AdminClient来实现这一功能。代码如下
```
try (AdminClient client = AdminClient.create(props)) {
DescribeLogDirsResult ret = client.describeLogDirs(Collections.singletonList(targetBrokerId)); // 指定Broker id
long size = 0L;
for (Map&lt;String, DescribeLogDirsResponse.LogDirInfo&gt; logDirInfoMap : ret.all().get().values()) {
size += logDirInfoMap.values().stream().map(logDirInfo -&gt; logDirInfo.replicaInfos).flatMap(
topicPartitionReplicaInfoMap -&gt;
topicPartitionReplicaInfoMap.values().stream().map(replicaInfo -&gt; replicaInfo.size))
.mapToLong(Long::longValue).sum();
}
System.out.println(size);
}
```
这段代码的主要思想是使用AdminClient的**describeLogDirs方法**获取指定Broker上所有分区主题的日志路径信息然后把它们累积在一起得出总的磁盘占用量。
## 小结
好了我们来小结一下。社区于0.11版本正式推出了Java客户端版的AdminClient工具该工具提供了几十种运维操作而且它还在不断地演进着。如果可以的话你最好统一使用AdminClient来执行各种Kafka集群管理操作摒弃掉连接ZooKeeper的那些工具。另外我建议你时刻关注该工具的功能完善情况毕竟目前社区对AdminClient的变更频率很高。
<img src="https://static001.geekbang.org/resource/image/06/f0/068a8cb7ea769799e60ad7f5a80a9bf0.jpg" alt="">
## 开放讨论
请思考一下如果我们要使用AdminClient去增加某个主题的分区代码应该怎么写请给出主体代码。
欢迎写下你的思考和答案,我们一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@@ -0,0 +1,260 @@
<audio id="audio" title="33 | Kafka认证机制用哪家" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/18/1d/18567e4919b8fa6971b8aa9b8607e91d.mp3"></audio>
你好我是胡夕。今天我要和你分享的主题是Kafka的认证机制。
## 什么是认证机制?
所谓认证又称“验证”“鉴权”英文是authentication是指通过一定的手段完成对用户身份的确认。认证的主要目的是确认当前声称为某种身份的用户确实是所声称的用户。
在计算机领域经常和认证搞混的一个术语就是授权英文是authorization。授权一般是指对信息安全或计算机安全相关的资源定义与授予相应的访问权限。
举个简单的例子来区分下两者:认证要解决的是你要证明你是谁的问题,授权要解决的则是你能做什么的问题。
在Kafka中认证和授权是两套独立的安全配置。我们今天主要讨论Kafka的认证机制在专栏的下一讲内容中我们将讨论授权机制。
## Kafka认证机制
自0.9.0.0版本开始Kafka正式引入了认证机制用于实现基础的安全用户认证这是将Kafka上云或进行多租户管理的必要步骤。截止到当前最新的2.3版本Kafka支持基于SSL和基于SASL的安全认证机制。
**基于SSL的认证主要是指Broker和客户端的双路认证**2-way authentication。通常来说SSL加密Encryption已经启用了单向认证即客户端认证Broker的证书Certificate。如果要做SSL认证那么我们要启用双路认证也就是说Broker也要认证客户端的证书。
对了你可能会说SSL不是已经过时了吗现在都叫TLSTransport Layer Security了吧但是Kafka的源码中依然是使用SSL而不是TLS来表示这类东西的。不过今天出现的所有SSL字眼你都可以认为它们是和TLS等价的。
Kafka还支持通过SASL做客户端认证。**SASL是提供认证和数据安全服务的框架**。Kafka支持的SASL机制有5种它们分别是在不同版本中被引入的你需要根据你自己使用的Kafka版本来选择该版本所支持的认证机制。
1. GSSAPI也就是Kerberos使用的安全接口是在0.9版本中被引入的。
1. PLAIN是使用简单的用户名/密码认证的机制在0.10版本中被引入。
1. SCRAM主要用于解决PLAIN机制安全问题的新机制是在0.10.2版本中被引入的。
1. OAUTHBEARER是基于OAuth 2认证框架的新机制在2.0版本中被引进。
1. Delegation Token补充现有SASL机制的轻量级认证机制是在1.1.0版本被引入的。
## 认证机制的比较
Kafka为我们提供了这么多种认证机制在实际使用过程中我们应该如何选择合适的认证框架呢下面我们就来比较一下。
目前来看使用SSL做信道加密的情况更多一些但使用SSL实现认证不如使用SASL。毕竟SASL能够支持你选择不同的实现机制如GSSAPI、SCRAM、PLAIN等。因此我的建议是**你可以使用SSL来做通信加密使用SASL来做Kafka的认证实现**。
SASL下又细分了很多种认证机制我们应该如何选择呢
SASL/GSSAPI主要是给Kerberos使用的。如果你的公司已经做了Kerberos认证比如使用Active Directory那么使用GSSAPI是最方便的了。因为你不需要额外地搭建Kerberos只要让你们的Kerberos管理员给每个Broker和要访问Kafka集群的操作系统用户申请principal就好了。总之**GSSAPI适用于本身已经做了Kerberos认证的场景这样的话SASL/GSSAPI可以实现无缝集成**。
而SASL/PLAIN就像前面说到的它是一个简单的用户名/密码认证机制通常与SSL加密搭配使用。注意这里的PLAIN和PLAINTEXT是两回事。**PLAIN在这里是一种认证机制而PLAINTEXT说的是未使用SSL时的明文传输**。对于一些小公司而言搭建公司级的Kerberos可能并没有什么必要他们的用户系统也不复杂特别是访问Kafka集群的用户可能不是很多。对于SASL/PLAIN而言这就是一个非常合适的应用场景。**总体来说SASL/PLAIN的配置和运维成本相对较小适合于小型公司中的Kafka集群**。
但是SASL/PLAIN有这样一个弊端它不能动态地增减认证用户你必须重启Kafka集群才能令变更生效。为什么呢这是因为所有认证用户信息全部保存在静态文件中所以只能重启Broker才能重新加载变更后的静态文件。
我们知道重启集群在很多场景下都是令人不爽的即使是轮替式升级Rolling Upgrade。SASL/SCRAM就解决了这样的问题。它通过将认证用户信息保存在ZooKeeper的方式避免了动态修改需要重启Broker的弊端。在实际使用过程中你可以使用Kafka提供的命令动态地创建和删除用户无需重启整个集群。因此**如果你打算使用SASL/PLAIN不妨改用SASL/SCRAM试试。不过要注意的是后者是0.10.2版本引入的。你至少要升级到这个版本后才能使用**。
SASL/OAUTHBEARER是2.0版本引入的新认证机制主要是为了实现与OAuth 2框架的集成。OAuth是一个开发标准允许用户授权第三方应用访问该用户在某网站上的资源而无需将用户名和密码提供给第三方应用。Kafka不提倡单纯使用OAUTHBEARER因为它生成的不安全的JSON Web Token必须配以SSL加密才能用在生产环境中。当然鉴于它是2.0版本才推出来的,而且目前没有太多的实际使用案例,我们可以先观望一段时间,再酌情将其应用于生产环境中。
Delegation Token是在1.1版本引入的它是一种轻量级的认证机制主要目的是补充现有的SASL或SSL认证。如果要使用Delegation Token你需要先配置好SASL认证然后再利用Kafka提供的API去获取对应的Delegation Token。这样Broker和客户端在做认证的时候可以直接使用这个token不用每次都去KDC获取对应的ticketKerberos认证或传输Keystore文件SSL认证
为了方便你更好地理解和记忆,我把这些认证机制汇总在下面的表格里了。你可以对照着表格,进行一下区分。
<img src="https://static001.geekbang.org/resource/image/4a/3d/4a52c2eb1ae631697b5ec3d298f7333d.jpg" alt="">
## SASL/SCRAM-SHA-256配置实例
接下来我给出SASL/SCRAM的一个配置实例来说明一下如何在Kafka集群中开启认证。其他认证机制的设置方法也是类似的比如它们都涉及认证用户的创建、Broker端以及Client端特定参数的配置等。
我的测试环境是本地Mac上的两个Broker组成的Kafka集群连接端口分别是9092和9093。
### 第1步创建用户
配置SASL/SCRAM的第一步是创建能否连接Kafka集群的用户。在本次测试中我会创建3个用户分别是admin用户、writer用户和reader用户。admin用户用于实现Broker间通信writer用户用于生产消息reader用户用于消费消息。
我们使用下面这3条命令分别来创建它们。
```
$ cd kafka_2.12-2.3.0/
$ bin/kafka-configs.sh --zookeeper localhost:2181 --alter --add-config 'SCRAM-SHA-256=[password=admin],SCRAM-SHA-512=[password=admin]' --entity-type users --entity-name admin
Completed Updating config for entity: user-principal 'admin'.
```
```
$ bin/kafka-configs.sh --zookeeper localhost:2181 --alter --add-config 'SCRAM-SHA-256=[password=writer],SCRAM-SHA-512=[password=writer]' --entity-type users --entity-name writer
Completed Updating config for entity: user-principal 'writer'.
```
```
$ bin/kafka-configs.sh --zookeeper localhost:2181 --alter --add-config 'SCRAM-SHA-256=[password=reader],SCRAM-SHA-512=[password=reader]' --entity-type users --entity-name reader
Completed Updating config for entity: user-principal 'reader'.
```
在专栏前面我们提到过kafka-configs脚本是用来设置主题级别参数的。其实它的功能还有很多。比如在这个例子中我们使用它来创建SASL/SCRAM认证中的用户信息。我们可以使用下列命令来查看刚才创建的用户数据。
```
$ bin/kafka-configs.sh --zookeeper localhost:2181 --describe --entity-type users --entity-name writer
Configs for user-principal 'writer' are SCRAM-SHA-512=salt=MWt6OGplZHF6YnF5bmEyam9jamRwdWlqZWQ=,stored_key=hR7+vgeCEz61OmnMezsqKQkJwMCAoTTxw2jftYiXCHxDfaaQU7+9/dYBq8bFuTio832mTHk89B4Yh9frj/ampw==,server_key=C0k6J+9/InYRohogXb3HOlG7s84EXAs/iw0jGOnnQAt4jxQODRzeGxNm+18HZFyPn7qF9JmAqgtcU7hgA74zfA==,iterations=4096,SCRAM-SHA-256=salt=MWV0cDFtbXY5Nm5icWloajdnbjljZ3JqeGs=,stored_key=sKjmeZe4sXTAnUTL1CQC7DkMtC+mqKtRY0heEHvRyPk=,server_key=kW7CC3PBj+JRGtCOtIbAMefL8aiL8ZrUgF5tfomsWVA=,iterations=4096
```
这段命令包含了writer用户加密算法SCRAM-SHA-256以及SCRAM-SHA-512对应的盐值(Salt)、ServerKey和StoreKey。这些都是SCRAM机制的术语我们不需要了解它们的含义因为它们并不影响我们接下来的配置。
### 第2步创建JAAS文件
配置了用户之后我们需要为每个Broker创建一个对应的JAAS文件。因为本例中的两个Broker实例是在一台机器上所以我只创建了一份JAAS文件。但是你要切记在实际场景中你需要为每台单独的物理Broker机器都创建一份JAAS文件。
JAAS的文件内容如下
```
KafkaServer {
org.apache.kafka.common.security.scram.ScramLoginModule required
username=&quot;admin&quot;
password=&quot;admin&quot;;
};
```
关于这个文件内容,你需要注意以下两点:
- 不要忘记最后一行和倒数第二行结尾处的分号;
- JAAS文件中不需要任何空格键。
这里我们使用admin用户实现Broker之间的通信。接下来我们来配置Broker的server.properties文件下面这些内容是需要单独配置的
```
sasl.enabled.mechanisms=SCRAM-SHA-256
```
```
sasl.mechanism.inter.broker.protocol=SCRAM-SHA-256
```
```
security.inter.broker.protocol=SASL_PLAINTEXT
```
```
listeners=SASL_PLAINTEXT://localhost:9092
```
第1项内容表明开启SCRAM认证机制并启用SHA-256算法第2项的意思是为Broker间通信也开启SCRAM认证同样使用SHA-256算法第3项表示Broker间通信不配置SSL本例中我们不演示SSL的配置最后1项是设置listeners使用SASL_PLAINTEXT依然是不使用SSL。
另一台Broker的配置基本和它类似只是要使用不同的端口在这个例子中端口是9093。
### 第3步启动Broker
现在我们分别启动这两个Broker。在启动时你需要指定JAAS文件的位置如下所示
```
$KAFKA_OPTS=-Djava.security.auth.login.config=&lt;your_path&gt;/kafka-broker.jaas bin/kafka-server-start.sh config/server1.properties
......
[2019-07-02 13:30:34,822] INFO Kafka commitId: fc1aaa116b661c8a (org.apache.kafka.common.utils.AppInfoParser)
[2019-07-02 13:30:34,822] INFO Kafka startTimeMs: 1562045434820 (org.apache.kafka.common.utils.AppInfoParser)
[2019-07-02 13:30:34,823] INFO [KafkaServer id=0] started (kafka.server.KafkaServer)
```
```
$KAFKA_OPTS=-Djava.security.auth.login.config=&lt;your_path&gt;/kafka-broker.jaas bin/kafka-server-start.sh config/server2.properties
......
[2019-07-02 13:32:31,976] INFO Kafka commitId: fc1aaa116b661c8a (org.apache.kafka.common.utils.AppInfoParser)
[2019-07-02 13:32:31,976] INFO Kafka startTimeMs: 1562045551973 (org.apache.kafka.common.utils.AppInfoParser)
[2019-07-02 13:32:31,978] INFO [KafkaServer id=1] started (kafka.server.KafkaServer)
```
此时两台Broker都已经成功启动了。
### 第4步发送消息
在创建好测试主题之后我们使用kafka-console-producer脚本来尝试发送消息。由于启用了认证客户端需要做一些相应的配置。我们创建一个名为producer.conf的配置文件内容如下
```
security.protocol=SASL_PLAINTEXT
sasl.mechanism=SCRAM-SHA-256
sasl.jaas.config=org.apache.kafka.common.security.scram.ScramLoginModule required username=&quot;writer&quot; password=&quot;writer&quot;;
```
之后运行Console Producer程序
```
$ bin/kafka-console-producer.sh --broker-list localhost:9092,localhost:9093 --topic test --producer.config &lt;your_path&gt;/producer.conf
&gt;hello, world
&gt;
```
可以看到Console Producer程序发送消息成功。
### 第5步消费消息
接下来我们使用Console Consumer程序来消费一下刚刚生产的消息。同样地我们需要为kafka-console-consumer脚本创建一个名为consumer.conf的脚本内容如下
```
security.protocol=SASL_PLAINTEXT
sasl.mechanism=SCRAM-SHA-256
sasl.jaas.config=org.apache.kafka.common.security.scram.ScramLoginModule required username=&quot;reader&quot; password=&quot;reader&quot;;
```
之后运行Console Consumer程序
```
$ bin/kafka-console-consumer.sh --bootstrap-server localhost:9092,localhost:9093 --topic test --from-beginning --consumer.config &lt;your_path&gt;/consumer.conf
hello, world
```
很显然,我们是可以正常消费的。
### 第6步动态增减用户
最后我们来演示SASL/SCRAM动态增减用户的场景。假设我删除了writer用户同时又添加了一个新用户new_writer那么我们需要执行的命令如下
```
$ bin/kafka-configs.sh --zookeeper localhost:2181 --alter --delete-config 'SCRAM-SHA-256' --entity-type users --entity-name writer
Completed Updating config for entity: user-principal 'writer'.
$ bin/kafka-configs.sh --zookeeper localhost:2181 --alter --delete-config 'SCRAM-SHA-512' --entity-type users --entity-name writer
Completed Updating config for entity: user-principal 'writer'.
$ bin/kafka-configs.sh --zookeeper localhost:2181 --alter --add-config 'SCRAM-SHA-256=[iterations=8192,password=new_writer]' --entity-type users --entity-name new_writer
Completed Updating config for entity: user-principal 'new_writer'.
```
现在我们依然使用刚才的producer.conf来验证以确认Console Producer程序不能发送消息。
```
$ bin/kafka-console-producer.sh --broker-list localhost:9092,localhost:9093 --topic test --producer.config /Users/huxi/testenv/producer.conf
&gt;[2019-07-02 13:54:29,695] ERROR [Producer clientId=console-producer] Connection to node -1 (localhost/127.0.0.1:9092) failed authentication due to: Authentication failed during authentication due to invalid credentials with SASL mechanism SCRAM-SHA-256 (org.apache.kafka.clients.NetworkClient)
......
```
很显然此时Console Producer已经不能发送消息了。因为它使用的producer.conf文件指定的是已经被删除的writer用户。如果我们修改producer.conf的内容改为指定新创建的new_writer用户结果如下
```
$ bin/kafka-console-producer.sh --broker-list localhost:9092,localhost:9093 --topic test --producer.config &lt;your_path&gt;/producer.conf
&gt;Good!
```
现在Console Producer可以正常发送消息了。
这个过程完整地展示了SASL/SCRAM是如何在不重启Broker的情况下增减用户的。
至此SASL/SCRAM配置就完成了。在专栏下一讲中我会详细介绍一下如何赋予writer和reader用户不同的权限。
## 小结
好了我们来小结一下。今天我们讨论了Kafka目前提供的几种认证机制我给出了它们各自的优劣势以及推荐使用建议。其实在真实的使用场景中认证和授权往往是结合在一起使用的。在专栏下一讲中我会详细向你介绍Kafka的授权机制即ACL机制敬请期待。
<img src="https://static001.geekbang.org/resource/image/af/5c/af705cb98a2f46acd45b184ec201005c.jpg" alt="">
## 开放讨论
请谈一谈你的Kafka集群上的用户认证机制并分享一个你遇到过的“坑”。
欢迎写下你的思考和答案,我们一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@@ -0,0 +1,285 @@
<audio id="audio" title="34 | 云环境下的授权该怎么做?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/30/cd/30095ea813a91514e49c747a28a17ecd.mp3"></audio>
你好我是胡夕。今天我要分享的主题是Kafka的授权机制。
## 什么是授权机制?
我们在上一讲中花了不少时间讨论Kafka的认证机制今天我们来看看Kafka的授权机制Authorization。所谓授权一般是指对与信息安全或计算机安全相关的资源授予访问权限特别是存取控制。
具体到权限模型,常见的有四种。
- ACLAccess-Control List访问控制列表。
- RBACRole-Based Access Control基于角色的权限控制。
- ABACAttribute-Based Access Control基于属性的权限控制。
- PBACPolicy-Based Access Control基于策略的权限控制。
在典型的互联网场景中,前两种模型应用得多,后面这两种则比较少用。
ACL模型很简单它表征的是用户与权限的直接映射关系如下图所示
<img src="https://static001.geekbang.org/resource/image/eb/ad/eb85325aa6858b45a53ecaae6e58d0ad.jpg" alt="">
而RBAC模型则加入了角色的概念支持对用户进行分组如下图所示
<img src="https://static001.geekbang.org/resource/image/43/aa/4368827128d1309709fe51199a11b7aa.jpg" alt="">
Kafka没有使用RBAC模型它用的是ACL模型。简单来说这种模型就是规定了什么用户对什么资源有什么样的访问权限。我们可以借用官网的一句话来统一表示这种模型“**Principal P is [Allowed/Denied] Operation O From Host H On Resource R.**” 这句话中出现了很多个主体,我来分别解释下它们的含义。
- Principal表示访问Kafka集群的用户。
- Operation表示一个具体的访问类型如读写消息或创建主题等。
- Host表示连接Kafka集群的客户端应用程序IP地址。Host支持星号占位符表示所有IP地址。
- Resource表示Kafka资源类型。如果以最新的2.3版本为例Resource共有5种分别是TOPIC、CLUSTER、GROUP、TRANSACTIONALID和DELEGATION TOKEN。
当前Kafka提供了一个可插拔的授权实现机制。该机制会将你配置的所有ACL项保存在ZooKeeper下的/kafka-acl节点中。你可以通过Kafka自带的kafka-acls脚本动态地对ACL项进行增删改查并让它立即生效。
## 如何开启ACL
在Kafka中开启ACL的方法特别简单你只需要在Broker端的配置文件中增加一行设置即可也就是在server.properties文件中配置下面这个参数值
```
authorizer.class.name=kafka.security.auth.SimpleAclAuthorizer
```
authorizer.class.name参数指定了ACL授权机制的实现类。当前Kafka提供了Authorizer接口允许你实现你自己的授权机制但更常见的做法还是直接使用Kafka自带的**SimpleAclAuthorizer实现类**。一旦设置好这个参数的值并且启动Broker后该Broker就默认开启了ACL授权验证。在实际生产环境中你需要为集群中的每台Broker都做此设置。
## 超级用户Super User
在开启了ACL授权之后你还必须显式地为不同用户设置访问某项资源的权限否则在默认情况下没有配置任何ACL的资源是不能被访问的。不过这里也有一个例外**超级用户能够访问所有的资源即使你没有为它们设置任何ACL项**。
那么我们如何在一个Kafka集群中设置超级用户呢方法很简单只需要在Broker端的配置文件server.properties中设置super.users参数即可比如
```
super.users=User:superuser1;User:superuser2
```
**注意,如果你要一次性指定多个超级用户,那么分隔符是分号而不是逗号,这是为了避免出现用户名中包含逗号从而无法分割的问题**
除了设置super.users参数Kafka还支持将所有用户都配置成超级用户的用法。如果我们在server.properties文件中设置allow.everyone.if.no.acl.found=true那么所有用户都可以访问没有设置任何ACL的资源。不过我个人不太建议进行这样的设置。毕竟在生产环境中特别是在那些对安全有较高要求的环境中采用白名单机制要比黑名单机制更加令人放心。
## kafka-acls脚本
在了解了Kafka的ACL概念之后我们来看一下如何设置它们。当前在Kafka中配置授权的方法是通过kafka-acls脚本。举个例子如果我们要为用户Alice增加了集群级别的所有权限那么我们可以使用下面这段命令。
```
$ kafka-acls --authorizer-properties zookeeper.connect=localhost:2181 --add --allow-principal User:Alice --operation All --topic '*' --cluster
```
在这个命令中All表示所有操作topic中的星号则表示所有主题指定 --cluster则说明我们要为Alice设置的是集群权限。
这个脚本的参数有很多,我们再来看看它的另一个常见用法。
```
$ bin/kafka-acls --authorizer-properties zookeeper.connect=localhost:2181 --add --allow-principal User:'*' --allow-host '*' --deny-principal User:BadUser --deny-host 10.205.96.119 --operation Read --topic test-topic
```
User后面的星号表示所有用户allow-host后面的星号则表示所有IP地址。这个命令的意思是允许所有的用户使用任意的IP地址读取名为test-topic的主题数据同时也禁止BadUser用户和10.205.96.119的IP地址访问test-topic下的消息。
kafka-acls脚本还有其他的功能比如删除ACL、查询已有ACL等。它们的实际用法与上面这条命令类似我在这里就不一一列举了你可以使用kafka-acls.sh来查询它的所有用法。
## ACL权限列表
刚才的这两条命令分别涉及了主题的集群权限和读权限。你可能会问Kafka到底提供了多少种ACL权限呢我们一起来看看下面这张表格它完整地展示了Kafka所有的ACL权限。
<img src="https://static001.geekbang.org/resource/image/62/bc/620bc02b57c49fa2d7390c698db515bc.jpg" alt="">
看到这么大一张表格你是不是很惊讶其实这恰好证明Kafka当前提供的授权机制是非常细粒度的。现在我来跟你分享一下这个表格的使用方法。
举个例子假如你要为你的生产者程序赋予写权限那么首先你要在Resource列找到Topic类型的权限然后在Operation列寻找WRITE操作权限。这个WRITE权限是限制Producer程序能否向对应主题发送消息的关键。通常情况下Producer程序还可能有**创建主题、获取主题数据**的权限所以Kafka为Producer需要的这些常见权限创建了快捷方式即 --producer。也就是说在执行kafka-acls命令时直接指定 --producer就能同时获得这三个权限了。 --consumer也是类似的指定 --consumer可以同时获得Consumer端应用所需的权限。
## 授权机制能否单独使用?
关于授权有一个很常见的问题是Kafka授权机制能不配置认证机制而单独使用吗其实这是可以的只是你只能为IP地址设置权限。比如下面这个命令会禁止运行在127.0.0.1IP地址上的Producer应用向test主题发送数据
```
$ bin/kafka-acls.sh --authorizer-properties zookeeper.connect=localhost:2181 --add --deny-principal User:* --deny-host 127.0.0.1 --operation Write --topic test
$ bin/kafka-console-producer.sh --broker-list localhost:9092 --topic test
&gt;hello
[2019-07-16 10:10:57,283] WARN [Producer clientId=console-producer] Error while fetching metadata with correlation id 3 : {test=TOPIC_AUTHORIZATION_FAILED} (org.apache.kafka.clients.NetworkClient)
[2019-07-16 10:10:57,284] ERROR [Producer clientId=console-producer] Topic authorization failed for topics [test] (org.apache.kafka.clients.Metadata)
[2019-07-16 10:10:57,284] ERROR Error when sending message to topic test with key: null, value: 5 bytes with error: (org.apache.kafka.clients.producer.internals.ErrorLoggingCallback)
org.apache.kafka.common.errors.TopicAuthorizationException: Not authorized to access topics: [test]
```
请注意一下输出中的橙色字体部分。虽然没有设置任何认证机制但是通过设置IP地址的ACL授权我们依然可以禁止这些IP地址上的客户端访问Kafka资源。不过尽管授权机制能够有限度地单独使用但我更推荐的做法是和我们在专栏上一讲提到的认证机制搭配使用。
接下来我来给出一个SSL + ACL配置的实例来演示一下云环境下的ACL授权应该怎么做。
## 配置实例
在演示ACL之前我先简单说一下SSL的配置。我给出一个SHELL脚本它可以方便你设置SSL代码如下
```
#!/bin/bash
#设置环境变量
BASE_DIR=/Users/huxi/testenv #你需要修改此处
CERT_OUTPUT_PATH=&quot;$BASE_DIR/certificates&quot;
PASSWORD=test1234
KEY_STORE=&quot;$CERT_OUTPUT_PATH/server.keystore.jks&quot;
TRUST_STORE=&quot;$CERT_OUTPUT_PATH/server.truststore.jks&quot;
CLIENT_KEY_STORE=&quot;$CERT_OUTPUT_PATH/client.keystore.jks&quot;
CLIENT_TRUST_STORE=&quot;$CERT_OUTPUT_PATH/client.truststore.jks&quot;
KEY_PASSWORD=$PASSWORD
STORE_PASSWORD=$PASSWORD
TRUST_KEY_PASSWORD=$PASSWORD
TRUST_STORE_PASSWORD=$PASSWORD
CERT_AUTH_FILE=&quot;$CERT_OUTPUT_PATH/ca-cert&quot;
DAYS_VALID=365
DNAME=&quot;CN=Xi Hu, OU=YourDept, O=YourCompany, L=Beijing, ST=Beijing, C=CN&quot;
mkdir -p $CERT_OUTPUT_PATH
echo &quot;1. 产生key和证书......&quot;
keytool -keystore $KEY_STORE -alias kafka-server -validity $DAYS_VALID -genkey -keyalg RSA \
-storepass $STORE_PASSWORD -keypass $KEY_PASSWORD -dname &quot;$DNAME&quot;
keytool -keystore $CLIENT_KEY_STORE -alias kafka-client -validity $DAYS_VALID -genkey -keyalg RSA \
-storepass $STORE_PASSWORD -keypass $KEY_PASSWORD -dname &quot;$DNAME&quot;
echo &quot;2. 创建CA......&quot;
openssl req -new -x509 -keyout $CERT_OUTPUT_PATH/ca-key -out &quot;$CERT_AUTH_FILE&quot; -days &quot;$DAYS_VALID&quot; \
-passin pass:&quot;$PASSWORD&quot; -passout pass:&quot;$PASSWORD&quot; \
-subj &quot;/C=CN/ST=Beijing/L=Beijing/O=YourCompany/OU=YourDept,CN=Xi Hu&quot;
echo &quot;3. 添加CA文件到broker truststore......&quot;
keytool -keystore &quot;$TRUST_STORE&quot; -alias CARoot \
-importcert -file &quot;$CERT_AUTH_FILE&quot; -storepass &quot;$TRUST_STORE_PASSWORD&quot; -keypass &quot;$TRUST_KEY_PASS&quot; -noprompt
echo &quot;4. 添加CA文件到client truststore......&quot;
keytool -keystore &quot;$CLIENT_TRUST_STORE&quot; -alias CARoot \
-importcert -file &quot;$CERT_AUTH_FILE&quot; -storepass &quot;$TRUST_STORE_PASSWORD&quot; -keypass &quot;$TRUST_KEY_PASS&quot; -noprompt
echo &quot;5. 从keystore中导出集群证书......&quot;
keytool -keystore &quot;$KEY_STORE&quot; -alias kafka-server -certreq -file &quot;$CERT_OUTPUT_PATH/server-cert-file&quot; \
-storepass &quot;$STORE_PASSWORD&quot; -keypass &quot;$KEY_PASSWORD&quot; -noprompt
keytool -keystore &quot;$CLIENT_KEY_STORE&quot; -alias kafka-client -certreq -file &quot;$CERT_OUTPUT_PATH/client-cert-file&quot; \
-storepass &quot;$STORE_PASSWORD&quot; -keypass &quot;$KEY_PASSWORD&quot; -noprompt
echo &quot;6. 使用CA签发证书......&quot;
openssl x509 -req -CA &quot;$CERT_AUTH_FILE&quot; -CAkey $CERT_OUTPUT_PATH/ca-key -in &quot;$CERT_OUTPUT_PATH/server-cert-file&quot; \
-out &quot;$CERT_OUTPUT_PATH/server-cert-signed&quot; -days &quot;$DAYS_VALID&quot; -CAcreateserial -passin pass:&quot;$PASSWORD&quot;
openssl x509 -req -CA &quot;$CERT_AUTH_FILE&quot; -CAkey $CERT_OUTPUT_PATH/ca-key -in &quot;$CERT_OUTPUT_PATH/client-cert-file&quot; \
-out &quot;$CERT_OUTPUT_PATH/client-cert-signed&quot; -days &quot;$DAYS_VALID&quot; -CAcreateserial -passin pass:&quot;$PASSWORD&quot;
echo &quot;7. 导入CA文件到keystore......&quot;
keytool -keystore &quot;$KEY_STORE&quot; -alias CARoot -import -file &quot;$CERT_AUTH_FILE&quot; -storepass &quot;$STORE_PASSWORD&quot; \
-keypass &quot;$KEY_PASSWORD&quot; -noprompt
keytool -keystore &quot;$CLIENT_KEY_STORE&quot; -alias CARoot -import -file &quot;$CERT_AUTH_FILE&quot; -storepass &quot;$STORE_PASSWORD&quot; \
-keypass &quot;$KEY_PASSWORD&quot; -noprompt
echo &quot;8. 导入已签发证书到keystore......&quot;
keytool -keystore &quot;$KEY_STORE&quot; -alias kafka-server -import -file &quot;$CERT_OUTPUT_PATH/server-cert-signed&quot; \
-storepass &quot;$STORE_PASSWORD&quot; -keypass &quot;$KEY_PASSWORD&quot; -noprompt
keytool -keystore &quot;$CLIENT_KEY_STORE&quot; -alias kafka-client -import -file &quot;$CERT_OUTPUT_PATH/client-cert-signed&quot; \
-storepass &quot;$STORE_PASSWORD&quot; -keypass &quot;$KEY_PASSWORD&quot; -noprompt
echo &quot;9. 删除临时文件......&quot;
rm &quot;$CERT_OUTPUT_PATH/ca-cert.srl&quot;
rm &quot;$CERT_OUTPUT_PATH/server-cert-signed&quot;
rm &quot;$CERT_OUTPUT_PATH/client-cert-signed&quot;
rm &quot;$CERT_OUTPUT_PATH/server-cert-file&quot;
rm &quot;$CERT_OUTPUT_PATH/client-cert-file&quot;
```
你可以把上面的代码保存成一个SHELL脚本然后在一台Broker上运行。该脚本主要的产出是4个文件分别是server.keystore.jks、server.truststore.jks、client.keystore.jks和client.truststore.jks。
你需要把以server开头的两个文件拷贝到集群中的所有Broker机器上把以client开头的两个文件拷贝到所有要连接Kafka集群的客户端应用程序机器上。
接着你要配置每个Broker的server.properties文件增加以下内容
```
listeners=SSL://localhost:9093
ssl.truststore.location=/Users/huxi/testenv/certificates/server.truststore.jks
ssl.truststore.password=test1234
ssl.keystore.location=/Users/huxi/testenv/certificates/server.keystore.jks
ssl.keystore.password=test1234
security.inter.broker.protocol=SSL
ssl.client.auth=required
ssl.key.password=test1234
```
现在我们启动Broker进程。倘若你发现无法启动或启动失败那么你需要检查一下报错信息看看和上面的哪些配置有关然后有针对性地进行调整。接下来我们来配置客户端的SSL。
首先我们要创建一个名为client-ssl.config的文件内容如下
```
security.protocol=SSL
ssl.truststore.location=/Users/huxi/testenv/certificates/client.truststore.jks
ssl.truststore.password=test1234
ssl.keystore.location=/Users/huxi/testenv/certificates/server.keystore.jks
ssl.keystore.password=test1234
ssl.key.password=test1234
ssl.endpoint.identification.algorithm=
```
注意一定要加上最后一行。因为自Kafka 2.0版本开始它默认会验证服务器端的主机名是否匹配Broker端证书里的主机名。如果你要禁掉此功能的话一定要将该参数设置为空字符串。
配置好这些你可以使用ConsoleConsumer和ConsoleProducer来测试一下Producer和Consumer是否能够正常工作。比如下列命令指定producer-config指向刚才我们创建的client-ssl配置文件。
```
$ bin/kafka-console-producer.sh --broker-list localhost:9093 --topic test --producer.config client-ssl.config
```
好了现在我们来说说ACL的配置。
如果你在运营一个云上的Kafka集群那么势必会面临多租户的问题。**除了设置合理的认证机制外为每个连接Kafka集群的客户端授予恰当的权限也是非常关键的**。现在我来给出一些最佳实践。
第一就像前面说的要开启ACL你需要设置authorizer.class.name=kafka.security.auth.SimpleAclAuthorizer。
第二我建议你采用白名单机制这样的话没有显式设置权限的用户就无权访问任何资源。也就是说在Kafka的server.properties文件中不要设置allow.everyone.if.no.acl.found=true。
第三你可以使用kafka-acls脚本为SSL用户授予集群的权限。我们以前面的例子来进行一下说明。
在配置SSL时我们指定用户的Distinguished Name为“CN=Xi Hu, OU=YourDept, O=YourCompany, L=Beijing, ST=Beijing, C=CN”。之前在设置Broker端参数时我们指定了security.inter.broker.protocol=SSL即强制指定Broker间的通讯也采用SSL加密。
如果不为指定的Distinguished Name授予集群操作的权限你是无法成功启动Broker的。因此你需要在启动Broker之前执行下面的命令
```
$ bin/kafka-acls.sh --authorizer-properties zookeeper.connect=localhost:2181 --add --allow-principal User:&quot;CN=Xi Hu,OU=YourDept,O=YourCompany,L=Beijing,ST=Beijing,C=CN&quot; --operation All --cluster
```
第四你要为客户端程序授予相应的权限比如为生产者授予producer权限为消费者授予consumer权限。假设客户端要访问的主题名字是test那么命令如下
```
$ bin/kafka-acls.sh --authorizer-properties zookeeper.connect=localhost:2181 --add --allow-principal User:&quot;CN=Xi Hu,OU=YourDept,O=YourCompany,L=Beijing,ST=Beijing,C=CN&quot; --producer --topic 'test'
```
```
$ bin/kafka-acls.sh --authorizer-properties zookeeper.connect=localhost:2181 --add --allow-principal User:&quot;CN=Xi Hu,OU=YourDept,O=YourCompany,L=Beijing,ST=Beijing,C=CN&quot; --consumer --topic 'test' --group '*'
```
注意这两条命令中的 --producer和 --consumer它们类似于一个快捷方式直接将Producer和Consumer常用的权限进行了一次性的授予。
作为云环境PaaS管理员除了以上这些必要的权限你最好不要把其他权限授予客户端比如创建主题的权限。总之你授予的权限越少你的Kafka集群就越安全。
## 小结
讲到这里我们就完整地把Kafka授权机制梳理了一遍。除此之外我还附赠了SSL端配置方法。希望你能将这两讲关于安全配置的内容结合起来学习打造一个超级安全的Kafka集群。
<img src="https://static001.geekbang.org/resource/image/39/66/39431082a84db9a3ed0dacd085f60f66.jpg" alt="">
## 开放讨论
Kafka提供的权限有很多种我们今天讨论的内容只覆盖了其中最重要的几个权限。如果要让一个客户端能够查询消费者组的提交位移数据你觉得应该授予它什么权限
欢迎写下你的思考和答案,我们一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@@ -0,0 +1,169 @@
<audio id="audio" title="35 | 跨集群备份解决方案MirrorMaker" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/d3/9b/d3ba7bf9197dcf37d13646b0749a009b.mp3"></audio>
你好我是胡夕。今天我要和你分享的主题是Kafka的跨集群数据镜像工具MirrorMaker。
一般情况下我们会使用一套Kafka集群来完成业务但有些场景确实会需要多套Kafka集群同时工作比如为了便于实现灾难恢复你可以在两个机房分别部署单独的Kafka集群。如果其中一个机房出现故障你就能很容易地把流量打到另一个正常运转的机房下。再比如你想为地理相近的客户提供低延时的消息服务而你的主机房又离客户很远这时你就可以在靠近客户的地方部署一套Kafka集群让这套集群服务你的客户从而提供低延时的服务。
如果要实现这些需求除了部署多套Kafka集群之外你还需要某种工具或框架来帮助你实现数据在集群间的拷贝或镜像。
值得注意的是,**通常我们把数据在单个集群下不同节点之间的拷贝称为备份,而把数据在集群间的拷贝称为镜像**Mirroring
今天我来重点介绍一下Apache Kafka社区提供的MirrorMaker工具它可以帮我们实现消息或数据从一个集群到另一个集群的拷贝。
## 什么是MirrorMaker
从本质上说MirrorMaker就是一个消费者+生产者的程序。消费者负责从源集群Source Cluster消费数据生产者负责向目标集群Target Cluster发送消息。整个镜像流程如下图所示
<img src="https://static001.geekbang.org/resource/image/a7/e2/a771601d702eb35187a0a8894307eee2.jpg" alt="">
MirrorMaker连接的源集群和目标集群会实时同步消息。当然你不要认为你只能使用一套MirrorMaker来连接上下游集群。事实上很多用户会部署多套集群用于实现不同的目的。
我们来看看下面这张图。图中部署了三套集群:左边的源集群负责主要的业务处理;右上角的目标集群可以用于执行数据分析;而右下角的目标集群则充当源集群的热备份。
<img src="https://static001.geekbang.org/resource/image/03/70/036955f42db6fe759849fb24a0d16070.jpg" alt="">
## 运行MirrorMaker
Kafka默认提供了MirrorMaker命令行工具kafka-mirror-maker脚本它的常见用法是指定生产者配置文件、消费者配置文件、线程数以及要执行数据镜像的主题正则表达式。比如下面的这个命令就是一个典型的MirrorMaker执行命令。
```
$ bin/kafka-mirror-maker.sh --consumer.config ./config/consumer.properties --producer.config ./config/producer.properties --num.streams 8 --whitelist &quot;.*&quot;
```
现在我来解释一下这条命令中各个参数的含义。
<li>
consumer.config参数。它指定了MirrorMaker中消费者的配置文件地址最主要的配置项是**bootstrap.servers**也就是该MirrorMaker从哪个Kafka集群读取消息。因为MirrorMaker有可能在内部创建多个消费者实例并使用消费者组机制因此你还需要设置group.id参数。另外我建议你额外配置auto.offset.reset=earliest否则的话MirrorMaker只会拷贝那些在它启动之后到达源集群的消息。
</li>
<li>
producer.config参数。它指定了MirrorMaker内部生产者组件的配置文件地址。通常来说Kafka Java Producer很友好你不需要配置太多参数。唯一的例外依然是**bootstrap.servers**,你必须显式地指定这个参数,配置拷贝的消息要发送到的目标集群。
</li>
<li>
num.streams参数。我个人觉得这个参数的名字很容易给人造成误解。第一次看到这个参数名的时候我一度以为MirrorMaker是用Kafka Streams组件实现的呢。其实并不是。这个参数就是告诉MirrorMaker要创建多少个KafkaConsumer实例。当然它使用的是多线程的方案即在后台创建并启动多个线程每个线程维护专属的消费者实例。在实际使用时你可以根据你的机器性能酌情设置多个线程。
</li>
<li>
whitelist参数。如命令所示这个参数接收一个正则表达式。所有匹配该正则表达式的主题都会被自动地执行镜像。在这个命令中我指定了“.*”,这表明我要同步源集群上的所有主题。
</li>
## MirrorMaker配置实例
现在我就在测试环境中为你演示一下MirrorMaker的使用方法。
演示的流程大致是这样的首先我们会启动两套Kafka集群它们是单节点的伪集群监听端口分别是9092和9093之后我们会启动MirrorMaker工具实时地将9092集群上的消息同步镜像到9093集群上最后我们启动额外的消费者来验证消息是否拷贝成功。
### 第1步启动两套Kafka集群
启动日志如下所示:
>
<p>[2019-07-23 17:01:40,544] INFO Kafka version: 2.3.0 (org.apache.kafka.common.utils.AppInfoParser)<br>
[2019-07-23 17:01:40,544] INFO Kafka commitId: fc1aaa116b661c8a (org.apache.kafka.common.utils.AppInfoParser)<br>
[2019-07-23 17:01:40,544] INFO Kafka startTimeMs: 1563872500540 (org.apache.kafka.common.utils.AppInfoParser)<br>
[2019-07-23 17:01:40,545] INFO [KafkaServer id=0] started (kafka.server.KafkaServer)</p>
>
<p>[2019-07-23 16:59:59,462] INFO Kafka version: 2.3.0 (org.apache.kafka.common.utils.AppInfoParser)<br>
[2019-07-23 16:59:59,462] INFO Kafka commitId: fc1aaa116b661c8a (org.apache.kafka.common.utils.AppInfoParser)<br>
[2019-07-23 16:59:59,462] INFO Kafka startTimeMs: 1563872399459 (org.apache.kafka.common.utils.AppInfoParser)<br>
[2019-07-23 16:59:59,463] INFO [KafkaServer id=1] started (kafka.server.KafkaServer)</p>
### 第2步启动MirrorMaker工具
在启动MirrorMaker工具之前我们必须准备好刚刚提过的Consumer配置文件和Producer配置文件。它们的内容分别如下
```
consumer.properties
bootstrap.servers=localhost:9092
group.id=mirrormaker
auto.offset.reset=earliest
```
```
producer.properties:
bootstrap.servers=localhost:9093
```
现在我们来运行命令启动MirrorMaker工具。
```
$ bin/kafka-mirror-maker.sh --producer.config ../producer.config --consumer.config ../consumer.config --num.streams 4 --whitelist &quot;.*&quot;
WARNING: The default partition assignment strategy of the mirror maker will change from 'range' to 'roundrobin' in an upcoming release (so that better load balancing can be achieved). If you prefer to make this switch in advance of that release add the following to the corresponding config: 'partition.assignment.strategy=org.apache.kafka.clients.consumer.RoundRobinAssignor'
```
请你一定要仔细阅读这个命令输出中的警告信息。这个警告的意思是在未来版本中MirrorMaker内部消费者会使用轮询策略Round-robin来为消费者实例分配分区现阶段使用的默认策略依然是基于范围的分区策略Range。Range策略的思想很朴素它是将所有分区根据一定的顺序排列在一起每个消费者依次顺序拿走各个分区。
Round-robin策略的推出时间要比Range策略晚。通常情况下我们可以认为社区推出的比较晚的分区分配策略会比之前的策略好。这里的好指的是能实现更均匀的分配效果。该警告信息的最后一部分内容提示我们**如果我们想提前“享用”轮询策略需要手动地在consumer.properties文件中增加partition.assignment.strategy的设置**。
### 第3步验证消息是否拷贝成功
好了启动MirrorMaker之后我们可以向源集群发送并消费一些消息然后验证是否所有的主题都能正确地同步到目标集群上。
假设我们在源集群上创建了一个4分区的主题test随后使用kafka-producer-perf-test脚本模拟发送了500万条消息。现在我们使用下面这两条命令来查询一下目标Kafka集群上是否存在名为test的主题并且成功地镜像了这些消息。
```
$ bin/kafka-run-class.sh kafka.tools.GetOffsetShell --broker-list localhost:9093 --topic test --time -2
test:0:0
```
```
$ bin/kafka-run-class.sh kafka.tools.GetOffsetShell --broker-list localhost:9093 --topic test --time -1
test:0:5000000
```
-1和-2分别表示获取某分区最新的位移和最早的位移这两个位移值的差值就是这个分区当前的消息数在这个例子中差值是500万条。这说明主题test当前共写入了500万条消息。换句话说MirrorMaker已经成功地把这500万条消息同步到了目标集群上。
讲到这里你一定会觉得很奇怪吧我们明明在源集群创建了一个4分区的主题为什么到了目标集群就变成单分区了呢
原因很简单。**MirrorMaker在执行消息镜像的过程中如果发现要同步的主题在目标集群上不存在的话它就会根据Broker端参数num.partitions和default.replication.factor的默认值自动将主题创建出来**。在这个例子中我们在目标集群上没有创建过任何主题因此在镜像开始时MirrorMaker自动创建了一个名为test的单分区单副本的主题。
**在实际使用场景中,我推荐你提前把要同步的所有主题按照源集群上的规格在目标集群上等价地创建出来**。否则,极有可能出现刚刚的这种情况,这会导致一些很严重的问题。比如原本在某个分区的消息同步到了目标集群以后,却位于其他的分区中。如果你的消息处理逻辑依赖于这样的分区映射,就必然会出现问题。
除了常规的Kafka主题之外MirrorMaker默认还会同步内部主题比如在专栏前面我们频繁提到的位移主题。MirrorMaker在镜像位移主题时如果发现目标集群尚未创建该主题它就会根据Broker端参数offsets.topic.num.partitions和offsets.topic.replication.factor的值来制定该主题的规格。默认配置是50个分区每个分区3个副本。
在0.11.0.0版本之前Kafka不会严格依照offsets.topic.replication.factor参数的值。这也就是说如果你设置了该参数值为3而当前存活的Broker数量少于3位移主题依然能被成功创建只是副本数取该参数值和存活Broker数之间的较小值。
这个缺陷在0.11.0.0版本被修复了这就意味着Kafka会严格遵守你设定的参数值如果发现存活Broker数量小于参数值就会直接抛出异常告诉你主题创建失败。因此在使用MirrorMaker时你一定要确保这些配置都是合理的。
## 其他跨集群镜像方案
讲到这里MirrorMaker的主要功能我就介绍完了。你大致可以感觉到执行MirrorMaker的命令是很简单的而且它提供的功能很有限。实际上它的运维成本也比较高比如主题的管理就非常不便捷同时也很难将其管道化。
基于这些原因,业界很多公司选择自己开发跨集群镜像工具。我来简单介绍几个。
1.**Uber的uReplicator工具**
Uber公司之前也是使用MirrorMaker的但是在使用过程中他们发现了一些明显的缺陷比如MirrorMaker中的消费者使用的是消费者组的机制这不可避免地会碰到很多Rebalance的问题。
为此Uber自己研发了uReplicator。它使用Apache Helix作为集中式的主题分区管理组件并且重写了消费者程序来替换之前MirrorMaker下的消费者使用Helix来管理分区的分配从而避免了Rebalance的各种问题。
讲到这里我个人有个小小的感慨社区最近正在花大力气去优化消费者组机制力求改善因Rebalance导致的各种场景但其实其他框架开发者反而是不用Group机制的。他们宁愿自己开发一套机制来维护分区分配的映射。这些都说明Kafka中的消费者组还是有很大的提升空间的。
另外Uber专门写了一篇[博客](https://eng.uber.com/ureplicator/)详细说明了uReplicator的设计原理并罗列了社区的MirrorMaker工具的一些缺陷以及uReplicator的应对方法。我建议你一定要读一读这篇博客。
2.**LinkedIn开发的Brooklin Mirror Maker工具**
针对现有MirrorMaker工具不易实现管道化的缺陷这个工具进行了有针对性的改进同时也对性能做了一些优化。目前在LinkedIn公司Brooklin Mirror Maker已经完全替代了社区版的MirrorMaker。如果你想深入了解它是如何做到的我给你推荐一篇[博客](https://www.slideshare.net/jyouping/brooklin-mirror-maker-how-and-why-we-moved-away-from-kafka-mirror-maker),你可以详细阅读一下。
3.**Confluent公司研发的Replicator工具**
这个工具提供的是企业级的跨集群镜像方案是市面上已知的功能最为强大的工具可以便捷地为你提供Kafka主题在不同集群间的迁移。除此之外Replicator工具还能自动在目标集群上创建与源集群上配置一模一样的主题极大地方便了运维管理。不过凡事有利就有弊Replicator是要收费的。如果你所在的公司预算充足而且你们关心数据在多个集群甚至是多个数据中心间的迁移质量不妨关注一下Confluent公司的[Replicator工具](https://www.confluent.io/confluent-replicator/)。
## 小结
好了我们总结一下今天所讲的MirrorMaker。它是Apache Kafka社区提供的跨集群镜像解决方案主要实现将Kafka消息实时从一个集群同步复制或镜像到另一个集群。你可以把MirrorMaker应用到很多实际的场景中比如数据备份、主备集群等。MirrorMaker本身功能简单应用灵活但也有运维成本高、性能差等劣势因此业界有厂商研发了自己的镜像工具。你可以根据自身的业务需求选择合适的工具来帮助你完成跨集群的数据备份。
<img src="https://static001.geekbang.org/resource/image/c0/5e/c0b75891bf37c1086dea98358c481d5e.jpg" alt="">
## 开放讨论
今天我只演示了MirrorMaker最基本的使用方法即把消息原样搬移。如果我们想在消息被镜像前做一些处理比如修改消息体内容那么我们应该如何实现呢提示指定kafka-mirror-maker脚本的 --message.handler参数。
欢迎写下你的思考和答案,我们一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@@ -0,0 +1,122 @@
<audio id="audio" title="36 | 你应该怎么监控Kafka" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/51/08/51aa021a7b8a6ab04426ed50c9e7ae08.mp3"></audio>
你好我是胡夕。今天我要和你分享的主题是如何监控Kafka。
监控Kafka历来都是个老大难的问题。无论是在我维护的微信公众号还是Kafka QQ群里面大家问得最多的问题一定是Kafka的监控。大家提问的内容看似五花八门但真正想了解的其实都是监控这点事也就是我应该监控什么怎么监控。那么今天我们就来详细聊聊这件事。
我个人认为和头疼医头、脚疼医脚的问题类似在监控Kafka时如果我们只监控Broker的话就难免以偏概全。单个Broker启动的进程虽然属于Kafka应用但它也是一个普通的Java进程更是一个操作系统进程。因此我觉得有必要从Kafka主机、JVM和Kafka集群本身这三个维度进行监控。
## 主机监控
主机级别的监控,往往是揭示线上问题的第一步。**所谓主机监控指的是监控Kafka集群Broker所在的节点机器的性能**。通常来说一台主机上运行着各种各样的应用进程这些进程共同使用主机上的所有硬件资源比如CPU、内存或磁盘等。
常见的主机监控指标包括但不限于以下几种:
- 机器负载Load
- CPU使用率
- 内存使用率包括空闲内存Free Memory和已使用内存Used Memory
- 磁盘I/O使用率包括读使用率和写使用率
- 网络I/O使用率
- TCP连接数
- 打开文件数
- inode使用情况
考虑到我们并不是要系统地学习调优与监控主机性能因此我并不打算对上面的每一个指标都进行详细解释我重点分享一下机器负载和CPU使用率的监控方法。我会以Linux平台为例来进行说明其他平台应该也是类似的。
首先我们来看一张图片。我在Kafka集群的某台Broker所在的主机上运行top命令输出的内容如下图所示
<img src="https://static001.geekbang.org/resource/image/00/e0/00f0ead463b17e667d09b6cea4e42de0.png" alt="">
在图片的右上角我们可以看到load average的3个值4.852.76和1.26它们分别代表过去1分钟、过去5分钟和过去15分钟的Load平均值。在这个例子中我的主机总共有4个CPU核但Load值却达到了4.85这就说明一定有进程暂时“抢不到”任何CPU资源。同时Load值一直在增加也说明这台主机上的负载越来越大。
举这个例子其实我真正想说的是CPU使用率。很多人把top命令中“%CPU”列的输出值当作CPU使用率。比如在上面这张图中PID为2637的Java进程是Broker进程它对应的“%CPU”的值是102.3。你不要认为这是CPU的真实使用率这列值的真实含义是进程使用的所有CPU的平均使用率只是top命令在显示的时候转换成了单个CPU。因此如果是在多核的主机上这个值就可能会超过100。在这个例子中我的主机有4个CPU核总CPU使用率是102.3那么平均每个CPU的使用率大致是25%。
## JVM监控
除了主机监控之外另一个重要的监控维度就是JVM监控。Kafka Broker进程是一个普通的Java进程所有关于JVM的监控手段在这里都是适用的。
监控JVM进程主要是为了让你全面地了解你的应用程序Know Your Application。具体到Kafka而言就是全面了解Broker进程。比如Broker进程的堆大小HeapSize是多少、各自的新生代和老年代是多大用的是什么GC回收器这些监控指标和配置参数林林总总通常你都不必全部重点关注但你至少要搞清楚Broker端JVM进程的Minor GC和Full GC的发生频率和时长、活跃对象的总大小和JVM上应用线程的大致总数因为这些数据都是你日后调优Kafka Broker的重要依据。
我举个简单的例子。假设一台主机上运行的Broker进程在经历了一次Full GC之后堆上存活的活跃对象大小是700MB那么在实际场景中你几乎可以安全地将老年代堆大小设置成该数值的1.5倍或2倍即大约1.4GB。不要小看700MB这个数字它是我们设定Broker堆大小的重要依据
很多人会有这样的疑问我应该怎么设置Broker端的堆大小呢其实这就是最合理的评估方法。试想一下如果你的Broker在Full GC之后存活了700MB的数据而你设置了堆大小为16GB这样合理吗对一个16GB大的堆执行一次GC要花多长时间啊
因此我们来总结一下。要做到JVM进程监控有3个指标需要你时刻关注
1. Full GC发生频率和时长。这个指标帮助你评估Full GC对Broker进程的影响。长时间的停顿会令Broker端抛出各种超时异常。
1. 活跃对象大小。这个指标是你设定堆大小的重要依据同时它还能帮助你细粒度地调优JVM各个代的堆大小。
1. 应用线程总数。这个指标帮助你了解Broker进程对CPU的使用情况。
总之你对Broker进程了解得越透彻你所做的JVM调优就越有效果。
谈到具体的监控前两个都可以通过GC日志来查看。比如下面的这段GC日志就说明了GC后堆上的存活对象大小。
>
2019-07-30T09:13:03.809+0800: 552.982: [GC cleanup 827M-&gt;645M(1024M), 0.0019078 secs]
这个Broker JVM进程默认使用了G1的GC算法当cleanup步骤结束后堆上活跃对象大小从827MB缩减成645MB。另外你可以根据前面的时间戳来计算每次GC的间隔和频率。
自0.9.0.0版本起社区将默认的GC收集器设置为G1而G1中的Full GC是由单线程执行的速度非常慢。因此**你一定要监控你的Broker GC日志即以kafkaServer-gc.log开头的文件**。注意不要出现Full GC的字样。一旦你发现Broker进程频繁Full GC可以开启G1的-XX:+PrintAdaptiveSizePolicy开关让JVM告诉你到底是谁引发了Full GC。
## 集群监控
说完了主机和JVM监控现在我来给出监控Kafka集群的几个方法。
**1.查看Broker进程是否启动端口是否建立。**
千万不要小看这一点。在很多容器化的Kafka环境中比如使用Docker启动Kafka Broker时容器虽然成功启动了但是里面的网络设置如果配置有误就可能会出现进程已经启动但端口未成功建立监听的情形。因此你一定要同时检查这两点确保服务正常运行。
**2.查看Broker端关键日志。**
这里的关键日志主要涉及Broker端服务器日志server.log控制器日志controller.log以及主题分区状态变更日志state-change.log。其中server.log是最重要的你最好时刻对它保持关注。很多Broker端的严重错误都会在这个文件中被展示出来。因此如果你的Kafka集群出现了故障你要第一时间去查看对应的server.log寻找和定位故障原因。
**3.查看Broker端关键线程的运行状态。**
这些关键线程的意外挂掉往往无声无息但是却影响巨大。比方说Broker后台有个专属的线程执行Log Compaction操作由于源代码的Bug这个线程有时会无缘无故地“死掉”社区中很多Jira都曾报出过这个问题。当这个线程挂掉之后作为用户的你不会得到任何通知Kafka集群依然会正常运转只是所有的Compaction操作都不能继续了这会导致Kafka内部的位移主题所占用的磁盘空间越来越大。因此我们有必要对这些关键线程的状态进行监控。
可是一个Kafka Broker进程会启动十几个甚至是几十个线程我们不可能对每个线程都做到实时监控。所以我跟你分享一下我认为最重要的两类线程。在实际生产环境中监控这两类线程的运行情况是非常有必要的。
- Log Compaction线程这类线程是以kafka-log-cleaner-thread开头的。就像前面提到的此线程是做日志Compaction的。一旦它挂掉了所有Compaction操作都会中断但用户对此通常是无感知的。
- 副本拉取消息的线程通常以ReplicaFetcherThread开头。这类线程执行Follower副本向Leader副本拉取消息的逻辑。如果它们挂掉了系统会表现为对应的Follower副本不再从Leader副本拉取消息因而Follower副本的Lag会越来越大。
不论你是使用jstack命令还是其他的监控框架我建议你时刻关注Broker进程中这两类线程的运行状态。一旦发现它们状态有变就立即查看对应的Kafka日志定位原因因为这通常都预示会发生较为严重的错误。
**4.查看Broker端的关键JMX指标。**
Kafka提供了超多的JMX指标供用户实时监测我来介绍几个比较重要的Broker端JMX指标
- BytesIn/BytesOut即Broker端每秒入站和出站字节数。你要确保这组值不要接近你的网络带宽否则这通常都表示网卡已被“打满”很容易出现网络丢包的情形。
- NetworkProcessorAvgIdlePercent即网络线程池线程平均的空闲比例。通常来说你应该确保这个JMX值长期大于30%。如果小于这个值就表明你的网络线程池非常繁忙你需要通过增加网络线程数或将负载转移给其他服务器的方式来给该Broker减负。
- RequestHandlerAvgIdlePercent即I/O线程池线程平均的空闲比例。同样地如果该值长期小于30%你需要调整I/O线程池的数量或者减少Broker端的负载。
- UnderReplicatedPartitions即未充分备份的分区数。所谓未充分备份是指并非所有的Follower副本都和Leader副本保持同步。一旦出现了这种情况通常都表明该分区有可能会出现数据丢失。因此这是一个非常重要的JMX指标。
- ISRShrink/ISRExpand即ISR收缩和扩容的频次指标。如果你的环境中出现ISR中副本频繁进出的情形那么这组值一定是很高的。这时你要诊断下副本频繁进出ISR的原因并采取适当的措施。
- ActiveControllerCount即当前处于激活状态的控制器的数量。正常情况下Controller所在Broker上的这个JMX指标值应该是1其他Broker上的这个值是0。如果你发现存在多台Broker上该值都是1的情况一定要赶快处理处理方式主要是查看网络连通性。这种情况通常表明集群出现了脑裂。脑裂问题是非常严重的分布式故障Kafka目前依托ZooKeeper来防止脑裂。但一旦出现脑裂Kafka是无法保证正常工作的。
其实Broker端还有很多很多JMX指标除了上面这些重要指标你还可以根据自己业务的需要去官网查看其他JMX指标把它们集成进你的监控框架。
**5.监控Kafka客户端。**
客户端程序的性能同样需要我们密切关注。不管是生产者还是消费者我们首先要关心的是客户端所在的机器与Kafka Broker机器之间的**网络往返时延**Round-Trip TimeRTT。通俗点说就是你要在客户端机器上ping一下Broker主机IP看看RTT是多少。
我曾经服务过一个客户他的Kafka生产者TPS特别低。我登到机器上一看发现RTT是1秒。在这种情况下无论你怎么调优Kafka参数效果都不会太明显降低网络时延反而是最直接有效的办法。
除了RTT客户端程序也有非常关键的线程需要你时刻关注。对于生产者而言有一个以kafka-producer-network-thread开头的线程是你要实时监控的。它是负责实际消息发送的线程。一旦它挂掉了Producer将无法正常工作但你的Producer进程不会自动挂掉因此你有可能感知不到。对于消费者而言心跳线程事关Rebalance也是必须要监控的一个线程。它的名字以kafka-coordinator-heartbeat-thread开头。
除此之外客户端有一些很重要的JMX指标可以实时告诉你它们的运行情况。
从Producer角度你需要关注的JMX指标是request-latency即消息生产请求的延时。这个JMX最直接地表征了Producer程序的TPS而从Consumer角度来说records-lag和records-lead是两个重要的JMX指标。我们在专栏[第22讲](https://time.geekbang.org/column/article/109238)解释过这两个指标的含义这里我就不再赘述了。总之它们直接反映了Consumer的消费进度。如果你使用了Consumer Group那么有两个额外的JMX指标需要你关注下一个是join rate另一个是sync rate。它们说明了Rebalance的频繁程度。如果在你的环境中它们的值很高那么你就需要思考下Rebalance频繁发生的原因了。
## 小结
好了我们来小结一下。今天我介绍了监控Kafka的方方面面。除了监控Kafka集群我还推荐你从主机和JVM的维度进行监控。对主机的监控往往是我们定位和发现问题的第一步。JVM监控同样重要。要知道很多Java进程碰到的性能问题是无法通过调整Kafka参数是解决的。最后我罗列了一些比较重要的Kafka JMX指标。在下一讲中我会专门介绍一下如何使用各种工具来查看这些JMX指标。
<img src="https://static001.geekbang.org/resource/image/28/93/28e6d8c2459b5d123f443173ac122c93.jpg" alt="">
## 开放讨论
请分享一下你在监控Kafka方面的心得以及你的运维技巧。
欢迎写下你的思考和答案,我们一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@@ -0,0 +1,164 @@
<audio id="audio" title="37 | 主流的Kafka监控框架" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/12/32/1242204af096f82f9c4f14731d199732.mp3"></audio>
你好我是胡夕。今天我要和你分享的主题是那些主流的Kafka监控框架。
在上一讲中我们重点讨论了如何监控Kafka集群主要是侧重于讨论监控原理和监控方法。今天我们来聊聊具体的监控工具或监控框架。
令人有些遗憾的是Kafka社区似乎一直没有在监控框架方面投入太多的精力。目前Kafka的新功能提议已超过500个但没有一个提议是有关监控框架的。当然Kafka的确提供了超多的JMX指标只是单独查看这些JMX指标往往不是很方便我们还是要依赖于框架统一地提供性能监控。
也许正是由于社区的这种“不作为”很多公司和个人都自行着手开发Kafka监控框架其中并不乏佼佼者。今天我们就来全面地梳理一下主流的监控框架。
## JMXTool工具
首先我向你推荐JMXTool工具。严格来说它并不是一个框架只是社区自带的一个工具罢了。JMXTool工具能够实时查看Kafka JMX指标。倘若你一时找不到合适的框架来做监控JMXTool可以帮你“临时救急”一下。
Kafka官网没有JMXTool的任何介绍你需要运行下面的命令来获取它的使用方法的完整介绍。
```
bin/kafka-run-class.sh kafka.tools.JmxTool
```
JMXTool工具提供了很多参数但你不必完全了解所有的参数。我把主要的参数说明列在了下面的表格里你至少要了解一下这些参数的含义。
<img src="https://static001.geekbang.org/resource/image/79/d3/795399a24e665c1bf744085b5344f5d3.jpg" alt="">
现在,我举一个实际的例子来说明一下如何运行这个命令。
假设你要查询Broker端每秒入站的流量即所谓的JMX指标BytesInPerSec这个JMX指标能帮助你查看Broker端的入站流量负载如果你发现这个值已经接近了你的网络带宽这就说明该Broker的入站负载过大。你需要降低该Broker的负载或者将一部分负载转移到其他Broker上。
下面这条命令表示每5秒查询一次过去1分钟的BytesInPerSec均值。
```
bin/kafka-run-class.sh kafka.tools.JmxTool --object-name kafka.server:type=BrokerTopicMetrics,name=BytesInPerSec --jmx-url service:jmx:rmi:///jndi/rmi://:9997/jmxrmi --date-format &quot;YYYY-MM-dd HH:mm:ss&quot; --attributes OneMinuteRate --reporting-interval 1000
```
在这条命令中,有几点需要你注意一下。
- 设置 --jmx-url参数的值时需要指定JMX端口。在这个例子中端口是9997在实际操作中你需要指定你的环境中的端口。
- 由于我是直接在Broker端运行的命令因此就把主机名忽略掉了。如果你是在其他机器上运行这条命令你要记得带上要连接的主机名。
- 关于 --object-name参数值的完整写法我们可以直接在Kafka官网上查询。我们在前面说过Kafka提供了超多的JMX指标你需要去官网学习一下它们的用法。我以ActiveController JMX指标为例介绍一下学习的方法。你可以在官网上搜索关键词ActiveController找到它对应的 --object-name即kafka.controller:type=KafkaController,name=ActiveControllerCount这样你就可以执行下面的脚本来查看当前激活的Controller数量。
```
$ bin/kafka-run-class.sh kafka.tools.JmxTool --object-name kafka.controller:type=KafkaController,name=ActiveControllerCount --jmx-url service:jmx:rmi:///jndi/rmi://:9997/jmxrmi --date-format &quot;YYYY-MM-dd HH:mm:ss&quot; --reporting-interval 1000
Trying to connect to JMX url: service:jmx:rmi:///jndi/rmi://:9997/jmxrmi.
&quot;time&quot;,&quot;kafka.controller:type=KafkaController,name=ActiveControllerCount:Value&quot;
2019-08-05 15:08:30,1
2019-08-05 15:08:31,1
```
总体来说JMXTool是社区自带的一个小工具对于一般简单的监控场景它还能应付但是它毕竟功能有限复杂的监控整体解决方案还是要依靠监控框架。
## Kafka Manager
说起Kafka监控框架最有名气的当属Kafka Manager了。Kafka Manager是雅虎公司于2015年开源的一个Kafka监控框架。这个框架用Scala语言开发而成主要用于管理和监控Kafka集群。
应该说Kafka Manager是目前众多Kafka监控工具中最好的一个无论是界面展示内容的丰富程度还是监控功能的齐全性它都是首屈一指的。不过目前该框架已经有4个月没有更新了而且它的活跃的代码维护者只有三四个人因此很多Bug或问题都不能及时得到修复更重要的是它无法追上Apache Kafka版本的更迭速度。
当前Kafka Manager最新版是2.0.0.2。在其Github官网上下载tar.gz包之后我们执行解压缩可以得到kafka-manager-2.0.0.2目录。
之后我们需要运行sbt工具来编译Kafka Manager。sbt是专门用于构建Scala项目的编译构建工具类似于我们熟知的Maven和Gradle。Kafka Manager自带了sbt命令我们直接运行它构建项目就可以了
```
./sbt clean dist
```
经过漫长的等待之后你应该可以看到项目已经被成功构建了。你可以在Kafka Manager的target/universal目录下找到生成的zip文件把它解压然后修改里面的conf/application.conf文件中的kafka-manager.zkhosts项让它指向你环境中的ZooKeeper地址比如
```
kafka-manager.zkhosts=&quot;localhost:2181&quot;
```
之后运行以下命令启动Kafka Manager
```
bin/kafka-manager -Dconfig.file=conf/application.conf -Dhttp.port=8080
```
该命令指定了要读取的配置文件以及要启动的监听端口。现在我们打开浏览器输入对应的IP:8080就可以访问Kafka Manager了。下面这张图展示了我在Kafka Manager中添加集群的主界面。
<img src="https://static001.geekbang.org/resource/image/b4/66/b492ae08e527e295d29da65d07ad9566.png" alt="">
注意要勾选上Enable JMX Polling这样你才能监控Kafka的各种JMX指标。下图就是Kafka Manager框架的主界面。
<img src="https://static001.geekbang.org/resource/image/99/14/990944c78f22adc6f6c836d489eade14.png" alt="">
从这张图中我们可以发现Kafka Manager清晰地列出了当前监控的Kafka集群的主题数量、Broker数量等信息。你可以点击顶部菜单栏的各个条目去探索其他功能。
除了丰富的监控功能之外Kafka Manager还提供了很多运维管理操作比如执行主题的创建、Preferred Leader选举等。在生产环境中这可能是一把双刃剑毕竟这意味着每个访问Kafka Manager的人都能执行这些运维操作。这显然是不能被允许的。因此很多Kafka Manager用户都有这样一个诉求把Kafka Manager变成一个纯监控框架关闭非必要的管理功能。
庆幸的是Kafka Manager提供了这样的功能。**你可以修改config下的application.conf文件删除application.features中的值**。比如如果我想禁掉Preferred Leader选举功能那么我就可以删除对应KMPreferredReplicaElectionFeature项。删除完之后我们重启Kafka Manager再次进入到主界面我们就可以发现之前的Preferred Leader Election菜单项已经没有了。
<img src="https://static001.geekbang.org/resource/image/16/01/16b5ac5eeb4f32f872265ec91d130401.png" alt="">
总之作为一款非常强大的Kafka开源监控框架Kafka Manager提供了丰富的实时监控指标以及适当的管理功能非常适合一般的Kafka集群监控值得你一试。
## Burrow
我要介绍的第二个Kafka开源监控框架是Burrow。**Burrow是LinkedIn开源的一个专门监控消费者进度的框架**。事实上当初其开源时我对它还是挺期待的。毕竟是LinkedIn公司开源的一个框架而LinkedIn公司又是Kafka创建并发展壮大的地方。Burrow应该是有机会成长为很好的Kafka监控框架的。
然而令人遗憾的是它后劲不足发展非常缓慢目前已经有几个月没有更新了。而且这个框架是用Go写的安装时要求必须有Go运行环境所以Burrow在普及率上不如其他框架。另外Burrow没有UI界面只是开放了一些HTTP Endpoint这对于“想偷懒”的运维来说更是一个减分项。
如果你要安装Burrow必须要先安装Golang语言环境然后依次运行下列命令去安装Burrow
```
$ go get github.com/linkedin/Burrow
$ cd $GOPATH/src/github.com/linkedin/Burrow
$ dep ensure
$ go install
```
等一切准备就绪执行Burrow启动命令就可以了。
```
$GOPATH/bin/Burrow --config-dir /path/containing/config
```
总体来说Burrow目前提供的功能还十分有限普及率和知名度都是比较低的。不过它的好处是该项目的主要贡献者是LinkedIn团队维护Kafka集群的主要负责人所以质量是很有保证的。如果你恰好非常熟悉Go语言生态那么不妨试用一下Burrow。
## JMXTrans + InfluxDB + Grafana
除了刚刚说到的专属开源Kafka监控框架之外其实现在更流行的做法是**在一套通用的监控框架中监控Kafka**,比如使用**JMXTrans + InfluxDB + Grafana的组合**。由于Grafana支持对**JMX指标**的监控因此很容易将Kafka各种JMX指标集成进来。
我们来看一张生产环境中的监控截图。图中集中了很多监控指标比如CPU使用率、GC收集数据、内存使用情况等。除此之外这个仪表盘面板还囊括了很多关键的Kafka JMX指标比如BytesIn、BytesOut和每秒消息数等。将这么多数据统一集成进一个面板上直观地呈现出来是这套框架非常鲜明的特点。
<img src="https://static001.geekbang.org/resource/image/22/5a/22f68c477d80919f4170da11c4fc8d5a.jpeg" alt="">
与Kafka Manager相比这套监控框架的优势在于你可以在一套监控框架中同时监控企业的多个关键技术组件。特别是**对于那些已经搭建了该监控组合的企业来说,直接复用这套框架可以极大地节省运维成本,不失为一个好的选择**。
## Confluent Control Center
最后我们来说说Confluent公司发布的Control Center。这是目前已知的最强大的Kafka监控框架了。
**Control Center不但能够实时地监控Kafka集群而且还能够帮助你操作和搭建基于Kafka的实时流处理应用。更棒的是Control Center提供了统一式的主题管理功能。你可以在这里享受到Kafka主题和Schema的一站式管理服务。**
下面这张图展示了Control Center的主题管理主界面。从这张图中我们可以直观地观测到整个Kafka集群的主题数量、ISR副本数量、各个主题对应的TPS等数据。当然Control Center提供的功能远不止这些你能想到的所有Kafka运维管理和监控功能Control Center几乎都能提供。
<img src="https://static001.geekbang.org/resource/image/f0/e2/f067a91f29d24c181ddc19ace163f3e2.png" alt="">
不过如果你要使用Control Center就必须使用Confluent Kafka Platform企业版。换句话说Control Center不是免费的你需要付费才能使用。如果你需要一套很强大的监控框架你可以登录Confluent公司官网去订购这套真正意义上的企业级Kafka监控框架。
## 小结
其实除了今天我介绍的Kafka Manager、Burrow、Grafana和Control Center之外市面上还散落着很多开源的Kafka监控框架比如Kafka Monitor、Kafka Offset Monitor等。不过这些框架基本上已经停止更新了有的框架甚至好几年都没有人维护了因此我就不详细展开了。如果你是一名开源爱好者可以试着到开源社区中贡献代码帮助它们重新焕发活力。
值得一提的是国内最近有个Kafka Eagle框架非常不错。它是国人维护的而且目前还在积极地演进着。根据Kafka Eagle官网的描述它支持最新的Kafka 2.x版本除了提供常规的监控功能之外还开放了告警功能Alert非常值得一试。
总之每个框架都有自己的特点和价值。Kafka Manager框架适用于基本的Kafka监控Grafana+InfluxDB+JMXTrans的组合适用于已经具有较成熟框架的企业。对于其他的几个监控框架你可以把它们作为这两个方案的补充加入到你的监控解决方案中。
<img src="https://static001.geekbang.org/resource/image/51/7a/514225120dca48f08b20b09400217b7a.jpg" alt="">
## 开放讨论
如果想知道某台Broker上是否存在请求积压我们应该监控哪个JMX指标
欢迎写下你的思考和答案,我们一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@@ -0,0 +1,145 @@
<audio id="audio" title="38 | 调优Kafka你做到了吗" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/33/10/33db1bd2ffd88dbe7b57194ecf0c7410.mp3"></audio>
你好我是胡夕。今天我要和你分享的主题是如何调优Kafka。
## 调优目标
在做调优之前我们必须明确优化Kafka的目标是什么。通常来说调优是为了满足系统常见的非功能性需求。在众多的非功能性需求中性能绝对是我们最关心的那一个。不同的系统对性能有不同的诉求比如对于数据库用户而言性能意味着请求的响应时间用户总是希望查询或更新请求能够被更快地处理完并返回。
对Kafka而言性能一般是指吞吐量和延时。
吞吐量也就是TPS是指Broker端进程或Client端应用程序每秒能处理的字节数或消息数这个值自然是越大越好。
延时和我们刚才说的响应时间类似它表示从Producer端发送消息到Broker端持久化完成之间的时间间隔。这个指标也可以代表端到端的延时End-to-EndE2E也就是从Producer发送消息到Consumer成功消费该消息的总时长。和TPS相反我们通常希望延时越短越好。
总之高吞吐量、低延时是我们调优Kafka集群的主要目标一会儿我们会详细讨论如何达成这些目标。在此之前我想先谈一谈优化漏斗的问题。
## 优化漏斗
优化漏斗是一个调优过程中的分层漏斗,我们可以在每一层上执行相应的优化调整。总体来说,层级越靠上,其调优的效果越明显,整体优化效果是自上而下衰减的,如下图所示:
<img src="https://static001.geekbang.org/resource/image/94/59/94486dc0eb55b68855478ef7e5709359.png" alt="">
**第1层应用程序层**。它是指优化Kafka客户端应用程序代码。比如使用合理的数据结构、缓存计算开销大的运算结果抑或是复用构造成本高的对象实例等。这一层的优化效果最为明显通常也是比较简单的。
**第2层框架层**。它指的是合理设置Kafka集群的各种参数。毕竟直接修改Kafka源码进行调优并不容易但根据实际场景恰当地配置关键参数的值还是很容易实现的。
**第3层JVM层**。Kafka Broker进程是普通的JVM进程各种对JVM的优化在这里也是适用的。优化这一层的效果虽然比不上前两层但有时也能带来巨大的改善效果。
**第4层操作系统层**。对操作系统层的优化很重要,但效果往往不如想象得那么好。与应用程序层的优化效果相比,它是有很大差距的。
## 基础性调优
接下来我就来分别介绍一下优化漏斗的4个分层的调优。
### 操作系统调优
我先来说说操作系统层的调优。在操作系统层面你最好在挂载Mount文件系统时禁掉atime更新。atime的全称是access time记录的是文件最后被访问的时间。记录atime需要操作系统访问inode资源而禁掉atime可以避免inode访问时间的写入操作减少文件系统的写操作数。你可以执行**mount -o noatime命令**进行设置。
至于文件系统我建议你至少选择ext4或XFS。尤其是XFS文件系统它具有高性能、高伸缩性等特点特别适用于生产服务器。值得一提的是在去年10月份的Kafka旧金山峰会上有人分享了ZFS搭配Kafka的案例我们在专栏[第8讲](https://time.geekbang.org/column/article/101763)提到过与之相关的[数据报告](https://www.confluent.io/kafka-summit-sf18/kafka-on-zfs)。该报告宣称ZFS多级缓存的机制能够帮助Kafka改善I/O性能据说取得了不错的效果。如果你的环境中安装了ZFS文件系统你可以尝试将Kafka搭建在ZFS文件系统上。
另外就是swap空间的设置。我个人建议将swappiness设置成一个很小的值比如110之间以防止Linux的OOM Killer开启随意杀掉进程。你可以执行sudo sysctl vm.swappiness=N来临时设置该值如果要永久生效可以修改/etc/sysctl.conf文件增加vm.swappiness=N然后重启机器即可。
操作系统层面还有两个参数也很重要,它们分别是**ulimit -n和vm.max_map_count**。前者如果设置得太小你会碰到Too Many File Open这类的错误而后者的值如果太小在一个主题数超多的Broker机器上你会碰到**OutOfMemoryErrorMap failed**的严重错误因此我建议在生产环境中适当调大此值比如将其设置为655360。具体设置方法是修改/etc/sysctl.conf文件增加vm.max_map_count=655360保存之后执行sysctl -p命令使它生效。
最后不得不提的就是操作系统页缓存大小了这对Kafka而言至关重要。在某种程度上我们可以这样说给Kafka预留的页缓存越大越好最小值至少要容纳一个日志段的大小也就是Broker端参数log.segment.bytes的值。该参数的默认值是1GB。预留出一个日志段大小至少能保证Kafka可以将整个日志段全部放入页缓存这样消费者程序在消费时能直接命中页缓存从而避免昂贵的物理磁盘I/O操作。
### JVM层调优
说完了操作系统层面的调优我们来讨论下JVM层的调优其实JVM层的调优我们还是要重点关注堆设置以及GC方面的性能。
1.设置堆大小。
如何为Broker设置堆大小这是很多人都感到困惑的问题。我来给出一个朴素的答案**将你的JVM堆大小设置成68GB**。
在很多公司的实际环境中这个大小已经被证明是非常合适的你可以安心使用。如果你想精确调整的话我建议你可以查看GC log特别是关注Full GC之后堆上存活对象的总大小然后把堆大小设置为该值的1.52倍。如果你发现Full GC没有被执行过手动运行jmap -histo:live &lt; pid &gt;就能人为触发Full GC。
2.GC收集器的选择。
**我强烈建议你使用G1收集器主要原因是方便省事至少比CMS收集器的优化难度小得多**。另外你一定要尽力避免Full GC的出现。其实不论使用哪种收集器都要竭力避免Full GC。在G1中Full GC是单线程运行的它真的非常慢。如果你的Kafka环境中经常出现Full GC你可以配置JVM参数-XX:+PrintAdaptiveSizePolicy来探查一下到底是谁导致的Full GC。
使用G1还很容易碰到的一个问题就是**大对象Large Object**反映在GC上的错误就是“too many humongous allocations”。所谓的大对象一般是指至少占用半个区域Region大小的对象。举个例子如果你的区域尺寸是2MB那么超过1MB大小的对象就被视为是大对象。要解决这个问题除了增加堆大小之外你还可以适当地增加区域大小设置方法是增加JVM启动参数-XX:+G1HeapRegionSize=N。默认情况下如果一个对象超过了N/2就会被视为大对象从而直接被分配在大对象区。如果你的Kafka环境中的消息体都特别大就很容易出现这种大对象分配的问题。
### Broker端调优
我们继续沿着漏斗往上走来看看Broker端的调优。
Broker端调优很重要的一个方面就是合理地设置Broker端参数值以匹配你的生产环境。不过后面我们在讨论具体的调优目标时再详细说这部分内容。这里我想先讨论另一个优化手段**即尽力保持客户端版本和Broker端版本一致**。不要小看版本间的不一致问题它会令Kafka丧失很多性能收益比如Zero Copy。下面我用一张图来说明一下。
<img src="https://static001.geekbang.org/resource/image/53/6e/5310d7d29235b080c872e0a9eb396e6e.png" alt="">
图中蓝色的Producer、Consumer和Broker的版本是相同的它们之间的通信可以享受Zero Copy的快速通道相反一个低版本的Consumer程序想要与Producer、Broker交互的话就只能依靠JVM堆中转一下丢掉了快捷通道就只能走慢速通道了。因此在优化Broker这一层时你只要保持服务器端和客户端版本的一致就能获得很多性能收益了。
### 应用层调优
现在,我们终于来到了漏斗的最顶层。其实,这一层的优化方法各异,毕竟每个应用程序都是不一样的。不过,有一些公共的法则依然是值得我们遵守的。
- **不要频繁地创建Producer和Consumer对象实例**。构造这些对象的开销很大,尽量复用它们。
- **用完及时关闭**。这些对象底层会创建很多物理资源如Socket连接、ByteBuffer缓冲区等。不及时关闭的话势必造成资源泄露。
- **合理利用多线程来改善性能**。Kafka的Java Producer是线程安全的你可以放心地在多个线程中共享同一个实例而Java Consumer虽不是线程安全的但我们在专栏[第20讲](https://time.geekbang.org/column/article/108512)讨论过多线程的方案,你可以回去复习一下。
## 性能指标调优
接下来我会给出调优各个目标的参数配置以及具体的配置原因希望它们能够帮助你更有针对性地调整你的Kafka集群。
### 调优吞吐量
首先是调优吞吐量。很多人对吞吐量和延时之间的关系似乎有些误解。比如有这样一种提法还挺流行的假设Kafka每发送一条消息需要花费2ms那么延时就是2ms。显然吞吐量就应该是500条/秒因为1秒可以发送1 / 0.002 = 500条消息。因此吞吐量和延时的关系可以用公式来表示TPS = 1000 / Latency(ms)。但实际上,吞吐量和延时的关系远不是这么简单。
我们以Kafka Producer为例。假设它以2ms的延时来发送消息如果每次只是发送一条消息那么TPS自然就是500条/秒。但如果Producer不是每次发送一条消息而是在发送前等待一段时间然后统一发送**一批**消息比如Producer每次发送前先等待8ms8ms之后Producer共缓存了1000条消息此时总延时就累加到10ms即 2ms + 8ms而TPS等于1000 / 0.01 = 100,000条/秒。由此可见虽然延时增加了4倍但TPS却增加了将近200倍。这其实也是批次化batching或微批次化micro-batching目前会很流行的原因。
在实际环境中用户似乎总是愿意用较小的延时增加的代价去换取TPS的显著提升。毕竟从2ms到10ms的延时增加通常是可以忍受的。事实上Kafka Producer就是采取了这样的设计思想。
当然你可能会问发送一条消息需要2ms那么等待8ms就能累积1000条消息吗答案是可以的Producer累积消息时一般仅仅是将消息发送到内存中的缓冲区而发送消息却需要涉及网络I/O传输。内存操作和I/O操作的时间量级是不同的前者通常是几百纳秒级别而后者则是从毫秒到秒级别不等因此Producer等待8ms积攒出的消息数可能远远多于同等时间内Producer能够发送的消息数。
好了说了这么多我们该怎么调优TPS呢我来跟你分享一个参数列表。
<img src="https://static001.geekbang.org/resource/image/7a/cb/7aec00207dc149bd804d20df6e3b9ccb.jpg" alt="">
我稍微解释一下表格中的内容。
Broker端参数num.replica.fetchers表示的是Follower副本用多少个线程来拉取消息默认使用1个线程。如果你的Broker端CPU资源很充足不妨适当调大该参数值加快Follower副本的同步速度。因为在实际生产环境中**配置了acks=all的Producer程序吞吐量被拖累的首要因素就是副本同步性能**。增加这个值后你通常可以看到Producer端程序的吞吐量增加。
另外需要注意的就是避免经常性的Full GC。目前不论是CMS收集器还是G1收集器其Full GC采用的是Stop The World的单线程收集策略非常慢因此一定要避免。
**在Producer端如果要改善吞吐量通常的标配是增加消息批次的大小以及批次缓存时间即batch.size和linger.ms**。目前它们的默认值都偏小特别是默认的16KB的消息批次大小一般都不适用于生产环境。假设你的消息体大小是1KB默认一个消息批次也就大约16条消息显然太小了。我们还是希望Producer能一次性发送更多的消息。
除了这两个你最好把压缩算法也配置上以减少网络I/O传输量从而间接提升吞吐量。当前和Kafka适配最好的两个压缩算法是**LZ4和zstd**,不妨一试。
同时由于我们的优化目标是吞吐量最好不要设置acks=all以及开启重试。前者引入的副本同步时间通常都是吞吐量的瓶颈而后者在执行过程中也会拉低Producer应用的吞吐量。
最后如果你在多个线程中共享一个Producer实例就可能会碰到缓冲区不够用的情形。倘若频繁地遭遇TimeoutExceptionFailed to allocate memory within the configured max blocking time这样的异常那么你就必须显式地增加**buffer.memory**参数值,确保缓冲区总是有空间可以申请的。
说完了Producer端我们来说说Consumer端。Consumer端提升吞吐量的手段是有限的你可以利用多线程方案增加整体吞吐量也可以增加fetch.min.bytes参数值。默认是1字节表示只要Kafka Broker端积攒了1字节的数据就可以返回给Consumer端这实在是太小了。我们还是让Broker端一次性多返回点数据吧。
### 调优延时
讲完了调优吞吐量,我们来说说如何优化延时,下面是调优延时的参数列表。
<img src="https://static001.geekbang.org/resource/image/26/3a/2688329a0614601fed497f3858c98e3a.jpg" alt="">
在Broker端我们依然要增加num.replica.fetchers值以加快Follower副本的拉取速度减少整个消息处理的延时。
在Producer端我们希望消息尽快地被发送出去因此不要有过多停留所以必须设置linger.ms=0同时不要启用压缩。因为压缩操作本身要消耗CPU时间会增加消息发送的延时。另外最好不要设置acks=all。我们刚刚在前面说过Follower副本同步往往是降低Producer端吞吐量和增加延时的首要原因。
在Consumer端我们保持fetch.min.bytes=1即可也就是说只要Broker端有能返回的数据立即令其返回给Consumer缩短Consumer消费延时。
## 小结
好了我们来小结一下。今天我跟你分享了Kafka调优方面的内容。我们先从调优目标开始说起然后我给出了调优层次漏斗接着我分享了一些基础性调优包括操作系统层调优、JVM层调优以及应用程序调优等。最后针对Kafka关心的两个性能指标吞吐量和延时我分别从Broker、Producer和Consumer三个维度给出了一些参数值设置的最佳实践。
最后,我来分享一个性能调优的真实小案例。
曾经我碰到过一个线上环境的问题该集群上Consumer程序一直表现良好但是某一天它的性能突然下降表现为吞吐量显著降低。我在查看磁盘读I/O使用率时发现其明显上升但之前该Consumer Lag很低消息读取应该都能直接命中页缓存。此时磁盘读突然飙升我就怀疑有其他程序写入了页缓存。后来经过排查我发现果然有一个测试Console Consumer程序启动“污染”了部分页缓存导致主业务Consumer读取消息不得不走物理磁盘因此吞吐量下降。找到了真实原因解决起来就简单多了。
其实,我给出这个案例的真实目的是想说,对于性能调优,我们最好按照今天给出的步骤一步一步地窄化和定位问题。一旦定位了原因,后面的优化就水到渠成了。
<img src="https://static001.geekbang.org/resource/image/d4/38/d40b07ca7bcc0fc43c5266b0b2c81c38.jpg" alt="">
## 开放讨论
请分享一个你调优Kafka的真实案例详细说说你是怎么碰到性能问题的又是怎么解决的。
欢迎写下你的思考和答案,我们一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@@ -0,0 +1,277 @@
<audio id="audio" title="39 | 从0搭建基于Kafka的企业级实时日志流处理平台" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/61/dc/6107b53eb86da2856c87be5fff9d11dc.mp3"></audio>
你好我是胡夕。今天我要和你分享的主题是从0搭建基于Kafka的企业级实时日志流处理平台。
简单来说,我们要实现一些大数据组件的组合,就如同玩乐高玩具一样,把它们“插”在一起,“拼”成一个更大一点的玩具。
在任何一个企业中,服务器每天都会产生很多的日志数据。这些数据内容非常丰富,包含了我们的**线上业务数据**、**用户行为数据**以及**后端系统数据**。实时分析这些数据能够帮助我们更快地洞察潜在的趋势从而有针对性地做出决策。今天我们就使用Kafka搭建一个这样的平台。
## 流处理架构
如果在网上搜索实时日志流处理你应该能够搜到很多教你搭建实时流处理平台做日志分析的教程。这些教程使用的技术栈大多是Flume+Kafka+Storm、Spark Streaming或Flink。特别是Flume+Kafka+Flink的组合逐渐成为了实时日志流处理的标配。不过要搭建这样的处理平台你需要用到3个框架才能实现这既增加了系统复杂度也提高了运维成本。
今天我来演示一下如何使用Apache Kafka这一个框架实现一套实时日志流处理系统。换句话说我使用的技术栈是Kafka Connect+Kafka Core+Kafka Streams的组合。
下面这张图展示了基于Kafka的实时日志流处理平台的流程。
<img src="https://static001.geekbang.org/resource/image/09/51/09e4dc28ea338ee69b5662596c0b8751.jpg" alt="">
从图中我们可以看到日志先从Web服务器被不断地生产出来随后被实时送入到Kafka Connect组件Kafka Connect组件对日志进行处理后将其灌入Kafka的某个主题上接着发送到Kafka Streams组件进行实时分析。最后Kafka Streams将分析结果发送到Kafka的另一个主题上。
我在专栏前面简单介绍过Kafka Connect和Kafka Streams组件前者可以实现外部系统与Kafka之间的数据交互而后者可以实时处理Kafka主题中的消息。
现在我们就使用这两个组件结合前面学习的所有Kafka知识一起构建一个实时日志分析平台。
## Kafka Connect组件
我们先利用Kafka Connect组件**收集数据**。如前所述Kafka Connect组件负责连通Kafka与外部数据系统。连接外部数据源的组件叫连接器Connector。**常见的外部数据源包括数据库、KV存储、搜索系统或文件系统等。**
今天我们使用文件连接器File Connector实时读取Nginx的access日志。假设access日志的格式如下
```
10.10.13.41 - - [13/Aug/2019:03:46:54 +0800] &quot;GET /v1/open/product_list?user_key=****&amp;user_phone=****&amp;screen_height=1125&amp;screen_width=2436&amp;from_page=1&amp;user_type=2&amp;os_type=ios HTTP/1.0&quot; 200 1321
```
在这段日志里请求参数中的os_type字段目前有两个值ios和android。我们的目标是实时计算当天所有请求中ios端和android端的请求数。
### 启动Kafka Connect
当前Kafka Connect支持单机版Standalone和集群版Cluster我们用集群的方式来启动Connect组件。
首先我们要启动Kafka集群假设Broker的连接地址是localhost:9092。
启动好Kafka集群后我们启动Connect组件。在Kafka安装目录下有个config/connect-distributed.properties文件你需要修改下列项
```
bootstrap.servers=localhost:9092
rest.host.name=localhost
rest.port=8083
```
第1项是指定**要连接的Kafka集群**第2项和第3项分别指定Connect组件开放的REST服务的**主机名和端口**。保存这些变更之后我们运行下面的命令启动Connect。
```
cd kafka_2.12-2.3.0
bin/connect-distributed.sh config/connect-distributed.properties
```
如果一切正常此时Connect应该就成功启动了。现在我们在浏览器访问localhost:8083的Connect REST服务应该能看到下面的返回内容
```
{&quot;version&quot;:&quot;2.3.0&quot;,&quot;commit&quot;:&quot;fc1aaa116b661c8a&quot;,&quot;kafka_cluster_id&quot;:&quot;XbADW3mnTUuQZtJKn9P-hA&quot;}
```
### 添加File Connector
看到该JSON串就表明Connect已经成功启动了。此时我们打开一个终端运行下面这条命令来查看一下当前都有哪些Connector。
```
$ curl http://localhost:8083/connectors
[]
```
结果显示目前我们没有创建任何Connector。
现在我们来创建对应的File Connector。该Connector读取指定的文件并为每一行文本创建一条消息并发送到特定的Kafka主题上。创建命令如下
```
$ curl -H &quot;Content-Type:application/json&quot; -H &quot;Accept:application/json&quot; http://localhost:8083/connectors -X POST --data '{&quot;name&quot;:&quot;file-connector&quot;,&quot;config&quot;:{&quot;connector.class&quot;:&quot;org.apache.kafka.connect.file.FileStreamSourceConnector&quot;,&quot;file&quot;:&quot;/var/log/access.log&quot;,&quot;tasks.max&quot;:&quot;1&quot;,&quot;topic&quot;:&quot;access_log&quot;}}'
{&quot;name&quot;:&quot;file-connector&quot;,&quot;config&quot;:{&quot;connector.class&quot;:&quot;org.apache.kafka.connect.file.FileStreamSourceConnector&quot;,&quot;file&quot;:&quot;/var/log/access.log&quot;,&quot;tasks.max&quot;:&quot;1&quot;,&quot;topic&quot;:&quot;access_log&quot;,&quot;name&quot;:&quot;file-connector&quot;},&quot;tasks&quot;:[],&quot;type&quot;:&quot;source&quot;}
```
这条命令本质上是向Connect REST服务发送了一个POST请求去创建对应的Connector。在这个例子中我们的Connector类是Kafka默认提供的**FileStreamSourceConnector**。我们要读取的日志文件在/var/log目录下要发送到Kafka的主题名称为access_log。
现在我们再次运行curl http: // localhost:8083/connectors 验证一下刚才的Connector是否创建成功了。
```
$ curl http://localhost:8083/connectors
[&quot;file-connector&quot;]
```
显然名为file-connector的新Connector已经创建成功了。如果我们现在使用Console Consumer程序去读取access_log主题的话应该会发现access.log中的日志行数据已经源源不断地向该主题发送了。
如果你的生产环境中有多台机器操作也很简单在每台机器上都创建这样一个Connector只要保证它们被送入到相同的Kafka主题以供消费就行了。
## Kafka Streams组件
数据到达Kafka还不够我们还需要对其进行实时处理。下面我演示一下如何编写Kafka Streams程序来实时分析Kafka主题数据。
我们知道Kafka Streams是Kafka提供的用于实时流处理的组件。
与其他流处理框架不同的是它仅仅是一个类库用它编写的应用被编译打包之后就是一个普通的Java应用程序。你可以使用任何部署框架来运行Kafka Streams应用程序。
同时你只需要简单地启动多个应用程序实例就能自动地获得负载均衡和故障转移因此和Spark Streaming或Flink这样的框架相比Kafka Streams自然有它的优势。
下面这张来自Kafka官网的图片形象地展示了多个Kafka Streams应用程序组合在一起共同实现流处理的场景。图中清晰地展示了3个Kafka Streams应用程序实例。一方面它们形成一个组共同参与并执行流处理逻辑的计算另一方面它们又都是独立的实体彼此之间毫无关联完全依靠Kafka Streams帮助它们发现彼此并进行协作。
<img src="https://static001.geekbang.org/resource/image/6f/67/6fc7c835c993b48b1ef1558e02f24f67.png" alt="">
关于Kafka Streams的原理我会在专栏后面进行详细介绍。今天我们只要能够学会利用它提供的API编写流处理应用帮我们找到刚刚提到的请求日志中ios端和android端发送请求数量的占比数据就行了。
### 编写流处理应用
要使用Kafka Streams你需要在你的Java项目中显式地添加kafka-streams依赖。我以最新的2.3版本为例分别演示下Maven和Gradle的配置方法。
```
Maven:
&lt;dependency&gt;
&lt;groupId&gt;org.apache.kafka&lt;/groupId&gt;
&lt;artifactId&gt;kafka-streams&lt;/artifactId&gt;
&lt;version&gt;2.3.0&lt;/version&gt;
&lt;/dependency&gt;
```
```
Gradle:
compile group: 'org.apache.kafka', name: 'kafka-streams', version: '2.3.0'
```
现在,我先给出完整的代码,然后我会详细解释一下代码中关键部分的含义。
```
package com.geekbang.kafkalearn;
import com.google.gson.Gson;
import org.apache.kafka.common.serialization.Serdes;
import org.apache.kafka.streams.KafkaStreams;
import org.apache.kafka.streams.StreamsBuilder;
import org.apache.kafka.streams.StreamsConfig;
import org.apache.kafka.streams.Topology;
import org.apache.kafka.streams.kstream.KStream;
import org.apache.kafka.streams.kstream.Produced;
import org.apache.kafka.streams.kstream.TimeWindows;
import org.apache.kafka.streams.kstream.WindowedSerdes;
import java.time.Duration;
import java.util.Properties;
import java.util.concurrent.CountDownLatch;
public class OSCheckStreaming {
public static void main(String[] args) {
Properties props = new Properties();
props.put(StreamsConfig.APPLICATION_ID_CONFIG, &quot;os-check-streams&quot;);
props.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, &quot;localhost:9092&quot;);
props.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String().getClass());
props.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.String().getClass());
props.put(StreamsConfig.DEFAULT_WINDOWED_KEY_SERDE_INNER_CLASS, Serdes.StringSerde.class.getName());
final Gson gson = new Gson();
final StreamsBuilder builder = new StreamsBuilder();
KStream&lt;String, String&gt; source = builder.stream(&quot;access_log&quot;);
source.mapValues(value -&gt; gson.fromJson(value, LogLine.class)).mapValues(LogLine::getPayload)
.groupBy((key, value) -&gt; value.contains(&quot;ios&quot;) ? &quot;ios&quot; : &quot;android&quot;)
.windowedBy(TimeWindows.of(Duration.ofSeconds(2L)))
.count()
.toStream()
.to(&quot;os-check&quot;, Produced.with(WindowedSerdes.timeWindowedSerdeFrom(String.class), Serdes.Long()));
final Topology topology = builder.build();
final KafkaStreams streams = new KafkaStreams(topology, props);
final CountDownLatch latch = new CountDownLatch(1);
Runtime.getRuntime().addShutdownHook(new Thread(&quot;streams-shutdown-hook&quot;) {
@Override
public void run() {
streams.close();
latch.countDown();
}
});
try {
streams.start();
latch.await();
} catch (Exception e) {
System.exit(1);
}
System.exit(0);
}
}
class LogLine {
private String payload;
private Object schema;
public String getPayload() {
return payload;
}
}
```
这段代码会实时读取access_log主题每2秒计算一次ios端和android端请求的总数并把这些数据写入到os-check主题中。
首先我们构造一个Properties对象。这个对象负责初始化Streams应用程序所需要的关键参数设置。比如在上面的例子中我们设置了bootstrap.servers参数、application.id参数以及默认的序列化器Serializer和解序列化器Deserializer
bootstrap.servers参数你应该已经很熟悉了我就不多讲了。这里的application.id是Streams程序中非常关键的参数你必须要指定一个集群范围内唯一的字符串来标识你的Kafka Streams程序。序列化器和解序列化器设置了默认情况下Streams程序执行序列化和反序列化时用到的类。在这个例子中我们设置的是String类型这表示序列化时会将String转换成字节数组反序列化时会将字节数组转换成String。
构建好Properties实例之后下一步是创建StreamsBuilder对象。稍后我们会用这个Builder去实现具体的流处理逻辑。
在这个例子中我们实现了这样的流计算逻辑每2秒去计算一下ios端和android端各自发送的总请求数。还记得我们的原始数据长什么样子吗它是一行Nginx日志只不过Connect组件在读取它后会把它包装成JSON格式发送到Kafka因此我们需要借助Gson来帮助我们把JSON串还原为Java对象这就是我在代码中创建LogLine类的原因。
代码中的mapValues调用将接收到的JSON串转换成LogLine对象之后再次调用mapValues方法提取出LogLine对象中的payload字段这个字段保存了真正的日志数据。这样经过两次mapValues方法调用之后我们成功地将原始数据转换成了实际的Nginx日志行数据。
值得注意的是代码使用的是Kafka Streams提供的mapValues方法。顾名思义**这个方法就是只对消息体Value进行转换而不变更消息的键Key**。
其实Kafka Streams也提供了map方法允许你同时修改消息Key。通常来说我们认为**mapValues要比map方法更高效**因为Key的变更可能导致下游处理算子Operator的重分区降低性能。如果可能的话最好尽量使用mapValues方法。
拿到真实日志行数据之后我们调用groupBy方法进行统计计数。由于我们要统计双端ios端和android端的请求数因此我们groupBy的Key是ios或android。在上面的那段代码中我仅仅依靠日志行中是否包含特定关键字的方式来确定是哪一端。更正宗的做法应该是**分析Nginx日志格式提取对应的参数值也就是os_type的值**。
做完groupBy之后我们还需要限定要统计的时间窗口范围即我们统计的双端请求数是在哪个时间窗口内计算的。在这个例子中我调用了**windowedBy方法**要求Kafka Streams每2秒统计一次双端的请求数。设定好了时间窗口之后下面就是调用**count方法**进行统计计数了。
这一切都做完了之后,我们需要调用**toStream方法**将刚才统计出来的表Table转换成事件流这样我们就能实时观测它里面的内容。我会在专栏的最后几讲中解释下流处理领域内的流和表的概念以及它们的区别。这里你只需要知道toStream是将一个Table变成一个Stream即可。
最后,我们调用**to方法**将这些时间窗口统计数据不断地写入到名为os-check的Kafka主题中从而最终实现我们对Nginx日志进行实时分析处理的需求。
### 启动流处理应用
由于Kafka Streams应用程序就是普通的Java应用你可以用你熟悉的方式对它进行编译、打包和部署。本例中的OSCheckStreaming.java就是一个可执行的Java类因此直接运行它即可。如果一切正常它会将统计数据源源不断地写入到os-check主题。
### 查看统计结果
如果我们想要查看统计的结果一个简单的方法是使用Kafka自带的kafka-console-consumer脚本。命令如下
```
$ bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic os-check --from-beginning --property value.deserializer=org.apache.kafka.common.serialization.LongDeserializer --property print.key=true --property key.deserializer=org.apache.kafka.streams.kstream.TimeWindowedDeserializer --property key.deserializer.default.windowed.key.serde.inner=org.apache.kafka.common.serialization.Serdes\$StringSerde
[android@1565743788000/9223372036854775807] 1522
[ios@1565743788000/9223372036854775807] 478
[ios@1565743790000/9223372036854775807] 1912
[android@1565743790000/9223372036854775807] 5313
[ios@1565743792000/9223372036854775807] 780
[android@1565743792000/9223372036854775807] 1949
[android@1565743794000/9223372036854775807] 37
……
```
由于我们统计的结果是某个时间窗口范围内的因此承载这个统计结果的消息的Key封装了该时间窗口信息具体格式是**[ios或android@开始时间/结束时间]**而消息的Value就是一个简单的数字表示这个时间窗口内的总请求数。
如果把上面ios相邻输出行中的开始时间相减我们就会发现它们的确是每2秒输出一次每次输出会同时计算出ios端和android端的总请求数。接下来你可以订阅这个Kafka主题将结果实时导出到你期望的其他数据存储上。
## 小结
至此基于Apache Kafka的实时日志流处理平台就简单搭建完成了。在搭建的过程中我们只使用Kafka这一个大数据框架就完成了所有组件的安装、配置和代码开发。比起Flume+Kafka+Flink这样的技术栈纯Kafka的方案在运维和管理成本上有着极大的优势。如果你打算从0构建一个实时流处理平台不妨试一下Kafka Connect+Kafka Core+Kafka Streams的组合。
其实Kafka Streams提供的功能远不止做计数这么简单。今天我只是为你展示了Kafka Streams的冰山一角。在专栏的后几讲中我会重点向你介绍Kafka Streams组件的使用和管理敬请期待。
<img src="https://static001.geekbang.org/resource/image/fd/9f/fdb79f35b43cab31edb945b977cc609f.jpg" alt="">
## 开放讨论
请比较一下Flume+Kafka+Flink方案和纯Kafka方案思考一下它们各自的优劣之处。在实际场景中我们该如何选择呢
欢迎写下你的思考和答案,我们一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。