從DSL扯開去
智能運維平台的內核驅動力來自數據(日誌和指標)分析。從廣義範疇來說,所有可以用作數據處理的軟體系統,都可以用來構建這個平台。從遠古時代的awstats到piwik,到人手一個的hadoop集群(確實沒有更抽象具體的運維向子產品),到目前最流行的ELK,包括新近的基於PostgreSQL搞的TimeseriesDB,基於Solr搞的Rocana等等。
在對比所有這些產品的技術選擇和介面設計的時候,總讓我想起一句話:「一個幽靈,查詢語言的幽靈,在社區徘徊」。
SQL 與 DSL
其實在剛流行hadoop的時候,並沒有這麼多事兒。熟悉java的開開心心寫mapreduce,不熟悉java的人也樂呵呵的走streaming API,用自己熟悉的旁的編程語言寫mapreduce。
但隨後各種SQL-like的項目就雨後春筍般的湧現了。SQL的全稱:structured query language。雖然在資料庫面前,SQL更像是一種API,但是在談論DSL的時候,SQL無疑就是最成功的DSL之一。
對於我這個半吊子程序員來說,上圖這些樣例只了解regex和SQL兩樣。不過最給我印象深刻的DSL設計,是Ruby社區的sinatra項目。
# myapp.rbrequire "sinatra"get "/" do "Hello world!"end
這個漂亮的語法簡直讓我驚為天人。從此對DSL大法深信不疑。
SQL 是數據處理 DSL 的唯一選擇么?
SQL雖然是最成功的DSL之一,但它當然不是數據處理領域唯一的DSL——因為數據處理這個「領域」還是太大了。
比如,細分到CEP(複雜事件處理)領域,更通行的就是CQL。像Esper、Siddhi等,大致寫法是這樣(注意看分號的位置):
define stream TempStream (deviceID long, roomNo int, temp double);from TempStream select roomNo, temp * 9/5 + 32 as temp, "F" as scale, roomNo >= 100 and roomNo < 110 as isServerRoominsert into RoomTempStream;
包括oracle,華為等,也都有CQL設計(是的,我就是在寫這行文字前剛搜索得知的)。
又比如,細分到BI(商業智能)領域,行業老大tableau,有一套自己的VizQL?。這也是證明DSL設計很有趣的一點。infoQ上有一篇文章叫《領域專用語言(DSL)迷思》,其中第三條誤解就是「DSL必須以文本代碼的形式出現」。tableau的VizQL就是一個典型的範例——這完全是一種視覺互動式的查詢語言,和文本代碼半點不相干。
那麼 DSL 怎麼搞?
我在 http://t.cn/Ra53rH9 上看到有這麼一個回答:
分解任務、解決任務、歸併相似任務、把解決方案原型化、最終產品化。真是漂亮的步驟,把這個步驟,套回到我們最原始的目的:智能運維平台,就可以發現,所謂DSL設計,主要考驗的是設計者對運維工作的理解力。
BTW:這個問題里的另一回答把crontab作為一個DSL範例舉出來了,這麼說我要收回前文有關sinatra的驚嘆……
到底智能運維平台需要什麼樣的DSL?
從problem看,我們有這麼幾大類:
- 按照某些邏輯查找或排除日誌中的有效部分;
- 分析某些系統的狀態並判定其異常;
- 按照某些邏輯確定異常是否發送以及以何種形式發送給哪些處理方(人或系統)。
第一類顯然最簡單了,仿照grep -E或者grep -P的搞法可以是一種,仿照搜索引擎的搞法也是一種。(是的,並不是所有的日誌產品都用lucene querystring syntax)
第三類也是比較明確的,nagios的object group設計就很棒,而近來流行的IFTTT風格也不錯。我見過攜程的朋友提供這種風格的DSL給開發做主動監控,而prometheus的alertmanager里也是一樣的玩法。
唯獨第二類話題極其大。系統狀態,包括了性能指標、行為基線等不同方面,可以動用各種簡單的複雜的數學統計乃至機器學習知識。所以還要繼續拆解。
簡單的均值趨勢、佔比統計,這也是大多數監控系統儀錶盤最愛用的功能了。這些統計函數,基本上在SQL里也都有。由此很自然會引發一個想法:是不是可以用SQL來解決第二類需求?
為什麼SQL不適合?
我們再念一遍SQL的全稱:structured query language,structured * 3。
這和智能運維平台所承載的logdata是衝突的。和metricdata也在漸漸衝突……(越來越多的metric系統也在JSON化)
logdata是帶有時間戳屬性的非結構化數據。雖然平台為了許可權管理和分析方便,除了timestamp,一般還會內置有hostname、tag、logtype等少量信息,但是總體上來說,日誌信息依然是非結構化的。
即使在目前常見的 ELK 系統中,logstash 的預解析欄位有點類似 create table 的意思,也不能改變這個欄位解析結果只存在於單條日誌中的事實。對於日誌整體來說,這個 schema 依然是不固定的。
把眼光從ELK系統再往上一層,需要搭建的是一個智能運維平台,平台用戶是橫跨部門的。這時候還會有更嚴重的一個問題:同一份日誌,業務部門、運維部門、安全部門可能需要關注的信息完全不一樣。即便是單條日誌內的預解析為結構化數據都不可行。
由此,就得到了第一個problem:不同人對同一條日誌可能採取不同的欄位解析。
其次,日誌信息受限於碼農水平或者心情,很可能是極其雜亂無章的。多線程交叉多行列印一個事件是經常會發生的事情。怎麼抽絲剝繭,從複雜文本中獲取業務處理請求的關係鏈,以及各級關係的權重,這是第二個problem。
再次,異常狀態如何表達,表格並不是唯一的選擇,甚至多數時候表格完全表達不出來重點和非重點數據的區別。針對不同場景理所當然應該有不同的表達方式。雖然這涉及更多是可視化效果的選擇,(即便我們拋開VizQL這種特例不談)我們也需要自己的 DSL 給出前端可用的特定屬性信息作為一種指向。比如,我們希望根據橫向對比的情況來查找某種異常的可能性,就會同時用到 GROUPBY 和 HISTOGRAM 兩個方式的組合,而根據 group 的層級和含義,可能就會選擇簡單的多折線,聯動的 timeline,或者表格里的 sparkline 迷你圖。這是第三個problem:需要有針對場景的表達力。
當然,比起餅圖,還是表格更好。
那什麼合適呢?
這個事情可能真的就是看個(P)人(M)偏好了。比如我作為一個運維+perl/ruby愛好者,就覺得不管是UNIX pipeline式,還是method chaining式,都很棒。這兩種設計,把複雜方案隱藏起來,只留給最終用戶一個command/method給用戶按需選用即可。(讓JSON地獄去死)
不過從保持一致性的角度出發,對於日誌系統,可能還是選用shell pipeline式更合適一點。jordansissel 在介紹 Logstash 的內部原理時,就使用了 pipeline 的概念(事實上連代碼里也叫 pipeline):
inputs | filters | outputs
所以對數據的後半段,繼續沿用pipeline概念就是很順理成章的事情了。
這是其一。
其二,在處理尤其常用的檢索需求時,pipeline比method更靈活一些。還是一致性的考慮,最初的inputs,對於pipeline可以直接無縫對接,但是對於method,是不是我們還需要搞個Object.new?
讓我們來看看兩個示例吧,其實我覺得都還好??:
index=summary starttime=now-7d/d endtime=now/d domain=(aaa OR bbb) | bucket timestamp span=15m as ts | stats avg(apache.reqtime) as avg_ by ts | esma avg_ timefield=ts futurecount=24 | where typeof(_predict_avg_) == "double" | eval time = formatdate(ts, "HH:mm") | table time, _predict_avg_ | join type=left time [[ starttime="now/d" * | bucket timestamp span=15m as ts | stats avg(apache.reqtime) as avg_ by ts | eval time = formatdate(ts, "HH:mm") | table time, avg_ ]]
然後寫成:
Search(index="summary", starttime="now-7d/d", endtime="now/d", domain=["aaa", "bbb"]) .bucket(timestamp, span=15m) .avg(apache.reqtime) .esma(timefield=ts, futurecount=24) .select { |ts| ts._predict_avg_.is_a?(Double) } .formatdate("HH:mm") .table("time", "_predict_avg_") .join(type=left, id=time, Search(starttime="now/d", "*") .bucket(timestamp, span=15m) .avg(apache.reqtime) .formatdate("HH:mm") .table("time", "avg_") )
對比一下,可能最明顯的感覺就是:.table()函數里的那些欄位名是怎麼突然出現的?因為一個method對object的作用不是顯式的,你不看文檔是沒法知道調用一個method以後會生成什麼object,擁有哪些attributes的。而前者的as參數就非常的簡明扼要。
你扯了這麼多,別人的想法呢?
是的,其實做一個PM很多時候相互關心一下同行的思路太應該了……國內同行不太開放,所以只能收集到國外同行的數據。下圖為主要AIOps產品的DSL所提供的的指令/函數數量的雷達圖:
(基於2017.05數據,畢竟AIOps的公司大多在高速發展中)此外:
HPE也有類似形式的AQL,不過他們太瘋狂,直接跟自己另一款分散式R語言產品捆綁銷售,AQL里可以調用R函數,尼瑪那一下子太多了……
logscape是半pipeline半method方式,很奇葩的寫法,如下。我個人覺得連一致性都無法保證的設計是失敗的。
type="agent-stats" | hosts(cache,db) cpu.avg(_host) chart(line) buckets(1)
ELK中timelion是method方式,如下:
.es("metric:0", metric="avg:value") .label("#0 90th surprise"),.es("metric:0", metric="avg:value") .showifgreater( .es("metric:0", metric="avg:value") .movingaverage(6) .sum( .es("metric:0", metric="avg:value") .movingstd(6) .multiply(3) ) ).bars() .yaxis(2) .label("#0 anomalies")
這裡幾乎把所有的query和aggregation都合併到.es()的參數里,導致method本身功能局限在圖形設置和最終的pipeline aggregation功能上,感覺還是有待改進~
最後
本文其實是在回答了 日誌易產品的SPL真正價值點是什麼呢,使用的場景有哪些? - 知乎 以後,整理的補充內容。也歡迎大家看原題回答~
最後的補充
能扯的其實已經扯完了,不過突然發現之前我一直保留的1.4.2版本的a life of logstash event鏈接已經失效,目前最新的 ELK 文檔里對logstash pipeline的描述改成了這樣:
inputs -> filters -> outputs
. 和 -> 是最常見的兩種調用方法的意符。感覺 ELK 全線走向method chaining風格的節奏啊~
推薦閱讀: