验证重构系统的终极方法——流量复制

1. 引言

重构一个系统时会面临这样一种场景,当经过业务梳理、重新设计、代码重构等一系列环节后,发现最难的却是如何验证优化后系统的正确性。

你可能会说,没有QA吗?这不是QA要保证的吗?

没错,QA是应该做好系统的测试和验证工作,但这并不够。原因是:

  1. 一个软件系统的复杂性是很高的,研发都无法保证评估到所有Case,又何况站在黑盒外面的QA呢?
  2. 用户的使用环境和使用场景总会有差异,不论你投入多少测试资源,也无法完全模拟产线所有用户的真实使用场景;

站在最终要对重构结果负责的角度,必须想尽一切办法做好系统的验证工作,尽最大努力减少重构带来的风险。

而最有效的方法就是拿线上真实的用户流量来验证,也就是我们今天要讨论的主题——流量复制。

2. 实现思路

流量复制的本质是将发往旧系统的请求拷贝一份,重新转发给新的系统。流程大概如下:
<流程图>

这个流程分为几个步骤:

  • 请求拷贝
  • 流量转发
  • 结果验证

2.1 请求拷贝

不同语言的实现不同,我们以go语言为例简单说明。

net/http包下的Request类型已经自带了拷贝请求的两种方法:浅拷贝和深拷贝。

  1. 浅拷贝:只复制Request对象本身,里面指针型属性指向的结构体则不作复制。对于没有并发使用的简单场景可以使用此方法,方法源码如下。
// WithContext returns a shallow copy of r with its context changed
// to ctx. The provided ctx must be non-nil.
// ……
func (r *Request) WithContext(ctx context.Context) *Request {
	if ctx == nil {
		panic("nil context")
	}
	r2 := new(Request)
	*r2 = *r
	r2.ctx = ctx
	return r2
}
  1. 深拷贝:逐层展开,递归将每层字段都复制一遍,如果想避免请求对象在并发场景下相互影响,可以使用此方法,源码摘抄如下。
// Clone returns a deep copy of r with its context changed to ctx.
// The provided ctx must be non-nil.
func (r *Request) Clone(ctx context.Context) *Request {
	if ctx == nil {
		panic("nil context")
	}
	r2 := new(Request)
	*r2 = *r
	r2.ctx = ctx
	r2.URL = cloneURL(r.URL)
	if r.Header != nil {
		r2.Header = r.Header.Clone()
	}
	if r.Trailer != nil {
		r2.Trailer = r.Trailer.Clone()
	}
	if s := r.TransferEncoding; s != nil {
		s2 := make([]string, len(s))
		copy(s2, s)
		r2.TransferEncoding = s2
	}
	r2.Form = cloneURLValues(r.Form)
	r2.PostForm = cloneURLValues(r.PostForm)
	r2.MultipartForm = cloneMultipartForm(r.MultipartForm)
	return r2
}

2.2 流量转发

流量最终需要转发到一个目标服务器,所以首先要配置一个接收流量的镜像服务器地址:

MirrorServerUrl = http://192.168.28.212

相比普通请求,流量复制需要在转发前修改下目标服务器地址,封装一个方法来完成此步操作:

func MirrorTraffic(request *http.Request) {
    serverUrl := beego.AppConfig.String("MirrorServerUrl")  // 镜像服务器地址
	u, err := url.Parse(serverUrl)
	if err != nil {
	    // 错误处理省略
		return
	}
    // 复制请求,并修改目标服务器为镜像地址
    r := request.WithContext(context.Background())
	r.URL.Scheme = u.Scheme
	r.URL.Host = u.Host

    / 发送请求
	resp, err := http.DefaultTransport.RoundTrip(r)
	if err != nil {
	    // 错误处理省略
		return
	}
	defer resp.Body.Close()
	if resp.StatusCode != 200 {
		// 错误处理省略
		return
	}
	
    // 读取响应
	responseBody, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		// 错误处理省略
		return
	}
	// 后续验证
}

这样,当用户请求到达旧系统时,就能将流量源源不断的转发到新系统进行验证。

不过,如果用于验证的目标服务器资源有限,处理不了生产环境的全部流量,则有可能因物理资源瓶颈而出现慢、超时、失败等问题。此时,就需要进行流量控制。

2.3 流量控制

我们可以封装一个简单的、能满足COPY场景即可的流量控制模块,大概思路:

  • 定义一个字段copyRatio,用于设置复制流量的比例阀值;
  • 定义一个字段copiedCounts,用于记录已经复制的请求数;
  • 定义reqCounts用于实时记录总请求数量, 并根据比例计算当前允许复制的请求数allow;
  • 如果allowCounts > copiedCounts,则此次请求允许复制,并将copiedCounts+1

代码实现示例如下 :

// 简单请求流量和拷贝流量统计
// 说明:并发请求下计数并不完全准确,但出于场景和性能考虑,未加锁
type TrafficStats struct {
	reqCounts  int64          // 流经服务器的请求总数量
	copiedCounts int64     // 已经复制的请求数
	copyRatio  float64       // 复制比例,例如:0.5
}
// 修改流量复制比例
func (t *TrafficStats) SetCopyRatio(ratio float64) {
	……
}

// 判断本次请求是否能复制
func (t *TrafficStats) AllowCopy() bool {
	t.reqCounts++
	allowCounts := int64(math.Floor(float64(reqCounts) * t.copyRatio))
	if allowCounts > t.copiedCounts {
		t.copiedCounts++
		return true
	} else {
		return false
	}
}

说明:上面流量统计是比较粗糙的,像并发场景下就不是完全准确,如果实际需要精确,可以加锁控制。

