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

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


  • 首页

  • 归档

  • 搜索

一键创建k8s用户并授于查看pod日志权限

发表于 2021-11-01

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

✍️前言

✨本文一键脚本 根据 juejin.cn/post/701773… 这位小哥的文章,二次开发而来,小哥的内容说得比较详细,如果看了本文脚本,不明其意的同学,可以看看这位小哥的文章

🙋‍♂️本着凡是自动化能解决的,决不一条一条命令去敲,博主决定写个一键创建用户,然后授于查看pod权限的脚本

🙆‍♂️下图为脚本运行截图,可见非常方便。。还可以集成到Golang和php代码中哦。
image.png

✍️脚本内容如下

命名为: create_user.sh 执行: bash create_user.sh

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
95
96
97
98
99
js复制代码#!/bin/bash

#异常直接中断流程,退出脚本
set -e

#接收两个参数,第1个参数为用户名,第2个参数为命令空间(可选)
name=$1
namespace=$2

#如果不设置用户名,默认为dmp+随机数
if [ -z $name ];then
name=dmp`date +%N`
fi

#如果不设置namespace,默认为test命令空间
if [ -z $namespace ];then
namespace="test"
fi

group=peach
caPath=/etc/kubernetes/pki/

#1. 创建用户证书私钥
openssl genrsa -out ${name}.key 2048

#2. 创建证书签名文件
openssl req -new -key ${name}.key -out ${name}.csr -subj "/CN=${name}/O=${group}"

#3. 生成最终证书文件
openssl x509 -req -in ${name}.csr -CA ${caPath}ca.crt -CAkey ${caPath}ca.key -CAcreateserial -out ${name}.crt -days 3650

#4. 创建集群凭证
kubectl config set-credentials ${name} --client-certificate=${name}.crt --client-key=${name}.key

#5. 创建集群上下文context
cluster=`kubectl config get-clusters | grep -v NAME`
kubectl config set-context ${name}-context --cluster=${cluster} --namespace=${namespace} --user=${name}

#6. 创建用户成功提示
echo "----------------"
echo "创建用户成功\n"
echo "用户名: ${name} namespace: $namespace\n"
#查看当前已创建的users和contexts
echo "------显示user列表-------" && kubectl config get-users
echo "------显示context列表-------" && kubectl config get-contexts
echo "----------------"
echo "接下来创建k8s集群角色和权限"

#7. 编写角色配置文件
cat >> role.yaml << EOF
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: ${namespace}
name: ${name}-log-role
rules:
- apiGroups: [""]
resources: ["pods", "pods/log"]
verbs: ["get", "list"]

EOF

# 替换角色配置(可选)
#sed 's/NS/${namespace}/g' role.yaml
#8. 创建角色
kubectl apply -f role.yaml

#9. 编写角色权限绑定配置文件
cat >> rolebinding.yaml << EOF
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: ${name}-hangzhou-rolebinding
namespace: ${namespace}
subjects:
- kind: User
name: ${name}
apiGroup: ""
roleRef:
kind: Role
name: ${namespace}-log-role
apiGroup: ""
EOF

#10. 创建角色权限绑定
kubectl apply -f rolebinding.yaml

#11. 创建权限成功提示
echo "----------------"
echo "创建角色权限成功\n"
#权限测试
kubectl get pods -n ${namespace} --context=${name}-context
#查看角色和角色绑定列表并过滤用户名
echo "-----查看角色列表----"
kubectl get role -A | grep ${name}
echo "-----查看角色绑定列表----"
kubectl get rolebinding -A | grep ${name}
echo "查看pod命令: kubectl get pods -n ${namespace} --context=${name}-context"
echo "-------脚本处理完成,谢谢使用---------"

😅脚本中有非常详细的注释,方便看官们快速理解哦

本文转载自: 掘金

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

😸都闪一闪!你们的喵咪都是我的了!

发表于 2021-11-01

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

最近掘友们在疯狂的晒自己的猫猫,同时,又有广大掘友在疯狂的云吸猫!

我也一样,但是我觉得有点不过瘾,如果能够把你们的猫都搞到手就好了,全部为我所有!

但这不可能实现,很失望,怎么办呢?

814268e3gw1f1980afb74j205k05dwel.jpg

有了!!!虽然搞不到你们的猫,那就退一步,搞到你们的猫的照片,四舍五入一下,不就相当于搞到你们的猫了吗!机智!!!!

那怎样搞到所有猫的照片呢?

一个一个的右键另存为? 不可能的!我来爬一下试试!

掘金运营大大不要封我ip封我号,毕竟我只是一个想撸猫的普通群众(小声哔哔)!

9e31678cgy1fve1k7ynobj204k03cwea.jpg

开干!!

分析网站

通过抓包,我找到了沸点的请求接口,如下:
image.png
我大胆猜一下,"theme_id": "7007350783603638279"大概就是”猫星人最可爱的一点”的话题,limit是每次请求的沸点条数,sort是热门和最新两种排序方式,cursor大概就是游标,代表从哪一条开始请求。

通过分析返回体,证明我的猜想是正确的,哈!

image.png

image.png

image.png

而且,我在返回体里找到了,你们的用户名,沸点的内容,以及你们发的猫····的照片url链接!优秀啊!!

那么现在,只差代码了。这都是小事!

编写程序

到这里,我似乎发现事情有些不对了!

image.png

上面说的cursor是一个”eyJ2IjoiNzAyMzU5NjI1MjkzNTc2NjA1MyIsImkiOjEyMH0=”,这是个什么玩意,明显加密了啊!

于是,我开始尝试解密,看看加密前的是什么。我发现是通过Base64的方式进行加密的,哇,也太仗义了,我以为会是什么奇奇怪怪的加密方式,就到这走不下去了呢!

如下,解密出来是{"v":"7023596252935766053","i":100}字符串,根据我的经验来看,里面的i就是游标,代表着从那一条开始。
image.png
整明白了!开干!

代码实现

请求数据加密

1
2
3
4
5
6
7
8
python复制代码def base64_encrypt(text):
"""
base64加密
:param text: 文本
:return:
"""
encrypt = base64.b64encode(str(text).encode('utf-8'))
return str(encrypt, "utf-8")

单页爬取测试

先把cursor数据内的i设置为0,爬取第一页测试一下:

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
python复制代码import base64
import requests


def jujin_cat_spider():
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/81.0.4044.43 Safari/537.36',
'Referer': 'https://juejin.cn/'
}
url = "https://api.juejin.cn/content_api/v1/short_msg/list_by_theme?aid=2608&uuid=6983930498641806848"

cursor = '{"v":"7023596252935766053","i":0}'

cursor = base64_encrypt(cursor)
print("加密后:", cursor)

data = {
"theme_id": "7007350783603638279",
"sort": 1,
"limit": 20,
"cursor": cursor
}
res = requests.post(url, json=data, headers=headers)
print(res.json())


jujin_cat_spider()

经测试,完美运行,可以得到第一页的数据,上面已经说过,返回体内的msg_Info里面的pic_list是猫的照片列表,循环所有的沸点数据,再循环照片列表把照片下载下来即可。

我感觉胜利就在眼前了,这里提前庆祝一下~

2021-05-12 125053.gif

翻页爬取

接下来唯一要做的就是构造翻页请求了,通过查看返回体,我看到有个has_more属性,代表着是否有下一页,那就好办了,判断一下就可以,如果有下一页,把cursor数据内的i加20,请求下一页的数据就行了,知道has_more为Fslse停止。

运行过程截图:
image.png

大功告成!!
共爬下来几百张猫猫的照片,下面是部分截图,看看有没有你家的哈哈哈!
image.png

完整代码就不放了,毕竟我还要在掘金混的,万一造成不好的影响,啪!把我号一封!我以后还怎么在掘金摸鱼哈哈哈!

原创不易,如果小伙伴们觉得有帮助,麻烦点个赞再走呗~

最后,感谢女朋友在工作和生活中的包容、理解与支持 !

本文转载自: 掘金

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

数据泵(EXPDP)异机导出---network_link

发表于 2021-11-01

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

📚 前言

数据泵(EXPDP)异机导出是日常工作中经常使用的技能,但是数据泵是基于服务端执行导出的,有些用户无法登录服务端主机,因为无法使用该方式进行的导出。

因此,Oracle提供了 network_Link 参数,使用dblink远程导出的方式来实现!

实战演示

