ゲルフでの冒険

コンテナーでアプリを実行していて、Docker の GELF ログ ドライバーを使用している (または使用を検討している) 場合は、次の考え方が興味に関連している可能性があります。

いくつかのコンテキスト

コンテナーでアプリケーションを実行する場合、最も簡単なロギング方法は、標準出力に書き込むことです。 、、( write またはプログラミング言語で同等のもの) だけで echo print 、コンテナエンジンがアプリケーションの出力をキャプチャします。

もちろん、他のアプローチも可能です。例えば:

最後のシナリオでは、このサービスは次のようになります。

  • クラウドプロバイダーが運用する独自のロギングメカニズム。 AWS CloudWatch または Google Stackdriver
  • ログまたはイベントの管理を専門とするサードパーティによって提供されます。 ハニカムログリースプランクなど。
  • 社内で実行され、自分で展開して維持するもの。

アプリケーションが非常に簡潔な場合、またはトラフィックがほとんどない場合(あなたとあなたの犬を含む3人のユーザーがいるため)、ロギングサービスを社内で実行できます。 私の オーケストレーションワークショップにはロギング に関する章もあり、独自の ELKクラスターを実行することはすべてユニコーンとレインボーであるという誤った考えを与える可能性がありますが、 真実は非常に異なり信頼性の高いロギングシステムを大規模に実行することは困難です

したがって、半構造化データのリアルタイムの保存、インデックス作成、およびクエリに伴う複雑さ(および苦痛)に対処する 他の誰かにログを送信する可能性を確実に望んでいます。 これらの人々はあなたのログを管理する以上のことができることは言及する価値があります。 Sentryのような一部のシステムは、エラーから洞察を抽出するのに特に適しています(トレースバック解剖を考えてください)。Honeycombのような多くの最新のツールは、ログだけでなくあらゆる種類のイベントも処理し、すべてをクロスマッチさせて、午前3時の厄介な停止の実際の原因を見つけることができます。

しかし、そこにたどり着く前に、実装が簡単で無料のもの(可能な限り)から始めたいと思います。

そこで便利なのがコンテナロギングです。 ログをstdoutに書き込むだけで、コンテナエンジンにすべての作業を任せてください。 最初は、単純な退屈なファイルを書き込みます。しかし、後で、アプリケーションコードを変更することなく、ログをよりスマートに処理するように再構成できます。

ここで説明するアイデアとツールは、使用している場合と使用していない場合があるオーケストレーションプラットフォーム(Kubernetes、Mesos、Rancher、Swarmなど)と直交していることに注意してください。それらはすべてDockerエンジンのロギングドライバーを活用できるので、私はあなたをカバーします!

デフォルトのロギングドライバ: json-file

既定では、Docker エンジンはすべてのコンテナーの標準出力 (および標準エラー) をキャプチャし、JSON 形式を使用してファイルに書き込みます (したがって、この既定のログ ドライバーの名前json-fileです)。JSON 形式では、各行にその起点 (stdout または stderr) とタイムスタンプが注釈され、各コンテナー ログが個別のファイルに保持されます。

コマンド (または同等の API エンドポイント) を使用するとdocker logDocker Engine はこれらのファイルから読み取り、コンテナーによって出力されたものを表示します。今のところ大丈夫です。

json-file ただし、ドライバーには(少なくとも)2つの問題点があります。

  • 既定では、ログ ファイルはディスク領域がなくなるまで無制限に大きくなります。
  • 「HTTPステータスコードが200/OK250ミリ秒の場合にのみ、応答時間が250ミリ秒を超える午前2時から午前7時までの仮想ホストapi.container.churchのすべてのHTTP要求を表示する」などの複雑なクエリを実行することはできません

最初の問題は、Dockerのドライバーに json-file いくつかの追加のパラメーター を指定して ログローテーションを有効にすることで簡単に修正できます。ただし、2番目のサービスには、私がほのめかしていたこれらの派手なログサービスの1つが必要です。

クエリがそれほど複雑でなくても、ログを何らかの方法で一元化して、次のようにする必要があります。

  • コンテナを実行しているクラウドインスタンスが消えても、ログは永久に失われません。
  • 少なくとも、Docker APIを介して完全にダンプしたり、SSHでやり取りしたりすることなく、複数のコンテナのログをgrepできます。

