怎么理解并发多进程服务和多线程服务器

怎么理解并发多进程服务和多线程服务器,第1张

1,进程:子进程是父进程的复制品。子进程获得父进程数据空间、堆和栈的复制品。
2,线程:相对与进程而言,线程是一个更加接近与执行体的概念,它可以与同进程的其他线程共享数据,但拥有自己的栈空间,拥有独立的执行序列。
两者都可以提高程序的并发度,提高程序运行效率和响应时间。
线程和进程在使用上各有优缺点:线程执行开销小,但不利于资源管理和保护;而进程正相反。同时,线程适合于在SMP机器上运行,而进程则可以跨机器迁移。
答案二:
根本区别就一点:用多进程每个进程有自己的地址空间(address space),线程则共享地址空间。所有其它区别都是由此而来的:
1。速度:线程产生的速度快,线程间的通讯快、切换快等,因为他们在同一个地址空间内。
2。资源利用率:线程的资源利用率比较好也是因为他们在同一个地址空间内。
3。同步问题:线程使用公共变量/内存时需要使用同步机制还是因为他们在同一个地址空间内。
网上的答案的 版本怎么想怎么都太学术了。我当时看到过一个比喻特别的好, 我就模仿者把它说下来哈,有错误希望支持哈:
多进程的服务器就好比是
立体的交通系统(立交桥)虽然说建造的时候花费比较大,消耗的资源比较多,但是真要是跑起来不会交通堵塞。但是汽车在上面跑,相互通信就是个很费事儿问题(进程间通信比较麻烦);多线程就好比是平面的交通系统,造价低,但是很容易交通堵塞,
但是也有好处同步的时候方便。
在网络服务器方面:
单进程 < 多进程(单线程)< 多进程(多线程)
在游戏方面的应用:
I、多线程服务器,玩家数据缓存和向DB的存储我们可以开一个线程单独去做,这样不会有什么大的问题。日志和网络上面说过可以很容易切割出去,主要就是对游戏逻辑的切割。
A:按场景分线程,一个线程管理若干个场景。这样配置灵活,一个线程可以管理若干个小场影,除非有个场景人多到一个CPU跑不下来,一般的游戏都会满足需求。缺点则是不在同一线程的Object在做逻辑交互时,必须用异步,如果用到了脚本,那么这里的复杂度和性能要值得注意。如果项目中出现单个服务器解决不鸟的问题(例如战场服务器),似乎就成了多线程多进程的庞大架构。
B:将某些功能切割到其它线程,例如Object的管理和查找,NPCAI的寻路,这种方式貌似在做逻辑需要分离到别的线程模块功能时有点麻烦,如果直接上锁等待肯定不是最好的方式,所以这些逻辑必须变成异步。
2、多进程服务器,其实这里的多进程和场景多线程改成了多进程。这里玩家数据缓存和向DB的存储我觉得用一个单独的DB服务器。多进程服务器可以在GameServer和GameClient之间加一个Gate,因为在跨服场景不需频繁断线连接。多进程服务器所有的通讯都依靠网络,有些逻辑必须有网络延迟的消耗。优点是配置灵活,在物理机器性能不够时可以通过扩充物理机器来解决
服务器还有有一个很蛋疼的问题就是过载: 下面介绍一下产生的原因和解决办法:
服务器过载:
原因是高优先级处理阶段对CPU的不公平抢占。所以,如果限制高优先级处理阶段对CPU的占用率,或者限制处理高优先级的CPU个数,都可以减轻或者消除收包活锁现象。具体的可以采用以下的方法:
方法一、采用轮询机制
为了减少中断对系统性能的影响,在负载正常的情况下采用“下半处理”的方法就非常有效,而在高负荷情况下,采用这个方法仍然会造成活锁现象,这时可以采用轮询机制。虽然这个方法在负载正常的情况下会造成资源的浪费和响应速度降低,但在网络数据频繁到达服务器时就要比中断驱动技术有效的多。
方法二、减低中断的频率
这里主要有两种方法:批中断和暂时关闭中断。批中断可以在超载时有效的抑制活锁现象,但对服务器的性能没有什么根本性的改进;当系统出现接收活锁迹象时,可以采用暂时关闭中断的方法来缓和系统的负担,当系统缓存再次可用时可以再打开中断,但这种方法在接收缓存不够大的情况下会造成数据包丢失。
方法三、减少上下文切换
这种方法不管服务器在什么情况下对性能改善都很有效,这时可以采用引入核心级(kerne1—leve1)或硬件级数据流的方法来达到这个目的。核心级数据流是将数据从源通过系统总线进行转发而不需要使数据经过应用程序进程,这个过程中因为数据在内存中,因此需要CPU *** 作数据。
硬件级数据流则是将数据从源通过私有数据总线或是虽等DMA通过系统总线进行转发而不需要使数据经过应用程序进程,这个过程不需要CPU *** 作数据。这样在数据传输过程中不需要用户线程的介入,减少了数据被拷贝的次数,减少了上下文切换的开销。

本文主要包括tomcat服务器的目录结构、工作模式、整体架构、I/O模型以及NIO、NIO2、APR三者的对比介绍。

我们先来看一下tomcat85和tomcat9中的home目录中的文件:

可以看到除掉一些说明文件之后,还有7个目录:

实际上除了主目录里有lib目录,在webapps目录下的web应用中的WEB-INF目录下也存在一个lib目录:

两者的区别在于:

● Tomcat主目录下的lib目录:存放的JAR文件 不仅能被Tomcat访问,还能被所有在Tomcat中发布的Java Web应用访问
● webapps目录下的Java Web应用的lib目录:存放的JAR文件 只能被当前Java Web应用访问

既然有多个lib目录,那么肯定就有使用的优先顺序,Tomcat类加载器的目录加载优先顺序如下:

Tomcat的类加载器负责为Tomcat本身以及Java Web应用加载相关的类。假如Tomcat的类加载器要为一个Java Web应用加载一个类,类加载器会按照以下优先顺序到各个目录中去查找该类的class文件,直到找到为止,如果所有目录中都不存在该类的class文件,则会抛出异常:

Tomcat不仅可以单独运行,还可以与其他的Web服务器集成,作为其他Web服务器的进程内或进程外的servlet容器。集成的意义在于:对于不支持运行Java Servlet的其他Web服务器,可通过集成Tomcat来提供运行Servlet的功能。

Tomcat有三种工作模式:

我们先从tomcat的源码目录来分析一下tomcat的整体架构,前面我们配置jsvc运行tomcat的时候,我们知道tomcat中启动运行的最主要的类是 orgapachecatalinastartupBootstrap ,那么我们在tomcat的源码中的java目录下的org目录的apache目录可以找到主要的源码的相对应的类。

图中的目录如果画成架构图,可以这样表示:

Tomcat 本质上就是一款Servlet 容器,因此 catalina 才是Tomcat的核心 ,其他模块都是为 catalina 提供支撑的。

单线程阻塞I/O模型是最简单的一种服务器I/O模型,单线程即同时只能处理一个客户端的请求,阻塞即该线程会一直等待,直到处理完成为止。对于多个客户端访问,必须要等到前一个客户端访问结束才能进行下一个访问的处理,请求一个一个排队,只提供一问一答服务。

如上图所示:这是一个同步阻塞服务器响应客户端访问的时间节点图。

这种模型的特点在于单线程和阻塞I/O。 单线程即服务器端只有一个线程处理客户端的所有请求,客户端连接与服务器端的处理线程比是 n:1 ,它无法同时处理多个连接,只能串行处理连接。而阻塞I/O是指服务器在读写数据时是阻塞的,读取客户端数据时要等待客户端发送数据并且把 *** 作系统内核复制到用户进程中,这时才解除阻塞状态。写数据回客户端时要等待用户进程将数据写入内核并发送到客户端后才解除阻塞状态。 这种阻塞带来了一个问题,服务器必须要等到客户端成功接收才能继续往下处理另外一个客户端的请求,在此期间线程将无法响应任何客户端请求。

该模型的特点:它是最简单的服务器模型,整个运行过程都只有一个线程,只能支持同时处理一个客户端的请求(如果有多个客户端访问,就必须排队等待), 服务器系统资源消耗较小,但并发能力低,容错能力差。

多线程阻塞I/O模型在单线程阻塞I/O模型的基础上对其进行改进,加入多线程,提高并发能力,使其能够同时对多个客户端进行响应,多线程的核心就是利用多线程机制为每个客户端分配一个线程。

如上图所示,服务器端开始监听客户端的访问,假如有两个客户端同时发送请求过来,服务器端在接收到客户端请求后分别创建两个线程对它们进行处理,每条线程负责一个客户端连接,直到响应完成。 期间两个线程并发地为各自对应的客户端处理请求 ,包括读取客户端数据、处理客户端数据、写数据回客户端等 *** 作。

这种模型的I/O *** 作也是阻塞的 ,因为每个线程执行到读取或写入 *** 作时都将进入阻塞状态,直到读取到客户端的数据或数据成功写入客户端后才解除阻塞状态。尽管I/O *** 作阻塞,但这种模式比单线程处理的性能明显高了,它不用等到第一个请求处理完才处理第二个,而是并发地处理客户端请求,客户端连接与服务器端处理线程的比例是 1:1 。

多线程阻塞I/O模型的特点:支持对多个客户端并发响应,处理能力得到大幅提高,有较大的并发量,但服务器系统资源消耗量较大,而且如果线程数过多,多线程之间会产生较大的线程切换成本,同时拥有较复杂的结构。

在探讨单线程非阻塞I/O模型前必须要先了解非阻塞情况下套接字事件的检测机制,因为对于单线程非阻塞模型最重要的事情是检测哪些连接有感兴趣的事件发生。一般会有如下三种检测方式。

