Skip to content

Cluster

HadesTop edited this page Aug 17, 2026 · 8 revisions

集群

多台机器分担请求,通常是为了多个出口 IP,其次才是吞吐。IPClick 有两种集群形态, 用 [CLUSTER].forward 选。不需要 Redis 之类的中间件。

两种形态

客户端分发(forward = "off",默认)

        ┌─→ 节点 A
调用方 ──┼─→ 节点 B
        └─→ 节点 C

调用方(ClusterDownloader)自己持有全部节点地址,按策略挑一台直连

  • ✅ 少一跳,不占任何节点的入口带宽
  • ✅ 故障转移在客户端做,最快
  • ❌ 调用方必须能连到所有节点

服务端转发(forward = "on"

                    ┌─→ 节点 B
调用方 ──→ 节点 A ──┼─→ 节点 C
                    └─→ 自己执行

调用方只需要知道一个地址。A 按策略挑节点:挑到自己就本地干,挑到别人就把请求 原样转过去,拿到结果再回给调用方。

  • ✅ 调用方只需一个地址,防火墙规则简单
  • ✅ 子节点可以完全不对调用方暴露
  • ❌ 多一跳;响应体要经过入口节点,占它的带宽

四条性质,配之前值得知道:

  1. 只转一跳。 转发时带 ipclick-forwarded 标记,收到带标记的请求一律本地执行。 环路在协议层就不可能出现,不靠 TTL 计数之类的兜底。
  2. 任意节点都能当入口。 五台机器可以用完全相同的配置 + 相同的 .env, 谁被访问谁就是入口。平时只把流量打给 A,A 挂了直接改指向 B。
  3. 入口自己也干活(只要它在 nodes 里)。子节点全挂时入口会自己兜底, 而不是让请求失败。
  4. 流式下载(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" 时这份列表就是集群全貌,包括本机自己——本机也要列进来才会分到活。

节点身份(self_id

节点要知道"我是谁",否则会把请求转给自己、或者永远不给自己派活。

解析顺序:

  1. [CLUSTER].self_id
  2. 环境变量 IPCLICK_CLUSTER_SELF_ID多台机器共用一份配置时用这个
  3. [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_EXCEEDEDINVALID_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,不用动任何节点的配置。

K8s

[GENERAL]
mode = "cluster"

[CLUSTER]
forward = "on"

[CLUSTER.discovery]
mode = "dns"
dns_name = "ipclick.default.svc.cluster.local"   # headless Service
port = 9527
refresh_interval = 30

self_id 用 downward API 注入 IPCLICK_CLUSTER_SELF_ID(一般用 Pod IP 或 Pod 名)。

集群状态页

[CLUSTER.status_page]
enabled = false
port = 9529
host = "127.0.0.1"      # 默认只监听本机

只读页面:节点列表、健康状态、请求统计。不提供任何变更操作——运维变更请改配置文件, 网页改配置等于再开一个高价值攻击面。它会暴露内网节点地址与拓扑,所以默认只监听本机; 要远程访问请自行加反向代理并做鉴权。

(这跟 Web 管理端 是两个东西:状态页只读、无登录、只看集群; Web 管理端有登录、能试请求、能改白名单内的配置。)

客户端 API

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 管理端管节点

Web 管理端/nodes 页可以增删改节点,写回 [CLUSTER].nodes。0.4 的三处变化都直接影响加机器这件事:

1. 保存即生效,不用重启

0.3 里保存只是改文件,页面上明写着"改完需要重启才生效"——真正在路由的 ClusterConfigNodePool 在构造时就建好存死了,生命周期等于进程生命周期。

0.4 保存完会原地重建它们:新节点立刻参与转发轮询,被移除或改了地址的节点连接会被 关掉。重建时按 id 复用已有的节点状态——直接重建会把健康计数清零,那样 "连续 N 次才切状态"的判定永远达不到,熔断与恢复双双失效。

热更新覆盖节点列表、权重、策略、阈值;监听端口这类要重建 gRPC server 的项不在范围内 (但改节点列表本来也不会动到端口)。DNS 发现模式下节点是解析出来的,热更新只改策略 与阈值、不动节点列表。

2. 「测试连接」分得清"连不上"和"鉴权不通过"

每行一个按钮,只验连通性与集群内部鉴权,不发业务请求

0.3 里加完节点只能等真实流量转过去才发现连不上,而那时错误已经混在业务失败里了。

结果 去查什么
连不上 进程、防火墙、地址写没写对
鉴权不通过 各节点 .env 里的 IPCLICK_CLUSTER_SECRET 是否完全一致
通过(对方未设防) 那台没启用鉴权,任何人都能调它

为什么不能只用健康检查grpc.health.v1 刻意免鉴权(编排系统的探针通常拿不到 密钥),所以在它眼里"那台机器没起来"和"起来了但我的令牌不对"长得一模一样——而这两件 事的排查方向完全相反。

所以探两层:健康检查回答"连得上吗",0.4 新增的 Ping RPC(走鉴权、不做任何业务 动作)回答"令牌对吗"。第二层的返回码就是结论:

返回 结论
OK 鉴权通过(响应里还带对端的 id、版本、是否启用鉴权、是否开着转发、在途数)
UNAUTHENTICATED 令牌不匹配
UNIMPLEMENTED 鉴权是通的,只是对端还是 0.3(那个版本没有 Ping)

最后一种必须单独说,否则滚动升级期间会被误报成鉴权失败。

对端还会自报 auth_required——探测成功本身分不清"我的令牌对"和"它根本不验"。

如果对方自报的 id 和你列表里写的对不上,也会提示:转发的路由与链路记录里的 "谁执行的"都以列表里那个为准,对不上两边的记录就拼不到一起。

3. 「试一试」可以点名目标节点

试一试页多一个"目标节点"下拉,选中后跳过负载均衡 强制打到那一台——验证新加的机器配对没有,不用再反复点、靠轮询碰运气命中。

怎么确认转发真的生效了

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 n1

Web 管理端的请求流页也能实时看到分发情况,每条记录都带执行节点。

想确定性地验证某一台,用试一试的"目标节点"下拉点名, 比连点几次靠轮询命中可靠得多。

Clone this wiki locally