Upload
lintaro-ina
View
12.210
Download
2
Embed Size (px)
Citation preview
はてなブックマーク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系言語への応用を研究
いちから作りなおし!
いちから作りなおし!
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 Patternpackage 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 toResults[R, Q](
ms: Seq[Monad[R, Q]]
): Seq[R] = ... // HasA.map を使った処理
implicit def toResultOption[R, Q](
m: Monad[R, Q]
): Option[R] = toResults(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による紐づけは変換時にやってくれる
CICICICICICICI
CI
dockerで
◮ 単一の.jarファイルを生成◮ テストを実行◮ 開発用ホスト環境 (予定)
◮ chrootして本番環境に?
生成した.jarファイル
◮ テストに使用 (本番と同一バイナリ)
◮ デプロイに使用
まとめ
◮ はてなブックマークを作りなおし◮ Perlと Scalaのハイブリッド
◮ 薄いフレームワークを採用◮ ScalaでのDDD実践方法を模索◮ dockerでモダンなCI
WE ARE HIRING
◮ Scalaエンジニア 絶賛募集中◮ 東京 / 京都 どちらの勤務でも可