OSSにコントリビュートしてログ収集基盤におけるCloud Pub/Subのリージョン間通信費用を削減した話

ogp

こんにちはSRE部の川津です。ZOZOTOWNにおけるログ収集基盤の開発を進めています。開発を進めていく中でCloud Pub/Subのリージョン間費用を削減できる部分が見つかりました。

今回、OSSであるfluent-plugin-gcloud-pubsub-customにコントリビュートした結果、Cloud Pub/Subのリージョン間費用を削減できました。その事例を、ログ収集基盤開発の経緯と実装要件を踏まえて紹介します。

目次

ログ収集基盤の紹介

はじめに、ZOZOテクノロジーズにおけるログ収集基盤を紹介します。

開発経緯

ZOZOテクノロジーズではZOZOTOWNにおけるログを収集する基盤に課題があるため、新たな基盤の開発を進めています。

現在のログ収集は、サイト利用者のアクセスパターン等の分析をするために、Google Analyticsを利用しています。収集したデータはBigQuery Exportを介してBigQueryに保存されます。

しかし、Google Analyticsを利用する際、下記3点の問題がありました。

  • フロントエンドのログしか取得できない
  • BigQuery ExportはSLAを担保されていない
  • リアルタイムにログを保存できない

フロントエンドのログしか取得できない

Google Analyticsはフロントエンドのログしか取得できません。そのため、バックエンド側のログが必要な場合でもGoogle Analyticsでは検知されず収集できません。

バックエンド側のログをGoogle Analyticsで利用するためには、一度フロントエンド側にバックエンド側のログを送信する必要があります。

BigQuery ExportはSLAを担保されていない

現在、Google Analyticsで収集したログを利用する際はBigQuery Exportの機能を使いBigQueryへログを転送しています。しかし、下記の資料からGoogle AnalyticsのBigQuery Exportでは、SLAが担保されていないことがわかります。そのため、BigQuery Exportの機能に障害が発生した場合、復旧時間は保証されません。実際にBigQuery Exportで障害が発生し数時間分のログが欠損したケースが過去にありました。 support.google.com

リアルタイムにログを保存できない

分析の結果は推薦や検索へ用いるため、ログをリアルタイムに利用したい要望があります。

Google AnalyticsはBigQueryのStreaming Exportを利用できます。しかし、内部ではBigQuery Exportを利用しています。そのため、BigQuery ExportはSLAが担保されておらず、障害が発生する場合を想定すると可用性に欠けます。

以上の問題を解消するため、バックエンドを含めZOZOTOWNにおいて発生するログを集約し、リアルタイムにログを収集できる基盤を構築することにしました。

実装要件

ログ収集基盤を構築するにあたり、以下の4つの要件があります。

  • ログ送信側の環境に依存しない共通の仕組みで実装する
  • 転送されるログの量に応じてオートスケールする構成にする
  • 送られてくるログをロストしない
  • リアルタイムにログが保存される

各要件について説明します。

ログ送信側の環境に依存しない共通の仕組みで実装する

ログを送信するサーバは様々な環境で動いています。例えばAWSやGCP、オンプレミス環境です。また環境によってOSや使用しているアプリケーションのバージョンや言語も異なるので、様々な環境で同じ挙動をする共通の仕組みが必要です。

転送されるログの量に応じてオートスケールする構成にする

ZOZOTOWNのアクセス数は時期や時間帯によって大きく変化します。日中の時間帯はアクセス数が多いのに対し、深夜の時間帯のアクセス数は多くありません。また年末年始のセールや人気ブランド商品の発売等、急激にアクセス数が伸びる場合もあります。アクセスの増減に対してオートスケールしない構成の場合、料金が余分に発生してしまったり、収集するログを捌ききれず遅延が発生してしまいます。

このような問題を防ぐために、アクセス数が少ない場合はサーバ台数を自動的に減らし、逆にアクセス数が多い場合はサーバを自動的に増やすような構成にします。

送られてくるログをロストしない

