続・Mobage を支える技術

YAPC::Asia 2012 - 09/28 at 東京大学
Yuji Shimada (xaicron)

Me

Me

CPAN Author

static/img/metacpan.png

Me

デザインセンスがとてもよい

static/img/blog.png

Me

New! 最近ぎっくり腰になりました

Works

宣伝

宣伝

春頃に「Mobage を支える技術」という本を書きました。

static/img/zuttomo-book.jpg

⇡これね

他にうちの部では

static/img/zigorou.jpg @zigorou

static/img/nekokak.png @nekokak

の二人が書いています。

ちなみに

static/img/zigorou.jpg

static/img/nekokak.png

static/img/xaicron.jpg

ちなみに

static/img/zigorou.jpg JPA 理事

static/img/nekokak.png JPA 理事

static/img/xaicron.jpg 路傍の石

はい

紹介ブログの名言集

marqs さんには寿司ビールをおごろうと思います。

みんな買ってね♡

static/img/zuttomo-book.jpg

宣伝終わり

諸注意

諸注意

発表中に dankogai ばりにガンガン質問してもらって大丈夫です。

免責

免責

今日は

免責

今日は

免責

今日は

というような話はしないのでご了承ください

Agenda

落ち穂拾い的な話

落ち穂拾い的な話

@Yappo

「個人的には worker でのシグナル処理入ってて欲しかった。」

なるほど、
話しましょう

Worker でのシグナル処理について

の前に

Worker のおさらい

Worker のおさらい

static/img/worker_introduce.png

Worker のおさらい2

まずは愚直に実装してみる

Parallel::Prefork を使った worker の実装例

use strict;
use warnings;
use DBI;
use Parallel::Prefork;

my $queue_table  = 'neko_queue';
my $connect_info = [ ... ];

my $pm = Parallel::Prefork->new({
    max_workers  => 10,
    trap_signals => {
        TERM => 'TERM',
        HUP  => 'TERM',
    },
});

while ($pm->signal_received ne 'TERM') {
    $pm->start(sub {
        my $q4m = DBI->connect(@$connect_info);
        my $index = $q4m->selectrow_array(
            'SELECT queue_wait(?)',
            undef,
            $queue_table,
        );
        return unless $index; # queue not found

        my $queue = $q4m->selectrow_hashref(
            'SELECT * FROM ' . $queue_table,
        );
        # do something

        $q4m->do('SELECT queue_end()');
    });
}

$pm->wait_all_children;

やったーできたよー!

...

Q. いまのコードの問題点はなにか?

Q. いまのコードの問題点はなにか?

A. シグナルを受け取ると子プロセスが処理中でも死ぬ

DEMO

子プロセスでのシグナル処理

...
while ($pm->signal_received ne 'TERM') {
    $pm->start(sub {
        # 追加
        my $signal_received = 0;
        $SIG{TERM} = sub {
            $signal_received = 1;
        };

        my $q4m = DBI->connect(@$connect_info);
        my $index = $q4m->selectrow_array(
            'SELECT queue_wait(?)',
            undef,
            $queue_table,
        );
        return unless $index; # queue not found

        # シグナルを受け取っていたら終了する
        return if $signal_received;

        my $queue = $q4m->selectrow_hashref(
            'SELECT * FROM ' . $queue_table,
        );
        # do something

        $q4m->do('SELECT queue_end()');
    });
}
...

Q. これで問題ないか?

Q. これで問題ないか?

A. 全然ダメですね

いまのコードの問題点

DEMO

→ Sys::SigAction を使うと良い

Sys::SigAction でシグナルトラップ

use POSIX qw(:signal_h);
use Sys::SigAction qw(set_sig_handler);

...
while ($pm->signal_received ne 'TERM') {
    $pm->start(sub {
        my $signal_received = 0;

        # Sys::SigAction を使う
        my $h = set_sig_handler(
            'TERM',
            sub {
                $signal_received = 1;
            },
            { flags => SA_RESTART },
        );

        my $q4m = DBI->connect(@$connect_info);
        my $index = $q4m->selectrow_array(
            'SELECT queue_wait(?)',
            undef,
            $queue_table,
        );
        return unless $index; # queue not found

        # シグナルを受け取っていたら終了する
        return if $signal_received;

        my $queue = $q4m->selectrow_hashref(
            'SELECT * FROM ' . $queue_table,
        );
        # do something

        $q4m->do('SELECT queue_end()');
    });
}
...

解説

Q. これで完璧か?

Q. これで完璧か?

A. 相変わらず queue_wait() でブロック

DEMO

やりたいこと

正しくシグナルトラップするコード

