Ruby + jemalloc でメモリ使用量が増える場合

Ruby でスレッドを1000個ほど作るとプロセスサイズが 4GB ほどになります。

% ruby -e 'system "ps -o vsz -p #$$"; 1000.times{Thread.new{sleep}}; system "ps -o vsz -p #$$"'
   VSZ
 46392
   VSZ
4151572

jemalloc 使うと 2.5GB くらいになります。jemalloc すばらしい。

% LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.1 ruby -e 'system "ps -o vsz -p #$$"; 1000.times{Thread.new{sleep}}; system "ps -o vsz -p #$$"'
   VSZ
 55672
   VSZ
2509080

ところが jemalloc を使ったほうが時間がかかります。

% time ruby -e '1000.times{Thread.new{sleep}}'
ruby -e '1000.times{Thread.new{sleep}}'  0.10s user 0.06s system 115% cpu 0.132 total
% time LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.1 ruby -e '1000.times{Thread.new{sleep}}'
LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.1 ruby -e   0.23s user 0.81s system 109% cpu 0.941 total

jemalloc を使わない場合は 0.13秒ですが、jemalloc を使うと 0.94秒かかってます。

プロセスサイズではなく OS の使用メモリを見てみます。

% ruby -e 'system "free"; 1000.times{Thread.new{sleep}}; system "free"'
             total       used       free     shared    buffers     cached
Mem:       8056184    7719868     336316      97356     587492    5373632
-/+ buffers/cache:    1758744    6297440
Swap:      4194300       2800    4191500
             total       used       free     shared    buffers     cached
Mem:       8056184    7766988     289196      97356     587492    5373632
-/+ buffers/cache:    1805864    6250320
Swap:      4194300       2800    4191500

jemalloc を使わない場合は、プロセスサイズは 4GB くらいでしたが、実際に使われているメモリは 47MB くらいです (前後 の used の差分)。

% LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.1 ruby -e 'system "free"; 1000.times{Thread.new{sleep}}; system "free"'
             total       used       free     shared    buffers     cached
Mem:       8056184    7721732     334452      89052     587520    5365352
-/+ buffers/cache:    1768860    6287324
Swap:      4194300       2800    4191500
             total       used       free     shared    buffers     cached
Mem:       8056184    7780264     275920      87260     587520    5363568
-/+ buffers/cache:    1829176    6227008
Swap:      4194300       2800    4191500

jemalloc を使った場合は 60MB くらいで、逆に多く使用してしまっています。

さらに 10個ほど fork してみます。

% ruby -e 'system "free"; 1000.times{Thread.new{sleep}}; 10.times{fork{sleep}}; sleep 1; system "free"'
             total       used       free     shared    buffers     cached
Mem:       8056184    2053828    6002356      68744     372796     613836
-/+ buffers/cache:    1067196    6988988
Swap:      4194300          0    4194300
             total       used       free     shared    buffers     cached
Mem:       8056184    2246508    5809676      68744     372804     613856
-/+ buffers/cache:    1259848    6796336
Swap:      4194300          0    4194300

メモリ使用量の増加分は 192MB 程度です。

% LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.1 ruby -e 'system "free"; 1000.times{Thread.new{sleep}}; 10.times{fork{sleep}}; sleep 1; system "free"'
             total       used       free     shared    buffers     cached
Mem:       8056184    2084772    5971412      67856     372892     613568
-/+ buffers/cache:    1098312    6957872
Swap:      4194300          0    4194300
             total       used       free     shared    buffers     cached
Mem:       8056184    4041740    4014444      67856     372892     613568
-/+ buffers/cache:    3055280    5000904
Swap:      4194300          0    4194300

jemalloc 使うと 2GB 程メモリを食ってしまっています。

なんでこういうことになるのか理屈は全然わかってませんが、jemalloc 使うことでメモリに悪影響を及ぼすこともあるという例でした。

まあ fork なんて使うのが悪いのかもしれませんけどね。

Postfix mruby plugin

Postfix の mruby plugin を作ってみました。

誰得かわからないんですけど、Postfix のルックアップテーブルで mruby スクリプトで結果を返すことができます。

たとえば、次のようなスクリプトを作れば、

class Hoge
  def lookup(key)
    key.reverse
  end
end
Hoge.new

与えられたキーを反転して返すことができます。

% postmap -q hoge mruby:/path/to/hoge.rb
egoh

Postfix で独自のルックアップテーブルを作る

Postfix 3.0 から導入された動的データベースプラグイン機構を使用して、独自のルックアップテーブルを作ってみます。

Postfix を次のようにしてインストールしてあります。

重要なのは dynamicmaps=yes なので、これさえあれば他のパラメータ指定はなんでもいいです。

% make makefiles dynamicmaps=yes\
 command_directory=/usr/local/postfix/sbin\
 config_directory=/usr/local/postfix/etc\
 default_database_type=hash\
 daemon_directory=/usr/local/postfix/libexec\
 data_directory=/var/local/postfix/lib\
 html_directory=no\
 mail_spool_directory=/var/mail\
 mailq_path=/usr/local/postfix/bin/mailq\
 manpage_directory=/usr/local/man\
 meta_directory=/usr/local/postfix/etc\
 newaliases_path=/usr/local/postfix/bin/newaliases\
 queue_directory=/var/local/postfix/spool\
 readme_directory=no\
 sendmail_path=/usr/local/postfix/sbin/sendmail\
 shlib_directory=/usr/local/postfix/lib
% make
% sudo make install
% sudo cp -r include /usr/local/postfix/

ルックアップテーブルは与えられた文字列に対して一つの値を返します。

今回は与えられた文字列中の小文字を大文字に変換して返すようなルックアップテーブルのプラグインを作成してみます。

#include "sys_defs.h"
#include "dict.h"

/* name に対応する値を返す */
static const char *dict_upcase_lookup(DICT *dict, const char *name)
{
  static char buf[256];
  int i = 0;

  /* 256バイト以上の文字列に対する値は見つからなかったことにする */
  if (strlen(name) > 255) {
    return NULL;
  }
  while (*name) {
    buf[i++] = toupper(*name++);
  }
  buf[i] = '\0';
  return buf;
}

/* 終了 */
static void dict_upcase_close(DICT *dict)
{
  dict_free(dict);
}

/* 初期化 */
DICT *dict_upcase_open(const char *name, int open_flags, int dict_flags)
{
  DICT *dict;

  dict = dict_alloc("upcase", name, sizeof(DICT));
  dict->lookup = dict_upcase_lookup;
  dict->close = dict_upcase_close;
  dict->flags = dict_flags;
  return dict;
}

作成

% gcc -I /usr/local/postfix/include -fPIC -g -O -DLINUX3 -c dict_upcase.c
% gcc -shared -o postfix-upcase.so dict_upcase.o
# sudo cp postfix-upcase.so /usr/local/postfix/lib

設定 [/usr/local/postfix/etc/dynamicmaps.cf]

upcase  postfix-upcase.so       dict_upcase_open

作成したライブラリのファイル名と初期化関数を dynamicmaps.cf に登録します。 shlib_directory 以外の場所に置くこともできます。その場合はフルパスで記述します。

postmap コマンドで試してみます。

% /usr/local/postfix/sbin/postmap -q hoge upcase:dummy
HOGE

ちゃんと動いてるようです。

ルックアップテーブルは hash:/etc/aliases のように type:name の形式で指定します。 今回は name 部分は特に使用していませんが、初期化関数の第一引数として渡されるので、必要な場合はそれを使えばいいです。

Postfix 3.0 の主な変更

Postfix 3.0.0 がリリースされたのでアナウンス文を勝手に翻訳してみました。

原文: http://www.postfix.org/announcements/postfix-3.0.0.html


Postfix stable release 3.0.0 が利用可能になりました。このリリースにより Postfix 2.8 のサポートは終了します。

