高并发架构解决方案总结
最近公司新起了几台服务器准备要搞一次大规模的红包活动(我们客户端应用及服务端业务的分发是用node在做,活动、积分、抽奖、财务、用户管理等是用其他语言在做,我们本次活动的数据都来自于node的转发或推送)。由于之前一直政策的原因很多棋牌游戏不能推,所以,趁着新舂伊始赶紧搞一些活动回流一下“老顾客”、吸引一下“新顾客”。顺利做完了这次活动,我把从这次高并发活动中学习到的经验,以及之前分布式爬虫项目及aws集群海外部署经历中涉及到的高并发的经验总结一下。
高并发这个事情要搞的前提是你对网络的分层有一个比较深刻的理解,现在业界关于传输协议有三个版本。7层版本、5层版本和4层版本。
1. 七层版本。 七层就是指OSI(Operation System Interconnect)七层协议模型,主要是应用层(Application)、表示层(Presentation)、会话层(Session)、传输层(Transport)、网络层(Network)、数据链路层(Data Link)、物理层(Physical)。 2. 五层版本。 五层为OSI和TCP/IP综合的版本,是生产中许多具体的使用都是符合五层协议的版本,算是一种非官方靠“民间”俗成的归纳。五层体系结构包括:应用层、运输层、网络层、数据链路层和物理层。 3. 四层版本。 四层就是纯的TCP/IP传输协议了。主要包括:应用层、运输层、网际层和网络接口层。如果把它们综合来看,其实用四层模型可以解释五层和七层的模型,用五层也可以解释七层的模型,四层五层只是把七层模型中一些层又进行了归类。
回到我做的这个,大规模的红包活动中来。我们虚拟币的代理商为了调动当地的玩家的兴趣,主动拿出一部分钱活跃用户(只针对本服用户)。另一方面,平台为了调动代理商积极性,推出充值满多少送多少活动,最终实打实到用户手里的钱是由公司买单的,但是没办法,为了玩家回流平台,所以我们来分析怎么来做这个有着高并发特征的活动最合理。奖池的奖金总量一定,但是由于是棋牌游戏,特别讲求的是“同屏”,所以用户从不同的服务器通过用户身份验签登录,最终加入房间游戏,或者被邀请加入房间参与游戏,这个时候最好的方式就是虽然用户在不同机器进行登录,但是在游戏的时候,要能保证玩家都在同一台服务器上进行游戏。这个活动是准备在晚上8点推给游戏用户的,由于我们不同的游戏,分成不同的组,有些规模大的游戏,比如说跑胡子,可能背后的服务器集群是很大,但是有些游戏,像永州麻将,云南麻将、四川麻将等地域玩法区分度很大的游戏,其实游戏人数规模不是特别大,针对服务器并发请求的压力上不是特别大。所以要解决的问题实际就是那些爆火的游戏,当公告一推出,瞬间很多人点击链接去抢红包时候,如何能保证服务器有能力应付这种高并发情况?
对于我们线上正运行的这款用户体量特别大的游戏,搞这样一次活动,在活动公告推出的瞬间肯定会造成高并发的场景出现,要解决问题,抵得住抢红包的游戏中瞬间激增的并发压力,下面这些事是非做不可的。
先说立马可以用得上的。按照SWOT原则把自己手里攥的资源的优势、劣势、机会、威胁都列出来,找几个最痛的点。比如说我们当时分析,威胁就来自于以下三类人。普通用户这一类,比如说我们做红包活动,可能参与的人有十几万,但是实际我们后台添加活动,允许发放出的红包就3000个,用户可能点击了一次,看没什么反应,又点啊点,请求中80%的无效请求都这么来的,所以浏览器端或者app端,要对用户的操作进行限制,点击之后多少秒之内按钮置灰,不允许用户再点了。加上用户身份验证,如果你是世界视角,所有个体各自独立,那么,对应相应规则生成全局唯一的uid,如果你是各成独立王国,而是通过应用名称或者其他维度来运营的,那么,各体系维护一个自己的服务器数据,只有涉及到数据概况统计的时候,才把带独立王国的数据汇总到一起排名。第二种用户可能就是程序员或者懂点技术的那波人。他客户端就写loop,就是一顿往你后端接口做请求,这样你浏览器端、app端那些限流的举措就拦不住他了,这就到了后台服务端,我就靠咱们说这些全局唯一的uid或者是独立王国的uid(总都会有个唯一确认身份的维度),用他来做拦截,同一uid每秒只能请求一次,再发请求就给他响应“请等待”。第三种用户就是那种手里“操纵”着几千几万甚至几十万“肉鸡”的人,这个时候就要上缓存了,因为你预先知道我们这波活动只有3000个红包,那么,你放进几万几十万的请求到数据库有什么意义呢?所以直接把请求放到队列里面,然后一个一个处理,我们活动的做法是,把需要写库的3000个有效请求放到队列,因为用户抢到了钱包,先给一个恭喜您获得钱包,然后给用户一段话,要关注公众号xx,回复领红包会返回给你一个链接,因为我们手机端的游戏注册的方式就是一定要拉起微信做授权,然后第二步让你完善手机号等信息,所以,你针对我们公众号就是个已知的用户,你在app外操作抢的红包并且已经中奖,但是你只要关注了公众号你的unionid在我们系统里就是唯一的,你抢红包并中奖那一刻我们只把你中奖及中奖金额这些写到“用户获奖表”里了,然后你公众号回复“领红包”的时候,我们再判断你是否中奖,然后结合第三方红包业务公司接口为你领红包这个动作创建一个链接,然后你点击链接之后红包就到账了。
同时,由于服务器的多地部署,棋牌游戏又属于“世界服”系的游戏,用户在自己的客户端登录游戏,无论这用户之前作为玩家最近的地理ip是哪里,你都要在当前地域让用户迅速登录服务器,而别很拗的一定要把用户定向回最近ip所在区域才让他登录,而要时时处处迅速响应登录,然后再根据用户自己的选择或者是受邀或邀请的实际情况最后定位到实际建立房间所在地,再让用户连接到房间服务器的所在地,其实也就是常说的负载均衡(Load Balance)。如果是单机,针对用户量小的游戏,其实要做的事情服务器性能的优化,就是怎么能把单服务器的性能给他榨干?常用的手段,比如你可以利用多进程来让带宽、io、cup性能发挥到最大。比如你可以使用基于netty、Vert.x开发的服务框架,这样单机可以提高到500的并发量。这里还要再啰嗦两句,其实单纯的使用Netty,它帮你解决的问题只是如何使用有限的线程处理更多的连接,也就是解决的同吞吐的网络IO问题,而不是编程的问题,你连接建立起来以后,后面业务逻辑处理的快慢要看你业务逻辑写的好坏,并不能因为你用Netty迅速建立了连接你后面业务处理的能力就自然变快了。回忆一下你平时使用的Tomcat servlet,它的网络I/O也是基于NIO实现的,它跟Netty差也就差在一些规范使它更“笨重”一些,后面调用xxStream的时候,用servlet返回响应时会先block当前线程而Netty就是只判断一下是不是处在指定NIO线程中,在就马上执行,快也就快在这里了。而Vert.x就不一样了,它鼓励你用JDBC调用、Redis查询、文件I/O等所有可能导致线程阻塞的操作都换成全异步化的方式以进一步榨取CPU的利用率。同时还提供异步协调工具(CompositeFeature)和一条龙“解决方案”(如Event Bus以及为当下正火微服务概念准备的注册中心Vert.x Service Discover及断路器Vert.x Circuit Breaker等),总之Vert.x既为你解决了迅速建立连接或者少线程处理多连接的基础问题,并为你后续业务逻辑处理问题提供了完整封装的方案,让你各环节都可以利用它来“加速”你的业务,所以更建议用Vert.x在你的业务中而不单纯的使用Netty。如果你使用编译型语言,如 C、C++、GO等,所说单u可以提高到3000qps/核处理量,还有一种是使用C+AIO听说可能做到单机50万,顺便吐槽一下现在人人言高并发,像什么redis单机十万,nginx单机六万,我觉得他们说法更可能是在万兆网卡,超小数据包,高速ssd,无网络碰撞,无io争抢的条件下产获得的测试结果吧?据实际使用经验,nginx最多3万,redis超过1万就会出问题,所以他们的宣传加了多少水份进来?
要做严格的地域隔离,严格的用户隔离,可以用redis中的incr()方法生成全局唯一id,并让他可以原子性的递增,如果怕别人用id猜出用户数可以用随机选取递增的梯度来完成随机。服务器的多地部署前面已经说了,多地部署的同时还要有nginx的vip机,就是只负责处理对请求定向到相应业务逻辑服务器,而服务器本身不做业务逻辑相关的事,部署的时候vip的nginx要有个master的机器,还要有个slave的机器,然后要用keepalive模块来检测master的状态,如果出现超时响应马上把slave切成master,然后如果master节点恢复工作,再重新根据遴选规则确认优先级作为新的master节点,另外一个自动成为slave节点。nginx集群的后边就是缓存,建议使用redis作缓存,memcache常见会出现的几种问题是雪崩现象,由于缓存节点失效,缓存命中率出现问题,导致数据直接去数据库查询,造成短时间内数据查询请求激增,最终导致数据服务器崩溃,现象常见于memcache周期性失效或者memcache没拦截住去数据库的流量导致数据库被直接打死,反复几次,db打死后memcache中恢复一点,再打死反反复复最终导致所有缓存恢复,解决方法就是设置缓存失效时间为一个随机值,比如是3~9小时之内的某个随机值,再有就是设置memcache的闲时失效,比如说用户使用量可能不大的夜里。缓存无底洞现象,2010年facebook公布了memcache这一现象,就是没有随着往memcache集群里加节点的增多而提高整个缓存系统的处理能力,原因就是facebook当时存储数据的时候,只是把数据随机存到相应服务器上,但却没有规范存储的结构,导致查询一个用户的信息可能要跨多台缓存服务器,跨服务器访问速度受影响因素就比较复杂了,所以单纯的往memcache集群里加机器并没有很好的解决问题,解决方案就是借鉴于结构化数据库mysql的存储方式,数据标识用共同前缀,在共同前缀后面挂更详细的信息,使相同前缀数据只出现在一个节点上,访问时只需连接一个节点,而不需要连接多个节点。永久数据被踢现象,导致原因是memcache的惰性删除机制,即memcache中存储数据在内存中失效后并不会立马被删除,只有在下次get的时候,系统才会将其删除,这会导致memcache的空间被一些未被请求但已失效的数据占满。LRU算法的淘汰机制,永久数据很少使用会把内存占满的情况,此时进来新数据被缓存后,永久数据可能会被删除,解决方案就是永久数据和非永久数据分开。同时也不建议使用mongodb作为缓存,在内存中的时候性能很高,但是会自动把一些内存数据剧到磁盘,这样会影响性能,总之更建议用redis做缓存。事先做好根据网站数据增长及与之匹配的数据库分库分表方案,这里如果用mongodb部署服务器集群比较简单,我司现在做持久化存储使用的就是mongodb,但是要根据你业务的具体需求。同时要把业务服务器与用户登录验签的服务器分开,避免出现业务逻辑出现问题或报bug而影响到用户的登录。业务服务器上还要部署消息队列,比如说我们做棋牌游戏的全服公告或者是给相应用户发通知等,所依据的理论就是统一由网关发送给本网关中的所有用户。对于有管理后台(我们是php)发送过来的用户通知,如玩家充值成功提示,自动广播等,可以直接发送至网关提供的HTTP服务接口,再由网管统一发给网管中的所有用户。对于游戏内的通知,如赢取金币数额提示,好友上线提示等,可以通过游戏中的通知模块处理之后,发送给网管,再由网管发送给各客户端。这个选择也比较多,这里我们看到非常多一个词就是网关,在棋牌游戏开发中网关是什么呢?
传统意义上网关(gateway)又被称为网间连接器、协议转换器。网关在网络层以上实现网络互连,是最复杂的网络互联设备,仅用于两个高层协议不同的网络连接。在广域网和局域网中都有应用,它在计算机或设备上充当着重要的转换任务,在不同的通信协议、数据格式或语言,甚至完全不同的两个操作系统上,它都在以一个翻译器的角色发挥着重要作用,网桥如果懂信息通信的人都知道它只简单的传达信息,而网关要把收到的信息重新打包,以适应目标系统对信息格式的需求。而通俗点理解网关其实就是就是“信息汇总转发”的集散中心,而tcp中包括的无论http还是udp,不过是网络请求的方式,网关管理你的请求,是不是觉得很熟悉的东西出现了呢?没错的,其实就是apache,nginx这些代理服务器。知道网关实际上的意义,那后面的问题就简单多了。网关的通信使用的tcp协议,也有brotobuf。tcp包结构体中有Len这个参数,意为包长度,有Extra这个参数,包含有目标服务器的编号,这两个是下面讲解中用到的参数,其他可以自行百度"TCP包结构体"。回到我们游戏里,我们看前文中的图,每当有服务上线会向动向ETCD进行注册,注册成功之后,ETCD会将此新注册成功的信息发送给网关,网关记录其号码。当客户端在游戏时,会频繁的发送信息,前面说了Extra包含有目标服务器的编号,网关只要对着内存进行寻找及转发,而不用再去缓存中取数据,从而降低了延迟,提高了性能,所以这就引出了如果要做高并发要遵循的另一个准则,就是尽量减小或者合并请求,从而能使单位带宽单位时间内通过处理的信息更大,所以,能在客户端完成的,不要不加过滤的直接送到服务器端,能在内存中完成的,不要硬是要去与磁盘进行数据交换,徒劳的增加请求响应时间,可以开个异步进程嘛,数据推到队列里,让新启的进程用在其他时间或者用闲置的资源去消化队列。从逻辑服务器上部署的各种模块功能可以看出,整个高并发需求的产生是伴随着业务的壮大与业务的不断拆分,如果体系涉及的东西多而杂,那么,整个体系要维护起来就特别麻烦,而且可能产生出各种故障又不好定位出问题。比如说我们棋牌游戏最开始的时候,只有单个游戏大厅里面就一两款游戏,但是随着用户体量的不断增大,随着新业务线的不断开辟,新上线的游戏特别多,游戏多了,就涉及到各个游戏都可能出现相同的需求,如果按照老方法不断在一个文件或者一个项目里改来改去,让它来兼容所有的项目,一是越来越难维护,二是效率特别低下,所以才有了微服务的概念出现。微服务的每一项都是从原来大项目中抽出来的点,通过扩展和完善使其更具鲁棒性和可复用性,单独提出来以后,也方便单一变量原则,更好的寻找优化的切入点,从而提高性能。所以高并发请求进来以后,由vip服务器定向到各业务服务器以后,交给各服务器已部署的微服务+task+消息队列来完成业务的处理,现在人工智能再火,你还可以再后边再绑个hfs或者htfs来满足你大数据运算的需求。
这里要再强调一遍分库分表提前规划的重要性,这对高并发状态下的及时有效响应至关重要。伴随着参与游戏的场次及玩法越来越多样,可能之前你只规划了一个表针对用户维护的不同游戏记录用户的一次又一次的成绩,但是时间一久,这个数据库会非常的庞大,即使mongodb加了索引,如果数量量增长量过快,比如说我们一天跑胡子游戏会增加300~500万的游戏数据,月增长大概1.9亿数据,这种规模之下,mongodb的插入变得极其缓慢,主从部署的mongodb集群主节偶尔cpu load负载过高,从节点尤其明显,所以就需要提前做好冷热数据的拆分,及数据的分表。
再谈一些之前工作中与高并发高可用相关的一些细节的点,可能你整个架构体系做的特别好,能够把整个服务器集群的性能发挥到最大,但是这是建立在你做推演时任何细节点都没有问题,网络畅通性良好,ssd广泛使用导致数据I/O十分流畅,代码无bug不会凭添消耗等前提之下的,但是实战过程中还是避免不了“不完美状况”的出现。
我之前在做大规模分布式爬虫的时候,由于所在行业对信息的精度要求比较高,可能针对不同地区部署的爬虫在一次爬取数据不完整(通常通过网络状态或计数统计)的情况下要进行反复爬取,当时就发现一个现象,在分析某网站一个动态加载的数据时候,偶然发现竟然DNS解析也会占用甚至很长时间,当时因为已经从phpspider换到了scrapy,所以就想应该可以用scrapy自有框架中的配置参数来解决这个问题吧,果断查看官方文档,当时scrapy的官方文档中也没有太多说明(后来具体写代码才发现,其实提供了和scrapy cluster crawler一样的配置参数,只是也没处理ttl而已)。倒是在scrapy cluster crawler中找到了说法,他们是建议用DNSCACHE_ENABLE=False,因为如果你抓取的不是固定域名下的内容,那么,如果你开启了DNSCACHE的话,可能会导致如果目标站使用了动态域名解析你可能会抓不到想要的页面,导致访问报错等,所以它建议是用scrapy自己内存中对dns进行缓存的方式来管理域名解析上时间的损耗,但遗憾是他提供的v1.0的版本没有ttl(可存活时间检查)处理,而且我们当时分布式是结合的scrapy-redis来做的,并且不同域名的数量很大,这也和他提供这个参数要解决的问题出入比较大,综合这点,及我们项目已经用scrapy+scrapy redis写完了,只是再做一些优化,此时再换框架虽未计算成本,但是比目前状态一定高出不少,所以没有更换框架,当时采用的方式就是尝试自己通过中间件的方式给scrapy加上dns缓存的功能,当时准备写中间件前已经考虑到了可能面临的问题:一是中间件写好之后部署的优先级应该靠近下载器,这样它能够第一时间执行,同时在用完之后能够第一时间销毁,同时决定采用redis作为dns缓存数据库,因为属于内存数据库,同机房部署调用基本0秒级别,另一问题要调整之前对scrapy的修改,之前开启的并发线程太多,已经暴露过DNS超时的问题,只是当时没有在意,现在想想,你启的线程太多,同一时间大量请求涌入,但是dns解析线程的数量却是有限的,就导致很多解析请求排队,最终导致超时结果的产生。还有一个问题是全不全量缓存dns解析结果的问题,如果全量的话,单机内存总有限,我们又抓的都是不同域名的网站,所以如果都进行缓存,当时的条件不容易搞,当时就想到下载器的数量基本都是小于并发请求量的,所以只针对下载器中的域名处理不就解决了?在进入下载器之前把dns缓存起来,离开下载器的时候就把缓存干掉。还有就是对于请求发生301重定向,302临时重定向的问题,要禁止掉重定向,如果请求域名和响应的域名不同,那就要把meta中传的ip值给删掉,所以还会涉及到重写scrapy的RedirectMiddlewar,围绕这些问题最终写出了代码,成功解决了当时耗费在dns缓存上的问题。在之前公司做海外项目中,新加坡,马来西亚和澳大利亚都有购入aws服务器,进行wifi连接服务功能部署的时候又遇到了这些坑。国内访问速度太慢。另外问题是马来西亚和澳大利亚都有机房,当时用户wifi的连接数据会写到aws的bucket里面,然后当时由于马来西马线上部署portal页系统,后台wifi管理系统,管理员系统,财务报表等一套系统下来特别麻烦,同时由于刚进入市场有些功能还用不到,那个时候,马亚西亚部署有管理员系统,可以对aws的bucket数据读取,生成服务报表,给管理人员和运营人员,而澳大利亚当时没有部署,所以最开始只在它里面加了一个接口,供马亚西亚这边操作读取数据,由于两地间隔较远,马亚西亚调用接口延迟特别高(延时几百ms甚至半分钟不等),又因为我们的商用路由都有人工智能的人脸检测拍照等功能,收集数据方便进一步对人群进行消费分析,所以aws桶内一天的数据累加下来都特别大,两地距离,网络传输环境复杂,干扰因素多且不可控,dns解析劫持及tcp拥塞策略的限制等等原因导致payload的文件越大传输越慢。针对这个问题,后来针对性的采用了对传输数据进行减小传输量的操作,应用了一些传统的作法,如数据压缩图片压缩及选取更高效的压缩算法如protocal buffer算法。距离太远是所有问题产生的根本原因,针对此问题,我的经验是,在国内购买国内的加速服务,国外呢,由于cloudflare在全球有很多dns的加速节点,所以我们会购买cloudflare的dns加速服务,因为即使我们在各国当地都有服务器,但是我们当时只是试水当地市场,所以当地服务器数量并不多,不能达到很好覆盖当地的目的,而cloudflare全球有很多cdn的加速访问点,可以满足我们快速分发请求到达对应服务器的需求,完成这步目的就是对cdn进行优化。而像facebook等设备在安卓端想要拉起,几乎所有机型都要适配,当时由于人数不够没有做,但是当时调研了很久,因为美团也在拓展海外市场,我觉得他们的方法可能对海外市场用户体验的优化有一些好的建议值得推荐。
好了,对我上面讲的这些做个总结:
- 一定要把无论是四层、五层还是七层协议,一定要把应用层、DNS解析层或者是nginx/Tengine+varnish服务层给他隔离好,基础服务和业务逻辑拆分好,不要让业务逻辑影响基础服务的运行。如果用nginx做负载均衡(也可以用LVS+Keepalive),那只让vip服务器中转流量,然后让业务服务器上业务微服务化,每个细化出的服务只干一件事,把系统的耦合性降到最近,同时可以复用的部分要尽可能抽象拆分出来,然后用控制变量的原则来优化单一点来取得优化方案上的突破。还有最大好处,微服务独立部署互不依赖所以它是不限语言的,好的微服务之间都是用很轻量级的通信机制进行的,所以,性能要求高的场景下,尽量使用c,c++,go等编译型语言完成微服务模块的开发。
- 好好利用redis做缓存,不推荐memcache,也不推荐mongodb。部署主从简单,哨兵功能完善,针对海量数据可以横向扩容,单机都可以支持10万(官方说法)qps,横向扩容这个数字可以被不断放大再放大,用好这一条,已经可以解决绝大多数高并发问题了。
- 一定要尽量把请求拦截在送达数据库之前。前面已经讲过了,过滤掉无效的请求,可能你所谓的高并发已经不再是高并发了,可能单机就帮你解决了。
- 熔断降级,容灾不能少。断路器(前面有提过的Vert.x)这应该是个电路学的概念,现在被用在it行业内了。断路器这个功能如果用的好可以避免很多“运维事故”的发生,最典型的就是它会跟踪故障出现次数,假如达到阀值时就断开服务,执行回退或者是消息通知等,之前有一个叫做hystrix,不过现在已经停止维护了,现在官方推荐用的是Resilience4j,这个我没用过,不好发表见解,推荐用Vert.x,java里用做熔断降级文档全面、性能高。容灾目的就是出现意外可以不致业务中断或中断后可以自动恢复。
- 要有完善的日志系统。单从高并发业务方面的好处来说就是方便后期追踪性能问题产生的本质原因,从而有针对性的采取措施解决或优化问题。要让分散在不同机器上的日志方便检索,可以让日志系统订阅消息中间件kafka或者是rocketmq或者是zeromq的消息推送服务,然后放到elasticsearch中方便日志检索,同时为了日志系统不影响主业务性能,建议业务系统内部引用线程池来专门异步处理日志,为了不与主业务争抢资源,建议建成阻塞队列,线程池先压缩日志然后丢到mq中,mq的订阅消费者就可以消费这些日志了,如果日志量特别大还是建议用kafka。
- 对应自己业务需求选型mysql还是mongodb做数据持久化。不要现在你也芒果我也芒果,你就要上芒果,上芒果很吃机器的,你体量没到多大,可能在阿里腾讯云就充进不少钱了,所以明白自己所处位置很重要。
- 异地多活如果最终统一到一个数据中台里面,那么及时做好数据的分为分表及数据冲突解决的方案。
- 要留意技术细节对整个系统运行性能上的影响,有个理论叫木桶效应,你的能力上限不在于你究竟有多厉害,而在于你的“短板”是哪块。