Go言語のHTTPリクエストのレスポンスボディーとEOF

Reader interface の Read 関数は、どのタイミングで io.EOF を返すのでしょうか。 まずは strings.Reader で見てみましょう。

package main

import (
    "fmt"
    "strings"
)

func main() {
    r := strings.NewReader("example\n")
    for {
        var b [1]byte
        n, err := r.Read(b[:])
        fmt.Printf("%d %q %v\n", n, b, err)
        if err != nil {
            break
        }
    }
}

結果

1 "e" <nil>
1 "x" <nil>
1 "a" <nil>
1 "m" <nil>
1 "p" <nil>
1 "l" <nil>
1 "e" <nil>
1 "\n" <nil>
0 "\x00" EOF

Readの結果は、読み込んだbyte数です。なにもなくなってから io.EOF を返していることがわかります。

ファイルだとどうでしょうか。

package main

import (
    "fmt"
    "os"
)

func main() {
    f, err := os.Open("main.go")
    if err != nil {
        fmt.Fprintf(os.Stderr, "%v\n", err)
        os.Exit(1)
    }
    defer f.Close()
    for {
        var b [1]byte
        n, err := f.Read(b[:])
        fmt.Printf("%d %q %v\n", n, b, err)
        if err != nil {
            break
        }
    }
}
1 "b" <nil>
1 "r" <nil>
1 "e" <nil>
1 "a" <nil>
1 "k" <nil>
1 "\n" <nil>
1 "\t" <nil>
1 "\t" <nil>
1 "}" <nil>
1 "\n" <nil>
1 "\t" <nil>
1 "}" <nil>
1 "\n" <nil>
1 "}" <nil>
1 "\n" <nil>
0 "\x00" EOF

同じですね。

ではHTTPリクエストだとどうでしょうか。

package main

import (
    "fmt"
    "net/http"
    "os"
)

func main() {
    resp, err := http.Get("http://example.com")
    if err != nil {
        fmt.Fprintf(os.Stderr, "%v\n", err)
        os.Exit(1)
    }
    defer resp.Body.Close()
    for {
        var b [1]byte
        n, err := resp.Body.Read(b[:])
        fmt.Printf("%d %q %v\n", n, b, err)
        if err != nil {
            break
        }
    }
}
1 "<" <nil>
1 "/" <nil>
1 "h" <nil>
1 "t" <nil>
1 "m" <nil>
1 "l" <nil>
1 ">" <nil>
1 "\n" EOF

なぜなのか… なぜなのか!!!

検索するとruiさんのエントリーが出てきました。 qiita.com (三年前の記事だった… もしかして… これは常識なのか!?) そして、mattnさんがgolang-nutsでスレッドを立てられていたのでざっと見ました。 Issue 49570044: code review 49570044 も勉強になります。 これはContent-Lengthがセットされたレスポンスボディーを最後まで読んだ後に、すぐにコネクションを使いまわせるようにするための意図した挙動であるということがわかりました。

Readerはわりと使い慣れたinterfaceなのにハマってしまいました。 Go言語を書いていると手癖ですぐにerrを返してしまいがちですが、Readは読み込んだバイトがありながら io.EOF になることがあることに気をつけなくてはいけません。 いや、これはruiさんの記事を同じことを言ってますね… ほんとは常識なのかもしれない…

ライブラリーが Reader 使ったインターフェースを公開しておきながら、io.EOF の扱いが適切でないと簡単にバグを踏んでしまいます。 strings.Reader でテストして満足していると、resp.Body に繋いだ瞬間なぜか挙動が変わるということがあるかもしれません。

Read の返り値のバイト数を捨ててませんか? err != nil でもデータを読み込んでいるかもしれませんよ?

みなさん、気をつけましょう。おわり。

zshの標準エラー出力の色を赤くする

[追記]以下の方法は良くないようです。必ず、このエントリー最後の「stderredを使う」を参照してください[/追記]

