OneAPM 工作兩年總結

雨帆發表於2017-10-27

掐指一算,從 OneAPM 離職也快一個月了,在 OneAPM 工作的種種,彷彿還像是在昨天。細數兩年的工作經歷,我很慶幸在恰當的時間點和這麼一群有激情有活力的人共事。那麼,是時候總結一下我在 OneAPM 做的牛(cai)逼(ji)事情了。

大家好,今天由我來分享一下,我在上家公司做的 Ai 和 告警 相關的一些內容。

首先,我先簡單介紹一下,今天我要分享的兩個專案:

  1. Ai 是 OneAPM 伺服器端應用效能監控分析程式,它主要是能收集Java、CSharp、Python等偏後端語言的系統的一些指標資料。然後分析出呼叫 Trace 和完整的呼叫拓撲圖,還有一些其他圖表資料的展示。
  2. 告警系統原先作為一個 Ai 系統的子模組,用的是流式計算框架 Flink,後面不能滿足對外交付和業務功能需求。我們就重新設計開發了純粹的CEP計算引擎,依託於此在Ai上構建了新的告警系統,然後服務化拆分成獨立的告警系統,並接入了其他類似Ai的業務線。

這次分享,一是我對以前2年工作的整理和思考,二也是和大家交流學習。

對於 Ai,我不屬於它的主要研發,我只是在上面剝離開發了現有的告警系統。所以我就講講我接觸過的架構部分的演進。本身,就功能部分,其實沒有東西。 我在說告警的時候會說的比較細一些。

我是15年年底入職OneAPM,17年9月初離職加入我們們這個團隊。這期間Ai伴隨著業務的需求,也進行了三次大的技術架構演進。最明顯的,就是每次演進中,Ai對應的儲存在不斷變化。同時,比較巧的是,每次架構變化的同時,我們的資料結構也略有不同,並且學習的國外競品也不大一樣。

說老實話,我們每次改變的步子都邁的略大,這中間也走了不少彎路。很多技術、框架,一開始看十分好,但是卻不一定契合我們的需求。專案在變革初期就拆分出SaaS和企業級兩套程式碼,並且各自都有比較多的開發分支,這些東西的維護,也讓我們的程式碼管理一度崩潰。

但是,我這裡主要想分享的,就是我們在業務和資料量不斷增長的同時的架構設計變化,以及最後如何實現靈活部署,一套程式碼適配各種環境。

OneAPM 在 2013 年開始涉足 APM 市場,當時在13年做了我們的第一代產品 Si ,它是那種龐大的單體應用,功能也十分單一。

在 2014 年初 OneAPM 基於使用者需求和學習國外同類產品 NewRelic 開發了第一版 Ai 3.0。它的架構非常簡單,就是一個收集端收集探針的資料寫入Kafka,然後落到HBase裡,還有一個資料展示端直接查詢HBase的資料做展示。

在 2015 年初的時候,企業版開始做架構演進,首先是在儲存這塊,對於之前用 HBase 的聚合查詢部分改用 Druid,對於 Trace 和 Transaction 資料轉而使用 MySQL,同時,我們學習國外競品 dynaTrace 完善了我們的分析模型。

2016 年的時候,我們發現儲存是比較大的問題,無論是交付上,還是未來按照資料量擴容上。且 Druid 的部署、查詢等都存在一些問題。在SaaS上線Druid版本之後,我們調研各類儲存系統結合業務特點選用ClickHouse,並基於它開發了代號為金字塔的查詢和儲存模組。

2017 年的時候,我們開始梳理各個業務系統、元件,將它們全部拆分,公共元件服務化、Boot化,打通了各個系統。

這是2014年初期的第一次封閉開發後的架構,當時正好大資料Hadoop之類的比較火,所以初期的架構我們完全是基於它來做的。我們的前端應用分為 Data Collector 資料收集端,Data Viewer 資料展示層。探針端走 Nginx 將資料上傳至DC來進行分析處理,頁面訪問通過DV獲取各種資料。 Data Viewer 初期是直接讀取 HBase 的,後面進行簡化,部分熱資料(最近5分鐘呼叫統計),快取於 Redis。

這裡要提一下它和雲跡的應用效能分析的區別,我們為了減少HTTP請求量和流量(小公司)探針端做了聚合和壓縮,一分鐘上傳一個資料包。所以DC端變為解包,然後寫入Kafka,對於最新的 Trace 資料,我們寫入 Redis 用於快速查詢生成拓撲圖。

Consumer 主要是處理翻譯探針的 Metric 資料,將其翻譯為具體的監控指標名稱,然後寫入 HBase。

這套架構部署到 SaaS 之後,我們的市場部就開始推廣,當時的日活蠻高,幾十萬獨立 IP。瞬間,我們就遇到了第一個直接問題——HBase 存在寫入瓶頸,HBase在大量資料持續寫入的場景下,經常OOM,十分痛苦。

我們開始分析問題,首先,寫入上,我們拆成了如圖所示的三大部分,而不是之前的單一 HBase。

而就OLAP系統而言,資料讀寫上最大的特點就是寫多讀少,實時性要求不高。所以,查詢中,HBase主要的效能問題是在對於歷史某條具體的 Trace 呼叫指標的查詢(也就是 Select One 查詢)。我們在系統中引入了 MySQL,Metric 資料開始雙寫 HBase 和 MySQL。Redis 負責生成最新的呼叫拓撲,只有一條最新的 Trace 記錄,MySQL 儲存 Metric 資料,HBase 儲存所有的 Trace 和 Metric 資料進行聚合查詢。DV 還會將一些熱查詢結果快取於 Redis 中。

這個時候的 Consumer 開始負責一定量的計算,會分出多個 Worker 在 Kafka 上進行一些處理,再將資料寫入 Kafka,HBase 改為消費 Kafka 的資料。(這麼做的目的,就是為了線上上拆分出不同的 Consumer 分機器部署,因為 SaaS 上的資料量,連 Consumer 都開始出現瓶頸。)

在這個時候,我們引入了 Camel 這個中介軟體,用它將 Kafka 的操作,MySQL 的操作,還有和 Redis 的部分操作都轉為使用 Camel 操作。在我介紹為什麼使用 Camel 之前,我想先簡單介紹一下它。(下一頁PPT)

我們在引入 Camel 的時候,主要考慮幾個方面:

第一,遮蔽Kafka這一層。當時SOA還比較流行,我們希望能找到一個類似 ESB 的設計,能將各個模組的資料打通。就比如MQ,它可能是Kafka,也可能是 RabbitMQ,或者是別的東西,但是程式開發人員不需要關心。
第二,我們希望一定程式上簡化部署運維的麻煩,因為所有的 camel 呼叫 Route 的核心,就是 URL Scheme,部署配置變為生成 URL。而不是一個個變數屬性配置。
第三,camel 自身的整合路由,可以實現比較高的可用性,它有多 Source 可以定義選舉,還有 Fallback,可以保證資料儘可能不會丟失。(我們就曾經遇到 Kafka 掛了丟資料的情況,大概丟了3個小時,後面通過配置失敗寫檔案的 camel 策略,資料很大程度上,避免了丟失。)

