作为一家初创公司,构建软件要坚持创新,要有吸引力和竞争力。因为,市场在不断变化,新的需求也在不断出现。
从软件角度来说,要保持这样的优势就意味着必须尽可能缩短文档和开发阶段所占的时间。当然,保持软件的弹性也很重要,提供优秀的服务是Algolia的重要目标之一。我们有许多高端用户,搜索功能对业务有非常重要的影响,所以不能接受宕机时间,尤其是在黑色星期五之类的特殊时间段。
因此,开发者必须在软件的弹性与创新之间找到合适的平衡点。这两方面是相互牵制的:要让软件具有弹性,就要进行详尽的测试,这会消耗大量精力,占用我们进行创新的时间。因此,一个比较好的折衷方案就是在生产环境进行测试。
在生产环境进行测试就是把新代码发布到生产环境中,直接用真实的生产数据和流量进行“测试”的过程。与之形成对比的就是运行全面的测试用例集。这个风险很大,开发者的第一直觉肯定不要这么做。但随着软件规模的发展,你会发现进行详尽的测试越来越不可能了。
让我们看看Algolia引擎。我们有二十多个查询参数。假设它们全是Boolean类型,那么要运行的用例总数就会达到一百万个,二十个参数,每个有两个可能值,那就是2^20种可能的场景。
谈到与测试相关的工作所要消耗的时间,有三方面要考虑:
写出一百万个测试用例来,这个工作量已经很惊人了。一旦写出来,它们就成了项目内容的一部分。就像维护别的源代码一样,也要花精力去维护它们,所以每次软件迭代所要做的事情就更多了。
假设你的团队足够大,有充足的人力可以编写和维护这些测试用例,但运行它们一样需要不少时间。假设运行每个测试用例只需要10毫秒,那全运行一遍就需要2小时45分钟。不管代码中有什么更新,都需要花2小时45分钟才能验证完。
客户购买我们的产品不止是看中了当前已经具备的功能,更是看中了未来将会发布的功能。他们希望我们可以定期发布新功能,帮助他们成长,变得更容易创新和更具有弹性。因此,我们必须提升自己的效率。
在开发新功能时,我们只会写些简单的用例来验证功能,并对明显的边界条件进行测试。要对功能进行全面验证,我们会采取灰度发布的方式,直接在生产环境中进行测试。这样即使代码中有缺陷,也可以把对客户的影响控制得尽量小。通过这样的方法,我们就可以按时发布新功能了。
以我们最近重写的一个Algolia的核心功能做具体例子。如果要对它进行充分测试,就要用所有可能的Unicode字符(超过一百万个)对这二十几个参数组合成的一百万个用例进行测试。这样算起来总数会超过十亿次。假设运行一个测试要用10毫秒,那完成全部测试内容就要11天。
我们不得不寻找更好的解决方案。因此我们放弃了这十亿次测试。不能因为这件事而显著地拖慢我们的发布流程,我们决定在生产环境进行测试。
我们最大的顾虑就是可能对用户造成的影响,因此我们定义了要把它部署出去所必须要做的事:
所有通过我们的网站发往Algolia的请求,最终都是由一个集群来提供服务的,集群由三个节点组成。每个节点中都包含100%的数据,可以独立响应请求,因此可以提供健壮的部署方案。假设普通的服务器平均可用率为95%,那这种方案可以提供99.987%的可用性。只有当所有服务器全部宕机的时候,你的服务才会真的宕机。所以这种可能性是5%*5%*5%,即可能会有0.0125%的宕机时间。
但即使是在这样的架构下,软件的缺陷仍然可能造成服务中断。因此,我们采取了渐进式的灰度发布方案。新的软件全部发布完成需要三天的时间。这样的部署策略给了我们足够的时间来发现问题。
另外还有一点很重要的,就是我们的客户端API会采用重试的策略。假如它正好把请求发往了一个有问题的节点,那么在请求失败之后,它会自动寻找另一个节点进行重试,直到取得成功的响应为止。因此最终用户对这个问题是没有感知的。
在我们部署新版本的高亮功能时,曾经发生过一个标准化问题。我们的目标是把所有文本都转化成标准化的格式,这样就可以方便地对不同的输入进行对比。一般来说,标准化后的内容长度不会比输入的原文更长,这也是高亮功能的前提。结果,我们却发现有些字符在标准化之后长度会增加:字母ß(德语)就会被标准化成了“ss”。在重写的时候,我们增加了运行时前置条件检查,以确保标准化后的长度比原来长度更小,或者相等。这段代码发挥了作用,把这个问题暴露出来了。
当我们把新版本代码部署到第一个节点上时,对于那些标准化之后长度会增加的请求,它马上就停止了响应。幸亏我们的客户端API有重试的功能,所以客户没有受到影响,没有用户注意到这一点。而在后台,我们的监控系统则发出了告警,所以我们马上对发布进行了回滚,以保持整个集群的稳定。通过这种办法,我们为自己争取到了足够的时间来理解问题和完成代码修复,并进行相应的测试。
如果你也想在生产环境进行测试的话,有三个至关重要的前提条件:
在现在这个时代,配置一套可复制的基础设施是非常容易的:所有云服务商都可以在多个虚拟机的前面提供负载均衡。就我们公司的架构来说,我们在整个集群的级别复制搜索引擎的数据,每个节点都100%地拥有全量数据。通过这种方式,每个节点都可以独立完成对请求的响应。
这一部分就要看你是怎样构建软件的了。在Algolia引擎的代码中,有许多关于健康状态的检查,会校验函数的前置条件是否满足,以及是否处于期望的状态等。当运行到非正常的状态时,引擎就会停止处理,以避免返回有问题的数据。它会强制API客户端在别的节点上透明地进行重试。
这一点被最后提到,但它也一样非常重要。这里提到的部署策略的主要目标,就是要在逐步发步新版本软件的过程同时,注意控制风险。
Algolia的基础设施主要包括四个环境:测试环境、准生产环境、生产环境和安全环境。每个环境都有不同的SLA:
测试环境只包含供内部使用的集群。出故障时只影响公司内部员工。 准生产环境包含面向外部大众用户项目的搜索集群。 生产环境包含我们客户的集群。 安全环境包含我们最重要的SLA客户的集群。 根据部署策略,我们通过使用多个不同的环境来降低风险。也就是说,我们会先在对客户影响最小的集群上进行部署。
除了不同的环境,我们还利用了三份复制这个特性,制定了如下的12步部署流程:
先部署到测试环境的所有三个节点上; 部署到准生产环境的第一个节点上,再部署到生产环境的第一个节点,再部署到安全环境的第一个节点上; 观察一天; 部署到准生产环境的第二个节点上,再部署到生产环境的第二个节点,再部署到安全环境的第二个节点上; 再观察一天; 部署到准生产环境的第三个节点上,再部署到生产环境的第三个节点,再部署到安全环境的第三个节点上; 第一步可以帮我们发现分布式系统内部的处理集群里,节点之间交互的代码问题。
接下来一个节点一个节点的部署,可以帮我们确认集群内部是否可以同时支持两个不同的版本,以及代码是否足够稳定。
为什么在部署节点的过程之中有两次要观察一天呢?因为这样可以让我们有充足的时间发现性能问题、数据问题或需要长时间运行才能发现的问题。到这一步时,我们就已经解决掉大部分问题了。接下来的部署步骤只是帮助我们发现一些可能的未知缺陷。
每当发现新问题时,我们都会立刻将新版本代码回滚。这样我们提供的服务就可以恢复到一个稳定的状态,我们也可以有充足的时间去修复问题,并增加相应的测试用例。
使用了这样的方法,我们的测试用例集就是客户的真实使用场景。这样效率就非常高了,我们可以每周都发布新版本,满足客户的需求。尽管我们的代码量已经非常庞大,我们仍然做到了这一点。
初创公司的生态环境是相当严峻的。小团队要找到高效的方法,打造出比大公司的大团队更好的产品。
定期发布新功能的重要性,不亚于有着良好用户体验,可以满足用户需求的稳定产品。选择做足够的测试还是选择有足够的测试覆盖率并可以定期发布?对效率的需求逼着我们在这两者之间找到了一个中间的平衡点。
在增加新测试用例时你必须特别小心,因为要消耗的时间太多了:要花时间去写、去维护和运行。那你知道什么时候该写测试吗?90-90法则适用于这种情况:测试一个功能90%的内容是非常容易而直接的,再测试剩下的10%会花费相同的时间。所以根据客户的使用情况来处理这剩下的10%非常重要,不应该追求完全的覆盖率。
为了降低风险,请多花些时间对软件和基础设施进行设计,让它们可以支持在生产环境进行测试,并把对客户的影响限制到最小。
本文翻译自“Tech Startup Dilemmas: Resilient Deployment vs. Exhaustive Tests”,原作者Xavier Grand。
领取专属 10元无门槛券
私享最新 技术干货