歷時四年,Dropbox 用 Rust 重寫同步引擎核心代碼

本文首發於 InfoQ Pro,更多精彩內容搶先看,盡在 InfoQ Pro

開源 GO 語言工具庫、研究 iOS 和 Android 的 C++ 跨平臺開發,花費五年時間從雲平臺向數據中心反向遷移…Dropbox 從未停止對技術的“折騰”。如今,這家公司又花費了四年時間重寫了內部最古老、最重要的同步引擎核心代碼。

Dropbox 花費四年完成重構

過去四年,我們一直在努力重構 Dropbox 桌面客戶端同步引擎,這是 Dropbox 文件夾背後的重要技術,也是 Dropbox 最古老、最重要的代碼之一。經過四年努力,我們已經向所有 Dropbox 用戶發佈了用 Rust 寫的新同步引擎(代號爲 “Nucleus” )。

重寫同步引擎很難,我們也並不想盲目慶祝新版同步引擎的發佈,因爲在很多情景下,重寫是一個糟糕的想法。不過,事實證明,重寫對於 Dropbox 來說是一個好想法,但這只是因爲我們對整個過程考慮得非常周全。我們將在本文分享關於如何考慮一個重要軟件的重寫問題,並強調使該項目取得成功的關鍵舉措,比如,擁有一個非常乾淨的數據模型。

重構實屬無奈:問題太多

2008 年,Dropbox 同步首次進入測試階段。用戶安裝 Dropbox 應用程序,Dropbox 就會在他們電腦上創建一個文件夾,只要將文件保存在這個文件夾中,就可以把它們同步到 Dropbox 服務器和用戶的其他設備上。Dropbox 服務器可以持久而安全地存儲文件,並且這些文件還可以通過互聯網連接在任何地點訪問。

簡單地說,同步引擎駐留在計算機上,負責對用戶文件上傳和下載到遠程文件系統進行協調。

大規模同步很難

我們的第一個同步引擎稱之爲 “Sync Engine Classic” (意即 “經典同步引擎” ),它的數據模型存在一些基本問題,這些問題只有在大規模的情況下才會表現出來,使得漸進式改進變得不可能。

分佈式系統很難

單就 Dropbox 的規模而言,構建分佈式系統本身就是一項艱鉅的任務。撇開原始規模不談,文件同步就是一個獨特的分佈式系統問題,因爲允許客戶端長時間離線,並在重新上線時協調它們的更改。對許多分佈式系統算法來說,網絡分區是異常情況,但對我們來說卻是標準操作。

正確處理這一點很重要:用戶信任 Dropbox,並將自己最珍貴的內容託付給 Dropbox,因此,Dropbox 必須保證這些內容的安全,這沒有商量餘地。但是,雙向同步有許多極端情況,持久性比僅僅確保不刪除或破壞服務器上的數據要難得多。例如,Sync Engine Classic 將 “移動” 表示爲一對操作:在舊位置上進行 “刪除” 操作,並在新位置上進行 “添加” 操作。如果發生網絡中斷,刪除操作會進行,但對應的添加操作卻沒有進行。然後,用戶將會發現服務器和其他設備上出現文件丟失,即使他們只是在本地對文件進行移動操作也是如此。

保持持久性很難

Dropbox 的目標是:無論用戶的計算機配置如何,都能 “正常工作” 。我們支持 Windows、macOS 和 Linux,這些平臺都有各種各樣的文件系統,且所有這些文件系統的行爲略有不同。在操作系統下,硬件有很大差異,何況用戶還會安裝不同的內核擴展或驅動程序來改變操作系統行爲。而在 Dropbox 之上,應用程序以不同的方式使用文件系統,並且依賴的行爲可能實際上並不是其規範的一部分。

要保證特定環境的持久性,就需要理解其實現,有時甚至需要在調試生產問題時,對其進行逆向工程。這些問題通常會影響大量用戶,而一個罕見的文件系統錯誤可能隻影響很小一部分用戶。因此,從規模上來看,在大部分環境下能夠“正常工作”,並提供強大的持久性保證,從根本上是對立的。

測試文件同步很難

有了足夠大的用戶羣,幾乎所有理論上可能發生的事情都會在生產環境中發生。生產環境中的調試問題比開發環境中的調試問題要貴得多,特別是對於在用戶設備上運行的軟件而言。因此,在大規模生產之前,通過自動化測試來捕捉迴歸至關重要。

然而,同步引擎的測試很難,因爲文件狀態和用戶動作的可能組合是一個天文數字。一個共享文件夾可能有數千個成員,每個成員都有一個同步引擎,該引擎具有不同的連接性,以及不同的 Dropbox 文件系統的過期視圖。每個用戶可能有不同的本地更改等待上傳,並且他們從服務器下載文件的部分進度也可能有所不同。因此,系統有許多可能的 “快照” ,所以,所有這些都必須進行測試。

從系統狀態中採取的有效操作的數量也非常龐大。同步文件是一個高度併發的過程,用戶可能會同時上傳和下載許多文件。單個文件的同步可能涉及並行傳輸內容塊、將內容寫入磁盤或從本地文件系統讀取內容。全面測試需要嘗試這些操作的不同順序,以確保我們的系統不存在併發錯誤。

指定同步行爲很難

最後,通常很難精確定義同步引擎的正確行爲。例如,考慮這樣一種情況:假設我們有三個文件夾,其中一個文件夾嵌套在另一個文件夾中。

假設兩個用戶 Alberto 和 Beatrice,他們脫機使用這個文件夾。Alberto 將 “Archives” 文件夾移到 “January” 文件夾;而 Beatrice 將 “Drafts” 文件夾移到 “Archives” 文件夾。

當他們重新聯網後會發生什麼情況呢?如果直接應用這些步驟,那麼,我們的文件系統圖中將會出現一個循環:“Archives” 文件夾是 “Drafts” 文件夾的父目錄,“Drafts” 文件夾是 “January” 文件夾的父目錄,而 “January” 文件夾是 “Archives” 文件夾的父目錄。

在這種情況下,正確的最終系統狀態是什麼?Sync Engine Classic 複製每個文件夾,合併 Alberto 和 Bratrice 的目錄樹。使用 Nuclues,我們保留原始目錄,最終的順序取決於哪個同步引擎先上傳它們的移動操作。

在這種三個文件夾和兩個動作的簡單情況下,Nucleus 有一個令人滿意的最後狀態。但是,我們該如何在一般情況下指定同步行爲,而不會淹沒在一系列的邊角案例中呢?

譯註:邊角案例(Corner case),或病態案例(pathological case)是指其操作參數在正常範圍以外的問題或是情形,而且多半是幾個環境變量或是條件都在極端值的情形,即使這些極端值都還在參數規格範圍內(或是邊界),也算是邊角案例。

例如有某個揚音器會扭曲聲音,但只有在音量最大、低音最大及高溼度的環境下才會出現。或者服務器會有不穩定的情形,但條件是在最多 64 個輔助微處理器、內存爲最大值是 512 Gigabyte,同時一萬個用戶上線時纔會不穩定,這些都是邊角案例。

邊角案例和邊緣案例不同,邊緣條件只是單一個變量爲最大值或最小值。若某個揚音器只要音量最大,不論其他條件是否正常或是極端,聲音都會扭曲,這是邊緣案例。

如何解決這些問題?

大規模同步文件很難。在 2016 年,我們已經很好地解決了這一問題。我們有數以億計的用戶,像 Smart Sync 這樣的新產品特性正在開發中,還有一支強大的同步專家團隊。Sync Engine Classic 經過多年生產強化,花了很多時間來尋找並修復最罕見的錯誤。

Joel Spolsky 稱從頭開始重寫代碼是 “任何軟件公司都可能會犯的最嚴重的戰略錯誤” 。要想成功地完成重寫通常需要減緩特性開發速度,因爲在舊系統上取得的進展需要移植到新系統上。當然,還有很多面向用戶的項目,我們的同步工程師可以進行。

儘管 Sync Engine Classic 取得了成功,但卻非常不健康。在構建 Smart Sync 的過程中,我們對系統做了許多漸進式改進,清理了低劣代碼,並重構了接口,甚至還添加了 Python 類型註釋。我們添加了大量遙測技術,並建立了流程,以確保維護既安全又簡單。但是,這些漸進的改進還是遠遠不夠。

交付任何更改以同步行爲都需要進行艱苦的部署,而且我們仍然會在生產中發現複雜的不一致性。團隊必須放下一切,診斷問題,解決問題,然後花時間讓他們的應用程序恢復到良好的狀態。儘管我們有一支強大的專家團隊,但要讓新工程師融入這個系統還是需要花費數年時間。最後,我們投入了大量時間來提高性能,但未能明顯擴展同步引擎可以管理的文件總數。

這些問題有幾個根本原因,但最重要的是 Sync Engine Classic 的數據模型。數據模型是爲一個沒有共享的、更簡單的世界而設計的,並且文件缺少穩定的標識符,這個標識符可以在移動的過程中保持。我們會花費數個小時來調試理論上可能但 “極不可能” 出現在生產環境中的問題。改變一個系統的基本名詞通常不可能一蹴而就,很快我們就沒有有效的漸進式改進的方法了。

其次,該系統並不是爲可測試性而設計的。我們依賴於緩慢的發佈和現場調試問題,而不是自動化的預發佈測試。Sync Engine Classic 容許數據模型意味着我們不能在壓力測試中進行太多檢查,因爲有大量不受歡迎但仍然合法的結果而我們無法斷言。擁有一個具有約束不變量(tight invariant)的強大數據模型對於測試非常有價值,因爲檢查系統是否處於有效狀態總是一件很容易做到的事情。

我們在前文討論了爲什麼同步是一個併發問題,並且測試和調試併發代碼出了名的難。Sync Engine Classic 的基於線程的架構根本一點用都沒有。它將所有調度決策都交給了操作系統,使得集成測試變得不可重現。在實踐中,我們最終使用的是長時間持有的非常粗粒度的鎖。雖然這種架構犧牲了並行性的優點,但使系統變得更易於推理。

