発明のための再発明

Webプログラマーが、プログラムの内部動作を通してプログラムを作る時の参考になるような情報を書くブログ(サーバーサイドやDevOpsメイン)

Uber製Docker registy「Kraken」とTorrent

Krakenとは

Uberから、Krakenという拡張性と可用性に焦点を当てたP2P型のDocker registryが公開されました。
3Gのdocker imageを2,600ホストから同時にダウンロードしても、半数以上のホストが10秒で完了し、99%のホストで18秒以内にダウンロードを完了するという性能を持つ、強力なDocker registryです。

ただ、このレポジトリに注目した理由は性能や機能ではなく、torrentというディレクトリがあったからです。
正直、BitTorrentがまだ使われていることすら知らなかったので、「Krakenが言っているtorrentって何を指すんだろう」という興味から覗きました。

※本記事では3bca40a660c0da0a381b84865af3ad3c623283f3のコミットを使用しています。
また、Ubuntuで動作させていますが、サンプルコードがMacのみを想定していたため、スクリプトや設定を少しいじりました。

参考リンク:

Krakenのフロー

Krakenは以下のようなフローで動作します。

f:id:mrasu:20190319000002p:plain

つまり、 docker pushをした場合には、

  1. Proxyサーバーにpush
  2. Originサーバーを通してimageを保存

という順番でアップロードしたdocker imageがKraken上に保存されます。

また、docker pull には

  • Originからpull(P2P無し)
  • 別Agentからpull(P2P)

という2通りの選択肢がありますが、P2P無しは動作確認目的で、実際の稼働時にはP2Pを使用します。

P2Pダウンロード

さて、以下の画像のようにDocker imageのダウンロード元は既にダウンロードしている別ホストです。

f:id:mrasu:20190319000537p:plain

Trackerを通して、要求データがあるホスト(Agent)を見つけ、そこからダウンロードします。
なので、Agentがダウンロードする先は別のAgentです。P2Pですね。

この、「ダウンロード」を司るコードの中にTorrentという言葉が出てきます。
これは、Krakenの開発当初にBitTorrentのdriverを使用していたところから来ているようです。
ただBitTorrentの実装は要求に合わなかったようで、独自にP2Pを実装し直しています。

しかし、Krakenの作成者たちはBitTorrentが大好きなのか、KrakenにBitTorrentとの互換性をもたせたいという夢をROADMAPに掲げています。(ストレージのエンドポイントはただのHTTPなので、Docker registry以外の用途にも使えます)

Scheduler

Scheduler機能がKrakenにも実装されています。
Schedulerは1つのgoプロセス内で動くキューのことで、色々なイベントを順番に実行するために使われます。
例えばKrakenでは、

  • 別ホストとのコネクションを確立する
  • imageをダウンロードする
  • コネクションを切る

などの動作をSchedulerに入れて、順次実行しています。
また、多くの動作はその場で処理を終えるのではなく、ゴルーチンを使って後続処理をしています。
このようにすることで、順次実行しつつ平行に処理できるようになっています。

終わりに

以上Krakenについてでした。

CoreDNSにおけるプラグインチェーンの実装

はじめに

f:id:mrasu:20190217162512p:plain

CoreDNSはサービスディスカバリ機能を持ったDNSです。
CNCFでGraduatedになっているプロジェクトで、KubernetesのデフォルトDNSにもなっています。

この記事では、CoreDNSはプラグイン方式で拡張するようになっているので、その実装を見ていきます。
※ 実装はde2f63d78747b48ae458b8f2c327a01e44cf725cを基にしています。

プラグイン登録

CoreDNSのプラグインは動的にロードするのではなく、Caddyの仕組みを使ってビルド時に組み込まれます。
また、プラグインの実行順序もビルド時に決定されています。
プラグイン情報はzdirectives.goというファイルに定義されていて、zdirectives.goの作成がタスク化されています。
以下のステップを経ることで、プラグインがCoreDNSに組み込まれます。

plugin.cfgに定義

plugin.cfgにはビルドに使用するプラグインと、実行順序が定義されています。
plugin.cfgの内容は以下のようなものです。

metadata:metadata
tls:tls
reload:reload
...
on:github.com/mholt/caddy/onevent

左がプラグイン名で、右がパッケージ名です。
ここに書かれている順番で、プラグインが実行されます。

go generate を実行

go generateは以下のように動きます。

  1. go run directives_generate.goが呼ばれる
  2. zdirectives.goと関連ファイルを作成される

これによって、以下の内容がzdirectives.goに書き込まれます。

var Directives = []string{
    "metadata",
    "tls",
    "reload",
    ...
    "on",
}

このDirectivesが、プラグインの順序を定義しています。

go build を実行

go buildの実行時に、zdirectives.goが使用されます

プラグインチェーンの実行

次は、CoreDNSのプラグインは、実行時の振る舞いについて

開始時に初期化

CoreDNSの開始時に各プラグインを初期化して、zdirectives.goに書かれている順番でプラグインチェーンに登録します

プラグインチェーンの参照

リクエストが来たら、プラグインチェーンにある最初のプラグインが実行されます
server.goh.pluginChain.ServeDNSからプラグインチェーンが開始されます

最初のプラグインを実行

最初のプラグインが実行されます
プラグインはServeDNSというインターフェースを持つので、それが実行されます

後続プラグインの実行

次のプラグインが実行される
plugin#NextOrFailureを呼び出すことで次のプラグインが実行されるので、その関数の前後がプラグイン独自のコードを書く場所です。

まとめ

