掐指一算,從 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 的時候,最大的感受就是解析與語言生成,它們正好是兩個相反的過程。一個是將語言符號化解析成樹,另一個是基于類似的定義生成語言。這一正一反的過程,在我們適配舊的告警規則配置數據的時候,感受頗深,十分奇妙。