mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 12:30:44 +00:00
用户买了 AWS Lightsail(2G 内存 / 2 vCPU / 60G SSD / 公网 IP
13.54.30.196),中转从随身 WiFi 那台小机器搬过去。
== 为什么这次搬迁解决了一堆老问题 ==
原机器(Debian aarch64、460MB、走 4G)一直有几件搞不定的:
* 线路对 Cloudflare 隧道的 **7844 端口间歇性阻断**(443 通、7844 不通),
隧道每 3~10 分钟掉一次、公网跟着 530 —— 查了很久才从
cloudflared 自己的 precheck 里看清
* 只有 460MB 内存
* 会掉电硬挂(实测 2026-10-08 18:42 一次非正常关机)
新机器**有公网 IP**,中转直接监听,不经隧道、不过 7844。
实测公网延迟从 **1000~2000 ms 降到 1.1 ms**。
== 代码改动 ==
1. **relay.py 的 DATA_DIR 默认值不再写死**
原来是 /root/lanremote-relay。新机器上服务以 admin 普通用户跑
(它没理由要 root),而 /root 是 0700 —— 部署时直接
PermissionError: '/root/lanremote-relay'。
改成"本脚本所在目录下的 data/",搬到哪都跟着走。
2. **客户端支持"没有证书的中转"**
新服务器是明文 HTTP(没签证书),而两个客户端原来都往 https 走:
* 手表端:裸 IP 会被强制补 https:// -> 永远连不上,
报的还是 SSL 错误,很容易让人白查半天证书
* Android 9+ 默认禁止明文流量,没 usesCleartextTraffic
请求会被系统直接掐掉
Windows 端本来就是对的(正则里把任意纯 IP 都当"用明文")。
现在手表端规则和它一致,并加了 usesCleartextTraffic。
3. 清掉电脑端一段函数内的重复代码(return 之后还有一份拷贝,
缩进仍在函数体内所以不报错,但读起来很迷惑)。
== 新服务器上的部署 ==
/opt/ykxmz-relay/ 服务端(relay/admin/landing/showcase)
ykxmz-relay.service 监听 0.0.0.0:13245
ykxmz-monitor.service/.timer 每 60 秒查公网,连续 3 次不通就叫预案
rescue-lightsail.sh 四级阶梯抢救(重启中转 / 强杀重拉 /
查防火墙 / 重启整机[默认关])
预案针对新架构重写过:原来围着隧道转(重启隧道、重施 MSS/MTU),
现在没有隧道,改成重启中转 + 查端口/防火墙。
全量回归 46 个阶段全部通过。
286 lines
10 KiB
Bash
286 lines
10 KiB
Bash
#!/bin/bash
|
||
# ============================================================================
|
||
# 紧急预案(Lightsail 版)—— 网站打不开时按阶梯自动抢救
|
||
# ============================================================================
|
||
#
|
||
# 和随身 WiFi 那版的区别:
|
||
#
|
||
# 那版是**围着 Cloudflare 隧道**转的(第 1 级就是重启隧道、
|
||
# 第 3 级重施 MSS/MTU 调优)。现在这台新机器**有公网 IP**,
|
||
# 中转直接对外监听 13245,没有任何隧道 —— 所以:
|
||
#
|
||
# * 第 1 级从"重启隧道"改成"重启中转服务"
|
||
# * 去掉 MSS/MTU 那两级(那是 4G 线路的问题,这里没有)
|
||
# * 加一级"看端口有没有被占/进程死没死"
|
||
#
|
||
# 保留下来的核心设计没变:
|
||
#
|
||
# 1. **先记录现场,再动手**(重启会把证据冲掉)
|
||
# 2. **阶梯式,每级都验**:从代价最小到最大,好了就停
|
||
# 3. **全失败要留证据**:生成 txt 报告,后台可下载
|
||
# 4. **成功的招数记进知识库**:下次同类问题优先试
|
||
#
|
||
# 谁调用:monitor.sh(连续 N 次不通就调这里)
|
||
# 也可以人工跑:bash /opt/ykxmz-relay/rescue.sh
|
||
# ============================================================================
|
||
|
||
set -u
|
||
|
||
RELAY_DIR="${RELAY_DIR:-/opt/ykxmz-relay}"
|
||
PORT="${RELAY_PORT:-13245}"
|
||
# 公网健康检查地址。没有域名就填 IP。
|
||
PUBLIC_URL="${PUBLIC_URL:-http://13.54.30.196:$PORT}"
|
||
REPORTS_DIR="$RELAY_DIR/rescue_reports"
|
||
KNOWLEDGE="$RELAY_DIR/rescue_knowledge.md"
|
||
LOG="$RELAY_DIR/rescue.log"
|
||
LOCKFILE="/tmp/ykxmz-rescue.lock"
|
||
|
||
SETTLE_FAST=6
|
||
SETTLE_SLOW=15
|
||
|
||
# 是否允许"重启整机"这张底牌。默认关。
|
||
RESCUE_REBOOT="${RESCUE_REBOOT:-0}"
|
||
|
||
mkdir -p "$REPORTS_DIR"
|
||
|
||
if ! mkdir "$LOCKFILE" 2>/dev/null; then
|
||
echo "[rescue] 已经有一个预案在跑,本次退出"
|
||
exit 0
|
||
fi
|
||
trap 'rmdir "$LOCKFILE" 2>/dev/null' EXIT
|
||
|
||
STAMP="$(date +%Y%m%d-%H%M%S)"
|
||
REPORT="$REPORTS_DIR/$STAMP.txt"
|
||
|
||
say() {
|
||
echo "$*"
|
||
echo "$(date '+%F %T') $*" >> "$LOG"
|
||
}
|
||
|
||
# 公网健康检查 —— **看公网,不看本机**
|
||
is_healthy() {
|
||
local code
|
||
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 12 \
|
||
"$PUBLIC_URL/v1/health" 2>/dev/null)
|
||
[ "$code" = "200" ]
|
||
}
|
||
|
||
local_health() {
|
||
local code
|
||
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 6 \
|
||
"http://127.0.0.1:$PORT/v1/health" 2>/dev/null)
|
||
[ "$code" = "200" ]
|
||
}
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 现场记录
|
||
# ---------------------------------------------------------------------------
|
||
collect() {
|
||
{
|
||
echo "================================================================================"
|
||
echo " ykxmz 紧急预案诊断报告"
|
||
echo " 时间:$(date '+%F %T %Z')"
|
||
echo " 主机:$(hostname) 运行时长:$(uptime -p 2>/dev/null || uptime)"
|
||
echo " 触发原因:$1"
|
||
echo "================================================================================"
|
||
echo
|
||
echo "---- 一、结论速览 ----"
|
||
echo
|
||
echo " 公网 $PUBLIC_URL/v1/health -> $(is_healthy && echo 200-正常 || echo 不通)"
|
||
echo " 本机 http://127.0.0.1:$PORT/v1/health -> $(local_health && echo 200-正常 || echo 不通)"
|
||
echo
|
||
if local_health && ! is_healthy; then
|
||
echo " 判断:**本机是好的,问题在公网那一侧**(防火墙规则、安全组、上游线路)。"
|
||
elif ! local_health; then
|
||
echo " 判断:**本机的中转服务就没起来**,先看第二、三节。"
|
||
else
|
||
echo " 判断:两边都通(可能是刚才已经自愈,或者只是偶发抖动)。"
|
||
fi
|
||
echo
|
||
echo "---- 二、服务状态 ----"
|
||
echo
|
||
printf ' %-24s %s\n' "ykxmz-relay" "$(systemctl is-active ykxmz-relay 2>&1)"
|
||
printf ' %-24s %s\n' "ykxmz-monitor.timer" "$(systemctl is-active ykxmz-monitor.timer 2>&1)"
|
||
echo
|
||
echo " --- 最近 20 行服务日志 ---"
|
||
journalctl -u ykxmz-relay --no-pager -n 20 2>/dev/null | sed 's/^/ /'
|
||
echo
|
||
echo "---- 三、进程与端口 ----"
|
||
echo
|
||
echo " 监听 $PORT 的进程:"
|
||
ss -tlnp 2>/dev/null | grep ":$PORT " | sed 's/^/ /' \
|
||
|| echo " **没有任何进程在监听 $PORT**"
|
||
echo
|
||
echo " 相关进程:"
|
||
ps -eo pid,rss,etime,cmd --sort=-rss 2>/dev/null \
|
||
| grep -E 'relay\.py|python3' | grep -v grep | head -6 | sed 's/^/ /'
|
||
echo
|
||
echo "---- 四、系统资源 ----"
|
||
echo
|
||
free -m 2>/dev/null | sed 's/^/ /'
|
||
echo
|
||
uptime | sed 's/^/ /'
|
||
echo
|
||
df -h / /opt 2>/dev/null | sed 's/^/ /'
|
||
echo
|
||
echo "---- 五、网络 ----"
|
||
echo
|
||
ip -brief addr show 2>/dev/null | sed 's/^/ /'
|
||
echo
|
||
echo " 默认路由:"
|
||
ip route show default 2>/dev/null | sed 's/^/ /'
|
||
echo
|
||
echo " 本机自测各端口:"
|
||
for p in "$PORT" 80 443 22; do
|
||
printf ' %-6s ' "$p"
|
||
timeout 3 bash -c "</dev/tcp/127.0.0.1/$p" 2>/dev/null \
|
||
&& echo "通" || echo "不通"
|
||
done
|
||
echo
|
||
echo "---- 六、日志尾部 ----"
|
||
echo
|
||
echo "### 中转服务 relay.log"
|
||
tail -30 "$RELAY_DIR/relay.log" 2>/dev/null | sed 's/^/ /'
|
||
echo
|
||
echo "### 监测单元 monitor.log"
|
||
tail -20 "$RELAY_DIR/monitor.log" 2>/dev/null | sed 's/^/ /'
|
||
echo
|
||
echo "### 内核异常(OOM / 硬件)"
|
||
dmesg -T 2>/dev/null \
|
||
| grep -i -E 'oom|killed process|error|panic' | tail -15 | sed 's/^/ /'
|
||
echo
|
||
echo "---- 七、最近一次启动 ----"
|
||
echo
|
||
who -b 2>/dev/null | sed 's/^/ /'
|
||
echo " 运行时长:$(cut -d. -f1 /proc/uptime 2>/dev/null) 秒"
|
||
echo
|
||
echo "================================================================================"
|
||
echo " 报告结束。可以在中转站后台的「抢救报告」分区下载。"
|
||
echo "================================================================================"
|
||
} > "$REPORT" 2>&1
|
||
say " 现场已记录 -> $REPORT"
|
||
}
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 阶梯
|
||
# ---------------------------------------------------------------------------
|
||
|
||
step_1_restart_relay() {
|
||
say " [1/4] 重启中转服务(最轻的一招,不动系统其他部分)"
|
||
systemctl restart ykxmz-relay 2>/dev/null
|
||
sleep "$SETTLE_FAST"
|
||
}
|
||
|
||
step_2_kill_and_start() {
|
||
say " [2/4] 强杀残留进程再拉起(进程卡死/端口被占时用)"
|
||
# 有时候 systemd restart 会因为老进程没退干净而失败,
|
||
# 表现为"服务说 active 但端口没人听"。
|
||
pkill -f "$RELAY_DIR/relay.py" 2>/dev/null
|
||
sleep 3
|
||
systemctl reset-failed ykxmz-relay 2>/dev/null
|
||
systemctl start ykxmz-relay 2>/dev/null
|
||
sleep "$SETTLE_FAST"
|
||
}
|
||
|
||
step_3_check_firewall() {
|
||
say " [3/4] 检查本机防火墙有没有挡住 $PORT"
|
||
# Lightsail 有两层:网页上的防火墙规则(实例级)+ 系统内的 ufw/iptables。
|
||
# 网页那层我们改不了,但系统内这层能看。
|
||
if command -v ufw >/dev/null 2>&1; then
|
||
echo " ufw 状态:$(ufw status 2>/dev/null | head -1)" >> "$LOG"
|
||
ufw status 2>/dev/null | grep -q "$PORT" \
|
||
|| say " ⚠ ufw 里没有放行 $PORT 的规则"
|
||
fi
|
||
local n
|
||
n=$(iptables -L INPUT -n 2>/dev/null | grep -c "$PORT")
|
||
say " iptables INPUT 里提到 $PORT 的规则:$n 条"
|
||
# 重新绑一次端口(有时是监听 socket 坏了)
|
||
systemctl restart ykxmz-relay 2>/dev/null
|
||
sleep "$SETTLE_SLOW"
|
||
}
|
||
|
||
step_4_reboot() {
|
||
if [ "$RESCUE_REBOOT" != "1" ]; then
|
||
say " [4/4] 重启整机 —— **已跳过**(默认不开)"
|
||
say " 要允许自动重启,把 RESCUE_REBOOT=1 写进 ykxmz-monitor.service。"
|
||
return 1
|
||
fi
|
||
say " [4/4] 重启整机(最后的底牌)"
|
||
sync
|
||
systemctl reboot 2>/dev/null
|
||
return 0
|
||
}
|
||
|
||
remember() {
|
||
local step="$1" reason="$2"
|
||
{
|
||
echo
|
||
echo "## $(date '+%F %T') 第 $step 级奏效"
|
||
echo
|
||
echo "- 症状:$reason"
|
||
echo "- 有效步骤:$step"
|
||
echo "- 当时公网:$(is_healthy && echo 通 || echo 不通);本机:$(local_health && echo 通 || echo 不通)"
|
||
echo "- 详情见 rescue_reports/$(basename "$REPORT")"
|
||
} >> "$KNOWLEDGE"
|
||
say " 已把「第 $step 级奏效」记进 $KNOWLEDGE"
|
||
}
|
||
|
||
# ===========================================================================
|
||
# 主流程
|
||
# ===========================================================================
|
||
|
||
REASON="${1:-monitor 触发}"
|
||
|
||
say ""
|
||
say "================================================================================"
|
||
say " 紧急预案启动:$REASON"
|
||
say "================================================================================"
|
||
|
||
# ⚠ 先复查再记录现场。
|
||
# collect() 会写一份报告文件;如果网站其实已经好了,
|
||
# 那一次也会留下报告,把真正出事时的报告淹掉(后台只显示最近 30 份)。
|
||
if is_healthy; then
|
||
say " 复查:公网现在是通的,不需要动手 —— 不采现场、不留报告。"
|
||
exit 0
|
||
fi
|
||
|
||
collect "$REASON"
|
||
|
||
STEP_NAMES=("重启中转服务" "强杀残留再拉起" "查防火墙并重绑" "重启整机")
|
||
|
||
for i in 1 2 3 4; do
|
||
case $i in
|
||
1) step_1_restart_relay ;;
|
||
2) step_2_kill_and_start ;;
|
||
3) step_3_check_firewall ;;
|
||
4) step_4_reboot || continue ;;
|
||
esac
|
||
|
||
if is_healthy; then
|
||
say ""
|
||
say " ✅ 第 $i 级(${STEP_NAMES[$((i-1))]})救回来了,公网已恢复。"
|
||
remember "$i" "$REASON"
|
||
exit 0
|
||
fi
|
||
say " 第 $i 级没解决,继续下一级"
|
||
done
|
||
|
||
say ""
|
||
say " ❌ 四级都没解决。再采一次现场(状态已经变了)"
|
||
collect "$REASON(四级抢救后仍不通)"
|
||
|
||
{
|
||
echo "⚠ 这份报告是「自动抢救全部失败」之后生成的。"
|
||
echo " 机器自己试过了:重启中转 / 强杀重拉 / 查防火墙。"
|
||
echo " (重启整机默认没做。)"
|
||
echo " 需要人工看一眼 —— 特别是 Lightsail **网页上的防火墙规则**,"
|
||
echo " 那一层系统内部改不了。"
|
||
echo
|
||
cat "$REPORT"
|
||
} > "$REPORT.tmp" && mv "$REPORT.tmp" "$REPORT"
|
||
|
||
ls -1t "$REPORTS_DIR"/*.txt 2>/dev/null | tail -n +31 | xargs -r rm -f
|
||
|
||
say " 报告已生成:$REPORT"
|
||
exit 1
|