標籤:

熱愛你的 Bug

十月初的時候我在貝洛奧里藏特的巴西 Python 大會Python Brasil上做了主題演講。這是稍加改動過的演講文稿。你可以在這裡觀看演講視頻。

我愛 bug

我目前是 Pilot.com 的一位高級工程師,負責給創業公司提供自動記賬服務。在此之前,我曾是 Dropbox 的桌面客戶端組的成員,我今天將分享關於我當時工作的一些故事。更早之前,我是 Recurse Center 的導師,給身在紐約的程序員提供臨時的訓練環境。在成為工程師之前,我在大學攻讀天體物理學並在金融界工作過幾年。

但這些都不重要——關於我你唯一需要知道的是,我愛 bug。我愛 bug 因為它們有趣。它們富有戲劇性。調試一個好的 bug 的過程可以非常迂迴曲折。一個好的 bug 像是一個有趣的笑話或者或者謎語——你期望看到某種結果,但卻事與願違。

在這個演講中我會給你們講一些我曾經熱愛過的 bug,解釋為什麼我如此愛 bug,然後說服你們也同樣去熱愛 bug。

Bug 1 號

好,讓我們直接來看第一個 bug。這是我在 Dropbox 工作時遇到的一個 bug。你們或許聽說過,Dropbox 是一個將你的文件從一個電腦上同步到雲端和其他電腦上的應用。

+--------------+ +---------------+ | | | | | 元數據伺服器 | | 塊伺服器 | | | | | +-+--+---------+ +---------+-----+ ^ | ^ | | | | | +----------+ | | +---> | | | | | 客戶端 +--------+ +--------+ | +----------+

這是個極度簡化的 Dropbox 架構圖。桌面客戶端在你的電腦本地運行,監聽文件系統的變動。當它檢測到文件改動時,它讀取改變的文件,並把它的內容 hash 成 4 MB 大小的文件塊。這些文件塊被存放在後端一個叫做塊伺服器blockserver的巨大的鍵值對資料庫key-value store中。

當然,我們想避免多次上傳同一個文件塊。可以想見,如果你在編寫一份文檔,你應該大部分時候都在改動文檔最底部——我們不想一遍又一遍地上傳開頭部分。所以在上傳文件塊到塊伺服器之前之前,客戶端會先和一個負責管理元數據和許可權等等的伺服器溝通。客戶端會詢問這個元數據伺服器metaserver它是需要這個文件塊,還是已經見過這個文件塊了。元數據伺服器會返回每一個文件塊是否需要上傳。

所以這些請求和響應看上去大概是這樣:客戶端說「我有一個改動過的文件,分為這些文件塊,它們的 hash 是 abcd,deef,efgh。伺服器響應說「我有前兩塊,但需要你上傳第三塊」。然後客戶端會把那個文件塊上傳到塊伺服器。

+--------------+ +---------------+ | | | | | 元數據伺服器 | | 塊伺服器 | | | | | +-+--+---------+ +---------+-----+ ^ | ^ | | 有, 有, 無 |abcd,deef,efgh | | +----------+ | efgh: [內容] | +---> | | | | | 客戶端 +--------+ +--------+ | +----------+

這是問題的背景。下面是 bug。

+--------------+ | | | 塊伺服器 | | | +-+--+---------+ ^ | | | ???abcdldeef,efgh | | +----------+ ^ | +---> | | ^ | | 客戶端 + +--------+ | +----------+

有時候客戶端會提交一個奇怪的請求:每個 hash 值應該包含 16 個字母,但它卻發送了 33 個字母——所需數量的兩倍加一。伺服器不知道該怎麼處理它,於是會拋出一個異常。我們收到這個異常的報告,於是去查看客戶端的記錄文件,然後會看到非常奇怪的事情——客戶端的本地資料庫損壞了,或者 python 拋出 MemoryError,沒有一個合乎情理的。

如果你以前沒見過這個問題,可能會覺得毫無頭緒。但當你見過一次之後,你以後每次看到都能輕鬆地認出它來。給你一個提示:在那些 33 個字母的字元串中,l 經常會代替逗號出現。其他經常出現的字元是:

l x0c < $ ( . -

英文逗號的 ASCII 碼是 44。l 的 ASCII 碼是 108。它們的二進位表示如下:

bin(ord(,)): 0101100 bin(ord(l)): 1101100

你會注意到 l 和逗號只差了一位。問題就出在這裡:發生了位反轉。桌面客戶端使用的內存中的一位發生了錯誤,於是客戶端開始向伺服器發送錯誤的請求。

這是其他經常代替逗號出現的字元的 ASCII 碼:

, : 0101100l : 1101100x0c : 0001100< : 0111100$ : 0100100( : 0101000. : 0101110- : 0101101

位反轉是真的!

我愛這個 bug 因為它證明了位反轉是可能真實發生的事情,而不只是一個理論上的問題。實際上,它在某些情況下會比平時更容易發生。其中一種情況是用戶使用的是低配或者老舊的硬體,而運行 Dropbox 的電腦很多都是這樣。另外一種會造成很多位反轉的地方是外太空——在太空中沒有大氣層來保護你的內存不受高能粒子和輻射的影響,所以位反轉會十分常見。

你大概非常在乎在宇宙中運行的程序的正確性——你的代碼或許事關國際空間站中宇航員的性命,但即使沒有那麼重要,也還要考慮到在宇宙中很難進行軟體更新。如果你的確需要讓你的程序能夠處理位反轉,有很多硬體和軟體措施可供你選擇,Katie Betchold 還關於這個問題做過一個非常有意思的講座。

在剛才那種情況下,Dropbox 並不需要處理位反轉。出現內存損壞的是用戶的電腦,所以即使我們可以檢測到逗號字元的位反轉,但如果這發生在其他字元上我們就不一定能檢測到了,而且如果從硬碟中讀取的文件本身發生了位反轉,那我們根本無從得知。我們能改進的地方很少,於是我們決定無視這個異常並繼續程序的運行。這種 bug 一般都會在客戶端重啟之後自動解決。

不常見的 bug 並非不可能發生

這是我最喜歡的 bug 之一,有幾個原因。第一,它提醒我注意不常見和不可能之間的區別。當規模足夠大的時候,不常見的現象會以值得注意的頻率發生。

覆蓋面廣的 bug

這個 bug 第二個讓我喜歡的地方是它覆蓋面非常廣。每當桌面客戶端和伺服器交流的時候,這個 bug 都可能悄然出現,而這可能會發生在系統里很多不同的端點和組件當中。這意味著許多不同的 Dropbox 工程師會看到這個 bug 的各種版本。你第一次看到它的時候,你 真的 會滿頭霧水,但在那之後診斷這個 bug 就變得很容易了,而調查過程也非常簡短:你只需找到中間的字母,看它是不是個 l

文化差異

這個 bug 的一個有趣的副作用是它展示了伺服器組和客戶端組之間的文化差異。有時候這個 bug 會被伺服器組的成員發現並展開調查。如果你的 伺服器 上發生了位反轉,那應該不是個偶然——這很可能是內存損壞,你需要找到受影響的主機並儘快把它從集群中移除,不然就會有損壞大量用戶數據的風險。這是個事故,而你必須迅速做出反應。但如果是用戶的電腦在破壞數據,你並沒有什麼可以做的。

分享你的 bug

如果你在調試一個難搞的 bug,特別是在大型系統中,不要忘記跟別人討論。也許你的同事以前就遇到過類似的 bug。若是如此,你可能會節省很多時間。就算他們沒有見過,也不要忘記在你解決了問題之後告訴他們解決方法——寫下來或者在組會中分享。這樣下次你們組遇到類似的問題時,你們都會早有準備。

Bug 如何幫助你進步

Recurse Center

在加入 Dropbox 之前,我曾在 Recurse Center 工作。它的理念是建立一個社區讓正在自學的程序員們聚到一起來提高能力。這就是 Recurse Center 的全部了:我們沒有大綱、作業、截止日期等等。唯一的前提條件是我們都想要成為更好的程序員。參與者中有的人有計算機學位但對自己的實際編程能力不夠自信,有的人已經寫了十年 Java 但想學 Clojure 或者 Haskell,還有各式各樣有著其他的背景的參與者。

我在那裡是一位導師,幫助人們更好地利用這個自由的環境,並參考我們從以前的參與者那裡學到的東西來提供指導。所以我的同事們和我本人都非常熱衷於尋找對成年自學者最有幫助的學習方法。

刻意練習

在學習方法這個領域有很多不同的研究,其中我覺得最有意思的研究之一是刻意練習的概念。刻意練習理論意在解釋專業人士和業餘愛好者的表現的差距。它的基本思想是如果你只看內在的特徵——不論先天與否——它們都無法非常好地解釋這種差距。於是研究者們,包括最初的 Ericsson、Krampe 和 Tesch-Romer,開始尋找能夠解釋這種差距的理論。他們最終的答案是在刻意練習上所花的時間。

他們給刻意練習的定義非常精確:不是為了收入而工作,也不是為了樂趣而玩耍。你必須盡自己能力的極限,去做一個和你的水平相稱的任務(不能太簡單導致你學不到東西,也不能太難導致你無法取得任何進展)。你還需要獲得即時的反饋,知道自己是否做得正確。

這非常令人興奮,因為這是一套能夠用來建立專業技能的系統。但難點在於對於程序員來說這些建議非常難以實施。你很難知道你是否處在自己能力的極限。也很少有即時的反饋幫助你改進——有時候你能得到任何反饋都已經算是很幸運了,還有時候你需要等幾個月才能得到反饋。對於在 REPL 中做的簡單的事情你可以很快地得到反饋,但如果你在做一個設計上的決定或者技術上的選擇,你在很長一段時間裡都無法得到反饋。

但是在有一類編程工作中刻意練習是非常有用的,它就是 debug。如果你寫了一份代碼,那麼當時你是理解這份代碼是如何工作的。但你的代碼有 bug,所以你的理解並不完全正確。根據定義來說,你正處在你理解能力的極限上——這很好!你馬上要學到新東西了。如果你可以重現這個 bug,那麼這是個寶貴的機會,你可以獲得即時的反饋,知道自己的修改是否正確。

像這樣的 bug 也許能讓你學到關於你的程序的一些小知識,但你也可能會學到一些關於運行你的代碼的系統的一些更複雜的知識。我接下來要講一個關於這種 bug 的故事。

Bug 2 號

這也是我在 Dropbox 工作時遇到的 bug。當時我正在調查為什麼有些桌面客戶端沒有像我們預期的那樣持續發送日誌。我開始調查客戶端的日誌系統並且發現了很多有意思的 bug。我會挑一些跟這個故事有關的 bug 來講。

和之前一樣,這是一個非常簡化的系統架構。

+--------------+ | | +---+ +----------> | 日誌伺服器 | |日誌| | | | +---+ | +------+-------+ | | +-----+----+ | 200 ok | | | | 客戶端 | <-----------+ | | +-----+----+ ^ +--------+--------+--------+ | ^ ^ | +--+--+ +--+--+ +--+--+ +--+--+ | 日誌 | | 日誌 | | 日誌 | | 日誌 | | | | | | | | | | | | | | | | | +-----+ +-----+ +-----+ +-----+

桌面客戶端會生成日誌。這些日誌會被壓縮、加密並寫入硬碟。然後客戶端會間歇性地把它們發送給伺服器。客戶端從硬碟讀取日誌並發送給日誌伺服器。伺服器會將它解碼並存儲,然後返回 200。

如果客戶端無法連接到日誌伺服器,它不會讓日誌目錄無限地增長。超過一定大小之後,它會開始刪除日誌來讓目錄大小不超過一個最大值。

最初的兩個 bug 本身並不嚴重。第一個 bug 是桌面客戶端向伺服器發送日誌時會從最早的日誌而不是最新的日誌開始。這並不是很好——比如伺服器會在客戶端報告異常的時候讓客戶端發送日誌,所以你可能最在乎的是剛剛生成的日誌而不是在硬碟上的最早的日誌。

第二個 bug 和第一個相似:如果日誌目錄的大小達到了上限,客戶端會從最新的日誌而不是最早的日誌開始刪除。同理,你總是會丟失一些日誌文件,但你大概更不在乎那些較早的日誌。

第三個 bug 和加密有關。有時伺服器會無法對一個日誌文件解碼(我們一般不知道為什麼——也許發生了位反轉)。我們在後端沒有正確地處理這個錯誤,而伺服器會返回 500。客戶端看到 500 之後會做合理的反應:它會認為伺服器停機了。所以它會停止發送日誌文件並且不再嘗試發送其他的日誌。

對於一個損壞的日誌文件返回 500 顯然不是正確的行為。你可以考慮返回 400,因為問題出在客戶端的請求上。但客戶端同樣無法修復這個問題——如果日誌文件現在無法解碼,我們後也永遠無法將它解碼。客戶端正確的做法是直接刪除日誌文件然後繼續運行。實際上,這正是客戶端在成功上傳日誌文件並從伺服器收到 200 的響應時的默認行為。所以我們說,好——如果日誌文件無法解碼,就返回 200。

所有這些 bug 都很容易修復。前兩個 bug 出在客戶端上,所以我們在 alpha 版本修復了它們,但大部分的客戶端還沒有獲得這些改動。我們在伺服器代碼中修復了第三個 bug 並部署了新版的伺服器。

激增

突然日誌伺服器集群的流量開始激增。客服團隊找到我們並問我們是否知道原因。我花了點時間把所有的部分拼到一起。

在修復之前,這四件事情會發生:

  1. 日誌文件從最早的開始發送
  2. 日誌文件從最新的開始刪除
  3. 如果伺服器無法解碼日誌文件,它會返回 500
  4. 如果客戶端收到 500,它會停止發送日誌

一個存有損壞的日誌文件的客戶端會試著發送這個文件,伺服器會返回 500,客戶端會放棄發送日誌。在下一次運行時,它會嘗試再次發送同樣的文件,再次失敗,並再次放棄。最終日誌目錄會被填滿,然後客戶端會開始刪除最新的日誌文件,而把損壞的文件繼續保留在硬碟上。

這三個 bug 導致的結果是:如果客戶端在任何時候生成了損壞的日誌文件,我們就再也不會收到那個客戶端的日誌了。

問題是,處於這種狀態的客戶端比我們想像的要多很多。任何有一個損壞文件的客戶端都會像被關在堤壩里一樣,無法再發送日誌。現在這個堤壩被清除了,所有這些客戶端都開始發送它們的日誌目錄的剩餘內容。

我們的選擇

好的,現在文件從世界各地的電腦如洪水般湧來。我們能做什麼?(當你在一個有 Dropbox 這種規模,尤其是這種桌面客戶端的規模的公司工作時,會遇到這種有趣的事情:你可以非常輕易地對自己造成 DDoS 攻擊)。

當你部署的新版本發生問題時,第一個選項是回滾。這是非常合理的選擇,但對於這個問題,它無法幫助我們。我們改變的不是伺服器的狀態而是客戶端的——我們刪除了那些出錯文件。將伺服器回滾可以防止更多客戶端進入這種狀態,但它並不能解決根本問題。

那擴大日誌集群的規模呢?我們試過了——然後因為處理能力增加了,我們開始收到更多的請求。我們又擴大了一次,但你不可能一直這麼下去。為什麼不能?因為這個集群並不是獨立的。它會向另一個集群發送請求,在這裡是為了處理異常。如果你的一個集群正在被 DDoS,而你持續擴大那個集群,你最終會把它依賴的集群也弄壞,然後你就有兩個問題了。

我們考慮過的另一個選擇是減低負載——你不需要每一個日誌文件,所以我們可以直接無視一些請求。一個難點是我們並沒有一個很好的方法來區分好的請求和壞的請求。我們無法快速地判斷哪些日誌文件是舊的,哪些是新的。

我們最終使用的是一個 Dropbox 里許多不同場合都用過的一個解決方法:我們有一個自定義的頭欄位,chillout,全世界所有的客戶端都遵守它。如果客戶端收到一個有這個頭欄位的響應,它將在欄位所標註的時間內不再發送任何請求。很早以前一個英明的程序員把它加到了 Dropbox 客戶端里,在之後這些年中它已經不止一次地起了作用。

了解你的系統

這個 bug 的第一個教訓是要了解你的系統。我對於客戶端和伺服器之間的交互有不錯的理解,但我並沒有考慮到當伺服器和所有這些客戶端同時交互的時候會發生什麼。這是一個我沒有完全搞懂的層面。

了解你的工具

第二個教訓是要了解你的工具。如果出了差錯,你有哪些選項?你能撤銷你做的遷移嗎?你如何知道事情出了差錯,你又如何發現更多信息?所有這些事情都應該在危機發生之前就了解好——但如果你沒有,你會在危機發生時學到它們並不會再忘記。

功能開關 & 伺服器端功能控制

第三個教訓是專門針對移動端和桌面應用開發者的:你需要伺服器端功能控制和功能開關。當你發現一個問題時如果你沒有伺服器端的功能控制,你可能需要幾天或幾星期來推送新版本或者提交新版本到應用商店中,然後問題才能得到解決。這是個很糟糕的處境。Dropbox 桌面客戶端不需要經過應用商店的審查過程,但光是把一個版本推送給上千萬的用戶就已經要花很多時間。相比之下,如果你能在新功能遇到問題的時候在伺服器上翻轉一個開關:十分鐘之後你的問題就已經解決了。

這個策略也有它的代價。加入很多的功能開關會大幅提高你的代碼的複雜度。而你的測試代碼更是會成指數地複雜化:要考慮 A 功能和 B 功能都開啟,或者僅開啟一個,或者都不開啟的情況——然後每個功能都要相乘一遍。讓工程師們在事後清理他們的功能開關是一件很難的事情(我自己也有這個毛病)。另外,桌面客戶端會同時有好幾個版本有人使用,也會加大思考難度。

但是它的好處——啊,當你需要它的時候,你真的是很需要它。

如何去愛 bug

我講了幾個我愛的 bug,也講了為什麼要愛 bug。現在我想告訴你如何去愛 bug。如果你現在還不愛 bug,我知道唯一一種改變的方法,那就是要有成長型心態。

社會學家 Carol Dweck 做了很多關於人們如何看待智力的研究。她找到兩種不同的看待智力的心態。第一種,她叫做固定型心態,認為智力是一個固定的特徵,人類無法改變自己智力的多寡。另一種心態叫做成長型心態。在成長型心態下,人們相信智力是可變的而且可以通過努力來增強。

Dweck 發現一個人看待智力的方式——固定型還是成長型心態——可以很大程度地影響他們選擇任務的方式、面對挑戰的反應、認知能力、甚至是他們的誠信度。

【我在紐西蘭 Kiwi Pycon 會議所做的主題演講中也討論過成長型心態,所以在此只摘錄一部分內容。你可以在這裡找到完整版的演講稿】

關於誠信的發現:

在這之後,他們讓學生們給筆友寫信講這個實驗,信中說「我們在學校做了這個實驗,這是我得的分數」。他們發現 因智力而受到表揚的學生中幾乎一半人謊報了自己的分數 ,而因努力而受表揚的學生則幾乎沒有人不誠實。

關於努力:

數個研究發現有著固定型心態的人會不願真正去努力,因為他們認為這意味著他們不擅長做他們正努力去做的這件事情。Dweck 寫道,「如果每當一個任務需要努力的時候你就會懷疑自己的智力,那麼你會很難對自己的能力保持自信。」

關於面對困惑:

他們發現有成長型心態的學生大約能理解 70% 的內容,不論裡面是否有難懂的段落。在有固定型心態的學生中,那些被分配沒有難懂段落的手冊的學生同樣可以理解大約 70%。但那些看到了難懂段落的持固定型心態的學生的記憶則降到了 30%。有著固定型心態的學生非常不擅長從困惑中恢復。

這些發現表明成長型心態對 debug 至關重要。我們必須從從困惑中重整旗鼓,誠實地面對我們理解上的不足,並時不時地在尋找答案的路上努力奮鬥——成長型心態會讓這些都變得更簡單而且不那麼痛苦。

熱愛你的 bug

我在 Recurse Center 工作時會直白地歡迎挑戰,我就是這樣學會熱愛我的 bug 的。有時參與者會坐到我身邊說「唉,我覺得我遇到了個奇怪的 Python bug」,然後我會說「太棒了,我 奇怪的 Python bug!」 首先,這百分之百是真的,但更重要的是,我這樣是在對參與者強調,找到讓自己覺得困難的事情是一種成就,而他們做到了這一點,這是件好事。

像我之前說過的,在 Recurse Center 沒有截止日期也沒有作業,所以這種態度沒有任何成本。我會說,「你現在可以花一整天去在 Flask 里找出這個奇怪的 bug 了,多令人興奮啊!」在 Dropbox 和之後的 Pilot,我們有產品需要發布,有截止日期,還有用戶,於是我並不總是對在奇怪的 bug 上花一整天而感到興奮。所以我對有截止日期的現實也是感同身受。但是如果我有 bug 需要解決,我就必須得去解決它,而抱怨它的存在並不會幫助我之後更快地解決它。我覺得就算在截止日期臨近的時候,你也依然可以保持這樣的心態。

如果你熱愛你的 bug,你可以在解決困難問題時獲得更多樂趣。你可以擔心得更少而更加專註,並且從中學到更多。最後,你可以和你的朋友和同事分享你的 bug,這將會同時幫助你自己和你的隊友們。

鳴謝!

在此向給我的演講提出反饋以及給我的演講提供其他幫助的人士表示感謝:

  • Sasha Laundy
  • Amy Hanlon
  • Julia Evans
  • Julian Cooper
  • Raphael Passini Diniz 以及其他的 Python Brasil 組織團隊成員

via: akaptur.com/blog/2017/1

作者:Allison Kaptur 譯者:yixunx 校對:wxy

本文由 LCTT 原創編譯,Linux中國 榮譽推出

推薦閱讀:

TAG:Bug | 錯誤 |