叮叮当当 #2:热榜聚合站,搭建与踩坑实录

一、这一篇讲什么

系列第一篇把选型讲完了:dailyhot-api 主力、RSSHub 补位、redis 中转、Next.js 聚合渲染、SearXNG 做可选的搜索件,五容器跑在阿里云 2C1.8G 单机上。这一篇是搭建与踩坑实录——compose 怎么编排、nginx 反代怎么配、五个组件各自的坑在哪。照旧先亮底牌:这是本站真实在跑的栈,坑都是真踩过的;性能话题全部定性,一个没测过的数字都不给。

这一篇的破题句:“全限额单机”这条约束,会让部署里的每个默认值都变成坑。下面的坑位清单里,一半不是组件的 bug,是组件的默认行为撞上了小内存机器。

二、compose 全栈编排:五个容器一份文件

编排的纪律从第一篇延续:人人有内存限额、出门带重启策略、端口只绑回环口、镜像只 pull 不 build。全文骨架(数值为示意量级,按自己机器压):

# /opt/hotboard/docker-compose.yml —— 密码类统一走 .env,不写进文件进版本库
services:
  app:
    image: registry.example.com/hotboard-app:latest  # CI 构建产物,服务器从不 build(内存红线)
    environment:
      - HOSTNAME=0.0.0.0      # 坑四:不显式设,standalone 会绑到容器 ID 上,端口映射直接失联
      - REDIS_URL=redis://:${REDIS_PASSWORD}@redis:6379/0
      - TZ=Asia/Shanghai      # 坑五:不设 TZ,容器里的 cron 与日志全是 UTC,排查时间线会错位
    ports:
      - "127.0.0.1:8080:3000"  # 只暴露回环口,公网唯一入口是主站 nginx
    mem_limit: 400m
    restart: unless-stopped
    depends_on:
      redis:
        condition: service_healthy  # app 抢跑而 redis 没就绪时,首启就是白屏
  redis:
    image: redis
    command: >
      redis-server --requirepass ${REDIS_PASSWORD}
      --maxmemory 150mb --maxmemory-policy allkeys-lru
    healthcheck:                 # 上面 service_healthy 的依据
      test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "PING"]
      interval: 10s
    mem_limit: 200m              # 大于 maxmemory:给 fork 的写时复制留余量
    restart: unless-stopped
  dailyhot:
    image: imsyy/dailyhot-api
    mem_limit: 150m
    restart: unless-stopped
  rsshub:
    image: diygod/rsshub
    environment:
      - NODE_ENV=production
      1. 部分 radix 路由默认不启用,按官方文档配置表逐项补环境变量开关(坑一)
    mem_limit: 350m
    restart: unless-stopped
  searxng:
    image: searxng/searxng
    volumes:
      - ./searxng:/etc/searxng   # 挂进来的 settings.yml 负责关 limiter(坑二)
    ports:
      - "127.0.0.1:8081:8080"
    mem_limit: 250m
    restart: unless-stopped

限额的总和小于物理内存减去博客主站的占用,这条第一篇讲过;本篇补一句运维侧的配套:限额之外还要有巡检——docker stats --no-stream 每天扫一眼,哪个容器常年顶着限额跑,就说明它的预算定小了或它该被优化了;容器反复重启的排查路径,站内《Docker 重启循环排障》有完整展开。反模式:限额写了就不管。限额不是交完作业的仪式,被限额反复杀死再重启的容器,等于一个自带内存泄漏还每天准点报到的隐患。

三、nginx 反代:一个域名,两条路径

80/443 在主站 nginx 手里,新栈只听回环口。要点是一个子域按路径分流,证书与 DNS 全部复用,零新增成本:

