2016年05月30日

pixiv private isucon 2016 攻略 (1/n)

はてなブックマークに登録

pixiv さんが社内で開催したプライベート ISUCON の AMI を公開してくれたので、手順を残しながら攻略していきます。

この記事の対象読者は途中で何をすればいいかわからなくなってしまう ISUCON 初心者です。 Go を利用して攻略していきますが、他の言語で参加する場合でも考え方などは参考になると思います。

最低限の初期設定

ssh の公開鍵を入れたり、エディターや git などのツール類をインストール・設定します。

このあたりは事前に自分用のチートシートやスクリプトを作って、手早くできるようにしておきましょう。

マシン調査

スペック

とりあえず CPU とメモリくらいはちゃんと把握しておきましょう。

$ grep processor /proc/cpuinfo
processor       : 0
processor       : 1

$ free -m
             total       used       free     shared    buffers     cached
Mem:           998        906         92         12         25        335
-/+ buffers/cache:        545        453
Swap:            0          0          0

CPUは2コアですね。メモリはトータル1GBでスワップなし。要注意です。

ps aux

何が動いているか確認します。 メールサーバーとか、AppArmor のような、競技に明らかに不要そうなデーモンは、ベンチマークスコアを安定させるためとメモリの節約のために落としておいたほうが良いです。

今回は特に何もする必要はありませんでした。

アプリの仕様把握

初期状態に復元する手順を確認してから、実際にアプリをブラウザからさわってみましょう。

初期状態への復元は、 tar でアーカイブしてローカルに持ってきておいたり、 mysqldump を取ったりしておくと良いです。 ただし今回の場合はAMIからやり直せるので、 /initialize というパスで初期化されると言うことだけアプリのコードで確認し、バックアップは手抜きしました。 ISUCON 本番では初期状態を復元するのに必要なデータをオペミスで失うと即死する恐れがあるので注意しましょう。

ブラウザのデバッグ機能でどういう通信をしているのか見たり、アプリのコードのURLルーティング部分を中心に軽く目を通したり、 mysqldump --no-data isuconp で取得したスキーマを参考に、ざっくりと全体像を把握していきます。

nginx の基礎設定

アクセスログを事前に用意しておいたフォーマットに変更して、 upstream への keepalive を付ける。 コア数少ないので worker_processes を 1 にして、 keepalive の効率を上げる。

静的ファイルを nginx から返す設定は、アプリの動作を把握してからでも遅くない。 Go 以外を使う人は、完全に静的なファイルが分かった段階ですぐに、Go を使う人はある程度チューニングが進んで nginx を外すかどうかを判断するタイミングで nginx から返すようにしよう。

/etc/nginx/nginx.conf:

    worker_processes 1;
    ...
        ##
        # Logging Settings
        ##
        log_format  isucon '$time_local $msec\t$status\treqtime:$request_time\t'
                           'in:$request_length\tout:$bytes_sent\trequest:$request\t'
                           'acceptencoding:$http_accept_encoding\treferer:$http_referer\t'
                           'ua:$http_user_agent';
        access_log /var/log/nginx/access.log isucon;

/etc/nginx/sites-enabled/isucon.conf:

  upstream app {
    server 127.0.0.1:8080;
    keepalive 32;
  }

  server {
    listen 80;
  
    client_max_body_size 10m;
    root /home/isucon/private_isu/webapp/public/;
  
    location / {
      proxy_set_header Host $host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_pass http://app;
    }
  }

あと、今回は Go を使うので ruby と php のサービスも止めてしまいます。 Go のサービスは開発中はコマンドラインで起動したいのでここでは設定しません。

$ sudo systemctl stop isu-ruby
$ sudo systemctl disable isu-ruby
$ sudo systemctl stop php7.0-fpm
$ sudo systemctl disable php7.0-fpm

MySQL の基礎設定

/etc/mysql/my.cnf を見てみます。ほぼディストリのデフォルトで使っているようなので、メモリ使用量削減とIO待ち削減のために次の設定だけ追加しておきます。

innodb_flush_method = O_DIRECT
innodb_flush_log_at_trx_commit = 2

重いクエリを手早く見つけるためのツールを用意する。僕の場合は myprofiler を使う。

$ wget https://github.com/KLab/myprofiler/releases/download/0.1/myprofiler.linux_amd64.tar.gz
$ tar xf myprofiler.linux_amd64.tar.gz
$ sudo mv myprofiler /usr/local/bin/

myprofiler の使い方は、 myprofiler -user=root です。クエリが減ってきたら myprofiler -user=root -interval=0.2 くらいにサンプリング間隔を短くすると良いでしょう。それ以外のオプションは myprofiler -h で確認してください。

ベンチマークの実行とざっくり状況把握

スコア:

{"pass":true,"score":4745,"success":4408,"fail":0,"messages":[]}

top:

Tasks:  81 total,   1 running,  80 sleeping,   0 stopped,   0 zombie
%Cpu(s): 94.1 us,  4.2 sy,  0.0 ni,  0.3 id,  0.0 wa,  0.0 hi,  1.3 si,  0.0 st
KiB Mem:   1022972 total,   701036 used,   321936 free,    21396 buffers
KiB Swap:        0 total,        0 used,        0 free.   346564 cached Mem

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
  720 mysql     20   0 1087768 140276  10724 S 142.8 13.7   0:52.09 mysqld
 2908 isucon    20   0  582876 124364   8800 S  46.6 12.2   0:12.98 app
 2904 www-data  20   0   91680   4800   2988 S   1.7  0.5   0:00.61 nginx

MySQL が app の3倍CPU食ってるのがわかります。

myprofiler:

 198 SELECT * FROM `comments` WHERE `post_id` = N ORDER BY `created_at` DESC LIMIT N
 124 SELECT COUNT(*) AS `count` FROM `comments` WHERE `post_id` = N
  11 SELECT `id`, `user_id`, `body`, `mime`, `created_at` FROM `posts` ORDER BY `created_at` DESC
   9 SELECT * FROM `comments` WHERE `post_id` = N ORDER BY `created_at` DESC
   5 SELECT * FROM `posts` WHERE `id` = N
   4 SELECT COUNT(*) AS count FROM `comments` WHERE `post_id` IN (...N)
   2 SELECT * FROM `users` WHERE `id` = ?
   2 SELECT `id`, `user_id`, `body`, `mime`, `created_at` FROM `posts` WHERE `created_at` <= S ORDER BY `created_at` DESC
   1 SELECT * FROM users WHERE account_name = S AND del_flg = N
   1 SELECT COUNT(*) AS count FROM `comments` WHERE `user_id` = N

comments の SELECT と COUNT が遅い事がわかる。その次に posts の select。

一番最初のチューニング: インデックス

上位3件のクエリについて、インデックスが有るか確認して、なければ張っておく。

mysql> show create table comments\G
*************************** 1. row ***************************
       Table: comments
Create Table: CREATE TABLE `comments` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `post_id` int(11) NOT NULL,
  `user_id` int(11) NOT NULL,
  `comment` text NOT NULL,
  `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=100049 DEFAULT CHARSET=utf8mb4
1 row in set (0.00 sec)

mysql> show create table posts\G
*************************** 1. row ***************************
       Table: posts
Create Table: CREATE TABLE `posts` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) NOT NULL,
  `mime` varchar(64) NOT NULL,
  `imgdata` mediumblob NOT NULL,
  `body` text NOT NULL,
  `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=10056 DEFAULT CHARSET=utf8mb4
1 row in set (0.00 sec)

comments.post_idposts.created_at にインデックスがない。重いクエリ上位3つがこれ。

mysql> create index post_id on comments (post_id);
Query OK, 0 rows affected (0.19 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql> create index created_at on posts (created_at);
Query OK, 0 rows affected (0.03 sec)
Records: 0  Duplicates: 0  Warnings: 0

とりあえずこれでもう一度計測しておきます。

top:

top - 18:13:32 up  2:13,  1 user,  load average: 2.01, 0.84, 0.52
Tasks:  79 total,   1 running,  78 sleeping,   0 stopped,   0 zombie
%Cpu(s): 74.0 us, 19.3 sy,  0.0 ni,  3.2 id,  0.2 wa,  0.0 hi,  3.4 si,  0.0 st
KiB Mem:   1022972 total,   944288 used,    78684 free,    23344 buffers
KiB Swap:        0 total,        0 used,        0 free.   462656 cached Mem

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
 2908 isucon    20   0  640148 174444   8800 S 109.7 17.1   0:44.93 app
  720 mysql     20   0 1093212 212252  10740 S  58.5 20.7   1:56.57 mysqld

app が mysql の 2倍になり、クエリがネックじゃなくなったのがわかります。

スコア:

{"pass":true,"score":14208,"success":13157,"fail":0,"messages":[]}

myprofiler:

  36 SELECT `id`, `user_id`, `body`, `mime`, `created_at` FROM `posts` ORDER BY `created_at` DESC
  15 SELECT COUNT(*) AS count FROM `comments` WHERE `user_id` = N
   5 SELECT * FROM `comments` WHERE `post_id` = N ORDER BY `created_at` DESC LIMIT N
   4 SELECT `id`, `user_id`, `body`, `mime`, `created_at` FROM `posts` WHERE `created_at` <= S ORDER BY `created_at` DESC

上位2件を見ると

  • posts からの SELECT に LIMIT が無い
  • comments.user_id にインデックスがない

LIMIT がないのはアプリ側で修正が必要なのと、ボトルネックがアプリに移ってるので、アプリ改修に移る。

comments.user_id はついでにインデックス張っておく。(1番重いクエリじゃないのでベンチは省略)

mysql> create index user_id on comments (user_id);
Query OK, 0 rows affected (0.14 sec)
Records: 0  Duplicates: 0  Warnings: 0

まとめ

スコア: 4745 (初期状態) -> 14208 (インデックスチューニング)

systemd のコマンドとか nginx の設定とか MySQL の設定ファイルを見るとかインデックス張るとかいろいろ慣れて、ここまではスムーズにできるようになりましょう。

感覚値ですが、一人でやってる場合はここまでを2時間以内には終わらせられるようにしましょう。1時間を切れればスゴイです。


@methane


songofacandy at 11:09
この記事のURLComments(0)TrackBack(0)ISUCON | golang
2016年04月20日

ラズパイで作るネットワークエミュレータ(後編)

はてなブックマークに登録

前編で紹介したネットワークエミュレータ「PackDrop」のプロトタイプですが、雑に作ったわりに評判が良く予想外に需要が出てきてしまったので、ちゃんと業務での利用に耐えられる「量産型」を製作しました。

前回の記事がややウケで気を良くしています。@pandax381です。

機能追加

プロトタイプを作成してから暫くのあいだ試験運用してみたのですが、実際に使った方々からいつくか要望を受けました。

  • パケットをバーストロスさせるためのスイッチが欲しい
    一定時間、連続的にパケットが消失するバーストロスを再現するときに「パケロスのボリュームを100%まで回してまた戻す」という作業が面倒なので、専用のスイッチを設けて欲しい。

  • 稼働状況がわかるようにLEDをつけて欲しい
    試験用のパラメータを設定したまま元に戻すのを忘れてしまい「あれ、なんか今日ネット遅くね?」という事案が多発したので、遅延・パケロスが設定されている場合には一目でわかるようにLEDで警告して欲しい。

  • 特定のVLANだけを対象にしたい
    タグVLANを利用している場合に、特定のVLAN-IDのパケットだけをネットワークエミュレーションの対象にし、それ以外のパケットは素通しさせたい。

作り始めた時にはそこまで考えが及んでいなかったので、やっぱり他の人に触ってもらうのって大事ですね。どれもそんなに手間を掛けずに対応できそうなので、量産型の製作に先立って要望された機能を追加してみます。

工作パート

追加工作で必要なパーツは以下の通りです。例によってどれも秋月で調達できるものですが、スライドスイッチはマルツの方が種類が豊富だったのでそちらで購入しました。また、プロトタイプでラズパイ本体にマウントしたブレッドボードにはもうスペースがないので、量産型の製作までは同じブレッドボードをもう1つ後付けしてしのぐことにします。

000

相変わらず回路図が書けないクソザコなので配線図的なものを載せておきます。こういった図がちゃちゃっと作れてしまう「fritzing」めっちゃ便利ですね。LEDとスライドスイッチを、それぞれGPIOの17番と22番に接続します。LEDには330Ωの抵抗を噛ませてあります。スライドスイッチはオンの時にGPIOとGNDがショートするように配置します。

001

コード修正

制御プログラム packdrop.c のコードを修正します。まず、プログラムの冒頭でGPIOの初期化処理を追加します。wiringPiSetupGpio() を呼び出した後、pinMode() でGPIOピンの入出力モードを設定します。LEDが繋がるピンは出力(OUTPUT)に、スライドスイッチが繋がるピンは入力(INPUT)に設定します。加えて、スライドスイッチの繋がるピンは pullUpDnControl() でプルアップ(PUD_UP)に設定します。

ラズパイはプルアップ抵抗とプルダウン抵抗を内蔵していて、各 GPIO ピンに対してプルアップ/プルダウンをソフトウェア制御できます。今回のように入力ピンを扱う際には、そのピンをプルアップして使えば抵抗を減らせて回路がシンプルになります(と何処かに書いてあったのでそうしてみました)。

プルアップの設定をした入力ピンは、何もつながっていなければ「1(HIGH)」になり、回路が繋がると(電流はそちらに流れるため)「0(LOW)」になります。今回のスライドスイッチの繋ぎ方だと「オン=0」「オフ=1」になりますが、とっても紛らわしいので「SW_ON」として値を定義して使うようにしています。

メインループの中で、digitalRead() を呼び出してスライドスイッチの状態を読み取り、オンであれば burst_mode() を呼び出します。burst_mode() でやっていることは、パケロスを100%に設定してLCDの表示を切り替えてLEDを点灯させているだけです。LEDを点灯させるには digitalWrite() で「1」を書き込めば良く「0」を書き込めば消灯します。その後は digitalRead() でスイッチの状態を読み続けて、オフの状態になったらメインループに戻って通常動作に復帰するようになっています。

通常動作での変更点は、遅延またはパケロスが設定されている場合にはLEDを点灯させるようにしただけです。

/*
 * packdrop.c
 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <wiringPi.h>
#include <wiringPiSPI.h>
#include <mcp3002.h>

#define APP_NAME     "PackDrop"
#define APP_VERSION  "v1.0"

#define SPI_CHANNEL  (0)
#define PINBASE      (100)
#define VOLUME_DELAY (PINBASE)
#define VOLUME_LOSS  (PINBASE+1)
#define SW_ON        (0)
#define PIN_LED      (17)
#define PIN_SW       (22)

char **devices;
int devnum;

static void
tc_set (const char *dev, int delay, int loss) {
    char cmd[128];

    snprintf(cmd, sizeof(cmd), "tc qdisc change dev %s root netem delay %dms loss %d%%", dev, delay, loss);
    system(cmd);
}

static void
tc_init (const char *dev) {
    char cmd[128];

    snprintf(cmd, sizeof(cmd), "tc qdisc del dev %s root >/dev/null 2>&1", dev);
    system(cmd);
    snprintf(cmd, sizeof(cmd), "tc qdisc add dev %s root netem delay 0ms loss 0%%", dev);
    system(cmd);
}

static void
lcd_write_core (const char *upper, const char *lower) {
    char cmd[128];

    system("i2cset -y 1 0x3e 0 0x80 b");
    snprintf(cmd, sizeof(cmd), "i2cset -y 1 0x3e 0x40 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x i",
        upper[0], upper[1], upper[2], upper[3], upper[4], upper[5], upper[6], upper[7]);
    system(cmd);

    system("i2cset -y 1 0x3e 0 0xc0 b");
    snprintf(cmd, sizeof(cmd), "i2cset -y 1 0x3e 0x40 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x i",
        lower[0], lower[1], lower[2], lower[3], lower[4], lower[5], lower[6], lower[7]);
    system(cmd);
}

static void
lcd_write (int delay, int loss) {
    char upper[9], lower[9];

    snprintf(upper, sizeof(upper), "  %3d ms", delay);
    snprintf(lower, sizeof(lower), "  %3d %%", loss);
    lcd_write_core(upper, lower);
}

static void
lcd_init (void) {
    char name[9], ver[9];

    system("i2cset -y 1 0x3e 0 0x38 0x39 0x14 0x78 0x5f 0x6a i");
    system("i2cset -y 1 0x3e 0 0x0c 0x01 i");
    system("i2cset -y 1 0x3e 0 0x06 i");
    snprintf(name, sizeof(name), "%8s", APP_NAME);
    snprintf(ver, sizeof(ver), "%8s", APP_VERSION);
    lcd_write_core(name, ver);
}

static void
burst_mode (void) {
    int n, v;

    for (n = 0; n < devnum; n++) {
        tc_set(devices[n], 0, 100);
    }
    lcd_write_core(" burst  ", "   mode ");
    digitalWrite(PIN_LED, 1);
    while (1) {
        v  = digitalRead(PIN_SW);
        if (v != SW_ON) {
            break;
        }
        sleep(1);
    }
}

int
main (int argc, char *argv[]){
    int n, v, b, d, l;
    int delay = 0, loss = 0;

    lcd_init();
    if (wiringPiSetupGpio() == -1) {
        printf("wiringPiSetupGpio: failed\n");
        return -1;
    }
    pinMode(PIN_LED, OUTPUT);
    pinMode(PIN_SW, INPUT);
    pullUpDnControl(PIN_SW, PUD_UP);
    if (mcp3002Setup(PINBASE, SPI_CHANNEL) < 0) {
        printf("mpc3002Setup: failed\n");
        return -1;
    }
    devices = argv + 1;
    devnum  = argc - 1;
    for (n = 0; n < devnum; n++) {
        tc_init(devices[n]);
    }
    while (1) {
        v = digitalRead(PIN_SW);
        b = (v == SW_ON) ? 1 : 0;
        if (b) {
            burst_mode();
        }
        v = analogRead(VOLUME_DELAY);
        d = ++v / 2;
        v = analogRead(VOLUME_LOSS);
        l = ((float)++v / 1024) * 100;
        if (d != delay || l != loss || b) {
            delay = d;
            loss  = l;
            for (n = 0; n < devnum; n++) {
                tc_set(devices[n], delay, loss);
            }
            lcd_write(delay, loss);
            digitalWrite(PIN_LED, (delay || loss) ? 1 : 0);
        }
        sleep(1);
    }
    return 0;
}

以前と同じように -lwiringPi オプションをつけてコンパイルし、バイナリを生成しておきます。

# gcc -W -Wall -o packdrop packdrop.c -lwiringPi

自動起動のための設定

ethtool が必要になるので、事前に apt でインストールしておきます。

# apt-get install ethtool

毎回、手動でブリッジを作成しなくて済むように /etc/network/interfaces ファイルにブリッジの記述します。念のため、NICのオフロード機能やブリッジのマルチキャストスヌーピング機能を無効にする設定も記述しています。

auto lo
iface lo inet loopback

auto eth0
iface eth0 inet static
    address 0.0.0.0
    up ethtool -K $IFACE tso off ufo off gso off gro off lro off 2>/dev/null

auto eth1
iface eth1 inet static
    address 0.0.0.0
    up ethtool -K $IFACE tso off ufo off gso off gro off lro off 2>/dev/null

auto br0
iface br0 inet static
    address 0.0.0.0
    bridge_ports eth0 eth1
    bridge_maxwait 0
    up ethtool -K $IFACE tso off ufo off gso off gro off lro off 2>/dev/null
    up echo 0 > /sys/devices/virtual/net/$IFACE/bridge/multicast_snooping

PackDropにはIPアドレスは割り当てないので、dhcpクライアントが自動で起動しないようにします。

# systemctl disable dhcpcd.service
# systemctl disable avahi-daemon.service

続いて、/etc/rc.local に packdrop コマンドを自動起動するための処理を記述します。少し手間を掛けて、sysfs経由でブリッジに登録されているインタフェースの一覧を取得し、packdrop コマンドの引数として渡すようにしています。これにより、interfaces ファイル側でブリッジに登録するインタフェースを変更した場合に、その変更が自動的に反映されるようになります。また、ブリッジそのものやインタフェースが見つからない場合には、LCDにエラーメッセージを表示するようにしています。

#!/bin/bash -e

packdrop=/root/bin/packdrop
br=br0

function getbrif() {
    echo `ls -A "/sys/devices/virtual/net/$1/brif/" 2> /dev/null`
}

ifaces=`getbrif $br`
if [ -n "$ifaces" ]; then
    $packdrop $ifaces >/dev/null 2>&1 &
else
    i2cset -y 1 0x3e 0 0x38 0x39 0x14 0x78 0x5f 0x6a i
    i2cset -y 1 0x3e 0 0x0c 0x01 i
    i2cset -y 1 0x3e 0 0x06 i
    i2cset -y 1 0x3e 0 0x80 b
    i2cset -y 1 0x3e 0x40 0x45 0x72 0x72 0x6f 0x72 0x20 0x20 0x20 i
    i2cset -y 1 0x3e 0 0xc0 b
    i2cset -y 1 0x3e 0x40 0x20 0x45 0x42 0x52 0x4e 0x46 0x4e 0x44 i
fi

exit 0

動作確認

自動起動の設定を済ませたら、PackDropを再起動させて動作確認をします。

まず、ボリュームの値がゼロで、スライドスイッチもオフの状態です。この状態では遅延もパケロスも発生しないのでLEDは点灯していません。

002

遅延またはパケロスのボリュームを回すとLEDが点灯し、両方の値をゼロに戻せば消灯します。

003

スライドスイッチをオンにすると、LCDの表示が「Burst Mode」に変わりLEDも点灯します。このモードではボリュームの値に関係なく、全てのパケットが捨てられて一切の通信ができなくなります。スライドスイッチをオフにすると通常のモードへ戻り、ボリュームの値に従って動作します。

004

VLAN対応について

PackDropを通過するパケットの中から特定の VLAN ID のパケットのみを制御したい場合には、/etc/network/interfaces ファイルの内容を以下のように変更することで対応できます。ここでは、例として eth0 と eth1 に対して ID「100」の VLAN インタフェースを定義し、それを br0 に登録しています。ただ、この状態だと VLAN ID が 100 以外のパケットが破棄されてしまうので、新たなブリッジ「br1」を定義し、そこへ eth0 と eth1 を登録します。/etc/rc.local の処理で、packdrop には br0 に登録されているインタフェースのみが渡されるようにしたので、br1 に登録されているインタフェースは対象外、つまりパケットがそのまま素通しされることになります。

auto lo
iface lo inet loopback

auto eth0
iface eth0 inet static
    address 0.0.0.0
    up ethtool -K $IFACE tso off ufo off gso off gro off lro off 2>/dev/null

auto eth0.100
iface eth0.100 inet static
    address 0.0.0.0
    up ethtool -K $IFACE tso off ufo off gso off gro off lro off 2>/dev/null

auto eth1
iface eth1 inet static
    address 0.0.0.0
    up ethtool -K $IFACE tso off ufo off gso off gro off lro off 2>/dev/null

auto eth1.100
iface eth1.100 inet static
    address 0.0.0.0
    up ethtool -K $IFACE tso off ufo off gso off gro off lro off 2>/dev/null

auto br0
iface br0 inet static
    address 0.0.0.0
    bridge_ports eth0.100 eth1.100
    bridge_maxwait 0
    up ethtool -K $IFACE tso off ufo off gso off gro off lro off 2>/dev/null
    up echo 0 > /sys/devices/virtual/net/$IFACE/bridge/multicast_snooping
    
auto br1
iface br1 inet static
    address 0.0.0.0
    bridge_ports eth0 eth1
    bridge_maxwait 0
    up ethtool -K $IFACE tso off ufo off gso off gro off lro off 2>/dev/null
    up echo 0 > /sys/devices/virtual/net/$IFACE/bridge/multicast_snooping

PackDropとVLANスイッチをうまく組み合わせることで、PackDropにイーサネットポートを増設しない構成も作れます。

例えば、以下のような設定をしたVLANスイッチの port1 にPackDropを接続して、port3 と port4 のあいだで遅延とパケロスを挿入する、といったことができます。

(余談ですが NETGEAR のこのVLANスイッチ、コンパクトで使いやすいし激安なので1つ持っておくといいですよ)

011

上記の構成で動作させる場合、interfaces ファイルの内容は以下のようになります。

auto lo
iface lo inet loopback

auto eth0
iface eth0 inet static
    address 0.0.0.0
    up ethtool -K $IFACE tso off ufo off gso off gro off lro off 2>/dev/null

auto eth0.100
iface eth0.100 inet static
    address 0.0.0.0
    up ethtool -K $IFACE tso off ufo off gso off gro off lro off 2>/dev/null

auto eth0.200
iface eth0.200 inet static
    address 0.0.0.0
    up ethtool -K $IFACE tso off ufo off gso off gro off lro off 2>/dev/null

auto br0
iface br0 inet static
    address 0.0.0.0
    bridge_ports eth0.100 eth0.200
    bridge_maxwait 0
    up ethtool -K $IFACE tso off ufo off gso off gro off lro off 2>/dev/null
    up echo 0 > /sys/devices/virtual/net/$IFACE/bridge/multicast_snooping

量産型の製作

前置きが長くなってしまいましたが、ようやく量産型の製作に取り掛かります。プロトタイプの最大の欠点は、なんと言っても物理的にものすごく脆弱なことです。ブレッドボードにジャンプワイヤを生やしているだけなので、ちょっと指を引っ掛けただけでも断線の危険性がありますし、輸送したらバラバラになっているかもしれません...

ここでも同僚から「プリント基板設計しよう」「P版ドットコム」「中国に発注すれば安いよ」などと煽られましたが、ラズパイ本体にぴったりサイズのユニバーサル基板が各社から発売されているらしいので、それを使って量産型を作ることにしました。

部品一覧

今回はサンハヤト製のラズパイ用ユニバーサル基板を使うことにしました。こちら、表記は「B+用」となっていますがラズパイ2はB+と同じ形状なので全く問題なく使えます。あとは、プロトタイプで使っていたボリューム(可変抵抗)は、そのまま基板に実装できないタイプだったので、基板に実装できるタイプのものを改めて調達しました。

005

また、Eleduinoのケース付属のネジ(20mm)だと長さが足りなくてユニバーサル基板を固定できませんでした。このユニバーサル基板を固定するには少なくとも25mmのネジが必要です。通販で手ごろに入手できるところがなかったので、秋葉原にあるネジ専門店「西川電子部品」へ買いに行ってきました。付属ネジはM2.5ですが、M2.6で大丈夫です。

  • M2.6 25mm ネジ x 4
  • M2.6 ナット x 8
  • M2.6 ワッシャー x 12
006

製作

いままでブレッドボードで作っていたものをユニバーサル基板へ実装し直します。部品点数も少ないので、少し考えて配置すれば問題なく配線できると思います。配置と配線経路が決まったら、後はひたすら半田付けするだけです。

以下、僕の作例です。半田付けの初心者すぎてスズメッキ線の存在とか知らなくてブレッドボード用のジャンプワイヤだけで作ってあります。あと、このユニバーサル基板はスルーホールになっていて、パーツを間違って半田付けしてしまうと取り外すのにものすごく苦労します。何度も失敗してパーツを壊したり悲しい思いをしたので、それを教訓にしてパーツ部分にはICソケットとピンソケットを取り付けるようにしました。あと、左上に三本生えているピンヘッダはシリアル接続(UART)用です。

007

裏面はこんな感じです。電子工作に慣れている方ならもっとシンプルで見栄えのいいものが作れると思いますが「初心者が作るとこんな感じのものが出来上がるのか」と生温かい目で見てもらえると有難いです。。

008

ソケットにパーツを取り付けたら完成です。プロトタイプと比べてだいぶスッキリしましたね!

009

動作確認

量産型のユニバーサル基板をラズパイに取り付けて動作確認します。まず、プロトタイプに取り付けていたブレッドボードやボリュームを取り外して、ネジを25mmのものに交換します。ブレッドボードの両面テープは強力ですが、ゆっくりは剥がせばキレイに剥がれます。ユニバーサル基板を取り付ける前に、一旦ナットでケースを固定します。この際、スペーサーとしてワッシャーを3枚ずつ噛ませると丁度よくなります。

012

量産型のユニバーサル基板をピンヘッダに差し込みながら取り付けてナットで固定します。USBイーサネットアダプタは、プロトタイプの時と同じように側面に両面テープで固定しておきます。最後に電源を入れて、全ての機能が正常に動作することを確認します。

010

ファイルシステムの保護(電プチ対策)

こういったネットワーク機器は、シャットダウン処理などせず、なにも考えずに電源を切れたほうが使いやすいです(というより、そうでないと使いづらいです)。Raspbianは、素の状態だとSDカード上のファイルシステムを読み書き可能な状態でマウントしています。これで何が問題になるかというと、SDカード上のファイルシステムに常時書き込みが発生していると、SDカードの寿命を早めてしまう可能性があります。最近はどうかわかりませんが、一昔前だと何ヶ月かで壊れてしまうようなことが普通にありました。また、書き込み中に電源が落ちてファイルシステムを破壊してしまうことも考えられます。そういったことを防ぐために、SDカード上ファイルシステムを保護してあげる必要があります。ファイルシステムを保護するために fsprotect や overlayroot などのツールもありますが、ここでは initramfs と overlayfs を用いて自前でファイルシステムの保護を行います。

initramfs の作成

まず、busybox を使って initramfs の種を作成します。

# apt-get install busybox-static
# mkdir -p initramfs/{bin,dev,lib,proc,sys}
# /bin/busybox --install -s initramfs/bin
# cp /bin/busybox initramfs/bin

initramfs 内で必要になるカーネルモジュールをコピーします。最後のdepmodを実行し忘れるとカーネルがモジュールを認識できないので注意してください。

# mkdir -p initramfs/lib/modules/`uname -r`
# cp /lib/modules/`uname -r`/modules.{builtin,order} initramfs/lib/modules/`uname -r`
# mkdir -p initramfs/lib/modules/`uname -r`/kernel/fs/overlayfs
# cp -a /lib/modules/`uname -r`/kernel/fs/overlayfs/overlay.ko initramfs/lib/modules/`uname -r`/kernel/fs/overlayfs
# depmod -a --basedir initramfs

initramfs では /init スクリプトが自動実行されるので、initramfs ディレクトリ直下に、init という名前のシェルスクリプトを作成します。/dev/mmcblk0p2 を lower に、tmpfs を upper に指定して overlayfs をマウントしています。これだけで、SDカードのパーティションを Read Only でマウントしつつ、ルートファイルシステムは Read Write なシステムが構築できます。更にシステム起動後に発生したファイルシステム上の変更は電源断で消えてなくなるので、こういった装置にピッタリです。

また、コンフィグ投入の仕組みとして、FATパーティション(/dev/mmcblk0p1)に interfaces.txt という名前のファイルを置いておくと、それを /etc/network/interfaces にコピーしてから起動するようにしています。VLANの設定をしたくなった場合には、この仕組みを利用してPackDropの動作を変更できます。

#!/bin/sh

export PATH=/bin
export TZ=JST-9

mount -t devtmpfs devtmpfs /dev
mount -t proc proc /proc
mount -t sysfs sysfs /sys

echo "4 4 1 7" > /proc/sys/kernel/printk

modprobe overlay

mkdir /overlay /sysroot
mkdir /overlay/ro /overlay/rw
mount -t ext4 -o ro /dev/mmcblk0p2 /overlay/ro
mount -t tmpfs -o size=512M tmpfs /overlay/rw
mkdir /overlay/rw/root /overlay/rw/work
mount -t overlay -o lowerdir=/overlay/ro,upperdir=/overlay/rw/root,workdir=/overlay/rw/work overlay /sysroot
mount -t vfat -o ro /dev/mmcblk0p1 /sysroot/boot
mkdir /sysroot/overlay
mkdir /sysroot/overlay/ro /sysroot/overlay/rw
mount --move /overlay/ro /sysroot/overlay/ro
mount --move /overlay/rw /sysroot/overlay/rw

echo -n > /sysroot/etc/fstab
cp -f /sysroot/boot/interfaces.txt /sysroot/etc/network/interfaces 2>/dev/null

umount /sys
umount /proc
umount /dev

exec switch_root -c /dev/console /sysroot /sbin/init

作成したスクリプトに実行権限を付与します。

# chmod +x initramfs/init

initrafms ディレクトリの中身を cpio でアーカイブしてイメージファイルを作成します。initramfs のイメージファイルは、ブートローダが読み込める場所に配置する必要があるので /boot に出力します。

# cd initramfs
# find . | cpio -o -H newc | gzip -c > /boot/initramfs.img

ラズパイ本体の設定

作成した initramfs をロードしてブートするために、/boot/config.txt に以下の行を追記します。

initramfs initramfs.img

スワップファイルも無効にしておきます。

# dphys-swapfile swapoff
# dphys-swapfile uninstall
# update-rc.d dphys-swapfile disable

動作確認

上記の設定が済んだ状態で再起動させると、ファイルシステムが保護された状態で起動してきます。SDカードのパーティションは Read Only でマウントされているため、動作中に突然電源を切られてしまっても大丈夫です。

起動時のログを載せておきますので、うまくいかない場合には照らし合わせてみてください。 https://gist.github.com/pandax381/06508bd5e226f5aefd4997bba093cfde

また、以前の状態でファイルシステムを保護しない状態に戻したくなった場合には、以下のようにして /boot/config.txt に記述した initramfs の行をコメントアウトして再起動するだけで元どおりになります。

# mount -o rw,remount /boot
# sed -ie "s/^initramfs/#initramfs/" /boot/config.txt
# reboot

おわりに

電子工作初心者がラズパイで遊んでみたというだけの記事になってしまったような気がしなくもないですが、楽しかったです。そして、ソフトだけじゃなくハードもある程度できると幅が広がるなぁと当たり前のことを改めて実感しました。とりあえず、半田付けは初心者から少しだけレベルアップしたので、引き続き電子工作を楽しみながらハードにも強くなれるよう励みます。

あと、実際に社内で稼働しているバージョンはもう少し進化していて、ファームウェアの更新をしやすくするために rootfs の中身を squashfs でイメージ化したものをFATパーティションに置いて、それを使うようにしていたりします。このあたりのネタも面白いので、また個別に記事を書こうと思います。


klab_gijutsu2 at 19:02
この記事のURLComments(0)TrackBack(0)
2016年04月06日

ラズパイで作るネットワークエミュレータ(前編)

はてなブックマークに登録

ネットワークが絡んだ通信プログラムを開発していると、テストのために遅延やパケロスを意図的に発生させたくなることがあります。いまどきは IDE にネットワークエミュレーション機能が組み込まれていたり、仮想環境で容易に再現できたりもしますが、箱物のネットワークエミュレータがあるとネットワークの構成を気にせずカンタンに設置できるのですごく便利だったりします。世の中にはそういった製品が沢山あるので安価なものを買ってもいいのですが、新たにラズパイが届いたばかりだったので、これを使って超小型のネットワークエミュレータを自作してみました。前編と後編の二回に分けて紹介します。

最近、社内で「ラズパイおじさん」と呼ばれるようになりました。@pandax381 です。

001

ラズパイ + Linux = ネットワークエミュレータ

「ネットワークエミュレータを自作」と言うとなんだか凄そうな感じがしますが、実はものすごく簡単につくれます。なぜなら、ネットワークエミュレータに必要な機能を Linux が全て備えているからです。Linux をインストールしたラズパイがあれば、数万円の価格帯の製品と同等のことができます(というより、この価格帯の製品は単に Linux の機能を利用しただけのものが多いです)。

動作イメージ

作成したネットワークエミュレータは、ブリッジ(中継機)として動作するタイプのものです。ネットワーク機器同士の間に接続し、通過するパケットに対して遅延やパケロスを発生させます。

002

また、イーサネットポートを備えていないラップトップやスマートフォンの場合には、ネットワークシミュレータを「アクセスポイント〜スイッチ間」に設置することで、そのアクセスポイントに接続している端末の通信パケットに対して制御を加えることができます。例えば、Wi-Fi につないだままモバイルネットワークと同等の品質で通信するといったことができるようになります。

003

イーサネットポートが足りない問題

ブリッジとして動作させると言ったものの、ラズパイに搭載されているイーサネットポートは1つだけなので、物理的にポートが足りなくてネットワークをブリッジできません(個人的には2ポートのモデルがあるとすごく幸せなんですけど、一般的にはあまり需要ないんですかねぇ...)。

これはもう足りないなら増やすしかないので、ちょっと格好悪いですがUSBイーサネットアダプタを接続してポートを増設します。ラズパイに内臓されているイーサネットポートは100Mなので、増設するイーサネットアダプタも100Mで十分です。無駄にギガビットのアダプタを接続しても、ブリッジした際にもう片方の性能に引っ張られるので意味がないです。個人的には Logitec LAN-TXU2C あたりが安価で入手性もいいのでオススメです。Raspbian にはこのアダプタが使っているチップのドライバ(asix.ko)が入っているので、USBポートに接続するだけで認識します。

004

なお、USBイーサネットアダプタはそれなりに電力を消費するため、調子に乗って何個も接続するとラズパイ本体が電力不足で不安定になって落ちたりするので自重しましょう。

VLANスイッチと組み合わせるとイーサネットポートを増設しなくても済むようになりますが、それは後編で紹介します。

ブリッジを作ってパケットを通過させる

まず、ブリッジを作成してラズパイの持つ2つのイーサネットポート間でパケットが通過できるようにします。

005

ブリッジを作成するには、bridge-utils がインストールされている必要があります。Raspbian のイメージならインストールされているはずですが、なければ apt でインストールします。

# apt-get install bridge-utils

ブリッジの作成や操作は brctl コマンドで行います。brctl addbr でブリッジ を作成し、brctl addif でブリッジにインタフェースを追加します。ここでは、br0 という名前で新しくブリッジを作成し、そこに eth0 と eth1 を追加します。

# brctl addbr br0
# brctl addif br0 eth0 eth1

作成したブリッジは brctl show で確認できます。

# brctl show
bridge name     bridge id               STP enabled     interfaces
br0             8000.3495db294586       no              eth0
                                                        eth1

仕上げに、ip コマンドでインタフェースとブリッジを起動させれば、このラズパイは2ポートのハブのように振る舞いはじめます。試しに、PCとスイッチの間にこのラズパイを接続しても、PCは何事もなくこれまでと同じように通信できるはずです。

# ip link set eth0 up
# ip link set eth1 up
# ip link set br0 up

遅延とパケロスを発生させる

ブリッジを作成してパケットがラズパイを通過できるようになったので、次は通過するパケットに対して遅延やパケロスを発生させる設定をします。

Linux カーネルにはトラフィックコントロールやネットワークエミュレーションの機能が備わっていて、Raspbian を含めたほとんどのディストリビューションで有効になっています。

iproute2 に含まれる tc コマンドを使って、送信トラフィックに対して以下の制御を加えることができます。

  • 帯域制限
  • 帯域保証
  • 遅延
  • パケットロス
  • ビットエラー
  • 重複
  • 順序入れ替え

ネットワークエミュレータを名乗るからには全ての機能を取り入れたいところですが、今回は取り急ぎ必要だった 遅延パケロス のみを扱います。

tc コマンドを使いこなすと高度なトラフィック制御ができるようになりますが、Linux のトラフィックコントロールの概念はなかなかに複雑で、tc のコマンド体系もそれに習って難解です。ここでは決まった使い方しか示しませんが、tc コマンドやトラフィックコントロールの詳細について興味がある方には、GREEさんのエンジニアブログに掲載されている「よくわかるLinux帯域制限」という記事が秀逸でオススメです。

tc コマンドは複雑ですが、遅延とパケロスを設定するだけなら簡単です。例えば、eth0 に対して遅延とパケットロスを設定するには以下のコマンドを実行するだけです。これで、eth0 から「出て行く」パケットに対して 100ms の遅延が加わり、5% の確率でパケットロスが発生します。

# tc qdisc add dev eth0 root netem delay 100ms loss 5%

なお、新しく設定を追加する時は tc qdisc add ですが、設定済みの値を変更する際は tc qdisc change です。値が設定されていないのに change を指定したり、設定済みなのに add を指定するとエラーになります。

# tc qdisc change dev eth0 root netem delay 200ms loss 10%

設定した値は tc qdisc del で消せます。消去した後の再設定は tc qdisc add です。

# tc qdisc del dev eth0 root

ここでは、遅延やパケロスを固定的に発生させる方法を示していますが、遅延時間やパケロス発生頻度を一定の範囲内で分散させたりすることもできます。

UIについて考える

このように、Linux の標準的な機能を組み合わせることで、ラズパイを超小型のネットワークシミュレータに仕立て上げることができます。ただし、この状態だとめちゃくちゃ使いにくいです。遅延やパケロスの値を変えるのに、その都度シリアル経由でログインして tc コマンドを実行するとかありえないです。自分が手元で使うにしても、もう少し気の利いた UI が欲しいと思ってしまいます。

ネットワーク機器なので、シリアルでCLIを提供するというのも個人的にはアリですが、他の人に勧めようとした際に「USBシリアル変換ケーブルとか持ってないよ」と言われてしまう可能性が高いので微妙です。無難に Web で GUI を提供するのが現実的だと思いますが、メンテナンス用のイーサネットポートがないのでIPアドレスを割り当てる先がなく、これも微妙です。

どうにも決めかねていたので、たまたま通りがかった同僚に相談してみたところ「直感的なやつ」「アナログな感じで」「つまみでぐりぐり設定したい」「液晶もつけよう」「アキバ行こう」などと煽られました。

「まぁラズパイを使ってるんだから電子工作でアナログな UI を作るのも面白いなぁ〜」と思いはじめ、悪乗りしてネタにマジレスすることにしました。

UI(物理)を試作

パーツ一覧

まず、ラズパイ本体とケース、外付けのUSBイーサネットアダプタです。前回はRSコンポーネンツ製の純正ケースを買ったのですが、Eleduino のアクリルケースがお洒落だったのでこちらを選んでみました。ヒートシンクの効果は不明ですがカッコいいので付けてます。

006

続いて、UI用の工作パーツです。全て秋月で調達できます。コンセプトの「ぐりぐりして設定する」を実現するために、ボリューム(可変抵抗)を使います。なお、ラズパイはアナログ入力を備えていないので、SPI接続のADコンバータを使ってボリュームの値を読み取ります。設定した値を表示するために、I2C接続のキャラクタLCDも用意します。

007

UI用の工作パーツだけなら千円程度で揃います。

組み立て

回路図とか書けないクソザコなので、配線図的な何かを載せておきます。

007

設定した値を表示させるためのキャラクタLCDです。8文字×2行なので表示できる情報量は少ないですが、ものすごく小さいです。ピッチが狭いので変換基盤への半田付けが少し大変ですが、電子工作初心者で半田付けに不慣れな僕でもなんとかできたので大丈夫です。ちなみに、弊社では、その辺に FLUKE のオシロスコープが転がっていたりするのですが、なぜか半田ごてが備品として存在しておらず「これは由々しき問題だ」ということで、新たに温調式の半田ごてが備品に仲間入りすることなりました。

008

ボリュームは、GND(左)OUT(中)VCC(右)に接続するので、ジャンプワイヤを半田づけしておきます。

011

あとは、ブレッドボードとボリュームを両面テープでラズパイの上に固定して、ひたすら配線します。ブレッドボード付属の両面テープはかなり強力なので慎重に...貼ってしまうと剥がすのが大変です。ボリュームはクッション入りの両面テープで貼るとグラつかずに固定できます。

012

ちょっとジャンプワイヤがごちゃごちゃしていますが、まぁプロトタイプなのでこれでいいでしょう。ついでにUSBイーサネットアダプタも両面テープで側面に貼ってしまいます。なんだかそれっぽいガジェットに見えるようになってきましたね!

I2C接続のLCDが認識できない問題

秋月で調達したI2C接続のLCD「AE-AQM0802」が認識できないという問題に遭遇しました。ピッチ変換基盤への半田付けをミスったのかと思い、完成品を買ってきても認識してくれませんでした。少し調べてみたところ、同じようにラズパイで認識されないと報告している方がいました(逆にちゃんと動作したという方もいました)。

電子工作初心者のため、あまり良くわかってないのですが、どうも「I2C で使う GPIO ピンはラズパイ本体側に固定的にプルアップ抵抗が実装されていて、その抵抗値が小さすぎるためにうまく認識できない」ということのようです。解決策っぽいものの中で一番確実で分かりやすかったのが「ラズパイ本体側のプルアップ抵抗を取り除く」でした。ちょっと荒技な気もしますが、えいやでやってみたら見事に動いてくれました。

ラズパイ本体側のプルアップ抵抗とは、GPIOピンのすぐそばに表面実装されている R23 / R24 のチップ抵抗です。そのまま半田ごてを当ててもなかなか取れないので、半田を盛ってから吸い取り線で一緒に吸ってしまうとキレイに取れます。

010

ピッチ変換キットには10KΩのプルアップ抵抗が実装されていてるので、これをショートさせて有効にします。

009

その後、さらに調べてみたところ、ストロベリーリナックス製の LCD「SB0802GN」が、秋月のものとほぼ同じ仕様(形状もコントローラも同じ)で、これなら問題なく認識されるようです。ラズパイ本体に手を加えたくない場合には、代わりにこちらの LCD を使うのがいいかもしれません。

コードを書く

ラズパイの設定変更

ADコンバータとLCDの接続に、それぞれ SPI と I2C を使うので、これらを有効にするための設定をします。

/boot/config.txt に以下の内容を追記します。

dtparam=i2c_arm=on
dtparam=spi=on

/etc/modules に以下の内容を追記します。

i2c-bcm2708
i2c-dev
spi_bcm2835

再起動後に lsmod を実行して指定したモジュールがロードされていれば大丈夫です。

# lsmod
Module                  Size  Used by
sch_netem               8032  0 
bridge                 93064  0 
ipv6                  339514  17 bridge
stp                     1479  1 bridge
llc                     3600  2 stp,bridge
i2c_dev                 6047  0 
asix                   19981  0 
libphy                 24139  1 asix
i2c_bcm2708             5014  0 
spi_bcm2835             7248  0 
uio_pdrv_genirq         2966  0 
uio                     8235  1 uio_pdrv_genirq

必要なツールとライブラリのインストール

制御プログラムをC言語で書きますが、超絶便利なライブラリ「Wiring Pi」を使います。Wiring Pi は、ラズパイの GPIO を制御するためのライブラリで SPI や I2C にも対応しています。今回はADコンバータからボリュームの値を読み出す処理で Wiring Pi を使います。LCDの制御に関しては手抜きをして i2c-tools に頼ります。

  • i2c-tools
  • libi2c-dev
  • Wiring Pi

i2c-tools と libi2c-dev を apt でインストールします。Wiring Pi は git のリポジトリを clone してビルド&インストールします。

# apt-get install i2c-tools libi2c-dev
# git clone git://git.drogon.net/wiringPi
# cd wiringPi
# ./build

制御プログラム

アナログUIの制御プログラムです。一秒おきにADコンバータからボリュームの値を読み取り、前回の値から変改していたら tc コマンドの実行とLCDへの描画を行います。ほとんどの処理を system() 経由で外部コマンドに丸投げしていたり、エラー処理が手抜きだったり、かなり雑に作っていますがプロトタイプなのでご愛嬌。

/*
 * packdrop.c
 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <wiringPi.h>
#include <wiringPiSPI.h>
#include <mcp3002.h>

#define APP_NAME     "PackDrop"
#define APP_VERSION  "v0.4"

#define SPI_CHANNEL  (0)
#define PINBASE      (100)
#define VOLUME_DELAY (PINBASE)
#define VOLUME_LOSS  (PINBASE+1)

char **devices;
int devnum;

static void
tc_set (const char *dev, int delay, int loss) {
    char cmd[128];

    snprintf(cmd, sizeof(cmd), "tc qdisc change dev %s root netem delay %dms loss %d%%", dev, delay, loss);
    system(cmd);
}

static void
tc_init (const char *dev) {
    char cmd[128];

    snprintf(cmd, sizeof(cmd), "tc qdisc del dev %s root >/dev/null 2>&1", dev);
    system(cmd);
    snprintf(cmd, sizeof(cmd), "tc qdisc add dev %s root netem delay 0ms loss 0%%", dev);
    system(cmd);
}

static void
lcd_write_core (const char *upper, const char *lower) {
    char cmd[128];

    system("i2cset -y 1 0x3e 0 0x80 b");
    snprintf(cmd, sizeof(cmd), "i2cset -y 1 0x3e 0x40 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x i",
        upper[0], upper[1], upper[2], upper[3], upper[4], upper[5], upper[6], upper[7]);
    system(cmd);

    system("i2cset -y 1 0x3e 0 0xc0 b");
    snprintf(cmd, sizeof(cmd), "i2cset -y 1 0x3e 0x40 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x i",
        lower[0], lower[1], lower[2], lower[3], lower[4], lower[5], lower[6], lower[7]);
    system(cmd);
}

static void
lcd_write (int delay, int loss) {
    char upper[9], lower[9];

    snprintf(upper, sizeof(upper), "  %3d ms", delay);
    snprintf(lower, sizeof(lower), "  %3d %%", loss);
    lcd_write_core(upper, lower);
}

static void
lcd_init (void) {
    char name[9], ver[9];

    system("i2cset -y 1 0x3e 0 0x38 0x39 0x14 0x78 0x5f 0x6a i");
    system("i2cset -y 1 0x3e 0 0x0c 0x01 i");
    system("i2cset -y 1 0x3e 0 0x06 i");
    snprintf(name, sizeof(name), "%8s", APP_NAME);
    snprintf(ver, sizeof(ver), "%8s", APP_VERSION);
    lcd_write_core(name, ver);
}

int
main (int argc, char *argv[]){
    int n, v, d, l;
    int delay = 0, loss = 0;

    lcd_init();
    if (mcp3002Setup(PINBASE, SPI_CHANNEL) < 0) {
        printf("mpc3002Setup: failed\n");
        return -1;
    }
    devices = argv + 1;
    devnum  = argc - 1;
    for (n = 0; n < devnum; n++) {
        tc_init(devices[n]);
    }
    while (1) {
        v = analogRead(VOLUME_DELAY);
        d = ++v / 2;
        v = analogRead(VOLUME_LOSS);
        l = ((float)++v / 1024) * 100;
        if (d != delay || l != loss) {
            delay = d;
            loss  = l;
            for (n = 0; n < devnum; n++) {
                tc_set(devices[n], delay, loss);
            }
            lcd_write(delay, loss);
        }
        sleep(1);
    }
    return 0;
}

Wiring Pi のライブラリを使っているので、-lwiringPi を指定してコンパイルします。

# gcc -W -Wall -o packdrop packdrop.c -lwiringPi

動かしてみる

実際に制御プログラムを動かして、ちゃんと遅延とパケロスが発生するのかを確認します。

まず、事前にブリッジを作成しておきます。

# brctl addbr br0
# brctl addif br0 eth0 eth1
# ip link set eth0 up
# ip link set eth1 up
# ip link set br0 up

続けて、制御プログラムの packdrop を起動します。packdrop の引数には、ブリッジに追加したインタフェースを指定します。

# ./packdrop eth0 eth1

実際に動作させている動画です。

動画を見ると一目瞭然ですが、青いボリュームを回すと「遅延」が増加し、ピンクのボリュームを回すと「パケロス」が増加します。遅延もパケロスも「上りと下りの両方に適用」されるため、ping の RTT には設定した遅延の2倍の時間が加算され、パケロスが 10% の場合には 0.9 x 0.9 = 0.81(81%)の確率で応答パケットが届きます。

余談

このネットワークエミュレータには「PackDrop」という名前を付けたのですが、なぜか社内では「パケ落ちくん」という愛称で呼ばれています。

次回予告

半分ネタで雑に作ったネットワークエミュレータですが、実際に案件でデバッグに使ってみてもらったところ、わりと評判が良く、各所から「それ欲しい」という要望が出てきました。

ラズパイの在庫は豊富にあるので何台でも作れるのですが、指を引っ掛けたら配線が抜けてしまうんじゃないかとか、両面テープで固定しているだけなのでボリュームが取れてしまうんじゃないかとか、ちょっと物理的に脆弱すぎるという問題が...

他にも、ちゃんと起動スクリプトを作ったり電プチに対応したり、アプライアンス的に使えるようにした方が使い勝手が良さそうだったりします。そんな訳で、後編ではもう少し真面目に作った「量産型」を紹介します。

013

klab_gijutsu2 at 16:29
この記事のURLComments(0)TrackBack(0)network 
2016年04月01日

Thundering herd 対策の本命、 EPOLLEXCLUSIVE を試してみた

はてなブックマークに登録

epoll を使った prefork 型アプリケーションサーバーにおける Thundering herd 対策の決定版として注目されていた EPOLLEXCLUSIVE が、 3/13 にリリースされた Linux 4.5 で導入されました。

昨年 SO_REUSEPORT というソケットオプションが登場して、 Thundering herd 対策として話題になったものの、ワーカーごとに listen キューが作られるため graceful restart するときに listen キューに入ってるリクエストを取りこぼす可能性があり利用するのが難しい状況でした。

参考: epoll の thundering herd 問題について解説しているサイト

一方、 EPOLLEXCLUSIVE は、 listen キューは一本のまま、その listen キューを待っている複数プロセスの epoll_wait() を全部起こすのではなく、1つ以上(1つとは限らない)だけ起こすという機能です。 graceful shutdown 中は epoll_wait() をしていないはずなので、 listen キューに来たリクエストは他のワーカープロセスの epoll_wait() に通知され、取りこぼしがありません。

http://man7.org/linux/man-pages/man2/epoll_ctl.2.html

       EPOLLEXCLUSIVE (since Linux 4.5)
              Sets an exclusive wakeup mode for the epoll file descriptor
              that is being attached to the target file descriptor, fd.
              When a wakeup event occurs and multiple epoll file descriptors
              are attached to the same target file using EPOLLEXCLUSIVE, one
              or more of the epoll file descriptors will receive an event
              with epoll_wait(2).  The default in this scenario (when
              EPOLLEXCLUSIVE is not set) is for all epoll file descriptors
              to receive an event.  EPOLLEXCLUSIVE is thus useful for
              avoiding thundering herd problems in certain scenarios.

使い方は、 EPOLL_CTL_ADD するときに、 events で EPOLLIN などと bitwise OR で組み合わせて利用します。

struct epoll_event ev = {EPOLLIN | EPOLLEXCLUSIVE, NULL};
epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev);

環境準備

Ubuntu 16.04 LTS xenial xerus や Amazon Linux 2016.03 といった最新(あるいはもうすぐリリースされる)のディストリビューションでも、採用しているのは Linux 4.4 なので、そのままでは試すことができません。 (Fedora 24 は Linux 4.5 らしいですが、普段使わないので除外しました)

そこで、 Ubuntu 15.10 を元に、 https://wiki.ubuntu.com/KernelTeam/GitKernelBuild にある手順に従い Linux 4.5 をソースからビルドして再起動することにしました。 書いてある手順通りで問題ないのですが、私の場合 Ubuntu 15.10 の公式 AMI のデフォルトディスクサイズ (8GB) で容量が足らずに一度ビルドに失敗して32GBに拡張したので、同じくAWSを使われる方は注意してください。

カーネルのインストールが終わって再起動したら、Linux 4.5 カーネルを利用していることを確認します。 また、 <sys/epoll.h> にはまだ EPOLLEXCLUSIVE が定義されていないので、定数の値を確認しておきます。

$ uname -a
Linux ip-10-0-1-221 4.5.0-custom #1 SMP Thu Mar 31 07:35:29 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux

~/linux-4.5/include$ ag EPOLLEXC
uapi/linux/eventpoll.h
30:#define EPOLLEXCLUSIVE (1 << 28)

EPOLLEXCLUSIVE を使った場合にどれくらい性能に影響がでるか、簡単なベンチマークをしたいので、 TIME_WAIT で詰まる問題を回避するおまじないをしておきます。

$ echo 'net.ipv4.tcp_tw_reuse = 1' | sudo tee -a /etc/sysctl.conf
$ sudo sysctl -p
net.ipv4.tcp_tw_reuse = 1

meinheld

meinheld は奥一穂さんの picoev を利用した Python アプリケーション用の HTTP サーバーです。

listen ソケット以外に、ワーカー間で共有しつつ epoll を利用する fd はないので、 EPOLL_CTL_ADD しているところで無条件に EPOLLEXCLUSIVE してしまいます。

diff --git a/meinheld/server/picoev_epoll.c b/meinheld/server/picoev_epoll.c
index 06e1dbb..773cf3c 100644
--- a/meinheld/server/picoev_epoll.c
+++ b/meinheld/server/picoev_epoll.c
@@ -115,6 +115,7 @@ int picoev_update_events_internal(picoev_loop* _loop, int fd, int events)
     SET(EPOLL_CTL_MOD, 0);
     if (epoll_ret != 0) {
       assert(errno == ENOENT);
+      ev.events |= (1 << 28);  // EPOLLEXCLUSIVE (from linux 4.5)
       SET(EPOLL_CTL_ADD, 1);
     }
   }
@@ -124,7 +125,12 @@ int picoev_update_events_internal(picoev_loop* _loop, int fd, int events)
   if ((events & PICOEV_READWRITE) == 0) {
     SET(EPOLL_CTL_DEL, 1);
   } else {
-    SET(target->events == 0 ? EPOLL_CTL_ADD : EPOLL_CTL_MOD, 1);
+    if (target->events == 0) {
+      ev.events |= (1 << 28);  // EPOLLEXCLUSIVE (from linux 4.5)
+      SET(EPOLL_CTL_ADD, 1);
+    } else {
+      SET(EPOLL_CTL_MOD, 1);
+    }
   }

 #endif

試験に使っていたのは AWS の c4.xlarge で、4コアなので、8ワーカー起動し、 "hello, world" を返すだけのアプリに ab で4並列で負荷をかけてみました。 (wrk ではなく ab を使ったのは、今回は keep-alive しない試験だったからです。)

結果は、改造前がだいたい 15k req/sec で、 改造後がだいたい 20k req/sec を超えるくらいでした。

uWSGI

Python だけでなくいろんな言語に対応している uWSGI というサーバーでも試してみました。 epoll に追加する関数が read と write で別れているので、accept で利用する read 側にだけ EPOLLEXCLUSIVE を追加します。

diff --git a/core/event.c b/core/event.c
index 36751a6..32a1934 100644
--- a/core/event.c
+++ b/core/event.c
@@ -514,7 +514,7 @@ int event_queue_add_fd_read(int eq, int fd) {
        struct epoll_event ee;

        memset(&ee, 0, sizeof(struct epoll_event));
-       ee.events = EPOLLIN;
+       ee.events = EPOLLIN | (1 << 28);
        ee.data.fd = fd;

        if (epoll_ctl(eq, EPOLL_CTL_ADD, fd, &ee)) {

こちらもだいたい meinheld と同じような数値 (15k req/sec -> 20+k req/sec) で性能向上を確認できました。

また、1並列で ab を実行したときの accept の回数を、 strace で確認してみました。

ubuntu:~/app$ strace -f ~/.local/bin/uwsgi --master --workers=8 --http-socket :8080 --wsgi hello 2>trace.log
ubuntu:~$ ab -c1 -n1000 http://localhost:8080/  # 別セッションから
ubuntu:~/app$ grep -c '] accept4' trace.log  # 改造前
6884
ubuntu:~/app$ grep -c '] accept4' trace.log   # 改造後
1557

改造前は 6884 回ですが、これは 8 プロセスのうち accept に成功した 1 プロセスが、レスポンスを返してから epoll し直している間に、他のプロセスが次のリクエストを accept しているから、7000回弱になっているのでしょう。

一方改造後は 1557 回になっていて、リクエストの回数の約 1.5 倍になっています。 man に書いてあるとおり、必ずしも1つのイベントに対して1つの epoll だけが起こされるわけではなく、複数の epoll が起こされる場合があるのだと思います。

雑感

meinheld を strace していて気づいたのですが、 thundering herd 問題が起きている状況で multi accept (1度 epoll が起きたときに、 accept を1度だけでなく、エラーが出るまで複数回実行する) をすると、「起こされたのにすることがない」状況が発生しやすくなります。

keep-alive に対応しないシンプルなアプリケーション・サーバーなら、レスポンスを返したあと次にすることがなので、「自分(レスポンスを返したばかりのプロセス)が寝るか、他人(起こされたばかり)が寝るか」の違いでしかありません。自分で実行したほうが、コンテキストスイッチを待たずに処理を始められるので、ベンチマークスコアは上がると思います。 一方、 keep-alive に対応していたり、 nginx のようにいろんな処理を行う場合は、 multi accept を無効化した方が綺麗にコネクションを分散させることができて良いかもしれません。

あと、今回はかなり雑な感じに EPOLLEXCLUSIVE 対応したのですが、古いディストリビューションを使いつつもカーネルをアップグレードすることはよくあるので、 #ifdef EPOLLEXCLUSIVE せずに、今回のような強引な方法で対応するのは意外と現実的な気がします。 実際、 xenial の標準のカーネル (Linux 4.4) で EPOLLEXCLUSIVE を有効にしても、 epoll_ctl がエラーを返すこともなく普通に動作しました。


@methane


songofacandy at 15:58
この記事のURLComments(0)TrackBack(0)kernel | network
2016年03月11日

「入院者と外部とのコミュニケーション」への IoT の応用

はてなブックマークに登録

フェイススケールのこと

先日身内が入院した折にこういう顔の絵の入ったクリアファイルを病棟で見かけました。

フェイススケール(英: Face scale)とは、現在」の痛みを「にっこり笑った顔」から普通の顔、
そして「しかめっ面」そして「泣き顔」までの様々な段階の顔を用意して、神経痛などの痛みを
訴えている患者にどのぐらい痛むのかを示してもらうことで、その痛み度を客観的に知るために、
ペインクリニック・麻酔科などで用いられる用具・用紙のことである。
看護師さんがベッドに横になったままのご高齢者にこれを見せながら大きな明るい声で尋ねている様子が印象に残りました。たった一枚の印刷物ですが、自由に話したり体を動かしたりすることのむずかしい状態の入院者とのやりとりには特に有用なツールであることが想像されます。

入院者と外部とのコミュニケーションへの IoT の応用

携帯端末での電子メール利用の普及により入院者と外部との連絡はメール以前の時代に比べ格段に便利になりました。病室内での通話は無理でもメールの送受信は多くの場合認められており、おそらくは施設側が伝言を取り次ぐ頻度も昔よりもずっと少なくなったことでしょう。

その一方で、症状や状況によっては小さなキーを操作することが困難であったり、あるいは携帯端末やメールとの接点を想像しにくい雰囲気の入院者も少なくないことを連日の病棟内の光景であらためて実感しました。残念なことに、現実にはそういう人たちにこそ関係者との日常的なつながりがより必要なケースが多いようにも感じられます。たとえ最小限の内容であっても当事者間で手軽にコミュニケーションをとることができれば双方の不安が確実に軽くなることを考えあわせると、社会の高齢化が進行する状況にあってこの話題は潜在的な必要性の高いテーマのひとつと言えるのではないでしょうか。

試みに、フェイススケールをヒントに病室内での使用を想定してごく簡単な操作で離れた相手とメッセージを交わすしくみを形にしてみました。身近な場面での実用性のある IoT のアイディアとして紹介します。

試作装置の外観と概要

下の写真の装置のいずれかのボタンを押すと相手側の装置の同じ絵の下の緑の LED が点滅し、併せてその絵柄を添えたメールが所定のアドレスへ届きます。
親しい間柄の人や自分自身が病室のベッドで苦しんだ経験のある方にはわかるのではないかと思いますが、いま入院者に「ボタンを押すだけの余力」と「絵柄を選択する意識」があるということを情報として伝えられることには、離れて案ずる側にとっても入院者自身にとってもそれ自体に大きな意味があります。また、一方通行ではなく相手からの反応が手元に届くことは双方の精神的な支えとなるでしょう。写真のとおり、互いに言葉や意味ではなく気持ちや意識を伝えることに主眼を置いてそれぞれのボタンの絵柄はあえて解釈の余地の広いものを選んでいます。

※クリックで大きく表示
 

装置を病室に設置したイメージ

メールには押したボタンの画像がインラインで表示されリンクのクリックで返信用のページが開く

  • インターネット接続環境を備えた入院施設が少ないことを考慮し装置は現在もっとも低コストでモバイル通信のハンドリングが可能な Raspberry Pi + 3G モデムの構成で MVNO SIM を使用しておりコンセントひとつで稼働可能
  • 自分が押したボタンは赤 LED の点灯、相手の押したボタンは緑の LED の緩やかな点滅で示される
  • 装置間のメッセージングには Milkcocoa 、メール送信には SendGrid を利用
  • 赤ボタンの押下で各 LED を消灯、5秒以上の長押しで装置をシャットダウン
  • 右端の LED は装置の稼働を示すパイロットランプ

動作の様子(動画:2分20秒 音声なし)

このデモ動画は入院者側が実装置を使用し相手側がインターネット接続環境のもとで Web 版の仮想装置を使用することを想定した内容です。もちろん二台の実装置間での応酬も可能です。

画像について

今回の試作では画像がコミュニケーションの仲介者として大きな役割を担っており、これらは病室という気持ちの沈みがちな空間に小さな癒しを灯すシンボルでもあります。一連の画像は Beeboo.SLI(ビブースライ)様が運用される画像素材サイト 「イラスト無料素材 イラストボックス」I<アイ>さん が掲載された作品を加工して使用させて頂いています。I<アイ>さんの素敵なイラストと素晴らしいサイトに謹んで御礼申し上げます。

装置づくり

メインの要素

これらに説明は要らないでしょう。いずれも「持っていると便利」な道具です。
Raspberry Pi 2 Model B

アマゾンで 5,000円前後
3G USB モデム: L-05A

2015年10月に「イオシス」で 1,180円で購入(中古)
MVNO SIM:0 SIM
デジモノステーション 2016年02月号付属版)

手元の SORACOM Air とどちらを使うか迷ったが今回は 0 SIM を使用

筐体の材料と加工

今回の装置では操作用のボタンの選定が大きなポイントでした。小さすぎず大きすぎないものを探し以下の製品を選びました。

パネル取付用押しボタンスイッチ(緑色)です。過酷な使用に耐えられるヘビーデューティー
仕様です。プッシュ機構部(樹脂製)とマイクロスイッチで構成されています。

このボタンは取りつけ時の占有直径がおよそ 34mm で、これを 5mm 程度の間隔で 7つ設置するには最低 28cm ほどの長さが必要です。100円ショップをいくつか見てまわり筐体として写真のパスタケースを選びました(長さ:30.6cm 幅:8.5cm 高さ:7.6cm)。 蓋の方が少し広めの台形で、固い底のほうを上にすれば安定感があります。

このケースの底にボタンを取りつけるには厚さ 3mm のポリプロピレン板に 30mm 程度のまるい穴が 7つ必要です。今の自分の工作力ではこのように硬く厚みのある素材に綺麗なまるい穴をあけることが難しく、練習用の予備であれこれやってみましたがなかなかうまくいきません。店先でたまたま野菜調理用の金属製の抜き型が目にとまりました。下の写真の大2 + 小2 セットの「小」の持ち手側の外径が 3cm に近く、試しにこれを熱してケースに押し当てるとちょうどよいサイズの綺麗な穴が得られました。ケースの両端には 1cm のマージンを設け、シャットダウンボタン用の一番右の穴は隣りと 1cm 離してあとは 5mm 間隔で穴をあけました。

野菜の抜き型 (金属製)

穴をあけ終えたケース

パーツの取りつけ

実際にボタンを取りつけてみると全体の印象は当初のイメージどおりでした。ただ、ボタン 7個、LED 13個分の計 40本の配線を内部の残りのスペースの四方へめぐらせるには電子工作用の一般的なワイヤでは固すぎて取り回しに融通が利かず、柔らかい導線を加工して筐体内の隙間へ這わせることにしました。スペース節約のため GND への繋ぎ込みにはメスのピンソケットのオス側をスズメッキ線で連結したものを使いました。

ざっくり結線。ワイヤが固い

導線にLEDと抵抗とハンダづけして末端をオス・メス化

裁断した色画用紙を加工

ケースへ一式を収納して動作確認

ソフトウェア要素

作成したプログラム類一式を以下の場所で公開しています。

https://github.com/mkttanabe/iot_hospital

使用しているソフトウェア資産とサービス

土台の Raspberry Pi 2 Model B には標準ディストリビューションの Raspbian をインストールずみで今回は以下のソフトウェア資産を使用しています。

また、ふたつの優れたサービスを利用しています。
  • Milkcocoa 装置間のメッセージング用
  • SendGrid 実装置からのメール送信用

メインプログラム

実装置用のプログラム本体は Node.js スクリプトとして記述しています。

/home/pi/node/iot_hospital.js

Web 版仮想装置のページは以下の URL から参照できます。

http://dsas.blog.klab.org/data/iot_hospital/index.html?ID02&ID01

注:Web 版の動作には Milkcocoa アカウントを取得し 22行めに正しい APP ID を指定する必要があります

モデムまわり

L-05A + 0 SIM の組み合わせでのインターネットへの接続には wvdial ユーティリティを使用しています。/etc/wvdial.conf は以下の内容。"sudo wvdial" の実行で接続を開始します。

/etc/wvdial.conf

[Dialer Defaults]
init1 = ATZ
init2 = AT+CGDCONT=1,"IP","so-net.jp"
Password = nuro
Username = nuro
Dial Attempts = 3
Stupid Mode = 1
Modem Type = Analog Modem
Dial Command = ATD
Stupid Mode = yes
Baud = 115200
New PPPD = yes
Modem = /dev/ttyACM0
ISDN = 0
Phone = *99***1#
Carrier Check = no

※L-05A はゼロインストール機能を内蔵しており出荷時の設定により USB 接続時にはデフォルトで CD-ROM デバイスとして認識されます。eject コマンドで毎回これを解除するのが煩わしいため手元ではデフォルトで USB モデムとして認識されるように設定を変更しています。

モデムモードに設定した L-05A は lsusb コマンドで以下の要領で表示されます。

Bus 001 Device 004: ID 1004:6124 LG Electronics, Inc. 

スタートアップまわり

システム起動時に今回の一式を自動実行するために /etc/rc.local に iot_hospital.sh の呼び出しを記述しています。 iot_hospital.sh は L-05A が装着されていれば wvdial でインターネット接続を試行し、接続に成功すれば前掲の iot_hospital.js を実行します。

/etc/rc.local

#!/bin/sh -e
#
# rc.local
#
# This script is executed at the end of each multiuser runlevel.
# Make sure that the script will "exit 0" on success or any other
# value on error.
#
# In order to enable or disable this script just change the execution
# bits.
#
# By default this script does nothing.

# Print the IP address
_IP=$(hostname -I) || true
if [ "$_IP" ]; then
  printf "My IP address is %s\n" "$_IP"
fi

# run app
sudo /bin/bash /home/pi/node/iot_hospital.sh &

exit 0

/home/pi/node/iot_hospital.sh


(tanabe)
klab_gijutsu2 at 07:01
この記事のURLComments(2)TrackBack(0)IoT | cloud
2016年03月08日

Tornado アプリのログファイル書き込みのチューニング

はてなブックマークに登録

最近は協力プレイやPvPなどの「リアルタイムサーバー」を書くときは Go が主流になっているのですが、 Tornado を使ったシステムも健在です。 (以前の記事)

数人〜十数人程度の「部屋」を、1つの Tornado プロセスに複数もたせ、さらに一台のサーバーにその Tornado プロセスを複数置くことでCPUのマルチコアを活用する構成になっているのですが、最近各プロセスがログファイルを書く部分でブロックして応答性能が悪化するケースがあったので対策しました。

この記事ではその対策で行ったチューニングや、行わなかったチューニングについても紹介します。

※なお、この記事は Tornado を題材にしていますが、似たような仕組みになっている node.js などの他の言語のフレームワークでも同じ事が言えるはずです。

前提知識

Tornado は epoll や select などのIO多重化システムコールを利用したネットワークアプリケーション用のフレームワークです。

IO多重化はソケットなどのノンブロッキングIOと組み合わせたときは上手く行くのですが、ファイル書き込みはノンブロッキングIOにはできません。 epoll や select が「書き込み可能」と判断しても、実際に書き込もうとするとOSにブロックされてしまうことがあります。

とはいっても、ファイル書き込みは常にブロックするわけではありません。書き込み量やディスクの性能、メモリ量などによってはブロックしないことも多いです。なので、何も問題が起こってないなら Tornado アプリでも普通にログをファイルに書いていて良いのですが、 write がブロックするようになったなら IO 多重化を行っているループを阻害しないように対策をする必要があります。

write でブロックが発生しているかどうかは、 CPU の iowait が発生しているかどうかで調べることができます。さらに strace を使えばどの write が主なブロック要因になっているのかも調べることができます。

対策1: ログ書き込みを別スレッドで行う

ioloop が回っているメインスレッドでファイルへの write を直接行うと、そこでブロックしたときに全体が止まってしまい、応答性能が悪化してしまいます。 write を別スレッドで行えば、 write のブロックに ioloop が引きずられなくなるはずです。

今回問題になったアプリは Logbook というログライブラリを使っていました。このライブラリには標準で、ログ書き込みを専属のスレッドで行う仕組みがあるので、それを使えば既存コードに2行足すだけでログ書き込みの別スレッド化ができました。

import logbook
from logbook import FileHandler
from logbook.queues import ThreadedWrapperHandler  ## ThreadedWrapperHandler を import し

handler = FileHandler('application.log')
handler = ThreadedWrapperHandler(handler)          ## 既存の Handler をラップする
handler.push_application()
...

これでだいぶ応答性能は改善したのですが、この方法だとログを1つずつスレッド間通信が挟まるので、オーバーヘッドが大きく、CPU効率があまり良くありません。 Python は C言語と比べてスレッドの切り替えコストが高いのです。

対策2: ログ書き込みをパイプ経由で別プロセスで行う

ファイルへの書き込みがブロックする条件は複雑(あとで少し触れます)なのですが、パイプへの書き込みはパイプバッファ (一般的な Linux では多分64KB) がいっぱいになるまでブロックしません。

さらに、プロセスを分けてコマンドを実行する方式なら gzip 圧縮などを効率よく実行できます。 gzip 圧縮は Python 内で別スレッドで処理することができますが、スレッド間でのやりとりをバッファリングして大きな粒度で行うなどの最適化を行わなければ、「対策1」と同じくオーバーヘッドが大きくなってしまいます。それに対してパイプへの書き込みなら通常のファイルIOと同じ仕組みを使って簡単にブロックバッファリングが可能です。

また、パイプへの書き込みはファイル書き込みと違ってノンブロッキングIOにすることができるので、ソケットと同じように効率的に多重化することもできます。そこまでやれば、 gzip からの書き込みが詰まって、 gzip へのパイプまで詰まることになっても、 ioloop をブロックせずに済みます。

今回は gzip 化することで iowait が消え去ったのと、 gzipへのパイプまで詰まるようなスペックのサーバーではそもそも安定してサービスできないだろうという事で、よりシンプルな直接パイプへ書き込む方式を採用しました。 利用した Logbook でパイプ経由で gzip 圧縮するログハンドラのコードはこちらです。

import subprocess
import logbook
from logbook.helpers import is_unicode

class GZFileHandler(logbook.FileHandler):
    """GZFileHandler is FileHandler compressing output via piped gzip command"""

    proc = None

    def __init__(self, filename, complevel=9, **kwargs):
        kwargs['delay'] = False
        logbook.FileHandler.__init__(self, filename, **kwargs)

        cmd = ["gzip", "-" + str(complevel)]
        self.proc = subprocess.Popen(cmd, bufsize=-1,
                                     stdin=subprocess.PIPE,
                                     stdout=self.stream.fileno(),
                                     close_fds=True)

    def write(self, item):
        if is_unicode(item):
            item = item.encode(self.encoding)
        self.proc.stdin.write(item)

    def close(self):
        self.proc.stdin.close()
        self.proc.wait()
        logbook.FileHandler.close(self)

handler = GZFileHandler('application.log.gz')

「対策1」のコードとパフォーマンスを比較しましょう。ベンチマークでは、単純に100万回ログを出力します。

def main():
    for i in range(1000**2):
        logbook.warn("Hello, World: " + str(i))

if __name__ == '__main__':
    try:
        main()
    finally:
        handler.close()

「対策1」:

$ time python thread_log.py

real    0m58.131s
user    0m54.064s
sys     0m3.913s

「対策2」:

$ time python gzip_log.py

real    0m34.492s
user    0m37.763s
sys     0m0.122s

スレッド版に比べて処理速度の面でも大幅に効率的な事がわかります。もちろん gzip -9 しているので、ログファイルのサイズも大幅に圧縮できています。

対策3: カーネルパラメータチューニング

iowait 対策として、 write がブロックする条件をカーネルパラメータで調整することもできます。

特に重要なのが vm.dirty_ratio という値で、まだディスクに書き込んでない "dirty" なバッファの上限を、メインメモリに対するパーセント値で指定します。書き込みが済んでないデータの量がこの値に到達すると write() がブロックされるようになります。

この値を大きくすると、「時々詰まるけど全体としてはまだディスク書き込み性能に十分な余裕がある」という条件で iowait を消すことができるかもしれません。 一方で、本当に書き込み性能が足りなくなったときに、こまめに短時間詰まっていたのが、低頻度でも致命的に長時間詰まるようになる可能性もあります。 ディスク性能を把握していてまだ余裕があると解っているときに少しずつ増やすか、ちゃんと本番環境と同じスペックの負荷試験環境を用意して想定する上限の負荷を掛けながらチューニングしましょう。

ただし、このチューニングは、負荷試験していたときに思いつかなかったので今回は実施していません。思いつかなかった事が悔しかったのでここで紹介だけしておきました。

対策4: スケールアウト/スケールアップ

問題を把握して真っ先に行ったのが、サーバー台数を増やして数で解決することでした。その時点で現象は軽減されたのですが、解決したわけではなく、特にEBSのレイテンシが悪い一部のインスタンスでまだ問題になっていました。

利用していた EBS が Magnetic だったので SSD にすることもできましたが、今回はゲームの特性上エフェメラルディスクが利用可能だったので、SSDのエフェメラルディスクを利用するようにしました。

アプリケーション側で多少の iowait があっても問題ないようにした上で、ハードウェア側も(コスト的に許されるなら) iowait が無くなるまでスペックアップすることで、安心してサービスすることができます。


@methane
songofacandy at 20:02
この記事のURLComments(0)TrackBack(0)
2016年02月19日

ツイッター公式検索の謎と現時点での対処方法について

はてなブックマークに登録

(この記事は 2016年 2月時点の観察に基づくものです)

サイト上の記事に関するツイートは当事者にとっても閲覧する側にとっても興味深いものです。ただ、ツイッター公式の検索機能を使って所定の URL を含むツイート一覧を取得する際に、クエリ文字列の指定方法により検索結果が大きく異なるケースがあることに気がつきました。また、その現象はサイトにより一様ではありません。以下にその内容を控えます。お手元のサイトではどうでしょう?

話題の背景

米 Twitter 社は Web パーツとして提供中のツイートボタンへのカウンタ表示を 2015年 11月 20日に廃止しました。

この措置をめぐっては様々な話題がネットを賑わせましたが、一私企業による無償系のサービスにおいて仕様変更が発生することは避けがたい側面もあるため利用に際しては恩恵を享受すると同時にやはりこのような可能性についても含んでおくべきでしょう。下記のような非公式の代替 API の利用へ乗り換えることでツイート数表示を維持しているサイトも見受けられます。

ただ、カウンタ廃止そのものは甘受するとしても、従来カウンタ表示位置にリンクの設置されていたツイートの一覧表示(検索)機能も利用できなくなったことはサイト運営者にとってもサイト訪問者にとっても不便で、せっかくの SNS 連携であるにもかかわらずソーシャル性が少なからず削がれた残念な状況となりました。

そのため自前で一覧表示用のリンクを設置するサイトが増えています。当ブログにも 2015年末にツイートボタンの右側に「見る」リンクを加えました。
このツイート一覧表示用のリンクの設置に際し、今まで気づかずにいた発見がありました。

検索対象とする URL の指定方法で検索結果が異なる??

https://twitter.com/search-home - ツイッター公式検索画面)

当「DSAS 開発者の部屋」ブログにおいて、従来のカウンタつきツイートボタンの頃から次の現象を見ていました。

  • ツイートカウンタのクリックで一覧表示されるツイートのうち、日数の経ったものは順次消えていく
過去のツイートが時間の経過とともにツイッター公式の検索結果から消えてゆく現象は「普通のこと」であり「そういうもの」だと思っていました。一例として当ブログにおいて閲覧数の多いエントリのひとつである次の記事をピックアップしてみます。

このエントリは 2006年 12月公開のかなりふるいものです。現時点で、本項冒頭に掲載のツイッター公式検索を使って上記の URL を含むツイートを照会すると以下の結果となります。

https://twitter.com/search?f=tweets&q=http%3A%2F%2Fdsas.blog.klab.org%2Farchives%2F50860867.html%20&src=typd

この「見つかりませんでした」という結果は見慣れたもので、これを目にした場合はこのところ当該記事についてのツイートはなかったものと認識しごく普通の結果として受けとめていました。

ところが、自前で「見る」リンクを設置する作業の途中で、ふとしたコピペミスで
「http://dsas.blog.klab.org/archives/50860867.html」ではなく
「dsas.blog.klab.org/archives/50860867.html」と指定したところ意外な結果となりました。

https://twitter.com/search?f=tweets&q=dsas.blog.klab.org%2Farchives%2F50860867.html&src=typd

50件以上のツイートがヒットします。もっともふるいものは「2008年 5月 28日」とあります。少し驚きました。

このように、当ブログでのツイート検索においては「http://」のスキーム記述部分をクエリ文字列に含む・含まないの違いで検索結果が大きく異なります。ツイート本文中の当該記事の URL 表示箇所は「dsas.blog.klab.org/archives/…」の要領でありたしかに「http://」は含まれませんが、記述が短縮 URL であっても適切に拾われており、また、日数の経っていない新鮮なツイートであればどちらの書式で照会しても同じ検索結果となるためどうも事情が判然としません。

不思議な結果ではありますが、このことは同時に有用でもあると考えました。「見る」リンクの内容は自前で記述するのですから、この書式にすることで旧ツイートボタン上のリンクからは拾ってくることのできなかったツイートを参照できるのであればそれは一方では喜ばしいことです。記事についてのツイートは貴重な財産であり、投稿された方のモチベーションを下げないためにもその適切な表示は重要でしょう。

そんなわけで、当ブログでは暫定的にツイートの「見る」リンクでの検索クエリ文字列を「http://」を含めない内容としています。この措置により以前は参照できなかった過去ツイートの一覧を表示可能となりました。この件は今後も観察を続け、もし事情に変化があればそれに追随することを想定しています。

過去ツイートの検索そのものが可能となった背景

今回の一件に関連する情報を探す過程で、米 Twitter 社が 2014年 11月に 2006年 3月以降のすべての公開ツイートのインデクシングを完了し検索可能とした旨を発表していたことを知りました。この話題は当時見たような見なかったような、、いずれにせよその時点では特に興味がなかったはずなので流していたかもしれません。

端的には、公式の検索機能から前掲のように 2008年のツイートを参照可能であることには受け皿として論理的な整合性があり、あとは適切な方法で検索を行えばよいということですね。

謎はさらに

上に書いたように、当ブログにおいては現時点では記事に対するツイートを検索する際のクエリ文字列に「http://」のスキーム文字列を付与しないことが有効であることはほぼ確実のようです。

その一方で、このことがツイッター公式検索における一般論ではないことにも気づきました。手元で確認した範囲では大きくみっつのパターンがあるようです。以下にそれぞれの実例を挙げてみます。いずれも例示のみを目的に特別な意図なしに選んで確認を行ったエントリでありそれぞれの内容は本記事の内容と関係のあるものではありません。

※各サイト様には事前のご連絡なしに個別の記事へのリンクを掲載させて頂いております。もし何らかのご支障がありましたら速やかにリンクを削除しますので、その場合には大変お手数ですが本記事末尾のコメント用フォームよりお知らせ下さい

※以下の情報はすべて 2016年 2月時点の確認結果です

パターン A:「http://」を付与すると過去ツイート拾わず・付与しなければ拾う

(つまり 当ブログと同じケースです)

例:弊社「KLabGames Tech Blog」の記事より

例:「ライブドアニュース」様の記事より 例:「BLOGOS」様の記事より 例:「バズプラスニュース」様の記事より 例:「サイボウズエンジニアのブログ」様の記事より 例:「痛いニュース」様の記事より

パターン B:「http://」を付与すると過去ツイートを拾う・付与しなければ拾わず

例:「日経 BP ネット」様の記事より

例:「ITmedia ニュース」様の記事より 例:「ケータイ Watch」様の記事より 例:「JCAST ニュース」様の記事より

パターン C:「http://」を付与してもしなくても同様に過去ツイートを拾う

例:「GIGAZINE」様の記事より

例:「CNET Japan」様の記事より 例:「TechCrunch Japan」様の記事より 例:「CodeZine」様の記事より 例:「ASCII.jp」様の記事より 例:「ガジェット通信」様の記事より

所感など

上に挙げたみっつのパターンのうち、もっとも好ましいのはやはり「パターン C: "http://" を付与してもしなくても同様に過去ツイートを拾う」でしょう。それぞれの違いが何に起因するものであるかは今のところわかりませんが最後に少し整理してみます。

  • 各サイトのツイートボタンを押下しメッセージ記述用の雛形を確認してもこれといって特徴的な要素はみとめられず、そもそもツイートをポストする際は単に所定のページの URL を併記するのみであり当該ツイートボタンの使用・不使用を含め投稿者をサイト側の何らかのルールに従わせることはできないはず
  • 上のそれぞれの例を見る限り、URL のパターンによって検索時の所作が変わる規則性のようなものは想定し難い
  • となると、検索処理側で何らかの要素を加味している可能性が考えられる。当該システムは人間の手と判断の介在の度合いが大きいのかもしれない。こういう記事もあった
    ツイッター検索でURL付きのツイートが表示されない (2015-06-30) - 「Pagent」様のブログより
    自分のブログ記事のツイッター検索の結果が1件しか表示されない
    
    その後、別で書いている自分のブログでのことだが、上記同様ツイート数表示を
    クリックした時に表示されるページ(URLをツイッター検索した結果のページ)で、
    何人かツイートしてくれている人がいるのに、ツイートが1件しか表示されない
    状態になっていた。「トップ」でも「ライブ」でも1件のみ。誰のツイートが
    表示されるかは、リツイート数の多いツイートだったり、最新のツイートだったり。
    これではツイートしてくれる人も意欲を削がれるに違いない。
    
    上記の「Twitterヘルプセンター」から日本語で報告してみたが、しばらく待っても
    変化が無い。再度下手くそな英語で報告してみた。
                   :
    1〜2時間ほどすると、ほぼすべてのツイートが表示される状態に改善した。 
                   :
    
2016年 1月 12日に Twitter 社公式サイトの以下のフォームから当ブログ(dsas.blog.klab.org)記事へのツイート検索結果について問い合わせを行い、あわせて「http://」の有無にかかわらず過去ツイートがヒットするよう期待している旨を伝えました。 その後しばらく様子を見ましたが 2月 19日時点で検索結果に特に変化は見られません。上のフォームに個別の返信はされない旨が掲載されていることもあり前述のとおりこの件は当面観察を続けるつもりです。一連の話題について事情をご存知の方はぜひコメントをお寄せ下さい。
(tanabe)
klab_gijutsu2 at 17:16
この記事のURLComments(0)TrackBack(0)SNS 
2016年01月01日

ネットワーク対応の光通知デバイスを自作してみる

はてなブックマークに登録

「blink(1)」のこと

コードネームのような製品名ですが、「blink(1)」は KickStarter 発のカスタマイズ可能な USB デバイスです。接続先の機器やネットワークから所定の情報を受けとると LED 光の色や明滅により人間への通知を行うもので、2012年に製品化され 2013年には IFTTT との連携機能を強化した blink(1) mk2 がリリースされています。

文字の情報は人間が意識的にそこに向き合わなければ伝わらず、音の情報は状況により場の環境を乱す可能性があります。一方、「光」による情報はそれが適度な明るさであれば他の何かを妨げることなく人間の視界へ届き自然に認識され得ます。そのことを応用した例は他にも見られますが、blink(1) もまた光の特性を活かした機器のひとつです。

www.kickstarter.com gigazine.net gigazine.net
※詳細なレポートが GIGAZINE 様の以下の一連の記事に掲載されています。 この blink(1)、なかなか楽しそうなので使ってみたいと思いました。ただ、日本国内には今のところ代理店がなく国外のマーケットから購入することになりますが、これが * 微妙に高価 * なのですね。 そこで、一連の機能の中でもっとも興味のあるネットワーク連携部分を自作してみることにしました。どこかで何かがあったら知らせるというのはまさに IoT 向きのテーマであり、また、普段は主にバイプレイヤーである LED が一躍主役となるちょっと面白いケースでもあります。装置は「Wi-Fi 機能つきの小さなマイコン」としてこのところ何かと重宝している ESP8266 モジュールをメインに構成すれば手近な素材のみでも USB ポートへ直挿し可能なサイズに収めることが可能でしょう。装置へのメッセージング用のソリューションには今回 PubNub を選びました。

PubNub のこと / ESP8266 との関係

PubNub は Pub/Sub モデルのリアルタイムメッセージング機能を主軸とする BaaS です。 2010年のローンチ以来スマホや Web アプリ向けのプッシュ通知サービス等を展開、近年は IoT 方面にも注力しており対応言語・環境の広い有用なプラットフォームのひとつです。サポート対象は現在も拡大中、ちなみに無料枠もなかなかの太っ腹です。

また、GET リクエストベースの REST API セットが用意されており(HTTPS, HTTP)、これを利用すれば他のシステムやデバイスからも PubNub の所定の Channel へ柔軟にメッセージを送ることが可能です。 このように便宜の大きい PubNub ですが、この記事を書いている 2015年12月時点では実は肝心の ESP8266 がまだサポートされていません。でも幸い非公式のクライアントライブラリが公開されていることを知りました。本家の GitHub リポジトリにも fork されていることから非公式ながらも公認の位置づけのようです。 上の記事のとおりこの非公式ライブラリは任意のタイミングでの双方向通信には対応していない(subscribe() 呼び出し後の受信完了前には publish() 不可)ものの、今回想定している装置はもっぱら受信側であるため特に支障はなさそうです。作者の Kurt Clothier さん自身が「can be adjusted for a particular need」と書いているようにライブラリコードは用途に応じ適宜手を加えて利用すれば良いでしょう。
なお、このライブラリは Espressif 社による純正の ESP8266 SDK 向けに記述されています。より扱いやすい Arduino IDE for ESP8266 環境でも利用できるように併せて手を加えることにしました。

試作の動作の様子 (動画:1分11秒 環境音・操作音あり)

下の動画は、作成した装置へ PubNub の Web コンソールからメッセージを送り動作を確認している様子です。各メッセージはそれぞれ一行の REST API 記述に置き換えることができます。     

手元での現在の使途

手元ではこの試作を下記の要領で使用中。「光による通知」が想像以上に便利で実用的であることを実感しています。スマートさや表現性は blink(1) に及ばないものの必要になれば好きなようにいじれることが手作りの利点ですね。

  • 黄点灯:明日の予報が雨の場合
    (IFTTT レシピを利用)

         クリックで可読大表示
    ありがちなテーマだが実際に使ってみると手放せなくなる。好天続きで何となく油断している折に不意に点灯を目にしたら直ちに予報をチェック。

  • 青点滅:高齢者世帯の安否確認装置に反応があった場合
    Parse.com サーバサイドの Cloud Code を利用)

         クリックで可読大表示
    人を検知すると最短 30分間隔で Parse.com へレポートを送出する自作装置を実家で稼働中。対向するサーバ側コードに試作への通知処理を追加。光れば安心、メールによる通知よりも即時性が高く煩雑感が小さい。

  • 赤点滅:特定のユーザからのツイートがあった場合
    (IFTTT レシピを利用)

          クリックで可読大表示

    件数は少ないがいつも非常に有用な情報を提供してくれる某氏のツイートを見落とさないために。

  • 緑点滅:所定のブログが更新された場合
    (IFTTT レシピを利用)

          クリックで可読大表示

    とりあえず昨年末 「KLab Advent Calendar 2015」のチェック用に使ってみたらとても快適。

  • 管理用 HTML フォーム

          クリックで可読大表示

    PC・スマホの Web ブラウザから装置へメッセージを手早く簡単に送るために用意。 [ソース]

装置について

外観と構成

    

       クリックで可読大表示

材料

メモ

  • blink(1) は PC など対応機器の機能を利用するが今回の装置はネットワーク専用であり通信機能を内蔵しているため USB AC アダプタでも使える
  • USB 端子は規格上両端の2本が 5V (500mA) OUT と GND
        
  • フルカラー LED ではなく赤・緑・青・黄の 4つの単色 LED を使用。LED 用の抵抗は現物の発色をみながら「赤 75Ω 緑 75Ω 青 470Ω 黄 390Ω」に
  • LED の表示は各色「点灯・点滅」の二種類で合計 8パターン。これは手元での実利用を想定し妥当と判断した数
    (複数 LED の点滅時は明滅のタイミングが重ならないようプログラム側で管理)
  • 輪ゴムは USB コネクタ外抜け防止用
  • エアキャップはタッパ蓋の下に中敷き。LED 光拡散、外観柔和化、USB コネクタとタクトスイッチの圧迫用
  • タッパ蓋の上からタクトスイッチを押すと装置がリセットされる。電源接続時と LED 消灯用に使う

プログラムについて

ESP8266 用 PubNub ライブラリ

前述のように現時点では PubNub は ESP8266 を正式にサポートしておらず Espressif 社純正 SDK 向けの非公式ライブラリが公開されているのみです。これに以下の要領で手を加えて使用しています。

  • Arduino IDE で利用できるように
  • publish() の完了通知用にコールバックを登録可能に
  • HTTPS 対応

以下にソースコードを掲載します。ライブラリ「pubnub-esp8266」はファイル一式をローカルの Arduino ライブラリフォルダ下の pubnub-esp8266/ 下へコピーして使います。

(キーワード "DSAS" 部分がオリジナルからの変更箇所)

pubnub.h - GitHub

pubnub.cpp - GitHub

Arduino スケッチ

アプリ側のコードです。Arduino の SimpleTimer ライブラリを併用しています。

LEDMessage.ino - GitHub

作るのは簡単、アイディア次第でとても便利に使えます。


(tanabe)
klab_gijutsu2 at 00:41
この記事のURLComments(0)TrackBack(0)IoT | cloud
2015年12月25日

その先の世界へ

はてなブックマークに登録

このエントリーは、KLab Advent Calendar 2015 の12/25の記事です。

最終日なので緊張・・・はさすがにしません(笑

はじめに

KLabがゲーム事業に参入して今年で6年になります。 当時生まれた赤ちゃんが来年小学校へ入学する年月ですね、時が経つのは早いものです。

年末の締めの記事ということで、今年を振り返るような内容にしようかともおもいましたが、 2009年から現在までを技術者(CTO)の視点でざっくりと振り返ってみようとおもいます。

2009年

mixiアプリで「恋してキャバ嬢」をリリースしました。 恋キャバはおかげさまで今でも多くの方々に楽しんで頂いてますが、リリース当初は大量のリクエストを処理しきれずにサーバがダウンしてしまい、新規のユーザ登録を停止せざる得ない状況にまで陥ってしまったことがありました。(当時のユーザのみなさんほんとごめんなさい)

これまで経験したことのない膨大なトラフィックにあたふたしながらも、ボトルネックの調査方法や アプリケーションの軽量化、MySQLのチューニングテクニックなど、ソーシャルゲームの開発と運用に必要な様々なノウハウを習得することができました。

この時の経験は翌年にリリースした「DSAS Hosting for Social」に活かされており、現在でもサーバサイドの負荷対策のベースとなっています。


年始年末も休まずに深夜まで負荷対策に明け暮れていたのは今となっては良い思い出です。

この頃はつらかったけど楽しかったなあ・・・

2010年〜2011年

フィーチャーフォン向けのブラウザアプリ全盛期で、比較的短期間に数多くのタイトルをリリースしました。

技術要素としてはサーバサイドでの軽量な画像合成機能や動的なFlash生成技術などが必要とされ、これらを実現するためにKGDFlamixerといったツールが生まれました。

この頃はすべての案件がPHPで開発しており、フレームワークは symfony を独自拡張(mixi/Mobage/GREEのAPI呼び出しを隠蔽)したものを利用していました。 このフレームワークのおかげで、アプリケーションはプラットフォームの差異を気にする必要がなく、ひとつのアプリを簡単に複数のプラットフォームへ展開することができていました。

また、当時社内でよく聞いていた単語で印象に残っているのが「ヨコテン」とか「ガワガエ」といったものです。 基本的なゲームの骨子が似たものが多かったため、以前作ったゲームをベースにして低コストで新しいゲームを作りたくなりますが、新しいゲームには新しいアイデアやゲームシステムが追加されることが当たり前なので、斬新で面白いゲームを目指すほど古いコードやデータ構造が足かせになってしまい、逆に開発期間が伸びてしまうようなケースもあったように思います。

汎用性や再利用性を重視して設計したとしても、それが必ずしも未来において役に立つ保証はないので、手間ひまかけて時間を書けて設計・実装するより、短期間で開発できることを重視したほうがいいのかな、、とか思いつつ、かといってあまり設計で手を抜きすぎると機能追加や不具合改修時に支障をきたしてしまう恐れもあるので悩ましいところでした。

しかも当時は、多数の開発ラインが同時進行していたこともあり、綺麗に汎用化して実装できるほどの余裕がなかったのも事実で、結局のところ、開発メンバを信頼して「うまくやって」とお願いするしかなかったなあと振り返ってみたりしています。

現在では組織上に「共通基盤G」というグループがあり、全社的に共通化することが望ましい機能を切り出した上で、専任の開発ラインを組める体制ができています。


大晦日の23:30過ぎに大規模なDB障害が発生して元旦の夕方まで対応に追われたことは今となっては良い思い出です。

この頃もつらかったけど楽しかったなあ・・・

2012年〜2013年

スマートフォンの普及に伴い、スマホゲームの開発に着手したのがこの頃です。

最初はどうやって作ればいいのかさっぱりわからなかったので、アプリ開発の経験がある技術者を中心に開発体制を組み、JavaとObjective-Cで開発してみました。

これがまた想像以上に大変でして、開発を担当した技術者からは「この手法でゲームを開発するのは二度とやらないほうがいいよ」との声が上がってきました。Android版とiPhone版のソースコードが別管理になっているため、機能追加や動作検証に多大な手間と時間がかかる上、両方のプラットフォームに対応できるように技術者をアサインする必要があったのです。

では、今後はどうやって開発するのが良いのか考えてみたところ、

  • ゲームロジックをプラットフォームに依存しない共有言語で記述できる
  • AndroidやiPhone向けの処理に変換してビルドできる
こんなツールがあればいいよね、という話になり、実際に作ってみることにしました。 それがKLab製のゲームエンジン「Playground」です。

ちょうどこの時期に某案件で「リズムアクションゲームをつくりたい!」というニーズがでてきており、Unityの開発はノウハウがなかったですし、JavaとObjective-Cで作るのも避けたいということで、ゲーム開発と平行してPlaygroundの開発を本格的に進めることになりました。

このゲームの肝は「音楽をいかに遅延なく再生させるか」ということでした。通常の音楽プレイヤーであれば多少の再生遅延は許容できるかもしれませんが、このゲームではそうはいきません。また、より多くのユーザに遊んでもらえるよう、比較的低スペックな端末でも正しく動作することを目指して「軽量」と「高速」にとことんこだわりました。その反面、3Dを多用するようなリッチなゲームには向いていないので、そこはUnityにおまかせすることにしました。

それから、本格的にUnityを採用して開発するようになっていくわけですが、使ったことのないものを導入するには時間がかかるものです。とりあえずゲームを作ったとしても満足の行く品質に仕上がるかどうかはわからないといった心配もありました。

決してUnityが悪いとかそういう話ではなく、どんなツールも使いこなせなければ本来の性能は発揮できないので、品質の高いアプリを開発するには「Unityを正しく使いこなせるようになること」が必須の課題と考えていました。この課題を解消するため「Unity世話役」というチームを作ったりして様々な対策をしてきましたが、具体的に何をやったのかは、ひとことでは説明しきれないので今回は割愛させていただきます・・・

あと余談ですが、インフラでAWSを本格的に活用し始めたのもこの時期です。 海外向けのゲームをリリースする際に、日本のサーバを利用するのが怖かったというのが大きな理由でしたが、ものすごい勢いで押し寄せてくるクラウド化の波を無視しきれなくなってきたという側面もありました。


年越しで嫁の実家に親戚が集まった際、人目を気にせず某リズムアクションゲームをひたすらプレイしていて白い目で見られていた気がするのも今となっては良い思い出です。

いろんな意味でつらかったけど楽しかったなあ・・・

2014年〜2015年

スマホアプリが主流になっても、サーバサイドはWebサービスで培ってきた技術を利用し続けています。アプリとサーバはHTTPで通信してJSONでデータを送受信するのが主流となっており、テンプレートエンジンでHTML化しなくて済むのでサーバの処理は軽くなったと思います。また、ブラウザゲームでは画面遷移の度に通信が必要でしたが、アプリでは必要なときだけ通信すればよいのでHTTPのリクエスト数が大幅に削減できるようになりました。

また、去年から今年にかけてサーバのリプレースを進めており、1台あたりの性能が飛躍的に向上しています。2Uや4Uのスペースが必要だったDBサーバも1Uで収容できるようになって集積度が向上しましたし、SSDの導入によって以前とは比べ物にならないI/O性能を実現できるようになりました。

負荷対策に追われていた日々が嘘のようですね・・・

サーバサイドの技術はこのまま安息の日々を送るのかな、、などと思っていたらあらたな課題が浮上してきました。 チャットや共闘機能などの常時接続型のサービスです。

HTTPでもWebSocketを使えば常時接続型のサービスは可能ですが、バックエンドのPHPをどうにかしなくてはいけません。もしかするとPHP+WebSocketも可能かもしれませんが、ここであえてPHPにこだわる必要はないですよね。

おそらくMMOなどを運用されているような老舗のゲーム会社さんは、長年をかけてシステムを開発され、品質向上に取り組んでこられて、十分なノウハウをお持ちだと思いますが、当社のようにWebサービスを主体としてきた会社にとっては全く未知の世界です。

WebサービスではApacheやnginxなどのWebサーバに依存できるので、アプリケーションは「稼働し続けること」を意識する必要はありません。リクエストに対するレスポンスを返してしまえば終了してよいのですから。

しかし、常時接続型のサービスでは、自分自身が稼働し続けなければいけないわけで、安定稼働できるサーバを開発するとなると、技術的にまかせられる人はいるものの、品質を担保しながらチームで開発して運用した実績はありませんでした。

まあ、実績がないなら作ればいいわけで、やってみないとわからないならやってみればいいんですね。どんな手段で実現するかを開発者のみなさんにおまかせしたところ、

  • Python + tornado で書く
  • Go言語で書く
  • 外部のサービス(PhotonCloud)を利用する
これらの手法が選択されてリリースされています。 特に社内ではGo言語に対するモチベーションが高く、最近ではインフラの運用スクリプトなどもGoで書かれていたりします。

現状は試行錯誤を続けながら開発運用実績を積んでいき、KLabとしての最適解を選択できればよいかなと考えている次第です。


昨年の年越しは古いDBサーバを利用している案件が年始年末イベントの負荷に耐え切れず、障害を発生させてしまいましたが、今年はSSDに入れ替えたので大丈夫だと信じています。

これもきっと来年には良い思い出になることでしょう。

まとめ

辛いこともあるけれど、私はこの仕事が大好きです。落ち込んだりもしたけれど、私は元気です。

来年もよろしくおねがいします。


klab_gijutsu2 at 18:10
この記事のURLComments(0)TrackBack(0)
2015年12月03日

Raspberry Pi 2 で PXE boot してみる

はてなブックマークに登録

このエントリーは、KLab Advent Calendar 2015 の12/3の記事です。

KLabとしては久々のAdvent Calendar参戦です。3番手も緊張しますね。全国行脚の旅に出ている pandax381 です。よろしくお願いします。

はじめに

今日は大阪〜岡山の移動で念願のエヴァ新幹線に搭乗してきました。

IMG_1563
IMG_4729
IMG_1751

さて、本題。

RPi2 で遊んでいてネットワークブートしたい衝動に刈られ「PXEブートできると嬉しいけど、あれってIntel NIC 以外でもできるんだっけ?」とか「そもそも ARM だけど SYSLINUX 対応してるの?」とか思いながら調べていたら U-Boot が PXE クライアント機能を持っているらしいということが分かったので実際に PXE Boot 環境を構築してみました。

出回っている情報が古かったり、ドキュメント読んでも簡単に見つけられない事などが多かったので、備忘録も兼ねて公開することにしました。

準備

Raspbian をインストールした RPi2 を使いますので、公式サイトから Raspbian のイメージをダウンロードして microsd に書き込みます。

https://www.raspberrypi.org/downloads/raspbian/

最新版の 2015-11-21-raspbian-jessie-lite.img を dd コマンドで microsd に書き込みます

$ sudo dd if=~/Downloads/2015-11-21-raspbian-jessie-lite.img of=/dev/disk2 bs=1m
1391+0 records in
1391+0 records out
1458569216 bytes transferred in 166.354200 secs (8767853 bytes/sec)

多少のバージョンの違いは気にしなくても大丈夫です。

U-Boot のインストール

U-Boot(http://www.denx.de/wiki/U-Boot)は、様々なプラットフォームに対応したブートローダで、RPi2もサポートされています。U-Boot には PXE クライアントの機能があるため、これを利用して RPi2 で PXE ブートを試みます。

U-Boot をビルドするために git と bc が必要になるので、インストールされていなければ先にインストールしておきます。

$ sudo apt-get install git bc

まず、U-Boot の git リポジトリをクローンしてソースコードを入手します。最新のリリースは「v2016.01-rc1」というタグが付いているので、ビルド用のブランチを作ってそこにチェックアウトします。

$ git clone git://git.denx.de/u-boot.git
$ cd u-boot
$ git checkout -b build v2016.01-rc1

RPi2 用のコンフィグ「rpi_2_defconfig」が用意されているのでこれを使って make します。make all を実行すると u-boot.bin が生成されるので、これを /boot 直下へコピーします。

$ make rpi_2_defconfig
$ make all
$ sudo cp u-boot.bin /boot/

続いては RPi2 のファームウェアの設定です。組み込みのブートローダから実行される RPi2 のファームウェアは、次に実行するプログラムとして Linux Kernel をロードするようになっていますが、この動作はファームウェアの設定ファイルにて変更することが出来ます。RPi2 のファームウェアが Linux Kernel の代わりに u-boot.bin を実行するように、/boot/config.txt に以下の設定を追加します。

kernel=u-boot.bin

これで U-Boot のインストールは完了です。この状態で RPi2 をリブートすると、Linux ではなく U-Boot が起動するようになりました...が、次の手順を踏むまでまだ再起動はしないでください。

U-Boot を使って microSD からブートする

U-Boot が起動しても、U-Boot のコマンドを実行しなければ Linux をブートすることはできません。スクリプトファイルを作成しておくと、U-Boot が起動時にスクリプトファイルを読み込んで自動実行してくれる機能があるので、これを利用します。まず、以下の内容で boot.txt を作成します。

mmc dev 0
setenv bootargs "earlyprintk console=ttyAMA0,115200 root=/dev/mmcblk0p2 rootfstype=ext4 elevator=deadline rootwait"
fatload mmc 0:1 ${kernel_addr_r} kernel7.img
fatload mmc 0:1 ${fdt_addr_r} bcm2709-rpi-2-b.dtb
bootz ${kernel_addr_r} - ${fdt_addr_r}

boot.txt を U-Boot が読み込めるイメージ形式に変換するために mkimage コマンドを実行します。最終的な出力ファイルは /boot/boot.scr で、U-Boot は起動時にこのファイルが存在するかどうかチェックしています。

$ sudo ~/u-boot/tools/mkimage -A arm -O linux -T script -C none -a 0 -e 0 -n "MMC Boot" -d boot.txt /boot/boot.scr

ここまで出来たら、一度 RPi2 をリブートさせます。

$ sudo reboot

はじめに U-Boot が起動し、自動的に boot.scr スクリプトが実行されて Linux Kernel が起動します。RPi2 のファームウェアと Linux Kernel の間に U-Boot が追加されましたが、これまでと同じように Linux が起動しているはずです。

U-Boot 2016.01-rc1 (Dec 01 2015 - 19:08:12 +0900)

DRAM:  944 MiB
WARNING: Caches not enabled
RPI 2 Model B
MMC:   bcm2835_sdhci: 0
reading uboot.env

** Unable to read "uboot.env" from mmc0:1 **
Using default environment

In:    serial
Out:   lcd
Err:   lcd
Net:   Net Initialization Skipped
No ethernet found.
Hit any key to stop autoboot:  0
switch to partitions #0, OK
mmc0 is current device
Scanning mmc 0:1...
Found U-Boot script /boot.scr
reading /boot.scr
358 bytes read in 46 ms (6.8 KiB/s)
## Executing script at 02000000
switch to partitions #0, OK
mmc0 is current device
reading kernel7.img
4035396 bytes read in 5894 ms (668 KiB/s)
reading bcm2709-rpi-2-b.dtb
11113 bytes read in 54 ms (200.2 KiB/s)
Kernel image @ 0x1000000 [ 0x000000 - 0x3d9250 ]
## Flattened Device Tree blob at 00000100
   Booting using the fdt blob at 0x000100
   Loading Device Tree to 3ab42000, end 3ab47b68 ... OK

Starting kernel ...

Uncompressing Linux... done, booting the kernel.
[    0.000000] Booting Linux on physical CPU 0xf00
[    0.000000] Initializing cgroup subsys cpuset
[    0.000000] Initializing cgroup subsys cpu
[    0.000000] Initializing cgroup subsys cpuacct

...

Raspbian GNU/Linux 8 raspberrypi ttyAMA0

raspberrypi login: 

U-Boot を使って PXE でネットワークブートする

U-Boot が正常に動作することが確認できたので、本題の PXE boot の設定をします。まず、先ほど作成した boot.scr を boot_mmc.scr にリネームして退避させておきます。

$ sudo mv /boot/boot.scr /boot/boot_mmc.scr

ここから PXE でネットワークブートするための設定をしていきますが、失敗した場合など再び microSD からブートしたくなるかもしれません。その場合には、U-Boot が起動して「Hit any key to stop autoboot:」というメッセージが表示されたら何かキーを押すとコマンド入力できるようになります。その状態で以下のコマンドを入力すると、boot_mmc.scr を使って先ほどと同様に microSD からブートできます。

U-Boot> load mmc 0:1 0x2000000 boot_mmc.scr
U-Boot> source 0x2000000

PXE Boot 用に U-Boot のブートスクリプト boot.txt を作成します。

setenv autoload no
setenv bootcmd_pxe 'usb start; setenv ethaddr ${usbethaddr}; dhcp; if pxe get; then pxe boot; fi'
run bootcmd_pxe

先ほどと同様に mkimage コマンドで boot.scr を生成します。

$ sudo ~/u-boot/tools/mkimage -A arm -O linux -T script -C none -a 0 -e 0 -n "PXE Boot" -d boot.txt /boot/boot.scr

U-Boot の PXE クライアントが TFTP で取得する設定ファイルを作成します。設定ファイルの命名規則は SYSLINUX とほぼ同じなので http://www.syslinux.org/wiki/index.php/PXELINUX#Configuration_filename あたりを参考にしてください。

U-Boot の PXE クライアントは、pxeuuid が設定されていなければ、01-xx-xx-xx-xx-xx-xx というMACアドレスベースのファイルを一番はじめに取得します。したがって、pxelinux.cfg ディレクトリ配下にクライアント毎にMACアドレスベースの設定ファイルを作成しておけば大丈夫です。

SYSLINUX(PXELINUX)のコンフィグと雰囲気は似ていますが、若干書式が異なります。smsc95xx.macaddr に U-Boot の環境変数を使って MAC アドレスを渡していますが、これを指定し忘れると Kernel が実際の MAC アドレスを得られず自動生成されたものが使われてしまうので注意してください。

tftp-server$ cat /srv/tftpboot/pxelinux.cfg/01-xx-xx-xx-xx-xx-xx
menu title PXE boot menu
default mmc
prompt 1
timeout 30

label mmc
    menu label Linux raspberrypi 4.1.13-v7+ (mmc)
    kernel kernel7.img
    devicetree bcm2709-rpi-2-b.dtb
    append earlyprintk dwc_otg.lpm_enable=0 smsc95xx.macaddr=${usbethaddr} console=ttyAMA0,115200 root=/dev/mmcblk0p2 rootfstype=ext4 elevator=deadline rootwait

上記の設定は、Kernel と DeviceTree ファイルだけ TFTP で取得するものの rootfs は microSD 内にあるため、完全なネットワークブートではありませんが、U-Boot の PXE クライアント機能が正しく動作するか確認するために、この状態でリブートしてみます。

$ sudo reboot

PXE クライアントが pxelinux.cfg 配下の設定ファイルを取得し、続いて Kernel と DeviceTree ファイルを取得してブートし始めます。いずれかのファイルが取得できずにエラーとなるようであれば、TFTP サーバの設定やファイル名に誤りがある可能性が高いです。

U-Boot 2016.01-rc1 (Dec 01 2015 - 19:08:12 +0900)

DRAM:  944 MiB
WARNING: Caches not enabled
RPI 2 Model B
MMC:   bcm2835_sdhci: 0
reading uboot.env

** Unable to read "uboot.env" from mmc0:1 **
Using default environment

In:    serial
Out:   lcd
Err:   lcd
Net:   Net Initialization Skipped
No ethernet found.
Hit any key to stop autoboot:  0
switch to partitions #0, OK
mmc0 is current device
Scanning mmc 0:1...
Found U-Boot script /boot.scr
reading /boot.scr
205 bytes read in 47 ms (3.9 KiB/s)
## Executing script at 02000000
starting USB...
USB0:   Core Release: 2.80a
scanning bus 0 for devices... 3 USB Device(s) found
       scanning usb for storage devices... 0 Storage Device(s) found
       scanning usb for ethernet devices... 1 Ethernet Device(s) found
Waiting for Ethernet connection... done.
BOOTP broadcast 1
BOOTP broadcast 2
BOOTP broadcast 3
DHCP client bound to address 192.168.0.15 (1015 ms)
missing environment variable: pxeuuid
missing environment variable: bootfile
Retrieving file: pxelinux.cfg/01-xx-xx-xx-xx-xx-xx
Waiting for Ethernet connection... done.
Using sms0 device
TFTP from server 192.168.0.1; our IP address is 192.168.0.15
Filename 'pxelinux.cfg/01-xx-xx-xx-xx-xx-xx'.
Load address: 0x100000
Loading: #
         19.5 KiB/s
done 
Bytes transferred = 342 (156 hex)
Config file found
PXE boot menu
1:      Linux raspberrypi 4.1.13-v7+ (mmc)
Enter choice: 1:        Linux raspberrypi 4.1.13-v7+ (mmc)
missing environment variable: bootfile
Retrieving file: kernel7.img
Waiting for Ethernet connection... done.
Using sms0 device
TFTP from server 192.168.0.1; our IP address is 192.168.0.15
Filename 'kernel7.img'.
Load address: 0x1000000
Loading: #################################################################
         #################################################################
         #################################################################
         #################################################################
         #################################################################
         #################################################################
         #################################################################
         #################################################################
         #################################################################
         #################################################################
         #################################################################
         #################################################################
         #########
         326.2 KiB/s
done 
Bytes transferred = 4035396 (3d9344 hex)
append: earlyprintk dwc_otg.lpm_enable=0 smsc95xx.macaddr=xx:xx:xx:xx:xx:xx console=ttyAMA0,115200 root=/dev/mmcblk0p2 rootfstype=ext4 elevator=deadline rootwait
missing environment variable: bootfile
Retrieving file: bcm2709-rpi-2-b.dtb
Waiting for Ethernet connection... done.
Using sms0 device
TFTP from server 192.168.0.1; our IP address is 192.168.0.15
Filename 'bcm2709-rpi-2-b.dtb'.
Load address: 0x100
Loading: ###
         252 KiB/s
done 
Bytes transferred = 11113 (2b69 hex)
Kernel image @ 0x1000000 [ 0x000000 - 0x3d9250 ]
## Flattened Device Tree blob at 00000100
   Booting using the fdt blob at 0x000100
   Loading Device Tree to 3ab41000, end 3ab46b68 ... OK

Starting kernel ...

Uncompressing Linux... done, booting the kernel.
[    0.000000] Booting Linux on physical CPU 0xf00
[    0.000000] Initializing cgroup subsys cpuset
[    0.000000] Initializing cgroup subsys cpu
[    0.000000] Initializing cgroup subsys cpuacct

ブートできないケースとして、DHCP サーバの設定に next-server が設定されていない可能性もあります。PXEブートでは、DHCP の next-server で指定されたサーバに TFTP でファイルを取得しに行きます。そのため、DHCP サーバの設定には必ず next-server として TFTPサーバのアドレスを指定してください。なお、一般的な PXE 環境では filename に pxelinux.0 を指定しますが、これは不要です。pxelinux.0 は PXE クライアントプログラムですが、U-Boot には pxelinux.0 相当のクライアント機能が備わっているため、この設定は必要ありません。

initramfs を作って完全なネットワークブートにする

上記の例では rootfs が microsd にあるため、まだネットワークブートとは呼べない状態です。ここでは、U-Boot 以外の必要なファイルを全てネットワーク越しに取得し、ネットワークブートと呼べる状態にします。

手軽に確認できるよう、単純な initramfs を rootfs として利用します。initramfs は busybox があれば簡単につくれます。

$ sudo apt-get install busybox-static
$ sudo mkdir -p initramfs/{bin,dev,proc,sys}
$ sudo busybox --install -s initramfs/bin
$ sudo cp /bin/busybox initramfs/bin

initramfs では /init スクリプトが実行されるので、initramfs 直下に、init という名前のシェルスクリプトを作成します。

#!/bin/sh

export PATH="/bin"

mount -t devtmpfs devtmpfs /dev
mount -t proc proc /proc
mount -t sysfs sysfs /sys

sleep 3

exec setsid cttyhack sh

実行権限を忘れずに付与します。

$ sudo chmod +x initramfs/init

これを cpio でアーカイブすれば initramfs の出来上がりです。この initramfs.cpio.gz を Kernel や DeviceTree ファイルと同じように、TFTP サーバのドキュメントルート直下にコピーしておきます。

$ cd rootfs
$ sudo find . | cpio -o -H newc | gzip -c > ../initramfs.cpio.gz

PXE クライアントの設定ファイルに initrafms を使ってブートする設定を追加します。initramfs を使っていても initrd として記述します。

tfpt-server$ cat /srv/tftpboot/pxelinux.cfg/01-xx-xx-xx-xx-xx-xx
menu title PXE boot menu
default mmc
prompt 1
timeout 30

label mmc
    menu label Linux raspberrypi 4.1.13-v7+ (mmc)
    kernel kernel7.img
    devicetree bcm2709-rpi-2-b.dtb
    append earlyprintk dwc_otg.lpm_enable=0 smsc95xx.macaddr=${usbethaddr} console=ttyAMA0,115200 root=/dev/mmcblk0p2 rootfstype=ext4 elevator=deadline rootwait

label initramfs
    menu label Linux raspberrypi 4.1.13-v7+ (initramfs)
    kernel kernel7.img
    devicetree bcm2709-rpi-2-b.dtb
    initrd initramfs.cpio.gz
    append earlyprintk dwc_otg.lpm_enable=0 smsc95xx.macaddr=${usbethaddr} console=ttyAMA0,115200 elevator=deadline rootwait

RPi2 をリブートして動作確認します。

$ sudo reboot

今回は、PXEクライアントの設定ファイルに2つのエントリが存在するため、どちらの設定で起動するか選択することになります。なにも選択せずにいるとデフォルトの「1」が選ばれるので、素早く「2」を入力します。initramfs が正常に機能していれば、ブート後に busybox の ash が起動するはずです。

U-Boot 2016.01-rc1 (Dec 01 2015 - 19:08:12 +0900)

...

PXE boot menu
1:      Linux raspberrypi 4.1.13-v7+ (mmc)
2:      Linux raspberrypi 4.1.13-v7+ (initramfs)
Enter choice: 2

...

BusyBox v1.22.1 (Raspbian 1:1.22.0-9+deb8u1) built-in shell (ash)
Enter 'help' for a list of built-in commands.

/ # 

ここでは単純な initramfs だけで完結する例を示しましたが、initramfs の中から HTTP などで rootfs をダウンロードし swich_root するようなコードを書けば、しっかりとした実用的な PXE Boot のシステムが構築できます。

おわりに

明日は hasi_t さんです、お楽しみに。


klab_gijutsu2 at 19:06
この記事のURLComments(0)TrackBack(0)
このブログについて
DSASとは、KLab が構築し運用しているコンテンツサービス用のLinuxベースのインフラです。現在7ヶ所のデータセンタにて構築し、運用していますが、我々はDSASをより使いやすく、より安全に、そしてより省力で運用できることを目指して、日々改良に勤しんでいます。
このブログでは、そんな DSAS で使っている技術の紹介や、実験してみた結果の報告、トラブルに巻き込まれた時の経験談など、広く深く、色々な話題を織りまぜて紹介していきたいと思います。
KLabについて
KLab株式会社は、信頼性の高いクラウドサービス、ソフトウェアパッケージ、自社で企画・開発したソーシャルアプリやデジタルコンテンツを提供しています。
Blog内検索
最新コメント
最新トラックバック
Archives
Syndicate this site