1. 引言
重构一个系统时会面临这样一种场景,当经过业务梳理、重新设计、代码重构等一系列环节后,发现最难的却是如何验证优化后系统的正确性。
你可能会说,没有QA吗?这不是QA要保证的吗?
没错,QA是应该做好系统的测试和验证工作,但这并不够。原因是:
- 一个软件系统的复杂性是很高的,研发都无法保证评估到所有Case,又何况站在黑盒外面的QA呢?
- 用户的使用环境和使用场景总会有差异,不论你投入多少测试资源,也无法完全模拟产线所有用户的真实使用场景;
站在最终要对重构结果负责的角度,必须想尽一切办法做好系统的验证工作,尽最大努力减少重构带来的风险。
而最有效的方法就是拿线上真实的用户流量来验证,也就是我们今天要讨论的主题——流量复制。
2. 实现思路
流量复制的本质是将发往旧系统的请求拷贝一份,重新转发给新的系统。流程大概如下:
这个流程分为几个步骤:
- 请求拷贝
- 流量转发
- 结果验证
2.1 请求拷贝
不同语言的实现不同,我们以go语言为例简单说明。
net/http
包下的Request
类型已经自带了拷贝请求的两种方法:浅拷贝和深拷贝。
- 浅拷贝:只复制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
}
- 深拷贝:逐层展开,递归将每层字段都复制一遍,如果想避免请求对象在并发场景下相互影响,可以使用此方法,源码摘抄如下。
// 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进行验证。这种情况下,有几种选择:
- 实时验证:在复制流量时,直接拿到旧系统的response和新系统的response,作二进制内容比较;
- 先收集再验证:把新系统和旧系统的响应结果都异步收集到类似ES的系统中,结合一定的策略作分析比较;
- 请求采样验证:随机复制一定比例的样本流量,肉眼去新系统上观察结果是否如预期,可能比较低效,但有时候只能这样来;
- 字段采样验证:提前按请求定义一套需要验证的关键字段信息,部分关键信息一致即算验证通过;
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