2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

1. はじめに

一般的な Web システムでは、「1クエリが概ね3秒で終わること」のような応答時間の目安がよく求められます。大阪の AP サーバから東京の Autonomous Database(以下 ADB)へアクセスするような、リージョンをまたぐ構成では往復遅延(RTT)がそのまま乗るため、この「3秒ルール」を守れるかは事前に見積もっておきたいところです。前回の記事で SQL*Net の SDU(1往復あたりのデータサイズ)を振っても経過時間は変わらず、往復回数を決めるフェッチ行数のほうが経過時間を決める、というところまで確かめています。

今回はその続きとして、「では大阪→東京で3秒ルールを守るには何を調整すればよいのか」を、経過時間を要素に分解し、支配的な項目を実測で切り分けました。経過時間を次のように分けて、それぞれの大きさを測ります。

経過時間 ≒ 接続確立(初回のみ) + サーバ実行 + 結果転送 + クライアント処理
                                              └ 往復数 × RTT + 転送バイト数 ÷ 帯域

大阪と東京の間の往復遅延(RTT)は概ね 10 ms を想定し、Linux の tc netem で人工的に遅延を注入して測っています。

今回の検証ゴール

ゴール 達成条件
往復数・サーバ実行・転送の内訳を数値化し、経過時間の支配項を特定する 各項の内訳が数値で出て、どこを削れば3秒ルールを守れるかが言える
ソケットバッファ(RECV_BUF_SIZE / SEND_BUF_SIZE)が大量フェッチを速くするか、SDU と対比する バッファ拡大で転送スループットが変わる/変わらないが相対比で出る
コネクションプールが mTLS 接続確立コストを経過時間からどれだけ減らすか 毎回接続とプールの経過時間差が遅延注入下で数値化される

結論を先に

  • 経過時間の支配項は往復数 × RTT で、フェッチ行数(arraysize / 行プリフェッチ)で往復数を決められる。10 万行を arraysize 100 で取ると RTT 10 ms 想定で約 10 秒かかり超過するが、arraysize を 1000 / 10000 に上げると約 1.1 秒 / 0.34 秒に短くなる
  • サーバ実行時間は10 万行スキャンで 56〜98 ms と RTT に関係なく一定で、往復数 × RTT(RTT 10 ms 想定で最大 10 秒級)に比べれば小さい
  • ソケットバッファ RECV_BUF_SIZE / SEND_BUF_SIZE は、python-oracledb と JDBC のどちらの thin ドライバでも接続記述子・接続プロパティのどちらから渡しても反映されなかった。実際の受信バッファは OS の TCP 受信自動チューニングが決める
  • OS の受信バッファをデフォルトより大きくしても、この経路では経過時間は変わらない。経過時間が変わるのは、受信バッファを小さく設定しすぎたときに遅くなる方向だけ
  • 毎回接続(mTLS ハンドシェイク)は1回あたり約 98 ms の固定コスト(暗号処理)に往復ぶんが乗り、RTT 14 ms で約 185 ms。ウォームなプールなら acquire は約 0.1 ms で、無視できる水準になる

2. 検証環境

項目
対象 DB OCI Autonomous Database(Oracle AI Database 26ai、ECPU 2、mTLS / TCPS / port 1522)
リージョン ap-tokyo-1
クライアント WSL2(Ubuntu、kernel 6.6.87.2-microsoft-standard-WSL2)
python ドライバ python-oracledb 4.0.1(thin モード)
JDBC ドライバ Oracle JDBC thin(ojdbc11.jar、SQLcl 同梱を再利用)
遅延注入 tc netem(eth0 の送出方向に delay を付与)
TCP 輻輳制御 cubic(net.ipv4.tcp_congestion_control。WSL2 では bbr は利用不可、reno / cubic のみ)
対象表 sdu_bench(50 万行、payload は 200 バイト固定、合計約 95 MB、id に主キー)

クライアント側は WSL2 上の Linux で計測しました。WSL2 には 16 コアを割り当てており、計測中の CPU 使用率は低い水準(1分間の load average 0.34)で、クライアント側の処理能力が計測のボトルネックになっていないことを確認しています。RTT は tc netem で送出方向に遅延を足して作ります。netem の delay は送出方向にだけ乗るため、注入した値ぶん往復遅延が増えます。実際に小さなクエリ(select :n from dual)の往復時間の中央値を毎回測り、注入後の RTT を確認しています。

大量フェッチ・ソケットバッファの比較で使う接続別名は、tnsnames.ora に次の3種類を用意しました(_high サービス基準)。

  • 上書きなし(OS / ドライバまかせ)
  • RECV_BUF_SIZE / SEND_BUF_SIZE = 262144(256 KB)
  • RECV_BUF_SIZE / SEND_BUF_SIZE = 4194304(4 MB)

