1.4 技术栈对齐:Go 库与云产品地图
场景:白板上画不下的地图
选型和机器都定了,真正动手前还有一步:把技术栈对齐。我们团队的习惯是先画一张地图——每个环节写清楚用什么、为什么、备选是谁。没这张地图的后果是每个人按自己习惯选库,最后 JSON 解析能冒出三种写法。
这一节把当时的地图原样贴出来。如果你打算跟着这本书搭一遍,这张表就是你的采购清单。
方案:一张表看懂整体技术栈
整条链路分成采集、调度、存储、出口、运维五层。原则是:业务逻辑自己写(调度、清洗、去重),基础设施用云产品(数据库、对象存储、CDN、监控),通用能力用成熟库(HTTP、解析、日志)。这条原则后来被我们简称为三层分工,每次有新成员加入都用它做一遍入职讲解,比逐个文件讲代码快得多。
| 层 | 我们的选择 | 承担的职责 | 备选与放弃理由 |
|---|---|---|---|
| 采集层 | net/http 加 goquery | 页面下载与解析 | colly,回调模型与状态机冲突 |
| 调度层 | 自研 master | 任务分发、优先级、去重指纹库 | cron 撑不住分布式状态 |
| 消息层 | Redis(master 内) | 内存队列与指纹缓存 | Kafka 太重,量级用不上 |
| 存储层 | 云数据库 MySQL | 清洗后的结构化结果 | 自建 MySQL 不想运维 |
| 快照层 | 对象存储 COS | 原始 HTML 留档与重放 | 本地盘扩容贵且易丢 |
| 出口层 | 自研控制台加 CDN | 任务管理与数据分发 | 直接暴露源站带宽扛不住 |
| 运维层 | 云监控加自定义指标 | 任务健康度与告警 | 写脚本巡检不如直接接监控 |
每层对应的官方文档入口集中列在这里,后面正文提到时不再重复:轻量应用服务器 cloud.tencent.com/document/product/1207、对象存储 COS cloud.tencent.com/document/product/436、云数据库 MySQL cloud.tencent.com/document/product/236、CDN cloud.tencent.com/document/product/228、云监控 cloud.tencent.com/document/product/248。日志服务 CLS 在 3.4 节单独讲,它不在核心五件套里,但多机之后是刚需。
地图里有一个分工原则值得单独强调:凡是涉及状态和业务语义的层,都紧紧攥在自己手里;凡是纯基础设施的层,能买的都买。调度层自研不是因为没得选,而是因为任务状态是整个平台的核心资产,交给任何第三方以后退出成本都极高——这个教训在 1.2 节算退出成本时已经讲过。反过来,MySQL 和 COS 如果也自建,我们要额外维护备份、扩容、跨可用区,四个人根本排不过来。
配置与代码:解析层先跑通
技术栈里最先要验证的是采集层,因为后面所有环节都建立在能稳定拿到解析结果上。验证的方式是写一个能跑通的小 demo,只抓一个站、只做下载和解析、结果打印到控制台,不用连数据库不用连队列。这个 demo 我们跑了三天,重点观察两件事:一是解析稳定性,选择器对不对、页面结构变化时崩不崩;二是连接复用,net/http 默认的连接池需要显式配置 MaxIdleConnsPerHost,不然高并发时每个请求都新建连接,延迟和资源消耗都会上去。
goquery 的用法很直白,我们抽取详情页的套路长这样:
func parseDetail(doc *goquery.Document) *PageItem {
item := &PageItem{}
item.Title = strings.TrimSpace(doc.Find("h1.art-title").Text())
item.Body = strings.TrimSpace(doc.Find("div.art-content").Text())
pub, _ := doc.Find("span.pub-time").Attr("content")
item.PubAt = pub
return item
}
这段代码故意写得很短,因为真实的抽取规则变化频率很高——几乎每加一个站点就要适配一次选择器。我们把规则配置化,存进任务结构体的 ListRule 与 DetailRule 字段(见 1.1 节),解析引擎本身保持稳定。规则配置化是 4.1 节清洗管道的核心思想,这里先埋个伏笔。
存储层的选择多说两句。采集结果为什么要落 MySQL 而不是直接写文件或者 ES:业务侧要按站点、时间范围、关键词联合查询,MySQL 的索引最符合使用习惯;量级上日均三万条、总量五百万行,基础版实例完全 hold 住。如果日均上到百万级,可能要考虑分库分表或换存储,但那不是我们的量级。MySQL 的使用姿势在 2.3 节详述。
版本管理也属于技术栈对齐的一部分。我们所有 Go 项目统一走 go.mod,库版本在各服务之间保持一致,尤其是 zap 和 goquery 这种被多个服务共同依赖的库。曾经出现过 master 用 goquery 新版、worker 用旧版,抽取结果细微不一致的怪事,排查了一下午。从那以后规定:公共库版本由一个人统一升级,升级走完整的灰度。
约定:先定规范再写代码
地图之外还有一份附注,是我们的开发规范,同样属于技术栈对齐的产物。内容不长,但避免了很多返工:
| 规范项 | 约定 | 踩过坑之后才加的 |
|---|---|---|
| JSON 字段命名 | 全小写加下划线 | 是,master 与 worker 联调翻过车 |
| 日志格式 | zap 结构化,固定字段 | 是,标准库 log 后来全换了 |
| 时间表示 | 统一 UTC 存库,展示层转时区 | 是,跨 zone 采的时候乱过 |
| 错误码 | 按服务分段,1000 为一个段 | 否,一开始就定了 |
| 配置文件 | 同一份模板分环境覆盖 | 否 |
时间表示那条多解释一句:采集对象的站点分布在不同时区,发布时间是业务关键字段,如果存库时带本地时区,聚合统计时会错乱。我们的做法是入库统一 UTC,仅在控制台展示时按用户时区转换。这个小约定在 4.1 节清洗管道里能看到具体实现。
规范定好后还有一个好处:onboarding 成本极低。后来招了个实习生,给他看地图加规范,再对着代码走一遍,第二天就能改清洗规则了。如果规范散在各个人的脑子里,知识传递只能靠口口相传,团队永远长不大。
补充一点地图与规范的关系:地图管选什么,规范管怎么用。有的团队只有地图没有规范,结果三个人写出三种风格的 HTTP 客户端封装;有的团队只有规范没有地图,规范里充满过时的技术债。两者一起维护,每季度 review 一次,是我们后来固定的节奏。## 踩坑:技术栈对齐阶段的三个后悔
第一个后悔是没早点定 JSON 字段命名规范。早期接口里 site_id 与 siteId 混用,master 与 worker 联调时静默解析失败,排查了半天。后来统一全小写下划线,并写在技术地图的附注里。这类规范越早定越省事。
第二个后悔是日志库换晚了。最初用标准库 log,多机之后想接日志服务发现没有结构化字段,全部推倒换 zap。换库本身不贵,贵的是历史日志格式不统一。3.4 节会讲我们最终怎么把多机日志收进 CLS。
第三个后悔是低估快照的价值。最初觉得原始 HTML 落 COS 是浪费存储,三个月后清洗规则升级,历史数据要用新规则重跑时,快照救了命——不用重新请求目标站,直接从 COS 回灌。快照的存储策略在 2.4 节。
还有一个后悔与云产品有关:MySQL 实例选小了。图省钱买的最低配,结果凌晨窗口集中写入时连接数偶尔打满,任务延迟抬升。后来升了一档就稳了。选云数据库规格和选服务器一样,先按当前量的两倍估,观察一周再降配也不迟,云数据库升降配比迁移数据容易得多。
再补一条与 CDN 有关的后悔。控制台上线时我们直接用源站回源,所有静态资源都从源站拉,浪费了轻量机器本就不宽裕的带宽。后来把静态资源全推 CDN 才消停,带宽占用直接降了六成。这个决定最后在 4.4 节展开成完整的 CDN 策略,也成了 5.3 节省钱清单里最立竿见影的一条。
小结
技术栈对齐的本质是分清三件事:自己写什么、用什么库、买什么云服务。我们的经验是业务核心逻辑自己写(调度、去重、清洗),通用能力用库,基础设施买云服务。地图画清楚之后,后面每一章都在往里填具体实现。多说一句,这张地图不用一次定稿,我们的地图到第三篇分布式改造时又改过一轮,加了日志服务和代理池——技术栈是活的,但它必须始终是张图,而不是每个人脑子里的不同版本。下一章开始写单机原型,先跑通从种子页到第一条结果的最小链路。地图贴在墙上,剩下的就是写代码了。
地图贴上墙不算完,还得定期巡检。我们每季度做一次依赖清点:哪些库有了新版本、哪些新出的云产品规格比我们现在的便宜、哪些依赖其实早就没人用却还留在 go.mod 里。清点一次总能挤出两三个小优化,比如把 MySQL 从旧一代规格升到新一代,同样的钱多一些连接数。技术栈地图是活的,季度巡检就是它的心跳。
