一次大量數據更新的性能優化調研


問題

工作中需要同步一些數據,大概接近百萬條的量級,更新時間非常慢,需要7個多小時,更新的頻率是每周一次。隨着數據量的一步步增加,時間也越來越多,逐漸成為一個風險因子,於是想到要嘗試做一些優化,降低同步時間。

分析

經過調查,需要同步的是TABLE_A,同步的過程可以簡化表述為兩步:

  1. Call API_B to get updated value.
  2. 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批量執行性能差解決
雖然最終看下來和本題無關,但還是挺有收獲的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM