evryの実装をシンプルにした

evry を公開してからオンラインオフライン問わず様々な場でフィードバックをいただいて、それをもとに修正したv0.2.0をリリースしました。

github.com

指定コマンドを実行するための実装をシンプルにした

Use `sh -c` instead of go-shellwords by k1LoW · Pull Request #3 · k1LoW/evry · GitHub

もともとevryでは、オプションに渡された指定コマンドの文字列を、go-shellwords を利用してパースし、パイプ処理を go-pipelineの実装を参考に 組み込んで実行するようにしていましたが、シンプルに sh -c に文字列を渡して実行するように変更しました。

evryにオプションだけでなく引数でもコマンドを渡せるようにした

Allow specifying of command by args by k1LoW · Pull Request #4 · k1LoW/evry · GitHub

具体的には従来の -c オプションによるコマンド指定だけでなく、

$ tail -F access.log | evry -s 10 -c 'wc -l'

以下のようにそのまま引数で渡せるようにもしました。

$ tail -F access.log | evry -s 10 -- wc -l

両方利用できます。

watchコマンドと同じ引数に揃えるのはあきらめました

watch コマンドは -n (--interval) で実行間隔(秒)を指定できるのですが、evryは秒でまとめるだけでなく行数でもまとめることができるので、 --interval は使いづらく、変更せず従来のままのオプションとしました。

公開することで得られるフィードバック

ツールやコードを公開することで得られるフィードバックは本当にありがたいですし、特に今回は勉強になりました。

こちらが勝手にフィードバックを拾っていった方もいるのであえてお名前は挙げませんが、 「俺かな?」と思われた皆さんありがとうございますー!!

tail -Fなどの標準出力を一定時間ごとに指定のコマンドに渡し、その実行結果をシーケンシャルに出力するコマンド evry を作った

ログの流量やサマリを、カジュアルに、そしてリアルタイムに確認したかったので作ってみました。

github.com

これはなに

標準入力の内容を、一定時間ごと(もしくは一定行ごと)にまとめて指定のコマンドに渡し、その実行結果をシーケンシャルに出力するコマンドです。

使い方

例えば、「NGINXの10秒あたりのリクエスト数を確認する」には、以下のように evry で10秒ごとにまとめた標準入力を wc -l に渡します。

$ tail -F /var/log/nginx/access.log | evry -s 10 -c 'wc -l'
    8700
    1260
    8580
    6430
    [...]

例えば、「Apacheの1000リクエストあたりのアクセス先ランキング上位5を確認する」には、以下のように書けます(GNU sedを使用)。

$ tail -F /var/log/httpd/access.log | evry -l 1000 -c 'cut -d" " -f7 | sort | uniq -c | sort -nr | head -5 | sed "0,/^/ s/^/---\\n/g"'
---
  90 /category/electronics
  65 /category/software
  55 /category/games
  45 /category/books
  27 /category/computers
---
 108 /category/electronics
  76 /category/software
  63 /category/games
  44 /category/books
  31 /category/computers
---
  97 /category/electronics
  55 /category/games
  52 /category/software
  48 /category/books
  35 /category/office
---
  99 /category/electronics
  55 /category/software
  53 /category/games
  [...]

他のコマンドを使ったり組み合わせた書き方はあるかと思うのですが、直感的に書ける のが良いと思っています。

インストール

go get か Homebrewでインストールできます。

$ go get github.com/k1LoW/evry
$ brew install k1LoW/tap/evry

シーケンシャルに入力を受け取り、パラレルに実行し、シーケンシャルに出力する

evry を実装するにあたって実現したかったのが、

  1. 標準入力はリアルタイムに受け取って処理したい
  2. 出力は1の標準入力から受け取った順に出力したい(そのほうが 直感的 だと考えているから)

ということです。そのために、

  • 標準入力からの値を、都度 -c のコマンドに渡して、コマンド自体はそれぞれ並行に実行することによって、時間のかかるコマンドでもできるかぎり処理をブロックしないようにする
  • 並列に実行しているコマンドの実行時間によって出力順番が変わらないようにする

