惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

C
CXSECURITY Database RSS Feed - CXSecurity.com
K
Kaspersky official blog
A
Arctic Wolf
Attack and Defense Labs
Attack and Defense Labs
L
LINUX DO - 热门话题
N
News | PayPal Newsroom
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
L
Lohrmann on Cybersecurity
PCI Perspectives
PCI Perspectives
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
The Last Watchdog
The Last Watchdog
B
Blog RSS Feed
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
W
WeLiveSecurity
Know Your Adversary
Know Your Adversary
博客园 - Franky
T
Tenable Blog
T
Tailwind CSS Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Help Net Security
Help Net Security
WordPress大学
WordPress大学
T
The Exploit Database - CXSecurity.com
www.infosecurity-magazine.com
www.infosecurity-magazine.com
博客园 - 司徒正美
阮一峰的网络日志
阮一峰的网络日志
D
Darknet – Hacking Tools, Hacker News & Cyber Security
H
Heimdal Security Blog
TaoSecurity Blog
TaoSecurity Blog
S
Security Affairs
J
Java Code Geeks
小众软件
小众软件
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Apple Machine Learning Research
Apple Machine Learning Research
NISL@THU
NISL@THU
O
OpenAI News
The Cloudflare Blog
月光博客
月光博客
Google Online Security Blog
Google Online Security Blog
V
V2EX
罗磊的独立博客
美团技术团队
博客园 - 三生石上(FineUI控件)
Security Latest
Security Latest
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
C
Cyber Attacks, Cyber Crime and Cyber Security
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Cyberwarzone
Cyberwarzone
L
LINUX DO - 最新话题
Hacker News - Newest:
Hacker News - Newest: "LLM"
大猫的无限游戏
大猫的无限游戏

蛋蛋之家

SUMIFS和LET函数——让 Excel 自己动起来 我用这个开源项目,把 AI 绘画提示词变成了"代码"——awesome-gpt-image-2 深度体验报告 群晖部署 LX Music Server:浏览器就是你的音乐播放器【详细版】 500道Word题库太痛苦?于是我写了个智能学习平台 别用一个人的奔跑,耗尽两个人的余生——《你凭什么认为我会一直喜欢你呢》 博客评论自动填充新方案:兼容 Vue/React 响应式绑定与跨架构填充逻辑 2026 生产级指南:Halo 2.x + PostgreSQL 自动化部署与性能优化手册 从 Typecho 转向 Halo 2.x:全能型 Nginx 架构与 SEO 无损迁移指南 Typecho 迁移 Halo 完整教程:数据库视图解决表前缀 + 自动提取封面图 + 去重检测 我的 CodeBuddy 装备库:218个技能全公开 + 使用心得 为博客添加自定义评论表情 Comment2Bark:Typecho评论推送到Bark插件 又拍云插件 UpyunFile v1.2 更新:修复 Typecho 1.3.0 兼容性问题 手把手修复RSS 403:从PHP警告到Cloudflare拦截的全链路排查 网站被镜像怎么办?2026年反镜像攻防实战指南 VLESS + Reality + Vision:2026 极致隐蔽节点搭建全解析 给OpenList和Alist加上液态玻璃效果,颜值直接拉满! 网页截图瞬间“赛博飞升”?手把手教你根治 Windows HDR 截图发白 群晖 NAS 影音终极方案:Docker Rclone + OpenList 完美挂载 Plex 全攻略 一个简洁高效的SVG图标库 告别重复劳动!一键填充评论的神奇书签脚本 - 蛋蛋之家 解锁思源笔记的隐藏力量:siyuan-patch 深度体验与技术实现 Oracle Cloud 自救指南:旧手机丢失后如何绕过 MFA 重新登录【更新2026年最新政策】 本站已加入“萌国ICP备案”联萌!😄 Excel 月报自动化指南:告别重复劳动,让数据主动为你服务 梦呓:为异地女友准备的2周年惊喜礼物 Typecho 多吉云插件优化:更灵活的存储目录配置 甲骨文云硬盘扩容指南:从50G到200G的“免费午餐”怎么吃? ⏰ 时间同步:当你的服务器开始“穿越”时,如何优雅地把它拉回现实 Typecho 又拍云文件管理插件修改版使用指南:新增自定义目录结构功能 华为鸿蒙6.0回退4.2记录 最近小公主情绪不佳 劳动节的家乡景色 烟台山公园一游 群晖搭建ChatGPT Web 推荐脚本:ChatGPT - 提示选择器 Activation Method for JetBrains Products (English version) JetBrains 系列产品激活方法 疫情三年后再进校园 修复searchEngineJump在谷歌搜索页面不生效问题 事情既然发生了,可以是遗憾,不要去后悔。 侄女的试唱会比赛 新一代图片编码格式测试 省科技馆之行 Typora Windows版破解补丁及主题推荐 在Typora中使用PicList上传图片 域名ICP备案之变更网站 Ubuntu 系统如何使用 root 用户登录 中国联通——让一切自由连不通 如何选购适合的口罩? 今天移动营业厅遇到一个小姐姐,真的好美 它不乐意 如何解决Typecho文章不是Markdown语法创建问题 探手抓住这一天的尾巴,为自己庆生 收到腾讯云自媒体分享计划2月份礼品 尝试百度Spider抓取异常问题 甲骨文DD重装系统及失联救砖教程 终于MrDoc升级v0.8.7成功了 Windows 端口查询及杀死进程 甲骨文实例开通与X-UI安装 国艺花鸟市场的悠闲 在思源笔记中使用群晖Synology C2对象存储服务 MrDoc升级v0.8.7失败 居居的手工作品 Linux系统时间同步命令小结 AList安装指南 宝塔面板降级 v7.7.0 及解除强制绑定手机、免费安装专业版插件的方法 修复群晖Synology Drive client右键菜单缺失问题 Windows11 展开折叠的右键菜单为传统模式 在 Windows 下批量将 Markdown 文档转为 Word 文档 Typecho的一些修改记录 Markdown语法——Mermaid 有两种新方案—CloudFlare官方免费CNAME接入教程 居居的新冠历程 群辉安装Transmission套件、增强汉化WebUI并备份做种列表 群晖 docker 部署思源笔记 Office for Mac 2019 License Installer 公众号通过api发送小程序消息 Windows 10下 iTunes 备份默认存储路径修改 Excel查找返回多个值,并将返回的值放在同一个单元格里面 七牛云测试域名失效后如何下载空间中的文件 入手三星860 evo 500g SSD固态硬盘 分享一套耐看的鼠标指针 如何重置OmniFocus数据库 Office for Mac 2016 License Installer Mac下配置终端+iTerm 2主题 如何制作苹果系统安装U盘 LNMP部署typecho,开启SSL、强制开启https Stylish样式如何设置为特定网址不生效? Typecho插件错误号:42S01的解决方法 电动车被偷了 热火 R150 上手开箱 UltraMon 3.3.0 汉化+注册机 如何用 MWeb 发布 Typecho 文章 测试 Mweb 发布到印象笔记功能 忘记了iPhone访问限制的密码该怎么办? 新玩具KAREE电子烟,先试试 Mac mini 2011 开启Handoff方法 见微知著 浅谈手游创新 PhoneClean Pro授权注册激活码
Halo 博客晚间卡顿排查记:从"玄学卡顿"到"真凶落网"
吴蛋蛋 · 2026-05-22 · via 蛋蛋之家

服务器:Oracle Cloud ARM | 应用:Halo 2.x + PostgreSQL 15 + Nginx | 记录时间:2026.05.21–22


挺玄学的头图,阴间版
挺玄学的头图,阴间版

一、问题的诡异之处

我的博客(wuqishi.com)有个很邪门的毛病:每天晚上 20:00 到 23:00 准时卡,但同服务器上的其他网站屁事没有

服务器是 Oracle Cloud 的 ARM 实例,4 核 24G 内存,跑 Docker Compose。上面除了 Halo,还有几个 Typecho 站点和纯静态页面。白天所有站点都飞快,唯独 Halo 一到晚上就跟中了邪似的——页面加载转圈、后台进不去、偶尔直接报错 ERR_CONNECTION_CLOSED

更离谱的是,不是一直卡,是断断续续的卡。有时候能打开,点两下又卡死;有时候彻底连不上,过几分钟又好了。这种 "薛定谔的卡顿" 最让人头疼,因为你根本抓不到规律。

一开始我怀疑是 Halo 2.x 基于 Java(Spring Boot)的锅,毕竟 Java 吃内存是出了名的,而 Typecho 是 PHP,随用随释放。但看了眼 Docker 配置,JVM 堆内存已经给了 5G,G1 垃圾回收器也开了,理论上不应该啊。

💡 Tip 1:遇到 "选择性卡顿",先别急着调代码或加配置。

第一步永远是确认问题范围。同服务器其他站点正常 → 排除全局资源问题;只有某个应用卡 → 问题大概率在这个应用自身、它的数据库、或者这个域名独享的流量上。


二、排查的第一步:翻日志,逐层往下挖

2.1 Nginx error.log 的异常

先看 Nginx 的 error.log,发现大量这种报错:

2026/05/21 22:14:57 [error] 1234#1234: *45678 recv() failed (104: Connection reset by peer)
2026/05/21 22:14:57 [error] 1234#1234: *45678 upstream prematurely closed connection

而且全部指向同一个路径:

/apis/online-user.zyx2012.cn/v1alpha1/online-ws

这是我在 Halo 里装的一个 "在线人数统计" 插件。看起来 Nginx 把请求转发给 Halo 后,Halo 还没返回响应头就直接把连接掐断了。更可怕的是,这些报错是刷屏级别的,一秒钟能蹦出来十几条。

2.2 dmesg 里的两个关键线索

同时我看了 dmesg,发现两个现象:

第一,Docker 容器在频繁重启。

vetha1b2c3d4: entered disabled state
vetha1b2c3d4: left promiscuous mode
docker0: port 2(vetha1b2c3d4) entered disabled state

虚拟网卡在不断销毁重建。这意味着 Halo 容器在崩溃 → 被 Docker 自动拉起 → 又崩溃的循环里。

第二,UFW 防火墙在疯狂拦截扫描。

[UFW BLOCK] IN=eth0 OUT= MAC=... SRC=185.191.171.12 DST=... DPT=5432
[UFW BLOCK] IN=eth0 OUT= MAC=... SRC=194.165.16.73 DST=... DPT=5432

满屏的 [UFW BLOCK]​,各种境外 IP 在扫我的端口:5432(PostgreSQL)、5985(WinRM)、3389(远程桌面)……

等等,5432?我的数据库端口怎么暴露在外网了

💡 Tip 2:dmesg 是排查系统级问题的利器。

很多人只看应用日志,忽略了内核日志。Docker 容器频繁重启、网卡状态变化、OOM killer 杀进程,这些在 dmesg​ 里一目了然。建议用 dmesg -T | grep -E "(UFW|veth|docker0)"​ 快速过滤关键信息。


三、真凶一:数据库裸奔在公网上

我打开 docker-compose.yml 一看,傻眼了:

services:
  halodb:
    image: postgres:15-alpine
    ports:
      - "5432:5432"

当时为了方便用 DBeaver 连数据库,直接映射到了宿主机。但我忘了 Linux 上一个巨坑:Docker 会绕过 UFW 防火墙

3.1 Docker 为什么能绕过 UFW?

这里有个很多人不知道的细节:

UFW 本质上是对 iptables 的封装,它在 INPUT 链里插入规则。但 Docker 在启动容器时,会在 iptables​DOCKER 链(属于 nat 表和 filter 表)里插入自己的规则,优先级高于 UFW 的 INPUT

具体来说,Docker 会创建这样的 DNAT 规则:

sudo iptables -t nat -L DOCKER -n --line-numbers | grep 5432

输出:

DNAT       tcp  --  0.0.0.0/0            0.0.0.0/0            tcp dpt:5432 to:172.18.0.2:5432

这条规则的意思是:所有访问宿主机 5432 端口的流量,都会被 DNAT 到 Docker 容器内部的 172.18.0.2:5432。它发生在 UFW 的过滤规则之前,所以 UFW 根本拦不住。

3.2 数据库日志里的暴力破解

看数据库日志更触目惊心:

2026-05-21 22:14:57.123 UTC [12345] FATAL: password authentication failed for user "postgres"
2026-05-21 22:14:57.123 UTC [12345] DETAIL: Role "postgres" does not exist.
2026-05-21 22:14:57.456 UTC [12346] FATAL: password authentication failed for user "postgres"
2026-05-21 22:14:57.789 UTC [12347] FATAL: password authentication failed for user "postgres"

