2012年4月にでた「サーバ/インフラエンジニア養成読本 管理/監視編」の改訂新版が発売されました。技術評論社様、献本ありがとうございます。

表紙に「DevOps」の文字が入ったのが以前との違いですね。

単に、監視ツールやトラブルシューティングにつかうコマンドの使い方だけではなく、どのようなマインドで運用に取り組むべきなのかまで、広く記事が集められているので、この春からシステム運用に関わるようになった方もそうでない方も、ぜひ手に取って読んで頂けたらと思います。

先日のJVM Operations Casual Talks、GCやメモリ管理についてまとまった発表や、モニタリングの手法などの話が聞けてよい会でした。

微妙に意識が高まっているところで、メモリ使用量やGCの統計情報を取得して、GrowthForecastでグラフを作ってくれるスクリプトを書きました。それPla、それFluentd系のやつです

https://github.com/kazeburo/jstat2gf

この元ネタは JVM Operation Casual Talksでのモリスさんの発表にでてきたグラフです。あれを簡単に作れるツールになります。

某JVMのプロセスに対して実行すると、こんな感じのグラフになります。上から「NEW領域」「OLD領域」「Permament領域」「1秒あたりのFull GCの回数」「1秒あたりのFull GCにかかった時間」となっています。

jstat2gf.png

なんかFull GCがいっぱい実行されてますね!

デーモンではない普通のscriptなので、cronで実行します

PATH=/path/to/java/bin:/usr/bin
* * * * * perl /path/to/jstat2gf.pl --gf-uri=http://gf/ --gf-service=example --gf-section=jvm --gf-name-prefix=app001 --jvm-pid=$(pgrep -of 'process name')

今のところインストールはgit cloneとcpanmで。

$ git clone https://github.com/kazeburo/jstat2gf.git
$ cd jstat2gf
$ cpanm --installdeps .

perl-5.14以上なら追加の依存モジュールはないと思われます。

最後に、JVM Operation Casual Talksでの自分のLT資料はこちらです。普段やっているperlのWebアプリケーションの運用を紹介して、それJVMだとどうしたらええの?という疑問をまとめたつもり

ES + kibanaでサーバモニタリングをやってみたのですが、ESのCPU負荷がかなり高くて、リアルタイムにモニタリングできない状況だったので、graphite + grafanaにしてみた。ちなみに、ESのサーバのCPU負荷はこんな感じ。

es-cpu.png

GrafanaはGraphite用のDash boardを作るツール。最近、influxDBにも対応していてなかなか野心的。

Grafana - Graphite Dashboard

kibanaをforkしただけあって、画面はそっくり。まだ修正もれがあるのか、メッセージにkibanaって文字がでてくることもある

grafana.png

セットアップ

もろもろのセットアップのメモ

監視サーバ

まず、監視サーバにGraphiteとGrafanaをいれる。環境はCentOS6

CentOS6.x - CentOSにRPMでGraphite+Diamondをインストールする - Qiita

を参考にしました。

GraphiteはEPELのrpmを使いました。graphite-webとcarbonをいれます。

$ sudo rpm -ivh https://dl.fedoraproject.org/pub/epel/6/x86_64/epel-release-6-8.noarch.rpm
$ sudo yum install graphite-web python-carbon mysql mysql-server MySQL-python

外部からのアクセスがないサーバなので、mysqlはこだわらず、そのまま起動して、ユーザを作っておく。

$ sudo service mysqld start
$ mysql -uroot -e "GRANT ALL PRIVILEGES ON graphite.* TO 'graphite'@'localhost' IDENTIFIED BY 'foobar';"
$ mysql -uroot -e "CREATE DATABASE graphite;"

そして、graphiteの設定

$ sudo vim /etc/graphite-web/local_settings.py

一番最後にでも、以下を追加

TIME_ZONE = 'Asia/Tokyo'
DATABASES = {
  'default': {
    'NAME': 'graphite',
    'ENGINE': 'django.db.backends.mysql',
    'USER': 'graphite',
    'PASSWORD': 'foobar',
    'HOST': 'localhost',
    'PORT': '3306',
  }
}

graphiteのweb uiはapache経由でアクセスするので、apacheの設定

$ sudo vim cat /etc/httpd/conf.d/graphite-web.conf

中身はこんな感じにしてみた

Listen 8081
<VirtualHost *:8081>

    Header set Access-Control-Allow-Origin "http://foo.bar.baz:8080"
    Header set Access-Control-Allow-Methods "GET, OPTIONS"
    Header set Access-Control-Allow-Headers "origin, authorization, accept"

    ServerName graphite-web
    DocumentRoot "/usr/share/graphite/webapp"
    ErrorLog /var/log/httpd/graphite-web-error.log
    CustomLog /var/log/httpd/graphite-web-access.log common
    Alias /media/ "/usr/lib/python2.6/site-packages/django/contrib/admin/media/"

    WSGIScriptAlias / /usr/share/graphite/graphite-web.wsgi
    WSGIImportScript /usr/share/graphite/graphite-web.wsgi process-group=%{GLOBAL} application-group=%{GLOBAL}

    <Location "/content/">
        SetHandler None
    </Location>

    <Location "/media/">
        SetHandler None
    </Location>

</VirtualHost>

そして、Graphiteが使うCarbonというストレージサービスの設定。

$ sudo vim /etc/carbon/storage-schemas.conf

メトリクスを保存するDBの設定を行う

[dstat]
pattern = ^dstat\.
retentions = 10s:21d

