开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

Hadoop企业级生产调优手册(二)

发表于 2021-11-27

「这是我参与11月更文挑战的第27天,活动详情查看:2021最后一次更文挑战」。

上节:Hadoop企业级生产调优手册(一)

五、HDFS存储优化

注:演示纠删码和异构存储需要一共 5 台虚拟机。尽量拿另外一套集群。提前准备 5 台服务器的集群。

5.1 纠删码

5.1.1 纠删码原理

HDFS 默认情况下,一个文件有 3 个副本,这样提高了数据的可靠性,但也带来了 2 倍的冗余开销。 Hadoop3.x 引入了纠删码, 采用计算的方式, 可以节省约 50%左右的存储空间。

纠删码操作相关的命令

1
2
3
4
5
6
7
8
9
10
11
12
powershell复制代码[Tom@hadoop102 hadoop-3.1.3]$ hdfs ec
Usage: bin/hdfs ec [COMMAND]
[-listPolicies]
[-addPolicies -policyFile <file>]
[-getPolicy -path <path>]
[-removePolicy -policy <policy>]
[-setPolicy -path <path> [-policy <policy>] [-replicate]]
[-unsetPolicy -path <path>]
[-listCodecs]
[-enablePolicy -policy <policy>]
[-disablePolicy -policy <policy>]
[-help <command-name>]

查看当前支持的纠删码策略

1
2
3
4
5
6
7
powershell复制代码[Tom@hadoop102 hadoop-3.1.3]$ hdfs ec -listPolicies
Erasure Coding Policies:
ErasureCodingPolicy=[Name=RS-10-4-1024k, Schema=[ECSchema=[Codec=rs, numDataUnits=10, numParityUnits=4]], CellSize=1048576, Id=5], State=DISABLED
ErasureCodingPolicy=[Name=RS-3-2-1024k, Schema=[ECSchema=[Codec=rs, numDataUnits=3, numParityUnits=2]], CellSize=1048576, Id=2], State=DISABLED
ErasureCodingPolicy=[Name=RS-6-3-1024k, Schema=[ECSchema=[Codec=rs, numDataUnits=6, numParityUnits=3]], CellSize=1048576, Id=1], State=ENABLED
ErasureCodingPolicy=[Name=RS-LEGACY-6-3-1024k, Schema=[ECSchema=[Codec=rs-legacy, numDataUnits=6, numParityUnits=3]], CellSize=1048576, Id=3], State=DISABLED
ErasureCodingPolicy=[Name=XOR-2-1-1024k, Schema=[ECSchema=[Codec=xor, numDataUnits=2, numParityUnits=1]], CellSize=1048576, Id=4], State=DISABLED

纠删码策略解释

(1)RS-3-2-1024k:使用 RS 编码,每 3 个数据单元,生成 2 个校验单元,共 5 个单元,也就是说:这 5 个单元中,只要有任意的 3 个单元存在(不管是数据单元还是校验单元,只要总数=3),就可以得到原始数据。每个单元的大小是 1024k=1024*1024=1048576。

(2)RS-10-4-1024k:使用 RS 编码,每 10 个数据单元( cell),生成 4 个校验单元,共 14 个单元,也就是说:这 14 个单元中,只要有任意的 10 个单元存在 (不管是数据单元还是校验单元,只要总数 =10),就可以得到原始数据。每个单元的大小是 1024k=1024*1024=1048576。

(3)RS-6-3-1024k:使用 RS 编码,每 6 个数据单元,生成 3 个校验单元,共 9 个单元,也就是说:这 9 个单元中,只要有任意的 6 个单元存在(不管是数据单元还是校验单元,只要总数 =6),就可以得到原始数据。每个单元的大小是 1024k=1024*1024=1048576。

(4)RS-LEGACY-6-3-1024k:策略和上面的 RS-6-3-1024k 一样,只是编码的算法用的是 rs-legacy。

(5)XOR-2-1-1024k:使用 XOR 编码(速度比 RS 编码快),每 2 个数据单元,生成 1 个校验单元,共 3 个单元,也就是说:这 3 个单元中,只要有任意的 2 个单元存在(不管是数据单元还是校验单元,只要总数 = 2),就可以得到原始数据。每个单元的大小是 1024k=1024*1024=1048576。

5.1.2 纠删码案例实操

纠删码策略是给具体一个路径设置 。所有往此路径下存储的文件,都会执行此策略。默认只开启对 RS-6-3-1024k 策略的支持 ,如要使用别的策略需要提前启用 。

需求:将 /input 目录设置为 RS-3-2-1024k 策略

具体步骤

(1)开启对 RS-3-2-1024k 策略的支持

1
2
powershell复制代码[Tom@hadoop102 hadoop-3.1.3]$ hdfs ec -enablePolicy -policy RS-3-2-1024k
Erasure coding policy RS-3-2-1024k is enabled

(2)在 HDFS 创建目录,并设置 RS-3-2-1024k 策略

1
2
3
powershell复制代码[Tom@hadoop102 hadoop-3.1.3]$ hdfs dfs -mkdir /input
[Tom@hadoop102 hadoop-3.1.3]$ hdfs ec -setPolicy -path /input -policy RS-3-2-1024k
Set RS-3-2-1024k erasure coding policy on /input

(3)上传文件,并查看文件编码后的存储情况

1
powershell复制代码[Tom@hadoop102 hadoop-3.1.3]$ hdfs dfs -put web.log /input

(4)查看存储路径的数据单元和校验单元,并作破坏实验

5.2 异构存储(冷热数据分离)

异构存储主要解决,不同的数据,存储在不同类型的硬盘中,达到最佳性能的问题。

关于存储类型

RAM_DISK:(内存镜像文件系统)

SSD:(SSD 固态硬盘)

DISK:(普通磁盘,在 HDFS 中,如果没有主动声明数据目录存储类型默认都是 DISK)

ARCHIVE:(没有特指哪种存储介质,主要的指的是计算能力比较弱而存储密度比较高的存储介质,用来解决数据量的容量扩增的问题,一般用于归档)

关于存储策略 说明:从 Lazy_Persist 到 Cold,分别代表了设备的访问速度从快到慢

5.2.1 异构存储Shell操作

(1)查看当前有哪些存储策略可以用

1
2
3
4
5
6
7
8
9
powershell复制代码[Tom@hadoop102 ~]$ hdfs storagepolicies -listPolicies
Block Storage Policies:
BlockStoragePolicy{PROVIDED:1, storageTypes=[PROVIDED, DISK], creationFallbacks=[PROVIDED, DISK], replicationFallbacks=[PROVIDED, DISK]}
BlockStoragePolicy{COLD:2, storageTypes=[ARCHIVE], creationFallbacks=[], replicationFallbacks=[]}
BlockStoragePolicy{WARM:5, storageTypes=[DISK, ARCHIVE], creationFallbacks=[DISK, ARCHIVE], replicationFallbacks=[DISK, ARCHIVE]}
BlockStoragePolicy{HOT:7, storageTypes=[DISK], creationFallbacks=[], replicationFallbacks=[ARCHIVE]}
BlockStoragePolicy{ONE_SSD:10, storageTypes=[SSD, DISK], creationFallbacks=[SSD, DISK], replicationFallbacks=[SSD, DISK]}
BlockStoragePolicy{ALL_SSD:12, storageTypes=[SSD], creationFallbacks=[DISK], replicationFallbacks=[DISK]}
BlockStoragePolicy{LAZY_PERSIST:15, storageTypes=[RAM_DISK, DISK], creationFallbacks=[DISK], replicationFallbacks=[DISK]}

(2)为指定路径 (数据存储目录) 设置指定的存储策略

1
powershell复制代码hdfs storagepolicies -setStoragePolicy -path xxx -policy xxx

(3)获取指定路径(数据存储目录或文件)的存储策略

1
powershell复制代码hdfs storagepolicies -getStoragePolicy -path xxx

(4)取消存储策略;执行改命令之后该目录或者文件,以其上级的目录为准,如果是根目录,那么就是 HOT

1
powershell复制代码hdfs storagepolicies -unsetStoragePolicy -path xxx

(5)查看文件块的分布

1
powershell复制代码bin/hdfs fsck xxx -files -blocks -locations

(6)查看集群节点

1
powershell复制代码hadoop dfsadmin -report

5.2.2 测试环境准备

测试环境描述

服务器规模:5 台

集群配置:副本数为 2,创建好带有存储类型的目录(提前创建)

集群规划:

节点 存储类型分配
hadoop102 RAM_DISK,SSD
hadoop103 SSD,DISK
hadoop104 DISK,RAM_DISK
hadoop105 ARCHIVE
hadoop106 ARCHIVE

配置文件信息

(1)为 hadoop102 节点的 hdfs-site.xml 添加如下信息

1
2
3
4
5
6
7
8
9
10
11
12
xml复制代码<property>
<name>dfs.replication</name>
<value>2</value>
</property>
<property>
<name>dfs.storage.policy.enabled</name>
<value>true</value>
</property>
<property>
<name>dfs.datanode.data.dir</name>
<value>[SSD]file:///opt/module/hadoop-3.1.3/hdfsdata/ssd,[RAM_DISK]file:///opt/module/hadoop-3.1.3/hdfsdata/ram_disk</value>
</property>

(2)为 hadoop103 节点的 hdfs-site.xml 添加如下信息

1
2
3
4
5
6
7
8
9
10
11
12
xml复制代码<property>
<name>dfs.replication</name>
<value>2</value>
</property>
<property>
<name>dfs.storage.policy.enabled</name>
<value>true</value>
</property>
<property>
<name>dfs.datanode.data.dir</name>
<value>[SSD]file:///opt/module/hadoop-3.1.3/hdfsdata/ssd,[DISK]file:///opt/module/hadoop-3.1.3/hdfsdata/disk</value>
</property>

(3)为 hadoop104 节点的 hdfs-site.xml 添加如下信息

1
2
3
4
5
6
7
8
9
10
11
12
xml复制代码<property>
<name>dfs.replication</name>
<value>2</value>
</property>
<property>
<name>dfs.storage.policy.enabled</name>
<value>true</value>
</property>
<property>
<name>dfs.datanode.data.dir</name>
<value>[RAM_DISK]file:///opt/module/hdfsdata/ram_disk,[DISK]file:///opt/module/hadoop-3.1.3/hdfsdata/disk</value>
</property>

(4)为 hadoop105 节点的 hdfs-site.xml 添加如下信息

1
2
3
4
5
6
7
8
9
10
11
12
xml复制代码<property>
<name>dfs.replication</name>
<value>2</value>
</property>
<property>
<name>dfs.storage.policy.enabled</name>
<value>true</value>
</property>
<property>
<name>dfs.datanode.data.dir</name>
<value>[ARCHIVE]file:///opt/module/hadoop-3.1.3/hdfsdata/archive</value>
</property>

(5)为 hadoop106 节点的 hdfs-site.xml 添加如下信息

1
2
3
4
5
6
7
8
9
10
11
12
xml复制代码<property>
<name>dfs.replication</name>
<value>2</value>
</property>
<property>
<name>dfs.storage.policy.enabled</name>
<value>true</value>
</property>
<property>
<name>dfs.datanode.data.dir</name>
<value>[ARCHIVE]file:///opt/module/hadoop-3.1.3/hdfsdata/archive</value>
</property>

数据准备

(1)启动集群

1
2
powershell复制代码[Tom@hadoop102 hadoop-3.1.3]$ hdfs namenode -format
[Tom@hadoop102 hadoop-3.1.3]$ myhadoop.sh start

(2)在 HDFS 上创建文件目录

1
powershell复制代码[Tom@hadoop102 hadoop-3.1.3]$ hadoop fs -mkdir /hdfsdata

(3)将文件资料上传

1
powershell复制代码[Tom@hadoop102 hadoop-3.1.3]$ hadoop fs -put /opt/module/hadoop-3.1.3/NOTICE.txt /hdfsdata

5.2.3 HOT存储策略案例

(1)最开始我们未设置存储策略的情况下,我们获取该目录的存储策略

1
powershell复制代码[Tom@hadoop102 hadoop-3.1.3]$ hdfs storagepolicies -getStoragePolicy -path /hdfsdata

(2)我们查看上传的文件块分布

1
2
powershell复制代码[Tom@hadoop102 hadoop-3.1.3]$ hdfs fsck /hdfsdata-files -blocks -locations
[DatanodeInfoWithStorage[192.168.10.104:9866,DS-0b133854-7f9e-48df-939b-5ca6482c5afb,DISK], DatanodeInfoWithStorage[192.168.10.103:9866,DS-ca1bd3b9-d9a5-4101-9f92-3da5f1baa28b,DISK]]

未设置存储策略,所有文件块都存储在 DISK 下。 所以, 默认存储策略为 HOT。

5.2.4 WARM存储策略测试

(1)接下来我们为数据降温

1
powershell复制代码[Tom@hadoop102 hadoop-3.1.3]$ hdfs storagepolicies -setStoragePolicy -path /hdfsdata -policy WARM

(2)再次查看文件块分布,我们可以看到文件块依然放在原处。

1
powershell复制代码[atguigu@hadoop102 hadoop-3.1.3]$ hdfs fsck /hdfsdata-files -blocks -locations

(3)我们需要让他 HDFS 按照存储策略自行移动文件块

1
powershell复制代码[Tom@hadoop102 hadoop-3.1.3]$ hdfs mover /hdfsdata

(4)再次查看文件块分布

1
2
powershell复制代码[Tom@hadoop102 hadoop-3.1.3]$ hdfs fsck /hdfsdata -files -blocks -locations
[DatanodeInfoWithStorage[192.168.10.105:9866,DS-d46d08e1-80c6-4fca-b0a2-4a3dd7ec7459,ARCHIVE], DatanodeInfoWithStorage[192.168.10.103:9866,DS-ca1bd3b9-d9a5-4101-9f92-3da5f1baa28b,DISK]]

文件块一半在 DISK,一半在 ARCHIVE,符合我们设置的 WARM 策略

5.2.5 COLD策略测试

(1)我们继续将数据降温为 cold

1
powershell复制代码[Tom@hadoop102 hadoop-3.1.3]$ hdfs storagepolicies -setStoragePolicy -path /hdfsdata -policy COLD

注意 :当我们将目录设置为 COLD 并且我们未配置 ARCHIVE 存储目录的情况下,不可以向该目录直接上传文件,会报出异常。

(2)手动转移

1
powershell复制代码[Tom@hadoop102 hadoop-3.1.3]$ hdfs mover /hdfsdata

(3)检查文件块的分布

1
2
powershell复制代码[Tom@hadoop102 hadoop-3.1.3]$ bin/hdfs fsck /hdfsdata -files -blocks -locations
[DatanodeInfoWithStorage[192.168.10.105:9866,DS-d46d08e1-80c6-4fca-b0a2-4a3dd7ec7459,ARCHIVE], DatanodeInfoWithStorage[192.168.10.106:9866,DS-827b3f8b-84d7-47c6-8a14-0166096f919d,ARCHIVE]]

所有文件块都在 ARCHIVE,符合 COLD 存储策略。

5.2.6 ONE_SSD策略测试

(1)接下来我们将存储策略从默认的 HOT 更改为 One_SSD

1
powershell复制代码[Tom@hadoop102 hadoop-3.1.3]$ hdfs storagepolicies -setStoragePolicy -path /hdfsdata -policy One_SSD

(2)手动转移文件块

1
powershell复制代码[Tom@hadoop102 hadoop-3.1.3]$ hdfs mover /hdfsdata

(3)转移完成后,检查文件块的分布

1
2
powershell复制代码[Tom@hadoop102 hadoop-3.1.3]$ bin/hdfs fsck /hdfsdata -files -blocks -locations
[DatanodeInfoWithStorage[192.168.10.104:9866,DS-0b133854-7f9e-48df-939b-5ca6482c5afb,DISK], DatanodeInfoWithStorage[192.168.10.103:9866,DS-2481a204-59dd-46c0-9f87-ec4647ad429a,SSD]]

文件块分布为一半在 SSD,一半在 DISK,符合 One_SSD 存储策略。

5.2.7 ALL_SSD策略测试

(1)接下来我们将存储策略从默认的 HOT 更改为 All_SSD

1
powershell复制代码[Tom@hadoop102 hadoop-3.1.3]$ hdfs storagepolicies -setStoragePolicy -path /hdfsdata -policy All_SSD

(2)手动转移文件块

1
powershell复制代码[Tom@hadoop102 hadoop-3.1.3]$ hdfs mover /hdfsdata

(3)转移完成后,检查文件块的分布

1
2
powershell复制代码[Tom@hadoop102 hadoop-3.1.3]$ bin/hdfs fsck /hdfsdata -files -blocks -locations
[DatanodeInfoWithStorage[192.168.10.102:9866,DS-c997cfb4-16dc-4e69-a0c4-9411a1b0c1eb,SSD], DatanodeInfoWithStorage[192.168.10.103:9866,DS-2481a204-59dd-46c0-9f87-ec4647ad429a,SSD]]

所有的文件块都存储在 SSD,符合 All_SSD 存储策略。

5.2.8 LAZY_PERSIST策略测试

(1))继续改变策略,将存储策略改为 lazy_persist

1
powershell复制代码[Tom@hadoop102 hadoop-3.1.3]$ hdfs storagepolicies -setStoragePolicy -path /hdfsdata -policy policy lazy_persist

(2)手动转移文件块

1
powershell复制代码[Tom@hadoop102 hadoop-3.1.3]$ hdfs mover /hdfsdata

(3)转移完成后,检查文件块的分布

1
2
powershell复制代码[Tom@hadoop102 hadoop-3.1.3]$ bin/hdfs fsck /hdfsdata -files -blocks -locations
[DatanodeInfoWithStorage[192.168.10.104:9866,DS-0b133854-7f9e-48df-939b-5ca6482c5afb,DISK], DatanodeInfoWithStorage[192.168.10.103:9866,DS-ca1bd3b9-d9a5-4101-9f92-3da5f1baa28b,DISK]]

这里我们发现所有的文件块都是存储在

DISK,按照理论一个副本存储在 RAM_DISK,其他副本存储在 DISK 中,这是因为,我们还需要配置” dfs.datanode.max.locked.memory”,”dfs.block.size”参数。

那么出现存储策略为 LAZY_PERSIST 时,文件块副本都存储在 DISK 上的原因有如下两点:

(1)当客户端所在的 DataNode 节点没有 RAM_DISK 时,则会写入客户端所在的 DataNode 节点的 DISK 磁盘,其余副本会写入其他节点的 DISK 磁盘。

(2)当客户端所在的 DataNode 有 RAM_DISK,但 dfs.datanode.max.locked.memory 参数值未设置或者设置过小(小于“ dfs.block.size”参数值)时,则会写入客户端所在的 DataNode 节点的 DISK 磁盘,其余副本会写入其他节点的 DISK 磁盘。

但是由于虚拟机的“max locked memory”为 64KB,所以,如果参数配置过大,还会报出错误:

1
powershell复制代码ERROR org.apache .hadoop.hdfs.server.datanode.DataNode: Exception in secureMainjava.lang.RuntimeException: Cannot start datanode because the configured max locked memory size(dfs.datanode.max.locked.memory) of 209715200 bytes is more than the datanode's available RLIMIT_ MEMLOCK ulimit of 65536 bytes.

我们可以通过该命令查询此参数的内存

1
2
powershell复制代码[Tom@hadoop102 hadoop-3.1.3]$ ulimit -a
max locked memory (kbytes, -l) 64

六、HDFS故障排除

6.1 集群安全模式

安全模式:文件系统只接受读数据请求,而不接受删除、修改等变更请求

进入安全模式场景:

NameNode 在加载镜像文件和编辑日志期间处于安全模式;

NameNode 再接收 DataNode 注册时,处于安全模式。

退出安全模式条件

dfs.namenode.safemode.min.datanodes:最小可用 datanode 数量 ,默认 0

dfs.namenode.safemode.threshold-pct:副本数达到最小要求的 block 占系统总 block 数的百分比 ,默认 0.999f。(只允许丢一个块)

dfs.namenode.safemode.extension:稳定时间 ,默认值 30000 毫秒,即 30 秒

基本语法

集群处于安全模式,不能执行重要操作(写操作) 。 集群启动完成后,自动退出安全模式。

1
2
3
4
powershell复制代码(1)bin/hdfs dfsadmin -safemode get(功能描述:查看安全模式状态)
(2)bin/hdfs dfsadmin -safemode enter (功能描述:进入安全模式状态)
(3)bin/hdfs dfsadmin -safemode leave(功能描述:离开安全模式状态)
(4)bin/hdfs dfsadmin -safemode wait(功能描述:等待安全模式状态)

案例:集群启动后,立即来到集群上删除数据,提示集群处于安全模式

6.2 慢磁盘监控

“慢磁盘”指的时写入数据非常慢的一类磁盘。其实慢性磁盘并不少见,当机器运行时间长了,上面跑的任务多了,磁盘的读写性能自然会退化,严重时就会出现写入数据延时的问题。

如何发现慢磁盘?

正常在 HDFS 上创建一个目录,只需要不到 1s 的时间。如果你发现创建目录超过 1 分钟及以上,而且这个现象并不是每次都有。只是偶尔慢了一下,就很有可能存在慢磁盘。可以采用如下方法找出是哪块磁盘慢:

通过心跳未联系时间

一般出现慢磁盘现象,会影响到 DataNode 与 NameNode 之间的心跳。正常情况心跳时间间隔是 3s。超过 3s 说明有异常。

fio 命令,测试磁盘的读写性能

(1)顺序读测试

1
2
3
4
powershell复制代码[Tom@hadoop102 ~]#sudo yum install -y fio
[Tom@hadoop102 ~]# sudo fio -filename=/home/Tom/test.log -direct=1 -iodepth 1 -thread -rw=read -ioengine=psync-bs=16k -size=2G -numjobs=10 -runtime=60 -group_reporting -name=test_r
Run status group 0 (all jobs):
READ: bw=360MiB/s (378MB/s), 360MiB/s-360MiB/s (378MB/s-378MB/s), io=20.0GiB (21.5GB), run=56885-56885msec

结果显示,磁盘的总体顺序读速度为 360MiB/s

(2)顺序写测试

1
2
3
powershell复制代码[Tom@hadoop102 ~]# sudofio -filename=/home/Tom/test.log -direct=1 -iodepth 1 -thread -rw=write -ioengine=psync -bs=16k -size=2G -numjobs=10 -runtime=60 -group_reporting -name=test_w
Run status group 0 (all jobs):
WRITE: bw=341MiB/s (357MB/s), 341MiB/s-341MiB/s (357MB/s-357MB/s), io=19.0GiB (21.4GB), run=60001-60001msec

结果显示,磁盘的总体顺序写速度为 341MiB/s

(3)随机写测试

1
2
3
powershell复制代码[Tom@hadoop102 ~]#sudofio -filename=/home/Tom/test.log -direct=1 -iodepth 1 -thread -rw=randwrite -ioengine=psync-bs=16k -size=2G -numjobs=10 -runtime=60 -group_reporting -name=test_randw
Run status group 0 (all jobs):
WRITE: bw=309MiB/s (324MB/s), 309MiB/s-309MiB/s (324MB/s-324MB/s), io=18.1GiB (19.4GB), run=60001-60001msec

结果显示,磁盘的总体随机写速度为 309MiB/s。

(4)顺序读测试

1
2
3
4
powershell复制代码[Tom@hadoop102 ~]# sudo fio -filename=/home/Tom/test.log -direct=1 -iodepth 1 -thread -rw=randrw -rwmixread=70 -ioengine=psync -bs=16k -size=2G -numjobs=10 -runtime=60 -group_reporting -name=test_r_w -ioscheduler=noop
Run status group 0 (all jobs):
READ: bw=220MiB/s(231MB/s), 220MiB/s-220MiB/s (231MB/s-231MB/s), io=12.9GiB (13.9GB), run=60001-60001msec
WRITE: bw=94.6MiB/s (99.2MB/s), 94.6MiB/s-94.6MiB/s (99.2MB/s-99.2MB/s), io=5674MiB (5950MB), run=60001-60001msec

结果显示,磁盘的总体混合随机读写 ,读速度为 220MiB/s,写速度 94.6MiB/s。

6.3 小文件归档

HDFS 存储小文件弊端

每个文件均按块存储,每个块的元数据存储在 NameNode 的内存中,因此 HDFS 存储小文件会非常低效。因为大量的小文件会耗尽 NameNode 中的大部分内存。但注意,存储小文件所需要的磁盘容量和数据块的大小无关。例如,一个 1MB 的文件设置为 128MB 的块存储,实际使用的是 1MB 的磁盘空间,而不是 128MB。

解决存储小文件办法之一

HDFS 存档文件或 HAR 文件,是一个更高效的文件存档工具, 它将文件存入 HDFS 块,在减少 NameNode 内存使用的同时,允许对文件进行透明的访问。具体说来, HDFS 存档文件对内还是一个一个独立文件,对 NameNode 而言却是一个整体,减少了 NameNode 的内存。

案例实操

(1)需要启动 YARN 进程

1
powershell复制代码[Tom@hadoop102 hadoop 3 1 3 ]$ start-yarn.sh

(2)归档文件

把 /input 目录里面的所有文件归档成一个叫 input.har 的归档文件,并把归档 后文件存储到 /output 路径下。

1
powershell复制代码[Tom@hadoop102 hadoop-3.1.3]$ hadoop archive -archiveName input.har -p /input /output

(3)查看文档

1
2
3
4
5
6
7
8
9
10
11
12
13
powershell复制代码[Tom@hadoop102 hadoop-3.1.3]$ hadoop fs -ls /output/input.har
Found 4 items
-rw-r--r-- 3 Tom supergroup 0 2021-06-26 17:26 /output/input.har/_SUCCESS
-rw-r--r-- 3 Tom supergroup 268 2021-06-26 17:26 /output/input.har/_index
-rw-r--r-- 3 Tom supergroup 23 2021-06-26 17:26 /output/input.har/_masterindex
-rw-r--r-- 3 Tom supergroup 74 2021-06-26 17:26 /output/input.har/part-0

[Tom@hadoop102 hadoop-3.1.3]$ hadoop fs -ls har:///output/input.har
2021-06-26 17:33:50,362 INFO sasl.SaslDataTransferClient: SASL encryption trust check: localHostTrusted = false, remoteHostTrusted = false
Found 3 items
-rw-r--r-- 3 Tom supergroup 38 2021-06-26 17:24 har:///output/input.har/shu.txt
-rw-r--r-- 3 Tom supergroup 19 2021-06-26 17:24 har:///output/input.har/wei.txt
-rw-r--r-- 3 Tom supergroup 17 2021-06-26 17:24 har:///output/input.har/wu.txt

(4)解归档文件

1
powershell复制代码[Tom@hadoop102 hadoop-3.1.3]$ hadoop fs -cp har:///output/input.har/* /

七、MapReduce生产经验

MapReduce 跑的慢的原因

(1)计算机性能:CPU、内存、磁盘、网络

(2)I/O 操作优化:数据倾斜;Map 运行时间太长,导致 Reduce 等待过久;小文件过多

MapReduce 常用调优参数

MapReduce 数据倾斜问题

数据频率倾斜——某一个区域的数据量要远远大于其他区域。

数据大小倾斜——部分记录的大小远远大于平均值。

减少数据倾斜的方法:

(1)首先检查是否空值过多造成的数据倾斜。生产环境,可以直接过滤掉空值;如果想保留空值,就自定义分区,将空值加随机数打散。最后再二次聚合 。

(2)能在 map 阶段提前处理,最好先在 Map 阶段处理。如: Combiner、 MapJoin

(3)设置多个 reduce 个数

八、Hadoop综合调优

8.1 Hadoop小文件优化方法

8.1.1 Hadoop小文件弊端

HDFS 上每个文件都要在 NameNode 上创建对应的元数据,这个元数据的大小约为 150byte,这样当小文件比较多的时候,就会产生很多的元数据文件 一方面会大量占用 NameNode 的内存空间 另一方面就是元数据文件过多,使得寻址索引速度变慢。

小文件过多,在进行 MR 计算时,会生成过多切片,需要启动过多的 MapTask。每个 MapTask 处理的数据量小, 导致 MapTask 的处理时间比启动时间还小,白白消耗资源。

8.1.2 Hadoop小文件解决方案

1)在数据采集的时候,就将小文件或小批数据合成大文件再上传 HDFS(数据源头)

2)Hadoop Archive(存储方向)

是一个高效的将小文件放入 HDFS 块中的文件存档工具,能够将多个小文件打包成一个 HAR 文件,从而达到减少 NameNode 的内存使用。

3)CombineTextInputFormat(计算方向)

CombineTextInputFormat 用于将多个小文件在切片过程中生成一个单独的切片或者少量的切片。

4)开启 uber 模式,实现 JVM 重用 (计算方向)

默认情况下,每个 Task 任务都需要启动一个 JVM 来运行,如果 Task 任务计算的数据量很小,我们可以让同一个 Job 的多个 Task 运行在一个 JVM 中,不必为每个 Task 都开启一个 JVM。

(1)未开启 uber 模式,在 /input 路径上上传多个小文件 并执行 wordcount 程序

1
powershell复制代码[Tom@hadoop102 hadoop-3.1.3]$ hadoop jar share/hadoop/mapreduce/hadoop-mapreduce-examples-3.1.3.jar wordcount /input /output2

(2)观察控制台

1
powershell复制代码2021-06-26 16:18:07,607 INFO mapreduce.Job: Job job_1613281510851_0002 running in uber mode : false

(3)观察 http://hadoop103:8088/cluster

(4)开启 uber 模式,在 mapred-site.xml 中添加如下配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
xml复制代码<!--开启uber模式,默认关闭-->
<property>
<name>mapreduce.job.ubertask.enable</name>
<value>true</value>
</property>
<!--uber模式中最大的mapTask数量,可向下修改-->
<property>
<name>mapreduce.job.ubertask.maxmaps</name>
<value>9</value>
</property>
<!--uber模式中最大的reduce数量,可向下修改-->
<property>
<name>mapreduce.job.ubertask.maxreduces</name>
<value>1</value>
</property>
<!--uber模式中最大的输入数据量,默认使用dfs.blocksize 的值,可向下修改-->
<property>
<name>mapreduce.job.ubertask.maxbytes</name>
<value></value>
</property>