...
while ($pm->signal_received ne 'TERM') {
    $pm->start(sub {
        my $signal_received = 0;

        my $h = set_sig_handler(
            'TERM',
            sub {
                $signal_received = 1;

                # 追加
                my $sth = $DBI::lasth;
                if ($sth && $sth->{Database}{private_in_queue_wait}) {
                    die 'RECEIVED TERM SIGNAL into queue_wait()';
                }
            },
            { flags => SA_RESTART },
        );

        my $q4m = DBI->connect(@$connect_info);

        $q4m->{private_in_queue_wait} = 1; # 追加
        my $index = $q4m->selectrow_array(
            'SELECT queue_wait(?)',
            undef,
            $queue_table,
        );
        $q4m->{private_in_queue_wait} = 0; # 追加

        return unless $index; # queue not found

        # シグナルを受け取っていたら終了する
        return if $signal_received;

        my $queue = $q4m->selectrow_hashref(
            'SELECT * FROM ' . $queue_table,
        );
        # do something

        $q4m->do('SELECT queue_end()');
    });
}
...

解説

queue_wait() について

ちなみに、queue_wait('table') はデフォルトでは 60秒で timeout しますが、
queue_wait('table', 10) とかやると 10秒で timeout します。

でも俺は即死して欲しいんだ!!!!!11

Q. これで完璧か?

Q. これで完璧か?

A. まぁまぁいいけど、プロセスのライフサイクルが短すぎる

無駄

無駄

無駄

無駄

無駄

無駄

DEMO

やりたいこと

最終決定版

# 追加
my $max_requests_per_child = 10000;

...
while ($pm->signal_received ne 'TERM') {
    $pm->start(sub {
        my $signal_received = 0;

        my $h = set_sig_handler(
            'TERM',
            sub {
                $signal_received = 1;

                my $sth = $DBI::lasth;
                if ($sth && $sth->{Database}{private_in_queue_wait}) {
                    die 'RECEIVED TERM SIGNAL into queue_wait()';
                }
            },
            { flags => SA_RESTART },
        );

        # ここでの接続を使いまわす
        my $q4m = DBI->connect(@$connect_info);

        # シグナルを受け取るか、max_requests_per_child までループ
        my $i = 0;
        while (!$signal_received && $max_requests_per_child > $i++) {
            $q4m->{private_in_queue_wait} = 1;
            my $index = $q4m->selectrow_array(
                'SELECT queue_wait(?)',
                undef,
                $queue_table,
            );
            $q4m->{private_in_queue_wait} = 0;

            return unless $index; # queue not found

            return if $signal_received;

            my $queue = $q4m->selectrow_hashref(
                'SELECT * FROM ' . $queue_table,
            );
            # do something

            $q4m->do('SELECT queue_end()');
        }
    });
}
...

解説

というわけで、だいたいこんなかんじで worker を書いて使ってます。

はい

新しい API の話

Remote Notification API の裏側

Remote Notification API の概要

Remote Notification API の概要

static/img/remote_notification_introduction.png

実際の処理の説明の前に

APNs と GCM (C2DM) の概要

APNs とは

APNs とは

APNs の利用

いろんなモジュール

APNs の利用

いろんなモジュール

拡張形式ってなんぞ?

というわけで作りました

Net::APNs::Extended

Net::APNs::Extended

use Net::APNs::Extended;

my $device_token = 'xxxxxxxxx'; # あとで説明

my $apns = Net::APNs::Extended->new(
    is_sandbox => 1,
    cert_file  => 'xxx.pem', # 証明書
);

my $apns = Net::APNs::Extended->new(
    is_sandbox => 1,
    cert_file  => 'apns.pem',
);
 
# send notification to APNs
$apns->send($device_token, {
    aps => {
        alert => "Hello, APNs!",
        badge => 1,
        sound => "default",
    },
    foo => [qw/bar baz/],
});
 
# if you want to handle the error
if (my $error = $apns->retrive_error) {
    die Dumper $error;
}

APNs のむかつくところ

APNs まとめ

なんで HTTP じゃないんや... (帯域とかわかるけどさ...)

GCM について

の前に

C2DM について

static/img/deplicate_c2dm.png

今年の Google I/O で突然の終了宣言\(^o^)/

とか書いたんだけどね...

static/img/GCM_logo.png

_人人人人人人 人人人人人人_
そこで颯爽と GCM の登場
 ̄Y^Y^Y^Y^Y^Y^Y^Y^Y^Y ̄

GCM について

さっそくモジュール書きました

W::G::Cloud::Messaging

use WWW::Google::Cloud::Messaging;
 
my $api_key = 'Your API Key';
my $gcm = WWW::Google::Cloud::Messaging->new(api_key => $api_key);
 
my $res = $gcm->send({
    registration_ids => [ $reg_id, ... ],
    collapse_key     => $collapse_key,
    data             => {
      message => 'blah blah blah',
    },
});
 
die $res->error unless $res->is_success;
 
my $results = $res->results;
while (my $result = $results->next) {
    my $reg_id = $result->target_reg_id;
    if ($result->is_success) {
        say sprintf 'message_id: %s, reg_id: %s',
            $result->message_id, $reg_id;
    }
    else {
        warn sprintf 'error: %s, reg_id: %s',
            $result->error, $reg_id;
    }
 
    if ($result->has_canonical_id) {
        say sprintf 'reg_id %s is old! refreshed reg_id is %s',
            $reg_id, $result->registration_id;
    }
}

W::G::Cloud::Messaging

GCM のまとめ

HTTP 素晴らしいですね

APNs と GCM についての説明終わり

ユーザーに通知されるまでの仕組み

↓の図重要

static/img/remote_notification_introduction.png

ユーザーに通知されるまでの仕組み

device token とは

device token の取得

API リクエスト

大まかに、

の二種類がある。

API はここでそれぞれのリクエストに応じて q4m に enqueue する。

特定のユーザーへの送信の場合

static/img/remote_notificaiton_dispatch_queue.png

特定のユーザーへの送信の場合

Broadcast の場合

static/img/remote_notification_broadcat_queue.png

Broadcast の場合

device token の失効について

APNs と GCM でそれぞれ処理が違う

APNs

GCM

APNs の feedbacks service

Net::APNs::Extended::Feedback を使うと簡単に無効な device token 一覧が取れる

use Net::APNs::Extended::Feedback;

my $feedback = Net::APNs::Extended::Feedback->new(
    is_sandbox => 1,
    cert_file   => 'xxx',
);

my $feedbacks = $feedback->retrieve_feedback;
# [
#   {
#     time_t    => ...,
#     token_bin => ...,
#     token_hex => ...,
#   },
#   {
#     time_t    => ...,
#     token_bin => ...,
#     token_hex => ...,
#   },
#   ...
# ]

Remote Notification API まとめ

Remote Notification 終わり

Leaderboard API の裏側

Leaderboard API の概要

Redis について

Redis について 2

Redis の運用について

Redis の運用について

@hirose31 さんが書いた神の書があるので安心

Redis の Perl Binding

などなど大量にある

最初は Redis::hiredis が高速だったので検討していたが、XS レベルで multi がバグっていた!!

パッチを送ったものの取り込まれない

仕方ないので、やりたいことが一応全部できる RedisDB を利用することに

しかし、当初は RedisDB は PP だったが、いつの間にか XS に!!

まだ XS 版は使っていません。

RedisDB の使い方

use RedisDB;

my $redis = RedisDB->new(host => 'localhost', port => 6379);
say $redis->set(foo => 'bar'); # 1
say $redis->get('foo');        # 'bar'

say '-'x80;

say $redis->send_command('SET', foo => 'hoge'); # 1
say $redis->send_command('GET', 'foo');        # 1

my @results = $redis->get_all_replies;
say Dumper \@results; # [ 'OK', 'hoge' ]

RedisDB の使い方

Leaderboard API の概要

Leaderboard API の概要

更新機能

新しくスコアを登録したり、更新する場合は Redis と MySQL 両方に

static/img/leaderboard_update.png

更新機能

Redis で発行しているコマンド

use RedisDB;

my $redis = RedisDB->new(...);

$redis->send_command('MULTI');
$redis->send_command('ZADD', 'score', $score, $user_id);
# このへんで付加情報的な奴の更新
$redis->send_command('EXEC');

実際は、小さいスコアを許容するかどうかで処理を変えていたりと複雑

だけど、基本的には ZADD してるだけです!

個別のユーザーのランキングの取得

use RedisDB;

# score が高い人が 1位の場合
$redis->send_command('MULTI');
    $redis->send_command('ZSCORE', 'score', $user_id);
    $redis->send_command('ZREVRANK', 'score', $user_id);
$redis->send_command('EXEC');

@results = $redis->get_all_replies;
say Dumper {
    score => $results[-1]->[0],
    rank  => $results[-1]->[1] + 1,
};

解説

全体のランキングの取得

use RedisDB;

my $redis = RedisDB->new(host => 'localhost', port => 6379);

# score が高い人が 1位の場合
my $start = 1;
my $end   = 100;
$redis->send_command('MULTI');
    $redis->send_command('ZCOUNT', $score_key, '-inf', '+inf');
    $redis->send_command('ZREVRANGE', $score_key, $start, $end, 'WITHSCORES');
$redis->send_command('EXEC');

@results = $redis->get_all_replies;
my $data = $results[0];

my $total = $results[-1][0];
my $data  = $results[-1][1];

my $rs   = [];
my $rank = $start;
for (my $i = 0; $i < @$data; $i += 2) {
    push @$rs, {
        rank    => $rank++,
        user_id => $data->[$i],
        score   => $data->[$i+1],
    };
}

say Dumper [ $total, $rs ];

解説

友達間のランキング一覧の取得

友達間のランキング一覧の取得

Leaderboard まとめ

Redis 問題点

まとめ

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

Question?

j or →: next

k or ←: prev

h or ↑: list

l or ↓: return

o or ↵: open

? or /: toggle this help