これを、一番上に追加する。今回のメトリクス取得は期間が限られているので、10秒ごとの値を3週間保存するだけとしています。

次はElasticSearch。前回のを参考にしてインストール。

今のところ、Grafanaはdashboardの設定保存などに、ElasticSearchを使うのでESもいれる

$ sudo yum install java-1.7.0-openjdk
$ sudo rpm -Uvh https://download.elasticsearch.org/elasticsearch/elasticsearch/elasticsearch-1.x.x.noarch.rpm
$ sudo service elasticsearch start

さいごにGrafana。

Grafanaはまだパッケージはないので、githubからcloneして、設定する。

$ cd /path/to
$ git clone https://github.com/torkelo/grafana.git
$ cd grafana/src
$ cp config.sample.js config.js
$ vim config.js

書き換えたのはgraphiteUrlだけ

graphiteUrl: "http://"+window.location.hostname+":8081",

そしてApacheの設定を追加して、grafana/srcがみれるようにする

Listen 8080
<VirtualHost *:8080>
    ServerName grafana
    DocumentRoot "/path/to/grafana/src"
</VirtualHost>

これで監視サーバの準備は完了

監視対象サーバのfluentdの設定

まず、td-agentと使うpluginを入れます

$ sudo yum install td-agent
$ sudo /usr/lib64/fluent/ruby/bin/fluent-gem install fluent-plugin-map fluent-plugin-dstat

graphiteのpluginはセコンさんことhotchpotchさんが作っていますが、ruby-gemsにリリースされてないので、cloneしてきて入れました

$ cd /tmp
$ git clone https://github.com/hotchpotch/fluent-plugin-graphite.git
$ cd fluent-plugin-graphite
$ /usr/lib64/fluent/ruby/bin/rake 
$ sudo /usr/lib64/fluent/ruby/bin/fluent-gem install pkg/fluent-plugin-graphite-0.2.1.gem

んで、td-agent.confの設定。

<source>
  type exec
  command sh /etc/td-agent/stats.sh
  format tsv
  keys hostname,nginx,memcached
  tag stats
  run_interval 5
</source>

<match stats>
  type copy
  <store>
    type map
    tag  "map.dstat.nginx-req"
    time time
    record {"key" => record["hostname"]+".nginx-req", "gauge" => record["nginx"] }
  </store>
  <store>
    type map
    tag  "map.dstat.memcached-incr"
    time time
    record {"key" => record["hostname"]+".memcached-incr", "gauge" => record["memcached"] }
  </store>
</match>

<source>
  type dstat
  tag dstat
  option -lcn
  delay 5
</source>

<match dstat>
  type copy
  <store>
    type map
    tag  "map.dstat.loadavg-short"
    time time
    record {"key" => record["hostname"]+".loadavg-short", "gauge" => record["dstat"]["load avg"]["1m"].to_f*100  }
  </store>
  <store>
    type map
    tag  "map.dstat.cpu-usr"
    time time
    record {"key" => record["hostname"]+".cpu-usr", "gauge" => record["dstat"]["total cpu usage"]["usr"] }
  </store>
  <store>
    type map
    tag  "map.dstat.cpu-sys"
    time time
    record {"key" => record["hostname"]+".cpu-sys", "gauge" => record["dstat"]["total cpu usage"]["sys"] }
  </store>
  <store>
    type map
    tag  "map.dstat.cpu-hiq"
    time time
    record {"key" => record["hostname"]+".cpu-hiq", "gauge" => record["dstat"]["total cpu usage"]["hiq"] }
  </store>
  <store>
    type map
    tag  "map.dstat.cpu-siq"
    time time
    record {"key" => record["hostname"]+".cpu-siq", "gauge" => record["dstat"]["total cpu usage"]["siq"] }
  </store>
  <store>
    type map
    tag  "map.dstat.net-recv"
    time time
    record {"key" => record["hostname"]+".net-recv", "gauge" => record["dstat"]["net/total"]["recv"] }
  </store>  
  <store>
    type map
    tag  "map.dstat.net-send"
    time time
    record {"key" => record["hostname"]+".net-send", "gauge" => record["dstat"]["net/total"]["send"] }
  </store>  
</match>

<match map.dstat.*>
  type           graphite
  host           監視サーバのアドレス
  port           2003
  key_prefix     dstat
  flush_interval 5s
</match>

stats.shは前回と同じなので省略

この設定を行うことで、graphiteには

dstat.gauges.サーバ名.ドメイン.nginx-req

のような名前でメトリクスが記録されるようになります。

設定をいれたら、fluentdを起動

$ sudo service td-agent start

grafanaを設定

また、ぽちぽちやっていきます。Graphiteの関数・フィルタが使えるのが便利

sum_series.png

nginx/memcachedは全てのサーバの合計値を表示。1秒毎の増加分を計算して、sumSeriesを使う。rrdのようにcouterをいれるスキーマを予め用意しなくても良いのが楽

load-avg.png

ロードアベレージは全てのサーバを表示

filter-topn.png

トラフィックは大きい値を5つだけ表示。highestCurrentを使いました。マウス操作で関数・フィルタを選んで行けるので楽ですね。

これで、試しにabを流したのが以下の動画。

普段はサーバのメトリクス可視化のためにcloudforecastを使っていますが、某案件用に数秒単位で数十台のサーバのメトリクスを表示したいので、記事タイトルのような構成を作ってみた。

kibana.jpg

