持續(xù)部署Microservices的實(shí)踐和準(zhǔn)則
當(dāng)我們討論Microservices架構(gòu)時(shí),我們通常會(huì)和Monolithic架構(gòu)(單體架構(gòu) )進(jìn)行比較。
在Monolithic架構(gòu)中,一個(gè)簡(jiǎn)單的應(yīng)用會(huì)隨著功能的增加、時(shí)間的推移變得越來(lái)越龐大。當(dāng)Monoltithic App變成一個(gè)龐然大物,就沒(méi)有人能夠完全理解它究竟做了什么。此時(shí)無(wú)論是添加新功能,還是修復(fù)Bug,都是一個(gè)非常痛苦、異常耗時(shí)的過(guò)程。
Microservices架構(gòu)漸漸被許多公司采用(Amazon、eBay、Netflix),用于解決Monolithic架構(gòu)帶來(lái)的問(wèn)題。
其思路是將應(yīng)用分解為小的、可以相互組合的Microservices。這些Microservices通過(guò)輕量級(jí)的機(jī)制進(jìn)行交互,通常會(huì)采用基于HTTP協(xié)議的服務(wù)。
每個(gè)Microservices完成一個(gè)獨(dú)立的業(yè)務(wù)邏輯,它可以是一個(gè)HTTP API服務(wù),提供給其他服務(wù)或者客戶端使用。也可以是一個(gè)ETL服務(wù),用于完成數(shù)據(jù)遷移工作。每個(gè)Microservices除了在業(yè)務(wù)獨(dú)立外,也會(huì)有自己獨(dú)立的運(yùn)行環(huán)境,獨(dú)立的開(kāi)發(fā)、部署流程。
這種獨(dú)立性給服務(wù)的部署和運(yùn)營(yíng)帶來(lái)很大的挑戰(zhàn)。因此持續(xù)部署(Continuous Deployment)是Microservices場(chǎng)景下一個(gè)重要的技術(shù)實(shí)踐。本文將介紹持續(xù)部署Microservices的實(shí)踐和準(zhǔn)則。
實(shí)踐:
- 使用Docker容器化服務(wù)
- 采用Docker Compose運(yùn)行測(cè)試
準(zhǔn)則:
- 構(gòu)建適合團(tuán)隊(duì)的持續(xù)部署流水線
- 版本化一切
- 容器化一切
1. 使用Docker容器化服務(wù)
我們?cè)跇?gòu)建和發(fā)布服務(wù)的時(shí)候,不僅要發(fā)布服務(wù)本身,還需要為其配置服務(wù)器環(huán)境。使用Docker容器化微服務(wù),可以讓我們不僅發(fā)布服務(wù),同時(shí)還發(fā)布其需要的運(yùn)行環(huán)境。容器化之后,我們可以基于Docker構(gòu)建我們的持續(xù)部署流水線:
上圖描述了一個(gè)基于Ruby on Rails(簡(jiǎn)稱(chēng):Rails)服務(wù)的持續(xù)部署流水線。我們用Dockerfile配置Rails項(xiàng)目運(yùn)行所需的環(huán)境,并將Dockerfile和項(xiàng)目同時(shí)放在Git代碼倉(cāng)庫(kù)中進(jìn)行版本管理。下面Dockerfile可以描述一個(gè)Rails項(xiàng)目的基礎(chǔ)環(huán)境:
- 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"]
在持續(xù)集成服務(wù)器上會(huì)將項(xiàng)目代碼和Dockerfile同時(shí)下載(git clone)下來(lái)進(jìn)行構(gòu)建(Build Image)、單元測(cè)試(Testing)、最終發(fā)布(Publish)。此時(shí)整個(gè)構(gòu)建過(guò)程都基于Docker進(jìn)行,構(gòu)建結(jié)果為Docker Image,并且將最終發(fā)布到Docker Registry。
在部署階段,部署機(jī)器只需要配置Docker環(huán)境,從Docker Registry上Pull Image進(jìn)行部署。
在服務(wù)容器化之后,我們可以讓整套持續(xù)部署流水線只依賴Docker,并不需要為環(huán)境各異的服務(wù)進(jìn)行單獨(dú)配置。
2. 使用Docker Compose運(yùn)行測(cè)試
在整個(gè)持續(xù)部署流水線中,我們需要在持續(xù)集成服務(wù)器上部署服務(wù)、運(yùn)行單元測(cè)試和集成測(cè)試Docker Compose為我們提供了很好的解決方案。
Docker Compose可以將多個(gè)Docker Image進(jìn)行組合。在服務(wù)需要訪問(wèn)數(shù)據(jù)庫(kù)時(shí),我們可以通過(guò)Docker Compose將服務(wù)的Image和數(shù)據(jù)庫(kù)的Image組合在一起,然后使用Docker Compose在持續(xù)集成服務(wù)器上進(jìn)行部署并運(yùn)行測(cè)試。
上圖描述了Rails服務(wù)和Postgres數(shù)據(jù)庫(kù)的組裝過(guò)程。我們只需在項(xiàng)目中額外添加一個(gè)docker-compose.yml來(lái)描述組裝過(guò)程:
- 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運(yùn)行單元測(cè)試和集成測(cè)試:
- docker-compose run -rm ci bundle exec rake
3. 構(gòu)建適合團(tuán)隊(duì)的持續(xù)部署流水線
當(dāng)我們的代碼提交到代碼倉(cāng)庫(kù)后,持續(xù)部署流水線應(yīng)該能夠?qū)Ψ?wù)進(jìn)行構(gòu)建、測(cè)試、并最終部署到生產(chǎn)環(huán)境。
為了讓持續(xù)部署流水線更好的服務(wù)團(tuán)隊(duì),我們通常會(huì)對(duì)持續(xù)部署流水線做一些調(diào)整,使其更好的服務(wù)于團(tuán)隊(duì)的工作流程。例如下圖所示的,一個(gè)敏捷團(tuán)隊(duì)的工作流程:
通常團(tuán)隊(duì)會(huì)有業(yè)務(wù)分析師(BA)做需求分析,業(yè)務(wù)分析師將需求轉(zhuǎn)換成適合工作的用戶故事卡(Story Card),開(kāi)發(fā)人員(Dev)在拿到新的用戶故事卡時(shí)會(huì)先做分析,之后和業(yè)務(wù)分析師、技術(shù)主管(Tech Lead)討論需求和技術(shù)實(shí)現(xiàn)方案(Kick off)。
開(kāi)發(fā)人員在開(kāi)發(fā)階段會(huì)在分支(Branch)上進(jìn)行開(kāi)發(fā),采用Pull Request的方式提交代碼,并且邀請(qǐng)他人進(jìn)行代碼評(píng)審(Review)。在Pull Request被評(píng)審?fù)ㄟ^(guò)之后,分支會(huì)被合并到Master分支,此時(shí)代碼會(huì)被自動(dòng)部署到測(cè)試環(huán)境(Test)。
在Microservices場(chǎng)景下,本地很難搭建一整套集成環(huán)境,通常測(cè)試環(huán)境具有完整的集成環(huán)境,在部署到測(cè)試環(huán)境之后,測(cè)試人員(QA)會(huì)在測(cè)試環(huán)境上進(jìn)行測(cè)試。
測(cè)試完成后,測(cè)試人員會(huì)跟業(yè)務(wù)分析師、技術(shù)主管進(jìn)行驗(yàn)收測(cè)試(User Acceptance Test),確認(rèn)需求的實(shí)現(xiàn)和技術(shù)實(shí)現(xiàn)方案,進(jìn)行驗(yàn)收。驗(yàn)收后的用戶故事卡會(huì)被部署到生產(chǎn)環(huán)境(Production)。
在上述團(tuán)隊(duì)工作的流程下,如果持續(xù)部署流水線僅對(duì)Master分支進(jìn)行打包、測(cè)試、發(fā)布,在開(kāi)發(fā)階段(即:代碼還在分支)時(shí),無(wú)法從持續(xù)集成上得到反饋,直到代碼被合并到Master并運(yùn)行構(gòu)建后才能得到反饋,通常會(huì)造成“本地測(cè)試成功,但是持續(xù)集成失敗”的場(chǎng)景。
因此,團(tuán)隊(duì)對(duì)僅基于Master分支的持續(xù)部署流水線做一些改進(jìn)。使其可以支持對(duì)Pull Request代碼的構(gòu)建:
如上圖所示:
- 持續(xù)部署流水線區(qū)分Pull Request和Master。Pull Request上只運(yùn)行單元測(cè)試,Master運(yùn)行完成全部構(gòu)建并自動(dòng)將代碼部署到測(cè)試環(huán)境。
- 為生產(chǎn)環(huán)境部署引入手動(dòng)操作,在驗(yàn)收測(cè)試完成之后再手動(dòng)觸發(fā)生產(chǎn)環(huán)境部署。
經(jīng)過(guò)調(diào)整后的持續(xù)部署流水線可以使團(tuán)隊(duì)在開(kāi)發(fā)階段快速?gòu)某掷m(xù)集成上得到反饋,并且對(duì)生產(chǎn)環(huán)境的部署有更好的控制。
4. 版本化一切
版本化一切,即將服務(wù)開(kāi)發(fā)、部署相關(guān)的系統(tǒng)都版本化控制。我們不僅將項(xiàng)目代碼納入版本管理,同時(shí)將項(xiàng)目相關(guān)的服務(wù)、基礎(chǔ)設(shè)施都進(jìn)行版本化管理。 對(duì)于一個(gè)服務(wù),我們一般會(huì)為它單獨(dú)配置持續(xù)部署流水線,為它配置獨(dú)立的用于運(yùn)行的基礎(chǔ)設(shè)施。此時(shí)會(huì)涉及兩個(gè)非常重要的技術(shù)實(shí)踐:
- 構(gòu)建流水線即代碼
- 基礎(chǔ)設(shè)施即代碼
構(gòu)建流水線即代碼。通常我們使用Jenkins或者Bamboo來(lái)搭建配置持續(xù)部署流水線,每次創(chuàng)建流水線需要手動(dòng)配置,這些手動(dòng)操作不易重用,并且可讀性很差,每次對(duì)流水線配置的改動(dòng)并不會(huì)保存在歷史記錄中,也就是說(shuō)我們無(wú)從追蹤配置的改動(dòng)。
在今年上半年,團(tuán)隊(duì)將所有的持續(xù)部署流水線從Bamboo遷移到了BuildKite,BuildKite對(duì)構(gòu)建流水線即代碼有很好的支持。下圖描述了BuildKite的工作方式:
在BuildKite場(chǎng)景下,我們會(huì)在每個(gè)服務(wù)代碼庫(kù)中新增一個(gè)pipeline.yml來(lái)描述構(gòu)建步驟。構(gòu)建服務(wù)器(CI Service)會(huì)從項(xiàng)目的pipeline.yml中讀取配置,生成構(gòu)建步驟。例如,我們可以使用如下代碼描述流水線:
- 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)分別是具體的構(gòu)建腳本,這些腳本被放在一個(gè)公共的sharedciscript代碼庫(kù)中,sharedciscript會(huì)以git submodule的方式被引入到每個(gè)服務(wù)代碼庫(kù)中。
經(jīng)過(guò)構(gòu)建流水線即代碼方式的改造,對(duì)于持續(xù)部署流水線的任何改動(dòng)都會(huì)在Git中被追蹤,并且有很好的可讀性。
基礎(chǔ)設(shè)施即代碼。對(duì)于一個(gè)基于HTTP協(xié)議的API服務(wù)基礎(chǔ)設(shè)施可以是:
- 用于部署的機(jī)器
- 機(jī)器的IP和網(wǎng)絡(luò)配置
- 設(shè)備硬件監(jiān)控服務(wù)(CPU,Memory等)
- 負(fù)載均衡(Load Balancer)
- DNS服務(wù)
- AutoScaling Service(自動(dòng)伸縮服務(wù))
- Splunk日志收集
- NewRelic性能監(jiān)控
- PagerDuty報(bào)警
這些基礎(chǔ)設(shè)施我們可以使用代碼進(jìn)行描述,AWS Cloudformation在這方面提供了很好的支持。我們可以使用AWS Cloudformation設(shè)計(jì)器或者遵循AWS Cloudformation的語(yǔ)法配置基礎(chǔ)設(shè)施。下圖為一個(gè)服務(wù)的基礎(chǔ)設(shè)施構(gòu)件圖,圖中構(gòu)建了上面提到的大部分基礎(chǔ)設(shè)施:
在AWS Cloudformation中,基礎(chǔ)設(shè)施描述代碼可以是JSON文件,也可以是YAML文件。我們將這些文件也放到項(xiàng)目的代碼庫(kù)中進(jìn)行版本化管理。
所有對(duì)基礎(chǔ)設(shè)施的操作,我們都通過(guò)修改AWS Cloudformation配置進(jìn)行修改,并且所有修改都應(yīng)該在Git的版本化控制中。
由于我們采用代碼描述基礎(chǔ)設(shè)施,并且大部分服務(wù)遵循相通的部署流程和基礎(chǔ)設(shè)施,基礎(chǔ)設(shè)施代碼的相似度很高。DevOps團(tuán)隊(duì)會(huì)為團(tuán)隊(duì)創(chuàng)建屬于自己的部署工具來(lái)簡(jiǎn)化基礎(chǔ)設(shè)施配置和部署流程。
5. 容器化一切
通常在部署服務(wù)時(shí),我們還需要一些輔助服務(wù),這些服務(wù)我們也將其容器化,并使用Docker運(yùn)行。下圖描述了一個(gè)服務(wù)在AWS EC2 Instance上面的運(yùn)行環(huán)境:
在服務(wù)部署到AWS EC2 Instance時(shí),我們需要為日志配置收集服務(wù),需要為服務(wù)配置Nginx反向代理。
按照12-factors原則,我們基于fluentd,采用日志流的方式處理日志。其中l(wèi)ogs-router用來(lái)分發(fā)日志、splunk-forwarder負(fù)責(zé)將日志轉(zhuǎn)發(fā)到Splunk。
在容器化一切之后,我們的服務(wù)啟動(dòng)只需要依賴Docker環(huán)境,相關(guān)服務(wù)的依賴也可以通過(guò)Docker的機(jī)制運(yùn)行。
總結(jié)
Microservices給業(yè)務(wù)和技術(shù)的擴(kuò)展性帶來(lái)了極大的便利,同時(shí)在組織和技術(shù)層面帶來(lái)了極大的挑戰(zhàn)。由于在架構(gòu)的演進(jìn)過(guò)程中,會(huì)有很多新服務(wù)產(chǎn)生,持續(xù)部署是技術(shù)層面的挑戰(zhàn)之一,好的持續(xù)部署實(shí)踐和準(zhǔn)則可以讓團(tuán)隊(duì)從基礎(chǔ)設(shè)施抽離出來(lái),關(guān)注與產(chǎn)生業(yè)務(wù)價(jià)值的功能實(shí)現(xiàn)。