IntelliJ で Python の virtualenv を効かせる設定

IntelliJ の使い方が分からな過ぎて生産性が低い… つらい。

Python の virtualenv を使っている環境で、virtualenv 内のライブラリ群と Python 本体の組み込みライブラリの両方で補完やジャンプを効かせるのに苦労をした。うう…たかだかこれだけのことに、なんでこんなに時間を…。同じことで時間を無駄にしないよう、ここにメモを残す。

端的に言えば、「SDK」と「Modules」の両方に設定を入れる必要がある、ということを理解すればよい。

start

この何とも言えないアイコンから「Project Settings」に飛ぶ。

modules-dependencies.png

その後、「Modules => Dependencies」から「Python 3.6 interpreter Library」にチェックを入れる。

site-packages.png

最後に virtualenv の中の site-packages を Classpath に含めてやればよい。うまく反映されないようであれば、「File => Invalidate Caches / Restart…」を実行する。

入門 Kubernetes

先週『入門 Kubernetes』という本が出版されました。わたしもレビュアーの一人として、この本の製作に携わらせていただきました。

入門 Kubernetes (オライリー・ジャパン)

翻訳者の松浦さんのブログにもある通り、この本の価値のひとつは原著で古くなってしまった部分が訳注として補われているところにあると思います。

原著はKubernetes 1.5あるいは1.6をベースに書かれています。日本語版には、翻訳完了時の最新版であるKubernetes 1.9までに加えられた変更などを、注釈などの形でできるだけ反映しました。

英語に堪能な方であっても、これから読むなら翻訳版の方を読む方がよいでしょう。

また、原著は内容こそ素晴らしいものの、ディテールに難がありました。率直に言うと「サンプルをコピペしても動かない」ことや「説明とサンプルが微妙に一致しない」ことなどがありました。ここに関しても、松浦さんやわたしなどが原著に Errata を提出しつつ、丁寧に内容を修正して日本語版に反映させました。

Kubernetes は今でも勢いがあり、この本も数年後には「古い」本になるだろうなーと思います。買おうか迷っているのであれば、なるべく早く買ってしまうことをオススメします 😉

輪読会:Site Reliability Engineering – 20章

前回に続いて、職場での SRE 本の輪読会のサマリを職場の Qiita:Team から転記しました。

Load Balancing in the Datacenter / データセンタ内でのロードバランス

データセンタ内のマシンの種類は色々あるけれど、その上では単一な (homogeneous な) サーバープロセスが走ります。小さいサービスでも少なくとも3つのプロセスが動き、大きいものであれば10,000プロセスが動くかもしれません。典型的にはサービスは100~1000くらいのプロセスから成ります。このプロセスは backend tasks もしくは単に backends と呼びます。その他のプロセスは client tasks と呼びます。client tasks はどの backend tasks にクエリを処理させるか決めなければなりません。

Google では多種多様なサービスがあり、ポリシの組み合わせも多岐にわたります。ここでは一般的に有用な技術を紹介します。

Google では (ほぼ) 全ての HTTP リクエストはリバースプロキシであるところの GFE (Google Frontend) が受けます。GFE の後ろにいるサーバーに対するロードバランスが本章の対象です。

The Ideal Case / 理想的な場合

どの時間帯にもバックエンドに均一に負荷が分散されることが理想です。全ての backend tasks が同じ CPU 使用率であるべきです。

1

2.png

(右肩下がりなのがダメなわけではなく、均一でないのがダメです。このグラフは CPU[0] が一番 CPU 使用率が高くなるように、意図的にソートされています)

CPU が遊んでいる分だけ無駄にコストがかかっている、ということです。

Identifying Bad Tasks: Flow Control and Lame Ducks / ダメなタスクを特定する: フロー制御とダメなやつ

client request を処理する backends を決めるため、先に backends のヘルスチェックが必要です。

A Simple Approach to Unhealthy Tasks: Flow Control / 機能不全な (backend) tasks を避ける単純な方法: フロー制御

ある backend task に対するアクティブなリクエストが (まだレスポンスを返せていないリクエストという意味) しきい値を超えたとき、その backend task は機能不全だと仮定しましょう。client tasks が機能不全な backend tasks を避けるだけでも簡易なロードバランサと言えるでしょう。

ただし、これだけだと backend tasks に計算資源が余っていても、長い時間居がかかるリクエスト存在するだけで (例えばネットワーク越しで I/O 待ちをしているような task)、それ以上のリクエストを割り当てられなくなります。

A Robust Approach to Unhealthy Tasks: Lame Duck State / 機能不全な (backend) tasks を避ける強力な方法: 役立たず状態

(Lame Duck State という英語を「役立たず状態」と訳しています)

client 視点での backend tasks は次のいずれかの状態に分けられます。

  1. Healthy / (自明なので略)
  2. Refusing connections / 起動中やシャットダウン中であったり、異常な状態に陥っている状態
  3. Lame duck / ポートを listen しているけど、client に対して明示的に「もうリクエストを送らないで」と言っている状態 (=> 以降は「役立たず状態」と表現します)

