导读:在 4 月 11 日 TGIP-CN 直播活动上,我们邀请到 StreamNative 工程师徐昀泽,他为大家分享了 KoP 2.8.0 新特性前瞻。下面是徐昀泽分享视频的简洁文字整理版本,供大家参考。
在 4 月 11 日 TGIP-CN 直播中,来自 StreamNative 的软件工程师徐昀泽为大家带来了《KoP 2.8.0 功能特性预览》的分享。下面是其分享视频的简洁文字整理版本,敬请参考。
今天分享的内容是《KoP(Kafka on Pulsar)2.8.0 新特性前瞻》,首先我简单自我介绍下:我就职于 StreamNative ,是 Apache Pulsar 的 Contributor,也是 KoP 的主要维护者。
关于 KoP 版本号规范
首先,我们先聊一聊 KoP 版本号的问题。
Apache Pulsar 拥有 Major release。因为 KoP 前期版本管理比较混乱,所以从 2.6.2.0 开始,KoP 的版本号跟 Pulsar 基本保持一致。KoP Master 分支会不定期的更新依赖的 Pulsar 版本。这样的话,如果想要有一些新的功能可以在 Pulsar 中提一个 PR,然后 KoP 去依赖这个方法就行了。KoP 2.8.0 是一个可以适用于生产的一个版本。
今天我主要从如下主要四点为大家展开本次的直播:
- 为什么需要 KoP
- KoP 的基本实现
- KoP 2.8.0-SNAPSHOT 版本的近期进展
- 近期计划及未来展望
Kafka Vs Pulsar
首先说一下我对 Kafka 和 Pulsar 这两个系统的看法,抛开一些杂七杂八的 feature 的话,这两个系统还是很相似的,但最大的区别是它们的存储模型。
Kafka 的 Broker 是兼具计算和存储的。所谓计算是指把 Client 发过来的数据抽象成不同的 topic 和 partition,可能还会做一些 schema 等等之类的处理,处理后消息会写到存储上。Kafka 的 Broker 处理完会直接写到这台机器上的 file system。而 Pulsar 却不同,它会写到一个 Bookie 集群,每个 Bookie 节点是对等的。
分层存储带来了很多好处,比如你想增加吞吐量可以加一台 Broker;如果想增加磁盘容量的话,可以增加 Bookie,而且由于它的每个节点是对等的,所以是无需进行 rebalance 也没有 Kafka 的 Leader 的 Follower 之分。当然这不是本次 Talk 的重点,我想表达的是它们架构上最大的差异就是 Kafka 写入本地文件,Pulsar 写入 Bookie。
另一方面虽然我认为两者没有绝对的优劣,但是大家有选择的自由。我相信有很多场景都是可以用 Pulsar 来代替 Kafka 的。
从 Kafka 迁移到 Pulsar
如果我看中的 Pulsar 的一些优点,想从 Kafka 迁移到 Pulsar,那么会遇到什么问题呢?
- 推动业务更换客户端?
- 业务说太麻烦,不想换。
- Pulsar adaptors?(Pulsar 推出了一个 adaptor,Kafka 的代码无需改变要改一下 maven 的依赖即可)
- 看起来不错,可惜我不是用的 Java 客户端。
- 我不嫌麻烦,但我只会 PHP。
- 用户直接使用了 Kafka 连接器(近百种)连接到外部系统怎么办?
- 用户使用外部系统的连接器连接到 Kafka 怎么办?
KoP(Kafka on Pulsar)
面对上述从 Kafka 迁移到 Pulsar 的多种问题,KoP(Kafka on Pulsar)项目应运而生。KoP 将 Kafka 协议处理插件引入 Pulsar Broker,从而实现 Apache Pulsar 对原生 Apache Kafka 协议的支持。借助 KoP,用户不用修改代码就可以将现有的 Kafka 应用程序和服务迁移到 Pulsar,从而使用 Pulsar 的强大功能。关于 KoP 项目的背景,可以了解 KoP 相关资料,这里不再赘述。
如上图,从 Pulsar 2.5.0 开始引入 Protocol Handler,它运行在 Broker 的服务之上。默认的是 Pulsar Protocol Handler 其实只是一个概念,它是与 Pulsar 的客户端进行通信。Kafka Protocol Handler 是动态加载的,配置好相当于加载了一层插件,通过这个插件与 Kafka 客户端进行通信。
KoP 的使用非常简单,只需将 Protocol Handler 的 NAR 包放入 Pulsar 目录下的 protocols 子目录,对 broker.conf 或 standalone.conf 添加相应配置,启动时就会默认启动 9092 端口的服务,与 Kafka 类似。
目前来说,KoP 支持的客户端:
- Java >= 1.0
- C/C++: librdkafka
- Golang: sarama
- NodeJS:
- 其他基于 rdkafka 的客户端
Protocol Handler
Protocol Handler 其实是一个接口,我们可以实现自己的 Protocol Handler。Broker 的启动流程:
从目录下加载这个 Protocol Handler,再去加载 Class,用 accept
方法和 protocolName
方法来验证,然后就是按部就班的三步:
- initialize()
- start()
- newChannelInitializer()
第一步,加载 Protocol Handler 的配置。Protocol Handler 与 Broker 共用同一配置,所以这里用的也是 ServiceConfiguiation。Start 这一步就最重要的因为它传入了 BrokerService 这个参数。
BrokerService 掌控每个 Broker 的一切资源:
- 连接的 producers,subscriptions
- 持有的 topic 及其对应的 managed ledgers
- 内置的 admin 和 client
- …
KoP 的实现
Topic & Partition
Kafka 与 Pulsar 很多地方都很相似。Kafka 里面 TopicPartition 是一个字符串和 int;Pulsar 稍微复杂一些,分了如下部分:
- 是否持久化
- 租户
- 命名空间
- 主题
- 分区编号
KoP 中有这三项配置:
- 默认租户:kafkaTenant=Public
- 默认命名空间:kafkaNamespace=default
- 禁止自动创建 non-partitioned topic:allowAutoTopicCreationType=partitioned
为什么要配一个禁止自动创建 non-partitioned topic 的配置呢?因为 Kafka 中只有 partitioned topic 的概念而没有这个 non-partitioned topic 的概念。如果用 Pulsar 客户端去自动创建一个 topic,可能导致 Kafka 的客户端无法访问这个 topic。在 KoP 里面做一些简单的处理,将默认的租户跟命名空间独立映射。
Produce & Fetch 请求
PRODUCE 请求:
- 通过 topic 名字找到 PersistentTopic 对象(内含 ManagedLedger)。
- 对消息格式进行转换。
- 异步写入消息到 Bookie。
FETCH 请求:
- 通过 topic 名字找到 PersistentTopic 对象。
- 通过 Offset 找到对应的 ManagedCursor。
- 从 ManagedCursor 对应位置读取 Entry。
- 对 Entry 格式进行转换后将消息返回给客户端。
Group Coordinator
Group Coordinator 是用来进行 rebalance,决定 partition 和 group 的映射关系。因为 Group 会有多个消费者,消费者会访问哪些 partition,这个就是由 Group Coordinator 来决定的。
当 consumer 加入(订阅)一个 group 时:
- 会发送 JoinGroup 请求,通知 Broker 有新的消费者加入。
- 会发送 SyncGroup 请求用于 partition 的分配。
还会把信息发给 Client,consumer 再发一个新的请求,拿到 Broker 的一些分配的信息。Group Coordinator 会把这些 group 相关的信息写入一个特殊的 topic。
KoP 这里也做了一些配置,这个特殊的 topic 会存在于一个默认的 namespace 下,它的 partition 数量默认是 8。Kafka group 基本等价于 Pulsar Failover subscription。如果想让 Kafka 的 Offset 被 Pulsar 客户端识别的话,就需要 Offset 对应的 MessageId 进行 ACK。因此 KoP 里面有个组件是叫 OffsetAcker,它维护了一组 Consumer。每次 Group Coordinator 要进行 ACK 时,就会创建一个 partition 对应的 consumer 来把 group ACK。
这里会提到一个“namespace bundle” 的概念。Group Coordinator 决定了 consumer 与 partition 的映射关系。
在 Apache Pulsar 中,每台 broker 都拥有(own)一些 Bundle range(如上图示例);topic 会按名字哈希到其中一个 Bundle range,这个 range 的 owner broker 就是 topic 的 owner broker,那么你订阅的 topic 就连接对应到 broker。这里大家要注意两个问题,一是 bundle 可能会分裂(你也可以配置使其禁止分裂),二是 Broker 有可能挂掉,因此导致 bundle 和 Broker 的映射关系可能发生改变。因此为了防止这两个问题的发生,KoP 注册了一个监听器(listener),可用来感知 bundle ownership 的变化,一旦 bundle ownership 发生变化则通知 Group Coordinator 调用处理函数进行处理。
Kafka Offset
先介绍下 Kafka Offset 与 Pulsar MessageId 这两个概念。Kafka Offset 是一个 64 位整型,用来标识消息存储的位置,Kafka 的消息存储在本机所以可以用整数来表示消息的序号。Pulsar 是将消息存储在 Bookie 上,Bookie 可能分布在多台机器,因此 Bookie 使用 Ledger ID 与 Entry ID 来表示消息的位置。Ledger ID 可以理解对应 Kafka 中的 Segment,Entry ID 则近似等价于 Kafka Offset。Pulsar 中的 Entry 对应的不是单条消息,而是一条打包后的消息,因此产生了 Batch Index。由此,需要 Ledger ID、Entry ID 和 Batch Index 三个字段共同标记一条 Pulsar 的消息。
那么,就不能单纯的将 Kafka Offset 映射为 Pulsar 的 MessageID,这样简单的处理可能会造成 Pulsar 消息丢失。在 KoP 2.8.0 之前,通过对 Pulsar LedgerID、Entry ID 和Batch Index 分别分配 20 位、32 位、12 位拼凑成一个 Kafka Offset (如上图所示),这种分配策略在多数情况下可行,能够保证 Kafka offset 的有序性,但面对 MessageID 拆分仍然难以提出「合适」的分配方案,存在以下几种情形的问题:
- 比如,分配给 LedgerID 20 字节,在 2^20 时会发生 LedgerID 耗尽的问题,也容易造成 Batch Index 字节用光的情况;
- 从 cursor 读取 entry 时只能一个一个读取,否则可能导致
Maximum offset delta exceeded
问题; - 有些第三方组件(比如 Spark)依赖于连续 Offset 的功能
鉴于上述关于 Kafka Offset 的种种问题,StreamNative 联合腾讯工程师共同提出基于 broker entry metadata 的优化方案 PIP 70: Introduce lightweight broker entry metadata ,新方案可参考下图右侧示意。
上图左侧:目前 Pulsar 消息组成包括 Metadata 与 Payload 两部分,Payload 指的是具体写入的数据,Metadata 则是元数据如发布时间戳等。Pulsar Broker 会将消息写入 Client,同时将消息存到 Bookie 中。
上图右侧:右侧展示的是 PIP 70 提出的改进方案。在新方案中,Broker 仍然会将消息写入到 Client 中,但写入到 Bookie 中的是 Raw Message ── 何为 Raw Message?就是在原 Message 基础上增加了 BrokerEntryMetadata。从上图可以看到 Client 是无法获取 Raw Message 的,只有 Broker 可以获取 Raw Message。之前提到,Protoctol handler 可以获取 Broker 全部权限,因此 Protocol Handler 也获取 Raw Message。如果在 Pulsar 中置入 offset,那么KoP 就可以获取 Offset。
我们做了这样的实现:在 protocol buffer 文件中有两个字段,主要的是第二字段。Index 对应 Kafka 的 Offset,相当于在 Pulsar 中将 Kafka 实现了一遍。有两个 intercepter 分别是ManagedLedgerInterceptor
private boolean beforeAddEntry(OpAddEntry addOperation) {
// if no interceptor, just return true to make sure addOperation will be
initiate()
if (managedLedgerInterceptor == null) {
return true;
}
try {
managedLedgerInterceptor.beforeAddEntry(addOperation,
addOperation.getNumberOfMessages());
return true;
和 BrokerEntryMetadataInterceptor。
public OpAddEntry beforeAddEntry(OpAddEntry op, int numberOfMessages) {
if (op == null || numberOfMessages <= 0) {
return op;
}
op.setData(Commands.addBrokerEntryMetadata(op.getData(),
brokerEntryMetadataInterceptors, numberOfMessages));
return op;
}
addOperation 包含了从 producer 发过来的消息的字节和数量,由此 interceptor 可以拦截到所有生产的消息。而 Commands.addBrokerEntryMetadata
的作用是在 OpAddEntry data 前面加一个 BrokerEntryMetadata。加在前面的原因是为了易于解析,可以先读前面的字段是否是 magic number,是的话就可以接着读 BrokerEntryMetadata,不是的话就可以按正常的协议解析普通的 Metadata。BrokerEntryMetadataInterceptor 相当于在 Broker 端做的拦截器。
因此,在 KoP 中基于 BrokerEntryMetadata 就很容易实现连续 Offset:
- FETCH 请求:直接读 Bookie,解析 BrokerEntryMetadata 即可;
- PRODUCE 请求:将 ManagedLedger 传入异步写 Bookie 的上下文,从 ManagedLedger 的 interceptor 中拿到 Offset
- COMMIT_OFFSET 请求:对于 __consumer_offsets,原封不动写入 topic,对于 Pulsar 的 cumulative acknowledgement,则对 ManagedLedger 进行二分查找。
鉴于上述改动,在 KoP 2.8.0 中必须进行如下配置,以确保 Offset 操作正常使用:
brokerEntryMetadataInterceptors=org.apache.pulsar.common.intercept.AppendIndexMetadataInterceptor
消息的编码与解码
这也是 KoP 2.8.0 改进比较重要的一部分。
在 KoP 2.8.0 之前,KoP 对消息的生产和消费都需要经过对消息的解压缩、解 batch 等操作,操作时延较严重。我们也提出了一个问题:为什么 KoP 要兼容 Pulsar 的客户端呢?如果从 Kafka 迁移到 Pulsar,大部分情况可能只存在 Kafka 客户端,不太可能存在 Kafka client 与 Pulsar Client 的交互,针对消息的编解码就显得没有必要。在生产消息时,将 MemoryRecords 内部的 ByteBuffer 直接写入 Bookie 即可。
在消息消费时相对不太一样,我们是用 ManagedCursor 进行读取的,也需要将若干个 Entry 转成一个 ByteBuf,但在实际应用场景中发现这种方式开销仍然比较大,进一步排查后发现是在 appendWithOffset
对每条消息重新计算校验和时产生的,如果 batch 数量较多则计算次数过多,产生了不必要的开销。针对该问题,BIGO 团队成员给到了相关 PR 方案,提交了一个 appendWithOffset 简化版本(如下图),去掉了非必要动作,当然该提案也是基于前面提交的连续 Offset 改进基础上进行的。
性能测试(WIP)
性能测试还处于 WIP(Work in Progress)阶段,目前发现了一些问题。首先,在下图图峰中,端到端延时为 6ms 对 4ms,该时间在可接受范围内。但是在后续排查中,我发现经常出现 full GC 高达 600 ms 的情况,甚至出现延时更高的情况,我们在排查这个问题。
以下几张图分别为 HandleProduceRequest、ProduceEncode、MessageQueuedLatency、MessagePublish 的监控。从监控来看,HandleProduceRequest(PRODUCE 请求的处理开始,到这次请求所有消息全部成功写入 Bookie)的延时为 4 ms 左右,与 Pulsar 客户端差不多但是少了一趟网络的往返。
我们主要看编码 ProduceEncode (对 Kafka 消息编码的时间)的时间,我的测试是用 Kafka 的 EntryFormat,可以看到只消耗不到 0.1 ms 的时间;如果用 Pulsar 的 EntryFormat,那么监控结果在零点几 ms ~ 几 ms 之间。
其实这里的实现还存在一点问题,因为目前还在用一个队列,所以会有下图的指标 MessageQueuedLatency。MessageQueuedLatency 是从每个分区的消息队列开始,到准备异步发送的时间。我们怀疑是不是队列导致性能变差,但是从监控来看 0.1 ms 的延时影响不大。
最后是 MessagePublish 是 Bookie 的延时,即单个分区的消息从异步发送开始,到成功写入 Bookie 的时间。监控结果较理想,所以近期我们会研究 GC 的问题来源。
KoP Authentication
在 2.8.0 版本之前
在实际生产环境中如果需要部署到云上,需要支持 Authentication。在 2.8.0 之前,KoP 对 Authentication 的支持还比较简单,支持仅限于 SASL/PLAIN 机制,它基于 Pulsar 的 JSON Web Token 认证,在 Broker 的基本配置之外,只需要额外配置 saslAllowedMechanism=Plain
。用户端则需要输入 namespace 和 token 作为 JAAS 配置的用户名和密码。
security.protocol=SASL_PLAINTEXT # or security.protocol=SASL_SSL if SSL connection is used
sasl.mechanism=PLAIN
sasl.jaas.config=org.apache.kafka.common.security.plain.PlainLoginModule \
Required username=''public/default'' password=''token:xxx'';
支持 OAuth 2.0
最近 KoP 2.8.0 支持 OAuth 2.0 进行认证,也就是 SASL/OAUTHBEARER 机制。简单科普一下,它采用了简单的第三方服务。首先 Client 从 Resource Owner 取得授权 Grant,Resource Owner 可以是类似于微信公众号的授权码, 也可以是真人事先给的 Grant。然后将 Grant 发给 Authorization Server,即 OAuth 2 的 Server,通过授权码可取得 Access Token,即可访问 Resource Server,即 Pulsar 的 Broker,Boker 会进行 Token 的验证。获取 Token 的方式是第三方验证,这个方法相对安全。
KoP 默认的 Handler 和 Kafka 相同。类似 Kafka,KoP 也需要在 broker 端配置 Server Callback Handler 用于 token 验证:
- kopOauth2AuthenticateCallbackHandler :handler 类
- kopOauth2ConfigFile:配置文件路径
这里面用 JAAS 的方法,用单独的配置文件。KoP 提供了一种实现类,它基于 Pulsar Broker 配置的 AutnticationProvider 进行验证。因为 KoP 有 Broker Service,那么即拥有 Broker 的所有权限,可以去调用 Broker 配置的 provider authentication 方法进行验证,因此配置文件中仅需配置 auth.validate.method=,该 method 对应的是 provider 的 getAuthNa me
方法返回值。如果用 JWT 认证的话,这个 method 是 token;用 OAuth 2 认证的话,这个 method 可能会不同。
客户端
对于 Kafka 客户端,KoP 提供了一种 Login Callback Handler 实现。Kafka Java 客户端 OAuth 2.0 认证:
sasl.login.callback.handler.class=io.streamnative.pulsar.handlers.kop.security.oauth.OauthLoginCallbackHandler
security.protocol=SASL_PLAINTEXT # or security.protocol=SASL_SSL if SSL connection is used sasl.mechanism=OAUTHBEARER
sasl.jaas.config=org.apache.kafka.common.security.oauthbearer.OAuthBearerLoginModule \
required oauth.issuer.url="https://accounts.google.com"\
oauth.credentials.url="file:///path/to/credentials_file.json"\
oauth.audience="https://broker.example.com";
Server Callback 是用来验证 client token 的,Login Callback Handler 是从第三方的 OAuth 2 服务上获取第三方 token。我的实现是参考 Pulsar 的实现,配置是按 Kafka 的 JAAS 进行配置。主要有三个需要配置:issueUrl、credentialsUrl、audience。它的含义和 Pulsar 的 Java 客户端认证是一样的,因此可以参考 Pulsar 的文档。Pulsar Java 客户端 OAuth 2.0 认证:
String issuerUrl = "https://dev-kt-aa9ne.us.auth0.comH;
String credentialsUrl = "file:///path/to/KeyFile.json";
String audience = "https://dev-kt-aa9ne.us.auth0.com/api/v2/";
PulsarClient client = PulsarClient.builder()
.serviceUrl("pulsar://broker.example.com:6650/")
.authentication(
AuthenticationFactoryOAuth2.clientcredentials(issuerUrl, credentialsUrl, audience)) .build();
因此 KoP 对 OAuth 2 的支持在于它提供了 Client 端和默认的 Server 端的 Callback Handler。在 Kafka 使用 OAuth 2 验证时,需要自己写 Handler;但是 KoP 和 Pulsar 的实现类似,不需要自己写 Handler,开箱即用。
KoP 2.8.0 其他进展
- 移植了 Kafka 的 Transaction Coordinator。若想启用 Transaction,需要添加如下配置:
enableTransactionCoordinator=true
brokerid=<id>
- 基于 PrometheusRawMetricsProvider 添加了 KoP 自定义的 metrics。该特性由 BIGO 的陈航添加,即刚刚展示的监测。
- 暴露 advertised listeners,从而支持 Envoy Kafka Filter 进行代理。在以前的 KoP 中不友好的一点是,配置的 listener 必须和 broker 的 advertised listener 一样。在新版本中我们将 listener 和 advertised listener 分开,可以支持代理,比如:部署在云上可以用 Envoy 代理。
- 完善对 Kafka AdminClient 的支持。这是从前被忽略的一点。大家认为用 Pulsar 的 admin 就可以了,实际上一方面用户习惯使用 Kafka AdminClient,另一方面有些用户配置的组件内置了 AdminClient,如果不支持该协议会影响使用。
近期计划
Pulsar 2.8.0 争取在 4 月底发布。在正式发布前需要排查一些性能测试的问题:
- 添加更详细的 metrics。
- 排查压测过程中内存持续增长以及 full GC 的问题。
- 进行更为系统的性能测试。
- 处理社区近期反馈的问题。
相关阅读
点击 链接,获取 Apache Pulsar 硬核干货资料!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。