本文適合有 Java 基礎知識的人群
作者:HelloGitHub-Salieri
HelloGitHub 推出的《講解開源項目》系列。
項目地址:
PowerJob 的在線日志一直是飽受好評的一個功能,它能在前端界面實時展示開發者在任務處理過程中輸出的日志,幫助開發者更好的監控任務的執行情況。其功能展示如下圖所示(前端界面略丑,請自動忽略~)。
在線日志這個功能,乍一聽很簡單,無非 worker 向 server 發日志數據,server 接受后前端展示。但對於 PowerJob 這種任意節點都支持分布式部署且支持分布式計算的系統來說,還是存在着不少難點的,簡單來說,有以下幾點:
- 多對多問題:在 PowerJob 的理想部署模式中,會存在多個 server 和多個 worker,當某個任務開始分布式計算時,其日志散布於各台機器上,要想在前端統一展示,需要有收集器將分散的日志匯集到一起。
- 並發問題:當 worker 集群規模較大時,一旦執行分布式計算任務,其產生的日志 QPS 也是一個不小的數目,要想輕松支持百萬量級的分布式任務,需要解決並發情況下 QPS 過高的問題。
- 排序問題:分布式計算時,日志散布在不同機器,即便收集匯總到同一台機器,由於網絡延遲等原因,不能保證日志的有序性,而日志按時間排序是強需求(否則根本沒法看啊...),因此,還需要解決大規模日志數據的排序問題。
- 數據的存儲問題:當日志數據量非常大時,如何高效的存儲和讀取這一批數據,也是需要解決的問題。
因此,為了完美實現在線日志功能,PowerJob 在內部實現了一個麻雀雖小五臟俱全的分布式日志系統。話不多說,下面正式開始逐一分析~
一、多對多問題
這個問題,其實在 PowerJob 解決多 worker 多 server 的選主問題時順帶着解決了。簡單來說,PowerJob 系統中,某一個分組下的所有 worker,在運行時都只會連接到某一台 server。因此,日志數據上報時,選擇當前 worker 進行上報即可。由於任務不可能跨分組執行,因此某個任務在運行過程中產生的所有日志數據都會上報給該分組當前連接的 server,這樣就做到了日志的收集,即日志會匯總到負責當前分組調度的 powerjob-server,由該 server 統一處理。
二、並發問題
並發問題的解決也不難。
大家一定都聽說過消息中間件,也知道消息中間件的一大功能為削峰。引入消息中間件后,把原來同步式的調用轉化為異步的間接推送,中間通過一個隊列在一端承接瞬時的流量洪峰,在另一端平滑地將消息推送出去。消息中間件就像水庫一樣,攔蓄上游的洪水,削減進入下游河道的洪峰流量,從而達到減免洪水災害的目的。
PowerJob 在處理日志的高並發問題時也采用了類似的方式,通過引入本地隊列,對需要發送給 server 的消息進行緩存,再定時將消息批量發送給 server,化同步為異步,並引入批量發送的機制,充分利用每一次數據傳輸的機會發送盡可能多的數據,從而降低對 server 的沖擊。
三、排序問題
3.1 日志的存儲
將排序問題之前,先來聊一聊 server 怎么處理接收到的日志數據,也就是如何存儲日志。
這個抉擇其實並不難,用一下簡單的排除法就能獲取正確答案:
- 存內部還是存外部?PowerJob 作為任務調度中間件,最小依賴一直是需要牢牢把控的指導思想。因此,在已知最小依賴僅為數據庫的情況下,似乎不太可能使用外部的存儲介質,至少不能把收到的日志直接發送到外部存儲介質,否則又是一波龐大的 QPS,會對依賴的外部組件有非常高的性能要求,不符合框架設計原則。因此,在線日志的第一級存儲介質應該由 server 本身來承擔。
- 存內存還是磁盤?既然確定了由 server 來存儲原始數據,那么就面臨內存和磁盤二選一的問題了。但,這還用選嗎?成百上千萬的文本數據存內存,這不妥妥的 OutOfMemory 嗎?顯然,存磁盤。
經過一波簡單的排除法,日志的一級存儲方案確定了:server 的本機磁盤。那么,存磁盤會帶來什么問題呢?
且不說文件操作的復雜性和難度,一個最簡單的需求就能讓這個方案跌入萬丈深淵,那就是:排序。
眾所周知,日志必須按時間排序,否則根本沒法看。而 PowerJob 又是一個純粹的分布式系統,顯然不可能指望所有的日志數據按順序發到 server,因此對日志的再排序是一件必須要做的事情。但讓我們來考慮一下難度。
- 首先,日志是純文本數據,要想做排序,首先要將整個日志文件變為一堆日志記錄,即分行。
- 其次,分完行后,由於日志是給人看的,時間肯定已經被轉化為 yyyy-MM-dd HH:mm:ss.SSS 這種方便人閱讀的格式,那么將它反解析回可排序的時間戳又是一件麻煩事。
- 最后,也是最終 BOSS,就是排序了。要知道,之所以會選擇磁盤存儲這個方案,是因為沒有足夠的內存。這也就意味着,這個排序沒辦法在內存完成。外部排序的難度和效率,想必不用我多說了吧。同時,我也相信,大部分程序員(包括我在內)應該從來沒有接觸過外部排序,這趟渾水,我又何必去趟呢?
3.2 H2 數據庫簡介
那么,有沒有什么既能使用磁盤做存儲,又有排序能力的框架/軟件呢?世上會有這等好事嗎?你別說,還真有。而且是遠在天邊,近在眼前,可以說是和程序員形影不離的一樣東西——數據庫。
“等等,你剛才不是說,不拿數據庫作為一級存儲介質嗎?怎么滴,出爾反爾?”
“哼,年輕人。此數據庫非彼數據庫,這個數據庫啊,是 powerjob-server 內置的嵌入式數據庫 H2”
H2 是一個用 Java 開發的嵌入式數據庫,它本身只是一個類庫,即只有一個 jar 文件,可以直接嵌入到應用項目中。嵌入式模式下,應用在 JVM 中啟動 H2 數據庫並通過 JDBC 連接。該模式同時支持數據持久化和內存兩種方式。
H2 的使用很簡單,在項目中引入依賴后,便會自動隨 JVM 啟動,應用可以通過 JDBC URL 進行連接,並在 JDBC URL 中指定所使用的模式,比如對於 powerjob-server 來說,需要使用嵌入式磁盤持久化模式,因此使用以下 JDBC URL 進行連接:
jdbc:h2:file:~/powerjob-server/powerjob_server_db
同時,H2 支持相當標准的 SQL 規范,也和 Spring Data Jpa、MyBatis 等 ORM 框架完美兼容,因此使用非常方便。在 powerjob-server 中,我便通過 Spring Data Jpa 來使用 H2,用戶體驗非常友好(當然,多數據源的配置很不友好!)。
綜上,有了內置的 H2 數據庫,日志的存儲和排序也就不再是難以解決的問題了~
3.3 存儲與排序
引入 H2 之后,powerjob-server 處理在線日志的流程如下:
- 接收來自 worker 的日志數據,直接寫入內嵌數據庫 H2 中
- 在線調用時,通過 SQL 查詢語句的 order by log_time 功能,完成日志的排序和輸出
可見,合適的技術選型能讓問題的解決簡單很多~
四、一些其他的優化
以上介紹了 PowerJob 分布式日志組件的核心原理和實現,當然,在實際使用中,還引入了許多優化,限於篇幅,這里簡單提一下,有興趣的同學可以自己去看源碼~
- 高頻率在線訪問降壓:如果每次用戶查看日志,都需要從數據庫中查詢並輸出,這個效率和速度都會非常慢。畢竟當數據量達到一定程度時,光是磁盤 I/O 就得花去不少時間。因此,powerjob-server 會為每次查詢生成緩存文件,一定時間范圍內的日志查詢,會通過文件緩存直接返回,而不是每次都走 DB 查詢方案。
- 日志分頁:成百上千萬條數據的背后,生成的文件大小也以及遠遠高於正常網絡帶寬所能輕松承載的范圍了。因此,為了在前端控制台快速顯示在線日志,需要引入分頁功能,一次顯示部分日志數據。這也是一項較為復雜的文件操作。
- 遠程存儲:所有日志都存在 server 本地顯然不符合高可用的設計目標,畢竟換一台 server 就意味着所有的日志數據都丟了,因此 PowerJob 引入了 mongoDB 作為日志的持久化存儲介質。mongodb 支持用戶直接使用其底層的分布式文件系統 GridFS,經過我仔細的考量,認為這是一個可接受且較為強大的擴展依賴,因此選擇引入。
五、最后
好了,本期的內容就到這里結束了,下一期,我將會大家講述 PowerJob 作為一個各個節點時刻需要進行通訊的框架,底層序列化框架該如何選擇,具體的序列化方案又該如何設計~
那么我們下期再見嘍~
關注 HelloGitHub 公眾號