想定する本番経路(大阪 AP → 東京 ADB)と、今回の計測経路(WSL2 のクライアントに netem で RTT を注入 → 東京 ADB)の対応は次のとおりです。RTT を横から注入することで、実際に大阪へ計測用のサーバを立てなくても、想定 RTT ぶんの遅延がある経路を再現しています。


3. 経過時間の内訳の測り方

経過時間を分解して測るために、次の値を1回のフェッチごとに取りました。

  • 往復数v$mystatSQL*Net roundtrips to/from client の差分(フェッチ前後で読む)
  • 転送バイト数v$mystatbytes sent via SQL*Net to client の差分
  • サーバ実行時間:対象クエリを実行した直後に v$sessionprev_sql_id から sql_id を取り、v$sqlelapsed_time(DB 内で使った時間)の差分を読む
  • クライアント側の経過時間execute と全件フェッチをそれぞれ計測

往復数・転送バイト数・サーバ実行時間は RTT に依存しない構造的な値です。これらを baseline(遅延なし)で取り、そこへ netem で RTT を足したときに経過時間がどう増えるかを見れば、往復数 × RTT の項を引き算で取り出せます。RTT を足しても変わらない部分(サーバ実行・クライアント処理・帯域が十分なときの転送)は baseline の値がそのまま下限になります。

ソケットバッファについては、接続中のソケットの実バッファサイズを WSL 側の ss -tmi で読み、接続別名を変えたときに実バッファが変わるかを確認しました。

各パターンは、遅延なし(baseline)・netem 5 ms・netem 10 ms の3条件それぞれで3回以上ずつ測っています(遅延の切り替えは条件ごとに1回で、条件内の繰り返し測定中は同じ遅延のまま測っています)。


4. ベンチマーク設計

3つのゴールに合わせて、次のクエリ形状とパラメータを比較します。

代表クエリ(想定する業務のレンジをはさむ3形状)

形状 クエリ 行数 想定業務
1件参照 where id = :x(主キー) 1 OLTP の主キー参照
ページ where id between :a and :b(主キー範囲) 1,000 API / 画面1ページ
大量 全表 rownum <= :n 100,000 エクスポート / バッチ

フェッチ行数(arraysize、行プリフェッチ)は 100 / 1000 / 10000 を振ります。RTT は baseline(遅延なし)/netem 5 ms/netem 10 ms の3点で、往復数と経過時間の関係が直線になるかを見ます。python-oracledb の arraysize に当たるのは JDBC では Statement.setFetchSize() で、未設定のままだと 26ai の JDBC thin ドライバが行プリフェッチを 4〜250 行の範囲で自動調整します。今回の計測は python-oracledb で行いますが、フェッチ行数が往復数を決め、往復数が経過時間を決めるという関係は、前回の記事で JDBC でも同じ傾向であることを確認済みです。

ソケットバッファは、接続別名(上書きなし/256 KB/4 MB)と、OS 側の受信自動チューニング上限(tcp_rmem の最大値)を組み合わせて、大量フェッチのスループットを比べます。

接続コストは、毎回接続(接続 → 1件参照 → 切断)とウォームなプール(acquire → 1件参照 → release)を、同じ RTT スイープで比べます。


5. 計測結果

5.1. 往復数が経過時間を決める

まず3形状の構造的な値(RTT に依存しない)と、RTT を振ったときの経過時間です。

形状 arraysize 行数 往復数 転送 サーバ実行 経過 baseline 経過 netem5 経過 netem10
1件参照 1 1 5 0.0 MB 0.6 ms 0.007 s 0.018 s 0.027 s
ページ 100 1,000 13 0.2 MB 2.9 ms 0.043 s 0.102 s 0.157 s
ページ 1000 1,000 3 0.2 MB 1.0 ms 0.009 s 0.021 s 0.029 s
大量 100 100,000 1,002 19.8 MB 98.0 ms 3.911 s 9.336 s 14.193 s
大量 1000 100,000 102 19.7 MB 64.8 ms 0.505 s 1.069 s 1.572 s
大量 10000 100,000 12 19.7 MB 55.9 ms 0.252 s 0.318 s 0.396 s

小さなクエリの往復時間の中央値は baseline 3.79 ms、netem5 で 9.43 ms、netem10 で 14.15 ms でした。

大量フェッチを arraysize で振ったときの経過時間を、arraysize 100(往復が一番多い)を 1.00 とした相対比で見ると、往復数の比とほぼ一致します。