当多个客户端向服务器请求时,服务器端会保存一个套接字连接列表中,应用层线程对套接字列表轮询尝试读取或写入。如果成功则进行处理,如果失败则下次继续。这样不管有多少个套接字连接,它们都可以被一个线程管理,这很好地利用了阻塞的时间,处理能力得到提升。

但这种模型需要在应用程序中遍历所有的套接字列表,同时需要处理数据的拼接,连接空闲时可能也会占用较多CPU资源,不适合实际使用。

这种方式将套接字的遍历工作交给了 *** 作系统内核,把对套接字遍历的结果组织成一系列的事件列表并返回应用层处理。对于应用层,它们需要处理的对象就是这些事件,这是一种事件驱动的非阻塞方式。

服务器端有多个客户端连接,应用层向内核请求读写事件列表。内核遍历所有套接字并生成对应的可读列表readList和可写列表writeList。readList和writeList则标明了每个套接字是否可读/可写。应用层遍历读写事件列表readList和writeList,做相应的读写 *** 作。

内核遍历套接字时已经不用在应用层对所有套接字进行遍历,将遍历工作下移到内核层,这种方式有助于提高检测效率。 然而,它需要将所有连接的可读事件列表和可写事件列表传到应用层,假如套接字连接数量变大,列表从内核复制到应用层也是不小的开销。 另外,当活跃连接较少时, 内核与应用层之间存在很多无效的数据副本 ,因为它将活跃和不活跃的连接状态都复制到应用层中。

通过遍历的方式检测套接字是否可读可写是一种效率比较低的方式,不管是在应用层中遍历还是在内核中遍历。所以需要另外一种机制来优化遍历的方式,那就是 回调函数 。内核中的套接字都对应一个回调函数,当客户端往套接字发送数据时,内核从网卡接收数据后就会调用回调函数,在回调函数中维护事件列表,应用层获取此事件列表即可得到所有感兴趣的事件。

内核基于回调的事件检测方式有两种

第一种是用 可读列表readList 和 可写列表writeList 标记读写事件, 套接字的数量与 readList 和 writeList 两个列表的长度一样

上面两种方式由 *** 作系统内核维护客户端的所有连接并通过回调函数不断更新事件列表,而应用层线程只要遍历这些事件列表即可知道可读取或可写入的连接,进而对这些连接进行读写 *** 作,极大提高了检测效率,自然处理能力也更强。

单线程非阻塞I/O模型最重要的一个特点是,在调用读取或写入接口后立即返回,而不会进入阻塞状态。虽然只有一个线程,但是它通过把非阻塞读写 *** 作与上面几种检测机制配合就可以实现对多个连接的及时处理,而不会因为某个连接的阻塞 *** 作导致其他连接无法处理。在客户端连接大多数都保持活跃的情况下,这个线程会一直循环处理这些连接,它很好地利用了阻塞的时间,大大提高了这个线程的执行效率。

单线程非阻塞I/O模型的主要优势体现在对多个连接的管理,一般在同时需要处理多个连接的发场景中会使用非阻塞NIO模式,此模型下只通过一个线程去维护和处理连接,这样大大提高了机器的效率。一般服务器端才会使用NIO模式,而对于客户端,出于方便及习惯,可使用阻塞模式的套接字进行通信。

在多核的机器上可以通过多线程继续提高机器效率。最朴实、最自然的做法就是将客户端连接按组分配给若干线程,每个线程负责处理对应组内的连接。比如有4个客户端访问服务器,服务器将套接字1和套接字2交由线程1管理,而线程2则管理套接字3和套接字4,通过事件检测及非阻塞读写就可以让每个线程都能高效处理。

多线程非阻塞I/O模式让服务器端处理能力得到很大提高,它充分利用机器的CPU,适合用于处理高并发的场景,但它也让程序更复杂,更容易出现问题(死锁、数据不一致等经典并发问题)。

最经典的多线程非阻塞I/O模型方式是Reactor模式。首先看单线程下的Reactor,Reactor将服务器端的整个处理过程分成若干个事件,例如分为接收事件、读事件、写事件、执行事件等。Reactor通过事件检测机制将这些事件分发给不同处理器去处理。在整个过程中只要有待处理的事件存在,即可以让Reactor线程不断往下执行,而不会阻塞在某处,所以处理效率很高。

基于单线程Reactor模型,根据实际使用场景,把它改进成多线程模式。常见的有两种方式:一种是在耗时的process处理器中引入多线程,如使用线程池;另一种是直接使用多个Reactor实例,每个Reactor实例对应一个线程。

Reactor模式的一种改进方式如下图所示。其整体结构基本上与单线程的Reactor类似,只是引入了一个线程池。由于对连接的接收、对数据的读取和对数据的写入等 *** 作基本上都耗时较少,因此把它们都放到Reactor线程中处理。然而,对于逻辑处理可能比较耗时的工作,可以在process处理器中引入线程池,process处理器自己不执行任务,而是交给线程池,从而在Reactor线程中避免了耗时的 *** 作。将耗时的 *** 作转移到线程池中后,尽管Reactor只有一个线程,它也能保证Reactor的高效。

