EIP / Problem Solving with Patterns


Case Study: Bond Pricing System

ここまでのあらすじ

とりあえず何とか動くシステムができあがったところ。

Problem Solving with Patterns

パターンは道具、そしてパターン集は道具箱のようなものだ。問題解決の助けになる。どうも、パターンは設計のときにしか使えないと思ってる人がいるらしい。道具箱のたとえでいうなら、それは、道具箱の道具が使えるのは新築のときだけで、改築のときには使えないと言っているようなものだ。ここからは、さっきまでと同様のパターンの探求プロセスを使って、稼働中のシステムの問題を解決していく。

Flashing Market Data Updates

新しいデータがやってきたら、テーブルのセルをフラッシュさせてほしい。更新があったことをわかりやすくするためだ。ただ問題があって、データの更新頻度がめちゃめちゃ高い。普通にやると、GUIスレッドのスタックがあふれてフリーズする。そこで、フラッシュの頻度を抑えて最適化したい。現状は、1秒あたり数件の更新を受け取っていて、場合によっては更新と更新の間隔が1ミリ秒以下にもなり得る。ここで使えそうなパターンはAggregatorMessage Filterだ。

まずは、Message Filterでメッセージの流量をコントロールする手を考える(たとえば、あるメッセージを受け取ってから5ミリ秒以内にやってきたメッセージは無視するなど)。

このアプローチには問題がある。全部のデータ項目が同時に更新されるとは限らないということだ。ひとつの債権について、価格を含めて50程度の項目がユーザー向けに表示される。ひとつのメッセージで全項目が書き換わるわけではない。ひとつの債権に関する一連のメッセージの一部を捨ててしまったら、重要なデータを見落としてしまうかもしれない。

もうひとつのアプローチがAggregatorだ。Aggregatorを使って、関連する一連のメッセージをひとつにまとめれば、メッセージの流量を減らせる可能性がある。最終的に、とりまとめた債権データをひとつのメッセージとしてクライアントに渡す。とりあえず、Aggregatorがメッセージを5ミリ秒おきに送るものとしよう。Message Filterと同じようなものだ。あとで、別の選択肢を探る。

Aggregatorだって銀の弾丸ではない。メリットもあればデメリットもある。Aggregator方式で問題になりそうなのは、この方式でメッセージ数を削減できるとは限らないというところだ。たとえば、全債権の特定の項目だけを更新するメッセージが一斉に届いた場合は、何の役にも立たない。仮に一定期間内の4つの債権に関する1000件のメッセージが届いたとすると、メッセージ数を1000から4に減らせる。一方、750の債権に関する1000件のメッセージが届いた場合は、メッセージ数は1000から750にしか減らない。少し調べてみたところ、実際にやってくるメッセージは、同じ債権に関する更新がまとまって届くことが多いとわかった。つまり、今回に関してはAggregatorが役立ちそうだということだ。

残る問題は、Aggregatorが集約したメッセージを送信するタイミングをどう判断するかということだ。どの方法を使うにせよ問題になるのは、メッセージの流れをコントロールするのがクライアントではなくAggregatorになるという点だ。このとき、メッセージの流れではなくクライアントがボトルネックになる。

なぜなら、Aggregatorは、クライアントがEvent-Driven Consumerだと想定しているからだ。クライアントをボトルネックにしないためには、クライアントをPolling Consumerに変更し、クライアントアプリケーションがメッセージの流れを制御できるようにする必要がある。この仕組みは簡単に実装できる。Command MessageAggregatorに送って更新を始めればいい。AggregatorDocument Messageでそれに答える。その中身は更新されたフィールドで、クライアントがそれを処理する。

Message FilterではなくAggregatorを選んだのは、単に今回のシステムの要件的にあっていたからにすぎない。

Major Production Crash

さて、次の問題。

本番稼動に入ったある日のこと。システムがダウンしてしまった。MQSeriesがクラッシュし、それに伴っていくつかのコンポーネントもダウンした。しばらく調べた結果、原因はMQSeriesのdead letter queueであることがわかった(これはDead Letter Channelの実装だ)。キューがあまりにも肥大化しすぎたため、サーバー全体がダウンしてしまったということだ。たまったメッセージは、期限切れの市場データメッセージばかり。コンシューマーがメッセージをさばききれないためにそうなった。期限切れの市場データメッセージが大量にdead letter queueにたまっているというのはつまり、メッセージの流量が多すぎるということだ。なんとかせねば!

最初の一歩としては、Aggregatorを使った解決の道を探ってみるのが妥当だろう。が、今回のシステムは、市場データ更新のメッセージを即時に送ることを前提としている。つまり、メッセージの収集と集約を待つことはできないということだ。残念ながら、Aggregatorによる解決はできない。

メッセージを一斉処理するために使えるパターンは、Competing ConsumersMessage Dispatcherのふたつ。

Competing Consumersを使うメリットは、やってくるメッセージを並行処理できることだ。しかし今回は、Competing Consumersではうまくいかない。なぜなら、サーバーとクライアントとの通信にPublish-Subscribe Channelを使っているからだ。ひとつのメッセージをすべてのコンシューマーが処理してしまうことになる。余計な作業が必要になる割には何もいいことがなく、今回の問題解決には役立たない。この手もアウトだ。

Message Dispatcherは、いくつかのパフォーマーをプールに追加するアプローチだ。ひとつのメインコンシューマーがMessage Channelを待ちうけ、メッセージをプール内の空きパフォーマーに委譲する。そしてすぐに、チャネルの待ちうけに戻る。これでCompeting Consumersと同じく並行処理のメリットを受けられるし、Publish-Subscribe Channelでも動作する。

今回のシステムにこの仕組みを組み込むのは簡単だ。まず、MessageListener?をひとつ作る(ディスパッチャー)。ここでは、その他のMessageListeners?(パフォーマー)のコレクションを保持する。DispatcherのonMessageメソッドが呼ばれたら、コレクションの中から実際にメッセージを処理するPerformerを選び出す。これはPublish-Subscribe Channel上でもうまく動作する。というのも、これもまたPoint-to-Point Channel上にあるからだ。

このセクションで取り上げたクラッシュ問題と、そのMessage Dispatcherによる解決は、パターンの適用の限界を示すよい例だ。このパフォーマンス問題は設計の不備によるもので、そもそもクライアントがメッセージを並列処理できないことが原因だった。パターンを適用することで問題はかなり軽減したが、根本的に解決したわけではない。真の問題は、クライアントがボトルネックになったことだからだ。どんなパターンを使えども、この問題は解決できない。結局その後、この問題に対応するためにメッセージフローの基盤をリファクタリングした。メッセージを、pricing gatewayからcontribution gatewayに直接まわすようにしたのだ。要は、パターンはシステムの設計や保守に役立つが、もともとの設計が腐っている場合には必ずしも役立たないということだ。


Summary

本章では、債券取引システムのいろいろな場面にパターンを適用してきた。事前の設計の際の問題を解決したり、本番環境での致命的なクラッシュに対応したりといったところでパターンを使った。また、サードパーティのプロダクトやレガシーコンポーネント、そしてJMSやTIBCOなどのメッセージングシステムにもパターンを適用した。いちばん重要なのは、これらは自分でシステムを設計したり保守したりするときに実際に現れる問題と同種のものであるということだ。本章でのパターンの適用例を読めば、自分たちのシステムにパターンを適用するときにも役立つことだろう。

担当者のつぶやき

「もともとの設計がダメなら何やってもダメよねー」身もふたもない… :)

みんなの突っ込み