我们管着一堆来源不同的资源,机器来自不同渠道,状态分散在各家的接口后面。最早和很多团队一样,用定时任务去各家轮询,把结果拉回来比对。资源少的时候十分钟问一次,勉强够用。
1.轮询越走越重
后来发现这条路走不动。轮询频率低了,状态滞后,用户看到的和真实情况对不上,他以为机器还活着,其实早被回收了,提交的任务直接丢。频率高了,请求量先把自己打挂,光轮询就吃掉不少算力,监控系统自己先报警。更麻烦的是,几家接口的时间戳口径不一,有的用 UTC,有的带时区,有的只给相对时间,同一台机器的状态在系统里能出现三个版本,谁真谁假要人去猜。
2.换个思路:变了就来报
不让系统去问各家现在怎么样,而是让各家把变化报出来。数据库把每一次写操作记进 binlog,我们读 binlog,把变更整理成一条条事件,按顺序发往需要它的地方。一条机器从空闲变占用的变更,就变成事件流里的一个点,下游按顺序消费。
{ "type": "pod.state_changed", "pod_id": "pod-9f2a", "from": "idle", "to": "occupied", "region": "ap-shanghai", "ts": 1719000000123, "seq": 48217}每个事件带一个全局递增的 seq,下游严格按序消费,乱序的事件先挂起等补齐。这么做有几个实在的好处:状态以事件形式存在,顺序天然确定,不再有三个版本打架;下游服务被动接收,平时可以安静睡觉,有变更才被叫醒,我们接了事件流之后,轮询请求量掉了九成;数据分析的库也能订阅同一份事件流,报表和线上看到的是同一份事实,财务对账不用再解释为什么两边数字对不上。
3.踩过的坑:schema 漂移
事件流依赖上游把 schema 改动能同步出来。某家悄悄加了个字段没通知,下游解析就会错位,轻则少算一条,重则整条流卡住。我们的做法是把 schema 变更和代码发布绑在一起走流程,谁改了结构,谁负责让事件流也跟着对上,不传出来的变更不允许上线。入口还加了一层校验,遇到不认识的字段先告警而不是默默吞掉。
4.长任务靠持久化执行兜底
还有一类跨多资源的调度,中间任何一步失败都得能回退。我们把这类流程交给带持久化执行能力的框架管,每一步的状态自己记着,机器重启了也能从断点接着跑。这比定时存快照稳,快照之间的那段丢失了就是丢失了,事件流式的状态是连续的。一个跨三区域的数据同步任务,靠这套机制在机房一次意外断电后自动从断点续上,没丢一条已确认的变更。
5.代价:接入要花时间
事件流对上游有要求。各家得愿意把变更吐出来,得保证顺序,得处理重复投递,网络抖一下同一条事件可能到两次,下游得能去重。我们接入一个新来源,光把它的事件格式对齐、把乱序和重发兜住,就要花上一两周。比直接轮询慢,但省下的是后面长期的维护头疼。来源从十家涨到一百家,轮询的维护成本差不多涨了十倍,事件流只涨了一成多点,因为新增来源只是多接一个生产者,下游逻辑不变。
6.现在:binlog 是单一事实来源
我们现在的做法,是 binlog 作为单一事实来源,所有服务和报表都从同一份事件流里取数,这一条当作铁律。哪天要接新业务方,第一条规矩就是先订阅事件流,别再自己起个轮询去扒。新来的工程师问能不能直接查某家接口拿状态,标准回答是:可以临时查,但别把它当成系统的真相来源,真相只在事件流里。
事件流也让我们做容量规划容易了。过去的增长曲线靠拍脑袋,现在是看事件速率推算。哪类资源快不够了,提前一周就能从事件流的斜率里看出来,不用等告警炸了才反应。