Author Archives: kanazawa

WordPressのプラグインを改造して本家に取り込んでもらおうと思った話

kanazawa|2018年12月07日
Aurora

SREチームの金澤です。
Lancers(ランサーズ) Advent Calendar 2018 7日目の記事になります。

昨日は、odrum428さんの「slackで動くピアボーナス機能を実装した話」でした。

今年は、Lancers以外のAdvent Calendarも参加させていただいてます。(以下)

5日同様、PHP Advent Calendar 7日目との掛け持ちにしようと思っていたのですが、それができないことがわかり、新しいネタをひねりだすことに。。

ということで、WordPressネタを書かせていただきました。

WordPressのYARPPプラグイン

ランサーズでは、10個以上のサービスをWordPressで運用しています。
そのうち、以下のサービスについては、Yet Another Related Posts Plugin(YARPP)を利用しています。

ランサーズエリアパートナープログラム
さすらいワーク

YARPPは、投稿した記事のキーワードを解析し、記事を表示する時に関連記事やキーワードを表示することができるプラグインです。

例えば、以下のページの右側に表示される情報は、YARPPプラグインが生成しています。
https://lohai.jp/teshio/

 

MySQLの全文検索機能

このプラグインは、MySQLの全文検索機能を使っています。
この全文検索機能は日本語の形態素解析まではサポートしておらず、英語のような単純なスペース区切りの文章が対象になりますが、YARPPプラグインはこれを利用して解析を行っています。

MySQLのストレージエンジン

MySQLは、大きく2つのストレージエンジンをサポートしています。
MyISAMとInnoDBです。

MyISAMは、古くからサポートされているストレージエンジンで、高速な処理が可能ですが、トランザクションをサポートしていません。
InnoDBは、現在主流のストレージエンジンで、トランザクションをサポートしています。

そして、MySQLの全文検索機能は永らくMyISAMしかサポートしていませんでした。

同様に、YARPPプラグインもMyISAMしか対応していませんでした。

※InnoDBの場合、管理画面上で動作しない旨の警告が表示されます。

WordPressのサーバー拡張

ランサーズのWordPressサーバーは、最初はAWSのEC2を1台で運用していました。
しかし、事業拡張に伴い、様々なサービスをWordPressで構築することになり、アクセス数も増えてきたため、徐々にサーバーリソースが足りなくなり、AWSのマネージドサービスと連携したサーバー拡張を行いました。

※この時の詳細な内容はエンジニアブログの以下にまとめています。
AWSでWordPressのスケールアウト

 

その過程で、EC2内にインストールしていたMySQLをRDSに引っ越すことになりました。

 

しかし、RDSのMySQLはInnoDBしかサポートしていません。
(MyISAMが使えないわけではないですが、サポート対象外になります。)

RDSに移行するのであれば、DBの全テーブルをInnoDBに全て移行しておきたいところですが、ここでYARPPプラグインがネックになりました。

MySQL InnoDBの全文検索サポート

しかし、MySQLもバージョン5.6.4からInnoDBの全文検索機能(InnoDB FTS)がサポートされるようになりました。
MyISAMの全文検索同様、SQLのMATCH~AGAINST構文で利用することができ、互換性があります。

YARPPは世界的にそこそこ有名なプラグインなので、InnoDBへの対応が期待されていました。

Title and body indexes with InnoDB
https://wordpress.org/support/topic/title-and-body-indexes-with-innodb/

Support for InnoDB with full-text index
https://wordpress.org/support/topic/support-for-with-full-text-index/?replies=4

しかしながら、対応される様子はありませんでした。

YARPPプラグインのInnoDB対応

RDSへの移行を進めるため、YARPPプラグインに手をいれてInnoDB対応をすることにしました。

この時の詳細は、以下の記事に記載しています。
http://qiita.com/yKanazawa/items/70686b13b17e7bd2e9e6

プラグインのマージを依頼

YARPPプラグインのInnoDB対応に需要があることは分かっていたので、この修正をプラグインの作者にマージしてもらおうと思いました。

GitHubであれば、

– 本家からFork
– 修正してPull Request
– レビューしてもらい、OKならマージして取り込み

のようなフローになるので、そのイメージでWorpressコミュニティ周りを調査し始めました。

その結果、WordPressは未だに、SVNで管理していることが分かってきました。
http://core.svn.wordpress.org/

そして、WordPressプラグインはTracで管理されているようでした。
https://plugins.trac.wordpress.org/

YARPPのフォーラムを見つけたので、見よう見まねでマージを依頼してみました。
https://plugins.trac.wordpress.org/ticket/2655

ソースの差分はGitHub上に提示するというドロくささ。
https://github.com/yKanazawa/yet-another-related-posts-plugin/pull/1/files

その結果

めでたくマージされ、YARPPプラグインはInnoDB対応の最新版になりました!

と言いたいところでしたが、結局、なしのつぶてで、マージされることはありませんでした。

そして、2018/10/23にYARPPプラグインは見事に廃止となりました。
https://wordpress.org/plugins/yet-another-related-posts-plugin/

もはや、WordPressのサイトからこのプラグインをダウンロードすることはできなくなりました。
とはいえ、今も現役で利用しているので、こっそりとメンテナンスは続けていきたいと思います。

もはや本家もないし、GitHubのマージボタンに手を伸ばし、自分のMasterに取り込み完了。
https://github.com/yKanazawa/yet-another-related-posts-plugin

※追記:2019/5に復活したようです。

YARPP – Yet Another Related Posts Plugin

お知らせ

2019年1月26日(土) に行われるPHPカンファレンス仙台に登壇します。

https://phpcon-sendai.net/2019/
Track C 10:25-10:55
AWSでWordPressのスケールアウト

この記事でもお話していました、WordPressのスケールアウトについて、最新情報とセキュリティ対策を交えてお話する予定です。
地元開催なので楽しみにしています。

ランサーズ版SQLチューニングポリシー

kanazawa|2018年12月05日
MySQL

SREチームの金澤です。
Lancers(ランサーズ) Advent Calendar 2018 5日目の記事になります。

昨日は、inamuuさんのElasticCloudからAmazon Elasticsearch Serviceへの移行して良かったこと大変だったことでした。

今年は、Lancers以外のAdvent Calendarも参加させていただいてます。(以下)

本日の記事は、CakePHP Advent Calendar 5日目との掛け持ちにしようと思っていたのですが、それができないことが今更ながらわかりました。なんという失態。。

なので、お蔵入りしてした社内用ドキュメント(SQLチューニングポリシー)を公開することにしました。(なぜそうなるのかの考察をもっとしたいものではありましたが。。。)

なお、この記事は

と関連する記事でもあります。


イントロダクション

このドキュメントは、アプリエンジニアが新機能を実装する際の資料として用意したものです。

過去、新規機能実装時に、新規テーブルに適切なインデックスが付与されていなかったり、不用意に負荷のかかるSQLを発行していたことが多かったため、実装前の参考資料として書いていました。

※ドキュメント作成時のMySQLのバージョンは5.6です。

本番DBでの事前検証

EXPLAIN文での確認

実装時に、負荷のかかりそうなSQLを発行する場合は、本番DBのレプリカで事前に実行計画を確認しておいてください。

mysql> EXPLAIN SELECT …;

EXPLAIN文については、以下のページに詳しい解説があります。

漢(オトコ)のコンピュータ道:MySQLのEXPLAINを徹底解説!!

レスポンス確認

同様に、実際のレスポンスタイムを本番DBのレプリカで事前に確認しておいてください。

SQLをチューニングして、EXPLAIN上は改善していても、実際のレスポンスは改善しないパターンもあります。その場合は、レスポンスタイムの早いほうを選択してください。


SQL発行時の注意点

日付型カラムでのインデックス範囲検索は1ヶ月以内が目安

1ヶ月以上広範囲になると、インデックスが採用されずフルスキャンがかかる可能性が高くなります。

※データ量や統計情報に依存します。開発環境で適切にインデックスが採用されていても本番環境で同じ動作をするとは限らないので、事前に本番DBのレプリカでも検証しておいてください。

※例:proposalsテーブルのcreated範囲検索にインデックスが採用されない例

mysql> EXPLAIN SELECT `Proposal`.`id`, `Proposal`.`string` FROM `proposals` AS `Proposal` WHERE Proposal.created > '2014-11-01 00:00:00';
+----+-------------+----------+-------+------------------------+------------------------+---------+------+--------+-----------------------+
| id | select_type | table    | type  | possible_keys          | key                    | key_len | ref  | rows   | Extra                 |
+----+-------------+----------+-------+------------------------+------------------------+---------+------+--------+-----------------------+
| 1  |      SIMPLE | Proposal | range | proposal_index_created | proposal_index_created |       6 | NULL | 221634 | Using index condition |
+----+-------------+----------+-------+------------------------+------------------------+---------+------+--------+-----------------------+
1 row in set (0.01 sec)

FORCE INDEX(USE INDEX)を使うときの注意点

MySQLがどうしても適切なインデックスを選択してくれない場合、FORCE INDEX(USE INDEX)を使うことで、強制的にインデックスを選択させる方法があります。

ただし、FORCE INDEX(USE INDEX)を使うと、将来的に以下のようなデメリットが生じるので、あくまでも一時的な手段にしておいてください。

  • インデックスチューニングをしても、FORCE INDEXを指定しているクエリは改善されない
  • FORCE INDEXに指定したインデックスを削除するとエラーを引き起こす可能性がある
    • FORCE INDEX(PRIMARY)は大丈夫
    • FORCE INDEX(インデックス名)のインデックス名が削除されるとエラーになる

理想的なのは、FORCE INDEXを使わず、MySQLが自然に適切なインデックスを選択できるようなインデックス構成やSQLにしておくことです。

上記の注意点を理解した上で、一時的なパフォーマンス対策の手段として利用してください。


テーブル作成時のインデックス付与の指針

やみくもにインデックスを付与しない

インデックスを付与すると、データ更新時の負荷が増えます。
マスターDBに負荷をかけることになりますので計画的に付与する必要があります。
特に更新頻度の多いテーブルは注意が必要です。

パフォーマンスが悪いからと、やみくもにインデックスを付与してしまうと、MySQLの実行計画が狂い、他のSQLにも悪影響を与える可能性があります。

createdでソートする目的でインデックスを付与しない

PRIMARY KEYであるidカラムでも同様のソートが可能です。

※抽出目的でcreatedに付与する場合は、ソートもcreatedで統一します。

カーディナリティの低いカラムにはインデックスを付与しない

例えば、各テーブルのdeletedカラムは、カーディナリティが2しかない(0か1の値しか持たない)ため、インデックスの付与はほとんど効果がありません。

むしろ、MySQLが誤ってこのインデックスを採用してしまい、パフォーマンスが落ちることもあります。

※ただし、0より1の割合が圧倒的に少数であるカラムに対し、かつ1での検索が大半を占める場合などは、カーディナリティが低くても有効に機能しますのでその条件を満たす場合は付与できます。

複合インデックスの採用条件

MySQLでは、原則1テーブルにつき1つしかインデックスを使わないため、複数カラムにインデックスを効かせるためには複合インデックスを付与する必要があります。

しかしながら、MySQLの場合は複合インデックスを作成すると、既存のSQLの実行計画が変更され、場合によっては既存SQLのパフォーマンスが悪化する可能性があります。

パフォーマンス検証スクリプト(※独自に用意した、サービス全画面のSQLを実行するスクリプト)を実行し、大幅にパフォーマンスが悪化するSQLがないことを確認しておきましょう。

また、複合インデックスは更新負荷が大きくなりますので、採用頻度と合わせて慎重に検討が必要です。

複合インデックスを採用する場合、先頭カラムの単一インデックスは削除する

複合インデックスの先頭カラムは単一インデックスとしても作用するので、複合インデックスを付与する場合は、先頭カラムの単一インデックスが存在していたら削除しておくことで、インデックス量が減り、マスターDBの更新負荷が軽減されます。

ただし、単一インデックスのときよりもパフォーマンスが落ちる場合もあるので、事前検証が必要です。

複合インデックスは、カーディナリティの高いカラム順に並べる

複合インデックスを付与する場合は、できるだけカーディナリティの高い順に並べてください。
発行するSQLによるので、どんなときも適用できるルールではありませんが、極端にカーディナリティの低いカラムを先頭にすると効果が出にくくなります。 (MySQLが優先的に採用してしまい、効果が落ちることもある)

以下、categorizationsテーブルの(よくない)例

mysql> SHOW INDEX FROM categorizations;
+-----------------+------------+------------------------------------------------------------------+---------------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| Table           | Non_unique | Key_name                                          | Seq_in_index | Column_name         | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+-----------------+------------+------------------------------------------------------------------+---------------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| categorizations |          0 | PRIMARY                                           |            1 | id                  | A         |     1295306 |     NULL |   NULL |      |      BTREE |         |               |
| categorizations |          1 | category_id                                       |            1 | category_id         | A         |         164 |     NULL |   NULL |      |      BTREE |         |               |
| categorizations |          1 | categorization_type                               |            1 | categorization_type | A         |           6 |     NULL |   NULL | YES  |      BTREE |         |               |
| categorizations |          1 | categorization_id                                 |            1 | categorization_id   | A         |     1295306 |     NULL |   NULL | YES  |      BTREE |         |               |
| categorizations |          1 | category_id_categorization_type_categorization_id |            1 | category_id         | A         |         154 |     NULL |   NULL |      |      BTREE |         |               |
| categorizations |          1 | category_id_categorization_type_categorization_id |            2 | categorization_type | A         |         398 |     NULL |   NULL | YES  |      BTREE |         |               |
| categorizations |          1 | category_id_categorization_type_categorization_id |            3 | categorization_id   | A         |     1295306 |     NULL |   NULL | YES  |      BTREE |         |               |
+-----------------+------------+---------------------------------------------------+--------------+---------------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
7 rows in set (0.01 sec)

