8月28日から30日まで開かれる YAPC::Asia Tokyo 2014 で Dockerの話をします。

コマは2日目、30日の朝イチの「多目的教室3」です

Dockerで遊んでみよっかー - YAPC::Asia Tokyo 2014

各所で盛り上がりに盛り上がっているDockerを試して、遊んで、どんなものなのか理解していこうという内容です。Hello Worldで終わるのではなく、身近な作業に活用したり、ちょっとしたアプリケーションの実行環境として使ってみようという内容です。

資料できてきていますが、やや基本の部分のボリュームが大きくなっています。Dockerに興味がある、でもまだ手を付けてないような方がいましたら、聞きに来てくれると嬉しいです。

前日HUBで飲み過ぎないでね!

<おまけ>

Dockerとは関係ありませんが、ISUCON4 オンライン予選の参加登録が開始されています!!!Webアプリケーションを書いている方もインフラを扱っているエンジニアも運用エンジニアも、ぜひチャレンジしてください!!私もでます!!

参加はこちらから↓↓↓↓

ISUCON4 オンライン予選の参加登録を開始しました

昨日のエントリで紹介した「Webアプリケーションの パフォーマンス向上のコツ 実践編」ですが、いくつかスライドを追加して、「完全版」として公開しました。

ISUCONだけに限らず、一般的なWebアプリケーション、SQLのチューニングの参考となる資料となっていると思いますので、見て頂けたら嬉しいです。

<追記>

ISUCON4 オンライン予選の参加登録が開始されています!!!Webアプリケーションを書いている方もインフラを扱っているエンジニアも運用エンジニアも、ぜひチャレンジしてください!!私もでます!!

参加はこちらから↓↓↓↓

ISUCON4 オンライン予選の参加登録を開始しました

学生さん限定のイベント「ISUCON 夏期講習」今年もやりました。イベントは、tagomorisからISUCONやWebアプリケーションについての座学を行ったあとに、ISUCON3の予選問題にチャレンジをしてもらいました。またチャレンジをしてもらいながら、どのようにWebアプリケーションのパフォーマンスをあげていったらよいのか、自分の方から説明をしました。今年はスコアをあげることができる参加者が多く、驚きました。

去年と同じようにISUCONの問題にチャレンジする環境としてEC2を使いましたので、そのAMIを公開します。

AMI ID:ami-e796b3e6
AMI Name: isucon_summer_class_2014
Region: Asia Pacific (Tokyo)

「ISUCON 夏期講習 2014」サーバのつくりかた

EC2のインスタンスを起動する際に、上記のAMIを指定して起動してください。HVM対応のインスタンスが必要になります。夏期講習では「c3.xlarge」を使いました。スポットインスタンスを活用するとリーズナブルに試す事ができると思われます。

ベンチマークツールにポート5043が必要になるので、ssh、http、tcpの5043をセキュリティグループの設定をつかってアクセス可能にする必要があります。

サーバが起動したら、「ec2-user」でログインしてください。

$ ssh -i keyfile.pem ec2-....amazonaws.com

ISUCON2の環境は「isu-user」ユーザにて構築してあります。

$ sudo su - isu-user

パスワードが設定されてないので、適宜設定するなりsshの公開鍵を設置すると作業がしやすいですね。

ベンチマークツールはWebUIを用意してあります。ブラウザでサーバのtcp 5043にアクセスします

http://ec2-....amazonaws.com:5043/

benchui.png

こんな画面になります。説明が下の方にありますので、読んでください。

ベンチマークツールですが、去年の予選とは若干異なり、時間は30秒。workloadは「1」に固定されています。よりチャレンジしたい場合はソースコード見て変更してみてください。

「Webアプリケーションの パフォーマンス向上のコツ」の発表資料

夏期講習の座学用に用意した資料となります。Webアプリケーションのパフォーマンスをあげるにはどんなことを考えて、やればいいのかについて簡単に解説しています。資料は2つに分かれていて、一つ目が「概要編」、二つ目が「実践編」となっています。

