QQ18年,解密8亿月活的QQ后台服务接口隔离技术

QQ18年

1999年2月10日,腾讯QQ横空出世。光阴荏苒,那个在你屏幕右下角频频闪动的企鹅已经度过了18个年头。随着QQ一同成长的你,还记得它最初的摸样吗?

1999年:腾讯QQ的前身OICQ诞生,该版本具备中文网络寻呼机、公共聊天室以及传输文件功能。

1999年QQ界面

2000年,OICQ正式更名为QQ,发布视频聊天功能、QQ群和QQ秀等功能。

2003年版本,QQ发布聊天场景、捕捉屏幕、给好友播放录影及QQ炫铃等功能。

2004年,QQ新增个人网络硬盘、远程协助和QQ小秘书功能。

···

几经更迭,QQ版本也产生许多变化,很多操作方式都变了,也让QQ更有现代感了。如今的QQ越来越精美,越来越简洁,如你所见。

据不完全统计,腾讯QQ月活用户达到8.7亿左右,而这个数字还在不断增加。。。

如此庞大的用户群的任何行为,都会产生巨大的影响。

2017年春节,QQ推出AR红包加入红包大战,经调查手机QQ的红包全网渗透率达到52.9%。

在此期间,后台想必又一次承受了海量的压力,年后第一波推送,来看看腾讯内部对QQ后台的接口处理的相关技术干货,或许可以给到你答案。

背景

QQ后台提供了一套内部访问的统一服务接口,对腾讯各业务部门提供统一的资料关系链访问服务,后面我们把这套接口简称为DB。

现在说说分set的背景:2013年的某一天,某个业务的小朋友在申请正式环境的DB接入权限后,使用正式环境来验证刚写完的测试程序,循环向DB接口机发送请求包,但因为这个包格式非法,触发了DB解包的一个bug,导致收到这些请求包的服务器群体core

dump,无一幸免。。。。整个DB系统的服务顿时进入瘫痪状态。

因此有了故障隔离的需求,2014年初,我们着手DB的故障隔离增强改造。实现方法就是分set服务–把不同业务部门的请求定向到不同的服务进程组上,如果某个业务的请求有问题,最多只影响一个部门,不会影响整个服务系统。

总体方案

为了更清楚描述分set的方案,我们通过两个图进行分set前后的对比。

分set之前:

分set之后:

从图中可以看出,实现方式其实非常简单,就是多启动一个proxy进程根据IP到set的映射关系分发请求包到对应set的进程上。

分set尝试

很多事情往往看起来非常简单,实现起来却十分复杂,DB分set就是一个典型的例子。怎么说呢?先看看我们刚开始实现的分set方案。

实现方案一:通过socket转包给分set进程,分set进程直接回包给前端。

这个方案刚发布几台后就发现问题:

1,有前端业务投诉回包端口不对导致访问失败。后来了解这些业务会对回包端口进行校验,如果端口不一致就会把包丢弃。

2,CPU比原来上涨了25%(同样的请求量,原来是40%,使用这个方案后CPU变成50%)

回包端口改变的问题因为影响业务(业务就是我们的上帝,得罪不起^^),必须马上解决,于是有了方案二。

实现方案二:通过socket转包给分set进程,分set进程回包给proxy,由proxy回包。

改动很快完成,一切顺利,马上铺开批量部署。。。。

晚上10点准时迎来第一次高峰,DB出现大量的丢包和CPU告警,运维紧急迁移流量。

第二天全部回滚为未分set的版本。

重新做性能验证的时候,发现CPU比原来涨了50%,按这个比例,原来600多台机器,现在需要增加300多台机器才能撑起同样请求的容量。(这是写本文时候的机器数,目前机器数已经翻倍了~)

后来分析原因的时候,发现网卡收发包量都涨了一倍,而CPU基本上都消耗在内核socket队列的处理上,其中竞争socket资源的spin_lock占用了超过30%的CPU — 这也正是我们决定一定要做无锁队列的原因。

最终实现方案

做互联网服务,最大的一个特点就是,任何一项需求,做与不做,都必须在投入、产出、时间、质量之间做一个取舍。

前面的尝试选择了最简单的实现方式,目的就是为了能够尽快上线,减少群体core掉的风险,但却引入了容量不足的风险。

既然这个方案行不通,那就得退而求其次(退说的是延期,次说的是牺牲一些人力和运维投入),方案是很多的,但是需要以人力作为代价。

举个简单的实现方法:安装一个内核模块,挂个netfilter钩子,直接在网络层进行分set,再把回包改一下发送端口。