Reactor模式的另一种改进方式如下图所示。其中有多个Reactor实例,每个Reactor实例对应一个线程。因为接收事件是相对于服务器端而言的,所以客户端的连接接收工作统一由一个accept处理器负责,accept处理器会将接收的客户端连接均匀分配给所有Reactor实例,每个Reactor实例负责处理分配到该Reactor上的客户端连接,包括连接的读数据、写数据和逻辑处理。这就是多Reactor实例的原理。

Tomcat支持的I/O模型如下表(自85/90 版本起,Tomcat移除了对BIO的支持),在 80 之前 , Tomcat 默认采用的I/O方式为 BIO , 之后改为 NIO。 无论 NIO、NIO2 还是 APR, 在性能方面均优于以往的BIO。

Tomcat中的NIO模型是使用的JAVA的NIO类库,其内部的IO实现是同步的(也就是在用户态和内核态之间的数据交换上是同步机制),采用基于selector实现的异步事件驱动机制(这里的异步指的是selector这个实现模型是使用的异步机制)。 而对于Java来说,非阻塞I/O的实现完全是基于 *** 作系统内核的非阻塞I/O,它将 *** 作系统的非阻塞I/O的差异屏蔽并提供统一的API,让我们不必关心 *** 作系统。JDK会帮我们选择非阻塞I/O的实现方式。

NIO2和前者相比的最大不同就在于引入了异步通道来实现异步IO *** 作,因此也叫AIO(Asynchronous I/O)。NIO2 的异步通道 APIs 提供方便的、平台独立的执行异步 *** 作的标准方法。这使得应用程序开发人员能够以更清晰的方式来编写程序,而不必定义自己的 Java 线程,此外,还可通过使用底层 OS 所支持的异步功能来提高性能。如同其他 Java API 一样,API 可利用的 OS 自有异步功能的数量取决于其对该平台的支持程度。

异步通道提供支持连接、读取、以及写入之类非锁定 *** 作的连接,并提供对已启动 *** 作的控制机制。Java 7 中用于 Java Platform(NIO2)的 More New I/O APIs,通过在 javaniochannels 包中增加四个异步通道类,从而增强了 Java 14 中的 New I/O APIs(NIO),这些类在风格上与 NIO 通道 API 很相似。他们共享相同的方法与参数结构体,并且大多数对于 NIO 通道类可用的参数,对于新的异步版本仍然可用。主要区别在于新通道可使一些 *** 作异步执行。

异步通道 API 提供两种对已启动异步 *** 作的监测与控制机制。第一种是通过返回一个 javautilconcurrentFuture 对象来实现,它将会建模一个挂起 *** 作,并可用于查询其状态以及获取结果。第二种是通过传递给 *** 作一个新类的对象, javaniochannelsCompletionHandler ,来完成,它会定义在 *** 作完毕后所执行的处理程序方法。每个异步通道类为每个 *** 作定义 API 副本,这样可采用任一机制。

Apache可移植运行时(Apache Portable Runtime,APR) 是Apache >有多个 CPU 可用。单核机器上多线程的优势不明显。
线程间有共享数据。如果没有共享数据,用模型 3b 就行。虽然我们应该把线程间的共享数据降到最低,但不代表没有;
共享的数据是可以修改的,而不是静态的常量表。如果数据不能修改,那么可以在进程间用 shared memory,模式 3 就能胜任;
提供非均质的服务。即,事件的响应有优先级差异,我们可以用专门的线程来处理优先级高的事件。防止优先级反转;
latency 和 throughput 同样重要,不是逻辑简单的 IO bound 或 CPU bound 程序;
利用异步 *** 作。比如 logging。无论往磁盘写 log file,还是往 log server 发送消息都不应该阻塞 critical path;
能 scale up。一个好的多线程程序应该能享受增加 CPU 数目带来的好处,目前主流是 8 核,很快就会用到 16 核的机器了。
具有可预测的性能。随着负载增加,性能缓慢下降,超过某个临界点之后急速下降。线程数目一般不随负载变化。
多线程能有效地划分责任与功能,让每个线程的逻辑比较简单,任务单一,便于编码。而不是把所有逻辑都塞到一个 event loop 里,就像 Win32 SDK 程序那样。

你至少说一下你用什么语言吧?给你个伪代码,C、C++,java或其他语言的处理过程基本相似:
//创建服务端Socket,指定地址族,连接地址,传输协议
ServerSocket servSocket = new ServerSocket (AF_INET,"127001",STREAM);
//指定服务端启用端口
bind(servSocket ,port);
//把服务端socket转化为监听socket
listene(servSocket );
//监听客户端的请求
for(;;) {
ClientSocket cliSocket =new ClientSocket ();
//此处没有连接请求的时候会产生阻塞,会把主线程挂起,有连接请求由 *** 作系统或运行环境通知主线程,继续处理
accept(servSocket,cliSocket);
//执行到此处说明有客户端请求,创建线程处理客户端请求,此处耗费的时间仅仅是各语言的线程创建时间,不处理任何其他工作,具体工作写到线程回调代码中执行。
createNewThreadHandleClientRequet(cliSocket);
//到此循环回去,等待下一次客户端请求
}

