一、这个站要做什么,在什么约束下做
开个新系列,代号”叮叮当当”——热榜你一条我一条往上跳,就是这个项目名的全部由来。要做的东西一句话说完:一个自托管的聚合页,把微博、知乎、B 站、GitHub Trending 这些散在各处的热榜收进一个页面,自己开、自己看、自己养。
先亮底牌:这是本站真实在跑的架构,不是脑内演练。整套栈跑在阿里云 2 核 1.8G 的单机上——就是跑着博客主站的那台(同机另一半内存预算的去向,见《MySQL 到 SQLite 的不停机迁移实录》)。所以本篇性能话题全部用定性描述,一个没测过的数字都不给;重点放在选型决策上,因为“2C1.8G 单机”这个约束,会让热榜站的每个决策都变重。
先把约束摊开。这些不是”要注意的点”,是每一条都直接杀掉一批选项的东西:
| 约束 | 具体情况 | 直接杀死了哪些选项 |
|---|---|---|
| 内存 1.8G,还要与博客主站分 | 全栈总预算(示意)1G 出头 | 在服务器上构建镜像(docker build / next build 的内存峰值能把机器打穿);无头浏览器类抓取 |
| 80/443 已被主站 nginx 占用 | 新栈不能直接对外监听 | 任何”组件直接暴露公网”的方案;只剩”主站 nginx 反代到内网口”一条路 |
| 单机,无弹性 | 加内存等于加钱 | “先跑起来再说、不限额”的野路子——一个小机最怕的就是不限额 |
验收标准同样在开工前定死,全部是可定性验证的:
| 验收项 | 目标 | 怎么测 |
|---|---|---|
| 页面可访问 | 首屏完整渲染 | curl -I 看状态码 + 浏览器实测 |
| 榜单新鲜 | 分钟级更新 | 卡片上渲染”更新时间”;redis-cli INFO stats 看 keyspace 命中情况 |
| 内存有界 | 每个容器常驻不破限额 | docker stats --no-stream 定期巡检 |
| 故障隔离 | 某个上游挂了只黑一张卡片 | 手动停掉一个抓取容器,页面其余部分照常出 |
二、整体架构:五个容器,一条数据流
整套栈是一个 compose 文件拉起来的五个容器,数据流只有一条:
定时抓取(RSSHub / dailyhot-api)→ 写入 redis(带 TTL)→ Next.js 服务端读 redis 聚合渲染 → 主站 nginx 反代吐给浏览器。每个组件一句话定位:
| 组件 | 角色 | 一句话理由 |
|---|---|---|
| dailyhot-api | 各平台热榜的主力数据源 | 专为热榜而生,JSON 即取即用,实现轻 |
| RSSHub | 补位数据源 | 路由库覆盖了大量”独占源”,社区维护 |
| redis | 抓取与渲染之间的缓冲 | 上游挂了页面不空,内存还可控 |
| app(Next.js) | 聚合与展示 | 服务端渲染卡片,浏览器不直连任何上游 |
| SearXNG | 站内搜索(可选件) | 顺着热点往下挖的入口,与榜单数据无关 |
内存预算是这套架构的命根子。本站的真实做法是”每个容器必有限额,限额总和小于物理内存再减去博客主站的占用”;下面这张表给出的是示意量级,具体数值要在自己的机器上压出来(压法和踩坑在系列第二篇展开):
| 服务 | 限额(示意量级) | 备注 |
|---|---|---|
| app(Next.js) | 400m 级 | Node 的堆外内存(Buffer、线程)不进 V8 堆统计,预算要按整个进程算 |
| RSSHub | 350m 级 | Node 常驻,固定开销打底 |
| dailyhot-api | 150m 级 | 轻量 API,五个里最省心的 |
| SearXNG | 250m 级 | 搜索是并发行为,内存上浮明显 |
| redis | 200m 级 | maxmemory 再设低一档,给 fork 留余量 |
三、数据来源选型:自写爬虫、RSSHub 还是 dailyhot-api
热榜站的第一道决策是数据从哪来。三个候选摆在一起:
| 方案 | 优点 | 缺点 | 适合的场景 |
|---|---|---|---|
| 自写爬虫 | 想抓什么抓什么,依赖最少 | 每个上游一套解析代码,改版即挂;反爬升级要持续跟进;JS 渲染页需要无头浏览器,1.8G 内存直接出局 | 只有一两个独占源,且愿意长期维护 |
| RSSHub | 社区维护的庞大路由库,上游改版常有人先你一步修好;官方镜像开箱即用;RSS/JSON 两种输出 | 重心在”万物转 RSS”,公告与更新流是强项、热榜只是覆盖的一部分;部分路由要配 cookie/token;Node 常驻有固定内存开销 | 榜单之外还想把订阅源一起收进来 |
| dailyhot-api(imsyy/DailyHotApi) | 专为各平台热榜而生,JSON 即取即用,实现轻、内存小 | 覆盖站点以项目维护为准;上游改版要等项目更新;自身无缓存,裸连会被上游限流 | 热榜数据的主力源 |
本站的结论是组合:dailyhot-api 当主力,RSSHub 补位,自写只做兜底。两个开源上游都没有的源,在 Next.js 里写一个小抓取函数(带超时、带 try/catch),它挂了也只黑一张卡片,不连坐。
反模式一:”自写爬虫最可控”。可控的是你的代码,不可控的是上游的改版节奏。业余维护者跟不上一圈上游的改版速度,最后不是某个源烂掉,是整个页面跟着烂掉,然后你失去维护它的兴致。
反模式二:浏览器直连上游 API。CORS 大概率过不去;过得去,也是把上游的风控引到每个访客的 IP 上;更糟的是一个上游卡住会拖死整页渲染。所以抓取一律在服务端做、进 redis、带超时——浏览器从头到尾只认识你自己的域名。
四、缓存与展示:redis 中转,Next.js 渲染
为什么抓取和展示之间要塞一个 redis,三个理由:
- 解耦:上游挂掉或限流时,页面照常出旧数据,卡片上标注”数据更新于 xx”——降级而不是报错;
- 共用:今天只有 app 消费这份数据,明天任何一个脚本、任何一次统计想用,读同一个 redis 就行,不用再打上游;
- 有界:redis 的 maxmemory 加驱逐策略让缓存自己管住自己。注意,这台机器上的 redis 是纯缓存实例(里面只有可重建的数据),才敢用 allkeys 一类的策略——为什么”才敢”、混存业务数据时 allkeys-lru 会闯什么祸,展开在《Redis 内存打满与驱逐策略》。
展示层选 Next.js,一个硬理由:本站已经有完整的Next.js 内容站实战打底,服务端聚合渲染、按卡片粒度控制缓存这套路数是现成的,不需要新学任何东西。卡片在服务端拼好,浏览器拿到的是完整 HTML;数据新鲜度由 redis 里的 TTL 和定时抓取节奏决定,与页面部署解耦。
反模式三:构建期把榜单数据打进页面。热榜在 next build 时就成了静态快照,之后每次”更新”都要重新部署——一个”热”榜被做成了”冷”站。数据获取必须与构建解耦,这是本系列最重要的一条纪律。
五、SearXNG 在这套栈里的角色
SearXNG 是开源自托管元搜索引擎(metasearch engine),聚合多个搜索引擎的结果、不追踪用户。在这套栈里它不产任何榜单数据,是”顺着某条热点往下搜一把”的入口,挂在独立路径(如 /search/)下。
诚实地说:它是这套栈里最贵的可选件。搜索天然是并发行为,每次查询都要同时打多个上游、内存上浮明显。所以在 1.8G 的机器上,它的待遇是独立限额、独立路径、可整体摘除——哪天要给别的组件腾内存,第一个砍的就是它。只做热榜聚合的话,这一节可以直接跳过,整套架构不缺它。
六、部署形态的三个决策
决策一:镜像从哪来。约束一已经判了”服务器上构建”的死刑,剩下的选项:
| 方案 | 做法 | 结论 |
|---|---|---|
| 服务器上构建 | docker build 在本机跑 |
出局——构建期内存峰值对小机是灾难(这类问题的定位思路见《OOM 与内存泄漏的定位》) |
| CI 构建,服务器只 pull | 代码推仓库,CI 出镜像推 registry,服务器 docker compose pull && up -d |
本站自建镜像(app)的做法 |
| 全用现成镜像 | RSSHub、dailyhot-api、SearXNG、redis 全部官方镜像 | 覆盖四个组件,唯一需要自建的只有 app |
决策二:怎么对外。80/443 在主站 nginx 手里,那就让主站 nginx 顺手多带一个 server 块,新栈只听内网口:
# 主站 nginx;80/443 已被博客占用,新栈只暴露 127.0.0.1 上的端口
server {
listen 80;
server_name hot.example.com; # 独立子域,证书与续期复用主站那套
location / {
proxy_pass http://127.0.0.1:8080; # app 容器映射到本机回环口
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
决策三:内存怎么管。落到 compose 上就是”人人有限额,出门带重启策略”:
# /opt/hotboard/docker-compose.yml;数值为示意量级,按机器压
services:
app:
image: registry.example.com/hotboard-app:latest # CI 构建产物,服务器从不 build
ports:
- "127.0.0.1:8080:3000" # 只绑回环口,公网摸不到
mem_limit: 400m # 限额是纪律,不是优化项
restart: unless-stopped # 单机自愈的最后一道保险
dailyhot:
image: imsyy/dailyhot-api # 官方镜像;tag 固定策略自行决定,本文不替你钉版本
mem_limit: 150m
restart: unless-stopped
redis:
image: redis
command: redis-server --maxmemory 150mb --maxmemory-policy allkeys-lru
1. allkeys-lru 只因这是纯缓存实例,业务数据绝不能混进来
mem_limit: 200m # 限额大于 maxmemory,给 fork 时的写时复制留余量
restart: unless-stopped
1. rsshub 与 searxng 同款写法,限额见第二节预算表
七、诚实结论与本系列的边界
这套栈的定位是自用信息面板:同时在线的用户是个位数里的个位数。它跑得稳的三个前提是全限额、无本机构建、上游足够宽容。如果哪天要对外运营,2C1.8G 的正确顺序不是先加内存:先砍 SearXNG,再把定时抓取移出去(让 CI 或另一台更小的机器只跑抓取、写 redis),最后才轮到升配。顺序反了,钱花在刀背上。
本篇回答”为什么这么选”;系列的下一篇讲搭建与踩坑(compose 全文、抓取调度、每个组件的真实坑位),第三篇讲运行数据与复盘。配套阅读上文已给出入口:Next.js 内容站实战是展示层的底子;《OOM 与内存泄漏的定位》在限额被顶破的那天用得上;《Redis 内存打满与驱逐策略》是缓存实例选 policy 前的必读。
版本核实(撰写时检索):RSSHub(DIYgod/RSSHub)与 SearXNG 均活跃维护并提供官方 Docker 镜像;DailyHotApi 为 imsyy 维护的开源项目,镜像为 imsyy/dailyhot-api。安装命令一律不钉版本,以各自官方仓库为准。本文无基准测试,性能描述全部为定性;限额数值为示意量级,架构本身为本站真实在跑的组合。