聚星注册-聚星在线注册-聚星账号注册-聚星注册地址-聚星注册开户

注册:背景

时间:2019-08-14

  随着业务的迅速发展,我们的在线系统已演变成一个大型分布式系统,子系统已有上千个。大部分子系统都要跟几十个子系统进行交互,在开发测试过程中,由于测试环境和子系统较多,环境调试成本非常高,主要体现在下面几点:

  1)要保证调用的远程服务是正确环境的服务,我们使用dubbo作为服务框架,服务部署时会把group为环境名(静态指定)的服务注册到注册中心,消费者需要指定该环境group才能调用到该环境的服务,比如消费者要调用test1环境的某个服务,那么消费者需要把group属性设置为test1。当调用链路较长时,设置group的成本会非常高。假设修改了一个比较底层的服务,从环境入口(一般是带域名的web应用)走到你的目标服务中间要经过几十个其它子应用,在测试环境中测试该修改的服务时,即使调用链路中上层的子系统并没有代码修改,也需要把链路上面的所有子系统全部部署到该环境,并且把服务调用链中涉及的所有服务的group设置为test1。部署各个环境再加上环境调试时间,消耗的工作量是非常大的,经常会出现任务开发半小时,环境调试花半天的情况,严重影响生产效率。

  

  2)消息队列方面,测试环境的消息队列要隔离,以免消息被错误的环境消费造成错误的测试结果,但是由于测试环境的不稳定性,当对应的测试环境的消费者未部署时导致消费无法消费也会影响测试结果,经常采用的做法是任务上线之后把相关的队列都切成稳定环境的队列,但是由于测试环境数量很多,这样操作的成本也很高。

  3)系统有上百个子域名,每个测试环境都要维护一套域名host并且部署单独的nginx实例,无论增加环境还是增加域名维护成本都非常高。

  设计方案

  基于上面这些痛点,我们需要设计一套环境隔离方案,想要达到的效果是:在测试时,只需要在web应用入口处增加一个环境标,添加环境标之后后面的子系统服务,如果存在当前环境服务时调用当前环境服务,如果当前环境不存在则调用默认的服务,这样在联调或测试时只需在环境部署有代码修改的子系统即可完成功能测试,同时nginx只保留一个实例,host域名只保留一份,在nginx中通过环境标来把请求路由到正确的环境。

  这个环境标放到一个特殊的头域中,当前主流的浏览器都有修改头域的插件,比如chrome的ModHeader插件,一些代理工具如Charlets也支持修改头域,所以无论是pc端还是app端在入口处添加环境标很容易实现的。

  为了达到上面的目标,我们定义了两种类型的测试环境:稳定测试环境和普通的测试环境,稳定测试环境是重要性仅次于线上生产环境的环境,它权限控制很严格,不会轻易关停或重启,只会在固定的时间点由专人部署,目的就是保证该测试环境绝对稳定,在绝大多数情况下稳定环境的所有子系统都是可以提供服务的。普通环境就是用来正常测试的环境,我们把稳定测试环境叫做所有普通测试环境的父环境。

  我们已有两个中间件支持我们的环境隔离方案:

  1)环境&集群管理平台,每个测试环境的应用是一个独立的集群,在创建集群时并分配机器之后,会向集群下的所有机器下发环境信息包括父环境和子环境,以一个json串的形式:{"parentEnv": "stable","currentEnv": "test1"}下发,应用启动之后会读取该json串,目的是让当前机器上的集群能感知到它所在的环境。

  2)全链路跟踪框架,该框架能让环境标在全链路透传,在不侵入业务代码的情况下让链路中所有的上下游系统都能拿到入口处设置的环境标,从而链路中各节点才能根据环境标进行服务路由。它实现了JDK的Instrumentation规范,对dubbo、kafka、连接池、线程池、spring-web框架的DispatcherServlet等做字节码增强无需侵入业务代码,给JVM的javaagent参数指定一个agent jar包注入增强逻辑,我们管这个全链路跟踪框架叫trace框架,这个框架可以帮我们全链路透传很多特殊标识辅助我们完成如环境隔离、压测改造、性能分析等,关于这个trace框架这里不一一展开,后面会再写一篇文章专门介绍这个框架,这里只需要知道它能全链路透传环境标。所有应用都需接入trace框架。

  系统中一条完整的请求路径是DNS->nginx->tomcat->app,此外有些流程还会使用消息队列发送异步消息,所以还需对nginx、tomcat服务器、远程服务框架、消息队列都需要做改造。

  

  nginx路由改造

  为了简化host域名管理,所有子环境的host不再维护,只维护一套稳定环境的host和nginx实例,所有环境测试入口都由稳定环境的nginx实例接收,因为后端有N套环境的web应用,这就需要在nginx中能通过头域中的环境标正确的路由到对应环境的web应用,改造方式如下:

  1)nginx的Server配置标准化,upstream名称统一采用web应用名+环境名,比如交易trade-web应用的test1环境,那它的upstream名称应该为trade-webtest1,在Server配置中请求转发根据头域中的环境标动态路由,如下配置由头域entrance_env决定路由到哪个后端web应用:

  

  同时请求转发时把头域中的环境标透传到后端web应用,以便于后端web应用用此环境标进行后续的请求路由:

  proxy_set_header entrance_env 聚星登陆 $http_entrance_env;

  2)创建测试环境时,会自动给系统中所有的子域名下发一段upstream配置到nginx,比如创建了test1环境,那么会下发一段名称是trade-webtest1的upstream配置,upstream中的ip和端口是稳定测试环境集群的web应用ip和端口,这样当该环境中没有创建子域名web应用集群时,访问test1环境的子域名系统时请求会转发到稳定环境:

  

  3)创建测试环境集群时,如果该集群应用是带域名的web应用,在集群分配机器之后,下发一段test1环境upstream配置到nginx覆盖创建环境时生成的upstream配置,ip和端口是test1环境的ip和端口:

  

  这样当请求entrance_env头域中带了环境标时,会根据环境标匹配到入口环境对应的upstream,请求会被转发到入口环境集群。

  基于上面的实现机制,可以到达如下效果:当test1环境没登录有创建trade-web应用集群时,访问trade-web时请求会转发到stable环境;当test1环境创建了trade-web应用集群时,访问trade-web时会请求test1本环境,这样就完成入口web环境的路由。

  

  dubbo路由改造

  在介绍nginx改造时介绍了nginx会把环境头域传递到后端,请求到达web环境之后,全链路跟踪框架增强了spring-web的DispatcherServlet,在增强逻辑中trace框架会从entrance_env头域中解析出环境标,并把环境标识设置到当前线程上下文中,后面可以从该线程上线文中读取环境标。在介绍的过程中会涉及到三个概念的环境:

  1)稳定测试环境;

  2)入口测试环境,这个测试环境的标会在整个调用链路全程透传;

  3)当前测试环境,即当前处理请求所在的测试环境,处理请求的测试环境有可能是入口测试环境,也有可能是稳定测试环境。

  后面的服务路由由远程服务调用框架完成,需要达到的目标就是,在远程服务调用时,当调用的目标服务在入口测试环境有部署时,调用入口环境的目标服务,当目标服务在入口测试环境没有部署时,调用稳定测试环境的目标服务。

  注册

  我们采用dubbo框架作为远程服务调用框架,为了达到上面的目标,我们对dubbo做了如下改造:

  1)应用在定义服务消费者时,不再设置消费者的group属性,服务的group不再由使用者指定而是由框架自动设置;

  2)应用在环境部署时,会读取创建应用集群时下发到应用服务器上的环境json串解析出当前所属环境,注册服务时给服务的URL增加一个env参数,属性值为当前环境名;

  3)服务调用时,消费者透传环境标,这一步由trace框架完成,trace框架增强了dubbo框架的DubboInvoker,在消费者调用服务之前把环境标放到RPC上下问题RpcContext的attachments中,dubbo框架在远程调用时会自动传递attachments中的所有数据。

  4)设置好环境标之后,接下来是关键的一步,要根据全链路环境标选择目标服务调用,假设上下文环境是test1,那么选择逻辑是当目标服务在test1环境有部署时,选择test1环境的服务调用,test1环境没有部署目标服务时,选择稳定环境的服务调用。我们在dubbo rpc调用流程源码分析这篇文章介绍了,消费端由LoadBalance组件在所有服务提供者中间选择一个提供者调用,在进入LoadBalance组件之前,服务提供者要先经过Router组件过滤一遍,我们扩展了一个Router组件来做环境标过滤。

  具体逻辑就是:在从RegistryDirectory获取到目标接口所有的Invoker List之后,Router遍历一遍Invoker,并且根据Invoker URL中的env参数进行Invoker分组,转换成一个(环境名,Invoker List)的键值对map,然后检查环境名为全链路环境的的Invoker List是否是空的,如果非空那么把该Invoker List继续往后传递给LoadBalance做负载均衡,如果当前环境Invoker List为空,那么获取稳定环境的Invoker List,把稳定环境的Invoker List往后传递给LoadBalance做负载均衡,至此就完成对提供者的环境过滤。

  

  从这里可以看出,即使当前在稳定测试环境发生,只要全链路透传的环境标是test1,那么在调用服务时会优先选择test1的提供者。

  5)服务提供者接收请求时需要获取环境标,trace框架增强了DubboProxyInvoker,服务端在接收到请求之后调用目标服务之前,DubboProxyInvoker会被激活,在增强逻辑中,trace从RpcContext的attachments中拿到透传的环境标,并且把环境标再次设置到trace的线程上线文中,供后续的链路使用。

  完成上述改造之后,改造后的新dubbo框架就可以满足远程服务调用的环境隔离要求了。

  消息队列路由改造

  消息队列是分布式系统一个非常重要的中间件,在某些业务场景它能消除不同子应用的直接依赖降低耦合度,并且在一定程度对应用流量进行削峰,消息队列异步消息也是整个业务链路中的重要一环。我们的所有子系统都采用kafka当做我们的消息队列中间件。消息队列的痛点跟远程服务调用类似,消息topic不同环境是分开的,topic名称会带上环境名称,比如稳定环境的topic为topic.xxx.stable,test1环境的topic是topic.xxx.test1,当入口测试环境的消费者没有部署时,该测试环境的topic消息无法被消费导致业务流程中断影响测试结果。所以我们最终想要达到的目标是:当入口测试环境没有部署topic消费者时,由稳定测试环境来消费该topic消息来持续后续的业务流程,为此我们在原生的kafka客户端进行扩展,具体的处理方式如下:

  1)设计了一个topic模板的概念,模板中带上环境占位符,像topic.xxx.{env}这种形式,topic生产者和消费者中都只配置topic模板,到每个环境上会按照topic模板生成一个具体的topic,比如在test1环境上会生成一个对应的topic.xxx.test1;

  2)部署一套zk集群,用来协调topic各测试环境的生产者集群和消费者集群。

  3)消费者部署时,在zk集群上创建一个临时节点,节点信息包括:topic、类型(消费者)、消费者group、消费者所在环境;

  4)生成者部署时,在zk集群上创建一个临时节点,节点信息包括:topic、类型(生产者)、生产者所在环境,并且订阅该topic的zk消费类型节点,这样生产者可感知该topic有多少消费者group,已经各环境消费者存活情况;

  

  5)生产者发送消息时,当前链路上下文中环境标为test1,会有几种不同的场景:

  5.1)topic.xxx.test1无消费者部署(监听zk临时节实现),生产者向稳定环境的topic(topic.xxx.stable)发送消息;

  5.2)topic.xxx.test1有消费者部署,且topic只有一个消费者group,生产者向入口环境topic(topic.xxx.test1)发送消息;

  5.3)topic.xxx.test1有消费者部署,且topic有多个消费者group,所有消费者group都已部署,生产者向入口环境topic(topic.xxx.test1)发送消息;

  5.4)topic.xxx.test1有消费者部署,且topic有多个消费者group,存在未部署的消费者group,生产者分别向入口测试环境topic.xxx.test1和稳定测试环境topic.xxx.stable发送消息,这样未部署的消费者group的消息就由稳定测试环境的消费者消费;

  6)生产者发送消息时,在消息体中携带链路中的环境标test1;

  7)应用订阅了topic.xxx.{env}中的消息之后,每个环境的集群只会订阅本环境的topic,入口测试环境会订阅topic.xxx.test1消息,稳定环境会订阅topic.xxx.stable消息,消费消息时,从消息体中解析出透传的环境标,并把环境标设置到trace线程上下文中,供后续的链路继续使用。

  

  文章来源:https://baijiahao.baidu.com/s?id=1641316549420168211&wfr=spider&for=pc


登录 注册 聚星登陆