前段时间重构了公司的商城系统,主营方向是轻奢品,每单平均5000元以上,所以订单量比较少每天就200单左右,而每天系统的请求量在百万级,峰值qps晚上9点-12点半,达到万级,由此可见大多数用户都是喜欢看热闹,这也是一个典型的读多写少的高并发系统,也终于有机会把这几年积累的高并发系统设计方面的知识运用到实际,下面就来聊聊我所理解的几个解决方向。

高并发系统同一时间有多个需处理的请求的一种现象

高可用系统持续可靠的为用户提供服务,响应。(这里很多人理解为为客户端提供响应,服务,在我看来,在系统设计中,客户端也是程序的一部分,只有用户和系统,也可以理解为只有用户和公司两个个体。)

高负载同一时间内系统最大限度能处理的请求量,工作量

突刺流量系统突然收到大量请求的一种现象。

一. 分发

一直常听的一句话,加机器嘛,为什么加机器能解决,这就是最简单,最粗暴的高并发系统架构解决方案,比如我们万级,百万级并发,我一台机器扛不住,十台来抗嘛,十台扛不住,一百台嘛;万级并发分发到10台才1000qps,分到100台才100请求量,就是以大化小,这种思维也是算法解决中最常用最容易想到的分治法,我单台性能再差扛不住多的,总能抗住少的吧,所以一众大厂都多次在一些演讲场合公开表示他们早已过了靠不住就野蛮加机器的阶段,他们只是走到了混合多种技术方向的柏油路了,但是他们当初也是这么过来的,再说说常见的技术分发手段:

1. dns分发

这是在大型高并发系统常见的流量第一层分发手段,dns是基于互联网根服务器来做的,也就是用户用户请求你的域名时,先要通过互联网根域名服务器找到你的服务器ip,再请求你的服务器地址,你才会收到相应的请求,dns就是在这一层做文章,将你的域名请求流量分发到不同的ip上,目前主流的云服务提供商都提供相应的服务,这种方式在小公司很少见,基本用不到。这一层还能做许多事情,后面再细说。

2. nginx分发

nginx反向代理,这个词做我们这个的或多或少都听说过吧,这一层一般是一些公司的第一层流量分发手段,如果用上了上面说的dns分发技术,那这就是第二层。这一层主要就是将流量分发到下一层不同的服务器集群。

3. 注册中心分发

这一层就不陌生了吧,现在基本是个公司都要求微服务,分布式系统,那注册中心就必不可少了,这一层就功能之一就是为了结合负载均衡策略将流量分发到具体的服务或者机器,这也是最后一层分发技术。

经过上面三层技术手段的流量分发,基本上流量分发到每一台机器或者服务上,理论上只要你的钞票够多,没有处理不了的流量,哈哈哈,处理不了,加机器嘛。

二. 高可用

1. 负载均衡

有了上面的分发技术,如果没有负载均衡,别说三层分发,你就是+10层分发也没啥用,我们必须将流量均匀的分配到每一台机器或者每一个服务上,不能有的撑死,闲的闲死,要让每一台机器或者服务都物尽其用,从第一层往下看,其实每一层都是一个集群,如果不能负载均衡,先撑死一台,马上就是第二台,你有再多的机器,等待你的都是雪崩、多米诺骨牌效应,一台一台的倒下直至最后一台。负载均衡手段其实也是分发的具体策略研究,我们常见的一致性hash分发,轮询,最不常用,nginx4层/7层负载均衡等等,负载均衡也是系统高可用的研究方向之一,对于这三层分发的每一层,甚至每一个组件都是必不可少的。

2. 限流

系统上线前要充分的压测,评估系统的平稳运行的请求量是多少,防止过量请求、突刺流量,避免干崩服务器,当然还要设计好被拒绝服务的一个处理方式,如等待,直接拒绝等。常见的限流算法有固定窗口算法,滑动窗口算法,令牌桶算法,漏桶算法等,开源框架有Sentinel,Gateway,zuul等。

3. 兜底方案

这一步应该是所有系统都应该考虑的事情,说一个我亲自经历的事情,之前入职一家做仓储自动化设备的公司,货架上的穿梭机器人一旦发生网络延迟或是其他问题总是跳楼、撞车,经过了解,这些机器人全靠中央机房的saas系统来控制,而机器人本身无任何兜底方案,后面提出建议,让嵌入式的同事做了一些兜底程序,如断开控制1秒即降低运行速度,并标记相关路径,通知避障,断开三秒即按照设置的退出路径回到检修位置,从此相关事故大大减少。

