【3,4日目】graqt開発日誌 ~PercentileとStddev(標準偏差)~

f:id:serinuntius:20180820080328p:plain

「べ、別に土日早起き出来なかったからブログ書けなかったわけじゃないんだからね。。。」

冗談はさておき、土日はISHOCON2やgraqtの開発に力を入れてしまい、記事を書けませんでした。

いろいろ機能が追加されたので、紹介したいと思います。

新機能

Percentileが見れるようになりました

graqtはbetter than alpを標語として、作っているのですが、alpにあるPercentileを移植しました。

P1, P50, P99とかのことです。

なんで、これが必要なのか調べていると、レスポンスタイムの平均(Avg)だけ見てしまうと、スロークライアントの結果に引っ張られる可能性があるからです。

参考 takeshiyako.blogspot.com

Nginxの$request_timeは、nginxがリクエストを受けてからレスポンスをクライアントに返しきるまでの時間なので、変に遅いクライアントが居ないとも限らない・・・。 平均値だと、極端な異常値が少しでもあったばあい、これに引きづられて数字が大きくなったりして、あまり信用できなくなったりします。 測定誤差をできるだけ減らしたいので、平均値ではなくてパーセンタイルを取ろうという考え方です。

ベンチマーカー相手だとあんまり考えなくてもいいかもですが(わざわざスロークライアントになるようなベンチマーカーって過去のISUCONでないよね?)、本番のアプリケーションでは重要ですね。

Stddev(標準偏差)が見れるようになりました

標準偏差も、かなりわかりにくい指標なのですが、こちらの記事にわかりやすく書いてありました。詳しく知りたい方はこちらの記事をお読みください。

atarimae.biz

この機能もalpについている機能で、そのままパクリましたリスペクトして実装しました。

Pathを集約して見れるようになりました

これもalpリスペクトの機能で、alpではAggregate機能として実装されています。

例えばですけど、

/diary/entry/1
/diary/entry/2
/diary/entry/3

こういう感じのリクエストがあると、集約しないとそのまま3行で表示されてしまいますが、設定ファイルに正規表現を書いておくと集約して1行でまとめて表示できます。

graqt.yaml

aggregates:
  - /diary/entry/\d+

このように書いておくと、集約され表示されるようになります。

実際の例を見たほうがわかりやすいと思うので、貼っておきます。

これが、 f:id:serinuntius:20180820074544p:plain

こうなります!!! f:id:serinuntius:20180820074624p:plain

けど、これ使うと「一部のページだけ異様に遅い!」みたいなのがあった場合わかりにくいので気をつけてください。

レスポンスボディのサイズが表示されるようになりました

今まで項目としてはちゃんと用意されていたのですが、ロガーの方で取得していなかったので、ちゃんとレスポンスボディのサイズを取得してロガーに載せるようにしました。

f:id:serinuntius:20180820075216p:plain

やったぜ!

ginというフレームワークに(一部)対応しました

ginというフレームワークは、http.Handlerを準拠して作られているわけではないので、それようのmiddlewareを作るしかありませんでした。(たぶん)

個人的には、http.Handlerに準拠しないフレームワークはgo wayじゃないと思っていて、いろいろな制約がつきまとって非常に面倒くさいことになります。

会社の先輩方が、「Go書くときは薄いフレームワークしか使わなくて良い」とおっしゃっていた理由が今回とても身にしみました。

具体的には、Ginのmiddlewareのシグネチャーにあわせて書いてあげるという感じです。めっちゃ処理が増えるとかではないですが、わざわざ1つのフレームワークのためにどんどんを作るのは非常に面倒です・・・。

func RequestIdForGin() gin.HandlerFunc {
    return func(c *gin.Context) {
        t1 := time.Now()

        id := newRequestID()

        c.Set(string(RequestIDKey), id)

        c.Next()

        RLogger.Info("",
            zap.Duration("time", time.Since(t1)),
            zap.String("request_id", id),
            zap.String("path", c.Request.RequestURI),
            zap.String("method", c.Request.Method),
            zap.Uint64("content_length", uint64(c.Writer.Size())),
        )

    }
}

https://github.com/serinuntius/graqt/blob/master/middleware.go#L38-L57

今回は、ISHOCON2でGinが利用されていたので、実装せざるおえませんでした。

一部対応と書いたのは、クエリのロガーの方で、contextに仕込んでいるはずのrequest id(uuid)が取れてないので、クエリのログは出ているのですが、紐付けることができていません 。たぶん、改善できるはずなので、いずれ改善いたします。

まとめ

やっぱり、実際のISUCON(ISHOCON2)を解きながら、graqtの開発をすると、

「うおぉぉぉぉ〜、この情報知りたいんだけどぉ!!!!」

とか

「ginに対応してないやん!!!対応せねば!!」

みたいな気持ちがわいてきて、モチベーションになりやすいです。

どんどん開発して、素晴らしいソフトウェアにするので、今後共よろしくお願いいたします。

【2日目】graqt開発日誌 ~GolangでスクロールできるCLIを作る~

今日から、朝5時に起きて、筋トレして7時に出勤して、始業の9時半までgraqtを書くことにした。夜は22時に寝る(つもり)。

1日3時間半程開発時間を取れるため、結構捗る。

進捗

午前中

帰宅後

  • スクロールの実装
  • リファクタ
  • query widgetの閉じる処理

コード

implement scroll by serinuntius · Pull Request #1 · serinuntius/go-cui-sandbox · GitHub

きれいなGIFを作る方法

craftzdog.hateblo.jp

まとめ

5時起き開発、やってくぞ!!!!

【1日目】graqt開発日誌 ~GolangでリッチなCLIを作る~

最近ブログ書いてないので、graqtの進捗を1mmでも進んだら書くことにする。

graqtについては、以下の記事を参考にされたし。 一言で言うと、バカでも使えるGolang用のWebアプリのボトルネックを調査するソフトウェアです。主にISUCONをターゲットにしています。

serinuntius.hatenablog.jp github.com

進捗

現在、CLI部分を作っていて、見た目だけ少し動くようになった。 まだ、ログ部分とつなぎこみはしていない。

別日だけど、sketchでワイヤー作ったりもした。

golangでリッチなCLIを作るには

jroimartin/gocuiというOSSがすごく便利です。

_exampleに参考になるコードが結構ある。 github.com

今日のコードを貼っておきます。 汚いのは自覚してるし、graqtに持っていくときは清書しますw

github.com

package main

import (
    "log"

    "github.com/jroimartin/gocui"
    "fmt"
    "text/tabwriter"
    "github.com/pkg/errors"
)

type RequestWidget struct {
    name    string
    x, y    int
    w, h    int
    counter int
    v       *gocui.View
    tw      *tabwriter.Writer
}

func NewRequestWidget(name string, x, y, w, h int) *RequestWidget {
    // add initialize
    return &RequestWidget{name: name, x: x, y: y, w: w, h: h, counter: 0}
}

func (w *RequestWidget) Layout(g *gocui.Gui) error {
    // _ => vgit
    v, err := g.SetView(w.name, w.x, w.y, w.x+w.w, w.y+w.h)
    if err != nil {
        if err != gocui.ErrUnknownView {
            return err
        }
        v.Title = "Request Index"
        v.Highlight = true
        v.SelBgColor = gocui.ColorGreen
        v.SelFgColor = gocui.ColorBlack

        v.SetCursor(0, 1)

        if err := w.KeyBindings(g); err != nil {
            return errors.Wrap(err, "Failed to Set KeyBindings")
        }

        if _, err := g.SetCurrentView("request"); err != nil {
            log.Panicln(err)
        }

        w.InitTabWriter(v)

        if _, err := w.PrintHeader(); err != nil {
            return err
        }

        // TODO テスト用
        for i := 0; i < 10; i++ {
            fmt.Fprintln(w.tw, "\t300\tGET\t/hoge\t10\t0.3\t5\t3000\t3\t8\t9\t8\t300mb\t50mb\t30mb\t30000mb")
        }

        w.tw.Flush()
        fmt.Fprintln(w.v)
    }

    return nil
}

func (w *RequestWidget) InitTabWriter(v *gocui.View) error {
    w.v = v
    w.tw = tabwriter.NewWriter(v, 0, 8, 2, ' ', 0)
    return nil
}

