GENIEEエンジニアブログ

株式会社ジーニーのエンジニアがアドテクノロジー・マーケティングテクノロジーなどについて情報発信するブログです。

アドフラウドに対する技術的な取り組み

はじめに

こんにちは、R&D本部アドプラットフォーム開発部の加瀬です。

2018年も残すところあと1ヶ月となりましたが、みなさんいかがお過ごしでしょうか。
振り返ってみると「平成最後の年」ということもあってか、2018年も世間は話題に事欠かない年だったかと思います。

さてさて、アドテクの視点で2018年を振り返ってみると、テレビ番組などでも取り上げられていたように、アドフラウドがメイントピックとして取り上げられることが多かったかと思います。

アドフラウドを取り巻く状況

不正な広告掲載・配信に関しては昔からぼちぼちあり、例えば

  • 見えないページを設置したり
  • ボットに巡回させてトラフィックを水増しさせたり
  • 強制的にリダイレクトさせてみたり

いろいろありますが、先述した2018年の状況に伴い広く認知されるようになった気がします。

また、日本のデスクトップ(PC)におけるアドフラウド率が81%だとか、年100億円の被害だとか、どうも日本ではアドフラウドとなじみが深いのは数字としても出ているようです。
(数字の実際のところ、という観点はありますが。。。)

■参考:

ご多分に漏れず、弊社でもサプライ・デマンド問わず、アドフラウド・不正広告の被害に遭遇することがあります。

黙って指を咥えているわけではありません。

みなさんは「NO MORE 映画泥棒」をご存知でしょうか。
映画上映前のCMで、劇場内での映画の撮影・録音が犯罪である、ということを啓発するためのあれです。
こちらからキャッチコピーを一部拝借して、「アドフラウド・不正広告許さないぞ」と言う旗の下にアドフラウド・不正広告の調査・検知に日々勤しんでおります。

ということで、弊社での取り組みを簡単に頭出しの部分ではありますが、

  • メディア・ユーザー向けの取り組み
  • 広告主向けの取り組み

をご紹介します。

メディア・ユーザー向けの取り組み

メディア・ユーザー向けの技術的な取り組みとして、リダイレクト広告への対策を説明します。
遭遇した方もいらしゃるかもしれませんが、リダイレクト広告はWebページを閲覧中に強制的に意図しないページへの遷移をさせるものを指します。

そこで対策として

  • DSPから返却される広告タグの定期的な自動チェック
  • ブラウザ側での(ほぼ)リアルタイムチェック

を行なっています。

広告タグの定期的なチェック

悲しいことに、DSPからレスポンスされる広告タグが悪意のないものだとは言い切れなかったりします。
そこで

ということをシステム的に行なっています。
これにより、リアルタイムではありませんが強制リダイレクトが起きたDSPとその広告タグを特定し、停止作業を行うことにより被害を最小限に防ぐことができるようになります。

ブラウザ側での(ほぼ)リアルタイムチェック

なるべくリアルタイムで起こっているものを早くキャッチしたい、という要望に答えて「JavaScriptで強制リダイレクトの検知ができないか」という取り組みを行なっています。
JavaScriptにはページ離脱をしたときに発生するbeforeunloadなどのイベントがあり、それをevent listenerでハンドリングすることで検知できないかを試みました。
が、一筋縄ではいきませんでした。

ページ離脱にはいくつかパターンがあり、一例を挙げると

  • リンクのクリック
  • URL直接入力による遷移
  • 戻る/進むボタンによる遷移
  • ページのリロード

など「ユーザーの能動的なアクションによって発生するページ離脱をどのように判断するか」という点が課題としてありました。

結局、

  • click、history、focus、blurなどのイベントハンドリングを愚直に行いユーザーのアクションと切り分ける
  • 上記をすり抜けてきたものに関してログを書き、バッチで集計する

という結論にいたりました。

ある程度の誤検知は防げるようになったのですが、まだ一部把握しきれていないパターンがあるみたいなので、今後の課題として取り組みたいと考えています。

広告主向けの技術的取り組み

広告主向けの取り組みとして、テレビ番組でも取り上げられた「隠し裏広告」のほかに、名前が似ていますが「隠し広告」への対策も紹介させていただきます。

隠し広告の配信検知

実は昔からよくある手法でして、0x0サイズのiframeの中に別のウェブページを書き出したり、広告だけのページを作りそれを書き出したりするやり方が多いです。

この時

が簡単にできてしまうというわけです。

著作権問題のあるサイトや、昔のまとめサイトでもよく見かけたお馴染みの手法だったりします。
手法の性質上、ユーザーの目に触れるとバレてしまうので、明らかにおかしなサイズのiframeの中に書き出します。
そのためJavaScriptでwindowサイズを取得し、適当に閾値を決めればある程度対応できる残念な手法だったりもします。

隠し裏広告の配信検知

隠し裏広告は、古くからあるポップアンダーと呼ばれる広告の配信方法の延長です。
結局のところ仕組みとしては

  1. 広告タグが呼び出される
  2. 広告タグのクリックURLを取得する
  3. ページ上のaタグのクリックイベントとして
    • location.href = 2のリンクを登録
    • window.open(正しいリンク)を登録

することにより

  • 前面に本来のリンク先
  • 裏面(もともとのwindow)に広告のリンク先

を表示させるものでした。

隠し裏広告は、ポップアンダーにおける広告のリンク先を別のメディアのURLにすることで

  • 前面に本来のリンク先
  • 裏面に別のメディアのURL

を表示することにより、トラフィックの融通や広告の不正表示を実現します。
つまり「ページが前面にあるかどうか」という点がポイントとなってきます。

都合のいいことにJavaScriptではdocument.visibilityStateで現在のページが「見える状態かどうか」を判定することができます。