ログがロストする状況を考えると、ネットワークの影響でデータの送信が止まった際が考えられます。そのため、各インフラリソースを利用する際は、バッファリングを行いデータのロストを防ぐ必要があります。

リアルタイムにログが保存される

ログ収集基盤で取り扱うログは、アイテムの検索や推薦で利用されることが想定されます。

検索や推薦で求められるリアルタイム性は最短で1分、最長で60分です。今回ログ収集基盤に送られるログのメッセージ数はおよそ20,000msg/sです。よって送られてくる20,000msg/sのデータを60分以内に保存できる仕組みが必要です。

インフラ構成

ログ収集基盤のインフラ構成は下記の図の通りです。

architechture

各リソースを採用した理由を順に説明します。

Fluentd

Fluentdの利点は以下の点です。

  • バッファリングができる
  • アプリケーションの実装環境に依らず利用できる

バッファリングができる

Fluentd側でバッファリングする目的はCloud Pub/Subに障害が起こった際や、ネットワークの影響でCloud Pub/Subへログが送れなかった際にロストする問題を防ぐためです。Cloud Pub/Subの可用性は月間99.95%以上です。詳細についてはGoogleのPub/Sub Service Level Agreementに記載されています。

このSLAを基にダウンタイムを考えるとおよそ20分です。20分間のダウンタイムが発生した場合、バッファリング無しだとその20分間のログデータがロストしてしまいます。このロストは許容できないため、ログの送信側であるFluentdでバッファリングを行う必要があります。

アプリケーションの実装環境に依らず利用できる

Fluentdはアプリケーションとして独立しており、多様な環境で利用可能です。そのため、ログを出力するアプリケーション側の言語に依らず導入できます。アプリケーション側はログをFluentd側に送るだけで、FluentdがCloud Pub/Subへ送信してくれます。

Cloud Pub/Sub

Cloud Pub/Subの利点は以下の点です。

  • バッファリングを行ってくれる
  • 自動でスケーリングされる
  • ログの送信側と受信側を疎結合にできる

バッファリングを行ってくれる

Cloud Pub/Subでバッファリングする目的はログのロストを防ぐためです。

Cloud Pub/Subのバッファリングの性能は、リソースの上限を確認すると下記のように記載されています。

Retains unacknowledged messages in persistent storage for 7 days from the moment of publication. There is no limit on the number of retained messages. If subscribers don't use a subscription, the subscription expires. The default expiration period is 31 days.

確認応答されていないメッセージは7日間保存されます。そのため、Subscriber側のDataflowに障害が起こった場合でも7日間復旧の猶予ができます。また、保存されるメッセージ数に上限はないので大量のログによるバッファあふれでログをロストする心配もありません。

自動でスケーリングされる

ログの受け口がオートスケール可能であることは、実装要件で挙げたようなセールや時間帯等によるログの送信数増減でリソースの枯渇と余剰を防ぐために必要です。Cloud Pub/Subでは、Cloud Pub/Sub側が定義した「負荷」によってリソースが可変します。詳しくはGoogleの公式ドキュメントのスケーラビリティに記載されています。

オートスケールの上限はCloud Pub/Subの割り当て上限を確認すると、大規模リージョンと小規模リージョンによって上限が異なります。大規模リージョンに該当するリージョンはeurope-west1、us-central1、us-east1で、小規模リージョンはそれ以外の全てのリージョンです。

大規模リージョンの場合、Publisherのスループットは下記のように記載されています。

12,000,000 kB per minute (200 MB/s) in large regions

Cloud Pub/SubへのPublishに関しては1リクエスト最大10MBで、1リクエスト1,000メッセージまでまとめて送れます。

大規模リージョンにおける最大メッセージ数は上記に記載されている通り200MB/sです。今回想定しているログのメッセージサイズは1メッセージ1kBなので200,000kB/sは200,000msg/sです。想定されるメッセージ数は20,000msg/sなので、上限に引っかかることはありません。

一方、小規模リージョンの場合、Publisherのスループットは下記のように記載されています。

3,000,000 kB per minute (50 MB/s) in small regions

