参考
- Netty实战
- Netty权威指南
- How RPC Work
- 深入理解 RPC : 基于 Python 自建分布式高并发 RPC 服务
- 目前为止最透彻的的Netty高性能原理和框架架构解析
- Apache Dubbo
- Scalable IO in Java
- Dubbo官方文档
- Java核心技术面试精讲
BIO
缺点:
- 在任何时候都有大量的线程处于休眠状态(等待输入或输出数据就绪)
- 需要为每个线程的调用栈都分配内存(默认大小64KB到1MB)
- 即使JVM物理上支持非常大量的线程,但远在达该极限前,上下文切换所带来的开销会非常麻烦
NIO
使用selector
select poll epoll
Reactor模型
- 经典的服务设计: 每个handler在单独的线程中处理
/**
* Classic ServerSocket Loop
* 忽略了异常处理
*/
public class Server implements Runnable {
@Override
public void run() {
ServerSocket ss = new ServerSocket(8080);
while (! Thread.interrupted()) {
new Thread(new Handler(ss.accept())).start();
}
}
static class Handler implements Runnable {
final Socket socket;
Handler(Socket s) {
socket = s;
}
@Override
public void run() {
byte[] input = new byte[1024];
socket.getInputStream().read(input);
byte[] output = process(input);
socket.getOutputStream().write(output);
}
}
}
- Reactor: 通过分配合适的handler来响应IO时间
- Handlers:执行非阻塞的动作
单Reactor单线程模型
- Channels: 连接文件、scokets等等,支持non-blocking读
- Buffers:类似于数组,可以被Channels读写
- Selectors:识别哪些Channels有IO事件
- SelectionKeys:维护IO事件状态和绑定
- Reactor: Setup / Dispatch Loop
public class Reactor implements Runnable {
final Selector selector;
final ServerSocketChannel serverSocketChannel;
public Reactor(int port) throws IOException {
this.selector = Selector.open();
this.serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(port));
serverSocketChannel.configureBlocking(false);
SelectionKey sk = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
sk.attach(new Acceptor());
}
@Override
public void run() {
try {
// Dispatch Loop
while (! Thread.interrupted()) {
selector.select();
Set selected = selector.selectedKeys();
Iterator it = selected.iterator();
while (it.hasNext()) {
dispatch((SelectionKey)(it.next()));
}
}
} catch (IOException ex) {
}
}
void dispatch(SelectionKey k) {
// Acceptor构建Handler的时候,会将自己attach到SelectionKey中
Runnable r = (Runnable) (k.attachment());
if (r != null) {
r.run();
}
}
}
- Reactor: Acceptor
class Acceptor implements Runnable {
@Override
public void run() {
try {
SocketChannel c = serverSocketChannel.accept();
if (null != c) {
new Handler(selector, c);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
- Reactor: Handler Setup / Request handling
final class Handler implements Runnable {
final SocketChannel socketChannel;
final SelectionKey sk;
ByteBuffer input = ByteBuffer.allocate(1024);
ByteBuffer output = ByteBuffer.allocate(1024);
static final int READING = 0, SENDING = 1;
int state = READING;
Handler(Selector sel, SocketChannel socketChannel) throws IOException {
this.socketChannel = socketChannel;
this.sk = socketChannel.register(sel, 0);
// 将自己attach,这样dispatch的时候就可以获取对应的Handler
sk.attach(this);
sk.interestOps(SelectionKey.OP_READ);
sel.wakeup();
}
@Override
public void run() {
try {
if (state == READING) read();
else if (state == SENDING) send();
} catch (IOException e) {
}
}
void read() throws IOException {
socketChannel.read(input);
if (inputIsComplete()) {
process();
state = SENDING;
sk.interestOps(SelectionKey.OP_WRITE);
}
}
void send() throws IOException {
socketChannel.write(output);
if (outputIsComplete()) {
sk.cancel();
}
}
}
多线程模型
- Woker Threads(工作线程)
- Reactors应当快速的出发handlers
- 将NIO处理分发给其他线程
- 多个Reactor线程
线程模型
线程池Executor
- 从线程池中获取一个
Thread,并指派它去运行一个已提交的任务(一个Runable的实现) - 当任务完成时,将该
Thread返回给该列表,使其可被重用
虽然池化和重用线程相对简单地为每个任务创建和销毁线程,但它并不能消除由上下文切换所带来的开销。
EventLoop
见Netty说明
Rest
REST 代表表现层状态转移(REpresentational State Transfer)。REST 是一种软件架构风格,不是技术框架,REST 有一系列规范,满足这些规范的 API 均可称为 RESTful API。REST 规范中有如下几个核心:
- REST 中一切实体都被抽象成资源,每个资源有一个唯一的标识 —— URI,所有的行为都应该是在资源上的 CRUD 操作
- 使用标准的方法来更改资源的状态,常见的操作有:资源的增删改查操作
- 无状态:这里的无状态是指每个 RESTful API 请求都包含了所有足够完成本次操作的信息,服务器端无须保持 Session
REST 由于天生和 HTTP 协议相辅相成,HTTP 协议已经成了实现 RESTful API 事实上的标准:
| HTTP方法 | 行为 | URI | 示例说明 |
|---|---|---|---|
| GET | 获取资源列表 | /users | 获取用户列表 |
| GET | 获取一个具体的资源 | /users/admin | 获取admin用户的详细信息 |
| POST | 创建一个新的资源 | /users | 创建一个新用户 |
| PUT | 以整体的方式更新一个资源 | /users/1 | 更新id为1的用户 |
| DELETE | 删除服务器上的一个资源 | /users/1 | 删除id为1的用户 |
RPC
远程过程调用(Remote Procedure Call,RPC)是一个计算机通信协议。该协议允许运行于一台计算机的程序调用另一台计算机的子程序,而程序员无须额外地为这个交互作用编程。
过程如下:
- Client通过本地调用,调用 Client Stub
- Client Stub将参数打包(也叫 Marshalling)成一个消息,然后发送这个消息
- Client所在的OS将消息发送给Server
- Server端接收到消息后,将消息传递给Server Stub
- Server Stub将消息解包(也叫Unmarshalling)得到参数
- Server Stub调用服务端的子程序(函数),处理完后,将最终结果按照相反的步骤返回给Client
- Stub负责调用参数和返回值的流化(serialization)、参数的打包解包,以及负责网络层的通信。Client端一般叫Stub,Server 端一般叫Skeleton。
REST VS RPC
RPC优势:
- RPC一般传输效率更高
- 实际应用中,很多操作不能抽象为资源,不能严格按照REST规范做
- RPC屏蔽网络细节,易用,与本地调用类似
但RPC开发过程比较繁琐
REST优势:
- 轻量级,简单易用,维护性和扩展性都很好
- REST更规范,标准,通用,支持的语言多,满足HTTP即可
- JSON格式可读性更强,开发调试方便
- REST API更清晰,更易理解
Netty
特性
关系如下:
- 一个
EventLoopGroup包含一个或多个EventLoop - 一个
EventLoop在它的生命周期只绑定一个Thread - 由
EventLoop处理的所有I/O事件都在一个专用的Thread上 - 一个
Channel在它的生命周期只注册到一个EventLoop - 一个
EventLoop可能被分配一个或多个Channel
组件
Channel: 大大降低直接使用Socket类进行I/O操作的复杂性(bind()、connect()、read()、write())
EventLoop: 处理连接的生命周期中所发生的事件ChannelFuture: Netty中所有的I/O操作都是异步的,一个操作可能不会立即返回,是一种用于在之后某个时间点确定其结果的方法。其addListener()方法注册了一个ChannelFutureListener,以便在某个操作完成时得到通知ChannelHandler: 充当了所有处理入站和出站的应用程序逻辑的容器(比如数据格式转换)ChannelInboundHandler: 处理入站事件和数据ChannelOutbondHandler: 处理出站事件和数据
ChannelPipeline:ChannelPipeline是ChannelHandler链的容器,ChannelPipeline拦截了流经Channel的inbound和outbound请求事件。
每创建一个Channel都要关联一个ChannelPipleline,这种关联是不可变的。
ChannelHandlerContext它使得ChannelHandler可以和ChannelPipeline以及其他的handler交互。一个handler可以通知在ChannelPipeline中的下一个ChannelHandler,甚至动态修改handler所属的ChannelPipeline
通过Channel或ChannelPipeline进行事件传播
通过ChannelHandlerContext触发的事件流
Bootstrap
| 类别 | Bootstrap | ServerBootstrap |
|---|---|---|
| 场景 | 客户端 | 服务器端 |
EventLoopGroup个数 | 1 | 2 |
与
ServerChannel关联的EventLoopGroup分配了一个负责创建接收连接请求Channel的EventLoop。一旦连接接收,第二个EventLoopGroup分配一个EventLoop给它的Channel
EventLoop
事件循环模型
在这个模型中,一个EventLoop由一个永不变的Thread驱动,同时任务(Runnable或Callable)可以直接提交给EventLoop,立即执行或者调度执行。根据配置和可用核心的不同,可能会创建多个EventLoop以优化资源使用,而一个EventLoop可以服务于多个Channel。
实现细节
- 线程管理
- 线程分配
异步传输
阻塞传输
Bootstrap
- 客户端
Bootstrap
- 服务端
ServerBootstrap
- 从客户端Bootstrapping客户端
传输
Netty内置了以下内置传输
| 名称 | 包 | 描述 |
|---|---|---|
| NIO | io.netty.channel.socket.nio | 以java.nio.channels包为基础->使用selector方式 |
| Epoll | io.netty.channel.epoll | 通过JNI调用epoll()和NIO接口。一些特性只在Linux系统支持 |
| OIO | io.netty.channel.socket.oio | 以java.net包为基础-> 使用阻塞流 |
| Local | io.netty.channel.local | 可以在JVM内部通过pipe进行本地通信 |
| Embedded | io.netty.channel.embedded | 基于ChannelHandler而又不需要真正的网络传输 |
NIO -> 非阻塞IO
Netty的NIO传输基于Java提供的异步/非阻塞网络编程通用抽象。保证了Netty的非阻塞API可以再任何平台使用。
Epoll -> 用于Linux的本地非阻塞传输
Netty为Linux提供了一组NIO API,可以使用Linux自身的epoll调用,并以一种更加轻量的方式使用中断。在高负载下的性能优于JDK的NIO实现
OIO -> 旧的阻塞I/O
Netty的OIO传输实现代表了一种折中:它可以通过Netty通用API使用,但是不是异步的(建立在阻塞的java.net包实现之上)。
Netty利用了Socket的SO_TIMEOUT标志(超时I/O操作,将会抛出异常),Netty捕获这个异常,并继续正常的循环流程。这是像Netty这种异步框架能支持同步OIO的原因。
基于JVM内部通信的Local传输
Netty的Local传输:同一JVM下面的客户端与服务端异步通信(类似于Android中的Binder?类比Linux pipe?)。
在这个传输中,与服务Channel关联的SocketAddress没有绑定到一个物理地址,而是注册到注册表中,直到Channel关闭。
Embedded传输
允许内嵌ChannelHandler实例到其他ChannelHandlers中,可以扩展一个ChannelHandler功能还不需要改变其内部代码(类比Spring AOP?)。可以用于单元测试:
编解码
-
解码
-
编码
Dubbo
框架设计
注册中心
线程模型
老的线程模型
我们重点关注 Consumer 部分:
- 业务线程发出请求,拿到一个 Future 实例。
- 业务线程紧接着调用 future.get 阻塞等待业务结果返回。
- 当业务数据返回后,交由独立的 Consumer 端线程池进行反序列化等处理,并调用 future.set 将反序列化后的业务结果置回。
- 业务线程拿到结果直接返回
新的线程模型(2.7.5)
- 业务线程发出请求,拿到一个 Future 实例。
- 在调用 future.get() 之前,先调用 ThreadlessExecutor.wait(),wait 会使业务线程在一个阻塞队列上等待,直到队列中被加入元素。
- 当业务数据返回后,生成一个 Runnable Task 并放入 ThreadlessExecutor 队列
- 业务线程将 Task 取出并在本线程中执行:反序列化业务数据并 set 到 Future。
- 业务线程拿到结果直接返回
这样,相比于老的线程池模型,由业务线程自己负责监测并解析返回结果,免去了额外的消费端线程池开销。