なので

  • 広告タグの書き出し
  • document.visibilityStateで現在の状態を判定
  • hiddenの場合だとオークションに行かない、もしくは何かしらのビーコンを書き出す

とすることで、ある程度の正しくない状態における表示の検知/停止が可能になると考えています。

終わりに

以上、

  • アドフラウドを取り巻く現状
  • 弊社での取り組み

を簡単ではありますが紹介させていただきました。

改めて振り返ってみるとアドフラウドにも色々な種類があり、各々に対する対策もそれに従って生まれてきている状況だということが分かるかと思います。

現状はバナー広告のクリック課金によるものが多いのですが、今後、特に動画のような高単価かつ視聴による課金を求めるようなものに関しては、不正なインプレッションが発生するだけで更に大きな被害になることが予想されるので、引き続き検証と対策を行なっていきます。

ありがとうございました。

MySQLのbinlogを使ってSSPの配信設定反映を40倍速くした話

はじめに

こんにちは、R&D本部アドプラットフォーム開発部の村岡です。

九州工業大学の先端情報工学専攻を予定通り修了してジーニーに17卒入社し、現在は主にGenieeSSPの開発を行っています。

以前こちらの記事を書きましたが、今回もMySQL関連の記事となります。

GenieeSSPについて

GenieeSSPは、広告配信のレスポンスタイムを短くするために、数十万の広告枠の配信設定をすべてインメモリで保持しています。

全広告枠の配信設定はMySQLに保存されています。配信設定を変更する、つまりDBのデータを変更する方法は、現在の運用では4つあります。

  • 営業担当や、広告運用チームなどが操作画面を使って更新する。
  • 操作画面では対応できない場合などに、エンジニアの運用チームが手作業で更新する。
  • 配信パラメータ最適化のためのバッチが更新する。
  • リリース時などにエンジニアが権限をもらって更新する(平時は更新する権限がない)。

また、GenieeSSPは広告枠単位で配信設定をキャッシュしており、広告枠に紐づく何かしらの情報が変更となった場合は、その広告枠の情報をすべて再取得しキャッシュを更新します。

以上の事情から、GenieeSSP配信サーバが使用するMySQLのテーブルにはトリガーが設定されており、あるテーブルのレコードが変更された場合に、どの広告枠が変更となったか、わかるようになっています。

f:id:tech-geniee:20171225190804p:plain

数十台あるGenieeSSP配信サーバの全てが、全広告枠の配信情報をインメモリキャッシュとして保持しており、各サーバが定期的にrefresh_counterテーブルのカウンターが更新されているか調べるクエリを投げ、どの広告枠を更新すべきか調べています。

GenieeSSPの課題

このようなキャッシュ更新の仕組みでは、ある広告枠の配信設定がひとつだけ変更となった際に、紐づく配信設定全てを再取得しなおさなければならない、という問題があります。 配信サーバが使用するテーブルは60程度あり、一つの広告枠キャッシュを作成するために数百のクエリを投げなければなりません。

このキャッシュ更新の処理が重く、バッチによる大量更新が行われた際には、最悪2時間程度の間、配信設定が配信サーバに反映されないままになってしまう、という深刻な問題がありました。

今回、DBに配信設定が保存されてから、配信サーバに反映されるまで5分以内にする、という目標のもと、GenieeSSPの長年の課題であったこの配信設定反映遅すぎ問題を解決しました。

実装案

設計段階で出た実装案について、各案についてどのように実装しようとしていたか、なぜその案が採用されなかったか、最終的に選ばれた実装案はなぜ採用されたかについて説明します。

実装案を考えるときに重要だったポイントは、

  • テストが行えるような方法であること
  • 既存のテーブルのトリガーを使用しないような方法であること
  • 既存の運用方法で配信設定の反映が行われること

でした。

テストには、GenieeSSPが定期的にJSON形式でファイルに書き出す、全広告枠の配信情報が使えます。 これは、GenieeSSPがクラッシュしたとき、JSONファイルがないと起動に二時間かかってしまうため、JSONから配信情報をロードすることで起動時間を短縮しています。 高速化後の実装と古い実装で、このJSONファイルを出力させ比較すれば容易にテストが行えます。

案1:updated時間を見る

この実装案は、一つの広告枠の情報を取るのに数百のクエリを投げたくないなら、テーブルデータまるごと落としてしまえ、というかなり力技な案です。

まず、前準備として、DBの各テーブルに更新時間を記録するカラムを追加します。カラムの更新時間は自動で更新されるように、以下のように定義します。

updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP

GenieeSSPが起動するとき、配信に必要な約60のテーブルデータを全て取得します。 テーブルデータを全てメモリに載せても10GBも使用しないため、今後、データが増えることを考えてもメモリ使用量としては全く問題になりません。

そして、キャッシュした各テーブルデータをもとに広告枠情報を構築するようにします。

キャッシュ更新は、以下のようなクエリを定期的に投げて、各テーブルに追加したupdated_atカラムを見るようにします。

SELECT * FROM table WHERE updated_at > "最後のポーリング時刻";

updated_atカラムの時刻が、以前更新したときの時刻から更新されていれば、データが変更されたとわかります。 更新されたレコードから、キャッシュの再構築が必要な広告枠だけ、再構築を行うようにします。

この実装案は仕組み自体はとてもシンプルですが、採用されずお蔵入りとなりました。 理由は、テーブルのレコード削除がわからない、という致命的な問題があったためです。

案2:操作画面変更時に配信サーバに通知する

この実装案は、操作画面で配信設定が変更されたら、配信画面側でJSONファイルを生成し、RedisかAerospikeに保存する、という案です。

GenieeSSPは、定期的に保存場所をポーリングし、更新された広告枠についてJSONをダウンロードしデシリアライズしてキャッシュを更新します。

GenieeSSP側の変更はほぼ必要なく、操作画面側でJSONを作る処理を追加する必要があります。

