headscale 压力测试 总结摘要
用法速记:
解压后 cp .env.example .env
,填好 HEADSCALE_URL
与 TS_AUTHKEY
bash scripts/00_check_env.sh
bash scripts/01_bootstrap_up.sh
(默认 200 节点)
bash scripts/02_list_nodes.sh
→ 生成 nodes.tsv
终端 A:bash scripts/03_churn.sh
(每 30s 随机 up/down/restart)
终端 B:watch -n 20 'bash scripts/04_traffic_round.sh'
持续抽样流量
结束:bash scripts/99_cleanup.sh
200个客户端同时连接
headscale服务器资源消耗
tailscale客户端服务器资源消耗-200个客户端
一、推荐机器规格(200 设备规模)
将被测 headscale 与**发压端(loadgen)**分离,更易定位瓶颈。若你只买一台也能做,但推荐两台。
A. 发压端(运行 200 个 Tailscale 客户端容器)
vCPU:16 核
内存:32 GB
磁盘:100 GB NVMe (Docker 层与容器日志)
网卡:≥ 1 Gbps (最好 5–10 Gbps)
OS:Ubuntu 22.04 LTS / Debian 12
内核参数(建议):
fs.file-max=200000
net.core.rmem_max=16777216
net.core.wmem_max=16777216
net.ipv4.ip_local_port_range=10000 65000
Docker 守护进程日志轮转(避免日志撑满磁盘):
log-driver: "local"
, log-opts: {"max-size":"10m","max-file":"3"}
B. 被测 headscale(控制面)
vCPU:4 核
内存:8 GB
磁盘:100 GB
网卡:≥ 1 Gbps
数据库:SQLite(WAL 打开)或 PostgreSQL(随你已有环境)
反向代理:如必须放在反代后,请放宽超时/保持连接,保证长轮询不被切。
C.(可选)自建 DERP 中继
vCPU:2–4 核
内存:2–4 GB
网卡:≥ 1 Gbps
开放 TCP(derp) 与 UDP 3478(STUN)
二、目录结构(建议) 你可以在任意目录新建如下文件(均在本文给出内容):
1 2 3 4 5 6 7 8 9 10 11 headscale-loadtest/ ├─ docker-compose.yml ├─ .env.example ├─ scripts/ │ ├─ 00_check_env.sh │ ├─ 01_bootstrap_up.sh │ ├─ 02_list_nodes.sh │ ├─ 03_churn.sh │ ├─ 04_traffic_round.sh │ ├─ 99_cleanup.sh └─ README.md (可把本文另存)
三、使用说明
准备 :在 headscale 上生成一个 pre-auth key (预授权密钥)。
编辑 .env
(复制 .env.example
)填入 HEADSCALE_URL
和 TS_AUTHKEY
。
bash scripts/00_check_env.sh
检查环境(tun、docker 等)。
bash scripts/01_bootstrap_up.sh
一键起 200 个 tailscale 客户端容器。
bash scripts/02_list_nodes.sh
导出容器名 ↔ Tailscale IP 列表。
抖动 :另开终端运行 bash scripts/03_churn.sh
(每 30s 随机 up/down/restart 一批)。
流量 :再开终端循环跑 bash scripts/04_traffic_round.sh
(随机挑对做 TCP/UDP 10s 压力)。
结束后 bash scripts/99_cleanup.sh
清理容器与临时 sidecar。
四、docker-compose 与 .env
说明:我们用 docker compose up --scale tsnode=200 -d
来“一行扩容 200 台设备”。hostname 不必手配 ,容器会使用唯一的容器 ID 作为主机名,注册到 headscale 时也会保持唯一性。 iperf3 不安装进 tailscale 容器里,而是用短生命的 sidecar 容器 通过 --network=container:<tsnode>
共享网络命名空间来发流/收流,避免镜像扩展与 apt 依赖。
docker-compose.yml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 services: tsnode: image: tailscale/tailscale:stable restart: unless-stopped cap_add: - NET_ADMIN devices: - /dev/net/tun:/dev/net/tun environment: - TS_AUTHKEY=${TS_AUTHKEY} - TS_EXTRA_ARGS=--login-server=${HEADSCALE_URL} --accept-dns=false sysctls: net.ipv6.conf.all.disable_ipv6: "0"
.env.example(复制为 .env 并修改)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 HEADSCALE_URL=https://headscale.example.com TS_AUTHKEY=tskey-auth-xxxxxxxxxxxxxxxxxxxxxxxxxxxx REPLICAS=200 CHURN_INTERVAL=30 CHURN_BATCH_PERCENT=10 TRAFFIC_PAIR_COUNT=30 TRAFFIC_TCP_DURATION=10 TRAFFIC_TCP_PARALLEL=4 TRAFFIC_UDP_DURATION=10 TRAFFIC_UDP_BW=50M
五、脚本集合(保存到 scripts/
)
注意 :脚本使用 docker compose
(v2),以及 networkstatic/iperf3
作为临时 sidecar 镜像。首次需要联网 docker pull
,之后就可离线复用。
1) 00_check_env.sh 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 #!/usr/bin/env bash set -euo pipefailif [[ -f .env ]]; then source .env ; else echo ".env 未找到,请复制 .env.example" ; exit 1; fi command -v docker >/dev/null || { echo "docker 未安装" ; exit 1; }docker info >/dev/null || { echo "docker daemon 不可用" ; exit 1; } if ! docker compose version >/dev/null 2>&1; then echo "docker compose v2 未安装" ; exit 1 fi if [[ ! -e /dev/net/tun ]]; then echo "/dev/net/tun 不存在,尝试加载内核模块..." if command -v modprobe >/dev/null; then sudo modprobe tun || true fi fi [[ -e /dev/net/tun ]] || { echo "仍无 /dev/net/tun,请在宿主机启用 TUN 模块" ; exit 1; } echo "预拉取镜像(可选)..." docker pull tailscale/tailscale:stable docker pull networkstatic/iperf3:latest echo "检查通过。"
2) 01_bootstrap_up.sh 1 2 3 4 5 6 7 8 9 10 11 12 13 14 #!/usr/bin/env bash set -euo pipefailsource .env echo "以 $REPLICAS 个副本拉起 tsnode..." docker compose up -d --scale tsnode="${REPLICAS} " echo "等待节点启动并完成登录(首次 30~60s)..." sleep 45echo "当前副本数量:" docker compose ps tsnode echo "可运行 scripts/02_list_nodes.sh 导出节点 IP。"
3) 02_list_nodes.sh 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #!/usr/bin/env bash set -euo pipefailOUT="nodes.tsv" : > "$OUT " mapfile -t CTS < <(docker compose ps --format '{{.Name}}' tsnode)echo -e "container\tts_ip4" >> "$OUT " for c in "${CTS[@]} " ; do ip=$(docker exec "$c " tailscale ip -4 2>/dev/null | head -n1 || true ) if [[ -n "$ip " ]]; then echo -e "${c} \t${ip} " >> "$OUT " fi done echo "已导出 $OUT (容器名 ↔ Tailscale IPv4)。"
4) 03_churn.sh(每 30s 随机 up/down/restart 一批) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 #!/usr/bin/env bash set -euo pipefailsource .env SERVICE="tsnode" INTERVAL="${CHURN_INTERVAL:-30} " PCT="${CHURN_BATCH_PERCENT:-10} " echo "每 ${INTERVAL} s 随机对 ${PCT} % 容器执行 up/down/restart 抖动。按 Ctrl-C 结束。" while true ; do mapfile -t CTS < <(docker compose ps --format '{{.Name}}' "$SERVICE " ) TOT=${#CTS[@]} ((TOT>0)) || { echo "未发现容器" ; exit 1; } BATCH=$(( (TOT*PCT + 99 )/100 )) mapfile -t PICKED < <(printf "%s\n" "${CTS[@]} " | shuf -n "$BATCH " ) echo "=== Churn round: $(date '+%F %T') 目标 $BATCH /$TOT ===" for c in "${PICKED[@]} " ; do op=$((RANDOM%3 )) case "$op " in 0) echo " - $c : tailscale down" docker exec "$c " tailscale down || true ;; 1) echo " - $c : tailscale up" docker exec "$c " tailscale up --login-server="${HEADSCALE_URL} " --authkey="${TS_AUTHKEY} " --accept-dns=false || true ;; 2) echo " - $c : docker restart" docker restart "$c " >/dev/null || true ;; esac done sleep "$INTERVAL " done
5) 04_traffic_round.sh(随机起对做 TCP/UDP 流量)
设计为一轮 10s 的抽样压测;你可以循环调用或配合 watch -n 20
调度。 服务器与客户端都用 临时 sidecar 容器 ,--network=container:<tsnode>
共享网络命名空间,无需在 tsnode 内安装 iperf3。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 #!/usr/bin/env bash set -euo pipefailsource .env PAIRS="${TRAFFIC_PAIR_COUNT:-30} " [[ -f nodes.tsv ]] || bash scripts/02_list_nodes.sh mapfile -t LINES < <(tail -n +2 nodes.tsv)CNT=${#LINES[@]} ((CNT>=2)) || { echo "可用节点不足 2" ; exit 1; } pick_node () { local line="${LINES[$((RANDOM % CNT))]} " local name ip name=$(awk '{print $1}' <<<"$line " ) ip=$(awk '{print $2}' <<<"$line " ) echo "$name $ip " } run_pair_tcp () { local cA ipA cB ipB read -r cA ipA <<<"$(pick_node) " read -r cB ipB <<<"$(pick_node) " [[ "$cA " != "$cB " ]] || return 0 docker run --rm -d --name "iperf-srv-${cB} " --network="container:${cB} " networkstatic/iperf3 -s -1 >/dev/null echo "[TCP] $cA -> $cB ($ipB )" docker run --rm --network="container:${cA} " networkstatic/iperf3 \ -c "$ipB " -t "${TRAFFIC_TCP_DURATION:-10} " -P "${TRAFFIC_TCP_PARALLEL:-4} " >/dev/null 2>&1 || true } run_pair_udp () { local cA ipA cB ipB read -r cA ipA <<<"$(pick_node) " read -r cB ipB <<<"$(pick_node) " [[ "$cA " != "$cB " ]] || return 0 docker run --rm -d --name "iperf-srv-${cB} " --network="container:${cB} " networkstatic/iperf3 -s -1 >/dev/null echo "[UDP] $cA -> $cB ($ipB )" docker run --rm --network="container:${cA} " networkstatic/iperf3 \ -c "$ipB " -u -b "${TRAFFIC_UDP_BW:-50M} " -t "${TRAFFIC_UDP_DURATION:-10} " >/dev/null 2>&1 || true } echo "=== Traffic round @ $(date '+%F %T') pairs=${PAIRS} ===" for ((i=0; i<PAIRS; i++)); do if (( RANDOM % 2 )); then run_pair_tcp & else run_pair_udp & fi done wait echo "=== Round done ==="
6) 99_cleanup.sh 1 2 3 4 5 6 7 8 9 #!/usr/bin/env bash set -euo pipefailecho "停止并清理 tsnode 副本与残留 iperf sidecar..." docker ps --format '{{.ID}} {{.Names}}' | awk '/iperf-srv-/{print $1}' | xargs -r docker rm -f docker compose down -t 5
六、测试步骤建议
单节点验证
修改 .env
为 REPLICAS=1
,bash scripts/01_bootstrap_up.sh
。
docker compose exec tsnode-1 tailscale status
看是否已连上你的 headscale(可用 tailscale netcheck
验证直连/DERP)。
bash scripts/02_list_nodes.sh
确认导出了 IP。
扩到 200 节点
.env
改回 REPLICAS=200
,再次 01_bootstrap_up.sh
。
02_list_nodes.sh
导出 nodes.tsv
。
控制面抖动
终端 A:bash scripts/03_churn.sh
(默认每 30s 对 10% 做 up/down/restart)
观察 headscale /metrics
、CPU、内存、日志中 PollNetMap
等请求量与耗时。
数据面吞吐
终端 B:watch -n 20 'bash scripts/04_traffic_round.sh'
连续抽样回合。
对比直连与 DERP(可在安全组/防火墙层面封掉 UDP,观察 DERP 回退下的吞吐变化)。
扩大/缩小与对比
把 REPLICAS
改为 300/500 试试 headscale 的上限趋势(若机器足够)。
分别在无抖动 与强抖动 两种状态下记录 headscale 的指标差异。
七、常见问题(FAQ)
为什么用 sidecar 跑 iperf3? 这样无需在 tailscale 镜像里安装任何包;--network=container:<tsnode>
会共享该 tsnode 的网络命名空间,等价于“在该设备上跑 iperf3”。
Compose 扩容 200 个需要唯一主机名吗? 不需要手工设置。默认容器主机名是容器 ID,Tailscale 设备名会唯一。也可自行加 --hostname
脚本模式(不建议和 --scale
混用)。
抖动脚本中的 tailscale down/up
与 docker restart
区别? down/up
更接近“客户端主动注销/重登录”;restart
模拟“设备重启或网络闪断”,三者混合能覆盖更多状态机分支。