最近stderrを赤くするように設定したら、コマンドの出力がかなり見やすくなりました。 f:id:itchyny:20171116233845p:plain

設定はこんな感じに書いてます。

zmodload zsh/terminfo zsh/system
color_stderr() {
  while sysread std_err_color; do
    syswrite -o 2 "${fg_bold[red]}${std_err_color}${terminfo[sgr0]}"
  done
}
exec 2> >(color_stderr)

fg_bold[red] のところを fg[red] とかbg_bold[red] とかするとスタイルを変更できます。 古いzshでは動かないらしいので、古い環境も気にしたい場合は is-at-least 4.3.4 でチェックするとよさそうです。

この設定の元ネタはcoloring stderr - was Re: piping stderrです。 大体はうまくいくし、普段使う分には大きな問題は起きていないのですが、わりと乱暴なことをやっているという自覚はあります。 元ネタのスレッドでも触れられていますが、 echo L1; echo L2 >&2; echo L3; echo L4 >&2 とやると L2L3 が逆に出力されたりします。 あと bash -i したあとに top を起動できないといったりします。

いくつか問題が起きるケースはありそうだけど、二週間ほど試してみて普段端末を触る範囲では困っていないのと、stderrに色が付くのがありがたいので使っています。もっといい方法があれば教えてください。

ついでに使っているPROMPTの設定はこんな感じです。終了コードを元に色を変更しています。

PROMPT="%(?.%{$fg[green]%}.%{$fg[blue]%})%B%~%b%{${reset_color}%} "
PROMPT2="%{$bg[blue]%}%_>%{$reset_color%}%b "
SPROMPT="%{$bg[red]%}%B%r is correct? [n,y,a,e]:%{${reset_color}%}%b "

stderredを使う

上の設定を使うと、 echo foobar | vim - が動かなくなります。これは結構困ります。端末で出力先を無理やり変える方法はやはり色々トラブルの元となるようです。 代わりにstderredを使うのをおすすめします。ビルドする必要はありますが、上の方法よりも直接的な方法なのでトラブルは起きないと思います。 github.com

if [ -f /usr/local/lib/libstderred.dylib ]; then
  export DYLD_INSERT_LIBRARIES="/usr/local/lib/libstderred.dylib${DYLD_INSERT_LIBRARIES:+:$DYLD_INSERT_LIBRARIES}"
  export STDERRED_ESC_CODE=$'\x1b[1;31m'
fi

Serverlessconf Tokyo 2017で『サーバレスアーキテクチャによる時系列データベースの構築と監視』という発表してきました

先日開催されたServerlessconf Tokyo 2017にスピーカーとして参加しました。

2017.serverlessconf.tokyo

Mackerelの今の時系列データベースは、マネージドサービスを組み合わせて作っています。 検証・実装・投入フェーズを終えて、運用・新機能開発フェーズに入っています。そんな中で、監視サービスを提供する私たちが、サーバーレスアーキテクチャで作ったミドルウェアをどのように監視しているかについてお話しました。 何かしら役に立つことや発想の元となるようなことをお伝えできていたらいいなと思います。

私も他の発表から様々なことを学びました。特に面白かった発表を挙げておきます。

真のサーバレスアーキテクトとサーバレス時代のゲーム開発・運用

ゲーム開発を支えるBaaSを開発されるなかで得られた様々な知見についてお話されていました。 プッシュ通知のために大量のコネクションを張りたい場面でAWS IoTを用いるといった話はとてもおもしろいと思いました。 アクセス権限の話でLambdaのメモリーにキャッシュを持つという話をされていて、そんな手があるのかと驚きましたが、物理的な限界がありスケールしないので、使いどころ (というかヒット率に基づくキャッシュの削除) が難しいなと思いました。

サーバーレスについて語るときに僕の語ること

