Elixir の Elixir による Elixir のためのログ解析

これは?

こんにちは。YAMAP 分析チームの松本です。

これは Elixir Advent Calendar 2020 23日目の記事です。 昨日は @g_kenkun さんの Elixirで他人に扱われるプログラムを書くときに考える例外の話 でした。

私が Elixir が好きな理由の一つは、パターンマッチとエラーハンドリング機構により、正常処理と例外処理を分離してコードの可読性を高めることが出来る点で、興味深く読ませて頂きました 🙏

さらに YAMAP エンジニア Advent Calendar 2020 24日目の記事でもあります。

さて自分の記事はというと、YAMAP の画像アップロードサービス を Elixir/Phoenix でリニューアルして2年経つので、その振り返りをしてみようと思いました。

画像アップロードサービス については↓で紹介しています。

何を振り返るか

振り返りの題材としては、以下にしようと思います。

  • YAMAP の MAU、活動日記数 の成長
  • それに従い、画像アップロードサービスはどうスケールしたか?

YAMAP の MAU、活動日記数 の成長

note.yamap.com

こちらの記事の通り、2019-10 vs 2020-10 で以下の成長となっています。

  • MAU 160%
  • 活動日記数 220%

画像アップロードサービスのスケール指標をどう定義するか

先のスライドにもある通り、画像アップロードサービスは AWS ECS クラスタ上で動いています。 なので単純に起動しているコンテナ数を、スケールの指標とできそうです。

Phoenix が出力するログ(カスタマイズしてます)には、コンテナの ID か IP かを記録していた気がします。 例えば5分ごとのコンテナの IP のユニーク数を集計すれば、5分ごとに何台のコンテナが起動していたかが分かります。 YAMAP ではサーバーのログは BigQuery に転送しているので、シュッと集計できます。簡単簡単。

...そう思っていた時期が、俺にもありました。

f:id:hidetakafm:20201224004239p:plain

出典:グラップラー刃牙 ©板垣恵介

Phoenix のログを諦めて ALB のログを調べる

Phoenix のログ(カスタマイズしてます)に、コンテナを識別する情報が無かった

はい、ございませんでした。涙目です😭

CloudWatch のメトリクスにあってくれぇ!と望みをかけましたが、コンテナ数に相当するものは見つけられませんでした。

ALB ログで何とかする

と、ここで ALB(Application Load Balancer)のログを取っていることを思い出し、調べてみるとリクエストを処理した後段の IP とポートが記録されていました。

docs.aws.amazon.com

f:id:hidetakafm:20201224005836p:plain

ECS では ALB から後段のサービス(コンテナ)に、動的ポートマッピングで接続しています。 なのでこの target:port が、コンテナを識別する情報とできそうです。

ALB ログを解析する

ちょちょっと Phoenix のログを分析して終わりのはずが、やることが増えてしまいました...

とりあえず以下をやらなくてはいけません。

    1. ALB ログをダウンロードして、
    1. パースして必要な情報を抽出し、
    1. 5分ごとのコンテナ数を可視化する

github.com

実装一式、↑に up しました。

1. ALB ログをダウンロード

alb_log_sandbox/download_log.sh at master · hidetaka-f-matsumoto/alb_log_sandbox · GitHub

指定日のログを aws s3 cp コマンドで取ってきて gzip 回答しています。

2. パースして必要な情報を抽出

alb_log_sandbox/alb_log.ex at master · hidetaka-f-matsumoto/alb_log_sandbox · GitHub

先の ALB のログフォーマットに従い、ログ1行を正規表現でパースするモジュールを作りました。

alb_log_sandbox/alb_log_sandbox.ex at master · hidetaka-f-matsumoto/alb_log_sandbox · GitHub

こちらは 1 でダウンロードした大量のログファイルを一括パースして、必要な情報を csv 1ファイルに出力するモジュールです。 Flow で多量のログファイルを並列処理している点が、Elixir らしいポイントかと思います。

  def convert_log_to_request_csv(src_dir, dst_file, pattern) do
    Path.wildcard("#{src_dir}/*.log")
    |> Flow.from_enumerable(max_demand: 20)
    |> Flow.flat_map(fn path ->
      parse_log(path)
      |> select_by_pattern(pattern)
    end)
    |> Enum.to_list
    |> output_request_csv(dst_file)
  end

