刘泉皓

没有最强的法术,只有最强的法师。

突发百万级流量应对总结

10 May 2020 » memory

二月份的时候,当时的公司网站流量突然从平时的日IP10万爆发到日IP200万,所以写了这篇博客,说明当时的情况,以及以后设计高并发系统的时候应该怎么做。以下问题我也是初次处理,有不对的地方请指正,学习学习。

一、网站情况说明

众所周知,2020年的新年,也就是一月底,新冠状病毒在中国爆发,湖北封省,我在湖北深刻体会到了生存的艰难。各行各业停工,但是互联网却十分坚挺,特别是我当时的公司,一个中小学在线教育的网站。网站H心功能有用户登陆,个人中心管理,点播课,直播,一对一在线作业辅导。

从技术角度说明,刚到公司的时候是2016年,当时网站还是html和php混写的网站,还有php4的痕迹。。只有个人登陆和点播课,然后去了就开始和几个同事做改版,由于总监禁止使用任何框架(看不懂),所以我做了MVC分层,然后兼容老配置,开发了公共的DB类,对数据库做读写分离。

通过腾讯云平台的监控,平时白天也就几百到几千QPS,晚上6点到10点会高一点。周六早上8点都有电视台推广活动,突发的QPS大概几万。基于LNMP,其中linux为centos7,没有bbr,php为5.6。全部为腾讯云服务,所用的服务有CDN、点播服务、直播服务、负载均衡,2台4H16G的服务器做memcache服务器,4台4H16G云服务器,入网带宽为1台30Mbps,3台20Mbps,5个4H16Gmysql数据库,为1台写库,4台读库。

因为网站大部分流量都是静态页面,所以CDN吃掉了大部分流量,做数据库读的时候,能用memcache缓存的地方我们开发团队都会缓存,所以到mysql的流量基本最多就几百QPS,即使是周六早上高峰期,数据库也就3k-5k的QPS。(没有TPS,历史原因没有使用事务,因为以前一直都是MyISAM引擎,18年从机房的服务器转移到腾讯云才切到InnoDB)。

二、突发百万流量后的解决流程

下面是当时的情况。

  1. 当时是二月初,大家都在家,有的还在休假,突然群里说网站崩溃了,发现是网站压力太大,QPS接近几十万。我们才听到总监和老板说和四川教育局在合作,四川的学生全部使用我们的网站在线学习,总监正在处理网站崩溃问题。(评价:这件事太突然,网站突然有大流量,老板应该提前通知,技术部可以提前做好压力测试。不过实际上即使知道了也没用。因为在9月份,我在做百度小度机器人的时候,百度那边的技术就和我说过双十一活动可能有大流量到我们服务器,我曾经计算并告知总监我们的服务器扛不住预估的压力,但是被置之不理,得到的回复是:“先用着,不够可以加宽带加机器”。毕竟需要花钱,不过如果是我自己的站我这钱我肯定出,做好压力测试,哪怕白花了)

  2. 然后总监做了第一件事:花了几十万,买了32台8H16G和16H32G的服务器,还有16台8H16G的数据库。(评价:其实在公司几年,几个后端的同事一直私下吐槽,总监买硬件总是性能过剩,平时CPU、内存基本使用率不到10%。。其实也不能说是坏事,一是估计老板有钱,一年网站毛利润5千万到1亿,不差这点服务器的钱;二是可以预防突然的小型高峰流量,例如周六活动,或者被CC攻击)

  3. 然而依然一到早上9点多高峰期就崩溃,总监的调查原因是数据库压力过大,所以让技术团队做了第二件事,砍掉所有有数据库写功能的接口,所有资源主要用于点播课。并且要求所有读sql能缓存的全部缓存。(评价:当时老板群里火气很大,因为被四川教育局领导们一个电话一个电话的批评,这火气自然主要落到了总监身上。当时的情况紧急,使用这种应对方案是很合理的,这也是我当时可以想出来的方案)

  4. 开始优化数据库,增加读库数量,写库拆分业务。(评价:实际上一般情况下,网站所有组件都能横向扩展,唯独写库不行,然而水桶效应导致网站流量被限制在写库上。解决办法有两个,一是数据库拆分,就是把压力大的表分到单独的服务器,这样就可以把写库的各个表压力拆散;二是拆分表,分为hash分表和底层磁盘分块。hash分表就是把表分为user_1,user_2这种表名,我的直播课设计的表就是预先这么分表的。还有就是底层磁盘分块,就是依赖LVM这类虚拟磁盘系统,或者raid0之类的技术,将一个表的底层拆到多个磁盘,这样数据库的写压力就拆到多个磁盘,底层可以并发写入了。一是用于目前这种亡羊补牢可行的方案,二需要提前设计,此时用二需要重新迁移表,比较麻烦)

  5. 读写库添加修改好后,网站依然502,根据我的调查,发现是nginx能吃下大量的流量,但是到了php-fpm,却因为没有进程可以处理导致nginx返回给用户502。于是增加了php-fpm进程数,以及每个进程可以处理的请求数。(评价:修改pm.max_children和max_requests)

  6. 此时网站崩溃问题基本得到了解决,但是依然会出现间歇性访问失败。经过我调查,发现php的日志里显示无法连接数据库,于是添加了nofile和mysql的max_connectIOns参数。(评价:linux的nofile最好设置较大的数,1024000之类,因为很多服务都需要使用掉它)

  7. 网站从用户的角度上基本正常了,但是作为技术人员,监控各个服务,发现有的接口访问太慢,mysql的cpu资源消耗过高问题。于是我开始修改公共的DB类,记录sql查询日志统计分析,发现两个问题。一是有的接口访问量很大,分为被CC攻击和前端频繁调用接口;CC攻击直接用腾讯CDN屏蔽掉ip,前端也做了一些优化;二是有些sql请求特别慢,原因是索引没有建好,或者没有建索引,所以让相关开发添加上即可。然后还有php释放mysql连接不及时问题,会导致mysql连接数居高不下,解决办法是php使用了mysql对象查询后立刻释放连接,或者使用proxysql之类的连接池。(评价:这是个需要长期维护的事情,平时开发也得注意)

