亿级(无限级)并发,没那么难



肉眼品世界导读: 小编曾经看到高并发几个字也是欣喜若狂,如今变得淡定了,到底多高并发就是高并发了,TPS,QPS是指什么?亿级,千亿级并发真的那么难实现吗?互联网高并发架构设计的原则是什么,看遍了大厂小厂无数架构,是否可以发现都大同小异,万变不离其宗?好,我们就一起来揭秘吧;市面上很少有这么完善和透彻的,觉得不错就点赞转发吧
更多优质内容请关注微信公众号“肉眼品世界”(ID:find_world_fine),深度价值体系传递多大并发是高并发


一、什么是高并发
定义:
高并发(High Concurrency)是使用技术手段使系统可以并行处理很多请求。
关键指标:
-响应时间(Response Time)
-吞吐量(Throughput)
-每秒查询率QPS(Query Per Second)
-每秒事务处理量TPS(Transaction Per Second)
-同时在线用户数量
关键指标的维度:
-平均,如:小时平均、日平均、月平均
-Top百分数TP(Top Percentile),如:TP50、TP90、TP99、TP4个9
-最大值
-趋势
「并发」由于在互联网架构中,已经从机器维度上升到了系统架构层面,所以和「并行」已经没有清晰的界限。「并」(同时)是其中的关键。由于「同时」会引发多久才叫同时的问题,将时间扩大,又根据不同业务关注点不同,引申出了引申指标。
引申指标:
-活跃用户数,如:日活DAU(Daily Active User)、月活MAU(Monthly Active Users)
-点击量PV(Page View)
-访问某站点的用户数UV(Unique Visitor)
-独立IP数IP(Internet Protocol)
-日单量
二、多大算高并发
这个问题的答案不是一个数字。来看两个场景:
场景1:
木头同学去一家创业公司面试。这个公司做的产品还没有上线,面试官小熊之前就职过公司的产品都没有什么量。
小熊:“有高并发经验吗?”
木头:“我们服务单机QPS2000+,线上有4台机器负载均衡。”
这时候小熊心里的表情大概是:


但是如果小熊就职的公司是美团之类的。那这这时候小熊心里的表情大概是:


场景2:
固态硬盘SSD(Solid State Disk)说:我读取和写入高达 1000MB/秒
mysql说:我单机TPS10000+
nginx说:我单机QPS10W+
静儿说:给我一台56核200G高配物理机,我可以创建一个单机QPS1000W


不在同一维度,没有任何前提,无法比较谁更牛。“我的系统算不算高并发?”这个问题就如同一个女孩子爱问的问题:“我美不美?”
三、高并发是解决问题的方式
俗话说:「没有对比就没有伤害」。算不算高并发,这个问题的答案需要加对比和前提。
对比包括:
-业界:在业界同类产品中并发量处于什么位置。举个栗子,美团外卖的日单量是千万级别,一个系统日单量在百万,虽然差一个数量级,但是相比大多数公司已经很不错。
-自身:在自身系统中,并发问题是否已经是系统的瓶颈?如果是,这么这个瓶颈怎么打破?如果不是,那当初架构设计的时候是怎么保证并发不是问题的?(别告诉我:是通过系统没有访问量来保证的[擦汗])。
前提包括:
-业务复杂度:举个栗子,访问百度首页的时间基本就是看自己家的网速,通常情况下都是点一下就看到结果了。而扫描二维码支付,通常需要等很久,虽然这可能已经是业界最牛的支付公司出品了。
-配置:用高配物理机得出的数据和最老最低配的虚拟器上的出来的结果是无法比较的。通常的配置有:cpu、内存、磁盘、带宽、网卡
高并发的本质不是「多大算高并发」的一个数字,而是从架构上、设计上、编码上怎么来保证或者解决由并发引起的问题。当别人问你:“做过高并发吗?”回答者完全可以描述自己系统的各项指标,然后开始叙述自己对系统中对预防、解决并发问题作出的思考和行动。
这里就有一个非常重要的概念要讲一下了,经常面试官问,你们系统是多大并发,我说单台2000,面试官就会问2000 QPS ,还是PV
秒懂QPS、TPS、PV、UV、IP
QPS、TPS、PV、UV、GMV、IP、RPS等各种名词,外行看起来很牛X,实际上每个程序员都是必懂知识点。下面我来一一解释一下。

 

QPS
Queries Per Second,每秒查询数。每秒能够响应的查询次数。
QPS是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准,在因特网上,作为域名系统服务器的机器的性能经常用每秒查询率来衡量。每秒的响应请求数,也即是最大吞吐能力。

 

TPS
Transactions Per Second 的缩写,每秒处理的事务数目。一个事务是指一个客户机向服务器发送请求然后服务器做出反应的过程。客户机在发送请求时开始计时,收到服务器响应后结束计时,以此来计算使用的时间和完成的事务个数,最终利用这些信息作出的评估分。
TPS 的过程包括:客户端请求服务端、服务端内部处理、服务端返回客户端。
例如,访问一个 Index 页面会请求服务器 3 次,包括一次 html,一次 css,一次 js,那么访问这一个页面就会产生一个“T”,产生三个“Q”。

 

PV
PV(page view)即页面浏览量,通常是衡量一个网络新闻频道或网站甚至一条网络新闻的主要指标。
PV 即 page view,页面浏览量。用户每一次对网站中的每个页面访问均被记录 1 次。用户对同一页面的多次刷新,访问量累计。
根据这个特性,刷网站的 PV 就很好刷了。
与 PV 相关的还有 RV,即重复访问者数量(repeat visitors)。

 

UV
UV 访问数(Unique Visitor)指独立访客访问数,统计1天内访问某站点的用户数(以 cookie 为依据),一台电脑终端为一个访客。

 

IP
IP(Internet Protocol)独立 IP 数,是指 1 天内多少个独立的 IP 浏览了页面,即统计不同的 IP 浏览用户数量。同一 IP 不管访问了几个页面,独立 IP 数均为 1;不同的 IP 浏览页面,计数会加 1。IP 是基于用户广域网 IP 地址来区分不同的访问者的,所以,多个用户(多个局域网 IP)在同一个路由器(同一个广域网 IP)内上网,可能被记录为一个独立 IP 访问者。如果用户不断更换 IP,则有可能被多次统计。

 

GMV
GMV,是 Gross Merchandise Volume 的简称。只要是订单,不管消费者是否付款、卖家是否发货、是否退货,都可放进 GMV 。

 

RPS
RPS 代表吞吐率,即 Requests Per Second 的缩写。吞吐率是服务器并发处理能力的量化描述,单位是 reqs/s,指的是某个并发用户数下单位时间内处理的请求数。某个并发用户数下单位时间内能处理的最大的请求数,称之为最大吞吐率。
有人把 RPS 说等效于 QPS。其实可以看作同一个统计方式,只是叫法不同而已。RPS/QPS,可以使用 apche ab 工具进行测量。
承载高并发系统的本质
小编刚上班的时候听到高并发也是热血沸腾,呼啦啦的看各个厂的架构,恨不得一下吃完,后面越看越多,发现怎么各家都一样的。互联网架构的三架马车屡试不爽,提供了非常强大简易的标准化解决方案:微服务、消息队列、定时任务


那么高并发系统的本质是什么呢,第一个:这么多请求来了,要在你机器里跑,是不是需要网络流量,网络流量太大是不是把你的程序都卡了?那么就出现了把占流量的图片资源静态资源分析,再CDN一下,你的代码可以更安心的跑了;第二个就是:这么多请求来了,每个来的请求都是带着一堆数据过来的,第一层要对数据做处理,处理完又要存储,最后的处理都是把数据放到内存里,内存递给cpu来处理,而内存数据有时又需要从硬盘拿,这就变成了内存,硬盘,cpu 这三者之间的关系了,我们来看看这几者之间的关系

 

计算机为什么要并发?
为什么要并发?因为人类的贪婪!因为人类想要向计算机索取更多!人类想要一步,进一步的压榨计算机。所以,为了更高效的利用计算机的计算能力,人类想出来了让计算机并发执行多个任务的办法!

 

高速处理器与低速存储之间的矛盾
我们都知道,计算机中的处理器决定了计算机的处理速度,现代处理器的计算速度是惊人的。但是,处理器并不能自己单独工作,至少在进行运算时需要与进行内存交互,读取数据,或是写入数据。内存这种存储设备的速度是没有办法和处理器相比的,这就造成了造成了处理器计算能力的浪费。这种矛盾就犹如人民日益增长的美好生活需要和不平衡不充分的发展之间的矛盾一样,需要聪明的程序员去解决!

 

调和处理器与内存的矛盾
聪明的人类想到一个办法,他们在处理器与内存中间,增加一层高速缓存,这层缓存作为处理器与内存之间的缓冲区。将需要处理的数据放入高速缓存,处理结束后再重新放入到内存中,这样就处理器就不需要无尽的等待与内存之间的交互。这样一波操作看似十分的完美!但是,请往下看!

 

高速缓存带来的并发问题
高速缓存看似完美的解决了高速的处理器与低速缓存之间的矛盾。但是,问题确被复杂化了。在多处理器的系统中,每个处理器都会有自己的高速缓存,数据都会被复制到高速缓存中处理,处理后再放回主内存。由于主内存是共享的,如果不同处理器高速缓存中的数据不同,这样就会造成缓存不一致的问题!

 

解决并发问题高速缓存并发问题
高速缓存的数据不一致,这种现象就像邻居家的小孩子吵架,吵到最后就需要家长出面,把矛盾化解掉。对于计算机来说,这个家长就是缓存一致性协议,在读写操作时按照协议来操作,避免并发问题。这样,就完美的解决了所有的问题!


处理器、高速缓存、缓存一致性协议、主内存

 

计算机高效并发的其他操作
除了高效缓存机制外,还有一些其他的操作也可以提高效率。例如,与Java的指令重排序一样,处理器也会根据执行效率,将代码进行重新排序进行执行。有同学可能是我有多线程,我有缓存,我还有神器redis,那才是高并发,没错,尽可能高的提高cpu的利用率,把数据都放在内存里进行,最靠近cpu的地方,所以我们的所有的互联网高并发架构无非都是把数据更可能的不从磁盘里读取和运算,更多的放入内存,同时通过多线程等方式提高cpu的利用率,哈哈,就是监控你cpu有没有偷懒的时候,压榨cpu呀;cpu内存总有忙不过来的时候,这个时候怎么办呢,就是等你不忙的时候来个定时任务或者跑到另外一台机器的队列来执行。
实在单机扛不过来的时候,就喊人来帮忙,于是有了集群;有的单元模块嫌弃另外的单元模块影响我运行,于是有了微服务。
于是我们看到了大多数互联网架构都是往内存里塞数据,亲切的称之为缓存,通过优化算法减少程序的执行时间,实在不行来个磁盘顺序写,如kfaka和mysql的redo log,每次都写数据都嫌累,就有了redis pipeline ,mysql redo log 组提交这样的操作
在高并发场景下,有时又需要读的同时又在写,怎么避免读取到脏数据呢,这是大多数中间件要考虑的事情,而大多数中间件其实是C语言写的,在这种情况下C一般会干什么事呢,修改数据的时原内存块我不动你,我新在已经分配好的内存块里找一个地方去修改数据,修改完了,再重新拷贝过去
另外一块就是网络请求的处理方式,epoll,poll,select,java的nio了, 由于这块相对比较比较重要,稍微展开一下:

 

