前言这段时间一直在做MQ(Pulsar)相关的治理工作,其中一部分内容是关于消息队列的升级,比如:创建测试集群单击一下。运行一批测试用例,覆盖我们在线使用的功能,输出测试报告。模拟压力测试并输出测试结果。本质目的是思考在新版本升级之前和升级之后对现有业务是否有影响。一键创建集群和执行测试用例比较简单,只需要使用helm的SDK和k8sclient连接整个流程即可。压力测试其实稍微麻烦一点。Pulsar官方提供了压测工具;但是功能比较简单。它只能对某一批topic进行极压测试,最后输出测试报告。最后参考官方压测流程,增加了一些实时监控数据,方便分析整个压测过程中的性能变化。客户端超时随着压测时压力的增加而增加,比如压测时间增加,线程数增加,客户端发送消息会抛出超时异常。org.apache.pulsar.client.api.PulsarClientException$TimeoutException:生产者pulsar-test-212-20无法在给定的超时时间内向主题persistent://my-tenant/my-ns/perf-topic-0发送消息:createdAt82.964秒前,firstSentAt8.348秒前,lastSentAt8.348秒前,retryCount1并且在生产业务环境高峰期偶尔会出现该异常,会导致业务数据丢失;所以这次恰好被我转载了然后想想分析原因和解决办法。源码分析既然是客户端抛出的异常,那我们先来看异常点。其实整个过程和起因并不复杂,如下图所示:客户端过程:客户端生产者发送消息时,首先将消息发送到本地的一个挂起队列。当broker完成处理(写入bookkeeper)并返回ACK时,删除pending队列头部的消息。后台启动定时任务,定时扫描队头消息(队头最后写入的消息)是否过期(过期时间可配置,默认30s)。如果已经过期(头消息过期,说明所有消息都过期),则遍历队列中的消息依次抛出PulsarClientException$TimeoutException,最后清空队列。Serverbroker进程:收到消息后,调用bookkeeperAPI写入消息。在写消息的时候,同时写一个回调函数。写入成功后,执行回调函数。这时会记录一条消息的写延迟,通知客户端ACK。写延迟可以通过broker指标索引pulsar_broker_publish_latency获取。从上面的过程可以看出,如果客户端不采取自下而上的措施,就会在第四步发生消息丢失。这种消息本质上不是broker丢失消息,而是client认为当时broker的处理能力已经达到了上限。考虑消息实时性能因此丢弃未发送的消息。性能分析通过上面的分析,尤其是broker的写入过程,我们知道整个写入的主要操作是向bookkeeper进行写入,所以bookkeeper的写入性能关系到整个集群的写入性能。极端情况下,假设不考虑网络丢失,如果bookkeeper的写延迟为0ms,那么整个集群的写性能几乎是无限的;所以我们在压测的时候重点关注bookkeeper的各项指标。首先,CPU就是CPU:从图中可以看出,在压力测试的时候CPU有明显的增加,那么我们需要找出压测的时候,bookkeeper的CPU损失最多的地方在哪里?这里不得不吹一波阿里的arthas工具,可以非常方便的帮助我们生成火焰图。分析火焰图最简单的方法之一是查看顶部最宽的函数,这很可能是性能瓶颈。这个图上面没有明显的宽函数,大家都差不多,所以没有明显的耗CPU的函数。此时借助云厂商的监控,得知CPU的上限还没有拿到(限制为8核)。在使用arthas的过程中还有一个小坑。在k8s环境下,有可能应用启动后无法成功将pid写入磁盘,导致无法查询到Java进程。$java-jararthas-boot.jar[INFO]arthas-boot版本:3.6.7[INFO]找不到java进程。尝试在命令行中传递。请选择一个可用的pid。这个时候可以直接ps获取进程ID,然后在启动的时候直接传入pid。$java-jararthas-boot.jar1正常pid为1,既然CPU没有磁盘问题,那我们看看是不是磁盘瓶颈。可以看出压测时的IO等待时间明显高于日常请求。为了最终确认是否是磁盘问题,将磁盘类型改为SSD进行测试。果然,即使在压力测试中,SSD盘的IO延迟也低于普通硬盘的正常请求周期。现在磁盘IO延迟降低了,按照前面的分析,整个集群的性能理论上应该会有明显的提升。因此对比升级前后的消息TPS写入指标:升级后每秒写入速率从40k提升到80k左右,几乎翻了一番(果然花钱是解决问题最快的方法);但是即便如此,在极压测试之后还是会出现客户端超时,因为无论服务器如何提高处理性能,仍然没有办法做到无延时写入,每一个环节都会有损失。升级过程中还有一个超时的关键步骤是必须覆盖的:模拟集群升级对生产端大量生产者和消费者访问和收发消息时对客户端业务的影响。根据官方推荐的升级步骤,流程如下:升级Zookeeper.Disableautorecovery.升级Bookkeeper.UpgradeBroker.UpgradeProxy.Enableautorecovery。其中最关键的是升级Broker和Proxy,因为这两个是客户端直接交互的组件。本质上,升级的过程就是优雅的关闭,然后使用新版本的docker启动;因此,客户端必须感知到Broker已离线,然后重新连接。如果能快速自动重连,对客户端影响不大。在我的测试中,大约2000个生产者以1k的发送速率发送消息,所有组件在30分钟内升级。整个过程中,客户端会自动快速重连,不会出现异常和消息丢失的情况。一旦发送频率增加,在重启Broker的过程中会出现上述超时异常;最初似乎在默认的30s时间内没有成功重连,导致超时的消息积压。分析源码后发现关键步骤如下:客户端在与Broker的长期连接断开后会自动重连,重连到哪个Broker节点由LookUpService处理,由LookUpService处理根据主题使用的元数据获得。从理论上讲,如果这个过程足够快,它对客户端的敏感度就会降低。元数据包含绑定到主题所属的bundle的Broker的特定IP+端口,以便它可以重新连接和发送消息。Bundle是对一批topic的抽象,用于将一批topic与Broker绑定。当一个Broker宕机时,它的所有bundle都会被自动卸载,负载均衡器会自动将它们划分到在线的Broker中进行处理。LookUpServive获取元数据的速度会降低有两种情况:因为所有的Broker都是有状态节点,所以升级时从新节点开始升级,假设是broker-5,假设升级节点的bundle被切掉后正在转移到broker-4,此时client会自动重新连接到broker4。此时client正在重新发送累积的消息,下一个升级的节点正好是4,那么client就得等待bundle成功卸载到新的节点上。如果刚好是3,就得重新套娃,这样整个消息的重传过程就会加长,直到超过等待时间,就会超时。另一种情况是bundle数量比较多,会增加上面提到的卸载时更新元数据到zookeeper的时间。所以我在考虑是否可以在Broker升级过程中先将卸载的bundle绑定到Broker-0上,等所有升级成功后再做负载均衡,尽量减少客户端重连的几率。解决方案如果要解决这个超时异常,也有以下解决方案:将bookkeeper盘更换为写入延迟更低的SSD,以提高单节点性能。添加bookkeeper节点,但是因为bookkeeper是有状态的,所以横向扩展比较麻烦,而且一旦扩展也很难收缩。增加客户端写入的超时时间,可以配置。客户端要做好自下而上的措施,比如捕捉异常,记录日志,或者存入仓库,稍后再重新发送消息。添加簿记员写入延迟的警报。Spring官方的Pulsar-starter内置了producer相关的metrics,客户端也可以对此进行监控和告警。实现上述目标的最佳方法是第四步。效果好,成本低。建议未实现的尽快尝试捕获。整个测试过程花了我一两周的时间,第一次全面测试一个中间件,收获颇丰;无论是源码还是架构,都对Pulsar有了更深的理解。