abrtdを使用している時は `ulimit -c` は効かない

abrtdを使用している時は ulimit -c は効かない

背景

Postfixの認証を担っているsaslauthdがクラッシュしてコアを吐いたのだが、設定が悪く保存されなかった。 再発した時にちゃんとコアが保存されるように設定する過程でネットで調べた情報が一部自分の環境(CentOS release 6.6)には適用されないことがわかった。

課題

表題の通り ulimit -cによるcore file sizeの上限が適用されないことが分かった。 soft limitの値が0、つまりコアを保存しない、という設定は適用されない。

結論

結果から言えば、CentOSはコアをabrtdというデーモンが受け取ってファイルに書き込む設定になっている。 abrtdが生成するコアのファイルサイズはulimitが指定する値は適用されない。

説明

以下のようにsaslauthdの Max core file size のsoft limitは0、hard limitはunlimitedである。 ということは本来saslauthdのコアは破棄されることになる。

$ ps aux | grep saslauth[d]                                                                                                            
root      2710  0.0  0.0  69948  1088 ?        Ss   20:06   0:00 /usr/sbin/saslauthd -m /var/run/saslauthd -a httpform -c -t 60
root      2712  0.0  0.0  69948   772 ?        S    20:06   0:00 /usr/sbin/saslauthd -m /var/run/saslauthd -a httpform -c -t 60
root      2713  0.0  0.0  69948   772 ?        S    20:06   0:00 /usr/sbin/saslauthd -m /var/run/saslauthd -a httpform -c -t 60
root      2714  0.0  0.0  69948   772 ?        S    20:06   0:00 /usr/sbin/saslauthd -m /var/run/saslauthd -a httpform -c -t 60
root      2715  0.0  0.0  69948   772 ?        S    20:06   0:00 /usr/sbin/saslauthd -m /var/run/saslauthd -a httpform -c -t 60
$ sudo cat /proc/32411/limits | grep core
Max core file size        0                    unlimited            bytes  

しかし以下のように SIGABRT をプロセスに入れるとコアが生成される。

$ kill -ABRT 2710 
$ sudo tail /var/log/messages                                                                                                           
Jan 23 20:08:24 test001 abrtd: Directory 'ccpp-2017-01-23-20:08:24-2710' creation detected
Jan 23 20:08:24 test001 abrt[2756]: Saved core dump of pid 2710 (/usr/sbin/saslauthd) to /var/spool/abrt/ccpp-2017-01-23-20:08:24-2710 (1101824 bytes)

生成される事自体は問題ないが、 ulimit -c の値が適用されないのは気持ちが悪い。

調べた過程は省くがCentOSの場合は以下のような設定が適用されているため効かないようだ。

$ cat /proc/sys/kernel/core_pattern 
|/usr/libexec/abrt-hook-ccpp %s %c %p %u %g %t e

これでabrtdにコアが渡されている。

https://linuxjm.osdn.jp/html/LDP_man-pages/man5/core.5.html

このファイルの最初の文字がパイプ記号 (|) であれば、 その行の残りの部分は実行するプログラムとして解釈される。 コアダンプは、ディスク上のファイルに書き込まれるのではなく、 プログラムの標準入力として渡される。 以下の点に注意すること。 

このあたりでコアを標準出力でパイプを介してabrtdに渡しているから core file size が効かない(ファイルじゃないから)ということが薄々わかる。

あとはRedhatのabrtdのマニュアルを少し読み込むと以下のような記載がある。 https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/6/html/Deployment_Guide/sect-abrt-configuration-abrt.html

MakeCompatCore = yes/no
    This directive specifies whether ABRT's core catching hook should create a core file, as it could be done if ABRT would not be installed. The core file is typically created in the current directory of the crashed program but only if the ulimit -c setting allows it. The directive is set to yes by default. 

Coreとの互換性をもたせるオプション MakeCompatCore がyesの場合は ulimit -c が許可する場合だけコアを吐くようにできるとのこと。 つまり互換性を設定で有効にしない限り core file size の制限は効かないのが普通の動作だよ、ということである。

よく見ると、デフォルトでyesって書いてあるけど・・・? まぁいいか。

powerdns+dnsdistの構築

powerdns+dnsdistの構築

概要

もともとpdns(権威DNS)が動作していた環境にdnsdistを導入した。 以下のような構成。

       +
       | 53/udp
       |
+--------------+
| +----v-----+ |
| |          | |
| |  dnsdist | |
| |          | |
| +----+-----+ |
|      | 10053/udp
| +----v-----+ |
| |          | |
| |  pdns    | |
| |          | |
| +----------+ |
+--------------+

大量のクエリでpdnsがパンクするという障害を起こしたため、それの対策としてdnsdistによるQuery Per Second(QPS)の制限を入れた。

苦労した点

geobackend(既に開発が止められている拡張機能)にリゾルバのIPが伝わらないという問題を回避するための設定に苦労した。

geobackendはリゾルバのIPが所属する地域毎にレスポンスを変えることができる機能だが、pdnsが受け取るDNSリクエストはdnsdist経由で受け取るため、接続元IPアドレスがdnsdistが実行されているホスト(上記の構成ではlocalhost)のものになってしまう。

この問題はEDNS0をdnsdistとpdns間で強制することで解決できた。 EDNS0の一部であるedns-client-subnet (ECS)はRFC7871で定義されており、このECSが有効なリクエストであればdnsdist越しであってもpdnsがクライアント(リゾルバ)のIPを特定できる。 またgeobackendは以下のissueでECSに対応していることが確認できた。 https://github.com/PowerDNS/pdns/issues/1482

そして、dnsdist上のECSを有効化する設定方法については以下のML上のやり取りが非常に参考になった。 https://mailman.powerdns.com/pipermail/dnsdist/2016-September/000216.html

You can pass the full client IP address using EDNS0 client subnet extension (https://tools.ietf.org/html/draft-ietf-dnsop-edns-client-subnet-08).
newServer({ ... useClientSubnet=true .. })
setECSOverride(true)
setECSSourcePrefixV4(32)
setECSSourcePrefixV6(128)

dnsdistのインストール

dnsdistのインストールはrpmで入れたが、公式が案内する通りの方法でインストールできたため、ここでは書かない。

設定

pdns

  • ECSを有効化(edns-subnet-processing)
  • pdnsを53から10053に変更(local-port)
$ diff pdns.conf pdns.conf.20161221 
80c80
< edns-subnet-processing=yes
---
> # edns-subnet-processing=no
135c135
< local-port=10053
---
> # local-port=53

dnsdist

  • ECS有効化および強制(useClientSubnet=True, setECS*)
  • QPSの制限(addAction(MaxQPSIPRule())
-- balancing all packets to local pdns server
newServer({address="127.0.0.1:10053", useClientSubnet=true)

-- EDNS settings
setECSOverride(true)
setECSSourcePrefixV4(32)
setECSSourcePrefixV6(128)

-- enable control socket for dynamic configration
controlSocket("0.0.0.0")

-- allow access from everyone
addACL("0.0.0.0/0")

-- QPS for remote clients
addAction(MaxQPSIPRule(100, 28, 64), DropAction())

動作確認

以下のように外部ホスト上のリゾルバ経由でednsが有効/無効なクエリを行い、ログを確認する。

$ dig ${geobackendが効いているレコード} +noedns
$ dig ${geobackendが効いているレコード} +edns=0

ednsが有効か無効かにかかわらず、以下のようなログがでる。 /var/log/messages

Dec 27 17:31:52 dns001-v pdns[9116]: Remote 127.0.0.1<-x.x.x.x/32 wants '***|TXT', do = 0, bufsize = 512: packetcache MISS
Dec 27 17:31:52 dns001-v pdns[9116]: [geobackend] Serving *** CNAME *** to x.x.x.x/32 (392)

noednsでクエリを投げてもdnsdistとpdnsの間はEDNS0が強制されることを確認できた。

番外編: powerdnsとEDNS0とTCPフォールバックの関係

DNSを運用する上では当然の知識らしいが、 DNSUDPで通信を行い、最大長は512bytesであるらしい。 512bytes以上のDNSへのリクエストはTCPフォールバック(UDPではなくTCPで通信を行う)を使用するか、EDNS0で拡張されたプロトコルを使用する必要がある。

powerdnsはEDNSがデフォルトで有効になっているので、512bytes以上の通信をEDNSを使ってUDPで行うことができるが、実は1680bytes以上の通信はTCPフォールバックさせる。

これは以下に記載されているとおりリフレクション攻撃の影響を小さくするためであるらしい。

https://doc.powerdns.com/md/authoritative/settings/

udp-truncation-threshold

    Integer
    Default: 1680

EDNS0 allows for large UDP response datagrams, which can potentially raise performance. Large responses however also have downsides in terms of reflection attacks. Up till PowerDNS Authoritative Server 3.3, the truncation limit was set at 1680 bytes, regardless of EDNS0 buffer size indications from the client. Beyond 3.3, this setting makes our truncation limit configurable. Maximum value is 65535, but values above 4096 should probably not be attempted.

udp-truncation-thresholdの値を大きくすれば(最大で4096bytes?)より大きなサイズの通信をUDPを介して行うことができるようになる。

pylruをマルチスレッドで使うと「KeyError:」が発生する

githubにissueがないのでここに書く。

以下のような2スレッドで同時にlrucacheに書き込むとエラーが起きる。

test.py

import pylru
import threading
import random

cache = pylru.lrucache(1024)

def f():
    while True:
        key = 'key %s' % (random.randint(1,100000))
        val = 'value'
        cache[key] = val

def main():
    th_1 = threading.Thread(target=f, name="th_1")
    th_2 = threading.Thread(target=f, name="th_2")
    th_1.start()
    th_2.start()

if __name__ == '__main__':
    main()

version 1.0.3の場合

$ python test.py
Exception in thread th_1:
Traceback (most recent call last):
  File "/usr/lib64/python2.6/threading.py", line 532, in __bootstrap_inner
    self.run()
  File "/usr/lib64/python2.6/threading.py", line 484, in run
    self.__target(*self.__args, **self.__kwargs)
  File "test.py", line 11, in f
    cache[key] = val
  File "/usr/lib/python2.6/site-packages/pylru-1.0.3-py2.6.egg/pylru.py", line 145, in __setitem__
    del self.table[node.key]
KeyError: 'key 41240'

version 1.0.9の場合

$ python test.py                                                                                                                                                                     
Exception in thread th_1:
Traceback (most recent call last):
  File "/usr/lib64/python2.6/threading.py", line 532, in __bootstrap_inner
    self.run()
  File "/usr/lib64/python2.6/threading.py", line 484, in run
    self.__target(*self.__args, **self.__kwargs)
  File "test.py", line 11, in f
    cache[key] = val
  File "/usr/lib/python2.6/site-packages/pylru-1.0.9-py2.6.egg/pylru.py", line 145, in __setitem__
    del self.table[node.key]
KeyError: 'key 82904'

ロックをかければ問題が発生しなくなる。 ただロックが大きすぎると性能がでなくなるわけでどう修正したらベストだろうか。明日以降で取り組む。

2016/12/12 追記

Python 3.2からlru_cacheが標準で提供されているので、おとなしく2系から3系に乗り換えることにしよう。 それまではワークアラウンドで。 10.2. functools — 高階関数と呼び出し可能オブジェクトの操作 — Python 3.6.5 ドキュメント functoolsの実装を見るとRLockをつかっているようだ。

曖昧にならない後方参照の記述方法

python正規表現を使って置換処理をするときに、後方参照で少し困ったことが起きた。
一般的に使われる「\1」のような後方参照の直後に数字を続けると当然だけど後方参照の数字がどこまでなのかわからなくなる、というものだ。
解決方法は簡単で「\1」ではなく、「\g{1}」とする。

以下のようなケースは問題ない。

>>> import re  
>>> re.sub(r'(.*) (.*)', r'\1-\2', 'hoge fuga')
'hoge-fuga'

以下のようにすると問題が起きる。

>>> re.sub(r'(.*) (.*)', r'\1-\2012345', 'hoge fuga')
'hoge-\x812345'

解決は以下のようにする。

>>> re.sub(r'(.*) (.*)', r'\g<1>-\g<2>012345', 'hoge fuga')
'hoge-fuga012345'

公式のドキュメントにも書いてある。
http://docs.python.jp/2/library/re.html

pdnsを適当にチューニング

pdnsのチューニングは以下のページが参考になる。
https://doc.powerdns.com/md/authoritative/performance/

細かくやればきりがないと思うけど、receiver-threadsが一番効果がでそうなので、そこからやってみる。

チューニングのきっかけとしては、バックエンドのmysqlへのレイテンシーが非常に大きい環境があり、
その環境にあるDNSサーバがパンクしかけていたことだった。

レイテンシーが大きい場合は並列数を増やすことが効果的だと思ったのだ。

まずは設定値を変えてみる。デフォルトはreceiver-threads=1となっている。

$ sudo diff pdns.conf pdns.conf.20160908                                                                                  
251d250

< receiver-threads=3

以下のシェルスクリプトを使って検証を実施。
gist.github.com

receiver-threadsを上げてテストすると12ぐらいで最良の結果が得られた。
検証の方法が正しいかは保証できないが、適当にチューニングするぐらいならこれでよいのではないかと思う。

上記の公式ページだとreceiver-threadsは3,4ぐらいが適切と書いてあるが、
DBに対する通信のレイテンシーが大きい環境ではそうではないようだ(当たり前か)

2016-01-30 更新

`dnsperf`が大変便利なのでこんなシェル要らないことに気づいた。

pythonのsftpserverを試す

pythonで書いているバッチのテスト用にsftpサーバを簡単に立ち上げたかった。
探してみるとちょうどよいsftpseverというものを見つけたので使い方を含めてメモ。
GitHub - rspivak/sftpserver: A simple single-threaded SFTP server

gist.github.com

ニフティクラウドのロードバランサをAnsibleから操作できるようにしてみた

AWSならbotoと連携するモジュールをAnsibleが正式に提供しているが、
ニフティクラウドSDKがそもそもPythonに対応していないので、Ansible対応は難しい。

ということでAnsibleに対応するために2点実装した。
(1)botoをニフティクラウド用に修正
(2)Ansible用のモジュール作成

(1)はやり始めると途方がないので、ロードバランサからインスタンスの装着/取外しができるところまで実装した。

以下の様なplaybookを書くと動く
gist.github.com


(1)
github.com


(2)
gist.github.com