首页 文章 精选 留言 我的

精选列表

搜索[分布式调度],共10000篇文章
优秀的个人博客,低调大师

打包微服务前后端分离项目并部署到服务器 --- 分布式 Spring Cloud + 页面渲染 Nu

前言 Spring Cloud项目属于微服务项目,也就是含有多个Sping Boot模块集合而成的项目 Nuxt.js项目属于前端基于Vue的服务端渲染项目 最近在服务器部署上线了一个基于Spring Cloud + 服务端渲染技术Nuxt.js的项目,在这里记录一下 一、部署后端 1、打包 步骤: 在pom.xml中加入打包依赖 在IDEA中点击clean、选择install打包成jar包 在target文件夹中可以看到打包的jar包 注意:如果target文件夹中出现多个jar包,.jar.original 是普通jar包,不包含依赖,.jar 是可执行jar包,包含了pom.xml中的所有依赖,可以直接用java -jar 命令执行。 打包Spring Cloud项目中的每个模块加入打包依赖 比如在gateway模块 在pom.xml加入以下代码 <build> <finalName>service-gateway</finalName> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> <resources> <resource> <directory>src/main/resources</directory> <filtering>true</filtering> </resource> </resources> </build> 然后再IDEA中maven插件中点击 相互依赖的模块怎么打包? 比如A模块依赖B模块,就需要在A模块引用B模块的依赖中加入<scope>compile</scope>,否则打包的时候会显示报错 A模块中的pom.xml文件 <dependency> <groupId>com.zfz</groupId> <artifactId>common-util</artifactId> <version>0.0.1-SNAPSHOT</version> <scope>compile</scope> </dependency> 再点击IDEA中的clean和install打包jar包 2、上传jar包到服务器 保证需要的jar包和Dockerfile、docker-compose.yml文件在同一目录 3、构建镜像 创建Dockerfile文件,举例gateway模块 FROM java:8 MAINTAINER ADD service-gateway.jar app.jar EXPOSE 80 ENTRYPOINT ["java","-jar","app.jar"] 在XShell命令行工具中输入以下命令,构建镜像 docker build -t service-gateway . 以此类推,把所有想要构建的镜像都用以上命令构建出来 最后输入docker images查看构建镜像 4、运行容器 创建docker-compose.yml文件 version: '3.1' services: service-gateway: image: service-gateway ports: - "80:80" restart: "always" container_name: service-gateway volumes: - /root/service-gateway.jar:/root/cloud/service-gateway.jar entrypoint: java -jar /root/cloud/service-gateway.jar 服务名: image: 已存在的镜像名称 ports: - 映射端口 restart: "always" container_name: 容器名称 volumes: - 挂载路径 entrypoint: 构建容器后,运行命令 ...... 在XShell命令行工具中输入以下命令,一键部署jar包 docker-compose up -d 如果不识别这个命令,可能原因就是没有安装docker-compose 安装教程: # 安装 curl -L "https://get.daocloud.io/docker/compose/releases/download/1.27.3/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose # 赋予管理员权限 chmod +x /usr/local/bin/docker-compose # 重启docker service docker restart # 查看版本信息 docker-compose --version 最后输入docker ps查看运行中的jar包 二、部署前端 1、上传前端文件到服务器 2、构建镜像 创建Dockerfile文件 # 指定node环境 FROM node:14.16.0 # 作者 MAINTAINER # node环境为生产环境 ENV NODE_ENV=production # 允许所有ip访问 ENV HOST 0.0.0.0 RUN mkdir -p /app COPY . /app WORKDIR /app # 暴露端口 EXPOSE 3000 # 使用淘宝镜像 RUN npm config set registry https://registry.npm.taobao.org # 下载依赖 RUN npm install RUN npm run build CMD ["npm", "start"] 在XShell命令行中进入到/root/app目录中,输入以下命令,构建镜像,等待如图结果,表示成功 docker build -t nuxt . 最后再输入命令docker images查看构建镜像 3、运行容器 创建容器,并且运行 docker run -d --restart=always --name nuxt -p 3000:3000 nuxt 最后再输入命令docker ps查看正在运行的容器 弄完之后,记得在阿里云安全组中,开启3000端口,运行访问 公网访问nuxt项目,http://域名:3000/