而且,上面的功能,基本都是寫Camel DSL,而無需修改業務程式碼。核心就是一個詞——解耦。

Camel 用官方的話來說,就是基於 Enterprise Integration Patterns 的 Integration Framework。在我看來,Camel 在不同的常見中介軟體上實現整合,Camel 自身定義好鏈路呼叫 DSL(URL Scheme 和 Java、Scala、Spring XML 的實現),還有核心的企業級整合模式的設計思想,組成了 Camel 這個框架。

我們通過定義類似右側的資料呼叫路由,將Kafka等各類中介軟體完全抽象出來,應用程式的邏輯轉為,將資料存入Camel Producer,或者從 Camel Router 中註冊 Endpoint 獲取資料,處理轉入另一個資料 Endpoint。(回到前面的架構圖)

當然我們在開發過程中也設計了很多很有意思的小工具,Mock Agent 便是其中之一。

當時我們經常遇到的開發測試問題是,測試不好造資料來進行測試,無法驗證某些特定指標的資料,開發無法脫離探針團隊單獨驗證新功能和資料。於是我們決定自己寫一套探針資料生成器,定義了一套DSL語言,完整地定義了應用、探針等資料格式,並能自動按照定義規則隨機生成指定資料到後端。

測試需要做的事情,就是寫出不同的模擬探針模板。第一,簡化了測試。第二,將測試用例能程式碼化傳承。避免人員流動的問題。

後面基於它,我們還寫了超級有意思的壓測工具,用其打資料測試後端。還有自動化測試等。

當然,這也是我們嘗試開發的第一個 DSL。

主要是我們無法避免寫入熱點問題,即使基於 Row Key 進行了寫入優化,大資料量的寫入也常常把 HBase 搞掛。

最關鍵的是,持續的 OOM 丟資料,已經給我們的運維帶來的太多麻煩,對外的 SLA 也無法保證。(這個時間段你經常聽到外面對OneAPM的評價就是資料不準,老是丟資料。)

基於 HBase 的查詢時延也越來越高,甚至某種程度上,已經不大能支撐新的資料量。當時最高峰的時候,阿里雲機器數量高達 20 臺。所以,是時候考慮引入新的資料庫了。

這個時候,來自 IBM 研究院的劉麒贇向我們推薦了Druid,並在我們後面的實踐中取代了 HBase 作為主要的 Metric 儲存。

2015年的時候 Druid 架構主要就是上述這張圖,Druid 由4大節點組成, Real-time、Coordinator、Broker、Historical 節點,在設計之初就考慮任何一個節點掛了,不會影響其他節點。

Druid 對於資料的寫入方式有兩種,一種是實時的,直接寫入 Real-time 節點,對應的是那種寫多讀少的資料,還有一種是批量的直接寫入底層資料儲存的方式,一般是對應讀多寫少的資料。這兩種方式在 OneAPM 都有涉及,Ai 作為應用效能監控,對應的是海量的探針資料,主要是使用實時寫入。Mi 是移動端效能監控,探針上傳資料存在時延等問題,所以是在上層做了簡單的處理緩衝後,批量寫入 Deep Storage。

Real-time 節點主要接受實時產生的資料,例如 Kafka 中的資料。資料會在實時節點的記憶體中進行快取處理,構建 memtable,然後定時生成 Segment 寫入 Deep Storage。寫入 Deep Storage 的資料會在 MySQL 生成 meta 索引。

Deep Storage 一般是 HDFS 或者是 NFS,我們在查詢的時候,資料來源於 Deep Storage 或者是 Real-time 節點裡面的資料。

協調節點主要是用於將 Segment 資料在 Historical 節點上分配,Historical 節點會自行動態從 Deep Storage 下載 Segment 至磁碟,並載入到記憶體查詢提供結果。

Broker Nodes 只提供對外的查詢,它不儲存任何資料,只會對部分熱點資料做快取。它會從 Realtime 節點中查詢到還在記憶體未寫入 Deep Storage 的資料,並從 Historical 節點插入已經寫入 Deep Storage 的資料,然後聚合合併返回給使用者。

所以,我們可以看到資料寫入和查詢遵循上面的資料流圖,這裡我們沒有把協調節點畫出。

資料在 Druid 上的物理儲存單位為 Segment,他是基於 LSM-Tree 模型儲存的磁碟最小檔案單位,按照時間範圍劃分,連續儲存在磁碟上。 在邏輯上,資料按照 DataSource 為基本儲存單元,分為三類資料:

  1. Timestamp:時間戳,每條資料都必須有時間。
  2. Dimension:維度資料,也就是這條資料的一些元資訊。
  3. Metric:指標資料,這類資料將在 Druid 上進行聚合和計算,並會按照一定的維度聚合儲存到實際檔案中。

除了上述說的查詢方式 OLAP 的資料其實有幾大特性很關鍵:

  1. 不可變,資料一旦產生,基本上就不會變化。換言之,我們不需要去做UPDATE操作。
  2. 資料不需要單獨的刪除操作。
  3. 資料基於時間,每條資料都有對應的時間戳,且每天的資料量極高

所以,對於一個 OLAP 系統的資料庫,它需要解決的問題也就兩個維度:寫入 和 查詢。

對於 Druid 而言,它支援的查詢有且不僅有上面的四種方式。但是,我們進行梳理後發現,OneAPM的所有業務查詢場景,都可以基於上述四種查詢方式組合出來。

於是在基於 Druid 開發的時候我們遇到的第一個問題就是 Druid 的查詢方式是 HTTP,返回結果基本是 JSON。我們用 Druid 比較早,那個時候的 Druid 還不像現在這樣子,支援 SQL 外掛。

我們需要做的第一個事情,就是如何簡化這塊整合開發的難度。我們第一時間想到的就是,在這上面開發一套 SQL 查詢語法。我們使用 Antlr4 造了一套 Druid SQL,基於它可以解析為直接查詢 Druid 的 JSON。

並基於這套 DSL 模型,我們開發了對應的 jdbc 驅動,輕鬆和 MyBatis 整合在一起。最近這兩週,我也嘗試在 ES 上開發了類似的工具,SQL 模型與解析基本寫完了:https://github.com/syhily/elasticsearch-jdbc

當然這種實現不是沒有代價的,我的壓測的同事和我抱怨過,這種方式相比純 JSON 的方式,效能下降了 50%。我覺得,這裡我們當時這麼設計的首要考慮,是在於簡化開發難度,SQL對每個程式設計師都是最熟悉的,其次,我們還有一層考慮就是未來更容易適配別的儲存平臺,比如 ES(當時其實在15年中旬的時候也列入我們的技術選型)。

