問題
工作中需要同步一些數據,大概接近百萬條的量級,更新時間非常慢,需要7個多小時,更新的頻率是每周一次。隨着數據量的一步步增加,時間也越來越多,逐漸成為一個風險因子,於是想到要嘗試做一些優化,降低同步時間。
分析
經過調查,需要同步的是TABLE_A,同步的過程可以簡化表述為兩步:
- Call API_B to get updated value.
- Update records in DB.
首先,檢查log,看看這兩個過程分別耗時多少。分析生產環境的log,發現,對於一條數據,
- API call costs: ~350ms.
- DB update costs: ~20ms.
做一個計算,100萬條數據,假設每條同步需要0.4s,那么總耗時就是100h,和實際的6h不符。經過思考后解釋是:還需要考慮線程和冗余數據。
實際使用了一個大小為10的線程池,100h除以10的話,就是10h,再算上一些冗余的不需要更新的數據(估計20%左右),理論計算時間和實際時間是在一個數量級上的。
所以,可以從兩個方面着手優化,一是優化調用API的時間,二是優化DB寫數據的時間。
解決 - API
分析API調用的代碼,看到是一條一條調用的,使用了Object callAPI(Object input)
這個函數。首先,做一個壓力測試,看下callAPI()
這個方法每秒可以處理多少請求。
ExecutorService executor = Executors.newFixedThreadPool(100);
for (int i=0; i<100; i++) {
executor.submit(() -> log.info(callAPI(input)));
}
開100個線程,每個線程請求100次,總計10000個request。根據總耗時,計算出callAPI()
可以處理請求約50個/秒。這個速度顯然已經滿足不了我們的系統了。
聯系上游API開發部門,經過協商,短期來看,可以為我們配置單獨的機器來提高生產環境的query速度。從長遠來看,
- 需要聯合開發一個batch query的接口,即
List<Object> callBatchAPI(List<Object> inputs)
,減少調用的次數,節約中間環節的開銷。 - 精簡輸出Object的參數。原先對象內有30+個參數,有些字段還很長。但是並不是每個參數都有變化的。改進后,輸出只需要傳回變化的字段就可以了,減少網絡傳輸的時間。
解決 - DB
不難猜想到,DB也是一條一條調用更新的。優化的策略是batch update。
以下是模擬測試DB update的部分,單次更新:
@Test
public void updateOneByOne() {
// initialize
Environment.initialize("qa", "test_batch_update");
System.out.println("INI DONE, start count time.");
// update
long start = System.currentTimeMillis();
for (int i=0; i<recordNum; i++) {
jdbcTemplate.update(UPDATE_SQL_WITH_PARAM, "SP1_DUMMY_DESC1", i);
}
long end = System.currentTimeMillis();
// count time
String timeElapsed = DurationFormatUtils.formatPeriod(start, end, "H:mm:ss");
System.out.println("ACTION DONE, updateOneByOne, elapsed: " + timeElapsed);
}
批量更新:
@Test
public void updateByBatch() {
Environment.initialize("qa", "test_batch_update");
System.out.println("INI DONE, start count time.");
// prep obj
List<DummyObj> dummyObjList = new ArrayList<>();
for (int i=0; i<recordNum; i++) {
dummyObjList.add(new DummyObj(i, "BP2_DUMMY_NAME", "BP2_DUMMY_DESC1", "BP2_DUMMY_DESC2", "BP2_DUMMY_DESC3", "BP2_DUMMY_DESC4", "BP2_DUMMY_DESC5"));
}
// update
long start = System.currentTimeMillis();
this.batchUpdateObj(dummyObjList);
long end = System.currentTimeMillis();
// count time
String timeElapsed = DurationFormatUtils.formatPeriod(start, end, "H:mm:ss");
System.out.println("ACTION DONE, updateByBatch, time elapsed: " + timeElapsed);
}
private int[] batchUpdateObj(List<DummyObj> dummyObjList) {
int [] updateCounts = testBatchUpdateJdbcTemplate.batchUpdate(
UPDATE_SQL_WITH_PARAM,
new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement preparedStatement, int i) throws SQLException {
preparedStatement.setString(1, dummyObjList.get(i).desc1);
preparedStatement.setInt(2, dummyObjList.get(i).id);
}
@Override
public int getBatchSize() {
return dummyObjList.size();
}
}
);
return updateCounts;
}
Spring XML config:
<import resource="classpath:/datasource.xml" />
<bean id="batchUpdateTest" class="com.nomura.unity.udw.util.BatchUpdateTest">
<property name="jdbcTemplate" ref="jdbcTemplate"/>
</bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<constructor-arg name="dataSource" ref="dataSource"></constructor-arg>
</bean>
測試的結果是:對於500條數據,單次更新耗時370s,批量更新耗時9s。批量更新快了約40倍。
這里的一個坑是:不同數據庫,比如生產環境v.s.開發環境,有可能性能表現是不一樣的。比如上面的(簡化后的)例子是在開發環境下的測試結果,1條數據單次更新大概耗時1.3s,而在生產環境的實際數據是20ms。這時候做優化需要找准baseline,不然搞半天發現用了新方法竟然還比生產環境的舊方法慢,其實是開發的數據庫本身相對更慢而已。
總結
最終,解決的方案是:一方面,聯系上游API部門開發新接口。另一方面,使用batch update優化DB寫入。
題外話
在解決這個問題的過程中,還看了許多其它的文章,有一篇談到了Oracle改進sql,update之前先做一個排序,讓數據盡可能靠近。(Oracle大數據量更新方法)
另外,還看了一篇,講到了修改rewriteBatchedStatements
參數,解決Spring jdbcTemplate batch update無效的問題。(jdbcTemplate.batchUpdate批量執行性能差解決)
雖然最終看下來和本題無關,但還是挺有收獲的。