尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
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),仅供参考
RELATED

相关推荐

词元经济学:日均140万亿调用量下的成本坍塌机制与Token工厂盈利边界

词元经济学:日均140万亿调用量下的成本坍塌机制与Token工厂盈利边界

【摘要】国内日均词元调用量2年增长超1000倍,2026年3月突破140万亿;同等智能水平推理成本年降约10倍,头部独立服务商毛利率却跌至-24%。本文拆解推理成本坍塌的引擎机制与三类玩家毛利分化逻辑,给出5步效率治理SOP与可量化失效边界…

📅 2026/10/22 23:00:58
无头 Chrome 自动填表投递:freehire auto-apply 如何做到“所有必答项都有答案才提交“

无头 Chrome 自动填表投递:freehire auto-apply 如何做到“所有必答项都有答案才提交“

无头 Chrome 自动填表投递:freehire auto-apply 如何做到"所有必答项都有答案才提交" 【免费下载链接】freehire freehire — the open-source search engine for job seekers 项目地址: https://gitcode.com/GitHub_Trending/fr/freehire freehir…

📅 2026/10/22 23:00:58
langchain 构建智能体 Agent

langchain 构建智能体 Agent

1.理解 Agent 1.1 什么是 Agent? 在大模型应用开发中,智能体通常指一种以大语言模型为推理与决策核心,结合记忆 、 工具调用与环境交互能力,能够进行规划决策并执行复杂任务以达成目标的软件系统 Agent 的关键能力: …

📅 2026/10/22 23:00:58
MORE NEWS

更多资讯

📰

AI Agent框架选型:从链式工作流到多智能体的权衡

1. 为什么Agent开发需要框架?聊聊Agent开发的痛点1.1 Agent到底在解决什么问题?很多朋友第一次接触Agent时,会觉得“这不就是给大模型写个循环,让它自己调工具吗?”。没错,最原始的Agent确实可以这么玩&…

📰

Selenium实战:从零破解JavaScript渲染页面的爬虫核心思路

做了几年爬虫开发,我越来越觉得,高级爬虫技巧里最容易被低估的一项,就是处理JavaScript渲染页面。很多刚入门的朋友习惯用 requests 一把梭,正则、XPath 都写好了,结果换到一个前后端分离的站点,HTML 源码里…

📰

用虚拟机搞定博途多版本共存:TIA Portal标准化环境搭建全攻略

搞自动化这么多年,谁的电脑里没几个博途(TIA Portal)版本?项目现场碰到S7-300要用V15.1,老设备程序要用V13,新开发的项目又上了V17,客户那边可能还有V19甚至V20的备份程序等着你打开。最崩溃的是…

📰

开源免费舆情系统源码+数据库:架构部署与避坑实战

简介:这是一套开源免费的舆情监测分析系统完整源码及配套数据库,面向需要快速搭建本地舆情平台的技术人员、中小企业或高校研究者,可用于网络舆论采集、情感分析与热点发现。包内含2000个文件,以1757个JavaScript脚本、131个CSS样…

📰

K8S 节点监控:node-exporter 资源清单与镜像文件部署实践

简介:面向Kubernetes运维与监控实践者,这套资料围绕Prometheus生态中的node-exporter组件,整理了在K8s集群内完成主机监控部署所需的全部文件。内容涵盖Service、ConfigMap、Deployment等核心YAML清单,以及配套告警规则与镜像导出…

📰

慢SQL八大坑:从索引失效到深分页的优化指南

不少团队都经历过这样的时刻:线上数据库CPU突然飙高,慢查询日志里刷出来一条跑了七八秒的 SQL,表面看功能完全正常,返回的数据也挑不出毛病。你把它复制到本地,数据量小,执行计划还算体面,可一上…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