aws×phpでの 高信頼かつハイパフォーマンスなシステム
TRANSCRIPT
AWS×PHPでの高信頼かつハイパフォーマンスなシステム
2017.2.7 伊藤皓程
伊藤 皓程(いとう こうてい)
2014年(株)サイバーエージェントアルバイト
2015年(株)サイバーエージェント 入社
2016年(株)QualiArts 出向
所属プロジェクト
● by.S ● ガールフレンド(♩)
● ボーイフレンド(仮) きらめきノート
自己紹介
アジェンダ
1. はじめに(2分)
2. キャッシュの話(8分)
3. 自動化・自動生成の話(6分)
4. Auroraの話(3分)
5. その他(1分)
はじめに
はじめに
事前登録25万人突破し
2016年11月にリリース!
AppleStore
無料ランキング1位獲得
はじめに
リリースから約2ヶ月で以下の5種類の新イベントを11回開催
● マラソン
● レイド
● ハイスコア
● PVP
● バレンタイン
はじめに
システムの可用性
メンテナンス: 2回 6時間(1時間で終わるはずだった…)
システム障害: 2回 10分
稼働率: 約99.5%
APIサーバのパフォーマンス
平均レスポンス時間(動的コンテンツのみ): 160ms
1台あたりの最大スループット(c4.2xlarge): 350req/sec
はじめに
アーキテクチャ図 Auroraシャーディングは行わない
SELECTはすべてReaderを使用
ElastiCache(Redis)インスタンスタイプはm4.largeに抑
えて多く並べる
Webview、クライアントのマスター
データ、Assetsなどの静的コンテ
ンツはCDNで配信
はじめに
複雑・大規模Webサービスでの高信頼性かつハイパフォーマンスなシステムを
実現するには…
● 高信頼性・高可用性○ 冗長性を担保する(MultiAZなど)
○ マネージドサービスを活用する( RDS, ElastiCacheなど)○ APIが少ないコード量で実装できるような基盤を作る
○ 自動化・自動生成を行う
○ ユニットテストやデバック機能を充実させる
● ハイパフォーマンス○ PHPのバージョンを上げる(PHP7)○ Auroraを使用する
○ 各種キャッシュの活用する
○ BOT対策をする
コードを書かなけれ
ばバグは生まれな
い…
キャッシュの話
キャッシュの話
まずはPHPの仕様をざっくりと…
● リクエストごとに独立したメモリ空間を持つ
○ ステートレスで起動するのは悪くないが、パフォーマンスは劣化する
○ リクエストを横断した変数の共有には工夫が必要
● リクエストごとにスクリプトの読み込みとコンパイルが発生する
○ フルスタックなフレームワークを使用するとかなりパフォーマンスの劣化する
=> APCu+OPcacheを使用する
キャッシュの話
APCu OPcache
Req Req Req
共有
Add Get Set
スクリプト
コンパイル
最適化
実行
キャッシュ
初回
実行
以降
キャッシュ
キャッシュの話
キャッシュのスコープと保存方法
1. リクエスト
a. プレイヤーキャッシュ : 変数で保存する
2. サーバ
a. マスターキャッシュ : APCuで保存する
b. コードキャッシュ : OPcacheで保存する
3. 共通
a. マスターキャッシュ : ElastiCache(Redis)で保存する
b. ランキング: ElastiCache(Redis)で保存する
c. その他のキャッシュ : ElastiCache(Redis)で保存する
マスターデータ
マスターデータのキャッシュの話
version: v2
master_card-v2: {...}
master_music-v2: {...}
master_card-v3: {...}
master_music-v3: {...}
ElastiCacheAPI Server
API Server
APCumaster_music-v2
APCumaster_music-v2master_card-v2
Req
1 GETversion
2 GETmaster_card-v2
3 GETmaster_card-v2
4 SETmaster_card-v2
1 GETversion
2 GETmaster_card-v2
Req
マスターデータのキャッシュの話
正規化されたデータを整形してキャッシュする
各APIで整形する必要がなくなるのでロジックがシンプルになる。計算量も減少
する
master_event
master_event_music
master_event_reward
master_event_stage
master_event_episode
master_event_card
master_event{ “music_list”: [
{ “music_id”, “stage_map”: {} } ], “episode_list”: [
{ “episode_id”, “reward_list”: [] } ],}
マスターデータのキャッシュの話
キャッシュのフォーマットの比較
環境: PHP7, OS: CentOS 6.5, CPU: 1core, memory: 1GB
マスターデータ : カードマスター , 27カラム, 1000レコード
Serializeが約3倍に速い!Serializeのほうが良い理由は速さだけじゃない。
JSON Serialize
エンコード(1000回) 2.16s 0.86s
デコード(1000回) 4.83s 1.76s
メモリサイズ 608KB 692KB
マスターデータのキャッシュの話
Serializeを使用することでマスターデータのクラスのオブジェクトをそのままキャッシュ可
能!
ロジックがさらにシンプルになる。
// イベントエピソードを読むAPIのロジックのイメージ
$master_event_cache = MasterEventCache::forge();
$master_event_string = master_event_cahce->get($event_id); // エンコードされたオブジェクト
$master_event = unserialize($master_event_string); // デコードする(実際はgetする時にunserializeしている)
$master_event->check_term(); // 期間チェック
$master_event_episode = $master_event->get_music($episode_id); // イベントエピソードのモデルを取得
$master_event_episode->provide_reward(); // イベントエピソードを読んだ報酬付与
プレイヤーデータ
プレイヤーデータのキャッシュ
● DBへのアクセス回数を減らしたい
○ 取得データのキャッシュ
■ 同一レコードは1回目はDBからデータを取得、2回目以降はキャッシュから取得
○ 保存情報をまとめるため追加・更新・削除データのキャッシュ
■ 同一レコードを修正しても、キャッシュを利用し保存クエリは 1回のみ発行
● リクエストの最後で初めて更新処理を実行したい
○ 途中でエラーになった際余分なロールバックをさせたくない
■ 仮想通貨は基板側で持っておりロールバックできないため、クエリ保存 ->仮想通貨の消
費・増加->コミットという順番で行いたい
プレイヤーデータのキャッシュ
A
B
C
D
Aurora
SELECT2. SELECT A,B,C
A
B
C
1. SELECT A,B,Cプレイヤーキャッシュ
UPDATE
プレイヤーデータのキャッシュ
A
B
C
D
Aurora
SELECT
A
B
C
1. UPDATE Aプレイヤーキャッシュ
UPDATE
プレイヤーデータのキャッシュ
A
B
C
D
Aurora
SELECT
A
B
C
1. INSERT Eプレイヤーキャッシュ
INSERT
E
UPDATE
プレイヤーデータのキャッシュ
A
B
C
D
Aurora
SELECT
A
B
C
1. SELECT A, B, Eプレイヤーキャッシュ
INSERT
E
UPDATE
プレイヤーデータのキャッシュ
A
B
C
D
Aurora
SELECT
A
B
C
1. SELECT A, Dプレイヤーキャッシュ
INSERT
E
2. SELECT A,D
D
UPDATE
プレイヤーデータのキャッシュ
A
B
C
D
Aurora
SELECT
A
B
C
1. COMMITプレイヤーキャッシュ
INSERT
E
D
2. INSERT E
3. UPDATE A
4. COMMIT
プレイヤーデータのキャッシュ
queryAid:1
queryBtype:1
queryAの検索条件:{ id: 1, type: 1 }queryBの検索条件:{ type: 1 }
queryA ⊇ queryB(queryAがqueryBの上位集合)
resultBid:2, type:1
resultAid:1, type:1
resultA:{ id: 1, type: 1 }resultB:{ id: 1, type: 1 }, { id: 2, type: 1 },...,resultA ⊆ resultB(resultAはresultBの部分集合)
自動化・自動生成の話
自動化・自動生成の話
スプレットシートからDDLの自動生成
DBのスキーマからModelクラスの自動生成
DBのスキーマからテストのMockの自動生成
DBのスキーマからDBのJsonSchemaを自動生成
DBのJsonSchemaからクライアントのクラスを自動生成
JsonSchemaからマスターデータのバリデーションの自動化
Req/ResのJsonSchemaからクライアントのクラスを自動生成
Req/ResのJsonSchemaからAPI定義書の自動生成
スプレットシートからDDLの自動生成
自動化・自動生成の話
drop table if exists player_main_episode cascade;create table player_main_episode (player_id int(10) unsigned NOT NULL comment 'プレイヤー ID',main_episode_id int(10) unsigned NOT NULL comment 'エピソード ID',read_flg tinyint(3) unsigned NOT NULL comment '既読フラグ 0: 未読,1: 既読
',created_at datetime comment '作成日 ',updated_at datetime comment '更新日 ',deleted_at datetime default null comment '削除日 セットされるとレコード削除
扱い ',constraint player_main_episode_PKC primary key (player_id,main_episode_id))comment 'プレイヤーメインエピソード ' ENGINE=InnoDB DEFAULT CHARSET=utf8mb4partition by linear hash (player_id) partitions 16;
'
自動化・自動生成の話
DBのスキーマからModelクラスの自動生成
class Model_Db_PlayerMainEpisode extends Model_OwnPlayer{ protected static $_table_name = 'player_main_episode'; protected static $_primary_key = ['player_id', 'main_episode_id']; protected static $_properties = [ 'player_id' => [ 'schema' => [ 'data_type' => 'int', 'constraint' => 10, 'unsigned' => true, 'null' => false, 'comment' => 'プレイヤー ID', ], 'validation' => [ 'required', 'numeric_min' => [0], 'numeric_max' => [4294967295] ] ], 'main_episode_id' => [ 'schema' => [ 'data_type' => 'int', '
自動化・自動生成の話
DBのスキーマからテストのMockの自動生成
{ "default": { "player_id": 1, "main_episode_id": 1, "read_flg": 0, "created_at": "2015-01-01 00:00:00", "updated_at": "2015-01-01 00:00:00", "deleted_at": null }, "data": []}
自動化・自動生成の話
DBのスキーマからDBのJsonSchemaを自動生成
{ "$schema": "http://json-schema.org/draft-04/schema#", "title": "MasterMainEpisodeStory", "type": "object", "properties": { "MainEpisodeId": { "type": "integer", "description": "メインエピソードID" }, "ChapterId": { "type": "integer", "description": "章ID" }, "StoryId": { "type": "integer", "description": "話ID" },
自動化・自動生成の話
DBのJsonSchemaからクライアントのクラスを自動生成
{ "$schema": "http://json-schema.org/draft-04/schema#", "id": "definitions/player/characterpresent.json", "title": "MainEpisodeInfo", "type": "object", "properties": { "MainEpisodeId": { "type": "integer", "description": "メインエピソードID" }, "ReadFlg": { "type": "integer", "description": "0:未読, 1:既読" } }, "keys": ["MainEpisodeId"], "required": ["MainEpisodeId", "ReadFlg"]}
using System;using System.Collections.Generic;
namespace XXX{ /// <summary> /// No document /// </summary> [Serializable] public class MainEpisodeInfo : PlayerInfoBase<MainEpisodeInfo> { /// <summary> /// メインエピソードID /// </summary> public int MainEpisodeId { get; set; }
/// <summary> /// 0:未読, 1:既読
/// </summary> public int ReadFlg { get; set; }
}}
自動化・自動生成の話
DBのJsonSchemaからマスターデータのバリデーションの自動化
{ "$schema": "http://json-schema.org/draft-04/schema#", "title": "master_main_episode_story", "type": "object", "properties": { "main_episode_id": { "type": "integer", "description": "メインエピソードID" }, "chapter_id": { "type": "integer", "description": "章ID", "relation": { "table": "master_main_episode_chapter", "column": "chapter_id" } } }}
'
自動化・自動生成の話
Req/ResのJsonSchemaからクライアントのクラスを自動生成
{ "$schema": "http://json-schema.org/draft-04/schema#", "id": "req/episode/readevent", "type": "object", "properties": { "EventEpisodeId": { "type": "integer", "description": "イベントエピソード ID" } }, "required": ["EventEpisodeId"]}
using System;using System.Collections.Generic;
namespace XXX{ /// <summary> /// No document /// </summary> [Serializable] public class RequestEpisodeReadmain : RequestBase { /// <summary> /// メインエピソード ID /// </summary> public int MainEpisodeId { get; set; }
}}
自動化・自動生成の話
Req/ResのJsonSchemaからAPI定義書の自動生成
{ "$schema": "http://json-schema.org/draft-04/schema#", "id": "req/episode/readevent", "type": "object", "properties": { "EventEpisodeId": { "type": "integer", "description": "イベントエピソード ID" } }, "required": ["EventEpisodeId"]}
自動化について
ボイきらではプレイヤーデータを
差分管理しています
ログイン処理時にクライアント側に自分に関する全プレイヤーデータを返却。そ
れ以降は変更・追加・削除があったもののみ返却。
自動化について
ログイン
対象プレイヤーの全データ返却
ガチャ
INSERT, UPDATE, DELETEが発生したレコードの情報を共通レスポンスとして返却
プレイヤーデータの差分管理をクライアント、サーバ共に基盤部分で自動で管
理している。つまり各APIではViewに影響を与える見える部分のレスポンスの
みを実装すれば良いので楽になる!
Auroraの話
RDSとAuroraのMulti-AZの比較
Auroraの話
RDS Aurora書き込み 同期(ミラーリング) 非同期(Quorum方式)
リードレプリカ インスタンス追加 セカンダリを使用可能
レプリ遅延(最大) N秒 Nミリ秒(概ね20ms以内)
リードレプリカのフェイルオーバー
手動 自動
リードレプリカのエンドポイント
なし あり
=> Auroraを使うならReaderを待機系として遊ばせるのは勿体ない!
レプリケーションはバグの温床・・・
● ボタン連打や不正ツールによる並列リクエスト
● 急な負荷増加によるレプリ遅延時間の増加
● 同一リクエストの処理内で更新したレコードに対する再取得
Auroraの話
並列リクエストの対策
Auroraの話
1. 各コントローラの最初でユーザIDを使用してロック
2. Writerとリクエストのトークンをチェック
a. 同じだった場合、正常処理後にトークンを更新して返却
b. 違った場合、エラーとして処理
OK Token = B
Token = BToken = B
OK Token = CNG Token = C IGNORE
但し、意図せぬ連打でエラーダイアログが出るのはユーザ体感が悪いため、「クライアントは何もしないエラー」として制御する
レプリ遅延増加の対策
Auroraの話
1. WriterとReaderのトークンをチェック
a. 同じだった場合、正常処理後にトークンを更新して返却
b. 違った場合、エラーとして処理
OK Token = B
Token = B
NG Token = B Retry
但し、レプリ遅延の増加時にエラーダイアログが頻発すると、ユーザ体感が悪いため、「同じリクエストでリトライするエラー」として制御する
Writer
Reader
Token = B
Token = A
Token = B
レコード更新後の再取得の対策
Auroraの話
プレイヤーキャッシュの仕組みに
よって起きない
その他
Zephirについて
● C言語を書かずに、PHPのエクステンションを作成可能
● PHPライクな構文で静的+動的言語
● PHPの組み込み関数を使用可能
● PhalconPHPのv3がC言語からZephirに移行
● PHPよりも高速に動作
ハイパフォーマンスPHP
=> PHP7だったら?…ということで検証してみました。
Zephir vs PHP7
ハイパフォーマンスPHP
PHP5.6 Zephir PHP7
each(1,000,000) 470ms 167ms 105ms
without(1,000,000) 4800ms 90ms 40ms
search(1,000,000) 30ms 20ms 20ms
repeat(1,000,000) 80ms 10ms 10ms
フィボナッチ数列(38) 16s 23s 9s
検証環境
OS: CentOS 6.5 (vagrant) CPU: 1coreMemory: 1GB PHP: nginx × PHP-FPM × OPcache
実行速度はPHP7 ≧ Zephir > > > PHP5.6
ご静聴ありがとうございました