実際ここで並列処理することで CPU のコアも使い切り、体感で倍くらい早く処理が終わりました。

f:id:hidetakafm:20201224020906p:plain

3. 5分ごとのコンテナ数を可視化する

最後にコンテナ数を可視化します。 2 でリクエスト時刻 time, 処理した後段の IP:PORT targetcsv に出力しているので、5分毎の target のユニーク数を可視化してみます。

今回は Elixir と同じくパイプ演算子を持つ R 言語でやってみます。 R のパイプは %>% です。

alb_log_sandbox/visualize_target_count.R at master · hidetaka-f-matsumoto/alb_log_sandbox · GitHub

library(tidyverse)
library(lubridate, warn.conflicts = FALSE)

args <- commandArgs(trailingOnly=TRUE)
data <- read_csv(args[1])
data %>%
  mutate(time_group = round_date(data$time, '5 mins')) %>%
  group_by(time_group) %>%
  summarize(target_count = n_distinct(target)) %>%
  ggplot(mapping = aes(x = time_group, y = target_count)) +
    layer(geom = "bar", stat = "identity", position = "identity")

たったこれだけでデータの集約と、グラフの描画ができるのは良いですね。 パイプ演算子により、コードの可読性も高まっていると感じます。

f:id:hidetakafm:20201224023436j:plain

そして可視化してみた結果は、画像アップロードサービスは 2019-10-20 vs 2020-10-25 で 220 ~ 230% 程度スケールしていました。 これは活動日記数の成長と一致しており、自然な結果と考えられます。

※グラフの時間軸は UTC になっていますmm

まとめ

以上、いかがでしたでしょうか。 最初は優雅に Phoenix サービスのスケール状況を分析しつつ、何か Elixir ならではの特徴が見えないかなぁとか考えていましたが、甘かったですね...

蓋を開けてみると想定外にモリモリ実装する羽目になり、何のアドベントカレンダーなんだか分からなくなってしまいました😅

しかしログのパースなどは Elixir のパターンマッチの強みを活かせる好例ですので、その周辺の紹介が多少なりできなのは良かったかもしれません。 駆け足で実装したので綺麗なコードとは言いがたいかもしれませんが、何かしら皆様の参考になる箇所があれば幸いです。

それではまた。

Looker + hubot によるデータ業務の自動化

はじめに

🔺YAMAP 分析チームの松本です。 Looker Advent Calendar 2020YAMAP エンジニア Advent Calendar 2020 3日目の記事です。 よろしくお願いします。

YAMAP では登山地図の情報をより充実・正確にするために、ユーザーデータを活用した 伊能忠敬プロジェクト に取り組んでいます。 具体的な一例としては、ユーザーさんの軌跡を分析して新規ルートを抽出し、登山地図に反映しています。

このルート抽出処理は Python で実装しており、従来は分析者が手元のマシンで実行していました。 また、地図上のどこに対してルート抽出を試みるかは、データ基盤のデータを Looker で可視化して決めています。

f:id:hidetakafm:20201203152227p:plain

今夏にはじめたルート抽出の取り組みも軌道に乗り、最近では自動化・効率化の機運が高まっていました。

どう自動化するか

Python ルート抽出処理については、もともと運用していた分析システム(Python / GKE)に組み込みました。 あとは Looker のデータから抽出処理を kick する所を自動化すれば OK ですが、ここでは 2 つの方法を検討しました。

  1. Looker Action Hub でカスタムアクションを開発する
  2. Looker から Slack へ抽出候補 csv を投稿させる。hubot で csv を読み取り、抽出処理を kick する

