Disk I/O性能測定するときに活躍する道具たち

この記事はピョッコリンアドベントカレンダーのために書かれたものです。

サマリ

Disk(HDD/SSD/仮想ディスク等)のI/O性能を測定するのに 活躍するツールについて、簡単に使い方を説明する。

はじめに

Diskに限定せずとも、性能測定と言えば、 おおまかには、スループット(Diskに関して言えば、1秒あたりどれくらいのI/Oを処理できるか?)と レイテンシ(Diskに関して言えば、1個のI/Oを処理するのにどれくらい時間がかかったか?) の2つについて見ることになる。 特にLinuxに関しては、このあたりをよく使うような気がする(主観)。

  • fio:Diskに対してI/Oを発行してくれて、スループットやレイテンシを算出してくれるもの。
  • iostat:Diskごとにスループットなどの情報をリアルタイムで見せてくれるもの
  • blktrace:LinuxがDiskに対してどんなI/Oを出していたかの履歴(=トレース)を見せてくれるもの。

fioはスループットもレイテンシも算出してくれるので、基本的にはfioだけで事足りるのだけど、 本当にfioに指定したとおりにLinuxがI/Oを出しているのか、に関してはちゃんと確認する意義があるので、 iostatとblktraceを追加した。

以後、以下の環境を前提とするので、必要があれば適宜読み替えてほしい。

fio

fioのインストール

fioはメジャーなツールなので、こんな感じでaptでインストールできる。

$ sudo aptitude install fio

ソースからビルドしたい原理主義者は、作者様のgithubから落としてくると幸せになれる。 (ビルド前にlibaioを入れておくことを忘れなければ、さらに幸せになれる) github.com

手順としてはこんな感じ。

$ sudo aptitude install libaio1 libaio-dev
$ git clone git@github.com:axboe/fio.git
$ ./configure
$ make
$ sudo make install

使い方

コマンドとしてはこんな感じ。起動時に、どんなI/Oをかけるのかを記述した 設定ファイルを指定する。

$ fio -f script.fio --output output_filename # 場合によってはsudoが必要

fioはコマンド引数でもろもろ指定ができるんだけど、 後々の再利用を考えると、設定ファイルを逐一書いたほうがよいと思う。

設定ファイルの書き方

最小限ではないが、これだけ書けばI/Oをかけることができる。 細かいところはman fioを見るのがいちばん。 英語で書いてあるけど、割と簡単に読めると思う。

fioの設定ファイルは、全体で共通の[global]以下の設定と、 個別の[job]に分けられる。[global]予約語だけど、 [job]に関しては好きな名前を指定できる。もちろん、[job]複数個書くことができる。

[global]                                
ioengine=libaio    # I/Oを発行するのにlibaioを使う。性能測るときは
                   # とりあえずlibaioを指定しておけばよいと思う。
blocksize=4KB      # 1回のI/Oの転送長
blockalign=4KB     # I/Oのアラインメント。
                   # I/O先のアドレスが4KB刻みになる。
kb_base=1024       # K=1000かK=1024かわからなくなるので、
                   # 書いておくと親切。defaultは1024。
filesize=1-1GB     # I/Oをかけるアドレス範囲の指定。
                   # この設定だと、各ファイルそれぞれについて、
                   # 1-1GBの範囲にI/Oをかける(1始まりなので注意)。
direct=1           # Disk I/O性能を測るので、Linuxのキャッシュを
                   # 素通りするようにする。
ioscheduler=noop   # Disk I/O性能を測るので、Linuxに何も考えず
                   # I/Oしてもらうようにする。

[job]
rw=randread        # I/Oの種別。[global]で指定したアドレス範囲から
                   # ランダムに選んでリードする。                                       
filename=/dev/sdc:/dev/sdd:/dev/sde 
                   # どのファイルにI/Oをかけるかを指定。
                   # : で複数指定できる。
                   # 見ての通り、別にファイルでなくともよい。
ramp_time=30       # I/Oかけはじめの、性能測定はしない時間。
                   # I/Oかけはじめは安定しないこともあるので、
                   # 少し時間を置くほうがよいこともある。
runtime=60         # I/Oをかけて、性能測定をする時間。
time_based=1       # filesize分I/Oかけても、runtimeが経過するまで
                   # I/Oをかけ続ける。
numjobs=6          # いくつのプロセスでI/Oするかを指定。
                   # thread=1とすると、forkではなく
                   # pthreadを使ってくれる。
group_reporting=1  # プロセスごとに結果を報告するのではなく、
                   # ジョブごとにまとめて報告する。
