应对节日高峰-Web架构实践

今天要分享的主题是关于QQ会员活动运营平台的架构实践。首先作一个简单的自我介绍,我叫徐汉彬,如今在腾讯的SNG增值产品部工做,主要负责QQ会员生活特权以及今天分享的AMS系统的研发建设。前端

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

  • QQ增值业务在海量请求下的技术挑战以及背景;
  • Web系统高并发场景的综合优化策略;
  • 平台高可用的建设实践。

活动有不少特性,今天的主题主要关注点是在节假日高流量的推广,例如五一是典型的节假日,各个业务都有他们的推广需求,他们汇集在一块儿就会致使流量的突增。有的同窗可能会有疑问:我看你PPT上放的几个活动页面都很是简单,你今天讲的这些AMS系统会不会没有什么技术含量?是的,若是咱们的系统上只放这几个活动页面确实没有什么技术含量,可是若是把这几个活动换成800+个,即咱们的系统同时在线的活动有800+个呢?那么它就是一个相对来讲更具备挑战性的场景。后端

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

  • 知足QQ增值活动业务需求的发展;
  • 保证平台在海量用户面前的高可用,也就是稳定性。

咱们天天由用户触发的Web层CGI请求有3亿-8亿,同时在线的活动有800+个(每月新上线的活动大概450+个,活动的在线周期一般是1个月,不断有活动上线和下线,因此同时在线的是这个数目),咱们这个系统背后涉及的存储和Server超过100个,在典型节假日高峰期的请求量为7w/s。缓存

AMS系统涵盖了不少运营业务,包括QQ、腾讯游戏、个性化(表情)以及动漫阅读等。举一个例子:你们参加的2016年QQ春节红包活动,就是除夕下午抢红包,当时AMS系统就承载了红包活动的游戏礼包和阅读礼包的发放,所以,春节过年我不能回家,在公司值班到次日的凌晨3点,也就是大年初一的凌晨3点,因此作这个系统有时候也是不容易的。安全

Web系统高并发场景的综合优化策略

关于Web系统高并发的综合优化策略。咱们先汇集到一个指标上——吞吐率。咱们的吞吐量主要分为三个方面:性能优化

  1. 请求延时:用户请求CGI的响应耗时,举个例子,假设咱们的CGI平均耗时是是200毫秒,我想办法把它优化到100毫秒,那么相同的单位时间里面系统的吞吐能力就提高了一倍;
  2. 单机性能:咱们指望经过更少的CPU、内存和系统开销支撑更高的并发数和处理更多的用户请求;
  3. 规模:即机器越多,我能支撑的请求就越多,这里面须要在总体系统架构上支持全面的平行扩容能力。

针对上述吞吐量的三个方面,咱们对应提了如下解决方案:服务器

1、下降CGI请求延时

天下武功惟快不破,跟快相对的就是慢,慢是一个怎样的行为?它的本质其实就是“等待”。假设咱们MySQL在处理一个复杂的查询,它比较慢没有办法响应给后端Server,这个过程当中究竟发生了什么事?咱们从整个链路看,首先是浏览器端用户发起了链接,它在等待服务器响应(对咱们来讲它在等待咱们反向代理给它响应),反向代理在等待Web Server,Web Server在等待Server层,Server层在等待MySQL,固然这种链路真正在集群系统里远不止这么短,它极可能是一个更长的链路。整个链路全部的环节都在等待,它们等待的过程就须要付出系统的开销和资源。网络

有些同窗可能会提出疑问,Server实现异步化不就好了吗?其实在咱们的后台系统里大部分Server都已经实现了异步化,咱们是采用协程(微线程)来实现。实际上异步化的过程:程序在处理A任务,A任务遇到网络I/O等待时程序迅速切到B任务,但A任务的现场必须得保留,那么A任务所占据的内存、数据、句柄链接和系统开销资源都不能马上释放出来,都必须保留等到下次继续使用,也就是机器的资源并无真正释放出来,整条链路无论是同步等待仍是异步它都没有被释放,因此咱们能够下一个比较小的结论:等待的过程就是对资源占据浪费的过程。多线程

