Node.js design pattern : Reactor (Event Loop)

本文深入探讨了Node.js非阻塞I/O的工作原理,包括其Reactor模式、事件循环机制、以及如何通过libuv实现跨平台的非阻塞I/O处理。

Nodejs是非阻塞的,源于它是基于事件循环的设计模式,该模式也称为Reactor模式。

Nodejs同时也是单线程的,这里的单线程指的是开发人员编写的代码运行在单线程上,而Nodejs的内部一些实现代码却是多线程的,如对于I/O 的处理(读取文件、网络请求等)。关于Event Loop另一篇文章中有粗略提到,本文将详细阐述。

但对于I/O请求不也是开发人员编写的代码吗,不是说我们自己写的代码都是运行在单线程上的,怎么这里又可能变成多线程了? 这里就要讲到reactor模式了。在此之前,先简单了解下Blocking I/ONon-blocking I/O

Blocking I/O vs Non-blocking I/O

Blocking I/O

Blocking I/O是程序会等待I/O请求直到结果返回,相当于控制权一直在等待I/O这边,在等待的这段时间里程序不会去干其他事,就这么一直干等着。例子如:

	data = socket.read();
	// wait until the data fetch back
	print(data)

对于web server来说,是必须要处理多个请求的。对于Blocking I/O情况,是无法处理多个请求,每个请求都会在上一个请求处理完才能处理。解决的方法是启用多线程处理,该处理场景如下图:

blocking IO

开启多个线程处理的代价有点高(内存占用,上下文切换),而且从图中看到每个线程都有很多空余时间在干等着,无法充分利用时间。

Non-blocking I/O

对于Non-blocking I/O, 一般是请求后直接返回,不用等待请求结果返回。如果没有数据可以返回的话,是直接返回一个预设好的常量标识当前还没数据可以返回。

这里首先举例一个最基本的实现方式,不断循环这些资源直到能读取到数据。

	// 资源集合
	resources = [socketA, socketB, pipeA];
	// 只要还有资源没获取到数据,就一直循环操作
	while(!resources.isEmpty()) {
		for(i = 0; i < resources.length; i++) {
			resource = resources[i];

			// 直接返回non-blocking
			// 若无数据则直接返回预设常量
			let data = resource.read();

			if(data === NO_DATA_AVAILABLE)
			// 该资源还在等待中未准备好
				continue;

			if(data === RESOURCE_CLOSED)
			// 该资源已经读取完毕,从集合中删除
				resources.remove(i);
			else
			// 数据已经获取,处理数据
				consumeData(data);
			}
	}

这样就可以做到单个线程中处理并发处理多个请求资源了。这种做法被称为busy-wait,该做法虽然使得单个线程可以处理多个并发请求,但CPU会一直消耗在轮询中,无法抽身去干其他事情。因此non-blocking I/O一般通过synchronous event demultiplexer来实现。

关于什么是 synchronous event demultiplexer,这里引用wikipedia中的一段话。

Uses an event loop to block on all resources. The demultiplexer sends the resource to the dispatcher when it is possible to start a synchronous operation on a resource without blocking