軽快な語り口と盛り上げ方はランチセッションにふさわしく、うまいなぁと思いました。 サーバーレスというのは文字通りだと枠としては大きすぎるんですよね。 スケーラビリティーやイベント駆動により処理を繋げるアーキテクチャも包含する言葉がほしいなぁという気持ちになりました。 よっしゃサーバーレスやとかいってサービスを選んだ時に、その用途が本当にそのサービスの適した使い方なのかどうかはきちんと考えるべきですよね。

Open source application and Ecosystem on Serverless Framework

なるほど確かに、こういう方向に進んでいくよねという気持ちになりました。 サーバーレスミドルウェアという言葉が面白かったです。聞いた瞬間、様々なものが繋がった気がしました。 マネージドサービスを組み合わせて作るという流れはおそらくこれからも続いていくと思いますし、それを組み合わせることで1つの「ミドルウェア」として機能するものを作るのはよくあることだと思います。 そういうサーバーレスミドルウェアは一発で立ち上がるべきだし、利用者が実装内部 (どういうマネージドサービスを使っているか) を気にする必要なく使えるようになっているのが理想の形。 こういうミドルウェアって再利用可能な形でシェアできるといいし、それらを組み合わせてサービスを構築できるとかっこよさそうですよね。 そう、docker-composeみたいなやつが欲しいですね (あるのかしら、まだよく知らないです)。

最後に

私は対外発表は初めてこともあり、とてもよい経験になりました。 慣れていなくてストーリーを組み立てるのに苦労したり、資料を作るのに時間がかかったりしましたが、なんとか終わって今は安堵しています。 E2E監視というところを抽象化して、系全体として動いているかを見るというところに落とし所を見つけられたのは、発表の前日でした。 私たちが作ったのは時系列データベースの「サーバーレスミドルウェア」なんですよね、こういうまとめて見た時の概念を思いついていなかったのは正直くやしい。 k1LoWさんには発表や質疑応答の中でもdiamondに触れていただいて、親近感がわきました。

マネージドサービスを組み合わせてサービスを作ることは、セキュリティの問題やスケーラビリティー、運用コストの削減など様々な問題を解決してくれると思います。 ただし、各マネージドサービスにはそれぞれ使いどころや苦手なユースケースがありますので、他の成功事例だけを見て盲目的にコンポーネントを選ぶのではなく、適した使い方をしているかどうかをきちんと吟味しましょうということですね。 こういうアーキテクチャーに乗ったサービス開発はどんどん加速していくと思いますので、私も技術的に置いていかれないように精進していきたいと思います。

主催の吉田さん、運営スタッフのみなさま、本当にありがとうございました。

負荷を均すための『時間軸シャーディング』という考え方

ウェブアプリケーションを作っていると、負荷を分散させるために「タイミングをばらけさせる」場面に時々遭遇します。 データの更新、キャッシュのフラッシュ、バッチ処理など様々な問題で、同じ構造が見られます。

例えば、スマホアプリからバックグラウンドで1時間ごとに何らかの情報をサーバーに送りたいとします。 愚直に毎時0分に更新処理を行うようにすると、すべてのユーザーから同じタイミングでリクエストが来てしまいます。 ですから、リクエストのタイミングをユーザーごとにばらして負荷を均す必要があります。

他のケースを考えます。 5分ごとにジョブを投入して何らかの更新を行うタスクがあるとします。 本来ならデータベースに更新を行いたいのですが、データベースのハードウェアの限界が近いので、更新データをまずキャッシュに乗せるようにしました。 何らかのタイミングでキャッシュからデータベースにフラッシュする必要があります。 データベースに書き込むタイミングをジョブによってばらして負荷を均したいという要求が出るのは自然なことです。

負荷を均すときに考える基本的なことは、何が時間軸上でばらけるかということです。 例えばインストール時刻がばらけるならば、インストール時刻から1時間ごとに更新するという処理でユーザーごとにばらけるでしょう。 そんな都合のいいタイミングがない場合や、ストレージに情報を持てない場合には、id番号のようにすでにばらけているものから処理する時刻を決めるのが有効です。 例えば5分毎のジョブ投入のケースの場合、id番号のmod 12を取った値が分を5で割った値と同じになったタイミングで処理を行うようにすると、重い処理を1時間に一回にしてかつ負荷を分割することができます。

func shouldUpdate(job: Job) bool {
    return (job.ID % 12) == (time.Now().Minute() / 5)
}

idが文字列の場合は文字コードの和をとる、SHA-1を取って文字コード和をとる、CRCをとるなど、とにかくmodでばらけるような数字を作れたら成功です。

本来なら毎回行う更新処理を数回に一回にしてばらけさせる、あるいはリクエストの集中を避けるためにタイミングをばらけさせる。 このように、負荷を分割して均す目的で時間軸上でばらけさせる処理は、時間軸方向のシャーディングと言うことができます。

一般にシャーディングというと、データベースやストリームをスケールアウトさせて負荷を分散させる、空間軸方向のシャーディングを指します。 一つのデータベースに集中させていたものを、ハードウェアの限界などのためにデータベースを分割して負荷を軽減させるのがよくあるケースです。 何らかのハッシュ関数のmodからノードを決定してデータを書き込む。 ハッシュ値のmodが均等に分散すれば、各ノードの負荷は1/(ノード数)に軽減されます。

更新リクエストを時間軸上で分散させるときも、idから計算したハッシュ値のmod (単純なケースではidのmod) を取ります。 1時間を5分毎に分割するときは、ノード数が12ですからmod 12を取ってノードを決定します。 15ノードに分けても30ノードに分けても構いません。 一日一回の更新タスクを24分割するならmod 24を取って時刻と比較するとか、96分割してその日の経過した分を15で割った数字と比較するなど、分割方法はいくらでもあります。 ノードの分割方法は、タスクの投入の仕方、どれくらいの失敗が許容されるか、短い期間で同じ処理を行なっても大丈夫かなど、問題の性質によって変わってきます。 いずれにしても、idや名前などの固定値からハッシュ値を作ることが大切です。

もっと古典的なユースケースとして、重いバッチ処理をmodで分割するのも、時間軸のシャーディングと言えます。 偶数番号と奇数番号でバッチを分割するというのは最も単純なシャーディングです。

これまで「タイミングをばらけさせる」と言っていたものを「時間軸のシャーディング」という言葉で表現することで、空間軸のシャーディングの用語を類推して用いることができます。 例えば「シャードが偏る」という言葉がありますが、時間軸シャーディングの場合で偏る場合はだいたいハッシュ関数か元の値の選び方が悪いでしょう。 「シャードを分割する」場合は、特定の一つのシャードを分割するよりも、10分毎のノードを5分毎に分割するという風に時間軸上のノード数を増やすのが有効だと思います。 空間軸シャーディングとは違って、時間軸シャーディングの場合はデータ移行を考えなくて良いので、ハッシュ関数やノードの分割方法を変えて「リシャーディング」を行うことでうまくいく場合が多いと思います。

上記で書いた内容は、特に新しいことはやっていませんし、タイミングをいい具合にばらけさせるコードを書いたことがある人はたくさんいると思います。 そういう処理を時間軸のシャーディングと捉えることで、言葉を類推して用いることができるようになり、空間軸シャーディングと対比して利点と欠点を比較し、より良い方を選ぶことができるようになると思います。

「更新が多くてハードウェアの限界に来てますが、仕様としてもう少し更新回数減らしてよさそうですね。ただ、タイミングがばらばらになるとよさそう」のようなあやふやな言葉で伝えていたものを、「時間軸シャーディングするとよさそうですね。1時間を12ノードに分けましょう。1時間に一回の更新で仕様上は問題はありません。空間軸シャーディングはこれでだめになった時にまた考えましょう」のようにはっきりと伝えられるとかっこいいですね。

