编者按:本文源自阿里云云效团队出品的《阿里巴巴DevOps实践指南》,前往:https://developer.aliyun.com/...,下载完整版电子书,了解阿里十年DevOps实践经验。

开发一个需求,需要先进行代码的编写和个人验证,验证功能符合预期之后,再提交代码,并进入到集成环境,进行进一步的验证及验收。而这个编码和验证的过程占据了整个需求交付的大部分时间,因此提高这部分工作的效率就显得至关重要。

问题

有什么因素降低了开发调试的效率呢?

给定下面一个系统,其中为了开发某个需求,修改了 A 和 D 这两个应用(这里的应用指的是一个可提供服务的一组独立进程加上可选的负载均衡,比如一个 kubernetes 下的 service 及其后端的 deployment)。

接下来看看为了本地调测这两个应用,会遇到什么问题。

本地难以启动整个系统

我们通常都在开发一个复杂系统中的一个应用,这个应用可能在系统的最前端,也可能在系统的中间位置,有时候为了端到端验证整个流程,需要把相关的应用都启动起来。

比如上图中的应用 A 为最前端应用,应用 D 处在中间位置,而黑框中部分是为了完整的测试这个需求而涉及到的应用,如果是 Java 应用,开发机上启动这样 5 个进程,就已经不堪重负了,而很多时候需要完整启动的应用数量会远大于这个数字。

依赖系统不稳定

既然不能把整个系统都在本地启动起来,那么本地就会一部分依赖于公共测试环境。虽然前面提到应该本地测试符合预期之后再把代码部署到测试环境,但不可避免的还是会出现一些 bug,导致测试环境不可用(这也是测试环境的价值所在,尽早的发现问题)。一旦依赖系统不可用,就无法正常的进行测试。

云原生开发模式下的测试环境的连通性

在基于 Kubernetes 的基础设施下,整个系统中大部分的应用通常不需要通过 Ingress 暴露到公网。如果你的测试环境是独立的 K8S 集群,那就意味着无法从本地无法访问到集群内的应用,那么依赖公共测试环境这件事情都无法进行,比如上图中 A->C,D->E,D->F 的依赖。

还有另外一种依赖,即上游应用对本地应用的依赖,比如 C->D 的依赖。但因为 C 是公共测试环境,不可以将所有的 C 对 D 的请求都打到本地来,这就需要某种机制来保证只有特定规则的请求会路由到开发本地的 D 应用。

外部依赖系统到开发环境的连通性

有一些测试链路需要接受一些外部依赖系统的回调,比如微信或者支付宝的回调等。而本地应用通常没有公网地址,这也给调试带来了一些困难。

中间件的隔离

分布式系统中经常会用到 RocketMQ 等消息中间件,如果使用了公共测试环境,就意味着 MQ 也是共用的,那么 MQ 的消息到底是应该被测试环境消费,还是某个个人的开发环境消费呢,这也是需要解决的问题。

高效本地开发

为了进行全流程的高效开发,应该尽量使用反馈比较快的验证方式,并及早发现问题,逐步进行更加集成,更加真实的测试。

一般来讲,一个开发过程可以经过下面的三个阶段:

1、编码+单元测试。在小的逻辑单元的层面保证正确性。
2、针对单个应用的集成测试,可能需要对依赖的应用进行 HTTP 级别的 mock。
3、结合公共测试环境进行完整的集成测试。

基于上面的三个阶段,可以使用以下的方式来解决前面提到的几个问题。

1、使用各个语言相应的测试工具(比如 JUnit)来进行单元测试。
2、使用 moco 等 HTTP Mock 工具来解决本地隔离验证的问题,完成单个应用的集成测试。
3、使用 kt-connect 和 virtual-environment 等工具来解决云原生基础设施下,本地和测试环境的互相连通性问题,及 http 请求链路的染色和路由。
4、使用 ngrok 等工具解决外部依赖调用本地应用的问题。
5、使用“主干稳定环境”作为公共测试环境,提高其稳定性。
6、使用中间件的染色隔离能力保证 http 请求之外的其它链路(比如消息)的染色和路由。

其中第 1、4 是成熟的技术,这里不再赘述。第 5、6 点会在后面的测试环境相关的章节中我们详细讲解。本文主要就第 2、3 点展开讲解。

单应用的集成测试方案

比如对应用 D 而言,测试范围如下图的橙色框所示:

应用本身的持久化等依赖使用真实的(一般使用本地 DB),但外部应用(应用 E、F)使用基于 HTTP协议的测试替身。这样就可以保证所有的依赖都是稳定的。并且也可以很方便的修改测试替身的行为,以进行特定场景的测试。

应用 D 依赖了两个应用:

1、org-service(应用 F):提供查询组织信息等能力
2、user-service(应用 E):提供查询用户信息等能力

这两个应用的访问地址配置在应用 D 的配置项中:

...
org-service-host: org-service
user-service-host: user-service
...

我们使用 docker compose + moco 的方案来讲解如何使用本地测试替身。

首先创建如下的目录结构:

├── Dockerfile
├── docker-compose.yml
├── moco-runner.jar
└── services
    ├── org-service
    │   └── config.json
    └── user-service
        └── config.json

Dockerfile:

FROM openjdk:8-jre-slim

ARG SERVICE

ADD moco-runner.jar moco-runner.jar
COPY services/${SERVICE}/config.json config.json

ENTRYPOINT ["java", "-jar", "moco-runner.jar", "http", "-c", "config.json", "-p", "8080"]

docker-compose.yml:

