如何才能对遗留系统无风险重构和演进?

测试覆盖率为零,代码一改就挂!从前任手上接管过来的这团泥球一般的代码我再也推不动了。

而你写的每一行代码的宿命就是变成别人的遗留代码。

《如何才能对遗留系统无风险重构和演进?》

如何才能对遗留系统无风险重构和演进?v2pro最关键的目标就是通过线上代码的流量录制和回放,来实现测试用例的构造。用真实流量来测试驱动代码的重构和演进,解决测试用例欠缺、代码不敢改的问题。

《如何才能对遗留系统无风险重构和演进?》

 

遗留代码的救赎之路:

除了流量录制回放,v2pro 还是一整套的遗留系统的稳定性和质量保障方案。具体来说,提供以下4个方面的能力

第一步:基于版本代理实现灰度发布
第二步:流量镜像自动感知全网状态
第三步:流量回放实现单元测试快速独立开发
第四步:全面接管网络层用代理提供分布式云化的一站式解决方案 Stabilize

《如何才能对遗留系统无风险重构和演进?》

稳定性是第一位的!而保障稳定,没有比灰度上线更重要的事情了。传统的灰度上线是通过部署系统逐台上线来实现的。

《如何才能对遗留系统无风险重构和演进?》

这种方式有以下几个缺点:

1、部署系统需要改造支持各种停顿间隔

2、小集群的灰度步骤会比较大

3、灰度无法精确控制影响的用户范围

《如何才能对遗留系统无风险重构和演进?》

 

通过引入网络代理,把版本的切换的职责从部署系统移动到版本代理的管理端来。这种“版本代理”的模式有几个优点:

1、灰度的影响面可以更受控,比如精确到某个城市的用户使用到新版本

2、版本的回滚速度更快,因为省去了进程重启的时间

3、版本切换的业务中断时间缩短,不需要改代码就实现类似优雅重启的效果

Stabilize >>
下一步:流量镜像自动感知全网状态 Battlefield awareness

《如何才能对遗留系统无风险重构和演进?》

监控需要回答两个问题

1、挂了没有?是什么引起的。

2、慢了没有?资源瓶颈在哪里。

对于遗留应用来说,日志和指标上报往往都是非常不齐全的。通过修改源代码来添加监控容易出现遗漏,而且改了代码之后也不好进行回归。就是想在线下把代码跑到改动点都是一件很费事的事情。

《如何才能对遗留系统无风险重构和演进?》

关键的实现技术是这么几点

1、RPC等网络调用的数据源来自LD_PRELOAD的网络层钩子,镜像一份流量给本地分析聚合。这个可以用于定位故障的时候是哪个网络服务出了问题。

2、eBPF从操作系统可以拿到几乎所有的资源瓶颈的情况

3、海量的数据在本地聚合,使用hyperloglog,count-min sketch等概率数据结构保存比率和分布等摘要数据

4、中央聚合只需要对概率统计数据结构进行二次merge就可以实现,极大减少传输量和计算量

《如何才能对遗留系统无风险重构和演进?》

流量镜像使用的是进程内的网络钩子而不是tcpdump,是因为需要拿到tcp send/recv时所在的线程信息。因为绝大部分的业务流程代码都是在一个线程内顺序发生的,所以利用线程id可以还原整个rpc调用链的情况。zipkin这样的分布式trace系统都需要业务代码进行改造,把上下文的调用trace id埋入到rpc的代码里。利用进程内的网络钩子我们可以在遗留代码上无侵入地实现分布式trace跟踪。

Battlefield awareness >>
下一步:流量回放实现单元测试快速独立开发 Control

《如何才能对遗留系统无风险重构和演进?》

遗留代码最大的问题是没有测试覆盖。一有改动,就可能会挂。这就导致所有的整理代码的事情都要考虑到这个巨大的风险。而因为正是因为代码得不到整理,所以新加的功能就会更加凌乱。这是一个死亡螺旋循环。而流量回放是解开这个死结的最佳方案。

《如何才能对遗留系统无风险重构和演进?》

流量回放就是从线上录制实际的rpc交互,然后在开发环境进行流量回放。相当于在线下有了线上的真实场景来做单元测试或者集成测试。这个技术的最关键的收益是实现了不依赖集成环境的模块独立开发,成倍提升了开发效率。实现的关键技术是这么几点:

1、LD_PRELOAD拦截 tcp send/recv。这个和前面的监控使用的方案是一样的,把流量给录制下来

2、全量保存所有的流量是非常浪费的。利用 intel 的 hyperscan 技术可以高速地在流量落地保存之前用正则表达式筛选出不同场景的流量。避免重复测试完全相同的场景。

3、多台机器录制的流量可以利用 bloom filter 技术进行高压缩比地集中索引。提高场景的检索效率。

4、线下回放的时候仍然是通过 LD_PRELOAD 拦截 tcp send/recv 控制被测进程回放线上的流量

Control >>
下一步:全面接管网络层用代理提供分布式云化的一站式解决方案 Modernize

《如何才能对遗留系统无风险重构和演进?》

所谓“分布式云化”其实主要是解决以下几个问题:

1、服务自动注册和发现,避免硬编码ip到配置文件导致的迁移困难,扩缩容困难

2、访问关系的管理和权限审计,黑白名单的管理,不合法调用的自动阻断,以及集群之间的路由关系

3、故障节点自动摘除,一般小厂都是用中心化的负载均衡来实现,但是会导致巨大的中心汇聚的风险。而去中心化的rpc框架的方案又有改造成本,而且无法兼容mysql,redis等非rpc框架覆盖的rpc。

4、自动限流,在流量超出负载的时候可以提前拒绝请求避免资源耗尽

5、超时熔断,监控当前请求处理的超时配额,如果已经耗尽,不再继续请求下游做无谓的浪费

而遗留代码里服务之间的访问关系往往就是一个简单的硬编码的ip地址,而其使用的rpc框架可能不止一个,而且很有没有实现上面所有的功能。

《如何才能对遗留系统无风险重构和演进?》

通过用入口代理和出口代理把遗留代码给包装起来的方式,接管了其对外的所有rpc通信。从而可以在进程外帮遗留代码把所有的“分布式云化”所需要的做的事情都给做了。以服务发现为例,这个透明接管网络层的过程如下:

《如何才能对遗留系统无风险重构和演进?》

遗留代码里一般都是直接硬编码了ip地址。所以不牵涉gethostbyname的调用,直接用tcp connect链接到所依赖的服务。

《如何才能对遗留系统无风险重构和演进?》

通过网络钩子我们可以拦截gethostbyname的调用。这样业务代码里需要把10.1.3.5这样的ip地址换成srv!passport这样的“服务名”。如果这个服务需要经过出口代理,那么gethostbyname就会返回出口代理所绑定的本地的ip地址。如果有多个外部依赖服务,可以用loopback的alias来区分。如果服务不希望经过出口代理,那么gethostbyname可以返回一个虚拟的唯一ip地址(比如100.64.2.3之类的)来代表srv!passport这个服务。然后拦截connect操作,把100.64.2.3这样的虚拟ip地址映射成当前这次rpc操作应该连接的真实的下游节点的ip。因为100.64.2.3是一个虚拟的ip地址,所以即便业务进程有dns缓存(比如jvm)也没有关系,实际的服务发现是在connect的时候进行的。

    原文作者:JVM
    原文地址: https://juejin.im/entry/59dde91ef265da43283fd54e
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