唱吧DevOps的落地,微服務CI/CD的范本技術解讀
1、業務架構:從單體式到微服務
K歌亭是唱吧的一條新業務線,旨在提供線下便捷的快餐式K歌方式,用戶可以在一個電話亭大小的空間里完成K歌體驗。K歌亭在客戶端有VOD、微信和Web共三個交互入口,業務復雜度較高,如長連接池服務、用戶系統服務、商戶系統、增量更新服務、ERP等。對于服務端的穩定性要求也很高,因為K歌亭擺放地點不固定,很多場所的運營活動會造成突發流量。
為了快速開發上線,K歌亭項目最初采用的是傳統的單體式架構,但是隨著時間的推移,需求的迭代速度變得很快,代碼冗余變多,經常會出現牽一發動全身的改動。重構不但會花費大量的時間,而且對運維和穩定性也會造成很大的壓力;此外,代碼的耦合度高,新人上手較困難,往往需要通讀大量代碼才不會踩進坑里。
鑒于上述弊端,我們決定接下來的版本里采用微服務的架構模型。從單體式結構轉向微服務架構中會持續碰到服務邊界劃分的問題:比如,我們有user服務來提供用戶的基礎信息,那么用戶的頭像和圖片等是應該單獨劃分為一個新的service更好還是應該合并到user服務里呢?如果服務的粒度劃分的過粗,那就回到了單體式的老路;如果過細,那服務間調用的開銷就變得不可忽視了,管理難度也會指數級增加。目前為止還沒有一個可以稱之為服務邊界劃分的標準,只能根據不同的業務系統加以調節,目前K歌亭拆分的大原則是當一塊業務不依賴或極少依賴其它服務,有獨立的業務語義,為超過2個的其他服務或客戶端提供數據,那么它就應該被拆分成一個獨立的服務模塊。
在采用了微服務架構之后,我們就可以動態調節服務的資源分配從而應對壓力、服務自治、可獨立部署、服務間解耦。開發人員可以自由選擇自己開發服務的語言和存儲結構等,目前整體上使用PHP做基礎的Web服務和接口層,使用Go語言來做長連接池等其他核心服務,服務間采用thrift來做RPC交互。
2、系統架構的構思與解讀
2.1 容器編排
唱吧K歌亭的微服務架構采用了Mesos和Marathon作為容器編排的工具。在我們選型初期的時候還有三個其他選擇,Kubernetes、 Swarm、 DC/OS:
- DC/OS:作為Mesosphere公司的拳頭產品,基本上是希望一統天下的節奏。所以組件很多,功能也很全面。但是對于我們在進行微服務架構初期,功能過于龐大,學習成本比較高,后期的生產環境維護壓力也比較大。
- Swarm:Docker公司自己做的容器編排工具,當時了解到100個以上物理節點會有無響應的情況,對于穩定性有一些擔憂。
- Kubernetes:Google開源的的容器編排工具,在選型初期還沒有很多公司使用的案例,同時也聽到了很多關于穩定性的聲音,所以沒有考慮。但是在整個2016年,越來越多的公司開始在線上使用Kubernetes,其穩定性逐步提高,如果再選型應該也是個好選擇。
- Mesos:因為了解到Twitter已經把Mesos用于生產環境,并且感覺架構和功能也相對簡單,所以最后選擇了Mesos+Marathon作為容器編排的工具。
2.2 服務發現
我們采用了etcd作為服務發現的組件,etcd是一個高可用的分布式環境下的 key/value 存儲服務。在etcd中,存儲是以樹形結構來實現的,非葉結點定義為文件夾,葉結點則是文件。我們約定每個服務的根路徑為/v2/keys/service/$service_name/,每個服務實例的實際地址則存儲于以服務實例的uuid為文件名的文件中,比如賬戶服務account service當前啟動了3個可以實例,那么它在etcd中的表現形式則如下圖:
當一個服務實例向etcd寫入地址成功時我們就可以認為當前服務實例已經注冊成功,那么當這個服務實例由于種種原因down掉了之后,服務地址自然也需要失效,那么在etcd中要如何實現呢?
注意,圖中的每個文件有一個ttl值,單位是秒,當ttl的值為0時對應的文件將會被etcd自動刪除。當每個服務實例啟動之后第一次注冊時會把存活時間即ttl值初始化為10s,然后每隔一段時間去刷新ttl,用來像向etcd匯報自己的存活,比如7s,在這種情況下基本上可以保證服務有效性的更新的及時性。如果在一個ttl內服務down掉了,則會有10s鐘的時間是服務地址有效;而服務本身不可用,這就需要服務的調用方做相應的處理,比如重試或這選擇其它服務實例地址。
我們服務發現的機制是每個服務自注冊,即每個服務啟動的時候先得到宿主機器上面的空閑端口;然后隨機一個或多個給自己并監聽,當服務啟動完畢時開始向etcd集群注冊自己的服務地址,而服務的使用者則從etcd中獲取所需服務的所有可用地址,從而實現服務發現。
同時,我們這樣的機制也為容器以HOST的網絡模式啟動提供了保證。因為BRIDGE模式確實對于網絡的損耗太大,在最開始就被我們否決了,采用了HOST模式之后網絡方面的影響確實不是很大。
2.3 監控,日志與報警
我們選擇Prometheus匯總監控數據,用ElasticSearch匯總日志,主要的原因有:
- 生態相對成熟,相關文檔很全面,從通用的到專用的各種exporter也很豐富。
- 查詢語句和配置簡單易上手。
- 原生具有分布式屬性。
- 所有組件都可以部署在Docker容器內。
Mesos Exporter,是Prometheus開源的項目,可以用來收集容器的各項運行指標。我們主要使用了對于Docker容器的監控這部分功能,針對每個服務啟動的容器數量,每個宿主機上啟動的容器數量,每個容器的CPU、內存、網絡IO、磁盤IO等。并且本身他消耗的資源也很少,每個容器分配0.2CPU,128MB內存也毫無壓力。
在選擇Mesos Exporter之前,我們也考慮過使用cAdvisor。cAdvisor是一個Google開源的項目,跟Mesos Exporter收集的信息八成以上都是類似的;而且也可以通過image字段也可以變相實現關聯服務與容器,只是Mesos exporter里面的source字段可以直接關聯到marathon的application id,更加直觀一些。同時cAdvisor還可以統計一些自定義事件,而我們更多的用日志去收集類似數據,再加上Mesos Exporter也可以統計一些Mesos本身的指標,比如已分配和未分配的資源,所以我們最終選擇了Mesos Exporter。
如下圖,就是我們監控的部分容器相關指標在Grafana上面的展示:
Node exporter,是Prometheus開源的項目,用來收集物理機器上面的各項指標。之前一直使用Zabbix來監控物理機器的各項指標,這次使用NodeExporter+Prometheus主要是出于效率和對于容器生態的支持兩方面考慮。時序數據庫在監控數據的存儲和查詢的效率方面較關系數據庫的優勢確實非常明顯,具體展示在Grafana上面如下圖:
Filebeat是用來替換Logstash-forwarder的日志收集組件,可以收集宿主機上面的各種日志。我們所有的服務都會掛載宿主機的本地路徑,每個服務容器的會把自己的GUID寫入日志來區分來源。日志經由ElasticSearch匯總之后,聚合的Dashboard我們統一都會放在Grafana上面,具體排查線上問題的時候,會用Kibana去查看日志。
Prometheus配置好了報警之后可以通過AlertManager發送,但是對于報警的聚合的支持還是很弱的。在下一階段我們會引入一些Message Queue來自己的報警系統,加強對于報警的聚合和處理。
ElastAlert是Yelp的一個Python開源項目,主要的功能是定時輪詢ElasticSearch的API來發現是否達到報警的臨界值,它的一個特色是預定義了各種報警的類型,比如frequency、change、flatline、cardinality等,非常靈活,也節省了我們很多二次開發的成本。
2.4 事務追蹤系統——KTrace
對于一套微服務的系統結構來說,最大的難點并不是實際業務代碼的編寫,而是服務的監控和調試以及容器的編排。微服務相對于其他分布式架構的設計來說會把服務的粒度拆到更小,一次請求的路徑層級會比其他結構更深,同一個服務的實例部署很分散,當出現了性能瓶頸或者bug時如何第一時間定位問題所在的節點極為重要,所以對于微服務來說,完善的trace機制是系統的核心之一。
目前很多廠商使用的trace都是參考2010年Google發表的一篇論文《Dapper, a Large-Scale Distributed Systems Tracing Infrastructure》來實現的,其中最著名的當屬twitter的zipkin,國內的如淘寶的eagle eye。由于用戶規模量級的逐年提升,分布式設計的系統理念越來越為各廠商所接受,于是誕生了trace的一個實現標準opentracing ,opentracing標準目前支持Go、JavaScript、Java、 Python、Objective-C、C++六種語言。 由sourcegraph開源的appdash是一款輕量級的,支持opentracing標準的開源trace組件,使用Go語言開發K歌亭目前對appdash進行了二次開發,并將其作為其后端trace服務(下文直接將其稱之為Ktrace),主要原因是appdash足夠輕量,修改起來比較容易。唱吧K歌亭業務的膠水層使用PHP來實現,appdash提供了對protobuf的支持,這樣只需要我們自己在PHP層實現middleware即可。
在trace系統中有如下幾個概念
(1)Annotation
一個annotation是用來即時的記錄一個事件的發生,以下是一系列預定義的用來記錄一次請求開始和結束的核心annotation
- cs - Client Start。 客戶端發起一次請求時記錄
- sr - Server Receive。 服務器收到請求并開始處理,sr和cs的差值就是網絡延時和時鐘誤差
- ss - Server Send: 服務器完成處理并返回給客戶端,ss和sr的差值就是實際的處理時長
- cr - Client Receive: 客戶端收到回復時建立。 標志著一個span的結束。我們通常認為一但cr被記錄了,一個RPC調用也就完成了。
其他的annotation則在整個請求的生命周期里建立以記錄更多的信息 。
(2)Span
由特定RPC的一系列annotation構成Span序列,span記錄了很多特定信息如 traceId, spandId, parentId和RPC name。
Span通常都很小,例如序列化后的span通常都是kb級別或者更小。 如果span超過了kb量級那就會有很多其他的問題,比如超過了kafka的單條消息大小限制(1M)。 就算你提高kafka的消息大小限制,過大的span也會增大開銷,降低trace系統的可用性。 因此,只存儲那些能表示系統行為的信息即可。
(3)Trace
一個trace中所有的span都共享一個根span,trace就是一個擁有共同traceid的span的集合,所有的span按照spanid和父spanid來整合成樹形,從而展現一次請求的調用鏈。
目前每次請求由PHP端生成traceid,并將span寫入Ktrace,沿調用鏈傳遞traceid,每個service自己在有需要的地方埋點并寫入Ktrace。舉例如下圖:
每個色塊是一個span,表明了實際的執行時間,通常的調用層級不會超過10,點擊span則會看到每個span里的annotation記錄的很多附加信息,比如服務實例所在的物理機的IP和端口等,trace系統的消耗一般不會對系統的表現影響太大,通常情況下可以忽略,但是當QPS很高時trace的開銷就要加以考量,通常會調整采樣率或者使用消息隊列等來異步處理。不過,異步處理會影響trace記錄的實時性,需要針對不同業務加以取舍。
目前K歌亭在生產環境里的QPS不超過1k,所以大部分的記錄是直接寫到ktrace里的,只有歌曲搜索服務嘗試性的寫在kafka里,由mqcollector收集并記錄,ktrace的存儲目前只支持MySQL。一個好的trace設計可以極快的幫你定位問題,判斷系統的瓶頸所在。
2.5 自動擴容
在服務訪問峰值的出現時,往往需要臨時擴容來應對更多的請求。除了手動通過Marathon增加容器數量之外,我們也設計實現了一套自動擴縮容的系統來應對。我們擴縮容的觸發機制很直接,根據各個服務的QPS、CPU占用、內存占用這三個指標來衡量,如果三個指標有兩個指標達到,即啟動自動擴容。我們的自動擴容系統包括3個模塊:
- Scout:用于從各個數據源取得自動擴容所需要的數據。由于我們的日志全部都匯總在ElasticSearch里面,容器的運行指標都匯總到Prometheus里面,所以我們的自動擴容系統會定時的請求二者的API,得到每個服務的實時QPS、CPU和內存信息,然后送給Headquarter。
- Headquarter:用于數據的處理和是否觸發擴縮容的判斷。把從Scout收到的各項數據與本地預先定義好的規則進行比對,如果有兩個指標超過定義好的規則,則通知到Signalman模塊。
- Signalman:用于調用各個下游組件執行具體擴縮容的動作。目前我們只會調用Marathon的/v2/apps/{app_id}接口,去完成對應服務的擴容。因為我們的服務在容器啟動之后會自己向etcd注冊,所以查詢完容器狀態之后,擴縮容的任務就完成了。
3、基于Mesos+Marathon的CI/CD
3.1 持續集成與容器調度
在唱吧,我們使用Jenkins作為持續集成的工具。主要原因是我們想在自己的機房維護持續集成的后端,所以放棄了Travis之類的系統。
在實施持續集成的工作過程中,我們碰到了下列問題:
- Jenkins Master的管理問題。多個團隊共享一個Master,會導致權限管理困難,配置改動、升級門檻很高,Job創建和修改有很多規則;每個團隊用自己的Master,會導致各個Master之間的插件、更新、環境維護有很多的重復工作。
- Jenkins Slave 資源分配不平均:忙時Jenkins slave數量不足,Job運行需要排隊;閑時Jenkins Slave又出現空閑,非常浪費資源。
- Jenkins job運行需要的環境多種多樣,比如我們就有PHP,java,maven,Go,python等多種編譯運行環境,搭建和維護slave非常費時。
- 多個開發人員的同時提交,各自的代碼放到各自獨立的測試環境進行測試。
基于以上問題,我們選擇使用Mesos和Marathon來管理Jenkins集群,把Jenkins Master和Jenkins Slave都放到Docker容器里面,可以非常有效的解決以上問題。基礎架構如下圖:
- 不同開發團隊之間使用不同的Jenkins Master。把公用的權限、升級、配置和插件更新到私有Jenkins Master鏡像里面,推到私有鏡像倉庫,然后通過Marathon部署新的Master鏡像,新團隊拿到的Jenkins Master就預安裝好了各種插件,各個現有團隊可以無縫接收到整體Jenkins的升級。
- 各種不同環境的Jenkins Slave,做成Slave鏡像。按照需要,可以通過Swarm Plugin自動注冊到Jenkins master,從而組織成slave pool的形式;也可以每個job自己去啟動自己的容器,然后在容器里面去執行任務。
- Jenkins job從容器調度的角度分成兩類,如下圖:
- 環境敏感型:比如編譯任務,需要每次編譯的環境完全干凈,我們會從鏡像倉庫拉取一個全新的鏡像啟動容器,去執行Job,然后再Job執行完成之后關閉容器。
- 時間敏感型:比如執行測試的Job,需要盡快得到測試結果,但是測試機器的環境對于測試結果沒什么影響,我們就會從已經啟動好的Slave Pool里面去拉取一個空閑的Slave去執行Job。然后再根據Slave被使用的頻率去動態的擴縮容Slave pool的大小就好了。
3.2 CI/CD流程
基于上述的基礎架構,我們定義了我們自己的持續集成與持續交付的流程。其中除了大規模使用Jenkins與一些自定制的Jenkins插件之外,我們也自己研發了自己的部署系統——HAWAII。
在HAWAII中可以很直觀的查看各個服務與模塊的持續集成結果,包括最新的版本,SCM revision,測試結果等信息,然后選擇相應的版本去部署生產環境。
在部署之前,可以查看詳細的測試結果和與線上版本的區別,以及上線過程中的各個步驟運行的狀態。
基于上述基礎架構,我們的CI/CD流程如下:
- SVN或者GIT收到新的代碼提交之后,會通過hook啟動相應的Jenkins job,觸發整個CI流程。
- Jenkins從私有鏡像倉庫拉取相對應的編譯環境,完成代碼的編譯。
- Jenkins從私有鏡像倉庫拉取相對應的運行時環境,把上一步編譯好的產品包打到鏡像里面,并生成一個新版本的產品鏡像。產品鏡像是最終可以部署到線上環境的鏡像,該鏡像的metadata也會被提交到部署系統HAWAII,包括GUID,SCM revision,Committer,時間戳等信息
- 將步驟3中生成的產品鏡像部署到Alpha環境(該環境主要用于自動化回歸測試,實際啟動的容器其實是一個完整環境,包括數據容器,依賴的服務等)。
- Jenkins從私有鏡像倉庫拉取相對應的UT和FT的測試機鏡像,進行測試。測試完成之后,會銷毀Alpha環境和所有的測試機容器,測試結果會保存到部署系統HAWAII,并會郵件通知到相關人員。
- 如果測試通過,會將第3步生成的產品鏡像部署到QA環境,進行一系列更大范圍的回歸測試和集成測試。測試結果也會記錄到HAWAII,有測試不通過的地方會從第1步從頭開始迭代。
- 全部測試通過后,就開始使用HAWAII把步驟3中生成的產品鏡像部署到線上環境。
4、小結
隨著互聯網的高速發展,各個公司都面臨著巨大的產品迭代壓力,如何更快的發布高質量的產品,也是每個互聯網公司都面臨的問題。在這個大趨勢下,微服務與DevOps的概念應運而生,在低耦合的同時實現高聚合,也對新時代的DevOps提出了更高的技術與理念要求。
這也是我們公司在這個新的業務線上面進行,進行嘗試的主要原因之一。對于微服務、容器編排、虛擬化、DevOps這些領域,我們一步一步經歷了從無到有的過程,所以很多方面都是本著從滿足業務的目標來盡量嚴謹的開展,包括所有的服務與基礎架構都進行了高并發且長時間的壓力測試。
在下一步的工作中,我們也有幾個核心研究的方向與目標,也希望能跟大家一起學習與探討:
- 完善服務降級機制
- 完善報警機制
- 完善負載均衡機制