程式設計師,熱愛你的 bug

學以致用123發表於2018-07-26

2017 年 10 月初,我在貝洛奧裡藏特(巴西) 的 Python Brasil 大會做了一個主題演講。下面是這個演講的筆記 。 這裡可以下載視訊。

我熱愛 bug

我現在是 Pilot.com 的高階工程師,為初創公司開發自動記賬系統。在這之前,我在 Dropbox 的桌面客戶端團隊工作,後面我會講到在那裡工作時的一些小故事。在那之前,我是 Recurse Center 的一個推進者,Recurse Center 對於程式設計師的感覺很像寫作者的隱居地。 我在大學學習的是天體物理學,在成為工程師之前在金融機構工作了幾年。

但是這些事情沒有一件是重要的—你只需要記住我熱愛 bug 就足夠了。我熱愛 bug 是因為它們非常有趣。它們富有戲劇性。一個大 bug 的查詢過程曲折離奇。一個大 bug 很像一個很好的笑話或謎語,你期待一個輸出,但結果卻大相徑庭。

在這個講演中,我將會講述一些我熱愛的 bug,解釋我為什麼如此熱愛 bug,然後說服你也應該熱愛 bug。

程式設計師,熱愛你的 bug

第一個 Bug

好,直接進入 第一個 Bug。這是我在 Dropbox 遇到的一個 bug 。你可能知道,Dropbox 是個應用程式,可以將檔案從一臺計算機同步到雲端,並同步到其它計算機。

這是一個簡化的 Dropbox 架構圖。桌面客戶端在本地監控檔案系統的變化。當它找到一個改變的檔案,將閱讀檔案並對 4MB 塊中的內容進行雜湊處理。這些塊儲存在一個巨大的鍵值對儲存後端,我們稱之為 blockserver。鍵是經雜湊處理的內容的摘要,值是內容本身。

當然,我們要避免多次上傳同一個塊。想象一下,你正在寫一個文件,很可能只是更改了結尾–我們不希望一次又一次的上傳開頭部分。因此,在將塊上傳到 blockserver 之前,客戶端與另一個管理 metadata 和許可權的伺服器通訊,客戶端詢問 meta 伺服器是否需要這個塊或者是否見過這個塊。meta 伺服器對每個塊是否需要上傳進行響應。

所以,請求和響應看起來是這樣的:客戶端:’我有一個由雜湊塊 'abcd,deef,efgh'組成的更改檔案。伺服器響應”我有前面兩個,上傳第三個”。然後客戶端將第三個上傳到 blockserver。

上面是設想,下面的則是 bug 。

有時,客戶端會發出一個奇怪的請求:每個雜湊值應該是16個字元長,但是請求的長度是33個字元,比期望長度的兩倍還多 1 。伺服器不知道該怎麼處理這個異常,會丟擲一個異常。我們看到這個異常報告,並檢視客戶端的日誌檔案,真是奇怪的現象—客戶端本地資料庫損壞了,或者 python 將丟擲 MemoryErrors ,所有這些都沒有道理。

如果你從沒有見過這個問題,那麼這完全是個謎。但是一旦見過一次,之後的每一次都會認出它。這裡有個提示:我們經常看到的 33 個字元長的字串的中間的字元不是逗號而是l。下面是我們在中間位置看到的其他字元:

逗號的 ascii 碼是 44 ,l的 ascii 碼是 108,在二進位制中,它們是這表示的:

你將發現l與逗號僅僅相差 1 位。而這就是問題所在:一個位翻轉 (bitflip)。客戶端使用的記憶體有一個 bit 損壞了,現在客戶端正在向伺服器傳送垃圾請求。

下面是出現位翻轉時我們經常看到代替逗號的其他字元:

位翻轉是真實存在的!

我熱愛這個 bug 是因為它證明了位翻轉是真實存在的,而不只是理論概念。實際上,這種情況在一些領域中比其他領域更常見。 從低端或老硬體的使用者獲得請求是其中一個,這是很多執行 Dropbox 的膝上型電腦的真實情況。 另外一個有很多位翻轉的領域是外層空間——太空沒有大氣層來保護記憶體免受高能粒子和輻射的影響,所以位翻轉很常見。

