Slide 1

Slide 1 text

Java が支える 人気ニュースアプリ NewsPicks の裏側 2015.04.11 JJUG CCC 2015 SPRING Takuro MONJI @ UZABASE, Inc.

Slide 2

Slide 2 text

UZABASE, Inc. ABOUT ME Takuro MONJI @monzou 2014 年 9 月に UZABASE にジョイン 前職は金融系トレーディングシステム開発

Slide 3

Slide 3 text

NewsPicks, Inc. ABOUT ME Takuro MONJI @monzou 2014 年 9 月に UZABASE にジョイン 前職は金融系トレーディングシステム開発

Slide 4

Slide 4 text

昨年も講演させて頂きました https://speakerdeck.com/monzou/spa-development

Slide 5

Slide 5 text

AGENDA 1. UZABASE / NewsPicks について 2. NewsPicks の裏側 3. 急成長を支える技術と舞台裏

Slide 6

Slide 6 text

AGENDA 1. UZABASE / NewsPicks について 2. NewsPicks の裏側 3. 急成長を支える技術と舞台裏

Slide 7

Slide 7 text

ABOUT 2008 年 4 月 創業 従業員数 160 名(アルバイト含む) (NewsPicks は 40 名弱) 事業拠点 ・東京 ・シンガポール ・上海 ・香港 ミッション 「世界一の経済メディアをつくる」

Slide 8

Slide 8 text

世界一の経済メディア SPEEDA 高度な分析を行うためのインフラ 経済情報との接点 NewsPicks 新たなビジネス情報を発見するためのインフラ プロフェッショナル向け 世界展開中 一般ビジネスパーソン向け まずは国内 No.1 を目指す

Slide 9

Slide 9 text

No content

Slide 10

Slide 10 text

= ビジネスパーソン向け 経済特化型ニュースアプリ

Slide 11

Slide 11 text

= ビジネスパーソン向け 経済特化型ニュースアプリ 様々な経済情報をワンストップで提供 著名人・専門家によるキュレーション / コメント ビジネスパーソン向けオリジナルコンテンツ

Slide 12

Slide 12 text

=

Slide 13

Slide 13 text

=

Slide 14

Slide 14 text

No content

Slide 15

Slide 15 text

= PLATFORM + PUBLISHER

Slide 16

Slide 16 text

PLATFORM PUBLISHER キュレーション コンテンツ 多様な 経済情報 ビジネス パーソン向け 独自記事 ブランド 広告記事 PICKER キュレーション・コメント ... ... ... 厳選提携メディア 広告主 外部サイト 編集部

Slide 17

Slide 17 text

他ニュースアプリと比較した場合の特徴 機能面 経済特化 コメント 有料課金 独自記事 記事編成 ユーザー特性 意思決定者層が多い 著名人/専門家が多い 滞在時間が非常に長い 熱心なファンが多い 拡散力が強い

Slide 18

Slide 18 text

サービスの成長 ユーザー数 有料課金数 ユーザー数も 35 万人を超え、 有料課金ユーザー数も順調に増加中 (エンジニアも最近ようやく増えてきました ……)

Slide 19

Slide 19 text

チーム体制 プラットフォーム コンテンツ x テクノロジーの融合 EDITOR x ENGINEER ENGINEER x DESIGNER DESIGNER x EDITOR ... ブランド デザイン 編集部

Slide 20

Slide 20 text

チーム体制 プラットフォーム 各チームが緊密に連携しながら開発中 プラットフォーム(エンジニア)は 現在 10 名程度 ブランド デザイン 編集部 記事編成 / リコメンド / プッシュ通知 など

Slide 21

Slide 21 text

チーム体制 プラットフォーム 各チームが緊密に連携しながら開発中 プラットフォーム(エンジニア)は 現在 10 名程度 ブランド デザイン 編集部 記事編成 / リコメンド / プッシュ通知 など 編集長です

Slide 22

Slide 22 text

チーム体制 プラットフォーム 各チームが緊密に連携しながら開発中 プラットフォーム(エンジニア)は 現在 10 名程度 ブランド デザイン 編集部 記事編成 / リコメンド / プッシュ通知 など 編集長です 社長です

Slide 23

Slide 23 text

チーム体制 プラットフォーム 各チームが緊密に連携しながら開発中 プラットフォーム(エンジニア)は 現在 10 名程度 ブランド デザイン 編集部 記事編成 / リコメンド / プッシュ通知 など 編集長です 社長です 編集部員です

Slide 24

Slide 24 text

チーム体制 プラットフォーム 各チームが緊密に連携しながら開発中 プラットフォーム(エンジニア)は 現在 10 名程度 ブランド デザイン 編集部 記事編成 / リコメンド / プッシュ通知 など 編集長です 社長です 編集部員です 職人の手による 温かみのあるプッシュ通知です

Slide 25

Slide 25 text

チーム体制 プラットフォーム 各チームが緊密に連携しながら開発中 プラットフォーム(エンジニア)は 現在 10 名程度 ブランド デザイン 編集部 NewsPicks Paper

Slide 26

Slide 26 text

チーム体制 プラットフォーム ブランド デザイン 編集部 プロジェクト チーム グロース チーム

Slide 27

Slide 27 text

チーム体制 プラットフォーム ブランド デザイン 編集部 プロジェクト チーム グロース チーム

Slide 28

Slide 28 text

コンテンツとテクノロジーの融合で 世界一の経済メディアを目指す

Slide 29

Slide 29 text

AGENDA 1. UZABASE / NewsPicks について 2. NewsPicks の裏側 3. 急成長を支える技術と舞台裏

Slide 30

Slide 30 text

Dynamo DB ElastiCache INTERNET DESKTOP PC MOBILE ROUTE 53 ELB EIP INCOMING APP BAT Elasticsearch RDS Redshift CloudWatch SNS SES SQS

Slide 31

Slide 31 text

Dynamo DB ElastiCache INTERNET DESKTOP PC MOBILE ROUTE 53 ELB EIP INCOMING APP BAT Elasticsearch RDS Redshift CloudWatch SNS SES SQS

Slide 32

Slide 32 text

Dynamo DB ElastiCache INTERNET DESKTOP PC MOBILE ROUTE 53 ELB EIP INCOMING APP BAT Elasticsearch RDS Redshift CloudWatch SNS SES SQS AWS OR DIE

Slide 33

Slide 33 text

Dynamo DB ElastiCache INTERNET DESKTOP PC MOBILE ROUTE 53 ELB EIP INCOMING APP BAT Elasticsearch RDS Redshift CloudWatch SNS SES SQS DATABASE

Slide 34

Slide 34 text

Dynamo DB ElastiCache INTERNET DESKTOP PC MOBILE ROUTE 53 ELB EIP INCOMING APP BAT Elasticsearch RDS Redshift CloudWatch SNS SES SQS DATABASE RDS : Master Data Dynamo DB : Transaction Data ElastiCache : Timeline / Ranking / Cache etc Redshift : Access Log / Analytics

Slide 35

Slide 35 text

Dynamo DB ElastiCache INTERNET DESKTOP PC MOBILE ROUTE 53 ELB EIP INCOMING APP BAT Elasticsearch RDS Redshift CloudWatch SNS SES SQS

Slide 36

Slide 36 text

Dynamo DB ElastiCache INTERNET DESKTOP PC MOBILE ROUTE 53 ELB EIP INCOMING APP BAT Elasticsearch RDS Redshift CloudWatch SNS SES SQS Java OR DIE

Slide 37

Slide 37 text

Dynamo DB ElastiCache INTERNET DESKTOP PC MOBILE ROUTE 53 ELB EIP INCOMING APP BAT Elasticsearch RDS Redshift CloudWatch SNS SES SQS WHY Java ? SPEEDA SIMPLE Java 8 LOMBOK

Slide 38

Slide 38 text

Dynamo DB ElastiCache INTERNET DESKTOP PC MOBILE ROUTE 53 ELB EIP INCOMING APP BAT Elasticsearch RDS Redshift CloudWatch SNS SES SQS WHY Java ? DYNAMIC TYPING TOO SLOW COMPILE

Slide 39

Slide 39 text

INTERNET DESKTOP PC MOBILE ROUTE 53 ELB EIP INCOMING APP BAT Elasticsearch Java Applications CMS RECOMMEND ANALYTICS MANAGE DASHBOARD SUPPORT

Slide 40

Slide 40 text

newspicks-categorizer newspicks-model newspicks-web-core newspicks-web newspicks-api newspicks-contents newspicks-support newspicks-cms newspicks-core newspicks-worker APPLICATION LAYER WEB LAYER DOMAIN LAYER INFRASTRUCTURE newspicks-service newspicks-recommender

Slide 41

Slide 41 text

Dynamo DB ElastiCache INTERNET DESKTOP PC MOBILE ROUTE 53 ELB EIP INCOMING APP BAT Elasticsearch RDS Redshift CloudWatch SNS SES SQS APP SERVER

Slide 42

Slide 42 text

ELB SQS Elasticsearch RDS (MySQL) ElastiCache (Redis) Dynamo DB DATABASE READ / WRITE バックエンドに 非同期タスクを登録 全文検索 重複判定 Redshift api.newspicks.com contents.newspicks.com newspicks.com Java 8 Spring MVC

Slide 43