backend tasks が役立たず状態になると、アクティブな client tasks にその旨をブロードキャストします。アクティブでない client tasks も定期的なヘルスチェックをしているので、それほど時間がかからずに同じ情報を得られます。

役立たず状態を導入する利点は、シャットダウンの処理を簡潔化できることです。運の悪い client tasks が「アクティブだがシャットダウン中」の backend tasks にリクエストをすることを避けられます。

backend tasks のシャットダウンは次のように処理されます。

  1. スケジューラが SIGTERM を該当の backend task に発行する
  2. 該当の backend task が役立たず状態になり、クライアントに新しいリクエストは別な backend tasks に発行するように通知する
  3. 役立たず状態になる前に受け付けたリクエストを処理する
    1. の処理をしつつ、該当の backend task に対するリクエスト数が 0 になるのを待つ
  4. 設定された時間の経過後に、backend task は exit するか、もしくはスケジューラによって kill される

Limiting the Connections Pool with Subsetting / コネクションプールをサブセットで制限する

Google の RPC 実装では client の起動時に backend task に対してコネクションを作り、基本的には開きっぱなしになります。コネクションが idle な場合は、ヘルスチェックの頻度を下げ、TCP コネクションを落として UDP で接続し直します。

Picking the Right Subset / 正しくサブセットを作る

正しいサブセットのサイズは、サービスの種類に依存します。

  • clients の数が backends より極端に少ない場合は、ひとつの client あたりの backends の数を大きくして、リクエストを受けない backends を減らしたい
  • ある client が他よりも大量のリクエストを発行するような場合、その client のリクエストを処理する backends の数は大きくしたい

A Subset Selection Algorithm: Random Subsetting / サブセット選択アルゴリズム: ランダムなサブセット作り

backends をシャッフルして適当にピックアップするというサブセットの作り方もあるでしょう。これだと負荷はそれほど均等に分散されません。次の例を考えてみましょう。

  • 300 clients
  • 300 backends
  • サブセットのサイズは 30% (つまり、各 client は 90 個の backends と接続する)

3.png

最もヒマな backend は平均で 57 コネクションを持ち、忙しい backend は 109 コネクションを持つような分布になる。

サブセットのサイズを小さくしても自体は悪化するだけです。次は各 client に 30 個のコネクションを作らせたときの分布です。

4

A Subset Selection Algorithm: Deterministic Subsetting / サブセット選択アルゴリズム: 決定論的なサブセット作り

「ランダムなサブセット作り」の問題を解消するため、Google は次のようなアルゴリズムを考案しました。

def Subset(backends, client_id, subset_size):
  subset_count = len(backends) / subset_size

  # Group clients into rounds; each round uses the same shuffled list:
  round = client_id / subset_count
  random.seed(round)
  random.shuffle(backends)

  # The subset id corresponding to the current client:
  subset_id = client_id % subset_count

  start = subset_id * subset_size
  return backends[start:start + subset_size]

次のような環境を仮定しましょう。

  • backend tasks は全部で 12 個
  • subset size は 3 個
  • subset count (subset の数) は 4 個 (すなわち 12/3)
  • clients は 10 個 (これは subset_id にマップされる)

冒頭の Python のロジックで backends が次のようにランダムに並び替えられるとしましょう。

  • Round 0: [0, 6, 3, 5, 1, 7, 11, 9, 2, 4, 8, 10]
  • Round 1: [8, 11, 4, 0, 5, 6, 10, 3, 2, 7, 9, 1]
  • Round 2: [8, 3, 7, 2, 1, 4, 9, 10, 6, 5, 0, 11]

これに対して backends[start:start + subset_size] だけのリストを返します。これが client_id に対応する backends のリストになります。

いくつかの補足です。

  • シャッフルしないと backends が id 順にアップデートするような場合に普通になる可能性がある
  • (言葉で説明しづらいけど…) round ごとにシードを変えることで、全ての clients が不通になるケースを減らせる

300 個の clients が300 個の backends に対して各々 10 個のコネクションを張っているときの分布です。

5

Load Balancing Policies / ロードバランスポリシ

Simple Round Robin / シンプルなラウンドロビン

単に順繰りに backends へリクエストしていく方法をラウンドロビンと言います。ナイーブなラウンドロビンでは、暇な backend と忙しい backend でCPU利用率に2倍程度の差が出てきてしまいます。なぜこのような事態が発生してしまうのか、解説していきます。

Small subsetting / 小さなサブセット

ラウンドロビンがうまく負荷分散をできない理由のひとつは、各クライアントが均等に backends に対してリクエストを投げる保証がないからです。特に多くの clients が backends を共有している場合、すなわちサブセットのサイズが小さい場合に起こりがちです。

Varying query costs / 様々なクエリコスト

