日本最大級の求人検索エンジン「スタンバイ」を支える技術
スタンバイの検索機能について
- スタンバイ:800万件以上の様々な求人情報をまとめて検索
- 検索サーバは、Elasticsearchのクラスタ(+プラグイン)
- Elasticsearch
- スタンバイ的には、クラスタでノードを管理しやすい、プラグインで拡張しやすい
- データノード(index)
- 検索API ⇒ コーディネートノード(検索用)
- Spark/更新バッチ ⇒ コーディネートノード(更新用)
- 更新頻度が高いため、検索が共倒れにならないように検索用/更新用を分けている
- 検索の種類
- キーワード検索:タイトル、説明文、勤務時間などを全文検索
- 勤務地検索:勤務地を緯度経度変換しておいて位置情報検索
- LuceneだとAnalyzerをどうするかが検索のポイントになる(転置インデックス作成)
- CharFilter:文字単位で変換(例:①⇒1)
- Tokenizer:単語に分割
- TokenFilter:単語単位で変換(例:長音の伸ばしを切る、助詞を切る)
- Analyzerの利用箇所
- 検索時、インデクシング時に利用、ただし必ずしも同じAnalyzerを利用する必要はない
- 連載:OSS全文検索サーバFess入門
スタンバイのインデックスとクエリー
- 適切なスコアリングと検索漏れがないようにする
- 形態素解析 + よみがな * bi-gramの3つでそれぞれindexを作成している
- copy_toして、他のanalyzerでもindexを作成している(マルチフィールドでもよいが、余分なフィールドを増やさないため)
- クエリー
- or検索
- phrase(単語順を維持)のmulti_match(複数のフィールドに対してクエリに投げる)、job_title, job_content, bigram_content, reading_content(重みづけしている、例)job_title: 0.8)
- minimum_should_match: 単語順が異なる、多少かけても当たる(文章で検索したような場合も、それなりに拾えるようにして0件Hitを回避)、重みづけは0.01にしてphraseを優先
- Elasticsearchの拡張:新しいpathで受け付けるentry pointを作ることができる
- Elasticsearchのバージョンに依存するので、次のバージョンでそのまま動くことはあまりない・・・
検索課題と対応
- インデックスのバックアップ
- スナップショットを取得する仕組みを利用、分析や学習などではリストアされた環境を利用(現状、リアルタイム性が求められる集計はない)
- 1日以内は毎時で取得、2週間以内は1日単位、それ以上は1ヵ月に1つに間引いている(S3に保存)
- 独自のSimilarity
- SEO対策したもののスコアを下げたい(マッチしたキーワードの頻出回数が高すぎるものなど)
- BM25Similarityを継承して作っている
- 辞書ファイルの配布
- NFSにマウントでもいい
- REST的にAPIでファイルを配布したかったので、ConfigSyncプラグインを導入
- サーチテンプレート
- 業務ロジックと検索クエリーは切り分けて管理したい
- ちょっとしたロジックも書きたいので、Script-based Search Templateプラグイン + Velocityテンプレートを導入
- 再起動せずに辞書ファイルを再読み込みしたい
- 検索結果の並び替え
- 同じ求人名や同じ媒体が並んで表示されないようにしたい
- 検索結果の上位部分だけでも改善する
- DynaRankプラグインを導入:ヒットした上位N件を並び替える
- Minhashプラグインを導入:求人タイトルのビット列をインデックスに入れて、ビット列で類似度を判断して並び替える
- 無停止でインデックスの設定・マッピングを変更したい
- Indexing Proxyが更新リクエストをファイルに保存、別スレッドでindexを反映
- 新しいindexに再indexingしてコピーしてから、新しいindexにもファイルから書込み
- aliasを新しいindexに張り替える、古いindexは削除する
機械学習
- 職種・業種推定
- 求人情報には、ユニークな職種・業種のフィールドが存在しない
- 自然言語処理で求人の特徴を抽出、その特徴を学習して職種業種を推定、Chainerを利用
- 年収推定
- 年収非公開案件もあるため、求人の特徴を抽出、その特徴を学習して年収推定
今後
- オートスケーリングによるノード数の増減
- Learning To Rankによる検索結果の最適化
- Word2Vecなどの応用
質疑
- クエリーチューリングの評価は?
- 手元で検索して見ている、明らかにおかしくなればCTRが下がる
Java10まとめと、どうなるJava11
サポート
- 年月のバージョンになるという話はなくなり、従来通りメジャーバージョンをインクリメント
- Maintenance Releaseはリビジョンが上がる(4月が10.0.1、7月に10.0.2)、マイナーバージョンは常に0
- 2018/9はJDK11 LTS(バージョン名にLTSがつく)
- Java SE8のサポートは2019/1まで延長された(JDK11リリースの3ヵ月後)、個人ユーザは2020/12までサポート
- JDK 11以降
- AppletとWeb StartはJDK11からはサポートされない
- JavaFXがバンドルされなくなる(開発は進んでいく模様)、もともとOpenJDKにはbundleされていなかった
- AWT / SwingはJDK11対応で活発にコミットされている(機能拡張というよりは不具合解消)
JDK10
- 一番派手な変更は、Local-Variable Type Inferences
- varはkeywordではなく、特別な型(varという変数/methodは定義できるが、varというclassは定義できなくなった)
- 利用例:匿名クラスを変数に割り当てる、new ArrayList<>()のように右側に型名が書いてあるとき、(Optional/Fluxなどくるんだ型はvarにしない方がいい気がする)
- Java-Based JIT Compiler, Project Metoropolise (Graal)
- Prarell Full GC for G1
- そもそもFull GCを起こさないべきではあるが・・・
- Heap Allocation on Altenative Memory Devices
- 3D Xpointなどの不揮発メモリにHeapを割当てられるようになった
- Root Certificates
- OpenJDKにもRoot Certificatesが含まれるようになった
- API Changes
- java.io.Reader transferTo(Writer)
- process idが取れるようになった
- java.util.List/Map/Set copyOf(Collection)
- toUnmodifiableList/Set/Map
- Guavaを使う必要性が減った
- Docker
- CPU count/Memory sizeをちゃんと見るようになった
- 32bit版のJDKがbundleされなくなった
JDK11
- Launch Single-File Source-Code Programs
- ソースコードが1つであれば、コンパイルしなくてもいい
- java Hello.java
- Raw String Literals
-
- indentをどう扱うかは議論中、それがまとまらないと入らないかも
-
- Switch Expression
- 合わせて、caseに複数の値が書けるようになる
- Local-Variable Syntax for Lambda parameters
- HTTP Client
- Epsilon: A No-Op Garbage Collector
- GCのパフォーマンスを評価するための基準、Serverlessにもいいかも
- JavaEEとCORBAが外れる
- Flight RecorderがOpenSourceになった
- Unicode9とUnicode10に対応
- Nestmate
- Project Valhallaの成果物
- String
- repeat(): あらかじめ文字数分のメモリ領域を取る
- strip(): 全角スペースもtrimされる
- lines()
- Raw Literalの実装のために必要
- Predicate::not
Support
- OracleJDK
- web servicerにとっては非常に高い
- 100 Servers on AWS -> 1億円(価格表ベースでは)
- OpenJDK
- それぞれのリリースから6ヵ月しかサポートされない(セキュリティパッチの提供)
- OpenJDKもLTSが入るかも、Mark Reinholdは言っているが正式発表はない、LTS入ってもoverlapがないかも
- AdoptOpenJDK
- IBMスポンサー、London JUG
- 4年間のLTSサポートを提供
- Zulu
- 100 servers $28,750/year
- Unlimited servers $258,750/year
古いフレームワークでもマイクロサービスアーキテクチャにしたい
- 古いサービスをマイクロサービスに入れたい
- 運用負荷を下げる(共通基盤に乗せる)
- サービスを分割してリプレースを進めやすくする
Spring Cloud Config
- Netflix Eureka: Service Discovery(内部DNS)
- Spring Cloud Config:DB接続文字列などを保存
- SpringBoot以外の古いサービスでもSpring Cloud Configを起動時に読み込みたい
- RibbonClient
- Eureka上のアプリケーション情報をキャッシュしている
- サービスへのアクセスをロードバランシングする
- Eureka Client
- Ribbon Clientの中に入っているEureka Client
- ロードバランシングしない、サービス名に対して対象のサーバの場所を返してくれる最小限の機能
- eureka.registration.enabled=falseで使用した
- 既存フレームワークで利用していたRibblon Clientの中のEureka Clientのアプリケーション情報が更新されなくなった(スケールインができなくなる)
- Singletonのものを2つNewしてしまった
- DIコンテナから取得するにも、warの中にSeasarとSpringのDIコンテナが混在しており、Seasar側から取れなかった
- Eureka Clientを利用せずに、Http Clientを利用してEurekaのREST APIを直接叩いて、データを取得
Spring Cloud Stream(Apahce Kafka)
- 古いサービスのある処理に後処理を追加したくなった
- push通知、メール送信機能
- 他のサービスで利用している処理と同じもの、共通の処理をサービスごとに書きたくない
- モジュールにすると、古いサービスで利用できる形に合わせる必要が出てくる
- 非同期的に実行されていてほしい
- Apacke Kafkaを利用(Sourceは1つ、Sinkは2つ)
- Topicの設計
- 例:ある機能の後にPush通知する
- 「何をしたか」をTipicにする、例)いいねした/フォローした/投稿をした("XXXにYYYをPush通知する"をTopicにしない)
- メッセージ内容の変更方法
- いっそTopicを新しく作成するほうがよい
- Source側が新しいTopicにメッセージを送信しはじめてから、Sink側は新しいTopicを参照する
Logicadの秒間16万リクエストをさばく広告入札システムにおける、gRPCの活用事例
- Sonet Media Networks: DSP事業(Logicad)を提供
- Real-Time Bidding
- SSPがWebサイトの広告枠を管理
- DSP各社が入札リクエスト
- タイムアウトが発生するとオークションに参加できない(売上にならない) ⇒ レイテンシの低減 + スループット向上
- レイテンシ
- 最大100ms以内に返す必要がある
- ネットワークレイテンシ(東京~東京1-2ms、東京~台湾65ms) + 入札処理のレイテンシ
- Logicadの入札処理は平均3ms
- スループットも秒間16万件くらい処理
- アーキテクチャ構成
- nginxと入札サーバは数十台をメッシュ構造にしている
- 広告商品情報(画像URL、サイズ、LPなど)はレイテンシのためローカルに情報を保持
- Aerospike(KVS):ユーザ情報、広告予算消化情報
- AWS RDS:広告キャンペーン情報(事前定義、定期的にロードする必要がある情報を格納)
- Redis:アドフラウド情報(bot判定、3rd Partyから提供された情報を格納)
- gRPC採用前
- 全サーバに同一広告商品情報を持つ、データ量増大に伴ってスケールアップが必要
- TB級のデータ(億オーダーの商品数)になり各サーバで保持できるサイズを超える
- レイテンシ・スループットを維持しつつ、広告商品情報サーバを立てることに
- ロードバランスも必要
- 入札サーバがgRPCクライアント、クラスタ構成を組んだ広告商品情報サーバをgRPC Serverとして構築(Java8)
gRPC
- 選定候補
- Redis:シングルスレッドで動作するためスループットに影響あり(ブロッキングされる)
- Aerospike:データがシャーディングされておりレイテンシに影響あり(複数回の通信が発生してしまう、マルチスレッドにするとスレッド・コネクションが増える)
- gRPC + ローカルDB:採用
- gRPC
- Googleが開発した高速なRPCフレームワーク
- HTTP/2, Protocol Buffersを用いて高速化
- Google, Netflix, Docker, Ciscoなど
- gRPCのよいところ
- レイテンシの低減:HTTP/2(バイナリフレーム、インデックス化によるヘッダー圧縮、非SSL)、Protocaol Buffers(データ圧縮)
- スループット:HTTP/2(多重化)、クライアントサイドロードバランシング
- h2c
- HTTP/2 over TCP:ブラウザでは使えないが、内部ネットワークのみであればTLSを使わない通信を採用できる
- マルチプレキシング
- 1つのTCPコネクションで数万接続でも並列化が可能、3 way handshakeなどのオーバヘッドが削減できる、ブロッキングされない
- Protocol Buffers
- Google社製のシリアライザ
- IDL
- フィールド名をデータとして持たずにtagを使う、文字列より数値型を扱うように設計した方が圧縮率が高まる
- クライアントサイドロードバランシング
- gRPC ClientのName Resolver(DNS)を拡張して、動的なgRPCサーバの増減に対応(API or 定期実行でサーバリストを更新)
- nginxなど外部のロードバランサが不要で管理が楽
gRPCを実際に適用してみた結果
- ベンチマーク
- 広告商品情報サーバ単体でのストレステスト(最大のスループット、Connection数の変化を計測)
- 負荷試験環境でのレイテンシ、スループットの測定(入札の一連の処理)
- gRPCでもJMeterを用いて負荷をかけることが可能
- JavaSamplerClientを実装したクラスを用意して、任意のプロトコルをリクエストすることが可能
- 負荷試験
- Java8, 128GB, 8Coe/16thread
- 1億件、1.6TB
- 秒間数万件は問題なく処理できた ⇒ 広告商品情報サーバは3-4台用意
- Connection数はスループットに比例せずに一定だった(マルチプレキシングにより少ないTCPコネクションで処理できる)
- ローカル処理を外部化したにも関わらず、1割程度の変化(0.1ms程度の悪化)に収まった
DDDとクリーンアーキテクチャでサーバーアプリケーションを作っている話
- 突貫工事はうまくいかない
- 仕様の概要は決まった、コア機能のいくつかはパーツが存在していたのでそれらを使って動くものが早く見たい
- 機能仕様を作りながらプロトタイピングを進めた
- いろいろ押し押しになりプロトタイプがいつの間にかプロダクションコードになりそうになった ⇒ リリースできなかった
- もろもろの反省
- サーバから全クライアントをダウンロードして処理(異常なトラフィック量、クライアント処理負荷増)
- 外部連携先の仕様変更の反映が全コンポーネントに反映
- ビジネスロジックの主体となるコンポーネントが不在、各コンポーネントはコアロジックに集中できず変換処理を負担
設計
- 設計の順番
- エレベーターピッチを定義
- 機能仕様・利用シナリオを定義(非機能要件も定義)
- ロバストネス分析
- 境界づけられたコンテキストの抽出
- コンテキストマップの作成
- ロバストネス分析
- あらかじめユースケースシナリオを作成
- シナリオを満たす機能をどう作ったらよいか考える
- バウンダリ(画面、cron)、エンティティ(管理するデータ)、コントロール(処理。ユーザー認証とか値の取得など)でそれを表現する
- ノートに手書き、書いては消し、二人で分析し良さそうな方を採用
- コンテキストマップ
- ロバストネス図で出てきた機能について、似たものを集めて「境界づけられたコンテキスト」を規定
- 書いて→直して→書いて→直して
- コンテキスト間の関係について慎重に判断した(パートナーシップ、顧客/供給者、順応者)
- ユビキタス言語
- コンテキストごとに独自の用語を制定
- システムとしてはコンテキスト間で用語を統一した方が分かりやすく、同じ用語を別コンテキストで別の意味で使う、ということはしていない
- プログラム上での命名も決めた → これは好評
実装前に準備したこと
- ルール
- Gitリポジトリ運用、コーディング規約、パッケージ構成、Rest API規約、ログレベル
- 例外は発生したところに近いところでクラス定義している
- ソースコードレビュー観点
- 別の人が保守できるのか、今後の拡張性はあるか、無駄はないか、影響範囲が広すぎないか ⇒ Wikiに記載
- 安全性(Autowiredしたクラスのインスタンス変数を書き換えている、キャッシュのexpireが考慮されているか)
- 各コンテキスト内のアーキテクチャ
各コンテキスト内のアーキテクチャ
- Interface層
- Controller: Request
- Presenter: Response
- Gateway: Storage, 外部サービス
- それぞれにTranslator(自分のドメインの言葉と外部の言葉を変換)
- Usecase層
- Domain層
- Entity, ValueObject, Service, Repository
- Published Language:そのコンテキストの入出力データを定義、各コンテキストは呼び出し先のpl配下のクラスを利用
- library:ビジネスロジックに全く関与しない、複数コンテキスト間で共有する価値のあるライブラリ
- リポジトリの実装
- Domainではインタフェースだけ定義して、Gatewayで実処理
- DomainのRepositoryはテーブルと1:1対応ではないように定義(実際にどこに保存されるかを意識しないように定義)
- Gatewayは物理的な部分を定義
実装してみて分かったこと
- 関心事に集中できる
- @Transactionalで考えないといけない範囲が狭くていい
- 用語が統一されているので外部仕様からコードまですっきりつながる
- 処理の流れがパターン化されているので追いやすい
- 変換の嵐…
- よかったこと
- モノリスからの解放、各担当者が自分の担当部分のビジネスロジックに集中できる、RDB/キャッシュ分離で責任範囲が明確(DBをわが物として使える)
- 新技術への挑戦:Docker, AWS ECS(Fargateにしたかったが日本に来ておらず200-300msはレイテンシがあるため断念), GitLab CI
- 改善の余地あり
- ルールの徹底