【3,4日目】graqt開発日誌 ~PercentileとStddev(標準偏差)~
「べ、別に土日早起き出来なかったからブログ書けなかったわけじゃないんだからね。。。」
冗談はさておき、土日はISHOCON2やgraqtの開発に力を入れてしまい、記事を書けませんでした。
いろいろ機能が追加されたので、紹介したいと思います。
新機能
Percentileが見れるようになりました
graqtはbetter than alpを標語として、作っているのですが、alpにあるPercentileを移植しました。
P1, P50, P99とかのことです。
なんで、これが必要なのか調べていると、レスポンスタイムの平均(Avg)だけ見てしまうと、スロークライアントの結果に引っ張られる可能性があるからです。
Nginxの$request_timeは、nginxがリクエストを受けてからレスポンスをクライアントに返しきるまでの時間なので、変に遅いクライアントが居ないとも限らない・・・。 平均値だと、極端な異常値が少しでもあったばあい、これに引きづられて数字が大きくなったりして、あまり信用できなくなったりします。 測定誤差をできるだけ減らしたいので、平均値ではなくてパーセンタイルを取ろうという考え方です。
ベンチマーカー相手だとあんまり考えなくてもいいかもですが(わざわざスロークライアントになるようなベンチマーカーって過去のISUCONでないよね?)、本番のアプリケーションでは重要ですね。
Stddev(標準偏差)が見れるようになりました
標準偏差も、かなりわかりにくい指標なのですが、こちらの記事にわかりやすく書いてありました。詳しく知りたい方はこちらの記事をお読みください。
この機能もalpについている機能で、そのままパクリましたリスペクトして実装しました。
Pathを集約して見れるようになりました
これもalpリスペクトの機能で、alpではAggregate機能として実装されています。
例えばですけど、
/diary/entry/1 /diary/entry/2 /diary/entry/3
こういう感じのリクエストがあると、集約しないとそのまま3行で表示されてしまいますが、設定ファイルに正規表現を書いておくと集約して1行でまとめて表示できます。
graqt.yamlに
aggregates: - /diary/entry/\d+
このように書いておくと、集約され表示されるようになります。
実際の例を見たほうがわかりやすいと思うので、貼っておきます。
これが、
こうなります!!!
けど、これ使うと「一部のページだけ異様に遅い!」みたいなのがあった場合わかりにくいので気をつけてください。
レスポンスボディのサイズが表示されるようになりました
今まで項目としてはちゃんと用意されていたのですが、ロガーの方で取得していなかったので、ちゃんとレスポンスボディのサイズを取得してロガーに載せるようにしました。
やったぜ!
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時間半程開発時間を取れるため、結構捗る。
進捗
午前中
- ヘッダーを残したままスクロールする(結構考える必要があった)
今日のgraqtの進捗です。
— serinuntius@5時起き (@_serinuntius) August 17, 2018
スクロールできるようになりました。ちゃんとheaderを残しつつ、スクロールを実装したのがポイントです。https://t.co/6JZQyklKlP#golang #isucon #朝活 pic.twitter.com/lfubBmWUMt
帰宅後
- スクロールの実装
- リファクタ
- query widgetの閉じる処理
graqt夜の部の進捗。
— serinuntius@5時起き (@_serinuntius) August 17, 2018
今日はGIFにしてみた。
query windowを表示したり、閉じたりできるようになった!
リファクタして各windowごとのファイルにわけた。https://t.co/6JZQyklKlP#golang #isucon #graqt pic.twitter.com/xkkubqmeBi
コード
implement scroll by serinuntius · Pull Request #1 · serinuntius/go-cui-sandbox · GitHub
きれいなGIFを作る方法
まとめ
5時起き開発、やってくぞ!!!!
【1日目】graqt開発日誌 ~GolangでリッチなCLIを作る~
最近ブログ書いてないので、graqtの進捗を1mmでも進んだら書くことにする。
graqtについては、以下の記事を参考にされたし。 一言で言うと、バカでも使えるGolang用のWebアプリのボトルネックを調査するソフトウェアです。主にISUCONをターゲットにしています。
serinuntius.hatenablog.jp github.com
進捗
現在、CLI部分を作っていて、見た目だけ少し動くようになった。 まだ、ログ部分とつなぎこみはしていない。
別日だけど、sketchでワイヤー作ったりもした。
めっちゃ雑に今作ってるISUCON用の解析ツールのCLIのデザインをsketchで書いてみた!
— serinuntius (@_serinuntius) August 7, 2018
CLIツールでもGUIの場合ワイヤー引いとかないと、いろいろ要件漏れちゃいそうだからな〜。 pic.twitter.com/d79CjFq5DO
graqtの進捗 #golang #isucon pic.twitter.com/F35efDpx9w
— serinuntius (@_serinuntius) August 16, 2018
golangでリッチなCLIを作るには
jroimartin/gocuiというOSSがすごく便利です。
_exampleに参考になるコードが結構ある。 github.com
今日のコードを貼っておきます。 汚いのは自覚してるし、graqtに持っていくときは清書しますw
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】
6月の頭ぐらいから、ゲーミングPCとして利用していたタワー型の自作デスクトップを、サーバとして利用していた。
Mackerelで監視していたのだが、しばらくは安定して稼働していた。
しかし、ここ2週間ぐらいで不安定になってきた。ログとか見ても特に変なところがない。 連日の猛暑のせいで熱暴走しているっていう線も考えたけど、ほとんどCPU等はアイドルだし、温度を見てみてもそこまで異常値ではなかった。
このPCを組んでもう5年ぐらい経つので電源の寿命ではないかと思い、Amazonでポチって、サクッと換装したレポートだ。
古い電源を取り外す
こんな感じのタワーだ。今から掃除するので寝かせている。 サイドパネルをサクッと開ける。
昔の電源はSilverStone ST75F-Pだ。 750wのフルプラグイン式で80 Plus Silverだ。
別に悪くなかったように思う、ただ単に寿命なのだろう。
電源が刺さっているところ一覧
EATXPWR 24Pin
EATX 8Pin
見にくいけど、マザボの左上にある。
グラボ PCIe 6Pin * 2
ケースファン ペリフェラル4Pin * 2
ここでケースの右サイドのパネルを開ける。 配線汚い😇
こいつが、ケースのファンの電源といろいろ繋がっている。
絵面は似てるけどもう1つ。
HDD SATA15Pin
SSD SATA15Pin
SSD2 SATA15Pin
サーバ用のSSD。1ヶ月ぐらい前に買った、256GB。
DVDドライブ SATA15Pin
全部抜いて外に出す
電源ケーブルの抜き忘れがないように、ケースの外にケーブルを全部出して、全部抜いたかを確認する。
電源を固定しているネジを緩めて外す
4箇所ネジで止まっているので外す。
ホコリが少し溜まっている。
プラグイン式の電源は使うものだけ挿せばいいので、電源の前に使わないケーブルをまとめておかないくていいのでオススメである。
掃除
ケースは、今はなきZalmanのZ12 Plus。
フロントパネルをガバッと外す。結構硬い。
フロントパネルのファン。汚い。
フロントパネルに付いてる、フィルターを取ってみる。
うげぇ〜、汚い。
ケース底部のも掃除しておく。
ここでちらっと写った掃除機は、ダイソンのコードレス。 一人暮らしするときに、母に強くダイソンの掃除機をオススメされて購入に至った。
新しい電源
新しい電源はオウルテック Seasonic SSR-650FMを選択した。
この電源を選択した理由
- 信頼できるメーカー
- 驚きの7年間保証
- 80 Plus Gold
- サブプラグイン式
電源容量は、50% のときの一番効率よく変換できるので、50% + αになるように組む。
電源容量の見積もりは、電源容量計算(電源電卓)電源の選び方|ドスパラ通販【公式】 が大変参考になる。
開封の儀。じゃ〜ん。
電源に小物入れ(?)がついていてびっくりした。
電源は、メイン以外はプラグイン式になっている。 電源コードがわかりにくいけど、平麺みたいになっていて薄くて配線しやすかった。ベビースタードデカイラーメンみたいと言えばわかりやすいだろうか。
仮組み
意図せず、ブラックとゴールドが揃っていてかっこいい。
EATXPWR 24Pin
【プラグイン】ペリフェラル4Pin
【プラグイン】SATA 15Pin
【秘技】空中マウント
良い子は真似しない。
メモリテスト
ここで、サイドパネルとか開けたまま、メモリテストを回してみる。
電源のピンの刺し忘れや、メモリがちゃんと認識するかのテスト。*1
今回初めてメモリテストをしたが、memtest86+というのが定番らしい。
grub2から起動すると、何もしなくてもテストが回り始める。 このテストは自動的に何回もループするようで、3回ぐらい回すのがいいらしい。
一晩放置して、Pass: 4, Errors: 0 と出ていたので、メモリは大丈夫そう。
サイドパネルを閉めていく
組み立てているときに、今回このPCをサーバ用と割り切ることにして、HDDやWin用のSSDやGTX 970も取り付けるのをやめた。
安定性が増すことや、アイドル時の電気代を加味するとその方が良い選択な気がした。(が、今回650wを選択した意味がなくなってしまった。)
まとめ
換装前は半日で落ちていたサーバであったが、今回の電源の換装で安定してサーバが稼働するようになった。
完全に結果論だが、電源の寿命という推察はだいたい当たっていた。
オウルテック 7年間新品交換保証 80PLUS GOLD取得 ATX 電源 ユニット セミモジュラー Skylake対応 Seasonic FOCUSシリーズ 650W SSR-650FM
- 出版社/メーカー: オウルテック
- 発売日: 2018/03/23
- メディア: Personal Computers
- この商品を含むブログを見る
*1:本当はメモリが原因か、電源が原因か見極めるため、電源交換前にもやるべきだった。
【Golang】GitHubのAPIでIssueのコメントを全件取ってくるスクリプト
昨日モテるシェル芸とか言って、ブログ書いてたんですけど、
実はGitHubのIssueのコメントが大量すぎて、Load more
になっちゃってて、全部の画像をダウンロードできてなかったみたいです。
それをデザイナーに指摘されて、全部取ってこようと思ってGitHubのAPI見てたら、Headerに次のページのlinkが入っているという仕様で、シェルスクリプト書くぐらいならGoで書きたいな〜と思ってGoで書きました。
GitHubのTokenを取得する
GitHubのパスワード認証だけだったらわざわざToken取得しなくてもいいんですけど、2段階認証している方はTokenの取得が必要となってきます。
https://github.com/settings/tokens にアクセスして、Tokenを取得します。
スクリプト
が便利です。
使い方
Passwordをベタ打ちしない方法は、Qiitaに書きました。 qiita.com
あと、constのrepoとかownerは自分のものに変えてください。
read -s 'TOKEN?tokenを入れてください>' go run main.go | xargs wget
上記コマンドでIssueの画像が全件ダウンロードされるはずです。 されないときは、正規表現等が怪しいと思うので、その辺りを修正してください。
まとめ
これで、デザイナーから「おい!全部じゃねえぞ」ってツッコミが飛んでこなくなりましたね。*1
めでたしめでたし。
*1:実際にはそんなツッコミは飛んできていません。
【裏技】みんな知らないログイン必須ページの爆速スクレイピング【モテるシェル芸】
おはようございます。
裏技ってつけると急にワザップ感が出て、懐かしいですよね〜。 こないだ飲み会で同期とそんな話をしておりました。
本題
ログインが必要なWebサイトで画像を引っこ抜いて欲しいという依頼があり、スクリプトを書くかな〜と迷ったんですが、よく考えたらシェル芸だけで出来るな〜と思ったので共有したいと思います。
今回はデザイナーにGitHubのIssueに貼ってある画像200枚以上をzipで欲しいって言われたので、それを題材にします。
環境
やり方
1. Chromeでおもむろにデベロッパーツールを開く
Macなら Shift + Cmd + c
等で開けます。
2. networkを選択する
そのページのリクエストを見つける たぶん、一番上のはず。
3. 右クリックして、Copy as cURLを選択
今回の肝はこれで、ブラウザで送ったリクエストと全く同じリクエストをcurlで再現できるようになっております。 つまり、cookieの情報等も渡してくれるので、認証が通った状態でアクセス出来るわけですね。
GitHubのプライベートリポジトリは、認証してない状態でアクセスすると404を返してくるのですがそれを回避できます。
4. お好きなターミナルにコピペ
わかりにくいですが、めっちゃ長いcurlコマンドが貼り付けられてます。
5. いい感じにシェル芸してく
今回は、Issueに投稿された画像を抜いてきたいので、こんな感じになりました。
curl github.com/.../../ -H '....' \ |grep '<img' \ |egrep -o "https://user.*?\.(png|jpg)"\ |uniq\ |xargs wget
一応説明しておくと、
grep '<img'
で当たりをつける- grepの拡張版であるegrepで正規表現を使っていい感じに抜き取る (oオプションはマッチした場所だけ取ってくる便利なオプションです。egrepじゃなくて、grepのEオプションでも可)
- 何個か重複してたりしたので、uniqをかませる
- xargsでwgetにurlを渡す (ここで意外なのが、画像には認証が不要というとところです。 uuid振ってあるから、大丈夫という認識なのでしょうか?)
まとめ
これを頼まれて5分ぐらいでサクッとやって「出来たよ」って言って渡せば、モテること間違いなしですね!!!!
(念の為言っておきますがこれでモテていると勘違いしているわけではありません。24年生きてるので、それぐらいわかっているつもりです。)