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 从旧一代规格升到新一代,同样的钱多一些连接数。技术栈地图是活的,季度巡检就是它的心跳。

延伸阅读