SlideShare a Scribd company logo
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
APCu
master_music-v2
APCu
master_music-v2
master_card-v2
Req
1 GET
version
2 GET
master_card-v2
3 GET
master_card-v2
4 SET
master_card-v2
1 GET
version
2 GET
master_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
SELECT
2. 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
プレイヤーデータのキャッシュ
queryA
id:1
queryB
type:1
queryAの検索条件:{ id: 1, type: 1 }
queryBの検索条件:{ type: 1 }
queryA ⊇ queryB
(queryAがqueryBの上位集合)
resultB
id:2, type:1
resultA
id: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=utf8mb4
partition 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 = B
Token = B
OK Token = C
NG 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: 1core
Memory: 1GB       PHP: nginx × PHP-FPM × OPcache
実行速度はPHP7 ≧ Zephir > > > PHP5.6
ご静聴ありがとうございました

More Related Content

Aws×phpでの 高信頼かつハイパフォーマンスなシステム