共计 8802 个字符,预计需要花费 23 分钟才能阅读完成。
业务背景
随着 挪动云的疾速倒退 ,越来越多的客户对云原生消息中间件提出了 更多需要,从而能够将次要的精力聚焦在应用程序上,大抵有以下方面:
- 疾速弹性伸缩,计算和存储资源可能按需扩大,以满足不同流量峰值和存储规格的要求,并且在线扩大时不须要平衡数据
- 提供较高的平安防护,领有身份认证和受权机制,确保数据的安全性
- 能精确实时地发现问题,反对实例衰弱、吞吐量、音讯沉积等维度的监控
- 同时反对 IPv4/IPv6 双栈环境,满足不同网络环境下的诉求
- 在实例级别做到租户资源隔离,提供更细粒度的平安防护
- 反对跨区域复制服务,保证数据在集群间同步的稳定性和实时性
针对以上诉求,同时为了 对立私有云和公有云架构,挪动云抉择 Apache Pulsar 和 Kubernetes 来构建性能卓越、平安稳固、弹性伸缩、运维简便的云原生音讯零碎。
整体架构
基于 Apache Pulsar 计算存储拆散的云原生架构,咱们将用于计算的 Kubernetes 集群和用于存储的 BookKeeper 集群物理拆散,如下:
简略起见,这里咱们以共享 Zookeeper 为例(可依据实例数量及实例资源大小,在 Kubernetes 中独享 Zookeeper 集群)以及间接应用 NodePort 的服务裸露形式将 Proxy 服务提供给客户端(也可依据需要选用适合的 LB 云服务或者应用开源的 LB,例如:Metallb: https://metallb.universe.tf/)。
落地实际
???? 怎么实现共享 Bookie 资源
咱们冀望 Kubernetes 中的多个 Pulsar 实例可能同时共享底层的 Bookie 存储资源,这样能够更快捷地实现计算存储拆散。在 2.6.0 版本之前,Pulsar 实例在初始化元数据时,不反对设置 chroot 门路,并且只反对应用固定的 ledger 门路,不能应用已存在的 BookKeeper 集群。为此,咱们通过优化 initialize-cluster-metadata 命令来反对设置 chroot 门路,以及在 broker 配置中增加 bookkeeperMetadataServiceUri 参数来指定 BookKeeper 集群的连贯信息。
(详见:
- PR-4502:
https://github.com/apache/pul…,
- PR-5935:
https://github.com/apache/pul…,
- PR-6998:
https://github.com/apache/pul…)
这样就能够做到多个 Pulsar 实例共享已存在的 BookKeeper 集群,元数据结构大抵如下:
[zk: localhost:2181(CONNECTED) 1] ls /pulsar
[pulsar1, pulsar2]
[zk: localhost:2181(CONNECTED) 2] ls /pulsar/pulsar1
[counters, stream, bookies, managed-ledgers, schemas, namespace, admin, loadbalance]
[zk: localhost:2181(CONNECTED) 3] ls /bookkeeper
[ledgers]
[zk: localhost:2181(CONNECTED) 4] ls /bookkeeper/ledgers
[00, idgen, LAYOUT, available, underreplication, INSTANCEID, cookies]
???? 服务怎么裸露
Pulsar 通过引入可选的 Proxy 组件来解决客户端不能直连 Broker 以及直连可能带来的治理开销问题,例如在云环境中或者在 Kubernetes 集群中运行时,
(参考 PIP-1:
https://github.com/apache/pul…:-Pulsar-Proxy)
此外,Pulsar 官网提供了各个组件的 yaml 模板,
(参考 pulsar-helm-chart:
https://github.com/apache/pul…)
这样能够很快捷地在 Kubernetes 集群上构建一个 Pulsar 集群,咱们一开始采纳了如下的架构:
期间有遇到一些小的问题,例如,Proxy 无奈失常启动(在 Proxy 的 StatefulSet 中 initContainers 的条件是至多有一个 Broker 在运行),如下:
14:33:06.894 [main-EventThread] INFO org.apache.pulsar.zookeeper.ZooKeeperChildrenCache - reloadCache called in zookeeperChildrenCache for path /loadbalance/brokers
14:33:36.900 [main-EventThread] WARN org.apache.pulsar.proxy.server.util.ZookeeperCacheLoader - Error updating broker info after broker list changed.
java.util.concurrent.TimeoutException: null
at java.util.concurrent.CompletableFuture.timedGet(CompletableFuture.java:1771) ~[?:1.8.0_191]
at java.util.concurrent.CompletableFuture.get(CompletableFuture.java:1915) ~[?:1.8.0_191]
at org.apache.pulsar.zookeeper.ZooKeeperDataCache.get(ZooKeeperDataCache.java:97) ~[org.apache.pulsar-pulsar-zookeeper-utils-2.6.0-SNAPSHOT.jar:2.6.0-SNAPSHOT]
at org.apache.pulsar.proxy.server.util.ZookeeperCacheLoader.updateBrokerList(ZookeeperCacheLoader.java:118) ~[org.apache.pulsar-pulsar-proxy-2.6.0-SNAPSHOT.jar:2.6.0-SNAPSHOT]
at org.apache.pulsar.proxy.server.util.ZookeeperCacheLoader.lambda$new$0(ZookeeperCacheLoader.java:82) ~[org.apache.pulsar-pulsar-proxy-2.6.0-SNAPSHOT.jar:2.6.0-SNAPSHOT]
at org.apache.pulsar.zookeeper.ZooKeeperChildrenCache.lambda$0(ZooKeeperChildrenCache.java:85) ~[org.apache.pulsar-pulsar-zookeeper-utils-2.6.0-SNAPSHOT.jar:2.6.0-SNAPSHOT]
at java.util.concurrent.CompletableFuture.uniAccept(CompletableFuture.java:656) ~[?:1.8.0_191]
at java.util.concurrent.CompletableFuture$UniAccept.tryFire(CompletableFuture.java:632) ~[?:1.8.0_191]
at java.util.concurrent.CompletableFuture.postComplete(CompletableFuture.java:474) ~[?:1.8.0_191]
at java.util.concurrent.CompletableFuture.complete(CompletableFuture.java:1962) ~[?:1.8.0_191]
at org.apache.pulsar.zookeeper.ZooKeeperCache.lambda$22(ZooKeeperCache.java:434) ~[org.apache.pulsar-pulsar-zookeeper-utils-2.6.0-SNAPSHOT.jar:2.6.0-SNAPSHOT]
at org.apache.zookeeper.ClientCnxn$EventThread.processEvent(ClientCnxn.java:618) [org.apache.pulsar-pulsar-zookeeper-2.6.0-SNAPSHOT.jar:2.6.0-SNAPSHOT]
at org.apache.zookeeper.ClientCnxn$EventThread.run(ClientCnxn.java:510) [org.apache.pulsar-pulsar-zookeeper-2.6.0-SNAPSHOT.jar:2.6.0-SNAPSHOT]
能够通过将 podManagementPolicy 策略由 Parallel 改为 OrderedReady(或者将 Proxy 的 initContainers 条件批改为指定 Broker 的正本数都在运行)来长期解决这个问题。这其实是 Proxy 的一个死锁问题导致的,由 Masahiro Sakamoto 发现并修复,
(详见 PR-7690:
https://github.com/apache/pul…)
将在 2.6.2 和 2.7.0 版本中公布。此外,在一直的压测过程中,Proxy 会偶现内存持续增长,直至 Pod 重启(Error in writing to inbound channel. Closing java.nio.channels.ClosedChannelException: null),在做了一些验证和评估之后,咱们决定通过其余形式来实现 Pod 内的 Broker 服务裸露,大抵起因如下(供参考):
- Proxy 节点会耗费额定的 CPU 和内存资源
- 业务流量通过 Proxy 转发到 Broker 会减少额定的网络开销
- 生产速率过快会导致 Proxy 呈现 OOM,集群稳定性升高
- Proxy 自身不具备负载平衡的能力,对实例的弹性伸缩不敌对
其中 Client 配置 multi-hosts 的实现形式是配置几个 Proxy Url,理论就只用这几个 Proxy,可简略通过以下步骤验证:
1.2 个 broker: broker3:6650,broker4:6650
2.2 个 proxy: proxy1:6650,proxy2:6650
- 创立多个分区的 topic 用来生产生产(确保其 namespace 的 bundle 数量是 broker 的整数倍)
- client url: 配置为 proxy1:6650 时,proxy1 上有相应的负载(TCP 连贯),proxy2 没有负载
- client url: 配置为 proxy1:6650, proxy2:6650 时,两个 proxy 下面均有负载
???? 是否做到直连 Broker
因为 Broker 注册在 Zookeeper 中的服务地址是 podIP 或者 pod 域名,Kubernetes 集群外的客户端是不能间接拜访的,因而须要一种对外部客户端可见的服务地址。Pulsar 在 2.6.0 版本中引入 PIP-61: https://github.com/apache/pul…:-Advertised-multiple-addresses 来反对播送多个 URL 监听地址,例如,可设置如下内 / 外监听:
advertisedListeners=internal:pulsar://broker-0.broker-headless.pulsardev.svc.cluster.local.:6650,external:pulsar://10.192.6.23:38068
这里咱们应用 pod 域名(broker-0.broker-headless.pulsardev.svc.cluster.local.)作为 Broker 间的通讯地址,应用 Pod 所在的理论 Worker 节点 IP 和 事后调配的 NodePort 端口作为内部的通讯地址。
StatefulSet 中每个 Pod 的 DNS 格局为:statefulSetName-{0..N1}.serviceName.namespace.svc.cluster.local.
- statefulSetName 为 StatefulSet 的名字
- 0..N-1 为 Pod 所在的序号,从 0 开始到 N-1
- serviceName 为 Headless Service 的名字
- namespace 为服务所在的 namespace,Headless Service 和 StatefulSet 必须要在雷同的 namespace
- .svc.cluster.local. 为 Cluster Domain
为了使集群内部的客户端可能直连 Broker 所在的 Pod,咱们在 ConfigMap 中保护了 Worker 节点名字和 IP 的映射关系以及事后调配好的 NodePort 端口,这样在 StatefulSet 的 containers 启动脚本中,咱们就能够通过命令 bin/apply-config-from-env.py conf/broker.conf; 将须要裸露的理论 Worker 节点 IP 和 事后调配的 NodePort 端口写到 Broker 配置 advertisedListeners 中,这样 Broker 启动后注册在 Zookeeper 中的内部通讯地址(external:pulsar://10.192.6.23:38068)对集群内部的客户端就是可见的了。其中,比拟要害的一步是通过环境变量将 Pod 信息出现给容器,
(参考 Pod-Info:
https://kubernetes.io/docs/ta…)
例如,在 Broker 的 StatefulSet 的 yaml 文件中增加如下配置:
env:
- name: PULSAR_NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
- name: PULSAR_POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
这样在 bin/apply-config-from-env.py 中,咱们就能够通过上述信息依据 ConfigMap 中的节点和端口信息失去要裸露的理论 URL,写到 Broker 配置中,进而注册到 Zookeeper 中来发现理论的服务地址。
此外,PIP-61:
https://github.com/apache/pul…:-Advertised-multiple-addresses
目前只反对 TCP 协定的服务发现,当须要在集群内部拜访 HTTP 协定时,advertisedListeners 暂无奈提供帮忙。为了解决这个问题,咱们在 Broker 中定制化了一个 webServiceExternalUrl 配置,而后通过上述相似的办法将须要裸露的理论 Worker 节点 IP 和 事后调配的 NodePort 端口(HTTP 协定)注册到 Zookeeper 中,这样对集群内部的 Admin Client 就是可见的了。在 2.6.0 版本中,客户端在应用 advertisedListenerName 时,Broker 返回的地址是谬误的,咱们对此进行了修复。
(详见 PR-7737:
https://github.com/apache/pul…)。
另外,咱们在应用该个性的过程中修复了获取 bundle 时的空指针问题,以及反对客户端 Shell 指定监听名字,
(详见:
- PR-7620:
https://github.com/apache/pul…,
- PR-7621:
https://github.com/apache/pul…)
并在 2.6.1 版本中公布。
注:Service 在转发申请时,须要打上对应 Pod 节点名字的 selector 标签,例如:
> selector:
> app: pulsar
> component: broker
> statefulset.kubernetes.io/pod-name: broker-0
以上是咱们对于直连 Broker 的摸索,相比 Proxy 的计划有如下劣势:
- 缩小了计算资源的额定开销
- 进步了 Pulsar 集群的吞吐量和稳定性
- 可能较好地反对 Pulsar 实例的弹性伸缩
- 能够应用 Broker 的机制做一些集群优化(例如,通过 Broker 限流来防止 OOM 的产生)
???? 怎么解决 IPv4/IPv6 双栈
对于挪动云的场景,越来越多的用户期待云产品可能反对 IPv4/IPv6 双栈,以满足多种场景下的利用需要。
在 2.6.0 版本之前,Pulsar 只反对 IPv4 环境的部署,为此咱们减少了对 IPv6 的反对。
(详见 PR-5713:
https://github.com/apache/pul…)。
此外,Kubernetes 从 1.16+ 版本减少了对 Pods 和 Services 的双栈反对。
(参考 Dual-Stack:
https://kubernetes.io/docs/co…)
基于以上个性,咱们只须要为 Kubernetes 中的 Pulsar 实例减少 IPv6 的 Service 即可(spec.ipFamily 设置为 IPv6)。而后,通过后面相似的服务裸露计划,将对集群外客户端可见的 IPv6 服务地址注册到 Zookeeper 中即可,如下:
advertisedListeners=internal:pulsar://broker-0.broker-headless.pulsardev.svc.cluster.local.:6650,external:pulsar://10.192.6.23:38068,external-ipv6:pulsar://[fc66:5210:a152:12:0:101:bbbb:f027]:39021
值得注意的是,零碎属性 java.net.preferIPv4Stack 默认是 false,在反对 IPv6 的双栈零碎上,应用 Java 的 Socket 会默认通过底层 native 办法创立一个 IPv6 Socket(能够同时反对 IPv4 和 IPv6 主机通信),当 TCP 客户端的 java.net.preferIPv4Stack 属性设置为 true 时,如果要创立一个 host 为 IPv6 的 Socket,会抛出异样 java.net.SocketException: Protocol family unavailable。目前,Pulsar 客户端连贯时优先应用 IPv4,以后的环境变量和脚本中,该属性都设置为了 true。
(详见 PR-209:
https://github.com/apache/pul…)
因而,在反对 IPv6 的双栈时,须要将这些脚本中(即 bin 目录下的 pulsar,pulsar-admin,pulsar-client,pulsar-perf)的属性 java.net.preferIPv4Stack 设置为 false。其中,Broker 启动时会应用到 bin/pulsar 脚本,须要确保 Broker 启动后是同时监听 IPv4/IPv6 的端口,大抵如下:
[root@k8s1 ~]# kubectl exec -it broker-0 -n pulsardev -- /bin/bash
[pulsar@broker-0 pulsar]$ netstat -anp
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp6 0 0 :::8080 :::* LISTEN 1/java
tcp6 0 0 :::6650 :::* LISTEN
上述 Pod 里执行的后果中 ::: 代表同时监听了 IPv4/IPv6,如果是 0.0.0.0:,则只反对 IPv4。期间在应用过程中,咱们还修复和优化了一些问题,例如,客户端不反对 Mult-Hosts 的 IPv6 地址等。
(详见 PR-8120:
https://github.com/apache/pul…)
???? 如何简捷地治理实例
为了满足挪动云用户对于治理简洁可控的需要,咱们还定制化了一些治理上的性能,局部列举如下:
- PR-6456: 反对 Broker 可配置禁用主动创立订阅
https://github.com/apache/pul…
- PR-6637: 反对在 Namespace 级别设置主动创立订阅
https://github.com/apache/pul…
- PR-6383: 反对强制删除订阅
https://github.com/apache/pul…
- PR-7993、PR-8103: 反对强制删除 Namespace/Tenant
https://github.com/apache/pul…
https://github.com/apache/pul…
将来布局
挪动云音讯队列 Pulsar 目前已进入公测阶段,后续布局局部如下:
- 减少挪动云周边 Connector 生态的反对
- 减少跨域复制的反对
- 优化 HTTP 协定的裸露监听
- 优化 Broker 级别的限流机制
- 减少对传统音讯队列性能的反对和优化
- 多个 Pulsar 实例共享 Bookie 存储隔离优化
- 公布更多的技术博客
作者信息
孙方彬
Apache Pulsar Committer,中国移动云能力核心消息中间件团队负责人。
小福利:点击即可申请收费体验挪动云 Pulsar