この実装案が没となった理由は、操作画面以外からDBを更新された場合に対応できない、操作画面のコードの保守が大変になってしまうためです。

案3:MySQLのBinlogを使う

この実装案は、案1をベースに、DB更新の検知をMySQLが出力するbinary log(binlog)を見るようにする、というものです。

まず、起動時に約60のテーブルデータ全てを、通常のSELECTクエリで取得します。 その後、MySQLサーバにスレーブとして接続し送られてくるbinlogを処理し、インメモリのテーブルデータを更新していきます。 MySQLにクエリを投げるのは起動時のみ、あとは送られてくるbinlogを処理するだけです。

binlogをハンドリングするためのライブラリとして、MySQL Labsで公開されているlibbinlogeventsを使用します。

案1と同様に、更新されたレコードから、キャッシュの再構築が必要な広告枠を計算し、キャッシュの再構築を行うようにします。

この実装案は、experimentalなライブラリであり、社内社外ともに実績があまりないlibbinlogeventsを使用するため、本当にうまくいくのか疑問ではありましたが、以下の点でメリットがあり採用に至りました。

  • 他の案と異なり既存の運用にも対応できること
  • 配信設定の反映時間は最速が狙えること
  • 変更がpushされるため、ポーリングするより効率的
  • 何より純粋にbinlogを使う実装が面白そう

実装

GenieeSSP配信サーバが、experimentalなライブラリを使用してMySQLサーバにスレーブとして接続するのはあまりにもリスキーです。何かあったときに、配信サーバが全滅しかねません。 GenieeSSPは幸いにも、JSONファイルから配信設定をロードする機能があるため、これを利用することにします。

具体的には、MySQLサーバにスレーブとして接続しbinlogを処理、JSONファイルを生成する配信キャッシュ作成サーバを開発します。

DBのレコードが更新されると、以下のような流れで配信設定が反映されるように実装を行います。

f:id:tech-geniee:20171226140727p:plain

現状では、更新されていない広告枠も含む、全ての広告枠情報をJSONに出力しています。 これにより、テストが容易になる、実装が容易になる利点があります。 ただし、配信キャッシュ作成サーバから、JSONを各GenieeSSP配信サーバに転送するときはサイズが大きいため時間がかかってしまいます。

Zstandardで圧縮することでファイルサイズを98%削減できましたが、プロダクション環境ではZstandardが新しすぎるためにパッケージがない・バージョンが古く、自前でビルドしなければならなかったため、一旦LZ4で圧縮しています。 LZ4の方が少し処理時間は長く、ファイルサイズを94%削減できています。

配信キャッシュ作成サーバ

クラスをJSONシリアライズする部分など、流用できる/流用したいコードが多くあること、libbinlogeventsがC++だったことなどから、配信キャッシュ作成サーバもC++17で開発しました。

LL言語のほうが良いんじゃないか、とも言われましたが、

  • libbinlogeventsのバインディングを書くことがめんどくさい
  • 大量にメモリを使うのでマークスイープ系のGC走ると止まる時間長そう
  • シリアライズ部分とか並列化したいけどプロセス並列やりたくない
  • そもそも速度が最重要なのでC++でいいじゃん

という理由でC++です。 個人的にはRustを使いたかった気持ちもありますが、Rustは別の仕事や趣味で使うことにします。

処理フロー
f:id:tech-geniee:20171226175044p:plain

約60あるテーブルデータを保持するクラスを手で書くのは流石に大変なので(一度手で書こうとして止められた)、コードジェネレータを作りました(後述)。

GenieeSSPから流用したコードは"広告枠の情報を再構築、再シリアライズ"する部分となります。 当然ですが、GenieeSSPが投げていたSQLクエリは流用できないので、全てC++で書き直して実装しました。

C++で書き直したので何をしているのかわかりづらい…と思うかもしれませんが、元のSQLクエリが既に意味不明な部分も多かったため、全体としてそれほど読みにくくなってはいないように思います…が、SQLクエリのままのほうがやはり簡潔で良いと思います。

コードの生成

キャッシュ生成に必要なテーブルのテーブル名、及び、必要なカラム名などを記述したJSONファイルをもとに、MySQLサーバにSHOW CREATE TABLEクエリを投げてカラムの型情報、カラムインデックスなどを取得し、C++のコードを生成するコードジェネレータが必要となり作りました。

コードジェネレータを作ることで、あとからデバッグのための関数追加、コードの修正などが容易に行えるようになり開発がスムーズに進みます。

また、binlogからカラムデータ取得、MySQLにクエリを投げてデータを取得するコードは、JSONファイルにテーブル名、カラム名を追加しコードジェネレータを叩くだけで全て生成されるため、メンテナンスも容易になります。

今回はJinja2テンプレートエンジンを使いましたが、扱いやすく、テンプレート中で制御構文など使えて簡潔にテンプレートを書くことができました。

binlogのハンドリング

大真面目に実装していけばMySQLが作れます。

今回はその必要はないので、以下のイベントのみハンドリングします。

  • TABLE_MAP_EVENT
  • XID_EVENT
  • UPDATE_ROWS_EVENT
  • WRITE_ROWS_EVENT
  • DELETE_ROWS_EVENT

TABLE_MAP_EVENTは、ROWベースのbinlogにおいて、全ての行操作イベントの前に表定義とその後のイベントの値をマップするイベントです。 このイベントを見ることで、DB名、テーブル名、カラム定義などがわかります。

XID_EVENTは、テーブルの変更をするトランザクション処理がCOMMITされると発生するイベントです。 このイベントを見て、中途半端なところでbinlogを処理しないようにする必要があります。

UPDATE_ROWS_EVENTWRITE_ROWS_EVENTDELETE_ROWS_EVENTはそれぞれ、レコードがUPDATEINSERTDELETE時に発生するイベントです。