リクエストによって、必要な計算資源は大きく異なります。Google だと、重いリクエストと軽いものだと 1000 倍程度の差があります。こういう環境ではナイーブなラウンドロビンは負荷分散に向きません。

Machine diversity / マシンの多様性

色々なスペックのマシンが存在していると、ラウンドロビンでは均一に負荷分散できません。速いマシンの 1 CPU ユニットと、遅いマシンのユニットでは、処理効率が違います。この差を吸収するため、Google では GCU (Google Compute Unit) という単位で CPU リソースの計算をしています。

Unpredictable performance factors / 予想しづらいパフォーマンス上の要因

静的に解析しづらい不確定な要素もあります。例えば次のようなものです。

  • 近所の tasks が共有リソースを食い尽くしてしまう (e.g. ネットワーク帯域など)
  • tasks が再起動すると、数分間くらいリソースを多く使ってしまう (ことがある) (e.g. たとえば Java アプリケーションなど)

後者の場合、Google では tasks を役立たず状態 (前述の Lame Duck State) にして、pre-warm します。

Least-Loaded Round Robin / 負荷の低さを指標にしたラウンドロビン

ナイーブなラウンドロビンでは負荷分散しきれないならば、もっともアクティブなコネクションの少ない backends につなぎに行くのはどうでしょう。これは実際のところ だいたい うまくいきます。しかし、落とし穴があります。

healthy でない backend tasks があれば、それはリクエストに対して即座にエラーを返します。つまり、ここに対するアクティブなコネクションは残りにくいのです。結果として clients はこれに対して大量のリクエストを発行しますが、それらは全て失敗します。つらい。

なので、エラー数も鑑みて Least-Loaded Round Robin を組み込みましょう。

Least-Loaded Round Robin では解決しづらい問題は次のふたつです。

  1. tasks にとって支配的な時間は I/O 待ち (e.g. ネットワーク越しのレスポンス待ち) です。あるマシンは別なマシンよりも二倍の処理ができるかもしれないけど、I/O 待ち状態が支配的な場合はそれらのマシンは一緒くたに「アクティブなコネクション数は x」のように扱われてしまう
  2. 各々のクライアントは、別なクライアントが同じバックエンドにどのくらい接続に行っているのかわからない

過去の実験では Least-Loaded Round Robin はナイーブな Round Robin と同程度のパフォーマンスしか出せませんでした。

Weighted Round Robin / 重み付きのラウンドロビン

読んで字のごとしです。これで、ようやくマシンごとの負荷の差がぐっと減りました。

6.png

輪読会:Site Reliability Engineering – 8, 9, 10章

職場で SRE 本の輪読会をしている。職場の Qiita::Team でサマリをまとめて、輪読会仲間にシェアしたのだけれど、別にこの内容は一般に公開してもかまわないものなので、ブログ記事にしてみた。

以下で括弧書きされているものは、注釈と僕の心の声とが半々である。また、本記事の画像は上記の SRE 本からの引用である。

Chapter 8. Release Engineering

Google ではリリースに対して責任を持つ Release Engineering という仕事がある。そういう職種もある。

The Role of a Release Engineer

  • コードが変更されてからリリースされるまでの時間を測っていたりする。
    • この時間を release velocity と呼ぶ。
  • リリースエンジニアはリリースに必要なツールを作る。
  • リリースエンジニアはリリースのベストプラクティスを作る。
    • 一貫性のあるプラクティス
    • 繰り返しデプロイできるプラクティス。

(リリースエンジニアと SRE が協調して働く… みたいな記述があるので、これは SRE とは別な Role なのかな 🤔)

Philosophy

リリースエンジニアの哲学には4つの原則がある。

Self-Service Model

各々のチームがどのくらいの頻度でいつリリースするか自分で決められる。リリース/デプロイツールで、人手を極力排してリリースできるようにする。

High Velocity

少ない変更をどんどんリリースする。テスト結果が “Green” なら常にリリースするようにしているチームもある。

Hermetic Builds (日本語でどう訳されてるんだ… 外部環境に依存しないビルド、みたいな感じかな?)

ビルド/リリースツールは一貫性があり、かつ何度でも実行できなければならない。複数人が同じコードを同時にビルドしたら、成果物は同じでなければならない。

ビルド結果はビルドマシン内のライブラリやソフトウェアには依存しない。ビルドは 特定のバージョンの コンパイラやライブラリに依存する。ビルドは self-contained であり、外部環境には左右されない。(要ディスカッション: たとえば Docker みたいなビルド環境を準備しておいて、そこの中でビルドが閉じている…みたいなことを言いたいのでは)

ビルドツール自身も閉じた環境でビルドしている。

Enforcement of Policies and Procedures

誰が何をできるかコントロールする。誰がコードを変更できるか、新しいリリースを作れるか… etc.

Continuous Build and Deployment

Google には Rapid という自動リリースシステムがある (ぐぐっても詳細が出てこない)。なんかすごいらしい。前述の4つの哲学 (Self-Service Model, High Velocity, etc.) を満足するためのツールらしい。

以下、ソフトウェア開発のライフサイクル @ Google。

Building

Blaze (OSS 版は Bazel) で Google のソフトウェアはビルドされる。Make, Gradle, Maven に類するもの。色んなプログラミング言語をビルド/テストするのに使える。Rapid の一部として組み込まれている。

Branching

コードは全て mainline (Git で言うところの master) にコミットされる。mainline からリリースブランチを切ってリリースする。リリースブランチへの変更を mainline に戻すことはない。バグ修正は mainline にコミットされ、リリースブランチに cherry-pick される。

(そもそもの開発時にどこにコミットする、みたいな話はなし)

Testing

mainline への変更にはテストが走る。

全てのテストが通ったビルドをリリースするのがオススメ (当然では…)。

リリースブランチをリリースするときにもテストを走らせる。全てのテストが通ったことを示すログを保存する。

テストには独立した環境を準備して、ビルド成果物に対してシステムレベルのチェックをする (これは具体的には何を言っているんだ…??)。テストは手動でも起動できるし、Rapid からも起動できる。

Packaging

Google には MPM (Midas Package Manager) という仕組みがあり、これを介してリリースが行われる。MPM は Blaze の成果物をパッケージにする。

# (英辞郎より)
Midas  {名-1} : 《ギリシャ神話》ミダス◆欲張りな Phrygia の王。Dionysus によって、手に触れる物すべてが金になる褒美(苦悩?)を与えられる。

MPM パッケージにはラベルがつけられる。例えば devcanary, production など。新しいパッケージを既存のラベルに適用すると、既存のラベルが示すパッケージが上書きされる。

(割と普通の仕組みだ)

Rapid

  • Rapid は Blueprints というファイルで設定される。
    • DSL で記述される。テスト対象やデプロイ方法、プロジェクトオーナーなどが書かれる。
    • Rapid で誰が何をできるかはロールベースの ACL で管理される。

  • Workflow には、リリース時に何をするかが記述される。
  • Workflow は他の Workflow を起動できる。

Jenkins で例えると (だいたい):

  • Rapid Client: GitHub Webhook / ユーザーのブラウザ
  • Rapid Service: Jenkins サーバ
  • Rapid Build Job: Jenkins ジョブ
  • Tasks (実際には Borg のジョブ): Jenkins のタスク

典型的な Rapid のリリースプロセスは次のような感じ:

  1. 所定のリビジョンナンバーからリリースブランチを作成する。
  2. Blaze でテストとビルドが並列で走る。これらは独立の環境で実行される。
  3. ビルドの成果物が canary deployment 用に準備される。
  4. ここまでの処理内容がログとして記録される。

Deployment

RapidBorg のジョブに MPM の新しいパッケージ (Blueprint ファイルに記述された) を使うように指示してデプロイできる。

さらに込み入ったデプロイには Sisyphus を使う。 Sisyphus は SRE チームが開発した汎用的リリース自動化フレームワーク (Sisyphus もぐぐっても情報が出てこない)。 Python でデプロイメント手順を記述する (??)。

Sisyphus なら簡単なことから難しいことまで何でもできる。たとえば、クラスタ内の全てのジョブを一気に更新したり、数時間かけてゆっくり更新したりできる。なんなら数日間かけることもできる (それうれしいの?)。

Configuration Management

設定管理はリリースエンジニアと SRE が協調してがんばる領域だ。設定管理は簡単っぽくみえるけど、下手にやると不安定さの元凶になる。設定は SCM で管理し、変更にはコードレビューが課されるべきだ。

設定は mainline のモノを使おう…というのが初期の発想だった。これで、バイナリのリリースと設定変更を分離できた。ただし、これだとジョブを明示的に更新しないと設定変更が反映されないので、実行中のシステムと mainline の設定情報とが乖離することがあった。

MPM パッケージにバイナリと設定ファイルを詰め込もう…というのが次の発想だった。設定変更はデプロイ時にしかできないけど、デプロイそのものは単にパッケージを配布するだけに簡素化された。

設定ファイルも MPM パッケージにしよう…というのがさらに次の発想だった。たとえば、ある機能をフラグで on/off できるようにしておいて、実際の on/off は設定パッケージの種類で決まる、など。この場合、「ある機能を含むバイナリ」そのものは再起動する必要がない。

外部のデータストアから設定ファイルを読み込む方法もある。Chubby とか Bigtable に設定を置くこともある。

Conclusions

It’s Not Just for Googlers

(言っていることは分かるが、 Rapid の辺りは完全に Google べったりだろうよ)

Start Release Engineering at the Beginning

リリースエンジニアは後付で作られることが多いけど、それだと大変なので、早い内にそういうロールを考えましょう。

Chapter 9. Simplicity

(短いし思想的なことが主なので、さらっと)

ユーザーは増えるし、機能は追加されるし、ハードウェアは変わるし、安定して稼働するシステムを作るのは難しい。究極的には “agility” と “stability” のバランスを取ることが SRE の仕事だ。