想设计亿万级高并发架构,你要先知道高并发是什么?

面对流量高峰,不同的企业是如何通过技术手段解决高并发难题的呢

0、引言

软件系统有三个追求:高性能、高并发、高可用,俗称三高。三者既有区别也有联系,门门道道很多,全面讨论需要三天三夜,本篇讨论高并发。

高并发(High Concurrency)。并发是 *** 作系统领域的一个概念,指的是一段时间内多任务流交替执行的现象,后来这个概念被泛化,高并发用来指大流量、高请求的业务情景,比如春运抢票,电商双十一,秒杀大促等场景。

很多程序员每天忙着搬砖,平时接触不到高并发,哪天受不了跑去面试,还常常会被面试官犀利的高并发问题直接KO,其实吧,高并发系统也不高深,我保证任何一个智商在线的看过这篇文章后,都能战胜恐惧,重拾生活的信心。

本文先介绍高并发系统的度量指标,然后讲述高并发系统的设计思路,再梳理高并发的关键技术,最后结合作者的经验做一些延伸探讨。

1、高并发的度量指标

既然是高并发系统,那并发一定要高,不然就名不副实。并发的指标一般有QPS、TPS、IOPS,这几个指标都是可归为系统吞吐率,QPS越高系统能hold住的请求数越多,但光关注这几个指标不够,我们还需要关注RT,即响应时间,也就是从发出request到收到response的时延,这个指标跟吞吐往往是此消彼长的,我们追求的是一定时延下的高吞吐。

比如有100万次请求,99万次请求都在10毫秒内响应,其他次数10秒才响应,平均时延不高,但时延高的用户受不了,所以,就有了TP90/TP99指标,这个指标不是求平均,而是把时延从小到大排序,取排名90%/99%的时延,这个指标越大,对慢请求越敏感。

除此之外,有时候,我们也会关注可用性指标,这可归到稳定性。

一般而言,用户感知友好的高并发系统,时延应该控制在250毫秒以内。

什么样的系统才能称为高并发?这个不好回答,因为它取决于系统或者业务的类型。不过我可以告诉你一些众所周知的指标,这样能帮助你下次在跟人扯淡的时候稍微靠点儿谱,不至于贻笑大方。

通常,数据库单机每秒也就能抗住几千这个量级,而做逻辑处理的服务单台每秒抗几万、甚至几十万都有可能,而消息队列等中间件单机每秒处理个几万没问题,所以我们经常听到每秒处理数百万、数千万的消息中间件集群,而像阿某的API网关,每日百亿请求也有可能。

2、高并发的设计思路

高并发的设计思路有两个方向:

垂直方向扩展,也叫竖向扩展

水平方向扩展,也叫横向扩展

垂直方向:提升单机能力

提升单机处理能力又可分为硬件和软件两个方面:

硬件方向,很好理解,花钱升级机器,更多核更高主频更大存储空间更多带宽

软件方向,包括用各快的数据结构,改进架构,应用多线程、协程,以及上性能优化各种手段,但这玩意儿天花板低,就像提升个人产出一样,996、007、最多24 X 7。

水平方向:分布式集群

为了解决分布式系统的复杂性问题,一般会用到架构分层和服务拆分,通过分层做隔离,通过微服务解耦。

这个理论上没有上限,只要做好层次和服务划分,加机器扩容就能满足需求,但实际上并非如此,一方面分布式会增加系统复杂性,另一方面集群规模上去之后,也会引入一堆AIOps、服务发现、服务治理的新问题。

因为垂直向的限制,所以,我们通常更关注水平扩展,高并发系统的实施也主要围绕水平方向展开。

3、高并发的关键技术

玩具式的网络服务程序,用户可以直连服务器,甚至不需要数据库,直接写磁盘文件。但春运购票系统显然不能这么做,它肯定扛不住这个压力,那一般的高并发系统是怎么做呢?比如某宝这样的正经系统是怎么处理高并发的呢?

其实大的思路都差不多,层次划分 + 功能划分。可以把层次划分理解为水平方向的划分,而功能划分理解为垂直方向的划分。

首先,用户不能直连服务器,要做分布式就要解决“分”的问题,有多个服务实例就需要做负载均衡,有不同服务类型就需要服务发现。

集群化:负载均衡

负载均衡就是把负载(request)均衡分配到不同的服务实例,利用集群的能力去对抗高并发,负载均衡是服务集群化的实施要素,它分3种:

DNS负载均衡,客户端通过URL发起网络服务请求的时候,会去DNS服务器做域名解释,DNS会按一定的策略(比如就近策略)把URL转换成IP地址,同一个URL会被解释成不同的IP地址,这便是DNS负载均衡,它是一种粗粒度的负载均衡,它只用URL前半部分,因为DNS负载均衡一般采用就近原则,所以通常能降低时延,但DNS有cache,所以也会更新不及时的问题。

硬件负载均衡,通过布置特殊的负载均衡设备到机房做负载均衡,比如F5,这种设备贵,性能高,可以支撑每秒百万并发,还能做一些安全防护,比如防火墙。

软件负载均衡,根据工作在ISO 7层网络模型的层次,可分为四层负载均衡(比如章文嵩博士的LVS)和七层负载均衡(NGINX),软件负载均衡配置灵活,扩展性强,阿某云的SLB作为服务对外售卖,Nginx可以对URL的后半部做解释承担API网关的职责。

所以,完整的负载均衡链路是 client <-> DNS负载均衡 -> F5 -> LVS/SLB -> NGINX

不管选择哪种LB策略,或者组合LB策略,逻辑上,我们都可以视为负载均衡层,通过添加负载均衡层,我们将负载均匀分散到了后面的服务集群,具备基础的高并发能力,但这只是万里长征第一步。

数据库层面:分库分表+读写分离

前面通过负载均衡解决了无状态服务的水平扩展问题,但我们的系统不全是无状态的,后面通常还有有状态的数据库,所以解决了前面的问题,存储有可能成为系统的瓶颈,我们需要对有状态存储做分片路由。

数据库的单机QPS一般不高,也就几千,显然满足不了高并发的要求。

所以,我们需要做分库分表 + 读写分离。

就是把一个库分成多个库,部署在多个数据库服务上,主库承载写请求,从库承载读请求。从库可以挂载多个,因为很多场景写的请求远少于读的请求,这样就把对单个库的压力降下来了。

如果写的请求上升就继续分库分表,如果读的请求上升就挂更多的从库,但数据库天生不是很适合高并发,而且数据库对机器配置的要求一般很高,导致单位服务成本高,所以,这样加机器抗压力成本太高,还得另外想招。

读多写少:缓存

缓存的理论依据是局部性原理。

一般系统的写入请求远少于读请求,针对写少读多的场景,很适合引入缓存集群。

在写数据库的时候同时写一份数据到缓存集群里,然后用缓存集群来承载大部分的读请求,因为缓存集群很容易做到高性能,所以,这样的话,通过缓存集群,就可以用更少的机器资源承载更高的并发。

缓存的命中率一般能做到很高,而且速度很快,处理能力也强(单机很容易做到几万并发),是理想的解决方案。

CDN本质上就是缓存,被用户大量访问的静态资源缓存在CDN中是目前的通用做法。

缓存也有很多需要谨慎处理的问题:

一致性问题:(a)更新db成功+更新cache失败 -> 不一致 (b)更新db失败+更新cache成功 -> 不一致 ©更新db成功+淘汰缓存失败 -> 不一致

缓存穿透:查询一定不存在的数据,会穿透缓存直接压到数据库,从而导致缓存失去作用,如果有人利用这个漏洞,大量查询一定不存在的数据,会对数据库造成压力,甚至打挂数据库。解决方案:布隆过滤器 或者 简单的方案,查询不存在的key,也把空结果写入缓存(设置较短的过期淘汰时间),从而降低命失

缓存雪崩:如果大量缓存在一个时刻同时失效,则请求会转到DB,则对DB形成压迫,导致雪崩。简单的解决方案是为缓存失效时间添加随机值,降低同一时间点失效淘汰缓存数,避免集体失效事件发生

但缓存是针对读,如果写的压力很大,怎么办?

高写入:消息中间件

同理,通过跟主库加机器,耗费的机器资源是很大的,这个就是数据库系统的特点所决定的。

相同的资源下,数据库系统太重太复杂,所以并发承载能力就在几千/s的量级,所以此时你需要引入别的一些技术。

比如说消息中间件技术,也就是MQ集群,它是非常好的做写请求异步化处理,实现削峰填谷的效果。

消息队列能做解耦,在只需要最终一致性的场景下,很适合用来配合做流控。

假如说,每秒是1万次写请求,其中比如5千次请求是必须请求过来立马写入数据库中的,但是另外5千次写请求是可以允许异步化等待个几十秒,甚至几分钟后才落入数据库内的。

那么此时完全可以引入消息中间件集群,把允许异步化的每秒5千次请求写入MQ,然后基于MQ做一个削峰填谷。比如就以平稳的1000/s的速度消费出来然后落入数据库中即可,此时就会大幅度降低数据库的写入压力。

业界有很多著名的消息中间件,比如ZeroMQ,rabbitMQ,kafka等。

