AlexiaChen / AlexiaChen.github.io

My Blog https://github.com/AlexiaChen/AlexiaChen.github.io/issues
87 stars 11 forks source link

架构设计----服务端编程之网络高性能 #79

Open AlexiaChen opened 4 years ago

AlexiaChen commented 4 years ago

网络高性能

高性能是所有程序员的追求,无论是做底层系统还是做应用App,我们都希望在成本内尽可能的优化性能。它是最复杂的一环,磁盘,操作系统,CPU,内存,缓存,网络,编程语言,架构都有可能影响系统整体达到高性能呢个,一行不恰当的debug日志就可能把一个高性能服务器拖慢成龟速。一个服务器配置的参数,比如tcp_nodelay就可能将响应时间从2ms延长到40ms。

站在架构的角度,当然需要关注高性能的架构,关注在两个方面:

架构设计是高性能的基础,如果架构设计没有做到高性能,则后面的具体实现和编码能提升的空间有限。简单一句话就是,架构决定了系统的性能上限,具体实现细节决定了性能下限。

这里主要讲服务器端的网络高并发的性能

单服务器网络高性能

关键之一就是服务端采取的网络编程模型,有以下两个设计关键点:

以上两个设计点最终都和操作系统的I/O模型及进程模型相关。

one Process per connection

标题的字面意思就是,一个连接对应一个进程处理。每次有新的连接过来就新建一个进程专门处理这个连接的请求。大概是以下步骤:

同一个文件描述符多进程访问是有引用计数的,计数到0才会真正关闭连接。

该模型非常简单,最早的服务器网络模型大部分是这么搞的,但是随着互联网兴起,这模型才没落了,因为代价太高。

prefork

也就是服务器启动的时候,预先fork出多个子进程,这样连接过来的时候就不需要fork了,这样用户端的感受就是,速度快了些。

prefork实现的关键就是,多个子进程都accept同一个socket,当有新的连接进入时,操作系统保证只有一个进程能最后accept成功。不必担心引入额外的性能开销。

当然,问题也存在,就是父子通信复杂,支持的并发连接数有限。但Apache服务器提供了MPM prefork模式。现在很少用了,除非是一些老系统。

one thread per connection

就是一个新连接,就建立一个线程去处理这个连接请求(write read 业务逻辑处理)。与进程相比,当然会更轻量些,创建线程的消耗要比进程少得多。同时多线程是共享同一个进程的内存空间,通信成本会低些。

该模型确实是一个进步,但是也有问题:

该模型处理近一千个并发连接还是有问题的,如果是几百个并发,还不如用one Process per connection的模型,因为稳定性更高。

prethread

预先创建处理请求的线程,和prefork类似。让用户体验更快。

由于是同一进程内的多线程,通信方便,prethread的实现要比prefork灵活些。大概有下面两种方式:

Apache服务器的MPM worker模式本质就是一种prethread方案,但是稍微做了改进,服务器创建多个进程,每个进程又有多个线程,主要是考虑稳定性,因为其中某个线程异常崩溃,不至于导致整个服务器挂掉,还有其他进程提供继续提供服务。

Reactor

重点来了,该模型是现代互联网高并发的基础模型。

前面说的用多线程,多进程或者两者结合,或者引入线程池就会引入一个新的问题,进程如何才能高效的处理多个连接的业务?当一个连接一个线程时,线程可以采用"read -> 业务逻辑处理 -> write" 这种处理流程,如果当前连接没有数据可以读,则进程就阻塞在read操作上。这种阻塞的方式在一个连接一个线程的场景下没问题,但是如果一个线程处理多个连接,线程阻塞在某个连接的read操作上,即使此时此刻其他连接有数可以读,线程也无法去处理,很显然这是无法做到高性能的。

解决这个问题就最简单的就是将read操作改为非阻塞,然后线程不断地while去轮询多个连接。这种方式能解决阻塞带来的低效问题,但是不优雅,首先,轮询是要消耗CPU的,其次一个线程处理几千上万个连接,则轮询的效率就很低了。

为了能更好的解决问题,一种自然而然的想法就是只有当连接上有数据的时候,线程才去处理,这就是I/O多路复用技术的来源。

上面提到的“多路”就是多条连接,“复用”就是多条连接复用同一个阻塞对象,这个阻塞对象和具体的实现有关。在Linux上如果使用select,那么这个公共阻塞对象就是select用到的fd_set,如果是epoll,就是epoll_create创建的文件描述符。