以上、CoreDNSが実装しているプラグインチェーンを紹介しました。

このブログでは、この記事のようにプログラムを作る時の参考になることを書き続けるつもりです。
もし興味があれば、twitterやブログのフォローしていただけると嬉しいです。

稼働システムのマイグレーションの痛みを和らげる技法

はじめに

サービスを長く継続すると避けられないのが「マイグレーション」です。
しかし、マイグレーションには多くの手間がかかる上に致命的なバグも出やすく、難しい作業です。
そんなマイグレーションを実践し、詳細を公開している企業があります。
それを参考にどのような工夫ができるのかを見ていきます。

紹介するのは以下の企業です。

各事例には元記事へのリンクを書いているので、興味があればリンク先も覗いてみてください。

食べログ: Railsからマイクロサービスへの移行

元スライド: 食べログのマイクロサービス化PJについて

まずは、食べログのマイクロサービス移行についてです。
移行に際しては、共有しているDBをいきなり物理的に分けるのではなく、はじめに論理的に分割する方法を取ったようです。

※ 2018年9月末時点で進行中なので、現在も進行していると思われます。

移行理由

既存のシステムでは、

  • メインのデータベースが一つ
  • modelやlibをアプリケーション間で共有している
  • デプロイは全アプリケーション同時に行う

というアーキテクチャをもっているため、共有されたデータ構造を変更する際に影響範囲が広く対応が大変だそうです。
そのため、マイクロサービスへと移行し始めたそうです。

移行方法

移行の手順をまとめると、

  • 始めからサービスを細かく分解することを諦める
  • まずは境界が明確な2つのサービス(下図)へと分割し、境界の横断にはAPIを使うように修正する
  • 全サービスがAPIを使用するようにできたら、DBを分割する(予定)

このように、DBの共有を止めたいけれど、ビッグバンリリースでは大変なので、まずは、ソースコードの修正によって論理的にDBを分割します。
その後に、物理的にDBを分けるという戦略です。

The Guardian: MongoDBからPostgreSQLへの移行

元記事: Bye bye Mongo, Hello Postgres

次は、イギリスの新聞社である「The Guardian」がMongoDBを捨てて、PostgreSQLへ移行した方法についてです。
旧コードを本格稼働させつつ新コードにも同内容を実行することで、新旧コードが同じ結果になることを確かめながら移行したようです。

移行理由

The Guardianには記事の管理に使用している内製CMSツールがあり、MongoDBとマネジメントツールであるOpsManagerを使用していたのですが、

  • AWS上に自分たちでMongoDBとOpsManagerを建てて、管理する必要がある (マネージドサービスが使えなかった)
  • 障害時にMongoDBもOpsManagerも助けにならなかった
  • OpsManagerを入れれば管理が楽になると思ったが、そうでもなかった
  • 価格が高い

という要因が合わさり、移行先を探し始めてPostgreSQLへ決めたそうです。
ちなみに、移行時にスキーマを作り直すことはせず、JSONB型の列を用意してMongoDBの内容をそのまま持っていったようです。

移行方法

移行は下の手順で行ったそうです。

  1. 新DB(PostgreSQL)への書き込みAPIを用意する
  2. 前段にプロキシを用意して、新旧両方のAPIに同一のリクエストが流れるようにする(プライマリは旧API)
  3. 新DBへデータを移行
  4. 新旧APIのレスポンスが同一であることをチェック (別の結果が返ってきたら、新DBのデータを巻き戻してやり直し)
  5. APIをプライマリにする
  6. 旧DB(MongoDB)を削除

f:id:mrasu:20190115011442p:plain

プロキシを用意して変更の有無を確認することで、ユーザーに影響を与えずに新APIのエラー検出が出来るようになっています。
また、APIはelasticsearchなどとも連携しているので、APIが間違ったデータを返してしまうと影響が大きかったことからも、慎重に確認したかったようです。

Dropbox: 複数RPCからgRPCへの統合

元記事: Courier: Dropbox migration to gRPC

最後は、Dropboxが行ったRPCの統合についてです。
かつてDropboxは自作プロトコルでのRPCや、ApacheThrift、http1.1でのRPCを使用していたのを、CourierというgRPCを使うための新フレームワークへ移行したそうです。

移行理由

旧RPCフレームワークに変わり、Courierという新フレームワークを作成することで以下のことを可能にしたそうです。

  • サーバー・クライアントを認証・認可するためのDropbox内部にある機構との統合
  • ログや統計などの情報の統合・可視化
  • タイムアウト・サーキットブレーカー設定の強制

移行方法

移行に際しては、旧フレームワークからの移行を開発者が簡単に出来るように注意しつつ、以下のステップを踏んだそうです。

  1. 旧RPCフレームワークのコードのフリーズ
  2. Courierと旧フレームワーク双方を使用できる共通インターフェースの作成
  3. 共通インターフェースを使用するようにコードを変更 (稼働させるのは旧RPC)
  4. 旧RPCサーバーとCourierサーバーを同時稼働
  5. クライアントをCourierへ切り替え
  6. 旧コードの削除

フレームワークへの機能追加を止めることで開発者たちへ移行のインセンティブを与えたことと、新旧フレームワークを同時に使えるようにして順次切り替えを行い「移行」に対するリスクを抑えたという、工夫が見れます。

まとめ

以上、各企業がマイグレーションをした方法とその理由を掲載している記事を紹介しました。

このブログでは、この記事のようにプログラムを作る時の参考になることを書き続けるつもりです。
もし興味があれば、twitterやブログのフォローしていただけると嬉しいです。