1. 程式人生 > 程式設計 >springboot中@Async預設執行緒池導致OOM問題

springboot中@Async預設執行緒池導致OOM問題

前言:

1.最近專案上在測試人員壓測過程中發現了OOM問題,專案使用springboot搭建專案工程,通過檢視日誌中包含資訊:unable to create new native thread

記憶體溢位的三種類型:
1.第一種OutOfMemoryError: PermGen space,發生這種問題的原意是程式中使用了大量的jar或class
2.第二種OutOfMemoryError: Java heap space,發生這種問題的原因是java虛擬機器建立的物件太多
3.第三種OutOfMemoryError:unable to create new native thread,建立執行緒數量太多,佔用記憶體過大

初步分析:

1.初步懷疑是執行緒建立太多導致,使用jstack 執行緒號 > /tmp/oom.log將應用的執行緒資訊打印出來。檢視oom.log,發現大量執行緒處於Runnable狀態,基本可以確認是執行緒建立太多了。

程式碼分析:

1.出問題的微服務是日誌寫庫服務,對比日誌,鎖定在writeLog方法上,wirteLog方法使用spring-@Async註解,寫庫操作採用的是非同步寫入方式。
2.之前沒有對@Async註解深入研究過,只是知道可以自定義內部執行緒池,經檢視,日誌寫庫服務並未自定義非同步配置,使用的是spring-@Async預設非同步配置
3.首先簡單百度了下,網上提到@Async預設非同步配置使用的是SimpleAsyncTaskExecutor,該執行緒池預設來一個任務建立一個執行緒,在壓測情況下,會有大量寫庫請求進入日誌寫庫服務,這時就會不斷建立大量執行緒,極有可能壓爆伺服器記憶體。

藉此機會也學習了下SimpleAsyncTaskExecutor原始碼,總結如下:

1.SimpleAsyncTaskExecutor提供了限流機制,通過concurrencyLimit屬性來控制開關,當concurrencyLimit>=0時開啟限流機制,預設關閉限流機制即concurrencyLimit=-1,當關閉情況下,會不斷建立新的執行緒來處理任務,核心程式碼如下:

public void execute(Runnable task,long startTimeout) {
  Assert.notNull(task,"Runnable must not be null");
  Runnable taskToUse = (this.taskDecorator != null ? this.taskDecorator.decorate(task) : task);
  //判斷是否開啟限流機制
  if (isThrottleActive() && startTimeout > TIMEOUT_IMMEDIATE) {
   //執行前置操作,進行限流
   this.concurrencyThrottle.beforeAccess();
   //執行完執行緒任務,會執行後置操作concurrencyThrottle.afterAccess(),配合進行限流
   doExecute(new ConcurrencyThrottlingRunnable(taskToUse));
  }
  else {
   doExecute(taskToUse);
  }
}

2.SimpleAsyncTaskExecutor限流實現

首先任務進來,會迴圈判斷當前執行執行緒數是否超過concurrencyLimit,如果超了,則當前執行緒呼叫wait方法,釋放monitor物件鎖,進入等待

protected void beforeAccess() {
	if (this.concurrencyLimit == NO_CONCURRENCY) {
		throw new IllegalStateException(
				"Currently no invocations allowed - concurrency limit set to NO_CONCURRENCY");
	}
	if (this.concurrencyLimit > 0) {
		boolean debug = logger.isDebugEnabled();
		synchronized (this.monitor) {
			boolean interrupted = false;
			while (this.concurrencyCount >= this.concurrencyLimit) {
				if (interrupted) {
					throw new IllegalStateException("Thread was interrupted while waiting for invocation access," +
							"but concurrency limit still does not allow for entering");
				}
				if (debug) {
					logger.debug("Concurrency count " + this.concurrencyCount +
							" has reached limit " + this.concurrencyLimit + " - blocking");
				}
				try {
					this.monitor.wait();
				}
				catch (InterruptedException ex) {
					// Re-interrupt current thread,to allow other threads to react.
					Thread.currentThread().interrupt();
					interrupted = true;
				}
			}
			if (debug) {
				logger.debug("Entering throttle at concurrency count " + this.concurrencyCount);
			}
			this.concurrencyCount++;
		}
	}
}

