當前位置:
首頁 > 知識 > 程序員,請熱愛你的 bug(轉載)

程序員,請熱愛你的 bug(轉載)

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

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

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

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

程序員,請熱愛你的 bug(轉載)

640?wx_fmt=jpeg

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

程序員,請熱愛你的 bug(轉載)

640?wx_fmt=png

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

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

所以,請求和響應看起來是這樣的:客戶端:』我有一個由哈希塊 "abcd,deef,efgh"組成的更改文件。伺服器響應」我有前面兩個,上傳第三個」。然後客戶端將第三個上傳到 blockserver。

程序員,請熱愛你的 bug(轉載)

640?wx_fmt=png

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

程序員,請熱愛你的 bug(轉載)

640?wx_fmt=png

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

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

l x0c < $ ( . -

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

bin(ord(",")): 0101100

bin(ord("l")): 1101100

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

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

, : 0101100

l : 1101100

x0c : 0001100

< : 0111100

$ : 0100100

( : 0101000

. : 0101110

- : 0101101

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

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

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

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

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

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

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

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

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

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

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

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

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

這種類型的 bug 可能會使你了解一些關於自己程序的信息,也有可能學到代碼所運行的系統的更多內容。我這裡有一個這樣的 bug 的故事。

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

下面是系統架構簡圖。

程序員,請熱愛你的 bug(轉載)

640?wx_fmt=png

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

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

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

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

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

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

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

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

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

日誌文件從最老版本開始發送

日誌文件從最新版本開始刪除

如果伺服器無法解密日誌文件,它將返回 500 響應

如果客戶端接收到 500 響應,它將停止發送日誌

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

關於誠實:

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

關於努力:

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

對混亂的反應:

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

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

通過在 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 團隊的其他成員

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 程序員小新人學習 的精彩文章:

Python的字元串處理方法
Scala 中文亂碼解決

TAG:程序員小新人學習 |