iodepth=1          # I/Oの多重度。概して多重で出したほうが性能が上がる。

得られる結果

上記をtest.fioとして保存して、RAM Disk相手にfioを実行してみる。 I/O中はこんな感じで表示される。

$ sudo fio -f test.fio --output test.result             
Jobs: 6 (f=18): [r(6)] [60.0% done] [1137MB/0KB/0KB /s] [291K/0/0 iops] [eta 00m:36s] 

test.resultの中身はこんな感じ。 注意すべきはレイテンシがslat、clat、latの3つあること。 それぞれは以下のとおり。

  • slat:fioがI/O発行して、Linuxがコマンド発行までにかかっている時間。
  • clat:コマンド発行から応答を受け取るまでにかかった時間(単にディスクのレイテンシを見るだけならここだけ見ればよいかも)。
  • lat:全体。
read_test: (g=0): rw=randread, bs=4K-4K/4K-4K/4K-4K, ioengine=libaio, iodepth=1
(略)
read_test: (groupid=0, jobs=6): err= 0: pid=8858: Sun Dec 13 22:27:14 2015                             
  read : io=67192MB, bw=1119.9MB/s, iops=286680, runt= 60001msec                                       
    slat (usec): min=4, max=11476, avg=16.24, stdev=13.23                                              
    clat (usec): min=0, max=10280, avg= 2.26, stdev= 8.96                                              
     lat (usec): min=11, max=10293, avg=18.94, stdev=14.98                                             
    clat percentiles (usec):                                                                           
     |  1.00th=[    0],  5.00th=[    0], 10.00th=[    0], 20.00th=[    1],                             
     | 30.00th=[    1], 40.00th=[    1], 50.00th=[    1], 60.00th=[    1],                             
     | 70.00th=[    1], 80.00th=[    1], 90.00th=[   12], 95.00th=[   13],                             
     | 99.00th=[   15], 99.50th=[   15], 99.90th=[   18], 99.95th=[   20],                             
     | 99.99th=[   47]                                                                                 
    bw (KB  /s): min=    0, max=228328, per=16.54%, avg=189615.16, stdev=19299.85                      
    lat (usec) : 2=88.03%, 4=0.33%, 10=0.02%, 20=11.56%, 50=0.05%                                      
    lat (usec) : 100=0.01%, 250=0.01%, 500=0.01%, 750=0.01%, 1000=0.01%                                
    lat (msec) : 2=0.01%, 4=0.01%, 10=0.01%, 20=0.01%                                                  
  cpu          : usr=12.58%, sys=37.56%, ctx=17215603, majf=0, minf=8                                  
  IO depths    : 1=150.9%, 2=0.0%, 4=0.0%, 8=0.0%, 16=0.0%, 32=0.0%, >=64=0.0%                         
     submit    : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%                        
     complete  : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%                        
     issued    : total=r=17201111/w=0/d=0, short=r=0/w=0/d=0, drop=r=0/w=0/d=0                         
     latency   : target=0, window=0, percentile=100.00%, depth=1                                       
                                                                                                       
Run status group 0 (all jobs):                                                                         
   READ: io=67192MB, aggrb=1119.9MB/s, minb=1119.9MB/s, maxb=1119.9MB/s, mint=60001msec, maxt=60001msec
                                                                                                       
Disk stats (read/write):                                                                               
  sdc: ios=8650516/0, merge=0/0, ticks=102172/0, in_queue=103080, util=61.34%                          
  sdd: ios=8650522/0, merge=0/0, ticks=104708/0, in_queue=103800, util=62.18%                          
  sde: ios=8650522/0, merge=0/0, ticks=101136/0, in_queue=101780, util=60.36%                          

iostat

fioはおおよそのスループットを表示してくれるわけだけど、いかんせんざっくりすぎるので、 iostatを使って、もう少し細かく見てみる。

iostatの入手

何も考えずにaptでinstallする。

$ sudo aptitude install sysstat

iostatの使い方

1秒間隔で表示させておけば困ることはないはず。

$ iostat -x 1 sdc sdd sde

得られる結果

こんな感じで毎秒表示してくれる。 よく使う前半部分に関しては、こんな感じ。

項目 意味
rrqm/s 1秒あたりにマージしたリードリクエストの回数
wrqm/s 1秒あたりにマージしたライトリクエストの回数
r/s 1秒あたりのリード回数
w/s 1秒あたりのライト回数
rkB/s 1秒あたりにリードしたデータ量
wkB/s 1秒あたりのライトしたデータ量
%util I/OリクエストにCPU時間をどれくらい使ったか?(100%=CPUが全力でI/Oを発行しまくっている)
avg-cpu:  %user   %nice %system %iowait  %steal   %idle
          12.31    0.00   64.45    0.00    0.00   23.24