という挙動を実装しました。

具体的には図のように

f:id:k1LoW:20190114200758p:plain
evryの処理の流れ

  1. 標準入力から流れてくる値を一定時間もしくは一定行にまとめる(緑)
  2. 「入力の順番」を記憶する(オレンジ左)。
  3. 指定コマンドの標準入力に1でまとめた値を渡して、それぞれ並行に実行させる(青)。
  4. 実行結果の出力は記憶している「入力の順番」に出力する(オレンジ右)

という動きをします。

上記の evry の動きの例としては、以下のコマンドを実行するとわかりやすいかと思います。

$ echo "3\n5\n2" | evry -l 1 -c 'xargs -I@ sh -c "sleep @; echo @"'

コマンドは、352 をそれぞれ xargs -I@ sh -c "sleep @; echo @" に渡してその実行結果を出力する(@秒sleepしてecho @するだけ)というものです。

指定コマンドに sleep が入っているので、それぞれ3秒、5秒、2秒待つような挙動をします。

上記を実行すると、約3秒後に3が、その約2秒に 52 が一気に出力されます。

time を挟むとわかりやすいです。

$ time echo "3\n5\n2" | evry -l 1 -c 'xargs -I@ sh -c "sleep @; echo @"'
3
5
2
echo "3\n5\n2"  0.00s user 0.00s system 38% cpu 0.001 total
evry -l 1 -c 'xargs -I@ sh -c "sleep @; echo @"'  0.01s user 0.02s system 0% cpu 5.019 total

実行時間が(10秒ではなく)約5秒になっている ことがわかります。

実行時間からわかるように、'xargs -I@ sh -c "sleep @; echo @"' を並列に実行しています。

かつ、その出力順は、それぞれの実行時間(3秒、5秒、2秒)に関わらず352と元の標準入力の順を保っています。

Goの並行処理は自分にはまだ難しいけれどうまくハマると面白い

今回、evry で実現したかった「シーケンシャル->パラレル->シーケンシャル」を自分なりに sync.Mutexsync.WaitGroup とチャンネルを組み合わせて実装できました。面白かったです。

うんうん唸って設計して実装しただけに、意図したとおりに動いたときはテンションが上りました。Goで並行処理を実装する良いお題だったのかもしれません。

そして、もう少し並行処理を実装する感覚に慣れていく必要がありそうです。

今は、答え合わせの意味も含めて「Go言語による並行処理」を読み進めています。

読んでいて、全く思いついたことのないパターンに「なるほど」と思ったり、実装したことのあるパターンがすっきりと整理されていたり、とても勉強になります。「Go言語による並行処理」で学んだ内容をもとに evry の実装をもっと良くできそうな気がします。

Go言語による並行処理

Go言語による並行処理

2018年の振り返りと2019年の抱負

明けてました

2018年の振り返り

2018年は個人的にいろいろ環境が変わったので2017年と同じようにはいきませんでした。いい意味でも悪い意味でも。

2018年の目標は「努力を積み重ねる」でした。

これは、転職をして新たなチームにJOINしたことで半強制的に努力をし続けないといけない状況になりました(狙い通り)。

チームメンバー全員が全員専門領域をもって強いので(中には全方位型もいたり)、迷惑をかけないようにしつつ価値も出していくのが本当に厳しい。

特に「サーバに何かあったときの瞬発力」と「コンテナ技術とはじめとするLinuxを構成する要素の知識とアプリの実装が深く絡むときの判断力」が厳しいと感じました。これは今後の課題です。

チームメンバーだけでなく、席的に後ろにいるインフラチームや、眼の前のプロダクトチーム、CTL、技術部、Slackの先にいる東京の人たちをみていて、その動き(実装力、特定力、判断力)の速さに圧倒される状況がもよくありました。

その分、自分の技術スタックを広げることに集中できた気がします。