本文通过实战演示如何配置以及使用数据泵(EXPDP)异机导出。

1、配置TNS

配置dblink建议首先配置 TNS解析:

1
2
3
4
5
6
7
8
9
10
11
bash复制代码##配置tnsnames.ora
cat <<EOF>>$TNS_ADMIN/tnsnames.ora
ORCL =
(DESCRIPTION =
(ADDRESS = (PROTOCOL = TCP)(HOST = 10.211.55.100)(PORT = 1521))
(CONNECT_DATA =
(SERVER = DEDICATED)
(SERVICE_NAME = orcl)
)
)
EOF

2、创建数据泵导出目录

由于数据泵导出需要指定目录,因此需要创建导出目录:

1
2
3
4
bash复制代码##创建导出目录
mkdir -p /oradata/rmanbak/orcl
sqlplus / as sysdba
create or replace directory DATA_DUMP_ORCL as '/oradata/rmanbak/orcl';

3、创建DBLINK

上述准备工作做好之后,创建 dblink:

1
2
3
4
5
bash复制代码##创建ORCL的dblink用来传输dump到本地
sqlplus / as sysdba
create public database link DBLINK_ORCL connect to SYSTEM identified by using 'ORCL';

--drop public database link DBLINK_ORCL;

4、导出角色

执行导出命令,导出数据库所有角色:

1
2
bash复制代码--远程导出角色
expdp system/oracle network_link=dblink_orcl directory=DATA_DUMP_ORCL dumpfile=role.dmp full=y include=role LOGFILE=role_expdp.log

5、导出指定用户,排除部分表

执行导出命令,导出数据库指定用户:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
bash复制代码##编写expdp导出命令脚本
cat <<EOF>>/oradata/rmanbak/orcl/expdp_all_orcl.txt
directory=DATA_DUMP_ORCL
dumpfile=orcl_\%U.dmp
logfile=orcl_exp.log
SCHEMAS='A','B','C','D'
EXCLUDE=table:"in('AAAAA')"
parallel=4
filesize=30G
network_link=DBLINK_ORCL
EOF

##授予执行权限
chmod +x expdp_all_orcl.txt

expdp system/oracle parfile=expdp_all_orcl.txt

注意:这里的system密码是目标端数据库的密码。

6、导出结果

执行完导出后,导出结果如下:

1
2
3
4
5
6
7
8
9
bash复制代码Dump file set for SYSTEM.SYS_EXPORT_SCHEMA_01 is:
/oradata/rmanbak/orcl/orcl_01.dmp
/oradata/rmanbak/orcl/orcl_02.dmp
/oradata/rmanbak/orcl/orcl_03.dmp
/oradata/rmanbak/orcl/orcl_04.dmp
/oradata/rmanbak/orcl/orcl_05.dmp
/oradata/rmanbak/orcl/orcl_06.dmp
/oradata/rmanbak/orcl/orcl_07.dmp
/oradata/rmanbak/orcl/orcl_08.dmp

当然,使用该方式仍有一些限制:

限制:

1. 带有long数据类型的表不能在expdp 加NETWORK_LINK时被导出。

2.带有object_type数据类型的表不能在expdp 加NETWORK_LINK时被导出。

3. 在expdp 加NETWORK_LINK时,不能导出分区表中的某个分区。

参考文献:

Export/Import DataPump Parameter VERSION - Compatibility of Data Pump Between Different Oracle Versions [Video] [ID 553337.1]

如果觉得文章对你有帮助,点赞、收藏、关注、评论,一键四连支持,你的支持就是我创作最大的动力,技术交流可以添加公众号:Lucifer三思而后行~

本文转载自: 掘金

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

看动画学算法之 双向队列dequeue 简介 双向队列的实现

发表于 2021-11-01

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

简介

dequeue指的是双向队列,可以分别从队列的头部插入和获取数据,也可以从队列的尾部插入和获取数据。

本文将会介绍一下怎么创建dequeue和dequeue的一些基本操作。

双向队列的实现

和普通队列项目,双向队列可以分别在头部和尾部进行插入和删除工作,所以一个dequeue需要实现这4个方法:

  • insertFront(): 从dequeue头部插入数据
  • insertLast(): 从dequeue尾部插入数据
  • deleteFront(): 从dequeue头部删除数据
  • deleteLast(): 从dequeue尾部删除数据

同样的我们也需要一个head和一个rear来指向队列的头部和尾部节点。

也就是说实现了这四个方法的队列就是双向队列。我们不管它内部是怎么实现的。

接下来我们来直观的感受一下dequeue的插入和删除操作:

  1. 在头部插入

  1. 在尾部插入

  1. 在头部删除

  1. 在尾部删除

双向队列也可以有很多种实现方式,比如循环数组和链表。

双向队列的数组实现

因为数组本身已经有前后关系,也就是说知道head可以拿到它后面一个数据,知道rear也可以拿到它前面一个数据。

所以数组的实现中,存储head和rear的index值已经够了。

我们只需要添加向头部插入数据和向尾部删除数据的方法即可:

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
java复制代码
//从头部入队列
public void insertFront(int data){
if(isFull()){
System.out.println("Queue is full");
}else{
//从头部插入ArrayDeque
head = (head + capacity - 1) % capacity;
array[head]= data;
//如果插入之前队列为空,将real指向head
if(rear == -1 ){
rear = head;
}
}
}

//从尾部取数据
public int deleteLast(){
int data;
if(isEmpty()){
System.out.println("Queue is empty");
return -1;
}else{
data= array[rear];
//如果只有一个元素,则重置head和real
if(head == rear){
head= -1;
rear = -1;
}else{
rear = (rear + capacity - 1)%capacity;
}
return data;
}
}

双向队列的动态数组实现

动态数组可以动态改变数组大小,这里我们使用倍增的方式来扩展数组。

看下扩展方法怎么实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码    //因为是循环数组,这里不能做简单的数组拷贝
private void extendQueue(){
int newCapacity= capacity*2;
int[] newArray= new int[newCapacity];
//先全部拷贝
System.arraycopy(array,0,newArray,0,array.length);
//如果rear<head,表示已经进行循环了,需要将0-head之间的数据置空,并将数据拷贝到新数组的相应位置
if(rear < head){
for(int i=0; i< head; i++){
//重置0-head的数据
newArray[i]= -1;
//拷贝到新的位置
newArray[i+capacity]=array[i];
}
//重置rear的位置
rear = rear +capacity;
//重置capacity和array
capacity=newCapacity;
array=newArray;
}
}

因为是循环数组,这里不能做简单的数组拷贝,我们需要判断rear和head的位置来判断是否进入到了循环结构。

如果进入到了循环结构,我们需要重置相应的字段数据,并拷贝到新数组中。

向头部插入数据和向尾部删除数据的方法和基本队列的实现是一致的,这里就不列出来了。

双向队列的链表实现

如果使用链表来实现双向队列会有什么问题呢?

在头部插入和在尾部插入都可以快速定位到目标节点。但是我们考虑一下尾部删除的问题。

尾部删除我们需要找到尾部节点的前一个节点,将这个节点置位rear节点。这就需要我们能够通过rear节点找到它的前一个节点。

所以基本的链表已经满足不了我们的需求了。 这里我们需要使用双向链表。

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
java复制代码public class LinkedListDeQueue {
//head节点
private Node headNode;
//rear节点
private Node rearNode;

class Node {
int data;
Node next;
Node prev;
//Node的构造函数
Node(int d) {
data = d;
}
}

public boolean isEmpty(){
return headNode==null;
}

//从队尾插入
public void insertLast(int data){
Node newNode= new Node(data);
//将rearNode的next指向新插入的节点
if(rearNode !=null){
rearNode.next=newNode;
newNode.prev=rearNode;
}
rearNode=newNode;
if(headNode == null){
headNode=newNode;
}
}

//从队首插入
public void insertFront(int data){
if(headNode == null){
headNode= new Node(data);
}else{
Node newNode= new Node(data);
newNode.next= headNode;
headNode.prev= newNode;
headNode= newNode;
}
}

//从队首删除
public int deleteFront(){
int data;
if(isEmpty()){
System.out.println("Queue is empty");
return -1;
}else{
data=headNode.data;
headNode=headNode.next;
headNode.prev=null;
}
return data;
}

//从队尾删除
public int deleteLast(){
int data;
if(isEmpty()){
System.out.println("Queue is empty");
return -1;
}else{
data=rearNode.data;
rearNode=rearNode.prev;
rearNode.next=null;
}
return data;
}

}