咱们能够从三个方面进行优化:

  • 多级缓存和主动推送;
  • 既然咱们知道等待是一个很差的过程,那么咱们须要合适的超时时间设置;
  • 非核心操做异步化,尽量把CGI响应时间下降,把一条长的链路尽快释放出来给其它请求使用。

接下来仔细讲讲这三方面:

第一,多级缓存和主动推送

缓存是一个好东西,缓存的本质是让用户离咱们的数据端更近,例如浏览器本地的Cache、Server和Server之间内存Cache等等,Cache是很是经典的优化策略,被称之为万金油,一般是哪里不舒服就抹哪里,并且效果还不错。

可是,它在活动场景里就比较不同了,好比一个新活动,在这个活动上线前全部人都没有参加过这个活动,整个Cache链路从前端到后端没有地方会有Cache,这里面咱们对新的活动进行大规模推广会遇到一个问题:缓存穿透,咱们的缓存策略是没法直接应对的。

那怎么办呢?咱们采起的办法是主动推送,以2016年春节抢红包活动为例,当时高峰高达十万级每秒,对于十万级每秒的页面请求即便对CDN地理分布式静态文件服务都是有冲击的,咱们真正的作法,实际上是提早几天把这些静态CSS、JS和图片等资源推送到用户手机终端,当用户真正参加活动的时候就至关于有本地缓存,此时没有产生网络请求。

实现过程粗略归纳大概是这样:咱们向离线包管理系统申请一个BID对应的须要推送的离线文件,而后把BID写到URL的参数里面去,咱们手Q终端的WebView会拦截这个URL请求,发现有BID就会根据BID找到本地的离线文件,而后把它直接给WebView。经过这种方式避免了网络请求,只有真正找不到的本地离线文件时才会经过网络请求CDN,咱们利用这种方式解决静态文件的流量冲击问题。

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

可能从PPT上看,这里还不够形象,可是咱们把这个蒙层一加后不少同窗就看得很是直观,这就是很是经典的Server层和MySQL层之间引进的内存的Cache的模式,只不过咱们的内存Cache层的实现稍微复杂一点,有一个推送和同步数据的机制。

第二,超时时间设置

关于超时时间的合适设置,不少人认为超时等待时间过长很差,因此干脆直接设置短一点的超时时间。对于通常业务能够这样作,但对活动运营系统比较不合适,由于,活动运营系统是接入多方业务的系统。好比咱们接入的业务组件超过800个,仅游戏就接入了160多款游戏,每一款游戏都是一个独立而且复杂的外部服务Server,每个服务Server的性能和响应时间都是良莠不齐的,咱们对应的通讯接口数以千计,这时候就很难经过一刀切的方式来设置这个超时时间。

超时时间若是设置太长会有什么问题呢?假设有个服务流量比较大,若是你设置6秒而且该服务发生超时,就会发生一种状况:整个链路的系统资源在整整6秒的时候只处理了一个失败请求,本来能够处理几十个请求,可是如今整整6秒只处理一个失败请求,而且这个失败请求还会引发用户的重试。

为了不这个问题应该怎么作?咱们的作法是:因材施教,快慢分离,例如咱们设置每天酷跑平均响应时间为100毫秒,那我设置为1秒的超时时间就够了;例如某款新游戏平均须要700毫秒的响应时间,咱们认为把它设置为5秒超时时间比较合理。咱们经过这种方式动态的超时时间设置以及快慢分离的方法把它们隔离开来,最终使得整个系统的吞吐率不容易出现因为某个服务超时致使大量可控资源被占据的场景出现。

第3、非核心操做异步化

非核心操做异步化,该优化方式比较常规,就不展开详细的讲述。简而言之,就是将非核心操做直接提交到异步队列中,不等待响应结果,目的是尽量把CGI响应时间下降,把一条很长的链路尽快释放出来给其它请求使用。

CGI延时优化成果

