first commit

This commit is contained in:
张乾
2024-10-16 06:37:41 +08:00
parent 633f45ea20
commit 206fad82a2
3590 changed files with 680090 additions and 0 deletions

View File

@ -0,0 +1,60 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
00 开篇词 为什么要学习Kafka
你好我是胡夕Apache Kafka Committer老虎证券用户增长团队负责人也是《Apache Kafka实战》这本书的作者。
在过去5年中我经历了Kafka从最初的0.8版本逐步演进到现在的2.3版本的完整过程踩了很多坑也交了很多学费慢慢地我梳理出了一个相对系统、完整的Kafka应用实战指南最终以“Kafka核心技术与实战”专栏的形式呈现给你希望分享我对Apache Kafka的理解和实战方面的经验帮你透彻理解Kafka、更好地应用Kafka。
你可能会有这样的疑问我为什么要学习Kafka呢要回答这个问题我们不妨从更大的视角来审视它先聊聊我对这几年互联网技术发展的理解吧。
互联网蓬勃发展的这些年涌现出了很多令人眼花缭乱的新技术。以我个人的浅见截止到2019年当下互联网行业最火的技术当属ABC了即所谓的AI人工智能、BigData大数据和Cloud云计算云平台。我个人对区块链技术发展前景存疑毕竟目前没有看到特别好的落地应用场景也许在未来几年它会更令人刮目相看吧。
在这ABC当中坦率说A和C是有点曲高和寡的不是所有玩家都能入场。反观B要显得平民得多几乎所有公司都能参与进来。我曾经到过一个理发厅那里的人都宣称他们采用了大数据系统帮助客户设计造型足见BigData是很“下里巴人”的。
作为工程师或架构师,你在实际工作过程中一定参与到了很多大数据业务系统的构建。由于这些系统都是为公司业务服务的,所以通常来说它们仅仅是执行一些常规的业务逻辑,因此它们不能算是计算密集型应用,相反更应该是数据密集型的。
对于数据密集型应用来说如何应对数据量激增、数据复杂度增加以及数据变化速率变快是彰显大数据工程师、架构师功力的最有效表征。我们欣喜地发现Kafka在帮助你应对这些问题方面能起到非常好的效果。就拿数据量激增来说Kafka能够有效隔离上下游业务将上游突增的流量缓存起来以平滑的方式传导到下游子系统中避免了流量的不规则冲击。由此可见如果你是一名大数据从业人员熟练掌握Kafka是非常必要的一项技能。
刚刚所举的例子仅仅是Kafka助力业务的一个场景罢了。事实上Kafka有着非常广阔的应用场景。不谦虚地说目前Apache Kafka被认为是整个消息引擎领域的执牛耳者仅凭这一点就值得我们好好学习一下它。另外从学习技术的角度而言Kafka也是很有亮点的。我们仅需要学习一套框架就能在实际业务系统中实现消息引擎应用、应用程序集成、分布式存储构建甚至是流处理应用的开发与部署听起来还是很超值的吧。
不仅如此再给你看一个数据。援引美国2019年Dice技术薪资报告中的数据在10大薪资最高的技术技能中掌握Kafka以平均每年12.8万美元排名第二排名第一位的是13.2万美元/年的Go语言。好吧希望你看到这个之后不会立即关闭我的专栏然后转头直奔隔壁的Go语言专栏。虽然这是美国人才市场的数据但是我们有理由相信在国内Kafka的行情也是水涨船高。2019年两会上再一次提到了要深化大数据、人工智能等研发应用而Kafka无论是作为消息引擎还是实时流处理平台都能在大数据工程领域发挥重要的作用。
总之Kafka是个利器值得一试既然知道了为什么要学Kafka那我们就要行动起来把它学透而学透Kafka有什么路径吗
如果你是一名软件开发工程师的话掌握Kafka的第一步就是要根据你掌握的编程语言去寻找对应的Kafka客户端。当前Kafka最重要的两大客户端是Java客户端和libkafka客户端它们更新和维护的速度很快非常适合你持续花时间投入。
一旦确定了要使用的客户端,马上去官网上学习一下代码示例,如果能够正确编译和运行这些样例,你就能轻松地驾驭客户端了。
下一步你可以尝试修改样例代码尝试去理解并使用其他的API之后观测你修改的结果。如果这些都没有难倒你你可以自己编写一个小型项目来验证下学习成果然后就是改善和提升客户端的可靠性和性能了。到了这一步你可以熟读一遍Kafka官网文档确保你理解了那些可能影响可靠性和性能的参数。
最后是学习Kafka的高级功能比如流处理应用开发。流处理API不仅能够生产和消费消息还能执行高级的流式处理操作比如时间窗口聚合、流处理连接等。
如果你是系统管理员或运维工程师那么相应的学习目标应该是学习搭建及管理Kafka线上环境。如何根据实际业务需求评估、搭建生产线上环境将是你主要的学习目标。另外对生产环境的监控也是重中之重的工作Kafka提供了超多的JMX监控指标你可以选择任意你熟知的框架进行监控。有了监控数据作为系统运维管理员的你势必要观测真实业务负载下的Kafka集群表现。之后如何利用已有的监控指标来找出系统瓶颈然后提升整个系统的吞吐量这也是最能体现你工作价值的地方。
在明确了自己要学什么以及怎么学之后,你现在会不会有一种感慨:原来我要学习这么多东西呀!不用担心,刚刚我提到的所有内容都会在专栏中被覆盖到。
下面是我特意为专栏画的一张思维导图可以帮你迅速了解这个专栏的知识结构体系是什么样的。专栏大致从六个方面展开包括Kafka入门、Kafka的基本使用、客户端详解、Kafka原理介绍、Kafka运维与监控以及高级Kafka应用。
专栏的第一部分我会介绍消息引擎这类系统大致的原理和用途以及作为优秀消息引擎代表的Kafka在这方面的表现。
第二部分则重点探讨Kafka如何用于生产环境特别是线上环境方案的制定。
在第三部分中我会陪你一起学习Kafka客户端的方方面面既有生产者的实操讲解也有消费者的原理剖析你一定不要错过。
第四部分会着重介绍Kafka最核心的设计原理包括Controller的设计机制、请求处理全流程解析等。
第五部分则涵盖Kafka运维与监控的内容想获得高效运维Kafka集群以及有效监控Kafka的实战经验我必当倾囊相助
最后一个部分我会简单介绍一下Kafka流处理组件Kafka Streams的实战应用希望能让你认识一个不太一样的Kafka。
这里不得不提的是有熟悉我的读者可能知道我出版过的图书《Apache Kafka实战》。你可能有这样的疑问既然有书了那么这个专栏与书的区别又是什么呢《Apache Kafka实战》这本书是基于Kafka 1.0版本撰写的但目前Kafka已经演进到2.3版本了我必须要承认书中的部分内容已经过时甚至是不准确了而专栏的写作是基于Kafka的最新版。并且专栏作为一次全新的交付我希望能用更轻松更容易理解的语言和形式帮你获取到最新的Kafka实战经验。
我希望通过学习这个专栏你不仅能够将Kafka熟练运用到实际工作当中去而且还能培养出对于Kafka或是其他技术框架的浓厚学习兴趣。
最后我希望用一句话收尾与你共勉Stay focused and work hard

View File

@ -0,0 +1,79 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
01 消息引擎系统ABC
01 消息引擎系统ABC
你好我是胡夕。欢迎你来到“Kafka核心技术与实战”专栏。如果你对Kafka及其背后的消息引擎、流处理感兴趣很高兴我们可以在此相聚并在未来的一段日子里一同学习有关Kafka的方方面面。
毫无疑问你现在对Apache Kafka一定充满了各种好奇那么今天就允许我先来尝试回答下Kafka是什么这个问题。对了先卖个关子在下一期我还将继续回答这个问题而且答案是不同的。那么Kafka是什么呢用一句话概括一下Apache Kafka是一款开源的消息引擎系统。
倘若“消息引擎系统”这个词对你来说有点陌生的话那么“消息队列”“消息中间件”的提法想必你一定是有所耳闻的。不过说实话我更愿意使用消息引擎系统这个称谓因为消息队列给出了一个很不明确的暗示仿佛Kafka是利用队列的方式构建的而消息中间件的提法有过度夸张“中间件”之嫌让人搞不清楚这个中间件到底是做什么的。
像Kafka这一类的系统国外有专属的名字叫Messaging System国内很多文献将其简单翻译成消息系统。我个人认为并不是很恰当因为它片面强调了消息主体的作用而忽视了这类系统引以为豪的消息传递属性就像引擎一样具备某种能量转换传输的能力所以我觉得翻译成消息引擎反倒更加贴切。
讲到这里说点题外话。我觉得目前国内在翻译国外专有技术词汇方面做得不够标准化各种名字和提法可谓五花八门。我举个例子比如大名鼎鼎的Raft算法和Paxos算法。了解它的人都知道它们的作用是在分布式系统中让多个节点就某个决定达成共识都属于Consensus Algorithm一族。如果你在搜索引擎中查找Raft算法国内多是称呼它们为一致性算法。实际上我倒觉得翻译成共识算法是最准确的。我们使用“一致性”这个字眼太频繁了国外的Consistency被称为一致性、Consensus也唤作一致性甚至是Coherence都翻译成一致性。
还是拉回来继续聊消息引擎系统,那这类系统是做什么用的呢?我先来个官方严肃版本的答案。
根据维基百科的定义,消息引擎系统是一组规范。企业利用这组规范在不同系统之间传递语义准确的消息,实现松耦合的异步式数据传递。
果然是官方定义,有板有眼。如果觉得难于理解,那么可以试试我下面这个民间版:
系统A发送消息给消息引擎系统系统B从消息引擎系统中读取A发送的消息。
最基础的消息引擎就是做这点事的!不论是上面哪个版本,它们都提到了两个重要的事实:
消息引擎传输的对象是消息;
如何传输消息属于消息引擎设计机制的一部分。
既然消息引擎是用于在不同系统之间传输消息的,那么如何设计待传输消息的格式从来都是一等一的大事。试问一条消息如何做到信息表达业务语义而无歧义,同时它还要能最大限度地提供可重用性以及通用性?稍微停顿几秒去思考一下,如果是你,你要如何设计你的消息编码格式。
一个比较容易想到的是使用已有的一些成熟解决方案比如使用CSV、XML亦或是JSON又或者你可能熟知国外大厂开源的一些序列化框架比如Google的Protocol Buffer或Facebook的Thrift。这些都是很酷的办法。那么现在我告诉你Kafka的选择它使用的是纯二进制的字节序列。当然消息还是结构化的只是在使用之前都要将其转换成二进制的字节序列。
消息设计出来之后还不够,消息引擎系统还要设定具体的传输协议,即我用什么方法把消息传输出去。常见的有两种方法:
点对点模型也叫消息队列模型。如果拿上面那个“民间版”的定义来说那么系统A发送的消息只能被系统B接收其他任何系统都不能读取A发送的消息。日常生活的例子比如电话客服就属于这种模型同一个客户呼入电话只能被一位客服人员处理第二个客服人员不能为该客户服务。
发布/订阅模型与上面不同的是它有一个主题Topic的概念你可以理解成逻辑语义相近的消息容器。该模型也有发送方和接收方只不过提法不同。发送方也称为发布者Publisher接收方称为订阅者Subscriber。和点对点模型不同的是这个模型可能存在多个发布者向相同的主题发送消息而订阅者也可能存在多个它们都能接收到相同主题的消息。生活中的报纸订阅就是一种典型的发布/订阅模型。
比较酷的是Kafka同时支持这两种消息引擎模型专栏后面我会分享Kafka是如何做到这一点的。
提到消息引擎系统你可能会问JMS和它是什么关系。JMS是Java Message Service它也是支持上面这两种消息引擎模型的。严格来说它并非传输协议而仅仅是一组API罢了。不过可能是JMS太有名气以至于很多主流消息引擎系统都支持JMS规范比如ActiveMQ、RabbitMQ、IBM的WebSphere MQ和Apache Kafka。当然Kafka并未完全遵照JMS规范相反它另辟蹊径探索出了一条特有的道路。
好了,目前我们仅仅是了解了消息引擎系统是做什么的以及怎么做的,但还有个重要的问题是为什么要使用它。
依旧拿上面“民间版”举例我们不禁要问为什么系统A不能直接发送消息给系统B中间还要隔一个消息引擎呢
答案就是“削峰填谷”。这四个字简直比消息引擎本身还要有名气。
我翻了很多文献,最常见的就是这四个字。所谓的“削峰填谷”就是指缓冲上下游瞬时突发流量,使其更平滑。特别是对于那种发送能力很强的上游系统,如果没有消息引擎的保护,“脆弱”的下游系统可能会直接被压垮导致全链路服务“雪崩”。但是,一旦有了消息引擎,它能够有效地对抗上游的流量冲击,真正做到将上游的“峰”填满到“谷”中,避免了流量的震荡。消息引擎系统的另一大好处在于发送方和接收方的松耦合,这也在一定程度上简化了应用的开发,减少了系统间不必要的交互。
说了这么多可能你对“削峰填谷”并没有太多直观的感受。我还是举个例子来说明一下Kafka在这中间是怎么去“抗”峰值流量的吧。回想一下你在极客时间是如何购买这个课程的。如果我没记错的话极客时间每门课程都有一个专门的订阅按钮点击之后进入到付费页面。这个简单的流程中就可能包含多个子服务比如点击订阅按钮会调用订单系统生成对应的订单而处理该订单会依次调用下游的多个子系统服务 比如调用支付宝和微信支付的接口、查询你的登录信息、验证课程信息等。显然上游的订单操作比较简单它的TPS要远高于处理订单的下游服务因此如果上下游系统直接对接势必会出现下游服务无法及时处理上游订单从而造成订单堆积的情形。特别是当出现类似于秒杀这样的业务时上游订单流量会瞬时增加可能出现的结果就是直接压跨下游子系统服务。
解决此问题的一个常见做法是我们对上游系统进行限速但这种做法对上游系统而言显然是不合理的毕竟问题并不出现在它那里。所以更常见的办法是引入像Kafka这样的消息引擎系统来对抗这种上下游系统TPS的错配以及瞬时峰值流量。
还是这个例子当引入了Kafka之后。上游订单服务不再直接与下游子服务进行交互。当新订单生成后它仅仅是向Kafka Broker发送一条订单消息即可。类似地下游的各个子服务订阅Kafka中的对应主题并实时从该主题的各自分区Partition中获取到订单消息进行处理从而实现了上游订单服务与下游订单处理服务的解耦。这样当出现秒杀业务时Kafka能够将瞬时增加的订单流量全部以消息形式保存在对应的主题中既不影响上游服务的TPS同时也给下游子服务留出了充足的时间去消费它们。这就是Kafka这类消息引擎系统的最大意义所在。
如果你对Kafka Broker、主题和分区等术语还不甚了解的话也不必担心我会在专栏后面专门花时间介绍一下Kafka的常见概念和术语。
在今天结束之前我还想和你分享一个自己的小故事。在2015年那会儿我花了将近1年的时间阅读Kafka源代码期间多次想要放弃。你要知道阅读将近50万行源码是多么痛的领悟。我还记得当初为了手写源代码注释自己写满了一个厚厚的笔记本。不过幸运的是我坚持了下来之前的所有努力也没有白费以至于后面写书、写极客时间专栏就变成了一件件水到渠成的事情。
最后我想送给你一句话:聪明人也要下死功夫。我不记得这是曾国藩说的还是季羡林说的,但这句话对我有很大影响,当我感到浮躁的时候它能帮我静下心来踏踏实实做事情。希望这句话对你也有所启发。切记:聪明人要下死功夫!
开放讨论
请谈谈你对消息引擎系统的理解,或者分享一下你的公司或组织是怎么使用消息引擎来处理实际问题的。
欢迎写下你的思考和答案,我们一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@ -0,0 +1,75 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
02 一篇文章带你快速搞定Kafka术语
你好我是胡夕。今天我们正式开启Apache Kafka学习之旅。
在Kafka的世界中有很多概念和术语是需要你提前理解并熟练掌握的这对于后面你深入学习Kafka各种功能和特性将大有裨益。下面我来盘点一下Kafka的各种术语。
在专栏的第一期我说过Kafka属于分布式的消息引擎系统它的主要功能是提供一套完备的消息发布与订阅解决方案。在Kafka中发布订阅的对象是主题Topic你可以为每个业务、每个应用甚至是每类数据都创建专属的主题。
向主题发布消息的客户端应用程序称为生产者Producer生产者程序通常持续不断地向一个或多个主题发送消息而订阅这些主题消息的客户端应用程序就被称为消费者Consumer。和生产者类似消费者也能够同时订阅多个主题的消息。我们把生产者和消费者统称为客户端Clients。你可以同时运行多个生产者和消费者实例这些实例会不断地向Kafka集群中的多个主题生产和消费消息。
有客户端自然也就有服务器端。Kafka的服务器端由被称为Broker的服务进程构成即一个Kafka集群由多个Broker组成Broker负责接收和处理客户端发送过来的请求以及对消息进行持久化。虽然多个Broker进程能够运行在同一台机器上但更常见的做法是将不同的Broker分散运行在不同的机器上这样如果集群中某一台机器宕机即使在它上面运行的所有Broker进程都挂掉了其他机器上的Broker也依然能够对外提供服务。这其实就是Kafka提供高可用的手段之一。
实现高可用的另一个手段就是备份机制Replication。备份的思想很简单就是把相同的数据拷贝到多台机器上而这些相同的数据拷贝在Kafka中被称为副本Replica。好吧其实在整个分布式系统里好像都叫这个名字。副本的数量是可以配置的这些副本保存着相同的数据但却有不同的角色和作用。Kafka定义了两类副本领导者副本Leader Replica和追随者副本Follower Replica。前者对外提供服务这里的对外指的是与客户端程序进行交互而后者只是被动地追随领导者副本而已不能与外界进行交互。当然了你可能知道在很多其他系统中追随者副本是可以对外提供服务的比如MySQL的从库是可以处理读操作的但是在Kafka中追随者副本不会对外提供服务。对了一个有意思的事情是现在已经不提倡使用Master-Slave来指代这种主从关系了毕竟Slave有奴隶的意思在美国这种严禁种族歧视的国度这种表述有点政治不正确了所以目前大部分的系统都改成Leader-Follower了。
副本的工作机制也很简单:生产者总是向领导者副本写消息;而消费者总是从领导者副本读消息。至于追随者副本,它只做一件事:向领导者副本发送请求,请求领导者把最新生产的消息发给它,这样它能保持与领导者的同步。
虽然有了副本机制可以保证数据的持久化或消息不丢失但没有解决伸缩性的问题。伸缩性即所谓的Scalability是分布式系统中非常重要且必须要谨慎对待的问题。什么是伸缩性呢我们拿副本来说虽然现在有了领导者副本和追随者副本但倘若领导者副本积累了太多的数据以至于单台Broker机器都无法容纳了此时应该怎么办呢一个很自然的想法就是能否把数据分割成多份保存在不同的Broker上如果你就是这么想的那么恭喜你Kafka就是这么设计的。
这种机制就是所谓的分区Partitioning。如果你了解其他分布式系统你可能听说过分片、分区域等提法比如MongoDB和Elasticsearch中的Sharding、HBase中的Region其实它们都是相同的原理只是Partitioning是最标准的名称。
Kafka中的分区机制指的是将每个主题划分成多个分区Partition每个分区是一组有序的消息日志。生产者生产的每条消息只会被发送到一个分区中也就是说如果向一个双分区的主题发送一条消息这条消息要么在分区0中要么在分区1中。如你所见Kafka的分区编号是从0开始的如果Topic有100个分区那么它们的分区号就是从0到99。
讲到这里你可能有这样的疑问刚才提到的副本如何与这里的分区联系在一起呢实际上副本是在分区这个层级定义的。每个分区下可以配置若干个副本其中只能有1个领导者副本和N-1个追随者副本。生产者向分区写入消息每条消息在分区中的位置信息由一个叫位移Offset的数据来表征。分区位移总是从0开始假设一个生产者向一个空分区写入了10条消息那么这10条消息的位移依次是0、1、2、……、9。
至此我们能够完整地串联起Kafka的三层消息架构
第一层是主题层每个主题可以配置M个分区而每个分区又可以配置N个副本。
第二层是分区层每个分区的N个副本中只能有一个充当领导者角色对外提供服务其他N-1个副本是追随者副本只是提供数据冗余之用。
第三层是消息层分区中包含若干条消息每条消息的位移从0开始依次递增。
最后,客户端程序只能与分区的领导者副本进行交互。
讲完了消息层次我们来说说Kafka Broker是如何持久化数据的。总的来说Kafka使用消息日志Log来保存数据一个日志就是磁盘上一个只能追加写Append-only消息的物理文件。因为只能追加写入故避免了缓慢的随机I/O操作改为性能较好的顺序I/O写操作这也是实现Kafka高吞吐量特性的一个重要手段。不过如果你不停地向一个日志写入消息最终也会耗尽所有的磁盘空间因此Kafka必然要定期地删除消息以回收磁盘。怎么删除呢简单来说就是通过日志段Log Segment机制。在Kafka底层一个日志又进一步细分成多个日志段消息被追加写到当前最新的日志段中当写满了一个日志段后Kafka会自动切分出一个新的日志段并将老的日志段封存起来。Kafka在后台还有定时任务会定期地检查老的日志段是否能够被删除从而实现回收磁盘空间的目的。
这里再重点说说消费者。在专栏的第一期中我提到过两种消息模型即点对点模型Peer to PeerP2P和发布订阅模型。这里面的点对点指的是同一条消息只能被下游的一个消费者消费其他消费者则不能染指。在Kafka中实现这种P2P模型的方法就是引入了消费者组Consumer Group。所谓的消费者组指的是多个消费者实例共同组成一个组来消费一组主题。这组主题中的每个分区都只会被组内的一个消费者实例消费其他消费者实例不能消费它。为什么要引入消费者组呢主要是为了提升消费者端的吞吐量。多个消费者实例同时消费加速整个消费端的吞吐量TPS。我会在专栏的后面详细介绍消费者组机制所以现在你只需要了解消费者组是做什么的即可。另外这里的消费者实例可以是运行消费者应用的进程也可以是一个线程它们都称为一个消费者实例Consumer Instance
消费者组里面的所有消费者实例不仅“瓜分”订阅主题的数据而且更酷的是它们还能彼此协助。假设组内某个实例挂掉了Kafka能够自动检测到然后把这个Failed实例之前负责的分区转移给其他活着的消费者。这个过程就是Kafka中大名鼎鼎的“重平衡”Rebalance。嗯其实既是大名鼎鼎也是臭名昭著因为由重平衡引发的消费者问题比比皆是。事实上目前很多重平衡的Bug社区都无力解决。
每个消费者在消费消息的过程中必然需要有个字段记录它当前消费到了分区的哪个位置上这个字段就是消费者位移Consumer Offset。注意这和上面所说的位移完全不是一个概念。上面的“位移”表征的是分区内的消息位置它是不变的即一旦消息被成功写入到一个分区上它的位移值就是固定的了。而消费者位移则不同它可能是随时变化的毕竟它是消费者消费进度的指示器嘛。另外每个消费者有着自己的消费者位移因此一定要区分这两类位移的区别。我个人把消息在分区中的位移称为分区位移而把消费者端的位移称为消费者位移。
小结
我来总结一下今天提到的所有名词术语:
消息Record。Kafka是消息引擎嘛这里的消息就是指Kafka处理的主要对象。
主题Topic。主题是承载消息的逻辑容器在实际使用中多用来区分具体的业务。
分区Partition。一个有序不变的消息序列。每个主题下可以有多个分区。
消息位移Offset。表示分区中每条消息的位置信息是一个单调递增且不变的值。
副本Replica。Kafka中同一条消息能够被拷贝到多个地方以提供数据冗余这些地方就是所谓的副本。副本还分为领导者副本和追随者副本各自有不同的角色划分。副本是在分区层级下的即每个分区可配置多个副本实现高可用。
生产者Producer。向主题发布新消息的应用程序。
消费者Consumer。从主题订阅新消息的应用程序。
消费者位移Consumer Offset。表征消费者消费进度每个消费者都有自己的消费者位移。
消费者组Consumer Group。多个消费者实例共同组成的一个组同时消费多个分区以实现高吞吐。
重平衡Rebalance。消费者组内某个消费者实例挂掉后其他消费者实例自动重新分配订阅主题分区的过程。Rebalance是Kafka消费者端实现高可用的重要手段。
最后我用一张图来展示上面提到的这些概念,希望这张图能够帮助你形象化地理解所有这些概念:
开放讨论
请思考一下为什么Kafka不像MySQL那样允许追随者副本对外提供读服务
欢迎写下你的思考和答案,我们一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@ -0,0 +1,74 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
03 Kafka只是消息引擎系统吗
你好我是胡夕。今天我们来聊一个老生常谈的话题Kafka只是消息引擎系统吗
要搞清楚这个问题我们不可避免地要了解一下Apache Kafka的发展历程。有的时候我们会觉得说了解一个系统或框架的前世今生似乎没什么必要直接开始学具体的技术不是更快更好吗其实不论是学习哪种技术直接扎到具体的细节中亦或是从一个很小的点开始学习你很快就会感到厌烦。为什么呢因为你虽然快速地搞定了某个技术细节但无法建立全局的认知观这会导致你只是在单个的点上有所进展却没法将其串联成一条线进而扩展成一个面从而实现系统地学习。
我这么说是有依据的因为这就是我当初学习Kafka的方式。你可能不会相信我阅读Kafka源码就是从utils包开始的。显然我们不用看源码也知道这玩意是干什么用的对吧就是个工具类包嘛而且这种阅读源码的方式是极其低效的。就像我说的我是在一个点一个点地学习但全部学完之后压根没有任何感觉依然不了解Kafka因为不知道这些包中的代码组合在一起能达成什么效果。所以我说它是很低效的学习方法。
后来我修改了学习的方法转而从自上而下的角度去理解Kafka竟然发现了很多之前学习过程中忽略掉的东西。更特别的是我发现这种学习方法能够帮助我维持较长时间的学习兴趣不会阶段性地产生厌烦情绪。特别是在了解Apache Kafka整个发展历史的过程中我愉快地学到了很多运营大型开源软件社区的知识和经验可谓是技术之外的一大收获。
纵观Kafka的发展脉络它的确是从消息引擎起家的但正如文章标题所问Apache Kafka真的只是消息引擎吗通常在回答这个问题之前很多文章可能就要这样展开了那我们先来讨论下什么是消息引擎以及消息引擎能做什么事情。算了我还是直给吧就不从“唐尧虞舜”说起了。这个问题的答案是Apache Kafka是消息引擎系统也是一个分布式流处理平台Distributed Streaming Platform。如果你通读全篇文字但只能记住一句话我希望你记住的就是这句。再强调一遍Kafka是消息引擎系统也是分布式流处理平台。
众所周知Kafka是LinkedIn公司内部孵化的项目。根据我和Kafka创始团队成员的交流以及查阅到的公开信息显示LinkedIn最开始有强烈的数据强实时处理方面的需求其内部的诸多子系统要执行多种类型的数据处理与分析主要包括业务系统和应用程序性能监控以及用户行为数据处理等。
当时他们碰到的主要问题包括:
数据正确性不足。因为数据的收集主要采用轮询Polling的方式如何确定轮询的间隔时间就变成了一个高度经验化的事情。虽然可以采用一些类似于启发式算法Heuristic来帮助评估间隔时间值但一旦指定不当必然会造成较大的数据偏差。
系统高度定制化,维护成本高。各个业务子系统都需要对接数据收集模块,引入了大量的定制开销和人工成本。
为了解决这些问题LinkedIn工程师尝试过使用ActiveMQ来解决这些问题但效果并不理想。显然需要有一个“大一统”的系统来取代现有的工作方式而这个系统就是Kafka。
Kafka自诞生伊始是以消息引擎系统的面目出现在大众视野中的。如果翻看0.10.0.0之前的官网说明你会发现Kafka社区将其清晰地定位为一个分布式、分区化且带备份功能的提交日志Commit Log服务。
这里引出一个题外话你可能好奇Kafka这个名字的由来实际上Kafka作者之一Jay Kreps曾经谈及过命名的原因。
因为Kafka系统的写性能很强所以找了个作家的名字来命名似乎是一个好主意。大学期间我上了很多文学课非常喜欢Franz Kafka这个作家另外为开源软件起这个名字听上去很酷。
言归正传Kafka在设计之初就旨在提供三个方面的特性
提供一套API实现生产者和消费者
降低网络传输和磁盘存储开销;
实现高伸缩性架构。
在专栏后面的课程中我们将陆续探讨Kafka是如何做到以上三点的。总之随着Kafka的不断完善Jay等大神们终于意识到将其开源惠及更多的人是一个非常棒的主意因此在2011年Kafka正式进入到Apache基金会孵化并于次年10月顺利毕业成为Apache顶级项目。
开源之后的Kafka被越来越多的公司应用到它们企业内部的数据管道中特别是在大数据工程领域Kafka在承接上下游、串联数据流管道方面发挥了重要的作用所有的数据几乎都要从一个系统流入Kafka然后再流向下游的另一个系统中。这样的使用方式屡见不鲜以至于引发了Kafka社区的思考与其我把数据从一个系统传递到下一个系统中做处理我为何不自己实现一套流处理框架呢基于这个考量Kafka社区于0.10.0.0版本正式推出了流处理组件Kafka Streams也正是从这个版本开始Kafka正式“变身”为分布式的流处理平台而不仅仅是消息引擎系统了。今天Apache Kafka是和Apache Storm、Apache Spark和Apache Flink同等级的实时流处理平台。
诚然目前国内对Kafka是流处理平台的认知还尚不普及其核心的流处理组件Kafka Streams更是少有大厂在使用。但我们也欣喜地看到随着在Kafka峰会上各路大神们的鼎力宣传如今利用Kafka构建流处理平台的案例层出不穷而了解并有意愿使用Kafka Streams的厂商也是越来越多因此我个人对于Kafka流处理平台的前景也是非常乐观的。
你可能会有这样的疑问作为流处理平台Kafka与其他主流大数据流式计算框架相比优势在哪里呢我能想到的有两点。
第一点是更容易实现端到端的正确性Correctness。Google大神Tyler曾经说过流处理要最终替代它的“兄弟”批处理需要具备两点核心优势要实现正确性和提供能够推导时间的工具。实现正确性是流处理能够匹敌批处理的基石。正确性一直是批处理的强项而实现正确性的基石则是要求框架能提供精确一次处理语义即处理一条消息有且只有一次机会能够影响系统状态。目前主流的大数据流处理框架都宣称实现了精确一次处理语义但这是有限定条件的即它们只能实现框架内的精确一次处理语义无法实现端到端的。
这是为什么呢因为当这些框架与外部消息引擎系统结合使用时它们无法影响到外部系统的处理语义所以如果你搭建了一套环境使得Spark或Flink从Kafka读取消息之后进行有状态的数据计算最后再写回Kafka那么你只能保证在Spark或Flink内部这条消息对于状态的影响只有一次。但是计算结果有可能多次写入到Kafka因为它们不能控制Kafka的语义处理。相反地Kafka则不是这样因为所有的数据流转和计算都在Kafka内部完成故Kafka可以实现端到端的精确一次处理语义。
可能助力Kafka胜出的第二点是它自己对于流式计算的定位。官网上明确标识Kafka Streams是一个用于搭建实时流处理的客户端库而非是一个完整的功能系统。这就是说你不能期望着Kafka提供类似于集群调度、弹性部署等开箱即用的运维特性你需要自己选择适合的工具或系统来帮助Kafka流处理应用实现这些功能。
读到这你可能会说这怎么算是优点呢坦率来说这的确是一个“双刃剑”的设计也是Kafka社区“剑走偏锋”不正面PK其他流计算框架的特意考量。大型公司的流处理平台一定是大规模部署的因此具备集群调度功能以及灵活的部署方案是不可或缺的要素。但毕竟这世界上还存在着很多中小企业它们的流处理数据量并不巨大逻辑也并不复杂部署几台或十几台机器足以应付。在这样的需求之下搭建重量级的完整性平台实在是“杀鸡焉用牛刀”而这正是Kafka流处理组件的用武之地。因此从这个角度来说未来在流处理框架中Kafka应该是有一席之地的。
除了消息引擎和流处理平台Kafka还有别的用途吗当然有你能想象吗Kafka能够被用作分布式存储系统。Kafka作者之一Jay Kreps曾经专门写过一篇文章阐述为什么能把Kafka用作分布式存储。不过我觉得你姑且了解下就好了我从没有见过在实际生产环境中有人把Kafka当作持久化存储来用 。
说了这么多我只想阐述这样的一个观点Apache Kafka从一个优秀的消息引擎系统起家逐渐演变成现在分布式的流处理平台。你不仅要熟练掌握它作为消息引擎系统的非凡特性及使用技巧最好还要多了解下其流处理组件的设计与案例应用。
开放讨论
你觉得Kafka未来的演进路线是怎么样的如果你是Kafka社区的“掌舵人”你准备带领整个社区奔向什么方向呢提示下你可以把自己想象成Linus再去思考
欢迎写下你的思考和答案,我们一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@ -0,0 +1,97 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
04 我应该选择哪种Kafka
在专栏上一期中我们谈了Kafka当前的定位问题Kafka不再是一个单纯的消息引擎系统而是能够实现精确一次Exactly-once处理语义的实时流处理平台。
你可能听说过Apache Storm、Apache Spark Streaming抑或是Apache Flink它们在大规模流处理领域可都是响当当的名字。令人高兴的是Kafka经过这么长时间的不断迭代现在已经能够稍稍比肩这些框架了。我在这里使用了“稍稍”这个字眼一方面想表达Kafka社区对于这些框架心存敬意另一方面也想表达目前国内鲜有大厂将Kafka用于流处理的尴尬境地毕竟Kafka是从消息引擎“半路出家”转型成流处理平台的它在流处理方面的表现还需要经过时间的检验。
如果我们把视角从流处理平台扩展到流处理生态圈Kafka更是还有很长的路要走。前面我提到过Kafka Streams组件正是它提供了Kafka实时处理流数据的能力。但是其实还有一个重要的组件我没有提及那就是Kafka Connect。
我们在评估流处理平台的时候框架本身的性能、所提供操作算子Operator的丰富程度固然是重要的评判指标但框架与上下游交互的能力也是非常重要的。能够与之进行数据传输的外部系统越多围绕它打造的生态圈就越牢固因而也就有更多的人愿意去使用它从而形成正向反馈不断地促进该生态圈的发展。就Kafka而言Kafka Connect通过一个个具体的连接器Connector串联起上下游的外部系统。
整个Kafka生态圈如下图所示。值得注意的是这张图中的外部系统只是Kafka Connect组件支持的一部分而已。目前还有一个可喜的趋势是使用Kafka Connect组件的用户越来越多相信在未来会有越来越多的人开发自己的连接器。
说了这么多你可能会问这和今天的主题有什么关系呢其实清晰地了解Kafka的发展脉络和生态圈现状对于指导我们选择合适的Kafka版本大有裨益。下面我们就进入今天的主题——如何选择Kafka版本
你知道几种Kafka
咦? Kafka不是一个开源框架吗什么叫有几种Kafka啊 实际上Kafka的确有好几种这里我不是指它的版本而是指存在多个组织或公司发布不同的Kafka。你一定听说过Linux发行版吧比如我们熟知的CentOS、RedHat、Ubuntu等它们都是Linux系统但为什么有不同的名字呢其实就是因为它们是不同公司发布的Linux系统即不同的发行版。虽说在Kafka领域没有发行版的概念但你姑且可以这样近似地认为市面上的确存在着多个Kafka“发行版”。
下面我就来梳理一下这些所谓的“发行版”以及你应该如何选择它们。当然了“发行版”这个词用在Kafka框架上并不严谨但为了便于我们区分这些不同的Kafka我还是勉强套用一下吧。不过切记当你以后和别人聊到这个话题的时候最好不要提及“发行版”这个词 因为这种提法在Kafka生态圈非常陌生说出来难免贻笑大方。
1. Apache Kafka
Apache Kafka是最“正宗”的Kafka也应该是你最熟悉的发行版了。自Kafka开源伊始它便在Apache基金会孵化并最终毕业成为顶级项目它也被称为社区版Kafka。咱们专栏就是以这个版本的Kafka作为模板来学习的。更重要的是它是后面其他所有发行版的基础。也就是说后面提到的发行版要么是原封不动地继承了Apache Kafka要么是在此之上扩展了新功能总之Apache Kafka是我们学习和使用Kafka的基础。
2. Confluent Kafka
我先说说Confluent公司吧。2014年Kafka的3个创始人Jay Kreps、Naha Narkhede和饶军离开LinkedIn创办了Confluent公司专注于提供基于Kafka的企业级流处理解决方案。2019年1月Confluent公司成功融资D轮1.25亿美元估值也到了25亿美元足见资本市场的青睐。
这里说点题外话, 饶军是我们中国人清华大学毕业的大神级人物。我们已经看到越来越多的Apache顶级项目创始人中出现了中国人的身影另一个例子就是Apache Pulsar它是一个以打败Kafka为目标的新一代消息引擎系统。至于在开源社区中活跃的国人更是数不胜数这种现象实在令人振奋。
还说回Confluent公司它主要从事商业化Kafka工具开发并在此基础上发布了Confluent Kafka。Confluent Kafka提供了一些Apache Kafka没有的高级特性比如跨数据中心备份、Schema注册中心以及集群监控工具等。
3. Cloudera/Hortonworks Kafka
Cloudera提供的CDH和Hortonworks提供的HDP是非常著名的大数据平台里面集成了目前主流的大数据框架能够帮助用户实现从分布式存储、集群调度、流处理到机器学习、实时数据库等全方位的数据处理。我知道很多创业公司在搭建数据平台时首选就是这两个产品。不管是CDH还是HDP里面都集成了Apache Kafka因此我把这两款产品中的Kafka称为CDH Kafka和HDP Kafka。
当然在2018年10月两家公司宣布合并共同打造世界领先的数据平台也许以后CDH和HDP也会合并成一款产品但能肯定的是Apache Kafka依然会包含其中并作为新数据平台的一部分对外提供服务。
特点比较
Okay说完了目前市面上的这些Kafka我来对比一下它们的优势和劣势。
1. Apache Kafka
对Apache Kafka而言它现在依然是开发人数最多、版本迭代速度最快的Kafka。在2018年度Apache基金会邮件列表开发者数量最多的Top 5排行榜中Kafka社区邮件组排名第二位。如果你使用Apache Kafka碰到任何问题并提交问题到社区社区都会比较及时地响应你。这对于我们Kafka普通使用者来说无疑是非常友好的。
但是Apache Kafka的劣势在于它仅仅提供最最基础的组件特别是对于前面提到的Kafka Connect而言社区版Kafka只提供一种连接器即读写磁盘文件的连接器而没有与其他外部系统交互的连接器在实际使用过程中需要自行编写代码实现这是它的一个劣势。另外Apache Kafka没有提供任何监控框架或工具。显然在线上环境不加监控肯定是不可行的你必然需要借助第三方的监控框架实现对Kafka的监控。好消息是目前有一些开源的监控框架可以帮助用于监控Kafka比如Kafka manager
总而言之如果你仅仅需要一个消息引擎系统亦或是简单的流处理应用场景同时需要对系统有较大把控度那么我推荐你使用Apache Kafka。
2. Confluent Kafka
下面来看Confluent Kafka。Confluent Kafka目前分为免费版和企业版两种。前者和Apache Kafka非常相像除了常规的组件之外免费版还包含Schema注册中心和REST proxy两大功能。前者是帮助你集中管理Kafka消息格式以实现数据前向/后向兼容后者用开放HTTP接口的方式允许你通过网络访问Kafka的各种功能这两个都是Apache Kafka所没有的。
除此之外免费版包含了更多的连接器它们都是Confluent公司开发并认证过的你可以免费使用它们。至于企业版它提供的功能就更多了。在我看来最有用的当属跨数据中心备份和集群监控两大功能了。多个数据中心之间数据的同步以及对集群的监控历来是Kafka的痛点Confluent Kafka企业版提供了强大的解决方案帮助你“干掉”它们。
不过Confluent Kafka的一大缺陷在于Confluent公司暂时没有发展国内业务的计划相关的资料以及技术支持都很欠缺很多国内Confluent Kafka使用者甚至无法找到对应的中文文档因此目前Confluent Kafka在国内的普及率是比较低的。
一言以蔽之如果你需要用到Kafka的一些高级特性那么推荐你使用Confluent Kafka。
3. CDH/HDP Kafka
最后说说大数据云公司发布的KafkaCDH/HDP Kafka。这些大数据平台天然集成了Apache Kafka通过便捷化的界面操作将Kafka的安装、运维、管理、监控全部统一在控制台中。如果你是这些平台的用户一定觉得非常方便因为所有的操作都可以在前端UI界面上完成而不必去执行复杂的Kafka命令。另外这些平台提供的监控界面也非常友好你通常不需要进行任何配置就能有效地监控 Kafka。
但是凡事有利就有弊这样做的结果是直接降低了你对Kafka集群的掌控程度。毕竟你对下层的Kafka集群一无所知你怎么能做到心中有数呢这种Kafka的另一个弊端在于它的滞后性。由于它有自己的发布周期因此是否能及时地包含最新版本的Kafka就成为了一个问题。比如CDH 6.1.0版本发布时Apache Kafka已经演进到了2.1.0版本但CDH中的Kafka依然是2.0.0版本显然那些在Kafka 2.1.0中修复的Bug只能等到CDH下次版本更新时才有可能被真正修复。
简单来说如果你需要快速地搭建消息引擎系统或者你需要搭建的是多框架构成的数据平台且Kafka只是其中一个组件那么我推荐你使用这些大数据云公司提供的Kafka。
小结
总结一下我们今天讨论了不同的Kafka“发行版”以及它们的优缺点根据这些优缺点我们可以有针对性地根据实际需求选择合适的Kafka。下一期我将带你领略Kafka各个阶段的发展历程这样我们选择Kafka功能特性的时候就有了依据在正式开启Kafka应用之路之前也夯实了理论基础。
最后我们来复习一下今天的内容:
Apache Kafka也称社区版Kafka。优势在于迭代速度快社区响应度高使用它可以让你有更高的把控度缺陷在于仅提供基础核心组件缺失一些高级的特性。
Confluent KafkaConfluent公司提供的Kafka。优势在于集成了很多高级特性且由Kafka原班人马打造质量上有保证缺陷在于相关文档资料不全普及率较低没有太多可供参考的范例。
CDH/HDP Kafka大数据云公司提供的Kafka内嵌Apache Kafka。优势在于操作简单节省运维成本缺陷在于把控度低演进速度较慢。
开放讨论
设想你是一家创业公司的架构师公司最近准备改造现有系统引入Kafka作为消息中间件衔接上下游业务。作为架构师的你会怎么选择合适的Kafka发行版呢
欢迎你写下自己的思考或疑问,我们一起讨论 。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@ -0,0 +1,69 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
05 聊聊Kafka的版本号
你好我是胡夕。今天我想和你聊聊如何选择Kafka版本号这个话题。今天要讨论的内容实在是太重要了我觉得它甚至是你日后能否用好Kafka的关键。
上一期我介绍了目前流行的几种Kafka发行版其实不论是哪种Kafka本质上都内嵌了最核心的Apache Kafka也就是社区版Kafka那今天我们就来说说Apache Kafka版本号的问题。在开始之前我想强调一下后面出现的所有“版本”这个词均表示Kafka具体的版本号而非上一篇中的Kafka种类这一点切记切记
那么现在你可能会有这样的疑问我为什么需要关心版本号的问题呢直接使用最新版本不就好了吗当然了这的确是一种有效的选择版本的策略但我想强调的是这种策略并非在任何场景下都适用。如果你不了解各个版本之间的差异和功能变化你怎么能够准确地评判某Kafka版本是不是满足你的业务需求呢因此在深入学习Kafka之前花些时间搞明白版本演进实际上是非常划算的一件事。
Kafka版本命名
当前Apache Kafka已经迭代到2.2版本社区正在为2.3.0发版日期进行投票相信2.3.0也会马上发布。但是稍微有些令人吃惊的是很多人对于Kafka的版本命名理解存在歧义。比如我们在官网上下载Kafka时会看到这样的版本
于是有些同学就会纳闷难道Kafka版本号不是2.11或2.12吗其实不然前面的版本号是编译Kafka源代码的Scala编译器版本。Kafka服务器端的代码完全由Scala语言编写Scala同时支持面向对象编程和函数式编程用Scala写成的源代码编译之后也是普通的“.class”文件因此我们说Scala是JVM系的语言它的很多设计思想都是为人称道的。
事实上目前Java新推出的很多功能都是在不断向Scala语言靠近罢了比如Lambda表达式、函数式接口、val变量等。一个有意思的事情是Kafka新版客户端代码完全由Java语言编写于是有些人展开了“Java VS Scala”的大讨论并从语言特性的角度尝试分析Kafka社区为什么放弃Scala转而使用Java重写客户端代码。其实事情远没有那么复杂仅仅是因为社区来了一批Java程序员而已而以前老的Scala程序员隐退罢了。可能有点跑题了但不管怎样我依然建议你有空去学学Scala语言。
回到刚才的版本号讨论。现在你应该知道了对于kafka-2.11-2.1.1的提法真正的Kafka版本号实际上是2.1.1。那么这个2.1.1又表示什么呢前面的2表示大版本号即Major Version中间的1表示小版本号或次版本号即Minor Version最后的1表示修订版本号也就是Patch号。Kafka社区在发布1.0.0版本后特意写过一篇文章宣布Kafka版本命名规则正式从4位演进到3位比如0.11.0.0版本就是4位版本号。
坦率说这里我和社区的意见是有点不同的。在我看来像0.11.0.0这样的版本虽然有4位版本号但其实它的大版本是0.11而不是0所以如果这样来看的话Kafka版本号从来都是由3个部分构成即“大版本号 - 小版本号 - Patch号”。这种视角可以统一所有的Kafka版本命名也方便我们日后的讨论。我们来复习一下假设碰到的Kafka版本是0.10.2.2你现在就知道了它的大版本是0.10小版本是2总共打了两个大的补丁Patch号是2。
Kafka版本演进
Kafka目前总共演进了7个大版本分别是0.7、0.8、0.9、0.10、0.11、1.0和2.0其中的小版本和Patch版本很多。哪些版本引入了哪些重大的功能改进关于这个问题我建议你最好能做到如数家珍因为这样不仅令你在和别人交谈Kafka时显得很酷而且如果你要向架构师转型或者已然是架构师那么这些都是能够帮助你进行技术选型、架构评估的重要依据。
我们先从0.7版本说起,实际上也没什么可说的,这是最早开源时的“上古”版本了,以至于我也从来都没有接触过。这个版本只提供了最基础的消息队列功能,甚至连副本机制都没有,我实在想不出有什么理由你要使用这个版本,因此一旦有人向你推荐这个版本,果断走开就好了。
Kafka从0.7时代演进到0.8之后正式引入了副本机制至此Kafka成为了一个真正意义上完备的分布式高可靠消息队列解决方案。有了副本备份机制Kafka就能够比较好地做到消息无丢失。那时候生产和消费消息使用的还是老版本的客户端API所谓的老版本是指当你用它们的API开发生产者和消费者应用时你需要指定ZooKeeper的地址而非Broker的地址。
如果你现在尚不能理解这两者的区别也没关系我会在专栏的后续文章中详细介绍它们。老版本客户端有很多的问题特别是生产者API它默认使用同步方式发送消息可以想见其吞吐量一定不会太高。虽然它也支持异步的方式但实际场景中可能会造成消息的丢失因此0.8.2.0版本社区引入了新版本Producer API即需要指定Broker地址的Producer。
据我所知国内依然有少部分用户在使用0.8.1.1、0.8.2版本。我的建议是尽量使用比较新的版本。如果你不能升级大版本我也建议你至少要升级到0.8.2.2这个版本因为该版本中老版本消费者API是比较稳定的。另外即使你升到了0.8.2.2也不要使用新版本Producer API此时它的Bug还非常多。
时间来到了2015年11月社区正式发布了0.9.0.0版本。在我看来这是一个重量级的大版本更迭0.9大版本增加了基础的安全认证/权限功能同时使用Java重写了新版本消费者API另外还引入了Kafka Connect组件用于实现高性能的数据抽取。如果这么多眼花缭乱的功能你一时无暇顾及那么我希望你记住这个版本的另一个好处那就是新版本Producer API在这个版本中算比较稳定了。如果你使用0.9作为线上环境不妨切换到新版本Producer这是此版本一个不太为人所知的优势。但和0.8.2引入新API问题类似不要使用新版本Consumer API因为Bug超多的绝对用到你崩溃。即使你反馈问题到社区社区也不会管的它会无脑地推荐你升级到新版本再试试因此千万别用0.9的新版本Consumer API。对于国内一些使用比较老的CDH的创业公司鉴于其内嵌的就是0.9版本,所以要格外注意这些问题。
0.10.0.0是里程碑式的大版本因为该版本引入了Kafka Streams。从这个版本起Kafka正式升级成分布式流处理平台虽然此时的Kafka Streams还基本不能线上部署使用。0.10大版本包含两个小版本0.10.1和0.10.2它们的主要功能变更都是在Kafka Streams组件上。如果你把Kafka用作消息引擎实际上该版本并没有太多的功能提升。不过在我的印象中自0.10.2.2版本起新版本Consumer API算是比较稳定了。如果你依然在使用0.10大版本我强烈建议你至少升级到0.10.2.2然后使用新版本Consumer API。还有个事情不得不提0.10.2.2修复了一个可能导致Producer性能降低的Bug。基于性能的缘故你也应该升级到0.10.2.2。
在2017年6月社区发布了0.11.0.0版本引入了两个重量级的功能变更一个是提供幂等性Producer API以及事务Transaction API另一个是对Kafka消息格式做了重构。
前一个好像更加吸引眼球一些毕竟Producer实现幂等性以及支持事务都是Kafka实现流处理结果正确性的基石。没有它们Kafka Streams在做流处理时无法向批处理那样保证结果的正确性。当然同样是由于刚推出此时的事务API有一些Bug不算十分稳定。另外事务API主要是为Kafka Streams应用服务的实际使用场景中用户利用事务API自行编写程序的成功案例并不多见。
第二个重磅改进是消息格式的变化。虽然它对用户是透明的但是它带来的深远影响将一直持续。因为格式变更引起消息格式转换而导致的性能问题在生产环境中屡见不鲜所以你一定要谨慎对待0.11版本的这个变化。不得不说的是这个版本中各个大功能组件都变得非常稳定了国内该版本的用户也很多应该算是目前最主流的版本之一了。也正是因为这个缘故社区为0.11大版本特意推出了3个Patch版本足见它的受欢迎程度。我的建议是如果你对1.0版本是否适用于线上环境依然感到困惑那么至少将你的环境升级到0.11.0.3,因为这个版本的消息引擎功能已经非常完善了。
最后我合并说下1.0和2.0版本吧因为在我看来这两个大版本主要还是Kafka Streams的各种改进在消息引擎方面并未引入太多的重大功能特性。Kafka Streams的确在这两个版本有着非常大的变化也必须承认Kafka Streams目前依然还在积极地发展着。如果你是Kafka Streams的用户至少选择2.0.0版本吧。
去年8月国外出了一本书叫Kafka Streams in Action中文版《Kafka Streams实战》它是基于Kafka Streams 1.0版本撰写的。最近我用2.0版本去运行书中的例子,居然很多都已经无法编译了,足见两个版本变化之大。不过如果你在意的依然是消息引擎,那么这两个大版本都是适合于生产环境的。
最后还有个建议不论你用的是哪个版本都请尽量保持服务器端版本和客户端版本一致否则你将损失很多Kafka为你提供的性能优化收益。
小结
我希望现在你对如何选择合适的Kafka版本能做到心中有数了。每个Kafka版本都有它恰当的使用场景和独特的优缺点切记不要一味追求最新版本。事实上我周围的很多工程师都秉承这样的观念不要成为最新版本的“小白鼠”。了解了各个版本的差异之后我相信你一定能够根据自己的实际情况做出最正确的选择。
开放讨论
如何评估Kafka版本升级这件事呢你和你所在的团队有什么独特的见解
欢迎你写下自己的思考或疑问,我们一起讨论 。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@ -0,0 +1,107 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
06 Kafka线上集群部署方案怎么做
专栏前面几期内容我分别从Kafka的定位、版本的变迁以及功能的演进等几个方面循序渐进地梳理了Apache Kafka的发展脉络。通过这些内容我希望你能清晰地了解Kafka是用来做什么的以及在实际生产环境中该如何选择Kafka版本更快地帮助你入门Kafka。
现在我们就来看看在生产环境中的Kafka集群方案该怎么做。既然是集群那必然就要有多个Kafka节点机器因为只有单台机器构成的Kafka伪集群只能用于日常测试之用根本无法满足实际的线上生产需求。而真正的线上环境需要仔细地考量各种因素结合自身的业务需求而制定。下面我就分别从操作系统、磁盘、磁盘容量和带宽等方面来讨论一下。
操作系统
首先我们先看看要把Kafka安装到什么操作系统上。说起操作系统可能你会问Kafka不是JVM系的大数据框架吗Java又是跨平台的语言把Kafka安装到不同的操作系统上会有什么区别吗其实区别相当大
的确如你所知Kafka由Scala语言和Java语言编写而成编译之后的源代码就是普通的“.class”文件。本来部署到哪个操作系统应该都是一样的但是不同操作系统的差异还是给Kafka集群带来了相当大的影响。目前常见的操作系统有3种Linux、Windows和macOS。应该说部署在Linux上的生产环境是最多的也有一些Kafka集群部署在Windows服务器上。Mac虽然也有macOS Server但是我怀疑是否有人特别是国内用户真的把生产环境部署在Mac服务器上。
如果考虑操作系统与Kafka的适配性Linux系统显然要比其他两个特别是Windows系统更加适合部署Kafka。虽然这个结论可能你不感到意外但其中具体的原因你也一定要了解。主要是在下面这三个方面上Linux的表现更胜一筹。
I/O模型的使用
数据网络传输效率
社区支持度
我分别来解释一下首先来看I/O模型。什么是I/O模型呢你可以近似地认为I/O模型就是操作系统执行I/O指令的方法。
主流的I/O模型通常有5种类型阻塞式I/O、非阻塞式I/O、I/O多路复用、信号驱动I/O和异步I/O。每种I/O模型都有各自典型的使用场景比如Java中Socket对象的阻塞模式和非阻塞模式就对应于前两种模型而Linux中的系统调用select函数就属于I/O多路复用模型大名鼎鼎的epoll系统调用则介于第三种和第四种模型之间至于第五种模型其实很少有Linux系统支持反而是Windows系统提供了一个叫IOCP线程模型属于这一种。
你不必详细了解每一种模型的实现细节通常情况下我们认为后一种模型会比前一种模型要高级比如epoll就比select要好了解到这一程度应该足以应付我们下面的内容了。
说了这么多I/O模型与Kafka的关系又是什么呢实际上Kafka客户端底层使用了Java的selectorselector在Linux上的实现机制是epoll而在Windows平台上的实现机制是select。因此在这一点上将Kafka部署在Linux上是有优势的因为能够获得更高效的I/O性能。
其次是网络传输效率的差别。你知道的Kafka生产和消费的消息都是通过网络传输的而消息保存在哪里呢肯定是磁盘。故Kafka需要在磁盘和网络间进行大量数据传输。如果你熟悉Linux你肯定听过零拷贝Zero Copy技术就是当数据在磁盘和网络进行传输时避免昂贵的内核态数据拷贝从而实现快速的数据传输。Linux平台实现了这样的零拷贝机制但有些令人遗憾的是在Windows平台上必须要等到Java 8的60更新版本才能“享受”到这个福利。一句话总结一下在Linux部署Kafka能够享受到零拷贝技术所带来的快速数据传输特性。
最后是社区的支持度。这一点虽然不是什么明显的差别但如果不了解的话可能比前两个因素对你的影响更大。简单来说就是社区目前对Windows平台上发现的Kafka Bug不做任何承诺。虽然口头上依然保证尽力去解决但根据我的经验Windows上的Bug一般是不会修复的。因此Windows平台上部署Kafka只适合于个人测试或用于功能验证千万不要应用于生产环境。
磁盘
如果问哪种资源对Kafka性能最重要磁盘无疑是要排名靠前的。在对Kafka集群进行磁盘规划时经常面对的问题是我应该选择普通的机械磁盘还是固态硬盘前者成本低且容量大但易损坏后者性能优势大不过单价高。我给出的建议是使用普通机械硬盘即可。
Kafka大量使用磁盘不假可它使用的方式多是顺序读写操作一定程度上规避了机械磁盘最大的劣势即随机读写操作慢。从这一点上来说使用SSD似乎并没有太大的性能优势毕竟从性价比上来说机械磁盘物美价廉而它因易损坏而造成的可靠性差等缺陷又由Kafka在软件层面提供机制来保证故使用普通机械磁盘是很划算的。
关于磁盘选择另一个经常讨论的话题就是到底是否应该使用磁盘阵列RAID。使用RAID的两个主要优势在于
提供冗余的磁盘存储空间
提供负载均衡
以上两个优势对于任何一个分布式系统都很有吸引力。不过就Kafka而言一方面Kafka自己实现了冗余机制来提供高可靠性另一方面通过分区的概念Kafka也能在软件层面自行实现负载均衡。如此说来RAID的优势就没有那么明显了。当然我并不是说RAID不好实际上依然有很多大厂确实是把Kafka底层的存储交由RAID的只是目前Kafka在存储这方面提供了越来越便捷的高可靠性方案因此在线上环境使用RAID似乎变得不是那么重要了。综合以上的考量我给出的建议是
追求性价比的公司可以不搭建RAID使用普通磁盘组成存储空间即可。
使用机械磁盘完全能够胜任Kafka线上环境。
磁盘容量
Kafka集群到底需要多大的存储空间这是一个非常经典的规划问题。Kafka需要将消息保存在底层的磁盘上这些消息默认会被保存一段时间然后自动被删除。虽然这段时间是可以配置的但你应该如何结合自身业务场景和存储需求来规划Kafka集群的存储容量呢
我举一个简单的例子来说明该如何思考这个问题。假设你所在公司有个业务每天需要向Kafka集群发送1亿条消息每条消息保存两份以防止数据丢失另外消息默认保存两周时间。现在假设消息的平均大小是1KB那么你能说出你的Kafka集群需要为这个业务预留多少磁盘空间吗
我们来计算一下每天1亿条1KB大小的消息保存两份且留存两周的时间那么总的空间大小就等于1亿 * 1KB * 2 / 1000 / 1000 = 200GB。一般情况下Kafka集群除了消息数据还有其他类型的数据比如索引数据等故我们再为这些数据预留出10%的磁盘空间因此总的存储容量就是220GB。既然要保存两周那么整体容量即为220GB * 14大约3TB左右。Kafka支持数据的压缩假设压缩比是0.75那么最后你需要规划的存储空间就是0.75 * 3 = 2.25TB。
总之在规划磁盘容量时你需要考虑下面这几个元素:
新增消息数
消息留存时间
平均消息大小
备份数
是否启用压缩
带宽
对于Kafka这种通过网络大量进行数据传输的框架而言带宽特别容易成为瓶颈。事实上在我接触的真实案例当中带宽资源不足导致Kafka出现性能问题的比例至少占60%以上。如果你的环境中还涉及跨机房传输,那么情况可能就更糟了。
如果你不是超级土豪的话我会认为你和我平时使用的都是普通的以太网络带宽也主要有两种1Gbps的千兆网络和10Gbps的万兆网络特别是千兆网络应该是一般公司网络的标准配置了。下面我就以千兆网络举一个实际的例子来说明一下如何进行带宽资源的规划。
与其说是带宽资源的规划其实真正要规划的是所需的Kafka服务器的数量。假设你公司的机房环境是千兆网络即1Gbps现在你有个业务其业务目标或SLA是在1小时内处理1TB的业务数据。那么问题来了你到底需要多少台Kafka服务器来完成这个业务呢
让我们来计算一下由于带宽是1Gbps即每秒处理1Gb的数据假设每台Kafka服务器都是安装在专属的机器上也就是说每台Kafka机器上没有混部其他服务毕竟真实环境中不建议这么做。通常情况下你只能假设Kafka会用到70%的带宽资源,因为总要为其他应用或进程留一些资源。
根据实际使用经验超过70%的阈值就有网络丢包的可能性了故70%的设定是一个比较合理的值也就是说单台Kafka服务器最多也就能使用大约700Mb的带宽资源。
稍等这只是它能使用的最大带宽资源你不能让Kafka服务器常规性使用这么多资源故通常要再额外预留出2/3的资源即单台服务器使用带宽700Mb / 3 ≈ 240Mbps。需要提示的是这里的2/3其实是相当保守的你可以结合你自己机器的使用情况酌情减少此值。
好了有了240Mbps我们就可以计算1小时内处理1TB数据所需的服务器数量了。根据这个目标我们每秒需要处理2336Mb的数据除以240约等于10台服务器。如果消息还需要额外复制两份那么总的服务器台数还要乘以3即30台。
怎么样,还是很简单的吧。用这种方法评估线上环境的服务器台数是比较合理的,而且这个方法能够随着你业务需求的变化而动态调整。
小结
所谓“兵马未动粮草先行”。与其盲目上马一套Kafka环境然后事后费力调整不如在一开始就思考好实际场景下业务所需的集群环境。在考量部署方案时需要通盘考虑不能仅从单个维度上进行评估。相信今天我们聊完之后你对如何规划Kafka生产环境一定有了一个清晰的认识。现在我来总结一下今天的重点
开放讨论
对于今天我所讲的这套评估方法,你有什么问题吗?你还能想出什么改进的方法吗?
欢迎你写下自己的思考或疑问,我们一起讨论 。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@ -0,0 +1,113 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
07 最最最重要的集群参数配置(上)
你好我是胡夕。今天我想和你聊聊最最最重要的Kafka集群配置。我这里用了3个“最”字并非哗众取宠而是因为有些配置的重要性并未体现在官方文档中并且从实际表现看很多参数对系统的影响要比从文档上看更加明显因此很有必要集中讨论一下。
我希望通过两期内容把这些重要的配置讲清楚。严格来说这些配置并不单单指Kafka服务器端的配置其中既有Broker端参数也有主题后面我用我们更熟悉的Topic表示级别的参数、JVM端参数和操作系统级别的参数。
需要你注意的是这里所说的Broker端参数也被称为静态参数Static Configs。我会在专栏后面介绍与静态参数相对应的动态参数。所谓静态参数是指你必须在Kafka的配置文件server.properties中进行设置的参数不管你是新增、修改还是删除。同时你必须重启Broker进程才能令它们生效。而主题级别参数的设置则有所不同Kafka提供了专门的kafka-configs命令来修改它们。至于JVM和操作系统级别参数它们的设置方法比较通用化我介绍的也都是标准的配置参数因此你应该很容易就能够对它们进行设置。
下面我先从Broker端参数说起。
Broker端参数
目前Kafka Broker提供了近200个参数这其中绝大部分参数都不用你亲自过问。当谈及这些参数的用法时网上的文章多是罗列出一些常见的参数然后一个一个地给出它们的定义事实上我以前写文章时也是这么做的。不过今天我打算换个方法按照大的用途类别一组一组地介绍它们希望可以更有针对性也更方便你记忆。
首先Broker是需要配置存储信息的即Broker使用哪些磁盘。那么针对存储信息的重要参数有以下这么几个
log.dirs这是非常重要的参数指定了Broker需要使用的若干个文件目录路径。要知道这个参数是没有默认值的这说明什么这说明它必须由你亲自指定。
log.dir注意这是dir结尾没有s说明它只能表示单个路径它是补充上一个参数用的。
这两个参数应该怎么设置呢很简单你只要设置log.dirs即第一个参数就好了不要设置log.dir。而且更重要的是在线上生产环境中一定要为log.dirs配置多个路径具体格式是一个CSV格式也就是用逗号分隔的多个路径比如/home/kafka1,/home/kafka2,/home/kafka3这样。如果有条件的话你最好保证这些目录挂载到不同的物理磁盘上。这样做有两个好处
提升读写性能:比起单块磁盘,多块物理磁盘同时读写数据有更高的吞吐量。
能够实现故障转移即Failover。这是Kafka 1.1版本新引入的强大功能。要知道在以前只要Kafka Broker使用的任何一块磁盘挂掉了整个Broker进程都会关闭。但是自1.1开始这种情况被修正了坏掉的磁盘上的数据会自动地转移到其他正常的磁盘上而且Broker还能正常工作。还记得上一期我们关于Kafka是否需要使用RAID的讨论吗这个改进正是我们舍弃RAID方案的基础没有这种Failover的话我们只能依靠RAID来提供保障。
下面说说与ZooKeeper相关的设置。首先ZooKeeper是做什么的呢它是一个分布式协调框架负责协调管理并保存Kafka集群的所有元数据信息比如集群都有哪些Broker在运行、创建了哪些Topic每个Topic都有多少分区以及这些分区的Leader副本都在哪些机器上等信息。
Kafka与ZooKeeper相关的最重要的参数当属zookeeper.connect。这也是一个CSV格式的参数比如我可以指定它的值为zk1:2181,zk2:2181,zk3:2181。2181是ZooKeeper的默认端口。
现在问题来了如果我让多个Kafka集群使用同一套ZooKeeper集群那么这个参数应该怎么设置呢这时候chroot就派上用场了。这个chroot是ZooKeeper的概念类似于别名。
如果你有两套Kafka集群假设分别叫它们kafka1和kafka2那么两套集群的zookeeper.connect参数可以这样指定zk1:2181,zk2:2181,zk3:2181/kafka1和zk1:2181,zk2:2181,zk3:2181/kafka2。切记chroot只需要写一次而且是加到最后的。我经常碰到有人这样指定zk1:2181/kafka1,zk2:2181/kafka2,zk3:2181/kafka3这样的格式是不对的。
第三组参数是与Broker连接相关的即客户端程序或其他Broker如何与该Broker进行通信的设置。有以下三个参数
listeners学名叫监听器其实就是告诉外部连接者要通过什么协议访问指定主机名和端口开放的Kafka服务。
advertised.listeners和listeners相比多了个advertised。Advertised的含义表示宣称的、公布的就是说这组监听器是Broker用于对外发布的。
host.name/port列出这两个参数就是想说你把它们忘掉吧压根不要为它们指定值毕竟都是过期的参数了。
我们具体说说监听器的概念,从构成上来说,它是若干个逗号分隔的三元组,每个三元组的格式为<协议名称主机名端口号>。这里的协议名称可能是标准的名字比如PLAINTEXT表示明文传输、SSL表示使用SSL或TLS加密传输等也可能是你自己定义的协议名字比如CONTROLLER: //localhost:9092。
一旦你自己定义了协议名称你必须还要指定listener.security.protocol.map参数告诉这个协议底层使用了哪种安全协议比如指定listener.security.protocol.map=CONTROLLER:PLAINTEXT表示CONTROLLER这个自定义协议底层使用明文不加密传输数据。
至于三元组中的主机名和端口号则比较直观不需要做过多解释。不过有个事情你还是要注意一下经常有人会问主机名这个设置中我到底使用IP地址还是主机名。这里我给出统一的建议最好全部使用主机名即Broker端和Client端应用配置中全部填写主机名。 Broker源代码中也使用的是主机名如果你在某些地方使用了IP地址进行连接可能会发生无法连接的问题。
第四组参数是关于Topic管理的。我来讲讲下面这三个参数
auto.create.topics.enable是否允许自动创建Topic。
unclean.leader.election.enable是否允许Unclean Leader选举。
auto.leader.rebalance.enable是否允许定期进行Leader选举。
我还是一个个说。
auto.create.topics.enable参数我建议最好设置成false即不允许自动创建Topic。在我们的线上环境里面有很多名字稀奇古怪的Topic我想大概都是因为该参数被设置成了true的缘故。
你可能有这样的经历要为名为test的Topic发送事件但是不小心拼写错误了把test写成了tst之后启动了生产者程序。恭喜你一个名为tst的Topic就被自动创建了。
所以我一直相信好的运维应该防止这种情形的发生特别是对于那些大公司而言每个部门被分配的Topic应该由运维严格把控决不能允许自行创建任何Topic。
第二个参数unclean.leader.election.enable是关闭Unclean Leader选举的。何谓Unclean还记得Kafka有多个副本这件事吗每个分区都有多个副本来提供高可用。在这些副本中只能有一个副本对外提供服务即所谓的Leader副本。
那么问题来了这些副本都有资格竞争Leader吗显然不是只有保存数据比较多的那些副本才有资格竞选那些落后进度太多的副本没资格做这件事。
好了现在出现这种情况了假设那些保存数据比较多的副本都挂了怎么办我们还要不要进行Leader选举了此时这个参数就派上用场了。
如果设置成false那么就坚持之前的原则坚决不能让那些落后太多的副本竞选Leader。这样做的后果是这个分区就不可用了因为没有Leader了。反之如果是true那么Kafka允许你从那些“跑得慢”的副本中选一个出来当Leader。这样做的后果是数据有可能就丢失了因为这些副本保存的数据本来就不全当了Leader之后它本人就变得膨胀了认为自己的数据才是权威的。
这个参数在最新版的Kafka中默认就是false本来不需要我特意提的但是比较搞笑的是社区对这个参数的默认值来来回回改了好几版了鉴于我不知道你用的是哪个版本的Kafka所以建议你还是显式地把它设置成false吧。
第三个参数auto.leader.rebalance.enable的影响貌似没什么人提但其实对生产环境影响非常大。设置它的值为true表示允许Kafka定期地对一些Topic分区进行Leader重选举当然这个重选举不是无脑进行的它要满足一定的条件才会发生。严格来说它与上一个参数中Leader选举的最大不同在于它不是选Leader而是换Leader比如Leader A一直表现得很好但若auto.leader.rebalance.enable=true那么有可能一段时间后Leader A就要被强行卸任换成Leader B。
你要知道换一次Leader代价很高的原本向A发送请求的所有客户端都要切换成向B发送请求而且这种换Leader本质上没有任何性能收益因此我建议你在生产环境中把这个参数设置成false。
最后一组参数是数据留存方面的,我分别介绍一下。
log.retention.{hours|minutes|ms}这是个“三兄弟”都是控制一条消息数据被保存多长时间。从优先级上来说ms设置最高、minutes次之、hours最低。
log.retention.bytes这是指定Broker为消息保存的总磁盘容量大小。
message.max.bytes控制Broker能够接收的最大消息大小。
先说这个“三兄弟”虽然ms设置有最高的优先级但是通常情况下我们还是设置hours级别的多一些比如log.retention.hours=168表示默认保存7天的数据自动删除7天前的数据。很多公司把Kafka当作存储来使用那么这个值就要相应地调大。
其次是这个log.retention.bytes。这个值默认是-1表明你想在这台Broker上保存多少数据都可以至少在容量方面Broker绝对为你开绿灯不会做任何阻拦。这个参数真正发挥作用的场景其实是在云上构建多租户的Kafka集群设想你要做一个云上的Kafka服务每个租户只能使用100GB的磁盘空间为了避免有个“恶意”租户使用过多的磁盘空间设置这个参数就显得至关重要了。
最后说说message.max.bytes。实际上今天我和你说的重要参数都是指那些不能使用默认值的参数这个参数也是一样默认的1000012太少了还不到1MB。实际场景中突破1MB的消息都是屡见不鲜的因此在线上环境中设置一个比较大的值还是比较保险的做法。毕竟它只是一个标尺而已仅仅衡量Broker能够处理的最大消息大小即使设置大一点也不会耗费什么磁盘空间的。
小结
再次强调一下今天我和你分享的所有参数都是那些要修改默认值的参数因为它们的默认值不适合一般的生产环境。当然我并不是说其他100多个参数就不重要。事实上在专栏的后面我们还会陆续提到其他的一些参数特别是那些和性能息息相关的参数。所以今天我提到的所有参数我希望作为一个最佳实践给到你可以有的放矢地帮助你规划和调整你的Kafka生产环境。
开放讨论
除了今天我分享的这些参数,还有哪些参数是你认为比较重要而文档中没有提及的?你曾踩过哪些关于参数配置的“坑”?欢迎提出来与我和大家一起讨论。
欢迎你写下自己的思考或疑问,我们一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@ -0,0 +1,111 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
08 最最最重要的集群参数配置(下)
今天我们继续来聊那些重要的Kafka集群配置下半部分主要是Topic级别参数、JVM参数以及操作系统参数的设置。
在上一期中我们讨论了Broker端参数设置的一些法则但其实Kafka也支持为不同的Topic设置不同的参数值。当前最新的2.2版本总共提供了大约25个Topic级别的参数当然我们也不必全部了解它们的作用这里我挑出了一些最关键的参数你一定要把它们掌握清楚。除了Topic级别的参数我今天还会给出一些重要的JVM参数和操作系统参数正确设置这些参数是搭建高性能Kafka集群的关键因素。
Topic级别参数
说起Topic级别的参数你可能会有这样的疑问如果同时设置了Topic级别参数和全局Broker参数到底听谁的呢哪个说了算呢答案就是Topic级别参数会覆盖全局Broker参数的值而每个Topic都能设置自己的参数值这就是所谓的Topic级别参数。
举个例子说明一下上一期我提到了消息数据的留存时间参数在实际生产环境中如果为所有Topic的数据都保存相当长的时间这样做既不高效也无必要。更适当的做法是允许不同部门的Topic根据自身业务需要设置自己的留存时间。如果只能设置全局Broker参数那么势必要提取所有业务留存时间的最大值作为全局参数值此时设置Topic级别参数把它覆盖就是一个不错的选择。
下面我们依然按照用途分组的方式引出重要的Topic级别参数。从保存消息方面来考量的话下面这组参数是非常重要的
retention.ms规定了该Topic消息被保存的时长。默认是7天即该Topic只保存最近7天的消息。一旦设置了这个值它会覆盖掉Broker端的全局参数值。
retention.bytes规定了要为该Topic预留多大的磁盘空间。和全局参数作用相似这个值通常在多租户的Kafka集群中会有用武之地。当前默认值是-1表示可以无限使用磁盘空间。
上面这些是从保存消息的维度来说的。如果从能处理的消息大小这个角度来看的话有一个参数是必须要设置的即max.message.bytes。它决定了Kafka Broker能够正常接收该Topic的最大消息大小。我知道目前在很多公司都把Kafka作为一个基础架构组件来运行上面跑了很多的业务数据。如果在全局层面上我们不好给出一个合适的最大消息值那么不同业务部门能够自行设定这个Topic级别参数就显得非常必要了。在实际场景中这种用法也确实是非常常见的。
好了你要掌握的Topic级别的参数就这么几个。下面我来说说怎么设置Topic级别参数吧。其实说到这个事情我是有点个人看法的我本人不太赞同那种做一件事情开放给你很多种选择的设计方式看上去好似给用户多种选择但实际上只会增加用户的学习成本。特别是系统配置如果你告诉我只能用一种办法来做我会很努力地把它学会反之如果你告诉我说有两种方法甚至是多种方法都可以实现那么我可能连学习任何一种方法的兴趣都没有了。Topic级别参数的设置就是这种情况我们有两种方式可以设置
创建Topic时进行设置
修改Topic时设置
我们先来看看如何在创建Topic时设置这些参数。我用上面提到的retention.ms和max.message.bytes举例。设想你的部门需要将交易数据发送到Kafka进行处理需要保存最近半年的交易数据同时这些数据很大通常都有几MB但一般不会超过5MB。现在让我们用以下命令来创建Topic
bin/kafka-topics.sh --bootstrap-server localhost:9092 --create --topic transaction --partitions 1 --replication-factor 1 --config retention.ms=15552000000 --config max.message.bytes=5242880
我们只需要知道Kafka开放了kafka-topics命令供我们来创建Topic即可。对于上面这样一条命令请注意结尾处的--config设置我们就是在config后面指定了想要设置的Topic级别参数。
下面看看使用另一个自带的命令kafka-configs来修改Topic级别参数。假设我们现在要发送最大值是10MB的消息该如何修改呢命令如下
bin/kafka-configs.sh --zookeeper localhost:2181 --entity-type topics --entity-name transaction --alter --add-config max.message.bytes=10485760
总体来说你只能使用这么两种方式来设置Topic级别参数。我个人的建议是你最好始终坚持使用第二种方式来设置并且在未来Kafka社区很有可能统一使用kafka-configs脚本来调整Topic级别参数。
JVM参数
我在专栏前面提到过Kafka服务器端代码是用Scala语言编写的但终归还是编译成Class文件在JVM上运行因此JVM参数设置对于Kafka集群的重要性不言而喻。
首先我先说说Java版本我个人极其不推荐将Kafka运行在Java 6或7的环境上。Java 6实在是太过陈旧了没有理由不升级到更新版本。另外Kafka自2.0.0版本开始已经正式摒弃对Java 7的支持了所以有条件的话至少使用Java 8吧。
说到JVM端设置堆大小这个参数至关重要。虽然在后面我们还会讨论如何调优Kafka性能的问题但现在我想无脑给出一个通用的建议将你的JVM堆大小设置成6GB吧这是目前业界比较公认的一个合理值。我见过很多人就是使用默认的Heap Size来跑Kafka说实话默认的1GB有点小毕竟Kafka Broker在与客户端进行交互时会在JVM堆上创建大量的ByteBuffer实例Heap Size不能太小。
JVM端配置的另一个重要参数就是垃圾回收器的设置也就是平时常说的GC设置。如果你依然在使用Java 7那么可以根据以下法则选择合适的垃圾回收器
如果Broker所在机器的CPU资源非常充裕建议使用CMS收集器。启用方法是指定-XX:+UseCurrentMarkSweepGC。
否则,使用吞吐量收集器。开启方法是指定-XX:+UseParallelGC。
当然了如果你在使用Java 8那么可以手动设置使用G1收集器。在没有任何调优的情况下G1表现得要比CMS出色主要体现在更少的Full GC需要调整的参数更少等所以使用G1就好了。
现在我们确定好了要设置的JVM参数我们该如何为Kafka进行设置呢有些奇怪的是这个问题居然在Kafka官网没有被提及。其实设置的方法也很简单你只需要设置下面这两个环境变量即可
KAFKA_HEAP_OPTS指定堆大小。
KAFKA_JVM_PERFORMANCE_OPTS指定GC参数。
比如你可以这样启动Kafka Broker即在启动Kafka Broker之前先设置上这两个环境变量
$> export KAFKA_HEAP_OPTS=--Xms6g --Xmx6g
$> export KAFKA_JVM_PERFORMANCE_OPTS= -server -XX:+UseG1GC -XX:MaxGCPauseMillis=20 -XX:InitiatingHeapOccupancyPercent=35 -XX:+ExplicitGCInvokesConcurrent -Djava.awt.headless=true
$> bin/kafka-server-start.sh config/server.properties
操作系统参数
最后我们来聊聊Kafka集群通常都需要设置哪些操作系统参数。通常情况下Kafka并不需要设置太多的OS参数但有些因素最好还是关注一下比如下面这几个
文件描述符限制
文件系统类型
Swappiness
提交时间
首先是ulimit -n。我觉得任何一个Java项目最好都调整下这个值。实际上文件描述符系统资源并不像我们想象的那样昂贵你不用太担心调大此值会有什么不利的影响。通常情况下将它设置成一个超大的值是合理的做法比如ulimit -n 1000000。还记得电影《让子弹飞》里的对话吗“你和钱谁对我更重要都不重要没有你对我很重要”。这个参数也有点这么个意思。其实设置这个参数一点都不重要但不设置的话后果很严重比如你会经常看到“Too many open files”的错误。
其次是文件系统类型的选择。这里所说的文件系统指的是如ext3、ext4或XFS这样的日志型文件系统。根据官网的测试报告XFS的性能要强于ext4所以生产环境最好还是使用XFS。对了最近有个Kafka使用ZFS的数据报告貌似性能更加强劲有条件的话不妨一试。
第三是swap的调优。网上很多文章都提到设置其为0将swap完全禁掉以防止Kafka进程使用swap空间。我个人反倒觉得还是不要设置成0比较好我们可以设置成一个较小的值。为什么呢因为一旦设置成0当物理内存耗尽时操作系统会触发OOM killer这个组件它会随机挑选一个进程然后kill掉即根本不给用户任何的预警。但如果设置成一个比较小的值当开始使用swap空间时你至少能够观测到Broker性能开始出现急剧下降从而给你进一步调优和诊断问题的时间。基于这个考虑我个人建议将swappniess配置成一个接近0但不为0的值比如1。
最后是提交时间或者说是Flush落盘时间。向Kafka发送数据并不是真要等数据被写入磁盘才会认为成功而是只要数据被写入到操作系统的页缓存Page Cache上就可以了随后操作系统根据LRU算法会定期将页缓存上的“脏”数据落盘到物理磁盘上。这个定期就是由提交时间来确定的默认是5秒。一般情况下我们会认为这个时间太频繁了可以适当地增加提交间隔来降低物理磁盘的写操作。当然你可能会有这样的疑问如果在页缓存中的数据在写入到磁盘前机器宕机了那岂不是数据就丢失了。的确这种情况数据确实就丢失了但鉴于Kafka在软件层面已经提供了多副本的冗余机制因此这里稍微拉大提交间隔去换取性能还是一个合理的做法。
小结
今天我和你分享了关于Kafka集群设置的各类配置包括Topic级别参数、JVM参数以及操作系统参数连同上一篇一起构成了完整的Kafka参数配置列表。我希望这些最佳实践能够在你搭建Kafka集群时助你一臂之力但切记配置因环境而异一定要结合自身业务需要以及具体的测试来验证它们的有效性。
开放讨论
很多人争论Kafka不需要为Broker设置太大的堆内存而应该尽可能地把内存留给页缓存使用。对此你是怎么看的在你的实际使用中有哪些好的法则来评估Kafka对内存的使用呢
欢迎写下你的思考和答案,我们一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@ -0,0 +1,124 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
09 生产者消息分区机制原理剖析
09 生产者消息分区机制原理剖析
我们在使用Apache Kafka生产和消费消息的时候肯定是希望能够将数据均匀地分配到所有服务器上。比如很多公司使用Kafka收集应用服务器的日志数据这种数据都是很多的特别是对于那种大批量机器组成的集群环境每分钟产生的日志量都能以GB数因此如何将这么大的数据量均匀地分配到Kafka的各个Broker上就成为一个非常重要的问题。
今天我就来和你说说Kafka生产者如何实现这个需求我会以Java API为例进行分析但实际上其他语言的实现逻辑也是类似的。
为什么分区?
如果你对Kafka分区Partition的概念还不熟悉可以先返回专栏第2期回顾一下。专栏前面我说过Kafka有主题Topic的概念它是承载真实数据的逻辑容器而在主题之下还分为若干个分区也就是说Kafka的消息组织方式实际上是三级结构主题-分区-消息。主题下的每条消息只会保存在某一个分区中而不会在多个分区中被保存多份。官网上的这张图非常清晰地展示了Kafka的三级结构如下所示
现在我抛出一个问题你可以先思考一下你觉得为什么Kafka要做这样的设计为什么使用分区的概念而不是直接使用多个主题呢
其实分区的作用就是提供负载均衡的能力或者说对数据进行分区的主要原因就是为了实现系统的高伸缩性Scalability。不同的分区能够被放置到不同节点的机器上而数据的读写操作也都是针对分区这个粒度而进行的这样每个节点的机器都能独立地执行各自分区的读写请求处理。并且我们还可以通过添加新的节点机器来增加整体系统的吞吐量。
实际上分区的概念以及分区数据库早在1980年就已经有大牛们在做了比如那时候有个叫Teradata的数据库就引入了分区的概念。
值得注意的是不同的分布式系统对分区的叫法也不尽相同。比如在Kafka中叫分区在MongoDB和Elasticsearch中就叫分片Shard而在HBase中则叫Region在Cassandra中又被称作vnode。从表面看起来它们实现原理可能不尽相同但对底层分区Partitioning的整体思想却从未改变。
除了提供负载均衡这种最核心的功能之外,利用分区也可以实现其他一些业务级别的需求,比如实现业务级别的消息顺序的问题,这一点我今天也会分享一个具体的案例来说明。
都有哪些分区策略?
下面我们说说Kafka生产者的分区策略。所谓分区策略是决定生产者将消息发送到哪个分区的算法。Kafka为我们提供了默认的分区策略同时它也支持你自定义分区策略。
如果要自定义分区策略你需要显式地配置生产者端的参数partitioner.class。这个参数该怎么设定呢方法很简单在编写生产者程序时你可以编写一个具体的类实现org.apache.kafka.clients.producer.Partitioner接口。这个接口也很简单只定义了两个方法partition()和close()通常你只需要实现最重要的partition方法。我们来看看这个方法的方法签名
int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster);
这里的topic、key、keyBytes、value和valueBytes都属于消息数据cluster则是集群信息比如当前Kafka集群共有多少主题、多少Broker等。Kafka给你这么多信息就是希望让你能够充分地利用这些信息对消息进行分区计算出它要被发送到哪个分区中。只要你自己的实现类定义好了partition方法同时设置partitioner.class参数为你自己实现类的Full Qualified Name那么生产者程序就会按照你的代码逻辑对消息进行分区。虽说可以有无数种分区的可能但比较常见的分区策略也就那么几种下面我来详细介绍一下。
轮询策略
也称Round-robin策略即顺序分配。比如一个主题下有3个分区那么第一条消息被发送到分区0第二条被发送到分区1第三条被发送到分区2以此类推。当生产第4条消息时又会重新开始即将其分配到分区0就像下面这张图展示的那样。
这就是所谓的轮询策略。轮询策略是Kafka Java生产者API默认提供的分区策略。如果你未指定partitioner.class参数那么你的生产者程序会按照轮询的方式在主题的所有分区间均匀地“码放”消息。
轮询策略有非常优秀的负载均衡表现,它总是能保证消息最大限度地被平均分配到所有分区上,故默认情况下它是最合理的分区策略,也是我们最常用的分区策略之一。
随机策略
也称Randomness策略。所谓随机就是我们随意地将消息放置到任意一个分区上如下面这张图所示。
如果要实现随机策略版的partition方法很简单只需要两行代码即可
List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
return ThreadLocalRandom.current().nextInt(partitions.size());
先计算出该主题总的分区数,然后随机地返回一个小于它的正整数。
本质上看随机策略也是力求将数据均匀地打散到各个分区,但从实际表现来看,它要逊于轮询策略,所以如果追求数据的均匀分布,还是使用轮询策略比较好。事实上,随机策略是老版本生产者使用的分区策略,在新版本中已经改为轮询了。
按消息键保序策略
也称Key-ordering策略。有点尴尬的是这个名词是我自己编的Kafka官网上并无这样的提法。
Kafka允许为每条消息定义消息键简称为Key。这个Key的作用非常大它可以是一个有着明确业务含义的字符串比如客户代码、部门编号或是业务ID等也可以用来表征消息元数据。特别是在Kafka不支持时间戳的年代在一些场景中工程师们都是直接将消息创建时间封装进Key里面的。一旦消息被定义了Key那么你就可以保证同一个Key的所有消息都进入到相同的分区里面由于每个分区下的消息处理都是有顺序的故这个策略被称为按消息键保序策略如下图所示。
实现这个策略的partition方法同样简单只需要下面两行代码即可
List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
return Math.abs(key.hashCode()) % partitions.size();
前面提到的Kafka默认分区策略实际上同时实现了两种策略如果指定了Key那么默认实现按消息键保序策略如果没有指定Key则使用轮询策略。
在你了解了Kafka默认的分区策略之后我来给你讲一个真实的案例希望能加强你对分区策略重要性的理解。
我曾经给一个国企进行过Kafka培训当时碰到的一个问题就是如何实现消息的顺序问题。这家企业发送的Kafka的消息是有因果关系的故处理因果关系也必须要保证有序性否则先处理了“果”后处理“因”必然造成业务上的混乱。
当时那家企业的做法是给Kafka主题设置单分区也就是1个分区。这样所有的消息都只在这一个分区内读写因此保证了全局的顺序性。这样做虽然实现了因果关系的顺序性但也丧失了Kafka多分区带来的高吞吐量和负载均衡的优势。
后来经过了解和调研,我发现这种具有因果关系的消息都有一定的特点,比如在消息体中都封装了固定的标志位,后来我就建议他们对此标志位设定专门的分区策略,保证同一标志位的所有消息都发送到同一分区,这样既可以保证分区内的消息顺序,也可以享受到多分区带来的性能红利。
这种基于个别字段的分区策略本质上就是按消息键保序的思想其实更加合适的做法是把标志位数据提取出来统一放到Key中这样更加符合Kafka的设计思想。经过改造之后这个企业的消息处理吞吐量一下提升了40多倍从这个案例你也可以看到自定制分区策略的效果可见一斑。
其他分区策略
上面这几种分区策略都是比较基础的策略除此之外你还能想到哪些有实际用途的分区策略其实还有一种比较常见的即所谓的基于地理位置的分区策略。当然这种策略一般只针对那些大规模的Kafka集群特别是跨城市、跨国家甚至是跨大洲的集群。
我就拿“极客时间”举个例子吧假设极客时间的所有服务都部署在北京的一个机房这里我假设它是自建机房不考虑公有云方案。其实即使是公有云实现逻辑也差不多现在极客时间考虑在南方找个城市比如广州再创建一个机房另外从两个机房中选取一部分机器共同组成一个大的Kafka集群。显然这个集群中必然有一部分机器在北京另外一部分机器在广州。
假设极客时间计划为每个新注册用户提供一份注册礼品比如南方的用户注册极客时间可以免费得到一碗“甜豆腐脑”而北方的新注册用户可以得到一碗“咸豆腐脑”。如果用Kafka来实现则很简单只需要创建一个双分区的主题然后再创建两个消费者程序分别处理南北方注册用户逻辑即可。
但问题是你需要把南北方注册用户的注册消息正确地发送到位于南北方的不同机房中因为处理这些消息的消费者程序只可能在某一个机房中启动着。换句话说送甜豆腐脑的消费者程序只在广州机房启动着而送咸豆腐脑的程序只在北京的机房中如果你向广州机房中的Broker发送北方注册用户的消息那么这个用户将无法得到礼品
此时我们就可以根据Broker所在的IP地址实现定制化的分区策略。比如下面这段代码
List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
return partitions.stream().filter(p -> isSouth(p.leader().host())).map(PartitionInfo::partition).findAny().get();
我们可以从所有分区中找出那些Leader副本在南方的所有分区然后随机挑选一个进行消息发送。
小结
今天我们讨论了Kafka生产者消息分区的机制以及常见的几种分区策略。切记分区是实现负载均衡以及高吞吐量的关键故在生产者这一端就要仔细盘算合适的分区策略避免造成消息数据的“倾斜”使得某些分区成为性能瓶颈这样极易引发下游数据消费的性能下降。
开放讨论
在你的生产环境中使用最多的是哪种消息分区策略?实际在使用过程中遇到过哪些“坑”?
欢迎写下你的思考和答案,我们一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@ -0,0 +1,116 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
10 生产者压缩算法面面观
你好,我是胡夕。今天我要和你分享的内容是:生产者压缩算法面面观。
说起压缩compression我相信你一定不会感到陌生。它秉承了用时间去换空间的经典trade-off思想具体来说就是用CPU时间去换磁盘空间或网络I/O传输量希望以较小的CPU开销带来更少的磁盘占用或更少的网络I/O传输。在Kafka中压缩也是用来做这件事的。今天我就来跟你分享一下Kafka中压缩的那些事儿。
怎么压缩?
Kafka是如何压缩消息的呢要弄清楚这个问题就要从Kafka的消息格式说起了。目前Kafka共有两大类消息格式社区分别称之为V1版本和V2版本。V2版本是Kafka 0.11.0.0中正式引入的。
不论是哪个版本Kafka的消息层次都分为两层消息集合message set以及消息message。一个消息集合中包含若干条日志项record item而日志项才是真正封装消息的地方。Kafka底层的消息日志由一系列消息集合日志项组成。Kafka通常不会直接操作具体的一条条消息它总是在消息集合这个层面上进行写入操作。
那么社区引入V2版本的目的是什么呢V2版本主要是针对V1版本的一些弊端做了修正和我们今天讨论的主题相关的修正有哪些呢先介绍一个就是把消息的公共部分抽取出来放到外层消息集合里面这样就不用每条消息都保存这些信息了。
我来举个例子。原来在V1版本中每条消息都需要执行CRC校验但有些情况下消息的CRC值是会发生变化的。比如在Broker端可能会对消息时间戳字段进行更新那么重新计算之后的CRC值也会相应更新再比如Broker端在执行消息格式转换时主要是为了兼容老版本客户端程序也会带来CRC值的变化。鉴于这些情况再对每条消息都执行CRC校验就有点没必要了不仅浪费空间还耽误CPU时间因此在V2版本中消息的CRC校验工作就被移到了消息集合这一层。
V2版本还有一个和压缩息息相关的改进就是保存压缩消息的方法发生了变化。之前V1版本中保存压缩消息的方法是把多条消息进行压缩然后保存到外层消息的消息体字段中而V2版本的做法是对整个消息集合进行压缩。显然后者应该比前者有更好的压缩效果。
我对两个版本分别做了一个简单的测试结果显示在相同条件下不论是否启用压缩V2版本都比V1版本节省磁盘空间。当启用压缩时这种节省空间的效果更加明显就像下面这两张图展示的那样
何时压缩?
在Kafka中压缩可能发生在两个地方生产者端和Broker端。
生产者程序中配置compression.type参数即表示启用指定类型的压缩算法。比如下面这段程序代码展示了如何构建一个开启GZIP的Producer对象
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("acks", "all");
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
// 开启GZIP压缩
props.put("compression.type", "gzip");
Producer<String, String> producer = new KafkaProducer<>(props);
这里比较关键的代码行是props.put(“compression.type”, “gzip”)它表明该Producer的压缩算法使用的是GZIP。这样Producer启动后生产的每个消息集合都是经GZIP压缩过的故而能很好地节省网络传输带宽以及Kafka Broker端的磁盘占用。
在生产者端启用压缩是很自然的想法那为什么我说在Broker端也可能进行压缩呢其实大部分情况下Broker从Producer端接收到消息后仅仅是原封不动地保存而不会对其进行任何修改但这里的“大部分情况”也是要满足一定条件的。有两种例外情况就可能让Broker重新压缩消息。
情况一Broker端指定了和Producer端不同的压缩算法。
先看一个例子。想象这样一个对话。
Producer说“我要使用GZIP进行压缩。”
Broker说“不好意思我这边接收的消息必须使用Snappy算法进行压缩。”
你看这种情况下Broker接收到GZIP压缩消息后只能解压缩然后使用Snappy重新压缩一遍。如果你翻开Kafka官网你会发现Broker端也有一个参数叫compression.type和上面那个例子中的同名。但是这个参数的默认值是producer这表示Broker端会“尊重”Producer端使用的压缩算法。可一旦你在Broker端设置了不同的compression.type值就一定要小心了因为可能会发生预料之外的压缩/解压缩操作通常表现为Broker端CPU使用率飙升。
情况二Broker端发生了消息格式转换。
所谓的消息格式转换主要是为了兼容老版本的消费者程序。还记得之前说过的V1、V2版本吧在一个生产环境中Kafka集群中同时保存多种版本的消息格式非常常见。为了兼容老版本的格式Broker端会对新版本消息执行向老版本格式的转换。这个过程中会涉及消息的解压缩和重新压缩。一般情况下这种消息格式转换对性能是有很大影响的除了这里的压缩之外它还让Kafka丧失了引以为豪的Zero Copy特性。
所谓“Zero Copy”就是“零拷贝”我在专栏第6期提到过说的是当数据在磁盘和网络进行传输时避免昂贵的内核态数据拷贝从而实现快速的数据传输。因此如果Kafka享受不到这个特性的话性能必然有所损失所以尽量保证消息格式的统一吧这样不仅可以避免不必要的解压缩/重新压缩对提升其他方面的性能也大有裨益。如果有兴趣你可以深入地了解下Zero Copy的原理。
何时解压缩?
有压缩必有解压缩通常来说解压缩发生在消费者程序中也就是说Producer发送压缩消息到Broker后Broker照单全收并原样保存起来。当Consumer程序请求这部分消息时Broker依然原样发送出去当消息到达Consumer端后由Consumer自行解压缩还原成之前的消息。
那么现在问题来了Consumer怎么知道这些消息是用何种压缩算法压缩的呢其实答案就在消息中。Kafka会将启用了哪种压缩算法封装进消息集合中这样当Consumer读取到消息集合时它自然就知道了这些消息使用的是哪种压缩算法。如果用一句话总结一下压缩和解压缩那么我希望你记住这句话Producer端压缩、Broker端保持、Consumer端解压缩。
除了在Consumer端解压缩Broker端也会进行解压缩。注意了这和前面提到消息格式转换时发生的解压缩是不同的场景。每个压缩过的消息集合在Broker端写入时都要发生解压缩操作目的就是为了对消息执行各种验证。我们必须承认这种解压缩对Broker端性能是有一定影响的特别是对CPU的使用率而言。
事实上最近国内京东的小伙伴们刚刚向社区提出了一个bugfix建议去掉因为做消息校验而引入的解压缩。据他们称去掉了解压缩之后Broker端的CPU使用率至少降低了50%。不过有些遗憾的是,目前社区并未采纳这个建议,原因就是这种消息校验是非常重要的,不可盲目去之。毕竟先把事情做对是最重要的,在做对的基础上,再考虑把事情做好做快。针对这个使用场景,你也可以思考一下,是否有一个两全其美的方案,既能避免消息解压缩也能对消息执行校验。
各种压缩算法对比
那么我们来谈谈压缩算法。这可是重头戏!之前说了这么多,我们还是要比较一下各个压缩算法的优劣,这样我们才能有针对性地配置适合我们业务的压缩策略。
在Kafka 2.1.0版本之前Kafka支持3种压缩算法GZIP、Snappy和LZ4。从2.1.0开始Kafka正式支持Zstandard算法简写为zstd。它是Facebook开源的一个压缩算法能够提供超高的压缩比compression ratio
对了看一个压缩算法的优劣有两个重要的指标一个指标是压缩比原先占100份空间的东西经压缩之后变成了占20份空间那么压缩比就是5显然压缩比越高越好另一个指标就是压缩/解压缩吞吐量比如每秒能压缩或解压缩多少MB的数据。同样地吞吐量也是越高越好。
下面这张表是Facebook Zstandard官网提供的一份压缩算法benchmark比较结果
从表中我们可以发现zstd算法有着最高的压缩比而在吞吐量上的表现只能说中规中矩。反观LZ4算法它在吞吐量方面则是毫无疑问的执牛耳者。当然对于表格中数据的权威性我不做过多解读只想用它来说明一下当前各种压缩算法的大致表现。
在实际使用中GZIP、Snappy、LZ4甚至是zstd的表现各有千秋。但对于Kafka而言它们的性能测试结果却出奇得一致即在吞吐量方面LZ4 > Snappy > zstd和GZIP而在压缩比方面zstd > LZ4 > GZIP > Snappy。具体到物理资源使用Snappy算法占用的网络带宽最多zstd最少这是合理的毕竟zstd就是要提供超高的压缩比在CPU使用率方面各个算法表现得差不多只是在压缩时Snappy算法使用的CPU较多一些而在解压缩时GZIP算法则可能使用更多的CPU。
最佳实践
了解了这些算法对比,我们就能根据自身的实际情况有针对性地启用合适的压缩算法。
首先来说压缩。何时启用压缩是比较合适的时机呢?
你现在已经知道Producer端完成的压缩那么启用压缩的一个条件就是Producer程序运行机器上的CPU资源要很充足。如果Producer运行机器本身CPU已经消耗殆尽了那么启用消息压缩无疑是雪上加霜只会适得其反。
除了CPU资源充足这一条件如果你的环境中带宽资源有限那么我也建议你开启压缩。事实上我见过的很多Kafka生产环境都遭遇过带宽被打满的情况。这年头带宽可是比CPU和内存还要珍贵的稀缺资源毕竟万兆网络还不是普通公司的标配因此千兆网络中Kafka集群带宽资源耗尽这件事情就特别容易出现。如果你的客户端机器CPU资源有很多富余我强烈建议你开启zstd压缩这样能极大地节省网络资源消耗。
其次说说解压缩。其实也没什么可说的。一旦启用压缩,解压缩是不可避免的事情。这里只想强调一点:我们对不可抗拒的解压缩无能为力,但至少能规避掉那些意料之外的解压缩。就像我前面说的,因为要兼容老版本而引入的解压缩操作就属于这类。有条件的话尽量保证不要出现消息格式转换的情况。
小结
总结一下今天分享的内容我们主要讨论了Kafka压缩的各个方面包括Kafka是如何对消息进行压缩的、何时进行压缩及解压缩还对比了目前Kafka支持的几个压缩算法最后我给出了工程化的最佳实践。分享这么多内容我就只有一个目的就是希望你能根据自身的实际情况恰当地选择合适的Kafka压缩算法以求实现最大的资源利用率。
开放讨论
最后给出一道作业题请花时间思考一下前面我们提到了Broker要对压缩消息集合执行解压缩操作然后逐条对消息进行校验有人提出了一个方案把这种消息校验移到Producer端来做Broker直接读取校验结果即可这样就可以避免在Broker端执行解压缩操作。你认同这种方案吗
欢迎写下你的思考和答案,我们一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@ -0,0 +1,116 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
11 无消息丢失配置怎么实现?
你好我是胡夕。今天我要和你分享的主题是如何配置Kafka无消息丢失。
一直以来很多人对于Kafka丢失消息这件事情都有着自己的理解因而也就有着自己的解决之道。在讨论具体的应对方法之前我觉得我们首先要明确在Kafka的世界里什么才算是消息丢失或者说Kafka在什么情况下能保证消息不丢失。这点非常关键因为很多时候我们容易混淆责任的边界如果搞不清楚事情由谁负责自然也就不知道由谁来出解决方案了。
那Kafka到底在什么情况下才能保证消息不丢失呢
一句话概括Kafka只对“已提交”的消息committed message做有限度的持久化保证。
这句话里面有两个核心要素,我们一一来看。
第一个核心要素是“已提交的消息”。什么是已提交的消息当Kafka的若干个Broker成功地接收到一条消息并写入到日志文件后它们会告诉生产者程序这条消息已成功提交。此时这条消息在Kafka看来就正式变为“已提交”消息了。
那为什么是若干个Broker呢这取决于你对“已提交”的定义。你可以选择只要有一个Broker成功保存该消息就算是已提交也可以是令所有Broker都成功保存该消息才算是已提交。不论哪种情况Kafka只对已提交的消息做持久化保证这件事情是不变的。
第二个核心要素就是“有限度的持久化保证”也就是说Kafka不可能保证在任何情况下都做到不丢失消息。举个极端点的例子如果地球都不存在了Kafka还能保存任何消息吗显然不能倘若这种情况下你依然还想要Kafka不丢消息那么只能在别的星球部署Kafka Broker服务器了。
现在你应该能够稍微体会出这里的“有限度”的含义了吧其实就是说Kafka不丢消息是有前提条件的。假如你的消息保存在N个Kafka Broker上那么这个前提条件就是这N个Broker中至少有1个存活。只要这个条件成立Kafka就能保证你的这条消息永远不会丢失。
总结一下Kafka是能做到不丢失消息的只不过这些消息必须是已提交的消息而且还要满足一定的条件。当然说明这件事并不是要为Kafka推卸责任而是为了在出现该类问题时我们能够明确责任边界。
“消息丢失”案例
好了理解了Kafka是怎样做到不丢失消息的那接下来我带你复盘一下那些常见的“Kafka消息丢失”案例。注意这里可是带引号的消息丢失哦其实有些时候我们只是冤枉了Kafka而已。
案例1生产者程序丢失数据
Producer程序丢失消息这应该算是被抱怨最多的数据丢失场景了。我来描述一个场景你写了一个Producer应用向Kafka发送消息最后发现Kafka没有保存于是大骂“Kafka真烂消息发送居然都能丢失而且还不告诉我”如果你有过这样的经历那么请先消消气我们来分析下可能的原因。
目前Kafka Producer是异步发送消息的也就是说如果你调用的是producer.send(msg)这个API那么它通常会立即返回但此时你不能认为消息发送已成功完成。
这种发送方式有个有趣的名字叫“fire and forget”翻译一下就是“发射后不管”。这个术语原本属于导弹制导领域后来被借鉴到计算机领域中它的意思是执行完一个操作后不去管它的结果是否成功。调用producer.send(msg)就属于典型的“fire and forget”因此如果出现消息丢失我们是无法知晓的。这个发送方式挺不靠谱吧不过有些公司真的就是在使用这个API发送消息。
如果用这个方式可能会有哪些因素导致消息没有发送成功呢其实原因有很多例如网络抖动导致消息压根就没有发送到Broker端或者消息本身不合格导致Broker拒绝接收比如消息太大了超过了Broker的承受能力等。这么来看让Kafka“背锅”就有点冤枉它了。就像前面说过的Kafka不认为消息是已提交的因此也就没有Kafka丢失消息这一说了。
不过就算不是Kafka的“锅”我们也要解决这个问题吧。实际上解决此问题的方法非常简单Producer永远要使用带有回调通知的发送API也就是说不要使用producer.send(msg)而要使用producer.send(msg, callback)。不要小瞧这里的callback回调它能准确地告诉你消息是否真的提交成功了。一旦出现消息提交失败的情况你就可以有针对性地进行处理。
举例来说如果是因为那些瞬时错误那么仅仅让Producer重试就可以了如果是消息不合格造成的那么可以调整消息格式后再次发送。总之处理发送失败的责任在Producer端而非Broker端。
你可能会问发送失败真的没可能是由Broker端的问题造成的吗当然可能如果你所有的Broker都宕机了那么无论Producer端怎么重试都会失败的此时你要做的是赶快处理Broker端的问题。但之前说的核心论据在这里依然是成立的Kafka依然不认为这条消息属于已提交消息故对它不做任何持久化保证。
案例2消费者程序丢失数据
Consumer端丢失数据主要体现在Consumer端要消费的消息不见了。Consumer程序有个“位移”的概念表示的是这个Consumer当前消费到的Topic分区的位置。下面这张图来自于官网它清晰地展示了Consumer端的位移数据。
比如对于Consumer A而言它当前的位移值就是9Consumer B的位移值是11。
这里的“位移”类似于我们看书时使用的书签,它会标记我们当前阅读了多少页,下次翻书的时候我们能直接跳到书签页继续阅读。
正确使用书签有两个步骤第一步是读书第二步是更新书签页。如果这两步的顺序颠倒了就可能出现这样的场景当前的书签页是第90页我先将书签放到第100页上之后开始读书。当阅读到第95页时我临时有事中止了阅读。那么问题来了当我下次直接跳到书签页阅读时我就丢失了第9699页的内容即这些消息就丢失了。
同理Kafka中Consumer端的消息丢失就是这么一回事。要对抗这种消息丢失办法很简单维持先消费消息阅读再更新位移书签的顺序即可。这样就能最大限度地保证消息不丢失。
当然,这种处理方式可能带来的问题是消息的重复处理,类似于同一页书被读了很多遍,但这不属于消息丢失的情形。在专栏后面的内容中,我会跟你分享如何应对重复消费的问题。
除了上面所说的场景,其实还存在一种比较隐蔽的消息丢失场景。
我们依然以看书为例。假设你花钱从网上租借了一本共有10章内容的电子书该电子书的有效阅读时间是1天过期后该电子书就无法打开但如果在1天之内你完成阅读就退还租金。
为了加快阅读速度你把书中的10个章节分别委托给你的10个朋友请他们帮你阅读并拜托他们告诉你主旨大意。当电子书临近过期时这10个人告诉你说他们读完了自己所负责的那个章节的内容于是你放心地把该书还了回去。不料在这10个人向你描述主旨大意时你突然发现有一个人对你撒了谎他并没有看完他负责的那个章节。那么很显然你无法知道那一章的内容了。
对于Kafka而言这就好比Consumer程序从Kafka获取到消息后开启了多个线程异步处理消息而Consumer程序自动地向前更新位移。假如其中某个线程运行失败了它负责的消息没有被成功处理但位移已经被更新了因此这条消息对于Consumer而言实际上是丢失了。
这里的关键在于Consumer自动提交位移与你没有确认书籍内容被全部读完就将书归还类似你没有真正地确认消息是否真的被消费就“盲目”地更新了位移。
这个问题的解决方案也很简单如果是多线程异步处理消费消息Consumer程序不要开启自动提交位移而是要应用程序手动提交位移。在这里我要提醒你一下单个Consumer程序使用多线程来消费消息说起来容易写成代码却异常困难因为你很难正确地处理位移的更新也就是说避免无消费消息丢失很简单但极易出现消息被消费了多次的情况。
最佳实践
看完这两个案例之后我来分享一下Kafka无消息丢失的配置每一个其实都能对应上面提到的问题。
不要使用producer.send(msg)而要使用producer.send(msg, callback)。记住一定要使用带有回调通知的send方法。
设置acks = all。acks是Producer的一个参数代表了你对“已提交”消息的定义。如果设置成all则表明所有副本Broker都要接收到消息该消息才算是“已提交”。这是最高等级的“已提交”定义。
设置retries为一个较大的值。这里的retries同样是Producer的参数对应前面提到的Producer自动重试。当出现网络的瞬时抖动时消息发送可能会失败此时配置了retries > 0的Producer能够自动重试消息发送避免消息丢失。
设置unclean.leader.election.enable = false。这是Broker端的参数它控制的是哪些Broker有资格竞选分区的Leader。如果一个Broker落后原先的Leader太多那么它一旦成为新的Leader必然会造成消息的丢失。故一般都要将该参数设置成false即不允许这种情况的发生。
设置replication.factor >= 3。这也是Broker端的参数。其实这里想表述的是最好将消息多保存几份毕竟目前防止消息丢失的主要机制就是冗余。
设置min.insync.replicas > 1。这依然是Broker端参数控制的是消息至少要被写入到多少个副本才算是“已提交”。设置成大于1可以提升消息持久性。在实际环境中千万不要使用默认值1。
确保replication.factor > min.insync.replicas。如果两者相等那么只要有一个副本挂机整个分区就无法正常工作了。我们不仅要改善消息的持久性防止数据丢失还要在不降低可用性的基础上完成。推荐设置成replication.factor = min.insync.replicas + 1。
确保消息消费完成再提交。Consumer端有个参数enable.auto.commit最好把它设置成false并采用手动提交位移的方式。就像前面说的这对于单Consumer多线程处理的场景而言是至关重要的。
小结
今天我们讨论了Kafka无消息丢失的方方面面。我们先从什么是消息丢失开始说起明确了Kafka持久化保证的责任边界随后以这个规则为标尺衡量了一些常见的数据丢失场景最后通过分析这些场景我给出了Kafka无消息丢失的“最佳实践”。总结起来我希望你今天能有两个收获
明确Kafka持久化保证的含义和限定条件。
熟练配置Kafka无消息丢失参数。
开放讨论
其实Kafka还有一种特别隐秘的消息丢失场景增加主题分区。当增加主题分区后在某段“不凑巧”的时间间隔后Producer先于Consumer感知到新增加的分区而Consumer设置的是“从最新位移处”开始读取消息因此在Consumer感知到新分区前Producer发送的这些消息就全部“丢失”了或者说Consumer无法读取到这些消息。严格来说这是Kafka设计上的一个小缺陷你有什么解决的办法吗
欢迎写下你的思考和答案,我们一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@ -0,0 +1,176 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
12 客户端都有哪些不常见但是很高级的功能?
你好,我是胡夕。今天我要和你分享的主题是:客户端都有哪些不常见但是很高级的功能。
既然是不常见那就说明在实际场景中并没有太高的出场率但它们依然是很高级很实用的。下面就有请今天的主角登场Kafka拦截器。
什么是拦截器?
如果你用过Spring Interceptor或是Apache Flume那么应该不会对拦截器这个概念感到陌生其基本思想就是允许应用程序在不修改逻辑的情况下动态地实现一组可插拔的事件处理逻辑链。它能够在主业务操作的前后多个时间点上插入对应的“拦截”逻辑。下面这张图展示了Spring MVC拦截器的工作原理
图片来源
拦截器1和拦截器2分别在请求发送之前、发送之后以及完成之后三个地方插入了对应的处理逻辑。而Flume中的拦截器也是同理它们插入的逻辑可以是修改待发送的消息也可以是创建新的消息甚至是丢弃消息。这些功能都是以配置拦截器类的方式动态插入到应用程序中的故可以快速地切换不同的拦截器而不影响主程序逻辑。
Kafka拦截器借鉴了这样的设计思路。你可以在消息处理的前后多个时点动态植入不同的处理逻辑比如在消息发送前或者在消息被消费后。
作为一个非常小众的功能Kafka拦截器自0.10.0.0版本被引入后并未得到太多的实际应用我也从未在任何Kafka技术峰会上看到有公司分享其使用拦截器的成功案例。但即便如此在自己的Kafka工具箱中放入这么一个有用的东西依然是值得的。今天我们就让它来发挥威力展示一些非常酷炫的功能。
Kafka拦截器
Kafka拦截器分为生产者拦截器和消费者拦截器。生产者拦截器允许你在发送消息前以及消息提交成功后植入你的拦截器逻辑而消费者拦截器支持在消费消息前以及提交位移后编写特定逻辑。值得一提的是这两种拦截器都支持链的方式即你可以将一组拦截器串连成一个大的拦截器Kafka会按照添加顺序依次执行拦截器逻辑。
举个例子假设你想在生产消息前执行两个“前置动作”第一个是为消息增加一个头信息封装发送该消息的时间第二个是更新发送消息数字段那么当你将这两个拦截器串联在一起统一指定给Producer后Producer会按顺序执行上面的动作然后再发送消息。
当前Kafka拦截器的设置方法是通过参数配置完成的。生产者和消费者两端有一个相同的参数名字叫interceptor.classes它指定的是一组类的列表每个类就是特定逻辑的拦截器实现类。拿上面的例子来说假设第一个拦截器的完整类路径是com.yourcompany.kafkaproject.interceptors.AddTimeStampInterceptor第二个类是com.yourcompany.kafkaproject.interceptors.UpdateCounterInterceptor那么你需要按照以下方法在Producer端指定拦截器
Properties props = new Properties();
List<String> interceptors = new ArrayList<>();
interceptors.add("com.yourcompany.kafkaproject.interceptors.AddTimestampInterceptor"); // 拦截器1
interceptors.add("com.yourcompany.kafkaproject.interceptors.UpdateCounterInterceptor"); // 拦截器2
props.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, interceptors);
……
现在问题来了我们应该怎么编写AddTimeStampInterceptor和UpdateCounterInterceptor类呢其实很简单这两个类以及你自己编写的所有Producer端拦截器实现类都要继承org.apache.kafka.clients.producer.ProducerInterceptor接口。该接口是Kafka提供的里面有两个核心的方法。
onSend该方法会在消息发送之前被调用。如果你想在发送之前对消息“美美容”这个方法是你唯一的机会。
onAcknowledgement该方法会在消息成功提交或发送失败之后被调用。还记得我在上一期中提到的发送回调通知callback吗onAcknowledgement的调用要早于callback的调用。值得注意的是这个方法和onSend不是在同一个线程中被调用的因此如果你在这两个方法中调用了某个共享可变对象一定要保证线程安全哦。还有一点很重要这个方法处在Producer发送的主路径中所以最好别放一些太重的逻辑进去否则你会发现你的Producer TPS直线下降。
同理指定消费者拦截器也是同样的方法只是具体的实现类要实现org.apache.kafka.clients.consumer.ConsumerInterceptor接口这里面也有两个核心方法。
onConsume该方法在消息返回给Consumer程序之前调用。也就是说在开始正式处理消息之前拦截器会先拦一道搞一些事情之后再返回给你。
onCommitConsumer在提交位移之后调用该方法。通常你可以在该方法中做一些记账类的动作比如打日志等。
一定要注意的是指定拦截器类时要指定它们的全限定名即full qualified name。通俗点说就是要把完整包名也加上不要只有一个类名在那里并且还要保证你的Producer程序能够正确加载你的拦截器类。
典型使用场景
Kafka拦截器都能用在哪些地方呢其实跟很多拦截器的用法相同Kafka拦截器可以应用于包括客户端监控、端到端系统性能检测、消息审计等多种功能在内的场景。
我以端到端系统性能检测和消息审计为例来展开介绍下。
今天Kafka默认提供的监控指标都是针对单个客户端或Broker的你很难从具体的消息维度去追踪集群间消息的流转路径。同时如何监控一条消息从生产到最后消费的端到端延时也是很多Kafka用户迫切需要解决的问题。
从技术上来说我们可以在客户端程序中增加这样的统计逻辑但是对于那些将Kafka作为企业级基础架构的公司来说在应用代码中编写统一的监控逻辑其实是很难的毕竟这东西非常灵活不太可能提前确定好所有的计算逻辑。另外将监控逻辑与主业务逻辑耦合也是软件工程中不提倡的做法。
现在通过实现拦截器的逻辑以及可插拔的机制我们能够快速地观测、验证以及监控集群间的客户端性能指标特别是能够从具体的消息层面上去收集这些数据。这就是Kafka拦截器的一个非常典型的使用场景。
我们再来看看消息审计message audit的场景。设想你的公司把Kafka作为一个私有云消息引擎平台向全公司提供服务这必然要涉及多租户以及消息审计的功能。
作为私有云的PaaS提供方你肯定要能够随时查看每条消息是哪个业务方在什么时间发布的之后又被哪些业务方在什么时刻消费。一个可行的做法就是你编写一个拦截器类实现相应的消息审计逻辑然后强行规定所有接入你的Kafka服务的客户端程序必须设置该拦截器。
案例分享
下面我以一个具体的案例来说明一下拦截器的使用。在这个案例中,我们通过编写拦截器类来统计消息端到端处理的延时,非常实用,我建议你可以直接移植到你自己的生产环境中。
我曾经给一个公司做Kafka培训在培训过程中那个公司的人提出了一个诉求。他们的场景很简单某个业务只有一个Producer和一个Consumer他们想知道该业务消息从被生产出来到最后被消费的平均总时长是多少但是目前Kafka并没有提供这种端到端的延时统计。
学习了拦截器之后我们现在知道可以用拦截器来满足这个需求。既然是要计算总延时那么一定要有个公共的地方来保存它并且这个公共的地方还是要让生产者和消费者程序都能访问的。在这个例子中我们假设数据被保存在Redis中。
Okay这个需求显然要实现生产者拦截器也要实现消费者拦截器。我们先来实现前者
public class AvgLatencyProducerInterceptor implements ProducerInterceptor<String, String> {
private Jedis jedis; // 省略Jedis初始化
@Override
public ProducerRecord<String, String> onSend(ProducerRecord<String, String> record) {
jedis.incr("totalSentMessage");
return record;
}
@Override
public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
}
@Override
public void close() {
}
@Override
public void configure(Map<java.lang.String, ?> configs) {
}
上面的代码比较关键的是在发送消息前更新总的已发送消息数。为了节省时间,我没有考虑发送失败的情况,因为发送失败可能导致总发送数不准确。不过好在处理思路是相同的,你可以有针对性地调整下代码逻辑。
下面是消费者端的拦截器实现,代码如下:
public class AvgLatencyConsumerInterceptor implements ConsumerInterceptor<String, String> {
private Jedis jedis; //省略Jedis初始化
@Override
public ConsumerRecords<String, String> onConsume(ConsumerRecords<String, String> records) {
long lantency = 0L;
for (ConsumerRecord<String, String> record : records) {
lantency += (System.currentTimeMillis() - record.timestamp());
}
jedis.incrBy("totalLatency", lantency);
long totalLatency = Long.parseLong(jedis.get("totalLatency"));
long totalSentMsgs = Long.parseLong(jedis.get("totalSentMessage"));
jedis.set("avgLatency", String.valueOf(totalLatency / totalSentMsgs));
return records;
}
@Override
public void onCommit(Map<TopicPartition, OffsetAndMetadata> offsets) {
}
@Override
public void close() {
}
@Override
public void configure(Map<String, ?> configs) {
在上面的消费者拦截器中我们在真正消费一批消息前首先更新了它们的总延时方法就是用当前的时钟时间减去封装在消息中的创建时间然后累计得到这批消息总的端到端处理延时并更新到Redis中。之后的逻辑就很简单了我们分别从Redis中读取更新过的总延时和总消息数两者相除即得到端到端消息的平均处理延时。
创建好生产者和消费者拦截器后我们按照上面指定的方法分别将它们配置到各自的Producer和Consumer程序中这样就能计算消息从Producer端到Consumer端平均的处理延时了。这种端到端的指标监控能够从全局角度俯察和审视业务运行情况及时查看业务是否满足端到端的SLA目标。
小结
今天我们花了一些时间讨论Kafka提供的冷门功能拦截器。如之前所说拦截器的出场率极低以至于我从未看到过国内大厂实际应用Kafka拦截器的报道。但冷门不代表没用。事实上我们可以利用拦截器满足实际的需求比如端到端系统性能检测、消息审计等。
从这一期开始我们将逐渐接触到更多的实际代码。看完了今天的分享我希望你能够亲自动手编写一些代码去实现一个拦截器体会一下Kafka拦截器的功能。要知道“纸上得来终觉浅绝知此事要躬行”。也许你在敲代码的同时就会想到一个使用拦截器的绝妙点子让我们拭目以待吧。
开放讨论
思考这样一个问题Producer拦截器onSend方法的签名如下
public ProducerRecord<K, V> onSend(ProducerRecord<K, V> record)
如果我实现的逻辑仅仅是return null你觉得Kafka会丢弃该消息还是原封不动地发送消息请动手试验一下看看结果是否符合你的预期。
欢迎写下你的思考和答案,我们一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@ -0,0 +1,134 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
13 Java生产者是如何管理TCP连接的
13 Java生产者是如何管理TCP连接的
你好我是胡夕。今天我要和你分享的主题是Kafka的Java生产者是如何管理TCP连接的。
为何采用TCP
Apache Kafka的所有通信都是基于TCP的而不是基于HTTP或其他协议。无论是生产者、消费者还是Broker之间的通信都是如此。你可能会问为什么Kafka不使用HTTP作为底层的通信协议呢其实这里面的原因有很多但最主要的原因在于TCP和HTTP之间的区别。
从社区的角度来看在开发客户端时人们能够利用TCP本身提供的一些高级功能比如多路复用请求以及同时轮询多个连接的能力。
所谓的多路复用请求即multiplexing request是指将两个或多个数据流合并到底层单一物理连接中的过程。TCP的多路复用请求会在一条物理连接上创建若干个虚拟连接每个虚拟连接负责流转各自对应的数据流。其实严格来说TCP并不能多路复用它只是提供可靠的消息交付语义保证比如自动重传丢失的报文。
更严谨地说作为一个基于报文的协议TCP能够被用于多路复用连接场景的前提是上层的应用协议比如HTTP允许发送多条消息。不过我们今天并不是要详细讨论TCP原理因此你只需要知道这是社区采用TCP的理由之一就行了。
除了TCP提供的这些高级功能有可能被Kafka客户端的开发人员使用之外社区还发现目前已知的HTTP库在很多编程语言中都略显简陋。
基于这两个原因Kafka社区决定采用TCP协议作为所有请求通信的底层协议。
Kafka生产者程序概览
Kafka的Java生产者API主要的对象就是KafkaProducer。通常我们开发一个生产者的步骤有4步。
第1步构造生产者对象所需的参数对象。
第2步利用第1步的参数对象创建KafkaProducer对象实例。
第3步使用KafkaProducer的send方法发送消息。
第4步调用KafkaProducer的close方法关闭生产者并释放各种系统资源。
上面这4步写成Java代码的话大概是这个样子
Properties props = new Properties ();
props.put(“参数1”, “参数1的值”)
props.put(“参数2”, “参数2的值”)
……
try (Producer<String, String> producer = new KafkaProducer<>(props)) {
producer.send(new ProducerRecord<String, String>(……), callback);
……
}
这段代码使用了Java 7 提供的try-with-resource特性所以并没有显式调用producer.close()方法。无论是否显式调用close方法所有生产者程序大致都是这个路数。
现在问题来了当我们开发一个Producer应用时生产者会向Kafka集群中指定的主题Topic发送消息这必然涉及与Kafka Broker创建TCP连接。那么Kafka的Producer客户端是如何管理这些TCP连接的呢
何时创建TCP连接
要回答上面这个问题我们首先要弄明白生产者代码是什么时候创建TCP连接的。就上面的那段代码而言可能创建TCP连接的地方有两处Producer producer = new KafkaProducer(props)和producer.send(msg, callback)。你觉得连向Broker端的TCP连接会是哪里创建的呢前者还是后者抑或是两者都有请先思考5秒钟然后我给出我的答案。
首先生产者应用在创建KafkaProducer实例时是会建立与Broker的TCP连接的。其实这种表述也不是很准确应该这样说在创建KafkaProducer实例时生产者应用会在后台创建并启动一个名为Sender的线程该Sender线程开始运行时首先会创建与Broker的连接。我截取了一段测试环境中的日志来说明这一点
[2018-12-09 09:35:45,620] DEBUG [Producer clientId=producer-1] Initialize connection to node localhost:9093 (id: -2 rack: null) for sending metadata request (org.apache.kafka.clients.NetworkClient:1084)
[2018-12-09 09:35:45,622] DEBUG [Producer clientId=producer-1] Initiating connection to node localhost:9093 (id: -2 rack: null) using address localhost/127.0.0.1 (org.apache.kafka.clients.NetworkClient:914)
[2018-12-09 09:35:45,814] DEBUG [Producer clientId=producer-1] Initialize connection to node localhost:9092 (id: -1 rack: null) for sending metadata request (org.apache.kafka.clients.NetworkClient:1084)
[2018-12-09 09:35:45,815] DEBUG [Producer clientId=producer-1] Initiating connection to node localhost:9092 (id: -1 rack: null) using address localhost/127.0.0.1 (org.apache.kafka.clients.NetworkClient:914)
[2018-12-09 09:35:45,828] DEBUG [Producer clientId=producer-1] Sending metadata request (type=MetadataRequest, topics=) to node localhost:9093 (id: -2 rack: null) (org.apache.kafka.clients.NetworkClient:1068)
你也许会问怎么可能是这样如果不调用send方法这个Producer都不知道给哪个主题发消息它又怎么能知道连接哪个Broker呢难不成它会连接bootstrap.servers参数指定的所有Broker吗是的Java Producer目前还真是这样设计的。
我在这里稍微解释一下bootstrap.servers参数。它是Producer的核心参数之一指定了这个Producer启动时要连接的Broker地址。请注意这里的“启动时”代表的是Producer启动时会发起与这些Broker的连接。因此如果你为这个参数指定了1000个Broker连接信息那么很遗憾你的Producer启动时会首先创建与这1000个Broker的TCP连接。
在实际使用过程中我并不建议把集群中所有的Broker信息都配置到bootstrap.servers中通常你指定34台就足以了。因为Producer一旦连接到集群中的任一台Broker就能拿到整个集群的Broker信息故没必要为bootstrap.servers指定所有的Broker。
让我们回顾一下上面的日志输出请注意我标为橙色的内容。从这段日志中我们可以发现在KafkaProducer实例被创建后以及消息被发送前Producer应用就开始创建与两台Broker的TCP连接了。当然了在我的测试环境中我为bootstrap.servers配置了localhost:9092、localhost:9093来模拟不同的Broker但是这并不影响后面的讨论。另外日志输出中的最后一行也很关键它表明Producer向某一台Broker发送了METADATA请求尝试获取集群的元数据信息——这就是前面提到的Producer能够获取集群所有信息的方法。
讲到这里,我有一些个人的看法想跟你分享一下。通常情况下,我都不认为社区写的代码或做的设计就一定是对的,因此,很多类似的这种“质疑”会时不时地在我脑子里冒出来。
拿今天的这个KafkaProducer创建实例来说社区的官方文档中提及KafkaProducer类是线程安全的。我本人并没有详尽地去验证过它是否真的就是thread-safe的但是大致浏览一下源码可以得出这样的结论KafkaProducer实例创建的线程和前面提到的Sender线程共享的可变数据结构只有RecordAccumulator类故维护了RecordAccumulator类的线程安全也就实现了KafkaProducer类的线程安全。
你不需要了解RecordAccumulator类是做什么的你只要知道它主要的数据结构是一个ConcurrentMap。TopicPartition是Kafka用来表示主题分区的Java对象本身是不可变对象。而RecordAccumulator代码中用到Deque的地方都有锁的保护所以基本上可以认定RecordAccumulator类是线程安全的。
说了这么多我其实是想说纵然KafkaProducer是线程安全的我也不赞同创建KafkaProducer实例时启动Sender线程的做法。写了《Java并发编程实践》的那位布赖恩·格茨Brian Goetz大神明确指出了这样做的风险在对象构造器中启动线程会造成this指针的逃逸。理论上Sender线程完全能够观测到一个尚未构造完成的KafkaProducer实例。当然在构造对象时创建线程没有任何问题但最好是不要同时启动它。
好了我们言归正传。针对TCP连接何时创建的问题目前我们的结论是这样的TCP连接是在创建KafkaProducer实例时建立的。那么我们想问的是它只会在这个时候被创建吗
当然不是TCP连接还可能在两个地方被创建一个是在更新元数据后另一个是在消息发送时。为什么说是可能因为这两个地方并非总是创建TCP连接。当Producer更新了集群的元数据信息之后如果发现与某些Broker当前没有连接那么它就会创建一个TCP连接。同样地当要发送消息时Producer发现尚不存在与目标Broker的连接也会创建一个。
接下来我们来看看Producer更新集群元数据信息的两个场景。
场景一当Producer尝试给一个不存在的主题发送消息时Broker会告诉Producer说这个主题不存在。此时Producer会发送METADATA请求给Kafka集群去尝试获取最新的元数据信息。
场景二Producer通过metadata.max.age.ms参数定期地去更新元数据信息。该参数的默认值是300000即5分钟也就是说不管集群那边是否有变化Producer每5分钟都会强制刷新一次元数据以保证它是最及时的数据。
讲到这里我们可以“挑战”一下社区对Producer的这种设计的合理性。目前来看一个Producer默认会向集群的所有Broker都创建TCP连接不管是否真的需要传输请求。这显然是没有必要的。再加上Kafka还支持强制将空闲的TCP连接资源关闭这就更显得多此一举了。
试想一下在一个有着1000台Broker的集群中你的Producer可能只会与其中的35台Broker长期通信但是Producer启动后依次创建与这1000台Broker的TCP连接。一段时间之后大约有995个TCP连接又被强制关闭。这难道不是一种资源浪费吗很显然这里是有改善和优化的空间的。
何时关闭TCP连接
说完了TCP连接的创建我们来说说它们何时被关闭。
Producer端关闭TCP连接的方式有两种一种是用户主动关闭一种是Kafka自动关闭。
我们先说第一种。这里的主动关闭实际上是广义的主动关闭甚至包括用户调用kill -9主动“杀掉”Producer应用。当然最推荐的方式还是调用producer.close()方法来关闭。
第二种是Kafka帮你关闭这与Producer端参数connections.max.idle.ms的值有关。默认情况下该参数值是9分钟即如果在9分钟内没有任何请求“流过”某个TCP连接那么Kafka会主动帮你把该TCP连接关闭。用户可以在Producer端设置connections.max.idle.ms=-1禁掉这种机制。一旦被设置成-1TCP连接将成为永久长连接。当然这只是软件层面的“长连接”机制由于Kafka创建的这些Socket连接都开启了keepalive因此keepalive探活机制还是会遵守的。
值得注意的是在第二种方式中TCP连接是在Broker端被关闭的但其实这个TCP连接的发起方是客户端因此在TCP看来这属于被动关闭的场景即passive close。被动关闭的后果就是会产生大量的CLOSE_WAIT连接因此Producer端或Client端没有机会显式地观测到此连接已被中断。
小结
我们来简单总结一下今天的内容。对最新版本的Kafka2.1.0而言Java Producer端管理TCP连接的方式是
KafkaProducer实例创建时启动Sender线程从而创建与bootstrap.servers中所有Broker的TCP连接。
KafkaProducer实例首次更新元数据信息之后还会再次创建与集群中所有Broker的TCP连接。
如果Producer端发送消息到某台Broker时发现没有与该Broker的TCP连接那么也会立即创建连接。
如果设置Producer端connections.max.idle.ms参数大于0则步骤1中创建的TCP连接会被自动关闭如果设置该参数=-1那么步骤1中创建的TCP连接将无法被关闭从而成为“僵尸”连接。
开放讨论
对于今天我们“挑战”的社区设计,你有什么改进的想法吗?
欢迎写下你的思考和答案,我们一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@ -0,0 +1,109 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
14 幂等生产者和事务生产者是一回事吗?
你好我是胡夕。今天我要和你分享的主题是Kafka消息交付可靠性保障以及精确处理一次语义的实现。
所谓的消息交付可靠性保障是指Kafka对Producer和Consumer要处理的消息提供什么样的承诺。常见的承诺有以下三种
最多一次at most once消息可能会丢失但绝不会被重复发送。
至少一次at least once消息不会丢失但有可能被重复发送。
精确一次exactly once消息不会丢失也不会被重复发送。
目前Kafka默认提供的交付可靠性保障是第二种即至少一次。在专栏第11期中我们说过消息“已提交”的含义即只有Broker成功“提交”消息且Producer接到Broker的应答才会认为该消息成功发送。不过倘若消息成功“提交”但Broker的应答没有成功发送回Producer端比如网络出现瞬时抖动那么Producer就无法确定消息是否真的提交成功了。因此它只能选择重试也就是再次发送相同的消息。这就是Kafka默认提供至少一次可靠性保障的原因不过这会导致消息重复发送。
Kafka也可以提供最多一次交付保障只需要让Producer禁止重试即可。这样一来消息要么写入成功要么写入失败但绝不会重复发送。我们通常不会希望出现消息丢失的情况但一些场景里偶发的消息丢失其实是被允许的相反消息重复是绝对要避免的。此时使用最多一次交付保障就是最恰当的。
无论是至少一次还是最多一次都不如精确一次来得有吸引力。大部分用户还是希望消息只会被交付一次这样的话消息既不会丢失也不会被重复处理。或者说即使Producer端重复发送了相同的消息Broker端也能做到自动去重。在下游Consumer看来消息依然只有一条。
那么问题来了Kafka是怎么做到精确一次的呢简单来说这是通过两种机制幂等性Idempotence和事务Transaction。它们分别是什么机制两者是一回事吗要回答这些问题我们首先来说说什么是幂等性。
什么是幂等性Idempotence
“幂等”这个词原是数学领域中的概念指的是某些操作或函数能够被执行多次但每次得到的结果都是不变的。我来举几个简单的例子说明一下。比如在乘法运算中让数字乘以1就是一个幂等操作因为不管你执行多少次这样的运算结果都是相同的。再比如取整函数floor和ceiling是幂等函数那么运行1次floor(3.4)和100次floor(3.4)结果是一样的都是3。相反地让一个数加1这个操作就不是幂等的因为执行一次和执行多次的结果必然不同。
在计算机领域中,幂等性的含义稍微有一些不同:
在命令式编程语言比如C若一个子程序是幂等的那它必然不能修改系统状态。这样不管运行这个子程序多少次与该子程序关联的那部分系统状态保持不变。
在函数式编程语言比如Scala或Haskell很多纯函数pure function天然就是幂等的它们不执行任何的side effect。
幂等性有很多好处,其最大的优势在于我们可以安全地重试任何幂等性操作,反正它们也不会破坏我们的系统状态。如果是非幂等性操作,我们还需要担心某些操作执行多次对状态的影响,但对于幂等性操作而言,我们根本无需担心此事。
幂等性Producer
在Kafka中Producer默认不是幂等性的但我们可以创建幂等性Producer。它其实是0.11.0.0版本引入的新功能。在此之前Kafka向分区发送数据时可能会出现同一条消息被发送了多次导致消息重复的情况。在0.11之后指定Producer幂等性的方法很简单仅需要设置一个参数即可即props.put(“enable.idempotence”, ture)或props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG true)。
enable.idempotence被设置成true后Producer自动升级成幂等性Producer其他所有的代码逻辑都不需要改变。Kafka自动帮你做消息的重复去重。底层具体的原理很简单就是经典的用空间去换时间的优化思路即在Broker端多保存一些字段。当Producer发送了具有相同字段值的消息后Broker能够自动知晓这些消息已经重复了于是可以在后台默默地把它们“丢弃”掉。当然实际的实现原理并没有这么简单但你大致可以这么理解。
看上去幂等性Producer的功能很酷使用起来也很简单仅仅设置一个参数就能保证消息不重复了但实际上我们必须要了解幂等性Producer的作用范围。
首先它只能保证单分区上的幂等性即一个幂等性Producer能够保证某个主题的一个分区上不出现重复消息它无法实现多个分区的幂等性。其次它只能实现单会话上的幂等性不能实现跨会话的幂等性。这里的会话你可以理解为Producer进程的一次运行。当你重启了Producer进程之后这种幂等性保证就丧失了。
那么你可能会问如果我想实现多分区以及多会话上的消息无重复应该怎么做呢答案就是事务transaction或者依赖事务型Producer。这也是幂等性Producer和事务型Producer的最大区别
事务
Kafka的事务概念类似于我们熟知的数据库提供的事务。在数据库领域事务提供的安全性保障是经典的ACID即原子性Atomicity、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)。
当然在实际场景中各家数据库对ACID的实现各不相同。特别是ACID本身就是一个有歧义的概念比如对隔离性的理解。大体来看隔离性非常自然和必要但是具体到实现细节就显得不那么精确了。通常来说隔离性表明并发执行的事务彼此相互隔离互不影响。经典的数据库教科书把隔离性称为可串行化(serializability),即每个事务都假装它是整个数据库中唯一的事务。
提到隔离级别这种歧义或混乱就更加明显了。很多数据库厂商对于隔离级别的实现都有自己不同的理解比如有的数据库提供Snapshot隔离级别而在另外一些数据库中它们被称为可重复读repeatable read。好在对于已提交读read committed隔离级别的提法各大主流数据库厂商都比较统一。所谓的read committed指的是当读取数据库时你只能看到已提交的数据即无脏读。同时当写入数据库时你也只能覆盖掉已提交的数据即无脏写。
Kafka自0.11版本开始也提供了对事务的支持目前主要是在read committed隔离级别上做事情。它能保证多条消息原子性地写入到目标分区同时也能保证Consumer只能看到事务成功提交的消息。下面我们就来看看Kafka中的事务型Producer。
事务型Producer
事务型Producer能够保证将消息原子性地写入到多个分区中。这批消息要么全部写入成功要么全部失败。另外事务型Producer也不惧进程的重启。Producer重启回来后Kafka依然保证它们发送消息的精确一次处理。
设置事务型Producer的方法也很简单满足两个要求即可
和幂等性Producer一样开启enable.idempotence = true。
设置Producer端参数transactional. id。最好为其设置一个有意义的名字。
此外你还需要在Producer代码中做一些调整如这段代码所示
producer.initTransactions();
try {
producer.beginTransaction();
producer.send(record1);
producer.send(record2);
producer.commitTransaction();
} catch (KafkaException e) {
producer.abortTransaction();
}
和普通Producer代码相比事务型Producer的显著特点是调用了一些事务API如initTransaction、beginTransaction、commitTransaction和abortTransaction它们分别对应事务的初始化、事务开始、事务提交以及事务终止。
这段代码能够保证Record1和Record2被当作一个事务统一提交到Kafka要么它们全部提交成功要么全部写入失败。实际上即使写入失败Kafka也会把它们写入到底层的日志中也就是说Consumer还是会看到这些消息。因此在Consumer端读取事务型Producer发送的消息也是需要一些变更的。修改起来也很简单设置isolation.level参数的值即可。当前这个参数有两个取值
read_uncommitted这是默认值表明Consumer能够读取到Kafka写入的任何消息不论事务型Producer提交事务还是终止事务其写入的消息都可以读取。很显然如果你用了事务型Producer那么对应的Consumer就不要使用这个值。
read_committed表明Consumer只会读取事务型Producer成功提交事务写入的消息。当然了它也能看到非事务型Producer写入的所有消息。
小结
简单来说幂等性Producer和事务型Producer都是Kafka社区力图为Kafka实现精确一次处理语义所提供的工具只是它们的作用范围是不同的。幂等性Producer只能保证单分区、单会话上的消息幂等性而事务能够保证跨分区、跨会话间的幂等性。从交付语义上来看自然是事务型Producer能做的更多。
不过切记天下没有免费的午餐。比起幂等性Producer事务型Producer的性能要更差在实际使用过程中我们需要仔细评估引入事务的开销切不可无脑地启用事务。
开放讨论
你理解的事务是什么呢通过今天的分享你能列举出未来可能应用于你们公司实际业务中的事务型Producer使用场景吗
欢迎写下你的思考和答案,我们一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@ -0,0 +1,87 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
15 消费者组到底是什么?
你好我是胡夕。今天我要和你分享的主题是Kafka的消费者组。
消费者组即Consumer Group应该算是Kafka比较有亮点的设计了。那么何谓Consumer Group呢用一句话概括就是Consumer Group是Kafka提供的可扩展且具有容错性的消费者机制。既然是一个组那么组内必然可以有多个消费者或消费者实例Consumer Instance它们共享一个公共的ID这个ID被称为Group ID。组内的所有消费者协调在一起来消费订阅主题Subscribed Topics的所有分区Partition。当然每个分区只能由同一个消费者组内的一个Consumer实例来消费。个人认为理解Consumer Group记住下面这三个特性就好了。
Consumer Group下可以有一个或多个Consumer实例。这里的实例可以是一个单独的进程也可以是同一进程下的线程。在实际场景中使用进程更为常见一些。
Group ID是一个字符串在一个Kafka集群中它标识唯一的一个Consumer Group。
Consumer Group下所有实例订阅的主题的单个分区只能分配给组内的某个Consumer实例消费。这个分区当然也可以被其他的Group消费。
你应该还记得我在专栏第1期中提到的两种消息引擎模型吧它们分别是点对点模型和发布/订阅模型,前者也称为消费队列。当然,你要注意区分很多架构文章中涉及的消息队列与这里的消息队列。国内很多文章都习惯把消息中间件这类框架统称为消息队列,我在这里不评价这种提法是否准确,只是想提醒你注意这里所说的消息队列,特指经典的消息引擎模型。
好了传统的消息引擎模型就是这两大类它们各有优劣。我们来简单回顾一下。传统的消息队列模型的缺陷在于消息一旦被消费就会从队列中被删除而且只能被下游的一个Consumer消费。严格来说这一点不算是缺陷只能算是它的一个特性。但很显然这种模型的伸缩性scalability很差因为下游的多个Consumer都要“抢”这个共享消息队列的消息。发布/订阅模型倒是允许消息被多个Consumer消费但它的问题也是伸缩性不高因为每个订阅者都必须要订阅主题的所有分区。这种全量订阅的方式既不灵活也会影响消息的真实投递效果。
如果有这么一种机制既可以避开这两种模型的缺陷又兼具它们的优点那就太好了。幸运的是Kafka的Consumer Group就是这样的机制。当Consumer Group订阅了多个主题后组内的每个实例不要求一定要订阅主题的所有分区它只会消费部分分区中的消息。
Consumer Group之间彼此独立互不影响它们能够订阅相同的一组主题而互不干涉。再加上Broker端的消息留存机制Kafka的Consumer Group完美地规避了上面提到的伸缩性差的问题。可以这么说Kafka仅仅使用Consumer Group这一种机制却同时实现了传统消息引擎系统的两大模型如果所有实例都属于同一个Group那么它实现的就是消息队列模型如果所有实例分别属于不同的Group那么它实现的就是发布/订阅模型。
在了解了Consumer Group以及它的设计亮点之后你可能会有这样的疑问在实际使用场景中我怎么知道一个Group下该有多少个Consumer实例呢理想情况下Consumer实例的数量应该等于该Group订阅主题的分区总数。
举个简单的例子假设一个Consumer Group订阅了3个主题分别是A、B、C它们的分区数依次是1、2、3总共是6个分区那么通常情况下为该Group设置6个Consumer实例是比较理想的情形因为它能最大限度地实现高伸缩性。
你可能会问我能设置小于或大于6的实例吗当然可以如果你有3个实例那么平均下来每个实例大约消费2个分区6 / 3 = 2如果你设置了8个实例那么很遗憾有2个实例8 6 = 2将不会被分配任何分区它们永远处于空闲状态。因此在实际使用过程中一般不推荐设置大于总分区数的Consumer实例。设置多余的实例只会浪费资源而没有任何好处。
好了说完了Consumer Group的设计特性我们来讨论一个问题针对Consumer GroupKafka是怎么管理位移的呢你还记得吧消费者在消费的过程中需要记录自己消费了多少数据即消费位置信息。在Kafka中这个位置信息有个专门的术语位移Offset
看上去该Offset就是一个数值而已其实对于Consumer Group而言它是一组KV对Key是分区V对应Consumer消费该分区的最新位移。如果用Java来表示的话你大致可以认为是这样的数据结构即Map其中TopicPartition表示一个分区而Long表示位移的类型。当然我必须承认Kafka源码中并不是这样简单的数据结构而是要比这个复杂得多不过这并不会妨碍我们对Group位移的理解。
我在专栏第4期中提到过Kafka有新旧客户端API之分那自然也就有新旧Consumer之分。老版本的Consumer也有消费者组的概念它和我们目前讨论的Consumer Group在使用感上并没有太多的不同只是它管理位移的方式和新版本是不一样的。
老版本的Consumer Group把位移保存在ZooKeeper中。Apache ZooKeeper是一个分布式的协调服务框架Kafka重度依赖它实现各种各样的协调管理。将位移保存在ZooKeeper外部系统的做法最显而易见的好处就是减少了Kafka Broker端的状态保存开销。现在比较流行的提法是将服务器节点做成无状态的这样可以自由地扩缩容实现超强的伸缩性。Kafka最开始也是基于这样的考虑才将Consumer Group位移保存在独立于Kafka集群之外的框架中。
不过慢慢地人们发现了一个问题即ZooKeeper这类元框架其实并不适合进行频繁的写更新而Consumer Group的位移更新却是一个非常频繁的操作。这种大吞吐量的写操作会极大地拖慢ZooKeeper集群的性能因此Kafka社区渐渐有了这样的共识将Consumer位移保存在ZooKeeper中是不合适的做法。
于是在新版本的Consumer Group中Kafka社区重新设计了Consumer Group的位移管理方式采用了将位移保存在Kafka内部主题的方法。这个内部主题就是让人既爱又恨的__consumer_offsets。我会在专栏后面的内容中专门介绍这个神秘的主题。不过现在你需要记住新版本的Consumer Group将位移保存在Broker端的内部主题中。
最后我们来说说Consumer Group端大名鼎鼎的重平衡也就是所谓的Rebalance过程。我形容其为“大名鼎鼎”从某种程度上来说其实也是“臭名昭著”因为有关它的bug真可谓是此起彼伏从未间断。这里我先卖个关子后面我会解释它“遭人恨”的地方。我们先来了解一下什么是Rebalance。
Rebalance本质上是一种协议规定了一个Consumer Group下的所有Consumer如何达成一致来分配订阅Topic的每个分区。比如某个Group下有20个Consumer实例它订阅了一个具有100个分区的Topic。正常情况下Kafka平均会为每个Consumer分配5个分区。这个分配的过程就叫Rebalance。
那么Consumer Group何时进行Rebalance呢Rebalance的触发条件有3个。
组成员数发生变更。比如有新的Consumer实例加入组或者离开组抑或是有Consumer实例崩溃被“踢出”组。
订阅主题数发生变更。Consumer Group可以使用正则表达式的方式订阅主题比如consumer.subscribe(Pattern.compile(“t.*c”))就表明该Group订阅所有以字母t开头、字母c结尾的主题。在Consumer Group的运行过程中你新创建了一个满足这样条件的主题那么该Group就会发生Rebalance。
订阅主题的分区数发生变更。Kafka当前只能允许增加一个主题的分区数。当分区数增加时就会触发订阅该主题的所有Group开启Rebalance。
Rebalance发生时Group下所有的Consumer实例都会协调在一起共同参与。你可能会问每个Consumer实例怎么知道应该消费订阅主题的哪些分区呢这就需要分配策略的协助了。
当前Kafka默认提供了3种分配策略每种策略都有一定的优势和劣势我们今天就不展开讨论了你只需要记住社区会不断地完善这些策略保证提供最公平的分配策略即每个Consumer实例都能够得到较为平均的分区数。比如一个Group内有10个Consumer实例要消费100个分区理想的分配策略自然是每个实例平均得到10个分区。这就叫公平的分配策略。如果出现了严重的分配倾斜势必会出现这种情况有的实例会“闲死”而有的实例则会“忙死”。
我们举个简单的例子来说明一下Consumer Group发生Rebalance的过程。假设目前某个Consumer Group下有两个Consumer比如A和B当第三个成员C加入时Kafka会触发Rebalance并根据默认的分配策略重新为A、B和C分配分区如下图所示
显然Rebalance之后的分配依然是公平的即每个Consumer实例都获得了2个分区的消费权。这是我们希望出现的情形。
讲完了Rebalance现在我来说说它“遭人恨”的地方。
首先Rebalance过程对Consumer Group消费过程有极大的影响。如果你了解JVM的垃圾回收机制你一定听过万物静止的收集方式即著名的stop the world简称STW。在STW期间所有应用线程都会停止工作表现为整个应用程序僵在那边一动不动。Rebalance过程也和这个类似在Rebalance过程中所有Consumer实例都会停止消费等待Rebalance完成。这是Rebalance为人诟病的一个方面。
其次目前Rebalance的设计是所有Consumer实例共同参与全部重新分配所有分区。其实更高效的做法是尽量减少分配方案的变动。例如实例A之前负责消费分区1、2、3那么Rebalance之后如果可能的话最好还是让实例A继续消费分区1、2、3而不是被重新分配其他的分区。这样的话实例A连接这些分区所在Broker的TCP连接就可以继续用不用重新创建连接其他Broker的Socket资源。
最后Rebalance实在是太慢了。曾经有个国外用户的Group内有几百个Consumer实例成功Rebalance一次要几个小时这完全是不能忍受的。最悲剧的是目前社区对此无能为力至少现在还没有特别好的解决方案。所谓“本事大不如不摊上”也许最好的解决方案就是避免Rebalance的发生吧。
小结
总结一下今天我跟你分享了Kafka Consumer Group的方方面面包括它是怎么定义的它解决了哪些问题有哪些特性。同时我们也聊到了Consumer Group的位移管理以及著名的Rebalance过程。希望在你开发Consumer应用时它们能够助你一臂之力。
开放讨论
今天我貌似说了很多Consumer Group的好话除了Rebalance你觉得这种消费者组设计的弊端有哪些呢
欢迎写下你的思考和答案,我们一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@ -0,0 +1,96 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
16 揭开神秘的“位移主题”面纱
你好我是胡夕。今天我要和你分享的内容是Kafka中神秘的内部主题Internal Topic__consumer_offsets。
consumer_offsets在Kafka源码中有个更为正式的名字叫位移主题即Offsets Topic。为了方便今天的讨论我将统一使用位移主题来指代consumer_offsets。需要注意的是它有两个下划线哦。
好了,我们开始今天的内容吧。首先,我们有必要探究一下位移主题被引入的背景及原因,即位移主题的前世今生。
在上一期中我说过老版本Consumer的位移管理是依托于Apache ZooKeeper的它会自动或手动地将位移数据提交到ZooKeeper中保存。当Consumer重启后它能自动从ZooKeeper中读取位移数据从而在上次消费截止的地方继续消费。这种设计使得Kafka Broker不需要保存位移数据减少了Broker端需要持有的状态空间因而有利于实现高伸缩性。
但是ZooKeeper其实并不适用于这种高频的写操作因此Kafka社区自0.8.2.x版本开始就在酝酿修改这种设计并最终在新版本Consumer中正式推出了全新的位移管理机制自然也包括这个新的位移主题。
新版本Consumer的位移管理机制其实也很简单就是将Consumer的位移数据作为一条条普通的Kafka消息提交到consumer_offsets中。可以这么说consumer_offsets的主要作用是保存Kafka消费者的位移信息。它要求这个提交过程不仅要实现高持久性还要支持高频的写操作。显然Kafka的主题设计天然就满足这两个条件因此使用Kafka主题来保存位移这件事情实际上就是一个水到渠成的想法了。
这里我想再次强调一下和你创建的其他主题一样位移主题就是普通的Kafka主题。你可以手动地创建它、修改它甚至是删除它。只不过它同时也是一个内部主题大部分情况下你其实并不需要“搭理”它也不用花心思去管理它把它丢给Kafka就完事了。
虽说位移主题是一个普通的Kafka主题但它的消息格式却是Kafka自己定义的用户不能修改也就是说你不能随意地向这个主题写消息因为一旦你写入的消息不满足Kafka规定的格式那么Kafka内部无法成功解析就会造成Broker的崩溃。事实上Kafka Consumer有API帮你提交位移也就是向位移主题写消息。你千万不要自己写个Producer随意向该主题发送消息。
你可能会好奇这个主题存的到底是什么格式的消息呢所谓的消息格式你可以简单地理解为是一个KV对。Key和Value分别表示消息的键值和消息体在Kafka中它们就是字节数组而已。想象一下如果让你来设计这个主题你觉得消息格式应该长什么样子呢我先不说社区的设计方案我们自己先来设计一下。
首先从Key说起。一个Kafka集群中的Consumer数量会有很多既然这个主题保存的是Consumer的位移数据那么消息格式中必须要有字段来标识这个位移数据是哪个Consumer的。这种数据放在哪个字段比较合适呢显然放在Key中比较合适。
现在我们知道该主题消息的Key中应该保存标识Consumer的字段那么当前Kafka中什么字段能够标识Consumer呢还记得之前我们说Consumer Group时提到的Group ID吗没错就是这个字段它能够标识唯一的Consumer Group。
说到这里我再多说几句。除了Consumer GroupKafka还支持独立Consumer也称Standalone Consumer。它的运行机制与Consumer Group完全不同但是位移管理的机制却是相同的。因此即使是Standalone Consumer也有自己的Group ID来标识它自己所以也适用于这套消息格式。
Okay我们现在知道Key中保存了Group ID但是只保存Group ID就可以了吗别忘了Consumer提交位移是在分区层面上进行的即它提交的是某个或某些分区的位移那么很显然Key中还应该保存Consumer要提交位移的分区。
好了我们来总结一下我们的结论。位移主题的Key中应该保存3部分内容。如果你认同这样的结论那么恭喜你社区就是这么设计的
接下来我们再来看看消息体的设计。也许你会觉得消息体应该很简单保存一个位移值就可以了。实际上社区的方案要复杂得多比如消息体还保存了位移提交的一些其他元数据诸如时间戳和用户自定义的数据等。保存这些元数据是为了帮助Kafka执行各种各样后续的操作比如删除过期位移消息等。但总体来说我们还是可以简单地认为消息体就是保存了位移值。
当然了位移主题的消息格式可不是只有这一种。事实上它有3种消息格式。除了刚刚我们说的这种格式还有2种格式
用于保存Consumer Group信息的消息。
用于删除Group过期位移甚至是删除Group的消息。
第1种格式非常神秘以至于你几乎无法在搜索引擎中搜到它的身影。不过你只需要记住它是用来注册Consumer Group的就可以了。
第2种格式相对更加有名一些。它有个专属的名字tombstone消息即墓碑消息也称delete mark。下次你在Google或百度中见到这些词不用感到惊讶它们指的是一个东西。这些消息只出现在源码中而不暴露给你。它的主要特点是它的消息体是null即空消息体。
那么何时会写入这类消息呢一旦某个Consumer Group下的所有Consumer实例都停止了而且它们的位移数据都已被删除时Kafka会向位移主题的对应分区写入tombstone消息表明要彻底删除这个Group的信息。
好了消息格式就说这么多下面我们来说说位移主题是怎么被创建的。通常来说当Kafka集群中的第一个Consumer程序启动时Kafka会自动创建位移主题。我们说过位移主题就是普通的Kafka主题那么它自然也有对应的分区数。但如果是Kafka自动创建的分区数是怎么设置的呢这就要看Broker端参数offsets.topic.num.partitions的取值了。它的默认值是50因此Kafka会自动创建一个50分区的位移主题。如果你曾经惊讶于Kafka日志路径下冒出很多__consumer_offsets-xxx这样的目录那么现在应该明白了吧这就是Kafka自动帮你创建的位移主题啊。
你可能会问除了分区数副本数或备份因子是怎么控制的呢答案也很简单这就是Broker端另一个参数offsets.topic.replication.factor要做的事情了。它的默认值是3。
总结一下如果位移主题是Kafka自动创建的那么该主题的分区数是50副本数是3。
当然你也可以选择手动创建位移主题具体方法就是在Kafka集群尚未启动任何Consumer之前使用Kafka API创建它。手动创建的好处在于你可以创建满足你实际场景需要的位移主题。比如很多人说50个分区对我来讲太多了我不想要这么多分区那么你可以自己创建它不用理会offsets.topic.num.partitions的值。
不过我给你的建议是还是让Kafka自动创建比较好。目前Kafka源码中有一些地方硬编码了50分区数因此如果你自行创建了一个不同于默认分区数的位移主题可能会碰到各种各样奇怪的问题。这是社区的一个Bug目前代码已经修复了但依然在审核中。
创建位移主题当然是为了用的那么什么地方会用到位移主题呢我们前面一直在说Kafka Consumer提交位移时会写入该主题那Consumer是怎么提交位移的呢目前Kafka Consumer提交位移的方式有两种自动提交位移和手动提交位移。
Consumer端有个参数叫enable.auto.commit如果值是true则Consumer在后台默默地为你定期提交位移提交间隔由一个专属的参数auto.commit.interval.ms来控制。自动提交位移有一个显著的优点就是省事你不用操心位移提交的事情就能保证消息消费不会丢失。但这一点同时也是缺点。因为它太省事了以至于丧失了很大的灵活性和可控性你完全没法把控Consumer端的位移管理。
事实上很多与Kafka集成的大数据框架都是禁用自动提交位移的如Spark、Flink等。这就引出了另一种位移提交方式手动提交位移即设置enable.auto.commit = false。一旦设置了false作为Consumer应用开发的你就要承担起位移提交的责任。Kafka Consumer API为你提供了位移提交的方法如consumer.commitSync等。当调用这些方法时Kafka会向位移主题写入相应的消息。
如果你选择的是自动提交位移那么就可能存在一个问题只要Consumer一直启动着它就会无限期地向位移主题写入消息。
我们来举个极端一点的例子。假设Consumer当前消费到了某个主题的最新一条消息位移是100之后该主题没有任何新消息产生故Consumer无消息可消费了所以位移永远保持在100。由于是自动提交位移位移主题中会不停地写入位移=100的消息。显然Kafka只需要保留这类消息中的最新一条就可以了之前的消息都是可以删除的。这就要求Kafka必须要有针对位移主题消息特点的消息删除策略否则这种消息会越来越多最终撑爆整个磁盘。
Kafka是怎么删除位移主题中的过期消息的呢答案就是Compaction。国内很多文献都将其翻译成压缩我个人是有一点保留意见的。在英语中压缩的专有术语是Compression它的原理和Compaction很不相同我更倾向于翻译成压实或干脆采用JVM垃圾回收中的术语整理。
不管怎么翻译Kafka使用Compact策略来删除位移主题中的过期消息避免该主题无限期膨胀。那么应该如何定义Compact策略中的过期呢对于同一个Key的两条消息M1和M2如果M1的发送时间早于M2那么M1就是过期消息。Compact的过程就是扫描日志的所有消息剔除那些过期的消息然后把剩下的消息整理在一起。我在这里贴一张来自官网的图片来说明Compact过程。
图中位移为0、2和3的消息的Key都是K1。Compact之后分区只需要保存位移为3的消息因为它是最新发送的。
Kafka提供了专门的后台线程定期地巡检待Compact的主题看看是否存在满足条件的可删除数据。这个后台线程叫Log Cleaner。很多实际生产环境中都出现过位移主题无限膨胀占用过多磁盘空间的问题如果你的环境中也有这个问题我建议你去检查一下Log Cleaner线程的状态通常都是这个线程挂掉了导致的。
小结
总结一下今天我跟你分享了Kafka神秘的位移主题__consumer_offsets包括引入它的契机与原因、它的作用、消息格式、写入的时机以及管理策略等这对我们了解Kafka特别是Kafka Consumer的位移管理是大有帮助的。实际上将很多元数据以消息的方式存入Kafka内部主题的做法越来越流行。除了Consumer位移管理Kafka事务也是利用了这个方法当然那是另外的一个内部主题了。
社区的想法很简单既然Kafka天然实现了高持久性和高吞吐量那么任何有这两个需求的子服务自然也就不必求助于外部系统用Kafka自己实现就好了。
开放讨论
今天我们说了位移主题的很多好处请思考一下与ZooKeeper方案相比它可能的劣势是什么
欢迎写下你的思考和答案,我们一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@ -0,0 +1,110 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
17 消费者组重平衡能避免吗?
你好,我是胡夕。今天我要和你分享的内容是:消费者组重平衡能避免吗?
其实在专栏第15期中我们讲过重平衡也就是Rebalance现在先来回顾一下这个概念的原理和用途。Rebalance就是让一个Consumer Group下所有的Consumer实例就如何消费订阅主题的所有分区达成共识的过程。在Rebalance过程中所有Consumer实例共同参与在协调者组件的帮助下完成订阅主题分区的分配。但是在整个过程中所有实例都不能消费任何消息因此它对Consumer的TPS影响很大。
你可能会对这里提到的“协调者”有些陌生我来简单介绍下。所谓协调者在Kafka中对应的术语是Coordinator它专门为Consumer Group服务负责为Group执行Rebalance以及提供位移管理和组成员管理等。
具体来讲Consumer端应用程序在提交位移时其实是向Coordinator所在的Broker提交位移。同样地当Consumer应用启动时也是向Coordinator所在的Broker发送各种请求然后由Coordinator负责执行消费者组的注册、成员管理记录等元数据管理操作。
所有Broker在启动时都会创建和开启相应的Coordinator组件。也就是说所有Broker都有各自的Coordinator组件。那么Consumer Group如何确定为它服务的Coordinator在哪台Broker上呢答案就在我们之前说过的Kafka内部位移主题__consumer_offsets身上。
目前Kafka为某个Consumer Group确定Coordinator所在的Broker的算法有2个步骤。
第1步确定由位移主题的哪个分区来保存该Group数据partitionId=Math.abs(groupId.hashCode() % offsetsTopicPartitionCount)。
第2步找出该分区Leader副本所在的Broker该Broker即为对应的Coordinator。
简单解释一下上面的算法。首先Kafka会计算该Group的group.id参数的哈希值。比如你有个Group的group.id设置成了“test-group”那么它的hashCode值就应该是627841412。其次Kafka会计算__consumer_offsets的分区数通常是50个分区之后将刚才那个哈希值对分区数进行取模加求绝对值计算即abs(627841412 % 50) = 12。此时我们就知道了位移主题的分区12负责保存这个Group的数据。有了分区号算法的第2步就变得很简单了我们只需要找出位移主题分区12的Leader副本在哪个Broker上就可以了。这个Broker就是我们要找的Coordinator。
在实际使用过程中Consumer应用程序特别是Java Consumer API能够自动发现并连接正确的Coordinator我们不用操心这个问题。知晓这个算法的最大意义在于它能够帮助我们解决定位问题。当Consumer Group出现问题需要快速排查Broker端日志时我们能够根据这个算法准确定位Coordinator对应的Broker不必一台Broker一台Broker地盲查。
好了我们说回Rebalance。既然我们今天要讨论的是如何避免Rebalance那就说明Rebalance这个东西不好或者说至少有一些弊端需要我们去规避。那么Rebalance的弊端是什么呢总结起来有以下3点
Rebalance影响Consumer端TPS。这个之前也反复提到了这里就不再具体讲了。总之就是在Rebalance期间Consumer会停下手头的事情什么也干不了。
Rebalance很慢。如果你的Group下成员很多就一定会有这样的痛点。还记得我曾经举过的那个国外用户的例子吧他的Group下有几百个Consumer实例Rebalance一次要几个小时。在那种场景下Consumer Group的Rebalance已经完全失控了。
Rebalance效率不高。当前Kafka的设计机制决定了每次Rebalance时Group下的所有成员都要参与进来而且通常不会考虑局部性原理但局部性原理对提升系统性能是特别重要的。
关于第3点我们来举个简单的例子。比如一个Group下有10个成员每个成员平均消费5个分区。假设现在有一个成员退出了此时就需要开启新一轮的Rebalance把这个成员之前负责的5个分区“转移”给其他成员。显然比较好的做法是维持当前9个成员消费分区的方案不变然后将5个分区随机分配给这9个成员这样能最大限度地减少Rebalance对剩余Consumer成员的冲击。
遗憾的是目前Kafka并不是这样设计的。在默认情况下每次Rebalance时之前的分配方案都不会被保留。就拿刚刚这个例子来说当Rebalance开始时Group会打散这50个分区10个成员 * 5个分区由当前存活的9个成员重新分配它们。显然这不是效率很高的做法。基于这个原因社区于0.11.0.0版本推出了StickyAssignor即有粘性的分区分配策略。所谓的有粘性是指每次Rebalance时该策略会尽可能地保留之前的分配方案尽量实现分区分配的最小变动。不过有些遗憾的是这个策略目前还有一些bug而且需要升级到0.11.0.0才能使用,因此在实际生产环境中用得还不是很多。
总而言之Rebalance有以上这三个方面的弊端。你可能会问这些问题有解吗特别是针对Rebalance慢和影响TPS这两个弊端社区有解决办法吗针对这两点我可以很负责任地告诉你“无解”特别是Rebalance慢这个问题Kafka社区对此无能为力。“本事大不如不摊上”既然我们没办法解决Rebalance过程中的各种问题干脆就避免Rebalance吧特别是那些不必要的Rebalance。
就我个人经验而言在真实的业务场景中很多Rebalance都是计划外的或者说是不必要的。我们应用的TPS大多是被这类Rebalance拖慢的因此避免这类Rebalance就显得很有必要了。下面我们就来说说如何避免Rebalance。
要避免Rebalance还是要从Rebalance发生的时机入手。我们在前面说过Rebalance发生的时机有三个
组成员数量发生变化
订阅主题数量发生变化
订阅主题的分区数发生变化
后面两个通常都是运维的主动操作所以它们引发的Rebalance大都是不可避免的。接下来我们主要说说因为组成员数量变化而引发的Rebalance该如何避免。
如果Consumer Group下的Consumer实例数量发生变化就一定会引发Rebalance。这是Rebalance发生的最常见的原因。我碰到的99%的Rebalance都是这个原因导致的。
Consumer实例增加的情况很好理解当我们启动一个配置有相同group.id值的Consumer程序时实际上就向这个Group添加了一个新的Consumer实例。此时Coordinator会接纳这个新实例将其加入到组中并重新分配分区。通常来说增加Consumer实例的操作都是计划内的可能是出于增加TPS或提高伸缩性的需要。总之它不属于我们要规避的那类“不必要Rebalance”。
我们更在意的是Group下实例数减少这件事。如果你就是要停掉某些Consumer实例那自不必说关键是在某些情况下Consumer实例会被Coordinator错误地认为“已停止”从而被“踢出”Group。如果是这个原因导致的Rebalance我们就不能不管了。
Coordinator会在什么情况下认为某个Consumer实例已挂从而要退组呢这个绝对是需要好好讨论的话题我们来详细说说。
当Consumer Group完成Rebalance之后每个Consumer实例都会定期地向Coordinator发送心跳请求表明它还存活着。如果某个Consumer实例不能及时地发送这些心跳请求Coordinator就会认为该Consumer已经“死”了从而将其从Group中移除然后开启新一轮Rebalance。Consumer端有个参数叫session.timeout.ms就是被用来表征此事的。该参数的默认值是10秒即如果Coordinator在10秒之内没有收到Group下某Consumer实例的心跳它就会认为这个Consumer实例已经挂了。可以这么说session.timeout.ms决定了Consumer存活性的时间间隔。
除了这个参数Consumer还提供了一个允许你控制发送心跳请求频率的参数就是heartbeat.interval.ms。这个值设置得越小Consumer实例发送心跳请求的频率就越高。频繁地发送心跳请求会额外消耗带宽资源但好处是能够更加快速地知晓当前是否开启Rebalance因为目前Coordinator通知各个Consumer实例开启Rebalance的方法就是将REBALANCE_NEEDED标志封装进心跳请求的响应体中。
除了以上两个参数Consumer端还有一个参数用于控制Consumer实际消费能力对Rebalance的影响即max.poll.interval.ms参数。它限定了Consumer端应用程序两次调用poll方法的最大时间间隔。它的默认值是5分钟表示你的Consumer程序如果在5分钟之内无法消费完poll方法返回的消息那么Consumer会主动发起“离开组”的请求Coordinator也会开启新一轮Rebalance。
搞清楚了这些参数的含义接下来我们来明确一下到底哪些Rebalance是“不必要的”。
第一类非必要Rebalance是因为未能及时发送心跳导致Consumer被“踢出”Group而引发的。因此你需要仔细地设置session.timeout.ms和heartbeat.interval.ms的值。我在这里给出一些推荐数值你可以“无脑”地应用在你的生产环境中。
设置session.timeout.ms = 6s。
设置heartbeat.interval.ms = 2s。
要保证Consumer实例在被判定为“dead”之前能够发送至少3轮的心跳请求即session.timeout.ms >= 3 * heartbeat.interval.ms。
将session.timeout.ms设置成6s主要是为了让Coordinator能够更快地定位已经挂掉的Consumer。毕竟我们还是希望能尽快揪出那些“尸位素餐”的Consumer早日把它们踢出Group。希望这份配置能够较好地帮助你规避第一类“不必要”的Rebalance。
第二类非必要Rebalance是Consumer消费时间过长导致的。我之前有一个客户在他们的场景中Consumer消费数据时需要将消息处理之后写入到MongoDB。显然这是一个很重的消费逻辑。MongoDB的一丁点不稳定都会导致Consumer程序消费时长的增加。此时max.poll.interval.ms参数值的设置显得尤为关键。如果要避免非预期的Rebalance你最好将该参数值设置得大一点比你的下游最大处理时间稍长一点。就拿MongoDB这个例子来说如果写MongoDB的最长时间是7分钟那么你可以将该参数设置为8分钟左右。
总之你要为你的业务处理逻辑留下充足的时间。这样Consumer就不会因为处理这些消息的时间太长而引发Rebalance了。
如果你按照上面的推荐数值恰当地设置了这几个参数却发现还是出现了Rebalance那么我建议你去排查一下Consumer端的GC表现比如是否出现了频繁的Full GC导致的长时间停顿从而引发了Rebalance。为什么特意说GC那是因为在实际场景中我见过太多因为GC设置不合理导致程序频发Full GC而引发的非预期Rebalance了。
小结
总而言之,我们一定要避免因为各种参数或逻辑不合理而导致的组成员意外离组或退出的情形,与之相关的主要参数有:
session.timeout.ms
heartbeat.interval.ms
max.poll.interval.ms
GC参数
按照我们今天所说的内容恰当地设置这些参数你一定能够大幅度地降低生产环境中的Rebalance数量从而整体提升Consumer端TPS。
开放讨论
说说在你的业务场景中Rebalance发生的频率、原因以及你是怎么应对的我们一起讨论下是否有更好的解决方案。
欢迎写下你的思考和答案,我们一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@ -0,0 +1,158 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
18 Kafka中位移提交那些事儿
你好我是胡夕。今天我们来聊聊Kafka中位移提交的那些事儿。
之前我们说过Consumer端有个位移的概念它和消息在分区中的位移不是一回事儿虽然它们的英文都是Offset。今天我们要聊的位移是Consumer的消费位移它记录了Consumer要消费的下一条消息的位移。这可能和你以前了解的有些出入不过切记是下一条消息的位移而不是目前最新消费消息的位移。
我来举个例子说明一下。假设一个分区中有10条消息位移分别是0到9。某个Consumer应用已消费了5条消息这就说明该Consumer消费了位移为0到4的5条消息此时Consumer的位移是5指向了下一条消息的位移。
Consumer需要向Kafka汇报自己的位移数据这个汇报过程被称为提交位移Committing Offsets。因为Consumer能够同时消费多个分区的数据所以位移的提交实际上是在分区粒度上进行的即Consumer需要为分配给它的每个分区提交各自的位移数据。
提交位移主要是为了表征Consumer的消费进度这样当Consumer发生故障重启之后就能够从Kafka中读取之前提交的位移值然后从相应的位移处继续消费从而避免整个消费过程重来一遍。换句话说位移提交是Kafka提供给你的一个工具或语义保障你负责维持这个语义保障即如果你提交了位移X那么Kafka会认为所有位移值小于X的消息你都已经成功消费了。
这一点特别关键。因为位移提交非常灵活你完全可以提交任何位移值但由此产生的后果你也要一并承担。假设你的Consumer消费了10条消息你提交的位移值却是20那么从理论上讲位移介于1119之间的消息是有可能丢失的相反地如果你提交的位移值是5那么位移介于59之间的消息就有可能被重复消费。所以我想再强调一下位移提交的语义保障是由你来负责的Kafka只会“无脑”地接受你提交的位移。你对位移提交的管理直接影响了你的Consumer所能提供的消息语义保障。
鉴于位移提交甚至是位移管理对Consumer端的巨大影响Kafka特别是KafkaConsumer API提供了多种提交位移的方法。从用户的角度来说位移提交分为自动提交和手动提交从Consumer端的角度来说位移提交分为同步提交和异步提交。
我们先来说说自动提交和手动提交。所谓自动提交就是指Kafka Consumer在后台默默地为你提交位移作为用户的你完全不必操心这些事而手动提交则是指你要自己提交位移Kafka Consumer压根不管。
开启自动提交位移的方法很简单。Consumer端有个参数enable.auto.commit把它设置为true或者压根不设置它就可以了。因为它的默认值就是true即Java Consumer默认就是自动提交位移的。如果启用了自动提交Consumer端还有个参数就派上用场了auto.commit.interval.ms。它的默认值是5秒表明Kafka每5秒会为你自动提交一次位移。
为了把这个问题说清楚我给出了完整的Java代码。这段代码展示了设置自动提交位移的方法。有了这段代码做基础今天后面的讲解我就不再展示完整的代码了。
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("group.id", "test");
props.put("enable.auto.commit", "true");
props.put("auto.commit.interval.ms", "2000");
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
consumer.subscribe(Arrays.asList("foo", "bar"));
while (true) {
ConsumerRecords<String, String> records = consumer.poll(100);
for (ConsumerRecord<String, String> record : records)
System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
}
上面的第3、第4行代码就是开启自动提交位移的方法。总体来说还是很简单的吧。
和自动提交相反的就是手动提交了。开启手动提交位移的方法就是设置enable.auto.commit为false。但是仅仅设置它为false还不够因为你只是告诉Kafka Consumer不要自动提交位移而已你还需要调用相应的API手动提交位移。
最简单的API就是KafkaConsumer#commitSync()。该方法会提交KafkaConsumer#poll()返回的最新位移。从名字上来看它是一个同步操作即该方法会一直等待直到位移被成功提交才会返回。如果提交过程中出现异常该方法会将异常信息抛出。下面这段代码展示了commitSync()的使用方法:
while (true) {
ConsumerRecords<String, String> records =
consumer.poll(Duration.ofSeconds(1));
process(records); // 处理消息
try {
consumer.commitSync();
} catch (CommitFailedException e) {
handle(e); // 处理提交失败异常
}
}
可见调用consumer.commitSync()方法的时机是在你处理完了poll()方法返回的所有消息之后。如果你莽撞地过早提交了位移,就可能会出现消费数据丢失的情况。那么你可能会问,自动提交位移就不会出现消费数据丢失的情况了吗?它能恰到好处地把握时机进行位移提交吗?为了搞清楚这个问题,我们必须要深入地了解一下自动提交位移的顺序。
一旦设置了enable.auto.commit为trueKafka会保证在开始调用poll方法时提交上次poll返回的所有消息。从顺序上来说poll方法的逻辑是先提交上一批消息的位移再处理下一批消息因此它能保证不出现消费丢失的情况。但自动提交位移的一个问题在于它可能会出现重复消费。
在默认情况下Consumer每5秒自动提交一次位移。现在我们假设提交位移之后的3秒发生了Rebalance操作。在Rebalance之后所有Consumer从上一次提交的位移处继续消费但该位移已经是3秒前的位移数据了故在Rebalance发生前3秒消费的所有数据都要重新再消费一次。虽然你能够通过减少auto.commit.interval.ms的值来提高提交频率但这么做只能缩小重复消费的时间窗口不可能完全消除它。这是自动提交机制的一个缺陷。
反观手动提交位移它的好处就在于更加灵活你完全能够把控位移提交的时机和频率。但是它也有一个缺陷就是在调用commitSync()时Consumer程序会处于阻塞状态直到远端的Broker返回提交结果这个状态才会结束。在任何系统中因为程序而非资源限制而导致的阻塞都可能是系统的瓶颈会影响整个应用程序的TPS。当然你可以选择拉长提交间隔但这样做的后果是Consumer的提交频率下降在下次Consumer重启回来后会有更多的消息被重新消费。
鉴于这个问题Kafka社区为手动提交位移提供了另一个API方法KafkaConsumer#commitAsync()。从名字上来看它就不是同步的而是一个异步操作。调用commitAsync()之后它会立即返回不会阻塞因此不会影响Consumer应用的TPS。由于它是异步的Kafka提供了回调函数callback供你实现提交之后的逻辑比如记录日志或处理异常等。下面这段代码展示了调用commitAsync()的方法:
while (true) {
ConsumerRecords<String, String> records =
consumer.poll(Duration.ofSeconds(1));
process(records); // 处理消息
consumer.commitAsync((offsets, exception) -> {
if (exception != null)
handle(exception);
});
}
commitAsync是否能够替代commitSync呢答案是不能。commitAsync的问题在于出现问题时它不会自动重试。因为它是异步操作倘若提交失败后自动重试那么它重试时提交的位移值可能早已经“过期”或不是最新值了。因此异步提交的重试其实没有意义所以commitAsync是不会重试的。
显然如果是手动提交我们需要将commitSync和commitAsync组合使用才能达到最理想的效果原因有两个
我们可以利用commitSync的自动重试来规避那些瞬时错误比如网络的瞬时抖动Broker端GC等。因为这些问题都是短暂的自动重试通常都会成功因此我们不想自己重试而是希望Kafka Consumer帮我们做这件事。
我们不希望程序总处于阻塞状态影响TPS。
我们来看一下下面这段代码它展示的是如何将两个API方法结合使用进行手动提交。
try {
while(true) {
ConsumerRecords<String, String> records =
consumer.poll(Duration.ofSeconds(1));
process(records); // 处理消息
commitAysnc(); // 使用异步提交规避阻塞
}
} catch(Exception e) {
handle(e); // 处理异常
} finally {
try {
consumer.commitSync(); // 最后一次提交使用同步阻塞式提交
} finally {
consumer.close();
}
}
这段代码同时使用了commitSync()和commitAsync()。对于常规性、阶段性的手动提交我们调用commitAsync()避免程序阻塞而在Consumer要关闭前我们调用commitSync()方法执行同步阻塞式的位移提交以确保Consumer关闭前能够保存正确的位移数据。将两者结合后我们既实现了异步无阻塞式的位移管理也确保了Consumer位移的正确性所以如果你需要自行编写代码开发一套Kafka Consumer应用那么我推荐你使用上面的代码范例来实现手动的位移提交。
我们说了自动提交和手动提交也说了同步提交和异步提交这些就是Kafka位移提交的全部了吗其实我们还差一部分。
实际上Kafka Consumer API还提供了一组更为方便的方法可以帮助你实现更精细化的位移管理功能。刚刚我们聊到的所有位移提交都是提交poll方法返回的所有消息的位移比如poll方法一次返回了500条消息当你处理完这500条消息之后前面我们提到的各种方法会一次性地将这500条消息的位移一并处理。简单来说就是直接提交最新一条消息的位移。但如果我想更加细粒度化地提交位移该怎么办呢
设想这样一个场景你的poll方法返回的不是500条消息而是5000条。那么你肯定不想把这5000条消息都处理完之后再提交位移因为一旦中间出现差错之前处理的全部都要重来一遍。这类似于我们数据库中的事务处理。很多时候我们希望将一个大事务分割成若干个小事务分别提交这能够有效减少错误恢复的时间。
在Kafka中也是相同的道理。对于一次要处理很多消息的Consumer而言它会关心社区有没有方法允许它在消费的中间进行位移提交。比如前面这个5000条消息的例子你可能希望每处理完100条消息就提交一次位移这样能够避免大批量的消息重新消费。
庆幸的是Kafka Consumer API为手动提交提供了这样的方法commitSync(Map)和commitAsync(Map)。它们的参数是一个Map对象键就是TopicPartition即消费的分区而值是一个OffsetAndMetadata对象保存的主要是位移数据。
就拿刚刚提过的那个例子来说如何每处理100条消息就提交一次位移呢在这里我以commitAsync为例展示一段代码实际上commitSync的调用方法和它是一模一样的。
private Map<TopicPartition, OffsetAndMetadata> offsets = new HashMap<>();
int count = 0;
……
while (true) {
ConsumerRecords<String, String> records =
consumer.poll(Duration.ofSeconds(1));
for (ConsumerRecord<String, String> record: records) {
process(record); // 处理消息
offsets.put(new TopicPartition(record.topic(), record.partition()),
new OffsetAndMetadata(record.offset() + 1)
ifcount % 100 == 0
consumer.commitAsync(offsets, null); // 回调处理逻辑是null
count++;
}
}
简单解释一下这段代码。程序先是创建了一个Map对象用于保存Consumer消费处理过程中要提交的分区位移之后开始逐条处理消息并构造要提交的位移值。还记得之前我说过要提交下一条消息的位移吗这就是这里构造OffsetAndMetadata对象时使用当前消息位移加1的原因。代码的最后部分是做位移的提交。我在这里设置了一个计数器每累计100条消息就统一提交一次位移。与调用无参的commitAsync不同这里调用了带Map对象参数的commitAsync进行细粒度的位移提交。这样这段代码就能够实现每处理100条消息就提交一次位移不用再受poll方法返回的消息总数的限制了。
小结
好了我们来总结一下今天的内容。Kafka Consumer的位移提交是实现Consumer端语义保障的重要手段。位移提交分为自动提交和手动提交而手动提交又分为同步提交和异步提交。在实际使用过程中推荐你使用手动提交机制因为它更加可控也更加灵活。另外建议你同时采用同步提交和异步提交两种方式这样既不影响TPS又支持自动重试改善Consumer应用的高可用性。总之Kafka Consumer API提供了多种灵活的提交方法方便你根据自己的业务场景定制你的提交策略。
开放讨论
实际上手动提交也不能避免消息重复消费。假设Consumer在处理完消息和提交位移前出现故障下次重启后依然会出现消息重复消费的情况。请你思考一下如何实现你的业务场景中的去重逻辑呢
欢迎写下你的思考和答案,我们一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@ -0,0 +1,103 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
19 CommitFailedException异常怎么处理
你好我是胡夕。今天我来跟你聊聊CommitFailedException异常的处理。
说起这个异常我相信用过Kafka Java Consumer客户端API的你一定不会感到陌生。所谓CommitFailedException顾名思义就是Consumer客户端在提交位移时出现了错误或异常而且还是那种不可恢复的严重异常。如果异常是可恢复的瞬时错误提交位移的API自己就能规避它们了因为很多提交位移的API方法是支持自动错误重试的比如我们在上一期中提到的commitSync方法。
每次和CommitFailedException一起出现的还有一段非常著名的注释。为什么说它很“著名”呢第一我想不出在近50万行的Kafka源代码中还有哪个异常类能有这种待遇可以享有这么大段的注释来阐述其异常的含义第二纵然有这么长的文字解释却依然有很多人对该异常想表达的含义感到困惑。
现在,我们一起领略下这段文字的风采,看看社区对这个异常的最新解释:
Commit cannot be completed since the group has already rebalanced and assigned the partitions to another member. This means that the time between subsequent calls to poll() was longer than the configured max.poll.interval.ms, which typically implies that the poll loop is spending too much time message processing. You can address this either by increasing max.poll.interval.ms or by reducing the maximum size of batches returned in poll() with max.poll.records.
这段话前半部分的意思是本次提交位移失败了原因是消费者组已经开启了Rebalance过程并且将要提交位移的分区分配给了另一个消费者实例。出现这个情况的原因是你的消费者实例连续两次调用poll方法的时间间隔超过了期望的max.poll.interval.ms参数值。这通常表明你的消费者实例花费了太长的时间进行消息处理耽误了调用poll方法。
在后半部分,社区给出了两个相应的解决办法(即橙色字部分):
增加期望的时间间隔max.poll.interval.ms参数值。
减少poll方法一次性返回的消息数量即减少max.poll.records参数值。
在详细讨论这段文字之前我还想提一句实际上这段文字总共有3个版本除了上面的这个最新版本还有2个版本它们分别是
Commit cannot be completed since the group has already rebalanced and assigned the partitions to another member. This means that the time between subsequent calls to poll() was longer than the configured session.timeout.ms, which typically implies that the poll loop is spending too much time message processing. You can address this either by increasing the session timeout or by reducing the maximum size of batches returned in poll() with max.poll.records.
Commit cannot be completed since the group has already rebalanced and assigned the partitions to another member. This means that the time between subsequent calls to poll() was longer than the configured max.poll.interval.ms, which typically implies that the poll loop is spending too much time message processing. You can address this either by increasing the session timeout or by reducing the maximum size of batches returned in poll() with max.poll.records.
这两个较早的版本和最新版相差不大,我就不详细解释了,具体的差异我用橙色标注了。我之所以列出这些版本,就是想让你在日后看到它们时能做到心中有数,知道它们说的是一个事情。
其实不论是哪段文字它们都表征位移提交出现了异常。下面我们就来讨论下该异常是什么时候被抛出的。从源代码方面来说CommitFailedException异常通常发生在手动提交位移时即用户显式调用KafkaConsumer.commitSync()方法时。从使用场景来说,有两种典型的场景可能遭遇该异常。
场景一
我们先说说最常见的场景。当消息处理的总时间超过预设的max.poll.interval.ms参数值时Kafka Consumer端会抛出CommitFailedException异常。这是该异常最“正宗”的登场方式。你只需要写一个Consumer程序使用KafkaConsumer.subscribe方法随意订阅一个主题之后设置Consumer端参数max.poll.interval.ms=5秒最后在循环调用KafkaConsumer.poll方法之间插入Thread.sleep(6000)和手动提交位移,就可以成功复现这个异常了。在这里,我展示一下主要的代码逻辑。
Properties props = new Properties();
props.put("max.poll.interval.ms", 5000);
consumer.subscribe(Arrays.asList("test-topic"));
while (true) {
ConsumerRecords<String, String> records =
consumer.poll(Duration.ofSeconds(1));
// 使用Thread.sleep模拟真实的消息处理逻辑
Thread.sleep(6000L);
consumer.commitSync();
}
如果要防止这种场景下抛出异常你需要简化你的消息处理逻辑。具体来说有4种方法。
缩短单条消息处理的时间。比如之前下游系统消费一条消息的时间是100毫秒优化之后成功地下降到50毫秒那么此时Consumer端的TPS就提升了一倍。
增加Consumer端允许下游系统消费一批消息的最大时长。这取决于Consumer端参数max.poll.interval.ms的值。在最新版的Kafka中该参数的默认值是5分钟。如果你的消费逻辑不能简化那么提高该参数值是一个不错的办法。值得一提的是Kafka 0.10.1.0之前的版本是没有这个参数的因此如果你依然在使用0.10.1.0之前的客户端API那么你需要增加session.timeout.ms参数的值。不幸的是session.timeout.ms参数还有其他的含义因此增加该参数的值可能会有其他方面的“不良影响”这也是社区在0.10.1.0版本引入max.poll.interval.ms参数将这部分含义从session.timeout.ms中剥离出来的原因之一。
减少下游系统一次性消费的消息总数。这取决于Consumer端参数max.poll.records的值。当前该参数的默认值是500条表明调用一次KafkaConsumer.poll方法最多返回500条消息。可以说该参数规定了单次poll方法能够返回的消息总数的上限。如果前两种方法对你都不适用的话降低此参数值是避免CommitFailedException异常最简单的手段。
下游系统使用多线程来加速消费。这应该算是“最高级”同时也是最难实现的解决办法了。具体的思路就是让下游系统手动创建多个消费线程处理poll方法返回的一批消息。之前你使用Kafka Consumer消费数据更多是单线程的所以当消费速度无法匹及Kafka Consumer消息返回的速度时它就会抛出CommitFailedException异常。如果是多线程你就可以灵活地控制线程数量随时调整消费承载能力再配以目前多核的硬件条件该方法可谓是防止CommitFailedException最高档的解决之道。事实上很多主流的大数据流处理框架使用的都是这个方法比如Apache Flink在集成Kafka时就是创建了多个KafkaConsumerThread线程自行处理多线程间的数据消费。不过凡事有利就有弊这个方法实现起来并不容易特别是在多个线程间如何处理位移提交这个问题上更是极容易出错。在专栏后面的内容中我将着重和你讨论一下多线程消费的实现方案。
综合以上这4个处理方法我个人推荐你首先尝试采用方法1来预防此异常的发生。优化下游系统的消费逻辑是百利而无一害的法子不像方法2、3那样涉及到Kafka Consumer端TPS与消费延时Latency的权衡。如果方法1实现起来有难度那么你可以按照下面的法则来实践方法2、3。
首先你需要弄清楚你的下游系统消费每条消息的平均延时是多少。比如你的消费逻辑是从Kafka获取到消息后写入到下游的MongoDB中假设访问MongoDB的平均延时不超过2秒那么你可以认为消息处理需要花费2秒的时间。如果按照max.poll.records等于500来计算一批消息的总消费时长大约是1000秒因此你的Consumer端的max.poll.interval.ms参数值就不能低于1000秒。如果你使用默认配置那默认值5分钟显然是不够的你将有很大概率遭遇CommitFailedException异常。将max.poll.interval.ms增加到1000秒以上的做法就属于上面的第2种方法。
除了调整max.poll.interval.ms之外你还可以选择调整max.poll.records值减少每次poll方法返回的消息数。还拿刚才的例子来说你可以设置max.poll.records值为150甚至更少这样每批消息的总消费时长不会超过300秒150*2=300即max.poll.interval.ms的默认值5分钟。这种减少max.poll.records值的做法就属于上面提到的方法3。
场景二
Okay现在我们已经说完了关于CommitFailedException异常的经典发生场景以及应对办法。从理论上讲关于该异常你了解到这个程度已经足以帮助你应对应用开发过程中由该异常带来的“坑”了 。但其实该异常还有一个不太为人所知的出现场景。了解这个冷门场景可以帮助你拓宽Kafka Consumer的知识面也能提前预防一些古怪的问题。下面我们就来说说这个场景。
之前我们花了很多时间学习Kafka的消费者不过大都集中在消费者组上即所谓的Consumer Group。其实Kafka Java Consumer端还提供了一个名为Standalone Consumer的独立消费者。它没有消费者组的概念每个消费者实例都是独立工作的彼此之间毫无联系。不过你需要注意的是独立消费者的位移提交机制和消费者组是一样的因此独立消费者的位移提交也必须遵守之前说的那些规定比如独立消费者也要指定group.id参数才能提交位移。你可能会觉得奇怪既然是独立消费者为什么还要指定group.id呢没办法谁让社区就是这么设计的呢总之消费者组和独立消费者在使用之前都要指定group.id。
现在问题来了如果你的应用中同时出现了设置相同group.id值的消费者组程序和独立消费者程序那么当独立消费者程序手动提交位移时Kafka就会立即抛出CommitFailedException异常因为Kafka无法识别这个具有相同group.id的消费者实例于是就向它返回一个错误表明它不是消费者组内合法的成员。
虽然说这个场景很冷门但也并非完全不会遇到。在一个大型公司中特别是那些将Kafka作为全公司级消息引擎系统的公司中每个部门或团队都可能有自己的消费者应用谁能保证各自的Consumer程序配置的group.id没有重复呢一旦出现不凑巧的重复发生了上面提到的这种场景你使用之前提到的哪种方法都不能规避该异常。令人沮丧的是无论是刚才哪个版本的异常说明都完全没有提及这个场景因此如果是这个原因引发的CommitFailedException异常前面的4种方法全部都是无效的。
更为尴尬的是无论是社区官网还是网上的文章都没有提到过这种使用场景。我个人认为这应该算是Kafka的一个bug。比起返回CommitFailedException异常只是表明提交位移失败更好的做法应该是在Consumer端应用程序的某个地方能够以日志或其他方式友善地提示你错误的原因这样你才能正确处理甚至是预防该异常。
小结
总结一下今天我们详细讨论了Kafka Consumer端经常碰到的CommitFailedException异常。我们从它的含义说起再到它出现的时机和场景以及每种场景下的应对之道。当然我也留了个悬念在专栏后面的内容中我会详细说说多线程消费的实现方式。希望通过今天的分享你能清晰地掌握CommitFailedException异常发生的方方面面从而能在今后更有效地应对此异常。
开放讨论
请比较一下今天我们提到的预防该异常的4种方法并说说你对它们的理解。
欢迎写下你的思考和答案,我们一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@ -0,0 +1,163 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
20 多线程开发消费者实例
你好我是胡夕。今天我们来聊聊Kafka Java Consumer端多线程消费的实现方案。
目前计算机的硬件条件已经大大改善即使是在普通的笔记本电脑上多核都已经是标配了更不用说专业的服务器了。如果跑在强劲服务器机器上的应用程序依然是单线程架构那实在是有点暴殄天物了。不过Kafka Java Consumer就是单线程的设计你是不是感到很惊讶。所以探究它的多线程消费方案就显得非常必要了。
Kafka Java Consumer设计原理
在开始探究之前我先简单阐述下Kafka Java Consumer为什么采用单线程的设计。了解了这一点对我们后面制定多线程方案大有裨益。
谈到Java Consumer API最重要的当属它的入口类KafkaConsumer了。我们说KafkaConsumer是单线程的设计严格来说这是不准确的。因为从Kafka 0.10.1.0版本开始KafkaConsumer就变为了双线程的设计即用户主线程和心跳线程。
所谓用户主线程就是你启动Consumer应用程序main方法的那个线程而新引入的心跳线程Heartbeat Thread只负责定期给对应的Broker机器发送心跳请求以标识消费者应用的存活性liveness。引入这个心跳线程还有一个目的那就是期望它能将心跳频率与主线程调用KafkaConsumer.poll方法的频率分开从而解耦真实的消息处理逻辑与消费者组成员存活性管理。
不过虽然有心跳线程但实际的消息获取逻辑依然是在用户主线程中完成的。因此在消费消息的这个层面上我们依然可以安全地认为KafkaConsumer是单线程的设计。
其实在社区推出Java Consumer API之前Kafka中存在着一组统称为Scala Consumer的API。这组API或者说这个Consumer也被称为老版本Consumer目前在新版的Kafka代码中已经被完全移除了。
我之所以重提旧事是想告诉你老版本Consumer是多线程的架构每个Consumer实例在内部为所有订阅的主题分区创建对应的消息获取线程也称Fetcher线程。老版本Consumer同时也是阻塞式的blockingConsumer实例启动后内部会创建很多阻塞式的消息获取迭代器。但在很多场景下Consumer端是有非阻塞需求的比如在流处理应用中执行过滤filter、连接join、分组group by等操作时就不能是阻塞式的。基于这个原因社区为新版本Consumer设计了单线程+轮询的机制。这种设计能够较好地实现非阻塞式的消息获取。
除此之外单线程的设计能够简化Consumer端的设计。Consumer获取到消息后处理消息的逻辑是否采用多线程完全由你决定。这样你就拥有了把消息处理的多线程管理策略从Consumer端代码中剥离的权利。
另外不论使用哪种编程语言单线程的设计都比较容易实现。相反并不是所有的编程语言都能够很好地支持多线程。从这一点上来说单线程设计的Consumer更容易移植到其他语言上。毕竟Kafka社区想要打造上下游生态的话肯定是希望出现越来越多的客户端的。
多线程方案
了解了单线程的设计原理之后我们来具体分析一下KafkaConsumer这个类的使用方法以及如何推演出对应的多线程方案。
首先我们要明确的是KafkaConsumer类不是线程安全的(thread-safe)。所有的网络I/O处理都是发生在用户主线程中因此你在使用过程中必须要确保线程安全。简单来说就是你不能在多个线程中共享同一个KafkaConsumer实例否则程序会抛出ConcurrentModificationException异常。
当然了这也不是绝对的。KafkaConsumer中有个方法是例外的它就是wakeup()你可以在其他线程中安全地调用KafkaConsumer.wakeup()来唤醒Consumer。
鉴于KafkaConsumer不是线程安全的事实我们能够制定两套多线程方案。
1.消费者程序启动多个线程每个线程维护专属的KafkaConsumer实例负责完整的消息获取、消息处理流程。如下图所示
2.消费者程序使用单或多线程获取消息同时创建多个消费线程执行消息处理逻辑。获取消息的线程可以是一个也可以是多个每个线程维护专属的KafkaConsumer实例处理消息则交由特定的线程池来做从而实现消息获取与消息处理的真正解耦。具体架构如下图所示
总体来说,这两种方案都会创建多个线程,这些线程都会参与到消息的消费过程中,但各自的思路是不一样的。
我们来打个比方。比如一个完整的消费者应用程序要做的事情是1、2、3、4、5那么方案1的思路是粗粒度化的工作划分也就是说方案1会创建多个线程每个线程完整地执行1、2、3、4、5以实现并行处理的目标它不会进一步分割具体的子任务而方案2则更细粒度化它会将1、2分割出来用单线程也可以是多线程来做对于3、4、5则用另外的多个线程来做。
这两种方案孰优孰劣呢?应该说是各有千秋。我总结了一下这两种方案的优缺点,我们先来看看下面这张表格。
接下来,我来具体解释一下表格中的内容。
我们先看方案1它的优势有3点。
实现起来简单因为它比较符合目前我们使用Consumer API的习惯。我们在写代码的时候使用多个线程并在每个线程中创建专属的KafkaConsumer实例就可以了。
多个线程之间彼此没有任何交互,省去了很多保障线程安全方面的开销。
由于每个线程使用专属的KafkaConsumer实例来执行消息获取和消息处理逻辑因此Kafka主题中的每个分区都能保证只被一个线程处理这样就很容易实现分区内的消息消费顺序。这对在乎事件先后顺序的应用场景来说是非常重要的优势。
说完了方案1的优势我们来看看这个方案的不足之处。
每个线程都维护自己的KafkaConsumer实例必然会占用更多的系统资源比如内存、TCP连接等。在资源紧张的系统环境中方案1的这个劣势会表现得更加明显。
这个方案能使用的线程数受限于Consumer订阅主题的总分区数。我们知道在一个消费者组中每个订阅分区都只能被组内的一个消费者实例所消费。假设一个消费者组订阅了100个分区那么方案1最多只能扩展到100个线程多余的线程无法分配到任何分区只会白白消耗系统资源。当然了这种扩展性方面的局限可以被多机架构所缓解。除了在一台机器上启用100个线程消费数据我们也可以选择在100台机器上分别创建1个线程效果是一样的。因此如果你的机器资源很丰富这个劣势就不足为虑了。
每个线程完整地执行消息获取和消息处理逻辑。一旦消息处理逻辑很重造成消息处理速度慢就很容易出现不必要的Rebalance从而引发整个消费者组的消费停滞。这个劣势你一定要注意。我们之前讨论过如何避免Rebalance如果你不记得的话可以回到专栏第17讲复习一下。
下面我们来说说方案2。
与方案1的粗粒度不同方案2将任务切分成了消息获取和消息处理两个部分分别由不同的线程处理它们。比起方案1方案2的最大优势就在于它的高伸缩性就是说我们可以独立地调节消息获取的线程数以及消息处理的线程数而不必考虑两者之间是否相互影响。如果你的消费获取速度慢那么增加消费获取的线程数即可如果是消息的处理速度慢那么增加Worker线程池线程数即可。
不过,这种架构也有它的缺陷。
它的实现难度要比方案1大得多毕竟它有两组线程你需要分别管理它们。
因为该方案将消息获取和消息处理分开了也就是说获取某条消息的线程不是处理该消息的线程因此无法保证分区内的消费顺序。举个例子比如在某个分区中消息1在消息2之前被保存那么Consumer获取消息的顺序必然是消息1在前消息2在后但是后面的Worker线程却有可能先处理消息2再处理消息1这就破坏了消息在分区中的顺序。还是那句话如果你在意Kafka中消息的先后顺序方案2的这个劣势是致命的。
方案2引入了多组线程使得整个消息消费链路被拉长最终导致正确位移提交会变得异常困难结果就是可能会出现消息的重复消费。如果你在意这一点那么我不推荐你使用方案2。
实现代码示例
讲了这么多纯理论的东西接下来我们来看看实际的实现代码大概是什么样子。毕竟就像Linus说的“Talk is cheap, show me the code!”
我先跟你分享一段方案1的主体代码
public class KafkaConsumerRunner implements Runnable {
private final AtomicBoolean closed = new AtomicBoolean(false);
private final KafkaConsumer consumer;
public void run() {
try {
consumer.subscribe(Arrays.asList("topic"));
while (!closed.get()) {
ConsumerRecords records =
consumer.poll(Duration.ofMillis(10000));
// 执行消息处理逻辑
}
} catch (WakeupException e) {
// Ignore exception if closing
if (!closed.get()) throw e;
} finally {
consumer.close();
}
}
// Shutdown hook which can be called from a separate thread
public void shutdown() {
closed.set(true);
consumer.wakeup();
}
这段代码创建了一个Runnable类表示执行消费获取和消费处理的逻辑。每个KafkaConsumerRunner类都会创建一个专属的KafkaConsumer实例。在实际应用中你可以创建多个KafkaConsumerRunner实例并依次执行启动它们以实现方案1的多线程架构。
对于方案2来说核心的代码是这样的
private final KafkaConsumer<String, String> consumer;
private ExecutorService executors;
...
private int workerNum = ...;
executors = new ThreadPoolExecutor(
workerNum, workerNum, 0L, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(1000),
new ThreadPoolExecutor.CallerRunsPolicy());
...
while (true) {
ConsumerRecords<String, String> records =
consumer.poll(Duration.ofSeconds(1));
for (final ConsumerRecord record : records) {
executors.submit(new Worker(record));
}
}
..
这段代码最重要的地方是最后一行当Consumer的poll方法返回消息后由专门的线程池来负责处理具体的消息。调用poll方法的主线程不负责消息处理逻辑这样就实现了方案2的多线程架构。
小结
总结一下今天我跟你分享了Kafka Java Consumer多线程消费的实现方案。我给出了比较通用的两种方案并介绍了它们各自的优缺点以及代码示例。我希望你能根据这些内容结合你的实际业务场景实现适合你自己的多线程架构真正做到举一反三、融会贯通彻底掌握多线程消费的精髓从而在日后实现更宏大的系统。
开放讨论
今天我们讨论的都是多线程的方案可能有人会说何必这么麻烦我直接启动多个Consumer进程不就得了那么请你比较一下多线程方案和多进程方案想一想它们各自的优劣之处。
欢迎写下你的思考和答案,我们一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@ -0,0 +1,127 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
21 Java 消费者是如何管理TCP连接的_
你好我是胡夕。今天我要和你分享的主题是Kafka的Java消费者是如何管理TCP连接的。
在专栏第13讲中我们专门聊过“Java生产者是如何管理TCP连接资源的”这个话题你应该还有印象吧今天算是它的姊妹篇我们一起来研究下Kafka的Java消费者管理TCP或Socket资源的机制。只有完成了今天的讨论我们才算是对Kafka客户端的TCP连接管理机制有了全面的了解。
和之前一样我今天会无差别地混用TCP和Socket两个术语。毕竟在Kafka的世界中无论是ServerSocket还是SocketChannel它们实现的都是TCP协议。或者这么说Kafka的网络传输是基于TCP协议的而不是基于UDP协议因此当我今天说到TCP连接或Socket资源时我指的是同一个东西。
何时创建TCP连接
我们先从消费者创建TCP连接开始讨论。消费者端主要的程序入口是KafkaConsumer类。和生产者不同的是构建KafkaConsumer实例时是不会创建任何TCP连接的也就是说当你执行完new KafkaConsumer(properties)语句后你会发现没有Socket连接被创建出来。这一点和Java生产者是有区别的主要原因就是生产者入口类KafkaProducer在构建实例的时候会在后台默默地启动一个Sender线程这个Sender线程负责Socket连接的创建。
从这一点上来看我个人认为KafkaConsumer的设计比KafkaProducer要好。就像我在第13讲中所说的在Java构造函数中启动线程会造成this指针的逃逸这始终是一个隐患。
如果Socket不是在构造函数中创建的那么是在KafkaConsumer.subscribe或KafkaConsumer.assign方法中创建的吗严格来说也不是。我还是直接给出答案吧TCP连接是在调用KafkaConsumer.poll方法时被创建的。再细粒度地说在poll方法内部有3个时机可以创建TCP连接。
1.发起FindCoordinator请求时。
还记得消费者端有个组件叫协调者Coordinator它驻留在Broker端的内存中负责消费者组的组成员管理和各个消费者的位移提交管理。当消费者程序首次启动调用poll方法时它需要向Kafka集群发送一个名为FindCoordinator的请求希望Kafka集群告诉它哪个Broker是管理它的协调者。
不过消费者应该向哪个Broker发送这类请求呢理论上任何一个Broker都能回答这个问题也就是说消费者可以发送FindCoordinator请求给集群中的任意服务器。在这个问题上社区做了一点点优化消费者程序会向集群中当前负载最小的那台Broker发送请求。负载是如何评估的呢其实很简单就是看消费者连接的所有Broker中谁的待发送请求最少。当然了这种评估显然是消费者端的单向评估并非是站在全局角度因此有的时候也不一定是最优解。不过这不并影响我们的讨论。总之在这一步消费者会创建一个Socket连接。
2.连接协调者时。
Broker处理完上一步发送的FindCoordinator请求之后会返还对应的响应结果Response显式地告诉消费者哪个Broker是真正的协调者因此在这一步消费者知晓了真正的协调者后会创建连向该Broker的Socket连接。只有成功连入协调者协调者才能开启正常的组协调操作比如加入组、等待组分配方案、心跳请求处理、位移获取、位移提交等。
3.消费数据时。
消费者会为每个要消费的分区创建与该分区领导者副本所在Broker连接的TCP。举个例子假设消费者要消费5个分区的数据这5个分区各自的领导者副本分布在4台Broker上那么该消费者在消费时会创建与这4台Broker的Socket连接。
创建多少个TCP连接
下面我们来说说消费者创建TCP连接的数量。你可以先思考一下大致需要的连接数量然后我们结合具体的Kafka日志来验证下结果是否和你想的一致。
我们来看看这段日志。
[2019-05-27 10:00:54,142] DEBUG [Consumer clientId=consumer-1, groupId=test] Initiating connection to node localhost:9092 (id: -1 rack: null) using address localhost/127.0.0.1 (org.apache.kafka.clients.NetworkClient:944)
……
[2019-05-27 10:00:54,188] DEBUG [Consumer clientId=consumer-1, groupId=test] Sending metadata request MetadataRequestData(topics=[MetadataRequestTopic(name=t4)], allowAutoTopicCreation=true, includeClusterAuthorizedOperations=false, includeTopicAuthorizedOperations=false) to node localhost:9092 (id: -1 rack: null) (org.apache.kafka.clients.NetworkClient:1097)
……
_[2019-05-27 10:00:54,188] TRACE [Consumer clientId=consumer-1, groupId=test] Sending FIND_COORDINATOR {key=test,keytype=0} with correlation id 0 to node -1 (org.apache.kafka.clients.NetworkClient:496)
_[2019-05-27 10:00:54,203] TRACE [Consumer clientId=consumer-1, groupId=test] Completed receive from node -1 for FIND_COORDINATOR with correlation id 0, received {throttle_time_ms=0,error_code=0,error_message=null, nodeid=2,host=localhost,port=9094} (org.apache.kafka.clients.NetworkClient:837)
……
[2019-05-27 10:00:54,204] DEBUG [Consumer clientId=consumer-1, groupId=test] Initiating connection to node localhost:9094 (id: 2147483645 rack: null) using address localhost/127.0.0.1 (org.apache.kafka.clients.NetworkClient:944)
……
[2019-05-27 10:00:54,237] DEBUG [Consumer clientId=consumer-1, groupId=test] Initiating connection to node localhost:9094 (id: 2 rack: null) using address localhost/127.0.0.1 (org.apache.kafka.clients.NetworkClient:944)
[2019-05-27 10:00:54,237] DEBUG [Consumer clientId=consumer-1, groupId=test] Initiating connection to node localhost:9092 (id: 0 rack: null) using address localhost/127.0.0.1 (org.apache.kafka.clients.NetworkClient:944)
[2019-05-27 10:00:54,238] DEBUG [Consumer clientId=consumer-1, groupId=test] Initiating connection to node localhost:9093 (id: 1 rack: null) using address localhost/127.0.0.1 (org.apache.kafka.clients.NetworkClient:944)
这里我稍微解释一下日志的第一行是消费者程序创建的第一个TCP连接就像我们前面说的这个Socket用于发送FindCoordinator请求。由于这是消费者程序创建的第一个连接此时消费者对于要连接的Kafka集群一无所知因此它连接的Broker节点的ID是-1表示消费者根本不知道要连接的Kafka Broker的任何信息。
值得注意的是日志的第二行消费者复用了刚才创建的那个Socket连接向Kafka集群发送元数据请求以获取整个集群的信息。
日志的第三行表明消费者程序开始发送FindCoordinator请求给第一步中连接的Broker即localhost:9092也就是nodeId等于-1的那个。在十几毫秒之后消费者程序成功地获悉协调者所在的Broker信息也就是第四行标为橙色的“node_id = 2”。
完成这些之后消费者就已经知道协调者Broker的连接信息了因此在日志的第五行发起了第二个Socket连接创建了连向localhost:9094的TCP。只有连接了协调者消费者进程才能正常地开启消费者组的各种功能以及后续的消息消费。
在日志的最后三行中消费者又分别创建了新的TCP连接主要用于实际的消息获取。还记得我刚才说的吗要消费的分区的领导者副本在哪台Broker上消费者就要创建连向哪台Broker的TCP。在我举的这个例子中localhost:9092localhost:9093和localhost:9094这3台Broker上都有要消费的分区因此消费者创建了3个TCP连接。
看完这段日志你应该会发现日志中的这些Broker节点的ID在不断变化。有时候是-1有时候是2147483645只有在最后的时候才回归正常值0、1和2。这又是怎么回事呢
前面我们说过了-1的来由即消费者程序其实也不光是消费者生产者也是这样的机制首次启动时对Kafka集群一无所知因此用-1来表示尚未获取到Broker数据。
那么2147483645是怎么来的呢它是由Integer.MAX_VALUE减去协调者所在Broker的真实ID计算得来的。看第四行标为橙色的内容我们可以知道协调者ID是2因此这个Socket连接的节点ID就是Integer.MAX_VALUE减去2即2147483647减去2也就是2147483645。这种节点ID的标记方式是Kafka社区特意为之的结果目的就是要让组协调请求和真正的数据获取请求使用不同的Socket连接。
至于后面的0、1、2那就很好解释了。它们表征了真实的Broker ID也就是我们在server.properties中配置的broker.id值。
我们来简单总结一下上面的内容。通常来说消费者程序会创建3类TCP连接
确定协调者和获取集群元数据。
连接协调者,令其执行组成员管理操作。
执行实际的消息获取。
那么这三类TCP请求的生命周期都是相同的吗换句话说这些TCP连接是何时被关闭的呢
何时关闭TCP连接
和生产者类似消费者关闭Socket也分为主动关闭和Kafka自动关闭。主动关闭是指你显式地调用消费者API的方法去关闭消费者具体方式就是手动调用KafkaConsumer.close()方法或者是执行Kill命令不论是Kill -2还是Kill -9而Kafka自动关闭是由消费者端参数connection.max.idle.ms控制的该参数现在的默认值是9分钟即如果某个Socket连接上连续9分钟都没有任何请求“过境”的话那么消费者会强行“杀掉”这个Socket连接。
不过和生产者有些不同的是如果在编写消费者程序时你使用了循环的方式来调用poll方法消费消息那么上面提到的所有请求都会被定期发送到Broker因此这些Socket连接上总是能保证有请求在发送从而也就实现了“长连接”的效果。
针对上面提到的三类TCP连接你需要注意的是当第三类TCP连接成功创建后消费者程序就会废弃第一类TCP连接之后在定期请求元数据时它会改为使用第三类TCP连接。也就是说最终你会发现第一类TCP连接会在后台被默默地关闭掉。对一个运行了一段时间的消费者程序来说只会有后面两类TCP连接存在。
可能的问题
从理论上说Kafka Java消费者管理TCP资源的机制我已经说清楚了但如果仔细推敲这里面的设计原理还是会发现一些问题。
我们刚刚讲过第一类TCP连接仅仅是为了首次获取元数据而创建的后面就会被废弃掉。最根本的原因是消费者在启动时还不知道Kafka集群的信息只能使用一个“假”的ID去注册即使消费者获取了真实的Broker ID它依旧无法区分这个“假”ID对应的是哪台Broker因此也就无法重用这个Socket连接只能再重新创建一个新的连接。
为什么会出现这种情况呢主要是因为目前Kafka仅仅使用ID这一个维度的数据来表征Socket连接信息。这点信息明显不足以确定连接的是哪台Broker也许在未来社区应该考虑使用<主机名端口ID>三元组的方式来定位Socket资源这样或许能够让消费者程序少创建一些TCP连接。
也许你会问反正Kafka有定时关闭机制这算多大点事呢其实在实际场景中我见过很多将connection.max.idle.ms设置成-1即禁用定时关闭的案例如果是这样的话这些TCP连接将不会被定期清除只会成为永久的“僵尸”连接。基于这个原因社区应该考虑更好的解决方案。
小结
好了今天我们补齐了Kafka Java客户端管理TCP连接的“拼图”。我们不仅详细描述了Java消费者是怎么创建和关闭TCP连接的还对目前的设计方案提出了一些自己的思考。希望今后你能将这些知识应用到自己的业务场景中并对实际生产环境中的Socket管理做到心中有数。
开放讨论
假设有个Kafka集群由2台Broker组成有个主题有5个分区当一个消费该主题的消费者程序启动时你认为该程序会创建多少个Socket连接为什么
欢迎写下你的思考和答案,我们一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@ -0,0 +1,146 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
22 消费者组消费进度监控都怎么实现?
你好,我是胡夕。今天我要跟你分享的主题是:消费者组消费进度监控如何实现。
对于Kafka消费者来说最重要的事情就是监控它们的消费进度了或者说是监控它们消费的滞后程度。这个滞后程度有个专门的名称消费者Lag或Consumer Lag。
所谓滞后程度就是指消费者当前落后于生产者的程度。比方说Kafka生产者向某主题成功生产了100万条消息你的消费者当前消费了80万条消息那么我们就说你的消费者滞后了20万条消息即Lag等于20万。
通常来说Lag的单位是消息数而且我们一般是在主题这个级别上讨论Lag的但实际上Kafka监控Lag的层级是在分区上的。如果要计算主题级别的你需要手动汇总所有主题分区的Lag将它们累加起来合并成最终的Lag值。
我们刚刚说过对消费者而言Lag应该算是最最重要的监控指标了。它直接反映了一个消费者的运行情况。一个正常工作的消费者它的Lag值应该很小甚至是接近于0的这表示该消费者能够及时地消费生产者生产出来的消息滞后程度很小。反之如果一个消费者Lag值很大通常就表明它无法跟上生产者的速度最终Lag会越来越大从而拖慢下游消息的处理速度。
更可怕的是由于消费者的速度无法匹及生产者的速度极有可能导致它消费的数据已经不在操作系统的页缓存中了。这样的话消费者就不得不从磁盘上读取它们这就进一步拉大了与生产者的差距进而出现马太效应即那些Lag原本就很大的消费者会越来越慢Lag也会越来越大。
鉴于这些原因你在实际业务场景中必须时刻关注消费者的消费进度。一旦出现Lag逐步增加的趋势一定要定位问题及时处理避免造成业务损失。
既然消费进度这么重要我们应该怎么监控它呢简单来说有3种方法。
使用Kafka自带的命令行工具kafka-consumer-groups脚本。
使用Kafka Java Consumer API编程。
使用Kafka自带的JMX监控指标。
接下来我们分别来讨论下这3种方法。
Kafka自带命令
我们先来了解下第一种方法使用Kafka自带的命令行工具bin/kafka-consumer-groups.sh(bat)。kafka-consumer-groups脚本是Kafka为我们提供的最直接的监控消费者消费进度的工具。当然除了监控Lag之外它还有其他的功能。今天我们主要讨论如何使用它来监控Lag。
如果只看名字你可能会以为它只是操作和管理消费者组的。实际上它也能够监控独立消费者Standalone Consumer的Lag。我们之前说过独立消费者就是没有使用消费者组机制的消费者程序。和消费者组相同的是它们也要配置group.id参数值但和消费者组调用KafkaConsumer.subscribe()不同的是独立消费者调用KafkaConsumer.assign()方法直接消费指定分区。今天的重点不是要学习独立消费者,你只需要了解接下来我们讨论的所有内容都适用于独立消费者就够了。
使用kafka-consumer-groups脚本很简单。该脚本位于Kafka安装目录的bin子目录下我们可以通过下面的命令来查看某个给定消费者的Lag值
$ bin/kafka-consumer-groups.sh --bootstrap-server <Kafka broker连接信息> --describe --group <group名称>
Kafka连接信息就是<主机名端口>而group名称就是你的消费者程序中设置的group.id值。我举个实际的例子来说明具体的用法请看下面这张图的输出
在运行命令时我指定了Kafka集群的连接信息即localhost:9092。另外我还设置了要查询的消费者组名testgroup。kafka-consumer-groups脚本的输出信息很丰富。首先它会按照消费者组订阅主题的分区进行展示每个分区一行数据其次除了主题、分区等信息外它会汇报每个分区当前最新生产的消息的位移值即LOG-END-OFFSET列值、该消费者组当前最新消费消息的位移值即CURRENT-OFFSET值、LAG值前两者的差值、消费者实例ID、消费者连接Broker的主机名以及消费者的CLIENT-ID信息。
毫无疑问在这些数据中我们最关心的当属LAG列的值了图中每个分区的LAG值大约都是60多万这表明在我的这个测试中消费者组远远落后于生产者的进度。理想情况下我们希望该列所有值都是0因为这才表明我的消费者完全没有任何滞后。
有的时候,你运行这个脚本可能会出现下面这种情况,如下图所示:
简单比较一下我们很容易发现它和前面那张图输出的区别即CONSUMER-ID、HOST和CLIENT-ID列没有值如果碰到这种情况你不用惊慌这是因为我们运行kafka-consumer-groups脚本时没有启动消费者程序。请注意我标为橙色的文字它显式地告诉我们当前消费者组没有任何active成员即没有启动任何消费者实例。虽然这些列没有值但LAG列依然是有效的它依然能够正确地计算出此消费者组的Lag值。
除了上面这三列没有值的情形还可能出现的一种情况是该命令压根不返回任何结果。此时你也不用惊慌这是因为你使用的Kafka版本比较老kafka-consumer-groups脚本还不支持查询非active消费者组。一旦碰到这个问题你可以选择升级你的Kafka版本也可以采用我接下来说的其他方法来查询。
Kafka Java Consumer API
很多时候你可能对运行命令行工具查询Lag这种方式并不满意而是希望用程序的方式自动化监控。幸运的是社区的确为我们提供了这样的方法。这就是我们今天要讲的第二种方法。
简单来说社区提供的Java Consumer API分别提供了查询当前分区最新消息位移和消费者组最新消费消息位移两组方法我们使用它们就能计算出对应的Lag。
下面这段代码展示了如何利用Consumer端API监控给定消费者组的Lag值
public static Map<TopicPartition, Long> lagOf(String groupID, String bootstrapServers) throws TimeoutException {
Properties props = new Properties();
props.put(CommonClientConfigs.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
try (AdminClient client = AdminClient.create(props)) {
ListConsumerGroupOffsetsResult result = client.listConsumerGroupOffsets(groupID);
try {
Map<TopicPartition, OffsetAndMetadata> consumedOffsets = result.partitionsToOffsetAndMetadata().get(10, TimeUnit.SECONDS);
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false); // 禁止自动提交位移
props.put(ConsumerConfig.GROUP_ID_CONFIG, groupID);
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
try (final KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props)) {
Map<TopicPartition, Long> endOffsets = consumer.endOffsets(consumedOffsets.keySet());
return endOffsets.entrySet().stream().collect(Collectors.toMap(entry -> entry.getKey(),
entry -> entry.getValue() - consumedOffsets.get(entry.getKey()).offset()));
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
// 处理中断异常
// ...
return Collections.emptyMap();
} catch (ExecutionException e) {
// 处理ExecutionException
// ...
return Collections.emptyMap();
} catch (TimeoutException e) {
throw new TimeoutException("Timed out when getting lag for consumer group " + groupID);
}
}
}
你不用完全了解上面这段代码每一行的具体含义只需要记住3处地方即可第1处是调用AdminClient.listConsumerGroupOffsets方法获取给定消费者组的最新消费消息的位移第2处则是获取订阅分区的最新消息位移最后1处就是执行相应的减法操作获取Lag值并封装进一个Map对象。
我把这段代码送给你你可以将lagOf方法直接应用于你的生产环境以实现程序化监控消费者Lag的目的。不过请注意这段代码只适用于Kafka 2.0.0及以上的版本2.0.0之前的版本中没有AdminClient.listConsumerGroupOffsets方法。
Kafka JMX监控指标
上面这两种方式都可以很方便地查询到给定消费者组的Lag信息。但在很多实际监控场景中我们借助的往往是现成的监控框架。如果是这种情况以上这两种办法就不怎么管用了因为它们都不能集成进已有的监控框架中如Zabbix或Grafana。下面我们就来看第三种方法使用Kafka默认提供的JMX监控指标来监控消费者的Lag值。
当前Kafka消费者提供了一个名为kafka.consumer:type=consumer-fetch-manager-metrics,client-id=“{client-id}”的JMX指标里面有很多属性。和我们今天所讲内容相关的有两组属性records-lag-max和records-lead-min它们分别表示此消费者在测试窗口时间内曾经达到的最大的Lag值和最小的Lead值。
Lag值的含义我们已经反复讲过了我就不再重复了。这里的Lead值是指消费者最新消费消息的位移与分区当前第一条消息位移的差值。很显然Lag和Lead是一体的两个方面Lag越大的话Lead就越小反之也是同理。
你可能会问为什么要引入Lead呢我只监控Lag不就行了吗这里提Lead的原因就在于这部分功能是我实现的。开个玩笑其实社区引入Lead的原因是只看Lag的话我们也许不能及时意识到可能出现的严重问题。
试想一下监控到Lag越来越大可能只会给你一个感受那就是消费者程序变得越来越慢了至少是追不上生产者程序了除此之外你可能什么都不会做。毕竟有时候这也是能够接受的。但反过来一旦你监测到Lead越来越小甚至是快接近于0了你就一定要小心了这可能预示着消费者端要丢消息了。
为什么我们知道Kafka的消息是有留存时间设置的默认是1周也就是说Kafka默认删除1周前的数据。倘若你的消费者程序足够慢慢到它要消费的数据快被Kafka删除了这时你就必须立即处理否则一定会出现消息被删除从而导致消费者程序重新调整位移值的情形。这可能产生两个后果一个是消费者从头消费一遍数据另一个是消费者从最新的消息位移处开始消费之前没来得及消费的消息全部被跳过了从而造成丢消息的假象。
这两种情形都是不可忍受的因此必须有一个JMX指标清晰地表征这种情形这就是引入Lead指标的原因。所以Lag值从100万增加到200万这件事情远不如Lead值从200减少到100这件事来得重要。在实际生产环境中请你一定要同时监控Lag值和Lead值。当然了这个lead JMX指标的确也是我开发的这一点倒是事实。
接下来我给出一张使用JConsole工具监控此JMX指标的截图。从这张图片中我们可以看到client-id为consumer-1的消费者在给定的测量周期内最大的Lag值为714202最小的Lead值是83这说明此消费者有很大的消费滞后性。
Kafka消费者还在分区级别提供了额外的JMX指标用于单独监控分区级别的Lag和Lead值。JMX名称为kafka.consumer:type=consumer-fetch-manager-metrics,partition=“{partition}”,topic=“{topic}”,client-id=“{client-id}”。
在我们的例子中client-id还是consumer-1主题和分区分别是test和0。下图展示出了分区级别的JMX指标
分区级别的JMX指标中多了records-lag-avg和records-lead-avg两个属性可以计算平均的Lag值和Lead值。在实际场景中我们会更多地使用这两个JMX指标。
小结
我今天完整地介绍了监控消费者组以及独立消费者程序消费进度的3种方法。从使用便捷性上看应该说方法1是最简单的我们直接运行Kafka自带的命令行工具即可。方法2使用Consumer API组合计算Lag也是一种有效的方法重要的是它能集成进很多企业级的自动化监控工具中。不过集成性最好的还是方法3直接将JMX监控指标配置到主流的监控框架就可以了。
在真实的线上环境中我建议你优先考虑方法3同时将方法1和方法2作为备选装进你自己的工具箱中随时取出来应对各种实际场景。
开放讨论
请说说你对这三种方法的看法。另外,在真实的业务场景中,你是怎么监控消费者进度的呢?
欢迎写下你的思考和答案,我们一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@ -0,0 +1,117 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
23 Kafka副本机制详解
你好我是胡夕。今天我要和你分享的主题是Apache Kafka的副本机制。
所谓的副本机制Replication也可以称之为备份机制通常是指分布式系统在多台网络互联的机器上保存有相同的数据拷贝。副本机制有什么好处呢
提供数据冗余。即使系统部分组件失效,系统依然能够继续运转,因而增加了整体可用性以及数据持久性。
提供高伸缩性。支持横向扩展,能够通过增加机器的方式来提升读性能,进而提高读操作吞吐量。
改善数据局部性。允许将数据放入与用户地理位置相近的地方,从而降低系统延时。
这些优点都是在分布式系统教科书中最常被提及的但是有些遗憾的是对于Apache Kafka而言目前只能享受到副本机制带来的第1个好处也就是提供数据冗余实现高可用性和高持久性。我会在这一讲后面的内容中详细解释Kafka没能提供第2点和第3点好处的原因。
不过即便如此副本机制依然是Kafka设计架构的核心所在它也是Kafka确保系统高可用和消息高持久性的重要基石。
副本定义
在讨论具体的副本机制之前,我们先花一点时间明确一下副本的含义。
我们之前谈到过Kafka是有主题概念的而每个主题又进一步划分成若干个分区。副本的概念实际上是在分区层级下定义的每个分区配置有若干个副本。
所谓副本Replica本质就是一个只能追加写消息的提交日志。根据Kafka副本机制的定义同一个分区下的所有副本保存有相同的消息序列这些副本分散保存在不同的Broker上从而能够对抗部分Broker宕机带来的数据不可用。
在实际生产环境中每台Broker都可能保存有各个主题下不同分区的不同副本因此单个Broker上存有成百上千个副本的现象是非常正常的。
接下来我们来看一张图它展示的是一个有3台Broker的Kafka集群上的副本分布情况。从这张图中我们可以看到主题1分区0的3个副本分散在3台Broker上其他主题分区的副本也都散落在不同的Broker上从而实现数据冗余。
副本角色
既然分区下能够配置多个副本而且这些副本的内容还要一致那么很自然的一个问题就是我们该如何确保副本中所有的数据都是一致的呢特别是对Kafka而言当生产者发送消息到某个主题后消息是如何同步到对应的所有副本中的呢针对这个问题最常见的解决方案就是采用基于领导者Leader-based的副本机制。Apache Kafka就是这样的设计。
基于领导者的副本机制的工作原理如下图所示,我来简单解释一下这张图里面的内容。
第一在Kafka中副本分成两类领导者副本Leader Replica和追随者副本Follower Replica。每个分区在创建时都要选举一个副本称为领导者副本其余的副本自动称为追随者副本。
第二Kafka的副本机制比其他分布式系统要更严格一些。在Kafka中追随者副本是不对外提供服务的。这就是说任何一个追随者副本都不能响应消费者和生产者的读写请求。所有的请求都必须由领导者副本来处理或者说所有的读写请求都必须发往领导者副本所在的Broker由该Broker负责处理。追随者副本不处理客户端请求它唯一的任务就是从领导者副本异步拉取消息并写入到自己的提交日志中从而实现与领导者副本的同步。
第三当领导者副本挂掉了或者说领导者副本所在的Broker宕机时Kafka依托于ZooKeeper提供的监控功能能够实时感知到并立即开启新一轮的领导者选举从追随者副本中选一个作为新的领导者。老Leader副本重启回来后只能作为追随者副本加入到集群中。
你一定要特别注意上面的第二点即追随者副本是不对外提供服务的。还记得刚刚我们谈到副本机制的好处时说过Kafka没能提供读操作横向扩展以及改善局部性吗具体的原因就在于此。
对于客户端用户而言Kafka的追随者副本没有任何作用它既不能像MySQL那样帮助领导者副本“扛读”也不能实现将某些副本放到离客户端近的地方来改善数据局部性。
既然如此Kafka为什么要这样设计呢其实这种副本机制有两个方面的好处。
1.方便实现“Read-your-writes”。
所谓Read-your-writes顾名思义就是当你使用生产者API向Kafka成功写入消息后马上使用消费者API去读取刚才生产的消息。
举个例子比如你平时发微博时你发完一条微博肯定是希望能立即看到的这就是典型的Read-your-writes场景。如果允许追随者副本对外提供服务由于副本同步是异步的因此有可能出现追随者副本还没有从领导者副本那里拉取到最新的消息从而使得客户端看不到最新写入的消息。
2.方便实现单调读Monotonic Reads
什么是单调读呢?就是对于一个消费者用户而言,在多次消费消息时,它不会看到某条消息一会儿存在一会儿不存在。
如果允许追随者副本提供读服务那么假设当前有2个追随者副本F1和F2它们异步地拉取领导者副本数据。倘若F1拉取了Leader的最新消息而F2还未及时拉取那么此时如果有一个消费者先从F1读取消息之后又从F2拉取消息它可能会看到这样的现象第一次消费时看到的最新消息在第二次消费时不见了这就不是单调读一致性。但是如果所有的读请求都是由Leader来处理那么Kafka就很容易实现单调读一致性。
In-sync ReplicasISR
我们刚刚反复说过追随者副本不提供服务只是定期地异步拉取领导者副本中的数据而已。既然是异步的就存在着不可能与Leader实时同步的风险。在探讨如何正确应对这种风险之前我们必须要精确地知道同步的含义是什么。或者说Kafka要明确地告诉我们追随者副本到底在什么条件下才算与Leader同步。
基于这个想法Kafka引入了In-sync Replicas也就是所谓的ISR副本集合。ISR中的副本都是与Leader同步的副本相反不在ISR中的追随者副本就被认为是与Leader不同步的。那么到底什么副本能够进入到ISR中呢
我们首先要明确的是Leader副本天然就在ISR中。也就是说ISR不只是追随者副本集合它必然包括Leader副本。甚至在某些情况下ISR只有Leader这一个副本。
另外能够进入到ISR的追随者副本要满足一定的条件。至于是什么条件我先卖个关子我们先来一起看看下面这张图。
图中有3个副本1个领导者副本和2个追随者副本。Leader副本当前写入了10条消息Follower1副本同步了其中的6条消息而Follower2副本只同步了其中的3条消息。现在请你思考一下对于这2个追随者副本你觉得哪个追随者副本与Leader不同步
答案是要根据具体情况来定。换成英文就是那句著名的“It depends”。看上去好像Follower2的消息数比Leader少了很多它是最有可能与Leader不同步的。的确是这样的但仅仅是可能。
事实上这张图中的2个Follower副本都有可能与Leader不同步但也都有可能与Leader同步。也就是说Kafka判断Follower是否与Leader同步的标准不是看相差的消息数而是另有“玄机”。
这个标准就是Broker端参数replica.lag.time.max.ms参数值。这个参数的含义是Follower副本能够落后Leader副本的最长时间间隔当前默认值是10秒。这就是说只要一个Follower副本落后Leader副本的时间不连续超过10秒那么Kafka就认为该Follower副本与Leader是同步的即使此时Follower副本中保存的消息明显少于Leader副本中的消息。
我们在前面说过Follower副本唯一的工作就是不断地从Leader副本拉取消息然后写入到自己的提交日志中。如果这个同步过程的速度持续慢于Leader副本的消息写入速度那么在replica.lag.time.max.ms时间后此Follower副本就会被认为是与Leader副本不同步的因此不能再放入ISR中。此时Kafka会自动收缩ISR集合将该副本“踢出”ISR。
值得注意的是倘若该副本后面慢慢地追上了Leader的进度那么它是能够重新被加回ISR的。这也表明ISR是一个动态调整的集合而非静态不变的。
Unclean领导者选举Unclean Leader Election
既然ISR是可以动态调整的那么自然就可以出现这样的情形ISR为空。因为Leader副本天然就在ISR中如果ISR为空了就说明Leader副本也“挂掉”了Kafka需要重新选举一个新的Leader。可是ISR是空此时该怎么选举新Leader呢
Kafka把所有不在ISR中的存活副本都称为非同步副本。通常来说非同步副本落后Leader太多因此如果选择这些副本作为新Leader就可能出现数据的丢失。毕竟这些副本中保存的消息远远落后于老Leader中的消息。在Kafka中选举这种副本的过程称为Unclean领导者选举。Broker端参数unclean.leader.election.enable控制是否允许Unclean领导者选举。
开启Unclean领导者选举可能会造成数据丢失但好处是它使得分区Leader副本一直存在不至于停止对外提供服务因此提升了高可用性。反之禁止Unclean领导者选举的好处在于维护了数据的一致性避免了消息丢失但牺牲了高可用性。
如果你听说过CAP理论的话你一定知道一个分布式系统通常只能同时满足一致性Consistency、可用性Availability、分区容错性Partition tolerance中的两个。显然在这个问题上Kafka赋予你选择C或A的权利。
你可以根据你的实际业务场景决定是否开启Unclean领导者选举。不过我强烈建议你不要开启它毕竟我们还可以通过其他的方式来提升高可用性。如果为了这点儿高可用性的改善牺牲了数据一致性那就非常不值当了。
小结
今天我主要跟你分享了Apache Kafka的副本机制以及它们实现的原理。坦率地说我觉得有些地方可能讲浅了如果要百分之百地了解Replication你还是要熟读一下Kafka相应的源代码。不过你也不用担心在专栏后面的内容中我会专门从源码角度分析副本机制特别是Follower副本从Leader副本拉取消息的全过程。从技术深度上来说那一讲应该算是本专栏中最贴近技术内幕的分析了你一定不要错过。
开放讨论
到目前为止我反复强调了Follower副本不对外提供服务这件事情。有意思的是社区最近正在考虑是否要打破这个限制即允许Follower副本处理客户端消费者发来的请求。社区主要的考量是这能够用于改善云上数据的局部性更好地服务地理位置相近的客户。如果允许Follower副本对外提供读服务你觉得应该如何避免或缓解因Follower副本与Leader副本不同步而导致的数据不一致的情形
欢迎写下你的思考和答案,我们一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@ -0,0 +1,115 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
24 请求是怎么被处理的?
你好我是胡夕。今天我要和你分享的主题是Kafka请求是怎么被处理的。
无论是Kafka客户端还是Broker端它们之间的交互都是通过“请求/响应”的方式完成的。比如客户端会通过网络发送消息生产请求给Broker而Broker处理完成后会发送对应的响应给到客户端。
Apache Kafka自己定义了一组请求协议用于实现各种各样的交互操作。比如常见的PRODUCE请求是用于生产消息的FETCH请求是用于消费消息的METADATA请求是用于请求Kafka集群元数据信息的。
总之Kafka定义了很多类似的请求格式。我数了一下截止到目前最新的2.3版本Kafka共定义了多达45种请求格式。所有的请求都是通过TCP网络以Socket的方式进行通讯的。
今天我们就来详细讨论一下Kafka Broker端处理请求的全流程。
处理请求的2种常见方案
关于如何处理请求,我们很容易想到的方案有两个。
1.顺序处理请求。如果写成伪代码,大概是这个样子:
while (true) {
Request request = accept(connection);
handle(request);
}
这个方法实现简单,但是有个致命的缺陷,那就是吞吐量太差。由于只能顺序处理每个请求,因此,每个请求都必须等待前一个请求处理完毕才能得到处理。这种方式只适用于请求发送非常不频繁的系统。
2.每个请求使用单独线程处理。也就是说,我们为每个入站请求都创建一个新的线程来异步处理。我们一起来看看这个方案的伪代码。
while (true) {
Request = request = accept(connection);
Thread thread = new Thread(() -> {
handle(request);});
thread.start();
}
这个方法反其道而行之,完全采用异步的方式。系统会为每个入站请求都创建单独的线程来处理。这个方法的好处是,它是完全异步的,每个请求的处理都不会阻塞下一个请求。但缺陷也同样明显。为每个请求都创建线程的做法开销极大,在某些场景下甚至会压垮整个服务。还是那句话,这个方法只适用于请求发送频率很低的业务场景。
既然这两种方案都不好那么Kafka是如何处理请求的呢用一句话概括就是Kafka使用的是Reactor模式。
Kafka是如何处理请求的
谈到Reactor模式大神Doug Lea的“Scalable IO in Java”应该算是最好的入门教材了。即使你没听说过Doug Lea那你应该也用过ConcurrentHashMap吧这个类就是这位大神写的。其实整个java.util.concurrent包都是他的杰作
好了我们说回Reactor模式。简单来说Reactor模式是事件驱动架构的一种实现方式特别适合应用于处理多个客户端并发向服务器端发送请求的场景。我借用Doug Lea的一页PPT来说明一下Reactor的架构并借此引出Kafka的请求处理模型。
Reactor模式的架构如下图所示
从这张图中我们可以发现多个客户端会发送请求给到Reactor。Reactor有个请求分发线程Dispatcher也就是图中的Acceptor它会将不同的请求下发到多个工作线程中处理。
在这个架构中Acceptor线程只是用于请求分发不涉及具体的逻辑处理非常得轻量级因此有很高的吞吐量表现。而这些工作线程可以根据实际业务处理需要任意增减从而动态调节系统负载能力。
如果我们来为Kafka画一张类似的图的话那它应该是这个样子的
显然这两张图长得差不多。Kafka的Broker端有个SocketServer组件类似于Reactor模式中的Dispatcher它也有对应的Acceptor线程和一个工作线程池只不过在Kafka中这个工作线程池有个专属的名字叫网络线程池。Kafka提供了Broker端参数num.network.threads用于调整该网络线程池的线程数。其默认值是3表示每台Broker启动时会创建3个网络线程专门处理客户端发送的请求。
Acceptor线程采用轮询的方式将入站请求公平地发到所有网络线程中因此在实际使用过程中这些线程通常都有相同的几率被分配到待处理请求。这种轮询策略编写简单同时也避免了请求处理的倾斜有利于实现较为公平的请求处理调度。
好了你现在了解了客户端发来的请求会被Broker端的Acceptor线程分发到任意一个网络线程中由它们来进行处理。那么当网络线程接收到请求后它是怎么处理的呢你可能会认为它顺序处理不就好了吗实际上Kafka在这个环节又做了一层异步线程池的处理我们一起来看一看下面这张图。
当网络线程拿到请求后它不是自己处理而是将请求放入到一个共享请求队列中。Broker端还有个IO线程池负责从该队列中取出请求执行真正的处理。如果是PRODUCE生产请求则将消息写入到底层的磁盘日志中如果是FETCH请求则从磁盘或页缓存中读取消息。
IO线程池处中的线程才是执行请求逻辑的线程。Broker端参数num.io.threads控制了这个线程池中的线程数。目前该参数默认值是8表示每台Broker启动后自动创建8个IO线程处理请求。你可以根据实际硬件条件设置此线程池的个数。
比如如果你的机器上CPU资源非常充裕你完全可以调大该参数允许更多的并发请求被同时处理。当IO线程处理完请求后会将生成的响应发送到网络线程池的响应队列中然后由对应的网络线程负责将Response返还给客户端。
细心的你一定发现了请求队列和响应队列的差别请求队列是所有网络线程共享的而响应队列则是每个网络线程专属的。这么设计的原因就在于Dispatcher只是用于请求分发而不负责响应回传因此只能让每个网络线程自己发送Response给客户端所以这些Response也就没必要放在一个公共的地方。
我们再来看看刚刚的那张图图中有一个叫Purgatory的组件这是Kafka中著名的“炼狱”组件。它是用来缓存延时请求Delayed Request的。所谓延时请求就是那些一时未满足条件不能立刻处理的请求。比如设置了acks=all的PRODUCE请求一旦设置了acks=all那么该请求就必须等待ISR中所有副本都接收了消息后才能返回此时处理该请求的IO线程就必须等待其他Broker的写入结果。当请求不能立刻处理时它就会暂存在Purgatory中。稍后一旦满足了完成条件IO线程会继续处理该请求并将Response放入对应网络线程的响应队列中。
讲到这里Kafka请求流程解析的故事其实已经讲完了我相信你应该已经了解了Kafka Broker是如何从头到尾处理请求的。但是我们不会现在就收尾我要给今天的内容开个小灶再说点不一样的东西。
控制类请求和数据类请求分离
到目前为止我提及的请求处理流程对于所有请求都是适用的也就是说Kafka Broker对所有请求是一视同仁的。但是在Kafka内部除了客户端发送的PRODUCE请求和FETCH请求之外还有很多执行其他操作的请求类型比如负责更新Leader副本、Follower副本以及ISR集合的LeaderAndIsr请求负责勒令副本下线的StopReplica请求等。与PRODUCE和FETCH请求相比这些请求有个明显的不同它们不是数据类的请求而是控制类的请求。也就是说它们并不是操作消息数据的而是用来执行特定的Kafka内部动作的。
Kafka社区把PRODUCE和FETCH这类请求称为数据类请求把LeaderAndIsr、StopReplica这类请求称为控制类请求。细究起来当前这种一视同仁的处理方式对控制类请求是不合理的。为什么呢因为控制类请求有这样一种能力它可以直接令数据类请求失效
我来举个例子说明一下。假设我们有个主题只有1个分区该分区配置了两个副本其中Leader副本保存在Broker 0上Follower副本保存在Broker 1上。假设Broker 0这台机器积压了很多的PRODUCE请求此时你如果使用Kafka命令强制将该主题分区的Leader、Follower角色互换那么Kafka内部的控制器组件Controller会发送LeaderAndIsr请求给Broker 0显式地告诉它当前它不再是Leader而是Follower了而Broker 1上的Follower副本因为被选为新的Leader因此停止向Broker 0拉取消息。
这时一个尴尬的场面就出现了如果刚才积压的PRODUCE请求都设置了acks=all那么这些在LeaderAndIsr发送之前的请求就都无法正常完成了。就像前面说的它们会被暂存在Purgatory中不断重试直到最终请求超时返回给客户端。
设想一下如果Kafka能够优先处理LeaderAndIsr请求Broker 0就会立刻抛出NOT_LEADER_FOR_PARTITION异常快速地标识这些积压PRODUCE请求已失败这样客户端不用等到Purgatory中的请求超时就能立刻感知从而降低了请求的处理时间。即使acks不是all积压的PRODUCE请求能够成功写入Leader副本的日志但处理LeaderAndIsr之后Broker 0上的Leader变为了Follower副本也要执行显式的日志截断Log Truncation即原Leader副本成为Follower后会将之前写入但未提交的消息全部删除依然做了很多无用功。
再举一个例子同样是在积压大量数据类请求的Broker上当你删除主题的时候Kafka控制器我会在专栏后面的内容中专门介绍它向该Broker发送StopReplica请求。如果该请求不能及时处理主题删除操作会一直hang住从而增加了删除主题的延时。
基于这些问题社区于2.3版本正式实现了数据类请求和控制类请求的分离。其实在社区推出方案之前我自己尝试过修改这个设计。当时我的想法是在Broker中实现一个优先级队列并赋予控制类请求更高的优先级。这是很自然的想法所以我本以为社区也会这么实现的但后来我这个方案被清晰地记录在“已拒绝方案”列表中。
究其原因,这个方案最大的问题在于,它无法处理请求队列已满的情形。当请求队列已经无法容纳任何新的请求时,纵然有优先级之分,它也无法处理新的控制类请求了。
那么社区是如何解决的呢很简单你可以再看一遍今天的第三张图社区完全拷贝了这张图中的一套组件实现了两类请求的分离。也就是说Kafka Broker启动后会在后台分别创建两套网络线程池和IO线程池的组合它们分别处理数据类请求和控制类请求。至于所用的Socket端口自然是使用不同的端口了你需要提供不同的listeners配置显式地指定哪套端口用于处理哪类请求。
小结
讲到这里Kafka Broker请求处理流程的解析应该讲得比较完整了。明确请求处理过程的最大意义在于它是你日后执行Kafka性能优化的前提条件。如果你能从请求的维度去思考Kafka的工作原理你会发现优化Kafka并不是一件困难的事情。
开放讨论
坦白来讲,我对社区否定优先级队列方案是有一点不甘心的。如果是你的话,你觉得应该如何规避优先级队列方案中队列已满的问题呢?
欢迎写下你的思考和答案,我们一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@ -0,0 +1,131 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
25 消费者组重平衡全流程解析
你好,我是胡夕。今天我要和你分享的主题是:消费者组重平衡全流程解析。
之前我们聊到过消费者组的重平衡流程它的作用是让组内所有的消费者实例就消费哪些主题分区达成一致。重平衡需要借助Kafka Broker端的Coordinator组件在Coordinator的帮助下完成整个消费者组的分区重分配。今天我们就来详细说说这个流程。
先提示一下我会以Kafka 2.3版本的源代码开启今天的讲述。在分享的过程中,对于旧版本的设计差异,我也会显式地说明。这样,即使你依然在使用比较旧的版本也不打紧,毕竟设计原理大体上是没有变化的。
触发与通知
我们先来简单回顾一下重平衡的3个触发条件
组成员数量发生变化。
订阅主题数量发生变化。
订阅主题的分区数发生变化。
就我个人的经验来看在实际生产环境中因命中第1个条件而引发的重平衡是最常见的。另外消费者组中的消费者实例依次启动也属于第1种情况也就是说每次消费者组启动时必然会触发重平衡过程。
这部分内容我在专栏第15讲中已经详细介绍过了就不再赘述了。如果你不记得的话可以先去复习一下。
今天我真正想引出的是另一个话题重平衡过程是如何通知到其他消费者实例的答案就是靠消费者端的心跳线程Heartbeat Thread
Kafka Java消费者需要定期地发送心跳请求Heartbeat Request到Broker端的协调者以表明它还存活着。在Kafka 0.10.1.0版本之前发送心跳请求是在消费者主线程完成的也就是你写代码调用KafkaConsumer.poll方法的那个线程。
这样做有诸多弊病最大的问题在于消息处理逻辑也是在这个线程中完成的。因此一旦消息处理消耗了过长的时间心跳请求将无法及时发到协调者那里导致协调者“错误地”认为该消费者已“死”。自0.10.1.0版本开始,社区引入了一个单独的心跳线程来专门执行心跳请求发送,避免了这个问题。
但这和重平衡又有什么关系呢其实重平衡的通知机制正是通过心跳线程来完成的。当协调者决定开启新一轮重平衡后它会将“REBALANCE_IN_PROGRESS”封装进心跳请求的响应中发还给消费者实例。当消费者实例发现心跳响应中包含了“REBALANCE_IN_PROGRESS”就能立马知道重平衡又开始了这就是重平衡的通知机制。
对了很多人还搞不清楚消费者端参数heartbeat.interval.ms的真实用途我来解释一下。从字面上看它就是设置了心跳的间隔时间但这个参数的真正作用是控制重平衡通知的频率。如果你想要消费者实例更迅速地得到通知那么就可以给这个参数设置一个非常小的值这样消费者就能更快地感知到重平衡已经开启了。
消费者组状态机
重平衡一旦开启Broker端的协调者组件就要开始忙了主要涉及到控制消费者组的状态流转。当前Kafka设计了一套消费者组状态机State Machine来帮助协调者完成整个重平衡流程。严格来说这套状态机属于非常底层的设计Kafka官网上压根就没有提到过但你最好还是了解一下因为它能够帮助你搞懂消费者组的设计原理比如消费者组的过期位移Expired Offsets删除等。
目前Kafka为消费者组定义了5种状态它们分别是Empty、Dead、PreparingRebalance、CompletingRebalance和Stable。那么这5种状态的含义是什么呢我们一起来看看下面这张表格。
了解了这些状态的含义之后,我们来看一张图片,它展示了状态机的各个状态流转。
我来解释一下消费者组启动时的状态流转过程。一个消费者组最开始是Empty状态当重平衡过程开启后它会被置于PreparingRebalance状态等待成员加入之后变更到CompletingRebalance状态等待分配方案最后流转到Stable状态完成重平衡。
当有新成员加入或已有成员退出时消费者组的状态从Stable直接跳到PreparingRebalance状态此时所有现存成员就必须重新申请加入组。当所有成员都退出组后消费者组状态变更为Empty。Kafka定期自动删除过期位移的条件就是组要处于Empty状态。因此如果你的消费者组停掉了很长时间超过7天那么Kafka很可能就把该组的位移数据删除了。我相信你在Kafka的日志中一定经常看到下面这个输出
Removed ✘✘✘ expired offsets in ✘✘✘ milliseconds.
这就是Kafka在尝试定期删除过期位移。现在你知道了只有Empty状态下的组才会执行过期位移删除的操作。
消费者端重平衡流程
有了上面的内容作铺垫,我们就可以开始介绍重平衡流程了。重平衡的完整流程需要消费者端和协调者组件共同参与才能完成。我们先从消费者的视角来审视一下重平衡的流程。
在消费者端重平衡分为两个步骤分别是加入组和等待领导者消费者Leader Consumer分配方案。这两个步骤分别对应两类特定的请求JoinGroup请求和SyncGroup请求。
当组内成员加入组时它会向协调者发送JoinGroup请求。在该请求中每个成员都要将自己订阅的主题上报这样协调者就能收集到所有成员的订阅信息。一旦收集了全部成员的JoinGroup请求后协调者会从这些成员中选择一个担任这个消费者组的领导者。
通常情况下第一个发送JoinGroup请求的成员自动成为领导者。你一定要注意区分这里的领导者和之前我们介绍的领导者副本它们不是一个概念。这里的领导者是具体的消费者实例它既不是副本也不是协调者。领导者消费者的任务是收集所有成员的订阅信息然后根据这些信息制定具体的分区消费分配方案。
选出领导者之后协调者会把消费者组订阅信息封装进JoinGroup请求的响应体中然后发给领导者由领导者统一做出分配方案后进入到下一步发送SyncGroup请求。
在这一步中领导者向协调者发送SyncGroup请求将刚刚做出的分配方案发给协调者。值得注意的是其他成员也会向协调者发送SyncGroup请求只不过请求体中并没有实际的内容。这一步的主要目的是让协调者接收分配方案然后统一以SyncGroup响应的方式分发给所有成员这样组内所有成员就都知道自己该消费哪些分区了。
接下来我用一张图来形象地说明一下JoinGroup请求的处理过程。
就像前面说的JoinGroup请求的主要作用是将组成员订阅信息发送给领导者消费者待领导者制定好分配方案后重平衡流程进入到SyncGroup请求阶段。
下面这张图描述的是SyncGroup请求的处理流程。
SyncGroup请求的主要目的就是让协调者把领导者制定的分配方案下发给各个组内成员。当所有成员都成功接收到分配方案后消费者组进入到Stable状态即开始正常的消费工作。
讲完这里,消费者端的重平衡流程我已经介绍完了。接下来,我们从协调者端来看一下重平衡是怎么执行的。
Broker端重平衡场景剖析
要剖析协调者端处理重平衡的全流程,我们必须要分几个场景来讨论。这几个场景分别是新成员加入组、组成员主动离组、组成员崩溃离组、组成员提交位移。接下来,我们一个一个来讨论。
场景一:新成员入组。
新成员入组是指组处于Stable状态后有新成员加入。如果是全新启动一个消费者组Kafka是有一些自己的小优化的流程上会有些许的不同。我们这里讨论的是组稳定了之后有新成员加入的情形。
当协调者收到新的JoinGroup请求后它会通过心跳请求响应的方式通知组内现有的所有成员强制它们开启新一轮的重平衡。具体的过程和之前的客户端重平衡流程是一样的。现在我用一张时序图来说明协调者一端是如何处理新成员入组的。
场景二:组成员主动离组。
何谓主动离组就是指消费者实例所在线程或进程调用close()方法主动通知协调者它要退出。这个场景就涉及到了第三类请求LeaveGroup请求。协调者收到LeaveGroup请求后依然会以心跳响应的方式通知其他成员因此我就不再赘述了还是直接用一张图来说明。
场景三:组成员崩溃离组。
崩溃离组是指消费者实例出现严重故障突然宕机导致的离组。它和主动离组是有区别的因为后者是主动发起的离组协调者能马上感知并处理。但崩溃离组是被动的协调者通常需要等待一段时间才能感知到这段时间一般是由消费者端参数session.timeout.ms控制的。也就是说Kafka一般不会超过session.timeout.ms就能感知到这个崩溃。当然后面处理崩溃离组的流程与之前是一样的我们来看看下面这张图。
场景四:重平衡时协调者对组内成员提交位移的处理。
正常情况下每个组内成员都会定期汇报位移给协调者。当重平衡开启时协调者会给予成员一段缓冲时间要求每个成员必须在这段时间内快速地上报自己的位移信息然后再开启正常的JoinGroup/SyncGroup请求发送。还是老办法我们使用一张图来说明。
小结
好了消费者重平衡流程我已经全部讲完了。虽然全程我都是拿两个成员来举例子但你可以很容易地扩展到多个成员的消费者组毕竟它们的原理是相同的。我希望你能多看几遍今天的内容彻底掌握Kafka的消费者重平衡流程。社区正在对目前的重平衡流程做较大程度的改动如果你不了解这些基础的设计原理后面想深入学习这部分内容的话会十分困难。
开放讨论
在整个重平衡过程中组内所有消费者实例都会暂停消费用JVM GC的术语来说就是重平衡过程是一个stop the world操作。请思考一下针对这个问题我们该如何改进这个过程我们是否能允许部分消费者在重平衡过程中继续消费以提升消费者端的可用性以及吞吐量
欢迎写下你的思考和答案,我们一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@ -0,0 +1,125 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
26 你一定不能错过的Kafka控制器
你好我是胡夕。今天我要和你分享的主题是Kafka中的控制器组件。
控制器组件Controller是Apache Kafka的核心组件。它的主要作用是在Apache ZooKeeper的帮助下管理和协调整个Kafka集群。集群中任意一台Broker都能充当控制器的角色但是在运行过程中只能有一个Broker成为控制器行使其管理和协调的职责。换句话说每个正常运转的Kafka集群在任意时刻都有且只有一个控制器。官网上有个名为activeController的JMX指标可以帮助我们实时监控控制器的存活状态。这个JMX指标非常关键你在实际运维操作过程中一定要实时查看这个指标的值。下面我们就来详细说说控制器的原理和内部运行机制。
在开始之前我先简单介绍一下Apache ZooKeeper框架。要知道控制器是重度依赖ZooKeeper的因此我们有必要花一些时间学习下ZooKeeper是做什么的。
Apache ZooKeeper是一个提供高可靠性的分布式协调服务框架。它使用的数据模型类似于文件系统的树形结构根目录也是以“/”开始。该结构上的每个节点被称为znode用来保存一些元数据协调信息。
如果以znode持久性来划分znode可分为持久性znode和临时znode。持久性znode不会因为ZooKeeper集群重启而消失而临时znode则与创建该znode的ZooKeeper会话绑定一旦会话结束该节点会被自动删除。
ZooKeeper赋予客户端监控znode变更的能力即所谓的Watch通知功能。一旦znode节点被创建、删除子节点数量发生变化抑或是znode所存的数据本身变更ZooKeeper会通过节点变更监听器(ChangeHandler)的方式显式通知客户端。
依托于这些功能ZooKeeper常被用来实现集群成员管理、分布式锁、领导者选举等功能。Kafka控制器大量使用Watch功能实现对集群的协调管理。我们一起来看一张图片它展示的是Kafka在ZooKeeper中创建的znode分布。你不用了解每个znode的作用但你可以大致体会下Kafka对ZooKeeper的依赖。
掌握了ZooKeeper的这些基本知识现在我们就可以开启对Kafka控制器的讨论了。
控制器是如何被选出来的?
你一定很想知道控制器是如何被选出来的呢我们刚刚在前面说过每台Broker都能充当控制器那么当集群启动后Kafka怎么确认控制器位于哪台Broker呢
实际上Broker在启动时会尝试去ZooKeeper中创建/controller节点。Kafka当前选举控制器的规则是第一个成功创建/controller节点的Broker会被指定为控制器。
控制器是做什么的?
我们经常说控制器是起协调作用的组件那么这里的协调作用到底是指什么呢我想了一下控制器的职责大致可以分为5种我们一起来看看。
1.主题管理(创建、删除、增加分区)
这里的主题管理就是指控制器帮助我们完成对Kafka主题的创建、删除以及分区增加的操作。换句话说当我们执行kafka-topics脚本时大部分的后台工作都是控制器来完成的。关于kafka-topics脚本我会在专栏后面的内容中详细介绍它的使用方法。
2.分区重分配
分区重分配主要是指kafka-reassign-partitions脚本关于这个脚本后面我也会介绍提供的对已有主题分区进行细粒度的分配功能。这部分功能也是控制器实现的。
3.Preferred领导者选举
Preferred领导者选举主要是Kafka为了避免部分Broker负载过重而提供的一种换Leader的方案。在专栏后面说到工具的时候我们再详谈Preferred领导者选举这里你只需要了解这也是控制器的职责范围就可以了。
4.集群成员管理新增Broker、Broker主动关闭、Broker宕机
这是控制器提供的第4类功能包括自动检测新增Broker、Broker主动关闭及被动宕机。这种自动检测是依赖于前面提到的Watch功能和ZooKeeper临时节点组合实现的。
比如控制器组件会利用Watch机制检查ZooKeeper的/brokers/ids节点下的子节点数量变更。目前当有新Broker启动后它会在/brokers下创建专属的znode节点。一旦创建完毕ZooKeeper会通过Watch机制将消息通知推送给控制器这样控制器就能自动地感知到这个变化进而开启后续的新增Broker作业。
侦测Broker存活性则是依赖于刚刚提到的另一个机制临时节点。每个Broker启动后会在/brokers/ids下创建一个临时znode。当Broker宕机或主动关闭后该Broker与ZooKeeper的会话结束这个znode会被自动删除。同理ZooKeeper的Watch机制将这一变更推送给控制器这样控制器就能知道有Broker关闭或宕机了从而进行“善后”。
5.数据服务
控制器的最后一大类工作就是向其他Broker提供数据服务。控制器上保存了最全的集群元数据信息其他所有Broker会定期接收控制器发来的元数据更新请求从而更新其内存中的缓存数据。
控制器保存了什么数据?
接下来,我们就详细看看,控制器中到底保存了哪些数据。我用一张图来说明一下。
怎么样图中展示的数据量是不是很多几乎把我们能想到的所有Kafka集群的数据都囊括进来了。这里面比较重要的数据有
所有主题信息。包括具体的分区信息比如领导者副本是谁ISR集合中有哪些副本等。
所有Broker信息。包括当前都有哪些运行中的Broker哪些正在关闭中的Broker等。
所有涉及运维任务的分区。包括当前正在进行Preferred领导者选举以及分区重分配的分区列表。
值得注意的是这些数据其实在ZooKeeper中也保存了一份。每当控制器初始化时它都会从ZooKeeper上读取对应的元数据并填充到自己的缓存中。有了这些数据控制器就能对外提供数据服务了。这里的对外主要是指对其他Broker而言控制器通过向这些Broker发送请求的方式将这些数据同步到其他Broker上。
控制器故障转移Failover
我们在前面强调过在Kafka集群运行过程中只能有一台Broker充当控制器的角色那么这就存在单点失效Single Point of Failure的风险Kafka是如何应对单点失效的呢答案就是为控制器提供故障转移功能也就是说所谓的Failover。
故障转移指的是当运行中的控制器突然宕机或意外终止时Kafka能够快速地感知到并立即启用备用控制器来代替之前失败的控制器。这个过程就被称为Failover该过程是自动完成的无需你手动干预。
接下来,我们一起来看一张图,它简单地展示了控制器故障转移的过程。
最开始时Broker 0是控制器。当Broker 0宕机后ZooKeeper通过Watch机制感知到并删除了/controller临时节点。之后所有存活的Broker开始竞选新的控制器身份。Broker 3最终赢得了选举成功地在ZooKeeper上重建了/controller节点。之后Broker 3会从ZooKeeper中读取集群元数据信息并初始化到自己的缓存中。至此控制器的Failover完成可以行使正常的工作职责了。
控制器内部设计原理
在Kafka 0.11版本之前控制器的设计是相当繁琐的代码更是有些混乱这就导致社区中很多控制器方面的Bug都无法修复。控制器是多线程的设计会在内部创建很多个线程。比如控制器需要为每个Broker都创建一个对应的Socket连接然后再创建一个专属的线程用于向这些Broker发送特定请求。如果集群中的Broker数量很多那么控制器端需要创建的线程就会很多。另外控制器连接ZooKeeper的会话也会创建单独的线程来处理Watch机制的通知回调。除了以上这些线程控制器还会为主题删除创建额外的I/O线程。
比起多线程的设计更糟糕的是这些线程还会访问共享的控制器缓存数据。我们都知道多线程访问共享可变数据是维持线程安全最大的难题。为了保护数据安全性控制器不得不在代码中大量使用ReentrantLock同步机制这就进一步拖慢了整个控制器的处理速度。
鉴于这些原因社区于0.11版本重构了控制器的底层设计,最大的改进就是,把多线程的方案改成了单线程加事件队列的方案。我直接使用社区的一张图来说明。
从这张图中,我们可以看到,社区引入了一个事件处理线程,统一处理各种控制器事件,然后控制器将原来执行的操作全部建模成一个个独立的事件,发送到专属的事件队列中,供此线程消费。这就是所谓的单线程+队列的实现方式。
值得注意的是,这里的单线程不代表之前提到的所有线程都被“干掉”了,控制器只是把缓存状态变更方面的工作委托给了这个线程而已。
这个方案的最大好处在于控制器缓存中保存的状态只被一个线程处理因此不再需要重量级的线程同步机制来维护线程安全Kafka不用再担心多线程并发访问的问题非常利于社区定位和诊断控制器的各种问题。事实上自0.11版本重构控制器代码后社区关于控制器方面的Bug明显少多了这也说明了这种方案是有效的。
针对控制器的第二个改进就是将之前同步操作ZooKeeper全部改为异步操作。ZooKeeper本身的API提供了同步写和异步写两种方式。之前控制器操作ZooKeeper使用的是同步的API性能很差集中表现为当有大量主题分区发生变更时ZooKeeper容易成为系统的瓶颈。新版本Kafka修改了这部分设计完全摒弃了之前的同步API调用转而采用异步API写入ZooKeeper性能有了很大的提升。根据社区的测试改成异步之后ZooKeeper写入提升了10倍
除了以上这些社区最近又发布了一个重大的改进之前Broker对接收的所有请求都是一视同仁的不会区别对待。这种设计对于控制器发送的请求非常不公平因为这类请求应该有更高的优先级。
举个简单的例子假设我们删除了某个主题那么控制器就会给该主题所有副本所在的Broker发送一个名为StopReplica的请求。如果此时Broker上存有大量积压的Produce请求那么这个StopReplica请求只能排队等。如果这些Produce请求就是要向该主题发送消息的话这就显得很讽刺了主题都要被删除了处理这些Produce请求还有意义吗此时最合理的处理顺序应该是赋予StopReplica请求更高的优先级使它能够得到抢占式的处理。
这在2.2版本之前是做不到的。不过自2.2开始Kafka正式支持这种不同优先级请求的处理。简单来说Kafka将控制器发送的请求与普通数据类请求分开实现了控制器请求单独处理的逻辑。鉴于这个改进还是很新的功能具体的效果我们就拭目以待吧。
小结
好了有关Kafka控制器的内容我已经讲完了。最后我再跟你分享一个小窍门。当你觉得控制器组件出现问题时比如主题无法删除了或者重分区hang住了你不用重启Kafka Broker或控制器。有一个简单快速的方式是去ZooKeeper中手动删除/controller节点。具体命令是rmr /controller。这样做的好处是既可以引发控制器的重选举又可以避免重启Broker导致的消息处理中断。
开放讨论
目前控制器依然是重度依赖于ZooKeeper的。未来如果要减少对ZooKeeper的依赖你觉得可能的方向是什么
欢迎写下你的思考和答案,我们一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@ -0,0 +1,189 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
27 关于高水位和Leader Epoch的讨论
你好我是胡夕。今天我要和你分享的主题是Kafka中的高水位和Leader Epoch机制。
你可能听说过高水位High Watermark但不一定耳闻过Leader Epoch。前者是Kafka中非常重要的概念而后者是社区在0.11版本中新推出的主要是为了弥补高水位机制的一些缺陷。鉴于高水位机制在Kafka中举足轻重而且深受各路面试官的喜爱今天我们就来重点说说高水位。当然我们也会花一部分时间来讨论Leader Epoch以及它的角色定位。
什么是高水位?
首先我们要明确一下基本的定义什么是高水位或者说什么是水位水位一词多用于流式处理领域比如Spark Streaming或Flink框架中都有水位的概念。教科书中关于水位的经典定义通常是这样的
在时刻T任意创建时间Event Time为T且T≤T的所有事件都已经到达或被观测到那么T就被定义为水位。
“Streaming System”一书则是这样表述水位的
水位是一个单调增加且表征最早未完成工作oldest work not yet completed的时间戳。
为了帮助你更好地理解水位,我借助这本书里的一张图来说明一下。
图中标注“Completed”的蓝色部分代表已完成的工作标注“In-Flight”的红色部分代表正在进行中的工作两者的边界就是水位线。
在Kafka的世界中水位的概念有一点不同。Kafka的水位不是时间戳更与时间无关。它是和位置信息绑定的具体来说它是用消息位移来表征的。另外Kafka源码使用的表述是高水位因此今天我也会统一使用“高水位”或它的缩写HW来进行讨论。值得注意的是Kafka中也有低水位Low Watermark它是与Kafka删除消息相关联的概念与今天我们要讨论的内容没有太多联系我就不展开讲了。
高水位的作用
在Kafka中高水位的作用主要有2个。
定义消息可见性,即用来标识分区下的哪些消息是可以被消费者消费的。
帮助Kafka完成副本同步。
下面这张图展示了多个与高水位相关的Kafka术语。我来详细解释一下图中的内容同时澄清一些常见的误区。
我们假设这是某个分区Leader副本的高水位图。首先请你注意图中的“已提交消息”和“未提交消息”。我们之前在专栏第11讲谈到Kafka持久性保障的时候特意对两者进行了区分。现在我借用高水位再次强调一下。在分区高水位以下的消息被认为是已提交消息反之就是未提交消息。消费者只能消费已提交消息即图中位移小于8的所有消息。注意这里我们不讨论Kafka事务因为事务机制会影响消费者所能看到的消息的范围它不只是简单依赖高水位来判断。它依靠一个名为LSOLog Stable Offset的位移值来判断事务型消费者的可见性。
另外,需要关注的是,位移值等于高水位的消息也属于未提交消息。也就是说,高水位上的消息是不能被消费者消费的。
图中还有一个日志末端位移的概念即Log End Offset简写是LEO。它表示副本写入下一条消息的位移值。注意数字15所在的方框是虚线这就说明这个副本当前只有15条消息位移值是从0到14下一条新消息的位移是15。显然介于高水位和LEO之间的消息就属于未提交消息。这也从侧面告诉了我们一个重要的事实那就是同一个副本对象其高水位值不会大于LEO值。
高水位和LEO是副本对象的两个重要属性。Kafka所有副本都有对应的高水位和LEO值而不仅仅是Leader副本。只不过Leader副本比较特殊Kafka使用Leader副本的高水位来定义所在分区的高水位。换句话说分区的高水位就是其Leader副本的高水位。
高水位更新机制
现在我们知道了每个副本对象都保存了一组高水位值和LEO值但实际上在Leader副本所在的Broker上还保存了其他Follower副本的LEO值。我们一起来看看下面这张图。
在这张图中我们可以看到Broker 0上保存了某分区的Leader副本和所有Follower副本的LEO值而Broker 1上仅仅保存了该分区的某个Follower副本。Kafka把Broker 0上保存的这些Follower副本又称为远程副本Remote Replica。Kafka副本机制在运行过程中会更新Broker 1上Follower副本的高水位和LEO值同时也会更新Broker 0上Leader副本的高水位和LEO以及所有远程副本的LEO但它不会更新远程副本的高水位值也就是我在图中标记为灰色的部分。
为什么要在Broker 0上保存这些远程副本呢其实它们的主要作用是帮助Leader副本确定其高水位也就是分区高水位。
为了帮助你更好地记忆这些值被更新的时机我做了一张表格。只有搞清楚了更新机制我们才能开始讨论Kafka副本机制的原理以及它是如何使用高水位来执行副本消息同步的。
在这里我稍微解释一下什么叫与Leader副本保持同步。判断的条件有两个。
该远程Follower副本在ISR中。
该远程Follower副本LEO值落后于Leader副本LEO值的时间不超过Broker端参数replica.lag.time.max.ms的值。如果使用默认值的话就是不超过10秒。
乍一看这两个条件好像是一回事因为目前某个副本能否进入ISR就是靠第2个条件判断的。但有些时候会发生这样的情况即Follower副本已经“追上”了Leader的进度却不在ISR中比如某个刚刚重启回来的副本。如果Kafka只判断第1个条件的话就可能出现某些副本具备了“进入ISR”的资格但却尚未进入到ISR中的情况。此时分区高水位值就可能超过ISR中副本LEO而高水位 > LEO的情形是不被允许的。
下面我们分别从Leader副本和Follower副本两个维度来总结一下高水位和LEO的更新机制。
Leader副本
处理生产者请求的逻辑如下:
写入消息到本地磁盘。
更新分区高水位值。-
i. 获取Leader副本所在Broker端保存的所有远程副本LEO值LEO-1LEO-2……LEO-n。-
ii. 获取Leader副本高水位值currentHW。-
iii. 更新 currentHW = max{currentHW, minLEO-1, LEO-2, ……LEO-n}。
处理Follower副本拉取消息的逻辑如下
读取磁盘(或页缓存)中的消息数据。
使用Follower副本发送请求中的位移值更新远程副本LEO值。
更新分区高水位值(具体步骤与处理生产者请求的步骤相同)。
Follower副本
从Leader拉取消息的处理逻辑如下
写入消息到本地磁盘。
更新LEO值。
更新高水位值。-
i. 获取Leader发送的高水位值currentHW。-
ii. 获取步骤2中更新过的LEO值currentLEO。-
iii. 更新高水位为min(currentHW, currentLEO)。
副本同步机制解析
搞清楚了这些值的更新机制之后我来举一个实际的例子说明一下Kafka副本同步的全流程。该例子使用一个单分区且有两个副本的主题。
当生产者发送一条消息时Leader和Follower副本对应的高水位是怎么被更新的呢我给出了一些图片我们一一来看。
首先是初始状态。下面这张图中的remote LEO就是刚才的远程副本的LEO值。在初始状态时所有值都是0。
当生产者给主题分区发送一条消息后,状态变更为:
此时Leader副本成功将消息写入了本地磁盘故LEO值被更新为1。
Follower再次尝试从Leader拉取消息。和之前不同的是这次有消息可以拉取了因此状态进一步变更为
这时Follower副本也成功地更新LEO为1。此时Leader和Follower副本的LEO都是1但各自的高水位依然是0还没有被更新。它们需要在下一轮的拉取中被更新如下图所示
在新一轮的拉取请求中由于位移值是0的消息已经拉取成功因此Follower副本这次请求拉取的是位移值=1的消息。Leader副本接收到此请求后更新远程副本LEO为1然后更新Leader高水位为1。做完这些之后它会将当前已更新过的高水位值1发送给Follower副本。Follower副本接收到以后也将自己的高水位值更新成1。至此一次完整的消息同步周期就结束了。事实上Kafka就是利用这样的机制实现了Leader和Follower副本之间的同步。
Leader Epoch登场
故事讲到这里似乎很完美依托于高水位Kafka既界定了消息的对外可见性又实现了异步的副本同步机制。不过我们还是要思考一下这里面存在的问题。
从刚才的分析中我们知道Follower副本的高水位更新需要一轮额外的拉取请求才能实现。如果把上面那个例子扩展到多个Follower副本情况可能更糟也许需要多轮拉取请求。也就是说Leader副本高水位更新和Follower副本高水位更新在时间上是存在错配的。这种错配是很多“数据丢失”或“数据不一致”问题的根源。基于此社区在0.11版本正式引入了Leader Epoch概念来规避因高水位更新错配导致的各种不一致问题。
所谓Leader Epoch我们大致可以认为是Leader版本。它由两部分数据组成。
Epoch。一个单调增加的版本号。每当副本领导权发生变更时都会增加该版本号。小版本号的Leader被认为是过期Leader不能再行使Leader权力。
起始位移Start Offset。Leader副本在该Epoch值上写入的首条消息的位移。
我举个例子来说明一下Leader Epoch。假设现在有两个Leader Epoch和那么第一个Leader Epoch表示版本号是0这个版本的Leader从位移0开始保存消息一共保存了120条消息。之后Leader发生了变更版本号增加到1新版本的起始位移是120。
Kafka Broker会在内存中为每个分区都缓存Leader Epoch数据同时它还会定期地将这些信息持久化到一个checkpoint文件中。当Leader副本写入消息到磁盘时Broker会尝试更新这部分缓存。如果该Leader是首次写入消息那么Broker会向缓存中增加一个Leader Epoch条目否则就不做更新。这样每次有Leader变更时新的Leader副本会查询这部分缓存取出对应的Leader Epoch的起始位移以避免数据丢失和不一致的情况。
接下来我们来看一个实际的例子它展示的是Leader Epoch是如何防止数据丢失的。请先看下图。
我稍微解释一下单纯依赖高水位是怎么造成数据丢失的。开始时副本A和副本B都处于正常状态A是Leader副本。某个使用了默认acks设置的生产者程序向A发送了两条消息A全部写入成功此时Kafka会通知生产者说两条消息全部发送成功。
现在我们假设Leader和Follower都写入了这两条消息而且Leader副本的高水位也已经更新了但Follower副本高水位还未更新——这是可能出现的。还记得吧Follower端高水位的更新与Leader端有时间错配。倘若此时副本B所在的Broker宕机当它重启回来后副本B会执行日志截断操作将LEO值调整为之前的高水位值也就是1。这就是说位移值为1的那条消息被副本B从磁盘中删除此时副本B的底层磁盘文件中只保存有1条消息即位移值为0的那条消息。
当执行完截断操作后副本B开始从A拉取消息执行正常的消息同步。如果就在这个节骨眼上副本A所在的Broker宕机了那么Kafka就别无选择只能让副本B成为新的Leader此时当A回来后需要执行相同的日志截断操作即将高水位调整为与B相同的值也就是1。这样操作之后位移值为1的那条消息就从这两个副本中被永远地抹掉了。这就是这张图要展示的数据丢失场景。
严格来说这个场景发生的前提是Broker端参数min.insync.replicas设置为1。此时一旦消息被写入到Leader副本的磁盘就会被认为是“已提交状态”但现有的时间错配问题导致Follower端的高水位更新是有滞后的。如果在这个短暂的滞后时间窗口内接连发生Broker宕机那么这类数据的丢失就是不可避免的。
现在我们来看下如何利用Leader Epoch机制来规避这种数据丢失。我依然用图的方式来说明。
场景和之前大致是类似的只不过引用Leader Epoch机制后Follower副本B重启回来后需要向A发送一个特殊的请求去获取Leader的LEO值。在这个例子中该值为2。当获知到Leader LEO=2后B发现该LEO值不比它自己的LEO值小而且缓存中也没有保存任何起始位移值 > 2的Epoch条目因此B无需执行任何日志截断操作。这是对高水位机制的一个明显改进即副本是否执行日志截断不再依赖于高水位进行判断。
现在副本A宕机了B成为Leader。同样地当A重启回来后执行与B相同的逻辑判断发现也不用执行日志截断至此位移值为1的那条消息在两个副本中均得到保留。后面当生产者程序向B写入新消息时副本B所在的Broker缓存中会生成新的Leader Epoch条目[Epoch=1, Offset=2]。之后副本B会使用这个条目帮助判断后续是否执行日志截断操作。这样通过Leader Epoch机制Kafka完美地规避了这种数据丢失场景。
小结
今天我向你详细地介绍了Kafka的高水位机制以及Leader Epoch机制。高水位在界定Kafka消息对外可见性以及实现副本机制等方面起到了非常重要的作用但其设计上的缺陷给Kafka留下了很多数据丢失或数据不一致的潜在风险。为此社区引入了Leader Epoch机制尝试规避掉这类风险。事实证明它的效果不错在0.11版本之后关于副本数据不一致性方面的Bug的确减少了很多。如果你想深入学习Kafka的内部原理今天的这些内容是非常值得你好好琢磨并熟练掌握的。
开放讨论
在讲述高水位时我是拿2个副本举的例子。不过你应该很容易地扩展到多个副本。现在请你尝试用3个副本来说明一下副本同步全流程以及分区高水位被更新的过程。
欢迎写下你的思考和答案,我们一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@ -0,0 +1,175 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
28 主题管理知多少_
你好我是胡夕。今天我想和你讨论一下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的原因主要有两个。
使用 zookeeper会绕过Kafka的安全体系。这就是说即使你为Kafka集群设置了安全认证限制了主题的创建如果你使用 zookeeper的命令依然能成功创建任意主题不受认证体系的约束。这显然是Kafka集群的运维人员不希望看到的。
使用 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 <topic_name>
如果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 <topic_name> --partitions <新分区数>
这里要注意的是你指定的分区数一定要比原有分区数大否则Kafka会抛出InvalidPartitionsException异常。
2.修改主题级别参数。
在主题创建之后我们可以使用kafka-configs脚本修改对应的参数。
这个用法我们在专栏第8讲中讨论过现在先来复习一下。假设我们要设置主题级别参数max.message.bytes那么命令如下
bin/kafka-configs.sh --zookeeper zookeeper_host:port --entity-type topics --entity-name <topic_name> --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 <topic_name>
删除主题的命令并不复杂关键是删除操作是异步的执行完这条命令不代表主题立即就被删除了。它仅仅是被标记成“已删除”状态而已。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上。该文件具体格式如下
{"version":1, "partitions":[
{"topic":"__consumer_offsets","partition":0,"replicas":[0,1,2]},
{"topic":"__consumer_offsets","partition":1,"replicas":[0,2,1]},
{"topic":"__consumer_offsets","partition":2,"replicas":[1,0,2]},
{"topic":"__consumer_offsets","partition":3,"replicas":[1,2,0]},
...
{"topic":"__consumer_offsets","partition":49,"replicas":[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 "kafka.coordinator.group.GroupMetadataManager\$OffsetsMessageFormatter" --from-beginning
除了查看位移提交数据,我们还可以直接读取该主题消息,查看消费者组的状态信息。
bin/kafka-console-consumer.sh --bootstrap-server kafka_host:port --topic __consumer_offsets --formatter "kafka.coordinator.group.GroupMetadataManager\$GroupMetadataMessageFormatter" --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工具库。
开放讨论
请思考一下为什么Kafka不允许减少分区数如果减少分区数可能会有什么样的问题
欢迎写下你的思考和答案,我们一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@ -0,0 +1,172 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
29 Kafka动态配置了解下
你好我是胡夕。今天我要和你讨论的主题是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表中增加了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路径如下图所示。
我来解释一下图中的内容。changes是用来实时监测动态参数变更的不会保存参数值topics是用来保存Kafka主题级别参数的。虽然它们不属于动态Broker端参数但其实它们也是能够动态变更的。
users和clients则是用于动态调整客户端配额Quota的znode节点。所谓配额是指Kafka运维人员限制连入集群的客户端的吞吐量或者是限定它们使用的CPU资源。
分析到这里,我们就会发现,/config/brokers znode才是真正保存动态Broker参数的地方。该znode下有两大类子节点。第一类子节点就只有一个它有个固定的名字叫< default >保存的是前面说过的cluster-wide范围的动态参数另一类则以broker.id为名保存的是特定Broker的per-broker范围参数。由于是per-broker范围因此这类子节点可能存在多个。
我们一起来看一张图片它展示的是我的一个Kafka集群环境上的动态Broker端参数。
在这张图中,我首先查看了/config/brokers下的子节点我们可以看到这里面有< default >节点和名为0、1的子节点。< default >节点中保存了我设置的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参数 > cluster-wide参数 > static参数 > 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}
这条命令的输出信息很多。我们关注两点即可。
在Broker 1层面上该参数被设置成了false这表明命令运行成功了。
从倒数第二行可以看出在全局层面上该参数值依然是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参数都有哪些。我们可以先来看看下面这两张图。
看到有这么多动态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就可以令变更生效因此能够极大地降低运维成本。除此之外我还给出了动态参数的保存机制和设置方法。在专栏的后面我还会给出动态参数设置的另一种方法敬请期待。
开放讨论
目前社区只是将一部分Broker参数升级为动态参数在实际使用过程中你觉得还有哪些参数也应该变为可动态修改
欢迎写下你的思考和答案,我们一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@ -0,0 +1,229 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
30 怎么重设消费者组位移?
你好,我是胡夕。今天我要跟你分享的主题是:如何重设消费者组位移。
为什么要重设消费者组位移?
我们知道Kafka和传统的消息引擎在设计上是有很大区别的其中一个比较显著的区别就是Kafka的消费者读取消息是可以重演的replayable
像RabbitMQ或ActiveMQ这样的传统消息中间件它们处理和响应消息的方式是破坏性的destructive即一旦消息被成功处理就会被从Broker上删除。
反观Kafka由于它是基于日志结构log-based的消息引擎消费者在消费消息时仅仅是从磁盘文件上读取数据而已是只读的操作因此消费者不会删除消息数据。同时由于位移数据是由消费者控制的因此它能够很容易地修改位移的值实现重复消费历史数据的功能。
对了之前有很多同学在专栏的留言区提问在实际使用场景中我该如何确定是使用传统的消息中间件还是使用Kafka呢我在这里统一回答一下。如果在你的场景中消息处理逻辑非常复杂处理代价很高同时你又不关心消息之间的顺序那么传统的消息中间件是比较合适的反之如果你的场景需要较高的吞吐量但每条消息的处理时间很短同时你又很在意消息的顺序此时Kafka就是你的首选。
重设位移策略
不论是哪种设置方式,重设位移大致可以从两个维度来进行。
位移维度。这是指根据位移值来重设。也就是说,直接把消费者的位移值重设成我们给定的位移值。
时间维度。我们可以给定一个时间让消费者把位移调整成大于该时间的最小位移也可以给出一段时间间隔比如30分钟前然后让消费者直接将位移调回30分钟之前的位移值。
下面的这张表格罗列了7种重设策略。接下来我来详细解释下这些策略。
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<TopicPartition> partitions);
void seekToEnd(Collection<TopicPartition> 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, "earliest");
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 = "test"; // 要重设位移的Kafka主题
try (final KafkaConsumer<String, String> consumer =
new KafkaConsumer<>(consumerProperties)) {
consumer.subscribe(Collections.singleton(topic));
consumer.poll(0);
consumer.seekToBeginning(
consumer.partitionsFor(topic).stream().map(partitionInfo ->
new TopicPartition(topic, partitionInfo.partition()))
.collect(Collectors.toList()));
}
这段代码中有几个比较关键的部分,你需要注意一下。
你要创建的消费者程序,要禁止自动提交位移。
组ID要设置成你要重设的消费者组的组ID。
调用seekToBeginning方法时需要一次性构造主题的所有分区对象。
最重要的是一定要调用带长整型的poll方法而不要调用consumer.poll(Duration.ofSecond(0))。
虽然社区已经不推荐使用poll(long)了,但短期内应该不会移除它,所以你可以放心使用。另外,为了避免重复,在后面的实例中,我只给出最关键的代码。
Latest策略和Earliest是类似的我们只需要使用seekToEnd方法即可如下面的代码所示
consumer.seekToEnd(
consumer.partitionsFor(topic).stream().map(partitionInfo ->
new TopicPartition(topic, partitionInfo.partition()))
.collect(Collectors.toList()));
实现Current策略的方法很简单我们需要借助KafkaConsumer的committed方法来获取当前提交的最新位移代码如下
consumer.partitionsFor(topic).stream().map(info ->
new TopicPartition(topic, info.partition()))
.forEach(tp -> {
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<TopicPartition, Long> timeToSearch =
consumer.partitionsFor(topic).stream().map(info ->
new TopicPartition(topic, info.partition()))
.collect(Collectors.toMap(Function.identity(), tp -> ts));
for (Map.Entry<TopicPartition, OffsetAndTimestamp> entry :
consumer.offsetsForTimes(timeToSearch).entrySet()) {
consumer.seek(entry.getKey(), entry.getValue().offset());
}
这段代码构造了LocalDateTime实例然后利用它去查找对应的位移值最后调用seek实现了重设位移。
最后我来给出实现Duration策略的代码。假设我们要将位移调回30分钟前那么代码如下
Map<TopicPartition, Long> timeToSearch = consumer.partitionsFor(topic).stream()
.map(info -> new TopicPartition(topic, info.partition()))
.collect(Collectors.toMap(Function.identity(), tp -> System.currentTimeMillis() - 30 * 1000 * 60));
for (Map.Entry<TopicPartition, OffsetAndTimestamp> 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 <offset> --execute
Shift-By-N策略直接指定--shift-by N。
bin/kafka-consumer-groups.sh --bootstrap-server kafka-host:port --group test-group --reset-offsets --shift-by <offset_N> --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的方式了。
开放讨论
你在实际使用过程中,是否遇到过要重设位移的场景,你是怎么实现的?
欢迎写下你的思考和答案,我们一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@ -0,0 +1,219 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
31 常见工具脚本大汇总
你好我是胡夕。今天我要跟你分享的主题是Kafka常见的脚本汇总。
命令行脚本概览
Kafka默认提供了很多个命令行脚本用于实现各种各样的功能和运维管理。今天我以2.2版本为例详细地盘点下这些命令行工具。下图展示了2.2版本提供的所有命令行脚本。
从图中我们可以知道2.2版本总共提供了30个SHELL脚本。图中的windows实际上是个子目录里面保存了Windows平台下的BAT批处理文件。其他的.sh文件则是Linux平台下的标准SHELL脚本。
默认情况下,不加任何参数或携带 help运行SHELL文件会得到该脚本的使用方法说明。下面这张图片展示了kafka-log-dirs脚本的调用方法。
有了这些基础的了解,我来逐一地说明这些脚本的用途,然后再给你详细地介绍一些常见的脚本。
我们先来说说connect-standalone和connect-distributed两个脚本。这两个脚本是Kafka Connect组件的启动脚本。在专栏第4讲谈到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端的适配性。
我截取了部分输出内容,现在我稍微解释一下这些输出的含义。我们以第一行为例:
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
>
在这段命令中我们指定生产者参数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的消费者的位移那么命令如图所示
图中的CURRENT-OFFSET表示该消费者当前消费的最新位移LOG-END-OFFSET表示对应分区最新生产消息的位移LAG列是两者的差值。CONSUMER-ID是Kafka消费者程序自动生成的一个ID。截止到2.2版本你都无法干预这个ID的生成过程。如果运行该命令时这个消费者程序已经终止了那么此列的值为空。
小结
好了我们小结一下。今天我们一起梳理了Kafka 2.2版本自带的所有脚本我给出了常见的运维操作的工具行命令。希望这些命令对你操作和管理Kafka集群有所帮助。另外我想强调的是由于Kafka依然在不断演进我们今天提到的命令的用法很可能会随着版本的变迁而发生变化。在具体使用这些命令时你最好详细地阅读一下它们的Usage说明。
开放讨论
你在使用Kafka命令的过程中曾经踩过哪些“坑”或者说有哪些惨痛的经历呢
欢迎写下你的思考和答案,我们一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@ -0,0 +1,177 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
32 KafkaAdminClientKafka的运维利器
你好我是胡夕。今天我要和你分享的主题是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需要增加以下依赖项
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>2.3.0</version>
</dependency>
如果你使用的是Gradle那么添加方法如下
compile group: 'org.apache.kafka', name: 'kafka-clients', version: '2.3.0'
功能
鉴于社区还在不断地完善AdminClient的功能所以你需要时刻关注不同版本的发布说明Release Notes看看是否有新的运维操作被加入进来。在最新的2.3版本中AdminClient提供的功能有9大类。
主题管理:包括主题的创建、删除和查询。
权限管理:包括具体权限的配置与删除。
配置参数管理包括Kafka各种资源的参数设置、详情查询。所谓的Kafka资源主要有Broker、主题、用户、Client-id等。
副本日志管理:包括副本底层日志路径的变更和详情查询。
分区管理:即创建额外的主题分区。
消息删除:即删除指定位移之前的分区消息。
Delegation Token管理包括Delegation Token的创建、更新、过期和详情查询。
消费者组管理:包括消费者组的查询、位移查询和删除。
Preferred领导者选举推选指定主题分区的Preferred Broker为领导者。
工作原理
在详细介绍AdminClient的主要功能之前我们先简单了解一下AdminClient的工作原理。从设计上来看AdminClient是一个双线程的设计前端主线程和后端I/O线程。前端线程负责将用户要执行的操作转换成对应的请求然后再将请求发送到后端I/O线程的队列中而后端I/O线程从队列中读取相应的请求然后发送到对应的Broker节点上之后把执行结果保存起来以便等待前端线程的获取。
值得一提的是AdminClient在内部大量使用生产者-消费者模式将请求生成与处理解耦。我在下面这张图中大致描述了它的工作原理。
如图所示前端主线程会创建名为Call的请求对象实例。该实例有两个主要的任务。
构建对应的请求对象。比如如果要创建主题那么就创建CreateTopicsRequest如果是查询消费者组位移就创建OffsetFetchRequest。
指定响应的回调逻辑。比如从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参数。如果你想了解完整的参数列表可以去官网查询一下。如果要销毁AdminClient实例需要显式调用AdminClient的close方法。
你可以简单使用下面的代码同时实现AdminClient实例的创建与销毁。
Properties props = new Properties();
props.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka-host:port");
props.put("request.timeout.ms", 600000);
try (AdminClient client = AdminClient.create(props)) {
// 执行你要做的操作……
}
这段代码使用Java 7的try-with-resource语法特性创建了AdminClient实例并在使用之后自动关闭。你可以在try代码块中加入你想要执行的操作逻辑。
常见的AdminClient应用实例
讲完了AdminClient的工作原理和构造方法接下来我举几个实际的代码程序来说明一下如何应用它。这几个例子都是我们最常见的。
创建主题
首先,我们来看看如何创建主题,代码如下:
String newTopicName = "test-topic";
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 = "test-group";
try (AdminClient client = AdminClient.create(props)) {
ListConsumerGroupOffsetsResult result = client.listConsumerGroupOffsets(groupID);
Map<TopicPartition, OffsetAndMetadata> 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<String, DescribeLogDirsResponse.LogDirInfo> logDirInfoMap : ret.all().get().values()) {
size += logDirInfoMap.values().stream().map(logDirInfo -> logDirInfo.replicaInfos).flatMap(
topicPartitionReplicaInfoMap ->
topicPartitionReplicaInfoMap.values().stream().map(replicaInfo -> replicaInfo.size))
.mapToLong(Long::longValue).sum();
}
System.out.println(size);
}
这段代码的主要思想是使用AdminClient的describeLogDirs方法获取指定Broker上所有分区主题的日志路径信息然后把它们累积在一起得出总的磁盘占用量。
小结
好了我们来小结一下。社区于0.11版本正式推出了Java客户端版的AdminClient工具该工具提供了几十种运维操作而且它还在不断地演进着。如果可以的话你最好统一使用AdminClient来执行各种Kafka集群管理操作摒弃掉连接ZooKeeper的那些工具。另外我建议你时刻关注该工具的功能完善情况毕竟目前社区对AdminClient的变更频率很高。
开放讨论
请思考一下如果我们要使用AdminClient去增加某个主题的分区代码应该怎么写请给出主体代码。
欢迎写下你的思考和答案,我们一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@ -0,0 +1,234 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
33 Kafka认证机制用哪家
你好我是胡夕。今天我要和你分享的主题是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版本来选择该版本所支持的认证机制。
GSSAPI也就是Kerberos使用的安全接口是在0.9版本中被引入的。
PLAIN是使用简单的用户名/密码认证的机制在0.10版本中被引入。
SCRAM主要用于解决PLAIN机制安全问题的新机制是在0.10.2版本中被引入的。
OAUTHBEARER是基于OAuth 2认证框架的新机制在2.0版本中被引进。
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认证
为了方便你更好地理解和记忆,我把这些认证机制汇总在下面的表格里了。你可以对照着表格,进行一下区分。
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="admin"
password="admin";
};
关于这个文件内容,你需要注意以下两点:
不要忘记最后一行和倒数第二行结尾处的分号;
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=<your_path>/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=<your_path>/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="writer" password="writer";
之后运行Console Producer程序
$ bin/kafka-console-producer.sh --broker-list localhost:9092,localhost:9093 --topic test --producer.config <your_path>/producer.conf
>hello, world
>
可以看到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="reader" password="reader";
之后运行Console Consumer程序
$ bin/kafka-console-consumer.sh --bootstrap-server localhost:9092,localhost:9093 --topic test --from-beginning --consumer.config <your_path>/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
>[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 <your_path>/producer.conf
>Good!
现在Console Producer可以正常发送消息了。
这个过程完整地展示了SASL/SCRAM是如何在不重启Broker的情况下增减用户的。
至此SASL/SCRAM配置就完成了。在专栏下一讲中我会详细介绍一下如何赋予writer和reader用户不同的权限。
小结
好了我们来小结一下。今天我们讨论了Kafka目前提供的几种认证机制我给出了它们各自的优劣势以及推荐使用建议。其实在真实的使用场景中认证和授权往往是结合在一起使用的。在专栏下一讲中我会详细向你介绍Kafka的授权机制即ACL机制敬请期待。
开放讨论
请谈一谈你的Kafka集群上的用户认证机制并分享一个你遇到过的“坑”。
欢迎写下你的思考和答案,我们一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@ -0,0 +1,271 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
34 云环境下的授权该怎么做?
你好我是胡夕。今天我要分享的主题是Kafka的授权机制。
什么是授权机制?
我们在上一讲中花了不少时间讨论Kafka的认证机制今天我们来看看Kafka的授权机制Authorization。所谓授权一般是指对与信息安全或计算机安全相关的资源授予访问权限特别是存取控制。
具体到权限模型,常见的有四种。
ACLAccess-Control List访问控制列表。
RBACRole-Based Access Control基于角色的权限控制。
ABACAttribute-Based Access Control基于属性的权限控制。
PBACPolicy-Based Access Control基于策略的权限控制。
在典型的互联网场景中,前两种模型应用得多,后面这两种则比较少用。
ACL模型很简单它表征的是用户与权限的直接映射关系如下图所示
而RBAC模型则加入了角色的概念支持对用户进行分组如下图所示
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权限。
看到这么大一张表格你是不是很惊讶其实这恰好证明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
>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="$BASE_DIR/certificates"
PASSWORD=test1234
KEY_STORE="$CERT_OUTPUT_PATH/server.keystore.jks"
TRUST_STORE="$CERT_OUTPUT_PATH/server.truststore.jks"
CLIENT_KEY_STORE="$CERT_OUTPUT_PATH/client.keystore.jks"
CLIENT_TRUST_STORE="$CERT_OUTPUT_PATH/client.truststore.jks"
KEY_PASSWORD=$PASSWORD
STORE_PASSWORD=$PASSWORD
TRUST_KEY_PASSWORD=$PASSWORD
TRUST_STORE_PASSWORD=$PASSWORD
CERT_AUTH_FILE="$CERT_OUTPUT_PATH/ca-cert"
DAYS_VALID=365
DNAME="CN=Xi Hu, OU=YourDept, O=YourCompany, L=Beijing, ST=Beijing, C=CN"
mkdir -p $CERT_OUTPUT_PATH
echo "1. 产生key和证书......"
keytool -keystore $KEY_STORE -alias kafka-server -validity $DAYS_VALID -genkey -keyalg RSA \
-storepass $STORE_PASSWORD -keypass $KEY_PASSWORD -dname "$DNAME"
keytool -keystore $CLIENT_KEY_STORE -alias kafka-client -validity $DAYS_VALID -genkey -keyalg RSA \
-storepass $STORE_PASSWORD -keypass $KEY_PASSWORD -dname "$DNAME"
echo "2. 创建CA......"
openssl req -new -x509 -keyout $CERT_OUTPUT_PATH/ca-key -out "$CERT_AUTH_FILE" -days "$DAYS_VALID" \
-passin pass:"$PASSWORD" -passout pass:"$PASSWORD" \
-subj "/C=CN/ST=Beijing/L=Beijing/O=YourCompany/OU=YourDept,CN=Xi Hu"
echo "3. 添加CA文件到broker truststore......"
keytool -keystore "$TRUST_STORE" -alias CARoot \
-importcert -file "$CERT_AUTH_FILE" -storepass "$TRUST_STORE_PASSWORD" -keypass "$TRUST_KEY_PASS" -noprompt
echo "4. 添加CA文件到client truststore......"
keytool -keystore "$CLIENT_TRUST_STORE" -alias CARoot \
-importcert -file "$CERT_AUTH_FILE" -storepass "$TRUST_STORE_PASSWORD" -keypass "$TRUST_KEY_PASS" -noprompt
echo "5. 从keystore中导出集群证书......"
keytool -keystore "$KEY_STORE" -alias kafka-server -certreq -file "$CERT_OUTPUT_PATH/server-cert-file" \
-storepass "$STORE_PASSWORD" -keypass "$KEY_PASSWORD" -noprompt
keytool -keystore "$CLIENT_KEY_STORE" -alias kafka-client -certreq -file "$CERT_OUTPUT_PATH/client-cert-file" \
-storepass "$STORE_PASSWORD" -keypass "$KEY_PASSWORD" -noprompt
echo "6. 使用CA签发证书......"
openssl x509 -req -CA "$CERT_AUTH_FILE" -CAkey $CERT_OUTPUT_PATH/ca-key -in "$CERT_OUTPUT_PATH/server-cert-file" \
-out "$CERT_OUTPUT_PATH/server-cert-signed" -days "$DAYS_VALID" -CAcreateserial -passin pass:"$PASSWORD"
openssl x509 -req -CA "$CERT_AUTH_FILE" -CAkey $CERT_OUTPUT_PATH/ca-key -in "$CERT_OUTPUT_PATH/client-cert-file" \
-out "$CERT_OUTPUT_PATH/client-cert-signed" -days "$DAYS_VALID" -CAcreateserial -passin pass:"$PASSWORD"
echo "7. 导入CA文件到keystore......"
keytool -keystore "$KEY_STORE" -alias CARoot -import -file "$CERT_AUTH_FILE" -storepass "$STORE_PASSWORD" \
-keypass "$KEY_PASSWORD" -noprompt
keytool -keystore "$CLIENT_KEY_STORE" -alias CARoot -import -file "$CERT_AUTH_FILE" -storepass "$STORE_PASSWORD" \
-keypass "$KEY_PASSWORD" -noprompt
echo "8. 导入已签发证书到keystore......"
keytool -keystore "$KEY_STORE" -alias kafka-server -import -file "$CERT_OUTPUT_PATH/server-cert-signed" \
-storepass "$STORE_PASSWORD" -keypass "$KEY_PASSWORD" -noprompt
keytool -keystore "$CLIENT_KEY_STORE" -alias kafka-client -import -file "$CERT_OUTPUT_PATH/client-cert-signed" \
-storepass "$STORE_PASSWORD" -keypass "$KEY_PASSWORD" -noprompt
echo "9. 删除临时文件......"
rm "$CERT_OUTPUT_PATH/ca-cert.srl"
rm "$CERT_OUTPUT_PATH/server-cert-signed"
rm "$CERT_OUTPUT_PATH/client-cert-signed"
rm "$CERT_OUTPUT_PATH/server-cert-file"
rm "$CERT_OUTPUT_PATH/client-cert-file"
你可以把上面的代码保存成一个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:"CN=Xi Hu,OU=YourDept,O=YourCompany,L=Beijing,ST=Beijing,C=CN" --operation All --cluster
第四你要为客户端程序授予相应的权限比如为生产者授予producer权限为消费者授予consumer权限。假设客户端要访问的主题名字是test那么命令如下
$ bin/kafka-acls.sh --authorizer-properties zookeeper.connect=localhost:2181 --add --allow-principal User:"CN=Xi Hu,OU=YourDept,O=YourCompany,L=Beijing,ST=Beijing,C=CN" --producer --topic 'test'
$ bin/kafka-acls.sh --authorizer-properties zookeeper.connect=localhost:2181 --add --allow-principal User:"CN=Xi Hu,OU=YourDept,O=YourCompany,L=Beijing,ST=Beijing,C=CN" --consumer --topic 'test' --group '*'
注意这两条命令中的 producer和 consumer它们类似于一个快捷方式直接将Producer和Consumer常用的权限进行了一次性的授予。
作为云环境PaaS管理员除了以上这些必要的权限你最好不要把其他权限授予客户端比如创建主题的权限。总之你授予的权限越少你的Kafka集群就越安全。
小结
讲到这里我们就完整地把Kafka授权机制梳理了一遍。除此之外我还附赠了SSL端配置方法。希望你能将这两讲关于安全配置的内容结合起来学习打造一个超级安全的Kafka集群。
开放讨论
Kafka提供的权限有很多种我们今天讨论的内容只覆盖了其中最重要的几个权限。如果要让一个客户端能够查询消费者组的提交位移数据你觉得应该授予它什么权限
欢迎写下你的思考和答案,我们一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@ -0,0 +1,160 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
35 跨集群备份解决方案MirrorMaker
你好我是胡夕。今天我要和你分享的主题是Kafka的跨集群数据镜像工具MirrorMaker。
一般情况下我们会使用一套Kafka集群来完成业务但有些场景确实会需要多套Kafka集群同时工作比如为了便于实现灾难恢复你可以在两个机房分别部署单独的Kafka集群。如果其中一个机房出现故障你就能很容易地把流量打到另一个正常运转的机房下。再比如你想为地理相近的客户提供低延时的消息服务而你的主机房又离客户很远这时你就可以在靠近客户的地方部署一套Kafka集群让这套集群服务你的客户从而提供低延时的服务。
如果要实现这些需求除了部署多套Kafka集群之外你还需要某种工具或框架来帮助你实现数据在集群间的拷贝或镜像。
值得注意的是通常我们把数据在单个集群下不同节点之间的拷贝称为备份而把数据在集群间的拷贝称为镜像Mirroring
今天我来重点介绍一下Apache Kafka社区提供的MirrorMaker工具它可以帮我们实现消息或数据从一个集群到另一个集群的拷贝。
什么是MirrorMaker
从本质上说MirrorMaker就是一个消费者+生产者的程序。消费者负责从源集群Source Cluster消费数据生产者负责向目标集群Target Cluster发送消息。整个镜像流程如下图所示
MirrorMaker连接的源集群和目标集群会实时同步消息。当然你不要认为你只能使用一套MirrorMaker来连接上下游集群。事实上很多用户会部署多套集群用于实现不同的目的。
我们来看看下面这张图。图中部署了三套集群:左边的源集群负责主要的业务处理;右上角的目标集群可以用于执行数据分析;而右下角的目标集群则充当源集群的热备份。
运行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 ".*"
现在我来解释一下这条命令中各个参数的含义。
consumer.config参数。它指定了MirrorMaker中消费者的配置文件地址最主要的配置项是bootstrap.servers也就是该MirrorMaker从哪个Kafka集群读取消息。因为MirrorMaker有可能在内部创建多个消费者实例并使用消费者组机制因此你还需要设置group.id参数。另外我建议你额外配置auto.offset.reset=earliest否则的话MirrorMaker只会拷贝那些在它启动之后到达源集群的消息。
producer.config参数。它指定了MirrorMaker内部生产者组件的配置文件地址。通常来说Kafka Java Producer很友好你不需要配置太多参数。唯一的例外依然是bootstrap.servers你必须显式地指定这个参数配置拷贝的消息要发送到的目标集群。
num.streams参数。我个人觉得这个参数的名字很容易给人造成误解。第一次看到这个参数名的时候我一度以为MirrorMaker是用Kafka Streams组件实现的呢。其实并不是。这个参数就是告诉MirrorMaker要创建多少个KafkaConsumer实例。当然它使用的是多线程的方案即在后台创建并启动多个线程每个线程维护专属的消费者实例。在实际使用时你可以根据你的机器性能酌情设置多个线程。
whitelist参数。如命令所示这个参数接收一个正则表达式。所有匹配该正则表达式的主题都会被自动地执行镜像。在这个命令中我指定了“.*”,这表明我要同步源集群上的所有主题。
MirrorMaker配置实例
现在我就在测试环境中为你演示一下MirrorMaker的使用方法。
演示的流程大致是这样的首先我们会启动两套Kafka集群它们是单节点的伪集群监听端口分别是9092和9093之后我们会启动MirrorMaker工具实时地将9092集群上的消息同步镜像到9093集群上最后我们启动额外的消费者来验证消息是否拷贝成功。
第1步启动两套Kafka集群
启动日志如下所示:
[2019-07-23 17:01:40,544] INFO Kafka version: 2.3.0 (org.apache.kafka.common.utils.AppInfoParser)-
[2019-07-23 17:01:40,544] INFO Kafka commitId: fc1aaa116b661c8a (org.apache.kafka.common.utils.AppInfoParser)-
[2019-07-23 17:01:40,544] INFO Kafka startTimeMs: 1563872500540 (org.apache.kafka.common.utils.AppInfoParser)-
[2019-07-23 17:01:40,545] INFO [KafkaServer id=0] started (kafka.server.KafkaServer)
[2019-07-23 16:59:59,462] INFO Kafka version: 2.3.0 (org.apache.kafka.common.utils.AppInfoParser)-
[2019-07-23 16:59:59,462] INFO Kafka commitId: fc1aaa116b661c8a (org.apache.kafka.common.utils.AppInfoParser)-
[2019-07-23 16:59:59,462] INFO Kafka startTimeMs: 1563872399459 (org.apache.kafka.common.utils.AppInfoParser)-
[2019-07-23 16:59:59,463] INFO [KafkaServer id=1] started (kafka.server.KafkaServer)
第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 ".*"
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专门写了一篇博客详细说明了uReplicator的设计原理并罗列了社区的MirrorMaker工具的一些缺陷以及uReplicator的应对方法。我建议你一定要读一读这篇博客。
2.LinkedIn开发的Brooklin Mirror Maker工具
针对现有MirrorMaker工具不易实现管道化的缺陷这个工具进行了有针对性的改进同时也对性能做了一些优化。目前在LinkedIn公司Brooklin Mirror Maker已经完全替代了社区版的MirrorMaker。如果你想深入了解它是如何做到的我给你推荐一篇博客你可以详细阅读一下。
3.Confluent公司研发的Replicator工具
这个工具提供的是企业级的跨集群镜像方案是市面上已知的功能最为强大的工具可以便捷地为你提供Kafka主题在不同集群间的迁移。除此之外Replicator工具还能自动在目标集群上创建与源集群上配置一模一样的主题极大地方便了运维管理。不过凡事有利就有弊Replicator是要收费的。如果你所在的公司预算充足而且你们关心数据在多个集群甚至是多个数据中心间的迁移质量不妨关注一下Confluent公司的Replicator工具。
小结
好了我们总结一下今天所讲的MirrorMaker。它是Apache Kafka社区提供的跨集群镜像解决方案主要实现将Kafka消息实时从一个集群同步复制或镜像到另一个集群。你可以把MirrorMaker应用到很多实际的场景中比如数据备份、主备集群等。MirrorMaker本身功能简单应用灵活但也有运维成本高、性能差等劣势因此业界有厂商研发了自己的镜像工具。你可以根据自身的业务需求选择合适的工具来帮助你完成跨集群的数据备份。
开放讨论
今天我只演示了MirrorMaker最基本的使用方法即把消息原样搬移。如果我们想在消息被镜像前做一些处理比如修改消息体内容那么我们应该如何实现呢提示指定kafka-mirror-maker脚本的 message.handler参数。
欢迎写下你的思考和答案,我们一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@ -0,0 +1,136 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
36 你应该怎么监控Kafka
你好我是胡夕。今天我要和你分享的主题是如何监控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命令输出的内容如下图所示
在图片的右上角我们可以看到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个指标需要你时刻关注
Full GC发生频率和时长。这个指标帮助你评估Full GC对Broker进程的影响。长时间的停顿会令Broker端抛出各种超时异常。
活跃对象大小。这个指标是你设定堆大小的重要依据同时它还能帮助你细粒度地调优JVM各个代的堆大小。
应用线程总数。这个指标帮助你了解Broker进程对CPU的使用情况。
总之你对Broker进程了解得越透彻你所做的JVM调优就越有效果。
谈到具体的监控前两个都可以通过GC日志来查看。比如下面的这段GC日志就说明了GC后堆上的存活对象大小。
2019-07-30T09:13:03.809+0800: 552.982: [GC cleanup 827M->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讲解释过这两个指标的含义这里我就不再赘述了。总之它们直接反映了Consumer的消费进度。如果你使用了Consumer Group那么有两个额外的JMX指标需要你关注下一个是join rate另一个是sync rate。它们说明了Rebalance的频繁程度。如果在你的环境中它们的值很高那么你就需要思考下Rebalance频繁发生的原因了。
小结
好了我们来小结一下。今天我介绍了监控Kafka的方方面面。除了监控Kafka集群我还推荐你从主机和JVM的维度进行监控。对主机的监控往往是我们定位和发现问题的第一步。JVM监控同样重要。要知道很多Java进程碰到的性能问题是无法通过调整Kafka参数是解决的。最后我罗列了一些比较重要的Kafka JMX指标。在下一讲中我会专门介绍一下如何使用各种工具来查看这些JMX指标。
开放讨论
请分享一下你在监控Kafka方面的心得以及你的运维技巧。
欢迎写下你的思考和答案,我们一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@ -0,0 +1,157 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
37 主流的Kafka监控框架
你好我是胡夕。今天我要和你分享的主题是那些主流的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工具提供了很多参数但你不必完全了解所有的参数。我把主要的参数说明列在了下面的表格里你至少要了解一下这些参数的含义。
现在,我举一个实际的例子来说明一下如何运行这个命令。
假设你要查询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 "YYYY-MM-dd HH:mm:ss" --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 “YYYY-MM-dd HH:mm:ss” reporting-interval 1000
Trying to connect to JMX url: service:jmx:rmi:///jndi/rmi://:9997/jmxrmi.
“time”,“kafka.controller:type=KafkaController,name=ActiveControllerCount:Value”
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="localhost:2181"
之后运行以下命令启动Kafka Manager
bin/kafka-manager -Dconfig.file=conf/application.conf -Dhttp.port=8080
该命令指定了要读取的配置文件以及要启动的监听端口。现在我们打开浏览器输入对应的IP:8080就可以访问Kafka Manager了。下面这张图展示了我在Kafka Manager中添加集群的主界面。
注意要勾选上Enable JMX Polling这样你才能监控Kafka的各种JMX指标。下图就是Kafka Manager框架的主界面。
从这张图中我们可以发现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菜单项已经没有了。
总之作为一款非常强大的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和每秒消息数等。将这么多数据统一集成进一个面板上直观地呈现出来是这套框架非常鲜明的特点。
与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几乎都能提供。
不过如果你要使用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的组合适用于已经具有较成熟框架的企业。对于其他的几个监控框架你可以把它们作为这两个方案的补充加入到你的监控解决方案中。
开放讨论
如果想知道某台Broker上是否存在请求积压我们应该监控哪个JMX指标
欢迎写下你的思考和答案,我们一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@ -0,0 +1,153 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
38 调优Kafka你做到了吗
你好我是胡夕。今天我要和你分享的主题是如何调优Kafka。
调优目标
在做调优之前我们必须明确优化Kafka的目标是什么。通常来说调优是为了满足系统常见的非功能性需求。在众多的非功能性需求中性能绝对是我们最关心的那一个。不同的系统对性能有不同的诉求比如对于数据库用户而言性能意味着请求的响应时间用户总是希望查询或更新请求能够被更快地处理完并返回。
对Kafka而言性能一般是指吞吐量和延时。
吞吐量也就是TPS是指Broker端进程或Client端应用程序每秒能处理的字节数或消息数这个值自然是越大越好。
延时和我们刚才说的响应时间类似它表示从Producer端发送消息到Broker端持久化完成之间的时间间隔。这个指标也可以代表端到端的延时End-to-EndE2E也就是从Producer发送消息到Consumer成功消费该消息的总时长。和TPS相反我们通常希望延时越短越好。
总之高吞吐量、低延时是我们调优Kafka集群的主要目标一会儿我们会详细讨论如何达成这些目标。在此之前我想先谈一谈优化漏斗的问题。
优化漏斗
优化漏斗是一个调优过程中的分层漏斗,我们可以在每一层上执行相应的优化调整。总体来说,层级越靠上,其调优的效果越明显,整体优化效果是自上而下衰减的,如下图所示:
第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讲提到过与之相关的数据报告。该报告宣称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 < pid >就能人为触发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。下面我用一张图来说明一下。
图中蓝色的Producer、Consumer和Broker的版本是相同的它们之间的通信可以享受Zero Copy的快速通道相反一个低版本的Consumer程序想要与Producer、Broker交互的话就只能依靠JVM堆中转一下丢掉了快捷通道就只能走慢速通道了。因此在优化Broker这一层时你只要保持服务器端和客户端版本的一致就能获得很多性能收益了。
应用层调优
现在,我们终于来到了漏斗的最顶层。其实,这一层的优化方法各异,毕竟每个应用程序都是不一样的。不过,有一些公共的法则依然是值得我们遵守的。
不要频繁地创建Producer和Consumer对象实例。构造这些对象的开销很大尽量复用它们。
用完及时关闭。这些对象底层会创建很多物理资源如Socket连接、ByteBuffer缓冲区等。不及时关闭的话势必造成资源泄露。
合理利用多线程来改善性能。Kafka的Java Producer是线程安全的你可以放心地在多个线程中共享同一个实例而Java Consumer虽不是线程安全的但我们在专栏第20讲讨论过多线程的方案你可以回去复习一下。
性能指标调优
接下来我会给出调优各个目标的参数配置以及具体的配置原因希望它们能够帮助你更有针对性地调整你的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呢我来跟你分享一个参数列表。
我稍微解释一下表格中的内容。
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端一次性多返回点数据吧。
调优延时
讲完了调优吞吐量,我们来说说如何优化延时,下面是调优延时的参数列表。
在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读取消息不得不走物理磁盘因此吞吐量下降。找到了真实原因解决起来就简单多了。
其实,我给出这个案例的真实目的是想说,对于性能调优,我们最好按照今天给出的步骤一步一步地窄化和定位问题。一旦定位了原因,后面的优化就水到渠成了。
开放讨论
请分享一个你调优Kafka的真实案例详细说说你是怎么碰到性能问题的又是怎么解决的。
欢迎写下你的思考和答案,我们一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@ -0,0 +1,263 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
39 从0搭建基于Kafka的企业级实时日志流处理平台
你好我是胡夕。今天我要和你分享的主题是从0搭建基于Kafka的企业级实时日志流处理平台。
简单来说,我们要实现一些大数据组件的组合,就如同玩乐高玩具一样,把它们“插”在一起,“拼”成一个更大一点的玩具。
在任何一个企业中服务器每天都会产生很多的日志数据。这些数据内容非常丰富包含了我们的线上业务数据、用户行为数据以及后端系统数据。实时分析这些数据能够帮助我们更快地洞察潜在的趋势从而有针对性地做出决策。今天我们就使用Kafka搭建一个这样的平台。
流处理架构
如果在网上搜索实时日志流处理你应该能够搜到很多教你搭建实时流处理平台做日志分析的教程。这些教程使用的技术栈大多是Flume+Kafka+Storm、Spark Streaming或Flink。特别是Flume+Kafka+Flink的组合逐渐成为了实时日志流处理的标配。不过要搭建这样的处理平台你需要用到3个框架才能实现这既增加了系统复杂度也提高了运维成本。
今天我来演示一下如何使用Apache Kafka这一个框架实现一套实时日志流处理系统。换句话说我使用的技术栈是Kafka Connect+Kafka Core+Kafka Streams的组合。
下面这张图展示了基于Kafka的实时日志流处理平台的流程。
从图中我们可以看到日志先从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] "GET /v1/open/product_list?user_key=**&user_phone=**&screen_height=1125&screen_width=2436&from_page=1&user_type=2&os_type=ios HTTP/1.0" 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服务应该能看到下面的返回内容
{"version":"2.3.0","commit":"fc1aaa116b661c8a","kafka_cluster_id":"XbADW3mnTUuQZtJKn9P-hA"}
添加File Connector
看到该JSON串就表明Connect已经成功启动了。此时我们打开一个终端运行下面这条命令来查看一下当前都有哪些Connector。
$ curl http://localhost:8083/connectors
[]
结果显示目前我们没有创建任何Connector。
现在我们来创建对应的File Connector。该Connector读取指定的文件并为每一行文本创建一条消息并发送到特定的Kafka主题上。创建命令如下
$ curl -H "Content-Type:application/json" -H "Accept:application/json" http://localhost:8083/connectors -X POST --data '{"name":"file-connector","config":{"connector.class":"org.apache.kafka.connect.file.FileStreamSourceConnector","file":"/var/log/access.log","tasks.max":"1","topic":"access_log"}}'
{"name":"file-connector","config":{"connector.class":"org.apache.kafka.connect.file.FileStreamSourceConnector","file":"/var/log/access.log","tasks.max":"1","topic":"access_log","name":"file-connector"},"tasks":[],"type":"source"}
这条命令本质上是向Connect REST服务发送了一个POST请求去创建对应的Connector。在这个例子中我们的Connector类是Kafka默认提供的FileStreamSourceConnector。我们要读取的日志文件在/var/log目录下要发送到Kafka的主题名称为access_log。
现在我们再次运行curl http: // localhost:8083/connectors 验证一下刚才的Connector是否创建成功了。
$ curl http://localhost:8083/connectors
["file-connector"]
显然名为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帮助它们发现彼此并进行协作。
关于Kafka Streams的原理我会在专栏后面进行详细介绍。今天我们只要能够学会利用它提供的API编写流处理应用帮我们找到刚刚提到的请求日志中ios端和android端发送请求数量的占比数据就行了。
编写流处理应用
要使用Kafka Streams你需要在你的Java项目中显式地添加kafka-streams依赖。我以最新的2.3版本为例分别演示下Maven和Gradle的配置方法。
Maven:
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-streams</artifactId>
<version>2.3.0</version>
</dependency>
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, "os-check-streams");
props.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
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<String, String> source = builder.stream("access_log");
source.mapValues(value -> gson.fromJson(value, LogLine.class)).mapValues(LogLine::getPayload)
.groupBy((key, value) -> value.contains("ios") ? "ios" : "android")
.windowedBy(TimeWindows.of(Duration.ofSeconds(2L)))
.count()
.toStream()
.to("os-check", 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("streams-shutdown-hook") {
@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组件的使用和管理敬请期待。
开放讨论
请比较一下Flume+Kafka+Flink方案和纯Kafka方案思考一下它们各自的优劣之处。在实际场景中我们该如何选择呢
欢迎写下你的思考和答案,我们一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@ -0,0 +1,167 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
40 Kafka Streams与其他流处理平台的差异在哪里
你好我是胡夕。今天我要和你分享的主题是Kafka Streams与其他流处理平台的差异。
近些年来开源流处理领域涌现出了很多优秀框架。光是在Apache基金会孵化的项目关于流处理的大数据框架就有十几个之多比如早期的Apache Samza、Apache Storm以及这两年火爆的Spark以及Flink等。
应该说每个框架都有自己独特的地方也都有自己的缺陷。面对这众多的流处理框架我们应该如何选择呢今天我就来梳理几个主流的流处理平台并重点分析一下Kafka Streams与其他流处理平台的差异。
什么是流处理平台?
首先我们有必要了解一下流处理平台的概念。“Streaming Systems”一书是这么定义“流处理平台”的流处理平台Streaming System是处理无限数据集Unbounded Dataset的数据处理引擎而流处理是与批处理Batch Processing相对应的。
所谓的无限数据是指数据永远没有尽头。流处理平台是专门处理这种数据集的系统或框架。当然这并不是说批处理系统不能处理这种无限数据集只是通常情况下它更擅长处理有限数据集Bounded Dataset
那流处理和批处理究竟该如何区分呢?下面这张图应该能帮助你快速且直观地理解它们的区别。
好了,现在我来详细解释一下流处理和批处理的区别。
长期以来,流处理给人的印象通常是低延时,但是结果不准确。每来一条消息,它就能计算一次结果,但由于它处理的大多是无界数据,可能永远也不会结束,因此在流处理中,我们很难精确描述结果何时是精确的。理论上,流处理的计算结果会不断地逼近精确结果。
但是,它的竞争对手批处理则正好相反。批处理能提供准确的计算结果,但往往延时很高。
因此业界的大神们扬长避短将两者结合在一起使用。一方面利用流处理快速地给出不那么精确的结果另一方面依托于批处理最终实现数据一致性。这就是所谓的Lambda架构。
延时低是个很好的特性但如果计算结果不准确流处理是无法完全替代批处理的。所谓计算结果准确在教科书或文献中有个专属的名字叫正确性Correctness。可以这么说目前难以实现正确性是流处理取代批处理的最大障碍而实现正确性的基石是精确一次处理语义Exactly Once SemanticsEOS
这里的精确一次是流处理平台能提供的一类一致性保障。常见的一致性保障有三类:
至多一次At most once语义消息或事件对应用状态的影响最多只有一次。
至少一次At least once语义消息或事件对应用状态的影响最少一次。
精确一次Exactly once语义消息或事件对应用状态的影响有且只有一次。
注意我这里说的都是对应用状态的影响。对于很多有副作用Side Effect的操作而言实现精确一次语义几乎是不可能的。举个例子假设流处理中的某个步骤是发送邮件操作当邮件发送出去后倘若后面出现问题要回滚整个流处理流程已发送的邮件是没法追回的这就是所谓的副作用。当你的流处理逻辑中存在包含副作用的操作算子时该操作算子的执行是无法保证精确一次处理的。因此我们通常只是保证这类操作对应用状态的影响精确一次罢了。后面我们会重点讨论Kafka Streams是如何实现EOS的。
我们今天讨论的流处理既包含真正的实时流处理也包含微批化Microbatch的流处理。所谓的微批化其实就是重复地执行批处理引擎来实现对无限数据集的处理。典型的微批化实现平台就是Spark Streaming。
Kafka Streams的特色
相比于其他流处理平台Kafka Streams最大的特色就是它不是一个平台至少它不是一个具备完整功能Full-Fledged的平台比如其他框架中自带的调度器和资源管理器就是Kafka Streams不提供的。
Kafka官网明确定义Kafka Streams是一个Java客户端库Client Library。你可以使用这个库来构建高伸缩性、高弹性、高容错性的分布式应用以及微服务。
使用Kafka Streams API构建的应用就是一个普通的Java应用程序。你可以选择任何熟悉的技术或框架对其进行编译、打包、部署和上线。
在我看来这是Kafka Streams与Storm、Spark Streaming或Flink最大的区别。
Java客户端库的定位既可以说是特色也可以说是一个缺陷。目前Kafka Streams在国内推广缓慢的一个重要原因也在于此。毕竟很多公司希望它是一个功能完备的平台既能提供流处理应用API也能提供集群资源管理与调度方面的能力。所以这个定位到底是特色还是缺陷仁者见仁、智者见智吧。
Kafka Streams与其他框架的差异
接下来我从应用部署、上下游数据源、协调方式和消息语义保障Semantic Guarantees4个方面详细分析一下Kafka Streams与其他框架的差异。
应用部署
首先我们从流处理应用部署方式上对Kafka Streams及其他框架进行区分。
我们刚刚提到过Kafka Streams应用需要开发人员自行打包和部署你甚至可以将Kafka Streams应用嵌入到其他Java应用中。因此作为开发者的你除了要开发代码之外还要自行管理Kafka Streams应用的生命周期要么将其打包成独立的jar包单独运行要么将流处理逻辑嵌入到微服务中开放给其他服务调用。
但不论是哪种部署方式你需要自己处理不要指望Kafka Streams帮你做这些事情。
相反地其他流处理平台则提供了完整的部署方案。我以Apache Flink为例来解释一下。在Flink中流处理应用会被建模成单个的流处理计算逻辑并封装进Flink的作业中。类似地Spark中也有作业的概念而在Storm中则叫拓扑Topology。作业的生命周期由框架来管理特别是在Flink中Flink框架自行负责管理作业包括作业的部署和更新等。这些都无需应用开发人员干预。
另外Flink这类框架都存在资源管理器Resource Manager的角色。一个作业所需的资源完全由框架层的资源管理器来支持。常见的资源管理器如YARN、Kubernetes、Mesos等比较新的流处理框架如Spark、Flink等都是支持的。像Spark和Flink这样的框架也支持Standalone集群的方式即不借助于任何已有的资源管理器完全由集群自己来管理资源。这些都是Kafka Streams无法提供的。
因此从应用部署方面来看Kafka Streams更倾向于将部署交给开发人员来做而不是依赖于框架自己实现。
上下游数据源
谈完了部署方式的差异我们来说说连接上下游数据源方面的差异。简单来说Kafka Streams目前只支持从Kafka读数据以及向Kafka写数据。在没有Kafka Connect组件的支持下Kafka Streams只能读取Kafka集群上的主题数据在完成流处理逻辑后也只能将结果写回到Kafka主题上。
反观Spark Streaming和Flink这类框架它们都集成了丰富的上下游数据源连接器Connector比如常见的连接器MySQL、ElasticSearch、HBase、HDFS、Kafka等。如果使用这些框架你可以很方便地集成这些外部框架无需二次开发。
当然由于开发Connector通常需要同时掌握流处理框架和外部框架因此在实际使用过程中Connector的质量参差不齐在具体使用的时候你可以多查查对应的jira官网看看有没有明显的“坑”然后再决定是否使用。
在这个方面我是有前车之鉴的。曾经我使用过一个Connector我发现它在读取Kafka消息向其他系统写入的时候似乎总是重复消费。费了很多周折之后我才发现这是一个已知的Bug而且早就被记录在jira官网上了。因此我推荐你多逛下jira也许能提前避开一些“坑”。
总之目前Kafka Streams只支持与Kafka集群进行交互它没有提供开箱即用的外部数据源连接器。
协调方式
在分布式协调方面Kafka Streams应用依赖于Kafka集群提供的协调功能来提供高容错性和高伸缩性。
Kafka Streams应用底层使用了消费者组机制来实现任意的流处理扩缩容。应用的每个实例或节点本质上都是相同消费者组下的独立消费者彼此互不影响。它们之间的协调工作由Kafka集群Broker上对应的协调者组件来完成。当有实例增加或退出时协调者自动感知并重新分配负载。
我画了一张图来展示每个Kafka Streams实例内部的构造从这张图中我们可以看出每个实例都由一个消费者实例、特定的流处理逻辑以及一个生产者实例组成而这些实例中的消费者实例共同构成了一个消费者组。
通过这个机制Kafka Streams应用同时实现了高伸缩性和高容错性而这一切都是自动提供的不需要你手动实现。
而像Flink这样的框架它的容错性和扩展性是通过专属的主节点Master Node全局来协调控制的。
Flink支持通过ZooKeeper实现主节点的高可用性避免单点失效某个节点出现故障会自动触发恢复操作。这种全局性协调模型对于流处理中的作业而言非常实用但不太适配单独的流处理应用程序。原因就在于它不像Kafka Streams那样轻量级应用程序必须要实现特定的API来开启检查点机制checkpointing同时还需要亲身参与到错误恢复的过程中。
应该这样说在不同的场景下Kafka Streams和Flink这种重量级的协调模型各有优劣。
消息语义保障
我们刚刚提到过EOS目前很多流处理框架都宣称它们实现了EOS也包括Kafka Streams本身。关于精确一次处理语义有一些地方需要澄清一下。
实际上当把Spark、Flink与Kafka结合使用时如果不使用Kafka在0.11.0.0版本引入的幂等性Producer和事务型Producer这些框架是无法实现端到端的EOS的。
因为这些框架与Kafka是相互独立的彼此之间没有任何语义保障机制。但如果使用了事务机制情况就不同了。这些外部系统利用Kafka的事务机制保障了消息从Kafka读取到计算再到写入Kafka的全流程EOS。这就是所谓的端到端精确一次处理语义。
之前Spark和Flink宣称的EOS都是在各自的框架内实现的无法实现端到端的EOS。只有使用了Kafka的事务机制它们对应的Connector才有可能支持端到端精确一次处理语义。
Spark官网上明确指出了用户若要实现与Kafka的EOS必须自己确保幂等输出和位移保存在同一个事务中。如果你不能自己实现这套机制那么就要依赖于Kafka提供的事务机制来保证。
而Flink在Kafka 0.11之前也宣称提供EOS不过是有前提条件的即每条消息对Flink应用状态的影响有且只有一次。
举个例子如果你使用Flink从Kafka读取消息然后不加任何处理直接写入到MySQL那么这个操作就是无状态的此时Flink无法保证端到端的EOS。
换句话说Flink最后写入到MySQL的Kafka消息可能有重复的。当然Flink社区自1.4版本起正式实现了端到端的EOS其基本设计思想正是基于Kafka 0.11幂等性Producer的两阶段提交机制。
两阶段提交2-Phase Commit2PC机制是一种分布式事务机制用于实现分布式系统上跨多个节点事务的原子性提交。下面这张图来自于神书“Designing Data-Intensive Applications”中关于2PC讲解的章节。它清晰地描述了一次成功2PC的过程。在这张图中两个数据库参与到分布式事务的提交过程中它们各自做了一些变更现在需要使用2PC来保证两个数据库的变更被原子性地提交。如图所示2PC被分为两个阶段Prepare阶段和Commit阶段。只有完整地执行了这两个阶段这个分布式事务才算是提交成功。
分布式系统中的2PC常见于数据库内部实现或以XA事务的方式供各种异质系统使用。Kafka也借鉴了2PC的思想在Kafka内部实现了基于2PC的事务机制。
但是对于Kafka Streams而言情况就不同了。它天然支持端到端的EOS因为它本来就是和Kafka紧密相连的。
下图展示了一个典型的Kafka Streams应用的执行逻辑。
通常情况下一个Kafka Streams需要执行5个步骤
读取最新处理的消息位移;
读取消息数据;
执行处理逻辑;
将处理结果写回到Kafka
保存位置信息。
这五步的执行必须是原子性的,否则无法实现精确一次处理语义。
在设计上Kafka Streams在底层大量使用Kafka事务机制和幂等性Producer来实现多分区的原子性写入又因为它只能读写Kafka因此Kafka Streams很容易地就实现了端到端的EOS。
总之虽然Flink自1.4版本也提供与Kafka的EOS但从适配性来考量的话应该说Kafka Streams与Kafka的适配性是最好的。
小结
好了我们来小结一下。今天我重点分享了Kafka Streams与其他流处理框架或平台的差异。总的来说Kafka Streams是一个轻量级的客户端库而其他流处理平台都是功能完备的流处理解决方案。这是Kafka Streams的特色所在但同时可能也是缺陷。不过我认为很多情况下我们并不需要重量级的流处理解决方案采用轻量级的库API帮助我们实现实时计算是很方便的情形我想这或许是Kafka Streams未来的破局之路吧。
在专栏后面的内容中我会详细介绍如何使用Kafka Streams API实现实时计算并跟你分享一个实际的案例希望这些能激发你对Kafka Streams的兴趣并为你以后的探索奠定基础。
开放讨论
知乎上有个关于Kafka Streams的“灵魂拷问”为什么Kafka Streams没什么人用我推荐你去看一下并谈谈你对这个问题的理解和答案。
欢迎写下你的思考和答案,我们一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@ -0,0 +1,249 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
41 Kafka Streams DSL开发实例
你好我是胡夕。今天我要和你分享的主题是Kafka Streams DSL开发实例。
DSL也就是Domain Specific Language意思是领域特定语言。它提供了一组便捷的API帮助我们实现流式数据处理逻辑。今天我就来分享一些Kafka Streams中的DSL开发方法以及具体实例。
Kafka Streams背景介绍
在上一讲中我们提到流处理平台是专门处理无限数据集的引擎。就Kafka Streams而言它仅仅是一个客户端库。所谓的Kafka Streams应用就是调用了Streams API的普通Java应用程序。只不过在Kafka Streams中流处理逻辑是用拓扑来表征的。
一个拓扑结构本质上是一个有向无环图DAG它由多个处理节点Node和连接节点的多条边组成如下图所示
图中的节点也称为处理单元或Processor它封装了具体的事件处理逻辑。Processor在其他流处理平台也被称为操作算子。常见的操作算子包括转换map、过滤filter、连接join和聚合aggregation等。后面我会详细介绍几种常见的操作算子。
大体上Kafka Streams开放了两大类API供你定义Processor逻辑。
第1类就是我刚刚提到的DSL它是声明式的函数式API使用起来感觉和SQL类似你不用操心它的底层是怎么实现的你只需要调用特定的API告诉Kafka Streams你要做什么即可。
举个简单的例子,你可以看看下面这段代码,尝试理解下它是做什么的。
movies.filter((title, movie) -> movie.getGenre().equals("动作片")).xxx()...
这段代码虽然用了Java 8的Lambda表达式但从整体上来看它要做的事情应该还是很清晰的它要从所有Movie事件中过滤出影片类型是“动作片”的事件。这就是DSL声明式API的实现方式。
第2类则是命令式的低阶API称为Processor API。比起DSL这组API提供的实现方式更加灵活。你可以编写自定义的算子来实现一些DSL天然没有提供的处理逻辑。事实上DSL底层也是用Processor API实现的。
目前Kafka Streams DSL提供的API已经很丰富了基本上能够满足我们大部分的处理逻辑需求我今天重点介绍一下DSL的使用方法。
不论是用哪组API实现所有流处理应用本质上都可以分为两类有状态的Stateful应用和无状态的Stateless应用。
有状态的应用指的是应用中使用了类似于连接、聚合或时间窗口Window的API。一旦调用了这些API你的应用就变为有状态的了也就是说你需要让Kafka Streams帮你保存应用的状态。
无状态的应用是指在这类应用中,某条消息的处理结果不会影响或依赖其他消息的处理。常见的无状态操作包括事件转换以及刚刚那个例子中的过滤等。
关键概念
了解了这些背景之后,你还需要掌握一些流处理领域内的关键概念,即流、表以及流表二元性,还有时间和时间窗口。
流表二元性
首先,我来介绍一下流处理中流和表的概念,以及它们之间的关系。
流就是一个永不停止至少理论上是这样的的事件序列而表和关系型数据库中的概念类似是一组行记录。在流处理领域两者是有机统一的流在时间维度上聚合之后形成表表在时间维度上不断更新形成流这就是所谓的流表二元性Duality of Streams and Tables。流表二元性在流处理领域内的应用是Kafka框架赖以成功的重要原因之一。
下面这张图展示了表转换成流,流再转换成表的全过程。
刚开始时表中只有一条记录“张三1”。将该条记录转成流变成了一条事件。接着表增加了新记录“李四1”。针对这个变更流中也增加了对应的新事件。之后表中张三的对应值从1更新为2流也增加了相应的更新事件。最后表中添加了新数据“王五1”流也增加了新记录。至此表转换成流的工作就完成了。
从这个过程中我们可以看出流可以看作是表的变更事件日志Changelog。与之相反的是流转换成表的过程可以说是这个过程的逆过程我们为流中的每条事件打一个快照Snapshot就形成了表。
流和表的概念在流处理领域非常关键。在Kafka Streams DSL中流用KStream表示而表用KTable表示。
Kafka Streams还定义了GlobalKTable。本质上它和KTable都表征了一个表里面封装了事件变更流但是它和KTable的最大不同在于当Streams应用程序读取Kafka主题数据到GlobalKTable时它会读取主题所有分区的数据而对KTable而言Streams程序实例只会读取部分分区的数据这主要取决于Streams实例的数量。
时间
在流处理领域内,精确定义事件时间是非常关键的:一方面,它是决定流处理应用能否实现正确性的前提;另一方面,流处理中时间窗口等操作依赖于时间概念才能正常工作。
常见的时间概念有两类事件发生时间Event Time和事件处理时间Processing Time。理想情况下我们希望这两个时间相等即事件一旦发生就马上被处理但在实际场景中这是不可能的Processing Time永远滞后于Event Time而且滞后程度又是一个高度变化无法预知就像“Streaming Systems”一书中的这张图片所展示的那样
该图中的45°虚线刻画的是理想状态即Event Time等于Processing Time而粉色的曲线表征的是真实情况即Processing Time落后于Event Time而且落后的程度Lag不断变化毫无规律。
如果流处理应用要实现结果的正确性就必须要使用基于Event Time的时间窗口而不能使用基于Processing Time的时间窗口。
时间窗口
所谓的时间窗口机制就是将流数据沿着时间线切分的过程。常见的时间窗口包括固定时间窗口Fixed Windows、滑动时间窗口Sliding Windows和会话窗口Session Windows。Kafka Streams同时支持这三类时间窗口。在后面的例子中我会详细介绍如何使用Kafka Streams API实现时间窗口功能。
运行WordCount实例
好了关于Kafka Streams及其DSL的基本概念我都阐述完了下面我给出大数据处理领域的Hello World实例WordCount程序。
每个大数据处理框架第一个要实现的程序基本上都是单词计数。我们来看下Kafka Streams DSL如何实现WordCount。我先给出完整代码稍后我会详细介绍关键部分代码的含义以及运行它的方法。
package kafkalearn.demo.wordcount;
import org.apache.kafka.clients.consumer.ConsumerConfig;
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.kstream.KStream;
import org.apache.kafka.streams.kstream.KTable;
import org.apache.kafka.streams.kstream.Produced;
import java.util.Arrays;
import java.util.Locale;
import java.util.Properties;
import java.util.concurrent.CountDownLatch;
public final class WordCountDemo {
public static void main(final String[] args) {
final Properties props = new Properties();
props.put(StreamsConfig.APPLICATION_ID_CONFIG, "wordcount-stream-demo");
props.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
props.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String().getClass().getName());
props.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.String().getClass().getName());
props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
final StreamsBuilder builder = new StreamsBuilder();
final KStream<String, String> source = builder.stream("wordcount-input-topic");
final KTable<String, Long> counts = source
.flatMapValues(value -> Arrays.asList(value.toLowerCase(Locale.getDefault()).split(" ")))
.groupBy((key, value) -> value)
.count();
counts.toStream().to("wordcount-output-topic", Produced.with(Serdes.String(), Serdes.Long()));
final KafkaStreams streams = new KafkaStreams(builder.build(), props);
final CountDownLatch latch = new CountDownLatch(1);
Runtime.getRuntime().addShutdownHook(new Thread("wordcount-stream-demo-jvm-hook") {
@Override
public void run() {
streams.close();
latch.countDown();
}
});
try {
streams.start();
latch.await();
} catch (final Throwable e) {
System.exit(1);
}
System.exit(0)
在程序开头我构造了一个Properties对象实例对Kafka Streams程序的关键参数进行了赋值比如application id、bootstrap servers和默认的KV序列化器Serializer和反序列化器Deserializer。其中application id是Kafka Streams应用的唯一标识必须要显式地指定。默认的KV序列化器、反序列化器是为消息的Key和Value进行序列化和反序列化操作的。
接着我构造了一个StreamsBuilder对象并使用该对象实例创建了一个KStream这个KStream从名为wordcount-input-topic的Kafka主题读取消息。该主题消息由一组单词组成单词间用空格分割比如zhangsan lisi wangwu。
由于我们要进行单词计数所以就需要将消息中的单词提取出来。有了前面的概念介绍你应该可以猜到KTable是很合适的存储结构因此下一步就是将刚才的这个KStream转换成KTable。
我们先对单词进行分割这里我用到了flatMapValues方法代码中的Lambda表达式实现了从消息中提取单词的逻辑。由于String.split()方法会返回多个单词因此我们使用flatMapValues而不是mapValues。原因是前者能够将多个元素“打散”成一组单词而如果使用后者我们得到的就不是一组单词而是多组单词了。
这些都做完之后程序调用groupBy方法对单词进行分组。由于是计数相同的单词必须被分到一起然后就是调用count方法对每个出现的单词进行统计计数并保存在名为counts的KTable对象中。
最后我们将统计结果写回到Kafka中。由于KTable是表是静态的数据因此这里要先将其转换成KStream然后再调用to方法写入到名为wordcount-output-topic的主题中。此时counts中事件的Key是单词而Value是统计个数因此我们在调用to方法时同时指定了Key和Value的序列化器分别是字符串序列化器和长整型序列化器。
至此Kafka Streams的流计算逻辑就编写完了接下来就是构造KafkaStreams实例并启动它了。通常来说这部分的代码都是类似的即调用start方法启动整个流处理应用以及配置一个JVM关闭钩子Shutdown Hook实现流处理应用的关闭等。
总体来说Kafka Streams DSL实现WordCount的方式还是很简单的仅仅调用几个操作算子就轻松地实现了分布式的单词计数实时处理功能。事实上现在主流的实时流处理框架越来越倾向于这样的设计思路即通过提供丰富而便捷的开箱即用操作算子简化用户的开发成本采用类似于搭积木的方式快捷地构建实时计算应用。
待启动该Java程序之后你需要创建出对应的输入和输出主题并向输入主题不断地写入符合刚才所说的格式的单词行之后你需要运行下面的命令去查看输出主题中是否正确地统计了你刚才输入的单词个数
bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 \
--topic wordcount-output-topic \
--from-beginning \
--formatter kafka.tools.DefaultMessageFormatter \
--property print.key=true \
--property print.value=true \
--property key.deserializer=org.apache.kafka.common.serialization.StringDeserializer \
--property value.deserializer=org.apache.kafka.common.serialization.LongDeserializer
开发API
介绍了具体的例子之后我们来看下Kafka Streams还提供了哪些功能强大的API。我们可以重点关注两个方面一个是常见的操作算子另一个是时间窗口API。
常见操作算子
操作算子的丰富程度和易用性是衡量流处理框架受欢迎程度的重要依据之一。Kafka Streams DSL提供了很多开箱即用的操作算子大体上分为两大类无状态算子和有状态算子。下面我就向你分别介绍几个经常使用的算子。
在无状态算子中filter的出场率是极高的。它执行的就是过滤的逻辑。依然拿WordCount为例假设我们只想统计那些以字母s开头的单词的个数我们可以在执行完flatMapValues后增加一行代码代码如下
.filter(((key, value) -> value.startsWith("s")))
另一个常见的无状态算子当属map一族了。Streams DSL提供了很多变体比如map、mapValues、flatMap和flatMapValues。我们已经见识了flatMapValues的威力其他三个的功能也是类似的只是所有带Values的变体都只对消息体执行转换不触及消息的Key而不带Values的变体则能修改消息的Key。
举个例子假设当前消息没有Key而Value是单词本身现在我们想要将消息变更成这样的KV对即Key是单词小写而Value是单词长度那么我们可以调用map方法代码如下
KStream<String, Integer> transformed = stream.map(
(key, value) -> KeyValue.pair(value.toLowerCase(), value.length()));
最后我再介绍一组调试用的无状态算子print和peek。Streams DSL支持你使用这两个方法查看你的消息流中的事件。这两者的区别在于print是终止操作一旦你调用了print方法后面就不能再调用任何其他方法了而peek则允许你在查看消息流的同时依然能够继续对其进行处理比如下面这两段代码所示
stream.print(Printed.toFile("streams.out").withLabel("debug"));
stream.peek((key, value) -> System.out.println("key=" + key + ", value=" + value)).map(...);
常见的有状态操作算子主要涉及聚合Aggregation方面的操作比如计数、求和、求平均值、求最大最小值等。Streams DSL目前只提供了count方法用于计数其他的聚合操作需要你自行使用API实现。
假设我们有个消息流每条事件就是一个单独的整数现在我们想要对其中的偶数进行求和那么Streams DSL中的实现方法如下
final KTable<Integer, Integer> sumOfEvenNumbers = input
.filter((k, v) -> v % 2 == 0)
.selectKey((k, v) -> 1)
.groupByKey()
.reduce((v1, v2) -> v1 + v2);
我简单解释一下selectKey调用。由于我们要对所有事件中的偶数进行求和因此需要把这些消息的Key都调整成相同的值因此这里我使用selectKey指定了一个Dummy Key值即上面这段代码中的数值1。它没有任何含义仅仅是让所有消息都赋值上这个Key而已。真正核心的代码在于reduce调用它是执行求和的关键逻辑。
时间窗口实例
前面说过Streams DSL支持3类时间窗口。前两类窗口通过TimeWindows.of方法来实现会话窗口通过SessionWindows.with来实现。
假设在刚才的WordCount实例中我们想每一分钟统计一次单词计数那么需要在调用count之前增加下面这行代码
.windowedBy(TimeWindows.of(Duration.ofMinutes(1)))
同时你还需要修改counts的类型此时它不再是KTable了而变成了KTable因为引入了时间窗口所以事件的Key也必须要携带时间窗口的信息。除了这两点变化WordCount其他部分代码都不需要修改。
可见Streams DSL在API封装性方面还是做得很好的通常你只需要增加或删减几行代码就能实现处理逻辑的修改了。
小结
好了我们来小结一下。今天我跟你分享了Kafka Streams以及DSL的背景与概念然后我根据实例展示了WordCount单词计数程序以及运行方法最后针对常见的操作算子和时间窗口我给出了示例代码这些内容应该可以帮你应对大部分的流处理开发。另外我建议你经常性地查询一下官网文档去学习一些更深入更高级的用法比如固定时间窗口的用法。在很多场景中我们都想知道过去一段时间内企业某个关键指标的值是多少如果要实现这个需求时间窗口是必然要涉及到的。
开放讨论
今天给出的WordCount例子没有调用时间窗口API我们统计的是每个单词的总数。如果现在我们想统计每5分钟内单词出现的次数应该加一行什么代码呢
欢迎写下你的思考和答案,我们一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@ -0,0 +1,225 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
42 Kafka Streams在金融领域的应用
你好我是胡夕。今天我要和你分享的主题是Kafka Streams在金融领域的应用。
背景
金融领域囊括的内容有很多我今天分享的主要是如何利用大数据技术特别是Kafka Streams实时计算框架来帮助我们更好地做企业用户洞察。
众所周知金融领域内的获客成本是相当高的一线城市高净值白领的获客成本通常可达上千元。面对如此巨大的成本压力金融企业一方面要降低广告投放的获客成本另一方面要做好精细化运营实现客户生命周期内价值Custom Lifecycle Value, CLV的最大化。
实现价值最大化的一个重要途径就是做好用户洞察而用户洞察要求你要更深度地了解你的客户即所谓的Know Your CustomerKYC真正做到以客户为中心不断地满足客户需求。
为了实现KYC传统的做法是花费大量的时间与客户见面做面对面的沟通以了解客户的情况。但是用这种方式得到的数据往往是不真实的毕竟客户内心是有潜在的自我保护意识的短时间内的面对面交流很难真正洞察到客户的真实诉求。
相反地渗透到每个人日常生活方方面面的大数据信息则代表了客户的实际需求。比如客户经常浏览哪些网站、都买过什么东西、最喜欢的视频类型是什么。这些数据看似很随意但都表征了客户最真实的想法。将这些数据汇总在一起我们就能完整地构造出客户的画像这就是所谓的用户画像User Profile技术。
用户画像
用户画像听起来很玄妙但实际上你应该是很熟悉的。你的很多基本信息比如性别、年龄、所属行业、工资收入和爱好等都是用户画像的一部分。举个例子我们可以这样描述一个人某某某男性28岁未婚工资水平大致在15000到20000元之间是一名大数据开发工程师居住在北京天通苑小区平时加班很多喜欢动漫或游戏。
其实这一连串的描述就是典型的用户画像。通俗点来说构建用户画像的核心工作就是给客户或用户打标签Tagging。刚刚那一连串的描述就是用户系统中的典型标签。用户画像系统通过打标签的形式把客户提供给业务人员从而实现精准营销。
ID映射ID Mapping
用户画像的好处不言而喻而且标签打得越多越丰富就越能精确地表征一个人的方方面面。不过在打一个个具体的标签之前弄清楚“你是谁”是所有用户画像系统首要考虑的问题这个问题也被称为ID识别问题。
所谓的ID即Identification表示用户身份。在网络上能够标识用户身份信息的常见ID有5种。
身份证号这是最能表征身份的ID信息每个身份证号只会对应一个人。
手机号:手机号通常能较好地表征身份。虽然会出现同一个人有多个手机号或一个手机号在不同时期被多个人使用的情形,但大部分互联网应用使用手机号表征用户身份的做法是很流行的。
设备ID在移动互联网时代这主要是指手机的设备ID或Mac、iPad等移动终端设备的设备ID。特别是手机的设备ID在很多场景下具备定位和识别用户的功能。常见的设备ID有iOS端的IDFA和Android端的IMEI。
应用注册账号这属于比较弱的一类ID。每个人在不同的应用上可能会注册不同的账号但依然有很多人使用通用的注册账号名称因此具有一定的关联性和识别性。
Cookie在PC时代浏览器端的Cookie信息是很重要的数据它是网络上表征用户信息的重要手段之一。只不过随着移动互联网时代的来临Cookie早已江河日下如今作为ID数据的价值也越来越小了。我个人甚至认为在构建基于移动互联网的新一代用户画像时Cookie可能要被抛弃了。
在构建用户画像系统时我们会从多个数据源上源源不断地收集各种个人用户数据。通常情况下这些数据不会全部携带以上这些ID信息。比如在读取浏览器的浏览历史时你获取的是Cookie数据而读取用户在某个App上的访问行为数据时你拿到的是用户的设备ID和注册账号信息。
倘若这些数据表征的都是一个用户的信息我们的用户画像系统如何识别出来呢换句话说你需要一种手段或技术帮你做各个ID的打通或映射。这就是用户画像领域的ID映射问题。
实时ID Mapping
我举个简单的例子。假设有一个金融理财用户张三他首先在苹果手机上访问了某理财产品然后在安卓手机上注册了该理财产品的账号最后在电脑上登录该账号并购买了该理财产品。ID Mapping 就是要将这些不同端或设备上的用户信息聚合起来然后找出并打通用户所关联的所有ID信息。
实时ID Mapping的要求就更高了它要求我们能够实时地分析从各个设备收集来的数据并在很短的时间内完成ID Mapping。打通用户ID身份的时间越短我们就能越快地为其打上更多的标签从而让用户画像发挥更大的价值。
从实时计算或流处理的角度来看实时ID Mapping能够转换成一个流-表连接问题Stream-Table Join即我们实时地将一个流和一个表进行连接。
消息流中的每个事件或每条消息包含的是一个未知用户的某种信息它可以是用户在页面的访问记录数据也可以是用户的购买行为数据。这些消息中可能会包含我们刚才提到的若干种ID信息比如页面访问信息中可能包含设备ID也可能包含注册账号而购买行为信息中可能包含身份证信息和手机号等。
连接的另一方表保存的是用户所有的ID信息随着连接的不断深入表中保存的ID品类会越来越丰富也就是说流中的数据会被不断地补充进表中最终实现对用户所有ID的打通。
Kafka Streams实现
好了现在我们就来看看如何使用Kafka Streams来实现一个特定场景下的实时ID Mapping。为了方便理解我们假设ID Mapping只关心身份证号、手机号以及设备ID。下面是用Avro写成的Schema格式
{
"namespace": "kafkalearn.userprofile.idmapping",
"type": "record",
"name": "IDMapping",
"fields": [
{"name": "deviceId", "type": "string"},
{"name": "idCard", "type": "string"},
{"name": "phone", "type": "string"}
]
}
顺便说一下Avro是Java或大数据生态圈常用的序列化编码机制比如直接使用JSON或XML保存对象。Avro能极大地节省磁盘占用空间或网络I/O传输量因此普遍应用于大数据量下的数据传输。
在这个场景下我们需要两个Kafka主题一个用于构造表另一个用于构建流。这两个主题的消息格式都是上面的IDMapping对象。
新用户在填写手机号注册App时会向第一个主题发送一条消息该用户后续在App上的所有访问记录也都会以消息的形式发送到第二个主题。值得注意的是发送到第二个主题上的消息有可能携带其他的ID信息比如手机号或设备ID等。就像我刚刚所说的这是一个典型的流-表实时连接场景连接之后我们就能够将用户的所有数据补齐实现ID Mapping的打通。
基于这个设计思路我先给出完整的Kafka Streams代码稍后我会对重点部分进行详细解释
package kafkalearn.userprofile.idmapping;
// omit imports……
public class IDMappingStreams {
public static void main(String[] args) throws Exception {
if (args.length < 1) {
throw new IllegalArgumentException("Must specify the path for a configuration file.");
}
IDMappingStreams instance = new IDMappingStreams();
Properties envProps = instance.loadProperties(args[0]);
Properties streamProps = instance.buildStreamsProperties(envProps);
Topology topology = instance.buildTopology(envProps);
instance.createTopics(envProps);
final KafkaStreams streams = new KafkaStreams(topology, streamProps);
final CountDownLatch latch = new CountDownLatch(1);
// Attach shutdown handler to catch Control-C.
Runtime.getRuntime().addShutdownHook(new Thread("streams-shutdown-hook") {
@Override
public void run() {
streams.close();
latch.countDown();
}
});
try {
streams.start();
latch.await();
} catch (Throwable e) {
System.exit(1);
}
System.exit(0);
}
private Properties loadProperties(String propertyFilePath) throws IOException {
Properties envProps = new Properties();
try (FileInputStream input = new FileInputStream(propertyFilePath)) {
envProps.load(input);
return envProps;
}
}
private Properties buildStreamsProperties(Properties envProps) {
Properties props = new Properties();
props.put(StreamsConfig.APPLICATION_ID_CONFIG, envProps.getProperty("application.id"));
props.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, envProps.getProperty("bootstrap.servers"));
props.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String().getClass());
props.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.String().getClass());
return props;
}
private void createTopics(Properties envProps) {
Map<String, Object> config = new HashMap<>();
config.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, envProps.getProperty("bootstrap.servers"));
try (AdminClient client = AdminClient.create(config)) {
List<NewTopic> topics = new ArrayList<>();
topics.add(new NewTopic(
envProps.getProperty("stream.topic.name"),
Integer.parseInt(envProps.getProperty("stream.topic.partitions")),
Short.parseShort(envProps.getProperty("stream.topic.replication.factor"))));
topics.add(new NewTopic(
envProps.getProperty("table.topic.name"),
Integer.parseInt(envProps.getProperty("table.topic.partitions")),
Short.parseShort(envProps.getProperty("table.topic.replication.factor"))));
client.createTopics(topics);
}
}
private Topology buildTopology(Properties envProps) {
final StreamsBuilder builder = new StreamsBuilder();
final String streamTopic = envProps.getProperty("stream.topic.name");
final String rekeyedTopic = envProps.getProperty("rekeyed.topic.name");
final String tableTopic = envProps.getProperty("table.topic.name");
final String outputTopic = envProps.getProperty("output.topic.name");
final Gson gson = new Gson();
// 1. 构造表
KStream<String, IDMapping> rekeyed = builder.<String, String>stream(tableTopic)
.mapValues(json -> gson.fromJson(json, IDMapping.class))
.filter((noKey, idMapping) -> !Objects.isNull(idMapping.getPhone()))
.map((noKey, idMapping) -> new KeyValue<>(idMapping.getPhone(), idMapping));
rekeyed.to(rekeyedTopic);
KTable<String, IDMapping> table = builder.table(rekeyedTopic);
// 2. 流-表连接
KStream<String, String> joinedStream = builder.<String, String>stream(streamTopic)
.mapValues(json -> gson.fromJson(json, IDMapping.class))
.map((noKey, idMapping) -> new KeyValue<>(idMapping.getPhone(), idMapping))
.leftJoin(table, (value1, value2) -> IDMapping.newBuilder()
.setPhone(value2.getPhone() == null ? value1.getPhone() : value2.getPhone())
.setDeviceId(value2.getDeviceId() == null ? value1.getDeviceId() : value2.getDeviceId())
.setIdCard(value2.getIdCard() == null ? value1.getIdCard() : value2.getIdCard())
.build())
.mapValues(v -> gson.toJson(v));
joinedStream.to(outputTopic);
return builder.build();
}
}
这个Java类代码中最重要的方法是buildTopology函数它构造了我们打通ID Mapping的所有逻辑。
在该方法中我们首先构造了StreamsBuilder对象实例这是构造任何Kafka Streams应用的第一步。之后我们读取配置文件获取了要读写的所有Kafka主题名。在这个例子中我们需要用到4个主题它们的作用如下
streamTopic保存用户登录App后发生的各种行为数据格式是IDMapping对象的JSON串。你可能会问前面不是都创建Avro Schema文件了吗怎么这里又用回JSON了呢原因是这样的社区版的Kafka没有提供Avro的序列化/反序列化类支持如果我要使用Avro必须改用Confluent公司提供的Kafka但这会偏离我们专栏想要介绍Apache Kafka的初衷。所以我还是使用JSON进行说明。这里我只是用了Avro Code Generator帮我们提供IDMapping对象各个字段的set/get方法你使用Lombok也是可以的。
rekeyedTopic这个主题是一个中间主题它将streamTopic中的手机号提取出来作为消息的Key同时维持消息体不变。
tableTopic保存用户注册App时填写的手机号。我们要使用这个主题构造连接时要用到的表数据。
outputTopic保存连接后的输出信息即打通了用户所有ID数据的IDMapping对象将其转换成JSON后输出。
buildTopology的第一步是构造表即KTable对象。我们修改初始的消息流以用户注册的手机号作为Key构造了一个中间流之后将这个流写入到rekeyedTopic最后直接使用builder.table方法构造出KTable。这样每当有新用户注册时该KTable都会新增一条数据。
有了表之后我们继续构造消息流来封装用户登录App之后的行为数据我们同样提取出手机号作为要连接的Key之后使用KStream的leftJoin方法将其与上一步的KTable对象进行关联。
在关联的过程中我们同时提取两边的信息尽可能地补充到最后生成的IDMapping对象中然后将这个生成的IDMapping实例返回到新生成的流中。最后我们将它写入到outputTopic中保存。
至此我们使用了不到200行的Java代码就简单实现了一个真实场景下的实时ID Mapping任务。理论上你可以将这个例子继续扩充扩展到任意多个ID Mapping甚至是含有其他标签的数据连接原理是相通的。在我自己的项目中我借助于Kafka Streams帮助我实现了用户画像系统的部分功能而ID Mapping就是其中的一个。
小结
好了我们小结一下。今天我展示了Kafka Streams在金融领域的一个应用案例重点演示了如何利用连接函数来实时关联流和表。其实Kafka Streams提供的功能远不止这些我推荐你阅读一下官网的教程然后把自己的一些轻量级的实时计算线上任务改为使用Kafka Streams来实现。
开放讨论
最后我们来讨论一个问题。在刚刚的这个例子中你觉得我为什么使用leftJoin方法而不是join方法呢小提示可以对比一下SQL中的left join和inner join。
欢迎写下你的思考和答案,我们一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@ -0,0 +1,168 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
加餐 搭建开发环境、阅读源码方法、经典学习资料大揭秘
你好,我是胡夕。
截止到现在专栏已经更新了38讲你掌握得怎么样了呢如果暂时掌握得不是很好也没有关系慢慢来有问题记得在留言区留言我们一起讨论。
今天我们来聊点儿不一样的。我总结了3个讨论热度很高的话题现在一一来为你“揭秘”。
如何搭建Kafka开发环境很多人对于编译和调试Kafka饶有兴致却苦于无从下手。今天我就给你完整地演示一遍搭建Kafka开发环境的过程。
如何阅读Kafka源码我曾经在专栏第1讲提到过我自己阅读Kafka源码的经历后来我收到很多留言问我是如何阅读的今天我就跟你分享一些阅读Kafka源代码的比较好的法则或者技巧。
Kafka的学习资料。幸运的是我在这方面还是有过一些总结的今天我会毫无保留地把资料全部分享给你。
Kafka开发环境搭建
现在我先来回答第1个问题如何搭建Kafka开发环境。我以IDEA为例进行说明Eclipse应该也是类似的。
第1步安装Java和Gradle
要搭建Kafka开发环境你必须要安装好Java和Gradle同时在IDEA中安装Scala插件。你最好把Java和Gradle环境加入到环境变量中。
第2步下载Kafka的源码
完成第1步之后下载Kafka的源码命令如下
$ cd Projects
$ git clone https://github.com/apache/kafka.git
这个命令下载的是Kafka的trunk分支代码也就是当前包含所有已提交Patch的最新代码甚至比Kafka官网上能够下载到的最新版本还要超前很多。值得注意的是如果你想向Kafka社区贡献代码通常要以trunk代码为主体进行开发。
第3步下载Gradle的Wrapper程序套件
代码下载完成之后会自动创建一个名为kafka的子目录此时需要进入到该目录下执行下面的这条命令主要目的是下载Gradle的Wrapper程序套件。
$ gradle
Starting a Gradle Daemon (subsequent builds will be faster)
> Configure project :
Building project 'core' with Scala version 2.12.9
Building project 'streams-scala' with Scala version 2.12.9
Deprecated Gradle features were used in this build, making it incompatible with Gradle 6.0.
Use '--warning-mode all' to show the individual deprecation warnings.
See https://docs.gradle.org/5.3/userguide/command_line_interface.html#sec:command_line_warning
第4步将Kafka源码编译打包成Jar文件
现在你可以运行下列命令将Kafka源码编译打包成Jar文件
./gradlew clean releaseTarGz
通常你需要等待一段时间经过一系列操作之后比如Gradle拉取依赖Jar包、编译Kafka源码、打包等你可以在core的build/distributions下面找到生成的tgz包kafka_2.12-2.4.0-SNAPSHOT。解压之后这就是一个可以正常启动运行的Kafka环境了。
第5步把Kafka源码工程导入到IDEA中
这也是搭建开发环境的最后一步。你可以先执行下面的命令去创建IDEA项目所需要的项目文件
$ ./gradlew idea #如果你用的是Eclipse,执行./gradlew eclipse即可
接着你需要打开IDEA选择“打开工程”然后再选择kafka目录即可。
至此我们就在IDEA中搭建了Kafka源码环境。你可以打开Kafka.scala文件右键选择“运行”这时你应该可以看到启动Kafka Broker的命令行用法说明如下图所示
总体来说Kafka工程自从由使用sbt改为使用Gradle管理之后整个项目的编译和构建变得简单多了只需要3、4条命令就能在本机环境中搭建测试开发环境了。
Kafka源码阅读方法
搭建好了开发环境下一步自然就是阅读Kafka源码并尝试自行修改源码了。下图是IDEA上Kafka工程的完整目录列表。
在这张图中,有几个子目录需要你重点关注一下。
coreBroker端工程保存Broker代码。
clientsClient端工程保存所有Client代码以及所有代码都会用到的一些公共代码。
streamsStreams端工程保存Kafka Streams代码。
connectConnect端工程保存Kafka Connect框架代码以及File Connector代码。
我之前说过Kafka源码有50万行之多没有重点地进行通读效率会特别低。最初我就是盲读源码的深感效果极差所以我觉得非常有必要为你推荐几条最佳实践。
我建议你先从core包读起也就是先从Broker端的代码着手。你可以按照下面的顺序进行阅读。
log包。log包中定义了Broker底层消息和索引保存机制以及物理格式非常值得一读。特别是Log、LogSegment和LogManager这几个类几乎定义了Kafka底层的消息存储机制一定要重点关注。
controller包。controller包实现的是Kafka Controller的所有功能特别是里面的KafkaController.scala文件它封装了Controller的所有事件处理逻辑。如果你想弄明白Controller的工作原理最好多读几遍这个将近2000行的大文件。
coordinator包下的group包代码。当前coordinator包有两个子packagegroup和transaction。前者封装的是Consumer Group所用的Coordinator后者封装的是支持Kafka事务的Transaction Coordinator。我个人觉得你最好把group包下的代码通读一遍了解下Broker端是如何管理Consumer Group的。这里比较重要的是GroupMetadataManager和GroupCoordinator类它们定义了Consumer Group的元数据信息以及管理这些元数据的状态机机制。
network包代码以及server包下的部分代码。如果你还有余力的话可以再读一下这些代码。前者的SocketServer实现了Broker接收外部请求的完整网络流程。我们在专栏第24讲说过Kafka用的是Reactor模式。如果你想搞清楚Reactor模式是怎么在Kafka“落地”的就把这个类搞明白吧。
从总体流程上看Broker端顶部的入口类是KafkaApis.scala。这个类是处理所有入站请求的总入口下图展示了部分请求的处理方法
你可以进到不同的方法里面去看实际的请求处理逻辑。比如handleProduceRequest方法是处理Producer生产消息请求的而handleFetchRequest方法则是处理消息读取请求的。
我们刚刚说的都是core代码包下的重要类文件。在客户端clients包下我推荐你重点阅读4个部分的内容。
org.apache.kafka.common.record包。这个包下面是各种Kafka消息实体类比如用于在内存中传输的MemoryRecords类以及用于在磁盘上保存的FileRecords类。
org.apache.kafka.common.network包。这个包不用全看你重点关注下Selector、KafkaChannel就好了尤其是前者它们是实现Client和Broker之间网络传输的重要机制。如果你完全搞懂了这个包下的Java代码Kafka的很多网络异常问题也就迎刃而解了。
org.apache.kafka.clients.producer包。顾名思义它是Producer的代码实现包里面的Java类很多你可以重点看看KafkaProducer、Sender和RecordAccumulator这几个类。
org.apache.kafka.clients.consumer包。它是Consumer的代码实现包。同样地我推荐你重点阅读KafkaConsumer、AbstractCoordinator和Fetcher这几个Java文件。
另外在阅读源码的时候不管是Broker端还是Client端你最好结合Java调试一起来做。通过Debug模式下打断点的方式一步一步地深入了解Kafka中各个类的状态以及在内存中的保存信息这种阅读方式会让你事半功倍。
Kafka推荐学习资料
如果你暂时对搭建开发环境或阅读源码没有兴趣但又想快速深入地学习Kafka的话直接学习现成的资料也不失为一个妙法。接下来我就向你推荐一些很有价值的Kafka学习资料。
第1个不得不提的当然就是Kafka官网。很多人会忽视官网但其实官网才是最重要的学习资料。你只需要通读几遍官网并切实掌握里面的内容就已经能够较好地掌握Kafka了。
第2个是Kafka的JIRA列表。当你碰到Kafka抛出的异常的时候不妨使用异常的关键字去JIRA中搜索一下看看是否是已知的Bug。很多时候我们碰到的问题早就已经被别人发现并提交到社区了。此时JIRA列表就是你排查问题的好帮手。
第3个是Kafka KIP列表。KIP的全称是Kafka Improvement Proposals即Kafka新功能提议。你可以看到Kafka的新功能建议及其讨论。如果你想了解Kafka未来的发展路线KIP是不能不看的。当然如果你想到了一些Kafka暂时没有的新功能也可以在KIP中提交自己的提议申请等待社区的评审。
第4个是Kafka内部团队维护的设计文档。在这里你几乎可以找到所有的Kafka设计文档。其中关于Controller和新版本Consumer的文章都很有深度我建议你一定要重点读一读。
第5个是著名的StackOverflow论坛。当今StackOverflow论坛对程序员意味着什么想必我不说你也知道。这里面的Kafka问题很有深度。事实上从仅仅是StackOverflow上的一个问题到最后演变成了Kafka的Bug修复或新功能实现的情况屡见不鲜。
第6个是Confluent公司维护的技术博客。这是Kafka商业化公司Confluent团队自己维护的技术博客里面的技术文章皆出自Kafka Committer之手质量上乘我从中受益匪浅。比如讲述Kafka精确一次处理语义和事务的文章含金量极高你一定要去看一下。
第7个是我自己的博客。我会定期在博客上更新Kafka方面的原创文章。有的是我对Kafka技术的一些理解有的是Kafka的最新动态。虽然不是国内质量最好的但应该是坚持时间最长的。毕竟我这个博客就只有Kafka的内容而且已经写了好几年了。
最后我给推荐你3本学习Kafka的书。
第1本是我的《Apache Kafka实战》我在里面总结了我这几年使用和学习Kafka的各种实战心得。这本书成书于2018年虽然是以Kafka 1.0为模板撰写的而Kafka目前已经出到了2.3版本,但其消息引擎方面的功能并没有什么重大变化,因此绝大部分内容依然是有效的。
第2本是《Kafka技术内幕》。我个人非常喜欢这个作者的书写风格而且这本书内容翔实原理分析得很透彻配图更是精彩。
第3本是2019年新出的一本名为《深入理解Kafka》的书。这本书的作者是一位精通RabbitMQ和Kafka的著名技术人对消息中间件有着自己独特的见解。
这些资料各有侧重,你可以根据自己的实际需求,选择相应的资料进行学习。
小结
好了我们来小结一下。在今天的文章里我跟你分享了很多经验比如如何搭建Kafka开发环境、如何阅读Kafka源码等希望这些经验可以帮你有效地节省时间避免走一些弯路。另外我把我收集到的相关学习资料全部列了出来分享给你也希望这些资料能够帮你更好地学习Kafka。
讲到这里,我想再强调一下,学习是个持续的过程。经验和外部帮助固然重要,但最关键的,还是自己要付出努力,持之以恒。
还是那句话Stay focused and work hard
开放讨论
最后我们来讨论这样一个问题你觉得学习Kafka或者任何一种技术最重要的是什么
欢迎写下你的思考和答案,我们一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@ -0,0 +1,69 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
用户故事 黄云:行百里者半九十
你好我是黄云目前是一名后端开发工程师坐标苏州已经工作三载有余了。在“Kafka核心技术与实战”专栏刚刚上线的时候我就第一时间订阅了。在学习专栏的几个月时间里我有许多心得体会想要跟你分享一下。
为什么要学习“Kafka核心技术与实战”专栏
众所周知在当今时代“大数据”已经成为了一个家喻户晓的概念。动辄GB级、TB级甚至是PB级的大数据都需要服务器在短时间内进行处理。如何充分利用每台服务器的性能削峰填谷就成为了数据处理的关键。Kafka作为一种主流的消息队列工具已经被很多大公司使用。掌握Kafka成为了大数据时代必不可少的专项技能。
我们公司目前也在用Kafka对接实时流量数据数据量约20GB/天。作为一名开发工程师我必须要尽快掌握Kafka的核心技术修炼专项技能才能使其更好地为我所用。一直以来在工作之余我都会主动地搜集Kafka的相关资料看各位大佬的分享。但是那些零散的碎片拼凑在一起却始终很难形成一个系统的知识体系这也是一直让我很头疼的事情。
“道”与“术”的平衡
作为一名程序员我和很多人都一样看到好的东西就想动手实践。在刚刚了解到Kafka的强大功能之后我就迫不及待地参照Kafka官网的教程着手搭建Kafka的服务器。除此之外我还曾小试牛刀在1万条/s的实时环境对接中使用了Kafka作为消息队列。
但是,在实际使用过程中,我发现了一个严重的问题,那就是,我只知道如何去用,但不知道如何用好。意识到这个问题后,我赶紧查看了官网以及很多主流网站的文章。官网上将几百个参数一一列出,并注明含义,其他资料则普遍重理论,轻实践。总体来看,这些内容都将“道”放在了第一位,却完全忽略了“术”的实现。
说实话,我们程序员看到这些文章,其实是很恼火的。盯着满篇的原理,却完全不知道从何处下手,那就注定无法解决我的实际问题。
直到有一天我无意中发现胡夕老师的专栏“Kafka核心技术与实战”在极客时间上线了顿时有一种如获至宝的感觉。看完课程简介和开篇词之后我觉得这就是我一直期待看到的课程于是就立即订阅了。从此以后每周二、四、六准时学习更新的文章逐渐成为了我的习惯。
在学习过程中我印象最深刻的是胡夕老师在讲参数配置的时候在第7讲和第8讲的标题中用了3个“最”字即“最最最重要的集群参数配置”并且在文章里分享了最重要、最核心的集群参数配置。看完这两讲之后我一直以来持有的对于“Kafka配置优化过程中怎么有这么多参数”的怨念才得以消解。不必贪多求全只要掌握最核心的内容就可以了。胡夕老师的专栏真正做到了“道”与“术”的平衡慢慢地为我揭开了Kafka的神秘面纱。
我是怎么学习专栏的?
说实话查看极客时间的专栏是一件十分愉悦的事情。每天清晨正式开始工作前的20分钟是我固定的学习专栏的时间。学习Kafka专栏时注意力必须高度集中因为里面干货非常多。如果只是粗浅阅读不加思考就会留下很多问题。
当我遇到困惑的时候,第一件事就是查看文章下面的评论。很多人的总结概括能力都非常强,能对信息进行二次加工和提炼,这些留言帮助我对文章内容进行了二次梳理。最重要的是,我经常在留言里看到和我一样的困惑,瞬间就有种找到同类的感觉,这也是一种很奇妙的体验。
胡夕老师回复也很及时,第一时间解决我们的疑惑。今日疑,今日解,不积攒遗留问题,也能让我更加高效地学习后面的内容。
在学习专栏时,我走过一些弯路,也在不断调整地自己的学习方法和习惯。在这个过程中,我吸取了一些经验,也有很多的感悟,想要跟你分享一下。
多思考,多总结。每次查看留言区评论的时候,我都会先自己思考一下问题的答案,等老师回复之后,我会对照着确认下答案。很多时候,我在留言区看其他人的总结,总觉得言简意赅,但是自己描述的话,就会发现自己完全没有这种抽象概括的能力。换句话说,就是没有办法用自己的话描述出文章表达的含义,其实也就是没有真正地理解文章的内容。所以,我就一直跟自己强调,要多总结,提升自己的概括能力。
多动手实践,实践出真知。专栏中有很多实践的知识,我每次发现可操作性比较强的内容时,就会自己在搭建的测试环境下进行实践,查看实践结果。这样一来,我就对课程的内容有了更好的理解与掌握。
多回头看,不留疑惑。我发现,专栏里的很多重点内容,我只读一遍的话,印象会不太深刻,但隔一段时间再去回顾,就会有很多新的认知和收获。有些章节满篇都是干货,一旦遇到不懂的术语,大脑就会本能地跳过。在意识到大脑的这个惯性动作之后,我再遇到不懂的问题时,就会主动花时间去查询相关资料,直到理解术语的含义。否则,一知半解地往下读,对后面的理解会有很大的影响。
多记笔记。对于一些我很感兴趣的课程内容,我会有针对性地进行记录。在记录时,我一般不会直接复制粘贴原文,而是用自己的话写出来。如果实在写不出来,就把原文摘抄一遍,这样也会加深理解。
多提问。针对一些自己实在无法理解的问题,我就让自己多提问、多留言。每次老师回答完我的问题,我都觉得非常开心,后续的学习劲头也会更高,这可能就是老师经常提到的提高参与感吧。
关于留言这件事情,我想跟你分享一下我曾经的心理变化。刚开始学习时,遇到不懂的问题,其实我是不敢提问的,因为我担心我之所以不懂这个问题,是自己理解能力有问题,怕提问的问题过于低级,只好藏着。但是后来我发现,越是这样,我越不懂,慢慢就丧失了很多学习热情。意识到这一点之后,我就及时调整了学习策略,不断提醒自己:对于思考之后实在不能理解的问题,一定要多问。不藏拙,才是成长的最佳路径。
学习专栏有什么收获?
学习这个专栏我最大的收获就是对Kafka的原理、机制以及参数的理解更加深入了。我很佩服胡夕老师的抽象概括能力。胡夕老师花费了一年多的时间阅读Kafka的源码以及相关文献但最后呈现给我们的并不是长篇大论而是经过多次抽象总结的内容。老师从我们用户最关心的入门、配置、客户端、内核、监控这几个点去介绍Kafka的内容将官方文档的上百个参数精简成十几个重要的参数将几百页的内容抽象成几十讲的核心知识。
这一点让我很受启发。在学习过程中,追求大而全并不好,我们只需要掌握实际生产环境中最常用的那么十几个参数,保障消息传输稳定、快速、不丢失就可以了。很多很深奥的内容,如果精力和时间允许的话,再去深究,要以终为始,不能钻牛角尖。
可能只有程序员才知道阅读大段的源码是多么痛苦的事情但胡夕老师做到了而且还在“加餐”中分享了他阅读源码的方法。令人欣喜的是胡夕老师是直接将源码在IDE中展示出来并且对着实际代码描述阅读代码的方式实操性很强。而这一点其实正好符合我当初选择学习Kafka专栏的预期。专栏里的大部分内容都是可以落地实践的。勿在浮沙筑高台胡夕老师就是这样一步一步地带着我们稳步前进的。
总结
专栏不知不觉就结束了比较惭愧的是我后半段没能跟上老师的节奏做到更新1讲阅读1讲往往是堆积了好久才去阅读。但是我真的很感谢胡夕老师的付出老师在专栏里分享的内容让我对Kafka的参数以及一些实战性很强的知识形成了自己的理解。
胡夕老师在专栏中提过一句话我印象非常深刻“行百里者半九十”Kafka的学习之旅不会随着专栏的完结而停止只是读完专栏的内容其实才走了旅程的一半而已。后续的话针对专栏中提到的落地信息我会一一记录并实践对于Kafka真正做到能懂、会用、能用好。
好了,我的分享就到此结束了。你在学习专栏的过程中,有没有什么独特的学习方法和心路历程呢?欢迎你写在留言区,我很期待能跟你在这里交流、讨论,一起学习进步。

View File

@ -0,0 +1,63 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
结束语 以梦为马,莫负韶华!
你好我是胡夕。今天我的专栏“Kafka核心技术与实战”就正式结束了。
回顾与你在专栏相聚的这几个月我的内心充满了成就感。且不必说这42讲的文字全是我一字一键敲下来的也不必说那长达十几个小时的录音单是留言区那些踊跃积极的提问与讨论就足以使我深受感动并收获满满了。
此时此刻,千言万语汇成一句话:“感谢!”感谢你对我和本专栏的支持,感谢你曾经的鼓励与提问,也感谢你的肯定与期望。另外,我也要向你表示祝贺,祝贺你完整地学习了专栏的全部课程,你的恒心与坚持令人敬佩。
虽然专栏更新结束了但是我相信我们的Kafka学习之旅不会结束。相反这对于你来说或许是一个新的开始。
还记得开篇词里的那句话吧“Stay focused and work hard”我一直觉得学习任何技术甚至是搞定任何事情只要下足了功夫理论上你可以藐视一切学习方法或捷径。但是如果你忽视了毅力和坚持再多的速成教程也无法引领你达到你期望的高度。著名的“10000小时定律”就明确表示10000个小时的锤炼是所有人从平凡人变成世界级大师的必要条件。
还是那句话,只要你持之以恒地投入时间去学习,你就能成为某个领域的专家。因此,从某种意义上说,我这碗“鸡汤”的配料非常简单,就四个字:干就完了。
那这是不是在说书籍、专栏之类的他人智慧总结就没用了呢?当然不是!他山之石,可以攻玉,书籍和专栏的最大作用就在于,当你遇到岔路口时,它们能够帮助你快速地识别前进中的已知路障,让你少走弯路,更快地实现目标。但前提是你要在路上,而不是单纯地想要依赖它们速成。
在专栏的最后我想再和你分享一些学习大数据框架的个人经验。这些经验不仅仅适用于学习Kafka对于其他框架甚至是分布式系统的学习都是适用的。
首先最重要的就是夯实技术基本功。这是我们IT从业者赖以生存的基石。
这里的基本功包含很多方面比如操作系统、数据结构等但我更想说的还是对Java语言的掌握。
目前大数据框架多是以Java或JVM系语言开发而成的因此熟练掌握甚至精通Java是学好大数据框架的基石所谓精通不仅仅是要求你熟练使用Java进行代码开发更要求你对JVM底层有详细的了解。就这个层面的学习而言我想给你3条建议。
持续精进自己的Java功底。比如你可以去Java官网上把Java语言规范和JVM规范熟读一遍。很多人都不太重视语言规范文档但实际上Java中关于线程和同步的知识在Java语言规范中都有相关的阐释。
提升自己的Java多线程开发以及I/O开发能力。很多大数据框架底层都大量使用Java多线程能力以及NIO帮助实现自身功能。就拿Kafka来说多线程自不必说Kafka可是大量使用NIO实现网络通信的。所以这部分的知识是你必须要熟练掌握的。
掌握JVM调优和GC。我推荐你去读一读“Java Performance”这本书。虽然目前GC收集器大部分演进到了G1时代但书中大部分的调优内容依然是适用的。调优Kafka的JVM也要依赖这部分知识给予我们指导。
除此之外,你还要学习分布式系统的设计。
分布式系统领域内的诸多经典问题是设计并开发任何一款分布式系统都要面临和解决的问题比如我们耳熟能详的一致性问题、领导者选举问题、分区备份问题等。这些问题在Kafka中都有体现我们在专栏里面也有所涉及。因此分布式系统的诸多基础性概念是帮助你日后深入掌握大数据分布式框架的重要因素。
而且,很多经典的分布式问题在业界早已被研究多年,无论是理论还是实践案例,都有着翔实的记录。比如我们在专栏前面谈到的分区概念,分区在分布式系统设计中早就不是什么新鲜的概念了,早在上世纪六七十年代,就已经有行业专家在研究分区数据库的实现问题了。要较好地掌握大数据框架中的分区或分片,是不可能绕过分布式系统中的分区以及分区机制的。
这些经验都偏重理论的学习。你千万不要小看理论的价值,毕竟,列宁说过:“没有革命的理论,就没有革命的运动。”这里的“运动”就是一种实践。先让理论指导实践,再借助实践补充理论,才是学习任何东西无往而不利的最佳法则。
强调完理论自然就要引出实践了。我这里所说的实践不仅仅是对框架的简单使用。你从官网上下载Kafka启动它然后创建一个生产者和一个消费者跑通端到端的消息发送这不叫实践这只是应用罢了。真正的实践一定要包含你自己的思考和验证而且要与真实业务相绑定。我不排斥你单纯地学习某个框架但以我个人的经验而言在实际工作中进行学习往往是学得最快、掌握得也最扎实的学习方式。
另外,在实际学习过程中,你最好记录下遇到问题、解决问题的点点滴滴,并不断积累。要知道,很多技术大家之所以成为技术大家,不仅仅是因为理论掌握得很牢固,填过的“坑”多,更重要的是,他们不重复犯错。
孔子曾经称赞他的学生颜回“不贰过”。“不贰过”也就是不重复犯错。在我看来,在实践方面,一个不犯相同过错的人,就已经可以被称为大家了。
好了,这些就是我在学习技术的过程中总结的经验,现在全部分享给你,希望对你有所帮助。
我为你准备了一份结课问卷,题目不多,两三分钟就可以完成。希望你能畅所欲言,把自己真实的学习感受和意见表达出来,我一定会认真看,期待你的反馈。
](https://jinshuju.net/f/FFQ5PT)
最后一期了,也欢迎你在这里给自己的专栏学习做个总结。关于专栏或是其他内容,如果你还有什么问题,也尽管提出来,我会持续回复你的留言。
送君千里,终须一别。所谓行百里者半九十,愿你继续在技术这条道路上深耕,并得偿所愿,不负韶华。我们江湖再见!