背景
在软件开发和运维领域,变更管理是一个至关重要的环节。无论是对现有系统的改进、功能的增加还是修复漏洞,变更都是不可避免的。这些变更可能涉及到软件代码的修改、配置的调整、服务器的扩容、三方jar包的变更等等。然而,变更的执行过程往往伴随着一系列的风险和挑战。变更管理对于确保系统的稳定性至关重要。只有通过有效的变更管理措施,如合理的变更计划、全面的测试和验证、及时的问题解决等,才能够最大限度地减少变更带来的风险,确保系统的稳定性和可靠性。因此,对于任何一个组织或团队来说,都需要高度重视变更管理,并将其作为稳定性建设的重要组成部分。
一、兼容设计
在变更管控各项变更中,如果考虑好兼容设计,其整体的变更就会比较平滑,整个变更的兼容设计会从硬件、软件、数据三个层面展开,其中软件部分还区分基础软件和应用软件,现在从以上部分展开对应的兼容设计需要考虑的原则如下描述。
1、硬件变更兼容设计
硬件平台变更,原则上不应该影响在其之上运行的应用服务(主机硬件平台升级,网络设备升级,存储设备升级,防火墙升级),所有硬件升级必须考虑线下兼容性,需要在线下环境进行详细的测试验证,保证生产系统变更稳定性。
2、基础软件变更兼容设计
任何基础技术和系统软件的升级,原则上不应该影响使用其的应用服务(框架,消息组件,缓存,存储中间件,操作系统,JVM,Apache,JBoss,Tomcat等),所有基础软件必须考虑线下兼容性,需要在线下进行严格并且详细的测试,保证生产系统变更稳定性。
案例1:mysql数据库5.5升级5.7风险点
1、MySQL版本从5.5升级到5.7,最大的风险在于数据精度不同,尤其体现在时间类型的精度方面。
2、timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' 类型在5.5版本中,create_time虽然定义为not null ,但是实际是能插入null值,会自动转换为current_time,5.6版本直接报错,拦截语句插入。
3、timestamp 时间类型在5.6.4版本后支持毫秒。例如:timestamp(3)即保留3位毫秒,如果不指,则不存储毫秒。
4、数据精度变化,5.5直接截断,5.6、5.7版本会按4舍5入取位。关于时间类型、数值类型、浮点类型请进行严格验证。
mysql更新到5.6.4 之后 , 新增了一个叫factional seconds的特性 , 可以记录时间的毫秒值.
但是目前的数据库是不记录毫秒值的 , 所以会产生一个java中时间的Milliseconds超过500就会四舍五入的问题.
例如一个时间是2015-08-31 23:59:59.520 , 毫秒值超过500 , 入库的时候 , 时间就会变成2015-09-01 00:00:00 . 下面的两条sql就可以看出效果.
insert into money_record
values(null,1711929, 'jerry1bean', NULL, NULL, 20.00, 2.00, 1250,
'2015-08-31 23:59:59.500000', NULL, NULL, NULL, 'just a test', NULL);
insert into money_record
values(null,1711929, 'jerry1bean', NULL, NULL, 20.00, 2.00, 1250,
'2015-08-31 23:59:59.499999', NULL, NULL, NULL, 'just a test', NULL);
5、5.5版本sql where 条件的值即使传入带毫秒,sql解析后也不带毫秒,5.7 查询sql解析后会带着毫秒,所以在范围查询时同样的sql查询的结果可能不同。
3、应用软件变更兼容设计
应用升级方案中应该考虑应用向下兼容能力,无法完全向下兼容的应用升级过程,在联调、预发布及正式上线过程中会引起已有业务服务的不可用,在关键业务路径上的一级服务如果发生不兼容现象后果更加严重,会直接导致变更过程中的大量业务处理中断,引起核心业务下跌。应用可向下兼容的评估点包括但不仅限于:服务接口、方法、入参、返回值及服务方法具体实现的向下兼容性能力;其中服务方法具体实现向下兼容是应用向下兼容能力的最核心表现。对于一、二级关键服务,应用升级过程中必须完全向下兼容,确保发布过程中不产生兼容性问题进而导致业务下跌或其他关键服务不可用。同时服务消费端设计上需要做到客户端可不要求同步升级。
案例1:JSF默认的Msgpack序列化,接口对象里增减字段如何处理?
【现象描述】:JSF默认的Msgpack序列化,接口对象里增减字段如何处理
【原因分析】:Msgpack是按字段顺序进行序列化和反序列化的,其缺点是无法改变字段顺序。
【解决方案】:
因Msgpack序列化不能改变字段顺序,所以在两边不同时升级的情况下,字段兼容规则如下:(包括Bean和枚举)
1、不要调整原有字段顺序,不能删减字段,除非是删最后一个字段。
2、新加的字段必须在字段最后面(只是字段顺序,不是文件最后面,getter/setter方法等随意)。
3、父类的字段不能变。因为父类一变相当于子类的中间插入一个字段。
满足上面规则,服务端和客户端哪边先升级都无所谓。
如果是需要父类加字段,或者中间加减字段这种,则需要服务端和调用端同时升级。
4、数据变更兼容设计
应用软件系统升级方案往往附有数据存储格式变更,良好的数据兼容性设计对升级后应用平稳上线起到重要的保障作用。数据兼容性设计要求设计方案遵循安全的增量变更原则,即在保障已有的数据存储结构不发生语义变化的前提下,合理增加升级应用所必须的数据列;并且所增加数据列不对已有业务服务造成影响,如外部系统所调用的查询服务不会中断、业务返回结果不变。原则上,当已有数据存储结构语义发生变化,原存储列所存储值业务含义发生变化时,应该通过新增存储列来完成,避免直接复用已有存储列或修改已有存储列名的做法。对于重要性高的服务,数据升级后必须完全向下兼容;确实无法做到数据向下兼容的,如原有存储列完全废弃的,应该首先确保外围使用系统业务改造完成后方可上线。
案例1:mysql表结构变更
背景:新需求上线,表结构需要增加个新的字段
1、代码上线前,提交sql语句如下,字段apply_type 为not null 未给默认值,导致不兼容线上逻辑
ALTER TABLE store_capacity_approve
ADD COLUMN apply_type tinyint(4) NOT NULL
COMMENT 'XXX类型' AFTER match_standard;
2、但由于代码未上线,导致线上报错Filed 'apply_type' doesn't hava a default value
3、修改sql,给apply_type该字段添加默认值,兼容线上代码逻辑
ALTER TABLE store_capacity_approve
MODIFY COLUMN apply_type tinyint(4) DEFAULT 0
COMMENT 'XXX申请类型';
二、新版本发布设计
1、停机性发布
原则上建议非高优先级系统不进行停机发布。对于高优先级系统,应在系统设计阶段尽量避免停机发布,如因系统拆分,数据库拆分,整体架构升级等原因一定需要停机,需严格限定停机范围、停机的时间点与停机时长。如需停机的系统及业务可以独立发布,则除这些系统外,其他系统尽量保障采取非停机平滑发布方式。如因系统耦合度或者业务耦合度复杂无法独立发布,则进行整体停机发布;
2、发布顺序是否合理
根据系统间依赖指定合适的发布先后顺序。系统发布顺序遵照以下原则:禁止系统启动依赖,无因系统启动依赖导致的发布顺序依赖;对于业务依赖,需保证无相互依赖。高优先级系统原则上不应该依赖于低优先级系统;其他系统默认无发布顺序,可以根据发布进度进行无序发布。
3、发布时间点
发布时间点需尽量避开业务高峰,尤其是发布过程会对业务产生影响的核心系统。系统发布因尽量避免影响业务,如确实对业务影响较大又无法在系统设计上避免,需将发布时间点放在绝对业务低峰点。
涉及新旧功能切换。验证切换方案地合理性,可逆性。发布过程中涉及到的新旧功能切换方案,应确保可逆,即切换失败后能及时切回到旧功能。方案需在研发环境进行详细测试,如无法在研发环境进行测试,需在预发布环境进行模拟测试,确保方案正确有效,可回滚。
三、灰度变更
1、灰度意义
1.我们在编码完成后会在测试环境中进行测试验证,这是为了找到问题和错误。当我们完成整个测试流程后,我们可以认为已知的问题已经得到了解决。由于测试环境与线上真实业务和用户环境是隔离的,所以测试环境中的问题不会对线上业务和用户造成影响。
2.灰度发布是为了验证我们的假设,即“还存在我们不知道的问题”。因此,在进行灰度发布时需要更加谨慎,确保即使问题在生产环境中出现,也能控制其对业务和用户的影响。通过灰度发布,我们可以逐步验证系统的稳定性和可靠性,减少风险并提高产品质量。
3.我们需要明确一点:灰度从来不是为了测试。它的主要目的是对抗“未知的不确定性”。在软件开发过程中,我们无法预测所有可能的问题和错误,因此需要通过灰度发布来验证系统的稳定性和可靠性。
4.在分布式系统中常见通用的灰度过程有 beta 发布、蓝绿发布,进行流量级别的灰度过程,能够满足绝大部分变更灰度验证需求。如果变更复杂度较高或者业务比较重要,在方案设计中也需要进行更精细变更影响面控制,例如按照影响用户维度逐步生效的设计,但要注意一次业务完整流程中开关一致性问题。
5.灰度发布是一种有效的风险管理方法,可以帮助我们在软件开发过程中识别和解决潜在的问题,提高产品质量和用户体验。
在灰度的落地与推进过程中,有效性非常重要。因为灰度是一个很耗时的复杂的过程。如果不注意的话,很容易出现“形式化”的情况,即只是表面上的灰度,而实际上并没有达到预期的效果。
为了确保灰度的有效性,需要注意以下几个方面:
1.制定详细的灰度计划:在进行灰度之前,应该制定详细的计划,包括灰度的范围、时间、节点等信息,以确保灰度过程的可控性和可预测性。
2.逐步推进灰度:在进行灰度时,应该逐步推进,而不是一下子全面铺开。比如,可以先在一个机房的一个分组中部分节点进行灰度,然后再扩大到全部节点和集群,最后再扩展到另外一个机房的相同步骤。
1.监控和反馈:在进行灰度时,应该及时监控和反馈,以发现和解决可能出现的问题和风险。关键点在于时间和流量
时间: 每个灰度阶段至少有 5 ~ 10 分钟的观察时间,这个时间可以根据业务系统的具体情况进行调整。在观察期间,需要密切关注监控、日志和各方反馈等信息,以发现和解决可能出现的问题和风险。只有当这些信息没有异常时,才能扩大灰度范围,进一步推广灰度计划。在灰度过程中,需要保持高度警惕和敏锐的洞察力,及时发现和解决问题,以保证系统的稳定和可靠性。
流量: 在进行灰度时,流量是一个非常重要的因素,需要特别注意。特别是对于一些业务场景,可能需要特定的触发条件才能进行灰度测试,比如只有满足某些条件的用户或订单才能参与测试。 在这种情况下,仅仅通过单位时间内是否存在异常来判断灰度是否成功是不足够的。还需要确保有足够的有效流量来触发这些特定的业务场景。否则,即使系统在灰度测试中没有出现异常,也不能完全保证系统在实际使用中的稳定性和可靠性。 因此,在进行灰度测试时,需要确保有足够的有效流量来触发这些特定的业务场景。同时,还需要注意监控和日志等信息,及时发现和解决可能出现的问题和风险。通过这种方式,可以更好地保证系统的稳定和可靠性,提高灰度测试的效果和价值。
有效的灰度可以把问题影响锁定在一个小范围内,但是同样也降低了问题的“明显性”,所以你要通过监控和日志更加仔细、谨慎地去寻找、观测异常并对比发现问题。灰度是一个复杂的过程,需要仔细考虑和规划。通过制定详细的计划、逐步推进和及时监控和反馈等措施,可以确保灰度的有效性和可持续性。
2、部署编排灰度
为解决用户手动部署操作耗时高、对人依赖度高、人工容易遗漏等导致线上问题痛点,强烈推荐您使用【部署编排】功能,用户可灵活制定部署策略,实现从编译构建到实例部署的自动化运行,提高部署效率!但部署编排第一次使用的时候需要验证好。
比如这分组每次发10%,具体分组灰度比例多少需根据业务特性而定。
四、数据迁移分析
发布过程所需的数据迁移方案,需事先在线下环境进行模拟演练,反复梳理迁移过程执行步骤,将可能发生的迁移风险降到最小。数据迁移方案的可行性包括:
方案的完整性:是否本次升级内容所必须包含的待迁移数据项全部覆盖到位。
方案的安全性:对于敏感信息如用户隐私信息的迁移方案,是否存在由于迁移脚本的不合理导致隐私信息泄露风险。
方案的可实施性:包括数据迁移操作方案的合理度(发布过程中完成或者发布前、发布中、发布后多阶段完成),相关角色配合实施步骤,同时必须考虑本项目的数据迁移方案所占用时间是否对同一发布窗口的其他项目造成影响。
方案的可检测性:迁移过程各个阶段的数据完整性、准确性检查脚本是否准备到位。
方案的可回滚性:迁移过程中各个阶段如果发生了计划外风险,必须要终止迁移操作的,是否具备了已迁移数据回滚能力。
涉及重要性高的服务的数据迁移方案必须完整、安全、可实施、可检测、可回滚。
案例:可参考 Redis渐进式rehash 兼容性
扩展或收缩哈希表需要将 ht[0]里面的所有键值对 rehash 到 ht[1]里面, 但是, 这个 rehash 动作并不是一次性、集中式地完成的, 而是分多次、渐进式地完成的。
这样做的原因在于,如果哈希表里保存的键值对数量很大时, 如:四百万、四千万甚至四亿个键值对, 那么一次性将这些键值对全部 rehash 到 ht[1] 的话,庞大的计算量(需要重新计算链表在桶中的位置)可能会导致服务器在一段时间内停止服务(redis是单线程的,如果全部移动会引起客户端长时间阻塞不可用)。
因此, 为了避免 rehash 对服务器性能造成影响, 服务器不是一次性将 ht[0]里面的所有键值对全部 rehash 到 ht[1], 而是分多次、渐进式地将 ht[0]里面的键值对慢慢地 rehash 到 ht[1]。
总结:渐进式 rehash 执行期间的哈希表操作
(1)删除和查找:在进行渐进式rehash的过程中,字典会同时使用ht[0]和ht[1]两个哈希表,所以在渐进式rehash进行期间,字典的删除、查找、更新等操作会在两个哈希表上进行。比如说,要在字典里面查找一个键的话,程序会先在ht[0]里面进行查找,如果没找到的话,就会继续到ht[1]里面进行查找,诸如此类
(2)新增数据:在渐进式 rehash 执行期间,新添加到字典的键值对一律会被保存到ht[1]里面,而ht[0]则不再进行任何添加操作。这一措施保证了ht[0]包含的键值对数量会只减不增,并随着rehash操作的执行而最终变成空表。
五、可回滚设计
1、制定回滚计划
故障恢复最好的手段是各种预案,而回滚则是预案中最普遍、也最有效的。
回滚的必要性:应用上线应该制定详尽的回滚计划,能够在最短时间内将应用恢复至上一稳定运行版本;然而系统并不是天然可以无缝回滚的,想要系统具备回滚的能力,在设计与实现阶段需要付出额外的精力。可回滚的本质是系统的兼容性设计与实现,比如常见的“只增不改”,一个 API 内要调整很多实现逻辑才能满足新业务的需求,此时不妨直接新增一个 API ,两个 API 保持参数一致,那么一旦新 API 有异常直接通过开关技术切换回旧的 API 即可。一般情况下应用本身可回滚,而数据层面的可回滚性是重要的考量因素之一。遵循安全的增量变更原则所设计的数据变更方案具备可回滚能力,发布过程中所产生的增量数据列存储值要求可废弃。原则上任何应用服务在发布之前都必须具备可回滚的能力,没有回滚能力的系统不允许发布上线。
回滚操作对业务的影响:由于应用升级的回滚实施,必然会影响本次升级业务所服务的业务需求,同时会直接影响对本次升级有依赖的其他业务系统;回滚方案中必须明确本次发布窗口所有相关性需求项目,明确一旦发生回滚处理受影响范围,提前告知相关项目组及业务方,同时尽可能降低多个业务关联性较强项目同一发布窗口的回滚风险。
涉及重要性较高的服务应用升级方案要求必须提供回滚方案,且此回滚方案事先在线下环境得到完整模拟演练并确认可行;回滚完成后要求不得中断服务,业务运行正常
2、回滚原子性
回滚的复杂性:除应用本身及数据层面的可回滚性考虑外,若服务使用客户端已完成同步升级,则必须考量客户端的可回滚性;极端情况下,若客户端的本次同步升级也造成了其作为服务提供方的使用客户端同步升级,则存在多个应用系统复杂的连带可回滚需求;相关系统也需要评估其应用本身及其数据层面的可回滚能力,作为本次应用升级回滚方案的一并考虑项。在升级方案设计中,应该提前预知复杂回滚方案的实施成本,防止发生上述的同步升级的多重强依赖关系。回滚方案包括但不仅限于:应用回滚、数据回滚及清理、代码回滚、运维策略回滚、监控方案回滚等。
切记:代码需要及时回滚,以防在未修复问题前,下次团队其他同事上线把未回滚代码部署到线上导致二次问题发生。
3、代码回滚之开关技术
在大部分场景下,开关技术才是线上代码问题快速止血,快速回滚的最佳方式(需根据业务系统特性而定)。如遇线上问题的话,采用通用的回滚方式需要5-10+分钟()并且回滚如果操作不当会加重问题,而采用开关技术则是秒级。同时Promise在处理日常迭代需求和稳定性保障方面,功能开关技术同样发挥了重要的作用。针对改动范围大、影响面广的需求,我通常会问上线了最坏情况是什么?应急预案是什么?你带开关了吗?。当然开关也是有成本的,
4、行云部署的回滚方式
4.1、部署编排回滚
优点:回滚过程平滑,操作简单
缺点:回滚时长与发布时长相同,时间较长。
应用场景:对于非紧急场景,我们建议使用部署编排一键回滚的方式。
4.2、分组回滚
优点:回滚步伐灵活可控,速度快。
缺点:需要每个分组单独回顾。
应用场景:针对需要快速止血的场景,可以采用分组回滚的方式,这样可以更快地恢复系统状态。需要注意的是,我们需要设置好回滚批次间隔,以确保回滚操作不会对系统造成过大的影响。
六、配置变更控制
涉及生产配置参数的变更,原则上必须进行严格变更审批流程保障。所有对于生产动态配置变更由专业运维保障团队统一操作。
动态配置能力可以从以下方面进行设计:
动态配置变更的时机:预发布变更、发布后变更等;
动态配置的可验证:变更接收方能够以日志等形式验证推送的内容,否则是否推送,何时推送,推送的内容正确与否无法确证;
动态配置的生效同步性:在某动态配置涉及多个系统都需要同步时,应用需要考虑在多个系统间不同步时会出现的问题;
动态配置的容错性处理:防止进行线上配置填写错误时,系统即按照错误的情况运行,动态配置必须有默认值;
动态配置是否系统启动时加载:需要系统初起时加载的内容,需要防止出现系统启动依赖;
周期性动态配置:对于定时刷新缓存方式实现的动态配置,需要保证刷新成功后才更新或者替换缓存内容;不能在主线程中判断和刷新缓存,而应该另起线程刷新,防止刷新缓存出现抖动或者阻塞而影响主线程的功能。
案例:
1、修改JMQ的消费策略,可能影响下游链路相关风险(比如依赖的下游流量、中间件等)
2、修改JSF限流,可能导致对应风险点
七、复核验证
每个变更都需要有复核人,对于标准变更,复核人可只对结果进行复核。对于普通变更和重大变更,复核人需要对变更流程、变更表单、实际操作进行核对确认,对变更后的结果进行日志、监控等检查。复核人应对变更不当而引发的问题负责。
每个变更后,需要有一系列的基于变更清单管理的效果检查的内容。如:服务是否正常启动,功能是否可用,性能是否正常,以及变更的内容是否符合预期。通过对变更效果进行验证,才能最终确认本次变更是否正确。同时,针对服务相关的全局核心指标的监控,在变更期间既不应该出现异常,也不应该随意屏蔽。
附:Promise的checklist清单及DoubleCheck机制
1、建立CheckList清单
检查列表可以提醒人遗漏的东西、用来减少失败,保持一致性和完整性。把checklist清单作为xbp流程中一部分,集成到了行云部署发布中,申请上线的时候必须填写。
附:Promise上线案例之CheckList清单,包含
1、向下兼容,是否兼容之前功能
2.抽取文件检查,比如JSF别名配置,JIMDB配置等
3.ducc配置检查、是否新增加了开关
4.生产环境DDL检查,比如数据库表字段等
5.JSF别名配置,如上新接口或者服务需检查
6.jar包相关
7.JMQ配置检查
8.新日志文件检查(异步)
9、代码比对
10.上线过度方案检查、回滚策略检查
11.UAT环境是否测试、性能测试、R2引流验证等
2、DoubleCheck验证机制
小组群同步上线信息,并且执行DoubleCheck机制
那DoubleCheck验证看哪些?
1、对外核心接口UMP(TP99、可用率、流量)或者MQ 等,这个没什么好讲的。
2、根据日志验证对应场景(新功能场景及之前线上核心流程场景)。比如promise场景复杂,上线会验证不同订单类型的下传时间等相关的重要场景订单,如下图
3、通过可视化界面验证线上订单全流程(用户、业务角度等)。
比如xxx订单号,预分拣环节命中了上线的机器,通过持久化页面发现跟其他环节(转移、worker环节)时效是一样,并且通过OM系统查看全程跟踪时效也是一直。说明上线的机器跟之前机器算出来时效是一直的,如果不一致,需要分析是由于业务刚修改了配置还是系统代码问题。
4、用户角度
总结
变更管理在稳定性建设中扮演着至关重要的角色。它涵盖了兼容设计、新版本发布计划、灰度变革、数据迁移、可回滚设计、配置变更控制和复核验证等多个方面,旨在确保系统在变更过程中的稳定性和可靠性。
首先,兼容设计和新版本发布计划是变更管理的基础。通过充分考虑现有系统的功能和架构,我们可以预测并解决可能出现的兼容性问题。同时,制定详细的新版本发布计划,可以确保变更过程有序进行,避免对用户造成不必要的影响。
其次,灰度变革和数据迁移是降低变更风险的重要手段。通过逐步引入变更,我们可以及时发现和解决问题,减少对整个系统的影响。而数据迁移则是确保用户数据安全和完整性的关键步骤,需要仔细规划和执行。
另外,可回滚设计和配置变更控制是保障变更可控性的重要措施。可回滚设计意味着我们可以随时将系统恢复到变更前的状态,以应对可能出现的问题。而配置变更控制则可以确保变更过程的合规性和安全性,防止未经授权的变更发生。
最后,复核验证是确认变更有效性和正确性的关键步骤。通过对变更后的系统进行全面的测试和验证,我们可以确保变更没有引入新的问题,并且达到了预期的效果。
综上所述,变更管理在稳定性建设中起着至关重要的作用。通过合理的变更管理措施,我们可以降低变更带来的风险,确保系统的稳定性和可靠性。只有在充分重视和有效实施变更管理的前提下,我们才能够建立一个稳定、可靠的系统。
参考:
信通院稳定性建设指南