双向链表中的每一个节点都有next和prev两个指针。通过这两个指针,我们可以快速定位到他们的后一个节点和前一个节点。

双向链表的时间复杂度

上面的3种实现的enQueue和deQueue方法,基本上都可以立马定位到要入队列或者出队列的位置,所以他们的时间复杂度是O(1)。

本文的代码地址:

learn-algorithm

本文已收录于 www.flydean.com/13-algorith…

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧等你来发现!

欢迎关注我的公众号:「程序那些事」,懂技术,更懂你!

本文转载自: 掘金

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

设计模式——装饰器模式 概述Decorator 实现 总结

发表于 2021-11-01

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

概述Decorator

目的

向一个存在的对象中增加新的功能,且不改其结构。相对于子类更加灵活且没有那么臃肿

描述

就像是名称一样“装饰”,装饰即给物品增加了新的功能“展览”,又保留物品原本的功能属性。而这个装饰的存在与否不影响这个物品的本质功能

使用场景

  1. 动态增加功能,动态撤销
  2. 类功能类似于子类的扩展

例子

「杯子」杯子本来可以用来装水,但是杯子挂了一个挂件

「肉夹馍」肉夹馍摊子上

  • 只有一个馍
  • 带有肉的馍
    • 带有牛肉的馍
    • 带有猪肉的馍
  • 带有包装袋的馍
    • 带有纸袋的馍
    • 带有塑料袋的馍
  • 带有塑料袋和牛肉的馍
  • 。。。。。。

实现

逻辑实现

  1. 杯子这个接口,有一个方法,takeInWater();装水
  2. 有大杯子和小杯子,大杯子可以装1000ml,小杯子可以装500ml
  3. 有一个带有装饰物的杯子的抽象类,其中有一个属性是杯子,还有一个方法是挂件pendant()
  4. 带有装饰物的杯子的实现类,pendant()方法是挂了一个小猪

代码实现

  1. 创建基类的接口Cup,并定义一个方法“装水”
1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码/**
* @ClassName Cup
* @Desc 杯子的接口
* @Author YangMingYu
* @Date 2021/10/29 3:54 下午
* @Version 1.0
**/
public interface Cup {
/**
* 装水
*/
public void takeInWater();
}
  1. 创建两个接口的实现largeCup和smallCup
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复制代码/**
* @ClassName LargeCup
* @Desc 大杯子可以装1000ml
* @Author YangMingYu
* @Date 2021/10/29 3:54 下午
* @Version 1.0
**/
public class LargeCup implements Cup{
@Override
public void takeInWater() {
System.out.println("i can take 1000ml water!!!");
}
}

/**
* @ClassName SmallCup
* @Desc 小杯子可以装500ml
* @Author YangMingYu
* @Date 2021/10/29 3:55 下午
* @Version 1.0
**/
public class SmallCup implements Cup {
@Override
public void takeInWater() {
System.out.println("i can only take 500ml water!!!");
}
}
  1. 创建Cup的装饰器DecoratorCup,带有挂件的杯子
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
java复制代码/**
* @ClassName CupDecorator
* @Desc 带有装饰物的杯子
* @Author YangMingYu
* @Date 2021/10/29 3:56 下午
* @Version 1.0
**/
public abstract class DecoratorCup implements Cup {

protected Cup decoratorCup;

public DecoratorCup(Cup cup) {
this.decoratorCup = cup;
}

/**
* 实现装水的操作
*/
public void takeInWater() {
decoratorCup.takeInWater();
takeDecorator();
}

/**
* 带挂件,具体挂件是什么实现类决定
*/
public abstract void takeDecorator();

}
  1. 实现装饰器,小熊挂件的杯子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码/**
* @ClassName LittleBearDecoratorCup
* @Desc 带有小熊装饰物的杯子
* @Author YangMingYu
* @Date 2021/10/29 3:59 下午
* @Version 1.0
**/
public class LittleBearDecoratorCup extends DecoratorCup {

public LittleBearDecoratorCup(Cup cup) {
super(cup);
}

public void takeDecorator() {
System.out.println("i take a little bear");
}
}

总结

使用装饰器模式:创建的对象可以是没有装饰器的也可以是带有各种各样的装饰器的。

也就是这个只是一个装饰,改变不了本质是这个馍可以吃

然后这个装饰可有可无,并不影响吃这个操作

只是自己确实很不理解,为什么要将这个模式归于结构型模式,在开发中也使用过(肯定没有这么标准的使用,装饰器并没有创建抽象类再去实现)但是还是归咎于是在创建自己需要的对象。这样看来又像是创建型模式

最后补充一句,也是看到好多博客都在说的一个问题,确实如此,这种装饰器模式其实有些违背了开发的原则,也就是很多功能在开发后期进行一个补充操作。而且装饰器和原本的接口其实是完全区分出来了的,也就是后期进行维护的时候,可能根本就想不起来这边还有一个装饰器。所以活用活现吧,具体问题具体分析

本文转载自: 掘金

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

大数据集群被窃取数据怎么办?透明加密可以一试

发表于 2021-11-01

​​摘要:传统大数据集群中,用户数据明文保存在HDFS中,集群的维护人员或者恶意攻击者可在OS层面绕过HDFS的权限控制机制或者窃取磁盘直接访问用户数据。

本文分享自华为云社区《FusionInsight MRS透明加密方案》,作者: 一枚核桃 。

概述

传统大数据集群中,用户数据明文保存在HDFS中,集群的维护人员或者恶意攻击者可在OS层面绕过HDFS的权限控制机制或者窃取磁盘直接访问用户数据。

FusionInsight MRS引入了Hadoop KMS服务并进行增强,通过对接第三方KMS,可实现数据的透明加密,保障用户数据安全。

  • HDFS支持透明加密,Hive、HBase等在HDFS保存数据的上层组件也将通过HDFS加密保护,加密密钥通过HadoopKMS从第三方KMS获取。
  • 对于Kafka、Redis等业务数据直接持久化存储到本地磁盘的组件,通过基于LUKS的分区加密机制保护用户数据安全。

HDFS透明加密

  • HDFS透明加密支持AES、SM4/CTR/NOPADDING加密算法,Hive、HBase使用HDFS透明加密做数据加密保护。SM4加密算法由A-LAB基于OpenSSL提供。
  • 加密使用的密钥从集群内的KMS服务获取,KMS服务支持基于Hadoop KMS REST API对接第三方KMS。
  • 一套FusionInsight Manager内部署一个KMS服务,KMS服务到第三方KMS使用公私钥认证,每个KMS服务在第三方KMS对应拥有一个CLK。
  • 在CLK下可以申请多个EZK,与HDFS上的加密区对应,用于加密数据加密密钥,EZK在第三方KMS中持久化保存。
  • DEK由第三方KMS生成,通过EZK加密后持久化保存到NameNode中,使用的时候使用EZK解密。
  • CLK和EZK两层密钥可以轮转。CLK作为每个集群的根密钥,在集群侧不感知,轮转完全由第三方KMS控制管理。EZK可通过FI KMS管理,轮转在FI KMS可控制管理,同时第三方KMS管理员拥有KMS内密钥的管理能力,也可以做EZK的轮转。

LUKS分区加密

对于Kafka、Redis等业务数据直接持久化存储到本地磁盘的组件,FusionInsight集群支持基于LUKS的分区加密进行敏感信息保护。

FusionInsight安装过程的脚本工具使用Linux统一密钥设置(Linux Unified Key Setup,简称LUKS)分区加密方案,该方案加密分区时会在集群每个节点生成或者从第三方KMS获取访问密钥,用于加密数据密钥,以保护数据密钥安全性。磁盘分区加密后,重启操作系统或者更换磁盘场景下,系统能够自动获取密钥并挂载或创建新的加密分区。

点击关注,第一时间了解华为云新鲜技术~

本文转载自: 掘金

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

设计模式-抽象工厂模式及应用

发表于 2021-11-01

在 工厂方法模式 中,可以看到一种工厂,只 ==产生==一种类别的具体产品信息,同种类称为同等级,也就是说:工厂方法模式只考虑生产同等级的产品,但是在现实生活中许多工厂是综合型的工厂,能生产多等级(种类) 的产品,如农场里既养动物又种植物,电器厂既生产电视机又生产洗衣机或空调,大学既有软件专业又有生物专业等。

模式的定义与特点

抽象工厂(AbstractFactory)模式的定义:是一种为访问类提供一个创建一组相关或相互依赖对象的接口,且访问类无须指定所要产品的具体类就能得到同族的不同等级的产品的模式结构。

抽象工厂模式是工厂方法模式的升级版本,工厂方法模式只生产一个等级的产品,而抽象工厂模式可生产多个等级的产品。

模式的结构与实现

抽象工厂模式同工厂方法模式一样,也是由抽象工厂、具体工厂、抽象产品和具体产品等 4 个要素构成,但抽象工厂中方法个数不同,抽象产品的个数也不同。现在我们来分析其基本结构和实现方法。

  1. 模式的结构
    抽象工厂模式的主要角色如下。
  • 抽象工厂(Abstract Factory):提供了创建产品的接口,它包含多个创建产品的方法 newProduct(),可以创建多个不同等级的产品。
  • 具体工厂(Concrete Factory):主要是实现抽象工厂中的多个抽象方法,完成具体产品的创建。
  • 抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能,抽象工厂模式有多个抽象产品。
  • 具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间是多对一的关系。

抽象工厂模式的结构图:
在这里插入图片描述

代码案例:

  • 抽象工厂
1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public interface ElectronicsFactory {	
/**
* 生产pad
* @return
*/
Pad producePad();

/**
* 生产手机
* @return
*/
Mobile produceMobile();

}
  • 具体工厂1 (华为工厂)
1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public class HuaweiFactory implements ElectronicsFactory{

@Override
public Pad producePad() {
return new HuaweiPad();
}

@Override
public Mobile produceMobile() {
return new HuaweiMobile();
}

}
  • 具体工厂2 (苹果工厂)
1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public class AppleFactory implements ElectronicsFactory{

@Override
public Pad producePad() {
return new Ipad();
}

@Override
public Mobile produceMobile() {
return new IphoneMoible();
}

}
  • 抽象产品1 手机
1
2
3
4
java复制代码public abstract class Mobile {
public abstract void produce();

}
  • 抽象产品2 平板
1
2
3
4
java复制代码public abstract class Pad {

public abstract void produce();
}
  • 具体产品1-1 iphone手机
1
2
3
4
5
6
java复制代码public class IphoneMoible extends Mobile{
public void produce() {
System.out.println("Apple produce Iphone12 mobile");
}

}
  • 具体产品1-2 华为手机
1
2
3
4
5
6
java复制代码public class HuaweiMobile extends Mobile {

public void produce() {
System.out.println("Huawei produce P40 mobile");
}
}
  • 具体产品2-1 苹果平板ipad
1
2
3
4
5
6
7
java复制代码public class Ipad extends Pad{

@Override
public void produce() {
System.out.println("Apple produce ipad air");
}
}
  • 具体产品2-2 华为平板mate
1
2
3
4
5
6
7
8
java复制代码public class HuaweiPad extends Pad{

@Override
public void produce() {
System.out.println("Huawei produce mate pad");

}
}
  • client 使用方
1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public class Client {

public static void main(String[] args) {
// 指定苹果电子工厂
ElectronicsFactory electronicsFactory = new AppleFactory();
//生产苹果系列产品
Pad pad = electronicsFactory.producePad();
Mobile mobile = electronicsFactory.produceMobile();
//生产
pad.produce();
mobile.produce();
}

}

执行结果:

1
2
java复制代码Apple produce ipad air
Apple produce Iphone12 mobile

案例类图
在这里插入图片描述

优缺点

优点:
抽象工厂模式除了具有工厂方法模式的优点外,其他主要优点如下。

  • 可以在类的内部对产品族中相关联的多等级产品共同管理,而不必专门引入多个新的类来进行管理。
  • 当需要产品族时,抽象工厂可以保证客户端始终只使用同一个产品的产品组。
  • 抽象工厂增强了程序的可扩展性,当增加一个新的产品族时,不需要修改原代码,满足开闭原则。

缺点:
当产品族中需要增加一个新的产品时,所有的工厂类都需要进行修改。增加了系统的抽象性和理解难度。

使用场景

使用抽象工厂模式一般要满足以下条件。

  • 系统中有多个产品族,每个具体工厂创建同一族但属于不同等级结构的产品。
  • 系统一次只可能消费其中某一族产品,即同族的产品一起使用。

抽象工厂模式通常适用于以下场景:

  • 当需要创建的对象是一系列相互关联或相互依赖的产品族时,如电器工厂中的电视机、洗衣机、空调等。
  • 系统中有多个产品族,但每次只使用其中的某一族产品。如有人只喜欢穿某一个品牌的衣服和鞋。
  • 系统中提供了产品的类库,且所有产品的接口相同,客户端不依赖产品实例的创建细节和内部结构。

抽象工厂模式在JDK中源码分析

  • 抽象工厂在数据库连接及使用场景的运用
    在我们使用原生的JDBC数据库连接时,会写如下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码public static void testJdbcSql(){
String driverClassName = "com.mysql.jdbc.Driver";
String url = "jdbc:mysql://127.0.0.1:3306/test";
String mysqlusername = "root";
String password = "123";
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try
{
Class.forName(driverClassName);
con = DriverManager.getConnection(url, mysqlusername, password);
String sql = "SELECT * FROM USER WHERE username=?";
...//略
} catch (Exception e){
...//略
}
}

我们所有的操作都是获取完Connection连接对象进行后续操作的,java.sql.Connection是个接口,Connection 接口中有几个抽象方法,源码如下所示。

1
2
3
4
5
6
7
8
java复制代码public interface Connection {
//提供一个执行对象
Statement createStatement() throws SQLException;
//提供一个支持预编译的执行对象
PreparedStatement prepareStatement(String sql) throws SQLException;
//提供一个支持存储过程的执行对象
CallableStatement prepareCall(String sql) throws SQLException;
}

当我们通过Class.forName(driverClassName),实则是往
DriverManager注册驱动,源码如下:

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
java复制代码public class Driver extends NonRegisteringDriver implements java.sql.Driver {
//注册DriverManager
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
}

public class DriverManager {
// 注册的JDBC驱动程序列表
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
....//省略
// 注册驱动
public static synchronized void registerDriver(java.sql.Driver driver,
DriverAction da)
throws SQLException {

/* Register the driver if it has not already been added to our list */
if(driver != null) {
registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
} else {
// This is for compatibility with the original DriverManager
throw new NullPointerException();
}
println("registerDriver: " + driver);
}
.... //省略代码
}

驱动注册完成后,我们是可以获取通过DriverManager.getConnection(url, mysqlusername, password) 去获取相应的连接,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码
public class DriverManager {

// 获取连接
private static Connection getConnection(
String url, java.util.Properties info, Class<?> caller) throws SQLException {
for(DriverInfo aDriver : registeredDrivers) {
// If the caller does not have permission to load the driver then
// skip it.
if(isDriverAllowed(aDriver.driver, callerCL)) {
try {
println(" trying " + aDriver.driver.getClass().getName());
Connection con = aDriver.driver.connect(url, info);
if (con != null) {
// Success!
println("getConnection returning " + aDriver.driver.getClass().getName());
return (con);
}
... //省略
}

各个组件在抽象工厂中所扮演的角色,Connection 是一个经典的抽象工厂,而 Statement、PreparedStatement、CallableStatement 是 Connection 这个抽象工厂中提供的三个抽象产品。

Driver 起到 Client 的作用,我们只需要把 Driver 注册进 DriverManager,就可以生成需要的 Connection。每次操作数据库只需要使用 Java 提供的这套接口就可以,不需要考虑使用的是什么 SQL 数据库(不考虑特殊SQL语法的情况下)。

这些抽象工厂与抽象产品均由对应的数据库驱动实现,下面以 MySQL 与 Oracle 的驱动进行举例。

UML结构图:
在这里插入图片描述

✨✨ 欢迎🔔订阅个人的微信公众号 享及时博文更新

✨✨ 个人GitHub地址

本文转载自: 掘金

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

用Python去除图片水印

发表于 2021-11-01

今天介绍下用 Python 去除 PDF (图片)的水印。思路很简单,代码也很简洁。

首先来考虑 Python 如何去除图片的水印,然后再将思路复用到 PDF 上面。

这张图片是前几天整理《数据结构和算法》PDF里的一个截图,带着公众号的水印。

从上图可以明显看到,为了不影响阅读正文,水印颜色一般比较浅。因此,我们可以利用颜色差这个特征来去掉水印。即:用 Python 读取图片的颜色,并将浅颜色部分变白。

Python 标准库 PIL 可以获取图片的颜色,Python2 是系统自带的,Python3 需要自己安装,我用的 Python 3.8,需要执行以下命令安装

1
sh复制代码pip install pillow

安装完成,读取图片,并获取图片的尺寸(宽度和高度)

1
2
3
4
python复制代码from PIL import Image

img = Image.open('watermark_pic.png')
width, height = img.size

进行下一步之前,先简单介绍下计算机里关于颜色的知识。光学三原色是红绿蓝(RGB),也就是说它们是不可分解的三种基本颜色,其他颜色都可以通过这三种颜色混合而成,三种颜色等比例混合就是白色,没有光就是黑色。

在计算机中,可以用三个字节表示 RGB 颜色,1个字节能表示的最大数值是 255, 所以,(255, 0, 0)代表红色,(0, 255, 0)代表绿色,(0, 0, 255)代表蓝色。相应地,(255, 255, 255)代表白色,(0, 0, 0)代表黑色。从(0, 0, 0) ~ (255, 255, 255) 之间的任意组合都可以代表一个不同的颜色。

接下来我们可以通过下面代码读取图片的 RGB

1
2
3
4
python复制代码for i in range(width):
for j in range(height):
pos = (i, j)
print(img.getpixel(pos)[:3])

图片每个位置颜色由四元组表示,前三位分别是 RGB,第四位是 Alpha 通道,我们不需要关心。

有了 RGB ,我们就可以对其修改。

从图中可以发现,水印的 RGB 是 #d9d9d9,这里是用十六进制表示的,其实就是(217, 217, 217)。

这三个颜色值都越靠近 255,颜色就越淡,当它们都变成 255,也就成了白色。所以只要 RGB 都大于 217 的位置,我们都可以给它填成白色。即:RGB 三位数之和大于等于 651。

1
2
python复制代码if sum(img.getpixel(pos)[:3]) >= 651:
img.putpixel(pos, (255, 255, 255))

完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
python复制代码from PIL import Image

img = Image.open('watermark_pic.png')
width, height = img.size

for i in range(width):
for j in range(height):
pos = (i, j)
if sum(img.getpixel(pos)[:3]) >= 651:
img.putpixel(pos, (255, 255, 255))

img.save('watermark_removed_pic.png')

有了上面的基础,去除 PDF 的水印就简单了,思路是将每页 PDF 转成图片,然后修改水印的 RGB,最后输出图片即可。

安装 pymupdf 库,用来来操作 PDF

1
sh复制代码pip install pymupdf

读取 PDF,并转图片

1
2
3
4
5
6
7
python复制代码import fitz


doc = fitz.open("数据结构和算法手册@公众号渡码.pdf")

for page in doc:
pix = page.get_pixmap()

该 PDF 共 480 页,所以需要遍历每一页,并获取每一页对应的图片pix。pix对象类似于我们上面看到的img对象,可以读取、修改它的 RGB。

page.get_pixmap() 这个操作是不可逆的,即能够实现从 PDF 到图片的转换,但修改图片 RGB 后无法应用到 PDF 上,只能输出为图片。

修改水印 RGB 跟刚才一样,区别是这里的 RGB 是一个三元组,没有 Alpha 通道,代码如下:

1
2
3
4
5
python复制代码from itertools import product

for pos in product(range(pix.width), range(pix.height)):
if sum(pix.pixel(pos[0], pos[1])) >= 651:
pix.set_pixel(pos[0], pos[1], (255, 255, 255))

完整代码如下:

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


doc = fitz.open("数据结构和算法手册@公众号渡码.pdf")

page_no = 0
for page in doc:
pix = page.get_pixmap()

for pos in product(range(pix.width), range(pix.height)):
if sum(pix.pixel(pos[0], pos[1])) >= 651:
pix.set_pixel(pos[0], pos[1], (255, 255, 255))

pix.pil_save(f"pdf_pics/page_{page_no}.png", dpi=(30000, 30000))

print(f'第 {page_no} 页去除完成')
page_no += 1

这种方案是有缺点的,第一,输出并非 PDF 格式;第二,输出的图片比较模糊,后续还有待优化,最好是能直接修改 PDF。

后续继续分享 Python 基础以及使用工具,欢迎关注。

本文转载自: 掘金

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

通过实战走近Java Agent探针技术 一、前言 二、相关

发表于 2021-11-01

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

一、前言

所谓Java Agent,其功能都是基于java.lang.instrument中的类去完成。Instrument提供了允许Java编程语言代理检测JVM上运行的程序的功能,而检测的机制就是修改字节码。Instrument位于rt.jar中,java.lang.instrument包下,使用Instrument可以用来检测或协助运行在 JVM中的程序;甚至对已加载class进行替换修改,这也就是我们常说的热部署、热加载。一句话总结Instrument:检测类的加载行为对其进行干扰(修改替换)

image.png

Instrument的实现基于JVMTI(Java Virtual Machine Tool Interface)的,所谓JVMTI就是一套由 Java 虚拟机提供的,为JVM 相关的工具提供的本地编程接口集合。JVMTI基于事件驱动,简单点讲就是在JVM运行层面添加一些钩子可以供开发者去自定义实现相关功能。

有哪些开源软件使用了该技术?

github.com/alibaba/art…

github.com/apache/skyw…

等等。。。

二、相关API初探

2.1 Instrumentation

java.lang.instrument包下关键的类为:java.lang.instrument.Instrumentation。该接口提供一系列替换转化class定义的方法。接下来看一下该接口的主要方法进行以下说明:

addTransformer

用于注册transformer。除了任何已注册的转换器所依赖的类的定义外,所有未来的类定义都将可以被transformer看到。当类被加载或被重新定义(redefine,可以是下方的redefineClasses触发)时,transformer将被调用。如果canRetransform为true,则表示当它们被retransform时(通过下方的retransformClasses),该transformer也会被调用。addTransformer共有如下两种重载方法:

1
2
3
java复制代码void addTransformer(ClassFileTransformer transformer,boolean canRetransform)

void addTransformer(ClassFileTransformer transformer)

redefineClasses

1
2
3
java复制代码void redefineClasses(ClassDefinition... definitions)
throws ClassNotFoundException,
UnmodifiableClassException

此方法用于替换不引用现有类文件字节的类定义,就像从源代码重新编译以进行修复并继续调试时所做的那样。该方法对一系列ClassDefinition进行操作,以便允许同时对多个类进行相互依赖的更改(类a的重新定义可能需要类B的重新定义)。假如在redifine时,目标类正在执行中,那么执行中的行为还是按照原来字节码的定义执行,当对该类行为发起新的调用时,将会使用redefine之后的新行为。

注意:此redefine不会触发类的初始化行为

当然redefine时,并不是随心所欲,我们可以重新定义方法体、常量池、属性、但是不可以添加、移除、重命名方法和方法和入参,不能更改方法签名或更改继承。当然,在未来的版本中,这些限制可能不复存在。

在转换之前,不会检查、验证和安装类文件字节,如果结果字节出现错误,此方法将抛出异常。而抛出异常将不会有类被重新定义

retransformClasses

针对JVM已经加载的类进行转换,当类初始加载或重新定义类(redefineClass)时,可以被注册的ClassFileTransformer进行转化;但是针对那些已经加载完毕之后的类不会触发这个transform行为进而导致这些类无法被我们agent进行监听,所以可以通过retransformClasses触发一个事件,而这个事件可以被ClassFileTransformer捕获进而对这些类进行transform。

此方法将针对每一个通过addTransformer注册的且canRetransform是true的,进行调用其transform方法,转换后的类文件字节被安装成为类的新定义,从而拥有新的行为。

redefineClasses和retransformClasses区别

通过上面的定义可以看得出,貌似redefineClasses是在为JVM启动前未加载完成的class服务,而retransformClasses是针对JVM启动之后,那些已经完成加载初始化的class服务。

2.2 ClassFileTransformer

在我们的agent中,需要提供该接口的实现,以便在JVM定义类之前转换class字节码文件,该接口中就提供了一个方法,此方法的实现可以转换提供的类文件并返回一个新的替换类文件:

1
2
3
4
5
6
java复制代码byte[] transform(ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer)
throws IllegalClassFormatException

三、Java Agent的两种实现

java agent其实就是一个jar文件,通过在该jar文件中的manifest中通过相关属性指定要加载的agent实现类。对于agent的实现有两种方式:一种实现是通过命令行方式在JVM启动之前进行代理设置;另一种则是在JVM启动之后通过attach机制去设置。

JVM启动前的agent实现

Instrument是JDK5开始引入,在JDK5中Instrument要求在目标JVM程序运行之前通过命令行参数javaagent来设置代理类,在JVM初始化之前,Instrument启动在JVM中设置回调函数,检测特点类加载情况完成实际增强工作。

-javaagent: jarpath[ =options]

这里jarpath就是我们的agent jar的路径,agent jar必须符合jar文件规范。代理JAR文件的manifest(META-INF/MANIFEST.MF)必须包含属性Premain-Class。此属性的值是代理类的类名。代理类必须实现一个公共静态premain方法,该方法原则上与主应用程序入口点类似。在JVM初始化之后,将按照指定代理的顺序调用每个主方法(premain),然后将调用实际应用程序的主方法(main)。每个premain方法必须按照启动顺序返回。

premain方法可以有如下两种重载方法,如果两者同时存在,则优先调用多参数的方法:

1
2
3
java复制代码public static void premain(String agentArgs, Instrumentation inst);

public static void premain(String agentArgs);

我们的代理类将被SystemClassLoader进行加载,premain方法将在和我们的主应用程序main方法同等的安全和类加载器规则下执行,主应用程序main方法可以干的,premain都可以去干。如果我们的agent无法被解析,这包括agent class无法被加载、或agent class没有premain方法、agent class的方法出现异常等都会导致JVM启动终止!

JVM启动后的agent实现

JDK6开始为Instrument增加很多强大的功能,其中要指出的就是在JDK5中如果想要完成增强处理,必须是在目标JVM程序启动前通过命令行指定Instrument,然后在实际应用中,目标程序可能是已经运行中,针对这种场景下如果要保证 JVM不重启得以完成我们工作,这不是我们想要的,于是JDK6中Instrument提供了在JVM启动之后指定设置java agent达到Instrument的目的。

该实现需要确保以下3点:

1)agent jar中manifest必须包含属性Agent-Class,其值为agent类名。

2)agent类中必须包含公有静态方法agentmain

3)system classload必须支持可以将agent jar添加到system class path。

agent jar将被添加到system class path,这个路径就是SystemClassLoader加载主应用程序的地方,agent class被加载后,JVM将会尝试执行它的agentmain方法,同样的,如果以下两个方法都存在,则优先执行多参数方法:

1
2
3
4
java复制代码public static void agentmain(String agentArgs, Instrumentation inst);


public static void agentmain(String agentArgs);

看到这里,结合JVM前启动前agent的实现和JVM启动后agent的实现,可能想问是否可以在一个agent class中同时包含premain、agentmain呢,答案是可以的,只不过在JVM启动前不会执行agentmain,同样的,JVM启动后不会执行premain。

如果我们的agent无法启动(agent class无法被加载、agentmain出异常、agent class没有合法的agentmain方法等),JVM将不会终止!

四、 Manifest

4.1 属性构成

通过上述我们知道,有一个关键文件META-INF/MANIFEST.MF,我们需要在这个文件中指定agent class,结下来看下相关属性:

属性名称 描述
Premain-Class 这个属性用来指定JVM启动时的代理agent,它必须包含premain方法,如果这个属性不存在,则JVM将终止。注意是类的全路径
Agent-Class 如果agent实现支持在JVM启动后某个时间启动代理的机制,那么该属性则指定该代理类。如果该属性不存在,代理将不会启动。
Boot-Class-Path 该属性可以指定BootStrapClassLoad加载的路径(路径需要带上指定哪个文件),多个路径用空格分开
Can-Redefine-Classes 可选值就是true false,大小写无所谓,默认false。该属性用来指定该agent是否针对redefineClass产生作用
Can-Retransform-Classes 可选值就是true false,大小写无所谓,默认false。该属性用来指定该agent是否针对retransformClass产生作用

4.2 文件生成方式

有两种方式生成此文件:

1)我们手动创建此文件

