文/吕健
当我们讨论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给业务和技术的扩展性带来了极大的便利,同时在组织和技术层面带来了极大的挑战。由于在架构的演进过程中,会有很多新服务产生,持续部署是技术层面的挑战之一,好的持续部署实践和准则可以让团队从基础设施抽离出来,关注与产生业务价值的功能实现。