消息队列:从 Kafka 到 Pulsar 的进阶之路

1,734 阅读8分钟

随着用户规模迅速扩张,业务场景越来越多,对消息产品功能要求高,如死信队列、延迟队列、 消费重置、消息查看等;写消息的延迟和稳定性要求也越来越高。 如有相对更加丰富的功能,可以节省很多时间,同时提升业务开发效率。

Kafka 方案及其痛点

之前,我们采用 Apache Kafka 作为消息平台, 为了让业务在高峰期(晚上八点到十点)不受影响,我们根据消息业务量的大小, 分别搭建了不同的集群。对于一些业务场景的需求, 比如需要重置 offset 来消费过去几天的消息,使用 Kafka 需要停掉消费者才可以进行, 这种方式对大量在线业务非常不利,只能采用重写消息或者一些不太灵活的方式来实现, 极大降低了使用体验。

我们在使用 Kafka 集群过程中,主要遇到以下问题:

  1. Kafka 没有租户概念,需要手动维护多个集群,不方便运维。
  2. Kafka 集群扩容后需要做 Reassign Partitions,IO 消耗大。
  3. Kafka 监控体系不完善,排查问题较为繁琐。
  4. 在线业务消息重置不方便,实现起来较为麻烦,需要停掉消费组。
  5. Kafka 不支持死信队列和延迟队列。
  6. Kafka 没有官方维护和支持的 Go 语言客户端。
  7. 在 Kafka 中支持 schema,需要引入额外组件,不方便维护。

为什么选择 Pulsar

“伴鱼关注新技术,期望通过创新的基础组件选型,打造更加稳定、高效的技术体系,实现技术赋能业务的目的。– 徐成选,伴鱼基础架构负责人”

在 Pulsar 毕业成为 Apache 顶级项目时,我们有关注过 Pulsar。Pulsar 社区非常活跃,给我们留下了深刻的印象。 在使用 Kafka 遇到各种问题后,我们又进一步调研了 Pulsar。

  • Pulsar 采用云原生的架构,存储和计算分离。
  • Pulsar 支持多租户,我们可以按照不同的业务线、业务小组和对应的服务级别来管理消息保存时间、持久化、堆积清除策略等,统一维护一套 Pulsar 集群。
  • Pulsar 支持灵活的水平扩容。当存储不够时,直接增加 bookie 进行扩容,不会对用户产生任何影响。
  • Pulsar 自带监控体系,broker,bookie 相关指标清晰,方便快速定位问题,给出解决方案。
  • Pulsar cursor 方便重置消息,给业务带来很好的体验。
  • Pulsar 支持死信队列和延迟队列。
  • Pulsar schema 集成在 broker 中,不需要引入单独的组件。Golang client 支持 schema,减少了维护成本。

Pulsar 在伴鱼的实践应用

目前我们在生产环境接入指标为 50+ topics,10+ namespaces,接入了 AI 课、每日学、成长圈等业务,日志上报服务在接入 Pulsar 后性能出色。 启用双写的过程中,Pulsar 在高峰期业务的稳定性优于 Kafka,延迟波动较小。现在新申请的 topic 都已经使用 Pulsar 接入, 结合我们内部已有的消息队列管理平台集成进 Pulsar,统一进行管理。伴鱼内部还有一些其他业务也想从 Kafka 迁移到 Pulsar 集群, 我们提供双写和双读的方案,通过配置来动态切换,实现 Kafka 到 Pulsar 的迁移。

下面我和大家详细介绍下 Pulsar 在伴鱼内部实践过程中的解决方案和应用。

部署方式

我们在生产环境部署了一个 Pulsar 集群,采用 3 个 bookie + 3 个 broker 的配置,JVM 内存分配方式如下:

  • OS: 1 ~ 2 GB
  • JVM: 1/2
    • heap: 1/3
    • direct memory: 2/3
  • PageCache: 1/2

通过两个 pulsar-proxy 来保证 proxy 的可用,同时 proxy 上方挂了 SLB 来做负载均衡。以下是伴鱼 Pulsar 集群的架构图。

平台管理

在平台管理方面,我们打造了一体式的管理平台,集成了 Pulsar、Kafka 和内部延迟队列, 提供了 topic 申请、订阅申请、消息查看、重置、监控、告警功能,大大增强了对消息队列的控制力度和可观测性。 业务同学使用的 topic 均需登记记录。在替换过程中,通过限制平台新创建的 topic 类型,从而逐步替换到 Pulsar。

申请后下发配置到配置管理平台,便可直接在程序中使用,提高业务开发效率。 在客户端参数配置设置方面,我们使用了 Apollo 管理 broker 地址、reader、writer 相关参数配置并在 SDK 中进行封装,相关参数统一配置在配置中心。 同时监听相关 topic 配置变更,在不重启服务的情况下,对参数配置进行热更新。这种方式对业务使用非常友好。下面是我们的内部平台架构图。

如下是我们的 topic 管理图。

客户端适配

引入 Pulsar 后,我们对内部已有的 Go client 进行了二次包装,引入实例管理,接入内部链路追踪,增加客户端的打点统计,同时结合内部环境进行泳道隔离。

链路追踪

为了增加调用链路的可观测性和故障排查时快速定位问题的能力,我们基于内部 jaeger 对读写操作进行了追踪,使写消息方能够清晰的看到消息被谁消费, 消费方也可以看到消息由谁写入。

我们使用 golang 的 context,从 context 中取出 span 信息,利用 jaeger 可以跨进程传递的 carrier,将消息体、header 信息、 内部泳道信息包装为 payload 进行传递。消费端接收后取出相关信息,同时放入 context 中进行后面的传递,这样链路追踪比较准确,并且携带一些自定义的信息。以下是一个追踪样例。

客户端打点统计

在 Pulsar 暴露出服务端统计的相关耗时和吞吐后,我们将客户端比较关心的、监测 SDK 的一些关键指标暴露给 Prometheus,并通过 Grafana 展示, 这样能够反应出客户端的一些状态,同时对比客户端和服务端的区别和问题,更为准确和迅速。

目前,我们根据使用 topic 的不同服务,将打点按照服务维度统计,使服务维护者能够精确定位服务关联 topic 中的问题。在初期切换过程中, 我们对一个服务同时进行 Pulsar 写和Kafka 写时,在客户端统计打点 P99 延迟上有比较明显的对比,具体打点对比如下(上图为Pulsar 写,下图为 Kafka 写)。

泳道隔离

在实际开发过程中,如果有多个需求,多位负责人使用同一个 topic 进行测试开发,他们生产的数据不同且消费逻辑不同,就会给测试带来不稳定性。

基于这样的背景,我们在伴鱼内部发布环境中引入泳道概念,泳道之间相互隔离,并行开发,互不影响。比如测试环境有 4~5 条泳道,这样在并行开发时,写消息逻辑部署在哪个泳道,消息会指定写到对应泳道的 topic,也只有在对应泳道上才能消费。这样在读写消息队列时,做到泳道隔离,避免不同的读写逻辑互相干扰,提高研发测试效率。关于内部泳道的设计,从前端到网关再到 API 层, 层层之间携带泳道信息。在关于消息队列这块的处理逻辑中使用了 golang 的 context,通过 context 传递泳道信息。 在 SDK 实例管理中,我们维护了一个实例池,通过不同的 instance_key 做区分。对于相应泳道的请求,取出对应 key 的 reader 和 writer 实例进行读写。

相信开源的力量

在使用 Pulsar 的过程中,我们遇到一些问题,特别感谢 StreamNative 各位小伙伴的大力支持,尤其是鹏辉大佬耐心、热情的解答。

从最初使用 Kafka 遇到各种功能和性能问题,到我们慢慢熟悉 Pulsar 并在伴鱼测试、部署 Pulsar,接入平台管理、SDK 开发,到目前 10+ 业务线接入 Pulsar,我们和社区一起见证了 Pulsar 的快速发展。在生产环境中使用 Pulsar 后,我们更是坚信开源 Pulsar 的力量。

未来我们会进一步扩大 Pulsar 的业务应用范围,用 Pulsar 替换内部的延迟队列,提供更加安全可靠、更健全的监控系统。同时,我们也在考虑引入 Pulsar Functions,接入内部实时计算平台,对数据做函数式计算。

每一种选择,都需要付出艰辛的努力,在未来业务进一步发展中,我们还会遇到很多挑战,有了 Pulsar 社区的支持,我们坚信一定会把事情做得更好。我们会和 Pulsar 社区一起成长,并为 Pulsar 社区的发展添砖加瓦!