更新版本1.1

pull/2/head
wuyou 4 years ago
parent c3083898dc
commit a47476f9bb

@ -1,10 +1,10 @@
![](lemon.png)
# lemon <small>1.0</small>
# lemon <small>1.1</small>
> 一款优秀的开源笔记
- 操作系统、算法、解决方案
- JAVA、JVM、数据库、中间件
- 架构、DevOps、大数据
- 操作系统、算法、解决方案、学习笔记
- 架构、DevOps、大数据、开放性问题
[回到博客](https://view6view.club/)
[点击进入](./README.md)

@ -9,5 +9,6 @@
* [✍ 大数据](src/BigData/README)
* [✍ DevOps](src/DevOps/README)
* [✍ 解决方案](src/Solution/README)
* [✍ 开放性问题](src/Open/README)
* [✍ 科班学习](src/university/README)
* [✍ 其它](src/Others/README)

@ -9,5 +9,6 @@
* [✍ 大数据](src/BigData/README)
* [✍ DevOps](src/DevOps/README)
* [✍ 解决方案](src/Solution/README)
* [✍ 开放性问题](src/Open/README)
* [✍ 科班学习](src/university/README)
* [✍ 其它](src/Others/README)

@ -1,3 +1 @@
在Storm中需要先设计一个实时计算结构我们称之为拓扑topology。之后这个拓扑结构会被提交给集群其中主节点master node负责给工作节点worker node分配代码工作节点负责执行代码。在一个拓扑结构中包含spout和bolt两种角色。数据在spouts之间传递这些spouts将数据流以tuple元组的形式发送而bolt则负责转换数据流。
![Apache-Storm](images/BigData/Apache-Storm.png)
![Apache-Hive](images/BigData/Apache-Hive架构图.png)

@ -1,3 +1,3 @@
Spark Streaming即核心Spark API的扩展不像Storm那样一次处理一个数据流。相反它在处理数据流之前会按照时间间隔对数据流进行分段切分。Spark针对连续数据流的抽象我们称为DStreamDiscretized Stream。 DStream是小批处理的RDD弹性分布式数据集 RDD则是分布式数据集可以通过任意函数和滑动数据窗口窗口计算进行转换实现并行操作
在Storm中需要先设计一个实时计算结构我们称之为拓扑topology。之后这个拓扑结构会被提交给集群其中主节点master node负责给工作节点worker node分配代码工作节点负责执行代码。在一个拓扑结构中包含spout和bolt两种角色。数据在spouts之间传递这些spouts将数据流以tuple元组的形式发送而bolt则负责转换数据流
![Apache-Spark](images/BigData/Apache-Spark.png)
![Apache-Storm](images/BigData/Apache-Storm.png)

@ -1,530 +1,3 @@
Apache Flink是一个**框架**和**分布式处理引擎**,用于在**无界**和**有界**数据流上进行**有状态的计算**。Flink被设计为在所有常见的集群环境中运行以内存中的速度和任何规模执行计算
Spark Streaming即核心Spark API的扩展不像Storm那样一次处理一个数据流。相反它在处理数据流之前会按照时间间隔对数据流进行分段切分。Spark针对连续数据流的抽象我们称为DStreamDiscretized Stream。 DStream是小批处理的RDD弹性分布式数据集 RDD则是分布式数据集可以通过任意函数和滑动数据窗口窗口计算进行转换实现并行操作
![Apache-Flink](images/BigData/Apache-Flink.png)
针对流数据+批数据的计算框架。把批数据看作流数据的一种特例,延迟性较低(毫秒级),且能保证消息传输不丢失不重复。
Flink创造性地统一了流处理和批处理作为流处理看待时输入数据流是无界的而批处理被作为一种特殊的流处理只是它的输入数据流被定义为有界的。Flink程序由Stream和Transformation这两个基本构建块组成其中Stream是一个中间结果数据而Transformation是一个操作它对一个或多个输入Stream进行计算处理输出一个或多个结果Stream。
### 处理无界和有界数据
数据可以作为无界流或有界流被处理:
- **Unbounded streams**(无界流)有一个起点,但没有定义的终点。它们不会终止,而且会源源不断的提供数据。无边界的流必须被连续地处理,即事件达到后必须被立即处理。等待所有输入数据到达是不可能的,因为输入是无界的,并且在任何时间点都不会完成。处理无边界的数据通常要求以特定顺序(例如,事件发生的顺序)接收事件,以便能够推断出结果的完整性。
- **Bounded streams**(有界流)有一个定义的开始和结束。在执行任何计算之前,可以通过摄取(提取)所有数据来处理有界流。处理有界流不需要有序摄取,因为有界数据集总是可以排序的。有界流的处理也称为批处理。
![处理无界和有界数据](images/BigData/处理无界和有界数据.png)
**Apache Flink擅长处理无界和有界数据集**。对时间和状态的精确控制使Flink的运行时能够在无边界的流上运行任何类型的应用程序。有界流由专门为固定大小的数据集设计的算法和数据结构在内部处理从而产生出色的性能。
### 分层API
Flink提供了三层API。每个API在简洁性和表达性之间提供了不同的权衡并且针对不同的使用场景
![Flink-分层API](images/BigData/Flink-分层API.png)
### 应用场景
Apache Flink是开发和运行许多不同类型应用程序的最佳选择因为它具有丰富的特性。Flink的特性包括支持流和批处理、复杂的状态管理、事件处理语义以及确保状态的一致性。此外Flink可以部署在各种资源提供程序上例如YARN、Apache Mesos和Kubernetes也可以作为裸机硬件上的独立集群进行部署。配置为高可用性Flink没有单点故障。Flink已经被证明可以扩展到数千个内核和TB级的应用程序状态提供高吞吐量和低延迟并支持世界上一些最苛刻的流处理应用程序。
下面是Flink支持的最常见的应用程序类型
- Event-driven Applications事件驱动的应用程序
- Data Analytics Applications数据分析应用程序
- Data Pipeline Applications数据管道应用程序
#### Event-driven Applications
Event-driven Applications事件驱动的应用程序。事件驱动的应用程序是一个有状态的应用程序它从一个或多个事件流中获取事件并通过触发计算、状态更新或外部操作对传入的事件作出反应。
事件驱动的应用程序基于有状态的流处理应用程序。在这种设计中,数据和计算被放在一起,从而可以进行本地(内存或磁盘)数据访问。通过定期将检查点写入远程持久存储,可以实现容错。下图描述了传统应用程序体系结构和事件驱动应用程序之间的区别。
![Event-driven-Applications](images/BigData/Event-driven-Applications.png)
代替查询远程数据库,事件驱动的应用程序在本地访问其数据,从而在吞吐量和延迟方面获得更好的性能。可以定期异步地将检查点同步到远程持久存,而且支持增量同步。不仅如此,在分层架构中,多个应用程序共享同一个数据库是很常见的。因此,数据库的任何更改都需要协调,由于每个事件驱动的应用程序都负责自己的数据,因此更改数据表示或扩展应用程序所需的协调较少。
对于事件驱动的应用程序Flink的突出特性是savepoint。保存点是一个一致的状态镜像可以用作兼容应用程序的起点。给定一个保存点就可以更新或调整应用程序的规模或者可以启动应用程序的多个版本进行A/B测试。
典型的事件驱动的应用程序有:
- 欺诈检测
- 异常检测
- 基于规则的提醒
- 业务流程监控
- Web应用(社交网络)
#### Data Analytics Applications
Data Analytics Applications数据分析应用程序。传统上的分析是作为批处理查询或应用程序对已记录事件的有限数据集执行的。为了将最新数据合并到分析结果中必须将其添加到分析数据集中然后重新运行查询或应用程序结果被写入存储系统或作为报告发出。
有了复杂的流处理引擎分析也可以以实时方式执行。流查询或应用程序不是读取有限的数据集而是接收实时事件流并在使用事件时不断地生成和更新结果。结果要么写入外部数据库要么作为内部状态进行维护。Dashboard应用程序可以从外部数据库读取最新的结果也可以直接查询应用程序的内部状态。
Apache Flink支持流以及批处理分析应用程序如下图所示
![Data-Analytics-Applications](images/BigData/Data-Analytics-Applications.png)
典型的数据分析应用程序有:
- 电信网络质量监控
- 产品更新分析及移动应用实验评估
- 消费者技术中实时数据的特别分析
- 大规模图分析
#### Data Pipeline Applications
Data Pipeline Applications数据管道应用程序。提取-转换-加载ETL是在存储系统之间转换和移动数据的常用方法。通常会定期触发ETL作业以便将数据从事务性数据库系统复制到分析数据库或数据仓库。
数据管道的作用类似于ETL作业。它们转换和丰富数据并可以将数据从一个存储系统移动到另一个存储系统。但是它们以连续流模式运行而不是周期性地触发。因此它们能够从不断产生数据的源读取记录并以低延迟将其移动到目的地。例如数据管道可以监视文件系统目录中的新文件并将它们的数据写入事件日志。另一个应用程序可能将事件流物化到数据库或者增量地构建和完善搜索索引。
下图描述了周期性ETL作业和连续数据管道之间的差异
![Data-Pipeline-Applications](images/BigData/Data-Pipeline-Applications.png)
与周期性ETL作业相比连续数据管道的明显优势是减少了将数据移至其目的地的等待时间。此外数据管道更通用可用于更多场景因为它们能够连续消费和产生数据。
典型的数据管道应用程序有:
- 电商中实时搜索索引的建立
- 电商中的持续ETL
### 安装Flink
https://flink.apache.org/downloads.html
下载安装包,这里下载的是 flink-1.10.1-bin-scala_2.11.tgz
安装参考 https://ci.apache.org/projects/flink/flink-docs-release-1.10/getting-started/tutorials/local_setup.html
```shell
./bin/start-cluster.sh # Start Flink
```
![安装Flink1](images/BigData/安装Flink1.png)
访问 http://localhost:8081
![安装Flink2](images/BigData/安装Flink2.png)
运行 WordCount 示例
![安装Flink3](images/BigData/安装Flink3.png)
![安装Flink4](images/BigData/安装Flink4.png)
![安装Flink5](images/BigData/安装Flink5.png)
### 商品实时推荐
基于Flink实现的商品实时推荐系统。flink统计商品热度放入redis缓存分析日志信息将画像标签和实时记录放入Hbase。在用户发起推荐请求后根据用户画像重排序热度榜并结合协同过滤和标签两个推荐模块为新生成的榜单的每一个产品添加关联产品最后返回新的用户列表。
#### 系统架构
![基于Flink商品实时推荐](images/BigData/基于Flink商品实时推荐.jpg)
在日志数据模块(flink-2-hbase)中又主要分为6个Flink任务:
- **用户-产品浏览历史 -> 实现基于协同过滤的推荐逻辑**
通过Flink去记录用户浏览过这个类目下的哪些产品,为后面的基于Item的协同过滤做准备 实时的记录用户的评分到Hbase中,为后续离线处理做准备。数据存储在Hbase的p_history表
- **用户-兴趣 -> 实现基于上下文的推荐逻辑**
根据用户对同一个产品的操作计算兴趣度,计算规则通过操作间隔时间(如购物 - 浏览 < 100s) FlinkValueState,Action=3(收藏),则清除这个产品的state,如果超过100s没有出现Action=3的事件,也会清除这个state。数据存储在Hbase的u_interest表
- **用户画像计算 -> 实现基于标签的推荐逻辑**
v1.0按照三个维度去计算用户画像,分别是用户的颜色兴趣,用户的产地兴趣,和用户的风格兴趣.根据日志不断的修改用户画像的数据,记录在Hbase中。数据存储在Hbase的user表
- **产品画像记录 -> 实现基于标签的推荐逻辑**
用两个维度记录产品画像,一个是喜爱该产品的年龄段,另一个是性别。数据存储在Hbase的prod表
- **事实热度榜 -> 实现基于热度的推荐逻辑**
通过Flink时间窗口机制,统计当前时间的实时热度,并将数据缓存在Redis中。通过Flink的窗口机制计算实时热度,使用ListState保存一次热度榜。数据存储在redis中,按照时间戳存储list
- **日志导入**
从Kafka接收的数据直接导入进Hbase事实表,保存完整的日志log,日志中包含了用户Id,用户操作的产品id,操作时间,行为(如购买,点击,推荐等)。数据按时间窗口统计数据大屏需要的数据,返回前段展示。数据存储在Hbase的con表
#### 推荐引擎逻辑
**基于热度的推荐逻辑**
![基于热度的推荐逻辑](images/BigData/基于热度的推荐逻辑.jpg)
&#8203;根据用户特征,重新排序热度榜,之后根据两种推荐算法计算得到的产品相关度评分,为每个热度榜中的产品推荐几个关联的产品。
**基于产品画像的产品相似度计算方法**
基于产品画像的推荐逻辑依赖于产品画像和热度榜两个维度,产品画像有三个特征,包含color/country/style三个角度,通过计算用户对该类目产品的评分来过滤热度榜上的产品。
![基于产品画像的产品相似度计算方法](images/BigData/基于产品画像的产品相似度计算方法.jpg)
在已经有产品画像的基础上计算item与item之间的关联系,通过余弦相似度来计算两两之间的评分,最后在已有物品选中的情况下推荐关联性更高的产品。
| 相似度 | A | B | C |
| ------ | ---- | ---- | ---- |
| A | 1 | 0.7 | 0.2 |
| B | 0.7 | 1 | 0.6 |
| C | 0.2 | 0.6 | 1 |
**基于协同过滤的产品相似度计算方法**
根据产品用户表Hbase 去计算公式得到相似度评分:
![基于协同过滤的产品相似度计算方法.jpg](images/BigData/基于协同过滤的产品相似度计算方法.jpg)
**前台推荐页面**
当前推荐结果分为3列分别是热度榜推荐协同过滤推荐和产品画像推荐
![前台推荐页面.jpg](images/BigData/前台推荐页面.jpg)
### 实时计算TopN热榜
本案例将实现一个“实时热门商品”的需求我们可以将“实时热门商品”翻译成程序员更好理解的需求每隔5分钟输出最近一小时内点击量最多的前 N 个商品。将这个需求进行分解我们大概要做这么几件事情:
- 抽取出业务时间戳,告诉 Flink 框架基于业务时间做窗口
- 过滤出点击行为数据
- 按一小时的窗口大小每5分钟统计一次做滑动窗口聚合Sliding Window
- 按每个窗口聚合输出每个窗口中点击量前N名的商品
#### 数据准备
这里我们准备了一份淘宝用户行为数据集(来自[阿里云天池公开数据集](https://tianchi.aliyun.com/datalab/index.htm)。本数据集包含了淘宝上某一天随机一百万用户的所有行为包括点击、购买、加购、收藏。数据集的组织形式和MovieLens-20M类似即数据集的每一行表示一条用户行为由用户ID、商品ID、商品类目ID、行为类型和时间戳组成并以逗号分隔。关于数据集中每一列的详细描述如下
| 列名称 | 说明 |
| :--------- | :------------------------------------------------- |
| 用户ID | 整数类型加密后的用户ID |
| 商品ID | 整数类型加密后的商品ID |
| 商品类目ID | 整数类型加密后的商品所属类目ID |
| 行为类型 | 字符串,枚举类型,包括(pv, buy, cart, fav) |
| 时间戳 | 行为发生的时间戳,单位秒 |
你可以通过下面的命令下载数据集到项目的 `resources` 目录下:
```shell
$ cd my-flink-project/src/main/resources
$ curl https://raw.githubusercontent.com/wuchong/my-flink-project/master/src/main/resources/UserBehavior.csv > UserBehavior.csv
```
这里是否使用 curl 命令下载数据并不重要,你也可以使用 wget 命令或者直接访问链接下载数据。关键是,**将数据文件保存到项目的 `resources` 目录下**,方便应用程序访问。
#### 编写程序
#### 创建模拟数据源
我们先创建一个 `UserBehavior` 的 POJO 类(所有成员变量声明成`public`便是POJO类强类型化后能方便后续的处理。
```java
/**
* 用户行为数据结构
**/
public static class UserBehavior {
public long userId; // 用户ID
public long itemId; // 商品ID
public int categoryId; // 商品类目ID
public String behavior; // 用户行为, 包括("pv", "buy", "cart", "fav")
public long timestamp; // 行为发生的时间戳,单位秒
}
```
接下来我们就可以创建一个 `PojoCsvInputFormat` 了, 这是一个读取 csv 文件并将每一行转成指定 POJO
类型(在我们案例中是 `UserBehavior`)的输入器。
```java
// UserBehavior.csv 的本地文件路径
URL fileUrl = HotItems2.class.getClassLoader().getResource("UserBehavior.csv");
Path filePath = Path.fromLocalFile(new File(fileUrl.toURI()));
// 抽取 UserBehavior 的 TypeInformation是一个 PojoTypeInfo
PojoTypeInfo<UserBehavior> pojoType = (PojoTypeInfo<UserBehavior>) TypeExtractor.createTypeInfo(UserBehavior.class);
// 由于 Java 反射抽取出的字段顺序是不确定的,需要显式指定下文件中字段的顺序
String[] fieldOrder = new String[]{"userId", "itemId", "categoryId", "behavior", "timestamp"};
// 创建 PojoCsvInputFormat
PojoCsvInputFormat<UserBehavior> csvInput = new PojoCsvInputFormat<>(filePath, pojoType, fieldOrder);
```
下一步我们用 `PojoCsvInputFormat` 创建输入源。
```java
DataStream<UserBehavior> dataSource = env.createInput(csvInput, pojoType);
```
这就创建了一个 `UserBehavior` 类型的 `DataStream`
#### EventTime与Watermark
当我们说“统计过去一小时内点击量”,这里的“一小时”是指什么呢? 在 Flink 中它可以是指 ProcessingTime ,也可以是 EventTime由用户决定。
- **ProcessingTime****事件被处理的时间**。也就是由机器的系统时间来决定
- **EventTime****事件发生的时间**。一般就是数据本身携带的时间
在本案例中,我们需要统计业务时间上的每小时的点击量,所以要基于 EventTime 来处理。那么如果让 Flink 按照我们想要的业务时间来处理呢?这里主要有两件事情要做:
- 告诉 Flink 我们现在按照 EventTime 模式进行处理Flink 默认使用 ProcessingTime 处理,所以我们要显式设置下。
```java
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
```
- 指定如何获得业务时间,以及生成 Watermark。Watermark 是用来追踪业务事件的概念,可以理解成 EventTime 世界中的时钟,用来指示当前处理到什么时刻的数据了。由于我们的数据源的数据已经经过整理,没有乱序,即事件的时间戳是单调递增的,所以可以将每条数据的业务时间就当做 Watermark。这里我们用 `AscendingTimestampExtractor` 来实现时间戳的抽取和 Watermark 的生成。
**注意**:真实业务场景一般都是存在乱序的,所以一般使用 `BoundedOutOfOrdernessTimestampExtractor`
```java
DataStream<UserBehavior> timedData = dataSource
.assignTimestampsAndWatermarks(new AscendingTimestampExtractor<UserBehavior>() {
@Override
public long extractAscendingTimestamp(UserBehavior userBehavior) {
// 原始数据单位秒,将其转成毫秒
return userBehavior.timestamp * 1000;
}
});
```
这样我们就得到了一个带有时间标记的数据流了,后面就能做一些窗口的操作。
#### 过滤出点击事件
在开始窗口操作之前先回顾下需求“每隔5分钟输出过去一小时内点击量最多的前 N 个商品”。由于原始数据中存在点击、加购、购买、收藏各种行为的数据,但是我们只需要统计点击量,所以先使用 `FilterFunction` 将点击行为数据过滤出来。
```java
DataStream<UserBehavior> pvData = timedData
.filter(new FilterFunction<UserBehavior>() {
@Override
public boolean filter(UserBehavior userBehavior) throws Exception {
// 过滤出只有点击的数据
return userBehavior.behavior.equals("pv");
}
});
```
#### 窗口统计点击量
由于要每隔5分钟统计一次最近一小时每个商品的点击量所以窗口大小是一小时每隔5分钟滑动一次。即分别要统计 [09:00, 10:00), [09:05, 10:05), [09:10, 10:10)… 等窗口的商品点击量。是一个常见的滑动窗口需求Sliding Window
```java
DataStream<ItemViewCount> windowedData = pvData
// 对商品进行分组
.keyBy("itemId")
// 对每个商品做滑动窗口1小时窗口5分钟滑动一次
.timeWindow(Time.minutes(60), Time.minutes(5))
// 做增量的聚合操作它能使用AggregateFunction提前聚合掉数据减少 state 的存储压力
.aggregate(new CountAgg(), new WindowResultFunction());
```
**CountAgg**
这里的`CountAgg`实现了`AggregateFunction`接口,功能是统计窗口中的条数,即遇到一条数据就加一。
```java
/**
* COUNT 统计的聚合函数实现,每出现一条记录加一
**/
public static class CountAgg implements AggregateFunction<UserBehavior, Long, Long> {
@Override
public Long createAccumulator() {
return 0L;
}
@Override
public Long add(UserBehavior userBehavior, Long acc) {
return acc + 1;
}
@Override
public Long getResult(Long acc) {
return acc;
}
@Override
public Long merge(Long acc1, Long acc2) {
return acc1 + acc2;
}
}
```
**WindowFunction**
`.aggregate(AggregateFunction af, WindowFunction wf)` 的第二个参数`WindowFunction`将每个 key每个窗口聚合后的结果带上其他信息进行输出。这里实现的`WindowResultFunction`将主键商品ID窗口点击量封装成了`ItemViewCount`进行输出。
```java
/**
* 用于输出窗口的结果
**/
public static class WindowResultFunction implements WindowFunction<Long, ItemViewCount, Tuple, TimeWindow> {
@Override
public void apply(
Tuple key, // 窗口的主键,即 itemId
TimeWindow window, // 窗口
Iterable<Long> aggregateResult, // 聚合函数的结果,即 count 值
Collector<ItemViewCount> collector // 输出类型为 ItemViewCount
) throws Exception {
Long itemId = ((Tuple1<Long>) key).f0;
Long count = aggregateResult.iterator().next();
collector.collect(ItemViewCount.of(itemId, window.getEnd(), count));
}
}
/**
* 商品点击量(窗口操作的输出类型)
**/
public static class ItemViewCount {
public long itemId; // 商品ID
public long windowEnd; // 窗口结束时间戳
public long viewCount; // 商品的点击量
public static ItemViewCount of(long itemId, long windowEnd, long viewCount) {
ItemViewCount result = new ItemViewCount();
result.itemId = itemId;
result.windowEnd = windowEnd;
result.viewCount = viewCount;
return result;
}
}
```
现在我们得到了每个商品在每个窗口的点击量的数据流。
#### TopN计算最热门商品
为了统计每个窗口下最热门的商品,我们需要再次按窗口进行分组,这里根据`ItemViewCount`中的`windowEnd`进行`keyBy()`操作。然后使用 `ProcessFunction` 实现一个自定义的 TopN 函数 `TopNHotItems` 来计算点击量排名前3名的商品并将排名结果格式化成字符串便于后续输出。
```java
DataStream<String> topItems = windowedData
.keyBy("windowEnd")
.process(new TopNHotItems(3)); // 求点击量前3名的商品
```
`ProcessFunction` 是 Flink 提供的一个 low-level API用于实现更高级的功能。它主要提供了定时器 timer 的功能支持EventTime或ProcessingTime。本案例中我们将利用 timer 来判断何时**收齐**了某个 window 下所有商品的点击量数据。由于 Watermark 的进度是全局的,
`processElement` 方法中,每当收到一条数据(`ItemViewCount`),我们就注册一个 `windowEnd+1` 的定时器Flink 框架会自动忽略同一时间的重复注册)。`windowEnd+1` 的定时器被触发时,意味着收到了`windowEnd+1`的 Watermark即收齐了该`windowEnd`下的所有商品窗口统计值。我们在 `onTimer()` 中处理将收集的所有商品及点击量进行排序,选出 TopN并将排名信息格式化成字符串后进行输出。
这里我们还使用了 `ListState` 来存储收到的每条 `ItemViewCount` 消息,保证在发生故障时,状态数据的不丢失和一致性。`ListState` 是 Flink 提供的类似 Java `List` 接口的 State API它集成了框架的 checkpoint 机制,自动做到了 exactly-once 的语义保证。
```java
/**
* 求某个窗口中前 N 名的热门点击商品key 为窗口时间戳,输出为 TopN 的结果字符串
**/
public static class TopNHotItems extends KeyedProcessFunction<Tuple, ItemViewCount, String> {
private final int topSize;
public TopNHotItems(int topSize) {
this.topSize = topSize;
}
// 用于存储商品与点击数的状态,待收齐同一个窗口的数据后,再触发 TopN 计算
private ListState<ItemViewCount> itemState;
@Override
public void open(Configuration parameters) throws Exception {
super.open(parameters);
// 状态的注册
ListStateDescriptor<ItemViewCount> itemsStateDesc = new ListStateDescriptor<>("itemState-state", ItemViewCount.class);
itemState = getRuntimeContext().getListState(itemsStateDesc);
}
@Override
public void processElement(ItemViewCount input, Context context, Collector<String> collector) throws Exception {
// 每条数据都保存到状态中
itemState.add(input);
// 注册 windowEnd+1 的 EventTime Timer, 当触发时说明收齐了属于windowEnd窗口的所有商品数据
context.timerService().registerEventTimeTimer(input.windowEnd + 1);
}
@Override
public void onTimer(long timestamp, OnTimerContext ctx, Collector<String> out) throws Exception {
// 获取收到的所有商品点击量
List<ItemViewCount> allItems = new ArrayList<>();
for (ItemViewCount item : itemState.get()) {
allItems.add(item);
}
// 提前清除状态中的数据,释放空间
itemState.clear();
// 按照点击量从大到小排序
allItems.sort(new Comparator<ItemViewCount>() {
@Override
public int compare(ItemViewCount o1, ItemViewCount o2) {
return (int) (o2.viewCount - o1.viewCount);
}
});
// 将排名信息格式化成 String, 便于打印
StringBuilder result = new StringBuilder();
result.append("====================================\n");
result.append("时间: ").append(new Timestamp(timestamp-1)).append("\n");
for (int i=0;i<topSize;i++) {
ItemViewCount currentItem = allItems.get(i);
// No1: 商品ID=12224 浏览量=2413
result.append("No").append(i).append(":")
.append(" 商品ID=").append(currentItem.itemId)
.append(" 浏览量=").append(currentItem.viewCount)
.append("\n");
}
result.append("====================================\n\n");
out.collect(result.toString());
}
}
```
#### 打印输出
最后一步我们将结果打印输出到控制台,并调用`env.execute`执行任务。
```java
topItems.print();
env.execute("Hot Items Job");
```
#### 运行程序
直接运行 main 函数就能看到不断输出的每个时间点的热门商品ID。
![实时计算TopN热榜](images/BigData/实时计算TopN热榜.png)
![Apache-Spark](images/BigData/Apache-Spark.png)

@ -0,0 +1,530 @@
Apache Flink是一个**框架**和**分布式处理引擎**,用于在**无界**和**有界**数据流上进行**有状态的计算**。Flink被设计为在所有常见的集群环境中运行以内存中的速度和任何规模执行计算。
![Apache-Flink](images/BigData/Apache-Flink.png)
针对流数据+批数据的计算框架。把批数据看作流数据的一种特例,延迟性较低(毫秒级),且能保证消息传输不丢失不重复。
Flink创造性地统一了流处理和批处理作为流处理看待时输入数据流是无界的而批处理被作为一种特殊的流处理只是它的输入数据流被定义为有界的。Flink程序由Stream和Transformation这两个基本构建块组成其中Stream是一个中间结果数据而Transformation是一个操作它对一个或多个输入Stream进行计算处理输出一个或多个结果Stream。
### 处理无界和有界数据
数据可以作为无界流或有界流被处理:
- **Unbounded streams**(无界流)有一个起点,但没有定义的终点。它们不会终止,而且会源源不断的提供数据。无边界的流必须被连续地处理,即事件达到后必须被立即处理。等待所有输入数据到达是不可能的,因为输入是无界的,并且在任何时间点都不会完成。处理无边界的数据通常要求以特定顺序(例如,事件发生的顺序)接收事件,以便能够推断出结果的完整性。
- **Bounded streams**(有界流)有一个定义的开始和结束。在执行任何计算之前,可以通过摄取(提取)所有数据来处理有界流。处理有界流不需要有序摄取,因为有界数据集总是可以排序的。有界流的处理也称为批处理。
![处理无界和有界数据](images/BigData/处理无界和有界数据.png)
**Apache Flink擅长处理无界和有界数据集**。对时间和状态的精确控制使Flink的运行时能够在无边界的流上运行任何类型的应用程序。有界流由专门为固定大小的数据集设计的算法和数据结构在内部处理从而产生出色的性能。
### 分层API
Flink提供了三层API。每个API在简洁性和表达性之间提供了不同的权衡并且针对不同的使用场景
![Flink-分层API](images/BigData/Flink-分层API.png)
### 应用场景
Apache Flink是开发和运行许多不同类型应用程序的最佳选择因为它具有丰富的特性。Flink的特性包括支持流和批处理、复杂的状态管理、事件处理语义以及确保状态的一致性。此外Flink可以部署在各种资源提供程序上例如YARN、Apache Mesos和Kubernetes也可以作为裸机硬件上的独立集群进行部署。配置为高可用性Flink没有单点故障。Flink已经被证明可以扩展到数千个内核和TB级的应用程序状态提供高吞吐量和低延迟并支持世界上一些最苛刻的流处理应用程序。
下面是Flink支持的最常见的应用程序类型
- Event-driven Applications事件驱动的应用程序
- Data Analytics Applications数据分析应用程序
- Data Pipeline Applications数据管道应用程序
#### Event-driven Applications
Event-driven Applications事件驱动的应用程序。事件驱动的应用程序是一个有状态的应用程序它从一个或多个事件流中获取事件并通过触发计算、状态更新或外部操作对传入的事件作出反应。
事件驱动的应用程序基于有状态的流处理应用程序。在这种设计中,数据和计算被放在一起,从而可以进行本地(内存或磁盘)数据访问。通过定期将检查点写入远程持久存储,可以实现容错。下图描述了传统应用程序体系结构和事件驱动应用程序之间的区别。
![Event-driven-Applications](images/BigData/Event-driven-Applications.png)
代替查询远程数据库,事件驱动的应用程序在本地访问其数据,从而在吞吐量和延迟方面获得更好的性能。可以定期异步地将检查点同步到远程持久存,而且支持增量同步。不仅如此,在分层架构中,多个应用程序共享同一个数据库是很常见的。因此,数据库的任何更改都需要协调,由于每个事件驱动的应用程序都负责自己的数据,因此更改数据表示或扩展应用程序所需的协调较少。
对于事件驱动的应用程序Flink的突出特性是savepoint。保存点是一个一致的状态镜像可以用作兼容应用程序的起点。给定一个保存点就可以更新或调整应用程序的规模或者可以启动应用程序的多个版本进行A/B测试。
典型的事件驱动的应用程序有:
- 欺诈检测
- 异常检测
- 基于规则的提醒
- 业务流程监控
- Web应用(社交网络)
#### Data Analytics Applications
Data Analytics Applications数据分析应用程序。传统上的分析是作为批处理查询或应用程序对已记录事件的有限数据集执行的。为了将最新数据合并到分析结果中必须将其添加到分析数据集中然后重新运行查询或应用程序结果被写入存储系统或作为报告发出。
有了复杂的流处理引擎分析也可以以实时方式执行。流查询或应用程序不是读取有限的数据集而是接收实时事件流并在使用事件时不断地生成和更新结果。结果要么写入外部数据库要么作为内部状态进行维护。Dashboard应用程序可以从外部数据库读取最新的结果也可以直接查询应用程序的内部状态。
Apache Flink支持流以及批处理分析应用程序如下图所示
![Data-Analytics-Applications](images/BigData/Data-Analytics-Applications.png)
典型的数据分析应用程序有:
- 电信网络质量监控
- 产品更新分析及移动应用实验评估
- 消费者技术中实时数据的特别分析
- 大规模图分析
#### Data Pipeline Applications
Data Pipeline Applications数据管道应用程序。提取-转换-加载ETL是在存储系统之间转换和移动数据的常用方法。通常会定期触发ETL作业以便将数据从事务性数据库系统复制到分析数据库或数据仓库。
数据管道的作用类似于ETL作业。它们转换和丰富数据并可以将数据从一个存储系统移动到另一个存储系统。但是它们以连续流模式运行而不是周期性地触发。因此它们能够从不断产生数据的源读取记录并以低延迟将其移动到目的地。例如数据管道可以监视文件系统目录中的新文件并将它们的数据写入事件日志。另一个应用程序可能将事件流物化到数据库或者增量地构建和完善搜索索引。
下图描述了周期性ETL作业和连续数据管道之间的差异
![Data-Pipeline-Applications](images/BigData/Data-Pipeline-Applications.png)
与周期性ETL作业相比连续数据管道的明显优势是减少了将数据移至其目的地的等待时间。此外数据管道更通用可用于更多场景因为它们能够连续消费和产生数据。
典型的数据管道应用程序有:
- 电商中实时搜索索引的建立
- 电商中的持续ETL
### 安装Flink
https://flink.apache.org/downloads.html
下载安装包,这里下载的是 flink-1.10.1-bin-scala_2.11.tgz
安装参考 https://ci.apache.org/projects/flink/flink-docs-release-1.10/getting-started/tutorials/local_setup.html
```shell
./bin/start-cluster.sh # Start Flink
```
![安装Flink1](images/BigData/安装Flink1.png)
访问 http://localhost:8081
![安装Flink2](images/BigData/安装Flink2.png)
运行 WordCount 示例
![安装Flink3](images/BigData/安装Flink3.png)
![安装Flink4](images/BigData/安装Flink4.png)
![安装Flink5](images/BigData/安装Flink5.png)
### 商品实时推荐
基于Flink实现的商品实时推荐系统。flink统计商品热度放入redis缓存分析日志信息将画像标签和实时记录放入Hbase。在用户发起推荐请求后根据用户画像重排序热度榜并结合协同过滤和标签两个推荐模块为新生成的榜单的每一个产品添加关联产品最后返回新的用户列表。
#### 系统架构
![基于Flink商品实时推荐](images/BigData/基于Flink商品实时推荐.jpg)
在日志数据模块(flink-2-hbase)中又主要分为6个Flink任务:
- **用户-产品浏览历史 -> 实现基于协同过滤的推荐逻辑**
通过Flink去记录用户浏览过这个类目下的哪些产品,为后面的基于Item的协同过滤做准备 实时的记录用户的评分到Hbase中,为后续离线处理做准备。数据存储在Hbase的p_history表
- **用户-兴趣 -> 实现基于上下文的推荐逻辑**
根据用户对同一个产品的操作计算兴趣度,计算规则通过操作间隔时间(如购物 - 浏览 < 100s) FlinkValueState,Action=3(收藏),则清除这个产品的state,如果超过100s没有出现Action=3的事件,也会清除这个state。数据存储在Hbase的u_interest表
- **用户画像计算 -> 实现基于标签的推荐逻辑**
v1.0按照三个维度去计算用户画像,分别是用户的颜色兴趣,用户的产地兴趣,和用户的风格兴趣.根据日志不断的修改用户画像的数据,记录在Hbase中。数据存储在Hbase的user表
- **产品画像记录 -> 实现基于标签的推荐逻辑**
用两个维度记录产品画像,一个是喜爱该产品的年龄段,另一个是性别。数据存储在Hbase的prod表
- **事实热度榜 -> 实现基于热度的推荐逻辑**
通过Flink时间窗口机制,统计当前时间的实时热度,并将数据缓存在Redis中。通过Flink的窗口机制计算实时热度,使用ListState保存一次热度榜。数据存储在redis中,按照时间戳存储list
- **日志导入**
从Kafka接收的数据直接导入进Hbase事实表,保存完整的日志log,日志中包含了用户Id,用户操作的产品id,操作时间,行为(如购买,点击,推荐等)。数据按时间窗口统计数据大屏需要的数据,返回前段展示。数据存储在Hbase的con表
#### 推荐引擎逻辑
**基于热度的推荐逻辑**
![基于热度的推荐逻辑](images/BigData/基于热度的推荐逻辑.jpg)
&#8203;根据用户特征,重新排序热度榜,之后根据两种推荐算法计算得到的产品相关度评分,为每个热度榜中的产品推荐几个关联的产品。
**基于产品画像的产品相似度计算方法**
基于产品画像的推荐逻辑依赖于产品画像和热度榜两个维度,产品画像有三个特征,包含color/country/style三个角度,通过计算用户对该类目产品的评分来过滤热度榜上的产品。
![基于产品画像的产品相似度计算方法](images/BigData/基于产品画像的产品相似度计算方法.jpg)
在已经有产品画像的基础上计算item与item之间的关联系,通过余弦相似度来计算两两之间的评分,最后在已有物品选中的情况下推荐关联性更高的产品。
| 相似度 | A | B | C |
| ------ | ---- | ---- | ---- |
| A | 1 | 0.7 | 0.2 |
| B | 0.7 | 1 | 0.6 |
| C | 0.2 | 0.6 | 1 |
**基于协同过滤的产品相似度计算方法**
根据产品用户表Hbase 去计算公式得到相似度评分:
![基于协同过滤的产品相似度计算方法.jpg](images/BigData/基于协同过滤的产品相似度计算方法.jpg)
**前台推荐页面**
当前推荐结果分为3列分别是热度榜推荐协同过滤推荐和产品画像推荐
![前台推荐页面.jpg](images/BigData/前台推荐页面.jpg)
### 实时计算TopN热榜
本案例将实现一个“实时热门商品”的需求我们可以将“实时热门商品”翻译成程序员更好理解的需求每隔5分钟输出最近一小时内点击量最多的前 N 个商品。将这个需求进行分解我们大概要做这么几件事情:
- 抽取出业务时间戳,告诉 Flink 框架基于业务时间做窗口
- 过滤出点击行为数据
- 按一小时的窗口大小每5分钟统计一次做滑动窗口聚合Sliding Window
- 按每个窗口聚合输出每个窗口中点击量前N名的商品
#### 数据准备
这里我们准备了一份淘宝用户行为数据集(来自[阿里云天池公开数据集](https://tianchi.aliyun.com/datalab/index.htm)。本数据集包含了淘宝上某一天随机一百万用户的所有行为包括点击、购买、加购、收藏。数据集的组织形式和MovieLens-20M类似即数据集的每一行表示一条用户行为由用户ID、商品ID、商品类目ID、行为类型和时间戳组成并以逗号分隔。关于数据集中每一列的详细描述如下
| 列名称 | 说明 |
| :--------- | :------------------------------------------------- |
| 用户ID | 整数类型加密后的用户ID |
| 商品ID | 整数类型加密后的商品ID |
| 商品类目ID | 整数类型加密后的商品所属类目ID |
| 行为类型 | 字符串,枚举类型,包括(pv, buy, cart, fav) |
| 时间戳 | 行为发生的时间戳,单位秒 |
你可以通过下面的命令下载数据集到项目的 `resources` 目录下:
```shell
$ cd my-flink-project/src/main/resources
$ curl https://raw.githubusercontent.com/wuchong/my-flink-project/master/src/main/resources/UserBehavior.csv > UserBehavior.csv
```
这里是否使用 curl 命令下载数据并不重要,你也可以使用 wget 命令或者直接访问链接下载数据。关键是,**将数据文件保存到项目的 `resources` 目录下**,方便应用程序访问。
#### 编写程序
#### 创建模拟数据源
我们先创建一个 `UserBehavior` 的 POJO 类(所有成员变量声明成`public`便是POJO类强类型化后能方便后续的处理。
```java
/**
* 用户行为数据结构
**/
public static class UserBehavior {
public long userId; // 用户ID
public long itemId; // 商品ID
public int categoryId; // 商品类目ID
public String behavior; // 用户行为, 包括("pv", "buy", "cart", "fav")
public long timestamp; // 行为发生的时间戳,单位秒
}
```
接下来我们就可以创建一个 `PojoCsvInputFormat` 了, 这是一个读取 csv 文件并将每一行转成指定 POJO
类型(在我们案例中是 `UserBehavior`)的输入器。
```java
// UserBehavior.csv 的本地文件路径
URL fileUrl = HotItems2.class.getClassLoader().getResource("UserBehavior.csv");
Path filePath = Path.fromLocalFile(new File(fileUrl.toURI()));
// 抽取 UserBehavior 的 TypeInformation是一个 PojoTypeInfo
PojoTypeInfo<UserBehavior> pojoType = (PojoTypeInfo<UserBehavior>) TypeExtractor.createTypeInfo(UserBehavior.class);
// 由于 Java 反射抽取出的字段顺序是不确定的,需要显式指定下文件中字段的顺序
String[] fieldOrder = new String[]{"userId", "itemId", "categoryId", "behavior", "timestamp"};
// 创建 PojoCsvInputFormat
PojoCsvInputFormat<UserBehavior> csvInput = new PojoCsvInputFormat<>(filePath, pojoType, fieldOrder);
```
下一步我们用 `PojoCsvInputFormat` 创建输入源。
```java
DataStream<UserBehavior> dataSource = env.createInput(csvInput, pojoType);
```
这就创建了一个 `UserBehavior` 类型的 `DataStream`
#### EventTime与Watermark
当我们说“统计过去一小时内点击量”,这里的“一小时”是指什么呢? 在 Flink 中它可以是指 ProcessingTime ,也可以是 EventTime由用户决定。
- **ProcessingTime****事件被处理的时间**。也就是由机器的系统时间来决定
- **EventTime****事件发生的时间**。一般就是数据本身携带的时间
在本案例中,我们需要统计业务时间上的每小时的点击量,所以要基于 EventTime 来处理。那么如果让 Flink 按照我们想要的业务时间来处理呢?这里主要有两件事情要做:
- 告诉 Flink 我们现在按照 EventTime 模式进行处理Flink 默认使用 ProcessingTime 处理,所以我们要显式设置下。
```java
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
```
- 指定如何获得业务时间,以及生成 Watermark。Watermark 是用来追踪业务事件的概念,可以理解成 EventTime 世界中的时钟,用来指示当前处理到什么时刻的数据了。由于我们的数据源的数据已经经过整理,没有乱序,即事件的时间戳是单调递增的,所以可以将每条数据的业务时间就当做 Watermark。这里我们用 `AscendingTimestampExtractor` 来实现时间戳的抽取和 Watermark 的生成。
**注意**:真实业务场景一般都是存在乱序的,所以一般使用 `BoundedOutOfOrdernessTimestampExtractor`
```java
DataStream<UserBehavior> timedData = dataSource
.assignTimestampsAndWatermarks(new AscendingTimestampExtractor<UserBehavior>() {
@Override
public long extractAscendingTimestamp(UserBehavior userBehavior) {
// 原始数据单位秒,将其转成毫秒
return userBehavior.timestamp * 1000;
}
});
```
这样我们就得到了一个带有时间标记的数据流了,后面就能做一些窗口的操作。
#### 过滤出点击事件
在开始窗口操作之前先回顾下需求“每隔5分钟输出过去一小时内点击量最多的前 N 个商品”。由于原始数据中存在点击、加购、购买、收藏各种行为的数据,但是我们只需要统计点击量,所以先使用 `FilterFunction` 将点击行为数据过滤出来。
```java
DataStream<UserBehavior> pvData = timedData
.filter(new FilterFunction<UserBehavior>() {
@Override
public boolean filter(UserBehavior userBehavior) throws Exception {
// 过滤出只有点击的数据
return userBehavior.behavior.equals("pv");
}
});
```
#### 窗口统计点击量
由于要每隔5分钟统计一次最近一小时每个商品的点击量所以窗口大小是一小时每隔5分钟滑动一次。即分别要统计 [09:00, 10:00), [09:05, 10:05), [09:10, 10:10)… 等窗口的商品点击量。是一个常见的滑动窗口需求Sliding Window
```java
DataStream<ItemViewCount> windowedData = pvData
// 对商品进行分组
.keyBy("itemId")
// 对每个商品做滑动窗口1小时窗口5分钟滑动一次
.timeWindow(Time.minutes(60), Time.minutes(5))
// 做增量的聚合操作它能使用AggregateFunction提前聚合掉数据减少 state 的存储压力
.aggregate(new CountAgg(), new WindowResultFunction());
```
**CountAgg**
这里的`CountAgg`实现了`AggregateFunction`接口,功能是统计窗口中的条数,即遇到一条数据就加一。
```java
/**
* COUNT 统计的聚合函数实现,每出现一条记录加一
**/
public static class CountAgg implements AggregateFunction<UserBehavior, Long, Long> {
@Override
public Long createAccumulator() {
return 0L;
}
@Override
public Long add(UserBehavior userBehavior, Long acc) {
return acc + 1;
}
@Override
public Long getResult(Long acc) {
return acc;
}
@Override
public Long merge(Long acc1, Long acc2) {
return acc1 + acc2;
}
}
```
**WindowFunction**
`.aggregate(AggregateFunction af, WindowFunction wf)` 的第二个参数`WindowFunction`将每个 key每个窗口聚合后的结果带上其他信息进行输出。这里实现的`WindowResultFunction`将主键商品ID窗口点击量封装成了`ItemViewCount`进行输出。
```java
/**
* 用于输出窗口的结果
**/
public static class WindowResultFunction implements WindowFunction<Long, ItemViewCount, Tuple, TimeWindow> {
@Override
public void apply(
Tuple key, // 窗口的主键,即 itemId
TimeWindow window, // 窗口
Iterable<Long> aggregateResult, // 聚合函数的结果,即 count 值
Collector<ItemViewCount> collector // 输出类型为 ItemViewCount
) throws Exception {
Long itemId = ((Tuple1<Long>) key).f0;
Long count = aggregateResult.iterator().next();
collector.collect(ItemViewCount.of(itemId, window.getEnd(), count));
}
}
/**
* 商品点击量(窗口操作的输出类型)
**/
public static class ItemViewCount {
public long itemId; // 商品ID
public long windowEnd; // 窗口结束时间戳
public long viewCount; // 商品的点击量
public static ItemViewCount of(long itemId, long windowEnd, long viewCount) {
ItemViewCount result = new ItemViewCount();
result.itemId = itemId;
result.windowEnd = windowEnd;
result.viewCount = viewCount;
return result;
}
}
```
现在我们得到了每个商品在每个窗口的点击量的数据流。
#### TopN计算最热门商品
为了统计每个窗口下最热门的商品,我们需要再次按窗口进行分组,这里根据`ItemViewCount`中的`windowEnd`进行`keyBy()`操作。然后使用 `ProcessFunction` 实现一个自定义的 TopN 函数 `TopNHotItems` 来计算点击量排名前3名的商品并将排名结果格式化成字符串便于后续输出。
```java
DataStream<String> topItems = windowedData
.keyBy("windowEnd")
.process(new TopNHotItems(3)); // 求点击量前3名的商品
```
`ProcessFunction` 是 Flink 提供的一个 low-level API用于实现更高级的功能。它主要提供了定时器 timer 的功能支持EventTime或ProcessingTime。本案例中我们将利用 timer 来判断何时**收齐**了某个 window 下所有商品的点击量数据。由于 Watermark 的进度是全局的,
`processElement` 方法中,每当收到一条数据(`ItemViewCount`),我们就注册一个 `windowEnd+1` 的定时器Flink 框架会自动忽略同一时间的重复注册)。`windowEnd+1` 的定时器被触发时,意味着收到了`windowEnd+1`的 Watermark即收齐了该`windowEnd`下的所有商品窗口统计值。我们在 `onTimer()` 中处理将收集的所有商品及点击量进行排序,选出 TopN并将排名信息格式化成字符串后进行输出。
这里我们还使用了 `ListState` 来存储收到的每条 `ItemViewCount` 消息,保证在发生故障时,状态数据的不丢失和一致性。`ListState` 是 Flink 提供的类似 Java `List` 接口的 State API它集成了框架的 checkpoint 机制,自动做到了 exactly-once 的语义保证。
```java
/**
* 求某个窗口中前 N 名的热门点击商品key 为窗口时间戳,输出为 TopN 的结果字符串
**/
public static class TopNHotItems extends KeyedProcessFunction<Tuple, ItemViewCount, String> {
private final int topSize;
public TopNHotItems(int topSize) {
this.topSize = topSize;
}
// 用于存储商品与点击数的状态,待收齐同一个窗口的数据后,再触发 TopN 计算
private ListState<ItemViewCount> itemState;
@Override
public void open(Configuration parameters) throws Exception {
super.open(parameters);
// 状态的注册
ListStateDescriptor<ItemViewCount> itemsStateDesc = new ListStateDescriptor<>("itemState-state", ItemViewCount.class);
itemState = getRuntimeContext().getListState(itemsStateDesc);
}
@Override
public void processElement(ItemViewCount input, Context context, Collector<String> collector) throws Exception {
// 每条数据都保存到状态中
itemState.add(input);
// 注册 windowEnd+1 的 EventTime Timer, 当触发时说明收齐了属于windowEnd窗口的所有商品数据
context.timerService().registerEventTimeTimer(input.windowEnd + 1);
}
@Override
public void onTimer(long timestamp, OnTimerContext ctx, Collector<String> out) throws Exception {
// 获取收到的所有商品点击量
List<ItemViewCount> allItems = new ArrayList<>();
for (ItemViewCount item : itemState.get()) {
allItems.add(item);
}
// 提前清除状态中的数据,释放空间
itemState.clear();
// 按照点击量从大到小排序
allItems.sort(new Comparator<ItemViewCount>() {
@Override
public int compare(ItemViewCount o1, ItemViewCount o2) {
return (int) (o2.viewCount - o1.viewCount);
}
});
// 将排名信息格式化成 String, 便于打印
StringBuilder result = new StringBuilder();
result.append("====================================\n");
result.append("时间: ").append(new Timestamp(timestamp-1)).append("\n");
for (int i=0;i<topSize;i++) {
ItemViewCount currentItem = allItems.get(i);
// No1: 商品ID=12224 浏览量=2413
result.append("No").append(i).append(":")
.append(" 商品ID=").append(currentItem.itemId)
.append(" 浏览量=").append(currentItem.viewCount)
.append("\n");
}
result.append("====================================\n\n");
out.collect(result.toString());
}
}
```
#### 打印输出
最后一步我们将结果打印输出到控制台,并调用`env.execute`执行任务。
```java
topItems.print();
env.execute("Hot Items Job");
```
#### 运行程序
直接运行 main 函数就能看到不断输出的每个时间点的热门商品ID。
![实时计算TopN热榜](images/BigData/实时计算TopN热榜.png)

@ -7,5 +7,6 @@
* [✍ HDFS](src/BigData/202 "HDFS")
* 🏁 数据分析
* [✍ Apache Storm](src/BigData/301 "Apache Storm")
* [✍ Apache Spark](src/BigData/302 "Apache Spark")
* [✍ Apache Flink](src/BigData/303 "Apache Flink")
* [✍ Apache Storm](src/BigData/302 "Apache Storm")
* [✍ Apache Spark](src/BigData/303 "Apache Spark")
* [✍ Apache Flink](src/BigData/304 "Apache Flink")

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

@ -2,10 +2,12 @@
**作用**
**binlog使用场景**
- **主从复制**在主从复制中从库利用主库上的binlog进行重播实现主从同步
- **数据恢复**通过使用mysqlbinlog`工具来实现数据库基于时间点的还原
在实际应用中, `binlog` 的主要使用场景有两个,分别是 **主从复制** 和 **数据恢复**。
- **主从复制**:在 `Master` 端开启 `binlog` ,然后将 `binlog`发送到各个 `Slave` 端, `Slave` 端重放 `binlog` 从而达到主从数据同步
- **数据恢复**:通过使用`mysqlbinlog`工具来实现数据库基于时间点的还原

@ -1,107 +1,288 @@
### 镜像命令
### 容器信息
```shell
# 查看docker容器版本
docker version
# 查看docker容器信息
docker info
# 查看docker容器帮助
docker --help
```
### 镜像操作
提示对于镜像的操作可使用镜像名、镜像长ID和短ID。
#### 镜像查看
```shell
# 列出本地images
docker images
# 含中间映像层
docker images -a
# 只显示镜像ID
docker images -q
# 含中间映像层
docker images -qa
# 显示镜像摘要信息(DIGEST列)
docker images --digests
# 显示镜像完整信息
docker images --no-trunc
# 显示指定镜像的历史创建;参数:-H 镜像大小和日期默认为true--no-trunc 显示完整的提交记录;-q 仅列出提交记录ID
docker history -H redis
```
```sh
# 列出 Docker 本地镜像列表
$ docker images
$ docker image ls -a
# 运行 Docker 镜像(守护态方式)
$ docker run -d {镜像名}
# 删除指定 Docker 镜像
$ docker image rm {镜像名}
#### 镜像搜索
```shell
# 搜索仓库MySQL镜像
docker search mysql
# --filter=stars=600只显示 starts>=600 的镜像
docker search --filter=stars=600 mysql
# --no-trunc 显示镜像完整 DESCRIPTION 描述
docker search --no-trunc mysql
# --automated :只列出 AUTOMATED=OK 的镜像
docker search --automated mysql
```
### 容器命令
#### 镜像下载
```shell
# 下载Redis官方最新镜像相当于docker pull redis:latest
docker pull redis
# 下载仓库所有Redis镜像
docker pull -a redis
# 下载私人仓库镜像
docker pull bitnami/redis
```
```bash
# 列出正在运行的容器
$ docker ps -a
#### 镜像删除
# 列出所有容器(包括已停止容器)
$ docker ps -l
```shell
# 单个镜像删除相当于docker rmi redis:latest
docker rmi redis
# 强制删除(针对基于镜像有运行的容器进程)
docker rmi -f redis
# 多个镜像删除,不同镜像间以空格间隔
docker rmi -f redis tomcat nginx
# 删除本地全部镜像
docker rmi -f $(docker images -q)
```
```bash
$ docker exec -it {容器ID} /bin/bash
#### 镜像构建
```shell
# 编写dockerfile
cd /docker/dockerfile
vim mycentos
# 构建docker镜像
docker build -f /docker/dockerfile/mycentos -t mycentos:1.1
```
停止 Docker 容器:
```bash
$ docker stop {容器ID}
### 容器操作
提示对于容器的操作可使用CONTAINER ID 或 NAMES。
#### 容器启动
```shell
# 新建并启动容器,参数:-i 以交互模式运行容器;-t 为容器重新分配一个伪输入终端;--name 为容器指定一个名称
docker run -i -t --name mycentos
# 后台启动容器,参数:-d 已守护方式启动容器
docker run -d mycentos
```
删除指定 Docker 容器:
注意:此时使用"docker ps -a"会发现容器已经退出。这是docker的机制要使Docker容器后台运行就必须有一个前台进程。解决方案将你要运行的程序以前台进程的形式运行。
```bash
$ docker rm -f {容器ID}
```shell
# 启动一个或多个已经被停止的容器
docker start redis
# 重启容器
docker restart redis
```
删除停止的 Docker 容器:
```bash
$ docker container prune
#### 容器进程
```shell
# top支持 ps 命令参数格式docker top [OPTIONS] CONTAINER [ps OPTIONS]
# 列出redis容器中运行进程
docker top redis
# 查看所有运行容器的进程信息
for i in `docker ps |grep Up|awk '{print $1}'`;do echo \ &&docker top $i; done
```
查看 Docker 容器历史运行日志:
```bash
$ docker logs {容器名}
#### 容器日志
```shell
$ docker logs [OPTIONS] CONTAINER
Options:
--details 显示更多的信息
-f, --follow 跟踪实时日志
--since string 显示自某个timestamp之后的日志或相对时间如42m即42分钟
--tail string 从日志末尾显示多少行日志, 默认是all
-t, --timestamps 显示时间戳
--until string 显示自某个timestamp之前的日志或相对时间如42m即42分钟
# 查看redis容器日志默认参数
docker logs rabbitmq
# 查看redis容器日志参数-f 跟踪日志输出;-t 显示时间戳;--tail 仅列出最新N条容器日志
docker logs -f -t --tail=20 redis
# 查看容器redis从2019年05月21日后的最新10条日志。
docker logs --since="2019-05-21" --tail=10 redis
# 查看指定时间后的日志只显示最后100行
$ docker logs -f -t --since="2018-02-08" --tail=100 CONTAINER_ID
# 查看最近30分钟的日志
$ docker logs --since 30m CONTAINER_ID
# 查看某时间之后的日志
$ docker logs -t --since="2018-02-08T13:23:37" CONTAINER_ID
# 查看某时间段日志
$ docker logs -t --since="2018-02-08T13:23:37" --until "2018-02-09T12:23:37" CONTAINER_ID
# 查看最后100行并过滤关键词Exception
$ docker logs -f --tail=100 CONTAINER_ID | grep "Exception"
```
实时监听 Docker 容器运行日志:
```bash
$ docker logs -f {容器名}
#### 容器的进入与退出
```shell
# 使用run方式在创建时进入
docker run -it centos /bin/bash
# 关闭容器并退出
exit
# 仅退出容器,不关闭
快捷键Ctrl + P + Q
# 直接进入centos 容器启动命令的终端不会启动新进程多个attach连接共享容器屏幕参数--sig-proxy=false 确保CTRL-D或CTRL-C不会关闭容器
docker attach --sig-proxy=false centos
# 在 centos 容器中打开新的交互模式终端,可以启动新进程,参数:-i 即使没有附加也保持STDIN 打开;-t 分配一个伪终端
docker exec -i -t centos /bin/bash
# 以交互模式在容器中执行命令,结果返回到当前终端屏幕
docker exec -i -t centos ls -l /tmp
# 以分离模式在容器中执行命令,程序后台运行,结果不会反馈到当前终端
docker exec -d centos touch cache.txt
```
### 数据卷命令
#### 查看容器
创建 Docker 数据卷:
```shell
# 查看正在运行的容器
docker ps
# 查看正在运行的容器的ID
docker ps -q
# 查看正在运行+历史运行过的容器
docker ps -a
# 显示运行容器总文件大小
docker ps -s
```bash
$ docker volume create {数据卷名}
# 显示最近创建容器
docker ps -l
# 显示最近创建的3个容器
docker ps -n 3
# 不截断输出
docker ps --no-trunc
# 获取镜像redis的元信息
docker inspect redis
# 获取正在运行的容器redis的 IP
docker inspect --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' redis
```
列出所有 Docker 数据卷:
```bash
$ docker volume ls
#### 容器的停止与删除
```shell
#停止一个运行中的容器
docker stop redis
#杀掉一个运行中的容器
docker kill redis
#删除一个已停止的容器
docker rm redis
#删除一个运行中的容器
docker rm -f redis
#删除多个容器
docker rm -f $(docker ps -a -q)
docker ps -a -q | xargs docker rm
# -l 移除容器间的网络连接,连接名为 db
docker rm -l db
# -v 删除容器,并删除容器挂载的数据卷
docker rm -v redis
```
删除指定 Docker 数据卷:
```bash
$ docker volume rm {数据卷名}
#### 生成镜像
```shell
# 基于当前redis容器创建一个新的镜像参数-a 提交的镜像作者;-c 使用Dockerfile指令来创建镜像-m :提交时的说明文字;-p :在commit时将容器暂停
docker commit -a="DeepInThought" -m="my redis" [redis容器ID] myredis:v1.1
```
删除未关联(失效) Docker 数据卷:
```bash
$ docker volume prune
$ docker volume rm $(docker volume ls -qf dangling=true)
#### 容器与主机间的数据拷贝
```shell
# 将rabbitmq容器中的文件copy至本地路径
docker cp rabbitmq:/[container_path] [local_path]
# 将主机文件copy至rabbitmq容器
docker cp [local_path] rabbitmq:/[container_path]/
# 将主机文件copy至rabbitmq容器目录重命名为[container_path]注意与非重命名copy的区别
docker cp [local_path] rabbitmq:/[container_path]
```
### 文件操作命令
## 其它操作
从主机复制文件到 Docker 容器中:
### 清理环境
```bash
$ sudo docker cp {主机内文件路径} {容器ID}:{容器内文件存储路径}
```shell
# 查询容器列表
docker ps -a
# 停用容器
sudo docker stop [CONTAINER ID]
# 删除容器
sudo docker rm [CONTAINER ID]
# 删除镜像
sudo docker rmi [Image ID]
# 检查是否被删除
sudo docker images
# 停止所有容器
docker stop $(docker ps -a -q)
# 删除所有容器
docker rm $(docker ps -a -q)
# 删除所有镜像
docker rmi $(docker images -q)
```
从 Docker 容器中复制文件到主机中:
```bash
$ sudo docker cp {容器ID}:{容器内文件路径} {主机内文件存储路径}
```
### 文件拷贝
```shell
# 从主机复制到容器
sudo docker cp host_path containerID:container_path
# 从容器复制到主机
sudo docker cp containerID:container_path host_path
```

@ -0,0 +1,32 @@
### 清理环境
```shell
# 查询容器列表
docker ps -a
# 停用容器
sudo docker stop [CONTAINER ID]
# 删除容器
sudo docker rm [CONTAINER ID]
# 删除镜像
sudo docker rmi [Image ID]
# 检查是否被删除
sudo docker images
# 停止所有容器
docker stop $(docker ps -a -q)
# 删除所有容器
docker rm $(docker ps -a -q)
# 删除所有镜像
docker rmi $(docker images -q)
```
### 文件拷贝
```shell
# 从主机复制到容器
sudo docker cp host_path containerID:container_path
# 从容器复制到主机
sudo docker cp containerID:container_path host_path
```

@ -1,147 +1,87 @@
### 侦听端口
```nginx
server {
# Standard HTTP Protocol
listen 80;
# Standard HTTPS Protocol
listen 443 ssl;
# For http2
listen 443 ssl http2;
# Listen on 80 using IPv6
listen [::]:80;
# Listen only on using IPv6
listen [::]:80 ipv6only=on;
}
```
## 发布方式
应用程序升级面临最大挑战是新旧业务切换将软件从测试的最后阶段带到生产环境同时要保证系统不间断提供服务。长期以来业务升级渐渐形成了几个发布策略蓝绿发布、灰度发布和滚动发布目的是尽可能避免因发布导致的流量丢失或服务不可用问题。三种方式均可以做到平滑式升级在升级过程中服务仍然保持服务的连续性升级对外界是无感知的。那生产上选择哪种部署方法最合适呢这取决于哪种方法最适合你的业务和技术需求。如果你们运维自动化能力储备不够肯定是越简单越好建议蓝绿发布如果业务对用户依赖很强建议灰度发布。如果是K8S平台滚动更新是现成的方案建议先直接使用。
### 访问日志
```nginx
server {
# Relative or full path to log file
access_log /path/to/file.log;
# Turn 'on' or 'off'
access_log on;
}
```
### 域名
```nginx
server {
# Listen to yourdomain.com
server_name yourdomain.com;
# Listen to multiple domains server_name yourdomain.com www.yourdomain.com;
# Listen to all domains
server_name *.yourdomain.com;
# Listen to all top-level domains
server_name yourdomain.*;
# Listen to unspecified Hostnames (Listens to IP address itself)
server_name "";
}
```
### 静态资产
```nginx
server {
listen 80;
server_name yourdomain.com;
location / {
root /path/to/website;
}
}
```
### 重定向
```nginx
server {
listen 80;
server_name www.yourdomain.com;
return 301 http://yourdomain.com$request_uri;
}
server {
listen 80;
server_name www.yourdomain.com;
location /redirect-url {
return 301 http://otherdomain.com;
}
}
```
### 反向代理
```nginx
server {
listen 80;
server_name yourdomain.com;
location / {
proxy_pass http://0.0.0.0:3000;
# where 0.0.0.0:3000 is your application server (Ex: node.js) bound on 0.0.0.0 listening on port 3000
}
}
```
### 负载均衡
```nginx
upstream node_js {
server 0.0.0.0:3000;
server 0.0.0.0:4000;
server 123.131.121.122;
}
server {
listen 80;
server_name yourdomain.com;
location / {
proxy_pass http://node_js;
}
}
```
### SSL 协议
```nginx
server {
listen 443 ssl;
server_name yourdomain.com;
ssl on;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/privatekey.pem;
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /path/to/fullchain.pem;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_session_timeout 1h;
ssl_session_cache shared:SSL:50m;
add_header Strict-Transport-Security max-age=15768000;
}
# Permanent Redirect for HTTP to HTTPS
server {
listen 80;
server_name yourdomain.com;
return 301 https://$host$request_uri;
}
```
其实可以采用可视化的方式对 Nginx 进行配置,我在 GitHub 上发现了一款可以一键生成 Nginx 配置的神器,相当给力。
先来看看它都支持什么功能的配置反向代理、HTTPS、HTTP/2、IPv6, 缓存、WordPress、CDN、Node.js 支持、 Python (Django) 服务器等等。
如果你想在线进行配置,只需要打开网站:**https://nginxconfig.io/**
- 蓝绿发布:两套环境交替升级,旧版本保留一定时间便于回滚
- 滚动发布:按批次停止老版本实例,启动新版本实例
- 灰度发布根据比例将老版本升级例如80%用户访问是老版本20%用户访问是新版本
### 蓝绿发布
蓝绿部署中,一共有两套系统:一套是正在提供服务系统(也就是上面说的旧版),标记为“绿色”;另一套是准备发布的系统,标记为“蓝色”。两套系统都是功能完善的,并且正在运行的系统,只是系统版本和对外服务情况不同。正在对外提供服务的老系统是绿色系统,新部署的系统是蓝色系统。
![蓝绿发布示意图](images/DevOps/蓝绿发布示意图.jpg)
蓝色系统不对外提供服务,用来做啥?
用来做发布前测试,测试过程中发现任何问题,可以直接在蓝色系统上修改,不干扰用户正在使用的系统。蓝色系统经过反复的测试、修改、验证,确定达到上线标准之后,直接将用户切换到蓝色系统, 切换后的一段时间内,依旧是蓝绿两套系统并存,但是用户访问的已经是蓝色系统。这段时间内观察蓝色系统(新系统)工作状态,如果出现问题,直接切换回绿色系统。当确信对外提供服务的蓝色系统工作正常,不对外提供服务的绿色系统已经不再需要的时候,蓝色系统正式成为对外提供服务系统,成为新的绿色系统。原先的绿色系统可以销毁,将资源释放出来,用于部署下一个蓝色系统。
**特点**
- 蓝绿部署的目的是减少发布时的中断时间、能够快速撤回发布
- 发布策略简单
- 用户无感知,平滑过渡
- 升级/回滚速度快
**缺点**
- 需要准备正常业务使用资源的两倍以上服务器,防止升级期间单组无法承载业务突发
- 短时间内浪费一定资源成本
- 基础设施无改动,增大升级稳定性
### 滚动发布
一般是取出一个或者多个服务器停止服务,执行更新,并重新将其投入使用。周而复始,直到集群中所有的实例都更新成新版本。
![滚动发布示意图](images/DevOps/滚动发布示意图.jpg)
**特点**
- 用户无感知,平滑过渡
- 节约资源
**缺点**
- 部署时间慢,取决于每阶段更新时间
- 发布策略较复杂
- 无法确定OK的环境不易回滚
**部署过程**
- 先升级1个副本主要做部署验证
- 每次升级副本自动从LB上摘掉升级成功后自动加入集群
- 事先需要有自动更新策略,分为若干次,每次数量/百分比可配置
- 回滚是发布的逆过程先从LB摘掉新版本再升级老版本这个过程一般时间比较长
- 自动化要求高
### 灰度发布
灰度发布, 也叫金丝雀发布。是指在黑与白之间能够平滑过渡的一种发布方式。AB test就是一种灰度发布方式让一部分用户继续用A一部分用户开始用B如果用户对B没有什么反对意见那么逐步扩大范围把所有用户都迁移到B上面来。灰度发布可以保证整体系统的稳定在初始灰度的时候就可以发现、调整问题以保证其影响度而我们平常所说的金丝雀部署也就是灰度发布的一种方式。
![灰度发布示意图](images/DevOps/灰度发布示意图.jpg)
灰度发布是通过切换线上并存版本之间的路由权重,逐步从一个版本切换为另一个版本的过程。灰度发布只升级部分服务,即让一部分用户继续用老版本,一部分用户开始用新版本,如果用户对新版本没什么意见,那么逐步扩大范围,把所有用户都迁移到新版本上面来。
![灰度发布](images/DevOps/灰度发布.png)
**特点**
- 保证整体系统稳定性,在初始灰度的时候就可以发现、调整问题,影响范围可控
- 新功能逐步评估性能,稳定性和健康状况,如果出问题影响范围很小,相对用户体验也少
- 用户无感知,平滑过渡
**缺点**
- 自动化要求高
**部署过程**
- 从LB摘掉灰度服务器升级成功后再加入LB
- 少量用户流量到新版本
- 如果灰度服务器测试成功,升级剩余服务器

@ -1,437 +0,0 @@
### HTTP服务器
Nginx本身也是一个静态资源的服务器当只有静态资源的时候就可以使用Nginx来做服务器如果一个网站只是静态页面的话那么就可以通过这种方式来实现部署。
1、 首先在文档根目录`Docroot(/usr/local/var/www)`下创建html目录, 然后在html中放一个test.html;
2、 配置`nginx.conf`中的server
```nginx
user mengday staff;
http {
server {
listen 80;
server_name localhost;
client_max_body_size 1024M;
# 默认location
location / {
root /usr/local/var/www/html;
index index.html index.htm;
}
}
}
```
3、访问测试
- `http://localhost/` 指向`/usr/local/var/www/index.html`, index.html是安装nginx自带的html
- `http://localhost/test.html` 指向`/usr/local/var/www/html/test.html`
> 注意如果访问图片出现403 Forbidden错误可能是因为nginx.conf 的第一行user配置不对默认是#user nobody;是注释的linux下改成user root; macos下改成user 用户名 所在组; 然后重新加载配置文件或者重启,再试一下就可以了, 用户名可以通过who am i 命令来查看。
4、指令简介
- server : 用于定义服务http中可以有多个server块
- listen : 指定服务器侦听请求的IP地址和端口如果省略地址服务器将侦听所有地址如果省略端口则使用标准端口
- server_name : 服务名称,用于配置域名
- location : 用于配置映射路径uri对应的配置一个server中可以有多个location, location后面跟一个uri,可以是一个正则表达式, / 表示匹配任意路径, 当客户端访问的路径满足这个uri时就会执行location块里面的代码
- root : 根路径,当访问`http://localhost/test.html`,“/test.html”会匹配到”/”uri, 找到root为`/usr/local/var/www/html`,用户访问的资源物理地址=`root + uri = /usr/local/var/www/html + /test.html=/usr/local/var/www/html/test.html`
- index : 设置首页,当只访问`server_name`时后面不跟任何路径是不走root直接走index指令的如果访问路径中没有指定具体的文件则返回index设置的资源如果访问`http://localhost/html/` 则默认返回index.html
5、location uri正则表达式
- `.` :匹配除换行符以外的任意字符
- `?` 重复0次或1次
- `+` 重复1次或更多次
- `*` 重复0次或更多次
- `\d` :匹配数字
- `^` :匹配字符串的开始
- `$` :匹配字符串的结束
- `{n}` 重复n次
- `{n,}` 重复n次或更多次
- `[c]` 匹配单个字符c
- `[a-z]` 匹配a-z小写字母的任意一个
- `(a|b|c)` : 属线表示匹配任意一种情况每种情况使用竖线分隔一般使用小括号括括住匹配符合a字符 或是b字符 或是c字符的字符串
- `\` 反斜杠:用于转义特殊字符
小括号()之间匹配的内容,可以在后面通过`$1`来引用,`$2`表示的是前面第二个()里的内容。正则里面容易让人困惑的是`\`转义特殊字符。
### 静态服务器
在公司中经常会遇到静态服务器,通常会提供一个上传的功能,其他应用如果需要静态资源就从该静态服务器中获取。
在`/usr/local/var/www` 下分别创建images和img目录分别在每个目录下放一张`test.jpg`
```nginx
http {
server {
listen 80;
server_name localhost;
set $doc_root /usr/local/var/www;
# 默认location
location / {
root /usr/local/var/www/html;
index index.html index.htm;
}
location ^~ /images/ {
root $doc_root;
}
location ~* \.(gif|jpg|jpeg|png|bmp|ico|swf|css|js)$ {
root $doc_root/img;
}
}
}
```
自定义变量使用set指令语法 set 变量名值;引用使用变量名值;引用使用变量名; 这里自定义了doc_root变量。
静态服务器location的映射一般有两种方式
- 使用路径,如 /images/ 一般图片都会放在某个图片目录下,
- 使用后缀,如 .jpg、.png 等后缀匹配模式
访问`http://localhost/test.jpg` 会映射到 `$doc_root/img`
访问`http://localhost/images/test.jpg` 当同一个路径满足多个location时优先匹配优先级高的location由于`^~` 的优先级大于 `~`, 所以会走`/images/`对应的location
常见的location路径映射路径有以下几种
- `=` 进行普通字符精确匹配。也就是完全匹配。
- `^~` 前缀匹配。如果匹配成功则不再匹配其他location。
- `~` 表示执行一个正则匹配,区分大小写
- `~*` 表示执行一个正则匹配,不区分大小写
- `/xxx/` 常规字符串路径匹配
- `/` 通用匹配,任何请求都会匹配到
**location优先级**
当一个路径匹配多个location时究竟哪个location能匹配到时有优先级顺序的而优先级的顺序于location值的表达式类型有关和在配置文件中的先后顺序无关。相同类型的表达式字符串长的会优先匹配。推荐[Java面试题大全](http://mp.weixin.qq.com/s?__biz=MzI4Njc5NjM1NQ==&mid=2247504489&idx=1&sn=afd92248113146b086652ad7f89c7a7c&chksm=ebd5ed45dca26453c0cf91265d669711a4e2b4ea52f3ff4a00b063a9de46d69fcc599f151210&scene=21#wechat_redirect)
以下是按优先级排列说明:
- 等号类型(=)的优先级最高。一旦匹配成功,则不再查找其他匹配项,停止搜索。
- `^~`类型表达式,不属于正则表达式。一旦匹配成功,则不再查找其他匹配项,停止搜索。
- 正则表达式类型(`~ ~*`的优先级次之。如果有多个location的正则能匹配的话则使用正则表达式最长的那个。
- 常规字符串匹配类型。按前缀匹配。
- / 通用匹配,如果没有匹配到,就匹配通用的
优先级搜索问题不同类型的location映射决定是否继续向下搜索
- 等号类型、`^~`类型一旦匹配上就停止搜索了不会再匹配其他location了
- 正则表达式类型(`~ ~*`,常规字符串匹配类型`/xxx/` : 匹配到之后还会继续搜索其他其它location直到找到优先级最高的或者找到第一种情况而停止搜索
location优先级从高到底
(`location =`) > (`location 完整路径`) > (`location ^~ 路径`) > (`location ~,~* 正则顺序`) > (`location 部分起始路径`) > (`/`)
```nginx
location = / {
# 精确匹配/,主机名后面不能带任何字符串 /
[ configuration A ]
}
location / {
# 匹配所有以 / 开头的请求。
# 但是如果有更长的同类型的表达式,则选择更长的表达式。
# 如果有正则表达式可以匹配,则优先匹配正则表达式。
[ configuration B ]
}
location /documents/ {
# 匹配所有以 /documents/ 开头的请求,匹配符合以后,还要继续往下搜索。
# 但是如果有更长的同类型的表达式,则选择更长的表达式。
# 如果有正则表达式可以匹配,则优先匹配正则表达式。
[ configuration C ]
}
location ^~ /images/ {
# 匹配所有以 /images/ 开头的表达式,如果匹配成功,则停止匹配查找,停止搜索。
# 所以即便有符合的正则表达式location也不会被使用
[ configuration D ]
}
location ~* \.(gif|jpg|jpeg)$ {
# 匹配所有以 gif jpg jpeg结尾的请求。
# 但是 以 /images/开头的请求,将使用 Configuration DD具有更高的优先级
[ configuration E ]
}
location /images/ {
# 字符匹配到 /images/,还会继续往下搜索
[ configuration F ]
}
location = /test.htm {
root /usr/local/var/www/htm;
index index.htm;
}
```
注意location的优先级与location配置的位置无关
### 反向代理
反向代理应该是Nginx使用最多的功能了反向代理(Reverse Proxy)方式是指以代理服务器来接受internet上的连接请求然后将请求转发给内部网络上的服务器并将从服务器上得到的结果返回给internet上请求连接的客户端此时代理服务器对外就表现为一个反向代理服务器。
简单来说就是真实的服务器不能直接被外部网络访问,所以需要一台代理服务器,而代理服务器能被外部网络访问的同时又跟真实服务器在同一个网络环境,当然也可能是同一台服务器,端口不同而已。
反向代理通过`proxy_pass`指令来实现。
启动一个Java Web项目端口号为8081
```nginx
server {
listen 80;
server_name localhost;
location / {
proxy_pass http://localhost:8081;
proxy_set_header Host $host:$server_port;
# 设置用户ip地址
proxy_set_header X-Forwarded-For $remote_addr;
# 当请求服务器出错去寻找其他服务器
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503;
}
}
```
当我们访问localhost的时候就相当于访问 `localhost:8081`
### 负载均衡
负载均衡也是Nginx常用的一个功能负载均衡其意思就是分摊到多个操作单元上进行执行例如Web服务器、FTP服务器、企业关键应用服务器和其它关键任务服务器等从而共同完成工作任务。
简单而言就是当有2台或以上服务器时根据规则随机的将请求分发到指定的服务器上处理负载均衡配置一般都需要同时配置反向代理通过反向代理跳转到负载均衡。而Nginx目前支持自带3种负载均衡策略还有2种常用的第三方策略。负载均衡通过upstream指令来实现。
#### RR(round robin :轮询 默认)
每个请求按时间顺序逐一分配到不同的后端服务器,也就是说第一次请求分配到第一台服务器上,第二次请求分配到第二台服务器上,如果只有两台服务器,第三次请求继续分配到第一台上,这样循环轮询下去,也就是服务器接收请求的比例是 1:1 如果后端服务器down掉能自动剔除。轮询是默认配置不需要太多的配置同一个项目分别使用8081和8082端口启动项目
```nginx
upstream web_servers {
server localhost:8081;
server localhost:8082;
}
server {
listen 80;
server_name localhost;
#access_log logs/host.access.log main;
location / {
proxy_pass http://web_servers;
# 必须指定Header Host
proxy_set_header Host $host:$server_port;
}
}
```
访问地址仍然可以获得响应 `http://localhost/api/user/login?username=zhangsan&password=111111` ,这种方式是轮询的
#### 权重
指定轮询几率weight和访问比率成正比, 也就是服务器接收请求的比例就是各自配置的weight的比例用于后端服务器性能不均的情况,比如服务器性能差点就少接收点请求,服务器性能好点就多处理点请求。
```nginx
upstream test {
server localhost:8081 weight=1;
server localhost:8082 weight=3;
server localhost:8083 weight=4 backup;
}
```
示例是4次请求只有一次被分配到8081上其他3次分配到8082上。backup是指热备只有当8081和8082都宕机的情况下才走8083
#### ip_hash
上面的2种方式都有一个问题那就是下一个请求来的时候请求可能分发到另外一个服务器当我们的程序不是无状态的时候(采用了session保存数据)这时候就有一个很大的很问题了比如把登录信息保存到了session中那么跳转到另外一台服务器的时候就需要重新登录了所以很多时候我们需要一个客户只访问一个服务器那么就需要用iphash了iphash的每个请求按访问ip的hash结果分配这样每个访客固定访问一个后端服务器可以解决session的问题。
```nginx
upstream test {
ip_hash;
server localhost:8080;
server localhost:8081;
}
```
#### fair(第三方)
按后端服务器的响应时间来分配请求,响应时间短的优先分配。这个配置是为了更快的给用户响应
```nginx
upstream backend {
fair;
server localhost:8080;
server localhost:8081;
}
```
#### url_hash(第三方)
按访问url的hash结果来分配请求使每个url定向到同一个后端服务器后端服务器为缓存时比较有效。在upstream中加入hash语句server语句中不能写入weight等其他的参数`hash_method`是使用的hash算法
```nginx
upstream backend {
hash $request_uri;
hash_method crc32;
server localhost:8080;
server localhost:8081;
}
```
以上5种负载均衡各自适用不同情况下使用所以可以根据实际情况选择使用哪种策略模式,不过fair和url_hash需要安装第三方模块才能使用。
### 动静分离
动静分离是让动态网站里的动态网页根据一定规则把不变的资源和经常变的资源区分开来,动静资源做好了拆分以后,我们就可以根据静态资源的特点将其做缓存操作,这就是网站静态化处理的核心思路。
```nginx
upstream web_servers {
server localhost:8081;
server localhost:8082;
}
server {
listen 80;
server_name localhost;
set $doc_root /usr/local/var/www;
location ~* \.(gif|jpg|jpeg|png|bmp|ico|swf|css|js)$ {
root $doc_root/img;
}
location / {
proxy_pass http://web_servers;
# 必须指定Header Host
proxy_set_header Host $host:$server_port;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root $doc_root;
}
}
```
### 其他
#### return指令
返回http状态码 和 可选的第二个参数可以是重定向的URL
```nginx
location /permanently/moved/url {
return 301 http://www.example.com/moved/here;
}
```
#### rewrite指令
重写URI请求 rewrite通过使用rewrite指令在请求处理期间多次修改请求URI该指令具有一个可选参数和两个必需参数。
第一个(必需)参数是请求URI必须匹配的正则表达式。
第二个参数是用于替换匹配URI的URI。
可选的第三个参数是可以停止进一步重写指令的处理或发送重定向(代码301或302)的标志
```nginx
location /users/ {
rewrite ^/users/(.*)$ /show?user=$1 break;
}
```
#### error_page指令
使用error_page指令您可以配置NGINX返回自定义页面以及错误代码替换响应中的其他错误代码或将浏览器重定向到其他URI。在以下示例中`error_page`指令指定要返回404页面错误代码的页面(/404.html)。
```nginx
error_page 404 /404.html;
```
#### 日志
访问日志:需要开启压缩 gzip on; 否则不生成日志文件,打开`log_format`、`access_log`注释
```nginx
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /usr/local/etc/nginx/logs/host.access.log main;
gzip on;
```
#### deny 指令
```nginx
# 禁止访问某个目录
location ~* \.(txt|doc)${
root $doc_root;
deny all;
}
```
#### 内置变量
nginx的配置文件中可以使用的内置变量以美元符`$`开始也有人叫全局变量。其中部分预定义的变量的值是可以改变的。另外关注Java知音公众号回复“后端面试”送你一份面试题宝典
- `$args` `#`这个变量等于请求行中的参数,同`$query_string`
- `$content_length` 请求头中的Content-length字段。
- `$content_type` 请求头中的Content-Type字段。
- `$document_root` 当前请求在root指令中指定的值。
- `$host` :请求主机头字段,否则为服务器名称。
- `$http_user_agent` 客户端agent信息
- `$http_cookie` 客户端cookie信息
- `$limit_rate` :这个变量可以限制连接速率。
- `$request_method` 客户端请求的动作通常为GET或POST。
- `$remote_addr` 客户端的IP地址。
- `$remote_port` :客户端的端口。
- `$remote_user` 已经经过Auth Basic Module验证的用户名。
- `$request_filename` 当前请求的文件路径由root或alias指令与URI请求生成。
- `$scheme` HTTP方法如httphttps
- `$server_protocol` 请求使用的协议通常是HTTP/1.0或HTTP/1.1。
- `$server_addr` :服务器地址,在完成一次系统调用后可以确定这个值。
- `$server_name` :服务器名称。
- `$server_port` :请求到达服务器的端口号。
- `$request_uri` 包含请求参数的原始URI不包含主机名”`/foo/bar.php?arg=baz`”。
- `$uri` 不带请求参数的当前URI`$uri`不包含主机名,如”`/foo/bar.html`”。
- `$document_uri` :与`$uri`相同

@ -1,58 +1,147 @@
VMware15.5安装傻瓜式安装只记录变动步骤其余都下一步软件安装位置自己选择最好别选c盘软件地址https://www.nocmd.com/windows/740.html内含激活码安装时需要注意它文件不会在一个文件夹下自己多建一个版本文件夹方便管理。
### 侦听端口
```nginx
server {
# Standard HTTP Protocol
listen 80;
# Standard HTTPS Protocol
listen 443 ssl;
# For http2
listen 443 ssl http2;
# Listen on 80 using IPv6
listen [::]:80;
# Listen only on using IPv6
listen [::]:80 ipv6only=on;
}
```
![在这里插入图片描述](images/DevOps/20210717124755337.png)
![在这里插入图片描述](images/DevOps/2021071712480749.png)
**文件 > 新建虚拟机**
![在这里插入图片描述](images/DevOps/20210717125629709.png)
![在这里插入图片描述](images/DevOps/20210717125643324.png)
![在这里插入图片描述](images/DevOps/20210717125659179.png)
![在这里插入图片描述](images/DevOps/20210717125714713.png)
![在这里插入图片描述](images/DevOps/20210717125727428.png)
![在这里插入图片描述](images/DevOps/20210717125743717.png)
点击安装计算机的设置 > 选择镜像后 > 点确定:
![在这里插入图片描述](images/DevOps/20210717125911587.png)
开启虚拟机》选第一个install centos7
![在这里插入图片描述](images/DevOps/20210717125946871.png)
等待一段时间不要乱点乱点会卡死》软件选择》最小安装或gui服务器或gnome桌面选好后点完成。开发中一般都选最小安装需要什么软件在自行选择但其它安装可以省略jdkmysql等安装会自行安装。
![在这里插入图片描述](images/DevOps/20210717130014474.png)
在这一步也可以选择自动配置分区,这里更快,这里我选择我要配置分区。
![在这里插入图片描述](images/DevOps/202107171300526.png)设置好/boot要1Gswap要2G剩余都在根目录分区大小后设备类型点标准分区点完成。
![在这里插入图片描述](images/DevOps/20210717130123660.png)
点接受更改
![在这里插入图片描述](images/DevOps/20210717130215238.png)
网络和主机名设置,需要联网就打开以太网。
![在这里插入图片描述](images/DevOps/20210717130237758.png)
最后一个像一把锁的安检策略可以关闭。
点开始安装
在这个页面配置root账号密码创建用户账号密码。在实际开发中root账号要复杂点避免被破解。
![在这里插入图片描述](images/DevOps/20210717130319942.png)等待完成后,点击重启。
![在这里插入图片描述](images/DevOps/20210717130343591.png)
![在这里插入图片描述](images/DevOps/20210717130404642.png)
再把网络连接打开。
### 访问日志
```nginx
server {
# Relative or full path to log file
access_log /path/to/file.log;
# Turn 'on' or 'off'
access_log on;
}
```
### 域名
```nginx
server {
# Listen to yourdomain.com
server_name yourdomain.com;
# Listen to multiple domains server_name yourdomain.com www.yourdomain.com;
# Listen to all domains
server_name *.yourdomain.com;
# Listen to all top-level domains
server_name yourdomain.*;
# Listen to unspecified Hostnames (Listens to IP address itself)
server_name "";
}
```
### 静态资产
```nginx
server {
listen 80;
server_name yourdomain.com;
location / {
root /path/to/website;
}
}
```
### 重定向
```nginx
server {
listen 80;
server_name www.yourdomain.com;
return 301 http://yourdomain.com$request_uri;
}
server {
listen 80;
server_name www.yourdomain.com;
location /redirect-url {
return 301 http://otherdomain.com;
}
}
```
### 反向代理
```nginx
server {
listen 80;
server_name yourdomain.com;
location / {
proxy_pass http://0.0.0.0:3000;
# where 0.0.0.0:3000 is your application server (Ex: node.js) bound on 0.0.0.0 listening on port 3000
}
}
```
### 负载均衡
```nginx
upstream node_js {
server 0.0.0.0:3000;
server 0.0.0.0:4000;
server 123.131.121.122;
}
server {
listen 80;
server_name yourdomain.com;
location / {
proxy_pass http://node_js;
}
}
```
### SSL 协议
```nginx
server {
listen 443 ssl;
server_name yourdomain.com;
ssl on;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/privatekey.pem;
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /path/to/fullchain.pem;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_session_timeout 1h;
ssl_session_cache shared:SSL:50m;
add_header Strict-Transport-Security max-age=15768000;
}
# Permanent Redirect for HTTP to HTTPS
server {
listen 80;
server_name yourdomain.com;
return 301 https://$host$request_uri;
}
```
其实可以采用可视化的方式对 Nginx 进行配置,我在 GitHub 上发现了一款可以一键生成 Nginx 配置的神器,相当给力。
先来看看它都支持什么功能的配置反向代理、HTTPS、HTTP/2、IPv6, 缓存、WordPress、CDN、Node.js 支持、 Python (Django) 服务器等等。
如果你想在线进行配置,只需要打开网站:**https://nginxconfig.io/**

@ -1,3 +1,437 @@
- 桥接模式同一网段中最多只能连接255台机子一旦超出容易造成IP冲突。IP地址的前3位就是网段192.168.0.1
- NAT网络地址装换模式虚拟机和外部通信不会造成IP冲突。虚拟机地址不再是以0开头而是生成1-255之间的数如192.168.6.1然后主机会生成一个对应的虚拟网卡如192.168.6.6两者能通信。这种模式下虚拟机能访问192.168.0.1由于网段不同192.168.0.1不能访问虚拟机
- 主机模式:虚拟网络对主机可见,虚拟机不能上网
### HTTP服务器
Nginx本身也是一个静态资源的服务器当只有静态资源的时候就可以使用Nginx来做服务器如果一个网站只是静态页面的话那么就可以通过这种方式来实现部署。
1、 首先在文档根目录`Docroot(/usr/local/var/www)`下创建html目录, 然后在html中放一个test.html;
2、 配置`nginx.conf`中的server
```nginx
user mengday staff;
http {
server {
listen 80;
server_name localhost;
client_max_body_size 1024M;
# 默认location
location / {
root /usr/local/var/www/html;
index index.html index.htm;
}
}
}
```
3、访问测试
- `http://localhost/` 指向`/usr/local/var/www/index.html`, index.html是安装nginx自带的html
- `http://localhost/test.html` 指向`/usr/local/var/www/html/test.html`
> 注意如果访问图片出现403 Forbidden错误可能是因为nginx.conf 的第一行user配置不对默认是#user nobody;是注释的linux下改成user root; macos下改成user 用户名 所在组; 然后重新加载配置文件或者重启,再试一下就可以了, 用户名可以通过who am i 命令来查看。
4、指令简介
- server : 用于定义服务http中可以有多个server块
- listen : 指定服务器侦听请求的IP地址和端口如果省略地址服务器将侦听所有地址如果省略端口则使用标准端口
- server_name : 服务名称,用于配置域名
- location : 用于配置映射路径uri对应的配置一个server中可以有多个location, location后面跟一个uri,可以是一个正则表达式, / 表示匹配任意路径, 当客户端访问的路径满足这个uri时就会执行location块里面的代码
- root : 根路径,当访问`http://localhost/test.html`,“/test.html”会匹配到”/”uri, 找到root为`/usr/local/var/www/html`,用户访问的资源物理地址=`root + uri = /usr/local/var/www/html + /test.html=/usr/local/var/www/html/test.html`
- index : 设置首页,当只访问`server_name`时后面不跟任何路径是不走root直接走index指令的如果访问路径中没有指定具体的文件则返回index设置的资源如果访问`http://localhost/html/` 则默认返回index.html
5、location uri正则表达式
- `.` :匹配除换行符以外的任意字符
- `?` 重复0次或1次
- `+` 重复1次或更多次
- `*` 重复0次或更多次
- `\d` :匹配数字
- `^` :匹配字符串的开始
- `$` :匹配字符串的结束
- `{n}` 重复n次
- `{n,}` 重复n次或更多次
- `[c]` 匹配单个字符c
- `[a-z]` 匹配a-z小写字母的任意一个
- `(a|b|c)` : 属线表示匹配任意一种情况每种情况使用竖线分隔一般使用小括号括括住匹配符合a字符 或是b字符 或是c字符的字符串
- `\` 反斜杠:用于转义特殊字符
小括号()之间匹配的内容,可以在后面通过`$1`来引用,`$2`表示的是前面第二个()里的内容。正则里面容易让人困惑的是`\`转义特殊字符。
### 静态服务器
在公司中经常会遇到静态服务器,通常会提供一个上传的功能,其他应用如果需要静态资源就从该静态服务器中获取。
在`/usr/local/var/www` 下分别创建images和img目录分别在每个目录下放一张`test.jpg`
```nginx
http {
server {
listen 80;
server_name localhost;
set $doc_root /usr/local/var/www;
# 默认location
location / {
root /usr/local/var/www/html;
index index.html index.htm;
}
location ^~ /images/ {
root $doc_root;
}
location ~* \.(gif|jpg|jpeg|png|bmp|ico|swf|css|js)$ {
root $doc_root/img;
}
}
}
```
自定义变量使用set指令语法 set 变量名值;引用使用变量名值;引用使用变量名; 这里自定义了doc_root变量。
静态服务器location的映射一般有两种方式
- 使用路径,如 /images/ 一般图片都会放在某个图片目录下,
- 使用后缀,如 .jpg、.png 等后缀匹配模式
访问`http://localhost/test.jpg` 会映射到 `$doc_root/img`
访问`http://localhost/images/test.jpg` 当同一个路径满足多个location时优先匹配优先级高的location由于`^~` 的优先级大于 `~`, 所以会走`/images/`对应的location
常见的location路径映射路径有以下几种
- `=` 进行普通字符精确匹配。也就是完全匹配。
- `^~` 前缀匹配。如果匹配成功则不再匹配其他location。
- `~` 表示执行一个正则匹配,区分大小写
- `~*` 表示执行一个正则匹配,不区分大小写
- `/xxx/` 常规字符串路径匹配
- `/` 通用匹配,任何请求都会匹配到
**location优先级**
当一个路径匹配多个location时究竟哪个location能匹配到时有优先级顺序的而优先级的顺序于location值的表达式类型有关和在配置文件中的先后顺序无关。相同类型的表达式字符串长的会优先匹配。推荐[Java面试题大全](http://mp.weixin.qq.com/s?__biz=MzI4Njc5NjM1NQ==&mid=2247504489&idx=1&sn=afd92248113146b086652ad7f89c7a7c&chksm=ebd5ed45dca26453c0cf91265d669711a4e2b4ea52f3ff4a00b063a9de46d69fcc599f151210&scene=21#wechat_redirect)
以下是按优先级排列说明:
- 等号类型(=)的优先级最高。一旦匹配成功,则不再查找其他匹配项,停止搜索。
- `^~`类型表达式,不属于正则表达式。一旦匹配成功,则不再查找其他匹配项,停止搜索。
- 正则表达式类型(`~ ~*`的优先级次之。如果有多个location的正则能匹配的话则使用正则表达式最长的那个。
- 常规字符串匹配类型。按前缀匹配。
- / 通用匹配,如果没有匹配到,就匹配通用的
优先级搜索问题不同类型的location映射决定是否继续向下搜索
- 等号类型、`^~`类型一旦匹配上就停止搜索了不会再匹配其他location了
- 正则表达式类型(`~ ~*`,常规字符串匹配类型`/xxx/` : 匹配到之后还会继续搜索其他其它location直到找到优先级最高的或者找到第一种情况而停止搜索
location优先级从高到底
(`location =`) > (`location 完整路径`) > (`location ^~ 路径`) > (`location ~,~* 正则顺序`) > (`location 部分起始路径`) > (`/`)
```nginx
location = / {
# 精确匹配/,主机名后面不能带任何字符串 /
[ configuration A ]
}
location / {
# 匹配所有以 / 开头的请求。
# 但是如果有更长的同类型的表达式,则选择更长的表达式。
# 如果有正则表达式可以匹配,则优先匹配正则表达式。
[ configuration B ]
}
location /documents/ {
# 匹配所有以 /documents/ 开头的请求,匹配符合以后,还要继续往下搜索。
# 但是如果有更长的同类型的表达式,则选择更长的表达式。
# 如果有正则表达式可以匹配,则优先匹配正则表达式。
[ configuration C ]
}
location ^~ /images/ {
# 匹配所有以 /images/ 开头的表达式,如果匹配成功,则停止匹配查找,停止搜索。
# 所以即便有符合的正则表达式location也不会被使用
[ configuration D ]
}
location ~* \.(gif|jpg|jpeg)$ {
# 匹配所有以 gif jpg jpeg结尾的请求。
# 但是 以 /images/开头的请求,将使用 Configuration DD具有更高的优先级
[ configuration E ]
}
location /images/ {
# 字符匹配到 /images/,还会继续往下搜索
[ configuration F ]
}
location = /test.htm {
root /usr/local/var/www/htm;
index index.htm;
}
```
注意location的优先级与location配置的位置无关
### 反向代理
反向代理应该是Nginx使用最多的功能了反向代理(Reverse Proxy)方式是指以代理服务器来接受internet上的连接请求然后将请求转发给内部网络上的服务器并将从服务器上得到的结果返回给internet上请求连接的客户端此时代理服务器对外就表现为一个反向代理服务器。
简单来说就是真实的服务器不能直接被外部网络访问,所以需要一台代理服务器,而代理服务器能被外部网络访问的同时又跟真实服务器在同一个网络环境,当然也可能是同一台服务器,端口不同而已。
反向代理通过`proxy_pass`指令来实现。
启动一个Java Web项目端口号为8081
```nginx
server {
listen 80;
server_name localhost;
location / {
proxy_pass http://localhost:8081;
proxy_set_header Host $host:$server_port;
# 设置用户ip地址
proxy_set_header X-Forwarded-For $remote_addr;
# 当请求服务器出错去寻找其他服务器
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503;
}
}
```
当我们访问localhost的时候就相当于访问 `localhost:8081`
### 负载均衡
负载均衡也是Nginx常用的一个功能负载均衡其意思就是分摊到多个操作单元上进行执行例如Web服务器、FTP服务器、企业关键应用服务器和其它关键任务服务器等从而共同完成工作任务。
简单而言就是当有2台或以上服务器时根据规则随机的将请求分发到指定的服务器上处理负载均衡配置一般都需要同时配置反向代理通过反向代理跳转到负载均衡。而Nginx目前支持自带3种负载均衡策略还有2种常用的第三方策略。负载均衡通过upstream指令来实现。
#### RR(round robin :轮询 默认)
每个请求按时间顺序逐一分配到不同的后端服务器,也就是说第一次请求分配到第一台服务器上,第二次请求分配到第二台服务器上,如果只有两台服务器,第三次请求继续分配到第一台上,这样循环轮询下去,也就是服务器接收请求的比例是 1:1 如果后端服务器down掉能自动剔除。轮询是默认配置不需要太多的配置同一个项目分别使用8081和8082端口启动项目
```nginx
upstream web_servers {
server localhost:8081;
server localhost:8082;
}
server {
listen 80;
server_name localhost;
#access_log logs/host.access.log main;
location / {
proxy_pass http://web_servers;
# 必须指定Header Host
proxy_set_header Host $host:$server_port;
}
}
```
访问地址仍然可以获得响应 `http://localhost/api/user/login?username=zhangsan&password=111111` ,这种方式是轮询的
#### 权重
指定轮询几率weight和访问比率成正比, 也就是服务器接收请求的比例就是各自配置的weight的比例用于后端服务器性能不均的情况,比如服务器性能差点就少接收点请求,服务器性能好点就多处理点请求。
```nginx
upstream test {
server localhost:8081 weight=1;
server localhost:8082 weight=3;
server localhost:8083 weight=4 backup;
}
```
示例是4次请求只有一次被分配到8081上其他3次分配到8082上。backup是指热备只有当8081和8082都宕机的情况下才走8083
#### ip_hash
上面的2种方式都有一个问题那就是下一个请求来的时候请求可能分发到另外一个服务器当我们的程序不是无状态的时候(采用了session保存数据)这时候就有一个很大的很问题了比如把登录信息保存到了session中那么跳转到另外一台服务器的时候就需要重新登录了所以很多时候我们需要一个客户只访问一个服务器那么就需要用iphash了iphash的每个请求按访问ip的hash结果分配这样每个访客固定访问一个后端服务器可以解决session的问题。
```nginx
upstream test {
ip_hash;
server localhost:8080;
server localhost:8081;
}
```
#### fair(第三方)
按后端服务器的响应时间来分配请求,响应时间短的优先分配。这个配置是为了更快的给用户响应
```nginx
upstream backend {
fair;
server localhost:8080;
server localhost:8081;
}
```
#### url_hash(第三方)
按访问url的hash结果来分配请求使每个url定向到同一个后端服务器后端服务器为缓存时比较有效。在upstream中加入hash语句server语句中不能写入weight等其他的参数`hash_method`是使用的hash算法
```nginx
upstream backend {
hash $request_uri;
hash_method crc32;
server localhost:8080;
server localhost:8081;
}
```
以上5种负载均衡各自适用不同情况下使用所以可以根据实际情况选择使用哪种策略模式,不过fair和url_hash需要安装第三方模块才能使用。
### 动静分离
动静分离是让动态网站里的动态网页根据一定规则把不变的资源和经常变的资源区分开来,动静资源做好了拆分以后,我们就可以根据静态资源的特点将其做缓存操作,这就是网站静态化处理的核心思路。
```nginx
upstream web_servers {
server localhost:8081;
server localhost:8082;
}
server {
listen 80;
server_name localhost;
set $doc_root /usr/local/var/www;
location ~* \.(gif|jpg|jpeg|png|bmp|ico|swf|css|js)$ {
root $doc_root/img;
}
location / {
proxy_pass http://web_servers;
# 必须指定Header Host
proxy_set_header Host $host:$server_port;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root $doc_root;
}
}
```
### 其他
#### return指令
返回http状态码 和 可选的第二个参数可以是重定向的URL
```nginx
location /permanently/moved/url {
return 301 http://www.example.com/moved/here;
}
```
#### rewrite指令
重写URI请求 rewrite通过使用rewrite指令在请求处理期间多次修改请求URI该指令具有一个可选参数和两个必需参数。
第一个(必需)参数是请求URI必须匹配的正则表达式。
第二个参数是用于替换匹配URI的URI。
可选的第三个参数是可以停止进一步重写指令的处理或发送重定向(代码301或302)的标志
```nginx
location /users/ {
rewrite ^/users/(.*)$ /show?user=$1 break;
}
```
#### error_page指令
使用error_page指令您可以配置NGINX返回自定义页面以及错误代码替换响应中的其他错误代码或将浏览器重定向到其他URI。在以下示例中`error_page`指令指定要返回404页面错误代码的页面(/404.html)。
```nginx
error_page 404 /404.html;
```
#### 日志
访问日志:需要开启压缩 gzip on; 否则不生成日志文件,打开`log_format`、`access_log`注释
```nginx
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /usr/local/etc/nginx/logs/host.access.log main;
gzip on;
```
#### deny 指令
```nginx
# 禁止访问某个目录
location ~* \.(txt|doc)${
root $doc_root;
deny all;
}
```
#### 内置变量
nginx的配置文件中可以使用的内置变量以美元符`$`开始也有人叫全局变量。其中部分预定义的变量的值是可以改变的。另外关注Java知音公众号回复“后端面试”送你一份面试题宝典
- `$args` `#`这个变量等于请求行中的参数,同`$query_string`
- `$content_length` 请求头中的Content-length字段。
- `$content_type` 请求头中的Content-Type字段。
- `$document_root` 当前请求在root指令中指定的值。
- `$host` :请求主机头字段,否则为服务器名称。
- `$http_user_agent` 客户端agent信息
- `$http_cookie` 客户端cookie信息
- `$limit_rate` :这个变量可以限制连接速率。
- `$request_method` 客户端请求的动作通常为GET或POST。
- `$remote_addr` 客户端的IP地址。
- `$remote_port` :客户端的端口。
- `$remote_user` 已经经过Auth Basic Module验证的用户名。
- `$request_filename` 当前请求的文件路径由root或alias指令与URI请求生成。
- `$scheme` HTTP方法如httphttps
- `$server_protocol` 请求使用的协议通常是HTTP/1.0或HTTP/1.1。
- `$server_addr` :服务器地址,在完成一次系统调用后可以确定这个值。
- `$server_name` :服务器名称。
- `$server_port` :请求到达服务器的端口号。
- `$request_uri` 包含请求参数的原始URI不包含主机名”`/foo/bar.php?arg=baz`”。
- `$uri` 不带请求参数的当前URI`$uri`不包含主机名,如”`/foo/bar.html`”。
- `$document_uri` :与`$uri`相同

@ -0,0 +1,58 @@
VMware15.5安装傻瓜式安装只记录变动步骤其余都下一步软件安装位置自己选择最好别选c盘软件地址https://www.nocmd.com/windows/740.html内含激活码安装时需要注意它文件不会在一个文件夹下自己多建一个版本文件夹方便管理。
![在这里插入图片描述](images/DevOps/20210717124755337.png)
![在这里插入图片描述](images/DevOps/2021071712480749.png)
**文件 > 新建虚拟机**
![在这里插入图片描述](images/DevOps/20210717125629709.png)
![在这里插入图片描述](images/DevOps/20210717125643324.png)
![在这里插入图片描述](images/DevOps/20210717125659179.png)
![在这里插入图片描述](images/DevOps/20210717125714713.png)
![在这里插入图片描述](images/DevOps/20210717125727428.png)
![在这里插入图片描述](images/DevOps/20210717125743717.png)
点击安装计算机的设置 > 选择镜像后 > 点确定:
![在这里插入图片描述](images/DevOps/20210717125911587.png)
开启虚拟机》选第一个install centos7
![在这里插入图片描述](images/DevOps/20210717125946871.png)
等待一段时间不要乱点乱点会卡死》软件选择》最小安装或gui服务器或gnome桌面选好后点完成。开发中一般都选最小安装需要什么软件在自行选择但其它安装可以省略jdkmysql等安装会自行安装。
![在这里插入图片描述](images/DevOps/20210717130014474.png)
在这一步也可以选择自动配置分区,这里更快,这里我选择我要配置分区。
![在这里插入图片描述](images/DevOps/202107171300526.png)设置好/boot要1Gswap要2G剩余都在根目录分区大小后设备类型点标准分区点完成。
![在这里插入图片描述](images/DevOps/20210717130123660.png)
点接受更改
![在这里插入图片描述](images/DevOps/20210717130215238.png)
网络和主机名设置,需要联网就打开以太网。
![在这里插入图片描述](images/DevOps/20210717130237758.png)
最后一个像一把锁的安检策略可以关闭。
点开始安装
在这个页面配置root账号密码创建用户账号密码。在实际开发中root账号要复杂点避免被破解。
![在这里插入图片描述](images/DevOps/20210717130319942.png)等待完成后,点击重启。
![在这里插入图片描述](images/DevOps/20210717130343591.png)
![在这里插入图片描述](images/DevOps/20210717130404642.png)
再把网络连接打开。

@ -0,0 +1,3 @@
- 桥接模式同一网段中最多只能连接255台机子一旦超出容易造成IP冲突。IP地址的前3位就是网段192.168.0.1
- NAT网络地址装换模式虚拟机和外部通信不会造成IP冲突。虚拟机地址不再是以0开头而是生成1-255之间的数如192.168.6.1然后主机会生成一个对应的虚拟网卡如192.168.6.6两者能通信。这种模式下虚拟机能访问192.168.0.1由于网段不同192.168.0.1不能访问虚拟机
- 主机模式:虚拟网络对主机可见,虚拟机不能上网

@ -33,12 +33,15 @@
* 🏁 Docker
* [✍ 安装](src/DevOps/601 "安装")
* [✍ 常用命令](src/DevOps/602 "常用命令")
* [✍ 其它操作](src/DevOps/603 "其它操作")
* 🏁 CI/CD
* [✍ 发布方式](src/DevOps/701 "发布方式")
* 🏁 Nginx
* [✍ 常用配置](src/DevOps/701 "常用配置")
* [✍ 应用场景](src/DevOps/702 "应用场景")
* [✍ 常用配置](src/DevOps/801 "常用配置")
* [✍ 应用场景](src/DevOps/802 "应用场景")
* 🏁 VMware
* [✍ 虚拟机安装](src/DevOps/801 "虚拟机安装")
* [✍ 虚拟机网络连接方式](src/DevOps/802 "虚拟机网络连接方式")
* [✍ 安装vmtools](src/DevOps/803 "安装vmtools")
* [✍ 虚拟机目录](src/DevOps/804 "虚拟机目录")
* [✍ CentOS7找回root密码](src/DevOps/805 "CentOS7找回root密码")
* [✍ 虚拟机安装](src/DevOps/901 "虚拟机安装")
* [✍ 虚拟机网络连接方式](src/DevOps/902 "虚拟机网络连接方式")
* [✍ 安装vmtools](src/DevOps/903 "安装vmtools")
* [✍ 虚拟机目录](src/DevOps/904 "虚拟机目录")
* [✍ CentOS7找回root密码](src/DevOps/905 "CentOS7找回root密码")

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

@ -1,5 +1,74 @@
从功能上,流程可以分为服务启动、建立连接、读取数据、业务处理、发送数据、关闭连接以及关闭服务。整体流程如下所示(图中没有包含关闭的部分)
![Netty逻辑架构](images/Middleware/Netty逻辑架构.png)
![Netty整体流程](images/Middleware/Netty整体流程.png)
Netty内部逻辑的流转
![Netty内部逻辑的流转](images/Middleware/Netty内部逻辑的流转.png)
### 网络通信层
网络通信层的职责是**执行网络I/O的操作**它支持多种网络协议和I/O模型的连接操作。当网络数据读取到内核缓冲区后会触发各种网络事件这些网络事件会分发给事件调度层进行处理。三个核心组件包括
- **BootStrap和ServerBootStrap**
主要负责整个Netty程序的启动、初始化、服务器连接等过程它相当于一条主线串联了Netty的其它核心组件。Bootstrap和ServerBootStrap十分相似两者的区别在于
- Bootstrap可用于连接远端服务器只绑定一个EventLoopGroup
- ServerBootStrap则用于服务端启动绑定本地端口会绑定两个EventLoopGroup通常称为Boss和WorkerBoss 会不停地接收新的连接然后将连接分配给一个个Worker处理连接
- **Channel**
提供了基本的API用于网络I/O操作如register、bind、connect、read、write、flush 等。Netty的Channel是以JDK NIO Channel为基础的相比较于JDK NIONetty的Channel提供了更高层次的抽象同时屏蔽了底层Socket的复杂性赋予了Channel更加强大的功能在使用Netty时基本不需要再与Java Socket类直接打交道。
### 事件调度层
事件调度层的职责是通过Reactor线程模型对各类事件进行聚合处理通过Selector主循环线程集成多种事件I/O事件、信号事件、定时事件等实际的业务处理逻辑是交由服务编排层中相关的Handler完成。两个核心组件包括
- **EventLoopGroup、EventLoop**
EventLoopGroup是Netty的核心处理引擎本质是一个线程池主要负责接收I/O请求并分配线程执行处理请求。EventLoopGroup的实现类**NioEventLoopGroup**也是 Netty 中最被推荐使用的线程模型。是基于NIO模型开发的可以把NioEventLoopGroup理解为一个线程池每个线程负责处理多个Channel而同一个Channel只会对应一个线程。
![Netty事件调度层](images/Middleware/Netty事件调度层.png)
- 一个 EventLoopGroup 往往包含一个或者多个 EventLoop。EventLoop 用于处理 Channel 生命周期内的所有 I/O 事件,如 accept、connect、read、write 等 I/O 事件
- EventLoop 同一时间会与一个线程绑定,每个 EventLoop 负责处理多个 Channel
- 每新建一个 ChannelEventLoopGroup 会选择一个 EventLoop 与其绑定。该 Channel 在生命周期内都可以对 EventLoop 进行多次绑定和解绑
其实EventLoopGroup是Netty Reactor线程模型的具体实现方式Netty通过创建不同的EventLoopGroup参数配置就可以支持Reactor的三种线程模型
- **单线程模型**EventLoopGroup只包含一个EventLoopBoss和Worker使用同一个EventLoopGroup
- **多线程模型**EventLoopGroup包含多个EventLoopBoss和Worker使用同一个EventLoopGroup
- **主从多线程模型**EventLoopGroup包含多个EventLoopBoss是主ReactorWorker是从Reactor它们分别使用不同的EventLoopGroup主Reactor负责新的网络连接Channel创建然后把Channel注册到从Reactor
### 服务编排层
服务编排层的职责是负责组装各类服务是Netty的核心处理链用以实现网络事件的动态编排和有序传播。核心组件包括
- **ChannelPipeline**
ChannelPipeline 是 Netty 的核心编排组件,负责组装各种 ChannelHandler实际数据的编解码以及加工处理操作都是由 ChannelHandler 完成的。ChannelPipeline 可以理解为ChannelHandler 的实例列表——内部通过双向链表将不同的 ChannelHandler 链接在一起。当 I/O 读写事件触发时ChannelPipeline 会依次调用 ChannelHandler 列表对 Channel 的数据进行拦截和处理。
ChannelPipeline 是线程安全的,因为每一个新的 Channel 都会对应绑定一个新的 ChannelPipeline。一个 ChannelPipeline 关联一个 EventLoop一个 EventLoop 仅会绑定一个线程。
ChannelPipeline、ChannelHandler 都是高度可定制的组件。开发者可以通过这两个核心组件掌握对 Channel 数据操作的控制权。下面我们看一下 ChannelPipeline 的结构图:
![ChannelPipeline结构图](images/Middleware/ChannelPipeline结构图.png)
ChannelPipeline中包含入站ChannelInboundHandler和出站 ChannelOutboundHandler两种处理器结合客户端和服务端的数据收发流程
![ClientServerChannelPipeline](images/Middleware/ClientServerChannelPipeline.png)
- **ChannelHandler & ChannelHandlerContext**
数据的编解码工作以及其他转换工作实际都是通过 ChannelHandler 处理的。ChannelHandlerContext 用于保存 ChannelHandler 上下文,通过 ChannelHandlerContext 可以知道 ChannelPipeline 和 ChannelHandler 的关联关系。ChannelHandlerContext 可以实现 ChannelHandler 之间的交互ChannelHandlerContext 包含了 ChannelHandler 生命周期的所有事件,如 connect、bind、read、flush、write、close 等。
![ChannelHandler](images/Middleware/ChannelHandler.png)
每创建一个 Channel 都会绑定一个新的 ChannelPipelineChannelPipeline 中每加入一个 ChannelHandler 都会绑定一个 ChannelHandlerContext。
![Netty线程模型](images/Middleware/Netty线程模型.png)

@ -1,85 +1,39 @@
Netty中Reactor线程和worker线程所处理的事件
![Reactor线程模型运行机制](images/Middleware/Reactor线程模型运行机制.png)
1、Server端NioEventLoop处理的事件
Reactor线程模型运行机制的四个步骤分别为**连接注册、事件轮询、事件分发、任务处理**。
![Server端NioEventLoop处理的事件](images/Middleware/Server端NioEventLoop处理的事件.png)
- 连接注册Channel建立后注册至Reactor线程中的Selector选择器
- 事件轮询轮询Selector选择器中已注册的所有Channel的I/O事件
- 事件分发为准备就绪的I/O事件分配相应的处理线程
- 任务处理Reactor线程还负责任务队列中的非I/O任务每个Worker线程从各自维护的任务队列中取出任务异步执行
2、Client端NioEventLoop处理的事件
![Client端NioEventLoop处理的事件](images/Middleware/Client端NioEventLoop处理的事件.png)
### 单Reactor单线程
![单Reactor单线程](images/Middleware/单Reactor单线程.png)
### 服务启动
上图描述了 Reactor 的单线程模型结构,在 Reactor 单线程模型中,所有 I/O 操作(包括连接建立、数据读写、事件分发等),都是由一个线程完成的。单线程模型逻辑简单,缺陷也十分明显:
服务启动时,以 example 代码中的 EchoServer 为例,启动的过程以及相应的源码类如下:
- 一个线程支持处理的连接数非常有限CPU 很容易打满,性能方面有明显瓶颈
- 当多个事件被同时触发时,只要有一个事件没有处理完,其他后面的事件就无法执行,这就会造成消息积压及请求超时
- 线程在处理 I/O 事件时Select 无法同时处理连接建立、事件分发等操作
- 如果 I/O 线程一直处于满负荷状态,很可能造成服务端节点不可用
1. `EchoServer#new NioEventLoopGroup(1)->NioEventLoop#provider.openSelector()` : 创建 selector
2. `EchoServer#b.bind(PORT).sync->AbstractBootStrap#doBind()->initAndRegister()-> channelFactory.newChannel() / init(channel)` : 创建 serverSocketChannel 以及初始化
3. `EchoServer#b.bind(PORT).sync->AbstractBootStrap#doBind()->initAndRegister()-> config().group().register(channel)` :从 boss group 中选择一个 NioEventLoop 开始注册 serverSocketChannel
4. `EchoServer#b.bind(PORT).sync->AbstractBootStrap#doBind()->initAndRegister()->config().group().register(channel)->AbstractChannel#register0(promise)->AbstractNioChannel#javaChannel().register(eventLoop().unwrappedSelector(), 0, this)` : 将 server socket channel 注册到选择的 NioEventLoop 的 selector
5. `EchoServer#b.bind(PORT).sync()->AbstractBootStrap#doBind()->doBind0()->AbstractChannel#doBind(localAddress)->NioServerSocketChannel#javaChannel().bind(localAddress, config.getBacklog())` : 绑定地址端口开始启动
6. `EchoServer#b.bind(PORT).sync()->AbstractBootStrap#doBind()->doBind0()->AbstractChannel#pipeline.fireChannelActive()->AbstractNioChannel#selectionKey.interestOps(interestOps|readInterestOp)`: 注册 OP_READ 事件
上述启动流程中1、2、3 由我们自己的线程执行即mainThread4、5、6 是由Boss Thread执行。相应时序图如下
![Netty流程-服务启动](images/Middleware/Netty流程-服务启动.jpg)
### 单Reactor多线程
![单Reactor多线程](images/Middleware/单Reactor多线程.png)
### 建立连接
由于单线程模型有性能方面的瓶颈多线程模型作为解决方案就应运而生了。Reactor 多线程模型将业务逻辑交给多个线程进行处理。除此之外多线程模型其他的操作与单线程模型是类似的例如读取数据依然保留了串行化的设计。当客户端有数据发送至服务端时Select 会监听到可读事件,数据读取完毕后提交到业务线程池中并发处理。
服务启动后便是建立连接的过程了,相应过程及源码类如下:
1. `NioEventLoop#run()->processSelectedKey()` NioEventLoop 中的 selector 轮询创建连接事件OP_ACCEPT
2. `NioEventLoop#run()->processSelectedKey()->AbstractNioMessageChannel#read->NioServerSocketChannel#doReadMessages()->SocketUtil#accept(serverSocketChannel)` 创建 socket channel
3. `NioEventLoop#run()->processSelectedKey()->AbstractNioMessageChannel#fireChannelRead->ServerBootstrap#ServerBootstrapAcceptor#channelRead-> childGroup.register(child)` 从worker group 中选择一个 NioEventLoop 开始注册 socket channel
4. `NioEventLoop#run()->processSelectedKey()->AbstractNioMessageChannel#fireChannelRead->ServerBootstrap#ServerBootstrapAcceptor#channelRead-> childGroup.register(child)->AbstractChannel#register0(promise)-> AbstractNioChannel#javaChannel().register(eventLoop().unwrappedSelector(), 0, this)` 将 socket channel 注册到选择的 NioEventLoop 的 selector
5. `NioEventLoop#run()->processSelectedKey()->AbstractNioMessageChannel#fireChannelRead->ServerBootstrap#ServerBootstrapAcceptor#channelRead-> childGroup.register(child)->AbstractChannel#pipeline.fireChannelActive()-> AbstractNioChannel#selectionKey.interestOps(interestOps | readInterestOp)` 注册 OP_ACCEPT 事件
同样,上述流程中 1、2、3 的执行仍由 Boss Thread 执行,直到 4、5 由具体的 Work Thread 执行。
![Netty流程-建立连接](images/Middleware/Netty流程-建立连接.jpg)
### 主从Reactor多线程
![主从Reactor多线程](images/Middleware/主从Reactor多线程.png)
主从多线程模型由多个 Reactor 线程组成,每个 Reactor 线程都有独立的 Selector 对象。MainReactor 仅负责处理客户端连接的 Accept 事件,连接建立成功后将新创建的连接对象注册至 SubReactor。再由 SubReactor 分配线程池中的 I/O 线程与其连接绑定,它将负责连接生命周期内所有的 I/O 事件。
### 读写与业务处理
连接建立完毕后是具体的读写,以及业务处理逻辑。以 EchoServerHandler 为例,读取数据后会将数据传播出去供业务逻辑处理,此时的 EchoServerHandler 代表我们的业务逻辑,而它的实现也非常简单,就是直接将数据写回去。我们将这块看成一个整条,流程如下:
1. `NioEventLoop#run()->processSelectedKey() NioEventLoop 中的 selector` 轮询创建读取事件OP_READ
2. `NioEventLoop#run()->processSelectedKey()->AbstractNioByteChannel#read()`nioSocketChannel 开始读取数据
3. `NioEventLoop#run()->processSelectedKey()->AbstractNioByteChannel#read()->pipeline.fireChannelRead(byteBuf)`把读取到的数据传播出去供业务处理
4. `AbstractNioByteChannel#pipeline.fireChannelRead->EchoServerHandler#channelRead`在这个例子中即 EchoServerHandler 的执行
5. `EchoServerHandler#write->ChannelOutboundBuffer#addMessage` 调用 write 方法
6. `EchoServerHandler#flush->ChannelOutboundBuffer#addFlush` 调用 flush 准备数据
7. `EchoServerHandler#flush->NioSocketChannel#doWrite` 调用 flush 发送数据
在这个过程中读写数据都是由 Work Thread 执行的,但是业务处理可以由我们自定义的线程池来处理,并且一般我们也是这么做的,默认没有指定线程的情况下仍然由 Work Thread 代为处理。
![Netty流程-读写与业务处理](images/Middleware/Netty流程-读写与业务处理.jpg)
### 关闭连接
服务处理完毕后,单个连接的关闭是什么样的呢?
1. `NioEventLoop#run()->processSelectedKey()` NioEventLoop 中的 selector 轮询创建读取事件OP_READ),这里关闭连接仍然是读取事件
2. `NioEventLoop#run()->processSelectedKey()->AbstractNioByteChannel#read()->closeOnRead(pipeline)`当字节<0 时开始执行关闭 nioSocketChannel
3. `NioEventLoop#run()->processSelectedKey()->AbstractNioByteChannel#read()->closeOnRead(pipeline)->AbstractChannel#close->AbstractNioChannel#doClose()` 关闭 socketChannel
4. `NioEventLoop#run()->processSelectedKey()->AbstractNioByteChannel#read()->closeOnRead(pipeline)->AbstractChannel#close->outboundBuffer.failFlushed/close` 清理消息不接受新信息fail 掉所有 queue 中消息
5. `NioEventLoop#run()->processSelectedKey()->AbstractNioByteChannel#read()->closeOnRead(pipeline)->AbstractChannel#close->fireChannelInactiveAndDeregister->AbstractNioChannel#doDeregister eventLoop().cancel(selectionKey())` 关闭多路复用器的 key
时序图如下:
![Netty流程-关闭连接.jpg](images/Middleware/Netty流程-关闭连接.jpg)
### 关闭服务
最后是关闭整个 Netty 服务:
1. `NioEventLoop#run->closeAll()->selectionKey.cancel/channel.close` 关闭 channel取消 selectionKey
2. `NioEventLoop#run->confirmShutdown->cancelScheduledTasks` 取消定时任务
3. `NioEventLoop#cleanup->selector.close()` 关闭 selector
时序图如下,为了好画将 NioEventLoop 拆成了 2 块:
![Netty流程-关闭服务.jpg](images/Middleware/Netty流程-关闭服务.jpg)
Netty 推荐使用主从多线程模型,这样就可以轻松达到成千上万规模的客户端连接。在海量客户端并发请求的场景下,主从多线程模式甚至可以适当增加 SubReactor 线程的数量,从而利用多核能力提升系统的吞吐量。

@ -1,3 +1,434 @@
### 更多连接
### Netty EventLoop原理
### 更高QPS
EventLoop 这个概念其实并不是 Netty 独有的,它是一种事件等待和处理的程序模型,可以解决多线程资源消耗高的问题。例如 Node.js 就采用了 EventLoop 的运行机制,不仅占用资源低,而且能够支撑了大规模的流量访问。
EventLoop 可以说是 Netty 的调度中心负责监听多种事件类型I/O 事件、信号事件、定时事件等。
#### EventLoop运行模式
![EventLoop通用的运行模式](images/Middleware/EventLoop通用的运行模式.png)
上图展示了 EventLoop 通用的运行模式。每当事件发生时,应用程序都会将产生的事件放入事件队列当中,然后 EventLoop 会轮询从队列中取出事件执行或者将事件分发给相应的事件监听者执行。事件执行的方式通常分为**立即执行、延后执行、定期执行**几种。
#### NioEventLoop原理
在 Netty 中 EventLoop 可以理解为 Reactor 线程模型的事件处理引擎,每个 EventLoop 线程都维护一个 Selector 选择器和任务队列 taskQueue。它主要负责处理 I/O 事件、普通任务和定时任务。Netty 中推荐使用 NioEventLoop 作为实现类,那么 Netty 是如何实现 NioEventLoop 的呢?首先我们来看 NioEventLoop 最核心的 run() 方法源码:
```java
protected void run() {
for (;;) {
try {
try {
switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) {
case SelectStrategy.CONTINUE:
continue;
case SelectStrategy.BUSY_WAIT:
case SelectStrategy.SELECT:
// 轮询 I/O 事件
select(wakenUp.getAndSet(false));
if (wakenUp.get()) {
selector.wakeup();
}
default:
}
} catch (IOException e) {
rebuildSelector0();
handleLoopException(e);
continue;
}
cancelledKeys = 0;
needsToSelectAgain = false;
final int ioRatio = this.ioRatio;
if (ioRatio == 100) {
try {
// 处理 I/O 事件
processSelectedKeys();
} finally {
// 处理所有任务
runAllTasks();
}
} else {
final long ioStartTime = System.nanoTime();
try {
// 处理 I/O 事件
processSelectedKeys();
} finally {
final long ioTime = System.nanoTime() - ioStartTime;
// 处理完 I/O 事件,再处理异步任务队列
runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
}
}
} catch (Throwable t) {
handleLoopException(t);
}
try {
if (isShuttingDown()) {
closeAll();
if (confirmShutdown()) {
return;
}
}
} catch (Throwable t) {
handleLoopException(t);
}
}
}
```
上述源码的结构比较清晰NioEventLoop 每次循环的处理流程都包含事件轮询 select、事件处理 processSelectedKeys、任务处理 runAllTasks 几个步骤,是典型的 Reactor 线程模型的运行机制。而且 Netty 提供了一个参数 ioRatio可以调整 I/O 事件处理和任务处理的时间比例。下面我们将着重从**事件处理**和**任务处理**两个核心部分出发,详细介绍 Netty EventLoop 的实现原理。
#### 事件处理机制
![事件处理机制](images/Middleware/事件处理机制.png)
结合Netty的整体架构看上述EventLoop的事件流转图以便更好地理解 Netty EventLoop 的设计原理。NioEventLoop 的事件处理机制采用的是**无锁串行化的设计思路**
- **BossEventLoopGroup****WorkerEventLoopGroup** 包含一个或者多个 NioEventLoop
BossEventLoopGroup 负责监听客户端的 Accept 事件,当事件触发时,将事件注册至 WorkerEventLoopGroup 中的一个 NioEventLoop 上。每新建一个 Channel 只选择一个 NioEventLoop 与其绑定。所以说 Channel 生命周期的所有事件处理都是**线程独立**的,不同的 NioEventLoop 线程之间不会发生任何交集。
- NioEventLoop 完成数据读取后,会调用绑定的 ChannelPipeline 进行事件传播
ChannelPipeline 也是**线程安全**的,数据会被传递到 ChannelPipeline 的第一个 ChannelHandler 中。数据处理完成后,将加工完成的数据再传递给下一个 ChannelHandler整个过程是**串行化**执行,不会发生线程上下文切换的问题。
NioEventLoop 无锁串行化的设计不仅使系统吞吐量达到最大化,而且降低了用户开发业务逻辑的难度,不需要花太多精力关心线程安全问题。虽然单线程执行避免了线程切换,但是它的缺陷就是不能执行时间过长的 I/O 操作,一旦某个 I/O 事件发生阻塞,那么后续的所有 I/O 事件都无法执行,甚至造成事件积压。在使用 Netty 进行程序开发时,我们一定要对 ChannelHandler 的实现逻辑有充分的风险意识。
NioEventLoop 线程的可靠性至关重要,一旦 NioEventLoop 发生阻塞或者陷入空轮询,就会导致整个系统不可用。在 JDK 中, Epoll 的实现是存在漏洞的,即使 Selector 轮询的事件列表为空NIO 线程一样可以被唤醒,导致 CPU 100% 占用。这就是臭名昭著的 JDK epoll 空轮询的 Bug。Netty 作为一个高性能、高可靠的网络框架,需要保证 I/O 线程的安全性。那么它是如何解决 JDK epoll 空轮询的 Bug 呢?实际上 Netty 并没有从根源上解决该问题,而是巧妙地规避了这个问题。
抛开其它细枝末节直接定位到事件轮询select()方法中的最后一部分代码一起看下Netty是如何解决epoll空轮询的Bug
```java
long time = System.nanoTime();
if (time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos) {
selectCnt = 1;
} else if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 &&
selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {
selector = selectRebuildSelector(selectCnt);
selectCnt = 1;
break;
}
```
Netty提供了一种检测机制判断线程是否可能陷入空轮询具体的实现方式如下
- 每次执行 select 操作之前记录当前时间 currentTimeNanos
- **time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos**,如果事件轮询的持续时间大于等于 timeoutMillis那么说明是正常的否则表明阻塞时间并未达到预期可能触发了空轮询的 Bug
- Netty 引入了计数变量 selectCnt。在正常情况下selectCnt 会重置,否则会对 selectCnt 自增计数。当 selectCnt 达到 SELECTOR_AUTO_REBUILD_THRESHOLD默认512 阈值时,会触发重建 Selector 对象
Netty 采用这种方法巧妙地规避了 JDK Bug。异常的 Selector 中所有的 SelectionKey 会重新注册到新建的 Selector 上,重建完成之后异常的 Selector 就可以废弃了。
#### 任务处理机制
NioEventLoop 不仅负责处理 I/O 事件,还要兼顾执行任务队列中的任务。任务队列遵循 FIFO 规则可以保证任务执行的公平性。NioEventLoop 处理的任务类型基本可以分为三类:
- **普通任务**:通过 NioEventLoop 的 execute() 方法向任务队列 taskQueue 中添加任务。例如 Netty 在写数据时会封装 WriteAndFlushTask 提交给 taskQueue。taskQueue 的实现类是多生产者单消费者队列 MpscChunkedArrayQueue在多线程并发添加任务时可以保证线程安全
- **定时任务**:通过调用 NioEventLoop 的 schedule() 方法向定时任务队列 scheduledTaskQueue 添加一个定时任务,用于周期性执行该任务。例如,心跳消息发送等。定时任务队列 scheduledTaskQueue 采用优先队列 PriorityQueue 实现
- **尾部队列**tailTasks 相比于普通任务队列优先级较低,在每次执行完 taskQueue 中任务后会去获取尾部队列中任务执行。尾部任务并不常用,主要用于做一些收尾工作,例如统计事件循环的执行时间、监控信息上报等
下面结合任务处理 runAllTasks 的源码结构,分析下 NioEventLoop 处理任务的逻辑,源码实现如下:
```java
protected boolean runAllTasks(long timeoutNanos) {
// 1. 合并定时任务到普通任务队列
fetchFromScheduledTaskQueue();
// 2. 从普通任务队列中取出任务
Runnable task = pollTask();
if (task == null) {
afterRunningAllTasks();
return false;
}
// 3. 计算任务处理的超时时间
final long deadline = ScheduledFutureTask.nanoTime() + timeoutNanos;
long runTasks = 0;
long lastExecutionTime;
for (;;) {
// 4. 安全执行任务
safeExecute(task);
runTasks ++;
// 5. 每执行 64 个任务检查一下是否超时
if ((runTasks & 0x3F) == 0) {
lastExecutionTime = ScheduledFutureTask.nanoTime();
if (lastExecutionTime >= deadline) {
break;
}
}
task = pollTask();
if (task == null) {
lastExecutionTime = ScheduledFutureTask.nanoTime();
break;
}
}
// 6. 收尾工作
afterRunningAllTasks();
this.lastExecutionTime = lastExecutionTime;
return true;
}
```
在代码中以注释的方式标注了具体的实现步骤,可以分为 6 个步骤:
- **fetchFromScheduledTaskQueue函数**:将定时任务从 scheduledTaskQueue 中取出,聚合放入普通任务队列 taskQueue 中,只有定时任务的截止时间小于当前时间才可以被合并
- **从普通任务队列taskQueue中取出任务**
- **计算任务执行的最大超时时间**
- **safeExecute函数**:安全执行任务,实际直接调用的 Runnable 的 run() 方法
- **每执行 64 个任务进行超时时间的检查**:如果执行时间大于最大超时时间,则立即停止执行任务,避免影响下一轮的 I/O 事件的处理
- **最后获取尾部队列中的任务执行**
#### EventLoop最佳实践
在日常开发中用好 EventLoop 至关重要,这里结合实际工作中的经验给出一些 EventLoop 的最佳实践方案:
- 网络连接建立过程中三次握手、安全认证的过程会消耗不少时间。这里建议采用 Boss 和 Worker 两个 EventLoopGroup有助于分担 Reactor 线程的压力
- 由于 Reactor 线程模式适合处理耗时短的任务场景,对于耗时较长的 ChannelHandler 可以考虑维护一个业务线程池,将编解码后的数据封装成 Task 进行异步处理,避免 ChannelHandler 阻塞而造成 EventLoop 不可用
- 如果业务逻辑执行时间较短,建议直接在 ChannelHandler 中执行。例如编解码操作,这样可以避免过度设计而造成架构的复杂性
- 不宜设计过多的 ChannelHandler。对于系统性能和可维护性都会存在问题在设计业务架构的时候需要明确业务分层和 Netty 分层之间的界限。不要一味地将业务逻辑都添加到 ChannelHandler 中
### ChannelPipeline
Pipeline 的字面意思是管道、流水线。它在 Netty 中起到的作用,和一个工厂的流水线类似。原始的网络字节流经过 Pipeline被一步步加工包装最后得到加工后的成品。是Netty的核心处理链用以实现网络事件的动态编排和有序传播。
#### ChannelPipeline内部结构
ChannelPipeline 可以看作是 ChannelHandler 的容器载体,它是由一组 ChannelHandler 实例组成的,内部通过双向链表将不同的 ChannelHandler 链接在一起,如下图所示。当有 I/O 读写事件触发时ChannelPipeline 会依次调用 ChannelHandler 列表对 Channel 的数据进行拦截和处理。
![ChannelPipeline内部结构](images/Middleware/ChannelPipeline内部结构.png)
根据网络数据的流向ChannelPipeline 分为入站 ChannelInboundHandler 和出站 ChannelOutboundHandler 两种处理器。在客户端与服务端通信的过程中,数据从客户端发向服务端的过程叫出站,反之称为入站。
![ChannelPipeline入站和出站](images/Middleware/ChannelPipeline入站和出站.png)
#### ChannelHandler接口设计
整个ChannelHandler是围绕I/O事件的生命周期所设计的如建立连接、读数据、写数据、连接销毁等。ChannelHandler 有两个重要的**子接口****ChannelInboundHandler**和**ChannelOutboundHandler**,分别拦截**入站和出站的各种 I/O 事件**。
**① ChannelInboundHandler的事件回调方法与触发时机**
| 事件回调方法 | 触发时机 |
| ------------------------- | -------------------------------------------------- |
| channelRegistered | Channel 被注册到 EventLoop |
| channelUnregistered | Channel 从 EventLoop 中取消注册 |
| channelActive | Channel 处于就绪状态,可以被读写 |
| channelInactive | Channel 处于非就绪状态Channel 可以从远端读取到数据 |
| channelRead | Channel 可以从远端读取到数据 |
| channelReadComplete | Channel 读取数据完成 |
| userEventTriggered | 用户事件触发时 |
| channelWritabilityChanged | Channel 的写状态发生变化 |
**② ChannelOutboundHandler的事件回调方法与触发时机**
ChannelOutboundHandler 的事件回调方法非常清晰,直接通过 ChannelOutboundHandler 的接口列表可以看到每种操作所对应的回调方法,如下图所示。这里每个回调方法都是在相应操作执行之前触发,在此就不多做赘述了。此外 ChannelOutboundHandler 中绝大部分接口都包含ChannelPromise 参数,以便于在操作完成时能够及时获得通知。
![ChannelOutboundHandler](images/Middleware/ChannelOutboundHandler.png)
#### ChannelPipeline事件传播机制
上述ChannelPipeline可分为入站 ChannelInboundHandler 和出站 ChannelOutboundHandler 两种处理器,与此对应传输的事件类型可以分为**Inbound 事件**和**Outbound 事件**。
- **Inbound事件**传播方向为Head->Tail即按照添加的顺序进行正向传播A→B→C
- **Outbound事件**传播方向为Tail->Head即按照添加的顺序进行反向传播C→B→A
代码示例体验 ChannelPipeline 的事件传播机制:
```java
serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
ch.pipeline()
.addLast(new SampleInBoundHandler("SampleInBoundHandlerA", false))
.addLast(new SampleInBoundHandler("SampleInBoundHandlerB", false))
.addLast(new SampleInBoundHandler("SampleInBoundHandlerC", true));
ch.pipeline()
.addLast(new SampleOutBoundHandler("SampleOutBoundHandlerA"))
.addLast(new SampleOutBoundHandler("SampleOutBoundHandlerB"))
.addLast(new SampleOutBoundHandler("SampleOutBoundHandlerC"));
}
}
```
执行结果:
![SampleOutBoundHandler执行结果](images/Middleware/SampleOutBoundHandler执行结果.png)
#### ChannelPipeline异常传播机制
ChannelPipeline 事件传播的实现采用了经典的责任链模式调用链路环环相扣。那么如果有一个节点处理逻辑异常会出现什么现象呢ctx.fireExceptionCaugh 会将异常按顺序从 Head 节点传播到 Tail 节点。如果用户没有对异常进行拦截处理,最后将由 Tail 节点统一处理。
![Netty异常处理的最佳实践](images/Middleware/Netty异常处理的最佳实践.png)
建议用户自定义的异常处理器代码示例如下:
```java
public class ExceptionHandler extends ChannelDuplexHandler {
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
if (cause instanceof RuntimeException) {
System.out.println("Handle Business Exception Success.");
}
}
}
```
加入统一的异常处理器后,可以看到异常已经被优雅地拦截并处理掉了。这也是 Netty 推荐的最佳异常处理实践。
![Netty统一异常处理验证](images/Middleware/Netty统一异常处理验证.png)
### 定时器TimerTask
### 时间轮HashedWheelTimer
时间轮其实就是一种环形的数据结构,可以想象成时钟,分成很多格子,一个格子代表一段时间。并用一个链表保存在该格子上的计划任务,同时一个指针随着时间一格一格转动,并执行相应格子中的所有到期任务。任务通过时间取模决定放入那个格子。
![HashedWheelTimer](images/Middleware/HashedWheelTimer.png)
在网络通信中管理上万的连接每个连接都有超时任务如果为每个任务启动一个Timer超时器那么会占用大量资源。为了解决这个问题可用Netty工具类HashedWheelTimer。
Netty 的时间轮 `HashedWheelTimer` 给出了一个**粗略的定时器实现**,之所以称之为粗略的实现是**因为该时间轮并没有严格的准时执行定时任务**,而是在每隔一个时间间隔之后的时间节点执行,并执行当前时间节点之前到期的定时任务。
当然具体的定时任务的时间执行精度可以通过调节 HashedWheelTimer 构造方法的时间间隔的大小来进行调节,在大多数网络应用的情况下,由于 IO 延迟的存在,并**不会严格要求具体的时间执行精度**,所以默认的 100ms 时间间隔可以满足大多数的情况,不需要再花精力去调节该时间精度。
**HashedWheelTimer的特点**
- 从源码分析可以看出,其实 HashedWheelTimer 的时间精度并不高,误差能够在 100ms 左右,同时如果任务队列中的等待任务数量过多,可能会产生更大的误差
- 但是 HashedWheelTimer 能够处理非常大量的定时任务,且每次定位到要处理任务的候选集合链表只需要 O(1) 的时间,而 Timer 等则需要调整堆,是 O(logN) 的时间复杂度
- HashedWheelTimer 本质上是`模拟了时间的轮盘`,将大量的任务拆分成了一个个的小任务列表,能够有效`节省 CPU 和线程资源`
**源码解读**
```java
public HashedWheelTimer(ThreadFactory threadFactory, long tickDuration, TimeUnit unit,
int ticksPerWheel, boolean leakDetection, long maxPendingTimeouts) {
......
}
```
- `threadFactory`:自定义线程工厂,用于创建线程对象
- `tickDuration`:间隔多久走到下一槽(相当于时钟走一格)
- `unit`定义tickDuration的时间单位
- `ticksPerWheel`:一圈有多个槽
- `leakDetection`:是否开启内存泄漏检测
- `maxPendingTimeouts`最多待执行的任务个数。0或负数表示无限制
**优缺点**
- **优点**
- 可以添加、删除、取消定时任务
- 能高效的处理大批定时任务
- **缺点**
- 对内存要求较高,占用较高的内存
- 时间精度要求不高
**定时任务方案**
目前主流的一些定时任务方案:
- Timer
- ScheduledExecutorService
- ThreadPoolTaskScheduler基于ScheduledExecutorService
- Netty的schedule用到了PriorityQueue
- Netty的HashedWheelTimer时间轮
- Kafka的TimingWheel层级时间轮
使用案例:
```java
// 构造一个 Timer 实例
Timer timer = new HashedWheelTimer();
// 提交一个任务,让它在 5s 后执行
Timeout timeout1 = timer.newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) {
System.out.println("5s 后执行该任务");
}
}, 5, TimeUnit.SECONDS);
// 再提交一个任务,让它在 10s 后执行
Timeout timeout2 = timer.newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) {
System.out.println("10s 后执行该任务");
}
}, 10, TimeUnit.SECONDS);
// 取消掉那个 5s 后执行的任务
if (!timeout1.isExpired()) {
timeout1.cancel();
}
// 原来那个 5s 后执行的任务,已经取消了。这里我们反悔了,我们要让这个任务在 3s 后执行
// 我们说过 timeout 持有上、下层的实例,所以下面的 timer 也可以写成 timeout1.timer()
timer.newTimeout(timeout1.task(), 3, TimeUnit.SECONDS);
```
### 无锁队列mpsc queue
### FastThreadLocal
### ByteBuf
### 编解码协议
netty-codec模块主要负责编解码工作通过编解码实现原始字节数据与业务实体对象之间的相互转化。Netty支持大多数业界主流协议的编解码器如**HTTP、HTTP2、Redis、XML**等,为开发者节省了大量的精力。此外该模块提供了抽象的编解码类**ByteToMessageDecoder**和**MessageToByteEncoder**,通过继承这两个类可以轻松实现自定义的编解码逻辑。
![Netty协议](images/Middleware/Netty协议.png)
### 拆包粘包
- 拆包/粘包的解决方案
- **消息长度固定**:每个数据报文都需要一个固定的长度。当接收方累计读取到固定长度的报文后,就认为已经获得一个完整的消息。当发送方的数据小于固定长度时,则需要空位补齐。消息定长法使用非常简单,但是缺点也非常明显,无法很好设定固定长度的值,如果长度太大会造成字节浪费,长度太小又会影响消息传输,所以在一般情况下消息定长法不会被采用。
- **特定分隔符**
既然接收方无法区分消息的边界,那么我们可以在每次发送报文的尾部加上特定分隔符,接收方就可以根据特殊分隔符进行消息拆分。
- **消息长度 + 消息内容**
消息长度 + 消息内容是项目开发中最常用的一种协议,如上展示了该协议的基本格式。消息头中存放消息的总长度,例如使用 4 字节的 int 值记录消息的长度,消息体实际的二进制的字节数据。接收方在解析数据时,首先读取消息头的长度字段 Len然后紧接着读取长度为 Len 的字节数据,该数据即判定为一个完整的数据报文。

@ -1,42 +1,111 @@
Netty通过Reactor模型基于多路复用器接收并处理用户请求内部实现了两个线程池boss线程池和work线程池其中boss线程池的线程负责处理请求的accept事件当接收到accept事件的请求时把对应的socket封装到一个NioSocketChannel中并交给work线程池其中work线程池负责请求的read和write事件由对应的Handler处理。
### 服务端启动流程
### 单线程Reactor线程模型
Netty 服务端的启动过程大致分为三个步骤:
下图演示了单线程reactor线程模型之所以称之为单线程还是因为只有一个accpet Thread接受任务之后转发到reactor线程中进行处理。两个黄色框表示的是Reactor Thread Group里面有多个Reactor Thread。一个Reactor Thread Group中的Reactor Thread功能都是相同的例如第一个黄色框中的Reactor Thread都是处理拆分后的任务的第一阶段第二个黄色框中的Reactor Thread都是处理拆分后的任务的第二步骤。任务具体要怎么拆分要结合具体场景下图只是演示作用。**一般来说,都是以比较耗时的操作(例如IO)为切分点**。
- 配置线程池
- Channel 初始化
- 端口绑定
![单线程reactor线程模型](images/Middleware/单线程reactor线程模型.png)
特别的如果我们在任务处理的过程中不划分为多个阶段进行处理的话那么单线程reactor线程模型就退化成了并行工作和模型。**事实上可以认为并行工作者模型就是单线程reactor线程模型的最简化版本。**
#### 配置线程池
**单线程模式**
Reactor 单线程模型所有 I/O 操作都由一个线程完成,所以只需要启动一个 EventLoopGroup 即可。
### 多线程Reactor线程模型
```java
EventLoopGroup group = new NioEventLoopGroup(1);
ServerBootstrap b = new ServerBootstrap();
b.group(group);
```
所谓多线程reactor线程模型无非就是有多个accpet线程如下图中的虚线框中的部分。
**多线程模式**
Reactor 单线程模型有非常严重的性能瓶颈,因此 Reactor 多线程模型出现了。在 Netty 中使用 Reactor 多线程模型与单线程模型非常相似,区别是 NioEventLoopGroup 可以不需要任何参数,它默认会启动 2 倍 CPU 核数的线程。当然,你也可以自己手动设置固定的线程数。
![多线程reactor线程模型](images/Middleware/多线程reactor线程模型.png)
```java
EventLoopGroup group = new NioEventLoopGroup();
ServerBootstrap b = new ServerBootstrap();
b.group(group);
```
**主从多线程模式**
在大多数场景下,我们采用的都是主从多线程 Reactor 模型。Boss 是主 ReactorWorker 是从 Reactor。它们分别使用不同的 NioEventLoopGroup主 Reactor 负责处理 Accept然后把 Channel 注册到从 Reactor 上,从 Reactor 主要负责 Channel 生命周期内的所有 I/O 事件。
```java
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup);
```
### 混合型Reactor线程模型
混合型reactor线程模型实际上最能体现reactor线程模型的本质
- 将任务处理切分成多个阶段进行,每个阶段处理完自己的部分之后,转发到下一个阶段进行处理。不同的阶段之间的执行是异步的,可以认为每个阶段都有一个独立的线程池。
- 不同的类型的任务,有着不同的处理流程,划分时需要划分成不同的阶段。如下图蓝色是一种任务、绿色是另一种任务,两种任务有着不同的执行流程
#### Channel 初始化
![混合型reactor线程模型](images/Middleware/混合型reactor线程模型.png)
**设置Channel类型**
```java
// 客户端Channel
b.channel(NioSocketChannel.class);
b.channel(OioSocketChannel.class);
// 服务端Channel
b.channel(NioServerSocketChannel.class);
b.channel(OioServerSocketChannel.class);
b.channel(EpollServerSocketChannel.class);
### Netty-Reactor线程模型
// UDP
b.channel(NioDatagramChannel.class);
b.channel(OioDatagramChannel.class);
```
![Netty-Reactor](images/Middleware/Netty-Reactor.png)
**注册ChannelHandler**
图中大致包含了5个步骤而我们编写的服务端代码中可能并不能完全体现这样的步骤因为Netty将其中一些步骤的细节隐藏了笔者将会通过图形分析与源码分析相结合的方式帮助读者理解这五个步骤。这个五个步骤可以按照以下方式简要概括
ServerBootstrap 的 childHandler() 方法需要注册一个 ChannelHandler。ChannelInitializer是实现了 ChannelHandler接口的匿名类通过实例化 ChannelInitializer 作为 ServerBootstrap 的参数。
- 设置服务端ServerBootStrap启动参数
- 通过ServerBootStrap的bind方法启动服务端bind方法会在parentGroup中注册NioServerScoketChannel监听客户端的连接请求
- Client发起连接CONNECT请求parentGroup中的NioEventLoop不断轮循是否有新的客户端请求如果有ACCEPT事件触发
- ACCEPT事件触发后parentGroup中NioEventLoop会通过NioServerSocketChannel获取到对应的代表客户端的NioSocketChannel并将其注册到childGroup中
- childGroup中的NioEventLoop不断检测自己管理的NioSocketChannel是否有读写事件准备好如果有的话调用对应的ChannelHandler进行处理
```java
b.childHandler(new ChannelInitializer<SocketChannel>() {
    @Override
    public void initChannel(SocketChannel ch) {
        ch.pipeline()
// HTTP 编解码处理器
                .addLast("codec", new HttpServerCodec())
// HTTPContent 压缩处理器
                .addLast("compressor", new HttpContentCompressor())
// HTTP 消息聚合处理器
                .addLast("aggregator", new HttpObjectAggregator(65536)) 
// 自定义业务逻辑处理器
                .addLast("handler", new HttpServerHandler());
    }
});
```
**设置Channel参数**
ServerBootstrap 设置 Channel 属性有option和childOption两个方法option 主要负责设置 Boss 线程组,而 childOption 对应的是 Worker 线程组。
```java
b.option(ChannelOption.SO_KEEPALIVE, true);
```
常用参数如下:
| 参数名 | 描述信息 |
| ---------------------- | ------------------------------------------------------------ |
| SO_KEEPALIVE | 设置为 true 代表启用了 TCP SO_KEEPALIVE 属性TCP 会主动探测连接状态,即连接保活 |
| SO_BACKLOG | 已完成三次握手的请求队列最大长度,同一时刻服务端可能会处理多个连接,在高并发海量连接的场景下,该参数应适当调大 |
| TCP_NODELAY | Netty 默认是 true表示立即发送数据。如果设置为 false 表示启用 Nagle 算法,该算法会将 TCP 网络数据包累积到一定量才会发送虽然可以减少报文发送的数量但是会造成一定的数据延迟。Netty 为了最小化数据传输的延迟,默认禁用了 Nagle 算法 |
| SO_SNDBUF | TCP 数据发送缓冲区大小 |
| SO_RCVBUF | TCP数据接收缓冲区大小TCP数据接收缓冲区大小 |
| SO_LINGER | 设置延迟关闭的时间,等待缓冲区中的数据发送完成 |
| CONNECT_TIMEOUT_MILLIS | 建立连接的超时时间 |
#### 端口绑定
bind() 方法会真正触发启动sync() 方法则会阻塞,直至整个启动过程完成,具体使用方式如下:
```java
ChannelFuture f = b.bind(8080).sync();
```

@ -1,311 +1,5 @@
### 工作流程
从功能上,流程可以分为服务启动、建立连接、读取数据、业务处理、发送数据、关闭连接以及关闭服务。整体流程如下所示(图中没有包含关闭的部分)
ByteBuf维护两个不同的索引`读索引(readerIndex)` 和 `写索引(writerIndex)` 。如下图所示:
![Netty整体流程](images/Middleware/Netty整体流程.png)
![ByteBuf工作流程](images/Middleware/ByteBuf工作流程.png)
- `ByteBuf` 维护了 `readerIndex``writerIndex` 索引
- 当 `readerIndex > writerIndex` 时,则抛出 `IndexOutOfBoundsException`
- `ByteBuf`容量 = `writerIndex`
- `ByteBuf` 可读容量 = `writerIndex` - `readerIndex`
- `readXXX()``writeXXX()` 方法将会推进其对应的索引。自动推进
- `getXXX()``setXXX()` 方法将对 `writerIndex``readerIndex` 无影响
### 使用模式
ByteBuf本质是一个由不同的索引分别控制读访问和写访问的字节数组。ByteBuf共有三种模式`堆缓冲区模式(Heap Buffer)`、`直接缓冲区模式(Direct Buffer)` 和 `复合缓冲区模式(Composite Buffer)`
#### 堆缓冲区模式(Heap Buffer)
堆缓冲区模式又称为`支撑数组(backing array)`。将数据存放在JVM的堆空间通过将数据存储在数组中实现。
- **优点**由于数据存储在Jvm堆中可以快速创建和快速释放并且提供了数组直接快速访问的方法
- **缺点**每次数据与I/O进行传输时都需要将数据拷贝到直接缓冲区
```java
public static void heapBuffer() {
// 创建Java堆缓冲区
ByteBuf heapBuf = Unpooled.buffer();
if (heapBuf.hasArray()) { // 是数组支撑
byte[] array = heapBuf.array();
int offset = heapBuf.arrayOffset() + heapBuf.readerIndex();
int length = heapBuf.readableBytes();
handleArray(array, offset, length);
}
}
```
#### 直接缓冲区模式(Direct Buffer)
Direct Buffer属于堆外分配的直接内存不会占用堆的容量。适用于套接字传输过程避免了数据从内部缓冲区拷贝到直接缓冲区的过程性能较好。对于涉及大量I/O的数据读写建议使用Direct Buffer。而对于用于后端的业务消息编解码模块建议使用Heap Buffer。
- **优点**: 使用Socket传递数据时性能很好避免了数据从Jvm堆内存拷贝到直接缓冲区的过程。提高了性能
- **缺点**: 相对于堆缓冲区而言Direct Buffer分配内存空间和释放更为昂贵
```java
public static void directBuffer() {
ByteBuf directBuf = Unpooled.directBuffer();
if (!directBuf.hasArray()) {
int length = directBuf.readableBytes();
byte[] array = new byte[length];
directBuf.getBytes(directBuf.readerIndex(), array);
handleArray(array, 0, length);
}
}
```
#### 复合缓冲区模式(Composite Buffer)
Composite Buffer是Netty特有的缓冲区。本质上类似于提供一个或多个ByteBuf的组合视图可以根据需要添加和删除不同类型的ByteBuf。
- Composite Buffer是一个组合视图。它提供一种访问方式让使用者自由组合多个ByteBuf避免了拷贝和分配新的缓冲区
- Composite Buffer不支持访问其支撑数组。因此如果要访问需要先将内容拷贝到堆内存中再进行访问
下图是将两个ByteBuf头部 + Body 组合在一起,没有进行任何复制过程。仅仅创建了一个视图:
![CompositeBuffer](images/Middleware/CompositeBuffer.png)
```java
public static void byteBufComposite() {
// 复合缓冲区,只是提供一个视图
CompositeByteBuf messageBuf = Unpooled.compositeBuffer();
ByteBuf headerBuf = Unpooled.buffer(); // can be backing or direct
ByteBuf bodyBuf = Unpooled.directBuffer(); // can be backing or direct
messageBuf.addComponents(headerBuf, bodyBuf);
messageBuf.removeComponent(0); // remove the header
for (ByteBuf buf : messageBuf) {
System.out.println(buf.toString());
}
}
```
### 字节级操作
#### 随机访问索引
ByteBuf的索引与普通的Java字节数组一样。第一个字节的索引是0最后一个字节索引总是capacity()-1。访问方式如下
- readXXX()和writeXXX()方法将会推进其对应的索引readerIndex和writerIndex。自动推进
- getXXX()和setXXX()方法用于访问数据对writerIndex和readerIndex无影响
```java
public static void byteBufRelativeAccess() {
ByteBuf buffer = Unpooled.buffer(); // get reference form somewhere
for (int i = 0; i < buffer.capacity(); i++) {
byte b = buffer.getByte(i); // 不改变readerIndex值
System.out.println((char) b);
}
}
```
#### 顺序访问索引
Netty的ByteBuf同时具有读索引和写索引但JDK的ByteBuffer只有一个索引所以JDK需要调用flip()方法在读模式和写模式之间切换。ByteBuf被读索引和写索引划分成3个区域**可丢弃字节区域**、**可读字节区域** 和 **可写字节区域** 。
![ByteBuf顺序访问索引](images/Middleware/ByteBuf顺序访问索引.png)
#### 可丢弃字节区域
可丢弃字节区域是指:[0readerIndex)之间的区域。可调用discardReadBytes()方法丢弃已经读过的字节。
- discardReadBytes()效果 ----- 将可读字节区域(CONTENT)[readerIndex, writerIndex)往前移动readerIndex位同时修改读索引和写索引
- discardReadBytes()方法会移动可读字节区域内容(CONTENT)。如果频繁调用,会有多次数据复制开销,对性能有一定的影响
#### 可读字节区域
可读字节区域是指:[readerIndex, writerIndex)之间的区域。任何名称以read和skip开头的操作方法都会改变readerIndex索引。
#### 可写字节区域
可写字节区域是指:[writerIndex, capacity)之间的区域。任何名称以write开头的操作方法都将改变writerIndex的值。
#### 索引管理
- markReaderIndex()+resetReaderIndex() ----- markReaderIndex()是先备份当前的readerIndexresetReaderIndex()则是将刚刚备份的readerIndex恢复回来。常用于dump ByteBuf的内容又不想影响原来ByteBuf的readerIndex的值
- readerIndex(int) ----- 设置readerIndex为固定的值
- writerIndex(int) ----- 设置writerIndex为固定的值
- clear() ----- 效果是: readerIndex=0, writerIndex(0)。不会清除内存
- 调用clear()比调用discardReadBytes()轻量的多。仅仅重置readerIndex和writerIndex的值不会拷贝任何内存开销较小
#### 查找操作(indexOf)
查找ByteBuf指定的值。类似于String.indexOf("str")操作
- 最简单的方法 —— indexOf()
- 利用ByteProcessor作为参数来查找某个指定的值
```java
public static void byteProcessor() {
ByteBuf buffer = Unpooled.buffer(); //get reference form somewhere
// 使用indexOf()方法来查找
buffer.indexOf(buffer.readerIndex(), buffer.writerIndex(), (byte)8);
// 使用ByteProcessor查找给定的值
int index = buffer.forEachByte(ByteProcessor.FIND_CR);
}
```
#### 派生缓冲——视图
派生缓冲区为ByteBuf提供了一个访问的视图。视图仅仅提供一种访问操作不做任何拷贝操作。下列方法都会呈现给使用者一个视图以供访问:
- duplicate()
- slice()
- slice(int, int)
- Unpooled.unmodifiableBuffer(...)
- Unpooled.wrappedBuffer(...)
- order(ByteOrder)
- readSlice(int)
**理解**
- 上面的6中方法都会返回一个新的ByteBuf实例具有自己的读索引和写索引。但是其内部存储是与原对象是共享的。这就是视图的概念
- 请注意如果你修改了这个新的ByteBuf实例的具体内容那么对应的源实例也会被修改因为其内部存储是共享的
- 如果需要拷贝现有缓冲区的真实副本请使用copy()或copy(int, int)方法
- 使用派生缓冲区,避免了复制内存的开销,有效提高程序的性能
```java
public static void byteBufSlice() {
Charset utf8 = Charset.forName("UTF-8");
ByteBuf buf = Unpooled.copiedBuffer("Netty in Action rocks!", utf8);
ByteBuf sliced = buf.slice(0, 15);
System.out.println(sliced.toString(utf8));
buf.setByte(0, (byte)'J');
assert buf.getByte(0) == sliced.getByte(0); // return true
}
public static void byteBufCopy() {
Charset utf8 = Charset.forName("UTF-8");
ByteBuf buf = Unpooled.copiedBuffer("Netty in Action rocks!", utf8);
ByteBuf copy = buf.copy(0, 15);
System.out.println(copy.toString(utf8));
buf.setByte(0, (byte)'J');
assert buf.getByte(0) != copy.getByte(0); // return true
}
```
#### 读/写操作
如上文所提到的,有两种类别的读/写操作:
- get()和set()操作 ----- 从给定的索引开始,并且保持索引不变
- read()和write()操作 ----- 从给定的索引开始,并且根据已经访问过的字节数对索引进行访问
- 下图给出get()操作API对于set()操作、read()操作和write操作可参考书籍或API
![ByteBuf-get](images/Middleware/ByteBuf-get.png)
#### 更多操作
![ByteBuf-更多操作](images/Middleware/ByteBuf-更多操作.png)
下面的两个方法操作字面意思较难理解,给出解释:
- **hasArray()**如果ByteBuf由一个字节数组支撑则返回true。通俗的讲ByteBuf是堆缓冲区模式则代表其内部存储是由字节数组支撑的
- **array()**如果ByteBuf是由一个字节数组支撑泽返回数组否则抛出UnsupportedOperationException异常。也就是ByteBuf是堆缓冲区模式
### ByteBuf分配
创建和管理ByteBuf实例的多种方式**按序分配(ByteBufAllocator)**、**Unpooled缓冲区** 和 **ByteBufUtil类**
#### 按序分配ByteBufAllocator接口
Netty通过接口ByteBufAllocator实现了(ByteBuf的)池化。Netty提供池化和非池化的ButeBufAllocator:
- `ctx.channel().alloc().buffer()`本质就是ByteBufAllocator.DEFAULT
- `ByteBufAllocator.DEFAULT.buffer()`返回一个基于堆或者直接内存存储的Bytebuf。默认是堆内存
- `ByteBufAllocator.DEFAULT`:有两种类型: UnpooledByteBufAllocator.DEFAULT(非池化)和PooledByteBufAllocator.DEFAULT(池化)。对于Java程序默认使用PooledByteBufAllocator(池化)。对于安卓默认使用UnpooledByteBufAllocator(非池化)
- 可以通过BootStrap中的Config为每个Channel提供独立的ByteBufAllocator实例
![img](images/Middleware/ByteBufAllocator.png)
解释:
- 上图中的buffer()方法返回一个基于堆或者直接内存存储的Bytebuf ----- 缺省是堆内存。源码: AbstractByteBufAllocator() { this(false); }
- ByteBufAllocator.DEFAULT ----- 可能是池化,也可能是非池化。默认是池化(PooledByteBufAllocator.DEFAULT)
#### Unpooled缓冲区——非池化
Unpooled提供静态的辅助方法来创建未池化的ByteBuf。
![Unpooled缓冲区](images/Middleware/Unpooled缓冲区.png)
注意:
- 上图的buffer()方法返回一个未池化的基于堆内存存储的ByteBuf
- wrappedBuffer() ----- 创建一个视图返回一个包装了给定数据的ByteBuf。非常实用
创建ByteBuf代码:
```java
public void createByteBuf(ChannelHandlerContext ctx) {
// 1. 通过Channel创建ByteBuf
ByteBuf buf1 = ctx.channel().alloc().buffer();
// 2. 通过ByteBufAllocator.DEFAULT创建
ByteBuf buf2 = ByteBufAllocator.DEFAULT.buffer();
// 3. 通过Unpooled创建
ByteBuf buf3 = Unpooled.buffer();
}
```
#### ByteBufUtil类
ByteBufUtil类提供了用于操作ByteBuf的静态的辅助方法: hexdump()和equals
- hexdump()以十六进制的表示形式打印ByteBuf的内容。非常有价值
- equals()判断两个ByteBuf实例的相等性
### 引用计数
Netty4.0版本中为ButeBuf和ButeBufHolder引入了引用计数技术。请区别引用计数和可达性分析算法(jvm垃圾回收)
- 谁负责释放:一般来说,是由最后访问(引用计数)对象的那一方来负责将它释放
- buffer.release()引用计数减1
- buffer.retain()引用计数加1
- buffer.refCnt():返回当前对象引用计数值
- buffer.touch():记录当前对象的访问位置,主要用于调试
- 引用计数并非仅对于直接缓冲区(direct Buffer)。ByteBuf的三种模式: 堆缓冲区(heap Buffer)、直接缓冲区(dirrect Buffer)和复合缓冲区(Composite Buffer)都使用了引用计数,某些时候需要程序员手动维护引用数值
```java
public static void releaseReferenceCountedObject(){
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
// 引用计数加1
buffer.retain();
// 输出引用计数
buffer.refCnt();
// 引用计数减1
buffer.release();
}
```
![Netty线程模型](images/Middleware/Netty线程模型.png)

@ -1,89 +1,40 @@
**Netty** 的`Zero-copy` 体现在如下几个个方面:
Netty通过Reactor模型基于多路复用器接收并处理用户请求内部实现了两个线程池boss线程池和work线程池其中boss线程池的线程负责处理请求的accept事件当接收到accept事件的请求时把对应的socket封装到一个NioSocketChannel中并交给work线程池其中work线程池负责请求的read和write事件由对应的Handler处理。
- **通过CompositeByteBuf实现零拷贝**Netty提供了`CompositeByteBuf` 类可以将多个ByteBuf合并为一个逻辑上的ByteBuf避免了各个ByteBuf之间的拷贝
- **通过wrap操作实现零拷贝**:通过`wrap`操作可以将byte[]、ByteBuf、ByteBuffer等包装成一个Netty ByteBuf对象进而避免了拷贝操作
- 通过slice操作实现零拷贝ByteBuf支持`slice`操作可以将ByteBuf分解为多个共享同一个存储区域的ByteBuf避免了内存的拷贝
- **通过FileRegion实现零拷贝**:通过 `FileRegion` 包装的`FileChannel.tranferTo` 实现文件传输,可以直接将文件缓冲区的数据发送到目标 Channel避免了传统通过循环write方式导致的内存拷贝问题
### 单线程Reactor线程模型
### 零拷贝操作
下图演示了单线程reactor线程模型之所以称之为单线程还是因为只有一个accpet Thread接受任务之后转发到reactor线程中进行处理。两个黄色框表示的是Reactor Thread Group里面有多个Reactor Thread。一个Reactor Thread Group中的Reactor Thread功能都是相同的例如第一个黄色框中的Reactor Thread都是处理拆分后的任务的第一阶段第二个黄色框中的Reactor Thread都是处理拆分后的任务的第二步骤。任务具体要怎么拆分要结合具体场景下图只是演示作用。**一般来说,都是以比较耗时的操作(例如IO)为切分点**。
#### 通过CompositeByteBuf实现零拷贝
![单线程reactor线程模型](images/Middleware/单线程reactor线程模型.png)
![CompositeByteBuf实现零拷贝](images/Middleware/CompositeByteBuf实现零拷贝.png)
特别的如果我们在任务处理的过程中不划分为多个阶段进行处理的话那么单线程reactor线程模型就退化成了并行工作和模型。**事实上可以认为并行工作者模型就是单线程reactor线程模型的最简化版本。**
```java
ByteBuf header = null;
ByteBuf body = null;
// 传统合并header和body两次额外的数据拷贝
ByteBuf allBuf = Unpooled.buffer(header.readableBytes() + body.readableBytes());
allBuf.writeBytes(header);
allBuf.writeBytes(body);
// 合并header和body内部这两个 ByteBuf 都是单独存在的, CompositeByteBuf 只是逻辑上是一个整体
CompositeByteBuf compositeByteBuf = Unpooled.compositeBuffer();
compositeByteBuf.addComponents(true, header, body);
// 底层封装了 CompositeByteBuf 操作
ByteBuf allByteBuf = Unpooled.wrappedBuffer(header, body);
```
### 多线程Reactor线程模型
所谓多线程reactor线程模型无非就是有多个accpet线程如下图中的虚线框中的部分。
![多线程reactor线程模型](images/Middleware/多线程reactor线程模型.png)
#### 通过wrap操作实现零拷贝
```java
byte[] bytes = null;
// 传统方式直接将byte[]数组拷贝到ByteBuf中
ByteBuf byteBuf = Unpooled.buffer();
byteBuf.writeBytes(bytes);
### 混合型Reactor线程模型
// wrap方式将bytes包装成为一个UnpooledHeapByteBuf对象, 包装过程中, 是不会有拷贝操作的
// 即最后我们生成的生成的ByteBuf对象是和bytes数组共用了同一个存储空间, 对bytes的修改也会反映到ByteBuf对象中
ByteBuf byteBuf = Unpooled.wrappedBuffer(bytes);
混合型reactor线程模型实际上最能体现reactor线程模型的本质
```
- 将任务处理切分成多个阶段进行,每个阶段处理完自己的部分之后,转发到下一个阶段进行处理。不同的阶段之间的执行是异步的,可以认为每个阶段都有一个独立的线程池。
- 不同的类型的任务,有着不同的处理流程,划分时需要划分成不同的阶段。如下图蓝色是一种任务、绿色是另一种任务,两种任务有着不同的执行流程
![混合型reactor线程模型](images/Middleware/混合型reactor线程模型.png)
### Netty-Reactor线程模型
#### 通过slice操作实现零拷贝
![Netty-Reactor](images/Middleware/Netty-Reactor.png)
slice 操作和 wrap 操作刚好相反,`Unpooled.wrappedBuffer` 可以将多个 ByteBuf 合并为一个, 而 slice 操作可以将一个 **ByteBuf **`切片` 为多个共享一个存储区域的 ByteBuf 对象. ByteBuf 提供了两个 slice 操作方法:
图中大致包含了5个步骤而我们编写的服务端代码中可能并不能完全体现这样的步骤因为Netty将其中一些步骤的细节隐藏了笔者将会通过图形分析与源码分析相结合的方式帮助读者理解这五个步骤。这个五个步骤可以按照以下方式简要概括
```java
public ByteBuf slice();
public ByteBuf slice(int index, int length);
```
不带参数的`slice`方法等同于`buf.slice(buf.readerIndex(), buf.readableBytes())` 调用, 即返回 buf 中可读部分的切片. 而 `slice(int index, int length)`方法相对就比较灵活了, 我们可以设置不同的参数来获取到 buf 的不同区域的切片。
`slice` 方法产生 header 和 body 的过程是没有拷贝操作的, header 和 body 对象在内部其实是共享了 byteBuf 存储空间的不同部分而已,即:
![slice操作实现零拷贝](images/Middleware/slice操作实现零拷贝.png)
#### 通过FileRegion实现零拷贝
Netty 中使用 FileRegion 实现文件传输的零拷贝, 不过在底层 FileRegion 是依赖于 **Java NIO** `FileChannel.transfer` 的零拷贝功能。当有了 FileRegion 后, 我们就可以直接通过它将文件的内容直接写入 **Channel** 中, 而不需要像传统的做法: 拷贝文件内容到临时 **buffer**, 然后再将 **buffer** 写入 **Channel.** 通过这样的零拷贝操作, 无疑对传输大文件很有帮助。
### 传统IO的流程
![传统IO的流程Copy](images/Middleware/传统IO的流程Copy.png)
![传统IO的流程](images/Middleware/传统IO的流程.png)
- **「第一步」**:将文件通过 **「DMA」** 技术从磁盘中拷贝到内核缓冲区
- **「第二步」**:将文件从内核缓冲区拷贝到用户进程缓冲区域中
- **「第三步」**:将文件从用户进程缓冲区中拷贝到 socket 缓冲区中
- **「第四步」**将socket缓冲区中的文件通过 **「DMA」** 技术拷贝到网卡
### 零拷贝整体流程图
![零拷贝CPU](images/Middleware/零拷贝CPU.png)
![零拷贝整体流程图](images/Middleware/零拷贝整体流程图.png)
- 设置服务端ServerBootStrap启动参数
- 通过ServerBootStrap的bind方法启动服务端bind方法会在parentGroup中注册NioServerScoketChannel监听客户端的连接请求
- Client发起连接CONNECT请求parentGroup中的NioEventLoop不断轮循是否有新的客户端请求如果有ACCEPT事件触发
- ACCEPT事件触发后parentGroup中NioEventLoop会通过NioServerSocketChannel获取到对应的代表客户端的NioSocketChannel并将其注册到childGroup中
- childGroup中的NioEventLoop不断检测自己管理的NioSocketChannel是否有读写事件准备好如果有的话调用对应的ChannelHandler进行处理

@ -1,111 +1,89 @@
### 粘包拆包图解
**Netty** 的`Zero-copy` 体现在如下几个个方面:
![CP粘包拆包图解](images/Middleware/CP粘包拆包图解.png)
- **通过CompositeByteBuf实现零拷贝**Netty提供了`CompositeByteBuf` 类可以将多个ByteBuf合并为一个逻辑上的ByteBuf避免了各个ByteBuf之间的拷贝
- **通过wrap操作实现零拷贝**:通过`wrap`操作可以将byte[]、ByteBuf、ByteBuffer等包装成一个Netty ByteBuf对象进而避免了拷贝操作
- 通过slice操作实现零拷贝ByteBuf支持`slice`操作可以将ByteBuf分解为多个共享同一个存储区域的ByteBuf避免了内存的拷贝
- **通过FileRegion实现零拷贝**:通过 `FileRegion` 包装的`FileChannel.tranferTo` 实现文件传输,可以直接将文件缓冲区的数据发送到目标 Channel避免了传统通过循环write方式导致的内存拷贝问题
假设客户端分别发送了两个数据包D1和D2给服务端由于服务端一次读取到字节数是不确定的故可能存在以下几种情况
### 零拷贝操作
- 服务端分两次读取到两个独立的数据包分别是D1和D2没有粘包和拆包
- 服务端一次接收到了两个数据包D1和D2粘在一起发生粘包
- 服务端分两次读取到数据包第一次读取到了完整D1包和D2包的部分内容第二次读取到了D2包的剩余内容发生拆包
- 服务端分两次读取到数据包第一次读取到部分D1包第二次读取到剩余的D1包和全部的D2包
- 当TCP缓存再小一点的话会把D1和D2分别拆成多个包发送
#### 通过CompositeByteBuf实现零拷贝
![CompositeByteBuf实现零拷贝](images/Middleware/CompositeByteBuf实现零拷贝.png)
```java
ByteBuf header = null;
ByteBuf body = null;
// 传统合并header和body两次额外的数据拷贝
ByteBuf allBuf = Unpooled.buffer(header.readableBytes() + body.readableBytes());
allBuf.writeBytes(header);
allBuf.writeBytes(body);
// 合并header和body内部这两个 ByteBuf 都是单独存在的, CompositeByteBuf 只是逻辑上是一个整体
CompositeByteBuf compositeByteBuf = Unpooled.compositeBuffer();
compositeByteBuf.addComponents(true, header, body);
// 底层封装了 CompositeByteBuf 操作
ByteBuf allByteBuf = Unpooled.wrappedBuffer(header, body);
```
### 产生原因
产生粘包和拆包问题的主要原因是操作系统在发送TCP数据的时候底层会有一个缓冲区例如1024个字节大小
- 如果一次请求发送的数据量比较小没达到缓冲区大小TCP则会将多个请求合并为同一个请求进行发送这就形成了粘包问题
- 如果一次请求发送的数据量比较大超过了缓冲区大小TCP就会将其拆分为多次发送这就是拆包也就是将一个大的包拆分为多个小包进行发送
#### 通过wrap操作实现零拷贝
```java
byte[] bytes = null;
// 传统方式直接将byte[]数组拷贝到ByteBuf中
ByteBuf byteBuf = Unpooled.buffer();
byteBuf.writeBytes(bytes);
### 解决方案
// wrap方式将bytes包装成为一个UnpooledHeapByteBuf对象, 包装过程中, 是不会有拷贝操作的
// 即最后我们生成的生成的ByteBuf对象是和bytes数组共用了同一个存储空间, 对bytes的修改也会反映到ByteBuf对象中
ByteBuf byteBuf = Unpooled.wrappedBuffer(bytes);
#### 固定长度
```
对于使用固定长度的粘包和拆包场景,可以使用:
- `FixedLengthFrameDecoder`:每次读取固定长度的消息,如果当前读取到的消息不足指定长度,那么就会等待下一个消息到达后进行补足。其使用也比较简单,只需要在构造函数中指定每个消息的长度即可。
```java
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// 这里将FixedLengthFrameDecoder添加到pipeline中指定长度为20
ch.pipeline().addLast(new FixedLengthFrameDecoder(20));
// 将前一步解码得到的数据转码为字符串
ch.pipeline().addLast(new StringDecoder());
// 这里FixedLengthFrameEncoder是我们自定义的用于将长度不足20的消息进行补全空格
ch.pipeline().addLast(new FixedLengthFrameEncoder(20));
// 最终的数据处理
ch.pipeline().addLast(new EchoServerHandler());
}
```
#### 通过slice操作实现零拷贝
slice 操作和 wrap 操作刚好相反,`Unpooled.wrappedBuffer` 可以将多个 ByteBuf 合并为一个, 而 slice 操作可以将一个 **ByteBuf **`切片` 为多个共享一个存储区域的 ByteBuf 对象. ByteBuf 提供了两个 slice 操作方法:
```java
public ByteBuf slice();
public ByteBuf slice(int index, int length);
```
#### 指定分隔符
不带参数的`slice`方法等同于`buf.slice(buf.readerIndex(), buf.readableBytes())` 调用, 即返回 buf 中可读部分的切片. 而 `slice(int index, int length)`方法相对就比较灵活了, 我们可以设置不同的参数来获取到 buf 的不同区域的切片。
对于通过分隔符进行粘包和拆包问题的处理Netty提供了两个编解码的类
`slice` 方法产生 header 和 body 的过程是没有拷贝操作的, header 和 body 对象在内部其实是共享了 byteBuf 存储空间的不同部分而已,即:
- `LineBasedFrameDecoder`:通过换行符,即`\n`或者`\r\n`对数据进行处理
- `DelimiterBasedFrameDecoder`:通过用户指定的分隔符对数据进行粘包和拆包处理
![slice操作实现零拷贝](images/Middleware/slice操作实现零拷贝.png)
```java
@Override
protected void initChannel(SocketChannel ch) throws Exception {
String delimiter = "_$";
// 将delimiter设置到DelimiterBasedFrameDecoder中经过该解码一器进行处理之后源数据将会
// 被按照_$进行分隔这里1024指的是分隔的最大长度即当读取到1024个字节的数据之后若还是未
// 读取到分隔符,则舍弃当前数据段,因为其很有可能是由于码流紊乱造成的
ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024,
Unpooled.wrappedBuffer(delimiter.getBytes())));
// 将分隔之后的字节数据转换为字符串数据
ch.pipeline().addLast(new StringDecoder());
// 这是我们自定义的一个编码器,主要作用是在返回的响应数据最后添加分隔符
ch.pipeline().addLast(new DelimiterBasedFrameEncoder(delimiter));
// 最终处理数据并且返回响应的handler
ch.pipeline().addLast(new EchoServerHandler());
}
```
#### 通过FileRegion实现零拷贝
#### 数据包长度字段
Netty 中使用 FileRegion 实现文件传输的零拷贝, 不过在底层 FileRegion 是依赖于 **Java NIO** `FileChannel.transfer` 的零拷贝功能。当有了 FileRegion 后, 我们就可以直接通过它将文件的内容直接写入 **Channel** 中, 而不需要像传统的做法: 拷贝文件内容到临时 **buffer**, 然后再将 **buffer** 写入 **Channel.** 通过这样的零拷贝操作, 无疑对传输大文件很有帮助。
处理粘拆包的主要思想是在生成的数据包中添加一个长度字段,用于记录当前数据包的长度。
- `LengthFieldBasedFrameDecoder`:按照参数指定的包长度偏移量数据对接收到的数据进行解码,从而得到目标消息体数据。解码过程如下图所示:
![LengthFieldBasedFrameDecoder](images/Middleware/LengthFieldBasedFrameDecoder.png)
### 传统IO的流程
- `LengthFieldPrepender`:在响应的数据前面添加指定的字节数据,这个字节数据中保存了当前消息体的整体字节数据长度。编码过程如下图所示:
![传统IO的流程Copy](images/Middleware/传统IO的流程Copy.png)
![LengthFieldPrepender](images/Middleware/LengthFieldPrepender.png)
![传统IO的流程](images/Middleware/传统IO的流程.png)
```java
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// 这里将LengthFieldBasedFrameDecoder添加到pipeline的首位因为其需要对接收到的数据
// 进行长度字段解码,这里也会对数据进行粘包和拆包处理
ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024, 0, 2, 0, 2));
// LengthFieldPrepender是一个编码器主要是在响应字节数据前面添加字节长度字段
ch.pipeline().addLast(new LengthFieldPrepender(2));
// 对经过粘包和拆包处理之后的数据进行json反序列化从而得到User对象
ch.pipeline().addLast(new JsonDecoder());
// 对响应数据进行编码主要是将User对象序列化为json
ch.pipeline().addLast(new JsonEncoder());
// 处理客户端的请求的数据,并且进行响应
ch.pipeline().addLast(new EchoServerHandler());
}
```
- **「第一步」**:将文件通过 **「DMA」** 技术从磁盘中拷贝到内核缓冲区
- **「第二步」**:将文件从内核缓冲区拷贝到用户进程缓冲区域中
- **「第三步」**:将文件从用户进程缓冲区中拷贝到 socket 缓冲区中
- **「第四步」**将socket缓冲区中的文件通过 **「DMA」** 技术拷贝到网卡
#### 自定义粘包拆包器
### 零拷贝整体流程图
可以通过实现`MessageToByteEncoder`和`ByteToMessageDecoder`来实现自定义粘包和拆包处理的目的。
![零拷贝CPU](images/Middleware/零拷贝CPU.png)
- `MessageToByteEncoder`作用是将响应数据编码为一个ByteBuf对象
- `ByteToMessageDecoder`将接收到的ByteBuf数据转换为某个对象数据
![零拷贝整体流程图](images/Middleware/零拷贝整体流程图.png)

@ -1,5 +1,111 @@
- **IO线程模型**:同步非阻塞,用最少的资源做更多的事
- **内存零拷贝**:尽量减少不必要的内存拷贝,实现了更高效率的传输
- **内存池设计**:申请的内存可以重用,主要指直接内存。内部实现是用一颗二叉查找树管理内存分配情况
- **串形化处理读写**:避免使用锁带来的性能开销
- **高性能序列化协议**:支持 protobuf 等高性能序列化协议
### 粘包拆包图解
![CP粘包拆包图解](images/Middleware/CP粘包拆包图解.png)
假设客户端分别发送了两个数据包D1和D2给服务端由于服务端一次读取到字节数是不确定的故可能存在以下几种情况
- 服务端分两次读取到两个独立的数据包分别是D1和D2没有粘包和拆包
- 服务端一次接收到了两个数据包D1和D2粘在一起发生粘包
- 服务端分两次读取到数据包第一次读取到了完整D1包和D2包的部分内容第二次读取到了D2包的剩余内容发生拆包
- 服务端分两次读取到数据包第一次读取到部分D1包第二次读取到剩余的D1包和全部的D2包
- 当TCP缓存再小一点的话会把D1和D2分别拆成多个包发送
### 产生原因
产生粘包和拆包问题的主要原因是操作系统在发送TCP数据的时候底层会有一个缓冲区例如1024个字节大小
- 如果一次请求发送的数据量比较小没达到缓冲区大小TCP则会将多个请求合并为同一个请求进行发送这就形成了粘包问题
- 如果一次请求发送的数据量比较大超过了缓冲区大小TCP就会将其拆分为多次发送这就是拆包也就是将一个大的包拆分为多个小包进行发送
### 解决方案
#### 固定长度
对于使用固定长度的粘包和拆包场景,可以使用:
- `FixedLengthFrameDecoder`:每次读取固定长度的消息,如果当前读取到的消息不足指定长度,那么就会等待下一个消息到达后进行补足。其使用也比较简单,只需要在构造函数中指定每个消息的长度即可。
```java
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// 这里将FixedLengthFrameDecoder添加到pipeline中指定长度为20
ch.pipeline().addLast(new FixedLengthFrameDecoder(20));
// 将前一步解码得到的数据转码为字符串
ch.pipeline().addLast(new StringDecoder());
// 这里FixedLengthFrameEncoder是我们自定义的用于将长度不足20的消息进行补全空格
ch.pipeline().addLast(new FixedLengthFrameEncoder(20));
// 最终的数据处理
ch.pipeline().addLast(new EchoServerHandler());
}
```
#### 指定分隔符
对于通过分隔符进行粘包和拆包问题的处理Netty提供了两个编解码的类
- `LineBasedFrameDecoder`:通过换行符,即`\n`或者`\r\n`对数据进行处理
- `DelimiterBasedFrameDecoder`:通过用户指定的分隔符对数据进行粘包和拆包处理
```java
@Override
protected void initChannel(SocketChannel ch) throws Exception {
String delimiter = "_$";
// 将delimiter设置到DelimiterBasedFrameDecoder中经过该解码一器进行处理之后源数据将会
// 被按照_$进行分隔这里1024指的是分隔的最大长度即当读取到1024个字节的数据之后若还是未
// 读取到分隔符,则舍弃当前数据段,因为其很有可能是由于码流紊乱造成的
ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024,
Unpooled.wrappedBuffer(delimiter.getBytes())));
// 将分隔之后的字节数据转换为字符串数据
ch.pipeline().addLast(new StringDecoder());
// 这是我们自定义的一个编码器,主要作用是在返回的响应数据最后添加分隔符
ch.pipeline().addLast(new DelimiterBasedFrameEncoder(delimiter));
// 最终处理数据并且返回响应的handler
ch.pipeline().addLast(new EchoServerHandler());
}
```
#### 数据包长度字段
处理粘拆包的主要思想是在生成的数据包中添加一个长度字段,用于记录当前数据包的长度。
- `LengthFieldBasedFrameDecoder`:按照参数指定的包长度偏移量数据对接收到的数据进行解码,从而得到目标消息体数据。解码过程如下图所示:
![LengthFieldBasedFrameDecoder](images/Middleware/LengthFieldBasedFrameDecoder.png)
- `LengthFieldPrepender`:在响应的数据前面添加指定的字节数据,这个字节数据中保存了当前消息体的整体字节数据长度。编码过程如下图所示:
![LengthFieldPrepender](images/Middleware/LengthFieldPrepender.png)
```java
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// 这里将LengthFieldBasedFrameDecoder添加到pipeline的首位因为其需要对接收到的数据
// 进行长度字段解码,这里也会对数据进行粘包和拆包处理
ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024, 0, 2, 0, 2));
// LengthFieldPrepender是一个编码器主要是在响应字节数据前面添加字节长度字段
ch.pipeline().addLast(new LengthFieldPrepender(2));
// 对经过粘包和拆包处理之后的数据进行json反序列化从而得到User对象
ch.pipeline().addLast(new JsonDecoder());
// 对响应数据进行编码主要是将User对象序列化为json
ch.pipeline().addLast(new JsonEncoder());
// 处理客户端的请求的数据,并且进行响应
ch.pipeline().addLast(new EchoServerHandler());
}
```
#### 自定义粘包拆包器
可以通过实现`MessageToByteEncoder`和`ByteToMessageDecoder`来实现自定义粘包和拆包处理的目的。
- `MessageToByteEncoder`作用是将响应数据编码为一个ByteBuf对象
- `ByteToMessageDecoder`将接收到的ByteBuf数据转换为某个对象数据

@ -1,63 +1,5 @@
### 文件描述符
- 设置系统最大文件句柄数
```bash
# 查看
cat /proc/sys/fs/file-max
# 修改
在/etc/sysctl.conf插入fs.file-max=1000000
# 配置生效
sysctl -p
```
- 设置单进程打开的最大句柄数
默认单进程打开的最大句柄数是 `1024`,通过 `ulimit -a` 可以查看相关参数,示例如下:
```powershell
[root@test ~]# ulimit -a
core file size (blocks, -c) 0
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited
pending signals (-i) 256324
max locked memory (kbytes, -l) 64
max memory size (kbytes, -m) unlimited
open files (-n) 1024
......
```
当并发接入的TCP连接数超过上限时就会报“too many open files”所有新的客户端接入将失败。通过 `vi /etc/security/limits.conf` 命令添加如下配置参数:
```powershell
* soft nofile 1000000
* hard nofile 1000000
```
修改之后保存,注销当前用户,重新登录,通过 `ulimit -a` 查看修改的状态是否生效。
**注意**:尽管我们可以将单个进程打开的最大句柄数修改的非常大,但是当句柄数达到一定数量级之后,处理效率将出现明显下降,因此,需要根据服务器的硬件配置和处理能力进行合理设置。
### TCP/IP相关参数
需要重点调优的TCP IP参数如下
- net.ipv4.tcp_rmem为每个TCP连接分配的读缓冲区内存大小。第一个值时socket接收缓冲区分配的最小字节数。
### 多网卡队列和软中断
- **TCP缓冲区**
根据推送消息的大小,合理设置以下两个参数,对于海量长连接,通常 32K 是个不错的选择:
- **SO_SNDBUF**:发送缓冲区大小
- **SO_RCVBUF**:接收缓冲区大小
- **软中断**
使用命令 `cat /proc/interrupts` 查看网卡硬件中断的运行情况如果全部被集中在CPU0上处理则无法并行执行多个软中断。Linux kernel内核≥2.6.35的版本可以开启RPS网络通信能力提升20%以上RPS原理是根据数据包的源地址、目的地址和源端口等计算出一个Hash值然后根据Hash值来选择软中断运行的CPU即实现每个链接和CPU绑定通过Hash值来均衡软中断运行在多个CPU上。
- **IO线程模型**:同步非阻塞,用最少的资源做更多的事
- **内存零拷贝**:尽量减少不必要的内存拷贝,实现了更高效率的传输
- **内存池设计**:申请的内存可以重用,主要指直接内存。内部实现是用一颗二叉查找树管理内存分配情况
- **串形化处理读写**:避免使用锁带来的性能开销
- **高性能序列化协议**:支持 protobuf 等高性能序列化协议

@ -1,159 +1,63 @@
### 设置合理的线程数
### 文件描述符
**boss线程池优化**
- 设置系统最大文件句柄数
对于Netty服务端通常只需要启动一个监听端口用于端侧设备接入但是如果集群实例较少甚至是单机部署那么在短时间内大量设备接入时需要对服务端的监听方式和线程模型做优化即服务端监听多个端口利用主从Reactor线程模型。由于同时监听了多个端口每个ServerSocketChannel都对应一个独立的Acceptor线程这样就能并行处理加速端侧设备的接人速度减少端侧设备的连接超时失败率提高单节点服务端的处理性能。
**work线程池优化(I/O工作线程池)**
对于I/O工作线程池的优化可以先采用系统默认值cpu内核数*2进行性能测试在性能测试过程中采集I/O线程的CPU占用大小看是否存在瓶颈具体策略如下
- 通过执行 `ps -ef|grep java` 找到服务端进程pid
- 执行`top -Hp pid`查询该进程下所有线程的运行情况通过“shift+p”对CPU占用大小做排序获取线程的pid及对应的CPU占用大小
- 使用`printf'%x\n' pid`将pid转换成16进制格式
- 通过`jstack -f pid`命令获取线程堆栈或者通过jvisualvm工具打印线程堆栈找到I/O work工作线程查看他们的CPU占用大小及线程堆栈关键词`SelectorImpl.lockAndDoSelect`
**分析**
- 如果连续采集几次进行对比发现线程堆栈都停留在SelectorImpl.lockAndDoSelect处则说明I/O线程比较空闲无需对工作线程数做调整
- 如果发现I/O线程的热点停留在读或写操作或停留在ChannelHandler的执行处则可以通过适当调大NioEventLoop线程的个数来提升网络的读写性能。调整方式有两种
- 接口API指定在创建NioEventLoopGroup实例时指定线程数
- 系统参数指定:通过-Dio.netty.eventLoopThreads来指定NioEventLoopGroup线程池不建议
### 心跳检测优化
心跳检测的目的就是确认当前链路是否可用,对方是否活着并且能够正常接收和发送消息。从技术层面看,要解决链路的可靠性问题,必须周期性地对链路进行有效性检测。目前最流行和通用的做法就是心跳检测。
**海量设备接入的服务端心跳优化策略**
- **要能够及时检测失效的连接,并将其剔除**。防止无效的连接句柄积压导致OOM等问题
- **设置合理的心跳周期**。防止心跳定时任务积压造成频繁的老年代GC新生代和老年代都有导致STW的GC不过耗时差异较大导致应用暂停
- **使用Nety提供的链路空闲检测机制**。不要自己创建定时任务线程池,加重系统的负担,以及增加潜在的并发安全问题
**心跳检测机制分为三个层面**
- **TCP层的心跳检测**即TCP的 Keep-Alive机制它的作用域是整个TCP协议栈
- **协议层的心跳检测**主要存在于长连接协议中例如MQTT
- **应用层的心跳检测**:它主要由各业务产品通过约定方式定时给对方发送心跳消息实现
**心跳检测机制分类**
- **Ping-Pong型心跳**由通信一方定时发送Ping消息对方接收到Ping消息后立即返回Pong答应消息给对方属于“请求-响应型”心跳
- **Ping-Ping型心跳**不区分心跳请求和答应由通信双发按照约定时间向对方发送心跳Ping消息属于”双向心跳“
**心跳检测机制策略**
- **心跳超时**连续N次检测都没有收到对方的Pong应答消息或Ping请求消息则认为链路已经发生逻辑失效
- **心跳失败**在读取和发送心跳消息的时候如果直接发生了IO异常说明链路已经失效
**链路空闲检测机制**
- **读空闲**链路持续时间T没有读取到任何消息
- **写空闲**链路持续时间T没有发送任何消息
- **读写空闲**链路持续时间T没有接收或者发送任何消息
**案例分析**
由于移动无线网络的特点推送服务的心跳周期并不能设置的太长否则长连接会被释放造成频繁的客户端重连但是也不能设置太短否则在当前缺乏统一心跳框架的机制下很容易导致信令风暴例如微信心跳信令风暴问题。具体的心跳周期并没有统一的标准180S 也许是个不错的选择,微信为 300S。
在 Netty 中,可以通过在 ChannelPipeline 中增加 IdleStateHandler 的方式实现心跳检测,在构造函数中指定链路空闲时间,然后实现空闲回调接口,实现心跳的发送和检测。拦截链路空闲事件并处理心跳:
```java
public class MyHandler extends ChannelHandlerAdapter {
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent) {
// 心跳处理
}
}
}
```bash
# 查看
cat /proc/sys/fs/file-max
# 修改
在/etc/sysctl.conf插入fs.file-max=1000000
# 配置生效
sysctl -p
```
**心跳优化结论**
- 对于百万级的服务器,一般不建议很长的心跳周期和超时时长
- 心跳检测周期通常不要超过60s心跳检测超时通常为心跳检测周期的2倍
- 建议通过IdleStateHandler实现心跳不要自己创建定时任务线程池加重系统负担和增加潜在并发安全问题
- 发生心跳超时或心跳失败时,都需要关闭链路,由客户端发起重连操作,保证链路能够恢复正常
- 链路空闲事件被触发后并没有关闭链路而是触发IdleStateEvent事件用户订阅IdleStateEvent事件用于自定义逻辑处理。如关闭链路、客户端发起重新连接、告警和日志打印等
- 链路空闲检测类库主要包括IdleStateHandler、ReadTimeoutHandler、WriteTimeoutHandler
### 接收和发送缓冲区调优
对于长链接每个链路都需要维护自己的消息接收和发送缓冲区JDK 原生的 NIO 类库使用的是java.nio.ByteBuffer, 它实际是一个长度固定的byte[],无法动态扩容。
**场景**假设单条消息最大上限为10K平均大小为5K为满足10K消息处理ByteBuffer的容量被设置为10K这样每条链路实际上多消耗了5K内存如果长链接链路数为100万每个链路都独立持有ByteBuffer接收缓冲区则额外损耗的总内存Total(M) =1000000×5K=4882M
Netty提供的ByteBuf支持容量动态调整同时提供了两种接收缓冲区的内存分配器
- **FixedRecvByteBufAllocator**固定长度的接收缓冲区分配器它分配的ByteBuf长度是固定大小的并不会根据实际数据报大小动态收缩。但如果容量不足支持动态扩展
- **AdaptiveRecvByteBufAllocator**容量动态调整的接收缓冲区分配器会根据之前Channel接收到的数据报大小进行计算如果连续填充满接收缓冲区的可写空间则动态扩展容量。如果连续2次接收到的数据报都小于指定值则收缩当前的容量以节约内存
相对于FixedRecvByteBufAllocator使用AdaptiveRecvByteBufAllocator更为合理可在创建客户端或者服务端的时候指定RecvByteBufAllocator。
```java
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.option(ChannelOption.RCVBUF_ALLOCATOR, AdaptiveRecvByteBufAllocator.DEFAULT)
- 设置单进程打开的最大句柄数
默认单进程打开的最大句柄数是 `1024`,通过 `ulimit -a` 可以查看相关参数,示例如下:
```powershell
[root@test ~]# ulimit -a
core file size (blocks, -c) 0
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited
pending signals (-i) 256324
max locked memory (kbytes, -l) 64
max memory size (kbytes, -m) unlimited
open files (-n) 1024
......
```
**注意**:无论是接收缓冲区还是发送缓冲区,缓冲区的大小建议设置为消息的平均大小,不要设置成最大消息的上限,这会导致额外的内存浪费。
当并发接入的TCP连接数超过上限时就会报“too many open files”所有新的客户端接入将失败。通过 `vi /etc/security/limits.conf` 命令添加如下配置参数:
### 合理使用内存池
每个NioEventLoop线程处理N个链路。**链路处理流程**开始处理A链路→**创建接收缓冲区(创建ByteBuf)**→消息解码→封装成POJO对象→提交至后台线程成Task→**释放接收缓冲区**→开始处理B链路。
如果使用内存池则当A链路接收到新数据报后从NioEventLoop的内存池中申请空闲的ByteBuf解码完成后调用release将ByteBuf释放到内存池中供后续B链路继续使用。使用内存池优化后单个NioEventLoop的ByteBuf申请和GC次数从原来的N=1000000/64= 15625次减少为最少0次假设每次申请都有可用的内存
Netty默认不使用内存池需要在创建客户端或者服务端的时候进行指定
```java
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
```powershell
* soft nofile 1000000
* hard nofile 1000000
```
使用内存池之后,内存的申请和释放必须成对出现,即 retain() 和 release() 要成对出现,否则会导致内存泄露。值得注意的是,如果使用内存池,完成 ByteBuf 的解码工作之后必须显式的调用 ReferenceCountUtil.release(msg) 对接收缓冲区 ByteBuf 进行内存释放,否则它会被认为仍然在使用中,这样会导致内存泄露。
修改之后保存,注销当前用户,重新登录,通过 `ulimit -a` 查看修改的状态是否生效。
**注意**:尽管我们可以将单个进程打开的最大句柄数修改的非常大,但是当句柄数达到一定数量级之后,处理效率将出现明显下降,因此,需要根据服务器的硬件配置和处理能力进行合理设置。
### 防止I/O线程被意外阻塞
通常情况不能在Netty的I/O线程上做执行时间不可控的操作如访问数据库、调用第三方服务等但有一些隐形的阻塞操作却容易被忽略如打印日志。
生产环境中一般需要实时打印接口日志其它日志处于ERROR级别当服务发生I/O异常后会记录异常日志。如果磁盘的WIO比较高可能会发生写日志文件操作被同步阻塞阻塞时间无法预测就会导致Netty的NioEventLoop线程被阻塞Socket链路无法被及时管理其它链路也无法进行读写操作等。
### TCP/IP相关参数
常用的log4j虽然支持异步写日志(AsyncAppender),但当日志队列满之后,它会同步阻塞业务线程,直到日志队列有空闲位置可用。
需要重点调优的TCP IP参数如下
类似问题具有极强的隐蔽性往往WIO高的时间持续非常短或是偶现的在测试环境中很难模拟此类故障问题定位难度大
- net.ipv4.tcp_rmem为每个TCP连接分配的读缓冲区内存大小。第一个值时socket接收缓冲区分配的最小字节数。
### I/O线程与业务线程分离
### 多网卡队列和软中断
- 如果服务端不做复杂业务逻辑操作仅是简单内存操作和消息转发则可通过调大NioEventLoop工作线程池的方式直接在I/O线程中执行业务ChannelHandler这样便减少了一次线上上下文切换性能反而更高
- 如果有复杂的业务逻辑操作则建议I/O线程和业务线程分离。
- 对于I/O线程由于互相之间不存在锁竞争可以创建一个大的NioEventLoopGroup线程组所有Channel都共享一个线程池
- 对于后端的业务线程池则建议创建多个小的业务线程池线程池可以与I/O线程绑定这样既减少了锁竞争又提升了后端的处理性能
- **TCP缓冲区**
根据推送消息的大小,合理设置以下两个参数,对于海量长连接,通常 32K 是个不错的选择:
- **SO_SNDBUF**:发送缓冲区大小
- **SO_RCVBUF**:接收缓冲区大小
### 服务端并发连接数流控
- **软中断**
无论服务端的性能优化到多少都需要考虑流控功能。当资源成为瓶颈或遇到端侧设备的大量接入需要通过流控对系统做保护。一般Netty主要考虑并发连接数的控制
使用命令 `cat /proc/interrupts` 查看网卡硬件中断的运行情况如果全部被集中在CPU0上处理则无法并行执行多个软中断。Linux kernel内核≥2.6.35的版本可以开启RPS网络通信能力提升20%以上RPS原理是根据数据包的源地址、目的地址和源端口等计算出一个Hash值然后根据Hash值来选择软中断运行的CPU即实现每个链接和CPU绑定通过Hash值来均衡软中断运行在多个CPU上。

@ -1,28 +1,159 @@
### 疑似内存泄漏
- 设置合理的线程数
**环境**8C16G的Linux
**boss线程池优化**
**描述**boss为1worker为6其余分配给业务使用保持10W用户长链接2W用户并发做消息请求
对于Netty服务端通常只需要启动一个监听端口用于端侧设备接入但是如果集群实例较少甚至是单机部署那么在短时间内大量设备接入时需要对服务端的监听方式和线程模型做优化即服务端监听多个端口利用主从Reactor线程模型。由于同时监听了多个端口每个ServerSocketChannel都对应一个独立的Acceptor线程这样就能并行处理加速端侧设备的接人速度减少端侧设备的连接超时失败率提高单节点服务端的处理性能。
**分析**dump内存堆栈发现Netty的ScheduledFutureTask增加了9076%达到110W个实例。通过业务代码分析发现用户使用了IdleStateHandler用于在链路空闲时进行业务逻辑处理但空闲时间比较大为15分钟。Netty 的 IdleStateHandler 会根据用户的使用场景启动三类定时任务分别是ReaderIdleTimeoutTask、WriterIdleTimeoutTask 和 AllIdleTimeoutTask它们都会被加入到 NioEventLoop 的 Task 队列中被调度和执行。由于超时时间过长10W 个长链接链路会创建 10W 个 ScheduledFutureTask 对象,每个对象还保存有业务的成员变量,非常消耗内存。用户的持久代设置的比较大,一些定时任务被老化到持久代中,没有被 JVM 垃圾回收掉,内存一直在增长,用户误认为存在内存泄露,即小问题被放大而引出的问题。
**解决**重新设计和反复压测之后将超时时间设置为45秒内存可以实现正常回收。
**work线程池优化(I/O工作线程池)**
对于I/O工作线程池的优化可以先采用系统默认值cpu内核数*2进行性能测试在性能测试过程中采集I/O线程的CPU占用大小看是否存在瓶颈具体策略如下
### 当心CLOSE_WAIT
- 通过执行 `ps -ef|grep java` 找到服务端进程pid
- 执行`top -Hp pid`查询该进程下所有线程的运行情况通过“shift+p”对CPU占用大小做排序获取线程的pid及对应的CPU占用大小
- 使用`printf'%x\n' pid`将pid转换成16进制格式
- 通过`jstack -f pid`命令获取线程堆栈或者通过jvisualvm工具打印线程堆栈找到I/O work工作线程查看他们的CPU占用大小及线程堆栈关键词`SelectorImpl.lockAndDoSelect`
由于网络不稳定经常会导致客户端断连,如果服务端没有能够及时关闭 socket就会导致处于 close_wait 状态的链路过多。close_wait 状态的链路并不释放句柄和内存等资源如果积压过多可能会导致系统句柄耗尽发生“Too many open files”异常新的客户端无法接入涉及创建或者打开句柄的操作都将失败。
**分析**
close_wait 是被动关闭连接是形成的,根据 TCP 状态机,服务器端收到客户端发送的 FINTCP 协议栈会自动发送 ACK链接进入 close_wait 状态。但如果服务器端不执行 socket 的 close() 操作,状态就不能由 close_wait 迁移到 last_ack则系统中会存在很多 close_wait 状态的连接。通常来说,一个 close_wait 会维持至少 2 个小时的时间(系统默认超时时间的是 7200 秒,也就是 2 小时)。如果服务端程序因某个原因导致系统造成一堆 close_wait 消耗资源,那么通常是等不到释放那一刻,系统就已崩溃。
- 如果连续采集几次进行对比发现线程堆栈都停留在SelectorImpl.lockAndDoSelect处则说明I/O线程比较空闲无需对工作线程数做调整
- 如果发现I/O线程的热点停留在读或写操作或停留在ChannelHandler的执行处则可以通过适当调大NioEventLoop线程的个数来提升网络的读写性能。调整方式有两种
- 接口API指定在创建NioEventLoopGroup实例时指定线程数
- 系统参数指定:通过-Dio.netty.eventLoopThreads来指定NioEventLoopGroup线程池不建议
导致 close_wait 过多的可能原因如下:
- **程序处理Bug**:导致接收到对方的 fin 之后没有及时关闭 socket这可能是 Netty 的 Bug也可能是业务层 Bug需要具体问题具体分析
- **关闭socket不及时**:例如 I/O 线程被意外阻塞,或者 I/O 线程执行的用户自定义 Task 比例过高,导致 I/O 操作处理不及时,链路不能被及时释放
**解决方案**
### 心跳检测优化
- **不要在 Netty 的 I/O 线程worker线程上处理业务心跳发送和检测除外**
- **在I/O线程上执行自定义Task要当心**
- **IdleStateHandler、ReadTimeoutHandler和WriteTimeoutHandler使用要当**
心跳检测的目的就是确认当前链路是否可用,对方是否活着并且能够正常接收和发送消息。从技术层面看,要解决链路的可靠性问题,必须周期性地对链路进行有效性检测。目前最流行和通用的做法就是心跳检测。
**海量设备接入的服务端心跳优化策略**
- **要能够及时检测失效的连接,并将其剔除**。防止无效的连接句柄积压导致OOM等问题
- **设置合理的心跳周期**。防止心跳定时任务积压造成频繁的老年代GC新生代和老年代都有导致STW的GC不过耗时差异较大导致应用暂停
- **使用Nety提供的链路空闲检测机制**。不要自己创建定时任务线程池,加重系统的负担,以及增加潜在的并发安全问题
**心跳检测机制分为三个层面**
- **TCP层的心跳检测**即TCP的 Keep-Alive机制它的作用域是整个TCP协议栈
- **协议层的心跳检测**主要存在于长连接协议中例如MQTT
- **应用层的心跳检测**:它主要由各业务产品通过约定方式定时给对方发送心跳消息实现
**心跳检测机制分类**
- **Ping-Pong型心跳**由通信一方定时发送Ping消息对方接收到Ping消息后立即返回Pong答应消息给对方属于“请求-响应型”心跳
- **Ping-Ping型心跳**不区分心跳请求和答应由通信双发按照约定时间向对方发送心跳Ping消息属于”双向心跳“
**心跳检测机制策略**
- **心跳超时**连续N次检测都没有收到对方的Pong应答消息或Ping请求消息则认为链路已经发生逻辑失效
- **心跳失败**在读取和发送心跳消息的时候如果直接发生了IO异常说明链路已经失效
**链路空闲检测机制**
- **读空闲**链路持续时间T没有读取到任何消息
- **写空闲**链路持续时间T没有发送任何消息
- **读写空闲**链路持续时间T没有接收或者发送任何消息
**案例分析**
由于移动无线网络的特点推送服务的心跳周期并不能设置的太长否则长连接会被释放造成频繁的客户端重连但是也不能设置太短否则在当前缺乏统一心跳框架的机制下很容易导致信令风暴例如微信心跳信令风暴问题。具体的心跳周期并没有统一的标准180S 也许是个不错的选择,微信为 300S。
在 Netty 中,可以通过在 ChannelPipeline 中增加 IdleStateHandler 的方式实现心跳检测,在构造函数中指定链路空闲时间,然后实现空闲回调接口,实现心跳的发送和检测。拦截链路空闲事件并处理心跳:
```java
public class MyHandler extends ChannelHandlerAdapter {
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent) {
// 心跳处理
}
}
}
```
**心跳优化结论**
- 对于百万级的服务器,一般不建议很长的心跳周期和超时时长
- 心跳检测周期通常不要超过60s心跳检测超时通常为心跳检测周期的2倍
- 建议通过IdleStateHandler实现心跳不要自己创建定时任务线程池加重系统负担和增加潜在并发安全问题
- 发生心跳超时或心跳失败时,都需要关闭链路,由客户端发起重连操作,保证链路能够恢复正常
- 链路空闲事件被触发后并没有关闭链路而是触发IdleStateEvent事件用户订阅IdleStateEvent事件用于自定义逻辑处理。如关闭链路、客户端发起重新连接、告警和日志打印等
- 链路空闲检测类库主要包括IdleStateHandler、ReadTimeoutHandler、WriteTimeoutHandler
### 接收和发送缓冲区调优
对于长链接每个链路都需要维护自己的消息接收和发送缓冲区JDK 原生的 NIO 类库使用的是java.nio.ByteBuffer, 它实际是一个长度固定的byte[],无法动态扩容。
**场景**假设单条消息最大上限为10K平均大小为5K为满足10K消息处理ByteBuffer的容量被设置为10K这样每条链路实际上多消耗了5K内存如果长链接链路数为100万每个链路都独立持有ByteBuffer接收缓冲区则额外损耗的总内存Total(M) =1000000×5K=4882M
Netty提供的ByteBuf支持容量动态调整同时提供了两种接收缓冲区的内存分配器
- **FixedRecvByteBufAllocator**固定长度的接收缓冲区分配器它分配的ByteBuf长度是固定大小的并不会根据实际数据报大小动态收缩。但如果容量不足支持动态扩展
- **AdaptiveRecvByteBufAllocator**容量动态调整的接收缓冲区分配器会根据之前Channel接收到的数据报大小进行计算如果连续填充满接收缓冲区的可写空间则动态扩展容量。如果连续2次接收到的数据报都小于指定值则收缩当前的容量以节约内存
相对于FixedRecvByteBufAllocator使用AdaptiveRecvByteBufAllocator更为合理可在创建客户端或者服务端的时候指定RecvByteBufAllocator。
```java
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.option(ChannelOption.RCVBUF_ALLOCATOR, AdaptiveRecvByteBufAllocator.DEFAULT)
```
**注意**:无论是接收缓冲区还是发送缓冲区,缓冲区的大小建议设置为消息的平均大小,不要设置成最大消息的上限,这会导致额外的内存浪费。
### 合理使用内存池
每个NioEventLoop线程处理N个链路。**链路处理流程**开始处理A链路→**创建接收缓冲区(创建ByteBuf)**→消息解码→封装成POJO对象→提交至后台线程成Task→**释放接收缓冲区**→开始处理B链路。
如果使用内存池则当A链路接收到新数据报后从NioEventLoop的内存池中申请空闲的ByteBuf解码完成后调用release将ByteBuf释放到内存池中供后续B链路继续使用。使用内存池优化后单个NioEventLoop的ByteBuf申请和GC次数从原来的N=1000000/64= 15625次减少为最少0次假设每次申请都有可用的内存
Netty默认不使用内存池需要在创建客户端或者服务端的时候进行指定
```java
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
```
使用内存池之后,内存的申请和释放必须成对出现,即 retain() 和 release() 要成对出现,否则会导致内存泄露。值得注意的是,如果使用内存池,完成 ByteBuf 的解码工作之后必须显式的调用 ReferenceCountUtil.release(msg) 对接收缓冲区 ByteBuf 进行内存释放,否则它会被认为仍然在使用中,这样会导致内存泄露。
### 防止I/O线程被意外阻塞
通常情况不能在Netty的I/O线程上做执行时间不可控的操作如访问数据库、调用第三方服务等但有一些隐形的阻塞操作却容易被忽略如打印日志。
生产环境中一般需要实时打印接口日志其它日志处于ERROR级别当服务发生I/O异常后会记录异常日志。如果磁盘的WIO比较高可能会发生写日志文件操作被同步阻塞阻塞时间无法预测就会导致Netty的NioEventLoop线程被阻塞Socket链路无法被及时管理其它链路也无法进行读写操作等。
常用的log4j虽然支持异步写日志(AsyncAppender),但当日志队列满之后,它会同步阻塞业务线程,直到日志队列有空闲位置可用。
类似问题具有极强的隐蔽性往往WIO高的时间持续非常短或是偶现的在测试环境中很难模拟此类故障问题定位难度大。
### I/O线程与业务线程分离
- 如果服务端不做复杂业务逻辑操作仅是简单内存操作和消息转发则可通过调大NioEventLoop工作线程池的方式直接在I/O线程中执行业务ChannelHandler这样便减少了一次线上上下文切换性能反而更高
- 如果有复杂的业务逻辑操作则建议I/O线程和业务线程分离。
- 对于I/O线程由于互相之间不存在锁竞争可以创建一个大的NioEventLoopGroup线程组所有Channel都共享一个线程池
- 对于后端的业务线程池则建议创建多个小的业务线程池线程池可以与I/O线程绑定这样既减少了锁竞争又提升了后端的处理性能
### 服务端并发连接数流控
无论服务端的性能优化到多少都需要考虑流控功能。当资源成为瓶颈或遇到端侧设备的大量接入需要通过流控对系统做保护。一般Netty主要考虑并发连接数的控制。

@ -0,0 +1,28 @@
### 疑似内存泄漏
**环境**8C16G的Linux
**描述**boss为1worker为6其余分配给业务使用保持10W用户长链接2W用户并发做消息请求
**分析**dump内存堆栈发现Netty的ScheduledFutureTask增加了9076%达到110W个实例。通过业务代码分析发现用户使用了IdleStateHandler用于在链路空闲时进行业务逻辑处理但空闲时间比较大为15分钟。Netty 的 IdleStateHandler 会根据用户的使用场景启动三类定时任务分别是ReaderIdleTimeoutTask、WriterIdleTimeoutTask 和 AllIdleTimeoutTask它们都会被加入到 NioEventLoop 的 Task 队列中被调度和执行。由于超时时间过长10W 个长链接链路会创建 10W 个 ScheduledFutureTask 对象,每个对象还保存有业务的成员变量,非常消耗内存。用户的持久代设置的比较大,一些定时任务被老化到持久代中,没有被 JVM 垃圾回收掉,内存一直在增长,用户误认为存在内存泄露,即小问题被放大而引出的问题。
**解决**重新设计和反复压测之后将超时时间设置为45秒内存可以实现正常回收。
### 当心CLOSE_WAIT
由于网络不稳定经常会导致客户端断连,如果服务端没有能够及时关闭 socket就会导致处于 close_wait 状态的链路过多。close_wait 状态的链路并不释放句柄和内存等资源如果积压过多可能会导致系统句柄耗尽发生“Too many open files”异常新的客户端无法接入涉及创建或者打开句柄的操作都将失败。
close_wait 是被动关闭连接是形成的,根据 TCP 状态机,服务器端收到客户端发送的 FINTCP 协议栈会自动发送 ACK链接进入 close_wait 状态。但如果服务器端不执行 socket 的 close() 操作,状态就不能由 close_wait 迁移到 last_ack则系统中会存在很多 close_wait 状态的连接。通常来说,一个 close_wait 会维持至少 2 个小时的时间(系统默认超时时间的是 7200 秒,也就是 2 小时)。如果服务端程序因某个原因导致系统造成一堆 close_wait 消耗资源,那么通常是等不到释放那一刻,系统就已崩溃。
导致 close_wait 过多的可能原因如下:
- **程序处理Bug**:导致接收到对方的 fin 之后没有及时关闭 socket这可能是 Netty 的 Bug也可能是业务层 Bug需要具体问题具体分析
- **关闭socket不及时**:例如 I/O 线程被意外阻塞,或者 I/O 线程执行的用户自定义 Task 比例过高,导致 I/O 操作处理不及时,链路不能被及时释放
**解决方案**
- **不要在 Netty 的 I/O 线程worker线程上处理业务心跳发送和检测除外**
- **在I/O线程上执行自定义Task要当心**
- **IdleStateHandler、ReadTimeoutHandler和WriteTimeoutHandler使用要当**

@ -40,18 +40,19 @@
* [✍ ZK选举过程](src/Middleware/414 "ZK选举过程")
* [✍ Zookeeper安装](src/Middleware/415 "Zookeeper安装")
* 🏁 Netty
* [✍ Netty流程](src/Middleware/501 "Netty流程")
* [✍ 处理事件](src/Middleware/502 "处理事件")
* [✍ 长连接优化](src/Middleware/503 "长连接优化")
* [✍ 线程模型](src/Middleware/504 "线程模型")
* [✍ HashedWheelTimer](src/Middleware/505 "HashedWheelTimer")
* [✍ ByteBuf](src/Middleware/506 "ByteBuf")
* [✍ Zero-Copy](src/Middleware/507 "Zero-Copy")
* [✍ TCP粘包拆包](src/Middleware/508 "TCP粘包拆包")
* [✍ 高性能](src/Middleware/509 "高性能")
* [✍ 操作系统调优](src/Middleware/510 "操作系统调优")
* [✍ Netty性能调优](src/Middleware/511 "Netty性能调优")
* [✍ 案例分析](src/Middleware/512 "案例分析")
* [✍ Netty逻辑架构](src/Middleware/501 "Netty逻辑架构")
* [✍ Reactor线程模型](src/Middleware/502 "Reactor线程模型")
* [✍ 核心设计](src/Middleware/503 "核心设计")
* [✍ 核心流程](src/Middleware/504 "核心流程")
* [✍ Netty流程](src/Middleware/505 "Netty流程")
* [✍ 线程模型](src/Middleware/506 "线程模型")
* [✍ ByteBuf](src/Middleware/507 "ByteBuf")
* [✍ Zero-Copy](src/Middleware/508 "Zero-Copy")
* [✍ TCP粘包拆包](src/Middleware/509 "TCP粘包拆包")
* [✍ 高性能](src/Middleware/510 "高性能")
* [✍ 操作系统调优](src/Middleware/511 "操作系统调优")
* [✍ Netty性能调优](src/Middleware/512 "Netty性能调优")
* [✍ 案例分析](src/Middleware/513 "案例分析")
* 🏁 RabbitMQ
* [✍ 模式介绍](src/Middleware/601 "模式介绍")
* [🏁 Dubbo](src/Middleware/701 "Dubbo")

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 386 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 954 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 438 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 320 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 357 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

@ -1,36 +1,14 @@
信号驱动式I/O是指进程预先告知内核使得某个文件描述符上发生了变化时内核使用信号通知该进程。在信号驱动式I/O模型进程使用socket进行信号驱动I/O并建立一个SIGIO信号处理函数当进程通过该信号处理函数向内核发起I/O调用时内核并没有准备好数据报而是返回一个信号给进程此时进程可以继续发起其他I/O调用。也就是说在第一阶段内核准备数据的过程中进程并不会被阻塞会继续执行。当数据报准备好之后内核会递交SIGIO信号通知用户空间的信号处理程序数据已准备好此时进程会发起recvfrom的系统调用这一个阶段与阻塞式I/O无异。也就是说在第二阶段内核复制数据到用户空间的过程中进程同样是被阻塞的
AIO(异步非阻塞IO,即NIO.2)。异步IO模型中用户线程直接使用内核提供的异步IO API发起read请求且发起后立即返回继续执行用户线程代码。不过此时用户线程已经将调用的AsynchronousOperation和CompletionHandler注册到内核然后操作系统开启独立的内核线程去处理IO操作。当read请求的数据到达时由内核负责读取socket中的数据并写入用户指定的缓冲区中。最后内核将read的数据和用户线程注册的CompletionHandler分发给内部ProactorProactor将IO完成的信息通知给用户线程一般通过调用用户线程注册的完成事件处理函数完成异步IO
**信号驱动式I/O的整个过程图如下**
![异步非阻塞IO](images/OS/异步非阻塞IO.png)
![信号驱动式IO](images/OS/信号驱动式IO.png)
**第一阶段(非阻塞):**
- ①进程使用socket进行信号驱动I/O建立SIGIO信号处理函数向内核发起系统调用内核在未准备好数据报的情况下返回一个信号给进程此时进程可以继续做其他事情
- ②内核将磁盘中的数据加载至内核缓冲区完成后会递交SIGIO信号给用户空间的信号处理程序
**第二阶段(阻塞):**
- ③进程在收到SIGIO信号程序之后进程向内核发起系统调用recvfrom
- ④内核再将内核缓冲区中的数据复制到用户空间中的进程缓冲区中真正执行IO过程的阶段直到数据复制完成
- ⑤内核返回成功数据处理完成的指令给进程进程在收到指令后再对数据包进程处理处理完成后此时的进程解除不可中断睡眠态执行下一个I/O操作
**特点:**借助socket进行信号驱动I/O并建立SIGIO信号处理函数
**特点:**第一阶段和第二阶段都是有内核完成。
**优点**
- 线程并没有在第一阶段(数据等待)时被阻塞,提高了资源利用率;
- 能充分利用DMA的特性将I/O操作与计算重叠提高性能、资源利用率与并发能力
**缺点**
- 在程序的实现上比较困难
- 信号 I/O 在大量 IO 操作时可能会因为信号队列溢出导致没法通知。信号驱动 I/O 尽管对于处理 UDP 套接字来说有用,即这种信号通知意味着到达一个数据报,或者返回一个异步错误。但是,对于 TCP 而言,信号驱动的 I/O 方式近乎无用,因为导致这种通知的条件为数众多,每一个来进行判别会消耗很大资源,与前几种方式相比优势尽失
**信号通知机制**
- **水平触发:**指数据报到内核缓冲区准备好之后内核通知进程后进程因繁忙未发起recvfrom系统调用内核会再次发送通知信号循环往复直到进程来请求recvfrom系统调用。很明显这种方式会频繁消耗过多的系统资源
- **边缘触发:**内核只会发送一次通知信号
- 要实现真正的异步 I/O操作系统需要做大量的工作。目前 Windows 下通过 IOCP 实现了真正的异步 I/O。而在 Linux 系统下Linux 2.6才引入,目前 AIO 并不完善,因此在 Linux 下实现高并发网络编程时都是以 复用式I/O模型为主

@ -0,0 +1,36 @@
信号驱动式I/O是指进程预先告知内核使得某个文件描述符上发生了变化时内核使用信号通知该进程。在信号驱动式I/O模型进程使用socket进行信号驱动I/O并建立一个SIGIO信号处理函数当进程通过该信号处理函数向内核发起I/O调用时内核并没有准备好数据报而是返回一个信号给进程此时进程可以继续发起其他I/O调用。也就是说在第一阶段内核准备数据的过程中进程并不会被阻塞会继续执行。当数据报准备好之后内核会递交SIGIO信号通知用户空间的信号处理程序数据已准备好此时进程会发起recvfrom的系统调用这一个阶段与阻塞式I/O无异。也就是说在第二阶段内核复制数据到用户空间的过程中进程同样是被阻塞的。
**信号驱动式I/O的整个过程图如下**
![信号驱动式IO](images/OS/信号驱动式IO.png)
**第一阶段(非阻塞):**
- ①进程使用socket进行信号驱动I/O建立SIGIO信号处理函数向内核发起系统调用内核在未准备好数据报的情况下返回一个信号给进程此时进程可以继续做其他事情
- ②内核将磁盘中的数据加载至内核缓冲区完成后会递交SIGIO信号给用户空间的信号处理程序
**第二阶段(阻塞):**
- ③进程在收到SIGIO信号程序之后进程向内核发起系统调用recvfrom
- ④内核再将内核缓冲区中的数据复制到用户空间中的进程缓冲区中真正执行IO过程的阶段直到数据复制完成
- ⑤内核返回成功数据处理完成的指令给进程进程在收到指令后再对数据包进程处理处理完成后此时的进程解除不可中断睡眠态执行下一个I/O操作
**特点:**借助socket进行信号驱动I/O并建立SIGIO信号处理函数
**优点**
- 线程并没有在第一阶段(数据等待)时被阻塞,提高了资源利用率;
**缺点**
- 在程序的实现上比较困难
- 信号 I/O 在大量 IO 操作时可能会因为信号队列溢出导致没法通知。信号驱动 I/O 尽管对于处理 UDP 套接字来说有用,即这种信号通知意味着到达一个数据报,或者返回一个异步错误。但是,对于 TCP 而言,信号驱动的 I/O 方式近乎无用,因为导致这种通知的条件为数众多,每一个来进行判别会消耗很大资源,与前几种方式相比优势尽失
**信号通知机制**
- **水平触发:**指数据报到内核缓冲区准备好之后内核通知进程后进程因繁忙未发起recvfrom系统调用内核会再次发送通知信号循环往复直到进程来请求recvfrom系统调用。很明显这种方式会频繁消耗过多的系统资源
- **边缘触发:**内核只会发送一次通知信号

@ -1,24 +1,75 @@
每个客户端的Socket连接请求服务端都会对应有个处理线程与之对应对于没有分配到处理线程的连接就会被阻塞或者拒绝。相当于是`一个连接一个线程`。
### 数据的四次拷贝与四次上下文切换
用户需要等待read将socket中的数据读取到buffer后才继续处理接收的数据。整个IO请求的过程中用户线程是被阻塞的这导致用户在发起IO请求时不能做任何事情对CPU的资源利用率不够。
很多应用程序在面临客户端请求时,可以等价为进行如下的系统调用:
![同步阻塞IO](images/OS/同步阻塞IO.png)
- File.read(file, buf, len);
- Socket.send(socket, buf, len);
**特点:**I/O执行的两个阶段进程都是阻塞的。
例如消息中间件 Kafka 就是这个应用场景从磁盘中读取一批消息后原封不动地写入网卡NICNetwork interface controller进行发送。在没有任何优化技术使用的背景下操作系统为此会进行 4 次数据拷贝,以及 4 次上下文切换,如下图所示:
- 使用一个独立的线程维护一个socket连接随着连接数量的增多对虚拟机造成一定压力
- 使用流来读取数据,流是阻塞的,当没有可读/可写数据时,线程等待,会造成资源的浪费
![Zero-Copy](images/OS/Zero-Copy.png)
如果没有优化,读取磁盘数据,再通过网卡传输的场景性能比较差:
**4次copy**
**优点**
- CPU 负责将数据从磁盘搬运到内核空间的 Page Cache 中
- CPU 负责将数据从内核空间的 Socket 缓冲区搬运到的网络中
- CPU 负责将数据从内核空间的 Page Cache 搬运到用户空间的缓冲区
- CPU 负责将数据从用户空间的缓冲区搬运到内核空间的 Socket 缓冲区中
- 能够及时的返回数据,无延迟
- 程序简单进程挂起基本不会消耗CPU时间
**4次上下文切换**
- read 系统调用时:用户态切换到内核态
- read 系统调用完毕:内核态切换回用户态
- write 系统调用时:用户态切换到内核态
- write 系统调用完毕:内核态切换回用户态
**问题分析**
**缺点**
- CPU 全程负责内存内的数据拷贝还可以接受,因为效率还算可以接受,但是如果要全程负责内存与磁盘、网络的数据拷贝,这将难以接受,因为磁盘、网卡的速度远小于内存,内存又远远小于 CPU
- 4 次 copy 太多了4 次上下文切换也太频繁了
- I/O等待对性能影响较大
- 每个连接需要独立的一个进程/线程处理当并发请求量较大时为了维护程序内存、线程和CPU上下文切换开销较大因此较少在开发环境中使用
### 零拷贝技术
零拷贝技术是一个思想指的是指计算机执行操作时CPU 不需要先将数据从某处内存复制到另一个特定区域。
可见,零拷贝的特点是 CPU 不全程负责内存中的数据写入其他组件CPU 仅仅起到管理的作用。但注意,零拷贝不是不进行拷贝,而是 CPU 不再全程负责数据拷贝时的搬运工作。如果数据本身不在内存中,那么必须先通过某种方式拷贝到内存中(这个过程 CPU 可以不参与),因为数据只有在内存中,才能被转移,才能被 CPU 直接读取计算。
零拷贝技术的具体实现方式有很多,例如:
- **sendfile**
- **mmap**
- **splice**
- **直接 Direct I/O**
不同的零拷贝技术适用于不同的应用场景,下面依次进行 sendfile、mmap、Direct I/O 的分析。
- DMA 技术回顾DMA 负责内存与其他组件之间的数据拷贝CPU 仅需负责管理,而无需负责全程的数据拷贝;
- 使用 page cache 的 zero copy
- sendfile一次代替 read/write 系统调用,通过使用 DMA 技术以及传递文件描述符,实现了 zero copy
- mmap仅代替 read 系统调用将内核空间地址映射为用户空间地址write 操作直接作用于内核空间。通过 DMA 技术以及地址映射技术,用户空间与内核空间无须数据拷贝,实现了 zero copy
- 不使用 page cache 的 Direct I/O读写操作直接在磁盘上进行不使用 page cache 机制,通常结合用户空间的用户缓存使用。通过 DMA 技术直接与磁盘/网卡进行数据交互,实现了 zero copy
#### sendfile
snedfile 的应用场景是用户从磁盘读取一些文件数据后不需要经过任何计算与处理就通过网络传输出去。此场景的典型应用是消息队列。在传统 I/O 下,上述应用场景的一次数据传输需要四次 CPU 全权负责的拷贝与四次上下文切换。sendfile 主要使用到了两个技术:
- DMA 技术
- 传递文件描述符代替数据拷贝
**利用DMA技术**
sendfile 依赖于 DMA 技术,将四次 CPU 全程负责的拷贝与四次上下文切换减少到两次,如下图所示:
![利用DMA技术](images/OS/利用DMA技术.png)
利用 DMA 技术减少 2 次 CPU 全程参与的拷贝DMA 负责磁盘到内核空间中的 Page cacheread buffer的数据拷贝以及从内核空间中的 socket buffer 到网卡的数据拷贝。
链接1https://blog.csdn.net/weixin_38726452/article/details/120168360
链接2https://mp.weixin.qq.com/s?__biz=Mzg4MDAxNzkyMw==&mid=2247489694&idx=1&sn=3c9f532eb9d650b2ae2b1db5d0c91b09&chksm=cf7acff2f80d46e431c0bd0d60270a8ce3d31af8a50678e11d5b39bb6c8a9dfa333e7ac56704&mpshare=1&scene=23&srcid=0903RuYh2ynm5j60Y7QtETJO&sharer_sharetime=1638369376739&sharer_shareid=0f9991a2eb945ab493c13ed9bfb8bf4b%23rd

@ -1,26 +1,24 @@
服务器端保存一个Socket连接列表然后对这个列表进行轮询
每个客户端的Socket连接请求服务端都会对应有个处理线程与之对应对于没有分配到处理线程的连接就会被阻塞或者拒绝。相当于是`一个连接一个线程`。
- 如果发现某个Socket端口上有数据可读时说明读就绪则调用该Socket连接的相应读操作
- 如果发现某个Socket端口上有数据可写时说明写就绪则调用该Socket连接的相应写操作
- 如果某个端口的Socket连接已经中断则调用相应的析构方法关闭该端口
用户需要等待read将socket中的数据读取到buffer后才继续处理接收的数据。整个IO请求的过程中用户线程是被阻塞的这导致用户在发起IO请求时不能做任何事情对CPU的资源利用率不够。
这样能充分利用服务器资源效率得到了很大提高在进行I/O操作请求时候再用个线程去处理是`一个请求一个线程`。Java中使用Selector、Channel、Buffer来实现上述效果。
![同步阻塞IO](images/OS/同步阻塞IO.png)
- `Selector`Selector允许单线程处理多个Channel。如果应用打开了多个连接通道但每个连接的流量都很低使用Selector就会很方便。要使用Selector得向Selector注册Channel然后调用他的select方法这个方法会一直阻塞到某个注册的通道有事件就绪。一旦这个方法返回线程就可以处理这些事件事件的例子入有新连接接进来数据接收等。
- `Channel`基本上所有的IO在NIO中都从一个Channel开始。Channel有点像流数据可以从channel**读**到buffer也可以从buffer**写**到channel。
- `Buffer`:缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个容器对象(含数组)该对象提供了一组方法可以更轻松的使用内存块缓冲区对象内置了一些机制能够跟踪和记录缓冲区的状态变换情况Channel提供从文件网络读取数据的渠道但是读取或者写入的数据都必须经由Buffer。
**特点:**I/O执行的两个阶段进程都是阻塞的。
用户需要不断地调用read尝试读取socket中的数据直到读取成功后才继续处理接收的数据。整个IO请求过程中虽然用户线程每次发起IO请求后可以立即返回但为了等到数据仍需要不断地轮询、重复请求消耗了大量的CPU的资源。
- 使用一个独立的线程维护一个socket连接随着连接数量的增多对虚拟机造成一定压力
- 使用流来读取数据,流是阻塞的,当没有可读/可写数据时,线程等待,会造成资源的浪费
![同步非阻塞IO](images/OS/同步非阻塞IO.png)
**特点:**non-blocking I/O模式需要不断的主动询问kernel数据是否已准备好。
**优点**
- 进程在等待当前任务完成时可以同时执行其他任务进程不会被阻塞在内核等待数据过程每次发起的I/O请求会立即返回具有较好的实时性
- 能够及时的返回数据,无延迟
- 程序简单进程挂起基本不会消耗CPU时间
**缺点**
- 不断轮询将占用大量CPU时间系统资源利用率大打折扣影响性能整体数据吞吐量下降
- 该模型不适用web服务器
- I/O等待对性能影响较大
- 每个连接需要独立的一个进程/线程处理当并发请求量较大时为了维护程序内存、线程和CPU上下文切换开销较大因此较少在开发环境中使用

@ -1,15 +1,26 @@
通过Reactor的方式可以将用户线程轮询IO操作状态的工作统一交给handle_events事件循环进行处理。用户线程注册事件处理器之后可以继续执行做其他的工作异步而Reactor线程负责调用内核的select函数检查socket状态。当有socket被激活时则通知相应的用户线程或执行用户线程的回调函数执行handle_event进行数据读取、处理的工作。
服务器端保存一个Socket连接列表然后对这个列表进行轮询
![IO多路复用](images/OS/IO多路复用.png)
- 如果发现某个Socket端口上有数据可读时说明读就绪则调用该Socket连接的相应读操作
- 如果发现某个Socket端口上有数据可写时说明写就绪则调用该Socket连接的相应写操作
- 如果某个端口的Socket连接已经中断则调用相应的析构方法关闭该端口
**特点:**通过一种机制能同时等待多个文件描述符而这些文件描述符套接字描述符其中的任意一个变为可读就绪状态select()/poll()函数就会返回。
这样能充分利用服务器资源效率得到了很大提高在进行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)
**特点:**non-blocking I/O模式需要不断的主动询问kernel数据是否已准备好。
**优点**
- 可以基于一个阻塞对象,同时在多个描述符上可读就绪,而不是使用多个线程(每个描述符一个线程),即能处理更多的连接
- 可以节省更多的系统资源
- 进程在等待当前任务完成时可以同时执行其他任务进程不会被阻塞在内核等待数据过程每次发起的I/O请求会立即返回具有较好的实时性
**缺点:**
**缺点**
- 如果处理的连接数不是很多的话使用select/poll的web server不一定比使用multi-threading + blocking I/O的web server性能更好
- 可能延迟还更大因为处理一个连接数需要发起两次system call
- 不断轮询将占用大量CPU时间系统资源利用率大打折扣影响性能整体数据吞吐量下降
- 该模型不适用web服务器

@ -1,14 +1,15 @@
AIO(异步非阻塞IO,即NIO.2)。异步IO模型中用户线程直接使用内核提供的异步IO API发起read请求且发起后立即返回继续执行用户线程代码。不过此时用户线程已经将调用的AsynchronousOperation和CompletionHandler注册到内核然后操作系统开启独立的内核线程去处理IO操作。当read请求的数据到达时由内核负责读取socket中的数据并写入用户指定的缓冲区中。最后内核将read的数据和用户线程注册的CompletionHandler分发给内部ProactorProactor将IO完成的信息通知给用户线程一般通过调用用户线程注册的完成事件处理函数完成异步IO
通过Reactor的方式可以将用户线程轮询IO操作状态的工作统一交给handle_events事件循环进行处理。用户线程注册事件处理器之后可以继续执行做其他的工作异步而Reactor线程负责调用内核的select函数检查socket状态。当有socket被激活时则通知相应的用户线程或执行用户线程的回调函数执行handle_event进行数据读取、处理的工作
![异步非阻塞IO](images/OS/异步非阻塞IO.png)
![IO多路复用](images/OS/IO多路复用.png)
**特点:**第一阶段和第二阶段都是有内核完成
**特点:**通过一种机制能同时等待多个文件描述符而这些文件描述符套接字描述符其中的任意一个变为可读就绪状态select()/poll()函数就会返回
**优点**
- 能充分利用DMA的特性将I/O操作与计算重叠提高性能、资源利用率与并发能力
- 可以基于一个阻塞对象,同时在多个描述符上可读就绪,而不是使用多个线程(每个描述符一个线程),即能处理更多的连接
- 可以节省更多的系统资源
**缺点**
**缺点**
- 在程序的实现上比较困难
- 要实现真正的异步 I/O操作系统需要做大量的工作。目前 Windows 下通过 IOCP 实现了真正的异步 I/O。而在 Linux 系统下Linux 2.6才引入,目前 AIO 并不完善,因此在 Linux 下实现高并发网络编程时都是以 复用式I/O模型为主
- 如果处理的连接数不是很多的话使用select/poll的web server不一定比使用multi-threading + blocking I/O的web server性能更好
- 可能延迟还更大因为处理一个连接数需要发起两次system call

@ -3,11 +3,12 @@
* [✍ Reactor模式](src/OS/3 "Reactor模式")
* [✍ Proactor模式](src/OS/4 "Proactor模式")
* [✍ select/poll/epoll](src/OS/5 "select/poll/epoll")
* [✍ BIO(同步阻塞I/O)](src/OS/6 "BIO(同步阻塞I/O)")
* [✍ NIO(同步非阻塞I/O)](src/OS/7 "NIO(同步非阻塞I/O)")
* [✍ IO多路复用(异步阻塞I/O)](src/OS/8 "IO多路复用(异步阻塞I/O)")
* [✍ AIO(异步非阻塞I/O)](src/OS/9 "AIO(异步非阻塞I/O)")
* [✍ 信号驱动式I/O](src/OS/10 "信号驱动式I/O")
* [✍ 零拷贝(Zero-Copy)](src/OS/6 "零拷贝(Zero-Copy)")
* [✍ BIO(同步阻塞I/O)](src/OS/7 "BIO(同步阻塞I/O)")
* [✍ NIO(同步非阻塞I/O)](src/OS/8 "NIO(同步非阻塞I/O)")
* [✍ IO多路复用(异步阻塞I/O)](src/OS/9 "IO多路复用(异步阻塞I/O)")
* [✍ AIO(异步非阻塞I/O)](src/OS/10 "AIO(异步非阻塞I/O)")
* [✍ 信号驱动式I/O](src/OS/11 "信号驱动式I/O")
* [🏁 TCP](src/OS/101 "TCP")
* [✍ 网络模型](src/OS/102 "网络模型")
* [✍ TCP状态](src/OS/103 "TCP状态")

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

@ -0,0 +1,58 @@
**问题**A文件有30亿个QQ号码B文件有40亿个QQ号码求A文件和B文件中QQ号码的交集内存大小限制为1GB。
### 方案一:直接暴力比较
最简单的方法是直接暴力比较双重循环比较。显然这种方法要比较的次数是30亿×40亿时间复杂度太大。
### 方案二利用hashmap
将B文件中的40亿个QQ号码放入Hash表中然后遍历B文件中的30亿个QQ号码准一判断是否已在Hash表中存在。
显然应该用哈希表优化查找速度使得时间复杂度大大降低只需要遍历上面一层即可。然而空间的占用还是太大了1GB的内存根本无法容纳40亿个QQ号。
### 方案三:分治切割文件
既然内存容纳不下那就想办法进行切割比如根据QQ号码的最后一位的值来切割A文件和B文件使文件变小。显然尾数为j的QQ号码只可能在Aj文件和Bj文件中只需要比较Aj和Bj文件即可。
| QQ号最后一位 | A文件的切割 | B文件的切割 |
| ------------ | ----------- | ----------- |
| 0 | A0 | B0 |
| 1 | A1 | B1 |
| 2 | A2 | B2 |
| 3 | A3 | B3 |
| ... | ... | ... |
通过切割的方法可以化大为小让内存容纳得下。需要强调的是仅仅以QQ号最后一位来划分那么每个小文件的数据量大约是原来文件的1/10, 可能还是偏大。所以可以考虑以QQ号的最后3位来划分那么每个小文件的数据量大约是原来大文件的1/1000甚至还可以更细地来进行划分。通过一定的规则进行分割把A文件和B文件中同类型数据划分到对应的小文件中解决了内存问题。
### 方案四利用bitmap
可以对hashmap进行优化采用bitmap这种数据结构可以顺利地同时解决时间问题和空间问题。
| bit | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 |
| ----- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- |
| index | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
1个unsigned char类型的数据可以标识0~7这8个整数的存在与否。以此类推
- 1个unsigned int类型数据可以标识0~31这322^5个整数的存在与否
- 2个unsigned int类型数据可以标识0~63这642^6个整数的存在与否
- 3个unsigned int类型数据可以标识0~127这1282^7个整数的存在与否
- 4个unsigned int类型数据可以标识0~255这2562^8个整数的存在与否
- ......
- 28个unsigned int类型数据可以标识0~2^32-1这43亿2^32个整数的存在与否
10位QQ号码的理论最大值为 2^32 - 1 ≈ 43 亿bit。
占用内存43亿bit≈5.37亿bytes≈52万KB≈512MB
显然可以推导出512MB大小足够标识所有QQ号码的存在与否请注意QQ号码的理论最大值为2^32 - 1大概是43亿左右。接下来的问题就很简单了
用512MB的unsigned int数组来记录B文件中QQ号码的存在与否形成一个bitmap。然后遍历A文件中的QQ看是否在bitmap中如果在那么该QQ号码就同时存在于A和B两个文件中。
https://mp.weixin.qq.com/s/Q_EvlN9LvkdA5M5EharBBA

@ -0,0 +1,11 @@
文件中有40亿个QQ号码请设计算法对QQ号码去重相同的QQ号码仅保留一个内存限制1G。
https://mp.weixin.qq.com/s/YlLYDzncB6tqbffrg__13w
### 方法一:排序
### 方法二hashmap
### 方法三:文件切割
### 方法四bitmap

@ -0,0 +1,11 @@
企业IM比如企业微信、钉钉里面的群消息的有个已读未读的功能发送者刚发出消息时当前群里其他群成员都是未读状态陆陆续续有人看了这个消息这时候消息的详情变成x人已读y人未读。每条消息对应一个唯一的`messageiduint64_t`,每个用户对应一个唯一的`useriduint64_t`,应该如何保存这个消息对应的已读未读详情呢?
### 简单粗暴方案
对于每一个messageid存当前`read_ids + unread_ids`当群成员A已读某一条消息时把A的userid从unread_ids移除写到read_ids上就好了客户端更新到messageid对应的详情列表就可以展示m人已读n人未读。
按照目前的设计每一条消息已读未读详情就要占用8B × 群成员数的内存如果一个活跃的200人大群每发一条消息已读未读就要1600B如果平均每天消息量是1k那每个这样的群每天就要1.6MB磁盘空间,对于客户端来说,特别是手机端,占用磁盘空间是用户不能接受的,又不能把工作消息删了,对于服务器端来说,用户群体如果特别大,那数据库存储这个成本也不小。
### bitmap方案

@ -0,0 +1,4 @@
!> 你所查看的内容即将发布敬请期待有问题欢迎点击右上角Gitee仓库留言
🚀点击左侧菜单栏查看其它文章吧!

@ -0,0 +1,5 @@
<div style="color:#16b0ff;font-size:50px;font-weight: 900;text-shadow: 5px 5px 10px var(--theme-color);font-family: 'Comic Sans MS';">Open</div>
<span style="color:#16b0ff;font-size:20px;font-weight: 900;font-family: 'Comic Sans MS';">Introduction</span>:收纳开放性设计问题相关的知识总结!
## 🚀点击左侧菜单栏开始吧!

@ -0,0 +1,5 @@
* 🏁 开放性设计
* [✍ 求两个文件中的QQ交集](src/Open/1 "求两个文件中的QQ交集")
* [✍ 40亿个QQ号码如何去重](src/Open/2 "40亿个QQ号码如何去重")
* [✍ 群聊消息已读未读功能设计](src/Open/3 "群聊消息已读未读功能设计")
* [✍ 未支付订单超时关闭](src/Open/4 "未支付订单超时关闭")

@ -39,3 +39,32 @@
- **警告**有些服务在一段时间内成功率有波动如在95~100%之间),可以自动降级或人工降级,并发送告警
- **错误**比如可用率低于90%,或数据库连接池被打爆,或访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级
- **严重错误**:比如因为特殊原因数据错误了,此时需要紧急人工降级
### 热点数据
数据库里有2000W数据Redis中只存20W的数据如何保证 Redis 中的数据都是热点数据?
当 Redis 中的数据集上升到一定大小的时候,就需要实施数据淘汰策略,以保证 Redis 的内存不会被撑爆;那么如何保证 Redis 中的数据都是热点数据,需要先看看 Redis 有哪些数据淘汰策略。
**Redis 数据淘汰策略**
- volatile-lru从已设置过期时间的数据集中挑选最近最少使用的数据淘汰
- volatile-ttl从已设置过期时间的数据集中挑选将要过期的数据淘汰
- volatile-random从已设置过期时间的数据集中任意选择数据淘汰
- allkeys-lru当内存不足以容纳新写入数据时移除最近最少使用的key
- allkeys-random从数据集中任意选择数据淘汰
- no-eviction禁止淘汰数据也就是说当内存不足时新写入操作会报错
**到了4.0版本后,又增加以下两种淘汰策略:**
- volatile-lfu从已设置过期时间的数据集中挑选最不经常使用的数据淘汰注意lfu和lru的区别
- allkeys-lfu当内存不足以容纳新写入数据时移除最不经常使用的key
**如何选择策略规则**
针对题目中的问题,还需要考虑数据的分布情况:
- 如果数据呈现幂律分布一部分数据访问频率高一部分数据访问频率低则可以使用allkeys-lru或allkeys-lfu
- 如果数据呈现平等分布所有的数据访问频率都相同则使用allkeys-random
Loading…
Cancel
Save