消息队列本身也跟缓存系统一样,可以用很少的资源支撑很高的并发请求,用它来支撑部分允许异步化的高并发写入是很合适的,比使用数据库直接支撑那部分高并发请求要减少很多的机器使用量。

避免挤兑:流控

再强大的系统,也怕流量短事件内集中爆发,就像银行怕挤兑一样,所以,高并发另一个必不可少的模块就是流控。

流控的关键是流控算法,有4种常见的流控算法。

计数器算法(固定窗口):计数器算法是使用计数器在周期内累加访问次数,当达到设定的限流值时,触发限流策略,下一个周期开始时,进行清零,重新计数,实现简单。计数器算法方式限流对于周期比较长的限流,存在很大的弊端,有严重的临界问题。

滑动窗口算法:将时间周期分为N个小周期,分别记录每个小周期内访问次数,并且根据时间滑动删除过期的小周期,当滑动窗口的格子划分的越多,那么滑动窗口的滚动就越平滑,限流的统计就会越精确。此算法可以很好的解决固定窗口算法的临界问题。

漏桶算法:访问请求到达时直接放入漏桶,如当前容量已达到上限(限流值),则进行丢弃(触发限流策略)。漏桶以固定的速率进行释放访问请求(即请求通过),直到漏桶为空。分布式环境下实施难度高。

令牌桶算法:程序以r(r=时间周期/限流值)的速度向令牌桶中增加令牌,直到令牌桶满,请求到达时向令牌桶请求令牌,如获取到令牌则通过请求,否则触发限流策略。分布式环境下实施难度高。

4、高并发的实践经验

接入-逻辑-存储是经典的互联网后端分层,但随着业务规模的提高,逻辑层的复杂度也上升了,所以,针对逻辑层的架构设计也出现很多新的技术和思路,常见的做法包括系统拆分,微服务。

除此之外,也有很多业界的优秀实践,包括某信服务器通过协程(无侵入,已开源libco)改造,极大的提高了系统的并发度和稳定性,另外,缓存预热,预计算,批量读写(减少IO),池技术等也广泛应用在实践中,有效的提升了系统并发能力。

为了提升并发能力,逻辑后端对请求的处理,一般会用到生产者-消费者多线程模型,即I/O线程负责网络IO,协议编解码,网络字节流被解码后产生的协议对象,会被包装成task投入到task queue,然后worker线程会从该队列取出task执行,有些系统会用多进程而非多线程,通过共享存储,维护2个方向的shm queue,一个input q,一个output q,为了提高并发度,有时候会引入协程,协程是用户线程态的多执行流,它的切换成本更低,通常有更好的调度效率。

另外,构建漏斗型业务或者系统,从客户端请求到接入层,到逻辑层,到DB层,层层递减,过滤掉请求,Fail Fast(尽早发现尽早过滤),嘴大屁眼小,哈哈。

漏斗型系统不仅仅是一个技术模型,它也可以是一个产品思维,配合产品的用户分流,逻辑分离,可以构建全方位的立体模型。

5、小结

莫让浮云遮望眼,除去繁华识真颜。我们不能掌握了大方案,吹完了牛皮,而忽视了编程最本质的东西,掌握最基本最核心的编程能力,比如数据架构和算法,设计,惯用法,培养技术的审美,也是很重要的,既要致高远,又要尽精微。

硬件的内存模型物理机并发处理的方案对于jvm的内存模型实现,也有很大的参考作用,毕竟jvm也是在硬件层上来做事情,底层架构也决定了上层的建筑建模方式。
计算机并发并非只是多个处理器都参与进来计算就可以了,会牵扯到一些列硬件的问题,最直接的就是要和内存做交互。
但计算机的存储设备与处理器的预算速度相差太大,完全不能满足处理器的处理速度,怎么办,这就是后续加入的一层读写速度接近处理器运算速度的高速缓存来作为处理器和内存之间的缓冲。
高速缓存一边把使用的数据,从内存复制搬入,方便处理器快速运算,一边把运算后的数据,再同步到主内存中,如此处理器就无需等待了。
高速缓存虽然解决了处理器和内存的矛盾,但也为计算机带来了另一个问题:缓存一致性。
特别是当多个处理器都涉及到同一块主内存区域的时候,将可能会导致各自的缓存数据不一致。
那么出现不一致情况的时候,以谁的为准?为了解决这个问题,处理器和内存之间的读写的时候需要遵循一定的协议来 *** 作,这类协议有:MSI、MESI、MOSI、Synapse、Firefly以及DragonProtocol等。
这就是上图中处理器、高速缓存、以及内存之间的处理方式。
另外除了高速缓存之外,为了充分利用处理器,处理器还会把输入的指令码进行乱序执行优化,只要保证输出一致,输入的信息可以乱序执行重组,所以程序中的语句计算顺序和输入代码的顺序并非一致。
 JVM内存模型上面我们了解了硬件的内存模型,以此为借鉴,我们看看jvm的内存模型。