追記: ジョブキュー使いましょうというのは真っ当なご意見ですし、負荷のパターンと工数によってジョブキューを作るべき場面もあるでしょう。もともと時間軸シャーディングを考えたのは負荷が一定でスパイクのない系で工数をかけずにタイミングをバラすというものだったので、負荷を均す一つの解法にすぎないことは把握しております。

Mackerelのプラグインを書く楽しみ ― Rustでプラグインを書くためのライブラリを作りました!

Mackerelは「エンジニアをワクワクさせる」ツールであることをサービスの大事な考え方の一つとして捉えています。 一体どういう場面でエンジニアはワクワクするのでしょうか。 簡単にインストールできるmackerel-agentや、直感的で触りやすい画面、チャットツールとの連携は大事な機能です。 しかし、監視ツールとしてもっと重要なのは、ミドルウェアのメトリックをどのように可視化し、何を監視するかということです。

Mackerelは公式のプラグインリポジトリに各種プラグインを揃えています (contributorの皆様ありがとうございます)。 これらはすべてGo言語で書かれています。 しかし、MackerelのプラグインはGo言語で書かなければいけない、ということはありません。 例えばカスタムメトリックのヘルプページのサンプルプラグインRubyで書かれていますし、メタデータのヘルプのサンプルスクリプトPerlで書かれています。

Mackerelのプラグインはどんな言語で書いていただいても構いません。 Rubyで書いてもシェルスクリプトで書いてもHaskellで書いても構いません。 世の中にはいろいろなミドルウェアがありますから、公式プラグインのなかに要件を満たせない場合もあるかもしれません。 自分の好きな言語でミドルウェアのメトリックを可視化する、そして監視をかけて、異常をいち早く察知する、つまり自らコードを書いて監視を作ること、これはすごくワクワクすることなのです。

こういうMackerelのhackableな部分はすごく大事だと思っていますし、これからも大事にしていきたいと思います。 初期のデザインドキュメントを見てみても、APIで様々な操作を行えることを最初から重要視していたことが伺える記述が見つかります。 そのまま実行したらグラフを確認できるcurlコマンドサンプルが置かれているのも、このあたりの考えが反映さています。 f:id:itchyny:20171005000230p:plain

ところで、私はRustという言語がとても気に入っています。 家ではだいたいRustのことを考えていて、Rustで書かれたプロダクトのコードを読んだり、いろいろ作ったりして楽しんでいます。 好きな言語があるとMackerelのAPIを叩いてみたくなるわけで、mackerel-client-rsmackerel-client-hsを作ってきました。 ただ、APIクライアント作りってやることが淡々としていて、正直飽きてくるんですよね。 まだすべてのAPIに対応できていないのでがんばらないといけないわけですが、いかんせんモチベーションが上がりにくいわけです。

一方で、Mackerelのメトリックプラグインを作るのは、APIクライアントを作るよりも格段におもしろいわけです。 そもそもメトリックのとり方はいろいろありますし、サーバーの変化を時系列データとして可視化できると、これがすごく楽しい。 ひょっとして、MackerelプラグインをRustで書いたらもっと楽しいんじゃない?

本題です。 RustでMackerelのプラグインを書くためのヘルパーライブラリ、mackerel-plugin-rsを作りました。 公式のgo-mackerel-pluginのRust版です。

github.com

まだ作ったばかりで、差分計算など一部の機能をまだ足りていませんが、今はモチベーションがすごく高いので、すぐに実装されると思います。 上記ライブラリを使って、手始めにloadavgプラグインuptimeプラグインを書いてみました。 f:id:itchyny:20171005005452p:plain topやuptimeコマンドで3つの数字が並んだ状態のloadavgはよく目にしますが、実際にグラフにしてみると激しく変化するloadavg1、それに上下するloadavg5、よりなだらかなloadavg15というメトリックの特徴がよく出ていて、すごく美しいわけです。 f:id:itchyny:20171005005912p:plain プラグインのコードはこういう感じです。

extern crate libc;
#[macro_use]
extern crate mackerel_plugin;

use std::collections::HashMap;
use mackerel_plugin::*;

