0%

前言

轻型目录访问协议英文Lightweight Directory Access Protocol缩写LDAP,/ˈɛldæp/)是一个开放的,中立的,工业标准的应用协议,通过IP协议提供访问控制和维护分布式信息的目录信息。

LDAP的主要应用场景

1.网络服务:DNS服务
2.统一认证服务:
3.Linux PAM (ssh, login, cvs. . . )
4.Apache访问控制
5.各种服务登录(ftpd, php based, perl based, python based. . . )
6.个人信息类,如地址簿
7.服务器信息,如帐号管理、邮件服务等

注意:从OpenLDAP2.4.23版本开始所有配置数据都保存在/etc/openldap/slapd.d/中,不再使用slapd.conf作为配置文件

安装

LDAP

安装工具

1
2
docker pull osixia/openldap
docker image list
  • 安装后的目录:/etc/openldap/
  • 数据文件路径:/var/lib/ldap
  • 配置文件路径:/etc/ldap/slapd.d
  • 模板数据库配置文件:/usr/share/openldap-servers/DB_CONFIG.example
  • /usr/share/openldap-servers/slapd.ldif

启动容器

映射端口

1
docker run -p 389:389 -p 636:636 --name openldap-container --detach osixia/openldap:latest

映射环境

1
2
3
4
5
6
7
docker run -p 389:389 -p 636:636 \
--env LDAP_ORGANISATION="maxzhao" \
--env LDAP_DOMAIN="maxzhao.com" \
--env LDAP_ADMIN_PASSWORD="maxzhao" \
--volume /data/slapd/var/lib/ldap:/var/lib/ldap \
--volume /data/slapd/etc/ldap/slapd.d:/etc/ldap/slapd.d \
--name openldap-container --detach osixia/openldap:latest

构建自己的镜像

主机与容器匹配用户

1
2
3
4
5
6
7
docker build \
--build-arg LDAP_OPENLDAP_GID=1234 \
--build-arg LDAP_OPENLDAP_UID=2345 \
-t my_ldap_image .
docker run --name my_ldap_container -d my_ldap_image
# this should output uid=2345(openldap) gid=1234(openldap) groups=1234(openldap)
docker exec my_ldap_container id openldap

连接工具

apache directory studio下载

查看配置

1
cat /data/slapd/etc/ldap/slapd.d/cn\=config/olcDatabase\=\{1\}mdb.ldif

image-20220519165543873

新建 Connection

image-20220519165509438

image-20220519165521134

参考:

docker-openldap

本文地址: https://github.com/maxzhao-it/blog/post/4c3b1032/

前言

网络文件系统Network File System(NFS)

是由SUN公司研制的UNIX表示层协议(presentation layer protocol),能使使用者访问网络上别处的文件就像在使用自己的计算机一样。

是一种 CS 架构

安装

服务端

安装依赖

1
2
# rpcbind 会默认安装 rpcbind
sudo yum install -y nfs-utils

启动服务

1
2
3
4
sudo systemctl start rpcbind
sudo systemctl start nfs
sudo systemctl enable rpcbind
sudo systemctl enable nfs

配置共享目录

1
2
sudo mkdir /data
sudo chmod 755 /data

配置 nfs

1
sudo vim /etc/exports

写入下面配置

1
/data/ *(rw,sync,no_root_squash,no_all_squash)
  • /data: 共享目录位置。
  • *: 客户端 IP 范围,*代表没有限制 ,限制某个子网(192.168.0.0/24)。
  • rw: 权限设置,可读可写。
  • sync: 同步共享目录。
  • no_root_squash: 当NFS客户端以root管理员访问时,映射为NFS服务器的root管理员(不常用)。
  • root_squash:当NFS客户端以root管理员访问时,映射为NFS服务器的匿名用户(不常用)
  • no_all_squash: 可以使用普通用户授权。

使配置生效

1
exportfs -r

重启服务

1
sudo systemctl restart nfs

验证

1
2
3
4
5
6
# 查询某 `IP` 的 `NFS` 服务
showmount -e 127.0.0.1
# 检查当前服务
exportfs
# 查询端口占用
rpcinfo -p 192.168.14.118

配置端口

  1. portmapper:默认 111
  2. nfs: 默认 2049
  3. mountd:使用默认892
  4. statd:使用默认 662
1
sudo vim /etc/sysconfig/nfs

添加

1
2
3
4
LOCKD_TCPPORT=32803
LOCKD_UDPPORT=32769
MOUNTD_PORT=892
STATD_PORT=662
1
sudo vim /etc/modprobe.d/lockd.conf 

添加

1
2
options lockd nlm_tcpport=32803
options lockd nlm_udpport=32769

重启

1
2
3
4
5
6
7
# 重启服务
sudo systemctl restart rpcbind
sudo systemctl restart nfs
sudo systemctl restart nfs-config
sudo systemctl restart nfs-idmap
sudo systemctl restart nfs-lock
sudo systemctl restart nfs-server

防火墙

1
2
3
4
5
6
7
8
9
10
11
12
sudo firewall-cmd --zone=public --add-service=nfs --permanent
sudo firewall-cmd --zone=public --add-port=32803/tcp --permanent
sudo firewall-cmd --zone=public --add-port=32769/udp --permanent
sudo firewall-cmd --zone=public --add-port=111/tcp --add-port=111/udp --permanent
sudo firewall-cmd --zone=public --add-port=892/tcp --add-port=892/udp --permanent
sudo firewall-cmd --zone=public --add-port=662/tcp --add-port=662/udp --permanent
sudo firewall-cmd --zone=public --add-port=2049/tcp --add-port=2049/udp --permanent
sudo firewall-cmd --reload
sudo firewall-cmd --zone=public --list-ports
sudo firewall-cmd --zone=public --list-services
# 查询需要开放的端口
rpcinfo -p 192.168.14.118

