[关闭]
@citian3094 2016-08-24T14:16:26.000000Z 字数 8665 阅读 1586

如何应对节日高峰—QQ会员活动运营系统架构实践

未分类


主持人:最后一场了,给ArchSummit做一个广告,我们这次在讲师的选拔上,包括在PPT准备上,都是比较高的水平,也希望大家在后续给我们继续对健讲师,如果大家有同事或者朋友自己做的东西还可以,我们可以自己推荐,其实真正的高手在民间,很多做技术的人不太想抛头露面,或者站在讲台上很紧张,但从实际经验来看我们的讲师做得非常好,如果有同事或者朋友愿意分享,希望大家帮我们推荐。
下面同样来自腾讯的徐汉彬,给大家分享《腾讯QQ会员活动运营系统架构实践》。汉彬之前做了非常多努力,我们谢谢他。

徐汉彬:各位同学,大家下午好!今天要分享的主题是关于QQ会员活动运营平台的架构实践。首先做一个简单的自我介绍,我叫做徐汉彬,现在在腾讯的SNG增值产品部工作,主要负责QQ会员特权以及今天分享的系统。

今天我要分享的内容主要介为三部分:

  1. 介绍QQ增值业务在海量请求下的技术挑战以及背景;

  2. Web系统高并发场景下综合优化策略;

  3. 平台高可用的建设实践。

既然是活动运营平台,首先什么是活动?活动就像PPT上看到的这四个页面。活动有很多特性,在今天的主题主要关注的点是节假日高流量的推广,例如说五一是典型的节假日,各个业务都有他们的推广需求,他们聚集在一起就会导致流量的突增。有的同学可能想问:楼主,我读书多,你不要骗我,我看你PPT上放的4个页面非常简单,你今天分享的这个所谓的系统会不会没有什么技术含量?是的,如果我的系统上只放PPT这4个页面确实没有什么技术含量,但是如果把这4个活动换成800+个,我的系统上同时在线的活动有800+个呢?那么它就会变成一个相对来说更具有挑战性的问题。

这个系统叫做QQ会员活动运营平台,在内部简称AMS系统,承载QQ增值运营业务的Web系统,它有两点定位:

  1. 满足QQ增值业务需求的发展;
  2. 保证这个平台在海量用户面前的高可用,也就是稳定性

我们每天由用户触发的Web层的CGI请求有3-8亿,同时在线的活动有800+个(每个月新上线的活动大概450+个,只要是活动就有周期的,所以在线的是这个数目),我们这个系统背后涉及的存储和Sever超过100个,在典型节假日高峰的请求量7w/s。

我们涵盖了很多业务,包括QQ本身的还有腾讯游戏、个性化(表情)以及动漫阅读等等。举一个活动例子:大家参加的2016年春节红包活动,就是除夕下午抢的红包,活动的那天我就在公司值班,没得回去。那天我们系统承载了当时游戏礼包和阅读礼包的发放,那天我值班到第二天的凌晨3点,也就是说大年初一的凌晨3点,所以做这个系统有些时候还是蛮不容易的。

进入正题:关于Web系统高并发的综合优化策略。我们先聚集到一个指标——吞吐率。我们的吞吐量主要分为三个方面:

  1. 延时
    用户请求这个CGI响应耗时,举个例子,假设我们的CGI平均耗时是是200毫秒,我想办法把它优化到100毫秒,那么相同的单位时间里面系统的吞吐能力就提升了一倍。

  2. 单机性能
    我们期望通过更少的CPU、内存和系统开销支撑更高的并发数和更多的用户请求。

  3. 规模
    即机器越多,我能支撑的请求就越多,这里面需要在整体系统架构上支持平行扩容的能力。

降低延时的方法,天下武功唯快不破,跟快相对的就是慢,慢是一个怎样的行为?就是等待的过程。假设我们MySQL在处理一个进行复杂的查询,它比较慢,它没有办法响应,这个过程中究竟发生了什么事呢?我们从整个链路看,首先是浏览器端用户发起了连接,他在等待(对我们来说他在等待我们反向代理给他响应),方向代理在等待WebServer,WebServer在等待Server层,Server层在等待我们MySQL,当然这种链路真正在整个系统里可能还不止这么短,它是一个很长的链路。整个链路所有的环节都在等待。他们在等待的过程就是需要付出系统的开销和资源。