おそらく、QUERY_EVENTをハンドリングすることでテーブルのALTERなどもわかるはずですが、試したことはないです。 配信キャッシュ作成サーバでは、テーブルをALTERするときは新しいテーブル定義を使ってコード生成しなおし、再リリースすることにしています。 テーブルのALTERはめったに行わないため、この運用で問題ありません。

スレーブとしてMySQLサーバに接続したあと、binlogのポジションを設定する必要があります。 binlogポジションはSHOW MASTER STATUSコマンドを投げることでわかります。 SELECTクエリでテーブルデータを取得したあとは、binlogによる更新となるのでbinlogポジションはちゃんと設定してあげなければデータの整合性がとれなくなります。

今回は、MySQLサーバにSTOP SLAVEして、テーブルデータとbinlogポジションを取得、MySQLサーバにスレーブとして接続してからSTART SLAVEするようにしています。 そのため、配信キャッシュ作成サーバ専用のMySQLサーバを動かしています。

libbinlogevents ver1.0.2を使用しましたが、このライブラリには重大な問題があります。 それは、

  • Decimal型のカラムの値を正しく取得できないこと
  • DateTime型のカラムの値を正しく取得できないことがあること

です。

これらの問題は、libbinlogeventsにそれらのデータを処理するコードがないことが原因です。 この問題を修正するには、MySQLソースコードを漁って、MySQLがbinaryフォーマットからdoubleDateTimeに変換している部分のコードを移植してやる必要があります。

どちらもすぐに処理を行っているコードは見つかります。 MySQL本体とべったり書かれているわけではないので、移植もわりとやりやすいと思います。

特にDecimalについては、内部でどういう風に扱われているのか全く知らなかったため、MySQLソースコードとコメントを読むのは楽しかったです。

テスト

テスト環境に配信キャッシュ作成サーバ専用のMySQLサーバを置きます。 このサーバは本番サーバとレプリケーションするようにしておき、このMySQLサーバに古いGenieeSSPと配信キャッシュ作成サーバを接続し、一定時間ごとにレプリケーションを止めて、生成されたJSONを比較します。

JSONの比較のためのツールをPythonで書きましたが、やたらとメモリ食いで40〜50GB程度使っていました。 しかもGCが走ると数十秒平気で止まるので、なかなかテストが進みません…仕方ないですね。

2〜3週間程度、自動でテストが動くようにして、毎日差分をチェック、問題があれば調べて修正を繰り返しました。

最後の一週間は問題のある差は出なかったため、プロダクション環境に一台だけリリースして一週間程度様子を見てから全台リリースを行いました。

リリース後

配信設定反映まで、最悪2時間かかっていましたがリリース後は約3分程度となっています。 3分の内訳は、

  • Binlog処理・JSONシリアライズ…1秒
  • 全広告枠分のJSONRamdiskに書き出し…5秒
  • LZ4で圧縮…30秒
  • 転送…30秒
  • 配信サーバのJSONロード…平時10秒程度
  • 残りは、ファイルの転送はcronで毎分行われているためそのディレイと、配信サーバが新しいJSONファイルを見つけるまでのディレイ

となっています。

binlog周りで問題が発生しても、MySQLに通常のクエリを投げてテーブルデータを全件取得するのにかかる時間は1分程度なので、毎回全件取得で更新できるようにもしています。 この場合は更新に少し時間がかかるようになりますが、5分以内更新は達成できます。

f:id:tech-geniee:20171225192938p:plain

上図はリリース後のMySQLサーバの負荷です。データの転送量、CPU負荷が格段に下がっています。

f:id:tech-geniee:20171225192948p:plain

MySQL Enterprise Monitorで見られる、受け付けたクエリの統計です。 リリース後は、赤線のSELECTクエリが圧倒的に減っています。

f:id:tech-geniee:20171225192927p:plain

上図は配信キャッシュ作成サーバの負荷です。配信キャッシュ作成サーバのCPU負荷は高くありません。 定期的に走るバッチによる大量更新でもそれほど負荷は上がらず、更新できています。

ただし、テーブルの全データに加え、数十万の広告枠のシリアライズ済みのJSONデータを保持しているため、メモリ使用量は多く25GB程度使っています。 25GBに、数GBのJSONファイルをRamdiskに保存するため、場合によってはメモリ64GBのサーバでもカツカツだったりします。

ネットワーク帯域もかなり消費していますが、これは今後、差分更新の仕組みなどを入れることでかなり改善すると思われます。

おわりに

9月頃に設計を開始し、libbinlogeventsが使えるかの検証、コーディング、テスト、ドキュメントなども書いたりしていたら、リリースが12月になってしまいました。

とりあえずリリースしてから、二週間経っていますが、大きな障害は起きておらず快調に動いています。

お客様等からの声はまだほとんど届いていないのでわかりませんが、今後開発を行っていき、広告配信のパラメータを高頻度に更新していける環境ができあがったと思っています。 今まで、この性能の問題から更新頻度が低く抑えられていたものなど頻度を上げてパラメータを最適化していければ、メディア様の収益もあげていけるでしょう。

とりあえず、二ヶ月程度様子を見て、テーブルのトリガー削除や、旧GenieeSSPの不要コードの完全削除などを行う予定です。 その後は、差分更新の仕組みを入れてより高速に配信設定を反映できるようにしていきたいと思っています。

正直、タイトルの"binlogを使って"は高速化のキモではなく、MySQLにクエリを投げる回数を減らしてC++で処理するようになったら速くなった、という感じではあります。 ただ、MySQLのbinlogを使う、という経験はなかなかないと思うので、これはこれで楽しい経験と思っています。

libbinlogeventsを使う上で、以下の2つの記事には大変助けられました。この場を借りてお礼申し上げます。ありがとうございました。

qiita.com

www.slideshare.net

