RareJob Tech Blog

レアジョブのエンジニア・デザイナーによる技術ブログです

オンライン英会話のDMPを支える技術

インフラエンジニアとして採用してもらい、4月からはマイクロサービスでの基盤を作るチームのリーダーをさせてもらってます塚田と申します。 引き出しの中はランニングのシューズや着替えやサプリやらで溢れかえっており、ランナー版の「釣りバカ日誌」のハマちゃんだと思っていただければよろしいかと思います。

3月までEdTech(Education 教育 とTechnology テクノロジー)の領域でデータをさらなる活用するべく、DMP(Data Management Portal)プロジェクトを立ち上げリードしてきました。 今回はEdTechの領域におけるDMPについてお話ししたいと思います。

DMPとは

自社サイトのログデータなどを一括管理、分析し最終的にアクションプランの最適化を実現するためのプラットフォームで、主に広告業界で使われるデータのプラットフォームです。

このDMPを構築する前にも、我々は独自の解析プラットフォームを持っていました。 しかし、様々な課題があったのでここで一度作り変える判断をしました

旧データ解析基盤の課題

レアジョブ英会話のサービスがはじまり、今年で12年目になります。 12年前にサービスがローンチして程なく旧データ解析基盤が作られ、アーキテクチャこそシンプルですが、そこに肉付けに肉付けが重なり、旧データ解析基盤は巨大肉団子状態になっていました。

f:id:sumito1984:20190513183313p:plain
旧データ解析基盤

なにより辛いのがソースコードにコメントが無いため、作り上げられた当時どのような思想のもとその計算式に至ったのか、今の担当者である我々から見てサッパリわからないという問題がありました。

また、レッスンで使った教材の統計や、売上情報、KPIなどの集計が日付が変わった瞬間バッチ処理として1つのサーバの上で大量にさばいておりましたが、 サーバはマルチコアではあったものの、集計が多すぎるため処理が直列になってしまい、何時間たっても処理が終わらないという課題がありました。

それでこの度、旧データ解析基盤を改修していくのではなく、根本的に作り直す方向に舵を切りました。

DMPのアーキテクチャ

アーキテクチャは以下の通り。参照先DB、計算処理、格納先、描写をそれぞれ分け疎結合に作り替えました。

f:id:sumito1984:20190513183220p:plain
rarejob DMP

当初データベースからデータを持ってきてBigQueryに入れるだけであればembulkで進めていたのですが 複雑なクエリになりすぎるメンテが難しくなるという課題があったので、中間データベースに一時的に出力させるようにしました。 登場人物は増えたものの、そのおかげで抽出処理のコードがシンプルになりました。

計算処理はコンテナベースで作っています。 コンテナイメージをECRへpushし、fargateのタスクスケジュールとして起動しています。 パスワードのようなセキュアな情報はAWS Systems Manager パラメータストアに保存しており、限られた人だけが見られるようにしています。 ちなみにタスクは以下のようなjsonで作っております。

{
    "executionRoleArn":"arn:aws:iam::123456789:role/DMP_RDS",
    "containerDefinitions":[
        {
            "logConfiguration":{
                "logDriver":"awslogs",
                "options":{
                    "awslogs-group":"/ecs/dmp-student",
                    "awslogs-region":"ap-northeast-1",
                    "awslogs-stream-prefix":"ecs"
                }
            },
            "entryPoint":[
                "sh"
            ],
            "portMappings":[

            ],
            "command":[
                "/var/www/dmp/execute.sh"
            ],
            "cpu":1,
            "environment":[
                {
                    "name":"ENV",
                    "value":"prd"
                },
                {
                    "name":"ORG_DB_USER",
                    "value":"dmp"
                },
                {
                    "name":"CLASSNAME",
                    "value":"Student"
                }
            ],
            "ulimits":[
                {
                    "name":"cpu",
                    "softLimit":0,
                    "hardLimit":0
                }
            ],
            "mountPoints":[

            ],
            "secrets":[
                {
                    "valueFrom":"DMP-DB-PASSWORD",
                    "name":"DB_PASSWORD"
                }
            ],
            "memory":2048,
            "memoryReservation":2048,
            "volumesFrom":[

            ],
            "image":"123456789.dkr.ecr.ap-northeast-1.amazonaws.com/dmp/extract",
            "interactive":true,
            "essential":true,
            "pseudoTerminal":true,
            "readonlyRootFilesystem":false,
            "dockerLabels":{
                "KeyName":""
            },
            "privileged":false,
            "name":"dmp-student"
        }
    ],
    "placementConstraints":[

    ],
    "memory":"2048",
    "taskRoleArn":"arn:aws:iam::123456789:role/DMP_RDS2RDS",
    "family":"dmp-extract-student-task",
    "requiresCompatibilities":[
        "FARGATE"
    ],
    "networkMode":"awsvpc",
    "cpu":"1024",
    "volumes":[

    ]
}

