mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 10:10:43 +00:00
清理误提交的测试数据目录 + 扩展 .gitignore
This commit is contained in:
@@ -64,3 +64,11 @@ desktop.ini
|
||||
/_build/.gitee_token
|
||||
*.token
|
||||
.env
|
||||
# ---- 测试用的临时数据目录(会被测试脚本移来移去)----
|
||||
/windows/data/
|
||||
/windows/data_bk_*/
|
||||
/_build/*_data/
|
||||
/_build/_pairtest/
|
||||
/_build/_whitetest/
|
||||
/_tests/_stubdata/
|
||||
/_tests/_*_data/
|
||||
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
r'''分析 2 小时长测数据(列格式:时间戳 \t 状态码 \t 毫秒)。
|
||||
|
||||
上一版解析写错了 —— 它把任何 3 位数字都当状态码,
|
||||
于是毫秒(921、968…)也被算成状态码,可用率算出来只有 84.8%,
|
||||
是**假数字**。这里改成按列位置取。
|
||||
'''
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
|
||||
path = Path(sys.argv[1] if len(sys.argv) > 1 else "/root/long2h_health.tsv")
|
||||
if not path.is_file():
|
||||
print(" 文件不存在: %s" % path)
|
||||
raise SystemExit(0)
|
||||
|
||||
rows = []
|
||||
bad = 0
|
||||
for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
parts = line.split("\t")
|
||||
if len(parts) < 3:
|
||||
bad += 1
|
||||
continue
|
||||
stamp, code, ms = parts[0], parts[1].strip(), parts[2].strip()
|
||||
try:
|
||||
rows.append((stamp, code, float(ms)))
|
||||
except ValueError:
|
||||
bad += 1
|
||||
|
||||
print(" 样本 %d 条(解析不了 %d 条)" % (len(rows), bad))
|
||||
if not rows:
|
||||
raise SystemExit(0)
|
||||
|
||||
print(" 时间范围: %s -> %s" % (rows[0][0], rows[-1][0]))
|
||||
|
||||
codes = Counter(c for _s, c, _m in rows)
|
||||
total = len(rows)
|
||||
ok = codes.get("200", 0)
|
||||
|
||||
print()
|
||||
print(" ── HTTP 状态分布 ──")
|
||||
for c, n in codes.most_common():
|
||||
print(" %-6s %4d 次 %5.1f%%" % (c, n, n * 100.0 / total))
|
||||
print(" " + "-" * 30)
|
||||
print(" 可用率: %d/%d = %.2f%%" % (ok, total, ok * 100.0 / total))
|
||||
|
||||
# 只看成功的耗时(失败的那些耗时没有意义)
|
||||
good = sorted(m for _s, c, m in rows if c == "200")
|
||||
if good:
|
||||
def p(q):
|
||||
return good[min(int(len(good) * q), len(good) - 1)]
|
||||
print()
|
||||
print(" ── 成功请求耗时(毫秒)──")
|
||||
print(" 最小 %.0f 中位 %.0f P90 %.0f P99 %.0f 最大 %.0f"
|
||||
% (good[0], good[len(good) // 2], p(0.9), p(0.99), good[-1]))
|
||||
|
||||
# 如果还有第 4 列及以后(连接数之类),也统计一下
|
||||
extra_cols = set()
|
||||
for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
|
||||
p = line.strip().split("\t")
|
||||
if len(p) > 3:
|
||||
extra_cols.add(len(p))
|
||||
if extra_cols:
|
||||
print()
|
||||
print(" (文件里还有更多列,最多 %d 列)" % max(extra_cols))
|
||||
|
||||
fails = [(s, c, m) for s, c, m in rows if c != "200"]
|
||||
print()
|
||||
if fails:
|
||||
print(" ── 失败样本(共 %d 条)──" % len(fails))
|
||||
for s, c, m in fails[:15]:
|
||||
print(" %s HTTP %-5s %.0fms" % (s, c, m))
|
||||
if len(fails) > 15:
|
||||
print(" … 还有 %d 条" % (len(fails) - 15))
|
||||
# 失败时间分布(按 10 分钟分桶)
|
||||
print()
|
||||
print(" ── 失败时刻分布(按小时)──")
|
||||
hours = Counter(s.split(" ")[1].split(":")[0] if " " in s else s[:2]
|
||||
for s, _c, _m in fails)
|
||||
for h in sorted(hours):
|
||||
print(" %s 点 %d 次" % (h, hours[h]))
|
||||
else:
|
||||
print(" ✓ 全程没有非 200 —— 2 小时 100% 可用")
|
||||
@@ -0,0 +1,98 @@
|
||||
#!/bin/bash
|
||||
# 分析 relay-stability 的 2 小时健康长测数据(只读)
|
||||
set -u
|
||||
|
||||
F=/root/long2h_health.tsv
|
||||
[ -f "$F" ] || { echo " 数据文件不存在: $F"; exit 0; }
|
||||
|
||||
echo " 文件: $F 行数: $(wc -l < "$F")"
|
||||
echo " 前 3 行(看格式):"
|
||||
head -3 "$F" | sed 's/^/ /'
|
||||
echo
|
||||
|
||||
python3 - "$F" <<'PYEOF'
|
||||
import sys
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
|
||||
path = Path(sys.argv[1])
|
||||
lines = [l.strip() for l in path.read_text(encoding="utf-8",
|
||||
errors="replace").splitlines()
|
||||
if l.strip()]
|
||||
if not lines:
|
||||
print(" 没有数据")
|
||||
raise SystemExit(0)
|
||||
|
||||
# 猜列:先看表头,没有表头就按 tab 切
|
||||
head = lines[0].lower()
|
||||
has_header = any(k in head for k in ("time", "code", "ms", "http"))
|
||||
rows = lines[1:] if has_header else lines
|
||||
if has_header:
|
||||
print(" 表头: %s" % lines[0])
|
||||
print(" 样本数: %d" % len(rows))
|
||||
|
||||
codes = Counter()
|
||||
times = []
|
||||
conns = []
|
||||
fails = []
|
||||
for i, line in enumerate(rows, 1):
|
||||
parts = line.split("\t")
|
||||
code = None
|
||||
ms = None
|
||||
for p in parts:
|
||||
p = p.strip()
|
||||
if p.isdigit() and len(p) == 3:
|
||||
code = p
|
||||
if p.replace(".", "").isdigit() and "." in p:
|
||||
try:
|
||||
v = float(p)
|
||||
if ms is None and v < 300000:
|
||||
ms = v
|
||||
except ValueError:
|
||||
pass
|
||||
if code:
|
||||
codes[code] += 1
|
||||
if code != "200":
|
||||
fails.append((i, line[:100]))
|
||||
if ms:
|
||||
times.append(ms)
|
||||
# 连接数:找形如 quic=N h2=M 或者单独的数字
|
||||
for p in parts:
|
||||
if "quic=" in p:
|
||||
conns.append(p.strip())
|
||||
|
||||
total = sum(codes.values())
|
||||
ok = codes.get("200", 0)
|
||||
print()
|
||||
print(" ── HTTP 结果分布 ──")
|
||||
for c, n in codes.most_common():
|
||||
pct = n * 100.0 / total if total else 0
|
||||
print(" %-6s %4d 次 %.1f%%" % (c, n, pct))
|
||||
print(" --------")
|
||||
print(" 可用率: %d/%d = %.1f%%" % (ok, total, ok * 100.0 / total if total else 0))
|
||||
|
||||
if times:
|
||||
s = sorted(times)
|
||||
def pct(q):
|
||||
return s[min(int(len(s) * q), len(s) - 1)]
|
||||
print()
|
||||
print(" ── 耗时分布(毫秒)──")
|
||||
print(" 最小 %.0f 中位 %.0f P90 %.0f P99 %.0f 最大 %.0f"
|
||||
% (s[0], s[len(s)//2], pct(0.9), pct(0.99), s[-1]))
|
||||
|
||||
if conns:
|
||||
c = Counter(conns)
|
||||
print()
|
||||
print(" ── 连接数分布(前 8)──")
|
||||
for k, n in c.most_common(8):
|
||||
print(" %-28s %d 次" % (k, n))
|
||||
|
||||
if fails:
|
||||
print()
|
||||
print(" ── 失败样本(最多 12 条)──")
|
||||
for i, line in fails[:12]:
|
||||
print(" 第 %d 行: %s" % (i, line))
|
||||
else:
|
||||
print()
|
||||
print(" ✓ 全程没有非 200")
|
||||
PYEOF
|
||||
@@ -0,0 +1,35 @@
|
||||
#!/bin/bash
|
||||
# 看设备上隧道现在的配比 + 连接数(判断 relay-stability 有没有在跑对照)
|
||||
set -u
|
||||
|
||||
echo " ── 两个隧道单元的配置 ──"
|
||||
for s in lanremote-tunnel lanremote-tunnel-h2; do
|
||||
if [ -f "/etc/systemd/system/$s.service" ]; then
|
||||
cfg=$(grep -oE -- '--protocol [a-z0-9]+|--ha-connections [0-9]+|--metrics [0-9.:]+' \
|
||||
"/etc/systemd/system/$s.service" | tr '\n' ' ')
|
||||
printf " %-24s %s (%s)\n" "$s" "$cfg" "$(systemctl is-active $s 2>/dev/null)"
|
||||
else
|
||||
printf " %-24s (不存在)\n" "$s"
|
||||
fi
|
||||
done
|
||||
|
||||
echo
|
||||
echo " ── 实时连接数 ──"
|
||||
for p in 20241 20242; do
|
||||
n=$(curl -s --max-time 4 "http://127.0.0.1:$p/metrics" 2>/dev/null \
|
||||
| grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}')
|
||||
printf " 端口 %s -> %s 条\n" "$p" "${n:-读不到}"
|
||||
done
|
||||
|
||||
echo
|
||||
echo " ── 公网自检 ──"
|
||||
curl -s --max-time 20 -o /dev/null -w ' HTTP %{http_code} %{time_total}s\n' \
|
||||
-H 'User-Agent: LanRemote/1.8.2' https://relay.xxmxxs.top/v1/health
|
||||
|
||||
echo
|
||||
echo " ── relay-stability 的测量进程 ──"
|
||||
pgrep -af 'rp_|longstab|bench' 2>/dev/null | head -5 | sed 's/^/ /' || echo " (没有在跑)"
|
||||
|
||||
echo
|
||||
echo " ── 它最近写的文件 ──"
|
||||
ls -la --time-style=+%H:%M:%S /root/rp_* /root/*.tsv 2>/dev/null | tail -8 | sed 's/^/ /' || true
|
||||
@@ -0,0 +1,148 @@
|
||||
#!/bin/bash
|
||||
# 长窗口配比对照用的测量脚本(支持 A/B 两组,各跑 >=40 分钟)
|
||||
#
|
||||
# 与 rp_long2h.sh 的区别:
|
||||
# * 时长可指定(分钟)
|
||||
# * **额外采样内存和 CPU**(Lead 问 h2 提到 4 条对 460MB 设备有无影响)
|
||||
# * 输出文件名带标签,方便 A/B 分开比对
|
||||
#
|
||||
# 用法: bash rp_ab_long.sh <标签> <分钟数>
|
||||
set -u
|
||||
|
||||
LABEL=${1:-A}
|
||||
MINS=${2:-45}
|
||||
BASE=https://relay.xxmxxs.top
|
||||
UA='LanRemote/1.8.2'
|
||||
DUR=$((MINS * 60))
|
||||
HEALTH_EVERY=15
|
||||
CONN_EVERY=60
|
||||
RES_EVERY=60
|
||||
|
||||
H=/root/ab_${LABEL}_health.tsv
|
||||
C=/root/ab_${LABEL}_conns.tsv
|
||||
R=/root/ab_${LABEL}_res.tsv
|
||||
S=/root/ab_${LABEL}_summary.txt
|
||||
|
||||
rm -f "$H" "$C" "$R" "$S"
|
||||
|
||||
conns_of() {
|
||||
curl -s --max-time 4 "http://127.0.0.1:$1/metrics" 2>/dev/null \
|
||||
| grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}'
|
||||
}
|
||||
|
||||
M0=$(wc -l < /root/lanremote-relay/tunnel.log 2>/dev/null || echo 0)
|
||||
H0=$(wc -l < /root/lanremote-relay/tunnel-h2.log 2>/dev/null || echo 0)
|
||||
|
||||
T0=$(date +%s)
|
||||
NEXT_CONN=$T0
|
||||
NEXT_RES=$T0
|
||||
|
||||
echo " 开始 $LABEL: $(date '+%H:%M:%S') 持续 ${MINS} 分钟"
|
||||
|
||||
while true; do
|
||||
NOW=$(date +%s)
|
||||
[ $((NOW - T0)) -ge "$DUR" ] && break
|
||||
|
||||
out=$(curl -s -o /dev/null -w '%{http_code} %{time_total}' --max-time 25 \
|
||||
-H "User-Agent: $UA" "$BASE/v1/health")
|
||||
code=$(echo "$out" | awk '{print $1}')
|
||||
ms=$(echo "$out" | awk '{printf "%d", $2*1000}')
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') $code $ms" >> "$H"
|
||||
|
||||
if [ "$NOW" -ge "$NEXT_CONN" ]; then
|
||||
a=$(conns_of 20241); b=$(conns_of 20242)
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') ${a:-X} ${b:-X}" >> "$C"
|
||||
NEXT_CONN=$((NOW + CONN_EVERY))
|
||||
fi
|
||||
|
||||
if [ "$NOW" -ge "$NEXT_RES" ]; then
|
||||
# 两个 cloudflared 各占多少常驻内存 + CPU
|
||||
qm=0; hm=0; qc=0; hc=0
|
||||
for p in $(pgrep cloudflared); do
|
||||
proto=$(tr '\0' ' ' < /proc/$p/cmdline 2>/dev/null | grep -oE '\-\-protocol [a-z0-9]+' | awk '{print $2}')
|
||||
rss=$(awk '/VmRSS/{print $2}' /proc/$p/status 2>/dev/null)
|
||||
cpu=$(ps -o %cpu= -p $p 2>/dev/null | tr -d ' ')
|
||||
if [ "$proto" = "quic" ]; then qm=${rss:-0}; qc=${cpu:-0};
|
||||
elif [ "$proto" = "http2" ]; then hm=${rss:-0}; hc=${cpu:-0}; fi
|
||||
done
|
||||
total=$(free -m | awk '/Mem:/{print $3}')
|
||||
avail=$(free -m | awk '/Mem:/{print $7}')
|
||||
load=$(awk '{print $1}' /proc/loadavg)
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') ${qm} ${hm} ${qc} ${hc} ${total} ${avail} ${load}" >> "$R"
|
||||
NEXT_RES=$((NOW + RES_EVERY))
|
||||
fi
|
||||
|
||||
sleep "$HEALTH_EVERY"
|
||||
done
|
||||
|
||||
M1=$(wc -l < /root/lanremote-relay/tunnel.log 2>/dev/null || echo 0)
|
||||
H1=$(wc -l < /root/lanremote-relay/tunnel-h2.log 2>/dev/null || echo 0)
|
||||
|
||||
{
|
||||
echo "════════════════════════════════════════════════════════"
|
||||
echo " $LABEL 组 · ${MINS} 分钟 · $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
echo "════════════════════════════════════════════════════════"
|
||||
echo
|
||||
echo "── 配置 ──"
|
||||
for svc in lanremote-tunnel lanremote-tunnel-h2; do
|
||||
echo " $svc: $(systemctl cat $svc 2>/dev/null | grep -oE '\-\-protocol [a-z0-9]+|\-\-ha-connections [0-9]+' | tr '\n' ' ')"
|
||||
done
|
||||
echo
|
||||
echo "── 公网可用率 ──"
|
||||
n=$(grep -c . "$H" 2>/dev/null || echo 0)
|
||||
ok=$(awk -F'\t' '$2=="200"' "$H" 2>/dev/null | wc -l)
|
||||
echo " $ok/$n = $(awk -v a="$ok" -v b="$n" 'BEGIN{if(b>0)printf "%.2f", a*100/b; else print 0}')%"
|
||||
echo " 状态码: $(awk -F'\t' '{print $2}' "$H" 2>/dev/null | sort | uniq -c | tr '\n' ' ')"
|
||||
awk -F'\t' '$2=="200"{print $3}' "$H" 2>/dev/null | sort -n | awk '
|
||||
{a[NR]=$1}
|
||||
END{if(NR==0){print " 无数据"; exit}
|
||||
printf " 耗时: 最小 %d 中位 %d P90 %d P99 %d 最大 %d ms\n",
|
||||
a[1], a[int(NR/2)+1], a[int(NR*0.9)+1], a[int(NR*0.99)+1], a[NR]}'
|
||||
echo " 失败时刻:"
|
||||
awk -F'\t' '$2!="200"{print " " $1 " 码=" $2 " " $3 "ms"}' "$H" 2>/dev/null
|
||||
echo
|
||||
echo "── 连接数 ──"
|
||||
awk -F'\t' 'NR>1 && $2 ~ /^[0-9]+$/ && $3 ~ /^[0-9]+$/ {
|
||||
t=$2+$3; cnt[t]++
|
||||
if (t<=3) low++
|
||||
if (t<min || min=="") min=t
|
||||
if (t>max) max=t
|
||||
} END {
|
||||
printf " 最小 %d 最大 %d 平均 %.2f\n", min, max, 0
|
||||
printf " 总连接数<=3 的采样: %d 次(占 %.1f%%)\n", low+0, (low+0)*100/NR
|
||||
}' "$C" 2>/dev/null
|
||||
echo " 分布: $(awk -F'\t' 'NR>1 && $2 ~ /^[0-9]+$/ && $3 ~ /^[0-9]+$/ {print $2+$3}' "$C" | sort -n | uniq -c | tr '\n' ' ')"
|
||||
echo
|
||||
echo "── 内存 / CPU ──"
|
||||
awk -F'\t' 'NR>1 {
|
||||
if ($2+0>qm) qm=$2
|
||||
if ($3+0>hm) hm=$3
|
||||
if ($4+0>qc) qc=$4
|
||||
if ($5+0>hc) hc=$5
|
||||
if ($6+0>tm) tm=$6
|
||||
if (minav=="" || $7+0<minav) minav=$7
|
||||
if ($8+0>ld) ld=$8
|
||||
} END {
|
||||
printf " QUIC 实例常驻内存峰值: %.1f MB\n", qm/1024
|
||||
printf " HTTP/2 实例常驻内存峰值: %.1f MB\n", hm/1024
|
||||
printf " 两者合计: %.1f MB\n", (qm+hm)/1024
|
||||
printf " CPU 峰值: quic %.1f%% http2 %.1f%%\n", qc, hc
|
||||
printf " 设备已用内存峰值: %d MB 最少可用: %d MB\n", tm, minav
|
||||
printf " 负载峰值: %.2f\n", ld
|
||||
}' "$R" 2>/dev/null
|
||||
echo
|
||||
echo "── 隧道事件(本轮增量)──"
|
||||
printf " QUIC 注册 %s 失败 %s 掉线 %s\n" \
|
||||
"$(tail -n +$((M0+1)) /root/lanremote-relay/tunnel.log 2>/dev/null | grep -c 'Registered tunnel connection')" \
|
||||
"$(tail -n +$((M0+1)) /root/lanremote-relay/tunnel.log 2>/dev/null | grep -c 'Failed to dial')" \
|
||||
"$(tail -n +$((M0+1)) /root/lanremote-relay/tunnel.log 2>/dev/null | grep -c 'Lost connection')"
|
||||
printf " HTTP/2 注册 %s 失败 %s 掉线 %s\n" \
|
||||
"$(tail -n +$((H0+1)) /root/lanremote-relay/tunnel-h2.log 2>/dev/null | grep -c 'Registered tunnel connection')" \
|
||||
"$(tail -n +$((H0+1)) /root/lanremote-relay/tunnel-h2.log 2>/dev/null | grep -c 'Failed to dial')" \
|
||||
"$(tail -n +$((H0+1)) /root/lanremote-relay/tunnel-h2.log 2>/dev/null | grep -c 'Lost connection')"
|
||||
echo
|
||||
echo "── watchdog 处置(本轮)──"
|
||||
tail -12 /root/lanremote-relay/watchdog.log 2>/dev/null | sed 's/^/ /'
|
||||
} > "$S" 2>&1
|
||||
|
||||
cat "$S"
|
||||
@@ -0,0 +1,147 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
r'''塌陷窗口分析器:专门看"连接总数 <= 3"的那些窗口里发生了什么。
|
||||
|
||||
== 为什么需要专门看窗口 ==
|
||||
|
||||
总体中位数两次都会被稳态主导(都是 61 秒 / 1.2 秒),区分不出 A/B 好坏。
|
||||
真正的差别只在 QUIC 塌陷的那几分钟里体现:
|
||||
A 组 quic8+h2 2:塌陷时只剩 2 条在扛
|
||||
B 组 quic6+h2 4:塌陷时还有 4 条
|
||||
|
||||
所以核心指标是:
|
||||
* 「连接总数 <= 3」的采样占比(= 风险暴露时间)
|
||||
* 这些窗口里公网可用率
|
||||
|
||||
用法:
|
||||
python _build/rp_collapse.py <标签> <conns文件> <health文件>
|
||||
'''
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
# 塌陷判定:总连接数 <= 这个值就算"容量很薄"
|
||||
COLLAPSE_TOTAL = 3
|
||||
|
||||
|
||||
def parse_ts(path: str, cols: int):
|
||||
rows = []
|
||||
for line in Path(path).read_text(encoding="utf-8", errors="replace").splitlines():
|
||||
parts = line.split("\t")
|
||||
if len(parts) < cols:
|
||||
continue
|
||||
try:
|
||||
t = datetime.strptime(parts[0].strip(), "%Y-%m-%d %H:%M:%S")
|
||||
except ValueError:
|
||||
continue
|
||||
rows.append((t, parts))
|
||||
return rows
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) < 4:
|
||||
print("用法: rp_collapse.py <标签> <conns> <health>")
|
||||
return 2
|
||||
label, conns_path, health_path = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||
|
||||
conns = parse_ts(conns_path, 3)
|
||||
health = parse_ts(health_path, 3)
|
||||
if not conns:
|
||||
print(" %s: 连接采样为空" % label)
|
||||
return 1
|
||||
|
||||
# ---- 连接数统计 ----
|
||||
samples = []
|
||||
for t, p in conns:
|
||||
try:
|
||||
q = int(p[1])
|
||||
except ValueError:
|
||||
q = None
|
||||
try:
|
||||
h = int(p[2])
|
||||
except ValueError:
|
||||
h = None
|
||||
samples.append((t, q, h))
|
||||
|
||||
valid = [(t, q, h) for t, q, h in samples if q is not None and h is not None]
|
||||
if not valid:
|
||||
print(" %s: 没有有效的连接采样" % label)
|
||||
return 1
|
||||
|
||||
span = (valid[-1][0] - valid[0][0]).total_seconds()
|
||||
total_list = [q + h for _t, q, h in valid]
|
||||
|
||||
collapse = [(t, q, h) for t, q, h in valid if q + h <= COLLAPSE_TOTAL]
|
||||
collapse_secs = len(collapse) * 60.0 # 每 60 秒一个采样
|
||||
collapse_pct = len(collapse) * 100.0 / len(valid)
|
||||
|
||||
print("=" * 62)
|
||||
print(" %s" % label)
|
||||
print(" 采样窗口 %.0f 分钟(%d 个连接采样)" % (span / 60, len(valid)))
|
||||
print("=" * 62)
|
||||
print()
|
||||
print(" 连接数分布:")
|
||||
from collections import Counter
|
||||
cnt = Counter(total_list)
|
||||
for k in sorted(cnt, reverse=True):
|
||||
bar = "#" * min(cnt[k], 50)
|
||||
print(" 总数 %2d : %3d 次 %s" % (k, cnt[k], bar))
|
||||
print()
|
||||
print(" 平均连接数: %.2f" % (sum(total_list) / len(total_list)))
|
||||
print()
|
||||
|
||||
# ---- 塌陷窗口 ----
|
||||
print(" ── 塌陷窗口(连接总数 <= %d)──" % COLLAPSE_TOTAL)
|
||||
print(" 出现 %d 次采样 = 约 %.0f 秒 = 占窗口的 %.1f%%"
|
||||
% (len(collapse), collapse_secs, collapse_pct))
|
||||
if collapse:
|
||||
print(" 明细:")
|
||||
for t, q, h in collapse:
|
||||
print(" %s quic=%s http2=%s 合计=%d"
|
||||
% (t.strftime("%H:%M:%S"), q, h, q + h))
|
||||
|
||||
# ---- 塌陷窗口内的公网可用率 ----
|
||||
print()
|
||||
print(" ── 塌陷窗口内的公网可用率 ──")
|
||||
if not collapse or not health:
|
||||
print(" (窗口内没有健康样本)")
|
||||
else:
|
||||
# 把塌陷时刻扩展成 ±60 秒的窗口,再取落在里面的健康样本
|
||||
windows = [(t.timestamp() - 60, t.timestamp() + 60) for t, _q, _h in collapse]
|
||||
inside = []
|
||||
for t, p in health:
|
||||
ts = t.timestamp()
|
||||
if any(a <= ts <= b for a, b in windows):
|
||||
inside.append((t, p[1]))
|
||||
if inside:
|
||||
ok = sum(1 for _t, c in inside if c == "200")
|
||||
print(" 窗口覆盖到 %d 个健康样本,成功 %d = %.1f%%"
|
||||
% (len(inside), ok, ok * 100.0 / len(inside)))
|
||||
bad = [(t, c) for t, c in inside if c != "200"]
|
||||
if bad:
|
||||
print(" 失败明细:")
|
||||
for t, c in bad:
|
||||
print(" %s 码=%s" % (t.strftime("%H:%M:%S"), c))
|
||||
else:
|
||||
print(" (窗口内没有健康样本)")
|
||||
|
||||
# ---- 总体可用率 ----
|
||||
if health:
|
||||
ok = sum(1 for _t, p in health if p[1] == "200")
|
||||
print()
|
||||
print(" ── 总体公网可用率(对照)──")
|
||||
print(" %d/%d = %.2f%%" % (ok, len(health), ok * 100.0 / len(health)))
|
||||
|
||||
# ---- 一句话结论 ----
|
||||
print()
|
||||
print(" ── 一句话 ──")
|
||||
print(" 风险暴露(总数<=%d): %.1f%% 窗口内可用率: %s"
|
||||
% (COLLAPSE_TOTAL, collapse_pct,
|
||||
("%.1f%%" % (sum(1 for _t, c in inside if c == "200") * 100.0 / len(inside))
|
||||
if collapse and health and inside else "无数据")))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -240,3 +240,72 @@ time code ms
|
||||
2026-10-07 18:41:34 200 1914
|
||||
2026-10-07 18:41:51 200 1286
|
||||
2026-10-07 18:42:07 200 1029
|
||||
2026-10-07 18:42:23 200 1023
|
||||
2026-10-07 18:42:39 200 1373
|
||||
2026-10-07 18:42:55 200 1320
|
||||
2026-10-07 18:43:12 200 1275
|
||||
2026-10-07 18:43:28 200 1420
|
||||
2026-10-07 18:43:44 200 1468
|
||||
2026-10-07 18:44:01 200 1541
|
||||
2026-10-07 18:44:17 200 1353
|
||||
2026-10-07 18:44:33 200 1142
|
||||
2026-10-07 18:44:50 200 1330
|
||||
2026-10-07 18:45:06 200 1138
|
||||
2026-10-07 18:45:22 200 1141
|
||||
2026-10-07 18:45:38 200 1108
|
||||
2026-10-07 18:45:54 200 1130
|
||||
2026-10-07 18:46:10 200 1114
|
||||
2026-10-07 18:46:27 200 1127
|
||||
2026-10-07 18:46:43 200 1023
|
||||
2026-10-07 18:46:59 200 1316
|
||||
2026-10-07 18:47:15 200 991
|
||||
2026-10-07 18:47:31 200 1013
|
||||
2026-10-07 18:47:48 200 2106
|
||||
2026-10-07 18:48:04 200 1385
|
||||
2026-10-07 18:48:21 200 1168
|
||||
2026-10-07 18:48:37 200 1115
|
||||
2026-10-07 18:48:53 200 1156
|
||||
2026-10-07 18:49:09 200 1121
|
||||
2026-10-07 18:49:25 200 1440
|
||||
2026-10-07 18:49:42 200 1414
|
||||
2026-10-07 18:49:58 200 1015
|
||||
2026-10-07 18:50:14 200 1119
|
||||
2026-10-07 18:50:30 200 1108
|
||||
2026-10-07 18:50:46 200 1093
|
||||
2026-10-07 18:51:02 200 1287
|
||||
2026-10-07 18:51:19 200 1461
|
||||
2026-10-07 18:51:35 200 1104
|
||||
2026-10-07 18:51:51 200 1318
|
||||
2026-10-07 18:52:08 200 1276
|
||||
2026-10-07 18:52:24 200 1408
|
||||
2026-10-07 18:52:41 200 1446
|
||||
2026-10-07 18:52:57 200 1498
|
||||
2026-10-07 18:53:15 200 3390
|
||||
2026-10-07 18:53:33 200 2790
|
||||
2026-10-07 18:53:50 200 1457
|
||||
2026-10-07 18:54:06 200 1416
|
||||
2026-10-07 18:54:22 200 1175
|
||||
2026-10-07 18:54:38 200 1195
|
||||
2026-10-07 18:54:55 200 1429
|
||||
2026-10-07 18:55:11 200 1180
|
||||
2026-10-07 18:55:28 200 1526
|
||||
2026-10-07 18:55:44 200 1253
|
||||
2026-10-07 18:56:00 200 875
|
||||
2026-10-07 18:56:16 200 886
|
||||
2026-10-07 18:56:31 200 871
|
||||
2026-10-07 18:56:47 200 862
|
||||
2026-10-07 18:57:03 200 956
|
||||
2026-10-07 18:57:19 200 863
|
||||
2026-10-07 18:57:35 200 802
|
||||
2026-10-07 18:57:51 200 1144
|
||||
2026-10-07 18:58:07 200 1097
|
||||
2026-10-07 18:58:23 200 853
|
||||
2026-10-07 18:59:01 200 22552
|
||||
2026-10-07 18:59:17 200 1472
|
||||
2026-10-07 18:59:34 200 1476
|
||||
2026-10-07 18:59:50 200 1386
|
||||
2026-10-07 19:00:07 200 1598
|
||||
2026-10-07 19:00:28 200 6359
|
||||
2026-10-07 19:00:44 200 1199
|
||||
2026-10-07 19:01:00 200 1335
|
||||
2026-10-07 19:01:17 200 1437
|
||||
|
||||
|
Binary file not shown.
+42
-14
@@ -71,6 +71,8 @@ THRESHOLD=3
|
||||
# 重启本地隧道解决不了,只会自伤。给足时间让它自己恢复。
|
||||
# 连接真的掉下去了(降级),才用 THRESHOLD=3 快速重启。
|
||||
THRESHOLD_HEALTHY=8
|
||||
# 连接"中等"(4~5 条)时的阈值:比很差时耐心一点,比健康时严一点。
|
||||
THRESHOLD_MID=5
|
||||
STALE_MINUTES=10 # 日志多久没动静算卡死
|
||||
# 两次重启至少隔这么久。
|
||||
# 150 秒不是拍脑袋:隧道在差的 4G 上重建 5 条连接要 30~60 秒,
|
||||
@@ -105,15 +107,26 @@ write_state() {
|
||||
|
||||
# 读某个 metrics 端口的连接数。
|
||||
# 端口不通(实例死了)→ 输出 "X"(区分于 "0":0 是活着但没连接)
|
||||
#
|
||||
# ⚠ 必须重试。之前只用一次 `curl --max-time 4` 判定,
|
||||
# 设备忙的时候(CPU 被别的进程占住、SSD/IO 抖动)curl 会超时,
|
||||
# 于是被误判成"实例无响应"进而重启隧道 —— 一次误判就是 26 秒中断。
|
||||
# 历史重启原因里 8 次有 6 次写的是"实例无响应",其中一部分很可能是
|
||||
# 这种假阳性(真死的只有那次服务被我 stop 掉的)。
|
||||
# 给 3 次机会、每次间隔 1 秒,把瞬时超时排除掉。
|
||||
conns_of() {
|
||||
local port="$1" out
|
||||
out=$(timeout 5 curl -s --max-time 4 "http://127.0.0.1:$port/metrics" 2>/dev/null \
|
||||
| grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}')
|
||||
if [ -z "$out" ]; then
|
||||
echo "X"
|
||||
else
|
||||
echo "$out"
|
||||
fi
|
||||
local port="$1" out attempt
|
||||
for attempt in 1 2 3; do
|
||||
out=$(timeout 6 curl -s --max-time 5 \
|
||||
"http://127.0.0.1:$port/metrics" 2>/dev/null \
|
||||
| grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}')
|
||||
if [ -n "$out" ]; then
|
||||
echo "$out"
|
||||
return
|
||||
fi
|
||||
[ "$attempt" -lt 3 ] && sleep 1
|
||||
done
|
||||
echo "X"
|
||||
}
|
||||
|
||||
# 重启指定的一组服务(带最小间隔保护)
|
||||
@@ -295,15 +308,30 @@ if [ "$DEGRADED" = "1" ]; then
|
||||
fi
|
||||
|
||||
if [ "$FAIL" -ge "$THRESHOLD" ] || [ "$ZERO" -ge "$THRESHOLD" ]; then
|
||||
# 连接还健康的话,用更宽的阈值 —— 别把"抖一下"升级成 1 分钟中断
|
||||
if [ "$TOTAL" -ge "$BUILT_MIN" ] 2>/dev/null && [ "$ZERO" -lt "$THRESHOLD" ]; then
|
||||
if [ "$FAIL" -lt "$THRESHOLD_HEALTHY" ]; then
|
||||
log "公网连续 $FAIL 次不通,但连接健康(合计 $TOTAL 条)—— 再等等,不重启"
|
||||
write_state "$ZERO" "$FAIL" "$BUILT"
|
||||
exit 0
|
||||
# 按连接健康程度分三档,越健康越有耐心 ——
|
||||
# 别把"抖一下"升级成 1 分钟中断。
|
||||
#
|
||||
# TOTAL >= 6 健康 -> 要连续 8 次(约 2.5 分钟)
|
||||
# 4 <= TOTAL < 6 中等 -> 连续 5 次(约 1.7 分钟)
|
||||
# TOTAL < 4 很差 -> 保持 3 次(约 1 分钟)
|
||||
#
|
||||
# 中间的 5 次那一档是补上的:原来只要 TOTAL < 6 就直接用 3 次,
|
||||
# 于是 4 条连接(虽然不满但还能扛)也会在一分钟内被重启。
|
||||
local_threshold=$THRESHOLD
|
||||
if [ "$ZERO" -lt "$THRESHOLD" ]; then
|
||||
if [ "$TOTAL" -ge "$BUILT_MIN" ] 2>/dev/null; then
|
||||
local_threshold=$THRESHOLD_HEALTHY
|
||||
elif [ "$TOTAL" -ge 4 ] 2>/dev/null; then
|
||||
local_threshold=$THRESHOLD_MID
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$FAIL" -lt "$local_threshold" ]; then
|
||||
log "公网连续 $FAIL 次不通,连接 $TOTAL 条(阈值 $local_threshold)—— 再等等,不重启"
|
||||
write_state "$ZERO" "$FAIL" "$BUILT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 有实例只是"低连接"(不是完全没响应)时,只重启那些实例;
|
||||
# 全都低就重启全部。
|
||||
if [ "${#LOW_SVCS[@]}" -gt 0 ] && [ "${#LOW_SVCS[@]}" -lt "${#ALL_SVCS[@]}" ]; then
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
{
|
||||
"port": 8841,
|
||||
"server_name": "DESKTOP-RN5I625",
|
||||
"allow_run": true,
|
||||
"allow_power": true,
|
||||
"allow_screen": true,
|
||||
"start_minimized": false,
|
||||
"allow_new_pairing": true,
|
||||
"image_timeout": 0,
|
||||
"image_display": "window",
|
||||
"image_custom_width": 0,
|
||||
"image_custom_height": 0,
|
||||
"image_window_title": "",
|
||||
"tts_voice": "",
|
||||
"tts_rate": 0,
|
||||
"mp3_dir": "",
|
||||
"file_roots": "",
|
||||
"extra_apps": "",
|
||||
"extra_commands": "",
|
||||
"download_dir": "received",
|
||||
"screen_max_width": 480,
|
||||
"screen_quality": 72,
|
||||
"screen_fps": 12,
|
||||
"pairing_code": "097783",
|
||||
"pairing_created": 1791366548.4543607,
|
||||
"close_pair_after_first": true
|
||||
}
|
||||
Reference in New Issue
Block a user