また、開発時、リリース時、モニタリングの仕組み構築など私のメンターの伊藤さんには大変助けて頂きました。この場を借りてお礼申し上げます。ありがとうございました。

ジーニーのエンジニアが勉強会へ登壇しました

こんにちは、人事の見並です。前職で6年システム開発の現場に携わり、現職ではキャリアチェンジをして4年人事をしています。今回は、ジーニーのエンジニアが登壇した勉強会を2つレポートします。

はじめに

勉強会のレポートをする前に、ジーニーの事業全体を簡単にご紹介いたします。

f:id:tech-geniee:20171127102853p:plain
ジーニーの事業全体図

アド・プラットフォーム事業

創業から事業の根幹を作っていったのが、広告配信を行っているアド・プラットフォーム事業です。1秒間に数十万回ものリクエストをリアルタイムに捌き、最適な広告配信を実現しています。

マーケティングオートメーション事業

マーケティングオートメーション事業は昨年度から開始した新規事業です。多様化するマーケティング活動を最適な形で自動化し、データドリブンな取り組みを実現させるためのプラットフォームを目指しています。

これらの両プロダクトをインフラからアプリケーションレイヤーまで、60名以上のエンジニアが内製で構築しています。

アド・プラットフォーム事業の登壇レポート

LINE社が定期的に開催している技術者向けミートアップ「LINE Developer Meetup」へ、CTOの篠塚が登壇しました。「広告技術」をテーマとした回に、Supership社やLINE社の方々と共に、アドテクノロジー領域でのコアな取り組みを発表しました。

f:id:tech-geniee:20171127103742j:plain

f:id:tech-geniee:20171127103813j:plain

篠塚は「世界分散配信システムとレポーティングシステム刷新のお話」と題し、一度大きな作り直しを経験した後、現在はアジアを中心に国内外で大規模なトラフィックを捌くまでに成長したGenieeSSPのアーキテクチャやその変遷、直近取り組んでいるチャレンジなどをお話ししました。

海外に置いているサーバとの通信やレプリケーションの実現方法、事業の急拡大に伴う技術課題へ取り組んだ事例などに対して質問も多くいただきました。

マーケティングオートメーション事業の登壇レポート

オプト社にて開催された「マーケティング事業の開発現場でリアルに使われるJS事情」と題した勉強会にて、マーケティングオートメーション開発部の若手エンジニア2名が登壇しました。

近年活用が広がっているAngular.js , Vue.jsといったJavaScriptフレームワークの実際の活用事例を発表する形で、ウィルゲート社やオプト社の方々と登壇させていただきました。

f:id:tech-geniee:20171127104042j:plain

f:id:tech-geniee:20171127104053j:plain

「MAJINを支えるフロントエンド技術」というタイトルで、MAJINのアプリケーション開発で活用をしているVue.jsの開発事例を発表しました。

現在のアーキテクチャに至るまでの変遷や背景、選定理由など開発現場で様々な課題に直面したエンジニアとしてのナレッジを共有できたように思います。

今後も積極的に発表いたします!

社内で取り組んできた事例や新しく取り入れた技術など、今後も積極的に発表する機会を作っていきたいと考えていますので、ぜひ共同開催などお声がけいただけると嬉しいです!

WebのHTTPS化とMAJIN

こんにちは。R&D本部マーケティングオートメーション開発部の杉野です。

今回はMAJINのHTTPS対応についてお話していこうと思います。

HTTPS化のトレンド

昨今はWebのHTTPS化が強いトレンドとなっており、企業が運営するWebサイトのHTTPS化が進んできております。 最近のブラウザベンダの施策により、WebサイトをHTTPS化しない場合、以下のようなデメリットが発生することとなります。

  • ChromeFirefoxといった大手ブラウザは、HTTPSでないページにある入力フォームに対して警告を表示する*1*2
  • GoogleHTTPSで暗号化したサイトの掲載順位を引き上げることを公表*3している
  • HTTPSでないWebサイトからは、HTTPSで暗号化されたサイトのリファラを取得できない
    • リファラはお客様のページ遷移を知り広告効果などを測定するために用いられています
  • 次世代の高速通信規格であるHTTP/2は、現在対応している全てのブラウザでHTTPSの場合のみをサポートする

もちろんこれらのデメリットがあることは別としても、MAJINはお客様の情報を参考にマーケティングの最適化を行う都合上、暗号化を通さずに通信するべきでない内容を取り扱っております。 そのため、通信内容を暗号化しておくことは重要な責務だと認識して対応を行っています。

MAJINタグにおけるHTTPS設定

証明書

MAJINの各種機能を利用するために使用するMAJINタグは、デフォルトでHTTPSで通信するように設定されています。 MAJINのHTTPS通信に使われている証明書はブラウザで確認することができますので見てみましょう。

f:id:tech-geniee:20171023142757p:plain

ご覧の通りこの証明書の発行元はAmazonとなっております。

MAJINのサービスはAWS上で動いており、この証明書はAWSのサービスの1つである証明書取得サービスで取得・設定したものです。 当然ながら有効な証明書となっていますので、MAJINタグはHTTPSによる通信で取得することが可能です。

正しく強度が出るHTTPSの設定

HTTPSは単純に証明書を設置してデフォルトの設定で運用するだけでは、セキュリティ上の問題が残る場合があります。

例えばプロトコルのSSL3.0を有効にしたままでは、POODLEという攻撃手法のターゲットとなります*4

また、RC4などの仕様上存在する弱い暗号を有効にしたままでは、暗号を解読され通信内容を盗み見られる可能性もあります*5

このような設定は知識がないと正しく行うことが難しいのですが、 Firefoxの開発元であるMozillaが、サーバ毎に最適な設定を行えるツールを公開している他、 Qualys SSL Labsでは、HTTPSの設定をしたページのセキュリティレベルを診断するツールを公開して います。

