【未来虫教育】从 Apollo 看长轮询
如果让我设计一个配置中心,最先想到的两个核心功能:一个是如何将配置存储下来,另一个是怎么能够实时的获取到最新的配置;最简单的方式我们可以直接利用现有的一些中间件:Zookeeper、Redis 等;
- Zookeeper: 本身提供了持久化功能,同时客户端可以监听某个节点,节点数据变更,可以实时推送给客户端;
- Redis: Redis 也提供了持久化的方案,同时可以通过 psubscribe 提供的订阅功能做到配置的实时推动;
以上两个中间件在客户端进行连接的时候,其实建立的是长连接;长连接就是客户端和服务端建立连接后连接是保持的,这样服务端可以很容易的把最新的配置推送给客户端.
数据交互模式
常见的数据交互模式:Push 模式和 Pull 模式
- Push 模式:服务端主动推送数据给客户端,上面说到的 Zookeeper 和 Redis 就是这种模式;这种模式实时性很高,对客户端来说也简单,接收处理消息即可;缺点就是服务端不知道客户端处理消息的能力,可能会导致数据积压,同时也增加了服务端的工作量,影响服务端的性能;
- Pull 模式:拉取模式,即客户端主动去服务端拉取数据,主动权在客户端,拉取数据,然后处理数据,再拉取数据,一直循环下去,具体拉取数据的时间间隔不好设定,太短可能会导致大量的连接拉取不到数据,太长导致数据接收不及时;
可以发现两种模式各有优缺点;Apollo 既没有使用 Push 模式也没有使用 Pull 模式,而是使用了长轮询的数据交互模式;
- 长轮询模式:通过客户端和服务端的配合,达到主动权在客户端,同时也能保证数据的实时性;长轮询本质上也是轮询,只不过对普通的轮询做了优化处理,服务端在没有数据的时候并不是马上返回数据,会 hold 住请求,等待服务端有数据,或者一直没有数据超时处理;
长轮询模式其实在很多中间件中被广泛使用比如:RocketMQ、Kakfa、Nacos 等;当然具体每个中间件是如何实现自己的长轮询方案是不一样的,本文重点介绍的是 Apollo 如何利用 Servlet3.0 中提供的异步请求处理机制来实现自己的长轮询;下面先看一下 Servlet3.0 的异步处理机制原理.
Servlet 异步处理
Servlet 3.0 开始支持异步处理请求,在接收到请求之后 Servlet 线程可以将耗时的操作委派给另一个线程来完成,这样 Servlet 线程就可以被释放出来,可以去接收其他的请求,可以提高系统的吞吐量;为了更加清楚的了解异步处理,我们需要了解一下线程模型,下面以常用的容器 Tomcat 为例,来看一下 Tomcat 的线程模型;
Tomcat 线程模型
Unix 系统 I/O 模型主要包含以下五种类型:同步阻塞 I/O、同步非阻塞 I/O、I/O 多路复用、信号驱动 I/O 和异步 I/O;
目前主流使用的是 I/O 多路复用模型,像以高性能著称的 Netty, 以及下面要介绍的 Tomcat 都是用来此模型;此模型依赖非阻塞 Channel, 可以 通过一个线程来管理多个数据通道(Channel)的状态,极大的提供了性能;当然此模型下也演变出多个变种包括:单线程模型、多线程模型、主从多线程模型,这里就不展开讲了可以参考:Netty 系列之 Netty 线程模型
长轮询
在了解了以上这些之后,我们就可以考虑一下 Apollo 为什么使用 DeferredResult 来实现长轮询机制;从上面对长轮询的描述中,有几个点很重要分别是:
- 没有结果能 hold 住请求,能阻塞多久,这里面是存在一个超时时间的;
- 在被 hold 的这段时间内,需要释放资源,主要是线程资源;
- 有了结果能立即通知,对时效性要求高;
基于以上这三点,使用 DeferredResult 来实现长轮询是非常合适的:
- 能够将 request 和 response 保存到 AsyncContext 中,不给 Channel 响应 SocketChannel 本身就会阻塞住;
- 返回一个 DeferredResult 对象,处理完上下文信息之后,就释放 Executor 主线程了;
- 提供了 complete 和 dispatch 操作,有结果能实时通知;