実践編は途中で力尽きている感じですが、ちゃんと書けば仕事でも役に立ちそうなので、あとで完全版にしたいと思っております。

<追記>
完全版公開しました
「ISUCONで学ぶ Webアプリケーションのパフォーマンス向上のコツ 実践編 完全版」を公開しました
</追記>

学生のみなさまのISUCON4への参加と活躍を楽しみにしております。

追記(2014/08/22)と謝辞

実践編の資料を作るにあたり、@acidlemon さんの記事を参考にしました
ざっくりと #isucon 2013年予選問題の解き方教えます

サーバ環境の構築にあたっては「オンライン予選で使用した問題が手元で再現できるAMIを公開しました」で紹介されているAMIも参考にしています。

改めて出題の @fujiwaraさん @acidlemonさんをはじめKAYACの皆様に感謝しております。

追記2

ISUCON4 オンライン予選の参加登録が開始されています!!!Webアプリケーションを書いている方もインフラを扱っているエンジニアも運用エンジニアも、ぜひチャレンジしてください!!私もでます!!

参加はこちらから↓↓↓↓

ISUCON4 オンライン予選の参加登録を開始しました

最近、Module::CoreListのWebUIと、perl-buildで使っているperlのリリースとアーカイブのリストの生成をHerokuに移動しました。両方とも、情報を更新するworkerとappサーバが必要になるので、Heroku上でProcletを使ってappサーバとworkerなどを動かしてみました。

HerokuでのProcletの使い方

まず、cpanfileとProcfileを用意します。

$ cat cpanfile
requires 'HTTP::Tiny','0.043';
requires 'Getopt::Long';
requires 'Proclet';
requires 'Plack';
requires 'Starlet';

$ cat Procfile
web: ./server.pl --port $PORT

Procfileに書くのは1つだけです。2つ以上起動する場合は、別途お金がかかります。

server.plはこんな感じで、オプションでportを受け取り、workerとStarletのプロセスを起動します。

#!/usr/bin/env perl

use strict;
use warnings;
use FindBin;
use lib "$FindBin::Bin/lib";
use Proclet;
use Plack::Loader;
use Getopt::Long;
use HTTP::Tiny;

my $port = 5000;
Getopt::Long::Configure ("no_ignore_case");
GetOptions(
    "p|port=s" => \$port,
);

$proclet->service(
    tag => 'worker',
    code => sub {
        my $worker = MyWorker->new;
        $worker->run
    },
);

my $app = MyWeb->to_psgi;
$proclet->service(
    code => sub {
        my $loader = Plack::Loader->load(
            'Starlet',
            port => $port,
            host => 0,
            max_workers => 5,
        );
        $loader->run($app);
    },
    tag => 'web',
);

$proclet->run;

場合によっては定期的にサービスを叩くworkerも合った方がいいかもしれません

$proclet->service(
    every => '*/30 * * * *',
    tag => 'ping',
    code => sub {
        my $ua = HTTP::Tiny->new;
        $ua->get("http://${yourservicename}.herokuapp.com/");
    }
);

Procletはcron likeなworkerもサポートしています。

server.plは実行権限も付けておきます

$ chmod +x server.pl

準備ができたら、herokuコマンドを使ってアプリケーションを登録します。

$ heroku create yourappname --buildpack https://github.com/kazeburo/heroku-buildpack-perl-procfile.git

拙作のbuildpackを使うと、cpanfileを使って依存モジュールをいれ、環境変数を設定してくれます。

最後にpushしてサービスを起動します。

$ git push heroku master
...
-----> Heroku receiving push
-----> Fetching custom buildpack
-----> Perl/Procfile app detected
-----> Installing dependencies

便利ですね!

タイトルがそのままですが、GrowthForecastのDocker imageを作りました。

https://registry.hub.docker.com/u/kazeburo/growthforecast/

使い方は単純に起動するだけなら次のようになります。