2.4 结果验证

结果验证可能要分场景,在不同业务、不同要求下结果验证的方式也不一样,这里只简单讨论下。

简单验证场景

像我们的业务场景——分流器重构,验证起来就比较简单,只需要对新、旧系统分流后的结果——目标环境URL 进行比对即可验证结果。
为此,我们可以通过请求Header传递一些期望的结果给新系统,这样,新系统就知道该如何运行和验证结果。

r.Header.Set("Run-Mode", "test")      // 告诉新系统运行在测试模式下,只验证分流结果,请求不需要转发到业务侧
r.Header.Set("Expect-Target-Url", targetUrl) // 期望的分流结果地址
r.Header.Set("Original-Request-ID", requestID) // 旧系统中的请求唯一标识,用于新、旧结果不一致时来排查问题
复杂场景

如果是纯业务系统的重构,可能就需要对接口的response进行验证。这种情况下,有几种选择:

  1. 实时验证:在复制流量时,直接拿到旧系统的response和新系统的response,作二进制内容比较;
  2. 先收集再验证:把新系统和旧系统的响应结果都异步收集到类似ES的系统中,结合一定的策略作分析比较;
  3. 请求采样验证:随机复制一定比例的样本流量,肉眼去新系统上观察结果是否如预期,可能比较低效,但有时候只能这样来;
  4. 字段采样验证:提前按请求定义一套需要验证的关键字段信息,部分关键信息一致即算验证通过;

3. 开源工具

上面讨论的是方式对系统有一定的代码侵入,那是否有不侵入代码的流量复制方式呢?

有一个开源工具goreplay可以做到,它能将系统中的实时流量记录下来,用于回放、分析和负载测试。运行原理如下:

  • 在业务服务器所在机器上启动gor程序,监听和业务服务相同的网络端口;
  • 从端口上抓取请求,并在QA测试环境重播;
  • 将产线和测试两个环境返回的response,作结果的对比和分析;
    在这里插入图片描述

使用前需要先下载安装对应平台的可执行程序包,下载地址:

https://github.com/buger/goreplay/releases

此工具的使用方式很丰富,按场景有:

  • 简单测试请求抓取,结果输出到控制台
# –input-raw 指明要捕捉请求数据的网络端口,这里是80端口
$gor --input-raw :80 --output-stdout

Interface: eth0 . BPF Filter: ((tcp dst port 80) and (dst host 10.255.0.187 or dst host fe80::250:56ff:febe:42df))
Interface: lo . BPF Filter: ((tcp dst port 80) and (dst host 127.0.0.1 or dst host ::1))
2023/10/17 15:25:38 [PPID 32153 and PID 32719] Version:1.3.0

1 8ff400500aff0180d8069d4a 1697527542939395480 0
POST /uniform/rs/conference/geteventsimpleinfo HTTP/1.1
Accept: application/json
Content-Type: application/json;charset=UTF-8
Content-Length: 18
Host: testcloudb.quanshi.com
Connection: Keep-Alive
User-Agent: Apache-HttpClient/4.5.12 (Java/1.8.0_202)
Accept-Encoding: gzip,deflate

{"eventId":644164}
  • 从指定端口记录流量,直接复制到目标Server:
sudo ./gor --input-raw :80 --output-http = "http://copytest.quanshi.com"
  • 将流量记录到本地文件
gor --input-raw :80 --output-file=requests.gor
  • 回放本地文件记录的请求
gor --input-file=requests_0.gor --output-http="http://copytest.quanshi.com"
  • 请求过滤:只回放指定URL路径下的请求
gor --input-file=requests_0.gor --output-http="http://copytest.quanshi.com" --http-allow-url=/uniform
  • 限速:回放不超过原流量的10%
gor --input-file=requests_0.gor --output-http="http://copytest.quanshi.com|10%"
  • 性能压测:模拟大量用户并发请求,性能场景下可以忽略请求的顺序
# --input-file 从文件中获取请求数据,重放的时候 10x 倍速
# --input-file-loop 无限循环,而不是读完这个文件就停止
# --output-http-workers 模拟100用户并发请求
# --stats --output-http-stats 每 5 秒输出一次 TPS 数据
gor --input-file="requests_0.gor|1000%" --input-file-loop --output-http="http://testcloud3.quanshi.com" --output-http-workers 100 --stats --output-http-stats
  • 将请求流量输出到kafka,以便进行流量的异步分析和结果对比。
# --output-kafka-topic: 指定kafka的topic名称
# --output-kafka-host:指定kafka的broker地址
# --input-raw-track-response: 默认只记录请求,此选项可以将响应也一起输出
# --output-kafka-json-format: 使用json格式输出到kafka
gor --input-raw :80 --input-raw-track-response --output-kafka-host "10.255.0.94:9092" --output-kaf-topic "testdd" --output-kafka-json-format

小结

本文主要介绍了一种提前发现新系统中问题的方法:流量复制。先是以golang语言为例介绍了如何复制请求流量并控制转发速率,后又以开源工具goreplay为例,介绍了如何在不侵入代码的情况下完成流量复制。

显然,后者是一种更推荐的选择。不过,如果需要做一些特殊的逻辑和验证,自己实现代码会更灵活些。流量复制还有多种方法,像基于nginx的mirror指令,tcpcopy等,具体可以翻看下面的参考链接阅读更多内容。

参考阅读:

  • goreplay下载地址:https://github.com/buger/goreplay/releases
  • 常见流量复制工具:https://blog.csdn.net/zuozewei/article/details/116466415
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

沉下心来学鲁班

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值