优秀的个人博客,低调大师

分布式技术专题-中间件容器的实现原理(1)Tomcat的原理之架构设计模式

Tomcat的设计模式分析 Tomcat 中运用的许多经典设计模式,如模版模式、工厂模式和单例模式等。通过学习它们的实践运用能给我们以后的软件设计起到一定的借鉴作用。 门面设计模式 门面设计模式在 Tomcat中有多处使用,在 Request 和 Response 对象封装中Standard Wrapper 到 ServletConfig 封装中、ApplicationContext 到 ServletContext 封装中等都用到了这种设计模式 门面设计模式的原理 顾名思义,就是将一个东西封装成一个门面好与人家更容易进行交流,就像一个国家的外交部一样。 这种设计模式主要用在一个大的系统中有多个子系统组成时,多个子系统肯定要涉及到相互通信,但是每个子系统又不能将自己的内部数据过多的暴露给其它系统,不然就没有必要划分子系统了。 每个子系统都会设计一个门面,把别的系统感兴趣的数据封装起来,通过这个门面来进行访问。这就是门面设计模式存在的意义。 门面设计模式示意图如下: Client 只能访问到 Façade 中提供的数据是门面设计模式的关键,至于Client 如何访问 Façade 和 Subsystem 如何提供 Façade 门面设计模式并没有规定死。 Tomcat 的门面设计模式示例 Tomcat 中门面设计模式使用的很多,因为 Tomcat 中有很多不同组件,每个组件要相互交互数据,用门面模式隔离数据是个很好的方法。 下面是 Request 上使用的门面设计模式 从图中可以看出 HttpRequestFacade 类封装了 HttpRequest 接口能够提供数据,通过 HttpRequestFacade 访问到的数据都被代理到HttpRequest 中,通常被封装的对象都被设为 Private 或者Protected 访问修饰,以防止在 Façade 中被直接访问。 观察者设计模式 设计模式也是常用的设计方法通常也叫发布 - 订阅模式,也就是事件监听机制,通常在某个事件发生的前后会触发一些操作。 观察者模式的原理 观察者模式原理也很简单,就是你在做事的时候旁边总有一个人在盯着你,当你做的事情是它感兴趣的时候,它就会跟着做另外一些事情。但是盯着你的人必须要到你那去登记,否则你不会允许它盯着你或者监视着你的。 观察者模式通常包含下面这几个角色: Subject 就是抽象主题:它负责管理所有观察者的引用,同时定义主要的事件操作。 ConcreteSubject 具体主题:它实现了抽象主题的所有定义的接口,当自己发生变 化时,会通知所有观察者。 Observer 观察者:监听主题发生变化相应的操作接口。 Tomcat 的观察者模式示例 Tomcat 中观察者模式也有多处使用,前面讲的控制组件生命周期的 Lifecycle 就是这种模式的体现,还有对 Servlet 实例的创建、Session 的管理、Container 等都是同样的原理。下面主要看一下Lifecycle 的具体实现。 Lifecycle 的观察者模式结构图: 上面的结构图中,LifecycleListener 代表的是抽象观察者,它定义一个 lifecycleEvent 方法,这个方法就是当主题变化时要执行的方法。 ServerLifecycleListener 代表的是具体的观察者,它实现了LifecycleListener 接口的方法,就是这个具体的观察者具体的实现方式。 Lifecycle 接口代表的是抽象主题,它定义了管理观察者的方法和它要所做的其它方法。而 StandardServer 代表的是具体主题,它实现了抽象主题的所有方法。这里 Tomcat 对观察者做了扩展,增加了另外两个类:LifecycleSupport、LifecycleEvent,它们作为辅助类扩展了观察者的功能。 LifecycleEvent使得可以定义事件类别,不同的事件可区别处理,更加灵活。 LifecycleSupport 类代理了主题对多观察者的管理,将这个管理抽出来统一实现,以后如果修改只要修改 LifecycleSupport 类就可以了,不需要去修改所有具体主题, 因为所有具体主题的对观察者的操作都被代理给 LifecycleSupport类了。这可以认为是观察者模式的改进版。实际执行者就是LifecyleSupport去执行 LifecycleSupport 调用观察者的方法代码如下: 清单 1. LifecycleSupport 中的 fireLifecycleEvent 方 public void fireLifecycleEvent(String type, Object data) { LifecycleEvent event = new LifecycleEvent(lifecycle, type, data); LifecycleListener interested[] = null; synchronized (listeners) { interested = (LifecycleListener[]) listeners.clone(); } for (int i = 0; i < interested.length; i++) interested[i].lifecycleEvent(event); } } 主题是怎么通知观察者呢?看下面代码: 清单 2. 容器中的 start 方法 public void start() throws LifecycleException { lifecycle.fireLifecycleEvent(BEFORE_START_EVENT, null); lifecycle.fireLifecycleEvent(START_EVENT, null); started = true; synchronized (services) { for (int i = 0; i < services.length; i++) { if (services[i] instanceof Lifecycle) ((Lifecycle) services[i]).start(); } } lifecycle.fireLifecycleEvent(AFTER_START_EVENT, null); } 命令设计模式 Tomcat 中两个核心组件 Connector 和 Container,比作一对夫妻。男的将接受过来的请求以命令的方式交给女主人。对应到 Connector 和 Container,Connector 也是通过命令模式调用Container 的。 命令模式的原理 下面是命令模式通常包含下面几个角色: Client:创建一个命令,并决定接受者 Command 命令:命令接口定义一个抽象方法 ConcreteCommand:具体命令,负责调用接受者的相应操作 Invoker 请求者:负责调用命令对象执行请求 Receiver 接受者:负责具体实施和执行一次请求 Tomcat 中的命令模式的示例 Tomcat 中命令模式在 Connector 和 Container 组件之间有体现,Tomcat 作为应用服务器,无疑会接受到很多请求,如何分配和执行这些请求是必须的功能。 下面看一下 Tomcat 是如何实现命令模式的结构图 Connector 作为抽象请求者,HttpConnector 作为具体请求者。 HttpProcessor 作为命令。 Container 作为命令的抽象接受者,ContainerBase 作为具体的接受者。 客户端就是应用服务器 Server组件了。 Server 首先创建命令请求者 HttpConnector 对象,创建命令 HttpProcessor 命令对象,把请求者封装到命令对象里面。 再把命令对象交给命令接受者ContainerBase 容器来处理命令。命令的最终是被 Tomcat 的Container 执行的。 命令可以以队列的方式进来,Container可以以不同的方式来处理请求,如HTTP1.0 协议和 HTTP1.1 的处理方式就会不同。 责任链模式 Tomcat 中一个最容易发现的设计模式就是责任链模式,这个设计模式也是 Tomcat 中 Container 设计的基础,整个容器的就是通过一个链连接在一起,这个链一直将请求正确的传递给最终处理请求的那个 Servlet。 责任链模式的原理 责任链模式,就是很多对象有每个对象对其下家的引用而连接起来形成一条链,请求在这条链上传递,直到链上的某个对象处理此请求,或者每个对象都可以处理请求,并传给下一家,直到最终链上每个对象都处理完。这样可以不影响客户端而能够在链上增加任意的处理节点。 通常责任链模式包含下面几个角色: Handler(抽象处理者):定义一个处理请求的接口 ConcreteHandler(具体处理者):处理请求的具体类,或者传给下家 Tomcat中责任链模式示例 在 tomcat 中这种设计模式几乎被完整的使用,tomcat 的容器设置就是责任链模式,从 Engine 到 Host 再到 Context 一直到Wrapper 都是通过一个链传递请求。 Tomcat 中责任链模式的类结构图如下: 上图基本描述了四个子容器使用责任链模式的类结构图,对应的责任链模式的角色,Container 扮演抽象处理者角色,具体处理者由StandardEngine 等子容器扮演。与标准的责任链不同的是,这里引入了 Pipeline 和 Valve 接口。他们有什么作用呢? 实际上 Pipeline 和 Valve 是扩展了这个链的功能,使得在链往下传递过程中,能够接受外界的干预。Pipeline 就是连接每个子容器的管子,里面传递的 Request 和 Response 对象好比管子里流的水,而 Valve 就是这个管子上开的一个个小口子,让你有机会能够接触到里面的水,做一些额外的事情。 为了防止水被引出来而不能流到下一个容器中,每一段管子最后总有一个节点保证它一定能流到下一个子容器,所以每个容器都有一个 StandardXXXValve。只要涉及到这种有链式是处理流程这是一个非常值得借鉴的模式。

