聊聊 Sdk 和問題排查
不分享什么知識,聊一下最近的一些思考和看到的一些內容。這兩個內容看起來沒什么關系,其實也有關系。
sdk 大家都不陌生,比如我們經常用到的 npm 包。當我們以 sdk 的方式提供一種能力的時候,我們的實現不僅決定了業務的使用方式和成本,還決定用戶是否樂意使用它。所以我們不能只考慮到功能,還需要考慮到使用方式以及 sdk 本身對業務的影響,不管是穩定性還是性能。當我們的 sdk 對業務來說是剛需時,如果 sdk 有問題,業務可能會聯系我們處理,因為它需要這個 sdk。但是如果對業務來說這個 sdk 不是剛需時,業務可能直接 uninstall 我們的 sdk 并刪除對應的代碼。這對于提供 sdk 的我們來說顯然不是個好事情。但是不管是否剛需,作為提供方,我們都需要努力去做好所提供的服務。
1 內嵌形式
## 1.1 內嵌于業務代碼的形式
我們使用的 sdk 大多數都是引入業務代碼中,然后使用它提供的功能,這種情況下,有兩種模式,第一種是業務要感知 sdk 提供的 API。我們需要知道什么時候使用什么 API。第二種是業務不需要感知 sdk 提供的 API,或者說這時候 sdk 不提供 API,它本身就像一個黑盒子,業務引入后就內置了某些功能,比如我們提供一個定時上報業務內存使用情況的 sdk,那么業務就不需要關注 sdk 的具體實現。下面以統計請求耗時為例看看如何實現這個 sdk。
1.第一種
- {
- start(...) {}
- end(...) {}
- }
第一種方式是比較樸素的實現,sdk 提供了一個 start 和 end 的 API,業務在開始請求和結束請求時分別執行這兩個 API,這樣 sdk 就可以計算出這個請求的耗時。但是這種方式看起來并不是那么友好,首先會侵入業務的代碼邏輯,其次業務還需要感知這個 sdk,需要考慮什么時候調 start,什么時候調 end,而且 sdk 還依賴業務傳入請求和響應的上下文,才能計算出某一個請求的耗時,總的來說,這種方式比較麻煩。
2 第二種
我們希望對業務的侵入性和感知少一點,所以決定直接劫持 Node.js 里的 API。Node.js 里以下面的形式可以創建一個服務器。
- http.createServer((req, res) => {})
那么我們直接劫持這個 createServer。
- const createServer = http.createServer;
- http.createServer = (cb) => {
- return createServer((req, res) => {
- const start = Date.now();
- res.on('finish', () => {
- const cost = end - start;
- });
- cb();
- });
- }
通常,sdk 是提供 API,由業務主動調用,或者說觸發 sdk 的代碼,因為 sdk 無法捕獲業務代碼什么時候需要使用 sdk 的某個功能。但是當我們可以捕獲到業務什么時候需要我們時,就可以以更好的方式去提供這個 sdk。這種方式可以使得業務不需要過多感知 sdk,比如上面的例子中,業務只需要保證在調用http.createServer 之前執行我們的 sdk 就可以。sdk 內嵌業務代碼中是非常常見的形式,但是我們希望盡量減少對業務的侵入,或者說減少業務的心智負擔,大家可能都有過這種經歷,當看到一個 sdk 提供密密麻麻的參數時,第一反應就不想用了。
2 脫離業務代碼的形式
那么是否能以一種脫離于業務代碼的方式提供一個 sdk,這樣不僅不會影響業務代碼,對于升級 sdk 來說也更容易。但是這種方式往往不容易,主要取決于場景,比如業務需要通過一個 sdk 上傳文件,那么這個 sdk 以內嵌的方式會比較合適。但是,某些場景下,脫離業務代碼的 sdk 是可以做到的,比如排查問題類的工具。在 Node.js 里,我們調試或診斷進程的方式通常是在業務代碼里內嵌相關的代碼,然后在必要的時候執行對應的代碼,比如獲取堆快照。因為我們的代碼只有置身于進程中,才能獲取到這個信息。但是不是所有的信息都需要置身于進程中才能獲取,比如系統級的數據。我之前碰到一個問題,就是在某個場景下,WebSocket 連接會很快底被斷開,通過再客戶端 wireshark 捕獲的流量中,發現服務器會發送一個 fin 包給客戶端,這樣就知道是服務器的問題了,但是又因為從客戶端到真正的服務器中間還隔了很多層,無法知道是哪一層服務器主動斷開了連接,最后通過服務器提供的工具找到了主動發送 fin 包的服務器從而解決了問題。但是我發現服務器的那些工具用起來都非常復雜,如果不經常用,很快就忘了各種命令和參數,像這種場景,就可以封裝 sdk 給業務使用,這種形式不僅可以幫助業務排查問題,還不需要侵入業務代碼。
3 問題排查
我們排查問題通常借助日志,但是日志很多時候也解決不了問題,日志是靜態埋點,打多了不僅浪費存儲,而且消耗性能,打少了可能缺少排查問題的上下文。但是無論如何,重點是日志是靜態埋點,如果我們要加埋點,就得重啟服務,有些問題稍縱即逝,重啟后可能就很難復現了。所以除了靜態追蹤技術外,動態追蹤技術就非常必要,也非常 cool 了,之前看了一下 ebpf,但是后來沒看了,最近重新研究了一下 ebpf 和所衍生的一些排查問題的工具,也看了一下 openresty 作者的文章《動態追蹤技術漫談》,可謂是精彩。當一個進程或者系統有問題時,我們希望保留現場,然后再慢慢分析。但是我們在進程之外怎么能獲得進程的數據呢?除了系統本身提供的一些命令外,這里想說的是一種更復雜但更強大的技術。操作系統和我們寫的業務代碼一樣,都是一些代碼的邏輯,我們在寫代碼時,經常會用到鉤子或者劫持的技術。同樣,操作系統也不例子,但是操作系統為了提供這種技術,實現上復雜得多。這種技術就是 ebpf,ebpf 是把用戶寫的代碼注入到內核中,內核有一個虛擬機,滿足條件的時候就會執行我們的代碼。
操作系統提供了鉤子機制,比如我們可以注冊一個鉤子到系統,當系統收到網絡包時,就會回調我們。另外一種就是劫持,比如 kprobe 到實現,當我們寫一段代碼指示操作系統當有人調用 x 的時候回調我們,操作系統就會把這個地址對應的指令改成 int3(x86 架構),然后執行到 x 這個函數的時候,就會觸發 int3 中斷,對應的處理函數就會執行我們注冊的回調,然后再執行真正的函數。很多技術都依賴 ebpf,比如 tcpdump。ebpf 厲害之處在于內核編程可編程的了,真正情況下,我們可以通過基于 ebpf 的工具,從內核中查到非常多的信息,以幫助我們排查問題。ebpf 非常流行,也非常復雜,就不討論太多,大家可以自行查閱相關信息。