MakingSenseofStreamProcessing / How Logs Are Used in Practice


Chapter 2 How Logs Are Used in Practice

要約

(これからまとめる)

詳細

この章の残りの部分では、実際にログをどのように使用するかの例をいくつか説明します(図2-14)。 毎日使う可能性が高いデータベースやシステムにログが既に存在していることが分かります。 さまざまなシステムでログがどのように使用されているかを理解すると、データ統合の問題をどのように解決するのかを理解する上でより良い立場になります。

図2-14。 ログを使用する4つのコンピューティング分野。 私たちはそれぞれを順番に見ていきます。

まず、データベースストレージエンジンの内部について説明します。

1) Database Storage Engines

アルゴリズムクラス(図2-15)からBツリーを覚えていますか?これらはストレージエンジンにとって非常に広く使用されているデータ構造です。ほとんどすべてのリレーショナルデータベースと多くの非リレーショナルデータベースで使用されています。

図2-15。 Bツリーの上位レベル。

簡潔に要約すると、Bツリーはページで構成されます。ページはディスク上の固定サイズのブロックで、通常4〜8 KBのサイズです。特定のキーを検索するには、ツリーのルートにある1ページから始めます。ページには他のページへのポインタが含まれ、各ポインタには一連のキーが付いています。たとえば、キーが0〜100の場合、最初のポインタに従います。あなたのキーが100〜300の場合は、2番目のポインタに従います。等々。

ポインタを使い別のページに移動し、キー範囲をさらに下位の範囲に分割します。最終的に、あなたが探している特定のキーを含むページになります。

新しいキー/値ペアをBツリーに挿入する必要がある場合はどうなりますか?キー範囲に挿入しているキーが含まれているページに挿入する必要があります。そのページに十分な余裕がある場合は問題ありません。しかし、ページがいっぱいになると、2つの別々のページに分割する必要があります(図2-16)。

図2-16。 完全なBツリーページを2つの兄弟ページ(赤いアウトライン)に分割する。親のページポインタ(黒いアウトライン、赤い塗りつぶし)も更新する必要があります。

ページを分割するときは、分割の結果である2ページと、分割ページへのポインタを更新するための親ページを少なくとも3ページ、ディスクに書き込む必要があります。ただし、これらのページはディスク上のさまざまな場所に格納されている可能性があります。 これにより、データベースの一部がディスクに書き込まれた後、操作が途中でクラッシュしたり、電源が切れたり、何かが間違ってしまうとどうなるのでしょうか?その場合、一部のページには古い(分割前の)データがあり、他のページには新しい(分割後の)データがあり、それは悪いニュースです。あなたは誰も指していないポインタやページをぶら下げてしまう可能性が最も高いです。つまり、インデックスが破損しています。

さて、ストレージエンジンは何十年も前からこれをやってきましたが、B-Treesをどのように信頼できるものにしていますか?答えは、先行書き込みログを使用していることです。

Write-ahead log

先行書き込みログ(WAL)は特定の種類のログです。 ストレージエンジンがB-Treeに何らかの変更を加えたい場合は、最初にディスク上の追加専用ファイルであるWALに変更を書き込む必要があります。 変更がWALに書き込まれ、永続的にディスクに書き込まれた後でのみ、ストレージエンジンはディスク上の実際のB-Treeページを変更できます。

これにより、B-Treeが信頼できるものになります。データがWALに追加されている間にデータベースがクラッシュした場合でも、B-Treeにはまだ触れられていないため、問題はありません。 また、B-Treeが変更されている間にクラッシュしても問題はありません。なぜなら、WALには何が起こるかについての情報が含まれているからです。 クラッシュ後にデータベースが復旧すると、WALを使用してB-Treeを修復し、一貫性のある状態に戻すことができます。

これは、ログが本当にすっきりしたアイデアであることを示す最初の例です。

Log-structured storage

ストレージエンジンはBツリーで停止しませんでした。賢い人々の中には、とにかくログにすべてを書き込むと、ログをプライマリストレージメディアとして使用する可能性があることに気付きました。これは、HBaseとCassandraで使用されるログ構造のストレージと呼ばれ、Riakではバリアントが表示されます。

