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

Post on 14-Feb-2017

870 Views

Category:

Engineering

0 Downloads

Preview:

Click to see full reader

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

ご静聴ありがとうございました

top related