主な変更(順不同):

  • RFC 6530 と関連ドキュメントで定義された国際化ドメイン名とアドレスのローカルパートのための SMTPUTF8 サポート。 この実装は Arnt Gulbrandsen が寄稿したコードをベースとし、CNNIC によってスポンサーされました。 SMTPUTF8 サポートはまだ途中です; Postfix 3.1 開発サイクル中で完了する予定です。 要約と制限については SMTPUTF8_README を参照してください。

  • Postfix 動的リンクライブラリとデータベースプラグインのサポート。 この実装は LaMont Jones による Debian Linux のためのコードをベースとしています。 有効なオプションの詳細な説明は INSTALL を参照してください。

  • 新しい Postfix デフォルト設定の選択的な採用のための OPT-IN 安全策。 何もしなければ、基本的には古い Postfix デフォルト設定は残ったままにすべきです (そうでなければ、あなたの下流のメンテナに文句を言ってください)。 Postfix ログファイルメッセージの詳細な説明は COMPATIBILITY_README を参照してください。

  • 複数のルックアップテーブル操作のサポート。 pipemap:{map1,map2...} データベースタイプはルックアップテーブルのパイプラインを実行します。 これは、一つのルックアップテーブルからの結果が次のテーブルのクエリになります; unionmap:{map1,map2,...} データベースタイプは、同じクエリを複数のルックアップテーブルに送り、その結果を結合します。

  • 単純なものを簡単に行う仮想テーブルのサポート。 inline:{key1=value1,key2=value2,...} テーブルは、少ないアイテムのためだけに外部のファイルを生成する必要を回避します; randmap{value1,value2,...} テーブルは、指定された値からのランダムな選択を実行します。

  • DNS ルックアップ結果と、配送エージェントステータスコードとメッセージのテーブル駆動変換。 通常は、問題のある DNS 応答を修正したり、配送エラーのハンドリングを修正するために、PCRE テーブルを使用します。 smtp_dns_reply_filter, smtp_delivery_status_filter と、他の Postfix デーモン用の類似の名前のパラメータを参照してください。

  • 設定ファイルの文法の改善。 ${name?{iftrue}:{iffalse}} のような三項演算子のサポート、${{expr1}==${expr2}?{iftrue}:{iffalse}} のような比較演算子、Milter毎とポリシーサーバー毎のタイムアウトと他の設定、空白を含む master.cf パラメータ、空白を含む import/export_environment 設定、空白を含む "static" テーブルルックアップ結果。 access(5) と header/body_checks(5) テーブルの複数のルックアップ結果のサポートは Postfix 3.1 開発サイクルで完了する予定です。

  • セッション毎のコマンドプロファイル。各インバウンド SMTP セッションの最後に記録されます。 たとえば、パスワード推測ボットは、"disconnect from name[addr] ehlo=1 auth=0/1 commands=1/2" と記録され、 これは、クライアントが、成功したひとつの EHLO コマンドと失敗したひとつの AUTH コマンドを送信し、QUIT コマンドを送信せずにハングアップしたことを意味しています。 この情報は常に記録され、冗長なロギングやネットワークの監視なしにパズルを解くことを助けます。

Postfix のソースコードは http://www.postfix.org/ にリストされたミラーにあります。

Ruby, MySQL のうるう秒の扱い

2015/7/1 にうるう秒が挿入されるということで、うるう秒の話題が盛り上がってるようなので自分も書いてみます。

Linux 上のプログラムが時刻で60秒を刻むには、うるう秒対応のタイムゾーンを使う必要があります。

通常はうるう秒を考慮していないタイムゾーンが使用されているので、60秒を含む時刻になることはありません。 60秒を含む時刻を扱うには、right/Japan のように right/ を前につけたタイムゾーンを指定します。

前回のうるう秒は 2012/7/1 08:59:60 (JST) だったので、これで試してみます。