Druid 另一個比較大的問題就是,它實在是太吃硬體了。記得之前和今日頭條的廣告部門研發聊天,聊到的第一個問題就是 Druid 的部署需要 SSD。

我們在前面的架構分析當中很容易發現,Druid 本質上還是屬於 Hadoop 體系裡面的,它的資料儲存還是需要 HDFS,只是它的資料模型基於 LSM-Tree 做了一些優化。換言之,它還是很吃磁碟 IO。每個 Historical 節點去查詢的時候,都有資料從 Deep Storage 同步的過程,都需要載入到記憶體去檢索資料。雖然資料的儲存上有一定的連續性,但是記憶體的大小直接決定了查詢的快慢,磁碟的 IO 決定了 Druid 的最終吞吐量。

另外一個問題就是,查詢代價問題。Druid 上所有的資料都是要制定聚合粒度的,小聚合粒度的資料支援比它更大粒度的聚合資料的查詢。

比如說,資料是按照1分鐘為聚合粒度儲存的化,我們可以按照比1分鐘還要長的粒度去查詢,比如按照5分鐘一條資料的方式查詢結果。但是,查詢的時間聚合單位越大,在分鐘的聚合表上的代價也就越高,效能損失是指數級的。

針對上面兩個問題,我們的最終解決方案,就是資料不是寫一份。而是寫了多份,我們按照業務的查詢間隔設定了3~4種不同的聚合表(SaaS和企業級的不同)。查詢的時候按照間隔路由到不同的 Druid 資料表查詢。某種程度上規避了磁碟 IO 瓶頸和查詢瓶頸。

在充分調研和實踐後,有了上面的新架構圖。3.0 到 4.0 的變化主要在HBase儲存的替換,資料流向的梳理。

我們將探針的資料分為三大類,針對每類的資料,都有不同的儲存方式和處理方式。

探針上傳的資料,分為三大類,Trace、Metrics、Analytic Event。Trace 就是一次完整的呼叫鏈記錄,Metrics 就是系統和應用的一些指標資料。Analytic 資料使我們在探針中對於一些慢 Trace 資料的詳細資訊抓取。最終所有的 Metrics 資料都寫入 Druid,因為我們要按照不同的查詢間隔和時間點去分析展示圖表。Traces 和事物類資訊直接儲存 MySQL,它對應的詳細資訊還需要從 Druid 查詢。對於慢 Trace 一類的分析資料,因為比較大,切實時變化,我們存入到 Redis 內。

但是,Druid 一類的東西從來都不是一個開箱即用的產品。我們前面在進行資料多寫入優化,還有一些類似 SelectOne 查詢的時候,越來越發現,為了相容 Druid 的資料結構,我們的研發需要定製很多非業務類的程式碼。

比如,最簡單的一個例子,Druid 中查到一個 Metric 指標資料為 0,到底是這個資料沒有上傳不存在,還是真的為 0,這是需要商榷的。我們有些基於 Druid 進行的基線資料計算,想要在 Druid 中儲存,就會遇到 Druid 無法更新的弊端。換句話說,Druid 解決了我們資料寫入這個直接問題,查詢上適用業務,但是有些難用。

針對上述這些問題,我們在16年初開始調研開發了現有的金字塔儲存模組。它主要由金字塔聚合模組 Metric Store 和金字塔讀取模組 Analytic Store 兩部分組成。

因為架構有一定的傳承性。所以它和 Druid 類似,我們只支援 Kafka 的方式寫入 Metric 資料,HTTP JSON 的方式暴露查詢介面。基於它我們改造 Druid SQL,適配了現有的儲存。它的誕生,第一點,解決了我們之前對於資料雙寫甚至多寫的查詢問題。

我們在要求業務接入金子塔的時候,需要它提供上述的資料格式定義。然後我們會按照前面定義的聚合粒度表,自動在 Backend 資料庫建立不同的粒度表。

金字塔儲存引擎的誕生,其實主要是為了 ClickHouse 服務的,接下來,請允許我先介紹一下 ClickHouse。

從某種角度而言,Druid 的架構,查詢特性,效能等各項指標都十分滿足我們的需求。無論是 SaaS,還是在 PICC 的部署實施結果都十分讓人滿意。

但是,我們還是遇到了很多問題。

  1. 就是 Druid 的丟資料問題,因為它的資料對於時間十分敏感,超過一個指定閾值的舊資料,Druid 會直接丟棄,因為它無法更新已經持久化寫入磁碟的資料。
  2. 和第一點類似,就是 Druid 無法刪除和更新資料,遇到髒資料就會很麻煩。
  3. Druid 的部署太麻煩,每次企業級的交付,實施人員基本無法在現場獨立完成部署。(可以結合我們前面看到的架構圖,它要MySQL去存meta,用 zk 去做協調,還有多個部署單元,不是一個簡單到能傻瓜安裝的程式。這也是 OneAPM 架構中逐漸淘汰一些元件的主要原因,包括我們後面談到的告警系統。)
  4. Druid 對於 null 的處理,查詢出來的 6個時間點的資料都是0,是沒資料,還是0,我們判斷不了。

所以,我們需要在企業級的交付架構中,採取更簡單更實用的儲存架構,能在機器不變或者更小的情況下,實現部署,這個時候 ClickHouse 便進入我們的技術選型中。

https://yufan.me/evolution-of-data-structures-in-yandexmetrica/

在介紹 ClickHouse 之前,我覺得有必要分享一下常見的兩種資料儲存結構。

第一種是 B+ Tree或者是基於它的擴充套件結構,它常見於關係型資料的索引資料結構。我們以 MySQL 的 MyISAM 引擎為例,資料在其上儲存的時候分為兩部分,按照插入順序寫入的資料檔案和 B+ Tree 的索引。葉子節點儲存資料檔案的位移。當我們讀取一個索引中的範圍資料時,首先從索引中查出一組滿足查詢條件的資料檔案位移,然後按照查出來的位移依次去從資料檔案中查詢出實際的資料。

所以,我們很容易發現,我們想要檢索的資料,往往在資料庫上不是連續的,上圖顯示常見的資料庫磁碟中的檔案分佈情況。當然我們可以換用 InnoDB,它會基於主機定義的索引,寫入順序更加連續。但是,這勢必會匯入寫入效能十分難看。事實上,如果拿關係型資料庫儲存我們這種類似日誌、探針指標類海量資料,勢必會遇到的問題就是寫入快,查詢慢;查詢快,寫入慢的死迴圈。而且,擴容等操作基本不可能,分庫分表等操作還會增加程式碼複雜度。

所以,在非關係型資料庫裡面,常見的儲存結構是 LSM-Tree(Log-Structured Merge-Tree)。首先,對於磁碟而言,順序寫入的效能是最理想的。所以常見的 NoSQL 都是將磁碟看做一個大的日誌,每次直接在後端批量增加新的資料以達到連續寫入的目的。但就和 MyISAM 一樣,會遇到查詢時的問題。所以 LSM-Tree 就應運而生。