I/O多路复用有两个关键实现点:

I/O多路复用结合线程池,完美的解决了之前所提到的模型的所有问题。而且有个很酷的名字-----Reactor,就是反应堆。实质就是事件反应的意思,就是来了一个事件,我就有相应的反应。有些开源系统里面叫Dispatcher模式,即I/O多路服用统一监听事件,收到事件后分配(Dispatch)给某个线程处理。

Reactor模式的核心组成部分包括Reactor和线程池,Reactor负责监听和分配事件,线程池负责处理事件。感觉起来简单嘛,但是不是,Reactor模式具体实现方案灵活多变。主要体现以下两点:

最终Reactor模式有以下三种典型实现方案:

以上方案具体选择进程还是线程,更多的是个人口味。Java的高性能网络库Netty用的是线程,Nginx使用进程,memcached使用线程。

单Reactor 单进程 or 单线程

该模式优点简单,没有进程/线程间通信,没有竞争,全部在同一个进程内完成,但是缺点也明显:

因此,单Reactor单进程的方案在实践中应用场景不多,只适用业务处理非常快速的场景,业界的Redis用的就是这个模型,因为它是数据结构服务器,修改各种数据结构一般不会太耗时。

单Reactor 多线程

为了避免单Rector 单进程/单线程方案的缺点,由此改进的模型

该模型能够充分利用多核多CPU的处理能力,但是也有问题存在:

单个Reactor承担所有事件的监听和响应,只在主I/O线程中运行,难以应对瞬时高并发访问,也就是突发高并发事件。这样会有单点性能瓶颈。

多Reactor 多进程 or 多线程

这个方案在保证上一个方案模型优点的同时,也解决了应对瞬时高并发访问的问题。

看起来这个模型比单Reactor多线程复杂,但是实际实现更简单,而且优点多多:

目前Nginx是采用多Reactor多进程,memcahed和Netty是多Reactor多线程。

Proactor

Reactor是非阻塞同步网络模型,因为真正的read,send网络操作都需要用户进程同步操作,简而言之就是read和send这样的I/O操作是同步的(epoll也是同步的),如果把I/O操作换成异步就能够进一步提升性能,这就是异步网络模型Proactor。

Proactor翻译为--前摄器模式,它与Reactor有啥区别呢?

其实用大白话说就是Reactor是被动,Proactor是主动。

Proactor的主要处理步骤如下:

理论上,Proactor比Reactor性能高一些,异步I/O能够充分利用DMA特性,让I/O操作与计算同时进行。但实现真正的异步I/O,操作系统需要做大量的工作,windows下的IOCP实现了真正的异步I/O,而Linux下的AIO并不完善,因此Linux下实现高并发网络编程都是以Reactor这样的同步模式为主。boost::asio实现了Proactor在windows下采用了IOCP,但是在Linux下用的是Reactor(epoll)模式之上模拟出来一个Proactor异步模型。

集群高性能

主要是负载均衡了,包括DNS,硬件负载均衡,软件负载均衡。

DNS成本低,负载均衡基本上交给DNS服务器处理,无须自己开发维护负载均衡设备,就近原则,提高访问速度,缺点也多多,DNS缓存的时间比较长,修改配置后,用户还会访问修改前的IP,达到负载失败,DNS的负载均衡控制权在域名商那里,无法根据业务特点做定制扩展。(一般用于地理级别的负载均衡)

硬件负载均衡成本高,但是功能强大,全面支持各个层级,各个负载均衡算法,支持全局负载均衡。软件负载均衡支持到100K级别已经非常厉害,但是硬件负载均衡可以支持1000k并发。然后稳定性又高,还具备防火墙,防DDoS的攻击的安全功能。缺点就是扩展性差,价格普通公司承受不起。比如F5。(一般用于机房集群级别的负载均衡,比如北京机房有多个集群,这一个机房用一台F5设备,来负载集群与集群之间的负载)

软件负载均衡有Nginx和LVS。Nginx是7层负载均衡,LVS是4层负载均衡。Nginx支持http等协议,LVS因为太底层,与协议无关,所有几乎所有应用都可以做,比如聊天和数据库等。(一般用于集群内部机器之间的负载均衡)

负载均衡算法也就是网络上罗列的那几样,参考着选择就行。