在太空中,你可能真的非常關心資料的正確性。比如,你的程式碼可能用於讓國際空間站中的宇航員生存下去,即使不是這樣的關鍵任務,在太空中進行軟體更新是很難的。如果真的需要應用程式不存在位翻轉 ,可以採取多種硬體和軟體方法。對於這個問題,Katie Betchold 有一個非常有趣的演講

Dropbox 不需要處理位翻轉 。損壞記憶體的電腦是使用者的,我們可以檢測到逗號是否發生了位翻轉,但如果它是不同的字元,我們不一定會知道,如果位翻轉發生在磁碟讀取的實際檔案中,我們就不知道了。我們可以發現這個問題的空間太有限了,因此我們決定不對異常進行處理並繼續。這類 bug 通常可以通過客戶端重啟電腦解決。

不容易發生的 bug 並不是不可能的

這是它成為我最喜歡的 bug 的原因之一。 它可以提醒我們 unlikely 和 impossible 的區別。 在足夠的規模下, unlikely 事件以明顯的速率發生。

通用 bug

我最喜歡這個錯誤的第二個原因在於通用。 這個 bug 可能發生在桌面客戶端與 server 通訊的任何位置,系統中有很多不同的端點和元件。這意味著 Dropbox 的許多工程師將會看到這個 bug 的不同版本。當你第一次看到它時,真的非常傷腦筋,但是之後很容易診斷,而且檢查非常快:只需要看看中間的字元是不是l

文化差異

這個 bug 的一個有趣的副作用是它暴露了伺服器團隊和客戶端團隊的文化差異。 有時候伺服器小組的成員會發現這個 bug 並進行調查。 如果一臺伺服器正在翻轉位,這可能不是偶然的現象 – 很可能是記憶體損壞,你需要找到受影響的機器並儘快將其從伺服器池中移出,否則可能會損壞大量的使用者資料。 這是一個事件,你需要快速回應。 但是,如果使用者的機器正在損壞資料,那麼可以做的事情就不多了。

分享你的 Bug

所以,如果你正在研究一個令人困惑的 bug ,特別是大系統中的一個 bug ,不要忘了與別人交流。 也許你的同事之前看到過一個這樣 bug 。 如果他們看到過,可以節省很多時間。 如果他們不知道,記得告訴別人解決問題的方法 – 寫下來或在團隊會議上講出來。 下一次你們的隊伍有類似的事情發生時,你們會更有準備。

Bug 如何幫助我們學習

Recurse Center

加入 Dropbox 之前,我在 Recurse Center (RC) 工作。RC 是一個社群,它的的理念是幫助具備自我導向的學習者通過協作共同成長為更好的程式設計師。這是 RC 的全部:這裡沒有任何課程、作業或者截止日期。唯一的課題是分享變為更好的程式設計師的目標。我們看到很多獲得 CS 學位但是對實際程式設計沒有把握的人蔘加這個專案,或者寫了十年 Java 又想學習 Clojure 或者 Haskell 的人蔘加這個專案,當然還有很多其他的參與者。

我的工作是推進者,工作職責是幫助使用者填補缺乏的結構和根據從以前的參與者身上學到的東西提供指導。 所以我和我的同事對於幫助自我激勵的成年人學習最好的技術非常感興趣。

刻意練習

這個領域有很多不同的研究,我認為最有趣的一項研究是刻意練習的思想。刻意練習試圖解釋專家與業餘愛好者的差別。這裡的指導原則是,如果你只關注與生俱來的特徵-遺傳或其他-它們不會對解釋差異做出太大貢獻。因此研究人員(開始是 Ericsson , Krampe 和 Tesch-Romer )開始研究是什麼造成了這些差異。他們的結論是花費在刻意練習上的時間。

刻意練習定義的範圍非常狹窄:不是為了報酬,也不是為了玩樂。我們必須在自己能力的邊緣進行練習,做一個適合自己水平的專案(不會容易的學不到任何東西,也不會困難到毫無進展)。還必須獲得做法是否有效的及時反饋。