こちらの診断ツールでMAJINタグのセキュリティレベルを調査した結果が以下となります。

f:id:tech-geniee:20171023143222p:plain

十分なレベルを確保していることがおわかりいただけるかと思います。

HTTPS化が必須なサービス

また最近では、HTTPS化が前提の先進的サービスも増えてきています。

最初に説明した通り、高速通信規格であるHTTP/2はHTTPSでなければ有効化できません。 また、MAJINで提供しているサービスの1つで使用しているWebPushも、そのようなサービスの1つです。

WebPushは、スマートフォンアプリで使用されている「Push通知」を、Webブラウザでもで行えるようにするものです。 アプリ開発なしにPush通知を送信できるということで、新機能ながらお客様に好評を頂いているのですが、こちらの機能は導入に際してページのHTTPS化が必須となっています。上記のとおりMAJINはHTTPS化してあるため問題ないのですが、導入先のページもHTTPS化する必要があります。

元々HTTPS化してあるサイトであるか、もしくはHTTPS化の予定があるサイトであれば問題なく導入でき、こちらの方がオプションが豊富なのですが、 中にはどうしても導入サイトのHTTPS化はできないが、WebPushを導入したいというお客様もいらっしゃいます。

そういった場合には、MAJINでWebPush登録専用のHTTPS化されたページを設定し、お客様のサイトからはこのページに誘導するリンクを設置することで、MAJINが管理するページでWebPush登録を代行する、という回避策を取っています。

まとめ

MAJINでは、お客様の情報を守るため、および先進的なサービスを提供するために、積極的なHTTPSの導入を行っております。情報セキュリティ分野は日々情報が更新されるため、現在MAJINに施している設定も半年後には古いものになっているかもしれません。

そのような中でも最新の対策を適用し、安心して情報を任せていただけるようなサービスを提供し続けられるよう、日々努力しています。

Redashの活用とカスタマイズ

こんにちは。R&D本部経営情報システム開発部の友安です。

経営情報システム開発部では、社内システム・プロダクト横断でのお金周りの開発や監査対応、社内のコミュニケーション効率化など、多岐にわたり社内の問題解決に努めています。

Redash

f:id:tech-geniee:20170831161044p:plain

ジーニーでは広告配信成果の分析、定形のデータ抽出作業の自動化にOSSのBIツールであるRedashを使っています。

既に各所で紹介されていますが、Redashは様々なデータソースに対するクエリを作成し、それらと実行結果を共有できるWebアプリケーションです。以前の記事で紹介したclickhouseもデータソースとして使えます。クエリの実行結果をチャートやピボットテーブルにするビジュアライゼーションの機能も豊富です。

導入によって、データ分析の促進や定形のデータ抽出作業の効率化といった効果が出ています。

また、クエリにコメントをつける機能でクエリの意図(なぜこのようなクエリを書いたか・このテーブルは何かなど)を記載することによって、他チームの業務やテーブル設計のナレッジ共有にも繋がっています。

潜在的な要望への気づき

Redashの存在が社内でじわじわと広がると、「実はこれを一括で見られると嬉しい」とか「こういうデータを裏で持っているなら見たかった」といった要望が次々と出てきました。

今はクエリを書く非エンジニアのメンバーも現れてくれて、データを欲しい人がデータを出すということが増えました。

カスタマイズ

利用が増えると、「こうだったら良いのに」という要望が徐々に出てくるようになりました。 RedashはOSSなので、自分たちで実装することにしました。
実装した機能を以下で少し紹介します。

スケジューリング強化

Redashには、クエリの実行結果の更新スケジュールを設定する機能があり大変便利なのですが、 より細かいスケジューリングをしたいという要望が出てきました。 Redashの更新スケジュールは、毎日X時更新や7日間ごとといった単位で設定します。 しかし、週2回更新してモニタリングしたいなど、 Redashの既存のスケジューリング機能では対応できない要望がありました。 そのような細かいスケジューリングに対応するため、月・木曜日更新や月初更新などの設定ができるようにしました。

この機能は本家でプルリク が出ていたのですが、開発が止まっていました。

そちらを元に実装しました。

f:id:tech-geniee:20170728105655p:plain

クエリの実行結果フィルタの全選択/解除

Redashにはクエリの実行結果をフィルターする機能があります。
フィルタする値を選択する際に、全選択/解除のオプションを使えるようにしました。 やや小さめの改修ですが、このような細かいこともやっているという紹介でした。

[WIP] Slack通知

最後に、先日作り始めた機能を紹介します。 弊社では、コミュニケーションツールにslackを採用しています。

多くのメンバーが頻繁にslackを利用しているため、slackとRedashが連携できれば、よりデータ活用を促進できる可能性がありました。

公式のslackbotがありますが、弊社では閉域網でRedashサーバを運用しているため、slackbot→ Redashサーバへのリクエストが届きません。

そこで、Redashでデータ更新があったときにRedashサーバからslackチャンネルに通知する設定をするSubscribe機能を実装することにしました。

f:id:tech-geniee:20170907193207p:plain

導入の経緯

私は月初になると「経理作業用のクエリを実行してはせっせとエクセルに貼り付けてメールで各所に送る」という作業を引き継ぎで行っていました。この作業をツールに任せたいというのがきっかけでした。

分析のためのデータ抽出依頼も頻繁に発生していたので、その解決にも繋がるのではと思いました(こちらの方がインパクトは大きい)。自分のマシンでマネージャーにデモをすると導入を即決。やるぞと思っているうちに、既にマネージャーが構築して使い始めていました。

おわりに   

Redashは Angular、Python Flaskで開発されているSPAです。私は業務ではPHPで社内システムの開発をしていることが多いので、普段業務で使うものとは異なる技術に触れて刺激になりました。

個人的には、SPA開発のキャッチアップが出来た点が収穫でした。

Redashには大変お世話になっているので、社内で好評かつ汎用性の高い機能についてはプルリクを出して本家にコントリビュートしたいところです。

MySQL接続数の削減

はじめに

こんにちは、R&D本部アドプラットフォーム開発部の村岡です。

私は、ジーニーでGenieeSSPの開発を主に行っています。

今回、私が入社前のインターンで行った開発内容について紹介したいと思います。

GenieeSSP

SSPは、ブラウザなどからのリクエストを処理し、レスポンスとして広告を返します。 当然ですが、メディアごとにサーバを用意するわけではないため、複数のメディアから来る膨大な広告リクエストを、短時間のうちに処理し広告を返す必要があります。

リクエストがきてから、レスポンスを返せるまでの時間は短ければ短いほどよいのですが、膨大なリクエスト数となるため、システムのパフォーマンスが非常に重要になってきます。

GenieeSSPはそういったSSP特有のパフォーマンス要求のため、C++で開発されており、現在16台のサーバで全リクエストを処理しています。

GenieeSSPでは、広告枠の情報などはMySQLに保存されていますが、広告リクエストごとにDBに枠情報を問い合わせていたのでは、広告を返すまでのレスポンスタイムが長くなってしまうため、全ての枠情報はオンメモリで保持されています。 これらの広告枠情報は、更新のあった枠に対してだけ一定時間ごとにDBにクエリを投げ、メモリ上の枠情報を更新するようになっています。

また、GenieeSSPはlibmysqlclientを直接使用してクエリを投げています。

課題

私がインターンとして入ったとき、GenieeSSPは1プロセスで使用するMySQLコネクション数が多すぎるという問題を抱えていました。

システムの機能拡張を繰り返した結果、MySQLからデータをフェッチする順序が複雑に絡まり、一本のMySQLコネクションでクエリを投げることが困難な状態にありました。

GenieeSSPでは、プリペアドステートメントを使用して全てのクエリを投げています。

この場合、MySQLでデータをフェッチするためには下図の順序でライブラリ関数を呼び出す必要があります。 この順序を守らないと、CR_COMMANDS_OUT_OF_SYNCエラーとなってしまいます。

f:id:tech-geniee:20170808175326p:plain

GenieeSSPにおいて、プロセスが大量のMySQLコネクションを使用する原因は、mysql_stmt_fetch関数を呼び出した後、別のMySQLコネクションとプリペアドステートメントmysql_stmt_execute関数を呼び出すことがあるためです。

一本のMySQLコネクションで、CR_COMMANDS_OUT_OF_SYNCエラーを回避するためには、上図のようにmysql_stmt_execute関数を呼び出したあとはMYSQL_NO_DATAとなるまでmysql_stmt_fetch関数を繰り返し呼び出す必要があります。

調査

ライブラリ関数の呼び出し順序を調査するため、下記のコードを使います。

static int
mysql_stmt_execute0(MYSQL_STMT *stmt, const char *func, int line)
{
    std::cerr << func <<  ":" << line << " execute" << std::endl;
    return mysql_stmt_execute(stmt);
}
#define mysql_stmt_execute(stmt) mysql_stmt_execute0((stmt), __func__, __LINE__)

static int
mysql_stmt_fetch0(MYSQL_STMT *stmt, const char *func, int line)
{
    std::cerr << func <<  ":" << line << " fetch";
    int ret = mysql_stmt_fetch(stmt);
    if (ret == MYSQL_NO_DATA)
    {
        std::cerr << " done";
    }
    std::cerr << std::endl;
    return ret;
}
#define mysql_stmt_fetch(stmt) mysql_stmt_fetch0((stmt), __func__, __LINE__)

このコードを挿入し、テスト環境で実行すると標準エラー出力mysql_stmt_execute関数とmysql_stmt_fetch関数を呼び出した関数名と 呼び出し箇所のソースコードの行数が出力されます。 この出力を元に、呼び出し順序が正しいか調査します、が、呼び出し順が間違っていれば実行途中でエラーとなるため、直前の出力を調べれば原因は大体わかります。

原因がわかった後は、正しいライブラリ関数の呼び出し順序となるようにソースコードを修正するだけです。

修正後

修正後、副次的な効果として全ての広告枠情報を取得する時間が2割減となりました。

また、コネクション数が減ったため、MySQLサーバも安定して動作するようになったような気がします。

f:id:tech-geniee:20170817135838p:plain

おわりに

今回、色々あってMySQL接続数を減らすことになりましたが、日々機能拡張が続くソフトウェアをメンテナンスしていくのは難しく、時間が取れず改善課題は溜まる一方です。

当記事を読んで、弊社とSSP開発に興味を持って頂ければ幸いです。

ありがとうございました。

MySQL×LDAP認証でつらくないデータベース権限管理

こんにちは。R&D本部・経営情報システム部の友安です。

経営情報システム部では社内システム・プロダクト横断でのお金周りの開発や監査対応、
社内のコミュニケーション効率化など、多岐にわたり社内の問題解決に努めています。

社内の権限管理

組織が大きくなってくると色々と統制が求められるようになります。 統制の1つに権限管理の強化が挙げられます。 しかし管理の強化には運用コストが伴います。そのため、より小さな運用コストで実現することは重要なテーマになります。

今回は商用版MySQLの権限管理に便利なPAM認証プラグインの機能を紹介します。

MySQL×LDAP認証

やりたかったこと

  • 運用時やリリース時に利用していた共用のMySQLユーザを廃止し、DB上の権限をメンバーごとに管理する。

  • それを低い運用コストで実現したい。

  • 弊社はサーバへのアクセス権限などをLDAP*1で管理していたので、MySQL上の権限もLDAPで一元的に管理したかった。

MySQL PAM認証プラグイン

商用版MySQLの機能にPAM認証プラグインがあります。

