-
Notifications
You must be signed in to change notification settings - Fork 0
Cluster
多台机器分担请求,通常是为了多个出口 IP,其次才是吞吐。IPClick 有两种集群形态,
用 [CLUSTER].forward 选。不需要 Redis 之类的中间件。
┌─→ 节点 A
调用方 ──┼─→ 节点 B
└─→ 节点 C
调用方(ClusterDownloader)自己持有全部节点地址,按策略挑一台直连。
- ✅ 少一跳,不占任何节点的入口带宽
- ✅ 故障转移在客户端做,最快
- ❌ 调用方必须能连到所有节点
┌─→ 节点 B
调用方 ──→ 节点 A ──┼─→ 节点 C
└─→ 自己执行
调用方只需要知道一个地址。A 按策略挑节点:挑到自己就本地干,挑到别人就把请求 原样转过去,拿到结果再回给调用方。
- ✅ 调用方只需一个地址,防火墙规则简单
- ✅ 子节点可以完全不对调用方暴露
- ❌ 多一跳;响应体要经过入口节点,占它的带宽
四条性质,配之前值得知道:
-
只转一跳。 转发时带
ipclick-forwarded标记,收到带标记的请求一律本地执行。 环路在协议层就不可能出现,不靠 TTL 计数之类的兜底。 -
任意节点都能当入口。 五台机器可以用完全相同的配置 + 相同的
.env, 谁被访问谁就是入口。平时只把流量打给 A,A 挂了直接改指向 B。 -
入口自己也干活(只要它在
nodes里)。子节点全挂时入口会自己兜底, 而不是让请求失败。 -
流式下载(
stream=True)不转发,永远由收到请求的节点自己执行。 把每个分片再中转一次会让入口带宽翻倍。要让子节点出流量就用客户端分发模式直连它。
[GENERAL]
mode = "cluster" # standalone / cluster / auto
[CLUSTER]
forward = "on" # off = 客户端分发;on = 服务端转发
self_id = "" # 本节点在 nodes 里的 id;留空则自动识别
forward_timeout = 0 # 0 = 自动推算
load_balancer = "round_robin" # round_robin / random / weight
failure_threshold = 2 # 连续失败几次摘除
recovery_threshold = 2 # 连续成功几次加回
probe_interval = 10 # 探活间隔(秒)
probe_timeout = 3
max_failover = 2 # 一次请求最多换几个节点
nodes = [
{ id = "node-101", address = "192.168.1.101:9527", region = "us-east-1", zone = "1a", weight = 100 },
{ id = "node-102", address = "192.168.1.102:9527" },
{ id = "node-103", address = "192.168.1.103:9527", token = "..." },
]forward = "on" 时这份列表就是集群全貌,包括本机自己——本机也要列进来才会分到活。
节点要知道"我是谁",否则会把请求转给自己、或者永远不给自己派活。
解析顺序:
[CLUSTER].self_id- 环境变量
IPCLICK_CLUSTER_SELF_ID← 多台机器共用一份配置时用这个 - 按
[SERVER]的监听端口 + 本机地址自动识别
识别不出来会打警告,且本节点只转发不执行任务——这是安全的降级(不会转给自己造成 环路),但会白白浪费一台机器的算力,所以别忽略这条警告。
所有节点放同一个共享密钥,每台的令牌由它派生:
token = HMAC-SHA256(secret, "ipclick-node:" + node_id)
# .env(所有节点相同)
IPCLICK_CLUSTER_SECRET=<一串随机>这样安排的原因:
- 每台机器的令牌各不相同——拿到 B 的令牌不能调 C
- 但谁都不用抄谁的令牌,加机器不用发新凭据
- 密钥只有一个,轮换只改一处
想给某台单独指定令牌(比如是别人维护的节点),在它的 nodes 条目里写 token = "...",
会覆盖派生值。
ipclick config-info 会显示集群密钥是否配了、来自哪里。密钥缺失时会告警——
无鉴权的转发集群等于任何人都能拿它当跳板。
[CLUSTER.discovery]
mode = "static" # static / dns
dns_name = "" # 例如 "ipclick.default.svc.cluster.local"
port = 9527
refresh_interval = 30 # 0 = 只在启动时解析一次-
static(默认)—— 用上面的nodes。扩缩容要改配置重启。 -
dns—— 解析一个域名,每条 A/AAAA 记录就是一个节点,后台定期重解析。 K8s 的 headless Service、Consul DNS、云内网负载均衡域名都能直接用。
DNS 模式下扩缩容不用改配置,也不用重启——新 Pod 起来,下一轮解析就带上了。
| 策略 | 行为 |
|---|---|
round_robin(默认) |
轮询,最均匀 |
random |
随机 |
weight |
按 nodes 里的 weight 加权,机器配置不一致时用 |
- 探活走
grpc.health.v1(免鉴权),间隔probe_interval -
连续
failure_threshold次失败才摘除,连续recovery_threshold次成功才加回。 用连续计数而不是单次判定,是为了避免一次网络抖动就让流量反复横跳 - 一次请求最多换
max_failover个节点
什么才算"节点故障":只有 gRPC 的 UNAVAILABLE(连接压根没建起来)会把节点标记为
不健康。DEADLINE_EXCEEDED、INVALID_ARGUMENT 这些不会——目标站点慢或者参数写错
是任务的问题,不是节点的问题。把它们算成节点故障的话,抓一个慢站点就能把整个集群摘空。
forward_timeout = 0 # 0 = 自动推算自动推算的公式:任务自身的 timeout × (重试次数 + 1) + 15s 余量。
浏览器适配器另算——冷启动要拉起浏览器进程,会额外给出宽限。写死一个值的话, 要么普通请求等太久,要么浏览器请求还没热起来就被掐断。
# ipclick.toml —— 五台完全一样
[GENERAL]
mode = "cluster"
[CLUSTER]
forward = "on"
nodes = [
{ id = "n1", address = "10.0.0.1:9527" },
{ id = "n2", address = "10.0.0.2:9527" },
{ id = "n3", address = "10.0.0.3:9527" },
{ id = "n4", address = "10.0.0.4:9527" },
{ id = "n5", address = "10.0.0.5:9527" },
]# 每台机器的 .env 只差一行
IPCLICK_CLUSTER_SECRET=<同一串>
IPCLICK_CLUSTER_SELF_ID=n1 # n2 / n3 / n4 / n5调用方:
Downloader(host="10.0.0.1", port=9527, token="<n1 的派生令牌>")A 挂了就把 host 改成 10.0.0.2,不用动任何节点的配置。
[GENERAL]
mode = "cluster"
[CLUSTER]
forward = "on"
[CLUSTER.discovery]
mode = "dns"
dns_name = "ipclick.default.svc.cluster.local" # headless Service
port = 9527
refresh_interval = 30self_id 用 downward API 注入 IPCLICK_CLUSTER_SELF_ID(一般用 Pod IP 或 Pod 名)。
[CLUSTER.status_page]
enabled = false
port = 9529
host = "127.0.0.1" # 默认只监听本机只读页面:节点列表、健康状态、请求统计。不提供任何变更操作——运维变更请改配置文件, 网页改配置等于再开一个高价值攻击面。它会暴露内网节点地址与拓扑,所以默认只监听本机; 要远程访问请自行加反向代理并做鉴权。
(这跟 Web 管理端 是两个东西:状态页只读、无登录、只看集群; Web 管理端有登录、能试请求、能改白名单内的配置。)
from ipclick import ClusterDownloader, create_client
# 显式
with ClusterDownloader() as d:
resp = d.get("https://example.com")
print(resp.trace.node_id) # 实际哪台执行的
# 或让配置决定
with create_client() as d: # 按 [GENERAL].mode
...mode = "cluster" 却没配任何节点会直接报错,不会静默退回单机——静默退回会让你以为
集群生效了,实际所有流量都打在一台上,也没有故障转移。
Web 管理端的 /nodes 页可以增删改节点,写回
[CLUSTER].nodes。0.4 的三处变化都直接影响加机器这件事:
0.3 里保存只是改文件,页面上明写着"改完需要重启才生效"——真正在路由的
ClusterConfig 与 NodePool 在构造时就建好存死了,生命周期等于进程生命周期。
0.4 保存完会原地重建它们:新节点立刻参与转发轮询,被移除或改了地址的节点连接会被 关掉。重建时按 id 复用已有的节点状态——直接重建会把健康计数清零,那样 "连续 N 次才切状态"的判定永远达不到,熔断与恢复双双失效。
热更新覆盖节点列表、权重、策略、阈值;监听端口这类要重建 gRPC server 的项不在范围内 (但改节点列表本来也不会动到端口)。DNS 发现模式下节点是解析出来的,热更新只改策略 与阈值、不动节点列表。
每行一个按钮,只验连通性与集群内部鉴权,不发业务请求。
0.3 里加完节点只能等真实流量转过去才发现连不上,而那时错误已经混在业务失败里了。
| 结果 | 去查什么 |
|---|---|
| 连不上 | 进程、防火墙、地址写没写对 |
| 鉴权不通过 | 各节点 .env 里的 IPCLICK_CLUSTER_SECRET 是否完全一致 |
| 通过(对方未设防) | 那台没启用鉴权,任何人都能调它 |
为什么不能只用健康检查:grpc.health.v1 刻意免鉴权(编排系统的探针通常拿不到
密钥),所以在它眼里"那台机器没起来"和"起来了但我的令牌不对"长得一模一样——而这两件
事的排查方向完全相反。
所以探两层:健康检查回答"连得上吗",0.4 新增的 Ping RPC(走鉴权、不做任何业务
动作)回答"令牌对吗"。第二层的返回码就是结论:
| 返回 | 结论 |
|---|---|
OK |
鉴权通过(响应里还带对端的 id、版本、是否启用鉴权、是否开着转发、在途数) |
UNAUTHENTICATED |
令牌不匹配 |
UNIMPLEMENTED |
鉴权是通的,只是对端还是 0.3(那个版本没有 Ping) |
最后一种必须单独说,否则滚动升级期间会被误报成鉴权失败。
对端还会自报 auth_required——探测成功本身分不清"我的令牌对"和"它根本不验"。
如果对方自报的 id 和你列表里写的对不上,也会提示:转发的路由与链路记录里的 "谁执行的"都以列表里那个为准,对不上两边的记录就拼不到一起。
试一试页多一个"目标节点"下拉,选中后跳过负载均衡 强制打到那一台——验证新加的机器配对没有,不用再反复点、靠轮询碰运气命中。
resp.trace.node_id 是实际执行的那台。连着发几个请求,node_id 应该在节点之间轮转:
with Downloader(host="10.0.0.1") as d:
for _ in range(6):
print(d.get("https://example.com").trace.node_id)
# n1 n2 n3 n4 n5 n1Web 管理端的请求流页也能实时看到分发情况,每条记录都带执行节点。
想确定性地验证某一台,用试一试的"目标节点"下拉点名, 比连点几次靠轮询命中可靠得多。