System Stability Versus Agility

“agility” のために “stability” を犠牲にすることはままある。未知の領域を手探りでコーディングすることもある。そういうときには、挑戦して失敗することが問題を真に理解するために必要なステップだったりする。

プロダクションでは “agility” と “stability” のバランスが肝要である。SRE はシステムを安定させることに腐心しつつ、それが agility の低下につながらないようにする。実際には reliable さと agile さには相関がある。reliable なシステム (例えばビルド) がないと、agile に開発できないし。

The Virtue of Boring

退屈なシステムは素晴らしい。予期せぬタイミングで不思議なトラブルが起こるようなシステムは最悪だ。

essential complexity と accidental complexity は区別しましょう。essential complexity は本質的な複雑さ。accidental なのはエンジニアリングで排除できる複雑さ。たとえば、速く動作するウェブサーバーを書くのは essentially complex だ。Java で GC のインパクトを軽減させるのは accidental complexity を減らす仕事だ。 (わかりにくくないですか、この例…)

  • SRE は accidental complexity が導入される変更は差し戻すべきだ。
  • SRE は定期的に複雑さを削減していくべきだ。

I Won’t Give Up My Code!

エンジニアは人間なので、自分が書いたコードに対して執着がある (そうなの?)。

次のような判断は基本的に悪手。

  • 後で必要になるかもしれないので、消すのではなくコメントアウトする。
  • 今はいらないかもしれないけど、消すほどではないので feature flag でロジックが通らないようにする。

書かれたコードは全て負債だと思え。

The “Negative Lines of Code” Metric

新しい行や変更された行はバグをもたらす可能性がある。短いコードは理解しやすく、テストもしやすい。つまり、コードを消すのは素晴らしいことだ。

Minimal APIs

“perfection is finally attained not when there is no longer more to add, but when there is no longer anything to take away”

「足すものが何もない状態ではなく、削るものが何もない状態が最も美しい」的な。

Unix 哲学的な、ひとつのことをうまくやる API を作りましょう、というお話。

Modularity

これも Unix 哲学にしたがう感じの話。ひとつのことをうまくやる API は Modular であるべきで、他の API と組み合わせて使えるようにしましょう、というお話。

Modular なシステムであれば、バグ修正も局所的なリリースで直せる。

API の提供側は API をバージョニングするべき。そうでないと、API のクライアントにコードの変更を強いることになる。

データフォーマットも Modularity を意識すべき。Google の protobuf はシステム間のコミュニケーションを backward and forward compatible に行うためのフォーマット。

Release Simplicity

小さいリリースの方がインパクトも計測しやすい。小さい変更をどんどんリリースしよう。

A Simple Conclusion

Simple なモノは Reliable なモノである。

(SRE が) 機能追加に対して “No” と言うとき、それはイノベーションに反対しているわけではない。環境を Simple に保つことで、イノベーションを加速しているのだ。

ʅ(◞‿◟)ʃ

Chapter 10. Practical Alerting from Time-Series Data

(今回の輪読会の肝。さらっと嘘を書いている可能性あり。ご指摘お願いしたく。ただ、 Borgmon rules は真面目に読めたとしても、どうせ僕らが使えるものではないので、そこはゆるふわで)

Monitoring は Hierarchy of Production Needs の最も基礎的なレイヤーである。

大規模システムのモニタが難しい理由はいくつかある。

  • 単にモニタするコンポーネントが多すぎる。
  • 適切にモニタリングすることで、システムを担当するエンジニアの負荷を下げる必要がある。

Google 規模だと、ひとつのマシンが不具合を起こしているくらいでアラートが鳴ると noisy 過ぎる。

大規模システムは個々のコンポーネントを管理するより、データをまとめあげて異常値 (outlier) を刈りとるべきである。

The Rise of Borgmon

2003 年に Borg ができて、割とすぐに Borgmon というモニタリングシステムが作られた。似たようなソフトウェアには Prometheus などがある。

(ほとんど Borg と Prometheus は同じもののようなので、Prometheus の典型的なシステムを掲載する)

/varz エンドポイントを叩くとメトリクスが取れる。

% curl http://webserver:80/varz
http_requests 37
errors_total 12

Borgmon は他の Borgmon からデータを取得できる。普通はクラスタごとに Borgmon を立てて、グローバルに Borgmon のペアを置く。

(クラスタと一言で表現しても、そのサイズはさまざまなのでは…)

Instrumentation of Applications / アプリケーションの計測一巡り

/varz が plain text でスペース区切りのメトリクスを出力する (前述のやつ)。後の拡張で、ひとつの変数に対して複数のラベルをつけて出力できるようになった。例えば、 HTTP 200 が 25 件で、500 が 12 件の map-valued な値は次のような感じになる。

http_responses map:code 200:25 404:0 500:12

スキーマレスなので、新しいメトリクスを追加するのも簡単で便利 (それだけだと JSON とかでも同じでは?)。

Collection of Exported Data

