正式环境的稳定性,除去软件自身的质量因素,主要与运行的主机、网络等基础设施相关,而测试环境的稳定性则更多受到人为因素影响。由于频繁的版本变更,以及部署未经充分验证的代码,测试环境出故障的情况屡见不鲜。本文介绍了阿里巴巴是如何管理测试环境的。
阿里的许多实践看似简单,背后却蕴涵着许多思考,譬如测试环境的管理。
互联网产品的服务通常是由 Web 应用、中间件、数据库和许多后台业务程序组成的,一套运行环境就是一个自成一体的小生态。最基本的运行环境是线上环境,部署产品的正式发布版本,为用户提供持续可靠的服务。
除此以外,还有许多不对外部用户开放的运行环境,用于产品团队日常的开发和验证,统称为测试环境。正式环境的稳定性,除去软件自身的质量因素,主要与运行的主机、网络等基础设施相关,而测试环境的稳定性则更多受到人为因素影响。由于频繁的版本变更,以及部署未经充分验证的代码,测试环境出故障的情况屡见不鲜。
良好的代码提交习惯、适当的变更前检查有助于减少故障的发生,但无法彻底杜绝后患。增加多套测试环境副本能够有效控制故障的影响范围,然而企业的资源终归有限,降低测试环境成本和提高测试环境稳定性成为了矛盾的两面。
在这个领域里,独具匠心的阿里研发效能团队设计了一种服务级复用的虚拟化技术,称为“特性环境”,其巧妙的思路令人赞叹。本文将围绕测试环境管理的话题,聊聊这种具有阿里特色的工作方式。
测试环境管理的困局
测试环境的用途很广泛,常见的测试环境譬如系统集成测试环境、用户验收测试环境、预发布测试环境、灰度测试环境等,它们体现了产品的交付生命周期,也间接反映出整个团队的组织结构。
小作坊型产品团队的测试环境管理起来十分简单,每个工程师本地就能启动全套软件组件进行调试,倘若不放心,再加上一个公共的集成测试环境也就足够。
随着产品规模扩大,本地启动所有服务组件逐渐变得既费时又费事,工程师们只能在本地运行一部分待调试的组件,然后利用公共测试环境上的其余组件组成完整系统。
与此同时,团队规模的扩张,使得每个团队成员的职责进一步细分,新的子团队被划分出来,这意味着项目的沟通成本增加,公共测试环境的稳定性开始变得难以控制。在这个过程中,测试环境管理复杂性带来的影响,不仅体现在服务联调变得繁琐,更直接反映在交付流程和资源成本的变化上。
在交付流程方面,一个显著的变化是测试环境种类增多。出于不同的用途和目的,工程师们设计出了各式各样的专用测试环境。这些测试环境的组合形成了各个企业独具特色的交付流程。下图展示了一种用于大型项目的复杂交付流程。
从单独服务的角度来看,环境与环境之间是由流水线相连的,再加上自动化测试或手工审批操作组成关卡,实现环境之间的传递。通常越高级别环境的部署频率越低,因此相对稳定性也越高。与之相反,在级别较低的环境上,就随时可能存在新的部署,会打扰正在使用该环境的其他人。有时为了复现某些特殊的问题场景,一些开发者不得不直接登录到服务器上面去“搞事情”,进一步影响环境的稳定性和可用性。
面对随时可能崩溃的测试环境,小企业会试着去“堵”:约束服务变更时间、设立严格的变更规范,大企业则善于用“疏”:增加测试环境副本,隔离故障影响范围。显然,不堪重负的测试环境一定越“堵”越“漏”,千年以前大禹治水的故事早就揭示了的道理,刻意的管控拯救不了脆弱的测试环境。
近年来,DevOps 文化的兴起,端到端解放了开发者的双手,这对于测试环境的管理而言却是一把双刃剑。一方面,DevOps 鼓励开发人员参与运维,了解产品的完整生命周期,有助于减少不必要的低级运维事故;另一方面,DevOps 让更多的手伸向测试环境,更多的变更、更多的 Hotfix 出现了。这些实践从全局来看利大于弊,然而并不能缓解测试环境的动荡。单纯的流程疏通同样拯救不了脆弱的测试环境。
那么该投入的还得投入。将不同团队所用的低级别测试环境各自独立,此时每个团队看到的都是线性流水线,从整体上观察,则会程现出河流汇聚的形状。
由此推广,理想情况下,每位开发者都应该得到独占且稳定的测试环境,各自不受干扰的完成工作。然而由于成本因素,现实中在团队内往往只能共享有限的测试资源,不同成员在测试环境相互干扰成为影响软件开发质量的隐患。增加测试环境副本数本质上是一种提高成本换取效率的方法,然而许多试图在成本和效率之间寻找最优平衡的探索者们,似乎都在同一条不归路上越行越远。
由于客观的规模和体量,上述这些测试环境管理的麻烦事儿,阿里的产品团队都无法幸免。
首先是测试环境种类的管理
在阿里内部,同样有十分丰富的测试环境区分。各种测试环境的命名与其作用息息相关,虽然业界有些常用的名称,但都未形成权威的标准。实际上,环境的名称只是一种形式,关键还在于各种测试环境应当分别适配于特定应用场景,且场景之间应当或多或少存在一些差异。
这种差异有些在于运行的服务种类,譬如性能测试环境很可能只需要运行与压力测试相关的那部分访问量最大的关键业务服务,其他服务运行了也是浪费资源。有些差异在于接入数据的来源,譬如开发自测的环境的数据源与正式环境肯定不一样,这样测试使用的假数据就不会污染线上用户的请求;预发布环境(或用户验收测试环境)会用与正式环境一致的数据源(或正式数据源的拷贝),以便反映新功能在真实数据上运行的情况;自动化测试相关的环境会有单独的一套测试数据库,以避测试运行过程中受到其他人为操作的干扰。
还有些差异在于使用者的不同,譬如灰度和预发布环境都使用正式的数据源,但灰度环境的使用者是一小撮真实的外部用户,而预发布环境的使用者都是内部人员。总之,没必要为一个不存在业务特殊性的测试场景专门发明一种测试环境。
在集团层面,阿里对流水线形式的约束相对宽松。客观的讲,只有在一线的开发团队知道最适合团队的交付流程应该是什么样子。阿里的开发平台只是规范了一些推荐的流水线模板,开发者可在此基础上进行发挥。列举几个典型的模板例子:
这里出现了几种外界不太常见的环境类型名称,稍后会详细介绍。
其次是测试环境成本的管理
成本管理的问题十分棘手且十分值得深究。与测试环境相关的成本主要包括管理环境所需的“人工成本”和购买基础设施所需的“资产成本”。通过自动化以及自服务化的工具可以有效降低人工相关的成本,自动化又是个很大的话题,宜另起一篇文章讨论,此处暂且收住。
资产购买成本的降低依赖技术的改良和进步(排除规模化采购带来的价格变化因素),而基础设施技术的发展史包括两大领域:硬件和软件。硬件发展带来的成本大幅下降,通常来自于新的材料、新的生产工艺、以及新的硬件设计思路;软件发展带来的基础设施成本大幅下降,目前看来,大多来自于虚拟化(即资源隔离复用)技术的突破。
最早的虚拟化技术是虚拟机,早在 20 世纪 50 年代,IBM 就开始利用这种硬件级的虚拟化方法获得成倍的资源利用率提升。虚拟机上的不同隔离环境之间各自运行完整操作系统,具有很好的隔离性,通用性强,但对于运行业务服务的场景,显得略为笨重。2000 年后,KVM、XEN 等开源项目使得硬件级虚拟化广泛普及。
与此同时,另一种更轻量的虚拟化技术出现了,以 OpenVZ、LXC 为代表的早期容器技术,实现了建立于操作系统内核之上的运行环境虚拟化,减少了独立操作系统的资源消耗,以牺牲一定隔离性为代价,获得更高的资源利用率。
之后诞生的 Docker 以其镜像封装和单进程容器的理念,将这种内核级虚拟化技术推上百万人追捧的高度。阿里紧随技术前进的步伐,早早的就用上了虚拟机和容器,在 2017 年双十一时,在线业务服务的容器化比例已经达到 100%。然而,接下来的挑战是,基础设施资源利用率还能做得更高吗?
甩掉了虚拟机的硬件指令转换和操作系统开销,运行在容器中的程序与普通程序之间只有一层薄薄的内核 Namespace 隔离,完全没有运行时性能损耗,虚拟化在这个方向上似乎已经发展到了极限。唯一的可能是,抛开通用场景,专注到测试环境管理的特定场景上,继续寻找突破。终于,阿里在这个领域里发现了新的宝藏:服务级虚拟化。
所谓服务级虚拟化,本质上是基于消息路由的控制,实现集群中部分服务的复用。在服务级虚拟化方式下,许多外表庞大的独立测试环境实际只需要消耗极小的额外基础设施资源,即使给每个开发者配备一套专用的测试环境集群都不再是吹牛。
具体来说,在阿里的交付流程上,包含两种特殊类型的测试环境:“公共基础环境”和“特性环境”,它们形成了具有阿里特色的测试环境使用方法。公共基础环境是一个全套的服务运行环境,它通常运行一个相对稳定的服务版本,也有些团队将始终部署各服务的最新版本的低级别环境(称为“日常环境”)作为公共基础环境。
特性环境是这套方法中最有意思的地方,它是虚拟的环境。从表面上看,每个特性环境都是一套独立完整的测试环境,由一系列服务组成集群,而实际上,除了个别当前使用者想要测试的服务,其余服务都是通过路由系统和消息中间件虚拟出来的,指向公共基础环境的相应服务。由于在阿里通常的开发流程中,开发任务需要经过特性分支、发布分支和诸多相关环节最后发布上线,大多数环境都从发布分支部署,唯独这种开发者自用的虚拟环境部署来自代码特性分支的版本,故可称为“特性环境”(阿里内部叫“项目环境”)。
举个具体例子,某交易系统的完整部署需要由鉴权服务、交易服务、订单服务、结算服务等十几种小系统以及相应的数据库、缓存池、消息中间件等组成,那么它的公共基础环境就是这样一套具备所有服务和周边组件的完整环境。假设此时有两套特性环境在运行,一套只启动了交易服务,另一套启动了交易服务、订单服务和结算服务。对于第一套特性环境的使用者而言,虽然除交易服务外的所有服务实际上都由公共基础环境代理,但在使用时就像是自己独占一整套完整环境:可以随意部署和更新环境中交易服务的版本,并对它进行调试,不用担心会影响其他用户。对于第二套特性环境的使用者,则可以对部署在该环境中的三个服务进行联调和验证,倘若在场景中使用到了鉴权服务,则由公共基础环境的鉴权服务来响应。
咋看起来,这不就是动态修改域名对应的路由地址、或者消息主题对应的投递地址么?实事并没那么简单,因为不能为了某个特性环境而修改公共基础环境的路由,所以单靠正统路由机制只能实现单向目标控制,即特性环境里的服务主动发起调用能够正确路由,若请求的发起方在公共基础环境上,就无法知道该将请求发给哪个特性环境了。对于 HTTP 类型的请求甚至很难处理回调的情况,当处于公共基础环境的服务进行回调时,域名解析会将目标指向公共基础环境上的同名服务。
如何才能实现数据双向的正确路由和投递呢?不妨先回到这个问题的本质上来:请求应该进入哪个特性环境,是与请求的发起人相关的。因此实现双向绑定的关键在于,识别请求发起人所处的特性环境和进行端到端的路由控制。这个过程与“灰度发布”很有几分相似,可采用类似的思路解决。
得益于阿里在中间件领域的技术积累,和鹰眼等路由追踪工具的广泛使用,识别请求发起人和追溯回调链路都不算难事。如此一来,路由控制也就水到渠成了。当使用特性环境时,用户需要“加入”到该环境,这个操作会将用户标识(如 IP 地址或用户 ID)与指定的特性环境关联起来,每个用户只能同时属于一个特性环境。当数据请求经过路由中间件(消息队列、消息网关、HTTP 网关等),一旦识别到请求的发起人当前处在特性环境中,就会尝试把请求路由给该环境中的服务,若该环境没有与目标一致的服务,才路由或投递到公共基础环境上。
特性环境并不是孤立存在的,它可以建立在容器技术之上,从而获得更大的灵活性。正如将容器建立在虚拟机之上得到基础设施获取的便利性一样,在特性环境中,通过容器快速而动态的部署服务,意味着用户可以随时向特性环境中增加一个需要修改或调试的服务,也可以将环境中的某个服务随时销毁,让公共基础环境的自动接替它。
还有一个问题是服务集群调试
配合 AoneFlow 的特性分支工作方式,倘若将几个服务的不同特性分支部署到同一个特性环境,就可以进行多特性的即时联调,从而将特性环境用于集成测试。不过,即使特性环境的创建成本很低,毕竟服务是部署在测试集群上的。这意味着每次修改代码都需要等待流水线的构建和部署,节约了空间开销,却没有缩短时间开销。
为了进一步的降低成本、提高效率,阿里团队又捣鼓出了一种开脑洞的玩法:将本地开发机加入特性环境。在集团内部,由于开发机和测试环境都使用内网 IP 地址,稍加变通其实不难将特定的测试环境请求直接路由到开发机。这意味着,在特性环境的用户即使访问一个实际来自公共基础环境的服务,在后续处理链路上的一部分服务也可以来自特性环境,甚至来自本地环境。现在,调试集群中的服务变得非常简单,再也不用等待漫长的流水线构建,就像整个测试环境都运行在本地一样。
DIY 体验特性环境
觉得服务级虚拟化太小众,离普通开发者很远?实事并非如此,我们现在就可以动手 DIY 个体验版的特性环境来玩。
阿里的特性环境实现了包括 HTTP 调用、RPC 调用、消息队列、消息通知等各类常用服务通信方式的双向路由服务级虚拟化。要完成这样的功能齐全的测试环境有点费劲,从通用性角度考虑,咱不妨从最符合大众口味的 HTTP 协议开始,做个支持单向路由的简易款。
为了便于管理环境,最好得有一个能跑容器的集群,在开源社区里,功能齐全的 Kubernetes 是个不错的选择。在 Kubernetes 中有些与路由控制有关的概念,它们都以资源对象的形式展现给用户。
简单介绍一下,Namespace 对象能隔离服务的路由域(与容器隔离使用的内核 Namespace 不是一个东西,勿混淆),Service 对象用来指定服务的路由目标和名称,Deployment 对象对应真实部署的服务。类型是 ClusterIP(以及 NodePort 和 LoadBalancer 类型,暂且忽略它们)的 Service 对象可路由相同 Namespace 内的一个真实服务,类型是 ExternalName 的 Service 对象则可作为外部服务在当前 Namespace 的路由代理。这些资源对象的管理都可以使用 YAML 格式的文件来描述,大致了解完这些,就可以开始动工了。
基础设施和 Kubernetes 集群搭建的过程略过,下面直接进正题。先得准备路由兜底的公共基础环境,这是一个全量测试环境,包括被测系统里的所有服务和其他基础设施。暂不考虑对外访问,公共基础环境中的所有服务相应的 Service 对象都可以使用 ClusterIP 类型,假设它们对应的 Namespace 名称为 pub-base-env。这样一来,Kubernetes 会为此环境中的每个服务自动赋予 Namespace 内可用的域名“服务名.svc.cluster”和集群全局域名“服务名.pub-base-env.svc.cluster”。有了兜底的保障后,就可以开始创建特性环境了,最简单的特性环境可以只包含一个真实服务(例如 trade-service),其余服务全部用 ExternalName 类型的 Service 对象代理到公共基础环境上。假设它使用名称为 feature-env-1 的 Namespace,其描述的 YAML 如下(省略了非关键字段的信息):
kind: Namespace
metadata:
name: feature-env-1
---
kind: Service
metadata:
name: trade-service
namespace: feature-env-1
spec:
type: ClusterIP
...
---
kind: Deployment
metadata:
name: trade-service
namespace: feature-env-1
spec:
...
---
kind: Service
metadata:
name: order-service
namespace: feature-env-1
spec:
type: ExternalName
externalName: order-service.pub-base-env.svc.cluster
...
---
kind: Service
...
注意其中的 order-service 服务,它在当前特性环境 Namespace 中可以使用局部域名 order-service.svc.cluster 访问,请求会路由到它配置的全局域名 order-service.pub-base-env.svc.cluster,即公共基础环境的同名服务上处理。处于该 Namespace 中的其它服务感知不到这个差异,而是会觉得这个 Namespace 中部署了所有相关的服务。
若在特性的开发过程中,开发者对 order-service 服务也进行了修改,此时应该将修改过的服务版本添加到环境里来。只需修改 order-service 的 Service 对象属性(使用 Kubernetes 的 patch 操作),将其改为 ClusterIP 类型,同时在当前 Namespace 中创建一个 Deployment 对象与之关联即可。
由于修改 Service 对象只对相应 Namespace(即相应的特性环境)内的服务有效,无法影响从公共基础环境回调的请求,因此路由是单向的。在这种情况下,特性环境中必须包含待测调用链路的入口服务和包含回调操作的服务。例如待测的特性是由界面操作发起的,提供用户界面的服务就是入口服务。即使该服务没有修改,也应该在特性环境中部署它的主线版本。
通过这种机制也不难实现把集群服务局部替换成本地服务进行调试开发的功能,倘若集群和本地主机都在内网,将 ExternalName 类型的 Service 对象指向本地的 IP 地址和服务端口就可以了。否则需要为本地服务增加公网路由,通过动态域名解析来实现。
与此同时,云效也正在逐步完善基于 Kubernetes 的特性环境解决方案,届时将会提供更加全面的路由隔离支持。值得一提的是,由于公有云的特殊性,在联调时将本地主机加入云上集群是个必须克服的难题。为此云效实现了通过隧道网络 +kube-proxy 自身路由能力,将本地局域网主机(无需公网 IP 地址)加入到不在同一内网 Kubernetes 集群进行联调的方式。其中的技术细节也将在近期的云效公众号向大家揭晓,敬请留意。
小结
当许多人还在等待,在虚拟机和容器之后,下一轮虚拟化技术的风口何时到来的时候,阿里已经给出了一种答案。创业者的心态让阿里人懂得,能省必须省。其实,限制创新的往往不是技术而是想象力,服务级虚拟化的理念突破了人们对环境副本的传统认知,以独特的角度化解了测试环境成本与稳定性的矛盾。
作为一种颇具特色的技术载体,特性环境的价值不仅仅在于轻量的测试环境管理体验,更在于为每位开发人员带来流畅的工作方式,实则是“简约而不简单”。