RDBMSでも大規模データをあきらめないためには

第6回ネットワークボトルネックの原因と解決策

クライアントとDBサーバ間のネットワークがボトルネックになる2つのケース

これまで、大規模データ処理RDBMSの2大ボトルネックであるI/OとCPUについて、発生の原因と改善策を紹介してきました。その2大巨頭に隠れて目立たないのですが、大規模データ処理を行う際には、I/OやCPUと同じくらいネットワークもボトルネックになりやすいものです。

ネットワークがボトルネックになるケースとしては、以下の2つがあります。

  • DBサーバとストレージ間のネットワークI/O
  • クライアントとDBサーバ間の結果セット転送

前者は第2回、第3回で紹介しているので、今回はクライアントとDBサーバ間のネットワークに焦点を当てて解説していきます。

大規模データ処理を行うRDBMSにおいて、クライアントとDBサーバ間のネットワークがボトルネックになる代表的なケースは下記の2つです。

ケース①
⇒ DBサーバがクライアントに転送するデータサイズが大きく、ネットワーク帯域が枯渇する
ケース②
⇒ クライアントとDBサーバ間のラウンドトリップ(やりとり)の回数が多い
図1 ネットワークがボトルネックになる2つのケース

図1 ネットワークがボトルネックになる2つのケース

「大規模データ処理」と聞くと、ケース①が問題になりやすそうですが、じつはケース②のせいで性能が出ないことが多くあります。

しかも、ケース①はネットワーク帯域の拡張(1GbEから10GbEへの変更)や、Oracle*Netの送受信バッファサイズ(SEND_BUFF_SIZEなど)の拡大などで性能を改善できるのですが、ケース②は抜本的な解決が困難であるとともに、⁠そもそもそれが原因であることに気づくことすら難しい」という非常に厄介な性質を持っています。

なぜネットワークラウンドトリップによるボトルネックは気づきにくいのか