お知らせ

12/15(土)にPHPカンファレンスに登壇いたします。
http://phpcon.php.gr.jp/2018/

13:00 ~ 13:25 Track 6 3F特別会議室
ランサーズのCakePHP1.3→Cake2.8移行

ランサーズでは、約1年かけて、CakePHP1.3をCakePHP2.8への移行を進めており、現在の進捗は99%です。(今年中には完了させたい)

カンファレンスでは、移行で得たノウハウを詳細にお話したいと思います。
明日は、odrum428さんの「slackで動くピアボーナス機能を実装した話」になります。

ランサーズ開発合宿2018 バージョンアップチーム

kanazawa|2018年08月30日
CakePHP

SREチームの金澤です。

ランサーズ開発合宿のバージョンアップチームの成果についてお話させていただきます。

ランサーズのバージョンアッププロジェクト

現在、ランサーズのバージョンアップはSREチームが主導で行っています。

バージョンアップは、以下のフェーズにわけて進行中です。

  1. CakePHP 1.3 → CakePHP 2.8
  2. PHP 5.3 → PHP 5.6
  3. CakePHP 2.8 → CakePHP 2.x最新
  4. PHP 5.6 → PHP 7 最新

そして、現在、1を進めている最中です。
コントローラー単位で移行を進めており、2018/6時点での進捗は70%でしたが、残る30%については、難易度が高いコントローラー、バッチ、管理画面などが含まれていました。

2018年度のSREチーム

2018/4から4人体制でSREチームが発足しました。
※その詳細は、「SREチームの発足」に書かせていただきました。

そして、バージョンアッププロジェクトをSREチームで担当することになりました。

現在、SREチームは

SRE
CRE
バージョンアップ

の3部門を抱えていて、それぞれがお互いの領域を跨ってカバーしている体制になっています。

※CREチームについては、また別途機会がありましたらお話ししたいと思います。

2018年度のバージョンアップ体制

ランサーズのCakePHP 1.3 → 2.8 バージョンアッププロジェクトは、当初3人でスタートしましたが、現在は、エンジニア全員での総力体制で進めています。

※この経緯については「PHPカンファレンス福岡2018に登壇しました」に書かせていただきました。

バージョンアップを専任で行っているメンバーは現在1人しかいないのですが、これまでの蓄積からある程度手順がパターン化できており、そのノウハウをGithub Wikiにまとめ、エンジニア全員で担当できる体制を整えています。

合宿の方針

総力体制とはいえ、普段の業務でバージョンアップ作業に充てられるのは、SREチームのエンジニアで30%くらい、その他のエンジニアは10%くらいという感覚です。

開発合宿は、普段の業務ではできないことを集中して行うことができる場です。
この機会に一気にバージョンアップを進めたいと思っていました。

今までの合宿では、ほぼ全員が、普段できないようなクリエイティブなテーマで取り組んでいました。
それはそれで楽しいのですが、今年の合宿は過去最大の人数で行うこともあり、確実にアウトプットを出したいと考えていました。

合宿は1泊2日で行われますが、実質的な作業時間は6時間+αくらいしかありません。
限られた時間で、できる限りのアウトプットを出すため、以下の方針で進めることにしました。

バッチのバージョンアップにリソースを集中

バッチのバージョンアップは、比較的パターンが単純で、検証もしやすいため、今回の合宿ではバッチのバージョンアップに全リソースを集中することにしました。

事前にアサイン

事前に、どのバッチを誰が担当するかを決めておきました。
バッチのバージョンアップは、ソース修正自体はそれほど難しいものではないのですが、動作確認を適切にできるかがポイントになります。
バッチを作った人が内容を一番理解しているので、基本的にはバッチを作った人に優先的にバージョンアップをアサインするようにしました。

フライング

合宿は時間が限られているので、何も準備せずに行くと、開発環境のセットアップだけで終わってしまうことにもなりかねません。
各担当者には、事前に1つ以上のバッチのバージョンアップを消化してもらい、一通りのフローを理解してから合宿に臨みました。

現地でリアルタイムリリース

原則、リリース作業は社内でしかできないようにしていますが、今回は合宿時でリリースまでしてしまうことで、確実にアウトプットを確定するようにしました。

そのために、熱海に「槍」を持ってきました。

ランサーズでは、リリース時に「槍」を持つことでリリースロックを実現しています。
古典的な方法ですが、カナリア確認時に別の人が重複リリースしてトラブルになることを防止しています。

現地到着後、リリースできる環境をSREチームが10分で構築しました。

 

結果

バッチだけで20リリースできました!

リリースすると、全社チャットにリリース通知が来るので、休日にも関わらず開発部が猛烈な勢いで進めていることをアピールでき、合宿がどれほど効果が高いイベントかをアピールできたかと思います。

バージョンアップの進捗は、72% → 80%になりました。
1年がかりで進めているプロジェクトが、わずか1日で8%進んだことになります。

この合宿をあと3回やりたい気分です。

おまけ

夕食後の1コマ。

ストIIとぷよぷよは世代関係なく遊べますね。

SREチームの発足

kanazawa|2018年06月25日
DevOps

SREチームの金澤です。

2018年度より、SREチームを発足しました。
その経緯をお話ししたいと思います。

インフラエンジニアとして

私は、2013年11月にランサーズに入社しました。
ランサーズ5年目にして、サービスが本格的に伸び始めた時期で、アプリエンジニアが運用を兼務するには荷が重くなり始めており、専任のインフラエンジニアが必要でした。

入社後、4年半にわたり、サービスの安定化や負荷対策、最新の技術やサービスへの追従など、様々な施策を実行してきました。

スタートアップのインフラエンジニアに求められる仕事は多岐にわたります。
時には、サービスのソースコードに手を入れることも度々ありました。
また、この時期のランサーズは、開発環境の支援や、社内インフラの整備もできる人材が不足しており、そのような業務もインフラエンジニアである私が担っていました。

インフラエンジニア採用の難しさ

2017年度のインフラチームは、新卒、インターン含めて4人でしたが、来年度の体制変更に伴い、再び1人になることがほぼ確定していました。

そのため、経験豊富なインフラエンジニアを採用することが急務になりました。

しかしながら、スタートアップにマッチするインフラエンジニアは採用が非常に難しいです。
今までも採用活動を継続的に行ってきましたが、なかなか採用までに至らず、苦戦していました。

スタートアップでは、特定の分野に精通するだけでなく、複数の分野の業務をこなす必要があります。
今思えば、インフラエンジニアという枠組みで採用を試みた結果、書類選考の過程で多くのミスマッチが生じていたと思います。

スタートアップとSRE

ここ数年で、SRE(Site Reliability Engineering)という言葉が浸透してきました。
Googleが提唱した言葉で、オライリーから書籍が出版されて、日本でもより認知度が広まってきたと思います。


私自身、SREという言葉は最近認知したのですが、この書籍を読み、スタートアップ企業に必要なエッセンスがたくさん含まれていると感じました。

そして、ランサーズのインフラチームが担ってきた

  • 新サービスのローンチ支援
  • リリースシステムの構築
  • 定型作業の自動化
  • 開発環境の構築
  • 分析基盤の運用
  • セキュリティの強化

などは、ランサーズが成長する過程で積み重ねてきた施策であり、SREそのものでした。

SRE(Site Reliability Engineer)の採用

ランサーズに必要なエンジニア像として、SREはとても腑に落ちるものであることに気づき、今後はSRE(Site Reliability Engineer)というキーワードで採用を行おうと決意しました。

これまでの集大成として、2月にTECH PLAYでSREをテーマにしたイベントを行わせていただきました。

その時のスライドがこちらになります。

また、採用方法もエージェントに頼らず、ダイレクトリクルーティングによる採用に切り替えました。
エンジニアが自ら採用活動を行うため、エンジニアの負担が大きくなるのですが、採用の初期段階からエンジニアが参画するため、ミスマッチが大幅に減り、精度の高い採用ができるようになったと思います。

SREチームの発足

その結果、2018年度より新規に優秀なSREメンバーが3名加わり、正式にSREチームが発足しました。
当初、1人採用できればと思っていたので、大変嬉しく思います。

今年度のSREチームは、PHPバージョンアップやCRE(Customer Reliability Engineering)チームとの連携も含め、幅広いミッションが与えられましたが、未来につながる施策も含めて、今後進めていきたいと思います。

※次回は、SREチームの新メンバーにブログを書いてもらいます。

お知らせ

7/3に行われるSRE Lounge #4に登壇させていただくことになりました。
https://sre-lounge.connpass.com/event/91566/

おかげさまで満員御礼となりました。

皆さんに少しでも有益な情報が提供できるように頑張りたいと思います。

PHPカンファレンス福岡2018に登壇しました

kanazawa|2018年06月18日
CakePHP

SREチームの金澤です。

2018/06/16(土)に、PHPカンファレンス福岡に登壇させていただきました。

昨年に続き、2度目の登壇となりました。
この度は、採択していただきありがとうございました。

登壇内容

CakePHP 1.3 + PHP 5.3 → CakePHP 3 + PHP 7 バージョンアップ報告

Fusicホール/16:15~16:45(30分)

PHPバージョンアップ全般に関しては金澤が発表し、CakePHPのバージョンアップについてはアプリエンジニアの小林が発表しました。

昨年、バージョンアップすることを決定し、エンジニアブログPHPカンファレンス福岡で社外に公表し、外部のPHP有識者の方々から助言をいただきながら進めてきました。

昨年の発表時は、まだ前準備のフェーズで、実際のバージョンアップに取り掛かったのは、2017/7頃からでした。
SREチームでは、PHP5.3 → 5.6 のバージョンアップから提案しましたが、アプリチームと協議した結果、CakePHP1.3 → 2.8から始めることにしました。

CakePHP1.3 → CakePHP2.8の移行作業は、共存させながらコントローラー単位で徐々に行っております。
普段の開発を止めずに同時並行でバージョンアップを行うためにこの方法を採用しました。

このあたりの経緯は、昨年のPHPカンファレンス東京でも発表させていただきました。

バージョンアップ開始当初は、専任の担当が3人で進めていましたが、2018/4から正式にSREチームを発足し、バージョンアッププロジェクト全体を担当しています。
現在は、手の空いたエンジニアにスポットでバージョンアップを依頼し、片手間ながらほぼ総力体制で取り掛かっています。
※今までに行ってきたバージョンアップの手順がほぼ確立されていたので、Github Wikiに手順を集約することで、難しいコントローラーでなければ、新卒エンジニアでも手伝える環境になっています。

2018/6時点で、CakePHP1.3 → CakePHP2.8の移行進捗は約70%です。
今後の展望としては、CakePHP1.3 → CakePHP2.8移行が完了したら、

PHP 5.3 → 5.6
CakePHP2.8 → 2.10
PHP 5.6 → 7.2

とバージョンアップを進めていく予定です。

登壇後、今年もたくさんの方にアドバイスをいただきました。
大変感謝しております。

年単位のプロジェクトでなので、時に会社の事情に左右されることもありますが、今後もプロジェクトを停滞させずに進めていきたいと思います。

CloudFront

EC2オリジンのCloudFrontで静的ファイルをキャッシュした話

kanazawa|2017年12月11日
AWS

ランサーズ Advent Calendar 2017 11日目の記事です。

インフラエンジニアの金澤です。

CloudFrontでサムネイルをキャッシュした話に続きまして、静的ファイルをキャッシュした手順も記録として残しておきたいと思います。

導入に至った経緯

Appサーバーの負荷とレスポンス遅延

当時は、サーバー費用をそこまでかけられないフェーズで、なるべく抑えた運用を心掛けていました。

AppサーバーのEC2は、CPU使用率が80%~90%になってもサービスが落ちたりすることはありませんが、50%を超えるとサーバーレスポンスに影響が出始めます。

レスポンスを悪化させないためには、CPU使用率はなるべく50%以下に抑えたいところです。

静的ファイルのサーバー分離

静的コンテンツへのリクエスト数は、動的コンテンツの10倍以上あります。

静的ファイルのアクセスをAppサーバーから分離させればAppサーバーの接続数を大幅に削減でき、リソースを動的コンテンツに集中させることができます。

Webサイトホスティング設定をしたS3に分離する方法もありましたが、リリースフローが複雑になります。

CloudFrontにキャッシュさせれば、リリースフローも変更する必要がなく、エッジローケーションからの配信になるのでブラウザレスポンスの改善も期待できます。

CloudFrontの費用

img.lancers.jpにサムネイルをキャッシュしたときの月額費用は約月4万円(当時)でした。

