最近项目中用到了 Vertx ,顺带记录下学习笔记
Vert.xInAction 读书笔记一 (Vertx 与 异步编程)
Asynchronous 异步和 reactive 响应式是现代应用程序中重要的话题,本文中主要介绍这两点。
通过网络连接的分布式软件是大势所趋
现在,应用程序可以使用云数据库,云存储,云函数。应用程序也更愿意会给自己终端用户提供接口。这一切使得程序网络化,分布式化。以及面向服务的软件架构也能通过请求其他第三方服务暴漏的接口来复用功能。
栗如应用程序中的身份验证委托给谷歌、Facebook或Twitter等流行的帐户提供商,或将支付处理委托给Stripe或PayPal
不要生活在信息孤岛上
图1.1 的现代应用程序由以下网络服务组成
网络服务的弊端
而网络恰恰是应用中最不稳定的地方
-
带宽波动很大
- 因此服务之间的数据密集型交互可能会受到影响。
- 并非所有服务都能在同一数据中心内享受快速带宽
- 即使在同一数据中心,它仍然比同一机器上进程之间的通信慢
-
延迟波动很大
- 所有网络延迟都会造成请求的延迟
-
网络故障造成可用性问题
- 网络故障时无法确定它是另一个服务故障还是网络故障
-
从本质上讲,现代应用程序是由分布式和网络化的服务组成的。且每个服务需要维护几个传入和传出连接(集群,来避免网络问题)
阻塞 api 的问题
为每个连接分配一个线程,它更简单。
阻塞 api 浪费资源
阻塞型 API 主要问题是,它为每个传入的连接分配了一个新线程
-
线程绝不是廉价的资源
- 线程需要内存
- 线程越多,对操作系统内核调度器的压力就越大
- 因为它需要为线程提供CPU时间。
-
我们可以通过使用线程池在关闭连接后重用线程来改进
-
但在任何给定的时间点,我们仍然需要为n个连接使用n个线程
被操作系统阻塞的原因:
- 读操作可能正在等待来自网络的数据到达
- 之前的写操作已经将缓冲区填满,写操作可能需要等待缓冲区被清空
bio 的问题
现代操作系统可以正确地处理几千个并发线程。
- 并不是每个网络服务都会面临这么多并发请求的负载
- 但是当我们讨论数以万计的并发连接时,这个模型很快就显示出它的局限性。
我们通常需要比连接更多的线程,栗如某个 API 网关(边缘服务)通过向其他 HTTP 服务请求来实现价格计算
- 按顺序请求每个服务,会变得非常慢
- 启动四个并发请求。这意味着要启动另外四个线程
- 如果我们有1000个并发的网络请求,会使用到 5000 个线程
最后,应用程序经常部署到容器化或虚拟化环境中
- 这意味着它们分配的CPU时间可能有限
- 进程的可用内存也可能受到限制
- 这类应用程序必须与其他应用程序共享CPU资源,如果所有应用程序都使用阻塞I/O api
- 那么很快就会有太多线程需要管理和调度,这就需要在启动更多的服务器/容器实例
使用非阻塞 NIO 异步编程
无需等待 I/O 操作完成,我们可以转向 NIO ,您可能已经尝过了 C 语言中的 select 函数。
NIO 背后的思想是请求阻塞时,继续执行其他任务,直到操作结果准备好。
栗如,一个非阻塞的读取可能会通过网络套接字请求最多256个字节
- 执行线程会做其他事情(比如处理另一个连接)
- 直到数据被放入缓冲区,准备好在内存中使用。
- 在此模型中,许多并发连接可以在单个线程上复用,因为网络延迟通常超过读取传入字节所需的CPU时间
Java 早就有了 nio
多路复用,EventLoop
EventLoop 是一个流行处理异步事件的线程模型。事件被推入 EventLoop,而不是前面 NIO 中那样轮询 SelectKey。
在上图中可以看到
-
事件到达时进行排队
-
它们可以是I/O事件,栗如:
- 数据已经准备好使用
- 或者缓冲区已经完全写入套接字
-
它们也可以是任何其他事件,栗如:
- 计时器触发
-
分配一个线程给 EventLoop 处理接入事件
- 被处理事件不应该执行任何阻塞或长时间运行的操作(只做线程压栈)
- 否则,线程会阻塞,违背了使用 EventLoop 的目的。
Reactive 是什么
到目前为止我们已经讨论了如何做到以下几点:
- 利用异步编程和 NIO 来处理更多并发连接,并使用更少的线程
- 使用一个线程模型进行异步事件处理( EventLoop)
通过结合这两种技术,我们可以构建可伸缩且资源高效的应用程序。现在让我们讨论什么是响应式 Reactive 系统,以及它为何不“仅仅”是异步编程。
Reactive 的四大特性:响应式、弹性、弹性和消息驱动(responsive, resilient, elastic, message-driven)这里简要介绍一下这些属性:
-
Elastic - 弹性是应用程序处理实例的水平扩展能力。即应用程序通过启动新实例和跨实例负载均衡来响应流量峰值。
- 这对代码设计有一些影响,栗如无状态化(识别和限制使用跨实例的状态共享(栗如:服务器端 web session))
- 应用实例需要做健康报告,这样协调器就可以根据网络流量和实例的健康报告的指标决定何时新增启动或停止实例。
-
Resilient - 高可用性是弹性的另一面。当一个实例崩溃时,可以通过将流量重定向到其他实例来实现弹性,并且可以在必要时启动一个新实例。
- 当一个实例由于某些条件而不能满足请求时,它仍然尝试以 降级模式(degraded mode)响应
- 根据应用程序域的不同,可以使用较旧的缓存值进行响应
- 甚至可以使用空数据或默认数据进行响应
- 也可以将请求转发到其他非错误实例
- 在最坏的情况下,可以及时的响应错误信息
-
Responsive - 适应性是水平扩展 elasticity 和高可用性 resiliency 结合的结果。是一种强大的服务性能级别上的协议保证,一致的响应时间。 这归功于:
-
在需要时启动新实例的能力(以保持可接受的响应时间)
-
在出现错误时实例仍能快速响应。
-
需要注意的是,
- 如果一个组件依赖于不可伸缩的资源(如单个中央数据库)
- 则不可能实现响应
- 如果所有实例都向一个正在快速运行的资源发出请求
- 那么启动更多实例并不能解决问题
-
-
Message-driven - 使用异步消息传递而不是像远程过程调用这样的阻塞范式是实现水平扩展 elasticity 和高可用 resiliency 的关键。
- 这使消息能够被分派到更多的实例(使系统具有弹性)
- 并控制消息生产者和消息消费者之间的流(back-pressure)
Reactive 系统表现出这四种特性,这使得系统可靠且资源高效。
异步和 Reactive 的区别?
Reactive 意味着异步,但反过来不一定是正确的。
举个栗子,考虑一个购物 web 应用程序,用户可以将商品放入购物车。这通常是通过在 服务器端 存储 session 来实现的。只要 session 存储在内存或本地文件中时,这个系统就不是 Reactive 的,即使系统内部使用 NI/O 和异步编程。因为,应用程序的一个实例不能被另一个实例接管,因为 session 是应用程序级的状态,且该状态不会在节点之间复制和共享。
这个栗子的一个 Reactive 变体是使用内存网格服务(例如Hazelcast,
Redis或Infinispan)来存储 web session,这样传入的请求可以路由到任何实例。
Reactive 还意味着什么?
-
系统层面:Reactive 还意味著:可靠,消息驱动、水平扩展、高可用的和适应性。
-
编码层面:Reactive 即对变化和事件反应的手段。Excel 是 Reactive 编程的一个很好的例子: 当单元格数据发生变化时,其他相关公式单元格将自动重新计算。在本书的后面部分,您将看到RxJava,一个流行的 Reactive 的,对 Java 的 Reactive API 的扩展,它帮助协调异步事件和数据处理。还有函数式编程,这是一种编程风格,我们不会在本书中讨论,Stephen的函数式响应式编程就是针对这种风格 。
- 参考:Stephen Blackheath and Anthony Jones (Manning, 2016)
-
数据层面:当系统处理连续的 stream 数据,经典的生产者/消费者问题就出现了。
- 特别是要提供反向压力(back-pressure)机制,以便消费者可以在消费过快时通知生产者。
- 反应流
- 主要目标是达到系统之间的最佳吞吐量。
Vert.x是什么?
根据 Vertx 官方的说法。
Eclipse Vert.x是一个在 JVM 上构建 Reactive 应用程序的工具包。
.........