woshidan's blog

そんなことよりコードにダイブ。

ライブラリ開発屋がAthenaを利用してログの収集分析をやりやすくした話

この記事はServerless Advent Calender 2017の16日目の記事です。

ライブラリ開発屋として仕事でAthenaを使ってログの収集分析をやりやすくした話をします。

はじめに

普段は開発者としてiOS/Android両対応のモバイルアプリ向けのライブラリの開発やテストをしています。その業務の中でお問い合わせを受けた際、お客さんの状況を聞いてライブラリを修正したり使い方を提案したりして対応させていただくことがあります*1

その中で、なかなか言葉で状況の説明が難しい場合があり、そういうときは動作検証時のログをいただいて状況の確認をさせていただきます。しかし、いかんせんそういう状況は再現が難しかったりするもので動作検証のログがとれないか試しているうちに時間が経ってしまってもどかしいことが結構ありました。

そこで、なるべく早くお客様に解決方法の提案ができるように、それらしいログを検索して取得できたらいいな、セキュリティ的・S3の予算的にピンポイントで取得したいな、ということで今回Athenaを使ってやってみました。

Athenaについて簡単に

Athenaは標準的なSQLを利用してS3内のデータを分析できるサービスです。

実際中で動いているのは分散SQLクエリエンジンのPrestoで、データフォーマットにJSON形式を用いる場合は下記のような形式のJSONが入っているS3のバケットのパスが s3://athena-examples/users/logs だったとして

{
  "name": "太郎",
   "address": "日本のそのあたり",
   "comment": "JSONは実際は1行にminifyしておく必要があります"
}

以下のクエリを実行すればSQLを用いて s3://athena-examples/users/logs 以下のデータを検索できるようにしてくれます。

// 単純化のため少々それっぽくないログとなっています
CREATE EXTERNAL TABLE IF NOT EXISTS user_logs (
  name string, 
  address string, 
  comment string
 ) 
ROW FORMAT  serde 'org.apache.hive.hcatalog.data.JsonSerDe'
LOCATION 's3://athena-examples/users/logs/';

テーブル定義の際に指定した LOCATION のパス以下はテーブル定義の形式に沿って処理できるデータのみが入っている必要がありますが、上記のテーブル定義はAthenaが検索の際に用いるだけで、S3に入っているデータとは結びついていないので何度でも破棄したり作り直すことが可能です。

他に対応しているデータフォーマットなど、詳しい話についてはこちらをご覧ください。

実際にやってみて気を使ったこと

やってみたことを書くとログの形式をAthenaで検索しやすい形に変更してAthenaを使いましたで終わってしまうので、その際気を使ったことなどを書いていこうかと思います。

Athenaのコスト対策の話

Athenaの料金形態は検索を実行するときのデータスキャン量に対する従量課金で、 スキャンされたデータ 1 TB あたり 5 USD となっています*2

そのため、ログをgzipなどに圧縮するとスキャン対象の容量をかなり削減することができます。幸いログファイルには同じ文字列が多く含まれるためか、自分が行った事前調査では単純にgzipにしただけで圧縮前の容量の15~20%まで小さくすることができました。S3にアップロードするファイルの圧縮は比較的簡単に対策ができるためやったほうがいいでしょう。

Athenaのコスト対策はAthenaの料金の話だけでは終わりません。AthenaはS3にあるファイルを取得するためにS3を利用しており、Athenaの料金とは別途にS3の利用料金がかかります。このS3の利用料金が場合によってはAthenaのスキャン料金と同等以上になることがあり、

  • 同じクエリを何度も実行しないようにする
  • LIMIT句をつけると早めにスキャン & データ取得を打ち切るので必ずLIMIT句をつけるようにする
  • パーティションやテーブル定義に利用するロケーションのパスを工夫してスキャン範囲を狭くする

などの対応を取る必要があります。

パーティションやテーブル定義に利用するロケーションのパスを決めるにあたって

前節で パーティションやテーブル定義に利用するロケーションのパスを工夫してスキャン範囲を狭くする と書きましたが、バケットのパスやパーティション分割のために使える要素としては、モバイルライブラリの場合

  • 日付
  • 対応OS
  • ライブラリのバージョン
  • ユーザのサービスアカウントごとのid