兜底方案在我们的高并发系统中同样是必不可少的,这就是系统的最后保障了,如我们设计的系统预估压测的最大处理流量峰值是1w并发,但是实际上线突然峰值流量达到了2万,那我们的系统就会出现宕机的情况,这种情况下如果我们设置了如过载保护,拒绝服务这样兜底方案,就可以保证系统的可用性,虽然会损失小部分客流,但是顾全了大局,我们在设计系统时很多时候都会遇到这样的取舍问题,这时候能做的就是把利益最大化(可能是偏向服务的,偏向的客户的,具体情况得具体分析)。

兜底方案这是一个大方向的说法,在我们的web系统中常见的兜底方案包括限流,隔离,兜底路由,降级,拒绝服务等等。

三. 缓存

缓存似乎可以称为高并发系统的流量处理的银弹,可以万能一样,我们经常的口头禅就是性能不够,缓存来凑。其实也不然,适当的缓存可以提高系统吞吐性能,但是复杂度随之而来,说白了就是以空间换时间,多个空间对比一个空间,复杂度可想而知,这里的问题、平衡度很难处理,如如何优雅的解决比如静态缓存更新,缓存一致性,缓存三邪剑客缓存穿透,缓存穿透,缓存雪崩,如何识别热点数据等等,我相信大多数兄弟都有被各式缓存bug搞得找不到系统问题所在而抓头发的经历。

使用缓存的根本原理就是为了剪短请求链路,缓存总体可以分为两种,读缓和写缓存。读缓比较常见,用于快速响应用户获取系统已有数据的一种处理方式。写缓一般用于用户提交数据,数据保存并发量大,实时性不高的场景,遵循的理念是先收集后处理。

1. 浏览器,app缓存,电脑,手机(系统)缓存

浏览器其实就是一个公共标准的app,所以这两个一起说,我们在请求某个域名或者系统时,比如www.baidu.com,第一次请求是不是感觉很慢,当你第二次请求和之后请求相同内容时,就感觉快多了,这是浏览器会自动的把你的请求内容进行缓存,当你请求相同内容,浏览器第一步就会识别是否已经缓存过了,如果存在会直接取浏览器的缓存内容,当然我们也可以通过响应头Expires、Cache-control进行控制.在app中,就玩的更疯狂了,毕竟是自家的想怎么玩都可以,现在动辄10个g的app多半就是缓存占的,比如我要搞双11活动了,为了减少双11当天请求的压力,我会在双11之前就偷偷的把相关的资源缓存到用户手机上,当天直接使用就行了。这种机制下就要充分的评估这些数据的实时性、变更频次、安全性,适合不太大变化,实时性,变更程度不大,特定使用的一些数据,比如双11这种特定场景使用的一些数据,布局结构,省市区县这种静态数据。

电脑(系统)缓存这里我们是不能去控制的,可以忽略不记,比如内存寄存器、cpu缓存、路由器缓存,我们控制不了,也不会去改变他,只要知道他的存在就行,只是提一嘴。

2. cdn缓存

这一层呢是浏览器发现自己没有缓存,那就需要向服务器发起请求,中途到了上面dns解析这一层,也是这些头部企业玩的花样比较多的一层,这里就需要稍微的详细说说dns和cdn。

①.什么dns和cdn

dns,域名系统(英文:DomainNameSystem), 当我们在浏览器中输入一个域名时,不能直接识别域名,所以必须依靠某种环节将域名翻译成IP地址才能,这个环节就是DNS,上面就说到必须要先请求dns根域名拿到ip才能访问到我们的服务器。当我们向DNS服务器发起解析域名的请求时,首先会先查询你附近的国家基础网络服务器有没有缓存相应的域名信息,如果缓存中存在该域名,则可以直接返回IP地址,如果没有则会向更高一级的DNS服务器首先会查询缓存中有没有该域名,如果缓存中没有,服务器则会以递归的方式层层访问。例如,我们要访问www.google.com,假设每级dns服务器都没有缓存,一路请求到底大概就是这样子,首先首先是电脑浏览器,本地dns,我们再访问我们附近网络基础设备dns服务器,比如我现在在杭州,先会访问浙江省级dns服务器(我知道的就省级dns服务器,不知道更细粒度的有没有),再就是就进分配请求中国内的国家dns服务器,再就是请求全球13个dns根服务器的亚洲节点(我国互联网参与较晚,ipv4主根和辅根服务器没有一台在国内),然后先后向全球13个根服务器发起请求,直到找到www.google.com这条记录,发现是com域名,询问com域名的地址,然后再向负责com域名的名称服务器发送请求,找到google.com,如果是二级,三级域名还会往下请求,这样层层递归,最终找到我们需要的IP地址

