美文网首页
定时器在大型web项目中的应用和实现

定时器在大型web项目中的应用和实现

作者: 流星狂飙 | 来源:发表于2016-04-20 22:46 被阅读804次

    在大规模分布式系统中,每个业务都可能是集群,每个业务机都会产生定时任务,不同的业务会有不同的任务管理需求,统一的任务调度和管理变得非常有必要。

    1. 定时如何准确,大量的定时被同时触发怎么办?
    2. 定时结束的时候,怎么通知业务机去处理呢?
    3. 某台业务机下线了怎么办?
    4. 如何提供任务更新、删除功能?

    基本模型如下图:

    Paste_Image.png

    定时器在社会中有着广泛的应用,比如每天叫你起床的闹钟。在软件项目中,定时器也被应用到了各方各面,本文将从 web 项目入手,讲述定时器,本文的例子都以 node 为例。

    为什么要用定时器?

    没有什么比机器更加准时!在我接触单片机的时候,已经开始感叹,为什么机器时间可以做到这么准!

    比如文章的定时发布、商品的准点开始抢购、活动定时上下架,肯定不会是一个又一个管理员在后台帮你点击按钮,完成操作!系统的准时可以定位到毫秒级,虽然每个用户可能和服务器的时间不一致,秒级的差别还是在可接受范围的,但是在某些领域也会有很多精细到毫秒级的定时任务需求,比如航空航天、定时炸弹等等。

    定时器总类

    定时器有两种 intervaltimeout, 对应重复任务和一次性任务。在我的理解里,interval 任务只是在 timeout 的时候再次注册了本任务。

    // 重复性任务
    var timer = setInterval(function(){
     // do something
    }, milliseconds)
    
    // 一次性任务
    var timer = setTimeout(function(){
     // do something
    }, milliseconds)
    

    unix crontab 能解决问题吗?

    crontab 并不能精确到秒,crontab 的最小粒度是分,即当第一位是「*/1」时,即最小单位是每分钟执行,(不排除你们有奇淫技巧可以做到秒级控制的)。unix 本身支持强大的定时任务管理 crontab,定时的格式也是强大得令人惊叹。

    * * * * * *
    ┬ ┬ ┬ ┬ ┬ ┬
    │ │ │ │ │ |
    │ │ │ │ │ └ day of week (0 - 7) (0 or 7 is Sun)
    │ │ │ │ └───── month (1 - 12)
    │ │ │ └────────── day of month (1 - 31)
    │ │ └─────────────── hour (0 - 23)
    │ └──────────────────── minute (0 - 59)
    └───────────────────────── second (0 - 59, optional)
    

    1)Cron 表达式的格式:秒 分 时 日 月 周 年 (可选)。

    字段名 允许的值 允许的特殊字符
    秒 0-59 , - * /
    分 0-59 , - * /
    小时 0-23 , - * /
    日 1-31 , - * ? / L W C
    月 1-12 or JAN-DEC , - * /
    周几 1-7 or SUN-SAT , - * ? / L C #
    年 (可选字段) empty, 1970-2099 , - * /

    「?」字符:表示不确定的值

    「,」字符:指定数个值

    「-」字符:指定一个值的范围

    「/」字符:指定一个值的增加幅度。n/m 表示从 n 开始,每次增加 m

    「L」字符:用在日表示一个月中的最后一天,用在周表示该月最后一个星期 X

    「W」字符:指定离给定日期最近的工作日 (周一到周五)

    「#」字符:表示该月第几个周 X。6#3 表示该月第 3 个周五

    Cron 表达式范例:

    每隔 5 秒执行一次:*/5 * * * * ?

    每隔 1 分钟执行一次:0 */1 * * * ?

    每天 23 点执行一次:0 0 23 * * ?

    每天凌晨 1 点执行一次:0 0 1 * * ?

    每月 1 号凌晨 1 点执行一次:0 0 1 1 * ?

    每月最后一天 23 点执行一次:0 0 23 L * ?

    每周星期天凌晨 1 点实行一次:0 0 1 ? * L

    在 26 分、29 分、33 分执行一次:0 26,29,33 * * * ?

    每天的 0 点、13 点、18 点、21 点都执行一次:0 0 0,13,18,21 * * ?

    每种开发语言都提供了 crontab 的相关封装,让开发者调用起来得心应手。以 node 为例:

    
    require('crontab').load(function(err, crontab) {
     // create with string expression
     var job = crontab.create('ls -la', '0 7 * * 1,2,3,4,5');
    });
    

    你在 github 搜索 crontab 能搜到主流语言的实现。

    有个问题,定时器不准时!

    setInterval 的回调函数并不是到时后立即执行,而是等系统计算资源空闲下来后才会执行。而下一次触发时间则是在 setInterval 回调函数执行完毕之后才开始计时,所以如果 setInterval 内执行的计算过于耗时,或者有其他耗时任务在执行,setInterval 的计时会越来越不准, 延迟很厉害。crontab 也是同样的原理。

    var startTime = new Date().getTime();
    var count = 0;
    //耗时任务
    setInterval(function(){
     var i = 0;
     while(i++ < 100000000);
    }, 0);
    setInterval(function(){
     count++;
     console.log(new Date().getTime() - (startTime + count * 1000));
    }, 1000);
    

    结果

    126
    176
    163
    112
    109
    107
    203
    189
    170
    

    当然,不排除你们有奇淫技巧可以做到秒级控制的。

    成千上万定时任务时怎么管理?

    Crontab 存在任务上限(其实我也不知道上限是多少,知道的麻烦告诉我),任务的同步、备份管理都比较麻烦,也会有比较多的并发问题需要处理。在分布式系统中,单独去部署一个定时任务机器也是可行的。不过任务调度、定时结束通知客户端也需要蛮多工作量的。

    unix 的 crontab 不再是我们的第一选择,每种编程可能都有定时任务管理的相关框架。比如 java 的 Quartz,Python 的 APScheduler。nodejs 的 node-schedule。但是这些东西是否能真的满足你的需求呢?

    So,我们需要一个定时任务管理平台。

    思路和实现

    目标

    1. 业务方可以定义定时时间、时间结束的触发任务
    2. 业务方可以更新或者删除已经发布的定时任务
    3. 定时任务管理平台统一接收和调度任务

    主要解决两个问题:

    1. 设置准确的定时时间
    2. 时间结束触发客户端,不能重复消费

    redis 在 2.8.X 版本可以开启了键空间通知,更多相关请移步 Redis Keyspace Notifications。(默认不开启,3.x 版本好像就失效了。),redis 支持的很多键空间事件,比如:DEL,RENAME,EXPIRE等等,redis 本身可以定义某个键的过期时间,ttl key

    这个值正好用来设置为定时任务的时间。更多相关请移步 Redis Keyspace Notifications。如果客户端订阅了某种规则的键通知,比如过期,那么在某个键过期的时候就会收到一个通知,这个事件就是定时结束,可以告诉业务机可以开启任务了。

    ** 可如果有多个 redis 客户端订阅了某个键的过期时间,那么任务还是会被触发很多次。** 因为每个客户端
    都是平等的,你能订阅,我同样可以订阅。解决办法就是 生产者和消费者模式。同一个过期消息只能被消费一次。

    重点来了

    把所有的定时任务按照定时开启的时间倒序排列,存入 sorted Sets , 把时间设置为 score。这样就会形成一个按照时间排好序的集合,可以按照时间先后依次取出所有的任务,需要新增和修改任务,也是可以通过 redis 的命令实现的。

    定时管理服务器每 1000ms 去取 sorted sets 顶部的数据,如果获取到的 task 离触发小于 1s,那么就可以执行 pop() 操作,表示这个任务开始被调度执行,因为 redis 的 pop() 是原子性的,同一个 task 永远只会被消费一次。这样就解决了 redis 键空间通知会被重复消费的问题。

    伪代码如下:

    var taskSorts = new Sets(task1, task2, task3); // 在 redis 中建立按时间排序的集合
    
    // 每隔一秒执行一下操作,
    var newOne = taskSorts.zrank(-1); // 获取到最快发生的任务
    if(newOne.time < 1000){ // 如果满足消费条件
     newOne = taskSorts.pop(); // 消费该任务,重复此循环,继续消费下一个任务
     setTimeout(function(){
     // dosomething
     }, newOne.time)
    }
    
    

    任务触发

    1. 任务的提交和触发都应该在业务方完成。定时任务管理平台只是帮助管理和调度任务。在定义的任务里面定义好任务执行的回调参数和接口。
    2. 客户端定义任务的时候,同时注册好定时结束的回调接口,或者应该在项目启动的时候,就注册好所有回调的接口。因为同一个业务的 A 机器提交了任务,触发的时候可能 A 机器下线了,只能定时任务平台只能去触发业务 A 的 B 机器了。
    3. 引入跨服务远程调用。业务和定时任务管理平台可能不在同一个机器,可能分布在不同的 ip。听起来很复杂,实际上跨语言的调用调用方式有很多,比如 REST API、消息队列、RPC。我的团队选择了 Thrift(Facebook 开源的,跨语言的,现在共享给了 Apache 基金)。以上的方式都可以实现任务只被触发了一次,远程通知给客户端(任务注册方)。

    成品 -- nodejs 的实现 cron-redis

    https://github.com/MZMonster/cron-redis
    主要依赖 bull 实现了任务队列的管理功能实现的定时任务管理工具。

    demo:

    // 就这样定义,3 秒钟之后,hello 函数将被执行。
    function hello (x, y){
     console.log(new Date());
     console.log(x + ' + '+ y +' = %s', x+y);
    }
    
    // 我是一个任务
    var task1 = {
     method: hello.name, // 任务回调的函数
     params: [2, 3], // 任务执行的参数
     rule: moment().add(3, 's').toDate() // 任务执行间隔,支持 crontab 格式
    }
    
    queue.register(hello)
    queue.publish(task1);
    

    如果你要求不高,unix 自带的 crontab 也足够你折腾了。使用 redis 来实现定时也是一种极好的思路,cron-redis 值得你去试一试。

    该库只是一个定时任务的库,实际上可以通过以上的思路实现微服务————定时任务管理平台。通过 cron-redis 组合远程服务调用 thrift、服务的注册发现工具 zookeeper,定时任务管理平台分分钟就被搭建了(等我下一篇文章吧,分分钟搭建微服务)。

    相关文章

      网友评论

          本文标题:定时器在大型web项目中的应用和实现

          本文链接:https://www.haomeiwen.com/subject/rmctrttx.html