PAM認証プラグインを用いると、MySQLログイン時の認証をLDAPに委譲し、更にLDAPのグループとMySQLユーザのマッピングを指定できます。

これにより、LDAPdb_admin というグループに所属するエントリがMySQLログインした際は、強力な権限を持つMySQLユーザになる、といったことができます。

既にLDAPを運用している組織では、多重管理による運用コスト増大を回避しつつMySQL上の権限を制御出来ます。

設定例

想定するLDAPのグループとマッピング先のMySQLユーザに付与したい権限は以下とします。
今回の例ではシンプルな3グループで設定します。

LDAPグループ 所属メンバー DB上の権限 MySQLユーザ
db_admin DBA ALL db_admine_user
operation 運用チーム データ更新 operation_user
engineer 全エンジニア readonly engineer_user

pam設定ファイルの作成

以下の形式でPAM認証プラグインが使用するpam設定ファイルを作成します。

#%PAM-1.0
auth required {認証モジュール}
account required {認証モジュール}

使用するLDAPクライアントはpbis-openです。

/etc/pam.d以下にpam設定ファイルを作成します。ファイル名は mysql とします。

/etc/pam.d/mysql

#%PAM-1.0
auth required pam_lsass.so
account required pam_lsass.so

MySQL側の設定

install plugin authentication_pam soname 'authentication_pam.so';
  • MySQLユーザを作成、権限設定
-- プロキシされるユーザの作成
CREATE USER 'db_admin_user'@'192.168.%.%' IDENTIFIED BY 'hoge1';
CREATE USER 'operation_user'@'192.168.%.%' IDENTIFIED BY 'hoge2';
CREATE USER 'engineer_user'@'192.168.%.%' IDENTIFIED BY 'hoge3';

-- 各ユーザに権限付与
GRANT ALL ON *.* TO 'db_admin_user'@'192.168.%.%';
GRANT SELECT,UPDATE,INSERT,DELETE ON *.* TO 'operation_user'@'192.168.%.%';
GRANT SELECT ON *.* TO 'engineer_user'@'192.168.%.%';

-- プロキシ用の匿名ユーザを作成
-- AS以降の文字列(authentiation_string)は {pam設定ファイル名}, {LDAPグループ}={MySQLユーザ} の形式で記述する 
CREATE USER ''@'192.168.%.%' IDENTIFIED WITH authentication_pam AS 'mysql, db_admin=db_admin_user, operation=operation_user, engineer=engineer_user';

-- プロキシ権限を付与
GRANT PROXY ON 'db_admin_user'@'192.168.%.%' TO ''@'192.168.%.%';
GRANT PROXY ON 'operation_user'@'192.168.%.%' TO ''@'192.168.%.%';
GRANT PROXY ON 'engineer_user'@'192.168.%.%' TO ''@'192.168.%.%';

ここまでの設定で、LDAPのユーザ名でログイン時に次のような流れで認証されるようになります。

  1. ユーザ名をLDAPユーザにしてログイン。
  2. mysql.userテーブル上に合致するユーザ名が見つからない。
  3. 合致するユーザ名が無い場合、匿名ユーザにマッチングされる。
  4. 匿名ユーザの認証にPAM認証プラグインが設定されているため、PAM認証が行われる。
  5. 認証に成功すると、匿名ユーザはLDAPグループごとにマッピングしたMySQLユーザをプロキシする。

上記のLDAPグループに複数所属している場合、記述した順でマッピングされる。

例だと db_admin > operation > engineerの優先度でマップされるので、 db_adminとengineerに所属するユーザはdb_admin_userとなる

ログイン動作確認

  • ログイン
mysql -u {ldap_user} -p
  • ログイン後
SELECT user(),current_user();

上記のクエリ実行でuser()LDAPのユーザ名が、current_user()LDAPグループに対応したMySQLユーザ名が表示されます。

踏み台ホストで権限を変える

MySQLサーバにアクセスする踏み台ホストを変えることで、マッピングするユーザを変えることもできます。

これにより、192.168.0.1からのアクセス時はreadonlyなユーザにマッピングし、SELECTして調査したいだけの時は、不要な権限を持たないMySQLユーザを使ってもらうといった運用も可能です。

192.168.0.1からアクセスした際はreadonlyなユーザにマッピングするように設定

CREATE USER 'engineer_readonly'@'192.168.0.1' IDENTIFIED BY 'hogehoge';
GRANT SELECT ON *.* TO 'engineer_readonly'@'192.168.0.1';
-- エンジニア全員が所属するengineerグループにengineer_readonlyユーザをマッピング。これにより192.168.0.1からアクセスした際は全エンジニアがreadonly権限のユーザになる
CREATE USER ''@'192.168.0.1' IDENTIFIED WITH authentication_pam AS 'mysql, engineer=engineer_readonly';
GRANT PROXY ON 'engineer_readonly'@'bar' TO ''@'192.168.0.1';

監査ログ

MySQLの監査ログプラグイン(こちらも商用版MySQL)と組み合わせると、 LDAPグループとマッピングされるMySQLユーザを指定したフィルタを作ることで、簡単にLDAP認証でログインした全ユーザをロギング対象にすることができます。*2

注意

MySQL 5.7.12以前のバージョンでは監査ログプラグインの挙動にバグがあり、 LDAP認証でログインした全てのユーザをロギング対象にするには、 全LDAPエントリ×アクセス元となり得る全てのhostを指定するフィルタを作成しなければいけません。 *3

まとめ

商用版MySQLの機能であるPAM認証プラグインを利用し、ログイン認証をLDAPに委譲することで、 低運用コストでのデータベース権限管理を実現する方法を紹介しました。

特にLDAPを運用している組織にとって有用な方法になり得ます。
実運用ではリリーススピードを緩めないような権限委譲など、組織の設計が重要になると思います。