pull/1/head
595208882@qq.com 3 years ago
parent 52b34dc5a6
commit 60e153b6fa

@ -4,6 +4,10 @@
[TOC]
https://mp.weixin.qq.com/s?__biz=MzI4NjI1OTI4Nw==&mid=2247489183&idx=1&sn=02ab3551c473bd2c8429862e3689a94b&chksm=ebdef7a7dca97eb17194c3d935c86ade240d3d96bbeaf036233a712832fb94af07adeafa098b&mpshare=1&scene=23&srcid=0812OQ78gD47QJEguFGixUVa&sharer_sharetime=1628761112690&sharer_shareid=0f9991a2eb945ab493c13ed9bfb8bf4b#rd
# JVM
## JVM常量池

@ -2775,6 +2775,434 @@ Apache Dubbo 是一款高性能、轻量级的开源 Java 服务框架。Apache
![ApacheDubbo](images/Middleware/ApacheDubbo.jpg)
## RPC
Remote Procedure Call Protocol 既 **远程过程调用**,一种能让我们**像调用本地服务一样调用远程服务**,可以让调用者对网络通信这些细节**无感知**,比如服务消费方在执行 helloWorldService.sayHello("sowhat") 时,实质上调用的是远端的服务。这种方式其实就是**RPC****RPC**思想在各大互联网公司中被广泛使用,如阿里巴巴的**dubbo**、当当的**Dubbox** 、Facebook 的 **thrift**、Google 的**grpc**、Twitter的**finagle**等。
## 框架设计
### Dubbo 简介
Dubbo 是阿里巴巴研发开源工具主要分为2.6.x 跟 2.7.x 版本。是一款分布式、高性能、透明化的 RPC 服务框架提供服务自动注册、自动发现等高效服务治理方案可以和Spring 框架无缝集成它提供了6大核心能力
- 面向接口代理的高性能RPC调用
- 智能容错和负载均衡
- 服务自动注册和发现
- 高度可扩展能力
- 运行期流量调度
- 可视化的服务治理与运维
**调用过程**
- 服务提供者 **Provider** 启动然后向 **Registry** 注册自己所能提供的服务。
- 服务消费者 Consumer 向**Registry**订阅所需服务,**Consumer** 解析**Registry**提供的元信息,从服务中通过负载均衡选择 **Provider**调用。
- 服务提供方 **Provider** 元数据变更的话**Registry**会把变更推送给**Consumer**,以此保证**Consumer**获得最新可用信息。
**注意点**
- **Provider****Consumer** 在内存中记录调用次数跟时间,定时发送统计数据到**Monitor**,发送的时候是**短**连接。
- **Monitor****Registry** 是可选的,可直接在配置文件中写好,**Provider** 跟 **Consumer**进行直连。
- **Monitor****Registry** 挂了也没事, **Consumer** 本地缓存了 **Provider** 信息。
- **Consumer** 直接调用 **Provider** 不会经过 **Registry**。**Provider**、**Consumer**这俩到 **Registry**之间是长连接。
### Dubbo框架分层
![Dubbo框架分层](images/Middleware/Dubbo框架分层.jpg)
如上图,总的而言 Dubbo 分为三层。
- **Busines**层:由用户自己来提供接口和实现还有一些配置信息。
- **RPC**真正的RPC调用的核心层封装整个RPC的调用过程、负载均衡、集群容错、代理。
- **Remoting**层:对网络传输协议和数据转换的封装。
如果每一层再细分下去,一共有十层:
1. 接口服务层Service该层与业务逻辑相关根据 provider 和 consumer 的业务设计对应的接口和实现。
2. 配置层Config对外配置接口以 ServiceConfig 和 ReferenceConfig 为中心初始化配置。
3. 服务代理层Proxy服务接口透明代理Provider跟Consumer都生成代理类使得服务接口透明代理层实现服务调用跟结果返回。
4. 服务注册层Registry封装服务地址的注册和发现以服务 URL 为中心。
5. 路由层Cluster封装多个提供者的路由和负载均衡并桥接注册中心以Invoker 为中心,扩展接口为 Cluster、Directory、Router 和 LoadBlancce。
6. 监控层MonitorRPC 调用次数和调用时间监控,以 Statistics 为中心,扩展接口为 MonitorFactory、Monitor 和 MonitorService。
7. 远程调用层Protocal封装 RPC 调用,以 Invocation 和 Result 为中心,扩展接口为 Protocal、Invoker 和 Exporter。
8. 信息交换层Exchange封装请求响应模式同步转异步。以 Request 和Response 为中心,扩展接口为 Exchanger、ExchangeChannel、ExchangeClient 和 ExchangeServer。
9. 网络传输层Transport抽象 mina 和 netty 为统一接口,以 Message 为中心,扩展接口为 Channel、Transporter、Client、Server 和 Codec。
10. 数据序列化层Serialize可复用的一些工具扩展接口为 Serialization、ObjectInput、ObjectOutput 和 ThreadPool。
他们之间的调用关系直接看下面官网图即可:
![Dubbo调用关系](images/Middleware/Dubbo调用关系.jpg)
## SPI机制
**Dubbo** 采用 **微内核设计** + **SPI** 扩展技术来搭好核心框架,同时满足用户定制化需求。这里重点说下**SPI**。
### SPI 含义
**SPI** 全称为 **Service Provider Interface**,是一种**服务发现机制**。它约定在**ClassPath**路径下的**META-INF/services**文件夹查找文件,自动加载文件里所定义的类。
### Java SPI缺点
- 不能按需加载Java SPI在加载扩展点的时候会一次性加载所有可用的扩展点很多是不需要的会浪费系统资源。
- 获取某个实现类的方式不够灵活,只能通过 Iterator 形式获取,不能根据某个参数来获取对应的实现类。
- 不支持AOP与依赖注入JAVA SPI可能会丢失加载扩展点异常信息导致追踪问题很困难。
### Dubbo SPI
JDK自带的不好用Dubbo 就自己实现了一个 SPI该SPI **可以通过名字实例化指定的实现类,并且实现了 IOC 、AOP 与 自适应扩展 SPI** 。
```properties
key = com.sowhat.value
```
Dubbo 对配置文件目录的约定,不同于 Java SPI Dubbo 分为了三类目录。
- META-INF/services/ :该目录下 SPI 配置文件是为了用来兼容 Java SPI
- META-INF/dubbo/ :该目录存放用户自定义的 SPI 配置文件
- META-INF/dubbo/internal/ :该目录存 Dubbo 内部使用的 SPI 配置文件
### Dubbo SPI源码追踪
**ExtensionLoader.getExtension** 方法的整个思路是 查找缓存是否存在不存在则读取SPI文件通过反射创建类然后设置依赖注入这些东西有包装类就包装下执行流程如下图所示
![DubboSPI源码追踪](images/Middleware/DubboSPI源码追踪.png)
### injectExtension IOC
查找 set 方法,根据参数找到依赖对象则注入。
### WrapperClass AOP
包装类Dubbo帮你自动包装只需某个扩展类的构造函数只有一个参数并且是扩展接口类型就会被判定为包装类。
### Activate
Active 有三个属性group 表示修饰在哪个端,是 provider 还是 consumervalue 表示在 URL参数中出现才会被激活order 表示实现类的顺序。
### Adaptive自适应扩展
**需求**:根据配置来进行 SPI 扩展的加载后不想在启动的时候让扩展被加载,想根据请求时候的参数来动态选择对应的扩展。**实现**Dubbo用代理机制实现了自适应扩展为用户想扩展的接口 通过JDK 或者 Javassist 编译生成一个代理类,然后通过反射创建实例。实例会根据本来方法的请求参数得知需要的扩展类,然后通过 ExtensionLoader.getExtensionLoader(type.class).getExtension(name)来获取真正的实例来调用,看个官网样例。
```java
public interface WheelMaker {
Wheel makeWheel(URL url);
}
// WheelMaker 接口的自适应实现类
public class AdaptiveWheelMaker implements WheelMaker {
public Wheel makeWheel(URL url) {
if (url == null) {
throw new IllegalArgumentException("url == null");
}
// 1. 调用 url 的 getXXX 方法获取参数值
String wheelMakerName = url.getParameter("Wheel.maker");
if (wheelMakerName == null) {
throw new IllegalArgumentException("wheelMakerName == null");
}
// 2. 调用 ExtensionLoader 的 getExtensionLoader 获取加载器
// 3. 调用 ExtensionLoader 的 getExtension 根据从url获取的参数作为类名称加载实现类
WheelMaker wheelMaker = ExtensionLoader.getExtensionLoader(WheelMaker.class).getExtension(wheelMakerName);
// 4. 调用实现类的具体方法实现调用。
return wheelMaker.makeWheel(URL url);
}
}
```
查看Adaptive注解源码可知该注解可用在**类**或**方法**上Adaptive 注解在类上或者方法上有不同的实现逻辑。
#### Adaptive注解在类上
Adaptive 注解在类上时Dubbo 不会为该类生成代理类Adaptive 注解在类上的情况很少,在 Dubbo 中,仅有两个类被 Adaptive 注解了,分别是 AdaptiveCompiler 和 AdaptiveExtensionFactory表示拓展的加载逻辑由人工编码完成这不是我们关注的重点。
#### Adaptive注解在方法上
Adaptive注解在方法上时Dubbo则会为该方法生成代理逻辑表示拓展的加载逻辑需由框架自动生成实现机制如下
- 加载标注有 @Adaptive 注解的接口,如果不存在,则不支持 Adaptive 机制
- 为目标接口按照一定的模板生成子类代码,并且编译生成的代码,然后通过反射生成该类的对象
- 结合生成的对象实例通过传入的URL对象获取指定key的配置然后加载该key对应的类对象最终将调用委托给该类对象进行
```java
@SPI("apple")
public interface FruitGranter {
Fruit grant();
@Adaptive
String watering(URL url);
}
---
// 苹果种植者
public class AppleGranter implements FruitGranter {
@Override
public Fruit grant() {
return new Apple();
}
@Override
public String watering(URL url) {
System.out.println("watering apple");
return "watering finished";
}
}
---
// 香蕉种植者
public class BananaGranter implements FruitGranter {
@Override
public Fruit grant() {
return new Banana();
}
@Override
public String watering(URL url) {
System.out.println("watering banana");
return "watering success";
}
}
```
调用方法实现:
```java
public class ExtensionLoaderTest {
@Test
public void testGetExtensionLoader() {
// 首先创建一个模拟用的URL对象
URL url = URL.valueOf("dubbo://192.168.0.1:1412?fruit.granter=apple");
// 通过ExtensionLoader获取一个FruitGranter对象
FruitGranter granter = ExtensionLoader.getExtensionLoader(FruitGranter.class)
.getAdaptiveExtension();
// 使用该FruitGranter调用其"自适应标注的"方法,获取调用结果
String result = granter.watering(url);
System.out.println(result);
}
}
```
通过如上方式生成一个内部类。
## 服务暴露流程
### 服务暴露总览
**Dubbo**框架是以**URL**为总线的模式,运行过程中所有的状态数据信息都可以通过**URL**来获取,比如当前系统采用什么序列化,采用什么通信,采用什么负载均衡等信息,都是通过**URL**的参数来呈现的,所以在框架运行过程中,运行到某个阶段需要相应的数据,都可以通过对应的**Key**从**URL**的参数列表中获取。**URL** 具体的参数如下:
- protocol指的是 dubbo 中的各种协议dubbo thrift http
- username/password用户名/密码
- host/port主机/端口
- path接口的名称
- parameters参数键值对
```properties
protocol://username:password@host:port/path?k=v
```
服务暴露从代码流程看分为三部分:
- 检查配置,最终组装成 **URL**
- 暴露服务到到本地服务跟远程服务。
- 服务注册至注册中心。
服务暴露从对象构建转换看分为两步:
- 将服务封装成**Invoker**。
- 将**Invoker**通过协议转换为**Exporter**。
### 服务暴露源码追踪
- 容器启动Spring IOC刷新完毕后调用 onApplicationEvent 开启服务暴露ServiceBean
- export 跟 doExport 来进行拼接构建URL为屏蔽调用的细节统一暴露出一个可执行体通过ProxyFactory 获取到 invoker
- 调用具体 Protocol 将把包装后的 invoker 转换成 exporter此处用到了SPI
- 然后启动服务器server监听端口使用NettyServer创建监听服务器
- 通过 RegistryProtocol 将URL注册到注册中心使得consumer可获得provider信息
## 服务引用流程
Dubbo中一个可执行体就是一个invoker所以 provider 跟 consumer 都要向 invoker 靠拢。通过上面demo可知为了无感调用远程接口底层需要有个代理类包装 invoker。
**服务的引入时机有两种**
- 饿汉式
通过实现 Spring 的 InitializingBean 接口中的 afterPropertiesSet 方法,容器通过调用 **ReferenceBean**的 afterPropertiesSet 方法时引入服务。
- 懒汉式(默认)
懒汉式是只有当服务被注入到其他类中时启动引入流程。
**服务引用的三种方式**
- **本地引入**:服务暴露时本地暴露,避免网络调用开销
- **直接连接引入远程服务**:不启动注册中心,直接写死远程**Provider**地址 进行直连
- **通过注册中心引入远程服务**:通过注册中心抉择如何进行负载均衡调用远程服务
**服务引用流程**
- 检查配置构建map map 构建 URL 通过URL上的协议利用自适应扩展机制调用对应的 protocol.refer 得到相应的 invoker ,此处
- 想注册中心注册自己然后订阅注册中心相关信息得到provider的 ip 等信息再通过共享的netty客户端进行连接。
- 当有多个 URL 时,先遍历构建出 invoker 然后再由 **StaticDirectory** 封装一下,然后通过 cluster 进行合并,只暴露出一个 invoker 。
- 然后再构建代理,封装 invoker 返回服务引用,之后 Comsumer 调用的就是这个代理类。
**调用方式**
- oneway不关心请求是否发送成功。
- Async异步调用Dubbo天然异步客户端调用请求后将返回的 ResponseFuture 存到上下文中,用户可随时调用 future.get 获取结果。异步调用通过唯一**ID** 标识此次请求。
- Sync同步调用在 Dubbo 源码中就调用了 future.get用户感觉方法被阻塞了必须等结果后才返回。
## 调用整体流程
**调用之前你可能需要考虑这些事**
- consumer 跟 provider 约定好通讯协议dubbo支持多种协议比如dubbo、rmi、hessian、http、webservice等。默认走dubbo协议连接属于**单一长连接****NIO异步通信**。适用传输数据量很小(单次请求在100kb以内),但是并发量很高。
- 约定序列化模式,大致分为两大类,一种是字符型(XML或json 人可看懂 但传输效率低),一种是二进制流(数据紧凑,机器友好)。默认使用 hessian2作为序列化协议。
- consumer 调用 provider 时提供对应接口、方法名、参数类型、参数值、版本号。
- provider列表对外提供服务涉及到负载均衡选择一个provider提供服务。
- consumer 跟 provider 定时向monitor 发送信息。
**调用大致流程**
- 客户端发起请求来调用接口接口调用生成的代理类。代理类生成RpcInvocation 然后调用invoke方法。
- ClusterInvoker获得注册中心中服务列表通过负载均衡给出一个可用的invoker。
- 序列化跟反序列化网络传输数据。通过NettyServer调用网络服务。
- 服务端业务线程池接受解析数据从exportMap找到invoker进行invoke。
- 调用真正的Impl得到结果然后返回。
**调用方式**
- oneway不关心请求是否发送成功消耗最小。
- sync同步调用在 Dubbo 源码中就调用了 future.get用户感觉方法被阻塞了必须等结果后才返回。
- Async 异步调用Dubbo天然异步客户端调用请求后将返回的 ResponseFuture 存到上下文中用户可以随时调用future.get获取结果。异步调用通过**唯一ID**标识此次请求。
## 集群容错负载均衡
Dubbo 引入了**Cluster**、**Directory**、**Router**、**LoadBalance**、**Invoker**模块来保证Dubbo系统的稳健性关系如下图
![Dubbo集群容错负载均衡](images/Middleware/Dubbo集群容错负载均衡.jpg)
- 服务发现时会将多个多个远程调用放入**Directory**,然后通过**Cluster**封装成一个**Invoker**,该**invoker**提供容错功能
- 消费者代用的时候从**Directory**中通过负载均衡获得一个可用**invoker**,最后发起调用
- 你可以认为**Dubbo**中的**Cluster**对上面进行了大的封装,自带各种鲁棒性功能
### 集群容错
集群容错是在消费者端通过**Cluster**子类实现的Cluster接口有10个实现类每个**Cluster**实现类都会创建一个对应的**ClusterInvoker**对象。核心思想是**让用户选择性调用这个Cluster中间层屏蔽后面具体实现细节**。
| Cluster | Cluster Invoker | 作用 |
| :--------------- | :---------------------- | :---------------------------- |
| FailoverCluster | FailoverClusterInvoker | 失败自动切换功能,**默认** |
| FailfastCluster | FailfastClusterInvoker | 一次调用,失败异常 |
| FailsafeCluster | FailsafeClusterInvoker | 调用出错则日志记录 |
| FailbackCluster | FailbackClusterInvoker | 失败返空定时重试2次 |
| ForkingCluster | ForkingClusterInvoker | 一个任务并发调用一个OK则OK |
| BroadcastCluster | BroadcastClusterInvoker | 逐个调用invoker全可用才可用 |
| AvailableCluster | AvailableClusterInvoker | 哪个能用就用那个 |
| MergeableCluster | MergeableClusterInvoker | 按组合并返回结果 |
### 智能容错之负载均衡
Dubbo中一般有4种负载均衡策略。
- **RandomLoadBalance**:加权随机,它的算法思想简单。假设有一组服务器 servers = [A, B, C],对应权重为 weights = [5, 3, 2]权重总和为10。现把这些权重值平铺在一维坐标值上[0, 5) 区间属于服务器 A[5, 8) 区间属于服务器 B[8, 10) 区间属于服务器 C。接下来通过随机数生成器生成一个范围在 [0, 10) 之间的随机数,然后计算这个随机数会落到哪个区间上。**默认实现**。
- **LeastActiveLoadBalance**:最少活跃数负载均衡,选择现在活跃调用数最少的提供者进行调用,活跃的调用数少说明它现在很轻松,而且活跃数都是从 0 加起来的,来一个请求活跃数+1一个请求处理完成活跃数-1所以活跃数少也能变相的体现处理的快。
- **RoundRobinLoadBalance**:加权轮询负载均衡,比如现在有两台服务器 A、B轮询的调用顺序就是 A、B、A、B如果加了权重A 比B 的权重是2:1那现在的调用顺序就是 A、A、B、A、A、B。
- **ConsistentHashLoadBalance**:一致性 Hash 负载均衡,将服务器的 IP 等信息生成一个 hash 值将hash 值投射到圆环上作为一个节点,然后当 key 来查找的时候顺时针查找第一个大于等于这个 key 的 hash 值的节点。一般而言还会引入虚拟节点,使得数据更加的分散,避免数据倾斜压垮某个节点。如下图 Dubbo 默认搞了 160 个虚拟节点。
![ConsistentHashLoadBalance](images/Middleware/ConsistentHashLoadBalance.png)
### 智能容错之服务目录
关于 服务目录Directory 你可以理解为是相同服务Invoker的集合核心是RegistryDirectory类。具有三个功能。
- 从注册中心获得invoker列表
- 监控着注册中心invoker的变化invoker的上下线
- 刷新invokers列表到服务目录
### 智能容错之服务路由
服务路由其实就是路由规则,它规定了服务消费者可以调用哪些服务提供者。条件路由规则由两个条件组成,分别用于对服务消费者和提供者进行匹配。比如有这样一条规则:
```properties
host = 10.20.153.14 => host = 10.20.153.12
```
该条规则表示 IP 为 10.20.153.14 的服务消费者只可调用 IP 为 10.20.153.12 机器上的服务,不可调用其他机器上的服务。条件路由规则的格式如下:
```properties
[服务消费者匹配条件] => [服务提供者匹配条件]
```
如果服务消费者匹配条件为空,表示不对服务消费者进行限制。如果服务提供者匹配条件为空,表示对某些服务消费者禁用服务。
## 设计RPC
一个RPC框架大致需要以下功能
1. 服务的注册与发现
2. 用动态代理
3. 负载均衡LoadBalance
4. 通信协议
5. 序列化与反序列化
6. 网络通信(Netty)
7. Monitor
# Nacos