黑客在用默认用户名 postgres 暴力破解我的数据库。因为我在 Docker Compose 里把数据库用户名改成了其他,系统压根没有 postgres 这个用户,所以每次破解都是 FATAL 级别报错。

为什么晚上特别卡?

因为晚间是全球扫描器的高峰期(对应欧美白天工作时间),每秒几十次恶意连接。PostgreSQL 的默认配置下,每次连接都要:

  1. Fork 一个后端进程(约 5-10MB 内存)

  2. 进行 SSL 握手和认证计算

  3. 查询系统表验证用户名

  4. 失败后记录 FATAL 日志

这些垃圾请求把 CPU 和 I/O 占满了。我的 Halo 博客想查数据库?排队去吧。

3.3 修复方案

把端口绑定到本地回环:

services:
  halodb:
    image: postgres:15-alpine
    ports:
      - "127.0.0.1:5432:5432"
    environment:
      - POSTGRES_USER=halo
      - POSTGRES_PASSWORD=your_strong_password
      - POSTGRES_DB=halo

改完重启,数据库日志瞬间清净,FATAL 报错彻底消失。

💡 Tip 3:所有敏感服务的 Docker 端口映射,永远写成 127.0.0.1:PORT:PORT

不要偷懒只写 PORT:PORT,那等于裸奔在公网上。UFW 在 Docker 面前就是纸老虎。如果你确实需要从外网连数据库,用 SSH 隧道或 WireGuard,而不是直接暴露端口。


四、真凶二:Nginx 反代没开长连接

数据库安静了,但网站还是偶尔卡顿。继续看日志,发现 Nginx 的 access.log 里有个规律:每次页面加载,除了主请求,还有一堆 /apis/...​ 的 API 请求。这些请求全部走的是:

proxy_pass http://127.0.0.1:8090;

也就是直连 Halo,没有走 upstream。

问题出在这里:我没配 upstream keepalive

4.1 为什么短连接会拖垮你?

Nginx 每次反向代理都新建一个 TCP 连接,用完就扔。晚上并发一高,系统里堆积成千上万个 TIME_WAIT 状态的连接。

你可以用 ss 命令验证:

ss -tan state time-wait | wc -l

我当时高峰期这个数字飙到了 8000+

TIME_WAIT 是 TCP 四次挥手的正常状态(主动关闭方等待 2MSL,确保最后的 ACK 被对方收到),但数量太多会:

  • 耗尽临时端口:Linux 默认临时端口范围是 32768–60999,约 28000 个。8000+ TIME_WAIT 意味着可用端口骤减,新的请求连不上,表现为 "转圈"、"白屏"。

  • 占用内存:每个 TIME_WAIT 连接在内核里占用约 1–2KB 的 TCP 控制块,积少成多。

4.2 修复方案

在 Nginx 配置顶部定义 upstream:

upstream halo_backend {
    server 127.0.0.1:8090;
    keepalive 64;
}

然后把所有 proxy_pass http://127.0.0.1:8090 改成 proxy_pass http://halo_backend,并在对应的 location 里加上:

proxy_http_version 1.1;
proxy_set_header Connection "";

这样 Nginx 和 Halo 之间保持 64 个长连接复用,再也不用频繁三次握手了。

注意两个细节:

  1. proxy_http_version 1.1 是必须的,因为 HTTP/1.0 默认不支持 keepalive。

  2. proxy_set_header Connection ""​ 是为了防止 Nginx 把客户端的 Connection: close 透传给后端,导致后端主动断开长连接。

改完后可以立刻用 ss 验证:

ss -tan state established '( dport = :8090 or sport = :8090 )' | wc -l

连接数从之前的几百个飙升到稳定的 64 个左右,而且不再增长。TIME_WAIT 数量也降到了正常水平(几十到一百个)。

💡 Tip 4:用 ss 替代 netstat,它更快更准。

查看某个端口的连接状态分布:

ss -tan state time-wait '( dport = :8090 )' | wc -l
ss -tan state established '( dport = :8090 )' | wc -l

如果 TIME_WAIT 超过 5000,就要警惕了。


五、真凶三:WebSocket 插件的重连风暴

现在说回那个 "在线人数统计" 插件。它的原理是前端通过 WebSocket 长连接实时上报在线状态。