図2-17。 ログ構造ストレージでは、ログセグメントに書き込みが追加され、バックグラウンドで定期的にマージ/圧縮されます。

ログ構造のストレージでは、ファイルが大きすぎるため、探しているキーを見つけるのが難しくなるため、常に同じファイルに追加する必要はありません。代わりに、ログはセグメントに分割され、図2-17に示すように、ストレージエンジンはセグメントをマージして重複したキーを破棄します。セグメントは、キーで内部的にソートすることもできます。これにより、探しているキーを簡単に見つけることができ、マージも簡単になります。ただし、これらのセグメントは引き続きログに記録されます。これらのセグメントは順次書き込まれ、書き込まれた後は不変です。

ご覧のように、ログはストレージエンジンで重要な役割を果たします。

2) Database Replication

ログが使用される2番目の例に移ってみましょう:データベースレプリケーション。

レプリケーションは、多くのデータベースで使用できる機能です。同じデータのコピーを複数の異なるノードに保存することができます。これは、負荷を分散させるのに役立ちます。また、あるノードが停止した場合、別のノードにフェールオーバーすることもできます。

レプリケーションを実装する方法はいくつかありますが、1つのノードをリーダー(プライマリまたはマスター)とし、他のレプリカをフォロワー(スタンバイまたはスレーブとも呼ばれます)として指定するのが一般的です(図2-18 )。私はマスター/スレーブの用語が好きではないので、リーダー/フォロワーに固執するつもりです。

図2-18。 リーダーベースのレプリケーションでは、リーダーは書き込みを処理し、レプリケーションログを使用してフォロワーに書き込みについて通知します。

クライアントがデータベースに何かを書こうと思うときはいつも、それはリーダーに話す必要があります。読み取り専用クライアントは、リーダーまたはフォロワーのどちらかを使用できます(ただし、フォロワーは通常非同期なので、最新の書き込みがまだ適用されていない場合は期限切れの情報がある可能性があります)。

クライアントがリーダーにデータを書き込むとき、そのデータはどのようにフォロワーに到達しますか?大きな驚き:彼らはログを使用する!実際には先読みログと同じレプリケーションログを使用します(これはPostgresのようなものです)。または、別のレプリケーションログである可能性があります(MySQLはこれを行います)。

レプリケーションログは次のように動作します。リーダーにデータが書き込まれるたびに、レプリケーションログにも追加されます。フォロワーは、書き込まれた順番でそのログを読み取り、それぞれの書き込みをそれぞれのデータのコピーに適用します。その結果、各フォロワーはリーダーと同じ順序で同じ書き込みを処理するため、同じデータのコピーが作成されます(図2-19)。

図2-19。 フォロワーは、レプリケーションログに表示される順序で書き込みを適用します。

書き込みがリーダー上で並行して行われても、ログには合計順序で書き込みが含まれます。したがって、ログは実際には書き込みから同時性を取り除きます。書き込みのストリームから非決定論をすべて絞り出します。フォロワーでは、書き込みが発生した順序は間違いありません。

では、先に議論した二重書き込みの競合状態はどうでしょうか(図2-9)?

クライアントがフォロワーに直接書き込みを行わないため、この競合状態はリーダーベースのレプリケーションでは発生しません。フォロワーによって処理される書き込みは、レプリケーションログから受け取るものだけです。そして、ログはそれらの書き込みの順序を修正するので、最初に起こったことに関するあいまいさはありません。

さらに、すべてのフォロワーが同じ順序でログを見ることが保証されているので、2回の上書きが連続して発生すると、それは問題ではありません。すべてのフォロワーが同じ順序で書き込みを適用し、結果としてすべてが同じになります最終状態。

しかし、前に議論した二重書き込みの第2の問題、すなわち書き込みが成功し、別の書き込みが失敗する可能性があります(図2-11)。フォロワーは、トランザクションからの最初の書き込みを正常に処理できますが、トランザクションからの2番目の書き込みを処理できません(図2-20に示すように、ディスクがいっぱいであるかネットワークが中断されている可能性があります)。