Device:         rrqm/s   wrqm/s     r/s     w/s    rkB/s    wkB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %util
sdc               0.00     0.00 96948.00    0.00 387796.00     0.00     8.00     1.08    0.01    0.01    0.00   0.01  60.00
sdd               0.00     0.00 96949.00    0.00 387796.00     0.00     8.00     1.15    0.01    0.01    0.00   0.01  64.40
sde               0.00     0.00 96949.00    0.00 387796.00     0.00     8.00     1.08    0.01    0.01    0.00   0.01  57.60

blktrace

測定をしたはよいが、芳しくない結果が得られたときに、一体何が起こっていたのかを 確認するためのトレース採取ツール。困ったときに覗くとよいと思う。

blktraceの入手

考える前にまずはaptを叩く。

$ sudo aptitude install blktrace

blktraceの使い方

I/O中に、デバイスを指定してトレースを取得する。 トレースはコア数分できる。

$ sudo blktrace -d /dev/sdc # Ctrl-Cでトレース採取終了
$ ls
sdc.blktrace.0  sdc.blktrace.2  sdc.blktrace.4  sdc.blktrace.6 
sdc.blktrace.1  sdc.blktrace.3  sdc.blktrace.5  sdc.blktrace.7  

得られる結果

得られたファイルはバイナリなので、blktraceとセットになっているblkparseというコマンドを使って、 読みやすい形に整形する。

$ blkparse -i sdc.blktrace.0 > sdc.blkparse.0

sdc.blkparse.0を覗いてみると、左から順に以下のようになっている(デフォルト設定なのでもちろん変えられる)。

  • デバイスのmajor番号, minor番号
  • CPUコア番号
  • シーケンス番号
  • トレース採取時からの経過時間(秒)
  • I/Oしていたプロセスのpid
  • コマンド状況を表すアルファベット(詳細はmanを参照のこと)
  • 処理を表すアルファベット(詳細はmanを参照のこと)
  • I/Oの先頭アドレス+オフセット
  • プロセス名
  8,32   0        1     0.000000000  9718  G   R 1839944 + 8 [fio]   
  8,32   0        2     0.000000383  9718  P   N [fio]               
  8,32   4        1     0.000000413  9717  G   R 1831472 + 8 [fio]   
  8,32   4        2     0.000000643  9717  P   N [fio]               
  8,32   0        3     0.000001059  9718  I   R 1839944 + 8 [fio]   
  8,32   0        4     0.000001355  9718  U   N [fio] 1             

CPUコア0だけ取り出したのが以下。 多重でI/Oかけていると、トレースが混迷を極めそうだ・・・

8,32   0        1     0.000000000  9718  G   R 1839944 + 8 [fio]
→ fioがI/Oの先頭アドレスを算出した
8,32   0        2     0.000000383  9718  P   N [fio]              
→ fioがカーネルに対してリードリクエストを発行するために
   コマンドをためておくキューに接続
8,32   0        3     0.000001059  9718  I   R 1839944 + 8 [fio]  
→ fioのリードリクエストがコマンドをためておくキューに、
  コマンドを追加
8,32   0        4     0.000001355  9718  U   N [fio] 1            
→ fioのリードリクエストがコマンドをためておくキューとの接続を解除
8,32   0        5     0.000001688  9718  D   R 1839944 + 8 [fio]
→ コマンド発行
8,32   0        6     0.000014064     3  C   R 1839944 + 8 [0]    
→ 応答が返ってきた

さいごに

以上、Disk I/Oの性能測定をするときに活躍する道具たちとその使い方でした。 使い方とか説明が間違っていたらごめんね!

the platinum searcher(pt)をビルドして使ってみよう

※この記事はピョッコリンアドベントカレンダーのために書かれたものです。

サマリ

Golang初心者だけどthe platinum searcherをビルドして使ってみた。ついでにpull requestもしてみた。

the platinum searcherとは何か

ひとことで言うと、プログラマ向けの速いgrep。 作者様のblogはこちら

世の中には改良版grepとして、以下のものがあるみたい。

ptとagの違いは、EUC-JPやShift_JISに対応しているか否か。 (agはUTF-8しか対応していなくて、EUC-JPやShift_JISのファイルが スキップされてしまうという罠がある)

the platinum searcherのビルドとインストール

README.mdにしたがっていけばOK。