客户端挂载NFS

安装工具

1
sudo yum install nfs-utils

查询某 IPNFS 服务

1
showmount -e 192.168.2.140

输出

1
2
Export list for 192.168.2.140:
/data/ *

挂载

1
2
3
4
# 创建本地目录
sudo mkdir /mnt/data
# 挂载服务端目录到本地
sudo mount -t nfs 192.168.2.140:/data /mnt/data

本文地址: https://github.com/maxzhao-it/blog/post/de2905c4/

前言

轻型目录访问协议英文Lightweight Directory Access Protocol缩写LDAP,/ˈɛldæp/)是一个开放的,中立的,工业标准的应用协议,通过IP协议提供访问控制和维护分布式信息的目录信息。

LDAP的主要应用场景

1.网络服务:DNS服务
2.统一认证服务:
3.Linux PAM (ssh, login, cvs. . . )
4.Apache访问控制
5.各种服务登录(ftpd, php based, perl based, python based. . . )
6.个人信息类,如地址簿
7.服务器信息,如帐号管理、邮件服务等

注意:从OpenLDAP2.4.23版本开始所有配置数据都保存在/etc/openldap/slapd.d/中,不再使用slapd.conf作为配置文件

安装

LDAP

安装工具

1
2
sudo yum install -y openldap compat-openldap openldap  openldap-clients  openldap-devel openldap-servers 
# 可选 collectd-openldap
  • 安装后的目录:/etc/openldap/
  • 数据文件路径:/var/lib/ldap
  • 模板数据库配置文件: /usr/share/openldap-servers/DB_CONFIG.example
  • /usr/share/openldap-servers/slapd.ldif

如果是虚拟机安装:需要挂载cdrom

启动服务

1
2
3
sudo systemctl start slapd
sudo systemctl enable slapd
sudo systemctl status slapd

查看端口

占用 389 端口

1
netstat -tlnp | grep slapd

查看用户

1
tail -n 1 /etc/passwd

配置

修改域信息

设置管理员密码

1
2
3
slappasswd
# 也可以指明密码
slappasswd -s maxzhao

输出的密码:{SSHA}0tKDKqne3gJ30xZ73rDO491r/AlaJd0N

修改域信息

1
2
cd /etc/openldap/slapd.d/cn\=config
vim /etc/openldap/slapd.d/cn\=config/olcDatabase\=\{2\}hdb.ldif

image-20220518194041425

  • olcRootDN:cn=root 中的 root是管理员用户名
  • olcRootPW:表示管理员密码

修改管理员信息

1
vim /etc/openldap/slapd.d/cn\=config/olcDatabase\=\{1\}monitor.ldif

image-20220518193954213

  • dn.base是修改OpenLDAP的管理员的相关信息

验证OpenLDAP的基本配置,使用如下命令:

1
slaptest -u

image-20220518194138726

重启

1
2
sudo systemctl restart slapd
sudo systemctl status slapd

配置OpenLDAP数据库

OpenLDAP默认使用的数据库是BerkeleyDB,现在来开始配置OpenLDAP数据库,使用如下命令:

1
2
3
4
5
cp /usr/share/openldap-servers/DB_CONFIG.example /var/lib/ldap/DB_CONFIG
cp /usr/share/openldap-servers/slapd.ldif /etc/openldap/slapd.ldif
chown ldap:ldap -R /var/lib/ldap
chmod 700 -R /var/lib/ldap
ll /var/lib/ldap/

image-20220518194451664

/var/lib/ldap/就是 BerkeleyDB 数据库默认存储的路径。

导入基本Schema

1
2
3
ldapadd -Y EXTERNAL -H ldapi:/// -f /etc/openldap/schema/cosine.ldif
ldapadd -Y EXTERNAL -H ldapi:/// -f /etc/openldap/schema/nis.ldif
ldapadd -Y EXTERNAL -H ldapi:/// -f /etc/openldap/schema/inetorgperson.ldif

安装 migrationtools

migrationtools 实现OpenLDAP 用户及用户组的添加,migrationtools 开源工具通过查找/etc/passwd、/etc/shadow、/etc/groups 生成LDIF 文件,并通过ldapadd 命令更新数据库数据,完成用户添加。

1
yum install -y migrationtools

修改migrate_common.ph文件

migrate_common.ph文件主要是用于生成ldif文件使用,修改migrate_common.ph文件,如下:

1
vim /usr/share/migrationtools/migrate_common.ph +71

写入

1
2
3
$DEFAULT_MAIL_DOMAIN = “maxzhao.com”;
$DEFAULT_BASE = “dc=maxzhao,dc=com”;
$EXTENDED_SCHEMA = 1;

image-20220518195011644

添加用户和用户组

管理员用户就是 root,没有普通用户

添加系统用户

1
2
3
4
5
6
groupadd ldapg1
groupadd ldapg2
useradd -g ldapg1 ldapu1
useradd -g ldapg2 ldapu2
echo '1' | passwd --stdin ldapu1
echo '2' | passwd --stdin ldapu2

生成 ldif文件

取出用户

1
2
3
4
grep ":10[0-9][0-9]" /etc/passwd > /root/users
grep ":10[0-9][0-9]" /etc/group > /root/groups
cat users
cat groups

image-20220518195600679

根据上述生成的用户和用户组属性,使用migrate_passwd.pl文件生成要添加用户和用户组的ldif,如下:

1
2
3
4
/usr/share/migrationtools/migrate_passwd.pl /root/users > /root/users.ldif
/usr/share/migrationtools/migrate_group.pl /root/groups > /root/groups.ldif
cat users.ldif
cat groups.ldif

image-20220518195823844

image-20220518195845785

导入用户到LDAP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
cat > /root/base.ldif << EOF
dn: dc=maxzhao,dc=com
o: maxzhao com
dc: maxzhao
objectClass: top
objectClass: dcObject
objectclass: organization
dn: cn=root,dc=maxzhao,dc=com
cn: root
objectClass: organizationalRole
description: Directory Manager
dn: ou=People,dc=maxzhao,dc=com
ou: People
objectClass: top
objectClass: organizationalUnit
dn: ou=Group,dc=maxzhao,dc=com
ou: Group
objectClass: top
objectClass: organizationalUnit
EOF

导入基础数据库:

1
ldapadd -x -w "maxzhao" -D "cn=root,dc=maxzhao,dc=com" -f /root/base.ldif

导入用户到数据库

1
ldapadd -x -w "maxzhao" -D "cn=root,dc=maxzhao,dc=com" -f /root/users.ldif

image-20220518200312581

查看BerkeleyDB数据库文件

1
ll /var/lib/ldap/

https://www.ilanni.com/?p=13775

本文地址: https://github.com/maxzhao-it/blog/post/4754f56b/

前言

计算机系统时间是从 1970-01-01 00:00:00 开始计算的。

脚本

获取当前日期+时间

1
2
3
4
5
6
7
8
# 2022-05-16 21:05:53
currentTime =`date "+%Y-%m-%d %H:%M:%S"`
# 简写
currentTime =`date "+%F %T"`
# yyyy-MM-dd
currentTime =`date "+%Y-%m-%d"`
# HH:mm:ss
currentTime =`date "+%H:%M:%S"`

获取时间戳

1
2
3
4
5
6
# 时间戳(秒) 输出:1652706914
seconds=`date '+%s'`
# 获取时间纳秒数
nanoseconds=`date '+%N'`
# 获取当前时间的纳秒级时间戳(不建议使用)
current_timestamp=$((`date '+%s'`*1000+`date '+%N'`/1000000))

获取某个时间的秒数

1
date -d "2022-05-16 00:00:00" +%s

将时间戳转换为时间

1
2
3
4
#Mon May 16 21:15:14 CST 2022
date -d @1652706914
# yyyy-MM-dd HH:mm:ss
date -d "1970-01-01 UTC 1652706914 seconds" "+%F %T"

format格式说明表如下

1
2
# 查看脚本
date --help
格式 说明
%% %的转义
%a 当地星期几的缩写,例如Sun、日
%A 当地星期几的全称,例如Sunday、星期二
%b 当地月份的缩写,例如Jan、12月
%B 当地月份的全称,例如January、十二月
%c 当地日期和时间,例如Thu Mar 3 23:05:25 2005,2018年12月18日 星期二 15时46分23秒
%C 输出世纪,例如现在是2
%d 当前月份的第几天,例如18(2018-12-18)
%D 日期,格式与%m%d%y,年为两位数,例如12/18/18
%e 当前月份的第几天,例如08(2018-12-08)
%F 完整格式的日期,与%Y-%m-%d相同,例如2018-12-18
%g 年份中的后两位数,例如18
%G
%h 与%b一样
%H 小时(00…23),即24小时制
%I 小时(01…12),即12小时制
%j 一年中的第几天(001…366)
%k 小时(1…23)
%l 小时(1…12)
%m 月份(01…12)
%M 分钟(01…59)
%n 新行
%N 纳秒(000000000…999999999)
%p 当地上午或下午,例如PM、下午
%P 当地上午或下午(小写),例如pm、下午
%q 第几季度(1…4)
%r 当地12小时制的时间格式,例如下午 04时06分24秒
%R 24小时制的时分(%H:%M),例如16:07
%s 从1970-01-01 00:00:00 UTC到现在的秒数
%S 当前分钟的秒数(00…59)
%T 等价%H:%M:%S,时分秒
%u 从星期一开始数,一周中的第几天(1…7)
%U 从星期日开始数,一年中的第几周(00…53)
%V ISO周数,从周一开始数(01…53)
%w 从周日开始数,一周中的第几天(0…6)
%W 从星期一开始数,一年中的第几周(00…53)
%x 当地日期,例如2018年12月18日
%X 当地时间,例如16时16分17秒
%y 年份的后两位数(00…99)
%Y 年份
%z 时区,+hhmm,例如东八区+0800
%? 时区,+hh::mm,例如东八区+08:00
%:? 时区,+hh::mm:ss,例如东八区+08:00:00
%Z 时区的缩写,例如东八区CST

本文地址: https://github.com/maxzhao-it/blog/post/805f48c5/

导出

手动导出内存快照

1
jmap -dump:format=b,file=./java_pid6902.hprof 6902

自动导出

添加脚本命令

1
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./dump/

jhat 查看

打开 hprof

1
2
# 端口默认 7000 -J-Xmx512m
jhat -port 7000 -J-Xmx512m java_pid14827.hprof
  • 使用-J-Xmx512m来设置最大堆大小为512M

jvisualvm 查看

直接载入文件

参考:

IBM:使用 HPROF 概要文件分析器

IBM:HPROF 输出文件的说明

HPROF: A Heap/CPU Profiling Tool

深入浅出JProfiler

使用JProfiler进行内存分析

Introduction To JProfiler

本文地址: https://github.com/maxzhao-it/blog/post/a8a44639/

RestTemplate

介绍