dstatでとった各種値の他に、nginxとmemcachedの情報も合わせて表示させています。

セットアップ

もろもろのセットアップのメモ

監視サーバ

まず、監視サーバにElasticsearchkibanaをいれる。環境はCentOS6

$ sudo yum install java-1.7.0-openjdk
$ sudo rpm -Uvh https://download.elasticsearch.org/elasticsearch/elasticsearch/elasticsearch-1.x.x.noarch.rpm

Elasticsearchは特に設定なく起動

$ sudo service elasticsearch start

次、kibanaとkibanaをhostingするwebサーバの設定

$ cd /path/to
$ wget https://download.elasticsearch.org/kibana/kibana/kibana-3.x.x.tar.gz
$ tar zxf kibana-3.x.x.tar.gz

して、webサーバのドキュメントルートに、/path/to/kibana-3.x.x を指定。

データをいれ始める前に、Elasticsearchにmappingのヒントを与える

$ curl -XPUT localhost:9200/_template/template_1 -d '
{
   "template" : "logstash-*",
   "mappings" : {
       "dstat": {
           "properties": {
                "host" : { "type" : "string", "index" : "not_analyzed" },
                "value" : {"type" : "double"}
           }
       }
   }
}
'

監視対象サーバの設定

モニタリング対象となるサーバにはfluentdをいれる

$ sudo yum install td-agent

弊社環境だとモリス作成のtd-agentがyum repositoryにあるのでこれを使った。一般的にはtreasuredata社のrepositoryを使うと思う。

pluginを追加

$ sudo /usr/lib64/fluent/ruby/bin/fluent-gem install fluent-plugin-map fluent-plugin-dstat fluent-plugin-elasticsearch

fluent-plugin-elasticsearchのインストールで依存性解決ができなくて失敗することがなんどかあった。

/etc/td-agent/td-agent.confを次のようにしてみた。dstatの情報をそれぞれ別のログにするところは、「dstatをkibanaで可視化+3.0.0milestone5新機能 を参考にさせて頂きました。

<source>
  type exec
  command sh /etc/td-agent/stats.sh
  format tsv
  keys hostname,nginx,memcached
  tag stats
  run_interval 5
</source>

<match stats>
  type copy
  <store>
    type map
    tag  "map.dstat.nginx-req"
    time time
    record {"value" => record["nginx"], "stat" => "nginx-req", "host" => record["hostname"]}
  </store>
  <store>
    type map
    tag  "map.dstat.memcached-incr"
    time time
    record {"value" => record["memcached"], "stat" => "memcached-incr", "host" => record["hostname"]}
  </store>
</match>

<source>
  type dstat
  tag dstat
  option -lcn
  delay 5
</source>

<match dstat>
  type copy
  <store>
    type map
    tag  "map.dstat.loadavg-short"
    time time
    record {"value" => record["dstat"]["load avg"]["1m"], "stat" => "loadavg-short", "host" => record["hostname"]}
  </store>
  <store>
    type map
    tag  "map.dstat.cpu-usr"
    time time
    record {"value" => record["dstat"]["total cpu usage"]["usr"], "stat" => "cpu-usr", "host" => record["hostname"]}
  </store>
  <store>
    type map
    tag  "map.dstat.cpu-sys"
    time time
    record {"value" => record["dstat"]["total cpu usage"]["sys"], "stat" => "cpu-sys", "host" => record["hostname"]}
  </store>
  <store>
    type map
    tag  "map.dstat.cpu-hiq"
    time time
    record {"value" => record["dstat"]["total cpu usage"]["hiq"], "stat" => "cpu-hiq", "host" => record["hostname"]}
  </store>
  <store>
    type map
    tag  "map.dstat.cpu-siq"
    time time
    record {"value" => record["dstat"]["total cpu usage"]["siq"], "stat" => "cpu-siq", "host" => record["hostname"]}
  </store>
  <store>
    type map
    tag  "map.dstat.net-recv"
    time time
    record {"value" => record["dstat"]["net/total"]["recv"], "stat" => "net-recv", "host" => record["hostname"]}
  </store>  
  <store>
    type map
    tag  "map.dstat.net-send"
    time time
    record {"value" => record["dstat"]["net/total"]["send"], "stat" => "net-send", "host" => record["hostname"]}
  </store>  
</match>

<match map.dstat.*>
  type elasticsearch
  type_name       dstat
  host            監視サーバのIP
  port            9200
  logstash_format true
  logstash_prefix logstash
  flush_interval  5s
</match>

nginxとmemcachedの統計情報をとるstats.shは