func (w *RequestWidget) PrintHeader() (int, error) {
    return fmt.Fprintln(w.tw, "\tCount\tMethod\tPath\tMax\tMin\tAvg\tSum\tP1\tP50\tP99\tStddev\tMaxBody\tMinBody\tAvgBody\tSumBody")
}

func (w *RequestWidget) enter(g *gocui.Gui, v *gocui.View) error {
    w.v.Clear()
    w.PrintHeader()
    w.counter++
    for i := 0; i < w.counter; i++ {
        fmt.Fprintln(w.tw, "\taaaaaaaaaa\tbbb")
    }

    w.tw.Flush()
    fmt.Fprintln(w.v)

    return nil
}

func (w *RequestWidget) cursorUp(g *gocui.Gui, v *gocui.View) error {
    if v != nil {
        ox, oy := v.Origin()
        cx, cy := v.Cursor()

        // cy == 0 is header
        if cy == 1 {
            return nil
        }
        if err := v.SetCursor(cx, cy-1); err != nil && oy > 0 {
            if err := v.SetOrigin(ox, oy-1); err != nil {
                return err
            }
        }
    }
    return nil
}

func (w *RequestWidget) cursorDown(g *gocui.Gui, v *gocui.View) error {
    if v != nil {
        cx, cy := v.Cursor()
        if err := v.SetCursor(cx, cy+1); err != nil {
            ox, oy := v.Origin()
            if err := v.SetOrigin(ox, oy+1); err != nil {
                return err
            }
        }
    }
    return nil
}

func (w *RequestWidget) Printf(format string, a ...interface{}) {
    fmt.Fprintf(w.tw, format, a)
    w.tw.Flush()
    fmt.Fprintln(w.v)
}

type HelpWidget struct {
    name string
    x, y int
    w, h int
    body string
}

func NewHelpWidget(name string, x, y, w, h int) *HelpWidget {
    // add initialize
    return &HelpWidget{name: name, x: x, y: y, w: w, h: h}
}

func (w *HelpWidget) Layout(g *gocui.Gui) error {
    // _ => v
    v, err := g.SetView(w.name, w.x, w.y, w.x+w.w, w.y+w.h)
    if err != nil {
        if err != gocui.ErrUnknownView {
            return err
        }
        v.Title = "Help"
        fmt.Fprintf(v, "%d:%d:%d:%d", w.x, w.y, w.x+w.w, w.y+w.h)
    }
    return nil
}

type QueryWidget struct {
    name string
    x, y int
    w,

    h int
}

func NewQueryWidget(name string, x, y, w, h int) *QueryWidget {
    // add initialize
    return &QueryWidget{name: name, x: x, y: y, w: w, h: h}
}

func (w *QueryWidget) Layout(g *gocui.Gui) error {
    // _ => v
    v, err := g.SetView(w.name, w.x, w.y, w.x+w.w, w.y+w.h)
    if err != nil {
        if err != gocui.ErrUnknownView {
            return err
        }
        v.Title = "Query Index"
    }
    return nil
}

type ParameterWidget struct {
    name string
    x, y int
    w, h int
}

func NewParameterWidget(name string, x, y, w, h int) *ParameterWidget {
    // add initialize
    return &ParameterWidget{name: name, x: x, y: y, w: w, h: h}
}

func (w *ParameterWidget) Layout(g *gocui.Gui) error {
    // _ => v
    v, err := g.SetView(w.name, w.x, w.y, w.x+w.w, w.y+w.h)
    if err != nil {
        if err != gocui.ErrUnknownView {
            return err
        }
        v.Title = "Query Parameter"
    }
    return nil
}

func quit(g *gocui.Gui, v *gocui.View) error {
    return gocui.ErrQuit
}

func (w *RequestWidget) KeyBindings(g *gocui.Gui) error {
    if err := g.SetKeybinding("request", gocui.KeyEnter, gocui.ModNone, w.enter); err != nil {
        return err
    }
    if err := g.SetKeybinding("request", gocui.KeyArrowDown, gocui.ModNone, w.cursorDown); err != nil {
        return err
    }
    if err := g.SetKeybinding("request", gocui.KeyArrowUp, gocui.ModNone, w.cursorUp); err != nil {
        return err
    }
    if err := g.SetKeybinding("request", gocui.KeyCtrlN, gocui.ModNone, w.cursorDown); err != nil {
        return err
    }
    if err := g.SetKeybinding("request", gocui.KeyCtrlP, gocui.ModNone, w.cursorUp); err != nil {
        return err
    }

    return nil
}