有同学可能挑战我,Server实现异步化不就行了吗?其实在我们的后台系统里面大部分的Server都已经实现了异步化,我们是采用协程(微线程)来实现的。大家可以这么想异步化:我在处理A任务,A任务遇到网络IO等待的时候我迅速切到B任务,其实A任务的现场必须得保留,那么A任务所占据的内存资源、数据、句柄连接和系统开销都不能释放出来,都必须保留等到下次处理,也就是你的资源并没有真正释放出来,整条链路不管是同步等待还是异步的,它都没有被释放,所以我们可以下一个比较小的结论:等待的过程就是对资源占据很浪费的过程。

我们可以从三个方面进行优化:

  1. 多级缓存和主动推送

  2. 既然我们知道等待是一个不好的过程,那么我们需要超时时间分类设置

  3. 非核心操作异步化,尽可能把CGI响应时间降低,把一条很长的链路尽快释放出来,能给其它请求使用。

多级缓存方面:
缓存是一个好东西,缓存的本质是用户离取我们数据端更近的东西,例如说浏览器本地的Cache、Server和Server之间内存Cache等等,Cache是非常经典的优化策略,被称之为万金油,通常是哪里不舒服就抹哪里,通常效果还不错。

但是活动运营里面是不一样的,有一个活动是新的,在这个活动上线之前所有人都没有参加过这个活动,在整个Cache链路从前端到后端没有地方会有Cache,这里面我对新的活动进行大规模推广会遇到一个问题:缓存穿透,我的缓存策略是无法生效的。

那怎么办呢?这里我们用主动推送,以2016年春节抢红包活动为例子,当时的高峰高达几十万每秒,几十万每秒这个请求哪怕对CDN地理分布式静态文件的服务都是有冲击的,我们提前几天把这些静态CSS资源和图片资源推送到用户手机终端,当用户真正参加活动的时候就相当于有本地缓存,根本没有网络请求。

实现过程是这样:我们向离线包管理系统申请一个BID对应的需要推送的离线文件,然后把BID写到URL里面去,我们手Q终端的WebView会拦截这个URL请求,发现有BID就会根据BID找到本地的存储文件,然后把它推送给WebView。通过这种方式避免了网络请求,只有真正找不到的时候才会请求CDN,通过这种方式我们解决静态文件的流量冲击问题。

那么动态呢?动态的文件我们这样做:在早期我们Server里面数据是保存在MySQL,MySQL在这种大流量并发冲击下通常支撑力是不够的,怎么办?我们通过数据同步的系统,不断地把这些数据从MySQL(我们认为比较弱的存储)同步到内存级的Cache服务上去,包括还有另外一些能在前端直接展示的内容(比如说一个活动的提示语)直接通过这个系统打包成静态JSON文件,再把静态JSON文件通过CDN分发出去,简而言之:就是把支撑力弱的东西通过这种推送的机制放到强的服务里。

可能有人说你讲得不明白,但是我们把这个蒙层一加后很多同学就看得非常直观,这就是非常经典的Server层和MySQL层之间引进的内存的Cache,只不过我们的内存Cache稍微复杂一点。

接下来是超时时间是否合适的设计,很多人说超时等待是不好的,我们直接设置短一点,对于一般业务可以这样做,但是我们可能不行,活动运营系统是接入多方业务的系统。比如说我们接入的业务组件超过800多个,仅游戏接入了160多款游戏,每一款游戏都需要服务Server实现,每一个服务Server的实现性能和响应时间都是参差不齐的,我们对应的接口数数以千计,这时候就很难通过一刀切的接口来设置这个超时时间。

超时时间如果设置太长会有什么问题呢?假设你有个服务流量比较大,假如你设置6秒,就会发生一种情况:整个链路资源在整整6秒的时候只处理了一个失败请求,原本可以处理几十个请求,但是现在整整6秒只处理一个失败请求,并且这个失败请求会引起用户的重试。为了避免这个问题应该怎么做呢?我们的做法是因材施教快慢分离:例如我们设置天天酷跑平均响应时间为100毫秒,那我设置为1秒的超时时间就够了。第二种是像新游戏,平均需要700毫秒的响应时间,我们把它设置为5秒钟的超时时间比较合理。我们通过这种方式动态的超时时间设置以及快慢分离的方法把它们隔离开来,最终使得整个系统的吞吐率比较难出现由于一种超时导致大量可控资源被占据的场景出现。

这是我们做到的效果图,大家可以看到,在我们的Nginx系统主框架逻辑内部耗时在CGI层大概只需要35毫秒,这35毫秒我们大概处理了将近10个流程,包括登陆态校验、配置读取、Session等安全检测的流程,当然可以看到平均的耗时还是需要100多毫秒,但是这100多毫秒主要耗时都是第三方不可控,比如说我请求一个和我们合作的游戏方的Server,它的耗时我们是不可控的,在可控的范围我们通过CGI的延时优化把它优化到35毫秒。

关于单机性能。我们是运营系统,当时的运营系统比较适合PHP开发,在PHP上有三个技术方案,这些我们当时都没有采纳。没有采纳的原因是基础服务的升级是需要兼顾业务场景和投入产出比。

  • 首先是HHVM和NodeJS是因为迁移成本太高,前面讲到我们的Server接入的服务非常多,我们PHP代码量是几十万级别的,这里的兼容性迁移是大成本。

  • 比较平滑的是Nginx+PHP-FPM,这个方案我们专门做了压测,Nginx本身会比Apache性能高很多,但在我们的业务场景里面我们发觉:在一台机里搭了一台Nginx,再加PHP Sever,在我们的压测中提升并没有那么明显。

我们最终选择的升级方案是:升级到Apache2.4的Event模式+PHP7。有必要说明我们以前使用的是老Apache的Prefork模式,这里粗略地提下Prefork和Event的两点区别:

  1. 进程
    Prefork是多进程模式,处理一个任务出一个进程;Event则是多进程多线程模式,会起数量比较少的进程,每个进程会有几十个线程,Event是出一个线程来处理任务的,线程通常比进程更轻量,这样可以让我们并发数更高;

  2. 长连接(keep-alive)
    在Perfork上如果保持长连接,在开始肯定会频繁地使用长连接的通道,但是通信完后它会保持一段时间,保持期间Perfork模式进程会被占据,除了等什么都不能做;在Event上它解决了这个问题,它用了专门的线程来保持这些长连接,当用户真正触发请求的时候,它再把请求给到后端的Worker线程,Worker线程处理完就把自己释放出来避免占据。

PHP7同比以前的版本主要是大的性能优化,主要通过减少CPU和内存方面占用使得占据的资源比以前更少。

在这个升级过程中有遇到什么问题?首先我们的版本跨度比较大,Apache2.0升级到Apache2.4,PHP5.2升级到PHP7,我们真正的升级如果一步到位会比较危险,所以我们先升级到过渡版本,PHP方面我们先升级到PHP5.6(当然我们是去年实施升级的,当时PHP7还不是正式版);除此之外我们还要解决线程安全的问题,以前多进程是不需要考虑线程安全,有一些扩展要同步跟上升级等等。

我们大概在今年4月底的时候进行了单机灰度,5月初在单集群全量发布。确实同比老架构从业务压测结果来看大概3倍的性能增长。从线上的CPU占据数据来看,我们也做到用更少的资源来支撑更高的并发、处理更多的请求。例如以前一台机器常规启500个进程,机器这时已经运作地比较满,但我们现在已经可以启到上千个线程。

三、关于规模
规模方面我们必须实现快速扩容与缩容。扩容这个行为本身在我们公司有丰富的运维工具支持,包括机器的安装、部署、启动等各方面都是高度自动化完成的。但是在我们当时扩容的时候我们依然要花一天多的时间,为什么?还是因为我们是活动运营系统,活动运营系统背后对接了很多发货接口,这些发货接口中有很多很敏感,例如发Q币,还有发一些游戏高价值的道具。一般我们通信有两步:加密签名校验和来源IP限制,每次扩容都需要新增IP并且审批,因此我们大部分时间都用在不是扩容中就在扩容路上。所以我们那的时候不是在扩容中,就是在去往扩容的路上,这是第一个问题;

第二个问题是机器持有成本的问题,活动运营是流量上串下跳的典型业务,持有机器太多平时会低负载,运维团队会挑战你的机器成本和预算,说你占据那么多资源会浪费。如果占据的机器太少,你在节假日支撑不住,瞬间七八倍峰值下来你发现你又有风险。

对于第一个问题的解决方案
我们通过搭建一个中转Proxy Server,我们把通信的IP进行收拢,收拢为中间的Proxy角色,我们的Proxy是采用协程实现的,性能比较高。我们内部再重新跟自己内部的Proxy实行签名校验和来源IP限制,简而言之我们把一些外部的授权变为内部授权,内部授权把它变成自动授权,以及会把一些中间的验证的过程尽可能做到自动化的验证过程。我们的扩容时间从原来的一天多缩小到一到两个小时,其中的根本点是减少大幅度的人工依赖。