下图是咱们作到的优化成果,你们能够看到在咱们的AMS系统主框架逻辑内部耗时在CGI层大概只须要35毫秒,这35毫秒咱们大概处理了将近10个流程,包括登录态校验、活动配置读取、Session等安全检测的流程,固然能够看到平均的耗时仍是须要100多毫秒,可是这100多毫秒主要耗时在于不可控的外部第三方,好比说我请求一个和咱们合做的游戏方的Server,它的耗时咱们是不可控的,在可控的范围咱们经过CGI的延时优化把它优化到35毫秒。

2、提高Web服务单机性能

由于咱们是活动运营系统,考虑到活动运营系统的灵活多变的特色,它比较适合用PHP开发,所以业务逻辑实现主要采用了PHP编写。然而,随着AMS系统规模的逐步扩大,日请求规模从2012年的百万级一直增加到如今的8亿级别,WebServer和PHP的性能不足问题日益突出。

基础软件服务的升级,一般是一件吃力不讨好的事情,由于若是作得好,活动业务侧不必定能感知获得,可是,若是升级搞出问题,则要承担比较严重的后果。并且,AMS系统上不少都是营收活动,和金钱收入直接挂钩,对这个系统作基础升级,并非一件轻松的事情,勇气和安全风险可控的升级策略,缺一不可。

在WebServer的性能优化方面,咱们考虑了三个方案。咱们最终都没有采纳HHVM和Node升级安全,没有采纳的缘由是基础服务的升级是须要兼顾业务场景和投入产出比的考虑。

首先是HHVM方案和NodeJS方案都是由于迁移成本过高,前面提到咱们的Server接入的服务很是多,咱们PHP代码有四十多万行,比较多的业务PHP扩展和组件,这里的兼容性迁移是大成本,而且,迁移风险也比较大;

PHP7+Apache2.4(Event)的升级方案是比较平滑的,由于这个方案便可以兼容业务代码,又能够比较可观的提高单机性能和Web Server并发能力。

咱们最终选择的升级方案是:Apache2.0升级到2.4的Event模式和PHP5.2升级到PHP7.0。有必要简单介绍下咱们之前使用的是老Apache的Prefork模式,这里粗略地提下Prefork和Event的两点区别:

MPM模式:Prefork是多进程模式,一个任务对接一个服务进程;Event则是多进程多线程模式,会启数量比较少的进程,每一个进程会有几十个线程;Event是出一个线程来处理任务,线程一般比进程更轻量,这样可让咱们并发数更高;

长链接(keep-alive)问题:咱们不少Web服务都会开启HTTP长链接,用于减小HTTP链接的创建和断开的系统开销。长链接,一般在刚开始确定会频繁,但通信完后它会被保持一段时间,保持期间Perfork模式的服务进程会被占据,除了等什么都不能作,直到长链接断开为止,这是一种系统资源的浪费;在Event模式下,它解决了这个问题,它用了专门的线程来保持这些长链接,当用户真正触发请求的时候,它再把请求给到后端的工做线程,工做线程处理完就把本身释放出来避免被占据,而后工做线程就能够继续为其余请求提供服务。

PHP7同比之前的版本区别主要是大的性能优化,对CPU和内存资源方面的占用比之前更少。

在这个升级过程当中有遇到什么问题?

  • 首先咱们的版本跨度比较大,Apache2.0升级到Apache2.4,PHP5.2升级到PHP7,咱们真正的升级若是一步到位会比较危险,因此咱们先升级到过渡版本,PHP方面咱们先升级到PHP5.6(另外咱们是去年实施升级的,当时PHP7的正式版尚未发布);
  • 除此以外咱们还要解决语法兼容性、解决线程安全的问题(之前多进程是不须要考虑线程安全);
  • 有一些扩展要同步升级等等;
  • 比较系统的控制风险策略:逐步升级、灰度观察、现网PHP7运维和监控工具支持等。

咱们大概在2016年4月底的时候进行了单机灰度,5月初在单集群全量发布,在日请求亿级的Web系统中,在国内属于比较早升级到PHP7的。PHP7的AMS对比老AMS,从业务压测结果来看大概有3倍的性能提高。从线上的CPU数据来看,咱们实现了用更少的资源来支撑更高的并发、处理更多请求的目标。例如,之前一台普通硬件配置的机器启动500个进程,机器这时已经运做地比较满,但咱们如今已经能够启到上千个线程。