這非常令人興奮,因為這是如何構建專業知識的框架。但是挑戰在於,對於程式設計師來講,這個建議難以實現。程式設計師很難知道自己是否在能力邊緣工作,及時反饋也非常罕見(在某些情況下可能會立即得到反饋,而在其他情況下可能需要幾個月的時間才會有反饋)。你可以在 REPL 等一些小事上得到快速反饋,但是如何進行設計決策或者選擇技術,很可能很長時間都無法得到反饋。

但是刻意練習對於除錯程式碼非常有用。如果編寫程式碼,編程式碼時會有程式碼如何工作的心智模式。如果程式碼有一個 bug ,那麼心智模式並不完全正確。根據刻意練習的定義,你處在理解的邊緣,太棒了,你即將學習新的東西。如果你能夠重現 bug ,那麼可以立即獲得修復是否正確的反饋(這種情況非常罕見)。

這種型別的 bug 可能會使你瞭解一些關於自己程式的資訊,也有可能學到程式碼所執行的系統的更多內容。我這裡有一個這樣的 bug 的故事。

第二個 Bug

這個 bug 也是在 Dropbox 工作時遇到的。那時,我正在研究為什麼有些桌面客戶端不按時傳送日誌 。我深入研究了客戶端日誌系統並發現一些有意思的 bug 。我們這裡談到的只是其中與這個故事有關一部分 。

下面是系統架構簡圖。

桌面客戶端將生成日誌 。這些日誌被壓縮、加密並寫入磁碟,然後客戶端定期將它們傳送到伺服器。客戶端將從磁碟讀取日誌並將它們傳送到日誌伺服器。日誌伺服器將解密並儲存,然後返回 200 響應。

如果客戶端無法連線日誌伺服器,它不會讓日誌目錄無限增大。當日志目錄達到一定大小時,客戶端將刪除日誌從而保證日誌目錄的大小在最大範圍之內。

最初的兩個 bug 是些小問題。第一個是桌面客戶端向伺服器傳送日誌時從最舊的開始(而不是從最新的開始)。這不是我們想要的,比如,如果客戶端報告了一個異常,伺服器將要求客戶端傳送日誌檔案,這時你可能關心剛剛發生的情況的日誌,而不是磁碟上最舊的日誌。

第二個 bug 與第一個類似:如果日誌目錄達到設定的最大值,客戶端將從最新的日誌開始刪除(而不是刪除最舊的日誌)。這時,哪種方法都會刪除日誌,只是我們更關心比較新的日誌。

第三個 bug 與加密有關。有時,伺服器無法解密日誌檔案(我們通常無法找到原因-可能是位元組反轉)。後端無法正確處理這個錯誤,因此伺服器會返回 500 響應。客戶端在接收到 500 響應時的表現相當合理:它將假設伺服器已關閉。因此,它會停止傳送日誌檔案,不再嘗試傳送其它檔案。

對損壞的日誌檔案返回 500 響應顯然是錯誤的行為。我們可以考慮返回 400 響應,因為這是客戶端的問題。但是客戶端也無法解決這個問題-如果日誌檔案現在無法解密,將來也無法解密。因此,我們真正想讓客戶端做的只是刪除日誌檔案並繼續工作。實際上,客戶端從伺服器獲取 200 響應時預設日誌檔案儲存成功。所以,如果日誌檔案無法解密,返回 200 響應就可以了。

所有這些 bug 都很容易修復。前兩個錯誤發生在客戶端,所以我們在 alpha 版本進行修復,但是還沒有釋出給大多數客戶。我們在伺服器上修復第三個錯誤並部署。

突然之間,日誌叢集流量激增。服務團隊詢問我們是否知道發生了什麼事情。我花了一分鐘的時間把所有情況放在一起。

在這些問題修復之前,四件事情正在發生:

  1. 日誌檔案從最老版本開始傳送
  2. 日誌檔案從最新版本開始刪除
  3. 如果伺服器無法解密日誌檔案,它將返回 500 響應
  4. 如果客戶端接收到 500 響應,它將停止傳送日誌

客戶端可能會嘗試傳送損壞的日誌檔案,伺服器返回 500 響應,客戶端放棄傳送日誌。下一次執行時,它會嘗試再次傳送相同的檔案,再次失敗並再次放棄。最終日誌目錄會變滿,客戶端將開始刪除最新日誌檔案,並將損壞的日誌檔案保留在磁碟上。

這三個 bug 的結果是:如果客戶端曾經有一個損壞的日誌檔案,我們將再也看不到來自該客戶端的日誌檔案。

問題在於,處於這種狀態的客戶端比我們想象的要多得多。 任何具有單個損壞檔案的客戶端都無法將日誌檔案傳送到伺服器。 現在這個問題被解決了,他們都在傳送日誌目錄中的其餘內容。

我們的選擇

世界各地的機器會造成很大的流量,我們可以做什麼呢?(在與 Dropbox 規模相當的公司工作是件有趣的事情,特別是 Dropbox 的桌面客戶端規模:你可以輕易地觸發自我 DDOS )。

進行部署時,發現問題的第一個選擇是回滾。這是完全合理的選擇,但是在這種情況下沒有任何幫助。我們要轉換的不是伺服器上的狀態,而是客戶端上的狀態–我們已經刪除了這些檔案。回滾伺服器將防止其它客戶端進入這個狀態,但是不能解決問題。

增加日誌叢集的規模可行嗎?我們這樣做了,並開始接收到更多的請求,現在我們已經進行了擴容。我們又進行了一次擴容,但是不能總這樣。為什麼不能?這些叢集不是隔離的,它將請求另外一個叢集(這裡是為了處理異常)。如果遇到指向一個叢集的 DDOS ,並且持續擴大叢集規模,那麼需要解決它們的依賴關係,這樣就變成兩個問題了。

我們考慮的另一個選擇是減輕負擔-你不需要每個日誌檔案,所以我們可以放棄請求。這裡的一個挑戰在於很難確定哪個需要哪個不需要,我們無法快速區分新日誌和舊日誌。

我們確定的解決方案是 Dropbox 在許多不同場合使用的解決方案:我們有一個自定義標頭chillout,所有的客戶端都可以接收這個標頭。如果客戶端接收到包含這個標頭的響應,那麼它在設定時間內不傳送任何請求。有人非常明智的在很早的時候將它新增到 Dropbox 客戶端中,多年來它不止一次派上用場。日誌記錄伺服器無法設定這個標頭,但這是一個容易解決的問題。我們的兩個同事( Isaac Goldberg 和 John Lai )提供了支援。我們首先將日誌叢集的 chillout 設定為兩分鐘,高峰過去幾天之後再將其關閉。

瞭解你的系統

這個 bug 的第一個教訓是瞭解你的系統。我頭腦中有一個很好的客戶端和伺服器進行互動的模型。但是,我並沒有想到伺服器同時與所有客戶端互動時會發生什麼?這是我從來沒有想到過的複雜程度。

瞭解你的工具

第二個教訓是瞭解你的工具。如果事情發生了,你可以採取什麼措施?你可以反轉遷移嗎?如果事情發生了,你如何瞭解它,如何找到更多資訊?最好在危機發生之前瞭解這些內容,如果你沒有這樣做,你將在危機發生過程中學到,然後永遠不會忘記。

功能標誌位 & 服務端門控

如果寫移動或客戶端應用,這是第三個教訓:需要服務端特性門控和服務端標誌位。當你發現一個問題並且無法控制服務端,釋出一個新的版本或者嚮應用商店提交一個新版本可能需要幾天甚至幾周的時間。那是一種很不好的方法。Dropbox 客戶端不需要處理應用商店審查流程,但是向幾千萬客戶端推送也需要時間。我們也可以這樣解決,出現問題時翻轉伺服器上的開關然後十分鐘解決問題。

但是,這個策略也有開銷。新增很多標誌位會增加程式碼的複雜度。在測試中會遇到組合問題:如何同時啟用了功能 A 和功能 B,或者只有一個,或者一個都不啟動 —如果具有 N 個特性則會非常複雜。完成之後請工程師清理功能標誌位也將會非常困難(我也犯了這個錯誤)。對於桌面客戶端來講,可能同時會有很多版本,這將很難處理。

