Slide 1

Slide 1 text

How to build a high performance feed in Rails

Slide 2

Slide 2 text

Agenda - Introduction / 自己紹介 - About activity feeds / アクティビティフィードについて - Architecture / アーキテクチャ - Rails implementation / Rails実装

Slide 3

Slide 3 text

Carlos Donderis Sansan エンジニア ● 10年間Rubyist ● Crystal, Python, Javascriptと遊ぶ ● ElixirとGoを学んでいます。 ● 趣味:写真、空手、居合 Introduction / 自己紹介

Slide 4

Slide 4 text

About activity feeds アクティビティフィードについて

Slide 5

Slide 5 text

アクティビティフィードとは? About activity feeds / アクティビティフィードについて

Slide 6

Slide 6 text

アクティビティフィードは何ですか? About activity feeds / アクティビティフィードについて

Slide 7

Slide 7 text

About activity feeds / アクティビティフィードについて フィードはユーザーが興味ありそうな情報を流します。 例えば: - 太郎さんは投稿しました。 - 太郎さんは写真をシェアしました。 - 金太郎さんと健太郎さんは友達になりました。

Slide 8

Slide 8 text

パターンがありませんか? 太郎さんは写真をシェアしました。 太郎さんは投稿しました。 金太郎さんと健太郎さんは友達になりました。 About activity feeds / アクティビティフィードについて

Slide 9

Slide 9 text

パターンがあるんじゃない? 誰が何かにを「アクション」しました。 アクター アクション ターゲット About activity feeds / アクティビティフィードについて

Slide 10

Slide 10 text

Eightのアクティビティフィードの場合、ユーザーのネットワークの - コンタクトの会社のニュース。 - コンタクトの異動や転職。 - コンタクトがプロモーションしたもの。 - コンタクトが投稿・シェアしたもの。 の項目をピックアップをしてアクティビティフィードを 作っています。 About activity feeds / アクティビティフィードについて

Slide 11

Slide 11 text

Architecture アーキテクチャ

Slide 12

Slide 12 text

フィードのアーキテクチャ設計をする前に、 どのようなフィードを作りたいを考えましょう! Architecture / アーキテクチャ

Slide 13

Slide 13 text

フィードの設計をする時には、色々検討しないといけないことがいっぱい - 投稿編集:できる・できない - 投稿の公開範囲:ある・無し - 投稿シェア: できる・できない - 機能: メンション、タグ.... Architecture / アーキテクチャ

Slide 14

Slide 14 text

フィードの設計をする時には、色々検討しないといけないことがいっぱい - 投稿編集:できる・できない - 投稿の公開範囲:ある・無し - 投稿シェア: できる・できない - 機能: メンション、タグ.... Architecture / アーキテクチャ

Slide 15

Slide 15 text

でも後でもいいです。 とりあえず配信だけ考えましょう! Architecture / アーキテクチャ

Slide 16

Slide 16 text

Architecture / アーキテクチャ 配信は何ですか? 英語:Fan-out “In message-oriented middleware solutions, fan-out is a messaging pattern used to model an information exchange that implies the delivery (or spreading) of a message to one or multiple destinations possibly in parallel, and not halting the process that executes the messaging to wait for any response to that message.” Wikipedia: https://en.wikipedia.org/wiki/Fan-out_(software)

Slide 17

Slide 17 text

フィードの場合、配信のやり方は3つ: ● 書き込み時の配信 ● 読み込み時の配信 ● ミックス Architecture / アーキテクチャ

Slide 18

Slide 18 text

書き込み時の配信: メリット - 読み込みは早い - データを非正規化できる場合、実装が簡単 デメリット - DBの書き込み負荷 - 配信は時間がかかります。 (realtimeは厳しい) - 投稿の変更ができる場合、データ更新が大変 - 無駄データが多い(inactiveユーザーなど) Architecture / アーキテクチャ

Slide 19

Slide 19 text

読み込み時の配信: メリット - 実装は簡単(RDB+ActiveRecord) - 投稿更新ができる場合おすすめ - 無駄なデータが少ない - 配信は早い(realtimeに近い) デメリット - DBの読む負荷(joins) - 読み込みは遅くなる Architecture / アーキテクチャ

Slide 20

Slide 20 text

ミックス - 優先度が高いユーザーには書き込み時配信 - 優先度が低いユーザーには読み込み時配信 メリット - 適切なユーザーには早い - 無駄なデータ少ない デメリット - 開発は複雑 - 必要なインフラが増えます Architecture / アーキテクチャ

Slide 21

Slide 21 text

Rails Implementation Rails実装 https://github.com/CaDs/simple_feed https://simple-feed.herokuapp.com

Slide 22

Slide 22 text

読み込み時の配信にしましょ!

Slide 23

Slide 23 text

モデル実装 class User < ApplicationRecord has_many :posts, dependent: :destroy has_many :connections, dependent: :destroy has_many :follower_connections, foreign_key: :following_id, class_name: 'Connection' has_many :followers, through: :follower_connections, source: :follower has_many :following_connections, foreign_key: :follower_id, class_name: 'Connection' has_many :following, through: :following_connections, source: :following end class Post < ApplicationRecord belongs_to :user end class Connection < ApplicationRecord belongs_to :follower, class_name: 'User', foreign_key: :follower_id belongs_to :following, class_name: 'User', foreign_key: :following_id end

Slide 24

Slide 24 text

コントローラー実装 Class PostsController < ApplicationController before_action :auth_user def index @posts = @user.fetch_feed end end class User < ApplicationRecord def fetch_feed Post.eager_load(:user) .where(user: self.following + [self]) .order('posts.created_at DESC') end end

