基于golang的简单分布式延时队列服务的实现

作者:侯盛鑫 时间:2024-05-08 10:44:03 

一、引言

背景

我们在做系统时,很多时候是处理实时的任务,请求来了马上就处理,然后立刻给用户以反馈。但有时也会遇到非实时的任务,比如确定的时间点发布重要公告。或者需要在用户做了一件事情的X分钟/Y小时后,EG:

“PM:我们需要在这个用户通话开始10分钟后给予提醒给他们发送奖励”

对其特定动作,比如通知、发券等等。一般我接触到的解决方法中在比较小的服务里都会自己维护一个backend,但是随着这种backend和server增多,这种方法很大程度和本身业务耦合在一起,所以这时需要一个延时队列服务。

名词解释

topic_list队列:每一个来的延时请求都应该又一个延时主题参考kafka,在逻辑上划分出一个队列出来每个业务分开处理;

topic_info队列:每一个队列topic都存在一个新的队列里,每次扫描topic信息检测新的topic建立与销毁管理服务协程数量;

offset:当前消费的进度;

new_offset:新消费的进度,预备更迭offset;

topic_offset_lock:分布式锁。

二、设计目标

 功能清单

1、延时信息添加接口基于http调用

2、拥有存储队列特性,可保存近3天内的队列消费数据

3、提供消费功能

4、延时通知

性能指标

预计接口的调用量:单秒单类任务数3500,多秒单类任务数1300

压测结果:

简单压测

wrk写入qps:259.3s 写入9000条记录 单线程 无并发

触发性能/准确率:单秒1000,在测试机无延长。单秒3000时,偶尔出现1-2秒延迟。受内存和cpu影响。

三、系统设计

交互流程

时序图

基于golang的简单分布式延时队列服务的实现

本设计基于http接口调用,当向topic存在的队列中添加消息的时候,消息会被添加到相应topic队列的末尾储存,当添加到不存在的相应topic队列时,首先建立新topic队列,当定时器触发的时候或者分布式锁,抢到锁的实例先获得相应队列的offset,设置新offset,就可以释放锁了让给其他实例争抢,弹出队列头一定数量元素,然后拿到offset段的实例去存储中拿详细信息,在协程中处理,主要协程等待下次触发。然后添加协程去监控触发。

模块划分

1、队列存储模块

1·delay下的delay.base模块,主要负责接收写请求,将队列信息写入存储,不负责backend逻辑,调用存储模块

2、backend模块。delay下的delay.backend模块,负责时间触发扫描对应的topic队列,调用存储模块,主要负责访问读取存储模块,调用callback模块

1·扫描topic添加groutine

2·扫描topic_list消费信息

3·扫描topic_list如果一定时间没有消费到则关闭groutine

3、callback模块,主要负责发送已经到时间的数据,向相应服务通知

3、存储模块

1·分布式锁模块,系统多机部署,保证每次消费的唯一性,对每次topic消费的offset段进行上锁offset到new_offset段单机独享

2·topic管理列表,管理topic数量控制协程数

3·topic_list,消息队列

4·topic_info,消息实体,可能需要回调中会携带一些信息统一处理

4、唯一号生成模块。

五、缓存设计

目前使用全缓存模式

key设计:

topic管理list key: XX:DELAY_TOPIC_LIST type:list

topic_list key: XX:DELAY_SIMPLE_TOPIC_TASK-%s(根据topic分key) type:zset

topic_info key: XX:DELAY_REALL_TOPIC_TASK-%s(根据topic分key) type:hash

topic_offset key: XX:DELAY_TOPIC_OFFSET-%s(根据topic分key) type:string

topic_lock key: xx:DELAY_TOPIC_RELOAD_LOCK-%s(根据topic分key) type:string

六、接口设计

delay.task.addv1 (延时队列添加v1)

请求示例


curl -d
'{
 "topic": "xxx", // 业务topic
 "timing_moment": ,    // 单位秒,要定时时刻
 "content": "{}"// 消息体,json串
}'
'http://127.0.0.1:xxxx/delay/task/add'

返回示例


{
 "dm_error": 0,
 "error_msg": "操作成功",
 "task_id":112345465765
}

pull回调方式返回(v2不再支持)

请求示例


curl -d
'{
 "topic": "xxxx", // 业务topic
 "task_id":1324568798765// taskid,选填,有则返回特定消息
}'
'http://127.0.0.1:xxxx/delay/task/pull'

返回示例


{
 "dm_error": 0,
 "error_msg": "操作成功"
 "content":"{"\xxx"\}"
}

delay.task.addv2 (延时队列添加v2)

请求示例


curl -d
'{
 "topic": "xxx", // 业务topic
 "timing_moment": ,// 单位秒,要定时时刻
 "content": "{                        // 消息内容(json string)
"sn":"message.call",                  // 服务发现名字(或为配置服务名)
"url":"/ev/tp/xxxx",                  // 回调url
"xxx":"xxx"                       // 其他字段
 }"
}'
'http://127.0.0.1:xxxx/delay/task/add'

示例


curl -d '{
 "topic":"xxxx_push",
 "content":"{
   "uid":"111111",
   "sn":"other.server",
   "url":"/xxxx/callback",
   "msg_type":"gift",
 }",
 "timing_moment":1565700615
}'
http://127.0.0.1:xxxx/delay/task/add

返回示例


{
 "dm_error": 0,
 "error_msg": "操作成功",
 "task_id":112345465765
}

七、MQ设计(v2不再支持)

关于kafka消费方式返回:


topic: delay_base_push

固定返回格式
{
 "topic": "xxxx",// 业务topic
 "content": "{}"// 单条生产消息content
}

八、其他设计

唯一号设计

调用存储模块,利用redis的自增结合逻辑生成唯一号具体逻辑如下:


func (c *CacheManager) OperGenTaskid() (uint64, error) {
now := time.Now().Unix()
key := c.getDelayTaskIdKey()
reply, err := c.DelayRds.Do("INCR", key)
if err != nil {
log.Errorf("genTaskid INCR key:%s, error:%s", key, err)
return 0, err
}
version := reply.(int64)
if version == 1 {
   //默认认为1秒能创建100个任务
c.DelayRds.Expire(key, time.Duration(100)*time.Second)
}
incrNum := version % 10000
taskId := (uint64(now)*10000 + uint64(incrNum))
log.Debugf("genTaskid INCR key:%s, taskId:%d", key, taskId)
return taskId, nil
}

分布式锁设计


func (c *CacheManager) SetDelayTopicLock(ctx context.Context, topic string) (bool, error) {
key := c.getDelayTopicReloadLockKey(topic)
reply, err := c.DelayRds.Do("SET", key, "lock", "NX", "EX", 2)
if err != nil {
log.Errorf("SetDelayTopicLock SETNX key:%s, cal:%v, error:%s", key, "lock", err)
return false, err
}
if reply == nil {
return false, nil
}
log.Debugf("SetDelayTopicLock SETNXEX topic:%s lock:%d", topic, false)
return true, nil
}

九、设计考虑

健壮性

熔断策略:

基于golang的简单分布式延时队列服务的实现

这版设计中有很多不足之处,当redis不可访问时,请求将大量积压给机器或者实例带来压力,导致其他服务不可用,所以采取降级策略(降级策略也有不足);在请求redis时加入重试,当重试次数多于报警次数,会记录一个原子操作atomic.StoreInt32(&stopFlag,1),其中stopFlag为一个全局的变量,在atomic.LoadInt32(&stopFlag)后,stopFlag的值为1则暂时不请求redis,同时记录当前时间,加入定时器,熔断器分为三个级别,开,关,半开,当定时器结束后stopFlag=2第二个定时将为半开状态计时,有概率访问redis,当成功次数到达阈值stopFlag=0,否则stopFlag=1继续计时

不足

 1、调用time定时