用户空间以及内核空间概念
我们知道现在操作系统都是采用虚拟存储器,那么对32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方)。操心系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核,保证内核的安全,操心系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。针对linux操作系统而言,将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为内核空间,而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,称为用户空间。每个进程可以通过系统调用进入内核,因此,Linux内核由系统内的所有进程共享。于是,从具体进程的角度来看,每个进程可以拥有4G字节的虚拟空间。
空间分配如下图所示:


有了用户空间和内核空间,整个linux内部结构可以分为三部分,从最底层到最上层依次是:硬件-->内核空间-->用户空间。
如下图所示:


需要注意的细节问题,从上图可以看出内核的组成:
 
  1. 内核空间中存放的是内核代码和数据,而进程的用户空间中存放的是用户程序的代码和数据。不管是内核空间还是用户空间,它们都处于虚拟空间中。
  2. Linux使用两级保护机制:0级供内核使用,3级供用户程序使用。
 

 

Linux 网络 I/O模型


我们都知道,为了OS的安全性等的考虑,进程是无法直接操作I/O设备的,其必须通过系统调用请求内核来协助完成I/O动作,而内核会为每个I/O设备维护一个buffer。如下图所示:


整个请求过程为:用户进程发起请求,内核接受到请求后,从I/O设备中获取数据到buffer中,再将buffer中的数据copy到用户进程的地址空间,该用户进程获取到数据后再响应客户端。
在整个请求过程中,数据输入至buffer需要时间,而从buffer复制数据至进程也需要时间。因此根据在这两段时间内等待方式的不同,I/O动作可以分为以下五种模式:
阻塞I/O (Blocking I/O) 非阻塞I/O (Non-Blocking I/O) I/O复用(I/O Multiplexing) 信号驱动的I/O (Signal Driven I/O) 异步I/O (Asynchrnous I/O) 说明:如果像了解更多可能需要linux/unix方面的知识了,可自行去学习一些网络编程原理应该有详细说明,不过对大多数java程序员来说,不需要了解底层细节,知道个概念就行,知道对于系统而言,底层是支持的。 本文最重要的参考文献是Richard Stevens的“UNIX® Network Programming Volume 1, Third Edition: The Sockets Networking ”,6.2节“I/O Models ”,公众号【肉眼品世界】回复:linuxsocket或者linux001 ,获取该资料,建议电脑下载(比较大以及chm格式),本文中的流程图也是截取自中。
 

 
记住这两点很重要 1 等待数据准备 (Waiting for the data to be ready) 2 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)

 

阻塞I/O (Blocking I/O)
在linux中,默认情况下所有的socket都是blocking,一个典型的读操作流程大概是这样:


当用户进程调用了recvfrom这个系统调用,内核就开始了IO的第一个阶段:等待数据准备。对于network io来说,很多时候数据在一开始还没有到达(比如,还没有收到一个完整的UDP包),这个时候内核就要等待足够的数据到来。而在用户进程这边,整个进程会被阻塞。当内核一直等到数据准备好了,它就会将数据从内核中拷贝到用户内存,然后内核返回结果,用户进程才解除block的状态,重新运行起来。所以,blocking IO的特点就是在IO执行的两个阶段都被block了。

 

非阻塞I/O (Non-Blocking I/O)
linux下,可以通过设置socket使其变为non-blocking。当对一个non-blocking socket执行读操作时,流程是这个样子:


当用户进程调用recvfrom时,系统不会阻塞用户进程,而是立刻返回一个ewouldblock错误,从用户进程角度讲 ,并不需要等待,而是马上就得到了一个结果。用户进程判断标志是ewouldblock时,就知道数据还没准备好,于是它就可以去做其他的事了,于是它可以再次发送recvfrom,一旦内核中的数据准备好了。并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回。当一个应用程序在一个循环里对一个非阻塞调用recvfrom,我们称为轮询。应用程序不断轮询内核,看看是否已经准备好了某些操作。这通常是浪费CPU时间,但这种模式偶尔会遇到。

 

I/O复用(I/O Multiplexing)
IO multiplexing这个词可能有点陌生,但是如果我说select,epoll,大概就都能明白了。有些地方也称这种IO方式为event driven IO。我们都知道,select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select/epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。它的流程如图:


当用户进程调用了select,那么整个进程会被block,而同时,内核会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从内核拷贝到用户进程。这个图和blocking IO的图其实并没有太大的不同,事实上,还更差一些。因为这里需要使用两个system call (select 和 recvfrom),而blocking IO只调用了一个system call (recvfrom)。但是,用select的优势在于它可以同时处理多个connection。(多说一句。所以,如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。) 在IO multiplexing Model中,实际中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。

 

文件描述符fd
Linux的内核将所有外部设备都可以看做一个文件来操作。那么我们对与外部设备的操作都可以看做对文件进行操作。我们对一个文件的读写,都通过调用内核提供的系统调用;内核给我们返回一个filede scriptor(fd,文件描述符)。而对一个socket的读写也会有相应的描述符,称为socketfd(socket描述符)。描述符就是一个数字,指向内核中一个结构体(文件路径,数据区,等一些属性)。那么我们的应用程序对文件的读写就通过对描述符的读写完成。

 

select
基本原理:select 函数监视的文件描述符分3类,分别是writefds、readfds、和exceptfds。调用后select函数会阻塞,直到有描述符就绪(有数据 可读、可写、或者有except),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。当select函数返回后,可以通过遍历fdset,来找到就绪的描述符。
缺点: 1、select最大的缺陷就是单个进程所打开的FD是有一定限制的,它由FDSETSIZE设置,32位机默认是1024个,64位机默认是2048。一般来说这个数目和系统内存关系很大,”具体数目可以cat /proc/sys/fs/file-max察看”。32位机默认是1024个。64位机默认是2048. 2、对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低。当套接字比较多的时候,每次select()都要通过遍历FDSETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。”如果能给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,那就避免了轮询”,这正是epoll与kqueue做的。3、需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大。

 

poll
基本原理:poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。
它没有最大连接数的限制,原因是它是基于链表来存储的,但是同样有一个缺点:1、大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。2 、poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。
注意:从上面看,select和poll都需要在返回后,通过遍历文件描述符来获取已经就绪的socket。事实上,同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。

 

epoll
epoll是在2.6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。
基本原理:epoll支持水平触发和边缘触发,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就绪态,并且只会通知一次。还有一个特点是,epoll使用“事件”的就绪通知方式,通过epollctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epollwait便可以收到通知。
epoll的优点:1、没有最大并发连接的限制,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口)。2、效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数;即Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll。3、内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销。
JDK1.5_update10版本使用epoll替代了传统的select/poll,极大的提升了NIO通信的性能。
备注:JDK NIO的BUG,例如臭名昭著的epoll bug,它会导致Selector空轮询,最终导致CPU 100%。官方声称在JDK1.6版本的update18修复了该问题,但是直到JDK1.7版本该问题仍旧存在,只不过该BUG发生概率降低了一些而已,它并没有被根本解决。这个可以在后续netty系列里面进行说明下。

 

信号驱动的I/O (Signal Driven I/O)
由于signal driven IO在实际中并不常用,所以简单提下。
 


很明显可以看出用户进程不是阻塞的。首先用户进程建立SIGIO信号处理程序,并通过系统调用sigaction执行一个信号处理函数,这时用户进程便可以做其他的事了,一旦数据准备好,系统便为该进程生成一个SIGIO信号,去通知它数据已经准备好了,于是用户进程便调用recvfrom把数据从内核拷贝出来,并返回结果。

 

异步I/O
一般来说,这些函数通过告诉内核启动操作并在整个操作(包括内核的数据到缓冲区的副本)完成时通知我们。这个模型和前面的信号驱动I/O模型的主要区别是,在信号驱动的I/O中,内核告诉我们何时可以启动I/O操作,但是异步I/O时,内核告诉我们何时I/O操作完成。


当用户进程向内核发起某个操作后,会立刻得到返回,并把所有的任务都交给内核去完成(包括将数据从内核拷贝到用户自己的缓冲区),内核完成之后,只需返回一个信号告诉用户进程已经完成就可以了。

 

5中I/O模型的对比
 
结果表明:前四个模型之间的主要区别是第一阶段,四个模型的第二阶段是一样的:过程受阻在调用recvfrom当数据从内核拷贝到用户缓冲区。然而,异步I/O处理两个阶段,与前四个不同。
 


从同步、异步,以及阻塞、非阻塞两个维度来划分来看:
 

 

零拷贝


CPU不执行拷贝数据从一个存储区域到另一个存储区域的任务,这通常用于在网络上传输文件时节省CPU周期和内存带宽。

 

缓存 IO
缓存 IO 又被称作标准 IO,大多数文件系统的默认 IO 操作都是缓存 IO。在 Linux 的缓存 IO 机制中,操作系统会将 IO 的数据缓存在文件系统的页缓存( page cache )中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。
缓存 IO 的缺点:数据在传输过程中需要在应用程序地址空间和内核进行多次数据拷贝操作,这些数据拷贝操作所带来的 CPU 以及内存开销是非常大的。

 

零拷贝技术分类
零拷贝技术的发展很多样化,现有的零拷贝技术种类也非常多,而当前并没有一个适合于所有场景的零拷贝技术的出现。对于 Linux 来说,现存的零拷贝技术也比较多,这些零拷贝技术大部分存在于不同的 Linux 内核版本,有些旧的技术在不同的 Linux 内核版本间得到了很大的发展或者已经渐渐被新的技术所代替。本文针对这些零拷贝技术所适用的不同场景对它们进行了划分。概括起来,Linux 中的零拷贝技术主要有下面这几种:
  • 直接 I/O:对于这种数据传输方式来说,应用程序可以直接访问硬件存储,操作系统内核只是辅助数据传输:这类零拷贝技术针对的是操作系统内核并不需要对数据进行直接处理的情况,数据可以在应用程序地址空间的缓冲区和磁盘之间直接进行传输,完全不需要 Linux 操作系统内核提供的页缓存的支持。
  • 在数据传输的过程中,避免数据在操作系统内核地址空间的缓冲区和用户应用程序地址空间的缓冲区之间进行拷贝。有的时候,应用程序在数据进行传输的过程中不需要对数据进行访问,那么,将数据从 Linux 的页缓存拷贝到用户进程的缓冲区中就可以完全避免,传输的数据在页缓存中就可以得到处理。在某些特殊的情况下,这种零拷贝技术可以获得较好的性能。Linux 中提供类似的系统调用主要有 mmap(),sendfile() 以及 splice()。
  • 对数据在 Linux 的页缓存和用户进程的缓冲区之间的传输过程进行优化。该零拷贝技术侧重于灵活地处理数据在用户进程的缓冲区和操作系统的页缓存之间的拷贝操作。这种方法延续了传统的通信方式,但是更加灵活。在Linux 中,该方法主要利用了写时复制技术。
前两类方法的目的主要是为了避免应用程序地址空间和操作系统内核地址空间这两者之间的缓冲区拷贝操作。这两类零拷贝技术通常适用在某些特殊的情况下,比如要传送的数据不需要经过操作系统内核的处理或者不需要经过应用程序的处理。第三类方法则继承了传统的应用程序地址空间和操作系统内核地址空间之间数据传输的概念,进而针对数据传输本身进行优化。我们知道,硬件和软件之间的数据传输可以通过使用 DMA 来进行,DMA 进行数据传输的过程中几乎不需要CPU参与,这样就可以把 CPU 解放出来去做更多其他的事情,但是当数据需要在用户地址空间的缓冲区和 Linux 操作系统内核的页缓存之间进行传输的时候,并没有类似DMA 这种工具可以使用,CPU 需要全程参与到这种数据拷贝操作中,所以这第三类方法的目的是可以有效地改善数据在用户地址空间和操作系统内核地址空间之间传递的效率。
注意,对于各种零拷贝机制是否能够实现都是依赖于操作系统底层是否提供相应的支持。
 


当应用程序访问某块数据时,操作系统首先会检查,是不是最近访问过此文件,文件内容是否缓存在内核缓冲区,如果是,操作系统则直接根据read系统调用提供的buf地址,将内核缓冲区的内容拷贝到buf所指定的用户空间缓冲区中去。如果不是,操作系统则首先将磁盘上的数据拷贝的内核缓冲区,这一步目前主要依靠DMA来传输,然后再把内核缓冲区上的内容拷贝到用户缓冲区中。接下来,write系统调用再把用户缓冲区的内容拷贝到网络堆栈相关的内核缓冲区中,最后socket再把内核缓冲区的内容发送到网卡上。
从上图中可以看出,共产生了四次数据拷贝,即使使用了DMA来处理了与硬件的通讯,CPU仍然需要处理两次数据拷贝,与此同时,在用户态与内核态也发生了多次上下文切换,无疑也加重了CPU负担。在此过程中,我们没有对文件内容做任何修改,那么在内核空间和用户空间来回拷贝数据无疑就是一种浪费,而零拷贝主要就是为了解决这种低效性。
### 让数据传输不需要经过user space,使用mmap 我们减少拷贝次数的一种方法是调用mmap()来代替read调用:
  1. buf = mmap(diskfd, len);
  2. write(sockfd, buf, len);
应用程序调用 mmap(),磁盘上的数据会通过 DMA被拷贝的内核缓冲区,接着操作系统会把这段内核缓冲区与应用程序共享,这样就不需要把内核缓冲区的内容往用户空间拷贝。应用程序再调用 write(),操作系统直接将内核缓冲区的内容拷贝到 socket缓冲区中,这一切都发生在内核态,最后, socket缓冲区再把数据发到网卡去。
同样的,看图很简单:


使用mmap替代read很明显减少了一次拷贝,当拷贝数据量很大时,无疑提升了效率。但是使用 mmap是有代价的。当你使用 mmap时,你可能会遇到一些隐藏的陷阱。例如,当你的程序 map了一个文件,但是当这个文件被另一个进程截断(truncate)时, write系统调用会因为访问非法地址而被 SIGBUS信号终止。SIGBUS信号默认会杀死你的进程并产生一个 coredump,如果你的服务器这样被中止了,那会产生一笔损失。
通常我们使用以下解决方案避免这种问题:
  1. 为SIGBUS信号建立信号处理程序 当遇到 SIGBUS信号时,信号处理程序简单地返回, write系统调用在被中断之前会返回已经写入的字节数,并且 errno会被设置成success,但是这是一种糟糕的处理办法,因为你并没有解决问题的实质核心。
  2. 使用文件租借锁 通常我们使用这种方法,在文件描述符上使用租借锁,我们为文件向内核申请一个租借锁,当其它进程想要截断这个文件时,内核会向我们发送一个实时的 RT_SIGNAL_LEASE信号,告诉我们内核正在破坏你加持在文件上的读写锁。这样在程序访问非法内存并且被 SIGBUS杀死之前,你的 write系统调用会被中断。 write会返回已经写入的字节数,并且置 errno为success。 我们应该在 mmap文件之前加锁,并且在操作完文件后解锁:
 
  1. if(fcntl(diskfd, F_SETSIG, RT_SIGNAL_LEASE) == -1) {
  2. perror("kernel lease set signal");
  3. return-1;
  4. }
  5. /* l_type can be F_RDLCK F_WRLCK 加锁*/
  6. /* l_type can be F_UNLCK 解锁*/
  7. if(fcntl(diskfd, F_SETLEASE, l_type)){
  8. perror("kernel lease set type");
  9. return-1;
  10. }
关于更多零拷贝的问题可以查看《如何理解 Linux 的零拷贝技术?
所以,我们可以看到高并发的本质是通过软件层编码的方法充分使用内存和cpu,这是单机,再加集群就是无限级了,当然集群的管理和监控也是相当重要的组成部分
这里面最重要的部分就是
1:充分利用内存,压榨cpu
2:处理好网络IO,socketfd这东西在贯穿网络的主要核心,了解了socketfd就基本了解了外界于linux打交道的原理,至于怎么打交道就是poll,select,epoll的事情了
3:算法,算法也是软件编码层的充分使用硬件相对独立的一个分支,关于算法相关内容可以参考:
十大经典排序算法(动图演示,收藏好文)
《一遍记住 Java 面试中常用的八种排序算法与代码实现!
主宰这个世界的10种算法
已经绝版的《数据结构和算法(第二版)》高清电子书!
(附下载) 435页经典书籍《算法心得:高效算法的奥秘(第2版)》
【干货】350+页的《数据结构与算法》pdf
一致性哈希算法在分布式场景中的应用
今日头条、抖音推荐算法原理全文详解
基于以上三点又构建了诸如docker,nginx server,lvs,redis,多线程语言,数据库,分布式存储的一系列应用,至于其他,小编也在射射发抖的学习...