3、关于规模——支持快速平行扩容

咱们必须实现快速扩容与缩容。扩容这个行为自己在咱们公司有丰富的运维工具支持,机器的安装、部署、启动等各方面都是高度自动化完成的。可是,之前咱们扩容是依然要花一天多的时间,为何?仍是由于咱们是活动运营系统,活动运营系统背后对接了不少外部发货接口,这些发货接口中有不少是比较敏感的发货服务,例如发Q币,还有发一些游戏高价值的道具。

通常咱们和敏感业务服务通讯有包含两步:加密签名校验和来源IP限制,每次扩容都须要新增IP,而后这些IP须要向各个敏感业务提出申请,让对方的业务领导进行人工审批,加到来源IP限制的白名单中,才能生效。所以,咱们大部分时间都花费在权限审批上,用一句话来总结问题就是:不是在审批中,就是在去往审批的路上。

咱们的解决方案是经过搭建一个中转Proxy Server把通讯的IP进行收拢,收拢为中间的Proxy角色。咱们内部再从新跟本身的Proxy实行签名校验和来源IP限制,只要咱们的Proxy Server不进行扩容,咱们就不须要从新申请IP权限审批。简而言之,咱们把一些外部的受权变为内部受权,内部受权尽可能作成自动受权,以及会把一些中间的验证的过程尽量作到自动化验证。咱们的扩容时间从原来的一天多缩小到1-2个小时,其中的关键点是大幅度减小人工依赖。

第二个问题是机器持有成本的问题,活动运营业务由于严重受到节假日效应的影响,是个流量上串下跳的典型业务。所以,若是咱们部署机器太多,平时会利用率不足而致使机器低负载,运维团队会挑战咱们的机器成本和预算,说咱们占据那么多资源会浪费;若是部署的机器太少,咱们在节假日又支撑不住,瞬间七八倍峰值的增加风险又很大。怎么解决机器占有的问题?虽然咱们具有快速的扩容和缩容能力,可是在通常状况下,咱们也不但愿每天变动咱们的现网环境,一般咱们但愿咱们现网环境能作一个安静的美男子,没有什么事你们别去随便变动它。

因而,咱们利用了运维团队提供的Linux Container虚拟化技术,主要是共享CPU资源支持。例如,一台24核的物理母机,上面分红8台虚拟子机,虚拟子机上进行业务混合部署(不一样业务各自占据一台子机),AMS也只占据一子机器,平时非节假日咱们具有高优先级使用1/8的CPU资源,但平时可能用不到,到节假日的时候1/8的CPU资源不够,咱们就把其它业务的空闲CPU资源拿过来用,就能够突破1/8的CPU的使用限制。实际上,这种CPU共享的技术方案,我感受是为活动运营类型系统量身订作的策略。可能有同窗说提出疑问,这样会带来后续扩展评估没法精准评估的问题,不过,咱们配合前面讲的快速扩容能力仍是能够很好地应对的。

关于平台高可用建设和实践

既然是高可用,即不能随便挂掉,AMS系统每日的CGI请求增加是比较快的,咱们的项目建立于2012年,2012年时每日PV是百万级的系统,以后每年基本都是跨数量级的增加,一直到如今高峰的时候是8亿多的流量,咱们的可用性屡次受到比较严峻的挑战。

一样是基于大流量Web系统下的高可用建设实践,那我讲述的又和其余讲师的有什么地方不同呢?主要有两点:

首先咱们面对的是活动运营业务场景下,活动在节假日流量峰值突增变化幅度是比较大;