Slide 25

Slide 25 text

クエリー user = User.first # => User Load (0.3ms) SELECT `users`.* FROM `users` ORDER BY `users`.`id` ASC LIMIT 1 user.followers # => SELECT `users`.* FROM `users` INNER JOIN `connections` ON `users`.`id` = `connections`.`follower_id` WHERE `connections`.`following_id` = 4 user.following # => SELECT `users`.* FROM `users` INNER JOIN `connections` ON `users`.`id` = `connections`.`following_id` WHERE `connections`.`follower_id` = 4 user.fetch_feed # => User Load (0.4ms) SELECT `users`.* FROM `users` INNER JOIN `connections` ON `users`.`id` = `connections`.`following_id` WHERE `connections`.`follower_id` = 4 # => SQL (0.7ms) SELECT ..... FROM `posts` LEFT OUTER JOIN `users` ON `users`.`id` = `posts`.`user_id` WHERE `posts`.`user_id` IN (5, 4) ORDER BY posts.created_at DESC LIMIT 11

Slide 26

Slide 26 text

読み込み時配信の敵はこちら: JOIN, INNER JOIN, LEFT OUTER JOIN ... データが増えるJOINは重くなります!

Slide 27

Slide 27 text

読み込み時配信の敵はこちら: JOIN, INNER JOIN, LEFT OUTER JOIN ... データが増えるJOINは重くなります!

Slide 28

Slide 28 text

読み込み時配信の敵はこちら: JOIN, INNER JOIN, LEFT OUTER JOIN ... データが増えるJOINは重くなります! けど 少し改善をしたらしばらく問題がありません。 - DB Index - Pagination - Caching - ...

Slide 29

Slide 29 text

じゃ、ミックスフィードはどうですか?

Slide 30

Slide 30 text

じゃ、ミックスフィードはどうですか?

Slide 31

Slide 31 text

なぜRedisですか? (https://redis.io/) - 早い - 簡単 - データ構造 - Hash - List - Sets - Sorted sets - ...

Slide 32

Slide 32 text

ユーザーのcacheを作りましょ! Redisのsorted set使います (https://redis.io/commands#sorted_set) - Value: post_id - Score: post#created_at

Slide 33

Slide 33 text

モデル実装 class Post < ApplicationRecord belongs_to :user after_create_commit :deliver_to_followers def cache_key_for(id:) "cached_post/#{id}" end def id_from_cache_key(key:) key.split('/').last end def deliver_to_followers (user.followers + [user]).each do |follower| follower.add_to_cache(post: self) end end end

Slide 34

Slide 34 text

モデル実装 class User < ApplicationRecord def fetch_feed cache_keys = redis.zrevrange(cache_key, 0, -1).map { |id| Post.cache_key_for(id: id) } return fetch_read_feed unless cache_keys.any? # Fall back to slow feed if there is no cache Rails.cache.fetch_multi(*cache_keys, expires: 1.minute, race_condition_ttl: 5.seconds) do |key| id = Post.id_from_cache_key(key: key) Post.find_by(id: id) end.values end def fetch_read_feed Post.eager_load(:user) .where(user: self.following + [self]) .order('posts.created_at DESC') end def add_to_cache(post:) redis.zadd(cache_key, post.created_at.to_i, post.id) end end

Slide 35

Slide 35 text

クエリー user = User.first # => User Load (0.3ms) SELECT `users`.* FROM `users` ORDER BY `users`.`id` ASC LIMIT 1 user.fetch_feed # => Post Load (5.9ms) SELECT `posts`.* FROM `posts` WHERE `posts`.`id` = 21 LIMIT 1 # => Post Load (0.7ms) SELECT `posts`.* FROM `posts` WHERE `posts`.`id` = 20 LIMIT 1 # => Post Load (0.6ms) SELECT `posts`.* FROM `posts` WHERE `posts`.`id` = 19 LIMIT 1 # => Post Load (0.3ms) SELECT `posts`.* FROM `posts` WHERE `posts`.`id` = 18 LIMIT 1 # => Post Load (0.3ms) SELECT `posts`.* FROM `posts` WHERE `posts`.`id` = 17 LIMIT 1 # => Post Load (0.5ms) SELECT `posts`.* FROM `posts` WHERE `posts`.`id` = 16 LIMIT 1 # => Post Load (0.4ms) SELECT `posts`.* FROM `posts` WHERE `posts`.`id` = 15 LIMIT 1 # => Post Load (0.3ms) SELECT `posts`.* FROM `posts` WHERE `posts`.`id` = 14 LIMIT 1

Slide 36

Slide 36 text

クエリー user = User.first # => User Load (0.3ms) SELECT `users`.* FROM `users` ORDER BY `users`.`id` ASC LIMIT 1 user.fetch_feed # => Post Load (5.9ms) SELECT `posts`.* FROM `posts` WHERE `posts`.`id` = 21 LIMIT 1 # => Post Load (0.7ms) SELECT `posts`.* FROM `posts` WHERE `posts`.`id` = 20 LIMIT 1 # => Post Load (0.6ms) SELECT `posts`.* FROM `posts` WHERE `posts`.`id` = 19 LIMIT 1 JOINがなくなりました!

Slide 37

Slide 37 text

Summary / まとめ - フィードはなんですか? - 配信のやりかた: - 書き込み時の配信 - 読み込み時の配信 - ミクス - Railsで簡単な実装 - Redis - RailsのCache

Slide 38

Slide 38 text

ありがとうございました