這是Jay Kreps在三月寫的一篇文章,用來介紹Kafka Streams。當時Kafka Streams尚未正式發佈,因此具體的API和功能和0.10.0.0版(2016年6月發佈)有所區別。可是Jay Krpes在這簡文章裏介紹了不少Kafka Streams在設計方面的考慮,仍是很值得一看的。html
如下的並不會徹底按照原文翻譯,由於那麼搞太累了……這篇文件的確很長,並且Jay Kreps寫的重複的地方也挺多,有些地方也有些故弄玄虛的意思。不過他想說的道理倒挺容易搞清楚。git
我很高興能宣佈Kafka的新特性-Kafka Streams的預覽。Kafka Streams是一個使用Apache Kafka來構造分佈式流處理程序的Java庫。它前做爲Kafka 0.10版本的一部分,其源碼在Apache Kafka項目下。。github
使用Kafka Streams構建的一個流處理程序看起來像是這樣:docker
須要注意的是:Kafka Streams是一個Java庫,而不是一個流處理框架,這點和Strom等流處理框架有明顯地不一樣數據庫
這個程序和0.10.0.0版在細節上有不少不一樣。對Kafka 0.10.0.0版的Kafka Streams, 實際能運行的例子能夠在Kafka Streams工程的examples包底下找到。須要注意的是,這個例子使用了lambda表達式,這是JAVA8的特性。apache
在KStream的構造上,體現了它跟Kafka的緊密關係。好比,它默認的輸入流的元素就是K,V對形式的,輸出流也是這樣,所以在構造輸入輸出流時須要分別指定K和V的Serde。其中KStream的API使用了不少集合函數,像map, flatMap, countByKey等,這個也能夠稱爲Kafka Streams的DSL。編程
雖然只是一個庫,可是Kafka Streams直接解決了在流處理中會遇到的不少難題:windows
對於想要跳過這些前言,想直接看文檔的人,大家能夠直接去到Kafka Streams documention. 這個blog的目的在於少談"what"(由於相關的文檔會進行詳細地描述),多談"why"。緩存
Kafka Streams是一個用來構建流處理程序的庫,特別是其輸入是一個Kafka topic,輸出是另外一個Kafka topic的程序(或者是調用外部服務,或者是更新數據庫,或者其它)。它使得你以一種分佈式以及容錯的方式來作這件事情。性能優化
在流處理領域有不少正在進行的有趣的工做,包括像 Apache Spark, Apache Storm, Apache Flink, 和 Apache Samza這樣的的開源框架,也包括像Google’s DataFlow 和 AWS Lambda同樣的專有服務。全部,須要列一下Kafka Streams和這些東西的類似以及不一樣的地方。
坦率地說,在這個生態系統中,有開源社區帶來的很是多的各類雜亂地創新。咱們對於全部這些不一樣的處理層(processing layer)感到很興奮:儘管有時候這會讓人感到有點困惑,可是技術水平的確在很快地進步。咱們想讓Kafka可以成爲全部這些處理層的合適的數據源。咱們想要Kafka Streams填充的空缺不大在於這些框架所關注的分析領域,而在於構建用於處理流式數據的核心應用和微服務。我在下面一節將會深刻講述這些不一樣之處,而且開始講解Kafka Streams是怎麼使這種類型的程序更簡單的。
若是想要知道一個系統設計是否在真實狀況下工做良好,惟一的方法就是把它構建出來,把它用於真實的程序,而後看看它有什麼不足。在我以前在LinkedIn的工做中,我很幸運地可以成爲設計和構造流處理框架Apache Samza的小組的成員。咱們把它推出到一系列內部程序之中,在生產中提供爲它提供支持,而且幫助把它做爲一個Apache項目開源。
那麼,咱們學到了什麼呢?不少。咱們曾經有過的一個關鍵的錯覺是覺得流處理將會被以一種相似於實時的MapReduce層的方式使用。咱們最終卻發現,大部分對流處理有需求的應用實際上和咱們一般使用Hive或者Spark job所作的事情有很大不一樣,這些應用更接近於一種異步的微服務,而不是批量分析任務的快速版本。
我所說是什麼意思呢?個人意思是大部分流處理程序是用來實現核心的業務邏輯,而不是用於對業務進行分析。
構建這樣的流處理程序須要解決的問題和典型的MapReduce或Spark任務須要解決的分析或ETL問題是很是不一樣的。它們須要一般的程序所經歷的處理過程,好比配置,佈署,監控,等。簡而言之,它們更像是微服務(我知道這是一個被賦予了過多意義的名詞),而不像是MapReduce任務。Kafka取代了HTTP請求爲這樣的流處理程序提供事件流(event streams)。
以前的話,人們用Kafka構造流處理程序時有兩個選擇:
1. 直接拿Consumer和Producer的API進行開發
2. 採用一個成熟的流處理框架
這兩種選擇各有不足。當直接使用Kafka consumer和producer API時,你若是想要實現比較複雜的邏輯,像聚合和join,就得在這些API的基礎上本身實現,仍是有些麻煩。若是用流處理框架,那麼就添加了不少不少複雜性,對於調試、性能優化、監控,都帶來不少困難。若是你的程序既有同步的部分,又有異步的部分,那麼就就不得不在流處理框架和你用於實現你的程序的機制之間分隔開。
雖然,事情不老是這樣。好比你已經有了一個Spark集羣用來跑批處理任何,這時候你想加一個Spark Streaming任務,額外添加的複雜性就挺小。可是,若是你專門爲了一個應用佈署一個Spark集羣,那麼這的確大大增長了複雜性。
可是,咱們對Kafka的定位是:它應該成爲流處理的基本元素,因此咱們想要Kafka提供給你可以擺脫流處理框架、可是又具備很是小的複雜性的東西。
咱們的目的是使流處理足夠簡化,使它可以成爲構造異步服務的主流編程模型。這有不少種方法,可是有三個大的方面是想在這個blog裏深刻討論一下:
這三個方面比較重要,因此把英文也列出來。
下面對每一個方面單獨進行討論。
Kafka Streams使得構建流處理服務更簡單的第一點就是:它不依賴於集羣和框架,它只是一個庫(並且是挺小的一個庫)。你只須要Kafka和你本身的代碼。Kafka會協調你的程序代碼,使得它們能夠處理故障,在不一樣程序實例間分發負載,在新的程序實例加入時從新對負載進行平衡。
我下面會講一下爲何我認爲這是很重要的,以及咱們以前的一點經歷,來幫助理解這個模型的重要性。
我前邊講到咱們構造Apache Samza的經歷,以及人們實際想要的(簡單的流服務)和咱們構建的東西(實時的MapReduce)之間的距離。我認爲這種概念的錯位是廣泛的,畢竟流處理作的不少事情是從批處理世界中接管一些能力,用於低延遲的領域。一樣的MapReduce遺產影響了其它主流的流處理平臺(Storm, Spark等),就像它們對Samza的影響同樣。
在LinkedIn在不少生產數據的處理服務是屬於低延遲領域的:email, 用戶生成的內容,新消息反饋等。其它的不少公司也應該有相似的異步服務,好比零售業須要給商品排序、從新訂價,而後賣出,對於金融公司,實時數據更是核心。大部分這些業務,都是異步的,對於渲染頁面或者更新移動app的屏幕就不會有這樣的問題(這些是同步的)。
那麼爲何在Storm, Samza, Spark Streaming這樣的流處理框架之上構建這樣的核心應用這麼繁瑣呢?
一個批處理框架,像是MapReduce或者Spark須要解決一些困難的問題:
不幸的是,爲了解決這些問題,框架就得變得頗有侵入性。爲了作到容錯和擴展,框架得控制你的程序如何佈署、配置、監控和打包。
那麼,Kafka Streams有什麼不一樣呢?
Kafka Streams對它想要解決的問題要更關注得多。它作了如下的事情:
它使用了Kafka爲普通的consumer所提供的一樣的組管理協議(group manager protocol)來實現。Kafka Streams能夠有一些本地的狀態,存儲在磁盤上,可是它只是一個緩存。若是這個緩存丟失了,或者這個程序實例被轉移到了別的地方,這個本地狀態是能夠被重建的。你能夠把Kafka Streams這個庫用在你的程序裏,而後啓動任意數量的你想要程序實例,Kafka將會把它們進行分區,而且在這些實例間進行負載的平衡。
這對於實現像滾動重啓(rolling restart)或者無宕機時間的擴展(no-downtime expansion)這樣簡單的事情是很重要的。在現代的軟件工程中,咱們把這樣的功能看作是應該的,可是不少流處理框架卻作不到這點。
從流處理框架中分離出打包和佈署的緣由是,打包和佈署這個領域自己就正在進行自身的復興。Kafka Streams可使用經典的老實巴交維工具,像是Puppet, Chef, Salt來佈署,把能夠從命令行來啓動。若是你年輕,時髦,你也能夠把你的程序作成Dock鏡像;或者你不是這樣的人,那麼你能夠用WAR文件。
可是,對於尋找更加有靈活的管理方式的人,有不少框架的目標就是讓程序更加靈活。這裏列了一部分:
這個生態系統就和流處理生態同樣專一。
的確,Mesos和Kubernets想要解決的問題是把進程分佈到不少機器上,這也是當你佈署一個Storm任務到Storm集羣時,Storm嘗試解決的問題。關鍵在於,這個問題最終被發現是挺難的,而這些通用的框架,至少是其中優秀的那些,會比其它的作得好得多-它們具備執行像在保持並行度的狀況下重啓、對主機的粘性(sticky host affinity)、真正的基於cgroup的隔離、用docker打包、花哨的UI等等功能。
你能夠在這些框架裏的任何一種裏使用Kafka Streams,就像你會對其它程序作的同樣,這是用來實現動態和有彈性的進程管理的一種簡單的方式。好比,若是你有Mesos和Marathon,你可使用Marathon UI直接啓動你的Kafka Streams程序,而後動態地擴展它,而不會有服務中斷, Meos會管理好進程,Kafka會管理和負載勻衡以及維護你的任務進程的狀態。
使用一種這些的框架的開銷是和使用Storm這樣的框架的集羣管理部分是同樣的,可是優勢是全部這些框架都是可選的(固然,Kafka Streams沒有了它們也能夠很好的工做)。
Kafka Strems用於簡化處理程序的另外一個關鍵方式是把「表」和"流「這兩個概念緊密地結合在一塊兒。咱們在以前的"turning the database inside out"中簡化這個想法。那句話抓住了做爲結果的系統是如何重鑄程序和它的數據之彰的關係以及它是怎麼應於數據變化,這樣的要點。爲了理想這些,我會回顧一下,解釋我對於」table"和"stream"的定義,以及把兩者結合在一塊兒如何可以簡化常見的異步程序。
傳統的數據庫都是關於在表格中存儲狀態的。當須要對事件流進行反應時,傳統數據庫作得並很差。什麼是事件呢?事件只是一些已經發生了的事-能夠是一個點擊、一次出售、源自某個傳感器的一個動態,或者抽象成任何這個世界上發生的事情。
像Storm同樣的流處理程序,是從這個等式的另外一端出發的。它們被設計用於處理事件流,可是基於流來產生狀態倒是後面才加進來的。
我認爲異步程序的基本問題是把表明當前世界狀態的tables與表明正在發生事件的event streams結合在一塊兒。框架須要處理好如何表示它們,以及如何在它們之間進行轉化。
爲何說這些概念是相關的呢?咱們舉一個零售商的簡單例子。對於零售商而言,核心的事件流是賣出商品、訂購新商品以及接收訂購的商品。「庫存表」是一個基於當前的存貨量,經過售出和接收流進行加減的「表」。對於零售商而言兩個關鍵的流處理動做是當庫存開始下降時訂購商品,以及根據供需關係調整商品價格。
在咱們開始研究流處理以前,讓咱們先試着想解表和流的關係。我想在這裏最好引用一下Pat Helland關於數據庫和日誌的話:
事務日誌記錄了對於數據庫的全部改變。高速的append操做是日誌發生改變的惟一方式。從這個角度來看,數據庫保存了日誌裏最新記錄的緩存。事實記錄於日誌中。數據庫是一部分日誌的緩存。被緩存的部分恰好是每一個記錄的最新值,以及源自於日誌的索引值。
這究竟是在說什麼呢?它的意義實際上位於表和流的關係的核心。
讓咱們以這個問題開始:什麼是流呢?這很簡單,流就是一系列的記錄。Kafka把流建模成日誌,也就是說,一個無盡的健/值對序列:
key1 => value1key2 => value2key1 => value3...
那麼,什麼是表呢?我認爲咱們都知道,表就是像這樣的東西:
Key1 |
Value1 |
Key2 |
Value3 |
其中value多是不少列,可是咱們能夠忽略其中的細節,簡單地把它們認爲是KV對(添加更多的列並不會改變將要討論的東西)。
可是當咱們的流隨時間持續更新,新的記錄出現了,這只是咱們的表在某個特定時間的snapshot。表格是怎麼變化的呢?它們是被更新的。一個表實際上並非單一一個東西,而是像下面這樣的一系列東西:
time = 0
key1 | value1 |
time = 1
Key1 |
Value1 |
Key2 |
Value2 |
time = 2
Key1 |
Value3 |
Key2 |
Value2 |
可是這個序列有一些重複。若是你把沒有改變的行去掉,只記錄更新,那麼就能夠用一個有序的更新序列來表示這張表:
可是,這不就變成流了嗎?這種類型的流一般補稱爲changelog, 由於它展現了更新序列,按照更新的順序記錄了每一個記錄的最新的值。
因此,表就是流之上的一個特殊的視圖。這樣說可能有些奇怪,可是我認爲這種形式的表跟咱們腦海中的長方形的表對於「表其實是什麼」是同樣能夠反映其本質的。或者,這樣實際上更加天然,由於它抓住了「隨時間改變」的概念(想想:有什麼數據真的不會改變呢?)。
換句話說,就像Pat Helland指出的那樣,一張表就是一個流裏的每一個key的最新的值的緩存。
用數據庫的術語來講:一個純粹的流就是全部的更新都被解釋成INSERT語句(由於沒有記錄會替換已有的記錄)的表,而一張表就是一個全部的改變都被解釋成UPDATE的流(由於全部使用一樣的key的已存在的行都會被覆蓋)。
這種雙面性被構建進Kafka中已經有一段時間了,它被以compacted topics的形式展示。
好的,這就是流和表是什麼。那麼,這跟流處理有啥關係呢?由於,最終你會發現,流和表的關係正是流處理問題的核心。
我上面已經給了一個零售商的例子,在這個例子裏「商品到貨」和「商品售出」這兩個流的結果就是一個存貨表,而對存貨表的更改也會觸發像「定貨」和「更改價格」這樣的處理。
在這個例子中,存貨表固然不僅是在流處理框架中創造出來的東西,它們可能已經在一個數據庫中了。那好,把一個由變化組成的流捕捉到一個表中被稱爲Change Capture, 數據庫就作了這個事。Change capture數據流的格式就是我以前描述的changelog格式。這類型的change capture是你可使用Kafka Connect輕鬆搞定的事情,Kafka Connect是一個用於data capture的框架,是Kafka 0.9版本新加的。
經過以這種方式構建表的概念,Kafka使得你從變化流(stream of changes)獲得的表中推導出數值。換句話說,就是讓你能夠像處理點擊流數據同樣處理數據庫的變化流。
你能夠把這種基於數據庫變化觸發計算的功能看做相似於數據庫的觸發器和物化視圖功能,可是這個功能卻不只限於一個數據庫,也不只限於PL/SQL,它能夠在數據中心的級別執行,而且能夠工做於任何數據源。
咱們到了怎麼樣能夠把一個把變成一個更新流(也是一個changelog),而且使用KafkaStreams基於它計算一些東西.可是表/流的雙面性用相反的方式也是可行的.
假如你有一個用戶的點擊流,你想計算每一個分戶的點擊總數.KafkaStreams可讓你計算這種聚合(aggregation),而且,你所計算出來的每一個用戶的佔擊數就是一張表.
在實現時,Kafka Streams把這種基於流計算出來的表存儲在一個本地數據庫中(默認是RocksDB,可是你能夠plugin其它數據庫).這個Job的輸出就是這個表的hcnagelog. 這個changelog是用於高可用的計算的(譯註:就是當一個計算任務失敗,而後在別的地方重啓時,能夠從失敗以前的位置繼續,而不用整個從新計算),可是它也能夠被其它的KafkaStreams進程消費和處理,也能夠用KafkaConnect導到其它的系統裏.
這種支持本地存儲的架構已經在Apache Samza中出現,我這前從系統架構的角度寫過一篇關於這個的文章.KafkaStreams與Apache Samza的關鍵的革新是表的概念再也不是一個低層的基礎設施,而是一個和stream同樣的一等成員.Streams在Kafka Streams提供的programming DSL中用KStream類表示, 表是用KTable類表示.它們有一些共同的操做,也能夠像表/流的雙面性暗示的那樣能夠互相轉換,可是,它們也有不一樣之處.(譯註:接下來的幾句比較難懂,若是以爲理解得不對,能夠看原文).好比,對一個KTable執行取合操做時,Kafka Streams知道這個KTable底層是一個stream of updates,所以會基於這個事實進行處理.這樣作是有必要的,是因對一個正在變化的表計算sum的語義跟對一個由不可變的更新組成的流計算sum的語義是徹底不一樣的.與之類似的還有,join兩個流(好比點擊點和印象流)的語法和對一個表和一個流(好比點擊流和帳戶表)進行join的語義是徹底不一樣的.經過用DSL對這兩個概念進行建模,這些細節自動就分清楚了.(譯註:我以爲這段話的意思是就Kafka Streams會考慮到KTable底層其實是一個流,因此會採用與計算普通表的aggregation和join不一樣的特殊的計算方式)
窗口、時間和亂序的事件是流處理領域的另外一個難搞的方面。可是,使人驚奇的是,能夠證實的是一個簡單的解決方案落到了表的概念上面。緊密關注流處理領域的人應該據說過"event time"這個概念,它被Google Dataflow團隊的人很是有說服力地頻繁地討論。他們抓住的問題是:若是事件無序地達到,那麼怎麼作windowed操做呢?亂序的數據在大多數分佈式場景下是沒法避免的,由於咱們的確沒法保證在不一樣的數據中心或者不一樣的設備上生成的數據的順序。
在零售商的例子中,一個這種windowed computing的例子就是在一個十分鐘的時間窗口中計算每種商品的售出數量。怎麼能知道何時這個窗口結束了呢?怎麼知道在這個時間段的全部出售事件都已經到達並且被處理了呢?若是這些都不能肯定的話,怎麼能給出每種商口的售出總數的最終值呢?你不管在何時基於此時統計的數量作出答案,均可能會太早了,在後續可能會有更多的事件到達,使得你以前的答案是錯誤的。
Kafka Streams使得處理這個問題變得很簡單:windowed aggregation的語義,例如count,就表示對於這個windows的「迄今爲止"的count。隨着新的數據的到達,它保持更新,下流的接收者能夠自已決定何時完成統計。對,這個能夠更新的數量的概念看起來莫名其秒的熟悉:它不是別的,就是一個表,被更新的windows就是key的一部分。天然而然的,下游操做知道這個流表示一個表,而且在這些更新到達時處理它們。
在數據庫變化流之上進行計算和處理亂序事件的windowd aggregation,使用的是一樣的機制,我認爲這是很優雅的。這種表和流的關係並非咱們發明的,在舊的流處理的文章中,好比CQL中,已經展現了它的不少細節,可是這個理論卻沒有融合進大多數現實世界的系統——數據庫處理表時、流處理系統處理流時,而且也沒多數沒有把二者都作爲一等公民。
有一個基於我上邊提出的一些特性的正在發展的功能可能不是那麼明顯。我討論了Kafka Streams是如何讓你透明地在RocksDB或其它本地數據結構中維護一個基於流推演出來的表的。由於這個處理的過程的狀態都在物理上存在於你的程序中,這就開啓了另外一項使人興奮的新的用途的可能性:使得你的程序能夠直接查詢這個狀態。
咱們當前還沒暴露出來這個接口-咱們如今還專一於使得流處理的API先穩事實上下來,可是,我以爲對於一些特定類型的數據敏感的程序,這是一個很吸引人的架構。
這意味着,你可構建,好比,一個嵌入了Kafka Streams的REST服務,它能夠直接查詢數據流經過流處理運算獲得的本地的聚合結果。這種類型的有狀態的服務的好處在這裏討論過。並非在全部領域這麼作都合適,你一般只是想要把結果輸出到一個外部的數據庫中。可是,假如你的服務的每一個請求都須要訪問不少數據,那麼把這些數據放在本地內存或者一個很快的本地RocksDB實例中會很是有用。
咱們的全部這些的最高目錄是使得構建和操做流處理程序的過程變得更簡單。咱們的信念是流處理應該是一個構建應用程序的主流方式,公司所作的事情的很大一部分在異步領域,流處理正是用來幹這個的。可是爲了使這點成爲現實,咱們還須要使Kafka Streams在這方面更簡單更可依賴。這種對於操做的簡化的一部分就是擺脫對外部集羣的依賴,可是它還簡化了其它的地方。
若是對人們是怎麼構建流處理程序進行觀察的話,你會發現除了框架自己,流處理程序傾向於具備高度的架構複雜性。這是一個典型的流處理程序的架構圖。
(圖)
這裏有如此多的會變化的部分:
把這麼一大堆東西弄下來不只不是人們想追求的,並且一般也是不現實的。即便你已經有了這個架構的全部部分,把它個整合在一塊兒,把它監控好,可以發揮它的全部做用,也是很是很是困難的。
Kafka Streams的一個最使人欣喜的事情就是它的核心概念不多,並且它們貫穿於整個系統中。
咱們已經談論過一些大的點:擺脫額外的流處理集羣,把表格和有狀態的處理徹底整合進流處理自己。使用Kafka Streams,這個架構能夠瘦身成這樣:
可是使得流處理變得簡單這個目標比這兩點遠得多。
由於它直接構建於Kafka的基礎操做之上,Kafka Streams很是小。它的整個代碼基礎只有不到九千行。你喜歡的話,一下行就看完了。這意味着你會遇到的除了Kafka本身的producer和consumer之外的複雜性是很容易承擔的。
這有不少小的含義:
簡單來講一個kafka Strems程序在不少方面看起來就像其它的直接用Kafka producer或consumer寫的程序同樣,可是它寫起來要簡潔得多。
除了Kafka client暴露出來那些配置之外,額外的配置項很是少。
若是你改了代碼,想要使用新的邏輯從新處理數據,你也不須要一個徹底不一樣的系統。你只須要回退你程序的Kafka offsets,而後讓它從新處理數據(你固然也能夠在Hadoop端或者其它地方從新處理,可是關鍵是你能夠選擇不這麼作).
儘管最初的樣例架構是由一系列獨立的組件組成,而且它們也只是部分地工做在一塊兒,可是咱們但願你未來會感受到Kafka、Kafka Connect和Kafka Streams就是爲了一塊兒工做而設計的。
就像其它的預覽版同樣,有一些功能咱們尚未完成。下面是一些將會添加進來的功能。
接下來會利用內置的表提供提供對程序狀態的查詢。
當前的KafkaStreams繼承了Kafka的"at least once"的消息傳遞語義。Kafka社區正在探索如何實現跨Kafka Connect, Kafka, KafkaStream和其它計算引擎的消息傳遞語義。