原始碼閱讀之我見

語言: CN / TW / HK

【概述】


在很多技術交流群裡,都看到過同樣一個問題:如何閱讀原始碼?

很多情況下,我們對一些開源的元件會用、或者通過官方文件、實際部署測試對其原理有一定程度的理解就可以了,不一定需要進行原始碼的閱讀。因為閱讀原始碼確實是一件非常耗精力的事情。

然而閱讀原始碼,尤其是優秀的原始碼,可以從中學到很多,不僅僅是可以深入掌握元件的原理、也可以從原始碼中學習優秀的設計、以及一些好程式碼的寫法,甚至可以從中抽取部分好的程式碼進行復用,避免重複造輪子。

筆者從2012年開始,陸續研究過memcached、redis、nginx、ejabberd、rabbitmq、還有一些不大的模組,例如libevent、tidb中的sql解析模組、以及最近兩年一直在研究的hadoop、ranger、kafka等,在原始碼閱讀上也算是有一定的積累,這裡就來談談自己閱讀原始碼的一些方式方法和技巧。

本文主要提到的方法如下圖所示:

【按業務流程】


一種閱讀原始碼的方式是完全按業務流程來,比如閱讀訊息佇列服務(rocketmq、kafka、rabbitmq等)的原始碼,一個通用的流程是服務端如何接收生產者傳送的訊息並持久化儲存的,那麼這裡可以拆分為服務端是如何接收客戶端的連線的;客戶端的連線建立後,如何處理客戶端傳送訊息的請求的;訊息是如何寫入檔案的幾個簡單的步驟來閱讀對應的原始碼。

另一通用的流程是服務端如何將檔案中的訊息傳送給消費者,同樣可以拆分成如何從檔案讀取訊息、訊息讀取後如何傳送給消費者、如何處理消費者的確認訊息等幾個子流程來閱讀對應的原始碼。

首次閱讀時,你可能只需要關注主流程就可以了,所有異常的流程一概忽略,這樣會省去很多分支程式碼的閱讀(程式碼裡的28原則,80%的程式碼屬於異常處理,20%的程式碼為正常邏輯流程,然而80%的時間跑的是正常邏輯的程式碼,20%的時間跑的異常邏輯的程式碼),從而快速的縷清楚前後邏輯流程。

對主流程的相關原始碼有一定了解後,可以進行一些擴充套件和加入一些異常因素繼續閱讀相關的原始碼。比如,接收到生產者傳送的訊息後,同時發現有消費者線上並等待消費訊息,那麼此時的處理邏輯是和前面梳理的邏輯保持一致,還是說中間會有一個分支處理流程,直接將訊息投遞給消費者;比如,生產者傳送的訊息接收了一半,生產者異常了(連線斷開了),此時會進行怎樣的處理;同樣,如果在將生產者傳送的訊息持久化到檔案時,寫失敗了,又是如何處理的?

這樣經過幾輪反覆閱讀後,想必對正常、異常的處理邏輯已經掌握得八九不離十了。

【按模組】


另一種閱讀原始碼的方式是按照模組來,有這麼幾種場景會涉及先對一整個模組的程式碼進行讀以達到熟悉的程度

  • 公共的工具類模組

一個專案中,通常會有不少工具類、配置類等公共的模組,在串業務流程中有時候會反覆遇到的公共類,可以考慮優先對這個模組進行走讀,梳理一些公共類中常見的欄位的含義、函式的作用等。這樣在按業務流程走讀原始碼時不必每次都進入到該類中理解其作用。

  • 前面按業務流程拆分後的模組

另外一種場景,則是明確知道一個流程會拆分成幾個模組,這樣可以先專注對這幾個模組進行走讀

還是以上面訊息佇列服務的程式碼為例,肯定會涉及這麼幾個型別的模組。

最常見的莫過於RPC模組:負責進行指定埠的監聽,以及接收客戶端的連線,並處理客戶端連線傳送過來的請求。不管是怎樣的實現方式,例如C/C++中的libevent,libuv、rabbitmq中使用的ranch、java中的netty等RPC模組(框架),通常會有一些固定的套路。比如IO模型使用的是reactor還是proactor資料收發處理的方式以及執行緒模型,是一個執行緒負責處理一個或多個客戶端的請求,還是有獨立執行緒(池)負責資料的收發,獨立的執行緒(池)負責資料的處理,如果是獨立執行緒收發與資料處理,那麼又會牽扯出執行緒的通訊方式,比如中間加個佇列進行訊息的快取,自然也就涉及對這個佇列的同步操作,以及後續效能調優時,佇列長度的問題。同時對應的異常情況也就隨之需要考慮:即如果佇列滿了,對於寫入的執行緒是如何處理,同時會對客戶端怎樣的影響等等。

