當我們討論Microservices架構時,我們通常會和Monolithic架構(單體架構 )進行比較。
在Monolithic架構中,一個簡單的應用會隨著功能的增加、時間的推移變得越來越龐大。當Monoltithic App變成一個龐然大物,就沒有人能夠完全理解它究竟做了什麼。此時無論是新增新功能,還是修復Bug,都是一個非常痛苦、異常耗時的過程。
Microservices架構漸漸被許多公司採用(Amazon、eBay、Netflix),用於解決Monolithic架構帶來的問題。其思路是將應用分解為小的、可以相互組合的Microservices。這些Microservices通過輕量級的機制進行互動,通常會採用基於HTTP協議的服務。
每個Microservices完成一個獨立的業務邏輯,它可以是一個HTTP API服務,提供給其他服務或者客戶端使用。也可以是一個ETL服務,用於完成資料遷移工作。每個Microservices除了在業務獨立外,也會有自己獨立的執行環境,獨立的開發、部署流程。
這種獨立性給服務的部署和運營帶來很大的挑戰。因此持續部署(Continuous Deployment)是Microservices場景下一個重要的技術實踐。本文將介紹持續部署Microservices的實踐和準則。
實踐:
- 使用Docker容器化服務
- 採用Docker Compose執行測試
準則:
- 構建適合團隊的持續部署流水線
- 版本化一切
- 容器化一切
使用Docker容器化服務
我們在構建和釋出服務的時候,不僅要釋出服務本身,還需要為其配置伺服器環境。使用Docker容器化微服務,可以讓我們不僅釋出服務,同時還發布其需要的執行環境。容器化之後,我們可以基於Docker構建我們的持續部署流水線:
上圖描述了一個基於Ruby on Rails(簡稱:Rails)服務的持續部署流水線。我們用Dockerfile配置Rails專案執行所需的環境,並將Dockerfile和專案同時放在Git程式碼倉庫中進行版本管理。下面Dockerfile可以描述一個Rails專案的基礎環境:
FROM ruby:2.3.3
RUN apt-get update -y && \
apt-get install -y libpq-dev nodejs git
WORKDIR /app
ADD Gemfile /app/Gemfile
ADD Gemfile.lock /app/Gemfile.lock
RUN bundle install
ADD . /app
EXPOSE 80
CMD ["bin/run"]
在持續整合伺服器上會將專案程式碼和Dockerfile同時下載(git clone)下來進行構建(Build Image)、單元測試(Testing)、最終釋出(Publish)。此時整個構建過程都基於Docker進行,構建結果為Docker Image,並且將最終釋出到Docker Registry。
在部署階段,部署機器只需要配置Docker環境,從Docker Registry上Pull Image進行部署。
在服務容器化之後,我們可以讓整套持續部署流水線只依賴Docker,並不需要為環境各異的服務進行單獨配置。
使用Docker Compose執行測試
在整個持續部署流水線中,我們需要在持續整合伺服器上部署服務、執行單元測試和整合測試Docker Compose為我們提供了很好的解決方案。
Docker Compose可以將多個Docker Image進行組合。在服務需要訪問資料庫時,我們可以通過Docker Compose將服務的Image和資料庫的Image組合在一起,然後使用Docker Compose在持續整合伺服器上進行部署並執行測試。
上圖描述了Rails服務和Postgres資料庫的組裝過程。我們只需在專案中額外新增一個docker-compose.yml來描述組裝過程:
db:
image: postgres:9.4
ports:
- "5432"
service:
build: .
command: ./bin/run
volumes:
- .:/app
ports:
- "3000:3000"
dev:
extends:
file: docker-compose.yml
service: service
links:
- db
environment:
- RAILS_ENV=development
ci:
extends:
file: docker-compose.yml
service: service
links:
- db
environment:
- RAILS_ENV=test
採用Docker Compose執行單元測試和整合測試:
docker-compose run -rm ci bundle exec rake
構建適合團隊的持續部署流水線
當我們的程式碼提交到程式碼倉庫後,持續部署流水線應該能夠對服務進行構建、測試、並最終部署到生產環境。
為了讓持續部署流水線更好的服務團隊,我們通常會對持續部署流水線做一些調整,使其更好的服務於團隊的工作流程。例如下圖所示的,一個敏捷團隊的工作流程:
通常團隊會有業務分析師(BA)做需求分析,業務分析師將需求轉換成適合工作的使用者故事卡(Story Card),開發人員(Dev)在拿到新的使用者故事卡時會先做分析,之後和業務分析師、技術主管(Tech Lead)討論需求和技術實現方案(Kick off)。
開發人員在開發階段會在分支(Branch)上進行開發,採用Pull Request的方式提交程式碼,並且邀請他人進行程式碼評審(Review)。在Pull Request被評審通過之後,分支會被合併到Master分支,此時程式碼會被自動部署到測試環境(Test)。
在Microservices場景下,本地很難搭建一整套整合環境,通常測試環境具有完整的整合環境,在部署到測試環境之後,測試人員(QA)會在測試環境上進行測試。
測試完成後,測試人員會跟業務分析師、技術主管進行驗收測試(User Acceptance Test),確認需求的實現和技術實現方案,進行驗收。驗收後的使用者故事卡會被部署到生產環境(Production)。
在上述團隊工作的流程下,如果持續部署流水線僅對Master分支進行打包、測試、釋出。在開發階段(即:程式碼還在分支)時,無法從持續整合上得到反饋,直到程式碼被合併到Master並執行構建後才能得到反饋,通常會造成“本地測試成功,但是持續整合失敗”的場景。
因此,團隊對僅基於Master分支的持續部署流水線做一些改進。使其可以支援對Pull Request程式碼的構建:
如上圖所示:
- 持續部署流水線區分Pull Request和Master。Pull Request上只執行單元測試,Master執行完成全部構建並自動將程式碼部署到測試環境。
- 為生產環境部署引入手動操作,在驗收測試完成之後再手動觸發生產環境部署。經過調整後的持續部署流水線可以使團隊在開發階段快速從持續整合上得到反饋,並且對生產環境的部署有更好的控制。
版本化一切
版本化一切,即將服務開發、部署相關的系統都版本化控制。我們不僅將專案程式碼納入版本管理,同時將專案相關的服務、基礎設施都進行版本化管理。 對於一個服務,我們一般會為它單獨配置持續部署流水線,為它配置獨立的用於執行的基礎設施。此時會涉及兩個非常重要的技術實踐:
- 構建流水線即程式碼
- 基礎設施即程式碼
構建流水線即程式碼。通常我們使用Jenkins或者Bamboo來搭建配置持續部署流水線,每次建立流水線需要手動配置,這些手動操作不易重用,並且可讀性很差,每次對流水線配置的改動並不會儲存在歷史記錄中,也就是說我們無從追蹤配置的改動。
在今年上半年,團隊將所有的持續部署流水線從Bamboo遷移到了BuildKite,BuildKite對構建流水線即程式碼有很好的支援。下圖描述了BuildKite的工作方式:
在BuildKite場景下,我們會在每個服務程式碼庫中新增一個pipeline.yml來描述構建步驟。構建伺服器(CI Service)會從專案的pipeline.yml中讀取配置,生成構建步驟。例如,我們可以使用如下程式碼描述流水線:
steps:
-
name: "Run my tests"
command: "shared_ci_script/bin/test"
agents:
queue: test
- wait
-
name: "Push docker image"
command: "shared_ci_script/bin/docker-tag"
branches: "master"
agents:
queue: test
- wait
-
name: "Deploy To Test"
command: "shared_ci_script/bin/deploy"
branches: "master"
env:
DEPLOYMENT_ENV: test
agents:
queue: test
- block
- name: "Deploy to Production"
command: "shared_ci_script/bin/deploy"
branches: "master"
env:
DEPLOYMENT_ENV: production
agents:
queue: production
在上述配置中,command中的步驟(即:test、docker-tag、deploy)分別是具體的構建指令碼,這些指令碼被放在一個公共的shared_ci_script程式碼庫中,shared_ci_script會以git submodule的方式被引入到每個服務程式碼庫中。
經過構建流水線即程式碼方式的改造,對於持續部署流水線的任何改動都會在Git中被追蹤,並且有很好的可讀性。
基礎設施即程式碼。對於一個基於HTTP協議的API服務基礎設施可以是:
- 用於部署的機器
- 機器的IP和網路配置
- 裝置硬體監控服務(CPU,Memory等)
- 負載均衡(Load Balancer)
- DNS服務
- AutoScaling Service(自動伸縮服務)
- Splunk日誌收集
- NewRelic效能監控
- PagerDuty報警
這些基礎設施我們可以使用程式碼進行描述,AWS Cloudformation在這方面提供了很好的支援。我們可以使用AWS Cloudformation設計器或者遵循AWS Cloudformation的語法配置基礎設施。下圖為一個服務的基礎設施構件圖,圖中構建了上面提到的大部分基礎設施:
在AWS Cloudformation中,基礎設施描述程式碼可以是JSON檔案,也可以是YAML檔案。我們將這些檔案也放到專案的程式碼庫中進行版本化管理。
所有對基礎設施的操作,我們都通過修改AWS Cloudformation配置進行修改,並且所有修改都應該在Git的版本化控制中。
由於我們採用程式碼描述基礎設施,並且大部分服務遵循相通的部署流程和基礎設施,基礎設施程式碼的相似度很高。DevOps團隊會為團隊建立屬於自己的部署工具來簡化基礎設施配置和部署流程。
容器化一切
通常在部署服務時,我們還需要一些輔助服務,這些服務我們也將其容器化,並使用Docker執行。下圖描述了一個服務在AWS EC2 Instance上面的執行環境:
在服務部署到AWS EC2 Instance時,我們需要為日誌配置收集服務,需要為服務配置Nginx反向代理。
按照12-factors原則,我們基於fluentd,採用日誌流的方式處理日誌。其中logs-router用來分發日誌、splunk-forwarder負責將日誌轉發到 Splunk。
在容器化一切之後,我們的服務啟動只需要依賴Docker環境,相關服務的依賴也可以通過Docker的機制執行。
總結
Microservices給業務和技術的擴充套件性帶來了極大的便利,同時在組織和技術層面帶來了極大的挑戰。由於在架構的演進過程中,會有很多新服務產生,持續部署是技術層面的挑戰之一,好的持續部署實踐和準則可以讓團隊從基礎設施抽離出來,關注與產生業務價值的功能實現。