問題排查神器 - Git Bisect 命令實戰分享

摘要:前段時間 Git 發佈正式版本 2.33.0 遇到一個客戶端與服務端不兼容的一個問題,在排查問題的過程中又一次用到了 Git bisect 命令,解決問題的同時結合近期的一些認知,又刷新了自己對 bisect 命令的認識,bisect 可以結合一些場景發揮其妙用,藉此分享下 bisect 命令以及 bisect 命令的一些其它使用方式的思考。

背景

Git 於 8 月 17 號發佈了 2.33.0 正式版本,本次的發佈內容可以參見:

https://gitee.com/mirrors/git/blob/master/Documentation/RelNotes/2.33.0.txt

不幸的是,在 2.33.0 版本發佈沒多久,就有用戶反饋 2.33.0 版本的 Git 在使用 SSH 通信協議的時候無法進行正常的 Clone/Fetch/Push 操作,具體的現象如下:

Cloning into 'xxxxxx'...
fetch-pack: unexpected disconnect while reading sideband packet
fatal: early EOF
fatal: fetch-pack: invalid index-pack output

看到這個提示第一時間就想到 Git 2.33.0 的更新日誌裏面好像是有關於 Fetch 和 Sideband 相關的更新,於是前往更新日誌發現瞭如下的兩個信息:

 * "git fetch" over protocol v2 left its side of the socket open after
   it finished speaking, which unnecessarily wasted the resource on
   the other side.
   (merge ae1a7eefff jk/fetch-pack-v2-half-close-early later to maint).
 * The side-band demultiplexer that is used to display progress output
   from the remote end did not clear the line properly when the end of
   line hits at a packet boundary, which has been corrected.

但是,爲了進一步定位此問題的源頭,方便精準的定位問題,所以使用了 Git Bisect 命令進行測試,找到具體的原因,才能夠更好的去排查分析問題並加以解決。

Git Bisect 介紹

git bisect 官方文檔:https://git-scm.com/docs/git-bisect

git bisect 命令的作用是使用二分查找法找到具體引起問題的 Commit。

上圖引自阮一峯的網絡日誌:http://www.ruanyifeng.com/blog/2018/12/git-bisect.html

簡單來說就是我們給到 bisect 命令一個範圍,它會自動的幫我們確認當前範圍的中點,在這個中點上進行測試,並且告訴它這是一個好的提交(good commit)還是一個壞的提交(bad commit),進而來縮小查找範圍,通過二分查找,我們就可以很快的定位到出問題的 Commit 以方便我們針對性的解決問題。

問題排查

由於我們已經確定了這個問題是 Git 2.33.0 版本引起的(因爲測試了 Git 2.32.0 沒問題呀 :D),所以在使用 bisect 命令的時候,這個版本範圍就是v2.32.0 ~ v2.33.0,我們只要找到具體引入這個問題的提交,就能夠更精準的定位問題,從而避免盲目的猜測和常識。

進入 bisect 模式

我們下載 Git 最新源碼,並且使用 git bisect start 命令來進入 bisect 模式

git bisect start 命令指定了一個範圍,並且 bisect 命令告訴我們,在這兩個版本之間一共有 304 個提交,大概需要 8 步就可以定位到具體的 commit,這就是二分查找的好處。

開始第一次測試

我們使用 make 命令進行 Git 源碼編譯構建,編譯構建完成後,就可以使用 Git 提供的一個 wrapper 進行 Git 命令的調用,這裏我們可以加上 -j4 參數來增加編譯構建的速度。命令完成後,我們就可以使用 ./bin-wrappers/git 來進行測試:

哦吼,還是不行,那麼我們可以確定導致問題出現的 Commit 是出現在這次提交之後的提交裏面。當然,這些不用我們自己來去記,使用 git bisect bad 命令標記即可:

標記完成後 Git 就告訴我們,接下來還有大概 7 次的測試就可以定位到引發問題的 Commit,如果遇到的提交是可以通過的,那麼需要使用 git bisect good 命令標記。

自動化的 bisect

以上的步驟我們只需要重複的進行即可,但是很明顯根本不需要人爲的去跟進,作爲一個新生代農民工,我們需要能夠自動化可自動化的一切,所以一方面是爲了省時間,另一方面是爲了避免人爲的失誤,我們可以使用 git bisect run 命令來自動化的進行執行。

git bisect run my_script arguments

bisect 命令是以 my_script 腳本的返回值來確定當前的提交是好是壞,來看看官方的介紹:

Note that the script (my_script in the above example) should exit with code 0 if the current source code is good/old, and exit with a code between 1 and 127 (inclusive), except 125, if the current source code is bad/new.

簡單來說就是:

所以我們來根據這個規則寫個腳本,但首先我們需要看看 Clone 失敗的返回值是多少

OK, let's do it via Shell Scripts ~~~

#!/bin/zsh
# 每次將 Clone 的代碼放到以當前 CommitID 爲名稱的目錄,避免衝突
make -j4 && ./bin-wrappers/git clone git@gitee.com:/kesin/taskover.git `git log -n 1 --pretty=format:"%H"`
s=$?
if [ $s -eq 0 ]; then # 正常
  exit 0;
elif [ $s -eq 128 ]; then # 失敗
  exit 1;
else # 其他情況,此處只是測試,建議針對不同的情況更加嚴謹點 XD
  exit 128;
fi

然後我們來執行這個腳本自動的進行二分查找定位:

這個時候 bisect 命令就會自動的執行編譯構建和 Clone 過程,並且根據返回值自動的確定 Commit 的範圍:

這裏是 bisect run 命令根據我們寫的腳本自動的判定當前提交是一個壞提交,進而自動的進行下一步。當然,在二分查找的過程中也會遇到好的提交,bisect 命令將會根據我們提供的返回值自動的縮小範圍: 

最終,bisect 將會爲我們定位到第一個出現此問題的提交:

ae1a7eefffe60425e6bf6a2065e042ae051cfb6c is the first bad commit

並且 bisect 命令也把這個導致此問題的提交的詳細信息打印了出來,接下來我們就可以根據這個提交相關的改動來分析我們的問題。

分析並解決問題

上面我們使用 bisect 命令找到出問題的提交:https://gitee.com/mirrors/git/commit/ae1a7eefffe60425e6bf6a2065e042ae051cfb6c

/*
 * this is the final request we'll make of the server;
 * do a half-duplex shutdown to indicate that they can
 * hang up as soon as the pack is sent.
 */
close(fd[1]);
fd[1] = -1;

分析下提交的變更我們可以知道,這次變更主要是爲了優化網絡連接的佔用,Git V2 via SSH 在傳輸的過程中,當客戶端接收完數據後,還需要進行一系列的本地操作,這個操作過程已經不需要再維持跟服務端的鏈接了,所以需要在客戶端發送完數據後就給服務端發送 FIN,進入半雙工狀態,等待服務端發送完數據後關閉連接即可,而無需經過漫長的本地操作後才關閉連接,從而避免了不必要的網絡資源佔用。

知道問題的原因之後,我們分析 Gitee 的 SSH 分發代理,發現我們的 SSH 代理在接收到客戶端的 FIN 之後馬上就會關閉這個 SSH 鏈接,進而導致上面的問題:客戶端還沒有接收完數據鏈接就提前斷開了

解決的方式也很簡單,在收到客戶端的 FIN 之後不馬上進行網絡連接的關閉,而是等數據發送完之後才進行關閉。

Git Bisect 使用的思考

現在的組織都在推崇提升研發效能,推崇 DevOps 文化及工具,是不是可以把 Bisect 這種邏輯用到整個流程中呢?

比如在自動化測試中,我們遇到一些測試不通過的 Case 的時候是直接失敗的,雖然告知了具體的用例以及相關的輸入輸出,但是如果能夠通過 Bisect 命令自動的找出第一個出現此問題的提交,那麼對於組織無疑是有價值的:

關於算法思想的思考

Git Bisect 所採用的二分查找思想我們耳熟能詳,在 Git 源碼中,同樣採用二分查找算法的地方還有 Git Pack idx 文件的查找,通過精妙的扇區劃分,加上二分查找算法快速定位到object的偏移量。除此之外,Git 中還採用了 SHA-1 Hash 算法、不同的 Diff 算法、大量的遞歸等。

/* hash-lookup.c */
int bsearch_hash(const unsigned char *hash, const uint32_t *fanout_nbo,
         const unsigned char *table, size_t stride, uint32_t *result)
{
    uint32_t hi, lo;
    hi = ntohl(fanout_nbo[*hash]);
    lo = ((*hash == 0x0) ? 0 : ntohl(fanout_nbo[*hash - 1]));
    while (lo < hi) {
        unsigned mi = lo + (hi - lo) / 2;
        int cmp = hashcmp(table + mi * stride, hash);
        if (!cmp) {
            if (result)
                *result = mi;
            return 1;
        }
        if (cmp > 0)
            hi = mi;
        else
            lo = mi + 1;
    }
    if (result)
        *result = lo;
    return 0;
}

但是在實際編碼的過程中,有多少開發者能夠拍着胸脯說:在編碼過程中,我腦中有模型,心中有算法,能夠以高效的方式、採用合理的邏輯去編寫代碼。

這個答案我想不言自明吧。

研究開源項目是一個很好的學習路徑,能夠從大量優秀的代碼中學習到優秀的實踐和思想,從研究學習到參與貢獻,相比於這個過程,結果倒顯得不那麼重要了。

最後

善用工具,樂於思考,多多來 Gitee(https://gitee.com )學習研究開源項目並貢獻代碼。

轉載請保留出處:微信公衆號「Zoker 隨筆」(zokersay)

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/IHeT_d0LhQ1wma_-2FJoXA