UECジャーナル

開眼☆シェルスクリプト サーバにデータを渡して処理させる―nc、ssh、scpを使う

ガンカーズのUNIX哲学には、主要な9か条のほかに10か条がある。その中に次のようなものがある。

「90パーセントの解決を模索せよ」

プログラムを書いたり仕事をしたりすると本筋でない雑事が気になるものだが、それにはある程度目をつぶれということを言っている。

これは時短の発想であるとも解釈できる。世の中には10%が気になりすぎて、その10%のことを「最重要事項」だと思い込んでバランスの悪い主張をする人々がいる。また、それに反論ばかりしているうちにやはりそれが「最重要事項」になってしまう犠牲者も出現する。

90%ルールはその負のループから我々を救ってくれるので、覚えておいて損はないだろう。

使用する環境

次の環境を使用する。

MacBook AIR

$ uname -a
Darwin uedamac.local 12.2.1 Darwin Kernel Version 12.2.1:  (略) 
$ bash --version
GNU bash, version 3.2.48(1)-release (x86_64-apple-darwin12)
Copyright (C) 2007 Free Software Foundation, Inc.
$ 

VPS上のFreeBSD

$ uname -a
FreeBSD bsd.example.org 9.0-RELEASE FreeBSD 9.0-RELEASE #0: (略)
$ 

VPS上のUSP友の会サーバ

$ cat /etc/redhat-release 
CentOS release 6.3 (Final)
$ 

usp Tukubaiが使えるサーバ

$ cat /etc/redhat-release 
CentOS release 5.9 (Final)
$ 

友の会のサーバはwww.usptomo.comというホスト名でDNSに登録されている。FreeBSDサーバは一般公開しているサーバではないので、ここではbsd.example.orgで登録されているとしておく。

通信あれこれ

bash(1)の/dev/tcp/

まずbash(1)の提供している通信機能を紹介する。少なくともバージョン3以降のbash(1)にはリスト2の方法で特定のホストの特定のポートにfileの内容を送信する機能がある。リダイレクトの左側はecho(1)でもgrep(1)でもなんでもかまわない。

リスト2: bashで通信するときの書式

$ cat file > /dev/tcp/<ホスト名>/<ポート番号>

たとえば次のように使用する。

リスト3: Apache HTTP Serverにデータを送信する

$ echo foo > /dev/tcp/www.usptomo.com/80
$ 

USP友の会のサーバのログに次のような記録が残る。

$ tail -n 1 /var/log/httpd/access_log
123.234.aa.bb - - [03/Mar/2013:00:58:21 +0900] "foo" 301 231 "-" "-"
$ 

リスト4のように調べると分かるように/dev/tcp/はシステム側にあるわけではなく、bash(1)が擬似的にファイルに見せかけているものになっている。

リスト4: /dev/tcp/は存在しない

$ ls /dev/tcp
ls: /dev/tcp: No such file or directory
$ 

/dev/udp/も準備されているのでUDPを使うサービスにも送信できる。

Netcatを使う

bash(1)の/dev/tcp/は基本的にデータを指定したポートに送信することしかできない。送信したデータの受け手として次にNetcatを紹介する。

Netcatはnc(1)というコマンド名でインストールされていることが多い。bash(1)からテキストを送信してnc(1)で受信するには次のようにする。データは暗号化されずにそのまま送られるので秘密のものは送らないようにしよう。また、この実験をするには受信側で使うポートが開いている必要がある。

リスト5: 10000番ポートで通信する

$ nc -l 10000 > log
      

先にnc(1)コマンドを実行して受信側でポートを開いておく。上のようにnc(1)コマンドを実行すると、nc(1)コマンドが実行されたままの状態になる。次にbash(1)の機能を使ってデータを送信する。

$ echo テキストデータ > /dev/tcp/www.usptomo.com/10000
$ 

データを受信すると受信側のnc(1)コマンドの実行が終了する。生成されるファイルを確認すると、送信したデータが書き込まれていることを確認できる。

$ cat log
テキストデータ
$ 

リスト6のようにシェルスクリプトにして実行すると、ちょっとしたサービスのように振る舞う。

リスト6: whileループで何回も受信

$ cat file.sh 
#!/usr/bin/env bash

mkdir -p ./tmp/

n=1
while nc -l 10000 > ./tmp/$n.txt ; do
	n=$(( n + 1 ))
done
$ ./file.sh
      

bash(1)の機能を使ってデータを送信する。

$ echo 送信テスト1 > /dev/tcp/www.usptomo.com/10000
$ echo 送信テスト2 > /dev/tcp/www.usptomo.com/10000
$ echo 送信テスト3 > /dev/tcp/www.usptomo.com/10000
$ 
      

実行中のfile.shをCtrl-Cで終了してファイルの中身を確認する。

$ ./file.sh 
^C
$ head ./tmp/{1,2,3}.txt
==> ./tmp/1.txt <==
送信テスト1

==> ./tmp/2.txt <==
送信テスト2

==> ./tmp/3.txt <==
送信テスト3
$ 

Netcatはネットワークを扱う万能ツールとして知られており、単にポートを指定してデータを受信するだけでなく、データの送信側になったりポートスキャナになったりさまざまな用途に使用できる。

ファイルを転送する

大きなデータを扱っているケースではサーバ間で何10GBものファイルをコピーしなければいけないことがある。ネットワーク経由のファイルコピーにはscp(1)が使われることが多い。リスト7の-P 11111はUSP友の会のサーバがでフォルトの22番でなく11111番でssh接続を受け付けているという体にするために指定している。

リスト7: scp(1)でファイルをコピー

$ time scp -P 11111 TESTDATA www.usptomo.com:~/
TESTDATA                           100% 4047MB   4.0MB/s   16:48    

real	16m49.064s
user	3m2.550s
sys	13m38.727s
$ 

scp(1)には圧縮してデータを送る-Cというオプションがある。ただし圧縮はプロセッサバウンダリであるため、効果のある場合と逆効果を生む場合の両方のケースが存在する。たとえば次のケースではプロセッサバウンダリで逆に通信速度が遅くなってしまっている。

リスト8: 圧縮送信したら遅くなったケース

$ time scp -C -P 11111 TESTDATA www.usptomo.com:~/
TESTDATA                           100% 4047MB   2.6MB/s   26:16    

real	26m16.678s
user	20m33.275s
sys	6m55.593s
$ 

暗号化しなくてよいならリスト9のようにnc(1)を使って転送する方が速い。user時間はほとんどゼロとなる。

リスト9: ポートをダイレクトに使ってファイル転送

先に受信側でnc(1)を実行

$ nc -l 10000 > TESTDATA
      

bash(1)の機能でファイルを送信

$ time cat TESTDATA > /dev/tcp/www.usptomo.com/10000

real	12m3.584s
user	0m0.000s
sys	10m22.737s
$ 

プロセッサが高速で通信速度が遅いときはscp(1)の-Cオプションが有効になるが、nc(1)とgzip(1)またはbzip2(1)などを組み合わせて送った方が速いこともある。

1つの巨大なファイルを複数のサーバにコピーしたい場合はリスト10のようなことを試みてもよい。

リスト10: 1度の転送で2つのサーバにファイルをコピー

USP友の会サーバで10000番ポートからファイルへリダイレクト

$ nc -l 10000 > TESTDATA
      

FreeBSDサーバで9999番ポートからの出力をtee(1)でファイルにためながら友の会サーバにリダイレクト

$ nc -l 9999 | tee TESTDATA > /dev/tcp/www.usptomo.com/10000
      

手元のMacBook AIRからFreeBSDサーバにデータを投げる

$ cat TESTDATA > /dev/tcp/bsd.example.org/9999
$ 

この方法でサーバを数珠つなぎにすると何台ものサーバに同時にコピーができる。

ssh(1)コマンドを使ってもファイルを転送できる。次の様にssh(1)コマンドは標準入力を受け付ける。

リスト11: ssh コマンドの標準入力を使う

$ time cat TESTDATA | ssh -p 11111 www.usptomo.com 'cat > TESTDATA'

real	16m22.054s
user	2m46.163s
sys	12m44.448s
$ 

リモートマシンで計算する

たとえば今使っているマシンが遅い場合や使いたいコマンド等がインストールされていない状況を考える。自分のケースであれば、商用版のusp Tukubaiを使いたい場合やあるマシンのLaTeXの環境を使いたいというケースがこれに相当する。

リスト12にシェルスクリプトを示す。これはあるリモートのサーバにscp(1)で1千万行あるファイルを送り込み、ソートした後にソート後のファイルを戻すという処理だ。1千万行のソートはオペレーティングシステムに搭載されている通常のsort(1)コマンドでは遅すぎて使い物にならない。ここでは高速処理に注力して開発されたマルチスレッド対応のmsort(1)という商用版usp Tukubaiに含まれているコマンドを利用することで問題への対処としている。

リスト12: リモートサーバ活用事例

$ head -n 2 TESTDATA10M
2377 高知県 -9,987,759 2001年1月5日
2910 鹿児島県 5,689,492 1992年5月6日
$ cat sort.sh 
#!/usr/bin/evn bash -xv

scp -P 11111 ./TESTDATA10M usp.usp-lab.com:~/
ssh -p 11111 usp.usp-lab.com "msort -p 8 key=1 ~/TESTDATA10M > ~/ueda.tmp"
scp -P 11111 usp.usp-lab.com:~/ueda.tmp ./TESTDATA10M.sort
$ 

実行すると次のような結果が得られる。

$ time ./sort.sh 

real	4m1.717s
user	0m13.969s
sys	0m10.090s
$ head -n 2 TESTDATA10M.sort
0000 岩手県 5,630,892 2006年5月26日
0000 新潟県 1,367,399 1998年8月22日
$ 

このシェルスクリプトでは中間ファイルがリモートのサーバにできてしまっている。これを避けるにはワンライラーとしてすべてパイプで接続すればよい。

リスト13: リモートサーバを使うワンライナー

$ time cat TESTDATA10M | ssh -p 10022 usp.usp-lab.com 'cat | msort -p 8 key=1' > TESTDATA10M.sort3

real	5m0.033s
user	0m14.077s
sys	0m9.415s
$ 

これでssh(1)経由でソートした出力は、手元のMacBook AIRのターミナルの標準出力から出てくる。ssh(1)が手元のマシンの標準入出力に字を出し入れしてくれることは、ssh(1)コマンドが手元のマシンで動いているので当然と言えば当然だが、よくよく考えるととても便利なことだ。

通信回線が遅い場合には次のようにgzip(1)とgunzip(1)を使ってさらに高速化できる。

リスト14: 圧縮を挟み込んだワンライナー

$ time gzip < TESTDATA10M | ssh -p 10022 usp.usp-lab.com 'gunzip | msort -p 8 key=1 | gzip' | gunzip > TESTDATA10M.sort3

real	1m10.669s
user	0m42.874s
sys	0m2.806s
$ 

終わりに

サーバ間でデータをやりとりし処理する方法について紹介した。bash(1)の通信機能や、ssh(1)、scp(1)、nc(1)などのコマンドについて使い方を解説した。

Software Design 2013年6月号 上田隆一著、「テキストデータならお手のもの 開眼シェルスクリプト 【18】サーバにデータを渡して処理させる―nc,ssh,scpを使う」より加筆修正後転載

Last modified: 2014-01-13 00:00:00