(5)分发配置

1
powershell复制代码[Tom@hadoop102 hadoop]$ xsync mapred-site.xml

(6)再次执行 wordcount 程序

1
powershell复制代码[Tom@hadoop102 hadoop-3.1.3]$ hadoop jar share/hadoop/mapreduce/hadoop-mapreduce-examples-3.1.3.jar wordcount /input /output2

(7)观察控制台

1
powershell复制代码2021-06-27 16:28:36,198 INFO mapreduce.Job: Job job_1613281510851_0003 running in uber mode : true

(8)观察 http://hadoop103:8088/cluster

8.2 测试MapReduce计算性能

使用 Sort 程序评测 MapReduce

注:一个虚拟机不超过 150G 磁盘尽量不要执行这段代码

(1)使用 RandomWriter 来产生随机数,每个节点运行 10 个 Map 任务,每个 Map 产生大约 1G 大小的二进制随机数

1
powershell复制代码[Tom@hadoop102 mapreduce]$ hadoop jar /opt/module/hadoop-3.1.3/share/hadoop/mapreduce/hadoop-mapreduce-examples-3.1.3.jar randomwriter random-data

(2)执行 Sort 程序

1
powershell复制代码[Tom@hadoop102 mapreduce]$ hadoop jar /opt/module/hadoop-3.1.3/share/hadoop/mapreduce/hadoop-mapreduce-examples-3.1.3.jar sortrandom-data sorted-data

(3)验证数据是否真正排好序了

1
powershell复制代码[Tom@hadoop102 mapreduce]$ hadoop jar /opt/module/hadoop-3.1.3/share/hadoop/mapreduce/hadoop-mapreduce-client-jobclient-3.1.3-tests.jar testmapredsort -sortInput random-data -sortOutput sorted-data

8.3 企业开发场景案例

8.3.1 需求

(1)需求:从 1G 数据中,统计每个单词出现次数。服务器 3 台,每台配置 4G 内存,4 核 CPU,4 线程。

(2)需求分析:

1G/128m=8个MapTask;1个ReduceTask;1个mrAppMaster,平均每个节点运行 10个/3台≈3个任务(4 3 3)

8.3.2 HDFS参数调优

(1)修改 hadoop-env.sh

1
2
xml复制代码export HDFS_NAMENODE_OPTS="-Dhadoop.security.logger=INFO,RFAS-Xmx1024m"
export HDFS_DATANODE_OPTS="-Dhadoop.security.logger=ERROR,RFAS-Xmx1024m"

(2)修改 hdfs-site.xml

1
2
3
4
5
xml复制代码<!--NameNode有一个工作线程池,默认值是10-->
<property>
<name>dfs.namenode.handler.count</name>
<value>21</value>
</property>

(3)修改 core-site.xml

1
2
3
4
5
xml复制代码<!--配置垃圾回收时间为60分钟-->
<property>
<name>fs.trash.interval</name>
<value>60</value>
</property>

(4)分发配置

1
powershell复制代码[Tom@hadoop102 hadoop]$ xsync hadoop-env.sh hdfs-site.xml core-site.xml

8.3.3 MapReduce参数调优

(1)修改 mapred-site.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
xml复制代码<!--环形缓冲区大小,默认100m-->
<property>
<name>mapreduce.task.io.sort.mb</name>
<value>100</value>
</property>

<!--环形缓冲区溢写阈值,默认0.8-->
<property>
<name>mapreduce.map.sort.spill.percent</name>
<value>0.80</value>
</property>

<!--merge合并次数,默认10个-->
<property>
<name>mapreduce.task.io.sort.factor</name>
<value>10</value>
</property>

<!--maptask内存,默认1g;maptask堆内存大小默认和该值大小一致mapreduce.map.java.opts-->
<property>
<name>mapreduce.map.memory.mb</name>
<value>-1</value>
<description>The amount of memory to request from the scheduler for each map task. If this is not specified or is non-positive, it is inferred frommapreduce.map.java.opts and mapreduce.job.heap.memory-mb.ratio. If java-opts are also not specified, we set it to 1024.
</description>
</property>

<!--matask的CPU核数,默认1个-->
<property>
<name>mapreduce.map.cpu.vcores</name>
<value>1</value>
</property>

<!--matask异常重试次数,默认4次-->
<property>
<name>mapreduce.map.maxattempts</name>
<value>4</value>
</property>

<!--每个Reduce去Map中拉取数据的并行数。默认值是5-->
<property>
<name>mapreduce.reduce.shuffle.parallelcopies</name>
<value>5</value>
</property>

<!--Buffer大小占Reduce可用内存的比例,默认值0.7-->
<property>
<name>mapreduce.reduce.shuffle.input.buffer.percent</name>
<value>0.70</value>
</property>

<!--Buffer中的数据达到多少比例开始写入磁盘,默认值0.66。-->
<property>
<name>mapreduce.reduce.shuffle.merge.percent</name>
<value>0.66</value>
</property>

<!--reducetask内存,默认1g;reducetask堆内存大小默认和该值大小一致mapreduce.reduce.java.opts -->
<property>
<name>mapreduce.reduce.memory.mb</name>
<value>-1</value>
<description>The amount of memory to request from the scheduler for each reduce task. If this is not specified or is non-positive, it is inferred
from mapreduce.reduce.java.opts and mapreduce.job.heap.memory-mb.ratio.
If java-opts are also not specified, we set it to 1024.
</description>
</property>

<!--reducetask的CPU核数,默认1个-->
<property>
<name>mapreduce.reduce.cpu.vcores</name>
<value>2</value>
</property>

<!--reducetask失败重试次数,默认4次-->
<property>
<name>mapreduce.reduce.maxattempts</name>
<value>4</value>
</property>

<!--当MapTask完成的比例达到该值后才会为ReduceTask申请资源。默认是0.05-->
<property>
<name>mapreduce.job.reduce.slowstart.completedmaps</name>
<value>0.05</value>
</property>

<!--如果程序在规定的默认10分钟内没有读到数据,将强制超时退出-->
<property>
<name>mapreduce.task.timeout</name>
<value>600000</value>
</property>

(2)分发配置

1
powershell复制代码[Tom@hadoop102 hadoop]$ xsync mapred-site.xml

8.3.4 Yarn参数调优

(1)修改 yarn-site.xml 配置参数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
xml复制代码<!--选择调度器,默认容量-->
<property>
<description>The class to use as the resource scheduler.</description>
<name>yarn.resourcemanager.scheduler.class</name>
<value>org.apache.hadoop.yarn.server.resourcemanager.scheduler.capacity.CapacityScheduler</value>
</property>

<!--ResourceManager处理调度器请求的线程数量,默认50;如果提交的任务数大于50,可以增加该值,但是不能超过3台* 4线程=12线程(去除其他应用程序实际不能超过8)-->
<property>
<description>Number of threads to handle scheduler interface.</description>
<name>yarn.resourcemanager.scheduler.client.thread-count</name>
<value>8</value>
</property>

<!--是否让yarn自动检测硬件进行配置,默认是false,如果该节点有很多其他应用程序,建议手动配置。如果该节点没有其他应用程序,可以采用自动-->
<property>
<description>Enable auto-detection of node capabilities such as memory and CPU.</description>
<name>yarn.nodemanager.resource.detect-hardware-capabilities</name>
<value>false</value>
</property>

<!--是否将虚拟核数当作CPU核数,默认是false,采用物理CPU核数-->
<property>
<description>Flag to determine if logical processors(such as hyperthreads) should be counted as cores. Only applicable on Linux when yarn.nodemanager.resource.cpu-vcores is set to -1 and yarn.nodemanager.resource.detect-hardware-capabilities is true.</description>
<name>yarn.nodemanager.resource.count-logical-processors-as-cores</name>
<value>false</value>
</property>

<!--虚拟核数和物理核数乘数,默认是1.0-->
<property>
<description>Multiplier to determine how to convert phyiscal cores to vcores. This value is used if yarn.nodemanager.resource.cpu-vcores is set to -1(which implies auto-calculate vcores) and yarn.nodemanager.resource.detect-hardware-capabilities is set to true. Thenumber of vcores will be calculated asnumber of CPUs * multiplier.</description>
<name>yarn.nodemanager.resource.pcores-vcores-multiplier</name>
<value>1.0</value>
</property>

<!--NodeManager使用内存数,默认8G,修改为4G内存-->
<property>
<description>Amount of physical memory, in MB, that can be allocated for containers. If set to -1 and yarn.nodemanager.resource.detect-hardware-capabilities is true, it is automatically calculated(in case of Windows and Linux).In other cases, the default is 8192MB.</description>
<name>yarn.nodemanager.resource.memory-mb</name>
<value>4096</value>
</property>

<!--nodemanager的CPU核数,不按照硬件环境自动设定时默认是8个,修改为4个-->
<property>
<description>Number of vcores that can be allocated
for containers. This is used by the RM scheduler when allocating resources for containers. This is not used to limit the number of CPUs used by YARN containers. If it is set to -1 and yarn.nodemanager.resource.detect-hardware-capabilities is true, it is automatically determined from the hardware in case of Windows and Linux.In other cases, number of vcores is 8 by default.</description>
<name>yarn.nodemanager.resource.cpu-vcores</name>
<value>4</value>
</property>

<!--容器最小内存,默认1G -->
<property>
<description>The minimum allocation for every container request at the RMin MBs. Memory requests lower than this will be set to the value of thisproperty. Additionally, a node manager that is configured to have less memorythan this value will be shut down by the resource manager.</description>
<name>yarn.scheduler.minimum-allocation-mb</name>
<value>1024</value>
</property>

<!--容器最大内存,默认8G,修改为2G -->
<property>
<description>The maximum allocation for every container request at the RMin MBs. Memory requests higher than this will throw anInvalidResourceRequestException.</description>
<name>yarn.scheduler.maximum-allocation-mb</name>
<value>2048</value>
</property>

<!--容器最小CPU核数,默认1个-->
<property>
<description>The minimum allocation for every container request at the RMin terms of virtual CPU cores. Requests lower than this will be set to thevalue of this property. Additionally, a node manager that is configured tohave fewer virtual cores than this value will be shut down by the resourcemanager.</description>
<name>yarn.scheduler.minimum-allocation-vcores</name>
<value>1</value>
</property>

<!--容器最大CPU核数,默认4个,修改为2个-->
<property>
<description>The maximum allocation for every container request at the RMin terms of virtual CPU cores. Requests higher than this will throw an InvalidResourceRequestException.</description>
<name>yarn.scheduler.maximum-allocation-vcores</name>
<value>2</value>
</property>

<!--虚拟内存检查,默认打开,修改为关闭-->
<property>
<description>Whether virtual memory limits will be enforced for containers.</description>
<name>yarn.nodemanager.vmem-check-enabled</name>
<value>false</value>
</property>

<!--虚拟内存和物理内存设置比例,默认2.1 -->
<property>
<description>Ratio between virtual memory to physical memory whensetting memory limits for containers. Container allocations areexpressed in terms of physical memory, and virtual memory usageis allowed to exceed this allocation by this ratio.</description>
<name>yarn.nodemanager.vmem-pmem-ratio</name>
<value>2.1</value>
</property>

(2)分发配置

1
powershell复制代码[Tom@hadoop102 hadoop]$ xsync yarn-site.xml

8.3.5 执行程序

(1)重启集群

1
2
powershell复制代码[Tom@hadoop102 hadoop-3.1.3]$ sbin/stop-yarn.sh
[Tom@hadoop103 hadoop-3.1.3]$ sbin/start-yarn.sh

(2)执行 WordCount 程序

1
2
3
powershell复制代码[Tom@hadoop102 hadoop 3.1.3]$ hadoop jar
share/hadoop/ mapreduce/hadoop mapreduce examples 3.1.3.jar
wordcount /input /output

(3)观察 Yarn 任务执行页面 http://hadoop103:8088/cluster/apps

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Maven 项目打包

发表于 2021-11-27

这是我参与11月更文挑战的第19天,活动详情查看:2021最后一次更文挑战

对于企业级项目,无论是本地测试,还是测试环境测试以及最终的项目上线,都会涉及项目的打包操作,对于每个环境下项目打包时,对应的项⽬所有要的配置资源就会有所区别,实现打包的方式有 很多种,可以通过ant,获取通过idea 自带的打包功能实现项目打包,但当项⽬很⼤并且需要的外界配置很多时,此时打包的配置就会异常复杂,对于maven 项目,我们可以通过pom.xml 配置的⽅式来实现打包时的环境选择,相⽐较其他形式打包工具,通过maven 只需要通过简单的配置,就可以轻松完成不同环境的项目的整体打包。

1、配置打包环境

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
xml复制代码<!--打包环境配置开发环境,测试环境,正式环境-->
<profiles>
<profile>
<id>dev</id>
<properties>
<env>dev</env>
</properties>
<!--未指定环境时,默认打包dev环境-->
<activation>
<activeByDefault>true</activeByDefault>
</activation>
</profile>

<profile>
<id>test</id>
<properties>
<env>test</env>
</properties>
</profile>

<profile>
<id>product</id>
<properties>
<env>product</env>
</properties>
</profile>
</profiles>

2、设置资源文件配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
xml复制代码<!--静态资源导出-->
<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
</build>

3、执行打包操作

1、打开Run/Debug Configuarations窗⼝,输⼊对应的打包命令

image-20210618084556971

image-20210618084120921

打包命令:-P(指定环境)

  1. 1
    xml复制代码clean compile package -Dmaven.test.skip=true

打包默认环境(开发环境)并且跳过maven测试操作
2.

1
xml复制代码clean compile package -Ptest -Dmaven.test.skip=true

打包测试环境(test环境)并且跳过maven测试操作
3.

1
xml复制代码clean compile package -Pproduct -Dmaven.test.skip=true

打包生产环境(product环境)并且跳过maven测试操作


2、打包成功,附带路径

image-20210617170533028


本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Maven项目创建

发表于 2021-11-27

这是我参与11月更文挑战的第19天,活动详情查看:2021最后一次更文挑战

JAVA项目

① 创建项目

  1. 选择 【File】→【NEW】→【Project】

image-20210617154533315
2. 选择【Maven】,设置JDK版本,选择maven项⽬的模板

image-20210617160500231
3. 设置项⽬的 GroupId 和 ArtifactId

image-20210617153549401
4. 检查Maven环境,选择 【Finish】

image-20210617154742175
5. 等待项⽬创建,下载资源

image-20210617162959476

image-20210617154105272
6. 创建完成后⽬录结构如下

image-20210617154503644


② 编译项目

  1. 点击右上⻆的 【Add Configurations 】打开 【Run/Debug Configurations】

image-20210617154949614
2. 选择【Maven】

image-20210617162522887
3. 设置编译项⽬的命令

image-20210617162803891
4. 执⾏编译命令

