vatt'ghern jaskier's ballads
本文 1 個互動圖表在手機上以重點摘要呈現,互動版請以桌面瀏覽器開啟。

git.kernel.org 現在有 90 顆核心分布在 5 個地理節點上,其中隨時有 14 到 16 顆什麼別的事都不做,只忙著把 linux.git 裡的某一個 commit 重新渲染成一頁 HTML,再原封不動地被丟進某支爬蟲的解析器裡。

kernel.org 平均兩成算力,都在替爬蟲重繪 commit

篇是 git.kernel.org 背後負責維運系統的 Konstantin Ryabitsev,把「AI 爬蟲很煩」這句抱怨換成一整套硬數字的過程——多少核心被吃掉、多少流量真的是人在看、防禦手段一路加碼又一路被追上,到最後他自己也承認,這問題沒有簡單解法。

先看數字:90 顆核心,14 到 16 顆在陪爬蟲空轉

這篇的起點是三個數字:linux.git 目前大約是 148 萬個 commit;git.kernel.org 上還掛著大約 922 個它的 fork;而現在,git.kernel.org 每天要應付大約 600 萬次「我要看某個隨機 commit」的請求。把這幾個數字跟後面會提到的攔截戰績放在一起看,會更清楚問題出在哪裡。

指標數值備註
linux.git commit 數約 148 萬撰文當下的規模
git.kernel.org 上的 fork 數約 922光是 linux.git 一個 repo
每日請求量約 600 萬次多數在要求看某個 commit
合法流量估計占比約 2%作者自承是寬鬆假設下的數字
全站核心數90 顆分布在 5 個地理節點
被爬蟲占用的核心14 到 16 顆平均約占全站容量 20%,但實際是一波一波的尖峰
Anubis 直接攔下比例66%連主站都碰不到
Anubis 解題成功比例33%算出答案、放行進站
數字皆出自 Ryabitsev 本人的觀察快照,除了 commit 與 fork 數以外,多數會隨流量與防禦戰況隨時變動。

這幾個數字都是某一個時間點的快照,不是恆定不變的常數。148 萬個 commit、922 個 fork,是撰文當下 linux.git 在 git.kernel.org 上的規模;隨著開發持續進行,commit 數只會往上疊加,fork 數也可能隨時增減。這些 fork 理論上都是同一份提交歷史的另一份拷貝——文章沒有明講爬蟲是否對每個 fork 都重複同樣的渲染請求,但合理的推測是,只要爬蟲清單裡包含這些 fork 的網址,148 萬個 commit 就有可能在不同 fork 上被重複渲染,讓真正的請求規模比單一倉庫看起來的還要大。600 萬次攤開來看,基本上是全年無休、日夜不停在打——這不是人類作息的節奏,而是照著清單一路掃過去的程式的節奏。

這些請求不是均勻地打在整個系統上。Ryabitsev 給出的是某一個時間點的快照:在 5 個地理節點加起來的 90 顆核心裡,隨時有 14 到 16 顆什麼都不做,只負責把某個 commit 重新渲染成 HTML 頁面——平均下來,這是全站運算容量的兩成。但他在同一句話裡就先把「兩成」這個讀法擋掉了:爬蟲是一波一波湧上來的,真實的負載曲線比「兩成的水平線」尖銳得多。換句話說,兩成是一個平均值,不是一條被綁死的基線——實際的樣子更接近尖峰一波一波打上來、中間再回落,平均起來剛好落在五分之一算力這個位置。文章沒有再往下講這件事的影響,但合理的推測是,這個差別對維運來說很要緊:要撐住的不是一條穩定的兩成負載,而是那些高過兩成的波峰。

「5 個地理節點」本身也是一個線索:git.kernel.org 不是單一機房裡的一台伺服器,而是分散在全球好幾個地點的叢集。文章只交代了節點數,沒有說明為什麼要這樣分散——地理分散通常跟就近存取有關,但那是一般性的背景,不是 Ryabitsev 在這篇裡講的事。他實際講的是規模的分布:14 到 16 顆核心是 5 個節點加起來的總和,代表爬蟲造成的負擔是全域性的,不是某一個節點的個案,也就沒辦法靠在某一處多加一台機器簡單解決。文章拿 linux.git 當例子,但 git.kernel.org 上不會只有這一個倉庫——文章沒有比較站上各個倉庫的規模,也沒有說 linux.git 是最大的那一個;合理的推測是,同一套「commit 逐一渲染」的爬取模式,套用在任何一個公開的 git 倉庫上都會成立,只是 linux.git 的 148 萬個 commit 加上 922 個 fork,讓這件事的量級最容易被看見。

播放查看核心即時占用狀態 · 90 顆核心中的 14 到 16 顆