今回は以下の理由から、2 の方法を採用しました。

  • 自動化の仕組みから GKE クラスタに対して kubectl コマンドを実行するため ※1
    • ルート抽出処理の kick は k8s job をデプロイする形で行いたく、そのために kubectl コマンドで実行したかった
    • これは 1 の方法では実現できなさそうに思われた
  • 一連のオペレーションは人間が ChatOps として実行できるようにもしておきたかったため
    • YAMAP ではユーザーさんからルート追加要望を承る場合もあり、その場合はオペレーターがルート抽出を kick できると便利

ということで、以下のような構想になります。

f:id:hidetakafm:20201203202938p:plain

自動化した結果

  • hubot の名前は "yak" にしました。ヤクはヒマラヤ地方の高地に生息する牛さんです
    • f:id:hidetakafm:20201203175818p:plain
  • Looker から csv をスケジュール配信します。コメントに hubot のコマンドを書いておきます
    • f:id:hidetakafm:20201203180256p:plain
  • yak がファイルをパースして、kubectl コマンドでルート抽出処理を kick します
    • f:id:hidetakafm:20201203180317p:plain
  • 分析システム(yama)が処理完了を教えてくれます
    • f:id:hidetakafm:20201203180411p:plain
    • この後、地図エディタで成果物を人間がチェックして、登山者の皆様にお届けしています

やったね 🎉

まとめと展望

Looker + hubot で、YAMAP 伊能忠敬プロジェクトのオペレーションの一部を自動化しました。 Looker には Action Hub という便利な仕組みも用意されていますが、今回のように事情がある場合は hubot などチャットボットを利用することが出来ます。

今回は僕が所属する分析チームのオペレーションを自動化しましたが、今後は hubot や Action Hub も活用して社内のデータ業務を自動化し、データ活用を加速したいと思います。

それにしても YAMAP では様々なデータ活用が広がっており、それを柔軟に支えてくれる Looker は本当に素晴らしいツールだと思います! (hubot アドベントカレンダーみたいになってしまいスミマセンでした 😅)

実装(ご参考まで)

何もコードがないのは寂しいので、hubot の yama extract-route コマンドの正常系の実装だけ載せておきます。

'use strict';
const util = require('util');
const childProcess = require('child_process');
const exec = util.promisify(childProcess.exec);
const fs = require('fs');
const csv = require('csv');
const request = require('request');

module.exports = (robot) => {
  robot.respond(/yama extract-route/i, async (res) => {
    const fileInfo = res.message.rawMessage.files[0],
          tmpFilePath = `tmp/${fileInfo.timestamp}_${fileInfo.name}`;
    let data = [];
    const parser = csv.parse((err, row) => {
      data = row;
    });
    request
      .get(fileInfo.url_private_download)
      .auth(null, null, true, process.env.HUBOT_SLACK_TOKEN)
      .pipe(fs.createWriteStream(tmpFilePath))
      .on('finish', () => {
        fs.createReadStream(tmpFilePath)
          .pipe(parser)
          .on('end', async () => {
            // process head 10 rows.
            for (let row of data.slice(1, 4)) {
              const result = await exec(`LANDMARK=${row[1]} k8s/scripts/deploy/run_route_extraction`);
              res.send(result.stdout);
            }
          });
      });
  });
}

補足

今回採用しなかった Looker Action Hub を改めて調べると、GCP の CloudStorage に csv を PUT するアクションが用意されていました。

CloudStorage の PUT イベントに対して CloudFunction をトリガすることで、※1 はクリアできる可能性があることに、この記事を書きながら気づきました...

Elixir/Phoenix + Neo4j でお手軽!高速!経路探索

はじめに

聖夜に fukuoka.ex Elixir/Phoenix Advent Calendar 2019YAMAP エンジニア Advent Calendar 2019 24日目の記事がやって参りました🎅

🔺YAMAP 分析チームの松本です。元バックエンドエンジニアです。

YAMAP のバックエンドでは、応答速度やスケーラビリティの性能が要求されるケースでの選択肢として、Elixir/Phoenix を活用しています。

また従来より YAMAP では、登山計画時の便利機能として、経路探索システムが熱望されていました。