如何构建高并发系统
知道了高并发系统的相关实质,那么我们还得会使用前辈们造好的轮子来搭配好,平时我们所说的高并发系统更多的是用工具来组合形成一个稳定高效的系统,也就是所谓的高并发高可用,如果并发太大又需要对前辈们造的轮子做改造,而大多数组件是C语言写的,所以做架构能会C,能了解到相关组件的源码并在需要的情况下做修改是架构师向更高层次发展需要具备的要素,大佬们造的那些轮子也是支撑高并发系统的核心要素,如redis,lvs,nginx,mysql,感谢开源,站在巨人的肩膀上做架构竟然变得如何美好;高可用是伴随高并发而产生的,前面说过高并发,这里又有一个高可用,什么是高可用呢

 

1、什么是高可用
高可用指系统的可用程度。没有100%的可用性。打个夸张的比方说,部署在全球的所有机房都同时停电了,那么系统就不能再提供服务。一般我们只需要做到4个9就已经很不错了,如下图:
 

 

2、高可用分类
按照业务=逻辑+数据来分,高可用分为计算高可用和存储高可用,逻辑即数据,数据即存储。

 

2.1 计算高可用
常见的计算高可用架构分为主备、主从、对称集群、非对称集群。
主备:


主从:


对称集群:


非对称集群:
 

 

2.2 存储高可用
常见的存储高可用有主备、主从、主备/主从切换,主主,集群(数据集中集群和数据分散集群),分区(洲际、国家、城市、同城分区)。
主备:


主从:


数据集中式集群:


主主集群:


数据分散集群: 比如HDFS的架构。


在存储界,还有一个比较有名的组件FastDFS,基本设计也是类似,关于高可用的知识图谱,可以作为参考:


往往我们开始做系统时,并不是一开始就设计这么复杂的架构,架构核心的内容是大道至简,架构也是根据需要一步步演进而来的,一般目前的架构要比现在实际用户量并发量至少高一个层级,创业公司刚上来也没必要搞那么复杂的系统,关于基础的技术选型可以查看《创业公司技术选型与管理注意事项》,一个springboot跑起来再说,够你玩好久了,等你量达到那种程度了;但对于技术来说,还是能够提前做到心中有数比较好

 

某些App怎么扛住1分钟10亿请求?
架构的演进路线百万级并发:1秒100万次请求。千万级并发:一分钟6亿次请求,差不多就是需求的极限。架构的设计和架构优化要符合需求本身,不能无限制优化。基本概念(1)分布式(系统中,多个模块在不同服务器上部署)(2)集群(一个软件部署在多台服务器,并作为一个整体,提供一类服务)(3)高可用(系统中部分节点失效,其他节点能够接替它继续工作或有相应的处理预案)(4)负载均衡(把请求均匀的发到多个节点上)架构演进:
1
单机架构


DNS服务器,做域名解析的服务器,作用是,经过DNS将www.taobao.com这类的域名转换为实际IP地址,浏览器转而访问这个IP对应的tomcat。如果是单机一般用服务商自己的DNS解析即可,如果是多机DNSPOD在域名轮询层就是不错解决方案;java用tomcat,php用nginx,springboot这骚货还自带tomcat,你要嫌麻烦直接启动springboot也是可以的瓶颈:用户增长,java(php)代码、静态资源和数据库之间竞争资源,单机性能不足以支撑业务,万一哪台机器内存突然出点儿状况,你的服务也就不可用了。
2
第一次演进:动静分离、代码与数据库分离


java(php,go)代码和数据库分别独占服务器资源,显著提高两者各自性能,这个时候也可以把html和图片静态文件资源分离,目前大多数情况下是前后端分离的,创业公司用vue把打包过后的文件单独部署一个域名,随时可以通过jenkins部署到新服务器(tomcat服务器找一个内存大的,DB服务器找一个硬盘大的,带宽更宽的)。瓶颈:用户量增长,数据库并发读写,尤其是读,成为瓶颈。注意,不会通过数据库集群解决
3
第二次演进:引进CDN,缓存甚至分布式缓存


在Tomcat服务器上(Java程序所在的地方)加入缓存,可以把绝大多数请求(尤其是查询)在访问数据库前拦截掉。如果你的服务器是nginx或者有nginx代理层,nginx可以直接访问和刷新缓存的


Redis一般放在单独的服务器上,也可以多弄几台Redis服务器,配置成主从同步(提升可用性可以加上哨兵)。 redis的进群解决方案一般有Codis,Redis Cluster,twemproxy,redis主从,这几种都各有优缺点,如果你的redis集群量不大,搞个主从就可以了;提到天猫和redis,自然少不了秒杀场景,秒杀场景可以参考《解密 Redis 助力双 11 背后电商秒杀系统》的一些做法当然,这个时候消息队列也可以出现在系统里了,开始用redis也可以抗一阵儿,金融场景RocketMQ是不错的选择,kafka追求高吞吐量,起源的时候是用于海量日志传输,但是随着版本的越来越完善,可靠性也在不断提高,据说(没亲眼见代码)某滴滴公司消息推送就使用kafka了瓶颈:用户数量增长,并发压力主要在单机的tomcat+java上,响应逐渐变慢。单机java变慢还可以优化呀,每个api接口的时间监控起来,mysql慢查询监控起来,推荐一款阿里问题定位神器《阿里问题定位神器 Arthas 的骚操作,定位线上BUG,超给力
4
第三次演进:引入反向代理和负载均衡


使用反向代理,将大量的用户请求,均匀分发到每个java应用程序中。关于nginx为什么能支撑这么大并发可以阅读《只知道 Nginx 牛逼,却不知道它怎么支持百万并发?》瓶颈:应用服务器可支持的并发量大大增加,缓存能力也可以轻易扩展,并发量增长意味着更多请求穿透到数据库,单机的数据库最终成为瓶颈。静态文件大幅度访问影响
5
第四次演进:数据库读写分离


把数据库划分为读库和写库,读库可以有多个,通过同步机制把写库的数据同步到读库,使用数据库中间件(Mycat),通过它来组织数据库的分离读写。php用mysqlnd或者在代码里来个读写分离,读写分离如何做呢,就是在执行sql前,匹配是delete,insert,update,replace操作就读主库,select等就读从库瓶颈:业务逐渐变多,不同业务之间的访问量差距较大,不同业务直接竞争数据库,相互影响性能。
6
第五次演进:数据库按业务分库


