4

一次抢口罩的负载优化

 3 years ago
source link: https://www.raye.wang/yi-ci-qiang-kou-zhao-de-fu-zai-you-hua/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

一次抢口罩的负载优化

23 三月 2020 4:26:35 下午

由于这次令人讨厌的疫情,导致口罩一罩难求,虽然各大厂商都在努力生产,但是毕竟僧多粥少,也幸好疫情快要结束了,也希望早点结束。因为口罩实在难以购买,所以很多地方的口罩采用统一调配,但是如何把口罩发放到大众手里,也成为了一个问题,所以预约购买成了最为安全的购买方式,购买后可以邮寄到家或者根据选择的时间段到店自提,但是由于购买人数过多,也带来了一个新的问题,服务器并发的问题,至少我在二月份参与了很多预约购买口罩的平台,没有一个平台不崩溃的,大多都是10几分钟了,就没有正常请求过服务器一次,不过幸好现在大部分平台已经调整为抽签的形式,而不是之前的先到先得规则,这样相对于先到先得更加公平,不受限于网络和运气,因为我们公司也做了相应的预约平台,不过由于客户要求先到先得原则,所以了本文。不过由于使用我们平台的用户并没有一线城市那么夸张,动不动几百万人同时抢购,而且我们平台的服务器也没有那么好的配置,所以本文的方式只是在几万人同时抢购的情况下测试过,当然其中思路还是可以参考的一下的,毕竟没有万金油,负载优化肯定是要根据实际情况来处理的

首先第一个要求是不能超发,第二个要求是尽可能提交成功的失败率不会太高,也就是说不要100人都提交成功了,但是最后只有一个人抢到了口罩,第三就是要求服务器不要崩溃,尽可能的不要出现请求服务器失败的情况。

首先一个要求解决方案:因为考虑到大并发的情况的,而且还做了负载均衡,所以要么采用分布式锁或者队列,

分布式锁的优点是可以实时知道抢购结果,坏处就是性能低下,处理不好容易造成服务器压力过大。

分布式锁可以采用zookeeper,当然还有别的分布式中间件或者框架,千万别直接用语言自带的锁,做了负载均衡后,这个锁是不适用的,当然如果采用分布式锁的方式,一定要注意处理时间和预判,也就是说比如库存10个,那么理论上来说你有10请求到了,后面请求就应该直接说没有库存了,不然又有锁,又不预判,最终结果就是服务器被拖死,所有请求都需要到锁里面去执行逻辑。

队列的好处就是你把请求扔进队列里面,就可以给客户反馈了,最终处理交给队列的消费者来处理就好,坏处就是不能实时知道处理结果,另外消费者也只能配置一个,注意查看一下所用的消息队列的文档,如果队列消费者是有支持多线程消费的,也要加分布式锁,如果需要配置多个消费者一样,需要在消费者中使用分布式锁,也可以直接用数据库乐观锁,不过大并发情况下到底是分布式锁效率高还是乐观锁效率高,还是要根据时间情况来分析,当然仅仅是分析是没用的,还要进行测试。当然用队列的方式也最好加一个库存预判,不然会出现很多人提交成功,但是却只有少数人能抢购到,如果有预判,首先可以降低消息数量,降低消费者运行时间,第二也不用让人一直期待

第二个要求解决方案就是加库存预判,我们在redis里面也保存了一分库存数据,每次有人提交先判断redis库存,如果有就马上扣减然后扔队列,当然因为redis的锁一直有问题,所以预判的库存肯定不会准确,但是影响不大,因为最终成功是在消息队列里面处理,不会出现超发

第三个就是简单来说就是尽可能的增加服务器并发量,当然前面2个问题的处理方式已经一定程度上增加了并发量,但是却远远不够,所以为了增加并发量,还要继续优化代码和做负载均衡

首先前端因为是网页的(小程序更新版本审核太麻烦,不符合我们快速迭代要求)

首先可以使用cdn,不然所有请求都到我们服务器,宽带能否承受变成了一个问题,毕竟一个网页请求的数据量跟普通接口数据量差别还是挺大的

第二就是代码压缩,压缩之后能减少传输的数据量,虽然看起来可能只少了几十K,但是把这几十K放大几万倍也是不小的网络开销了(压缩包括图片,字体等各种前端资源)

第三就是不常更新的数据做本地缓存,比如用户信息等,这样就不用每次都请求接口了,不过要注意缓存失效,如果是放在localStorage里面一定要主动做失效控制,否则数据会导致数据一直不更新

由于不是专业前端,所以就只能想到这几个优化办法(因为处理这块的时候还没有开始上班,而且前端页面不复杂,所以就由我开发了)

首先数据能放缓存就尽量放缓存,避免经常查询数据库,但是也要注意缓存失效的问题,如果数据库有更新,一定要更新缓存,另外还要注意缓存击穿的问题,当然有些缓存数据不用一定准确,比如之前提到的抢购的时候的库存预判,为了性能只能让缓存跟数据库不同步,只要最终处理结果正确就行

这个优化程度倒是不是特别多,当然如果懂就更好了,写出更符合jvm优化的代码,不过一般代码规划好的工程师写出来的代码也差不多符合这些要求,不过也要检查是否有可以优化的地方

有时候一个页面需要请求多个接口获取数据,可以考虑同一个接口返回多个接口返回的数据,这样虽然单次请求时间会延长,但是确能降低请求量,也不失为一种增加并发的方法

主要是数据库、消息队列、缓存等连接池线程数量的优化,比如你配置的线程池最大只有10个线程,那么不管怎么样,你同一时间段都只能处理10个人的请求(这里的请求只是针对需要用到这些线程的),当然这些配置也不是越大越好,首先考虑一个切换成本,第二就是有了负载后,数据库这些本身的最大连接数,比如你数据库本身最大连接数是100,你给程序数据库连接池配置了1000,那么也没用,因为数据库只能接收你100个连接,最终也只有100个连接才能正常使用

中间件优化
  • Tomcat优化

优化jvm参数(堆栈初始化大小等),优化了tomcat的线程池配置

  • 数据库优化

启用MySQL缓存(如果是读明显大于写很多的情况,可以用这个,可以加快查询数量,如果是写跟读差不多,那也要考虑开启后带来的负面影响,增加了写入成本),增加最多连接数量等

由于中间件采用的各不相同,所以只能根据实际情况和硬件配置等来具体优化,切记不能千篇一律的照抄,毕竟别人的配置不一定就适合你的服务器,比如别人机器64核,所以给nginx开64个工作线程,但是你的机器只有16核,你也去开64个线程,这样不会更快,反而更慢

Nginx

由于Nginx是主要的负载软件,所以讲一下Nginx的相关配置,本文只设计到配置理论,不会出具体配置,还是那句话,具体情况下具体配置,没有万金油

  • Nginx负载策略

Nginx 常用的负载策略有

  • 轮询(默认方式)
  • weight(权重)
  • ip_hash(根据IP分配)
  • least_conn(最少连接方式)
  • fair(响应时间方式 第三方)
  • url_hash(根据URL分配 第三方)

由于服务器配置参差不齐,所以轮询、 ip-hash 、 least_conn这几种策略明显不使用,毕竟这样会造成配置高的服务器接收请求少,而配置差的服务器反而接收了更多请求。urlhash也不符合我们使用场景,主要因为这样会造成不同的服务器处理不同的接口,无法有效把压力分散,毕竟所有压力都集中在1,2个接口上,所以不适合

最终适合我们的负载策略只有weight和fair,理论上fair是更适合的,因为响应时间才能反应一台服务器真正的压力,如果响应快,那么理所应当接收更多负载,毕竟很多时候一台服务器不只是一个服务,但是因为fair是第三方的,首先有效性还有待考证,其次就是这样的话nginx需要重新编译,而我们nginx已经编译了太多模块,上次编译的命令无法找回,导致担心重新编译会有问题(所以以后这些需要自行编译的软件,一定要记录编译命令,否则万一哪天需要更改,麻烦就来了),所以最终选择了weight,然后根据服务器的配置以及所运行的其他服务来设置了权重

  • Nginx其他配置优化

工作线程数量优化、workerconnections和workerrlimit_nofile 优化

Nginx只是软件负载,但是一台服务器的再强,并发也是有限的,所以就需要用到多层负载。如果是真机,那么可以使用硬件负载,如F5、Array等硬件(由于不是专业运营,没有接触过这些高端玩意,也就知道这2个),如果是阿里云的服务器,也可以购买阿里云的负载均衡服务(其他云我不知道有没有,因为没用过),当然这2种办法都是需要增加成本的,所以还有一种免费的方法,就是DNS负载,配置简单,还不受限服务器配置,唯独一点不好的就是DNS负载不会监控服务健康状态,简单来说,假如你这台服务器挂了,那么请求该来的还是会来。多层负载用了之后的结果就是,可以有效的把请求分发到多台nginx服务器,可以避免单台nginx卡死,导致所有服务不可用

出现过的问题

  • Nginx单线程能处理的最大并发数问题

由于当时时间紧急,所以没有截图,原因是因为workerconnections设置的太小,之前同事直接网上复制了一份配置,上面设置的是2048,由于之前系统没有很大的并发,所以也没有怎么关注这个配置,直到出了问题百度才知道,这个是单个工作线程能处理的最大并发量,比如我设置了10个工作线程,workerconnections设置的2048,那么我nginx最大并发也就是20480,超过这个并发nginx就会抛异常提示,另外worker_connections不能超过worker_rlimit_nofile,具体可以百度相关关键词来配置

  • Nginx主机CPU 100%导致全部卡住

这个问题一开始没有出现,后面抢购直接由县城升级到开放全市抢购的时候,问题来了,主Nginx的服务器CPU 100%一直不降,所有服务器都没有请求进来,说明nginx已经没有进行请求分发了,htop命令查询服务器状态,发现Nginx工作线程占用CPU都很高,还有其他一个服务,这2个程序已经吧CPU全部占用了,但是尽管Nginx占用了CPU,但是却没有执行分发,最后只能kill掉nginx,重启(restart命令已经没用了),原因是服务器配置明显不够(也有可能是nginx本身的版本的问题,因为版本比较老,理论上来说那么点并发是没有多大问题的),再加上另外一个服务也很占用资源,所以第一步是把另外一个服务迁移到其他服务器上,第二就是启用了DNS负载,当时也看了阿里云的负载服务,不过DNS负载能满足我们的需求,虽然没有阿里云的负载那么好用和智能,但是好在DNS负载是免费的[笑着哭],而且配置也简单,这样从单台nginx变成多台,nginx就再也没有卡死了(虽然是因为并发也不够大,如果几千万的并发,我都不用想就知道,瞬间嘣了)

虽然没有很夸张的动辄几千万,几个亿的负载优化,但是我想所有开发都是一步一步来的,没有谁没有经历过几十万的负载,就去坐几百万的负载优化,纸上谈兵谁都会,但是实验才是检验真理的唯一准则,也希望此文能给阅读的你带来一点帮助


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK