導讀:在 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 硬核乾貨資料!