CDN的全称是Content Delivery Network,翻译成中文就是内容分发网络,主要内容存储和分发技术。CDN依靠部署在各地的边缘服务器,通过中心平台的负载均衡、内容分发、调度等功能模块,使用户就近获取内容,降低网络延迟,提高访问速度。简单来讲,CDN是用来进行加速的,把web内容存储在上面说到的离你最近的服务器,它可以让用户更快获得所需的数据,设计初衷就是为了改善网络质量而生。按我的理解cdn就介于我们前面说的每一层dns网络和更高一层范围的dns网络之间,也可以理解为一个中间代理,通过cdn的分发技术让你找到缓存的信息,上面说dns会缓存域名信息,cdn会缓存域名相关的信息,和相应的服务器信息,让你能更快的就近访问和更快找到想要的内容。

②高并发系统cdn和dns缓存和分发

看了上面的介绍,我们知道cdn能做内容存储和分发技术,我们前面说的dns分发其实也是依靠cdn来做的,如果细心就会发现这些头部企业的网站在同等网络情况下总是比你的网站先打开,那是因为这些企业就基于这个cdn分发负载均衡做了异地多活和就近机房转发。

cdn的内容存储呢,也能为我们缓存相应的资源,比如静态文件,图片,静态页面,而达到更快响应用户的需求。

这一层呢只能做读缓。

3. nginx静态资源缓存

nginx也能为我们缓存相应的资源,比如静态文件,图片,静态页面,而达到更快响应用户的需求,剪短请求链路,也只能做不容易变动的静态资源读取,只能做读缓。

4. 内存级别缓存

如我们常用的H2数据库,Ehcache等java进程级别缓存,这类缓存数据库都是嵌在我们的系统中使用,数据都存在内存中,还是系统内嵌的,所以读写速度极高,一般都用来做服务单节点的热点数据的存储,提高系统的响应速度,他们随着java系统的启动关闭而启动消亡,一般都不做持久化。

5. 系统级别缓存

我们常用redis,memcache之类的就属于这一类的,属于横跨与整个系统或者多个节点之上,对于我们的服务来说,这一级别的缓存已经属于一个二方服务组件,连接还是需要驱动程序通过网络连接,redis虽然号称秒级10万处理速度,对于内存级别的缓存速度还是低了不少,但是他们的优点是不再受限于单机的硬件限制,可以集群,主从,备份持久化等高可用策略的使用。这一级的缓存一般都是设置在数据库请求前的最后一道屏障了,既可以缓读也可以做缓写,我之前做过的一个银行开放平台api就是通过这一层来做的缓写,对于用户的贷款申请,先写到redis中,快速给出响应,然后再异步入到数据库中,但是这种方案一定得注意淘汰策略和持久化。

四. 剪短请求链路

1. 从技术上实现剪短请求链路上面长篇大论提到的缓存就是剪短请求链路的一方面。

2. 从业务流程上剪短请求链路,比如这一项业务依次调用10个服务,能不能通过设计只调用两个服务,流程短了服务响应时间自然就快了,这里基本上就存在一个和服务之间的耦合度问题,我们拆服务的很大一方面也是为了降到耦合度,需要一定的取舍,我的建议一般都是优先考虑耦合度。

五. 异步、缓冲、拆分、并行

做好了上面的这些囊括为网络层面的处理,到这一步才真正的到了我们业务层面,应对大流量,除开代码功底层面,主要的处理方向就这四个方向,这四个放在一起说,基本上都是一起使用。

1. 异步,缓冲

目前主流的系统设计都是基于数据库来实现的各种系统,最终万川归海都要操作数据库,在大流量高并发下,一般最大的压力和瓶颈都是在数据库,往往程序往往都是先从数据库开始崩溃,而数据库的操作无非是增删改查,而增删改都是要改变数据,我们把他认为是写,这样一划分其实就只有两种了,查和写。

