• Home
  • コーポレートブログ Geniee’s BLOG
コーポレートブログ

Geniee’s BLOG

ジーニーは最先端の広告テクノロジーで
顧客の収益を最大化します。

こんにちは。R&D本部の萩原です。
Yandex社が中心となって開発しているClickHouseの日本初Meetupイベントがジーニーオフィスで11/14(木)に開催されます。
※過去のClickHouseの記事はこちら

ClickHouseとは?

ClickHouseはYandex社が開発しているオープンソースの優れた列指向データベース管理システムで、ジーニーでも利用しています。
従来のアプローチに比べて数十〜数百倍の処理速度で動作し、SQLクエリを使用してリアルタイムで数十億行・数十GBのデータを集計・解析できます。
加えて、スケーラビリティや耐障害性も備えており、複数のデータセンターにまたがってデプロイすることもできます。

列指向DBといった意味ではRedshift等は気軽に触ったことある人はいるとおもいますが
ClickHouseは日本では使われているケース使ってみたケースはあまりないと思っています。

 

ClickHouse Meetup Tokyo 2019

当日は以下の内容を予定しています。

1.Yandex社によるClickHouseのイントロダクション
2.GenieeによるClickHouseの導入事例
3.参加者によるライトニングトーク
4.Yandex社によるClickHouseの将来展望について
5.懇親会

今回のmeetupでは、事例やLTを通じてClickHouseの知見や体験を共有できると思いますのでお気軽にご参加ください!
また、事例等のLT参加メンバーもお待ちしています!

登録はconnpassよりどうぞ

Date
Author
エンジニアの視点から、様々な技術、サービス開発秘話、イベントをご紹介していきます。 ジーニーエンジニアチーム

はじめに

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

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

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

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

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

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

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

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

■参考:

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

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

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

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

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

をご紹介します。

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

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

そこで対策として

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

を行なっています。

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

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

  • トラフィックの一部のレスポンスに含まれる広告タグの取得と保存
  • headless chromeを用いて上記のレンダリングを行い、リダイレクトのチェック

ということをシステム的に行なっています。
これにより、リアルタイムではありませんが強制リダイレクトが起きた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の場合だとオークションに行かない、もしくは何かしらのビーコンを書き出す

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

終わりに

以上、

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

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

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

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

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

Date
Author
エンジニアの視点から、様々な技術、サービス開発秘話、イベントをご紹介していきます。 ジーニーエンジニアチーム

はじめに

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

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

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

GenieeSSPについて

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

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

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

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

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

数十台ある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のレコードが更新されると、以下のような流れで配信設定が反映されるように実装を行います。

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

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

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

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

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

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

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

処理フロー

約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秒
  • 全広告枠分のJSONをRamdiskに書き出し…5秒
  • LZ4で圧縮…30秒
  • 転送…30秒
  • 配信サーバのJSONロード…平時10秒程度
  • 残りは、ファイルの転送はcronで毎分行われているためそのディレイと、配信サーバが新しいJSONファイルを見つけるまでのディレイ

となっています。

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

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

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

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

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

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

おわりに

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

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

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

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

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

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

qiita.com


www.slideshare.net

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

Date
Author
エンジニアの視点から、様々な技術、サービス開発秘話、イベントをご紹介していきます。 ジーニーエンジニアチーム

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

はじめに

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

ジーニーの事業全体図

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

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

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

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

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

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

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

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


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

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

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

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

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

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


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

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

Date
Author
エンジニアの視点から、様々な技術、サービス開発秘話、イベントをご紹介していきます。 ジーニーエンジニアチーム

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

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

HTTPS化のトレンド

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

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

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

MAJINタグにおけるHTTPS設定

証明書

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

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

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

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

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

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

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

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

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

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

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に施している設定も半年後には古いものになっているかもしれません。

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

Date
Author
エンジニアの視点から、様々な技術、サービス開発秘話、イベントをご紹介していきます。 ジーニーエンジニアチーム

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

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

Redash

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

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

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

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

潜在的な要望への気づき

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

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

カスタマイズ

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

スケジューリング強化

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

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

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

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

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

[WIP] Slack通知

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

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

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

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

導入の経緯

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

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

おわりに

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

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

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

Date
Author
エンジニアの視点から、様々な技術、サービス開発秘話、イベントをご紹介していきます。 ジーニーエンジニアチーム

はじめに

こんにちは、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エラーとなってしまいます。

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サーバも安定して動作するようになったような気がします。

おわりに

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

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

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

Date
Author
エンジニアの視点から、様々な技術、サービス開発秘話、イベントをご紹介していきます。 ジーニーエンジニアチーム

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

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

社内の権限管理

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

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

MySQL×LDAP認証

やりたかったこと

  • 運用時やリリース時に利用していた共用のMySQLユーザを廃止し、DB上の権限をメンバーごとに管理する。
  • それを低い運用コストで実現したい。
  • 弊社はサーバへのアクセス権限などをLDAP*1で管理していたので、MySQL上の権限もLDAPで一元的に管理したかった。

MySQL PAM認証プラグイン

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

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

これにより、LDAPのdb_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側の設定

  • PAM認証プラグインをインストール
----------------------------------------------------------------
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を運用している組織にとって有用な方法になり得ます。
実運用ではリリーススピードを緩めないような権限委譲など、組織の設計が重要になると思います。

*1:ネットワーク機器やユーザーなどの情報を管理するディレクトリサービスへ接続するためのプロトコルhttp://www.atmarkit.co.jp/aig/06network/ldap.html

*2:MySQL :: MySQL 5.7 Reference Manual :: 6.4.5 MySQL Enterprise Audit

*3:18 Changes in MySQL 5.7.12 (2016-04-11, General Availability)

Date
Author
エンジニアの視点から、様々な技術、サービス開発秘話、イベントをご紹介していきます。 ジーニーエンジニアチーム

はじめに

こんにちは、ジーニーR&D本部アド・プラットフォーム開発部の宮下です。

今日は弊社の開発業務で普段どんなことをしているのかな、

という業務の一旦を弊社プロダクトのGenieeDSPを例にとってご紹介したいと思います。

現象

GenieeDSPは、弊社のDSP広告配信システムでSSPから来るリクエストのうち、おおむね月間800億リクエストを処理して、広告を返しています。

ある日のこと、監視システムから、配信サーバーの一台で負荷が高まっていることを示すアラートが上がってきました。

ジーニーでは各サーバーの稼働状況をモニタリングするものとして、Grafanaというツールを使用しています。

この時の状況でいうと、

  • CPU使用率は30%程度
  • load averageはほぼ限界まで高まっている(全てのCPUが処理待ち)という症状でした。

現象はリクエストの多い時間帯で起きており、症状の起きたサーバーではリクエストの処理能力が著しく落ち、ろくにリクエストを処理できない状態でした。

何かの処理がボトルネックになっていて配信サーバーのパフォーマンスが悪化しているものと思われました。

困った時はプロファイラ

ここで原因を特定すべく、Linuxのパフォーマンス解析ツールであるperfを使用しました。

perfは、関数別にCPU使用率を計測・表示してくれる便利なツールです。

ジーニーでは速度上のボトルネックになりそうな箇所を特定するためによく利用しているツールになります。(詳細は[こちら](https://perf.wiki.kernel.org/index.php/Main_Page))

早速実施してみたところ、下記のような配信サーバーの関数の各使用率が表示されました。

“`
$sudo perf top

7.55% adserver [.] backend::doPreAdSelect(geniee::lot&, std::vector<geniee::banner, std::allocator >&)
5.09% [kernel] [k] _raw_spin_lock_irq
4.06% adserver [.] geniee::util::FastIDSet::includes(int) const
3.29% adserver [.] targeting::Targeting::doIncludeTargeting(std::vector > const&, std::vector<
2.87% adserver [.] geniee::memory::zone(std::string const&)
2.40% adserver [.] std::string::find(char const*, unsigned long, unsigned long) const
“`

上記から見ると、案件のターゲティング処理をしている関数が重たそうなことがわかります。

しかしそれは今までも上位に来ていたので特に問題ではなさそうでした。

ここで一点、見慣れない関数が上位に表示されていました。

“`
5.09% [kernel] [k] _raw_spin_lock_irq
“`

また、他の現象が発言していない同スペックのサーバーを調査した結果、
`_raw_spin_lock_irq`がCPU使用率の上位に現れることはないということがわかりました。

どうやらこの関数が怪しそうです。

この関数がおそらく原因であろうという仮説のもと、より詳細に調査をすることになりました。

_raw_spin_lock_irq:ですが、これは[kernel]とperfで出力されていることからも分かるとおり、osのスケジューラ内で使用されている関数で、ある処理が、あるCPUのコアを使用している時に、他の処理からの割り込みを受けないように、変数をロックするためのものになります。

ここで、perfの機能である、その関数がどこから呼び出されてるか(callgraph)の出力をしてみます。

“`
$perf record -a — sleep 30
$perf report –stdio

|
— _raw_spin_lock_irq
|
|–49.40%–
void std::vector<geniee::banner, std::allocator >::_M_emplace_back_aux(geniee::banner&&)
| |
| |–1.55%– 0x7fd18d0a1000
| | 0x0

“`

結果、ここでわかったのは、std::vectorに要素を追加するときに呼ばれている、ということでした。

何か自分たちで作り込んだ関数が変な処理をしていてそこが特定できればなおせるのではないか?と考えていましたが、std::vectorへの要素追加は基本的な操作なので、改善の余地はあまりないように思えました。

そこで、一旦状況を整理してみました。

  • 配信サーバーはマルチスレッドかつイベントドリブンで動いている。
  • 全スレッドはカーネルにより任意のCPUを割り当てられる。
  • あるスレッドがvectorへの要素追加をしようとすると_raw_spin_lock_irqが呼ばれ、そのCPUを使おうとする別のスレッドが処理待ちになる?のではないかと思われました。

すると、

vectorへの要素追加時に他の処理がそのCPUをそもそも使わないようにすれば良い、ように思えます。

これでいくと、各スレッドが特定のCPUのみを使用するようにすると解決しそうです。

そこでtasksetコマンドを利用して、各スレッドに一つのCPUを割り当てることにしました。

tasksetコマンドは、コマンドやプログラムを特定のコアで実行することを可能にするコマンドです。(詳細は[こちら](https://linux.die.net/man/1/taskset))

“`
$taskset -c -p CPUコア番号 プロセスID
“`

というように、配信サーバーのスレッドのプロセスIDを、順々に各スレッドに対してCPUコアを割り当てていきます。

後日この現象が再発した際にサーバーにログインし、上記コマンドを実施したところ、load averageが下がり、_raw_spin_lock_irq関数がhtopの上位に来ることはほぼなくなり、なんとか事象を解決することができました。

また、この方法だとサーバーが再起動するたびに全スレッドに対してtasksetを実施しないといけないことになりそうだったので、後日配信サーバーのプログラムに1スレッドが1CPUコアを使用するように修正を行いました。

終わりに

いかがでしたでしょうか。

ジーニーでは日々増加していく膨大なリクエスト数に対して、いかにしてその処理をさばいていくのか、という課題に日夜向き合っております。

大規模なトラフィックに向き合う中で通常では遭遇しないような問題・課題もありますが、そんな時に知恵を絞って解決した時の喜びや達成感があります。

当記事でその一端をご紹介させていただきましたが、そのような課題解決に興味を持たれたり、面白そうだな、と感じていただけましたら幸いです。

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

Date
Author
エンジニアの視点から、様々な技術、サービス開発秘話、イベントをご紹介していきます。 ジーニーエンジニアチーム

はじめに

どうも、R&D本部のマーケティングオートメーション開発部所属の張です。

業務では弊社のマーケティングオートメーションプラットフォーム「[MAJIN](https://ma-jin.jp/)」のフロントエンド開発・保守、基盤の改善、DevOps推進などなどに携わっています。最近仕事で主に使う言語はGoです。

初回なので、今回はMAJINのフロントエンドで使う技術を簡単に紹介いたします。

MAJINのフロントエンド

MAJINは2016年7月にリリースしたばかりのマーケティングオートメーションプラットフォームで、社内では比較的若いプロジェクトです。そのため、MAJINでは他のプロジェクトよりトレンディな技術を多く採用する傾向があります。

ローンチの時にはフロントエンドとバックエンドを分けて別々で開発する体制をとりましたが、最近はチーム内の交流が増え、フルスタックエンジニアを目指すチームメンバーも急増しました。

今回は「フロントエンド」をテーマとしていますが、JavascriptやHTMLを使ったWebサイトの設計・実装といった「狭義のフロントエンド」ではなく、DBのデータを取得や更新する為のサーバ側の設計・実装(API等)も含めた「広義のフロントエンド」と定義させていただきます。

技術沿革

  • ローンチ時のフロントエンド:PHP 7 + Symfony 3.x + ES6 + Vue.js 1.x
  • 最近のフロントエンド: Python 3.5 + Flask + ES6 + Vue.js 2.x

PHP+Symfonyを選んだ&やめた理由

PHP+Symfonyを選んだ一番最初の理由は、社内で既に使われていて短期間にエンジニアを確保しやすいところでした。しかし、MAJINの開発加速に伴い、PHP+Symfonyの限界を徐々に感じました。

  • MAJINではRDBMSのMySQLの他に、DynamoDBやAerospikeなどのNoSQL系のデータベースも使用しています。これらDBに対し、Symfonyのジェネレータ(Ruby on Rails の scaffolding に相当)やORMapperなどの機能のメリットをほとんど生かせませんでした。例えば最初にDoctrine Annotation ReaderでDynamoDB用のORMapperを作りましたが、クエリの性能とデータ一貫性のコントロールを考慮して結局AWS SDKをそのまま使うことにしました。
  • MAJINのフロントエンドの他のマイクロサービスのAPIとのやりとりが想定よりも多くなりました。これらのAPIで処理する機能にさらにSymfonyのようなフレームワークを挟むとオーバーヘッドが増えます。デバッグとテストもやりにくくなります。
  • MAJINの仕様上、コンテンツやシナリオなどのビジネス要件にはリッチな画面が要求されています。フロントエンドのSPA化(Single-Page Application)によってSymfonyの存在感がさらに薄れました。

Python 3.5+Flaskを選んだ理由

  • 一部のバックエンドとAPIがPython 3.5で書かれているのでコードを共有ができ、PHPで車輪の再発明をせずに工数を節約できます。
  • Flaskは手軽で軽量でウェブアプリケーションフレームワークとして使いやすいです。ライブラリも豊富で特に不自由さを感じません。
  • Flaskは自由度が高いので、必要に応じて機能の拡張も簡単にできます。MAJINにも開発効率向上のためにFlaskでいくつかのプラグインを開発しました。

ES6+Vue.jsを選んだ理由

[f:id:tech-geniee:20170718181028j:plain]

モダンなSPAの開発にはMVVM(Model–view–viewmodel)フレームワークが不可欠です。数多く存在するMVVMフレームワークの中でも、下記の理由でVue.jsを選びました。

  • 使い方が簡単、勉強コストが低い。
  • Monolithicなフレームワークではなく、初めから少しづつ適用していける(Progressive framework)。
  • 理論上と実際の性能がよかった。

Vue.jsの実装はごく簡単で、下記のようにModel,View,ViewModelをそれぞれ定義するだけで、入力値が即Modelに保存され、Model内の値もすぐViewに反映されます。一見簡単そうな仕組みですが、データフローを正確に定義することによって大規模な開発に莫大な威力を発揮できます。

<p data-height=”265″ data-theme-id=”0″ data-slug-hash=”GEYGgX” data-default-tab=”html,result” data-user=”cheungchifung” data-embed-version=”2″ data-pen-title=”Example of two-way binding” class=”codepen”>See the Pen <a href=”https://codepen.io/cheungchifung/pen/GEYGgX/”>Example of two-way binding</a> by Cheung Chifung (<a href=”https://codepen.io/cheungchifung”>@cheungchifung</a>) on <a href=”https://codepen.io”>CodePen</a>.</p>
<script async src=”https://production-assets.codepen.io/assets/embed/ei.js”></script>

“`html
<div id=”main_view”>
<label for=”name”>Enter name:</label>
<input type=”text” v-model=”name” id=”name” name=”name” />
<p>Welcome, {{ name }}!</p>
</div>
“`

世の中のMVVMフレームワークのReactiveの実装はいくつもあります。[参考](https://teropa.info/blog/2015/03/02/change-and-its-detection-in-javascript-frameworks.html)

    • – Pub-Sub(backbone.js): pub, subの方式でデータを更新する。
    • – Dirty Checking(augular.js): click等の特定のイベントが発火したらデータの更新検知を行う。
    • – Virtual DOM(React): Virtual DOMを用いて、Modelの変化を高速にViewに反映する。
    • – Hook(Vue.js): ECMAScript5の`Object.defineProperty()`を利用して値の変化を検知し、変化があったらSubscriberにメッセージを送りコールバックを呼ぶ。(2.0からVue.jsもVirtual DOMを導入した)

Vue.jsが`Object.defineProperty`を使うことで、`this.name = “MAJIN”`のように書くだけで、Observer(Vue.jsで変化を監視するコンポネート)が自動的に変化を検出してテンプレートに反映され、開発はかなり楽になります。その代わり、IE8以下などのECMAScript5未対応のブラウザは対応できません。
(MAJINのサポートブラウザは全てES5対応なので問題ありませんでした)
Vue.jsのReactiveの仕組みを詳しく説明したいところですが、今回は割愛します。

また、Vue.jsの公式の[比較記事](https://jp.vuejs.org/v2/guide/comparison.html)によると、Vue.jsはReactに劣らないパフォーマンスを提供できます。MAJINではVue.js 1.0と2.0を使っていますが、性能面の問題は特に感じませんでした。(と言っても使い方次第でパフォーマンスが落ちることはもちろんあります。例えば検知不要な値を大量にObserverに突っ込んでブラウザが固まることもあります)

当初Vue.jsを選んだ時、公式のデモで使われていたES6をそのまま導入しましたが、ES6には型チェックがないので心細く、TypeScriptに乗り換えようと試みたこともあります。しかし、当時はTypeScriptとVue.jsの相性が悪く感じ、挫折してしまいました。
最近Vue.jsとTypeScriptを取り巻く環境も大きく変わりましたので、そろそろ再チャレンジしようと思います。

Vue.jsでハマったところ

Vue 1.x から 2.x

MAJINリリース時に2.xはまだstableになっていないため、古いコードは全部1.xを使いました。

2.xではいろんな特性が追加されましたが、特にアップグレードしなければならない理由が無く、そのまま放置しようと思いました。しかしVue.js 2.0リリース後半年も経たず、多くのプラグインが2.0にしか対応しない状態になってしまって、止むを得ずにVue.js 2.0に移行しました。

2.0へ移行するには、書き直しが必要な箇所が多く思ったより大変でした。MAJINは複数のエンドポイントでVue.jsを使っているので、vue1.xと2.xのページのwebpackファイルを分けて少しずつ移行することにしました。Vue.js 1.xに書いたUIコンポーネントもVue.js 2.xでそのまま動かないので、一部のUIコンポーネントも2.0版で書き直しました。

移行は辛かったですが、1.xと比べて使えるライブラリが増え、Virtual DOMなどの2.xのメリットを手に入れたので、移行する価値はあると思います。

エコシステムが未熟

Vue.js 1.0を導入する時にすでにVuex(ReactのReduxのVue.js版)やVue-routerがすでに出ましたが、多くのVue.jsのライブラリはまだ使いづらいものでした。最近はある程度改善されたようですが、いざという時にはやはり自力でカスタマイズするしかありません。

解決案は主に二つ:

        1. . jQueryなどのライブラリのVue.js版を作る。
        2. . 自分でVue.jsのプラグインを作る。

1についてはVue.jsの公式サイトに[デモ](https://vuejs.org/v2/examples/select2.html)があります。多くのUIコンポーネントはこちらの方法で対応しました。Vue.jsには様々なUIのライブラリが存在していますが、MAJINに合うものはなかなか見つからず、従来のUIライブラリをこの方法で流用し、Vue.jsに対応しました。

2は難しそうに見えますが、実は簡単でした。Vue.jsのプラグインを作ることで、既存のシステムから移行する時にそのギャップを埋められる重要な手法です。ここでは一番簡単なMAJINの「i18nプラグイン」を使って説明いたします(説明の便宜上、言語を日本語に固定します)。

`plugins/i18n.js`

“`javascript
import campaign from ‘i18n/campaign.ja.yml’
import _ from ‘lodash’

const i18nMaster = {
campaign,
}

let i18nCache = {} // In-memory Cache

export const translate = (expression) => {
if (i18nCache[expression] !== undefined) {
return i18nCache[expression]
}

const paths = expression.split(‘.’)
let f = (_paths, master) => {
try {
let p = _paths.shift()
let v = master[p]
return typeof master === ‘string’ ? master : f(_paths, v)
} catch (e) {
if (__DEV__) console.warn(`[i18n] cannot find expression: ${expression}`)
return null
}
}
let v = f(paths, i18nMaster)
if (v !== null) {
i18nCache[expression] = v
}
return v
}

function I18n (Vue) {
// Step 1: プラグインがインストール済みかのチェック
if (I18n.installed) return
I18n.installed = true

// Step 2: Mixinでコードを入れる
Vue.mixin({
beforeCreate() {
Object.defineProperty(this, “translate”, {
value: translate,
})

Vue.filter(‘translate’, translate)
},
})
}

export default I18n
“`

このプラグインの目的は、Symfony形式のyamlのi18nの定義ファイルをそのままVue.jsで使い回すことです。
冒頭の`i18n/campaigin.js.yml`は今回使うi18nファイルの名前であり、その中の `i18n` は `webpack.config.js`で設定したパスです。

`webpack.config.js(抜粋)`

“`javascript
resolve: {
“alias”: {
“i18n”: __dirname + “/path/to/translations”,
}
}
“`

`npm install yaml-loader`して`webpack.config.js`に下記のコードを追加すると、i18nの定義ファイルはJavaScript Objectとして読み込まれます。

“`javascript
module: {
loaders: [

{
test: /\.(yaml|yml)$/,
loader: “json!yaml”
},

],
}
“`

そしてエンドポイントファイルに
“`javascript
Vue.use(I18n)
“`
を追加すれば、ViewModelで使えます。

“`javascript
var myApp = new Vue({
el: ‘#ma-app’,
data(): {
return {
message: this.translate(‘campaign.path.to.translate’)
}
},
})
“`

またはViewでfilterとして使えます。

“`html
<span>{{ ‘campaign.path.to.translate’ | translate }}</span>
“`

MAJINでは、i18nのほか認証やValidatorなどのプラグインも自作しました。これらのプラグインを利用しフロントエンドのシステム移行を加速できました。

ビルド時間が長い

Vue.jsで書かれたコードの増加に伴い、ビルド時間が急増しました。また、Webpackのビルド時のCPUとメモリの消費率もかなり高まり、ひどい時はAWS上のJenkinsがOutOfMemoryで死ぬこともありました。

この問題の解決案は二つあります。

        1. . `webpack.optimize.CommonsChunkPlugin`を利用して重複ビルドを省く。(マルチエンドポイントのMAJINでは大きな効果がありました)
        2. . `HappyPack`(https://github.com/amireh/happypack)でビルド処理を非同期化する。

この二つの手段の組み合わせで、本来10分以上かかったビルドが、今は2分弱で完了します。

終わりに

MAJINのフロントエンドで使った技術を選んだ理由とそれぞれの問題点を簡単に紹介しました。より細かい話は、これからのブログで随時掲載いたします。
どうぞご期待ください。

Date
Author
エンジニアの視点から、様々な技術、サービス開発秘話、イベントをご紹介していきます。 ジーニーエンジニアチーム

はじめに

みなさん始めまして、R&D本部 基盤技術開発部の石田祥英です。
北大の情報科学研究科を卒業前に飛び出しジーニーに半年早く17卒入社し、現在は主にFlink, Kafka, Play Framework(scala)や、バッチ用途でgolang,pythonなどを使いプロダクト横断のシステムの開発・検証をやっています。

TL;DR

  • ClickHouseで分析系クエリが1000倍の速度に爆速化
  • DBの容量もMySQLと比べ1/13に軽量化
  • ClickHouse導入はMySQLには無い制限もあるけど、なんとかなる
  • あんまり日本での導入事例が見当たら無いけど、クエリの高速化・DBの軽量化したい人はぜひ使うといいことがあるかもしれません

社内勉強会用の資料

ClickHouse導入の背景

一般的に広告プラットフォームでは「どの広告を、どの広告枠(サイト)に、いつ、どのように(RTB or Adnetwork)配信したのか」という様な情報をDBに格納し、広告主向けの管理画面がDBからレポートする内容を取得し、代理店や広告主の運用者に配信結果をレポート表示しています。

通常、「〜ごとのレポートを見たい」となった場合、それに相当するカラムをキーとして保存しなくてはいけません。

しかし、実際には「1日ではなく1時間ごとにレポートを見たい」、「OS、地域、デバイスごとなどのImpression(広告表示)や広告のClickを集計したい」となると、キーがどんどん増え、それに伴ってDBの行数が指数関数的に増えていってしまいます。

1日に15TBほどの広告配信のログが出るジーニーでは、実際にキーの数を4個から10個に増やそうとした時に、もうMySQLではインデックスを正しく張って頑張っても、運用者が我慢できる時間内にクエリが返ってこなくなってしまう、という問題にぶち当たっていました。

また事業成長の要因も考慮し、トラフィックが10倍に増えても低レイテンシでレポートを返すDBが求められており、何か手はないのかと言うことで色々な方法を模索していました。

DBに必要な要件

  • 広告の運用者の方々がレポート画面をみて運用のPDCAを回せるように、とにかく低レイテンシであること
  • 集計プログラムにバグが見つかった時に再集計してDBの内容を正せること
  • ログ転送遅延時やハードウェア故障時などへの耐障害性があること

検証した他サービス・手法

MySQLに並列でクエリを投げる

レポートAPIがscalaのPlayで書かれていることもあり、akkaを使い並列でクエリを投げる案があったのですが、正しくソートされたレポートを並列化されたクエリでうまくかける方法が見つからず実現が難しいと判断しました。

BigQuery

Googleの巨大なリソースを使い、SaaSであるので開発・メンテが不要なことやSQLライクなインターフェースと柔軟なUDFが魅力で検討対象に。

実データでの検証は行なっていませんが、Google社の営業担当にテーブルの行数とそれに対するクエリのベンチマークの結果を教えてもらい、それをクエリのパフォーマンスとして考えることができました。

BigQueryはクエリごとの課金とデータ量による課金があり、クエリ課金部分は月額3万ドル(2016/12時点)で叩き放題のプランがりましたが、データ量が膨大なことにより予算をオーバーしたことと、そもそもAPI向けの秒単位の低レイテンシ用途に最適ではないと判断し諦めることになりました。

InfoBright

MySQL上にビルドされたカラムナDB。列志向による軽量化と高速化が期待できる上、MySQLからの移行もスムーズに行えそうなことが魅力に映り実データでの検証に至りました。

しかしユニークキーがはれないことや、INSERT 〜 ON DUPLICATE KEY UPDATEができない、頻繁なアップデートに不向きで、特別な運用をしないといけないことに加え、手元のデータではClickHouseよりもクエリ性能が出なかったこともあり今回は使用しないことになりました。

ClickHouse

公式HPにMySQL・VerticaやInfoBrightなどのカラムナDB・Hiveとのベンチマークを乗せており、ほとんどの場合で、高速なことが魅力でした。特にレポートで頻出するSum関数やGroup By句でのパフォーマンスが高く、実データでの検証に至りました。特にフリーかつオープンソースであること、何よりドキュメントがしっかりしていたことも大きな魅力でした。

https://clickhouse.yandex/benchmark.html

ClickHouseの性質・性能

スキャンする性能がものすごく高い

手元の検証用データではスキャン性能 3億行/s 9GB/sほど出る。(後述のSumming Merge Treeエンジン使用時)

圧縮率が高くストレージの節約になる

カラムナDBなので圧縮率が高い。
検証用データ使用時にはMySQLに比べてストレージ使用量が1/13になりました。

ただし後述のMergeテーブルを使い、1つのテーブルを450テーブルに分散した状態で測定したので、1テーブルで行った場合にはさらに圧縮されるポテンシャルがあると考えられます。

多種多様なテーブルエンジンがある

ClickHouseには20種類のテーブルエンジンがあり、CREATE TABLE時に1つorいくつか組み合わせて選んでテーブルを作ります。

This is the most advanced table engine in ClickHouse. Don’t confuse it with the Merge engine.

とドキュメントにあるように、Merge TreeエンジンがClickHouseに置ける看板エンジンであり、広告主向けレポートで使用したテーブルもMerge Treeエンジンの亜種であるSumming Merge Treeエンジンを使用しています。

広告主向けレポートで使用している2つのテーブルエンジンを紹介しようと思います。

Summing MergeTreeエンジン

Summing Merge Treeエンジンは足し上げが起こるレポート等の用途に向いているエンジンです。キーになるカラムとメジャーとなるカラムを指定しておき、同じキーのレコードをinsertするとメジャーは足し上げられて一行として扱われるようになるという性質を持ちます。ImpressionやClick数、広告主にかかるコストなど、足し上げが発生するレポートにおいては非常に便利なテーブルエンジンで広告向けレポートにはピッタリでした。ClickHouseの行のUPDATEができないという問題も、足し上げるだけのためのUPDATEであればこのエンジンを使うだけで問題なくなります。

Mergeエンジン

Mergeエンジンは複数のテーブルを1つのテーブルとして扱うエンジンです。

テーブル作成時にテーブルの正規表現を指定しておくと、それにマッチした複数のテーブルを1つのテーブルとして扱い、Mergeエンジンにクエリを投げれば配下のDBにクエリを投げた結果を返してくれるという性質を持つため、巨大なテーブルを分割して保存しておくことが可能になります。

UPDATE・DELETEがない

ClickHouseでは行に対するUPDATEとDELETEができません

これがClickHouseの性能に飛びつくも導入時に困るであろう大きな問題で、今回広告主向けレポートではどう乗り越えたかを紹介したいと思います。

ClickHouseの運用

困ったこと

SQLはほぼMySQLと同じであり、ClickHouse移行で最大にして唯一困ることは

「UPDATE・DELETEが無い問題」

でした。

具体的には

  1. UPDATEができないので、同じ行への更新ができない。
  2. DELETEができないので、集計プログラムのバグ発覚や、集計遅延時等にDB内のデータ修正ができない。行う場合は、DROP TABLEしてまた正しいデータを入れ直さないといけないため時間がかかる。

といった問題にぶつかりました。

今回の広告主向けレポートの要件として

  1. 1時間ごとの集計
  2. 再集計が短時間で可能

というものがあり、要件を満たしながら制限を回避する方法にたどり着きました。

 乗り越え方

簡単に箇条書きにすると

  • Mergeエンジンを使い1日ごとにテーブルを作成することで、データが壊れた時のデータの入れ直しの時間を減らす。
  • Summing Merge Treeエンジンを使い、1時間ごとに最新データを入れていく。同じキーごとにメジャーが足し上げられるので、同じ行のimpressionやclick数などのメジャーの集計が遅れても正しく足し上げられる。
  • RENAME句のみがトランザクションを使える(複数テーブルの一括RENAME時にグローバルロックが走る)ので、それを使って壊れたデータと正しいデータに入れ替えを行う。

ここら辺は文字で書くとややこしいので冒頭のスライドにデータの入れ直しのテクニックを図解しているので参照してみてください。

ClickHouseの苦手だと感じたこと

非常に派手なベンチマークのClickHouseも以下の用途には向かない(そもそもそれ用に設計されてない)です。

OLTP処理

ClickHouseはそもそもがOLAP処理に作られたDBであり、トランザクション処理も実装していません。唯一RENAME句内での複数RENAMEにグローバルロックがかかるぐらいです。ECなどのトランザクション取引が発生するサービスのDBとしては向いていません。

分散ストリーミング環境からののインサート

ドキュメントに

 We recommend inserting data in packets of at least 1000 rows, or no more than a single request per second

とあるよう1回のINSERTには1秒あけることと少なくとも1000行のデータをINSERTすることが推奨されており、ストリーミング処理したデータをポイポイ入れていくのではなく、ストリーミングの場合はどこかに一度貯めてからバッチ的にINSERTする必要があるようです。

分散処理基盤からのインサート

また広告主向けレポートでも現在Apache KafkaとApache Flinkなどを用いて分散ストリーミング処理を今後行っていくつもりですが、Apache kafkaやApache Flinkで分散したデータを最終的には1つのtsvファイルにまとめる必要があり、分散したままのINSERTには向いていないようです

https://clickhouse.yandex/docs/en/single/#performance-on-data-insertion

まとめ

社内の広告運用者からは「ロードに3分かかっていたレポート画面が0.5秒で返ってくるようになりました!」と感謝され、DBAからは「DBを圧迫していたレポートテーブルが小さくなった」と喜びの声をいただきました。

現状の使い方ではレプリケーションしかしていませんが、今後格納行数がさらに大きくなった時にもShardingしながら分散するDistributeテーブルエンジンもあるので、まだクエリが遅くなることがすぐに起こる予定はないですが、そちらを使って対処していこうかと思います。 ClickHouseの開発元であるYandex社では374台のサーバーに20兆行を超えるデータが入っているようです。

最後に、大規模なデータを持っていて超早い分析系のクエリを求めるプロダクト(アドテク、EC、ゲーム・メディアのユーザ分析等)なら本当に検証の価値ありだと思います。クエリが相当早くなりますし、データも軽量になります。

また、本番投入できるかの見極めについては、 「データ入れ直しが許容時間内に終わる見通し」が立つのであるならば本番投入できる可能性が高いと言えるのではないかと考えています。

https://clickhouse.yandex/docs/en/single/index.html#distributed

以上。

Date
Author
エンジニアの視点から、様々な技術、サービス開発秘話、イベントをご紹介していきます。 ジーニーエンジニアチーム

はじめまして。このブログの編集長を務めることになりました、R&D本部マーケティングオートメーション開発部の張です。ジーニーへは、2016年4月に入社して、主にフロントエンドを担当しています。

初回ということで、CTOの篠塚とブログを始めるきっかけや、ジーニーの開発部門の紹介をさせていただきます。

(写真左:CTO・篠塚 英伸、右:エンジニアブログ編集長・張 志鋒)

ブログを始めるワケ

張:

ジーニーは、結構、面白い技術を使っていて、色々発信したいと思っていたんです。でも、ブログもなく、あまり社外に伝える機会がないな、と思っていました。

篠塚(CTO):

そうですね。R&D本部全体では、「誇れるものをつくろう」といつも言っているので、「誇れるものをつくったら、自慢しなきゃ!」と。それを発信していきたいというのが、このブログを始めた1つのきっかけですね。
あと、自分で勉強して、プロダクト開発に反映させて、「よかったね」と。それはそれで良いんですけど、エンジニアのキャリアやスキルアップを考えると、社内外の場でお互いに共有したり、発信したりしていくのも大事。そこでいろんな人からフィードバックを得て、ブラッシュアップしていく方が成長できると思うんですよ。

張:
このブログを通して、優秀なエンジニアの方たちにジーニーの技術を知ってもらいたいし、それによって、友を呼びたいですね。

篠塚:
ジーニーは、あまり中のことを語らない人たちが多いけれど、アドテク自体がマニアックなので、おもしろい内容が書けるんじゃないかと思うんです。アドテクノロジーやマーケティングテクノロジーについて、興味を持ってくれるエンジニアが増えたらいいなと。そして、仲間が増えたら嬉しいですね。

ジーニーのエンジニアリング部門

篠塚:
R&D本部には、「アド・プラットフォーム開発部」「マーケティングオートメーション開発部」「基盤技術開発部」「経営情報システム開発部」の4つの部があり、それぞれ25人・15人・10人・10人で合計60人ぐらいいます。

張:
メンバーのバックグラウンドは、コンピュータサイエンス系が多いですが、文系の人もいれば、美大(アート)出身者もいて、面白い人が集まっています。ちなみに僕は経済学部でした。

篠塚:
張さん、文系だったんだ!びっくり。
新卒も8割くらいはコンピュータサイエンス出身ですね。理工系も少しいます。

張:
中途では、1/3ぐらいがアドテク出身で、SI出身も多いですね。あとは、ゲーム系とか、Webサービス系とか、いろんなバックグラウンドの人がいると思います。

篠塚:
総じて、尖っている人が多い気がしますね。みんなそれぞれ特長的。
あと、女性は現状2名しかいません!

張:
特にアドテク業界は、女性が少ないんですかね?!

篠塚:
アドテク女子って、本当に少ない(笑)

張:
ジーニーは、女性も働きやすい環境ではあると思うんですけど。

篠塚:
男性も育休取っていますし。エンジニアではないですが、女性社員で産休育休をとって復帰している前例もあります。女性の感性がもっとプロダクトに入ると、いいと思うんですよね。

仕事の仕方や雰囲気

張:
僕は、何でも屋ですね。以前は、フロントエンドとバックエンドが分かれていましたが、最近は、一緒に仕事をするようになってきています。タスクに対して、みんなで力を合わせてやるフルスタックというイメージです。

篠塚:
そうですね、インフラは分かれていますが、フルスタックを目指しています。
もちろん、人によって得意領域はあるけれど、他の人の業務を知らないと、最適な設計ができないと思います。
これまで規模が小さい頃は、とにかくスピード重視で役割を分けてやってきました。しかし、規模も大きくなり、今後、ジーニープラットフォームとして連携していくことを考えると、技術的な切り分けではなく、タスクの解決方法に合わせて分担していく方がいいと考えています。そのためには、エンジニア1人ひとりが広範囲のことを知らなければいけないですね。

張:
もちろん初めから全部できる、というのは難しいですが、例えば、データベースは何を使っているかとか、どういう構成で何を処理しているかなど、プロダクトを知らないと、フロントでもアプリケーションでもモノづくりはできません。お互いを知らないと、チームとして動けないんですよね。

篠塚:
この体制は、特長的かもしれませんね。

張:
最近は、チームを越えてお互いを知るようになってきたと思います。もっともっと、壁をなくしていきたいですね。そのための1つの方法として、週1回社内勉強会をやっています。こちらのチームでは新技術を使って開発しているのに、隣のチームはよくわからない、というのではもったいないですから。プログラミング業界でよく言われるような“車輪の再発明”とかしたくないからね。知識が共有できれば、ジーニーの開発パワーはもっと上がってくると思うんです。

篠塚:
勉強会のほかには、月に一度、R&Dの全体会があります。メンバー持ち回りでLTをやっているんですけど、わりと面白いですよ。飲み会とかより、ずっと盛り上がるんです。この前のLTでは、あるメンバーが「モテたいから髪色を変えたい」ということで、似合う色を判定するためだけの仕組みをわざわざAI使って作ってみましたって発表がありました。みんな大爆笑でしたよ。「変えればいいじゃん!」って。そんな雰囲気の部門です(笑)

ジーニーならではのこと

篠塚:
普段よく目に触れるWebサービスは、UIを提供するのが一般的ですが、アドテクやマーケティングオートメーションでは、見える部分がほとんどありません。表には出ませんが、例えば、潜在顧客に1通メールを出したり、1つバナー広告が表示されたりするまでには、いろいろなドラマがあるんですよ!データを集めて、次も出すべきかどうかというスコアリングや分析をして、良かったのか悪かったのか、その結果をフィードバックする仕組みは、顧客のマーケティング施策全体や業績に大きく関わってきます。目に見える部分はほんのわずかですが、裏側ではけっこう壮大なことをやっているんですよ。

張:
MAJIN(マーケティングオートメーションツール)も、目に見えないところで結構頑張っているんですよ。MAJINは、フロントもバックもなかなか複雑で、データベースの開発とかにも手を出そうとしています。最先端の論文で研究して技術を実装してみたり、普通ならあまり使わない言語を使ってみたり、いろいろ試して、改善して、結果どんどん良くなっています。ジーニーの場合、実験的な試みや失敗も許容してくれる環境があるので、最先端の技術にチャレンジできるのがいいですね。

篠塚:
他社が採用していないような技術も、積極的に研究したり、実験的に採用したり、産学連携したり。いろいろやっていることを活かして、オープンソースへの貢献もしていきたいですね。

張:
新しい技術の開発は、他社だとあまり裁量権がもらえないことが多いと思うんです。僕の場合、どんどん勉強して、実際に試せるというのは、ジーニーに入って良かったことの1つだと思います。

篠塚:
営業のみんなが頑張ってくれているから、エンジニアにチャレンジできる余裕が生まれるので、ありがたいですね。いい循環が生まれていると思います。
あと、ジーニーの特徴として、データ量(トラフィック)が多いことがあります。1日のデータ処理量は、だいたい15テラくらいあるんですが、それを効率よく24時間365日動かし続けなければいけない環境って、わりと珍しいと思うんですよ。

張:
MAJINの方では、機械学習も強化していますし、AI活用も進んでくるので、そういうチャレンジをしたい方にもいい環境だと思います。

アドテクノロジーで世界を変える(=ジーニーのミッション)

篠塚:
世の中にまだ解決できてない課題があって、それを解決すると、少し世界がよくなります。なぜ、いろいろな課題が解決できないかというと、アイデアがないわけではなくて、いろいろな困難があって越えられないんですよ。
顧客の本質的な課題にしっかり応えるために、必要な技術を見極めて、ベストなものを真面目につくる。有り体のものを使うのではなく、自分たちにしかできないものを自分たちの手でつくるんです。課題自体がどんどん複雑になっているので、必然的に最先端の技術を取り入れていかなければならないわけです。そうやって、1つずつ課題を解決していくと、世の中が良くなっていくんです。それが僕たちのミッションだし、それができると「誇れるものがつくれた」ということになります。

張:
その通りです。困難があって、他の人が解決できなかったことを、僕らがエンジニアリングで解決できたら達成感がありますよね。

篠塚:
MAだったら、どこまでオートメーションにすると、本当にマーケターが楽になるかとか、人では思いつかない(できない)成果が得られるかとか。そういった壁を一つひとつ解決していくことが大事だと思っています。

張:
こんな僕たちの学びとか気づきとか、勉強会や研究について、このブログで発信していきたいと思っています。
よろしくお願いします!

Date
Author
エンジニアの視点から、様々な技術、サービス開発秘話、イベントをご紹介していきます。 ジーニーエンジニアチーム
Back to top