サムネイルのアクセスと静的コンテンツのアクセス数は大体同じくらいでした。

CloudFrontが値下げされた時期でもあり、Appサーバー1台分よりも費用が抑えられそうな見積りができ、CloudFrontにキャッシュさせる案を採用することにしました。

導入手順

CloudFront用のドメインを定義

静的ファイル用に以下のドメインを定義します。

  • static.lancers.jp
    • CloudFrontのAレコードAlias
  • static-origin.lancers.jp
    • WWWサーバーで設定するドメイン

www.lancers.jpドメインでアクセスしていた静的ファイルを、以下のようにstatic.lancers.jpドメインでアクセスさせるようにします。

https://www.lancers.jp/touch_icon.png

https://static.lancers.jp/touch_icon.png

static.lancers.jpドメインでアクセスすると、CloudFrontを経由してキャッシュされ、2回目以降はCloudFrontのエッジロケーションから配信されます。

WWWサーバーの設定

static-origin.lancers.jp用のホスト設定を定義します。
Nginxの場合は以下のようになります。(動的コンテンツのphpはアクセスできないように設定)

server {
    listen 80;
    server_name static-origin.lancers.jp;
    root /var/www/lancers/app/webroot;
 
    location ~ \.(css|gif|ico|jpeg|jpg|js|otf|pdf|png|svg|swf|ttf|woff|woff2|zip)$ {
        expires max;
    }
 
    location ~ \.php$ {
        deny all;
    }
 
    location ~ /\. {
        deny all;
    }
}

CloudFrontのディストリビューションを作成

static-origin.lancers.jpをオリジンとしたCloudFrontのディストリビューションを作成します。
static.lancers.jpのAレコードAliasにCloudFrontのドメインを関連付けます。

静的ファイルのアクセスをstatic.lancers.jpに変更

www.lancers.jp でアクセスされていた静的ファイルをstatic.lnacers.jpに変更します。
静的ファイル読み込み箇所のソースを以下のように修正します。

<img src="/touch_icon.png">
↓
<img src="<?= Configure::read('static.url') ?>/touch_icon.png">

この対応は1300箇所以上あり、地道な作業になりました。
ただし、一度に全て変更する必要はないため、徐々に対応していきました。

導入後の問題とその解決方法

クエリストリングのハッシュ化

CloudFrontのキャッシュ更新は、img.lancers.jpのときと同様、クエリストリングを付与し、静的ファイル更新時にクエリストリングも更新させる必要があります。

しかし、この更新作業が開発者、特にデザイナーの方々の実装負荷を上げてしまうことになりました。

img.lancers.jpのときは、更新日時をDBから取得していましたが、静的ファイルの場合は?v1.4のようなバージョン表記にしたため、都度更新する必要があります。

そこで、アプリエンジニアの方に作っていただいたのが、git mergeのタイミングでハッシュ値を生成させる仕組みです。

具体的には、.git/hooks/post-merge でcommit_hash.phpというファイルを生成します。

commit_hash.php には、以下の様なハッシュ値の定義が出力されます。

Configure::write('static_file_version', '78152cf38634670a1a631275f1288f34a9d71e90'));

このハッシュ値をクエリストリングにすることで、リリース毎にクエリストリングが切り替わる仕組みです。
https://static.lancers.jp/touch_icon.png?v=78152cf38634670a1a631275f1288f34a9d71e90

リリースの順番

静的ファイルにCloudFrontを適用後、リリース時にハッシュ値を更新したにも拘わらず、キャッシュが更新されない事象が度々発生していました。

全てのソースを転送し終わる前にcommit_hash.phpを転送してしまうと、静的画像が更新される前にクエリストリングのハッシュ値が更新されてしまい、古い静的画像が再びキャッシュされてしまうためです。

リリース時は、最新のソースを全て転送し、最後にcommit_hash.phpを転送する必要があります。

また、Appサーバーが複数ある場合は、各Appサーバーへのリリースの時間差で同様の問題が発生することもあります。
commit_hash.phpの転送は、全サーバーへ極力同じタイミングで行う必要があります。

※シンボリックリンクを切り替えるリリース方式の場合も同様に、切り替えを最後に、そして極力同時に行う必要があります。

CORS設定

例えば、WEBフォントのwoffファイルは、サイズが大きい為、特にCloudFrontにキャッシュさせたいファイルですが、当初、woffファイルをstatc.lancers.jpで参照させるとCORSのエラーが出力されていました。

この問題を解決させるためには、static.lancers.jp側にwww.lancers.jpからの利用を許可させる必要があります。

具体的には、以下のようにAccess-Control-Allow-Originヘッダを付与する必要があります。

Access-Control-Allow-Origin: https://www.lancers.jp

Nginx側で以下のように設定しました。

