java项目线上OOM实例排查

项目背景

最近总是报警提示线上一个项目进程停止, 然后重启(机器上配置的检测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天,查看前端请求才最终确定了问题原因,唉,还能说什么呢。

  • 13
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值