version: '3.1'
services:
  service-f:
    ports:
    - 8091:8080
    build:
      context: .
      dockerfile: Dockerfile
      args:
        SERVICE: org-service
  service-e:
    ports:
    - 8092:8080
    build:
      context: .
      dockerfile: Dockerfile
      args:
        SERVICE: user-service

services/org-service/config.json:

[
  {
    "request": {
      "uri": "/"
    },
    "response": {
      "text": "org service stub"
    }
  },
  {
    "request": {
      "uri": {
        "match": "/orgs/[a-z0-9]{24}"
      }
    },
    "response": {
      "json": {
        "name": "some org name",
        "logo": "http://xx.assets.com/xxx.jpg"
      }
    }
  }
]

services/user-service/config.json:

[
  {
    "request": {
      "uri": "/"
    },
    "response": {
      "text": "user service stub"
    }
  },
  {
    "request": {
      "uri": {
        "match": "/users/[a-z0-9]{24}"
      }
    },
    "response": {
      "json": {
        "name": "somebody",
        "email": "[email protected]"
      }
    }
  }
]

然后使用如下命令来启动两个依赖的应用:
docker-compose up --build
验证下本地测试替身的行为:

$ curl http://localhost:8092/users/111111111111111111111111
{"name":"somebody","email":"[email protected]"}
$ curl http://localhost:8091/orgs/111111111111111111111111
{"name":"some org name","logo":"http://xx.assets.com/xxx.jpg"}

然后再把应用 D 的依赖配置改成本地测试替身即可进行测试:

...
org-service-host: localhost:8091
user-service-host: localhost:8092
...

至此,我们得到了一个稳定的单应用的集成测试环境。当需要修改依赖的行为时,只需要修改相应应用的config.json 即可。

使用 docker-componse 和 moco 是一种实现单应用集成测试的方式,你可以根据项目的具体情况选择合适的工具和方案。

本地和公共测试环境的互访及链路隔离

完成单应用的集成测试之后,可以获得单个应用级别的质量信心,但更大范围的验证还是需要和真实的依赖集成在一起进行。

如上图所示,为了能够在本地按需启动应用(A 和 D),并复用测试环境的其他应用(C),就需要解决两个问题:

1、本地如何调用到公共测试环境的应用,即 A 如何调用到 C
2、公共测试环境如何调用到本地,即 C 如何调用到本地的 D

关于第一点,如果本地环境和测试环境的网络是直接可达的,则直接修改本地应用 A 的配置项即可。如果你使用了云原生的基础设施,那么就需要类似云效 kt-connect 之类的工具来进行打通,这里不再展开,有需求要的可以参看 kt-connect 的 connect 部分。

关于第二点,需要解决三个问题:

1、从测试环境的 A 发起的调用链,应该最终访问到测试环境的 D,而从本地环境的 A 发起的调用链,应该最终访问到本地环境的 D,互不影响。为了能够对这两种调用进行区分,需要对调用链进行“染色”,这里采用的染色的方式是在请求中加入一个额外的 header。
2、根据这个染色的标志,即“染色标”,进行路由。
3、一个调用链会贯穿多个应用,要保证在调用到不同的应用时,染色标要能够自动的传递下去。
关于第一点和第二点,在阿里巴巴内部有一套完整的方案进行染色和路由,这套方案不仅仅适用于 HTTP 链路,也适用于 RPC,异步消息等。而在开源领域,也有基于云原生基础设施的 kt-connect 可以用,使用kt-connect 的 mesh 功能就可以针对特定染色规则的调用链进行路由。

kt-connect 基于 istio 的 VirtualService 和 DestinationRule 来进行路由。其基本原理是在集群内新建一个影子副本的 service 和 deployment,然后提交一个应用 D 的 DestinationRule 资源,使得包含“local-env: true”header 的请求被路由到应用 D 的影子副本,然后应用 D 的影子副本再把请求转发到本地。在这个过程里,除了提交和更新 is t i o 相关资源的操作需要手动进行之外,其他的事情都可以使用ktctl mesh 命令来完成,详情请参看 mesh 最佳实践。

接下来解决第三点,染色标传递。即需要保证当本地的应用 A 把含有“local-env: true”header 的请求打到测试环境的应用 C 后,应用 C 继续访问应用 D 时候,请求中也应该包含这个 header。

一般的思路是在 Web 层的入口加一个 Interceptor,将染色标记录下来到一个 ThreadLocal 中,然后再出口的 HttpClient 层再从 ThreadLocal 中把这个染色标取出来,并填充到 Request 对象中。这里有一个需要注意的问题,因为染色是放在 ThreadLocal 中的,因此在一个 web 请求的处理中一旦遇到多线程的情况,就需要小心的把这个 ThreadLocal 的值传递到相应的子线程中。所有的应用都正确的将染色标传递下去,就可以保证染色标在全链路进行传递。

使用 kt-connect 的 mesh 方案加上全链路染色标的方案,就可以轻松的在本地按需启动应用,并进行开发调测。

总结

使用单元测试、单应用集成测试、端到端集成测试结合的方式进行本地调测,提高获得反馈的效率。
本地按需启动应用进行端到端集成测试的关键技术是:全链路染色和路由。在不同的基础设施下可以有不同的实现方式。

【关于云效】

云原生时代一站式DevOps平台,数十万企业都在用。支持公共云、专有云和混合云多种部署形态,通过云原生新技术和研发新模式,助力创新创业和数字化转型企业快速实现研发敏捷和组织敏捷,打造“双敏”组织,实现多倍效能提升。

立即体验

03-05 22:02