server {
    listen 80;
    server_name static-origin.lancers.jp;
    root /var/www/lancers/app/webroot;
 
    set $cors "";
    if ($http_origin ~* https://www\.lancers\.jp$) {
        set $cors "true";
    }
 
    location ~ \.(css|gif|ico|jpeg|jpg|js|otf|pdf|png|svg|swf|ttf|woff|woff2|zip)$ {
        if ($cors = "true") {
            add_header Access-Control-Allow-Origin "$http_origin";
        }
        expires max;
    }
…

最後に

静的ファイルのアクセス負荷対策として、WebサーバーとAppサーバーを分離させる方法がよく紹介されていますが、CloudFrontを導入すれば、Webサーバー側の役目をほぼ担ってくれます。

大きなサーバー構成の変更が必要なく、ブラウザレスポンスの改善も期待できますので、CloudFrontにキャッシュさせるという方法も選択肢の一つになるかと思います。

CloudFront

EC2オリジンのCloudFrontでサムネイルをキャッシュした話

kanazawa|2017年12月04日
Apache

ランサーズ Advent Calendar 2017 4日目の記事です。

インフラエンジニアの金澤です。
少し古いネタになりますが、CloudFrontでサムネイルをキャッシュした手順を記録として残しておきたいと思います。

サムネイルの生成処理について

ランサーズは、2012年5月にAWSに移行しました。
ランサーズでは、プロフィール画像や提案画像をサムネイル処理しています。
例えば、ランサーズのコンペの閲覧一覧で閲覧できるロゴ等の提案画像は、圧縮、縮小されたサムネイル画像です。
(オリジナル画像はS3にあり、仕事を依頼したクライアントしか見ることができません)

これらのサムネイル画像は、オリジナル画像をImageMagickで圧縮、縮小して表示します。
この処理は大きな負荷がかかるため、一度作成したサムネイルはNFSに保管しキャッシュしていました。

サムネイル生成とキャッシュ

ランサーズでは、サムネイル画像をimg.lancers.jpというドメインで管理し、URLでアクセスされたタイミングでサムネイルを生成し、NFSに保管しています。
Apacheで、以下のように設定すると、

<VirtualHost *:80>
    ServerName img.lancers.jp
    DocumentRoot /var/www/lancers/app/tmp/thumbs
    <IfModule mod_rewrite.c>
        RewriteEngine On
        RewriteCond %{DOCUMENT_ROOT}/%{REQUEST_FILENAME} !-f
        RewriteRule ^/([a-z]+)/([0-9a-f]+)/([0-9a-f]+)/([0-9a-f]+)_([0-9]+)_([0-9]+)([_]*)([0-9]*).jpg$ http://www.lancers.jp/file/$1/$5/$6/$8 [R]
    </IfModule>
        <Directory /var/www/lancers/app/tmp/thumbs>
            Order allow,deny
            Allow from all
        </Directory>
</VirtualHost>

以下のように動作します。

  • NFSにサムネイルがキャッシュされていない場合
    • www.lancers.jpにリダイレクト
    • PHP経由でサムネイルを生成して返す
    • 生成したサムネイルをNFSにキャッシュ

  • NFSにサムネイルがキャッシュされている場合
    • キャッシュのサムネイルをそのまま返す

NFSのストレージ逼迫問題

サービスが成長していくにつれ、サムネイルの数も増え、NFSのストレージ容量が逼迫してきました。
当時のEC2は、ストレージ容量を拡張するには、一度AMIを取得してから再作成しなければなりませんでした。
NFSは常時稼働しているので、メンテナンスを入れる必要がありましたが、AMIの取得に3時間以上かかり、メンテナンス時間内に終わるか見積りが難しく、キャッシュ期間を短くすることで対応していました。
また、当時のEC2は1TBまでしかストレージを拡張できなかったので、拡張できてもいつかは限界が来ることが見えていました。

キャッシュをNFSからCloudFrontへ

ストレージ逼迫問題を解決する手段として、サムネイルもS3に格納する方法がまず思い浮かびます。
ですが、ストレージをS3にするとサムネイル存在チェックの遅延時間が長くなるため断念したそうです(当時の担当者談)
そこで、サムネイルをCloudFrontにキャッシュさせ、NFSを撤廃することを試みました。
img.lancers.jpを、ELBからCloudFrontのアドレスに変更し、ELBにはimg-origin.lancers.jpというアドレスを対応させます。

  • CloudFrontにサムネイルがキャッシュされていない場合
    • PHP経由でサムネイルを生成して返す
    • 生成したサムネイルはCloudFrontにキャッシュされる

  • CloudFrontにサムネイルがキャッシュされている場合
    • CloudFrontのキャッシュが返される
      • ELB、Appサーバーには到達しない


Apacheの設定を以下のように変更します。
※/var/www/lancers/app/tmp/thumbs はNFSから各EC2サーバーのディレクトリに設定変更し、サムネイル生成時の一時保管場所としてのみ利用します。

<VirtualHost *:80>
    ServerName img-origin.lancers.jp
    DocumentRoot /var/www/lancers/app/tmp/thumbs
    <IfModule mod_rewrite.c>
        RewriteEngine On
        RewriteCond %{DOCUMENT_ROOT}/%{REQUEST_FILENAME} !-f
        RewriteRule ^/([a-z]+)/([0-9a-f]+)/([0-9a-f]+)/([0-9a-f]+)_([0-9]+)_([0-9]+)([_]*)([0-9]*).jpg$ http://www.lancers.jp/file/$1/$5/$6/$8 [R]
    </IfModule>
    <Directory /var/www/lancers/app/tmp/thumbs>
        Order allow,deny
        Allow from all
    </Directory>
</VirtualHost>

ところが、この設定だとCloudFrontにはキャッシュされません。
img.lancers.jp→www.lancers.jpにリダイレクトされてしまうため、サムネイルの生成過程でCloudFrontを経由しなくなるためです。
img.lancers.jpドメインを維持したままサムネイルを生成させるため、リダイレクトではなく自分自身へプロキシさせる形にしました。

このときのApacheの設定は以下のようになります。

<VirtualHost *:80>
    ServerName img-origin.lancers.jp
    DocumentRoot /var/www/lancers/app/tmp/thumbs
    <IfModule mod_rewrite.c>
        RewriteEngine On
        RewriteCond %{DOCUMENT_ROOT}/%{REQUEST_FILENAME} !-f
        RewriteRule ^/([a-z]+)/([0-9a-f]+)/([0-9a-f]+)/([0-9a-f]+)_([0-9]+)_([0-9]+)([_]*)([0-9]*).jpg$ http://127.0.0.1/file/$1/$5/$6/$8 [P]
    </IfModule>
    <Directory /var/www/lancers/app/tmp/thumbs>
        Order allow,deny
        Allow from all
    </Directory>
</VirtualHost>

※Nginx化したときは、以下のように設定しました。

server {
    listen 80;
    server_name img-origin.lancers.jp;
 
    root /var/www/lancers/app/tmp/thumbs;
 
    location ~ ^/([a-z]+)/([0-9a-f]+)/([0-9a-f]+)/([0-9a-f]+)_([0-9]+)_([0-9]+)([_]*)([0-9]*).jpg$ {
        limit_req zone=one burst=100;
        proxy_set_header X-Forwarded-Server $host;
        proxy_pass http://127.0.0.1/file/$1/$5/$6/$8;
    }
 
    location ~ \.php$ {
        deny all;
    }
 
    location ~ /\. {
        deny all;
    }
}

CloudFrontのTIPS

証明書のアップロード

AWSの証明書の場合

CloudFrontで、AWSのCertificate Managerで作成したSSL証明書を利用したい場合は、「バージニア北部」のリージョンで作成する必要があります。

その他の証明書の場合

ELBとCertificate ManagerCloudFrontで共通の証明書を使いたい場合、マネジメントコンソールではなく、AWS CLIで登録する必要があります。
※詳細はQiitaに書かせていただきました。https://qiita.com/yKanazawa/items/f4362a21ead88888bffd

キャッシュの更新

一度CloudFrontにサムネイルがキャッシュされると、サムネイル画像が更新されても同じURLでは画像が切り替わりません。
そこで、URLにクエリ文字列を付与して対応します。
参考:クエリ文字列パラメータに基づいてキャッシュするように CloudFront を設定する
CloudFrontのBehavior設定で、以下のように設定しておくと、クエリ文字列が変更されたときに、再度オリジンのEC2に読み込みにいきます。

ランサーズでは、サムネイル画像の更新時に更新日時をクエリ文字列に付与して対応しています。
https://img.lancers.jp/userprofile/6/c/6ccc3…e7515_225170_130.jpg?20171130092919

キャッシュの確認

アクセスした画像が、CloudFrontのキャッシュが返したものなのか、オリジンのEC2が返したものなのかを確認したい場合は、Response HeadersのX-Cacheヘッダを確認します。

  • CloudFrontのキャッシュから返された場合
    • X-Cache: Hit From CloudFront
  • オリジンのEC2から返された場合
    • X-Cache: Miss From CloudFront

Chromeの場合は、デベロッパーツールの「Network」タブで確認できます。

アクセスログの取得

CloudFrontを経由すると、WebサーバーのアクセスログのRefererがすべて”Amazon CloudFront”になってしまいます。
(リモートIPアドレスも、CloudFrontのエッジロケーションのものになります)

10.0.x.xx [10/Jun/2014:17:32:30 +0900] 6.124.41.xx.xxx - - [10/Jun/2014:17:32:30 +0900] "GET / HTTP/1.1" 200 24154 "-" "Amazon CloudFront" xxx ms
10.0.x.xx [10/Jun/2014:17:31:39 +0900] 60 66.249.xx.xxx - - [10/Jun/2014:17:31:39 +0900] "GET /proposal/search/award HTTP/1.1" 200 12331 "-" "Amazon CloudFront" xxx ms
10.0.x.xx [10/Jun/2014:17:32:09 +0900] 60 66.249.xx.xxx - - [10/Jun/2014:17:32:09 +0900] "GET /proposal/search/award HTTP/1.1" 200 12331 "-" "Amazon CloudFront" xxx ms

Refererを確認するときは、CloudFrontのアクセスログをS3から取得する必要があります。

アクセスログの設定

CloudFrontのGeneral設定で、以下のように設定すれば

S3バケットにログが格納されます。

アクセスログフォーマット

以下のフォーマットで保存されています。

#Version: 1.0
#Fields: date time x-edge-location sc-bytes c-ip cs-method cs(Host) cs-uri-stem sc-status cs(Referer) cs(User-Agent) cs-uri-query cs(Cookie) x-edge-result-type x-edge-request-id x-host-header cs-protocol cs-bytes time-taken
2014-10-01  19:50:21    MIA50   417 66.249.71.8 GET xxxxxxxxxxxxx.cloudfront.net    /proposal/f/e/fec...ec2_3989074_150.jpg  307 -   Googlebot-Image/1.0 20140929163745?timestamp=1410048000048  -   Miss    dxn...maQ==    img.lancers.jp  http    307 0.400
2014-10-01  19:50:22    MIA50   417 66.249.71.8 GET xxxxxxxxxxxxx.cloudfront.net    /proposal/b/5/b5b...abd_3915699_150.jpg  307 -   Googlebot-Image/1.0 20140912094937?timestamp=1410048000041  -   Miss    aRH...mlw==    img.lancers.jp  http    307 0.372
2014-10-01  19:50:25    MIA50   417 66.249.71.8 GET xxxxxxxxxxxxx.cloudfront.net    /proposal/0/2/029...b70_3981251_150.jpg  307 -   Googlebot-Image/1.0 20140926205158?timestamp=1410048000036  -   Miss    _MS...gRg==    img.lancers.jp  http    307 0.342
2014-10-01  19:50:28    MIA50   417 66.249.71.8 GET xxxxxxxxxxxxx.cloudfront.net    /proposal/d/d/dd6...245_3966683_150.jpg  307 -   Googlebot-Image/1.0 20140923170904?timestamp=1410048000045  -   Miss    MLd...kJw==    img.lancers.jp  http    307 0.368

アクセスログ解析

CloudFrontのアクセスログは、10~20分単位で圧縮保存されるので、ファイル数が膨大になります。
AWS CLIで一括ダウンロードして解析を行います

2014-10-02のログ全てダウンロード

aws s3 sync s3://ログバケット名/cloudfront/ ./ --exclude "*" --include "*2014-10-02*"

2014-10-02のログから、リモートIPアドレスが「xxx.xxx.xxx.xxx」であるものを抽出する

gunzip -c *2014-10-02* | grep xxx.xxx.xxx.xxx

CloudFront導入効果

  • NFSのストレージ逼迫問題の解決
    • NFSにキャッシュする必要がなくなったため、NFSは必要なくなりました
  • ブラウザレスポンスの改善
    • 約0.5s程改善されました
  • AppサーバーのCPU負荷改善
    • AppサーバーのCPU負荷が約10%程改善しました

最後に

CloudFrontの事例は、S3との連携が主に紹介されていますが、S3以外をオリジンとして利用することも可能です。
今回は、EC2で生成したサムネイルをCloudFrontにキャッシュする事例を紹介させていただきました。
ランサーズでは、サムネイル以外の静的画像にもCloudFrontを利用していますので、その事例も後日紹介させていただければ幸いです。

ランサーズのNginx+PHP-FPM化

kanazawa|2017年09月05日
Apache

インフラエンジニアの金澤です。
以前ブログでお話しました、ランサーズのNginx+PHP-FPM化が一段落したのでまとめました。

移行の目的

以前は、Apache 2.2 + mod_php の構成で動作していました。
以前のブログでもお話ししましたように、PHP 5.3からバージョンアップする過程で、今までのhttpd.confをそのまま使うことができなくなったため、この機会に設定を一から見直し、Nginx + PHP-FPM 構成に移行しました。

移行時に行ったこと

Nginxのインストール

ランサーズは、Amazon Linuxで稼働しています。
yumパッケージのNginxには欲しい機能が含まれていなかったため、ソースからインストールしました。
configure時に以下のオプションでコンパイルすることで、yumパッケージでインストールした場合とほぼ同じディレクトリ構成になります。

./configure \
--user=nginx \
--group=nginx \
--prefix=/etc/nginx \
--sbin-path=/usr/sbin/nginx \
--conf-path=/etc/nginx/nginx.conf \
--error-log-path=/var/log/nginx/error.log \
--pid-path=/var/run/nginx.pid \
--lock-path=/var/run/nginx.lock \
--http-log-path=/var/log/nginx/access.log \
--with-http_addition_module \
--with-http_gzip_static_module \
--with-http_gunzip_module \
--with-http_realip_module \
--with-http_v2_module \
--with-http_ssl_module \
--with-http_stub_status_module

PHP-FPMとの連携

PHP-FPMとはunix socketで連携します。

server unix:/var/run/php-fpm/php-fpm.sock;

ELB対策

ELBのSSL Termination機能を利用しているため、https通信も、EC2内部ではhttpで処理しています。
https経由のアクセスであったかどうかを判定するために、X-Forwarded-Proto環境変数を利用しています。

set $elb_https off;
if ($http_x_forwarded_proto = https) {
    set $elb_https on;
}

location ~ \.php {
...
    fastcgi_param HTTPS $elb_https;
...
}

そして、アクセスログがELBのプライベートアドレスにならないように、以下の設定を行いました。

set_real_ip_from 10.0.0.0/8;
real_ip_header X-Forwarded-For;

プロキシ設定

http://www.lancers.jp/magazine/ 等、いくつかのURLをWordPressサーバーにプロキシしています。
Apache(httpd)の場合は、rewriteやProxyPassで手軽にプロキシ設定できましたが、Nginxの場合は、resolverにDNSのIPアドレスを設定する必要があります。
AWSの場合、10.0.0.0/16 のVPC上のresolverは10.0.0.2になります。

location ~ ^/magazine/(.*)$ {
    resolver 10.0.0.2;
    proxy_set_header X-Forwarded-Host $host;
    proxy_set_header X-Forwarded-Server $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    set $endpoint "magazine.lancers.jp";
    proxy_pass http://$endpoint/magazine/$1?$args;
}

開発環境ではhostsを利用しているので、dnsmasqをインストールし、hostsをDNSサーバーとして稼働させています。
このときのresolverのIPアドレスは 127.0.0.1 になります。

Apache機能依存関数の置き換え

Apache(httpd)機能依存のPHP関数を置き換える必要がありました。

apache_note関数

アクセスログに、ログインユーザーidを記録するために利用していました。

apache_note('userId', $this->loginUser['id']);

Apache → Nginx移行時にエラーにならないように、暫定的に以下のようにソースを修正しました。

if (strpos($_SERVER['SERVER_SOFTWARE'], 'pache') !== false) {
    apache_note('userId', $this->loginUser['id']);
} else { // nginxの場合はResponse Headerを利用
    header('X-User-ID: ' . $this->loginUser['id']);
}

getallheaders関数

このブログの記事を参考にさせていただきました。
app/config/bootstrap.php に以下のように実装しました。
Nginxに移行した場合に、こちらで実装したgetallheaders関数が実行されるようになります。

if (!function_exists('getallheaders')) {
    function getallheaders()
    {
        $headers = array();
        foreach ($_SERVER as $name => $value) {
            if (substr($name, 0, 5) == 'HTTP_') {
                $headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value;
            }
        }
        return $headers;
    }
}

チューニング設定

※AWSのEC2 c4.2xlarge(8コア、15GB)インスタンスで稼働させた場合の設置値です。

Apache + mod_phpの設定

Apache + mod_phpの時代は、試行錯誤しながら、最終的に以下の値で運用していました。

StartServers        100
MinSpareServers     100
MaxSpareServers     120
ServerLimit         350
MaxClients          350
MaxRequestsPerChild  30

MaxRequestsPerChildのデフォルトは10000ですが、かなり低く設定しています。
低く設定すると、すぐにプロセスがなくなるため、forkの量が増え、CPU負荷が増加します。
高く設定すると、プロセスのforkが少なくなりますが、アクセスの度にプロセスのメモリ使用量が増加していきます。
CPU能力の高いc4系のインスタンスを利用しているため、メモリ容量と比較してCPUに余裕があります。
CakePHP等のフレームワークを使ったサービスの場合、GCによるメモリ解放があまり期待できないので、こまめにプロセスを切ることでメモリを確保していました。

Nginx + PHP-FPMの設定

Nginxはイベント駆動のため、基本的にはCPU数分だけプロセスを稼働する運用になります。
インスタンスタイプの変更時に動的にプロセス数が変更されるように、worker_processesをautoに設定しました。

worker_processes auto;

※結果、現在、以下の10プロセスが稼働しています。

ID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND
 814 nginx     20   0 73920 6952 3148 S  0.0  0.0   0:00.98 nginx
 815 nginx     20   0 73920 6964 3148 S  0.0  0.0   0:01.78 nginx
 816 nginx     20   0 73920 6704 3148 S  0.0  0.0   0:02.59 nginx
 817 nginx     20   0 74168 6832 3148 S  0.0  0.0   0:05.45 nginx
 819 nginx     20   0 75604 8380 3148 S  0.0  0.1   0:15.86 nginx
 820 nginx     20   0 75948 8856 3148 S  0.0  0.1   1:04.30 nginx
 822 nginx     20   0 78564  11m 3148 S  0.0  0.1   5:00.41 nginx
 823 nginx     20   0 77844  10m 3148 S  0.0  0.1   4:53.30 nginx
 824 nginx     20   0 72920 5056 2448 S  0.0  0.0   0:00.20 nginx
2017 root      20   0 72920 5928 3416 S  0.0  0.0   0:04.44 nginx

PHP-FPMのプロセス数は100に設定しました。
ランサーズの場合、PHP-FPM のメモリ消費量は、1プロセスあたり最大で約100MBほど消費していました。
100プロセスの場合、PHP-FPMのプロセスだけで約10GBのメモリ消費となります。
pm_max_requests は、ApacheのMaxRequestsPerChild にあたる設定です。
Apacheのときと同様に30に設定しました。

pm = dynamic
pm.max_children = 100
pm.start_servers = 100
pm.min_spare_servers = 100
pm.max_spare_servers = 100
pm.max_requests = 30

※すべての設定を100に統一したので、今後はpm = static にしても良いかもしれません。

移行結果

※AWSのEC2 c4.2xlarge(8コア、15GB)インスタンスで稼働させた結果です。

リソース消費量

移行前(apache + mod_php)

CPUは8コア(800%)中、200%手前でほぼ安定。

メモリ15GBをほぼリミットまで利用していました。

移行後(Nginx + PHP-FPM)

CPU使用率は、100%~300%の間で変動するようになりました。

PHP-FPM 100プロセスの設定で、EC2のメモリ15GBをほぼリミットまで利用。
※その後、slabキャッシュを定期的に削除するようにしたところ、5GB程度の利用に収まっています。

サーバーレスポンス

結果、ほとんど変化はありませんでした。

Apache(httpd)は、mod_phpを利用するため、preforkで動作していました。
静的ファイルもpreforkのApache(httpd)プロセスで処理していたため、イベント駆動のnginxのほうがパフォーマンスは向上しそうですが、前段でCDN(CloudFront)を挟んでキャッシュしているため、EC2への静的ファイルへのアクセス頻度が低く、差が出にくいと考えています。

mod_phpとPHP-FPMは、ランサーズにおいては、レスポンス上はほとんど差がでませんでした。
ただし、今回はパフォーマンス向上ではなく、バージョンアップの前準備が目的でしたので、極端にパフォーマンスが劣化しなければOKという方針で取り組んでいます。
また、現在PHP-FPM 5.3で稼働していますが、今後、PHP-FPM 5.6にバージョンアップしたらどうなるかも計測していきたいと思います。

PHP、CakePHPのバージョンアップ状況

以前のブログで、以下のフェーズに分けてバージョンアップを行うと話していました。

Apache + mod_php + PHP 5.3 + CakePHP 1.3
↓(第1フェーズ)
Nginx + PHP-FPM + PHP 5.3 + CakePHP 1.3
↓(第2フェーズ)
Nginx + PHP-FPM + PHP 5.6 + CakePHP 1.3
↓(第3フェーズ)
Nginx + PHP-FPM + PHP 5.6 + CakePHP 2
↓(第4フェーズ)
Nginx + PHP-FPM + PHP 5.6 + CakePHP 3
↓(第5フェーズ)
Nginx + PHP-FPM + PHP 7.x + CakePHP 3

その後、計画の詳細を詰め、現在は以下のフェーズに分けて進めています。
※インフラ側では、PHP 5.6のバージョンアップ検証を進めていましたが、その前にCakePHPを2.8までバージョンアップする方向で進めることになりました。

Apache + mod_php + PHP 5.3 + CakePHP 1.3
↓(第1フェーズ)
Nginx + PHP-FPM + PHP 5.3 + CakePHP 1.3
↓(第2フェーズ)
Nginx + PHP-FPM + PHP 5.3 + CakePHP 2.8
↓(第3フェーズ)
Nginx + PHP-FPM + PHP 5.6 + CakePHP 2.8
↓(第4フェーズ)
Nginx + PHP-FPM + PHP 5.6 + CakePHP 2.9
↓(第5フェーズ)
Nginx + PHP-FPM + PHP 7.x + CakePHP 2.9
↓(第6フェーズ)
Nginx + PHP-FPM + PHP 7.x + CakePHP 3
↓(第7フェーズ)
Nginx + PHP-FPM + PHP 7.x + CakePHP 4

そして現在、第2フェーズの、CakePHP 1.3 → 2.8 バージョンアップ作業を進めています。

PHPカンファレンスに弊社エンジニアが登壇します

10/8 に開催される、PHPカンファレンス2017に弊社エンジニアの秋山が登壇します。
https://joind.in/event/japan-php-conference-2017/session23-lancers
上でお話しました、バージョンアップ状況の詳細についてもお話しする予定です。

PHPカンファレンス福岡 2017に登壇しました

kanazawa|2017年06月12日
AWS

インフラエンジニアの金澤です。

2017/06/10(土)に、PHPカンファレンス福岡に登壇させていただきました。

今回、30分枠とLT枠(5分)の2枠を採択していただきました。

登壇内容

CakePHP 1.3 + PHP 5.3 → CakePHP 3 + PHP 7 移行を決めた話

Dホール/16:15〜16:45 (30分)

事前に、概要をエンジニアブログで公表させていただいていたのですが、思いのほか反響がありました。
(特に、CakePHP 1.3 → 3 への移行に関しては賛否両論あり、参考にさせていただきました)

バージョンアップは以下のフェーズに分けて計画しています。

Apache + mod_php + PHP 5.3 + CakePHP 1.3
↓(第1フェーズ)
Nginx + PHP-FPM + PHP 5.3 + CakePHP 1.3
↓(第2フェーズ)
Nginx + PHP-FPM + PHP 5.6 + CakePHP 1.3
↓(第3フェーズ)
Nginx + PHP-FPM + PHP 5.6 + CakePHP 2
↓(第4フェーズ)
Nginx + PHP-FPM + PHP 5.6 + CakePHP 3
↓(第5フェーズ)
Nginx + PHP-FPM + PHP 7.x + CakePHP 3

現在、第2フェーズの最中です。

PHP 5.6のバージョンアップまでは、できそうな手ごたえを感じていますが、問題はCakePHPのバージョンアップです。
ここをどのように行うか、今も議論中で、会場の皆さんにもアドバイスをいただきたい旨を話しました。

最後、時間が少し余ったため、その場で質疑応答の時間としていただき、CakePHPのバージョンアップ経験者の方々から様々なアドバイスをいただきました。

  • CakePHP 2 → CakePHP 3の移行スクリプトはそれほど期待できない
  • UTがSimpleTestからPHP Unitに変更されるのをどう対応するか
  • 経験者の方々からの移行工数の目安

など、これからぶつかるであろう壁について、予め確認することができて良かったです。

※ブログの反響を受け、詳細な質問に答えられるように、PHPエンジニアの上野にも参加してもらいました。上野自身もかなり収穫があったようです。

WHERE 1 = 1

Fusicホール/17:00〜17:45 ライトニングトーク(各5分)

質疑応答(Ask the Speaker)でも盛り上がっていたのですが、LTは2番目の発表だったため、切り上げて急いでFusicホールに向かうことに。

間髪入れず、次の発表となり、緊張はしなかったのですが、時間がなくてあっという間に5分が過ぎてしまいました。
銅鑼が鳴り焦って帰って来たら、マイクを持って帰ってしまう失態(笑
LTもリハーサルしておくべきでした。またどこかで再チャレンジしたいです。
でも、なんとか伝えたいとこまでは伝えられたと思います。

反響としては、「面白かった」ではなく、「恐ろしい話だった」というもの多かったと思います。
会場の方々も、CakePHPでコーディングしている方々は多く、やはり他人事ではないのだと感じました。

こちらはスライドはアップできないのですが、
一番伝えたいことは、CakePHPの以下のバグを放置しないことです。
CakePHP generated UPDATE query with WHERE 1 = 1

このバグは2014/7にFixされていますので、
該当バージョンの方は速やかにアップデートしておくことをお勧めします。
https://github.com/cakephp/cakephp/pull/3959

※CakePHP 1.3側も、1.3.21 でFixされています。
https://github.com/cakephp/cakephp/commit/41082b1

懇親会

まず、LTの話題で盛り上がりました。
開発環境で起こったことがあるという話も聞き、表には出てこなくても、やっぱり同じ経験をしている方もいるのだと思いました。

そして、CakePHPのバージョンアップについて、引き続き熱い議論が繰り広げられました。
今後のバージョンアップ方法について、考えている方法をいくつか提案しましたが、その良し悪しについても極めて論理的にアドバイスを頂きました。
自分のサービスであるかのようにアドバイスしていただいた、Fusicさんや有識者の方々に深く感謝いたします。

特に、CakePHP 2 → CakePHP 3 へのバージョンアップは、フレームワークを別なものに変更するくらいに考えたほうがよく、このタイミングで、別なフレームワークに移行する方も多いとのことでした。

※この話を聞き、バージョンアップ計画としては、PHP 5.6 + CakePHP 2 までできたら、(CakePHP 3ではなく)先にPHP7.x化を着手した方が良さそうだなーという感触です。

福岡の参加者で、ランサーズを使っている方が多かったのも意外な驚きでした。
フリーランサーや学生さんが、ランサーズを知っていて、私たちのセッションを楽しみにしていただいてたことを聞き、嬉しかったです。

最後に

今回、登壇者という形で参加させていただきましたが、参加者としても非常に勉強になり、刺激を受けたイベントでした。特に、今回、PHPのバージョンアップをテーマにしたセッションが他にもあり、参考にさせていただいた内容も多かったです。

CakePHP 2 → 3 への移行がツラいということは身に染みて分かりましたが、それでもチャレンジしたいという気持ちも残っていますし、アドバイスいただいた方々はじめ、ランサーズがCakePHP 3までできるのか、興味深く見守ってもらえると感じました。

まずは、PHP 5.6 + CakePHP 2 まで、確実にバージョンアップしたいと思います。
次の日に、CakePHP 1.3 → 2の移行スクリプトを検証を開始してしまうくらい、テンション上がってしまいました。

お知らせ

CakePHP 3.4 クックブック & ソースコードリーディング

CakePHPの最新版を勉強しておく目的で、3月よりソースコードリーディング会を始めました。

2017/6/22(木)に第3回目を行います。

第3回 CakePHP 3.4 クックブック & ソースコードリーディング

社外からの参加者も募集しています。興味のある方は是非ご参加ください!

エンジニア募集中!

ランサーズでは、バージョンアップのプロジェクトに協力していただけるエンジニアを募集しています。

PHP/CakePHP大規模なバージョンアップを推進するエンジニアを募集!

本プロジェクトに興味を持った方と、是非一緒に推進したいです!

※もちろん、プロダクト成長させていくエンジニアも募集しています!

詳しくは↓から

PHP、CakePHPバージョンアップの決断

kanazawa|2017年05月26日
Apache

インフラエンジニアの金澤です。

この度、ランサーズ稼働環境(PHP + CakePHP)のバージョンアップを決断しました。
まずは私から、その経緯と計画についてお話いたします。

バージョンアップ決断の理由

ランサーズは、2008年にサービスを開始しました。
現在、PHP 5.3.29 + CakePHP 1.3.6 で稼働しており、長らくバージョンアップせずに開発を続けてきましたが、今回、PHP 7 + CakePHP 3までバージョンアップすることを決断しました。
その理由は、主に以下になります。

旧バージョンのサポート終了

PHP 5.3もCakePHP 1.3も、サポートが終了しています。
今後、特にセキュリティに関わるバグが報告された場合、今のバージョンを続ける限り、自分たちで対応する必要があります。
(実際、独自に手を入れている箇所がいくつか存在します)

ライブラリバージョンアップへの追従

サードパーティが提供するPHPライブラリも、PHP5.3をサポートしなくなってきています。
例えば、以下のライブラリです。

最新ライブラリを利用するためには、要求するPHPバージョンまで追従する必要がありました。

開発生産性向上とエンジニア採用

開発エンジニアからも、新しいPHPの関数が使えないことを嘆く声が出ていました。
PHPの古いバージョンで好んで開発したいエンジニアはほとんどいないでしょう。
最新のPHP、CakePHPの提供するライブラリを利用できれば、開発生産性の向上が期待できます。
また、常に最新の技術を採用し続けることで、エンジニアにとっても魅力的な開発環境を提供したいと思いました。

バージョンアップ計画

今回、バージョンアップの幅がかなり大きくなるので、条件を整理しました。

  • PHP 5.4にするには、Apacheのバージョンアップが必要(後述)
  • PHP 5.4→5.5→5.6については、大きな構成変更は生じない
  • CakePHP 3にするには、PHP 5.6以上が必要
  • PHP 7ではCakePHP 1.3はまともに動かない

上記の条件を踏まえ、バージョンアップは、以下のフェーズに分け、段階的に行うことにしました。

  • Apache + mod_php → Nginx + PHP-FPM移行
  • PHP 5.3 → PHP 5.6移行
  • CakePHP 1.3 → CakePHP 3移行
  • PHP 5.6 → PHP 7移行

Apache + mod_php → Nginx + PHP-FPM移行

現在のランサーズは、Amazon Linux + Apache(httpd)2.2 + mod_php で稼働しています。
※以前はCentOS6で稼働していたのですが、昨年Amazon Linuxに移行しました。
Amazon LinuxのyumでPHPをインストールすると、Apache(httpd)もセットでインストールされます。
PHPとApache(httpd)のバージョン組み合わせは以下の通りです。

php httpd
5.3.29 2.2.31
5.4.45 2.4.25
5.6.30 2.4.25

つまり、yumでPHPを5.3からバージョンアップすると、Apache(httpd)も2.4にバージョンアップされます。
Apache(httpd)2.2と2.4では設定ファイルの構成が大幅に変更されていますので、単純に2.2の設定ファイルをそのまま使うというわけにはいきません。
移行するなら、設定項目を1から見直す必要があります。

設定を1から見直すのであれば、Apache + mod_php以外の選択肢も候補に入ります。
今回は、パフォーマンスの向上が期待できるNginx + PHP-FPMを採用しました。

PHP 5.3 → PHP 5.6移行

Nginx + PHP-FPMの移行が終わったら、PHPを5.6までバージョンアップします。
PHP 5.6までバージョンアップするには、以下の条件を満たすようにソースを修正する必要があります。

当初、PHP 5.4 → PHP 5.5 → PHP 5.6と段階的にバージョンアップする計画でしたが、PHP5.4では関連ライブラリのインストールが難しく、断念しました。
PHP 5.6へ一気にバージョンアップする計画は、一度に修正する量は多くなりますが、総合的に考えてこちらの方が速く移行できると判断しました。

CakePHP 1.3 → CakePHP 3移行

ここが一番の難関になります。
CakePHP 1.3 → 2移行は大きな壁で、稼働中のサービスをどのように移行するのか、課題が山積みです。
しかしながら、CakePHP 2 → 3移行は、それと比較すれば、それほど難しくないと見積もっています。
移行状況や以降方法次第で計画が変更される可能性はありますが、現在はCakePHP 1.3 → CakePHP 3に一気に移行する計画を立てています。

CakePHP 1.3 → 3移行は、実績がほとんどないので、大きなチャレンジになります。

2017/5/30追記:

すみません。雑に書きすぎました。
CakePHP2→3の方が大変ではないかという意見を多く頂きましたので補足させてください。

開発エンジニアからも、CakePHP2→3のほうが大変だという意見が出ています。
(特に、CakePHP2→3でモデル周りが全部変更されている点など)
その一方で、古株のエンジニアは、CakePHP本体にも修正を入れている点を懸念しております。
(過去のセキュリティアップデートを自前で組み込んだり、独自の機能のためにcoreソースを書き換えている箇所が結構存在します)

CakePHP2に移行するには、

  • UTを完全整備(過去の試みはことごとく失敗)
  • ランサーズ独自の修正を完全に撤廃(かなり大変)
  • UTをチェックしながらCakePHP1.3→2へ移行

というステップが必要になります。

逆に、ここまでできれば、体制がかなり整います。
CakePHP2→3はランサーズ独自の修正がない状態で取り掛かれますし、他社での移行事例や意見もそのまま参考にできます。

とはいえ、CakePHP2→3の移行は、一般的にはCakePHP1→2よりも大変な作業になります。
(「難しくない」という表現が適切ではありませんでした)
同じ苦労を2度するのであれば、一度にやる方法も検討しても良いのではないかという意見もあります。
(CakePHP1.3のbootstrap.phpのuriのパス指定で3.4.xに切り替える方法で移行した方もいるとのこと)

何れにせよ、ここはまだ明確な結論が出ていません。
今後も議論しながら、現状は、今できる作業を着実に進めている状態です。
※ここら辺、知見のある方に是非ご意見いただきたいです。

PHP 5.6 → PHP 7移行

CakePHP 3に移行できれば、PHP 7に移行できます。
PHP 7は実装が新しくなり、5.6まで維持していた互換性を排除している部分も多いので、この移行作業は1フェーズとし、じっくり行う予定です。
※PHP 7.1も検討していましたが、現状、Amazon LinuxのyumリポジトリではPHP 7.0までしかサポートしていないため、現状は7.0までアップデートする計画にしています。

現在の状況

Nginx + PHP-FPM化

ほぼ検証が完了し、近日中に移行予定です。
移行が完了しましたら、その結果をブログに書きたいと思います。

PHP 5.3 → 5.6化

Nginx版でPHP5.6をインストールした開発環境用Dockerコンテナを構築中です。
ランサーズではDockerで開発環境を構築しています。

PHP 5.6のコンテナを構築し、PHP5.3版とは別にDockerレジストリにアップしておきます。
開発エンジニアは、利用するコンテナをPHP5.3、PHP5.6に自由に切り替えることができるので、この仕組みで互換性を検証していく予定です。

UTテストジョブの整備

現状のUTテストジョブのうち、正常に終了しないものや、非常に時間がかかるものがあります。
そのため、CIを完全に回しきれていない状態です。
バージョンアップによるエラーやバグを速やかに検知できるように、全て正常終了し、かつCIを現実時間内に回せるレベルまで整備中です。

エンジニア合宿

5/27~5/28にエンジニア合宿を行います。
UTのテストジョブ整備を中心に、プロジェクトを推進する予定です。

PHPカンファレンス登壇

6/10(金)PHPカンファレンス福岡 で登壇予定です。
http://phpcon.fukuoka.jp/2017/
今回ブログに書かせていただいた内容を、さらに詳しく発表したいと思います。

エンジニア募集中!

ランサーズでは、このバージョンアップのプロジェクトに協力していただけるエンジニアを募集しています。

PHP/CakePHP大規模なバージョンアップを推進するエンジニアを募集!

本プロジェクトに興味を持った方と、是非一緒に推進したいです!

いよいよ、プロジェクトが本格的に始動します。

AWSでWordPressのスケールアウト

kanazawa|2017年03月06日
Aurora

インフラエンジニアの金澤です。
今回は、AWS上のWordPressサーバーをスケールアウトするために行った手順について紹介いたします。

ランサーズで運用しているWordPressサービス

ランサーズでは10以上のWordPressサービスをAWSで運用しています。
ランサーズと比べてアクセス数が多くなく、負荷も低いのでこれらのサービスを1台のEC2で運用してきましたが、リソースが不足してきたため、複数台にスケールアウトできるように準備し始めました。

スケールアウトするために解決すべき問題

WordPressのEC2をスケールアウトするには、以下の問題を解決する必要があります。

MySQL

EC2のローカル上にMySQLがインストールされた状態では、複数台構成にできません。
MySQLを別サーバーに移動する必要があります。

uploadsディレクトリ

wp-content/uploadsディレクトリには、記事投稿時にアップロードされた写真等のメディアデータが格納されます。
EC2上にこのディレクトリがある状態では複数台構成にできません。
uploadsディレクトリを別サーバーに移動する必要があります。

pluginsディレクトリ

wp-content/pluginsディレクトリには、WordPressのプラグインが格納されています。
複数台構成にすると、管理画面からプラグインを追加しても、1台のサーバーにしか反映されません。
全サーバーにプラグインが配布されるような仕組みを作る必要があります。

ログ

複数台構成にすると、各サーバーにログが分散され、解析が困難になります。
アクセスログを解析したい場合は、別途ログを集約する仕組みを構築することになります。

スケールアウトするために行なった準備

以下、上記の問題を解決するために行った手順について紹介いたします。

スケールアウト前

元々は、1台のEC2にグローバルIPを付与して運用していました。
https通信については、必要なドメインのみSSL証明書を申請していました。

Git化

ランサーズでは、WordPressのプロジェクトは、全てGitHubで管理しています。

wp-content/plugins ディレクトリはGit化し、プラグインの追加もGitHubに反映しています。
プラグインの追加は本番サーバーの管理画面では行わず、開発環境で追加し、検証が完了したものをgit pullでリリースするフローにしています。
※wp-config.phpやwp-content/uploadsディレクトリはGit管理から外しています。

ELBを追加

スケールアウトの第一歩として、まずELB経由で通信する構成に変更します。
グローバルIPアドレスは必要なくなるため、EC2はプライベートVPCに移動させます。

このタイミングで、ELBの以下の機能を利用するように設定しました。

AWSのSSL証明書設定(https化)

AWSの無料SSL証明書が利用できるようになったため、サービスをhttps化しました。

SSL Termination

ELBのSSL Termination機能を利用することで、EC2内ではhttpのままで通信できます。
ELBアクセス時にhttp、httpsのどちらかでアクセスされたかを判定するために、X-Forwarded-Proto環境変数を利用するため、wp-config.php に以下の記述を追加します。
(wp-settings.phpの読み込み前に記述)

if (!empty( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https' ) {
    $_SERVER['HTTPS']='on';
}
 
/** Sets up WordPress vars and included files. */
require_once(ABSPATH . 'wp-settings.php');

MySQLをRDSに移行

RDS for MySQLを作成し、EC2内のMySQLをRDSに移行します。

MyISAMのテーブルが含まれているとRDSの機能が制限されますので、特に理由がなければInnoDBに変換しておいたほうが良いでしょう。

uploadsディレクトリをS3に移行

wp-content/uploadsディレクトリのコンテンツはS3に格納します。

S3に格納するために必要なプラグインは以下の2つになります。

Amazon Web Servicesプラグイン

プラグインを追加後、wp-config.phpに以下の設定を追加します。

define( 'DBI_AWS_ACCESS_KEY_ID', 'XXXXXXXXXXXXXXXXXXXX' );
define( 'DBI_AWS_SECRET_ACCESS_KEY', 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' );

WP Offload S3 Liteプラグイン

Webサイトホスティングを有効にしたS3バケットを作成し、プラグインを追加、設定します。
既存のuploadsディレクトリのコンテンツは、S3にアップしておく必要があります。
aws s3コマンドでS3にコピーします。

$ aws s3 sync ./wordpressディレクトリ名/wp-content/uploads s3://S3バケット名/wordpressディレクトリ名/wp-content/uploads

DB側のupload情報も修正が必要になります。
ここが一番面倒な作業になります。
このブログの記事を参考にさせていただきました。

wp_postmetaテーブルにデータ追加

INSERT文をSQLで生成し、出力されたINSERT文を適用してデータを追加します。

SELECT
  CONCAT(
    'INSERT INTO wp_postmeta(post_id, meta_key, meta_value) VALUES (',
    post_id,
    ', \'amazonS3_info\', \'a:3:{s:6:\"bucket\";s:13:\"S3バケット名\";s:3:\"key\";s:',
    length(meta_value) + 29,
    ':\"wordpress/wp-content/uploads/',
    meta_value,
    '\";s:6:\"region\";s:14:\"ap-northeast-1\";}\');'
  ) AS record
FROM
  wordpress.wp_postmeta
WHERE
  meta_key = '_wp_attached_file'
;

※length(meta_value) + 29の29という数字はuploadsディレクトリまでのパスの文字数です。
例えば、wordpress/wp-content/uploads/ というパスであれば、パスの文字数が29になるので、29を加算します。

データ追加が上手く行けば、管理画面の「メディア」メニューにアップロードしたデータのURLが、S3WebサイトホスティングのURLになります。

wp_postテーブルのURL修正

wp_postテーブルや、その他、アップロード画像をURL参照しているテーブルのURLも、S3のURLに変更します。
この作業はバックアップも兼ねて、mysqldumpでテーブルデータをエクスポートしてから、エディタでURLを変更後、インポートし直すのが良いと思います。

リリースシステムとの連携

ここまで準備すれば、EC2をスケールアウト可能になります。
複数台構成のEC2にリリースを行うために、リリースシステムからリリースするフローに変更しました。
ランサーズには、専用のリリースシステムがあります。
このリリースシステムに、WordPress系プロジェクトのリリース機能を追加します。
リリースシステム上でgit pullして、ansible経由でWordPress全サーバーに同期します。
WordPressサーバー側はGit管理する必要がなくなるため、このタイミングで.gitディレクトリを削除します。

今後の課題

ログの集約

fluentd + S3プラグインでログをS3に集約します。
ログサーバーにも転送し、全WordPressサーバーのアクセスログは、ログサーバーで参照するようにします。

DBのスケールアウト

HyperDBプラグインを導入すると、参照系と更新系を分離できるようになります。
RDSのリードレプリカを作成し、参照系はHAProxy等で分散させるようにします。

最後に

実は、WordPressのEC2はまだ1台のままです。(笑
スケールアウトの準備をした過程で、リソースがELB、EC2、RDS、S3に分散された結果、もう少しだけ耐えられそうな見込みになったためです。
もう少しアクセスが増えたら、EC2をスケールアウトし始めることになると思います。
今回は、その準備ができましたというお話でした。

開発環境のDocker化 その後

kanazawa|2016年12月19日
DevOps

インフラエンジニアの金澤です。

開発環境をDocker化してから1年経ちましたので、その後のアップデートについて書きたいと思います。

前提

ランサーズでは、Dockerを開発を以下の目的で導入しました

  • PCリソース(HDD、メモリ)を削減
    • コンテナ単位でサーバーを構成し、VMを1つに統一
  • 本番環境のサーバー構成と極力互換性を保つ
    • 開発環境との差異による障害発生を未然に防ぐ

そのため、Dockerのベストプラクティスではないことも行っております。
例えば、以下のようなことも行っております。

  • 1コンテナに複数サービスを稼働
    • sshdも稼働
  • サービスをフォアグラウンドで稼働
    • /etc/init.dのスクリプトで稼働
  • 軽量化よりも利便性を優先
    • 便利なパッケージはインストール

Amazon Linuxコンテナ

2016年11月に、Amazon LinuxのコンテナイメージがDocker Hubから提供されました。

今まで、本番AWSのEC2ではAmazon Linuxを利用していたのに対し、開発環境のコンテナはCentOS6を利用していました。
パッケージバージョンの細かい差異が問題になることがありましたが、開発用コンテナもAmazon Linuxで構築することができるようになり、その問題がなくなりました。
また、ランサーズでは、本番EC2と開発用コンテナに対し同じAnsibleのplaybookで構築していましたが、OS間の差異を吸収する処理がほとんどなくなりました。

DockerfileのFROMに以下のように記述することでAmazon Linuxのコンテナイメージを利用することができます。

FROM amazonlinux:2016.09

ただし、EC2のAmazon Linuxと完全に同じではないので注意する必要があります。

/etc/sysconfig/networkがない

これが原因で、開発環境のDockerコンテナ構築時にAnsibleのPlaybookが失敗することがあります。
また、ランサーズのDocker環境では/etc/init.d/の起動スクリプトでサービスを起動していますが、ここで/etc/sysconfig/networkを参照していることがあり、起動に失敗することがあります。
そのため、Dockerfileで構築時に/etc/sysconfig/networkを配置する等の対策をしています。

viがインストールされていない

EC2のAmazon Linuxにインストールされていたパッケージで、Amazon Linuxコンテナにインストールされていないものがたくさんあります。
viもその1つです。コンテナにviは必ずしも必要ありませんが、CentOS6コンテナにはviがインストールされていたので、それを利用していた方のために、viをインストールしています。

docker-compose対応

Docker導入段階でもdocker-composeは検討しておりましたが、コンテナ単位での細かい制御をしたいため、シェルスクリプトで運用していました。
その中で、コンテナ起動後にipコマンドで固定IPを付与する処理も行ってましたが、最近のDockerでは固定IPで運用できるようになりましたので、それに合わせてdocker-composeも導入し始めました。

docker-compose.ymlで、以下のように172.21.0.0/16のネットワークを新規に構築します。

networks:
  lancers:
    driver: bridge
    ipam:
      driver: default
      config:
      - subnet: 172.21.0.0/16
        gateway: 172.21.0.1

Appコンテナでは以下のように設定します。
ipv4_addressで固定IPアドレスを設定し、hosts設定はextra_hostsに記述しています。
コンテナのビルドは個人では行わず、構築済のコンテナをpullしています。
registryコンテナ経由で5000番ポートで取得しています。

services:
  app:
    image: localhost:5000/app:latest
    hostname: app
    networks:
      lancers:
        ipv4_address: 172.21.6.11
    extra_hosts:
       - "dev.lancers.jp:172.21.50.11"
       - "dev-img.lancers.jp:172.21.50.11"
…
    container_name: app-6-11
    volumes:
      - ~/www:/var/www

registryコンテナの起動は以下の通り。
構築したコンテナは東京リージョンのS3に格納しているため、AWSのアクセスキーを指定して起動します。

services:
  registry:
    image: registry:2.2
    container_name: registry
    ports:
      - 5000:5000
    environment:
      REGISTRY_STORAGE_S3_ACCESSKEY: XXXXXXXX
      REGISTRY_STORAGE_S3_SECRETKEY: xxxxxxxx
      REGISTRY_STORAGE_S3_BUCKET: docker-registory-lancers
      REGISTRY_STORAGE_S3_REGION: ap-northeast-1
      REGISTRY_STORAGE_S3_ROOTDIRECTORY: /
      REGISTRY_STORAGE: s3

registryコンテナは、コンテナをpull、updateするときのみに利用するため、registry.ymlファイルとして個別に定義しています。
registryコンテナを利用するときのみ、以下のコマンドで起動します。

docker-compose -f registry.yml up -d

ランサーズのDocker環境では、AWSのELBに相当するelbコンテナを用意しています。
elbコンテナに80番、443番のポートフォワーディングを設定しており、ランサーズのAppコンテナを始め、各種サービスのコンテナはすべてelbコンテナ経由でアクセスしています。

services:
  elb:
    image: localhost:5000/elb:latest
    hostname: elb
    networks:
      lancers:
        ipv4_address: 172.21.50.11
    extra_hosts:
       - "dev.lancers.jp:172.21.6.11"
       - "dev-img.lancers.jp:172.21.6.11"
…
       - "dev-engineer.blog.lancers.jp:172.21.4.51"
…
    container_name: elb-50-11
    ports:
      - 80:80
      - 443:443

以上、概要を説明させていただきましたが、docker-composeについては、複数サービスへの対応や、記述の最適化等でもっと工夫できる余地がありますので、また機会がありましたら別途書かせていただきたいと思います。

Docker for Mac(Windows)対応

VirtualBoxを利用しない、ハイパーバイザー型のDocker for Mac(Windows)がリリースされましたので、こちらも利用を開始しました。

運用面での変更点

IPアドレス

今までのDocker ToolboxでのDocker Machineと運用面で大きく変わるのは、DockerのIPアドレスです。
Docker Toolboxのときは、デフォルトで作成されるVirtualBox VMのIPアドレスは、192.168.99.100になります。
ローカルPC上のhostsにこのIPアドレスに対して、dev.lancers.jp等のドメインを記述していましたが、Docker for Mac(Windows)の場合は、このIPが127.0.0.1になります。

ディレクトリ共有

ソースをローカルPCで修正するために、Docker Mount + VirturlBox共有フォルダでソースを共有していましたが、Docker for Mac(Windows)では、VitrualBox共有フォルダがなくなりました。
Docker Toolboxのときは、VirtualBoxの共有フォルダ設定で、個人PCの作業ディレクトリの違いを吸収できたのですが、Docker for Mac(windows)ではこれがなくなるため、docker run時のマウント設定で調整する必要があります。

コンテナへのログイン

ランサーズのDocker環境ではsshdを稼働させており、docker-toolboxでは

docker-machine ssh xxxxx

として、docker-machineのVMにログイン後、sshコマンドで各種コンテナに一般ユーザーでログインしていました。
Docker for Mac(windows)では、docker-machine sshのログインプロセスがなく、ローカルPCから直接docker exec等でrootでログインするプロセスになります。

Kitematicを利用するとGUIから手軽にdocker execができますので、これを利用するのも良いと思います。
Kitematic

※ちなみに、Windows版KitematicでEXECを実行すると、Power Shellが起動します。私は、WindowsでDockerを操作するときはCygwin(Cygterm)を利用していたのですが、この環境だとdocker execが上手く動きません。そのため、Kitematicを利用していますが、ここでターミナルを選べないのが現状ちょっと辛いところです。

※sshを利用する方法として、どれかのコンテナの22番をポートマッピング設定し、それを踏み台にするという手は残っています。

Docker for Mac(Windows)のメリット

PC起動時のDocker設定が必要ない

PC起動時にDocker for Mac(Windows)を自動起動する設定にしておけば、事前準備が必要なくなります。
つまり、PCを起動する度に、以下のコマンドを打たなくても良くなります。

<

div class=”code panel pdl conf-macro output-block” data-hasbody=”true” data-macro-name=”code”>

<

div class=”codeContent panelContent pdl”>

<

div id=”highlighter_272017″ class=”syntaxhighlighter sh-midnight nogutter php”>

docker-machine start xxxxx
eval "$(docker-machine env xxxxx)"

Docker for Mac(Windows)のデメリット

複数のDocker VMの起動

VitualBoxを利用していたときは、例えば、192.168.99.100、192.168.99.101と2つのVMを作成し、それぞれのVM上でDockerコンテナを稼働することが手軽にできましたが、それができなくなります。

OS対応

ハイパーバイザーをサポートしたOSでないと動作しません。
Docker for MacはMacOS 10.10.3(Yosemite)以降の対応となります。
Docker for WindowsはWindows10以降の対応となります。

Linuxデスクトップ対応

Linuxデスクトップを利用するエンジニアも増えたため、Linux環境にも対応しました。

Linux環境でも、dockerとdocker-composeをインストールすれば、Mac、Windowsとほぼ同じように使えます。
違う点は、デフォルトでローカルPCからDockerコンテナのIPアドレスで直接コンテナにアクセスすることができることです。

Docker for Mac(Windows)のときと同様、VirtualBoxはないので、作業ディレクトリをDockerコンテナと共有する場合は、Docker Mountで動的に共有ディレクトリを調整する必要があります。

アップデートの移行状況

現在は、社内のアーリーアダプター中心に、順次アップデートを適用して頂いています。
現状の環境でも特に支障なく開発ができていますので、アップデートできる方からゆるやかに移行が進んでいる段階です。
今後も、互換性を保ちながら移行を進めていきたいと思います。

AWS

RDS for MySQLからAuroraへ移行しました

kanazawa|2016年01月29日
Aurora

インフラエンジニアの金澤です。

ランサーズでは、今月、DBMSをRDS for MySQLからAuroraに移行しました。
その内容について、2016/1/28に行われました「ヒカラボ MySQL勉強会」 で発表させていただきました。

Auroraの仕様詳細や、一般的なパフォーマンス測定の結果については、既に良い資料が発表されておりますので、今回の発表内容は「ランサーズで移行した結果どうだったのか?」を中心に発表させていただきました。

今回、ランサーズでAuroraに移行した目的は、主に以下の4点になります。

  1. パフォーマンスの向上
  2. メンテナンスなしでのカラム追加(Replica Lagの改善)
  3. TV対策(リードレプリカの上限が5台→15台に増加)
  4. 費用削減(MultiAZ分のインスタンス費用削減)

結果、パフォーマンスの向上以外については目的を達成できました。

パフォーマンスにつきましては、今後も継続的にウォッチし、改善できましたら再度報告させていただきたいと思います。

開発環境のDocker化

kanazawa|2015年12月10日
AWS

インフラエンジニアの金澤です。

今回、ランサーズの開発環境をDocker化しましたので、その内容を紹介いたします。

Docker移行の決断

Dockerについては、ここ1年で技術的な調査はしていましたが、決定的なメリットを見出していたわけではなく、採用に踏み切ってはいませんでした。

その一方で、開発規模は拡大し続けており、ランサーズ本体に加え、関連サービスのプロジェクトも並行して進むようになり、開発環境のリソース不足に悩まされるようになってきていました。

ランサーズの開発環境は、主に以下の構成で運用されています。

  • 個人PC
    • Virtualbox + Vagrant でCentOS環境を構築
    • Ansibleでプロビジョニング
  • 社内VMサーバー
    • VMWare 上にCentOS環境を構築
  • AWS検証用環境
    • EC2(CentOS、Amazon Linux)上に構築
    • Ansibleでプロビジョニング

個人PCの開発環境は、VirtualBoxにメモリ2GB、HDD 50GBのVMが稼働しています。
このVM上にランサーズが稼働しているわけですが、関連サービス(例えば、チャットサービス等)のプロジェクトが立ち上がると、その開発環境も必要になります。

関連サービスはランサーズと連動して動作しますが、ミドルウェア構成が異なるため、1つのVMに共存させるにはいろいろ不都合があります。
そのため、別VMを構築することになるのですが、個人PCに2台以上のVMを稼働させると、HDD、メモリ等のリソースが不足し始めてきます。

今回の開発環境のDocker移行は、差し迫った問題である、個人PCのリソース解決手段として導入しました。

Docker移行の方針

開発環境のDocker化は、以下の方針で行ないました。

  • 個人PC上にはDockerMachine(DockerToolbox)、VMWareにはCoreOSで構築する
  • 本番AWS環境のDocker化はしない
  • 極力、本番AWS環境と同じ構成にする
  • 個人でプロビジョニングはしない(コンテナを持ってくるだけ)
  • 開発環境、本番環境共に(ほぼ)同じコードでプロビジョニングできるようにする
  • Vagrant時代の利便性を維持する(共有ディレクトリ、XDebugによるステップ実行など)

「個人でプロビジョニングはしない」方針にしたのは、構築に時間がかかるためです。

Vagrant + Ansibleでプロビジョニングしていたときは、構築(特にMySQLデータのインポート)に時間がかかり、完了まで数時間ほどかかっていました。

そのため、積極的に再構築する人が減り、Ansible Playbookの更新が滞った結果、プロビジョニングエラーが増え、それを補うための手作業が増える悪循環に陥っていました。

また、Windowsの場合、Vagrant、Ansibleのバージョンなどによる相性がシビアで、構築がなかなか成功しないという悩みもありました。

そのため、Docker移行後は、コンテナのプロビジョニングは特定の人が行い、Dockerを使う人は、完成したコンテナをpullすれば構築できる手順にしました。

これらの方針で進めたため、一部、Dockerのベストプラクティスに従っていない方法も採用しています。

例えば、以下のような方法です。

  • Dockerfileだけでなく、Ansibleでもプロビジョニング
  • 1コンテナに2つ以上のサービスを稼働
  • sshdの稼働
  • root以外の一般ユーザーの作成
  • 固定IPの付与
  • MySQLにデータをインポートしたコンテナをそのままRegistryに登録

Dockerのベストプラクティスを実現させるには、リリース方法も踏まえた上で本番環境の構成にも手を入れる必要があります。
今回の目的は開発環境のリソース不足の解決がメインのため、その目的の達成に注力し、上記のような方法も採用しています。

開発エンジニアには、事前に以下のようなイメージを共有してから着手しました。


docker02

Dockerを使った開発環境構成2

 

移行時に直面した問題とその解決方法

コンテナのプロビジョニング

コンテナのプロビジョニングは、主にDockerfileで行いますが、本番環境のEC2で稼働するサービスのコンテナには、共通のAnsibleコードを利用してプロビジョニングします。

AWSのいわゆるマネージド系のサービス(ELB、RDS、Erasticache等)にあたるコンテナはDockerfileのみで構築、EC2で構築するサービスは、Dockerfileでベースを作成した上で、SSH経由でAnsibleのPlaybookを実行しています。

※docker-machineは再起動すると個人で作業したファイルが消えてしまうため、Playbookの実行用にAnsibleをインストールしたコンテナを別途用意しています。

Docker Registory

コンテナを格納するRegistryには、DockerHubなどのサービスがありますが、現状はどのサービスも有料で、サーバーも海外にあるものがほとんどです。

データをインポートしたMySQLのコンテナは数GBになるため、転送速度が遅いと運用に大きな影響を与えます。

そのため、プライベートレジストリを構築し、コンテナはAWS東京リージョンのS3に格納することにしました。

プライベートレジストリ用にサーバーを用意する選択肢もありましたが、Docker Registoryのコンテナが公開されていますので、それを各Docker Machine上で起動しています。

※コンテナをpull、pushするときのみ起動すればよいので、リソース的にはほとんど問題になりません。

Docker Registoryのコンテナ起動時に、以下のオプションを指定することで、格納先をS3に設定しています。

docker run -d \
    --name registry \
    -p 5000:5000 \
    -e REGISTRY_STORAGE_S3_ACCESSKEY=XXXXXXXX \
    -e REGISTRY_STORAGE_S3_SECRETKEY=xxxxxxxx \
    -e REGISTRY_STORAGE_S3_BUCKET=docker-registory-lancers \
    -e REGISTRY_STORAGE_S3_REGION=ap-northeast-1 \
    -e REGISTRY_STORAGE_S3_ROOTDIRECTORY=/ \
    -e REGISTRY_STORAGE=s3 \
registry:2.2

複数サービスの起動

Dockerは、1コンテナ1サービス、サービスの起動はフォアグラウンドが原則ですが、本番環境と同様の構成にするため、sshdを含め、サービスを複数起動しています。

複数サービスを起動するための手法については、様々な方法が紹介されていますが、以下のようなスクリプトで稼働させるのが最もわかりやすく、この方法を採用させていただきました。

service.sh
#!/bin/sh
/sbin/service sshd start
/sbin/service httpd start
 
while true
do
 sleep 10
done

Dockerfileに、起動時に上記のスクリプトを実行するように記述します。

Dockerfile
# service.shをコンテナに配置
COPY ./service.sh /root/
RUN chmod 744 /root/service.sh
 
# 起動時にservice.shを実行
CMD ["/root/service.sh"]

固定IPアドレスの付与

CentOS6の場合は、ip addr add コマンドで固定IPアドレスを付与できます。
docker run、docker start後に、以下のようにdocker execコマンドで付与しています。

DOCKER_ID=$(docker inspect --format="{{.Id}}" $DOCKER_NAME)
IP_STATIC=172.17.51.11
NIC=eth0
docker exec $DOCKER_ID ip addr add $IP_STATIC/16 dev $NIC

※つまり、コンテナ起動時に付与されるIPアドレスと、固定IPアドレスが2つ付与されている状態になります。

既存のIPアドレスの削除も試みましたが、iptablesの設定も必要で手順が煩雑になるためやめました。IPアドレスが衝突する可能性が僅かながらあるのですが、今は割り切って利用しています。

ELBコンテナ

AWSのELBに相当するコンテナを用意しています。

本番AWS環境では、ELB経由でAppサーバーにアクセスしています。
SSL Termination機能を利用しているため、Appサーバー内ではhttpしか処理していません。

その状態をシミュレートするため、ELBコンテナでhttps→http変換を行い、環境変数X-Fowarded-Protoをセットした上で、Appサーバーに処理を渡すように設定しています。

具体的には、以下のようなVirturlhostの設定を行っています。

ProxyAddHeaders Off

<VirtualHost *:80>
    ServerName dev.lancers.jp
    ServerAlias *.lancers.jp
    RequestHeader set X-Forwarded-Proto "http"
    RewriteEngine On
    RewriteCond %{SERVER_NAME} ^(.*).lancers.jp$
    RewriteRule ^/(.*) http://%1.lancers.jp/$1 [P,L,QSA]
</VirtualHost>
<VirtualHost *:443>
    ServerName dev.lancers.jp
    ServerAlias *.lancers.jp
    ErrorLog logs/ssl_error_log
    TransferLog logs/ssl_access_log
    LogLevel warn
    SSLEngine on
    SSLProtocol all -SSLv2
    SSLCipherSuite ALL:!ADH:!EXPORT:!SSLv2:RC4+RSA:+HIGH:+MEDIUM:+LOW
    SSLCertificateFile /etc/pki/tls/certs/lancers.jp.cert
    SSLCertificateKeyFile /etc/pki/tls/private/lancers.jp.key
    RequestHeader set X-Forwarded-Proto "https"
    RewriteEngine On
    RewriteCond %{SERVER_NAME} ^(.*).lancers.jp$
    RewriteRule ^/(.*) http://%1.lancers.jp/$1 [P,L,QSA]
</VirtualHost>

また、このELBコンテナは、lancers.jpドメイン以外のサービスも起動した場合に、同じIPアドレスのdocker-machineを利用するための、リバースプロキシの役目も果たしています。

共有ディレクトリ

docker-machineのVMを作成後、VirtualBoxの設定で以下のように共有フォルダーを設定することで、docker-machineのディレクトリとPC上のディレクトリを共有できます。

vb_setting_shared_folderさらに、docker run時に-vオプションを指定することでコンテナとdocker-machineのディレクトリを共有します。

これで、PC上で直接ソースコードの修正が可能となります。

以下は、コンテナの/var/wwwを共有した場合のコンテナ起動例です。

docker run \
--name $DOCKER_NAME \
-v /Users/www:/var/www \
--hostname $DOCKER_NAME \
--privileged \
-d $IMAGE_NAME

※ディレクトリの所有権の関係で、/var/www以下での書き込みは、root権限が必要になる場合があります。

hostsの設定

Dockerfileで/etc/hostsを編集してもコンテナには反映されません。
docker run時に–add-hostオプションを指定することで、hostsを設定できます。

docker run \
--name $DOCKER_NAME \
--hostname $DOCKER_NAME \
--add-host=dev.lancers.jp:172.17.50.11 \
--add-host=dev-img.lancers.jp:172.17.50.11 \
--privileged \
-d $IMAGE_NAME

ポートマッピング

ポートマッピングは、コンテナで起動したhttpのサービスをPC上で確認するために必ず設定することになりますが、MySQLコンテナにも設定しています。

以下のように設定することで、PC上からdocker-macineのIPアドレスで、直接MySQLコンテナのMySQLにアクセスできます。

Dockerfile
EXPOSE 3306

起動時に、-pオプションでポートをマッピング

IMAGE_NAME=mysql:latest
DOCKER_NAME=mysql-51-11
IP_STATIC=172.17.51.11
PORT=3306
 
docker run \
--name $DOCKER_NAME \
--hostname $DOCKER_NAME \
-p $PORT:$PORT \
--privileged \
-d $IMAGE_NAME

Xdebugによるステップ実行

Xdebugをインストールし、php.iniに以下の設定をすることで、PHP Storm等でステップ実行を可能にしています。
(※ステップ実行時に、xdebug.remote_autostart = Onにしてhttpdを再起動します)

php.ini

[xdebug]
zend_extension="/usr/lib64/php/modules/xdebug.so"
xdebug.remote_enable = On
xdebug.remote_port = 9000
;xdebug.remote_autostart = On
xdebug.remote_host = 192.168.99.1
xdebug.profiler_output_dir = "/tmp"
xdebug.max_nesting_level= 1000
xdebug.idekey = "PHPSTORM"

Docker化の効果

開発環境をDockerに移行したことで、複数のVMを起動する必要がなくなり、PCリソースの削減になったことはもちろんですが、加えて、短時間でかつ簡単に構築できるようにもなりました。

エンジニア以外の方も積極的に開発環境を構築するようになり、その結果、元エンジニアのディレクターが開発に復帰したり、子供が産まれたばかりのデザイナーも、リモートワークで子供の面倒を見ながら仕事を進めることができるようになりました。

また、データ変更や、インフラの構成変更時は、コンテナを破棄、更新するだけで良くなりました。

今後、ランサーへの開発依頼がよりスムーズに進むことも期待しています。

今後の展望

一定の効果はありましたが、いわゆるDockerのベストプラクティスにはまだ程遠い状態です。

今回のDocker移行で、エンジニアのDocker意識が高まりましたので、今後、マイクロサービス化も見据えながら、Docker導入をさらに進めていきたいと思います。

AWSのスポットインスタンス運用

kanazawa|2014年11月12日
AWS

はじめまして。ランサーズインフラエンジニアの金澤です。
AWSのスポットインスタンスを利用したことはありますか?
スポットインスタンスとは、AWSの余剰インスタンスを入札制で提供する仕組みです。
各インスタンスの1時間あたりの料金比較しても、スポットインスタンスは、(リアルタイムな相場の変動はありますが) オンデマンドインスタンスの1/10近い価格で提供されています。

Amazon EC2 料金表

http://aws.amazon.com/jp/ec2/pricing/

1 時間当たりの料金:アジアパシフィック(東京)Linuxの場合
(2014/11/13現在)

インスタンスクラス オンデマンド スポット 参考:リザーブド
(重度1年間)
t1.micro $0.026 $0.0031 $0.016
t2.micro $0.020 $0.005
m3.medium $0.101 $0.0115 $0.030
c3.large $0.128 $0.0212 $0.047
r3.large $0.210 $0.0265 $0.048
i2.xlarge $1.001 $0.228

 

デメリットは、以下の条件で強制的にTerminateされてしまうことです。

  • 入札価格よりスポット価格が高くなる
  • スポットインスタンスの在庫がなくなる

スポットインスタンスを利用する際は、Terminateのリスクを織り込んだ上で運用する必要があります。
ランサーズでは、検証用AWS環境をスポットインスタンスで運用しており、今回は、その手順、および運用方法を紹介いたします。

スポットインスタンスの起動方法

EC2インスタンスの作成時、Step3のPurchasing optionで「Request Spot Instances」をチェックすると、スポットインスタンス関連の項目が追加表示されます。

image2014-11-9 20-25-54

「Current price」が現在のスポット価格です。
スポットインスタンス稼働時に、この金額を超えるとTerminateされてしまいます。

Maximum priceの設定

「Maximum price」には、スポットインスタンス運用時に「最大で支払える金額」を指定できます。
インスタンスを起動する前に、過去のスポット価格の推移を調べておきましょう。
「Spot Requests」メニューから「Pricing History」を押すと確認できます。
image2014-11-9 22-40-18「Maximum price」を高く設定するほどTerminateされにくくなるわけですが、残念ながら設定できる値には上限値があります。

※上限値を超えて設定しようとすると、以下のメッセージが出力されます。

SpotRequestFailed

Maximum priceの上限値

上限値は、インスタンスクラスごとに変動します。
※調べた限りでは、以下の値が上限値のようです。

インスタンスクラス 上限値
t1.micro $0.108
m1.small $0.352
m1.medium $0.700
m1.large $1.400
m1.xlarge $2.800
m3.medium $0.684
m3.large $1.368
m3.xlarge $2.736
m3.2xlarge $5.472
c3.large $0.768
c3.xlarge $1.532
c3.2xlarge $3.064
c3.4xlarge $6.128
c3.8xlarge $12.256
cc2.8xlarge $2.960
m2.xlarge $2.020
m2.2xlarge $4.040
m2.4xlarge $8.080
hi1.4xlarge $0.1601

 

プライベートIPアドレスの指定(Webでは不可)

また、現時点では、Webコンソールからは固定プライベートIPアドレスを設定することができないようです。

「Request Spot instance」チェック前。
「Network interfaces」 でIPアドレスが設定可能です。

image2014-11-9 22-57-15
※「Request Spot instance」をチェックすると「Network interfaces」が消えてしまいます。

image2014-10-3 23-44-34

プライベートIPを指定してスポットインスタンスを作成する

Webコンソールからはできませんでしたが、AWS CLIを利用すれば、プライベートIPアドレスを指定してインスタンスを起動することが可能です。

AWS CLIによるスポットインスタンスのリクエスト

aws ec2 request-spot-instances \
  --spot-price “<入札価格>" \
  --instance-count 1 \
  --type "one-time" \
  --launch-specification "{ \
    \"ImageId\":\"\", \
    \"InstanceType\":\"<インスタンスタイプ>\", \
    \"NetworkInterfaces\":[{\"DeviceIndex\":0, \
    \"SubnetId\":\"<起動先サブネット>\",\
    \"PrivateIpAddresses\":[{\"PrivateIpAddress\":\"<プライベートIP>\", \"Primary\":\"true\"}] }], \
    \"KeyName\":\"<SSH鍵ペア名>\"
}"

スポットインスタンスのリクエストにあたっては、以下の点に注意が必要です。

  • リクエスト内にインスタンス名を含めることができない
  • Security Groupの設定とネットワークインターフェース設定(パブリックIP付与など)を同時に行うことができない

セキュリティグループはdefaultが割り当てられますので、起動後に適切なものに変更してください。

スポットインスタンス起動コマンドのオプションについて

オプション 説明
–instance-count 作成するインスタンス数を指定します。
※プライベートIPアドレスを指定して作成するので、1を入力します。
–launch-specifiation 起動するインスタンスの詳細設定をJSON形式で指定します。
–spot-price 入札価格をUSDで指定します。
–type “one-time”と”persistent”の2種類があります”one-time”では一回スポットインスタンスを起動したらリクエストが失効するのに対し、”persistent”を指定した場合はスポットインスタンス起動後も任意で入札をキャンセルするまでリクエストが維持されます。

 

その他のオプションにつきましては、以下のURLをご確認ください。
http://docs.aws.amazon.com/cli/latest/reference/ec2/request-spot-instances.html

スポットインスタンスの日次バックアップ

スポットで稼働中のEC2インスタンスについては、AMIを毎日取得し、Terminateされた場合は、このAMIから再度リクエストしています。
(※最近は発生してませんが、2014/4月~2014/6月頃は、在庫切れによるTerminateが度々発生していました。)

ランサーズでは、サーバーワークス社のCloudAutomatorを利用しています。
http://cloudautomator.com/

image2014-10-3 23-40-0

無償での利用には条件がありますが、バックアップの世代数が管理でき、バックアップ以外にも便利な機能がいくつもありますので、重宝しております。

最後に

スポットインスタンスは、Terminateのリスクがあるものの、適用領域とリスクコントロールを工夫すれば非常に安価に運用することができます。