bashでもpercolをイイ感じに使う方法

[twitter:@stillpedant](mooz)さん作のpercolはシェルとは独立しているのですが、ググるzshと共に使っている人が多いようです。これはzshの強力な行編集機能を提供するzleに因るところが大きいためだと思います。

bashでもpercolを使っている人もいるのですが、

「絞り込んだパスへのcdを実行する」ことはできても、「絞り込んだパスを現在のコマンドライン行に挿入する」ことは実現できていない、つまりコマンド実行はできるけど行編集はできていないようでした。

そこで今回ちょっと調べてみたところ、bashでも(zleには及ばないにしても)変数READLINE_LINEとREADLINE_POINTで行編集できることがわかったので紹介します。ただし、bash 4以上が必要です。

READLINE_LINEとREADLINE_POINT

READLINE_LINEには現在のコマンド行の内容が、READLINE_POINTにはカーソルの位置が格納されます。

例えば、

_readline_test() {
  {
    echo "LINE=$READLINE_LINE"
    echo "POINT=$READLINE_POINT"
  } >&2
}

bind -x '"\C-xx":_readline_test'

とし、対話シェル上で C-x x を打つとこのような結果になります。「■」の位置がカーソルの位置を示します。

$ ■
LINE=
POINT=0

$ foo bar baz■
LINE=foo bar baz
POINT=11

$ foo bar■baz
LINE=foo bar baz
POINT=7

percol --query に $READLINE_LINE 全体もしくは $READLINE_POINT までの部分文字列を渡せば、検索文字列が入った状態で percol を呼ぶことができそうです。


またこの READLINE_LINE と READLINE_POINT は変更することもできます。変更した場合、対話シェル上のコマンドライン行をその内容とカーソルの位置に変更できます。

例えば、