上限が50MBなのでおよそ50,000msg/sを送ることが可能です。

小規模リージョンと大規模リージョンを比べると、大規模リージョンの方がより多くのメッセージを捌けることが明らかです。セールによってメッセージ数が数倍に増加する場合を考慮すると、今回は大規模リージョンが適していると言えます。

ログの送信側と受信側を疎結合にできる

ログの送信側をステートレスにするため、Cloud Pub/Subを導入しログの送信側と受信側を疎結合にします。

もしCloud Pub/Subを利用しない場合は、FluentdからBigQueryへ直接Insertする方法があります。BigQueryのStreaming Insertを利用することで、リアルタイムにInsertできます。この構成にするとバッファリングはBigQuery側で行うことができないので、Fluentd側のみでバッファリングを行う必要があります。

ところが、Fluentd側でバッファリングを行った場合、アプリケーションのデプロイタイミングが難しくなります。バッファリングを行っている最中はログのデータを保持していますが、下記ドキュメントによると、Fluentdのバッファリングはメモリに保存する方法とファイルへ保存する方法があると書かれています。基本的にファイルへ保存する方法を利用するので、デプロイのタイミングでログがロストする心配はありません。しかし、デプロイのタイミングにはFluentd自体のログの送信が停止してしまいます。そのため、Fluentd側でバッファリングを行うとデプロイの最中にログ送信が止まってしまいます。影響範囲を少なくするために利用者が少ない時間帯にデプロイする必要が出てきます。

このような制約により、デプロイのタイミングを制限されてしまうと、アプリケーション側のリリースに影響が出るのでCloud Pub/Subを利用しています。

docs.fluentd.org

Dataflow

Dataflowの利点は下記の点です。

  • Streming形式でBigQueryにInsertできる
  • 自動でスケーリングされる

Streming形式でBigQueryにInsertできる

DataflowではStreaming形式がサポートされているので遅延を抑えてBigQueryにログを保存できます。そのため、実装要件として求められているリアルタイム性を担保できます。

自動でスケーリングされる

Dataflowにおいてスケーリングが必要となる理由は、Cloud Pub/Subでスケーリングが必要な理由と同様です。

Dataflowのオートスケール機能について、公式ドキュメントのオートスケーリング機能を確認すると、ワーカーの負荷やリソースの使用率に応じてワーカーの数は変更されることがわかります。

同様にオートスケールの上限については、Dataflowの割り当て上限を確認すると下記のように記載されています。

Each Dataflow job can use a maximum of 1,000 Compute Engine instances.

上記の記載はありますが、1インスタンスあたりの性能についての記載は見当たらないので、検証を実施しました。

具体的には、Cloud Pub/SubにACK処理がされていないメッセージを溜め込んだ状態にし、Dataflowを起動してBigQueryにInsertを行いました。なお、Dataflowのワーカーは下記のスペックで1台に固定しました。

  • CPU数:4
  • メモリ:15GB
  • ストレージ:430GB

このスペックはDataflowのJobを作成する際にデフォルトで割り当てられるものです。なお、Dataflowで利用できるCPUやメモリの割り当ては、Compute Engine の割り当てに記載のあるCompute Engineのマシンタイプを指定できます。

今回はn1-standard-4のマシンタイプを利用しています。CPUはデータを並列で処理したいので4つ割り当てており、メモリはCPUが4つの場合15GBと決まっているので15GBに設定しています。

Dataflowで扱うテンプレートはGoogleが提供しているPub/Sub Subscription to BigQuery テンプレートを利用しました。

以上の条件でCloud Pub/Subにデータサイズが1kBのメッセージを4,000,000件溜め込んだ状態で検証しました。

その結果、1インスタンスのスループットは約12,000msg/sでした。

今回、Cloud Pub/Subで想定されるメッセージ数は20,000msg/sです。その場合、約20,000msg/sのメッセージがDataflow側で処理されます。Dataflowでは1インスタンスで約12,000msg/s処理できるので、性能に関して問題ないことがわかりました。

よって、セール時に処理するメッセージ量が増加した場合でも処理できます。

ログ収集基盤の問題点

ログ収集基盤にはZOZOTOWNにおけるログが全て集約されます。そのため、Cloud Pub/SubにPublishするメッセージ数とBigQueryにInsertするデータ量も必然的に膨大なものになります。

データ量が大きくなる際に懸念すべき点が、各リソース間で発生するリージョン間通信です。リージョン間通信は現在のリージョンから別のリージョンへ通信が行われる際に発生する費用です。発生する料金はネットワーク料金表に記載されています。

今回のインフラ構成でリージョン間通信が発生するのは以下の通信です。

  • Fluentd → Cloud Pub/Sub → Dataflow
  • Dataflow → BigQuery

なお、「Dataflow → BigQuery」の通信はBigQueryをUSリージョンに配置する都合があるため、次の章で併わせて説明します。

Fluentd → Cloud Pub/Sub → Dataflow

Cloud Pub/Subのリージョン間通信はこちらの料金の説明に記載されています。

The fees for internet egress and message delivery between Google Cloud regions are consistent with the Compute Engine network rates, with the following exceptions:

Cloud Pub/Subのネットワーク料金はCompute Engineのネットワーク料金と同じ料金体系です。そのため、Cloud Pub/SubのメッセージをPublishする側とSubscribeする側のリージョンが別の場合、ネットワーク料金表の料金体系が適用されます。

課金される料金は下記のトラフィックの種類によって分類されています。

network_price 出典:ネットワーキングのすべての料金体系 | VPC | Google Cloud

内部IPかつ同じゾーンの場合は料金が発生せず、外部IPや異なるリージョン間の通信では料金が発生します。

今回利用するリージョンは、BigQueryの配置先が東京リージョンかUSリージョンになるので、必然的に東京リージョンかUSリージョンの2パターンです。料金が発生する条件はCloud Pub/SubのメッセージをPublishする側とSubscribeする側のリージョンが別の場合が条件です。つまり、Fluentd側とDataflow側を東京リージョンかUSリージョンのどちらかに統一しない場合に料金が発生します。例えばDataflow側をUSリージョン、Fluentd側を東京リージョンに配置する場合、下記の大陸間通信のトラフィックで料金が発生します。

network_price_list 出典:ネットワーキングのすべての料金体系 | VPC | Google Cloud

上記の通り$0.08/GBの料金が発生します。

今回仮定されるCloud Pub/Subへのメッセージサイズは、1メッセージ1kBです。Fluentdから送信されるメッセージ数を20,000msg/sと仮定すると1秒間で0.02GB転送されます。月で換算するとおよそ50,000GBのデータが転送されます。つまり、1か月に必要なリージョン間費用は$4,000です。金額換算すると無視できる額ではありません。

解決策

Cloud Pub/Subのリージョン間通信を抑えるために、下記2つの解決策を考えました。

  • Cloud Pub/SubのSubscribeする側とPublishする側を同じゾーンにする
  • FluentdのOutput PluginsでPub/Subのリージョンを指定できるようにする

Cloud Pub/SubのSubscribeする側とPublishする側を同じゾーンにする

同じゾーンにリソースを配置する際の選択肢は、Fluentdを動かすサーバとDataflowを動かすサーバをUSリージョンに置くパターンと東京リージョンに置くパターンです。それぞれのパターンを考えてみます。

USリージョンに揃えた場合

us_region

Cloud Pub/SubのSubscribeする側とPublishする側をUSリージョンに揃えた場合、リージョン間の料金は発生しません。

しかし、ZOZOTOWNの利用者は日本国内からのアクセスがほとんどです。そのため、USリージョンにインスタンスを配置すると大陸間の通信が発生します。その結果、東京リージョンへ配置する場合に比べ、距離的な問題で遅延が増加します。

下記の記事によると、アメリカ西海岸までの通信ではおよそ100msの往復遅延が発生します。

xtech.nikkei.com

つまり、Fluentd側をUSリージョンに配置する場合、ログを送信するアプリケーションもUSリージョンに配置する必要があり、アプリケーション側で100msの遅延が発生します。100msの遅延はユーザ体験に影響が出るレベルであるため、この遅延は防ぐべきです。

東京リージョンに揃えた場合

tokyo_region

東京リージョンに揃えた場合、前述のような距離に起因する大きな遅延は発生しません。

しかし、リージョン間の料金はBigQueryの配置が東京リージョンかUSリージョンかによって変わります。このBigQueryの配置先の選択肢を比較してみます。

BigQueryを東京リージョンに配置した場合

bq_tokyo_region

BigQueryを東京リージョンに配置する場合のメリットは、リージョン間通信が発生しないことです。

デメリットは、BigQueryの利用料金の単価がUSリージョンよりも高くなる点です。BigQueryの料金表で東京リージョンとUSリージョンを比較すると下記の金額差があります。

オンデマンド分析の料金(USD/TB) ストレージの料金(USD/GB)
東京 6.00 0.023
US 5.00 0.020

オンデマンド分析の料金やストレージの料金を比較するとUSリージョンの方が安いです。

BigQueryをUSリージョンに配置した場合

bq_us_region

BigQueryをUSリージョンに配置するメリットは2つあります。

  • BigQueryの利用料金の単価が安い
  • BigQueryの新機能を早期に使える

BigQueryの新機能はUSリージョンから順にリリースされることが多いです。そのため、USリージョンに配置することで新機能の早期利用が可能です。

デメリットは、BigQueryをUSリージョンに配置した場合は「Dataflow → BigQuery」間でリージョン間通信が発生することです。発生するリージョン間費用は「東京 → US」間の通信なのでCloud Pub/Subのリージョン間費用と同等の料金が発生します。

その結果、BigQueryをUSリージョン、BigQuery以外のリソースを東京リージョンに揃えた場合は料金を削減できません。

以上の結果から、BigQueryの利用単価が安い点と新機能が早期に利用できる点でUSリージョンへ配置することにしました。

FluentdのOutput PluginsでPub/Subのリージョンを指定できるようにする

fluentd_region

Cloud Pub/Subのリージョン間費用をなくす方法として、Cloud Pub/SubにメッセージをPublishする場合にendpointを指定する方法があります。

下記のドキュメントより、Cloud Pub/Subのサービス自体はグローバルなサービスですが、リージョン毎にendpointが用意されていることがわかります。endpointを指定しない場合はglobal endpoint https://pubsub.googleapis.com へリクエストが送られます。このglobal endpointにリクエストが送られると、Cloud Pub/Sub側が自動的にリクエストを送った場所の近くのリージョンのendpointへルーティングします。その仕様を回避させるために、Cloud Pub/Subのメッセージを受け取る側のリージョンと同じリージョンのendpointを直接指定することでリージョン間の料金を発生させなくできます。

ただし、FluentdをGCP内のリソースに構築しendpointを指定した場合は別途料金が発生します。

今回利用するインフラ構成のFluentdを東京リージョンのインスタンス上に構築した場合を考えてみます。この状態でCloud Pub/Subのendpointを指定した場合、指定したendpointのリージョンへPublishされます。ここで「東京 → US」間の通信費用が発生します。このリージョン間の通信費用もCloud Pub/Subのリージョン間通信と同等の料金が発生します。そのため、endpointを指定してCloud Pub/Subのリージョン間費用をなくす方法はGCP内のネットワーク外からCloud Pub/SubへPublishする場合のみ有効です。

cloud.google.com

今回、FluentdでCloud Pub/SubにPublishする部分はfluent-plugin-gcloud-pubsub-customを利用することにしました。しかし、このプラグインではCloud Pub/Subへログを送る際にパラメーターでendpointを指定できませんでした。

つまり、このプラグインでパラメータによりログを送信する際のendpointを指定できるようになれば、Cloud Pub/Subのリージョン間費用をなくすことができると言えます。

以上の検討結果より、OSSとして公開されているFluentdのプラグインであるfluent-plugin-gcloud-pubsub-customを修正することにしました。

OSSへのコントリビュート内容

実際に改修を加え、OSSへコントリビュートしていきます。

まず、fluent-plugin-gcloud-pubsub-customを利用する際に、Fluentdのconfigに対してパラメータでendpointを指定できるようにします。次に、内部で利用されているRubyのCloud Pub/Sub ClientからPublishする際にも、endpointを指定してPublishできるようにします。

なお、Cloud Pub/Subのドキュメントからnewでオブジェクトを生成する際にendpointを引数で渡すことが可能です。

そのため、Fluentdでendpointのパラメータを定義した後、newの引数に定義したendpointを渡すことで実現できます。

実際に改修を加えたPull Requestは下記の内容です。

github.com

このPull Requestの内容を簡単に説明します。

Configuration Parameter Typesにあるconfig_paramを利用することでFluentdのconfig内で扱えるパラメータを定義できます。これを利用し、今回は下記のように定義しました。

ruby config_param :endpoint, :string, :default => nil

config_paramのData TypeはStringなので、定義した値がインスタンス変数のendpointに格納されます。

あとはGoogle::Cloud::Pubsub.newをしている部分にendpointのパラメータを渡すだけです。Google::Cloud::Pubsub.newをしている部分が下記のinitializeメソッドの部分なので、このClassを利用している部分に先程定義したインスタンス変数を渡すように修正しました。

github.com

以上の修正でFluentdからCloud Pub/SubへPublishする際にendpointを指定できるようになりました。

効果検証

Pull Reqestがmasterにマージされ、実際にリージョン間の費用が抑えられているかの検証を実施しました。

検証として2MBのメッセージをローカルで立てたFluentdから、Cloud Pub/Subへ2,048回送信しました。つまり、合計4GBのデータがCloud Pub/Subへ送信されます。

endpointを指定しない場合はリージョン間通信が発生するので1GBにつき$0.08発生します。合計で約$0.32の料金が課金されます。

一方、endpointを指定する場合はリージョン間通信が発生せず、同一リージョン間で通信が行われるのでネットワーク料金表に記載のある下記の料金体系が適用されます。

non_exist_network_price_list 出典:ネットワーキングのすべての料金体系 | VPC | Google Cloud

上記の記載から無料であることがわかります。

インフラ構成は前述のログ収集基盤と同じ構成で構築し、Fluentdのendpointを指定する場合と指定しない場合のリージョン間の費用を確認しました。

まず、endpointを指定しない場合の結果は以下の通りです。

exist_cost

次にendpointを指定する場合の結果は以下の通りです。

non_exist_cost

上記の結果より、endpointを指定しない場合の料金はInter-region data delivery from Asia Pacific to North Americaの部分で料金が発生していることがわかります。価格はリージョン間費用が$0.08/GBなので、4GB送信されているので合計$0.32です。

一方、endpointを指定する場合はリージョン間の通信は発生しないのでInter-region data delivery from Asia Pacific to North Americaの部分の料金は発生していません。代わりにIntra-region data deliveryに対して送信したデータ量が記載されています。こちらの項目は同一リージョン間の通信に発生する項目です。料金は$0なので4GBのデータ送信は特に料金が発生しません。

以上の結果より、Pull Requestで修正した内容により、コスト削減が実現されていることを確認できました。

まとめ

FluentdのPluginであるfluent-plugin-gcloud-pubsub-customにendpointを追加で指定できるようにOSSを修正しました。また、実際に修正した機能を使ってリージョン間費用が発生しないことも確認できました。

その結果、プロジェクトにかかる費用を大きく抑えることができました。より低コストなログ収集基盤を提供できます。

OSSへのコントリビュートは初めてだったので良い経験になりました。OSSのコードを読むという点でも勉強になったので、今後も積極的にOSSへコントリビュートを行っていきたいと思います。

最後に

ZOZOテクノロジーズではより良いサービスを提供するための基盤を開発したい仲間を募集中です。以下のリンクからご応募ください。

tech.zozo.com

カテゴリー