記錄一次系統效能調優過程
問題回顧
線上上環境,由於業務場景需要,要求程式能夠在普通的4G機器中依然正常執行。 而原來的環境配置為8核16G,微服務部署,一共有6個功能模組。而現在要求在一臺4核4G的裝置上正常執行。
問題清單
-
- 模組合併過程中各種衝突,各種Bean無法正常載入
-
- 事件處理效能原來每秒3000~1w左右,現在突然驟降至幾百左右;
-
- 事件存在丟失現象,而且丟失比較嚴重
-
- 發現系統cache一直在不斷的上漲,free -m 後發現可餘記憶體幾乎用沒了(剩餘100M左右,其實就差不多是用完了,不會再降低)
問題排查
1. 程式碼衝突
- 包名衝突。不同模組的包名設計上有重複
- 類名衝突。
@Configuration @Bean @Controller @Service @Repository
2. 事件處理效能慢
現有的處理流程如下:
專案採用SpringBoot構建,引入 spring-boot-stater-redis
1. 通過HTTP接收到非同步事件,儲存到Redis;
2. 儲存的同時,將事件通過Redis的釋出訂閱傳送到不同的處理單元進行處理;
3. 每個事件處理單元通過Redis訂閱,然後處理事件;
4. 起一個定時器,每秒鐘從Redis中查詢一個時間視窗的事件,構建索引,然後bulkIndex到ES
複製程式碼
2.1 問題發現
1. Redis的訂閱釋出,內部會維護一個container執行緒,此執行緒會一直存在;
2. 每次訂閱,都會產生一個新的字首為RedisListeningContainer-的執行緒處理;
3. 通過jvisualvm.exe 檢視執行緒數,該類執行緒數一直在飆升
複製程式碼
2.2 問題定位
2.2.1 Redis訂閱釋出問題
程式中的實現如下:
@Bean
RedisMessageListenerContainer manageContainer(
RedisConnectionFactory factory,MessageListener listener) {
RedisMessageListenerContainer manageContainer =
new RedisMessageListenerContainer ();
manageContainer.setConnectionFactory(factory);
// manageContainer.setTaskExecutor();
}
複製程式碼
程式碼中被註釋掉的那一行,實際程式碼中是沒有該行的,也就是沒有設定taskExecutor
- 查詢了spring-redis.xsd中關於
listener-container
的說明,預設的task-executor
和subscription-task-executor
使用的是SimpleAsyncTaskExecutor
。 - 在原始碼中的位置
RedisMessageListenerContainer.class
...
protected TaskExecutor createDefaultTaskExecutor() {
String threadNamePrefix = (beanName != null ? beanName + "-" :
DEFAULT_THREAD_NAME_PREFIX) ;
return new SimpleAsyncTaskExecutor(threadNamePrefix);
}
...
複製程式碼
SimpleAsyncTaskExecutor.class
...
protected void doExecute(Runnable task) {
Thread thread =
(this.threadFactory != null
? this.threadFactory,newThread(task)
: createThread(task));
thread.start();
}
...
複製程式碼
-
SimpleAsyncTaskExecutor
的execute()方法,是很無恥的new Thread()
,呼叫thread.start()
來執行任務
2.2.2 事件處理都是耗時操作,造成執行緒數越來越多,甚至OOM
2.3 問題解決
找到問題的產生原因,主要的解決思路有三種:
-
配置
manageContainer.setTaskExecutor();
然後選擇自己建立的執行緒池; -
去掉一部分發布訂閱,改用
Spring
提供的觀察者模式,將絕大部分事件處理的場景,通過此方式完成釋出。SpringUtils.getApplicationContext() .publihEvent(newEventOperation(eventList));
-
採用
Rector
模式實現事件的非同步高效處理;
建立了2個執行緒組(參考netty的底層實現):
1. 一個用於處理事件接收 “event-recv-executor-”
coreSize = N * 2,CPU密集型
2. 一個用於事件的非同步處理 “event-task-executor-”
coreSize = N / 0.1,IO密集型
複製程式碼
事件處理邏輯
@Override
public void onApplicationEvent (EventOperation event) {
eventTaskExecutor.execute(() -> {
doDealEventOperation(event);
});
}
複製程式碼
3. 事件丟失
現有的處理流程如下:
專案採用SpringBoot構建,引入 spring-boot-stater-redis
1. 後臺維護了一個定時器,每秒鐘從Redis中查詢一個時間視窗的事件
複製程式碼
3.1 問題發現
在後臺定位日誌輸出,正常情況下,應該是每秒鐘執行一次定時,
但實際是,系統並不保證一定能每隔1S執行一次,
由於系統中執行緒比較多,CPU的切換頻繁,
導致定時有可能1S執行幾次或者每隔幾秒執行一次
複製程式碼
3.2 問題定位
3.2.1 定時任務不可靠
由於定時並無法保證執行,而定時任務獲取事件時,是按照時間視窗擷取,
通過redisTemplate.opsForZSet().rangeByScore(key,minScore,maxScore)實現,
勢必會造成有資料無法被載入到程式中,而一直儲存在Redis中,無法獲取,也無法刪除
複製程式碼
3.3 問題解決
找到問題的產生原因,主要的解決思路有兩種:
-
加大容錯率,將時間視窗拉大,原來是相隔1S的時間視窗,修改為相隔1MIN 【治標不治本,極端情況下,仍有可能造成該問題】;
-
採用MQ消費,此方法需要額外部署MQ伺服器,在叢集配置高的情況下,可以採用,在配置低的機器下不合適;
-
採用阻塞佇列,利用
Lock.newCondition()
或者最普通的網路監聽模式while()
都可以;
本次問題中採用的是第三種形式。起一個單獨的執行緒,阻塞監聽。
1. 事件接收後,直接塞到一個BlockingQueue中;
2. 當BlockingQueue有資料時,While迴圈不阻塞,逐條讀取佇列中的資訊;
3. 每隔1000條資料,或者每隔1S,將資料寫入ES,並分發其他處理流程
複製程式碼
4. 系統cache一直在不斷的上漲
在4G的機器下,發現經過一段時間的發包處理後,系統cache增長的非常快,最後幾近於全部佔滿:
大概每秒鐘10M的漲幅
複製程式碼
4.1 問題發現
1. 因為對於ES的瞭解,插入資料時,先寫快取,後fsync到磁碟上,因此懷疑ES可能存在問題;
2. 專案中日誌使用log4j2不當:
* 日誌輸出過多,
* 日誌沒有加判斷:if (log.isInfoEnabled())
* 日誌檔案append過大,沒有按照大小切分等(本專案此問題之前已解決)
複製程式碼
4.2 問題定位
4.2.1 ES插入機制問題
經過隔段分析,將有可能出現問題的地方,分別遮蔽後,進行測試。
最終定位到,在ES批量寫入資料時,才會出現cache大量增長的現象
複製程式碼
4.3 問題解決
用命令檢視記憶體free -m
,
-
buffer
: 作為buffer cache
的記憶體,是塊裝置的讀寫緩衝區 -
cached
表示page cache的記憶體
和檔案系統的cache
- 如果 cached 的值很大,說明cache住的檔案數很多
ES操作資料的底層機制:
資料寫入時,ES記憶體緩慢上升,是因為小檔案過多(ES本身會在index時候建立大量的小檔案),linux dentry
和 inode cache
會增加。
可以參考:ES記憶體持續上升問題定位
本問題其實並沒有完全解決,只是在一定程度上用效能換取快取。
-
- 修改系統引數,提高slab記憶體釋放的優先順序:
echo 10000 > /proc/sys/vm/vfs_cache_pressure;
複製程式碼
-
- 修改ES配置引數
## 這些引數是之前優化的
threadpool.bulk.type: fixed
threadpool.bulk.min: 10
threadpool.bulk.max: 10
threadpool.bulk.queue_size: 2000
threadpool.index.type: fixed
threadpool.index.size: 100
threadpool.index.queue_size: 1000
index.max_result_window: 1000000
index.query.bool.max_clause_count: 1024000
# 以下的引數為本次優化中新增的:
# 設定ES最大快取資料條數和快取失效時間
index.cache.field.max_size: 20000
index.cache.field.expire: 1m
# 當記憶體不足時,對查詢結果資料快取進行回收
index.cache.field.type: soft
# 當記憶體達到一定比例時,觸發GC。預設為JVM的70%[記憶體使用最大值]
#indices.breaker.total.limit: 70%
# 用於fielddata快取的記憶體數量,
# 主要用於當使用排序操作時,ES會將一些熱點資料載入到記憶體中來提供客戶端訪問
indices.fielddata.cache.expire: 20m
indices.fielddata.cache.size: 10%
# 一個節點索引緩衝區的大小[max 預設無限制]
#indices.memory.index_buffer_size: 10%
#indices.memory.min_index_buffer_size: 48M
#indices.memory.max_index_buffer_size: 100M
# 執行資料過濾時的資料快取,預設為10%
#indices.cache.filter.size: 10%
#indices.cache.filter.expire: 20m
# 當tranlog的大小達到此值時,會進行一次flush操作,預設是512M
index.translog.flush_threshold_size: 100m
# 在指定時間間隔內如果沒有進行進行flush操作,會進行一次強制的flush操作,預設是30分鐘
index.translog.flush_threshold_period: 1m
# 多長時間進行一次的磁碟操作,預設是5S
index.gateway.local.sync: 1s
複製程式碼
歷程回顧
- 對於本次調優過程,其主要修改方向還是程式碼,即程式碼的使用不當,或者考慮不周導致
- 其次,對於ES的底層實現機制並不很熟悉,定位到具體的問題所在;
- 本次優化過程中,涉及到對GC的定位,對Linux系統底層引數的配置等
- 由於日誌傳輸採用HTTP,故每次傳輸都是新的執行緒。IO開銷比較大,後續會考慮替換成長連線。
附:
如果文中有描述失誤內容,或者沒有描述清楚的,可以將問題發我郵箱,[email protected]
,如果有其他問題,也可以聯絡我,大家一起共同討論。
- 願大家共同進步,共同成長。