一次排队引起的优化之旅

业务场景

我司聊天服务是基于开源的Ejabberd项目(v19.02)搭建,其中有一项以平台部API形式提供给各项目组游戏服,用于发送Announce(即公告)到指定聊天室的功能。内在实现是以一个平台维护的、在Ejabberd已注册的客户端账号(该账号并不存在于各项目的各聊天室中)调用Ejabberd提供的API(

)发送指定信息至对应聊天室。

所以该方案的实现,实质上是一个玩家(对应一个Ejabberd客户端账号)发送聊天消息到指定聊天室。而对于单个玩家发送消息至指定聊天室的频率,Ejabberd是有配置参数可以设置的,官方给出的参考值是:

,具体含义是:单个玩家在单个聊天室发言频率不能高于0.4s/条。

原始解决方案

原始解决方案简单粗暴;

1.对于每个请求(这里指调用公告API的HTTP请求,下同),将请求数据丢到以room_name(聊天室name)为key的redis单向队列中(以RPush、LPop方式构造),然后将room_name扔到全局的channel中;

2.在处理channel数据的routine函数中,以阻塞模式监听channel中是否有数据产生,如果有则已以获取的room_name为key的redis单向队列中pop一条数据,而后强制sleep 0.5s, 再调用Ejabberd提供的API接口send_stanza发送刚才pop获取的消息至指定聊天室。

明显的缺陷

最大的缺陷就是,对于每个请求都强制sleep了0.5s,不论该请求将发往哪个聊天室。也就是全局只有一个channel,所有调用公告接口的请求都被强制排队了。其次,redis队列是全局的,而channel归属单个gochat服务实例,如此可能造成消息到达时失序等问题。至于测试时为什么没有发现问题,当然是没有做压测的缘故。

造成的后果

对于普通玩家而言,该限制对实际操作几乎没有任何影响(无论是正常玩家的发言频率、还是客户端的发言频率限制都可以保证普通玩家发言频率低于0.4s),而对于依赖该API进行各类系统公告或有基于该API玩法的项目组而言,该方案的限制大大影响到了游戏的正常进行。以C2为例,上线后对于单条公告的延迟最高竟达到了80分钟。。

优化之旅

经过线上几个小时的检验,原始方案显然是行不通的,紧急回滚之后,便踏上优化之旅。

优化方案一

原始方案最大的缺陷当然就是不分room,对每条公告请求都强制排队并sleep 0.5s. 所以最先想到的优化方案便是在保持现有Ejabberd发言频率限制的前提下,将队列的粒度细化为针对每个聊天室分别排队。再经分析,具体的方案是:

1.将最初设计的针对每个聊天室分别排队方案修改为:建立100个channel,根据游戏服id对100取模,模相同的游戏服中所有聊天室公用一个channel(以C2项目为例,全服共600余服,每服所含聊天室数量不一,大到十几万,小至几十),每个channel最多可容纳1w个room;

2.针对1中的每个channel,再建立一个wait_channel,最多也可容纳1w个room;

3.对于每个请求,首先将请求数据丢到以room_name(聊天室name)为key的redis单向队列中(上文提到过),然后将该room丢到所在游戏服对应的channel中(1提到的取模);

4.起100个routine,每个routine对应处理1中对应序列的channel. 阻塞模式监听channel,如果有数据则从获取的room_name为key的redis单向队列中pop一条数据,并检查redis中是否有以room_id(对应room_name)为key的记录,若没有记录,则表示当前可以向该room发送公告消息,执行对应操作;

5.若4中redis有以room_id(对应room_name)为key的记录,则表示正有进程对该room进行公告操作,当前操作需等待,此时便将4中pop获取的数据丢到2建立的wait_channel中,在wait_channel中阻塞 模式监听数据,对于获取的每条数据都间隔0.1s查询对应rid在redis是否有记录,直至返回没有记录后,则表示当前可以向该room发送公告消息,执行对应操作。

缺陷