Slide 43 text

ELB SQS Elasticsearch RDS (MySQL) ElastiCache (Redis) Dynamo DB DATABASE READ / WRITE バックエンドに 非同期タスクを登録 全文検索 重複判定 Redshift api.newspicks.com contents.newspicks.com newspicks.com REST API Java 8 Spring MVC

Slide 44

Slide 44 text

ELB SQS Elasticsearch RDS (MySQL) ElastiCache (Redis) Dynamo DB DATABASE READ / WRITE バックエンドに 非同期タスクを登録 全文検索 重複判定 Redshift api.newspicks.com contents.newspicks.com newspicks.com Java 8 + LOMBOK + SPRING Java 8 Spring MVC

Slide 45

Slide 45 text

Dynamo DB ElastiCache INTERNET DESKTOP PC MOBILE ROUTE 53 ELB EIP INCOMING APP BAT Elasticsearch RDS Redshift CloudWatch SNS SES SQS Java @Logging @Controller @RequestMapping("/news") public interface NewsController { @RequestMapping(value="/{id}/picks", method=GET, produces=ContentType.JSON) PageableCollection getPicks( @PathVariable Integer id, @ModelAttribute PickSearchParams params ); } Model と JSON のマッピングがツラい DTO を書くのが面倒くさい

Slide 46

Slide 46 text

Dynamo DB ElastiCache INTERNET DESKTOP PC MOBILE ROUTE 53 ELB EIP INCOMING APP BAT Elasticsearch RDS Redshift CloudWatch SNS SES SQS LOMBOK public class NewsControllerImpl implements NewsController { private final NewsFacade facade; @Inject public NewsControllerImpl(NewsFacade facade) { this.facade = facade; } @Override public PageableCollection getPicks(Integer id, PickSearchParams params) { return facade.getPicks(id, params.getSorting(), params.getPage()) .map(pick -> PickViewModel.build(pick)); } }

Slide 47

Slide 47 text

Dynamo DB ElastiCache INTERNET DESKTOP PC MOBILE ROUTE 53 ELB EIP INCOMING APP BAT Elasticsearch RDS Redshift CloudWatch SNS SES SQS LOMBOK @Data public class PickSearchParams { private String sortBy; private String order; private Integer offset; private Integer limit; public Sorting getSorting() { return Sorting.from(sortBy); } public Page getPage() { return Page.builder() .order(Order.from(order)).offset(offset).limit(limit).build(); } } @Builder @Value public class Page { private final Order order; private final Integer offset; private final Integer limit; }

Slide 48

Slide 48 text

