レベルエンター山本大のブログ

面白いプログラミング教育を若い人たちに

信じられないDB文化「固定長DB」でもあうんです。大規模コンシューマ向けサービスのRDB設計

ずいぶん時間があいてしまったけど、大規模コンシューマ向けサービスRDB設計の続き。


僕はこのプロジェクトを自分のRDBの知識を使って革新してやろうと思って臨んだ。

しかし結果として逆に、コンシューマ向けサービスに最適化されたRDBの使い方について教わることになった。


※ あと、KVSでいいじゃんって言ってる人もいるけど、それはKVS導入の苦労を知らない人だと思う。KVSの苦労は後で書く。

僕らが最近手がけているのは、とても大規模なコンシューマ向けサービスだ。
100万人の契約ユーザが使い、1テーブルに1億レコード以上のデータを貯め、24時間止めることが許されず、
要求から応答までのターンアラウンドタイムが1秒以内という厳しいSLAのサービスである。
中でも僕はDBやフレームワークの設計とアーキテクトっぽいことを担当している。

僕がこの現場に来て、驚愕した文化が2つある
それは「Join禁止」と「固定長DB」だ。
ありえない。
とはいえ、正直に言えば「またか、、、」という感想でもある。
RDBを知らないレガシーな人たちが設計したDBではよくありがちな設計だからだ。
と僕は早々にこの文化と戦って、絶対に覆してやろうと考えてた。
過去の経験上それはたやすいハズだった。

信じられないDB文化「Join禁止」に「固定長DB」、、でも、合うんです。大規模コンシューマ向けサービスのRDB設計 - 山本大の日記

はじまり

僕らの関わっていたシステムのDBは、カラムの型として固定長(CHARとDATE)しか許されない。
これについても、僕は参画当初、RDBの常識から考えて大いにバカバカしく感じていて、憤慨して絶対に覆してやろうと考えていた。


結論としては冒頭にも述べたとおり、Join禁止の時と似た結末となってしまった。
つまり固定長カラムしか使ってはいけないというポリシーを覆す事は出来なかったのだ。


これについて語るには前提知識として「Oracleのブロック」と「行移行・行連鎖」を理解してもらう必要がある。

Oralceのブロックとは

ブロックとは、Oracleがディスクにアクセスするときの最小単位だ。
ブロックサイズは、Oracleの初期設定時に変更できるが、その後変更できない。
データベースのレコード(行)は、ブロック単位に格納される。

行移行/行連鎖とは

行移行も行連鎖も、パフォーマンスを下げる要因として知られた、RDBの設計注意点だ。


どちらの事象も1レコードが複数の「ブロック」に分かれて格納されるのだが、発生メカニズムが異なる。


とにかくDBのパフォーマンスチューニングは、いかにディスクアクセスを減らすかがポイントだから
1レコードを読み込むのに2ブロックや3ブロック読むのでは効率が悪い。
大量データを扱っている場合には、この読取ブロック数をいかに少なくするかに神経を尖らせるのだ。

行連鎖とは

行連鎖とは1レコードのサイズが、そもそもOracleの読み取りの単位(1ブロック)のサイズを超えている場合に発生する。
たとえば1ブロックのサイズを8000バイトにしている場合には、9000バイト分挿入しようとすると1000バイトが余るので、Oracleは個のレコードの格納にもう1ブロック使う。そのため1レコードが、ディスク上は2ブロックに分かれて格納される。


こうなると1レコードを読み書きする時に2回のディスクアクセスが発生するから遅くなる。


この行連鎖の予防は比較的簡単で、ブロックサイズを超えるようなテーブル定義にしなければよい。
つまり上記の場合は、1テーブルのカラムサイズの合計が8000バイトを下回るようにすればよい。

行移行とは

次に、行移行。

行移行こそが、可変長の型を使っている時に発生する問題である

行移行の事象も行連鎖と同じで、1レコードが複数ブロックに格納されてしまうことだ。
しかし、メカニズムはちょっと異なる。


レコードを登録する時に、カラムを初め1バイトの文字だけで登録していたとする。
そして、Updateでそのカラムのデータを4000バイトまで増やしたとしよう。


そのカラムが、可変長型(VARCHARやNUMBERなど)であれば増えたデータは別ブロックに格納されるのだ。


こういった話、下記の本を読むと凄くよくわかる。行移行・行連鎖に対処する一般的な設計なんかも書いてる。*1

この本の著者さん、1日だけ僕らのプロジェクトにヘルプで来てくれた。印象深い人だった。

絵で見てわかるOracleの仕組み (DB Magazine SELECTION)

絵で見てわかるOracleの仕組み (DB Magazine SELECTION)

さて、


この行移行を阻止するために、CHARとDATEという固定長を使うというのが、我がプロジェクトのルールだった。
たしかに、VARCHARやNUMBERといった可変長型を使わなければデータの伸長はおきないが、それ以外に多大なる問題(開発効率を含め)を引きおこすように思える。


固定長DBで想定しうるデメリット

プロジェクト参画当初、このルールには僕は絶対反対を言い張った。
固定長にすることで、以下のようなデメリットをもたらすことは容易に考えられたからだ。
・Trimが必要になる。
・数値型はソートに対応するために0埋めをしなければならない。(0のTrimも必要)
・集合計算(SUMなど)には、CONVERTが必要でパフォーマンス悪化をもたらす。
・バッファキャッシュヒット率が低くなる。
・ディスクが高くつく。


これらを検証しながら、いろんな人たちとミーティングを重ね、どうにかこのルールを覆そうと奔走した。


しかし、僕の常識を越えた検討の末のルールであることがだんだんわかってきた。

コンシューマ向けサービスに最適化したDBとは

僕はそもそも一般的な企業システム向けのデータベースの使い方を基本として考えていたところがあった。


しかしコンシューマ向けサービスのRDBの使い方として重要なポイントは
コンシューマ向けサービスでは、データの検索・操作・集計の範囲が広範囲になることはない。または設計上で回避出来る
というところだ。
ほとんどのDBアクセスは、1ユーザの情報を読み書きするだけだ。


僕はコンシューマ向けサービスに最適化したデータベースを考えていなかった。


企業システムなら、帳票や集計こそがRDBの威力の見せ所だが、コンシューマ向けサービスは、他のユーザと集計して嬉しい部分はさほど多くはない。
SUMなどのGROUP関数を使うことはほとんどないのである。


だからそもそもこのプロジェクトではRDBの集計関数を原則禁止している。
プロジェクトの別のルールによって、DBへのCPU負荷をかけることを極力禁止しており、
集計などが発生するにあたっても「Java側でループして集計する」か「設計上、集計が発生しないよう考慮する」のが鉄則なのだ。

RDBに考えさせないポリシー

議論に入る前から、僕も「RDBに考えさせずJavaで考える」ポリシーは理にかなっていると思っていた。


なぜなら、DBサーバーは基本的にスケールアウト(サーバー増設する)が出来ないからだ。

DBがスケールする仕組みも考えてはいたのだけれど、巨大なサービスになることがわかっていたから、DBサーバに負荷をかけないにこしたことはない。


JavaEEサーバーであれば、負荷分散のためにスケールアウトすることが比較的容易であるが、DBサーバーは基本的にスケールアウトできない。*2

苦労するなら初期構築、運用に苦労を回すべきではない

Trimや0埋めなどはDBアクセスフレームワークで吸収できる範囲であるといわれた。
たしかに、パフォーマンス劣化に伴う運用/保守コストに比べれば、初期構築のコストは問題にならない。
というより、問題にされなかった。
なぜなら初期構築費用は一括でドカンと支払われるため、その辺の微細な実装コストはお客さんは誰も気にしないのだ。
そういうことで、僕があてにしていた固定長にすることでのCPUコストでは、このプロジェクトのポリシーを覆す程の問題提起にできなかったのだ。

敗北を決心した理由(苦労を運用に回さないとは)

僕が折れた決め手は運用に関することだ。
運用時には、パフォーマンスがらみの様々なトラブルが発生する。
そこに問題になるかもしれない箇所が1箇所増えるだけで
問題の切り分けや、対処に要する時間は数倍に膨れあがる。

「もしかしたら行移行が発生しているのでは?」

この疑問が、可変長を使っている限り、ことあるごとに誰かの頭の中に登場するだろう。
そしてその検証をしなければならなくなる。パフォーマンス問題のたびにだ。


無駄なスペースがDBに保存されるってことを割り切るだけで、運用時のトラブルが幾つかでも回避出来る。
僕は負けることにした。それは運用後正解だったと確信した。

そして運用へ

そんなこんなで、語り尽くせぬ程の苦労を中略するが、システムは無事にサービスインを迎えた。
今では運用開始してから1年数ヶ月が過ぎすっかり安定したが、サービスインしてから1年ほどは
本当にRDBチューニングの限界に挑戦している感じだった。


読取ブロック数をできるだけ下げる設計にしておいたことが、運用に入ってからとても救われた。


100万人のユーザが毎日毎日いろんな動きをしてくれる。
需要予測に従って設計したつもりが、需要予測無視で傾向が変わる。


上記の固定長DBポリシーに従わなかったら、いろんなところでもっと沢山の障害を踏んでいたかもしれない。
障害切り分け〜復旧にはより時間がかかっていただろう。

結論

コンシューマ向けサービスでRDBを使うのであれば、RDBの負荷をどれだけ下げられるかがとても重要なポイントだ。

これには以下のようなポイントで設計から考慮する必要がある。
・サービス設計上、ユーザをまたがるような処理をしないこと。
・DBに頭を使わせるのではなく、Webアプリケーションに頭を使わせること。
・ユーザアクセスやユーザ動向は常に変化すると考えて設計すること
・できるだけ運用に苦労を回さないこと
・パフォーマンス問題は必ず発生するから、要因をできるだけ減らしておくこと。


こんな話も今は昔

最近は、分散KVS(NoSQLという呼び名はあまり好きじゃない)などもしっかり導入実績のあるものが出てきて、この記事に書いたプロジェクトでも、それらを導入しているのだが、2年前の時点では、分散KVSの導入を検討する段階になかった。


しかし、そもそもKVSでは厳密なトランザクションを管理する部分では、RDBに及ばないと感じる。
だからRDBが必要な領域はまだまだ存在する。


そういうことで分散KVSの導入においても、導入検討にあたってはめちゃくちゃ苦労するんだけど、それは別の話。

あわせて読んでほしい

興味があれば、以前に書いた「Join禁止」のポリシーについても一読ください。

*1:行移行・行連鎖を発生させない仕組みとして普通はPCTFREEやPCTUSEDを使ってデータブロックに「遊び」の部分を設計するのが常識だ。しかしながら、サービスの初期構築でこの設計が適切に(明確な根拠を持って)行える者がどこにいるだろうか。サービスの使われ方、ユーザ動向といったものはどれだけ綿密に需要予測をやっていても崩れるものだ。現にそういう事例を運用してから嫌というほど見てきた。もしくは、アプリケーション設計で回避という考慮も可能だろう、しかしこれもまた難しい問題でカラムの最適な桁数を決めるのは本当に難しいことなのだ。結果として、利用しているAPIや外部システムやUIの桁数のうちで一番桁の大きなデータが入るように設計するようになる。こういうことだから、「PCTFREEやPCTUSEでデータがブロックからあふれない設計をします」と方針づけて設計したとしても根拠の薄い設計になってしまうんだ。僕らのお客さんにはそういう説明での設計は許されなかった。

*2:OracleRACを使えばいいという選択肢はあるのだが、お客さんの苦い経験上RACの選択肢ははなからないし、僕もRACに苦しめられるのは好きじゃない。