メルカリのデータベース戦略 / phpとmysqlの怖い話 myna会2015年8月
TRANSCRIPT
メルカリのデータベース戦略PHPとMySQLの怖い話
MyNA(日本MySQLユーザ会)会会会会会会会会会会 2015年8月Masahiro Nagano @kazeburo
@kamipo
@kamipoOracle 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 CPUSCSI または ATA HDD
SNSスケールアウトしたサーバをスケールアップし台数を抑える
64bit CPUSAS または 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 SQLSELECT 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_2xxFROM 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 Clustertable A,B,C table D table E,F,G,H
Multi-Source Replication
analyze-dbtable 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
ISUCON52015/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.phpPHP Warning: PDO::commit(): MySQL server has gone away in /private/tmp/hoge.php on line 14PHP Warning: PDO::commit(): Error reading result set's header in /private/tmp/hoge.php on line 14Hello!!$
commit() はエラーになっても例外を出さないエラーを別途補足して例外に変換
http://php.net/manual/ja/pdo.commit.phpには例外に関することが書かれてない
<?phpset_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.phpPHP Fatal error: Uncaught exception 'PDOException' with message 'There is no active transaction' in /private/tmp/hoge.php:17Stack trace:#0 /private/tmp/hoge.php(17): PDO->rollBack()#1 {main} thrown in /private/tmp/hoge.php on line 17$
ただし
$ rpm -qa|grep phpphp-5.3.3-27.el6_5.x86_64$ php -iPDO Driver for MySQL => enabledClient API version => 5.1.70
$ php hoge.phphello!!$
アイエエエエ!ナンデ!ウゴクナンデ!
• 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 phpphp-5.3.3-27.el6_5.x86_64$ php -iPDO Driver for MySQL => enabledClient API version => 5.1.70
$ php fuga.phphello!$
$ php -vPHP 5.6.5 (cli) (built: Jan 28 2015 16:00:57)
$ php fuga.phpPHP 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怖くない(´・ω・`)
おしまい