図2-20。 ネットワークの中断により、フォロワーはログからの書き込みの適用を停止しますが、ネットワークの修復時に簡単にレプリケーションを再開できます。

リーダーとフォロワーの間のネットワークが中断された場合、レプリケーションログはリーダーからフォロワーに流れることはできません。以前に議論したように、これは不整合なレプリカにつながる可能性があります。どのようにしてデータベースレプリケーションがそのようなエラーから回復し、矛盾を避けることができますか?

リーダーには常にリーダーが追加されるだけなので、ログ内の各レコードには常に増加する連続番号が与えられます(これはログの位置またはセットと呼ばれます)。さらに、フォロワは、それを順番に(左から右に、すなわちログ位置が増加する順に)処理するだけなので、フォロワの現在の状態を単一の番号、すなわち処理した最新のレコードの位置を記述することができる。

ログ内のフォロワーの現在の位置を知っていれば、ログ内のすべての以前のレコードがすでに処理されており、後続のレコードが処理されていないことを確認できます。

エラーリカバリが非常に簡単になるため、これは素晴らしいことです。フォロワーがリーダーから切断されるか、クラッシュした場合、フォロワーは、レプリケーションログを処理したログの位置を保存するだけで済みます。フォロワーが回復すると、リーダーに再接続し、以前に処理した最後のオフセットから開始するレプリケーションログを要求します。したがって、フォロワーは、切断された間に欠落したすべての書き込みを、データを失うことなく、またはレプリケーションを受信することなく追いつくことができます。

ログが完全に順序付けられているという事実は、すべての書き込みを個別に追跡する必要がある場合よりも、この回復をはるかに簡単にします。


3) Distributed Consensus

実際のログの3番目の例は別の領域です。 分散コンセンサス。 コンセンサスを達成することは、よく知られており、しばしば論議される分散システムの問題の1つです。それは重要ですが、解決することも驚くほど困難です。

現実世界のコンセンサスの一例は、昼食のためにどこに行くかについて友人のグループに合意させることです(図2-21)。これは洗練された文明の特徴であり、特にあなたの友人の中には気が散っていて(あなたの質問にいつも反応するとは限らない)、あるいは彼らが嫌な食べ物を食べている場合には、驚くほど困難な問題になることがあります。

図2-21。 コンセンサスは、空腹になりたくなく、データを失いたくない場合に便利です。

私たちの通常のコンピュータドメインにもっと近く、分散データベースシステムのコンセンサスが望まれる場所の例です: たとえば、すべてのデータベースノードが、データベースの特定のパーティション(シャード)に対してどのノードがリーダーであるかを一致させる必要がある場合があります。

彼らがリーダーであることに皆同意することは非常に重要です。 2つの異なるノードがどちらもリーダーであると考えている場合は、クライアントからの書き込みを受け入れることもできます。後で、そのうちの1人が間違っていて、結局リーダーではないと分かったとき、それが受け入れた執筆は失われるかもしれない。この状況はスプリットブレインと呼ばれ、データ損失を引き起こす可能性があります。

コンセンサスを実装するためのアルゴリズムはいくつかあります。 Paxosはおそらく最も有名ですが、Zab(ZooKeeper?が使用)、Raftなどもあります。これらのアルゴリズムは非常に扱いにくく、わかりにくい微妙な部分があります。このレポートでは、Raftアルゴリズムの一部を非常に簡単に説明します(図2-22)。

図2-22。Raftコンセンサスプロトコル:値X = 8が提案され、ノードがそれに投票する。

コンセンサスシステムでは、いくつかのノード(3つのノード:図2-22)は、特定の変数の値を一致させることを担当しています。クライアントは、ラフトノードの1つにそれを送ることによって、例えばX = 8(ノードXがパーティション8のリーダーであることを意味する)のような値を提案する。そのノードは他のノードから投票を収集する。大多数のノードが、値がX = 8であることに同意する場合、第1のノードは値をコミットすることが許可される。