Borgmon は監視対象の発見に Service Discovery のツールを使う (DNS [BNS] や Consul など)。

Borgmon は一定間隔ごとに /varz を叩いて、結果をメモリに貯める。結果はピアにも共有される。

Borgmon は監視対象ごとに “synthetic (作り物)” な情報も記録する。例えば名前解決が成功したか、いつデータを取得したか、などの情報。

/varz のアプローチは SNMP のような「極力ネットワークを使わないようにする」考え方と違う。HTTP のオーバーヘッドが問題になるケースはほとんどないし、そもそも Borgmon はメトリクスが取れないことそのものを signal として使うことができる。

Storage in the Time-Series Arena

Borgmon は (timestamp, value) の組みでデータを保存する。この形式を time-series と言う。各々の time-series はユニークにラベル (name=value の形式) 付される。

メモリの内容は定期的に TSDB (Time-Series Database) 形式でディスクに Sync される。少し遅くなるけど、Borgmon は TSDB に対してクエリを投げることができる。

Labels and Vectors

time-series の名前は labelset である。なぜなら、これは key=value のラベルのセットで表現されるから。TSDB でひとつの time-series をユニークに探すためには、次のラベルは必須である。

  • var: 変数名
  • job: モニタ対象のサーバー (アプリ) の種類
  • service: 雑に言うとジョブの集合を表すもの
  • zone: Borgmon が所属するデータセンタの名前

これらをまとめて、variable expression として表現できる。

{var=http_requests,job=webserver,instance=host0:80,service=web,zone=us-west}

こういう variable expressionlabelset がマッチする全ての time-series をベクタ形式で取得できる。たとえば、先ほどの expression から instance ラベルを外すと (クラスタ内に複数のインスタンスがあれば) 各々のインスタンスでの最新の http_requests の数がベクタで取得できる。

{var=http_requests,job=webserver,service=web,zone=us-west}

の結果:

{var=http_requests,job=webserver,instance=host0:80,service=web,zone=us-west} 10
{var=http_requests,job=webserver,instance=host1:80,service=web,zone=us-west} 9
{var=http_requests,job=webserver,instance=host2:80,service=web,zone=us-west} 11
{var=http_requests,job=webserver,instance=host3:80,service=web,zone=us-west} 0
{var=http_requests,job=webserver,instance=host4:80,service=web,zone=us-west} 10

期間を指定した time-series のクエリも可能である (variable expression と言ったり query と言ったりしているけど、これらは同じものを指しているのか?)。

{var=http_requests,job=webserver,service=web,zone=us-west}[10m]

ここの [10m] は “直近10分間” を表現する。もしデータを毎分で収集しているのであれば、次のような出力が得られるはず。

{var=http_requests,job=webserver,instance=host0:80, ...} 0 1 2 3 4 5 6 7 8 9 10
{var=http_requests,job=webserver,instance=host1:80, ...} 0 1 2 3 4 4 5 6 7 8 9
{var=http_requests,job=webserver,instance=host2:80, ...} 0 1 2 3 5 6 7 8 9 9 11
{var=http_requests,job=webserver,instance=host3:80, ...} 0 0 0 0 0 0 0 0 0 0 0
{var=http_requests,job=webserver,instance=host4:80, ...} 0 1 2 3 4 5 6 7 8 9 10

Rule Evaluation

Borgmon は単なる programable calculator である。Borgmon プログラミングは Borgmon rules で行う。これを使い代数表現で、ある time-series から別な time-series を作る。

Borgmon rules は可能な限り並列に評価される。後ろの評価が前の評価結果に依存する場合なんかは無理。評価結果として返るベクタのサイズなども実行時間を決める要素のひとつ。実行時間がかかるときは、いい CPU で Borgmon を動かせばいい。

(rateratio が英語版だと可換ではなくて、この書籍の文脈の rate は “時間” を表現しているようだ。でも Borgmon rulesrate() は比率を出してくれているような?)

(Borgmon にはかぎらないけど) モニタにはカウンターとゲージがある。カウンターは増加だけする。たとえば通算アクセス数など。計測インターバル中のデータがロスしないように、カウンターを使うのがオススメ。

次の Borgmon rules をどう評価するか…。

rules <<<
  # Compute the rate of requests for each task from the count of requests
  {var=task:http_requests:rate10m,job=webserver} =
    rate({var=http_requests,job=webserver}[10m]);

  # (前の評価結果に依存している)
  # Sum the rates to get the aggregate rate of queries for the cluster;
  # ‘without instance’ instructs Borgmon to remove the instance label
  # from the right hand side.
  {var=dc:http_requests:rate10m,job=webserver} =
    sum without instance({var=task:http_requests:rate10m,job=webserver})
>>>

このとき、 task:http_requests:rate10m は次のような感じ:

{var=task:http_requests:rate10m,job=webserver,instance=host0:80, ...} 1
{var=task:http_requests:rate10m,job=webserver,instance=host1:80, ...} 0.9
{var=task:http_requests:rate10m,job=webserver,instance=host2:80, ...} 1.1
{var=task:http_requests:rate10m,job=webserver,instance=host3:80, ...} 0
{var=task:http_requests:rate10m,job=webserver,instance=host4:80, ...} 1

dc:http_requests:rate10m はこんな感じ:

{var=dc:http_requests:rate10m,job=webserver,service=web,zone=us-west} 4

dc:http_requests:rate10m みたいな名前づけは Google の convention で、それぞれ “aggregation level”, “the variable name” そして “the operation that created that name” を表現する。

じゃあ、これはどう読む?

{var=task:http_responses:rate10m,job=webserver} はこんな感じの出力になる:

{var=task:http_responses:rate10m,job=webserver,code=200,instance=host0:80, ...} 1
{var=task:http_responses:rate10m,job=webserver,code=500,instance=host0:80, ...} 0
{var=task:http_responses:rate10m,job=webserver,code=200,instance=host1:80, ...} 0.5
{var=task:http_responses:rate10m,job=webserver,code=500,instance=host1:80, ...} 0.4
{var=task:http_responses:rate10m,job=webserver,code=200,instance=host2:80, ...} 1
{var=task:http_responses:rate10m,job=webserver,code=500,instance=host2:80, ...} 0.1
{var=task:http_responses:rate10m,job=webserver,code=200,instance=host3:80, ...} 0
{var=task:http_responses:rate10m,job=webserver,code=500,instance=host3:80, ...} 0
{var=task:http_responses:rate10m,job=webserver,code=200,instance=host4:80, ...} 0.9
{var=task:http_responses:rate10m,job=webserver,code=500,instance=host4:80, ...} 0.1

{var=dc:http_responses:rate10m,job=webserver} はこんな感じ:

{var=dc:http_responses:rate10m,job=webserver,code=200, ...} 3.4
{var=dc:http_responses:rate10m,job=webserver,code=500, ...} 0.6

そんで {var=dc:http_responses:rate10m,job=webserver,code=!/200/} はこんな感じ:

{var=dc:http_responses:rate10m,job=webserver,code=500, ...} 0.6

{var=dc:http_errors:rate10m,job=webserver} は…:

{var=dc:http_errors:rate10m,job=webserver, ...} 0.6

{var=dc:http_errors:ratio_rate10m,job=webserver} は:

{var=dc:http_errors:ratio_rate10m,job=webserver} 0.15

になる (そうなの?)。

Borgmon rules は新しい time-series を作るので on-call のときに読めるし、便利そうなら permanent にコンソールで表示することもできる。

Alerting

10分間のエラー率が1%以上で、1秒に1回以上のエラーが (2分以上) 出るときにアラートをあげる rule:

rules <<<
  {var=dc:http_errors:ratio_rate10m,job=webserver} > 0.01
    and by job, error
  {var=dc:http_errors:rate10m,job=webserver} > 1
    for 2m
    => ErrorRatioTooHigh
      details "webserver error ratio at %trigger_value%"
      labels { severity=page };
>>>

Borgmon は Alertmanager と接続している。これは Alert RPC を受け取ると alert notification を送信する。 Alertmanager で設定できる機能はこんな感じ:

  • 他のアラートが active なときには別なアラートを active にしない。
  • 同じラベルセットを持つ複数のアラートはひとつにまとめあげる。
  • 似たようなラベルセットのアラートが発火したときに fan-out したり fan-in したり。

Sharding the Monitoring Topology

Borgmon は別な Borgmon から time-series をインポートできる。Region ごとに Borgmon で監視対象の time-series を取得して、最終的に global な Borgmon が各 region の Borgmon から time-series を取得する。それが Google のジャスティス。

Black-Box Monitoring

Prometheus は White-Box Monitoring だけど、これだけだとユーザーにインパクトがあるメトリクスはとれない。Black-Box Monitoring も組み合わせましょう。

Prober が Black-Box のための機能で、監視対象に特定のプロトコル (たとえば HTTP) でリクエストを投げて結果が success かどうかを調べる。失敗したら Alertmanager に通知をしてアラートを投げる。(つまりは Runscope みたいな機能かな)

なお prober は Prometheus にもある概念。

Maintaining the Configuration

Borgmon では複数のモニタ対象に同じ Borgmon rules を適用できる。同じような設定をモニタ対象ごとに何度も書き直す必要はない。

Borgmon には language templates というマクロ的な機能もある (しかし具体的にどう機能するか分からず)。これを使い、似通った rule はライブラリ化できる。

人工的な time-series をつかい、rule をテストすることもできる。(これが必要になるということは、rule は複雑になりかねない…ということですよね)

Ten Years On…

Borgmon のおかげで、スケールする管理システムが実現できましたよ、的な話。 check-and-alert な仕組みだとスケールしないですよ、と。

サービス規模のスケールに対してモニタリングのコストのスケールは sublinear であるべきだよ、と。

