perlワンライナー集計処理

以下の様なテキストを入力として受け、出力を合計値にしたい。

a 10000
b
c 33
b 100
a 3993

出力

c 33
a 13993
b 100

シェルスクリプトで実装する方法も考えたが、なんだか面倒なので、
なれないPerlワンライナーにチャレンジ

以下を参考にした。
Perlワンライナー覚書」tossh
http://qiita.com/tossh/items/f8d448c0c039f68c0ea3

結論としては以下とすればよい。

$ cat << EOF | perl -anle '$count{$F[0]}+=$F[1]; END{foreach(keys(%count)){print "$_ $count{$_}"}}'
> a 10000
> b
> c 33
> b 100
> a 3993
> EOF
c 33
a 13993
b 100

勉強したこと

perlワンライナーを各ときはオプションが重要

-a AWK的な感じで入力を区切り文字(スペース)で分割して変数で扱えるようにする。
-n インプットを一行ずつ処理する
-l printの文字列に改行を明示的に書かなくてよい
-e ワンライナーを指定するために必須

ハッシュは初期化しなくても良い。既知のキーが入ってなくても参照できるし、+=を使った場合に0が入っていたかのように振る舞う。
Perlはよくわかっていないが、ワンライナーのために頑張っている感がある。

「ls: 引数行が長すぎます」を回避する

ls: 引数行が長すぎます

ファイルが大量にあるディレクトリをワイルドカードを使用してlsすると以下のように怒られる。

  • ダメな例
$ rm /hoge/fuga/??/*/*/hige/
ls: 引数が多すぎます 

エラーの文字列でググると、回避方法としてfindとxargsの組み合わせが一般的らしい。 http://moviesearch1.blog15.fc2.com/blog-entry-8.html

【例】
find /var/www/html/movie-search/cache/ -name "*.cash" -print0 | xargs -0 rm -rf

エラーはシェルがワイルドカードを展開してコマンドに渡す際に上限値を超える場合に発生する。

上記の例だとfindの-nameで渡しているワイルドカードはシェルでは解釈されず、findコマンドが展開を担当しているので、問題を回避できる。

「ダメな例」の場合はこの方法を使えない。なぜなら、ディレクトリ階層がネストしていて、それを-nameできれいに吸収できないからだ。 やるとして以下の様な汚い感じになるだろう。

  • 回避例1
find  /hoge/fuga/ -name "??/*/*/hige/" -print0 | xargs -0 rm -rf

直感的じゃないし、余計なディレクトリを探しに行くだろう。

更に調べると、どうやらechoとxargsの組み合わせで回避できるらしい。 http://x68000.q-e-d.net/~68user/unix/pickup?xargs

echo はシェルの内部コマンドのため ARG_MAX の制限を受けない。一方、xargs は ARG_MAX の値を把握していて、引数が ARG_MAX を越えそうになると

バッドノウハウ感があるが採用する。

echoとxargsの組み合わせで「ダメな例」を回避するには以下のようにする。

  • 回避例2
$ echo /hoge/fuga/??/*/*/hige/ | xargs rm -rf

回避例1にくらべてだいぶマシなのがわかる。

xargs -I{}を使うとき

上記の例のrmをmvに書き換えるともう一つの問題に直面する。

  • ダメな例2
$ echo /hoge/fuga/??/*/*/hige/ | xargs -I mv {} {}.bk
xargs: 引数行が長すぎます

上記ではうまくいっていたxargsだが、-I{}を使用した途端が引数が長すぎると文句を行ってきた。 これはいかに??

困ったらmanを読む。以下のように書いてある。 http://linuxjm.osdn.jp/html/GNU_findutils/man1/xargs.1.html

-I replace-str
    xargs が実行するコマンドに対してユーザが引き数 (すなわち initial-arguments) を指定したとき、その initial-arguments 中にある replace-str の部分すべてを、標準入力から読み込んだ名前で 置き換える。 なお、空白は、クォートされていない場合も、入力される項目の区切りには ならない。区切り記号は改行文字だけになるのだ。 -x と -L 1 が自動的に設定される。 

「区切り記号は改行文字だけになるのだ」 サラッと書いてあるが、驚きである。 echoはファイル名を空白区切りで返すので、すべてのファイル名を一つの文字列として扱うためエラーとなる。 手っ取り早く解決するには、空白を改行に書き換えればよい。

$ echo /hoge/fuga/??/*/*/hige/ | tr ' ' '\n' | xargs -I mv {} {}.bk

この一文だけ見ると、なぜtrがいるのかわからない謎シェルになってしまうこと請け合いである。

Postfixがきりの良い時間にアクセスすると遅い

Postfixがきりの良い時間にアクセスすると遅い

背景

ユーザから「cronがキックするスクリプトがメールを送るとPostfixからの応答が遅くタイムアウトが起きている」と報告された。

結論

nslcdのレスポンスが遅いことが原因だった。 きりの良い時間はLDAPのアクセスが多く、負荷が高まるためレスポンスが遅くなっている。 対象のPostfixは動作上LDAPが必要なく、nslcdがLDAPの応答がない場合にさっさとタイムアウトするようにすることで改善した。

調査

最初はmilter、saslauthdを疑っていたのだが、両者を外した状態で検証しても改善しない。 master.cf, main.cfのパラメタも変えてみたが改善しない。 回りくどいことはやめて、straceでプロセスを追ってみた。

$ ps auxww | grep master
root     12984  0.0  0.0 107452   948 pts/1    S+   16:07   0:00 grep master
$ sudo strace -tt -s 2048 -f -p 12984 -t > strace.20151105 2>&1 
1:35:01 clone(Process 9798 attached
[pid 12984] 21:35:01 clone(Process 9799 attached

9799と9788はmasterがcloneを実行して作成したプロセス。

[pid  9799] 21:35:01 open("/lib64/libnss_ldap.so.2", O_RDONLY) = 1

ldapライブラリを読み込んでいる

[pid  9799] 21:35:01 execve("/usr/libexec/postfix/smtpd", ["smtpd", "-n", "465", "-t", "inet", "-u", "-o", "stress=", "-s", "2", "-o", "smtpd_sasl_auth_enable=yes", "-o", "smtpd_sasl_security_options=noanonymous"], [/* 4 vars */]
 <unfinished ...>

9799はmasterからsmtpdに転身する。

9799だけに関して見ると以下のように1秒から14秒まで飛んでいる。

[pid  9799] 21:35:01 munmap(0x7fc9a8b3b000, 34862) = 0
[pid  9799] 21:35:01 socket(PF_FILE, SOCK_STREAM, 0) = 10
[pid  9799] 21:35:01 connect(10, {sa_family=AF_FILE, path="/var/run/nslcd/socket"}, 110) = 0
[pid  9799] 21:35:01 select(1024, NULL, [10], NULL, {9, 999999}) = 1 (out [10], left {9, 999995})
[pid  9799] 21:35:01 sendto(10, "\1\0\0\0\213\23\0\0\7\0\0\0postfix", 19, MSG_NOSIGNAL, NULL, 0) = 19
[pid  9799] 21:35:01 select(1024, [10], NULL, NULL, {59, 999999} <unfinished ...>
[pid  9799] 21:35:14 <... select resumed> ) = 1 (in [10], left {46, 803711})
[pid  9799] 21:35:14 read(10, "\1\0\0\0\213\23\0\0\3\0\0\0", 1024) = 12
[pid  9799] 21:35:14 close(10)          = 0

/var/run/nslcd/socketにデータを送りつけるところで止まっているように見える。 nslcdはLDAPでユーザ情報を引くときに通信するデーモンらしい。 http://arthurdejong.org/nss-pam-ldapd/nslcd.conf.5

Postfixという名前のユーザはローカルに存在するので、LDAPに聞きに行く必要は無いはずだがよくわからない。 /etc/nsswitch.conf上もfilesが優先されていた。

とりあえず、以下のようにnslcdのタイムアウト値を減らしたところ、レスポンスが改善した。

bind_timelimit 2
timelimit 3

ちょっと釈然としないが、よいとしよう。

キーボードってそろそろ進化するべきなんじゃないだろうか

スマホのフリックは好きだ。でもキーボードの入力速度にはかなわない。
いつでもキーボードを持ち運べて、どこでも使えるならキーボードを使うだろうけど、そうはいかない。
キーボードは、まずどこかに置かないと行けないし、幅を取る。

キーボードが現在の形に落ち着いてからしばらく立つんだけど、そろそろ進化するべきじゃないかな?

なんとなく検索していたらkeygloveというものを見つけた。makezine.jp

手袋型のキーボードと呼んだらいいのだろうか。
これならスキー場でも使うこともできるだろう。
ちょっとごてごてしているのがネックだ。

今後に期待。

手のひらキーボードなるものもあるらしいgigazine.net

発送がキーボードから抜けていない気もするけど、応用の幅は広そうだ。
脳に限らず、人体の信号をセンサーにできればいいのだろうけど。
神経から信号を取り、逆にフィードバックを神経に与えるようなもの。失敗すると痛そうだけど。


prosheet.jp
指輪デバイスもある。これはキーボドの代わりにはならないだろうなぁ。

IPの偽装

検証目的で接続元のIPを偽装したい場合にループバックデバイスIPアドレスを変更するという方法がある。
参照:http://thesimplesynthesis.com/post/how-to-spoof-your-ip-address-to-test-geolocation


$ sudo ifconfig lo 218.xxx.xxx.xxx
$ telnet 218.xxx.xxx.xxx 25
$ sudo less /var/log/maillog
Aug 24 17:59:03 dev001 postfix/smtpd[27156]: disconnect from unknown[218.xxx.xxx.xxx]
Aug 24 17:59:21 dev001 postfix/smtpd[27160]: connect from unknown[218.xxx.xxx.xxx]

dockerのAmbassadorパターンを理解する

前回前々回で「単一のコンテナの起動」と、「2台のコンテナの起動と接続」について、ある程度理解した。 今回は2台のホスト(VM)にまたがるコンテナの接続について試す。 2台のホストにまたがる場合は、--linkによる接続はできない。これはひとつのホストにのっているDockerの中でのみ使える機能だからである。

参照: Dockerコンテナ接続パターン (2014年冬)

まず定石はAmbassadorパターンというものらしい。 他のホストとの接続だけを担うプロキシ機能しか持たないコンテナで軽量なものらしい。 ほかにも、いろいろあるらしいが、ひとまず定石であるAmbassadorパターンを理解しよう。

ちゃんとした例を考えるのが面倒くさいので、 今回も前回に引き続きhttpdとclientを例に使う。

構成は以下

ホスト コンテナ名 役割 expose/publishするポート リンク
VM1 httpd apacheサーバ 80 なし
httpd-amb ambassador 8080(publish) httpdコンテナにリンク
VM2 httpdclient クライアント なし httpdclient-ambコンテナにリンク
httpd-amb ambassador 80(expose) なし
ホスト IP
VM1 192.168.11.11
VM2 192.168.11.12

構成図

VM1[ httpdclient --- 80/tcp ---> httpd-amb ] --- 8080/tcp --> VM2[ httpd-amb -- 80/tcp --> httpd]

VM1: apacheサーバ+ambassadorを構築する

apacheサーバコンテナ作成する

# docker run --privileged -d --expose 80 --name httpd centos:centos7 /sbin/init
# docker exec -it httpd /bin/bash
[root@a4522c59ed0c /]#
[root@a4522c59ed0c /]# yum install httpd -y
[root@a4522c59ed0c /]# systemctl enable httpd.service
[root@a4522c59ed0c /]# systemctl start httpd.service
[root@a4522c59ed0c /]# exit

Ambassadorコンテナを作成する

Ambassadorコンテナの作成はイメージを持ってきて、あとはhttpdコンテナにlinkするだけで他に設定は不要。超簡単。

# docker run -d --link httpd:httd-alias --name httpd-amb -p 8080:80 svendowideit/ambassador

これでAmbassadorが8080ポートで受けた接続はコンテナ内では80ポートで受けて、それをhttpdの80ポートへそのまま流すことができるようになる。

現状確認。2つコンテナが実行されている。

# docker ps
CONTAINER ID        IMAGE                            COMMAND                CREATED             STATUS              PORTS                NAMES
c06aaa97b55a        svendowideit/ambassador:latest   "\"/bin/sh -c 'env |   30 seconds ago      Up 29 seconds       0.0.0.0:80->80/tcp   httpd-amb           
9c4495ffddfd        centos:centos7                   "/sbin/init"           4 minutes ago       Up 4 minutes        80/tcp               httpd 
# telnet localhost 8080
GET /

VM2: httpクライアント+ambassadorを構築する

Ambassadorコンテナを作成する

docker run -d --name httpd_ambassador --expose 80 -e HTTPD_PORT_80_TCP=tcp://192.168.11.11:8080 svendowideit/ambassador

環境変数HTTPD_PORT_80_TCP=tcp://192.168.11.11:8080を渡しているのが肝だと思われる。 多分Ambassadorは渡された環境変数をパースして自分がプロキシすべきポートを判断する。(かなり適当な予想なので間違えっているかも。)

httpdクライアントを作成する

# docker run --privileged -d --link httpd_ambassador:httpd_ambassador-alias --name httpdclient centos:centos7 /sbin/init

現状確認。2つのコンテナが実行されている。

# docker ps
CONTAINER ID        IMAGE                            COMMAND                CREATED             STATUS              PORTS               NAMES
bcabc10a7506        centos:centos7                   "/sbin/init"           14 minutes ago      Up 14 minutes                           httpdclient         
e526511520fa        svendowideit/ambassador:latest   "\"/bin/sh -c 'env |   17 minutes ago      Up 17 minutes       80/tcp              httpd_ambassador

VM2: httpdクライアントからAmbassadorの80番ポートにアクセスしてWebページをゲットする。

準備が整ったので、ホストを隔てたhttpdへのアクセスができるか試す。

流れ - VM2のhttpdclientコンテナの中でbashを実行する - 環境変数からAmbassadorのIPアドレスを取得する - Ambassadorの80ポートへcurlでアクセスする

# docker exec -it httpdclient /bin/bash

[root@bcabc10a7506 /]# env
HTTPD_AMBASSADOR_ALIAS_PORT_80_TCP=tcp://172.17.0.26:80
HOSTNAME=bcabc10a7506
HTTPD_AMBASSADOR_ALIAS_PORT=tcp://172.17.0.26:80
HTTPD_AMBASSADOR_ALIAS_ENV_HTTPD_PORT_80_TCP=tcp://192.168.11.11:8080
HTTPD_AMBASSADOR_ALIAS_PORT_80_TCP_PORT=80
LS_COLORS=
HTTPD_AMBASSADOR_ALIAS_PORT_80_TCP_PROTO=tcp
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
PWD=/
container_uuid=bcabc10a-7506-1b55-74a8-7c9f0d2ec7dc
SHLVL=1
HOME=/root
HTTPD_AMBASSADOR_ALIAS_NAME=/httpdclient/httpd_ambassador-alias
LESSOPEN=||/usr/bin/lesspipe.sh %s
HTTPD_AMBASSADOR_ALIAS_PORT_80_TCP_ADDR=172.17.0.26
_=/usr/bin/env

[root@bcabc10a7506 /]# curl -s http://172.17.0.26 | head -1

いけた! まぁあんまりAmbassadorのありがたみが無い感もある. どちらかというと、Ambassadorの使い方がわかっただけ。

docker --link --exposeの動きを理解する

dockerの--linkとかEXPOSEとか接続関係のコマンドがよくわからないので、動かしながら理解する。

参照: Dockerのネットワークの基礎

  • --link 他のコンテナのポートにつなぐ
  • --expose コンテナのポートを他のコンテナに晒す
  • --publish コンテナのポートをホストのポートにマッピングする

試しに2台のcentosのコンテナを作って、 一台にはapacheをインストールし、もう一台からそのapacheの80番ポートを叩いてみる。 --publishについては言及しない。

コンテナの名前は以下

コンテナ名 役割 exposeするポート リンク
httpd apacheサーバ 80 なし
httpdclient クライアント なし httpdコンテナにリンク

apacheコンテナを作成する。(1台目)

# docker run --privileged -d --expose 80 --name httpd centos:centos7 /sbin/init
# docker exec -it httpd /bin/bash
[root@a4522c59ed0c /]#
[root@a4522c59ed0c /]# yum install httpd -y
[root@a4522c59ed0c /]# systemctl enable httpd.service
[root@a4522c59ed0c /]# systemctl start httpd.service
[root@a4522c59ed0c /]# exit

一応確認

[root@centos001 ~]# docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
25fcc3bcc9b8        centos:centos7      "/sbin/init"        44 seconds ago      Up 43 seconds       80/tcp              httpd

ただのcentosを作成する。(2台目)

# docker run --privileged -d --link httpd:httpd-alias --name httpdclient centos:centos7 /sbin/init

--linkはコンテナ名:エイリアス名というフォーマットで指定する。

一応起動していることを確認する。2台動いている。

# docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
44e11aeaea7c        centos:centos7      "/sbin/init"        9 minutes ago       Up 9 minutes                            httpdclient         
25fcc3bcc9b8        centos:centos7      "/sbin/init"        14 minutes ago      Up 14 minutes       80/tcp              httpd     

作成されたコンテナの環境変数は以下のように調べられる。 この環境変数を使えば、このコンテナからapacheのポートを晒しているホストにアクセスする方法がわかる。

# docker exec -it httpdclient env
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=44e11aeaea7c
HTTPD_ALIAS_PORT=tcp://172.17.0.20:80
HTTPD_ALIAS_PORT_80_TCP=tcp://172.17.0.20:80
HTTPD_ALIAS_PORT_80_TCP_ADDR=172.17.0.20
HTTPD_ALIAS_PORT_80_TCP_PORT=80
HTTPD_ALIAS_PORT_80_TCP_PROTO=tcp
HTTPD_ALIAS_NAME=/httpdclient/httpd-alias
container_uuid=44e11aea-ea7c-97ee-6c8f-e903929a981d
HOME=/root

以下のように環境変数経由でアクセスする。

[root@44e11aeaea7c /]# curl -s http://$HTTPD_ALIAS_PORT_80_TCP_ADDR:$HTTPD_ALIAS_PORT_80_TCP_PORT/ | head -1

これでコンテナ同士をつなぐことができた。