1. 程式人生 > 程式設計 >記錄一次系統效能調優過程

記錄一次系統效能調優過程

問題回顧


線上上環境,由於業務場景需要,要求程式能夠在普通的4G機器中依然正常執行。 而原來的環境配置為8核16G,微服務部署,一共有6個功能模組。而現在要求在一臺4核4G的裝置上正常執行。

問題清單


    1. 模組合併過程中各種衝突,各種Bean無法正常載入
    1. 事件處理效能原來每秒3000~1w左右,現在突然驟降至幾百左右;
    1. 事件存在丟失現象,而且丟失比較嚴重
    1. 發現系統cache一直在不斷的上漲,free -m 後發現可餘記憶體幾乎用沒了(剩餘100M左右,其實就差不多是用完了,不會再降低)

問題排查

1. 程式碼衝突


  • 包名衝突。不同模組的包名設計上有重複
  • 類名衝突。@Configuration @Bean @Controller @Service @Repository
    等註解中沒有指定Bean例項的名稱。

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-executorsubscription-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 dentryinode cache會增加。 可以參考:ES記憶體持續上升問題定位

本問題其實並沒有完全解決,只是在一定程度上用效能換取快取。

    1. 修改系統引數,提高slab記憶體釋放的優先順序:
echo 10000 > /proc/sys/vm/vfs_cache_pressure;
複製程式碼
    1. 修改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],如果有其他問題,也可以聯絡我,大家一起共同討論。

  • 願大家共同進步,共同成長。