持續集成的實踐:基於容器的微服務架構設計

(同步發表於 Cepave Blog

敏捷開發關鍵的一環:持續集成

隨著軟體開發複雜度的不斷提高,團隊開發成員間如何更好地協同工作以確保軟體開發的質量已經慢慢成為開發過程中不可迴避的問題。尤其是近些年來,敏捷軟體開發逐漸成為顯學,如何能在不斷變化的需求中快速適應和保證軟體質量顯得尤其重要。

持續集成(Continuous Integration)作為敏捷開發重要的一步,其目的在於讓產品快速迭代的同時,盡可能保持高質量。每一次程式碼更新,都要通過自動化測試來檢測程式碼和功能的正確性,只有通過自動測試的程式碼才能進行後續的交付和部署。它倡導團隊開發成員必須經常集成他們的工作,甚至每天都可能發生多次。而每次集成都是通過自動化的構建來驗證,包括自動編譯和測試,從而盡快地發現集成錯誤,讓團隊能夠更快的開發產品。

持續集成的特點

  • 週期性的自動化過程: 從拉取程式碼、編譯構建、運行測試、結果記錄、到測試統計
  • 需要有專門的集成伺服器來執行集成構建,常見的如:Jenkins, Travis CI
  • 需要有程式碼托管工具支持,如:Git, SVN

持續集成的作用

  • 保證程式碼質量,減輕軟體發佈時的壓力:程式碼變更所帶來的風險與所修改的程式碼行數未必是線性相關的。如果每個迭代之間的週期拉得越長,程式碼庫積壓過多,越得不到生產檢驗。程式碼間交叉感染擴大,模組互相之間的依賴項越來越多,部署的複雜度和風險也越高
  • 節省人力開銷:任何一個環節都是自動完成的,無需太多的人工干預,有利於減少重復過程以節省時間、費用和工作量
  • 加快迭代速度:使用者越早用到就越早反饋,團隊越早得到反饋,好壞都是有價值的輸入

持續集成的下一步:持續交付

近幾年除了 CI 之外,我們還可以經常聽到 CD,CD 有兩種含義:

  1. 持續交付(Continuous Delivery)
  2. 持續部署(Continuous Deployment)

以上兩種 CD 跟 CI 最主要的差別就是把測試->發佈的流程也自動化了,而持續性交付與部署基本上是指同一件事,差別只在於手動或者自動部署,如下圖:

(因為篇幅關係,本文不會在 CD 的部分有過多的著墨。)

持續集成的實踐:基於容器的微服務架構設計

介紹了 CI/CD,也說了這麼多好處,那麼如果要導入持續集成,又該如何開始呢?現在業界普遍採用的實踐是將架構微服務化,系統容器化。

微服務(Microservices)把系統所做的事情拆解成小且可管理的條目並且分而治之,可以讓大型項目在團隊工作管理、程式碼變更以及發佈週期方面更具優勢。更小的程式碼庫可以讓程式員更加專注,並且與產品客戶有更投入的關係,這樣工程師就能夠對工作有更明晰的認識和更積極的動力。與使用者聯繫緊密後會得到更快的反饋,工程師可以更及時的發現產品所暴露的缺陷以及應當去實現的新功能。

容器化(Containerization)的用意則是把微服務中配置、依賴、與運行環境封裝成為一個鏡像,透過 Dockerfile 讓運行環境程式碼化(Infrastructure as Code),方便版本控管,讓運行環境不再是個黑箱。以標準化後的開發/測試/部署的運行環境,讓持續集成中的各個環節更容易地自動化。

傳統的系統架構

傳統的應用通常採用所謂的三層架構,例如:界面層 – 業務邏輯層 – 數據層,通常我們會把所有實現業務邏輯層的程式碼編譯構建後部署到中間件,再通過負載均衡、集群等解決分流、災備等問題。但是這種架構設計帶來的問題是:

  • 開發效率低:隨著應用複雜度的增加,越來越少開發人員對應用能有全局性的深度理解。新功能開發和缺陷修復難度呈幾何性增加。程式碼修改的正確性無法保障。而龐大的程式碼庫需要更龐大的開發團隊來維護,無形中又增添了管理、溝通和協調的成本。另外,新加入的團隊成員需要花費大量的時間和精力來熟悉一個複雜的程式碼庫。
  • 交付週期長:在單一進程的單塊架構下,任何微小的改動都需要重新編譯、集成、測試和部署整個應用。隨著應用體積的增大,交付流程和反饋週期都會相應變長,應用發佈的代價也隨之增加。於是應用交付週期變緩,交付間隙積累的程式碼變動增加,從而對於下次交付產生更大的壓力,形成惡性循環。
  • 技術轉型難:單一進程、單塊架構意味著中心化的技術選型。比如,應用的不同邏輯組建通常需要採用相對統一的編程語言、框架和技術棧。這些在項目初始階段便已定型。之後,即便是應用中全新的邏輯組件,也很難採用不同的技術棧。而當應用達到一定規模後,全局化的技術棧更新會面臨很高的風險。所以,單塊架構應用一旦定型,就很難再享受行業技術變更、發展所帶來的紅利。

微服務的優勢:靈活可控

在微服務架構下,我們將原本單一的應用按照功能邊界(Bounded Context)或者 DDD (Domain Driven Design) 精神設計/分解成一系列獨立、專注的微服務。每個微服務對應傳統應用中的一個組件,但是可以獨立編譯、部署和擴展。相對單模組的架構,微服務具備以下優勢:

  • 複雜度可控:在將應用分解的同時,規避了原本複雜度無止境的積累。每一個微服務專注於單一功能,並通過定義良好的接口清晰表述服務邊界。由於體積小、複雜度低,每個微服務可由一個小規模開發團隊完全掌控,易於保持高可維護性和開發效率。
  • 獨立部署:由於微服務具備獨立的運行進程,所以每個微服務也可以獨立部署。當某個微服務發生變更時無需編譯、部署整個應用。由微服務組成的應用相當於具備一系列可並行的發佈流程,使得發佈更加高效,同時降低對生產環境所造成的風險,最終縮短應用交付週期。
  • 技術選型靈活:微服務架構下,技術選型是去中心化的。每個團隊可以根據自身服務的需求和行業發展的現狀,自由選擇最適合的技術棧。由於每個微服務相對簡單,當需要對技術棧進行升級時所面臨的風險較低。因為服務間是以接口溝通,耦合度低,甚至完全重構一個微服務也是可行的。微服務架構中,可為每個服務選擇一個新的適合業務邏輯的資料庫系統,比如 MongoDB、PostgreSQL。這樣做的好處:首先我們可以根據業務類型來選擇適合的資料庫,同時也可以減小單個資料庫的負載。
  • 容錯機制易實現:當某一組件發生故障時,在單一進程的傳統架構下,故障很有可能在進程內擴散,形成應用全局性的不可用。在微服務架構下,故障會被隔離在單個服務中。若設計良好,其他服務可通過重試、平穩退化等機制實現應用層面的容錯。
  • 易於橫向擴展:單塊架構應用也可以實現橫向擴展,就是將整個應用完整的複製到不同的節點。當應用的不同組件在擴展需求上存在差異時,微服務架構便體現出其靈活性,因為每個服務可以根據實際需求獨立進行擴展。

微服務帶來的問題:更高的技術門檻

服務拆分為微服務之後不同模組之間的聯繫和依賴會更加複雜,勢必對於運維和部署有更高的要求,如果沒有工具和系統能夠提供這樣的能力,或者沒有更好的方法去做的話,微服務就是空中樓閣

微服務從字面上來看就是把大服務拆分為多個微服務,看似簡單。實際上採用微服務架構對整個團隊要求的門檻是更高的:

  • DevOps 技術棧:使用微服務架構後,團隊需要有 DevOps 精神,善用工具和自動化技術來解決問題。這要求了大家得懂更多不同以往類型的技術棧。
  • 運維成本:拆分成微服務後,部署變得複雜,監控的複雜度也隨之提高,要理解在微服務之間產生的複雜交互,需要優秀的診斷與監控工具,同時對運維技能的要求也提高了(如:必須瞭解多種技術棧)。而更多的服務也就意味著更多的運維工作,需要保證所有的相關服務都有完善的監控等基礎設施,傳統的架構開發者只需要保證一個應用正常運行,而現在卻需要保證幾十甚至上百道工序高效運轉,這是一個艱巨的任務。
  • 接口相依與版本控管:因為服務和服務之間通過接口溝通,當某一個服務更改接口格式時,可能涉及到此接口的所有服務都需要做調整,這要求了大家要對系統中各服務間的調用關係有一定的瞭解,並且開發接口必須遵循團隊的紀律,不能隨心所欲地變更接口。
  • 分布式系統的複雜性:分布式系統意味著開發者需要考慮不可靠的網路/網路延遲、容錯、消息序列化、同步/異步、版本控制、負載均衡等,而面對如此多的微服務都需要分布式時,整個產品需要有一整套完整的機制來保證各個服務可以正常運轉。

實現微服務架構的方式

微服務是一種觀念,沒有制式的標準,可以按照組織架構或者產品特性來定制,也可以利用開源工具與框架以節省技術勞動工作。如果不採用任何框架,各個團隊的服務提供方就需要各自實現一套序列化/反序列化、網路框架、連接池、收發線程、超時處理、和狀態機等業務之外的重復技術勞動,造成整體的低效。所以,統一處理上述非關業務的技術勞動,是服務化首要解決的問題。

模組間的內部溝通:RPC 框架

RPC 框架是架構微服務化的首要基礎組件,它能大大降低架構微服務化的成本,提高調用方與服務提供方的研發效率,屏蔽跨進程調用的各類複雜細節。

  1. 客戶端職責:序列化/反序列化、超時管理、連接池管理、負載均衡、故障轉移、隊列管理、異步管理等等。
  2. 服務端職責:序列化/反序列化、超時管理、服務端收發包隊列、I/O 線程、工作線程、上下文管理器、異步回調等等。

整體解決方案:微服務框架

Netflix 是微服務的先驅之一,在開源也做了許多貢獻。如果不滿足於 RPC 框架,可以參考 Netflix OSS 或者 Spring Cloud 這兩套目前比較盛行的微服務框架。比較可惜的是目前比較成熟的框架多數是基於 JAVA 實現的。(更多工具/框架可以參考 Github 上面的微服務推薦清單

一套完整的微服務框架,可能包括這些與業務不直接相關的組件:

  • 分散式訊息(Distributed messaging):透過輕量化的訊息分發器連接分散式系統中的各個節點。
  • 監控數據流與日誌處理(Metrics/Trace/Logging):服務分析需要處理海量來自實時租戶應用的通信追蹤,進一步發現應用程式拓撲結構,跟蹤當服務通過網路微服務時的單個請求等。
  • 可插拔的序列化組件(Pluggable Serialization): 支持 JSON, XML, Plain text, Compact Binary 等格式的序列化/反序列化。
  • 健康檢查(Health Check):提供對應的健康檢查接口供監控系統調用,便於第一時間發現異常。
  • HTTP RESTful API 與 RPC 接口:實現服務間接口調用的框架。
  • 分布式配置管理(Distributed/Versioned Configuration):可在分散式系統中實現動態變更、配置同步,並以版本控管。
  • 服務網關(Service Gateway):除了具備服務路由、負載均衡外,在外部訪問最前端的地方產生前門保護作用,避免權限控制機制貫穿並污染整個開放服務的業務邏輯,破壞了微服務無狀態的特點。服務網管包含:
    • 權限控管機制
    • 微代理/路由
    • 服務註冊/發現
    • 負載均衡
  • 斷路器(Circuit Breakers):避免在微服務架構中個別服務出現異常時引起的故障蔓延。通常包含了以下兩種機制:
    1. 限速(Rate Limiting)
    2. 容錯(Fault Tolerance)
  • 集群狀態管理(Cluster State Management):除了狀態查看之外,同時包含了服務故障發生後的領導選舉機制(Leadership Election)。

透過 Docker 來支撐微服務

前面我們探討了一下基於容器的微服務架構設計給傳統開發運維模式帶來的改變,看起來更多的還是正面的改變。

容器化微服務解決的更多的是架構設計的問題,按軟體工程來講,設計之後的下一步就是開發實現的事情了,在這個階段,傳統的開發測試會有不少的問題:

  • 難以實現混合部署:共用一個伺服器開發環境,隔離性差,互相衝突。
  • 可移植性差:例如和生產環境不一致,開發人員之間也無法共享;新人入職通常又折騰一遍開發環境,無法快速搭建。
  • 環境配置不透明:軟體安裝麻煩、來源不一致、安裝方式不一致、雜亂無章。
    • 版本兼容性:兼容性問題是軟體開發者的噩夢,如:依賴包間的版本兼容、程式碼與模組間的版本兼容。
    • 間接依賴與多重依賴:例如:A 依賴於 Python 2.7, A 還依賴於 B ,但 B 卻依賴於 Python 3,苦逼的是 Python 2.7 和 Python 3 不兼容。依賴中最痛苦的事莫過於此。
    • 系統層面的外部依賴:項目除了對程式包的依賴,對於運行環境也有些具體的要求。比如:Web 應用需要安裝和配置 Web 伺服器、應用伺服器、數據伺服器等,企業應用中可能需要消息隊列、緩存、定時作業,或是對其他系統以 Web Service 或 API 的方式暴露服務等。這些可以看成項目在系統層面對外部的依賴。

傳統的工作流

簡單項目中,開發和運行環境都由開發人員搭建,當公司變大時,系統的運行環境將由運維人員搭建,而開發測試環境如果由運維人員搭建則工作量太大,由開發人員自己搭建則操作複雜又容易產生不一致的情況。假設公司為項目 A 和項目 B 開發了新版本。但環境和軟體升級不是同步進行,出錯的可能性非常大(想一想間接依賴和多重依賴的情況)。大家對這樣的場景有沒有印象:當新版本部署時,發現問題,測試或部署人員說:「版本有問題,無法運行!」開發人員卻說:「我這裡沒問題啊,運行正常!」

在傳統的開發運維模式下,開發自測沒問題後提交程式碼;測試接手之後要花很多時間配置測試環境,到了測試環境程式無法運行,讓開發團隊排查,經過長時間排查最後才發現是測試環境的一個第三方庫過時了;同樣的問題從測試環境到了生產環境可能又在運維那裡發生了一次。這樣的現象在傳統的軟體開發中層出不窮,已經不適用如今的快速開發和部署。

歸納總結一下,傳統的工作流帶來了以下問題:

  • 溝通成本高:上線環節多,跨越多個部門。開發設計時未過多考慮運維,導致後續部署及維護的困難
  • 運行環境是個黑箱:從需求到版本上線過程中,運行環境完全是個黑箱,環境不一致風險不可控。運行環境中的資源還涉及多個部門的審核,包括資源的申請、環境權限的批准、需求測試驗收等等。多語言(Java, PHP, Go,...),多系統(Windows, Linux,...),多構建工具版本(Java7, Java8, Python2.x, Python3.x,...),如果還有各種配置文件和黑科技補丁腳本散落在系統的各個角落,沒人找得到,也沒人搞得懂。配置被誰改掉了,服務宕掉了,根本無從管理。
  • 煙囪式開發:開發各自為政,未考慮共享重用、聯調,開發的資產積累不能快速交移到運維手中。如果項目簡單,沒有任何歷史項目和程式碼的拖累,且各項目之間也沒有任何的關聯,只需進行資源方面的管理:分配機器,初始化系統,分配 IP 地址等。各個項目的運行環境、資料庫、開發環境等都由具體項目的開發人員手動完成,這樣的環境出問題怎麼辦?這類項目若成為歷史遺留程式碼,會對未來其他的項目有很不良的影響。

解決方法:透過容器讓開發/測試/部署標準化

以容器標準化:一致的基礎環境,配置參數,依賴包,一套部署腳本,建立多個環境

  1. 開發環境:在團隊內部構建本地的倉庫,並提供應用鏡像標準化所有的開發環境,使得團隊的新人可以快速上手。
  2. 測試環境:用 Docker 做分布式集群模擬和測試,成本會更加低廉,更加容易維護。
  3. 運維環境:在生產環境部署 Docker 是 PaaS 的虛擬化和自動化的一種方式,利用 Docker 能夠更便捷地實施 PaaS。

容器標準化後的工作流

項目開始,架構師根據項目預期創建好需要的基礎鏡像(如:Nginx, MySQL),或者將 Dockerfile 分發給所有開發人員,開發人員根據 Dockerfile 創建的容器或從內部倉庫下載的鏡像來進行開發,如果開發過程中需要添加新的軟體,則向架構師申請修改基礎鏡像的 Dockerfile。

開發任務結束後,架構師調整 Dockerfile 或鏡像,鏡像編譯完成後推送至鏡像庫#1。分發給測試部門,測試部門馬上就可以進行測試,測試完成後推送至鏡像庫#2,測試未通過的話讓開發重新準備新的鏡像至鏡像庫#1。這樣一來可以確保在鏡像庫#2中的任何鏡像都是可發佈,消除了部署困難等難纏的問題。

針對傳統應用交付過程中的這些問題,Docker 的引入帶來了明顯的改變,Docker 把應用及相關依賴項打包成一個輕量、可移植、自包含的容器,讓應用的部署和發佈基於容器進行,而不是基於程式碼部署。由此,Docker 重新定義了打包程式的方法。

容器化帶來了標準化的開發/測試環境,這對開發測試的意義體現在:測試環境搭建效率的提高,高效利用硬件資源同時又能敏捷輕便地搭建功能完備的開發測試環境,例如在一個資源有限的環境下面(比如開發人員的筆記本電腦)例如Chef 把系統的各個模組按照清晰的邏輯結構部署並運轉起來,從而快速高效地進行開發與測試;對運維部署的意義體現在:開發者本地測試、CI 伺服器測試、測試人員測試,以及生產環境運行的都可以是同一個 Docker 鏡像。因此可以實現應用的自動化快速部署及上線發佈。在 Docker 模式下,應用是以容器的形式存在,所有和該應用相關的依賴都會在容器中,因此移植非常方便,不會存在像傳統模式那樣的環境不一致。

總結:Docker 帶來的好處

  • 容器先天具備的可移植性封裝了程式的所有依賴,不論是對內部套件或者外部系統的依賴。讓運行環境在產品開發的任何階段都保持統一
    • 就像集裝箱一樣快速打包應用以及依賴項,在開發、測試、運維之間移動,所以可以推進開發 – 測試 – 運維環境的統一
    • 使用簡單,鏡像上傳之後,只需要 docker pull 並且按照指定的方式運行,即可將一個可能很複雜的系統跑起來
  • Infrastructure as Code,任何對環境的資源變更都可以用版本控管查詢改動內容。
  • 容器之間互相隔離,使得應用在運行時就像處於沙箱中,每個應用都認為自己是在系統中唯一運行的程式。這樣就可以很方便地在系統中混合部署多種不同環境來解決依賴複雜度的問題。
  • 相比傳統虛擬化技術,容器級的虛擬技術是操作系統內核層的虛擬,所以能節省更多資源、提升性能,在伺服器中啓動上百個容器也不是太大的問題
  • 善用 Docker 啓動快速的特性,我們可以透過保持兩套一樣的生產環境,而實際上只有一套環境真正的對外提供服務,讓系統的升級/降級都不影響現正運行的業務,最小化停機時間。
    • 系統秒級回滾:透過容器集群管理,我們可以批量秒級地回滾產品版本
    • 熱備份:兩套生產環境,實際上只運行一套,升級/降級,或是故障發生的時候馬上透過路由切換快速恢復
    • 鏡像分層技術,部署時可以按需獲取鏡像生成容器並快速啓動運行,因此有利於快速的部署擴容,解決運維中水平擴容的問題。

最好的運維 就是沒有運維

可能有些人的經驗是抱持著一些理想進入組織,但是在部門的分工之中,這些部門迷失了自我價值,人與人通常花了非常多的時間在溝通(猜忌、懷疑),而實際溝通不只沒有解決問題,還往往帶來更多的問題,而那些沒效率的溝通,卻莫名其妙的轉變成了一種好努力的正面的績效?不會感到矛盾嗎?但是若不溝通,又怎麼能找到問題?甚至找到方法解決問題呢?問題來自於匱乏的存在,這種匱乏的產生原因可能很多,組織內部門化、立場差異造成溝通問題,所以使得創新困難、效率不彰、技術落差、回應市場不及,使得部門的核心價值流失而彼此間的關係流於政治化,陷入衝突對立的迴圈。

  • 解決問題是核心價值
  • 溝通是一種理性工具
  • 從矛盾的關係中解脫

DevOps = Development + Operation

在過去,開發 – 測試 – 運維,基本上就是不相干的,開發寫完程式碼上傳到 Git 倉庫之後,就認為工作到此為止;測試要負責把開發的程式碼跑起來,而這些配置如果漏了推送或者是環境依賴變更了沒有通知到測試,測試工作就無法推進。即便測試通過了,運維又要重複同樣的工作在生產環境上,環境不統一,可能又發現當初測試階段沒有發現的問題,同樣的工作每個部門都做了一次,還沒能達到成效,不斷地重復技術勞動。

從字面上的意思來看,DevOps 就是要模糊開發與運維的邊界,降低跨部門之間的溝通成本,加快迭代速度。

因此 DevOps 可以說是一種工具理念,一種確認途徑的實踐精神,在這裡能發現有許多的工具不斷的被創造出來,而 DevOps 觀念的出現便是要協助突破困境,解決問題。文章前面提到的 Docker 與微服務基本上就是實踐 DevOps 精神的最佳工具。

DevOps = Culture + Tools + ?

DevOps 不是什麼方法論,所以通常很難明確的說明實踐方式,也沒有所謂的標準。有些人說 DevOps 是一種文化?是一種思維?或者是一種運動?其實都對,DevOps 可以是組織變革、可以是流程改造、也可以是一種實證精神。DevOps 沒有方法論、不是工具之爭,關鍵是尋找核心價值,而在確認了核心價值為目標的情況下,建立具有認知盈餘的關係。

Rather DevOps is an approach to culture, process, and tools. The high-level goal for DevOps is to deliver increased business value and responsiveness through rapid, iterative, and high-quality IT service delivery.

以工作流來看,DevOps 涵蓋多種面向,DevOps 是一個完整的面向IT運維的工作流,以 IT 自動化以及持續集成(CI)、持續部署(CD)為基礎,來優化程式開發、測試、系統運維等所有環節。DevOps 是一種有批判精神的價值理性,當品質、開發與營運上的透明,問題的困境常能直接地被凸顯出來,而這種面對問題與解決問題的核心價值,是需要建立維持起來。

DevOps isn’t just about tools. It isn’t just about process. It isn’t just about culture. If you’re looking at DevOps through a lens that makes it solely about a single aspect of IT, you’re probably doing it wrong and probably won’t succeed.

其實什麼是 DevOps?有沒有明確定義一點都不重要,因為每一個人都會講出相似但不盡相同的答案,而每一個公司、團隊的狀況不盡相同,因此沒有一套適合所有公司的 DevOps,每一個團隊的 DevOps 必定有相異之處,仍是強調那個重點持續改善

最大的困難在「人」

再次強調重點在於持續改善,而改善(或者改變)最大的關卡還是在「人」身上,因此 DevOps 並不是單指工具、方法,更重要的是文化上的改變。

DevOps 對組織內創新來說知易行難,因為傳統樹狀的組織結構必然帶來本位主義存在,文化與思維的慣性,管理階層一般能理解到創新的價值與推動變革的必要,但是一方面會擔心部門本位主義帶來更嚴重的權力鬥爭與衝突發生,而讓創新落為權力移轉的迷思,所以當核心價值的模糊可能是 DevOps 實施的困難,而讓這過程落入一種工具選擇/方法上的迷失。所以前提是創新的動能必須足夠大且極具破壞現有權力結構的可能,從樹狀結構自下而上貫穿權力的金字塔。

創新需要以達到核心價值為目標,不能陷入工具的泥沼。實踐上常是將開發、測試、維運部門碎片化、關係透明化,反思並抵制意在奪權的對立,以改善組織權力關係的衝突結構,讓部門間從關係制肘的受害者循環中解脫,進入到具有公共合作的當責式組織,能專注在組織的核心本質上,透過理性工具輔助並分析數據協助決策,成為一種具 DevOps 文化或是精神的組織,而中心是一種改善主義、分析策略漸進,以可預期的迭代流程來重構當下的問題結構。

實施 DevOps 的三個重要心法

  1. 從流程、組織、技術變革各方面做系統性思考
  2. 建立快速反饋迴路以及時發現問題(如:持續集成)
  3. 擁抱持續實驗和學習的文化

將測試/運維自動化、進而達成開發/測試/運維一體化,通過高度自動化工具與流程來使得軟體構建、測試、發佈更加快捷、頻繁和可靠。

最好的運維,就是沒有運維:)

參考資料