“世间可称之为天经地义的事情没几样,复杂的互联网架构也是如此,万丈高楼平地起,架构都是演变而来,那么演变的本质是什么?”
— 1 —
引子
软件复杂性来源于几个方面:高并发高性能、高可用、可扩展、低成本、低规模、可维护、安全等。架构演化、发展都是为了试图降低复杂性:
高并发、高性能:互联网系统特点,用户量大,请求量大,高并发高性能成为必备要求。性能差体验会差,用户会有别选择。
高可用:系统高可用可提升用户体验,也变为必备要求。十几年前我们买股票都需要T+N操作,而现在通过手机可以实时办理。
可扩展、易迭代:在产品初期,采用单体或简单的架构。成熟期,演进为现在大中台、小前台的概念,把不变的和变得拆分开来。产品经理、架构师需避免无限放大需求,面向未来设计,进入尴尬境地。
低成本:是个过程。ROI投入产出比越往后越低。
低规模:规模小,成本肯定低,运维、扩展.... 都将方便。所以简单、适用、演进架构设计原则很重要。
易运维:除了传统运维方面。业务的快速发展,灰度发布、快速发布回滚、部分功能升级、ab测试等对架构层面提出更高要求,也是现在容器化技术这么流行原因之一。
本文主要从如何实现高并发、高性能系统角度,剖析网络应用架构演进过程中,解决的那些关键点,并找到一些规律。也可指导我们构建高并发、高性能系统时,应该注意哪些环节。
如何更有效的利用单机资源?开源软件在高性能、高并发中做了哪些实践。
如何在高并发前提下,利用跨机器远程调用提升并发及“性能”。分布式服务如何拆分,怎么拆分才能达到高性能高可用,并不浪费资源?
注:太多的调用链路,性能是有很大损耗的。
... ...
篇幅有限,文章不会铺开讲所有细节。
— 2 —
从网络连接开始
浏览器/app与后端通信一般使用http、https协议,底层都是使用TCP(Transmission Control Protocol 传输控制协议),而RPC远程调用可直接使用TCP连接。我们从TCP连接开始文章。
大家都知道TCP 三次握手建立连接、四次挥手断开连接,简述如下:
建立连接都是客户端主动发起,经过三次交替交互后(中间会有状态),双方状态都变为 ESTABLISHED状态,可以开始双工数据传送。
断开连接双方都可以主动发起, 分别发起、回复一共四次交互(中间会有状态),关闭连接。
注:详细细节请参阅相关文档,Windows和Linux服务器都可以使用netstat -an命令查看。
网络编程中,关于连接这块我们一般会关注以下指标:
1、连接相关
服务端能保持,管理,处理多少客户端的连接。
活跃连接数:所有ESTABLISHED状态的TCP连接,某个瞬时,这些连接正在传输数据。如果您采用的是长连接的情况,一个连接会同时传输多个请求。也可以间接考察后端服务并发处理能力,注意不同于并发量。
非活跃连接数:表示除ESTABLISHED状态的其它所有状态的TCP连接数。
并发连接数:所有建立的TCP连接数量。=活跃连接数+非活跃连接数。
新建连接数:在统计周期内,从客户端连接到服务器端,新建立的连接请求的平均数。主要考察应对 突发流量或从正常到高峰流量的能力。如:秒杀、抢票场景。
丢弃连接数:每秒丢弃的连接数。如果连接服务器做了连接熔断处理,这部分数据即熔断的连接。
关于tcp连接数量,在linux下,跟文件句柄描述项有关,可以ulimit -n查看,也可修改。其它就是跟硬件资源cpu、内存、网络带宽有关。单机可以做到数十万级的并发连接数,如何实现呢?后面IO模型时讲解。
2、流量相关
主要是网络带宽的配置。
流入流量:从外部访问服务器所消耗的流量。
流出流量:服务器对外响应的流量。
3、数据包数
数据包是TCP三次握手建立连接后,传输的内容封装
流入数据包数:服务器每秒接到的请求数据包数量。
流出数据包数:服务器每秒发出的数据包数量。
关于TCP/IP包的细节请查阅相关文档。但是有一点一定注意,我们单次请求可能会分成多个包发送,拆包、粘包问题网络中间件都会为我们处理(比如消息补齐、回车结尾、自定义消息头体、自定义协议等解决方案)。如果我们传递的用户数据较小,那么效率肯定会提升。反过来无限制的压缩传输包的大小,解压也会耗费cpu资源,需平衡处理。
4、应用传输协议
传输协议压缩率好,传输性能好,对并发性能提升高。但是也需要看调用双方的语言可以使用协议才行。可以自己定义,也可以使用成熟的传输协议。比如redis的序列化传输协议、json传输协议、Protocol Buffers传输协议、http协议等。 尤其在 rpc调用过程中,这个传输协议选择需要仔细甄别选型。
5、长、短连接
长连接是指在一个TCP连接上,可以重用多次发送数据包,在TCP连接保持期间,如果没有数据包发送,需要双方发检测包以维持此连接。
半开连接的处理:当客户端与服务器建立起正常的TCP连接后,如果客户主机掉线(网线断开)、电源掉电、或系统崩溃,服务器将永远不会知道。长连接中间件,需要处理这个细节。linux默认配置2小时,可以配置修改。
短连接是指通信双方有数据交互时,就建立一个TCP连接,数据发送完成后,则断开此TCP连接。但是每次建立连接需要三次握手、断开连接需要四次挥手。
关闭连接最好由客户端主动发起,TIME_WAIT这个状态最好不要在服务器端,减少占用资源。
选择建议:
在客户端数量少场景一般使用长连接。后端中间件、微服务之间通信最好使用长连接。如:数据库连接,duboo默认协议等。
而大型web、app应用,使用http短连接(http1.1的keep alive变相的支持长连接,但还是串行请求/响应交互)。http2.0支持真正的长连接。
长连接会对服务端耗费更多的资源,上百万用户,每个用户独占一个连接,对服务端压力多大,成本多高。IM、push应用会使用长连接,但是会做很多优化工作。
由于https需要加解密运算等,最好使用http2.0(强制ssl),传输性能很好。但是服务端需要维持更多的连接。
6、关于并发连接与并发量
并发连接数:=活跃连接数+非活跃连接数。所有建立的TCP连接数量。网络服务器能并行管理的连接数。
活跃连接数:所有ESTABLISHED状态的TCP连接。
并发量:瞬时通过活跃连接传输数据的量,这个量一般在处理端好评估。跟活跃连接数没有绝对的关系。网络服务器能并行处理的业务请求数。
rt响应时间:各类操作单机rt肯定不相同。比如:从cache中读数据和分布式事务写数据库,资源的消耗不同,操作时间本身就不同。
吞吐量:QPS/TPS,每秒可以处理的查询或事务数,这个是关键指标。
从系统整体层面、各个服务个体、服务中某个方法都需综合考虑。
举例如下:
打开商品详情页操作,需要动静分离。后续一连串的动态服务、cache机制,整体rt本身会短,单机可以支持的qps较高。(服务间、方法间也有差别)
而提交订单操作需要分布式事务、分布式锁等,rt本身会长,单机可支持的qps较低。
那是否我们就会针对订单提交的服务部署更多机器呢?答案是不一定。因为用户浏览商品的频度会很高,而提交订单的频度很低。如何正确的评估呢?
需要服务分类:关键服务/非关键服务、高峰各服务的qps需求,来均衡考虑。
系统整体吞吐量、RT响应时间、支持并发数 是由小的操作、微服务组成的,各个微服务、操作也需要分别评估。平衡组合后,形成系统整体的各项指标。
7、小节
首先看一个典型的互联网服务端处理网络请求的典型过程:
注:另外关于用户态、内核态数据转换,有些特殊场景中,中间件如kafka可以使用zero copy技术,避免两态切换开销。
a、(1,2,3 )三个步骤表示客户端网络请求,建立连接(管理连接),发送请求,服务器接收请求数据。
b、(4)构建响应,在用户空间处理客户端的请求,构建响应完成。
c、(5,6,7) 服务器把响应,通过a中fd连接,send发送响应客户端。
可以把上面分为两个关键点:
a和c 服务器如何管理网络连接,从客户端获得输入数据,为客户端响应数据。
b服务器如处理请求。
网络应用应该考虑平衡a+c和b,处理这些连接的能力 与 能管理的连接请求达到平衡。
比如:有个应用并发连接数十万;而这些连接大约每秒请求2万次;需要管理10万连接,每秒处理2万请求能能力,才能达到平衡。如何达到处理高qps呢,两个方向:
单机优化(见后中间件例子)
转发到别多台机器处理(远程调用)
注:一般系统管理连接能力远远大于处理能力。
如上图,客户端的请求会形成一个大队列;服务器会处理这个大队列中的任务。这个队列能有多大,看连接管理能力;如何保证进入队列任务的速率和处理移除任务的速度平衡,是关键。达到平衡是目的。
— 3 —
网络编程中常用IO模型
客户端与服务器的交互都会产生个连接,linux中在服务器端由文件描述项 fd、socket编程中socket连接、java语言api中channel等体现。而IO模型,可以理解为管理fd,并通过fd从客户端read获取数据(客户端请求)和通过fd往客户端write数据(响应客户端)的机制。
关于同步,异步、阻塞、非阻塞 IO操作,网上、书籍上描述都不相同,也找不到准确描述。我们按照《UNIX网络编程:卷一》第六章——I/O复用为标准。书中向我们提及了5种类UNIX下可用的I/O模型:阻塞式I/O、非阻塞式I/O、I/O复用(selece,poll,epoll)、信号驱动式I/O、异步I/O。(详细可以查阅相关书籍资料)
1、阻塞式I/O:进程会卡在recvfrom的调用,等到最终结果数据返回。肯定属于同步。
2、非阻塞式I/O:进程反复轮训调用recvfrom,直到最终结果数据返回。也是同步调用,但是IO内核处理时非阻塞的。没什么实用意义,不讨论应用。
3、I/O复用也属于同步:进程卡在select、epoll调用上,不会卡在recvfrom上,直到最终结果返回。
注:select 模型:把要管理的fd放到一个数据里,循环这个数据。数组大小1024,可管理连接有限。poll 与select类似,只是把数组类型改为链表,没有1024大小限制。
而epoll 为 event poll,只会管理有事件发生的 fd,也就是只会处理活跃的连接。epoll通过内核和用户空间共享一块mmap()文件映射内存来实现的消息传递。参考 http://libevent.org/
4、信号驱动式I/O:也是同步。只有unix实现,不讨论。
5、异步:只有异步I/O属于异步。底层操作系统只有window实现,不讨论。nodejs中间件通过回调实现,java AIO也有实现。开发难度较大。
IO模型中同步/异步、阻塞/非阻塞的差别(好绕):
同步异步:访问数据的方式,同步需主动读写数据,要求被调用方IO返回最终的结果。而异步发出请求后,只需等待IO操作完成的通知,并不主动读写数据,由系统内核完成;
而阻塞和非租塞的区别在于,进程或线程要访问的数据是否就绪,进程或线程是否需要等待;等待就是阻塞,不需要等待就是非阻塞。
而我们平时在编程、函数接口调用过程中,除了超时以外,都会返回一个结果。同步异步调用按照以下区分:
如果返回的结果是最终结果,就是同步调用,如:调用数据查询sql。
如果返回的结果是个中间通知,那么是异步:如:发送消息给mq,只会返回ack信息。对于发消息来说,是同步;如果从系统架构层面看,算异步,因为处理结果由消息消费者来处理产生。如果发送成功,但是突然断网没有收到ack,这是属于故障,不在讨论范围内。
同步调用,参数中可以传递一个回调函数的方式:需要语言或中间件引擎执行。如jvm支持,node v8引擎支持。(需要回调函数的执行,跟调用端在一个context内,共享栈变量等)
注:select关键字可别混淆!!!IO多路复用从技术实现上有多种:select、poll、epoll 详细自己参阅资料,几乎所有中间件都会使用epoll模式。另外由于各个操作系统对多路复用实现机制不同,epoll、kqueue、IOCP接口都有自己的特点,第三方库的封装了这些差异,提供统一的API,如Libevent。另外如java语言,netty提供更高层面的封装,javaNIO和netty使用保留了select方法,也引起一些混淆。
小节:现在网络中间件都是用 阻塞IO和IO多路复用这两个模型来管理连接,通过网络IO获取数据。下节讲解,使用IO模型的一些中间件案例。
— 4 —
同步阻塞IO模型的具体实现模型-PPC,TPC
服务器处理数据问题,从纯网络编程技术角度看,主要思路有两个:
一个是对于每个连接处理分配一个独立的进程/线程,直到处理完成。PPC,TPC模式;
另一个思路是用同一进程/线程来同时处理若干连接,处理连接中数据,通过多线程、多进程技术。Reactor模式;
每个进程/线程处理一个连接,叫PPC或TPC。PPC是Process Per Connection TPC是Thread Per Conection ,传统阻塞IO模型实现的网络服务器采用这种模式。
注:close特指主进程对连接的计数,连接实际在子进程中关闭。而多线程实现中,主线程不需要close操作,因为父子线程共享存储。如:java中jmm
注:pre模式,预先创建线程和进行,连接进来,分配到预先创建好的线程或进程。多进程时有惊群现象。
申请线程或进程会占用很多系统资源,操作系统cpu、内存有限度,能同时管理的线程有限,处理连接的线程不能太多。虽然可以提前建立好进程或线程来处理数据(prefork/prethead)或通过线程池来减少线程建立压力。但是线程池的大小是个天花板。另外父子进程通信也比较复杂。
apache MPM prefork(ppc),可支持256的并发连接,tomcat 同步IO(tpc)采用阻塞IO方式工作,可支持500个并发连接。java可以创建线程池来降低一定创建线程资源开销来处理。
网络连接fd可以支持上万个,但是每个线程需要占有系统内存,线程同时存在的总数有限。linux下用命令ulimit -s可以查看栈内存分配。线程多了对cup的资源调度开销。失衡情况发生,如何解决呢?
小节:ppc、tpc瓶颈是能够管理的连接数少。本来多线程处理业务能力够,这下与fd绑定了,线程生命周期与fd一样了,限定了线程处理能力。拆分:把fd生命周期与线程的生命周期拆分开来。
— 5 —
IO模型的具体实现模型-Reactor
每个进程/线程同时处理多个连接(IO多路复用),多个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象上等待,无需阻塞等待所有连接。当某条连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回(还有更好优化,见下小节),开始进行业务处理;就是Reactor模式思想。
Reactor 模式,是指通过一个或多个输入同时传递给服务处理器的服务请求的事件驱动处理模式。服务端程序处理客户端传入的多路请求,并将它们同步分派给请求对应的处理线程,Reactor 模式也叫 Dispatcher 模式。即 I/O 多了复用统一监听事件,收到事件后分发(Dispatch 给某进程),是编写高性能网络服务器的必备技术之一。很多优秀的网络中间件都是基于该思想的实现。
注:由于epoll比select管理的连接数大了好多,libevent,netty等框架中底层实现都是epoll方式,但是编程API保留了select关键字。所以文章中epoll_wait跟select等同。
Reactor模式有几个关键的组成:
Reactor:Reactor在一个单独的线程运行,负责监听fd事件,分发给适当的处理程序对IO事件做出反应。建立连接事件分发给Acceptor;分发read/write处理事件给Handler。
Acceptor:负责处理建立连接事件,并建立对应的Handler对象。
Handlers:负责处理read和write事件。从fd中获取请求数据;处理数据得到相应数据;send相应数据。处理程序执行IO事件要完成的实际事情。
对于IO密集型(IO bound)场景,可以使用Reactor场景,但是ThreadLocal将不能使用。开发调试难度较大,一般不建议自己实现,使用现有框架即可。
小节:Reactor解决可管理的网络连接数量提升到几十万。但是如此多连接上请求任务,还是需要通过多线程、多进程机制处理。甚至负载转发到其它服务器处理。
— 6 —
Reactor模式实践案例(C语言)
通过几个开源框架的例子,了解不同场景下的网络框架,是如何使用Reactor模式,做了哪些细节调整。
注:实际实现肯定与图差别很大。客户端io及send比较简单,图中省略。
A、单Reactor+单线程处理(整体一个线程)redis为代表
如图所示:
客户端请求->Reactor对象接受请求,并通过select(epoll_wait)监听请求事件->通过dispatch分发事件;
如果是连接请求事件->dispatch->Acceptor(accept建立连接)->为这个连接创建一个Handler 对象等待后续业务处理。
如果不是建立连接事件->dispatch分发事件->触发到为这个连接创建的那个Handler对象(read、业务处理、send),形成一个任务/命令队列。
Handler对象完成read->业务处理->send整体流程。
把请求转化为命令队列,单进程处理。注意图中 队列,单线程处理,是没有竞争的。
优点:
模型简单。这个模型是最简单的,代码实现方便,适合计算密集型应用
不用考虑并发问题。模型本身是单线程的,使得服务的主逻辑也是单线程的,那么就不用考虑许多并发的问题,比如锁和同步
适合短耗时服务。对于像redis这种每个事件基本都是查内存,是十分适合的,一来并发量可以接受,二来redis内部众多数据结构都是非常简单地实现
缺点:
性能问题,只有一个线程,无法完全发挥多核 CPU 的性能。
顺序执行影响后续事件。因为所有处理都是顺序执行的,所以如果面对长耗时的事件,会延迟后续的所有任务,特别对于io密集型的应用,是无法承受的
这也是为什么redis禁止大家使用耗时命令
注:redis是自己实现的io多路复用,没有使用libevent,实现与图不符,更加轻巧。
这种模型对于处理读写事件操作很短很短时间内执行完。大约可达到10万QPS吞吐量(redis各种命令差别很大)。
注:redis发布版本中自带了redis-benchmark性能测试工具,可以使用它计算qps。示例:使用50个并发连接,发出100000个请求,每个请求的数据为2kb,测试host为127.0.0.1端口为6379的redis服务器性能:./redis-benchmark -h127.0.0.1 -p 6379 -c 50 -n 100000 -d 2
对于客户端数量多的网络系统,强调多客户端,也就是并发连接数。 对于后端连接数少的的网络系统,采用长连接,并发连接数少,但是每个连接发起的请求数多。
B、单 Reactor+单队列+业务线程池
如图所示,我们按把真正的业务处理从 Reactor线程中剥离出来,通过业务线程池来实现。那么Reactor中每个fd的Handler对象如何与 Worker线程池通信的,通过待处理请求队列 。客户端对服务器的请求,本来可以想象成一个请求队列IO, 这里经过Reactor(多路复用)处理后,(拆分)转化为一个待处理工作任务的队列。
注:处处是拆分啊!
业务线程池线程池分配独立的线程池,从队列中拿到数据进行真正的业务处理,将结果返回Handler。Handler收到响应结果后,send结果给客户端。
与A模型相比,利用线程池技术加快了客户端请求处理能力。例如:thrift0.10.0版本中 nonblocking server 采用这种模型,能达到几万级别的QPS。
缺点:这种模型的缺点就在于这个队列上,是性能瓶颈。线程池从队列获取任务需要加锁,会采用高性能的读写锁实现队列。
C、单 Reactor+N队列+N线程
这种模型是 A和B的变种模型,memcached采用这种模型。待处理工作队列分为多个,每个队列绑定一个线程来处理,这样最大的发挥了IO多路复用对网络连接的管理,把单队列引起的瓶颈得到释放。QPS估计可达到20万。
但是这种方案有个很大的缺点,负载均衡可能导致有些队列忙,有些空闲。好在memcached 也是内存的操作,对负载问题不是很敏感,可以使用该模型。
D、单进程Reactor监听+N进程(accept+epoll_wait+处理)模型
流程:
master(Reactor主进程)进程监听新连接的到来,并让其中一个worker进程accept。这里需要处理惊群效应问题,详见nginx的accept_mutex设计
worker(subReactor进程)进程accept到fd之后,把fd注册到到本进程的epoll句柄里面,由本进程处理这个fd的后续读写事件
worker进程根据自身负载情况,选择性地不去accept新fd,从而实现负载均衡
优点:
进程挂掉不会影响这个服务
是由worker主动实现负载均衡的,这种负载均衡方式比由master来处理更简单
缺点:
多进程模型编程比较复杂,进程间同步没有线程那么简单
进程的开销比线程更多
nginx使用这种模型,由于nginx主要提供反向代理与静态内容web服务功能,qps指标与被nginx代理的处理服务器有关系。
注:nodejs多进程部署方式与nginx方式类似。
小节:期望从这几个 Reactor的实例中,找到拆分解决了哪些问题,引起的哪些问题。
— 7 —
Reactor模式实践案例(Java语言Netty)
Netty是 一个异步事件驱动的网络应用程序框架,用于快速开发可维护的高性能协议服务器和客户端,java语言的很多开源网络中间件使用了netty,本文只描述针对NIO多路复用相关部分,很多拆包粘包、定时任务心跳监测、序列化钩子等等可参阅资料。如图所示:
netty可以通过配置,来实现各个模块在哪个线程(池)中运行:
1、单Reactor单线程
EventLoopGroup bossGroup = new NioEventLoopGroup(1);//netty默认只会单Reactor
EventLoopGroup workerGroup = bossGroup ;//监听线程和工作线程使用一个
ServerBootstrap server = new ServerBootstrap();
server.group(bossGroup, workerGroup);
2、单Reactor多线程subReactor
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();//默认cup核心*2
ServerBootstrap server = new ServerBootstrap();
server.group(bossGroup, workerGroup);//主线程和工作线程分开
3、单Reactor、多线程subReactor、指定线程池处理业务
https://netty.io/4.1/api/io/netty/channel/ChannelPipeline.html
我们在一个pipeline中定义多个ChannelHandler,用以接收I / O事件(例如,读取)和请求I / O操作(例如,写入和关闭)。例如,典型的服务器在每channel的pipiline中,都有以下Handler:(具体取决于使用的协议和业务逻辑的复杂性和特征):
Protocol Decoder - 将二进制数据(例如ByteBuf)转换为Java对象。
Protocol Encoder - 将Java对象转换为二进制数据。
Business Logic Handler - 执行实际的业务逻辑(例如数据库访问)。
如下例所示:
static final EventExecutorGroupgroup = new DefaultEventExecutorGroup(16);
...
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(“decoder”,new MyProtocolDecoder());
pipeline.addLast(“encoder”,new MyProtocolEncoder());
//告诉这个MyBusinessLogicHandler的事件处理程序方法不在I / O线程中,
//以便I / O线程不被阻塞,一项耗时的任务运行在自定义线程组(池)
//如果您的业务逻辑完全异步或很快完成,则不需要额外指定一个线程组。
pipeline.addLast(group,“handler”,new MyBusinessLogicHandler());
前文中提到过,web应用程序接受百万、千万的网络连接,并管理转化为请求、响应,就像一个大队列一样,如何更好的处理队列里面的任务,牵扯到负载均衡分配、锁、阻塞、线程池、多进程、转发、同步异步等一系列负载问题。 单机及分布式都要优化,netty做了很多优化,这部分netty源码不好读懂:
业务处理与IO任务公用线程池
自定义线程池处理业务
如图所示:netty中, 不固定数量的channel、固定的NioEventLoop、可外置线程池的EventExecutor,在众多channel不定时的事件驱动下,如何协调线程很是复杂。
留个问题:基于netty的spring webflux 、nodejs,为什么能支撑大量连接,而cpu成为瓶颈?
小节:这样我们从 客户端发起请求->到服务端建立连接->服务端非阻塞监听传输->业务处理->响应 整个流程,通过IO多路复用、线程池、业务线程池 让整个处理链条没有处理瓶颈、处理短板,达到整体高性能、高吞吐。
但是耗时处理能力远远低于IO连接的管理能力,单机都会达到天花板,继续拆分(专业中间件干专业事),RPC、微服务调用是解决策略。
— 8 —
分布式远程调用(不是结尾才是开始)
由前文看出,单机的最终瓶颈会出在业务处理上。对java语言来说,线程数量不可能无限扩大。就算使用go语言更小开销的协程,cpu也会成为单机瓶颈。所以跨机器的分布式远程调用肯定是解决问题的方向。业内已经有很多实践,我们从三个典型架构图,看看演进解决的问题是什么,靠什么解决的:
注:本文不从soa,rpc,微服务等方面讨论,只关注拆分的依据和目标。
A、单体应用
B、把网络连接管理和静态内容拆分
C、业务功能性拆分
A:典型单体应用。
A->B:连接管理与业务处理拆分。使用网络连接管理能力强大的nginx,业务处理单独拆分为多台机器。
B->C:业务处理从功能角度拆分。有些业务侧重协议解析、有些侧重业务判断、有些侧重数据库操作,继续拆分。
通过图C,从高性能角度,看服务分层(各层技术选型也有很多)的准则及需要注意点:
1、反向代理层(关联https连接)
可以通过nginx集群实现,也可以通过lvs,f5实现。
通过上层nginx实现,可以知道该层应对的是大量http或https请求。
核心指标是:并发连接数、活跃连接数、出入流量、出入包数、吞吐量等。
内部关于协议解析模块、压缩模块、包处理模块优化等。关键方向代理出去的请求吞吐量,也就是nginx转发到后端应用服务器的处理能力,决定整体吞吐量。
静态文件都走cdn。
关于https认证比较费时,建议使用http2.0,或保持连接时间长点。但这也与业务情况有关。如:每个app与后端交互是否频繁。毕竟维护太多连接,成本也很高,影响多路复用性能。
2、网关层(通用无业务的操作)
反向代理层通过http协议连接网关层,二者之间通过内网ip通信,效率高很多。我们假定网关层往下游都使用tcp长连接,java语言中dobbo等rpc框架都可以实现。
网关层主要做几个事情:
鉴权
数据包完整性检查
http json 传输协议转化为java对象
路由转义(转化为微服务调用)
服务治理相关(限流、降级、熔断等)功能
负载均衡
网关层可以由:有开源的Zuul,spring cloud gateway,nodejs等实现。nginx也可以做网关需要定制开发,与反向代理层物理上合并。
3、业务逻辑层(业务层面的操作)
从这层可以考虑按照业务逻辑垂直分层。例如:用户逻辑层、订单逻辑层等。如果这样拆分,可能会抽象一层通过的业务逻辑层。我们尽量保证业务逻辑层不横向调用,只上游调用下游。
业务逻辑判断
业务逻辑处理(组合)
分布式事务实现
分布式锁实现
业务缓存
4、数据访问层(数据库存储相关的操作)
专注数据增删改查操作。
orm封装
隐藏分库分表的细节。
缓存设计
屏蔽存储层差异
数据存储幂等实现
注:本节引用了孙玄老师《百万年薪架构师课程》中一些观点,推荐一下这门课,从架构实践、微服务实现、服务治理等方面,从本质到实战面面俱到。
网关层以下,数据库以上,RPC中间件技术选型及技术指标如下(来源dubbo官网):
核心指标是:并发量、TQps、Rt响应时间。
选择协议因素:dubbo、rmi、hesssion、webservice、thrift、memached、redis、rest
连接个数:长连接一般单个;短连接需要多个
是否长连接:长短连接
传输协议:TCP、http
传输方式::同步、NIO非阻塞
序列化:二进制(hessian)
使用范围:大文件、超大字符串、短字符串等
根据应用场景选择,一般默认dubbo即可。
小节:
单机时代:从每个线程管理一个网络连接;再到通过io多路复用,单个线程管理网络连接,腾出资源处理业务;再到io线程池和业务线程池分离;大家能发现个规律,客户端连接请求是总起点->后端处理能力逐步平衡加强的过程。业务处理能力总是赶不上接受处理的能力。
反向代理时代:nginx能够管理的连接足够的多了,后端可以转发到N台应用服务器tomcat。从某种程度上,更加有效的利用的资源,通过硬件、软件选型,把 管理连接(功能)和处理连接(功能)物理上拆分开,软件和硬件配合处理自己更擅长的事情。
SOA、微服务时代:(SOA的出现其实是为了低耦合,跟高性能高并发关系不大)业务处理有很多种类型。有的是运算密集型;有的需要操作数据库;有的只需从cache读一些数据;有些业务使用率很高;有些使用频度很低。为了更好利用又有了两种拆分机制。把操作数据库的服务单独拆出来(数据访问层),把业务逻辑处理的拆分出来(业务逻辑层);按照以上逻辑推断:可能一台nginx+3台tomcat网关+5台duboo业务逻辑+10台duboo数据访问配置合适。 我们配置的目的是,各层处理的专属的业务都能把服务器压到60%资源占用。
注:文章只关注了功能层面的水平分层。而垂直层面也需要分层。例如:用户管理和订单管理是两类不同的业务,业务技术特点、访问频次也不同。存储层面也需要垂直分库、分表。本文暂且略过。
单机阶段,多线程多进程其实相当于一种垂直并发拆分,尽量保证无状态,尽量避免锁等,跟微服务无状态、分布式锁原理上是一致的。
— 9 —
总结
回顾前文,客户端连接到服务器端后都要干什么呢?性能瓶颈是维护这么多连接?还是针对每个连接的处理达不到要求失衡?如何破局?从单机内部、再到物理机器拆分的描述看来,有三点及其重要:
关注平衡:达到平衡的架构,才可能是高性能、高并发架构。任何性能问题都会由某个点引起。甚至泛指业务需求与复杂度也要平衡。
拆分之道:合适的事情,让合适的技术、合适的中间件解决。具体:如何横向、纵向拆分还需分析场景。
了解业务场景、问题本质&&了解常用场景下解决方案:按照发现问题、分析问题、解决问题思路来看,我们把弹药库备齐,解决问题的过程,就是个匹配的过程。
除了文中提到的技术以及拆分方案,很多技术点,都可以提升吞吐及性能,列举如下:
IO多路复用:管理更多的连接
线程池技术:挖掘多核cpu的潜力
zero-copy:减少用户态和内核态交互次数。如java中transferTo,linux中sendfile系统接口;
磁盘顺序写:降低寻址开销。消息队列或数据库日志,都会采用此技术。
压缩更好的协议:网络传输上减少开支,如:自定义或二进制传输协议;
分区:在存储系统中,分库分表都算分区;而微服务中,设计服务无状态,本身也可以理解为分区。
批量传输:典型数据库 batch技术。很多网络中间件也可以使用,如消息队列中。
索引技术:这里不是特指数据库的索引技术。而是我们设计切合业务场景的索引,提供效率。例如:kafka针对文件的存储,采用一些hack的索引技巧。
缓存设计:当数据生命修改不频繁、变更规律性很强、生成一次成本太高时,可以考虑缓存
空间换时间:其实分区、索引技术、缓存技术都可归为这类。例如:我们使用倒排索引存储数据、使用多份数据多份节点提供服务等。
网络连接的选型:长短连接,可靠、非可靠协议等。
拆包粘包:batch、协议选型于此有些关系。
高性能分布式锁:并发编程中,锁不可避免。尽量使用高性能的分布式锁,能cas乐观锁,尽量避免悲观锁。如果业务允许,尽量异步锁,不要同步阻塞锁,减少锁竞争。
柔性事务代替刚性事务:有些异常或者故障,试图通过重试是恢复不了的。
最终一致性:如果业务场景允许,尽量保证数据最终一致性。
非核心业务异步化:把某些任务转化为另外一个队列(消息队列),消费端可以批量、多消费者处理。
direct IO:例如数据库等自己构建缓存机制的应用程序,直接使用directIO,放弃操作系统提供的缓存。
... .... 欢迎留言讨论补充拍砖
注:脱离业务场景,很多只能是纸上谈兵。但不了解手段,遇到场景也会懵逼。客户端请求形成的超级队列,后端如何分而治之、分散逐个击破,是整体思想。