应用不停机发布的思考与初识
Editor's Note
The following article is from 技术奇妙物语 Author 陈俊
应用发布,简单来说就是将已开发完成的系统功能部署到生产环境,并可正常对用户提供服务。
传统的应用发布步骤一般采用“三步曲”:
第一步:停止应用
第二步:更新应用
第三步:启动应用
那你肯定会问,从停止应用一直到启动应用期间,系统功能是不是无法正常使用?
没错。在应用发布过程中,可能会出现页面白屏、访问超时等各种异常,而且一般会持续很久,所以发布时间基本上都集中在凌晨,讲究点的可能就配上一句友好提示“系统正在维护,请稍后访问!”。
这种情况大部分都出现在传统行业,原因可能是觉得没有必要,因为:
1.传统行业的业务一般都集中的日间,也就是说凌晨基本没有业务,或者非重要业务
2.就算凌晨无法使用这些功能,觉得也没关系,第二天再来就是了,客户又不会跑了
但真的是这样吗?如今越来越多的传统行业,都在向互联网服务模式转型,其服务主要特点:“全天”不间断为客户提供“优质”的“线上”服务,拆分一下关键词
1.“全天”代表着任何时间
2.“优质”代表着客户体验
3.“线上”代表着线上自助
所以说,一个优质的客户服务势必会对服务可用性提出更高的要求,而传统的停机发布不但会对客户造成使用影响,还会变向对企业造成非直接经济损失。
例如:客户体验下降导致的客户流失
仅此而已?非也!作为曾经也同样是一名运维工程师的我来说,凌晨发布家常便饭,还时不时来一次长达8小时的“长途之旅”,身体就直接被掏空,加上第二天还在“补血”中,还会被各种骚扰电话轰炸,这简直就是一场噩梦。
由此可见,应用不停机发布的重要性,通过它可以帮助我们:一是在应用发布过程中线上服务持续可用,提升服务可用性,二是可在白天或是任何时间发布,提升运维人员的机动性。
那么我们需要怎么做,才能实现应用不停机发布呢,那接下来就让我们进入正题。
01
应用不停机发布,从字面上很好理解,就是应用发布过程中服务不中断。
但仔细回想一下,原先应用发布过程中,反正服务会中断,那就在应用发布完成后,通过网络屏蔽外部流量的方式,先进行核心或常用功能的内部验证,在确保没有问题后,再解除网络屏蔽,并对外提供服务。
那现在呢?应用发布后直接对外?不需要进行内部或小流量验证?想必每个人的答案都有所不同。对此,可以对应用不停机发布能力,简单定义三个成熟度。
成熟度 | 成熟度能力 |
成熟度一级 | 应用发布过程服务不中断 |
成熟度二级 | 应用发布过程服务不中断,单应用功能可先进行内部/小流量验证 |
成熟度三级 | 应用发布过程服务不中断,多应用(联动)功能可先进行内部/小流量验证 |
注:不同的成熟度对技术能力的要求会有所不同,建议通过逐步演进的方式来提升成熟度,并在演进过程中对不同的技术能力进行验证,从而不断完善。
有人会说蓝绿发布,或有人会说滚动发布,灰度发布就可以实现以上能力。但其实,这些答案并不准确,或者说并不全面,它们虽有交集,但无法涵盖。
既然提到发布模式,那我们先来简单比较一下几种发布模式的优缺点。
蓝绿发布 | 滚动发布 | 灰度发布 | |
资源成本 | 生产等比例资源投入,成本较高 | 基本无需额外资源投入,成本较低 | 基本无需额外资源投入,成本较低 |
回滚时长 | 两套环境直接切换,时长较短 | 根据滚动速率决定,时长较长 | 灰度部分实例回滚,时长适中 |
技术难点 | 资源完全隔离,技术难点较低 | 需制定滚动策略,技术难点适中 | 引入流量路由能力,技术难点较高 |
影响范围 | 影响所有用户,影响面较大 | 影响所有用户,影响面较大 | 影响少量用户,影响面较小 |
注:大部分的技术文章里都会提到,可通过以上任何一种发布模式,做到用户无感知的应用发布,但在实际情况下,并不完全足够,还需要通过一些辅助手段组合在一起才能实现。
结合以上发布模式的优缺点,如果你的组织在基础架构技术能力上已经非常成熟,那么灰度发布一定是最佳之选,但如果还处于初级阶段,那可能并不是,而蓝绿发布的资源投入较高,也不是一种非常好的选择。
剩下的只有滚动发布,但滚动发布的发布策略会比较复杂,特别在容器环境下,同一应用多个服务实例的滚动策略还需要单独来实现。
所以,前期可以选择一种折中的方式,即:单独搭建一套预发验证环境,应用架构需对齐生产环境,但容量方面可以最小化,一般一个应用至少2个服务实例。
这样,我们就可以在对生产环境做应用发布前,事先对生产预验证环境进行应用发布,发布后仅对内部用户可见,验证通过后,再对生产环境采用全量应用发布。
02
应用不停机发布是一项综合性能力,当明确好一种发布模式后,就需要逐步识别会涉及到哪些技术组件,以及明确技术组件在整个解决方案中所担任的职责边界,从而使它们能够相互协同工作。
如下列举了一些主要的技术组件:
技术组件 | 职责边界 |
应用管理平台 | 主要负责控制整个应用发布流程,以及集成并调度不同的技术组件,协同完成应用不停机发布 |
负载均衡(4层) | 主要负责服务请求的流量接入,可根据所识别的流量特征,负载分发到不同的负载均衡(7层) |
负载均衡(7层) | 主要负责服务请求的流量路由,可根据所识别的流量特征,路由分发到同一应用的不同实例 |
容器/虚拟机平台 | 主要负责容器/虚拟机资源管理,包括容器/虚拟机的创建、更新、销毁等 |
域名解析系统 | 主要负责域名地址管理,包括域名的注册、更新、解析等,以及提供用户访问应用或应用间访问等寻址能力 |
注册中心 | 主要负责服务资源管理,包括服务的注册、更新、注销等,以及提供服务请求方发现服务提供方的能力 |
在明确技术组件后,还需要对技术组件进行合理的架构规划,以适配不同的网络架构要求。
本次主要将对负载均衡进行特别说明,一方面它是负责处理流量的核心技术组件,另一方面网络架构的不同,对它的部署架构影响可能也是最大的。
在传统的网络架构环境中,出于对网络安全或其他考虑因素,通常会划分出多个不同的网络区域,网络区域间的访问需要通过开设防火墙访问策略才可以进行互通。
但这种方式必然会对应用服务间直接进行点对点访问的方式造成影响,主要原因是虚拟机或容器环境中,应用的IP地址可能会发生变化,导致无法提前明确防火墙访问策略。
所以,一般都会考虑使用负载均衡(代理模式)来解决这个问题。
建议前期优先选择方案三,虽然链路较长会小幅影响性能,但此部署方案相对较为成熟,一方面可以避免流量负载不均的问题,另一方面对于应用的改造成本也会相对较低。
注:除网络区域隔离会遇到这种情况外,在某些网络架构中,不同的容器集群间也同样无法访问,所以也同样适用。
另外,除必要情况下,也应尽量减少跨网络区域或跨容器集群间的访问,例如:优先容器集群内路由访问,跨网络区域频繁交互的应用服务建议迁移至同一网络区域等。
负载均衡通常可以分为4层模式和7层模式,其中4层更关注流量负载,而7层更关注流量路由。
一般建议将负载均衡(4层)和负载均衡(7层)进行分层部署,以充分发挥它们的强项。
建议前期优先选择方案二,虽然无法实现多容器集群间的全局流量调度,但对于当前可观测性和排障能力还不够健全的组织,通过物理隔离降低运维难度也是一种不错的选择。
综上架构决策,最终的全局流量链路大致如下,默认情况下,数据中心间流量隔离,即:单数据中心流量收敛,但可根据实际情况进行选择性放行。
03
应用不停机发布的基础是服务路由,在这里,我们可以把应用服务分为两种角色,服务请求方或服务提供方,而服务请求方通过不同的寻址方式,最终访问到服务提供方的形式,可以称之为服务路由。
服务路由可以分为南北向和东西向。
南北向:服务请求方—>负载均衡(4层)—>负载均衡(7层)—>服务提供方
东西向:服务请求方—>服务提供方
不难发现,其主要区别就是,南北向服务请求方需借助负载均衡(代理模式)访问服务提供方,而东西向服务请求方直接访问服务提供方。
注:域名解析结果会缓存在本地,可缓解域名解析服务压力,但缓存时间应根据不同的服务水平进行评估,否则将会延长解析地址切换及故障转移时长。有条件的话建议采用httpdns来解决本地缓存问题。
但不管是南北向还是东西向,服务提供方在被访问前,得让服务请求方感知或可见,常见的方式主要有两种,一种是不依赖注册中心的,而另一种则是依赖注册中心的。
如果当前应用架构上暂未使用微服务框架,即:不依赖注册中心,服务提供方可以手动或自动将服务在负载均衡上进行服务注册,服务请求方直接调用负载均衡。
如果当前应用架构上已经使用微服务框架,即:依赖注册中心,服务提供方可以在注册中心上进行服务注册,服务请求方在注册中心进行服务发现,或者由负载均衡在注册中心进行服务发现,服务请求方直接调用负载均衡。
注:在多注册中心的场景下,可通过统一注册中心完成异构注册中心的服务发现
另外,我们还会对同一应用的不同服务实例进行分组,分组策略可根据不同的条件来决定,例如:不同环境、不同版本等。
假设根据不同环境(预生产环境和生产环境)这个条件,可以把部署在预发验证环境的应用服务分为预发组,把部署在生产环境的应用服务分为线上组。
然后通过配置路由策略,下发到负载均衡或应用服务,就可以实现简单的服务路由了。
例如:不同分组间默认情况下不允许服务路由,但特殊场景下允许预发组服务路由至线上组。
注:若服务请求方是前端页面或客户端,也可以对前端流量也进行分组,例如:根据网络环境,将接入公司WI-FI网络环境的流量识别成预发组
04
应用不停机发布的核心是应用如何优雅停止,光有服务路由可能还不够,它虽然已经解决了大部分问题,但离成功还差最后一步。
前面我们提到过应用发布要做到用户无感知,那如果应用发布过程中出现瞬断或短时间中断,而用户又正好在使用,那算不算用户就感知到了呢?
不过,如果你觉得线上服务中断5分钟也可以忍受,那我只能呵呵了。但我相信大部分具备互联网服务模式的头部公司,别说发布一次服务停止5分钟,可能就连5秒钟也无法忍受。
那我们先来分析一下为什么会出现瞬断或短时间中断:
1.服务请求处理还没完成,应用就被强行停止(例如:运维必备大招之kill -9 进程),导致处理中的请求无法正常返回
2.服务提供方虽然已停止,但未通知服务请求方,服务请求方仍然继续访问已停止的服务提供方,导致出现异常
针对以上两种情况,还需要分南北向和东西向进行讨论。
在南北向,服务请求方与服务提供方间是通过负载均衡进行服务路由,所以,在应用发布时,需要在负载均衡上进行一些特殊处理。
1.在负载均衡上,逐一对应用服务实例进行流量屏蔽,该屏蔽只会影响新的请求,等待服务请求处理完成后,再执行如下操作:
容器:销毁服务实例,创建新版本服务实例
虚拟机:更新服务实例,更新后解除屏蔽
注:商业化的负载均衡一般都支持优雅下线/上线
2.因服务请求方的请求都会先经过负载均衡,而负载均衡的成员节点只要确保至少有1个服务实例存在即可,该场景不需要通知,因此不适用。
在东西向,服务请求方与服务提供方间是直接进行服务路由的,所以,在应用发布时,需要分别在服务请求方和服务提供方进行一些特殊处理,而这些处理方式受限于应用框架,这里先介绍一种常用框架,即:Springboot+Eureka应用框架
1.在服务提供方上实现Shutdown hook,即:等待服务请求处理完成后,再进行应用停止。
2.在服务提供方停止前,需通知所有服务请求方,并在完成通知后再进行应用停止。
其中第2点看上去似乎很简单,但实际上会比较复杂,对此我做了一些降级,用于权衡收益价值和改造成本。
实现效果 | 改造成本 |
方式一:最多中断5秒 | 对容器/虚拟机平台进行少量改造 |
方式二:不中断(不通知) | 对容器/虚拟机平台进行改造 |
方式三:不中断(通知) | 对容器/虚拟机平台进行改造 对注册中心/统一注册中心进行改造 |
方式一:最多中断5秒
step1:依赖spring-boot-starter-actuator组件,暴露/shutdown端点
management:
endpoint:
shutdown:
enabled: true
endpoints:
web:
exposure:
include: shutdown
step2:调整Eureka配置参数,该调整会分别增加Eureka客户端和服务端性能压力
配置项 | 配置描述 | 缺省值 | 建议值 |
eureka.client.registry-fetch-interval-seconds | Eureka客户端获取服务注册信息频率 | 30 | 4 |
ribbon.ServerListRefreshInterval | Eureka客户端ribbon刷新时间 | 30 | 1 |
eureka.server.useReadOnlyResponseCache | Eureka服务端是否使用只读缓存 | true | false |
step3:容器销毁服务实例或虚拟机更新服务实例前,请求如下地址进行应用服务实例停止
curl -X http://应用服务地址/actuator/shutdown
方式二:不中断(不通知)
step1:依赖spring-boot-starter-actuator组件,暴露/shutdown和/service-registry端点
management:
endpoint:
shutdown:
enabled: true
endpoints:
web:
exposure:
include: shutdown,service-registry
step2:调整Eureka配置参数,该调整会分别增加Eureka客户端和服务端性能压力
配置项 | 配置描述 | 缺省值 | 建议值 |
eureka.client.registry-fetch-interval-seconds | Eureka客户端获取服务注册信息频率 | 30 | 4 |
ribbon.ServerListRefreshInterval | Eureka客户端ribbon刷新时间 | 30 | 1 |
eureka.server.useReadOnlyResponseCache | Eureka服务端是否使用只读缓存 | true | false |
step3:容器销毁服务实例或虚拟机更新服务实例前,请求如下地址进行应用服务实例下线
curl -X http://应用服务地址/actuator/service-registry?status=DOWN
注:此时,服务提供方仍然可以提供服务,但再次获取服务实例的服务请求方不会再获取该服务实例
step4:服务请求方最多等待5秒后会更新服务列表,所以,在完成以上操作后,可以休眠5秒钟,再请求如下地址停止应用服务实例
curl -X http://应用服务地址/actuator/shutdown
注:eureka.client.registry-fetch-interval-seconds+ribbon.ServerListRefreshInterval=5秒
方式三:不中断(通知)
step1:依赖spring-boot-starter-actuator组件,暴露/shutdown和/service-registry端点
management:
endpoint:
shutdown:
enabled: true
endpoints:
web:
exposure:
include: shutdown,service-registry
step2:调整Eureka配置参数,该调整会增加Eureka服务端性能压力
配置项 | 配置描述 | 缺省值 | 建议值 |
eureka.server.useReadOnlyResponseCache | Eureka服务端是否使用只读缓存 | true | false |
step3:容器销毁服务实例或虚拟机更新服务实例前,请求如下地址进行应用服务实例下线
curl -X http://应用服务地址/actuator/service-registry?status=DOWN
注:此时,服务提供方仍然可以提供服务,但再次获取服务实例的服务请求方不会再获取该服务实例
step4:在注册中心上记录当前正订阅该服务实例的服务请求方列表,并根据列表通知它们立即重新获取最新的服务实例,通知完成后再请求如下地址停止应用服务实例
curl -X http://应用服务地址/actuator/shutdown
以上三种方式,可以结合价值收益和改造成本进行权衡,接受瞬断的可以选择方式一,而对技术有极致追求的可以选择方式三,如果两个都不是,那就选择方式二吧。
注:每个应用服务的Eureka配置参数并不一定能够完全统一,这样可能就会造成大量配置管理成本的增加,但如果可以统一,那方式二还是不错的选择。
05
在具备以上条件能力后,应用发布不停机的基本框架已成型,但这样应用发布就能实现不停机了?
那有这么简单,我们在开发上还需要遵循一些规范,但符合这些规范的话,可能会增加我们的一些开发成本。
因此,并不是说每次应用发布都强行需要实现不停机发布,而是应该进行合理的取舍。不过,一个好的系统设计必然能做到既能遵循开发规范,也不会增加太多开发成本。
如下列举了一些常用的开发规范,实际情况可按需调整,其目的是为了不管是接口更新还是数据库更新等,都应尽量做到向上兼容。
涉及方面 | 开发规范及原则 |
接口 | 1.允许新增字段,必填字段需要设置缺省值 2.允许原有字段扩展长度或新增字典值 3.不允许修改原有字段的语义及格式 4.不允许删除原有字段 5.无法兼容时,应新增接口; 6.接口下线前,需确保无调用方; |
数据库 | 1.允许新增字段,必填字段需要设置缺省值 2.允许原有字段扩展长度或新增字典值 3.不允许修改原有字段的语义及格式 4.不允许删除原有字段; 5.无法兼容时,应新增表; 6.新老表并存期间,数据统计需聚合处理; 7.老表下线前,需进行数据迁移; |
消息 | 1.优先考虑新老格式兼容; 2.无法兼容时,生产者和消费者可共同约定新的主题; 3.若生产者无法约定新的主题,消费者可增加消息分发层进行主题重命名; |
缓存 | 1.优先考虑新老格式兼容; 2.无法兼容时,则需要对不同的业务逻辑进行额外特殊处理; |
写在最后
感谢你可以很耐心的读到这里,整篇文章主要围绕应用不停机发布进行了思考,从为什么要做,能带来什么价值,一直到应该怎么做,要关心哪些方面,进行了简要说明,主要目的是为了能让大家,对应用不停机发布有一个大概的框架认识。
实现应用不停机发布的手段,也并非仅有文章中说的那些方式,涉及到的技术组件可能也不止这些,但其解决思路基本都差不多,具体的技术实现方式,可以结合自身的架构环境再进行适配和调整。
今天就先写到这里,后续我会针对在应用不停机发布中所涉及的技术组件,逐一进行深入研究,敬请期待!
本文是第一次尝试文章输出,也是受右导(于君泽)的激发,还有周围存在像头哥(王晔倞)一样的正能量存在,非常感谢。如有错误或不妥的地方还请多多包涵及指正。
期待同样追求技术的你,可以一起探讨与交流