などがあります。それぞれ、data=xxx/os=ios/version=nnn/user_app_id=xxxx のように素直にパーティションを掘ってもよいかもしれませんが、

  • ライブラリのバージョンは企業によってはなかなか更新の時間がとれないため、利用されているバージョンが10種類以上とばらついてしまう
  • 利用されているサービスアカウントはテスト用のものもあるので1日に数百以上ある

ということを踏まえると、あっという間にAthenaが制限しているパーティション上限数 20000に引っかかってしまうでしょう。

しかし、日頃の調査を振り返ると

  • Android用ライブラリの調査をするときにiOS用ライブラリの調査をすることはほとんどない
  • ライブラリのログは利用している開発者のアプリごとの利用者規模が6桁~導入時のお試しで1桁、2桁まで様々
    • ライブラリのバージョンで区切って検索しているつもりが一番利用者数の多いアプリのログしかスキャンしていないことがありうる
  • ログを調査するのはお問い合わせ起点、つまり、調査対象のサービスアカウントや利用バージョンの情報を持っていることが多い
  • 新バージョンの不具合などにもとづいてログを調べるとしたら、結局は最新の日付から探していくことが多いだろう

などの事実があり、これらを踏まえると、同時に範囲検索で使う要素は存外少なそうなことがわかります。

結局、大きなテーブルを一つ定義してその中でいくつかのパーティションを利用するのではなく、基本的にはテーブルのロケーションに深めのパスを設定し、そのパスごとにたくさんのテーブルを定義しては捨てる方針にしました。

ややかっこわるいのですが、放っておくとパーティションのキーにあたる値の組み合わせは基本的にどんどん増えていくため、ほとんど一つのクエリで同時に検索しない範囲はテーブルのロケーションの方に突っ込んでもよいと思います。

スクリプトでクエリを生成することにしてクエリ実行結果の再利用やLIMIT句の指定を徹底する

Athenaは従量課金性ですが、一度実行したクエリの結果を再利用することが可能です。クエリを実行した時のquery execution idをパラメータにしてGetQueryResultsのAPIにリクエストを送ることにより、データのスキャンを再度行わずに実行結果を再取得することができます*3

なので、一度実行したクエリとそれに対応するquery execution idを控えて、実行しようとしているクエリが直近で実行されたものなら以前の結果を再利用することで、コストを抑えることができます。

これも結構簡単なスクリプトで対応を行うことが可能です*4

表記揺れによってほぼ同じクエリが別のクエリとカウントされて似たようなクエリが何個も走るかもしれませんが、いっそのことクエリの生成もスクリプトで行うことにしました。そうすることで、

  • 表記揺れがないため、クエリとquery execution idの対応を管理するスクリプトが動きやすい
  • テーブルのロケーションを深めに掘ったり、セッションの属性と紐づけてログを検索・分析しやすくした結果、肥大化したテーブル定義を間違えない
  • LIMIT句の追加などクエリの中で外してはいけないルールが徹底される

ことになりました。--dry-run オプションでAthenaに投げる予定のクエリを吐き出すようにしていて、手動でクエリを書きたくなった場合の下書きとしても利用できるようにしたり、実行時のログを一部出力することで、目当てのログのパスを見つけやすくしてなるべくターミナル一つで完結するように工夫しています。最近のブームは小規模なスクリプトDIYです*5

今後

今回の対応で社内外からお問い合わせがきたらログを分析しやすくなったので、今後はお問い合わせが来る前になにか気づけたらいいなということで、S3とLamdbaを組み合わせて不具合が起きてそうなときのログの検知もやってみたいと考えています。

DIYの現場からは以上です。

*1:サポートの方とお客様にひたすら助けられるお仕事とも言います

*2:https://aws.amazon.com/jp/athena/pricing/

*3:http://docs.aws.amazon.com/athena/latest/APIReference/API_GetQueryResults.html

*4:こういう http://woshidan.hatenablog.com/entry/2017/10/04/055335

*5:自分以外みんな知ってそうな余談であれなのですが、検索結果は1行1レコードとなっています。一回のクエリで同じファイルが複数レコードとして引っかかることがあるため、類似の用途で使う場合はLIMIT句の数字を用途に応じて増減しましょう