image-20210617162452587
5. 编译成功

image-20210617161539248


JavaWeb项目

① 创建项目

  1. 创建Web项⽬与创建Java项⽬步骤基本⼀致,区别在于选择 Maven模板(web项⽬选择 webapp),如图:

image-20210617161645896

注:其他步骤与创建普通的Java项⽬相同。
2. 设置项⽬的 GroupId 和 ArtifactId

image-20210617163013667
3. 检查Maven环境,选择 【Finish】

image-20210617161728084
4. 等待项⽬创建,下载资源

image-20210617161925265
5. 项目目录结构如下:

image-20210617161835395


② 启动项目

  1. 修改 JDK 的版本
1
2
3
4
5
6
xml复制代码  <!-- JDK的版本修改为1.8-->
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
  1. 设置单元测试的版本
1
2
3
4
5
6
7
8
9
xml复制代码  <!-- junit的版本修改为4.12 -->
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
  1. 删除pluginManagement标签
1
2
3
4
xml复制代码<!-- 将这个标签及标签中的内容全部删除 -->
<pluginManagement>
...
</pluginManagement>
  1. 点击右上⻆的 【Add Configurations 】打开 【Run/Debug Configurations】

image-20210617154949614
5. 点击左上角【+】号,选择【Tomcat Server】下的【Local】

image-20210617170304385
6. 配置相关信息

image-20210617170502874
7. 点击【Deployment】→左上角【+】号→选择【Artifact】

image-20210617162452587
8. 选择【***:war】

20210707182146214
9. 点击【APPLY】→【OK】
10. 点击运行

image-20210617171225150
11. 等待自动弹出页面,启动成功

image-20210617171306421=

构建多模块项目

使用maven 提供的多模块构建的特性完成 maven 环境下多个模块的项目的管理与构建。

子模块继承父模块

子模块的 pom.xml 中添加对 父模块的继承

1
2
3
4
5
xml复制代码<parent>
<groupId>父 pom 中的groupId</groupId>
<artifactId>父 pom 中的artifactId</artifactId>
<version>父 pom 中的版本号</version>
</parent>

模块之间的依赖

例如:maven_service 模块对 maven_dao 模块添加依赖

在 maven_service 模块的 pom.xml 文件中添加对 maven_dao 模块的依赖

1
2
3
4
5
6
7
8
9
10
java复制代码<dependencies>

<!--引入 maven_dao模块的依赖-->
<dependency>
<groupId>maven_dao模块 pom 所在项目的 groupId</groupId>
<artifactId>maven_dao模块 pom 所在项目的中的artifactId</artifactId>
<version>maven_dao模块 pom 所在项目的中的版本号</version>
</dependency>

</dependencies>

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

垃圾收集概述和垃圾收集算法(超详细介绍) 垃圾收集概述和垃圾

发表于 2021-11-27

「这是我参与11月更文挑战的第8天,活动详情查看:2021最后一次更文挑战」

垃圾收集概述和垃圾收集算法(超详细介绍)

垃圾收集(Garbage Collection,下文简称GC)

为什么我们还要去了解垃圾收集和内存分配

当需要排查各种内存溢出、内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,我们就必须对这些“自动化”的技术实施必要的监控和调节。

哪些内存需要回收

不需要回收的

程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭,栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。

每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的(尽管在运行期会由即时编译器进行一些优化,但在基于概念模型的讨论里,大体上可以认为是编译期可知的),因此这几个区域的内存分配和回收都*具备确定性*, 在这几个区域内就不需要过多考虑如何回收的问题,当方法结束或者线程结束时,内存自然就跟随着回收了。**

需要回收的

Java堆和方法区这两个区域则有着很显著的不确定性:一个接口的多个实现类需要的内存可能会不一样,一个方法所执行的不同条件分支所需要的内存也可能不一样,只有处于运行期间,我们才能知道程序究竟会创建哪些对象,创建多少个对象,这部分内存的分配和回收是动态的。垃圾收集器所关注的正是这部分内存该如何管理,我们讨论的“内存”分配与回收也仅仅特指这一部分内存。

方法区的回收

方法区垃圾收集的“性价比”通常也是比较低的:在Java堆中,尤其是在新生代中,对常规应用进行一次垃圾收集通常 可以回收70%至99%的内存空间,相比之下,方法区回收囿于苛刻的判定条件,其区域垃圾收集的回收成果往往远低于此。
方法区的垃圾收集主要回收两部分内容:废弃的常量和不再使用的类型。

回收废弃常量

回收废弃常量与回收Java堆中的对象非常类似。举个常量池中字面量回收的例子,假如一个字符串“ java”曾经进入常量池 中,但是当前系统又没有任何一个字符串对象的值是“ java”,换句话说,已经没有任何字符串对象引用常量池中的“ java”常量,且虚拟机中也没有其他地方引用这个字面量。如果在这时发生内存回收,而且垃圾收集器判断确有必要的话,这个“ java”常量就将会被系统清理出常量池。常量池中其他类(接 口)、方法、字段的符号引用也与此类似。

回收“不再被使用的类”

要判定一个类型是否属于“不再被使用的类”的条件就比较苛刻了。需要同时满足下面三个条件:

  1. 该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。
  2. 加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如OSGi、JSP的重加载等,否则通常是很难达成的。
  3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

什么时候回收

回收死去的对象

在堆里面存放着Java世界中几乎所有的对象实例,垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象之中哪些还“存活”着,哪些已经“死去”(“死去”即不可能再被任何途径使用的对象)了。

如何判断对象死了

引用计数算法

在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;

当引用失效时,计数器值就减一;

任何时刻计数器为零的对象就是不可能再被使用的。

存在的问题

在Java领域,至少主流的Java虚拟机里面都没有选用引用计数算法来管理内存,主要原因是,这个看似简单的算法有很多例外情况要考虑,必须要配合大量额外处理才能保证正确地工作,譬如单纯的引用计数 就很难解决对象之间相互循环引用的问题。

例如:

  1. 虚拟机栈的一个栈帧引用了Java堆的对象,总引用+1
  2. 对象objA和objB又互相引用,总引用又+1

image.png

  1. 但是随着栈帧出栈,引用-1,但是对象之间的循环引用并没有结束

image.png

CodeDemo:

对象objA和objB都有字段instance,赋值令 objA.inst ance=objB及objB.inst ance=objA,除此之外,这两个对象再无任何引用,实际上这两个对象已 经不可能再被访问,但是它们因为互相引用着对方,导致它们的引用计数都不为零,引用计数算法也 就无法回收它们。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ini复制代码    static class ReferenceCountingGC {
public Object instance = null;
private static final int _1MB = 1024 * 1024;
/**
* 这个成员属性的唯一意义就是占点内存,以便能在GC日志中看清楚是否有回收过 */
private byte[] bigSize = new byte[2 * _1MB];
public static void testGC() {
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB;
objB.instance = objA;
objA = null; objB = null;
// 假设在这行发生GC,objA和objB是否能被回收?
System.gc(); }
}

运行上述程序后从运行结果中可以清楚看到内存回收日志中包含“4603K->210K”,意味着虚拟机并没有因为这两个对象互相引用就放弃回收它们,这也从侧面说明了Java虚拟机并不是通过引用计数算法来判断对象是否存活的。

可达性分析算法

可达性分析(Reachability Analysis)算法
这个算法的基本思路就是通过 一系列称为 “GC Roots”的根对象 作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连, 或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。

由于可达性算法可以处理循环引用的问题,解决了引用计数算法带来的缺陷。目前虚拟机基本都是采用可达性算法。

如图3-1所示,对象object 5、object 6、object 7虽然互有关联,但是它们到GC Roots是不可达的, 因此它们将会被判定为可回收的对象。

image.png

可作为GC Roots的对象的种类

在Java技术体系里面,固定可作为GC Roots的对象包括以下几种:

  1. 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
  2. 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
  3. 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
  4. 在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
  5. Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如
  6. NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
  7. 所有被同步锁(synchronized关键字)持有的对象。
  8. 反映Java虚拟机内部情况的JM XBean、JVM TI中注册的回调、本地代码缓存等。
    除了这些固定的GC Roots集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入,共同构成完整GC Roots集合。譬如分代收集和局部回收(Partial GC),如果只针对Java堆中某一块区域发起垃圾收集时(如最典型的只针对新生 代的垃圾收集),必须考虑到内存区域是虚拟机自己的实现细节(在用户视角里任何内存区域都是不可见的),更不是孤立封闭的,所以某个区域里的对象完全有可能被 位于堆中其他区域的对象所引用,这时候就需要将这些关联区域的对象也一并加入GC Roots集合中去,才能保证可达性分析的正确性。

目前最新的几款垃圾收集器[1]无一例外都具备了局部回收的特征,为了避免GC Roots包含过多对象而过度膨胀,它们在实现上也做出了各种优化处理。关于这些概念、优化技巧以及各种不同收集器 实现等内容,都将在本文后续内容中一一介绍。

引用

无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象是否引用链可达,判定对象是否存活都和“引用”离不开关系

引用的传统定义

Java里面的引用是很传统的定义: 如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称该reference数据是代表某块内存、某个对象的引用。

缺陷

一个对象在这种定义下只有“被引用”或者“未被引用”两种状态,对于描述一些“食之无味,弃之可惜”的对象就显得无能为力。

改进

在JDK 1.2版之后,Java对引用的概念进行了扩充,将引用分为强引用(Strongly Re-ference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)4种,这4种引用强度依次逐渐减弱。## 如何回收

强引用

强引用是最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值,即类似“Object obj=new Object()”这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回 收掉被引用的对象。

软引用

软引用是用来描述一些还有用,但非必须的对象。

只被软引用关联着的对象,在系统将要发生内 存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存, 才会抛出内存溢出异常。
在JDK 1.2版之后提供了SoftReference类来实现软引用。

弱引用

弱引用也是用来描述那些非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。

在JDK 1.2版之后提供了WeakReference类来实现弱引用。

虚引用

它是最弱的一种引用关系。一个对象是否有虚引用的 存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚 引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。

Phant omReference类来实现虚引用。

垃圾收集算法

从如何判定对象消亡的角度出发,垃圾收集算法可以划分为 “引用计数式垃圾收集”(Reference Counting GC) 和 “追踪式垃圾收集”(Tracing GC) 两大类,这两类也常被称作“直接垃圾收集”和“间接垃圾收集”。其中追踪式垃圾收集就是上文所说的可达性分析。

由于引用计数式垃圾收集算法在主流Java虚拟机中均未涉及(因为其缺陷),所以后续所有算法均属于追踪式垃圾收集的范畴。

如何理解追踪式垃圾是间接收集

追踪式垃圾回收算法都是采用的间接式的回收策略,也就是这种策略并非直接寻找垃圾本身,而是先寻找哪些对象存活,然后反过来判断其余所有的对象为垃圾对象。

追踪式收集和后文中的分代理论,标记-清处算法等联系

image.png

分代收集理论

为什么要分代

代收集名为理论,实质是一套符合大多数程序运行实际情况的经验法则,它建立在两个分代假说之上:

  1. 弱分代假说
    弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的。
  2. 强分代假说
    强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡。

收集器应该将Java堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储。

显而易见,如果一个区域中大多数对象都是朝生夕灭,难以熬过垃圾收集过程的话,那么把它们集中放在一起,每次回收时只关注如何保留少量存活而不是去标记那些大量将要被回收的对象,就能以较低代价回收到大量的空间;

如果剩下的都是难以消亡的对象,那把它们集中放在一块,虚拟机便可以使用较低的频率来回收这个区域。

这就同时兼顾了垃圾收集的时间开销和内存的空间有效利用。

在Java堆划分出不同的区域之后,垃圾收集器才可以每次只回收其中某一个或者某些部分的区域 ——因而才有了“Minor GC”“M ajor GC”“Full GC”这样的回收类型的划分;也才能够针对不同的区域安排与里面存储对象存亡特征相匹配的垃圾收集算法——因而发展出了“标记-复制算法”“标记-清除算 法”“标记-整理算法”等针对性的垃圾收集算法。

分成哪几代

把分代收集理论具体放到现在的商用Java虚拟机里,设计者一般至少会把Java堆划分为新生代(Young Generation)和老年代(Old Generation)两个区域。

在新生代中,每次垃圾收集 时都发现有大批对象死去,而每次回收后存活的少量对象,将会逐步晋升到老年代中存放。

但是仅仅这样考虑也有一个问题:

假如要现在进行一次只局限于新生代区域内的收集(Minor GC),但新生代中的对象是完全有可 能被老年代所引用的,为了找出该区域中的存活对象,不得不在固定的GC Roots之外,再额外遍历整
个老年代中所有对象来确保可达性分析结果的正确性。遍历整个老年代所有对象的方案虽然理论上可行,但无疑会为内存回收带来很大的性能负担。为了解决这个问题,就需要对分代收集理论添加第三条经验法则:

  1. 跨代引用假说(Intergenerational Reference Hypothesis):跨代引用相对于同代引用来说仅占极少数。
GC名称辨析
  • 新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集。 (Minor)美[ˈmaɪnər]少数的; 轻微的;
  • 老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有CMS收集器会有单独收集老年代的行为。另外请注意“Major GC”这个说法现在有点混淆,在不同资料上常有不同所指,需按上下文区分到底是指老年代的收集还是整堆收集。
  • 混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收 集器会有这种行为。
  • 整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。

如何处理跨代引用问题

跨代引用假说其实是可根据前两条假说逻辑推理得出的隐含推论:存在互相引用关系的两个对象,是应该倾向于同时生存或者同时消亡的。

举个例子,如果某个新生代对象存在跨代引用,由于老年代对象难以
消亡,该引用会使得新生代对象在收集时同样得以存活,进而在年龄增长之后晋升到老年代中,这时跨代引用也随即被消除了。

依据这条假说,我们就不应再为了少量的跨代引用去扫描整个老年代,也不必浪费空间专门记录每一个对象是否存在及存在哪些跨代引用,只需在新生代上建立一个全局的数据结构(该结构被称 为“记忆集”,Remembered Set),这个结构把老年代划分成若干小块,标识出老年代的哪一块内存会存在跨代引用。 此后当发生Minor GC时,只有包含了跨代引用的小块内存里的对象才会被加入到GC Roots进行扫描。虽然这种方法需要在对象改变引用关系(如将自己或者某个属性赋值)时维护记录数据的正确性,会增加一些运行时的开销,但比起收集时扫描整个老年代来说仍然是划算的。

标记-清除算法

是最基础的收集算法,后续的收集算法大多都是以标记-清除算法为基础,对其 缺点进行改进而得到的。

算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。标记过程就是对象是否属于垃圾的判定过程。

image.png

缺点:

  1. 第一个是执行效率不稳定,如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随对象数量增长而降低;
  2. 第二个是内存空间的碎片化问题,标记、清除之后会产生大 量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找 到足够的连续内存而不得不提前触发另一次垃圾收集动作。

标记-复制算法

标记-复制算法常被简称为复制算法。

概述

为了解决标记-清除算法面对大量可回收对象时执行效率低的问题,1969年Fenichel提出了一种称为“半区复制”(Semispace Copying)的垃圾收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