2)通过maven插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
xml复制代码    <build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<archive>
<!--自动添加META-INF/MANIFEST.MF -->
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Premain-Class>xxx</Premain-Class>
<Agent-Class>xxx</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>

</plugins>
</build>

四、实战

接下来通过实战来近距离感受下java agent的魅力。本次实战的目标是替换目标类的行为。

4.1 准备工作

这里初始化一个springboot工程,随便搞一个简单的controller如下:

1
2
3
4
5
6
7
java复制代码@RestController
public class MainController {
@RequestMapping("/index")
public String index(){
return "hello world";
}
}

那么当我访问这个地址时,浏览器将会展现hello world字样,如下:

image.png

接下来我们将通过java agent来改变这个controller的行为。

4.2 JVM启动前替换实现

4.2.1 定义ClassFileTransformer实现

在我们自定义的ClassFileTransformer中,通过javassist动态修改字节码,来更改controller输出的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码public class MyClassFileTransformer implements ClassFileTransformer

{
@SneakyThrows
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
if (className.contains("MainController")){
final ClassPool classPool = ClassPool.getDefault();
final CtClass clazz = classPool.get("com.cf.springboot.controller.MainController");
CtMethod convertToAbbr = clazz.getDeclaredMethod("index");
String methodBody = "return \"hello world【version2]\";";
convertToAbbr.setBody(methodBody);
// 返回字节码,并且detachCtClass对象
byte[] byteCode = clazz.toBytecode();
//detach的意思是将内存中曾经被javassist加载过的Date对象移除,如果下次有需要在内存中找不到会重新走javassist加载
clazz.detach();
return byteCode;
}
// 如果返回null则字节码不会被修改
return null;
}
}

4.2.1 定义agent class实现

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public class BeforeJvmAgent {
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("premain invoke!");

inst.addTransformer(new MyClassFileTransformer());
}

public static void main(String[] args) {
System.out.println("main invoke!");
}

}

4.2.2 打包,设置命令行参数启动spring boot

image.png

启动后,观察控制台输出

image.png

可以看到premain最新被执行了,这时候访问下试试

image.png

可以看到,我们的修改已经生效~

4.3 JVM启动后替换实现

在这里,ClassFileTransformer的实现我们还是复用4.2节中的,所以这里只需要看新实现。此刻开始,我们的应用属于一直启动之中了,我们要做的就是真正意义上的热替换。

4.3.1 agent class实现

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public class AfterJvmAgent {
public static void agentmain(String agentArgs, Instrumentation inst)
throws ClassNotFoundException, UnmodifiableClassException {
inst.addTransformer(new MyClassFileTransformer(), true);
// 关键点
inst.retransformClasses(Class.forName("com.cf.springboot.controller.MainController",false,ClassLoader.getSystemClassLoader()));
}

public static void main(String[] args) {

}

}

这里关键的一点就是在我们的agentmain中手动retransform一下我们需要增强的类。

4.3.2 启动应用程序,并attach

这里我们需要获取目标JVM程序,并且进行attach加载我们的agent

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public static void main(String[] args) throws Exception{
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for (VirtualMachineDescriptor vmd : list) {
//如果虚拟机的名称为 xxx 则 该虚拟机为目标虚拟机,获取该虚拟机的 pid
//然后加载 agent.jar 发送给该虚拟机
System.out.println(vmd.displayName());
if (vmd.displayName().equals("com.cf.springboot.Application")) {
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
virtualMachine.loadAgent("/Users/zhuyu/code/spring-boot/after_jvm_agent/target/after_jvm_agent-0.0.1-SNAPSHOT.jar");
virtualMachine.detach();
}
}
}

这个时候看再访问一下我们的请求:

image.png

完美!

五、参考

Java程序员必知:深入理解Instrument

javaagent使用指南

☆基于Java Instrument的Agent实现

docs.oracle.com/javase/8/do…

zhuanlan.zhihu.com/p/135872794…

本文转载自: 掘金

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

高并发场景下JVM调优实践之路 一、背景 二、优化目标 三、

发表于 2021-11-01

一、背景

2021年2月,收到反馈,视频APP某核心接口高峰期响应慢,影响用户体验。

通过监控发现,接口响应慢主要是P99耗时高引起的,怀疑与该服务的GC有关,该服务典型的一个实例GC表现如下图:

可以看出,在观察周期里:

  • 平均每10分钟Young GC次数66次,峰值为470次;
  • 平均每10分钟Full GC次数0.25次,峰值5次;

可见Full GC非常频繁,Young GC在特定的时段也比较频繁,存在较大的优化空间。由于对GC停顿的优化是降低接口的P99时延一个有效的手段,所以决定对该核心服务进行JVM调优。

二、优化目标

  • 接口P99时延降低30%
  • 减少Young GC和Full GC次数、停顿时长、单次停顿时长

由于GC的行为与并发有关,例如当并发比较高时,不管如何调优,Young GC总会很频繁,总会有不该晋升的对象晋升触发Full GC,因此优化的目标根据负载分别制定:

目标1:高负载(单机1000 QPS以上)

  • Young GC次数减少20%-30% ,Young GC累积耗时不恶化;
  • Full GC次数减少50%以上,单次、累积Full GC耗时减少50%以上,服务发布不触发Full GC。

目标2:中负载(单机500-600)

  • Young GC次数减少20%-30% ,Young GC累积耗时减少20%;
  • Full GC次数不高于4次/天,服务发布不触发Full GC。

目标3:低负载(单机200 QPS以下)

  • Young GC次数减少20%-30% ,Young GC累积耗时减少20%;
  • Full GC次数不高于1次/天,服务发布不触发Full GC。

三、当前存在的问题

当前服务的JVM配置参数如下:

1
2
3
java复制代码-Xms4096M -Xmx4096M -Xmn1024M
-XX:PermSize=512M
-XX:MaxPermSize=512M

单纯从参数上分析,存在以下问题:

**未显示指定收集器 **

JDK 8默认搜集器为ParrallelGC,即Young区采用Parallel Scavenge,老年代采用Parallel Old进行收集,这套配置的特点是吞吐量优先,一般适用于后台任务型服务器。

比如批量订单处理、科学计算等对吞吐量敏感,对时延不敏感的场景,当前服务是视频与用户交互的门户,对时延非常敏感,因此不适合使用默认收集器ParrallelGC,应选择更合适的收集器。

Young区配比不合理

当前服务主要提供API,这类服务的特点是常驻对象会比较少,绝大多数对象的生命周期都比较短,经过一次或两次Young GC就会消亡。

再看下当前JVM配置:

整个堆为4G,Young区总共1G,默认-XX:SurvivorRatio=8,即有效大小为0.9G,老年代常驻对象大小约400M。

这就意味着,当服务负载较高,请求并发较大时,Young区中Eden + S0区域会迅速填满,进而Young GC会比较频繁。

另外会引起本应被Young GC回收的对象过早晋升,增加Full GC的频率,同时单次收集的区域也会增大,由于Old区使用的是ParralellOld,无法与用户线程并发执行,导致服务长时间停顿,可用性下降, P99响应时间上升。

未设置

-XX:MetaspaceSize和-XX:MaxMetaspaceSize

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码Perm区在jdk 1.8已经过时,被Meta区取代,
因此-XX:PermSize=512M -XX:MaxPermSize=512M配置会被忽略,
真正控制Meta区GC的参数为
-XX:MetaspaceSize:
Metaspace初始大小,64位机器默认为21M左右

-XX:MaxMetaspaceSize:
Metaspace的最大值,64位机器默认为18446744073709551615Byte,
可以理解为无上限

-XX:MaxMetaspaceExpansion:
增大触发metaspace GC阈值的最大要求

-XX:MinMetaspaceExpansion:
增大触发metaspace GC阈值的最小要求,默认为340784Byte

这样服务在启动和发布的过程中,元数据区域达到21M时会触发一次Full GC (Metadata GC Threshold),随后随着元数据区域的扩张,会夹杂若干次Full GC (Metadata GC Threshold),使服务发布稳定性和效率下降。

此外如果服务使用了大量动态类生成技术的话,也会因为这个机制产生不必要的Full GC (Metadata GC Threshold)。

四、优化方案/验证方案

上面已分析出当前配置存在的较为明显的不足,下面优化方案主要先针对性解决这些问题,之后再结合效果决定是否继续深入优化。

当前主流/优秀的搜集器包含:

  • Parrallel Scavenge + Parrallel Old:吞吐量优先,后台任务型服务适合;
  • ParNew + CMS:经典的低停顿搜集器,绝大多数商用、延时敏感的服务在使用;
  • G1:JDK 9默认搜集器,堆内存比较大(6G-8G以上)的时候表现出比较高吞吐量和短暂的停顿时间;
  • ZGC:JDK 11中推出的一款低延迟垃圾回收器,目前处在实验阶段;

结合当前服务的实际情况(堆大小,可维护性),我们选择ParNew + CMS方案是比较合适的。

参数选择的原则如下:

1)Meta区域的大小一定要指定,且MetaspaceSize和MaxMetaspaceSize大小应设置一致,具体多大要结合线上实例的情况,通过jstat -gc可以获取该服务线上实例的情况。

1
2
3
java复制代码# jstat -gc 31247
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
37888.0 37888.0 0.0 32438.5 972800.0 403063.5 3145728.0 2700882.3 167320.0 152285.0 18856.0 16442.4 15189 597.209 65 70.447 667.655

可以看出MU在150M左右,因此-XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M是比较合理的。

2)Young区也不是越大越好。

当堆大小一定时,Young区越大,Young GC的频率一定越小,但Old区域就会变小,如果太小,稍微晋升一些对象就会触发Full GC得不偿失。

如果Young区过小,Young GC就会比较频繁,这样Old区就会比较大,单次Full GC的停顿就会比较大。因此Young区的大小需要结合服务情况,分几种场景进行比较,最终获得最合适的配置。

基于以上原则,以下为4种参数组合:

1.ParNew +CMS,Young区扩大1倍

1
2
3
4
5
6
java复制代码-Xms4096M -Xmx4096M -Xmn2048M
-XX:MetaspaceSize=256M
-XX:MaxMetaspaceSize=256M
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:+CMSScavengeBeforeRemark

**2.ParNew +CMS,**Young区扩大1倍,

去除-XX:+CMSScavengeBeforeRemark(使用【-XX:CMSScavengeBeforeRemark】参数可以做到在重新标记前先执行一次新生代GC)。

因为老年代和年轻代之间的对象存在跨代引用,因此老年代进行GC Roots追踪时,同样也会扫描年轻代,而如果能够在重新标记前先执行一次新生代GC,那么就可以少扫描一些对象,重新标记阶段的性能也能因此提升。)

1
2
3
4
5
java复制代码-Xms4096M -Xmx4096M -Xmn2048M
-XX:MetaspaceSize=256M
-XX:MaxMetaspaceSize=256M
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC

3.ParNew +CMS,Young区扩大0.5倍

1
2
3
4
5
6
java复制代码-Xms4096M -Xmx4096M -Xmn1536M
-XX:MetaspaceSize=256M
-XX:MaxMetaspaceSize=256M
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:+CMSScavengeBeforeRemark

4.ParNew +CMS,Young区不变

1
2
3
4
5
6
java复制代码-Xms4096M -Xmx4096M -Xmn1024M
-XX:MetaspaceSize=256M
-XX:MaxMetaspaceSize=256M
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:+CMSScavengeBeforeRemark

下面,我们需要在压测环境,对不同负载下4种方案的实际表现进行比较,分析,验证。

4.1 压测环境验证/分析

高负载场景(1100 QPS)GC表现

可以看出,在高负载场景,4种ParNew + CMS的各项指标表现均远好于Parrallel Scavenge + Parrallel Old。其中:

  • 方案4(Young区扩大0.5倍)表现最佳,接口P95,P99延时相对当前方案降低50%,Full GC累积耗时减少88%, Young GC次数减少23%,Young GC累积耗时减少4%,Young区调大后,虽然次数减少了,但Young区大了,单次Young GC的耗时也大概率会上升,这是符合预期的。
  • Young区扩大1倍的两种方案,即方案2和方案3,表现接近,接口P95,P99延时相对当前方案降低40%,Full GC累积耗时减少81%, Young GC次数减少43%,Young GC累积耗时减少17%,略逊于Young区扩大0.5倍,总体表现不错,这两个方案进行合并,不再区分。

