超过140个服务,三名全职工程师大部分时间都在维持系统运行。Segment这次合回单体,最刺眼的读数是团队还剩多少时间改产品。
这是Alexandra Noonan在2018年公开复盘的目的地转发系统。它接收客户事件,再送往不同外部API。团队把这些转发服务合并,原本想靠拆分获得的隔离,则需要重新安排。
共同变更和高频交接,值得优先检查;实际用不到的独立部署,才该合并。远程边界要靠实际获得的独立性证明价值。判断服务粒度,先算每次改动和每份数据要跨几道边界,再看这些边界保护了什么。
服务拆开以后,修改也跟着分叉
Segment早期共用队列。某个目的地变慢,失败请求和重试会拖住其他投递。按目的地拆队列、拆服务,有明确收益:外部接口的故障被分开承受。
问题出在后续维护。目的地各有代码仓库,却共享一批基础库。库修好后,要分别升级、测试和发布。只升级当前要用的服务,其余继续跑旧版,共享逻辑就逐渐分叉。
这里多出来的是“变更扇出”:一次共同修改,要落到多个部署单元上。拆分给了团队分别发布的能力,也把同步升级的工作交给了团队。
如果这些服务真正由不同团队演进,独立发布可以抵消这笔开销。若还是同一批人修改、测试和值班,仓库数量增加,自治收益却没有随之增长。成本会表现为延期、兼容补丁和不敢碰的公共库。
Segment还有一笔运行账。各目的地负载不同,扩容配置要分别照顾。合并后,共享处理池更容易吸收波峰。测试则用基于yakbak的Traffic Recorder,录下首次测试的HTTP请求与响应。后续用文件回放替代外部API,运行原有转换逻辑断言。共同依赖改坏了转换结果,会在这套回归里暴露;外部API是否仍兼容,还需在线联调。
合回去的逻辑,留在外面的调度
如果把所有事件重新塞回一个先进先出的队列,旧问题会原样回来:一个外部接口变慢,健康任务也排在后面。
Segment另做了Centrifuge,负责调度与重试。它按数据源与目的地的组合组织虚拟队列,用数据库查询调整执行顺序。失败任务往后排,健康任务先走,无需反复复制整份事件。这里的数据源是事件来源,客户可有多个来源;客户级限流与数据源粒度要分别考虑。
Calvin French-Owen公开的Centrifuge设计中,Director负责接收和执行job。它先把事件与投递信息写入RDS MySQL的JobDB,再开始发送。jobs表存载荷,job_state_transitions表追加状态记录;retry_at表示下一次重试时间。调度改的是执行次序,载荷留在原处。
Director向job指定的端点发HTTP请求。收到200便记成功;超时或500则等待重试,按任务策略写入下一次尝试的时间。它确认的是端点响应,业务副作用是否落定仍由接收方判定。按原文的过期路径,等待重试的任务先转入归档,写入S3后结束。
Director重启后,从JobDB读回任务恢复处理;Consul会话锁约束同一数据库的单写者。维护团队仍要管任务状态、调度公平性和积压恢复。Segment省掉分散的运行负担,却没有删掉可靠投递这份工作。
▲ Director持久化、端点响应与等待重试路径
还要分清两种故障。下游慢或限流,调度层可以把等待分开;代码缺陷让共享进程崩溃,则会波及同进程任务。Segment在复盘里明确接受了后一种隔离退步。
迁移到自己的系统时,请求超时后若接收方支持按业务标识去重,就用同一标识重投。若只支持查结果,查到已完成便停止;查不到仍可能是在途或结果尚未可见,应暂停自动重投,由业务维护者核对接收方结果。只有确认原请求已终止且未生效,才恢复发送。
Prime Video省下的是每一帧的交接
Prime Video在2023年的音视频质量监控改造,暴露了另一种扇出:同一份中间数据,要为几个组件反复搬运。
工程师Marcin Kolny公开的原方案中,媒体转换组件拆出视频帧。帧数据先写入S3对象存储。检测器再取回数据,分析画面和音频缺陷。Step Functions负责工作流编排,每秒视频对应多次状态转换。
这套方案按组件划分得很清楚。放大流量以后,交接本身成为扩展和成本问题。团队随后把转换、检测与编排放进同一进程,中间数据改走内存,部署在EC2与ECS上。
模块职责大体保留。远程传递方式被撤掉。转换结果给检测器消费时,少了一次对象写入和后续读取,也少了对应的远程编排。
▲ Prime Video改造前后的编排与帧交接关系
单实例装不下新增检测器后,团队又按检测器子集复制服务,并在外面分发请求。最终形态是多个较粗的处理单元,内部紧密协作,外部继续分工。
两家公司削减的成本位置不同。Segment侧重一次共同修改引出的维护工作;Prime Video侧重一份数据引出的存储与编排工作。它们都把业务职责与部署边界分开考虑。
用1440万次交接,倒推一条边界
以下为示意:100路视频,每路每秒取10帧,每帧0.2MB,交给3个检测器。每帧向对象存储写一次,每个检测器各读一次。MB按十进制计算,先忽略重试、缓存和元数据。
每小时产生的帧数是:100×10×3600=360万。
每帧一写三读,每小时就是360万×4=1440万次对象请求。其中360万次写入,1080万次读取。按载荷算,写入0.72TB,读取2.16TB,合计2.88TB逻辑读写量。
这些数对应组件之间的交接。它们既非公网流量账单,也未包含检测算法的计算量。换成进程内共享数据后,检测器仍要读取内存;被省掉的是用对象服务周转中间结果的那一段。
现在把取帧频率改成每秒1帧,其他条件保持原样。请求降到每小时144万次,逻辑读写量降到0.288TB。远程边界的负担缩小十倍,独立扩容或独立发布就更容易体现价值。
还可以换一个条件:只有某个检测器的需求大幅增长。把整个处理单元一起扩容,会连带复制其他检测器和媒体转换。把三个检测器分成热点A和稳定的B、C。A的扩容副本分摊全量帧,每帧只由一个副本读取,B、C保留进程内消费。对象存储每帧一写一读,每小时共720万次请求、1.44TB逻辑读写。再拿这笔交接,与整组扩容多复制的转换、B和C计算作对照。
所以,决定粒度的关键输入是处理频率、中间数据大小、消费者数量,以及各组件的增长差异。服务有多少行代码,对这些账几乎没有解释力。
一次公共协议升级,会牵动几个服务
对国内企业的渠道接入项目,可以把公共协议升级当作入口:多个适配器是否总因同一个字段变化一起修改?若外部接口由不同机构控制,发布窗口和凭据仍各自独立,这些边界就有保留价值。固定文件处理链则先查中间文件在几个步骤间反复落盘。
落地时,先选一组共同变化的模块。查看一次公共协议升级,是否总要改同一批服务;查看一次扩容,是否总把它们一起扩;查看故障,是否总由同一组人排查。若答案长期一致,就把它们列为合并候选,再用故障和权限边界筛一遍。
调度键要对齐实际限流对象。若外部接口按客户凭据限流,同一凭据下多个数据源就应共用额度。只按来源分队列,会让它们合计超限。代码归并到哪里,与任务失败后谁继续前进,是两项设计决定。
权限和故障边界要集中保留。持有不同敏感凭据的组件、必须隔离崩溃的高风险处理器、负载增长明显不同的计算模块,以及确有独立发布需求的团队,适合继续分开。合并后的进程若拿到所有权限,原先的访问限制也要重新设计。
验证无需从全系统开始。挑这一组服务,用相同输入比较新旧业务结果、完成延迟和资源消耗,再让一个下游持续超时。看其他租户是否照常完成,恢复后积压是否消退,重投是否造成重复业务动作。
国内团队还应把人工时间放进对照。同一次公共库升级,改了几个仓库、经历几轮联调、占去多少值班时间,都可以直接记下来。云资源费用由账单支付,变更扇出则从交付周期里扣。
每多保留一个服务,都应说清它保护了谁,以及谁负责付出维护与恢复的成本。
IT架构师联盟
面向架构师与工程同行,解析AI Agent、系统架构与可靠性,讲清技术机制、工程边界和实践方法。
热门跟贴