优点

如果内存中多数对象都是存活的,这种算法将会产生大量的内存间复制的开销,但对于多数对象都是可回收的情况,算法需要复制的就是占少数的存活对象,而且每次都是针对整个半区进行内存回收,分配内存时也就不用考虑有空间碎片的复杂情况,只要移动堆顶指针,按顺序分配即可。这样实现简单,运行高效

缺点

  1. 其缺陷也显而易见,这种复制回收算法的代价是将可用内存缩小为了原来的一半,空间浪费未免太多了一点。标记-复制算法的执行过程如图3-3所示。
  2. 在对象存活率较高时就要进行较多的复制操作,效率将会降低。

image.png

后续改进

现在的商用Java虚拟机大多都优先采用了这种收集算法去回收新生代,IBM公司曾有一项专门研究对新生代“朝生夕灭”的特点做了更量化的诠释——新生代中的对象有98%熬不过第一轮收集。因此并不需要按照1∶1的比例来划分新生代的内存空间。

新的比例-Appel式回收

“Appel式回收”:HotSpot虚拟机的Serial、ParNew等新生代收集器均采用了这种策略来设计新生代的内存布局。Appel式回收的具体做法是把新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次分配内存只使用Eden和其中一块Survivor。发生垃圾搜集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间。

HotSpot虚拟机默认Eden和Survivor的大小比例是8∶1,也即每次新生代中可用内存空间为整个新生代容量的90%(Eden的80%加上一个Survivor的10%),只有一个Survivor空间,即10%的新生代是会被“浪费”的。

Appel式回收还有一个充当罕见情况的“逃生门”的安全设计,当Survivor空间不足以容纳一次Minor GC之后存活的对象时,就需要依赖其他内存区域(实际上大多就是老年代)进行分配担保(Handle Promotion)。

标记-整理算法

概述

标记-复制算法在对象存活率较高时就要进行较多的复制操作,效率将会降低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存 活的极端情况,所以在老年代一般不能直接选用这种算法。

针对老年代对象的存亡特征,提出了另外一种有针对性的 “标记-整理”(Mark-Compact)算法,其中的标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可 回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存,“标记-整理”算法的示意图如图3-4所示。

image.png

与标记-清除算法区别

标记-清除算法与标记-整理算法的本质差异在于前者是一种非移动式的回收算法,而后者是移动式的。是否移动回收后的存活对象是一项优缺点并存的风险决策:

移动的缺点

移动则内存回收时会更复杂

如果移动存活对象,尤其是在老年代这种每次回收都有大量对象存活区域,移动存活对象并更新所有引用这些对象的地方将会是一种极为负重的操作,而且这种对象移动操作必须全程暂停用户应用 程序才能进行[1],这就更加让使用者不得不小心翼翼地权衡其弊端了,像这样的停顿被最初的虚拟机 设计者形象地描述为“Stop The World”[2]。

不移动的缺点

不移动则内存分配时会 更复杂

跟标记-清除算法那样完全不考虑移动和整理存活对象的话,弥散于堆中的存活对象导致的 空间碎片化问题就只能依赖更为复杂的内存分配器和内存访问器来解决。譬如通过“分区空闲分配链 表”来解决内存分配问题(计算机硬盘存储大文件就不要求物理连续的磁盘空间,能够在碎片化的硬盘
上存储和访问就是通过硬盘分区表实现的)。内存的访问是用户程序最频繁的操作,甚至都没有之
一,假如在这个环节上增加了额外的负担,势必会直接影响应用程序的吞吐量。

如何权衡

从垃圾收集的停顿时间来看,不移动对象停顿时间会更短,甚至可以不需要停顿,但是从整个程序的吞吐量来看,移动对象会更划算。

即使不移动对象会使得收集器的效率提升一些,但因内存分配和访问相比垃圾收集频率要 高得多,这部分的耗时增加,总吞吐量仍然是下降的。HotSpot虚拟机里面关注吞吐量的Parallel Scavenge收集器是基于标记-整理算法的,而关注延迟的CM S收集器则是基于标记-清除算法的,这也从 侧面印证这点。

一种“和稀泥式”解决方案

还有一种“和稀泥式”解决方案可以不在内存分配和访问上增加太大额外负担。

做法是让虚拟机平时多数时间都采用标记-清除算法,暂时容忍内存碎片的存在,直到内存空间的碎片化程度已经大到影响对象分配时,再采用标记-整理算法收集一次,以获得规整的内存空间。前面提到的基于标 记-清除算法的CM S收集器面临空间碎片过多时采用的就是这种处理办法。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Redis的这些拓展方案,用过一条的就是大牛

发表于 2021-11-27

| 前言

Redis大家都不陌生,就算是没用过,也都听说过了。

作为最广泛使用的KV内存数据库之一,在当今的大流量时代,单机模式略显单薄,免不了要有一些拓展的方案。

笔者下文会对各种方案进行介绍,并且给出场景,实现 等等概述,还会提到一些新手常见的误区。

| 正文

先从基础的拓展方式开始,这样更便于理解较高级的模式。

ps: 本文背景是以笔者落笔时官网最新稳定版5.0.8为准,虽然还没写完就变成了6.0.1。

分区

概述

分区(Partitioning)是一种最为简单的拓展方式。

在我们面临单机的存储空间瓶颈时,第一点就能想到像传统的关系型数据库一样,进行数据分区。

或者假设手中有N台机器可以作为Redis服务器,所有机器内存总和有256G, 而客户端正好也需要一个大内存的存储空间。

我们除了可以把内存条都拆下来焊到一个机器上,也可以选择分区使用,这样又拓展了计算能力。

单指分区来讲,即将全部数据分散在多个Redis实例中,每个实例不需要关联,可以是完全独立的。

)

使用方式

客户端处理

和传统的数据库分库分表一样,可以从key入手,先进行计算,找到对应数据存储的实例在进行操作。范围角度,比如orderId:1orderId:1000放入实例1,orderId:1001orderId:2000放入实例2。

哈希计算,就像我们的hashmap一样,用hash函数加上位运算或者取模,高级玩法还有一致性Hash等操作,找到对应的实例进行操作

使用代理中间件

我们可以开发独立的代理中间件,屏蔽掉处理数据分片的逻辑,独立运行。

当然也有他人已经造好的轮子,Redis也有优秀的代理中间件,譬如Twemproxy,或者codis,可以结合场景选择是否使用。

缺点

无缘多key操作,key都不一定在一个实例上,那么多key操作或者多key事务自然是不支持。

维护成本,由于每个实例在物理和逻辑上,都属于单独的一个节点,缺乏统一管理。

灵活性有限,范围分片还好,比如hash+MOD这种方式,如果想动态调整Redis实例的数量,就要考虑大量数据迁移,这就非常麻烦了。

同为开发者,深知我们虽然总能“曲线救国”的完成一些当前环境不支持的功能,但是总归要麻烦一些。

主从

概述数据迁移

常说的主从(Master-Slave),也就是复制(Replication)方式,怎么称呼都可以。

同上面的分区一样,也是Redis高可用架构的基础,新手可能会误以为这类基础模式即是“高可用”,这并不是十分正确的。

分区暂时能解决单点无法容纳的数据量问题,但是一个Key还是只在一个实例上,在大流量时代显得不那么可靠。

主从就是另一个纬度的拓展,节点将数据同步到从节点,就像将实例“分身”了一样,可靠性又提高了不少。

)

图画的有些夸张了,主要还是想体现结构灵活,是一主一从,还是一主多从,还是一主多从多从… 看你心情

有了“实例分身”,自然就可以做读写分离,将读流量均摊在各个从节点。

使用方式

)

高手云集的时代,聊天软件难免要备上这么一张表情包。

这表情包和使用方式有什么关系呢?首先看看使用方式:

作为主节点的Redis实例,并不要求配置任何参数,只需要正常启动

作为从节点的实例,使用配置文件或命令方式REPLICAOF 主节点Host 主节点port即可完成主从配置

是不是和表情包一样,“dalao”没动,我去“抱大腿”。

这样一个主从最小配置就完成了,主从实例即可对外提供服务。

命令里的“主节点”是相对的,slave也可以抱slave大腿,也就是上文提到的结构灵活。

缺点

slave节点都是只读的,如果写流量大的场景,就有些力不从心了。

那我把slave节点只读关掉不就行了?当然不行,数据复制是由主到从,从节点独有数据同步不到主节点,数据就不一致了。

故障转移不友好,主节点挂掉后,写处理就无处安放,需要手工的设定新的主节点,如使用REPLICAOF no one(谁大腿我都不抱了) 晋升为主节点,再梳理其他slave节点的新主配置,相对来说比较麻烦。

哨兵

概述

主从的手工故障转移,肯定让人很难接受,自然就出现了高可用方案-哨兵(Sentinel)。

我们可以在主从架构不变的场景,直接加入Redis Sentinel,对节点进行监控,来完成自动的故障发现与转移。

并且还能够充当配置提供者,提供主节点的信息,就算发生了故障转移,也能提供正确的地址。

哨兵本身也是Redis实例的一种,但不作为数据存储方使用,启动命令也是不一样的。

)

虽然图有些复杂,看起来像要召唤光能使者。

)

其实实际使用起来是很便捷的。

使用方式

Sentinel的最小配置,一行即可:

1sentinel monitor <主节点别名> <主节点host> <主节点端口> <票数>

只需要配置master即可,然后用redis-sentinel <配置文件> 命令即可启用。

Redis官网提到的“最小配置”是如下所示,除了上面提到的一行,还有其它的一些配置:

1
2
3
4
5
6
7
8
9
yaml复制代码1sentinel monitor mymaster 127.0.0.1 6379 2
2sentinel down-after-milliseconds mymaster 60000
3sentinel failover-timeout mymaster 180000
4sentinel parallel-syncs mymaster 1
5
6sentinel monitor resque 192.168.1.3 6380 4
7sentinel down-after-milliseconds resque 10000
8sentinel failover-timeout resque 180000
9sentinel parallel-syncs resque 5

这是因为官网加了一个修饰词,是“典型的最小配置”,把重要参数和多主的例子都写出来了,照顾大家CV大法的时候,不要忘记重要参数,其实都是有默认值的。

正如该例所示,设置主节点别名就是为了监控多主的时候,与其额外配置项能够与其对应, 以及sentinel一些命令,如SENTINEL get-master-addr-by-name就要用到别名了。

哨兵数量建议在三个以上且为奇数,在Redis官网也提到了各种情况的“布阵”方式,非常值得参考。

更多

既然是高可用方案,并非有严格意义上的“缺点”,还需配合使用场景进行考量。

故障转移期间短暂的不可用,但其实官网的例子也给出了parallel-syncs参数来指定并行的同步实例数量,以免全部实例都在同步出现整体不可用的情况,相对来说要比手工的故障转移更加方便。

分区逻辑需要自定义处理,虽然解决了主从下的高可用问题,但是Sentinel并没有提供分区解决方案,还需开发者考虑如何建设。

既然是还是主从,如果异常的写流量搞垮了主节点,那么自动的“故障转移”会不会变成自动“灾难传递”,即slave提升为Master之后挂掉,又进行提升又被挂掉。

不过最后这点也是笔者猜测,并没有听说过出现这种案例,可不必深究。

集群

概述

Redis Cluster是官方在3.0版本后推出的分布式方案。

对开发者而言,“官方支持”一词是大概率非常美好的,小到issue,大到feature。自定义去解决问题,成本总是要高一些。

有了官方的正式集群方案,从请求路由、故障转移、弹性伸缩几个纬度的使用上,将更为容易。

Cluster不同于哨兵,是支持分区的。有说法Cluster是哨兵的升级,这是不严谨的。

二者纬度不一样,如果因为Cluster也有故障转移的功能,就说它是哨兵的升级款,略显牵强。

Cluster在分区管理上,使用了“哈希槽”(hash slot)这么一个概念,一共有16384个槽位,每个实例负责一部分槽,通过CRC16(key)&16383这样的公式,计算出来key所对应的槽位。

)

虽然在节点和key二者中又引入了槽的概念,看起来不易理解,实际上因为颗粒度更细了,减少了节点的扩容和收缩难度,相比传统策略还是很有优势。

当然,“槽”是虚拟的概念,节点自身去维护“槽”的关系,并不是要真正下载启动个“槽服务”在跑。

使用方式

Redis的各种玩法,都是从配置文件着手,集群也不例外。

1
2
arduino复制代码1cluster-enabled yes
2cluster-config-file "redis-node.conf"

关键配置简洁明了,有两步:

开启集群

指定集群配置文件

集群配置文件(cluster-config-file)为内部使用,可以不去指定,Redis会帮助创建一个。启动还是普通的方式redis-server redis.conf

首先以集群方式启动了N台Redis实例,这当然还没完事。

接下来的步骤笔者称为“牵线搭桥分配槽”,听起来还算顺口。

“牵线搭桥分配槽”的方式也在不断升级,从直接用原始命令来处理,到使用脚本,以及现在的Redis-cli官方支持,使用哪种方式都可以。

1
2
3
lua复制代码1redis-cli --cluster create 127.0.0.1:7000 127.0.0.1:7001 \
2127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 \
3--cluster-replicas 1

上方的命令即是Redis官网给出的redis-cli的方式用法,一行命令完成“三主三从”以及自动分配槽的操作。

这样集群就搭建完成了,当然,使用官方提供的check命令检查一下,也是有必要的。

1
css复制代码1redis-cli --cluster check 127.0.0.1:7001

更多

虽然是对分区良好支持,但也有一些分区的老问题,譬如:如果不在同一个“槽”的数据,是没法使用类似mset的多键操作。

在select命令页有提到, 集群模式下只能使用一个库,虽然平时一般也是这么用的,但是要了解一下。

运维上也要谨慎,俗话说得好,“使用越简单底层越复杂”,启动搭建是很方便,使用时面对带宽消耗,数据倾斜等等具体问题时,还需人工介入,或者研究合适的配置参数。

| 结尾

趣谈

在写“主从”方案的时候,发现有一个有趣的事情:

笔者开始是记得主从的关键命令是SLAVEOF,后来查阅官方的时候,发现命令已经更改为REPLICAOF,虽然SLAVEOF还能用。

官网的一些描述词汇,有的地方还是Slave,也有些是用Replication。

好奇的笔者查了一下相关的资料,并看了些Redis作者antirez的有关此时博客,发现已经是两年前的事情了。

其实就是“Slave”这个变量名给了一些人机会,借此“喷”了一波作者,作者也做出了一部分妥协。

有兴趣的盆友可以自己搜搜看,技术外的东西就不做评价了,看个乐呵就行。

笔者的主要目的还是:看官方文档的时候,别让不同的“词汇”迷惑了。

END

本文对Redis这些拓展方案都作出了大致描述。

具体使用上,还需留意详细配置,以及客户端支持等综合情况来考量。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

设计模式之原型模式

发表于 2021-11-27

「这是我参与11月更文挑战的第27天,活动详情查看:2021最后一次更文挑战」