5.1 WebSocket 握手为什么失败?

WebSocket 不是普通的 HTTP 请求。它的建立过程是这样的:

  1. 客户端发送一个带 Upgrade: websocket​Connection: Upgrade 头的 HTTP 请求

  2. 服务器返回 101 Switching Protocols​,之后连接从 HTTP 升级为 WebSocket 全双工通道

  3. 后续数据通过帧(frame)传输,不再走 HTTP 请求 / 响应模型

问题出在第 2 步:我的 Nginx 没正确转发 Upgrade

Nginx 默认会把 Upgrade 这类非标准头过滤掉(或者当成普通 HTTP 请求处理),导致后端 Halo 收到的是一个普通的 GET 请求,而不是 WebSocket 握手请求。后端以为这只是一个普通的 API 调用,处理完就关闭了连接。

但前端 JS 检测到 WebSocket 断开,会自动重连,于是形成了"连接 → 断开 → 重连" 的死循环

5.2 日志里的套娃现场

access.log​ 里能清楚看到这个套娃过程:

22:14:57 "GET /apis/online-user.../online-ws HTTP/1.1" 101 197
22:14:59 "GET /apis/online-user.../online-ws HTTP/1.1" 101 169  ← 同一个 IP,2 秒后重连
22:16:04 "GET /apis/online-user.../online-ws HTTP/1.1" 101 0    ← 传输 0 字节,空连接
22:16:48 "GET /apis/online-user.../online-ws HTTP/1.1" 101 143
22:16:56 "GET /apis/online-user.../online-ws HTTP/1.1" 101 143  ← 8 秒后又重连

一个访客就能在短时间内发起 3–4 次 WebSocket 握手。晚上如果有几十个访客,加上各种爬虫,Halo 的线程池被这些无效握手占满,正常的页面请求只能排队,等到超时。

Nginx error.log​ 里的 Connection reset by peerprematurely closed connection 就是这么来的——Halo 被压垮了,直接粗暴地关闭连接自保。

5.3 修复方案

我没有卸载这个插件,因为上面加了长链接后目前暂时没问题,这里做记录:可以在 Nginx 里正确支持 WebSocket。在对应的 location 里加上:

location /apis/online-user.zyx2012.cn/ {
    proxy_pass http://halo_backend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_read_timeout 60s;
    proxy_send_timeout 60s;
}

验证:

curl -i -N   -H "Connection: Upgrade"   -H "Upgrade: websocket"   -H "Host: wuqishi.com"   -H "Origin: https://wuqishi.com"   https://wuqishi.com/apis/online-user.zyx2012.cn/v1alpha1/online-ws

返回:

HTTP/2 101 Switching Protocols
upgrade: websocket
connection: upgrade

101 Switching Protocols 就说明握手成功,长连接已建立。浏览器 F12 的 Network 面板里也能看到 WebSocket 连接状态从 (pending) 变成 101,之后变成绿色小圆点,不再频繁断开重连。

目前现在这个插件运行得很稳定,在线人数统计功能完全 正常,没有再出现之前的重连风暴。

💡 Tip 5:排查 WebSocket 问题时,不要只看浏览器控制台的报错。

用 curl 直接模拟握手请求是最快的方式。如果返回 200 OK​ 而不是 101,说明 Nginx 或后端没正确处理 Upgrade 头。另外注意,HTTP/2 对 WebSocket 的支持比较特殊,Nginx 从 1.25.1 开始才支持 HTTP/2 下的 WebSocket,如果你的 Nginx 版本较老,建议用 HTTP/1.1 测试。


六、第四个隐患:恶意爬虫的骚扰

排查过程中,Nginx access.log 里还有一类请求让我哭笑不得:

185.191.171.12 "GET /wp-content/themes/begin/inc/go.php?url=..." 404
85.208.96.196 "GET /wp-content/themes/begin/inc/go.php?url=..." 404

SemrushBot(一个臭名昭著的 SEO 爬虫)在疯狂请求我网站上根本不存在的 WordPress 路径,我早转Typecho了,现在转到Halo了。它以为我还是是 WordPress 站点,在探测 Begin 主题的重定向漏洞(历史漏洞),想拿我的服务器当跳板做黑帽 SEO。

这些请求全部返回 404,但问题是:它们都进了 Halo 后端