今回は

  • グラフ構造をもつデータの探索を得意とする GraphDB Neo4j
  • 応答速度に優れる Elixir/Phoenix
  • 特に理由はないけどフロントは Elm

を利用し、お手軽かつ高速な経路探索システムを開発してみようと思います。

Neo4j とは

neo4j.com

実は僕は詳しくなくて、SNS での人間関係の探索などに使われる DB くらいのイメージです。 「A さんは最近サーフィンに興味があるので、友達の友達の範囲内で、サーフィンが趣味の人をサジェストする」等

経路もグラフ構造なので、経路探索にも向いているようです。

Elixir/Phoenix とは

特に説明不要かと思いますが、詳しくはこいつを見てくれ。

すごく

  • 省リソースで
  • 耐障害性が高くて
  • 応答速度が早い

です。

問題設定

今回やることを定義します。 経路探索の舞台は、我らが「宝満山」にします。(福岡県)

f:id:hidetakafm:20191224214045p:plain

↑地図の通り、

  • Neo4j
    • n0 ~ 19 までの node を設定する
    • 実際の登山道に基づき、node 間の relationship を定義する(コースタイムも)
  • Elixir/Phoenix
    • Neo4j へのデータ登録タスク
    • node 一覧を返却する api:GET /nodes
    • 経路探索 api:GET /routes/search?from=<from_id>&to=<to_id>
  • Elm
    • 地図上に node を表示する
    • 2点を選択したら経路探索を実行
    • 結果のルートを表示する

が、今回やることです。

開発結果

f:id:hidetakafm:20191224222010g:plain

始点と終点を選択すると、所要時間が短い順に、3つの経路がサジェストされます。 登山計画が捗る予感がする!🗻

開発詳細

Repository & Setup

Elixir/Phoenix

github.com

$ docker-compose build
$ docker-compose up phoenix

すれば動くはず。

Neo4j

↑の後、localhost:7474 を開くと Neo4j コンソールが見れるので、初期ユーザー / パスワード = neo4j / neo4j でログインできます。 その後パスワードを password に変更します。(Phoenix の設定合わせ)

Elm

github.com

$ elm make src/App.elm --output src/app.js
$ elm reactor --port=8002

すれば動くはず。 ちなみに僕はフロントも Elm も素人なんでクソコード勘弁して下さい😓

mix task で Neo4j にデータ投入

$ docker-compose run --rm phoenix mix import_seed_data

localhost:7474 で Neo4j のデータを確認すると、

f:id:hidetakafm:20191224220152p:plain

宝満山の登山道のグラフ構造が確認できます。良いネ👍

データクリアしたい時は↓も用意してあります。

$ docker-compose run --rm phoenix mix clear_all_data

Neo4j 接続設定

Bolt.Sips を使いました。

github.com

config :bolt_sips, Bolt,
  url: "bolt://neo4j:7687",
  basic_auth: [username: "neo4j", password: "password"],
  pool_size: 10

検索実行

defmodule PhoenixNeo4jExample.Route do
  alias Bolt.Sips, as: Neo
  alias Bolt.Sips.Response
  alias Bolt.Sips.Types.Path
  alias Bolt.Sips.Types.Node

  ...

  def search(from, to) do
    query = cyper_query(:search, from, to)
    routes =
      Neo.conn()
      |> Neo.query!(query)
      |> parse_search_result
    {:ok, routes}
  end

  defp parse_search_result(%Response{records: records}) do
    records
    |> Enum.with_index
    |> Enum.map(fn({[_from, _to, %Path{nodes: nodes}, time], i}) ->
      %{
        node_ids: Enum.map(nodes, fn(%Node{properties: %{"id" => id}}) -> id end),
        total_time: time,
        color: @colors |> Enum.at(i)
      }
    end)
  end

  defp cyper_query(:search, from, to) do
    ~s"""
    MATCH (from:Node {id: '#{from}'}), (to:Node {id: '#{to}'}), path=((from)-[walk:WALK_TO*1..10]->(to))
    RETURN from, to, path,
    REDUCE(totalTime=0, w in walk | totalTime + w.time) as cost
    ORDER BY cost
    LIMIT 3
    """
  end

  ...
