项目背景
最近总是报警提示线上一个项目进程停止, 然后重启(机器上配置的检测java服务,停止后自动重启),报警时间不定,有时候一天可能要报一次,有时候一天报几次的,因为之前启动参数因为没有加上OOM自动dump,于是查看linux系统日志(/var/log messages)
messages
May 10 09:35:19 kernel: Out of memory: Kill process 8545 (java) score 243 or sacrifice child
May 10 09:35:19 kernel: Killed process 8545 (java), UID 2002, total-vm:5038396kB, anon-rss:1945804kB, file-rss:0kB, shmem-rss:0kB
找到这两行日志,可以指导进程8545的java程序被kenel kill了,原因是OOM了,linux内存不够,因此将得分最高的java程序给kill了。(关于linux内核因为内存吃紧而kill进程,这里就不说了,可以通过调整进程得分,让内核避免kill 掉java进程,但只是优先级问题)。
java启动配置
因为是被linux kill掉进程的,所以配置的OOM dump也不会生成。
-Xms4g -Xmx4g -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/www/dump/java.hprof
没有dump文件就自己手动dump一下吧,等待项目运行一段时间,看下linux内存情况(每10秒打印一次内存情况)
free命令
total used free shared buff/cache available
Mem: 7821 5099 169 1 2551 2433
Swap: 0 0 0
total used free shared buff/cache available
Mem: 7821 5099 169 1 2551 2434
Swap: 0 0 0
total used free shared buff/cache available
Mem: 7821 5099 169 1 2551 2434
Swap: 0 0 0
total used free shared buff/cache available
Mem: 7821 5101 168 1 2551 2432
Swap: 0 0 0
可以看到free内存是慢慢减小的,这时候手动dump一下程序内存快照。
手动dump
jmap -dumo:live,format=b,file=/dump/java.hprof <PID>
因为发生OOM的时候并没有快照,因此多dump几次,可以用来做后面的对比分析。
文件生成后down到本地,文件可能有点大,但也没办法。
MAT分析
这里我用的是eclipse memory analyzer(不知道的可以下载一个),打开后直接就提示了,是否需要泄露报告,当然选是了。
这里最好多dump几次,打开看看有什么共同点。
可以看到直接帮我们生成可能泄露的点
刚开始打开的时候就是这个org.apache.catalina.session.StandardManager占用了53.39%的内存!
但是这个StandardManager是个什么鬼,也不是我们项目里面的啊,本来预测可能是代码里面哪里有纰漏,结果出来个这,什么鬼啊。
点击details> 查看具体信息
可以看到是一个ConcurrentHasMap,里面存储的sessions。
点击with incoming references(查看哪里引用的该对象)
可以看到这个ConcurrentHasMap存储的session,然后每个session内部都有个manager。
shallow heap:占用内存
retained heap: 清理后预计释放多少内存
好吧, 这都不是我们项目里面的代码和对象啊, 看着是tomcat的(项目用的springboot,内置tomcat容器),什么鬼。。。
查看源码
还是看下这个是什么类吧,idea打开(我们项目内置tomcat是9版本的)
StandardManager
好像没有CMP哈,查看ManagerBase(找到啦):
package org.apache.catalina.session;
/**
* Minimal implementation of the <b>Manager</b> interface that supports
* no session persistence or distributable capabilities. This class may
* be subclassed to create more sophisticated Manager implementations.
*
* @author Craig R. McClanahan
*/
public abstract class ManagerBase extends LifecycleMBeanBase implements Manager {
private final Log log = LogFactory.getLog(ManagerBase.class); // must not be static
/**
* The set of currently active Sessions for this Manager, keyed by
* session identifier.
*/
protected Map<String, Session> sessions = new ConcurrentHashMap<>();
可以看到这里有个ConcurrentHashMap啊,里面正好存的是Session。
LOOK !LOOK !LOOK !
就是这个鬼,导致OOM的元凶,这样看MAT确实分析的没问题啊,但这个是tomcat的啊,难道是tomcat内存泄露BUG?
一通百度,GPT,好像低版本可能会有,让升级,这升级个毛线啊,tomcat都用的9版本了。
所以这个StandSession 究竟是什么,项目中哪里会用到?
说到这里,想到一个经常会看到的面试题:session和cookie有什么区别?
对,这个session就是题目中的session,因为我们项目比较小点(是其他项目的附属项目,用户信息什么的都是用的住项目的,所以直接就用了session存储了一下用户的信息(ID什么的),然后判断用户是否登录和全局拿用户ID直接就从session里面拿了(不用说,这个结构不合理,这里不讨论是否合理))。
tomcat是怎么管理session的
- HttpSession
我们都知道Tomcat是支持Servlet规范的web容器,所以内部会包含一些Servlet的内容。HttpSession
就是在Servlet规范中标准的Session定义。注意HttpSession
的全路径是javax.servlet.http.HttpSession
- Session
Session`接口是Catalina内部的Session的标准定义。全路径`org.apache.catalina.Session
- StandardSession
Catalina内部Session
接口的标准实现,基础功能的提供类,需要重点分析,全路径org.apache.catalina.session.StandardSession
- StandardSessionFacade
StandardSession
的外观类,也是Catalina内部的类,之所以存在这个类是因为不想直接让开发人员操作到StandardSession
,这样可以保证类的安全。该类的全路径org.apache.catalina.session.StandardSession
。
在Catalina中,Session由Session管理器组件负责管理,例如创建和销毁Session管理器是org.apache.catalina.Manager
接口的实例。
- Manager
Catalina中session管理器概念的顶层接口。其中定义了一些对session的基本操作,如创建,查找,添加,移除以及对特定session对象的属性的修改。除了基本操作以外还定义了一些特殊的例如session的序列化反序列化等等。
- ManagerBase
抽象类,对Manager
接口作了基本实现,方便继承类的复写以及实现,就像其他xxxBase
类起到了一样的作用。
- StandardManager
Catalina中默认的Session管理器的实现类。
项目代码
在项目中我们需要用到session的话,基本上也都是通过request获取httpSession
HttpSession httpSession = request(这里是HttpServletRequest).getSession();
查看这次的项目,确实,有这一样代码(大概逻辑),也只有这一个地方用到了session!
@Component
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
//先从session中获取
HttpSession httpSession = request.getSession();
if(StringUtils.equals(request.getMethod(), HttpMethod.OPTIONS.toString())){
return true;
}
//获取token
//token对应获取其他信息
//塞入session中
return true;
}
可以看到这里我们是直接getSession,那tomcat背后做了什么?
doGetSession中代码很长,我们简单点
protected Session doGetSession(boolean create) {
// There cannot be a session if no context has been assigned yet
Context context = getContext();
if (context == null) {
return null;
}
// Return the current session if it exists and is valid
if ((session != null) && !session.isValid()) {
session = null;
}
if (session != null) {
return session;
}
// Return the requested session if it exists and is valid
Manager manager = context.getManager();
if (manager == null) {
return null; // Sessions are not supported
}
if (requestedSessionId != null) {
try {
session = manager.findSession(requestedSessionId);
} catch (IOException e) {
if (log.isDebugEnabled()) {
log.debug(sm.getString("request.session.failed", requestedSessionId, e.getMessage()), e);
} else {
log.info(sm.getString("request.session.failed", requestedSessionId, e.getMessage()));
}
session = null;
}
if ((session != null) && !session.isValid()) {
session = null;
}
if (session != null) {
session.access();
return session;
}
}
// Create a new session if requested and the response is not committed
if (!create) {
return null;
}
boolean trackModesIncludesCookie =
context.getServletContext().getEffectiveSessionTrackingModes().contains(SessionTrackingMode.COOKIE);
if (trackModesIncludesCookie && response.getResponse().isCommitted()) {
throw new IllegalStateException(sm.getString("coyoteRequest.sessionCreateCommitted"));
}
// Re-use session IDs provided by the client in very limited
// circumstances.
String sessionId = getRequestedSessionId();
if (requestedSessionSSL) {
// If the session ID has been obtained from the SSL handshake then
// use it.
} else if (("/".equals(context.getSessionCookiePath())
&& isRequestedSessionIdFromCookie())) {
/* This is the common(ish) use case: using the same session ID with
* multiple web applications on the same host. Typically this is
* used by Portlet implementations. It only works if sessions are
* tracked via cookies. The cookie must have a path of "/" else it
* won't be provided for requests to all web applications.
*
* Any session ID provided by the client should be for a session
* that already exists somewhere on the host. Check if the context
* is configured for this to be confirmed.
*/
if (context.getValidateClientProvidedNewSessionId()) {
boolean found = false;
for (Container container : getHost().findChildren()) {
Manager m = ((Context) container).getManager();
if (m != null) {
try {
if (m.findSession(sessionId) != null) {
found = true;
break;
}
} catch (IOException e) {
// Ignore. Problems with this manager will be
// handled elsewhere.
}
}
}
if (!found) {
sessionId = null;
}
}
} else {
sessionId = null;
}
session = manager.createSession(sessionId);
// Creating a new session cookie based on that session
if (session != null && trackModesIncludesCookie) {
Cookie cookie = ApplicationSessionCookieConfig.createSessionCookie(
context, session.getIdInternal(), isSecure());
response.addSessionCookieInternal(cookie);
}
if (session == null) {
return null;
}
session.access();
return session;
}
可以看到先根据requestedSessionId查询是否sessions中是否有session(session = manager.findSession(requestedSessionId);),如果有直接就返回session,如果没有manager.createSession(sessionId);
那requestedSessionId是什么?
就是sessionId!!!
可以看到创建session 的时候:
String sessionId = getRequestedSessionId();
......
session = manager.createSession(sessionId);
createSession
@Override
public Session createSession(String sessionId) {
if ((maxActiveSessions >= 0) &&
(getActiveSessions() >= maxActiveSessions)) {
rejectedSessions++;
throw new TooManyActiveSessionsException(
sm.getString("managerBase.createSession.ise"),
maxActiveSessions);
}
// Recycle or create a Session instance
Session session = createEmptySession();
// Initialize the properties of the new session and return it
session.setNew(true);
session.setValid(true);
session.setCreationTime(System.currentTimeMillis());
session.setMaxInactiveInterval(getContext().getSessionTimeout() * 60);
String id = sessionId;
if (id == null) {
id = generateSessionId();
}
session.setId(id);
sessionCounter++;
SessionTiming timing = new SessionTiming(session.getCreationTime(), 0);
synchronized (sessionCreationTiming) {
sessionCreationTiming.add(timing);
sessionCreationTiming.poll();
}
return session;
}
创建后可以看到
String id = sessionId;
if (id == null) {
id = generateSessionId();
}
session.setId(id);
注意这里的session.setId(id);这不是简单的set Id !!!
protected String generateSessionId() {
String result = null;
do {
if (result != null) {
// Not thread-safe but if one of multiple increments is lost
// that is not a big deal since the fact that there was any
// duplicate is a much bigger issue.
duplicates++;
}
result = sessionIdGenerator.generateSessionId();
} while (sessions.containsKey(result));
return result;
}
StandardSession
@Override
public void setId(String id, boolean notify) {
if ((this.id != null) && (manager != null))
manager.remove(this);
this.id = id;
if (manager != null)
manager.add(this);
if (notify) {
tellNew();
}
}
可以看到是将this(StandardSession) add到manager!!!
ManagerBase
@Override
public void add(Session session) {
sessions.put(session.getIdInternal(), session);
int size = getActiveSessions();
if( size > maxActive ) {
synchronized(maxActiveUpdateLock) {
if( size > maxActive ) {
maxActive = size;
}
}
}
}
最终 session put到sessions中,这个sessions
package org.apache.catalina.session;
public abstract class ManagerBase extends LifecycleMBeanBase implements Manager {
.......
protected Map<String, Session> sessions = new ConcurrentHashMap<>();
.......
}
大概流程:
1、根据sessionId去ConcurrentHashMap中看是否已经存在session
2、如果不存在创建一个新session
3、将新session放入ConcurrentHashMap中。
session已经拿到,那sessionId(requestedSessionId)是怎么传递的?
session = manager.createSession(sessionId);
// Creating a new session cookie based on that session
if (session != null && trackModesIncludesCookie) {
Cookie cookie = ApplicationSessionCookieConfig.createSessionCookie(
context, session.getIdInternal(), isSecure());
response.addSessionCookieInternal(cookie);
}
一直往下看源码
public class SessionConfig {
private static final String DEFAULT_SESSION_COOKIE_NAME = "JSESSIONID";
private static final String DEFAULT_SESSION_PARAMETER_NAME = "jsessionid";
/**
* Determine the name to use for the session cookie for the provided
* context.
* @param context The context
* @return the cookie name for the context
*/
public static String getSessionCookieName(Context context) {
String result = getConfiguredSessionCookieName(context);
if (result == null) {
result = DEFAULT_SESSION_COOKIE_NAME;
}
return result;
}
看到没!!!默认是通过cookie中的DEFAULT_SESSION_COOKIE_NAME取出sessionId的!!!
问题解决
上面已经介绍了session的获取流程,那按理说代码中getSession也没问题啊,那为啥session数量巨大,占用大部分内存呢?
好捉急,胜利就在前方,为啥session就上去了呢………………
代码好像也没问题啊………………
———————————————————手动等待线———————————————————————————
没啥思路的时候,看了下页面请求,结果一下子找到问题了!!!!
每个请求都没有携带JSESSIONID!!!!
这里图片往截图了,就说下问题位置吧!
每次请求的request header中cookie中没有JSESSIONID,也就说每次请求都没有携带JSESSIONID,每次请求都要新建一个session!!!
JSESSIONID
为什么cookie没有携带JSESSIONID?
因为response 的时候,header中Set-Cookie属性报错了!!!
跨域的时候没有设置请求头中sameSite属性值为None!!!headers那里有个黄色叹号的!!!
大家看下正常设置的!!!
看红色那里!看红色那里!看红色那里!之前是没有的!!!
修改:很简单,response的时候设置cookie中属性值( HttpOnly;SameSite=None;Secure)。
至此修改、上线、结束!
写在最后
当然,整个排查过程也没有这么顺利了,中间top、jstat、jstack命令也各种用,包括arthas也用了,各种查看,猜测问题产生原因,刚开始一直想着是项目中哪块代码有问题,找了很多方案,结果排查到最后都不是,整个排查历时4天,查看前端请求才最终确定了问题原因,唉,还能说什么呢。