
1. 为什么会有 MARM把散落一地的资源管理收拢到一个底座先说背景避免你看半天不知道这东西是干嘛的。MARM 的全称按我们团队的叫法是 Modular Automated Resource Management模块化自动化资源管理平台。项目最初的目的说起来特别朴素把散落在各个系统里的资源申请、配额校验、权限审批、自动开通和回收统一收到一个可编程的控制平面里。开发期大概两三个月上线后直接承担了十几个内部业务的云资源、中间件实例、缓存和存储桶的自动化分配目前稳定跑了大半年。这个项目不是那种“看了标题就觉得高深”的底层基础设施它更像一个贴着自己团队血肉长出来的内部运维中台。如果你所在的团队也有类似情况——运维靠人肉、资源申请靠填单子、扩容靠半夜起床——那 MARM 这套设计思路应该能给你不少直接可抄的东西。先讲讲最初的痛点。团队早期规模不大各业务组自己管自己的资源基本都是这样的组合一部分人用脚本调云厂商 OpenAPI 创建机器一部分人靠控制台手动点还有一个资历比较深的同事专职维护一份 Excel 资源台账。三个方式各有各的问题最致命的是没有统一的“谁申请了什么、什么时候到期、配额还剩多少”的视图。实际工作中经常出现某业务组申请了 20 台 8C16G 的机器实际日均 CPU 只有 5%而另一个组在疯狂排队等扩容。资源利用率上不去成本却一直在涨。我评估过市面上的方案也踩过不少坑。直接上开源平台太重很多功能我们根本用不上反而要给团队增加学习成本。用脚本继续堆又解决不了权限和审批流的问题。最后确定下来一个原则不做大而全的 PaaS只做一个能把资源“管起来”的控制面。这成了 MARM 定位的起点后面的架构设计基本都是围绕这个原则展开的。这里我整理了一个对比表格看看当时各种方案的取舍逻辑方案优点致命缺点结论人工 控制台零成本上手无审计、无配额、无法自动化排除分散脚本灵活、执行快状态不可知、重复建设小规模可用人多了就是灾难商用云平台控制台功能完善、稳定多账号多 Region 之间割裂作为底层执行对象开源管平台功能全面部署重、定制难、学习成本高不适合我们当时团队MARM 自研贴合内部流程需要投入研发成本以最小可用版本起步现在回头看当时做的另一个关键决定是MARM 不直接操作云厂商 API 之外的资源而是把所有“具体怎么创建”的逻辑继续留在云控制台或已有脚本里MARM 只管“什么时候调、谁能调、调完怎么校验、出问题怎么回滚”。这种设计让最初版本的开发量小了很多也降低了推进阻力。2. 核心架构拆解控制面与执行层之间的一条“窄接口”MARM 的架构没有追求时髦的微服务拆分整体上就是很经典的控制面加执行面。控制面负责策略、审批、配额、元数据和审计执行面负责对接具体资源系统的代理层。这两者之间定义了一条非常窄的接口协议所有资源操作都通过这条协议走后续新增一种资源类型时核心控制面几乎不用动。外部看 MARM大概包含这几个模块Gateway统一接入层负责鉴权、限流、参数校验也是外部系统唯一可见的入口。Scheduler调度中心负责消费资源申请任务执行排队、并发控制、超时处理、失败重试。Resource Registry资源注册中心维护每个资源类型的元数据、可用区、配额模板、回收策略。Agent Hub代理管理模块管理一批资源代理Agent的注册、健康检查和远程调用。Audit Store审计存储记录每一次资源变更的前后状态、操作人、审批记录、调用链信息。Expire Worker定时巡检负责从资源到期提醒到自动回收整个生命周期管理。可能你已经发现了这套模块划分和大多数管理系统差不多。但 MARM 的关键差异不在模块名而在“Agent”的设计。我们给每个资源代理定义了一套标准接口包括 Probe、Provision、Terminate、Inspect 四个动作。Probe 用来做连通性和存量检测Provision 负责资源创建Terminate 负责销毁Inspect 用来获取资源当前详细状态。控制面永远不关心 Agent 内部是调云 API 还是执行 SSH 脚本只管这四件事的结果是否与预期状态机一致。这里有个很实际的设计经验接口一定要窄。我开始设计时激情澎湃给 Agent 定义了十几二十个接口有网络检测、磁盘格式化、监控数据上报、日志拉取等等。后来实现到第三个 Agent 就发现不行接口越多Agent 开发者的心智负担越重而且控制面能给的保证越模糊。后来砍到只剩上述四个反而清晰了。比如某个 Agent 需要在创建机器后额外装一个监控组件这个动作就放到 Provision 内部去处理而不是单独暴露一个 InstallMonitor 接口。控制面只管最终要的“状态结果”至于怎么达到这个状态Agent 自己决定。在这种架构下加一种新资源的成本是真的低。团队后来接了一个新的消息队列服务只花了两天半一天实现 MQ Agent 的四个接口半天在 Resource Registry 里录入元数据和配额策略半天把流程工具里的申请表单接上。跑通之后业务组通过 MARM 申请 MQ 实例和申请云主机走的完全一样的审批链和审计逻辑。模块之间的通信我们用了一个轻量级的异步任务队列而不是直接同步调用。原因很简单资源申请的很多操作天然就是慢的创建一台机器可能要几分钟在控制面同步等结果会占用大量连接资源而且中间任何一环节抖动都可能造成整体超时。异步化以后每个申请任务就是一个带状态机的作业记录调度器负责一步步推进失败时能明确知道自己打到了哪一步也方便做人工介入或者自动回滚。3. 调度器、状态机与幂等最花功夫的三个地方如果说模块划分决定了 MARM 的上限那么调度器与状态机的实现质量直接决定了它能不能在真实环境里活下来。这一章讲的是整个项目里最核心、也最不显眼的代码路径我把关键设计拆开讲。3.1 调度器的本质是一个带优先级的有限状态机推进器MARM 的调度器没有用复杂的竞态算法核心就是一个状态机推进循环。每个资源申请任务一开始是 pending经过审批变成 approved然后调度器开始尝试执行 provision执行过程中是 provisioning成功之后是 running失败会有重试或进入 failed到期之后进入 terminating最终是 terminated。状态之间的流转全部落到数据库里每次流转都写入审计日志。调度器从队列里取出任务后会根据任务类型匹配对应的 Agent然后做这几件事检查全局并发限制避免同时打得底层资源系统过载。检查该业务组的配额约束防止超额申请。生成一个全局唯一的 execution_id作为本次操作的幂等键。调用 Agent 的 Provision 接口传入参数带上 execution_id。轮询 Inspect 接口持续确认资源的真实状态直到达到目标状态或超时。用伪代码表示大概是这样def schedule(record): if not acquire_slot(record.task_type): return Requeue(delay5) if not quota_controller.check(record.owner, record.spec): return MarkFailed(reasonquota_exceeded) execution_id uuid4() try: agent agent_registry.get(record.agent_name) agent.provision(record.spec, execution_idexecution_id) except agent.AgentUnreachable: return Retry(current_retry 1) deadline time.now() record.max_provision_timeout while time.now() deadline: status agent.inspect(execution_id) if status.phase running: return MarkSucceed(execution_idexecution_id) if status.phase failed: return MarkFailed(execution_idexecution_id) time.sleep(poll_interval) return MarkFailed(reasonprovision_timeout)这段逻辑看起来简单但实际运行时藏着不少需要抠的细节。比如 acquire_slot 的释放时机不能等整个任务结束才释放而是在实际调用 Agent 之后、进入轮询阶段之前就释放因为轮询阶段并不占用底层资源的创建并发又比如重试策略不是简单的“失败就重试”而是根据异常类型分类Agent 网络不可达可以重试配额不足不能重试参数校验失败不能重试。分类错误会导致无效重试反而污染队列。3.2 幂等设计所有 Agent 接口都必须接受 execution_id这是整个 MARM 里我认为最重要的一条约定没有之一。实际资源操作不像内存操作一次云 API 调用超时后你完全不知道底层到底创建成功了没有。如果没有幂等键重试就可能创建出两台重复的机器如果不重试又可能因为网络抖动错过真正成功的申请。所以 MARM 从第一天起规定Agent 的每个操作接口都必须支持幂等。底层实现要求非常简单Agent 收到请求后先在本地持久化记录里查这个 execution_id如果已经处理过直接返回上次结果如果没有则执行创建流程创建之前先打标记创建成功后再更新状态。这本质上是分布式系统里最常见的“幂等表”模式。代理内部使用本地 SQLite 或单机文件表都够用不需要引入额外的中间件。这样做的效果在故障演练时体现得特别明显。我们故意在处理过程中杀掉 Agent 进程重启后重新发起同一个执行请求因为 execution_id 已经存在Agent 直接返回创建中的现状不会重复调用底层 API。控制面发现资源最终进入 running任务被标记为成功。整个过程没有开任何“额外补偿”的机器。3.3 数据库模型少做关联多留快照MARM 的存储层我用的是简单的关系型数据库应用场景决定了表不会特别复杂但有两个设计习惯很想分享。第一是任务表会冗余资源规格快照。也就是说申请任务里除了关联资源 ID还会把申请时的机器配置、网络配置、标签等都存一份 JSON 快照。这样做是为了避免之后配置表被修改时历史任务连自己当初申请的是什么都不知道。查历史问题、对账、成本分析时任务表自己就是一个完整的可回溯事实源。第二是所有变更都走追加写。任务状态变更、审批操作、Agent 结果上报统一写进一张 event_log 表。不是所有系统都适合这种设计但治理类系统非常适合因为追责和审计是这个系统的核心职责之一。后面出任何纠纷我们都只查 event_log不信任任何其他表里的当前值。这段实现代码虽然不多但调试和打磨花了很长时间。尤其是并发场景下同一个任务被调度器多个副本同时拉取的问题。我用了数据库行级乐观锁任务表加一个 version 字段更新时必须带上旧的 version如果更新影响行数为 0说明有其他调度副本抢先处理了当前副本直接放弃。4. 上线前两周踩过的坑从重试风暴到时钟漂移凡是做过这种基础设施项目的人都知道真正让系统变可靠的往往不是初始设计而是测试和演练阶段逼出来的修复。MARM 在上线前两周的各种测试里我前前后后修了十几个问题其中三个印象最深拿出来单独讲讲希望能帮你避开。4.1 重试风暴没有退避机制的定时重试让数据库连接被打满第一次压测模拟大量 Agent 不可达时调度器疯狂重试任务而且由于没有按异常类型区分重试策略所有失败任务都在同一个时间窗口重新入队。结果数据库连接池被瞬时打满正常任务也挤不进来系统的表现比不重试还差。排查过程不算复杂压测时观察监控面板发现数据库连接数曲线是陡峭的锯齿状每次重试周期一到就冲顶配合任务队列积压数一起看基本能确认是重试节奏的问题。修复方式是在失败重试里加入指数退避和随机抖动基础时间 1 秒每次翻倍上限 5 分钟再加上一个 0 到 3 秒的随机偏移。改完之后重试压力平滑了很多再也没出现连接泄漏式的尖峰。提示重试一定要区分“值得重试”和“不该重试”。超时、网络抖动值得重试参数错误、配额不足、鉴权失败属于确定性失败越早标记失败越好。4.2 回调丢失异步通知比轮询更省资源但前提是可靠最初版本里 Agent 完成资源创建时是通过 MQ 消息回调通知控制面的设计上没问题但测试时发现偶尔会有消息丢了任务卡在 provisioning 状态迟迟不走要靠人工手工修改状态。定位后发现是消息生产方在发送前检查了资源状态但发送时连接异常消息根本没发出去也没有本地补偿机制。我没有选择费劲去完善消息可靠发送链路而是调整了策略把所有任务的状态推进改成“调度器定时拉取为主回调消息只作为加速手段”。这样一来即使回调彻底丢失调度器最迟在一个轮询周期内会发现任务状态其实已经 running自动修正任务状态。系统对消息通道的依赖降级为优化而非必须可靠性提升了一个量级。4.3 时钟漂移一个隐藏很深的“时间旅行”问题这个坑可能只有做调度类系统的人才会遇到。我们有一个定时检查资源过期的 Worker轮询到快到期的资源会发送提醒邮件。测试时发现偶尔有资源还没到期就收到了提醒时间误差大概几分钟。后来查下来不是代码逻辑错而是部分测试机系统时间漂移了和主数据库服务器的时间差了 3 分 20 秒。修复也简单所有 Agent 启动时强制同步时间并在关键时间判断处统一用数据库时间而不是业务机器本地时间。这个坑提醒我在分布式系统里时间是极其基础又极其脆弱的假设不要想当然认为所有节点的时间是一致的。把这三个问题摆在一起看你会发现它们本质上是同一个主题对失败模式的准备不足。网络会断、消息会丢、机器时间会偏、下游会超时这些都是常态而不是异常。MARM 之后的每次改动我都会在心里跑一遍“如果这里失败了会发生什么”这个习惯让系统在正式环境里少出非常多事。5. 压测与故障演练复盘这些数据让团队认可了 MARM功能跑通后最需要说服人的就是稳定性和抗压能力。当时团队里有人觉得“又多了一个要维护的系统”也有人在质疑迁到 MARM 会不会影响现有业务的线上稳定性。为了回应这些质疑我组织了一轮非常贴近真实环境的压测和故障演练最终用数据把大家说服了。5.1 压测配置与结果压测环境是 3 台 4C8G 的虚拟机跑控制面1 台 4C8G 跑数据库每个资源类型挂 2 个 Agent 实例。模拟的负载有两种一种是大量短时任务并发提交另一种是持续的中等负载混合长时任务。关键结果如下指标结果说明并发提交任务量500 个/秒网关加限流后每秒进入调度器的任务数控制面 P95 响应时间180ms排除 Agent 执行耗时后仅控制面处理时间任务从提交到 Agent 收到指令的延迟P95 1.2s含审批模拟、调度排队、入队出队长时间运行内存占用控制面进程约 700MB任务快照缓存在内存里可接受数据库连接数峰值 74远低于上限 200Agent 批量执行 100 台机器创建平均总耗时 23 分钟每台机器实际创建时间约 13 分钟其余为排队最有参考价值的数据是第一行500 并发提交时控制面没有出现明显排队拥塞。这说明对于内部资源的申请频率MARM 的控制面设计是远远够用的瓶颈依然在底层云 API 的创建速度上这是合理的因为资源创建的物理时间就摆在那里。5.2 故障演练场景与表现压测通过之后我又做了几轮故意破坏式的故障演练这里列一下场景和结论杀掉某个 Agent 进程 30 秒调度器发现 Agent 探活失败任务进入短暂重试等 Agent 恢复后自动续跑没有任务失败。模拟底层云 API 持续 2 分钟返回超时任务重试了约 4 次后进入 failed 状态触发管理员通知没有无限重试也没有卡住队列。数据库主从切换切换期间调度器整体停止约 20 秒恢复后从 Wal 日志追平任务状态没有丢失。断网 5 分钟再恢复所有代理重新注册控制面触发了节点状态对账自动发现并补偿了一些状态不一致的任务。这几个演练里最让我感慨的是最后一项。断网恢复后有一个任务在断网期间其实已经创建成功但控制面的状态还停在 provisioning。对账机制发现 Agent 本地记录了 execution_id 对应的资源已经 running于是自动把任务状态修正为 running。这个过程没有人工参与恰恰是幂等设计和状态对账机制的价值所在。故障演练的结果以周报形式发给团队后有几个之前犹豫的同事主动来找我说“这个确实能解决问题”。他们最认可的不是架构多优雅而是出了问题系统知道自己该怎么办而不是卡在那儿等人处理。6. 如果再来一次架构演进方向与给后来者的建议项目上线之后MARM 稳定运行了大半年期间只出过两次小问题都是 Agent 里适配的第三方系统接口变更导致的控制面本身没有因为并发或状态错乱出过大故障。如果现在让我重新做一遍或者继续迭代我会在以下几个方向投入更多也给正在做类似系统的人一些建议。第一从一开始就把“成本归属”和“预算管理”做进去。MARM 目前能管住资源生命周期但成本分账做得比较粗糙每个业务组上报的成本依赖云账单标签而不是由 MARM 主动记录每次资源创建对应的成本估算值。如果早期就在资源规格快照里同步记录定价模型月度成本报表可以直接从 MARM 出准确性会高很多。第二权限模型值得投入更多。我最初只做了简单的角色加资源组的 RBAC后来业务多了发现跨组共享资源、临时授权、分级审批这些需求层出不穷。如果当时就把权限设计成策略可编程的模型后面扩展会容易许多。不过话说回来等功能真的长出来再重构权限也是常态刚开始做太复杂反而会拖慢进度。第三统一事件总线的抽象。MARM 目前所有事件都写进了 event_log 表但下游系统如果要订阅资源变更事件只能轮询查询接口体验不够好。后续我会考虑在 event_log 之上再加一层轻量级事件发布订阅机制让业务系统可以通过 MQ 直接消费自己关心的变更而不需要反复查询 MARM 的接口。最后给想复制这个项目的人一个具体的起步建议不要一上来就追求“全自动”。选一种你最痛、频率最高、又不那么危险的资源类型做试点比如测试环境的一次性机器。把这条链路完整跑通包括申请、审批、创建、状态回写、到期回收。链路通了系统能真实创造价值了再逐步扩展到更多资源类型。我见过太多类似的内部平台死在“想一口气管所有资源”的宏大设计上。慢慢来把小闭环做扎实比什么都重要。