通常golang 写循环执行的定时任务大概用三种实现方式:

1、time.Sleep方法:


for {
 time.Sleep(time.Second)
 fmt.Println("test")
}

2、time.Tick函数:


t1:=time.Tick(3*time.Second)
for {
 select {
 case <-t1:
   fmt.Println("test")
 }
}

3、其中Tick定时任务,也可以先使用time.Ticker函数获取Ticker结构体,然后进行阻塞监听信息,这种方式可以手动选择停止定时任务,在停止任务时,减少对内存的浪费。


t:=time.NewTicker(time.Second)
for {
 select {
 case <-t.C:
   fmt.Println("test")
   t.Stop()
 }
}

在最开始以为sleep是单独处理直接停掉了这个协程,所以第一版用的也是sleep,但是在收集资料后发现这几种方式都创建了timer,并加入了定时任务处理协程。实际上这两个函数产生的timer都放入了同一个timer堆(golang时间轮),都在定时任务处理协程中等待被处理。Tick,Sleep,time.After函数都使用的timer结构体,都会被放在同一个协程中统一处理,这样看起来使用Tick,Sleep并没有什么区别。实际上是有区别的,本文不是讨论golang定时执行任务time.sleep和time.tick的优劣,以后会在后续文章进行探讨。使用channel阻塞协程完成定时任务比较灵活,可以结合select设置超时时间以及默认执行方法,而且可以设置timer的主动关闭,所以,建议使用time.Tick完成定时任务。

2、存储模块问题

目前是全缓存,没有DB参与,首先redis(codis)的高可用是个问题,在熔断之后采取“不作为”的判断也是有问题的,所以对未来展望,首先是:

1·单机的数据结构使用多时间轮。为了减少数据的路程,将load数据的过程异步加载到机器,减少网络io所造成的时间损耗。同时也是减少对redis的依赖

2·引入ZooKeeper或者添加集群备份,leader。保证集群中至少有两台机器load一个topic的数据,leader可以协调消费保证高可用

来源:https://juejin.im/post/5e5792bff265da570b3f2f07

标签:golang,分布式,延时队列
0
投稿

猜你喜欢

  • js实现一个简易计算器

    2024-04-25 13:13:38
  • python数据可视化plt库实例详解

    2022-11-30 21:23:28
  • 400多行Python代码实现了一个FTP服务器

    2023-12-10 19:31:31
  • 订单转化率之回访确认

    2009-08-24 12:40:00
  • mysql数据库sql优化原则(经验总结)

    2024-01-22 17:55:12
  • Python程序中的观察者模式结构编写示例

    2022-08-04 22:12:11
  • 如何在页面中快捷地添加翻页按钮?

    2010-06-26 12:33:00
  • python网络爬虫基于selenium爬取斗鱼直播信息

    2023-03-05 09:33:19
  • PythonWeb项目Django部署在Ubuntu18.04腾讯云主机上

    2021-02-11 11:15:33
  • tensorflow 实现打印pb模型的所有节点

    2022-09-28 22:37:33
  • 微信小程序 调用微信授权窗口相关问题解决

    2024-04-18 10:08:44
  • 简单介绍一下pyinstaller打包以及安全性的实现

    2021-07-23 15:34:56
  • 微信小程序实现计算器(含历史记录)

    2024-04-17 10:30:20
  • laravel-admin select框默认选中的方法

    2024-05-13 09:51:06
  • python 爬取知乎回答下的微信8.0状态视频

    2022-09-11 15:17:57
  • ES6/JavaScript使用技巧分享

    2024-04-30 10:10:04
  • Python选择网卡发包及接收数据包

    2021-10-13 12:02:10
  • Python CSV模块使用实例

    2022-02-04 18:56:36
  • 基于Python函数的作用域规则和闭包(详解)

    2023-09-03 09:00:28
  • Pandas读取MySQL数据到DataFrame的方法

    2024-01-20 02:29:04
  • asp之家 网络编程 m.aspxhome.com