简介: 为什么线下环境的不稳定是必然的?我们怎么办?怎么让它尽量稳定一点?
这篇文章想讲两件事:
此外,还会谈一谈如何理解线下环境和线上环境的区别。
如果没有时间读完全文的话,这里是本文的主要观点:
以下是正文:
说起线下环境为什么不稳定,经常会听到大家给出这些原因:
其实这些原因中大部分都不是本质问题。换句话说,即便狠狠的砸钱、砸人、上KPI,即使机器不用过保机、硬件不超卖、工具建设好把配置监控自愈等和生产环境保持对齐、问题响应机制建立起来,线下环境也还是会不稳定的。因为线下环境不稳定的根源在于:
这两个原因是相互有关系的:我们需要有一个地方运行不稳定的代码,但我们怕不稳定的代码引起很大的问题,所以我们需要这个地方是低利害关系的(low-stakes);因为这个地方是低利害关系的,所以对它的问题我们必然是低优先级处理的。
所以,线下环境必然是不稳定的。
之所以Testing-in-Production(TiP)是一条出路,就是因为TiP把这两个根源中的一个(即第二点)给消除了:production不稳定带来的影响是很大的。但TiP注定是一条很漫长且艰难的道路,因为我们怕不稳定的代码引起很大的问题。我们需要首先在技术上有充分的能力充分确保不稳定的代码也不会引起很大的问题。这是很有难度的,今天我们还没有100%的信心做到能充分确保稳定的代码不会引起很大的问题。
既然TiP一时半会儿还用不上、发挥不了很大的作用,那么接下去的问题就是:怎么办?既然线下环境的不稳定是必然的,那我们怎么用不太夸张的投入让它尽量稳定一点?
对策还是要有的。否则,线下环境太不稳定了,大家就都放弃了,不用了,直接跳过,直接把还不太稳定的代码部署到预发环境(pre-production)去了。把预发环境当线下环境用,结果就是预发环境也被搞得像线下环境那样不稳定了。这样再发展下去,预发环境越来越不稳定了,我们还有地方可以去吗?所以,还是要有一整套对策让线下环境尽量稳定一点。
“环境问题”这个说法太笼统了,过多的笼统使用这个说法是很有害的,因为它会掩盖很多真正的问题。其中的一些问题也是有可能造成生产环境的稳定性和资金安全风险的。过多的笼统使用“环境问题”这个说法的另一个坏处是:会造成在大家的意识里“环境问题”是必然存在的、是无法避免的,导致大家一听到“环境问题”第一反应就是放弃,放弃排查、放弃抗争、放弃探究、放弃改进优化。
要提升线下环境稳定性,首先要正本清源,尽量避免笼统的使用“环境问题”这个说法。要尽量用具体一点的说法,比如,“网关配置问题”、“某某应用启动超时”、“数据库查询超时“。这些表象/症状背后的原因有很多种可能,是需要我们去排查清楚的,不能“刷墙”。所谓的“刷墙”的意思是:看到墙上有条裂缝,就找一桶乳胶漆刷一道,把裂缝遮盖掉。“刷墙”的行为的一个例子:某个应用启动失败,就换台服务器再试一试,成功了就继续干下面的事情,不去探究之前启动失败背后的原因了。
有些时候的确是项目时间太紧了,没时间排查每一个问题。可以现实一点,如果同样的问题出现第二次或第三次(例如,同一个应用、同一个项目分支,这周遇到两三次启动失败),就要追究一下了。
“环境问题”,归根到底,无外乎来自于三个地方:
这里要解释一下什么是stable和dev。线下环境的结构在蚂蚁集团和阿里集团的做法一般是这样的:
虽然这三趴对“环境问题”会因人而异,但都不可忽视。要提升线下环境稳定性,必须对基础设施、stable、dev这三趴三管齐下。
基础设施的稳定是非常关键的一环。如果基础设施不稳定,就会出现“排查疲劳”:每次遇到一些奇怪的问题(启动超时、调不通、等等),如果排查下来10次有9次是基础设施的问题,大家渐渐就不愿意排查了(因为不是代码的问题),一些真正的代码问题也会被漏过。
基础设施层要遵循的原则是:(业务应用的)线下环境的基础设施必须按照生产标准运维 。如果一个系统是运行在公有云上的,那么这个原则就很容易实现,因为线下环境也可以直接运行在公有云上。但有些公司、有些系统,是运行在自建机房、私有云上的,那最好的做法是撤销“线下机房”,直接把业务应用的线下环境放在基础设施的生产机房去跑(同时做好必要的访问控制和业务数据隔离)。线下环境直接放在基础设施的生产机房跑之后,基础设施团队直接按照运维其他生产机房那样去运维,中间件、数据库、缓存、物理机、网络、机房等所有的监控告警、巡检、发布和变更管控、应急、自愈能力、容量管理、等等都能做到位,稳定性可用率有明确的metrics和SLA。慢慢的,就能形成这样的心智:例如,当线下环境的某个业务应用出现数据库查询timeout的时候,我们首先怀疑的是应用自己的SQL查询语句有问题,而不是怀疑数据库有问题。
线下环境不稳定性的时候,工程师的心智是:当我在dev环境跑测试遇到错误的时候,我的第一反应是“一定是‘环境问题’”。也就是说,我的第一反应是“别人的问题”,只有当“别人的问题”都排出后我才会认真的去看是不是我自己的问题(包括项目的问题)。
当基础设施层稳定保障好以后,就能形成这样的心智:当某个应用出现数据库查询timeout的时候,我们首先怀疑的是应用(可能是stable的、可能是dev的)的SQL有问题,而不是怀疑数据库有问题。
当stable和基础设施这两趴的稳定性都治理好以后,就能形成这样的心智:当我在dev环境跑测试遇到错误的时候,我的第一反应是“一定是我们的项目有问题”。其实今天在生产环境大家就是这样的心智。一次变更、一次发布后,如果出现问题,做发布做变更的同学的第一反应都是怀疑是不是这个变更/发布有问题,而不是怀疑是不是(生产)环境本身不稳定。做stable和基础设施的稳定性治理也要达成这样的心智。
stable的稳定性治理,最终就是在做一道证明题:拿出数据来,证明stable是稳定的(所以,如果有问题,请先排查你的项目)。证明stable是稳定的数据分两类:
单应用就是检查应用是否起来了、是否或者、RPC调用是否调通(不管业务结果是成功还是失败,但至少RPC调用没有system error)。它验证的是单个应用是可用的,不管业务逻辑对不对,不管配置对不对,不管签约绑卡能不能work,至少这个应用、这个服务、这个微服务是up and running的。单应用稳定性必须达到100%,或者至少应该是“五个9”。这个要求是合理的,因为单应用的稳定性是链路稳定性的基础。如果单应用都没有up and running,链路功能的可用和正确性就根本无从谈起。
单应用的稳定性度量是很通用的,不需要理解业务场景就可以度量。我们需要做的事情就是:对目标形成共识,把度量跑起来,然后根据度量数据投入人力,一个个问题的排查解决,把稳定性一点点提升上来;后续再出现问题,第一时间排查解决,让稳定性维持在很高的水平。
链路的稳定性,说白了就是跑脚本、跑测试用例。频率是分钟级也可以,小时级也可以。验证链路的脚本是需要不断的补充丰富的,当发生了一个stable的问题但是验证脚本没有发现,就要把这个问题的场景补充到链路验证脚本(测试用例)里面去。也可以借用测试用例充分度的度量手段(例如,行覆盖率、业务覆盖率、等等),主动的补充链路验证脚本。很多其他测试用例自动生成的技术也可以用上来。
最后,达到的效果就是:用数据说话。用很有说服力的数据说话:stable的单应用都是好的,链路也都是通的,这时候出现了问题,就应该先怀疑是项目(dev环境)的问题。
顺便说一句:stable能不能像基础设施那样也直接用生产环境呢?可以的,stable用生产就是Testing-in-Production了。蚂蚁的影子链路压测就是这种做法的一个例子。只不过如果要把这个做法推广到更大面积的日常功能测试、支持更多链路和场景,复杂度和难度会比影子链路压测更高。
严格来说,dev环境的问题不能算是“环境问题”,也不能算是“线下环境稳定性问题”。因为dev环境就是被测对象(SUT),既然是还在写代码、联调集成和测试,那我们的预期就是它是不稳定的,是会有问题的。只不过实际工作中,dev环境本身的问题也构成了大家对线下环境不稳定的体感。
根据我们对一些项目进行的具体数据分析来分类,在dev环境遇到的问题的几个头部类型是:
自测没做好,解法就是要做好自测:
接口契约在软件行业已经有比较多的实践了,例如OpenAPI、ProtoBuf、Pact等。应用间的接口(包括RPC调用和消息),如果只是在一个文档里面用中文或者英文来描述的,上下游之间就比较容易出现gap。也经常出现接口改动只是通过钉钉说了一下,连文档都没有更新。应用间的接口应该以某种DSL来规范的描述,并且在单应用层面根据这个DSL描述进行契约测试,这样能大大减少两个应用到了dev环境放在一起一跑才发现跑不通的情况。
上面讲到,dev环境问题的第三个主要来源是相互干扰。既有同一个项目中几个同学各自在做联调集成时候的相互干扰,也有几个项目之间的相互干扰。项目之间的相互干扰的根源是共享数据库:
过去,stable环境以及多个项目的dev环境的代码都是访问同一个库的,相互影响就是不可避免的。数据的逻辑隔离和物理隔离都可以解决多项目间的干扰:
除了数据库,缓存、DRM等也需要进行隔离,减少多个项目之间的相互干扰。做好隔离对提升稳定性有很大的帮助。而且,数据库和缓存等的隔离也能大大降低“脏”数据引起的问题。
除了多个项目之间的干扰以外,同一个项目中几个同学各自在做联调集成时候,由于大家都在同一套dev环境(项目环境)上工作,也会出现相互干扰。
解决项目内相互干扰的出路是多环境:
IaC(Infrastructure-as-Code)和GitOps是实现多环境能力的关键。有了GitOps能力(包括Configuration-as-Code和Database-as-Code),能反复快速创建出一套套新的项目环境,并且保证新创建的项目环境中的配置都是对的(IaC也能更好更有效的确保stable的配置、二方包版本、CE版本等和生产环境是一致的)。
单应用的持续集成已经是比较普遍了:在master分支和项目分支上,每次有代码提交都会触发一次单应用的编译构建和测试(包括unit test和接口测试),或者以某个固定周期(例如每15分钟或者每小时)定时触发一次,确保该应用的编译构建和测试一直是好的。
多应用的持续集成就是:在项目分支上,每次有代码提交、或者每隔一定时间,把本项目各个应用的项目分支最新代码部署到dev环境上,并且跑一遍链路级别的用例,确保本项目的这些应用的项目分支代码还是好的。
在很多团队,今天开发同学的很多受挫感和时间的浪费都与缺乏项目级别的多应用持续集成有关,例如:
做好了多应用的持续集成,这些问题就都解决了:
…
做好了多应用的持续集成,其他的好处还有:
线下环境和线上环境的区别是什么,不同的人有不同的回答。有的说线下的容量没有线上大,有的说线下没有真实用户,有的说线下缺少生产的真实数据,等等,各种答案都有。线下环境和线上环境还有一个很本质的区别是:它们是两个不同的场景。
线下环境是一个场景。
我们做业务架构,先要搞明白业务场景,然后才能正确的设计业务架构和技术实现。数据的读和写是高频的还是低频的,数据块是大而少的还是小而多的,读取数据的时间段上有没有明显的峰谷,数据写入后是否会修改(mutable vs. immutable)等等,这些都会影响我们的架构和技术实现方案。
线下环境也是一个场景,一个和生产环境有不少差异的场景[3]:
一个配置值、一个开关值,在线上的改动是低频的,大部分情况下一天可能也就推个一两次,但在线下可能每天会有几十次、几百次,因为推送一个配置一个开关可能是测试的一部分。这个差异就是场景的差异。
服务器重启,在生产环境里是一个低频事件,很多应用只会在发布的时候重启一次,两次重启间的间隔一般都是数天。但在线下环境,重启的频率可能会高很多。
在生产环境,库的创建和销毁是一个低频事件,但是在线下,如果搞了持续回归和一键拉环境,线下环境数据库就会有比生产高的多得多的库创建销毁操作。
生产环境,我们是不允许数据丢失的。所以,数据库(例如蚂蚁的OceanBase)和DBA团队花了大量的心血在数据丢失场景上。但在线下,数据丢失是完全可以接受的。这个差异,对数据层的架构和技术实现意味着什么?例如,数据库在生产环境是三副本、五副本的,在线下不能支持单副本,能不能很容易的在单服务器、单库级别配置成单副本。
生产环境,一个系统,最多同时会有几个不同的代码版本在运行?线下环境呢?这个差异,意味着什么?
“抖动”是很难避免的,业务应用一般都有一些专门的设计能够容忍线上的基础设施层的一些”抖动“。因此,在生产环境场景里,基础设施层面每天抖N次、每次抖10-20秒,不是一个太大的问题。但这样的抖动在线下环境就是个比较大的问题:每次抖动,都会造成测试用例的失败。这并不是因为这些用例写的不够“健壮”,而是有很多时候测试用例就是不能有防抖逻辑的。例如,如果测试用例有某种retry逻辑,或者测试平台会自动重跑失败的案例[4],那么就会miss掉一些偶发的的bug[5]。在线下环境里,我们宁可接受每周有一次30分钟的outage(不可用),也不愿意接受每周几十次的10-20秒抖动。一次30分钟的outage,大不了就直接忽略掉那段时间的所有测试结果。而每周几十次的10-20秒抖动意味着大量的测试噪音[6],意味着要么是大量的额外的排查成本,要么是漏过一些问题的可能。
线下的数据模式和生产是不一样的。由于执行测试用例,线下的营销系统里的当前营销活动的数量可能比生产要高一个数量级。所以营销应用要在技术层面处理好线下这个场景,如果一个营销应用会在启动的时候就加载所有的当前活动,可能就会在线下出现很长的启动时间。
我一直倡导的一个原则是“Test environment is ephemeral”,也就是说,线下环境的存在时间是很短的。存在时间短,要求create的成功率高、时间短,但对数据清理要求比较低。存在时间长的,就要求upgrade的成功率高,对create的要求很低,对数据完整性和测试数据清理的要求非常高。继续推演下去,要做好测试数据清理,需要什么?基建层有什么技术方案?业务层需要做什么?业务层是否需要对数据进行打标?测试数据清理这件事,是放在业务层做(基建层提供原子能力),还是在基础设施层做(业务层按照规范打标)?这就是一个架构设计问题。这样的问题,要有顶层设计、架构设计,要针对场景进行设计,不能有啥用啥、凑合将就。
生产环境入驻一个商户,会经过一个人工审批流程,这个流程也许会走两三天,有六七个审批步骤。这在线上是OK的,因为线上的商户入驻是相对低频且能够接受较长的处理周期的。但在线下,由于要执行自动化的测试用例,而且要确保测试用例是“自包含”的,商户的创建就会是高频,而且必须快速处理的。所以在技术层面,针对线下环境的场景,要能够“短路”掉审批流程(除非本身要测试的就是审批流程)。类似的流程还有网关的映射配置,线上的网关配置是低频的,但线下的网关配置是高频动作,而且会反反复复。
3 其他
线上环境是有比较清楚的基线的,比较容易把失败的交易的链路数据和成功的交易的链路做比较。这个做法在线下环境同样有效吗?如果不是,为什么?是什么具体的线下环境的场景差异导致的?又比如说,对日志的需求,线上线下有差异吗?
线下数据库的权限,如果读和写的权限是绑定的、申请权限就是同时申请了读和写,就会很难受。因为工程师为了更好的做问题排查,希望申请上下游应用的数据库读权限,但他们只需要读权限,不需要写权限。如果读写权限是绑定的,即便他们只需要读权限,也要经过繁琐的申请审批,因为涉及了写权限,写权限如果缺乏管控,容易出现数据经常被改乱掉的情况。读写权限申请的时候是绑定的,这在线上环境的场景下也许是OK的,因为生产环境要跑DML本身是有工单流程的,不容易出现数据被改乱掉的情况。但读写权限绑定在线下就不合适了。从架构和设计层面说,读写权限绑定是因为ACL的模型本身没有支持到那个颗粒度。
我们一直说,做技术的要理解业务。比如,做支付系统的,要深刻理解不同的支付场景的差异(比如,代扣、协议支付、收银台、…),才能有效的进行架构设计和技术风险保障。例如,代扣场景,没有uid。这意味着什么?没有uid,意味着灰度引流的做法会不一样,精准灰度的做法可能会不一样,新建机房的切流方案也会不一样。
线下环境也是类似的道理。线下环境也是一个场景。这个场景和生产是不同的场景。每一层(SaaS、PaaS、IaaS)都要深刻的理解不同场景的差异,才能有效的把不同场景都保障好。如果一个应用、一个平台,它的设计和实现只考虑了X场景、没有考虑Y场景,那么它在Y场景下就会遇到这样那样的不舒服,也会使得Y场景下的客户不满意。
充分理解“线下环境”这个场景,把这个场景纳入到架构和技术实现的考虑中,有助于让线下环境尽量保持稳定。
总结一下上面所说的一些关键点:
Note[1] 线下环境:这里主要讲的是互联网应用的分布式系统的线下环境。也就是通常说的“服务端”的线下环境。这是阿里集团和蚂蚁集团里面涉及技术人员最多的一类线下环境。
[2] 其实,很多”脏“数据一点都不”脏“。很多时候,”脏“数据只不过是之前其他人测试和调试代码留下的数据,但这些数据的存在使得后面的执行结果不符合我们的预期。例如,我要测试的是一个文件打批功能,这个功能会把数据库里面尚未清算的支付都捞出来、写到一个文件里。我创建了一笔未清算的支付,然后运行打批,我预期结果是文件里面只有一条记录,但打出来实际有两条记录,不符合我的预期。这种情况其实是我的预期有问题,是我的测试用例里面的assert写的有问题,或者是我的测试用例的设计、我的测试架构的设计有问题,也有可能是被测代码的可测性(testability)有问题。
[3] 这些场景的差异,也许有人会把它们都归结为“可测性”。这样说也不是没有道理,因为测试就是线下环境最大的一个作用。但我们还是不建议把线下环境这个场景就直接说成“可测性”,因为“可测性”是一种能力,能力是用来支撑场景的,这就好像“可监控”是一种能力,“可监控”这种能力是用来支撑线上环境这个场景的。
[4] 我们是坚决反对测试平台提供自动重跑失败用例能力的,因为自动重跑对质量是有害的。自动重跑会掩盖一些bug和设计不合理的地方,久而久之这些问题就会积累起来。
[5] 偶发bug也可以是很严重的bug。曾经有过一个bug,这个bug会以1/16的几率出现。最后排查发现,原因是这段业务应用代码在处理GUID的时候代码逻辑有问题(而GUID是16进制编码的)。当时的test case只要rerun一下,大概率就会通过(有15/16的通过几率)。
[6] 有噪音的测试,比没有测试 还要糟糕。没有测试,是零资产。有噪音的测试,是负资产。有噪音的测试,要额外搭进去很多排查的时间,而且还会损害大家对测试的信心(类似“狼来了”)。
作者:开发者小助手_LS
原文链接
本文为阿里云原创内容,未经允许不得转载