微服务学习总结-> RPC/Netty/Dubbo

827 阅读9分钟

参考

BIO

缺点

  1. 在任何时候都有大量的线程处于休眠状态(等待输入或输出数据就绪)
  2. 需要为每个线程的调用栈都分配内存(默认大小64KB到1MB)
  3. 即使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事件状态和绑定
  1. 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();
        }
    }
}
  1. 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();
        }
    }
}
  1. 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)是一个计算机通信协议。该协议允许运行于一台计算机的程序调用另一台计算机的子程序,而程序员无须额外地为这个交互作用编程。

过程如下:

  1. Client通过本地调用,调用 Client Stub
  2. Client Stub将参数打包(也叫 Marshalling)成一个消息,然后发送这个消息
  3. Client所在的OS将消息发送给Server
  4. Server端接收到消息后,将消息传递给Server Stub
  5. Server Stub将消息解包(也叫Unmarshalling)得到参数
  6. 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: ChannelPipelineChannelHandler链的容器,ChannelPipeline拦截了流经Channel的inbound和outbound请求事件。

每创建一个Channel都要关联一个ChannelPipleline,这种关联是不可变的。

  • ChannelHandlerContext 它使得ChannelHandler可以和ChannelPipeline以及其他的handler交互。一个handler可以通知在ChannelPipeline中的下一个ChannelHandler,甚至动态修改handler所属的ChannelPipeline

通过ChannelChannelPipeline进行事件传播

通过ChannelHandlerContext触发的事件流

  • Bootstrap
类别BootstrapServerBootstrap
场景客户端服务器端
EventLoopGroup个数12

ServerChannel关联的EventLoopGroup分配了一个负责创建接收连接请求ChannelEventLoop。一旦连接接收,第二个EventLoopGroup分配一个EventLoop给它的Channel

EventLoop

事件循环模型

在这个模型中,一个EventLoop由一个永不变的Thread驱动,同时任务(RunnableCallable)可以直接提交给EventLoop,立即执行或者调度执行。根据配置和可用核心的不同,可能会创建多个EventLoop以优化资源使用,而一个EventLoop可以服务于多个Channel

实现细节

  • 线程管理
  • 线程分配

异步传输

阻塞传输

Bootstrap

  • 客户端 Bootstrap

  • 服务端 ServerBootstrap

  • 从客户端Bootstrapping客户端

传输

Netty内置了以下内置传输

名称描述
NIOio.netty.channel.socket.niojava.nio.channels包为基础->使用selector方式
Epollio.netty.channel.epoll通过JNI调用epoll()和NIO接口。一些特性只在Linux系统支持
OIOio.netty.channel.socket.oiojava.net包为基础-> 使用阻塞流
Localio.netty.channel.local可以在JVM内部通过pipe进行本地通信
Embeddedio.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 部分:

  1. 业务线程发出请求,拿到一个 Future 实例。
  2. 业务线程紧接着调用 future.get 阻塞等待业务结果返回。
  3. 当业务数据返回后,交由独立的 Consumer 端线程池进行反序列化等处理,并调用 future.set 将反序列化后的业务结果置回。
  4. 业务线程拿到结果直接返回

新的线程模型(2.7.5)

  1. 业务线程发出请求,拿到一个 Future 实例。
  2. 在调用 future.get() 之前,先调用 ThreadlessExecutor.wait(),wait 会使业务线程在一个阻塞队列上等待,直到队列中被加入元素。
  3. 当业务数据返回后,生成一个 Runnable Task 并放入 ThreadlessExecutor 队列
  4. 业务线程将 Task 取出并在本线程中执行:反序列化业务数据并 set 到 Future。
  5. 业务线程拿到结果直接返回

这样,相比于老的线程池模型,由业务线程自己负责监测并解析返回结果,免去了额外的消费端线程池开销。

序列化

负载均衡

线程池设置

未完待续