叮叮当当 #1:热榜聚合站,选型为什么这么选

一、这个站要做什么,在什么约束下做

开个新系列,代号”叮叮当当”——热榜你一条我一条往上跳,就是这个项目名的全部由来。要做的东西一句话说完:一个自托管的聚合页,把微博、知乎、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。安装命令一律不钉版本,以各自官方仓库为准。本文无基准测试,性能描述全部为定性;限额数值为示意量级,架构本身为本站真实在跑的组合。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