jvm定义的一套java内存模型为了能够跨平台达到一致的内存访问效果,从而屏蔽掉了各种硬件和 *** 作系统的内存访问差异。
这点和c和c++并不一样,C和C++会直接使用物理硬件和 *** 作系统的内存模型来处理,所以在各个平台上会有差异,这一点java不会。
java的内存模型规定了所有的变量都存储在主内存中,java课程>所谓并发服务器就是在同一个时刻可以处理来自多个客户端的请求;循环服务器是指服务器在同一时刻只可以响应一个客户端的请求。而且对于TCP和UDP套接字,这两种服务器的实现方式也有不同的特点。
1、TCP循环服务器:
首先TCP服务器接受一个客户端的连接请求,处理连接请求,在完成这个客户端的所有请求后断开连接,然后再接受下一个客户端的请求。创建TCP循环服务器的算法如下:
复制代码 代码如下:
socket(……); //创建一个TCP套接字
bind(……); //邦定公认的端口号
listen(……); //倾听客户端连接
while(1) //开始循环接收客户端连接
{
accept(……);//接收当前客户端的连接
while(1)
{ //处理当前客户端的请求
read(……);
process(……);
write(……);
}
close(……); //关闭当前客户端的连接,准备接收下一个客户端连接
}
TCP循环服务器一次只处理一个客户端的请求,如果有一个客户端占用服务器不放时,其它的客户机连接请求都得不到及时的响应。因此,TCP服务器一般很少用循环服务器模型的。
2、TCP并发服务器:
并发服务器的思想是每一个客户端的请求并不由服务器的主进程直接处理,而是服务器主进程创建一个子进程来处理。创建TCP并发服务器的算法如下:
复制代码 代码如下:
socket(……); //创建一个TCP套接字
bind(……); //邦定公认的端口号
listen(……);//倾听客户端连接
while(1) //开始循环接收客户端的接收
{
accept(……);//接收一个客户端的连接
if(fork(……)==0) //创建子进程
{
while(1)
{ //子进程处理某个客户端的连接
read(……);
process(……);
write(……);
}
close(……); //关闭子进程处理的客户端连接
exit(……) ;//终止该子进程
}
close(……); //父进程关闭连接套接字描述符,准备接收下一个客户端连接
}
TCP并发服务器可以解决TCP循环服务器客户端独占服务器的情况。但同时也带来了一个不小的问题,即响应客户机的请求,服务器要创建子进程来处理,而创建子进程是一种非常消耗资源的 *** 作。
3、UDP循环服务器:
UDP服务器每次从套接字上读取一个客户端的数据报请求,处理接收到的UDP数据报,然后将结果返回给客户机。创建UDP循环服务器的算法如下:
1 socket(……); //创建一个数据报类型的套接字 2 bind(……); //邦定公认的短口号 3 while(1) //开始接收客户端的连接 4 { //接收和处理客户端的UDP数据报 5 recvfrom(……); 6 process(……); 7 sendto(……);//准备接收下一个客户机的数据报 8 }
消除行号
因为UDP是非面向连接的,没有一个客户端可以独占服务器。只要处理过程不是死循环,服务器对于每一个客户机的请求总是能够处理的。
UDP循环服务器在数据报流量过大时由于处理任务繁重可能造成客户技数据报丢失,但是因为UDP协议本身不保证数据报可靠到达,所以UDP协议是允许丢失数据报的。
鉴于以上两点,一般的UDP服务器采用循环方式4、UDP并发服务器把并发的概念应用UDP就得到了并发UDP服务器,和并发TCP服务器模型一样是创建子进程来处理的。
创建UDP并发服务器的算法如下:
复制代码 代码如下:
socket(……); //创建一个数据报类型的套接字
bind(……); //邦定公认的短口号
while(1) //开始接收客户端的连接
{ //接收和处理客户端的UDP数据报
recvfrom(……);
if(fork(……)==0) //创建子进程
{
rocess(……);
sendto(……);
}
}
除非服务器在处理客户端的请求所用的时间比较长以外,人们实际上很少用这种UDP并发服务器模型的。
4、多路复用I/O并发服务器:
创建子进程会带来系统资源的大量消耗,为了解决这个问题,采用多路复用I/O模型的并发服务器。采用select函数创建多路复用I/O模型的并发服务器的算法如下:
初始化(socket,bind,listen);
复制代码 代码如下:
while(1)
{
设置监听读写文件描述符(FD_);
调用select;
如果是倾听套接字就绪,说明一个新的连接请求建立
{
建立连接(accept);
加入到监听文件描述符中去;
}
否则说明是一个已经连接过的描述符
{
进行 *** 作(read或者write);
}
多路复用I/O可以解决资源限制问题,此模型实际上是将UDP循环模型用在了TCP上面。这也会带了一些问题,如由于服务器依次处理客户的请求,所以可能导致友的客户会等待很久。


欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zz/13165999.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023-06-14
下一篇 2023-06-14

发表评论

登录后才能评论

评论列表(0条)

保存