10万件のUPDATEをマルチスレッドで分割した場合、実行時間にどのような影響があるか。実際にやってみる。
環境
準備
下記のテーブルに10万件追加し、このテーブルに対してUPDATEを1件ずつかけていく。実行時間の計測開始前にテーブルのDROP&CREATE&10万件INSERT&バッファキャッシュクリア、を行う。
DROP TABLE MUTLITHREAD_UPDATE_EXPR PURGE; CREATE TABLE MUTLITHREAD_UPDATE_EXPR( KEY_COLUMN INT , VALUE_COLUMN VARCHAR2(20) , CONSTRAINT MUTLITHREAD_UPDATE_EXPR_PK PRIMARY KEY (KEY_COLUMN) ); INSERT /*+ APPEND */ INTO MUTLITHREAD_UPDATE_EXPR(KEY_COLUMN, VALUE_COLUMN) SELECT ROWNUM , '00000000001111111111' FROM (SELECT ROWNUM FROM all_catalog WHERE ROWNUM <= 1000) , (SELECT ROWNUM FROM all_catalog WHERE ROWNUM <= 100); COMMIT; ALTER SYSTEM FLUSH BUFFER_CACHE;
ソースコード
UPDATEはOracleとは別のマシンのJavaから実行する。計測用のプログラムはスレッド数を変更できるようにしておく。例えばスレッド数4の場合各スレッドは、00001〜25000、25001〜50000、50001〜75000、75001〜100000の行をUPDATEする。コミットは各スレッドごとに1回。
package oraclestudy.multithreadupdate; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.CyclicBarrier; public class UpdateThread implements Runnable { private int start; private int size; private CyclicBarrier barrier; public UpdateThread(int start, int size, CyclicBarrier barrier) { this.start = start; this.size = size; this.barrier = barrier; } @Override public void run() { final String sqlStr = "UPDATE MUTLITHREAD_UPDATE_EXPR SET VALUE_COLUMN = '22222222223333333333' WHERE KEY_COLUMN = ?"; try (Connection connection = DriverManager.getConnection( "jdbc:oracle:thin:@//192.168.0.20:1521/ORCL.kagamihogex61", "kagamihoge", "a"); PreparedStatement sql = connection.prepareStatement(sqlStr);) { connection.setAutoCommit(false); final int n = start + size; for (int i = start; i < n; i++) { sql.setInt(1, i); sql.executeUpdate(); } connection.commit(); } catch (SQLException e) { e.printStackTrace(); } try { barrier.await(); } catch (InterruptedException | BrokenBarrierException e) { e.printStackTrace(); } } }
実行時間を計測するためのクラス。バリアを用意して各スレッドにawait
させることで、全スレッドの開始から終了までの実行時間を計測する。
package oraclestudy.multithreadupdate; import java.util.concurrent.CyclicBarrier; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class Main { public static void main(String[] args) { final int UPDATE_SIZE = 100_000; final int THREAD_SIZE = 200; final int UPDATE_PER_THREAD_SIZE = UPDATE_SIZE / THREAD_SIZE; ExecutorService threadPool = Executors.newFixedThreadPool(THREAD_SIZE); final long start = System.currentTimeMillis(); CyclicBarrier barrier = new CyclicBarrier(THREAD_SIZE, new Runnable() { public void run() { long end = System.currentTimeMillis(); System.out.println("time:" + (end - start)); } }); for (int i = 0; i < THREAD_SIZE; ++i) { int startNum = i * UPDATE_PER_THREAD_SIZE + 1; threadPool.submit(new UpdateThread(startNum, UPDATE_PER_THREAD_SIZE, barrier)); } } }
実行結果
スレッド数 | 1 | 2 | 3 | AVG |
---|---|---|---|---|
1 | 59806 | 60821 | 60181 | 60269 |
2 | 44107 | 42645 | 43252 | 43334 |
4 | 34609 | 37034 | 37517 | 36386 |
10 | 29214 | 30360 | 28886 | 29486 |
100 | 19282 | 19761 | 17664 | 18902 |
200 | 19713 | 19988 | 19919 | 19873 |
グラフにするとこんな感じ。なお200は除外。
どっかで見たような線の描き方になる。
感想とか
Oracle側のマシンでtop
を見ていると、スレッド数を増やすごとにCPUの使用率が上がっていく。そのため、スレッド数を増やすと実行時間が改善されるのは、各種I/Oなどで待ちとなっていたCPU時間を使えるようになったから、と推測できる。
しかし、スレッドを増やせば増やすほど改善されるかというと、そうでもない。この環境だと10ちょいくらいを越えても、スレッド増やしたほどには改善しないであろうことがグラフから読み取れる。スレッド数200も試してみたがCPU使用率60%くらいが精々だったので、おそらくネットワークなりディスクなりのI/Oがボトルネックとなり、馬鹿みたいに大量のスレッドがあったところで遊んでるプロセスが出てきている、と思われる。
以上のことを考えると、RAC環境であれば各ノードに重ならない範囲のUDPATEを割り振れば早くなるかもしれない。
ただまぁ結局は環境に依存するので、スレッド数がいくつが最適なのか、とかは分からないけれど。
ソースコード
https://github.com/kagamihoge/oraclestudy/tree/master/src/main/java/oraclestudy/multithreadupdate