重寫前,需要評估什麼?

讓我們將決定重寫的原因提煉成一份關於重寫的清單,它可以幫助在其他系統中進行此類決策。

你是否已經用盡漸進式改進方法?1、你是否嘗試過將代碼重構爲更好的模塊?

代碼質量低劣本身並非重寫系統的重要原因。重命名變量和解開糾纏在一起的模塊都可以用漸進式改進來完成,我們在 Sync Engine Classic 中花了很多時間來完成這些工作。Python 的動態性可能會讓這一點變得困難,因此,我們添加了 MyPy 註釋,以便在編譯時逐漸捕獲更多的錯誤。但是,系統的核心原語仍然保持不變,因爲僅靠重構並不能改變基本數據模型。

2、你是否嘗試通過優化來提高性能?

軟件通常把大部分時間花在很少的代碼上。許多性能問題都不是根本問題,優化分析器識別的熱點是一種漸進式改進性能的好方法。幾個月以來,團隊一直致力於性能和規模方面的工作,他們在提高文件內容傳輸性能方面取得了巨大成果。但是,對內存佔用的改進(比如增加系統可以管理的文件數量)仍然難以實現。

3、你能提供更多價值嗎?

即使你決定重寫,你能通過提高價值來降低風險嗎?這樣做可以驗證早期的技術決策,幫助項目保持發展勢頭,並減輕緩慢的特性開發帶來的痛苦。

你能進行重寫嗎?

1、你是否深刻理解並尊重當前的系統?

編寫新代碼比完全理解現有代碼要容易得多。因此,在重寫之前,你必須深刻理解並尊重 “經典” 系統。這就是你的團隊和業務存在的全部原因,它通過在生產環境中運行積累了多年的智慧。去做一番考古研究,探究爲什麼這一切都是這樣的。

2、你有時間嗎?

從頭開始重寫系統是一項艱苦的工作,而且要實現完整的特性需要花費大量的時間。你有這些資源嗎?你的組織是否足夠健康,能夠維持如此大規模的項目?

3、你能接受較慢的特性開發速度嗎?

我們並沒有完全停止 Sync Engine Classic 的特性開發,但是舊系統的每一次改變都會將新系統的終點線推得更遠。我們決定交付一些項目,在不拖累重寫團隊的情況下,我們必須有意識地分配資源來指導這些項目的發佈。我們還對 Sync Engine Classic 遙測技術進行了大量投資,以將其穩態維護成本維持在最低水平。

你的目標是什麼?

1、爲什麼第二次會更好?

如果你已經走到這一步,那麼你已經徹底地理解了舊系統,以及需要吸取的教訓。但是,重寫也應該受到不斷變化的要求或業務需求的推動。我們在上文中闡述了文件同步是如何變化的,但是,我們重寫的決定也是具有前瞻性的。Dropbox 瞭解協作用戶在工作中日益增長的需求,爲這些用戶構建新特性需要一個靈活、健壯的同步引擎。

2、你對新系統的原則是什麼?

對一個團隊來說,從頭開始是一個重塑技術文化的絕佳機會。鑑於我們操作 Sync Engine Classic 的經驗,我們從一開始就非常強調測試、正確性和可調試性,將所有這些原則編碼到數據模型中。我們在項目生命週期的早期就寫出了這些原則,它們爲自己帶來了一次又一次的回報。

我們用 Rust 重寫核心代碼

最終,我們用 Rust 編寫了 Nucleus。對我們團隊來說,押注 Rust 是我們做出的最好決定之一。除了性能之外,它對正確性的關注幫助我們克服了同步的複雜性。我們可以在類型系統中對系統的複雜不變量進行編碼,並讓編譯器爲我們檢查它們。

我們幾乎所有的代碼都在一個線程( “控制線程” )上運行,並使用 Rust 的 futures 庫在這個線程上調度許多併發操作。我們只在需要的時候纔將工作轉移給其他線程:網絡 IO 到事件循環,計算開銷大的工作,如哈希到線程池,文件系統 IO 到專用線程。這大大降低了開發人員在添加新特性時必須考慮的範圍和複雜性。

當控制線程的輸入和調度決策是固定的時,它被設計爲完全確定的。我們使用這一性質,用僞隨機模擬測試對其進行模糊處理。利用隨機數生成器的種子,我們可以生成隨機的初始文件系統狀態、時間表和系統擾動,並讓引擎運行到完成狀態。然後,如果我們沒有通過任何同步正確性檢查,我們總是可以從原始種子中重現錯誤。我們每天在測試基礎設施中運行數以百萬計的各種場景。

我們重新設計了客戶端 - 服務器協議,使其具有很強的一致性。該協議確保服務器和客戶端在考慮更改之前具有相同的遠程文件視圖。共享文件夾和文件具有全局唯一的標識符,客戶端永遠不會在臨時複製或丟失狀態下觀察到它們。現在,我們在客戶端和服務器的遠程文件系統視圖之間進行了強有力的一致性檢查,任何差異都是錯誤。

參考鏈接:

https://dropbox.tech/infrastructure/rewriting-the-heart-of-our-sync-engine

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章