目標「努力を積み重ねる」に対する結果としては良かったです。この初心を忘れないでおきたいです。

2018年の個人的ハイライトとしては

  • 転職そしてGMOペパボに入社した
  • 子供が生まれた
    • 働き方が大きく変わることになりました。チームメンバーにも会社にもいろいろ配慮してもらっていて感謝しかありません。まだ試行錯誤段階ですが、一段落したらまとめたいと思っています。
  • プリンシパルエンジニアになった

の3つです。

特に子供が生まれたことで他のハイライトがすべて吹っ飛びました。2018年買ってよかった大賞も圧倒的にピジョン 電子レンジスチームです。

OSS活動

Go製のものが増えました。あと実装力や技術領域に慣れていないことが原因かもしれませんが、作り上げるのに時間がかかることが多くなりました。ポジティブには捉えています。

ちなみに個人的お気に入りは tbls なのですが、いまいち認知度があがりません。2019年にもう1手打ちたいと思っていて、2018年に少し仕込んでいます。

発表

2017年と比べると少なくなりました。特に後半は家庭の事情で応募自体を控えていました。

2019年の抱負

今年は「変化に適応することを楽しむ」です。公私ともに環境が大きくかわったところで2018年が終わりました。2019年はその変化した環境からスタートです。

今までと同じような生活や、今までと同じようなアウトプットをしていくことはおそらくできないでしょう。

同じでないことは許容しても、そのせいで様々な面で劣化してしまうことは避けたいです。

変化した環境に適応して楽しんで、そして力にするところまでを目指したいです。

というわけで

今年もどうぞよろしくお願いします。

~/.ssh/configを使って *ssh.Client を作成する

GMOペパボ Advent Calendar 2018 の4日目の記事です。

運用しているサーバに何か問題が発生したら、SSH接続をして原因の特定をします。特定するためにいろいろ確認をします。

しかし、自分はチームメンバーの中では上記があまり速いほうではありません。勘所もまだまだ悪いです。

そこで、その差を埋めるべく、最低限の確認を一気にできるようにしようと考えてGoでツールを作ろうとしています(おそらくシェルスクリプトでもAnsibleでもいいのですが、なんとなく作った方が良さそうな気がしています。まだなんとなくで確証はありません)。

~/.ssh/config を読む

そこで、まずはSSHクライアントを書こうと思ったのですが、GoにはRubyNet::SSH::Config のような ~/.ssh/config を読むような機能は標準パッケージにはないようです。

探してみたところ、kevinburke/ssh_config に ~/.ssh/config を読む機能があったので、これを使って ~/.ssh/config の設定情報を使ってSSH接続をしてみます。

まず以下のような ~/.ssh/config があるとして

Host myhost
  HostName 203.0.113.1
  User k1low
  Port 10022
  IdentityFile /path/to/myhost_rsa

以下のように kevinburke/ssh_config で ~/.ssh/config を読んで、それを ssh.ClientConfig に設定することで、~/.ssh/configの情報でSSH接続ができます。

package main

import (
  "bytes"
  "io/ioutil"
  "log"

  "github.com/kevinburke/ssh_config"
  "golang.org/x/crypto/ssh"
)

func main() {
  host := "myhost"
  user := ssh_config.Get(host, "User")
  addr := ssh_config.Get(host, "Hostname") + ":" + ssh_config.Get(host, "Port")
  auth := []ssh.AuthMethod{}
  key, _ := ioutil.ReadFile(ssh_config.Get(host, "IdentityFile"))
  signer, _ := ssh.ParsePrivateKey(key)
  auth = append(auth, ssh.PublicKeys(signer))
  sshConfig := &ssh.ClientConfig{
    User:            user,
    Auth:            auth,
    HostKeyCallback: ssh.InsecureIgnoreHostKey(), // FIXME
  }
  client, _ := ssh.Dial("tcp", addr, sshConfig)
  session, _ := client.NewSession()
  defer session.Close()
  var stdout = &bytes.Buffer{}
  session.Stdout = stdout
  err = session.Run("hostname")
  if err != nil {
    log.Fatalf("error: %v", err)
  }
  log.Printf("result: %s", stdout.String())
}

