Amazon Web Services ブログ

Amazon CloudFront のアクセスログを大規模に分析する

多くの AWS の顧客が、グローバルなコンテンツ配信ネットワーク (CDN) サービスである Amazon CloudFront を使用しています。低いレイテンシーと高い転送速度で、ウェブサイト、動画、API 操作をブラウザやクライアントに配信します。 Amazon CloudFront は、キャッシングまたはウェブアプリケーションファイアウォールによって、大量の負荷や悪意のあるリクエストからバックエンドを保護します。その結果、バックエンドに届くのはすべてのリクエストのごく一部になります。Amazon Simple Storage Service (S3) へのすべてのリクエストの詳細情報とともにアクセスログを保存するように Amazon CloudFront を設定することができます。これにより、キャッシュの効率に関する洞察を得たり、顧客が製品をどのように使用しているかを知ることができます。

S3 のデータに対して標準の SQL クエリを実行するための一般的な選択肢は Amazon Athena です。 事前にインフラストラクチャを設定したりデータをロードしたりすることなく、クエリによってデータが即座に分析されます。 実行するクエリの分だけを支払います。Amazon Athena は、迅速でインタラクティブなクエリに最適です。大きな結合、合併、入れ子になったクエリ、ウィンドウ関数など、データの複雑な分析をサポートします。

このブログ記事では、Amazon CloudFront アクセスログストレージを再構築してクエリのコストとパフォーマンスを最適化する方法を説明します。時系列データの他のソースにも適用可能な一般的なパターンを示しています。

Amazon Athena クエリのための Amazon CloudFront アクセスログの最適化

コストとパフォーマンスという、最適化の 2 つの主な側面があります。

データの保存とクエリの両方でコストが安いことが必要です。アクセスログは、S3 に保存され、GB /月単位で請求されます。したがって、特にログを長期間保存したい場合は、データを圧縮することは意味があります。また、クエリにもコストはかかります。ストレージのコストを最適化すると、通常はクエリのコストが発生します。アクセスログは gzip によって圧縮されて配信され、Amazon Athena は圧縮を処理できます。Amazon Athena ではスキャンされた圧縮データの量に基づいて請求されるので、圧縮による利点はコスト削減として享受できます。

クエリは、さらにパーティショニングの利点を受けます。パーティショニングは、テーブルを複数の部分に分割し、列の値に基づいて関連データをまとめます。時間ベースのクエリの場合、年、月、日、時間ごとにパーティショニングすることが役に立ちます。Amazon CloudFront アクセスログでは、これはリクエスト時間を示します。データとクエリに応じて、パーティションにさらにディメンションを追加します。たとえば、アクセスログの場合、リクエストされたドメイン名が考えられます。データを照会するときに、パーティションに基づいてフィルターを指定して、Amazon Athena がスキャンするデータを少なくすることができます。

一般に、スキャンするデータが少なくなるとパフォーマンスが向上します。アクセスログをカラムナ形式に変換すると、スキャンするデータが大幅に削減されます。カラムナ形式はすべての情報を保持しますが、列ごとに値を保存します。これにより、辞書を作成したり、ランレングスエンコーディングやその他の圧縮技術を効果的に使用することができます。Amazon Athena は、列がフィルタまたはクエリの結果で使用されていない場合は列をまったくスキャンしないため、読み取るデータ量をさらに最適化できます。また、カラムナ形式はファイルをチャンクに分割し、ファイルレベルと範囲 (最小/最大)、カウント、値の合計などのチャンクレベルでメタデータを計算します。ファイルまたはチャンクがクエリに関連がないことをメタデータが示している場合、Amazon Athena はそれをスキップします。さらに、クエリと自分が探している情報が分かっていれば、データをさらに集計 (たとえば、日ごと) して、頻繁に実行されるクエリのパフォーマンスを向上させることができます。

このブログ記事では、パーティショニングカラムナ形式への変換という、最適化のために Amazon CloudFront アクセスログを再構築する 2 つの方法に焦点を当てます。パフォーマンスチューニングの詳細については、ブログ記事「top 10 performance tuning tips for Amazon Athena」を参照してください。

このブログ記事では、ソリューションの概念について説明し、実装を分かりやすくするためにコードの抜粋を掲載しています。概念の完全に機能する実装については AWS サンプルリポジトリをご覧ください。AWS Serverless Application Repository からパッケージ化されたサンプルアプリケーションを起動すると、1 ステップで数分以内にデプロイできます。

S3 で CloudFront アクセスログを分割する

Amazon CloudFront は、それぞれのアクセスログファイルを CSV 形式で、選択した S3 バケットに配信します。その名前は、次の形式に従います (詳細については、アクセスログの設定と使用を参照)。

/optional-prefix/distribution-ID.YYYY-MM-DD-HH.unique-ID.gz

ファイル名には、リクエストが発生した期間の日付と時刻が協定世界時 (UTC) で含まれています。Amazon CloudFront のディストリビューションにはオプションのプレフィックスを指定できますが、ディストリビューションのすべてのアクセスログファイルは同じプレフィックスで保存されます。

大量のアクセスログデータがある場合、その一部だけを効率的にスキャンして処理することは困難です。したがって、データを分割する必要があります。大きなデータ空間で使用されるツールの大半 (たとえば、Apache Hadoop エコシステム、Amazon Athena、AWS Glue など) は、Apache Hive スタイルを使用してパーティショニングを処理できます。パーティションとは、自己記述的なディレクトリです。ディレクトリ名は、列の値だけでなく列名も反映します。アクセスログの場合、これは望ましい構造です。

/optional-prefix/year=YYYY/month=MM/day=DD/hour=HH/distribution-ID.YYYY-MM-DD-HH.unique-ID.gz

この構造を生成するために、サンプルアプリケーションは S3 イベント通知によって各ファイルの処理を開始します。Amazon CloudFront が新しいアクセスログファイルを S3 バケットに配置するとすぐに、イベントによって AWS Lambda 関数 moveAccessLogs がトリガーされます。これによりファイルは、ファイル名に対応するプレフィックスに移動します。技術的には、この移動はコピーであり、その後に元のファイルが削除されます。

 

 

Amazon CloudFront アクセスログの移行

サンプルアプリケーションのデプロイメントには、<StackName>-cf-access-logs という名前の単一の S3 バケットが含まれています。 既存の Amazon CloudFront ディストリビューションの設定を変更して、new/ プレフィックスを持つこのバケットにアクセスログを配信することができます。ファイルは、バケットに配置されるとすぐに、Amazon Athena パーティショニング用の正規のファイル構造に移動されます。

以前のアクセスログファイルをすべて移行するには、それらをバケット内の new/ フォルダに手動でコピーします。たとえば、AWS コマンドラインインターフェイス (AWS CLI) を使用してファイルをコピーすることができます。これらのファイルは、Amazon CloudFront によって受信ファイルと同じ方法で処理されます。

パーティションのロードおよびアクセスログの照会

Amazon Athena でバケット内のアクセスログをクエリできるには、AWS Glue データカタログにメタデータが必要です。デプロイ時に、サンプルアプリケーションはスキーマとロケーションの定義を含むテーブルを作成します。この新しいテーブルは、Amazon CloudFront のドキュメントの CREATE TABLE ステートメントにパーティション情報を追加することで作成されます (PARTITIONED BY 句に注意してください)。

CREATE EXTERNAL TABLE IF NOT EXISTS
    cf_access_logs.partitioned_gz (
         date DATE,
         time STRING,
         location STRING,
         bytes BIGINT,
         requestip STRING,
         method STRING,
         host STRING,
         uri STRING,
         status INT,
         referrer STRING,
         useragent STRING,
         querystring STRING,
         cookie STRING,
         resulttype STRING,
         requestid STRING,
         hostheader STRING,
         requestprotocol STRING,
         requestbytes BIGINT,
         timetaken FLOAT,
         xforwardedfor STRING,
         sslprotocol STRING,
         sslcipher STRING,
         responseresulttype STRING,
         httpversion STRING,
         filestatus STRING,
         encryptedfields INT 
)
PARTITIONED BY(
         year string,
         month string,
         day string,
         hour string )
ROW FORMAT DELIMITED FIELDS TERMINATED BY '\t'
LOCATION 's3://<StackName>-cf-access-logs/partitioned-gz/'
TBLPROPERTIES ( 'skip.header.line.count'='2');

Amazon Athena クエリエディタを介してメタストアチェック (msck) ステートメントを実行することで、これまでに追加されたパーティションを読み込むことができます。 S3 でパーティション構造を検出し、メタストアにパーティションを追加します。

msck repair table cf_access_logs.partitioned_gz

これで、Amazon Athena クエリエディタでデータに対する最初のクエリの準備が整いました。

SELECT SUM(bytes) AS total_bytes
FROM cf_access_logs.partitioned_gz
WHERE year = '2017'
AND month = '10'
AND day = '01'
AND hour BETWEEN '00' AND '11';

このクエリは、テーブルのリクエスト日付 (前の例では date と呼ばれる) 列は指定せず、パーティショニングに使用される列を指定します。これらの列は date に依存しますが、テーブル定義はこの関係を指定しません。リクエスト日付列のみを指定すると、どのファイルに関連する行が含まれ、どのファイルに含まれないかについてのヒントがないため、Amazon Athena はすべてのファイルをスキャンします。パーティション列を指定することで、Amazon Athena は Amazon CloudFront アクセスログファイルの合計量のごく一部をスキャンします。これにより、クエリのパフォーマンスとコストの両方が最適化されます。time など、WHERE 句に列をさらに追加して、結果を絞り込むことができます。

コストを節約するには、パーティション列も WHERE 句に入れて、パーティションの範囲を最小に絞り込むことを検討します。クエリに対するクエリ実行統計でスキャンされたデータの量を測定することによってアプローチを検証します。こうした統計は、ステートメントが実行された後に Amazon Athena クエリエディタにも表示されます。

継続的なパーティションの追加

Amazon CloudFront はリクエストについて新しいアクセスログデータを継続的に配信するので、パーティション用の新しいプレフィックスが S3 で作成されます。ただし、Amazon Athena は既知のパーティション、つまり以前にメタストアに追加されたパーティションに含まれるファイルだけを照会します。そのため、msck コマンドを定期的に起動するのは最善の解決策ではありません。まず、Amazon Athena はすべての S3 パスをスキャンしてパーティションを検証して読み込むため、時間がかかります。さらに重要なことは、この方法ではすでにデータが配信されているパーティションだけを追加します。したがって、データが S3 に存在していても Amazon Athena のクエリにはまだ表示されていない期間があります。

パーティションはリクエスト時間に依存するだけなので、サンプルアプリケーションは事前に 1 時間ごとにパーティションを追加することでこの問題を解決します。こうして、Amazon Athena は、ファイルが S3 に存在するとすぐにそれらのファイルをスキャンします。スケジュールされた AWS Lambda 関数は、次のようなステートメントを実行します。

ALTER TABLE cf_access_logs.partitioned_gz
ADD IF NOT EXISTS 
PARTITION (
    year = '2017',
    month = '10',
    day = '01',
    hour = '02' );

列の値から自動的に導かれるので、このステートメントでは正規の location 属性の指定を省略することができます。

アクセスログのカラムナ形式への変換

前述のように、カラムナ形式では、Amazon Athena はクエリに関係のないデータのスキャンをスキップするので、結果としてコストが削減されます。Amazon Athena は現在、カラムナ形式の Apache ORC および Apache Parquet をサポートしています。

変換の鍵は、Amazon Athena の CREATE TABLE AS SELECT (CTAS) 機能です。CTAS クエリは、別の SELECT クエリの結果から新しいテーブルを作成します。Amazon Athena は、CTAS ステートメントによって作成されたデータファイルを Amazon S3 の指定された場所に保存します。CTAS を使用してデータを集計または変換し、それをカラムナ形式に変換することができます。サンプルアプリケーションでは CTAS を使用して、すべてのログを CSV 形式から Apache Parquet 形式に 1 時間ごとに書き換えています。その後、結果として得られたデータは単一のパーティションのテーブル (ターゲットテーブル) に追加されます。

Apache Parquet 形式でのターゲットテーブルの作成

ターゲットテーブルは、partitioned_gz テーブルを少し変更したものです。別のロケーション以外に、次のテーブルは Apache Parquet のさまざまなシリアライザ/デシリアライザ (SerDe) 設定を示しています。

CREATE EXTERNAL TABLE `cf_access_logs.partitioned_parquet`(
  `date` date,
  `time` string,
  `location` string,
  `bytes` bigint,
  `requestip` string,
  `method` string,
  `host` string,
  `uri` string,
  `status` int,
  `referrer` string,
  `useragent` string,
  `querystring` string,
  `cookie` string,
  `resulttype` string,
  `requestid` string,
  `hostheader` string,
  `requestprotocol` string,
  `requestbytes` bigint,
  `timetaken` float,
  `xforwardedfor` string,
  `sslprotocol` string,
  `sslcipher` string,
  `responseresulttype` string,
  `httpversion` string,
  `filestatus` string,
  `encryptedfields` int)
PARTITIONED BY ( 
  `year` string,
  `month` string,
  `day` string,
  `hour` string)
ROW FORMAT SERDE 
  'org.apache.hadoop.hive.ql.io.parquet.serde.ParquetHiveSerDe' 
STORED AS INPUTFORMAT 
  'org.apache.hadoop.hive.ql.io.parquet.MapredParquetInputFormat' 
OUTPUTFORMAT 
  'org.apache.hadoop.hive.ql.io.parquet.MapredParquetOutputFormat'
LOCATION
  's3://<StackName>-cf-access-logs/partitioned-parquet'
TBLPROPERTIES (
  'has_encrypted_data'='false',
  'parquet.compression'='SNAPPY')

CTAS クエリによる Apache Parquet への変換

サンプルアプリケーションは、1 時間のデータを考慮して、実行ごとに単一のパーティションで CTAS クエリを実行する、スケジュールされた AWS Lambda 関数である transformPartition を提供します。 Apache Parquet ファイルのターゲットの場所は、partitioned_parquet テーブルの場所にある Apache Hive スタイルのパスです。

 

 

S3 に書き込まれるファイルは重要ですが、このデータに対する AWS Glue Data Catalog のテーブルは単なる副産物です。したがって、この関数は CTAS テーブルをただちに削除し、代わりに partitioned_parquet テーブルに対応するパーティションを作成します。

CREATE TABLE cf_access_logs.ctas_2017_10_01_02
WITH ( format='PARQUET',
    external_location='s3://<StackName>-cf-access-logs/partitioned_parquet/year=2017/month=10/day=01/hour=02',
    parquet_compression = 'SNAPPY')
AS SELECT *
FROM cf_access_logs.partitioned_gz
WHERE year = '2017'
    AND month = '10'
    AND day = '01'
    AND hour = '02';

DROP TABLE cf_access_logs.ctas_2017_10_01_02;

ALTER TABLE cf_access_logs.partitioned_parquet
ADD IF NOT EXISTS 
PARTITION (
    year = '2017',
    month = '10',
    day = '01',
    hour = '02' );

新しいデータが書き込まれるとすぐにステートメントを実行する必要があります。Amazon CloudFront は通常、ログに表示されるイベントから 1 時間以内に、一定期間ログファイルを Amazon S3 バケットに配信します。サンプルアプリケーションは、1 時間ごとに transformPartition 関数をスケジュールして、1 時間前までの 1 時間分のデータを変換します。

ある期間のログファイルエントリの一部または全部が最大 24 時間遅れることがあります。この問題を軽減する必要がある場合は、その期間が経過した後にパーティションを削除して再作成します。また、以前の Amazon CloudFront アクセスログからパーティションを移行した場合も、各パーティションに対して transformPartition 関数を実行します。サンプルアプリケーションは、継続的に追加されたファイルだけを変換します。

gzip パーティションのすべてのファイルを Apache Parquet に変換すると、不要なデータを削除することでコストを節約できます。S3 のライフサイクルポリシーを使用して、より安価なストレージクラスに gzip ファイルをアーカイブするか、一定の日数が経過したら削除します。

複数のテーブルにわたるデータの照会

これで、元の Amazon CloudFront アクセスログデータから 2 つの派生テーブルができました。

  • partitioned_gz には、新しいファイルが配信されるとすぐに追加される gzip 圧縮された CSV ファイルが含まれています。
  • partitioned_parquet のアクセスログは、1 時間後に書き込まれます。大まかな仮定は、CTAS クエリが gzip パーティションを変換するのに最大 15 分かかることです。この仮定を測定し確認する必要があります。データのサイズによっては、これははるかに速くなります。

次の図は、すべてのデータに対する完全なビューが 2 つのテーブルからどのように構成されているかを示しています。Apache Parquet ファイルの最後の完全なパーティションは、現在時刻から変換期間と、Amazon CloudFront がアクセスログファイルを配信するまでの期間を引いた時刻までに終了します。

便宜上、サンプルアプリケーションは Amazon Athena のビュー combined を両方のテーブルの合併として作成します。file と呼ばれる追加の列を含みます。 これは行を保存するファイルです。

CREATE OR REPLACE VIEW cf_access_logs.combined AS
SELECT *, "$path" AS file
FROM cf_access_logs.partitioned_gz
WHERE concat(year, month, day, hour) >=
       date_format(date_trunc('hour', (current_timestamp -
       INTERVAL '15' MINUTE - INTERVAL '1' HOUR)), '%Y%m%d%H')
UNION ALL SELECT *, "$path" AS file
FROM cf_access_logs.partitioned_parquet
WHERE concat(year, month, day, hour) <
       date_format(date_trunc('hour', (current_timestamp -
       INTERVAL '15' MINUTE - INTERVAL '1' HOUR)), '%Y%m%d%H')

これで、ビューからデータを照会して、列ベースのファイルパーティションを自動的に利用することができます。前述のように、パーティション列 (year、month、day、hour) をステートメントに追加して、Amazon Athena がスキャンするファイルを制限する必要があります。

SELECT SUM(bytes) AS total_bytes
FROM cf_access_logs.combined
WHERE year = '2017'
   AND month = '10'
   AND day = '01'

まとめ

このブログ記事では、Amazon Athena クエリのコストとパフォーマンスを 2 段階で最適化する方法を学びました。まず、データ全体を小さなパーティションに分割します。これにより、スキャンするファイルの数を減らすことで、クエリをより高速に実行できます。2 番目の手順では、各パーティションをカラムナ形式に変換して、ストレージコストを削減し、Amazon Athena によるスキャンの効率を高めます。

両方の手順の結果が、アプリケーションなどによる便利な対話型クエリのための単一のビューに結合されます。すべてのデータは、リウエストの時刻によって分割されます。したがって、この形式は、列が制限され、時間範囲が分かっているログへの対話型ドリルダウンに最も適しています。このようにして、たとえば以下のものに簡単にアクセスできるようにして、Amazon CloudFront レポートを補完します。

  • 過去 60 日以上のデータ
  • 特定の日または時間における詳細な HTTP ステータスコード (200、403、404 など) の分布
  • URI パスに基づく統計
  • Amazon CloudFront の 50 の最も人気があるオブジェクトのレポートにリストされていないオブジェクトの統計
  • それぞれのリクエストの属性へのドリルダウン

このブログ記事とサンプルアプリケーションが、Amazon CloudFront アクセスログ以外の時系列データにも役立つことを願っています。ぜひ、ソースリポジトリ内のサンプルアプリケーションに対する機能拡張を送信したり、コメントにフィードバックを提供したりしてください。

 


著者について

Steffen Grunwald は、アマゾン ウェブ サービスのシニアソリューションアーキテクトです。ドイツの企業顧客がクラウドへの過程を進めるのをサポートしながら、彼はパフォーマンス、運用効率を高め、革新のスピードを上げるアプリケーションアーキテクチャと開発プロセスを深く掘り下げることに熱意を注いでいます。