但是好處在於—當你需要它們時,你真的非常需要它。

如何熱愛 bugs

我談到了我喜歡的一些 bug,並且談到了為什麼熱愛這些 bug 。 現在我想告訴你如何去熱愛 bug 。 如果你還不喜歡 bug,我知道一種學習方式–具有成長思維模式。

社會學家 Carol Dweck 在人們如何看待能力方面做過很多有趣的研究。她發現人們使用兩種不同的框架認識能力。第一個,她稱之為固定思維模式,認為能力是一成不變的,人們無法改變自己的能力。另一個思維模式為成長思維模式,在成長思維模式下,人們認為能力是可塑的,不斷的努力可以讓能力變得更強。

Dweck 發現一個人的能力框架-他們持有固定思維模式還是成長思維模式-會非常明顯的影響他們選擇任務的方式、他們應對挑戰的方式、他們的認知表現、甚至他們的誠實。

我在 Kiwi PyCon 主題演講中也談到了成長思維,下面這些只是部分摘錄,你可以閱讀完整版本這裡

關於誠實:

之後,他們讓學生把這項研究的結果寫信告訴筆友:“我們在學校做了這項研究,這是我得到的分數。” 他們發現近一半因為聰明被稱讚的學生篡改了分數,因為努力工作而受稱讚的學生則基本沒有不誠實的。

關於努力:

幾項研究發現,有固定思維模式的人可能不願意付出努力,因為他們認為需要努力意味著他們不擅長正在從事的事情。Dweck 指出:“ 如果每次任務都需要努力,那麼很難保持對自己能力的信心,你的能力將會受到質疑。”

對混亂的反應:

他們發現,不管資料裡是否含有混亂的段落,成長思維的學生大約能夠掌握資料的 70% 。固定思維的學生中,如果閱讀不包括混亂段落的書,他們也可以掌握資料的 70%。但是當固定思維的學生遇到混亂的段落,他們的掌握率下降到 30% 。固定思維的學生在從混亂恢復過來的過程中會遇到很大的困難。

這些發現表明,debug 過程中成長思維非常關鍵。我們需要從混亂過程中恢復過來,對我們理解的侷限性保持坦誠,有時找到解決方案的道路真的非常曲折—所有這些,具有成長思維的人更容易處理,遇到痛苦也會少一些。

熱愛你的 bug

通過在 Recurse Center 工作時的慶祝挑戰,我學會了熱愛 bug 。一位參與者會坐到我旁邊說:“[嘆氣] 我想我遇到了一個奇怪的 Python 錯誤”,我說:“太棒了,我熱愛奇怪的 Python 錯誤!”首先,這是絕對正確的,但是更重要的是,這強調參與者找出一些他們努力取得成就的東西,完成它對於他們來說是件好事。

正如我提到的, Recurse Center 沒有截止期限和重要節點,這種環境非常自由。我會說:“你可以花一整天的時間去查詢 Flask 中這個奇怪的 bug ,多麼刺激!” 在 Dropbox 和 Pilot,我們要釋出產品、有截止日期、有使用者,我並不總能花一天的時間解決一個奇怪的 bug 。因此,我對具有截止日期的現實世界深表同情。但是,如果我有一個需要修復的 bug ,我必須修復它,抱怨這個錯誤並不會幫助我更快地修復它。 我認為,即使在最終期限的即將到來的時候,你仍然可以持這種態度。

如果你熱愛 bug ,在解決棘手問題時可能會獲得更多的樂趣。你可能不那麼擔心並更加專注。最終會從中學到更多。最後,你可以與朋友和同事分享 bug ,這可以幫助你和你的隊友。

謝謝

感謝那些對這次演講給我反饋以及幫助我來到這裡的朋友:

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

打賞支援我翻譯更多好文章,謝謝!

打賞譯者

打賞支援我翻譯更多好文章,謝謝!

任選一種支付方式

程式設計師,熱愛你的 bug 程式設計師,熱愛你的 bug

相關文章