当前位置: 代码网 > 服务器>服务器>Tomcat > Tomcat出现假死原因及解决方法

Tomcat出现假死原因及解决方法

2024年05月14日 Tomcat 我要评论
1. 问题背景线上环境因为有个接口内部在错误的参数下,会不断生成字符串,导致oom,在oom之后服务还能正常运行,但是发送的api请求已经没有办法响应了。2. 问题复现模拟线上问题,在测试环境上进行复

1. 问题背景

线上环境因为有个接口内部在错误的参数下,会不断生成字符串,导致oom,在oom之后服务还能正常运行,但是发送的api请求已经没有办法响应了。

2. 问题复现

模拟线上问题,在测试环境上进行复现,一段时间后服务会爆出oom,但是不是每次都会导致tomcat假死,有些情况下tomcat还能正常访问。

情况一:核心线程丢失

oom之前tomcat线程情况

id线程名称group
125http-nio-9989-acceptor-0main
126http-nio-9989-asynctimeoutmain
123http-nio-9989-clientpoller-0main
124http-nio-9989-clientpoller-1main
113http-nio-9989-exec-1main

oom之后tomcat线程情况

id线程名称group
123http-nio-9989-clientpoller-0main
1431http-nio-9989-exec-103main

情况二:服务重启

日志打印java.lang.outofmemoryerror: java heap space后,服务重启。

情况三:tomcat后台线程丢失

只有后台线程丢失,但是acceptor线程和poller线程还存在

3. 假死情况

从tomcat的nio模型得知有几个组件,acceptor、poller、业务线程池。这三个组件情况如下:

  • acceptor线程:该线程主要是监听连接(socket.accept()),如果该线程挂掉,那么及时操作系统层面tcp3次握手成功,但是业务上也办法获取到这个连接。默认情况下,只有1个acceptor线程,可以通过acceptorthreadcount参数设置。
  • poller线程:acceptor获取到连接之后,会轮询从poller列表中取一个poller进行处理。如果poller线程挂掉了,那么就没法处理读请求了。默认情况下,会有min(2,cpu核数)个poller线程,可以通过pollerthreadcount参数设置。
  • 业务线程:poller线程将读请求放到业务线程处理,如果业务线程阻塞(比如被某个网络io阻塞),那么此刻的读请求还在业务线程池的队列中,没有被处理。默认情况下,最小线程为10,可以通过minsparethreads参数设置,最大线程为200,可以通过maxthreads参数设置。

此时分析再结合复现的情况,如果核心线程挂掉,那确实存在假死情况。但是从事发现场来看,并没有发现tomcat的acceptor、poller线程打印出outofmemoryerror的异常,及时将org.apache.tomcat和org.apache.catalina设置成debug级别。因此需要深入源码分析。

4. 异常处理分析

4.1 acceptor异常处理分析

acceptor逻辑如下,就是在循环内不断地监听accept(),查看是否有新连接。

//nioendpoint$acceptor#run
protected class acceptor extends abstractendpoint.acceptor {
    @override
    public void run() {
        int errordelay = 0;
        // loop until we receive a shutdown command
        while (running) {
            //...忽略一些代码
            state = acceptorstate.running;
            try {
                //if we have reached max connections, wait
                countuporawaitconnection();
                socketchannel socket = null;
                try {
                    socket = serversock.accept();
                } catch (ioexception ioe) {
                }
                // successful accept, reset the error delay
                errordelay = 0;
                // configure the socket
                if (running && !paused) {
                    // setsocketoptions() will hand the socket off to
                    // an appropriate processor if successful
                    if (!setsocketoptions(socket)) {
                        closesocket(socket);
                    }
                } else {
                    closesocket(socket);
                }
            } catch (throwable t) {
                exceptionutils.handlethrowable(t);
                log.error(sm.getstring("endpoint.accept.fail"), t);
            }
        }
        state = acceptorstate.ended;
    }
	//...忽略一些代码
}

其中setsocketoptions是往poller中调用register方法,把这个socket传递过去。getpoller0()方法会以轮询的策略获取一个poller

//nioendpoint#setsocketoptions
protected boolean setsocketoptions(socketchannel socket) {
    // process the connection
    try {
        //disable blocking, apr style, we are gonna be polling it
        socket.configureblocking(false);
        socket sock = socket.socket();
        socketproperties.setproperties(sock);

        niochannel channel = niochannels.pop();
        if (channel == null) {
            socketbufferhandler bufhandler = new socketbufferhandler(
                    socketproperties.getappreadbufsize(),
                    socketproperties.getappwritebufsize(),
                    socketproperties.getdirectbuffer());
            if (issslenabled()) {
                channel = new secureniochannel(socket, bufhandler, selectorpool, this);
            } else {
                channel = new niochannel(socket, bufhandler);
            }
        } else {
            channel.setiochannel(socket);
            channel.reset();
        }
        getpoller0().register(channel);
    } catch (throwable t) {
        exceptionutils.handlethrowable(t);
        try {
            log.error("",t);
        } catch (throwable tt) {
            exceptionutils.handlethrowable(tt);
        }
        // tell to close the socket
        return false;
    }
    return true;
}

