システムをリリースして無事運用に乗った後も、様々な要因によりシステムを拡張する必要が出てきます。今回は、システム拡張の要因、及び基本的なシステム拡張の方法を具体例を挙げつつ説明していきます。
初めに
これまで4回に渡り、インフラ運用に関する入門的な記事を書いてきました。それらの内容を実施して、システムが安定運用に乗ってきたと仮定します。業務系のシステムであれば、そこでインフラ担当の仕事は大体終わりとなり、オペレーターなどに主要な業務を引き継ぐことになると思います。それに対してRettyのようなWebサービスの場合は、システムが軌道に乗った後も継続的なシステム拡張が発生することが一般的です。
本記事では、まずどのような事をきっかけ・要因としてシステム拡張が必要になるのかについて説明します。次に、Rettyのシステム構成を簡単に説明し、システム拡張が必要となる各種要因に対して、どのような方針で拡張を実施していけばよいかを、Rettyでの実例を元に説明します。最後に、システム拡張の際の具体的なtips、注意点についても述べていきます。
システム拡張のきっかけとなる事象
まずは、どのような事が契機となって、システム拡張が必要になるかについて説明します。
アクセス数の増加
システムへのアクセスが増えると、現状でのシステム構成では捌ききれなくなり、一般的にはシステムの増強が必要になります。
アクセス数の増加原因としては、以下の2通りが存在します。
- 自然増
- 一時的な増加
「自然増」に関しては、以前説明したログ・メトリクスの収集を行なっていれば、いつくらいに処理能力の限界に達するかが分かるため、対策は比較的容易です[1]。
それに対し「一時的な増加」には、アクセスがどのくらい増加するかを予測しづらいという特徴があります。
また一時的な増加は、増加のタイミングがあらかじめ分かっているものとそうでないものがあります。前者の例としては、「メディアでのプレスリリース掲載」「テレビ番組での紹介」などが挙げられます。後者の例としては、影響力のある人がブログで紹介したりTwitterでつぶやいたり、特にサービス運営側への取材もなく、メディアが掲載してくれる、といったものが挙げられます。
[1] | 本連載の記事「ログ・メトリクスの収集」を参照。 |
データ量の増加
データ量の増加もシステム拡張要因の一つです。Rettyのようにユーザー登録してユーザーがコンテンツを投稿するようなサービスの場合、以下のようなデータが増えていきます。
- ユーザーデータ
- 投稿データ
- ユーザーの行動履歴
サービスの種類によって、増えるデータの種類はこれらとは異なることもありますが、いずれにしてもサービス提供期間が長くなるに連れて、システム内のデータは増えていきます。
機能追加
Webサービスは一旦リリースしたらそれで終わりではありません。随時、機能追加や修正を行うのが常です。それに伴い、基本的にはデータ量が増加していきます。
Rettyの例で言うと、リリース当初は投稿につき写真が1枚しかアップロードできませんでした。しかし、現在では10枚までアップロードできるようになっています。また「人気の投稿」を表示するために、各投稿に対して「行きたい」をクリックした人数を、定期ジョブで集計してDBに保存しています。これらにともない、システム内部に保存しているデータ量も多くなっています。
対応の基本的な方針とその実例
ここまで説明したような事象に対応してシステムを拡張する際に、どのような方針で行うのか、Rettyのシステムを具体例に説明していきます。
まずは、以下の表にRettyの各コンポーネントと拡張方針を一覧にしました。それぞれの項目について詳しく説明していきます。
構成要素 | 拡張方針 |
---|---|
Web | スケールアウト |
DB | スケールアップ キャッシュ システム分割 |
検索 | システム分割 スケールアップ |
静的コンテンツ | なし |
システム分割
システム分割とは、その名の通りシステムを分割して、(一般的には) 別々のサーバーに配置する事です。具体的には主に以下の2通りがあります。
- 機能別の分割
- データの分割
機能別の分割
Rettyで過去に実施した機能別の分割には、「全体を一台で運用していたシステムを、WebサーバーとDBサーバーに分割した」という例があります。
また今後、発生しそうな分割には「検索機能の分割」があります。Rettyにはいくつかの検索機能があります(代表的なものはお店検索)。現在はすべて1台のサーバーで処理しており、それぞれの検索機能間に依存関係は存在しません。現在の検索対象はいずれも10万〜100万件と比較的少ないのですが、件数が増えてきた段階でサーバーを分けることを検討しています。
機能別の分割の場合、上に挙げた例のように機能間が疎結合であればあるほど分割は容易ですが、そうでない場合にはシステムの構成やプログラムの修正などを伴う比較的大変な作業になります。
データの分割
データの分割には水平分割と垂直分割があります。
前者はDBのパーティショニングやシャーディングを思い浮かべてもらえれば、それに近いものだと思います。具体的には以下のようなものです。
- 時系列による分割(最新1週間分とそれ以外、月別等)
- IDによる分割(100万単位で分割等)
- ハッシュ値による分割
もう一方の垂直分割は、テーブルのカラムを別テーブルに移す(正規化も含む)ようなものです。
データを分割する場合には、プログラム修正やシステム構成の変更が発生する場合が多いでしょう。
Rettyで過去に行った主なデータ分割は、水平分割として「通知系データの時系列による分割」が、垂直分割には「お店データの複数テーブルへの分割」があります。詳しくは後述します。
スケールアウト
スケールアウトは、サーバーの台数を追加することによって処理能力の向上を図るものです。例えばWebサーバーを1台から2台に増やすといったことが挙げられます。人によっては前項の「システム分割」もスケールアウトと呼ぶ場合もありますが、ここでは単純な台数追加のみをスケールアウトと呼ぶことにします。
スケールアウトの特徴ですが、一点目として、クラウド環境では比較的簡単に実施できる点が挙げられます。RettyではAmazon EC2を使用していますが、EC2の場合、Amazon Machine Image(AMI)を事前に作成しておけば、インスタンスの追加は1分程度で行えます。また、一時的なトラフィック増に対応してインスタンスを追加した場合、トラフィックが落ち着いてきた時にインスタンス数を減らすのも容易です。
特徴の二点目として、通常はシステムのダウンタイムが発生しないという点を挙げたいと思います。「通常は」と書きましたが、システムが複数台構成に対応していない設計の場合、そこを修正するためにシステムダウンが発生するケースがあります。
Rettyでは、Webサーバーに対するトラフィック増加には、スケールアウトで対応しています。Webサーバーで行う処理は比較的単純なので、スケールアウトで線形に近い処理能力の増加が見込まれます。
また、Rettyでは使っていませんが、Amazon RDSであればMySQLのリードレプリカを追加する事で、比較的容易にDBサーバーの処理能力を向上させることができますが、これもスケールアウトの一例です。
スケールアップ
スケールアップとは、サーバーのリソースを増強する事で処理能力の向上を図るものです。具体的にはCPU、メモリ、HDDの追加などです。
スケールアップの特徴として、以下の4点を挙げておきます。
- 設計・プログラムの変更は無い
- システム停止を伴うため、増減はしづらい
- 必要十分な処理能力にスケールアップできるとは限らない
- 無制限にはスケールアップできない
1点目ですが、スケールアウトの場合、システムの設計が複数台構成に対応していない場合には、インフラの構成やプログラムの変更が発生します。しかし、スケールアップの場合にはそのような変更が発生しないため、技術的な難易度は低いと言えるでしょう。
2点目ですが、リソースの増強のために通常はサーバーを停止する必要があるため、一時的なアクセス増などに対応して頻繁にリソースの増減を実施するのには向いていません。基本的には、アクセス・データの自然増に対応するための施策と考えるのが良いと思います。
3点目はちょっと分かりづらいかもしれないので具体的な例を挙げます。Amzon EC2の場合、インスタンスタイプ にはSmall, Medium, Large, Extra Largeといった種類が存在します。メモリ量に関して言うと、Mediumを1とすれば、Largeは2、Extra Largeが4です。メモリが足りなくなって増強しようとした時に、若干オーバースペックと分かりつつも、2倍にするしか選択肢はありません。
別の例としては、EC2では一番メモリ量が多いインスタンスは、現在68.4GBのメモリのものなので、それより多くのメモリが必要な場合にはスケールアップでは対応できません。
他のクラウドベンダーの場合も、基本的には用意されているインスタンスタイプから選ぶ必要があるのは同じで、これと同様の問題があります。
4点目に関してですが、OSなどの各種制限により、無制限にスケールアップする事はできません。32bit OSの場合、メモリは4GBまでですし、Windows Serverの場合、エディションによって最大のCPU数が決まっています。また、CPUやメモリを増強しても、性能が線形に増加するとは限りません。
従って、リソース増強の要求をスケールアップのみで乗り切ることは、実際には不可能です。
Rettyの場合、DBサーバーの処理量増加に対して、これまで、そして今後もしばらくはスケールアップで対応する予定です。理由としては、テーブル間の依存関係が比較的強いため(「リレーショナル」データベースのため当然ですが)、分割に比較的手間がかかること。また、スケールアウトできる構成にするのには、それなりの手間がかかるという点が挙げられます。
キャッシュ、事前処理等
システム拡張のその他の方法として、キャッシュ・事前処理などをまとめて紹介しておきます。
これらに共通する基本的な考え方は、「高価な計算処理を1度で済ます」という事だと思います。具体的な方法としては、以下のようなものがあります。
- バッチによる集計
- コンテンツキャッシュの導入
- キャッシュサーバーの導入
「バッチによる集計」は一般的なので特に細かく説明する必要はないかと思いますが、具体例を挙げると「集計処理を定時バッチで事前に計算してDBに保管しておき、必要な時にはDBに保管してある計算結果を参照する」といった方法です。Rettyでも、この処理はさまざまな用途で使われています。
次に「コンテンツキャッシュの導入」ですが、具体的にはリバースプロキシや、あるいはフレームワークによるキャッシュの生成と言った方法があります。
最後に「キャッシュサーバーの導入」ですが、具体的にはmemcachedなどを指しています。使い方としては、例えば「比較的時間がかかるSQLの結果をキャッシュサーバーに保存して、2回目以降はそちらから取得する」などがあります。
これらの方法にはいずれも、処理時間を減らせるというメリットと引換えに、以下のようなデメリットがあります。
- 最新のデータ・状態を反映した結果であるとは限らない
- 使用するリソース(一般的にはディスクやメモリ)が増加する
システム拡張の際のtips、注意点
前章では、Rettyのシステム拡張の基本方針について説明しました。ここでは、そうしたシステム拡張の際の具体的なtips、あるいは注意点について述べていきます。
アーキテクチャ全般
システム構成要素間を疎結合に
アーキテクチャ全般に関しては、前述の通り、システム構成要素間を疎結合にしておくというのを第一に挙げたいと思います。複雑で大きな処理は作らずに、小さく独立した処理を組み合わせてシステムを構成するようにします。
n台構成に対応した設計
次に、スケールアウト戦略を取るコンポーネントに関しては、「n台構成に対応した設計」を最初から意識する必要があります。
Rettyで具体的に問題になった点をいくつか紹介すると、「ログ集計」、「監視」などが当初は対象が1台であることが前提になっていたため、Webサーバーを2台にする時点で修正が発生しました。
ログ集計や監視に限らず、こうしたサーバー間でやりとりが発生する仕組みを構築する際には、最初から複数台構成を想定した設計にしておく事をお勧めします。
また、複数台構成になった場合、1台でしか動かさない処理を明確にしておく必要があります。Rettyの場合、Webサーバーでもcronジョブをいくつか動かしているのですが、各サーバーで動かす必要のあるもの、1台だけで動かせばいいものを区別して把握しておく必要があります。
Webサーバーには状態を持たせない
これは1つ前の話の具体的なケースとも言えますが、「Webサーバーには状態を持たせない」というのがあります。なぜかというと、ロードバランサーやリバースプロキシを導入してWebサーバーを複数台構成にした場合、最初にアクセスした時と次にアクセスした時で、別のサーバーに飛ばされる事が一般的だからです[3]。
Webサーバーに保持する「状態」の代表的なものは、セッション情報です。PHPの場合、デフォルトでセッション情報をファイルに保存しますが、最初からそれをDBなどに保存するようにしておくと良いと思います。
また、ちょっと特殊な例ですが、Rettyではユーザーが投稿時にアップロードしたファイルが問題になったケースがあるので、ご紹介します。
投稿画面で画像を投稿するときには、以下の2つの処理が実行されます。
- Webサーバーへのファイルのアップロード(一時ディレクトリへの保存)
- WebサーバーからAmazon S3への転送
当初は、ファイルを選択した時点で前者が実行され、投稿ボタンを押した時点で後者が実行されるようになっていました。しかしファイルをアップロードしたサーバーと、投稿ボタンを押した時のリクエストが、異なるサーバーに送られることが起こりえます。現在は2つの処理を同時に行って、Webサーバーに状態を持たないようにしています。
[3] | 同じセッションに属するリクエストを常に同一のサーバーに振り分ける「sticky session」に対応したロードバランサーも多く、実際、ELBも対応しています。ただ、sticky sessionは必ずしもすべての環境で使えるわけではありません。例えば、ロードバランサーではありませんが、Google App Engineではsticky sessionがサポートされていません。 |
DB設計
DB設計に関するtipsとして、以下の3つを挙げたいと思います。
- まずは正規化、後から非正規化
- テーブル分割
- データベース分割
まずは正規化、後から非正規化
スケーラブルなDB設計と言うと、大規模システム等での「ベストプラクティス」として、JOINを使わないようにとか、非正規化、というものを想像する方もいらっしゃるかと思いますが、まずはオーソドックスに正規化をすることをお勧めします。
その理由は、ある程度システム規模が大きくなってからでないと、そうした非正規化が必要になるケースはそれほど多くなく、また後で必要になった時点で非正規化するのはそれほど大変ではないためです。
JOINに関しても同様で、最初は通常通りJOINを使ったクエリーでデータを取得するようにし、後述のデータベースの分割などが発生した時点で、JOINを使わないようにプログラムを書き換える方が効率的だと思います。
テーブルの分割
前の章でデータの分割について説明しましたが、ここではRettyで実施したデータ分割について、水平分割、垂直分割の例を2つ挙げて説明します。
1つ目の例は通知データです。Rettyではコメント等、ユーザーが何らかのアクションを行った際に、アクションを受けたユーザーにそれを通知する機能があります。
以前は1つのテーブルに通知データを格納していましたが、その件数は増えて現在では数百万レコードあるため、過去一定期間内のデータを入れるメインテーブルと、それ以外のアーカイブテーブルに分割しました。現在のメインテーブルのレコード数は数十万レコードなので、分割によって通知の閲覧処理の負荷が大幅に軽減されました。
これにより、メインのページで確認できるのは過去一定期間内に送られて来た通知だけで、それ以前の通知については別のページに行かなければならないようになりましたが、過去の通知を確認したいというユーザーは比較的少ないため、特に問題にはなっていません。
これは水平分割の例でしたが、垂直分割の例として、お店情報の分割について説明します。
お店には店名、位置情報、所在地、最寄り駅などの基本情報から、営業時間、ジャンル(和食、ラーメン等)、お店のホームページURL等、さまざまな情報が存在します。これらの情報を1つのテーブルに格納すると巨大なテーブルができてしまい、クエリーの速度等にも影響します。このような場合、機能別にどのカラムが使われるかを調査して、テーブルを分割します。例えば、お店名、エリアID、位置情報は大半のクエリーで参照される一方、所在地、営業時間などはお店情報ページなど、限られたページでしか使われないため、別テーブルに移動する、といった具合です。
DBの分割
最後にDBの分割です。この記事の前半で、Rettyは現在1台のDBサーバーで運用していると述べましたが、内部でのみ使う管理系のデータ、ログ関連のデータは、一部別のDBサーバーに格納されています。
機能毎のDB分割は、インフラ側の作業としては簡単で、負荷低減の効果は非常に高いですが、プログラム側では前述したような修正、例えばJOINを使っている部分の書き換えなどが発生します。そのため、プログラム設計時から、将来のDB分割をある程度考慮しておいた方が良いと思います。
プログラム
プログラム関連で見落としがちな点として、(一部の)大量データへの対応という問題を挙げておきます。
どのような事かというと、例えば大多数のユーザーは少数の投稿しかしませんが、ごく一部のユーザーが大量の投稿を行います。別の例としては、大多数の投稿にはコメントがつきませんが、ごくまれに、数百件のコメントが付く投稿が存在します。開発時には、そうした低頻度の大量データが発生することを想定しておく必要があります。
Rettyで過去に問題になった例として、ユーザーの投稿一覧ページがあります。前述した通り、通常は多くても100件程度の投稿数ですが、中には数千件の投稿をしているユーザーが何人かいて、その人のページが非常に遅くなってしまったという例がありました。この問題は、1ページに一定件数だけ表示するようにプログラムを改修して対応しました。
別の例では、あるIDの一覧を取得し、それを以下のようなSQLのIN句に使用する処理で問題が発生しました。
SELECT col.... FROM table1 WHERE id IN (IDの一覧);
IDの件数は通常は多くても数百なのですが、特殊な条件だと数万件になる事があり、その場合には長大なSQLを投げることになってしまい、処理に時間がかかっていました。
移行
前節で挙げたような方法でシステムを拡張する際に、システムの移行作業が発生する場合があります。ここでは、移行時に気をつけるべき点について紹介します。
Webサーバーを複数台に
Webサーバーを1台から2台にする場合、ロードバランサー等を導入する事になります。Rettyの移行手順を例として、簡単に説明します。
- Amazon Elastic Load Balancing(ELB)を導入
- 2台のWebサーバー(web1, web2)をELB配下に接続
- retty.meのAレコードをweb1のIPアドレスからELBのアドレスに変更
1番に関してはここでは説明しませんので、ELBのドキュメントなどを参照して下さい。2番に関してですが、この時点ではまだretty.meのAレコードはweb1のグローバルIPアドレスに直接向いており、web2は使われていません。3番を実施後、そのDNS設定変更が徐々に伝播すると共に、ELBへのアクセスが増加し、元のAレコードのTTLが経過後、完全に切り替わります。
ELBに関する注意点としては、ELB自体が複数のIPアドレスを持つため、通常はAレコードではなくCNAMEを使用する必要があります。ただ、「www.retty.me」ではなく、「retty.me」のようなnakedドメイン(apexドメインやbareドメインともいう)を使う場合、CNAMEを設定できないため、Amazon Route 53を使用する必要があります。
あとELB固有ではありませんが、ロードバランサーを導入することにより、Webサーバーのアクセス元IPアドレスがロードバランサーのものになってしまいます。解決策としては以下の2通りが挙げられます。
- 接続元が必要な箇所(例えばアクセスログ)では、X-Forwarded-Forを代わりに使う
- Apacheの場合、mod_rpaf、mod_extract_forwardedなどのモジュールを使う
スケールアップ・リソース増強時の注意点
スケールアップの際に注意して欲しいのは、サーバーのリソース増加に合わせて、リソース関連の設定項目を変更する必要があるという事です。例えばMySQLの場合、innodb_buffer_pool_sizeなどを変更する必要があります。
また、データ量の増加に伴いディスクを増設するというのも良くある事だと思います。DBのデータ増加が特に多いと思いますが、データ用のパーティションにLVMを使用する事で、サービスを停止することなく領域の拡張が可能です。
まとめ
システム拡張が必要となる要因にはいくつかの種類があります。それらの要因ごと、事前にある程度対策を考えておくと、スムーズにサービスを拡大できると思います。
また、システム拡張にはいくつかのパターンがあるので、それらのパターンごとに、最初から設計・実装をしておくべき事、後から対応すべき事を整理しておくと良いと思います。後からでもできることを最初からやってしまうと、余計な時間・コストが発生する事にもなります。
最後にRettyで過去行ったシステム拡張や問題となった点をいくつか紹介しました。実際のシステム・サービスの性格によって、これらの内容がそのまま当てはまるとは限りませんが、一部分でも読者の参考になる部分があれば幸いです。
執筆者紹介
鹿島和郎
ソーシャルグルメサイト「Retty」の開発・インフラ全般を担当。過去には米企業でのオフショア開発拠点の立ち上げから国内SIerでの運用保守業務まで、広く浅く経験している何でも屋。
Rettyは、「行った」お店の投稿・共有、友達・趣味嗜好の合う人のオススメから「行きたい」お店をリスト化、スマートフォンで現在地周辺のお店をリストから検索などできるサービスで、Facebook、Twitterアカウントがあれば無料で使用可能。PC、iPhone、Androidに対応。