98
OS.md

@ -33,7 +33,10 @@ Linux/Unix常见IO模型**阻塞Blocking I/O**、**非阻塞Non-Bloc
### 阻塞I/O
当用户程序执行 `read` ,线程会被阻塞,一直等到内核数据准备好,并把数据从内核缓冲区拷贝到应用程序的缓冲区中,当拷贝过程完成,`read` 才会返回。阻塞等待的是 **内核数据准备好** 和 **数据从内核态拷贝到用户态** 两个过程。过程如下图:
阻塞IO情况下当用户调用`read`后,用户线程会被阻塞,等内核数据准备好并且数据从内核缓冲区拷贝到用户态缓存区后`read`才会返回。阻塞分两个阶段:
- **等待CPU把数据从磁盘读到内核缓冲区**
- **等待CPU把数据从内核缓冲区拷贝到用户缓冲区**
![阻塞IO](images/OS/阻塞IO.png)
@ -41,7 +44,9 @@ Linux/Unix常见IO模型**阻塞Blocking I/O**、**非阻塞Non-Bloc
### 非阻塞I/O
非阻塞的 `read` 请求在数据未准备好的情况下立即返回,可以继续往下执行,此时应用程序不断轮询内核,直到数据准备好,内核将数据拷贝到应用程序缓冲区,`read` 调用才可以获取到结果。过程如下图:
非阻塞的 `read` 请求在数据未准备好的情况下立即返回可以继续往下执行此时应用程序不断轮询内核询问数据是否准备好当数据没有准备好时内核立即返回EWOULDBLOCK错误。直到数据准备好后内核将数据拷贝到应用程序缓冲区`read` 请求才获取到结果。
**注意**:这里最后一次 `read` 调用获取数据的过程,是一个**同步的过程**,是需要等待的过程。这里的同步指的是**内核态的数据拷贝到用户程序的缓存区这个过程**。
![非阻塞IO](images/OS/非阻塞IO.png)
@ -49,6 +54,14 @@ Linux/Unix常见IO模型**阻塞Blocking I/O**、**非阻塞Non-Bloc
### I/O多路复用
非阻塞情况下无可用数据时应用程序每次轮询内核看数据是否准备好了也耗费CPU能否不让它轮询当内核缓冲区数据准备好了以事件通知当机制告知应用进程数据准备好了呢应用进程在没有收到数据准备好的事件通知信号时可以忙写其他的工作。此时**IO多路复用**就派上用场了。像**select、poll、epoll** 都是I/O多路复用的具体的实现。
![IO-多路复用](images/OS/IO-多路复用.png)
### 同步I/O
无论 `read``send``阻塞I/O`,还是 `非阻塞I/O` 都是同步调用。因为在 `read` 调用时,内核将数据从内核空间拷贝到用户空间的过程都是需要等待的,即这个过程是同步的,如果内核实现的拷贝效率不高,`read` 调用就会在这个同步过程中等待比较长的时间。
@ -239,8 +252,6 @@ Proactor 模式的示意图如下:
## select/poll/epoll
select/poll/epoll对比
![select、poll、epoll对比](images/OS/select、poll、epoll对比.png)
**注意****遍历**相当于查看所有的位置,**回调**相当于查看对应的位置。
@ -251,22 +262,19 @@ select/poll/epoll对比
![select工作流程](images/OS/select工作流程.jpg)
POSIX所规定目前几乎在所有的平台上支持其良好跨平台支持也是它的一个优点本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理
`select` 本质上是通过设置或者检查存放 `fd` 标志位的数据结构来进行下一步处理。
**缺点**
- 单个进程可监视的fd数量被限制即能监听端口的数量有限,数值存在如下文件里:`cat /proc/sys/fs/file-max`
- 对socket是线性扫描即采用轮询的方法效率较低
- select采取了内存拷贝方法来实现内核将FD消息通知给用户空间这样一个用来存放大量fd的数据结构这样会使得用户空间和内核空间在传递该结构时复制开销大
- **单个进程可监视的`fd`数量被限制**。能监听端口的数量有限,数值存在文件:`cat /proc/sys/fs/file-max`
- **需要维护一个用来存放大量fd的数据结构**。这样会使得用户空间和内核空间在传递该结构时复制开销大
- **对fd进行扫描时是线性扫描**。`fd`剧增后,`IO`效率较低,因为每次调用都对`fd`进行线性扫描遍历,所以随着`fd`的增加会造成遍历速度慢的性能问题
- **`select()`函数的超时参数在返回时也是未定义的**。考虑到可移植性,每次在超时之后在下一次进入到`select`之前都需要重新设置超时参数
**优点**
select是第一版IO复用提出后暴漏了很多问题。
- select 会修改传入的参数数组,这个对于一个需要调用很多次的函数,是非常不友好的
- select 如果任何一个sock(I/O stream)出现了数据select 仅仅会返回但不会告诉是那个sock上有数据只能自己遍历查找
- select 只能监视1024个链接
- select 不是线程安全的如果你把一个sock加入到select, 然后突然另外一个线程发现这个sock不用要收回这个select 不支持的
- **`select()`的可移植性更好**。在某些`Unix`系统上不支持`poll()`
- **`select()`对于超时值提供了更好的精度:微秒**。而`poll`是毫秒
@ -274,21 +282,18 @@ select是第一版IO复用提出后暴漏了很多问题。
![poll工作流程](images/OS/poll工作流程.jpg)
本质上和select没有区别它将用户传入的数组拷贝到内核空间然后查询每个fd对应的设备状态
- 其没有最大连接数的限制,原因是它是基于链表来存储的
- 大量的fd的数组被整体复制于用户态和内核地址空间之间而不管这样的复制是不是有意义
- poll特点是“水平触发”如果报告了fd后没有被处理那么下次poll时会再次报告该fd
- 边缘触发只通知一次epoll用的就是边缘触发
poll本质上和select没有区别它将用户传入的数组拷贝到内核空间然后查询每个fd对应的设备状态如果设备就绪则在设备等待队列中加入一项并继续遍历如果遍历完所有fd后没有发现就绪设备则挂起当前进程直到设备就绪或者主动超时被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。poll还有一个特点是“水平触发”如果报告了fd后没有被处理那么下次poll时会再次报告该fd。
**缺点**
poll 修复了 select 的很多问题:
- 大量的fd的数组被整体复制于用户态和内核地址空间之间而不管这样的复制是不是有意义
- 与select一样poll返回后需要轮询pollfd来获取就绪的描述符
- poll 去掉了1024个链接的限制
- poll 从设计上来说不再修改传入数组
**优点**
但是poll仍然不是线程安全的 这就意味着不管服务器有多强悍,你也只能在一个线程里面处理一组 I/O 流。你当然可以拿多进程来配合了,不过然后你就有了多进程的各种问题。
- poll() 不要求开发者计算最大文件描述符加一的大小
- poll() 在应付大数目的文件描述符的时候速度更快相比于select
- 它没有最大连接数的限制,原因是它是基于链表来存储的
@ -296,41 +301,48 @@ poll 修复了 select 的很多问题:
![epoll工作流程](images/OS/epoll工作流程.jpg)
在Linux2.6内核中提出的select和poll的增强版本
epoll支持水平触发和边缘触发最大的特点在于边缘触发它只告诉进程哪些fd刚刚变为就需态并且只会通知一次。还有一个特点是epoll使用“事件”的就绪通知方式通过epoll_ctl注册fd一旦该fd就绪内核就会采用类似callback的回调机制来激活该fdepoll_wait便可以收到通知。
- 支持水平触发和边缘触发最大的特点在于边缘触发它只告诉进程哪些fd刚刚变为就绪态并且只会通知一次
- 使用“事件”的就绪通知方式通过epoll_ctl注册fd一旦该fd就绪内核就会采用类似callback的回调机制来激活该fdepoll_wait便可以收到通知
**优点**
- 没有最大并发连接的限制能打开的FD的上限远大于1024(1G的内存能监听约10万个端口)
- 效率提升非轮询的方式不会随着FD数目的增加而效率下降只有活跃可用的FD才会调用callback函数即epoll最大的优点就在于它只管理“活跃”的连接而跟连接总数无关
- 内存拷贝利用mmap()文件映射内存加速与内核空间的消息传递即epoll使用mmap减少复制开销
- 文件映射内存直接通过地址空间访问,效率更高,把文件映射到内存中
- **支持一个进程打开大数目的socket描述符(FD)**
select最不能忍受的是一个进程所打开的FD是有一定限制的由FD_SETSIZE设置默认值是1024/2048。对于那些需要支持的上万连接数目的IM服务器来说显然太少了。这时候你一是可以选择修改这个宏然后重新编译内核。不过 epoll则没有这个限制它所支持的FD上限是最大可以打开文件的数目这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左右具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。
- **IO效率不随FD数目增加而线性下降**
传统的select/poll另一个致命弱点就是当你拥有一个很大的socket集合不过由于网络延时任一时间只有部分的socket是"活跃"的但是select/poll每次调用都会线性扫描全部的集合导致效率呈现线性下降。但是epoll不存在这个问题它只会对"活跃"的socket进行操作---这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的。那么只有"活跃"的socket才会主动的去调用 callback函数其他idle状态socket则不会在这点上epoll实现了一个"伪"AIO因为这时候推动力在Linux内核。
epoll 可以说是 I/O 多路复用最新的一个实现epoll 修复了poll 和select绝大部分问题 比如:
- **使用mmap加速内核与用户空间的消息传递**
- epoll 现在是线程安全的
- epoll 现在不仅告诉你sock组里面数据还会告诉你具体哪个sock有数据你不用自己去找了
- epoll 内核态管理了各种IO文件描述符 以前用户态发送所有文件描述符到内核态然后内核态负责筛选返回可用数组现在epoll模式下所有文件描述符在内核态有存查询时不用传文件描述符进去了
这点实际上涉及到epoll的具体实现了。无论是select,poll还是epoll都需要内核把FD消息通知给用户空间如何避免不必要的内存拷贝就很重要在这点上epoll是通过内核与用户空间mmap同一块内存实现的。
## BIO(同步阻塞I/O)
每个客户端的Socket连接请求服务端都会对应有个处理线程与之对应对于没有分配到处理线程的连接就会被阻塞或者拒绝。相当于是`一个连接一个线程`。
用户需要等待read将socket中的数据读取到buffer后才继续处理接收的数据。整个IO请求的过程中用户线程是被阻塞的这导致用户在发起IO请求时不能做任何事情对CPU的资源利用率不够。
![同步阻塞IO](images/OS/同步阻塞IO.png)
**特点:**I/O执行的两个阶段进程都是阻塞的。
- 使用一个独立的线程维护一个socket连接随着连接数量的增多对虚拟机造成一定压力
- 使用流来读取数据,流是阻塞的,当没有可读/可写数据时,线程等待,会造成资源的浪费
**优点**
- 能够及时的返回数据,无延迟
- 程序简单进程挂起基本不会消耗CPU时间
**缺点**
- I/O等待对性能影响较大
@ -340,6 +352,18 @@ epoll 可以说是 I/O 多路复用最新的一个实现epoll 修复了poll
## NIO(同步非阻塞I/O)
服务器端保存一个Socket连接列表然后对这个列表进行轮询
- 如果发现某个Socket端口上有数据可读时说明读就绪则调用该Socket连接的相应读操作
- 如果发现某个Socket端口上有数据可写时说明写就绪则调用该Socket连接的相应写操作
- 如果某个端口的Socket连接已经中断则调用相应的析构方法关闭该端口
这样能充分利用服务器资源效率得到了很大提高在进行I/O操作请求时候再用个线程去处理是`一个请求一个线程`。Java中使用Selector、Channel、Buffer来实现上述效果。
- `Selector`Selector允许单线程处理多个Channel。如果应用打开了多个连接通道但每个连接的流量都很低使用Selector就会很方便。要使用Selector得向Selector注册Channel然后调用他的select方法这个方法会一直阻塞到某个注册的通道有事件就绪。一旦这个方法返回线程就可以处理这些事件事件的例子入有新连接接进来数据接收等。
- `Channel`基本上所有的IO在NIO中都从一个Channel开始。Channel有点像流数据可以从channel**读**到buffer也可以从buffer**写**到channel。
- `Buffer`:缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个容器对象(含数组)该对象提供了一组方法可以更轻松的使用内存块缓冲区对象内置了一些机制能够跟踪和记录缓冲区的状态变换情况Channel提供从文件网络读取数据的渠道但是读取或者写入的数据都必须经由Buffer。
用户需要不断地调用read尝试读取socket中的数据直到读取成功后才继续处理接收的数据。整个IO请求过程中虽然用户线程每次发起IO请求后可以立即返回但为了等到数据仍需要不断地轮询、重复请求消耗了大量的CPU的资源。
![同步非阻塞IO](images/OS/同步非阻塞IO.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 98 KiB

File diff suppressed because one or more lines are too long
Loading…
Cancel
Save