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