k8s中通过服务名请求tomcat出现http 400

最近在给项目组进行 gitops 部署迁移,可谓是屎山蝶泳。就遇到了一个比较有意思的问题。

  1. 前端进行跨域调用后端域名 xx.super-api.com时发生报错,响应码为 http 400。
  2. 排查 istio 之后发现是 java 代码在 Host 为服务名的时候会出现问题。

问题排查

  1. 首先尝试了去掉了 istio-proxy,但问题依旧。
  2. 通过别的容器通过服务名调用,直接调用xx.super-api请求 400,调用xx.super-api.svc.cluster.local正常
  3. 在容器内直接通过 127.0.0.1 进行调用,服务正常。
  4. 在容器内直接通过 curl 127.0.0.1 进行调用,手动添加通过-H 配置 host。

容器内调用

在容器内手动设置 host 进行调用 curl -i -X POST 127.0.0.1 -H 'host:xx.super-api.com' 能正常响应,而调用curl -i -X POST 127.0.0.1:8080 -H 'host:xx.super-api' 则发生了报错。

源码分析

通过分析代码,解析验证 host 的主要代码位于org.apache.tomcat.util.http.parser.HttpParser.readHostDomainName ,这个方法利用DomainParseState 保存 host 进行的流程。在 tomcat 8.5.31 中,当 host 的最后一段遇到 - 时,则会抛出 host 不合法(如 xx.super-api.com合法,而 xx.super-api不合法)。

DomainParseState的代码中,主要看 ALL_ALPHAALPHAHYPHEN的 allownEnd 取值。 当解析 xx.super-api-时,state 会变成HYPHEN,这时候再解析 e ,state 将会变成ALPHA,最终完成解析时因为 state 为HYPHEN且segmentIndex(当)大于 0,则会抛出异常,导致请求 400。

arthas 追踪请求参数

按照如上的源码分析,可以解释的通为什么服务内手动指定 host 调用时发生的异常。但前端跨越调用的情况下, host 应该是xx.super-api.com,而这从源码看是一个合法的 host,为什么还会出现 400。通过 arthas 分析请求参数。

1
watch org.apache.coyote.AbstractProcessor parseHost '{params,returnObj}' -x 3

通过 curl,请求的 host 为 xx.super-api.com ,但同时通过 arthas 解析到的 host 为 xx.super-api。推测是经过网关进行处理后变成了服务名,而不是原始的 host。

排查网关

通过追逐网关的流量。确定是因为 VirtualService 没有填写 host,导致流量无法正常使用 VirtualService 配置的规则,转发到了特殊的服务,这个特殊的兼容服务会通过服务名在调用到 svc,导致丢失 host。

总结

  1. 升级 tomcat 版本。项目使用的框架基于 sprinboot 1.5.14.RELEASE,内置的 tomcat 版本为 8.5.31,此版本的 tomcat 在对我们目前使用的服务名规则会判断为不合法导致请求 400,而稍微新一些的如 8.5.32 已经没有这个问题。
  2. VirtualService 配置 host,解决流量不能正确路由到 VirtualService 的问题。