个人学习笔记
2022-12-08 22:39:25 53 举报
AI智能生成
个人笔记
作者其他创作
大纲/内容
基础篇
正则表达式
举例
^abc
匹配出现在行首的abc
abc$
匹配出现在行尾的abc
^abc$
以abc打头和结尾,即匹配值为abc的行
[Aa]bc
匹配Abc或abc
语法
特殊字符
字符位置匹配
^
匹配字符串的开始位置。
在[]内,表示非。
如:[^a]bc,表示匹配bc,但不匹配abc
$
匹配字符串结束位置
\b
匹配一个单词的边界,也就是指单词和空格间的位置。例如,“er\b”可以匹配“never”中的“er”,但不能匹配“verb”中的“er”;
\B
匹配非单词边界。“er\B”能匹配“verb”中的“er”,但不能匹配“never”中的“er”。
\<
匹配词(word)的开始(\<)
\>
匹配词(word)的结束(\>)
表示前面表达式循环次数
*
匹配前面的子表达式任意次,等价{0,}
+
匹配前面的子表达式一次或多次(大于等于1次),等价{1,}
?
匹配前面的子表达式零次或一次,等价{0,1}
{n}
n是一个非负整数。匹配确定的n次
{n,}
n是一个非负整数。至少匹配n次
{n,m}
m和n均为非负整数,其中n<=m。最少匹配n次且最多匹配m次
正负元字符(上)
[xyz]
字符集合。匹配所包含的任意一个字符。例如,“[abc]”可以匹配“plain”中的“a”
[^xyz]
负值字符集合。匹配未包含的任意字符。例如 ,“[^abc]”可以匹配“place”中的“ple”任一字符。
[a-z]
字符范围。匹配指定范围内的任意字符。例如,“[a-z]”可以匹配“a”到“z”范围内的任意小写字母字符。
[^a-z]
负值字符范围。匹配任何不在指定范围内的任意字符。例如,“[^a-z]”可以匹配任何不在“a”到“z”范围内的任意
\d
匹配一个数字字符。等价于[0-9]
\D
匹配一个非数字字符。等价于[^0-9]。
\w
匹配包括下划线的任何单词字符。类似但不等价于“[A-Za-z0-9_]”,这里的"单词"字符使用Unicode字符集。
\W
匹配任何非单词字符。等价于“[^A-Za-z0-9_]”。
不可见字符
\s
匹配任何不可见字符,包括空格、制表符、换页符等等。等价于[ \f\n\r\t\v]。
\S
匹配任何可见字符。等价于[^ \f\n\r\t\v]。
\f
匹配一个换页符。等价于\x0c和\cL。
\n
匹配一个换行符。等价于\x0a和\cJ。
\r
匹配一个回车符。等价于\x0d和\cM。
\t
匹配一个制表符。等价于\x09和\cI。
\v
匹配一个垂直制表符。等价于\x0b和\cK。
其他元字符
\
转义字符,例如:\\n 匹配 \n换行符
?
当该字符紧跟在任何一个其他限制符(*,+,?,{n},{n,},{n,m})后面时,匹配模式是非贪婪的。
.
匹配除“\n”换行符和“\r”回车符之外的任何单个字符
|
或,例如,“z|food”能匹配“z”或“food”。“[z|f]ood”则匹配“zood”或“food”。
()
将( 和 ) 之间的表达式定义为“组”(group),并且将匹配这个表达式的字符保存到一个临时区域
(一个正则表达式中最多可以保存9个),它们可以用 \1 到\9 的符号来引用。
(一个正则表达式中最多可以保存9个),它们可以用 \1 到\9 的符号来引用。
,
分割,[a,h,t,w] 包含a或h或t或w字母 , [0,3,6,8] 包含0或3或6或8数字
修饰符
g
global (g),全局匹配
i
ignoreCase(i),不区分大小写的匹配
m
multiline(m),多(more)行匹配
s
允许.匹配换行符
u
Unicode(u),将模式视为Unicode代码点的序列,必须要在正则中包含unicode才能看到效果
进制转义值
\xn
匹配n,其中n为十六进制转义值。十六进制转义值必须为确定的两个数字长。例如,“\x41”匹配“A”。“\x041”则等价于“\x04&1”。正则表达式中可以使用ASCII编码。
\num
匹配num,其中num是一个正整数。对所获取的匹配的引用。例如,“(.)\1”匹配两个连续的相同字符。
\n
标识一个八进制转义值或一个向后引用。如果\n之前至少n个获取的子表达式,则n为向后引用。否则,如果n为八进制数字(0-7),则n为一个八进制转义值。
\nm
标识一个八进制转义值或一个向后引用。如果\nm之前至少有nm个获得子表达式,则nm为向后引用。
如果\nm之前至少有n个获取,则n为一个后跟文字m的向后引用。如果前面的条件都不满足,
若n和m均为八进制数字(0-7),则\nm将匹配八进制转义值nm。
如果\nm之前至少有n个获取,则n为一个后跟文字m的向后引用。如果前面的条件都不满足,
若n和m均为八进制数字(0-7),则\nm将匹配八进制转义值nm。
\nml
如果n为八进制数字(0-7),且m和l均为八进制数字(0-7),则匹配八进制转义值nml。
\un
匹配n,其中n是一个用四个十六进制数字表示的Unicode字符,例如,\u00A9匹配版权符号(©)。
模式匹配
获取匹配
(pattern)
匹配pattern并获取这一匹配。所获取的匹配可以从产生的Matches集合得到
非获取匹配
(?:pattern)
匹配pattern但不获取匹配结果,也就是说这是一个非获取匹配,不进行存储供以后使用。
这在使用或字符“(|)”来组合一个模式的各个部分是很有用。
例如“industr(?:y|ies)”就是一个比“industry|industries”更简略的表达式。
这在使用或字符“(|)”来组合一个模式的各个部分是很有用。
例如“industr(?:y|ies)”就是一个比“industry|industries”更简略的表达式。
正向(pattern在后面)预查
xxx(?=pattern)
逻辑
先向后(即正向)探测,看看有没有符合 pattern的。
如果有,则把左面的匹配出来;如果没有,则光标往后移一位,继续探测。
这个过程就是正向预查:预先判断为某个值然后匹配到的东西不包含这个元素。
注意:(?=pattern)在右边,查找返回值是左边的部分。(?=pattern) 后面不能跟任何字符
例如:/\d{1,3}(?=(\d{3})+$)/g 查找的是\d{1,3}后面是不是“3的倍数个数的数字”
/[a-z]+(?=ing)li/g 用exec查找,返回null,因为(?=pattern) 后面跟了其它字符
如果有,则把左面的匹配出来;如果没有,则光标往后移一位,继续探测。
这个过程就是正向预查:预先判断为某个值然后匹配到的东西不包含这个元素。
注意:(?=pattern)在右边,查找返回值是左边的部分。(?=pattern) 后面不能跟任何字符
例如:/\d{1,3}(?=(\d{3})+$)/g 查找的是\d{1,3}后面是不是“3的倍数个数的数字”
/[a-z]+(?=ing)li/g 用exec查找,返回null,因为(?=pattern) 后面跟了其它字符
非获取匹配,正向肯定预查,在任何匹配pattern的字符串开始处匹配查找字符串,
该匹配不需要获取供以后使用。例如:“Windows(?=95|98|NT|2000)”
能匹配“Windows2000”中的“Windows”,但不能匹配“Windows3.1”中的“Windows”。
该匹配不需要获取供以后使用。例如:“Windows(?=95|98|NT|2000)”
能匹配“Windows2000”中的“Windows”,但不能匹配“Windows3.1”中的“Windows”。
xxx(?!pattern)
逻辑
先向后(即正向)探测,看看有没有不符合pattern的。如果有,则把左面的匹配出来;
如果没有,则光标往后移一位,继续探测。
这个过程就是负正向预查:预先判断为某个值。然后匹配到的东西不包含这个元素。
注意:(?!pattern)在右边,查找返回值是左边的部分。(?!pattern) 后面不能跟任何字符
如果没有,则光标往后移一位,继续探测。
这个过程就是负正向预查:预先判断为某个值。然后匹配到的东西不包含这个元素。
注意:(?!pattern)在右边,查找返回值是左边的部分。(?!pattern) 后面不能跟任何字符
非获取匹配,正向否定预查,在任何不匹配pattern的字符串开始处匹配查找字符串,
该匹配不需要获取供以后使用。例如:“Windows(?!95|98|NT|2000)”
能匹配“Windows3.1”中的“Windows”,但不能匹配“Windows2000”中的“Windows”。
该匹配不需要获取供以后使用。例如:“Windows(?!95|98|NT|2000)”
能匹配“Windows3.1”中的“Windows”,但不能匹配“Windows2000”中的“Windows”。
反向(pattern在前面)预查
(?<=pattern)xxx
逻辑
从右向左(即反向)探测,看看有没有符合pattern的。如果有,则把右面的匹配出来;
如果没有,则光标往后移一位,继续探测。这个过程就是反向预查:预先判断为某个值。
然后匹配到的东西不包含这个元素。
如果没有,则光标往后移一位,继续探测。这个过程就是反向预查:预先判断为某个值。
然后匹配到的东西不包含这个元素。
非获取匹配,反向肯定预查,与正向肯定预查类似,只是方向相反。
例如:“(?<=95|98|NT|2000)Windows”能匹配“2000Windows”
中的“Windows”,但不能匹配“3.1Windows”中的“Windows”。
例如:“(?<=95|98|NT|2000)Windows”能匹配“2000Windows”
中的“Windows”,但不能匹配“3.1Windows”中的“Windows”。
(?<!pattern)xxx
逻辑
从右向左(即反向)探测,看看有没有不符合pattern的。如果有,则把右面的匹配出来;
如果没有,则光标往后移一位,继续探测。这个过程就是负反向预查:
预先判断为某个值, 然后匹配到的东西不包含这个元素。
如果没有,则光标往后移一位,继续探测。这个过程就是负反向预查:
预先判断为某个值, 然后匹配到的东西不包含这个元素。
非获取匹配,反向否定预查,与正向否定预查类似,只是方向相反。
例如:“(?<!95|98|NT|2000)Windows”能匹配“3.1Windows”中的“Windows”,
但不能匹配“2000Windows”中的“Windows”。
例如:“(?<!95|98|NT|2000)Windows”能匹配“3.1Windows”中的“Windows”,
但不能匹配“2000Windows”中的“Windows”。
举例
概念
贪婪模式、非贪婪模式
贪婪模式
尽可能多地匹配所搜索的字符串,默认为贪婪模式
非贪婪模式:
尽可能少地匹配所搜索的字符串
举例:对于字符串“oooo”,“o+”(贪婪模式)将尽可能多地匹配“o”,得到结果[“oooo”],而“o+?”(在贪婪模式的基础上追加一个?变为非贪婪模式)将尽可能少地匹配“o”,得到结果 ['o', 'o', 'o', 'o']
\r和\n 的区别
'\n' 10 换行(newline)'\r' 13 回车(return)
在windows系统下,回车换行符号是“\r \n"
在Linux等系统下是没有"\r"符号的
在解析文本或其他格式的文件内容时,常常要碰到判定回车换行的地方,这个时候就要注意既要判定"\r\n"又要判定"\n"。
在windows系统下,回车换行符号是“\r \n"
在Linux等系统下是没有"\r"符号的
在解析文本或其他格式的文件内容时,常常要碰到判定回车换行的地方,这个时候就要注意既要判定"\r\n"又要判定"\n"。
后向引用
我们使用正则表达式,在很多场景下的作用是为了查找和替换,大部分语言的正则表达式实现中,在查找时,使用后向引用来代表一个子模式,语法是"\数字",而在替换中,语法是"$数字"。在正则表达式中,我们可以使用 "\数字 " 来进行后向引用,数字 表示这里引用的是前面的第几个子模式。如下: \1代表前面的子模式([1-6])的匹配结果 1
举例
RegEx : <h([1-6])>.*?</h\1 >
<h1>This is a valid header</h1> 匹配
<h2>this is a valid</h3> 不匹配
子模式、获取匹配、非获取匹配
在使用正则表达式的时候,我们经常会使用()把某个部分括起来,称为一个子模式。
子模式有Capturing和Non-Capturing两种情况。
Capturing(获取匹配)
指获取匹配 ,是指系统会在幕后将所有的子模式匹配结果保存起来(最多保存9个),供我们查找或者替换。如后向引用的使用;
语法:(pattern)
获取匹配怎么用?
通常用来干啥?可以用于标签校验
Non-Capturing(非获取匹配)
语法:(?:pattern)
在子模式内部前面添加“?:” (?:),即为非获取匹配
而Non-Capturing指非获取匹配 ,这时系统并不会保存子模式的匹配结果,子模式的匹配更多的只是作为一种限制条件使用,如正向预查,反向预查,负正向预查,负反向预查等。
举例
正则:Windows (?:[\w]+\b)
匹配文本
Windows 95 and Windows 98 are the successor.Then Windows 2000 and Windows Xp appeared.Windows Vista is the Latest version of the family.
匹配结果(5处)
Windows 95
Windows 98
Windows 2000
Windows Xp
Windows Vista
[]
^在[]内第一个位置表示非
举例
正则
[^win].*
文本
Windows2008
匹配结果
dows2008
^在[]内中间位置,表示字母^本身
举例
正则
[w^n]
文本
adin^dows
匹配结果(3处)
n
^
w
^
w
{、}、(、)、?、*、+在[]出现表示匹配这些字符本身
分组
子主题
正向匹配、反向匹配
肯定匹配、否定匹配
正向肯定,正向否定,反向肯定,反向否定
数据结构
https://blog.csdn.net/YL3126/article/details/118141476
https://blog.csdn.net/weixin_43464964/article/details/124846086
linux命令
参见:https://www.linuxcool.com/
参见:https://www.linuxcool.com/
文本操作
查看文件内容
相关命令
cat
用途
连接所有指定文件并将结果写到标准输出
显示文件,可以连接多个文件形成新文件
语法
cat [选项] ... [文件]...
常用选项
-n 对输出内容中的所有行标注行号
-b 只对输出内容中的非空行标注行号
示例
1. cat > output.txt
会打开标准输入,用户输入信息后,按ctrl+D结束输入,之后将用户输入的内容,重定向到output.txt中
若output.txt不存在则创建该文件
2. cat >>output.txt
与1类似,不同之处在于这里是将内容追加到output.txt末尾,而1是将output.txt内容完全用用户输入的信息替换
3. cat test.txt - command.txt
先输出test.txt的内容 再输出标准输入的内容(输入完成后,以ctrl+D结束标准输入),再输出command.txt的内容
more & less
用途
more
分屏显示文件内容,只可向下翻屏
less
分屏显示文件内容,只可向上翻屏
语法
格式
more|less [选项] <文件>...
常用选项
-num 仅适用于more命令,每屏的行数
+num 从指定行开始显示
-c——从顶部清屏然后显示文件内容。
-N——仅适用于less命令,其作用是输出行号
交互操作方法
按Enter键向下逐行滚动
按空格键向下翻一屏、按b键向上翻一屏
文件末尾时more会自动退出,less 按q键退出
more 和 less的区别
1. less可以按键盘上下方向键显示上下内容,more不能通过上下方向键控制显示
2. less不必读整个文件,加载速度会比more更快
3. less退出后shell不会留下刚显示的内容,而more退出后会在shell上留下刚显示的内容
tail &head
用途
head
查看文件头部内容,默认前十行
tail
查看文件尾部内容,默认后十行
语法
格式
head | tail [选项] 文件名
常用选项
-num——指定需要显示文件多少行的内容,若不指定默认只显示十行
-f——使tail不停地去读取和显示文件最新的内容, 以监视文件内容的变化。这样有实时监视的效果。tail命令更多的用于查看系统日志文件,以便于观察重要的系统消息,特别是结合用-f选项,tail会自动实时地把打开文件中的新消息显示到屏幕上,从而跟踪日志文件末尾的内容变化,直至按【Ctrl+C】键终止显示和跟踪。
交互操作方法
按Enter键向下逐行滚动
按空格键向下翻一屏、按b键向上翻一屏
文件末尾时more会自动退出,less 按q键退出
举例
head|tail -3 aa.txt
显示aa.txt 前|后三行内容
查找与替换
文本内容查找
grep
用途
从文件或标准输入中,搜索匹配指定正则pattern的文本内容。
grep来自于英文词组“global search regular expression and print out the line”的缩写,意思是用于全面搜索的正则表达式,并将结果输出。人们通常会将grep命令与正则表达式搭配使用,参数作为搜索过程中的补充或对输出结果的筛选,命令模式十分灵活。
与之容易混淆的是egrep命令和fgrep命令。如果把grep命令当作是标准搜索命令,那么egrep则是扩展搜索命令,等价于“grep -E”命令,支持扩展的正则表达式。而fgrep则是快速搜索命令,等价于“grep -F”命令,不支持正则表达式,直接按照字符串内容进行匹配。
grep来自于英文词组“global search regular expression and print out the line”的缩写,意思是用于全面搜索的正则表达式,并将结果输出。人们通常会将grep命令与正则表达式搭配使用,参数作为搜索过程中的补充或对输出结果的筛选,命令模式十分灵活。
与之容易混淆的是egrep命令和fgrep命令。如果把grep命令当作是标准搜索命令,那么egrep则是扩展搜索命令,等价于“grep -E”命令,支持扩展的正则表达式。而fgrep则是快速搜索命令,等价于“grep -F”命令,不支持正则表达式,直接按照字符串内容进行匹配。
语法
grep [选项] pattern 文件
选项
-i 忽略大小写
-c 只输出匹配行的数量
-l 只列出符合匹配的文件名,不列出具体的匹配行
-n 列出所有的匹配行,显示行号
-h 查询多文件时不显示文件名
-s 不显示不存在、没有匹配文本的错误信息
-v 显示不包含匹配文本的所有行
-w 匹配整词
-x 匹配整行
-r 递归搜索
-q 禁止输出任何结果,已退出状态表示搜索是否成功
-b 打印匹配行距文件头部的偏移量,以字节为单位
-o 与-b结合使用,打印匹配的词据文件头部的偏移量,以字节为单位
-F 匹配固定字符串的内容
-E 支持扩展的正则表达式
举例
搜索某个文件(/etc/passwd)中,包含某个关键词(root)的内容
[root@linuxcool ~]# grep root /etc/passwd
root:x:0:0:root:/root:/bin/bash
operator:x:11:0:operator:/root:/sbin/nologin
root:x:0:0:root:/root:/bin/bash
operator:x:11:0:operator:/root:/sbin/nologin
搜索某个文件中,以某个关键词开头的内容
[root@linuxcool ~]# grep ^root /etc/passwd
root:x:0:0:root:/root:/bin/bash
root:x:0:0:root:/root:/bin/bash
搜索多个文件中,包含某个关键词的内容
[root@linuxcool ~]# grep linuxprobe /etc/passwd /etc/shadow
/etc/passwd:linuxprobe:x:1000:1000:linuxprobe:/home/linuxprobe:/bin/bash
/etc/shadow:linuxprobe:$6$9Av/41hCM17T2PrT$hoggWJ3J/j6IqEOSp62elhdOYPLhQ1qDho7hANcm5fQkPCQdib8KCWGdvxbRvDmqyOarKpWGxd8NAmp3j2Ln00::0:99999:7:::
/etc/passwd:linuxprobe:x:1000:1000:linuxprobe:/home/linuxprobe:/bin/bash
/etc/shadow:linuxprobe:$6$9Av/41hCM17T2PrT$hoggWJ3J/j6IqEOSp62elhdOYPLhQ1qDho7hANcm5fQkPCQdib8KCWGdvxbRvDmqyOarKpWGxd8NAmp3j2Ln00::0:99999:7:::
输出在某个(些)文件中,包含某个关键词行的数量
[root@linuxcool ~]# grep -c root /etc/passwd /etc/shadow
/etc/passwd:2
/etc/shadow:1
搜索某个文件中,不包含某个关键词的内容
[root@linuxcool ~]# grep -v nologin /etc/passwd
root:x:0:0:root:/root:/bin/bash
sync:x:5:0:sync:/sbin:/bin/sync
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
linuxprobe:x:1000:1000:linuxprobe:/home/linuxprobe:/bin/bash
搜索当前工作目录中,包含某个关键词内容的文件(-l 只列文件名),未找到则提示
[root@linuxcool ~]# grep -l root *
anaconda-ks.cfg
grep: Desktop: Is a directory
grep: Documents: Is a directory
grep: Downloads: Is a directory
initial-setup-ks.cfg
grep: Music: Is a directory
grep: Pictures: Is a directory
grep: Public: Is a directory
grep: Templates: Is a directory
grep: Videos: Is a directory
搜索当前工作目录中,包含某个关键词内容的文件(-l 只列文件名),未找到不提示(-s)
[root@linuxcool ~]# grep -sl root *
anaconda-ks.cfg
initial-setup-ks.cfg
anaconda-ks.cfg
initial-setup-ks.cfg
递归搜索,不仅搜索指定目录,还搜索其内子目录内是否有关键词文件
[root@linuxcool ~]# grep -srl root /etc
/etc/fstab
/etc/X11/xinit/Xclients
/etc/X11/xinit/xinitrc
/etc/libreport/events.d/collect_dnf.conf
/etc/libreport/events.d/bugzilla_anaconda_event.conf
/etc/libreport/forbidden_words.conf
/etc/fstab
/etc/X11/xinit/Xclients
/etc/X11/xinit/xinitrc
/etc/libreport/events.d/collect_dnf.conf
/etc/libreport/events.d/bugzilla_anaconda_event.conf
/etc/libreport/forbidden_words.conf
搜索某个文件中,精准匹配到某个关键词的内容(搜索词应与整行内容完全一样才会显示 -x,有别于一般搜索)
[root@linuxcool ~]# grep -x cd anaconda-ks.cfg
[root@linuxcool ~]# grep -x cdrom anaconda-ks.cfg
cdrom
[root@linuxcool ~]# grep -x cdrom anaconda-ks.cfg
cdrom
判断某个文件中,是否包含某个关键词,通过返回状态值输出结果(0为包含,1为不包含,-q),方便在Shell脚本中判断和调用
[root@linuxcool ~]# grep -q linuxprobe anaconda-ks.cfg
[root@linuxcool ~]# echo $?
0
[root@linuxcool ~]# grep -q linuxcool anaconda-ks.cfg
[root@linuxcool ~]# echo $?
1
搜索某个文件中,空行(正则表达式为^$)的数量
[root@linuxcool ~]# grep -c ^$ anaconda-ks.cfg
6
替换
sed
echo
用途
echo是用于在终端设备上输出指定字符串或变量提取后值的命令,能够给用户一些简单的提醒信息,也可以将输出的指定字符串内容同管道符一起传递给后续命令作为标准输入信息再来进行二次处理,又或者同输出重定向符一起操作,将信息直接写入到文件中。
如需提取变量值,需在变量名称前加入$符号做提取,变量名称一般均为大写形式。
如需提取变量值,需在变量名称前加入$符号做提取,变量名称一般均为大写形式。
语法
格式
echo [参数] 字符串/变量
常用选项
-n 不输出结尾的换行符
-e “\a” 发出警告音
-e “\b” 删除前面的一个字符
-e “\c” 结尾不加换行符
-e “\f” 换行,光标扔停留在原来的坐标位置
-e “\n” 换行,光标移至行首
-e “\r” 光标移至行首,但不换行
-E 禁止反斜杠转移,与-e参数功能相反
—version 查看版本信息
--help 查看帮助信息
举例
输出指定字符串到终端设备界面(默认为电脑屏幕)
[root@linuxcool ~]# echo LinuxCool
LinuxCool
输出某个变量值内容
[root@linuxcool ~]# echo $PATH
/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/root/bin
搭配转义符一起使用,输出纯字符串内容
[root@linuxcool ~]# echo \$PATH
$PATH
搭配输出重定向符一起使用,将字符串内容直接写入文件中
[root@linuxcool ~]# echo "Hello World" > Document
搭配反引号执行命令,并将执行结果输出
[root@linuxcool ~]# echo `uptime`
16:16:12 up 52 min, 1 user, load average: 0.00, 0.00, 0.00
输出带有换行符的内容
[root@linuxcool ~]# echo -e "First\nSecond\nThird"
First
Second
Third
指定删除字符串中某些字符,随后将内容输出
[root@linuxcool ~]# echo -e "123\b456"
12456
echo
cut
join
sort
fmt
wc
打印
lpr
lprm
lpq
cancel
pr
文件操作
查找
find
用途
find命令的功能是根据给定的路径和条件查找相关文件或目录,可以使用的参数很多,并且支持正则表达式,结合管道符后能够实现更加复杂的功能,是系统管理员和普通用户日常工作必须掌握的命令之一。
find命令通常进行的是从根目录(/)开始的全盘搜索,有别于whereis、which、locate……等等的有条件或部分文件的搜索。对于服务器负载较高的情况,建议不要在高峰时期使用find命令的模糊搜索,会相对消耗较多的系统资源。
find命令通常进行的是从根目录(/)开始的全盘搜索,有别于whereis、which、locate……等等的有条件或部分文件的搜索。对于服务器负载较高的情况,建议不要在高峰时期使用find命令的模糊搜索,会相对消耗较多的系统资源。
语法
find path -option [ -print ] [ -exec -ok command ] {} \;
find <指定目录> <指定条件> <指定动作>
- <指定目录>: 所要搜索的目录及其所有子目录。默认为当前目录。
- <指定条件>: 所要搜索的文件的特征。
- <指定动作>: 对搜索结果进行特定的处理。
- <指定条件>: 所要搜索的文件的特征。
- <指定动作>: 对搜索结果进行特定的处理。
-name 匹配名称
-perm 匹配权限(mode为完全匹配,-mode为包含即可)
-user 匹配所有者
-group 匹配所有组
-mtime -n +n 匹配修改内容的时间(-n指n天以内,+n指n天以前)
-atime -n +n 匹配访问文件的时间(-n指n天以内,+n指n天以前)
-ctime -n +n 匹配修改文件权限的时间(-n指n天以内,+n指n天以前)
-nouser 匹配无所有者的文件
-nogroup 匹配无所有组的文件
-newer f1 !f2 匹配比文件f1新但比f2旧的文件
-type b/d/c/p/l/f 匹配文件类型(后面的字幕字母依次表示块设备、目录、字符设备、管道、链接文件、文本文件)
-size 匹配文件的大小(+50KB为查找超过50KB的文件,而-50KB为查找小于50KB的文件)
-prune 忽略某个目录
-exec …… {}\; 后面可跟用于进一步处理搜索结果的命令
-perm 匹配权限(mode为完全匹配,-mode为包含即可)
-user 匹配所有者
-group 匹配所有组
-mtime -n +n 匹配修改内容的时间(-n指n天以内,+n指n天以前)
-atime -n +n 匹配访问文件的时间(-n指n天以内,+n指n天以前)
-ctime -n +n 匹配修改文件权限的时间(-n指n天以内,+n指n天以前)
-nouser 匹配无所有者的文件
-nogroup 匹配无所有组的文件
-newer f1 !f2 匹配比文件f1新但比f2旧的文件
-type b/d/c/p/l/f 匹配文件类型(后面的字幕字母依次表示块设备、目录、字符设备、管道、链接文件、文本文件)
-size 匹配文件的大小(+50KB为查找超过50KB的文件,而-50KB为查找小于50KB的文件)
-prune 忽略某个目录
-exec …… {}\; 后面可跟用于进一步处理搜索结果的命令
常用选项
-type #根据文件类型 find /var/log -type f -name "*.log" ;find /var/log -type d
-name #根据文件名 find /var/log -type f -name "*.log"
-perm #根据文件权限 find /var/log -perm 600 -type f -name "*.log"
-user #根据文件所属主 find /var/log -user XD
-name #根据文件名 find /var/log -type f -name "*.log"
-perm #根据文件权限 find /var/log -perm 600 -type f -name "*.log"
-user #根据文件所属主 find /var/log -user XD
举例
find /etc
列出/etc及子目录下的所有文件和目录
find . -name 'my*'
搜索当前目录(含子目录,以下同)中,所有文件名以my开头的文件。
find . -type f -mmin -10
搜索当前目录中,所有过去10分钟中更新过的普通文件。如果不加-type f参数,则搜索普通文件+特殊文件+目录。
find /etc -name "*.log"
搜索/etc目录下的.log文件
全盘搜索系统中所有以.conf结尾的文件
[root@linuxcool ~]# find / -name *.conf
/run/tmpfiles.d/kmod.conf
/etc/resolv.conf
/etc/dnf/dnf.conf
/etc/dnf/plugins/copr.conf
/etc/dnf/plugins/debuginfo-install.conf
/etc/dnf/plugins/product-id.conf
/etc/dnf/plugins/subscription-manager.conf
/run/tmpfiles.d/kmod.conf
/etc/resolv.conf
/etc/dnf/dnf.conf
/etc/dnf/plugins/copr.conf
/etc/dnf/plugins/debuginfo-install.conf
/etc/dnf/plugins/product-id.conf
/etc/dnf/plugins/subscription-manager.conf
在/etc目录中搜索所有大约1M大小的文件
[root@linuxcool ~]# find /etc -size +1M
/etc/selinux/targeted/policy/policy.31
/etc/udev/hwdb.bin
/etc/selinux/targeted/policy/policy.31
/etc/udev/hwdb.bin
在/home目录中搜索所有属于指定用户的文件
[root@linuxcool ~]# find /home -user linuxprobe
/home/linuxprobe
/home/linuxprobe/.mozilla
/home/linuxprobe/.mozilla/extensions
/home/linuxprobe/.mozilla/plugins
/home/linuxprobe/.bash_logout
/home/linuxprobe/.bash_profile
/home/linuxprobe/.bashrc
/home/linuxprobe
/home/linuxprobe/.mozilla
/home/linuxprobe/.mozilla/extensions
/home/linuxprobe/.mozilla/plugins
/home/linuxprobe/.bash_logout
/home/linuxprobe/.bash_profile
/home/linuxprobe/.bashrc
列出当前工作目录中的所有文件、目录以及子文件信息
[root@linuxcool ~]# find .
.
./.bash_logout
./.bash_profile
./.bashrc
./.cshrc
./.tcshrc
./anaconda-ks.cfg
.
./.bash_logout
./.bash_profile
./.bashrc
./.cshrc
./.tcshrc
./anaconda-ks.cfg
在/var/log目录下搜索所有指定后缀的文件,后缀不需要大小写。
[root@linuxcool ~]# find /var/log -iname "*.log"
/var/log/audit/audit.log
/var/log/rhsm/rhsmcertd.log
/var/log/rhsm/rhsm.log
/var/log/sssd/sssd.log
/var/log/sssd/sssd_implicit_files.log
/var/log/sssd/sssd_nss.log
/var/log/audit/audit.log
/var/log/rhsm/rhsmcertd.log
/var/log/rhsm/rhsm.log
/var/log/sssd/sssd.log
/var/log/sssd/sssd_implicit_files.log
/var/log/sssd/sssd_nss.log
在/var/log目录下搜索所有后缀不是.log的文件
[root@linuxcool ~]# find /var/log ! -name "*.log"
/var/log
/var/log/lastlog
/var/log/README
/var/log
/var/log/lastlog
/var/log/README
搜索当前工作目录中的所有近7天被修改过的文件
[root@linuxcool ~]# find . -mtime +7
./.bash_logout
./.bash_profile
./.bash_logout
./.bash_profile
全盘搜索系统中所有类型为目录,且权限为1777的目录文件
[root@linuxcool ~]# find / -type d -perm 1777
/dev/mqueue
/dev/shm
/var/tmp
/dev/mqueue
/dev/shm
/var/tmp
全盘搜索系统中所有类型为普通文件,且可以执行的文件信息
[root@linuxcool ~]# find / -type f -perm /a=x
/boot/vmlinuz-4.18.0-80.el8.x86_64
/boot/vmlinuz-0-rescue-c8b04558503242459d908c6c22a2d481
/etc/X11/xinit/xinitrc.d/50-systemd-user.sh
/boot/vmlinuz-4.18.0-80.el8.x86_64
/boot/vmlinuz-0-rescue-c8b04558503242459d908c6c22a2d481
/etc/X11/xinit/xinitrc.d/50-systemd-user.sh
全盘搜索系统中所有后缀为.mp4的文件,并删除所有查找到的文件
[root@linuxcool ~]# find / -name "*.mp4" -exec rm -rf {} \;
whereis
locate
which
type
替换
系统管理
uname 显示系统内核信息
用途
uname命令来自于英文词组”Unix name“的缩写,其功能是用于查看系统主机名、内核及硬件架构等信息。如果不加任何参数,默认仅显示系统内核名称,相当于-s参数。
语法
uname [参数]
-a 显示系统所有相关信息
-m 显示计算机硬件架构
-n 显示主机名称
-r 显示内核发行版本号
-s 显示内核名称
-v 显示内核版本
-p 显示主机处理器类型
-o 显示操作系统名称
-i 显示硬件平台
-m 显示计算机硬件架构
-n 显示主机名称
-r 显示内核发行版本号
-s 显示内核名称
-v 显示内核版本
-p 显示主机处理器类型
-o 显示操作系统名称
-i 显示硬件平台
举例
显示系统内核名称
[root@linuxcool ~]# uname
Linux
Linux
显示系统所有相关信息(含内核名称、主机名、版本号及硬件架构)
[root@linuxcool ~]# uname -a
Linux linuxcool.com 4.18.0-80.el8.x86_64 #1 SMP Wed Mar 13 12:02:46 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
Linux linuxcool.com 4.18.0-80.el8.x86_64 #1 SMP Wed Mar 13 12:02:46 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
显示系统内核版本号
[root@linuxcool ~]# uname -r
4.18.0-80.el8.x86_64
4.18.0-80.el8.x86_64
现在系统硬件架构
[root@linuxcool ~]# uname -i
x86_64
etstat命令 – 显示网络状态
用途
netstat命令来自于英文词组”network statistics“的缩写,其功能是用于显示各种网络相关信息,例如网络连接状态、路由表信息、接口状态、NAT、多播成员等等。
netstat命令不仅应用于Linux系统,而且在Windows XP、Windows 7、Windows 10及Windows 11中均已默认支持,并且可用参数也相同,有经验的运维人员可以直接上手
netstat命令不仅应用于Linux系统,而且在Windows XP、Windows 7、Windows 10及Windows 11中均已默认支持,并且可用参数也相同,有经验的运维人员可以直接上手
语法
格式
netstat [参数]
常用选项
-a 显示所有连线中的Socket
-p 显示正在使用Socket的程序识别码和程序名称
-l 仅列出在监听的服务状态
-t 显示TCP传输协议的连线状况
-u 显示UDP传输协议的连线状况
-i 显示网络界面信息表单
-r 显示路由表信息
-n 直接使用IP地址,不通过域名服务器
-p 显示正在使用Socket的程序识别码和程序名称
-l 仅列出在监听的服务状态
-t 显示TCP传输协议的连线状况
-u 显示UDP传输协议的连线状况
-i 显示网络界面信息表单
-r 显示路由表信息
-n 直接使用IP地址,不通过域名服务器
举例
显示系统网络状态中的所有连接信息
[root@linuxcool ~]# netstat -a
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 0.0.0.0:http 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:https 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:ms-wbt-server 0.0.0.0:* LISTEN
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 0.0.0.0:http 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:https 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:ms-wbt-server 0.0.0.0:* LISTEN
显示系统网络状态中的UDP连接信息
[root@linuxcool ~]# netstat -nu
Active Internet connections (w/o servers)
Proto Recv-Q Send-Q Local Address Foreign Address State
udp 0 0 172.19.226.238:68 172.19.239.253:67 ESTABLISHED
显示系统网络状态中的UDP连接端口号使用信息
[root@linuxcool ~]# netstat -apu
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
udp 0 0 linuxcool:bootpc _gateway:bootps ESTABLISHED 1024/NetworkManager
udp 0 0 localhost:323 0.0.0.0:* 875/chronyd
udp6 0 0 localhost:323 [::]:* 875/chronyd
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
udp 0 0 linuxcool:bootpc _gateway:bootps ESTABLISHED 1024/NetworkManager
udp 0 0 localhost:323 0.0.0.0:* 875/chronyd
udp6 0 0 localhost:323 [::]:* 875/chronyd
显示网卡当前状态信息
[root@linuxcool~]# netstat -i
Kernel Interface table
Iface MTU RX-OK RX-ERR RX-DRP RX-OVR TX-OK TX-ERR TX-DRP TX-OVR Flg
eth0 1500 31945 0 0 0 39499 0 0 0 BMRU
lo 65536 0 0 0 0 0 0 0 0 LRU
Kernel Interface table
Iface MTU RX-OK RX-ERR RX-DRP RX-OVR TX-OK TX-ERR TX-DRP TX-OVR Flg
eth0 1500 31945 0 0 0 39499 0 0 0 BMRU
lo 65536 0 0 0 0 0 0 0 0 LRU
显示网络路由表状态信息
[root@linuxcool ~]# netstat -r
Kernel IP routing table
Destination Gateway Genmask Flags MSS Window irtt Iface
default _gateway 0.0.0.0 UG 0 0 0 eth0
172.19.224.0 0.0.0.0 255.255.240.0 U 0 0 0 eth0
找到某个服务所对应的连接信息
[root@linuxcool ~]# netstat -ap | grep ssh
unix 2 [ ] STREAM CONNECTED 89121805 203890/sshd: root [
unix 3 [ ] STREAM CONNECTED 27396 1754/sshd
unix 3 [ ] STREAM CONNECTED 89120965 203890/sshd: root [
unix 2 [ ] STREAM CONNECTED 89116510 203903/sshd: root@p
unix 2 [ ] STREAM CONNECTED 89121803 203890/sshd: root [
unix 2 [ ] STREAM CONNECTED 29959 1754/sshd
unix 2 [ ] DGRAM 89111175 203890/sshd: root [
unix 3 [ ] STREAM CONNECTED 89120964 203903/sshd: root@p
unix 2 [ ] STREAM CONNECTED 89121805 203890/sshd: root [
unix 3 [ ] STREAM CONNECTED 27396 1754/sshd
unix 3 [ ] STREAM CONNECTED 89120965 203890/sshd: root [
unix 2 [ ] STREAM CONNECTED 89116510 203903/sshd: root@p
unix 2 [ ] STREAM CONNECTED 89121803 203890/sshd: root [
unix 2 [ ] STREAM CONNECTED 29959 1754/sshd
unix 2 [ ] DGRAM 89111175 203890/sshd: root [
unix 3 [ ] STREAM CONNECTED 89120964 203903/sshd: root@p
more
用途
子主题
语法
格式
常用选项
举例
JDK
JDK各版本演进及新功能
java对象内存模型:
主要分为两部分、5大区域
所有线程共有
堆
新生代 ( Young )
Eden
From Survivor (Survivor0 , S0)
To Survivor(Survivor1,S1)
老年代 ( Old )
方法区
VM1.8中,图中的 方法区为元数据区
用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据
线程私有
虚拟机栈
每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的Java方法调用。
虚拟机栈的作用:主管Java程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用和返回。
每个方法被执行的时候都会创建一个”栈帧”,用于存储局部变量表(包括参数)、操作栈、方法出口等信息。
每个方法被调用到执行完的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
栈帧(Stack Frame) 是用于虚拟机执行时方法调用和方法执行时的数据结构,它是虚拟栈的基本元素,栈帧由局部变量区、操作数栈等组成,如下图所示:
每一个方法从调用到方法返回都对应着一个栈帧入栈出栈的过程。最顶部的栈帧称为当前栈帧,栈帧所关联的方法称为当前方法,定义这个方法的类称为当前类,该线程中,虚拟机有且也只会对当前栈帧进行操作。
本地方法栈
栈帧的作用有存储数据,部分过程结果,处理动态链接,方法返回值和异常分派。
程序计数器
在JVM的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
程序计数器占用的内存空间很少,也是唯一一个在JVM规范中没有规定任何OutOfMemoryError(内存不足错误)的区域。
参考
https://blog.csdn.net/m0_71777195/article/details/125819073?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522166615991416800186577080%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id=166615991416800186577080&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~blog~first_rank_ecpm_v1~rank_v31_ecpm-1-125819073-null-null.nonecase&utm_term=jvm&spm=1018.2226.3001.4450
垃圾回收
垃圾回收核心算法
如何确定对象是否可以被回收? 两种方案
引用计数法
行不通
如果两个对象互相引用,那么就意味着两个对象永久性的无法被回收?对象无法被回收,显然这种方法时不行的
可达性分析
推荐
垃圾回收算法
标记清除算法-[marked-sweep]
思路
1. 标记
2. 清除
问题
这种回收算法:因为需要遍历所有的对象,所以是比较耗时的
标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程
序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
(1)标记和清除两个过程都比较耗时,效率不高
(2)会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存
而不得不提前触发另一次垃圾收集动作。
序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
(1)标记和清除两个过程都比较耗时,效率不高
(2)会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存
而不得不提前触发另一次垃圾收集动作。
解决方案
复制算法
复制算法
思路
将内存分为大小相等的两块,每次我们只使用其中一块
当前正在使用的这一块使用完了之后,就会把LiveObject 对象移动到另外一半上,其他的被回收掉
子主题
好处
不会产生内存碎片
问题
有一半的内存空间l浪费
解决办法
分代垃圾收集
标记整理算法(Mark Compact)
思路
标记过程仍然与"标记-清除"算法一样,但是后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,
然后直接清理掉端边界以外的内存
然后直接清理掉端边界以外的内存
最终JVM采用算法:分代
为什么分代
在Java程序运行的过程中,会产生大量的对象
但不同的对象的生命周期是不一样的
有些对象生命周期比较长,短期内不会死亡
如Http请求中的Session对象、线程、Socket连接,这类是与业务信息相关的对象
有些对象生命周期短,很快就会死亡而希望被快速回收
主要是程序运行过程中生成的临时变量,这些对象生命周期会比较短,比如:String对象,由于其不变类的特性,系统会产生大量的这些对象,有些对象甚至只用一次即可回收
若根据对象存活时间进行分代,则每次垃圾回收都是对整个堆空间进行回收,花费时间相对会长,并且每次回收都需要遍历所有存活对象,但实际上,对于生命周期长的对象而言,这种遍历是没有意义的,因为可能进行了很多次遍历,但是他们依旧存在。
因此,分代垃圾回收采用分治的思想,进行代的划分,把不同生命周期的对象放在不同代上,不同代上采用最适合它的垃圾回收方式进行回收
分代实现:将JVM分为两个区域
新生代(Yong区)
存放生命周期短,很快就会死亡而希望被快速回收的对象
特点
新对象都在新生代内创建
程序运行过程中,对象创建非常频繁,因此新生代内存分配非常频繁
绝大多数对象的生命周期很短
造成新生代会在短期内生成大量的垃圾对象
新生代绝大多数为垃圾对象,这些垃圾对象希望能被快速回收,以为后续新对象腾出空间,因此内存回收也非常频繁
根据上述特点,对这块区域的要求
1. 能快速创建大量对象
2. 频繁创建的垃圾对象能够被快速回收
3. 回收频率和效率都要很高,并且希望每次回收后尽量减少甚至不希望有内存碎片(对象创建销毁太频繁,若碎片太多可能找不到足够连续的空间分配给新对象)
回收算法
复制算法
原因:
新生代里绝大部分都是垃圾对象,可以使用复制算法将小部分存活对象复制到另一个区域,然后留下来的都是垃圾对象,可以一网打尽,一下子清除掉。因为存活的对象少,所以“复制”的次数少;虽然留下来的垃圾对象多,但是可以一网打尽,所以“删除”的次数也少,速度非常快,并且可以频繁执行回收操作。
就像你有一个文件夹,里面有 1000 张图片,其中只有 3 张是有用的,你要删除余下的 997 张垃圾图片,你会怎么删除呢?你肯定不会一张一张的去删除 997 张垃圾图片,这样需要删除 997 次,你显然认为把 3 张有用的图片复制出去,然后将整个文件夹干掉来的快。这也就是为什么新生代使用复制算法合适、使用标记清除算法不合适。
另外,也不会产生碎片,满足新生代的内存要求
要求
复制算法要求新生代内存不能太高,否则复制会比较慢
回收机制
将新生代分为三部分,比例默认为8:1:1
Eden (8)
新对象在此区被创建
在Edge区经历一次Minor GC之后,仍然存活的对象就会被放入S0区
From Survivor (又叫Survivor0 , S0, 1)
复制算法的对象复制区1
To Survivor(又叫Survivor1,S1, 1)
复制算法的对象复制区2
为什么要分为Eden和Survivor?
1.如果没有Survivor,Eden区,每进行一次Minor GC(也叫YGC),存活的对象就会被送到老年代。老年代很快被 填满,触发Major GC.老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC 长得多,所以需要分为Eden和Survivor
2.Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历16次Minor GC还能在新生代中存活的对象,才会被送到老年代。
新生代回收过程
参见:https://blog.csdn.net/Student_xx/article/details/121746854
可能不准确,因为没有涉及From Survivor区回收。From Survivor区在Minor GC时,不存活对象也需要被回收。
结论
新生代GC非常频繁
Minor GC时,会把Edge区和From Survivor区的存活对象,复制到To Survivor区,然后From Survivor和To Survivor区互换
时刻保持两个Survivor区有一个是空闲的,以用于下次GC时作为To Survivor区
大部分 Eden 区中的对象都能被认为是垃圾,永远也不会被复制到 Survivor 区或者老年代空间
因为这些对象大都是临时对象,生命周期非常短,在Edge区就死亡了,变成了垃圾对象
整个新生代(包括Edge区和 Survivor 区)都采用复制算法,因此不会产生任何碎片
Edge区满后,对象会被复制到S0(或S1),之后指针会从0开始跟踪内存,原有对象占用的内存空间直接被覆盖即可
从S0(或S1)到S1(或S0)的对象复制同样如此,因此不会产生任何碎片
执行 Minor GC 操作时,不会影响到永久代
出现了 Major GC,通常会伴随至少一次的 Minor GC ,MajorGC 的速度一般会比 Minor GC 慢 10倍以上
在垃圾回收过程中由于要移动对象,需要对对象引用进行更新,为了保证引用更新的正确性,Java将暂停所有其他的线程,这种情况被称为“Stop-The-World”,导致系统全局停顿。
垃圾收集后,若Survivor及old区仍然无法存放从Eden复制过来的部分对象,导致JVM无法在Eden区为新对象创建内存区域。则出现Out of memory。
这种方式分配内存和清理内存的效率都极高
这不代表着停止复制清理法很高效,其实,它也只在大部分对象存活周期很短的事实情况下高效
如果在老年代采用停止复制,则是非常不合适的
为什么要设置两个Survivor区?
设置两个Survivor区最大的好处就是解决了碎片化,刚刚新建的对象在Eden中,经历一次Minor GC,Eden中的存活对象就会被移动到第一块survivor space S0,Eden被清空;等Eden区再满 了,就再触发一次Minor GC,Eden和S0中的存活对象又会被复制送入第二块survivor space S1(这个过程非常重要,因为这种复制算法保证了S1中来自S0和Eden两部分的存活对象占用连续 的内存空间,避免了碎片化的发生)
老年代(Old区)
存放生命周期长,很长时间都不会死亡的对象
对象在新生代被创建后,若其生命周期比较长,就会被移动到老年代,因此老年代是存放生命周期比较长的对象的区域,对象回收周期比较长,不会被频繁GC,当老年代内存不足时,会触发Full GC
当老年代满时会触发MajorGC,只有CMS收集器会有单独收集老年代的行为,其他收集器均为Full GC。而针对新生代的Minor GC,各个收集器均支持。总之,单独发生收集行为的只有新生代,除了CMS收集器,都不支持老年代单独GC。
垃圾回收策略
只用于新生代
Serial
单线程
采用串行单线程方式完成GC任务,其中“Stop The World"简称STW,即垃圾回收的某个阶段会暂停整个应用程序的执行
F-GC的时间相对较长,频繁FGC会严重影响应用程序的性能。
F-GC的时间相对较长,频繁FGC会严重影响应用程序的性能。
为何会有 STW
除了清除,还要做压缩。怎么才能标记和清除清楚上百万对象呢?
答案就是STW,让全世界停止下来。
答案就是STW,让全世界停止下来。
只会使用一个CPU或一条GC线程进行垃圾回收,并且在垃圾回收过程中暂停其他所有的工作线程,从而用户的请求或图形化界面会出现卡顿.
适合Client模式
一般客户端应用所需内存较小,不会创建太多的对象,而且堆内存不大,因此垃圾回收时间比较短,即使在这段时间停止一切用户线程,也不会感到明显停顿.
简单高效
由于Serial收集器只有一条GC线程,避免了线程切换的开销.
采用"复制"算法
ParNew
是Serial的多线程版本
多线程并行执行
ParNew由多条GC线程并行地进行垃圾清理.但清理过程仍然需要STW.但由于有多条GC线程同时清理,清理速度比Serial有一定的提升.
默认开启的收集线程数与CPU数量相同.
适合多CPU的服务器环境
由于使用了多线程,因此适合CPU较多的服务器环境
采用"复制"算法
追求"降低停顿时间"
和Serial相比,ParNew使用多线程的目的就是缩短垃圾收集时间,从而减少用户线程被停顿的时间.
适合交互式应用,
良好的反应速度提升用户体验.
与Serial性能对比
ParNew和Serial唯一区别就是使用了多线程进行垃圾回收,在多CPU的环境下性能比Serial会有一定程度的提升;但线程切换需要额外的开销,因此在单CPU环境中表现不如Serial,双CPU环境也不一定就比Serial高效.
Parallel Scavenge
Parallel Scavenge和ParNew一样都是并行的多线程、新生代收集器,都使用"复制"算法进行垃圾回收.但它们有个巨大不同点
ParNew收集器追求降低GC时用户线程的停顿时间,适合交互式应用,良好的反应速度提升用户体验.
Parallel Scavenge追求CPU吞吐量,能够在较短的时间内完成指定任务,因此适合不需要太多交互的后台运算.
追求用户线程CPU吞吐量
吞吐量是指用户线程运行时间占CPU总时间的比例.
CPU总时间包括 : 用户线程运行时间 和 GC线程运行的时间.
吞吐量 = 运行用户代码时间/(运行用户代码时间+垃圾收集时间)
因此,吞吐量越高表示用户线程(不一定是交互式,可以是批处理任务)运行时间越长,从而用户线程能够被快速处理完.
降低停顿时间的两种方式
1.在多CPU环境中使用多条GC线程,从而垃圾回收的时间减少,从而用户线程停顿的时间也减少;
2.实现GC线程与用户线程并发执行。所谓并发,就是用户线程与GC线程交替执行,从而每次停顿的时间会减少,用户感受到的停顿感降低,但线程之间不断切换意味着需要额外的开销,从而垃圾回收和用户线程的总时间将会延长。
适合后台大量运算而不需要太多用户交互的场景
Parallel Scavenge提供的参数
-XX:GCTimeRadio
直接设置吞吐量大小,是一个大于0小于100的整数,也就是程序运行时间占总时间的比率,默认值是99,即垃圾收集运行最大1% =(1/(1+99))的垃圾收集时间
-XX:MaxGCPauseMillis
设置最大GC停顿时间.
Parallel Scavenge会根据这个值的大小确定新生代的大小.如果这个值越小,新生代就会越小,从而收集器就能以较短的时间进行一次回收;但新生代变小后,回收的频率就会提高,吞吐量也降下来了,因此要合理控制这个值.
设置最大GC停顿时间.
Parallel Scavenge会根据这个值的大小确定新生代的大小.如果这个值越小,新生代就会越小,从而收集器就能以较短的时间进行一次回收;但新生代变小后,回收的频率就会提高,吞吐量也降下来了,因此要合理控制这个值.
-XX:+UseAdaptiveSizePolicy
开启GC 自适应的调节策略(区别于ParNew).
新生代晋升年老代对象年龄(-XX:PretenureSizeThreshold)等细节参数,虚拟机会根据当前系统运行情况收集性能监控信息,动态调整这些参数以达到最大吞吐量,这种方式称为GC自适应调节策略,自适应调节策略也是ParallelScavenge收集器与ParNew收集器的一个重要区别。
这是个开关参数,打开之后就不需要手动指定新生代大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRation)
只要设置最大堆(-Xmx)和MaxGCPauseMillis或GCTimeRadio,收集器会自动调整新生代的大小、Eden和Survior的比例、对象进入老年代的年龄,以最大程度上接近我们设置的MaxGCPauseMillis或GCTimeRadio.
不能与CMS一起使用
HotSpot VM里多个GC有部分共享的代码。有一个分代式GC框架,Serial/Serial Old/ParNew/CMS都在这个框架内;在该框架内的young collector和old collector可以任意搭配使用,所谓的“mix-and-match”。
而ParallelScavenge(与G1)则不在这个框架内,而是各自采用了自己特别的框架,所以不能跟使用了那个框架的CMS搭配使用。
只用于老生代
Serial Old
Serial的老年代版本,它们都是单线程收集器,也就是垃圾收集时只启动一条GC线程,因此都适合客户端应用.
使用"标记-整理"算法
与Serial唯一的区别就是Serial Old工作在老年代,使用"标记-整理"算法;而Serial工作在新生代,使用"复制"算法.
Parallel Old
Parallel Scavenge的老年代版本,一般它们搭配使用,追求CPU吞吐量
它们在垃圾收集时都是由多条GC线程并行执行,并暂停一切用户线程
使用"标记-整理"算法.
追求用户线程CPU吞吐量
由于在GC过程中没有使垃圾收集和用户线程并行执行,因此它们是追求吞吐量的垃圾收集器.
CMS ((Concurrent Mark Sweep))
一种追求最短停顿时间的收集器,它在垃圾收集时使得用户线程和GC线程并发执行,因此在GC过程中用户也不会感受到明显卡顿。但用户线程和GC线程之间不停地切换会有额外的开销,因此垃圾回收总时间就会被延长。
追求"降低停顿时间" (与新生代ParNew目标相同)
回收停顿时间比较短,对许多应用来说,快速响应比端到端的吞吐量更为重要。
尽可能并发执行,每个 GC 周期只有2次短的停顿。
使用"标记-清除"算法
缺点
吞吐量低
由于CMS在垃圾收集过程使用用户线程和GC线程并行执行,从而线程切换会有额外开销,因此CPU吞吐量就不如在GC过程中停止一切用户线程的方式来的高。
无法处理浮动垃圾,导致频繁Full GC
由于垃圾清除过程中,用户线程和GC线程并发执行,即用户线程仍在执行,则在执行过程中会产生垃圾,这些垃圾称为"浮动垃圾"。
如果CMS在垃圾清理过程中,用户线程需要在老年代中分配内存时发现空间不足,就需再次发起Full GC,而此时CMS正在进行清除工作,因此此时只能由Serial Old临时对老年代进行一次Full GC。
如果CMS在垃圾清理过程中,用户线程需要在老年代中分配内存时发现空间不足,就需再次发起Full GC,而此时CMS正在进行清除工作,因此此时只能由Serial Old临时对老年代进行一次Full GC。
使用"标记-清除"算法产生碎片空间
由于CMS采用的是“标记-清除算法",因此产生大量的空间碎片,不利于空间利用率。
解决办法
为了解决这个问题,CMS可以通过配置-XX:+UseCMSCompactAtFullCollection强制JVM在FGC完成后対老年代进行压缩,执行一次空间碎片整理,但空间碎片整理阶段也会引发STW。为了减少STW次数,CMS还可以通过配置-XX:+CMSFullGCsBeforeCompaction=n在执行了n次FGC后,JVM再在老年代执行空间碎片整理。在并发收集失败的情况下,Java虚拟机会使用其他两个压缩型垃圾回收器进行一次垃圾回收。
漏标问题
无法解决,因此出现了G1,同时CMS被废弃
CMS在Java 9中已被废弃
由于G1的出现,CMS在Java 9中已被废弃。
不区分新老生代
G1(Garbage First)
概述
是一个横跨新生代和老年代的垃圾回收器。实际上,它已经打乱了前面所说的堆结构,直接将堆分成极其多个区域。每个区域都可以充当Eden区、Survivor区或者老年代中的一个。它采用的是标记-压缩算法,而且和CMS一样都能够在应用程序运行过程中并发地进行垃圾回收。
虽然G1依然有分代内存划分,但抛弃了连续的分代,他们可以是一些不连续的Region集合,正因为这样,每一个Region区域的功能会发生变化,比如一个Region之前按是年轻代,在做完垃圾回收之后又变成了老年代。
G1能够针对每个细分的区域来进行垃圾回收。在每次GC发生时会根据「用户指定的期望停顿时间或默认的期望停顿时间」,优先从列表中选择「回收价值最大(优先回收死亡对象较多的区域)」Region区回收,这也是G1名字的由来。
和CMS相比,Gl具备压缩功能,能避免碎片向題,G1的暂停时间更加可控。性能总体还是非常不错的,G1是当今最前沿的垃圾收集器成果之一
可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收
特点
追求停顿时间
多线程GC
面向服务端应用
不会产生内存空间碎片.
可对整个堆进行垃圾回收
并行与并发
G1能充分利用CPU、多核的环境优势来缩短STW停顿时间,部分其他收集器需要STW的区域,G1也可以并发执行。
分代收集
虽然G1去掉了连续内存空间分代的概念,也不需要其他收集器配合使用,但分代收集依然存在
空间整合
整体来看基于标记-整理,局部来看基于复制算法合并
可预测的停顿时间
用户可通过-XX:MaxGCPauseMillis参数设置最大停顿时间,拥有良好的用户体验,但是这个参数不可随意设置,不能设置的太小,否则每一次minor gc时间过短,收集的垃圾太少,容易触发full gc
内存模型
没有分代概念,而是将Java堆划分为一块块独立的大小相等的独立内存块Region.当要进行垃圾收集时,首先估计每个Region中的垃圾数量,每次都从垃圾回收价值最大的Region开始回收,因此可以获得最大的回收效率.
G1将Java堆空间分割成了许多相同大小的区域,即region
包括四类Region
Eden
Survivor
Old
Humongous
是特殊的Old类型,专门放置大型对象.
专门用于存放巨型对象,如果一个对象的大小超过Region容量的50%以上,G1 就认为这是个巨型对象。在其他垃圾收集器中,这些巨型对象默认会被分配在老年代,但如果它是一个短期存活的巨型对象,放入老年代就会对垃圾收集器造成负面影响,触发老年代频繁GC。为了解决这个问题,G1划分了一个H区专门存放巨型对象,如果一个H区装不下巨型对象,那么G1会寻找连续的H分区来存储,如果寻找不到连续的H区的话,就不得不启动 Full GC 了。
每个Region是连续的一段内存,具体大小根据堆的实际大小而定,整体被控制在 1M - 32M 之间,且为2的N次幂(1M、2M、4M、8M、16M和32M)
这样的划分方式意味着不需要一个连续的内存空间管理对象.
JVM最多可以有2048个Region,可以用参数-XX:G1HeapRegionSize指定Region的大小,一般不推荐指定
可达性分析快,不需要扫描整个堆内存
一个对象和它内部所引用的对象可能不在同一个Region中
每个Region都有一个Remembered Set,用于记录本区域中所有对象引用的对象所在的区域
Remember Set
在串行和并行收集器中,GC时是通过整堆扫描来确定对象是否处于可达路径中
G1为了避免STW式的整堆扫描,为每个分区各自分配了一个 RSet(Remembered Set),它内部类似于一个反向指针,记录了其它 Region 对当前 Region 的引用情况,这样就带来一个极大的好处:回收某个Region时,不需要执行全堆扫描,只需扫描它的 RSet 就可以找到外部引用,来确定引用本分区内的对象是否存活,进而确定本分区内的对象存活情况,而这些引用就是初始标记的根之一
事实上,并非所有的引用都需要记录在RSet中
如果引用源是本分区的对象,那么就不需要记录在 RSet 中;
同时 G1 每次 GC 时,由于所有的新生代都会被扫描,因此引用源是年轻代的对象,也不需要在RSet中记录;
所以最终只需要记录老年代到新生代之间的引用即可。
在进行可达性分析时,只要在GC Roots中再加上Remembered Set即可防止对所有堆内存的遍历
能够针对每个细分的区域来进行垃圾回收
优先回收垃圾最多的区域
采用的是Mark-Copy ,有非常好的空间整合能力,不会产生大量的空间碎片
垃圾收集过程
初始标记
先STW,只启动一条初始标记线程记录下gc roots直接能引用的对象,速度很快,如果不做STW,gc root会非常多
并发标记
根据初始标记的结果,做整个的一个可达性分析,找出所有的被引用的对象,这个过程耗时比较长(大约占整个收集过程的80%左右),但是这个过程和用户线程并发执行,所以用户无感知,但是因为用户程序继续运行,可能会有导致已经标记过的对象状态发生改变(就比如在并发标记前是非垃圾,标记之后是垃圾或者并发标记前是垃圾,并发标记后变非垃圾),详情这篇文章中的三色标记处理(https://blog.csdn.net/qq_41931364/article/details/106988008)
最终标记
先STW,同时修复在并发标记里面出现状态变换的对象,主要用到三色标记里的原始快照算法(见下面详解)做重新标记
多线程
筛选回收
会根据用户所指定的STW参数-XX:MaxGCPauseMillis(最大垃圾回收停顿时间)来制定回收计划,判断这轮回收需要回收多少个,所以这个阶段不一定会将所有的垃圾都回收掉,它在回收之前对堆有一个区域的回收时间估算,如果回收1/2就达到了用户指定的最大停顿时间,那么就只会回收1/2,但是这1/2如何去选具体是哪一块区域,有一个筛选算法在下面详解,剩下的在下一次的垃圾回收去回收,这也就是为什么没有重置标记,其实筛选回收是可以与用户线程并发执行的,但是由于我们指定了最大停顿时间,所以,在保证时间的情况下为了提高吞吐量,我们进行了STW,回收完成之后将旧的地址转换成新的地址。(注意:其实这个阶段可以与用户线程并发执行,但是由于G1内部算法过于复杂,没有实现并发执行,不过到了Shenandoah就实现了并发收集,Shenandoah可以看成是G1的升级版本。)
筛选算法
在G1收集器后台维护了一个优先列表,每次根据允许的收集时间,选择回收价值最大的Region,比如在同等大小的两个Region下,回收一个需要100ms,一个需要50ms,那么G1肯定会优先回收那个50ms的,这样就保证了在有效时间内能回收更多的堆空间,回收时间就是复制的时间,要复制的存活对象越多,回收时间就越长,回收的效益就越低,被回收的优先级就越低。
垃圾回收算法
无论是年轻代还是老年代主要的回收算法是复制算法
它会将要回收Region的存活对象挪到相邻的空的Region,然后清空之前的Region,这样就保证了内存碎片的减少。
G1垃圾收集分类
minor gc
Eden区的默认大小为5%,当Eden区放满的时候,G1不会马上做minor gc,它会先判断一下,触发一次minor gc的时间与我们用户设置的最大停顿时间的差距,如果大于或者接近最大停顿时间,则立即触发minor gc,如果回收时间比最大停顿时间小很多的话,将会扩大Eden区,继续放对象,过一段时间再次判断,依次重复,直到做了minor gc。
mixed gc
不是full gc,我们有一个参数-XX:InitiatingHeapOccupancyPercent可设定当老年代的占比达到一定的大小之后触发mixed gc,mixed gc主要回收所有年轻代和部分的老年代以及大对象区域,由于底层是复制算法,对剩余空间大小的要求比较高,所以,触发mixed gc的占比必须要调整到合适大小,如果没有足够的region去复制存活的对象,将会触发full gc。
full gc
先STW,然后采用单线程进行标记、清理和压缩整理,好空闲出来一批Region来供下一次MixedGC使用,非常耗时。在后续有一个收集器版本Shenandoah就优化成多线程收集了。
配置
年轻代比例配置
默认占整个堆的5%,可以通过设置-XX:G1NewSizePercent参数调整年轻代的初始占比,在运行过程中,JVM会动态的调整年轻代的占比,但最多不会超过整个堆的60%,最大占比也可以通过-XX:G1MaxNewSizePercent进行调整,年轻代的Eden区和Survivor区比例也是8:1:1。
在Java程序运行中,JVM会不停的给新生代增加更多的Region区,但是最多新生代的占比不会超过堆空间总大小的60%,可以通过-XX:G1MaxNewSizePercent调整(也不推荐,如果超过这个比例,年老代的空间会变的很小,容易触发全局GC)。新生代中的Eden区和Survivor区对应的Region区比例也跟之前一样,默认8:1:1,假设新生代现在有400个Region,那么整个新生代的占比则为Eden=320,S0/From=40,S1/To=40。
大对象
专门放大对象的区域,当一个对象的大小超过了一个Region区域的50%,则为大对象,直接放到Humongous区,在MixedGC或Full gc的时候会回收。
Humongous区存在的意义
可以避免一些“短命”的巨型对象直接进入年老代,节约年老代的内存空间,可以有效避免年老代因空间不足时的GC开销。
优势
一大优势在于可预测的停顿时间,能够个由用户指定「GC回收过程中的期望停顿时间」是G1的一大亮点,G1会尽可能快地在指定时间内完成垃圾回收任务
可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收
通过设置不同的「期望停顿时间」可以使得G1在任意场景下,都能在「吞吐量」和「响应时间/低延迟」之间取得很好的平衡。
但这里设置的「期望停顿时间」值必须要符合实际,如果想着:如果设置成20ms岂不是超低延迟了?这是并不现实的,只能称为异想天开,因为G1回收阶段也需要发生STW的来根扫描、复制对象、清理对象的,所以这个「期望停顿时间」值也得有个度,这个参数不能太大也不能太小。
太大
如果这个参数过于大,minor gc将很少发生,在它发生时有极大可能会有大量的存活对象进入Survivor区,如果Survivor放不下,就会进入老年代,就很容易触发mixed gc,不建议
太小
在触发minor gc时很难回收到垃圾,最后导致垃圾太多,空间被占,也很容易触发mixed gc
G1的默认的「停顿时间」目标为200ms,但一般而言,实际过程中中占到[几十 ~ 三百毫秒之间]都很正常
实际应用中,通常把「期望停顿时间」设置为100ms~300ms之间会是比较合理的选择。
另外,在追求响应时间的时候必然会牺牲吞吐量,而追求吞吐量的同时必然会牺牲响应时间。鱼和熊掌不可兼得。
G1的适合场景
50%以上的堆被存活对象占用
当大多数对象都存活的时候,说明老年代被占用的比例也会很大,这个时候就会触发full gc,full gc是很慢的,如果我们使用G1,那么G1就会触发mixed gc,而且mixed gc的GC最大停顿时间还是可控的。
对象分配和晋升的速度变化非常大
说明了对象往老年代挪动的频率很频繁,一样的,可以减少full gc的发生
垃圾回收时间特别长,超过1秒
可以设置停顿时间,提升用户体验。
8GB以上的堆内存(建议值)
内存如果在8G以下,收集的垃圾不是很多,而G1的算法相对于CMS较为复杂,还很有可能效率不如CMS,但是对于大内存,STW时间比较长,所以,在可控停顿时间这里,G1比较合适。
停顿时间是500ms以内
停顿时间可由用户控制
JDK11的默认垃圾收集器
参考资料
https://blog.csdn.net/qq_41931364/article/details/107040928
https://blog.csdn.net/a745233700/article/details/121724998
Epsilon
主要是用于测试的无操作收集器,如:性能测试、内存压力测试、VM接口测试等。在程序启动时选择装载Epsilon收集器,这样可以帮助我们过滤掉GC机制引起的性能假象。装配该款GC收集器的JVM,在运行期间不会发生任何GC相关的操作,程序所分配的堆空间一旦用完,Java程序就会因OOM原因退出
ZGC
概述
一款源自于JDK11的性能魔兽 - ZGC
是一款基于分区概念的内存布局GC器,这款GC器是真正意义上的不分代收集器,因为它无论是从逻辑上,还是物理上都不再保留分代的概念
ZGC也会把堆空间划分为一个个的Region区域,但ZGC中的Region区不存在分代的概念,仅仅只是简单的将所有Region区分为了大、中、小三个等级
使用读屏障、染色指针和内存多重映射等技术来实现的可并发的标记-整理算法的垃圾收集器。
由于不分代,ZGC中只存在一种GC类型,同时也不需要G1中Remembered Set这种概念存在,因为是单代的堆空间,所以每次回收都是扫描所有页面,不需要额外解决跨代引用问题。
问题
G1引入Remembered Set的目的是防止整堆扫码,提高性能,而不是为了解决跨代引用
ZGC扫码所有页面,是否会有性能问题?如何解决?
ZGC主打的是超低延迟与吞吐量,在实现时,ZGC也会在尽可能堆吞吐量影响不大的前提下,实现在任意堆内存大小下都可以把垃圾回收的停顿时间限制在10ms以内的低延迟
ZGC与GC收集器一样,也会存在「垃圾优先」的特性,在标记完成后,整个堆中会有很多分区可以回收,ZGC也会筛选出回收价值最大的页面来作为本次GC回收的目标。
基于64位指针实现的染色指针技术,最大支持16TB JVM
颜色指针可以说是ZGC的核心概念。因为他在指针中借了几个位出来做事情,所以它必须要求在64位的机器上才可以工作。并且因为要求64位的指针,也就不能支持压缩指针。
低延迟
ZGC通过多阶段的并发执行+几个短暂的STW阶段来达到低延迟的目的
ZGC不会因为堆空间的扩大而增大停顿时间
ZGC只会在处理根节点等阶段才会出现STW,而堆空间再怎么扩大,内存中的根节点数量不会出现质的增长,所以ZGC的停顿时间几乎不受限于内存大小
ZGC与之前的收集器还有一点很大的不同在于:ZGC标记的是指针而并非对象,但最终达到的效果是等价的,因为所有对象以及所有指针都会被遍历。
ZGC的不分代其实是它的缺点,因为对象都是满足朝生夕死的特性,ZGC不分代只是因为分代比较难实现。
ZGC最初是源于Azul System公司的C4(Concurrent Continuously Compacting Collector)收集器与PauselessGC,Java引入ZGC的目的主要有如下四点
①奠定未来GC特性的基础。
②为了支持超大级别堆空间(TB级别),最高支持16TB。
TB级别内存出处:NUMA架构
UMA架构
UMA即Uniform Memory Access Architecture(统一内存访问),UMA也就是一般正常电脑的常用架构,一块内存多颗CPU,所有CPU在处理时都去访问一块内存,所以必然就会出现竞争(争夺内存主线访问权),而操作系统为了避免竞争过程中出现安全性问题,注定着也会伴随锁概念存在,有锁在的场景定然就会影响效率。同时CPU访问内存都需要通过总线和北桥,因此当CPU核数越来越多时,渐渐的总线和北桥就成为瓶颈,从而导致使用UMA/SMP架构机器CPU核数越多,竞争会越大,性能会越低。
NUMA架构
NUMA即Non Uniform Memory Access Architecture(非统一内存访问),NUMA架构下,每颗CPU都会对应有一块内存,具体内存取决于处理器的内存位置,一般与CPU对应的内存都是在主板上离该CPU最近的,CPU会优先访问这块内存,每颗CPU各自访问距离自己最近的内存,效率自然而然就提高了。
但上述内容并非重点,重点是NUMA架构允许多台机器共同组成一个服务供给外部使用,NUMA技术可以使众多服务器像单一系统那样运转,该架构在中大型系统上一直非常盛行,也是高性能的解决方案,尤其在系统延迟方面表现都很优秀,因此,实际上堆空间也可以由多台机器的内存组成。
ZGC是能自动感知NUMA架构并可以充分利用NUMA架构特性的一款垃圾收集器。
③在最糟糕的情况下,对吞吐量的影响也不会降低超过15%。
④GC触发产生的停顿时间不会偏差10ms。
内存模型
ZGC的内存结构实际上被称为分页,源于Linux Kernel 2.6中引入了标准的大页huge page
大页存在两种尺度,分别为2MB以及1GB。
Linux内核引入大页的目的主要在于为了迎合硬件发展,因为在云计算、弹性调度等技术的发展,服务器硬件的配置会越来越高,如果再依照之前标准页4KB的大小划分内存,那最终反而会影响性能。
也会把堆空间划分为一个个的Region区域,但ZGC中的Region区不存在分代的概念,它仅仅只是简单的将所有Region(ZGC实际不叫Region,而是叫ZPage)区分为了大、中、小三个等级
小型区/页(Small)
固定大小为2MB,用于分配小于256KB的对象。
中型区/页(Medium)
固定大小为32MB,用于分配大于256KB小于4MB的对象
大型区/页(Large)
没有固定大小,容量可以动态变化,但是大小必须为2MB的整数倍,专门用于存放>4MB的巨型对象。
但值得一提的是:每个Large区只能存放一个大对象,也就代表着你的这个大对象多大,
那么这个Large区就为多大,所以一般情况下,Large区的容量要小于Medium区,
并且需要注意:Large区的空间是不会被重新分配的。
但值得一提的是:每个Large区只能存放一个大对象,也就代表着你的这个大对象多大,
那么这个Large区就为多大,所以一般情况下,Large区的容量要小于Medium区,
并且需要注意:Large区的空间是不会被重新分配的。
为何Large区不能被重分配/转移呢?
因为Large区中只会存储一个对象,在GC发生时标记完成后,直接决定是否回收即可,Large区中存储的对象并非不能转移到其他区,而是没有必要,本身当前Large区中就只有一个大对象,转移还得专门准备另外一个Large区接收,但本质上转不转移都不会影响,反而会增加额外的空间开销。
ZPage具有动态性
动态创建和销毁,以及动态的区域容量大小
ZGC问题
ZGC最大的问题是浮动垃圾
假设ZGC的一次完整GC需要八分钟,在这期间由于新对象的分配速率很高,所以堆中会产生大量的新对象,这些新对象是不会被计入本次GC的,会被直接判定为存活对象,而本轮GC回收期间可能新分配的对象会有大部分对象都成为了“垃圾”,但这些“浮动垃圾”只能等待下次GC的时候进行回收
参考
https://blog.csdn.net/weixin_45101064/article/details/123478022
ShenandoahGC
概述
在JDK11中推出ZGC后,JDK12马不停蹄的推出了ShenandoahGC收集器,它与G1、ZGC收集器一样,都是基于分区结构实现的一款收集器。
和ZGC对比
相同
ShenandoahGC也没有实现分代的架构,所以在触发GC时也不会有新生代、年老代之说,只会存在一种覆盖全局的GC类型。
它们的停顿时间都不会受到堆空间大小的影响
不同
ZGC是基于colored pointers染色指针实现的,而ShenandoahGC是基于brooks pointers转发指针实现
和G1比
相同
也会将堆内存划分为一个个 大小相同的Region区域,也同样有存放大对象的Humongous区
你可以把ShenandoahGC看做G1收集器的修改版
不同
它比G1收集器实现上来说更为激进,一味追求极致低延迟。
ShenandoahGC核心-BrooksPointers转发指针
解决的问题
回收过程中,如果一个对象被复制到新的区域,用户线程通过原本指针访问时如何定位对象呢?
ZGC
在前面的ZGC中可以通过染色指针+读屏障的方案获取到最新的地址
ShenandoahGC
BrooksPointers转发指针
BrooksPointers转发指针原理:
所谓的转发指针就是在每个对象的对象头前面添加了一个新的字段,也就是对象头前面多了根指针。对于未移动的对象而言,指针指向的地址是自己,但对于移动的对象而言,该指针指向的为对象新地址中的BrooksPointers转发指针,示意图如下
最大支持256TB JVM
高性能垃圾收集器(G1、ZGC、ShenandoahGC)总结
在三款高性能的GC器中,就目前而言,唯一保留了分代思想的是G1,而ZGC、ShenandoahGC并非是因为不分代性能好一些而不实现的,而是因为实现难度大所以才没有实现,在之前就曾提及过:逻辑分代+物理分区的结构才是最佳的,所以不分代的结构对于ZGC、ShenandoahGC来说,其实是一个缺点,因为不分代的堆空间,每次触发GC时都会扫描整堆。
G1收集器在后续的JDK版本中一直在做优化,因为G1是打算作为全能的默认GC器来研发的,但G1收集器最大的遗憾和短板在于:回收阶段需要发生STW,所以导致了使用G1收集器的程序会出现不短的停顿。
而ZGC、ShenandoahGC两款收集器,前者采用了染色指针+读屏障技术做到了并发回收,后者通过转发指针+读写屏障也实现了并发回收。因此,使用这两款收集器的应用程序,在运行期间触发GC时,造成的停顿会非常短暂,所以如果你的项目对延迟要求非常低,那么它两个是很不错的选择。
不过ZGC由于承诺了最大不超过10ms的低延迟,所以最恶劣的情况可能会导致降低15%左右的吞吐量,因此,如果你想使用它,那么要做好扩大堆空间的准备,因为只能通过加大堆空间来做到提升吞吐量。
同时,由于ZGC的染色指针使用了64位指针实现,所以也就代表着:在ZGC中指针压缩失效了,所以在32GB以下的堆空间中,相同的对象数据,ZGC会比其他的收集器占用的空间更多。
同时,由于ZGC的染色指针使用了64位指针实现,所以也就代表着:在ZGC中指针压缩失效了,所以在32GB以下的堆空间中,相同的对象数据,ZGC会比其他的收集器占用的空间更多。
而ShenandoahGC因为额外增加了转发指针,所以也存在两个问题:
①访问对象时,速度会更慢,因为需要至少经过一次地址转发。
②需要更多的空间存储多出来的这根指针。
同时,ShenandoahGC是没有给出类似于ZGC的“最大10ms的低延迟”承诺,所以就现阶段而言,ShenandoahGC性能会比ZGC差一些,但唯一的优势在于:它可以比ZGC支持更大的堆空间(虽然没啥用)。
①访问对象时,速度会更慢,因为需要至少经过一次地址转发。
②需要更多的空间存储多出来的这根指针。
同时,ShenandoahGC是没有给出类似于ZGC的“最大10ms的低延迟”承诺,所以就现阶段而言,ShenandoahGC性能会比ZGC差一些,但唯一的优势在于:它可以比ZGC支持更大的堆空间(虽然没啥用)。
好像G1收集器压根比不上其他两款,但实际上并非如此,因为每款收集器都会有自己的适用场景,就好比在几百MB的堆空间中,装载ZGC就一定比G1好吗?其实是不见得的。因为G1中存在分代的逻辑,而ZGC是单代的,所以如果在分配速率较快的情况下,ZGC可能会跟不上(因为ZGC的整个GC过程很久),而G1则可以完全胜任。
参考
https://blog.51cto.com/u_11440114/5103211
CMS回收器问题
https://blog.csdn.net/a745233700/article/details/121724998
参考资料
https://blog.51cto.com/u_11812624/5462445
参考资料
https://blog.csdn.net/m0_71777195/article/details/126247090?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522166615991416800186577080%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id=166615991416800186577080&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~blog~first_rank_ecpm_v1~rank_v31_ecpm-14-126247090-null-null.nonecase&utm_term=jvm&spm=1018.2226.3001.4450
https://blog.51cto.com/u_11812624/5462445
RPC及序列化框架
RPC框架
Thrift
gRPC
与thrift对比
https://blog.csdn.net/chengzhi0371/article/details/100788739
https://blog.csdn.net/Com_ma/article/details/97134775
Motan
Dubbox
rpcx
各种RPC框架比较
https://blog.csdn.net/asdcls/article/details/121661651
大数据
zookeeper
作用
提供低延迟、高可用的内存KV数据库服务
提供中心化的服务故障发现服务
提供分布式场景下的锁、计数器、队列等协调服务
核心特性
zk本身是一个高可用集群
通常有奇数个节点
每个节点存储的数据都相同
通过ZAB协议(由Paxos算法改进而来)保证数据的一致性
客户端连接zk集群时,只要能连到zk的任意一个节点,即可获得zk集群的所有节点信息
节点
zk包含4类节点
持久节点
临时节点
持久顺序节点
可创建多个子节点
记录子节点创建的先后顺序
临时顺序节点
节点内数据为树状结构,有父节点、子节点
watcher机制
异步事件反馈机制
由客户端watch zk节点的如下操作
getChildren
对应的watcher为子节点watcher
关注的事件有子节点的创建、删除等
exists、getData
关注的事件有:节点数据发生更新、子节点发生创建、删除操作等
zk客户端与服务端的连接是长连接
watch事件发生后,服务器会向客户端发送事件
客户端收到事件后,执行相应的处理(可编程)
客户端收到watch事件后,必须再次watch才能收到后续事件
session机制
zk客户端和服务端存在一个类似HTTP服务的session机制,每次请求均会带着session标识来标记本次session
session可以设置超时时间
连接断开后,session不会自动断开,除非session超时
使用场景
分布式锁
1. 客户端在临时顺序节点上创建子节点
2. 创建后查询
2.1 若自己创建的节点是最小节点,则认为成功获得锁,
2.1.1 执行后续业务操作(该业务操作一般是多个客户端必须串行执行的业务逻辑,否则会导致业务不一致)
2.1.2 业务执行完成后,退出当前session,业务结束。
退出当前session时zk服务器会自动向所有连接它的客户端发送子节点移除事件
2.2 若不是最小节点,则说明没拿到锁,则watch该节点的子节点移除事件
当监控到子节点移除事件(说明之前拿到锁的客户端释放了锁)后,跳转到步骤1,直到拿到锁,执行2.1.2执行业务逻辑并退出为止
Hbase
基础概念
Google三论文
GFS
HDFS
数据存储
一个HDFS文件由一个个块(Block)组成
块大小默认为128M
每个块会设置N个副本
N可配
高可用HDF集群主要由4个服务(两类作业)组成
元数据维护类组件
NameNode
作用
存储并管理HDFS文件系统的元数据
元数据组成
1. 文件属性
文件大小
拥有者
组
各个用户的访问权限
2. 文件的数据块的节点分布
NameNode本质上是一个独立的维护所有文件元数据的高可用KV数据库系统
如何保证数据不丢失
采用EditLog和FSImage方式保证元数据的高效持久化
EditLog即为WAL:每一次文件元数据的写入,先做一次EditLog的顺序写,然后再修改NameNode的内存状态
NameNode内部有一个线程,定期将内存持久化成FSImage
持久化成FSImage后,该时间点之前的EditLog可以被清理,这个过程叫checkpoint
HDFS不适合存储大量小文件,原因
HDFS会把所有文件的元数据维护在内存中
若存储大量小文件,则造成分配大量的Block,会耗尽NameNode的内存而OOM
后续版本的HDFS支持NameNode对元数据分片,解决了NameNode的扩展性问题
部署
需要部署两个NameNode
Active NameNode 一个
StandBy NameNode一个
作为Active的备份
不对外提供服务
ZKFailoverController检测到Active NameNode发生异常时,会把StandBy NameNode切换成Active
JournalNode
Active和Standby NameNode之间,保证数据一致性的组件(是一个服务)
当StandBy NameNode切换为Active NameNode时,必须保证新的Active NameNode的数据与之前一致
JournalNode就是用来维护EditLog一致性的Paxos组
类似zookeeper,线上一般部署奇数个,建议3个或5个
ZKFailoverController
实现NameNode的字段切换
数据存储类组件
DataNode
组成文件的所有Block都存储在DataNode上
一个逻辑上的Block会放放在N个不同的DataNode上
资源消耗
主要传输数据,所以主要消耗网络带宽和磁盘资源,即I/O密集
计算非密集:CPU和内存消耗很少
而Hbase的RegionServer主要是计算密集型,消耗CPU和内存
因此通常将RegionSever和DataNode部署在一起
文件写入/读取流程
写入流程
1. create
2. write
3. hflush或hsync(可配))
hflush和hsync区别
hflush
保证WAL持久
问题:
写WAL是不是也写入操作系统Cache?这样无法保证WAL的持久性
3个副本都写入,但可能在各自的操作系统Cache中,并未持久,有丢数据的风险
问题:
有WAL持久,数据怎么会丢失呢?
hsync
hflush基础上,数据持久,不会丢失数据
如何保证数据不丢失(高可靠性)
用hflush保证数据WAL持久性
p43,看不懂???
4. close
读取流程
步骤
1. 请求NameNode,获取指定文件的块信息,以及这些块都在哪些DataNode上
2. 连接各个DataNode,读取块数据
Hbase读取文件性能影响因素
locality
该文件的块数据在当前DFSClient所在的DataNode上的字节数/文件总的字节数
对满足可本地读的数据,开启短路读(可设置是否开启),本地读写,不走网络,性能提升非常大
短路读
特点
HDFS擅长的场景是大文件顺序写、顺序读、随机读
不支持随机写以及多个客户端同时写一个文件
MapReduce
MapReduce
BigTable
Hbase
数据模型
逻辑模型:表
物理模型:Map
Key的五大组成部分
rowid
2B
即rowid长度不能超过2^16=65536
列族
1B
即列族长度不能超过2^8=256字符
列名(qualifier)
时间戳
用于多版本控制
8B:时间戳的long值
值越大,排序越靠前
倒序排序
类型
PUT/DELETE/Delete Column/Delete Family
1字节
按列存储
按列族存储,即一个列族的数据存储在一起
Key比value占用更大的空间,因此key长度越少越好,即rowid、列族、列名的值越小越好
Hbase五大特症
稀疏
一行表若某列无值,则不需要存储,不占用存储空间。
RDB的空值列需要存储空间,其值填充为null,造成存储大量浪费且性能低下
节省空间,查询速度快
因此,稀疏性是Hbase的列可以无限扩展的一个重要条件,可以达到上百万列
分布
表分布在整个集群中,而不是集中存储在某台机器上
持久
多维
Map Key由五大部分组成
排序
根据key的五大组成部分排序
先比较rowkey,字典升序
若rowkey相同,再比较列族:列名,字典升序
比较时间戳,即版本,倒序排序
HBase相关数据结构
一个列族就是一个LSM树
LSM(Log-Structured Merge-Tree)
特点
无论PUT还是DELE,写入的都并非数据本身,而是操作记录,如PUT/DELETE一条rowid为1的记录
无论PUT还是DELE,落盘时都是Append操作,没有任何随机写操作
LSM对写入是极为友好的索引结构,能将磁盘的写入贷款利用到极致
但对读取操作非常不利,需要做多路归并(compact)
LSM分两部分
内存部分
内存部分即MemStore
是一个ConcurrentSckipListMap
跳跃表(又叫跳表,SckipList)
key就是Map Key五部分
value是一个字节数组
写入时,先写入MemStore
当MemStore写满(阈值可配)时,会flush到磁盘,生成一个数据块文件
为避免flush影响性能,flush操作是异步的
当MemStore写满后,该MemStore就会被标记为Snapshot标记,并禁止写入
数据flush到磁盘后,释放对应的内存空间
在内存开辟一块新的空MemStore,存储后续写入的数据
磁盘部分
由一个个独立的文件组成
由一个个操作日志组成
结构
keyLen(4B)|valueLen(4B)|rowkeyLen(2B)|rowkeyBytes|familyLen(1B)|familyBytes|qualifierBytes|timestamp(8B)|type(1B)valueBytes
每个文件是一个LSM块
随着不断写入磁盘,文件(数据块)数量越来越多
问题
对读取操作极为不利,影响读取性能
一旦用户有读取请求,则需要将大量的磁盘文件进行多路归并,之后才能读取到所需的数据
因为需要将那些key相同的数据全局综合起来,最终选择出合适的版本返回给用户
磁盘文件数量越多,读取的时候随机读取的次数也会越多,从而影响读取性能
解决方案
多路归并(compact)
原理
设置一个策略,让多个hfile进行多路归并(compact),合并成一个文件
文件个数越少,随机读写操作次数越少,读取性能越好
两种归并类型
major compact
将所有的Hfile一次性多路归并成一个文件
好处
合并后只有一个文件,读取性能最高
问题
合并操作时间很长,并消耗大量I/O带宽
因此Major Compact不宜太频繁,时候周期性(如每周一次,夜间执行)地运行
minor compact
选中少数几个hfile进行多路归并
优点
可进行局部compact, I/O较少
I/O较少,适合较高频率地执行
问题
部分comcat,性能没有major compact后性能高
无法彻底清除delete操作,无法保证数据的最小化
布隆过滤器
原理
设定一个数组,长度为N,每个元素的值要么为0,要么为1
设定一个长度为N的数组和一个整数K
数组长度为N,每个数组元素的值要么为0,要么为1
K表示一个固定整数
对于要存储的集合c中的每一个元素w
for (i in 0..K){
Array[hash(w)%N] =1
}
Array[hash(w)%N] =1
}
即w会在Array的K个元素位置设置为1
用途
用于检索指定的元素m是否在集合c中
根据同样的公式,计算m在数组中哪些位置是1,
查看数组中,对应的位置是否也为1
只要有一个位置不为1,则m一定不在集合c中
全部为1,则m可能(但不=确定)在集合c中
这种情况下,需要进一步比对c的内容才能进一步确定m是否在c中
论文证明,当N=K*|c| / ln2时,误判率最低 (|c|为集合元素个数)
误判率即布隆过滤器判定m可能在集合c但实际不在c中的占比
特点
布隆过滤器占用占用极少的空间
可迅速给定某个(而非一批)元素m一定不在集合m中或可能在集合m中的判断,性能极高
若判断某个元素m一定不在集合中,就不需要进一步比对数据内容,可大大节省磁盘I/O,提高效率
Hbase如何利用布隆过滤器
每一个LSM文件(数据块)对应一个布隆过滤器
在Hbase 1.x中,用好可以对某些列设置不同的布隆过滤器,共有3种类型
NONE
关闭布隆过滤
ROW
按照rowkey来计算布隆过滤器的二进制串并存储
get操作必须带rowkey,因此可以设置为默认值
对指定rowkey的get操作,可充分利用布隆过滤的特点,判断元素是否存在LSM数据块中,极大提升get效率
ROWCOL
按照rowkey+family+qualifier这三个字段拼出byte[]来计算布隆过滤器的二进制串并存储
get操作需同时指定这三个字段才可提升性能
对普通的scan操作,由于操作的是一批数据,rowkey不确定,因此无法利用布隆过滤器特性来提升扫描性能
但在某些特定场景下,scan操作可以借助布隆过滤器提升性能
1. 基于ROWCOL类型设置布隆过滤,当scan操作在同一行内切换下一列数据时,由于rowid+family+column确定,此时可以通过布隆过滤器迅速判断数据是否存在,因此可以优化scan性能
2. 前缀布隆过滤器:
方法
设置rowkey=在scan时将会作为过滤条件的、定长业务字段作为前缀
如设置rowkey=userid-其它字段,scan时会以userid作为过滤条件进行扫描
取rowkey的固定长度(长度与业务字段长度相同)计算布隆过滤器
长度与userid相同,即提取userid部分作为布隆过滤器
用户执行scan操作时,将业务字段作为过滤条件
好处
可借助布隆过滤器,提升scan操作的性能(可提升一倍以上)
HBase为什么选择LSM
HBase基于HDFS
HDFS擅长的场景是大文件顺序写、顺序读、随机读
不支持随机写以及多个客户端同时写一个文件
LSM结构正好满足这一要求
相关组件
组件构成
client
zookeeper
作用
实现master的高可用
active master通常只有一个,当该active master发生异常时,zk会选举出一个slave master作为active master
管理系统核心元数据hbase:meta
hbase:meta记录整个hbase集群的Region信息
存储了所有用户业务表的元数据
表定义
表分区(Region)及所在的RegionServer
RegionServer宕机异常检测
zk通过心跳可感知到RS是否宕机,并在宕机后通知master进行宕机处理
分布式表锁
Hbase对表进行各种管理操作(如alter操作)需要先对表上锁
通过conf/hbase-site.xml配置zk
hbase.zookeeper.quorum
zk集群的地址
hbase.zookeeper.property.clientPort
默认为2181
zookeeper.session.timeout
表示zk与RS之间的会话超时时间
一旦发生超时,zk会认为RS宕机,进而通知master
master收到通知后,会将该RS移出集群,并将该RS上的所有Region迁移到集群中其它RS上
zookeeper.znode.parent
默认值:/hbase
节点
/hbase
meta-region-server
即hbase:meta表存储在哪个meta-region-server上
hbase:meta本身也是一个Hbase表,hbase:meta记录整个hbase集群的Region信息
前缀 hbase表示namespace
hbase容许针对不同的业务设计不同的namespace
系统表采用统一的namespace,即hbase
meta表示表名
每行记录是一个Region信息
hbase:meta表定义
Rowkey组成:
tableName
业务表名
startRowkey
业务表Region区间的起始Rowkey
timestamp
Region创建时间戳
EncodedName
上面3个字段的MD5 hash值
列族:只有一个:info,包含的列有
regioninfo
存储4个信息
EncodeName
RegionName
StartRow
StopRow
seqnumDuringOpen
存储Region打开的sequenceId
server
Region所在的RegionServer
serverstartcode
存储所在RegionServer的启动时间
https://blog.csdn.net/qq_44665283/article/details/125719969
hbase:meta只有一个Region
只有一个Region的原因
为了确保meta表多次操作的原子性
因为hbase本质上只支持Region级别的事务
并且该表只存储在一个(而非多个)RegionServer上,具体在哪个Regionserver存储在zk的该节点
为提高性能,hbase:meta表的所有数据全部保存在内存中,大小限制为128M
hbase:meta设计存在的问题及解决方案
问题
大量客户端连接并访问hbase:meta,所在的RegionServer会成为热点
解决方案
客户端首次连接Hbase时,从Hbase服务器获取hbase:meta到客户端本地,并缓存起来(缓存到MetaCache中),之后基于本地缓存查找rowkey所在的Region
客户端根据rowkey查找业务表数据过程
1. 从zk meta-region-server节点,获知hbase:meta表存储在哪个RegionServer上
2. 连接该RegionServer,获取hbase:meta表信息到客户端本地缓存起来
3. 根据业务rowkey,与本地MetaCache比对,查找当前业务rowkey所在的region及所在的regionServer
1. 找不到 可能本地MetaCache已过期
连接Hbase集群,从服务器端重新获取hbase:meta,再次比对
1. 1还是找不到
说明rowkey真的不存在,返回给业务:rowkey不存在
1.2. 找到了,跳转到2
2. 找到了,连接对应的RegionServer
2.1 RegionServer返回rowkey不存在,说明本地MetaCache可能已经过期
连接Hbase集群,从服务器端重新获取hbase:meta,再次比对
1. 还是找不到
说明rowkey真的不存在,返回给业务:rowkey不存在
2. 找到了,跳转到2.2
2.2
找到数据,返回数据给业务
backup-masters
存储多个,其中一个为active,其它为slave。利用zk选举机制实现master的高可用
table
集群中所有表信息
问题:一个表有多少region,该Region在哪个RegionServer上存储在hbase:meta表中
但一个Region包含哪些HFile及对应存储路径,这些存储在哪里?
region-in-transition
table-block
master
balancer
namespace
hbaseid
online-snapshot
replication
split-WAL
recovering-regions
rs
集群中所有运行的RegionServer
master
RegionServer(*)
HLog(1)
BlockCache(1)
读缓存,读取数据时,先将数据读取到BlockCache
Region(*)
Store(*)
MemStore(1)
读写缓存
HFile(*)
说明
master、RegionServer、Region均是服务,而不是数据
数据存储在HDF中,哪些Region管理哪些数据,在hbase:meta表中存储
Hbase集群的文件目录结构(HDFS文件系统)
/hbase
${hbaseClusterName}
.hbase-snapshot
snapshot文件存储目录
用户执行snapshot后,相关的snapshot元数据文件存储在该目录
.tmp
临时文件
主要用于hbase表的创建和删除操作
表创建先在tmp目录下执行,执行成功后再将tmp目录下的表信息移动到实际表目录下
表删除操作会讲表目录移动到tmp目录下,一定时间后会再将tmp目录下的文件真正删除
MaserProcWALs
存储Master Procedure过程中的WAL文件。
Master Procedure功能主要用于可恢复的分布式DDL操作
WALs
存储集群中所有RegionServer的Hlog日志文件
archive
文件归档目录
所有对HFile文件的删除操作都会将待删除文件临时放在该目录
进行snapshot或者升级时使用到的归档目录
Compaction删除HFile的时候,也会把旧的Hfile移动到这里
corrupt
存储损坏的HLog或HFile文件
data
集群中所有Region的HFile数据
HFile文件在data目录下的完整路径为
/hbase/${hbaseClusterName}/data/命名空间/表名/Region名称/列蔟名/文件名
举例:hbase/hbase-nptest/data/default/usertable/1a1365282ac023d8wewe98we887/famiy/1053d223lysd23s32sd66
子目录
.tabledesc
表描述文件,记录对应表的基本schema信息
.tmp
用来存储flush和compaction的中间结果
以flush为例,MemStore落盘形成的HFile先生成到该目录,完成后再移动到对应的实际文件目录
.regioninfo
Region描述信息
recovered.edits
存储故障恢复时,该Region需要回放的WAL日志数据
RegionServer宕机后,该节点上还没来得及flush到磁盘的数据需要通过WAL回放恢复
WAL会先按Region进行切分,每个Region拥有对应的WAL数据片段
每个Region回放时,只需要回放自己的WAL数据切片即可
hbase.id
集群启动初始化时,创建的集群唯一ID
hbase.version
hbase软件版本号
oldWALs
WAL归档目录,一旦WAL中对应的数据持久化到HFile,那么该WAL文件会被移动到该目录
插件
Phoenix
https://blog.csdn.net/u013411339/article/details/90657429
客户端
代码调试
HBaseTestingUtility
作用
在调试MR任务或者操作Hbase表时,往往我们需要将本地代码打成Jar包,然后上传到Hadoop集群上去跑,这样不仅麻烦,还不方便调试,Hadoop开发团队提供了在本地调试代码的API,就是MiniHbaseCluster, 在本机JVM中模拟一个Hadoop集群,与真实环境的Hadoop集群并没有区别,方便我们提交任务和Debug。
HBASETestingUtility类;这个类提供了启动、停止、创建、配置等实用方法来管理Mini Cluster;
HBASETestingUtility类;这个类提供了启动、停止、创建、配置等实用方法来管理Mini Cluster;
相关代码
https://blog.csdn.net/weixin_43930865/article/details/121170645
集群的配置(对客户端来说)
包含三个配置文件
hbase-site.xml
core-site.xml
hdfs-site.xml
获取Hbase Configuration
Configuration configuration = HbaseConfiguration.create()
HBaseTestingUtility提供了API获取:Configuration configuration = hBaseTestingUtility.getConfiguration()
两类操作
管理表
通过Admin对象管表
Connection conn = ConnectionFactory.createConnection(conf);
Admin admin = connection.getAdmin()
Admin admin = connection.getAdmin()
admin.tableExists(TableName.valueOf(tableNameStr));
管理操作
admin.createTable
TableDescriptorBuilder tableDesc = TableDescriptorBuilder.newBuilder(TableName.valueOf(tableName));
List<ColumnFamilyDescriptor> colFamilyList = new ArrayList<>();
for (String columnFamilys : list) {
ColumnFamilyDescriptorBuilder c = ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes(columnFamilys));
colFamilyList.add(c.build());
}
tableDesc.setColumnFamilies(colFamilyList);
//创建表方式1:直接创建
admin.createTable(tableDesc.build());
//创建表方式2:预分区创建
// admin.createTable(tableDesc.build(), new RegionSplitter.HexStringSplit().split(splitNum));
TableDescriptorBuilder tableDesc = TableDescriptorBuilder.newBuilder(TableName.valueOf(tableName));
List<ColumnFamilyDescriptor> colFamilyList = new ArrayList<>();
for (String columnFamilys : list) {
ColumnFamilyDescriptorBuilder c = ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes(columnFamilys));
colFamilyList.add(c.build());
}
tableDesc.setColumnFamilies(colFamilyList);
//创建表方式1:直接创建
admin.createTable(tableDesc.build());
//创建表方式2:预分区创建
// admin.createTable(tableDesc.build(), new RegionSplitter.HexStringSplit().split(splitNum));
admin.tableExists
admin.deleteTable
TableName table = TableName.valueOf(tableName);
if (admin.tableExists(table)) {
//先禁用后删除
admin.disableTable(table);
admin.deleteTable(table);
}
TableName table = TableName.valueOf(tableName);
if (admin.tableExists(table)) {
//先禁用后删除
admin.disableTable(table);
admin.deleteTable(table);
}
admin.addColumnFamily
// 向表中添加一个列族
public static void addColumnFamily(String myTableName, String colFamily) throws IOException {
Admin admin = hbaseLocalTestingUtil.getHbaseAdmin();
TableName tableName = TableName.valueOf(myTableName);
if (admin.tableExists(tableName)) {
ColumnFamilyDescriptor colFamilyDes = ColumnFamilyDescriptorBuilder.newBuilder(colFamily.getBytes()).build();
admin.addColumnFamily(tableName, colFamilyDes);
}
}
// 向表中添加一个列族
public static void addColumnFamily(String myTableName, String colFamily) throws IOException {
Admin admin = hbaseLocalTestingUtil.getHbaseAdmin();
TableName tableName = TableName.valueOf(myTableName);
if (admin.tableExists(tableName)) {
ColumnFamilyDescriptor colFamilyDes = ColumnFamilyDescriptorBuilder.newBuilder(colFamily.getBytes()).build();
admin.addColumnFamily(tableName, colFamilyDes);
}
}
admin.deleteColumnFamily
// 从表中移除一个列族
public static void removeColumnFamily(String myTableName, String colFamily) throws IOException {
Admin admin = hbaseLocalTestingUtil.getHbaseAdmin();
TableName tableName = TableName.valueOf(myTableName);
if (admin.tableExists(tableName)) {
admin.deleteColumnFamily(tableName, colFamily.getBytes());
System.out.println("remove " + colFamily + " successful!");
}
}
// 从表中移除一个列族
public static void removeColumnFamily(String myTableName, String colFamily) throws IOException {
Admin admin = hbaseLocalTestingUtil.getHbaseAdmin();
TableName tableName = TableName.valueOf(myTableName);
if (admin.tableExists(tableName)) {
admin.deleteColumnFamily(tableName, colFamily.getBytes());
System.out.println("remove " + colFamily + " successful!");
}
}
管理表数据
通过table对象管理数据
Connection conn = ConnectionFactory.createConnection(conf);
TableName tablename = TableName.valueOf(tableNameStr);
if (admin.tableExists(tablename)) {
Table table = connection.getTable(tablename);
}
TableName tablename = TableName.valueOf(tableNameStr);
if (admin.tableExists(tablename)) {
Table table = connection.getTable(tablename);
}
数据管理操作
table.put(List<Row> puts)
/*put单条数据*/
public static boolean putRow(String tableName, String rowKey, String columnFamilyName, String qualifier,
String value) {
//用于存放失败数据
List<Row> errorList = new ArrayList<Row>();
try {
Table table = connection.getTable(TableName.valueOf(tableName));
Put put = new Put(Bytes.toBytes(rowKey));
put.addColumn(Bytes.toBytes(columnFamilyName), Bytes.toBytes(qualifier), Bytes.toBytes(value));
table.put(put);
table.close();
} catch (IOException e) {
e.printStackTrace();
if (e instanceof RetriesExhaustedWithDetailsException) {
RetriesExhaustedWithDetailsException ree =
(RetriesExhaustedWithDetailsException) e;
int failures = ree.getNumExceptions();
for (int i = 0; i < failures; i++) {
errorList.add(ree.getRow(i));
}
}
}
return true;
}
/*put list数据*/
public static boolean putList(String tablename, List<HbaseMessage> listMessage) {
Table table = null;
//用于存放失败数据
List<Row> errorList = new ArrayList<Row>();
try {
table = connection.getTable(TableName.valueOf(tablename));
ArrayList<Put> puts = new ArrayList<>();
for (HbaseMessage message : listMessage) {
Put put = new Put(Bytes.toBytes(message.getKVmessagr().get("ID").toString()));
Map kVmessagr = message.getKVmessagr();
Iterator iterator = kVmessagr.keySet().iterator();
while (iterator.hasNext()) {
String key = iterator.next().toString();
put.addColumn(Bytes.toBytes(message.getCloumnFamil()), Bytes.toBytes(key), Bytes.toBytes(kVmessagr.get(key).toString()));
}
puts.add(put);
}
table.put(puts);
} catch (IOException e) {
e.printStackTrace();
/*
如果是RetriesExhaustedWithDetailsException类型的异常,
说明这些数据中有部分是写入失败的这通常都是因为
1:HBase集群的进程异常引起
2:有时也会因为有大量的Region正在被转移,导致尝试一定的次数后失败
*/
if (e instanceof RetriesExhaustedWithDetailsException) {
RetriesExhaustedWithDetailsException ree =
(RetriesExhaustedWithDetailsException) e;
int failures = ree.getNumExceptions();
for (int i = 0; i < failures; i++) {
errorList.add(ree.getRow(i));
}
}
} finally {
if (table != null) {
try {
table.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return true;
}
public static boolean putList(String tablename, List<HbaseMessage> listMessage) {
Table table = null;
//用于存放失败数据
List<Row> errorList = new ArrayList<Row>();
try {
table = connection.getTable(TableName.valueOf(tablename));
ArrayList<Put> puts = new ArrayList<>();
for (HbaseMessage message : listMessage) {
Put put = new Put(Bytes.toBytes(message.getKVmessagr().get("ID").toString()));
Map kVmessagr = message.getKVmessagr();
Iterator iterator = kVmessagr.keySet().iterator();
while (iterator.hasNext()) {
String key = iterator.next().toString();
put.addColumn(Bytes.toBytes(message.getCloumnFamil()), Bytes.toBytes(key), Bytes.toBytes(kVmessagr.get(key).toString()));
}
puts.add(put);
}
table.put(puts);
} catch (IOException e) {
e.printStackTrace();
/*
如果是RetriesExhaustedWithDetailsException类型的异常,
说明这些数据中有部分是写入失败的这通常都是因为
1:HBase集群的进程异常引起
2:有时也会因为有大量的Region正在被转移,导致尝试一定的次数后失败
*/
if (e instanceof RetriesExhaustedWithDetailsException) {
RetriesExhaustedWithDetailsException ree =
(RetriesExhaustedWithDetailsException) e;
int failures = ree.getNumExceptions();
for (int i = 0; i < failures; i++) {
errorList.add(ree.getRow(i));
}
}
} finally {
if (table != null) {
try {
table.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return true;
}
/*put单条数据*/
public static boolean putRow(String tableName, String rowKey, String columnFamilyName, String qualifier,
String value) {
//用于存放失败数据
List<Row> errorList = new ArrayList<Row>();
try {
Table table = connection.getTable(TableName.valueOf(tableName));
Put put = new Put(Bytes.toBytes(rowKey));
put.addColumn(Bytes.toBytes(columnFamilyName), Bytes.toBytes(qualifier), Bytes.toBytes(value));
table.put(put);
table.close();
} catch (IOException e) {
e.printStackTrace();
if (e instanceof RetriesExhaustedWithDetailsException) {
RetriesExhaustedWithDetailsException ree =
(RetriesExhaustedWithDetailsException) e;
int failures = ree.getNumExceptions();
for (int i = 0; i < failures; i++) {
errorList.add(ree.getRow(i));
}
}
}
return true;
}
table.exists(Get get)
/*判断rk数据是否存在*/
public static boolean existsRowKey(String tableName, String rowkey) {
boolean exists = false;
try {
Table table = connection.getTable(TableName.valueOf(tableName));
Get get = new Get(Bytes.toBytes(rowkey));
if (!get.isCheckExistenceOnly()) {
//数据存在
exists = table.exists(get);
}
} catch (IOException e) {
e.printStackTrace();
}
return exists;
}
public static boolean existsRowKey(String tableName, String rowkey) {
boolean exists = false;
try {
Table table = connection.getTable(TableName.valueOf(tableName));
Get get = new Get(Bytes.toBytes(rowkey));
if (!get.isCheckExistenceOnly()) {
//数据存在
exists = table.exists(get);
}
} catch (IOException e) {
e.printStackTrace();
}
return exists;
}
/*判断某条数据的某列是否存在*/
public static boolean existsColumn(String tableName, String row, String family) {
boolean exists = false;
try {
Get get = new Get(Bytes.toBytes(row));
get.addFamily(Bytes.toBytes(family));
Table table = connection.getTable(TableName.valueOf(tableName));
exists = table.exists(get);
} catch (IOException e) {
e.printStackTrace();
}
return exists;
}
public static boolean existsColumn(String tableName, String row, String family) {
boolean exists = false;
try {
Get get = new Get(Bytes.toBytes(row));
get.addFamily(Bytes.toBytes(family));
Table table = connection.getTable(TableName.valueOf(tableName));
exists = table.exists(get);
} catch (IOException e) {
e.printStackTrace();
}
return exists;
}
table.get(get)
/*get查询某条某列数据:指定表,rowkey,列*/
public static String getCell(String tableName, String rowKey, String columnFamily, String qualifier) {
try {
Table table = connection.getTable(TableName.valueOf(tableName));
Get get = new Get(Bytes.toBytes(rowKey));
//校验数据是否存在
if (!get.isCheckExistenceOnly()) {
get.addColumn(Bytes.toBytes(columnFamily), Bytes.toBytes(qualifier));
Result result = table.get(get);
byte[] resultValue = result.getValue(Bytes.toBytes(columnFamily), Bytes.toBytes(qualifier));
return Bytes.toString(resultValue);
} else {
return null;
}
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
/**
* 指定rk get一条数据
*
* @param columnFamily 列簇
*/
public static Map<byte[], byte[]> getCellFromRowKey(String tableName, String rowKey, String columnFamily) {
try {
Table table = connection.getTable(TableName.valueOf(tableName));
Get get = new Get(Bytes.toBytes(rowKey));
Result result = table.get(get);
NavigableMap<byte[], byte[]> familyMap = result.getFamilyMap(Bytes.toBytes(columnFamily));
return familyMap;
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
public static String getCell(String tableName, String rowKey, String columnFamily, String qualifier) {
try {
Table table = connection.getTable(TableName.valueOf(tableName));
Get get = new Get(Bytes.toBytes(rowKey));
//校验数据是否存在
if (!get.isCheckExistenceOnly()) {
get.addColumn(Bytes.toBytes(columnFamily), Bytes.toBytes(qualifier));
Result result = table.get(get);
byte[] resultValue = result.getValue(Bytes.toBytes(columnFamily), Bytes.toBytes(qualifier));
return Bytes.toString(resultValue);
} else {
return null;
}
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
/**
* 指定rk get一条数据
*
* @param columnFamily 列簇
*/
public static Map<byte[], byte[]> getCellFromRowKey(String tableName, String rowKey, String columnFamily) {
try {
Table table = connection.getTable(TableName.valueOf(tableName));
Get get = new Get(Bytes.toBytes(rowKey));
Result result = table.get(get);
NavigableMap<byte[], byte[]> familyMap = result.getFamilyMap(Bytes.toBytes(columnFamily));
return familyMap;
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
table.getScanner
/**
* 查询全表
*/
public static ResultScanner getScanner(String tableName) {
try {
Table table = connection.getTable(TableName.valueOf(tableName));
Scan scan = new Scan();
return table.getScanner(scan);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
/**
* 查询表中用过滤器指定数据
*
* @param tableName 表名
* @param filterList 过滤器
*/
public static ResultScanner getScanner(String tableName, FilterList filterList) {
try {
Table table = connection.getTable(TableName.valueOf(tableName));
Scan scan = new Scan();
scan.setFilter(filterList);
return table.getScanner(scan);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
/**
* 检索表中根据rowkey范围指定数据
*
* @param tableName 表名
* @param startRowKey 起始 RowKey
* @param endRowKey 终止 RowKey
* @param filterList 过滤器
*/
public static ResultScanner getScanner(String tableName, String startRowKey, String endRowKey, FilterList filterList) {
try {
Table table = connection.getTable(TableName.valueOf(tableName));
Scan scan = new Scan();
scan.withStartRow(Bytes.toBytes(startRowKey));
scan.withStopRow(Bytes.toBytes(endRowKey));
scan.setFilter(filterList);
return table.getScanner(scan);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
* 查询全表
*/
public static ResultScanner getScanner(String tableName) {
try {
Table table = connection.getTable(TableName.valueOf(tableName));
Scan scan = new Scan();
return table.getScanner(scan);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
/**
* 查询表中用过滤器指定数据
*
* @param tableName 表名
* @param filterList 过滤器
*/
public static ResultScanner getScanner(String tableName, FilterList filterList) {
try {
Table table = connection.getTable(TableName.valueOf(tableName));
Scan scan = new Scan();
scan.setFilter(filterList);
return table.getScanner(scan);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
/**
* 检索表中根据rowkey范围指定数据
*
* @param tableName 表名
* @param startRowKey 起始 RowKey
* @param endRowKey 终止 RowKey
* @param filterList 过滤器
*/
public static ResultScanner getScanner(String tableName, String startRowKey, String endRowKey, FilterList filterList) {
try {
Table table = connection.getTable(TableName.valueOf(tableName));
Scan scan = new Scan();
scan.withStartRow(Bytes.toBytes(startRowKey));
scan.withStopRow(Bytes.toBytes(endRowKey));
scan.setFilter(filterList);
return table.getScanner(scan);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
scan操作
Hbase客户端的Scan操作应该是比较复杂的RPC操作,为了满足客户端多样化的数据库查询需求,Scan必须能设置众多维度的属性。
常用的有startRow、endRow、Filter、caching、batch、reversed、maxResultSize、version、timeRange等
常用的有startRow、endRow、Filter、caching、batch、reversed、maxResultSize、version、timeRange等
用户每次执行scanner.next(),都会尝试去名为cache的队列中拿result(步骤4),如果cache队列为空,则会发起一次RPC向服务端请求当前scanner的后续result数据(步骤1)。客户端收到result列表后(步骤2),通过scanResultCache把这些results内的多个Cell进行重组,最终组成用户需要的result放入到Cache中(步骤3)。其中步骤1+步骤2+步骤3统称为loadCache操作。
为什么要在步骤3对Rpc Responce中的result进行重组呢?
这是因为RS为了避免被当前RPC请求耗尽资源,实现了多个维度的资源限制(例如timeout、单次RPC响应最大字节数),一旦某个维度资源到达阈值,就马上把当前拿到的Cell返回给客户端。这样客户端拿到的result可能就不是一行完整的数据,因此在步骤3需要对result进行重组。
scan中的几个重要的概念
caching
每次loadCache操作最多放caching个result到cache队列中;
控制caching,也就能控制每次loadCache向服务端请求的数据量
避免出现某一次scanner.next。操作耗时极长的情况
batch
用户拿到的result中最多含有一行数据中的bacth个cell
如果某一行有5个Cell, Scan设的batch为2,那么用户会拿到3个result,每个result中Cell个数依次为2, 2, 1
allowPartial
用户能容忍拿到一行部分cell的Result,设置了这个属性,将跳过图4-3中的第三步重组流程,直接把服务端收到的result返回
maxResultSize
loadCache时,单次RPC操作最多拿到maxResultSize字节的结果集。
https://blog.csdn.net/qq_44665283/article/details/125719969
filter
PrefixFilter
单独用性能低下
因为会扫码(-∞, prefix)之间的数据
优化
1. 用startRow+PrefixFilter
Scan scan = new Scan();
scan.withStartRow(Bytes.toBytes("def"));
scan.setFilter(new PrefixFilter(Bytes.toBytes("def")));
scan.withStartRow(Bytes.toBytes("def"));
scan.setFilter(new PrefixFilter(Bytes.toBytes("def")));
2. 更好的方法:不用PrefixFilter
Scan scan = new Scan();
scan.withStartRow(Bytes.toBytes("def"));
scan.withStopRow(Bytes.toBytes("deg"));
scan.withStartRow(Bytes.toBytes("def"));
scan.withStopRow(Bytes.toBytes("deg"));
少量写和批量写
超时设置
相关参数
hbase.rpc.timeout
表示单次RPC请求的超时时间,一旦单次RPC超过该时间,上层将收到TimeOutException,默认时间为60000ms.
hbase.client.retries.number
表示调用API时最多容许发生多少次RPC重试,默认为35次。
hbase.client.pause
表示连续两次RPC重试之间的休眠时间,默认为100ms
重试策略
第1次 RPC重试 100ms
第2次 RPC重试 200ms
第3次 RPC重试 300ms
第4次 RPC重试 500ms
第5次 RPC重试 1000ms
第6次 RPC重试 2000ms
按照默认配置将会卡在休眠和重试两个步骤。
第2次 RPC重试 200ms
第3次 RPC重试 300ms
第4次 RPC重试 500ms
第5次 RPC重试 1000ms
第6次 RPC重试 2000ms
按照默认配置将会卡在休眠和重试两个步骤。
若按照默认的hbase.client.retries.number=35,客户端可能长期卡在休眠和重试两个步骤中
hbase.client.operation.timeout
表示单次API的超时时间,默认值为1200000ms
注意,get/put/delete等表操作称为一次API操作,一次API可能会有多次RPC重试,这个operation.timeout限制的是API操作的总超时。
如何设置
根据业务指标要求,合理设置上述参数
读写流程及相关内部组件架构
数据写入流程(三个阶段)
1.客户端处理阶段 p94
主要内容
客户端将写入请求进行预处理,根据集群元数据hbase:meta定位待写入数据所在的Region及Region Server,并将请求转发给对应的RegionServer
2. Region写入阶段 p95
主要内容
Region server接收到写入请求之后,将数据解析出来,首先写入HLog(WAL),再写入对应Region列蔟的MemStore
主要步骤(p95 2.Region写入阶段图)
1. Aquire Locks
2. Update Latest Timestamp timestamps
3. Build WALEdit
4. Append WALEdit to WAL
1. 追加写入HLog(即WAL) p97-p98
HLog说明
说明
每个RegionServer有一个或多个HLog
默认1个
hbase 1.1 可以开启MultiWAL功能,允许多个HLog
每个HLog是多个Region共享的
因此一个HLog中可能包含多个表及多个列蔟的数据
文件结构(p64)
一个HLog对象由多个WALEntry组成
一个WALEntry表示一次行级更新的最小追加单元
WALEntry组成部分
HLogKey
由TableName、Region name以及sequence id组成
没有列蔟
sequenceid是region级别一次行级事务的自增序号。每个region都维护属于自己的sequenceid,不同region的sequenceid相互独立。
sequenceid是自增序号。很好理解,就是随着时间推移不断自增,不会减小。,不同region的sequenceid相互独立。
sequenceid是一次行级事务的自增序号。行级事务是什么?简单点说,就是更新一行中的多个列族、多个列,行级事务能够保证这次更新的原子性、一致性、持久性以及设置的隔离性,HBase会为一次行级事务分配一个自增序号。
sequenceid是 region级别 的自增序号。每个region都维护属于自己的sequenceid
参见:https://www.modb.pro/db/99815
WALEdit
表示一个事务中的更新集合
一次行级事务可以原子操作同一行中的多个列(多个列可以是不同的列蔟),一次事务用一个sequenceid表示
一个WALEdit包含多个KeyValue
存储目录及文件格式(p65)
存储目录:/hbase/WALs/文件名格式RegionServer域名,RegionServer端口,文件生成时间戳
文件名格式RegionServer域名%2cRegionServer端口%2c文件生成时间戳.default(命名空间?).时间戳
举例:
/hbase/WALs/hbase17.xj.org,60020,1505980274300/hbase17.xj.org%2c60020%2c1505980274300.default.1506184980449
查看HLog命令
./hbase hlog
HLog生命周期(P66)
1. 构建
Hbase任何写入(创建、更新、删除)都会先将记录追加到HLog文件中
2. 滚动
Hbase后台启动一个线程,定期(hbase.regionserver.logroll.period参数,默认1小时)进行日志滚动。
日志滚动会新疆一个新的日志文件,接收新的数据
目的主要是为了方便过期日志数据能够以文件的形式直接删除
3. 失效
写入到HLog的数据,会同时写入内存MemStore,一旦数据从MemStore落盘,变成HFile,对应的HLog文件就会失效
一旦HLog文件失效,就会从WALs目录移动到oldWALs
4. 删除
master后台启动一个线程,定期(hbase.master.cleaner.interval参数,默认1分钟)检查一次oldWALs目录,确认是否可以删除
确认条件
1. 该HLog是否还在参与主从复制,若参与,先不删除
2. 该HLog是否已经在oldWALs目录中存在是否超过10分钟(通过hbase.master.logcleaner.ttl配置,默认10分钟),超过则删除
持久化HLog等级(在可靠性与性能之间做取舍) p97
SKIP_WAL
只写MemStore,不写HLog文件,相当于是否写HLOG的总开关
ASYNC_WAL
异步将数据写入HLog文件
SYNC_WAL
同步将数据写入HLog文件
但执行的是flush操作,只写入文件系统中,并没有真正落盘
FSYNC_WAL
同步将数据写入HLog文件并强制落盘
USER_DEFAULT
由用户指定持久化等级,若没有指定,默认使用SYNC_WAL
HLog数据写入实现机制 p97
基于LMAX Disruptor框架的无锁有界队列模型实现
生产者、消费者模式
生产者
先将一个个的WALEntry放入队列
最后将wal.sync事件放入队列,之后工作线程阻塞,等待消费者通知解除阻塞
消费者
消费WALEntry,然后执行文件的append操作追加到日志文件中
此时可能没有实际落盘,只是加入到文件缓存
消费wal.async事件
执行数据落盘操作,并唤醒生产者工作线程取消阻塞
5. write back to MemStore
2. 随机写入MemStore p98中
MemStore数据结构 p67
内存中是基于JDK提供的跳跃表ConcurrentSkipListMap(简称CSLM,该对象实际被包装在CellSet中),即LSM的内存部分
ConcurrentSkipListMap
非常友好地支持大规模并发顺序写入
跳表的有序存储,有利于数据有序落盘,且有利于提升查找性能
跳跃表的查找、删除、插入的复杂度都是O(logN)
MemStore优化历程
1. 所有KevValue都在JVM中申请和释放
问题:容易产生FullGC
许多KeyValue对象在新生代来不及销毁进入老年代,等等FullGC时才会被销毁,会不断积累FullGC的次数,性能严重低下
KeyValue不断申请和释放,产生大量、非常小(一个KeyValue对象大小为120Byte)的内存碎片
当再次申请KeyValue时,找不到能容纳该对象的连续内存空间,频繁触发FullGC
解决思路
内存碎片粗话,足够容纳KeyValue对象所需的内存(比如让空闲碎片空间>2M),即MSLAB
2. MSLAB
借鉴了线程本地分配缓存(Thread-Local Allocation Buffer, TLAB)的内存管理方式,通过顺序化分配内存、内存数据分块等特性使得内存碎片更加粗粒度,有效改善Full GC情况
实现思路
每个MemStore会实例化得到一个MemStoreLAB对象
MemStoreLAB会申请一个2M(比一个KeyValue对象所需的内存空间120Byte大的多)大小的Chunk数组,同时维护一个Chunk偏移量,该偏移量初始值为0。
当一个KeyValue值插入MemStore后,MemStoreLAB会首先通过KeyValue.getBuffer()取得data数组,并将data数组复制到Chunk数组中,之后再将Chunk偏移量往前移动data. length。
将KeyValue复制到Chunk中后,生成一个Cell对象(这个Cell对象在源码中为ByteBufferChunkKeyValue),这个Cell对象指向Chunk中的KeyValue内存区域,包含指向Chunk中对应数据块的指针、offsize以及length
将这个Cell对象作为Key和Value写入CSLM中。
原生的KeyValue对象写入到Chunk之后就没有再被引用,所以很快就会被Young GC回收掉。
当前Chunk满了之后,再调用new byte[2 1024 1024]申请一个新的Chunk。
MemStore flush之后,对应的chunk数据落盘,该Chunk被GC回收
好处
Chunk被GC回收,释放的空间连续性更强,碎片力度(2M)更粗,不易引发FullGC
问题
容易引发YGC
一旦写满一个Chunk后,会在Edge区申请新内存,如果申请比较频繁就会导致JVM Edge区满,触发YGC
那Cell对象不会引起内存碎片?
这个笔者查阅了很多资料都没有找到相关的说明,个人理解是因为Cell相对原生KeyValue来说占用内存小的多(一个Cell对象为40 Byte,是一个KeyValue对象(大小120 Byte)的1/3),可以一定程度上可以忽略
解决办法
Chunk池化
MemStore flush之后,Chunk不被释放,而是交给chunk池管理,继续被复用
3. MSLAB+ChunkPool
实现思路
1)系统创建一个Chunk Pool来管理所有未被引用的Chunk,这些Chunk就不会再被JVM当作垃圾回收。
2)如果一个Chunk没有再被引用,将其放入Chunk Pool。
3)如果当前Chunk Pool已经达到了容量最大值,就不会再接纳新的Chunk。
4)如果需要申请新的Chunk来存储KeyValue,首先从Chunk Pool中获取,如果能够获取得到就重复利用,否则就重新申请一个新的Chunk。
2)如果一个Chunk没有再被引用,将其放入Chunk Pool。
3)如果当前Chunk Pool已经达到了容量最大值,就不会再接纳新的Chunk。
4)如果需要申请新的Chunk来存储KeyValue,首先从Chunk Pool中获取,如果能够获取得到就重复利用,否则就重新申请一个新的Chunk。
好处
不会产生大量碎片,较少出现YGC和FullGC
问题
CSLM的LSM实现机制,虽方便了插入和查找,但会导致许多问题
创建大量对象
CSLM的跳跃表,用两个对象Node和Index对象构建LSM树,最下一层是Node对象,上面的几层是Index对象,除了创建Node节点,还会创建索引节点,创建了太多内部对象
太多的内部对象,占用的内存多,内存利用率低
http://www.360doc.com/content/21/0329/21/13328254_969627458.shtml
中关于50M对象那几段内存的计算看不懂,但结论是内存占用多,对象多
中关于50M对象那几段内存的计算看不懂,但结论是内存占用多,对象多
128M MemStore都是一颗CLSM树,CLSM为实现LSM目的都是为了方便数据写入和查找操作,而LSM本身是内存和CPU开销都比较大的数据结构,而且创建了大量对象,这些对象占用了大量内存,大量挤占了真正有效的KeyValue内存空间,内存利用率低。
CLSM落盘后,内部创建的对象会被GC,随着大量Cell、Nde和Index对象的频繁创建和销毁,会产生较多的内存碎片
以上诸多因素,会引起比较严重的FullGC
解决办法,2个
在内存中只开辟一小块CLSM写入数据,写满后转换成其它类型的只读的、方便查找、且节省内存的数据结构,如数组。
替换JDK自身的CSLM实现
4. CompactionMemStore (p268 15.2)
Hbase 2.x的实现
实现思路
在MemStore中构造一小块基于CLSM结构的Segment,叫MutableSegment,大小2M(可配),接收写入数据
进一步优化
存入MutableSegment CSLM中的Cell对象也在chunk中申请
当MutableSegment数据写满后,将其转换为只读的数组结构(数组结构占用内存更小、内存利用率更高)的ImmutableSegment并按顺序放入FIFO队列(Pipeline对象),然后再初始化一个新的MutableSegment供后续数据写入
LSM结构转换成了数组结构,解决了CSLM本身生成太多无用内存(比如跳跃表最底层之上的所有节点的内存开销)、产生大量垃圾对象、内存利用率低和容易引发FullGC问题
ImmutableSegment只读,数组结构同样支持二分查找,且查找性能比CSLM还要好
为什么MutableSegment内部不是数组而是CSLM
因为CSLM的插入性能比数组好许多,数组插入会移动大量数据,因此不适合插入场景。
当队列中的ImmutableSegment达到一定数量后(可配,默认是2),对所有的ImmutableSegment执行Memory Compaction,将多个ImmutableSegment归并成一个ImmutableSegment,再次放入Pipeline队列,等待下次进一步归并。
归并过程可以做无效数据清理工作,无效数据包括
TTL过期数据
超过列蔟指定版本的列值
被用户删除的列值
在内存中执行归并后,可以在内存中放更多有效的数据,大大提高内存的利用率,使MemStore占用的内存增长变慢,触发MemStore Flush的频率降低,大大提高MemStore内存使用率。
同时,也会使得HFile数据量变大,磁盘上HFile数量会增长缓慢,进而带来以下收益
磁盘上的HFile 归并频率降低,无论Minor Comaction还是Major Compaction次数都会降低,节省磁盘和网络带宽
生成的HFile梳理少,读取性能提升
新写入数据在内存中停留的时间长,对那种写完立即读的场景,性能大大提升
未来还可进一步优化:
如将ImmutableSegment直接编码成HFile格式,这样在flush时,直接把内存数据写盘即可,进一步提升flush的速度、吞吐量和稳定性
5. 利用堆外内存
6. 阿里的CCSMap(CompactedConcurrentSkipListMap)
原理
阿里巴巴内部版本为了优化CSLM数据结构内存利用效率低所实现的一个新的数据结构。CCSMap数据结构的基本理念是将原生的ConcurrentSkipListMap进行压缩,压缩的直观效果如下图所示
解决目标
解决MSLAB+ChunkPool的问题
实现思路
1. 只有Node对象,去掉了Index对象,索引信息在Node对象上保存,节省了内存空间
为Node对象创建一个索引数组,表示该对象在LSM树中各层的索引
疑惑?
既然阿里的CSMap已经将LSM中的cell放到ChunkPool了,那么是不是可以将KeyValue可以直接存入Node对象,让KeyValue随同Node对象一起放入chunk中,而无需将其单独放到ChunkPool中,也无需封装成Cell???
5. 上述几个MemStore的问题及解决办法
子主题
为防止因JVM内存不断申请和释放,产生过多内存碎片,而频繁导致JVM的Full GC和YGC,
采用了MSLAB方式,并在内存中申请许多大的chunk块,并将这些chunk用chunk池来管理
采用了MSLAB方式,并在内存中申请许多大的chunk块,并将这些chunk用chunk池来管理
每个chunk块默认大小2M
减少碎片
MemStore申请内存时,从chunk中申请一个或多个chunk
若池内没有可用的chunk,则从JVM申请一个chunk
MemStore落盘后,占用的chunk会被放入池中供后续MemStore使用,不用归还JVM,不会产生碎片,也没有GC发生
内部会维护两个跳跃表ConcurrentSkipListMap,当需要刷盘时,先把当前ConcurrentSkipListMap冻结,生成snapshot,作为flush的输入。同时创建一个新的ConcurrentSkipListMap接收后续的写入数据
http://www.360doc.com/content/21/0329/21/13328254_969627458.shtml
MemStore写入流程
检查当前chunk是否写满,若已写满,则从chunk池(若池已满,则从JVM中)申请新chunk
将当前数据(KeyValue对象)写入chunk中指定的offset处
将当前的KeyValue对象(地址)写入跳跃表ConcurrentSkipListMap中
6. Release Row Locks
7. sync wal
8. Advance MVCC
为了写数据期间实现读写一致性,需要一致性控制,采取的机制是MVCC (Multiversion Concurrency Control)
参见: http://t.zoukankan.com/dailidong-p-7571245.html
3. MemStore flush阶段 p98
主要内容
当MemStore达到触发条件,会异步执行flush操作,将内存中的数据写入文件,形成HFile
重点问题
1. MemStore Flush的触发条件 p99下
1. MemStore级别限制
Region中的任意一个MemStore大小达到了上限(hbase.hregion.memstore.flush.size,默认128MB)
2. Region级别限制
当Region中所有Memstore的大小总和达到了上限(hbase.hregion.memstore.block.multiplier * hbase.hregion.memstore.flush.size,默认 4 * 128M = 512M),整个Region中的所有MemStore都会flush。 (执行更新操作前,checkresource操作)
3. Region Server级别限制:
当一个Region Server中所有Memstore的大小总和达到了上限时,触发flush
1. hbase.regionserver.global.memstore.size
RegionServer分配给MemStore的JVM堆内存比例,默认是40%
举例:JVM设置为10G, 则RegionServer分配给MemStore的所有内存为:10*40%=4G
只有参数1, 则当一个RegionServer里面全部的memStore内存加一起,占到当前RegionServer内存的40%,就会触发flush操作,并且阻塞所有的memStore的写操作。但这样做有些不合理,在达到阈值前应该给一个缓冲的余地,避免直接进入阻塞状态。
因此设计参数2
因此设计参数2
2. hbase.regionserver.global.memstore.size.lower.limit
默认95%,相当于上一个参数的95%
这个参数就会在参数1的基础上再乘0.95,得出的值为6.08G,当全部的memStore占用内存达到这个数值时,就会开始flush操作,并且不会阻塞
若写入操作频繁,达到了JVM大小*参数1, 则会阻塞并强制刷盘,直到降低到JVM大小*参数1*参数2之下
停止刷盘条件
MemStore降低到JVM大小*参数1*参数2之下,停止刷盘
4. Region Server中HLog数量或内存占用空间限制
当一个Region Server中HLog数量达到上限(可通过参数hbase.regionserver.maxlogs配置)时,系统会选取最早的一个 HLog对应的一个或多个Region进行flush。
设计原因
因为HLog的数量太多会影响容灾后的数据恢复效率,而当memStore里面的数据都flush到磁盘后,HLog会被清空。
不止HLog数量过多会触发,当HLog文件占用的内存空间达到阈值时,也会触发memStore的flush操作。需要注意的是,如果增大了memStore的内存,那么也要增大HLog文件的内存占用阈值,否则HLog文件达到阈值直接触发flush,而memStore还远没有达到阈值,这样增大memStroe的内存就没有意义了。
问题
HLog文件占用内存空间吗?
HLog文件占用的内存空间如何设置?
5. HBase定期刷新Memstore
默认周期为1小时,确保Memstore不会长时间没有持久化。
考虑到可能会有多个memStore同时达到了一小时的时间阈值,都需要进行flush操作,为了避免同时进行,导致资源被大量占用。定期自动刷写策略里面有一个延迟机制,memStore不会立刻开始flush,而是随机等待0到5分钟之后才会开始flush操作
6. 手动flush
用户通过shell命令之下 flush 'tablename',flush 'regionname'分别对一个表或一个region进行flush
7.数据更新操作引起
如put/delete等
2. flush操作粒度
以Region为单位进行flush。
只要Region中有一个MemStore触发了flush,
则这个Region中的所有MemStore(无论是否写满)都需要flush
只要Region中有一个MemStore触发了flush,
则这个Region中的所有MemStore(无论是否写满)都需要flush
原因
Hbase为每一个Region而不是每一个MemStore维护了一个自增的sequenceid
每次写HLog时,都会写入数据所在Region的自增sequenceid
MemStore数据落盘后,Hbase会记录并将已落盘的数据对应的最大的SequenceId(叫做oldestUnflushedSequenceId)存储到HLog中
每次flush所生成的所有HFlie中都存储同一个sequenceId
由于Hbase以Region为单位,而不是以MemStore为单位维护oldestUnflushedSequenceId,因此每次flush时,必须刷新该Region下的所有MemStore,无论该MemStore是否已满
为什么不以MemStore(即列蔟)为单位?
个人理解:要保证行级事务
假如某个表有两个列族,某次写入了两个列蔟的数据,那么在执行写入时,从业务上希望该行数据保持事务一致性,即两个列蔟中的列要么都写入成功,要么都写入失败
若以列蔟为单位记录sequenceid,则无法保证事务
参见:https://www.it1352.com/2269279.html
3. flush流程(p99)
为了减少flush过程对读写的影响,HBase采用了类似
于两阶段提交的方式,将整个flush过程分为三个阶段
于两阶段提交的方式,将整个flush过程分为三个阶段
prepare阶段
对触发了flush操作的MemStore所在的Region中的所有Memstore生成snapshot
遍历触发了flush操作的MemStore所在的Region中的所有Memstore,将Memstore中当前数据集CellSet(内部是CSLM)做一个快照snapshot,然后再新建一个新的kvset。后期的所有写入操作都会写入新的CellSet中,而整个flush阶段读操作会首先分别遍历CellSet和snapshot,如果查找不到再会到HFile中查找。prepare阶段需要加一把updateLock对写请求阻塞,结束之后会释放该锁。因为此阶段没有任何费时操作,因此持锁时间很短。
为什么要将
flush阶段
遍历所有Memstore,将prepare阶段生成的snapshot持久化为临时HFile文件,临时文件会统一放到目录.tmp下。这个过程因为涉及到磁盘IO操作,因此相对比较耗时。
HFile文件结构(p72-p84 5.4HFile)
HFile文件结构
HFile文件主要分为四个部分
Scanned block section
顾名思义,表示顺序扫描HFile时所有的数据块将会被读取,包括DataBlock、Leaf Index Block和Bloom Block
Non-scanned block section
表示在HFile顺序扫描的时候数据不会被读取,主要包括Meta Block和Intermediate Level Data Index Blocks两部分。
Load-on-Open
这部分数据在RegionServer打开HFile时直接加载到内存中。包括FileInfo、布隆过滤器MetaBlock、Root Data Index和Meta Index Block
Trailer
主要记录了HFile的版本信息、其它各部分的偏移量和寻址信息、文件大小(未压缩)、压缩算法、存储KV个数等信息
大小固定
Block
HFile由不同类型的块组成,快大小可以在创建表列蔟的时候指定(blocksize=>65535),默认为32M。
大号的Block有利于顺序Scan,小号Block利于随机查询,因而需要权衡
Block是HFile文件读取的最小单元
这些块的类型不同,但却有相同的数据结构HFileBlock。
主要包括两部分
BlockHeader
主要存储block元数据
最核心的字段是BlockType字段,用来标示该block块的类型,HBase中定义了8种BlockType(p75表格),
每种BlockType对应的block都存储不同的数据内容,有的存储用户数据,有的存储索引数据,有的存储meta元数据
每种BlockType对应的block都存储不同的数据内容,有的存储用户数据,有的存储索引数据,有的存储meta元数据
HFile中各块的一对多关系见p73上
BlockData
来存储具体数据
对于任意一种类型的HFileBlock, 都拥有相同结构的BlockHeader , 但是BlockData结构却不相同
HFile包含的内容
KV数据,放到DataBlock中
写入的KV数据可以压缩,尤其是rowkey部分(p102M)
在Scanned block section区
DataBlock的索引,树状索引(树状),便于从HFile中定位Block。
包括Root Data Index、Intermediate Level Data Index以及Leaf Index,分别存储在对应的三种类型的Block中
包括Root Data Index、Intermediate Level Data Index以及Leaf Index,分别存储在对应的三种类型的Block中
HFile越大,DataBlock越多,需要建立索引以加快文件查找速度
HFile越大索引树层数越多
一条索引项指向一块DataBlock,而非一条KV
Root Data Index、Intermediate Level Data Index以及Leaf Index在不同的区
KV数据的布隆过滤器
存储到Bloom Block中
Scanned block section区
HFile中有多个布隆过滤器位数组
为什么有多个而非一个?
HFile文件越大,里面存储的KeyValue值越多,位数组就会相应越大。一旦位数组太大就不适合直接加载到内存了,因此HFile V2在设计上将位数组进行了拆分,拆成了多个独立的位数组(根据Key进行拆分,一部分连续的Key使用一个位数组)。这样,一个HFile中就会包含多个位数组,根据Key进行查询时,首先会定位到具体的位数组,只需要加载此位数组到内存进行过滤即可,从而降低了内存开销。
布隆过滤器的索引
为什么有布隆过滤器索引
便于查找布隆过滤器
存储位置
布隆过滤器MetaBlock
在Load-on-Open区
每一个Bloom Index Entry指向存储在BloomBlock中的某个布隆过滤器(位数组)
一个BloomBlock可能包含多个布隆过滤器
Bloom Index Entry 结构 p78图
BlockOffset
找到布隆过滤器维数组的位置:该索引项在哪个BloomBlock中的哪个偏移处
BlockOndiskSize
位数组长度,从BlockOffset开始,提取BlockOndiskSize长度的数据,即为维数组
BlockKey
是一个非常关键的字段,表示该Index Entry指向的布隆过滤器位图中,第一个执行Hash映射的rowKey
因为HFile 中的rowKey是按字母排序的,第一个执行布隆过滤器Hash映射的rowKey一定是这个布隆过滤器对应的KV集合中最小的那个rowKey
BlockKeyLen
BlockKey的长度
get(rowKey)请求查找过程
1. 根据rowKey,通过二分查找法,比对rowKey布隆过滤器索引中的Bloom Index Entry,找到对应的布隆过滤器维数组
比对依据
用客户请求的rowKey与Bloom Index Entry的BlockKey进行比对
若rowKey>BlockKey
继续比对该Bloom Index Entry节点的右侧子树
若rowKey<BlockKey
继续比对继续比对该Bloom Index Entry节点的左侧侧子树
命中条件
某个BloomIndexEntry.BlockKey<=rowkey<该BloomIndexEntry的
下一个BloomIndexEntry.BlockKey,则该BloomIndexEntry代表的布隆过滤器位数组被命中
下一个BloomIndexEntry.BlockKey,则该BloomIndexEntry代表的布隆过滤器位数组被命中
2. 根据布隆过滤器计算规则,对rowKey做hash映射,根据映射结果在位图中查看是否都为1
如果不是,表示布隆过滤器对应的KV集合中肯定不存在该rowKey
若是,则可能存在,需要进一步加载位图对应的KV集合逐个比对
若找到则返回找到的数据给客户端,退出
若找不到,则说明客户请求的rowKey在Hbase中不存在,返回客户端null,退出
commit阶段
遍历所有的Memstore,将flush阶段生成的临时文件移到指定的ColumnFamily目录下,针对HFile生成对应的storefile和Reader,把storefile添加到HStore的storefiles列表中,最后再清空prepare阶段生成的snapshot。
flush操作的性能影响
flush操作会阻塞用户的写请求,会对用户业务产生影响
触发Region级别的flush,只会阻塞对应Region上的写请求,阻塞时间较短,影响不大
触发RegionServer级别的flush,会阻塞该RegionServer上的所有写请求,直至MemStore数据量降低到配置的阈值内,耗时比较长,影响比较大
数据读取流程
批量数据导入BulkLoad(p104 6.2)
场景:比如系统上线时,有大量数据需要载入Hbase,通过API方式会使HBase承受不了巨大写入的内存和网络压力而崩溃
Coprocessor
Compaction实现
负载均衡
复制
备份与恢复
运维
系统调优
高级话题
二级索引
单行事务与跨行事务
Hbase开发与测试
常见业务场景
Hbase数据复制或迁移
从非Hbase数据导入Hbase(如项目上线)
首次全量
持续增量
HBase->Hbase(如主从复制)
全量复制
增量复制
Hbase->非Hbase
备份容灾
监控
管理操作
rowkey设计
数据过于离散影响性能
热点数据打到同一台机器上,造成机器过载
一次scan流程走过的客户端、服务端各个组件
分支主题
0 条评论
下一页