系统是如何一步一步支持高并发的-八零岁月
记录所见
分享所感

系统是如何一步一步支持高并发的

高并发系统各不相同,比如每秒百万并发的中间件系统、每日百亿请求的网关系统、瞬时每秒几十万请求的秒杀大促系统。 他们在应对高并发的时候,因为系统各自自身特点的不同,所以应对架构都是不一样的。 所以这里只是提供大部分系统支撑高并发的思路,供大家学习参考。

验证机制的演进

进入正题之前,先来介绍一下session,token。个人觉得这个是任何高并发系统都会涉及的。这里不分享他们的概念,只是给大家分享为什么高并发一定会涉及这两个。

  1. 最初Web应用的兴起,像在线购物网站,需要登录的网站等等,马上就面临一个问题,那就是要管理会话,必须记住哪些人登录系统, 哪些人往自己的购物车中放商品, 也就是说我必须把每个人区分开,这就是一个不小的挑战,因为HTTP请求是无状态的,所以想出的办法就是给大家发一个会话标识(session id), 说白了就是一个随机的字串,每个人收到的都不一样, 每次大家向我发起HTTP请求的时候,把这个字符串给一并捎过来, 这样我就能区分开谁是谁了。
  2. 这样问题解决了,可是服务器就遇到一个问题,每个人只需要保存自己的session id,而服务器要保存所有人的session id !如果访问服务器多了, 就得由成千上万,甚至几十万个。这对服务器说是一个巨大的开销 , 严重的限制了服务器扩展能力, 比如说我用两个机器组成了一个集群, 小F通过机器A登录了系统, 那session id会保存在机器A上, 假设小F的下一次请求被转发到机器B怎么办?机器B可没有小F的 session id啊。
  3. 当然你会跟我说把session id 集中存储到一个地方, 所有的机器都来访问这个地方的数据, 这样一来,就解决此问题了, 但是增加了单点失败的可能性, 要是那个负责session 的机器挂了, 所有人都得重新登录一遍。
  4. 最后你会不会想做一个集群,增加可靠性, 但不管如何, 这小小的session 对于我们来说都是一个沉重的负担。
  5. 那有没有更好的解决问题的思路,比如说, 小F已经登录了系统, 我给他发一个令牌(token), 里边包含了小F的 user id, 下一次小F 再次通过Http 请求访问我的时候, 把这个token 通过Http header 带过来不就可以了。不过这和session id没有本质区别啊, 任何人都可以可以伪造, 所以想点儿办法, 让别人伪造不了,比如对数据做一个签名吧, 用HMAC-SHA256 算法()(其他加密算法也可以,注意,一定要对称加密算法,否则像md5不可逆的,加了密自己都不知道是什么),加上一个只有我才知道的密钥, 对数据做一个签名, 把这个签名和数据一起作为token , 由于密钥别人不知道, 就无法伪造token了。
  1. 这个token 我不保存, 当小F把这个token 给我发过来的时候,我再用同样的HMAC-SHA256 算法和同样的密钥,对数据再计算一次签名, 和token 中的签名做个比较, 如果相同, 我就知道小F已经登录过了,并且可以直接取到小F的user id , 如果不相同, 数据部分肯定被人篡改过, 我就告诉发送者:对不起,没有认证。
  2. 到此也就解除了服务器存储session id这个负担, 我的集群现在可以轻松地做水平扩展, 用户访问量增大, 直接加机器就行。这种无状态的感觉是不是很好!
  3. token是否安全。Token 中的数据是明文保存的(当然你也可以用Base64做下编码) 如果一个人的token 被别人偷走了, 那也没办法, 只能认为小偷就是合法用户, 这其实和一个人的session id 被别人偷走是一样的,其实没有什么安全不安全的。唯一的解决方法生成的token包含过期参数,比如token有效期7天,7天之后服务器解密 token 的过期参数,如果超过了7天强制失效。

最后总结一下

  1. Seesion:每次认证用户发起请求时,服务器需要去创建一个记录来存储信息。当越来越多的用户发请求时,内存的开销也会不断增加。
  2. 无状态,可扩展性:在服务端的内存中使用Seesion存储登录信息,伴随而来的是可扩展性问题。
  3. CORS(跨域资源共享):当我们需要让数据跨多台移动设备上使用时,跨域资源的共享会是一个让人头疼的问题。在使用Ajax抓取另一个域的资源,就可以会出现禁止请求的情况。

