记一次网关代理压测问题:Invalid character in chunk size

背景

  • 使用springmvc实现一个通用网关代理
  • 使用webClient作为请求被代理服务的http客户端
  • 代码大致如下:
@RequestMapping("/proxy/{path}/**")
    public void execute(@PathVariable("path") String path,
                        HttpServletRequest request, HttpServletResponse response) throws IOException {

		// 1. 准备请求, 得到webClient的WebClient.RequestBodyUriSpec proxyRequest
        // 2. 处理一些业务逻辑, 确定被代理服务的请求地址
        // 3. 执行请求
        ResponseEntity<byte[]> proxyResponse = proxyRequest
                .retrieve()
                .toEntity(byte[].class)
                .block();
        
        // 4. 响应透传
        WebProxyUtil.copyResponse(proxyResponse, response);
    }
  • 页面功能正常,但是在压测时会出现1%左右的异常,压测结果如下:
    压测结果

单独压下游未发现问题,所有这个异常确定是代理引入的。

排查过程

  • 首先查看代理服务后台日志,未见异常
  • 压测客户端报错:ChunkedEncodingError(ProtocolError('Connection broken: InvalidChunkLength(…), 0 bytes read)))

至此没有得到足以定位问题的信息,只能进一步wrieshark抓包来排查


  • 通过抓包可以发现,在一次TCP连接的末尾出现异常包,该包的结构不符合http1.1对Transfer-Encoding:chunked的格式
    异常响应
    • 这里的Transfer-Encoding:chunked,其实是http1.1引入的分块传输机制,适用于无法事先确定响应长度的情形。具体可以参考wiki 链接。其结构应该如下:
      chunked结构

从上面的排查可以推测,很可能是响应头设置的问题


  • 来看看WebProxyUtil.copyResponse中响应头复制的相关代码
	HttpHeaders httpHeaders = proxy.getHeaders();
    // 写入响应头
    for (Map.Entry<String, List<String>> entry : httpHeaders.entrySet()) {
        String headerName = entry.getKey();
        List<String> headerValues = entry.getValue();
        for (String headerValue : headerValues) {
            origin.addHeader(headerName, headerValue);
        }
    }

可以看到响应头是全部复制的

原因分析

  • tomcat生成响应的设计与实现可以参考: 链接
  • 我们使用的tomcat-embeded-core的版本为10.1.8,对应的相关代码最终定位到:org.apache.coyote.http11.Http11Processor#prepareResponse
boolean connectionClosePresent = isConnectionToken(headers, Constants.CLOSE);
if {
  // some code
} else {
  // If the response code supports an entity body and we're on
  // HTTP 1.1 then we chunk unless we have a Connection: close header
  if (http11 && entityBody && !connectionClosePresent) {
      outputBuffer.addActiveFilter(outputFilters[Constants.CHUNKED_FILTER]);
      contentDelimitation = true;
      headers.addValue(Constants.TRANSFERENCODING).setString(Constants.CHUNKED);
  } else {
      outputBuffer.addActiveFilter(outputFilters[Constants.IDENTITY_FILTER]);
  }
}
  • 可以看到上述代码是自动维护Connection与Transfer-Encoding这两个请求头的,没有过多考虑业务传入这两个请求头的请况。
  • 尝试同时设置这两个请求头如下:
       origin.addHeader(Constants.TRANSFERENCODING, Constants.CHUNKED);
       origin.addHeader(Constants.CONNECTION, Constants.CLOSE);

这回所有请求都会报错了

总结

  1. http响应头不能随意透传,有些响应头是框架维护的,并且可以影响底层TCP连接。
  2. tomcat对http协议的处理逻辑在Http11Processor,后面可以进一步掌握与学习
  • 18
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值