Nginx 匹配不到物理文件,就丢给 @halo_proxy​,Halo 不得不为每个 404 请求初始化一次完整的页面渲染,吐出 404 页面。一秒钟十几条,线程池又被浪费了。

6.1 处理方案

我没有在 Nginx 层面拦截这些爬虫,而是选择在 Cloudflare 处理。Cloudflare 的防火墙规则可以基于 User-Agent 直接拦截 SemrushBot、DotBot 等垃圾爬虫,不需要消耗我服务器的任何资源。

这样做的好处是:

  • 垃圾流量在边缘节点就被挡掉了,根本到不了我的服务器

  • 不需要维护 Nginx 里的爬虫黑名单,Cloudflare 的威胁情报库会自动更新

  • 不会污染我的 Nginx 日志,access.log 里只保留真实访客

💡 Tip 6:如果用了 Cloudflare,优先在边缘节点拦截垃圾流量。

Cloudflare端自定义规则
Cloudflare端自定义规则

表达式代码:

(http.request.uri.path contains "/.env") or 
(http.request.uri.path contains "/.git") or 
(http.request.uri.path contains "/wp-") or 
(http.request.uri.path contains "/xmlrpc.php")

Cloudflare 的防火墙规则、Bot Fight Mode、User-Agent 拦截,都比在源站 Nginx 处理更高效。源站的资源应该留给真实用户,而不是浪费在响应爬虫上。如果你的站不是 WordPress,可以直接在 Cloudflare 里拦截所有包含 wp-contentwp-admin 的请求,连 404 都不用返回。


七、其他杂项与待办

/rss.xml 每次被请求都要动态生成 115KB 的 XML,而且多个 RSS 聚合器(FreshRSS、Inoreader、AstraHub 等)每隔几分钟就来抓一次。目前暂时没做静态缓存,先放着。

💡 Tip 7:RSS 聚合器的抓取间隔各不相同。

有的 5 分钟一次,有的 30 分钟一次。你可以通过 access.log 分析 User-Agent,找出最频繁的聚合器,针对性地设置缓存时间。另外,Halo 的 RSS 生成是全量文章列表,如果文章很多(几百篇),每次生成都是 O(n) 的遍历,非常耗 CPU。建议考虑分页 RSS 或限制输出数量。

7.2 缩略图代理的 302 风暴

日志里发现大量这种请求:

GET /apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https://cdn.ssslove.com/...&size=m HTTP/2.0" 302 0

Halo 的缩略图服务对外链图片做了一次 302 重定向。如果页面里图片多,这一来一回多了很多无谓的请求。后续考虑直接把图片链接换成 CDN 加参数的直链,跳过 Halo 的代理层。

💡 Tip 8:Halo 的缩略图代理适合处理本地上传的图片。

如果是外链图片(比如又拍云、OSS),建议直接在前端用 CDN 的缩略图参数(如 !m?x-oss-process=image/resize​),跳过 Halo 的代理层,减少一次 302 跳转。

7.3 HALO_EXTERNAL_URL 的坑

Docker Compose 里 HALO_EXTERNAL_URL 配成了 http://localhost:8090/,这会导致某些插件在生成回调地址、RSS 链接时用到错误的域名。

应该改成真实域名:

environment:
  - HALO_EXTERNAL_URL=https://wuqishi.com/

💡 Tip 9:HALO_EXTERNAL_URL 影响的不只是页面链接。

它还会影响:

  • Open Graph 标签(分享到微信、Twitter 时的预览卡片)

  • RSS 里的 <link> 和 [atom:link](atom:link) 元素

  • 某些插件的 OAuth 回调地址

  • 邮件通知里的链接

配错了会导致分享出去的文章链接是 http://localhost:8090/...,别人根本打不开。目前我暂未修改。


八、排查思路总结:我的一套方法论

这次排查我走了一些弯路,但也验证了一套有效的方法论:

排查顺序

做了什么

发现了什么

关键命令 / 工具

1. 确认范围

对比同服务器其他站点

只有 Halo 卡,排除全局资源问题

htop, free -h, df -h

2. 看 Nginx error.log

定位前后端断开原因

大量 Connection reset by peer​

​tail -f /var/log/nginx/error.log

3. 看 dmesg

检查系统级异常

Docker 频繁重启 + UFW 大量拦截

