業務背景
隨着移動雲的快速發展,越來越多的客户對雲原生消息中間件提出了更多需求,從而可以將主要的精力聚焦在應用程序上,大致有以下方面:
- 快速彈性伸縮,計算和存儲資源能夠按需擴展,以滿足不同流量峯值和存儲規格的要求,並且在線擴展時不需要均衡數據
- 提供較高的安全防護,擁有身份認證和授權機制,確保數據的安全性
- 能準確實時地發現問題,支持實例健康、吞吐量、消息堆積等維度的監控
- 同時支持 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/pulsar/pull/4502,
- PR-5935:https://github.com/apache/pulsar/issues/5935,
- PR-6998:https://github.com/apache/pulsar/pull/6998) 這樣就可以做到多個 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/pulsar/wiki/PIP-1:-Pulsar-Proxy) 此外,Pulsar 官網提供了各個組件的 yaml 模板, (參考 pulsar-helm-chart:https://github.com/apache/pulsar-helm-chart/tree/master/charts/pulsar/templates) 這樣可以很快捷地在 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/pulsar/pull/7690) 將在 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 3. 創建多個分區的topic用來生產消費(確保其 namespace 的 bundle 數量是 broker 的整數倍) 4. client url: 配置為 proxy1:6650 時,proxy1 上有相應的負載(TCP連接),proxy2沒有負載 5. client url: 配置為 proxy1:6650, proxy2:6650 時,兩個 proxy 上面均有負載
🔧 能否做到直連 Broker
由於 Broker 註冊在 Zookeeper 中的服務地址是 podIP或者 pod 域名,Kubernetes 集羣外的客户端是不能直接訪問的,因此需要一種對外部客户端可見的服務地址。Pulsar 在 2.6.0 版本中引入 PIP-61: https://github.com/apache/pulsar/wiki/PIP-61:-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/tasks/inject-data-application/environment-variable-expose-pod-information) 例如,在 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/pulsar/wiki/PIP-61:-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/pulsar/pull/7737)。 另外,我們在使用該特性的過程中修復了獲取 bundle 時的空指針問題,以及支持客户端 Shell 指定監聽名字, (詳見:
- PR-7620:https://github.com/apache/pulsar/pull/7620,
- PR-7621:https://github.com/apache/pulsar/pull/7621) 並在 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/pulsar/pull/5713)。 此外,Kubernetes 從 1.16+ 版本增加了對 Pods 和 Services 的雙棧支持。 (參考 Dual-Stack:https://kubernetes.io/docs/concepts/services-networking/dual-stack/) 基於以上特性,我們只需要為 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/pulsar/pull/209) 因此,在支持 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/pulsar/pull/8120)
🔧 如何簡捷地管理實例
為了滿足移動雲用户對於管理簡潔可控的需求,我們還定製化了一些管理上的功能,部分列舉如下:
- PR-6456: 支持 Broker 可配置禁用自動創建訂閲https://github.com/apache/pulsar/pull/6456
- PR-6637: 支持在 Namespace 級別設置自動創建訂閲https://github.com/apache/pulsar/pull/6637
- PR-6383: 支持強制刪除訂閲https://github.com/apache/pulsar/pull/6383
- PR-7993、PR-8103: 支持強制刪除 Namespace/Tenanthttps://github.com/apache/pulsar/pull/7993https://github.com/apache/pulsar/pull/8103
未來規劃
移動雲消息隊列 Pulsar 目前已進入公測階段,後續規劃部分如下:
- 增加移動雲周邊 Connector 生態的支持
- 增加跨域複製的支持
- 優化 HTTP 協議的暴露監聽
- 優化 Broker 級別的限流機制
- 增加對傳統消息隊列功能的支持和優化
- 多個 Pulsar 實例共享 Bookie 存儲隔離優化
- 發佈更多的技術博客
作者信息
孫方彬