func main() {
    g, err := gocui.NewGui(gocui.OutputNormal)
    if err != nil {
        log.Panicln(err)
    }
    defer g.Close()

    g.Highlight = true
    g.SelFgColor = gocui.ColorBlue

    winX, winY := g.Size()
    fmt.Println(winX, winY)

    request := NewRequestWidget("request", 0, 0, winX-1, winY-3)
    help := NewHelpWidget("help", 0, winY-3, winX-1, 2)
    //query := NewQueryWidget("query", winX/2, 0, winX/2-1, winY-3)
    //parameter := NewParameterWidget("parameter", winX/2, winY/2, winX/2-1, winY/2-2)

    g.SetManager(help, request) //, query, parameter)

    if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil {
        log.Panicln(err)
    }

    if err := g.MainLoop(); err != nil && err != gocui.ErrQuit {
        log.Panicln(err)
    }
}

【自宅鯖】自作PCの電源を換装する【オウルテック Seasonic SSR-650FM】

f:id:serinuntius:20180721195222p:plain

6月の頭ぐらいから、ゲーミングPCとして利用していたタワー型の自作デスクトップを、サーバとして利用していた。

Mackerelで監視していたのだが、しばらくは安定して稼働していた。

しかし、ここ2週間ぐらいで不安定になってきた。ログとか見ても特に変なところがない。 連日の猛暑のせいで熱暴走しているっていう線も考えたけど、ほとんどCPU等はアイドルだし、温度を見てみてもそこまで異常値ではなかった。

このPCを組んでもう5年ぐらい経つので電源の寿命ではないかと思い、Amazonでポチって、サクッと換装したレポートだ。

古い電源を取り外す

こんな感じのタワーだ。今から掃除するので寝かせている。 サイドパネルをサクッと開ける。 f:id:serinuntius:20180721092455j:plain

昔の電源はSilverStone ST75F-Pだ。 750wのフルプラグイン式で80 Plus Silverだ。

別に悪くなかったように思う、ただ単に寿命なのだろう。

f:id:serinuntius:20180721092732j:plain

電源が刺さっているところ一覧

EATXPWR 24Pin

f:id:serinuntius:20180721095410p:plain

EATX 8Pin

見にくいけど、マザボの左上にある。 f:id:serinuntius:20180721094959p:plain

グラボ PCIe 6Pin * 2

f:id:serinuntius:20180721095007p:plain

ケースファン ペリフェラル4Pin * 2

ここでケースの右サイドのパネルを開ける。 配線汚い😇

f:id:serinuntius:20180721095454j:plain

こいつが、ケースのファンの電源といろいろ繋がっている。 f:id:serinuntius:20180721095944p:plain

絵面は似てるけどもう1つ。 f:id:serinuntius:20180721100208p:plain

HDD SATA15Pin

f:id:serinuntius:20180721100517p:plain

SSD SATA15Pin

Windows用のSSD。5年前のもので、128GB。 f:id:serinuntius:20180721100645p:plain

SSD2 SATA15Pin

サーバ用のSSD。1ヶ月ぐらい前に買った、256GB。

f:id:serinuntius:20180721100937p:plain

DVDドライブ SATA15Pin

f:id:serinuntius:20180721101100p:plain

全部抜いて外に出す

電源ケーブルの抜き忘れがないように、ケースの外にケーブルを全部出して、全部抜いたかを確認する。

f:id:serinuntius:20180721101429j:plain

電源を固定しているネジを緩めて外す

4箇所ネジで止まっているので外す。 f:id:serinuntius:20180721101722p:plain

ホコリが少し溜まっている。 f:id:serinuntius:20180721101810j:plain

プラグイン式の電源は使うものだけ挿せばいいので、電源の前に使わないケーブルをまとめておかないくていいのでオススメである。 f:id:serinuntius:20180721101858j:plain

掃除

ケースは、今はなきZalmanのZ12 Plus

