daemon可以卸载吗


作者:刘若愚 腾讯WXG后台开发工程师

本文主要以作者的工作实践为基础,介绍如何使用平时部门最常用的组件快速实现一个业务常用的分布式定时器服务,并分享在此过程中遇到的一些解决方案。希望通过本文能给予类似场景提供一些解决思路。

一、定时器简介

定时器(Timer)是一种在业务开发中常用的组件,主要用在执行延时通知任务上。在指定的时间开始执行某一任务(也有周期性反复执行某一任务的Timer,我们这里暂不讨论)。它常常与延迟队列这一概念关联。

二、定时器的应用场景

我们先看看以下业务场景:

1. 当订单一直处于未支付状态时,如何及时地关闭订单,并退还库存?

2. 如何定期检查处于退款状态的订单是否已经退款成功?

3. 新创建店铺,N天内没有上传商品,系统如何知道该信息,并发送激活短信?

为了解决以上问题,最简单直接的办法就是定时去扫表。每个业务都要维护一个自己的扫表逻辑。当业务越来越多时,我们会发现扫表部分的逻辑会非常类似。我们可以考虑将这部分逻辑从具体的业务逻辑里面抽出来,变成一个公共的部分。这个时候定时器就出场了。

三、定时器的数据结构及操作

一个定时器本质上是这样的一个数据结构:deadline越近的任务拥有越高优先级,提供以下几种基本操作:Add 新增任务、Delete 删除任务、Run 执行到期的任务/到期通知对应业务处理、Update 更新到期时间。

Run通常有两种工作方式:

1. 轮询 每隔一个时间片就去查找哪些任务已经到期。

2. 睡眠/唤醒 不停地查找deadline最近的任务,如到期则执行;否则sleep直到其到期。在sleep期间,如果有任务被Add或Delete,则deadline最近的任务有可能改变,线程会被唤醒并重新进行查找。

四、定时器的设计目标及数据结构实现

定时器的设计目标通常包含以下几点要求:支持任务提交、任务删除、任务通知等基本功能;消息传输可靠性;数据可靠性;高可用性;实时性。

定时器通常与延迟队列密不可分,延时队列是什么?顾名思义它是一种带有延迟功能的消息队列。而延迟队列底层通常可以采用以下几种数据结构之一来实现:有序链表、堆、时间轮/多级时间轮等。

这里重点介绍一下时间轮(TimeWheel)。一个时间轮是一个环形结构,可以想象成时钟,分为很多格子,一个格子代表一段时间(越短Timer精度越高),并用一个List保存在该格子上到期的所有任务,同时一个指针随着时间流逝一格一格转动,并执行对应List中所有到期的任务。任务通过取模决定应该放入哪个格子。

五、基于定时器的业务实现

在我们组的实际业务中,有延迟任务的需求。一种典型的应用场景是:商户发起扣费请求后,立刻为用户下发扣费前通知,24小时后完成扣费;或者发券给用户,3天后通知用户券过期。基于这种需求背景,我们引出了定时器的开发需求。

我们首先调研了公司内外的定时器实现,避免重复造。最后从可用性、可靠性、易用性、时效性以及代码风格、运维代价等角度考虑,我们决定参人的一些优秀的技术方案,并根据我们团队的技术积累和组件情况,设计和实现一套定时器方案。

首先要确定定时器的存储数据结构。这里借鉴了时间轮的思想,基于微信团队最常用的存储组件tablekv进行任务的持久化存储。使用tablekv的原因是它天然支持按uin分表,分表数可以做到千万级别以上;其次其单表支持的记录数非常高,读写效率也很高,还可以如mysql一样按指定的条件筛选任务。

我们的目标是实现秒级时间戳精度,任务到期只需要单次通知业务方。故我们方案主要的思路是基于tablekv按任务执行时间分表。也就是使用方指定的start_time(时间戳)作为分表的uin,也即是时间轮bucket。为什么使用分表数进行Key收敛?主要是为了避免时间戳递增导致的key无限扩张的问题。通过mod分表数进行Key收敛的示例图如下所示:kv时间轮。

任务持久化存储之后,我们采用一个Daemon程序执行定期扫表任务,将到期的任务取出,最后将请求中带的业务信息(biz_data添加任务时带来,定时器透传,不关注其具体内容)回调通知业务方。这么一看流程还是很简单的。这里的扫描流程类似上面讲的时间轮算法,会有一个指针不断向后移动,保证不会漏掉任何一个bucket的任务。这个部分的工作模式我们可以简称为Scheduler。Scheduler拿到任务后只需要回调通知业务方即可。如果采用同步通知业务方的方式,由于业务方的超时情况是不可控的,则一个任务的投递时间可能会较长,导致拖慢这个时间点的任务整体通知进度。故而这里采用异步解耦的方式。即将任务发布至事件中心(微信内部的高可用、高可靠的消息平台),再由broker订阅事件中心的消息进行通知业务方的方式来实现异步解耦。主要模块包括任务扫描Daemon