Files
ykxmz/relay/rescue-lightsail.sh
T
xmz7891 5fc2f97be7 2.1.3:搬到 Lightsail 真服务器 + 支持"没有证书的中转"
用户买了 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 个阶段全部通过。
2026-10-08 22:29:56 +08:00

286 lines
10 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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