一、这一篇讲什么:收官
热榜聚合站系列到这里收口。第一篇讲选型:dailyhot-api 主力、RSSHub 补位、redis 中转、Next.js 聚合渲染、SearXNG 做可选搜索件,五容器跑在阿里云 2C1.8G 单机上;第二篇讲搭建与踩坑:compose 编排、nginx 反代、五个真实坑位。这一篇是季度运行复盘——跑了一个季度之后,哪些组件稳、哪些折腾、出过什么事、花了多少钱、重来一遍会怎么改、以及这个站接下来怎么办。
先把口径交代死,免得后面所有内容被误读:这个站是自托管的自用信息面板,从头到尾没装任何统计、没有公网流量数据。所以本文没有 PV/UV、没有”增长”、没有任何运营叙事——不是谦虚,是没这个数据,硬编一个就是吹牛。复盘的三个坐标系是:作为工具好不好用、作为一套栈稳不稳、作为一笔开销贵不贵。性能与资源占用照旧全定性,一个没测过的数字都不给。
二、运行画像:哪些组件稳,哪些折腾
一个季度下来,五个容器的”性格”分化得非常清楚:
| 组件 | 定性表现 | 折腾度 |
|---|---|---|
| dailyhot-api | 五个里最省心的:常驻内存低且平稳,从没因自身原因重启过 | 几乎为零 |
| redis | 稳定,但经历过一次策略调整(见第三节),是”需要动脑”的稳定 | 低,一次性 |
| app(Next.js) | 稳定;内存常驻在限额预算内,无泄漏迹象(docker stats 巡检口径) | 低 |
| RSSHub | 功能正常,但路由可用性跟着上游平台改版走,一个季度内需要照看数次(见第三节) | 中,持续性的 |
| SearXNG | 功能正常,但偶发 CPU 上浮,是全栈资源观感最差的一块(见第三节) | 中偏高 |
这张表背后有个清晰的分层:数据链路上的组件(抓取、缓存、展示)全都稳,麻烦全出在”附加价值”组件上。RSSHub 的折腾是外部性的——它没坏过,是它盯着的那些平台在变;SearXNG 的折腾是结构性的——搜索天然是并发行为,小机器天然不喜欢并发。资源占用的整体画像:全栈常驻内存没有突破过各容器限额(巡检口径的定性结论),机器整体没有出现过因这套栈导致的内存压力告警——”人人有限额”这条第一篇立下的纪律,一个季度下来没有还债。
巡检的惯例也值得留档,它便宜到没有理由不做:docker stats --no-stream 扫一眼各容器常驻内存与 CPU、页面上抽查两张卡片的更新时间戳、redis-cli INFO stats 看驱逐计数是否还在涨。三件事一分钟,每周一次——本季度的三起事件里有两起(SearXNG 尖峰、redis 驱逐频繁)就是这套巡检先发现的,而不是页面先坏的。自用系统没有告警系统,巡检就是告警系统。
三、故障与处理记录:三件事
事件一:RSSHub 路由失效(外部性故障)
现象:某平台热榜卡片突然只剩旧数据,时间戳停更。定位:dailyhot 同平台的卡片是新鲜的,说明不是网络或 redis 的问题;进 RSSHub 容器直接请求对应路由,返回空——上游平台改了页面结构,该路由的解析失效,等社区修复。处理分三层:一是该源的卡片靠 redis 里旧数据挂着,标”更新于”时间,页面不黑;二是 dailyhot 有同平台源的,临时把卡片切到 dailyhot 数据;三是社区新版本发布后 docker compose pull rsshub 升级,路由恢复。教训:这是选型时就该预期到的成本——用社区路由库,就是把自己的可用性外包给社区响应速度;第一篇选 dailyhot 当主力、RSSHub 只补位的架构,在这次事件里正好体现了价值:主力源稳定,补位源坏了不致命。反模式一:路由一失效就自己动手改 RSSHub 的路由源码顶上。救一次火没问题,救三次就是接盘——把”社区维护、我白用”的成本模型,悄悄改写成”上游每改一次版我加班一次”,业余维护者扛不住这个节奏,最终下场是整个源烂掉。正确姿势:切兜底数据源、等社区、升级,三步之外的事不做。
事件二:SearXNG 偶发 CPU 上浮(结构性故障)
现象:巡检时偶尔看到 SearXNG 容器 CPU 冲高一阵又自己落回去,频率不定,内存倒是安分。定位:这类”自己会好”的尖峰不值得上火焰图全量分析(真需要的时候,站内《CPU 100% 的定位路径》是完整方法),直接从行为推断:搜索是并发打多个上游引擎,一次稍复杂的查询就足以在小机器上掀起一阵 CPU;多个查询叠加就更高。处理:确认它被限额和 restart 兜着(最坏是被杀重启,不连坐全栈),然后把巡检里对它的预期从”不该尖峰”改成”尖峰正常、常驻异常才报警”。结论落在架构上:它仍然是全栈里第一个该被砍的可选件——这句话第一篇说过,一个季度的运行画像没有推翻它,反而坐实了它。这件事的通用教训值得单独点出来:对自愈型异常,先判断”边界是否兜得住”,再决定要不要深挖——限额兜得住就调预期、留观察,兜不住才值得投入完整定位;把排查预算花在会连坐的故障上,是单机自托管项目的资源观。
事件三:redis 内存策略调整(配置性故障)
现象:某段时间榜单卡片”变旧”的频率明显上升,检查发现 redis 驱逐频繁——缓存里的榜单数据过早被逐出,下次抓取窗口到来前页面只能挂更旧的数据。定位:maxmemory 初设得保守,而键的数量随源和分页缓存增长,实际工作集慢慢顶到了上限;allkeys-lru 在工作集大于内存时会把”还有用的”数据也逐出去。处理两步:一是按”源数量 × 每键大小 × TTL 窗口”重新估了工作集,把 maxmemory 上调一档(仍在容器限额内、仍给 fork 留余量);二是给各源 TTL 加了抖动、错开过期时刻,避免工作集同涨同落。纪律没变的是 policy 仍是 allkeys-lru——因为这台 redis 依旧只装可重建的缓存数据,业务数据混进来的前提不存在。驱逐策略的完整选型逻辑与八种 policy 的坑,站内《Redis 内存打满与驱逐策略》是专门展开,这次调整本质上就是那篇结论的一次实操。反模式二:给会增长的缓存一次性设死内存参数、之后不复查。缓存的工作集不是常量——源在加、分页在存、TTL 窗口在变,初值再准也只对得起设它的那一天。缓存类参数要进巡检清单(看驱逐计数、看命中率),让数据替你说”该调了”,而不是等卡片变旧倒逼排查。
四、成本账:一台服务器的复用经济学
这套站的成本结构简单到一张表说完:
| 成本项 | 实际情况 | 说明 |
|---|---|---|
| 服务器 | 0 新增 | 复用跑博客主站的那台 2C1.8G,热榜站以内存预算的形式”租”其中一块 |
| 域名与证书 | 0 新增 | 子域挂在主域下,证书与续期复用主站那套 |
| 带宽/流量 | 忽略级(定性) | 自用面板,访问量是个位数里的个位数 |
| CI 与镜像存储 | 忽略级(定性) | 只有 app 一个自建镜像,公开 registry 免费额度内 |
| 人力 | 不免费 | 一个季度约等于”三次照看 + 一轮策略调整”,全记在第三节 |
现金成本约等于零,但要把隐藏成本说透:省钱的代价是同机故障域。热榜栈与博客主站共享一台机器的内存与 CPU,任何一边失控理论上都会挤到另一边——这正是”全限额 + 只绑回环口 + 不本机构建”三条纪律存在的理由,它们不是优化项,是同机复用的安全边界。换一台独立小机器的月成本不高,但会引入新的运维面(多一台机器的证书、监控、更新);在自用定位下,这个交换不划算。
五、如果重来一遍会怎么改
复盘的价值在这张表。左边是当时的选择,中间是重来会做的,右边是理由:
| 当时的决策 | 重来会怎么做 | 为什么 |
|---|---|---|
| SearXNG 与全栈同机 | 要么一开始就不装,要么放独立最便宜的小机器 | 它是全栈唯一”结构性费资源”的组件,且与核心数据链路零耦合;同机部署让全栈为一个可选件承担内存与 CPU 预算 |
| redis maxmemory 拍脑袋定初值 | 开跑前先按”键数量 × 单键大小 × TTL 窗口”估算工作集再设 | 事件三本来可以不发生;驱逐频繁才调,等于用线上症状补作业 |
| 抓取 cron 跑在 app 容器里 | 抓取独立成容器/脚本,与展示解耦 | app 重启或升级时抓取跟着断;解耦后两边互不拖累,将来要外移抓取(第一篇的对外预案)也是现成接口 |
| 第三方镜像用浮动 tag | 钉 digest,升级显式拉新 | 事件一里”pull 一下就好”的从容,另一面就是”任何一次 pull 都可能带入未知变化”;钉 digest 让每次升级都是明确的决定 |
| Next.js 做展示层 | 维持不变 | 聚合页需要服务端读 redis 动态渲染,纯静态方案不成立;且复用已有知识栈的收益一个季度都在兑现 |
| dailyhot 主力 + RSSHub 补位 | 维持不变 | 事件一的实测反过来验证了这个分工:主力稳、补位可牺牲 |
四条要改、两条维持——对一次”约束优先”的选型来说,这个成绩单是体面的,但要改的四条恰好都指向同一个错误习惯:把”先跑起来”的临时决定留成了长期配置。SearXNG 是”顺手也装上”,maxmemory 是”先给小点”,cron 是”塞 app 里省事”,浮动 tag 是”方便升级”——每一条单看都合理,合起来就是运维面的慢性债。
六、下一步:对外开放与 API 的决策
收官前把两个悬而未决的问题正面回答掉,都用决策链不用感觉。
对外开放吗?不。三个硬约束摆着:机器(2C1.8G 与主站同机,前面说过故障域问题);责任(对外就是运营一个内容聚合站,上游内容的使用授权、平台条款、被上游封禁的应对,全是自用场景不存在的负担);观测(现在连访问统计都没有,对外开放需要的监控、告警、限流一套都没建)。把”开放前必须补的课”列出来,差距有多远一目了然:
| 开放前必须补的课 | 现状 | 差距定性 |
|---|---|---|
| 容量与隔离 | 全栈与博客同机,全限额 | 需要独立机器或至少独立抓取层;SearXNG 必须先砍 |
| 监控与告警 | 每周人工巡检三件事 | 从”人肉周检”到”对外可用”,中间差一整套告警闭环 |
| 限流与防滥用 | 无(自用不需要) | 公开页面与未来 API 都需要,否则一个爬虫就能把小机打满 |
| 内容合规 | 自用展示,无分发问题 | 对外分发涉及上游平台条款,需要逐源评估,这是最重的一课 |
这张表读出来的结论比”不”更具体:不是永远不,是这四门课一门都没修。真到要开放的那天,顺序也是先砍 SearXNG、再外移抓取、最后升配——这个顺序第一篇就写死了,一季度的运行画像没有给出任何修正它的理由。
开放 API 吗?不承诺。技术上最便宜:redis 里数据现成,app 加一个只读 JSON 路由就能输出。但公开 API 等于对陌生人承诺可用性,而这套栈的可用性上限是”上游平台 + 社区路由库 + 一台小机器”三者的交集——自己用它可以接受这个上限,把它写成对外承诺就是不诚实。折中做法:不开放公开端点,自己的脚本直接读 redis(本机回环口),需要数据的地方已经有数据。
收官总结
三篇走完,这套栈教会的东西收敛成几条:约束先于选型——内存、端口、单机三条约束直接杀掉了大部分候选,剩下的决策反而好做;默认值在小内存机器上全部可疑——监听地址、时区、限流、内存策略,每一个都值得怀疑一遍;限额是纪律不是优化——它把”失控”这个状态从”机器打穿”降级成”一个容器被杀”;可选件要按”砍掉它系统是否成立”来装——SearXNG 一个季度的表现反复确认这条;以及最诚实的一条:这套栈的”稳”,一半功劳是纪律,另一半是它只服务一个人的访问量——把这句话留在这里,将来谁(包括未来的我)想把它开放出去,先回来把这一段重读一遍。
系列的分工最后对齐一遍:第一篇是选型决策链(为什么是这五个组件),第二篇是搭建与坑位(怎么跑起来、默认值怎么撞墙),本篇是运行后的账本(稳不稳、贵不贵、改什么、走不走)。三篇合起来是一个完整的小项目生命周期记录,素材真实、数字克制——如果它对读者有价值,价值大概率不在”照抄这套栈”,而在”把约束、纪律、复盘这三件事套到自己的小项目上”。系列到此封箱:除非对外开放或 API 的决定真的翻案,不会再有第四篇。
版本核实(撰写时检索):涉及的组件 RSSHub(DIYgod/RSSHub)、DailyHotApi(imsyy)、SearXNG、redis、Next.js 均为活跃维护的开源项目,版本以各自官方仓库为准,本文不钉任何版本号。架构与三起事件处理记录为本站真实实践;资源占用、成本、频率全部为定性描述,本文无任何实测性能数字,也无任何公网流量数据。