(エラー処理などいろいろ省略)

ProxyCommandに対応してみる

対象サーバの前段に踏み台サーバがある場合、ProxyCommand を記述して利用します。

kevinburke/ssh_configssh_config をパースするライブラリで、ProxyCommand を解釈して実行するところまではサポートしません。

ところで、ProxyCommandに記載されているコマンドはローカルで実行されます。そして、大抵はProxyCommand に書かれた ssh -Wnc を使って確立したSSH通信を経由して(プロキシして)、目的のホストへSSH接続をすることになります。

ようは、通信をパイプでつなげられればいいはずなので net.Pipe() を使います。

c, s := net.Pipe()
cmd := exec.Command("sh", "-c", proxyCommand)
cmd.Stdin = s
cmd.Stdout = s
cmd.Stderr = os.Stderr
if err := cmd.Start(); err != nil {
  return nil, err
}
conn, incomingChannels, incomingRequests, err := ssh.NewClientConn(c, addr, sshConfig)
if err != nil {
  return nil, err
}
client := ssh.NewClient(conn, incomingChannels, incomingRequests)

%h の変換処理などいろいろ省略)

これでProxyCommandを利用したSSH接続ができます。

sshc

上記に書かれているような処理をまとめてsshcというパッケージを作りはじめました。

github.com

使い方は以下のような感じで sshc.NewClient("myhost") で ~/.ssh/config を解釈してProxyCommandでのプロキシもした *ssh.Client を得ることができます。

package main

import (
    "bytes"
    "log"

    "github.com/k1LoW/sshc"
)

func main() {
    client, _ := sshc.NewClient("myhost")
    session, _ := client.NewSession()
    defer session.Close()
    var stdout = &bytes.Buffer{}
    session.Stdout = stdout
    err = session.Run("hostname")
    if err != nil {
        log.Fatalf("error: %v", err)
    }
    log.Printf("result: %s", stdout.String())
}

とりあえず手元にある ~/.ssh/config に対応できればいいかな、というゆるい感じで作っています。

TODOとしては

  • 公開鍵のパスフレーズに対応
  • 現在書いているテストが実際にSSH接続が確立するかのテストで公開できないのでなんとかしたい
    • 多段SSHのテストってどう書けばいいのですかね。。
  • exec.Command() を使っていて結局 ssh コマンドを使っている
    • 若干あきらめています
  • kevinburke/ssh_configssh_config のファイルパスがprivateになっていて変更できない
    • Pull Request案件

など。

動くようになったのでまずは作りたかったツール作成に移りたいと思っています。

auditdのログをlogrotateでローテートする運用にしたかったがうまくいかなかったのでcronで直接ローテートするようにした

紆余曲折の記録です。

Auditとは何か

Linux AuditはLinuxのシステムで発生しているイベントを記録するための仕組みです。詳しくは以下のページ

第5章 システム監査 - Red Hat Customer Portal

具体的には、(デフォルトで) /var/log/audit/audit.logシステムコールやユーザが実行したコマンドなどの様々なシステムイベントを記録するサービス auditd が起動することになります。

auditdのログ (audit.log) を長期保存したい

audit.logはそのまま監査ログとして利用できるため、一定期間保存しようと考えます。

しかし、前述のようにauditdは様々なシステムイベントを記録するために単純にログの保存を続けるとかなりのディスク容量を消費していきます。

そのためログローテーションの設定を組むのが一般的であり、それ以前にauditd自体にはログローテーションの機構が備わっており、デフォルトで有効になっています。

しかし、そのままローテートをしていると端から消えていくことになるので、ローテートされた古いログを保存する仕組みが必要になります。

