mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 11:20:43 +00:00
单次请求失败不再立刻判离线,改成连续 3 次
实测单个请求有 2~4% 的偶发失败(4G 丢包打在一条隧道连接上, 或者那条连接正在重连)。原逻辑是一失败就 online = false, 于是界面上会闪一下已断开 —— 用户点按钮时正好撞上那一闪, 看起来就像又连不上了。 而链路其实 1.5~15 秒就自己恢复(客户端本来就有退避重试)。 改成连续失败 3 次才判离线(手表至少 4.5 秒、电脑至少 6 秒): * 单次抖动完全不影响界面 * 真的断了仍然如实显示 这不是掩盖问题,而是把是否可用的判断从一次采样改成连续采样 —— 单点采样在有丢包的链路上噪声太大。 两边同步改: android .../RelayTransport.java failStreak + OFFLINE_AFTER windows/lanremote/relay.py _fail_streak + OFFLINE_AFTER 顺带记录 PC 端基准(真实用户视角,之前都是在设备上量的, 那等于把设备 4G 算了两遍): 公网可用率 98% 耗时 中位 911 P90 1889 P99 23868 ms 长轮询 5/6 耗时 60.8~62.1 秒 对比设备端(中位 1344 / P90 2563)—— 用户实际体验比设备端量出来的好。
This commit is contained in:
+133
@@ -0,0 +1,133 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# 1) 把 MTU 1400 固化下来(重启也在)
|
||||||
|
# 2) 在 MTU 1400 下对照 quic+6 / quic+8 / quic+10
|
||||||
|
#
|
||||||
|
# 为什么要固化 MTU:
|
||||||
|
# QUIC 走 UDP,**我们那条 --set-mss 1400 的 iptables 规则只管 TCP**。
|
||||||
|
# 实测把 wlan0 MTU 从 1500 降到 1400 之后:
|
||||||
|
# QUIC 建齐 6 条连接的时间 28 秒 -> 13 秒
|
||||||
|
# dial 失败次数 2 -> 1
|
||||||
|
# 原因:UDP 丢包不会回 ICMP,QUIC 发了超出路径 MTU 的包只能等超时。
|
||||||
|
# 把接口 MTU 压到 1400,QUIC 的包就都在安全范围内。
|
||||||
|
#
|
||||||
|
# 为什么还要试更多连接:
|
||||||
|
# 已经看到 http2+8 建齐只要 12 秒且 0 失败(QUIC+6 是 28 秒/2 次失败)。
|
||||||
|
# 连接多 = 单条掉线影响小。但 QUIC 的连接建立更贵,得实测平衡点。
|
||||||
|
set -u
|
||||||
|
|
||||||
|
BASE=https://relay.xxmxxs.top
|
||||||
|
UA='LanRemote/1.8.2'
|
||||||
|
SVC=/etc/systemd/system/lanremote-tunnel.service
|
||||||
|
NETTUNE=/etc/systemd/system/lanremote-nettune.service
|
||||||
|
BACKUP=/root/tunnel.bak.v4.$(date +%s)
|
||||||
|
cp -a "$SVC" "$BACKUP"
|
||||||
|
N=60
|
||||||
|
MTU=1400
|
||||||
|
MSS=1360
|
||||||
|
|
||||||
|
echo "════════ 1. 固化 MTU(写进 net-tune 服务)════════"
|
||||||
|
echo " 现在的 net-tune 服务:"
|
||||||
|
systemctl cat lanremote-nettune 2>/dev/null | grep -E 'ExecStart' | sed 's/^/ /'
|
||||||
|
|
||||||
|
# 把 MTU 设置追加进去(幂等:已有的先删掉)
|
||||||
|
cp -a "$NETTUNE" "${NETTUNE}.bak.$(date +%s)" 2>/dev/null || true
|
||||||
|
python3 - <<'PYEOF'
|
||||||
|
from pathlib import Path
|
||||||
|
p = Path("/etc/systemd/system/lanremote-nettune.service")
|
||||||
|
if not p.is_file():
|
||||||
|
print(" ! net-tune 服务不存在,跳过")
|
||||||
|
raise SystemExit(0)
|
||||||
|
text = p.read_text(encoding="utf-8")
|
||||||
|
# 去掉旧的 MTU/MSS 行
|
||||||
|
lines = [l for l in text.splitlines()
|
||||||
|
if "ip link set wlan0 mtu" not in l and "--set-mss" not in l]
|
||||||
|
out = []
|
||||||
|
for line in lines:
|
||||||
|
out.append(line)
|
||||||
|
# 在每个 ExecStart 之后补上 MTU 设置
|
||||||
|
if line.strip().startswith("ExecStart="):
|
||||||
|
out.append("ExecStart=/sbin/ip link set wlan0 mtu 1400")
|
||||||
|
out.append("ExecStart=/sbin/iptables -t mangle -C OUTPUT -p tcp "
|
||||||
|
"--tcp-flags SYN,RST SYN -j TCPMSS --set-mss 1360 || "
|
||||||
|
"/sbin/iptables -t mangle -A OUTPUT -p tcp "
|
||||||
|
"--tcp-flags SYN,RST SYN -j TCPMSS --set-mss 1360")
|
||||||
|
p.write_text("\n".join(out) + "\n", encoding="utf-8")
|
||||||
|
print(" 已写入 MTU 1400 + MSS 1360")
|
||||||
|
PYEOF
|
||||||
|
|
||||||
|
systemctl daemon-reload
|
||||||
|
echo " 立即执行一次:"
|
||||||
|
ip link set wlan0 mtu $MTU && echo " wlan0 MTU -> $(cat /sys/class/net/wlan0/mtu)"
|
||||||
|
iptables -t mangle -C OUTPUT -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss $MSS 2>/dev/null \
|
||||||
|
|| iptables -t mangle -A OUTPUT -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss $MSS
|
||||||
|
echo " MSS 规则: $(iptables -t mangle -S OUTPUT | grep TCPMSS | head -1)"
|
||||||
|
echo " net-tune 现在的内容:"
|
||||||
|
systemctl cat lanremote-nettune 2>/dev/null | grep ExecStart | sed 's/^/ /'
|
||||||
|
echo " 服务状态: $(systemctl is-enabled lanremote-nettune 2>/dev/null) / $(systemctl is-active lanremote-nettune 2>/dev/null)"
|
||||||
|
|
||||||
|
wait_conns() {
|
||||||
|
local want="$1" limit="$2" t0 t1 n
|
||||||
|
t0=$(date +%s)
|
||||||
|
while true; do
|
||||||
|
n=$(curl -s --max-time 4 http://127.0.0.1:20241/metrics 2>/dev/null \
|
||||||
|
| grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}')
|
||||||
|
n=${n:-0}
|
||||||
|
t1=$(date +%s)
|
||||||
|
[ "$n" -ge "$want" ] || [ $((t1 - t0)) -ge "$limit" ] && { echo "$n $((t1 - t0))"; return; }
|
||||||
|
sleep 3
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
run_group() {
|
||||||
|
local conns="$1"
|
||||||
|
echo "════════ quic + $conns (MTU $MTU)════════"
|
||||||
|
cp -a "$BACKUP" "$SVC"
|
||||||
|
sed -i "s/--protocol [a-z0-9]*/--protocol quic/" "$SVC"
|
||||||
|
sed -i "s/--ha-connections [0-9]*/--ha-connections $conns/" "$SVC"
|
||||||
|
systemctl daemon-reload
|
||||||
|
local mark; mark=$(wc -l < /root/lanremote-relay/tunnel.log 2>/dev/null || echo 0)
|
||||||
|
systemctl restart lanremote-tunnel
|
||||||
|
local res; res=$(wait_conns "$conns" 150)
|
||||||
|
local n s
|
||||||
|
n=$(echo "$res" | awk '{print $1}'); s=$(echo "$res" | awk '{print $2}')
|
||||||
|
local df; df=$(tail -n +$((mark+1)) /root/lanremote-relay/tunnel.log 2>/dev/null | grep -c 'Failed to dial' || echo 0)
|
||||||
|
echo " 建齐 $n/$conns,用 ${s} 秒,dial 失败 ${df} 次"
|
||||||
|
|
||||||
|
local ok=0 times=()
|
||||||
|
for i in $(seq 1 "$N"); do
|
||||||
|
out=$(curl -s -o /dev/null -w '%{http_code} %{time_total}' --max-time 25 -H "User-Agent: $UA" "$BASE/v1/health")
|
||||||
|
c=$(echo "$out" | awk '{print $1}'); ms=$(echo "$out" | awk '{printf "%d", $2*1000}')
|
||||||
|
[ "$c" = "200" ] && { ok=$((ok+1)); times+=("$ms"); }
|
||||||
|
done
|
||||||
|
local pct=$((ok * 100 / N))
|
||||||
|
local stats="无数据"
|
||||||
|
[ "${#times[@]}" -gt 0 ] && stats=$(printf '%s\n' "${times[@]}" | sort -n | awk '{a[NR]=$1} END {
|
||||||
|
printf "中位 %d P90 %d P99 %d 最大 %d", a[int(NR/2)+1], a[int(NR*0.9)+1], a[int(NR*0.99)+1], a[NR]}')
|
||||||
|
printf " 可用率 %d%% (%d/%d) %s ms\n" "$pct" "$ok" "$N" "$stats"
|
||||||
|
echo "quic+$conns|$n|$s|$df|$pct|$stats" >> /tmp/v4.txt
|
||||||
|
echo
|
||||||
|
}
|
||||||
|
|
||||||
|
rm -f /tmp/v4.txt
|
||||||
|
run_group 6
|
||||||
|
run_group 8
|
||||||
|
run_group 10
|
||||||
|
|
||||||
|
echo "════════ 汇总 ════════"
|
||||||
|
printf " %-10s %-14s %-10s %-8s %s\n" "配置" "建齐/耗时" "dial失败" "可用率" "延迟"
|
||||||
|
while IFS='|' read -r label n s df pct stats; do
|
||||||
|
printf " %-10s %-14s %-10s %-8s %s\n" "$label" "${n}条 ${s}s" "$df" "${pct}%" "$stats"
|
||||||
|
done < /tmp/v4.txt
|
||||||
|
|
||||||
|
echo
|
||||||
|
BESTN=$(sort -t'|' -k5 -rn /tmp/v4.txt | head -1 | sed 's/quic+//' | cut -d'|' -f1)
|
||||||
|
echo " → 采用 quic + $BESTN"
|
||||||
|
cp -a "$BACKUP" "$SVC"
|
||||||
|
sed -i "s/--protocol [a-z0-9]*/--protocol quic/" "$SVC"
|
||||||
|
sed -i "s/--ha-connections [0-9]*/--ha-connections $BESTN/" "$SVC"
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl restart lanremote-tunnel
|
||||||
|
sleep 5
|
||||||
|
echo " 最终参数: $(grep -oE '\-\-protocol [a-z0-9]+|--ha-connections [0-9]+' "$SVC" | tr '\n' ' ')"
|
||||||
|
echo " MTU: $(cat /sys/class/net/wlan0/mtu)"
|
||||||
|
echo " 备份: $BACKUP"
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
r'''单次请求失败不再立刻判"离线",改成连续失败 3 次才算。
|
||||||
|
|
||||||
|
== 为什么 ==
|
||||||
|
|
||||||
|
实测单个请求会有 2~4% 的偶发失败(4G 丢包打在一条隧道连接上,
|
||||||
|
或者那条连接正在重连)。原来的逻辑是**一失败就 online = false**,
|
||||||
|
于是:
|
||||||
|
|
||||||
|
手表/电脑的界面上会闪一下"已断开"
|
||||||
|
用户点按钮时正好撞上那一闪 -> 看起来像"又连不上了"
|
||||||
|
|
||||||
|
而实际上链路 1.5~15 秒后就自己恢复了(客户端本来就有退避重试)。
|
||||||
|
|
||||||
|
改成"连续失败 3 次才判离线",效果是:
|
||||||
|
* 单次抖动完全不影响界面显示
|
||||||
|
* 真的断了(连续 3 次,至少 4.5 秒都没通)仍然会如实显示离线
|
||||||
|
|
||||||
|
这不是掩盖问题,而是把"是否可用"的判断从"一次采样"改成
|
||||||
|
"连续采样"—— 单点采样在有丢包的链路上噪声太大。
|
||||||
|
|
||||||
|
== 两边都要改 ==
|
||||||
|
|
||||||
|
1. android/src/com/lanremote/watch/RelayTransport.java (手表轮询)
|
||||||
|
2. windows/lanremote/relay.py (电脑轮询)
|
||||||
|
'''
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
|
||||||
|
BOM = b"\xef\xbb\xbf"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- 手表
|
||||||
|
WATCH = ROOT / "android/src/com/lanremote/watch/RelayTransport.java"
|
||||||
|
|
||||||
|
WATCH_CONST_OLD = " private static final int RETRY_MAX_MS = 15000;"
|
||||||
|
WATCH_CONST_NEW = """ private static final int RETRY_MAX_MS = 15000;
|
||||||
|
/**
|
||||||
|
* 连续失败几次才判定"离线"。
|
||||||
|
*
|
||||||
|
* <p>单次失败就报离线太敏感:4G 链路上随手就有 2~4% 的偶发失败,
|
||||||
|
* 而链路通常一两秒就自己恢复,界面却已经闪了一下"已断开"。
|
||||||
|
* 改成连续 3 次(至少 4.5 秒都不通)才判离线 ——
|
||||||
|
* 真的断了仍然会如实显示。
|
||||||
|
*/
|
||||||
|
private static final int OFFLINE_AFTER = 3;"""
|
||||||
|
|
||||||
|
WATCH_FIELD_OLD = " private Thread pollThread;"
|
||||||
|
WATCH_FIELD_NEW = """ private Thread pollThread;
|
||||||
|
/** 连续失败次数(成功一次就清零)。 */
|
||||||
|
private volatile int failStreak;"""
|
||||||
|
|
||||||
|
# 401/403 分支
|
||||||
|
W_401_OLD = """ if (result.code == 401 || result.code == 403) {
|
||||||
|
// 会话过期(中转重启等)—— 这种可以自动重连
|
||||||
|
online = false;
|
||||||
|
lastError = "会话已失效,正在重新接入";"""
|
||||||
|
W_401_NEW = """ if (result.code == 401 || result.code == 403) {
|
||||||
|
// 会话过期(中转重启等)—— 这种可以自动重连。
|
||||||
|
// 会话失效是明确的状态,直接标离线。
|
||||||
|
online = false;
|
||||||
|
lastError = "会话已失效,正在重新接入";"""
|
||||||
|
|
||||||
|
# 非 200 分支
|
||||||
|
W_OTHER_OLD = """ if (result.code != 200) {
|
||||||
|
online = false;
|
||||||
|
lastError = "轮询返回 " + result.code;
|
||||||
|
sleep(backoff);
|
||||||
|
backoff = Math.min(RETRY_MAX_MS, backoff * 2);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
online = true;
|
||||||
|
lastError = "";
|
||||||
|
backoff = RETRY_MIN_MS;"""
|
||||||
|
W_OTHER_NEW = """ if (result.code != 200) {
|
||||||
|
// 偶发的 502/504 不算断线,连续几次才算
|
||||||
|
failStreak++;
|
||||||
|
if (failStreak >= OFFLINE_AFTER) {
|
||||||
|
online = false;
|
||||||
|
}
|
||||||
|
lastError = "轮询返回 " + result.code;
|
||||||
|
sleep(backoff);
|
||||||
|
backoff = Math.min(RETRY_MAX_MS, backoff * 2);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
online = true;
|
||||||
|
failStreak = 0;
|
||||||
|
lastError = "";
|
||||||
|
backoff = RETRY_MIN_MS;"""
|
||||||
|
|
||||||
|
# 异常分支
|
||||||
|
W_EXC_OLD = """ } catch (Exception exc) {
|
||||||
|
if (closed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
online = false;
|
||||||
|
lastError = "轮询出错:" + exc.getClass().getSimpleName();
|
||||||
|
sleep(backoff);
|
||||||
|
backoff = Math.min(RETRY_MAX_MS, backoff * 2);
|
||||||
|
}"""
|
||||||
|
W_EXC_NEW = """ } catch (Exception exc) {
|
||||||
|
if (closed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 连接超时/重置很常见(隧道在重连),连续几次才算真断
|
||||||
|
failStreak++;
|
||||||
|
if (failStreak >= OFFLINE_AFTER) {
|
||||||
|
online = false;
|
||||||
|
}
|
||||||
|
lastError = "轮询出错:" + exc.getClass().getSimpleName();
|
||||||
|
sleep(backoff);
|
||||||
|
backoff = Math.min(RETRY_MAX_MS, backoff * 2);
|
||||||
|
}"""
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- 电脑
|
||||||
|
AGENT = ROOT / "windows/lanremote/relay.py"
|
||||||
|
|
||||||
|
A_CONST_OLD = "RETRY_MAX = 20.0"
|
||||||
|
A_CONST_NEW = """RETRY_MAX = 20.0
|
||||||
|
# 连续失败几次才判定"离线"。
|
||||||
|
# 单次失败就报离线太敏感:4G 链路上有 2~4% 的偶发失败,
|
||||||
|
# 而链路通常一两秒就自己恢复,界面却已经闪了一下"已断开"。
|
||||||
|
# 连续 3 次(至少 6 秒都不通)才判离线,真的断了仍然如实显示。
|
||||||
|
OFFLINE_AFTER = 3"""
|
||||||
|
|
||||||
|
A_FAIL_OLD = """ except Exception as exc: # noqa: BLE001
|
||||||
|
self.online = False
|
||||||
|
self.last_error = f"{type(exc).__name__}: {exc}"
|
||||||
|
self._log(f"中转连接出问题:{self.last_error}")"""
|
||||||
|
A_FAIL_NEW = """ except Exception as exc: # noqa: BLE001
|
||||||
|
# 偶发的连接超时/重置不算断线,连续几次才算
|
||||||
|
self._fail_streak = getattr(self, "_fail_streak", 0) + 1
|
||||||
|
if self._fail_streak >= OFFLINE_AFTER:
|
||||||
|
self.online = False
|
||||||
|
self.last_error = f"{type(exc).__name__}: {exc}"
|
||||||
|
self._log(f"中转连接出问题(第 {self._fail_streak} 次):"
|
||||||
|
f"{self.last_error}")"""
|
||||||
|
|
||||||
|
A_OK_OLD = """ self.online = True
|
||||||
|
self.last_ok = time.time()
|
||||||
|
self.last_error = \"\""""
|
||||||
|
A_OK_NEW = """ self.online = True
|
||||||
|
self._fail_streak = 0
|
||||||
|
self.last_ok = time.time()
|
||||||
|
self.last_error = \"\""""
|
||||||
|
|
||||||
|
|
||||||
|
def patch(text: str, old: str, new: str, label: str) -> tuple:
|
||||||
|
if old not in text:
|
||||||
|
if new.strip() and new.splitlines()[-1].strip() in text:
|
||||||
|
print(" • %s 已经是新值" % label)
|
||||||
|
return text, True
|
||||||
|
print(" ✗ 没找到:%s" % label)
|
||||||
|
return text, False
|
||||||
|
if text.count(old) != 1:
|
||||||
|
print(" ! 出现 %d 次:%s" % (text.count(old), label))
|
||||||
|
return text, False
|
||||||
|
print(" ✓ %s" % label)
|
||||||
|
return text.replace(old, new, 1), True
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
ok_all = True
|
||||||
|
|
||||||
|
print(" 手表 RelayTransport.java:")
|
||||||
|
raw = WATCH.read_bytes()
|
||||||
|
text = raw.decode("utf-8-sig")
|
||||||
|
for old, new, label in (
|
||||||
|
(WATCH_CONST_OLD, WATCH_CONST_NEW, "加 OFFLINE_AFTER 常量"),
|
||||||
|
(WATCH_FIELD_OLD, WATCH_FIELD_NEW, "加 failStreak 字段"),
|
||||||
|
(W_OTHER_OLD, W_OTHER_NEW, "非 200 用连续计数"),
|
||||||
|
(W_EXC_OLD, W_EXC_NEW, "异常用连续计数"),
|
||||||
|
):
|
||||||
|
text, ok = patch(text, old, new, label)
|
||||||
|
ok_all = ok_all and ok
|
||||||
|
|
||||||
|
if ok_all:
|
||||||
|
WATCH.write_text(text, encoding="utf-8", newline="\n")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print(" 电脑 relay.py:")
|
||||||
|
araw = AGENT.read_bytes()
|
||||||
|
atext = araw.decode("utf-8-sig")
|
||||||
|
for old, new, label in (
|
||||||
|
(A_CONST_OLD, A_CONST_NEW, "加 OFFLINE_AFTER 常量"),
|
||||||
|
(A_FAIL_OLD, A_FAIL_NEW, "异常用连续计数"),
|
||||||
|
(A_OK_OLD, A_OK_NEW, "成功时清零"),
|
||||||
|
):
|
||||||
|
atext, ok = patch(atext, old, new, label)
|
||||||
|
ok_all = ok_all and ok
|
||||||
|
|
||||||
|
if not ok_all:
|
||||||
|
print()
|
||||||
|
print(" 有失败项,没写回电脑端")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
AGENT.write_text(atext, encoding="utf-8", newline="\n")
|
||||||
|
print()
|
||||||
|
print(" 两边都改好了")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# 采用最优配置 quic + 10,并清理 MSS 规则。
|
||||||
|
#
|
||||||
|
# v4 实测(MTU 1400,各 60 次):
|
||||||
|
# quic+6 建齐 10s 0 失败 100% 中位 1399 P90 2611 P99 13705
|
||||||
|
# quic+8 建齐 16s 1 失败 100% 中位 1280 P90 2279 P99 3856
|
||||||
|
# quic+10 建齐 13s 0 失败 100% 中位 1256 P90 2068 P99 2991 <- 最好
|
||||||
|
#
|
||||||
|
# 注意:v4 脚本自己的排序逻辑有多行输出没处理干净,误选了 quic+8,
|
||||||
|
# 这里按数据手工改成 quic+10。
|
||||||
|
#
|
||||||
|
# MSS 说明:接口 MTU 已经是 1400,内核自己算出来的 MSS 就是 1360;
|
||||||
|
# iptables 的 --set-mss 只能把 MSS 往小改,所以那条 1400 的规则
|
||||||
|
# 现在实际是空转。统一改成 1360,避免以后 MTU 变回去时留下不一致。
|
||||||
|
set -u
|
||||||
|
|
||||||
|
SVC=/etc/systemd/system/lanremote-tunnel.service
|
||||||
|
UA='LanRemote/1.8.2'
|
||||||
|
|
||||||
|
echo "════════ 1. 采用 quic + 10 ════════"
|
||||||
|
sed -i "s/--protocol [a-z0-9]*/--protocol quic/" "$SVC"
|
||||||
|
sed -i "s/--ha-connections [0-9]*/--ha-connections 10/" "$SVC"
|
||||||
|
grep -oE '\-\-protocol [a-z0-9]+|--ha-connections [0-9]+|--edge-ip-version [0-9]+|--retries [0-9]+' "$SVC" | sed 's/^/ /'
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "════════ 2. 清理 MSS 规则(只留一条 1360)════════"
|
||||||
|
iptables -t mangle -S OUTPUT 2>/dev/null | grep TCPMSS | sed 's/^/ 清理前: /'
|
||||||
|
while iptables -t mangle -D OUTPUT -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss 1400 2>/dev/null; do :; done
|
||||||
|
while iptables -t mangle -D OUTPUT -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss 1360 2>/dev/null; do :; done
|
||||||
|
iptables -t mangle -A OUTPUT -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss 1360
|
||||||
|
iptables -t mangle -S OUTPUT 2>/dev/null | grep TCPMSS | sed 's/^/ 清理后: /'
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "════════ 3. MTU 和持久化 ════════"
|
||||||
|
echo " wlan0 MTU 现在: $(cat /sys/class/net/wlan0/mtu)"
|
||||||
|
grep -c 'mtu 1400' /etc/systemd/system/lanremote-nettune.service | sed 's/^/ net-tune 里的 MTU 设置行数: /'
|
||||||
|
echo " net-tune 服务: $(systemctl is-enabled lanremote-nettune) / $(systemctl is-active lanremote-nettune)"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "════════ 4. 重启隧道并等连接建齐 ════════"
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl restart lanremote-tunnel
|
||||||
|
for i in $(seq 1 40); do
|
||||||
|
sleep 4
|
||||||
|
n=$(curl -s --max-time 4 http://127.0.0.1:20241/metrics 2>/dev/null \
|
||||||
|
| grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}')
|
||||||
|
[ "${n:-0}" -ge 10 ] && break
|
||||||
|
done
|
||||||
|
echo " 建齐 $n/10 条(等了约 $((i*4)) 秒)"
|
||||||
|
curl -s --max-time 5 http://127.0.0.1:20241/metrics 2>/dev/null \
|
||||||
|
| grep 'server_locations.*} 1' | sed 's/^/ /'
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "════════ 5. 等公网可用 ════════"
|
||||||
|
for i in $(seq 1 20); do
|
||||||
|
c=$(curl -s -o /dev/null -w '%{http_code}' --max-time 12 -H "User-Agent: $UA" https://relay.xxmxxs.top/v1/health)
|
||||||
|
printf " 第 %d 次: %s\n" "$i" "$c"
|
||||||
|
[ "$c" = "200" ] && break
|
||||||
|
sleep 3
|
||||||
|
done
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "════════ 6. 快速验收(30 次)════════"
|
||||||
|
ok=0; times=()
|
||||||
|
for i in $(seq 1 30); do
|
||||||
|
out=$(curl -s -o /dev/null -w '%{http_code} %{time_total}' --max-time 25 -H "User-Agent: $UA" https://relay.xxmxxs.top/v1/health)
|
||||||
|
c=$(echo "$out" | awk '{print $1}'); ms=$(echo "$out" | awk '{printf "%d", $2*1000}')
|
||||||
|
[ "$c" = "200" ] && { ok=$((ok+1)); times+=("$ms"); }
|
||||||
|
done
|
||||||
|
stats=$(printf '%s\n' "${times[@]:-0}" | sort -n | awk '{a[NR]=$1} END {printf "中位 %d P90 %d P99 %d 最大 %d", a[int(NR/2)+1], a[int(NR*0.9)+1], a[int(NR*0.99)+1], a[NR]}')
|
||||||
|
echo " 可用率 $ok/30 = $((ok*100/30))% $stats ms"
|
||||||
|
echo
|
||||||
|
echo " 最终: $(grep -oE '\-\-protocol [a-z0-9]+|--ha-connections [0-9]+' "$SVC" | tr '\n' ' ') MTU=$(cat /sys/class/net/wlan0/mtu)"
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
r'''从这台 Windows 机器测中转 —— 这才是"真实用户"的视角。
|
||||||
|
|
||||||
|
== 为什么要换测量端 ==
|
||||||
|
|
||||||
|
之前所有测量都是在设备自己上跑的 curl。那等于:
|
||||||
|
设备 4G --> Cloudflare 边缘 (第一段)
|
||||||
|
边缘 --> 隧道 --> 设备 4G 源站 (第二段)
|
||||||
|
**两段都压在同一个 4G 链路上**,量出来的数字是双重打击。
|
||||||
|
|
||||||
|
真实用户(手机/电脑在 WiFi、宽带、5G 上):
|
||||||
|
用户 --> Cloudflare 边缘 (第一段,好网络)
|
||||||
|
边缘 --> 隧道 --> 设备 4G 源站 (第二段,只有这段是 4G)
|
||||||
|
|
||||||
|
所以从这台机器量出来的,才是用户实际会遇到的延迟和可用率。
|
||||||
|
|
||||||
|
用法: python _build/bench_from_pc.py [标签]
|
||||||
|
'''
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import statistics
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import urllib.error
|
||||||
|
import urllib.request
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
BASE = "https://relay.xxmxxs.top"
|
||||||
|
UA = "LanRemote/1.8.2"
|
||||||
|
OUT_DIR = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote\_build")
|
||||||
|
|
||||||
|
|
||||||
|
def req(path: str, method: str = "GET", data: bytes | None = None,
|
||||||
|
headers: dict | None = None, timeout: float = 150.0):
|
||||||
|
"""发一个请求,返回 (状态码, 字节数, 秒数, 响应头)。"""
|
||||||
|
hdrs = {"User-Agent": UA}
|
||||||
|
hdrs.update(headers or {})
|
||||||
|
r = urllib.request.Request(BASE + path, data=data, headers=hdrs,
|
||||||
|
method=method)
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(r, timeout=timeout) as resp:
|
||||||
|
body = resp.read()
|
||||||
|
return resp.status, len(body), time.perf_counter() - t0, resp.headers
|
||||||
|
except urllib.error.HTTPError as exc:
|
||||||
|
try:
|
||||||
|
exc.read()
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
pass
|
||||||
|
return exc.code, 0, time.perf_counter() - t0, exc.headers
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
return 0, 0, time.perf_counter() - t0, {}
|
||||||
|
|
||||||
|
|
||||||
|
def pct(values: list, q: float) -> int:
|
||||||
|
if not values:
|
||||||
|
return 0
|
||||||
|
s = sorted(values)
|
||||||
|
idx = min(int(len(s) * q), len(s) - 1)
|
||||||
|
return int(s[idx])
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
label = sys.argv[1] if len(sys.argv) > 1 else "pc"
|
||||||
|
lines: list[str] = []
|
||||||
|
|
||||||
|
def say(text: str = "") -> None:
|
||||||
|
print(text)
|
||||||
|
lines.append(text)
|
||||||
|
|
||||||
|
say("=" * 62)
|
||||||
|
say(" 从 PC 量中转 · %s · %s" % (label, time.strftime("%Y-%m-%d %H:%M:%S")))
|
||||||
|
say("=" * 62)
|
||||||
|
|
||||||
|
# ---------------- A. 公网可用率 ----------------
|
||||||
|
say()
|
||||||
|
say("── A. 公网可用率(100 次 /v1/health)──")
|
||||||
|
ok, times, fails = 0, [], 0
|
||||||
|
for i in range(100):
|
||||||
|
code, _n, secs, _h = req("/v1/health", timeout=25)
|
||||||
|
if code == 200:
|
||||||
|
ok += 1
|
||||||
|
times.append(int(secs * 1000))
|
||||||
|
else:
|
||||||
|
fails += 1
|
||||||
|
time.sleep(0.3)
|
||||||
|
say(" 可用率 %d/100 = %d%% 失败 %d 次" % (ok, ok, fails))
|
||||||
|
if times:
|
||||||
|
say(" 耗时 最小 %d 中位 %d P90 %d P99 %d 最大 %d ms"
|
||||||
|
% (min(times), int(statistics.median(times)), pct(times, 0.9),
|
||||||
|
pct(times, 0.99), max(times)))
|
||||||
|
|
||||||
|
# ---------------- E. 静态资源 ----------------
|
||||||
|
say()
|
||||||
|
say("── E. 静态资源(各 10 次)──")
|
||||||
|
for path in ("/", "/shot/pc-main.png", "/favicon.ico"):
|
||||||
|
o, ts = 0, []
|
||||||
|
for _ in range(10):
|
||||||
|
code, _n, secs, _h = req(path, timeout=25)
|
||||||
|
if code == 200:
|
||||||
|
o += 1
|
||||||
|
ts.append(int(secs * 1000))
|
||||||
|
time.sleep(0.2)
|
||||||
|
med = int(statistics.median(ts)) if ts else 0
|
||||||
|
say(" %-22s %2d/10 中位 %d 最大 %d ms"
|
||||||
|
% (path, o, med, max(ts) if ts else 0))
|
||||||
|
|
||||||
|
# ---------------- C. 下载 ----------------
|
||||||
|
say()
|
||||||
|
say("── C. 三个成品下载(完整下载)──")
|
||||||
|
dl_results = {}
|
||||||
|
for name in ("ykxmz-setup-1.8.2.exe", "ykxmz-server-1.8.2.zip",
|
||||||
|
"ykxmz-watch-1.8.2.apk"):
|
||||||
|
code, size, secs, hdrs = req("/download/" + name, timeout=300)
|
||||||
|
kbps = (size / secs / 1024) if secs > 0 else 0
|
||||||
|
cf = (hdrs.get("cf-cache-status") or "?") if hdrs else "?"
|
||||||
|
say(" %-30s HTTP %-4s %9d 字节 %6.1fs %7.0f KB/s cf=%s"
|
||||||
|
% (name, code, size, secs, kbps, cf))
|
||||||
|
if code == 200 and size > 0:
|
||||||
|
dl_results[name] = kbps
|
||||||
|
|
||||||
|
# ---------------- B. 长轮询 ----------------
|
||||||
|
say()
|
||||||
|
say("── B. 长轮询(6 次真实 poll,每次挂 60 秒)──")
|
||||||
|
stamp = int(time.time())
|
||||||
|
dev = "pcbench-%s-%d" % (label, stamp)
|
||||||
|
code6 = "%06d" % (stamp % 1000000)
|
||||||
|
payload = json.dumps({
|
||||||
|
"deviceId": dev, "code": code6, "name": "pcbench",
|
||||||
|
"version": "1.8.2",
|
||||||
|
}, ensure_ascii=False).encode("utf-8")
|
||||||
|
status, _n, _s, _h = req("/v1/agent/register", method="POST",
|
||||||
|
data=payload, timeout=30,
|
||||||
|
headers={"Content-Type": "application/json"})
|
||||||
|
token = ""
|
||||||
|
if status == 200:
|
||||||
|
# 重新取一次拿 body
|
||||||
|
r = urllib.request.Request(
|
||||||
|
BASE + "/v1/agent/register", data=payload,
|
||||||
|
headers={"User-Agent": UA, "Content-Type": "application/json"},
|
||||||
|
method="POST")
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(r, timeout=30) as resp:
|
||||||
|
token = json.loads(resp.read().decode("utf-8")).get("token", "")
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
pass
|
||||||
|
say(" 探针注册: %s" % ("成功" if token else "失败"))
|
||||||
|
|
||||||
|
if token:
|
||||||
|
okb, btimes, bfails = 0, [], []
|
||||||
|
for i in range(6):
|
||||||
|
code, _n, secs, _h = req(
|
||||||
|
"/v1/agent/poll", method="POST", data=b"",
|
||||||
|
headers={"X-Token": token}, timeout=150)
|
||||||
|
ms = int(secs * 1000)
|
||||||
|
if code == 200:
|
||||||
|
okb += 1
|
||||||
|
btimes.append(ms)
|
||||||
|
say(" %d: 200 %6dms" % (i + 1, ms))
|
||||||
|
else:
|
||||||
|
bfails.append("%d:%s" % (i + 1, code))
|
||||||
|
say(" %d: %-4s **失败**" % (i + 1, code))
|
||||||
|
say(" 长轮询成功 %d/6 = %d%% 失败: %s"
|
||||||
|
% (okb, okb * 100 // 6, " ".join(bfails) or "无"))
|
||||||
|
if btimes:
|
||||||
|
say(" 耗时 最小 %d 中位 %d P90 %d 最大 %d ms(理想 ≈ 61000)"
|
||||||
|
% (min(btimes), int(statistics.median(btimes)),
|
||||||
|
pct(btimes, 0.9), max(btimes)))
|
||||||
|
|
||||||
|
say()
|
||||||
|
say("=" * 62)
|
||||||
|
out = OUT_DIR / ("bench_pc_%s.txt" % label)
|
||||||
|
out.write_text("\n".join(lines), encoding="utf-8")
|
||||||
|
say(" 存档: %s" % out)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
==============================================================
|
||||||
|
从 PC 量中转 · optimized · 2026-10-07 16:01:23
|
||||||
|
==============================================================
|
||||||
|
|
||||||
|
── A. 公网可用率(100 次 /v1/health)──
|
||||||
|
可用率 98/100 = 98% 失败 2 次
|
||||||
|
耗时 最小 765 中位 911 P90 1889 P99 23868 最大 23868 ms
|
||||||
|
|
||||||
|
── E. 静态资源(各 10 次)──
|
||||||
|
/ 9/10 中位 1016 最大 1223 ms
|
||||||
|
/shot/pc-main.png 10/10 中位 1160 最大 1914 ms
|
||||||
|
/favicon.ico 8/10 中位 1075 最大 1993 ms
|
||||||
|
|
||||||
|
── C. 三个成品下载(完整下载)──
|
||||||
|
ykxmz-setup-1.8.2.exe HTTP 404 0 字节 0.8s 0 KB/s cf=BYPASS
|
||||||
|
ykxmz-server-1.8.2.zip HTTP 404 0 字节 0.8s 0 KB/s cf=BYPASS
|
||||||
|
ykxmz-watch-1.8.2.apk HTTP 404 0 字节 0.9s 0 KB/s cf=BYPASS
|
||||||
|
|
||||||
|
── B. 长轮询(6 次真实 poll,每次挂 60 秒)──
|
||||||
|
探针注册: 成功
|
||||||
|
1: 200 60817ms
|
||||||
|
2: 0 **失败**
|
||||||
|
3: 200 61758ms
|
||||||
|
4: 200 62027ms
|
||||||
|
5: 200 62060ms
|
||||||
|
6: 200 61247ms
|
||||||
|
长轮询成功 5/6 = 83% 失败: 2:0
|
||||||
|
耗时 最小 60817 中位 61758 P90 62060 最大 62060 ms(理想 ≈ 61000)
|
||||||
|
|
||||||
|
==============================================================
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 1.4 MiB |
@@ -53,6 +53,15 @@ public class RelayTransport implements Transport {
|
|||||||
/** 网络出错后的退避 */
|
/** 网络出错后的退避 */
|
||||||
private static final int RETRY_MIN_MS = 1500;
|
private static final int RETRY_MIN_MS = 1500;
|
||||||
private static final int RETRY_MAX_MS = 15000;
|
private static final int RETRY_MAX_MS = 15000;
|
||||||
|
/**
|
||||||
|
* 连续失败几次才判定"离线"。
|
||||||
|
*
|
||||||
|
* <p>单次失败就报离线太敏感:4G 链路上随手就有 2~4% 的偶发失败,
|
||||||
|
* 而链路通常一两秒就自己恢复,界面却已经闪了一下"已断开"。
|
||||||
|
* 改成连续 3 次(至少 4.5 秒都不通)才判离线 ——
|
||||||
|
* 真的断了仍然会如实显示。
|
||||||
|
*/
|
||||||
|
private static final int OFFLINE_AFTER = 3;
|
||||||
/** 单次读到的字节上限 */
|
/** 单次读到的字节上限 */
|
||||||
private static final int MAX_BODY = 8 * 1024 * 1024;
|
private static final int MAX_BODY = 8 * 1024 * 1024;
|
||||||
|
|
||||||
@@ -79,6 +88,8 @@ public class RelayTransport implements Transport {
|
|||||||
private final Object pendingLock = new Object();
|
private final Object pendingLock = new Object();
|
||||||
|
|
||||||
private Thread pollThread;
|
private Thread pollThread;
|
||||||
|
/** 连续失败次数(成功一次就清零)。 */
|
||||||
|
private volatile int failStreak;
|
||||||
private Thread sendThread;
|
private Thread sendThread;
|
||||||
private final Object closingLock = new Object();
|
private final Object closingLock = new Object();
|
||||||
|
|
||||||
@@ -344,13 +355,18 @@ public class RelayTransport implements Transport {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (result.code != 200) {
|
if (result.code != 200) {
|
||||||
online = false;
|
// 偶发的 502/504 不算断线,连续几次才算
|
||||||
|
failStreak++;
|
||||||
|
if (failStreak >= OFFLINE_AFTER) {
|
||||||
|
online = false;
|
||||||
|
}
|
||||||
lastError = "轮询返回 " + result.code;
|
lastError = "轮询返回 " + result.code;
|
||||||
sleep(backoff);
|
sleep(backoff);
|
||||||
backoff = Math.min(RETRY_MAX_MS, backoff * 2);
|
backoff = Math.min(RETRY_MAX_MS, backoff * 2);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
online = true;
|
online = true;
|
||||||
|
failStreak = 0;
|
||||||
lastError = "";
|
lastError = "";
|
||||||
backoff = RETRY_MIN_MS;
|
backoff = RETRY_MIN_MS;
|
||||||
if (result.body.length > 0) {
|
if (result.body.length > 0) {
|
||||||
@@ -360,7 +376,11 @@ public class RelayTransport implements Transport {
|
|||||||
if (closed) {
|
if (closed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
online = false;
|
// 连接超时/重置很常见(隧道在重连),连续几次才算真断
|
||||||
|
failStreak++;
|
||||||
|
if (failStreak >= OFFLINE_AFTER) {
|
||||||
|
online = false;
|
||||||
|
}
|
||||||
lastError = "轮询出错:" + exc.getClass().getSimpleName();
|
lastError = "轮询出错:" + exc.getClass().getSimpleName();
|
||||||
sleep(backoff);
|
sleep(backoff);
|
||||||
backoff = Math.min(RETRY_MAX_MS, backoff * 2);
|
backoff = Math.min(RETRY_MAX_MS, backoff * 2);
|
||||||
|
|||||||
@@ -43,6 +43,11 @@ MAX_BODY = 8 * 1024 * 1024
|
|||||||
# 网络断了之后的退避
|
# 网络断了之后的退避
|
||||||
RETRY_MIN = 2.0
|
RETRY_MIN = 2.0
|
||||||
RETRY_MAX = 20.0
|
RETRY_MAX = 20.0
|
||||||
|
# 连续失败几次才判定"离线"。
|
||||||
|
# 单次失败就报离线太敏感:4G 链路上有 2~4% 的偶发失败,
|
||||||
|
# 而链路通常一两秒就自己恢复,界面却已经闪了一下"已断开"。
|
||||||
|
# 连续 3 次(至少 6 秒都不通)才判离线,真的断了仍然如实显示。
|
||||||
|
OFFLINE_AFTER = 3
|
||||||
# 多久没成功通信就算掉线(用于界面上显示状态)
|
# 多久没成功通信就算掉线(用于界面上显示状态)
|
||||||
STALE_SECONDS = 90.0
|
STALE_SECONDS = 90.0
|
||||||
|
|
||||||
@@ -521,9 +526,13 @@ class RelayLink:
|
|||||||
break
|
break
|
||||||
continue
|
continue
|
||||||
except Exception as exc: # noqa: BLE001
|
except Exception as exc: # noqa: BLE001
|
||||||
self.online = False
|
# 偶发的连接超时/重置不算断线,连续几次才算
|
||||||
|
self._fail_streak = getattr(self, "_fail_streak", 0) + 1
|
||||||
|
if self._fail_streak >= OFFLINE_AFTER:
|
||||||
|
self.online = False
|
||||||
self.last_error = f"{type(exc).__name__}: {exc}"
|
self.last_error = f"{type(exc).__name__}: {exc}"
|
||||||
self._log(f"中转连接出问题:{self.last_error}")
|
self._log(f"中转连接出问题(第 {self._fail_streak} 次):"
|
||||||
|
f"{self.last_error}")
|
||||||
# 网络断了:把当前会话关掉,让上层重开
|
# 网络断了:把当前会话关掉,让上层重开
|
||||||
self._drop_socket()
|
self._drop_socket()
|
||||||
self.token = ""
|
self.token = ""
|
||||||
@@ -611,6 +620,7 @@ class RelayLink:
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
self.online = True
|
self.online = True
|
||||||
|
self._fail_streak = 0
|
||||||
self.last_ok = time.time()
|
self.last_ok = time.time()
|
||||||
self.last_error = ""
|
self.last_error = ""
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user