pub struct LoadavgPlugin {}

#[inline]
fn get_loadavgs() -> Result<[f64; 3], String> {
    let mut loadavgs: [f64; 3] = [0.0, 0.0, 0.0];
    let ret = unsafe { libc::getloadavg(loadavgs.as_mut_ptr(), 3) };
    if ret == 3 {
        Ok(loadavgs)
    } else {
        Err("failed to get load averages".to_string())
    }
}

impl Plugin for LoadavgPlugin {
    fn fetch_metrics(&self) -> Result<HashMap<String, f64>, String> {
        let mut metrics = HashMap::new();
        let loadavgs = get_loadavgs()?;
        metrics.insert("loadavg.loadavg1".to_string(), loadavgs[0]);
        metrics.insert("loadavg.loadavg5".to_string(), loadavgs[1]);
        metrics.insert("loadavg.loadavg15".to_string(), loadavgs[2]);
        Ok(metrics)
    }

    fn graph_definition(&self) -> Vec<Graph> {
        vec![
            graph! {
                name: "loadavg",
                label: "Load averages",
                unit: "float",
                metrics: [
                    { name: "loadavg15", label: "loadavg15" },
                    { name: "loadavg5", label: "loadavg5" },
                    { name: "loadavg1", label: "loadavg1" },
                ]
            },
        ]
    }
}

プラグインを作るとき、まずどのようにしてメトリックをとるかを調べなければいけません。 これが普段ウェブアプリケーションを書くのとは違っていて実に楽しいわけです。 loadavgの取得方法について調べていくと、getloadavg(3)LinuxBSDの両方で同じように使えることが分かりました。 mackerel-plugin-loadavgはlibc::getloadavgを使うことで、macOSUbuntuの両方で同じようにコンパイルして動作することを確認しています。 一方でuptimeのportableな取得する方法は難しく、LinuxではsysinfoBSDではsysctlboottimeを取得してから計算する必要がありました。

今回プラグインを書くためのライブラリを書く上で、公式のgo-mackerel-pluginをかなり読んで挙動を確認しました。 歴史的な経緯でinconsistentな挙動となってしまっていたり、プラグイン側でやったほうがよさそうな余計な機能があったり、エラーハンドリングを丁寧にやったほうがよさそうなところなど、よろしくないところがいくつかあることに気が付きました。 mackerel-plugin-rsを作る時間にはお給料は出ていませんが、go-mackerel-pluginへの理解が深まり、問題点が把握できたのはよかったなと思います。 いくつか良くない処理は直そうと思っています。 また、ヘルプの文言も不正確な部分は既にいくつか修正しています。

Mackerelのプラグインを知ることは、その監視対象を知ることです。 Redisのプラグインを作ろうと思ったら、redis-cli infoの各項目が何を表しているか、正確に把握する必要があります。 Linuxのメトリックを取得するということは、Linuxに対する理解を深めるということです。 Rustはそういう領域が得意な言語ですので、loadavgをとるには→man 3 getloadavgを見る→libcライブラリのドキュメントでgetloadavgを探すというようにすぐに目的の関数にたどり着くことができます。

Linuxや各種ミドルウェアへの理解を深めることは、エンジニアとしてすごくワクワクすることです。 監視対象に詳しくなると同時に、プログラムを書く楽しさを改めて教えてくれるのです。

mackerel.io

Mackerelはただのサーバー監視ツールではありません。 プラグインを作るワクワクを、みなさんも是非実感してみてください。

本日10/5はMackerel Dayを開催します。 私も会場にいるので、是非お声がけください。

mackerelio.connpass.com

Mackerel開発チームのメンバーが書いた公式の入門書、出ました。

Mackerel サーバ監視[実践]入門

Mackerel サーバ監視[実践]入門

はてなでは、ワクワクしたいエンジニアに限らず、ディレクターやCRE (Customer Reliability Engineer) も募集しております。 hatenacorp.jp