该方案理论上消除了全部请求都需要强制在一个队列排队0.5s的问题,但是却没有考虑另一个现实:向游戏服提供公告API功能的服务本身是以多副本形式存在的,每个副本的内存数据独立,但共享redis数据。

当前方案每个副本均维护了200个channel,每个副本接收到的请求都扔到了自己维护的channel,但是请求数据却写到了redis中。

在多个副本竞争redis中同一key下数据时并非原子操作,可能存在获取数据(Get操作)后、设置key对应TTL(Set操作)前,在进行逻辑操作期间其它副本先执行Set操作的可能,导致当前副本一直拿不到执行后续处理的机会,也就导致本身分发到当前副本的请求迟迟得不到处理,公告消息产生不可预测的延迟。

优化方案二

主要优化多副本、多channel、单redis导致的请求可能产生的延迟等问题。既然实际请求数据在redis中,对于同一聊天室当前是否可以发送数据的标记也记录在redis中,则实际上不需要副本中自行维护请求的channel队列,应将数据统一维护在redis中。具体方案:

1.redis中维护一张map,实际为map[string]queue类型,map中每个key对应一个room_name,value是一个单向队列,队列中每个元素对应一个请求数据;维护一个list,每个元素对应一个room_id。每当有新的请求道来,就向map和listpush相应数据;

2.单独起一routine(全局),采用redis的BLPop方法阻塞式的获取1中list的一个key,获取之后就起一routine单独处理该请求;

3.对于2中处理请求的routine,死循环采用redis的setNX方法获取room_id对应的锁,TTL设置为0.4s(SetNX(room_id, 1, 0.4s)),目的是获取当前routine对该room发送聊天消息的权限。获取权限后执行相应操作。

缺陷

对于单个副本而言,将所有接收到的HTTP请求都丢到了redis队列汇总,然后另起一全局routine循环从该队列中拿一个room_id,再起一临时routine处理该room_id对应的请求,实则有点多余。

最终方案

基于优化方案二的最终方案:

接收到请求后,直接调用redis的setNX方法检查当前是否可以发送消息到指定room_id(setNX实质上相当于该room_id的锁,拿到set的权限相当于获取锁),若可以发送则执行相应操作;若不可以发送则直接返错给调用方,简单直接。

同时评估项目组需求后,平台将上文提到的0.4s的限制降到了0.1s,尽量降低该限制对项目组需求的影响。

测试结果

在项目组配合修改调用及重试逻辑后,针对该接口的调用进行了贴近生产环境的压测,测试结果表明最终方案达到了生产环境的性能要求。

彩蛋

最终方案的性能要求是达到了,但是Ejabberd官方对send_stanza接口的实现中,并没有将以该接口发送的消息存档,也就是说,客户端重新登录后,之前收到的系统公告消息就丢掉了。这个缺陷对于强依赖该公告接口的项目而言是不可接受的。当两种基于send_stanza的补救方案(修改Ejabberd源码、构造该消息格式手动存档)因各种原因确认不可行后,此次所谓优化最终也被弃用,需要另寻出路。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容

  • 引言 context 是 Go 中广泛使用的程序包,由 Google 官方开发,在 1.7 版本引入。它用来简化在...
    51reboot阅读 3,479评论 0 10
  • Zookeeper用于集群主备切换。 YARN让集群具备更好的扩展性。 Spark没有存储能力。 Spark的Ma...
    Yobhel阅读 7,234评论 0 34
  • 安全性 设置客户端连接后进行任何其他指令前需要使用的密码。 警告:因为redis 速度相当快,所以在一台比较好的服...
    OzanShareing阅读 1,660评论 1 7
  • 摘自http://xiaoh.me/2016/06/30/redis-advanced/ 排序 redis支持对l...
    鸵鸟要抬头阅读 66,382评论 1 3
  • 创作你的创作 免费下载 《自在人生》第十期文字版笔记之【警醒于光】
    刘小明开心阅读 306评论 0 0