SENSORO 支撑百万级传感器的延时队列

文/升哲科技 刘鹏
摘要:本文主要描述升哲科技在打造物联智慧城市平台过程中关于如何实现延时队列服务的技术选型经验、延时队列服务的架构设计以及延时队列的底层细节实现原理。
背景升哲科技是一家物联网与人工智能领域的国家高新技术企业、独角兽企业。
要打造物联智慧城市平台,在业务中涉及到各种延时任务的需求,例如设备定时空气开关,定时更新设备状态,定时提醒等等,基于这些需求,需要一个可靠、实时、海量的延时队列服务作为基础设施。
那么延时队列是什么呢?延时队列不同于消息队列按照先入先出(fifo)的顺序来消费,而是根据消息指定时间延时消费。延时队列的使用在我们日常应用也非常多,比如:
· 在电商平台购物,在30分钟内没有支付自动取消订单;
· 待处理的工单超过1天未处理,二次发送提醒。
以上场景往往都需要延时队列实现。
早期延时队列的实现采用了数据库扫表方式,服务定期查询到期的任务,再通过kafka来中转消息。当任务量小,延时精度要求低时扫表方式还能应对,然而随着业务增长、任务数量不断增多,延时时间精度要求也变高,扫表的方式已经无法满足我们的业务,于是我们开始探索新的技术方案来支撑百万级任务的延时队列。
延时队列的设计目标
1.高可用:多副本部署,保证服务不出现单点故障;
2.可扩展:可随着业务量增长来扩容,同时生产消费的请求延时也要低;
3.兼容旧接口,保证旧的服务不需要做任何修改;
4.消息传递可靠,至少保证一次送达。
技术选型在开源社区已经存在一些解决方案:
方案 描述
beanstalkd beanstalkd      c语言实现,我们团队主要采用golang和java,二次开发有难度,beanstalkd不支持集群部署,高可用无法保证。
rabbitmq 延时队列 rabbitmq提供了延时队列插件,需要单独开启插件使用,其原理是通过死信队列实现。
nsq
nsq开源延时队列,nsq支持延时队列。
delayqueue延时队列
jdk中提供了一组实现延时队列的api,位于java.util.concurrent包下delayqueue。
时间轮算法
时间轮是一个算法,在 netty、akka、quartz、zookeeper 、kafka等组件中都有使用,适合做统一调度器。
redis sorted set
redis sorted set     利用它的score属性,启用一个线程轮询,根据score获取超时的数据,然后触发超时操作。
考虑到运维难度和可扩展性,最终我们选择了开源项目lmstfy作为基础来进行二次开发,选择lmstfy的原因如下:
● 无状态服务,使用redis来持久化,redis的高可用方案已经非常成熟,在公/私有云都有paas服务可使用;
● 支持扩容,可以配置多个redis集群;
● 提供java/go/rust/php客户端,监控面板完善;
● 采用golang开发,高并发性能优秀,也方便后续二次开发。
整体架构设计1.delayer:无状态服务,提供给业务服务调用,兼容旧接口,在delayer这一层直接操作redis实现了任务删除和更新任务等等功能;
2.lmstfy:无状态服务,提供延时队列基础服务,底层实现采用;
3.redis sentinel集群:保证redis发生故障时自动主备切换。
基础概念● namespace - 用于隔离业务,也可以通过配置namespace绑定不同的redis集群;
● queue - 队列,用区分同一业务不同消息类型;
● job - 业务定义的业务,主要包含以下几个属性:
  ○ id: 任务 id,全局唯一;
  ○ delay: 任务延时下发时间,单位是秒;
  ○ tries: 任务最大重试次数,tries = n 表示任务会最多下发 n 次;
  ○ ttr(time to run): 任务预期执行时间,超过 ttr 则认为任务消费失败,触发任务自动重试。
数据存储lmstfy 的 redis 存储由四部分组成:
● timer: 使用zset结构来存储延时任务,score即任务的到期时间来排序;
● ready queue -  使用list结构,存储已经到期的延时任务,实现fifo消费;
● deadletter- 使用list结构,消费失败(重试次数到达上限)的任务,可以手动重新放回到队列;
● job pool – string类型,存储消息meta信息;
● job mapping - string - 存储应用自定义id和job的关联关系。
创建任务创建任务会生成一个job id, job id包括写入时间戳、随机数和延时时长,然后将任务的meta信息写入redis,key为 j/{namespace}/queue/{id} ,当任务延时时间(delay)= 0,(实时消息队列我们使用kafka)表示不需要延时则直接写到 ready queue(list),当延时时间(delay) = n(n > 0),表示需要延时,将延时加上当前系统时间作为绝对时间戳写到 timer(sorted set),timer的实现是利用 zset 根据绝对时间戳进行排序,再由一个goroutine定期轮询将到期的任务通过 redis lua script 来将数据转移到 ready queue(list)中。
任务消费支持延时的任务队列本质上是两个数据结构的结合: ready queue (list)和 sorted set。
sorted set 用来实现延时的部分,将任务按照到期时间戳升序存储,随后定期将到期的任务迁移至 ready queue(list)。
任务的具体内容只会存储一份在 job pool 里面,其他的如 ready queue 只是存储job id,这样可以节省内存空间。
任务更新和删除lmstfy本身不支持删除和更新,我们在delayer层中在创建任务同时在redis中创建了一个mapping key,客户端可以自定一个id关联到job id ,delayer提供了删除和更新(先删除再创建)api,我们业务还需要支持多次执行的功能,在处理job ack时根据任务参数重新插入队列,结合我们二次开发整体结构如下:
性能表现通过本地限定1核cpu 压测生产消息数据如下:
200万任务量占内存600mb+,其中包括mapping key导致key数量翻倍。
以下是单核cpu的环境下压测结果,任务创建可高达1500tps:
延时任务到期时间比较分散的情况下,消费表现如下接800tps:
总结封装lmstfy的方案已足够支撑当前的使用场景,但还是有一些不足之处,比如:
● 在delayer中操作redis中的任务,无法保证原子性;
● 任务创建和消费另外会多一次网络请求,产生不必要的开销;
● 无法支持循环任务;
● lmstfy采用http协议,无法发挥更好性能。
未来,我们计划融合两个服务,完善任务crud功能,减少网络开销,并采用grpc来替换http协议通讯。

Sunlord板载式NTC热敏电阻—SDNC-G系列介绍
Zen 3 锐龙 R7-5800H 移动处理器的跑分成绩曝光:单线程性能大涨 35%
车联网之小汽车专题-口袋物联
有效减少扫描电镜荷电效应的几种方法
中国移动5G千兆家庭网络正式实现双G贯通视频通话
SENSORO 支撑百万级传感器的延时队列
24v超声电机的功率是多少,它都具有哪些特点
STM32基础知识:IIC总线操作EEPROM存储模块AT24C02
本田冠道能否成为爆款?价格说明一切
一文详解MES/MOM系统基础知识
模拟万用表的使用方法和注意事项
从产品角度看,小米和华为之间的差距有多大
我国工业互联网产业发展布局有着重要意义
一个区块链链接的生态系统是什么样子的
5G的价值与运用你知道吗
七旬独居老人的“居家安防5件套”
Linux创建者说,我不再编写任何代码
为什么要使用智能化妆镜,它可让你成为众人的焦点
中船重工成功研制国内首个3D缝纫机器人
下一代交付机器人将融合AI技术 机器换人创造物流行业新商机