Borgmon の話ばっかりしていたけど、Prometheus や Riemann、Heka や Bosun 辺りはコンセプトの似た OSS なので、そういうのを調べてみるのもいいかもね、と。

出来の悪いコインで、フェアなコイントスを実現する方法

Essential Algorithms: A Practical Approach to Computer Algorithms』という本を読んでいる。僕の知るかぎり、まだ日本語には翻訳されていないはずだ。勤務先が O’Reilly Safari への加入をサポートしてくれているので、ありがたく読ませてもらっている。

第二章で「コインをはじいたときに、表が出る確率と裏が出る確率が同様に確か ではない コインがあると仮定する。このコインで公平なコイントスを実現するにはどうすればよいか?」という話がある。少し意訳気味だけど、要旨は変えていない。なかなか面白い問いではないだろうか?

本に掲載されている答えはこうだ。

  1. コインを二度はじく。
    • 表、裏の順に出たら、表とみなす
    • 裏、表の順に出たら、裏とみなす
    • この他の場合は、またコインを二度はじく

これでなぜフェアなコイントスができるのだろうか?

この作りの悪いコインをはじいたとき、表が出る確率が P であるとしよう。このとき、裏が出る確率は 1 - P である。このとき:

  • 表、裏の順に出る確率は: P * (1 - P)
  • 裏、表の順に出る確率は: (1 - P) * P

つまり、これらは同じ確率になる。だから、このように組み合わせることでフェアでないコインでも、フェアなコイントスができるというわけだ。

言われてみれば当たり前だけど、なかなか思いつかないよなあ、と思った。

Kotlin の演算子オーバーロード

Kotlin では +- などの演算子をオーバーロードできる。例えば、a + b は Kotlin コンパイラによって a.plus(b) に置換されて実行される。つまり a + b を実現するためには aplus() メソッドを実装すればよい。どのメソッドがどの演算子に置換されるかについては、公式ドキュメントを参照されたい。

演算子オーバーロードの機能を例示するため、 plus() の実装例を次に示そう。

演算子を定義するときは operator キーワードを fun の前に置く。 plus() の実装そのものは、単にプロパティの値を加算するだけのもので、特に何の説明も必要な無いだろう。

実に簡単だ。

演算子オーバーロードの「オーバーライド」

演算子オーバーロードに関する Kotlin Koans の問題が秀逸であったので、紹介したい。

端的に言えば、次のようなクラス定義があるときに date + YEAR * 2 + WEEK * 3 + DAY * 15 のような演算を実現したい、というものだ (このとき dateMyDate 型のオブジェクト)。

data class MyDate(val year: Int, val month: Int, val dayOfMonth: Int)
enum class TimeInterval { DAY, WEEK, YEAR }

模範解答にコメントを追記して、実行可能な状態にしたのが次のコードだ。

TimeInterval* (times) を演算子オーバーロードして、その結果を RepeatedTimeInterval とすることと、 MyDate.plus をオーバーロードして引数が TimeIntervalRepeatedTimeInterval のときで処理を分岐させることは、なかなか自分にはできない発想だなあ、と感じた。

Kotlin の SAM 変換

この記事では Kotlin の SAM 変換について説明する。まず、前準備として SAM インタフェースについて触れる。

SAM インタフェースとは Single Abstract Method インタフェースの略称である。つまり、定義する抽象メソッドがひとつのインタフェースを SAM インタフェースと呼ぶ (後述の通り、この定義は厳密ではない)。

JDK に標準で含まれる SAM インタフェースには RunnableComparator がある。

Kotlin における SAM 変換とは、関数リテラルで SAM インタフェースを置き換えることである。とはいえ、言葉だけでは理解しづらいと思うので、Kotlin Koans のスニペットで解説しよう。

次は ArrayListComparator を実装した無名オブジェクトでソートするサンプルである。この無名オブジェクトは compare() を実装している。

同様のことを Kotlin の SAM 変換機能で次のように簡潔に書ける。

Collections.sort() の第二引数は単なる関数リテラルだが、これが SAM インタフェースの実装として扱われる。

補足

Comparator は本当に SAM インタフェースなのだろうか? Oracle のドキュメントによると、このインタフェースには compare()equals() のふたつの抽象メソッドがある。

この疑問が FAQ なのか分からないけれど、「Maurice Naftalin’s Lambda FAQ」には次のような記載がある。

The interface Comparator is functional because although it declares two abstract methods, one of these—equals— has a signature corresponding to a public method in Object. Interfaces always declare abstract methods corresponding to the public methods of Object, but they usually do so implicitly. Whether implicitly or explicitly declared, such methods are excluded from the count.

Comparator は functional interface (i.e. SAM interface) である。抽象メソッドがふたつ宣言されているものの、そのうち equals()Object クラスのパブリックメソッドとシグネチャが同じだ。interface は常に Object のパブリックメソッドを暗黙的に宣言している。暗黙的であれ明示的であれ、こういうメソッドは数えあげる対象ではない。

なるほど。