參與到一個專案中後,這個專案有一個非常棘手的問題,就是程式需要在初次啟動時載入大量資料,高達80G,這會導致成本高築,不得不使用裸金屬伺服器進行部署
上個版本現狀
tps:1790 rt:12.94ms 80G
目標是 tps 不下降的情況下,儘量使記憶體佔用下降
改進1
首次觀察載入資料,發現這個資料具有一定的規律,都是部門或者場地等的程式碼,重複率較高,每個字串出現次數平均80次;可以參考霍夫曼壓縮演算法的邏輯來處理,這裡
於是透過構建資料欄位生成Pair<String, Integer> 類似這樣的資料結構,左邊為字串,右邊為代表字串的數字
當前我們專案字串上限是60K個, 那麼(只這個特例)只要用兩個位元組的資料就可以表達出這個字串的Index(數字)
public class IndexArray {
private byte[] indexArray = new byte[0];
private final int UNIT;
public IndexArray(int size) {
if (size <= 255) {
UNIT = 1;
} else if (size <= 65535) {
UNIT = 2;
} else if (size <= 16777215) {
UNIT = 3;
} else {
UNIT = 4;
}
}
public void addIndex(int index) {
byte[] byteArray = new byte[UNIT];
for (int i = 0; i < UNIT; i++) {
byteArray[i] = (byte) ((index >> (i * 8)));
}
int srcBytesLength = this.indexArray.length;
byte[] mergedArray = new byte[srcBytesLength + byteArray.length];
System.arraycopy(this.indexArray, 0, mergedArray, 0, srcBytesLength);
System.arraycopy(byteArray, 0, mergedArray, srcBytesLength, byteArray.length);
this.indexArray = mergedArray;
}
}
這樣透過兩個位元組就表達了至少40個位元組的字串。
透過這個思路,使記憶體從80G 下降到了 17G
但是遇到了新的挑戰,就是tps 下降了,從tps:1700 下降到 1296
改進2
思考發現自己的定址演算法不是很好,是透過把Index的兩個位元組從位元組陣列中一個個取出,然後轉換成Integer,再來比較的方式處理的,相對耗時;
這裡改進為,將要用於比較的Index 轉換為兩個位元組的資料,在IndexArray上按兩個位元組滑動比較,效率更好
最佳化後
tps:1352 rt:17.35ms
效率上升10%
改進3
在這個介面的演算法邏輯中看到,這是個貪心演算法,由於請求中的引數較多,同時又是反覆取出比較,導致String 轉 Index 這個過程的演算法被反覆呼叫,經檢查這裡也是比較耗時
於是將這個轉化過程改進到進入核心演算法之前,提前轉換
最佳化後
tps: 1489 rt: 15.71ms
效率再上升10%
改進4
由於使用stopwatch列印關鍵步驟耗時過程中發現,在呼叫別的類或者本類中其他方法時,耗時不定但平均下來佔比很大;懷疑是java[CMS(Concurrent Mark Sweep)]記憶體回收導致stop the world的原因導致的耗時,而系統為大記憶體,計算密集型程式;選型使用G1作為垃圾回收器進行實驗
實驗引數:
-server -Xmx85g -Xms85g -Xmn1g -XX:+UseG1GC
-XX:+UnlockExperimentalVMOptions
-XX:MaxGCPauseMillis=100
-XX:InitiatingHeapOccupancyPercent=50
-XX:G1HeapRegionSize=16M
-XX:G1NewSizePercent=20
-XX:G1MaxNewSizePercent=30
-XX:G1ReservePercent=15
-XX:G1RSetUpdatingPauseTimePercent=5
-XX:ParallelGCThreads=16
-XX:ConcGCThreads=8
(引數含義可以查文件獲得)
效果比較顯著
tps:3369 rt:6.72ms
達到了最佳化目標