t = 0.0s · 忙碌核心 15 / 90(17%)· 合法請求閃過 0 次
90 顆核心分布在 5 個地理節點(每列 18 顆)。橘色格是當下正在替爬蟲渲染 commit 的核心,數量在 14 到 16 之間浮動;偶爾閃過的綠色格代表約 2% 的合法流量。作者強調實際負載是一波一波的尖峰,不是穩定的兩成水平線。

90 顆核心分布在 5 個地理節點(每列 18 顆)

90 顆核心中,隨時有 14 到 16 顆忙著替爬蟲渲染 commit,平均約占全站兩成算力,但實際負載是一波一波的尖峰;合法流量僅占 2%。

Ryabitsev 也強調現在還沒到打不開的地步——如果直接連到 git.kernel.org,多半還是反應快、用起來順。這句話讀起來像是在替團隊的工程判斷背書:平均兩成算力被吃掉確實是浪費,但整個系統目前還撐得住,沒有到需要用更激烈手段的地步。

更刺眼的是流量結構。按 Ryabitsev 自己的說法,在一堆寬鬆假設下,這 600 萬次請求裡,真正的合法流量大概只占 2%。他自己也承認這個數字算得寬鬆,但即使打了折扣,量級的落差已經足夠說明問題:伺服器忙的絕大多數時間,服務的都不是真人。這裡有一個細節容易被忽略:作者自己用的形容詞是「寬鬆假設」,換句話說,2% 很可能是上限,不是下限。拿掉那些寬鬆的認定條件,真人流量的實際占比只會更低,不會更高。Ryabitsev 講得更直白:走到這一步,已經沒辦法確定地分辨哪一個請求是機器人、哪一個是真人。這也解釋了為什麼防禦手段沒辦法只針對壞人——當合法流量本身就稀薄到接近雜訊,任何想要保護伺服器的手段,都得同時接受可能誤傷這極少數的真人使用者。

爬蟲怎麼爬:不是 clone,是把每個 commit 都重新渲染一次

如果只是想要 linux.git 的完整內容,最便宜的做法是 git clone 一次——把物件一次抓回本地,之後想看哪個 commit 就在本地端翻,伺服器只需要處理一次請求。但 Ryabitsev 形容,這些爬蟲選的是「最蠢的做法」:一個 commit 一個 commit 地把頁面渲染成 HTML,再把 HTML 解析回內容。148 萬個 commit,就等於 148 萬次各自獨立的請求——這還只是 linux.git 本身,還沒算進那 922 個 fork。

cgit——就是 git.kernel.org 用來把倉庫渲染成網頁的軟體——原本設計得很寬容:不只能看單一 commit,還能要求任兩個 commit 之間的 diff,或直接輸出 patch,這在「網路是給人類用的」的年代是貼心設計,方便任何人臨時查一筆紀錄。換算成網址空間,Ryabitsev 用他自己誇飾的講法形容,光是 linux.git 單一個 fork,能生出的合法網址組合就有「1.2 METRIC BAJILLION」個——這不是精確數字,是他刻意誇張到失去意義的說法,用意是讓讀者感受這個數量級有多離譜。爬蟲不需要精確鎖定哪一個網址,只要照著 URL 的規律亂猜亂掃,命中率就已經高到能把伺服器榨乾;922 個 fork 疊上去,等於把同一份提交歷史的渲染成本,重複乘了 922 遍。

文章沒有細講 git 這個協定本身,但補一句背景會比較好讀:git 的傳輸機制本來就是為了有效率地同步一份倉庫而設計的,clone 一次就能把物件搬回本地,之後想看哪個 commit 都在自己機器上翻,不必讓伺服器把整份歷史一頁一頁攤開成網頁。爬蟲繞過這套現成機制,改用 HTML 介面逐頁去掃——用 Ryabitsev 自己的話說,這是「最蠢的做法」,而對伺服器來說,它換來的是成本最高的那種負載模式。

切換兩種抓法 · 1 次 clone 對 148 萬次個別渲染

git clone --mirror linux.git

一次請求,拿走完整的提交歷史,之後全部在本地端翻找。

commit #1
commit #2
commit #3
commit #4
commit #5
…還有 1,479,995 個

148 萬個 commit,就是 148 萬次各自獨立的 HTML 渲染加解析請求——還沒算進 922 個 fork。

這解釋了為什麼流量規模會失控。git clone 對伺服器來說是一次性的操作;逐 commit 渲染卻是把同一份提交歷史,每一筆都拆成一個獨立的請求,每一個都要 cgit 當場把那一個 commit 重新渲染成一頁 HTML 送出去。伺服器的負擔不是「資料量」,而是「渲染次數」——這正是那 14 到 16 顆核心在做的事。要說清楚的是,Ryabitsev 並沒有拆解每一次渲染的 CPU 成本實際花在哪一道工序上,也完全沒有談到快取;他給的是結果,不是成因分解:14 到 16 顆核心、平均全站兩成的容量。文章沒有講,但一個合理的推測是,逐 commit 渲染出來的每一頁網址都不一樣,快取很難靠重複命中省下成本——這是讀者自己補的推論,不是文章的說法。

從 fail2ban 到 Anubis:一路升級的防禦,一路被追上

面對這種流量,Ryabitsev 團隊的防禦手段是一路加碼上來的,不是一次到位。最早的做法很土:翻 log,人工揪出明顯是爬蟲的 IP,直接封鎖。這招撐不了多久——爬蟲很快就學會偽裝成一般瀏覽器的 User-Agent,接著乾脆改用大量一次性的住宅、行動網路 IP,一個 IP 只打 4、5 次請求就消失,log 裡再也追不到同一個來源第二次。這些一次性住宅 IP 從哪裡來,Ryabitsev 也給出了自己的解讀:問題不在家用裝置本身,而是背後的 App 廠商為了找獲利模式,把使用者家裡的裝置在不知情的狀況下變成替爬蟲中轉流量的節點——換句話說,真人的網路連線,可能正在背地裡被借去打別人的仗。逐一封 IP 的做法徹底跟不上,下一步是整段 ASN 一起封——即使這樣偶爾會誤傷少數想自動檢查 commit 連結的合法流量,Ryabitsev 仍認為划算。fail2ban 原本是拿來對付暴力破解密碼這類重複失敗模式的工具——邏輯是看到同一個來源短時間內一直踩到某種可疑行為,就自動下發封鎖規則。用在爬蟲身上,等於借用同一套機制去對付另一種重複行為,只是對手換成了更會偽裝的一方。

點名詞看解釋 · 4 個關鍵字

這條升級路徑跟前面提到的爬蟲機制,牽涉到幾個先弄懂會比較好讀的名詞: cgitgit.kernel.org 用來把倉庫內容渲染成網頁的軟體——原本設計得很寬容,可以看任一個 commit、任兩個 commit 之間的 diff,也能直接要求輸出 patch,方便任何人臨時查一筆紀錄。ASNAutonomous System Number——網路上一段路由範圍的身分識別碼,通常對應一整個機房或雲端供應商,封鎖一個 ASN 等於把那整段位址空間都擋在外面。proof-of-work要求發出請求的一方先自己解一道刻意設計成耗運算資源的題目,才放行進入下一步——目的不是識破機器人,而是拉高每一次請求的成本。, 以及 難度(difficulty)Anubis 出的題目,是要找出一個字串,跟請求方的 IP、加上系統給的一個密鑰組合起來,算出的 sha256 雜湊值前面要有指定數量的 0。要求的 0 越多,平均得試的次數就跳得越高,這就是「難度」實際在調的東西。

四個名詞分別對應「爬蟲怎麼爬」與「防禦怎麼擋」這兩條線——點開任一個看完整解釋。

proof-of-work 這套邏輯背後的假設是:對一支真人瀏覽器來說,多解一道題目、多等幾秒鐘,幾乎感覺不到;但對一支想要用低成本掃過幾百萬個網址的爬蟲來說,每個請求都多出的運算成本會直接乘上規模,變成龐大的總開銷。這個假設在爬蟲的算力還跟不上時成立,一旦爬蟲那邊也捨得砸更多運算資源,天平就會往回擺。

大約一年前,問題開始變得嚴重,團隊天真地以為找到了辦法:在整個網站前面架一層 Anubis proof-of-work,邏輯很直接——不去猜哪個請求是機器人,而是讓每個請求都先自己燒一段 CPU,把爬蟲原本零成本、量大的經濟模型整個反過來。具體的題目是:找出一個字串,跟請求方的 IP、外加系統給的一個密鑰組合起來,算出的 sha256 雜湊值前面要有指定數量的 0——這跟採礦問題的結構一樣,沒有捷徑可抄,只能硬算,能調的旋鈕只有「要求幾個 0」這一個。剛上線時效果立竿見影,大多數爬蟲直接放棄。但沒有撐太久。數個月後,爬蟲開始解得開難度 4 的題目;團隊把難度加到 5——難度 5 在手機上要花上好幾秒才能解開,手機還會發燙,對真人使用者也是實打實的代價。Ryabitsev 說難度 5 確實有效,替他們「多買到幾個月的平靜」;然後,爬蟲也開始解得開難度 5 了。兩次被追上的間隔,他用的是同一個量級的說法——都是幾個月。

點任一層看它為什麼撐不住 · 4 層防禦

四層防禦,一層一層被追上

四層防禦,一層一層被追上 L1 · 查 log、手動 fail2ban 爬蟲偽裝瀏覽器、改用大量一次性住宅/行動 IP L2 · 整段 ASN 一起封 偶爾誤傷合法自動化流量,仍判斷值得 L3 · 上線 Anubis proof-of-work 剛上線效果拔群,數個月後爬蟲解開難度 4 L4 · 難度一路加到 5 手機解題會發燙,現在 33% 仍能闖過