Dynamo DB ElastiCache INTERNET DESKTOP PC MOBILE ROUTE 53 ELB EIP INCOMING APP BAT Elasticsearch RDS Redshift CloudWatch SNS SES SQS LOMBOK @Builder @Value public class PickViewModel { public static PickViewModel build(Pick pick) { UserViewModel user = UserViewModel.builder().delegate(pick.getUser()).build(); return PickViewModel.builder().delegate(pick).user(user).build(); } @JsonIgnore @Delegate(types=Picks.class, exclusions=Exclusions.class) private final Pick delegate; private final UserViewModel user; private static interface Exclusions { User getUser(); // + other properties ... } }

Slide 49

Slide 49 text

Dynamo DB ElastiCache INTERNET DESKTOP PC MOBILE ROUTE 53 ELB EIP INCOMING APP BAT Elasticsearch RDS Redshift CloudWatch SNS SES SQS SPRING ぶっちゃけしんどい TOO LARGE TOO COMPLICATED XML HELL SPRING SECURITY プロダクションには JAX-RS + Guice / Dagger がオススメ

Slide 50

Slide 50 text

ELB SQS Elasticsearch RDS (MySQL) ElastiCache (Redis) Dynamo DB DATABASE READ / WRITE バックエンドに 非同期タスクを登録 全文検索 重複判定 Redshift api.newspicks.com contents.newspicks.com newspicks.com Java 8 SQS (Simple Queue Service) 可用性が担保された分散キュー コンシューマはႈ等に実装する必要がある → スループット向上 / バックエンドの分離

Slide 51

Slide 51 text

Dynamo DB ElastiCache INTERNET DESKTOP PC MOBILE ROUTE 53 ELB EIP INCOMING APP BAT Elasticsearch RDS Redshift CloudWatch SNS SES SQS BACKEND BAT SERVER RECOMMEND SERVER etc ..

Slide 52

Slide 52 text

SQS Elasticsearch RDS (MySQL) ElastiCache (Redis) Dynamo DB CRON Java 提携サプライヤ/ 独自記事取込 Java ランキング 計算 Java コメント スコア計算 Java 類似記事計算 カテゴリ分類 Java タイムラインの 生成/伝播 Java 検索 インデックス更新 DATABASE READ / WRITE SCHEDULE UPDATE SQS SUBSCRIBE BACKEND SERVICES 他にも様々な サービスがあります Redshift ENQUEUE

Slide 53

Slide 53 text

SQS Elasticsearch RDS (MySQL) ElastiCache (Redis) Dynamo DB CRON Java 提携サプライヤ/ 独自記事取込 Java ランキング 計算 Java コメント スコア計算 Java 類似記事計算 カテゴリ分類 Java タイムラインの 生成/伝播 Java 検索 インデックス更新 DATABASE READ / WRITE SCHEDULE UPDATE SQS SUBSCRIBE BACKEND SERVICES 他にも様々な サービスがあります Redshift ENQUEUE

Slide 54

Slide 54 text

Article Java ユーザー毎の タイムライン 取り込んだ記事がタイムラインに反映されるまで Java Java Categorize Queue Propagate Queue Feed Service Categorize Service Propagate Service Vowpal Wabbit Sckit Learn etc. Py Dynamo DB ElastiCache (Redis) 機械学習 エンジン

Slide 55

Slide 55 text

Article Java ユーザー毎の タイムライン 取り込んだ記事がタイムラインに反映されるまで Java Java Categorize Queue Propagate Queue Feed Service Categorize Service Propagate Service Vowpal Wabbit Sckit Learn etc. Py Dynamo DB ElastiCache (Redis) 1. Update Feed 機械学習 エンジン

Slide 56

Slide 56 text

Article Java ユーザー毎の タイムライン 取り込んだ記事がタイムラインに反映されるまで Java Java Categorize Queue Propagate Queue Feed Service Categorize Service Propagate Service Vowpal Wabbit Sckit Learn etc. Py Dynamo DB ElastiCache (Redis) 1. Update Feed 2. Poll 機械学習 エンジン

Slide 57

Slide 57 text

Article Java ユーザー毎の タイムライン 取り込んだ記事がタイムラインに反映されるまで Java Java Categorize Queue Propagate Queue Feed Service Categorize Service Propagate Service Vowpal Wabbit Sckit Learn etc. Py Dynamo DB ElastiCache (Redis) 1. Update Feed 2. Poll 3. Save 機械学習 エンジン

Slide 58

Slide 58 text

Article Java ユーザー毎の タイムライン 取り込んだ記事がタイムラインに反映されるまで Java Java Categorize Queue Propagate Queue Feed Service Categorize Service Propagate Service Vowpal Wabbit Sckit Learn etc. Py Dynamo DB ElastiCache (Redis) 1. Update Feed 2. Poll 3. Save 4. Enqueue 機械学習 エンジン

Slide 59

Slide 59 text

Article Java ユーザー毎の タイムライン 取り込んだ記事がタイムラインに反映されるまで Java Java Categorize Queue Propagate Queue Feed Service Categorize Service Propagate Service Vowpal Wabbit Sckit Learn etc. Py Dynamo DB ElastiCache (Redis) 1. Update Feed 2. Poll 3. Save 4. Enqueue 5. Subscribe 機械学習 エンジン

Slide 60

Slide 60 text

Article Java ユーザー毎の タイムライン 取り込んだ記事がタイムラインに反映されるまで Java Java Categorize Queue Propagate Queue Feed Service Categorize Service Propagate Service Vowpal Wabbit Sckit Learn etc. Py Dynamo DB ElastiCache (Redis) 1. Update Feed 2. Poll 3. Save 4. Enqueue 5. Subscribe 6. Calculate Score 機械学習 エンジン

Slide 61

Slide 61 text

Article Java ユーザー毎の タイムライン 取り込んだ記事がタイムラインに反映されるまで Java Java Categorize Queue Propagate Queue Feed Service Categorize Service Propagate Service Vowpal Wabbit Sckit Learn etc. Py Dynamo DB ElastiCache (Redis) 7. Fetch Data 1. Update Feed 2. Poll 3. Save 4. Enqueue 5. Subscribe 6. Calculate Score 機械学習 エンジン

Slide 62

Slide 62 text

Article Java ユーザー毎の タイムライン 取り込んだ記事がタイムラインに反映されるまで Java Java Categorize Queue Propagate Queue Feed Service Categorize Service Propagate Service Vowpal Wabbit Sckit Learn etc. Py Dynamo DB ElastiCache (Redis) 7. Fetch Data 1. Update Feed 2. Poll 3. Save 4. Enqueue 5. Subscribe 6. Calculate Score 8. Return Score 機械学習 エンジン

Slide 63

Slide 63 text

Article Java ユーザー毎の タイムライン 取り込んだ記事がタイムラインに反映されるまで Java Java Categorize Queue Propagate Queue Feed Service Categorize Service Propagate Service Vowpal Wabbit Sckit Learn etc. Py Dynamo DB ElastiCache (Redis) 7. Fetch Data 1. Update Feed 2. Poll 3. Save 4. Enqueue 5. Subscribe 6. Calculate Score 8. Return Score 9. Save 機械学習 エンジン

Slide 64

Slide 64 text

Article Java ユーザー毎の タイムライン 取り込んだ記事がタイムラインに反映されるまで Java Java Categorize Queue Propagate Queue Feed Service Categorize Service Propagate Service Vowpal Wabbit Sckit Learn etc. Py Dynamo DB ElastiCache (Redis) 7. Fetch Data 1. Update Feed 2. Poll 3. Save 4. Enqueue 5. Subscribe 6. Calculate Score 8. Return Score 9. Save 10. Enqueue 機械学習 エンジン

Slide 65

Slide 65 text

Article Java ユーザー毎の タイムライン 取り込んだ記事がタイムラインに反映されるまで Java Java Categorize Queue Propagate Queue Feed Service Categorize Service Propagate Service Vowpal Wabbit Sckit Learn etc. Py Dynamo DB ElastiCache (Redis) 7. Fetch Data 1. Update Feed 2. Poll 3. Save 4. Enqueue 5. Subscribe 6. Calculate Score 8. Return Score 9. Save 10. Enqueue 11. Subscribe 機械学習 エンジン

Slide 66

Slide 66 text

Article Java ユーザー毎の タイムライン 取り込んだ記事がタイムラインに反映されるまで Java Java Categorize Queue Propagate Queue Feed Service Categorize Service Propagate Service Vowpal Wabbit Sckit Learn etc. Py Dynamo DB ElastiCache (Redis) 7. Fetch Data 1. Update Feed 2. Poll 3. Save 4. Enqueue 5. Subscribe 6. Calculate Score 8. Return Score 9. Save 10. Enqueue 11. Subscribe 12. Update 機械学習 エンジン

Slide 67

Slide 67 text

AWS 柔軟なスケール調整とインフラ運用コストの低減 Java Java 8 + LOMBOK で堅く軽く開発

Slide 68

Slide 68 text

AGENDA 1. UZABASE / NewsPicks について 2. NewsPicks の裏側 3. 急成長を支える技術と舞台裏

Slide 69

Slide 69 text

そろそろ疲れてきたと思うので 軽めのテイストにします

Slide 70

Slide 70 text

Redis 編

Slide 71

Slide 71 text

入社 1 ヶ月が経ったころ ...

Slide 72

Slide 72 text

入社 1 ヶ月が経ったころ ... (” ՞ਊ ՞)” 「Redis が危ないです」 (” ՞ਊ ՞)” 「最近はバックアップに失敗することも多いし …」 (” ՞ਊ ՞)” 「深夜アラートも多発しています」

Slide 73

Slide 73 text

入社 1 ヶ月が経ったころ ... (” ՞ਊ ՞)” 「Redis が危ないです」 (” ՞ਊ ՞)” 「最近はバックアップに失敗することも多いし …」 (” ՞ਊ ՞)” 「深夜アラートも多発しています」 ٩( 'ω' )و 「えっ ……」

Slide 74

Slide 74 text

入社 1 ヶ月が経ったころ ... (” ՞ਊ ՞)” 「Redis が危ないです」 (” ՞ਊ ՞)” 「最近はバックアップに失敗することも多いし …」 (” ՞ਊ ՞)” 「深夜アラートも多発しています」 ٩( 'ω' )و 「えっ ……」 (” ՞ਊ ՞)” 「このままではタイムラインが動かなくなってしまいます」 (” ՞ਊ ՞)” 「そろそろ Redis をスケールアウトしましょう」

Slide 75

Slide 75 text

入社 1 ヶ月が経ったころ ... (” ՞ਊ ՞)” 「Redis が危ないです」 (” ՞ਊ ՞)” 「最近はバックアップに失敗することも多いし …」 (” ՞ਊ ՞)” 「深夜アラートも多発しています」 ٩( 'ω' )و 「えっ ……」 (” ՞ਊ ՞)” 「このままではタイムラインが動かなくなってしまいます」 (” ՞ਊ ՞)” 「そろそろ Redis をスケールアウトしましょう」 ٩( 'ω' )و (これがスタートアップの現実か ……)

Slide 76

Slide 76 text

当時の Redis EC2 上に立てた Redis のインスタンスが 2 台 ・タイムライン用 ・ランキング / キャッシュ用 タイムライン用 Redis にアクセスが集中し遅延が発生 ・ピークタイムにリクエストが集中し遅延 ・夜間バッチ処理が遅延してレプリケーションが切れる イベントループモデル ・Redis の性能を超えた要求

Slide 77

Slide 77 text

当時の Redis EC2 上に立てた Redis のインスタンスが 2 台 ・タイムライン用 ・ランキング / キャッシュ用 タイムライン用 Redis にアクセスが集中し遅延が発生 ・ピークタイムにリクエストが集中し遅延 ・夜間バッチ処理が遅延してレプリケーションが切れる イベントループモデル ・Redis の性能を超えた要求 ElastiCache に移行

Slide 78

Slide 78 text

当時の Redis EC2 上に立てた Redis のインスタンスが 2 台 ・タイムライン用 ・ランキング / キャッシュ用 タイムライン用 Redis にアクセスが集中し遅延が発生 ・ピークタイムにリクエストが集中し遅延 ・夜間バッチ処理が遅延してレプリケーションが切れる イベントループモデル ・Redis の性能を超えた要求 ElastiCache に移行 ユーザー パーティショニング (垂直分散)

Slide 79

Slide 79 text

当時の Redis EC2 上に立てた Redis のインスタンスが 2 台 ・タイムライン用 ・ランキング / キャッシュ用 タイムライン用 Redis にアクセスが集中し遅延が発生 ・ピークタイムにリクエストが集中し遅延 ・夜間バッチ処理が遅延してレプリケーションが切れる イベントループモデル ・Redis の性能を超えた要求 ElastiCache に移行 ユーザー パーティショニング (垂直分散) BAT サーバーも スケールアウト

Slide 80

Slide 80 text

BEFORE ELB APP BAT Redis (slave) Redis (master) replication

Slide 81

Slide 81 text

BEFORE ELB APP BAT Redis (slave) Redis (master) 1台で 全ユーザーの タイムラインを更新 replication

Slide 82

Slide 82 text

BEFORE ELB APP BAT Redis (slave) Redis (master) 1台で 全ユーザーの タイムラインを更新 replication 自前で レプリケーション & バックアップ

Slide 83

Slide 83 text

BEFORE ELB APP BAT Redis (slave) Redis (master) 1台で 全ユーザーの タイムラインを更新 replication 自前で レプリケーション & バックアップ BAT サーバーは 1 台で処理

Slide 84

Slide 84 text

BEFORE ELB APP BAT Redis (slave) Redis (master) 1台で 全ユーザーの タイムラインを更新 replication 自前で レプリケーション & バックアップ AFTER ELB APP BAT slave master replication ElastiCache (Redis) slave master replication ElastiCache (Redis) slave master replication ElastiCache (Redis) USER ID 1-10 万 USER ID 10 -20 万 USER ID 20 -30 万 BAT サーバーは 1 台で処理

Slide 85

Slide 85 text

BEFORE ELB APP BAT Redis (slave) Redis (master) 1台で 全ユーザーの タイムラインを更新 replication 自前で レプリケーション & バックアップ AFTER ELB APP BAT slave master replication ElastiCache (Redis) slave master replication ElastiCache (Redis) slave master replication ElastiCache (Redis) USER ID 1-10 万 USER ID 10 -20 万 USER ID 20 -30 万 BAT サーバーは 1 台で処理 ElastiCache に移行 ユーザー ID で パーティショニング

Slide 86

Slide 86 text

BEFORE ELB APP BAT Redis (slave) Redis (master) 1台で 全ユーザーの タイムラインを更新 replication 自前で レプリケーション & バックアップ Redis 分散にあわせて BAT サーバーも 複数台に増やして 高速化を図る AFTER ELB APP BAT slave master replication ElastiCache (Redis) slave master replication ElastiCache (Redis) slave master replication ElastiCache (Redis) USER ID 1-10 万 USER ID 10 -20 万 USER ID 20 -30 万 BAT サーバーは 1 台で処理 ElastiCache に移行 ユーザー ID で パーティショニング

Slide 87

Slide 87 text

BEFORE ELB APP BAT Redis (slave) Redis (master) 1台で 全ユーザーの タイムラインを更新 replication 自前で レプリケーション & バックアップ Redis 分散にあわせて BAT サーバーも 複数台に増やして 高速化を図る AFTER ELB APP BAT slave master replication ElastiCache (Redis) slave master replication ElastiCache (Redis) slave master replication ElastiCache (Redis) USER ID 1-10 万 USER ID 10 -20 万 USER ID 20 -30 万 BAT サーバーは 1 台で処理 ElastiCache に移行 ユーザー ID で パーティショニング public class TimelineRepositoryImpl implements TimelineRepository { private final TimelineRedisTemplateSelector timelineRedisSelector; @Override public void addPickToTimeline(Pick pick) { redis(pick.getUserId()).execute(new AddPickTxCallback(pick) { ... }); } private TimelineRedisTemplate redis(Integer userId) { return timelineRedisSelector.select(userId); } }

Slide 88

Slide 88 text

垂直分散後

Slide 89

Slide 89 text

垂直分散後 (” ՞ਊ ՞)” 「これで一安心ですね」 (” ՞ਊ ՞)” 「しばらくはゆっくりとした夜が過ごせるでしょう」

Slide 90

Slide 90 text

垂直分散後 (” ՞ਊ ՞)” 「これで一安心ですね」 (” ՞ਊ ՞)” 「しばらくはゆっくりとした夜が過ごせるでしょう」 一週間後

Slide 91

Slide 91 text

垂直分散後 (” ՞ਊ ՞)” 「これで一安心ですね」 (” ՞ਊ ՞)” 「しばらくはゆっくりとした夜が過ごせるでしょう」 一週間後 (” ՞ਊ ՞)” 「垂直分散では負荷が思ったほど下がりませんね」 (” ՞ਊ ՞)” 「人気ユーザーが一部のパーティションに集中しているようです」 (” ՞ਊ ՞)” 「やはりハッシュで分散しましょう」 (” ՞ਊ ՞)” 「リバランスは運用でカバーっしょ!」

Slide 92

Slide 92 text

垂直分散後 (” ՞ਊ ՞)” 「これで一安心ですね」 (” ՞ਊ ՞)” 「しばらくはゆっくりとした夜が過ごせるでしょう」 ٩( 'ω' )و (思った通りの結果になったな ……) 一週間後 (” ՞ਊ ՞)” 「垂直分散では負荷が思ったほど下がりませんね」 (” ՞ਊ ՞)” 「人気ユーザーが一部のパーティションに集中しているようです」 (” ՞ਊ ՞)” 「やはりハッシュで分散しましょう」 (” ՞ਊ ՞)” 「リバランスは運用でカバーっしょ!」

Slide 93

Slide 93 text

BEFORE ELB APP BAT Redis (slave) Redis (master) 1台で 全ユーザーの タイムラインを更新 replication 自前で レプリケーション & バックアップ Redis 分散にあわせて BAT サーバーも 複数台に増やして 高速化を図る AFTER ELB APP BAT slave master replication ElastiCache (Redis) slave master replication ElastiCache (Redis) slave master replication ElastiCache (Redis) USER ID 1-10 万 USER ID 10 -20 万 USER ID 20 -30 万 BAT サーバーは 1 台で処理 ElastiCache に移行 ユーザー ID で パーティショニング public class TimelineRepositoryImpl implements TimelineRepository { private final TimelineRedisTemplateSelector timelineRedisSelector; @Override public void addPickToTimeline(Pick pick) { redis(pick.getUserId()).execute(new AddPickTxCallback(pick) { ... }); } private TimelineRedisTemplate redis(Integer userId) { return timelineRedisSelector.select(userId); } }

Slide 94

Slide 94 text

ハッシュ分散後

Slide 95

Slide 95 text

ハッシュ分散後 (” ՞ਊ ՞)” 「これで一安心ですね」 (” ՞ਊ ՞)” 「しばらくはゆっくりとした夜が過ごせるでしょう」

Slide 96

Slide 96 text

ハッシュ分散後 (” ՞ਊ ՞)” 「これで一安心ですね」 (” ՞ਊ ՞)” 「しばらくはゆっくりとした夜が過ごせるでしょう」 二週間後

Slide 97

Slide 97 text

ハッシュ分散後 (” ՞ਊ ՞)” 「これで一安心ですね」 (” ՞ਊ ՞)” 「しばらくはゆっくりとした夜が過ごせるでしょう」 二週間後 (” ՞ਊ ՞)” 「コメントの表示が遅いですね」 (” ՞ਊ ՞)” 「どうやらキャッシュ用の Redis がやられたようです」 (” ՞ਊ ՞)” 「奴は四天王でも最弱 ……」 (” ՞ਊ ՞)” 「30 万ユーザー程度に負けるとは KVS の面汚しよ」

Slide 98

Slide 98 text

ハッシュ分散後 (” ՞ਊ ՞)” 「これで一安心ですね」 (” ՞ਊ ՞)” 「しばらくはゆっくりとした夜が過ごせるでしょう」 二週間後 (” ՞ਊ ՞)” 「コメントの表示が遅いですね」 (” ՞ਊ ՞)” 「どうやらキャッシュ用の Redis がやられたようです」 (” ՞ਊ ՞)” 「奴は四天王でも最弱 ……」 (” ՞ਊ ՞)” 「30 万ユーザー程度に負けるとは KVS の面汚しよ」 ٩( 'ω' )و (なに言ってるんだこの人 ……) ٩( 'ω' )و 「READ / WRITE 分散ですね分かります」

Slide 99

Slide 99 text

BEFORE ELB APP BAT Redis (slave) Redis (master) replication 自前で レプリケーション & バックアップ Read / Write Read / Write

Slide 100

Slide 100 text

BEFORE ELB APP BAT Redis (slave) Redis (master) 1台で全ての 書き込み/読み込みを 処理 replication 自前で レプリケーション & バックアップ Read / Write Read / Write

Slide 101

Slide 101 text

BEFORE ELB APP BAT Redis (slave) Redis (master) 1台で全ての 書き込み/読み込みを 処理 replication 自前で レプリケーション & バックアップ Read / Write Read / Write AFTER ELB APP BAT slave master ElastiCache (Redis) Write slave slave replication replication Read Read

Slide 102

Slide 102 text

BEFORE ELB APP BAT Redis (slave) Redis (master) 1台で全ての 書き込み/読み込みを 処理 replication 自前で レプリケーション & バックアップ Read / Write Read / Write AFTER ELB APP BAT slave master ElastiCache (Redis) Write slave slave replication replication Read Read 書き込みは マスターに

Slide 103

Slide 103 text

BEFORE ELB APP BAT Redis (slave) Redis (master) 1台で全ての 書き込み/読み込みを 処理 replication 自前で レプリケーション & バックアップ Read / Write Read / Write AFTER ELB APP BAT slave master ElastiCache (Redis) Write slave slave replication replication Read Read 読み込みは リードレプリカから (スティッキー) 書き込みは マスターに

Slide 104

Slide 104 text

BEFORE ELB APP BAT Redis (slave) Redis (master) 1台で全ての 書き込み/読み込みを 処理 replication 自前で レプリケーション & バックアップ Read / Write Read / Write AFTER ELB APP BAT slave master ElastiCache (Redis) Write slave slave replication replication Read Read 読み込みは リードレプリカから (スティッキー) 書き込みは マスターに リードレプリカが 落ちた場合は フェイルオーバー

Slide 105

Slide 105 text

BEFORE ELB APP BAT Redis (slave) Redis (master) 1台で全ての 書き込み/読み込みを 処理 replication 自前で レプリケーション & バックアップ Read / Write Read / Write AFTER ELB APP BAT slave master ElastiCache (Redis) Write slave slave replication replication Read Read 読み込みは リードレプリカから (スティッキー) 書き込みは マスターに リードレプリカが 落ちた場合は フェイルオーバー public class PickRepositoryImpl implements PickRepository { private final ReadWriteRedisTemplateSelector redisSelector; @Override public boolean isLiked(Pick pick, Integer user) { return new LikeOps(redisSelector.read(), user).isLiked(pick); } @Override public boolean like(Pick pick, Integer user) { return new LikeOps(redisSelector.write(), user).like(pick); } }

Slide 106

Slide 106 text

SEO 編

Slide 107

Slide 107 text

入社 2 ヶ月が経ったころ ...

Slide 108

Slide 108 text

入社 2 ヶ月が経ったころ ... (” ՞ਊ ՞)” 「SEO がヤバいです」 (” ՞ਊ ՞)” 「NewsPicks は Google にインデックスされていません」 (” ՞ਊ ՞)” 「新時代の経済メディアとしては由々しき事態です」

Slide 109

Slide 109 text

入社 2 ヶ月が経ったころ ... (” ՞ਊ ՞)” 「SEO がヤバいです」 (” ՞ਊ ՞)” 「NewsPicks は Google にインデックスされていません」 (” ՞ਊ ՞)” 「新時代の経済メディアとしては由々しき事態です」 ٩( 'ω' )و 「えっ ……」

Slide 110

Slide 110 text

入社 2 ヶ月が経ったころ ... (” ՞ਊ ՞)” 「SEO がヤバいです」 (” ՞ਊ ՞)” 「NewsPicks は Google にインデックスされていません」 (” ՞ਊ ՞)” 「新時代の経済メディアとしては由々しき事態です」 ٩( 'ω' )و 「えっ ……」 (” ՞ਊ ՞)” 「編集部が頑張っても検索から流入しないのは困りものです」 (” ՞ਊ ՞)” 「そろそろSEO 対策をするときが来たようです」

Slide 111

Slide 111 text

入社 2 ヶ月が経ったころ ... (” ՞ਊ ՞)” 「SEO がヤバいです」 (” ՞ਊ ՞)” 「NewsPicks は Google にインデックスされていません」 (” ՞ਊ ՞)” 「新時代の経済メディアとしては由々しき事態です」 ٩( 'ω' )و 「えっ ……」 (” ՞ਊ ՞)” 「編集部が頑張っても検索から流入しないのは困りものです」 (” ՞ਊ ՞)” 「そろそろSEO 対策をするときが来たようです」 ٩( 'ω' )و (スタートアップは大変だな ……) ٩( 'ω' )و (Phantom でちょちょいと描画すれば良かろう ……)

Slide 112

Slide 112 text

当時の SEO 状況 夏に Web 版をリリース ・Angular で開発された SPA ・時間とリソースの関係上 SEO 対策まったく無し 秋に編集部が発足 ・独自記事が徐々に拡散されるように ・ソーシャルからの流入は増えてきたが検索から全く流入しない

Slide 113

Slide 113 text

翌日

Slide 114

Slide 114 text

翌日 ٩( 'ω' )و 「真面目に作る時間も勿体無いので ……」 ٩( 'ω' )و 「Phantom でスナップショットを返すようにしました」 ٩( 'ω' )و 「ぽちっとリリースしたけど Phantom の暴走が心配です」

Slide 115

Slide 115 text

翌日 (” ՞ਊ ՞)” 「しばらく様子を見てみましょう」 (” ՞ਊ ՞)” 「念のため node を自動再起動するようにしておきましょう」 ٩( 'ω' )و 「真面目に作る時間も勿体無いので ……」 ٩( 'ω' )و 「Phantom でスナップショットを返すようにしました」 ٩( 'ω' )و 「ぽちっとリリースしたけど Phantom の暴走が心配です」

Slide 116

Slide 116 text

翌日 (” ՞ਊ ՞)” 「しばらく様子を見てみましょう」 (” ՞ਊ ՞)” 「念のため node を自動再起動するようにしておきましょう」 ٩( 'ω' )و 「はい」 ٩( 'ω' )و 「取り敢えず forever で動かしときました」 ٩( 'ω' )و 「真面目に作る時間も勿体無いので ……」 ٩( 'ω' )و 「Phantom でスナップショットを返すようにしました」 ٩( 'ω' )و 「ぽちっとリリースしたけど Phantom の暴走が心配です」

Slide 117

Slide 117 text

3 日後

Slide 118

Slide 118 text

3 日後 (” ՞ਊ ՞)” 「インデックスに登録されるようになってきましたね」 (” ՞ਊ ՞)” 「遂に NewsPicks にも検索から流入する時代が ……」 (” ՞ਊ ՞)” 「しかしクロールエラーが頻発していますね」

Slide 119

Slide 119 text

3 日後 (” ՞ਊ ՞)” 「インデックスに登録されるようになってきましたね」 (” ՞ਊ ՞)” 「遂に NewsPicks にも検索から流入する時代が ……」 (” ՞ਊ ՞)” 「しかしクロールエラーが頻発していますね」 ٩( 'ω' )و 「やはり Phantom が暴走しまくってます ……」

Slide 120

Slide 120 text

3 日後 (” ՞ਊ ՞)” 「インデックスに登録されるようになってきましたね」 (” ՞ਊ ՞)” 「遂に NewsPicks にも検索から流入する時代が ……」 (” ՞ਊ ՞)” 「しかしクロールエラーが頻発していますね」 ٩( 'ω' )و 「やはり Phantom が暴走しまくってます ……」

Slide 121

Slide 121 text

3 日後 (” ՞ਊ ՞)” 「インデックスに登録されるようになってきましたね」 (” ՞ਊ ՞)” 「遂に NewsPicks にも検索から流入する時代が ……」 (” ՞ਊ ՞)” 「しかしクロールエラーが頻発していますね」 ٩( 'ω' )و 「やはり Phantom が暴走しまくってます ……」

Slide 122

Slide 122 text

3 日後 (” ՞ਊ ՞)” 「インデックスに登録されるようになってきましたね」 (” ՞ਊ ՞)” 「遂に NewsPicks にも検索から流入する時代が ……」 (” ՞ਊ ՞)” 「しかしクロールエラーが頻発していますね」 ٩( 'ω' )و 「やはり Phantom が暴走しまくってます ……」 (” ՞ਊ ՞)” 「さっさと直して下さい」 (” ՞ਊ ՞)” 「明日からクロールエラーはダメよ〜ダメダメ」

Slide 123

Slide 123 text

3 日後 (” ՞ਊ ՞)” 「インデックスに登録されるようになってきましたね」 (” ՞ਊ ՞)” 「遂に NewsPicks にも検索から流入する時代が ……」 (” ՞ਊ ՞)” 「しかしクロールエラーが頻発していますね」 ٩( 'ω' )و 「やはり Phantom が暴走しまくってます ……」 (” ՞ਊ ՞)” 「さっさと直して下さい」 (” ՞ਊ ՞)” 「明日からクロールエラーはダメよ〜ダメダメ」 ٩( 'ω' )و (最近この人よくこのフレーズ使うな ……) ٩( 'ω' )و (流行語か何かなのかな ……)

Slide 124

Slide 124 text

3 日後 ٩( 'ω' )و 「node-phantom が悪さをしていたみたいです」 ٩( 'ω' )و 「child_process を使うと良い感じになりました」

Slide 125

Slide 125 text

3 日後 ٩( 'ω' )و 「node-phantom が悪さをしていたみたいです」 ٩( 'ω' )و 「child_process を使うと良い感じになりました」

Slide 126

Slide 126 text

3 日後 ٩( 'ω' )و 「node-phantom が悪さをしていたみたいです」 ٩( 'ω' )و 「child_process を使うと良い感じになりました」

Slide 127

Slide 127 text

3 日後 ٩( 'ω' )و 「node-phantom が悪さをしていたみたいです」 ٩( 'ω' )و 「child_process を使うと良い感じになりました」 (” ՞ਊ ՞)” 「良いじゃないですか」 (” ՞ਊ ՞)” 「しばらくコレで運用していきましょう」 (” ՞ਊ ՞)” 「これは SEO 対策の第一歩ですからね」

Slide 128

Slide 128 text

3 日後 ٩( 'ω' )و 「node-phantom が悪さをしていたみたいです」 ٩( 'ω' )و 「child_process を使うと良い感じになりました」 (” ՞ਊ ՞)” 「良いじゃないですか」 (” ՞ਊ ՞)” 「しばらくコレで運用していきましょう」 (” ՞ਊ ՞)” 「これは SEO 対策の第一歩ですからね」 ٩( 'ω' )و 「はい」 ٩( 'ω' )و (省コストで対応出来て良かった ……)

Slide 129

Slide 129 text

急成長の代償との闘争編

Slide 130

Slide 130 text

入社 3 ヶ月が経ったころ ...

Slide 131

Slide 131 text

٩( 'ω' )و 「この API って何ですか?」 ٩( 'ω' )و 「JSON に要らないプロパティが大量にあって重いんですけど」 ٩( 'ω' )و 「noinclude っていう変なパラメータがあるんですけど」 入社 3 ヶ月が経ったころ ...

Slide 132

Slide 132 text

(” ՞ਊ ՞)” 「ソフトウェアに歴史アリです」 (” ՞ਊ ՞)” 「急成長の代償に失われたものがあるのです」 ٩( 'ω' )و 「この API って何ですか?」 ٩( 'ω' )و 「JSON に要らないプロパティが大量にあって重いんですけど」 ٩( 'ω' )و 「noinclude っていう変なパラメータがあるんですけど」 入社 3 ヶ月が経ったころ ...

Slide 133

Slide 133 text

(” ՞ਊ ՞)” 「ソフトウェアに歴史アリです」 (” ՞ਊ ՞)” 「急成長の代償に失われたものがあるのです」 ٩( 'ω' )و 「わかります」 ٩( 'ω' )و 「リファクタして良いですか?」 ٩( 'ω' )و 「この API って何ですか?」 ٩( 'ω' )و 「JSON に要らないプロパティが大量にあって重いんですけど」 ٩( 'ω' )و 「noinclude っていう変なパラメータがあるんですけど」 入社 3 ヶ月が経ったころ ...

Slide 134

Slide 134 text

当時のコードの状況 API の後方互換性を維持したままひたすら拡張 ・既に使われていない大量のプロパティ ・同じモデルが複数の API で使い回されてひたすら拡張されていた ・最早どこかを弄ったらどこかが壊れる状態 レイヤリングが殆どされていない ・業務レイヤにまで Web レイヤの実装が登場 ・単体テストなど夢のまた夢 急成長の代償なので一概に悪いものでは無い ・スタートアップは生きるか死ぬか

Slide 135

Slide 135 text

とりあえず

Slide 136

Slide 136 text

٩( 'ω' )و 「少しずつ依存を切り離して ……」 ٩( 'ω' )و 「業務レイヤは独立させよう」 ٩( 'ω' )و 「とはいえ時間が無いので少しずつやっていくか ……」 とりあえず

Slide 137

Slide 137 text

(” ՞ਊ ՞)” 「スタートアップは生きるか死ぬかですからね」 (” ՞ਊ ՞)” 「バランスが大事です」 ٩( 'ω' )و 「少しずつ依存を切り離して ……」 ٩( 'ω' )و 「業務レイヤは独立させよう」 ٩( 'ω' )و 「とはいえ時間が無いので少しずつやっていくか ……」 とりあえず

Slide 138

Slide 138 text

(” ՞ਊ ՞)” 「スタートアップは生きるか死ぬかですからね」 (” ՞ਊ ՞)” 「バランスが大事です」 ٩( 'ω' )و 「わかります」 ٩( 'ω' )و 「あ、この実装まるっと消しますね」 ٩( 'ω' )و 「少しずつ依存を切り離して ……」 ٩( 'ω' )و 「業務レイヤは独立させよう」 ٩( 'ω' )و 「とはいえ時間が無いので少しずつやっていくか ……」 とりあえず

Slide 139

Slide 139 text

もくもく

Slide 140

Slide 140 text

٩( 'ω' )و 「/api/v2/ なんて幻想だな ……」 ٩( 'ω' )و 「C 向けアプリなんてすぐに要件が変わってくるし」 ٩( 'ω' )و 「エンドポイント毎にバージョンをサクッと切り替えたいな」 もくもく

Slide 141

Slide 141 text

(” ՞ਊ ՞)” 「スタートアップは生きるか死ぬかですからね」 (” ՞ਊ ՞)” 「バランスが大事です」 ٩( 'ω' )و 「/api/v2/ なんて幻想だな ……」 ٩( 'ω' )و 「C 向けアプリなんてすぐに要件が変わってくるし」 ٩( 'ω' )و 「エンドポイント毎にバージョンをサクッと切り替えたいな」 もくもく

Slide 142

Slide 142 text

(” ՞ਊ ՞)” 「スタートアップは生きるか死ぬかですからね」 (” ՞ਊ ՞)” 「バランスが大事です」 ٩( 'ω' )و 「わかります」 ٩( 'ω' )و (もう REST じゃなくて RPC で良いよな ……) ٩( 'ω' )و 「/api/v2/ なんて幻想だな ……」 ٩( 'ω' )و 「C 向けアプリなんてすぐに要件が変わってくるし」 ٩( 'ω' )و 「エンドポイント毎にバージョンをサクッと切り替えたいな」 もくもく

Slide 143

Slide 143 text

(” ՞ਊ ՞)” 「スタートアップは生きるか死ぬかですからね」 (” ՞ਊ ՞)” 「バランスが大事です」 ٩( 'ω' )و 「わかります」 ٩( 'ω' )و (もう REST じゃなくて RPC で良いよな ……) ٩( 'ω' )و 「/api/v2/ なんて幻想だな ……」 ٩( 'ω' )و 「C 向けアプリなんてすぐに要件が変わってくるし」 ٩( 'ω' )و 「エンドポイント毎にバージョンをサクッと切り替えたいな」 もくもく @Controller public class NewsController extends ControllerBase { @RequestMapping(value = "/{id}/picks", method = GET, headers = Headers.API_VERSION_2) @ResponseBody public PageableCollection getPicks( @PathVariable Long id, @ModelAttribute PickSearchParams params) { return getPicks(id, params).map(PickViewModelV2.mapper()); } @RequestMapping(value = "/{id}/picks", method = GET, headers = Headers.API_VERSION_3) @ResponseBody public PageableCollection getPicks( @PathVariable Long id, @ModelAttribute PickSearchParams params) { return getPicks(id, params).map(PickViewModelV3.mapper()); } } "CASUAL" VERSION UP

Slide 144

Slide 144 text

年末 ٩( 'ω' )و 「来年は少しずつリファクタしていきたいな」 ٩( 'ω' )و 「まずは大量に出ているエラーメールを撲滅するところから始めよう」 ٩( 'ω' )و 「来年はコードベースをまるっと変えるのを目標にしよう」 ٩( 'ω' )و 「年明けから頑張るぞい」

Slide 145

Slide 145 text

グロース編

Slide 146

Slide 146 text

入社 4 ヶ月が経ったころ ...

Slide 147

Slide 147 text

入社 4 ヶ月が経ったころ ... (” ՞ਊ ՞)” 「DAU が伸び悩んでいます」 (” ՞ਊ ՞)” 「そろそろ感覚で改善していては駄目なようです」 (” ՞ਊ ՞)” 「チームをつくってグロースハックをして下さい」

Slide 148

Slide 148 text

入社 4 ヶ月が経ったころ ... (” ՞ਊ ՞)” 「DAU が伸び悩んでいます」 (” ՞ਊ ՞)” 「そろそろ感覚で改善していては駄目なようです」 (” ՞ਊ ՞)” 「チームをつくってグロースハックをして下さい」 ٩( 'ω' )و (今年の目標が早くも崩れた ……)

Slide 149

Slide 149 text

入社 4 ヶ月が経ったころ ... (” ՞ਊ ՞)” 「DAU が伸び悩んでいます」 (” ՞ਊ ՞)” 「そろそろ感覚で改善していては駄目なようです」 (” ՞ਊ ՞)” 「チームをつくってグロースハックをして下さい」 ٩( 'ω' )و (今年の目標が早くも崩れた ……) (” ՞ਊ ՞)” 「事業計画上の数値はこうです」 (” ՞ਊ ՞)” 「今日と明日は分析をして来週から数値をこれだけ上げましょう」

Slide 150

Slide 150 text

入社 4 ヶ月が経ったころ ... (” ՞ਊ ՞)” 「DAU が伸び悩んでいます」 (” ՞ਊ ՞)” 「そろそろ感覚で改善していては駄目なようです」 (” ՞ਊ ՞)” 「チームをつくってグロースハックをして下さい」 ٩( 'ω' )و (今年の目標が早くも崩れた ……) (” ՞ਊ ՞)” 「事業計画上の数値はこうです」 (” ՞ਊ ՞)” 「今日と明日は分析をして来週から数値をこれだけ上げましょう」 ٩( 'ω' )و (スピード感がヤバい) ٩( 'ω' )و (そもそもログは残ってるんだっけ ……?)

Slide 151

Slide 151 text

REDSHIFT とりあえずトラッキングログを整備 ユーザー行動の分析 KPI ダッシュボードの作成

Slide 152

Slide 152 text

KPI DASHBOARD

Slide 153

Slide 153 text

KPI DASHBOARD

Slide 154

Slide 154 text

KPI DASHBOARD

Slide 155

Slide 155 text

KPI DASHBOARD SPRING BOOT + DOMA 2 + ANGULAR JS + D3

Slide 156

Slide 156 text

KPI DASHBOARD SPRING BOOT + DOMA 2 + ANGULAR JS + D3 管理画面をサクっと作るには最適

Slide 157

Slide 157 text

KPI DASHBOARD SPRING BOOT + DOMA 2 + ANGULAR JS + D3 管理画面のようなサクっと作るアプリには オーバースペック気味 …… 分析用のサーバーが Redshift に 格納したデータを取得

Slide 158

Slide 158 text

KPI DASHBOARD SPRING BOOT + DOMA 2 + ANGULAR JS + D3 管理画面のようなサクっと作るアプリには オーバースペック気味 …… 分析用のサーバーが Redshift に 格納したデータを取得 SELECT news.news, count(news.id) "count" FROM news /*%if condition.paidUser != null*/ INNER JOIN users ON news.user_id = users.user_id /*%if condition.paidUser */ AND users.paid_expired >= current_date /*%else*/ AND (users.paid_expired < current_date OR users.paid_expired is null) /*%end*/ /*%end*/ /*%if condition.userType != null*/ INNER JOIN users ON news.feed = users.user_id /*%if condition.userType == "original" */ AND users.type = 'ORIGINAL_CONTENTS' /*%else*/ AND users.type != 'ORIGINAL_CONTENTS' /*%end*/ /*%end*/ WHERE news.time between /*condition.from*/date'2015-01-07 00:00:00' and /*condition.to*/date'2015-01-15 23:59:59' /*%if condition.newsType != null */ AND news.type = /*condition.newsType*/'body' /*%end*/ /*%if condition.free != null */ AND news.paid /*%if condition.free*/ is null /*%else*/ = true/*%end*/ /*%end*/ /*%if(condition.mobileDevices != null && condition.web)*/ AND 正直しんどい

Slide 159

Slide 159 text

KPI DASHBOARD SPRING BOOT + DOMA 2 + ANGULAR JS + D3 管理画面のようなサクっと作るアプリには オーバースペック気味 …… 分析用のサーバーが Redshift に 格納したデータを取得 SQL2O SIMPLE JDBC QUERY LIBRARY public List findOpenNewsEvents(Date from, Date to) { String query = "select * from users_events where time >= :from and time < :to and event = ..."; try (Connection connection = db.open()) { return connection.createQuery(query) .throwOnMappingFailure(false) .addParameter("from", from) .addParameter("to", to) .executeAndFetch(UsersEvent.class); } }

Slide 160

Slide 160 text

その後

Slide 161

Slide 161 text

٩( 'ω' )و 「ダッシュボードが出来ました」 ٩( 'ω' )و 「ユーザー行動も分析しました」 ٩( 'ω' )و 「やはり ○○ の辺りが怪しいです」 その後

Slide 162

Slide 162 text

(” ՞ਊ ՞)” 「やっと数値が見られるようになりましたね。喜ばしいことです」 (” ՞ਊ ՞)” 「では早速 KAIZEN しましょう。結果を必ず分析しましょうね」 ٩( 'ω' )و 「ダッシュボードが出来ました」 ٩( 'ω' )و 「ユーザー行動も分析しました」 ٩( 'ω' )و 「やはり ○○ の辺りが怪しいです」 その後

Slide 163

Slide 163 text

(” ՞ਊ ՞)” 「やっと数値が見られるようになりましたね。喜ばしいことです」 (” ՞ਊ ՞)” 「では早速 KAIZEN しましょう。結果を必ず分析しましょうね」 ٩( 'ω' )و 「はい」 ٩( 'ω' )و (そういえば A/B テストとか出来るのかな ……) ٩( 'ω' )و 「ダッシュボードが出来ました」 ٩( 'ω' )و 「ユーザー行動も分析しました」 ٩( 'ω' )و 「やはり ○○ の辺りが怪しいです」 その後

Slide 164

Slide 164 text

(” ՞ਊ ՞)” 「やっと数値が見られるようになりましたね。喜ばしいことです」 (” ՞ਊ ՞)” 「では早速 KAIZEN しましょう。結果を必ず分析しましょうね」 ٩( 'ω' )و 「はい」 ٩( 'ω' )و (そういえば A/B テストとか出来るのかな ……) ٩( 'ω' )و 「ダッシュボードが出来ました」 ٩( 'ω' )و 「ユーザー行動も分析しました」 ٩( 'ω' )و 「やはり ○○ の辺りが怪しいです」 その後 当然できなかった

Slide 165

Slide 165 text

悲しいのでサクっと改修した

Slide 166

Slide 166 text

٩( 'ω' )و 「アプリの文言はサーバーから取得するようにして ……」 ٩( 'ω' )و 「タブ情報もユーザー毎に切り替えられるように ……」 ٩( 'ω' )و 「ユーザーをセグメント化して一部ユーザーだけにリリースしたいな」 悲しいのでサクっと改修した

Slide 167

Slide 167 text

(” ՞ਊ ՞)” 「最小のコストで最大の成果をデスヨ」 (” ՞ਊ ՞)” (開発なんてしてんじゃねーよ) ٩( 'ω' )و 「アプリの文言はサーバーから取得するようにして ……」 ٩( 'ω' )و 「タブ情報もユーザー毎に切り替えられるように ……」 ٩( 'ω' )و 「ユーザーをセグメント化して一部ユーザーだけにリリースしたいな」 悲しいのでサクっと改修した

Slide 168

Slide 168 text

(” ՞ਊ ՞)” 「最小のコストで最大の成果をデスヨ」 (” ՞ਊ ՞)” (開発なんてしてんじゃねーよ) ٩( 'ω' )و 「アプリの文言はサーバーから取得するようにして ……」 ٩( 'ω' )و 「タブ情報もユーザー毎に切り替えられるように ……」 ٩( 'ω' )و 「ユーザーをセグメント化して一部ユーザーだけにリリースしたいな」 悲しいのでサクっと改修した ٩( 'ω' )و 「わかります」 ٩( 'ω' )و 「UPDATE features SET roles = 'BETA-TESTERS' WHERE id = 'AWESOME_FEATURE'」 ٩( 'ω' )و 「カチャカチャ・・・ッターン!」 ٩( 'ω' )و (ドヤァ・・・)

Slide 169

Slide 169 text

社内メンバー限定公開機能も作った VERY GOOD GOOD BAD RECOMMEND ENGINE

Slide 170

Slide 170 text

社内メンバー限定公開機能も作った VERY GOOD GOOD BAD RECOMMEND ENGINE ふむふむ (学習中)

Slide 171

Slide 171 text

その後

Slide 172

Slide 172 text

٩( 'ω' )و 「Facebook ページが息をしていないようです」 ٩( 'ω' )و 「メルマガを配信するお金が無いです」 ٩( 'ω' )و 「プッシュ通知が〜」 その後

Slide 173

Slide 173 text

(” ՞ਊ ՞)” 「最小のコストで最大の成果をデスヨ」 (” ՞ਊ ՞)” 「地道な努力が必要デスヨ」 ٩( 'ω' )و 「Facebook ページが息をしていないようです」 ٩( 'ω' )و 「メルマガを配信するお金が無いです」 ٩( 'ω' )و 「プッシュ通知が〜」 その後

Slide 174

Slide 174 text

(” ՞ਊ ՞)” 「最小のコストで最大の成果をデスヨ」 (” ՞ਊ ՞)” 「地道な努力が必要デスヨ」 ٩( 'ω' )و 「Facebook ページが息をしていないようです」 ٩( 'ω' )و 「メルマガを配信するお金が無いです」 ٩( 'ω' )و 「プッシュ通知が〜」 その後 ٩( 'ω' )و 「わかります」 ٩( 'ω' )و 「取り敢えずメール配信サービス契約しますね」 ٩( 'ω' )و 「リテンション・・・ダイジ・・・AARRR・・・」

Slide 175

Slide 175 text

(” ՞ਊ ՞)” 「最小のコストで最大の成果をデスヨ」 (” ՞ਊ ՞)” 「地道な努力が必要デスヨ」 ٩( 'ω' )و 「Facebook ページが息をしていないようです」 ٩( 'ω' )و 「メルマガを配信するお金が無いです」 ٩( 'ω' )و 「プッシュ通知が〜」 その後 ٩( 'ω' )و 「わかります」 ٩( 'ω' )و 「取り敢えずメール配信サービス契約しますね」 ٩( 'ω' )و 「リテンション・・・ダイジ・・・AARRR・・・」 闘いの日々は続く

Slide 176

Slide 176 text

NewsPicks Angular やめるってよ編

Slide 177

Slide 177 text

入社 5 ヶ月が経ったころ ...

Slide 178

Slide 178 text

入社 5 ヶ月が経ったころ ... (” ՞ਊ ՞)” 「ウェブ版がヤバいです」 (” ՞ਊ ՞)” 「SEO の効果もイマイチだし」 (” ՞ਊ ՞)” 「モバイル版は初期表示が遅すぎます」

Slide 179

Slide 179 text

入社 5 ヶ月が経ったころ ... (” ՞ਊ ՞)” 「ウェブ版がヤバいです」 (” ՞ਊ ՞)” 「SEO の効果もイマイチだし」 (” ՞ਊ ՞)” 「モバイル版は初期表示が遅すぎます」 ٩( 'ω' )و (Angular 使ってるからな・・・)

Slide 180

Slide 180 text

入社 5 ヶ月が経ったころ ... (” ՞ਊ ՞)” 「ウェブ版がヤバいです」 (” ՞ਊ ՞)” 「SEO の効果もイマイチだし」 (” ՞ਊ ՞)” 「モバイル版は初期表示が遅すぎます」 ٩( 'ω' )و (Angular 使ってるからな・・・) (” ՞ਊ ՞)” 「デザインリニューアルもしたいですが」 (” ՞ਊ ՞)” 「取り敢えず Angular 止めようぜ」

Slide 181

Slide 181 text

入社 5 ヶ月が経ったころ ... (” ՞ਊ ՞)” 「ウェブ版がヤバいです」 (” ՞ਊ ՞)” 「SEO の効果もイマイチだし」 (” ՞ਊ ՞)” 「モバイル版は初期表示が遅すぎます」 ٩( 'ω' )و (Angular 使ってるからな・・・) (” ՞ਊ ՞)” 「デザインリニューアルもしたいですが」 (” ՞ਊ ՞)” 「取り敢えず Angular 止めようぜ」 ٩( 'ω' )و 「わかります」 ٩( 'ω' )و 「Server Side React ですね」

Slide 182

Slide 182 text

入社 5 ヶ月が経ったころ ... (” ՞ਊ ՞)” 「技術者のエゴはダメよ〜ダメダメ」 (” ՞ਊ ՞)” 「テスト含めて 3 週間で作り直し頼む」 (” ՞ਊ ՞)” 「エンジニアは 1.5 人ぐらいで頼む」

Slide 183

Slide 183 text

入社 5 ヶ月が経ったころ ... (” ՞ਊ ՞)” 「技術者のエゴはダメよ〜ダメダメ」 (” ՞ਊ ՞)” 「テスト含めて 3 週間で作り直し頼む」 (” ՞ਊ ՞)” 「エンジニアは 1.5 人ぐらいで頼む」 ٩( 'ω' )و (スピード感がヤバい)

Slide 184

Slide 184 text

入社 5 ヶ月が経ったころ ... (” ՞ਊ ՞)” 「技術者のエゴはダメよ〜ダメダメ」 (” ՞ਊ ՞)” 「テスト含めて 3 週間で作り直し頼む」 (” ՞ਊ ՞)” 「エンジニアは 1.5 人ぐらいで頼む」 ٩( 'ω' )و (スピード感がヤバい) (” ՞ਊ ՞)” 「アプリのコピーではなくメディアサイトに作り直したいので」 (” ՞ਊ ՞)” 「ちょちょっと直してよ、あ、IE 7 対応でおなしゃす!」

Slide 185

Slide 185 text

入社 5 ヶ月が経ったころ ... (” ՞ਊ ՞)” 「技術者のエゴはダメよ〜ダメダメ」 (” ՞ਊ ՞)” 「テスト含めて 3 週間で作り直し頼む」 (” ՞ਊ ՞)” 「エンジニアは 1.5 人ぐらいで頼む」 ٩( 'ω' )و (スピード感がヤバい) (” ՞ਊ ՞)” 「アプリのコピーではなくメディアサイトに作り直したいので」 (” ՞ਊ ՞)” 「ちょちょっと直してよ、あ、IE 7 対応でおなしゃす!」 ٩( 'ω' )و 「わかります」 ٩( 'ω' )و 「PHP ですね」

Slide 186

Slide 186 text

ぶっちゃけ React したかった

Slide 187

Slide 187 text

٩( 'ω' )و (いつやるの?今でしょ!) ٩( 'ω' )و (今やったら死ぬ未来しか見えない) ٩( 'ω' )و (・・・) ぶっちゃけ React したかった

Slide 188

Slide 188 text

(” ՞ਊ ՞)” 「最小のコストで最大の成果をデスヨ」 (” ՞ਊ ՞)” 「転送量の削減も頼む」 ٩( 'ω' )و (いつやるの?今でしょ!) ٩( 'ω' )و (今やったら死ぬ未来しか見えない) ٩( 'ω' )و (・・・) ぶっちゃけ React したかった

Slide 189

Slide 189 text

(” ՞ਊ ՞)” 「最小のコストで最大の成果をデスヨ」 (” ՞ਊ ՞)” 「転送量の削減も頼む」 ٩( 'ω' )و (いつやるの?今でしょ!) ٩( 'ω' )و (今やったら死ぬ未来しか見えない) ٩( 'ω' )و (・・・) ぶっちゃけ React したかった ٩( 'ω' )و 「わかります」 ٩( 'ω' )و 「昔ながらの作りでいきましょう」

Slide 190

Slide 190 text

(” ՞ਊ ՞)” 「最小のコストで最大の成果をデスヨ」 (” ՞ਊ ՞)” 「転送量の削減も頼む」 ٩( 'ω' )و (いつやるの?今でしょ!) ٩( 'ω' )و (今やったら死ぬ未来しか見えない) ٩( 'ω' )و (・・・) ぶっちゃけ React したかった ٩( 'ω' )و 「わかります」 ٩( 'ω' )و 「昔ながらの作りでいきましょう」 CLIENT No jQuery No FRAMEWORK POLYFILL SERVICE BOWER GRUNT SERVER NON-REST API NO XDR SPRING MVC THYMELEAF EMBEDDED TOMCAT 5 年前のアーキテクチャを 少しだけモダンにする

Slide 191

Slide 191 text

(” ՞ਊ ՞)” 「最小のコストで最大の成果をデスヨ」 (” ՞ਊ ՞)” 「転送量の削減も頼む」 ٩( 'ω' )و (いつやるの?今でしょ!) ٩( 'ω' )و (今やったら死ぬ未来しか見えない) ٩( 'ω' )و (・・・) ぶっちゃけ React したかった ٩( 'ω' )و 「わかります」 ٩( 'ω' )و 「昔ながらの作りでいきましょう」 CLIENT No jQuery No FRAMEWORK POLYFILL SERVICE BOWER GRUNT SERVER NO XDR NON-REST API SPRING MVC THYMELEAF EMBEDDED TOMCAT 5 年前のアーキテクチャを 少しだけモダンにする POLYFILL SERVICE UA に応じて最適な polyfill が提供される $el.find(selector) → el.querySelector(selector) $.getJSON(url) → fetch(url)

Slide 192

Slide 192 text

(” ՞ਊ ՞)” 「最小のコストで最大の成果をデスヨ」 (” ՞ਊ ՞)” 「転送量の削減も頼む」 ٩( 'ω' )و (いつやるの?今でしょ!) ٩( 'ω' )و (今やったら死ぬ未来しか見えない) ٩( 'ω' )و (・・・) ぶっちゃけ React したかった ٩( 'ω' )و 「わかります」 ٩( 'ω' )و 「昔ながらの作りでいきましょう」 CLIENT No jQuery No FRAMEWORK POLYFILL SERVICE BOWER GRUNT SERVER NO XDR NON-REST API SPRING MVC THYMELEAF EMBEDDED TOMCAT 5 年前のアーキテクチャを 少しだけモダンにする BUILD 安定の GRUNT & BOWER COMPILE CONCAT MINIFY CACHE BURST etc ...

Slide 193

Slide 193 text

(” ՞ਊ ՞)” 「最小のコストで最大の成果をデスヨ」 (” ՞ਊ ՞)” 「転送量の削減も頼む」 ٩( 'ω' )و (いつやるの?今でしょ!) ٩( 'ω' )و (今やったら死ぬ未来しか見えない) ٩( 'ω' )و (・・・) ぶっちゃけ React したかった ٩( 'ω' )و 「わかります」 ٩( 'ω' )و 「昔ながらの作りでいきましょう」 CLIENT No jQuery No FRAMEWORK POLYFILL SERVICE BOWER GRUNT SERVER NO XDR NON-REST API SPRING MVC THYMELEAF EMBEDDED TOMCAT 5 年前のアーキテクチャを 少しだけモダンにする PRODUCTIVITY WATCH & COMPILE & SYMLINK

Slide 194

Slide 194 text

(” ՞ਊ ՞)” 「最小のコストで最大の成果をデスヨ」 (” ՞ਊ ՞)” 「転送量の削減も頼む」 ٩( 'ω' )و (いつやるの?今でしょ!) ٩( 'ω' )و (今やったら死ぬ未来しか見えない) ٩( 'ω' )و (・・・) ぶっちゃけ React したかった ٩( 'ω' )و 「わかります」 ٩( 'ω' )و 「昔ながらの作りでいきましょう」 CLIENT No jQuery No FRAMEWORK POLYFILL SERVICE BOWER GRUNT SERVER NO XDR NON-REST API SPRING MVC THYMELEAF EMBEDDED TOMCAT 5 年前のアーキテクチャを 少しだけモダンにする THYMELEAF ぶっちゃけしんどい HTML フラグメントはツラいよ

Slide 195

Slide 195 text

(” ՞ਊ ՞)” 「最小のコストで最大の成果をデスヨ」 (” ՞ਊ ՞)” 「転送量の削減も頼む」 ٩( 'ω' )و (いつやるの?今でしょ!) ٩( 'ω' )و (今やったら死ぬ未来しか見えない) ٩( 'ω' )و (・・・) ぶっちゃけ React したかった ٩( 'ω' )و 「わかります」 ٩( 'ω' )و 「昔ながらの作りでいきましょう」 CLIENT No jQuery No FRAMEWORK POLYFILL SERVICE BOWER GRUNT SERVER NO XDR NON-REST API SPRING MVC THYMELEAF EMBEDDED TOMCAT 5 年前のアーキテクチャを 少しだけモダンにする THYMELEAF ぶっちゃけしんどい HTML フラグメントはツラいよ CLIENT SERVER HANDLEBARS GRUNT COMPILED TEMPLATES WEB-INF TEMPLATES RENDER TEMPLATE GET NEWS PAGE RETURN HTML RELOAD BROWSER RECEIVE REQUEST GET NEXT PAGE FRAGMENT RETURN JSON CHECK DUPLICATION RENDER COMPILED TEMPLATE

Slide 196

Slide 196 text

(” ՞ਊ ՞)” 「最小のコストで最大の成果をデスヨ」 (” ՞ਊ ՞)” 「転送量の削減も頼む」 ٩( 'ω' )و (いつやるの?今でしょ!) ٩( 'ω' )و (今やったら死ぬ未来しか見えない) ٩( 'ω' )و (・・・) ぶっちゃけ React したかった ٩( 'ω' )و 「わかります」 ٩( 'ω' )و 「昔ながらの作りでいきましょう」 CLIENT No jQuery No FRAMEWORK POLYFILL SERVICE BOWER GRUNT SERVER NO XDR NON-REST API SPRING MVC THYMELEAF EMBEDDED TOMCAT 5 年前のアーキテクチャを 少しだけモダンにする THYMELEAF ぶっちゃけしんどい HTML フラグメントはツラいよ CLIENT SERVER HANDLEBARS GRUNT COMPILED TEMPLATES WEB-INF TEMPLATES RENDER TEMPLATE GET NEWS PAGE RETURN HTML RELOAD BROWSER RECEIVE REQUEST GET NEXT PAGE FRAGMENT RETURN JSON CHECK DUPLICATION RENDER COMPILED TEMPLATE TO BE CONTINUED ...

Slide 197

Slide 197 text

適切なタイミングで必要な技術を取捨選択 ビジネスの成長と技術的負債のバランス

Slide 198

Slide 198 text

まとめ ビジネスの成長と技術的負債のバランスを取る 用途に応じて適切な技術を採用する Java は素早く簡単に作ることも堅く作ることも出来る コンテンツ x テクノロジーで世界一の経済メディアをつくる

Slide 199

Slide 199 text

ありがとうございました

Slide 200

Slide 200 text

UZABASE / NewsPicks では 一緒に働いてくれるエンジニアを募集しています 世界一の経済メディアを作りたい人 グローバルな職場で世界展開するサービスに携わりたい人 大量データ / 大量アクセスのあるサービスの開発をしたい人 若くて勢いのあるスタートアップで働きたい人 エンジニアが自由に働ける会社で挑戦したい人 お待ちしております