它在記憶體中和磁碟中分別使用兩種不同的樹結構儲存資料,並同時對外提供查詢能力。如 Druid 為例,在記憶體中的資料,會按照時間範圍去聚合排序。然後定時寫入磁碟,所以在磁碟中的檔案寫入的時候已經是排好序的。這也是為何 Druid 的查詢一定要提供時間範圍,只有這樣,才能選取出需要的資料塊去查詢。

當然,聰明的你一定會問,如果記憶體中的資料,沒有寫入磁碟,資料庫崩潰了怎麼辦。其實所有的資料,會先以日誌的形式寫入檔案,所以基本不會丟資料。

這種結構,從某種角度,儲存十分快,查詢上通過各種方式的優化,也是可觀的。我記得在研究 Cassandra 程式碼的時候印象最深的就是它會按照資料結構計算位移大小,寫入的時候,不足都要對齊資料,使得檢索上有近似 O(1) 的效果。

昨天湯總說道 Schema On Read,覺得很好,我當時回覆說,要在 HDFS 上動手腳。其實本質上就可以基於 LSM-Tree 以類似 Druid 的方式做。但是還是得有時間這個指標,查詢得有時間的範疇,基於這幾個特點才有可能實現無 Schema 寫入。

Druid 的特點是資料需要預聚合,然後按照聚合粒度去查詢。而 ClickHouse 屬於一種列式儲存資料庫,在查詢 SQL 上,他和傳統的關係型資料庫十分類似(SQL引擎直接是基於MySQL的靜態庫編譯的)它對資料的儲存索引進行優化,按照 MergeEngine 的定義去寫入,所以你會發現它的查詢,就和上面的圖一樣,是連續的資料。

因為 ClickHouse 的文件十分少,大部分是俄文,當時我在開發的時候,十分好奇去看過原始碼。他們的資料結構本質上還是樹,類似 LSM tree。印象深刻的是磁碟操作部分的原始碼,是大段大段的彙編語句,甚至考慮到4K對齊等操作。在查詢的時候也有類似經驗性質的位移指數,他們的註釋就是基於這種位移,最容易命中資料。

對於 ClickHouse,OneAPM 乃至國內,最多隻實現用起來,但是真正意義上的開發擴充套件,暫時沒有。因為 ClickHouse 無法實現我們的聚合需求,金字塔也為此擴充套件了聚合功能。和 Druid 一樣,在 ClickHouse 上建立多種粒度聚合庫,然後儲存。

這個階段的架構,就已經實現了我們最初的目標,將所有的中介軟體解耦,我們沒有直接使用 Kafka 原生的 High Level API,而是基於 Low Level API開發了 Doko MQ。目的是為了實現不同版本 Kafka 的相容,因為我們現在還有使用者在使用 0.8 的 Kafka 版本。Doko MQ 只是一層外部的封裝,Backend 不一定是 Kafka,考慮到有對外去做 POC 需求,我們還原生支援 Redis 做MQ,這些都在 Doko 上實現。

儲存部分,除了特定的資料還需要專門去操作 MySQL,大部分直接操作我們開發的金字塔儲存,它的底層可以適配 Druid 和 ClickHouse,來應對 SaaS 和企業級不同資料量部署的需要。對外去做 POC 的時候,還支援 MySQL InnoDB 的方式,但是聚合一類的查詢,需要耗費大量的資源。

部署與交付是週一按照湯總的要求臨時加的,可能 PPT 準備的不是很充分,還請大家多多包涵。

Java 應用部署於應用容器中,其實是受到 J2EE 的影響,也算是 Java Web 有別於其他 Web 快速開發語言的一大特色。一個大大的 war 壓縮包,包含了全部的依賴,程式碼,靜態資源,模板。

在虛擬化流行之前,應用都是部署在物理機上的,為了節約成本,多 war 包部署在一個 Servlet 容器內。

但是為了部署方便,如使用的框架有漏洞、專案 jar包的升級,我們會以解壓 war 包的方式去部署。或者是打一個不包含依賴的空 war 包,指定容器的載入某個目錄,這樣所有的war專案公用一套公共依賴,減少記憶體。當然缺點很明顯,容易造成容器汙染。

避免容器汙染,多 war 部署變為多虛擬機器單 war、單容器。

DevOps 流行,應用和容器不再分離,embedded servlet containers開始流行 Spring Boot 在這個階段應運而生。於是專案部署變為 fat jar + 虛擬機器

Docker的流行,開始推行不可變基礎設施思想,例項(包括伺服器、容器等各種軟硬體)一旦建立之後便成為一種只讀狀態,不可對其進行任何更改。如果需要修改或升級某些例項,唯一的方式就是建立一批新的例項以替換。

基於此,我們將配置檔案外接剝離,由專門的配置中心下發配置檔案。

最初的時候,Docker 只屬於我們的預研專案,當時 Docker 由劉斌(他也是很多中文 Docker 書的譯者)引入,公司所有的應用都實現了容器化。這一階段,我們所有的應用都單獨維護了一套獨立的 Docker 配置檔案,通過 Maven 打包的方式指定 Profile 的方式,然後部署到專門的測試環境。換句話說,Docker 只是作為我們當時的一種測試手段,本身可有可無。

2015年上半年,紅帽的姜寧老師加入 OneAPM,他帶來了 Camel 和 AcmeAir。AcmeAir 本來是 IBM 對外吹牛逼賣他的產品的演示專案,Netflix 公司合作之後覺得不好,自己開發了一套微服務架構,並把 AcmeAir 重寫改造成它元件的演示專案,後面 Netflix 全家桶程式設計了現在很多北京企業在嘗試的 Spring Cloud。而 AcmeAir 在 PPT 中的 Docker 部署拓撲也成了我們主要的學習方式。

那個時候還沒有 docker-compose、docker-swarm,我們將單獨維護的配置檔案,寫死的配置地址,全部變為動態的 Hosts,本質上還是指令碼的方式,但是已經部分實現服務編排的東西。

然後我們開始調研最後選型了 Mesos 作為我們主要的程式部署平臺,使用 Mesos 管理部署 Docker 應用。在上層基於 Marthon 的管理 API 增加了配置中心,原有指令碼修改或者單獨打包的配置檔案變為配置中心下發的方式。最後,Mesos 平臺只上線了 SaaS 並部署 Pinpoint 作為演示專案,並未投產。

後面,在告警系統的立項開發過程中,因為要和各個系統的整合測試需要,我們慢慢改寫出 docker-compose 的方式,廢棄掉額外的 SkyDNS。

Mesos 計劃的夭折,主要原因是我們當時應用還沒有準備好,我們的應用主要還都是單體應用各個系統間沒有打通。於是在 16年我們解決主要的儲存問題之後,就開始著力考慮應用整合的問題。