本篇文章是设计模式专题的第六篇文章,我会将遇到的设计模式都一一总结在该专题下,我会把自己对每一种设计模式的感悟写下来,以及在实际工作中我们该如何去灵活应用这些设计模式,欢迎大家关注。本篇文章我们就来讲一讲,用于创建重复对象的原型模式。

原型模式的简单介绍

原型模式也属于创建型模式的一种,它是用来创建重复对象的,也就是我们常说的拷贝对象。

原型模式是通过实现一个原型接口,然后调用clone()就可以完成对象复制,需要注意的是clone()方法是Object提供的,当需要深拷贝的时候,就需要通过重写克隆方法进行对象的拷贝。

原型模式类图:

image.png

原型模式扩展:

原型模式可以进行扩展,在原有的基础上增加一个原型管理器 PrototypeManager 类。该类用 HashMap 缓存多个复制的原型,客户端可以通过管理器的 get(String id) 方法从中获取复制的原型。

image.png

浅拷贝与深拷贝:

  • 浅拷贝:不重写clone方法的都是浅拷贝,浅拷贝如果对象内属性是引用类型的话,拷贝的是引用对象的地址。
  • 深拷贝:深拷贝需要重写clone方法,通过串行话或者其他方式将属性是引用类型的创建空间拷贝一份独立的。

原型模式的具体实现思路

  • 浅拷贝:直接实现Cloneable接口即可
  • 深拷贝:需要实现Cloneable接口,并且需要重写clone方法。
  • 原型管理器:
+ 创建抽象原型对象,继承Cloneable接口
+ 创建抽象原型对象的具体实现
+ 创建原型管理器,初始化缓存,提供获取拷贝对象的方法

原型模式的具体实现方案

  • 浅拷贝
1
2
3
4
5
6
java复制代码public class Prototype implements Cloneable {
   @Override
   public Object clone()  throws CloneNotSupportedException {
       return super.clone();
  }
}
  • 深拷贝

深拷贝的方式有很多,我们的目的是为了将引用类型重新开辟空间,而不是引用原引用的地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
java复制代码// 实现的方式有很多,针对属性进行再次拷贝赋值,或者使用序列化
// 1. 再次拷贝
public class Prototype implements Cloneable {
   private OtherPrototype other;
   
   @Override
   public Object clone()  throws CloneNotSupportedException {
       Object obj = super.clone();
// 将原引用的内容,重新开辟空间保存一份,使用新的引用
       obj.setOther((OtherPrototype) obj.getOther.clone());
       return obj;
  }
}
// 2. 序列化实现 注意需要实现Serializable
public class Prototype implements Serializable {
   public Object deepClone() throws Exception
  {
       // 序列化
       ByteArrayOutputStream bos = new ByteArrayOutputStream();
       ObjectOutputStream oos = new ObjectOutputStream(bos);
       oos.writeObject(this);
       // 反序列化
       ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
       ObjectInputStream ois = new ObjectInputStream(bis);
       return ois.readObject();
  }
}
  • 原型管理器方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
typescript复制代码// 抽象原型对象
public interface Prototype extends Cloneable {
   @Override
   public Object clone();
}
// 真实原型对象
public class Realizetype implements Prototype {
   @Override
   public Object clone()  throws CloneNotSupportedException {
       return super.clone();
  }
}
// 原型管理器
class ProtoTypeManager {
   // 缓存
   private HashMap<String, Object> cache = new HashMap<String, Object>();
   
   // 初始化缓存
   public ProtoTypeManager() {
       cache.put("realizetype", new Realizetype());
  }
   
   // 向缓存中添加原型对象
   public void addProtoType(String key, Object obj) {
       cache.put(key, obj);
  }
   // 获取对应原型对象的拷贝对象
   public Prototype getPrototype(String key) {
       Object temp = cache.get(key);
       return (Prototype) temp.clone();
  }
}

原型模式的优缺点

优点

  • 原型模式性能很好,基于二进制的流复制比new性能更好。
  • 原型模式可以避免构造函数的约束。
  • Java 为我们将原型模式封装好,我们使用很方便。

缺点

  • 当需要深拷贝时,一个类引用不支持串行化的间接对象,或者引用含有循环结构的时候,拷贝会变的很困难。
  • 必须实现 Cloneable 接口。
  • clone方法位于类的内部,如果需要对已有类进行改造,需要改变类的内部结构,违背了开闭原则。

原型模式的适用场景

  1. 对象之间相同或相似,只是个别的几个属性不同的时候。
  2. 创建对象成本较大,比如初始化时间长,占用CPU资源多,占用网络资源多等,需要优化资源。
  3. 创建一个对象需要繁琐的数据准备或访问权限等,需要提高性能或者提高安全性。
  4. 系统中大量使用该类对象,需要各个调用者给它的属性重新赋值。
  5. 当一个对象需要提供给其他对象访问,并且各个调用者都需要修改其值时。
  6. 原型模式很少单独出现,一般是伴随工厂方法模式一起出现,通过 clone 的方法创建一个对象,然后由工厂方法提供给调用者。

原型模式总结

原型模式虽然实现起来比较简单,用法也很简单。但是深拷贝和浅拷贝的问题我们一定要搞清楚,有时候使用浅拷贝导致数据混乱,排查起来也是很困难。再就是我们要善用原型管理器,通过原型管理器创建克隆对象会使我们事半功倍。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

详解TCP/IP的三次握手和四次挥手 三次握手 四次挥手

发表于 2021-11-27

本文正在参与 “网络协议必知必会”征文活动

我们知道TCP协议也被称为好人协议,是可靠传输的,TCP数据包没有长度限制,理论上可以无限长,但是为了保证网络的效率,通常TCP数据包的长度不会超过IP数据包的长度,以确保单个TCP数据包不必再分割。

基于TCP协议的通信,客户端与服务端必须建立一个双向通信的通道,客户端和服务端在建立双向连接时需要进行三次握手,断开连接时则需要四次挥手。

下面就对三次握手和四次挥手进行详细的解释:

三次握手

下图是客户端与服务端建立双向连接时进行三次握手的图解:

img

上述图片用语言文字来表达就是:

1 客户端向服务端发送一段TCP报文—请求连接 标记位:SYN,表示请求建立连接 序号:seq=x 客户端进入SYN_SENT阶段

2 服务端从半连接池中取出来自客户端的连接请求,结束LISTEN状态,返回一段TCP报文给客户端: 标记位:ACK=x+1:表示确认收到了客户端的连接序列号seq=x,并将x+1作为确认号返回给客户端 标记位:SYN(seq=y):确认收到连接请求的同时,向客户端发送连接请求,序号为seq=y 客户端进入EATABLISHED阶段,服务端进入SYN_RCVD阶段

3 客户端接收到从服务端发送的确认收到收到请求的结果(ACK=x+1)及服务端发起的连接请求(SYN seq=y),并向服务端发送确认连接。 标记位:ACK=y+1,表示确认收到了服务端的连接请求seq=y,并建立连接,将y+1作为确认号返回给服务端 服务端进入EATABLISHED阶段

如果你觉得上述描述不是很容易理解,我们可以与日常生活中的实际例子进行对比理解,比如将把客户端比作男孩,服务器比作女孩。用他们的交往来说明“三次握手”过程:

(1)男孩喜欢女孩,于是写了一封信告诉女孩:我爱你,请和我交往吧!;写完信之后,男孩焦急地等待,因为不知道信能否顺利传达给女孩。

(2)女孩收到男孩的情书后,心花怒放,原来我们是两情相悦呀!于是给男孩写了一封回信:我收到你的情书了,也明白了你的心意,其实,我也喜欢你!我愿意和你交往!; 写完信之后,女孩也焦急地等待,因为不知道回信能否能顺利传达给男孩。

(3)男孩收到回信之后很开心,因为发出的情书女孩收到了,并且从回信中知道了女孩喜欢自己,并且愿意和自己交往。然后男孩又写了一封信告诉女孩:你的心意和信我都收到了,谢谢你,还有我爱你! 女孩收到男孩的回信之后,也很开心,因为发出的情书男孩收到了。由此男孩女孩双方都知道了彼此的心意,之后就快乐地交流起来了~~

img

客户端与服务端建立连接时为什么需要进行三次握手来建立双向通信连接呢?为了防止服务器端开启一些无用的连接增加服务器开销以及防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。 也可以这样理解:“第三次握手”是客户端向服务器端发送数据,这个数据就是要告诉服务器,客户端有没有收到服务器“第二次握手”时传过去的数据。若发送的这个数据是“收到了”的信息,接收后服务器就正常建立TCP连接,否则建立TCP连接失败,服务器关闭连接端口。由此减少服务器开销和接收到失效请求发生的错误。 由于网络传输是有延时的(要通过网络光纤和各种中间代理服务器),在传输的过程中,比如客户端发起了SYN=1创建连接的请求(第一次握手)。 如果服务器端就直接创建了这个连接并返回包含SYN、ACK和Seq等内容的数据包给客户端,这个数据包因为网络传输的原因丢失了,丢失之后客户端就一直没有接收到服务器返回的数据包。 客户端可能设置了一个超时时间,时间到了就关闭了连接创建的请求。再重新发出创建连接的请求,而服务器端是不知道的,如果没有第三次握手告诉服务器端客户端收的到服务器端传输的数据的话, 服务器端是不知道客户端有没有接收到服务器端返回的信息的。

四次挥手

四次挥手对应的是客户端与服务端的连接断开,断开连接由哪一方先提出都可以,如下图所示

img

比如是客户端主动断开连接,上图用语言文字描述为:

1 首先客户端想要断开连接,向服务器端发送一段TCP报文–请求断开连接

1
2
3
4
5
6
7
> ini复制代码标记位:FIN,表示请求断开连接 
>
> 序号:seq=x+2
>
> 客户端此时处于FIN_WAIT_1阶段,即半关闭状态,停止客户端向服务端发送数据,但是服务端仍然能够向客户端发送数据。
>
>

2 服务端接收到从客户端发出的TCP报文即断开连接请求,服务端会返回确认收到客户端想要关闭连接

1
2
3
4
5
6
7
> ini复制代码标记位:ACK:表示收到客户端的断开连接请求 
>
> 序号:ACK=x+3
>
> 服务端由ESTABLISHED状态进入CLOSE_WAIT半关闭状态,并开始准备释放服务端到客户端的连接 客户端结束FIN_WAIT_1阶段,进入FIN_WAIT_2阶段
>
>

3 服务端向客户端传输数据已完成,再次向客户端发送断开连接的请求

1
2
3
4
5
> ini复制代码标记位:FIN 
>
> 序号:seq=y+1
>
>

服务端结束CLOSE_WAIT状态,进入LAST_ACK状态,并停止服务端到客户端的数据传输,但是客户端可以向服务端传输数据(前提是客户端通向服务端的连接没有关闭)

4 客户端接收从服务端发出的TCP报文,确认了服务端已经做好断开连接的准备,向服务端发送确认断开连接的报文

1
2
3
4
5
6
7
> ini复制代码标记位:ACK:表示接收到服务端断开连接的请求 
>
> 序号:ACK=y+2
>
> 客户端结束FIN_WAIT_2阶段,进入TIME_WAIT阶段,随后客户端开始在TIME_WAIT阶段等待2MSL 服务器端收到从客户端发出的TCP报文之后结束LAST-ACK阶段,进入CLOSED阶段。由此正式确认关闭服务器端到客户端方向上的连接。
>
>

客户端等待完2MSL之后,结束TIME-WAIT阶段,进入CLOSED阶段,由此完成“四次挥手”。

如果还用男生女生谈恋爱来通俗的表示四次挥手的过程,则可以通俗的理解为:

把客户端比作男孩,服务器比作女孩。通过他们的分手来说明“四次挥手”过程。

“第一次挥手”:日久见人心,男孩发现女孩变成了自己讨厌的样子,忍无可忍,于是决定分手,随即写了一封信告诉女孩。

“第二次挥手”:女孩收到信之后,知道了男孩要和自己分手,怒火中烧,心中暗骂:你算什么东西,当初你可不是这个样子的!于是立马给男孩写了一封回信:分手就分手,给我点时间,我要把你的东西整理好,全部还给你!男孩收到女孩的第一封信之后,明白了女孩知道自己要和她分手。随后等待女孩把自己的东西收拾好。

“第三次挥手”:过了几天,女孩把男孩送的东西都整理好了,于是再次写信给男孩:你的东西我整理好了,快把它们拿走,从此你我恩断义绝!

“第四次挥手”:男孩收到女孩第二封信之后,知道了女孩收拾好东西了,可以正式分手了,于是再次写信告诉女孩:我知道了,这就去拿回来! 这里双方都有各自的坚持。 女孩自发出第二封信开始,限定一天内收不到男孩回信,就会再发一封信催促男孩来取东西! 男孩自发出第二封信开始,限定两天内没有再次收到女孩的信就认为,女孩收到了自己的第二封信;若两天内再次收到女孩的来信,就认为自己的第二封信女孩没收到,需要再写一封信,再等两天…..

倘若双方信都能正常收到,最少只用四封信就能彻底分手!这就是“四次挥手”。

四次挥手时客户端必须在TIME-WAIT状态下等待2MSL,原因就是要确认服务器端是否收到客户端发出的ACK确认报文,当客户端发出最后的ACK确认报文时,并不能确定服务器端能够收到该段报文。所以客户端在发送完ACK确认报文之后,会设置一个时长为2MSL的计时器。MSL指的是Maximum Segment Lifetime:一段TCP报文在传输过程中的最大生命周期。2MSL即是服务器端发出为FIN报文和客户端发出的ACK确认报文所能保持有效的最大时长。 服务器端在1MSL内没有收到客户端发出的ACK确认报文,就会再次向客户端发出FIN报文; 如果客户端在2MSL内,再次收到了来自服务器端的FIN报文,说明服务器端由于各种原因没有接收到客户端发出的ACK确认报文。客户端再次向服务器端发出ACK确认报文,计时器重置,重新开始2MSL的计时; 否则客户端在2MSL内没有再次收到来自服务器端的FIN报文,说明服务器端正常接收了ACK确认报文,客户端可以进入CLOSED阶段,完成“四次挥手”。 所以,客户端要经历时长为2SML的TIME-WAIT阶段;这也是为什么客户端比服务器端晚进入CLOSED阶段的原因。

结语

文章首发于微信公众号程序媛小庄,同步于掘金、知乎。

码字不易,转载请说明出处,走过路过的小伙伴们伸出可爱的小指头点个赞再走吧(╹▽╹)

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

性能监控之 Linux 命令 top、vmstat、iost

发表于 2021-11-27

「这是我参与11月更文挑战的第27天,活动详情查看:2021最后一次更文挑战」

一、前言

“子曰:“温故而知新,可以为师矣。”
—–《论语》

二、top

在这里插入图片描述

执行命令:

1
bash复制代码top [-] [d] [p] [q] [c] [C] [S] [s]  [n]

参数说明:

  • d:指定每两次屏幕信息刷新之间的时间间隔。当然用户可以使用 s 交互命令来改变之。
  • p:通过指定监控进程 ID 来仅仅监控某个进程的状态。
  • q:该选项将使 top 没有任何延迟的进行刷新。如果调用程序有超级用户权限,那么top将以尽可能高的优先级运行。
  • S:指定累计模式。
  • s:使 top 命令在安全模式中运行。这将去除交互命令所带来的潜在危险。
  • i:使 top 不显示任何闲置或者僵死进程。
  • c:显示整个命令行而不只是显示命令名。