$ go get -u github.com/monochromegane/the_platinum_searcher
$ go get -u github.com/jessevdk/go-flags        # なぜか入らなかったので別立て。普通はいらないはず。
$ go get -u github.com/monochromegane/conflag   # なぜか入らなかったので別立て。普通はいらないはず。
$ go get -u github.com/monochromegane/terminal  # なぜか入らなかったので別立て。普通はいらないはず。
$ cd $GOPATH/src/github.com/monochromegane/the_platinum_searcher
$ go build -o $GOPATH/bin/pt cmd/pt/main.go

おまけ:pull requestを投げてみた

pull requestした背景

githubを使ってはいるが、恥ずかしながら1度もpull requestというものを投げたことがなかったので、 pull requestを投げてみたかったんです・・・ というわけで、A Tour of Goをひととおりやった程度の僕でもなんとかできそうなissueを選んでpull requestを投げてみた。

以下のissueは、.gitconfigにexcludesfileが指定されていないときに、グローバルなgitignoreファイルを 読んでくれないよ、っていうもの。

github.com

問題箇所の特定

おおよそこんな感じだったと思う。gdbで実行するまでもなかったかもしれないけど、 gdbを使ってみたかったのもあって、gdbでステップ実行しながら特定した。

  1. gdbで追いかけられるように、まずはptをビルドし直す:go build -v -gcflags "-N -l" -o pt_debug cmd/pt/main.go

  2. ptなりgrepなりで"gitignore"をソースのどこで使っているか探す

  3. ソースコードでは、option.goとoption_test.goとignore.goがひっかかるので、中を覗く。ignore.goだけが関係あることがわかる。

  4. gdbでptを起動する。入り口と思しきignore.go内のglobalGitIgnore関数にbreakpointを作成する。"b globalGitIgnore"と打つと、gdbはそんな関数しらねとのたまうので、"b ignore.go:88"とファイルかつ行数指定でbreakpointを作成する。

  5. gdbでステップ実行していくと、globalGitIgnoreName関数内で、gitconfig内にexcludesfilesが定義されていないときに空文字列が返されることがわかるので、その部分を修正すればよいことがわかる。

追記

何か中途半端だったし、元のソースコードとコンフリクトしてたしでプルリクを引っ込めた。

プログラミング言語pyoko

※この記事はピョッコリンアドベントカレンダーのために書かれたものです

はじめに

みんなのアイドルピョッコリンさんのために、 プログラミング言語pyoko(以後、単にpyokoと呼びます)を開発しました!!

pyokoとは

pyokoは、言語のシンプルさとピョコ感を出すことを念頭に置いて設計された言語です。 字面の似ているpythonと同等の表現力を持ちながらも、圧倒的可読性・記述性を達成しました。 また、ピョコ感を演出することうけあいでしょう。

サンプルコード

お決まりのHello, Worldをみてみましょう。 ね、圧倒的可読性・記述性とピョコ感が伝わるでしょう?

ピョコピョコピョコピョコピョコピョコピョコピョコピョコピョリコピョッコ
ピョコピョコピョコピョコピョコピョコピョコピョコピョッコピョコピョコ
ピョコピョコピョコピョコピョコピョコピョコピョコピョコピョッコピョコ
ピョコピョコピョコピョコピョ?!ピョ?!ピョ?!ピョコリピョコ-ピョッコ
ピョッコリンピョッコピョコピョコピョッコリンピョコピョコピョコピョコピョコ
ピョコピョコピョッコリンピョッコリンピョコピョコピョコピョッコリンピョッコ
ピョコリピョッコリンピョコリピョコリピョコリピョコリピョコリピョコリピョコリ
ピョコリピョコリピョコリピョコリピョコリピョッコリンピョ?!ピョコピョコピョコ
ピョコピョコピョコピョコピョコピョッコリンピョコリピョコリピョコリピョコリ
ピョコリピョコリピョコリピョコリピョッコリンピョコピョコピョコピョッコリンピョコリ
ピョコリピョコリピョコリピョコリピョコリピョッコリンピョコリピョコリピョコリピョコリ
ピョコリピョコリピョコリピョコリピョッコリンピョッコピョコピョッコリン

上記テキストをhello.pyokoとして保存し、インタプリタを実行します。

$ pyoko hello.pyoko
Hello, world!

ね、Hello, worldでしょう?

ソースコード

pyokoのインタプリタpythonで実装されています。

#!/usr/bin/env python
#fileencoding: utf-8

from __future__ import print_function
import sys
import codecs