好了,介绍了这么多,我们开始进入正题吧。

网站雏形,单点系统

假设网站在起步阶段,刚刚开始你的系统就部署在一台机器上,背后就连接了一台数据库,数据库部署在一台服务器上。我们甚至可以再现实点,给个例子,你的系统部署的机器是 4 核 8G,数据库服务器是 16 核 32G。

此时假设你的系统用户量总共就 10 万,用户量很少,日活用户按照不同系统的场景有区别,我们取一个较为客观的比例,10% 吧,每天活跃的用户就 1 万。

按照 28 法则,每天高峰期算他 4 个小时,高峰期活跃的用户占比达到 80%,就是 8000 人活跃在 4 小时内。然后每个人对你的系统发起的请求,我们算他每天是 20 次吧。那么高峰期 8000 人发起的请求也才 16 万次,平均到 4 小时内的每秒(14400 秒),每秒也就 10 次请求。

然后系统层面每秒是 10 次请求,对数据库的调用每次请求都会好几次数据库操作的,比如做做 crud 之类的。那么我们取一个一次请求对应 3 次数据库请求吧,那这样的话,数据库层每秒也就 30 次请求,对不对?按照这台数据库服务器的配置,支撑是绝对没问题的。

好吧!完全跟高并发搭不上边,对不对?不着急一步一步给你介绍。

上述描述的系统,用一张图表示,就是下面这样:

系统集群化部署

假设此时你的用户数开始快速增长,比如注册用户量增长了 50 倍,上升到了 500 万。此时日活用户是 50 万,高峰期对系统每秒请求是 500/s。然后对数据库的每秒请求数量是 1500/s,这个时候会怎么样呢?

按照上述的机器配置来说,如果你的系统内处理的是较为复杂的一些业务逻辑,是那种重业务逻辑的系统的话,是比较耗费 CPU 的,当前的服务器配置可能勉强支撑,但是碰到爆点的时候,就有点心有余而力不足了。

所以此时你需要做的一个事情,首先就是要支持你的系统集群化部署。你可以在前面挂一个负载均衡层,把请求均匀打到系统层面,让系统可以用多台机器集群化支撑更高的并发压力。比如说这里假设给系统增加部署一台机器,那么每台机器就只有 250/s 的请求了。

所以,简单小结,第一步要做的:

  1. 添加负载均衡层,将请求均匀打到系统层。
  2. 系统层采用集群化部署多台机器,扛住初步的并发压力。

此时的架构图变成下面的样子:

数据库分库分表 + 读写分离

假设此时用户量继续增长,达到了 1000 万注册用户,然后每天日活用户是 100 万。

那么此时对系统层面的请求量会达到每秒 1000/s,系统层面,你可以继续通过集群化的方式来扩容,反正前面的负载均衡层会均匀分散流量过去的。但是,这时数据库层面接受的请求量会达到 3000/s,这个就有点问题了。此时数据库层面的并发请求翻了一倍,你一定会发现线上的数据库负载越来越高。

因为数据库压力过大,首先一个问题就是高峰期系统性能可能会降低,因为数据库负载过高对性能会有影响。 另外一个,压力过大把你的数据库给搞挂了怎么办? 所以此时你必须得对系统做分库分表 + 读写分离,也就是把一个库拆分为多个库,部署在多个数据库服务上,这是作为主库承载写入请求的。 然后每个主库都挂载至少一个从库,由从库来承载读请求。

此时假设对数据库层面的读写并发是 3000/s,其中写并发占到了 1000/s,读并发占到了 2000/s。 那么一旦分库分表之后,采用两台数据库服务器上部署主库来支撑写请求,每台服务器承载的写并发就是 500/s。每台主库挂载一个服务器部署从库,那么 2 个从库每个从库支撑的读并发就是 1000/s。

简单总结,并发量继续增长时,我们就需要在数据库层面:分库分表、读写分离。

此时的架构图变成下面的样子:

缓存集群引入

接着就好办了,如果你的访问用户量越来越大,此时你可以不停的加机器,比如说系统层面不停加机器,就可以承载更高的并发请求。

