手撸一个简易分布式定时任务的记录

公司内部系统有个痛点:十几个 Go 服务各自跑着 cron,日志、监控、去重各玩各的,出了问题要去十几个服务里翻。申请专项做统一调度被拒了(理由:收益不明确),于是用了两个月的晚间时间自己撸了个最小可用版本,先在自己负责的服务里跑起来,跑通再说。这篇记录核心设计。 需求收敛。 首先想清楚不做什么:不做秒级调度(cron 表达式到分钟足够)、不做依赖编排(任务依赖用上线时间错开解决)、不做可视化界面(CLI + 配置文件够用)。砍完之后剩下的核心就三件事:时间触发、任务分发、不重复执行。 关键设计是抢锁。 多实例部署下同一个任务只能有一个实例执行,我们没有引入额外的调度中心,而是直接用 MySQL 的行锁: -- t_job 表,每分钟由各实例尝试 CAS UPDATE job_lock SET owner = ?, locked_until = NOW() + INTERVAL 2 MINUTE WHERE job_name = ? AND (locked_until < NOW() OR owner = ?); 抢到锁的实例负责执行并把心跳写进去,锁过期自然释放。整个方案只依赖已经有的 MySQL,没有新组件。有人会问为什么不用 Redis:因为任务幂等性出错的代价是重复扣库存,而 MySQL 事务的可靠性在我们团队是经过验证的,不熟的东西不引入。 分片是第二个关键。 单机任务量上来之后,把任务按 jobName 的 hash % 实例数 分片,每个实例只认领自己的那份。简单,但配合上面的锁,二十万级任务/天的量完全够。 踩的坑:时钟漂移。有两台机器的 NTP 没配好,相差 40 秒,导致任务在整点附近被执行了两次。锁的有效期改成大于任务最长执行时间 + 漂移容忍之后解决。现在新机器进集群先校时,成了硬性流程。 现在这个调度器稳定跑了三个月,另外两个组也接进来了。回头看,“先做一个丑但能跑的版本,让收益自己说话"这个策略是对的——下个季度统一调度的专项预算,批了。

2024-12-03 · 芮靖安

iOS 组件化改造:从私有 pod 源开始

App 三岁了,单工程 40 万行代码,全量编译要 15 分钟,改一行代码喝杯水回来才能跑真机。终于说服老板让我们做组件化。这篇记录前期的方案取舍,给后来人省点力气。 先说结论:不要一步到位上"总线"架构。 网上流传的组件化方案大多是 CTMediator 那一派,运行时解注册表调用。演示很漂亮,但真落地会发现调用关系全靠字符串,IDE 跳转变瞎,重构基本靠全局搜索。我们最后的方案保守得多: 底层工具库(网络、缓存、埋点)拆成独立的私有 pod,用 cocoapods-packager 发到自建的私有源; 业务模块先只在物理上拆目录,通过 protocol 文件互相依赖,暂时不做运行时解耦; 主工程退化为一个壳,只负责组装 tab 和路由。 这个方案跑了半年,编译时间从 15 分钟降到 4 分钟(业务模块可以用 pod install 之后按需引入调试),虽然没到网上吹的"秒级编译",但对日常开发体验的提升已经非常明显。 私有源的坑。 自建源用的是 nexus,权限管理一塌糊涂,最后是靠 git tag + CI 校验版本号一致性来保证不出乱子。后来听说公司内部有 Artifactory 的 license,就迁过去了,省心很多。建议准备做组件化的团队先解决制品库的问题,工具链不顺手,组件化推行不下去——这不是技术问题,是工程习惯问题。 下一阶段的计划是把业务模块间的 protocol 依赖收敛到一个独立的接口层仓库里,谁改接口谁发版。半年后 hopefully 能再写一篇续集,写写中间件方案到底要不要上。

2019-05-21 · 芮靖安