一、什么是 HTQ
先介绍下基本概念。
我们在编写程序时,偶尔会遇到需要用到异步队列的情况。比如说,我发送一万封邮件,如果单纯使用一个for循环来发送,则执行时间要很长,要等很久才能发完,同时很容易导致阻塞、超时等问题。当邮件更多,比如一百万封的时候,问题会更加明显。这时最好的解决方案就是把这十万封邮件排队,一一发出去。这就是任务队列的概念。
并且,我们并不需要等到十万封邮件都发送完毕后才在网站前台通知用户。我们可以把邮件一入队列,就通知用户。这样,用户等待的时间就不是漫长的“发十万封邮件”的时间,而是“把十万封邮件排队”的时间。因此能明显地缩短了用户等待时间。这就是异步的概念。
HTQ ,全称 Http Task Queue ,是一个以Http方式执行异步任务的队列服务。你可以推送若干url进HTQ队列,HTQ会以Http GET 的方式访问这些url。如果url所在的脚本写上各种具体的任务操作,如发送邮件等,便可以实现异步操作了。HTQ使用node.js编写,可跟各种后台语言如PHP、java配合使用以增强异步处理能力。目前支持的队列类型有实时异步队列、定时异步队列、可变异步队列。
如果你依然对HTQ陌生,则可往下看详细的应用场景以加深了解。
二、应用场景
1、实时异步队列
所谓实时,指的是把任务一推进队列就马上执行。一个典型的应用场景就是我们上面所说的发送邮件。邮件推送进任务队列,队列马上把它发出去。如果它推进队列后有其他邮件正在发送中,它则等待当前邮件发送完毕后才发送。
除了发邮件,我们在发文章、发微博、发评论的时候都可以用得上HTQ的实时任务队列,尤其是数量非常大的时候。比如评论用户太多,如果一瞬间让服务器处理,服务器可能因为支撑不了太高的并发从而造成阻塞。这个时候就可以让评论们进入队列再一一处理。
2、定时异步队列
定时,顾名思义,就是在特定的时间执行任务队列。这种队列服务可用于定时邮件、定时短信。需要说明的是,这里的定时,不一定是精准的定时。假如你设置了明天12点执行某个任务,那么,它在明天12点的时候将进入队列。假如队列中已经有任务在执行,那么它会等待到前面的任务完毕才执行。此时可能是12点01分钟才执行。
3、可变队列
我们推送10个任务进队列,这10个队列会反复循环地执行,并且它们的执行快慢能够根据返回情况进行调整,这就是可变队列。比如,我们做扫描监控,会反复地执行“扫描”这个任务。我们希望,在有异常情况的时候,能加快扫描的速度以便更快速地发现问题;而在没有长期异常的情况能减慢一下扫描速度以节省机器资源。
再举一个场景例子,通过API拉取微博新动态。我们网站上有10万绑定了新浪微博的用户,我们需要时常获取他们的最新动态以展示在我们的网站的用户主页上。 如果是采用定时获取动态的方式,那么,假设1分钟能获取1千个用户的动态(因为受API使用频率和网络等原因限制,我们获取不了太快。这里先假设一个数字),那么,获取完所有用户状态需要100分钟。对用户来说,他在微博更新动态后,100分钟后才显示到我们网站。这明显滞后太多。有没有办法加快点呢?此时可以使用HTQ的可变队列。可变队列会对长期没有更新动态的那部分不活跃用户进行减缓速度,减缓对他们微博的获取频率,同时加大对活跃用户的获取频率。这样,一个活跃用户更新微博后,可能10分钟就能同步到我们网站了。对于不活跃用户,可能获取时间会变长了些,但不要紧,我们愿意分配更多的资源去满足活跃用户的需求。
使用可变队列,能让我们有所侧重地使用我们的资源,以减少浪费、增加利用率。
三、安装和使用
1、安装
首先安装好node环境和redis服务,请参考:http://nodejs.cn/download/ 和 http://redis.io/download
clone或下载代码:https://github.com/star7th/htq
下载到你想要放置的目录,命令行进入该目录,执行命令:
npm install
安装完毕后,执行以下命令启动:
node htq.js
上面这种启动方式是临时运行的,关闭命令行窗口就会停止了。如果想一直在后台运行,则可:
nohup node htq.js > ~/htq.log 2>&1 &
如果想关闭退出,可运行:
killall -9 node
2、如何使用
启动后,HTQ默认监听本机的5999端口。你可以通过此端口访问HTQ的API,以添加队列和任务。详细的API文档可看:http://www.showdoc.cc/htq?page_id=37198
你可以根据API文档来在你的项目中调用API以新建任务。官方提供了一个PHP调用的SDK(在/PHPSDK目录)。欢迎其他语言的开发者也将HTQ的API封装成其他语言的SDK
如果要修改默认端口以及默认的redis地址,可修改配置文件config.json。修改完毕需重启HTQ才能生效
四、安全与容错
1、程序安全
访问HTQ 的API时需要填写简单的token认证,认证信息在配置文件config.json里定义。为了安全起见,你可以在下载代码将token设置为其他随机数。如果你已经启动了HTQ,则需要关闭后再重启才能让新配置生效。
如果你担心直接执行url会带来安全隐患,怕自己暴露的url被外部访问,那你可以在推送进HTQ的url上带参数签名校验。然后在url触发的任务脚步里检验签名即可。
2、数据安全
HTQ使用redis来储存队列。Redis自身带有持久化功能。如另外需要对数据进行备份,则备份redis即可,不用在业务中实现数据备份。
3、正确性
HTQ能执行url,但不能保证业务上的正确。比如说HTQ确实是触发了发文章的脚本,然而这个脚步可能自身因为网络原因发布文章失败。此时应该在业务层做好相应的容错处理,比如让该url重新入队列。
给出benchmark更有说服力
killall -9 node 你这是把所有node进程都关了吧。。
大概看了下实现原理,针对于定时异步处理想确认一点,假如定时1分钟后执行task,你是如何做到依靠redis做到定时器的?还是说是你自己server做setTimeout
@airyland 这个是简单粗暴的方法,主要给非node开发者看的。对于node开发者,自己懂如何关闭node进程
@haozxuan 自己server定时读redis。如果某个队列正在执行中,则不重复执行;如果队列不在执行中,则启动队列。这样相当于定时监控队列的运行
@haozxuan 补充下,在自己server实现了定时监控队列的运行机制后,便能保证队列几乎一直在跑(直到没数据会停止跑,但有数据进来在1秒内会启动队列)。队列一直在跑的话,就能一直按照时间读取任务了。在这种机制下,定时会很简单实现,在进队列的时候设置时间即可。往后的执行步骤将和实时队列一样
@star7th 在自己的server中坐setTimeout会不会影响性能?当短时间内产生大批量task时,每个task假设定时1h,当task数量到10K级别的时候,server自身的进程内存会不会吃不消?而且,不太建议server做定时,其一是因为setTimeout本来就不准确(取决上一个task是否存在cpu密集运算),其二,由于定时的不准确导致的延迟被放大后也是不容小觑的;如果不能解决这个问题,那么这个队列只能是一个demo了。
@haozxuan 在同一个队列里,同一时间只允许执行一个task。它还有排队机制的。所以不用担心随着task的增多而消耗server自身的内存。
@haozxuan 举个例子。HTQ正在执行队列A,队列A有5个task,分别是task1 ,task2 ,task 3, task 4 ,task5 如何保证taks是one by one 执行的呢? 把执行task2的代码放到执行task1的回调函数里,便可保证执行taks1完毕后会马上执行task2。 如果反复,一直到5个task全部执行完毕。 当执行完毕,执行队列 A 的函数会暂停。此时会用一个setInterval函数去检测是否有新数据进队列。如果有,则重启执行队列 A 的函数。然后又会再次执行task一直到task没有为止。 从这个机制可以看出,setInterval是用来重启队列而已,不需要时间上的精准。所以不会有你所说的延迟放大问题。
@haozxuan 再单独回复一个关于性能的问题。 全局只有一个setInterva,这个setInterva用于检测队列重启。 task并没有用setInterva,所以100W级别的task也不会带来server因使用setInterva而带来的内存开销。 HTQ本身对性能损耗不多。它的性能瓶颈在于服务器能支撑的http连接数而非自身的内存消耗。
有没应用demo
@haozxuan 我认真地思考了下你为什么会有那样的疑问,我猜可能你是以为我在实现定时队列的时候,对于每一个task都采用setTimeout的方式了吧?这种方式性能太差,自然不可取。 每个task都会保存着一个时间戳,在执行队列的时候,HTQ从redis中取出时间戳小于当前时间的task来执行。这个操作会反复。 当你在进task的时候将时间戳设定到指定时间,那这个task便会在指定时间才会被读取出来执行
@liushaobo05 这个倒没。但我花了很多精力去写文档,阅读文档应该会对它有所了解。只要按照文档去执行一遍就什么都懂了 。后续再考虑写个应用demo
@star7th 是的,我的顾虑就在此;举个例子,在队列 A中,我分别入队列了3个task A(3分钟后执行), taskB(2分钟后执行), task C(1分钟后执行),那么,前1分钟的watch操作都是没有实际意义的从,基于事件触发性质的架构来看,这种循环遍历的方总感觉有些浪费性能; BTW:当前架构还有一个瓶颈在于,如果上一个task是cpu密集型运算,将大大影响后续task的执行,而且从你的解释看整个task是串行执行,一旦出现异常或者长时间不结束,将对后续的task产生影响;
mark关注
@haozxuan 所谓队列,就是指串行。task与task之间本身是串行,这样才起到one by one 的效果。否则就不叫队列服务了。 task 与 task 之间是串行, 但队列与队列之间是并行。如果task是CPU密集型需要计算很久,不妨把这些task都放到不同的队列中,这样就可以并行了
@haozxuan 你说的“基于事件触发性质的架构来看,这种循环遍历的方总感觉有些浪费性能”,其实是的。我当初是考虑过用触发性质的架构。这种架构适合实时队列,一旦进数据,就马上触发。 但是,如果出现异常,导致触发了却没执行怎么办?已经触发了就不能重复触发了。此时如果用循环遍历的方式能够很好地容错。 而且对于定时队列和可变队列,这两种队列用触发的方式很难做到。 之前在架构方面我花了很多时间思考。当前架构应付小规模数据是没问题的了
@star7th 恩,其实触发后不一定就是处理,可以更丰富些,可以做到触发完成无异常后,再继续,如果有异常就重新放入队列,这样也能更好的容错;其实主要是感觉做任务队列这块有种“出力不讨好的感觉”,简单的需求可以直接入redis的list,用brpop等阻塞式读取来满足队列的实时处理性;复杂的就要考虑容灾、出错回滚、集群管理等等问题;不过最后还是必须对你的努力报以肯定的态度,可能这是一个小轮子,以后会在某个项目中起到不可获取的作用;
@star7th 这里面有两个问题;第一,我并不否则one by one的模式,因为队列本质上就是一个先进先出的通道,我想表达的是,可能我的定时task会因为这样的one by one策略导致延时误差很大,第二,每个task之间相互独立,如果是遍历的方式肯定不能完成定时队列的功能,因为我不可能在执行前就知道把耗时的放在队列A,不耗时的放在队列B,这样的结果就是cpu密集型运算的task势必会影响后续的任务,而后续的定时任务可能因为这个原因而出现比较大的时间误差;综上,我想表述的是这个属于设计上或者应用面上的问题(即不适合cpu密集型的task);
@haozxuan 用redis实现队列,一般人都会想到list。我也是。当初写程序的时候就用list,后来发现很难实现定时队列和可变队列的,至少我是没想到好的解决方法。redis的list不能保存每个元素的时间戳,如果使用list,则需要另外新建key来放置时间戳,这样平均就多读了至少两倍量级的redis,反而会影响了性能。最后我是使用了有序集合来实现的。 定时队列,其实你不用太纠结。文中已经说了
“需要说明的是,这里的定时,不一定是精准的定时。假如你设置了明天12点执行某个任务,那么,它在明天12点的时候将进入队列。假如队列中已经有任务在执行,那么它会等待到前面的任务完毕才执行。此时可能是12点01分钟才执行。”
这里的定时队列设置上不打算用于精确定时的场合。所以如果无论是你所说的CPU密集还是其他也好,对时间精度要求高的都不适合。它比较适合定时发邮件、短信之类的场合。
LZ的代码我还没看,不过想讨论一下设计思路:1.如果是同时有多个队列处理同一种任务,这种方法跟线程池有什么不同?2.一般的异步任务都是互相独立的,换句话说,根本不需要按生成时间排队,顺序执行,这种情况下,nodejs本身的单线程非阻塞不是已经很好的解决了“阻塞、超时”的问题了吗?
HTQ跟线程池是两个不同机制的东西,我不知道应该如何做比较。好像它们没有任何相同之处吧。 对于第二个问题,如果只是实时队列,确实是不需要“按生成时间排队,顺序执行”。我按时间排序是为了实现后面两种队列,定时队列和可变队列。文中有介绍它们的使用场景。 如果可以,建议你看下代码。注释比较详细。核心代码也只是百来行