はじめに
数多くの大規模分散システムの構築を通して、いくつかの悪習慣を目にする(そして実装する)機会がありました。ほとんどの場合こういった習慣は当初は無害ですが、放置しておくとシステムの成長と拡張性を阻害することになります。システムの保守容易性や拡張性を確保するためのベスト・プラクティスに関する記事は数多くありますが、この記事では避けた方がいい、いくつかの悪習慣(ワースト・プラクティス)を強調します。
テクノロジー
ある一つの技術やアーキテクチャで全ての要求を満たすことはできません。既存のアイディアを見直すタイミングを知ること、技術動向の見極め方を知ること、依存関係を綿密に調整する方法を知ること、これらはいずれも拡張性の重要な要素です。それぞれについてもう少し細かく見ていきましょう。
金のハンマー
「金のハンマー」とは、ハンマーを持つ人には、すべてが釘に見える、という古いことわざを指します。多くの技術者が一つの技術を使い続けようという考えに侵されます。このことは、特定の問題領域の機能に対して採用した技術より勝る他の技術が既にあるとしても、採用した技術で基盤を構築し運用していくという犠牲を払うことになります。本来意図されていなかった用途で特定の技術を利用することは、時に非生産的な結果につながります。
例えば、キーと値の組み合わせを管理する一般的な解決策はデータベースを用いる方法です。この方法が選択されるのは通常その組織ないしは開発者がデータベースに精通していることと多くの問題に対して同じ解決策を選択するからです。ところがデータベースの機能(参照整合性、ロッキング、結合やスキーマ)がボトルネックや拡張の阻害要因となった途端、問題が発生します。データベースに基づく解決策を拡張することは、それ以外の技術を利用した場合に比べて一般的にコストが高いからです。データベースに格納したキーと値に対するアクセスが頻繁になるに連れ、その優れた機能を利用しないことにより、データベースの並列処理モデルがパフォーマンスを落とす要因となるのです。伝統的なリレーショナル・データベースに対してCouchDB、SimpleDBやBigTableといった多くの代替手段でこれらの欠点を解決することができます。
別の一般的なハンマーは並列処理のプログラムに対して常にスレッドを用いるというものです。スレッドは確かに並列処理を可能にしますが、それによってコードの複雑性が増加し、スレッドが現在備えるロッキングとアクセス・モデルによって真のコンポーネント化は失敗することになります。現在普及しているプログラミング言語の多くが並列処理の制御にスレッドを利用するため、何千ものソースコードが実行状態を保持しデッドロックや一貫性のないデータ・アクセス制御の可能性を秘めているのです。スレッドを利用した場合の拡張性の問題がない、スレッドに代わる並列処理方法を示しているコミュニティがあります。すなわちErlangやStackless Pythonのコミュニティがそのような並列処理モデルを促進しています。例えこれらの言語が製品開発に選択されなかったとしてもこれらの言語から、メッセージ・パッシングや非同期I/Oといった、コンセプトを学ぶことはとてもいいことです。
リソースの乱用
開発者というのは小さなスケールの問題に取り組むことが快感なようです。プロファイラを使用し、アルゴリズムの空間的・時間的な複雑さを理解し、特定のアプリケーションに対してどのようなリストの実装を利用したらいいのかを知るといったことです。ところが、共有リソースのパフォーマンスの特徴を確認するといったより大きなシステムの制約やサービスのクライアントを知るとか、データベースのアクセス・パターンを知るといったことについては、全ての開発者が精通したいと考えているわけではないようです。
アプリケーションを拡張するための一般的な方法は、冗長でステートレスな何も共有しないサービスを水平分散するというアーキテクチャによるものです。ところが、経験上この増設方式に関しては、追加されたサービスによって利用される共有リソースの問題については解決しないというのを知っています。
例えば、永続化層にデータベースを使用しているサービスは、通常スレッド・プールを通してデータベースとのコネクションを管理しているハズです。コネクション・プールを使うことはとてもいいことであり、過度なコネクションの管理からデータベースを守るのに役立ちます。しかしそれでもまだデータベースは共有リソースであり、コネクション・プールは、ローカル環境上に構成されているにせよ、データベースと合わせて全体として管理しなければならないのです。これについて、失敗に繋がる2つの習慣を紹介します。
- 次第にサービスが増えているにもかかわらずコネクション・プールの最大値を減らさない
- 稼動するサービスを減らすことなく個別の(サービスに対する)コネクション・プールのサイズを増やす
どちらのケースでも、それぞれに加えて要求されるパフォーマンス特性を得るためにコネクションの総数はアプリケーションの設定により管理しなければなりません。
共有リソースを利用できるように管理することはとても重要なことです。というのも、共有リソースの取得に失敗すると当然ながらその失敗は一つのサービスにとどまらず広く伝播してしまうからです。
大きな泥団子
依存関係というのは多くのシステムにおいて必要悪ですが、依存関係の管理や念入りなバージョン管理の失敗はシステムの柔軟性や拡張性を阻害します。
コードの依存性管理には異なる方法があります:
- コードベース全体をコンパイルする
- 既知のバージョンに基づいてコンポーネントやサービスを選択する
- 後方互換に影響のある変更だけで構成されるサービスやモデルを公開する
それぞれのシナリオについて見ていきましょう。始めに、「大きな泥団子」パターンではシステム全体を一つの構成単位としてコンパイル、デプロイします。この手法には依存性管理をコンパイラに任せ、いくつかの問題を即座につかむことが出来るという明らかに優れている点がある一方、拡張性の問題に(テストやリソースの確保、そして大規模な変更のリスクが加わることを含めた)システム全体のデプロイの問題を巻き込んでしまいます。このモデルではシステムに対する変更を分離するのがとても難しくなります。
二つ目のモデルでは依存関係は好きなように選択することができますが、推移的な依存関係に対する変更があると一つ目のモデルにおける複雑性が現れます。
三つ目のモデルでは依存関係の管理とクライアントに後方互換性のあるインタフェースを提供することについてはサービスが責務を負います。これによってクライアントにとっては重荷が軽減されることになります。クライアントは緩やかに新しいモデルやサービスのインタフェースにアップグレードすることができるのです。加えて、データの変換が必要な場合、その変換はクライアント側ではなくサービス側で行われるのです。これが最終的に分離を確立します。後方互換を維持した変更の必要性とは、パッチの提供、クライアントの振舞いを考慮したアップグレード及び変更の取り消しを意味します。
後方互換性を維持するようなサービス・アーキテクチャを採用することで依存性の問題は最小化されます。またこのアーキテクチャは調整された環境において独立的にテストをしたりクライアントを気にせずにデータを更新することができます。サービスの変更をクライアントから分離するのにはこれら三つの利点全てが必要です。最近リリースされたGoogle Protocol Buffers(リンク)プロジェクトは新たな後方互換を維持したサービス・モデルとインタフェースを提唱しています。
全てが一部か
依存関係の管理についてもう一つ考えなければならないのがアプリケーションに含まれるコンテンツのまとめかたです。
Amazon Machine ImagesやGoogle AppEngineアプリケーションといったいくつかのケースでは、アプリケーション全体とその依存するものが一つにまとめられ配信されています。この「全部入り」のアプローチはアプリケーションを自己完結させます。しかし、この方法はアプリケーションをまとめたサイズを肥大化させる共にアプリケーションに対するほんの些細な修正(同一物理マシン上で利用する共有ライブラリに対する変更を含む)でさえもアプリケーション全体を再デプロイさせることになります。
代わりとなるシナリオではアプリケーションの依存関係をホスト・システムに任せ、アプリケーションは依存するもののほんの一部と一緒にまとめられます。この方法はアプリケーションをまとめたサイズを小さくしますが、サービスを利用可能にするには事前にいくつかのコンポーネントを各マシンに設定する必要があるのでデプロイの手間は増えます。自己完結型のパッケージでデプロイしないということは標準構成でない代替マシンへの移行を阻みます。それは依存するものが即座にみつからなかったり、マシンがたまたまテスト中だったり依存するものの状態が正しくなかったりするためです。
後者のアプローチでは異なるスコープ(グローバル、マシン、アプリケーション)間に及ぶ依存関係の管理はミスをしやすく複雑です。構成機器間の分離と依存関係の分離を減少させることで操作手順の複雑さを増大させるのです。ルールに例外はつきものですが、一般的に言えば他と分離することによって拡張性は増します。従って、可能な限り「全部入り」の方法を採用してください。
コードとアプリケーションの依存関係管理の両面において、最悪なのは依存関係を理解せずに管理を助けるためのモデルを考案することです。綿密な制御の実行に失敗することは拡張性の妨害に貢献することに等しいのです。
時間の確認を忘れる
分散システムにおいてはしばしば、開発者からの実行要求を分散するために可能な限りきれいに責務を分割することがゴールとなります。このことによって開発の大部分をコア・ビジネス・ロジックに注力することができ、フェールオーバーやタイムアウト、その他分散システムに要求される多くのことを気に掛けることから解放されます。ところが、リモート呼び出しをまるでローカル呼び出しのように見せる手段を提供するということは、開発者はローカル環境で呼び出しを行っているのと同様なコーディングを行うということを意味しています。
私はあまりに多くのコードが、リモートへの要求が時間内に成功するという根拠のない期待をしているのを目にします。例えばJavaではJDK1.5に含まれるHTTPURLConnectionというクラスで読み取りタイムアウトしか公開していません。プロセスを殺すためのスレッドを作成するのかただ単純にレスポンスを待ち続けるのかの判断を開発者に委ねています。
JavaにおけるDNSルックアップは時間管理の欠如に関するもう一つの例になります。典型的な長時間稼働のシステムでは初期にDNSルックアップが実行され、明示的に指定されない限り、JVMの稼働中ずっとその結果がキャシュされます。外部システムがホスト名に対応するIPアドレスを変更すると当初の結果は間違った結果を示すことになり、コネクション・タイムアウトがプログラム的に設定されていないために、多くの場合コネクションがハングすることになります。
システムを適切にスケーリングするにはリクエスト処理の許容時間を管理することが欠かせません。その手段はいろいろあります。いくつかは開発言語(例えばErlang)に組み込まれていますし、例えばJavaに対するNIOのようにライブラリもあります。実装言語やアーキテクチャにかかわらず、適切な待ち時間の管理は必要不可欠なものなのです。
運用時
費用効果の高い拡張モデルを確立すること、依存関係を適切に管理すること、処理の失敗を事前に察知すること、これら全ては優れたアーキテクチャの一面です。しかし、運用環境において容易にデプロイや運用出来ることもこれらと同様に重視されなければなりません。この点について、システムの拡張性を危うくする悪習慣が数多くあります。
ヒーロー・パターン
運用の問題における最も一般的な解決策は、運用時の大量のニーズに全て対処し得るそして対処してしまうヒーローを用意することです。このヒーロー・パターンは小規模な環境で一個人が、適切に稼働するために必要となる様々なニュアンスを含む、システム全体を理解する能力を有しているときに機能します。多くコンポーネントからなるより大規模なシステムに対してはこのアプローチは通用しません。にもかかわらず往々にしてこの解決策が適用されています。
たいていの場合、ヒーローは公式な仕様が存在していないサービス間の依存関係を理解し、様々な機能をどのようにオン/オフするのかを覚えていたり、他の誰もが忘れてしまったシステムのことを知っていたりします。ヒーローの存在は極めて重要ですが、それが一個人であってはいけません。
私はヒーロー・パターンに対する最善の策は自動化だと思います。また、組織が許容すれば自動化によって担当者をチームからチームへとローテーションすることも容易にします。銀行業では、しばしば強制的に休暇を取得させられるので「自宅から実行するよ」という行為が明るみに出ることがあります。
未自動化
人手による介入に大きく依存しているシステムは、多くの場合ヒーローが存在している結果ですが、再現性や「誰かがバスにひかれたら」症候群の問題に対してとても危険な状態にさらされています。特定のビルドやデプロイや環境が再現できることはとても重要なことです。また、明示的なメタデータによる自動化は再現性を高めるキーとなります。
いくつかのオープン・ソース・プロジェクトでは、成果物をリリースするプロセスは自分のマシン上で製品を開発している特定の開発者に依存していました。そして提供された成果物のバージョンがソース管理システムのブランチを反映していることは必ずしも保証されていない状態でした。このような状況下では、一度もソース管理システムにコミットされたことのないコードをベースにしたソフトウェアをリリースすることもあり得ました。
上で述べたように、ヒーローの行動は自動化するのに適しています。個人(もしくは複数の人)を比較的簡単に置き換えられることを保証する自動化。自動化の代替策はプロセスの追加です -- Clay Shirky氏(リンク)はプロセスを面白い表現で定義しています。
プロセスとは、間抜けな行動の結果に対する反動のことである
間抜けな行動は避けられないのでしょうか。自動化は学習成果を反映してなければなりません。
モニタリング
モニタリングは、テストのように、時間がないときに真っ先に削られるものです。時々コンポーネントの稼働時の特性について質問すると答えが返ってこないことがあります。稼働中のシステム内部を可視化出来ていなくてこのような質問に即座に答えられない状態は、どこを見て何を探せばいいのかということについて狂いなく重要な決断をすることを阻害します。
Orbitz社は幸いなことに、サービスの実行状況に関する詳細情報と問題のある領域をピンポイントで可視化するとても洗練されたモニタリング・ソフトウェアを利用しています。モニタリング設備から得られるメトリクスによって素早く効果的に問題の優先付けを行うことができます。
まとめ
今般のAmazon社S3の機能停止の後、Jeff Bezos氏(リンク)は以下のように述べました。
問題が生じたら、直前の原因は分かります。そこから解析を始め根本的な原因を突き止め、修正し先に進むのです。
ソフトウェアやシステムの開発は反復的なプロセスで失敗と成功の機会が多くあります。シンプルでも拡張には向かない解決策にも居場所はあります。とりわけ未成熟/未完成段階のアイディアやアプリケーションに対しては役立つでしょう。完璧は優良の敵ではありません。しかし、システムが成熟するにつれ、上で述べた悪習慣は取り除かれるべきです。そしてあなたも成功を収めるでしょう。
この記事の下書きにフィードバックをくれたMonika Szymanski氏に感謝します。
著者について
Brian Zimmer氏はトラベル・サービスYapta(リンク)のアーキテクトで、オープン・ソース・コミュニティで尊敬されているメンバであり、Python Software Foundationのメンバでもあります。以前はOrbitz社のシニア・アーキテクトとして勤務していました。ブログを運営しています http://bzimmer.ziclix.com。
原文はこちらです:http://www.infoq.com/articles/scalability-worst-practices
(このArticleは2008年8月18日に原文が掲載されました)