あわせて、以下の3つの要件を実現しようと考えました。

  1. 時間ベースでローテートをしたい
    • あとでログを見つけやすくするため。ログローテートされたaudit.logに時刻のプレフィックス (ex. audit.log-2018111115 ) をつけたい
  2. ローテーション時に圧縮して保存したい
    • ログファイルが大きくなりがちのため
  3. audit.logのみで保存したい

ぱっと見、logrotateに任せたほうが良さそうです(しかし、後述しますが失敗しました)

logrotateの前に検討したいくつかの方法

auditd の設定でなんとかする

auditd.confの設定パラメータを見る限りでは時間ベースのローテートの仕組みや古いログを圧縮するためのフック機構などはなさそうでした。

audisp pluginを利用してsyslogに記録する

audisp pluginを利用して、auditdのログをsyslogに記録して、syslogに設定したログローテーションにのっかるという方法があります。

  • auditd.conf で dispatcher = /sbin/audispd を指定
  • /etc/audisp/plugins.d/syslog.conf を有効化
active = yes
direction = out
path = builtin_syslog
type = builtin
args = LOG_INFO
format = string

これでsyslogがlogrotateでログローテートされてれば、合わせてaudit.logの情報もローテートされるという仕掛けです。

しかし、今回はaudit.logのみで保存をしたいのとsyslogにもよく参照する情報があって混在するのを避けたかったため、この方法は見送ることにしました。

audit.logを直接logrotateの仕組みに載せる -> 失敗

これが本命 ですでした。

auditdが独自にログローテーション機能を止めてlogrotateの仕組みに載せることができれば、「時間ベースのローテーション」も「古いログの圧縮保存」も「audit.logのみでのローテーション」も実現できると考えました。

まず、/etc/audit/auditd.conf の max_log_file_action の設定を変更して auditdのログローテーションを止めます。

max_log_file_action = IGNORE

そしてlogrotateの設定を追加します。

   /var/log/audit/audit.log
    {
      rotate 24
      hourly
      ifempty
      dateext
      missingok
      compress
      delaycompress
    }

これでauditdを再起動して、確認しましたが 結果、うまくいきませんでした

ログファイルはローテートするのですが、auditdが audit.log ではなく、ローテートしたほうの audit.log-2018111115 の方にそのままログを書き続けてしまいました。

原因は、auditdが最初に掴んだログファイルのファイルディスクリプタにそのまま書き込んでいるからだと予想できます。

ちなみに、logrotateで考えるべきことについては @catatsuy さんの以下のエントリがとてもわかりやすかったです。

medium.com

ここまでで、auditdは上記エントリで示されている「ちゃんとローテートをしたい場合」の3つの方法のうちの 「1. ファイルに書き込む度にファイルをopenして書き込み後にcloseする」という挙動にも「3. ファイル名が変更されたタイミングを自力で検知して、元のファイル名でopenし直す」 という挙動にもなっていないということがわかりました。

なので、最後に残った「2. ファイル名が変更されたタイミングを教えてもらい、元のファイル名でopenし直す」方法を考えてみました。

しかし、auditdにはNGINXのUSR1のような「元のファイル名でopenし直す」ようなシグナルは用意されておらず、これも断念することになりました。おそらくauditd自体がログローテーションの仕組みを持っているので、そのようなシグナルを受け取る処理を作る必要がなかったのではないか?と予想しました。

logrotateで、ファイルディスクリプタを掴むようなプロセスのときに使うらしい copytruncate オプションを付与すれば一見うまくいくのですが、ログファイルをコピーして(copy)元のファイルの内容を消去する(truncate)するときにログを一部ロストしてしまう可能性があるということで(監査ログという意味でも)選択できませんでした。

cronで直接ローテートするようなスクリプトを組む

というわけでlogrotateの仕組みに載せるのはあきらめて、直接cronをトリガーに意図したログローテーションをするようにスクリプトを組みました。

#!/bin/bash

set -eu

DATEEXT=`date +%Y%m%d%H`
KEEP=5
[ -f /var/run/auditd.pid ] && kill -USR1 `cat /var/run/auditd.pid`
[ -f /var/log/audit/audit.log.1 ] && mv /var/log/audit/audit.log.1 /var/log/audit/audit.log-${DATEEXT}
[ -f /var/log/audit/audit.log-${DATEEXT} ] && gzip -f /var/log/audit/audit.log-${DATEEXT}
ls -t /var/app/audit.log-* | tail -n+${KEEP} | xargs --no-run-if-empty -i mv {} /backup/'

仕組みとしては以下のページの方法と同じです。

How to implement audit log rotation with compression based on time instead of size - Red Hat Customer Portal

  1. まずUSR1シグナルをauditdプロセスに渡すことで、auditdのログローテーション機能を実行します。
  2. 生成された audit.log.1 を時刻つきファイル名にリネームします
  3. 2でリネームしたファイルをgzipで圧縮します
  4. 古いログファイルから移動します

上記スクリプトをcronで1時間ごとに実行することで、なんとかやりたいことを実現することができました。

結局

audit.logを時間でローテートするベストな方法は何だったのか。。是非どなたか教えて欲しいです。

ここまでくるのに時間がかかってしまった。。

Fukuoka.go#12 で GoでTCPパケットを読むための最初の一歩の話 をした #fukuokago

Fukuoka.go に参加してきました!

fukuokago.connpass.com

主催者、発表者のみなさん、ありがとうございました!

ちょっとトラブルがあって楽しみにしていた「Road to your goroutines」をまともに聞けなかったので残念がっていたら、素晴らしいまとめエントリが上がっていて本当に感謝です!!

GoでTCPパケットを読む

最近、tcpdpというTCPパケットを読むツールを作っているのですが、その開発をするにあたって「Goもやっぱりライブラリが揃っているな」と感じたので、そういった意味での発表をしました。

tcpdpの話をすると、プロトコルの解析苦労話になりそうだったのでやめときました(Goっぽくない)

google/gopacket

スライドを見てもらったらわかるとおり、ほとんど google/gopacket の紹介みたいになっています。

それでも、あまりにも簡単にtcpdumpぽいものが作れてしまうので、最初、個人的には本当にびっくりしました。

発表で作った簡易tcpdumpはわずか32行です。

package main

import (
  "encoding/hex"
  "fmt"
  "log"

  "github.com/google/gopacket"
  "github.com/google/gopacket/pcap"
)

func main() {
  device := "en0"
  filter := "tcp and port 80"

  handle, err := pcap.OpenLive(
    device, int32(0xFFFF), true, pcap.BlockForever,
  )
  if err != nil {
    log.Fatal(err)
  }
  defer handle.Close()
  if err := handle.SetBPFFilter(filter); err != nil {
    log.Fatal(err)
  }

  packetSource := gopacket.NewPacketSource(handle, handle.LinkType())
  for packet := range packetSource.Packets() {
    fmt.Printf("%s\n", packet)
    fmt.Printf("%s", hex.Dump(packet.Data()))
  }
}

たったこれだけで動くtcpdumpっぽいコマンドが出来上がりです。

そうするとあとは、拡張したいと思ったところをいじればいいし、気になるところはコードから定義ジャンプをしていけば理解が深まります。

書いて動くと理解が進む

発表資料をみたらわかるように、自分はネットワーク周辺は雰囲気で触っていることが多いです(注意: 上記ツイートはわざと面白おかしくしてもらった結果です)

しかし、実際にコードに落とし込んで動くようになると、次の試行錯誤をすることができます。

試行錯誤ができるところまでできたら、次の「使えるツールが作れるかも」というモチベーションを持った開発ができます。

モチベーションを持てるとちょっとばかり面倒な部分についても調べたり検証したりする気になります。

そして、結果として周辺の理解が進むと感じています。

というわけで

今回の発表で、自分と同じように「ネットワーク周り得意じゃないな」と思っている人がちょっとでも「Goで書けるならやってみようかな」と思ってくれたら幸いです。

そういえば、今週末もずっと []byte{} を眺めてたな。。。

pprofでCPU100%になる原因を特定する

tcpdpのprobeモードで、CPUが100%に張り付くという問題(結果、原因としてはしょうもないミス)がありました。

機能としては正しく動いているので、単純にテストケースを増やしてもどうにもならず、pprofを使って原因となっている箇所を絞り込んでみました。

本エントリはその記録です。なお、原因特定の問題としてはおそらく初級レベルだと思います(わからない)。

現状把握

tcpdp probetcpdump のようにインターフェースに流れるパケットをキャプチャするコマンドです。

ところが、以下のように実行したら、まだパケットを流していないのにCPUが100%に張り付きます。

$ sudo tcpdp probe -c t.toml
Password:
2018-09-30T00:40:42.199+0900    info    Starting probe. interface: lo0, target: 33306
2018-09-30T00:40:42.201+0900    info    Select dumper mysql.
$ sudo ps au
USER    PID  %CPU %MEM      VSZ    RSS   TT  STAT STARTED      TIME COMMAND
root  69915 100.3  0.2  4402600  36692 s003  R+   12:14AM   5:17.36 ./tcpdp probe -c t.toml
root  43796   0.3  0.1  4405812   8708 s002  S+   10:44PM   0:25.13 ./tcpdp probe -c t.toml
[...]

net/http/pprof を設置する

とりあえず net/http/pprof を設置します。

$ git diff
diff --git a/main.go b/main.go
index bc38d4c..2a0119b 100644
--- a/main.go
+++ b/main.go
@@ -20,8 +20,19 @@

 package main

-import "github.com/k1LoW/tcpdp/cmd"
+import (
+       "log"
+       "net/http"
+       _ "net/http/pprof"
+       "runtime"
+
+       "github.com/k1LoW/tcpdp/cmd"
+)

 func main() {
+       runtime.SetBlockProfileRate(1)
+       go func() {
+               log.Println(http.ListenAndServe("0.0.0.0:6060", nil))
+       }()
        cmd.Execute()
 }

参考URL

golang の net/http/pprof を触ってみたメモ - sonots:blog

go tool pprof でCPUのプロファイルを見る

net/http/pprof を設置したので、もう一度 tcpdp probe を実行します。そうすると http://localhost:6060/debug/pprof/ でプロファイル情報のページが生成されます。

そのプロファイルページに go tool pprof でアタッチして、CPUのプロファイルを見てみます。

localhost:6060 で起動している pprof のプロファイルページの /debug/pprof/profilego tool pprof でアタッチして、top で確認してみます。

$ go tool pprof -seconds 5 http://localhost:6060/debug/pprof/profile
Fetching profile over HTTP from http://localhost:6060/debug/pprof/profile?seconds=5
Please wait... (5s)
Saved profile in /Users/k1low/pprof/pprof.samples.cpu.007.pb.gz
Type: cpu
Time: Sep 29, 2018 at 12:54am (JST)
Duration: 5.14s, Total samples = 4.40s (85.59%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top
Showing nodes accounting for 3870ms, 87.95% of 4400ms total
Dropped 10 nodes (cum <= 22ms)
Showing top 10 nodes out of 21
      flat  flat%   sum%        cum   cum%
    1440ms 32.73% 32.73%     3550ms 80.68%  runtime.selectgo 👈
     480ms 10.91% 43.64%      480ms 10.91%  runtime.unlock
     450ms 10.23% 53.86%      450ms 10.23%  runtime.lock
     360ms  8.18% 62.05%      360ms  8.18%  runtime.cputicks
     240ms  5.45% 67.50%      720ms 16.36%  runtime.selunlock
     240ms  5.45% 72.95%      240ms  5.45%  sync.(*Mutex).Unlock
     220ms  5.00% 77.95%      220ms  5.00%  sync.(*Mutex).Lock
     160ms  3.64% 81.59%      160ms  3.64%  runtime.(*hchan).sortkey (inline)
     140ms  3.18% 84.77%     4350ms 98.86%  github.com/k1LoW/tcpdp/reader.(*PacketReader).ReadAndDump 👈
     140ms  3.18% 87.95%      140ms  3.18%  runtime.duffzero

あああ、、、もう特定できてしましましたね。

runtime.selectgo が実行時間 (flat)では多いですが、関数内の呼び出しを含めた実行時間 (cum) をみると github.com/k1LoW/tcpdp/reader.(*PacketReader).ReadAndDump が支配的です。

この関数の中で何かをやらかしているのは明白です。

参考URL

Go pprof 入門編 (CPU Profile とコマンドラインツール) : KLabGames Tech Blog

go tool pprof -http を使ってWebUI上でグラフで確認する

ここで終わってもそっけないので、今度は pprof の WebUIを使ってグラフで確認してみます。

localhost:6060 で起動している pprof のプロファイルページにアタッチして 8888 で pprof の WebUIを立ち上げます。

$ go tool pprof -http=":8888" localhost:6060

するとWebページが開いて、グラフを確認することができます。

f:id:k1LoW:20180930121902p:plain

あああ、ReadAndDumpさん。。。

参考URL

Go 1.10でGolangのプロファイリングツール pprof のWebUIが入った話 - Keepdata Blog

go-torch を使ってFlame Graphで確認する

懲りずにさらに、go-torch を使ってFlame Graphで確認してみます。

まず、準備として go-torch と FlameGraph を落としておきます。

$ go get github.com/uber/go-torch
$ git clone https://github.com/brendangregg/FlameGraph

localhost:6060 で起動している pprof のプロファイルページにアタッチしてFlame Graphのsvgファイルを生成します。

$ PATH=$PATH:/path/to/FlameGraph go-torch --time 5 --url http://localhost:6060/debug/pprof/profile

f:id:k1LoW:20180930122026p:plain

。。。。ReadAndDumpで決定ですね。

参考URL

GolangでFlame Graphを描く | SOTA

原因特定

原因はReadAndDumpで実行しているforの中のselect句で、「何もしないdefault:が存在している」ことで、ビジーループになっていたことでした。しょうもないバグ。

default: を取り除き、最終的にpprof のプロファイル結果も以下のようにスッキリしました。

$ go tool pprof -seconds 5 http://localhost:6060/debug/pprof/profile
Fetching profile over HTTP from http://localhost:6060/debug/pprof/profile?seconds=5
Please wait... (5s)
Saved profile in /Users/k1low/pprof/pprof.samples.cpu.013.pb.gz
Type: cpu
Time: Sep 29, 2018 at 2:27am (JST)
Duration: 5s, Total samples = 20ms (  0.4%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top
Showing nodes accounting for 20ms, 100% of 20ms total
      flat  flat%   sum%        cum   cum%
      20ms   100%   100%       20ms   100%  github.com/k1LoW/tcpdp/vendor/github.com/google/gopacket/pcap.(*Handle).getNextBufPtrLocked.func1
         0     0%   100%       20ms   100%  github.com/k1LoW/tcpdp/vendor/github.com/google/gopacket.(*PacketSource).NextPacket
         0     0%   100%       20ms   100%  github.com/k1LoW/tcpdp/vendor/github.com/google/gopacket.(*PacketSource).packetsToChannel
         0     0%   100%       20ms   100%  github.com/k1LoW/tcpdp/vendor/github.com/google/gopacket/pcap.(*Handle).ReadPacketData
         0     0%   100%       20ms   100%  github.com/k1LoW/tcpdp/vendor/github.com/google/gopacket/pcap.(*Handle).getNextBufPtrLocked

f:id:k1LoW:20180930122104p:plain

該当の修正Pull Requestはこちら

まとめ

今回はpprofを使ってCPU100%張り付きの原因特定をしてみました。

自分は、こういう「計測」には慣れていないので、これからも素振りをしていこうと思います。