优秀的个人博客,低调大师

大数据、分布式都用到了的Netty,这几大核心知识你一定要看看!

1. Netty 基础 Netty 是一个高性能、异步事件驱动的 NIO 框架,它提供了对 TCP、UDP 和文件传输的支持,作为一个异步 NIO 框架,Netty 的所有 IO 操作都是异步非阻塞的,通过 Future-Listener 机制,用户可以方便的主动获取或者通过通知机制获得 IO 操作结果。它是一个网路应用框架。 2. Netty 高性能之道 2.1. RPC 调用的性能模型分析 2.1.1. 传统 RPC 调用性能差的三宗罪 网络传输方式问题:传统的 RPC 框架或者基于 RMI 等方式的远程服务(过程)调用采用了同步阻塞 IO,当客户端的并发压力或者网络时延增大之后,同步阻塞 IO 会由于频繁的 wait 导致 IO 线程经常性的阻塞,由于线程无法高效的工作,IO 处理能力自然下降。 下面,我们通过 BIO 通信模型图看下 BIO 通信的弊端: 图2-1 BIO 通信模型图 采用 BIO 通信模型的服务端,通常由一个独立的 Acceptor 线程负责监听客户端的连接,接收到客户端连接之后为客户端连接创建一个新的线程处理请求消息,处理完成之后,返回应答消息给客户端,线程销毁,这就是典型的一请求一应答模型。该架构最大的问题就是不具备弹性伸缩能力,当并发访问量增加后,服务端的线程个数和并发访问数成线性正比,由于线程是 JAVA 虚拟机非常宝贵的系统资源,当线程数膨胀之后,系统的性能急剧下降,随着并发量的继续增加,可能会发生句柄溢出、线程堆栈溢出等问题,并导致服务器最终宕机。 序列化方式问题:Java 序列化存在如下几个典型问题: Java 序列化机制是 Java 内部的一种对象编解码技术,无法跨语言使用;例如对于异构系统之间的对接,Java 序列化后的码流需要能够通过其它语言反序列化成原始对象(副本),目前很难支持; 相比于其它开源的序列化框架,Java 序列化后的码流太大,无论是网络传输还是持久化到磁盘,都会导致额外的资源占用; 序列化性能差(CPU 资源占用高)。 线程模型问题:由于采用同步阻塞 IO,这会导致每个 TCP 连接都占用1个线程,由于线程资源是 JVM 虚拟机非常宝贵的资源,当 IO 读写阻塞导致线程无法及时释放时,会导致系统性能急剧下降,严重的甚至会导致虚拟机无法创建新的线程。 2.1.2. 高性能的三个主题 传输:用什么样的通道将数据发送给对方,BIO、NIO 或者 AIO,IO 模型在很大程度上决定了框架的性能。 协议:采用什么样的通信协议,HTTP 或者内部私有协议。协议的选择不同,性能模型也不同。相比于公有协议,内部私有协议的性能通常可以被设计的更优。 线程:数据报如何读取?读取之后的编解码在哪个线程进行,编解码后的消息如何派发, Reactor 线程模型的不同,对性能的影响也非常大。 图2-2 RPC 调用性能三要素 2.2. Netty 高性能之道 2.2.1. 异步非阻塞通信 在 IO 编程过程中,当需要同时处理多个客户端接入请求时,可以利用多线程或者 IO 多路复用技术进行处理。IO 多路复用技术通过把多个 IO 的阻塞复用到同一个 select 的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求。与传统的多线程/多进程模型比,I/O 多路复用的最大优势是系统开销小,系统不需要创建新的额外进程或者线程,也不需要维护这些进程和线程的运行,降低了系统的维护工作量,节省了系统资源。 JDK1.4 提供了对非阻塞 IO(NIO)的支持,JDK1.5_update10 版本使用 epoll 替代了传统的 select/poll,极大的提升了 NIO 通信的性能。 JDK NIO 通信模型如下所示: 图2-3 NIO 的多路复用模型图 与 Socket 类和 ServerSocket 类相对应,NIO 也提供了 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现。这两种新增的通道都支持阻塞和非阻塞两种模式。阻塞模式使用非常简单,但是性能和可靠性都不好,非阻塞模式正好相反。开发人员一般可以根据自己的需要来选择合适的模式,一般来说,低负载、低并发的应用程序可以选择同步阻塞 IO 以降低编程复杂度。但是对于高负载、高并发的网络应用,需要使用 NIO 的非阻塞模式进行开发。 Netty 架构按照 Reactor 模式设计和实现,它的服务端通信序列图如下: 图2-3 NIO 服务端通信序列图 客户端通信序列图如下: 图2-4 NIO 客户端通信序列图 Netty 的 IO 线程 NioEventLoop 由于聚合了多路复用器 Selector,可以同时并发处理成百上千个客户端 Channel,由于读写操作都是非阻塞的,这就可以充分提升 IO 线程的运行效率,避免由于频繁 IO 阻塞导致的线程挂起。另外,由于 Netty 采用了异步通信模式,一个 IO 线程可以并发处理 N 个客户端连接和读写操作,这从根本上解决了传统同步阻塞 IO 一连接一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升。 2.2.2. 零拷贝技术 很多用户都听说过 Netty 具有“零拷贝”功能,但是具体体现在哪里又说不清楚,本小节就详细对 Netty 的“零拷贝”功能进行讲解。 Netty 的“零拷贝”主要体现在如下三个方面: Netty 的接收和发送 ByteBuffer 采用 DIRECT BUFFERS,使用堆外直接内存进行 Socket 读写,不需要进行字节缓冲区的二次拷贝。如果使用传统的堆内存(HEAP BUFFERS)进行 Socket 读写,JVM 会将堆内存 Buffer 拷贝一份到直接内存中,然后才写入 Socket 中。相比于堆外直接内存,消息在发送过程中多了一次缓冲区的内存拷贝。 Netty 提供了组合 Buffer 对象,可以聚合多个 ByteBuffer 对象,用户可以像操作一个 Buffer 那样方便的对组合 Buffer 进行操作,避免了传统通过内存拷贝的方式将几个小 Buffer 合并成一个大的 Buffer。 Netty 的文件传输采用了 transferTo 方法,它可以直接将文件缓冲区的数据发送到目标 Channel,避免了传统通过循环 write 方式导致的内存拷贝问题。 下面,我们对上述三种“零拷贝”进行说明,先看 Netty 接收 Buffer 的创建: 图2-5 异步消息读取“零拷贝” 每循环读取一次消息,就通过 ByteBufAllocator的ioBuffer 方法获取 ByteBuf 对象,下面继续看它的接口定义: 图2-6 ByteBufAllocator 通过 ioBuffer 分配堆外内存 当进行 Socket IO 读写的时候,为了避免从堆内存拷贝一份副本到直接内存,Netty 的 ByteBuf 分配器直接创建非堆内存避免缓冲区的二次拷贝,通过“零拷贝”来提升读写性能。 下面我们继续看第二种“零拷贝”的实现 CompositeByteBuf,它对外将多个 ByteBuf 封装成一个 ByteBuf,对外提供统一封装后的 ByteBuf 接口,它的类定义如下: 图2-7 CompositeByteBuf 类继承关系 通过继承关系我们可以看出 CompositeByteBuf 实际就是个 ByteBuf 的包装器,它将多个 ByteBuf 组合成一个集合,然后对外提供统一的 ByteBuf 接口,相关定义如下: 图2-8 CompositeByteBuf 类定义 添加 ByteBuf,不需要做内存拷贝,相关代码如下: 图2-9 新增 ByteBuf 的“零拷贝” 最后,我们看下文件传输的“零拷贝”: 图2-10 文件传输“零拷贝” Netty 文件传输 DefaultFileRegion 通过 transferTo 方法将文件发送到目标 Channel 中,下面重点看 FileChannel 的 transferTo 方法,它的 API DOC 说明如下: 图2-11 文件传输 “零拷贝” 对于很多操作系统它直接将文件缓冲区的内容发送到目标 Channel 中,而不需要通过拷贝的方式,这是一种更加高效的传输方式,它实现了文件传输的“零拷贝”。 2.2.3. 内存池 随着 JVM 虚拟机和 JIT 即时编译技术的发展,对象的分配和回收是个非常轻量级的工作。但是对于缓冲区 Buffer,情况却稍有不同,特别是对于堆外直接内存的分配和回收,是一件耗时的操作。为了尽量重用缓冲区,Netty 提供了基于内存池的缓冲区重用机制。下面我们一起看下 Netty ByteBuf 的实现: 图2-12 内存池 ByteBuf Netty 提供了多种内存管理策略,通过在启动辅助类中配置相关参数,可以实现差异化的定制。 下面通过性能测试,我们看下基于内存池循环利用的 ByteBuf 和普通 ByteBuf 的性能差异。 用例一,使用内存池分配器创建直接内存缓冲区: 图2-13 基于内存池的非堆内存缓冲区测试用例 用例二,使用非堆内存分配器创建的直接内存缓冲区: 图2-14 基于非内存池创建的非堆内存缓冲区测试用例 各执行300万次,性能对比结果如下所示: 图2-15 内存池和非内存池缓冲区写入性能对比 性能测试表明,采用内存池的 ByteBuf 相比于朝生夕灭的 ByteBuf,性能高23倍左右(性能数据与使用场景强相关)。 下面我们一起简单分析下 Netty 内存池的内存分配: 图2-16 AbstractByteBufAllocator 的缓冲区分配 继续看 newDirectBuffer 方法,我们发现它是一个抽象方法,由 AbstractByteBufAllocator 的子类负责具体实现,代码如下: 图2-17 newDirectBuffer 的不同实现 代码跳转到 PooledByteBufAllocator 的 newDirectBuffer 方法,从 Cache 中获取内存区域 PoolArena,调用它的 allocate 方法进行内存分配: 图2-18 PooledByteBufAllocator 的内存分配 PoolArena 的 allocate 方法如下: 图2-18 PoolArena 的缓冲区分配 我们重点分析 newByteBuf 的实现,它同样是个抽象方法,由子类 DirectArena 和 HeapArena 来实现不同类型的缓冲区分配,由于测试用例使用的是堆外内存, 图2-19 PoolArena 的 newByteBuf 抽象方法 因此重点分析 DirectArena 的实现:如果没有开启使用 sun 的 unsafe,则 图2-20 DirectArena 的 newByteBuf 方法实现 执行 PooledDirectByteBuf 的 newInstance 方法,代码如下: 图2-21 PooledDirectByteBuf 的 newInstance 方法实现 通过 RECYCLER 的 get 方法循环使用 ByteBuf 对象,如果是非内存池实现,则直接创建一个新的 ByteBuf 对象。从缓冲池中获取 ByteBuf 之后,调用 AbstractReferenceCountedByteBuf的setRefCnt 方法设置引用计数器,用于对象的引用计数和内存回收(类似 JVM 垃圾回收机制)。 2.2.4. 高效的 Reactor 线程模型(重点) 常用的 Reactor 线程模型有三种,分别如下: Reactor 单线程模型; Reactor 多线程模型; 主从 Reactor 多线程模型(很重点) Reactor 单线程模型,指的是所有的 IO 操作都在同一个 NIO 线程上面完成,NIO 线程的职责如下: 作为 NIO 服务端,接收客户端的 TCP 连接; 作为 NIO 客户端,向服务端发起 TCP 连接; 读取通信对端的请求或者应答消息; 向通信对端发送消息请求或者应答消息。 Reactor 单线程模型示意图如下所示: 图2-22 Reactor 单线程模型 由于 Reactor 模式使用的是异步非阻塞 IO,所有的 IO 操作都不会导致阻塞,理论上一个线程可以独立处理所有 IO 相关的操作。从架构层面看,一个 NIO 线程确实可以完成其承担的职责。例如,通过 Acceptor 接收客户端的 TCP 连接请求消息,链路建立成功之后,通过 Dispatch 将对应的 ByteBuffer 派发到指定的 Handler 上进行消息解码。用户 Handler 可以通过 NIO 线程将消息发送给客户端。 对于一些小容量应用场景,可以使用单线程模型。但是对于高负载、大并发的应用却不合适,主要原因如下: 一个 NIO 线程同时处理成百上千的链路,性能上无法支撑,即便 NIO 线程的 CPU 负荷达到100%,也无法满足海量消息的编码、解码、读取和发送; 当 NIO 线程负载过重之后,处理速度将变慢,这会导致大量客户端连接超时,超时之后往往会进行重发,这更加重了 NIO 线程的负载,最终会导致大量消息积压和处理超时,NIO 线程会成为系统的性能瓶颈; 可靠性问题:一旦 NIO 线程意外跑飞,或者进入死循环,会导致整个系统通信模块不可用,不能接收和处理外部消息,造成节点故障。 为了解决这些问题,演进出了 Reactor 多线程模型,下面我们一起学习下 Reactor 多线程模型。 Rector 多线程模型与单线程模型最大的区别就是有一组 NIO 线程处理 IO 操作,它的原理图如下: 图2-23 Reactor 多线程模型 Reactor 多线程模型的特点: 有专门一个 NIO 线程-Acceptor 线程用于监听服务端,接收客户端的 TCP 连接请求; 网络 IO 操作-读、写等由一个 NIO 线程池负责,线程池可以采用标准的 JDK 线程池实现,它包含一个任务队列和 N 个可用的线程,由这些 NIO 线程负责消息的读取、解码、编码和发送; 1个 NIO 线程可以同时处理 N 条链路,但是1个链路只对应1个 NIO 线程,防止发生并发操作问题。 在绝大多数场景下,Reactor 多线程模型都可以满足性能需求;但是,在极特殊应用场景中,一个 NIO 线程负责监听和处理所有的客户端连接可能会存在性能问题。例如百万客户端并发连接,或者服务端需要对客户端的握手消息进行安全认证,认证本身非常损耗性能。在这类场景下,单独一个 Acceptor 线程可能会存在性能不足问题,为了解决性能问题,产生了第三种 Reactor 线程模型-主从 Reactor 多线程模型。 主从 Reactor 线程模型的特点是:服务端用于接收客户端连接的不再是个1个单独的 NIO 线程,而是一个独立的 NIO 线程池。Acceptor 接收到客户端 TCP 连接请求处理完成后(可能包含接入认证等),将新创建的 SocketChannel 注册到 IO 线程池(sub reactor 线程池)的某个 IO 线程上,由它负责 SocketChannel 的读写和编解码工作。Acceptor 线程池仅仅只用于客户端的登陆、握手和安全认证,一旦链路建立成功,就将链路注册到后端 subReactor 线程池的 IO 线程上,由 IO 线程负责后续的 IO 操作。 它的线程模型如下图所示: 图2-24 Reactor 主从多线程模型 利用主从 NIO 线程模型,可以解决1个服务端监听线程无法有效处理所有客户端连接的性能不足问题。因此,在 Netty 的官方 demo 中,推荐使用该线程模型。 事实上,Netty 的线程模型并非固定不变,通过在启动辅助类中创建不同的 EventLoopGroup 实例并通过适当的参数配置,就可以支持上述三种 Reactor 线程模型。正是因为 Netty 对 Reactor 线程模型的支持提供了灵活的定制能力,所以可以满足不同业务场景的性能诉求。 2.2.5. 无锁化的串行设计理念 在大多数场景下,并行多线程处理可以提升系统的并发性能。但是,如果对于共享资源的并发访问处理不当,会带来严重的锁竞争,这最终会导致性能的下降。为了尽可能的避免锁竞争带来的性能损耗,可以通过串行化设计,即消息的处理尽可能在同一个线程内完成,期间不进行线程切换,这样就避免了多线程竞争和同步锁。 为了尽可能提升性能,Netty 采用了串行无锁化设计,在 IO 线程内部进行串行操作,避免多线程竞争导致的性能下降。表面上看,串行化设计似乎 CPU 利用率不高,并发程度不够。但是,通过调整 NIO 线程池的线程参数,可以同时启动多个串行化的线程并行运行,这种局部无锁化的串行线程设计相比一个队列-多个工作线程模型性能更优。 Netty 的串行化设计工作原理图如下: 图2-25 Netty 串行化工作原理图 Netty 的 NioEventLoop 读取到消息之后,直接调用 ChannelPipeline 的 fireChannelRead(Object msg),只要用户不主动切换线程,一直会由 NioEventLoop 调用到用户的 Handler,期间不进行线程切换,这种串行化处理方式避免了多线程操作导致的锁的竞争,从性能角度看是最优的。 2.2.6 灵活的 TCP 参数配置能力 合理设置 TCP 参数在某些场景下对于性能的提升可以起到显著的效果,例如 SO_RCVBUF 和 SO_SNDBUF。如果设置不当,对性能的影响是非常大的。下面我们总结下对性能影响比较大的几个配置项: SO_RCVBUF 和 SO_SNDBUF:通常建议值为 128 K 或者 256 K; SO_TCPNODELAY:NAGLE 算法通过将缓冲区内的小封包自动相连,组成较大的封包,阻止大量小封包的发送阻塞网络,从而提高网络应用效率。但是对于时延敏感的应用场景需要关闭该优化算法; 软中断:如果 Linux 内核版本支持 RPS(2.6.35以上版本),开启 RPS 后可以实现软中断,提升网络吞吐量。RPS 根据数据包的源地址,目的地址以及目的和源端口,计算出一个 hash值,然后根据这个 hash 值来选择软中断运行的 cpu,从上层来看,也就是说将每个连接和 cpu 绑定,并通过这个 hash 值,来均衡软中断在多个 cpu 上,提升网络并行处理性能。 Netty 在启动辅助类中可以灵活的配置 TCP 参数,满足不同的用户场景。相关配置接口定义如下: 图2-27 Netty 的 TCP 参数配置定义 2.3. 总结 通过对 Netty 的架构和性能模型进行分析,我们发现 Netty 架构的高性能是被精心设计和实现的,得益于高质量的架构和代码,Netty 支持 10W TPS 的跨节点服务调用并不是件十分困难的事情。

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

WebStorm

WebStorm

WebStorm 是jetbrains公司旗下一款JavaScript 开发工具。目前已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的功能。

用户登录
用户注册