class pyokolang():
    plus   = "+"
    minus  = "-"
    gt     = ">"
    lt     = "<"
    lb     = "["
    rb     = "]"
    comma  = ","
    period = "."

    def __init__(self,filename):
        with codecs.open(filename, "r", "utf-8") as f:
            self.raw_code = ("".join(f.readlines())).strip()
        self.iptr = 0
        self.dptr = 0
        self.data = [0]*100
        self.loopmap = {} 
        self.insts = []
        self.__parser()

    def __parser(self):
        self.__tokenizer()

    def __get_token(self, pos):
        is_same = lambda raw_code, pos, x: raw_code[pos:pos+len(x)] == x
        quasi_plus   = u"ピョコ"
        quasi_minus  = u"ピョコリ"
        quasi_gt     = u"ピョッコ"
        quasi_lt     = u"ピョ?!"
        quasi_lb     = u"ピョリコ"
        quasi_rb     = u"ピョコ-"
        quasi_comma  = u"ピョコ?!"
        quasi_period = u"ピョッコリン"

        if is_same(self.raw_code, pos, quasi_minus):
            return quasi_minus, pyokolang.minus
        elif is_same(self.raw_code, pos, quasi_period):
            return quasi_period, pyokolang.period
        elif is_same(self.raw_code, pos, quasi_gt):
            return quasi_gt, pyokolang.gt
        elif is_same(self.raw_code, pos, quasi_lt):
            return quasi_lt, pyokolang.lt
        elif is_same(self.raw_code, pos, quasi_lb):
            return quasi_lb, pyokolang.lb
        elif is_same(self.raw_code, pos, quasi_rb):
            return quasi_rb, pyokolang.rb
        elif is_same(self.raw_code, pos, quasi_plus):
            return quasi_plus, pyokolang.plus
        elif self.raw_code[pos:pos+len(quasi_comma)] == quasi_comma:
            return quasi_comma, pyokolang.comma
        else:
            return False, False

    def __tokenizer(self):
        pos = 0
        while pos < len(self.raw_code):
            token, symbol = self.__get_token(pos)
            if token is False:
                pos += 1
                continue
            self.insts.append(symbol)
            pos += len(token)

    def __gen_loopmap(self):
        stack = []
        for i, val in enumerate(self.insts):
            if val == pyokolang.lb:
                stack.append(i)
            elif val == pyokolang.rb:
                if len(stack) == 0:
                    print("parse error: right brackets exists more than left brackets")
                    sys.exit()
                lb_index = stack.pop()
                self.loopmap[i] = lb_index
                self.loopmap[lb_index] = i

        if len(stack) != 0:
            print("parse error: left brackets exists more than right brackets")
            sys.exit()


    def executor(self):
        self.__gen_loopmap()
        while self.iptr < len(self.insts):
            if self.insts[self.iptr] == pyokolang.gt: # >
                self.dptr += 1
                if self.dptr >= len(self.data):
                    for i in range(len(self.data)):
                        self.data.append(0)
            elif self.insts[self.iptr] == pyokolang.lt: # <
                self.dptr -= 1
                if self.dptr < 0:
                    print("data pointer underflow", self.iptr, self.insts, self.dptr, self.data)
            elif self.insts[self.iptr] == pyokolang.plus: # +
                self.data[self.dptr] += 1
            elif self.insts[self.iptr] == pyokolang.minus: # -
                self.data[self.dptr] -= 1
            elif self.insts[self.iptr] == pyokolang.period: # .
                print(chr(self.data[self.dptr]), end="")
            elif self.insts[self.iptr] == pyokolang.comma:  # ,
                self.data[self.dptr] = sys.stdin.read(1)
            elif self.insts[self.iptr] == pyokolang.lb: # [
                if self.data[self.dptr] == 0:
                    self.iptr = self.loopmap[self.iptr]
            elif self.insts[self.iptr] == pyokolang.rb: # ]
                if self.data[self.dptr] != 0:
                    self.iptr = self.loopmap[self.iptr]
            self.iptr += 1

    def run(self):
        self.executor()

def usage():
    print("usage: python pyokolang.py <filename>")
    sys.exit()

def main():
    if len(sys.argv) != 2:
        usage()
    bp = pyokolang(sys.argv[1])
    bp.run()
    print()

if __name__ == "__main__":
    main()

実装に関して

ここまで来ればお気づきの方もいるかもしれませんね。 そう、pyokoは難読で有名なプログラミング言語brainf*ckを ベースに作られています。 実装方針は単純で、brainf*ckを構成する8個の実行命令(+/-/>/</[/]/,/.)を ピョコ感ある単語に置き換えるだけです。

処理の流れも単純で、以下の2ステップです。

  1. ピョコ感ある単語をbrainf*ckの8個の実行命令に置き換え(__tokenizer)

  2. brainf*ckインタプリタで実行(executor)

