メルカリのデータベース戦略
PHPとMySQLの怖い話
MyNA(日本MySQLユーザ会)会会会会会会会会会会 2015年8月
Masahiro Nagano @kazeburo
@kamipo
@kamipo
Oracle ACE おめでとうございます!!
Me
•長野雅広(Masahiro Nagano)
•@kazeburo
•Mercari, Inc.
•Operations Engineer, Site Reliability
•ISUCON芸人
メルカリの
データベース戦略
WEB+DB PRESS vol.88
メルカリのデータベース
について書きました
主要KPI
ダウンロード数
購入金額
出品数
2000万DL(JP+US)
月間数十億円
1日数十万品以上
Webアプリケーションの
スケール戦略
期 スケール戦略 ハードウェア/その他
Blog スケールアウト
32bit CPU
SCSI または ATA HDD
SNS
スケールアウトしたサーバを
スケールアップし台数を抑える
64bit CPU
SAS または SATA HDD
KVS の活用
ソーシャルゲーム スケールアップ
SSD、PCIE接続の
フラッシュデバイス
スマートフォン
アドテク
スケールアップしたサーバを
スケールアウト
SSD、PCIE接続の
フラッシュデバイス
NoSQL、クラウド
2000万DLを支えるインフラ
•JP: クラウド + 専用サーバ
専用Private LANで接続
•US: AWS
クラウドとオンプレの環境の両方を利用
2000万DLを支えるMySQL
•MySQL 5.6系を利用
•JP: 専用サーバ + ioMemory
•US: RDS
急増するデータへの対策
•データベースをテーブル毎に分割
•MySQL以外のデータベース・ミドルウ
ェアの利用
データベース分割
• 対象となるテーブルをそのまま別サーバに移動
• テーブル内のデータを複数台のサーバに分散する
Sharding はまだしていない
• 移動するテーブル
• データサイズの大きなテーブル
• 商品の購入など、トランザクションに含まれないテー
ブル
Master Backup
Master
Slave Backup
Master Backup Master Backup
Main Cluster Sub Cluster Sub2 Cluster
分割
スケールアップとスケールアウトを組み合わせた構成
table A,B,C table D table E,F,G,H
接続先の管理
$cluster1 = array('dsn' => 'mysql:host=db10;dbname=mercari');
$cluster2 = array('dsn' => 'mysql:host=db12;dbname=mercari');
$db_config = array();
$db_config['main'] = $dsn;
$db_config['todo_master'] = $cluster1
$db_config['comments_master'] = $cluster2
$db_config['likes_master'] = $cluster2
public static function conn($key = 'main') {
new PDO($db_config[$key],$user,$pass);
}
$pdo = MyDB::conn('todo_master');
$pdo->prepare('SELECT * FROM todo WHERE..');
テーブル、機能ごとに
接続先を管理
さらなる分割も視野にいれた仕組み
MySQL以外の
データストア/処理ミドルウェア・サービス
• データの一時的キャッシュ
• memcached
• 新着商品リスト
• Redis
• ログデータ分析
• Treasure Data
• BigQuery
• Norikra
• KPI
• MySQL 5.7
Treasure Data
BigQuery
ログデータ分析基盤
クラウドで爆発的に増えるデータを処理する
超大規模でもない限り、分析基盤を自前で構築するメリットは薄い
App
App
App
ログのStream処理
App
App
App SQLを投入
Norikraを使い、ログをリアルタイムに集計して
Mackerelで可視化、Slackに通知
Mackerel
Slack
Norikra SQL
SELECT
COUNT(1, status like "5%")/COUNT(1)*100 AS rate_5xx,
COUNT(1, status like "4%")/COUNT(1)*100 AS rate_4xx,
COUNT(1, status like "3%")/COUNT(1)*100 AS rate_3xx,
COUNT(1, status like "2%")/COUNT(1)*100 AS rate_2xx
FROM
access_log.win:time_batch(1 min)
WHERE
ua NOT LIKE '%some_bot%'
1分間のtime window毎に集計
Mackerel
グラフによる可視化に加え
アラートの設定ができる
Slackへの通知
エラーログをNorikraで集計してSlack通知
分析でのMySQLの利用
•KPI集計
•アドホックな分析、調査
✓ 3つのクラスタを統合して使いやすく
✓ 個人情報の取り扱い
MySQL 5.7
•Multi-Source Replication
•Trigger で書き換え
Master
Slave Backup
Master Backup Master Backup
Main Cluster Sub Cluster Sub2 Cluster
table A,B,C table D table E,F,G,H
Multi-Source Replication
analyze-db
table A,B,C,D,E,F,G,H...
Multi-Source Replicationの
使い方
CHANGE MASTER TO MASTER_HOST='db1',.. FOR CHANNEL 'db1';
START SLAVE FOR CHANNEL ‘db1’;
STOP SLAVE FOR CHANNEL ‘db1’;
SHOW SLAVE STATUS FOR CHANNEL ‘db1’G
FOR CHANNEL をつけるだけ。問題なく動作している
Triggerで書き換え
CREATE TRIGGER insert_user_address
BEFORE INSERT ON user_address
FOR EACH ROW
BEGIN
SET NEW.family_name = MD5(concat(NEW.family_name,'secret_key'));
SET NEW.first_name = MD5(concat(NEW.first_name,'secret_key'));
END;
CREATE TRIGGER update_user_address
BEFORE UPDATE ON user_address
FOR EACH ROW
BEGIN
SET NEW.family_name = MD5(concat(NEW.family_name,'secret_key'));
SET NEW.first_name = MD5(concat(NEW.first_name,'secret_key'));
END;
MD5でhashに変更
ユニーク性は確保
前半終了
CM
ISUCON5
2015/9/26-27 予選
2015/10/31 本選
/CM
PHPとMySQLの怖い話
2つほど..
PHPはじめました
この6ヶ月の間にハマったことを紹介します
1. commit() が例外を出さない
あるいはPHPの例外とエラーについて
<?php
$pdo = new PDO('mysql:dbname=test;host=127.0.0.1');
$pdo->query('SET SESSION wait_timeout=1');
$pdo->beginTransaction();
try {
sleep(2);
$pdo->commit(); # ここでエラー
} catch( Exception $e) {
$pdo->rollBack();
throw $e;
}
echo "Hello!!";
[kazeburo@kazeburomba2-2 /tmp]% php -v
PHP 5.6.5 (cli) (built: Jan 28 2015 16:00:57)
$ php hoge.php
PHP Warning: PDO::commit(): MySQL server has gone away
in /private/tmp/hoge.php on line 14
PHP Warning: PDO::commit(): Error reading result set's
header in /private/tmp/hoge.php on line 14
Hello!!
$
commit() はエラーになっても例外を出さない
エラーを別途補足して例外に変換
http://php.net/manual/ja/pdo.commit.php
には例外に関することが書かれてない
<?php
set_error_handler(function ($severity, $message, $file, $line) {
throw new ErrorException($message, 0, $severity, $file,
$line);
});
$pdo = new PDO('mysql:dbname=test;host=127.0.0.1', 'root', ‘’);
$pdo->query('SET SESSION wait_timeout=1');
$pdo->beginTransaction();
try {
sleep(2);
$pdo->commit();
} catch( Exception $e) {
$pdo->rollBack();
throw $e;
}
echo "Hello!!";
$ php hoge.php
PHP Fatal error: Uncaught exception 'PDOException'
with message 'There is no active transaction' in /
private/tmp/hoge.php:17
Stack trace:
#0 /private/tmp/hoge.php(17): PDO->rollBack()
#1 {main}
thrown in /private/tmp/hoge.php on line 17
$
ただし
$ rpm -qa|grep php
php-5.3.3-27.el6_5.x86_64
$ php -i
PDO Driver for MySQL => enabled
Client API version => 5.1.70
$ php hoge.php
hello!!
$
アイエエエエ!ナンデ!ウゴクナンデ!
•PHPのアップデート
•mysqlndの利用(PHP5.3でも問題なし)
•commit() の前に query(”SELECT 1”)
•PDOに ping() が欲しい
2. Empty row packet body
<?php
$pdo = new PDO('mysql:dbname=test;host=127.0.0.1', 'root', '');
$pdo->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false);
$sth = $pdo->prepare('SELECT * FROM buffer');
$sth->execute();
while ($rows = $sth->fetch(PDO::FETCH_ASSOC)) {
#job($rows)
}
echo “hello!n”;
十分に大きい
テーブル
$ rpm -qa|grep php
php-5.3.3-27.el6_5.x86_64
$ php -i
PDO Driver for MySQL => enabled
Client API version => 5.1.70
$ php fuga.php
hello!
$
$ php -v
PHP 5.6.5 (cli) (built: Jan 28 2015 16:00:57)
$ php fuga.php
PHP Warning: Empty row packet body in /private/tmp/
fuga.php on line 23
Warning: Empty row packet body in /private/tmp/fuga.php
on line 23
$
アイエエエエ!ナンデ!エラーナンデ!
•unbuffered queryを使わない
•net_write_timeout を伸ばす
<?php
$pdo = new PDO('mysql:dbname=test;host=127.0.0.1', 'root', '');
$pdo->exec("CREATE TABLE IF NOT EXISTS buffer (
buf varchar(256)
)");
$data = array();
for ($i = 0; $i < 100; $i++) $data[] = str_pad('', 256);
for ($k=0; $k < 500; $k++ ) {
$sql = "INSERT INTO buffer VALUES " .implode(",", array_fill(0, count($data),
"(?)")) . "";
$stmt = $pdo->prepare($sql);
$stmt->execute($data);
}
$pdo = new PDO('mysql:dbname=test;host=127.0.0.1', 'root', '');
$pdo->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false);
$pdo->query('SET SESSION net_write_timeout=1');
$sth = $pdo->prepare('SELECT * FROM buffer');
$sth->execute();
while ($rows = $sth->fetch(PDO::FETCH_ASSOC)) {
usleep(1000);
}
再現コード置いておきます
PHP怖くない(´・ω・`)
おしまい

メルカリのデータベース戦略 / PHPとMySQLの怖い話 MyNA会2015年8月