怎么解决机器占有的问题?哪怕我们具备快速的扩容和缩容能力,也不希望天天变动我们的接网环境,一般来说我们希望我们接网环境能做一个安静的美男子,没有什么事大家别去动它。

首先,我们利用了运维团队提供的Linux Container,比如说一台24核的物理母机,上面分成8台虚拟子机,虚拟子机上进行业务混部(?),AMS只需要启动一台,平时非节假日我们可能用不到八分之一的CPU资源,到节假日的时候,八分之一的CPU资源不够,我就把其它业务的空闲资源拿过来用,这样突破八分之一的使用限制,我认为这些方案是对我们活动运营系统是量身订做的策略。可能有同学说会有评估不足的问题,但是配合前面讲的快速扩容能力还是可以很好地应对的。

最后一部分,关于平台高可用建设和实践。
既然是高可用,即不能随便挂,AMS系统每日的CGI请求增长是比较快的,我们的项目启动于2012年,2012年时每日PV是百万级的系统,之后每一年基本都是跨数量级的增长,一直到现在高峰的时候是8亿多的流量。我们的可用性经常受到挑战。接下来我讲的内容和别的讲师的可用性流量增长什么不一样呢?有两点:

  1. 首先我们面对的是活动导致流量上串下跳的业务;

  2. 我是这个系统的初始开发人员,当年只有一两个人的时候我就是它的开发,后来系统越变越大我开始成为它的负责人,如果我发现系统上面的一个坑,领导会说是谁造成了这个Bug,怎么这么深的坑,这时候大家目光向我聚齐,这时候自己挖了一个坑,自己掉下去,然后千辛万苦爬起来。我就想当年的我究竟抱着一种丧心病狂报复社会的态度挖下这么深的坑,然后来坑四年后的我呢?这会引起我强烈的反思,更多地会让我想这是什么场景下的设计?因为架构?因为年少无知?还是没有预料到未来的发展趋势?

我们把AMS早期的问题进行划分,主要分三个方面:

  1. 首先是存储问题,主要是缓存穿透

  2. 架构,在架构早期性能小的时候写了很多写死IP的行为导致单点问题很明显,即可能会出现这个点挂了,系统整体可用性都受到影响,还有局部影响全局的问题;

  3. 协作方面,系统越来越大,参与的人越来越多,协作的成本也开始变得越来越高,如果来了一个新人,如果他修改了一个模块,这个模块涉及到了三个同学,他就需要和这三位同学都做确认,如果哪天确认漏了,最终发的版本可能就是有问题的。

我们总结一下这些问题就是天灾和人祸。天灾是什么?比如说网络故障、机房停电、机器宕机、硬件故障。人祸方面包括异常发布(你发的代码有问题影响了全局)、人工配置失误和多人协作失误。这些问题在后来进行反思,原因就是单体架构(单片架构),即系统如果没有做合适的拆分,导致所有的代码糅合在一起,这就造成失误和很多问题。

于是,我们对系统进行合适的调整。Uinx哲学有一句话非常好,叫做“Do one thing and do it well”,我只做一样的东西并把他做好,对应的架构思想就是SOA,所谓的服务化,以及微服务。

我们做的第一件事就是L5名字服务,做到去中心化、无状态和平行扩充,简而言之,如果你要访问一个Server,不管它是什么先和L5服务要IP和端口,它就在一组服务表里按照一个分配算法随机取一个给你,你去请求它,如果成功了就要上报成功给它,失败也上报失败给它,这时候L5服务会计算出每一台机器的延时和成功率情况,并且可以负责把失败率高的机器踢掉,如果恢复正常就再加回来,把宕机的问题也解决了。

但是这里有同学有小疑问,所有人都请求L5服务,L5服务会不会扛不住?确实,如果都去请求它,L5压力会很大,所以L5的实践有两层,一层部署在本地Server的Client层,相当于本地路由表;还有Server层,扩容的时候就往Server层多添加机器,它就能从Server层发到本地层。另外吧主要的存储从MySQL慢慢迁移到CKV,CKV是我们公司内部研发的Key-Value分布式存储,可以类似理解为分布式Redis。

这是AMS早期的系统架构图,可以看出明显的单体现象,服务也不多,首先是CGI层,我们根据不同的功能、业务进行物理和业务上的拆分,拆分成一个个Web Server集群,后端全部用L5的方式接入,让它们无状态且支持扩容,同时把一些Server从原来的大Server拆分成小的Server。我们前面提到,我们的系统对应的存储Server超过100个,就是因为这个原因。当我们做完这一点以后,它们的耦合就得到了相对来说比较好的优化。

