Skip to content

Cluster

HadesTop edited this page Aug 14, 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" 却没配任何节点会直接报错,不会静默退回单机——静默退回会让你以为 集群生效了,实际所有流量都打在一台上,也没有故障转移。

怎么确认转发真的生效了

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