把不同的业务数据保存到不同的数据库中,业务之间的资源竞争降低,访问量大的业务可以部署更多的服务器。用户中心一般是首先需要独立出来的,对应的应用也可以独立部署瓶颈:用户数量增长,单机的写库会逐渐达到性能瓶颈。
7
第六次演进:把数据库的大表拆成小表


比如针对订单生成,可以按照月份,甚至小时创建表。到底是1000万还是2000万数据分表的时候,这个需要看你这张表字段多少,和每个字段存储的量和你机器的配置,一般存一两千万没什么问题,跑得呼呼的;分表分库方法分几种,可以参考《不用找了,大厂在用的分库分表方案,都在这了!》这种做法可以称作分布式数据库,但逻辑上仍然是一个完整的数据库整体,现在有一种架构叫MPP(大规模并行处理),针对于各种这类问题的使用场景。分表分库倒是分好了,你查询写入和数据合并的时候麻烦了,常用的分表分库中间件有如下几种,各有优缺点,一般大厂都是自己根据业务场景独立开发的,
百度用的Heisenberg,https://github.com/brucexx/heisenberg(开源版已停止维护)
支撑天猫淘宝支付宝那么大流量也是人肉手工分表分库吗?没有,阿里大多数场景用了他们自己的oceanbase数据库,oceanbase数据库也是有一层自己的OBproxy代理的,只是没有这层代理oceanbase也能跑得不错的;可以发现要深入的做架构,需要对相关组件能根据业务做相应的改造,这个时候linux系统,c语言,数据结构就变得非常重要了,而人的精力有限,样样都精不可能,这个时候你就需要团队协作了,深入研究一个点儿,其他地方也会发现差不多。瓶颈:数据库和tomcat都可以大规模水平扩展,最终单机的nginx会成为瓶颈。
8
第七次演进:使用LVS让多个Nginx负载均衡LVS是软件,运行在操作系统内核态,可以对TCP请求或高层网络协议进行转发,分发的性能远高于Nginx,大概十几万。F5是硬件,跟LVS做的事情类似,但性能更高,而且价格昂贵。


这种方式,用户可达千万或上亿级别。瓶颈:LVS达到瓶颈,或用户分布在不同的地区,与服务器机房的距离不同,导致访问延迟的不同。
9
第八次演进:智能DNS轮询实现机房间的负载均衡


别说,DNSPOD还挺好用的,不同机房ip智能解析,响应速度也不错,当然还有收费版的,功能更多(这不是腾讯的广告
)在DNS中配置一个域名对应多个IP。每个IP地址对应到不同的机房里的虚拟IP。做到机房级别的水平扩展,已经是从请求并发量来讲,过亿的处理方案,十几亿或几十亿的并发,暂时没人考虑。瓶颈:检索,分析的需求越来越多,单靠数据库无法解决各种各样的需求。
10
第九次演进:引入NoSQL数据库和搜索引擎


数据量多到一定规模的时候,关系型数据库不再适用,而且数据量较大的查询,MySQL不一定能运行出结果,对于海量数据的处理,通过分布式文件系统HDFS来存储,对于key-value类型的数据,通过HBase、Redis等处理,对于抓取查询,可以通过ES等解决,对于多维分析,通过kylin、Druid等解决。阿里云上有各种时序数据库,图数据库,表格数据库,还有HybridDB for MySQL,支持OLTP和OLAP,tidb也干这活儿,是个不错的选择,也是近年以来发展不错的数据库厂商,有大厂给你当靠山,亿级流量就变得比以前更容易了引入更多的组件同时必然极大的提高系统复杂度,不同的组件的数据还需要同步,需要考虑一致性问题。
11
第十次演进:数据两地三中心
一般来说目前比较流行的灾备体系是至少建设三个数据中心,其中:
  • 主中心:正常情况下全面提供业务服务。
  • 同城中心:一般使用同步复制的方式来向同城灾备中心传输数据,保证同城中心数据复本为最新,随时可以接管业务,以保证RTO的指标。但是同城中心无法应对此类删库事件。
  • 异地中心:一般使用延时异步复制(延时时间一般为30分钟左右)的方式向异地灾备中心传输数据,其中同步复制的好处是一旦主中心被人工破坏,那么不会立刻涉及异地中心以保证RPO的指标。
 


一句话总结灾备体系的最佳实践就是两地三中心;同城保证业务连续性,优先负责用户体验;异地保证数据连续性,确保企业生存底线。而针对行为及日志等重要性等级不高的数据,一般采用异地磁带备份的方式。具体方式如下:


解决方案有商用的也有开源的,商用的华为SAN 3DC方案的不错,mysql开源的可以尝试一下阿里的canal:跨地域 MySQL binlog 增量订阅&消费组件
不过从目前情况看不少企业尤其是创业型企业,都没有百年老店的观念,因此在异地中心的建设上投入还不够,不过这样的模式缺点也很明显,一旦发生这种删库事件就影响就是致命的。
12
第十一次演进:大应用拆为小应用