感想や反省など

  • 感想:意外に簡単にできた。
  • 反省:もっとhello.pyokoを複雑な文章にすべきだった。少々面白みに欠ける。

おまけ

インタプリタソースコードはここにあります。 github.com

easy_perfmonをjavascriptで書きなおした

シルバーウィークに作ったおもちゃをjavascriptで書きなおしました、というお話。

サマリ

こんな感じになった。

f:id:nbisco:20151029234055p:plain

Smoothie ChartからFlotに移行し、bottle.pyからexpressに移行した。 Flotはこまごましたところまでいじれるので楽しい。

ソースコードはこちら。 github.com

使ったツール

bottleには負けるがexpressも結構簡単に使えたと思う。 jadeの素晴らしさには感動した。あの面倒なhtmlファイルが簡単に書けちゃう。

以前
webフレームワーク bottle express
テンプレートエンジン なし jade、stylus
グラフ Smoothie Charts Flot

使い方

app.jsを起動するだけ。ポート3000番でお待ちしております。

$ node app.js  # => 3000番で待ち受け

はまったところ

Flotの凡例(legend)出すのにはまった。 最初にplot.pushするときには、ちゃんとラベルを指定していたんだけど、 アップデートのときにラベル指定が抜けてたせいで凡例が描画されなかった。 当たり前っちゃ当たり前なような気もするけど、全然気づかなかったなあ・・・

これが正しい例。

// 最初にpush
plots.push($.plot(placeholders[0],
  [{label:"usr",data:usr},{label:"sys",data:sys},{label:"idle", data:idle}], options[0]));

/* 略 */

// データ更新時に描画
plots[0].setData([{label:"usr",data:usr}, {label:"sys",data:sys}, {label:"idle",data:idle}]);

こっちが間違いの例。

// 最初にpush
plots.push($.plot(placeholders[0],
  [{label:"usr",data:usr},{label:"sys",data:sys},{label:"idle", data:idle}], options[0]));

/* 略 */

// データ更新時に描画 => labelがnullになるせいで、描画されない。
plots[0].setData([usr, sys, idle]);

Flot本体のコードを覗いてみると一目瞭然であった。さっさと読めという話ですね。

/* jquery.flot.js */

/* 略 */
function insertLegend() {
/* 略 */
     // Build a list of legend entries, with each having a label and a color

    for (var i = 0; i < series.length; ++i) {
        s = series[i];  // seriesには、setDataメソッドに渡したオブジェクトとグラフのoptionが入っている。
        if (s.label) {  // labelをつけてないと、このネストに入れない。
            label = lf ? lf(s.label, s) : s.label;
            if (label) {
                entries.push({
                    label: label,
                    color: s.color
               });
            }
        }
    }
/* 略 */
}

Smoothie ChartsとBottleを使ったリアルタイムパフォーマンスモニタ

今週はシルバーウィーク。 しばらく楽しくプログラムを書いてなかったし、 せっかくの長い休みなので、単にイカで遊び呆けるだけじゃなくて、 ちょっとしたものを工作してみようと思い立った。

サマリ

Smoothie ChartsとBottleを使って、こんな感じのものができた。 f:id:nbisco:20150923214841p:plain

ソースコードはここ。READMEはまだ。。。 github.com

使い方

以下の通り立ち上げて、ブラウザでアクセスすれば上記画像のような画面が現れる(はず)。 mpstat、iostat、vmstat、sarを使うので、sysstatをaptとかで入れておく。

$ sudo aptitude install sysstat
$ git clone https://github.com/bisco/easy_perfmon.git
$ cd easy_perfmon
## 引数なしだとPort 8080をListenして立ち上がる。引数あげるとそのPort番号をListenして立ち上がる。
$ python easy_perfmon.py  

経緯

Linuxサーバの負荷状況を見るツールはいろいろあるわけだけど、 CUIなのでいかんせん味気ない。グラフとして見えたほうがいいよね、ということで、 Linuxサーバの負荷状況を可視化することにした。

単に可視化するだけじゃなく、どうせならリアルタイムに負荷を見たい。 ということで、リアルタイムに負荷状況が見えるツールを作ることにした。

使ったライブラリ

Smoothie Charts

お手軽に可視化するにはブラウザ+Javascriptの組み合わせが一番(だと思う)。 なので、Javascriptで簡単にリアルタイムなグラフが描けるSmoothie Chartsを選んだ。 Smoothie Chartsは大変シンプルで、設定できる項目もあまりないんだけど、 その分簡単に使えるのがよいところ。 本家サイトに行けば、コードジェネレータ(!)があるので、 それを使いながら見た目の調整ができる。何とも楽ちん。

Bottle

BottlePythonで書かれたweb frameworkの1つで、 非常にシンプルで簡単に使えるのが特徴。

今回は、主にSmoothie Chartsにデータを渡すために使用。 mpstatやiostatなどの結果をJSONに変換して、httpでアクセスできるようにした。

bootstrap

おなじみbootstrap。見た目調整のために使用。

プログラムのおおよその構造

JavascriptでBottleから定期的にJSONを取得し、Smoothie Chartsに渡しているだけ。 定期取得の部分はjQueryAJAX関数を使って実装した。

はまったところ

AJAXで同期的に情報を取ろうとしてはまった。 AJAXは基本非同期動作なので、同期的に情報を取るときは、明示的に同期動作にしてやらないといけない。

$.ajaxSetup({ async: false }); 
$.getJSON(); 
$.ajaxSetup({ async: true }); 

まとめと感想

Smoothie ChartsとBottleでお手軽にLinuxの負荷状況を可視化するツールを作った。 グラフィカルに見えるものを作るのは大変でもあり、楽しくもあり、って感じでした。

OpenIndianaで楽しいDLNAサーバ

はじめに

せっかくopenindianaでちょっと素敵なNASを仕立てたので、NASに放り込んだコンテンツを有効活用すべくDLNAサーバを動かしたいと思うのは当然の流れですよね。だがしかし、そこはopenindiana(以後面倒なのでoiと略す)。oiは素人に大変厳しいOSなので、aptやらyumやらでヒャッハーすることなんてできやしない。pkgなるapt相当のものがあって、多少はものが入るんだけど、種類が少なかったり古かったりでいまいちだし*1、もちろんDLNAサーバなんていう気の利いたものがあるはずもない。 そこで、VM上でLinuxを動かして、LinuxDLNAサーバをやらせることにした。

環境
  • hpの初代microserver
  • openindiana 151a
Virtualboxの準備

oiにはSolarisコンテナなる仮想化環境があるらしい。しかし、Linuxが動くのかどうか全くわからなかった*2ので、多少は親しみのもてるVirtualboxを使うことにする。もともとSunがやってただけあって、oi用のバイナリもちゃんとあるのが素晴らしいですね。

# 本体のダウンロードとインストール
$ wget http://download.virtualbox.org/virtualbox/4.3.6/VirtualBox-4.3.6-91406-SunOS.tar.gz
$ tar xvf VirtualBox-4.3.6-91406-SunOS.tar.gz
$ sudo pkgadd -d VirtualBox-4.3.6-SunOS-amd64-r91406.pkg

# extpackのダウンロードとインストール
$ wget http://download.virtualbox.org/virtualbox/4.3.6/Oracle_VM_VirtualBox_Extension_Pack-4.3.6-91406.vbox-extpack
$ sudo VBoxManage extpack install Oracle_VM_VirtualBox_Extension_Pack-4.3.6-91406.vbox-extpack
VirtualboxLinuxを動かす

普段はGUIでやってる仮想マシン作成をコマンドラインでやるのは結構大変。マニュアルを読みながら、時にGoogleに教えてもらいながらやるんだけど、マニュアルを読むのが一番正確だしわかりやすいと思う。普段はDebianな僕だけど、今回は何を思ったかUbuntuにした。

# VM作成
$ VBoxManage createvm -name "ubuntu" -register

# 基本設定。DVDのブート順位を上げる。DLNAサーバにするのでブリッジ接続にする。
$ VBoxManage modifyvm "ubuntu" --ostype Ubuntu_64 --memory 1536 -acpi on -boot1 dvd -nic1 bridged --bridgeadapter1 bge0

# 仮想HDD用のファイルを作る。特に何も調べずにVDIにしてしまったけど、よかったんだろうか。。。
# サイズはMB単位で指定
$ VBoxManage createhd -filename "ubuntu13.vdi" -size 20000

# 仮想SATAポートを用意する
$ VBoxManage storagectl "ubuntu" --name "SATA" --add sata

# SATAポートにHDD用ファイルとISOファイルをぶら下げる
$ VBoxManage storageattach "ubuntu" --storagectl "SATA" --port 0 --device 0 --type hdd --medium ubuntu13.vdi
$ VVBoxManage storageattach "ubuntu" --storagectl "SATA" --port 1 --device 0 --type dvddrive --medium ubuntu13.iso

# VM起動。画面を飛ばすのがちょっと面倒だったので、RDPでつなぐために以下コマンドで起動する。
$ VBoxHeadless --startvm "ubuntu"

# Linuxのインストールが終わったら、一旦シャットダウンしてISOファイルをアンマウントする。
$ VBoxManage controlvm "ubuntu" poweroff
$ VBoxManage storageattach "ubuntu" --storagectl "SATA" --port 1 --device 0 --type dvddrive --medium none

# 再度VMを起動
$ VBoxHeadless --startvm "ubuntu"
NFSマウントとDLNAサーバの準備

ubuntuを入れたので、あとはずいぶん楽。DLNAサーバはmediatombがあるし、ファイルはNFSでアクセスすればいい。

# まずは必要なものをインストール
$ sudo aptitude install nfs-common nfs-client mediatomb

# NFSでマウントする
$ sudo mount -t nfs <サーバ名>:/path/to/share/dir /path/to/mount/point
mediatombの設定

ここここを見ながらやる。 拡張子によってはmediatombにスキャンしてもらえない場合があった*3ので注意。これで完成。

iPadで試運転してみたけど、特にストレスなく動画が見られて大変ハッピー。

おわりに

OpenIndiana(でLinuxを動かした上)でDLNAサーバを動かすことができた。OpenIndianaはもうちょっといろんなパッケージを使えるようにしてくれないかなあと思う次第です。

*1:なぜかgccは普通にいれるとgcc-3が入ってしまうし、tmuxもない。

*2:何となくではあるが動く気はしなかった

*3:なぜか僕の環境では.tsは無視されたので、わざわざ.mpgをつけた。完全に無駄だし、美しくない。

authorized_keysを放り込むだけではなぜダメだったのか未だに不明

ssh-copy-id

今日はsshの公開鍵を配るのに嵌り込んだので、メモしとく。 LDAPとか高級な環境はないので、力技。

やりたかったこと:新サーバに既存サーバが公開鍵認証で入れるようにする

下記手順を想定してたんだけど、4でハッピーになれなかった。

  1. 新サーバで鍵を作る
  2. 新サーバで作った公開鍵を既存サーバに取り寄せて、authorized_keysに追記
  3. authorized_keysを各サーバにばら撒く
  4. アクセスできてハッピー

パーミッションが悪かった(ちゃんと認証できてるやつとできてないやつで差はなかったんだけど・・・)のか、 サーバの機嫌が悪かったのか、僕の運が悪かったのかはわからないけど、ともかくも公開鍵認証できなかった わけだ。こうして僕の1時間は消し飛んだと、そういうわけですね。

仕方なくやったこと:ssh-copy-idで鍵を交換してからばら撒く

こんな手順になった。改めて見なおしてみるとすごく無駄に溢れてるなあ・・・

  1. 新サーバで鍵を作る
  2. ssh-copy-idで既存サーバの1つと鍵交換する
  3. 既存サーバのauthorized_keysをばら撒く
  4. アクセスできるようになってハッピー

コマンドログを思い出しながらなので、間違いがあるかもしれない。 登場人物は新サーバと既存サーバ。

# まずは鍵作成
bisco@新サーバ $ ssh-keygen -t dsa -b 1024

# 既存サーバのログインパスを入力して、新サーバの公開鍵を既存サーバに放り込む
bisco@新サーバ $ ssh-copy-id -i ~/.ssh/id_dsa.pub bisco@既存サーバ

# 既存サーバのauthorized_keysをひっぱってくる
bisco@新サーバ $ scp bisco@既存サーバ:~/.ssh/authorized_keys ~/.ssh/authorized_keys

# 既存サーバの公開鍵を新サーバに放り込む
bisco@既存サーバ $ ssh-copy-id -i ~/.ssh/id_dsa.pub bisco@新サーバ

# 新サーバのauthorized_keysをひっぱってくる
bisco@既存サーバ $ scp bisco@新サーバ:~/.ssh/authorized_keys ~/.ssh/authorized_keys

# 既存サーバのauthorized_keysを他のサーバにばら撒く
bisco@既存サーバ $ ./baramaki.sh
これやって思ったこと
  • そもそも何でダメだったのか、不明のままで気持ち悪い。これのおかしなところは、1個の既存サーバと 交換しただけであとは全部OKになったところ。
  • screen+bashだったせいで、コマンドログを取りそこねてしまいへこんだ。やはりzshにすべきだった。
  • サーバが増えるのであれば、もっと簡単にユーザ管理できるような仕組みをつくるべきかもしれない。 だけど、LDAPサーバをたてるほど余力はないし、やる気もない。なんかこう、簡易的に公開鍵をばら撒く仕組みはないものかなあ。