點卡片或方框看細節

為什麼撐不住

爬蟲很快就學會偽裝成一般瀏覽器的 User-Agent,接著改用大量一次性的住宅、行動網路 IP;同一個 IP 只打 4、5 次請求就換掉,log 裡再也找不到第二次,逐一封鎖 IP 的做法完全跟不上。

為什麼還不夠

ASN 封鎖能一次擋掉一整個網段的來源,但爬蟲手上的 IP 池本身就在持續擴張;而且這一步本身就有代價——會誤傷少數在自動檢查 commit 連結的合法流量。

為什麼只撐了數個月

Anubis 的邏輯不是識破機器人,而是讓每個請求先燒一段 CPU,把爬蟲原本零成本、量大的經濟模型反過來。這個經濟帳一開始算得過去,但幾個月後爬蟲的運算資源也追了上來,開始解得開難度 4 的題目。

現在的戰況

難度加到 5 之後,真人使用者用手機解題要花幾秒鐘、手機還會發燙——這是防禦手段自己也要付的代價。難度 5 確實有效,又替團隊買到幾個月的平靜,但爬蟲後來也開始解得開它。現在仍有 33% 的請求能算出答案、闖過驗證,66% 才被直接擋下。

把這四層攤開來看,會發現一個共通的節奏:每一次防禦手段上線,都先讓爬蟲的成本瞬間暴增,逼得多數爬蟲放棄;但只要還有人想要這批資料,就會有人願意投入更多運算資源去解題。這樣的節奏,合理的推測是不會只發生在 git.kernel.org 身上——任何把內容公開、又想擋掉自動化流量的站台,恐怕都得面對類似的動態。

現在的樣子:33% 解得開,而且沒有簡單解法

現在的戰況是:66% 的請求還是被 Anubis 直接擋在門外,連主站都碰不到;但已經有 33% 能算出答案,順利進到主站。Ryabitsev 對這個 33% 的解讀不是技術上的,而是動機上的:顯然他們手上的東西,值得對方砸下大把運算週期去把 Anubis 的題目算出來。要注意的是,文章只給了這一組 66 對 33 的快照,沒有時間序列,也沒有比對更早之前的數字,更沒有說破解率正在往上爬——「這個比例會不會繼續升高」是讀者自己的推論,不是文章的說法。文章確實講了的是另一件事:難度 4 在數個月後被解開,難度 5 也只替團隊多買到幾個月的平靜,兩次都落在同一個量級的時間裡。把這場軍備競賽拉開來看,一年前只需要查 log 手動封 IP 就能解決,現在要動用整段 ASN 封鎖、proof-of-work、難度分級——而拉高算術題目難度這套邏輯,遲早會撞上真人使用者的手機也要跟著多花幾秒解題這條線。

拖曳滑塊比較四個百分比 · 同一把尺

2% 合法流量 20% 核心占用 33% 解題成功 66% 直接擋下 0%
拖曳圓點,比較這幾個百分比在同一把尺上的量級。

如果你正在維運類似的公開服務,這幾個數字給出的其實是一套可以複製的健檢指標:多少比例的流量在解 proof-of-work、解題成功率有沒有在往上爬、又有多少核心被綁在爬蟲相關的渲染工作上——這三個數字動起來的方向,會比單純看今天流量是不是變高了,更早警覺到情況在惡化。

把這幾個數字擺在同一把尺上,落差本身就是重點:真正需要服務的合法流量只有 2%,卻要付出平均兩成的運算容量;而即使 Anubis 已經把難度加到讓手機發燙的程度,還是有三分之一的請求能闖過去。這也是為什麼這篇文章讀起來不像一般維運團隊會寫的「我們擋住了攻擊」勝利宣言——數字攤開以後,勝負其實還沒有定局,讀者看到的是一場還在進行中的拉鋸,而不是一個已經打贏的故事。

Ryabitsev 自己給出的結論沒有粉飾——這問題沒有簡單解法,他寫得很直白。但他也沒有把資料關起來:他承諾 git.kernel.org 依然會開放下載給任何需要的人,只是拿到資料要多跳過幾道關卡。這些關卡具體會是什麼形式,文章沒有講。合理的推測是,未來取得完整資料的方式可能會從網頁介面往命令列工具或批次下載的方向靠攏,減少「每個 commit 一個網頁」這種天生對爬蟲友善的介面。

現實檢查:如果你也在維運任何公開的 git 或文件鏡像站,這條從查 log 手動封 IP、到整段 ASN 封鎖、到 proof-of-work 分級加碼的路徑,值得先摸清楚自己現在站在哪一步——因為照 git.kernel.org 的經驗,下一步不會是最後一步。