最近線上發現一個現象,應用例項剛剛啟動的時候,開始接收請求之後發生了一小段時間的請求阻塞,從 HTTP Servlet 請求佇列監控上可以看出(基於 spring-web 的普通阻塞的 HTTP 伺服器是有 HTTP 執行緒池的,當執行緒是滿了之後,請求在阻塞佇列中等待處理。基於 spring-webflux 的沒有這個現象,但是考慮的背壓問題其實和這個場景類似):
然後阻塞數量很快就下去了,通過 JFR 發現和 Spring 各種懶載入的 Bean,各種資源或者連線池初始化等有關。這些資源可以理解為是懶載入的,是在請求真正用到的時候才會初始化。這些資源初始化之前,微服務就已經註冊到註冊中心並開始接受請求了。這樣在平常業務低峰釋出的時候,是沒有問題的,因為這些資源的初始化耗時也就在幾十到幾百毫秒之間。但是在業務高峰需要動態擴容的時候,就會受一些影響,因為請求壓力會立刻大量打到這些新啟動的例項上,這種情況下,初始化耗時的影響就比較大了。
所以,我們希望在微服務開始真正提供服務之前,將這些比較耗時的需要初始化的資源提前初始化完成之後,再告訴註冊中心我們可以開始接受處理請求了。
Spring Boot 中的 MVC Servlet 與 Web Service Servlet 的提前初始化
在微服務例項啟動後,我們傳送第一個請求的時候,會看到類似於下面的日誌:
INFO: Initializing Servlet 'dispatcherServlet'
INFO: Initializing Spring DispatcherServlet 'dispatcherServlet'
這是在初始化 Servlet。
MVC Servlet 指的就是 Spring-MVC 的 Servlet,其實就是提供 HTTP 服務的一些核心 Servlet,例如最核心的 org.springframework.web.servlet.DispatcherServlet
。這些預設是懶載入的,需要通過下面的配置開啟:
spring.mvc.servlet.load-on-startup: 1
除了 MVC Servlet,另一些 Servlet 可能是提供除了 HTTP 以外的其他應用協議的 Servlet,這些 Servlet 預設也是懶載入的,需要通過下面的配置開啟:
spring.webservices.servlet.load-on-startup: 1
例如 WebSocket 的 org.springframework.ws.transport.http.MessageDispatcherServlet
就是通過這個配置進行初始化的。
spring-data-redis 連線池的初始化
我們專案中使用的是 spring-data-redis + lettuce。如果沒有使用 Pipeline 等需要獨佔連線的 redis 操作,其實不用使用連線池。但是我們在使用中為了效率,儘量都是用 Pipeline,所以我們都啟用了連線池配置。其連線池實現基於 common-pools2 庫。相關配置如下所示:
spring.redis.lettuce.pool.enabled
: 是否啟用連線池,如果依賴中有 common-pools2 依賴自動會啟用。一般這個配置是用來關閉連線池的。spring.redis.lettuce.pool.max-active
: 連線池最大連線數量,預設是 8spring.redis.lettuce.pool.max-idle
:連線池中最多保留的空閒連線數量,預設是 8,這個配置需要和spring.redis.lettuce.pool.time-between-eviction-runs
一起配置才可以生效。spring.redis.lettuce.pool.max-wait
:從連線池獲取連線最大的等待時間(毫秒),超過這個等待時間則會丟擲異常,預設是 -1,即不超時spring.redis.lettuce.pool.min-idle
:連線池中最小的空閒連線數量,預設是 0,這個配置需要和spring.redis.lettuce.pool.time-between-eviction-runs
一起配置才可以生效。spring.redis.lettuce.pool.time-between-eviction-runs
:common-pools 中 Evictor 初始執行延遲以及執行間隔。不配置的話,就沒有啟用 Evictor 任務。
這個配置經常有人會誤用,只配置了 spring.redis.lettuce.pool.min-idle
但是沒有配置 spring.redis.lettuce.pool.time-between-eviction-runs
,導致 Evictor 任務沒有啟動,導致並沒有初始化最小連線數量的連線。
Evictor 任務包括將池中空閒的超過 spring.redis.lettuce.pool.max-idle
配置數量的物件,進行過期,以及空閒物件不足 spring.redis.lettuce.pool.min-idle
時,建立物件補足:
public void run() {
final ClassLoader savedClassLoader =
Thread.currentThread().getContextClassLoader();
try {
if (factoryClassLoader != null) {
// Set the class loader for the factory
final ClassLoader cl = factoryClassLoader.get();
if (cl == null) {
// The pool has been dereferenced and the class loader
// GC'd. Cancel this timer so the pool can be GC'd as
// well.
cancel();
return;
}
Thread.currentThread().setContextClassLoader(cl);
}
// Evict from the pool
try {
evict();
} catch(final Exception e) {
swallowException(e);
} catch(final OutOfMemoryError oome) {
// Log problem but give evictor thread a chance to continue
// in case error is recoverable
oome.printStackTrace(System.err);
}
// Re-create idle instances.
try {
ensureMinIdle();
} catch (final Exception e) {
swallowException(e);
}
} finally {
// Restore the previous CCL
Thread.currentThread().setContextClassLoader(savedClassLoader);
}
}
對於這種連線池,最好初始化足夠的連線數量,即配置 spring.redis.lettuce.pool.min-idle
以及 spring.redis.lettuce.pool.time-between-eviction-runs
。由於我們的專案中大量使用了 Pipeline,執行緒會獨佔一個連線進行操作。所以初始化的連線數量最好等於執行緒池的數量,在我們專案中即 Http Servlet 執行緒池的數量。
資料庫連線池的初始化
這裡以 druid 連線池為例子,druid 連線池底層其實也是類似於 common-pools 的實現,但是配置設計的更全面複雜一些。可以直接配置 initialSize:
DruidDataSource dataSource = new DruidDataSource();
dataSource.setInitialSize(50);
dataSource.setMaxActive(50);
我們設定初始連線池大小就是最大連線數
微信搜尋“我的程式設計喵”關注公眾號,每日一刷,輕鬆提升技術,斬獲各種offer: