39
in Scala id:tarao @oarat 2015-08-01 @ Scala Summit 2015

はてなブックマーク in Scala

Embed Size (px)

Citation preview

Page 1: はてなブックマーク in Scala

はてなブックマークin Scala

伊奈 林太郎id:tarao @oarat

2015-08-01@ Scala関西 Summit 2015

Page 2: はてなブックマーク in Scala

自己紹介名前  

い な伊奈   

りんたろう林太郎  (id:tarao @oarat)

2008-08 はてなインターン2008-10 はてなアルバイト (ブックマークチーム)2010-04 日本学術振興会 特別研究員 (DC1)2013-04 はてな正社員2013-12 ブックマークチーム

◮ アルゴリズム屋さん◮ 検索技術 > 機械学習 ≫ アドテク◮ 最近は設計したり基盤寄りな部分を担当したり

Page 3: はてなブックマーク in Scala

自己紹介名前  

い な伊奈   

りんたろう林太郎  (id:tarao @oarat)

Scala歴: 4ヶ月◮ slick-jdbc-extension

◮ 型レベルのラムダ計算◮ Software Design 2015年 8月号

大学時代は

◮ 研究室: 型理論, プログラム意味論, 証明支援系◮ OCamlが公用語

◮  gradual typing

漸進的型付け の Java系言語への応用を研究

Page 4: はてなブックマーク in Scala
Page 5: はてなブックマーク in Scala

いちから作りなおし!

Page 6: はてなブックマーク in Scala

いちから作りなおし!

Scalaで!

Page 7: はてなブックマーク in Scala

いちから作りなおし!

Scalaで!

ねらい◮ コードベースの肥大化・老朽化への対処◮ 根本的なアーキテクチャの再設計

Page 8: はてなブックマーク in Scala

DISCLAIMER

◮ 内容は開発中のもの◮ 実際のリリース時には変更の可能性あり◮ 最終的にどうなったか公表するかどうか未定

Page 9: はてなブックマーク in Scala

構成◮ 前後でPerl/Scalaに分割◮ コアはB!以外からも利用

◮ Presso◮ B!KUMA

ビュー (Perl)

◮ ユーザ認証◮ HTMLのレンダリング

コア (Scala)

◮ APIサーバ◮ 社内基盤的側面

Page 10: はてなブックマーク in Scala

Why Scala?◮ 変更の少ない重要部分は堅牢にしたい

◮ 型安全性◮ ドメイン駆動設計

◮ Mackerelチームでの使用実績◮ LL勢にも書きやすい◮ エンジニアのチーム異動が容易になる

◮ Perlとの心中を避ける◮ 個人的には

◮ 新言語を導入するなら関数型でないと許さん◮ 強い静的型付言語でないと許さん◮ 型推論ないと許さん

Page 11: はてなブックマーク in Scala

Why Perl?

◮ 頻繁に変更があっても開発が楽◮ デザイナもHTMLテンプレートを触る

◮ コンパイルしなおさなくてよい◮ ローカル開発環境での作業が容易◮ 学習コスト 0

◮ 認証まわりは共通Perlモジュールでやりたい◮ あとで捨ててまた作り直すかもしれない

Page 12: はてなブックマーク in Scala

フレームワークフレームワークフレームワークフレームワークフレームワークフレームワークフレームワーク

Page 13: はてなブックマーク in Scala

これまで (Perl)

第 1世代 Apache mod perl上の簡易的なもの

第 2世代 内製のPerl版RoR的な重厚なもの

◮ 学習コスト高◮ 自由がきかない

第 3世代 薄いフレームワークの集合

◮ よくできた小モジュールの組み合わせ◮ 自由度が高く入れ替え可能◮ ≫ YAPC::Asia 2015 talk by id:hitode909

Page 14: はてなブックマーク in Scala

新コアサーバ (Scala)Web◮ Scalatra

◮ APISchemaのScala版 (予定)

DB◮ Slick 3.0

◮ slick-jdbc-extension

◮ 独自のべんりに使う層

テスト◮ ScalaTest

Page 15: はてなブックマーク in Scala

WebフレームワークScalatra

◮ APIサーバなので簡素でよい◮ 返り値が Anyなの嫌なのでラップして利用

APISchema

◮ Perlでの実績を元に Scala版を実装予定◮ 簡単なDSLで定義

◮ JSON Schemaによるリソース定義◮ パスごとのリクエストとレスポンス定義

◮ 同じ定義から自動生成◮ ドキュメント◮ ルーティング処理

Page 16: はてなブックマーク in Scala

DBフレームワークSlick 3.0

◮ 非同期DBアクセスも利用したい◮ 文字列補間による生 SQLのみで利用

◮ 予想外のクエリが生成されるのを防ぐ◮ インフラエンジニアでも読めるように◮ (あとでクエリビルダ導入の可能性はある)

◮ 拡張モジュール slick-jdbc-extension◮ 文字列補間のリストの扱いなどを強化◮ カラム名での結果型へのマッピング◮ ≫ 『Scalaで生 SQL - Slickの SQL補間子にリストを渡す 他』

