hy-app-monitor/tests/test_internal_clb_drain.py
zhx f89ebe797e fix: drain internal CLB targets before restarting service instances
部署/滚动重启非 gateway 实例时,先把该实例在内网 CLB(app-internal/pay-internal)
上的后端权重摘到 0 并等待排空,实例恢复健康后再还原权重。

此前只做 Nacos 摘流,但业务服务之间(如 live -> other)经内网 CLB 固定 VIP
互调,Nacos 摘流对这部分流量无效:容器被杀后 CLB 健康检查(5s x 3 次)摘除
死后端之前,调用方请求持续打到死后端,把调用方线程池拖垮,表现为
“部署 other 时 live 挂掉一个”(health 探测超时 60-70 秒)。

通过 INTERNAL_CLB_IDS 配置启用;未配置时保持历史行为。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 12:56:27 +08:00

128 lines
4.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from __future__ import annotations
import unittest
from unittest.mock import patch
from monitors.yumi.tencent_clb import (
collect_internal_bindings,
collect_internal_health_states,
prepare_internal_clb_snapshot,
)
APP1_OTHER = {
"host": "app-1",
"service": "other",
"ip": "10.2.11.3",
"instanceId": "ins-n566mmeg",
"port": 2400,
}
L4_LISTENERS = [
{
"ListenerId": "lbl-other",
"Protocol": "TCP",
"Port": 2400,
"Targets": [
{"InstanceId": "ins-n566mmeg", "Port": 2400, "Weight": 10, "PrivateIpAddresses": ["10.2.11.3"]},
{"InstanceId": "ins-kauk17la", "Port": 2400, "Weight": 10, "PrivateIpAddresses": ["10.2.12.6"]},
],
},
{
"ListenerId": "lbl-live",
"Protocol": "TCP",
"Port": 2500,
"Targets": [
{"InstanceId": "ins-n566mmeg", "Port": 2500, "Weight": 10, "PrivateIpAddresses": ["10.2.11.3"]},
],
},
]
class InternalClbBindingTests(unittest.TestCase):
def test_collect_internal_bindings_matches_only_service_port_and_host(self) -> None:
# 同一台宿主机同时挂 other-2400 / live-2500只允许命中当前服务自己的监听器。
bindings = collect_internal_bindings(L4_LISTENERS, APP1_OTHER)
self.assertEqual(len(bindings), 1)
self.assertEqual(bindings[0]["listenerId"], "lbl-other")
self.assertEqual(bindings[0]["port"], 2400)
self.assertEqual(bindings[0]["weight"], 10)
self.assertEqual(bindings[0]["instanceId"], "ins-n566mmeg")
@patch("monitors.yumi.tencent_clb.INTERNAL_CLB_IDS", ())
def test_prepare_snapshot_disabled_without_config(self) -> None:
# 未配置 INTERNAL_CLB_IDS 时必须整体跳过,保持历史行为。
self.assertIsNone(prepare_internal_clb_snapshot(APP1_OTHER))
@patch("monitors.yumi.tencent_clb.call_clb_api")
@patch("monitors.yumi.tencent_clb.INTERNAL_CLB_IDS", ("lb-app", "lb-pay"))
def test_prepare_snapshot_collects_matching_clbs_only(self, call_clb_api_mock) -> None:
def fake_describe(method: str, payload: dict) -> dict:
self.assertEqual(method, "DescribeTargets")
if payload["LoadBalancerId"] == "lb-app":
return {"Listeners": L4_LISTENERS}
return {"Listeners": []}
call_clb_api_mock.side_effect = fake_describe
snapshot = prepare_internal_clb_snapshot(APP1_OTHER)
self.assertIsNotNone(snapshot)
self.assertEqual(len(snapshot["groups"]), 1)
group = snapshot["groups"][0]
self.assertEqual(group["loadBalancerId"], "lb-app")
self.assertEqual(group["listenerIds"], ["lbl-other"])
self.assertEqual(len(group["bindings"]), 1)
self.assertEqual(int(group["bindings"][0]["weight"]), 10)
@patch("monitors.yumi.tencent_clb.call_clb_api")
@patch("monitors.yumi.tencent_clb.INTERNAL_CLB_IDS", ("lb-app",))
def test_prepare_snapshot_none_when_service_not_behind_clb(self, call_clb_api_mock) -> None:
call_clb_api_mock.return_value = {"Listeners": []}
self.assertIsNone(prepare_internal_clb_snapshot(APP1_OTHER))
class InternalClbHealthTests(unittest.TestCase):
def test_collect_internal_health_states_flattens_l4_rules(self) -> None:
load_balancers = [
{
"LoadBalancerId": "lb-app",
"Listeners": [
{
"ListenerId": "lbl-other",
"Rules": [
{
"Targets": [
{
"TargetId": "ins-n566mmeg",
"Port": 2400,
"HealthStatus": True,
"HealthStatusDetail": "Alive",
},
{
"TargetId": "ins-kauk17la",
"Port": 2400,
"HealthStatus": False,
"HealthStatusDetail": "Dead",
},
]
}
],
}
],
}
]
snapshot = {"service": APP1_OTHER}
states = collect_internal_health_states(load_balancers, snapshot)
self.assertEqual(len(states), 1)
self.assertEqual(states[0]["targetId"], "ins-n566mmeg")
self.assertTrue(states[0]["healthStatus"])
if __name__ == "__main__":
unittest.main()