2.SimpleAsyncTaskExecutor限流實現:首先任務進來,會迴圈判斷當前執行執行緒數是否超過concurrencyLimit,如果超了,則當前執行緒呼叫wait方法,釋放monitor物件鎖,進入等待狀態。

protected void beforeAccess() {
	if (this.concurrencyLimit == NO_CONCURRENCY) {
		throw new IllegalStateException(
				"Currently no invocations allowed - concurrency limit set to NO_CONCURRENCY");
	}
	if (this.concurrencyLimit > 0) {
		boolean debug = logger.isDebugEnabled();
		synchronized (this.monitor) {
			boolean interrupted = false;
			while (this.concurrencyCount >= this.concurrencyLimit) {
				if (interrupted) {
					throw new IllegalStateException("Thread was interrupted while waiting for invocation access,to allow other threads to react.
					Thread.currentThread().interrupt();
					interrupted = true;
				}
			}
			if (debug) {
				logger.debug("Entering throttle at concurrency count " + this.concurrencyCount);
			}
			this.concurrencyCount++;
		}
	}
}

執行緒任務執行完畢後,當前執行執行緒數會減一,會呼叫monitor物件的notify方法,喚醒等待狀態下的執行緒,等待狀態下的執行緒會競爭monitor鎖,競爭到,會繼續執行執行緒任務。

protected void afterAccess() {
	if (this.concurrencyLimit >= 0) {
		synchronized (this.monitor) {
			this.concurrencyCount--;
			if (logger.isDebugEnabled()) {
				logger.debug("Returning from throttle at concurrency count " + this.concurrencyCount);
			}
			this.monitor.notify();
		}
	}
}

雖然看了原始碼瞭解了SimpleAsyncTaskExecutor有限流機制,實踐出真知,我們還是測試下:
一、測試未開啟限流機制下,我們啟動20個執行緒去呼叫非同步方法,檢視Java VisualVM工具如下:

springboot中@Async預設執行緒池導致OOM問題

二、測試開啟限流機制,開啟限流機制的程式碼如下:

@Configuration
@EnableAsync
public class AsyncCommonConfig extends AsyncConfigurerSupport {
  @Override
  public Executor getAsyncExecutor() {
    SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor();
    //設定允許同時執行的執行緒數為10
 executor.setConcurrencyLimit(10);
    return executor;
  }
}

同樣,我們啟動20個執行緒去呼叫非同步方法,檢視Java VisualVM工具如下:

springboot中@Async預設執行緒池導致OOM問題

通過上面驗證可知:
1.開啟限流情況下,能有效控制應用執行緒數
2.雖然可以有效控制執行緒數,但執行效率會降低,會出現主執行緒等待,執行緒競爭的情況。
3.限流機制適用於任務處理比較快的場景,對於應用處理時間比較慢的場景並不適用。==

最終解決辦法:
1.自定義執行緒池,使用LinkedBlockingQueue阻塞佇列來限定執行緒池的上限
2.定義拒絕策略,如果佇列滿了,則拒絕處理該任務,列印日誌,程式碼如下:

public class AsyncConfig implements AsyncConfigurer{
  private Logger logger = LogManager.getLogger();

  @Value("${thread.pool.corePoolSize:10}")
  private int corePoolSize;

  @Value("${thread.pool.maxPoolSize:20}")
  private int maxPoolSize;

  @Value("${thread.pool.keepAliveSeconds:4}")
  private int keepAliveSeconds;

  @Value("${thread.pool.queueCapacity:512}")
  private int queueCapacity;

  @Override
  public Executor getAsyncExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(corePoolSize);
    executor.setMaxPoolSize(maxPoolSize);
    executor.setKeepAliveSeconds(keepAliveSeconds);
    executor.setQueueCapacity(queueCapacity);
    executor.setRejectedExecutionHandler((Runnable r,ThreadPoolExecutor exe) -> {
        logger.warn("當前任務執行緒池佇列已滿.");
    });
    executor.initialize();
    return executor;
  }

  @Override
  public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
    return new AsyncUncaughtExceptionHandler() {
      @Override
      public void handleUncaughtException(Throwable ex,Method method,Object... params) {
        logger.error("執行緒池執行任務發生未知異常.",ex);
      }
    };
  }
}

到此這篇關於springboot中@Async預設執行緒池導致OOM問題的文章就介紹到這了,更多相關springboot @Async執行緒池導致OOM內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!