Page 17: はてなブックマーク in Scala

DBフレームワーク独自層

◮ DBインスタンス管理◮ マスター/スレーブ切り替え◮ テスト時のプロセス毎DB分離

◮ モード管理 (トランザクション, 非同期)

◮ リクエストスレッドごとの非同期接続数管理

Page 18: はてなブックマーク in Scala

ドメイン駆動設計ドメイン駆動設計ドメイン駆動設計ドメイン駆動設計ドメイン駆動設計ドメイン駆動設計ドメイン駆動設計

Page 19: はてなブックマーク in Scala

旧ブックマークでは

DDDっぽいこともやられていた

◮ アプリケーションサービス◮ ドメインサービス◮ ドメインモデル◮ CQRS (コマンドクエリ責務分離)

Page 20: はてなブックマーク in Scala

旧ブックマークでは失敗

◮ ユビキタス言語がないまま進んだ◮ e.g. お気に入り or フォロー◮ e.g. 3つの「インタレスト」概念

◮ 方針が徹底されなかった◮ アプリ層とコントローラ層が互いに侵食

◮ 内製のPerl版Active Record的O/Rマッパ◮ モデル, サービス, リポジトリが渾然一体◮ 近頃の他Perlプロダクトではまともになった

◮ Perlの限界◮ インタフェースが言語機能にない

Page 21: はてなブックマーク in Scala

きちんとDDDしたい!

◮ 有志の社内勉強会を実施◮ 非エンジニアメンバーとも共有・議論◮ Scalaでのよいやり方を模索

Page 22: はてなブックマーク in Scala

方針

◮ リポジトリはインタフェース (依存関係逆転の原則)

◮ ドメインに実装上の都合を持ち込まない

Page 23: はてなブックマーク in Scala

悩んだ点と作戦

◮ 依存性の注入どうするか◮ Cake Patternを積極採用

◮ has-a関係を引くときのN+1問題◮ 関係モナドを定義

◮ トランザクションどこで張るか◮ いったん極小な範囲 (インフラ層)で◮ 全体的に結果整合性

Page 24: はてなブックマーク in Scala

悩んだ点と作戦

◮ 依存性の注入どうするか◮ Cake Patternを積極採用

◮ has-a関係を引くときのN+1問題◮ 関係モナドを定義

◮ トランザクションどこで張るか◮ いったん極小な範囲 (インフラ層)で◮ 全体的に結果整合性

Page 25: はてなブックマーク in Scala

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 ...

Page 26: はてなブックマーク in Scala

Cake Patternポイント

◮ traitを入れ子にしておく◮ 使う側は自分型アノテーションで依存を明示◮ AppRootには実装コンポーネントを結合◮ TestRoot等を用意して別実装に入れ替えも可

◮ 単体テストでコンポーネント単位で入れ替え◮ 全体でテスト用DBハンドラ実装に入れ替え

object TestRoot

extends repo.SomeMockedComponent

with app.ServiceComponent

with ...

Page 27: はてなブックマーク in Scala

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 (...)

Page 28: はてなブックマーク in Scala

N+1問題回避策(1) JOIN

解決方法

◮ bookmarksと locaitonsをいっぺんに引く◮ JOINして引けば可能

問題点

◮ 一般的には JOINしたくない場合もある◮ e.g. bookmarksが入力となるサービス内

◮ NG: 実装上の都合がモデルの引き方を左右する

Page 29: はてなブックマーク in Scala

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)) }

Page 30: はてなブックマーク in Scala

関係モナド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)

Page 31: はてなブックマーク in Scala

関係モナド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の返り値はモナドのインスタンス

Page 32: はてなブックマーク in Scala

関係モナド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の返り値はモナドのインスタンス◮ クエリはモナドから結果への暗黙変換で発生

Page 33: はてなブックマーク in Scala

関係モナド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の返り値はモナドのインスタンス◮ クエリはモナドから結果への暗黙変換で発生◮ 関係の引き方はモナド生成時に指定

Page 34: はてなブックマーク in Scala

関係モナドモナドのパラメータ◮ 複数件の引き方のみを実装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

Page 35: はてなブックマーク in Scala

関係モナド元の要素と対応づける場合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による紐づけは変換時にやってくれる

Page 36: はてなブックマーク in Scala

CICICICICICICI

Page 37: はてなブックマーク in Scala

CI

dockerで

◮ 単一の.jarファイルを生成◮ テストを実行◮ 開発用ホスト環境 (予定)

◮ chrootして本番環境に?

生成した.jarファイル

◮ テストに使用 (本番と同一バイナリ)

◮ デプロイに使用

Page 38: はてなブックマーク in Scala

まとめ

◮ はてなブックマークを作りなおし◮ Perlと Scalaのハイブリッド

◮ 薄いフレームワークを採用◮ ScalaでのDDD実践方法を模索◮ dockerでモダンなCI

Page 39: はてなブックマーク in Scala

WE ARE HIRING

◮ Scalaエンジニア 絶賛募集中◮ 東京 / 京都 どちらの勤務でも可