命令说明:

  1. 系统运行时间和平均负载:
1
bash复制代码top - 20:20:16 up 16:18,  4 users,  load average: 0.00, 0.01, 0.04

top 命令的顶部显示与 uptime 命令相似的输出

这些字段显示:

  • 当前时间
  • 系统已运行的时间
  • 当前登录用户的数量
  • 相应最近 1、5 和 15 分钟内的平均负载。

可以使用 ‘l’ 命令切换 uptime 的显示

  1. 任务
1
bash复制代码Tasks: 112 total,   1 running, 104 sleeping,   7 stopped,   0 zombie

Tasks — 任务(进程),系统现在共有 122 个进程,其中处于运行中的有 1 个,103 个在休眠(sleep),stoped 状态的有7个,zombie 状态(僵尸)的有 0 个,这些进程概括信息可以用 ‘t’ 切换显示。

  1. CPU 状态
1
bash复制代码%Cpu(s):  0.0 us,  0.3 sy,  0.0 ni, 99.7 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st

这里显示不同模式下所占 cpu 时间百分比,这些不同的 cpu 时间表示:

  • us, user:运行(未调整优先级的) 用户进程的CPU时间
  • sy,system: 运行内核进程的CPU时间
  • ni,niced:运行已调整优先级的用户进程的CPU时间
  • wa,IO wait: 用于等待IO完成的CPU时间
  • hi:处理硬件中断的 CPU 时间
  • si: 处理软件中断的 CPU 时间
  • st:这个虚拟机被 hypervisor 偷去的 CPU 时间(译注:如果当前处于一个 hypervisor 下的 vm,实际上hypervisor 也是要消耗一部分 CPU 处理时间的)。

可以使用 ‘t’ 命令切换显示。

  • 0.0% us — 用户空间占用 CPU 的百分比
  • 0.3% sy — 内核空间占用 CPU 的百分比
  • 0.0% ni — 改变过优先级的进程占用 CPU 的百分比
  • 99.7% id — 空闲 CPU 百分比
  • 0.0% wa — IO 等待占用 CPU 的百分比
  • 0.0% hi — 硬中断(Hardware IRQ)占用 CPU 的百分比
  • 0.0% si — 软中断(Software Interrupts)占用 CPU 的百分比
  1. 内存使用
1
2
bash复制代码KiB Mem :   995896 total,   432992 free,   168912 used,   393992 buff/cache
KiB Swap: 2097148 total, 2084084 free, 13064 used. 621592 avail Mem

接下来两行显示内存使用率,有点像 ‘free’ 命令。

  • 第一行是物理内存使用
    • 物理内存显示如下:全部可用内存、已使用内存、空闲内存、缓冲内存。
  • 第二行是虚拟内存使用(交换空间)
    • 交换部分显示的是:全部、已使用、空闲和缓冲交换空间。

内存显示可以用’m’命令切换。

  • 995896 total — 物理内存总量
  • 168912k used — 使用中的内存总量
  • 432992 k free — 空闲内存总量
  • 393992k buffers — 缓存的内存量

swap 交换分区:

  • 2097148k total — 交换区总量
  • 13064k used — 使用的交换区总量
  • 2084084k free — 空闲交换区总量
  • 621592k cached — 缓冲的交换区总量

第四行中使用中的内存总量(used)指的是现在系统内核控制的内存数
空闲内存总量(free)是内核还未纳入其管控范围的数量。纳入内核管理的内存不见得都在使用中,还包括过去使用过的现在可以被重复利用的内存,内核并不把这些可被重新使用的内存交还到free中去,因此在linux上free内存会越来越少,但不用为此担心。

如果出于习惯去计算可用内存数,这里有个近似的计算公式:
第四行的free+第四行的buffers+第五行的cached=服务器的可用内存第四行的free + 第四行的buffers + 第五行的cached=服务器的可用内存第四行的free+第四行的buffers+第五行的cached=服务器的可用内存

对于内存监控,在 top 里我们要时刻监控第五行 swap 交换分区的used,如果这个数值在不断的变化,说明内核在不断进行内存和 swap 的数据交换,这是真正的内存不够用了。

  1. 各进程(任务)的状态监控

在这里插入图片描述

参数说明:

  • PID:进程ID,进程的唯一标识符说明.
  • USER:进程所有者的实际用户名。
  • PR:进程的调度优先级。这个字段的一些值是 ‘rt’。这意味这这些进程运行在实时态。
  • NI:进程的 nice 值(优先级)。越小的值意味着越高的优先级。负值表示高优先级,正值表示低优先级
  • VIRT:进程使用的虚拟内存。进程使用的虚拟内存总量,单位kb。VIRT=SWAP+RES
  • RES:驻留内存大小。驻留内存是任务使用的非交换物理内存大小。进程使用的、未被换出的物理内存大小,单位kb。RES=CODE+DATA
  • SHR:SHR 是进程使用的共享内存。共享内存大小,单位 kb
  • S:这个是进程的状态。它有以下不同的值:
    • D - 不可中断的睡眠态。
    • R – 运行态
    • S – 睡眠态
    • T – 被跟踪或已停止
    • Z – 僵尸态
  • %CPU:自从上一次更新时到现在任务所使用的 CPU 时间百分比。
  • %MEM:进程使用的可用物理内存百分比。
  • TIME+:任务启动后到现在所使用的全部 CPU 时间,精确到百分之一秒。
  • COMMAND:运行进程所使用的命令。进程名称(命令名/命令行)
  1. 交互命令 - ‘h’帮助命令

在这里插入图片描述

在 top 基本视图中,按键盘数字“1”,可监控每个逻辑 CPU 的状况:(本虚拟机就是一个 cpu)

在这里插入图片描述

监控 Java 线程数:

1
bash复制代码ps -eLf | grep java | wc -l

监控网络客户连接数:

1
bash复制代码netstat -n | grep tcp | grep 侦听端口 | wc -l

三、vmstat

在这里插入图片描述

在这里插入图片描述

  • 2 表示每个两秒采集一次服务器状态
  • 1表示只采集一次。

结构说明:

  • r :表示运行队列(就是说多少个进程真的分配到 CPU),我测试的服务器目前 CPU 比较空闲,没什么程序在跑,当这个值超过了 CPU 数目,就会出现 CPU 瓶颈了。这个也和 top 的负载有关系,一般负载超过了 3 就比较高,超过了 5 就高,超过了 10 就不正常了,服务器的状态很危险。top 的负载类似每秒的运行队列。如果运行队列过大,表示你的 CPU 很繁忙,一般会造成 CPU 使用率很高。
  • b:表示阻塞的进程,这个不多说,进程阻塞,大家懂的。
  • swap:虚拟内存已使用的大小,如果大于0,表示你的机器物理内存不足了,如果不是程序内存泄露的原因,那么你该升级内存了或者把耗内存的任务迁移到其他机器。
  • free:空闲的物理内存的大小,我的机器内存总共 8G,剩余 3415M。
  • buff:Linux/Unix 系统是用来存储,目录里面有什么内容,权限等的缓存,我本机大概占用300多M
  • cache:cache 直接用来记忆我们打开的文件,给文件做缓冲,我本机大概占用300多M(这里是Linux/Unix的聪明之处,把空闲的物理内存的一部分拿来做文件和目录的缓存,是为了提高 程序执行的性能,当程序使用内存时,buffer/cached会很快地被使用。)
  • si:每秒从磁盘读入虚拟内存的大小,如果这个值大于 0,表示物理内存不够用或者内存泄露了,要查找耗内存进程解决掉。我的机器内存充裕,一切正常。
  • so:每秒虚拟内存写入磁盘的大小,如果这个值大于 0,同上。
  • bi:块设备每秒接收的块数量,这里的块设备是指系统上所有的磁盘和其他块设备,默认块大小是 1024 byte,我本机上没什么 IO 操作,所以一直是 0,但是我曾在处理拷贝大量数据(2-3T)的机器上看过可以达到140000/s,磁盘写入速度差不多140M每秒
  • bo:块设备每秒发送的块数量,例如我们读取文件,bo 就要大于0。bi和bo一般都要接近0,不然就是IO过于频繁,需要调整。
  • in:每秒 CPU 的中断次数,包括时间中断
  • cs:每秒上下文切换次数,例如我们调用系统函数,就要进行上下文切换,线程的切换,也要进程上下文切换,这个值要越小越好,太大了,要考虑调低线程或者进程的数目,例如在 apache 和 nginx 这种 web 服务器中,我们一般做性能测试时会进行几千并发甚至几万并发的测试,选择 web 服务器的进程可以由进程或者线程的峰值一直下调,压测,直到 cs 到一个比较小的值,这个进程和线程数就是比较合适的值了。系统调用也是,每次调用系统函数,我们的代码就会进入内核空间,导致上下文切换,这个是很耗资源,也要尽量避免频繁调用系统函数。上下文切换次数过多表示你的 CPU 大部分浪费在上下文切换,导致 CPU 干正经事的时间少了,CPU没有充分利用,是不可取的。
  • us:用户 CPU 时间,我曾经在一个做加密解密很频繁的服务器上,可以看到us接近100,r运行队列达到80(机器在做压力测试,性能表现不佳)。us的值比较高时,说明用户进程消耗的CPU时间多,但是如果长期超50%的使用,那么我们就该考虑优化程序算法或者进行加速。
  • sy:系统 CPU 时间,如果太高,表示系统调用时间长,例如是 IO 操作频繁。
  • id:空闲 CPU时间,一般来说,id + us + sy = 100,一般我认为 id 是空闲 CPU 使用率,us 是用户 CPU 使用率,sy是系统 CPU 使用率。
  • wt:等待 IO CPU 时间。注意:wa 的值高时,说明 IO等 待比较严重,这可能由于磁盘大量作随机访问造成,也有可能磁盘出现瓶颈(块操作)。

四、iostat

在这里插入图片描述

安装方法:

1
bash复制代码yum install sysstat
1
bash复制代码iostat [参数] [时间] [次数]

参数说明:

  • -c:显示 CPU 使用情况
  • -d:显示磁盘使用情况
  • -k:以 K 为单位显示
  • -m:以 M 为单位显示
  • -N:显示磁盘阵列(LVM) 信息
  • -n:显示 NFS 使用情况
  • -p:可以报告出每块磁盘的每个分区的使用情况
  • -t:显示终端和 CPU 的信息
  • -x:显示详细信息

在这里插入图片描述

  • rrqm/s:每秒这个设备相关的读取请求有多少被 Merge了(当系统调用需要读取数据的时候,VFS 将请求发到各个 FS,如果 FS 发现不同的读取请求读取的是相同 Block 的数据,FS会将这个请求合并Merge)
  • wrqm/s:每秒这个设备相关的写入请求有多少被 Merge 了
  • rsec/s:每秒读取的扇区数
  • wsec/:每秒写入的扇区数
  • rKB/s:The number of read requests that were issued to the device per second
  • wKB/s:The number of write requests that were issued to the device per second
  • avgrq-sz 平均请求扇区的大小
  • avgqu-sz 是平均请求队列的长度。毫无疑问,队列长度越短越好。
  • await: 每一个IO请求的处理的平均时间(单位是微秒毫秒)。这里可以理解为 IO 的响应时间,一般地系统 IO 响应时间应该低于 5 ms,如果大于 10 ms 就比较大了。这个时间包括了队列时间和服务时间,也就是说,一般情况下,await 大于 svctm,它们的差值越小,则说明队列时间越短,反之差值越大,队列时间越长,说明系统出了问题。
  • svctm:表示平均每次设备 I/O 操作的服务时间(以毫秒为单位)。如果 svctm 的值与 await 很接近,表示几乎没有 I/O 等待,磁盘性能很好,如果 await 的值远高于 svctm 的值,则表示 I/O 队列等待太长,系统上运行的应用程序将变慢。
  • %util:在统计时间内所有处理IO时间,除以总共统计时间。例如,如果统计间隔 1 秒,该设备有 0.8 秒在处理IO,而 0.2 秒闲置,那么该设备的 %util = 0.8/1 = 80%,所以该参数暗示了设备的繁忙程度
  • 一般地,如果该参数是 100% 表示设备已经接近满负荷运行了(当然如果是多磁盘,即使 %util 是 100%,因为磁盘的并发能力,所以磁盘使用未必就到了瓶颈)。

常见用法:

1
2
3
4
5
bash复制代码iostat -d -k 1 10    #查看 TPS 和吞吐量信息(磁盘读写速度单位为 KB)
iostat -d -m 2 #查看 TPS 和吞吐量信息(磁盘读写速度单位为 MB)
iostat -d -x -k 1 10 #查看设备使用率(%util)、响应时间(await)
iostat -c 1 10 #查看 cpu 状态
iostat -c 1 10 #查看 cpu 状态

注意点:

  • 网卡的大吞吐量可能导致更多的 cup
  • 大量的 cup 开销又会增加更多内存使用请求
  • 大量内存与磁盘的请求可能导致更多的 cpu 以及 IO 问题

五、free

在这里插入图片描述

  • Mem:行(第二行)是内存的使用情况
  • Swap:行(第三行)是交换空间的使用情况。
  • total:列显示系统总的可用物理内存和交换空间大小。
  • used:列显示已经被使用的物理内存和交换空间。
  • free:列显示还有多少物理内存和交换空间可用使用。
  • shared:列显示被共享使用的物理内存大小。
  • buff/cache:列显示被 buffer 和 cache 使用的物理内存大小。
  • available:列显示还可以被应用程序使用的物理内存大小。

六、iftop

在这里插入图片描述

界面相关说明:

  • 界面上面显示的是类似刻度尺的刻度范围,为显示流量图形的长条作标尺用的。
  • 中间的 <= => 这两个左右箭头,表示的是流量的方向。
  • TX:发送流量
  • RX:接收流量
  • TOTAL:总流量
  • Cumm:运行 iftop 到目前时间的总流量
  • peak:流量峰值
  • rates:分别表示过去 2s 10s 40s 的平均流量

常用的参数:

  • -i 设定监测的网卡,如:# iftop -i eth1
  • -B 以bytes为单位显示流量(默认是bits),如:# iftop -B
  • -n 使host信息默认直接都显示IP,如:# iftop -n
  • -N 使端口信息默认直接都显示端口号,如: # iftop -N
  • -F 显示特定网段的进出流量,如# iftop -F 100.100.30.25 或# iftop -F 100.100.30.25 /255.255.255.0
  • -h(display this message),帮助,显示参数信息
  • -p 使用这个参数后,中间的列表显示的本地主机信息,出现了本机以外的IP信息;
  • -b 使流量图形条默认就显示;
  • -f 这个暂时还不太会用,过滤计算包用的;
  • -P 使 host 信息及端口信息默认就都显示;
  • -m 设置界面最上边的刻度的最大值,刻度分五个大段显示,例:# iftop -m 100M
    iftop(注意大小写)

