CODING 代碼託管架構升級之路
本文為 CODING 創始團隊成員王振威在『CODING 技術小館:上海站』的演講實錄。
CODING 技術小館,是由國內專業的一站式軟體服務平台 CODING 主辦的一系列技術沙龍。將邀請數位業內知名大牛分享技術,交流經驗。同時也將邀請以為當地用戶進行技術分享,為開發者們帶來一場純粹的技術沙龍。
大家好,我叫王振威。我是 CODING 的初始創始團隊成員之一。CODING 從 14 年創業到現在,主要做的是代碼託管,我主要負責架構和運維方面的工作。今天給大家帶來一個技術分享是關於我們代碼託管的整個架構是如何從最開始殘破不堪的產品,一步一步升級到現在的產品的。這是今天的主題『CODING 架構升級的演變過程』。
說起架構的話,稍微有點寫程序經驗的人來說,都可以理解架構對於整個服務的重要性。架構最核心的三個點就是:穩定性、擴展性、性能。一個好的架構主要通過這三點來看。
會不會宕機,你的服務會不會因為自身或者第三方的原因突然之間中斷。可拓展性,當你的訪問量增長的時候,你的服務能不能迅速的 Copy 出很多個副本出來以適應快速增長的業務。再一個就是比如說你要做電商啊秒殺啊之類的功能的時候,能不能扛得住這種壓力。這就是評價一個架構好壞的三個基本點。
我們可以想想一下,一個架構比較亂是什麼樣子。就好像一個機房管理員面前所有的線亂成一團。
一個好的架構是什麼樣呢?這是Google 數據中心機房的排布,我們可以看到這是非常整齊的,看起來是賞心悅目的。
我們的軟體架構和硬體架構甚至跟建築學等都是有共通之處的。一個好的架構一定是邏輯清晰,條理明確的。
為什麼要講代碼託管的架構呢。這要從 Coding 的最開始說起,Coding 做了一個代碼託管,做了個代碼的質量分析,一個演示平台,還有一個在線的協作工具。代碼託管是我們非常重要的一個業務,在整個 Coding 的架構演變升級的過程中,包括對各種新技術的嘗試中,代碼託管一直走在最前列的,包括Docker 的嘗試等等。所以我今天和大家分享一下代碼託管架構的演變升級,我認為這個是整個 Coding 架構系列的一個典型。
我們從最開始開始講,整個升級過程分為四大階段。
第一階段:遠古時代
我們三月份開始寫第一行代碼,七月份把產品上線,所以當時非常倉促,但這也是很多創業公司不可避免的一個現狀,就是你必須要快速上線,產品可以不夠完美,但必須要快速切入市場。所以原則是先解決有沒有的問題,再解決好不好的問題。我們當時以這樣的思路來做這個工作。我們也參考了很多開源軟體,比如我們的架構就是仿照 Gitlab。那時候說白了就是租了一台伺服器,然後所有的服務跑在上面,用戶所有的請求都通過這台伺服器進行交互,我們來看一下當時的架構圖。
用戶從最左邊,通過我們在 Ucloud 的 ULB 進來,到我們自己的應用伺服器,應用伺服器和資料庫有些交互,和緩存等其他組件有些交互,和 Git 倉庫有些交互。黑框框起來的部分就是我們的應用伺服器和 Git 倉庫,它們當時是部署在同一台伺服器上的,也就是說應用程序,代碼都是部署在一起的。所以顯而易見,這個架構存在很多問題,如果 Ucloud 這台伺服器出了問題,我們的網站就掛了。雖然是虛擬機,但是出問題是難保的,這就是一個很不好的架構。會不會宕機?會。能不能擴展?不能。能不能抗壓?不能。所有東西都在一起。就像養寵物一樣,把所有服務都放在你的寵物身上保護好,但一旦你的寵物出現問題整個服務就都癱瘓了。
後來我們上線之後就慢慢在做一些改進,我們發現我們遇到最嚴重的問題,就是代碼託管和項目協作組合到一起了,但很多時候業務是相對獨立的,那我們就把他拆開吧。另外一個是代碼倉庫數據量越來越大,單台伺服器已經很難支撐了。我們的代碼倉庫必須具備負載均衡的策略和重新排布的這種能力。
第二階段:農耕時代
我們的架構是這個樣子的:
用戶還是通過 ULB 進來,但是我們的代碼倉庫已經分離到單獨的機器上,我們每一個機器上都會裝備一個 RepoManager,用來專門管理這台伺服器上的所有代碼倉庫。它和我們的主站服務是通過 RPC 這種方式來通訊的。我們的 Git 倉庫,也有了獨立的備份,不需要和主站一起備份。這是第二階段做的主要改動。我們的 Git 有多個伺服器,不只圖上所示三個。每一台都有一個 Repomanager 管理器,再通過 RPC 做交互。大概是這樣的一個結構。
然後我們就發現了更多問題,需要作出更多改變以承受更多的用戶量。
第三階段:手工業時代
代碼倉庫的服務分為兩部分,一個是用戶可以在本地使用 Git 工具來 push/pull 代碼。另外一個是,可以在網頁上直接看代碼、編輯代碼、查看提交歷史等。這兩部分操作在底層實現上來說是比較獨立的,所以我們選擇把他們進行更徹底的拆分。還有就是專門的認證服務,我和大家解釋一下,我們每次從網站上 clone 代碼或者 push 代碼的時候,我們必須要認證一下用戶的身份。而這個認證服務是決定了我們服務是否穩定的一個重要組件。現在我們就把他單獨的拆離了出來,就是說我們有一套專門的認證服務來處理這個事情。達成我們希望的各個環節可以實現規模化增長。具體我們來看一下架構圖。
這個圖就稍微有點不同了,用戶訪問我們 Git 的服務分兩條線,一部分是黑線標註出來的,另一部分是紅線標註出來的。紅線標註出來的實際上是使用 IDE 的插件、命令行工具來訪問我們的代碼倉庫,通過 SHH 協議、或者 Git 協議、HTTP 協議到 ULB 之後,會到我們的 Git 的 Server,Server 會交由我們的認證服務去做一個用戶許可權的認證。這個認證服務是獨立的,比如去資料庫中校驗密碼、校驗許可權,回來之後 Git Server 會在內網發一個 SSH 請求到我們的具體的代碼倉庫的存儲機器上,最終完成代碼的交互。另外一條線,黑線表示的是我們在網頁上操作的時候,比如查看代碼的文件數,編輯代碼等等,這條線上的請求全部都是 HTTP 請求。所以用戶到 ULB 之後,就直接代理到 Web Server,和階段二一樣,通過發送 RPC 請求,到具體的 Git 倉庫的存儲的 RepoManager 上面從而產生數據的交互。這是我們第三階段做的主要改進。
然而隨著時間的增長我們又發現了更多的問題。一個是我們把代碼倉庫按分區給分離開來,但會發現代碼倉庫的活躍是不均勻的。如果一台機器剛好這段時間的訪問量非常大,那這台機器的壓力就很大,尤其是計算方面的壓力,其他的伺服器又可能幾乎處於閑置狀態。存儲方面的話我們一般會做 500G-1T 的存儲,但是 CPU 我們一般不會配置太高,因為大多數都屬於冷數據的。這時候我們就需要一個彈性的計算池。計算和存儲是分離的,就是我們的存儲可以任意搭配計算池來進行計算。另外一個就是自動化監控,我們的服務從單台機擴展到很多台機,還有分區。組件也越來越多,我們有很多獨立的服務,比如有獨立的發郵件啊,有獨立的 markdown 編譯器啊,還有 qc 的服務,還有 CodeInsight 的服務、WebIDE 等等等。服務一多,運維的壓力就會成倍增長。這個時候我們需要自動化監控來幫我們解決一些問題。
第四階段:工業時代
用戶通過 Web、HTTP 或者 SSH Git 協議鏈接到我們的 ULB 之後,內網做轉發,網頁的訪問這邊我就沒畫,到 Web Server,還是通過 RPC 請求到 Repomanager。不一樣的是紅線區域。用戶到 GitServer 之後,先認證之後會連到伺服器池,下面也是一個伺服器池,組成一個計算池,主要是 CPU 和內存的配備,並沒有什麼磁碟這種配置。下面是存儲池,存儲池通過網路文件系統掛載到計算池上,所以現在就形成了這樣的結構。存儲由存儲的負載均衡策略來決定,但是計算池由計算的負載均衡決定。這樣壓力大時的請求並不會同時發在同一台機器上,就能解決我們之前說的不均勻問題。這個結構里還有很多細節我們接下來探討。其中一個細節是,我們所有的 Git 服務都用 CDN 將用戶連接起來。哪怕是中國最好的帶寬資源,都比不上用戶訪問的服務節點在他所在城市的骨幹節點,這時候我們就找了 CDN 的供應商,CDN 的地域性骨幹網路節點把我們的請求轉發到 UCloud 源伺服器上,雖然這樣成本很高,我們付出了兩倍帶寬的價格,但是最終的使用效果還是不錯的,很多用戶反映速度和穩定性有明顯提升。另外我們計劃推廣 CDN 到全站,所有的服務。使用 CDN 另外一個好處是可以防 DDOS 攻擊,我們同行包括我們自己經常遭受這樣的困擾。DDOS 的原理是針對一個 IP 地址,肉雞不斷往這個 IP 地址發送垃圾包,從而導致帶寬被佔滿。使用 CDN 之後,我們給用戶報 IP 地址都是全國性的,有幾百個 IP 地址,DDOS 往往都是針對單個 IP 地址來攻擊的。當我們的節點收到攻擊的時候,供應商可以立馬將節點替換掉,從而導致大範圍問題。所以用 CDN某種程度上來說可以避免 DDOS 攻擊。大家都知道 Git 服務關係著公司的線上部署,對穩定性要求非常高。所以我們還是願意花很大的成本來做這個。看一下,CDN 大概是這樣一個結構。
第二個細節是 LB,LB 就是負載均衡,我們現在的伺服器中大量的部署了這種形式的服務。LB 把無狀態的服務介面實現了統一。什麼叫無狀態服務,就是服務不會在內存中做一些狀態的存儲,比如說緩存,無狀態服務的請求應該是和前後文無關的,下一個請求不會受前面的請求影響導致數據改變。其優點是可以部署很多實例,這些實例沒有任何差別。針對這些服務,我們通過LB 把這些服務統一了起來。內部服務的相互依賴都通過 LB 完成。
然後是一個監控系統,我們用了 Google 一個團隊開源出來的叫 Prometheus 的一個監控器。據我們的 CTO 孫宇聰說這個監控器比較像谷歌內部的監控系統。目前我們使用的感覺還是很不錯的。
為什麼要做這個,大家都知道木桶原理,很多服務相互依賴的時候,必須所有的服務都可靠,你的服務最終才是可靠的。LB 系統的目的在於:當某個實例出現問題的時候,自動剔除掉,就是做監控級別的自動運維。
LB 和監控系統配合的工作流程是這個樣子的。
這張圖展示了我們內部系統的服務是如何相互依賴的。這裡有幾個角色,一個是 LB,一個是監控系統,一個是運維人員,就是左上角這個 ops。ops 操作線上的服務的時候,我么是直接操作這個 LB 配置。舉個例子,我們有個 markdown 的編譯器,以服務的形式存在,給我們服務中的其他服務提供 markdown 編譯的底層的支持。假如有一天你發現 markdown 編譯器的承載量已經不夠了,必須新加一個實例。這時候上線之後,往往要做一大堆配置才能生效,但是我們操作 LB 讓這個實例掛載在 LB上,LB 可是實現動態 reload ,所以可以實現快速上下線。LB 的工作是這樣的,我們的 LB 是用配置文件來描述的,ops 操作完 LB,Confd 會生成最終的配置文件。把這個配置文件發送到 Etcd 集群,Etcd 就是一個配置中心,有很多配置項在裡面。發到 Etcd 之後,會有另外一個程序就是 Confd,他會一直監控在這個 ETCD 的狀態,當 LB 狀態發生變化的時候,它就把這個變化過狀態的配置拿下來,生成最終的 LB,生成最終配置文件,reload 後服務就上線了。還有一個是監控系統的角色,我們的監控器是用 Prometheus 搭建的,監控系統有一個配置項,是用來配置服務監控的數據的介面,我們每一個服務都會起一個 HTTP 埠,提供基礎的「關鍵指標」。「關鍵指標」能顯示你的服務是否健康,壓力有多少。還是舉 markdown 編譯器的例子,他的「關鍵指標」是每秒的處理量,一個 markdown 文本編譯完的時間,就是一些自己健康狀態的指標。每個服務必須統計好自己的關鍵指標,再把這些信息以 HTTP Metric 的形式暴露出來,我們的監控系統每隔一段時間去抓取一下這個數據,如果抓取不到或者抓取的關鍵指標出現了異常,根據配置的警報策略和自動處理策略開始行動,比如他認定某個實例 down 的時候,他就去通知 Etcd 某個實例 down 了, Confd 偵測到後 ,LB 就把這個實例下線。另外,當某個實例出現問題的時候,監控會通過 WebHook 的形式,去通知我們的通知中心,把這個實例有問題的信息發給我們的運維人員,比如發簡訊,發 App push、發郵件等讓運維人員進行下一步處理。這是工業時代一個幾乎半自動化的架構。無人值守的時候這套流程也基本可以正常運轉。我們從系統上可以允許 Ucloud 內網出現波動,因為不論是任何情況,只要是「關鍵指標」有變化,我們的報警和自動處理策略就會生效。
工業時代,就是自動化生產的時代,另外要講的一點,就是容器化。其實我們在第三階段就開始嘗試容器化了。到目前我們 95% 以上的服務都是用 Docker 來提供的。我來介紹一下我們目前是如何使用 Docker 的。一個是我們自己搭建了 Docker Rigestry,目前發了第二版,叫做 distribution ,考慮過遷移到新版但居然不兼容,遷移工具也不夠好用,第一版又沒有明顯的問題,所以我們一直沿用到現在。
此外,我們線上的代碼,都是編譯在Docker 中,運行在 Docker 中,為什麼要這麼做呢?編譯在 Docker 中有一個明顯的好處,比如我們在本地開發的時候,我們是用 JDK8,那我們線上不可能用 JDK7 去編譯這個版本,如果更嚴格我們可能要求小的版本號也一致。想保證版本使用嚴謹,用容器是一個非常方便的選擇。如果在伺服器上 linux 操作系統上裝這個 JDK,可能過兩天就要升級一下,非常麻煩,但是如果在 Docker 中使用,很容易指定版本。另外我們在 Docker 中編譯程序,在 Docker 中運行程序。
然後是 Docker Daemon 的管理,每一台伺服器上都裝一台 Docker 的守護進程,它來管理上面的 Container,我們寫了一套工具,原理就是給 Docker 發請求,告訴他應該起哪個 Container,應該停哪個 Container。
這個圖大概展示了一下我們是如何用 Docker 的。
運維在操作的時候,有兩個介面,有一個 UI Dashboard,我們可以在 Dashboard 上去控制某一個實例,也就是某一個容器,還可以通過命令行的形式來處理,最終形成 Docker 運行的指令,我們的程序由此進行管理和運行,比如我們發一個請求說要構建 markdown 編譯器,代碼被檢出後,在 Docker 中構建,構建完了之後再把 Runtime 進行打包。打包了之後就把這個 Image 推到自建的 Docker Registry,這個時候我們的而其他伺服器都可以從這裡把 Image pull 下來,在推送完之後,他就通知某一台伺服器,比如我們指定了某一台 markdown 編譯器是運行在某一個伺服器上,他就通知這個伺服器,從上面拉下來 Image,然後去把他啟動起來,我們一般情況下不會直接操縱這些伺服器,都是直接在 UI Dashboard 完成了運維的操作。下一階段:信息時代
第四階段我認為是一個自動化的階段,下一步就是信息時代,也就是數據驅動的時代,自動化,規模化的生產才是我們的目標。最初農耕時代,可能所有都是程序員或者運維上去執行,搞完了之後進入手工業時代,有一些腳本和程序可以協助我們,後來又進入了工業時代,工業時代就有一些自動化的流程做一些自動化的運維處理,最終的目標是進入信息化時代,信息化時代就是整個我們的服務集群是一個雲,這個雲是彈性的,只需要告訴他我們需要什麼就行了,後面的事情他自己會解決。當然這個目標離實現還是有一定距離。
來展望一下:要做到這些,一個是自動化監控,我們現在有了,但是這個監控還是有些問題,比如說每個組件的關鍵指標都不一樣,每一個都需要單獨去配置,我們希望把一些關鍵指標統一化,更好的量化,這樣我們統一寫一些監控的報警規則或者自動化處理規則就可以了。日誌數據分析決策,這是什麼呢,我們很多組件每天在產生上百萬行上千萬行的日誌,這些日誌靠運維去看是不可能的,我們希望能對日誌進行一些分析,做一些自動化的決策,比如說某一個組件,當他數據出來,我們可能就認為他有問題了,通過 LB 把他下線,再把相關的問題發郵件給相關的人員去看,做自動化的目的是可以解放運維。
架構全球化,多機房異地部署,CODING 是肯定會走向國際的,這是我們已經在規劃的一件事情。尤其是碼市,會面向全球去接項目的,早晚有一天我們要向全球部署服務,所以我們的服務必須兼容異地化、高延時的跨機房部署。最後一個主要是為了節省成本,當我們服務越來越多的時候,有些服務這幾天要求計算資源高,有些服務哪幾天要求計算資源高,會存在浪費,所以我們希望實現整個系統可以自動的擴容,形成運維的閉環,在運維人員很少干涉的情況下,自動幫我們節省成本又不失穩定性,我相信在一個超大型的公司,幾百萬台伺服器,肯定都是自動化處理的,希望有一天,我們也能實現這樣的願景。
這是我們希望最終實現的模型。
最底層我們還是會選擇雲服務商,比如說 Ucloud,AWS,包括我們在香港也部署了一些服務。像最底層的服務,物理機房、CDN,這些我們都選擇找供應商,這些供應商都可以水平大規模擴展的。上面一層也是我們不用考慮的,這層主要是 VM,就是說這些服務商把下面這些硬體資源抽象化成上面的這些虛擬的計算機虛擬的網路,把虛擬的網路以 api 的形式提供給我們,我們去編寫一些程序,這個程序可能會在某個適當的時機幫我們啟動伺服器,幫我們自動增加帶寬,自動增加 CDN 節點,這就是Resource API。在這一層上面,是我們的 Docker Container 層,所有服務都運行在這層。再上面一層就是我們自己的服務了,例如一個 markdown 編譯器,一個 Web 網站,一個 Git 服務,還有我們的 LB,監控、緩存、消息隊列等等。我們的運維只通過 Job Manganer 告訴整個集群:我需要起一個實例,大概計算量是多少,那這個雲就會自動幫我們調 Resource API、幫我們開虛擬機,配置網路,監控等等把事情全部搞定。最終就是希望實現的架構運維的閉環。
今天我的分享就到這裡,謝謝大家。
推薦閱讀:
※雙十一絲般順滑體驗背後:阿里雲洛神網路虛擬化系統揭秘
※走過第六個雙11,雙11阿里雲技術負責人楊旭說:大考亦從容
※【逐雲】阿里巴巴通用計算平台負責人關濤:讓計算平台成為阿里的「水電煤」
※Spring Cloud在國內中小型公司能用起來嗎?