springboot微服务应用生产重启/卡死问题排查与解决
现象
公司业务与企业微信相关,客户数量百万以上,每个应用各2台容器,处理业务与企业微信回调,普通业务量并不大,主要流量为企业微信回调与其他三方调用,三方调用接口相应基本1s内,除了业务问题,接口没出现过其他异常情况。数据来源基本上是通过企业微信来,员工几万与客户几百万数据量还可以,正常处理没有什么问题,有些批量操作就要出现卡顿等情况了,比如定时任务,某些定时任务执行时间需要几小时,甚至一天还多,期间服务出现凌晨几乎每天重启,服务卡死无法处理请求的情况。接下来一起来分析解决一下这两个问题。
常用命令
服务部署在Kubernetes,日志虽然有收集在elk中,但是查看不方便,并且不实时,所以我一般直接在Kubernetes Dashboard进入容器查看日志,找到你的容器,点击如图按钮(可能版本不同入口不一样,大同小异)
开始之前先介绍几个常用命令
tail
- 查看日志文件的最后几行,显示 server.log 文件的最后 100 行
tail filename.log
- 查看文件最后20行:
tail -n 20 filename.log
- 实时跟踪查看文件末尾的新内容:
tail -f filename.log
- 实时跟踪查看多个文件末尾的新内容
tail -f filename1.log filename2.log
这些命令可以结合管道操作符(|)和其他命令一起使用,以提供过滤、搜索等功能。
less
less 是一个用于查看文件内容的工具,它支持分页显示,并且允许用户前后浏览文件。如果你需要使用 less 来查看日志文件,可以直接将 less 命令与日志文件相关联。
例如,如果你有一个名为 app.log 的日志文件,你可以使用以下命令来查看它:
less app.log
在 less 中,你可以使用以下快捷键来浏览文件:
j 箭头键向下移动一行
k 或箭头键向上移动一行
PgDn 下翻页
PgUp 上翻页
/ 始搜索文本 (搜索之后n下一个,N上一个)
q 出 less
grep、sed、awk
重启
每个服务2个pod,每日固定一个pod重启,且均为凌晨2点左右。
分析
应用使用定时任务xxjob,xxjob有部分任务执行效率低下,且有许多异步操作,不太好寻找原因,pod在重启前内存飙升,几乎跑满4G还多。
线上原因无法查看HeapDump(典型的带有堆转储的启动参数可以是:java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./java_pid
涉及命令
# 查看java应用pid。
jps
# 查看应用启动参数等其他信息。
jinfo <pid>
# 查看应用程序中用来生成Java虚拟机当前时刻线程的堆栈跟踪的命令行工具。
jstack <pid>
# jmap(Java Virtual Machine Memory Map)是JDK提供的一个可以生成Java虚拟机的堆转储快照dump文件的命令行工具。
# 除此以外,jmap命令还可以查看finalize执行队列、Java堆和方法区的详细信息,比如空间使用率、当前使用的什么垃圾回收器、分代情况等等。
jmap -heap <pid>
本地测试无法复现重启的情况,观察dump日志发现部分业务数据长期占用内存,没有及时清楚部分无用list,且数据量大,怀疑此处为导致重启的罪魁祸首。
首先本地模拟数据测试原方法堆栈情况,启动项目,调用可以代码,本地生成dump文件
jmap -dump:format=b,file=d:/dump.hprof <pid>
使用dump分析工具MAT。
预留空位,待招租。
代码如下
# 版本1
# 分页获取5个销售的所有客户
List<Customer> customers = batchGetCustomersByUser(users);
if (!customers.isEmpty()) {
//保存客户基本信息,并返回 客户ID列表
// customerIdRelationMap: 客户ID列表
final Map<String, Long> customerIdRelationMap = this.saveCustomerBasicInfo(customers);
Map<String, Long> userIdRelationMap = getUserIdRelationMap(users);
Map<String, Long> ignoreMembers = new ConcurrentHashMap<>();
//转换客户跟进备注信息
final Map<Long, Set<CustomerFollower>> followers = new HashMap<>();
final Map<Long, Set<CustomerFollowMobile>> followMobiles = new HashMap<>();
final Map<Long, List<WecomWrappers.MarkCustomerTag>> markCustomerTags = new HashMap<>();
Utils.analysisToDataSchema(customerIdRelationMap, userIdRelationMap, customers, followers, followMobiles, markCustomerTags, ignoreMembers);
if (!ignoreMembers.isEmpty()) log.warn("ignoreMembersData-{}", ignoreMembers);
//保存客户成员跟进信息
this.saveCustomerFollowers(followers);
//保存客户成员备注手机号信息
this.saveFollowMobiles(followMobiles);
//保存客户成员备注标签信息
this.saveFollowTags(authArgs, markCustomerTags);
}
猜测版本一中问题:
分页获取customers,customers中极端数据量可能为单销售2W客户,也就是10W客户,如果整个方法中10w数据一直存活,再扩展手机号,基础信息等其他数据,可能指数级数据均不能垃圾回收,可能导致OOM,修改为如下版本,大量数据使用结束之后及时清除,及时允许垃圾回收器回收,不用等到整个方法结束。
# 版本2
# 分页获取5个销售的所有客户
List<Customer> customers = batchGetCustomersByUser(users);
if (!customers.isEmpty()) {
//保存客户基本信息,并返回 客户ID列表
Set<CustomerInfo> customerInfos = getCustomerInfos(customers);
final Map<String, Long> customerIdRelationMap = this.saveCustomerBasicInfo(customerInfos);
//userIdRelationMap: 获取用户的具体信息的列表,通过成员ID列表 到system服务中获取数据
Map<String, Long> userIdRelationMap = getUserIdRelationMap(users);
Map<String, Long> ignoreMembers = new ConcurrentHashMap<>();
//转换客户跟进备注信息
Map<Long, Set<CustomerFollower>> followers = new HashMap<>();
Map<Long, Set<CustomerFollowMobile>> followMobiles = new HashMap<>();
Map<Long, List<WecomWrappers.MarkCustomerTag>> markCustomerTags = new HashMap<>();
Utils.analysisToDataSchema(customerIdRelationMap, userIdRelationMap, customers, followers, followMobiles, markCustomerTags, ignoreMembers);
if (!ignoreMembers.isEmpty()) {
log.warn("ignoreMembersData-{}", ignoreMembers);
}
customers.clear();
userIdRelationMap.clear();
ignoreMembers.clear();
//保存客户成员跟进信息
this.saveCustomerFollowers(followers);
followers.clear();
//保存客户成员备注手机号信息
this.saveFollowMobiles(followMobiles);
followMobiles.clear();
//保存客户成员备注标签信息
this.saveFollowTags(authArgs, markCustomerTags);
markCustomerTags.clear();
}
优化之后dump分析工具查看
《广告招租》
卡死
欲知后事如何,请看下回分解。
评论区