在如今的数字化时代,业务的稳定运行与高效迁移至关重要。最近,我们团队在将业务配置的负载均衡迁移至深信服应用交互中心 AD(类似硬件负载均衡器 F5)的过程中,遭遇了一场棘手的技术难题 —— 配置业务健康检测时,响应状态码突然出现 400,这一问题如同一颗定时炸弹,严重威胁着业务的正常运行。经过一系列惊心动魄的排查与尝试,我们终于成功解决了这一难题。今天,就带大家一同回顾这段充满挑战与突破的技术历程,希望能为各位技术同仁提供宝贵的经验与启示。
一、问题浮现:意料之外的 400 状态码
深信服应用交互中心 AD 提供了丰富多样的健康策略,足足有十几种之多。我们依据实际需求,选择了 HTTP 方式进行配置。按照官方给出的配置示例,
我们以相对路径 “/a/b” 的模式进行设置,监视地址则配置为云厂商的 ELB。然而,就在我们满心期待一切顺利通过检测时,意外却发生了。同样的健康检测配置信息,除了端口不同,springcloud gateway 能够顺利返回状态码 200,而检测 springboot 项目时,却无情地返回了 400 状态码。更为诡异的是,无论是通过浏览器、postman,还是直接进入宿主机访问 springboot 项目的健康检测地址,响应状态码均显示为 200。这一现象让整个团队陷入了深深的困惑,问题究竟出在哪里?
二、艰难排查:迷雾重重下的探索
(一)云厂商 ELB 疑云
遇到问题,我们首先想到的便是求助于云厂商。毕竟,使用云服务的一大优势就是在遇到技术难题时能够得到专业的支持。于是,我们迅速联系了云厂商的技术团队,他们迅速组织了多波相关人员展开排查工作。经过整整 3 天的不懈努力,进行了各种手段的定位检测,然而,他们传递给我们的消息却让问题变得更加扑朔迷离。虽然没有明确的结论,但他们暗示问题很可能出在我们的业务代码上。
(二)业务代码的自我审视
业务的健康检测配置使用的是 springboot 提供的 actuator/health 端点,直觉告诉我们,健康检测代码出现问题的可能性微乎其微。但云厂商的判断让我们不得不重新审视这一观点。为了深入排查问题,我们祭出了强大的工具 ——arthas。通过在 idea 中安装 arthas 插件,我们轻松地执行了跟踪命令:
trace -E org.springframework.boot.actuate.health.HealthIndicator health -n 5 --skipJDKMethod false '1==1'
注: 这个命令无需要自己敲,idea装下arthas idea插件,选中要进行跟踪的方法,右键选中自己想要的执行命令
这条命令的目的是对 HealthIndicator 进行深度跟踪,以便发现潜在的问题。然而,跟踪结果却让我们大失所望,始终没有进入到这个方法。这让我们再次怀疑问题出在 ELB 上,于是我们将相关证据反馈给云厂商。云厂商的技术人员依然保持着专业和耐心,与我们一同再次进行深入定位。经过一番努力,他们斩钉截铁地告诉我们,请求肯定已经到达了后端,400 状态码就是后端服务抛出的。
既然云厂商如此确定,我们只能再次将排查重点转移到业务代码上。我们利用 actuator 动态调整日志级别的功能,将日志级别调整为 DEBUG,以便获取更详细的信息。
curl -X POST "http://localhost:8550/actuator/loggers/root" -H "Content-Type: application/json" -d '{"configuredLevel": "DEBUG"}'
这一操作的前提是 loggers 端点处于激活状态,相关的动态调整日志级别代码可参考org.springframework.boot.actuate.logging.LoggersEndpoint。日志级别调整后,我们终于发现了一条关键线索。
借助这条线索,我们向 AI 发起了求助。
AI 给出的答案清晰明了 ——HOST头丢失了。为了进一步验证这一结论,我们再次使用 arthas 进行跟踪:
trace -E org.apache.coyote.http11.Http11Processor|org.springframework.boot.actuate.health.HealthIndicator service|health -n 5 --skipJDKMethod false '1==1'
这次,我们成功跟踪到了相关内容,终于确定 400 状态码确实是由业务代码抛出的。深入跟踪后发现,问题出在
org.apache.coyote.http11.Http11Processor#prepareRequest
中的一段代码:
// Check host header
MessageBytes hostValueMB = null;
try {
hostValueMB = headers.getUniqueValue("host");
} catch (IllegalArgumentException iae) {
// Multiple Host headers are not permitted
badRequest("http11processor.request.multipleHosts");
}
if (http11 && hostValueMB == null) {
badRequest("http11processor.request.noHostHeader");
}
三、多管齐下:寻求最佳解决方案
方案一:Tomcat 的 Pipeline-Valve 补救
我们首先想到的是在 Http11Processor 进行检测前,通过 tomcat 的 pipline-valve 机制塞一个默认的 host。具体做法是将 valve 塞到 Engine 或者 Host 元素中。然而,经过深思熟虑,我们认为这个方案存在较大的风险。客户端没有传递的信息,由服务端来兜底,不仅不符合常规的设计理念,而且可能会引发一系列意想不到的问题,因此我们果断放弃了这一方案。
方案二:配置全路径的尝试
交互应用 AD 提供了 3 种 http uri 的配置方式,我们尝试通过配置域名全路径,如
http://healthcheck.com/actuator/health/ping,来解决问题。这一方法确实成功解决了 400 状态码的问题,但我们还有一个重要的需求 —— 当出现非 200 状态码时,能够实现故障转移。而通过配置域名方式进行检测,无法满足这一需求,因此这个方案也不得不被舍弃。
方案三:补充缺失的 Host
在 AD.7.0.26 及以上版本中,http 健康检测新增了可以塞 host 值的文本框。
如图
理论上,通过配置 host 值就可以解决我们的问题。然而,由于我们使用的版本较低,并不具备这一功能。无奈之下,我们只能另辟蹊径。在仔细研究 http 检测文档后,我们发现可以采用 connect(TCP)来配置健康检测。
配置说明如下
最初,我们的发送内容设置如下:
GET /actuator/health/ping HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n
并设定接收内容必须包含 200。本以为问题就此解决,然而结果却依然不尽人意,检测仍然异常。通过 telnet 验证,我们发现出现了 505 状态码。经过一番调试,我们最终找到了正确的配置方式(如下图):
严格按照 http 请求的协议,在内容后面追加空行(即按两次 enter 键),以区分 header 和 body。通过这种方式,我们终于成功解决了 400 状态码的问题。
四、经验总结:复盘与启示
回顾整个排查过程,其实我们一开始就注意到云厂商 ELB 的日志中,抓到的请求是 “http:///actuator/health/ping”,当时我们也曾怀疑过请求头是否缺失。但由于 springcloud gateway 在同样的配置下没有出现问题,这一现象成功转移了我们的排查方向。现在看来,这是因为 springcloud gateway 底层使用的是 netty 容器,与 springboot 默认的 tomcat 容器对 host 的处理方式存在差异。在本次问题的定位过程中,AI 和 arthas 发挥了至关重要的作用,为我们提供了关键的线索和深入排查的手段。
最后,为了方便各位技术同仁查阅相关资料,特附上深信服交互应用 AD 的官方文档链接:https://support.sangfor.com.cn/productDocument/read?product_id=31&version_id=997&category_id=273432
希望本次的技术复盘能够对大家在处理类似问题时有所帮助,让我们共同在技术的道路上不断探索前行!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。