这一种思路多对于一些写处理耗时而不需要对用户同步返回最终结果,或是能给出简单结果的一般可以采用异步的处理方式,目的是快速的响应,这样子处理就能在同一时间处理更多的订单。比如购物下单,这个功能在大流量下我不能实时的去入库,又不能让用户一直等待着,但是我的系统只要收到这个订单的相应信息,确认无误,都可以给用户快速给出下单成功的响应,先存到缓存,然后再通过异步入库。这种情况下,采用的设计理念之一就是「先收集后处理」,一般都是接单先存到redis(缓存),利用redis的高吞吐量先收集,再采用性能比较高的worker框架推送到订单中心,这种是我们我自己去定制这种异步流程,现在一般的系统都采用Mq,利用mq的消息堆积能力来进行缓冲削峰,但是mq也需要注意磁盘负载过高的问题。

在我们的代码层面对于一些非实时性的功能,也应多使用异步的处理方式,如@sync注解,spring的Event listener机制等,用在如日志入库等非主线业务上能带来许多好处。

2. 拆分

在前几年主流单体服务设计思想中,整个系统就是一块儿,随着业务增长,这样设计的弊端也随着暴露出来,如一损俱损,哪怕只是一个很小非主线业务崩溃都会造成整个系统崩溃;如热点功能扩充机器,非热点功能也不得不扩充;这痛点就不一一列举了,于是就产生了微服务,这就是对一个大的系统按业务分界点或是技术实现进行拆分,在高并发系统设计中同样可以使用这种思路,如一个接口,需要耗时3s,那我可以把这个功能进行拆分成多个接口,还是上面拿上面举例说的购物订单这个场景,我可以把同步返回下单成功的接口拆分成两个接口,第一步为用户提交订单收集数据,第二部为主动推送结果或是加一个结果查询接口,这样就不用用户一直等待长时间处理接口的响应。

3.并行

上面两种思路多针对不需要对用户同步返回最终结果,或是能给出简单结果的一些场景,那如果我们某个业务场景需要同步返回结果呢,那就需要使用一些并行手段了,如一个业务场景是从1累加到100亿,那单个接口或是单个机器计算太慢了,又要同步给结果,那我可以把它差分成多个计算节点,每个节点只计算其中一小块数据,同步并行计算,这样子就快的多,目前大数据处理框架中多是运用这种思想。Java中有个比较经典的面试,我有abcd四个线程,我要等待abcd都执行完成了才执行主线程,可以用CountDownLatch和CyclicBarrier来解决,就是为了解决这种并行的场景而生的。

六. 拆分次要逻辑

这一点在我们的程序性能优化经常会遇到,其实我们在程序设计开发之初,就应该理清主次逻辑,甚至要将次要逻辑剥离主线逻辑,避免整个方法压力过大或是执行过长。

这里举两个例子:

1.我们常遇到的日志入库,那如果我们在程序主线逻辑中直接去调用入库方法,那么主线逻辑就会因为等待入库而增加耗时,那我们完全可以直接把日志入库的方法做成异步的,从而主线方法能很快的返回。

2.这里举说的一个我们之前遇到的业务场景,用户参加活动,或是领券,都会到一个截至时间,到了截至时间之前一个点会向用户发一个通知消息,就是基于这样一个场景,这里只是简单描述下,实际设计太复杂,详细讲必然长篇大论,最开始别人实现的用定时任务秒级去扫活动表,造成这几张表压力太大,对于活动来说,通知就是一个微不足道小功能,影响了主线任务,后面我对这块改造的是让其他查询活动的接口把数据通过event丢给我,这样就减少了很多的查询,为了防止还有没查询到的任务,再把定时任务时间时间调成5分钟做一个兜底扫描,这样就把这几张表的压力给分担出来了。后面去通知发消息的,原来做的也是同步发送,有的方法需要通知到多个端,处理很慢,就造成定时任务大量堆积,其实完全可以把执行这块的剥离出来,定时任务就充当一个发起的角色就好了,这里的详细设计就不讲了。

由于本人水平有限,以上内容仅供大家有个思考的方向,如有谬误或不足之处,欢迎斧正。以上一些举例也仅仅是为了举例说明,并不代表同名的实际业务场景就得这样设计,还是那句话,最好的技术,最好的设计永远只有一个评判标准就是是否最好的在基于当前技术环境等一切相关条件的基础上解决了业务场景,需求,痛点,没有最好的技术,只有最符合的技术。本来几个月前,就写好了一版,结果忘了保存,浏览器卡死啥都没了,后面也就没了心情,这两天又觉得还是要写一下,这一版总没有开始写的那种感觉了,很多时候灵感真的只是一瞬间,第一版写了1w多个字,这一版就连这些废话也只补了6000出头,还有些拼凑之感,等后面有想法了再补补。