#!/bin/sh
set -e
HOST=$(hostname)
NREQ=$(curl -s http://localhost/nginx_status|head -3|tail -1|awk '{print $3}')
MREQ=$(echo stats | nc localhost 11211| grep incr_hits | awk '{print $3}')
echo -e "$HOST\t$NREQ\t$MREQ"

のような簡単なshellscriptです。

設定をいれたら、fluentdを起動

$ sudo service td-agent start

kibanaをぽちぽち設定

データが入りはじめたら、kibanaでぽちぽちやって可視化。

nginxのリクエスト数なら、topNクエリを使い、

top-n.png

Histgramグラフの設定で、Transform SeriesのSeconds、Derivativeを両方とも有効にして秒間のリクエスト数に直し、stackグラフにします。

panel.png

あとはグラフによって、秒で割るかどうか、stackするかどうかを選びつつ可視化して行きます。すると最終的にこんなのが出来上がりました。

kibana.jpg

あとは本番を待つのみ。。

高速スケーラブル検索エンジン ElasticSearch Server
Rafal Kuc Marek Rogozinski
KADOKAWA/アスキー・メディアワークス
売り上げランキング: 2,398

前のエントリで紹介した HTTP::Entity::Parser ですが、プロファイリングとるとPOSTデータの解析で半分弱の時間を使っていたのでそこをXSにしたら良いんじゃないかということで、新しく WWW::Form::UrlEncoded ってのを書いて出しました。

https://metacpan.org/release/WWW-Form-UrlEncoded
https://github.com/kazeburo/WWW-Form-UrlEncoded

似たモジュールに URL::EncodeText::QueryString がありますが、HTTP::Bodyのパーサと互換性あるこの仕様を満たしていないので、新しく書き起こしてみました。URL::Encode(::XS) がかなり速いのですが、それに近い速度がでています。

              Rate wwwform_pp    text_qs    wwwform  urlencode
wwwform_pp 11933/s         --       -77%       -87%       -88%
text_qs    52609/s       341%         --       -43%       -46%
wwwform    91995/s       671%        75%         --        -5%
urlencode  97303/s       715%        85%         6%         --

そして、これをHTTP::Entitiy::Parserに組み込んで0.03としてリリースしました

https://metacpan.org/release/HTTP-Entity-Parser
https://github.com/kazeburo/HTTP-Entity-Parser/

ベンチマークはPOSTデータのサイズを変えながら3つとってみました

## content length => 38

Benchmark: running http_body, http_entity for at least 1 CPU seconds...
 http_body:  1 wallclock secs ( 1.08 usr +  0.00 sys =  1.08 CPU) @ 36201.85/s (n=39098)
http_entity:  1 wallclock secs ( 1.12 usr +  0.00 sys =  1.12 CPU) @ 76799.11/s (n=86015)
               Rate   http_body http_entity
http_body   36202/s          --        -53%
http_entity 76799/s        112%          --

## content length => 177

Benchmark: running http_body, http_entity for at least 1 CPU seconds...
 http_body:  1 wallclock secs ( 1.11 usr +  0.00 sys =  1.11 CPU) @ 14901.80/s (n=16541)
http_entity:  1 wallclock secs ( 1.08 usr +  0.00 sys =  1.08 CPU) @ 64474.07/s (n=69632)
               Rate   http_body http_entity
http_body   14902/s          --        -77%
http_entity 64474/s        333%          --

## content length => 1997

Benchmark: running http_body, http_entity for at least 1 CPU seconds...
 http_body:  1 wallclock secs ( 1.16 usr +  0.00 sys =  1.16 CPU) @ 1930.17/s (n=2239)
http_entity:  1 wallclock secs ( 1.11 usr +  0.00 sys =  1.11 CPU) @ 29519.82/s (n=32767)
               Rate   http_body http_entity
http_body    1930/s          --        -93%
http_entity 29520/s       1429%          --

データが大きくなればなるほど差が大きくなります。

HTTP::Bodyと互換性のある HTTPのEntityをパースするモジュールをリリースしました。

https://metacpan.org/release/HTTP-Entity-Parser
https://github.com/kazeburo/HTTP-Entity-Parser/

HTTPのEntityってのは、こういう範囲を指します。

POST /foo HTTP/1.1          # Not part of the entity.
Content-Type: text/plain    # ┬ The entity is from this line down...
Content-Length: 1234        # │
                            # │
Hello, World! ...           # ┘

元ネタは「java - What exactly is an HTTP Entity? - Stack Overflow

HTTP::Entity::Parserの使い方はこんな感じ。

use HTTP::Entity::Parser;

my $parser = HTTP::Entity::Parser->new;
$parser->register('application/x-www-form-urlencoded','HTTP::Entity::Parser::UrlEncoded');
$parser->register('multipart/form-data','HTTP::Entity::Parser::MultiPart');
$parser->register('application/json','HTTP::Entity::Parser::JSON');

sub app {
    my $env = shift;
    my ( $params, $uploads) = $parser->parse($env);
}

このモジュール、元々は tokuhirom の Plackへのpullreqにあるものですが、「だれか別モジュールにして」とのことでしたので、テスト書いてHTTP::Bodyとの互換性を検証して、リリースしました。

このモジュールの開発動機はこのスライドにありますね。ただ、スライド中にでてくるURL::Encodeは互換性の問題で使ってません。

公平かどうか微妙かもですが、一応ベンチマーク。

#!/usr/bin/perl

use strict;
use warnings;
use HTTP::Entity::Parser;
use HTTP::Body;
use Benchmark qw/:all/;

my $content = 'xxx=hogehoge&yyy=aaaaaaaaaaaaaaaaaaaaa';

my $parser = HTTP::Entity::Parser->new;
$parser->register('application/x-www-form-urlencoded','HTTP::Entity::Parser::UrlEncoded');

cmpthese(timethese(-1, {
    'http_entity' => sub {
        open my $input, '<', \$content;
        my $env = {
            'psgi.input' => $input,
            'psgix.input.buffered' => 1,
            CONTENT_LENGTH => length($content),
            CONTENT_TYPE => 'application/x-www-form-urlencoded',
        };
        $parser->parse($env);
    },
    'http_body' => sub {
        open my $input, '<', \$content;
        my $body   = HTTP::Body->new( 'application/x-www-form-urlencoded', length($content) );
        $input->read( my $buffer, 8192);
        $body->add($buffer);
        $body->param;
    }
}));

__END__
Benchmark: running http_body, http_entity for at least 1 CPU seconds...
 http_body:  1 wallclock secs ( 1.08 usr +  0.00 sys =  1.08 CPU) @ 36201.85/s (n=39098)
http_entity:  1 wallclock secs ( 1.10 usr +  0.01 sys =  1.11 CPU) @ 51661.26/s (n=57344)
               Rate   http_body http_entity
http_body   36202/s          --        -30%
http_entity 51661/s         43%          --

NYTProfでプロファイルをとると、application/x-www-form-urlencodedのパースがそれなりの時間を占めるので、互換性があるXSモジュールがあるともう少し速くなるんじゃないかなーと思ってる。

以前、「Q4Mを簡単に導入する方法 - MySQL Casual Advent Calendar 2011」で紹介したQ4M専用MySQLのセットアップスクリプトのMySQL5.6対応版を作りました。

kamipo先生によると、プラグインをあとからビルドしてMySQLに追加する事は推奨されていないとのことなので、Q4MのソースコードをMySQLのソースコードツリーにコピーしてから一緒にビルドします。これは同じくkamipo先生のmysql-buildを参考にさせて頂きました。

#!/bin/sh
set -e

MYVER=5.6.15
Q4MVER=0.9.11

CDIR=$(cd $(dirname $0) && pwd)
cd /usr/local/src
if [ -f $CDIR/mysql-$MYVER.tar.gz ]; then
    cp $CDIR/mysql-$MYVER.tar.gz ./
fi
if [ ! -f mysql-$MYVER.tar.gz ]; then
    wget http://ftp.jaist.ac.jp/pub/mysql/Downloads/MySQL-5.6/mysql-$MYVER.tar.gz
fi
tar zxf mysql-$MYVER.tar.gz

if [ -d q4m-$Q4MVER ]; then
    rm -rf q4m-$Q4MVER
fi
if [ ! -f q4m-$Q4MVER.tar.gz ]; then
    wget http://q4m.kazuhooku.com/dist/q4m-$Q4MVER.tar.gz
fi
tar zxf q4m-$Q4MVER.tar.gz
mv q4m-$Q4MVER mysql-$MYVER/storage/q4m
if [ ! -f mysql-$MYVER/storage/q4m/CMakeLists.txt ]; then
  curl -kL https://raw.github.com/q4m/q4m/$Q4MVER/CMakeLists.txt > mysql-$MYVER/storage/q4m/CMakeLists.txt
fi

yum install -y cmake ncurses-devel libaio-devel
cd mysql-$MYVER
cmake . -DCMAKE_INSTALL_PREFIX=/usr/local/q4m \
  -DDEFAULT_CHARSET=utf8 -DDEFAULT_COLLATION=utf8_general_ci \
  -DWITH_EXTRA_CHARSETS=all \
  -DWITH_ZLIB=bundled -DWITH_SSL=bundled -DWITH_READLINE=1 \
  -DWITH_PIC=ON -DWITH_FAST_MUTEXES=ON \
  -DWITH_DEBUG=OFF \
  -DCOMPILATION_COMMENT="Q4M" -DMYSQL_SERVER_SUFFIX="-q4m" \
  -DMYSQL_USER=nobody -DMYSQL_UNIX_ADDR="/tmp/mysql_q4m.sock" \
  -DMYSQL_TCP_PORT=13306 \
  -DWITH_DEFAULT_FEATURE_SET=xsmall \
  -DWITH_PARTITION_STORAGE_ENGINE=1 \
  -DWITHOUT_DAEMON_EXAMPLE_STORAGE_ENGINE=1 \
  -DWITHOUT_FTEXAMPLE_STORAGE_ENGINE=1 \
  -DWITHOUT_EXAMPLE_STORAGE_ENGINE=1 \
  -DWITHOUT_ARCHIVE_STORAGE_ENGINE=1 \
  -DWITHOUT_BLACKHOLE_STORAGE_ENGINE=1 \
  -DWITHOUT_FEDERATED_STORAGE_ENGINE=1 \
  -DWITHOUT_INNOBASE_STORAGE_ENGINE=1 \
  -DWITHOUT_PERFSCHEMA_STORAGE_ENGINE=1 \
  -DWITHOUT_NDBCLUSTER_STORAGE_ENGINE=1 \
  -DWITH_INNODB_MEMCACHED=OFF \
  -DWITH_EMBEDDED_SERVER=OFF \
  -DWITH_UNIT_TESTS=OFF
make
make install

mkdir -p /usr/local/q4m/etc
cp $CDIR/my.cnf /usr/local/q4m/etc
cp $CDIR/q4m.init /etc/init.d/q4m
chmod 755 /etc/init.d/q4m
chkconfig --add q4m

/usr/local/q4m/scripts/mysql_install_db --skip-name-resolve \
  --basedir=/usr/local/q4m --defaults-file=/usr/local/q4m/etc/my.cnf
rm -f /usr/local/q4m/my.cnf
chmod 755 /usr/local/q4m/var
/etc/init.d/q4m start

cat storage/q4m/support-files/install.sql | /usr/local/q4m/bin/mysql -S /tmp/mysql_q4m.sock
echo "show plugins" | | /usr/local/q4m/bin/mysql -S /tmp/mysql_q4m.sock

my.cnfやinitスクリプトなどソースはすべてgithubで公開中。使う時は

$ git clone git://github.com/kazeburo/mysetup.git
$ cd mysetup/q4m_mysql56
$ sudo sh ./setup.sh

とするだけで動くと思われます。CentOS6で検証しています。

これをと使うと、 /usr/local/q4m 以下にMySQLとQ4Mを入れて、ポート13306にてmysqldが起動します。データも /usr/local/q4m 以下に入ります。Q4M専用MySQLという位置付けなのでInnoDBが使えないのが注意点。

initスクリプトもインストールするので

$ sudo service q4m start|stop|restart

とサービスの制御ができます。

これで5.1系とおさらばできますね〜

strftime(3)でいうところの「%z」にあたる、「+0900」のようなタイムゾーンのGMT/UTCへのオフセット文字列を出力するモジュールをリリースしました。XSです

https://metacpan.org/release/Time-TZOffset
https://github.com/kazeburo/Time-TZOffset

機能は1つ

my @localtime = localtime;
say tzoffset(@localtime); => +0900

Windowsでも動作します。

use Benchmark qw/:all/;
use POSIX qw//;
use Time::Local;
use Time::TZOffset;

cmpthese(timethese(-1, {
    'posix' => sub {
        POSIX::strftime('%z', @lt);
    },
    'time_local' => sub {
        my $sec = Time::Local::timegm(@lt) - Time::Local::timelocal(@lt);
        sprintf '%+03d%02u', $sec/60/60, $min/60%60;
    },
    'tzoffset' => sub {
        Time::TZOffset::tzoffset(@lt);
    },
}));
__END__
Benchmark: running posix, time_local, tzoffset for at least 1 CPU seconds...
     posix:  1 wallclock secs ( 0.66 usr +  0.39 sys =  1.05 CPU) @ 179442.86/s (n=188415)
time_local:  1 wallclock secs ( 1.12 usr +  0.16 sys =  1.28 CPU) @ 25846.09/s (n=33083)
  tzoffset:  1 wallclock secs ( 0.75 usr +  0.25 sys =  1.00 CPU) @ 286720.00/s (n=286720)
               Rate time_local      posix   tzoffset
time_local  25846/s         --       -86%       -91%
posix      179443/s       594%         --       -37%
tzoffset   286720/s      1009%        60%         --

POSIX::strftime('%z')より速く動作します。

このUTCへのオフセットはLinuxやBSD系のOSでは、t\m構造体に含まれており、mktime(3)を使うと簡単に得られます。

struct tm mytm;

memset(&mytm,0,sizeof(mytm));
mytm.tm_min = min;
mytm.tm_hour = hour;
mytm.tm_mday = mday;
mytm.tm_mon = mon;
mytm.tm_year = year;
mytm.tm_isdst = -1;
mktime(&mytm);

mytm.tm_gmtoff;

XS不慣れなのでツッコミ等お待ちしています。

1週間ぐらいtrialでしたが、Windowsでも動いたようなので0.10を出しました。すごーーーく、、、、ニッチです

https://metacpan.org/release/POSIX-strftime-Compiler
https://github.com/kazeburo/POSIX-strftime-Compiler

POSIX::strftime::CompilerはGNU互換で、ロケールの設定に影響を受けないstrftime(3)を提供します。WindowsでもGNU互換の文字が使えます。おそらくloggerとかloggerとかloggerに便利です

$ LC_ALL=ja_JP.UTF-8 perl -Ilib -MPOSIX::strftime::Compiler -E '
say POSIX::strftime::Compiler::strftime(q!%d/%b/%Y:%T %z!,localtime);
say POSIX::strftime(q!%d/%b/%Y:%T %z!,localtime)
'
21/Jan/2014:10:48:12 +0900
21/ 1月/2014:10:48:12 +0900

POSIX::strftime::CompilerのstrftimeはPOSIX::strftimeをwrapし、ロケールに影響をうけるフォーマット文字の部分を先に入れ替えています。

Apache::LogFormat::Compilerではstrftimeの前後にsetlocale(3)をしていますが、POSIX::strftime::Compilerを使えばそのコストを減らす事が出来ます。

use Benchmark qw/:all/;
use POSIX qw//;
use POSIX::strftime::Compiler;

my $fmt = '%d/%b/%Y:%T';
cmpthese(timethese(-1, {
    'compiler_function' => sub {
        POSIX::strftime::Compiler::strftime($fmt, localtime($t));
    },
    'posix_and_locale' => sub {
        my $old_locale = POSIX::setlocale(&POSIX::LC_ALL);
        POSIX::setlocale(&POSIX::LC_ALL, 'C');
        POSIX::strftime($fmt,localtime($t));
        POSIX::setlocale(&POSIX::LC_ALL, $old_locale);
    },
   'posix' => sub {
       POSIX::strftime($fmt,localtime($t));
   },
}));

ベンチマーク結果

Benchmark: running compiler_function, posix, posix_and_locale for at least 1 CPU seconds...
compiler_function:  1 wallclock secs ( 1.10 usr +  0.00 sys =  1.10 CPU) @ 446836.36/s (n=491520)
     posix:  1 wallclock secs ( 1.01 usr +  0.00 sys =  1.01 CPU) @ 243326.73/s (n=245760)
posix_and_locale:  1 wallclock secs ( 1.10 usr +  0.00 sys =  1.10 CPU) @ 71087.27/s (n=78196)
                      Rate  posix_and_locale             posix compiler_function
posix_and_locale   71087/s                --              -71%              -84%
posix             243327/s              242%                --              -46%
compiler_function 446836/s              529%               84%                --

setlocaleする場合より6倍程度良い数値がでています。また、POSIX::strftimeの倍程度の速度がでています。これは全てのフォーマット文字が複雑な計算などをする必要が無い文字の場合にsprintfだけで結果を出してしまう為です。

Apache::LogFormat::Compilerで使おうかなと考えて作ったのですが、'%z'の計算が速くならないのであまり嬉しい事がなかったという今現在なう

わりと長い間悩んでいたんだけど、最近解決したのでメモ。

サービスで利用しているsmalllightの画像変換サーバが、Apacheが使っているメモリ以上のメモリを使用し、Swapしたりメモリ枯渇でサーバがダウンするなどのことが何度かありました。

dentry_cache1.png

↑メモリの動きはこんな感じ

いろいろ調べた結果「dentry cache」なるものがメモリ多くを占めていることがわかりました。dentry cacheはディレクトリやファイル名とinodeとを結びつけに使われるキャッシュです。smalllightでは画像を変換する際に一時ファイルを作成するので、その情報が残るようです。

手元で再現させる

本番で使っているサーバはCentOS5系ですが、手元のVagrant上のCentOS6(ファイルシステムはext4)で、再現させてみました。

use Parallel::Prefork;
use File::Temp qw/tempfile/;

my $pm = Parallel::Prefork->new({
  max_workers  => 50,
  trap_signals => {
    TERM => 'TERM',
    HUP  => 'TERM',
    USR1 => undef,
  }
}); 
while ($pm->signal_received ne 'TERM') {
  $pm->start(sub {
      srand();
      select undef, undef, undef, rand(1);
      while (1) {
          select undef, undef, undef, 0.01;
          my ($fh,$fn) = tempfile(UNLINK=>0);
          syswrite $fh, 'x'x128;
          close $fh;
          unlink $fn;
      }
  });
}
$pm->wait_all_children();

まず、上記のscriptを作成し、100並列で一時ファイルを作りまくります。そしてdstatでメモリ使用量を観察します。

[vagrant@localhost ~]$ dstat -Tcms 30
--epoch--- ----total-cpu-usage---- ------memory-usage----- ----swap---
  epoch   |usr sys idl wai hiq siq| used  buff  cach  free| used  free
1389191041| 44  38  18   1   0   0|48.5M 2424k 10.1M  430M|   0   992M
1389191071| 38  49  13   0   0   0| 132M 3956k 12.5M  342M|   0   992M
1389191101| 46  54   0   0   0   0| 167M 4004k 12.5M  307M|   0   992M
1389191131| 48  52   0   0   0   0| 200M 4052k 12.5M  274M|   0   992M
1389191161| 44  56   0   0   0   0| 236M 4108k 12.5M  238M|   0   992M
1389191191| 45  54   0   0   0   0| 270M 4156k 12.5M  204M|   0   992M
1389191221| 48  52   0   0   0   0| 303M 4204k 12.5M  170M|   0   992M
1389191251| 49  50   0   0   0   0| 337M 4252k 12.5M  137M|   0   992M
1389191281| 49  50   0   0   0   0| 369M 4428k 12.5M  105M|   0   992M
1389191311| 47  53   0   0   0   0| 402M 4476k 12.5M 71.9M|   0   992M
1389191341| 49  51   0   0   0   0| 435M 4524k 12.5M 38.9M|   0   992M
1389191371| 52  48   0   0   0   0| 467M 4572k 12.5M 6348k|   0   992M
1389191401| 44  56   0   0   0   0| 389M 4356k 11.8M 85.4M|   0   992M <- 一回解放されてる?
1389191431| 42  58   0   0   0   0| 425M 4820k 12.6M 48.2M|   0   992M
1389191461| 45  55   0   0   0   0| 460M 4868k 12.6M 12.7M|   0   992M
1389191491| 43  57   0   0   0   0| 467M 4916k 12.6M 6104k|   0   992M
1389191521| 46  54   0   0   0   0| 467M 4964k 12.6M 6600k|   0   992M
1389191551| 44  56   0   0   0   0| 463M 5012k 12.6M 9700k|   0   992M
1389191553| 44  56   0   0   0   0| 466M 5020k 12.6M 7344k|   0   992M

30秒ごとに30MB強、メモリ使用量が増えて行き、物理メモリいっぱいいっぱいまで使います。その後はキャッシュを順次解放するのかメモリ使用量に大きな変化がなくなります。

dentry cacheがどの程度あるのかは、slabtopコマンドを使うと確認できます。

[vagrant@localhost ~]$ slabtop -o
 Active / Total Objects (% used)    : 1943159 / 1945730 (99.9%)
 Active / Total Slabs (% used)      : 97116 / 97129 (100.0%)
 Active / Total Caches (% used)     : 90 / 183 (49.2%)
     Active / Total Size (% used)       : 369732.02K / 370130.38K (99.9%)
 Minimum / Average / Maximum Object : 0.02K / 0.19K / 4096.00K

  OBJS ACTIVE  USE OBJ SIZE  SLABS OBJ/SLAB CACHE SIZE NAME
1875580 1875580 100%    0.19K  93779       20    375116K dentry
 15680  15600  99%    0.03K    140      112       560K size-32
  8732   7813  89%    0.06K    148       59       592K size-64
  7102   7064  99%    0.07K    134       53       536K selinux_inode_security
  5859   5833  99%    0.14K    217       27       868K sysfs_dir_cache
  5776   5757  99%    0.20K    304       19      1216K vm_area_struct
  5390   5349  99%    0.05K     70       77       280K anon_vma_chain
  4212   4161  98%    0.58K    702        6      2808K inode_cache
  3036   2935  96%    0.04K     33       92       132K anon_vma

dentry cacheはメモリが必要になれば解放されるらしいので、その検証のため、もう一つメモリを大量に使うプロセスを起動します。

perl -e 'my $x="x"x200_000_000;sleep(3600);

これは300MBぐらいメモリを確保します。

そしてまた、dstatで観察すると、なんと、swapしてしまいました。

[vagrant@localhost ~]$ dstat -Tcms 30
--epoch--- ----total-cpu-usage---- ------memory-usage----- ----swap---
  epoch   |usr sys idl wai hiq siq| used  buff  cach  free| used  free
1389191593| 45  46   9   0   0   0| 464M 5076k 12.6M 9196k|   0   992M
1389191623| 48  52   0   0   0   0| 467M 5124k 12.6M 6476k|   0   992M
1389191653| 55  45   0   0   0   0| 464M 5172k 12.6M 8936k|   0   992M
1389191683| 55  45   0   0   0   0| 459M 5220k 12.6M 13.6M|   0   992M
1389191713| 53  47   0   0   0   0| 437M 4824k 12.5M 35.9M|  76k  992M <- ここで起動
1389191743| 46  54   0   0   0   0| 482M  104k 2868k 6072k|  27M  965M
1389191773| 46  53   0   0   0   0| 481M  112k 2432k 6792k|  29M  963M
1389191803| 45  55   0   0   0   0| 483M  152k 1956k 5924k|  36M  956M
1389191833| 45  54   0   0   0   0| 482M  204k 2120k 6676k|  48M  944M
...
1389192371| 48  52   0   0   0   0| 475M 1972k 8184k 6156k| 141M  851M
1389192401| 48  52   0   0   0   0| 477M 1544k 6844k 5756k| 142M  850M
1389192431| 48  52   0   0   0   0| 478M  932k 6044k 5760k| 144M  848M

dentry cacheを解放するスピードが間に合わないのか、一部のメモリがswapに追い出され、さらに徐々にswapが増えて行くる結果となりました。

画像変換ではたまに大きなサイズの画像などがあり、メモリを多く必要とする場合があります。その際にswapしてしまい性能がダウンしてしまうことが予想されます。またこのままswapが増え続ければメモリ枯渇の可能性もあります。

dentry cacheの解放方法

swapやメモリ枯渇を防ぐめには、dentry cacheを早めに解放してメモリを空けておくのが良さそうです。その方法は2つあります。

一つ目は vm.drop_caches を使う方法

$ sudo /sbin/sysctl -w vm.drop_caches=2 または 3
# echo 2 > /proc/sys/vm/drop_caches

ディスクキャッシュをクリアする方法と同じです。2をいれるとdentry cacheのクリア。3をいれるとdentry cacheとディスクキャッシュをクリアします。

もう一つが、vm.vfs_cache_pressure を設定する方法

$ sudo /sbin/sysctl -w vm.vfs_cache_pressure=10000

デフォルトは100で、数字を大きくすると早めにキャッシュを解放するようです。しかし、かなり大きな数字にしてもサービスの本番環境では目立った変化はありませんでした。

tmpfsを使う方法

最後に発見したのがtmpfsを使う方法。tmpfsだと、dentry cacheは溜まらないようです。

環境変数TMPDIRにtmpfsを指定して先ほどのscriptを実行します。

[vagrant@localhost vagrant]$ TMPDIR=/dev/shm perl mktmp.pl

またdstatで観察すると、メモリが増えて行かないのが分かります。

[vagrant@localhost ~]$ dstat -Tcms 30
--epoch--- ----total-cpu-usage---- ------memory-usage----- ----swap---
  epoch   |usr sys idl wai hiq siq| used  buff  cach  free| used  free
1389168490| 47  35  17   0   0   0|44.3M 2428k 8804k  435M|8444k  984M
1389168520| 49  37  14   0   0   0| 146M 3924k 11.1M  330M|8444k  984M
1389168550| 60  40   0   0   0   0| 146M 3932k 11.1M  330M|8444k  984M
1389168580| 55  44   0   0   0   0| 146M 3932k 11.1M  330M|8444k  984M
1389168610| 56  44   0   0   0   0| 146M 4228k 12.4M  327M|8444k  984M
1389168640| 57  43   0   0   0   0| 144M 4388k 12.4M  330M|8444k  984M
1389168642| 57  43   0   0   0   0| 144M 4388k 12.4M  330M|8444k  984M^C

これは、tmpfsがLinuxのVFSを使わないからでしょうか。

ここまで分かったので、Apacheの起動スクリプトに

export TMPDIR=/dev/shm

を追加して再起動したところ、サーバが安定して動くようになりました。

dentry_cache2.png

↑今のメモリの動き

まとめ

  • dentry cacheによりメモリが圧迫される可能性がある
  • tmpfsを使うとdentry cacheは溜まらない
  • TMPDIR環境変数で一時ファイルの場所は変更出来る

大量に一時ファイルを作成するようなアプリケーションはtmpfsを使うのが良い。そもそも速いので使わない手はないですね。

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

アイテム

  • jstat2gf.png
  • filter-topn.png
  • load-avg.png
  • sum_series.png
  • grafana.png
  • es-cpu.png
  • panel.png
  • top-n.png
  • kibana.jpg
  • dentry_cache2.png

ウェブページ

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