應用服務化是我們的內部嘗試,是在一次次測試部署和對外企業交付中的血淚總結。當時我們考慮過 Spring Integration,但是它和 camel 基本如出一轍,也調研過 Nexflix 全家桶,最後我們只選用了裡面的 zuul 做服務閘道器。

在應用層面,我們按照上圖所示,將所有的應用進行服務化拆分,分成不同的元件開發維護,並開發了註冊中心等元件。RPC 這邊,我們沒有使用 HTTP,而是和很多公司一樣包裝了 Thrift。

我們基於前面的服務拆分,每個應用在開發的時候,都是上述5大模組。中間核心的中介軟體元件,業務系統均無需操心。在交付的時候,也屬於類似公共資源,按照使用者的資料量業務特點彈性選擇。

最小化部署主要是為了給單獨購買我們的某一產品的使用者部署所採用的。

但是我們已經受夠了一個專案維護多套程式碼的苦楚,我們希望一套程式碼能相容 SaaS、企業級,減少開發中的分支管理。於是我們拆分後的另一大好處就體現了,它很容易結合投產未使用的 Mesos 在 SaaS 上實現部署。

為了打通各個產品,我們在原有的前後端分離的基礎上,還將展示層也做了合併,最後實現一體化訪問。後端因為實現了服務化,所有的應用都是動態 Mesos 擴容。CEP 等核心計算元件也能真正意義上和各個產品打通,而不是各做各的。

到了這裡,我的第一階段就算是講完了,大家有問題麼?

告警系統的開發,我們和 Ai 一樣,經歷了幾個階段,版本迭代的時間點也基本一致。整個開發過程中,我們最核心的問題其實不在於告警功能本身,而是其衍生的產品設計和開發設計。

和 Ai 一樣,初期的告警實現特別簡單。當時來自 IBM 研究院的吳海珊加入 OneAPM 團隊,帶來了 Cassandra 儲存,我們當時用的比較早,是 2014 年 2.0 版本的 Cassandra,我們在充分壓測之後,對它的資料儲存和讀寫效能十分滿意,基於它開發了初版告警(草案)。

初版告警的實現原理極其簡單,我們從 Kafka 接收要計算的告警指標資料,每接收到一條指標資料,都會按照配置的規則從 Cassandra 中查詢對應時間視窗的歷史指標資料,然後進行計算,產生警告嚴重或者是嚴重事件。然後將執行的告警指標寫入 Cassandra,將告警事件寫入 Kafka。(看下一頁)

所以你會發現初版的告警,從設計上就存在嚴重的 Cassandra 讀寫壓力和高可用問題。

你會發現,每從業務線推送一條指標資料,我們至少要讀寫兩次 Cassandra。和同時期的 Ai 架構相比,Ai 對 HBase 只有寫入瓶頸,但讀取,因為量不高,反而沒有瓶頸。(回上頁)

這裡是我們和 Ai HBase的對比總結。我們初版的設計和 Ai 一樣都需要全量地儲存指標資料,而且 Cassandra 的儲存分片本身是基於 Partition Key 的方式,資料必須基於 Partition Key 去查詢,我們對於計算指標,按照 業務系統、應用 ID、時間 作為 Partition Key 去儲存。很意外的是幾乎和 HBase 同時出現了讀寫瓶頸。而且比較尷尬的地方也和 Ai 類似,因為 Partition Key 的定義,完全無法解決寫入熱點問題。

所以我們首先想到的是,對於當前的告警架構進行優化,我們有了上述的新架構設計。但是在評審的時候,我們發現,我們做的正是一個典型的分散式流式處理框架都會做的事情,這些與業務邏輯關係不大的完全可以藉助現有技術實現。

由於這個時期(15年)公司整體投產大資料,我們自然把眼光也投入了當時流行的大資料計算平臺。

首先,對於初版的架構,我們需要保留的是原有的計算資料結構和 Kafka 的寫入方式,對於核心的告警計算應用需要去改造。

我們需要實現的是基於單條資料的實時流式計算。需要能分散式水平擴充套件。能按照規則分組路由,避免熱點問題。需要有比較好的程式設計介面。

首先我們考察的便是 Spark,Spark 最大的問題是需要我們人為指定計算的時間視窗,計算的資料也是批量的那種而非單條,這和告警的業務需求本身就不匹配。

因為當時我們想設計的告警計算是實時的,而非定時。Spark Streaming 在後面還因為執行模式進一步被我們淘汰。

Strom 各方面其實都蠻符合我們需求的,它也能實現所謂的單條實時計算。但是,它的計算節點不持有計算狀態,某些時候的視窗資料,是需要有類似 Redis 一類的外部儲存的。

Flink優勢:

Spark 有的功能 Flink 基本都有,流式計算比 Spark 支援要好。

  1. Spark是基於資料片集合(RDD)進行小批量處理,所以Spark在流式處理方面,不可避免增加一些延時。
  2. 而 Flink 的流式計算跟 Storm 效能差不多,支援毫秒級計算,而 Spark 則只能支援秒級計算。
  3. Flink 有自動優化迭代的功能,如有增量迭代。它與 Hadoop 相容性很好,還有 pipeline 模式增加計算效能。

這裡,我需要重點說一下 pipeline 模式。

Staged execution 就如它的名字一樣,資料處理分為不同的階段,只有一批資料一個階段完全處理完了,才會去執行下一個階段。典型例子就是 Spark Streaming

Pipeline 則是把執行序列在了一起,只有有計算資源空閒,就會去執行下一個的操作。在外部表象是隻有一個階段。

上面的不好理解,我們思考一個更形象的例子:

某生產線生產某鍾玩具需要A,B,C三個步驟,分別需要花費10分鐘,40分鐘,10分鐘,請問連續生產n個玩具大概需要多少分鐘?

總結:

stage的弊端是不能提前計算,必須等資料都來了才能開始計算(operateor等資料,空耗時間)。pipeline的優勢是資料等著下一個operateor有空閒就立馬開始計算(資料等operateor ,不讓operateor閒著,時間是有重疊的)

綜合前面的調研分析,我們有了上面這張表格。對於我們而言,其實在前面的分析中 Flink 就已經被我們考慮了,尤其是它還有能與 Hadoop 體系很好地整合這種加分項。

綜合前面的分析,我們最終選擇 Flink 來計算告警,因為:

  1. 高效的基於 Pipeline 的流式處理引擎
  2. 與 Hadoop、Spark 生態體系良好的相容性
  3. 簡單靈活的程式設計模型
  4. 良好的可擴充套件性,容錯性,可維護性

在架構邏輯上面,我們當時分成了上述五大塊。

後設資料管理主要指的是告警規則配置資料,資料接入層主要是對接業務系統的資料。