# 主站 nginx 追加的 server 块
server {
    listen 80;
    server_name hot.example.com;            # 独立子域,证书续期复用主站那套

    location / {
        proxy_pass http://127.0.0.1:8080;   # 聚合页(Next.js standalone 映射的回环口)
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_read_timeout 30s;             # 页面有 redis 兜底,不该陪一个卡住的上游等超时
    }

    location /search/ {
        proxy_pass http://127.0.0.1:8081/;  # 结尾斜杠:剥掉 /search 前缀再转发给 SearXNG
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

三个配置要点单独点名:proxy_read_timeout 要显式给一个有主见的值,默认 60 秒对”有缓存兜底的页面”太宽容;X-Forwarded-For 必须传,否则坑二里 SearXNG 的 limiter 看到的永远是 127.0.0.1;子路径反代 SearXNG 时,它自己的 settings.yml 里 base_url 要配成同样的子路径,否则页面出来了静态资源 404。反代层面的通用故障(502/504 一类)排查,站内《Nginx 502/504 排障》是专门的展开。

四、踩坑清单:五个坑,每个都是现象到修法

坑一:RSSHub 部分路由 404

现象:同一批路由,本地跑源码正常,进了生产容器就 404 或空结果。定位:不是网络问题,是这部分依赖 radix 路由匹配的接口默认不启用,需要按官方文档的配置表补对应的环境变量开关;另有部分路由要配 cookie/token 才出数据。修法:在 compose 的 rsshub 环境变量区逐项补齐,具体开关名随版本变动,以官方文档为准,不抄二手教程。配套纪律:dailyhot 和 RSSHub 都自带不了缓存兜底,抓取一律走服务端定时任务进 redis——裸连上游,改版和限流任何一个都能把页面打黑。

坑二:SearXNG 的 limiter 与外部访问

现象:反代后搜索前几下正常,接着全部 429 或直接被 ban。定位:SearXNG 默认开 limiter 做机器人防护,反代之后它看到的来源 IP 全是 nginx 所在机器,几个真实用户被折叠成一个 IP,瞬间触发限流。修法二选一:自用场景直接在挂载的 settings.yml 里 server.limiter: false;要保留限流就把真实 IP 传进去(limiter 配置里读 X-Forwarded-For 的 real_ip 设置)。附带一个次生坑:挂载 settings.yml 时路径和权限不对,容器会静默用默认配置起,你以为改了其实没改——改完务必 docker compose restart searxng 后进容器确认文件内容,这一步省掉,后面的排查全是在验证一个没生效的配置。

坑三:redis 的密码与内存策略

三个小坑叠在一起。密码:特殊字符在 compose 与连接 URL 里有两层转义,最省心的做法是密码只用字母数字,或统一走 .env 变量插值(第二节写法);连接串里带密码的格式是 redis://:密码@redis:6379,冒号别丢。内存策略:maxmemory-policy allkeys-lru 只因为这是纯缓存实例,里面只有可重建的数据;哪天想把业务数据也塞进来,先读《Redis 内存打满与驱逐策略》再动手。余量:mem_limit 必须大于 maxmemory,fork 时的写时复制需要这块差值,贴着设等于定时炸弹。

坑四:Next.js 容器内存与 standalone 产物

这条是五个坑里最隐蔽的。Next.js 官方自托管路径是 output: 'standalone' 加多阶段构建,运行镜像只拷 .next/standalone、.next/static 与 public 三样——公开对比数据(WeSeek 的实测文章)显示同一应用的镜像能从 2GB 级降到几百 MB 级,对小机的磁盘与内存双友好。但 standalone 的 server.js 拿环境变量 HOSTNAME 当监听地址,而 Docker 容器里 HOSTNAME 默认是容器 ID——于是服务”起来了”,端口映射却怎么都连不通。修法就是第二节那行 HOSTNAME=0.0.0.0。构建侧的配套红线:绝不在本机 build(构建内存峰值能打穿 1.8G 机器,出事的定位思路见《OOM 与内存泄漏的定位》),CI 出镜像、服务器只 pull。再给 Node 堆上一道 NODE_OPTIONS=--max-old-space-size 的显式上限,让它在限额内自己收着分配,比被 cgroup 硬杀体面。

坑五:定时刷新,cron 还是客户端 SWR

榜单数据的”新鲜”由谁驱动,两个流派对比:

方案 机制 优点 缺点 本站取用
服务端定时抓取(cron) node-cron 或系统 cron 定时打上游、写 redis 与访客无关;上游压力可控可排班;失败可重试可告警 无人访问也在跑;配错时区全部错位 主力
客户端 SWR 页面 stale-while-revalidate,访客到来触发刷新 有人看才回源,零后台 高峰时一窝蜂回源;新鲜度依赖前端正确性 展示层兜底

本站的组合是 cron 保底、SWR 微调。选这个组合还因为它顺手解决了缓存三件套:击穿——访客永远只读 redis,回源只发生在 cron 单点,天然 single-flight,访客再多也不会一起打上游;雪崩——各源 TTL 加随机抖动,避免同一时刻集体过期、下一轮 cron 一起压上游;穿透——上游挂掉时把空结果也写一个短 TTL 的降级标记,否则每个请求都会穿透到已经挂掉的上游。反模式:只上 SWR 不上 cron。看起来省了一个组件,实际是把回源时机交给不可控的访问模式,高峰击穿、夜间数据发霉两头挨打。

五、验收:怎么确认这套栈站住了

验收项 目标 怎么测
全栈起来 五容器 running,redis healthy docker compose ps;curl -I http://127.0.0.1:8080 本机通
反代路径 域名 / 与 /search/ 都通 curl -I https://hot.example.com/ 与 /search/ 各来一次
内存有界 限额不被常年顶破 docker stats --no-stream 连续巡检一周;超限重启看 docker inspect 的 OOMKilled 字段
故障降级 单个上游挂只黑一张卡 docker compose stop dailyhot,页面其余部分照常出、对应卡片标旧数据时间
榜单新鲜 随 cron 节奏更新 卡片上的更新时间戳跟着走;redis-cli --scan 抽查键与 TTL
搜索可用 limiter 不误伤真实使用 连续搜二十次不 429;换设备访问正常

六、诚实结论与下一篇

这套栈跑稳的三个前提没变:全限额、无本机构建、上游宽容。本文五个坑的共同教训是:小内存机器上,每个组件的默认配置都值得怀疑一遍——默认监听地址、默认时区、默认限流策略、默认无密码,全都是按”一台好机器、一次内网部署”的假设给的。性能与资源占用本篇全部定性,真实运行数据留给系列第三篇复盘,那篇会讲这套栈上线以来的内存曲线形状、哪些限额最终调过、以及”如果要重来一遍会改什么”。

版本核实(撰写时检索):RSSHub(DIYgod/RSSHub)、dailyhot-api(imsyy)、SearXNG 官方镜像均为活跃维护;Next.js 的 standalone 输出与自托管指引出自官方文档,镜像体积对比为公开数据引用(WeSeek 博客,约 2GB 降至几百 MB 量级);SearXNG 的 limiter 与 base_url 配置以官方文档为准。安装命令与镜像 tag 一律不钉版本;本文无本站实测性能数字,内存数值为示意量级。

暂无评论

发送评论 编辑评论


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