SlideShare a Scribd company logo
はてなブックマーク
in Scala
伊奈 林太郎
id:tarao @oarat
2015-08-01
@ Scala 関西 Summit 2015
自己紹介
名前  
い な
伊奈   
りんたろう
林太郎  (id:tarao @oarat)
2008-08 はてなインターン
2008-10 はてなアルバイト (ブックマークチーム)
2010-04 日本学術振興会 特別研究員 (DC1)
2013-04 はてな正社員
2013-12 ブックマークチーム
◮ アルゴリズム屋さん
◮ 検索技術 > 機械学習 ≫ アドテク
◮ 最近は設計したり基盤寄りな部分を担当したり
自己紹介
名前  
い な
伊奈   
りんたろう
林太郎  (id:tarao @oarat)
Scala 歴: 4ヶ月
◮ slick-jdbc-extension
◮ 型レベルのラムダ計算
◮ Software Design 2015 年 8 月号
大学時代は
◮ 研究室: 型理論, プログラム意味論, 証明支援系
◮ OCaml が公用語
◮  
gradual typing
漸進的型付け の Java 系言語への応用を研究
はてなブックマーク in Scala
いちから作りなおし!
いちから作りなおし!
Scalaで!
いちから作りなおし!
Scalaで!
ねらい
◮ コードベースの肥大化・老朽化への対処
◮ 根本的なアーキテクチャの再設計
DISCLAIMER
◮ 内容は開発中のもの
◮ 実際のリリース時には変更の可能性あり
◮ 最終的にどうなったか公表するかどうか未定
構成
◮ 前後で Perl/Scala に分割
◮ コアは B!以外からも利用
◮ Presso
◮ B!KUMA
ビュー (Perl)
◮ ユーザ認証
◮ HTML のレンダリング
コア (Scala)
◮ API サーバ
◮ 社内基盤的側面
Why Scala?
◮ 変更の少ない重要部分は堅牢にしたい
◮ 型安全性
◮ ドメイン駆動設計
◮ Mackerel チームでの使用実績
◮ LL 勢にも書きやすい
◮ エンジニアのチーム異動が容易になる
◮ Perl との心中を避ける
◮ 個人的には
◮ 新言語を導入するなら関数型でないと許さん
◮ 強い静的型付言語でないと許さん
◮ 型推論ないと許さん
Why Perl?
◮ 頻繁に変更があっても開発が楽
◮ デザイナも HTML テンプレートを触る
◮ コンパイルしなおさなくてよい
◮ ローカル開発環境での作業が容易
◮ 学習コスト 0
◮ 認証まわりは共通 Perl モジュールでやりたい
◮ あとで捨ててまた作り直すかもしれない
フレームワークフレームワークフレームワークフレームワークフレームワークフレームワークフレームワーク
これまで (Perl)
第 1 世代 Apache mod perl 上の簡易的なもの
第 2 世代 内製の Perl 版 RoR 的な重厚なもの
◮ 学習コスト高
◮ 自由がきかない
第 3 世代 薄いフレームワークの集合
◮ よくできた小モジュールの組み合わせ
◮ 自由度が高く入れ替え可能
◮ ≫ YAPC::Asia 2015 talk by id:hitode909
新コアサーバ (Scala)
Web
◮ Scalatra
◮ APISchemaのScala 版 (予定)
DB
◮ Slick 3.0
◮ slick-jdbc-extension
◮ 独自のべんりに使う層
テスト
◮ ScalaTest
Webフレームワーク
Scalatra
◮ API サーバなので簡素でよい
◮ 返り値が Any なの嫌なのでラップして利用
APISchema
◮ Perl での実績を元に Scala 版を実装予定
◮ 簡単な DSL で定義
◮ JSON Schema によるリソース定義
◮ パスごとのリクエストとレスポンス定義
◮ 同じ定義から自動生成
◮ ドキュメント
◮ ルーティング処理
DBフレームワーク
Slick 3.0
◮ 非同期 DB アクセスも利用したい
◮ 文字列補間による生 SQL のみで利用
◮ 予想外のクエリが生成されるのを防ぐ
◮ インフラエンジニアでも読めるように
◮ (あとでクエリビルダ導入の可能性はある)
◮ 拡張モジュール slick-jdbc-extension
◮ 文字列補間のリストの扱いなどを強化
◮ カラム名での結果型へのマッピング
◮ ≫ 『Scala で生 SQL - Slick の SQL 補間子にリストを渡す 他』
DBフレームワーク
独自層
◮ DB インスタンス管理
◮ マスター/スレーブ切り替え
◮ テスト時のプロセス毎 DB 分離
◮ モード管理 (トランザクション, 非同期)
◮ リクエストスレッドごとの非同期接続数管理
ドメイン駆動設計ドメイン駆動設計ドメイン駆動設計ドメイン駆動設計ドメイン駆動設計ドメイン駆動設計ドメイン駆動設計
旧ブックマークでは
DDDっぽいこともやられていた
◮ アプリケーションサービス
◮ ドメインサービス
◮ ドメインモデル
◮ CQRS (コマンドクエリ責務分離)
旧ブックマークでは
失敗
◮ ユビキタス言語がないまま進んだ
◮ e.g. お気に入り or フォロー
◮ e.g. 3 つの「インタレスト」概念
◮ 方針が徹底されなかった
◮ アプリ層とコントローラ層が互いに侵食
◮ 内製の Perl 版 Active Record 的 O/R マッパ
◮ モデル, サービス, リポジトリが渾然一体
◮ 近頃の他 Perl プロダクトではまともになった
◮ Perl の限界
◮ インタフェースが言語機能にない
きちんとDDDしたい!
◮ 有志の社内勉強会を実施
◮ 非エンジニアメンバーとも共有・議論
◮ Scala でのよいやり方を模索
方針
◮ リポジトリはインタフェース (依存関係逆転の原則)
◮ ドメインに実装上の都合を持ち込まない
悩んだ点と作戦
◮ 依存性の注入どうするか
◮ Cake Pattern を積極採用
◮ has-a 関係を引くときの N+1 問題
◮ 関係モナドを定義
◮ トランザクションどこで張るか
◮ いったん極小な範囲 (インフラ層) で
◮ 全体的に結果整合性
悩んだ点と作戦
◮ 依存性の注入どうするか
◮ Cake Pattern を積極採用
◮ has-a 関係を引くときの N+1 問題
◮ 関係モナドを定義
◮ トランザクションどこで張るか
◮ いったん極小な範囲 (インフラ層) で
◮ 全体的に結果整合性
Cake Pattern
package repo
trait SomeComponent {
def someLoader: SomeLoader
// リポジトリインタフェース
trait SomeLoader {
def find(...) = ...
} }
package db
trait SomeComponent
extends repo.SomeComponent {
def someLoader: SomeLoader =
SomeLoader
// 実装
object SomeLoader
extends SomeLoader { ... }
}
package app
trait ServiceComponent {
// 依存の明示
self: repo.SomeComponent =>
trait SomeService {
...
someLoader.find(...)
...
} }
package main
object AppRoot
extends db.SomeComponent
with app.ServiceComponent
with ...
Cake Pattern
ポイント
◮ trait を入れ子にしておく
◮ 使う側は自分型アノテーションで依存を明示
◮ AppRootには実装コンポーネントを結合
◮ TestRoot等を用意して別実装に入れ替えも可
◮ 単体テストでコンポーネント単位で入れ替え
◮ 全体でテスト用 DB ハンドラ実装に入れ替え
object TestRoot
extends repo.SomeMockedComponent
with app.ServiceComponent
with ...
N+1問題1 件の場合
val bookmark: Bookmark = ...
val locaiton: Location = bookmark.toLocation
// SELECT * FROM location WHERE ...
n 件の場合: n + 1 回のクエリが必要
val bookmarks: Seq[Bookmark] = ...
// SELECT * FROM bookmark WHERE ...
val locations: Seq[Location] = bookmarks.map(_.toLocation)
// SELECT * FROM location WHERE ...
// SELECT * FROM location WHERE ...
//
... × n
本当はせいぜい 2 回で済む
val locations: Seq[Location] =
locationLoader.findAll(bookmarks.map(_.locationId))
// SELECT * FROM location WHERE location_id IN (...)
N+1問題回避策(1) JOIN
解決方法
◮ bookmarksと locaitonsをいっぺんに引く
◮ JOIN して引けば可能
問題点
◮ 一般的には JOIN したくない場合もある
◮ e.g. bookmarksが入力となるサービス内
◮ NG: 実装上の都合がモデルの引き方を左右する
N+1問題回避策(2) 愚直に
解決方法
◮ ていねいに関係先を引いてくる
問題点
◮ 元の要素と対応づけたい場合に面倒
val bookmarks: Seq[Bookmark] = ...
val locations: Seq[Location] =
locationLoader.findAll(bookmarks.map(_.locationId))
val id2loc = locations.map{ l => l.id -> l }.toMap
val bookmarkAndLocationList: Seq[(Bookmark, Location)] =
bookmarks.map{ b => (b, id2loc(b.locationId)) }
関係モナド
class BookmarkRelation(b: Bookmark) {
def toLocation: Monad[Location, Bookmark] =
HasA.Monadic(b, new HasA[Bookmark, Location] { ... }) }
implicit def bookmarkRel(b: Bookmark) =
new BookmarkRelation(b)
val bookmark: Bookmark = ... // 1 件の場合
val location: Option[Location] = bookmark.toLocation
val bookmarks: Seq[Bookmark] = ... // n 件の場合
val locations: Seq[Location] = bookmarks.map(_.toLocation)
関係モナド
class BookmarkRelation(b: Bookmark) {
def toLocation: Monad[Location, Bookmark] =
HasA.Monadic(b, new HasA[Bookmark, Location] { ... }) }
implicit def bookmarkRel(b: Bookmark) =
new BookmarkRelation(b)
val bookmark: Bookmark = ... // 1 件の場合
val location: Option[Location] = bookmark.toLocation
val bookmarks: Seq[Bookmark] = ... // n 件の場合
val locations: Seq[Location] = bookmarks.map(_.toLocation)
◮ toLocationの返り値はモナドのインスタンス
関係モナド
class BookmarkRelation(b: Bookmark) {
def toLocation: Monad[Location, Bookmark] =
HasA.Monadic(b, new HasA[Bookmark, Location] { ... }) }
implicit def bookmarkRel(b: Bookmark) =
new BookmarkRelation(b)
val bookmark: Bookmark = ... // 1 件の場合
val location: Option[Location] = bookmark.toLocation
val bookmarks: Seq[Bookmark] = ... // n 件の場合
val locations: Seq[Location] = bookmarks.map(_.toLocation)
◮ toLocationの返り値はモナドのインスタンス
◮ クエリはモナドから結果への暗黙変換で発生
関係モナド
class BookmarkRelation(b: Bookmark) {
def toLocation: Monad[Location, Bookmark] =
HasA.Monadic(b, new HasA[Bookmark, Location] { ... }) }
implicit def bookmarkRel(b: Bookmark) =
new BookmarkRelation(b)
val bookmark: Bookmark = ... // 1 件の場合
val location: Option[Location] = bookmark.toLocation
val bookmarks: Seq[Bookmark] = ... // n 件の場合
val locations: Seq[Location] = bookmarks.map(_.toLocation)
◮ toLocationの返り値はモナドのインスタンス
◮ クエリはモナドから結果への暗黙変換で発生
◮ 関係の引き方はモナド生成時に指定
関係モナド
モナドのパラメータ
◮ 複数件の引き方のみを実装
new HasA[Bookmark, Location] {
def map(bookmarks: Seq[Bookmark]): Seq[Location] =
locationLoader.findAll(bookmarks.map(_.locationId)) }
暗黙変換
◮ Seq[] を一気に変換して N+1 問題を解決
implicit def run[R, Q](
ms: Seq[Monad[R, Q]]
): Seq[R] = ... // HasA.map を使った処理
implicit def toResultOption[R, Q](
m: Monad[R, Q]
): Option[R] = run(Seq(m)).headOption
関係モナド
元の要素と対応づける場合
type JoinBookmarkLocation =
Join[(Bookmark, Location), LocationId, Bookmark, Location]
def withLocation = Join.Monadic(b, new JoinBookmarkLocation {
def map(bs: Seq[Bookmark]): Seq[Location] = ...
def leftKey(b: Bookmark): LocationId = b.locationId
def rightKey(l: Location): LocationId = l.id
def merge(b: Bookmark, l: Location) = (b, l)
})
val bookmarks: Seq[Bookmark] = ...
val bookmarkAndLocationList: Seq[(Bookmark, Location)] =
bookmarks.map(_.withLocation)
◮ ID による紐づけは変換時にやってくれる
詳しくは
OSS 化された実装
◮ github.com/tarao/bullet-scala
日本語での解説
◮ bullet-scala: N+1 クエリ問題を回避する
CICICICICICICI
CI
docker で
◮ 単一の.jar ファイルを生成
◮ テストを実行
◮ 開発用ホスト環境 (予定)
◮ chrootして本番環境に?
生成した.jar ファイル
◮ テストに使用 (本番と同一バイナリ)
◮ デプロイに使用
まとめ
◮ はてなブックマークを作りなおし
◮ Perl と Scala のハイブリッド
◮ 薄いフレームワークを採用
◮ Scala での DDD 実践方法を模索
◮ docker でモダンな CI
WE ARE HIRING
◮ Scala エンジニア 絶賛募集中
◮ 東京 / 京都 どちらの勤務でも可

More Related Content

はてなブックマーク in Scala