(Example: a synchronous call to read() will block if there is no data to read. The demultiplexer uses select() on the resource, which blocks until the resource is available for reading. In this case, a synchronous call to read() won't block, and the demultiplexer can send the resource to the dispatcher.)

简单来说就是,对于事件循环中的资源会通过该多路分发器(demultiplexer)下发给对应的程序去处理,处理好了则把对应事件保存到event queue中等待事件循环轮询运行。

如上述例子说的调用read()之后马上可以运行接下来的代码而不会产生阻塞,阻塞的事情交给了分发器去做了,具体怎么做每个系统有不同的实现,这就是更底层的事了。

简单例子如:

	socketA, pipeB;
	// 注册事件
	watchedList.add(socketA, FOR_READ); 
	watchedList.add(pipeB, FOR_READ);
	
	// demultiplexer blocking 等待事件完成(成功取回数据)
 	// events保存成功的事件
	while(events = demultiplexer.watch(watchedList)) { 
		...
	}

Reactor Pattern

Nodejs中的事件循环正是基于event demultiplexerevent queue,而这两块正是Reactor Pattern的核心。对于Nodejs的事件循环,首选要明确的一点是:

只有一个主线程执行JS代码,我们写的代码就是在该线程执行的,该线程也同是event loop运行的线程。(并不是主线程运行JS代码,然后又有一个线程在同时运行event loop)。

该模式执行过程大致如下图所示:

event loop

  1. event demultiplexer接收到I/O请求然后下发给对应的底层去处理。

  2. 一旦I/O获取到了数据,event demultiplexer会把注册的回调函数添加到event queue中等待event loop去执行。

  3. event queue中的回调函数依次被event loop执行,直到event queue为空。

  4. event queue中没数据了或者event demultiplexer没有再接受到请求,程序即event loop就会结束,意味着该应用就退出了,否则回到第一步。

Event Demultiplexer

之前已经初略讲过了Event Demultiplexer是什么了,这里详细讲下nodejs中的event demultiplexer

event demultiplexer实际上是一个抽象的概念,不同的系统有不同的实现方式,如Linux的epoll,MacOS中的kqueue,Windows中的IOCP。nodejs则通过libuv屏蔽了对不同系统的实现支持跨平台,提供了针对多种不同I/O请求的具体处理方式的API(如File I/O,Network I/O,DNS处理等)。

可以认为libuv把这一堆复杂的东西都结合在一起形成了nodejs中的event demultiplexer。libuv结构如下图所示:

libuv

libuv中,对于一些I/O操作是直接利用系统层级I/O中的non-blockingasynchronous特性(如提到的epoll等),但对于一些类型的I/O,由于复杂性的问题libuv则通过thread pool来处理。

所以就如同一开始说的,用户开发层面的代码是单线程的,但在I/O处理中是有可能出现多线程,但不会涉及到开发人员写的JS代码,因为thread pool是在libuv库里面的。

Event Queue

上面说到了event queue,是用来存储回调函数等待被event loop处理的。但实际上,不止一个event queue队列,事件循环要处理的主要有4个类型的队列。

  • Timers and Intervals Queue: 保存setTimeoutsetInterval中的回调函数(实际上不是队列,数据结构是最小堆实现,这里就统一都叫队列了)
  • IO Event Queue: 保存已经完成的I/O回调函数。
  • Immediates Queue: 保存setImmediate中的回调函数。
  • Close Handlers Queue: 其他所有close事件的回调,如socket.on('close', ...)

除了上述四个主要队列外,还有两个比较特殊的队列:

  • Next Ticks Queue:保存process.nextTick中的回调函数。
  • Other Microtasks Queue:保存Promise等microtask中的回调函数。

这里又再插一句,macrotask和microtask的区别

那么这些队列是怎么被事件循环处理的呢?直接看图。

event queue

事件循环会依次处理timers and intervals queueIO event queueimmediates queueclose handlers queue这四个队列,如果处理完close hanlers queue后,timers and intervals没有数据再进来,就退出事件循环。

处理其中一个队列的过程称为一个phase。一次事件循环就是处理这四个phase的过程。那另外两个特殊的队列是在什么时候运行的呢? 答案就是在每个 phase运行完后马上就检查这两个队列有无数据,有的话就马上执行这两个队列中的数据直至队列为空。当这两个队列都为空时,event loop 就会接着执行下一个phase

这两个队列相比,Next Ticks Queue的权限要比Other Microtasks Queue的权限要高,因此Next Ticks Queue会先执行。

此外要注意的是,如果process.nextTick中出现递归调用没有停止条件的话,Next Ticks Queue将一直有数据进来一直都不会为空,则会阻塞event loop的执行。为了防止该情况,process.maxTickDepth定义了迭代的最大值,不过从NodeJS v0.12版本开始已经移除了。

该文章转载于 https://juejin.im/post/5a5e03eef265da3e5033c5b9

<think>我们遇到了两个关键错误: 1. `java.lang.BootstrapMethodError`:通常是由于字节码版本不兼容或方法句柄调用失败引起,根本原因可能是依赖库版本冲突。 2. `java.lang.NoClassDefFoundError: reactor/netty/http/client/HttpClient`:这表明在运行时找不到`reactor.netty.http.client.HttpClient`类,通常是因为缺少相关依赖或依赖版本不正确。 根据引用[1]和[2]的信息,问题主要与Spring Cloud Gateway和Reactor Netty的版本兼容性有关。引用[1]提到`NoClassDefFoundError`指向`reactor.ipc.netty.http.client.HttpClient`,而引用[2]则指向`reactor.netty.http.client.HttpClient`,这可能是由于Reactor Netty的版本升级导致的包路径变化(从`reactor.ipc.netty`变为`reactor.netty`)。 ### 解决方案 #### 1. **检查并统一Spring Boot和Spring Cloud版本** 这是最常见的原因。请确保你的Spring Boot和Spring Cloud版本兼容。根据引用[2]和[3]的建议: - 在Maven的`pom.xml`中,显式指定Spring Boot和Spring Cloud的版本,并确保它们匹配。例如: ```xml <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.3.RELEASE</version> <!-- 或者使用其他兼容版本 --> </parent> <properties> <java.version>1.8</java.version> <spring-cloud.version>Greenwich.SR3</spring-cloud.version> <!-- 对应Spring Boot 2.1.x --> </properties> ``` - 在`<dependencyManagement>`中引入Spring Cloud依赖管理: ```xml <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> ``` #### 2. **检查Reactor Netty依赖** 确保项目中包含了正确版本的`reactor-netty`依赖。对于Spring Boot 2.1.x和Spring Cloud Greenwich,通常需要`reactor-netty`版本为0.8.x。但根据引用[2]的错误信息,可能是版本不匹配导致的方法签名变化。 - 添加或更新`reactor-netty`依赖: ```xml <dependency> <groupId>io.projectreactor.netty</groupId> <artifactId>reactor-netty</artifactId> <version>0.8.12.RELEASE</version> <!-- 与Spring Boot 2.1.3.RELEASE兼容 --> </dependency> ``` #### 3. **解决依赖冲突** 使用Maven命令检查依赖树: ```bash mvn dependency:tree -Dincludes=io.projectreactor.netty:reactor-netty ``` 如果存在多个版本,Maven可能会选择高版本,而高版本可能与当前Spring Boot版本不兼容。可以通过`<exclusions>`排除冲突的版本。 #### 4. **清理和重新构建项目** 在修改依赖后,执行清理和重新构建: ```bash mvn clean install ``` #### 5. **检查类路径** 确保`reactor/netty/http/client/HttpClient.class`存在于构建输出的类路径中。可以在IDE中查看依赖库,或者检查打包后的jar/war文件。 ### 错误原因分析 - **版本不兼容**:Spring Boot 2.1.x需要特定的Reactor Netty版本(0.8.x)。如果使用了错误的版本(如0.9.x或更高),则可能因为包路径或方法签名改变而导致`NoClassDefFoundError`或`BootstrapMethodError`。 - **依赖传递冲突**:其他依赖可能引入了不兼容的Reactor Netty版本。 ### 验证是否解决 启动应用程序,观察是否还有相同的错误。如果问题依旧,请检查是否有其他依赖引入了冲突的版本。 ### 替代方案:升级Spring Boot 如果项目允许,可以考虑升级到更新的Spring Boot版本(如2.3.x或2.4.x),并相应调整Spring Cloud版本。例如: ```xml <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.4.13</version> </parent> <properties> <spring-cloud.version>2020.0.5</spring-cloud.version> </properties> ``` 这样可以使用更新的Reactor Netty(如1.0.x),避免旧版本的问题。 §§ 相关问题 §§ 1. 如何解决Spring Cloud Gateway中的`NoSuchMethodError`问题?[^2] 2. 如何排查Java项目中的依赖冲突?[^1] 3. Spring Boot和Spring Cloud版本如何对应?[^3] 4. 如何解决Reactor Netty中的SSL握手失败问题?[^1] 5. 在微服务中如何统一管理依赖版本?[^3]</think>### 解决 `java.lang.BootstrapMethodError` 导致的 `NoClassDefFoundError: reactor/netty/http/client/HttpClient` 问题 这个错误通常由 **Reactor Netty 版本冲突**或**依赖不兼容**引起。以下是完整的解决方案: --- #### 1. **统一 Spring Boot 和 Spring Cloud 版本(关键步骤) 在 `pom.xml` 中确保版本兼容: ```xml <properties> <!-- 统一使用兼容版本 --> <java.version>1.8</java.version> <spring-boot.version>2.1.3.RELEASE</spring-boot.version> <spring-cloud.version>Greenwich.SR3</spring-cloud.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> ``` > **重要**:Spring Cloud Greenwich.SR3 必须匹配 Spring Boot 2.1.x[^3] --- #### 2. **显式添加 Reactor Netty 依赖 强制指定正确版本: ```xml <dependency> <groupId>io.projectreactor.netty</groupId> <artifactId>reactor-netty</artifactId> <version>0.8.12.RELEASE</version> <!-- 与 Spring Boot 2.1.3 兼容 --> </dependency> ``` > **注意**:Spring Boot 2.1.x 需要 Reactor Netty 0.8.x,而 2.4+ 需要 1.0.x --- #### 3. **解决依赖冲突 检查冲突的依赖: ```bash mvn dependency:tree | grep netty ``` 若存在多个版本,通过 `<exclusions>` 解决: ```xml <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> <exclusions> <exclusion> <groupId>io.projectreactor.netty</groupId> <artifactId>reactor-netty</artifactId> </exclusion> </exclusions> </dependency> ``` --- #### 4. **清理并重建项目 ```bash mvn clean install -U ``` --- #### 错误原因分析 1. **版本不匹配**:Spring Boot 2.1.x 与 Reactor Netty 1.0+ 不兼容 2. **依赖冲突**:多个 Netty 版本共存导致类加载失败 3. **字节码不兼容**:`BootstrapMethodError` 通常由 JDK 与依赖库的字节码版本冲突引起 --- #### 验证是否解决 创建测试类验证 HttpClient 可用性: ```java import reactor.netty.http.client.HttpClient; public class NettyTest { public static void main(String[] args) { HttpClient.create().get().uri("https://example.com").response().block(); System.out.println("Netty 连接成功!"); } } ``` > 若成功输出,则问题已解决[^1] --- ### 替代方案:升级到兼容版本组合 若项目允许升级: ```xml <!-- 升级到新版本组合 --> <spring-boot.version>2.4.13</spring-boot.version> <spring-cloud.version>2020.0.5</spring-cloud.version> <reactor-netty.version>1.0.15</reactor-netty.version> ``` ---
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值