这在内核实现是非常非常简单的事情,但却带来很大的风险:

1,不是所有同事都懂内核代码

2,运营环境的机器不支持动态加载内核模块,只能重新编译内核

3,从运维的角度:动内核 == 杀鸡取卵 — 内核有问题,都不知道找谁了

好吧,我无法说服开发运营团队,就只能放弃这种想法了–即便很不情愿。

。。。跑题了,言归正传,这是我们重新设计的方案:

方案描述:

1,使用一写多读的共享内存队列来分发数据包,每个set创建一个shm_queue,同个set下面的多个服务进程通过扫描shm_queue进行抢包。

2,Proxy在分发的时候同时把收包端口、客户端地址、收包时间戳(用于防滚雪球控制,后面介绍)一起放到shm_queue中。

3,服务处理进程回包的时候直接使用Raw Socket回包,把回包的端口写成proxy收包的端口。

看到这里,各位同学可能会觉得这个实现非常简单。。。不可否认,确实也是挺简单的~~

不过,在实施的时候,有一些细节是我们不得不考虑的,包括:

1)这个共享内存队列是一写多读的(目前是一个proxy进程对应一组set化共享内存队列,proxy的个数可以配置为多个,但目前只配一个,占单CPU不到10%的开销),所以共享内存队列的实现必须有效解决读写、读读冲突的问题,同时必须保证高性能。

2)服务server需要侦听后端的回包,同时还要扫描shm_queue中是否有数据,这两个操作无法在一个select或者epoll_wait中完成,因此无法及时响应前端请求,怎么办?

3)原来的防滚雪球控制机制是直接取网卡收包的时间戳和用户层收包时系统时间的差值,如果大于一定阀值(比如100ms),就丢弃。现在server不再直接收包了,这个策略也要跟着变化。

基于signal通知机制的无锁共享内存队列

A. 对于第一个问题,解决方法就是无锁共享内存队列,使用CAS来解决访问冲突。

这里顺便介绍一下CAS(Compare And Swap),就是一个汇编指令cmpxchg,用于原子性执行CAS(mem, oldvalue, newvalue):如果mem内存地址指向的值等于oldvalue,就把newvalue写入mem,否则返回失败。

那么,读的时候,只要保证修改ReadIndex的操作是一个CAS原子操作,谁成功修改了ReadIndex,谁就获得对修改前ReadIndex指向元素的访问权,从而避开多个进程同时访问的情况。

B. 对于第二个问题,我们的做法就是使用注册和signal通知机制:

工作方式如下:

1)Proxy负责初始化信号共享内存

2)Server进程启动的时候调用注册接口注册自己的进程ID,并返回进程ID在进程ID列表中的下标(sigindex)

3)在Server进入睡眠之前调用打开通知接口把sigindex对应的bitmap置位,然后进入睡眠函数(pselect)

4)Proxy写完数据发现共享内存队列中的块数达到一定个数(比如40,可以配置)的时候,扫描进程bitmap,根据对应bit为1的位取出一定个数(比如8,可以配置为Server进程的个数)的进程ID

5)Proxy遍历这些进程ID,执行kill发送信号,同时把bitmap对应的位置0(防止进程死了,不断被通知)

6)Server进程收到信号或者超时后从睡眠函数中醒来,把sigindex对应的bit置0,关闭通知

除了signal通知,其实还有很多通知机制,包括pipe、socket,还有较新的内核引入的eventfd、signalfd等等,我们之所以选择比较传统的signal通知,主要因为简单、高效,兼容各种内核版本,另外一个原因,是因为signal的对象是进程,我们可以选择性发送signal,避免惊群效应的发生。

防滚雪球控制机制

前面已经说过,原来的防滚雪球控制机制是基于网卡收包时间戳的。但现在server拿不到网卡收包的时间戳了,只能另寻新路,新的做法是:

Proxy收包的时候把收包时间戳保存起来,跟请求包一起放到队列里面,server收包的时候,把这个时间戳跟当前时间进行对比。

这样能更有效的做到防滚雪球控制,因为我们把这个包在前面的环节里面经历的时间都考虑进来了,用图形描述可能更清楚一点。

性能验证

使用shm_queue和raw socket后,DB接口机处理性能基本跟原来未分set的性能持平,新加的proxy进程占用的CPU一直维持在单CPU 10%以内,但摊分到多个CPU上就变成非常少了(对于8核的服务器,只是增加了1.25%的平均CPU开销,完全可以忽略不计)。

最后,分set的这个版本已经正式上线运行一段时间了,目前状态稳定。

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

推荐阅读更多精彩内容