自 2013 年推出以來,資料量每年翻一番,截至 2021 年 3 月,Gitee 上有 600 萬+開發者和超過 1500 萬個倉庫,使其成為中國領先的研發協作平台。 在資料不斷增加的過程中,Gitee 的架構也經歷了多次迭代,以支援當前的資料量。 我在很多會議上分享過 Gitee 的架構,也和很多有類似場景的同學討論過,偶爾也會被問到有沒有專門的文章介紹 Gitee 架構,所以很少有時間把這個話題整理成一篇文章給大家參考。
Gitee作為國內發展最快的託管平台,每天都在快速成長,隨著DevOps概念的普及,持續建設也給平台帶來了更多的請求和更大的併發性,每天需要處理數千萬次Git操作
單機架構、分布式儲存架構、NFS架構、自研分片架構、RIME讀寫分離架構,再分享整個Gitee架構的演進歷史。
Gitee 於 2013 年 5 月推出,是乙個單體 Rails 應用程式,所有請求都通過它載入。
除了在單機上部署 MySQL 和 Redis,與大多數 Web 應用不同,Gitee 需要儲存大量的 Git 倉庫,無論是 Web 讀寫倉庫還是通過 Git 的倉庫操作,應用都需要直接操作伺服器上的裸倉庫。 這種單體架構在訪問次數不大的時候是可以的,比如用於團隊或者企業內部使用,但如果作為公有雲的SaaS服務提供,隨著訪問次數和使用量的增加,壓力會越來越明顯,主要有以下兩種:
儲存空間的壓力和計算資源的壓力 由於開源中文社群的影響,Gitee 在上線之初就已經淹沒了大部分使用者,種子使用者無需擔心。 相反,隨著社群使用者使用量的增加,首先遇到的問題就是儲存的壓力,因為當時使用的是阿里雲的雲主機,最大的磁碟只能選擇2T,雖然已經通過一些渠道實現了擴容,但雲主機背後的物理機只是一台1U的機器, 而且最多只能有4塊硬碟,所以當儲存達到接近8T時,除了外接儲存裝置,沒有更好的直接擴容方式了。
而且隨著使用量的增加,每個工作日的高峰期,比如上午9點左右和下午5點左右,就是推拉的高峰期,機器的IO幾乎滿載,所以這個時候整個系統會很慢,所以系統擴容很緊急。 經過討論,團隊決定使用分布式儲存系統 Ceph,經過一系列不是很嚴格的驗證(這是後來問題的根本原因),我們購買了機器,開始對系統進行擴容。
Ceph 是乙個分布式檔案系統,它的主要目標是設計乙個基於 Posix 的分布式檔案系統,沒有單點故障,可以輕鬆擴充套件到幾 PB 的容量,所以當時的想法是利用 Ceph 的水平擴充套件能力和高可靠性來實現儲存系統的擴充套件, 並在儲存系統的上層提供多組無狀態應用,使這些應用可以共享 Ceph 儲存,從而進一步實現計算資源擴充套件的目的。
因此,在 2014 年 7 月,我們購買了一批機器,開始構建和驗證系統,然後選擇乙個週末開始系統的遷移和上線。 遷移完成後,功能驗證正常,但平日裡,隨著接入的增加,一切都開始朝著不好的方向發展,整個系統開始變得非常緩慢。 我以為一切都很穩定,但更可怕的事情發生了,Ceph RBD 裝置突然被解除安裝,倉庫資料全部消失,整個團隊和社群瞬間爆炸,經過 14 個小時的分析和研究,裝置終於被重新掛載,然後資料全速移動到 iSCSI 儲存裝置中, 風波逐漸平息。
大量小檔案的讀寫效能瓶頸,RBD塊裝置解除安裝
後來經過研究發現,分布式儲存系統並不適合 git 等大量小檔案的場景,因為 git 的每次操作都需要遍歷大量的引用和物件,導致每次操作的整體耗時。 當塊裝置被解除安裝時,我們花了長達 14 個小時來恢復它,這是在沒有徹底了解的情況下使用該工具的結果。 經過這堂血淚的教訓,我們更加謹慎,更加謹慎地進行後續的一切調整。
但是,儲存壓力和計算壓力仍然存在,它們是亟待解決的問題所以為了暫時解決這個問題,我們採用了乙個比較原始的解決方案,這個方案是 GitLab 在 2014 年正式提供的。
該方案主要利用NFS共享磁碟,向上游構建多個應用例項來擴充套件計算資源,但由於儲存遍布網路,必然會帶來效能損失,在實際應用過程中,由於git操作場景的複雜性,會帶來一系列問題。
內網頻寬瓶頸,NFS效能問題導致雪崩效應,NFS緩衝檔案導致刪除不完整,橫向擴充套件儲存不方便,無需維護內網頻寬瓶頸
因為儲存是通過NFS掛載的,如果有比較大的倉庫比如1G以上,在執行轉殖的時候會消耗大量的內網頻寬,而且一般情況下我們伺服器的網口是1Gbps,所以很容易把網絡卡填滿,導致其他倉庫的執行速度變慢, 這反過來又導致大量請求阻塞。這還不是最嚴重的,最嚴重的情況是內部服務網口被占用,導致MySQL、Redis等服務丟包嚴重,整個系統會很慢,這種情況的解決辦法是通過其他網口呼叫核心服務來解決,但是NFS網口的問題還是解決不了。
NFS 效能問題導致雪崩效應這就比較容易理解了,如果一台NFS儲存機的IO效能太慢,並且所有應用機都有來自這台儲存機的讀寫請求,那麼整個系統就會出現問題,所以這種架構下的系統非常脆弱,經不起考驗。
NFS 緩衝區檔案導致刪除不完整這個問題很頭疼,因為為了提高檔案的讀寫效能,啟用了NFS記憶體快取,所以有些機器刪除了NFS儲存上的一些檔案,但是它們仍然存在於其他機器的記憶體中,導致應用程式出現一些邏輯問題。
例如,git 是在推送過程中生成的.lock
檔案,為了防止在分支推送過程中其他客戶端同時推送導致的問題,那麼如果我們去master
當分支推送**時,伺服器會生成它master.lock
檔案,這樣其他客戶端就無法同時通訊master
推在樹枝上。 推送完成後,會自動清除 gitmaster.lock
檔案,但由於我們上面提到的原因,在某些情況下,我們在處理應用程式上的推送請求後刪除了它master.lock
檔案,但它仍然存在於另一台應用機器的記憶體中,這將導致其無法推送。 解決這個問題的辦法是關閉NFS記憶體級快取,但是效能會受到影響,而且真的很難選擇,但好在這個問題很少見,所以為了效能,我不得不忍受。
維護不善俗話說,由於歷史原因,應用的儲存目錄結構是固定的,所以我們要通過軟連線來擴容整個目錄,而擴容的前提是將NFS儲存裝置掛載到目錄中,所以當時每台應用機在整個系統中的掛載情況非常複雜。
git@ gitee-app1:~$df -h
哇,看到這樣的目錄結構,運維都快哭了,維護起來極難,再這樣下去,失控也只是時間問題。
NFS 可以暫時抵制這種方法,但它不是乙個長期的解決方案,因此您必須尋找更改並改進體系結構。 理想的方式當然是 GitHub 的分片架構,它通過 RPC 將應用程式和儲存庫呼叫分開,以便於擴充套件和維護。
但是,這種轉型需要對應用進行改造,成本高,周期長,而且考慮到當時的情況,在架構上投入的研發資源基本不多,那麼我們該怎麼辦呢?當時我們在討論這個架構的時候,我們的一位前端同事(昵稱:乙隻大熊貓)提出了乙個想法,既然應用不能分離,為什麼不在網路的第一層做分片路由呢?
題外話:在團隊內部提問是非常必要的,可以激發團隊討論的氛圍,我們可以做一些有價值的事情,所以每個團隊成員,尤其是作為開發人員,永遠不要害怕說你的小想法可以對團隊產生非常長遠的影響。 比如熊貓先生的話,直接決定了後續gitee架構的發展方向,希望有時間的時候再一起吃竹子;d
於是,第乙個版本的架構應運而生,我們不改變應用原有的結構,而是允許應用是有狀態的,即應用和倉庫**,一組應用對應一批倉庫,只要能在請求上識別出來並分發給相應的應用進行處理即可。
從業務角度來看,Gitee 上有 3 種型別的請求:
http(s) 請求,瀏覽倉庫和 git 的 http(s) 模式來操作 SSH 請求,git 在 ssh 模式下操作 svn 請求,gitee 功能,使用 svn 來操作 git 倉庫,所以我們只需要對這三類請求進行分片路由,從請求中擷取倉庫資訊,根據倉庫資訊找到對應的機器, ,然後提出請求。在此基礎上,我們開發了 3 個元件,用於路由這三個請求中的每乙個。
Miracle HTTP(S) 動態分發**該元件基於 nginx 進行二次開發,主要功能是通過擷取 URL 來獲取倉庫的命名空間,然後根據這個命名空間進行代理。 例如,在上圖中,我們請求這個倉庫,Miracle 還是通過 URL 來了解該請求是否為請求
zoker
所以 Miracle 會先去路由 redis 查詢user.zoker
如果不存在,則會去資料庫查詢,快取在路由Redis中,提高獲取路由IP位址的速度。 獲取 IP 後,Miracle 會將請求動態應用到代理對應的後端 app1 中,以便使用者正確看到倉庫的內容。
對於路線的分布,必須保證準確,如果user.zoker
如果你得到乙個錯誤的IP,那麼使用者就會看到乙個空的倉庫,這不是我們所期望的。 此外,非倉庫請求,即與倉庫資源無關的請求,如登入、動態等,會隨機分發到任何後端機器上,因為它們與倉庫無關,所以任何後端機器都可以處理它們。
SSH 和 SVN 動態分發**sshd 元件主要用於將 ssh 請求分發給 git,libssh 用於輔助使用SVNSBZ 是 SVN 請求的動態分布。 這兩種實現的邏輯與 Miracle 類似,這裡就不贅述了。
遺留問題該架構上線後,無論是架構負載還是運維成本,都有了很大的提公升。 但架構的演進總是無止境的,沒有靈丹妙藥的當前架構還存在一些問題
GiteeWeb 通過 https 處理大型 git 請求,每個使用者有較大的分片,通過 SSH 和 SVN 操作影響 Git 的 API 仍由 GiteeWeb 處理 處理並不能解決單個倉庫負載過大的問題因為是以使用者或組織為原子單元進行分片,如果乙個使用者下的倉庫太多,體積太大, 機器可能無法處理它。而如果單個倉庫的訪問量過多,比如一些流行的開源專案,在極端情況下,一台機器可能無法承受這些請求,這仍然是有缺陷的。
另外,git 請求涉及到認證,所有的認證還是通過 giteeweb 介面,而 git 的 https 操作還是由 giteeweb 處理,沒有像 ssh 這樣單獨的元件來處理,所以耦合還是太強了。
基於以上一些問題,我們進一步完善了架構,主要做了以下幾點改動:
通過 https 服務分片 Git,以儲存庫為原子單元 SSH 和 SVN 相關操作的 API 分離 儲存庫分片使路由的原子單元更小,更易於管理和擴充套件儲存庫位址
對於鍵,類似於zoker/taskover
這種金鑰是路由的。
分離 git 的 http(s) 操作的主要目的是防止其影響對 Web 的訪問,因為 git 的操作非常耗時,場景不同,放在一起很容易產生影響。 認證相關 API 的獨立性也是為了減輕 GiteeWeb 的壓力,因為有很多推拉操作,所以 API 訪問量也會非常大,很容易和普通使用者的 Web 請求混在一起。
經過這些分離後,GiteeWeb 的穩定性有了很大的提高,由於 API 和 Git 操作導致的不穩定性減少了約 95%。 整個體系結構元件的組成看起來與此類似。
遺留問題雖然系統的整體穩定性有所提高,但我們仍然需要考慮一些極端的情況,比如單個倉庫太大怎麼辦單個倉庫訪問次數過大怎麼辦?幸運的是,該系統可以限制單個倉庫的容量,但如果是乙個非常熱的倉庫呢?如果您有這種突然的大併發訪問怎麼辦?
作為國內最大的研發協作平台和首屈一指的託管平台,Gitee在Gitee上為許多開源專案建立了生態,包括非常熱門的倉庫,也是高校、培訓機構、黑客馬拉松等場景託管平台的首選,經常會遇到大併發訪問。 但是,當前架構的主要問題是機器的備份是冷備用的,無法有效利用,單個倉庫需求負載過大的問題沒有得到解決。
為什麼要採用RIME架構?自從華為加入Gitee以來,我們開始真正關注這個問題。 為了迎接 2020 年 9 月享譽全球的 HarmonyOS 作業系統開源,我們在 2020 年上半年持續優化架構,使多台機器能夠載入同一倉庫的 IO 操作,這就是我們目前的 RIME 讀寫分離架構。
運作方式為了達到機器多次讀取的效果,需要考慮倉庫同步一致性的問題。 試想一下,如果乙個請求被分發到一台備用機上,而主機剛剛被推送完畢,那麼使用者在網頁上看到的倉庫就會在推送之前,這是乙個非常嚴重的問題,那麼如何保證使用者訪問的備用機也是最新的或者如何保證同步的及時性?在這裡,我們使用以下邏輯來確保這一點。
寫入主機,主機發起與備用伺服器的同步,以保持同步狀態,並根據同步狀態確定路由分布。
如上圖所示,我們將倉庫的操作分為讀和寫,一般來說,讀取可以平均分配給每個備用,這樣如果我們有一台主機和兩台備用,那麼在不考慮其他因素的情況下,倉庫的讀取容量理論上增加了 3 倍。 但考慮到倉庫會被寫入,會涉及到備用的同步,正如我們剛才說的,如果同步不及時,就會導致訪問舊的**,這顯然是乙個巨大的缺陷。
為了解決這個問題,我們在倉庫寫完後,使用 git hook 來觸發乙個同步佇列,這個佇列的主要任務如下:
將倉庫同步到備用機器,以驗證同步倉庫的一致性管理並更改同步狀態。
當倉庫推送時,git hook 會觸發乙個同步任務,會主動將增量同步到配置的備用資料庫,同步完成後,會進行引用的一致性檢查,使用這個一致性檢查blake3
雜湊演算法通過refs/
以確認同步儲存庫的版本完全相同。
對於狀態管理,當任務被觸發時,兩台備用機器的倉庫狀態會設定為不同步,我們的分發元件只會將讀取操作分發給設定為同步狀態的主機或備用機器,當同步完成並且一致性檢查完成時, 相關備用機的同步狀態將設定為已同步,讀取操作將再次分發到備用機。但是,如果同步失敗,例如上圖中同步到app1baka成功,那麼讀操作可以正常分發到備用機上,但是app1bakb失敗後,讀操作就不會分發到非同步的機器上,這樣可以避免訪問不一致的問題。
架構成果通過對架構讀寫分離的改造,系統可以輕鬆應對單個倉庫訪問量過大的情況。 2020 年 9 月 10 日,華為 HarmonyOS 在 Gitee 上正式開源,這個備受矚目的專案一開通就為 Gitee 帶來了巨大的流量和大量的倉庫操作。
後續優化一些細心的學生可能會認為,如果乙個儲存庫以不同的方式編寫並伴隨著大量的訪問,那麼它將成為處理所有這些請求的單一機器答案是肯定的,但是這種情況在正常情況下就不是了,正常情況下,寫操作的頻率要比讀操作低很多,如果真的發生這種情況,只能說明它遭到了攻擊,那麼我們也對元件進行了單個倉庫的最大併發限制, 這也是我們維護 Gitee 以來得到的合理限制,完全不會影響普通使用者的使用。
但是架構的優化是無止境的,對於上面提到的情況,我們還是有待改進的,主要的做法主要是在提交時同步更新,備用同步成功或者部分備用同步成功,這種方式的缺點是會延長使用者推送的時間, 但它可以解決主機單次讀取的問題。目前的架構是多讀單寫,如果後乙個領域存在一些頻繁寫入的場景,可以考慮改為多讀多寫來維護狀態和衝突。
當前架構最大的問題是應用和倉庫的操作沒有分離,這對架構的可擴充套件性極為不利,所以我們現在或者將來要做的就是解耦服務,優化其他方面:
倉庫的操作是分離的,應用的前端和後端分離以RPC的形式呼叫,佇列、通知等分離的熱點倉庫按需自動擴容,根據機器的指標分配新的倉庫。 自 2013 年 Gitee 上線以來,直到 2017 年才推出自研架構,真正解決了內外部問題,內部因為架構無法支撐訪問次數帶來的各種不穩定,以及外部的 DDoS、CC 攻擊等。
有句老話說,萬場景都說技術是流氓,架構也是一樣,斷章取義談架構是沒有意義的,很多時候我們做了很多工作,說不定現在或者未來幾年就能解決, 但我們需要用長遠的眼光來衡量後續產品的發展、資料的增長、功能的增強,以便更好地改變架構以適應這個快速發展的領域,進而更好地服務於企業,賦能開發者。