_readline_test() {
  READLINE_LINE="$READLINE_LINE hok"
  READLINE_POINT=${#READLINE_LINE}
}

bind -x '"\C-xx":_readline_test'

とした場合、C-x x を押す度に「 hok」が追加されカーソルが行末に移動するはずです。


続いては実例です。

コマンドヒストリー

C-r を percol を使うものに置き換えています。念の為、C-r だった reverse-search-history を C-x r に当てています。

ヒストリーはpercolの結果で完全に置き換えて欲しいので、READLINE_LINEにはそのまま絞り込み結果を代入し、READLINE_POINTにはその長さを代入しています。

_replace_by_history() {
  local l=$(HISTTIMEFORMAT= history | tac | sed -e 's/^\s*[0-9]\+\s\+//' | percol --query "$READLINE_LINE")
  READLINE_LINE="$l"
  READLINE_POINT=${#l}
}
bind -x '"\C-r": _replace_by_history'
bind    '"\C-xr": reverse-search-history'

gitのディレクトリ{ にcd | を挿入 }

自分は git clone したディレクトリは ~/repos の直下もしくは1階層サブディレクトリを掘った下に置いているので、percolでそれを絞り込むシェル関数はこのようになります。

_select_gitdir() {
  find ~/repos -maxdepth 3 -type d -a -name '.git' | \
    sed -e 's@/.git@/@' | \
    percol
}

そこへ移動したい場合は、単純にcdすればよいです。

_cd_gitdir() {
  cd "$(_select_gitdir)"
}
bind '"\C-xg": "_cd_gitdir\n"'

次に、コマンドライン行の現在のカーソル位置に、絞り込んだ git ディレクトリのパスを挿入する場合はこのようになります。

現在の行の内容のカーソル位置のところに絞り込んだ結果を挟んで、カーソル位置を挟んだ末尾に移動しています。

_insert_gitdir() {
  local l=$(_select_gitdir)
  READLINE_LINE="${READLINE_LINE:0:$READLINE_POINT}${l}${READLINE_LINE:$READLINE_POINT}"
  READLINE_POINT=$(($READLINE_POINT + ${#l}))
}
bind -x '"\C-x\C-g": _insert_gitdir'

最近移動したディレクトリの履歴

自分は昔から Bash の小枝集 で紹介されている cdhist.sh を使っています。これを使っていると配列変数 CDHIST_CDQ に最近移動したディレクトリが記録されているので、percolから利用してみます。

_cd_cdhist() {
  cd "$(for i in "${CDHIST_CDQ[@]}"; do echo $i; done | percol)"
}
bind '"\C-xj": "_cd_cdhist\n"'

screenのアクティブwindowを選択する

screen 4.1以上なら -Q windows でリストが取れるようなのでそれでできそうです。

が、諸般の事情で tmux に乗り換え中です><

tmux の例は https://github.com/mooz/percol#tmux をどうぞ!

peco

goで書かれた peco というのもあるようです。

バイナリだけコピーすれば使えるので、いろんな環境で使いたい場合はいいかもしれませんね!

Apacheのmod_statusの各状態はどういう意味か?

Apacheのmod_statusを使えば各プロセスの状態を知ることができます。CloudForecastやCacti等でそれを元に視覚化している人も多いと思います。

mod_statusで確認できる状態には以下の11種類があるのですが、

状態名 mod_stautsでの記号 説明
SERVER_DEAD 0 . Open slot with no current process
SERVER_STARTING 1 S Server Starting up
SERVER_READY 2 _ Waiting for connection (or accept() lock)
SERVER_BUSY_READ 3 R Reading a client request
SERVER_BUSY_WRITE 4 W Processing a client request
SERVER_BUSY_KEEPALIVE 5 K Waiting for more requests via keepalive
SERVER_BUSY_LOG 6 L Logging the request
SERVER_BUSY_DNS 7 D Looking up a hostname
SERVER_CLOSING 8 C Closing the connection
SERVER_GRACEFUL 9 G server is gracefully finishing request
SERVER_IDLE_KILL 10 I Server is cleaning up idle children

年に3回ぐらい「これって具体的にどういう状態なんですか?」と聞かれてテケトーに答えてたので、httpdが一生のうちどういう状態を変遷するのかと、いくつかの状態についてはソースも読みながら確認してみたのでそのメモです。

httpdの一生

Apache 2.2.27で、prefork MPMでのhttpdの一生はこんな感じでした。

  • 起動
    • DEAD -> READY
    • READY -> READY
  • リクエスト/レスポンス処理 (非 keepalive)
    • READY -> BUSY_READ
    • BUSY_READ -> BUSY_READ
    • BUSY_READ -> BUSY_WRITE
    • BUSY_WRITE -> BUSY_LOG
    • BUSY_LOG -> CLOSING
    • CLOSING -> READY
  • リクエスト/レスポンス処理 (keepalive)
    • READY -> BUSY_READ
    • BUSY_READ -> BUSY_READ
    • BUSY_READ -> BUSY_WRITE
    • BUSY_WRITE -> BUSY_LOG
    • BUSY_LOG -> BUSY_KEEPALIVE
    • BUSY_KEEPALIVE -> CLOSING
    • CLOSING -> READY

まずはDEADから始まり、もろもろ初期化を済ませると READY に遷移し、続いて生きてる限り回るループの中で(再度)READY に遷移し accept(2) してリクエストを待ちます。

リクエストが着弾してaccept(2)が抜けると、ap_read_requestによりHTTPリクエストを処理します。このap_read_requestの前にBUSY_READに遷移し、ap_read_requestが終わるとBUSY_WRITEに遷移します。

ここで留意すべきは、HTTPリクエスト処理を終えた時点でBUSY_WRITEに遷移するという点です。

低速な回線のクライアントへのレスポンスの送信の場合にBUSY_WRITE状態で滞留するのはもちろんですが、reverse proxy先のバックエンドサーバーの処理が遅い場合もBUSY_WRITEで滞留します

つまりBUSY_WRITEだけでは回線起因でレスポンス送信に時間がかかっているのか、バックエンドの処理が詰まっているのか判別できません。

バックエンドの処理が詰まっているかどうかは、その処理時間をバックエンドサーバーのログやレスポンスヘッダに入れる(で、フロントのApacheアクセスログで記録してクライアントへのレスポンスからは当該ヘッダを消す)ようにしておけばよいと思います。

さて、無事にレスポンスを返し終えると、BUSY_LOGに遷移しap_run_log_transactionでログの記憶を行います。ちなみに、異常系で終了する場合も同じくBUSY_LOGに遷移してログを書き出します。

あとはCLOSING→READYと遷移し最初に戻ります。HTTP keepaliveが有効の場合は、BUSY_KEEPALIVE→CLOSING→READYという遷移になります。


ちなみにですが、少なくとも今回確認した Apache 2.2.27, prefork MPM, Linuxでは、

  • BUSY_DNS
  • IDLE_KILL

は使われていないようでした。

InfluxDB をちょっとさわってみた

InfluxDBとは

メトリクスやイベントといった時系列データを格納するのに適したデータストアです。

ちなみに go で書かれています。

ちなみに 2013のOpen Source Rookiesに選ばれました。

InfluxDBの特徴

RRDやMySQLに時系列データを格納する場合と比較して、InfluxDBの特徴を紹介します。

バックエンドは LevelDB

LevelDBとは、キーでソートされた状態で可能されたKVSです(Google製)。詳しくはこのへん参照のこと。

将来的にLevelDBからほかの実装に変わる可能性もありそうです。

HTTP API

データの登録や問い合わせ等、HTTP APIで行います。問い合わせ結果は JSON で返ってきます。

ユーザー認証はquery stringかBasic認証で行えます。

databaseとseries

RDBMSのdatabaseとtableと同じく、databaseとseriesの2階層でデータの管理ができます。

schema-less

RDBMSのテーブル定義やRRDのdata sourceの定義と違い、格納するデータ構造を予め定義しておく必要がありません。

過去のデータも登録可能

RRDと違って、任意の時点のデータを登録可能です。

SQL風の問い合わせ言語

結果はPerlの構造体で書いてますが、ほんとうはJSONで返ってきています。

  • 時刻で範囲指定
select reqtime, url from web9999.httpd where time > now() - 1h limit 1000;

[
  {
    columns => ["time","sequence_number","reqtime","url"],
    name => "web9999.httpd",
    points => [
                ["1392030886","60703580001","0.21","/view/entry"],
                ["1392030885","60703560001","0.07","/login/error"],
                ["1392030884","60703540001","0.32","/login/"],
                ...
              ]
  }
]
  • カラムの値で絞り込み
select reqtime, url from web9999.httpd where reqtime > 2.5;

[
  {
    columns => ["time","sequence_number","reqtime","url"],
    name => "web9999.httpd",
    points => [
                ["1392030883","60703520001","2.81","/list/entry"],
                ["1392030878","60703420001","2.72","/view/entry"],
                ["1392030877","60703400001","2.94","/login/error"],
                ...
              ]
  }
]
select reqtime, url from web9999.httpd where url =~ /^\/login\//;

[
  {
    columns => ["time","sequence_number","reqtime","url"],
    name => "web9999.httpd",
    points => [
                ["1392030885","60703560001","0.07","/login/error"],
                ["1392030884","60703540001","0.32","/login/"],
                ["1392030881","60703480001","0.78","/login/error"],
                ...
              ]
  }
]
  • 複数seriesに同時に問い合わせ
select * from /web9999\..*/;

[
  {
    columns => ["time","sequence_number","sys","user"],
    name => "web9999.cpu",
    points => [
                ["1392030886","60703590001",3,57],
                ["1392030885","60703570001",9,1],
                ["1392030884","60703550001",28,42],
                ...
              ]
  }
  {
    columns => ["time","sequence_number","reqtime","url"],
    name => "web9999.httpd",
    points => [
                ["1392030886","60703580001","0.21","/view/entry"],
                ["1392030885","60703560001","0.07","/login/error"],
                ["1392030884","60703540001","0.32","/login/"],
                ...
              ]
  }
]
  • 複数seriesに問い合わせ、結果をmergeする
select reqtime, url from web9999.httpd merge web0001.httpd;

[
  {
    columns => ["time","sequence_number","reqtime","url","_orig_series"],
    name => "web9999.httpd_merge_web0001.httpd",
    points => [
                ["1392030886","60706000001","1.09","/view/entry","web0001.httpd"],
                ["1392030886","60703580001","0.21","/view/entry","web9999.httpd"],
                ["1392030885","60705980001","1.53","/login/error","web0001.httpd"],
                ...
              ]
  }
]

select reqtime, url from merge /.*\.httpd$/
のように、merge対象のseriesを正規表現で指定できるとドキュメントには書いてありますが、まだ実装されてないみたいです。 https://github.com/influxdb/influxdb/issues/72

集約関数が豊富

COUNTやMIN, MAX, MEANなどなど。

RRDのDERIVEと同等のことができるDERIVATIVEや、パーセンタイルを算出するPERCENTILEもあります。

MIN,MAXはRDBMSで算出するのはめんどうくさい、DERIVATIVEはかなりめんどくさいので便利だと思います。

ちなみにDERIVATIVEとは、( v(t1) - v(t0) ) / (t1 - t0) を時刻 t1 の値とする関数です。具体的には、ネットワークの転送バイト数など、増加カウンタの値をデータとして格納しておき、グラフ描写時には Mbps (増加割合)を計算して使いたいときに便利です。

GROUP BY によるダウンサンプリングが可能

例えば1分ごとのデータを格納している場合、

select mean(sys) from web9999.cpu group by time(15m);

で、15分おきにまびいてその間の平均値を求められます。

RRDでは一つのDSについて、保持粒度の異なる複数のRRAを定義しますが、それと同じ事をオンデマンドでできるわけです。

Continuous Query

問い合わせの度にいちいちGROUP BYでダウンサンプリングを行うのは効率的ではありません。そこで Continuous Query というものを登録しておくことで、定期的にダウンサンプリングを行いデータをまびくことができます。

用途を限定したRDBMSのtriggerのようなものです。


例えば

select mean(sys) from web9999.cpu group by time(15m)
  into web9999.cpu.15m;

のような Continuous Query を登録しておくと、自動的に web9999.cpu.15m にまびいたデータを入れてくれます。

この Continuous Query と次の Regularly Scheduled Deletes (後述)を活用すると、

  • もっとも粒度の小さい1分おきのデータは2週間分保持
  • 15分の平均データは2ヶ月分保持
  • 60分の平均データは永久保持

といったことが可能になりそうです。

Regularly Scheduled Deletes

指定した期間より古いデータを自動的に削除する機能があるようですが、InfluxDB 0.5.4ではまだ実装されていないようです。 https://github.com/influxdb/influxdb/issues/98

InfluxDB のいまいちなカワイイところ

InfluxDB v0.5.4 (git: 93e93fa8986ab782b60087aa5b194113af8e16e5) での感想です。

将来的には修正されるものもあると思うので注意してください。

データとして構造をもったJSONを登録できない

そういう仕様なので、フラットに展開して入れるしかないと思います。

{
  "a": {
    "i": "foo",
    "j": "bar",
  },
  "b": 3
}

{
  "a.i": "foo",
  "a.j": "bar",
  "b": 3
}

のような感じに平坦化するとか。

集約関数を複数カラムに使えない (修正済み)

select max(col1), max(col1) の結果の columns は ["max", "max"] になるので、データは返ってくるけどどっちが col1 の max が区別ができない。

SQLの様にカラムにもasで別名が付けられるようになればいいんだけど。(seriesにはasで別名をつけられる)

issue にも上がっているのでそのうち対応されるかも。

https://github.com/influxdb/influxdb/issues/69 → 対応されました! v0.5.0-rc3にはカラムの別名が使えるようになると思います。

v0.5.1だとasを使ってこんな感じでできるようになりました。

[9] pry(main)> select mean(sys) as sys, mean(user) as user from myhost.cpu group by time(10m) limit 10;
+---------------+--------------------+--------------------+
|                       myhost.cpu                        |
+---------------+--------------------+--------------------+
| time          | sys                | user               |
+---------------+--------------------+--------------------+
| 1359676200000 | 23                 | 52.599999999999994 |
| 1359675600000 | 28.9               | 41.29999999999999  |
カラム名予約語をつけると問い合わせ不能になる

例えば「in」というカラム名をつけると、select in from がエラーになる。select 'in' from とすればいいんだけど、今度は集約関数が使えなくなる。select count('in') は動かない。

あるdatabase内のseriesの一覧を得る API がない

select * from /./ limit 1で代用するしかないみたい。

と思っていたがlist series というクエリーを投げればよい。

が!、バグってて動かない。https://github.com/influxdb/influxdb/issues/358

データのバックアップ方法が不明

公式ドキュメントにバックアップについて書いてある文章がない。

LevelDBのデータファイルをファイルコピーすればいいのかもしれないけど、整合性が保たれるか、バックアップデータとして正常に使えるのか不明。

バックアップに限らず運用周りはこれからの感じがする。

目玉の Continuous Query 周りがいろいろアレ

database を削除しても Continuous Query の定義は消えないで残る。(仕様?)

前述の通り、カラム名の別名付与ができないので、select max(sys), max(user) from myhost.cpu into ... な Continuous Query を定義してselectすると、maxが2つ返ってきて区別できない。

今のところ、カラムごとに別の Continuous Query を定義するしかない。→カラムの別名が実装されたv0.5.0-rc3以降なら問題ないかも

v0.5.1だとこんな感じでできるようになりました!!

[25] pry(main)> select mean(sys) as sys, mean(user) as user from myhost.cpu group by time(10m) into myhost.cpu.10m
 
[24] pry(main)> select * from myhost.cpu.10m limit 10;
+---------------+-----------------+--------------------+--------------------+
|                              myhost.cpu.10m                               |
+---------------+-----------------+--------------------+--------------------+
| time          | sequence_number | sys                | user               |
+---------------+-----------------+--------------------+--------------------+
| 1359676200000 | 1               | 23                 | 52.599999999999994 |
| 1359675600000 | 1               | 28.9               | 41.29999999999999  |

Continuous Queryを定義した時点で、当該seriesにある前データ(過去のも)を処理してダウンサンプリングする。過去のデータを対象にできるのはこのときのみ。

なので、CQを定義した後で過去のデータを入れてもダウンサンプリングされない。検証時に過去のデータを入れてる時や、実運用でも過去のデータを入れる要件がある場合は注意。

ただ、再計算できるようにするというissueが上がっているので要注目。
https://github.com/influxdb/influxdb/issues/211

ほか

rootのパスワードの変更

↓で、できます。

curl -u root:root -X POST 'http://127.0.0.1:8086/cluster_admins/root' -d '{"password": "root"}'
PerlからInfluxDBにアクセスするのは

拙作の InfluxDB を使うといいと思います!

Imoutable Infrastructure

早速作りました。

Ubuntu 12.04 LTS で動作確認しました。Ubuntu じゃないと動かないと思います。

# aptitude install update-motd
# rm -f /etc/update-motd.d/*
# vi /etc/update-motd.d/99-imoutable
(後述)

# grep UsePAM /etc/ssh/sshd_config
UsePAM yes
(UsePAM yesじゃなかったらyesにして service ssh restart)

# grep motd /etc/pam.d/sshd
session    optional     pam_motd.so # [1]
(pam_motdがコメントアウトされていたらコメントを外す)

動作例


/etc/update-motd.d/99-imoutable

#!/bin/bash

imouto=(
  'こんにちは!お兄ちゃん!!'
  '久しぶりだね!さびしかったよ。。'
)

nmesg=${#imouto[@]}

idx=$(($RANDOM % $nmesg))
echo
echo ${imouto[$idx]}
echo

LimeChat for Macでjoinやpartメッセージを非表示にする方法 〜console編〜

自分はディスプレイの最下にLimeChatのconsole(全チャンネルのメッセージが流れるpane)が常に見えるようにしてチラ見してるんですが、join, partの嵐にメッセージがざーっと流されちゃうことがあってウムーと思ってたところ、

mac版はwebkitレンダリングしてるのでCSSいじれば簡単でした。

使ってるテーマのCSSファイルに次を追加するだけです。 デフォルトのテーマは /Application/Limechat.app/Contents/Themes にあります。

下の例ではjoin, quit, mode, part, nickを表示しないようにしてます。

Limechat for Macでjoinやpartメッセージを非表示にする方法 - モノノフ日記

おーなるほどーということでこんな感じのCSSを追加してみました。

.console .line[type=join] { display:none; }
.console .line[type=quit] { display:none; }
.console .line[type=mode] { display:none; }
.console .line[type=part] { display:none; }
.console .line[type=nick] { display:none; }
.console .line[type=reply] { display:none; }

ちなみにあたってるCSSのclassや属性は、右クリのCopy Log as HTMLで確認できます。

Redisのクエリーアナライザー "redis-traffic-stats" を書きました

redis-traffic-stats という Redis のクエリーアナライザーを作りました。


redis-traffic-statsはtcpdump -wで書き出したpcapデータを解析して、以下のような統計を表示します。

  • 総ネットワークトラフィック量と平均byte/sec
  • 総リクエスト数と平均とピークのreq/sec
  • コマンド毎のリクエスト数、総リクエスト数に占める割合、req/secを、リクエスト数が多い順に上位10コマンドを表示
  • コマンド毎の総転送バイト数、byte/secを、総転送バイト数が多い順に上位10コマンドを表示
  • コマンド別に、キー毎の総転送バイト数、byte/sec、リクエスト数、リクエスト数の割合、req/secを、総転送バイト数が多い順に上位10キーを表示
  • 時間のかかったリクエストのワースト20

解析結果はそのままでも読めますが、GitHub Flavored Markdownなので、gistとかにコピペすると整形したのが見られます。

サンプルはこちら→ https://gist.github.com/hirose31/9207096

類似ツールとの比較

類似のRedisのクエリアナライザーには、Instagramが作った redis-faniaや、GUIRedisLive があります。

これらのツールはRedisのMONITORコマンドを使ってデータを収集していますが、redis-traffic-statsはMONITORコマンドではなく、pcapデータをデータソースとして使っています。

pcapデータを使う利点は以下の2つです。

  • 性能劣化がない
  • 転送量の統計値がとれる
    • MONITORコマンドではレスポンスを知ることができないので、転送量がとれません

さっそく、使ってみる

詳しくは https://github.com/hirose31/redis-traffic-stats を参照ください。ここではざっくり書きます。

redis-traffic-statsはPerlで書かれていて、CPANからインストールできます。

cpanm App::redis_traffic_stats

でインストールしてください。必ずしも、Redisが動いているサーバーにredis-traffic-statsを入れる必要はないです。

redis-traffic-statsが使っているNet::Pcapがlibpcap.soを要求するので、rpmだとlibpcap-develを予めインストールしておく必要があります。


redis-server が動いているマシンで、pcapデータを採取します。

tcpdump -s 65535 tcp port 6379 -w redis.pcap -i eth0
(しばらくしたら ^C で終了する)
  • -iのデバイス名は、bond0などに適宜書き換えてください。
  • -c 10000と指定して、10000パケット採取したら終了するようにしてもよいでしょう。


データが採取できたら、redis-traffic-statsをインストールしたマシンにpcapデータファイルを持ってきて解析します。

redis-traffic-stats -r redis.pcap


redis-traffic-statsに-rを指定しなければ、redis-traffic-statsがtcpdumpを実行してpcapデータを採取し、それを元に解析することもできます。

おわりに

運用するにあたり、例えば CloudForecastのRedisのグラフ のような、定常的なリソースモニタリングはもちろん必要ですが、突発的に謎い感じでRedisのトラフィックが跳ねてる最中に、どんなコマンドのどんなキーがトラフィックを食っているか確認するのにこのredis-traffic-statsは有益だと思います。