arraysize 往復数 経過 netem10 経過比(arr=100 を 1.00)
100 1,002 14.193 s 1.00
1000 102 1.572 s 0.111
10000 12 0.396 s 0.028

経過時間は往復数 × RTT で直線に増える

経過時間が本当に 往復数 × RTT で増えているかを確かめるため、baseline から netem10 への経過時間の増分を RTT の増分で割った傾きを、実際の往復数と並べました。傾きが往復数と一致すれば、増えたぶんはすべて往復遅延だと言えます。

形状 arraysize 往復数(実測) 傾き(経過時間増分 ÷ RTT 増分)
大量 100 1,002 993 0.99
大量 1000 102 103 1.01
大量 10000 12 13.8 1.15

RTT ごとの経過時間をグラフにすると、直線関係がそのまま見えます。arraysize 100(往復 1,002 回)だけが 3秒ルールの線を大きく超え、RTT が伸びるほど傾きも急になっています。arraysize 1000 / 10000 は同じ RTT の範囲でもほぼ横ばいです。

経過時間は往復数×RTTで直線に増える。arraysize 100は3秒ルールを大きく超え、1000/10000はほぼ横ばい

往復数が多い大量フェッチでは、傾きが往復数とほぼ一致しました(比が 0.99〜1.15)。つまり経過時間の増分は往復遅延そのもので、SDU や転送でなく往復数が経過時間を決めています。

3秒ルールに当てはめる

上の直線モデルに、大阪→東京の想定 RTT 10 ms を入れて経過時間を見積もると、3秒を超えるのは10 万行を arraysize 100 で取るパターンだけでした。

形状 arraysize 往復数 サーバ実行 往復数 × 10 ms 見積り経過時間 3秒以内
1件参照 1 5 0.6 ms 0.05 s 0.02 s
ページ 1000 3 1.0 ms 0.03 s 0.02 s
大量 100 1,002 98.0 ms 10.02 s 10.08 s 超過
大量 1000 102 64.8 ms 1.02 s 1.15 s
大量 10000 12 55.9 ms 0.12 s 0.34 s

サーバ実行時間は10 万行スキャンでも 56〜98 ms で、arraysize を変えても RTT を足しても変わりません。表の「往復数 × 10 ms」の列と比べると、arraysize 100(往復 1,002 回)では往復数×RTT が 10.02 秒と、サーバ実行時間より2桁大きく、経過時間のほとんどを往復数×RTT が占めます。3秒を守るために触るのはフェッチ行数(arraysize / 行プリフェッチ)で、これで往復数を減らすのが要点です。

なお、baseline では 20 MB の転送をしても経過時間は 往復数 × RTT でほぼ説明でき、転送時間は経過時間に表れませんでした。帯域が十分だと、各フェッチ往復の中にデータのかたまりが収まってしまうためです。転送やバッファの影響を見るには帯域を絞る必要があり、これが次の 5.2 章の主題です。

5.2. ソケットバッファは thin では反映されず、OS が決める

前回 SDU が効かなかったのと同じく、今回の狙いは「ソケットバッファ(RECV_BUF_SIZE / SEND_BUF_SIZE)を帯域遅延積に合わせて上げると、遅延下の大量フェッチが速くなるか」でした。まず、接続別名でバッファ指定を変えたときに、実際のソケットバッファが変わるかを ss -tmi で確認します。

RECV_BUF_SIZE / SEND_BUF_SIZE を指定しても実バッファは同じ

100k 行フェッチの前後で、ソケットの受信バッファ(rb)と送信バッファ(tb)を読みました。上書きなしと 4 MB 指定を並べても、値は同じでした。

上書きなし : 接続直後 rb=131072  tb=87040 → フェッチ後 rb=6291456  tb=87040
4MB 指定  : 接続直後 rb=131072  tb=87040 → フェッチ後 rb=4538914  tb=87040

接続直後はどちらも rb=131072 / tb=87040(OS のデフォルト)で、指定した 4 MB は現れません。フェッチが進むと受信バッファは数 MB まで育ちますが、これは OS の TCP 受信自動チューニングによるもので、指定値とは無関係です。送信バッファは終始 87040 のままで、SEND_BUF_SIZE も反映されていません。net.core.rmem_max を 208 KB から 8 MB に上げても、この挙動は変わりませんでした(フェッチ後の rb が 208 KB を超えていることが、上限でなく自動チューニングが働いている証拠です)。

JDBC でも同じで、記述子・プロパティのどちらでも反映されない

別実装のドライバでも同じか、Oracle JDBC thin でも確かめました。tnsnames の記述子(RECV_BUF_SIZE)でも、接続プロパティ(oracle.net.RECV_BUF_SIZE)でも、接続直後の受信バッファは 131072 のまま、送信バッファは 87040 のままでした。JDBC でもソケットバッファ指定は反映されません。

渡し方 接続直後 rb / tb 反映されたか
python-oracledb thin(記述子) 131072 / 87040 されない
JDBC thin(記述子) 131072 / 87040 されない
JDBC thin(接続プロパティ、rmem_max 8 MB) 131072 / 87040 されない

実装の異なる2つの thin ドライバが、2通りの渡し方すべてで同じ挙動になりました。thin モードでは RECV_BUF_SIZE / SEND_BUF_SIZE は反映されず、実際のバッファは OS が決める、と言えます。

実バッファを決めるのは OS の受信自動チューニング上限

では実バッファを決める OS 側の値を振ってみます。TCP 受信自動チューニングの上限(tcp_rmem の最大値)を 6 MB(デフォルト)/256 KB/64 KB にして、netem 10 ms 下で 100k 行フェッチのスループットを測りました。デフォルトの 6 MB を 1.00 とした相対比です。

tcp_rmem 上限 実受信バッファ スループット netem10 比(6 MB を 1.00) フェッチ時間
6 MB(デフォルト) 6,291,456 41.0 MB/s 1.00 0.49 s
256 KB 262,144 10.7 MB/s 0.26 1.85 s
64 KB 65,536 2.0 MB/s 0.049 9.6 s

受信バッファを小さくするとスループットが落ちました。スループットは概ね「受信バッファ ÷ RTT」で決まるためで、64 KB まで絞ると RTT 14 ms 下で 2 MB/s まで落ち、20 MB の転送に 9.6 秒かかります。接続別名(上書きなし/4 MB)はこのどの行でも結果が同じで、スループットを決めるのは OS の tcp_rmem 上限であって Oracle Net のバッファ指定ではありませんでした。

デフォルトより大きくしても速くならない

では逆に、OS の受信バッファ上限をデフォルトより大きくすれば速くなるのか。全表 95 MB フェッチで tcp_rmem 上限を 6 MB と 32 MB にし、netem 10 ms と 100 ms で比べました。

遅延 RTT tcp_rmem 上限 スループット フェッチ後 rb
netem10 約 14 ms 6 MB 49 MB/s 6.3 MB
netem10 約 14 ms 32 MB 49 MB/s 10〜11 MB
netem100 約 200 ms 6 MB 8.9 MB/s 6.3 MB(上限に張り付き)
netem100 約 200 ms 32 MB 7〜12 MB/s 13〜22 MB

32 MB に上げると受信バッファは実際に 6 MB を超えて育ちましたが、スループットは改善しませんでした。RTT 200 ms でデフォルトの 6 MB が上限に張り付いていても、上限を上げて速くはなりません。受信バッファが動作点より小さいとき(5.2 章で 64 KB に絞ったとき)だけ遅くなる方向に働き、デフォルトの自動チューニングはすでに帯域遅延積を満たしているため、大きくしても経過時間は変わりませんでした。RTT 200 ms で頭打ちになるのは受信バッファでなく別の要因(この環境の輻輳制御アルゴリズムである cubic の特性やサーバ送信側)と考えられます。

RTT 200 ms のケースは netem のキュー設定で人工的なパケットロスが混じり得るため、これは「帯域も遅延も大きい経路で大きなバッファで速くなるか」を厳密に測ったものではありません。大阪→東京(RTT 約 10 ms、帯域遅延積は 1 MB 未満)では、そもそも受信バッファは問題になりません。

Windows でも同じ

クライアントを Windows にしても同じかを、Windows 11 上のネイティブ python-oracledb thin で確かめました(Windows 11 は Windows Server 2022 と同世代の TCP 受信自動チューニングを持ちます)。

項目
Receive Window Auto-Tuning normal(デフォルト。Linux 同様に受信ウィンドウを自動調整)
Windows → ADB の RTT 3.33 ms
全表 95 MB フェッチ 63〜75 MB/s

Windows も受信ウィンドウを自動調整し、スループットは WSL とほぼ同じでした(baseline で約 73 MB/s)。Windows でも受信バッファは自動チューニングで足り、手動でのバッファ調整は要りません。

5.3. 接続確立コストとプール

最後に、毎回接続(mTLS ハンドシェイクをそのつど行う)と、ウォームなプール(acquire で使い回す)を比べました。1件参照を実行するときの、接続部分と全体の経過時間です。

