発明のための再発明

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

Aurora - クラウド時代のDBアーキテクチャ

はじめに

Amazon Auroraは、AWSを触る人ならほとんどの人が利用を検討したことがあるでしょう。

Amazon社内ではOracleを止めたというtweetもありました

そんなAuroraは、従来のRDBとは違いクラウド上で動くことを念頭に設計されています。
また、ログが中心的な役割を持つことから「The log is the database」と表現されることもあります。

そんなAuroraの仕組みについての論文を読んだので紹介します。

読んだ論文は以下の2つです。

The log is the database

Auroraはクラウド時代のアーキテクチャを持っています。

従来のDB(MySQL, PostgreSQL, Oracle, SQLServer etc)は1台のマシン上で動作することを基本に設計されています。その上で、バックアップや性能向上などより複雑なニーズに対応するために複数台構成をサポートしています。

しかし、Auroraが前提としているのはクラウドです。
つまり、コンピュータが世界中に分散され、莫大な数のマシンが共同する世界です。
クラウドの世界ではレジリエンスとスケーラビリティを向上させるために、計算とストレージを分離しストレージを複数ノードで管理することが重要だとされています。
そこで、Auroraでもストレージは複数のAZを利用して6つの場所に保存されています。大まかには下図の構成をもっています。

f:id:mrasu:20190429142222p:plain

しかし、複数のAZを使用するとネットワークがボトルネックになります。また、関わるノードが増えるということは故障率も上がるということです。

そのため、Auroraでは以下を念頭に設計されています。

  1. 複数台へのデータ保存
  2. 通信量削減
  3. 高速なバックアップ・リカバリ
  4. 簡易なノード変更

これらを実現するために採用された「ログ中心のアーキテクチャ」を端的に表した言葉が、
The log is the database
です。

以下の章ではこれらについて、紹介します。

複数台へのデータ保存

Auroraはクラウドの世界に合わせ、6ノードに同じデータを保存するようになっています。

Auroraでは10GB単位で6ノードにデータを保存しています。
6箇所のデータをまとめた単位がPG(Protection Group)と呼ばれます。
このPGに保存されていることが、「DBに保存された」ということを表すので、PG内での合意やPG内のノード入れ替えなど、PGに関する動作がAuroraの耐久性に影響します。
ちなみに、10GB単位なのは各ノードの修復時間(MTTR)を短縮するためです。

さて、PGの合意にはquorumを使っています。
通常のquorumでは3ノードを使用して、2/3の票があればよいとしていますが、Auroraでは6ノードを使用しています。
具体的には、readが3/6, writeが4/6の票を必要とします。
これは、6ノードを3つのAZに配置して、AZ+1ノードの障害を許容するためにあります。
AZ+1ノードの障害を許容するとは「一つのAZ全体が障害になり、さらにもう1台の障害が起きた場合でも動く」ことを指します。これは、火災や洪水、屋根の崩落などでAZ全体が障害になる可能性があるため、同一AZに所属しているノードの障害は独立していないと考えたことから来ています。
下図のように障害耐性があがるということです。

f:id:mrasu:20190429141923p:plain

さらに、各ノードが持つデータについて詳しく見ると、全ノードが同じデータを持っているわけではありません。
6ノードは全てがredoログという、Auroraの変更履歴を持っていますが、3ノードしかAuroraの現在のデータをマテリアライズしていません。
これは、redoログだけがあればデータをマテリアライズすることが可能であることと、保有するデータ量を削減する目的で行われています。

通信量削減

Auroraの前提がクラウド環境であり、ネットワークがボトルネックだという話を最初に書きましたが、それに対するAuroraの対策を紹介します。
Auroraを象徴する「The log is the database」について、です。

