背景
采用快速迭代的开发方式的互联网公司常常一天之内要对线上业务做多次变更,如果在变更业务后出现了伤害用户体验或影响收入的意外情况,最简单快速的解决方法就是将线上业务回滚到变更前的状态。保证业务变更的可回滚性,对于控制风险和保障开发人员的安全感,都有重要的意义。本文介绍了几种保证业务变更可回滚的方法。
将代码和配置整体打包
如果将代码和配置分开部署,在回滚的时候就会遇到"应该是先回滚代码还是回滚配置"的难题,所以,要想轻松回滚,在部署的时候,一定要将代码和配置整体打包。在具体的实践方式上,我强烈建议采用容器化部署,在CI系统中将变更后的代码和配置build为一个docker image, 并给不同版本的容器打上不同的tag, 通过切换tag便能实现整体服务的快速回滚。
引入中间版本
对于单体应用,采用容器化的手段将代码和配置整体打包部署就能很好的保证可回滚性,但是,这仅是一个过度简化的模型,实际的线上系统则要复杂许多,一般都是由有依赖关系的多个子服务构成。例如,前端web系统会访问后端数据服务,后端数据服务会访问数据库,或者给批量作业系统下发任务。对于这种有依赖关系的变更,怎样开发和部署才能如何保证可回滚性?
首先,系统设计要保证调用的单向性,即如果服务A调用了服务B,那么服务B一定不会调用服务A。在满足单向调用的前提下,可以使用下列方法来保证可回滚性。
例如,有两个子服务A和B,服务A会调用服务B,业务需求需要同时将A变更为A'和将B变更为B'才能实现。
初始状态: A -> B
最终状态: A' -> B'
但是,由于A'和B以及A和B'的实现不兼容,所以不能出现A -> B'或A' -> B的中间状态。
解决办法:
- 将B变更为B'',B''可以同时兼容A和A',即A -> B''与A' -> B''均成立, 此时系统状态变为A -> B'',如果发现问题,可回滚成状态A -> B
- 将A变更为A', 此时系统状态变为A' -> B'',如果发现问题,可回滚成状态A -> B''
- 将B''变更为B', 此时系统状态变为A' -> B', 顺利抵达最终状态,并且切换过程平滑, 如果发现问题,可回滚成状态A' -> B''
综上可见,通过引入中间版本B'',就能保证对存在依赖关系的两个子服务的变更的可回滚性。
举个具体的例子,当前的用户登录系统只要求输入用户名和密码,由前端系统将用户名和密码发送给后端系统进行验证,现在,需要在用户登录时输入验证码。显然,这一改进需要前后端系统一起配合才能实现:
- 前端系统的界面要加入验证码的图片显示和输入框,并且调用验证接口时需加入验证码参数
- 后端系统的验证接口需提供对验证码参数的支持
应用前面的解决办法可以得出下面的开发和部署步骤:
- 修改后端系统的验证接口,加入对验证码参数的支持,但为了兼容不带验证码的前端系统(版本A),验证码参数是 可选 的
- 上线修改后的后端系统(版本B''),观察是否与A兼容,如果有问题,回滚到版本B
- 修改前端系统,显示验证码图片和表单字段,在调用验证接口时传入验证码参数
- 上线修改后的前端系统(版本A'),观察该版本是否工作正常,如果有问题,回滚到版本A
- 在前端A'和后端B''工作正常后,再修改后端系统的验证接口,将验证码参数由可选改为 必需
- 上线修改后的后端系统(版本B'),如果工作正常,业务变更完成;如果有问题,回滚到版本B''
数据迁移的处理
在业务变更涉及数据迁移时,应对数据表的字段采取"只增不删"的原则。原因很简单,当某个字段被当前代码引用的字段被删除后,线上业务是会出问题的,但新增一个没有被当前代码引用到的字段,则不会有问题。
等到确认新版代码工作完全正常,不会再回滚到旧版本时,才将旧字段删除。一旦旧字段被删除,引用到旧字段的旧版本代码就无法工作,也就无法回滚到旧版本了。
总结
- 使用容器化部署,代码和配置可以整体回滚
- 通过引入中间版本,让有依赖关系的系统可以分开独立回滚
- 数据迁移时,不能删除字段,要等到确认新版代码工作正常,无需回滚到旧版时才能删除