另外,rpc中還有一些經典的問題,由於通常是建立一個長連線進行互動,那麼必然要考慮到(空閒時、傳送請求、響應時)如果連線斷開會怎樣,即各種網路不可達的異常場景。然後就是連線之間是否有心跳保活,對應的機制是怎樣的,因為一旦沒有心跳保活,那麼對於tcp半開啟問題則會束手無策。

其他常見的模組,例如:寫持久化檔案的模組,包括相關檔案格式的定義(資料檔案、索引檔案)、檔案的讀寫、以及檔案格式到訊息體(訊息類)的轉換等;訊息的同步模組:有的服務會複用消費者的邏輯、有的則是獨立編寫一套邏輯;狀態機模組以及事件分發模組,負責不同事件觸發的有限狀態機流轉及對應處理等。

【按執行緒】


大多數的開源元件,通常是一個服務,服務內部都會有不同的執行緒、執行緒池來提供不同的服務。計算機執行時也是以執行緒為執行單元執行處理的。因此可以以各個執行緒執行的流程來進行原始碼的走讀,然後配合泳道圖、時序圖,這樣可以梳理出每個執行緒的處理邏輯,以及可能的執行緒之間的互動邏輯。

很多情況下,多個執行緒會呼叫到同一個類中的方法,因此可以將泳道圖與模組相結合,形成縱橫交錯的方式。縱列表示一個執行緒的處理(迴圈)流程,橫列表示各個模組類在不同執行緒中被呼叫的情況。這樣可以從程式碼的設計、計算機執行方式兩種角度來加深對原始碼的理解。

【從樹木到森林】


前面都是按一個一個功能流程、或者小的模組來進行原始碼走讀的,隨著多個功能流程,越來越多的小模組逐步熟悉後,需要開始建立一些全域性的視野,或者進行一些邏輯層次的劃分與抽象,以此來梳理整個元件的架構設計,不同模組之間的劃分,呼叫關係,以及與周邊配套元件之間的互動等。這不僅僅有助於提升自己的系統架構設計能力,也能從更高的角度來理解原始碼,可能之前有疑惑的一些程式碼,此時會豁然開朗。

【測試用例】


有時候,一個模組,或者一個功能覺得無從下手時,可以考慮先去看看自帶的單元測試用例,梳理這些測試用例,也就掌握了某個模組的使用方式、資料輸入輸出流,這樣可以根據結果倒推出實現的邏輯,以及原始碼中的具體實現。

【日誌分析】


以解決問題的形式出發,或者是對某個功能驗證其邏輯,對照日誌(或手動在關鍵位置增加列印)找到對應的原始碼位置,能快速掌握原始碼之間的呼叫流程。

【單步除錯】


就個人而言,單步除錯是我閱讀原始碼的最後採用的方式,這通常是出現問題時,糾結具體細節無法確認的情況下才採用的方式。平常的話, 不太推薦使用該方式,一方面是多執行緒的服務,單步除錯無法兼顧每個執行緒的處理流程,另一方面是多層級的函式跳轉,很容易迷失方向,從而不知道自己應該專注哪一塊了。

【輔助方法】


閱讀原始碼過程中,最常用也是最好用的輔助方式那就是畫圖了,所謂一圖勝千言,能用一張或多張圖將流程梳理清楚,比大段文字加程式碼更直觀,更清晰。

常畫的圖包括

  • 流程圖:可以直觀看出業務的邏輯流程

  • 時序圖:通常是多個元件之間rpc互動邏輯,或者多個執行緒之間的互動邏輯,這對於分析一些異常場景

  • 類關係圖:包括類的繼承關係、介面實現邏輯、類與類之間的關聯等等。

除了上面提到的幾個型別的圖,還包括用於描述協議二進位制、檔案結構的圖、描述業務場景的腦圖等等

另外,除了畫圖之外,通過tcpdump(wireshark)抓包、檢視檔案的二進位制內容等方式,也能幫助準確的瞭解原始碼的相關內容。

【總結】


以上,就是個人閱讀原始碼時所使用的方式方法,通常是幾種方法組合使用,然後配合畫圖,做到快速準確理解原始碼的邏輯。當然,我的方法不一定都對,也不一定適用於每個人,或許你有自己的一套方法和理論,但不管採用怎樣的方式方法,能從原始碼中探索真相,學習優秀的程式碼,架構設計才是最重要的事。


好了,這就是本文的全部內容,如果覺得本文對您有幫助,不要吝嗇點贊在看轉發,也歡迎加我微信交流~

本文分享自微信公眾號 - hncscwc(gh_383bc7486c1a)。
如有侵權,請聯絡 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。