按照业务板块来划分应用,使单个应用的职责更清晰,相互之间可以做到独立升级迭代,甚至可以做到部分功能关闭。瓶颈:不同应用之间,存在公用的模块,导致包含公共功能的部分在升级时,全部相关代码都要跟着升级。
13
第十一次演进:微服务、service mesh
“微服务架构是一种架构模式,它提倡将单一应用程序划分成一组小的服务,服务之间相互协调、互相配合,为用户提供最终价值。每个服务运行在其独立的进程中,服务和服务之间采用轻量级的通信机制相互沟通(通常是基于HTTP的Restful API).每个服务都围绕着具体的业务进行构建,并且能够被独立的部署到生产环境、类生产环境等。(知乎华为云技术宅基地)
微服务架构:
类似于我们现在写的SpringCloud的项目结构,尤其是当用户,支付等功能在多个应用中都存在,抽离出来效率更高;不要以为把服务独立出来,独立部署,能注册发现就是微服务了,服务治理,限流降级,权限管理,链路追踪也是微服务体系里比较重要的环节现在想实现这种效果,可以用现成的框架,spring cloud ,dubbo都是不错的选择,微服务要再升级就上docker,管理docker又需要k8s另外一种趋势就是Service Mesh,Service Mesh架构图:
目前流行的 Service Mesh 开源软件有 Linkerd、Envoy 和 Istio,而 Buoyant(开源 Linkerd 的公司)又发布了基于 Kubernetes 的 Service Mesh 开源项目 Conduit。
Service Mesh 开源项目简介:
  • Linkerd(https://github.com/linkerd/linkerd):第一代 Service Mesh,2016 年 1 月 15 日首发布,业界第一个 Service Mesh 项目,由 Buoyant 创业小公司开发(前 Twitter 工程师),2017 年 7 月 11 日,宣布和 Istio 集成,成为 Istio 的数据面板。
  • Envoy(https://github.com/envoyproxy/envoy):第一代 Service Mesh,2016 年 9 月 13 日首发布,由 Matt Klein 个人开发(Lyft 工程师),之后默默发展,版本较稳定。
  • Istio(https://github.com/istio/istio):第二代 Service Mesh,2017 年 5 月 24 日首发布,由 Google、IBM 和 Lyft 联合开发,只支持 Kubernetes 平台,2017 年 11 月 30 日发布 0.3 版本,开始支持非 Kubernetes 平台,之后稳定的开发和发布。
  • Conduit(https://github.com/runconduit/conduit):第二代 Service Mesh,2017 年 12 月 5 日首发布,由 Buoyant 公司开发(借鉴 Istio 整体架构,部分进行了优化),对抗 Istio 压力山大,也期待 Buoyant 公司的毅力。
  • nginMesh(https://github.com/nginmesh/nginmesh):2017 年 9 月首发布,由 Nginx 开发,定位是作为 Istio 的服务代理,也就是替代 Envoy,思路跟 Linkerd 之前和 Istio 集成很相似,极度低调,GitHub 上的 star 也只有不到 100。
  • Kong(https://github.com/Kong/kong):比 nginMesh 更加低调,默默发展中。
关于公众号“肉眼品世界”回复“servicemesh”,可以下载一份精基于k8s,Istio实践service mesh 的58页精美ppt再往下演进,ESB服务总线做接口管理,容器化技术实现运行环境隔离,云平台承载大型系统云平台中的几个概念:IaaS:基础设施即服务,可申请硬件资源的层面。PaaS:平台即服务,提供云平台常用的技术组件的开发和维护。SaaS:软件即服务,开发好一个应用,部署之后,按照功能或要求付费。思考总结一、高并发架构的实质通过充分使用内存的方式可以获取数据交换的速度,更多的饱和使用cpu在网络编程上充分使用异步通知的方法充分利用基于linux socket io(你会发觉好多地方都这么干的)通过高效的算法和数据结构提供软件层的基础数据库和分布式管理单机达到cpu饱和运转极限,然后就是以一定策略加机器,omg,多少并发都能抗住,当然现在还有云..二、架构调整是否必须按照上面的路线演变?不一定!!!上面讲的是电商方向,已有的演化线路。三、对于即将实施的系统,架构要设计到什么程度?够用就行,问清楚什么叫够用,设计要满足下一阶段用户量和性能指标的要求。四、我想做架构师有什么要求?主要是决策权衡力,技术只是思维的外在体现形式;不一定样样都会,但要有很强的学习能力,基础知识扎实很重要;还要能经得起喷,快速学习改进的心态;会用相关组件做架构是架构,了解每个组件和操作系统的一些基础知识很重要,大多数方法论都是建立在这基础上的,小编也是好多不懂在不断学习的五、都云了,做技术还有前途吗?方案都大同小异,大家都知道了,做技术还有前途吗?当然有,随着国家的发展科技创新占越来越重的比例,虽然有成熟框架和云,但是基于巨人肩膀上的创新是会层出不穷的,也会在细节点上不断深入的;就像微服务体系一样,看上去是技术,其实也是产品,这个产品解决人多了,业务多了开发难度的问题,通过注册中心、容器、服务治理、网关、限流形成一个技术产品体系,解决一些开发部署的问题,从本质上看是有这个产品思维体系才诞生了这些技术组件,所以技术更高维度的思考是需要考虑用什么样的产品解决什么样的问题,然后用自己所掌握的技术来实现,就插上了一双新的翅膀码字不易,打字太快有不完善的地方欢迎留言讨论,觉得不错点赞和转发,这同时推荐我们的另一篇文章,相信会有收获《不是你需要中台,而是一名合格的架构师(附各大厂中台建设PPT)》关于肉眼品世界,公众号后台回复666,加入启明星技术社群,与阿里oceanbase数据库创始人面对面
参考资料:
【NIO基础篇】 匠心零度 https://www.cnblogs.com/jiangxinlingdu/p/8116265.html
【什么是微服务架构】java团长 华为云技术宅基地 https://www.zhihu.com/question/65502802
【1 分钟抗住 10 亿请求】CSDN 阿凡博客 https://blog.csdn.net/qq_43107323/article/details/104919132

转载请注明:码出未来-php,java,架构,职场 » 亿级(无限级)并发,没那么难

喜欢 ()or分享 (0)

最后编辑于:05-17发布:

留言与评论(共有 0 条评论)
   
验证码: