北斗前端监控系统是 58 内部的一个线上质量监控排查解决方案,用于帮助用户大幅提升定位问题和优化项目的效率。系统共分为数据收集(SDK)、数据处理(Java)、数据存储(Druid、……)、数据分析(Node.js)、数据展示(React) 5 层模型。Node.js 作为系统中的数据分析层,提供各种数据分析和应用的方式。
在一期之后,系统的基础功能已经完备。平台可以收集 5 种类型、30 多种指标的数据,已经具备了很强的数据收集能力,数据应用的方式却很匮乏。
所以在二期开发时,我们计划在 Node 端加入多种数据应用的方式。实时告警,就是其中之一。
简单分析需求,服务端需要以一定的频次(例如每分钟)监测不同项目中用户配置关注的指标数值。当数值出现异常时,给用户发送邮件、短信等告警信息用于警示。
而其中的重点,就是如何在 Node.js 中设计并实现定时任务系统?
初期的定时任务,是为告警而开发的,所以场景较为简单。
由于指标已经固定,且当时没有告警外的其他应用场景,所以可扩展性不是影响系统的重要因素。而集团内部平台用户量级较小,安全、成本等其他因素也不会成为瓶颈。系统的复杂度主要来自于对可用性的支持。
作为一个稳定的系统是必须在集群内多个机器中运行的。分布式必然会带来额外的系统复杂度。
当集群内有多台服务器,我们如何保证定时任务可以由不同的机器不重复的执行呢?
大家可以很容易的想到 MQ(消息队列)和分布式锁。两者也都有相关的 Node.js 库,基于 MQ 的 node-kafaka 以及基于锁的 Redlock
在当时的场景下,我们需要低成本快速的上线告警系统。MQ 虽然功能完备,支持更为复杂的功能,但是相对于抢锁机制复杂度太高,在简单的场景下优势又不是很明显。所以最终选择基于 Redlock 的抢锁机制实现分布式定时任务。
这样我们就保证了分布式系统内定时任务只执行一次。
可以看到 1.0 版本的定时任务是单独为告警而开发的,此时的告警模块流程简单,且指标数量较少。系统内的生产者和消费者也没有进行解耦,整体流程是线性的。在当时的场景下,这种基于 Redlock 的定时任务方案完全可以支撑业务稳定运行。
随着平台的扩展,告警增加了自定义告警功能。定时任务也增加了采样分析、定时数据缓存、定时周报等大量定时任务的执行。此时,1.0 版本的定时任务系统开始涌现出各种问题。
随着系统的扩展,单独为告警而开发的定时任务运行着很多额外的模块。虽然整体流程还没有被阻塞,但是随着后续系统的扩展,必然会导致系统越来越不稳定。于是我们计划重构定时任务,使其能够作为一个通用的子模块,承载多样的定时任务。
1.0 时没有引入消息队列或任务队列来处理定时任务,是因为当时的场景下对保序、异常任务重试等特性没有强需求,接入任务队列后系统复杂度却会高很多。而 2.0 时作为通用模块,则需要一定的机制来保证这些特性,于是我们决定引入任务队列的概念来进行系统重构。
此时的系统结构如下:
生产者生产任务,会将任务推送到任务队列中,通过任务队列可以保证任务的有序性。
消费者异步按需的获取对应的 Job,也可以选择在空闲时再继续获取执行新的任务,而不是所有的定时任务堆积到系统中一起执行。解决了任务均匀下发,按需执行的问题。
通过任务队列,可以维护任务的执行状态。这样持久化存储或失败重试的功能也可以开发相应的功能去做扩展。
在业内 Node 端的任务队列也有很多完整的实现方案,且都经过了大量的场景校验,不需要我们去重复的造轮子。引用 bull 官方的一张对比表,我们整理部分对比如下。
Agenda 是基于 mongo 的任务队列解决方案,更偏向于定时任务。
Bee 是一个简单、快速的 Node.js 作业/任务队列,更专注于小粒度任务的处理,并极大的优化此场景下的性能,由 Redis 提供支持。
Bull 提供完整的任务队列解决方案,有较活跃的受众及系统更新频率。
对比其功能及差异,bull 是目前功能最完善的框架,社区也非常活跃,其丰富的事件回调也能支持我们对流程进行细粒度的把控。
最终在做了技术调研之后,新系统的重构方案就选择使用 bull 来作为基础框架,特定的功能场景下单独进行开发支持。
那么在 bull 中,任务是如何进行流转的呢?
bull 中分为生产者和消费者两种角色,使用 LPUSH/BRPOPLPUSH 命令来维护任务主队列,同时还加入了多种辅助数据结构,为系统丰富的功能做支持。
当生产者生成一个 Job 后,会尝试推送到 Redis 对应的任务队列中(List)。bull 会通过 JobId 区分是否重复推送了一个 Job,以此来实现任务去重功能。
消费者使用 BRPOPLPUSH 命令阻塞式读取任务列表,而不需要轮询的读取。当 List 中有新的 Job 后会将任务分配给消费者。通过配置并发数,也可以控制同一个消费者并发执行任务的数量。
当 Job 执行完成后,消费者给出反馈。此时 bull 更新任务队列中 Job 的状态,并触发对应的事件。
依靠其细粒度的状态存储记录,bull 也支持可靠的任务重试机制,当服务重启或任务执行失败时,bull 可以通过 Job 执行记录来重新执行未完成的任务,极大的提高了系统的可靠性。
可以看到,bull 与 2.0 的系统结构非常契合,我们只需要在此基础上进行二次开发就可以完成 2.0 定时任务系统的重构。
最终定时任务系统架构的设计如下:
生产者
消费者
到此,2.0 版本的定时任务系统就已经设计完成了。
纵观北斗监控系统中定时任务的发展。
初期由于功能简单,且需要快速上线。所以使用 Redlock 开发了基于抢锁机制的定时任务。
后续由于系统功能的扩展,需要支撑更多的定时任务。原系统中的弊端逐渐显露并对系统开发维护带来了困难,此时引入了消息队列的概念优化系统模型。
在持续的迭代中,系统功能也趋于稳定和完善。在目前的北斗监控系统中,定时任务系统支撑着实时告警、采样分析、定时数据缓存、定时周报等大量任务的稳定运行。
但这不是终点,后续一定会遇到更多的挑战,系统的架构也可能进一步升级。而我们需要做的就是拥抱变化,适配我们当下的业务场景去做合适的选择。
redlock:https://redis.com/redis-best-practices/communication-patterns/redlock/
bull:https://github.com/OptimalBits/bull
Redis 核心技术与实战:https://time.geekbang.org/column/article/284291
agenda:https://github.com/agenda/agenda
bee-queue:https://github.com/bee-queue/bee-queue
- END -
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8