end

Bolt.Sips は Cyper query の DSL は用意せず、生クエリを書いて query! 関数で実行するスタイルでした。 僕は DSL 好きじゃないので良いと思いました。

また Bolt.Sips.Response はかなりネストが深い構造でしたが、Elixir のパターンマッチで、割とスッキリ処理できました。 実は久しぶりに Elixir を書いたのですが、やっぱり書き味が素晴らしいです😊

まとめ

Elixir/Phoenix + Neo4j で、お手軽かつ高速な経路探索システムを開発しました。 実際どれくらいの応答速度になるのかは、データを大量に入れて検証していきたいと思います。

Elixir はパターンマッチが強力で、データ処理言語としても有用であることは、fukuoka.ex の @piacere さんも言われている 通りだと思います。

僕はバックエンド出身の分析エンジニアとして、分析処理した結果を api でユーザーに配信して〜といったシステムを作りたくなる時もあるのですが、Elixir/Phoenix は一つの良い選択肢だと考えています。 耐障害性に優れる点も、スタートアップ企業のような開発リソースが潤沢ではない局面で、とても助かるんですよね。

ということで、久しぶりに Elixir に触れて楽しいクリスマスイブでした! 達郎聴いて寝よ🎅🎸

Looker ノススメ

はじめに

Looker Advent Calendar 2019YAMAP エンジニア Advent Calendar 2019 1日目の記事です。 (元々 Qiita に書いていたもの を、こちらに引っ越しました)

こんばんは、🔺YAMAP 分析チームの松本です。好きな山は赤岳(八ヶ岳)です。 YAMAP は 2019-11 から、次世代 BI ツール Looker を導入しました。

この記事では、YAMAP で導入前に感じていた課題とその解決方法を中心に、Looker の素晴らしさを紹介したいと思います。

参考までに、YAMAP の概況

Looker をはじめ、 BI ツール導入を検討している方の参考になればと思い、会社の規模などを共有してみます。

  • 社員数: 50 くらい(開発メンバーは半分くらい)
  • 分析チーム: 2019-01 に発足、現在 2 人
  • 主な事業: 有料会員 , 登山保険 , EC , 観光・アウトドア周辺の営業

Looker 導入前の課題と、どう解決するか

私達は、おおよそ以下のような課題を感じていました。

f:id:hidetakafm:20191205225905j:plain
before looker

それがこうなります、Looker 凄い 🎉

f:id:hidetakafm:20191205230031j:plain
after looker

以下でそれぞれ説明したいと思います。 なお LookML については、分かりやすくまとめて頂いている記事 等ありますので、そちらをご参照下さい 👀

a. データ基盤をみんなに使ってもらうには敷居が高い

私達は BigQuery にデータ基盤を構築し、2019-06 から運用を始めました。 (構築に当たっては 私の考えた最強のログ&モニタリング設計 - 下町柚子黄昏記 by @yuzutas0 がとても参考になりました。この場を借りてお礼を 🙏 )

ですがデータが1箇所に集まったのは良いものの、

  • 何がどこにあるのか難易度が高く、分析者以外が使うのに敷居が高い

という課題がありました。

どう解決するか?

これは 「分析者が LookML を定義すれば、それを元に Looker が UI が提供してくれるので、利用者はポチポチでグラフが作れる」 という特徴により、解決できます。

b. Redash でクエリが乱立、メンテが大変

私達はもともと Redash を使っていました。 Redash は無料ですし、とても素晴らしかったのですが、運用を重ねるにつれ、

  • 似たようなクエリが増殖
  • どれが使われているのか分からない
  • スキーマに変更があった際、参照しているクエリを修正して回るのが辛い

という課題が出てきました。

どう解決するか?

これも LookML が分析用 Object-relational mapping の如く、グラフとテーブルの間に入ってくれるので、テーブルに変更があったときは LookML を修正すれば、それを利用しているグラフを一括修正できる ということで解決できます。

