Flink 使用 Hutool 调用 Kubernetes Service 负载不均处理笔记
1. 处理背景与目标
1.1 处理背景
Flink 作业中通过 Hutool 调用后端接口:
post = HttpUtil.post(requstUrl, JSONObject.toJSONString(cbtMessages));
调用目标为 Kubernetes 集群内的 Service,后端由多个 Pod 副本提供服务。
运行过程中发现:
- 后端 Pod 之间请求负载不均。
- 部分 Pod 请求量明显偏高,其他 Pod 请求量偏低。
- 前面增加
Nginx/Gateway后,负载会更均衡。 - 但代理层会增加一跳转发、资源消耗和运维复杂度。
因此,本次处理优先考虑在代码层调整连接行为,避免额外引入代理组件。
1.2 处理目标
本次处理目标如下:
- 核实 Hutool
HttpUtil.post默认连接行为。 - 分析 Flink 直连 Kubernetes Service 时后端 Pod 负载不均的原因。
- 通过最小化代码调整改善后端 Pod 请求分布。
- 验证短连接方案对成功率、响应耗时和后端资源消耗的影响。
- 明确异常观察项、回滚方式和后续优化方向。
2. 前置输入与依赖条件
2.1 前置输入
业务调用点:
post = HttpUtil.post(requstUrl, JSONObject.toJSONString(cbtMessages));
Hutool HttpRequest.keepAlive(boolean) 关键源码:
public HttpRequest keepAlive(boolean isKeepAlive) {
header(Header.CONNECTION, isKeepAlive ? "Keep-Alive" : "Close");
return this;
}
Hutool HttpRequest.isKeepAlive() 关键源码:
public boolean isKeepAlive() {
String connection = header(Header.CONNECTION);
if (connection == null) {
return !httpVersion.equalsIgnoreCase(HTTP_1_0);
}
return false == "close".equalsIgnoreCase(connection);
}
2.2 依赖条件与关键假设
本次分析基于以下条件:
- Flink 作业通过 Kubernetes
Service调用后端接口。 - 后端服务存在多个 Pod 副本。
- 当前请求未显式设置
Connection: Close。 - 调用场景为常见 HTTP/1.1 行为。
- Kubernetes Service 通常在新 TCP 连接建立时选择后端 Pod。
- 当前诉求是不新增
Nginx/Gateway等代理层。
2.3 环境等级判断
当前按 dev/test 场景处理,不默认引入以下复杂方案:
- Service Mesh。
- 新增 Gateway。
- 完整连接池改造。
- 网络治理平台化改造。
- 额外高可用或流量治理组件。
3. 详细处理步骤
3.1 核实 Hutool 默认连接行为
根据 Hutool HttpRequest.isKeepAlive() 源码:
if (connection == null) {
return !httpVersion.equalsIgnoreCase(HTTP_1_0);
}
可以得出:
- 如果未设置
Connection请求头。 - 且 HTTP 版本不是
HTTP/1.0。 - Hutool 默认认为该请求使用
Keep-Alive。
结论:
HttpUtil.post(...) 在 HTTP/1.1 场景下默认倾向长连接。
3.2 分析 Kubernetes Service 负载不均原因
Kubernetes Service 通常在新 TCP 连接建立时选择后端 Pod。
简化链路:
Flink Task
|
v
Kubernetes Service
|
v
Backend Pod
当 Flink 客户端复用长连接时,后续多个 HTTP 请求会继续走同一个后端 Pod。
示例:
Flink SubTask 1 -> Service -> Pod A
Flink SubTask 2 -> Service -> Pod A
Flink SubTask 3 -> Service -> Pod B
Flink SubTask 4 -> Service -> Pod C
可能结果:
- 少数 Pod 承担大量请求。
- 其他 Pod 请求量偏低。
- 后端副本未被充分利用。
- 高负载 Pod 响应耗时升高。
- P95 / P99 尾延迟变差。
3.3 评估代理层方案
增加 Nginx 或 Gateway 后,请求链路变为:
Flink
|
v
Nginx / Gateway
|
v
Backend Pod
代理层可以在应用层重新分发请求,因此可以改善后端 Pod 负载不均。
但该方案存在额外代价:
- 多一跳网络转发。
- 多一层代理处理成本。
- 增加部署组件。
- 增加故障点。
- 增加运维复杂度。
本次决策:
不优先增加代理层,先通过代码层最小化调整验证。
3.4 确定最小化代码调整方案
原始代码:
post = HttpUtil.post(requstUrl, JSONObject.toJSONString(cbtMessages));
建议调整为:
post = HttpRequest.post(requstUrl)
.keepAlive(false)
.setConnectionTimeout(3000)
.setReadTimeout(30000)
.body(JSONObject.toJSONString(cbtMessages))
.execute()
.body();
核心配置:
.keepAlive(false)
该配置会设置请求头:
Connection: Close
预期效果:
- 每次请求完成后关闭连接。
- 下次请求重新建立 TCP 连接。
- 新连接重新经过 Kubernetes Service 选择后端 Pod。
- 后端 Pod 请求分布更均衡。
3.5 验证修改效果
修改后通过监控观察:
- 后端 Pod 请求负载明显更平均。
- 服务实例负载分布改善。
- 接口成功率保持稳定。
- 响应耗时未明显恶化。
- 趋势图中短连接后平均响应时间及尾延迟下降。
对比结论:
9 点:未改短连接
11 点:已改短连接
观察结果:
短连接后 Pod 负载更均衡
Success Rate 保持 100%
Avg Response Time 下降
P95 / P99 尾延迟下降
4. 关键决策点与异常处理机制
4.1 关键决策点
决策点一:是否增加代理层
决策:
不增加 Nginx / Gateway,优先代码层最小化调整。
原因:
- 代理层虽然能改善负载均衡。
- 但会增加链路复杂度和额外消耗。
- 当前问题可通过调整连接行为解决。
决策点二:是否关闭 Keep-Alive
决策:
显式关闭 Hutool HTTP Keep-Alive。
原因:
- 当前负载不均与长连接复用高度相关。
- Kubernetes Service 对长连接不会按每个请求重新分发。
- 关闭 Keep-Alive 后,Pod 负载分布明显改善。
决策点三:短连接成本是否可接受
短连接可能增加:
- TCP 建连成本。
- 服务端
accept压力。 - 系统态 CPU。
TIME_WAIT数量。- HTTPS 场景下 TLS 握手成本。
当前观察结果:
短连接成本未造成明显负面影响,负载均衡收益大于连接开销。
4.2 异常处理机制
4.2.1 响应耗时异常
观察指标:
Avg Response TimeP95P99- 最大响应时间
异常表现:
P95 / P99 明显升高
接口超时增加
处理方式:
回滚 keepAlive(false),恢复原长连接逻辑。
4.2.2 成功率下降
观察指标:
Success Rate5xxtimeout
异常表现:
Success Rate 下降
5xx 增加
请求超时增加
处理方式:
优先回滚代码,再检查后端线程池、连接队列和服务端日志。
4.2.3 连接状态异常
检查命令:
ss -ant | awk '{print $1}' | sort | uniq -c
重点关注:
ESTAB
TIME-WAIT
CLOSE-WAIT
SYN-RECV
异常判断:
TIME-WAIT可以增加,但不应持续异常堆积。CLOSE-WAIT不应持续增加。SYN-RECV不应持续增加。
4.2.4 Flink 反压异常
观察指标:
- Flink Backpressure。
- Checkpoint 时长。
- Task 处理速率。
异常表现:
Backpressure 增加
Checkpoint 变慢
Task 吞吐下降
处理方式:
回滚短连接配置,检查后端响应耗时及 Flink 同步调用阻塞情况。
5. 最终输出结果及后续优化建议
5.1 最终输出结果
本次处理结果如下:
Hutool HttpUtil.post 在 HTTP/1.1 下默认倾向 Keep-Alive。
Kubernetes Service 通常在建连时选择后端 Pod。
长连接复用会导致请求持续绑定到部分 Pod。
显式设置 keepAlive(false) 后,后端 Pod 负载明显更均衡。
短连接后成功率保持稳定,平均响应时间和尾延迟未恶化。
当前推荐保留方案:
post = HttpRequest.post(requstUrl)
.keepAlive(false)
.setConnectionTimeout(3000)
.setReadTimeout(30000)
.body(JSONObject.toJSONString(cbtMessages))
.execute()
.body();
5.2 验证清单
后续上线或压测时,建议持续观察:
- 后端各 Pod 请求分布。
- 接口
Success Rate。 Avg Response Time。P95/P99。- 后端 Pod CPU。
TIME-WAIT/CLOSE-WAIT/SYN-RECV。- Flink Backpressure。
- Checkpoint 时长。
5.3 回滚方案
如果短连接导致明显负面影响,可快速回滚为原始写法:
post = HttpUtil.post(requstUrl, JSONObject.toJSONString(cbtMessages));
或移除:
.keepAlive(false)
回滚后重新发布 Flink 作业即可。
5.4 后续优化建议
如果完全短连接在更高 QPS 下带来明显连接成本,可考虑折中方案:
短 Keep-Alive + 连接 TTL
示例策略:
keepAliveMs = 3000
connectionTtlMs = 30000
优化目标:
- 允许连接短时间复用,降低建连成本。
- 避免连接长期绑定到固定 Pod。
- 定期重建连接,让 Kubernetes Service 有机会重新选择后端 Pod。
- 在连接成本和负载均衡之间取得平衡。
可选实现方向:
- Apache HttpClient。
- OkHttp。
当前建议:
先保留 keepAlive(false) 的最小化方案。
只有在短连接成本被监控明确验证为瓶颈后,再考虑引入连接池与连接 TTL。