做数据这行十来年,被问得最多的一类问题从来不是“用哪个工具”,而是“这份数据到底该怎么采”。日志、业务库、开放接口、网页、设备、音视频,表面上都归在数据采集这四个字底下,真落到实现层面,技术栈、参数、坑位几乎是完全不同的几套东西。我做过日志管道、做过业务库增量同步、做过接口层面的定时拉取、也做过设备侧的时序采集,踩过的坑比写过的代码多。
这篇东西不讲概念,讲不同类型数据采集的技术方案怎么选、怎么落地、参数怎么算、出了问题往哪儿查。适合两类人看:一类是刚接手采集任务、手上有一堆数据源不知道从哪儿下手的同学;另一类是做了一阵子、想把手上的管道从“能跑”调到“可靠”的同行。我会把每一类数据的判断逻辑、采样频率、批量大小、校验规则都写清楚,大部分段落可以直接照着抄成配置文件。
1. 整体方案设计:先把数据分类,再谈技术选型
采集方案做砸的常见原因,不是工具选错,而是分类没做对。把日志当接口采、把设备数据当数据库同步、把网页当文件下载,前面省下的十分钟,后面要用两周来补。所以第一步永远是给数据打标签。
1.1 把数据分成五类,方案自然就收敛了
我习惯按“数据长什么样 + 数据从哪儿来”这两个维度交叉分类,落到实际项目里基本是下面五类:
| 类别 | 典型来源 | 变化特征 | 主流采集方式 |
|---|---|---|---|
| 结构化数据 | 业务数据库、数仓表、对账文件 | 有主键、有更新时间 | 增量查询、CDC、批量导出 |
| 半结构化数据 | 开放接口、JSON 报文、配置文件 | 结构可变、字段可能新增 | 定时轮询、长连接推送 |
| 日志类文本 | 应用日志、访问日志、系统日志 | 持续追加、顺序写入 | 文件采集器 tail、消息队列中转 |
| 非结构化数据 | 网页、图片、音视频、文档 | 体积大、解析成本高 | 页面抓取、对象存储同步、元数据抽取 |
| 流式与时序数据 | 客户端埋点、设备上报、传感器 | 高频、乱序、需要窗口聚合 | SDK 上报、消息队列、时序库写入 |
这张表看起来简单,但它的价值在于:它决定了后面所有参数的量纲。结构化数据你关心的是“多少次查询、每批多少行”;日志你关心的是“每秒多少行、单行多大”;埋点你关心的是“每秒多少事件、峰值系数多少”。量纲搞错,后面所有容量规划都是错的。
我见过一个典型翻车:某团队把客户端埋点当成“接口拉取”来设计,做了一个每 5 分钟跑一次的定时任务,从服务端接口分页拉事件。结果峰值时段接口直接被打穿,事件还得在服务端排队落盘,等于多绕了一圈。埋点这种数据天生就是推的模式,硬改成拉,方向就反了。
1.2 选型时真正要回答的四个问题
工具选型的关键在于先把这四个问题答清楚,答完之后工具基本只剩一两个候选:
第一,数据在哪。数据在你能直接连的库里,就走库内增量;数据在别人的系统里只有接口,就走拉取;数据在客户端或者设备上,就必须走 SDK 上报。这一条决定了架构的起点,绕不过去。
第二,变化有多快。天级变化的报表数据,一天跑一次批处理完全够;分钟级变化的订单状态,增量轮询就得上;秒级变化的埋点和设备数据,必须走消息队列做缓冲。时效性每提升一个量级,架构复杂度大概翻一倍,别过度设计,也别欠账太多。
第三,体量有多大。这里要算的是峰值而不是均值。日均一千万条的管道和峰值每秒五千条的管道,是两种完全不同的生物。前者用单机批处理就行,后者必须有分区、有背压、有削峰。
第四,出错能不能重来。采集最怕的不是采不到,是采重了或者采漏了还不知道。所以每个方案落地前我都会问一句:这条链路能不能做到幂等重放?做不到的话,就得在落库侧加去重键,或者干脆把整批数据打成带版本号的分片,重跑时整体覆盖。
提示:这四条里,只要“出错能不能重来”答不上来,方案就先别开工。采集链路的返工成本远高于开发成本。
1.3 一个通用的四层结构,各类数据都能往上套
不管采什么,我基本都会落成四层:接入层 → 缓冲层 → 清洗层 → 落地层。
接入层负责“拿到原始数据”,尽量不做任何业务加工,拿到什么存什么,保留原始报文,出问题能回放。缓冲层通常是一个消息队列,作用是削峰和隔离,让上游的突发流量不会直接把下游打挂。清洗层做解析、字段映射、格式统一、基础校验。落地层写目标存储,同时负责幂等和去重。
这个结构最大的好处是故障定位边界清晰。接入层挂了,看上游;缓冲层积压,看消费能力;清洗层报错,看解析规则;落地层数据对不上,看主键和去重逻辑。四层各管一段,排查时不用在整个链路上瞎猜。
小体量场景可以直接砍掉缓冲层,接入完就清洗落库,别为了架构好看硬上一个队列。我现在的判断标准是:峰值 QPS 超过 200 或者上游不可控(比如第三方接口、客户端上报),就上队列;否则直连。这个阈值没什么理论依据,纯是经验,低于这个量级上队列,运维成本比收益高。
2. 结构化数据采集:增量识别是全部的核心
结构化数据的采集,难点从来不在“连上数据库读数据”,而在“怎么判断哪些是新的”。全量重跑一次两千万行的表,跑得动,但没必要;真正的功夫在增量识别上。
2.1 三种增量识别手段,各有各的适用边界
基于自增主键。最简单,WHERE id > 上次最大值,索引友好,分页效率高。缺点是只能捕捉新增,捕捉不到更新和删除;而且主键不连续、有回滚插入的场景会漏数据。适合只追加不修改的表,比如流水表、日志表。
基于更新时间戳。WHERE update_time > 上次水位 AND update_time <= 本次开始时间。能捕捉更新,但有两个经典坑:一是同一秒内的多条记录,如果用水位线直接取>,会漏掉同一秒的后半部分;二是时钟回拨或者事务提交顺序与时间戳不一致,导致数据被下一次窗口漏掉。解决办法是把水位线往回退一个安全窗口,比如退 5 分钟,靠主键去重来兜底。
基于变更日志(CDC)。直接订阅数据库的变更流,能完整拿到新增、更新、删除三种事件,延迟可以做到秒级甚至更低。代价是需要数据库开启相应配置、需要额外的组件消费变更流、还要处理 schema 变更。数据量大、要求实时、且需要感知删除的场景,这是唯一正解。
具体怎么选,我给一个粗略的判断:
| 场景 | 推荐手段 | 理由 |
|---|---|---|
| 只追加的流水表,天级同步 | 自增主键 | 实现成本最低,索引命中好 |
| 有更新的业务表,小时级同步 | 更新时间戳 + 回退窗口 | 兼顾实现成本和正确性 |
| 需要感知删除,秒级同步 | CDC | 唯一能拿到删除事件的方案 |
| 表结构频繁变更 | CDC + schema 注册 | 变更流带结构信息,好适配 |
2.2 时间戳增量的参数怎么算,别拍脑袋
假设一张订单表 2000 万行,每天变更 30 万行,需要每小时同步一次。看起来量很小,但坑在“回退窗口”和“批次大小”上。
回退窗口取多大?我的算法是:取该表最长事务时长的 2 到 3 倍,且不小于 1 分钟。如果这张表有批量更新任务,单次事务跑 90 秒,那回退窗口就取 5 分钟。窗口太大,重复数据多,去重压力大;窗口太小,漏数据风险高。宁可重复不可遗漏,因为重复能靠主键去重解决,遗漏基本发现不了。
批次大小取多少?简单算一下:每批 2000 行,单批查询加写入耗时约 120 毫秒,30 万行需要 150 批,约 18 秒,完全在 1 小时的窗口内。如果单批提到 10000 行,吞吐上去了,但单次事务变大,锁持有时间长,可能影响源库。我的经验值是单批 1000 到 5000 行,单批耗时控制在 200 毫秒以内,这样对源库的压力可控,失败重试的代价也小。
分页的方式也要注意,LIMIT offset, size在 offset 很大时会退化成扫索引,越翻越慢。改成游标分页:
-- 不推荐:深分页会越来越慢 SELECT id, order_no, status, update_time FROM t_order WHERE update_time > '2024-05-01 00:00:00' ORDER BY id LIMIT 1000000, 2000; -- 推荐:游标分页,每批都走主键索引 SELECT id, order_no, status, update_time FROM t_order WHERE update_time > :watermark AND id > :last_id ORDER BY id LIMIT 2000;游标分页的关键是把上一批的最大主键带回下一批,条件里带上id > :last_id,这样每一批都是范围扫描,不会随偏移量变慢。这个改动在做大批量同步时,性能差距经常是十倍以上。
2.3 CDC 落地的几个关键配置和真实取舍
CDC 不是开个开关就完事,几个配置直接决定后面顺不顺:
# 源库侧,行级变更记录,且记录完整的前后镜像 binlog_format = ROW binlog_row_image = FULL # 保留时长要大于最大消费延迟的 3 倍以上,防止消费者长时间停机后无法续读 binlog_expire_logs_seconds = 604800binlog_row_image = FULL这一项很多人会忽略。取MINIMAL能省空间,但变更事件里只有被改动的字段,下游做整行覆盖更新时就会出问题——没变的字段拿不到值。做数据同步的场景基本都得用FULL。
还有一个必须提前想清楚的问题:初次全量怎么办。CDC 只给增量,历史数据得先导一遍。标准做法是先做全量快照并记录快照对应的变更位点,然后从该位点开始接增量。这两步之间如果没对齐,就会出现一段数据重复或者一段数据丢失。我一般的做法是:全量快照期间对目标表做写入暂停或者写到临时表,快照完成后核对行数,再从快照起始位点回放增量,重复部分靠主键覆盖。
注意:CDC 组件本身重启后能不能从上次位点续读,一定要在测试环境验证一遍。有些组件默认不提交位点,重启直接从最新开始,等于静默丢数据,这种问题生产上很难第一时间发现。
2.4 结构化采集的三条实操心得
源库压力永远优先考虑。同步任务跑在源库上,慢查询会直接影响线上业务。所有同步 SQL 必须走索引、单批限流、避开业务高峰。我习惯在同步任务里加一个“源库负载探测”,发现源库连接数或慢查询数超标就自动降速甚至暂停。
字段类型要显式映射。别指望自动推断。数据库的decimal(18,4)、datetime带时区、tinyint表示布尔,这些在不同目标库里行为都不一样。我一般会维护一张字段映射表,读取和写入都按这张表来,省掉大量“数据对不上”的排查。
删除操作单独处理。如果没有 CDC,源库的物理删除是同步不到的。折中做法是业务侧改软删除,加一个is_deleted字段,同步时带上状态。物理删除的场景只能靠定期全量比对,或者让业务方提供删除清单。
3. 半结构化数据采集:接口、日志、文件的三种打法
半结构化数据的共同特点是“结构存在但不固定”,字段可能新增、可能缺失、可能改类型。这类采集的核心不是拿数据,而是在结构变化时不崩。
3.1 接口采集:限流、重试、签名,三件事定生死
接口拉取看起来最省事,实际上最容易出问题。我总结下来,决定成败的就三件事。
限流。对方给了配额就必须自己控住,不能靠“我请求慢一点应该没事”。做法是客户端侧维护一个令牌桶,按配额打八折配置。比如对方允许 100 次/秒,我配 80 次/秒,留出重试和并发的余量。多实例部署时,限流必须是全局的,要么用中心化的令牌服务,要么按实例数均分。均分更简单,但实例扩缩容时会失衡,得配合配置下发。
重试。只对可重试的错误重试——超时、5xx、限流拒绝。4xx 里的参数错误重试一万次也没用,只会把配额烧光。重试策略用指数退避加随机抖动:
import time import random import requests def fetch_with_retry(url, params, max_retry=5, timeout=8): """带指数退避与抖动的接口拉取,只对可重试错误进行重试""" for attempt in range(max_retry): try: resp = requests.get(url, params=params, timeout=timeout) if resp.status_code == 200: return resp.json() # 429 限流、5xx 服务端异常,属于可重试范围 if resp.status_code == 429 or resp.status_code >= 500: raise RuntimeError(f"retryable status: {resp.status_code}") # 其余 4xx 直接抛出,不浪费配额 resp.raise_for_status() except Exception as e: if attempt == max_retry - 1: raise # 指数退避 + 抖动,避免多实例同时重试造成二次冲击 backoff = min(2 ** attempt, 30) + random.uniform(0, 1) time.sleep(backoff) raise RuntimeError("unreachable")这段代码里两个细节值得说。min(2 ** attempt, 30)是给退避设上限,不然第 8 次重试要等 256 秒,任务早超时了。random.uniform(0, 1)的抖动是关键,多实例同时失败时,没有抖动会形成周期性尖峰,把刚刚恢复的接口再打一次。
签名。很多接口要求签名,签名相关的三个常见错误:时间戳用本地时间导致和对方有偏差被拒;参数排序规则没对齐导致签名不匹配;请求体参与签名但编码方式不一致。签名失败时,第一件事是打印出参与签名的原始字符串逐字符比对,别对着文档猜。
分页也要注意。有些接口的翻页是基于偏移量的,翻到后面数据变了会导致重复或跳过。这种情况下,如果接口支持按时间范围查询,优先用时间范围切分,把大区间切成小时甚至分钟级的小区间,每个区间独立翻页,把“漏数据”的风险控制在单个小区间内。
3.2 日志采集:多行合并和断点续传是两个硬骨头
日志采集器用什么都行,关键是配置。它的工作模式是盯着文件追加内容、记录读取位置、批量发送。
多行合并。Java 的异常堆栈是典型的多行一条日志,如果不做合并,一行堆栈会被拆成几十条独立记录,后面根本没法分析。做法是配置一个正则,匹配到“新日志开头”才切分,其余行都归到上一条:
# 日志采集器的多行配置示例 multiline: pattern: '^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}' negate: true # 不匹配该正则的行,视为上一条的续行 match: after # 续行追加到上一条之后 max_lines: 500 # 单条日志最多合并 500 行,防止异常刷屏撑爆内存 timeout: 3s # 3 秒内没有新行,强制把缓冲区内容发出去max_lines和timeout这两个参数必须有,不然一个死循环打印的进程会把整条管道拖死。timeout也别设太大,设成 5 秒以上,日志的端到端延迟就会明显变差。
断点续传。采集器必须记录“读到哪了”,记录的方式通常是文件标识加偏移量。文件标识不能只用文件名,因为日志轮转后文件名会重用,新文件会被当成老文件继续读,直接漏掉全部新内容。稳妥的做法是用 inode 加设备号作为标识,文件被重命名或者轮转后依然能正确识别。
批量大小。单批太小,网络往返次数多,吞吐上不去;单批太大,内存占用高,发送失败要重传的内容多。我常用的配置是单批 500 到 1000 条,或者按体积控制在 1 到 5 MB,两个条件哪个先满足就发。压缩打开,文本日志的压缩比通常能到 5 比 1 以上,带宽省下来的钱比 CPU 贵。
3.3 文件采集:编码、分隔符、字段数,三个必查项
文件类数据(对账文件、导出报表、第三方投递的 CSV/XML)最常见的问题就是解析失败。上线前我固定查三样:
编码。有没有 BOM,是 UTF-8 还是 GB 系列,换行是\n还是\r\n。BOM 这个东西特别阴,一个看起来正常的 CSV,表头第一个字段名前面多了三个不可见字节,映射就全对不上。读取时统一用utf-8-sig能自动吃掉 BOM。
分隔符与转义。逗号分隔的文件里如果字段内容本身带逗号,必须有引号包裹。很多导出工具默认不加引号,导致列数错乱。所以解析后一定要校验“每行字段数是否等于表头字段数”,不等的行单独落到异常文件,不要硬解析。
幂等处理。同一份文件可能被投递两次,采集端必须能识别。做法有两种:按文件名加文件指纹(大小加哈希)去重,或者按业务主键覆盖写入。前者简单,后者更可靠。我一般两个都做,指纹去重挡掉明显的重复投递,主键覆盖兜住内容层面的重复。
import hashlib import os def file_fingerprint(path): """文件名 + 大小 + 前 1MB 内容哈希,用于识别重复投递的文件""" stat = os.stat(path) h = hashlib.md5() h.update(os.path.basename(path).encode()) h.update(str(stat.st_size).encode()) with open(path, "rb") as f: h.update(f.read(1024 * 1024)) return h.hexdigest()只取前 1MB 做哈希是权衡:全文件哈希对几 GB 的文件要读一遍,浪费时间;前 1MB 加上文件大小,重复文件的识别率已经足够高,误判概率极低。
4. 非结构化数据采集:网页、媒体与文档
非结构化数据采集的复杂度不在“拿下来”,在“拿得下、拿得干净、拿得合规”。这一类要特别注意边界,采集行为本身必须尊重目标站点的服务条款和访问规则,只取公开可用的内容,控制请求频率,不做任何影响对方正常服务的事情。
4.1 页面采集:先看规则,再谈技术
动手之前先确认三件事:目标站点是否提供了开放接口或者数据导出,有的话优先用它,比解析页面可靠得多;站点公开的访问规则里是否允许该路径被采集;采集频率是否会给对方造成压力。
技术上,页面分两种。静态页面,内容直接在返回的 HTML 里,用请求库加解析库就够了,成本低、速度快。动态渲染页面,内容靠脚本执行后才生成,必须用浏览器内核来跑,资源消耗高一个量级,速度慢十倍以上。
判断方法很简单:直接请求一次,看返回内容里有没有目标数据,没有就是动态的。
动态页面的采集要注意三点:一是等待策略别用固定 sleep,改成等某个元素出现或者等网络空闲,固定 sleep 要么不够要么浪费;二是并发数要压住,一个浏览器实例的内存占用是几百 MB 起步,并发开大了机器直接 OOM;三是解析失败要能发现,页面结构一改,解析出来全是空值,如果不做空值监控,可能连续几天采到空数据都没人知道。
我固定会加一个结构探针:每次采集完,检查关键字段的非空率,低于 95% 就告警。这个简单的检查救过我很多次。
4.2 图片、音视频与文档:采集的重点是元数据
媒体类数据的特点是单个体积大,全量拉取代价高。方案上有两个取向。
只采元数据。记录地址、大小、格式、时长、分辨率、创建时间这些信息,真正的内容按需拉取。多数分析场景其实只需要元数据,比如统计某类内容的分布、做内容检索的索引。这种做法存储成本低一个数量级,落地快。
全量落地到对象存储。需要做内容分析、转码、审核的场景才需要。落地的关键是分片上传加断点续传,大文件直传失败一次就全废。同时要记录内容指纹(哈希),避免同一份内容被重复存储。
元数据抽取的工具有现成的,图片读 EXIF,音视频读容器信息,文档按格式解析。我遇到最多的问题是抽出来的时间字段时区不一致,有的带时区偏移,有的是裸的本地时间,混在一起排序就乱了。统一转成带时区的标准格式再入库,这一步别省。
4.3 非结构化采集的容量估算
这一类必须提前算容量,不然存储会以肉眼可见的速度被吃光。算法是:日均条数 × 平均单体大小 × 保留天数 × 副本数。
举个例子,每天采 20 万张图片,平均 400 KB,保留 90 天,两份副本:200000 × 400KB × 90 × 2 ≈ 14.4 TB。这个数字出来之后,再决定是全量落地还是只存元数据加冷备。
文档类还要额外考虑解析后的文本体积,通常比原文件大 1 到 3 倍,别只按原文件算。
5. 流式与时序数据采集:埋点、设备与消息缓冲
这一类数据的特征是高频、持续、可能乱序,架构上必须有缓冲和削峰,否则一个突发就能把下游打穿。
5.1 客户端埋点:上报策略决定成败
埋点采集的第一原则是不能拖慢客户端。所有上报必须异步、必须落本地队列、必须允许失败丢弃(非关键事件)。同步上报导致卡顿,是埋点方案最常见的原罪。
上报策略上,批量加延迟是标配:攒够 N 条或者等 T 秒,满足任一条件就发。N 和 T 怎么定?N 太小浪费请求,N 太大丢数据的风险高(App 被杀)。常用的组合是20 到 50 条一批,最长等待 5 到 10 秒。弱网环境下还要加失败重试和本地持久化,重启后能续传。
QPS 和容量怎么算,举个实际例子:日活 50 万,人均每天 100 条事件,事件集中在 10 小时内。日均事件量 5000 万条,平均 50000000 / 86400 ≈ 579 条/秒。峰值系数按 5 估(客户端流量峰谷差比服务端大得多),峰值约 2900 条/秒。每条约 500 字节,峰值带宽约 1.45 MB/s。
消费端的分区数按单分区消费能力倒推。假设单分区能处理 2000 条/秒,峰值 2900 条/秒,至少 2 个分区;考虑消费逻辑会变复杂、留出三倍余量,取 6 个分区。分区数只能增不能减,所以宁可一次多留点余量。
// 客户端埋点批量上报的简化实现 const MAX_BATCH = 30; const MAX_WAIT = 8000; class Reporter { constructor(send) { this.queue = []; this.timer = null; this.send = send; } track(event) { this.queue.push({ ...event, ts: Date.now() }); if (this.queue.length >= MAX_BATCH) { this.flush(); } else if (!this.timer) { // 最长等待,避免低频场景下事件一直积压不发 this.timer = setTimeout(() => this.flush(), MAX_WAIT); } } flush() { if (this.timer) { clearTimeout(this.timer); this.timer = null; } if (!this.queue.length) return; const batch = this.queue.splice(0, this.queue.length); // 发送失败时把数据塞回队首,下一轮重试 this.send(batch).catch(() => { this.queue.unshift(...batch); }); } }失败回塞队首这个处理要配一个上限,不然长时间断网会导致队列无限增长,把内存撑爆。超过上限就丢弃最老的数据,同时打一个丢弃计数,方便事后评估数据完整性。
5.2 消息缓冲:分区键、副本、消费位点
消息队列在这一类采集里的作用是削峰和解耦,配置上有三个点必须想清楚。
分区键怎么选。需要保序的按业务主键做分区键,比如按设备 ID 或者用户 ID 哈希,保证同一主体的数据进同一个分区、按顺序消费。不需要保序的直接轮询,负载更均衡。
副本与确认机制。数据不能丢的场景,副本数至少 2,生产端确认级别设为“所有副本确认”。这个设置会把写入延迟拉高一些,但采集链路里丢数据比慢一点严重得多。
消费位点提交时机。这是最容易出错的地方。先提交位点再处理业务,进程崩溃就丢数据;先处理业务再提交位点,崩溃就重复处理。结论很明确:先处理再提交,业务侧必须做幂等。幂等键用事件 ID 或者业务主键加版本号,落库时用唯一约束或者覆盖写。
消费积压的监控也要有。我一般配两级告警:积压条数超过一百万,或者最新一条数据的滞后时间超过 5 分钟。前者反映吞吐不够,后者反映链路卡住,两种问题的排查方向完全不同。
5.3 设备与时序数据:乱序、时钟、水位线
设备上报的数据最麻烦的是三个问题:乱序、时钟不准、断网补传。
乱序是常态,网络抖动、重发、多路径上报都会导致到达顺序和事件时间不一致。处理方法是不要在采集端假设顺序,把事件时间作为正式字段带上来,计算侧用窗口加水位线处理。水位线的含义是“我等到这个时间点为止,认为更早的数据都已经到了”,设得太短会漏算迟到的数据,设得太长会拖慢结果产出。常用取值是乱序延迟的 95 分位数,实测下来 30 秒到 2 分钟覆盖大多数场景。
时钟不准:设备本地时间可能偏差几分钟甚至更多。做法是采集端记录两个时间——设备上报的事件时间和接收端的入库时间,分析时优先用事件时间,同时监控两者差值,差值异常说明设备时钟有问题,需要推动侧修复。
断网补传会导致短时间内的数据洪峰,采集端必须能承受。服务端侧的应对是队列缓冲加限流写入,客户端侧的应对是补传时限速并在数据上打标,方便后续区分“实时数据”和“补传数据”。
-- 时序数据落库时的幂等写入,靠主键约束挡重复 INSERT INTO ts_metric (device_id, metric, event_time, value, recv_time) VALUES (:device_id, :metric, :event_time, :value, now()) ON CONFLICT (device_id, metric, event_time) DO UPDATE SET value = EXCLUDED.value, recv_time = EXCLUDED.recv_time;主键设计用“设备 + 指标 + 事件时间”,这样补传重复的数据会被自动覆盖,不会重复计入统计。这个设计比在应用层做去重要可靠得多,也省事。
6. 数据质量校验与问题排查实录
采集做完不等于做完,得能证明数据是对的。我在每条链路落地时都会配一套校验,规则不多,但必须自动化跑。
6.1 五类校验规则,覆盖八成问题
| 校验类型 | 检查内容 | 触发动作 |
|---|---|---|
| 完整性 | 主键非空、必填字段非空率 | 非空率低于阈值告警 |
| 唯一性 | 主键、业务唯一键是否重复 | 重复条数超阈值告警 |
| 有效性 | 枚举值范围、数值区间、格式正则 | 非法值占比超阈值告警 |
| 一致性 | 上游条数与下游条数比对 | 差异超 0.1% 告警 |
| 波动性 | 与昨日、上周同期的量级对比 | 偏离超过 30% 告警 |
波动性这一条最容易被忽略,也最有用。数据量突然掉一半,往往不是业务出问题,而是采集链路某个环节挂了。同环比告警能比业务方早几个小时发现问题。
条数比对的做法要讲究。上下游直接比总数,在允许重复或者允许延迟的场景下会一直误报。我的做法是比同一时间窗口内、去重后的唯一键数量,并且给下游留一个延迟容忍窗口,比如上游 T 时刻的数据,在下游 T+10 分钟时再比。
6.2 常见问题速查表
| 现象 | 优先排查方向 | 典型原因 |
|---|---|---|
| 数据量为 0 | 上游是否有数据、采集任务是否在跑 | 任务被调度系统跳过、源表当天无变更 |
| 数据延迟持续增大 | 消费能力、下游写入耗时 | 消费线程数不足、下游写入变慢、消息队列积压 |
| 数据重复 | 幂等键、重试逻辑 | 重试时未去重、消费位点未提交导致重复消费 |
| 字段大量为空 | 上游结构变更、解析规则 | 字段改名、嵌套层级变化、编码问题 |
| 中文乱码 | 编码配置 | 未按实际编码解析、BOM 未处理 |
| 时间字段错乱 | 时区配置 | 混用本地时间和 UTC、未带时区偏移 |
| 解析报错激增 | 上游格式变化 | 报文结构升级、分隔符变化、新增字段 |
| 部分数据缺失 | 分页边界、过滤条件 | 深分页漏页、时间窗口重叠或空洞 |
这张表我基本上是照着实际故障一条条填的,其中“部分数据缺失”是最难查的。我遇到过一次,按时间窗口切分拉取,窗口的边界写成了左闭右闭,相邻两个窗口的重叠部分被重复拉取,去重后看起来正常;但另一个地方写成了左开右开,窗口之间漏了一秒,整整一秒的数据静默消失。这种问题只能靠“源端独立计数 + 目标端计数”的比对发现,光看目标端永远看不出来。
6.3 三条踩出来的避坑经验
第一条,先做端到端的最小闭环,再谈优化。很多人一上来就设计完整的分层架构、上队列、上调度,结果两周了数据还没落地。正确顺序是先用最简单的方式跑通一条数据,从源到目标全程验证,然后在这个基础上加缓冲、加校验、加监控。跑通比跑好重要,因为跑通之后你才知道真实的瓶颈在哪。
第二条,所有采集任务必须能在不惊动任何人的情况下重跑。判断标准是:给你一个时间范围,你能把这段时间的数据完整重采一遍,且结果和第一次一致。做不到这一点的链路,一旦出问题就只能人工补数据,而人工补数据一定会出错。做到这一点需要三样东西:源端能按时间范围查询、任务参数化、目标端幂等。
第三条,把采集任务的“心跳”和“数据量”分开监控。任务进程活着不代表在产出数据,进程正常但一条数据没采到的情况太常见了。所以我给每个任务配两个指标:任务执行状态和本次产出的记录数。状态正常但记录数为 0,连续多次就告警。这个双指标监控帮我抓到过好几次“任务正常跑但源端接口返回空”的问题。
7. 几个容易被忽略的细节
写到这儿,还有几个点零散但值得单独说,都是我实际做项目时觉得“早知道就好了”的。
7.1 采样频率不是越高越好
采集频率每提高一档,源端压力、存储成本、下游计算量都是线性甚至超线性增长。我见过把设备数据采集频率从 1 分钟调到 1 秒的项目,数据量涨了 60 倍,结果分析侧根本用不上这么细的粒度,最后还是要按分钟聚合。
正确的做法是先问清楚下游的最小分析粒度。如果报表是按小时看的,采到分钟级已经足够;如果要做异常检测,可能确实需要秒级。把频率定在“下游需要的粒度”上,不要定在“技术上能做到的粒度”上。
7.2 保留原始报文,成本远比你想的低
接入层保留原始报文,能在出问题时直接重放,不用回源重拉。很多源端的数据是有时效的,比如接口只保留最近 7 天的数据,出问题那天如果没存原始报文,就永远补不回来了。
成本上,原始报文压缩后通常只有解析后数据的 1.5 到 3 倍,按冷存储算,一天的原始报文可能就几十块钱。这个投入和“数据补不回来”的代价完全不成比例。
7.3 采集配置要进版本管理,并且和代码一起走
采集任务最容易变成“配置漂移”的重灾区——某次线上排障手动改了一个参数,没记录,半年后没人知道为什么是这个值。我的做法是把所有采集配置(调度周期、批次大小、字段映射、校验规则)全部纳入版本管理,变更走同一个流程,配置里写清楚每一项的取值理由。这听起来是流程问题,但它是长期可维护性的关键。
7.4 我现在的默认起手式
接手一个新采集需求,我现在的固定流程是:先用半小时把数据分类(五类里的哪一类)、回答四个选型问题(在哪、多快、多大、能否重放),然后写一个最小闭环脚本跑通第一条数据,再根据实测的吞吐和延迟反推参数,最后补监控和校验。整个过程里,写代码的时间大概只占三成,剩下的都在确认边界和算参数。
这套流程不新鲜,但每次跳过它直接开写的项目,最后基本都要返工。数据采集这件事,前期多想二十分钟,后面能省两天。