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

2024-03-01 0 426
目录
  • 1. 问题背景
  • 2. 问题复现
    • 情况一:核心线程丢失
    • 情况二:服务重启
    • 情况三:Tomcat后台线程丢失
  • 3. 假死情况
    • 4. 异常处理分析
      • 4.1 Acceptor异常处理分析
      • 4.2 增加全局异常捕获
      • 4.3 Poller异常处理
    • 5. 结论

      1. 问题背景

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

      2. 问题复现

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

      情况一:核心线程丢失

      OOM之前Tomcat线程情况

      ID线程名称Group125http-nio-9989-Acceptor-0main126http-nio-9989-AsyncTimeoutmain123http-nio-9989-ClientPoller-0main124http-nio-9989-ClientPoller-1main113http-nio-9989-exec-1main

      OOM之后Tomcat线程情况

      ID线程名称Group123http-nio-9989-ClientPoller-0main1431http-nio-9989-exec-103main

      情况二:服务重启

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

      情况三:Tomcat后台线程丢失

      只有后台线程丢失,但是Acceptor线程和Poller线程还存在

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

      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线程有打印异常情况。

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

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

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

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

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

      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) 打赏

      感谢您的支持,我会继续努力的!

      打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
      点赞 (0)

      悠久资源 Tomcat Tomcat出现假死原因及解决方法 https://www.u-9.cn/server/tomcat/175809.html

      常见问题

      相关文章

      发表评论
      暂无评论
      官方客服团队

      为您解决烦忧 - 24小时在线 专业服务