用于同步客户端HTTP访问的Spring scentral类。它简化了与httpserver的通信,并实施了RESTful原则。它处理HTTP连接,让应用程序代码提供url(带有可能的模板变量)并提取结果。(简化了发起HTTP请求以及处理响应的过程,并且支持REST。)

RestTemplate默认依赖JDK提供http连接的能力(HttpURLConnection),如果有需要的话也可以通过setRequestFactory方法替换为例如 Apache HttpComponents、Netty或OkHttp等其它HTTP library。

静态属性配置超时时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;

@Slf4j
public class RestTemplateUtils {
/**
* 舆情请求工具
*/
public static final RestTemplate REST_TEMPLATE;

static {
/*设置请求工具*/
REST_TEMPLATE = new RestTemplate();
HttpComponentsClientHttpRequestFactory clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory();
clientHttpRequestFactory.setConnectionRequestTimeout(60000);
clientHttpRequestFactory.setConnectTimeout(60000);
clientHttpRequestFactory.setReadTimeout(60000);
REST_TEMPLATE.setRequestFactory(clientHttpRequestFactory);
}
}

Spring Bean配置类

创建HttpClientConfig类,设置连接池大小、超时时间、重试机制等。配置如下:

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
100
101
102
103
104
105
106
107
108
109
110
111
/**
* - Supports both HTTP and HTTPS
* - Uses a connection pool to re-use connections and save overhead of creating connections.
* - Has a custom connection keep-alive strategy (to apply a default keep-alive if one isn't specified)
* - Starts an idle connection monitor to continuously clean up stale connections.
*/
@Configuration
public class HttpClientConfig {

private static final Logger LOGGER = LoggerFactory.getLogger(HttpClientConfig.class);

@Resource
private HttpClientProperties p;

@Bean
public PoolingHttpClientConnectionManager poolingConnectionManager() {
SSLContextBuilder builder = new SSLContextBuilder();
try {
builder.loadTrustMaterial(null, new TrustStrategy() {
public boolean isTrusted(X509Certificate[] arg0, String arg1) {
return true;
}
});
} catch (NoSuchAlgorithmException | KeyStoreException e) {
LOGGER.error("Pooling Connection Manager Initialisation failure because of " + e.getMessage(), e);
}

SSLConnectionSocketFactory sslsf = null;
try {
sslsf = new SSLConnectionSocketFactory(builder.build());
} catch (KeyManagementException | NoSuchAlgorithmException e) {
LOGGER.error("Pooling Connection Manager Initialisation failure because of " + e.getMessage(), e);
}

Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder
.<ConnectionSocketFactory>create()
.register("https", sslsf)
.register("http", new PlainConnectionSocketFactory())
.build();

PoolingHttpClientConnectionManager poolingConnectionManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry);
poolingConnectionManager.setMaxTotal(p.getMaxTotalConnections()); //最大连接数
poolingConnectionManager.setDefaultMaxPerRoute(p.getDefaultMaxPerRoute()); //同路由并发数
return poolingConnectionManager;
}

@Bean
public ConnectionKeepAliveStrategy connectionKeepAliveStrategy() {
return new ConnectionKeepAliveStrategy() {
@Override
public long getKeepAliveDuration(HttpResponse response, HttpContext httpContext) {
HeaderElementIterator it = new BasicHeaderElementIterator
(response.headerIterator(HTTP.CONN_KEEP_ALIVE));
while (it.hasNext()) {
HeaderElement he = it.nextElement();
String param = he.getName();
String value = he.getValue();
if (value != null && param.equalsIgnoreCase("timeout")) {
return Long.parseLong(value) * 1000;
}
}
return p.getDefaultKeepAliveTimeMillis();
}
};
}

@Bean
public CloseableHttpClient httpClient() {
RequestConfig requestConfig = RequestConfig.custom()
.setConnectionRequestTimeout(p.getRequestTimeout())
.setConnectTimeout(p.getConnectTimeout())
.setSocketTimeout(p.getSocketTimeout()).build();

return HttpClients.custom()
.setDefaultRequestConfig(requestConfig)
.setConnectionManager(poolingConnectionManager())
.setKeepAliveStrategy(connectionKeepAliveStrategy())
.setRetryHandler(new DefaultHttpRequestRetryHandler(3, true)) // 重试次数
.build();
}

@Bean
public Runnable idleConnectionMonitor(final PoolingHttpClientConnectionManager connectionManager) {
return new Runnable() {
@Override
@Scheduled(fixedDelay = 10000)
public void run() {
try {
if (connectionManager != null) {
LOGGER.trace("run IdleConnectionMonitor - Closing expired and idle connections...");
connectionManager.closeExpiredConnections();
connectionManager.closeIdleConnections(p.getCloseIdleConnectionWaitTimeSecs(), TimeUnit.SECONDS);
} else {
LOGGER.trace("run IdleConnectionMonitor - Http Client Connection manager is not initialised");
}
} catch (Exception e) {
LOGGER.error("run IdleConnectionMonitor - Exception occurred. msg={}, e={}", e.getMessage(), e);
}
}
};
}

@Bean
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setThreadNamePrefix(" poolScheduler");
scheduler.setPoolSize(50);
return scheduler;
}
}