我是AMS系统的初始开发人员,对这个高可用建设实践有比较深入的体会。最初只有两我的的时候我就是它的开发人员,后来系统越变越大,我就慢慢地成为了AMS的负责人。若是咱们在系统上面发现一个年代就远的坑,有很是大的可能就会追溯到我本身,这种感受至关于,本身挖了一个坑本身跳下去,而后千辛万苦爬起来。我就会想,当年的我究竟抱着怎样一种丧心病狂、报复社会的心态挖下这么深的坑,而后来坑害四年后的我呢?这些都会引发我强烈的反思,当年是在什么场景下作出的这个设计?是由于我太年轻?仍是由于没有预料到将来的业务发展趋势?这些追溯性的反思,促使我更深刻的检讨和思考系统上的设计和缺陷,同时,也加深了我对架构演变的认知水平。

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

  • 存储:主要是缓存穿透;
  • 架构:在架构早期流量规模比较小的时候很多写死IP的行为致使单点问题很明显,便可能会出现这个点挂了,系统总体可用性都受到影响,还有局部影响全局的问题;
  • 协做:系统愈来愈大,参与的人愈来愈多,协做的成本也开始变得愈来愈高,若是来了一个新人修改了一个模块,这个模块可能涉及到了三个同窗的相关模块,他就须要和这三位同窗都作确认,若是哪天确认漏了,最终发的版本可能就是有问题的。

咱们总结上述各种问题为天灾和人祸。天灾包括网络故障、机房停电、机器宕机、硬件故障;人祸方面包括异常发布(例如某开发的代码有问题影响了全局)、人工配置失误和多人协做失误。咱们后来对这些问题进行了反思,它们之因此会比较频繁地发生的关键缘由就是单体架构(单片架构),即系统若是没有作合适的拆分,致使全部的代码或者服务糅合在一块儿,这会形成比较多问题。尤为是AMS系统是由小变大的,系统小的时候,一般就是一个单体。

因而,咱们对系统架构进行合适的调整。Uinx哲学有一句话很是好

“Do one thing and do it well”

我只作同样的东西并把它作好,对应的架构思想就是SOA和微服务,微服务近几年也被你们谈论得比较多。

咱们作的第一件事是L5名字服务,作到去中心化、无状态和平行扩充,简而言之,若是你要访问一个Server,无论它是什么都须要先向L5服务要对应服务的IP和端口,L5就从一组服务路由表里按照一个分配算法随机取一个给你,而后你再去请求它,若是成功了就要上报成功给它,失败也上报失败给它,这时候L5服务会计算出每一台机器的延时和成功率状况,而且能够将失败率高的机器踢掉,若是恢复正常就再加回来,从而,把偶然宕机或者机器异常的问题解决了。

可是这里有些同窗可能会有疑问:全部人都请求L5服务,L5服务会不会扛不住?确实,若是全部服务都去请求它,L5压力会很大,因此L5的实际分为两层:

一层部署在本地Server的Client层,至关于本地路由表(服务器本地Server);

另外一层是Server层,扩容的时候就往Server层添加机器IP,它就能从Server层下发到各个服务器的本地层。

另外咱们把主要的存储从MySQL慢慢迁移到CKV,CKV是咱们公司内部研发的Key-Value分布式存储,能够理解为相似的分布式的Redis存储。

下图是AMS早期的系统架构图,能够看出某些模块有比较明显的单体现象,后端服务数也很少。

架构通过多年演变以后以下图,首先是CGI层,咱们根据不一样的功能、业务进行物理和业务上的拆分,拆分红一个个Web Server集群,后端所有用L5的方式接入,让它们无状态且支持平行扩容,同时把一些Server从原来的大Server拆分红小的Server。当咱们作完这一点之后,它们的耦合问题获得了相对来讲比较好的优化。

回到前面的问题,天灾怎么避免?咱们归纳以下:

  • 网络故障:创建合适的机器部署方案,例如把两批机器跨网络端部署,哪一天这边被一锅端或被挖断了,另外一边边还能用;
  • 机房停电:咱们能够跨机房、跨IDC部署,咱们的Web Server层就分别部署在5个机房上,哪天哪一个机房停电了,对咱们是没有大的影响的;
  • 硬件故障:能够经过L5模式支持自动剔除和自动恢复;
  • 服务进程挂掉:经过Shell编写的监控进程脚本把它从新拉起来。

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

咱们聊一下人祸。首先是人工配置问题,配置能够说是业界的难题,为何?由于在咱们公司、甚至说业界,不少大型的现网事故自己并非由多么高大上的Bug所致使,而是由配置文件引发,例如,配置多一个参数、少一个参数、改错一个参数,进而致使很严重的问题。尤为咱们一个月上线450多个活动,每月对应上线的活动配置多达5000多份,怎么杜绝参数错误的问题呢?

以下图。假设这位运营同窗提交了一份有问题的业务配置,首先咱们会进行人工测试环节,若是人工测试没法发现,问题就会留落到现网。举个实际的例子:咱们库存一共100个,但这个运营同窗填写200个,这个问题测试能发现吗?不能,由于只有发送到101的时候才会出现异常。

对咱们来讲怎么避免?咱们的作法是在运营同窗发布前创建智能程序检测的环节,回到前面的例子,库存和配置数量不匹配,提交时咱们直接把库存两边拿过来对比一下,发现不匹配就直接提示,直到配置正确了以后再让发布经过。那么咱们的规则怎么来?这几十个规则就是活鲜鲜的多年血泪史(多么痛的领悟),现网每出一单事故或者问题,咱们就把事故拿出来分析、讨论、抽象和总结,看能不能成为检测规则的一部分。解决人工配置难题,我认为没有说放之四海皆通的解决方案,更可能是跟着业务亦步亦趋地共同发现和解决。

关于可用性,能不能用一个比较收拢的例子对前面所讲的事情进行归纳呢?咱们一块儿来讨论一个场景,假设有一个新同事刚加入公司,他修改了一个模块,这个模块修改后可能有问题,有没有办法经过可用性和架构的建设来减轻或者避免这个问题?

也许有同窗会质疑,人犯的错误难道还能干预和改变?

咱们认为是能够作一些工做的,咱们分为事前、事中和过后:

第1、事前

若是系统是单体架构,那么里面逻辑会很复杂,模块与模块之间的耦合会很重,若是不把里面的代码分离和隔离,耦合是自然的,就会有人写出耦合的代码。因此咱们把单体架构通过合理拆分,咱们把程序变得更简单,协做更少,让新人看到代码更少,更简单。首先从根源和架构上面尽量避免新人犯错。

其次,若是是单体架构,哪怕只是修改了一句简单的输出"hello world"的代码,从测试完整的角度出发是须要把单体架构上全部的逻辑都回归一次,这样回归测试的成本是很高的。但若是拆分过的话,只须要回归一个具体的小模块,小模块的测试就能够比较轻松完成回归测试。

这样能够作到三点:程序更简单、更少协做、更容易测试,尽量从源头上面避免新人犯下错误。从软件的长远生命周期出发,人员老是会变换的,总会有新人加入和人员调整,因此必须考虑新同窗加入的门槛成本问题。

另一个是创建自动化测试,发布以前跑一下自动化测试用例,创建灰度、观察和全量模式,即便有问题咱们也争取尽量早地发现它,若是事前挡不住了就到事中。

第2、事中

咱们经过对服务的分离以及对架构的物理隔离。之前是一个单体,若是有人写了一段代码引发CPU100%,可能单体内全部的机器都受到影响,但若是是业务上物理隔离的,它只影响到本身对应的业务或者功能模块,咱们经过这种架构隔离来缩小问题的影响范围。

另外是创建多维度的监控能力,好比说前端CGI响应、L五、模块之间的调用成功率、流量波动等监控,使得问题可以更快更早地被咱们发现。

总结一下:经过比较合适的架构设计和多维度监控能力,缩小问题的影响范围,减小问题的影响时长。

3、过后

咱们必须创建与发布对应的回滚能力,不能想办法再发一个新的版本去修复,而应该优先恢复现网系统正常,所以,发布系统应该要具有一个按钮,点一下让它回滚到上一个正常的版本,也就是一键回滚能力;另外还须要有可追诉的日志记录,能够把受影响的用户和相关的数据的统计出来,进行后续处理。

我今天的分享就到这里,谢谢你们!