また Looker には Look(グラフ) ごとのアクセス数を可視化する機能が備わっており、使われていない Look を整理するのにとても便利そうでした。

c. 人によってクエリの書き方が異なり、信頼性を担保しにくい

YAMAP では実際

  • SQL を書く人の技量により、間違った集計を行ってしまう
  • 技量が高くても分析対象のテーブルの細かい仕様までは把握しきれておらず、間違った集計を行ってしまう

といったケースが有りました。 (例えば、論理削除フラグや、契約履歴テーブルに "契約解除" でもレコードが挿入されるなどの仕様の把握漏れで、集計が水増しされてしまう. etc.)

こういう事が重なると、誰かがグラフを書いた時に「そもそも集計合ってるんだっけ?」といった確認作業が大変になるのも残念です。

どう解決するか?

これまた テーブル定義に精通した分析者が LookML を定義し、利用者はそれを使ってグラフを書く といった明確な分担ができることで、集計の信頼性が格段に担保しやすくなります。

d. 細かい集計依頼が多く、骨太な分析に時間を使えない

先に紹介したように、YAMAP は有料会員、登山保険、EC、営業と事業が多く、各チームから分析チームへ細かい集計依頼もコンスタントに来ています。

そして、YAMAP 分析チームは↓2つのミッションを掲げています。

  1. データ分析で事業の意思決定にコミットする
  2. ユーザーが投稿するデータを価値に変え、ユーザーに還元する

しかし先述したように現状分析チームは2人だけなので、各チームからの集計依頼が多くなると、2 に時間を使えなくなるという危機意識がありました。 (僕たちは 2 を "骨太な分析" と呼んでいます)

どう解決するか?

今まで書いたことでお分かり頂けると思うのですが、Looker は データの民主化 という視点で、とても強力です。 "LookML の定義に分析者が責任を持ち、利用者はそれを使ってグラフを書く" という切り分けができることで、データガバナンスとデータ民主化を両立できる 訳です。 今まで Looker を使ってみて、このバランス感は本当に素晴らしいと感じます 💜

ということで、Looker を各チームに浸透させ、簡単な集計や分析はチーム内で完結できるように持っていくことで、"骨太な分析" の時間も確保していく方針です。

加えて LookML はとても優秀で、クエリをかなり動的に生成してくれます。 故にお決まりの集計やデータ抽出業務は、グラフ(Look)の FILTERS パラメータを良い感じに用意してあげると、仕組み化しやすいです。 (Redash にもパラメータ機能はありますが、Looker はパラメータの選択状況に応じて JOIN 文レベルで切り替わってくれるので、柔軟に仕組み化できます)

まとめ

YAMAP での BI の課題と、それをどう解決できるのかを中心に、Looker をご紹介しました。

使えば使うほどに素晴らしいツールだと驚くのですが、特に

  • "LookML の定義に分析者が責任を持ち、利用者はそれを使ってグラフを書く" という切り分けができることで、データガバナンスとデータ民主化を両立できる

点がミソなのかなと感じています。

しっかり使いこなして、みんなの登山をより安全にしていきたいと思います。 🗻 明日の Looker Advent Calendar 2019, YAMAP エンジニア Advent Calendar 2019 もお楽しみに!

うぐいすオルゴール (Nightingale Music Box)

www.uguisumb.app

App

デモソング (Demo song)

  • アプリをインストール後に↓リンクをクリック (Click the bellow link after you install the App.)
  • canon

 操作方法 (How to)

  • 音符を作る/消す (Create/delete a note.)
    • テーブル上をタップ (Tap on the table.)
  • オクターブを上げる/下げる (Up/down the octave.)
    • テーブル上で左/右にスワイプ (Swipe on the table.)
  • 曲の出来具合を確認する (Check the song you making.)
    • テーブルを上/下にドラッグし、音を鳴らす (Play sounds by dragging the table.)
  • 曲が完成したので再生する (Play your song.)
    • 再生ボタンを押す (Tap the play button.)

f:id:hidetakafm:20150205001056j:plain