Young区不变的方案在新方案里,表现最差,淘汰。所以在中负载场景,我们只需要对比方案2和方案4。

中负载场景(600 QPS)GC表现

可以看出,在中负载场景,2种ParNew + CMS(方案2和方案4)的各项指标表现也均远好于Parrallel Scavenge + Parrallel Old。

  • Young区扩大1倍的方案表现最佳,接口P95,P99延时相对当前方案降低32%,Full GC累积耗时减少93%, Young GC次数减少42%,Young GC累积耗时减少44%;
  • Young区扩大0.5倍的方案稍逊一些。

综合来看,两个方案表现十分接近,原则上两种方案都可以,只是Young区扩大0.5倍的方案在业务高峰期的表现更佳,为尽量保证高峰期服务的稳定和性能,目前更倾向于选择ParNew + CMS,Young区扩大0.5倍方案。

4.2 灰度方案/分析

为保证覆盖业务的高峰期,选择周五、周六、周日分别从两个机房随机选择一台线上实例,线上实例的指标符合预期后,再进行全量升级。

目标组 xx.xxx.60.6

采用方案2,即目标方案

1
2
3
4
5
6
java复制代码-Xms4096M -Xmx4096M -Xmn1536M
-XX:MetaspaceSize=256M
-XX:MaxMetaspaceSize=256M
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:+CMSScavengeBeforeRemark

对照组1 xx.xxx.15.215

采用原始方案

1
2
3
java复制代码-Xms4096M -Xmx4096M -Xmn1024M
-XX:PermSize=512M
-XX:MaxPermSize=512M

对照组2 xx.xxx.40.87

采用方案4,即候选目标方案

1
2
3
4
5
6
java复制代码-Xms4096M -Xmx4096M -Xmn2048M
-XX:MetaspaceSize=256M
-XX:MaxMetaspaceSize=256M
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:+CMSScavengeBeforeRemark

灰度3台机器。

我们先分析下Young GC相关指标:

Young GC次数

Young GC累计耗时

Young GC单次耗时

可以看出,与原始方案相比,目标方案的YGC次数减少50%,累积耗时减少47%,吞吐量提升的同时,服务停顿的频率大大降低,而代价是单次Young GC的耗时增长3ms,收益是非常高的。

对照方案2即Young区2G的方案整体表现稍逊与目标方案,再分析Full GC指标。

老年代内存增长情况

Full GC次数

Full GC累计/单次耗时

与原始方案相比,使用目标方案时,老年代增长的速度要缓慢很多,基本在观测周期内Full GC发生的次数从155次减少至27次,减少82%,停顿时间均值从399ms减少至60ms,减少85%,毛刺也非常少。

对照方案2即Young区2G的方案整体表现逊于目标方案。到这里,可以看出,目标方案从各个维度均远优于原始方案,调优目标也基本达成。

但细心的同学会发现,目标方案相对原始方案,”Full GC”(实际上是CMS Background GC)耗时更加平稳,但每个若干次”Full GC”后会有一个耗时很高的毛刺出现,这意味这个用户请求在这个时刻会停顿2-3s,能否进一步优化,给用户一个更加极致的体验呢?

4.3 再次优化

这里首先要分析这现象背后的逻辑。

对于CMS搜集器,采用的搜集算法为Mark-Sweep-[Compact]。

CMS搜集器GC的种类:

CMS Background GC

这种GC是CMS最常见的一类,是周期性的,由JVM的常驻线程定时扫描老年代的使用率,当使用率超过阈值时触发,采用的是Mark-Sweep方式,由于没有Compact这种耗时操作,且可以与用户进程并行,所以CMS的停顿会比较低,GC日志中出现GC (CMS Initial Mark)字样就代表发生了一次CMS Background GC。

Background GC由于采用的是Mark-Sweep,会导致老年代内存碎片,这也是CMS最大的弱点。

CMS Foreground GC

这种GC是CMS搜集器里真正意义上的Full GC,采用Serial Old或Parralel Old进行收集,出现的频率就较低,当往往出现后就会造成较大的停顿。

触发CMS Foreground GC的场景有很多,场景的如下:

  • System.gc();
  • jmap -histo:live pid;
  • 元数据区域空间不足;
  • 晋升失败,GC日志中的标志为ParNew(promotion failed);
  • 并发模式失败,GC日志中的标志为councurrent mode failure字样。

不难推断,目标方案中的毛刺是晋升失败或并发模式失败造成的,由于线上没有开启打印gc日志,但也无妨,因为这两种场景的根因是一致的,就是若干次CMS Backgroud GC后造成的老年代内存碎片。

我们只需要尽可能减少由于老年代碎片触发晋升失败、并发模式失败即可。

CMS Background GC由JVM的常驻线程定时扫描老年代的使用率,当使用率超过阈值时触发,该阈值由-XX:CMSInitiatingOccupancyFraction; -XX:+UseCMSInitiatingOccupancyOnly两个参数控制,不设置,默认首次为92%,后续会根据历史情况进行预测,动态调整。

如果我们固定阈值的大小,将该阈值设置为一个相对合理的值,既不使GC过于频繁,又可以降低晋升失败或并发模式失败的概率,就可以大大缓解毛刺产生的频率。

目标方案的堆分布如下:

  • Young区 1.5G
  • Old区 2.5G
  • Old区常驻对象 约400M

按经验数据,75%,80%是比较折中的,因此我们选择-XX:CMSInitiatingOccupancyFraction=75 -

XX:+UseCMSInitiatingOccupancyOnly进行灰度观察(我们也对80%的场景做了对照实验,75%优于80%)。

最终目标方案的配置为:

1
2
3
4
5
6
7
8
java复制代码-Xms4096M -Xmx4096M -Xmn1536M 
-XX:MetaspaceSize=256M
-XX:MaxMetaspaceSize=256M
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:+CMSScavengeBeforeRemark
-XX:CMSInitiatingOccupancyFraction=75
-XX:+UseCMSInitiatingOccupancyOnly

如上配置,灰度 xx.xxx.60.6 一台机器;

从再次优化的结果上看,CMS Foreground GC引起的毛刺基本消失,符合预期。

因此,视频服务最终目标方案的配置为;

1
2
3
4
5
6
7
8
java复制代码-Xms4096M -Xmx4096M -Xmn1536M 
-XX:MetaspaceSize=256M
-XX:MaxMetaspaceSize=256M
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:+CMSScavengeBeforeRemark
-XX:CMSInitiatingOccupancyFraction=75
-XX:+UseCMSInitiatingOccupancyOnly

五、结果验收

灰度持续7天左右,覆盖工作日与周末,结果符合预期,因此符合在线上开启全量的条件,下面对全量后的结果进行评估。

Young GC次数

Young GC累计耗时

单次Young GC耗时

从Young GC指标上看,调整后Young GC次数平均减少30%,Young GC累积耗时平均减少17%,Young GC单次耗时平均增加约7ms,Young GC的表现符合预期。

除了技术手段,我们也在业务上做了一些优化,调优前实例的Young GC会出现明显的、不规律的(定时任务不一定分配到当前实例)毛刺,这里是业务上的一个定时任务,会加载大量数据,调优过程中将该任务进行分片,分摊到多个实例上,进而使Young GC更加平滑。

Full GC单次/累积耗时

从”Full GC”的指标上看,”Full GC”的频率、停顿极大减少,可以说基本上没有真正意义上的Full GC了。

核心接口-A (下游依赖较多) P99响应时间,减少19%(从 3457 ms下降至 2817 ms);

核心接口-B (下游依赖中等) P99响应时间,减少41%(从 1647ms下降至 973ms);

核心接口-C (下游依赖最少) P99响应时间,减少80%(从 628ms下降至 127ms);

综合来看,整个结果是超出预期的。Young GC表现与设定的目标非常吻合,基本上没有真正意义上的Full GC,接口P99的优化效果取决于下游依赖的多少,依赖越少,效果越明显。

六、写在最后

由于GC算法复杂,影响GC性能的参数众多,并且具体参数的设置又取决于服务的特点,这些因素都很大程度增加了JVM调优的难度。

本文结合视频服务的调优经验,着重介绍调优的思路和落地过程,同时总结出一些通用的调优流程,希望能给大家提供一些参考。

作者:vivo互联网技术团队Li Guanyun、 Jessica Chen

本文转载自: 掘金

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

1…444445446…956

开发者博客

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