計算層主要是兩類計算,異常檢測:按照配置的靜態閾值進行簡單的計算對比、No Event 無事件監測,主要是監控應用的活動性。

快取區主要是計算資料佇列的快取和應用告警狀態的快取。儲存區第一塊是從原有架構繼承的 Cassandra。離線儲存是考慮給別的大資料平臺共享資料使用的。

這裡畫的是 Standalone 的部署方式,也是我們在本地開發測試的架構,在生產上,我們採用了 Flink on YARN 的部署模式。

對於 Flink 的任務排程,我們以左下角的一個簡單操作為例,它是一個 source(4) -> map(4) -> reduce(3),其排程在 Flink 中如圖所示,會分成幾個不同的 TaskManager 來操作,每個 TaskMananger 中有多個執行單元,但呈管道式。將外部網狀的處理流程變為獨立的線性處理任務。

我們基於 Flink 首先需要開發的,就是異常檢測流程。告警的異常檢測就相當於 Flink 的一個 Job(Streaming),藉助 Flink 簡單易用的程式設計模型,我們可以快速的構建我們的 Flow。

在設計的初期,我們考慮了幾個方面的問題:

  1. 作為通用的計算引擎,誰可以使用這個 Job。
  2. 如果後面某些產品提出一些變態的需求,我們是否可以快速開發一個針對特殊需求的 Job 提交到同一的平臺去執行?
  3. 平臺是否可以提供穩定的執行環境、可維護性、可擴充套件性、可監控性以及簡單高效的程式設計模型,我們們可以把更多的精力放在兩個方面:a.業務;b.平臺研究(確保穩定性)
  4. 生產上統一到 Yarn 上之後,我們可以在一個叢集上公用一份資料,根據不同場景使用不同的計算引擎做他適合做的事情。比如,暴露資料給 Spark 團隊使用。
  5. Akka 叢集化研究,我們原有的 Akka 開發經驗,不能浪費。對於企業交付,還是需要有一個小而美的程式架構。Akka 那個時候是 2.3 版本,提供了 Akka Cluster,重新被我們納入研究範疇。

我們遇到的第一個問題,就是多資料來源,生產上提供計算資料來源的可能不僅僅是 Ai 一個產品,還有別的產品。我們研究後發現,Fink 原生支援多資料來源。

說到Rule的問題,我們逃不開一個問題:Rule管理模組到底應不應該拆出來。

首先,後設資料管理的壓力不大,資料量也不會大到哪裡去,他的更新也不是頻繁的。 其次,讓 Flink 在各個節點上啟動一個 Web服務去更新規則是不現實的,也不值當。

所以,把Rule管理模組單獨抽取出來是合適的。

抽取出來之後,自然就涉及告警計算的 Job 如何感知 Rule 變更的問題:

完全依賴外部儲存,例如 Redis,Job 每次都去查儲存獲取 Rule(這樣完全規避了 Rule 更新問題,但是外部儲存能夠扛得住是個問題,高併發下 Redis 還是會成為瓶頸)。 Job記憶體裡自己快取一份 Rule,並提供更新機制。

無論怎麼搞都得依賴外部通知機制來更新 Rule,比如後設資料管理模組更新完 Rule 就往 Kafka 傳送一個特殊的 Event。運算元收到特殊的 Event 就去資料庫裡把對應的 Rule 查詢出來並更新快取。

有了更新機制,我們要解決的就是如何在需要更新的時候通知所有的運算元,難點是一個特殊的Event只能傳送給一個運算元例項,所以我們上面採用了單例項,存在兩個問題。

  1. 效能瓶頸
  2. 訊息表變大了(key,event)—(key,event,rule),更加消耗資源

其實,我們忽略了一個問題,當Rule有更新的時候我們完全沒必要通知所有的運算元例項。

雖然我們不是一個 Rule 對應一個運算元,但是 Flink 是提供分割槽機制的,我們已經用 key 做了hash。Rule 的更新不會更新 key,產生的特殊 Event 會分割槽到固定的運算元具體例項,具有相同 key 的 Event 也必然被分割槽到相同的運算元例項。所以我們的擔心是多餘的,而且藉助分割槽機制,我們對記憶體的佔用會更小,每個運算元例項只快取自己要用的Rule。

所以 Rule 的更新只有三種場景:初始化時不做預載入快取,第一次使用Rule時查資料庫並快取,收到內建Event時更新快取。

No Events 檢測主要的問題是 Flink 是實時資料計算,他是來一條資料計算一次。無事件本身的的特點就是沒有資料推送過來,無法觸發計算。

這個問題其實已經非常好解決了,我們在告警計算的流程裡已經更新每個Rule對應event的最後達到時間到Redis了。我們可以單起一個批處理job簡單運算一次即可,邏輯非簡單,我測了一下16000個Rule,5個併發度,可以在5s內計算一次。注:帶註釋才用了不到120行java程式碼,稍加改進即可在生產上使用。

最終,我們在解決上述問題之後在阿里雲上實現了上述的告警計算平臺。

從某種角度而言 Flink 版本是第一個在 SaaS 上投產的系統,然而,它並不完美,有著上述這些問題。

從某種角度而言,Flink 計算告警有些大材小用,我們需要更輕量的架構。(這裡中斷,展示一下我們的告警系統。)

在 Flink 版本開發3個月後,我們開始著手開發新的企業級告警平臺。因為現有的 Flink 版本,因為很多原因無法對外交付。

我也是從這個時候開始參與 OneAPM 告警的研發,我們做的第一件事情,就是結合之前 DSL 開發的經驗,思考如何重新定義告警規則。這是因為 Flink 上定義的告警規則,就和現有的雲跡 CTMAM 的告警規則一樣,比較死板,不好擴充套件,且較為複雜。

這期間也參考了 Esper 之類的開源專案,比較後我們驚喜地發現,最好的告警規則定義方式就是 SQL。

我們在定義好規則模板之後,便開始由解析計算引擎 -> 處理佇列引擎 -> 分散式管理平臺 -> 操作介面的順序 開發了現有的告警引擎。

首先是基於規則 DSL 的解析計算引擎。之前的 Mock Agent,我們使用的是 Scala 原生提供的語法分析組合子設計的。Druid SQL 使用的是 Antlr4,先解析出基本的 AST 語法樹,然後轉義為 Druid JSON 查詢模型,最後序列化為查詢 JSON。

這裡的告警規則 SQL,我們用的是類似 Druid SQL 的方式。語法模板定義甚至都是類似的。只是增加了四則混合運算表示式的解析和運算,還有 avg 一類的計算函式的實現。

最終,它的解析處理流程就和 PPT 圖示的一樣。規則 SQL 語句被 Antlr4 做詞法語法分析,將部分非邏輯單元符號化,然後構建出一棵 SQL 語法樹。我們按照 Antlr4 提供的 Visitor 模式,以深度優先檢索的方式遍歷,然後不斷的將結果按照定義的運算元單元組合。最後對外暴露出兩個方法,一個返回布林值表示是否滿足規則運算定義,另一個返回計算中想要獲取的指標資料。