その値がコミットされると、どうなりますか? Raftでは、その値がログの末尾に追加されます。したがって、Raftがやっているのは、ノードがある特定の値に合意するだけではなく、時間の経過とともに合意された値のログを実際に構築しているということです。すべてのRaftノードは、ログにコミットされた値のシーケンスが完全に同じであることが保証されており、クライアントはこのログを消費できます(図2-23)。

図2-23。 Raftプロトコルは、単一の値だけでなく、合意された値のログを集約します。

新たに合意された値がコミットされ、ログに追加され、他のノードにレプリケートされた後、X = 8という値を最初に提案したクライアントには、システムがコンセンサスに達したという応答が送信されます。提案された値は現在、Raftログの一部です。

(理論上の問題として、コンセンサスと原子放送の問題、つまり正確に一回の配信でログを作成するという問題は、お互いに還元することができます.これは、Raftのログを使用することは単なる実装の詳細ではなく、解決しているコンセンサス問題の基本的な特性を反映しています。)

4) Kafka

ログは、ストレージエンジン、データベースレプリケーション、コンセンサスなど、驚くほど多くのコンピューティング分野で繰り返されるテーマであることがわかりました。最後の4番目の例として、Apache Kafka(ログの考え方に基づいて構築された別のシステム)について説明します。 Kafkaの興味深い点は、ログがあなたから隠されていないことです。 Kafkaは、ログを実装の詳細として扱うのではなく、それを公開してアプリケーションを構築することができます。

図2-24。 Kafkaは通常、パブリッシュ/サブスクライブイベントストリームのメッセージブローカーとして使用されます。

図2-24に示すように、メッセージブローカ(メッセージキュー)としての典型的な使用方法は、AMQP(RabbitMQなど)、JMS(ActiveMQやHornetQなど)、その他のメッセージングシステムと多少匹敵します。Kafkaには、Kafkaにメッセージを送信するプロデューサーまたはパブリッシャーと、Kafkaでメッセージのストリームを読むコンシューマまたはサブスクライバーの2種類のクライアントがあります。

たとえば、プロデューサはあなたのWebサーバーやモバイルアプリであり、Kafkaに送信するメッセージの種類は情報を記録することで、つまり、どのユーザーがどの時点でどのリンクをクリックしたかを示すイベントです。コンシューマは、起こっていることを知る必要のあるさまざまなプロセスです。 分析を生成したり、異常なアクティビティを監視したり、ユーザーのためのパーソナライズされた推奨事項を生成したりすることができます。

Kafkaを他のメッセージブローカーと面白くすることは、ログとして構成されていることです。実際、Log4jやSyslogの意味でのログファイルに多少似ています。プロデューサがKafkaにメッセージを送信すると、それは文字通りディスク上のファイルの末尾に追加されます。したがって、図2-25に示すように、Kafkaの内部データファイルは一連のログメッセージに過ぎません。 (アプリケーションログファイルでは通常、改行文字を使用してレコードを区切りますが、Kafkaはチェックサムと有用なメタデータを含むバイナリ形式を使用しますが、原則は非常に似ています)。

図2-25。 Kafkaのメッセージは、ファイルの最後にログレコードとして追加されます。

Kafkaがすべてのファイルを1つのファイルに順次書き込んだ場合、そのスループットはディスクのシーケンシャル書き込みスループットに制限されます(おそらく数十MB /秒ですが、これでは不十分です)。Kafkaをスケーラブルにするために、メッセージのストリーム(トピック)を複数のパーティションに分割します(図2-26)。各パーティションはログであり、完全に順序付けられた一連のメッセージです。ただし、異なるパーティションは互いに完全に独立しているため、異なるパーティション間で順序保証がありません。これにより、異なるパーティションを異なるサーバーで処理できるため、Kafkaは水平方向に拡大縮小することができます。

図2-26。 Kafkaのデータストリームはパーティションに分割されます。

各パーティションはディスクに保存され、複数のマシンに複製されるため、耐久性があり、データの損失なしにマシンの障害に耐えることができます。ログの生成と使用は、以前にデータベースレプリケーションのコンテキストで見たものと非常によく似ています。

•Kafkaに送信されるすべてのメッセージは、パーティションの最後に追加されます。それはKafkaがサポートする唯一の書き込み操作です:ログの最後に追加してください。過去のメッセージは変更できません。

•各パーティション内のメッセージは、単調に増加する集合(ログの位置)を持ちます。 Kafkaからのメッセージを消費するために、図2-26の紫色の矢印で示されるように、クライアントは特定のオフセットから順にメッセージを読み取ります。オフセットはコンシューマによって管理されます。

私達は以前、KafkaはAMQPやJMSメッセージングシステムに似たメッセージブローカーであると言いました。しかし、類似性は表面的ですが、メッセージはすべてプロデューサからコンシューマに中継されますが、ボンネットの実装は非常に異なります。

最大の違いは、システムがどのようにしてコンシューマがすべてのメッセージを処理するかを、エラーが発生した場合にメッセージを廃棄することなく確実にすることです。 AMQPおよびJMSベースのキューでは、コンシューマはすべての個々のメッセージが正常に処理された後でそれを確認します。ブローカはすべてのメッセージの確認ステータスを追跡します。メッセージを確認せずにコンシューマが死亡した場合、ブローカは図2-27に示すように配信を再試行します。

図2-27。 AMQPおよびJMSメッセージブローカは、メッセージごとの確認を使用して、どのメッセージが正常に消費されたかを追跡し、コンシューマが失敗したメッセージを再配信します。

この再配信動作の結果、メッセージは順不同で配信されます。コンシューマは、プロデューサがメッセージを送信した順序とまったく同じ順序でメッセージを必ずしも参照する必要はありません。 AMQPとJMSは、メッセージの順序が重要ではない状況に合わせて設計されているため、この再配信の動作が望ましいです。

ただし、データベース複製などの状況では、メッセージの順序が重要です。たとえば、図2-13では、Xが最初に6に設定されてから7に設定されるため、最終値は7になります。レプリケーション・システムでメッセージの順序を変更できる場合は、同じことを意味しなくなります。

Kafkaは1つのパーティション内でメッセージの固定順序を維持し、常に同じ順序でメッセージを配信します。その理由から、Kafkaはすべての単一のメッセージの承認を追跡する必要はありません。代わりに、コンシューマが各パーティションで処理した最新のメッセージオフセットを追跡するだけで十分です。メッセージの順序は固定されているので、現在のオフセットより前のすべてのメッセージが処理され、現在のオフセット後のすべてのメッセージがまだ処理されていないことがわかります。

Kafkaのモデルは、メッセージの順序が重要なデータベースのようなアプリケーションに使用できるという利点があります。他方、コンシューマオフセット追跡は、コンシューマが単一のスレッド上で順次にメッセージを処理しなければならないことを意味します。したがって、2つの異なるメッセージングシステムファミリを区別できます(図2-28)。

図2-28。 AMQPとJMSはジョブキューに適しています。Kafkaはイベントログに適しています。

一方で、個々のメッセージごとに肯定応答を追跡するメッセージブローカーは、あるサービスが別のサービスに代わってタスク(電子メールの送信、クレジットカードの請求など)を実行するように要求するジョブキューに適しています。このような状況では、メッセージの順序は重要ではありませんが、スレッドのプールを簡単に使用してジョブを並行して処理し、失敗したジョブを再試行できることが重要です。

一方、Kafkaは、イベントのロギング(ユーザーがウェブページを閲覧したという事実、または顧客が何らかの製品を購入したという事実)に関しては優れている。サブスクライバーがこれらのイベントを処理するときは、通常、非常に軽量な操作(イベントをデータベースに格納するか、またはいくつかのカウンタを増やすなど)であるため、1つのスレッドで1つのKafkaパーティション内のすべてのイベントを処理することが可能です。並列処理の場合、複数のスレッドを複数のマシンで使用するため、Kafkaのコンシューマーは複数のパーティションにデータを分散するだけです。

異なるツールはさまざまな目的に適しているため、同じアプリケーションでKafkaとJMSまたはAMQPメッセージングシステムの両方を使用することは完全に合理的です。

担当者のつぶやき

  • 文量がおおいです。。

みんなの突っ込み