问题发现
项目中需要对大数据量请求时间进行缩短优化的工作,优化过程中发现,浏览器响应报文压缩方法为br的情况会比gzip的时间要长11-13s,具体表现如下
服务端响应用时45s
但是浏览器等服务端返回却花了58s
这样浏览器就会比服务器响应多等了58-45= 13s,不是很正常
现在直接拿浏览器请求的cUrl 发起请求
可以看到非浏览器请求的响应使用了gzip压缩,总用时48s, 服务端用时46s, 耗时差2s
可见使用gzip压缩算法耗时是远优于br压缩的
解决办法
想办法禁用掉br压缩方法
- 指定service Mesh压缩方法
第一步,检查服务集群是否开启了service mesh,开启后指定才有效
第二步,直接在【通用流量平台->稳定性管理】指定压缩方法
Service mesh 在指定压缩方法后,会对所有请求按指定的压缩算法进行压缩,不管content-length 大小,也不管上游是否已经指定了其他压缩方法,简单粗暴,适合快速解决问题
- TLB + 项目配置
该方法是在探究原因过程中发现,过程比较曲折,需要排查修改两个地方,着急解决问题不适宜
- 确认下自己的服务是否为node服务且有使用koa-compress插件(注意排查框架是否有默认注入),需要将br 压缩算法关闭,具体关闭形式可能因框架不同配置姿势不同,但可以参考下插件官方配置
- 关闭TLB 路由Ngnix默认br 压缩算法配置,禁止使用br算法
虽然复杂,但是方案会比方法一更合理一些
为什么不在发起请求时直接更改accept-encoding?
解决这个问题的另一条途径就是从源头,请求发起端就去掉相关br的设置,也就是更改accept-encoding, 让它不包含br,如果客户端不支持br 压缩,那请求响应自然是不能使用br 压缩的,但是天有不测风云,accept-encoding 是一个不能通过代码去修改的请求报文(详见),所以这条路是行不通的。
这到底是怎么回事?
虽然使用方法一可以快速彻底的解决掉问题,但是不应用方法一时,可以发现的一个明显问题就是不同请求的压缩方法不同,而且存在不使用压缩方法的情况,这就激起了作者尘封已久的好奇心,到底是谁在指定content-encoding呢?
接下来就需要看一下从服务端到客户端,到底是哪个环节在决定content-encoding
Koa-compress
鉴于本人node服务项目基于ACE1.X构建,在搜索代码进行排查时,并没有在配置文件中搜到相关的配置,重新查阅框架文档的时候,才注意到框架有进行默认注入,这就从服务端源头找到了一个会更改content-encoding的地方,俗话说,灯下黑,不过如此。
既然有使用koa-compress, 而且源码不是很复杂,那就简单探索下它的压缩原理
查看源码可知,当content-length大于1024b时,会根据Accept-encoding进行压缩
在Accept-Encoding值是’gzip, deflate, br’情况下
压缩方法的选择逻辑就是accept-encoding有br 会优先使用br,如果br被禁用就使用gzip
由于默认注入时,没有指定压缩阈值,所以当我们的请求数据过大, 大于1024b时,自然就会触发koa-compress进行br压缩,也就是说上面问题的出现,罪魁祸首就是koa-compress
但是当数据量小于1024b时,又会出现br,甚至不进行压缩又是怎么回事呢?
whisle插曲
在排查过程中,相同条件请求,在本地开启whistle代理,通过域名进行本地访问,出现了响应始终是gzip 的情况,这对于大于1024b的响应就不对了,按上面koa-compress逻辑,应该是br才对
经过在http\://localhost:8899/#network 抓包,可以发现whistle给本地服务的请求报文accept-encoding是不带br的
经过与whistle开发者请教(issue),whistle确实会篡改我们的报文,把accept-encoding中的br 去掉,这样就实现了响应始终是gzip压缩的效果,因此,在本地的测试推荐大家直接使用localhost访问,避免代理的干扰
以下在本地进行的测试也均是在关闭代理情况下进行
TLB
根据请求响应链路,响应从node服务返回后,会依次经过Mesh, TLB然后到浏览器
由于mesh 在不指定压缩算法的情况下是不参与压缩的,所以对于小于1024的数据压缩,矛头指向了TLB
在开始验证前,先来了解下TLB的压缩原理TLB压缩问题oncall排查手册
文中对我们比较重要的信息是这部分
文中配置与tlb同学确认后就是默认配置,这样对于我们验证就有了参照物
在关掉koa-compress 的br 压缩后,我进行了如下实验
- 构造响应不同content-length的接口
- 分别通过本地localhost 访问,域名访问,以及关掉tlb 的br 压缩后再通过域名访问以上接口(保证经过tlb)
得到如下结果(no表示不压缩)
content-length | localhost:3000 | 域名访问 | tlb 设置 brotli = off |
---|---|---|---|
117 | no | no | no |
152 | no | br | no |
204 | no | br | gzip |
958 | no | br | gzip |
1208 | gzip | gzip | gzip |
从koa-compress 压缩原理我们可以知道从服务端响应的数据,大于1024采用gzip,小于则不压缩
所以本地访问是符合预期的
经过域名访问,我们可以看到小于1024大于150的响应被用br进行压缩了, 符合br 大于150就压缩
当把tlb 上nginx的br开启指令关掉,我们可以看到小于1024大于200的响应被用gzip压缩了,符合gzip 大于200就压缩的逻辑
再看大于1024的最后一行,当服务端已经指定content-encoding的时候,tlb 是不会进行压缩的,会沿用上游指定压缩算法
综上看来,TLB 会在上游响应未指定content-encoding的时候进行小于1M响应数据的压缩, 默认大于150b时会使用br压缩,大于200b且禁用br情况下才会使用gzip,如果上游指定了content-encoding, 就沿用上游压缩算法
至此,响应报文的content-encoding 来源我们搞清楚了,接下来回到解决办法一,验证下service mesh指定压缩方法后报文变化
集群插曲
虽然文档中指令是默认指令,但不并是所有TLB集群的默认Ngnix 配置,如果出现了与上述结论异常的情况,需要邀请TLB 的同学帮忙查一下域名依赖的TLB 集群是否就是文档中的默认配置(因为只有TLB同学有权限可以查)
比如,相同600B请求,Boe 环境是br压缩,但是线上则变成了gzip
按上面的结论,服务器不会对小于1024的请求进行压缩,经过tlb 默认配置会使用br,boe 环境是正常的,线上是不正常的,经过排查发现,线上tlb 依赖的集群默认配置没有开启br ,所以再走默认配置会进行gzip压缩
Cloud IDE
这里需要注意一点的是,上面我们在发现小于1024的压缩算法异常时,访问的是cloud IDE 上启动项目后帮我们生成的域名,我们在本地请求接口是没有进行压缩的,也就是说cloud IDE生成的域名是有经过TLB的,而且其集群默认开启了br压缩
Service Mesh
实验条件(复现问题):
TLB nginx 不禁用br
不禁用koa=compress的br压缩算法
content-length | localhost:3000 | 域名访问 | 域名访问 |
---|---|---|---|
117 | no | no | gzip |
152 | no | br | gzip |
204 | no | br | gzip |
958 | no | br | gzip |
1208 | br | br | gzip |
我们从浏览器发起请求
在最后一个中间件打印响应头,说明服务器没有参与数据压缩 (可以通过设置priority让中间件在最后一个执行)
然后通过监听端口报文
tcpdump -i eth0 port 3000 -nn
基本上通过上述表现我们基本上是可以判断是mesh 进行了压缩
但是,我们现在监听的是3000配置端口(其他服务监听实例输出的端口)
如果3000端口吐出来的是经过了mesh的话,那通信的结构应该是这样
往深了想一下,上面的判断逻辑并不是非常精确
- node 最后吐出来的数据的header 可能跟我们上面在最后一个中间件打印的header并不同,也就是说我们在最后一个中间件打印的header 并不是最终实例吐出数据的header,有一些 header 是会在最后吐数据的时候装的
- Gzip 的请求头真的是mesh 加上去的吗?实例和mesh 之间不会还有其他服务?
要解决上面两个疑问,就要想办法去抓取一下mesh 接收的数据,也就是服务吐给mesh的数据
抓取mesh socket
当给服务开启mesh 服务时,mesh 会给环境注入一些环境变量
其中SERVICE_MESH_HTTP_EGRESS_ADDR 这个变量对应的地址就是服务交给mesh 转发的数据
即服务会往这个地址吐数据,然后再由mesh从这里转发再吐出去
那我们接下来就要想办法去读这个socket
通过tcpdump对Unix Domain Socket 进行抓包解析
当我打算用curl 命令去执行相关方法时,却发现没有相应地址的socket
ok,拉mesh 同学onCall 说这种情况是因为服务器和mesh之间不是用的uds通信,用的ip PORT通信
抓取PORT 9507
那我现在需要找到MESH_EGRESS_PORT具体是什么
无论是通过打印环境变量
还是通过 cat /proc/\${pid}/environ 查看配置文件,以及通过查看监听端口
基本都确定lookback通信的port 是9507
Ok 那我们再回到用tcpdump 抓包的方式,会发现什么也抓不到
陷入死胡同, 那就是说没有数据包经过mesh 接收数据的端口
重新认识Service Mesh
入流量
我们之前是通过入流量开启压缩算法的
入流量在整个通信链路中的作用是这样的
所以现在需要抓取的是入流量的端口ByteMesh WebSocket & HTTP/1.1 & HTTP/2协议接入约定
需要找到MESH_INGRESS_PORT 通过查看pid 下面 environ 文件可以看到port 为3000,也就是配置端口
然后尝试监听 https://www.cnblogs.com/zgq123456/articles/10251860.html
tcpdump -i any -A -s 0 ‘tcp port 3000 and (((ip[2:2] - ((ip[0]&0xf)<<2)) - ((tcp[12]&0xf0)>>2)) != 0)’
这一次看到了报文的整个变化过程如上两图
出流量
通过给服务开启出流量代理,可以看到两种通信地址
SERVICE_MESH_EGRESS_ADDR 即如果node 跟下游服务通信会走dsl 通信
MESH_EGRESS_PORT 即如果node 发起http 请求会通过这个端口与mesh 进行http 通信,过程同上面入流量过程
看一下UDS报文长啥样
还是参考这篇文章的方法
- 为方便后续指令执行,切到rpc.egress.sock所在文件夹,
- 将给到rpc.egress.sock 的数据转发到 8089
- 用curl 发起请求,并用tcpdump 对8089进行抓包
注意使用curl –-unix-socket /rpc.egress.sock 时 如果不支持–-unix-socket 参数,需要使用apt-get 升级curl 版本,如无法升级,可能是linux 版本不再维护,可尝试替换基础镜像(指定高版本linux 的)进行部署后再测试 虽然位于rpc.egress.sock 所在文件夹下执行,但是前面的/ 不能省
先用tcpdump -i any -netvv port 8089 看看能不能
加上-A -s , ===> tcpdump -i any -A -s 0 -netvv port 8089 看看具体报文
知识收获
cUrl
cUrl 命令相关参数
-v/–verbose 用于打印更多信息,包括发送的请求信息
-o /dev/null 把输出写到该文件中,保留远程文件的文件名
-w ‘%{size_download}\n’ 获取下载大小
--unix-socket 测试socket 地址,注意要求curl 版本7.50+,如果webshell 不支持,需要考虑更换tce基础镜像
常用linux命令
tcpdump
tcpdump -i eth0 port 3000 -nn
tcpdump -i eth0 -nn -vv
tcpdump -i lo -nn -vv
https://www.cnblogs.com/zgq123456/articles/10251860.html
查看
lsof -i | grep LISTEN
ps -le
ps -ef | grep node
安装
apt/apt-get update
apt/apt-get install 包名