従来のDBではレプリケーションのために多くのデータが共有されます。例えば、redo log、binary log,FRMなどです。
対して、Auroraではredo log(以下、redoログ)のみを各ノードが共有します。
AuroraはDB上のデータを変更するためにログを共有しているのではなく、DBにあった出来事を記録するためにログを共有しています。
つまり、クライアントからSQLでデータ変更を指示された場合、各ノードはredoログを受け取り、そのログを基にデータを再生します。
下図がデータの流れを表しています。

f:id:mrasu:20190429141835p:plain

これは、ノードが新規に追加された場合や再起動された時にも同じです。足りないログを受け取り、再生する。
これによって、従来のDBにあるcheckpointやクラッシュリカバリに対する特殊処理が不要になります。
checkpointのように、定期的な「ディスク書き込み」は無くなり、redoログの書き込みで十分なのです。
また、障害が発生した場合にはAuroraが合意できる地点までのログを再生すれば良いのです。
この「ログを再生する」という処理はデータ変更時に常に行っている作業なので、障害発生時でも通常通りの動作を行っているということを意味します。

さて、各redoログにはLSN(Log Sequence Number)という番号が振られています。Auroraの各ストレージがどこまで変更に追いついているかを把握するためにも使われ、また、quorumで使用する4/6のwrite制限もLSNを利用しています。
つまり、Auroraは書き込み時に4/6の合意がある事をもって「書き込み」としています。
また、quorumのreadは通常使われません。各ノードのLSNの進み具合も把握しているため、通常のデータ読み込み時にはquorumでの合意は必要とせず、直接ノードに対してLSN時点でのデータを返させています。
quorumのreadの合意はクラッシュの復旧時などに使われます。

高速なバックアップ・リカバリ

redoログへの一本化によりバックアップ、リカバリも高速化されます。

Auroraにとってのバックアップとは、redoログを保存することです。また、リストアはredoログを再生することです。
各ノードは10GB単位でデータを持つので、各ノードにとっては自身に関わるログを手にすればリストアできるということです。
これは、障害時にも新規追加されるときにも同様で、自身が持っていないログをPG内の別ノードから貰えばよいということです。
同期の操作は10秒で終わります。

つまり、Auroraのquorumを破壊するためには、10秒以内に3台のノードを同時に破壊する必要が有ります。
この耐障害性によって、ソフトウェア更新などのためにノードを外すことが簡単に出来ます。LSNの進行が遅い(ノードが遅い)と検知されるたときも、簡単に交換できます。

簡易なノード変更

前章で簡単にノードを外せることに触れました。その時の動作について深堀します。

通常、quorumのノードを変更する場合、障害や遅延に対する影響が大きくなるので辛い作業です。
しかし、Auroraでは最大38,400(64TB使用時)のノードが存在するので障害の頻度が高くメンバー変更は簡単に出来る必要があります。

そのために、Auroraでは2段階の遷移を行います。
1度目は故障したノードと新規ノードを入れ替えたquorumを追加します。そして、新規ノードのデータが追いついたところで、故障したノードを含むquorumを削除します。
これによって、故障したノードが復帰することを考慮して、新規ノードの追加を遅延させる必要がなくなります。
下図のような遷移をします。

f:id:mrasu:20190429141950p:plain

ちなみに、故障したノードが現在データをマテリアライズしている唯一のノードだった場合、新規ノードは他のマテリアライズしているノードからデータをコピーした後に、redoログを再生することで最新までのデータをマテリアライズすることができます。

終わりに

以上、Auoraについてでした。

記事としては長くなりましたが、論文はさらにAuroraの詳細を語っています。
例えば、redoログに関する動作(readやwrite、共有方法、パフォーマンスなど)こそが、両論文のメインテーマですがこの記事では大幅に削っています。
もし、Auroraへの興味がさらに湧いたなら、ぜひ両論文を読んでみてください。特にAmazon Aurora: Design Considerations for High Throughput Cloud-Native Relational DatabasesはAuroraの全体を説明しているので、読みやすいです。

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やブログのフォローしていただけると嬉しいです。