# -*- coding: utf-8 -*- r'''监测单元适配"80 会 301 到 https"之后的新情况。 == 问题 == certbot 签证书时加了 `--redirect`,于是 nginx 的 80 端口现在这样: listen 80; server_name yk.xxmxxs.top; return 301 https://$host$request_uri; 而监测单元原来的判断是"curl 拿到 200 才算健康"。curl **默认不跟跳转**, 于是它拿到 301 -> 判定失败 -> 连续 3 次就启动紧急预案 -> 预案重启中转服务 -> 再查还是 301 -> **又一个无限循环**。 这个坑和刚才那个"改监听地址忘了改探测地址"是同一类: **改了入口的行为,必须回头看所有探测入口的地方。** == 改法 == 把探测地址和判断都改成"看用户真实路径": PRIMARY https://yk.xxmxxs.top/v1/health (经 Cloudflare,用户实际走的) FALLBACK http://127.0.0.1:13245/v1/health (中转本身,绕开 CF 和 nginx) 判断规则也放宽一点: * curl 加 -L 跟跳转(301 本身不算故障) * 200 才算健康 * **如果 PRIMARY 挂着但 FALLBACK 好**,说明问题在 Cloudflare / DNS / nginx 那一层 —— 这时需要的是 "重启 nginx",而不是重启中转。所以预案里补一级。 == 为什么 PRIMARY 用 https 而不是 http == 因为 http 现在只是个跳转,真正的服务在 https 上。 用 https 探测,用户遇到什么我们就测到什么。 (443 端口还没在 Lightsail 防火墙开之前,PRIMARY 会失败、 FALLBACK 成功 —— 这时不会误判成"中转挂了",见上面的规则。) ''' from __future__ import annotations import sys from pathlib import Path ROOT = Path(__file__).resolve().parent.parent sys.path.insert(0, str(ROOT / "_build")) from lssh import run # noqa: E402 RELAY_DIR = "/opt/ykxmz-relay" NEW_HEAD = '''# ============================================================================ # 探测地址 # ============================================================================ # # PRIMARY:用户实际走的路(经 Cloudflare -> nginx -> 中转,全程 https) # FALLBACK:中转本身,绕开 Cloudflare 和 nginx # # ⚠ 两个都查是有意的: # * PRIMARY 挂 + FALLBACK 好 -> 问题在 CF / DNS / nginx 层 # * 两个都挂 -> 中转服务本身出问题了 # 分开判才能选对抢救动作(重启 nginx 还是重启中转)。 PRIMARY_URL="${PRIMARY_URL:-https://yk.xxmxxs.top}" FALLBACK_URL="${FALLBACK_URL:-http://127.0.0.1:13245}" ''' def main() -> int: print("=" * 62) print(" 监测单元适配 https + 301") print("=" * 62) print() print(" ── 1. 改探测地址那几行 ──") script = ( "python3 - <<'PYEOF'\n" "from pathlib import Path\n" "p = Path('%s/monitor.sh')\n" "lines = p.read_text(encoding='utf-8').splitlines()\n" "out = []\n" "done = False\n" "for l in lines:\n" " if l.startswith('PUBLIC_URL=') or l.startswith('FALLBACK_URL='):\n" " continue\n" " if l.strip() == '# 探测地址:域名优先(用户实际走的路),IP:80 兜底' and not done:\n" " out.append(%r)\n" " done = True\n" " continue\n" " out.append(l)\n" "p.write_text('\\n'.join(out) + '\\n', encoding='utf-8', newline='\\n')\n" "print(' 替换完成: %s' % done)\n" "PYEOF" % (RELAY_DIR, NEW_HEAD)) out = run(script, timeout=120, quiet=True) for ln in out[1].strip().splitlines(): print(" " + ln) print() print(" ── 2. 健康检查改成 -L + 分清两层 ──") script2 = ( "python3 - <<'PYEOF'\n" "from pathlib import Path\n" "p = Path('%s/monitor.sh')\n" "t = p.read_text(encoding='utf-8')\n" "\n" "old = '''PROBE=\"$PUBLIC_URL\"\n" "CODE=$(curl -s -o /dev/null -w '%%{http_code}' --max-time \"$CHECK_TIMEOUT\" \"$PROBE/v1/health\" 2>/dev/null)\n" "if [ \"$CODE\" != \"200\" ] && [ -n \"$FALLBACK_URL\" ]; then\n" " PROBE=\"$FALLBACK_URL\"\n" " CODE=$(curl -s -o /dev/null -w '%%{http_code}' --max-time \"$CHECK_TIMEOUT\" \"$PROBE/v1/health\" 2>/dev/null)\n" "fi'''\n" "\n" "new = '''# -L 跟跳转:nginx 那边 http 会 301 到 https,\n" "# 不跟跳转的话拿到 301 会被误判成故障(这坑踩过一次)。\n" "PUB_CODE=$(curl -sL -o /dev/null -w '%%{http_code}' --max-time \"$CHECK_TIMEOUT\" \"$PRIMARY_URL/v1/health\" 2>/dev/null)\n" "LOC_CODE=$(curl -s -o /dev/null -w '%%{http_code}' --max-time \"$CHECK_TIMEOUT\" \"$FALLBACK_URL/v1/health\" 2>/dev/null)\n" "CODE=\"$PUB_CODE\"\n" "LAYER=\"\"\n" "if [ \"$PUB_CODE\" != \"200\" ] && [ \"$LOC_CODE\" = \"200\" ]; then\n" " LAYER=\"edge\" # 中转是好的,问题在 CF / DNS / nginx 层\n" "elif [ \"$PUB_CODE\" != \"200\" ] && [ \"$LOC_CODE\" != \"200\" ]; then\n" " LAYER=\"core\" # 中转本身也不行\n" "fi'''\n" "\n" "if old in t:\n" " t = t.replace(old, new, 1)\n" " print(' ok 健康检查替换完成')\n" "else:\n" " print(' x 没匹配上,实际内容:')\n" " i = t.find('CODE=$(curl')\n" " print(t[max(0,i-200):i+400])\n" "p.write_text(t, encoding='utf-8', newline='\\n')\n" "PYEOF" % RELAY_DIR) out = run(script2, timeout=120, quiet=True) for ln in out[1].strip().splitlines(): print(" " + ln) print() print(" ── 3. 语法检查 + 跑一次 ──") out = run("bash -n %s/monitor.sh && echo ' 语法 OK'; " "printf 'fails=0\\nlast_rescue=0\\n' > " "%s/monitor_state.txt; " "bash %s/monitor.sh; echo ' --- 状态 ---'; " "cat %s/monitor_state.txt; echo ' --- 日志尾 ---'; " "grep -v 'Permission denied' %s/monitor.log | tail -4" % (RELAY_DIR, RELAY_DIR, RELAY_DIR, RELAY_DIR, RELAY_DIR), timeout=180, quiet=True) for ln in out[1].strip().splitlines(): print(" " + ln) print() print(" ── 4. 现在探测的各层 ──") out = run( 'printf " %%-34s %%s\\n" "PRIMARY https://域名" ' '"$(curl -sL -o /dev/null -w \'%{http_code}\' --max-time 15 ' 'https://yk.xxmxxs.top/v1/health)"; ' 'printf " %%-34s %%s\\n" "FALLBACK 127.0.0.1:13245" ' '"$(curl -s -o /dev/null -w \'%{http_code}\' --max-time 8 ' 'http://127.0.0.1:13245/v1/health)"; ' 'printf " %%-34s %%s\\n" "nginx 本机:80(会 301)" ' '"$(curl -s -o /dev/null -w \'%{http_code}\' --max-time 8 ' 'http://127.0.0.1:80/v1/health)"; ' 'printf " %%-34s %%s\\n" "nginx 本机:443" ' '"$(curl -sk -o /dev/null -w \'%{http_code}\' --max-time 8 ' '--resolve yk.xxmxxs.top:443:127.0.0.1 ' 'https://yk.xxmxxs.top/v1/health)"', timeout=180, quiet=True) for ln in out[1].strip().splitlines(): print(ln) return 0 if __name__ == "__main__": sys.exit(main())