ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从零自研轻量级调度内核ax:定时任务治理与高可用实践

从零自研轻量级调度内核ax:定时任务治理与高可用实践 搞定时任务的人应该都有同感Cron表达式写起来不难难的是任务一多调度就变成了另一门学问。去年我们在重构数据平台的时候调研了一圈开源调度框架最后反而决定自己动手写一个轻量级的调度内核项目代号就叫ax。它全称是Async eXecution意思是异步执行后来平台上的同事习惯叫它“ax调度”。这套东西解决的不只是“到点跑一下任务”这么简单而是把定时触发、超时控制、失败重试、暂停恢复、执行器注册、任务依赖这些事统一成了一个可治理的体系。如果你也是后端开发、运维或者数据工程师正在纠结怎么处理一批批定时任务和异步任务这篇文章把 ax 从设计到落地的完整过程拆开给你看包括那些只在线上踩过坑才说得清楚的细节。1. ax 是什么一次被逼出来的自研调度器1.1 为什么会叫 ax名字这个事情其实很随意。当时内部立项写文档需要给调度内核起个短名字我随手写了ax全称 Async eXecution寓意是把所有需要异步执行的逻辑收拢到一个统一入口。后来“ax调度”这个叫法慢慢传开了大家发现念叨久了还挺顺口。名字背后代表的思路才是关键与其让每个业务各自实现定时任务、各自做失败重试不如让调度这件事变成一个平台能力。所以 ax 从一开始就不是某个单一脚本而是一套包含注册、触发、执行、回写、告警的最小闭环。为什么强调“最小闭环”这是吃过亏才总结出来的。以前项目里的定时任务散落在各个服务里有的是 Spring Scheduled有的是自己写死循环加 sleep还有的直接丢进 crontab。表面看都能跑真正出问题是三件事一是任务一多互相抢资源、日志混乱出了故障根本定位不了二是重试逻辑各写各的有人重试三次有人重试十次有人压根不重试三是没有统一的暂停和恢复手段想临时停一个任务只能去改代码或者注释 Cron 表达式。ax 要做的就是把这些散装能力收敛起来让调度从“人人都会写”变成“平台统一治理”。1.2 用它解决了哪些具体问题ax 在平台上线之后最直观的变化是任务从“多而乱”变成了“有状态、可监控”。具体来说它解决了四类高频问题。第一类是定时任务不可见。以前任务跑没跑、跑多少次、每次多久只能靠日志猜。ax 给每个任务分配了唯一 ID 和完整的生命周期状态任务从注册、等待、触发、运行到结束每一步都能查到记录。第二类是失败重试不统一。ax 在任务定义层内置了重试次数、重试间隔、退避策略业务方不用自己写 try-catch 重试逻辑只需声明“这个任务最多允许失败三次每次间隔 30 秒”即可。第三类是任务波动导致的雪崩。多个任务同时到点触发数据库瞬间涌进几十个大查询下游服务直接被压垮。ax 在触发侧加上了队列缓冲和线程池限流把同时触发的任务摊平到可控速率内。第四类是运维操作缺失。ax 支持暂停、恢复、立即执行、终止运行中任务这些操作在管理后台点一下就行不需要发版也不需要改配置。尤其是“立即执行”这个功能上线后救过很多次场业务同学发现数据算错了修正后不想等下一个周期直接手动触发一次就行。1.3 适合什么人参考如果你手里的系统只有三五个定时任务用 Spring Scheduled 或者 crontab 完全够用不建议引入一套调度系统那是给自己找麻烦。但如果你和我当时的情况类似任务量超过几十个多个服务都要跑周期任务对失败重试、暂停恢复、执行记录有要求同时还想把手工运维工作减下来那 ax 这套设计就很有参考价值。文章里涉及到的实现思路和代码适合负责基础组件或者中间件开发的工程师参考也适合想搞清楚调度系统内部原理的应用开发者阅读。后文涉及到的技术栈是 Go 语言、MySQL 和 Redis不过核心模型与设计思路本身是语言无关的你用 Java、Python 也可以对照落地。2. 整体设计调度器最核心的几个取舍2.1 自研 vs 开源调度框架当时我们真正调研过的候选方案包括 Quartz、XXL-JOB、Elastic-Job 以及后来了解的 Temporal 类工作流引擎。每个方案都有自己的优势但最终放弃它们不是因为它们不成熟而是因为我们的需求点比较刁钻。需求第一点是“任务依赖”。数据平台里有不少任务是分层的上游同步任务跑完下游清洗任务才能跑清洗跑完报表聚合才能跑。Quartz 本身不做这种 DAG 依赖XXL-JOB 的依赖支持也相对有限。需求第二点是“暂停粒度”。我们希望可以暂停某个任务但不影响它所在分组里的其他任务同时还要能暂停一个分组下的所有任务。开源框架在这块的操作粒度往往在设计上就固定了想改成我们需要的模式得动不少源码。需求第三点是“执行器轻量”。有些任务的执行逻辑就是一个 HTTP 回调或一个 RPC 请求我们不想在每个业务容器里都部署一套沉重的 SDK 和线程池。自研意味着对这些需求拥有绝对控制权也意味着要把调度系统最麻烦的那几个问题自己扛下来。当时我评估了一下团队情况核心成员对时间轮、分布式锁、状态机这些技术都有实战经验加上 ax 只做“准实时调度”不追求毫秒级精确风险是可控的。2.2 中心化调度与执行器模型ax 的架构采用了经典的“中心调度 轻量执行器”模型。调度中心负责任务元数据管理、触发时间计算、状态流转和负载分配执行器是部署在业务侧的一个小客户端职责只有一个收到触发指令后执行注册好的任务逻辑并把结果回传。为什么没把调度逻辑直接塞进业务进程里这是从故障隔离的角度考虑的。如果每个业务进程既要处理业务请求又要负责调度决策一旦调度逻辑卡死业务线程池也会被拖垮。反过来把调度中心独立出去之后即使执行器全部宕机调度中心依然能感知并告警而不会跟着一起崩溃。执行器非常轻量内部就三个核心组件注册器、任务执行线程池、结果回传通道。注册器负责向调度中心上报自己的地址和可用状态任务执行线程池负责真正运行业务逻辑结果回传通道把成功或失败的状态同步给调度中心。业务方接入时只需要提供任务名称和对应的执行函数剩下的调度、重试、超时逻辑由 ax 平台统一处理。2.3 为什么我不一开始就上分布式锁抢占很多调度系统喜欢用“多调度器实例 分布式锁抢占”的方式来保证高可用多个调度器节点同时活着谁抢到锁谁就负责派发任务。这种方案本身很成熟但我决定 ax 的第一版不这么做原因很现实为了避免“分布式锁过期导致双主同时调度”这种经典坑。用 MySQL 行锁做分布式锁时如果持有锁的节点执行时间稍长锁事务超时自动释放另一个节点就会拿到锁结果两个节点同时派发同一批任务。要规避这个问题需要引入 fencing token 或续约机制复杂度立刻上来了。ax 第一版采用了更朴素的“主备选举 虚拟 IP 漂移”方案两台调度中心一台为主一台为备主节点通过心跳维护自己的活跃状态备节点实时同步任务快照但不参与派发。主节点故障时虚拟 IP 自动漂移到备节点备节点接管调度。这个方案牺牲了双活的资源利用率但换来了极高的确定性。对于任务调度场景宁可让资源利用率低一点也不能出现同一条任务被两个节点同时派发。等 ax 运行稳定后后续再演进到真正的分布式抢占也不迟但第一版必须稳。2.4 任务状态机整个系统的骨架ax 里最核心的抽象不是 Cron 表达式而是任务状态机。每个任务在任何时刻都处于一个明确的、可查询、可流转的状态。我把任务状态设计成八种状态含义可流转目标WAITING任务已注册等待首次触发时间SCHEDULED、PAUSEDSCHEDULED已计算出执行时间等待下发RUNNING、FAILEDRUNNING已下发给执行器正在执行SUCCESS、FAILED、RETRYING、KILLEDRETRYING执行失败等待重试RUNNING、FAILEDSUCCESS执行成功等待下次触发SCHEDULEDFAILED超过重试次数任务异常终止RUNNING手动重跑PAUSED被主动暂停不参与调度WAITING、RUNNINGKILLED运行中被强制终止WAITING为什么状态机这么重要因为没有状态机报错和运维操作都无从谈起。比如业务方反映“这个任务今天怎么没跑”如果有状态机可以直接看到任务卡在 RUNNING 还是进了 PAUSED一眼就能定位方向。每次状态流转都会写入历史表等于给任务做了完整审计日志。设计状态机时有一个教训一开始我把 SUCCESS 直接当作终点状态后来发现定时任务根本没有终点成功的任务还要按 Cron 周期进入下一次调度所以必须加一条从 SUCCESS 回到 SCHEDULED 的流转路径否则整个调度链路就断了。3. 关键实现从建表到跑通一次调度3.1 任务注册与存储表结构ax 的任务元数据存在 MySQL因为任务本身的量级不算海量MySQL 足够可靠而且方便做条件查询和排序。我设计了两张核心表一张保存任务静态配置另一张保存调度的动态状态。第一张表task_def负责存任务的“定义”也就是它是什么、什么时候跑、怎么重试CREATE TABLE task_def ( id bigint unsigned NOT NULL AUTO_INCREMENT, task_name varchar(128) NOT NULL COMMENT 任务唯一名称, task_group varchar(64) NOT NULL DEFAULT default COMMENT 所属分组, cron_expr varchar(64) DEFAULT NULL COMMENT cron表达式与delay_ms二选一, delay_ms bigint DEFAULT NULL COMMENT 延迟任务延迟毫秒数, trigger_type varchar(16) NOT NULL COMMENT cron/delay/manual/dependency, timeout_sec int NOT NULL DEFAULT 300 COMMENT 执行超时时间, retry_count int NOT NULL DEFAULT 0 COMMENT 最多重试次数, retry_interval_sec int NOT NULL DEFAULT 30 COMMENT 重试间隔, callback_type varchar(16) NOT NULL DEFAULT http COMMENT http/function, callback_addr varchar(512) NOT NULL COMMENT 回调地址或方法名, status tinyint NOT NULL DEFAULT 1 COMMENT 0停用 1启用 2暂停, updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_task_name (task_name), KEY idx_next_trigger_time (next_trigger_time) ) ENGINEInnoDB COMMENT任务定义表;第二张表task_runtime负责存任务的“动态状态”也就是当前调度到哪一步了CREATE TABLE task_runtime ( id bigint unsigned NOT NULL AUTO_INCREMENT, task_def_id bigint NOT NULL, state varchar(16) NOT NULL COMMENT 状态机当前状态, current_ticket varchar(64) DEFAULT NULL COMMENT 调度票号用于幂等, next_trigger_time bigint DEFAULT NULL COMMENT 下次触发时间戳ms, last_trigger_time bigint DEFAULT NULL, last_end_time bigint DEFAULT NULL, last_executor varchar(128) DEFAULT NULL, execute_times int NOT NULL DEFAULT 0 COMMENT 累计成功执行次数, fail_times int NOT NULL DEFAULT 0 COMMENT 累计失败次数, version int NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, PRIMARY KEY (id), UNIQUE KEY uk_task_def_id (task_def_id), KEY idx_next_trigger_time (next_trigger_time) ) ENGINEInnoDB COMMENT任务运行状态表;表结构里的 details 看着简单但有三个地方是在生产环境里反复调整出来的。第一个是current_ticket字段它存着每次调度的唯一票据执行器回调时必须携带它调度中心拿到票据后才知道回传的数据对应哪一次调度而不是靠猜。第二个是version字段我用乐观锁防止两个线程同时对同一任务做状态流转时互相覆盖。第三个是把next_trigger_time建了索引这个索引直接决定了调度扫描的效率千万不能省。3.2 调度循环时间轮替代五分钟扫表最朴素的调度实现方式是每隔一分钟扫一次表找出next_trigger_time小于当前时间的任务然后逐条下发。这个实现起来很容易但存在两个问题。一是轮询间隔大任务最多会有一分钟的触发延迟对某些场景来说太慢二是每次扫表都要用SELECT ... FOR UPDATE这类锁行操作任务量大时会拖垮数据库。ax 用了时间轮做内存级触发数据库表只作为持久化兜底。主节点启动后会一次性把未来 5 分钟内需要触发的任务加载进时间轮时间轮每秒走一格到点后把任务提交给派发线程池。每隔 30 秒主节点再从数据库把新的五分钟窗口任务增量加载进来。为什么用五分钟窗口加载而不是一次性加载全部因为很多任务的 Cron 周期是小时或者天一次性加载未来一周的任务会让时间轮无效占用大量内存而且任务状态可能在下一秒就变化了比如被暂停缓存里的旧状态反而会造成误触发。五分钟窗口是一个折中既能保证准实时触发又能保证内存里的任务状态不会太陈旧。调度主循环的伪代码如下func (s *Scheduler) run() { ticker : time.NewTicker(1 * time.Second) defer ticker.Stop() for { select { case now : -ticker.C: s.rollTo(now) // 推动时间轮 s.loadFiveMinWindow(now) // 补齐近5分钟任务 s.dispatchDueTasks(now) // 派发到点任务 case -s.ctx.Done(): return } } }这个主循环看起来简单实际工程里关键在dispatchDueTasks内部要做状态校验和幂等保护。每个到点任务派发之前都要执行这样一段逻辑先把状态从 SCHEDULED 原子地流转成 RUNNING只有流转成功才允许派发如果流转失败说明这个任务已经被其他线程派发过了直接丢弃本次触发。这正好利用了前文说的version乐观锁避免同一个时间点重复派发。3.3 触发执行与结果回写调度中心算好时间通过 HTTP 回调或 SDK 下发指令给执行器。指令内容包含三件东西任务 ID、调度票据 current_ticket、以及本次调度的超时时间。执行器收到指令后会先用票据检查自己有没有处理过这笔请求处理过就直接返回上一次的结果防止网络重试导致的副作用叠加。执行器本身的实现围绕一个核心线程池展开。线程池大小不能盲目配置我根据线上机器规格总结过一个经验公式核心线程数 CPU 核数 * 2最大线程数 核心线程数 * 2队列容量不宜太大200 到 500 之间比较合适。因为调度任务大多是短时计算或接口调用队列太长反而会造成“任务明明提交了却迟迟不执行”的假象。执行完成后执行器要把结果回传给调度中心。回传的数据格式包括任务 ID、票据、状态成功或失败、耗时、错误信息。调度中心收到回传后做两件事。第一件事把任务状态流转到成功或失败第二件事计算下次触发时间把next_trigger_time更新到表里。这里有一个细节如果是重试场景调度中心会先把状态流转成 RETRYING再把原任务以延迟消息的形式塞回时间轮而不是立即重新调度这样能避免失败任务和正常任务抢同一个派发线程池。3.4 超时、重试与熔断配置超时控制是我认为整套系统里最容易写错的地方。很多人在执行器里直接用协程跑任务跑完就算完根本不设置超时。但实际线上任务经常出现“既不成功也不失败就是一直挂着”的情况比如调用的外部接口停住不动线程池被占满后续任务全部排队。ax 对超时的处理分两层。第一层在调度中心每个任务配置了timeout_sec到点还没收到回传调度中心直接标记该次执行为超时失败并触发重试逻辑。第二层在执行器执行器收到指令时会创建一个带超时上下文的执行环境业务逻辑里如果主动检查上下文就能实现协作式取消。两层都做是为了避免只有一层时出现误判。重试间隔我用了固定间隔加指数退避混合策略。前两次重试用固定间隔比如 30 秒从第三次开始间隔变为上一次的两倍但上限设置为 10 分钟。这样设计的理由很直观任务刚挂时重试频繁一点可以快速恢复但连续失败多次说明可能是下游系统出问题了盲目高频重试只会加重下游负担。熔断是针对“短时间大量失败”的保护措施。如果某个任务的失败率在五分钟内超过 50%调度中心会将该任务的派发频率降级为原来的十分之一同时发送告警。有人会觉得降级会影响业务恢复速度但实测下来熔断带来的保护远大于那点延迟代价很多下游系统就是这么被保住的。3.5 执行器注册与心跳执行器不是永久有效的它会随时扩容、缩容或重启。ax 采用注册加心跳模式管理执行器生命周期。每个执行器启动时会向调度中心发送注册请求上报自己的 IP、端口、支持的协议和可用任务列表。注册成功后执行器每隔 10 秒发送一次心跳调度中心根据心跳更新执行器的最后在线时间。调度中心不会向超过 30 秒没有心跳的执行器派发新任务但也不会立即把它从注册表里删除这个事件会给到告警让值班同学决定是否下机。为什么 30 秒就停止派发因为心跳间隔是 10 秒连续三次心跳丢失基本可以判定执行器不可用再往下派只会造成请求石沉大海白白消耗线程池。执行器弹出的任务会被重新标记为 SCHEDULED等执行器恢复后自动进入下一次触发周期。注册机制还有一个容易被忽略的点任务幂等注册。执行器重启后重复提交注册请求很正常调度中心必须按“任务名 执行器地址”做去重否则会出现一批幽灵执行器记录导致调度中心把任务随机派发到一个已经下线的节点上。4. 真正上线前必须处理好的边界场景4.1 幂等与重复执行调度系统最严峻的考验不是跑不起来而是“同一个任务被同时跑了两遍”。触发瞬间机器卡顿、网络超时后客户端重试、主备切换导致重复派发这些都会造成重复执行。ax 用三把锁从源头控制重复。第一把锁是任务状态机的乐观锁只有 SCHEDULED 状态的任务才能被流转为 RUNNING状态流转失败说明已有另一个节点在派发。第二把锁是票据去重每次调度生成唯一的 current_ticket执行器本地缓存最近处理过的票据重复指令直接丢弃。第三把锁是业务侧幂等约定文档里明确规定任务逻辑必须实现幂等键调度中心会把票据字符串透传给业务函数。这三把锁缺一不可。状态机锁防的是调度中心内部并发票据去重防的是网络重传业务侧幂等防的是最终执行那一下的边界。有一次线上事故就是调度中心主备切换后票据丢失业务侧又没有处理幂等结果账务任务重复执行当天对账差了十几万。从那以后ax 文档里把“业务幂等”从建议改成了强制。4.2 任务暂停、恢复与立即执行暂停和恢复这两个操作看起来只是改一个状态字段实际实现里要小心的是“暂停瞬间正在运行的任务怎么办”。ax 的处理策略是暂停只影响未来的触发不中断当前正在运行的实例。因为强行中断运行中的任务可能导致数据处于半写状态风险远大于让它自然结束。如果用户希望暂停立刻生效需要手动操作“终止运行中任务”这才会向执行器发送 kill 指令。立即执行的设计也踩过坑。如果用户手动触发一个任务时该任务刚好处于 RUNNING 状态直接再触发就会撞上乐观锁。ax 的处理是为手动触发生成一个临时任务实例使用独立的票据不占用定时任务的状态机。这样手动触发和周期触发天然隔离互不影响。4.3 高可用与故障转移ax 的高可用方案虽然朴素但非常管用。调度中心采用主备部署主节点挂着共享元数据库备节点启动后会持续读取任务快照到本地内存但它不派发任务只提供一个只读查询接口方便排查问题时查看任务状态。主节点每隔 5 秒写一次租约记录到数据库备节点通过租约超时感知主节点故障然后升级为主。故障转移有一个隐藏坑备节点升级成主节点后它内存中的时间轮窗口可能已经过期如果直接开始调度近几分钟的任务会全部错过。所以备节点升级的第一步不是派发任务而是先从数据库重新加载当前时刻后五分钟的任务窗口确保时间轮处于新鲜状态再开始调度。这个细节花了一个通宵才定位清楚当时现象是主节点宕机后任务恢复得很快但是宕机前五分钟内应该触发的任务全部安静地消失了。4.4 依赖触发与 DAG 优先级ax 的依赖触发没有做复杂的 DAG 引擎而是用“上游完成事件 下游条件检查”的方式实现。每个任务定义里可以声明依赖条件比如“依赖 task_sync_data 的成功事件”。当上游任务执行成功时调度中心会检查所有依赖它的下游任务把满足条件的下游任务从 WAITING 状态流转为 SCHEDULED并立即加入时间轮。依赖触发的优先级如何保证ax 内部给任务分了五级优先级数值越小优先级越高。调度线程池从队列取任务时按照优先级排序同优先级按触发时间先后排列。为什么不用带权重的随机调度因为任务依赖场景下上游同步任务通常要优先于下游聚合任务如果同步任务被聚合任务卡在后面整条链路都会延迟。优先级队列让调度结果可预期排查问题时也更容易解释“为什么先跑这个后跑那个”。5. 常见问题与排查技巧实录5.1 任务不触发但状态却是 SCHEDULED这个场景出现时第一反应不是去查业务代码而是查调度中心的时间轮窗口。我遇到过好几次原因都是主节点重启后备节点虽然成功升级但内存中的时间轮窗口加载失败任务一直停留在 SCHEDULED 状态没有进入派发队列。排查方法很直接查看调度中心的日志确认任务是否进入了 dispatchDueTasks 的执行路径再看派发线程池的活跃线程数如果线程池全在忙状态 SCHEDULED 就是正常的排队状态等线程释放自然会跑。另一个高频原因是配错 Cron 时区。ax 默认使用 Asia/Shanghai 时区但任务创建者可能在 Cron 表达式中混入了 UTC 时间导致任务触发时间和预期相隔八小时。我的排查习惯是先在管理后台看一眼任务最近十次触发时间的命中记录如果全部偏移优先检查时区配置。5.2 任务重复执行重复执行排查顺序老三样先查调度中心日志里同一个票据是否派发了两次再查执行器日志里同一个票据是否收到了两次最后查业务数据库里是否同一个幂等键被消费了两次。如果前两样都没问题基本就是网络重传导致执行器收到了两次指令而执行器的票据去重缓存恰恰没有命中。这里有一个容易忽略的坑执行器的票据去重缓存不能无限膨胀必须设置过期时间。我当时设置了 24 小时过期结果发现过长的缓存反而引入了 bug任务 A 的票据在第二天因为某种原因再次出现时执行器直接把它当成重复请求丢弃了。后来统一调整为“票据过期时间等于该任务最长可能重试周期 5 分钟”问题才彻底解决。5.3 任务卡在 RUNNING 状态任务卡在 RUNNING 状态绝大多数是执行器的线程池被占满任务排着队就是没有线程执行。这时候不要重启调度中心那没用要去看执行器当前的线程池指标。如果线程池全忙且队列堆积说明下游耗时远超预期。先人工终止当前运行中的实例再评估是否需要调超大线程池或者给该任务单独安排执行器资源。还有一类卡死的诱因是执行器内部发生了死锁很多业务代码里“任务内又调用同一个任务”导致递归等待。ax 在执行器 SDK 里特意加了一层“任务级重入保护”同一个任务在同一个执行器内再次被请求时直接拒绝避免了这种低级但致命的死锁。5.4 高峰批量任务把下游系统打爆大量任务同时到点触发是调度系统最大的灾难场景。现象就是一秒内派发出去几百个请求下游数据库连接池满redis 连接异常整个集群抖动。应对这个问题的核心机制是派发侧限流。ax 在派发线程池前加了一个令牌桶每秒最多派发 N 个任务N 的取值根据下游系统承载能力动态配置。比如下游数据库只能抗住每秒二十个重查询令牌桶容量就设成二十。任务从时间轮到派发队列之间再加一个缓冲队列超过令牌桶速率限制的任务在缓冲队列里等待而不是直接丢弃。这样处理的好处是高峰期的任务不会一次性全部压向下游同时任务不会丢失最多就是触发时间延迟几秒到几十秒。5.5 快速排查速查表现象最可能原因第一排查动作任务不触发状态 SCHEDULED时间轮窗口未加载或线程池排队查调度中心派发日志和线程池指标任务不触发状态 WAITING依赖上游任务未成功检查被依赖任务的状态任务重复执行票据去重缓存失效或业务幂等缺失查票据派发日志和业务幂等键任务全部延迟派发令牌桶限流生效查令牌桶参数与下游负载任务直接 FAILED 无重试重试次数配置为 0查任务定义 retry_count执行器心跳正常但任务不分配执行器可用列表中被剔除查执行器最后在线时间和剔除原因手动触发无效触发了乐观锁或任务处于 PAUSED查看任务状态与手动触发独立票据我自己在排查问题的时候最常用的一招是直接翻任务动作记录表。ax 每一次派发、回传、重试、暂停、恢复操作都会生成一条记录看动作记录往往比看业务日志更快定位问题尤其是那种“偶尔出现、然后消失”的玄学问题动作记录基本不会骗人。6. 最后再说点个人体会ax 这套调度系统从上线到现在稳定运行了大半年中途经历了几次大版本迭代。我最大的感受是做调度系统不要一上来就追求分布式和高可用先把单机版本的调度语义做正确把状态机理清楚把幂等和超时做扎实再往上叠高可用和扩展性会顺畅很多。很多人卡在调度系统这个坑里不是因为不懂时间轮或者分布式锁而是因为在朴素方案都没有验证可行的情况下直接引入了复杂方案最后连问题出在哪一层都找不到。另外如果让我重新选一次我依然会在第一版选择“主备 虚拟 IP”而不是“多活抢占”这个决定省下的排查功夫远比损失的那点资源利用率更值钱。记住一个原则任务调度这个东西少跑一次还能补救多跑一次可能就是事故。
返回列表