$ docker run -p 5125:5125 kazeburo/growthforecast

これだと、データが永続化されないので、適当なボリュームをマウントします。

$ docker run -p 5125:5125 -v /host/data:/var/lib/growthforecast kazeburo/growthforecast

起動オプションを変更したい場合は、コマンドを渡すか、Dockerfileを書いてビルドすると良いでしょう。

$ docker run -p 5125:5125 -v /host/data:/var/lib/growthforecast kazeburo/growthforecast \
    growthforecast.pl --time-zone UTC --data-dir /var/lib/growthforecast

ヘルプの表示

$ docker run -i kazeburo/growthforecast growthforecast.pl -h

Dockerfileの例

FROM kazeburo/growthforecast CMD growthforecast.pl —time-zone Asia/Tokyo —data-dir /var/lib/growthforecast —front-proxy 0.0.0.0/0 ..

GitHubとDocker Hubを組み合わせてDocker imageの作成を自動化

このDocker imageはGitHubとDocker HubのAutomated Build Repositoryの機能を組み合わせて、GitHub上でタグが作れたのをフックして自動的に作られています。

Automated Build Repositoryを使うには、まずDocker Hub側で、ビルドを開始するためのTrigger URLを有効にします。

gf_gh_trigger.png

そして、このURLをGitHubに入力します。新規のwebhookを追加し、Payloadに先ほどのURLをいれます。Secretは使わないので適当に入れました。今回はタグが作成された際にDocker Imageを作りたいので、Webhookを起動するイベントの「individual events」のラジオボタンを押し、出てきた中から、「Create」だけにチェックボックスをいれました。

gf_hook_gh.png

あと、Dockerfileで最新のタグを指定してソースコードを取得し、インストールを行うようにしています。

