2005年6月の陽がさんさんと降り注ぐ日、私たちが2年に渡り取り組んだ新しい発注システムが本番環境で稼動するのを目の当たりにしたので、私たちの気分は高揚しました。私たちのパートナーは、私たちに発注を送り始め、モニタリングシステムも全て順調な動きを見せていました。1、2時間後、私たちのCOOが、それらの発注を新しいシステムに送信しなければならないということを知らせるために、戦略的なパートナーへEメールを出しました。5分後、1台のサーバーがダウンしました。1分後、さらに2つがダウンしました。そして、パートナーは電話をかけ始めました。私たちは、暫くの間太陽を見ることはないだろうということを理解しました。
戦略的なパートナーからの発注の収益性を高めるためのシステムは、崩壊しました。逆巻いているCOOは、戦略的パートナーに再度Eメールを送信し、今は旧システムに戻すように頼みました。奇妙なことに、私たちは予備のサーバーを用意していましたが、ごくわずかの戦略的な顧客からの注文だけを、サーバーに持ってくることができました。そのシステムは、通常のパートナーの多くを広くスケールすることができましたが、少数派の戦略的パートナーでさえ処理することができませんでした。
これは、私たちが何を誤っていたのか、それを直すために何をしたのか、そして、どのようにしてそれが全てうまくいったかという話です。
「ベストプラクティス」では足りない
私たちは、階層化アーキテクチャ、層に分かれた配置、分離したOLTPやOLAPデータベースといった、様々なベンダーが文書化したベストプラクティスを利用してシステムを設計しました。しかし、誰もが私たちのシステムが取り扱わなければならない異なる種類のスケールについて、私たちに説明しませんでした。2003年には、私たちはシステムの重要な側面であるパフォーマンスを設計し、2004年には負荷テストをおこなったので、私たちは全てをカバーしたと確信していました。
サーバーログを精査し、システムイベントをモニタリングした後で、私たちは、戦略的なパートナーからの注文が通常のパートナーからのそれとは大きく異なることが分かりました。通常のパートナーが一度に数百の品目を発注するところ、戦略的な顧客は、そのときに数十万のラインを送信していました。リクエストは、数百メガバイトにも及びました。メッセージインフラとORマッピングコードの両方で、そこまでの負荷テストをおこなっていませんでした。サーバーのコアは、それらのXMLをデシリアライズしようとかつてないほどに高熱になりました。単一のリクエスト処理は、1Gの半分のメモリで耐えられました。データーベースロックは、ミリ秒でなく数分待たされました。スレッドはタイムアウトし、ガーベージコレクタはそれら全てのメモリを回収することでおかしくなり、システムの有効性さえ損ないました。
私たちがおこなった最初のことは、パフォーマンステストの実験室でこれらのシナリオを再現することでした。私たちがテストを実行し、システムがクラッシュするたびに、私たちは信じられないまま観察しました。私は自身で考え続けました。「私たちは本にある全てをおこなった。この事態はどうだろう?」
実を言うと、そこは、私が本によって全てをするために私たちに時間と予算を実際に与えてくれて仕事をした始めての会社でした。私たちは、いかなる弁解もしませんでした。しかし、本にあることだけでは足りないとき、あなたはどうしますか?
様々なスケーラビリティ
結局、秒単位のリクエストだけがスケーラビリティの側面ではないということがわかります。私たちが身にしみて知った他の側面は、以下のとおりです。
- メッセージサイズ
- リクエスト毎のCPU利用率
- リクエスト毎のメモリ利用率
- リクエスト毎のIO(とネットワーク)利用率
- リクエスト毎の総処理時間
メッセージサイズは、その他全ての側面において大きな影響を及ぼすものと思われます。メッセージが大きくなるにつれ、それらをデシリアライズするためのCPU、結果のデータを保持するためのメモリー、データーベースとのやり取りをするデータを得るためのネットワークやIOがより一層必要となります。結局のところ、全体の処理時間に影響を及ぼします。しかし、パートナーの未決の発注全てを割引するといった小さなリクエストでさえ、処理すべきデータ量に影響を受けました。
私たちはすべてを調べましたが、それについて得られるものはありませんでした。大きいメッセージをより小さくすることができない限り、問題は解決しないでしょう。以下は、私たちがした会話の一部です。
Dan: 「バイナリのシリアライズは、より少ない数の戦略的な顧客に対しては有効であるように思います。」
Barry: 「いや、よくないです。その間には合計5つの非互換のプラットフォームがあります。」
Sasha: 「さらに、メモリやIOによくないでしょう。」
Me: 「圧縮はどうですか? メッセージ基盤にかかる負担が軽くて済むのではないでしょうか。」
Dan: 「CPUにより大きな負荷がかかるでしょう。」
Sasha: 「再度、メモリーとIOの作業を繰り返す必要があるでしょうか?」
Barry: 「リクエスト/レスポンスがうまくいかないようです」
Me: 「あなたは、私がパブリッシュ/サブスクライブをどれだけ好きかを知っているでしょう。しかし、私はそれがどう機能しうるのかを知りません。」
しかし、私たちがコアメッセージングパターンについてより深く調べ始めた結果、私たちは解決の方向に向かいました。
実世界はメッセージ指向
解決について最も私たちを驚かせたことは、それが通常パートナーと戦略的パートナーの両方に適していて、両方にとって大きなパフォーマンス改善となったことです。それだけでなく、それぞれの発注に対する応答時をより早くすることができ、在庫管理機能を改善することができました。それは、私たちがそのときに考慮さえしなかったものです。
その解決法は、非常にシンプルなものでした。それは、一つの「注文メッセージを生成する」代わりに、パートナーがたくさんの「注文メッセージ群」を送信できるようにしました。彼らが購入注文番号に対する全ての項目を完了したとき、「完成」フラグにtrueを設定し、「発注メッセージ」を送信します。それは、ステートフルなインタラクションです。
知ってのとおり、通常、パートナーは発注を送信するための調達部門を持っています。それらの発注は、「完成」するまでの間、時間と共に組み立てられ、私たちに送信されます。私たちの解決法には、パートナーの調達システムが(作成中の最終注文情報でない)部分的なものをメッセージ送信することを含んでいました。彼らは、既に送信した情報を変更したり発注の一部をキャンセルすることを、(既存のERPで管理されている)私たちのシステムにある発注番号を知らずにできます。実際、発注が完了したことを示しているメッセージを受信するまで、私たちは発注書のためのERPを呼び出すことさえしませんでした。
私たちが「発注メッセージ」を受信したとき、私たちは「発注状態でメッセージを変更する」ことで対処しました。それらのシステムが検討された適切な時間で応答を受信しなかったのなら、前回のメッセージを再送することができます。言い換えれば、私たちはメッセージのべき等を行ったということです。これは、製品 SKUで何かをしたいときはいつでも、パートナーは、与えられた製品SKUに対する全てのライン(様々なオプションや設定を含む)を再送信する必要があるということです。実際には、たくさんのデータではありません。
等冪メッセージは、システムによって複数回処理されたとしても、あたかも1回だけ処理したのと同じ効果を持つものです。
これは、パフォーマンスに大きな副作用をもたらしました。それは、メッセージが消失しないように継続的にメッセージを使用する必要がないということです。非常に大量なメッセージをディスクに常に書き込む代わりに、パートナーのシステムによって状態を管理できるように、私たちのアプリケーションプロトコルでそれを調整しました。最終的には、本当に少しだけ複雑さが増すだけです。
スキーマー変更によるスケーラビリティの効果
私たちが新しい「バージョン」の発注システム(誰もが、敢えてそれをリライトとは呼びません)における、戦略的パートナーと共同作業をおこなうにつれ、新しい「発注メッセージ」の期待されるサイズが、1000ライン品目の組であることが分かりました。それらについては、通常パートナーも同様です。メッセージサイズが減少したので、他の観点でもスケーラビリティの改善が見られました。CPU、IO、メモリー使用は、同じように全て減少しました。
リクエスト毎のリソース利用が低下すると、同じように待ち時間も低下しました。しかし、スループットについては予想よりも増加しました。巨大なメッセージが、より小さなリクエストによって使用される「ホギング」のリソースであったということです。時間とともに、データベースコネクションプール全体が、大きなメッセージを処理するスレッドによって占有されました。実際、「サービス妨害」のスレッドが小さなメッセージを処理しようとして、それらをタイムアウトにしていました。
データ量が増加し、メッセージサイズが減少が解決しなかったことによって、今なお影響を受けているシステム部分が他にありました。例えば、パートナーに対する未決定の注文割引や、オブジェクト/リレーショナル設計を利用せずに実装したようなリクエストです。ロジックを扱っているそのリクエストは、setベースのロジックの利用をよりよく表現していました。つまり、純粋なSQLです。全てのデータをメモリにロードする代わりに、それを繰り返しおこなうことで、どうにか変更し、最終的にデータベースにそれらを格納して、単純なSQLステートメントを利用することができました。
UPDATE PendingOrders SET Discount=@Discount WHERE PartnerId = @PartnerId
その効果は目を見張るものでした。より早いレスポンスタイムと安定性が高まったのです。
明確な状態管理のスケール
新しいバージョンと前のバージョンの設計で、最も顕著な違いの一つは、メッセージを扱っているロジックが「ステートフル」であるということです。それに対し、全てのベンダーが何かしらの警告をあげていました。Martin Fowler(リンク)の書き物は、データベースの中の状態を管理することで、システムのスケールを魔法のようにおこなえる訳ではないということを理解する手助けとなりました。それは、データベースをボトルネックにするだけで、データベースベンダーはより多くのライセンスを売ることができます。私たちの新しい設計は、複数メッセージの処理の状態を明確に扱いました。私たちは、ロジックと状態の連携を記述するために「saga(※1)」という言葉を使用しました。明確な状態管理によって、私たちはストレージに対する最も適切な技術を選択することができました。
(※1)長期トランザクション
通常パートナーに対して、私たちはたくさんの小さなオブジェクトを作り、高速で読み込むようにしました。しかし、それらのオブジェクトの生存期間はかなり短いので、インメモリの分散キャッシュ製品を利用することに決定し、それは高度に利用可能であると確信できるものでした。戦略的パートナーに対しては、状態が数百メガバイトにも増大する可能性があり、それは数週間から数ヶ月に渡って、ゆっくりと大きくなることもあります。そこで、最終的に私たちはデータベースを使用することにしました。しかし、それはOLTPデータベースのために利用されるSANではなく、直接ストレージをアクセスするオープンソースのデータベースでした。格納された発注データは、インターオペラビリティを構成することなく、バイナリシリアライゼーションのメリットを与えてくれるXMLとして格納する必要もありませんでした。
Saga - メリットとチャレンジ
新しいメッセージ規約によって、私たちのパートナーが同じ発注番号で多くのメッセージを送信することができるので、私たちのシステムは、既存の発注処理をしているsagaに関連付けられたメッセージや、生成された新しいsagaを必要としているものとを識別できるようにする必要がありました。そのようなものとして、私たちはパートナーIDと発注番号によってsagaの永続化メカニズムを問い合わせる方法が必要でした。この要件は、IDによる問い合わせを可能とするだけですが、いくつかの有名な分散キャッシュ技術を除外することになりました。それでも、いくつかのハイエンドソリューションは、ちょうど良いものでした。
「saga」という用語は、長期間存続するトランザクションの扱い方を記述するために、リレーショナルデータベースコミュニティによって1987年に作られました。Sagaは、原子性と分離性を捨て、プロセスを一連の複数でより小さなACIDトランザクションとして扱っています。
私たちのチームの何人かが、当初、新しいプログラミングモデルへ移行することについて心配していましたが、それが通常のメッセージ処理と実質同じであるということにすぐに気づきました。メッセージがやってくると、そのデータはあるストレージからオブジェクト(saga)を検索するのに使用され、メソッドはそのオブジェクト上で呼ばれ、オブジェクトの一部の状態が変化し、いくつかのメッセージが送信され、そしてオブジェクトはストレージに戻ってきます。唯一異なる点は、sagaが管理する状態は、マスターデータベースやERPのデータではなく、インタラクションの一時的なデータでした。sagaが、「完成」フラグがtrueの発注メッセージを受信すると、それが蓄えた全てのデータを入力しているERPシステムを呼び出し、発注状態が単に「受信」ではなく、たった今「受入」られたということをパートナーのシステムに通知するためにメッセージを送り返します。
開発が進むにつれ、私たちは、私たちが「発注メッセージ」を受信したときにパートナーのシステムにただ応答するだけでなく、企業の他のシステムが、たとえ完了する前だとしても発注について知ることに関心を持っているということを実感しました。従って、私たちは、関心を持っている全てに対し、「発注状態が変更されたメッセージ」をパブリッシュすることを始めました。
パブリッシュ/サブスクライブは、エンタープライズレベルのパフォーマンスを強化させる
興味を持った最初の開発は、達成しました。厳密に言えば、「急ぎの仕事」でした。その会社が最近取り組んでいた挑戦の一つは、急ぎの仕事を要求しているクライアントによりよいサービスを提供することでした。そして、その問題は最終的に解決しました。知ってのとおり、私たちが全ての製品を時間通りに準備することができたとしても、正しい転送手段が準備できたとしても、正しい量は分からないでしょう。いくつかの製品は冷凍して格納しておく必要があるでしょうし、他のものはエアクッションで包む必要があります。しかし、そうすることで、あなたがアイデアを得られたのです。作業をするために手元に十分な社員がいるだけでは、解決は困難でした。
現在では、完成したシステムで、注文が何時までに提供される必要があるか、何の製品が必要とされているのかを事前に知ることができ、彼らは手元に十分なスタッフをおいたり、冷凍状態で十分な量をトラックで運送したり、時間通りに完了させるために必要な全てのことを事前に計画しておくことができます。
在庫管理は、優位なものとなりました。つまり、注文を知っていることで、事前に在庫として保持することが可能な商品を用意することができました。供給元には必要に応じて早く配達するようにプレッシャーをかけます。
負荷テストの結果
先の失敗した立ち上げから、私たちが記録したメッセージ利用のペースによって新しいシステムを動かした後で、私たちは興味深いことを発見しました。
まず第一に、最初に私たちがテストを実行したときには、私たちのテストエンジニアたちは発注外となるような正しくないメッセージ基盤を構成していました。私たちはそのパフォーマンスに喜び、実際に全てが正しいかどうか、全てのログやデータを調べました。私たちは、べき等メッセージが何度も処理されていることを知っていましたが、順序の問題を考慮していませんでした。確認のために、私たちは別ラウンドのコードレビューとして、メッセージ順序に関する問題を重点的にチェックしました。最終的に、私たちは順序制約を緩和することができると確信する前に、より機能的なテストを行いました。
第二に、同じシナリオで何度も何度も繰り返しテストをしていたにも関わらず、応答時間がテストの期間に渡ってゆっくりと低下していたことが分かりました。後のシナリオのCPU利用が初期のものと同じであると気づいた後で、私たちの注意はデータベースのロックの問題へと向きました。私たちの1人がsaga テーブルが100万本以上の行を持つと気がついたとき、それをすぐに除外しました。私たちが頭を悩ませて互いを見合っていたので、誰かがブツブツと言いました。「完了時にsagaを消すのはどうだろうか?」そして、それを行いました。sagaの永続化メカニズムのための一つの小さなコードの変更によって、応答時間はきちんと安定するようになりました。
パートナーの視点
当然、システムが機能するためには、私たちのパートナーが自身のシステムをさらに変更する必要があります。私たちが自身の設計で前へ進むためのゴーサインを得る前に、多くのパートナーが技術的なレベルとCOOのレベルの両方で意見を求められたということが想像できると思います。私たちの通常のパートナーは、その変更によって少し腹が立ったでしょうが、それは「発注メッセージを生成する」が「発注メッセージ」という程度の変化です。
私たちの戦略的パートナーの技術チームが私たちのデモコードを見た後で、私たちは彼らの顔が明るくなるのを見ました。「私たちが巨大なメッセージを送信するのを望んでいたとは思っていないですよね? これで最終的にはメモリーとCPUの制約も節約できそうです。」彼らは、自社の中で様々な形に発注が移るものとしてそれらの発注状態を持つためにファイヤーウォールのポートを空ける準備もしていました。
製造への移行は、少し敏感でした。私たちのパートナーは、彼らのシステムの新旧バージョン両方が同じ時刻にオンラインでセットアップする必要がありました。私たちのCOOは、少しの中断にも関わらず、明らかにその作業中はピリピリとしていましたが、結果として成功しました。
私たちは、以前の状態の指標を全て見ました。CPU、メモリー、IO、息をひそめる前の数時間続いたデータベースロック。オペレーションと開発スタッフが、切迫した悲運を探してこれらの状況を監視し続けた数日から数週間の間でしたが、何もおきませんでした。レスポンス時間は、以前見たものより2倍早くなりました。3ヵ月後には、以前の発注システムを廃棄する作業を行いました。
2005年の最初の夏の日から約2年が経ちました。私たちの頭上を覆っていた雲は、ついに消え去りました。COOは、システムの技術的に驚くべき機能について特に気にすることなく、戦略的なパートナーからの利益が増加したことに対して非常に注目していました。
貴重な経験から学んだこと
このプロジェクトは、現在のベストプラクティスの多くに対して本当に厳しい試練でした。私たちは、ベンダーが提供した技術や製品を盲目的に信じて、火傷をしました。パフォーマンスやスケーラビリティは常に念頭においていたものの、大きなデータボリュームによるより深いシステムへの影響を考慮していませんでした。私たちの全体的なアジャイルな物の見方によって、「時期尚早な最適化は、すべての悪の根源です。」という部分的な真実だけを受け入れました。完全な引用は、以下の通りです。:
「私たちは、わずかな効率について忘れるべきです。時間の97%はそう言えます。時期尚早な最適化は、すべての悪の根源です。」
-- Tony Hoare氏
これらは「わずかな効率」ではありませんでしたし、以降の再設計も少しも「最適化」ではありませんでした。私たちのサービスの規約を変更し、ステートフルなインタラクションを導入することによって、私たちは、システムにおけるパフォーマンスの臨界状態を管理することができました。私たちは、制御レベルが可能であることや特定技術の選択によって、システムパフォーマンスのためだけでなく費用対効果の高いソリューションとなることを知りませんでした。
私にとっての重要な成果は次の通りです。スケーラビリティはブール値(真か偽)ではありません。並列ユーザーの数やサーバーのオンラインを超えて、スケーラビリティは多次元コストで作用します。あるCPU時間の要求や、ピーク、平均要求率、メッセージサイズ、フォーマット、リクエスト毎のメモリーのワーキングセットサイズ、リクエスト毎のCPU/IO利用率を考えたとき、与えられたソリューションコストはどのくらいでしょうか? 戦略的パートナーにとって意味のある技術選択は、通常パートナーにとって割りのいいものではありませんでした。常に結果をビジネスに聞いてください。計画された収益を曇らせるような費用があるのならば、それらは単にパフォーマンス要求の変更だけかもしれません。
参考
- sagaの設計と単体テスト(リンク)
- saga(PDF) - Garcia-Molina, セーレム 87
- べき等メッセージング(リンク)
- http://www.nServiceBus.com - sagaをサポートするインフラ
著者について
Udi Dahan氏は、ソフトウェア単純化志向の人で、誰もが欲しがるソリューションアーキテクチャのMost Valuable Professional賞でマイクロソフトに認められ、現在3年になります。Udi氏は、マイクロソフトのWCF, WF, Osloの関連技術アドバイザの仕事をしています。彼は、世界中のクライアントにサービス指向、スケーラブルでセキュアな .Netアーキテクチャ設計に特化した、トレーニング、メンタリング、ハイエンドコンサルティングサービスを提供しています。
Udi氏は、国際.NET協会のヨーロッパ広報局のメンバーであり、ソフトウェアアーキテクトの国際協会の起草者でありトレーナーでもあります。さらに、そのアーキテクトトレーニング委員会の創立メンバーです。彼は、Dr. DobbがスポンサーのWebサービス、SOA、XMLに関するエキスパートでもあります。
TechEd USA, SD Best Practices, DevTeach Canada, Prio Germany, Oredev Sweden, TechEd Barcelona, QCon London, TechEd Israelといった国際カンファレンスにも参加しています。Udi氏は、継続的なサービス、永続ドメインモデル、マルチスレッド、時にはクライアントといったミッションクリティカルなトピックについて詳細な部分までおさえています。
Udi氏とは、彼のブログwww.UdiDahan.comを通してコンタクトを取ることができます。
原文はこちらです:http://www.infoq.com/articles/scale-with-service-contracts
(このArticleは2008年4月10日に原文が掲載されました)