% TZ=Japan date --date='2012-07-01 08:59:60'
date: `2012-07-01 08:59:60' は無効な日付です
% TZ=right/Japan date --date='2012-07-01 08:59:60'
2012年  7月  1日 日曜日 08:59:60 JST

Ruby

うるう秒なしのタイムゾーン:

% TZ=Japan ruby -rtime -e 'p Time.at(p Time.parse("2012-07-01 08:59:59").to_i)'
1341100799
2012-07-01 08:59:59 +0900
% TZ=Japan ruby -rtime -e 'p Time.at(p Time.parse("2012-07-01 08:59:60").to_i)'
1341100800
2012-07-01 09:00:00 +0900
% TZ=Japan ruby -rtime -e 'p Time.at(p Time.parse("2012-07-01 09:00:00").to_i)'
1341100800
2012-07-01 09:00:00 +0900

08:59:60 を指定しても 09:00:00 として扱われています。

うるう秒ありのタイムゾーン:

% TZ=right/Japan ruby -rtime -e 'p Time.at(p Time.parse("2012-07-01 08:59:59").to_i)'
1341100823
2012-07-01 08:59:59 +0900
% TZ=right/Japan ruby -rtime -e 'p Time.at(p Time.parse("2012-07-01 08:59:60").to_i)'
1341100824
2012-07-01 08:59:60 +0900
% TZ=right/Japan ruby -rtime -e 'p Time.at(p Time.parse("2012-07-01 09:00:00").to_i)'
1341100825
2012-07-01 09:00:00 +0900

ちゃんと 08:59:60 を扱うことができています。

うるう秒を扱えるかどうかで時刻の内部表現(1970-01-01 00:00:00 UTC からの経過秒数)が25秒ずれていますが、これは今までに25回うるう秒が挿入されたためです。

MySQL

うるう秒なしのタイムゾーン(TZ=Japan で mysqld を起動):

mysql> select from_unixtime(1341100799), unix_timestamp('2012-07-01 08:59:59');
+---------------------------+---------------------------------------+
| from_unixtime(1341100799) | unix_timestamp('2012-07-01 08:59:59') |
+---------------------------+---------------------------------------+
| 2012-07-01 08:59:59       |                            1341100799 |
+---------------------------+---------------------------------------+
mysql> select from_unixtime(1341100800), unix_timestamp('2012-07-01 08:59:60');
+---------------------------+---------------------------------------+
| from_unixtime(1341100800) | unix_timestamp('2012-07-01 08:59:60') |
+---------------------------+---------------------------------------+
| 2012-07-01 09:00:00       |                                     0 |
+---------------------------+---------------------------------------+
mysql> select from_unixtime(1341100800), unix_timestamp('2012-07-01 09:00:00');
+---------------------------+---------------------------------------+
| from_unixtime(1341100800) | unix_timestamp('2012-07-01 09:00:00') |
+---------------------------+---------------------------------------+
| 2012-07-01 09:00:00       |                            1341100800 |
+---------------------------+---------------------------------------+
1 row in set (0.00 sec)

08:59:60 はパースできずに 0 を返しています。

うるう秒ありのタイムゾーン(TZ=right/Japan で mysqld を起動):

mysql> select from_unixtime(1341100823), unix_timestamp('2012-07-01 08:59:59');
+---------------------------+---------------------------------------+
| from_unixtime(1341100823) | unix_timestamp('2012-07-01 08:59:59') |
+---------------------------+---------------------------------------+
| 2012-07-01 08:59:59       |                            1341100823 |
+---------------------------+---------------------------------------+
mysql> select from_unixtime(1341100824), unix_timestamp('2012-07-01 08:59:60');
+---------------------------+---------------------------------------+
| from_unixtime(1341100824) | unix_timestamp('2012-07-01 08:59:60') |
+---------------------------+---------------------------------------+
| 2012-07-01 08:59:59       |                                     0 |
+---------------------------+---------------------------------------+
mysql> select from_unixtime(1341100825), unix_timestamp('2012-07-01 09:00:00');
+---------------------------+---------------------------------------+
| from_unixtime(1341100825) | unix_timestamp('2012-07-01 09:00:00') |
+---------------------------+---------------------------------------+
| 2012-07-01 09:00:00       |                            1341100825 |
+---------------------------+---------------------------------------+

08:59:60 は 08:59:59 として扱われます。やはりパースはできません。

まとめ

  • Linux の場合は特別な設定をしない限り、通常は 60秒を含む時刻を返すことはありません。
  • 60秒を含む時刻をどのように扱うかはプログラム次第です。
  • Ruby は 60秒を含む時刻文字列をパースできますが、MySQL はできません。
  • うるう秒を扱えるタイムゾーンの場合、Ruby は 60秒を含む時刻を返すことがありますが、MySQL は 60秒になることはありません。

2014年の振り返り

あけましておめでとうございます。

2014年も無事終了したので一年を振り返ってみます。

ブログ

1年間で27件の記事を書きました。2013年が24件だったので少し増えました。

はてなブックマークで2桁以上ブクマされたものを並べてるとこんな感じです。

Twitter

1年間で4474件ツイートしました(公式RT含む)。平均すると一日 12.3件です。

月ごとの件数はこんな感じです。

329 2014-01
366 2014-02
354 2014-03
313 2014-04
391 2014-05
266 2014-06
346 2014-07
502 2014-08
502 2014-09
302 2014-10
340 2014-11
463 2014-12

リツイートが多かったツイート上位10件:

イベント

NSEG

毎月長野でやってる NSEG 勉強会にできるだけ参加してます。2014年は、第47回, 第49回, 第50回, 第51回, 第52回, 第53回, 第54回, 第55回 の8回参加しました。出席率は 8/12 = 66.7% でした。

自分が発表したのは、

第49回 MySQLの始め方

第50回 Dockerさわってみた

第54回 Dockerイメージを作る

でした。

その他

2015年は

ブログ

  • もうちょっと頻繁にブログを書こうと思ってます(去年も同じこと言ってた)。

技術文書

  • MySQLプロトコルについてまとめたものを電子書籍にできたらいいなぁ…とか。
  • 「Postfix辞典」を Postfix の最新版に対応したものとかも…。
  • (去年も同じこと言ってた)

イベント

  • NSEG にはできるだけ参加したいです。
  • 今年も RubyWorld Conference には行きたいです。
  • しばらく行ってなかった RubyKaigi にまた行ってみようかと思ってます。
  • YAPC::Asia Tokyo は行ったことなかったんですけど、楽しそうなので行ってみたいです。
  • 予定が合えば OSC にも行きたいです。

ということで 2015年もよろしくおねがいします。

Postfix Advent Calendar 2014

これはPostfix Advent Calendar 2014の26日目の記事…じゃなくてまとめです。

11月下旬、今年もどっかのアドベントカレンダーに記事を書こうかとQiita の Advent Calendar 2014を眺めてたんですが、Postfix が無いことに気がつきました。

無いなら自分で作ろうかと思ったんですけど、Postfixで25日分もネタが集まるとは思えなかったし、自分で全部埋められるとも思わなかったので、どうしようかなーと考えてたら、上島竜兵風に「押すなよ!絶対押すなよ!」と言ってる御仁を見つけたので、

押してみました。

はじめはどうなることかと思ってたのですが、終わってみるとちゃんと全枠埋まって良かったです。

まだ全部はちゃんと読めてないのですが、自分の知らなかったことが書かれた記事がいくつもありました。

ご参加頂いた皆様ありがとうございました。

  1. Postfix の DB ファイルを一括生成・更新
  2. Postfix の main.cf のフォーマットについて
  3. Postfix で IPv6 無効化
  4. Postfix の拡張メールアドレス
  5. Postfix 2.10 の smtpd_relay_restrictions 新設の背景
  6. 7bit と 8bit の狭間で
  7. Postfix の詳細ログを採取する
  8. postfix + milter + PostfixAdmin + Roundcube で作るメールサーバ
  9. parent_domain_matches_subdomains
  10. milterプロトコル
  11. Postfixでのサブミッションスパムの簡易対策方法
  12. Postfixの証明書認証アレコレ
  13. sendmailコマンドによるメール発信
  14. Amazon LinuxでPostfix/PostgreSQLを用いたPostfixAdmin構築
  15. Postfix から MySQL を使う
  16. Postfix 2.12 の compatibility_level
  17. 中継メールサーバーの高可用性・負荷分散に対応する
  18. postscreenってどんなもの?
  19. UNIXユーザーネームサービス障害時の宛先存在確認問題と対策
  20. セカンダリメールサーバの設定(認証サーバが落ちても動くようにプレインテキストにユーザ一覧を記入する構成)
  21. 1通のメールに対して procmail と Sieve の処理を両立させる
  22. Postfix aliasesとS3とSWFでメール受信時にイベントドリブン的に堅く処理する
  23. 危ないPostfix+MySQL構成
  24. CentOS7.0/64bitでメールサーバ構築
  25. VERP でメール不達エラーの宛先アドレスを識別する