ソフトウェア開発ディレクターで、以前チーフ・ソフトウェア・アーキテクトも務めたMax Indelicato氏は、拡張性のためにどのようにアプリケーションを設計するかという記事を書いた。氏は、正しいデプロイ、ストレージソリューションおよび拡張性のあるデータストレージとスキーマを選択し、抽象レイヤを利用するように推奨している。
仕事に適したツール
Indelicato氏の最初のアドバイスは、次のアーキテクチャ上のソリューションの中から、「仕事に適した正しいツールを選択する」というものだ。
- クラウドに配備するソリューションの利用
- MongoDBやCouchDB、Cassandra、Redisなどの拡張性のあるストレージソリューションの利用
- Memcachedのようなキャッシュ層の追加
これらのソリューションは、アプリケーションの初期から必須ではないが、後ほど切り替えを行わなくてすむように、拡張性のあるデータストレージソリューションを選択しておくことが賢明だと著者は考える。クラウドに配備することは、立ち上げ後の利用量を正確に見積もることが不可能なスタートアップ企業にとっては、特に利点がある。クラウドに配備することで、需要が増大すれば、アプリケーションを簡単に拡張させることができる。多くのソフトウェアアーキテクトは、アプリケーションが大きくなっていき、キャッシング層を追加し、問題を解決した話をしている。しかし、この解決策は設計のフェーズから必ずしも考慮にいれなければならないものでもない。途中で簡単に導入することができるのだ。
拡張性のあるデータストレージ
Indelicato氏は、パーティショニングやレプリケーションをサポートし、拡張に柔軟性のあるデータストレージ、つまり次のうちの一つを選ぶように勧めている。MongoDB、Cassandra、Redis、Tokyo Cabinet、Project Voldemort、リレーショナルデータベースならMySQL。アプリケーションのライフサイクルのどの時点でも、パーティショニングは必要なので、これは望ましい。レプリケーションは拡張性のためではなく、高度な可用性を保証するために必要だ。拡張に柔軟性があると負荷がピークに達したときに素早くノードを増やして対応できるだけでなく、ハードウェアの故障やアップグレード、大規模なスキーマの変更、その他ダウンタイムが必要な、なんらかの理由により、特定のノードに対するメンテナンスが必要な際にも対応可能となる。
拡張性のあるデータスキーマ
Indelicato氏は、データ共有を行いやすいスキーマを作成するように推奨し、UserとUserFeedEntryの例を次のように示した。
Collection (or Table, or Entries, etc) User { UserId : guid, unique, key Username : string PasswordHash : string LastModified : timestamp Created : timestamp }
Collection (or Table, or Entries, etc) UserFeedEntry { UserFeedEntryId : guid, unique, key UserId : guid, unique, foreign key Body : string LastModified : timestamp Created : timestamp }
そして、UserIdをつかったパーティショニングを次のように語った。
UserId項目によるパーティショニングにより、User CollectionとUserFeedEntry Collectionともに、2つの関連のあるデータの破片を同じノードに格納することになる。UserId xxx-xxx-xxx-xxxをもつ、すべてのUserFeedEntryは、UserIdの値が、xxx-xxx-xxx-xxxであるUserエントリと同じノードに格納される。
なぜ、このようにすると拡張性を確保できるのだろうか。理由としては、このアプリケーションの要件が、データの分散に最適なものだからだ。それぞれの訪問者は、ユーザのプロファイルページを訪れた際、ユーザの詳細情報を取得するリクエストが行われ、2回目のリクエストで、そのユーザのUserFeedEntriesが同じノードから取得される。2つのリクエストのうち一つは、一行を取得するもので、もう一つは複数行を取得するものだが、すべてが同じノードに含まれているのだ。大半のユーザのプロファイルページが、一日あたり同数のリクエストをうけると仮定すると、私たちは、このWebアプリケーションの要件を満たす拡張性のあるスキーマを設計したことになる。
抽象レイヤを利用する
Indelicato氏の推奨の最後のひとつは、全体のなかで次のような抽象レイヤを利用することだ。レポジトリ、キャッシング、そしてサービス。リポジトリ・レイヤを作成する際に、氏は次のように推奨している。
- 抽象化しようとしているデータストレージ特有の方式でメソッドの名前をつけてはならない。例えば、リレーショナルストレージを抽象化する場合、SQL問い合わせとコマンドを発行するためにSelect()、Insert()、Delete()、Update()というファンクションをよく見る。これは良くない。その代わりに、リレーショナル・ストレージに特定されない、Fetch()、Put()、Delete()、Replace()のように名前をつけるべきだ。このことによって、レポジトリ・パターンにより忠実に従うことができ、背後にあるストレージを置き換える際に、ずっと簡単におこなえるようになる。
- 可能であれば、インターフェース(もしくは抽象クラスなど)を利用すべきだ。インターフェースを高位のレイヤのアプリケーションに渡すことで、レポジトリの具体的な実装を直接参照することを避けることができる。これは単体テストの仕組みを構築する際にも、とても役にたつ。なぜなら、テストケース用のデータがあらかじめ設定されたテスト用の実装を記述することが簡単にできるからだ。
- ストレージ特有のコードすべてを、実際のレポジトリが参照するもしくは継承するクラス(もしくはモジュールなど)に記述する。それぞれのファンクションのなかで、必要な特定の接続用ファンクション(クエリ・テキストなど)を記載する。
- レポジトリの抽象化の対象は常に同一のストレージソリューションである必要はない。ユーザ情報をMySQLに格納し、UserFeedEntriesをMongoDBに格納することも望むなら可能であり、レポジトリはこのような実装を将来行う際にも、過剰なオーバーヘッドなく変更を行えるように実装されていなければならない。前に述べた3つのポイントも、このことを実現するために間接的に役に立つ。
キャッシング・レイヤに関して、Indelicato氏は、次のように語っている。「単純なページ(もしくはビューなど)のレベルのキャッシングもしくはサービスレイヤのキャッシングから始めることが多い。なぜならこれらのレイヤでは、ステートの変更がたまにしかおこなわれないことも、珍しくないからだ。」
Indelicato氏は、サービスレイヤは、十分な抽象度をもち、必要に応じて、内部実装を簡単にいれかえることができなければならないと考えている。
必要になったときに対処すればよいことで、拡張性に関しては当初からあまり心配する必要はないと考える人もいる。拡張性をはじめから考慮に入れておくとすれば、他にどのような推奨事項が考えられるだろうか。