但是这里有一个很大的问题:数据库其实本身不是用来承载高并发请求的,所以通常来说,数据库单机每秒承载的并发就在几千的数量级,而且数据库使用的机器都是比较高配置,比较昂贵的机器,成本很高。如果你就是简单的不停的加机器,其实是不对的。

还有一个问题就是没钱加服务器啊。

所以在高并发架构里通常都有缓存这个环节,缓存系统的设计就是为了承载高并发而生,你完全可以根据系统的业务特性,对那种写少读多的请求,引入缓存集群。

具体来说,就是在写数据库的时候同时写一份数据到缓存集群里,然后用缓存集群来承载大部分的读请求。这样的话,通过缓存集群,就可以用更少的机器资源承载更高的并发。

比如说上面那个图里,读请求目前是每秒 2000/s,两个从库各自抗了 1000/s 读请求,但是其中可能每秒 1800 次的读请求都是可以直接读缓存里的不怎么变化的数据的。

那么此时你一旦引入缓存集群,就可以抗下来这 1800/s 读请求,落到数据库层面的读请求就 200/s。

同样,给大家来一张架构图,一起来感受一下:

以上的选择都是加硬件来应对高并发,下面来介绍一下代码层面来应对高并发的业务。

基于redis消息队列

消息队列,简称它为 MQ(Message Queue) 。听其名字我们可以简单把它理解成把要传输的消息数据放在队列中。消息队列有两个很重要的概念一个生产者,一个消费者。把数据放到消息队列中叫做生产者,从消息队列里边取数据叫做消费者。

光说概念,可能比较空,下面来说一下具体应用场景。

  1. 系统业务解耦。 具体场景: 比如购买电影票,这个电影票是有库存量的。用户点击购买电影票,但是没有规定时间内支付,则需要将这个库存量还原,不能让用户一直霸占着这个票,常用的解决方式是使用crontab每个1分钟检测一下购买时间是否超过15分钟。但是使用 crontab 会耗费服务器和数据库资源,当然最主要的还是数据库,因为服务器每间隔 1 分钟都要去执行脚本。脚本的内容又是去全量扫描数据库。相信大家都知道网站很大一部分的瓶颈就是数据库。 这个时候,我们试想可以在用户下单之后订单 id 记录到 redis队列里,进而监听 redis key 过期事件,然后从 redis 过期事件拿到订单 id 的数据,再进行还原库存量业务操作就可以了。
  2. 异步处理业务逻辑。 具体场景:用户注册,服务器收到用户的注册请求后,接受参数,保存用户信息。但是往往还会伴随发送邮件或者生成不同尺寸的头像等耗时操作。服务器处理多久,那用户也要等多久。为了用户体验,用户注册完之后,耗时的操作完全没有必要让用户继续等待,我们可以把这些耗时的操作记录对应的消息队列,后台异步处理。把注册成功的消息先返回给用户。
  3. 流量削锋。 具体场景:商城抢购活动,比如前 3 名访问的用户可以拿到奖品,我们可以借助redis把前 3 名的访问用户 Id 记录到 redis 队列中,其他用户再来请求就直接返回结果,避免用户直接请求数据库,然后后台异步读取 redis 的 3 名用户更新到数据中。

微服务

关于微服务应该就是把基础代码原子性,拆成一个独立的系统。这个大家可以去查一下相关的文章,我只是知道为什么用,但是还没怎么运用到开发环境中,这里就不做详细讲解。

启发

本文就是给大家介绍高并发到底是怎么回事儿,到底对系统哪里有压力,要在系统架构里引入什么东西,才可以比较好的支撑住较高的并发压力。

而且你可以顺着本文的思路继续思考下去,结合你自己熟悉和知道的一些技术继续思考,比如说,你熟悉 Elasticsearch 技术,那么你就可以思考,唉?在高并发的架构之下,是不是可以通过分布式架构的 ES 技术支撑高并发的搜索?

上面所说,权当抛砖引玉。大家自己平时一定要多思考,自己多画图,盘点盘点自己手头系统的请求压力。计算一下分散到各个中间件层面的请求压力,到底应该如何利用最少的机器资源最好的支撑更高的并发请求。

这才是一个好的高并发架构设计思路。

文章转载请说明出处:八零岁月 » 系统是如何一步一步支持高并发的

分享到:更多 ()

吐槽集中营 抢沙发

评论前必须登录!