然后再配置RestTemplateConfig类,使用之前配置好的CloseableHttpClient类注入,同时配置一些默认的消息转换器:

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
/**
* RestTemplate客户端连接池配置
*/
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class RestTemplateConfig {

@Resource
private CloseableHttpClient httpClient;

@Bean
public RestTemplate restTemplate(MappingJackson2HttpMessageConverter jackson2HttpMessageConverter) {
RestTemplate restTemplate = new RestTemplate(clientHttpRequestFactory());

List<HttpMessageConverter<?>> messageConverters = new ArrayList<>();
StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter(Charset.forName("utf-8"));
messageConverters.add(stringHttpMessageConverter);
messageConverters.add(jackson2HttpMessageConverter);
restTemplate.setMessageConverters(messageConverters);

return restTemplate;
}

@Bean
public HttpComponentsClientHttpRequestFactory clientHttpRequestFactory() {
HttpComponentsClientHttpRequestFactory rf = new HttpComponentsClientHttpRequestFactory();
rf.setHttpClient(httpClient);
return rf;
}

}

自定义异常处理器

1
2
3
4
5
6
7
8
9
REST_TEMPLATE.setErrorHandler(new DefaultResponseErrorHandler() {
@Override
public void handleError(ClientHttpResponse response) throws IOException {
/*错误请求不为 400 时,抛出异常*/
if (!HttpStatus.BAD_REQUEST.equals(response.getStatusCode())) {
super.handleError(response);
}
}
});

本文地址: https://github.com/maxzhao-it/blog/post/63856b15/

前言

了解

驻场研发人员反应生产系统宕机,在开发环境连接线上数据库不会复现。

重启系统后系统正常访问,但是在一周之内又会出现当宕机情况。

在读取项目日志文件之后,发现 OOM:java head space

  • 生产环境项目使用:Spring boot jar 方式部署
  • Java环境:jdk1.8

第一阶段

主要思路

堆内存溢出,可能是由于某段时间处理内容过多导致的,所以增加堆内存空间解决。

  1. 加大堆内存
  2. 复现OOM
  3. 实时监控

过程

加大堆内存到8G

启动参数上添加

1
java -Xmx8G -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -Xloggc:gc.log

这里遇到了一个问题,在紧急响应系统重启后 gc.log 文件会被覆盖,在第二阶段会讲到解决覆盖问题。

Tomcat官方推荐在 setenv.bat/setenv.sh配置,Tomcat 说明

模拟用户访问

使用前端脚本模拟用户操作

实时监控

服务器使用实时监控

方式一:

1
2
3
4
# 查询程序的 pid
jps -mVl
# 实时监控 JVM 堆
jstat -gc -h10 -t pid 1000 8

方式二:

添加程序 jmx启动参数,使用 jconsole实时监控

1
2
3
4
5
6
7
8
java -Djava.rmi.server.hostname=xx.xx.xx.xx
-Dfile.encoding=utf-8
-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=50001
-Dcom.sun.management.jmxremote.rmi.port=50002
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.authenticate=maxzhao
-jar xxx.jar

使用 jconsole连接

image-20220524105611920

结果

偶发性极强,无法主观复现。

对于线上系统,实时监控效率低下,意义不大。

第二阶段

主要思路

由于实时分析JVM行不通,我们这里在OOM发生时自动导出 dump,然后分析。

  1. 打印GC日志、输出oom时的dump文件
  2. 分析OOM时的内存dump文件

过程

配置启动参数

打印JVM GC 信息和oom时的dump文件,启动参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
current=`date '+%s'`
java \
# 为了更好的复现,最大堆内存改为 4G
-Xmx4G \
# 打印出GC详细信息
-XX:+PrintGCDetails \
# 打印时间戳
-XX:+PrintGCDateStamps \
-XX:+PrintHeapAtGC \
# 记录到文件,添加文件名称时间戳,防止紧急重启后覆盖文件
-Xloggc:gc.log-${current} \
# JVM 一个日志文件达到了20M以后,会生成新的,保留50个历史
-XX:+UseGCLogFileRotation \
-XX:GCLogFileSize=20M \
-XX:NumberOfGCLogFiles=50 \
# 内存溢出异常时Dump出当前的堆内存转储快照
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=./dump/ \
-jar xxx.jar

这里使用时间戳的方式解决系统重启导致 gc.log 文件被覆盖的问题

这里堆内存设置4G,不能过小,否则可能无法获取真实的OOM产生原因。

查询系统 JVM 参数

查询 Java 进程

1
jps -mVl

查询 vm 参数信息

1
jinfo -flags pid

输出

1
2
3
4
5
6
7
Attaching to process ID 15724, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.291-b10
Non-default VM flags:
# 命令行指定的参数
Command line:

实时监控

实时监控分为三块

1、TOP 监控整理进程

1
top

现象:内存一直在涨,最总涨到 27% (服务器内存 16G),没有 oom

2、监控 GC 日志

1
tail -f  gc.log-xxx