フロントパネルをガバッと外す。結構硬い。 f:id:serinuntius:20180721102137j:plain

フロントパネルのファン。汚い。 f:id:serinuntius:20180721102153j:plain

フロントパネルに付いてる、フィルターを取ってみる。 f:id:serinuntius:20180721102218j:plain

うげぇ〜、汚い。 f:id:serinuntius:20180721102220j:plain

ケース底部のも掃除しておく。 f:id:serinuntius:20180721102247j:plain

ここでちらっと写った掃除機は、ダイソンのコードレス。 一人暮らしするときに、母に強くダイソンの掃除機をオススメされて購入に至った。 f:id:serinuntius:20180721102406j:plain

新しい電源

f:id:serinuntius:20180721102452j:plain

新しい電源はオウルテック Seasonic SSR-650FMを選択した。

この電源を選択した理由

  • 信頼できるメーカー
  • 驚きの7年間保証
  • 80 Plus Gold
  • サブプラグイン

電源容量は、50% のときの一番効率よく変換できるので、50% + αになるように組む。

電源容量の見積もりは、電源容量計算(電源電卓)電源の選び方|ドスパラ通販【公式】 が大変参考になる。

f:id:serinuntius:20180721102458j:plain 開封の儀。じゃ〜ん。

電源に小物入れ(?)がついていてびっくりした。 f:id:serinuntius:20180721102529j:plain

電源は、メイン以外はプラグイン式になっている。 電源コードがわかりにくいけど、平麺みたいになっていて薄くて配線しやすかった。ベビースタードデカイラーメンみたいと言えばわかりやすいだろうか。 f:id:serinuntius:20180721102606j:plain

仮組み

意図せず、ブラックとゴールドが揃っていてかっこいい。 f:id:serinuntius:20180721115813j:plain

EATXPWR 24Pin

f:id:serinuntius:20180721115847j:plain

プラグインペリフェラル4Pin

f:id:serinuntius:20180721115918j:plain

プラグインSATA 15Pin

f:id:serinuntius:20180721115930j:plain

【秘技】空中マウント

良い子は真似しない。 f:id:serinuntius:20180721115944j:plain

メモリテスト

ここで、サイドパネルとか開けたまま、メモリテストを回してみる。

電源のピンの刺し忘れや、メモリがちゃんと認識するかのテスト。*1

今回初めてメモリテストをしたが、memtest86+というのが定番らしい。

grub2から起動すると、何もしなくてもテストが回り始める。 このテストは自動的に何回もループするようで、3回ぐらい回すのがいいらしい。 f:id:serinuntius:20180721120106j:plain

一晩放置して、Pass: 4, Errors: 0 と出ていたので、メモリは大丈夫そう。 f:id:serinuntius:20180721192223p:plain

サイドパネルを閉めていく

組み立てているときに、今回このPCをサーバ用と割り切ることにして、HDDやWin用のSSDやGTX 970も取り付けるのをやめた。

安定性が増すことや、アイドル時の電気代を加味するとその方が良い選択な気がした。(が、今回650wを選択した意味がなくなってしまった。)

f:id:serinuntius:20180721192451j:plain

まとめ

換装前は半日で落ちていたサーバであったが、今回の電源の換装で安定してサーバが稼働するようになった。

完全に結果論だが、電源の寿命という推察はだいたい当たっていた。

*1:本当はメモリが原因か、電源が原因か見極めるため、電源交換前にもやるべきだった。

【Golang】GitHubのAPIでIssueのコメントを全件取ってくるスクリプト

f:id:serinuntius:20180720231919p:plain

昨日モテるシェル芸とか言って、ブログ書いてたんですけど、

serinuntius.hatenablog.jp

実はGitHubのIssueのコメントが大量すぎて、Load more になっちゃってて、全部の画像をダウンロードできてなかったみたいです。

f:id:serinuntius:20180720133258p:plain

それをデザイナーに指摘されて、全部取ってこようと思ってGitHubのAPI見てたら、Headerに次のページのlinkが入っているという仕様で、シェルスクリプト書くぐらいならGoで書きたいな〜と思ってGoで書きました。

GitHubのTokenを取得する