此处重点看一下exceptionutils.handlethrowable方法的逻辑,因为outofmemoryerror是virtualmachineerror的子类,所以这里会被直接抛出异常,。而outofmemeoryerror属于 uncheck exception,抛出uncheck exception就会导致线程终止,并且主线程和其他线程无法感知这个线程抛出的异常。如果线程代码(run方法之外)之外来捕获这个异常的话,可以通过thread的setuncaughtexceptionhandler处理。

//exceptionutils#handlethrowable
public static void handlethrowable(throwable t) {
    if (t instanceof threaddeath) {
        throw (threaddeath) t;
    }
    if (t instanceof stackoverflowerror) {
        // swallow silently - it should be recoverable
        return;
    }
    if (t instanceof virtualmachineerror) {
        throw (virtualmachineerror) t;
    }
    // all other instances of throwable will be silently swallowed
}

再看启动的时候,线程是否会设置uncaughtexceptionhandler,发现并没有设置,所以异常没法被正常打印到日志中。

//abstractendpoint#startacceptorthreads
protected final void startacceptorthreads() {
    int count = getacceptorthreadcount();
    acceptors = new acceptor[count];

    for (int i = 0; i < count; i++) {
        acceptors[i] = createacceptor();
        string threadname = getname() + "-acceptor-" + i;
        acceptors[i].setthreadname(threadname);
        thread t = new thread(acceptors[i], threadname);
        t.setpriority(getacceptorthreadpriority());
        t.setdaemon(getdaemon());
        t.start();
    }
}

小结:outofmemoryerror被捕获了,然后重新抛出,但是因为outofmemoryerror是uncheck exception,而线程没有设置uncaughtexceptionhandler,所以没法被打印。

4.2 增加全局异常捕获

在启动的时候,设置全局线程nncaughtexception处理器。这里简单打印线程名称,并且抛出异常。

thread.setdefaultuncaughtexceptionhandler(new thread.uncaughtexceptionhandler() {
    @override
    public void uncaughtexception(thread t, throwable e) {
        logger.error("[global handler]thread-name:{},happen exp,", t.getname(), e);
    }
});

重新复现问题,发现acceptor线程有打印异常情况。

不过,同时也发现,poller线程有打印错误日志,但并不是全局处理器打印的。

下图为arthas截图,发现仍然poller线程仍然存在。因此再分析poller的异常处理。

4.3 poller异常处理

从上面的异常日志俩看,poller线程是在处理pollerevent中处理register事件时的抛出异常,查看相关代码。发现此处捕获的是exception,而outofmemoryerror属于error,所以此处不会被捕获到,并且会往上抛出。

//nioendpoint$poller#events
public void run() {
    if (interestops == op_register) {
        try {
            socket.getiochannel().register(
                    socket.getpoller().getselector(), selectionkey.op_read, socketwrapper);
        } catch (exception x) {
            log.error(sm.getstring("endpoint.nio.registerfail"), x);
        }
    }
    //...
}

在poller的events方法中,会循环调用pollerevent的run方法,这里内部有捕获一个throwable,而error是继承throwable所以outofmemoryerror会在这里被捕获,而且会打印日志,并且线程不会挂掉。

//nioendpoint$poller#events
public boolean events() {
    boolean result = false;

    pollerevent pe = null;
    for (int i = 0, size = events.size(); i < size && (pe = events.poll()) != null; i++ ) {
        result = true;
        try {
            pe.run();
            pe.reset();
            if (running && !paused) {
                eventcache.push(pe);
            }
        } catch ( throwable x ) {
            log.error("",x);
        }
    }

    return result;
}

而在poller的循环中,发现也有exceptionutils.handlethrowable处理,如果在这里出现outofmemoryerror异常的话,那么poller线程将会被终止。

//nioendpoint$poller#run
public class poller implements runnable {
	public void run() {
		// loop until destroy() is called
		while (true) {
			boolean hasevents = false;
			try {
				if (!close) {
					hasevents = events();
					//....
				}
			}catch (throwable x) {
				exceptionutils.handlethrowable(x);
				log.error("",x);
				continue;
			}
            //...
		}
	}
}

小结:poller内部实现中,对于异常处理不同,有些地方能捕获异常并且poller线程正常处理,有些地方没有捕获异常,可能会因为outofmemoryerror导致线程终止

5. 结论

当应用程序出现oom的时候,tomcat核心线程有可能会挂掉,导致接口接口无法正常访问,因此要尽量避免业务上出现oom。此外,当出现oom后应用无法访问时,可以试着排查一下,是不是tomcat的核心线程挂掉导致

以上就是tomcat出现假死原因及解决方法的详细内容,更多关于tomcat假死的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com