现象:``YGC 正常,Full GC` 正常

OOM分析

系统宕机,oom发生

发生在 2022-5-20:17:31

gc.log.xxx日志分析

image-20220524110934865

可以从日志中了解到:

  1. oom 前半小时,在 16:54:32开始内存处于不正常的增长, YGCFGC异常。
  2. FGC频发占用资源。

oom dump文件分析

dump 文件:java_pid16600.hprof,大小 4.01G(我们配置的最大堆内存4G)。

jvisualvm打开 hprof

这里使用 jdk1.8jvisualvm

配置 jvisualvm的初始内存配置,本机文件所在路径 ${JAVA_HOME}\lib\visualvm\etc\visualvm.conf

找到 visualvm_default_options 配置 -J-Xmx8G就可以正常打开 4Ghprof

image-20220524111550357

命令行打开 jvisualvm,点击左上角的文件-载入

image-20220524111838333

概要信息

image-20220530114251459

在概要信息里,我们可以点击 异常错误的线程来查看异常信息。

对于OOM来讲,一般情况下,堆栈被几百上千个对象占用,无法判断是否有问题,而且错误的堆栈不一定是产生OOM的原因,可能是压垮整个系统的最后一根稻草,所以具体我们还需要查看堆中的实例

查看类的内容

image-20220524114048695

image-20220524113951091

这里我们可以看到

  1. char[] 占用2.2G
  2. String 占用 1G
  3. BigDecimal 占用 291MB
  4. 对象X 占用 412MB

查看实例

char[]是比较难看懂的,我们这里直接看 String

双击 String 所在的行

image-20220524115014932

随机看一看其它String 实例,发现绝大部分都是属于 对象X

对象X是一个百万级的集合,根据 gc.log.xxx 异常的时间,以及操作日志,找出所有请求的URL

然后根据这些接口排查持久层返回 对象X 的方法。

相关代码

1
2
3
4
5
6
7
判断条件A是否存在
存在加入where 条件
判断条件B是否存在
存在加入where 条件
判断条件C是否存在
存在加入where 条件
条件D加入where 条件

当前查询没有分页控制,如果主要的三个判断条件都不存在时,查询后会导致结果集过大,从而内存溢出。

修改方式

根据当前业务,这里添加了分页,取第一页 10000条数据,超过10000条则不获取。

特别注意

  1. 程序中不应该出现没有分页的情况,如果出现,请考虑数据量。
  2. 后端程序不能依赖前端参数来控制代码逻辑,要使得程序可控,提高代码的健壮性。
  3. 程序一定要有完整的操作日志(包括但不限于接口、方法的参数、响应数据)
  4. Spring Booot jar内置 Tomcat core,可以不用Tomcat部署。
  5. JVM堆内存不建议设置过大,默认为服务器 1/4

安装 VisualGC 插件

当前插件可以用于实时监控

image-20220524120157898

填入地址:https://visualvm.github.io/uc/8u131/updates.xml.gz

插件官网:https://visualvm.github.io/plugins.html

image-20220524120304503

本文地址: https://github.com/maxzhao-it/blog/post/a9539589/

JConsole

这是一个可视化的 JVM 监视工具 (jvisualvm 一样)。

JMX

开启

添加启动参数

1
2
3
4
5
6
7
8
9
10
11
-Xms1024m
-Xmx8192m
-XX:PermSize=256M
-XX:MaxPermSize=1024m
-Djava.rmi.server.hostname=192.168.2.1
-Dfile.encoding=utf-8
-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=50001
-Dcom.sun.management.jmxremote.rmi.port=50002
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.authenticate=maxzhao

远程连接

image-20220429114218667

口令就是 maxzhao

本文地址: https://github.com/maxzhao-it/blog/post/3c725d5d/

开启 JMX

添加启动参数

1
2
3
4
5
6
7
8
9
10
11
-Xms1024m
-Xmx8192m
-XX:PermSize=256M
-XX:MaxPermSize=1024m
-Djava.rmi.server.hostname=192.168.2.1
-Dfile.encoding=utf-8
-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=50001
-Dcom.sun.management.jmxremote.rmi.port=50002
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.authenticate=maxzhao

查询某进程 JVM 参数

1
jinfo -flags pid

启动参数

常见配置

堆配置

  • -Xms1024m 最小堆的大小,JVM 启动后默认分配的
  • -Xmx8196m 是最大堆的大小
  • -XX:NewSize=170m 设置年轻代大小
  • -XX:NewRatio=4 设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5
  • -XX:SurvivorRatio=4 设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6
  • -XX:MaxPermSize=16m 设置持久代大小为16m,必须以M为单位。
  • -Xmn2g 是年轻代大小,推荐设置为堆的 3/8整个JVM内存大小=年轻代大小 + 年老代大小 + 持久代大小(固定64m
  • -Xss128k 是每个线程的堆栈大小,默认1M(减少这个值就可以创建更多的线程)

收集器设置

  • -XX:+UseSerialGC:设置串行收集器
  • -XX:+UseParallelGC:设置并行收集器 (JDK8默认)
  • -XX:+UseParallelOldGC:设置并行年老代收集器
  • -XX:+UseConcMarkSweepGC:设置并发收集器
并行收集器设置
  • -XX:ParallelGCThreads=20 设置并行收集器收集时使用的CPU数。并行收集线程数。
  • -XX:MaxGCPauseMillis=100 设置并行收集最大暂停时间,如果无法满足此时间,JVM 会自动调整年轻代大小,以满足此值。
  • -XX:GCTimeRatio=n 设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)
并发收集器设置
  • -XX:+CMSIncrementalMode 设置为增量模式。适用于单CPU情况。
  • -XX:ParallelGCThreads=20 设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。
串行垃圾回收器
  • -XX:NewRatio=4 表示设置年轻代:老年代的大小比值为1:4,这意味着年轻代占整个堆的1/5。
  • -XX:SurvivorRatio=8 表示设置2个Survivor区:1个Eden区的大小比值为2:8,这意味着Survivor区占整个年轻代的1/5,这个参数默认为8。
  • -XX:PretenureSizeThreshold=3145728 表示对象大于3145728(3M)时直接进入老年代分配,这里只能以字节作为单位
  • -XX:MaxTenuringThreshold=1 表示对象年龄大于1,自动进入老年代,如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代.对于年老代比较多的应用,可以提高效率。**
    如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间**,增加在年轻代即被回收的概论。

垃圾回收统计信息

GC日志:

  • -XX:+PrintGC 表示在控制台上打印出GC信息 [GC 118250K->113543K(130112K), 0.0094143 secs]
  • -XX:+PrintGCDetails
    表示在控制台上打印出GC详细信息 [GC [DefNew: 8614K->8614K(9088K), 0.0000665 secs][Tenured: 112761K->10414K(121024K), 0.0433488 secs] 121376K->10414K(130112K), 0.0436268 secs]
  • -XX:+PrintGCDateStamps 表示在控制台上打印时间戳
  • -XX:+PrintGCTimeStamps 打印GC停顿耗时 11.851: [GC 98328K->93620K(130112K), 0.0082960 secs]
  • -XX:+PrintGCApplicationConcurrentTime 打印每次垃圾回收前,程序未中断的执行时间。可与上面混合使用 Application time: 0.5291524 seconds
  • -XX:+PrintGCApplicationStoppedTime
    打印垃圾回收期间程序暂停的时间。可与上面混合使用 Total time for which application threads were stopped: 0.0468229 seconds
  • -XX:+PrintHeapAtGC:表示可以看到每次GC前后堆内存布局,打印GC前后的详细堆栈信息
  • -XX:+PrintTenuringDistribution:打印存活实例年龄信息
  • -Xloggc:./gclog/gc.log:与上面几个配合使用,把相关日志信息记录到文件以便分析。
    1
    2
    current=`date '+%s'`
    -Xloggc:./gclog/gc.log-${current}
  • -XX:+UseGCLogFileRotation JVM 一个日志文件达到了20M以后,会生成新的,保留50个历史
  • -XX:GCLogFileSize=20M JVM 一个日志文件达到了20M以后,会生成新的,保留50个历史
  • -XX:NumberOfGCLogFiles=50 JVM 一个日志文件达到了20M以后,会生成新的,一共保存50个
  • -Xnoclassgc 表示关闭JVM对类的垃圾回收
  • -XX:+DisableExplictGC 禁用显示GC
  • -XX:+ExplictGCInvokesConcurrent 使用并发方式处理显示GC
  • -verbose:gc 输出虚拟机中GC的详细情况
  • -XX:+UseTLAB 开启TLAB分配,优先在本地线程缓冲区TLAB中分配对象,避免分配内存时的锁定过程,Sever模式下默认开启。
  • -XX:TLABSize 设置TLAB大小
  • -XX:+ResizeTLAB 自动调整TLAB大小
  • -XX:+PrintTLAB 表示可以看到TLAB的使用情况
  • -XX:+TraceClassLoading 表示查看类的加载信息
    1
    java -XX:+TraceClassLoading FollowClass 
  • -XX:+TraceClassUnLoading 表示查看类的卸载信息
  • -XX:CompileThreshold=1000 表示一个方法被调用1000次之后,会被认为是热点代码,并触发即时编译
  • -XX:+UseSpining 开启自旋锁
  • -XX:+PreBlockSpin 更改自旋锁的自旋次数,使用这个参数必须先开启自旋锁。

异常:

  • -XX:+HeapDumpOnOutOfMemoryError 表示可以让虚拟机在出现内存溢出异常时Dump出当前的堆内存转储快照
  • -XX:HeapDumpPath=./dump/

回收器选择

JVM给了四种选择:

串行收集器、并行收集器、并发收集器、G1

串行收集器只适用于小数据量的情况。默认情况下,JDK5.0以后,JVM会根据当前系统配置进行判断。

吞吐量优先的并行收集器

并行收集器主要以到达一定的吞吐量为目标,适用于科学技术和后台处理等。JDK1.8默认

典型配置:

  • java -Xmx8192m -Xms4096m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20 
    
    1
    2
    3
    4
    5
    6

    此配置仅对年轻代有效。即上述配置下,年轻代使用并发收集,而年老代仍旧使用串行收集。
    `-XX:ParallelGCThreads=20` 配置并行收集器的线程数,即:同时多少个线程一起进行垃圾回收。建议与处理器数目相等。

    - ```shell
    java -Xmx8192m -Xms4096m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20 -XX:+UseParallelOldGC
    `-XX:+UseParallelOldGC` 配置年老代垃圾收集方式为并行收集。JDK6.0支持对年老代并行收集。
  • ```shell
    java -Xmx8192m -Xms4096m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:MaxGCPauseMillis=100

    1
    2
    3
    4
    5

    `-XX:MaxGCPauseMillis=100` 设置每次年轻代垃圾回收的最长时间,如果无法满足此时间,`JVM` 会自动调整年轻代大小,以满足此值。

    - ```shell
    java -Xmx8192m -Xms4096m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:MaxGCPauseMillis=100 -XX:+UseAdaptiveSizePolicy

    -XX:+UseAdaptiveSizePolicy:设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低相应时间或者收集频率等,此值建议使用并行收集器时,一直打开。