GitHubのパスワード認証だけだったらわざわざToken取得しなくてもいいんですけど、2段階認証している方はTokenの取得が必要となってきます。

https://github.com/settings/tokens にアクセスして、Tokenを取得します。

スクリプト

GoのGitHub APIのクライアントは

github.com

が便利です。

gist.github.com

使い方

環境変数TOKENにGitHubのTokenを入れます。

Passwordをベタ打ちしない方法は、Qiitaに書きました。 qiita.com

あと、constのrepoとかownerは自分のものに変えてください。

read -s 'TOKEN?tokenを入れてください>'
go run main.go | xargs wget

上記コマンドでIssueの画像が全件ダウンロードされるはずです。 されないときは、正規表現等が怪しいと思うので、その辺りを修正してください。

まとめ

これで、デザイナーから「おい!全部じゃねえぞ」ってツッコミが飛んでこなくなりましたね。*1

めでたしめでたし。

*1:実際にはそんなツッコミは飛んできていません。

【裏技】みんな知らないログイン必須ページの爆速スクレイピング【モテるシェル芸】

f:id:serinuntius:20180718233358p:plain

おはようございます。

裏技ってつけると急にワザップ感が出て、懐かしいですよね〜。 こないだ飲み会で同期とそんな話をしておりました。

本題

ログインが必要なWebサイトで画像を引っこ抜いて欲しいという依頼があり、スクリプトを書くかな〜と迷ったんですが、よく考えたらシェル芸だけで出来るな〜と思ったので共有したいと思います。

今回はデザイナーにGitHubのIssueに貼ってある画像200枚以上をzipで欲しいって言われたので、それを題材にします。

環境

やり方

1. Chromeでおもむろにデベロッパーツールを開く

Macなら Shift + Cmd + c等で開けます。

2. networkを選択する

f:id:serinuntius:20180718222407p:plain

そのページのリクエストを見つける たぶん、一番上のはず。

f:id:serinuntius:20180718222514p:plain

3. 右クリックして、Copy as cURLを選択

f:id:serinuntius:20180718222603p:plain

今回の肝はこれで、ブラウザで送ったリクエストと全く同じリクエストをcurlで再現できるようになっております。 つまり、cookieの情報等も渡してくれるので、認証が通った状態でアクセス出来るわけですね。

GitHubのプライベートリポジトリは、認証してない状態でアクセスすると404を返してくるのですがそれを回避できます。

4. お好きなターミナルにコピペ

わかりにくいですが、めっちゃ長いcurlコマンドが貼り付けられてます。 f:id:serinuntius:20180718224134j:plain

5. いい感じにシェル芸してく

今回は、Issueに投稿された画像を抜いてきたいので、こんな感じになりました。

curl github.com/.../../ -H '....' \ 
|grep '<img' \
|egrep -o "https://user.*?\.(png|jpg)"\
|uniq\
|xargs wget

一応説明しておくと、

  1. grep '<img'で当たりをつける
  2. grepの拡張版であるegrepで正規表現を使っていい感じに抜き取る (oオプションはマッチした場所だけ取ってくる便利なオプションです。egrepじゃなくて、grepのEオプションでも可)
  3. 何個か重複してたりしたので、uniqをかませる
  4. xargsでwgetにurlを渡す (ここで意外なのが、画像には認証が不要というとところです。 uuid振ってあるから、大丈夫という認識なのでしょうか?)

まとめ

これを頼まれて5分ぐらいでサクッとやって「出来たよ」って言って渡せば、モテること間違いなしですね!!!!

f:id:serinuntius:20180718225759p:plain

(念の為言っておきますがこれでモテていると勘違いしているわけではありません。24年生きてるので、それぐらいわかっているつもりです。)

dockerのコンテナを楽に選択するpecoを使ったalias

ブログ書くの怠ってたから、カジュアルにアウトプットしていくぞ!!!

本題

環境

  • peco
  • zsh(zshのグローバルエイリアスという機能を使っているみたいなのでzshしかできないと思います。)
  • docker

手順

~/.zshrc に以下を書くだけ

alias -g C='`docker ps -a|peco| cut -d" " -f 1`'

使い方

  • docker logs C
  • dcoker exec -it C bash

これぐらいしか思いつかないけど、まあ便利