​dmesg -T | grep -E "(UFW|veth|docker0)"​

4. 看数据库日志

检查 DB 压力

公网暴力破解,FATAL 刷屏

​docker logs halodb​

5. 看 Nginx access.log

分析请求分布

恶意爬虫 + WebSocket 高频重连

awk '{print $6, $7}' access.log | sort | uniq -c | sort -rn​

6. 浏览器 F12

区分前端 / 后端慢

TTFB 高,确认后端响应慢

Chrome DevTools → Network → Timing

7. 检查连接状态

确认 TCP 资源耗尽

TIME_WAIT 8000+

​ss -tan state time-wait | wc -l​

8. 模拟 WebSocket 握手

验证 Upgrade 头转发

返回 200 而非 101

curl -H "Upgrade: websocket" ...​

核心原则:由外到内,先拦垃圾流量,再修内部配置。

如果一开始就去调 JVM 参数、加数据库索引,可能永远找不到真凶。因为根子不在性能不足,而在资源被垃圾请求白白浪费


九、现在的状态

改完配置、重启服务后,到今天盯着日志看了半小时:

  • 数据库日志:一片安静,偶尔正常的查询交互

  • Nginx error.log:零报错

  • Nginx access.log:清净了很多,Cloudflare 已经挡掉了大部分垃圾爬虫

  • WebSocket 插件:握手返回 101 Switching Protocols,长连接稳定,不再频繁重连,在线人数统计功能正常工作

  • 页面加载:iPad、Mac、手机 4G 测试,正常打开。

晚上 22:30 到 23:00 这段原本最卡的时间,现在流畅得跟白天一样。(今晚继续再看看


十、反思:六个容易被忽视的坑

坑 1:Docker 端口映射默认暴露公网,UFW 拦不住

数据库、Redis、MQ 这类敏感服务一定要绑 127.0.0.1。别信 UFW,信 iptables -t nat -L DOCKER

坑 2:Nginx 反代理默认是短连接

高并发场景必须开 keepalive,否则 TIME_WAIT 会拖垮你。ss -tan state time-wait | wc -l​ 超过 5000 就要警惕了。

坑 3:WebSocket 不是"配了就行"

Upgrade​Connection 头缺一不可。可以用 curl​ 模拟握手快速验证,返回 101 才算成功。另外注意 Nginx 版本,HTTP/2 下的 WebSocket 需要 Nginx ≥ 1.25.1

坑 4:第三方插件要审慎,但也不必一刀切

这次在线人数插件的问题根源不在插件本身,而在 Nginx 配置缺失。修复后插件运行得很稳定,功能完全正常。所以遇到插件相关的问题,先排查基础设施(Nginx、反向代理、防火墙),不要急着卸载。

坑 5:日志是最好的 debugger

这次排查全程没改一行 Java 代码,所有问题都是从日志里抠出来的。养成看日志的习惯,比盲目调参重要得多。

建议给 Nginx 日志加上这两个字段,一眼就能看出是前端慢还是后端慢:

log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                '$status $body_bytes_sent "$http_referer" '
                '"$http_user_agent" '
                'rt=$request_time urt=$upstream_response_time';
  • ​rt(request_time):客户端视角的总耗时

  • ​urt​(upstream_response_time):后端处理耗时

如果 rt 很大但 urt 很小 → 问题在网络或 Nginx;如果 urt 很大 → 问题在后端。

坑 6:不要忽视爬虫的影响

SemrushBot、DotBot 这些商业爬虫非常 aggressive,它们不会遵守你的 robots.txt,而且专门挑 WordPress 漏洞路径扫。如果你的站不是 WP,在 Cloudflare 或 Nginx 层直接拦截,别浪费后端资源。


如果你也在用 Halo 或者类似的 Java 博客系统遇到晚间卡顿,希望这篇记录能给你一些排查思路。有时候问题不在代码多复杂,而在某个配置细节、某个插件、或者某个暴露在公网上的端口。

最后吐槽一句:SemrushBot 是真的执着,从 21:00 到 23:00 不间断地扫了整整两个小时,被 Cloudflare 挡了上百次还在试。这种敬业精神,用在正道上早成架构师了。


写于 2026 年 5 月 22 日凌晨,部分内容资料由 AI 查询后进行整理而成。