我們基於解析出來的規則物件,在 Engine 層對計算的事件佇列和當前事件結合起來,就產生了實際想要的計算結果。

Engine 就相當於最小粒度的計算單元,但是,它缺少一些上下文管理。我們需要事件佇列管理,規則和計算資料的關聯,才能真正意義上呼叫 Engine 去計算。

基於這個需求,我們開發了 Runtime 模組。它在邏輯上有兩大抽象,一個是 RuntimeContext,一個是 EventChannel。

RuntimeContext 就和它的名稱一樣,表示執行時上下文,每個RuntimeContext 對應一條具體的規則示例,內部會維護對應的 RuleTemplate。我們在設計初期就考慮類似多資料來源的情況,一條計算規則可能對應多個探針資料,於是內部定義了 InputStream 的概念。

它相當於實際的一條計算指標資料流,實際儲存在 EventChannel 上,EventChannel 為在記憶體中儲存的一個指標資料佇列。它有兩塊資料:一個是一個 Event Queue,一個是當前才來的一條要計算的指標資料。Event Queue 的設計參考了 Guava Cache 裡面的佇列,因為規則建立時對應的資料視窗大小是確定的,於是這個 Queue 的大小也是確定的。

一個 RuntimeContext 示例可能對應多個 EventChannel,一個 EventChannel 也可能對應多個 RuntimeContext,二者基於一個唯一的 key 關聯起來。我們修改規則的時候,需要修改對應的 RuntimeContext。事件來了要計算的時候,是直接 sink 到 EventChannel 中。

Runtime 相當於 Flink 裡面最小的計算任務,有著自己的狀態,能解析 SQL 並進行運算。

但是對於分散式、叢集等部署環境,它還存在著較大的問題。在其之上,我們使用 Akka 開發了核心的執行模組。

我們使用 Akka Cluster 開發了計算叢集,Akka Cluster 將 Akka 應用分為 Seed Node 和一般 Node。啟動的時候,要先啟動 Seed Node,才能啟動子 Node。但是啟動後如果 Seed Node 掛了,Akka 可以選出一個新的存活節點當做 Seed Node。

我們在 Akka 叢集啟動後,會使用 Seed Node 建立 Kafka Message Dispatcher Actor 來和 Kafka 消費資料,然後分發到各個子節點上。這麼做的話,同一時刻,只有一個執行緒在從 Kafka 消費資料。使用單執行緒的考慮有很多,比如避免 Kafka repartition。其次,我們測試後發現,從 Kafka 消費這塊使用單執行緒不存在瓶頸。

每個 Akka 節點都分為 EvenStreamActor、RuleActor 兩類核心處理計算單元,EventStreamActor 除了管理 EventChannel 之外,還會將資料分發到別的 Akka 節點,做二次計算。RuleActor 管理 RuntimeContext,其下包含 Persist Actor 將告警事件和應用實時狀態持久化到金字塔儲存,Alert Actor 將告警資料寫入至 Doko MQ 用於接入系統執行告警行為(如簡訊、郵件、WebHook 等)。

Jetty模組本身用於暴露介面對外提供規則、事件、資料來源管理。和 Flink 版本一樣,我們遇到了一個問題就是如何在所有的 Akka 叢集上更新告警規則。

後面我們的實現策略和 Flink 的版本一樣,規則在 Cassandra 上更新完畢後,會以特定的更新訊息寫入 Kafka 中。這個時期,所有的告警規則配置,使用使用者,告警資料來源的配置,都儲存在 Cassandra 中。因為 Partition Key 的建立不大合理,也給我們在做檢索,分頁等操作時,尤其是告警事件的篩選,帶來了極大的麻煩。這也直接導致我們在 3.0 版本里面將所有的配置資料存於 MySQL,告警事件改為使用金字塔儲存。

基於計算引擎,我們抽象出三大邏輯模組,告警計算和管理模組、告警策略管理模組、推送行為管理模組。

  1. 計算引擎,也就是前面說的 Runtime 模組那層、它只關心什麼規則,基於資料算出一個 true/false 的布林值表示是否告警,同時返回計算的指標集。
  2. 事件生成引擎,它基於前面的計算結果,還有指標的後設資料等組合生成實際的告警事件。2.0 版本只有三種:普通、警告、嚴重。
  3. 推送行為模組,其實就是配置支援哪些通知使用者的方式,在 2.0 裡面只有發郵件、執行 Shell,3.0 之後支援 Web Hook 和簡訊。
  4. 策略模組,就是關聯某個規則應該用那些現存的通知配置。

2.0 版本的告警系統主要是 CEP 計算引擎模組,所以在部署上,他是整合在各個業務系統上的。

2.0 的時候,告警系統只產生三類事件,普通事件、警告事件、嚴重事件。我們調研之後發現,其實使用者在意的不是這類事件,而是這三類事件相互轉換之後產生的事件。

於是我們重新定義了告警事件。

所以我們將告警引擎產生的事件分為兩大類:HealthStatusEvent、HealthRuleVolatation 事件。

前者就是圖上的三個圈,也就是前面的正常、警告、嚴重。(做滑鼠指點狀)應用狀態從“普通”到“嚴重”會產生“開啟嚴重”事件。應用狀態從“警告”到“普通”會產生“關閉警告”事件,應用狀態持續在“警告”或者“嚴重”會產生持續類事件。我們對於告警的觸發配置轉為這種狀態轉換的事件。

有了前面的設計之後,我們遇到了第一個問題,如何在現行的 Akka 應用上設計一個告警事件狀態機。我們想了很多方式,後面我們發現,自己完全想岔了。

之前開發的 Engine 模組結合 Runtime 模組完全可以解決這個問題,我們只需要按照之前定義的 8 個事件轉換狀態定義 8條 SQL,配置三個子 RuntimeContext 即可解決這個問題。比如開啟警告事件,它的 SQL 定義如上。也就是之前一個告警事件如果為空或者為NORMAL事件,當前這條事件為警告事件,則生成開啟告警事件。

我們對於不同時間段應用的期望執行情況可能是不一樣的,比如一天當中的幾個小時,一星期中的幾天或者一年當中的幾個月。舉個例子來說,淘寶應用在週末兩天可能會存在較多的交易從而產生高於平時的吞吐量。一個工資支付應用可能相較於一個月中的其他事件,會在月初和月末產生較大的流量。一個客戶管理的應用在週一的營業時間相較於週末來說會有較大的工作負荷壓力。

我們在 2.0 的版本開始受制於 Cassandra。

一方面,我們建表的時候,為了某些效能在 Partition Key 內增加了時間戳導致查詢的時候必須要提供時間區間。另一方面,沿用的是2年前的 Cassandra 版本,無法像 3.0 之後的版本一樣有更豐富的查詢方式,比如基於某一列的查詢。

