【Golang】引数で渡した複数のストリームを行単位でマージしリアルタイム出力するツール作った

ツール書きました。

github.com

例えば以下のような3つのコマンドを同時に実行して、結果をリアルタイムでマージして出力したいとします。

1 (sleep 1;echo SlowSlow)
2 (sleep 0;for i in $(seq 1 25);do printf aaa;echo;done) 
3 (sleep 0;for i in $(seq 1 25);do printf bbb;echo;done)

SlowSlow の行は1秒待ってから出力なので、実際に受け取るタイミングを考えれば以下のような結果を得たいことになります。

aaa
bbb
bbb
aaa
...
...
bbb
aaa
SlowSlow

simple bash

bashでもできそうなのでやってみると、

((sleep 1;echo SlowSlow) & \
 (sleep 0;for i in $(seq 1 25);do printf aaa;echo;done) & \
 (sleep 0;for i in $(seq 1 25);do printf bbb;echo;done))

ab\n 改行が混ざってしまいます。

aaa
baaa
aaabaaa
\n
aaabaaa
\n
baaaaaa
\n
baaa
...
...
aaaSlowSlow

cat

cat を使えば行はキレイに出力されますが、引数に渡されたストリームを前から順番に処理するので、それぞれ前のストリーム待ちでブロックしてしまいます。

cat <(sleep 1;echo SlowSlow) \
 <(sleep 0;for i in $(seq 1 25);do printf aaa;echo;done) \
 <(sleep 0;for i in $(seq 1 25);do printf bbb;echo;done)  
SlowSlow  # 1秒待ってから出力
aaa            # ブロックされる。1つ目の出力SlowSlowを待って出力
aaa
...
...
bbb          # ブロックされる。2つ目のaaaが全て出力されるのを待って出力
bbb
...

paste

pasteというツールもあります。

paste -d \\n <(sleep 1;echo SlowSlow) \
 <(sleep 0;for i in $(seq 1 25);do printf aaa;echo;done) \
 <(sleep 0;for i in $(seq 1 25);do printf bbb;echo;done)  

3つの入力から1行受け取るのを待って一斉に出力します。つまりは、

SlowSlow\naaa\nbbb\n -> 出力 aaa\nbbb\n -> 出力 aaa\nbbb\n -> 出力

実際に打つと以下のようになります。

SlowSlow # 1秒待ってから出力
aaa           
bbb
aaa
bbb
...
...
aaa
bbb

linecmb

今回作ったlinecmbでは期待した結果が得られるはずです。

linecmb <(sleep 1;echo SlowSlow) \
 <(sleep 0;for i in $(seq 1 25);do printf aaa;echo;done) \
 <(sleep 0;for i in $(seq 1 25);do printf bbb;echo;done)  

実はこのツール参考にしたQiitaの記事にも書かれているfdlinecombineでも同じことができます。

github.com

しかもfdlinecombineの方が早い。頑張ってスピード上げたのですが追いつきませんでした。

開発中に、長い長い行を読み込むときに小さなReadBufferで頻度を上げて読み込むと、途中でファイルディスクリプタが切り替わってしまうという事象が発生しました。毎回ではなくて数十回に1回、かつgoroutineで並列度上げて10以上ののストリーム読んだときなどです。ファイルディスクリプタの番号をキーとして行を検出しているのですが、ここ切り替わってしまうと同じストリームかどうかの判断ができないといいう課題が。。。自分が知らないだけで詳しい人には当たり前の事象なのかもですがここも苦労しました。結論少しスピード犠牲にしても安定を選びました。

1行がそこまで長くないデータを扱うなら、この辺り下げるとパフォーマンス上がります。 https://github.com/yomon8/linecmb/blob/v0.1.0/readworker/readworker.go#L12

ReadStringやScannerを使って書き始めたのですが、パフォーマンスが思うように出ずにだんだん低いレイヤのAPIを使っていって、systemcallまで多用して書いてみたのですが、今度はLinuxMacの互換が取れなくなり、少しレイヤあげてReadとWriteで落ち着いています。それでも、特にMacのファイルディスクリプタ周りはの動きは初めて触ったし、情報も少なくて厳しかったです。途中でそもそもそれならCで書けばいいじゃんとなったり。。。他にもまだまだ改良の余地はあるでしょうが、それは追々。

便利な利用方法

早速使って便利だと思ったのはスケールアウトで複数ならんでるWEBサーバの情報を取得したりするコマンド。

linecmb <(ssh server1 -C "tail -f /path/to/file") <(ssh server2 -C "tail -f /path/to/file") <(ssh server3 -C "tail -f /path/to/file")

ちなみにログ見るときには更にこれ組み合わせています。

Bashのパイプから受け取ったテキストで複数の単語をハイライト表示させる - YOMON8.NET

参考記事

bashでストリームデータ処理 - Qiita