常用操作:

  • 按 h 切换是否显示帮助;
  • 按 n 切换显示本机的 IP 或主机名;
  • 按 s 切换是否显示本机的 host 信息;
  • 按 d 切换是否显示远端目标主机的 host 信息;
  • 按 t 切换显示格式为 2 行/ 1 行/只显示发送流量/只显示接收流量;
  • 按 N 切换显示端口号或端口服务名称;
  • 按 S 切换是否显示本机的端口信息;
  • 按 D 切换是否显示远端目标主机的端口信息;
  • 按 p 切换是否显示端口信息;
  • 按 P 切换暂停/继续显示;
  • 按 b 切换是否显示平均流量图形条;
  • 按 B 切换计算 2 秒或 10 秒或 40 秒内的平均流量;
  • 按 T 切换是否显示每个连接的总流量;
  • 按 l 打开屏幕过滤功能,输入要过滤的字符,比如 ip,按回车后,屏幕就只显示这个 IP 相关的流量信息;
  • 按 L 切换显示画面上边的刻度;刻度不同,流量图形条会有变化;
  • 按 j 或按 k 可以向上或向下滚动屏幕显示的连接记录;
  • 按 1 或 2 或 3 可以根据右侧显示的三列流量数据进行排序;
  • 按 < 根据左边的本机名或IP排序;
  • 按 > 根据远端目标主机的主机名或IP排序;
  • 按 o 切换是否固定只显示当前的连接;
  • 按 f 可以编辑过滤代码,这是翻译过来的说法,我还没用过这个!
  • 按 ! 可以使用 shell 命令,这个没用过 !没搞明白啥命令在这好用呢!
  • 按 q 退出监控。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【Tool】 python项目中集成使用Firebase推送

发表于 2021-11-27

这是我参与11月更文挑战的第27天,活动详情查看:2021最后一次更文挑战

背景介绍

  • 目前,App推送功能已经非常普遍,几乎所有App都有推送功能。
  • 推送功能可以自己实现,也可以使用第三方提供的推送服务(免费的收费的都有)。
  • 本文主要介绍使用Firebase提供的推送服务Firebase Cloud Messaging(简称FCM)。
  • 本文主要介绍FCM工作原理、后端集成流程,但不包括客户端集成方面的内容。

FCM使用

  • 官网:firebase.google.com/docs/cloud-…
  • 简介:Firebase Cloud Messaging (FCM) 是一种跨平台消息传递解决方案,可供您可靠地传递消息,且无需任何费用。使用 FCM,您可以通知客户端App有新的电子邮件或其他数据等待同步。您可以发送通知消息与用户互动并留住他们。
  • 主要功能:发送通知消息或数据消息
  • 工作原理:
+ FCM 实现包括用于发送和接收的两个主要组件:
+ 一个受信任的环境,例如 Cloud Functions for Firebase 或用于构建、定位和发送消息的应用服务器。
+ 一个通过针对具体平台的相应传输服务接收消息的 Apple、Android 或 Web (JavaScript) 客户端应用。
+ 您可以通过 [Firebase Admin SDK](https://firebase.google.com/docs/cloud-messaging/server#firebase-admin-sdk-for-fcm) 或 [FCM 服务器协议](https://firebase.google.com/docs/cloud-messaging/server#choose)发送消息。

image.png

  • 工作原理图解说明
+ 首先,可以通过后端api或者Console GUI创建发推送消息的请求,该请求会交给FCM backend
+ 然后,由FCM backend帮我们把消息推送到用户手机上,并且支持跨平台推送
+ 用户手机上能收到消息的前提:安装了集成FCM SDK的App
+ 注意1:后端通过api创建推送请求使用的是FCM Admin SDK,客户端开发使用的是FCM SDK
+ 注意2:后端通过api创建推送请求可以直接使用FCM Admin SDK, 或者按照[FCM 服务器协议](https://firebase.google.com/docs/cloud-messaging/server#choose)自己实现
  • 发送消息的方式:
+ 发送消息的方式有两种:按设备注册token、按主题
+ 每个安装了集成FCM SDK的客户端,都可以生成唯一的register\_token,这个register\_token会由App客户端给到App服务端。服务端拿到这个register\_token,就可以只给这个设备推送消息。
+ 每个设备也可以订阅主题,订阅主题后,可以直接给这个主题推送消息。这样所有订阅过该主题的设备都可以收到推送消息。

FCM集成到服务器(Python)

  • 目前,FCM Admin SDK支持五种服务端编程语言:Node.js、Java、Python、Go、C#
  • 前提条件:
+ 确保您拥有服务器应用。
+ 确保您的服务器运行 Admin Python SDK — Python 3.6+
  • 设置Firebase项目和服务账号
+ 如需使用 Firebase Admin SDK,您需要具备以下项:
+ Firebase 项目
+ 用于与 Firebase 通信的服务帐号
+ 包含服务帐号凭据的配置文件
  • 创建Firebase项目
1
2
3
4
5
6
7
8
markdown复制代码# Firebase控制台:https://console.firebase.google.com

# 创建流程
1. 在 Firebase 控制台中,点击添加项目
2. 如果出现 Firebase 条款提示,请查看并接受。
3. 点击继续。
4. 为您的项目设置 Google Analytics(可选)
5. 点击创建项目(如果使用现有的 Google Cloud 项目,则点击添加 Firebase)
  • 创建包含服务帐号凭据的配置文件
1
2
3
go复制代码1. 在 Firebase 控制台中,打开设置 > 服务帐号。
2. 点击生成新的私钥,然后点击生成密钥进行确认。
3. 妥善存储包含密钥的 JSON 文件.
  • 添加SDK

Firebase Admin Python SDK 可通过 pip 获得。

1
复制代码pip3 install firebase-admin
  • 初始化SDK

通过服务帐号进行授权时,有两种方式可为您的应用提供凭据。

方式1(推荐)

(1)将环境变量 GOOGLE_APPLICATION_CREDENTIALS 设置为包含服务帐号密钥的 JSON 文件的文件路径

1
shell复制代码export GOOGLE_APPLICATION_CREDENTIALS="/home/user/Downloads/service-account-file.json"

(2)完成上述步骤后,应用默认凭据 (ADC) 能够隐式确定您的凭据

1
2
3
4
python复制代码import firebase_admin


default_app = firebase_admin.initialize_app()

方式2:直接在代码中写死JSON文件路径

1
2
3
4
5
6
7
python复制代码
import firebase_admin
from firebase_admin import credentials


cred = credentials.Certificate("path/to/service_account.json")
default_app = firebase_admin.initialize_app(credential=cred)
  • 向指定设备发送消息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
python复制代码from firebase_admin import messaging

# This registration token comes from the client FCM SDKs.
registration_token = 'YOUR_REGISTRATION_TOKEN'

# See documentation on defining a message payload.
message = messaging.Message(
notification=messaging.Notification(
title='your_titme',
body='your_body',
image='your_img',
),
token=registration_token,
)

# Send a message to the device corresponding to the provided registration token.
response = messaging.send(message)
# Response is a message ID string.
print('Successfully sent message:', response)
  • 向主题发送消息

创建主题后,使用服务器 API向主题发送消息,则订阅过该主题的设备都会受到消息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
python复制代码from firebase_admin import messaging

# The topic name can be optionally prefixed with "/topics/".
topic = 'highScores'

# See documentation on defining a message payload.
message = messaging.Message(
notification=messaging.Notification(
title='your_titme',
body='your_body',
image='your_img',
),
topic=topic,
)

# Send a message to the devices subscribed to the provided topic.
response = messaging.send(message)
# Response is a message ID string.
print('Successfully sent message:', response)
  • 批量发送消息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
python复制代码# Create a list containing up to 500 messages.
messages = [
messaging.Message(
notification=messaging.Notification('Price drop', '5% off all electronics'),
token=registration_token,
),
# ...
messaging.Message(
notification=messaging.Notification('Price drop', '2% off all books'),
topic='readers-club',
),
]

response = messaging.send_all(messages)
# See the BatchResponse reference documentation
# for the contents of response.
print('{0} messages were sent successfully'.format(response.success_count))
  • 服务端订阅主题

客户端SDK可以帮用户订阅主题,服务端可以通过接口帮用户订阅主题。

您可以为客户端应用实例订阅任何现有主题,也可创建新主题。当您使用 API 为客户端应用订阅新主题(您的 Firebase 项目中尚不存在的主题)时,系统会在 FCM 中创建一个使用该名称的新主题,随后任何客户端都可订阅该主题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
python复制代码# 您可以将注册令牌列表传递给 Firebase Admin SDK 订阅方法,以便为相应的设备订阅主题

# These registration tokens come from the client FCM SDKs.
registration_tokens = [
'YOUR_REGISTRATION_TOKEN_1',
# ...
'YOUR_REGISTRATION_TOKEN_n',
]

# Subscribe the devices corresponding to the registration tokens to the
# topic.
response = messaging.subscribe_to_topic(registration_tokens, topic)
# See the TopicManagementResponse reference documentation
# for the contents of response.
print(response.success_count, 'tokens were subscribed successfully')

注意:在单次请求中,您最多可以为 1000 台设备订阅或退订主题。

  • 服务端推订主题

利用 Admin FCM API,您还可以将注册令牌传递给相应的方法,来为设备退订主题

1
2
3
4
5
6
7
8
9
10
11
12
13
python复制代码# These registration tokens come from the client FCM SDKs.
registration_tokens = [
'YOUR_REGISTRATION_TOKEN_1',
# ...
'YOUR_REGISTRATION_TOKEN_n',
]

# Unubscribe the devices corresponding to the registration tokens from the
# topic.
response = messaging.unsubscribe_from_topic(registration_tokens, topic)
# See the TopicManagementResponse reference documentation
# for the contents of response.
print(response.success_count, 'tokens were unsubscribed successfully')

总结

  • 使用Firebase可以快速实现消息推送,支持跨平台,并且免费使用。
  • 可以通过借助 Firebase Admin SDK实现消息推动和主题管理,也可以按照FCM 服务器协议自己实现接口。
  • Firebase Admin SDK优点:使用方便,啥都有。
  • Firebase Admin SDK缺点:包太大不轻便(推送功能仅使用FCM),不支持asyncio异步操作。

结语

文章首发于微信公众号程序媛小庄,同步于掘金。

码字不易,转载请说明出处,走过路过的小伙伴们伸出可爱的小指头点个赞再走吧(╹▽╹)

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

c# 高级编程 10章214页 【集合】【链表LinkedL

发表于 2021-11-27

LinkedList<T>

  • 双向链表。通过移动到下一个元素,可以正向遍历整个链表。通过移动到前一个元素,可以反向遍历整个链表
  • 插入元素,效率很高。只需要修改前一个元素的Next和后一个元素的Previous。相比之下,List<T>插入元素,需要移动后面所有元素。
  • 要访问链表位于中间的元素,效率较低。只能从头或者从尾一个一个找过去。
  • 元素为LinkedListNode<T>
成员 说明
First 第一个元素
Last 最后一个元素
AddAfter() 指定位置后,插入
AddBefore() 指定位置前,插入
AddFirst() 插入到最前面
AddLast() 插入到最后面
Remove() 指定位置,删除
RemoveFirst() 删除第一个
RemoveLast() 删除最后一个
Find() 从链表头,开始找
FindLast() 从链表尾,开始找

LinkedListNode<T>

成员 说明
List 与此Node,相关的LinkedList<T>对象
Next 下一个Node
Previous 前一个Node
Value 此Node中,T类型的值

示例

  • 动态插入Document的过程中,需要保持一种秩序
  • 这种秩序是:
    • Priority高的,排在前面
    • Priority相同时,先插入的排在前面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
js复制代码    class Program
{
static void Main()
{
var pdm = new PriorityDocumentManager();
pdm.AddDocument(new Document("one", "Sample", 8));
pdm.AddDocument(new Document("two", "Sample", 3));
pdm.AddDocument(new Document("three", "Sample", 4));
pdm.AddDocument(new Document("four", "Sample", 8));
pdm.AddDocument(new Document("five", "Sample", 1));
pdm.AddDocument(new Document("six", "Sample", 9));
pdm.AddDocument(new Document("seven", "Sample", 1));
pdm.AddDocument(new Document("eight", "Sample", 1));

pdm.DisplayAllNodes();
}
}

public class Document
{
public Document(string title, string content, byte priority)
{
Title = title;
Content = content;
Priority = priority;
}

public string Title { get; }
public string Content { get; }
public byte Priority { get; }
}

输出:

1
2
3
4
5
6
7
8
js复制代码priority: 9, title six
priority: 8, title one
priority: 8, title four
priority: 4, title three
priority: 3, title two
priority: 1, title five
priority: 1, title seven
priority: 1, title eight

实现:

  • List<LinkedListNode<Document>> _priorityNodes用来存储,在每一个Priority上,已经插入了的,最新的,Document。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
js复制代码    public class PriorityDocumentManager
{
private readonly LinkedList<Document> _documentList;

// priorities 0.9
private readonly List<LinkedListNode<Document>> _priorityNodes;

public PriorityDocumentManager()
{
_documentList = new LinkedList<Document>();

_priorityNodes = new List<LinkedListNode<Document>>(10);
for (int i = 0; i < 10; i++)
{
_priorityNodes.Add(new LinkedListNode<Document>(null));
}
}

public void AddDocument(Document d)
{
if (d is null) throw new ArgumentNullException(nameof(d));

AddDocumentToPriorityNode(d, d.Priority);
}

private void AddDocumentToPriorityNode(Document doc, int priority)
{
if (priority > 9 || priority < 0)
throw new ArgumentException("Priority must be between 0 and 9");

if (_priorityNodes[priority].Value == null)
{
--priority;
if (priority >= 0)
{
// check for the next lower priority
AddDocumentToPriorityNode(doc, priority);
}
else // now no priority node exists with the same priority or lower
// add the new document to the end
{
_documentList.AddLast(doc);
_priorityNodes[doc.Priority] = _documentList.Last;
}
return;
}
else // a priority node exists
{
LinkedListNode<Document> prioNode = _priorityNodes[priority];
if (priority == doc.Priority)
// priority node with the same priority exists
{
_documentList.AddAfter(prioNode, doc);

// set the priority node to the last document with the same priority
_priorityNodes[doc.Priority] = prioNode.Next;
}
else // only priority node with a lower priority exists
{
// get the first node of the lower priority
LinkedListNode<Document> firstPrioNode = prioNode;

while (firstPrioNode.Previous != null &&
firstPrioNode.Previous.Value.Priority == prioNode.Value.Priority)
{
firstPrioNode = prioNode.Previous;
prioNode = firstPrioNode;
}

_documentList.AddBefore(firstPrioNode, doc);

// set the priority node to the new value
_priorityNodes[doc.Priority] = firstPrioNode.Previous;
}
}
}

public void DisplayAllNodes()
{
foreach (Document doc in _documentList)
{
Console.WriteLine($"priority: {doc.Priority}, title {doc.Title}");
}
}

// returns the document with the highest priority
// (that's first in the linked list)
public Document GetDocument()
{
Document doc = _documentList.First.Value;
_documentList.RemoveFirst();
return doc;
}
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…157158159…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%