经过以上的处理,网站总算正常了。

三、假如让我重新设计百万访问的网站,我该怎么做

首先分析业务,百万用户的维基百科、淘宝、QQ、斗鱼,需要侧重使用的资源是不一样的。如果是维基百科,主要是静态页面访问,CDN和nginx可以解决大量访问;而淘宝有大量的商品展示、添加购物车、订单、支付之类的操作,牵扯到数据库事务,数据库资源消耗较大;QQ的消息分发,一个人可能群发给很多人,需要大量的带宽;而斗鱼直播,有直播推流、录像存储、打赏、聊天等,对数据库、带宽、存储等资源消耗都很大。所以同样百万用户的系统,需要的资源侧重点是不一样的。

现在就以在普通的web网站为例。

首先目前只有100用户,但是考虑以后有百万用户,我不会一下子买支撑百万用户的硬件资源,来支撑100用户。所以一开始的主要设计目标是“可扩展”。可扩展的意思是说,当我增加硬件资源的时候,确实可以支撑对应增长的流量,而不会出现水桶效应下的短板。例如100块钱的硬件可以支撑100用户,我花1万块钱,就应该可以支撑1万用户,而不是出现某个服务卡住了流量,导致只能支撑200用户。一般情况下,100块钱支撑100用户,1万块钱无法支撑1万用户,而只能支撑7千,会有衰减。原因是100用户可以产生150个硬件资源操作,资源消耗/用户量为1.5倍,但用户达到1万的时候,他们可能有2万个硬件资源操作,资源消耗/用户量为2倍。因为用户量持续发展过程中,网站功能也会慢慢变得复杂,资源消耗会更多。即使网站功能没有变化,用户操作热度也会变得更高。一个很简单的例子,两个小姐姐在一起可以聊100句,三个小姐姐在一起却可以聊1000句。

为了达到可扩展,我需要先确定我的网站需要那些组件。CDN(可扩展),nginx服务器(可扩展),php服务器(可扩展),memcache服务器(可扩展),mysql服务器(读库可扩展,写库不可扩展,如果是mysql集群,写库可扩展),消息通讯服务器(可扩展),点播服务器(可扩展),直播服务器(可扩展)。所以最后发现,只有mysql写库不可扩展,得好好设计它。由于设置到同一个内网中,所以内部之间的带宽可以暂时不用考虑。主要是外网带宽、CPU和内存,以及可选的磁盘。

