本文作者:何文强——腾讯云 CODING 高级架构师。 负责 CODING DevOps产品解决方案架构设计和技术产品布道以及 CODING 云原生技术研究与落地实践。在多个技术大会担任演讲嘉宾,腾讯云 CODING DevOps 课程认证出品人,腾讯云云原生训练营核心初创成员。 精通敏捷精益、DevOps 和云原生领域,技术扎实,视野开阔,格局前瞻;在泛互、教育、工业、政务、金融等多个行业拥有数字化落地规划和实战经验;多年技术开发和团队管理经验,目前专注于一站式研发效能平台的建设和推广,聚焦于“以应用为中心“的云原生的落地与实践,致力于中国软件工程能力的提升和改进。
良好的实践需要遵循一定的原则,通过原则指导的实践才能行稳致远。在云原生应用交付中,可通过 The Twelve-Factor App(应用 12 因素)原则作为云原生应用交付实践的指南。
图5-1
应用 12 因素(图5-1)主要包括:#1基准代码;#2依赖;#3配置;#4后端服务;#5构建、发布、运行;#6进程;#7端口绑定;#8并发;#9易处理;#10开发环境与线上环境等价;#11日志;#12进程管理。接下来我们将详细介绍基于这 12 个原则的良好实践。
基准代码和应用之间总是保持一一对应的关系:
● 一旦有多个基准代码,就不能称为一个应用,而是一个分布式系统。分布式系统中的每一个组件都是一个应用,每一个应用可以分别使用 12-Factor 进行开发。
● 多个应用共享一份基准代码是有悖于 12-Factor 原则的。解决方案是将共享的代码拆分为独立的类库,然后使用 依赖管理 策略去加载它们。
在“一份基准代码,多份部署”的良好实践中(图5-2),为一个应用的每个模块创建一个代码仓库,选择 Master 分支作为基线,并以 Master 分支构建镜像, Master 分支构建出来的镜像部署在不同的环境中,即所有环境共享由 master 分支构建出来的镜像,如 dev 环境、pre-production 环境、production 环境共享同一镜像。在基准代码实践中,不同环境使用的镜像是同一份,但是不同环境的配置信息不一致,通过镜像与配置信息实现不同环境的部署。
大多数编程语言都会提供一个打包系统,用来为各个类库提供打包服务,就像 Perl 的 CPAN 或是 Ruby 的 Rubygems 。通过打包系统安装的类库可以是系统级的,或仅供某个应用程序使用,部署在相应的目录中。
12-Factor 规则下的应用程序不会隐式依赖系统级的类库。 它一定通过依赖清单 ,确切地声明所有依赖项。此外,在运行过程中通过依赖隔离工具来确保程序不会调用系统中存在但清单中未声明的依赖项。这一做法会统一应用到生产和开发环境。
在“显示声明依赖”的良好实践中(图5-3),Python 通过 requirements.txt 显示声明所需要的第三方类库和组件,并记录这些类库和组件的所有依赖和精确的版本号;Gradle 通过 build.gradle(Maven 通过 pom.xml)的 dependencies 显示声明所需要的第三方类库和组件的所有依赖和版本号。通过显示声明依赖,可以有效减少因错误的依赖版本导致的缺陷,提升软件供应链的可见性。
图5-3
通常,应用的 配置 在不同 部署 (预发布、生产环境、开发环境等等)间会有很大差异。这其中包括:
● 数据库,Memcached,以及其他后端服务的配置;
● 第三方服务的证书,如 Amazon S3、企业微信等;
● 每份部署特有的配置,如域名等。
有些应用在代码中使用常量保存配置,这与 12-Factor 所要求的代码和配置严格分离显然大相径庭。配置文件在各部署间存在大幅差异,代码却完全一致。
在“在环境中存储配置”的良好实践中(图5-4),将配置管理信息存储在 Git 仓库中,对配置进行版本化管理,不同环境的配置通过不同分支进行区分和管理,如 reviews 模块中,每个环境创建一个配置管理分支,如 test-folder-ref 分支存储 dev 环境的配置信息、test-base 分支存储 pre-production 环境的配置信息、master 分支存储 production 环境的配置信息,实现应用配置与运行环境的对应。
图5-4
在“在环境中存储配置”的良好实践中(图5-4),应将应用的配置存储于环境变量中。在 Kubernetes 应用中,可通过 ConfigMap 和 Secret 对象的 Key-Value 存储配置信息,为不同环境创建不同的 ConfigMap 和 Secret,不同环境的 ConfigMap 和 Secret 的 Key 相同,但是 Value 不同;在容器环境变量中引用 ConfigMap 和 Secret 的 Key,实现不同环境不同 Value 的注入,并在应用启动时,加载注入到容器环境的 Key,获取到对应环境的 Value。通过将应用的配置存储于环境变量中,可以有效管理不同环境的配置。
后端服务是指程序运行所需要的通过网络调用的各种服务,如数据库(MySQL),消息/队列系统(RabbitMQ),SMTP 邮件发送服务(Postfix),以及缓存系统(Redis)。
12-Factor 应用不会区别对待本地或第三方服务。 对应用程序而言,两种都是附加资源,通过一个 url 或是其他存储在配置中的服务定位/服务证书来获取数据。12-Factor 应用的任意 部署 ,都应该可以在不进行任何代码改动的情况下,将本地 MySQL 数据库换成第三方服务(例如 Amazon RDS)。
在“把后端资源当做附加资源”的良好实践中(图5-5),Review 服务所依赖的 MySQL 服务和 Redis 服务都是独立部署的,应用服务和附属的后端服务保持松耦合,通过协议的方式进行访问(MySQL 通过 MySQL 协议,Redis 通过 Redis 协议)。通过“把后端服务当做附加资源”,部署可以按需加载或卸载资源,如 MySQL 服务异常,可从备份中快速恢复,卸载当前数据库,然后加载新的数据库,而不需要修改代码。
图5-5
基准代码 转化为一份部署(非开发环境)需要以下三个阶段(图5-6-1):
● 构建阶段 是指将代码仓库转化为可执行包的过程。构建时会使用指定版本的代码,获取和打包 依赖项,编译成二进制文件和资源文件。
● 发布阶段 会将构建的结果和当前部署所需配置相结合,并能够立刻在运行环境中投入使用。
● 运行阶段 (或者说“运行时”)是指针对选定的发布版本,在执行环境中启动一系列应用程序 进程。
图5-6-1
在“严格分离构建和运行”的良好实践中(图5-6-2),代码提交到 CODING 代码库后,通过 CODING 持续集成将源代码打包构建为二进制文件,将二进制文件存储在 CODING 制品库中,二进制文件和配置信息组成发布包,通过 CODING 持续部署选择指定发布包进行程序发布。通过“严格分离构建和运行”,可以更好的实现职责和关注点的分离,同时将代码打包成二进制文件是一种不可变基础设施的体现,禁止通过 patch 等方式修改运行时的代码,有利于代码的同步和二进制文件的一致性和完整性。
图5-6-2
运行环境中,应用程序通常是以一个和多个进程运行的。
无状态应用程序是一种应用程序,它不会保存在一个会话中生成的客户端数据,以便在与该客户端的下一个会话中使用。每个会话都像第一次一样进行,响应不依赖于前一个会话的数据。相反,有状态应用程序保存有关每个客户端会话的数据,并在客户端下次发出请求时使用该数据。
12-Factor 应用的进程必须无状态且无共享。任何需要持久化的数据都要存储在后端服务内,比如数据库。
在“以一个或多个无状态进程运行应用”的良好实践中(图5-7),在应用设计时需要将应用设计为无状态应用程序,同时在 Kubernetes 中采用 Deployment 对象进行声明式部署。
图5-7
12-Factor 应用完全自我加载 而不依赖于任何网络服务器就可以创建一个面向网络的服务。互联网应用通过端口绑定来提供服务 ,并监听发送至该端口的请求。
将网络服务器类库通过依赖声明载入应用。例如,Python 的 Tornado, Ruby 的Thin , Java 以及其他基于 JVM 语言的 Jetty。完全由用户端 ,确切的说应该是应用的代码发起请求和运行环境约定好绑定的端口即可处理这些请求。
在“通过端口绑定提供服务”的良好实践中,应在 Dockerfile 中指定端口,该端口与应用绑定的端口一致,在镜像构建时,会将 Dockerfile 中指定的端口进行暴露和通信。应用访问可直接通过 Dockerfile 构建的镜像暴露的端口进行访问。在云原生环境下,可在 Kubernetes 的 yaml 文件中的 containerPort 绑定 Dockerfile 中暴露的端口,实现 Kubernetes 资源与 Docker 镜像端口的绑定。端口绑定这种方式也意味着一个应用可以成为另外一个应用的 后端服务 ,调用方将服务方提供的相应 URL 当作资源存入配置以备将来调用
图5-8
任何计算机程序,一旦启动,就会生成一个或多个进程(图5-9-1)。互联网应用采用多种进程运行方式。
在 12-factor 应用中,进程是一等公民。12-Factor 应用的进程主要借鉴于 unix 守护进程模。12-Factor 应用的进程所具备的无共享,水平分区的特性 意味着添加并发会变得简单而稳妥。这些进程的类型以及每个类型中进程的数量就被称作进程构成
图5-9-1
在“通过进程模型进行扩展”的良好实践中(图5-9-2),面对并发,提倡通过水平扩展进程的数量(增加应用的副本数量,即部署多个相同资源的进程),而非垂直扩展进程的容量(如增加单一进程的CPU、内存等资源)。在云原生应用中,可充分利用Kubernetes的水平缩扩容的能力。通过设置repilcas的值来调整副本的数量,这种水平(横向)扩展进程的方式让并发变得简单、高效和安全。
图5-9-2
12-Factor 应用的进程是易处理(disposable)的,意思是说它们可以瞬间开启或停止。 这有利于快速、弹性的伸缩应用,迅速部署变化的代码或配置 ,稳健的部署应用。
进程应当追求最小启动时间 。理想状态下,进程从敲下命令到真正启动并等待请求的时间应该只需很短的时间。更少的启动时间提供了更敏捷的发布以及扩展过程,此外还增加了健壮性,因为进程管理器可以在授权情形下容易的将进程搬到新的物理机器上。
进程一旦接收终止信号(SIGTERM) 就会优雅的终止 。就网络进程而言,优雅终止是指停止监听服务的端口,即拒绝所有新的请求,并继续执行当前已接收的请求,然后退出。
在“快速启动”的良好实践中(图5-10),采用 Docker 镜像方式进行应用打包,Docker 镜像中包含应用本身及其所有的运行时依赖,能够快速复制到新环境中,并能够快速进行应用的部署。同时,Docker 镜像具备强大的跨平台和可移植性,可以快速将进程迁移到新的服务器上。
在“优雅停机”良好实践中(图5-10),有两个层面可进行设置,第一个层面是应用层面,若采用 Springboot 框架开发的应用,可在 Springboot 中进行优雅停机参数配置,通过设置 shutdown: graceful 即可实现应用的优雅终止。第二个层面是 Kubernetes 层面,在 Kubernetes yaml 配置中,可以通过 lifecycle 生命周期的 preStop 钩子,设置 terminationGracePeriodSeconds,即可实现容器镜像的优雅终止。通过在应用配置层面和 Kubernetes yaml 中的镜像层面进行优雅终止设置,可实现应用优雅停机。
图5-10
从以往经验来看,开发环境(即开发人员的本地 部署)和线上环境(外部用户访问的真实部署)之间存在着很多差异。这些差异表现在以下三个方面:
● 时间差异: 开发人员正在编写的代码可能需要几天,几周,甚至几个月才会上线。 ● 人员差异: 开发人员编写代码,运维人员部署代码。 ● 工具差异: 开发人员或许使用 Nginx,SQLite,OS X,而线上环境使用 Apache,MySQL 以及 Linux。
12-Factor 应用想要做到 持续部署 就必须缩小本地与线上差异
传统应用 | 12-factor 应用 | |
---|---|---|
每次部署间隔 | 数周 | 几小时 |
开发人员 vs 运维人员 | 不同的人 | 相同的人 |
开发环境 vs 线上环境 | 不同 | 尽量接近 |
在“尽可能保持开发环境,预发环境,线上环境相同”的良好实践中(图5-11)。在时间差异上,通过 CODING CI/CD 流水线缩短部署实践,并且可以采用自动化的方式,加速应用的交付速度,降低每次部署间隔实践;在人员差异上,采用谁开发,谁构建,谁运行原则,通过设置开发、构建、发布运行权限,实现开发运维一体化,授权团队具备端到端交付能力;在工具差异上,反对在不同环境中使用不同的后端服务,尽最大努力消除使用上的差异,同时使用 IaC(基础设施即代码)工具(如Terraform)进行各类环境资源的创建和维护,并将 IaC 配置信息存储到代码库中,在 CI/CD 根据 IaC 配置自动创建环境资源,实现各类环境的等价。
图5-11
日志使得应用程序运行的动作变得透明。在基于服务器的环境中,日志通常被写在硬盘的一个文件里,但这只是一种输出格式。
日志应该是事件流的汇总,将所有运行中进程和后端服务的输出流按照时间顺序收集起来。尽管在回溯问题时可能需要看很多行,日志最原始的格式确实是一个事件一行。日志没有确定开始和结束,但随着应用在运行会持续的增加。
12-factor 应用本身从不考虑存储自己的输出流。 不应该试图去写或者管理日志文件。相反,每一个运行的进程都会直接的标准输出(stdout)事件流。开发环境中,开发人员可以通过这些数据流,实时在终端看到应用的活动。
在“把日志当做事件流”的良好实践中(图5-12),应该使用 logstash 或 Loki 等工具以事件流的方式对日志进行输出,并将输出的事件流发送到 Splunk,ElasticSeach 等日志索引及分析系统中,统一对日志进行存储和检索(图5-12的良好实践)。日志信息不应该以文件的形式存储在运行节点的磁盘上(图5-12的不良实践)。
图5-12
进程构成(process formation)是指用来处理应用的常规业务(比如处理 web 请求)的一组进程。与此不同,开发人员经常希望执行一些管理或维护应用的一次性任务,例如:
● 运行数据移植,如数据库定期备份。
● 运行一个控制台 ,或是其他命令,如定期线上数据库状态检查。
● 运行一些提交到代码仓库的一次性脚本。如应用部署前运行数据库脚本
在“后台管理任务当做一次性进行运行”的良好实践中,应充分利用 Kubernetes 的 Job 和 CornJob 对象(图5-13)。对于只执行一次的后台管理任务,如应用部署前进行数据库表结构和表数据的导入,可以使用 Kubernetes Job 对象进行一次性进程的管理;对于重复性的后台管理任务,如每日凌晨两点对数据库进行备份,可以使用 Kubernetes CronJob 对象进行重复性进程的管理,通过设置 CrobJob 中的 schedule 的 Cron 表达式,即可实现重复性后台管理任务的执行。
图5-13