回到前面的问题,天灾怎么避免?网络故障可以通过合适的部署方案,例如我把这两批机器跨网络端部署,哪一天这边被一锅端或被挖断了,那边还能用。第二个关于机房停电,我们可以跨机房、跨IDC部署,我们的Web Server层部署在5个机房上,哪天哪个机房停电了,对我们是没有影响的。第三是硬件的故障,可以通过L5模式支持自动剔除和自动恢复。第四是服务进程挂掉,通过Shell脚本写的比较简单的监控进程把它重复拉起来。

做一个简单的汇总,我通过路由和L5的模式,以及合适的机器部署情况做跨网络、跨机房的部署,使得我整体的可用性能够承受各种各样的天灾的袭击。

解决完天灾,就要聊一下人祸。首先是配置可用性的问题,配置可以说是业界的难题,为什么呢?因为在我们公司也好、业界也好,很多很大的事故本身不是多么高大上的Bug导致的,通常是一份配置多一个参数、少一个参数、改错一个参数,都导致很严重的问题。尤其我们一个月上线450多个活动,每个月对应上线的活动配置5000多份,怎么杜绝参数错误的问题呢?

大家可以看到我上面所写的这个图。假设这个运营同学提交了一份业务配置,假如这个业务配置是有问题的,首先我们会进行人工测试,如果人工测试无法发现就会留落到现网,如果能发现就能避免,举个例子,我们库存一共100个,但这个运营同学填写200个,这个问题测试能发现吗?不能,因为只有发送到101的时候才会发现问题出来。对我们来说怎么避免问题?我们的做法是在运营同学发布前建立智能程序检测的阶段,首先回到前面的例子,你的库存不对,提交的时候直接把库存两边拿过来对比一下,发现不对就直接提示你,当对了之后再让你通过。有人可能好奇说,你的规则怎么来的?这几十的规则就是活鲜鲜的这么多年的血泪史,现网每出一单事故,我们就把事故拿出来提炼、抽象、讨论,看能不能成为规则的一部分。解决配置问题,我没有说放之四海皆通的解决方案,更多是跟着业务亦步亦趋地发现和解决。我们通过这种方式来解决我们的配置的问题。

有朋友说,关于可用性,能不能用一个比较收拢的例子对前面所讲的事情进行概括呢?我们一起来讨论一个场景,假设有一个新同事刚进来,他修改了一个模块,这个模块修改后可能有问题,有没有办法通过可用性和架构的建设来减轻或者避免这个问题?可能同学会说,大家都是人嘛,人犯的错误还能干预吗?

我们认为是可疑的,我们分为事前、事中和事后:

  1. 事前
    如果你的系统是单体架构,那么里面模块与模块之间的耦合会很重,如果不把里面的代码分离和隔离,耦合是天然的,就会有人写耦合代码。所以我们把单体架构经过合理拆分,我们把程序变得更简单,协作更少,让新人看到代码更少。首先从根源和架构上面尽可能避免新人犯错;其次,如果是单体架构,哪怕只是修改了"helloworld",从测试完整的角度出发是需要把单体架构上所有的逻辑都回归一次,这样回归的成本是很高的。但如果拆分过的话,只需要回归一个小的模块,因为小的模块的局部测试就可以比较轻松回归。你可以做到三点,程序更简单、更少协作、更容易测试,尽可能从源头上面避免新人出问题。从软件的生命周期出发,人员总是会变换的,总会有新人加入和人员调整,所以必须考虑新同学加入的门槛成本问题;

    另外一个是建立自动化测试,上线之前跑一下自动化测试用例,建立灰度、观察和全量模式,哪怕有问题避过了前面我们尽可能快的发现出来,如果事前挡不住了就到事中;

  2. 事中
    。我们通过对服务的分离以及对架构物理的隔离。以前是一个单体,如果有人写了一段代码引起CPU100%占用,可能单体所有机器受到影,但如果是业务上物理隔离的,它只影响到自己的小模块,我们通过架构缩小有问题的发布影响

    第二个是建立多维度的监控能力,比如说前端CGI响应、L5、模块之间的调用成功率的监控,使得他们能够更快地发现问题,前面是缩小影响范围,后面是减少影响时长。

  3. 事后
    你必须建立与发布与之同等级的回滚能力,不能让再发一个新的版本,更多的应该是最好有一个按钮点一下让它回滚回来。
    最后一个是有可追诉的日志,你可以把受影响的用户范围和相关的东西慢慢的统计出来。

我今天的分享就到这里,谢谢大家!

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注