其次,在 2.0 之前的版本,每條指標的計算結果,就算是 Normal 都會存入 Cassandra,這是因為 Flink 版本計算的遺留問題。而我們在設計了告警事件的狀態變化告警之後,儲存 Normal 變為意義不大。

最後,除了告警事件,其他的資料:如規則、策略、行為等配置資料,撐死了也就幾十萬條,完全沒有必要用 Cassandra 來儲存。它的使用,反而會增加企業級的部署麻煩。

所以我們進行了變更,用 MySQL 去儲存除告警事件之外的資料。告警事件因為有了金字塔模組,所以我們直接寫入 Kafka 即可。

為了應對 2.0 版本的接入麻煩,因為構造 SQL、告警通知行為等在 2.0 版本都是外包給業務線自己做的,我們只是打造了一個小而美的 CEP 引擎。所以只有主要的產品 Ai 接入了我們系統。為此,我們把 Ai 中開發的和告警相對於的程式碼剝離,專門打造了 CEP 上層的告警系統,並要求業務方提供了應用、指標等 API。自行消費處理 Kafka 中的告警事件,觸發行為。

其次,做的一個很大改動就是適配了各個業務線的探針資料,直接接受全量資料。

4.0 階段的告警其實並沒有開發,當時主要協作的另一位同事在6月離職,我在8月底完成 3.0 的工作後也離職,但是設計在年初就完成了。

我們在開發金子塔儲存的時候,很大的一個問題就是如何流式消費 Kafka 的資料,當時正好 Kafka 提供了 Stream 程式設計。我們使用了 Akka Stream 去開發了對應的聚合應用 Analytic Store。

同樣,我們希望這個單獨開發的 CEP 應用,也能變成 Reactive 化。對應的我們將上下行的 Kafka 分別抽象為 Source 和 Sink 層,它們可以使用 Restful API 動態建立,而非現在寫死在資料庫內。

基於這一思想,我們大概有上述的技術架構(圖可能不是很清晰)。

設計目標:

增加CEP處理資料的伸縮性(scalability),水平伸縮以及垂直伸縮 提高CEP引擎的彈性(Resilience),也就是CEP處理引擎的容錯能力

設計思路:

在資料來源對資料進行分流(分治);在Akka叢集裡,建立Kafka Conumser Group, Conumser個數與Topic的分割槽數一樣,分佈到Akka的不同節點上。這樣分佈到Akka某個節點到event資料就會大大減少。

在資料來源區分Command與Event;把Rule相關到Command與採集到metric event打到不同的topic,這樣當Event資料很大時,也不會影響Command的消費,減少Rule管理的延遲。

對Rule Command在Akka中採用singleton RuleDispatcher單獨消費,在叢集中進行分發,並且把註冊ruleId分發到叢集中每個EventDispatcher裡。因為Rule Command流量相對於Event流量太少,也不會出現系統瓶頸。

因為RuleDispatcher在Akka叢集是全域性唯一的,容易出現單點故障。因為RuleDispathcer會儲存註冊後的RuleIds,需要對RuleId進行備份,這個可以採用PersitentActor來

實現

對於RuleDispatcher down掉重啟的這段時間內,因為RuleDispatcher分發過RuleId到各個節點的EventDispatcher,因此各個節點事件分發暫時不會受到影響。 在Akka叢集裡每一個Kafka conumser,對於一個EventDipatcher,負責把事件分發對感興趣對RuleActor(根據每個RuleId對應感興趣對告警物件)。

常見的聚類演算法有三類:基於空間劃分的、基於層次聚類的和基於密度聚類的方法。聚類演算法一般要求資料具有多個維度,從而能夠滿足對海量樣本進行距離(相似性)度量,從而將資料劃分為多個類別。其中維度特徵一般分為CPU利用率、磁碟利用率、I/O使用情況、記憶體利用率、網路吞吐量等。

  1. 相似性度量方法

相似性度量一般採用LP正規化,如L0、L1、L2等,其一般作為兩個樣本間的距離度量;Pearson相關係數度量兩個變數的相似性(因為其從標準分佈及方差中計算得到,具有線性變換不變性);DTW(動態時間規整演算法)用於計算兩個時序序列的最優匹配。 其中基於LP正規化的時間複雜度最低O(D)

  1. 資料壓縮(降維)方法

在資料維度較大的情況下,通過資料壓縮方法對時序序列進行降維是聚類演算法的必備步驟。其中較為常用的資料降維方法有Discrete Fourier Transform, Singular Value Decomposition, Adaptive Piecewise Constant Approximation, Piecewise Aggregate Approximation, Piecewise Linear Approximation and the Discrete Wavelet Transform。下采樣方法也是一類在時序序列中較為常用的技術。 降維方法的時間複雜度一般在O(nlogn)到O(n^3)不等。

  1. 聚類方法

基於空間劃分的、基於層次聚類的和基於密度聚類的方法。如 K-means,DBSCAN 等。K-Means 方法是通過對整個資料集計算聚類中心並多次迭代(時間複雜度降為O(n*K*Iterations*attributes)),而Incremental K-Means方法是每加入一個資料項時,更新類別中心,時間複雜度為O(K*n),所以其對初始化中心不敏感,且能很快收斂。 時間複雜度一般在 O(nlogn) 到 O(n^2)

之前看 Openresty 的作者章亦春在 QCon 上的分享,他談到的最有意思的一個觀點就是面向 DSL 程式設計方式。將複雜的業務語義以通用 DSL 方式表達,而非傳統的重複編碼。誠然,DSL 不是萬金油,但是 OneAPM 的告警和 Ai 資料分析,很大程度上受益於各類 DSL 工具的開發。通過抽象出類 SQL 的語法,才有了非常可靠的擴充套件性。

Akka 和 Scala 函數語言程式設計的使用,很大程度上簡化了開發的程式碼量。我在16年年初的時候,還是拒絕 Scala 的,因為當時我看到的很多程式碼,用 Java 8 的 Lambda 和函式式都能解決。直到參與了使用 Scala 開發的 Mock Agent 之後才感受到 Scala 語言的靈活好用。函式式語言在寫這種分析計算程式時,因為其語言本身的強大表達能力寫起來真的很快。這也是為什麼目前大資料框架,很多都是 Scala 編寫的緣故。

Akka 的使用,我目前還只停留在表面,但是它提供的 Actor 模型,Actor Cluster 等,在分散式平臺還是極其便捷的。

Antlr4 的學習,符號化與 SQL 生成。在編寫 DSL 的時候,最大的感受就是解析與語言生成,它們正好是兩個相反的過程。一個是將語言符號化解析成樹,另一個是基於類似的定義生成語言。這一正一反的過程,在我們適配舊的告警規則配置資料的時候,感受頗深,十分奇妙。

相關文章