freehire 架构深度解析:Go 单体 + 60 个 run-once Worker + 事务性 Outbox,330 万职位如何持续流转

发布时间:2026/10/22 23:01:56
freehire 架构深度解析:Go 单体 + 60 个 run-once Worker + 事务性 Outbox,330 万职位如何持续流转 freehire 架构深度解析Go 单体 60 个 run-once Worker 事务性 Outbox330 万职位如何持续流转【免费下载链接】freehirefreehire — the open-source search engine for job seekers项目地址: https://gitcode.com/GitHub_Trending/fr/freehirefreehire 是一个开源职位搜索引擎它直接从 92 个 ATS 招聘平台爬取 330 万 条在线 IT 职位全部开源可自托管。这篇文章拆解 freehire 架构背后的三个关键工程决策Go 单体、run-once worker 模式与事务性 Outbox——看看一个 330 万规模的职位搜索引擎是如何让数据 7×24 持续流转而不积压的。先看全景一次搜索请求背后发生了什么freehire 的产品形态很简单按地区、工作形式、专业方向、职级做分面过滤每条结果都直链到公司自己的招聘页没有中介转帖。而支撑这个界面的是 docs/architecture.md 里描述的一整套数据管道。官方架构图SVG展示了完整链路用一句话概括数据进来靠爬虫数据流转靠 Postgres 里的事务性队列数据出去靠 Meilisearch。下面逐个拆解。为什么是 Go 单体少一个进程就少一种故障很多读者听到单体会联想到落后但 freehire 的选择恰恰相反——它把复杂度压到了最少的地方只有一台常驻 HTTP 服务cmd/server基于 Fiber 框架承载全部 API包括应用内 AI 助手的 LLM 调用唯一的例外是cmd/mail-ingest收件监听守护进程。除此之外没有任何常驻后台运行时没有消息队列守护进程存储与搜索外置PostgreSQL含 pgvector是唯一的事实源Meilisearch 负责分面全文检索Redis 只用于限流与实时推送。技术选型清单见 README.md 的 Stack 一节。单体的好处在这里体现得很直接所有写路径共享同一套代码、同一个数据库连接约定进程之间不需要互相通信只需要共享数据库——这为下一节的核心模式铺平了路。60 个 run-once Worker没有守护进程的世界如何运转打开 cmd/ 目录你会看到 100 多个可执行入口爬虫ingest、harvest-boards、队列消费者enrich、embed、search-drain、全量重建reindex、通知三件套notify、remind、nudge、以及各种一次性数据修补脚本backfill-*。它们全部遵循同一个生命周期由 cron/systemd timer 启动 → 读环境变量DATABASE_URL连接数据库 → 跑完一轮 → 退出失败时以非零状态码结束。这种 run-once-and-exit 模式带来几个意外的好处特性说明不堆叠靠 systemdTypeoneshot防止上一轮没跑完时新一轮又启动无需文件锁易排查进程活着就有工作退出了就该有日志没有常驻进程假死这种玄学问题统一骨架所有 worker 共用 internal/platform/worker/ 里的 Bootstrap 初始化、进度心跳、Sentry 异常捕获和统一的退出码约定0干净1有失败项让 cron 告警生效以爬取为例cmd/ingest/main.go 每次只负责一个招聘平台provider 作为参数传入从boards目录表里读取该平台的所有待爬看板逐一看板抓取、归一化、按UNIQUE (source, external_id)幂等去重后 upsert。这意味着每个平台可以按自己的节奏独立排期——某个慢平台卡住绝不会拖累其他平台的更新。 这种单进程单职责 数据库协调的模式本质上是用磁盘上的表代替了内存中的队列。Postgres 天然提供了事务、唯一约束和并发控制等于免费获得了一个可靠的消息系统。事务性 Outbox330 万职位流转的核心这是整篇文章最重要的一节。问题场景是这样的每次爬取都会产生成千上万条职位的写入其中一部分需要触发 LLM 富化、一部分需要向量嵌入、一部分需要更新搜索索引。如果每条写入都顺便调一次下游下游挂了就丢数据如果引入 Kafka 之类的中间件运维成本陡增。freehire 的答案是事务性 Outbox发件箱模式在同一个数据库事务里把写入职位和往队列表插一行一起提交。事务成功两边同时存在事务回滚两边同时消失——不存在职位写进去了但没进队列的中间状态。Postgres 里有三张这样的队列表队列表消费 Worker职责enrichment_outboxcmd/enrich新职位送 LLM 做 AI 富化技能、职级、工作模式等分面semantic_outboxcmd/embed职位内容向量化写入 pgvector 供相似职位查询search_outboxcmd/search-drain内容变化的职位推送到 Meilisearch 活索引以 migrations/0076_search_outbox.sql 为例这张表只有一个job_id主引用加上认领/重试记账字段attempts、claimed_at、failed_at并用UNIQUE (job_id)保证同一职位不会重复入队。文件头部的注释还记录了一段真实的事故复盘早期设计让 ~169 个独立 worker 进程各自直接推送 Meilisearch而 Meilisearch 每次推送都要对整个百万级倒排索引做一次 re-merge实测单次 50–180 秒直接打满了磁盘 IO 拖垮了网站。改为所有写入统一走 Outbox 后cmd/search-drain把一批变更合并成一次推送——很多次小而贵的合并变成少数几次大而廉价的合并。一个精妙的细节爬取发现职位内容没变时走廉价路径只刷新last_seen_at时间戳不入队。否则整个目录每几小时就得整体重建一次索引。搜索链路结果页如何做到不查数据库Outbox 把数据送进 Meilisearch 之后查询侧的设计同样值得学习索引里存的是完整职位search.JobDocument直接内嵌了职位的公开 JSON 结构jobview.Job所以搜索结果命中即可渲染不需要回 Postgres 取数据唯一的例外是幽灵职位信号Ghost Job Signal——需要查库做 best-effort 补充查询失败就隐藏徽标绝不拖垮搜索本身深分页直接拒绝offset limit上限 10000空查询默认按最新发布排序无关键词时相关性没有意义。活索引的保真靠 cmd/reindex 全量重建从 Postgres 从零构建新索引后原子切换在线读永远不受重建影响。相关细节见 internal/search/AGENTS.md。可观测性与运维让静默的失败无处藏身run-once worker 没有 HTTP 端口可抓指标freehire 的解法很有特色每个 worker 把本次运行结果写成 Prometheus textfile 文件node_exporter 采集freehire_worker_last_run_*三个指标回答worker 还活着吗——挂死的运行永远不会打上完成戳所以上次运行时间变老一个规则同时覆盖卡死和定时器停了两种故障cmd/queue-metrics定期巡检所有队列发布队列深度、死信数、最老等待时长回答worker 跟得上吗。文档里记录了真实案例notify连续数周报告delivered1 failed0但 114 万条匹配一直没发出去——只有最老等待时长这条曲线能提前暴露它全局暂停开关往 Redis 写一个freehire:pause:all键就能让整个 cron 舰队拒绝启动新任务开关本身故障时fail open绝不让限流设施成为目录停止更新的原因。这些约定完整写在 internal/platform/worker/AGENTS.md是学习 run-once worker 运维实践的好材料。职位如何死生命周期设计一个职位只要closed_at为空就算在线。关闭是软状态系统从不硬删除职位——每笔关闭都记录closed_reason哪个机制触发猜测性关闭因此可以整批回滚。四种独立机制各管一段爬取清扫某平台 48 小时未再出现的职位被关闭且只限本次真正爬到的公司范围流式源主动下线增量 feed 显式发出的删除事件存活探测对无人回访的孤儿链接做纯启发式 URL 探测连续两次已过期才关闭时效规则Telegram 这类发帖型来源 45 天强制关闭帖子会活得比职位久。详见 docs/agents/job-lifecycle.md。延伸阅读按模块深入你想了解从哪里读起系统全景与数据流docs/architecture.mdWorker 骨架与退出码约定internal/platform/worker/Outbox 表结构真实事故复盘在注释里migrations/0076_search_outbox.sql爬取管道与看板目录internal/ingest/pipeline/搜索索引与增量排水internal/search/功能清单做了什么而非怎么做docs/features.md总结这套架构给普通开发者的启示freehire 架构最值得抄作业的不是某个具体组件而是三条原则用数据库当协调中枢——事务性 Outbox 把消息可靠性问题降维成事务问题省掉一整层中间件运维run-once worker 优于常驻守护——可重试、可审计、可暂停故障模式少了一大类批量是免费的——把 169 次昂贵的小推送合并成几次大批量性能问题往往不是靠更快的机器而是靠少做几次。如果你正在自建一个内容聚合或搜索引擎这个开源职位搜索引擎的代码库值得认真读一读每个决策的为什么都写在对应模块的AGENTS.md里。【免费下载链接】freehirefreehire — the open-source search engine for job seekers项目地址: https://gitcode.com/GitHub_Trending/fr/freehire创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询