その後、以下のコマンドでタスクを再定義しています。

aws ecs register-task-definition --cli-input-json file://student.json 

DMPを構築する上での課題

参照先データ

DMPを作っていく上で、リアルなデータは必須になります。 本番の生のデータを元に開発したいものの、多くの開発者には見せたくないデータもあります。 直接本番環境に繋いでもらうわけにもいきません。 一方で、ステージングや開発環境で作るにしてもDMPが実行する膨大なSQLを実行すると、その他のプロジェクトに影響を与えてしまう恐れがあり、まずは環境を整理する必要がありました。

膨大すぎるレコード

データベースの中にはサービス開始時から蓄積された12年分のログがあります。 とにかく重く、indexを貼っても許容できるまでのレスポンス速度まで改善されません。

既存のデータベースの複雑性

やはり12年も継ぎ接ぎしてきたデータベースはとにかく複雑になっています。 同じようなデータを複数箇所に持っていて、どっちが正しいかわからない状態のものもあります。

fargateでのデバッグ

日付がかわったタイミングで大量の処理を行いため、EC2のように処理の数だけサーバを立てて処理するようでは、処理していない間コストがかさみます。 そのためfargateを用いて処理しているときだけ課金させるようにしました。 しかし、fargateはコンテナインスタンスを管理しないため、コンテナインスタンスSSHし、docker execなどでコンテナ内部でシェルを起動することができません。

解決したこと

参照先データ

本番環境のデータベースのバックアップを取得し、見せたくない情報をマスキングした上でスナップショットを取得しました。 ステージング環境へ毎日スナップショットを共有させ、ステージング環境で復元するようにしました。 こうすることで開発者がマスキングされたデータを参照することができ、リアルな数字がステージング環境でも見えるようになりました。 また、この復元したデータベースはDMPでのみ使うものとしました。仮にどんなに重いクエリを投げても業務影響は与えません。 データの解析は実際のデータを元に動かさないと正しいのか判断できないことがあるため、この手法は有益でした。

膨大すぎるレコード

データベースの作り直しを行い根本対応とするべきと考えましたが、今回そこまでの時間と労力は割けられません。(それに直したいところは他にもたくさんあります) そこでDMP用に解析に必要な分だけ抽出し、新規テーブルを作り必要なだけinsertさせ、そちらを利用することにして高速化を図りました。

mysql -u$dbuser -p$dbpass -h$dbserver $databasename -e "CREATE TABLE registration_recent LIKE registration";
mysql -u$dbuser -p$dbpass -h$dbserver $databasename -e "INSERT INTO registration_recent SELECT * FROM registration where lesson_date > (CURRENT_DATE() - INTERVAL 30 DAY)";

既存のデータベースの複雑性

同じようなデータが複数ある場合、とにかく過去のロジックを洗うしかありません。

しかし、先々これでよいか疑問に思っています。このままSQLを駆使してデータベースを検索していくのではなく、 マイクロサービスで箱を用意し、イベントドリブンでデータを入れていくよう抜本的なところから構成を変更していきたいと思っています。

fargateでのデバッグ

コンテナに穴を開け、sshを許可させるやり方も頭をよぎったのですが、コンテナの思想に合ってないと思いそれは行いませんでした。 現在行なっているのはCloudWatchでログを出力させ、さらに詳細の情報が欲しい場合は同一セグメントにEC2インスタンスを立てて、fargateで動かしている同じコンテナを動かし、docker execすることで再現させるようにしています。

なぜこんなに頑張るか

外国語の習得はすぐにマスターできるようになるようなものではなく、マラソンのトレーニングのように習慣化させ長期的に取り組んでいく必要があります。

村上春樹さんの著書「走ることについて語るときに僕の語ること」からの抜粋です。

大事なのは時間と競争をすることではない。どれくらいの充足感を持って42キロを走り終えられるか、どれくらい自分自身を楽しむことができるか、おそらくそれが、これから先より大きな意味をもってくることになるだろう。

つまり、いかに利用者に充足感を与え、楽しみながら英語学習というマラソンを走り切ってもらうことが、我々が提供すべきことになると思ってます。 英語学習者がランナーであれば、我々は伴走者であり、時にはコーチでありたいと思ってます。 オンライン英会話のパイオニアとしてデータは十分手元にある。データの中に答えがある。故に我々はDMPを軸にデータを活用していくことで、利用者に充足感を与え走り切ってもらうことができると考えています。

また、現在はまだDMPは社内だけに閉じてプライベートなものとして使っていますが、将来的には協業でのシナジーなども考えるとパブリック向けにも作り公開するという未来もあるかもしれません。 そのステージになると今以上にデータが集まり、rarejobのサービスミッションである「日本人1,000万人を英語が話せるようにする。」の実現を早め、新たな局面で英語教育に貢献できると考えています。