下面开始设计网站,假如百万用户就是一百万QPS(当然一般不可能,用户有活跃用户和不活跃用户,百万微博用户不会24小时一直一起发微博,同时发微博的估计也就几万,或者几千人,而且一天也就几条或者几天一条。流量也有波动,不同网站有自己的高峰期,在线教育的就是晚上5点到10点之间,白天小孩子上学基本没人访问,晚上回家做作业会用。)。其实技术角度我们一般讨论的是C10K和C10M问题。

  1. CDN的使用。网站所有能静态化的页面都静态化,尽量充分使用CDN,一是为了用户打开网站更快,二可以大量减轻网站后台压力。像点播课这类页面,基本完全可以把流量交给CDN处理。除非业务特殊,一般到CDN的流量/回源流量可能是100/1,或者更低。一百万QPS到后端就是一万QPS。

  2. nginx的使用。现在我们假设不使用CDN,一个页面100KB,一百万QPS也就是一秒100万个100KB页面请求,也就是说一秒钟100GB输出流量,所以带宽需要1000Gbps级别的。所以有时候有人说他们网站百万并发(QPS),你就知道他在吹牛逼。这也说明CDN的必要性。经常有文章写如何让nginx实现百万并发,根据其他人测试,8GB内存可以拿下一百万并发请求。实际操作需要调整linux内H参数,特别是tcp协议栈的内存,nofile,使用epoll等等细节,具体压力测试,需要需要从10k,50k,100k,500k,1m慢慢往上压力测试。但是实际业务肯定不会这么玩,高可用得保证上,而且一般php和nginx在同一台服务器上,所以用10台16H32G内存的服务器,为php准备一些资源。

  3. memcache的使用。现在我们假设不使用CDN,一百万QPS全部命中到memcache,所以我们把所有可以缓存的数据都缓存住。能缓存30秒的就30秒,能24小时就24小时,例如点播直播课列表,用户问题列表,个人信息等等。假如现在一百万QPS,全部命中到memcache,那么需要多少memcache服务器呢。其实并不需要很多,因为一百万用户访问一个播放列表页面,只需要缓存一个就行。但是他们的个人用户中心,每个人需要缓存自己的,也就需要缓存一百万个用户。但是实际情况并不是这样,因为正常情况下不会出现一百万用户同时访问他们的个人中心的情况。这也就是压力测试不真实的问题,压力测试会压测某个功能,实际用户使用不会同时只使用某个功能,其他功能的压力也可能影响到这个功能的使用。但压力测试可以很好说明这个功能最大能撑住多少访问,所以也不是一无是处。那么我们这么计算,我们的列表页有1000个,每个100KB表数据,所以公共内存占用100MB。个人数据每人100KB表数据,一百万也就100GB,但是memcache日常5万左右QPS,所以需要20台服务器,每台6GB内存,因为系统本身需要几百MB内存。但实际情况并不是这样,假如是一百万用户,而不是一百万QPS,日常十万QPS是上了天的。而且并不是每个用户都用到memcache,所以20GB内存,2台服务器即可。所以用2台8H16G内存服务器足矣。

  4. php的使用。php-fpm一般和nginx在同一服务器运行,使用127.0.0.1:9000或者php-fpm.sock通信。php的内存不好计算,因为会根据php文件代码量的多少,数据库数据的多少,网络请求数据等等而变化。总的来说,每个请求分配2MB不为过,默认php-fpm会配置一个页面最大内存为32MB。而且一百万的QPS,就算2MB也需要2TB内存,而且php-fpm请求是用进程一个一个处理请求的,每个请求需要创建超级变量,解析php脚本,执行,释放资源等等一堆操作,各种mysql等IO阻塞会限制住QPS,所以最消资源的就是它了。我们假设一个php-fpm占用2-3M,用16H32G的服务器,10G给nginx,2GB给系统,php-fpm用掉20G,进程数开600-1000左右,每个进程处理102400请求后重启。这样算下来,假如一个进程一秒处理10个请求,实际一百万QPS需要100台服务器。所以这个也说明了IO阻塞问题有多严重,假如是异步IO的程序,例如用了swoole,一秒可不止处理这么点,根据techempower测试报告,简单的带mysql访问,swoole可以达到30万QPS。一百万QPS也就3台服务器即可,但这是硬件跟的上的情况下。但实际情况下,有CDN和memcache的加持情况下,10台16H32G是没毛病的。

  5. mysql的使用。首先是读库,根据查询的复杂度不同,需要的配置也不同,一个请求1KB和1MB需要的内存肯定是不同的。如果还有事务,就更复杂了,会消耗大量内存和CPU,所以一百万数据库的QPS根据业务不同实际有很大不同。就以一个请求10KB来说,一百万QPS,就需要10GB内存。但是数据库不能只算内存,主要还是得算磁盘IO,腾讯云高可用mysql,磁盘IO可以在240000,假如跑满IO,一百万QPS需要5台读库。然而写库就更难了,因为无法通过添加服务器解决高并发问题,只能通过分库。可以把不同库放到不同服务器,压力大的库放到单独的服务器,压力大的表也可以存放在独立的服务器。但是现在还没这么多用户,根本没法知道哪些业务表访问量大,所以只能提前设计好,等到需要拆分业务的时候能够快速灵活的拆分出去。所以对于可能大的表,例如用户表、观看记录表等等一开始就hash分表。因为使用云服务,所以没法自己定制磁盘。但如果是自己的服务器搭建mysql服务,可以用多磁盘组raid0,或者raid10,或者选用更好的ssd提高磁盘IO。总的来说,使用云服务会更方便一些,只要做好业务上的分表,将来快速横向扩展会很方便。例如可以用一台备用的读库做数据迁移,将id为1-100的放到一台服务器,101-200的放到第二台服务器,根据当时压力的计算,划分到不同服务器即可。但实际情况,一百万QPS,能落到数据库身上的基本不到1万QPS,特别是有memcache的情况下,可能就两三千,所以数据库三读一写,配置为16H32G顶天了。

所以从整体来设计,一开始设计的系统,要让任何一个节点不要出现水桶效应限制住流量的情况。然后为了高可用,还需要配置好备用服务器,避免上游组件崩溃导致下游跟着崩溃。例如100个请求,memcache可以吃下50个请求,mysql最多能吃下70个请求。正常情况下,memcache吃下50,mysql吃下50,所以mysql还能有20个请求的预留空间。但这时memcache崩溃,导致120个请求全部落在mysql身上,这时mysql就只能跟着崩溃了。

总结

以上就是我这次学习到的经验了,可能有些不对,有些不够详细,特别是重新设计这部分,我没有实际操作测试,只能在未来的项目里看看有没有机会实际调试了。


知识共享许可协议    鄂ICP备 15002452号-5    鄂公网安备 42088102000048号