Aparté:私がまだポケットベルを持ち歩いていて、dotCloudプラットフォームの世話をしていたとき、私たちが好んだログ分析手法は「Ops Map/Reduce」と呼ばれ、ファブリック、並列SSH接続、grep、およびその他のいくつかの小物が含まれていました。 私たちの時代遅れの技術を笑う前に、6人のエンジニアのチームが5年前に100000個のコンテナのログファイルをどのように処理したかを尋ね、お茶、ビール、またはその他の適切な飲み物のマグカップの周りで私たちの戦いの傷跡とPTSD関連の治療費を比較してみましょう。 ♥

以遠 json-ファイル

さて、デフォルトのjson-file ドライバーで開発(および展開)を開始できますが、ある時点で、コンテナーによって生成されるログの量に対処するために何か他のものが必要になります。

そこで便利なのがロギング ドライバーで、アプリケーションのコードを 1 行も変更せずに、忠実なコンテナー エンジンにログを別の場所に送信するように依頼できます。 さっぱり。

Docker は、 以下を含むがこれらに限定されない、 他の多くのログ ドライバー をサポートしています。

  • アウログス、Amazonのクラウドで実行していて、他のものに移行する予定がない場合。
  • gcplogs、あなたがグーグルの人なら。
  • シスログ、すでに一元化されたsyslogサーバーがあり、それをコンテナに活用したい場合。
  • ゲルフ

GELFには、特に面白くて用途の広いものにするいくつかの機能があるため、ここでリストを停止します。

ゲルフ

GELF は グレイログ 拡張ログ形式 を表します。 当初は グレイログロギングシステム用に設計されました。 これまでGraylogについて聞いたことがない場合は、ELKのような「最新の」ロギングシステムを開拓したオープンソースプロジェクトです。実際、DockerログをELKクラスターに送信する場合は、おそらくGELFプロトコルを使用します。これは、多くのロギングシステム(オープンまたはプロプライエタリ)によって実装されているオープンスタンダードです。

GELFプロトコルの何がそんなにいいのですか? これは、syslogプロトコルの欠点のいくつか(ほとんどではないにしても)に対処します。

syslogプロトコルでは、ログメッセージはほとんど生の文字列であり、メタデータはほとんどありません。 syslog エミッタとレシーバの間には何らかの合意があります。 有効な syslog メッセージは、次の情報を抽出できるように、特定の方法でフォーマットする必要があります。

  • 優先度:これはデバッグメッセージ、警告、純粋に情報提供、重大なエラーなどです。
  • 物事がいつ起こったかを示す タイムスタンプ
  • 物事が起こった場所(つまり、どのマシンで)が起こったかを示す ホスト名
  • メッセージがメールシステムやカーネルなどから送信されたかどうかを示す 機能
  • プロセス名と番号。
  • 等。

そのプロトコルは80年代(さらには90年代)には素晴らしかったですが、いくつかの欠点があります。

  • 時間の経過とともに進化するにつれて、さまざまなユースケースに指定、拡張、および改造するための約10の異なるRFCがあります。
  • メッセージサイズは制限されており、非常に長いメッセージ(トレースバックなど)を切り捨てるか、メッセージ間で分割する必要があります。
  • 結局のところ、一部のメタデータを抽出できたとしても、ペイロードはプレーンで装飾されていないテキスト文字列です。

GELFは非常に 危険な動きをし、 すべてのログメッセージを辞書(またはマップやハッシュなど、あなたがそれらを呼びたいもの)にすることを決定しました。この「dict」には次のフィールドがあります。

  • バージョン;
  • ホスト(最初にメッセージを送信した人)。
  • タイムスタンプ;
  • メッセージの短いバージョンと長いバージョン。
  • あなたが望む余分なフィールド!

最初は「OK、どうしたの?」と思うかもしれませんが、これは、Webサーバーがリクエストをログに記録するときに、次のような生の文字列を持つ代わりに、

127.0.0.1 - フランク [10/Oct/2000:13:55:36 -0700] "GET /apache_pb.gif HTTP/1.0" 200 2326

 

あなたはそのような口述を得る:

{
  "クライアント": "127.0.0.1",
  "ユーザー": "フランク",
  "タイムスタンプ": "2000-10-10 13:55:36 -0700",
  "メソッド": "取得",
  "uri": "/apache_pb.gif","プロトコル": "HTTP/1.0","ステータス": 200,
  "サイズ": 2326
}

 

これは、ログが生の文字列ではなく構造化オブジェクトとして保存されることも意味します。 その結果、穴居人のようにgrepで正規表現を彫る代わりに、エラボレートクエリ(SQLに近いもの)を作成できます。

さて、GELFはDockerが発行できる便利な形式であり、GraylogLogstashFluentdなどの多くのツールで理解されています

さらに、デフォルト json-file からGELFに簡単に切り替えることができます;つまり、( つまり、Dockerクラスターに何もセットアップしない)から始めて、後でこれらのログエントリが最終的に役立つ可能性があると判断したら、アプリケーションの何も変更せずにGELFに切り替え、ログを自動的に一元化し、どこかにインデックスを付けます json-file 。

ロギングドライバの使用

GELF(またはその他の形式)に切り替えるにはどうすればよいですか?

Dockerには、そのための2つのコマンドラインフラグが用意されています。

  • --ログドライバ 使用するドライバーを示します。
  • --ログオプト 任意のオプションをドライバーに渡します。

これらのオプションは、 に渡すことができ、 この 1 つの特定のコンテナ で 異なるロギング・メカニズムを使用すること、または Docker Engine 自体 (起動時に) に渡して、すべてのコンテナのデフォルト・オプションにすることができます。 docker run

(Docker API を使用してコンテナーを起動している場合、これらのオプションは構造内で呼び出しに渡され createます HostConfig.LogConfig)。

「任意のオプション」はドライバーごとに異なります。 GELFドライバの場合、たくさんのオプションを指定できますが、必須のオプションがあります:GELFレシーバーのアドレス。

デフォルトのUDPポート12201のマシン1.2.3.4にGELFレシーバーがある場合は、次のようにコンテナを起動できます。

ドッカー実行\
–log-driver gelf –log-opt gelf-address=udp://1.2.3.4:12201 \
アルパインエコー ハローワールド

次のことが起こります。

  • Dockerエンジンはイメージをプルします alpine(必要な場合)
  • Dockerエンジンがコンテナを作成して起動します
  • コンテナは引数を指定してコマンドを実行 echoします hello world
  • コンテナ内のプロセスは 標準出力に 書き込み hello world ます
  • hello world メッセージは、見ている人(つまり、フォアグラウンドでコンテナを起動したので)に渡されます
  • メッセージは Docker によってもキャッチされ、 hello world ログ ドライバーに送信されます
  • ロギングドライバはgelf、ホスト名、タイムスタンプ、文字列hello worldを含む完全なGELFメッセージだけでなく、完全なID、名前、イメージ名とID、環境変数など、コンテナに関する一連の情報を準備します。
  • この GELF メッセージは、UDP 経由でポート 12201 の 1.2.3.4 に送信されます。

次に、うまくいけば1.2.3.4がUDPパケットを受信し、それを処理し、メッセージを永続的なインデックス付きストアに書き込み、取得またはクエリできるようにします。

願わくは。

私はあなたにUDPを教えます 冗談だがしかし

あなたがオンコールや他の人のコードを担当したことがある場合は、おそらく今ではうんざりしているでしょう。 私たちの貴重なロギングメッセージは、ロギングサーバーに到着する場合と到着しない場合があるUDPパケット内にあります(UDPには送信保証はありません)。 ロギングサーバーがなくなった場合(「ひどくクラッシュする」のいい言葉)、パケットが到着する可能性がありますが、メッセージは気付かずに無視され、何もわかりません。 (技術的には、ホストまたはポートに到達できないというICMPメッセージを受け取る場合がありますが、その時点では、これがどのメッセージに関するものであるかさえわからないため、手遅れになります!

おそらく、いくつかのドロップされたメッセージ(または、ロギングサーバーが再起動されている場合は束)で生きることができます。 しかし、私たちがクラウドに住んでいて、サーバーが蒸発した場合はどうなりますか? しかし、真剣に:ログメッセージをEC2インスタンスに送信していて、何らかの理由でそのインスタンスを別のインスタンスに置き換える必要がある場合はどうなりますか? 新しいインスタンスのIPアドレスは異なりますが、ログメッセージは頑固に古いアドレスに移動し続けます。

救助へのDNS

揮発性IPアドレスを回避する簡単な方法は、DNSを使用することです。 GELF ターゲットとして指定する 1.2.3.4代わりに、 を使用し gelf.container.church、これが 1.2.3.4 を指していることを確認します。そうすれば、別のマシンにメッセージを送信する必要があるときはいつでも、DNSレコードを更新するだけで、Dockerエンジンは新しいマシンにメッセージを喜んで送信します。

それとも?

リモートマシンにデータを送信するコード(たとえば、ポート12345のgelf.container.church)を記述する必要がある場合、最も単純なバージョンは次のようになります。

  1. IP アドレス (A.B.C.D) に 解決 gelf.container.church します
  2. ソケットを作成します。
  3. このソケットをポート 12345 の A.B.C.D に接続します。
  4. ソケットでデータを送信します。

データを複数回送信する必要がある場合は、利便性と効率の両方の理由から、ソケットを開いたままにします。 データを送信する前に、TCP接続を確立するために「3ウェイハンドシェイク」を実行する必要があるため、これはTCPソケットで特に重要です。言い換えれば、上記のリストの3番目のステップは非常に高価です(少量のデータパケットを送信するコストと比較して)。

UDPソケットの場合、「ああ、データを送信する前に3ウェイハンドシェイクを行う必要がないので(上記のリストの3番目のステップは基本的に無料です)、メッセージを送信する必要があるたびに4つのステップすべてを実行できます!」と考えたくなるかもしれません。 しかし実際には、そうすると、最初のステップであるDNS解決に困惑していることにすぐに気付くでしょう。 DNS解決はTCP 3ウェイハンドシェイクよりも安価ですが、DNSリゾルバーへのラウンドトリップが必要です。

アパルテ:はい、非常に効率的なローカルDNSリゾルバーを持つことは可能です。 pdns-recursorやdnsmasqのようなものが実行されています ローカルホスト キャッシュされたクエリのDNS応答時間が短くなります。 ただし、ログメッセージを送信する必要があるたびにDNSリクエストを行う必要がある場合は、すべてのログ行が1つのシステムコールだけでなく3つのシステムコールを生成するため、アプリケーションに間接的ではあるがかなりのコストがかかります。 くそったれ! また、一部の人々(EC2で実行しているほとんどすべての人など)は、クラウドプロバイダーのDNSサービスを使用しています。 これらのユーザーは、ログ行ごとに 2 つの追加のネットワーク パケットが発生します。 また、クラウドプロバイダーのDNSがダウンすると、ロギングが壊れます。 かっこよくない。

結論:UDP経由でログオンする場合、メッセージを送信するたびにロギングサーバーアドレスを解決することは望ましくありません。

うーん...では、TCPは救助に?

TCP接続を使用し、必要な限り維持することは理にかなっています。 ロギングサーバーに何か恐ろしいことが起こった場合、TCPステートマシンが最終的に(タイムアウトなどのために)それを検出し、通知することを信頼できます。 その場合は、サーバー名を再解決して再接続できます。 コンテナエンジンに少し余分なロジックが必要なのは、ソケット上で「 壊れたパイプ」または平易な英語で「もう一方の端がもう私たちに注意を払っていない 」というエラーを引き起こす EPIPE という不幸なシナリオ write に対処するためです。

TCPを使用してGELFサーバーと通信すると、問題は解決しますよね?

右。

残念ながら、Docker の GELF ロギング ドライバーは UDP のみをサポートしています。

(╯°□°)╯︵ ┻━┻

この時点で、あなたがまだ私たちと一緒にいるなら、コンピューティングは単なる特殊な種類の地獄であり、コンテナは反キリストであり、Dockerは変装した運命の前兆であると結論付けたかもしれません。

急いで結論を出す前に、コードを見てみましょう。

GELF ドライバーを使用してコンテナーを作成すると、 この関数が呼び出され、次の方法で新しい gelfWriter オブジェクトが作成されます。 天職 ゲルフ。ニューライター.

次に、コンテナーが何かを出力すると、最終的に GELF ドライバーの Log 関数が呼び出されます。 基本的に は、メッセージを gelfWriter に書き込みます

この GELF ライター オブジェクトは、外部依存関係 github.com/Graylog2/go-gelf によって実装 されます

ほら、私はそれが来るのを見ます、彼はいくつかの厄介な指差しをして、他の誰かのコードに責任を負わせるつもりです。 卑劣!

ホットポテト

このパッケージ、特に NewWriter 関数、Write メソッド、および後者によって呼び出されるその他のメソッド (WriteMessagewriteChunked) を調べてみましょう。Goにあまり精通していなくても、これらの関数はいかなる種類の再接続ロジックも実装していないことがわかります。何か悪いことが起こった場合、エラーは呼び出し元にバブルアップし、それだけです。

Docker側のコード(前のセクションのリンクを使用)で同じ調査を実行すると、同じ結論に達します。 ログメッセージの送信中にエラーが発生した場合、エラーは上のレイヤーに渡されます。 Dockerのコードでもgo-gelfのコードでも、再接続の試みはありません。

ちなみに、これは、DockerがUDPトランスポートのみをサポートする理由を説明しています。 TCP をサポートする場合は、UDP よりも多くのエラー条件をサポートする必要があります。 別の言い方をすれば、TCPサポートはより複雑でコード行数が多くなります。

憎しみは憎む

考えられる反応の1つは、go-gelfを実装した勇敢な魂、またはDockerにGELFドライバーを実装した魂に腹を立てることです。 別のより良い反応は、コードをまったく使用しないのではなく、彼らがそのコードを書いたことに感謝することです!

回避策

ロギングの問題を解決する方法を見てみましょう。

最も簡単な解決策は、「再接続」(技術的には解決して再接続)が必要なときはいつでもコンテナを再起動することです。 それは動作しますが、それは非常に迷惑です。

少し良い解決策は、 に127.0.0.1:12201ログを送信し、パケットリダイレクタを実行して、これらのパケットを実際のロガーに「バウンス」または「ミラーリング」することです。

socat UDP-LISTEN:12201 UDP:gelf.container.Church:12201

これは、各コンテナー ホストで実行する必要があります。 これは非常に軽量であり、更新されるたびに gelf.container.church 、コンテナを再起動する代わりに、 単に再起動 socat します。

(ログパケットを仮想IPに送信してから、いくつかの派手iptables -t nat ... -j DNATな使用 この仮想IPに向かうパケットの宛先アドレスを書き換えるルール。

別のオプションは、(だけでなくsocat)各ノードでLogstashを実行することです。最初はやり過ぎに思えるかもしれませんが、ログに多くの柔軟性が与えられます:ローカル解析、フィルタリング、さらには「フォーク」、つまりログを複数の場所に同時に送信することを決定することができます。これは、あるロギングシステムから別のロギングシステムに切り替える場合に、しばらくの間(移行期間中に)両方のシステムに並行してフィードできるため、特に便利です。

各ノードでLogstash(または別のロギングツール)を実行することは、キューを挿入するのに最適な場所であるため、ログメッセージが失われないようにする場合にも非常に便利です(単純なシナリオには Redis を使用し、 より厳しい要件がある場合は Kafka を使用します)。

別のプロトコルを使用してログをサービスに送信することになったとしても、GELFドライバーはおそらくDockerを接続するためにセットアップするのが最も簡単なドライバーです。 Logstash または Fluentd を使用してから、Logstash または Fluentd に他のプロトコルを使用してロギングサービスと通信させます。

UDPソケットがバッファスペースを使い果たした 場合を除き 、送信されたUDP localhost パケットが失われることはありません 。これは、送信者(Docker)が受信者(Logstash / Fluentd)よりも高速である場合に発生する可能性があるため、キューについて言及しました:キューは、受信者がオーバーフローを回避するためにUDPバッファをできるだけ早くドレインできるようにします。それを十分な大きさのUDPバッファと組み合わせると、安全になります。

今後の方向性

クラスター全体 socatを実行することが比較的簡単であっても(特にSwarmモードと docker service create --mode global)、箱から出してすぐに良い動作をしたいと考えています。

これに関連するいくつかのGitHubの問題がすでにあります:#23679、# 17904 、および #16330 。メンテナの1人が 会話に加わり、Docker Inc.にはこれが改善されることを望んでいる人がいます。

考えられる解決策の 1 つは、GELF サーバー名を時々再解決し、変更が検出されたらソケット宛先アドレスを更新することです。 DNSはTTL情報を提供するため、IPアドレスをキャッシュできる期間を知るためにも使用できます。

より良いGELFサポートが必要な場合は、良いニュースがあります。 「プルリクエストを送ってください、ハッハッハ!」と言うつもりはありません なぜなら、それを行うための時間と専門知識の両方を持っている人はごくわずかであることを知っているからです—しかし、あなたがその一人なら、ぜひそれをしてください! ただし、他にも役立つ方法があります。

まず、上記の GitHub の問題 (#23679 および #17904) を監視できます。コントリビューターとメンテナがフィードバックを求めた場合は、何があなたのために働くか(またはうまくいかないか)を示してください。意味のある提案が表示され、「+1」と言いたい場合は、GitHubの反応でそれを行うことができます(「親指を立てる」絵文字はそれに最適です)。そして、誰かがプルリクエストを提案した場合、それをテストすることは、それを受け入れるために非常に役立ち、興味をそそります。

これらのGitHubの問題の1つを見ると、ずっと前にすでにパッチが提案されていることがわかります。しかし、そもそも機能を求めた人はそれをテストしたことがなく、その結果、マージされることはありませんでした。 誤解しないでください、私はその人に責任を負わせていません! GitHubの問題を、機能を必要とする人々とそれを実装できる人々のための一種の「ミーティングポイント」として持つことは良いスタートです。

GELFドライバがTCP接続をサポートしたり、UDPアドレスのアドレスを正しく再解決したりするため、数か月以内にこの投稿の半分が廃止される可能性が非常に高いです。