在《造车还是买车?》中,谈到了对初创阶段的团队来说,最重要的是让产品或服务快速“下水”,以最快的速度在市场和用户中验证。为此,我们需要打造良好的持续交付能力。这种能力是一种在保证代码品质的前提下,快速上线的能力。说得再简单一点,就是具备能让工程师把代码“写”上线的工程实践能力。这说起来容易,做起来却很难,因为它所涉及的环节是非常纷繁复杂的。

对与初创团队来说,设计并实施一套完整和成熟的持续交付工程架构是不现实的,主要原因有以下两点:
1. 落地成本高,时间长。一套东西从设计、实施、落地,到成果维护可能需要花上数月甚至一年,也可能要花不少钱。尽管这能让团队一开始少背一些“技术债”,但却影响了“赚钱”的能力。很显然,这个买卖不划算。毕竟快速发展“赚钱”的能力才是生存之道啊。
2. 架构与应用能力不匹配,影响效率。一套完整成熟的架构需要有与之匹配的意识和应用实践能力,否则就有可能出现“强扭的瓜不甜”的情况。同时,由于工程师能力的限制,我们只能调高入库,release等关键节点的放行门槛。打回率的上升不仅影响效率,还影响心情。Work不happy了,work smart和work hard也就无从谈起。最终,影响得还是“赚钱”能力。

在我看来,初创阶段的团队并不非要落地一个完整和成熟的持续交付工程架构才能安心干活。许多公司的实践经验也告诉我们背负一定“技术债”也未必是坏事。那么什么样的架构才合适呢?结合自己的经验和思考,我觉得答案是:
Make it work first and everybody is happy!

搭建我们自己的工程架构

结合我们自己的实际情况,再利用《造车还是买车?》中的决策原则,我们很快选择以Github企业版仓库(包含7个member) + CircleCI(双容器,4个seat)+ 部署上线脚本的工程架构。整个架构的采购成本是900美金/年,约合人民币6300元/年(每月525元/月)。

在这样的架构下,一个典型的上线日可以被这样描绘出来:
1. 工程师小P做完本地测试后,将自己的feature分支提交到GitHub上,并创建了一个PR。
2. CircleCI运行了feature分支代码的静态检查和单元测试,检查和测试通过了。
3. 与此同时,技术经理大L在团队Slack上看到了PR审核请求,进入PR进行人工代码审查,并同意入库。
4. 入库后,CircleCI运行了开发分支的集成测试,测试没有过。问题通过邮件和Slack消息送达给工程师小P。
5. 小P修正了问题,重新提交了代码,代码得到了大L的再次通过。这次集成测试通过了。代码成功合并到开发分支。
6. 接下来,上线工作正式拉开序幕。大L将开发分支合并到release分支。
7. CircleCI的编译工序被触发,部署用的文件被打包成压缩文件包,并上传到CircleCI的artifact目录下。
8. 大L通过SSH登录我们的线上服务器,执行部署脚本。
9. 至此,整个上线活动完成。

在这个过程中,唯一可能对小P代码“写“上线产生拦截的就是在入库前和合并入开发分支前的审查。代码是否能放行进入下一个环节主要看重要的单元测试、集成测试和端到端测试是否通过,而非测试的数量,覆盖率这些指标。通过这种策略,我们可以实现在一定程度保证代码质量的前提下,尽可能少的blocking小P的代码。小P happy! 因为这些检查更多是自动化运行的,减少了人工检查的工作量,大L也happy。当然,由于测试覆盖率没有作为拦截的条件,上线后也容易出现问题。但我相信测试是解决不了上线故障率高的问题的,要回归问题的本质,去解决的是如何提高设计、编码能力以及迭代计划安排合理性的问题。

这样的方式,运行了近一年半时间。从代码入库检查到集成测试、编译基本实现了自动化。尽管没有出现什么问题,大家也没有不happy的情况,但部署上线的这“最后一里路”却没有实现自动化。这成了我心里的一块没落下的石头。

让石头落地,让“最后一里路”走得更爽

尽管可以通过SFTP从CircleCI把编译好的文件“扔”到我们的服务器上,但这种方式既不安全(CircleCI和我们的云服务运营商之间没有双向认证的部署集成服务)也不够优雅(我们仍然需要上服务器重启服务)。为了实现部署上线的自动化,并让持续集成服务与部署上线解耦,我决定开发一套基于消息机制的自动化部署上线服务。这个服务包含一个消息代理服务器和一个执行部署上线的程序。他们的工作原理可以这样来描绘:
1. CircleCI在完成编译后,向我们的消息代理服务器发送一个部署请求。这个请求需要有一个有效的电子签名,每个部署项目都有自己的签名。
2. 代理服务器接受到这个请求后,重新封装,并将其“publish"到指定的一个消息管道中(在Redis服务器上)。
3. 负责部署上线的程序从管道中收到一个新的部署请求消息后,执行对应的部署脚本。这个脚本与先前大L执行的脚本是一样的。

在这个过程中,消息代理服务器与负责部署上线的程序之间没有代码层面的耦合。前者是消息的制造者,而后者是消息的消费者。这样,CircleCI无需要等待部署工作完成才能结束自己的CI工作流、销毁容器及释放资源。也不会发生因等待超时导致部署被主进程中断的情况。同时,我们可以在无需改动部署上线程序代码的情况下,为新项目配置自动化部署上线用的钩子。

解决了”最后一里路“的问题后,上线流程变得更“爽“,更快了。同时,由于部署和更新服务没有人的参与,工程师也有了一种自己“写”代码上线的感觉。这种方式能让我们给予工程师更大的权利。但更大的权利也意味着更大的责任。某种程度上也倒逼了工程师去更好地编码、测试。在提高上线效率的同时,也促成了工程师的成长。双赢!

不稳定的网络

Github(G) + CircleCI(C) + 基于消息机制的自动化部署上线服务,的确提高了上线的工作效率。但有一个问题一直困扰着我们,那就是访问C的网络稳定性问题。由于我们的脚本需要从C中下载编译好的文件,并替换原有的服务器文件,完成服务重启。访问C的网络稳定性差,就有会造成下载文件速度慢,而慢不仅仅影响部署上线的整体速度,还有可能因为连接超时直接导致自动化上线部署失败。

C的服务器主要部署在国外的aws上。解决这个问题的第一个思路通常是在aws与我们的线上服务器之间搭建一个代理服务器。但这样并不是很妥(言不由衷的缘由),而且也解决不了aws端对下载速度限制的问题。所以,我决定采取另一个方案:切割需要下载的压缩包,并启用多线程下载:
1. 在编译时,自动将压缩包切割成每个不超过500K的小包。
2. 在下载部署用文件时,启用多线程,确保在较低连接速度的情况下,也不会因为超时连接被中断。

改造方案取得了明显的成效,原来需要20–30分钟才能下载成功的文件,在5分钟以内就完成下载。

爽只是第一步

Make it work,让大家happy只是我们打造持续交付能力的第一步。当团队规模扩大,业务场景变得更为复杂,交易量变得更多时,这样一套工程架构是否还能在保障较快的上线速度的同时,确保较低的线上故障率,并能继续让大家happy呢?我对此存疑。不过打造持续交付能力不就是一件要持续改进的事儿吗?

接下来的一年里,重构、拆分服务代码,构建更好的环境一致性,提升联调效率,上容器。在工程架构改进的路上,还有许多许多事要做。其实,故事才刚刚开始 …

发表评论

电子邮件地址不会被公开。 必填项已用*标注