RUN git clone -b $(curl -s https://api.github.com/repos/kazeburo/GrowthForecast/tags|jq -r '.[0].name') https://github.com/kazeburo/GrowthForecast.git /tmp/GrowthForecast
RUN cpanm -n --no-man-pages -v --no-interactive /tmp/GrowthForecast

GrowthForecast(や他のCPANモジュール)は、MinillaShipItというツールで、タグの追加とCPANへのリリースを同時に行っていますが、上の例でGrowthForecastのインストールをCPANから行わずにgithubから取得しているのは、DockerのAutomated Buildがgit pushした瞬間から開始されて、CPANのインデックスの更新が追いつかずdocker build中に最新版が取得できない可能性があるからです。

どうぞご利用くださいませ。

最近、Vagrantのprovisionerを使ってパッケージの作成などをいくつか行っているのですが、その際にVMを落とし忘れ、ホストしてるマシンの余計なリソースを使ってしまっていることがあります。

なので、provisionerでサーバをdestroy/haltするやつを書いてみました。

rubygems: https://rubygems.org/gems/vagrant-destroy-provisioner
github: https://github.com/kazeburo/vagrant-destroy-provisioner

勝手にshutdownしてイメージを破棄するデモ動画です。

インストールはvagrant pluginコマンドから行います。

$ vagrant plugin install vagrant-destroy-provisioner

使い方はこんな感じ

VAGRANTFILE_API_VERSION = "2"
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
  config.vm.box = "CentOS-6.4-x86_64-v20131103"
  config.vm.box_url = "http://developer.nrel.gov/downloads/vagrant-boxes/CentOS-6.4-x86_64-v20131103.box"
  config.vm.provision "shell", inline: "echo 'Hello Provisioner'"
  config.vm.provision "destroy"
end

VMのshutdownだけをするモードも一応サポートしていて

config.vm.provision "destroy", destroy: false

とすると、vagrant halt相当のみ実行されます。destroyオプションのデフォルトはtrueです。

Docker Provisionerを使う場合は、destroy: falserun: "always" を使うと多分便利。先日のPerl-Buildのfatpackの例だと

 config.vm.provision "docker", run: "always" do |d|
    d.build_image "/perl-build/author",
        args: "-t perl-build"
end
config.vm.provision "shell", run: "always",
    inline: "docker run -v /perl-build:/perl-build perl-build"
config.vm.provision "destroy", destroy: false, run: "always"

となります。毎度 vagrant up に —provision オプションを付けなくても良いので楽。

vagrant-destroy-provisionerを使うと、VMの使い捨てが自動化されて非常に便利。どうぞご利用ください。

以下のgems、サイトを参考にしました。

https://rubygems.org/gems/vagrant-reload

vagrantのprovisionerを自作する - Qiita

plenvやxbuildで使っているperl-buildなのですが、ひろむ氏からコミット権限頂いてアップデートをしました。

変更点としてはいつでも使えるように search.cpan.org への依存度を減らしたことと、だれでも同じように作業ができるよう依存モジュールを1つのスクリプトにまとめるfatpackにDockerを導入して自動化した点です。

search.cpan.org への依存度の削減

perl-buildはperlのバージョンを引数に渡してインストールを行います。

$ perl-build 5.20.0 /opt/perl-5.20

この際に、渡されたperlのバージョンからアーカイブのパスを調べる必要があります。アーカイブのパスとは以下のようなものです

R/RJ/RJBS/perl-5.21.0.tar.gz
R/RJ/RJBS/perl-5.20.0.tar.gz
R/RJ/RJBS/perl-5.20.0-RC1.tar.gz
S/SH/SHAY/perl-5.19.11.tar.gz
A/AR/ARC/perl-5.19.10.tar.bz2
T/TO/TONYC/perl-5.19.9.tar.bz2

アーカイブはリリースを行ったCPAN Authorのディレクトリ以下に配置されます。perlのソースコードをダウンロードするにはそのバージョンのperlをリリースしたAuthorのPAUSE IDやパスをなにかしらの方法で調べる必要があります。

この一つの方法が CPAN::Perl::Releases モジュールです。CPAN::Perl::Releasesを使うとこんな感じでパスが取れます。

use CPAN::Perl::Releases qw/perl_tarballs/;
perl_tarballs( '5.14.0' );

Returns a hashref like:

{
  "tar.bz2" => "J/JE/JESSE/perl-5.14.0.tar.bz2",
  "tar.gz" => "J/JE/JESSE/perl-5.14.0.tar.gz"
}

CPAN::Perl::Releasesのソースコード中にベタにバージョンとPAUSE IDの対照表が書かれています。

perl-buildのスクリプトにはCPAN::Perl::Releasesが組み込まれており、最初にCPAN::Perl::Releasesを使ってパスを得ようとします。しかし、組み込まれているCPAN::Perl::Releasesはperl-buildスクリプトをアップデートしない限り、どんどん古くなって行くので、新しいバージョンがリリースされたときに追従出来ず、パスを探せなくなります。

そこでperl-buildは次に http://search.cpan.org/dist/perl-$version をスクレイピングし、パスを調べます。ところが最近、しばしばsearch.cpan.orgがダウンし、またそのダウンタイムも長くなっている(感覚値)ことから、perlをインストールしたい時にエラーでインストールができないということも起きています。

そこで、search.cpan.orgへの依存を減らすべく、こんなページを作りました。curlでアクセスするとこんな感じ。

$ curl -s http://perl-releases.s3-website-us-east-1.amazonaws.com/|head
5.21.0  R/RJ/RJBS/perl-5.21.0.tar.gz
5.20.0  R/RJ/RJBS/perl-5.20.0.tar.gz
5.20.0-RC1      R/RJ/RJBS/perl-5.20.0-RC1.tar.gz
5.19.11 S/SH/SHAY/perl-5.19.11.tar.gz
5.19.10 A/AR/ARC/perl-5.19.10.tar.bz2
5.19.9  T/TO/TONYC/perl-5.19.9.tar.bz2
5.19.8  R/RJ/RJBS/perl-5.19.8.tar.bz2

中身はtsvファイルで、バージョンとパスの組み合わせが得られるだけのページです。静的ファイルなのでs3でホストしています。tsvファイルを作るスクリプトは

https://github.com/kazeburo/perl-releases-list

にあげてあります。このスクリプトをcronで定期的に動かし、s3にアップロードしています。

新しいperl-buildはsearch.cpan.orgにアクセスする前に、このページをダウンロードしてパスを解決します。このページでも解決できない場合はsearch.cpan.orgへアクセスします。

さらに、perlのアーカイブをダウンロードする際のデフォルトのミラーがsearch.cpan.orgだったので、www.cpan.orgへ変更し、search.cpan.orgへの依存をぐっと減らしました。これでもし、search.cpan.orgが落ちていたとしてもperl-buildが使えるようになりました。

ちなみにミラーは環境変数で変更が可能です。

$ PERL_BUILD_CPAN_MIRROR=http://ftp.jaist.ac.jp/pub/CPAN/ perl-build 5.18.1 /opt/perl-5.18

国内ミラーの方がダウンロード速度は速くなるんじゃないかなぁと思います。

Dockerを使ったfatpack

githubに上がっているperl-buildは依存モジュール等を1つにまとめたスクリプトとなっています。これはApp::Fatpackerを使って生成したファイルとなっています。

fatpackしたスクリプトが古いperl 5.8でも動くようにするために、fatpackを行うperlも5.8を使います。以前はtokuhirom氏のローカル環境でやっていた思うのですが、だれでも簡単に再現できるようにした方がいいよねということで、Docker(+Vagrant)で自動化してみました。

Vagrantは1.63が必要です。実行は

$ cd author
$ vagrant up --provision
$ vagrant halt

Docker provisionerを使ってimageを作って実行します。Vagrantfileはこんな感じ

Vagrant.configure("2") do |config|
  config.vm.box = "hashicorp/precise64"
  config.vm.synced_folder "../", "/perl-build"
  config.vm.provision "docker" do |d|
    d.build_image "/perl-build/author",
      args: "-t perl-build"
  end
  config.vm.provision "shell",
    inline: "docker run -v /perl-build:/perl-build perl-build"
end

最後、shell provisionerを使ってdocker runしていますが、これはDocker provisionerにサポートされているrun機能を使うとrunが終了するまえにvagrant upコマンドが終了してしまう(runはバックグラウンドで実行されている)からです。shellで実行するとfatpackが完了するまでvagrant upが待ってくれるので分かりやすくなります。

Dockerfileはこんなの

FROM jmmills/plenv-base:latest
RUN plenv install 5.8.5
RUN plenv global 5.8.5
ENV PLENV_VERSION 5.8.5
RUN curl -L http://cpanmin.us/ | plenv exec perl - -n ExtUtils::MakeMaker@6.56
RUN curl -L http://cpanmin.us/ | plenv exec perl - -n App::cpanminus
RUN curl -L http://cpanmin.us/ | plenv exec perl - -n Perl::Strip App::FatPacker
RUN plenv rehash
CMD bash -l -c 'cd /perl-build; cpanm -n --installdeps . ; bash author/fatpack.sh'

あとは、作ったスクリプトのテストもDocker使ってできるといいなぁと思ってます。

どうぞご利用ください。

あるモジュールがPerlのコアモジュールに含まれているか、どのバージョンが含まれているかをたまに確認したくなりますが、その時に使うのが Modure::CoreList です。Modure::CoreListにはコマンドラインツールも用意されているのですが、tokuhiormが Web Interface版を作っていてとても便利でした。が、こちらは今404になってしまっているので、tokuhiromに確認の上、新しくサイトを動かしました。

http://corelist.rpee.be/

画面はこんな感じ

corelistweb.png

あるバージョンのperlにどのモジュールのどのバージョンが含まれているのかと、モジュールがどのPerlに含まれているのかのリストがでます。

corelistweb2.png

もとのソースコードを参考にしつつ、Kossyとboostrap3で移植しました。移植するついでに、Proclet、Server::Starter、cpanmを使い、無停止でModule::CoreListが自動でアップデートされるようにしてみました。コードはこんな感じ

my $proclet = Proclet->new;

$proclet->service(
    every => '3 * * * *',
    tag => 'cron',
    code => sub {
        open(my $fh, "<", "server.pid") or die "$@";
        my $pid = <$fh>;
        chomp $pid;
        my ($result,$exit_code) = capture(['cpanm','-lCoreList-lib','Module::CoreList']);
        if ( $exit_code == 0 && $result =~ m!Successfully installed Module-CoreList! ) {
            warn "KILLHUP server-starter ($pid)\n";
            kill 'HUP', $pid;
        }
    }
);

$proclet->service(
    code => sub {
        exec(qw!start_server --port!,$port,qw!--pid-file server.pid  --!,
             qw!plackup -Mlib=CoreList-lib/lib/perl5 -E production!,
             qw!-s Starlet --max-workers 10 -a  app.psgi!);
    },
    tag => 'web',
);

$proclet->run;

ニッチですがご利用ください

弊社のGrowthForecast、グラフ数が5000件近くになっていて、複合グラフ作成ページのプルダウンが多くなり杉でグラフを選ぶのが大変な状態だったので、プルダウンを3つにわけました

gf_complex.png

どうぞご利用ください

TCP_DEFER_ACCEPTは、LinuxでサポートされているTCPのオプションで、サーバ側で使用した場合にはaccept(2)からのブロック解除をTCP接続が完了したタイミングではなく最初のデータが到着したタイミングで行ってくれるオプションです。

Webサーバ・アプリケーションサーバではリクエストが到着してからaccept(2)のブロックを解除するので、リクエストの到着をWebサーバ・アプリケーションサーバで待つ必要がなくなり、特にprefork型のサーバでは効率的にプロセスを使えるようになるという利点があります。PerlではStarletがこの機能を有効にしています

ところが、某サービスでTCP_DEFER_ACCEPTが有効にも関わらず、accept後のreadでデータが読めず、最悪の場合、デフォルトのtimeoutである5分間プロセスがストールすることがありました。straceで捕まえられたのが以下。

21:13:28.727597 accept(4, {sa_family=AF_INET, sin_port=htons(50776), sin_addr=inet_addr("127.0.0.1")}, [6396800329316302864]) = 5
21:13:28.727749 ioctl(5, SNDCTL_TMR_TIMEBASE or TCGETS, 0x7fff39d54050) = -1 EINVAL (Invalid argument)
21:13:28.727834 lseek(5, 0, SEEK_CUR)   = -1 ESPIPE (Illegal seek)
21:13:28.727905 ioctl(5, SNDCTL_TMR_TIMEBASE or TCGETS, 0x7fff39d54050) = -1 EINVAL (Invalid argument)
21:13:28.727982 lseek(5, 0, SEEK_CUR)   = -1 ESPIPE (Illegal seek)
21:13:28.728051 fcntl(5, F_SETFD, FD_CLOEXEC) = 0
21:13:28.728148 fcntl(5, F_GETFL)       = 0x2 (flags O_RDWR)
21:13:28.728226 fcntl(5, F_SETFL, O_RDWR|O_NONBLOCK) = 0
21:13:28.728311 setsockopt(5, SOL_TCP, TCP_NODELAY, [1], 4) = 0
21:13:28.728451 read(5, 0x123dacc0, 131072) = -1 EAGAIN (Resource temporarily unavailable)
21:13:28.728557 gettimeofday({1398946408, 728589}, NULL) = 0
21:13:28.728649 select(8, [5], NULL, [5], {300, 0}) = 0 (Timeout)
21:18:28.732551 gettimeofday({1398946708, 732602}, NULL) = 0
21:18:28.732952 close(5)                = 0

21:13:28.728451にread(2)を試みてますが、データがまだ到着せずEAGAINとなり、select(2)でソケットが読めるようになるまで待ちますが、5分後タイムアウトとなり接続を切っていることがわかります。他にも数十秒まってからリクエストが届いたりするパターンもあり、レスポンスがかなり不安定な状況になっていました。

調べても、なかなか同じような事例はでてこなかったのですが、ひとつだけ中国語のサイトでみつけました。

http://bbs.csdn.net/topics/380151037

google翻訳して読むと、somaxconn(backlog)が溢れるとEAGAINが返り、somaxconnを増やして解決したそうです。

なるほどと思い、某サービスの当時の状況を振り返ると、Starletのworker数は10とかなり少なめ、backlogは標準の128で起動されており、ゴールデンウィーク前のキャンペーンでアクセスがかなり増加しているところで、アプリケーションから呼び出している外部のAPIの遅延が目立ってきている状態。workerプロセスは全てbusyとなりbacklogが不足してしまっていたと容易に考えられます。

現在はこのサーバはアプリケーションの改善を行うとともに、worker数、backlogともに増やして再発しにくいよう設定しています。

再現実験

CentOS6のサーバに、Starletを起動し、abにて負荷をかけることで再現させてみます。

サーバのsomaxconnは大きめに設定しています

$ sysctl net.core.somaxconn
net.core.somaxconn = 32768

念のためtcpmaxsyn_backlogも確認

$ sysctl net.ipv4.tcp_max_syn_backlog
net.ipv4.tcp_max_syn_backlog = 2048

syncookieはoffです。onでも同じ状況が再現しています。

$ sysctl -a|grep cookie
net.ipv4.tcp_syncookies = 1

Perlは5.18.2。StarletやServer::Starterのバージョンは以下。

  • Starlet 0.23
  • Server::Starter 0.17

アプリケーションは、1msecほどsleepしてレスポンスするようにしました。

$ cat app.psgi 
my $body = "Hello World\n";
sub {
    my $env = shift;
    select undef, undef, undef, 0.001;
    [200,["Content-Type"=>"text/html"],[$body]];
};

まず、workerを3つ、backlogを10にて起動します。start_serverコマンドは—backlogオプションでbacklogが指定できます。

$ start_server --port 5432 --backlog 10 -- carton exec -- plackup -s Starlet --workers=3 --max-reqs-per-child 50000 --min-reqs-per-child 40000 --E production -a app.psgi

別ターミナルからab

$ ab -c 1000 -n 10000 http://127.0.0.1:5432/

さらに、別ターミナルで適当なプロセスにstraceでattachしてEAGAINがでるか観察します

$ strace -tt -p $pid 2>&1 | tee /tmp/trace.txt | grep EAGAIN
10:48:28.188696 read(5, 0x7fcd284a9010, 131072) = -1 EAGAIN (Resource temporarily unavailable)
10:48:33.201731 read(5, 0x7fcd284a9010, 131072) = -1 EAGAIN (Resource temporarily unavailable)
10:48:35.391262 read(5, 0x7fcd284a9010, 131072) = -1 EAGAIN (Resource temporarily unavailable)
10:48:36.000677 read(5, 0x7fcd284a9010, 131072) = -1 EAGAIN (Resource temporarily unavailable)

何回かでました。前後をみると、

10:48:36.000442 accept(4, {sa_family=AF_INET, sin_port=htons(23509), sin_addr=inet_addr("127.0.0.1")}, [16]) = 5
10:48:36.000469 ioctl(5, SNDCTL_TMR_TIMEBASE or TCGETS, 0x7fff5c142940) = -1 EINVAL (Invalid argument)
10:48:36.000501 lseek(5, 0, SEEK_CUR)   = -1 ESPIPE (Illegal seek)
10:48:36.000522 ioctl(5, SNDCTL_TMR_TIMEBASE or TCGETS, 0x7fff5c142940) = -1 EINVAL (Invalid argument)
10:48:36.000544 lseek(5, 0, SEEK_CUR)   = -1 ESPIPE (Illegal seek)
10:48:36.000566 fcntl(5, F_SETFD, FD_CLOEXEC) = 0
10:48:36.000602 fcntl(5, F_GETFL)       = 0x2 (flags O_RDWR)
10:48:36.000623 fcntl(5, F_SETFL, O_RDWR|O_NONBLOCK) = 0
10:48:36.000645 setsockopt(5, SOL_TCP, TCP_NODELAY, [1], 4) = 0
10:48:36.000677 read(5, 0x7fcd284a9010, 131072) = -1 EAGAIN (Resource temporarily unavailable)
10:48:36.000709 select(8, [5], NULL, [5], {300, 0}) = 1 (in [5], left {294, 901422})
10:48:41.099372 read(5, "GET / HTTP/1.0\r\nHost: 127.0.0.1:"..., 131072) = 82
10:48:41.099448 select(0, NULL, NULL, NULL, {0, 1000}) = 0 (Timeout)
10:48:41.100603 write(5, "HTTP/1.1 200 OK\r\nDate: Thu, 08 M"..., 145) = 145
10:48:41.100678 close(5)                = 0

EAGAINのあと、selectで5秒ほど待ちました。

abの結果のレスポンス時間をみると、一部のアクセスがかなり遅いようです。7秒と言えばTCPの再送?

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0   30 341.9      0    7001
Processing:     1   18 153.7      8    6364
Waiting:        1   18 153.7      8    6364
Total:          4   48 406.6      8   10077

Percentage of the requests served within a certain time (ms)
  50%      8
  66%     13
  75%     13
  80%     13
  90%     17
  95%     18
  98%     32
  99%   1014
 100%  10077 (longest request)

次に、backlogを十分に大きな8192にて起動します。worker数はそのまま。

$ start_server --port 5432 --backlog 8192 -- carton exec -- plackup -s Starlet --workers=3 --max-reqs-per-child 50000 --min-reqs-per-child 40000 --E production -a app.psgi

再びabしながら別ターミナルで適当なプロセスにstraceでattachしてEAGAINがでるか観察しましたが、

$ strace -tt -p $pid 2>&1 | tee /tmp/trace.txt | grep EAGAIN

何回か試行した中ではEAGAINがでませんでした。

abの結果は以下。遅いですが、安定してレスポンスができています。

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    1   2.8      0      10
Processing:     8  384  70.9    401     416
Waiting:        8  384  70.9    401     416
Total:         18  385  68.5    401     416

Percentage of the requests served within a certain time (ms)
  50%    401
  66%    402
  75%    407
  80%    412
  90%    414
  95%    414
  98%    415
  99%    415
 100%    416 (longest request)

まとめると

backlogが不十分な場合TCP_DEFER_ACCEPTが有効でも、accept(2)後の最初のreadでリクエストが読めないことがある。最悪のケース、長い時間プロセスが占有され、サービスに影響がでることがある。対策としては十分なbacklogを確保することと、prefork型のサーバでは十分なworker数を起動するのも対策となるでしょう。

イベントドリブンなサーバはTCP_DEFER_ACCEPTを使わないのも一つの手ですが、prefork型のサーバではプロセスを占有してリクエストを待つコストが大きく、解決にはならないと思われます。

データが来ないままacceptのブロックが解除されてしまう理由はkernelのソースを読めばわかるのかなぁ。。

最近のコンテンツはインデックスページで見られます。過去に書かれたものはアーカイブのページで見られます。

アイテム

  • benchui.png
  • gf_hook_gh.png
  • gf_gh_trigger.png
  • corelistweb2.png
  • corelistweb.png
  • gf_complex.png
  • jstat2gf.png
  • filter-topn.png
  • load-avg.png
  • sum_series.png

ウェブページ

OpenID対応しています OpenIDについて
Powered by Movable Type 4.27-ja