响应时间优先的并发收集器

并发收集器主要是保证系统的响应时间,减少垃圾收集时的停顿时间。适用于应用服务器、电信领域等。

典型配置:

  • ```shell
    java -Xmx8192m -Xms4096m -Xmn2g -Xss128k -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC
    1
    2
    3
    4
    5
    6

    ``-XX:+UseConcMarkSweepGC`:设置年老代为并发收集。测试中配置这个以后,-XX:NewRatio=4的配置失效了,原因不明。所以,此时年轻代大小最好用-Xmn设置。
    `-XX:+UseParNewGC`:设置年轻代为并行收集。可与CMS收集同时使用。JDK5.0以上,JVM会根据系统配置自行设置,所以无需再设置此值。

    - ```shell
    java -Xmx8192m -Xms4096m -Xmn2g -Xss128k -XX:+UseConcMarkSweepGC -XX:CMSFullGCsBeforeCompaction=5 -XX:+UseCMSCompactAtFullCollection
    -XX:CMSFullGCsBeforeCompaction:由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩、整理。
    -XX:+UseCMSCompactAtFullCollection:打开对年老代的压缩。可能会影响性能,但是可以消除碎片

调优

  1. 年轻代大小选择

    • 响应时间优先的应用尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达年老代的对象。
    • 吞吐量优先的应用:尽可能的设置大,可能到达Gbit的程度。因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用。
  2. 年老代大小选择

    • 响应时间优先的应用:年老代使用并发收集器,所以其大小需要小心设置,一般要考虑 并发会话率 和 会话持续时间

      等一些参数。如果堆设置小了,可以会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间。最优化的方案,一般需要参考以下数据获得:

      • 并发垃圾收集信息
      • 持久代并发收集次数
      • 传统GC信息
      • 花在年轻代和年老代回收上的时间比例

      减少年轻代和年老代花费的时间,一般会提高应用的效率

    • 吞吐量优先的应用:一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代。原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活对象。

  3. 较小堆引起的碎片问题

    因为年老代的并发收集器使用标 记、清除算法,所以不会对堆进行压缩。当收集器回收时,他会把相邻的空间进行合并,这样可以分配给较大的对象。但是,当堆空间较小时,运行一段时间以后,
    就会出现“碎片”,如果并发收集器找不到足够的空间,那么并发收集器将会停止,然后使用传统的标记、清除方式进行回收。如果出现“碎片”,可能需要进行如 下配置:

    • -XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩。
    • -XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次Full GC后,对年老代进行压缩

Java代码查询堆

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
/**
*
*在网上看到大部分的帖子都有介绍性能调优的案例,其中有一项就是告诉你 **`Xms`** 和 **`Xmx`** 参数一定要设置成相同的,这样就可以达到优化的目的,就像这样
*-Xms1024m -Xmx1024m
*但是却没说为什么要这么设置,那么这就是来告诉你这样设置的目的
*/
public class XmxAndXms {
public static void main(String[] args) throws InterruptedException {

XmxAndXms test = new XmxAndXms();
// 添加内存前先打印默认的堆内存情况
test.showHeapSpace();

// 开始添加内存
test.addMemory();
}

/**
* 展示堆空间大小
*/
public void showHeapSpace() {
// 每次增加对象时,totalMemory都会增大100倍
System.out.print("当前堆内存:" + Runtime.getRuntime().totalMemory() / 1024 / 1024 + "M");
System.out.print(",最大内存:" + Runtime.getRuntime().maxMemory() / 1024 / 1024 + "M");
System.out.print(",空闲内存:" + Runtime.getRuntime().freeMemory() / 1024 / 1024 + "M");
System.out.println();
}

/**
* 每2秒钟往堆内存添加一个占用100m内存的对象
* @throws InterruptedException
*/
public void addMemory() throws InterruptedException {
List<byte[]> list = new ArrayList<>();
for (int j = 0; j < 10; j++) {
System.out.print("第" + (j + 1) + "次添加内存");
// 每2秒增加100M内存
list.add(new byte[1024 * 1024 * 100]);
// 添加完后展示内存大小
this.showHeapSpace();
TimeUnit.SECONDS.sleep(2);
}
}
}

JVM 信息示例

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
Non-default VM flags: 
-XX:-BytecodeVerificationLocal
-XX:-BytecodeVerificationRemote
-XX:CICompilerCount=4
-XX:InitialHeapSize=534773760
-XX:+ManagementServer
-XX:MaxHeapSize=8589934592
-XX:MaxNewSize=2863136768
-XX:MinHeapDeltaBytes=524288
-XX:NewSize=178257920
-XX:OldSize=356515840
-XX:TieredStopAtLevel=1
-XX:+UseCompressedClassPointers
-XX:+UseCompressedOops
-XX:+UseFastUnorderedTimeStamps
-XX:-UseLargePagesIndividualAllocation
-XX:+UseParallelGC
Command line: -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:57489,suspend=y,server=n
-Dvisualvm.id=353982405207800
-Xmx8192m
-Djava.rmi.server.hostname=192.168.2.1
-Dfile.encoding=utf-8
-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=50001
-Dcom.sun.management.jmxremote.rmi.port=50002
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.authenticate=maxzhao
-XX:TieredStopAtLevel=1
-Xverify:none
-Dspring.output.ansi.enabled=always
-Dcom.sun.management.jmxremote
-Dspring.jmx.enabled=true
-Dspring.liveBeansView.mbeanDomain
-Dspring.application.admin.enabled=true

本文地址: https://github.com/maxzhao-it/blog/post/3c725d5d/

导出

手动导出内存快照

1
jmap -dump:format=b,file=./java_pid6902.hprof 6902

自动导出

添加脚本命令

1
2
current=`date '+%s'`
java -Xmx1G -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./dump/ -Xloggc:gclog.log-${current} -XX:+UseGCLogFileRotation -XX:GCLogFileSize=20M -XX:NumberOfGCLogFiles=50 -jar app.jar

更多参考JVM参数

jhat 查看

打开 hprof

1
2
# 端口默认 7000 -J-Xmx512m
jhat -port 7000 -J-Xmx512m java_pid14827.hprof
  • 使用-J-Xmx512m来设置最大堆大小为512M

jvisualvm 查看

直接载入文件,选择文件类型 .hprof

image-20220524103324385

参考:

IBM:使用 HPROF 概要文件分析器

IBM:HPROF 输出文件的说明

HPROF: A Heap/CPU Profiling Tool

深入浅出JProfiler

使用JProfiler进行内存分析

Introduction To JProfiler

本文地址: https://github.com/maxzhao-it/blog/post/f9aa9996/