「SQLの性能が出ないからチューニングして」と頼まれた場合、多くの方は、DBの性能情報をまず確認すると思います(私もそうです⁠⁠。

Oracle Databaseの場合だと、AWRレポートで待機イベントの発生状況やCPUの利用状況を「Top 5 Timed Foreground Events」セクションなどから確認し、

  • 「I/Oが遅いのかな?」
  • 「CPUが詰まったのかな?」

などの視点で、性能が出ていない原因を突き止めようとするでしょう。

これはまちがったアプローチではありません。しかし、ケース②で紹介したクライアントとDB間のネットワークラウンドトリップが原因の場合、DBの性能レポートではそれらを発見するのは非常に難しいです。

たとえば、100万行のデータを要求するSELECT文をクライアントが発行したとします。クライアントは、DBから送信されたデータをメモリ内にいったん保持した後に結果の演算や表示などを行う必要があるため、一度に100万件の結果すべてを受け取ることができません。そのため

  • 一度に100件などといった単位で結果を受け取る
  • 受け取り完了後、次の100件を再度DBに要求する

という動きになります。

したがって、Exadataのように高速なDBサーバが100万件の読み込みを1秒で完了できるとしても、上記のクライアント側の動作により、⁠SQLの実行完了に10分かかる」などということもあります。

図2 ラウンドトリップによるボトルネックが気づきにくい理由

図2 ラウンドトリップによるボトルネックが気づきにくい理由

結果セットの転送に時間がかかっているケース①の場合には、⁠SQL*Net more data to client」などのNetworkクラスの待機イベントで待つので、⁠Top 5 Timed Foreground Events」セクションで確認することができます。

しかし、ケース②のようにネットワーク上でのラウンドトリップでSQLの性能が出ない場合、DBはその間何も仕事をしていないように見えます。⁠問題はI/Oかな?CPUかな?」と思って性能レポートを見ても、なかなか手がかりが見つけられません。⁠Top 5 Timed Foreground Events」には、DBが何もしていないことを表す待機イベント(待機クラスがIdleの待機イベント)はリストされない(⁠⁠Foreground Wait Events」を確認する必要があります)ためです。

DBサーバがいかに速く大規模データを処理できても、クライアントとのネットワークラウンドトリップにより性能が出ないことがあることは理解いただけたかと思います。

ラウンドトリップ数を減らす2つの方法

では、どうすればラウンドトリップによるオーバーヘッドを軽減できるのでしょうか?

おもな解決策は下記の2つです。

  • 解決策① ⇒ 1回にフェッチする行数を増やす
  • 解決策② ⇒ クライアントに転送するデータ量を減らす

解決策① 1回にフェッチする行数を増やす

この解決策は、実装が容易である半面、効果も限定的ですが、数百万以上の結果セットをクライアントに返さなければならないシステムにおいては実装が必須となるテクニックです。

クライアントソフトがDBに対して1回に要求する(フェッチする)行数を増やすと、その分だけラウンドトリップ数が減ることになり、結果セットの転送時間が短くなることが期待できます。

たとえば、フェッチ行数が100行の場合、100万行の結果セットを転送するには、1万回のラウンドトリップが必要になりますが、フェッチ行数が1000行の場合、1000回のラウンドトリップで済むことになります。

このフェッチ行数は、JavaであればJDBCのprefetchの値を増やすことで変更できます。

ただし、無闇にフェッチ行数を増やせばいいわけではありません。フェッチ行数を増やした分だけ、クライアント側のメモリに一時的に蓄えられるデータが増えることになるので、使用されるメモリ領域とフェッチ行数の増加による性能改善効果を考慮しながら、最適な値を見つけなければなりません。

一般的には、100から200行前後の値が最適値となることが多いようです。そこをスタートポイントとして、いろいろと検証してみてください。

また、クライアントがJavaアプリケーションの場合、JDBCの「バッチ更新」という機能で、INSERT文やUPDATE文などをまとめてDBに送信することもできます。

結果セットの転送処理と同じく、更新処理用のSQL文も、ある程度まとめて一括送信することで、ラウンドトリップ数が減り、性能改善を図ることができます。

解決策② クライアントに転送するデータ量を減らす

この方法は、アプリケーションのロジックを改修する必要がありますが、おもにバッチ処理などの定型ジョブの性能を改善する場合には大きな効果があります。

バッチジョブにて「クライアントに転送するデータ量を減らす」とは、⁠処理ロジックを可能な限りDB側に配置する」ことを意味します。

たとえば、100万個の商品の値段を一律に1%増やす処理を考えてみましょう。この場合のロジックは「値段を1%アップさせる」になります。このとき、クライアントに100万件のデータを転送し、アプリ側で1%アップしたデータを生成して更新するよりも、DBに閉じて本処理を実行したほうが、結果セットをネットワークに転送する処理の分、バッチの時間を短縮することができます。

図3 処理の主体をどこに置くか

図3 処理の主体をどこに置くか

DB側に処理を移植する方法としては、以下の2つがあります。

  • 可能な限り1つのSQLで更新処理を行う
  • PL/SQLにより処理を行う

前者は、SUM()やROW_NUMBER()などの集計関数や分析関数、CASE文などを活用して、単一のSQLでロジックを表現する方法です。

後者については、⁠いまさら、PL/SQL?」と思われるかもしれません。しかし、Exadataのような高性能なDBサーバのI/OおよびCPUリソースを最大限活用するには、PL/SQLによってDBサーバへロジックを移植することでネットワークボトルネックを排除することが不可欠です。

現行のバッチ処理の高速化にはもちろんのこと、これから新規に開発するバッチ処理においても、DBサーバ側のリソースが潤沢である場合は処理ロジックをDBへ移植することを検討してみてください。

最終回となる次回は、CPUとI/Oのバランスについて説明しますのでお楽しみに!

おすすめ記事

記事・ニュース一覧