RTT 毎回接続 connect 毎回接続 全体 プール acquire プール 全体
3.57 ms 120.2 ms 132.1 ms 0.10 ms 7.4 ms
8.55 ms 149.3 ms 170.1 ms 0.20 ms 18.2 ms
14.02 ms 185.0 ms 214.8 ms 0.10 ms 28.5 ms

毎回接続の connect 時間を RTT で見ると、RTT が 0 に近いところでも約 98 ms かかり、そこへ RTT ぶんが乗ります。増分を RTT で割ると傾きは 6.2 で、mTLS ハンドシェイクと認証に約 6 往復かかっている計算です。RTT に依存しない約 98 ms は、暗号化された鍵ファイルの復号・TLS の鍵交換・認証の計算ぶんです。

一方プールの acquire は RTT によらず約 0.1 ms で、ハンドシェイクが起きません。RTT 14 ms のとき、毎回接続の1件参照は約 215 ms、プールは約 28 ms で、プールが1回あたり約 186 ms を減らしました。


6. 役割整理

今回の実測をふまえ、3秒ルールに対する各設定の役割を整理します。

設定 働くレイヤー 経過時間への影響 触るべきか
SDU(前回) SQL*Net の1往復あたりのサイズ 往復数を変えないので経過時間は変わらない 触らない
フェッチ行数(arraysize / 行プリフェッチ) クライアントの取得単位 往復数を決める支配項 これを上げて往復数を減らす
RECV_BUF_SIZE / SEND_BUF_SIZE Oracle Net の記述子 thin では反映されない 触らない
OS の受信バッファ(tcp_rmem) OS の TCP スタック 小さすぎると遅くなる。デフォルトで十分 自動チューニングに任せる
コネクションプール 接続の使い回し 接続確立の約 100〜185 ms を無視できる水準まで減らす プールを使う(現代の前提)

3秒ルールを守るうえで、大阪→東京で経過時間を決めるのはフェッチ行数です。ソケットバッファとプールは、デフォルトの自動チューニングと標準的なプール利用で足りていて、触って速くなるものではありませんでした。

  • 経過時間の増分は往復数 × RTT で直線に増え、傾きが往復数と一致した(比 0.99〜1.15)。3秒を守る要はフェッチ行数で往復数を減らすこと
  • RECV_BUF_SIZE / SEND_BUF_SIZE は python-oracledb thin と JDBC thin のどちらでも、記述子・接続プロパティのどちらから渡しても、ソケットの実バッファに反映されなかった
  • python-oracledb の thin モードは Oracle Client ライブラリ(C 実装)を介さず、python-oracledb 自身が Oracle Net プロトコルを直接実装してソケットを扱います(公式ドキュメント)。RECV_BUF_SIZE は Oracle Net の C クライアント層(thick モード)側の設定であり、thin モードにはこの解釈機構自体が無いために反映されないと考えられます(thick モードは本記事の実測の範囲外)
  • フェッチ時間の増分は往復遅延そのものであり、クライアント側の計算が経過時間の制約になっているわけではない(傾きが往復数と一致することが、時間が往復待ちで決まっている裏付け)

前回の SDU と合わせると、「クライアント側で指定する Oracle Net の値(SDU、RECV_BUF_SIZE)は経過時間を決めず、経過時間を決める設定はその下(OS の TCP スタック)か、フェッチ行数のようにクライアントの取得単位にある」という同じ整理になりました。


7. まとめ

ポイント 理由
3秒を守る要はフェッチ行数で往復数を減らすこと 経過時間は往復数 × RTT で直線に増え、傾きが往復数と一致した
10 万行は arraysize 100 だと RTT 10 ms で約 10 秒、1000 / 10000 なら約 1.1 秒 / 0.34 秒 往復数が 1002 → 102 → 12 に減る
サーバ実行は 56〜98 ms で RTT に関係なく一定 往復数 × RTT(大量フェッチで最大 10 秒級)に比べれば小さい
RECV_BUF_SIZE は thin では反映されない python / JDBC の2ドライバ、記述子・プロパティの2経路すべてで実バッファが変わらなかった
ソケットバッファは自動チューニングに任せる 小さすぎると遅くなるが、デフォルトで帯域遅延積を満たす。大きくしても速くならない
接続はプールを使う 毎回接続は約 98 ms の暗号コストに約 6 往復が乗る。プールの acquire は約 0.1 ms

大阪→東京で1クエリ3秒を守るときにまず見るのはフェッチ行数で、ソケットバッファやプールはデフォルト・標準のままで足ります。ソケットバッファの thick モードでの挙動は本記事の実測の範囲外です。

参考

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?