Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Node.jsv0.8からv4.xへのバージョンアップ ~大規模Push通知基盤の運用事例~

10,320 views

Published on

2015/11/07 Node学園祭での、伊藤の講演資料になります

Published in: Technology

Node.jsv0.8からv4.xへのバージョンアップ ~大規模Push通知基盤の運用事例~

  1. 1. Node.js v0.8からv4.xへのバージョンアップ ~ 大規模Push通知基盤の運用事例 ~ 株式会社リクルートテクノロジーズ APソリューショングループ 伊藤 瑛
  2. 2. Page 2 自己紹介 ■名前 伊藤 瑛 ■所属 リクルートテクノロジーズ アプリケーションソリューションG 2015年度新卒入社 / Node歴 3ヶ月 ■やっていること Node製の大規模Push基盤Pusna-RSの運用開発
  3. 3. Page 3 アジェンダ 1. Pusna-RSについて 2. Node.js v4.xへのアップデートについて 1. Pusna-RSのStream 2. アップデートの途中経過 3. まとめ
  4. 4. Page 4 Pusna-RSとは  Push Notification Aggregator Realtime & Scalable モバイルアプリのためのPush通知基盤  Node.jsを用いて高いリアルタイム性 AWSの各種機能を用いたスケーラビリティ 双方の確保  リクルートグループの主要なスマホアプリで 用いられている  運用開始から2年経ち、億のデバイスを扱う規模に なっても無停止で安定稼動中!
  5. 5. Page 5 システム概要 開発期間 • 2013年9月~2013年12月 サービス概要 • 2013年12月末 (約2年間稼 働) • 億を超えるデバイスにPush 通知を配信 • 100を超えるアプリで多様な 使われ方をしている。 PusnaRS プラットフォーム • Node.js v0.8.24 フレームワーク • Express 3 • Rendr システム構成概要
  6. 6. Page 6 Node.js v0.8からv4.xへ
  7. 7. Page 7 バージョンアップの検討 セキュリティリスクの排除 新技術への追従 Node.jsとIo.jsの関係性が落ち着いた システム上大きな問題は起きていないにもかかわらず、 いまこのタイミングでやる理由
  8. 8. Page 8 セキュリティリスク  今年に入ってNode.js / Io.jsに2件の脆弱性 CVE-2015-5380 V8起因のバグ。DoSの危険性 CVE-2015-7384 Dosの可能性 旧バージョンに脆弱性が見つかった場合対応できない
  9. 9. Page 9 新技術への追従  新規格への対応 APNsがiOS9からHTTP/2対応に。将来的にHTTP/2対応必須 になるかもしれない  ES2015などの新技術の開拓と知見の集積
  10. 10. Page 10 NodeとIoの関係性が落ち着いた  9 / 8にNode.js v4がリリース!  Node.jsとIo.jsの統合  LTSのリリース Node.js v4.2 Argon https://github.com/nodejs/LTS/  30ヶ月サポートされる 今が移行に最適なタイミング!
  11. 11. Page 11 今日の主旨 バージョンアップのなかで 見えてきたものを 共有したいと思います!
  12. 12. Page 12 全体構成 デバイス管理 プッシュ配信管理 APNs GCM スマホ リクルート DynamoDB elasticsearch 登録API 登録WorkerSQS 配信worker 操作用Web UI 管理API SQS 配信担当者 データ基盤 事業サーバ
  13. 13. Page 13 全体構成 デバイス管理 プッシュ配信管理 APNs GCM スマホ リクルート DynamoDB elasticsearch 登録API 登録WorkerSQS 配信worker 操作用Web UI 管理API SQS 配信担当者 データ基盤 事業サーバ
  14. 14. Page 14 全体構成 デバイス管理 プッシュ配信管理 APNs GCM スマホ リクルート DynamoDB elasticsearch 登録API 登録WorkerSQS 配信worker 操作用Web UI 管理API SQS 配信担当者 データ基盤 事業サーバ
  15. 15. Page 15 全体構成 デバイス管理 プッシュ配信管理 APNs GCM スマホ リクルート DynamoDB elasticsearch 登録API 登録WorkerSQS 配信worker 操作用Web UI 管理API SQS 配信担当者 データ基盤 事業サーバ Node Application
  16. 16. Page 16 どう移行させるか 正常動作させる • Node.js v4.xへアップグ レードすることにより 動かなくなるコードの修 正 • Node Core API • npmパッケージのアッ プグレード 新技術の取り入れ • 古い実装を新しい実装 に書き換える • Stream3 • ES2015 • npmパッケージのリプ レース • etc...
  17. 17. Page 17 どう移行させるか 正常動作させる • Node.js v4.xへアップグ レードすることにより、 動かなくなるコードの修 正 • Node Core API • npmパッケージのアッ プグレード 新技術の取り入れ • 古い実装を新しい実装 に書き換える • Stream3 • ES2015 • npmパッケージのリプ レース • etc...
  18. 18. Page 18 どう移行させるか 正常動作させる • Node.js v4.xへアップグ レードすることにより、 動かなくなるコードの修 正 • Node Core API • npmパッケージのアッ プグレード 新技術の取り入れ • 古い実装を新しい実装 に書き換える • Stream3 • ES2015 • npmパッケージのリプ レース • etc... Pusna-RSにおけるStreamの移行の話をします!
  19. 19. Page 19 Streamが使われている部分 デバイス管理 プッシュ配信管理 APNs GCM スマホ リクルート DynamoDB elasticsearch 登録API 登録WorkerSQS 配信worker 操作用Web UI 管理API SQS 配信担当者 データ基盤 事業サーバ Push配信の実行workerでStreamを活用
  20. 20. Page 20 Stream API  データの”流れ”を綺麗に扱うためのAPI  データを一括で読み込むのではなく、破片ごとに読み 処理することができる  各Streamをpipe()で連結することができる Readable • I/Oなどからの 読み込み Readable / Writable (Transform) • データの整形 Writable • I/Oなどへの書 き出し
  21. 21. Page 21 Node.jsのStream APIの変遷 実装Ver 安全性 後方互換性 Stream 1 - △ データの取りこぼしやStream のpause(), resume()が頻繁 に呼ばれ、パフォーマンスが 劣化する危険性 - Stream 2 v0.10 ◯ 内部バッファの実装により I/Oの不安定な流れに強く なった。 △ following mode (Stream1互 換モード)とpuase modeを交 互に行き交えない Stream 3 v0.12 ◯ ◯ Stream 3 = Stream 1 + Stream2
  22. 22. Page 22 Node.jsのStream APIの変遷 実装Ver 安全性 後方互換性 Stream 1 - △ データの取りこぼしやStream のpause(), resume()が頻繁 に呼ばれ、パフォーマンスが 劣化する危険性 - Stream 2 v0.10 ◯ 内部バッファの実装により I/Oの不安定な流れに強く なった。 △ following mode (Stream1互 換モード)とpuase modeを交 互に行き交えない Stream 3 v0.12 ◯ ◯ Stream 3 = Stream 1 + Stream2 現在のPusnaの実装 ここにUpgradeしたい
  23. 23. Page 23 StreamによるPush通知配信処理の実装 SQS 配信準備ストリーム 配信タイプ・デバ イスを判断 配信準備ストリームと配信実行ストリームにより構成  準備ストリーム SQSからのメッセージを受信して、配信タイプ・対象デバイス ごとに適切なストリームをインスタンス化し、配信実行スト リームを組み立てる  実行ストリーム 実際の配信処理を行うストリーム 実行ストリーム Android: 全デバイス 実行ストリーム iOS: 指定デバイス 実行ストリーム iOS: 個別デバイス
  24. 24. Page 24 配信準備ストリームの構成 配信処理を組み立てるためのストリーム SQS Readable Stream pollingしてメッ セージを受信 dataイベントを emit Execlusive Executor pipe() pipe() Readable/Writable 二重配信を防 ぐためDynamo のステータスを 更新 Writable 配信タイプ・対 象デバイスご とにストリーム を組み立て、 実行 データ受信 ステータス更新 配信処理組み立て 実際の処理へ
  25. 25. Page 25 reader データ抽出 データ形式変換 APNs/GCMに送信 送信結果を保存 scanStream queryStream searchStream transfer pushTransfer Stream idTransfer Stream notification apns Stream gcm Stream result dynamoResult Stream registration sns Stream pipe() pipe() pipe()pipe() 配信実行ストリームの構成 4段階の処理の流れ
  26. 26. Page 26 reader データ抽出 データ形式変換 APNs/GCMに送信 送信結果を保存 scanStream queryStream searchStream transfer pushTransfer Stream idTransfer Stream notification apns Stream gcm Stream result dynamoResult Stream registration sns Stream pipe() pipe() pipe()pipe() 配信実行ストリームの構成 4段階の処理の流れ Dynamo & ES からデータ抽出
  27. 27. Page 27 reader データ抽出 データ形式変換 APNs/GCMに送信 送信結果を保存 scanStream queryStream searchStream transfer pushTransfer Stream idTransfer Stream notification apns Stream gcm Stream result dynamoResult Stream registration sns Stream pipe() pipe() pipe()pipe() 配信実行ストリームの構成 4段階の処理の流れ データ整形
  28. 28. Page 28 reader データ抽出 データ形式変換 APNs/GCMに送信 送信結果を保存 scanStream queryStream searchStream transfer pushTransfer Stream idTransfer Stream notification apns Stream gcm Stream result dynamoResult Stream registration sns Stream pipe() pipe() pipe()pipe() 配信実行ストリームの構成 4段階の処理の流れ 配信依頼
  29. 29. Page 29 reader データ抽出 データ形式変換 APNs/GCMに送信 送信結果を保存 scanStream queryStream searchStream transfer pushTransfer Stream idTransfer Stream notification apns Stream gcm Stream result dynamoResult Stream registration sns Stream pipe() pipe() pipe()pipe() 配信実行ストリームの構成 4段階の処理の流れ 結果を保存 未達デバイス の削除
  30. 30. Page 30 reader データ抽出 データ形式変換 APNs/GCMに送信 送信結果を保存 scanStream queryStream searchStream transfer pushTransfer Stream idTransfer Stream notification apns Stream gcm Stream result dynamoResult Stream registration sns Stream pipe() pipe() pipe()pipe() 配信実行ストリームの構成 例) Android全端末配信の場合
  31. 31. Page 31 reader データ抽出 データ形式変換 APNs/GCMに送信 送信結果を保存 scanStream queryStream searchStream transfer pushTransfer Stream idTransfer Stream notification apns Stream gcm Stream result dynamoResult Stream registration sns Stream pipe() pipe() pipe()pipe() 配信実行ストリームの構成 例) iOS指定端末の配信
  32. 32. Page 32 配信処理実装上のポイント Stream1 Pusna-RSでの工夫 Stream3 データの 取りこぼし バッファリングを 独自実装 (Stream2と 同等の機能) Bufferingを 標準実装 Streamの作り方 が煩雑 through (by @dominictarr) 使いやすい 標準API
  33. 33. Page 33 配信処理実装上のポイント Stream1 Pusna-RSでの工夫 Stream3 データの 取りこぼし バッファリングを 独自実装 (Stream2と 同等の機能) Bufferingを 標準実装 Streamの作り方 が煩雑 through (by @dominictarr) 使いやすい 標準API
  34. 34. Page 34 Stream1では  自分でbufferで管理 function GcmNotificationStream(options) { Stream.call(this); this.requests = []; // ①: bufferを作成 } util.inherits(GcmNotificationStream, Stream); GcmNotificationStream.prototype.write = function(data) { var req = send(data, function(err, result) { this.requests.splice(....); // ③: bufferから削除 this.emit(‘data’, result); // ④: dataを書き出し });s this.requests.push(data); // ②: bufferにpush ............................. // hwm以下だったら書き込み可 return self.requests.length < self.highWaterMark; };
  35. 35. Page 35 Stream3では  標準でbufferingをしてくれる function GcmNotificationStream(options) { Transform.call(this); // Writable / Readable Stream } util.inherits(GcmNotificationStream, Transform); GcmNotificationStream.prototype._write = function(chunk, enc, cb) { // chunkを加工 this.push(data); };
  36. 36. Page 36 配信処理実装上のポイント Stream1 Pusna-RSでの工夫 Stream3 データの 取りこぼし バッファリングを 独自実装 (Stream2と 同等の機能) Bufferingを 標準実装 Streamの作り方 が煩雑 through (by @dominictarr) 使いやすい 標準API
  37. 37. Page 37 Stream1では  throughを使ってシンプルにかける var stream = through(function (data) { // データを加工 ............... this.push(data); });
  38. 38. Page 38 Stream3では  標準APIでthroughとほぼ同じ npmの依存を減らせる! const Transform = require(‘stream’).Transform; new Transform { transform: function(chunk, enc, cb) { // ロジックを実装 } })).....
  39. 39. Page 39 class構文  ES5での書き方  ES2015での書き方 function GcmNotificationStream(options) { Stream.call(this); } util.inherits(GcmNotificationStream, Stream); GcmNotificationStream.prototype._write ..... class GcmNotificationStream extends Writable{ constructor(options) { super(); } _wirte(chunk, enc, cb) { } }
  40. 40. Page 40 Pusna-RSのStream移行のまとめ Stream1の時代から 内部バッファを実装 throughなどの パッケージの活用 標準実装へ
  41. 41. Page 41 Node.js v4.x移行 進捗状況
  42. 42. Page 42 Node.js v4.x対応進捗 デバイス管理 プッシュ配信 APNs GCM スマホ リクルート DynamoDB elasticsearch 登録API 登録WorkerSQS 配信worker 操作用Web UI 管理API SQS 配信担当者 データ基盤 事業サーバ
  43. 43. Page 43 Node.js v4.x対応進捗 デバイス管理 プッシュ配信 APNs GCM スマホ リクルート DynamoDB elasticsearch 登録API 登録WorkerSQS 配信worker 操作用Web UI 管理API SQS 配信担当者 データ基盤 事業サーバ
  44. 44. Page 44 Node.js v4.xへの移行状況  各アプリケーションをv4.x系に移行中!  細かいCore APIの変更やnpmパッケージの問題などは あったが、正常動作させるのは難しくない  移行が終わったものは順次デプロイし動作試験中!
  45. 45. Page 45 簡易ベンチマーク  デバイス登録シナリオ デバイス管理 登録API 登録WorkerSQS DynamoDB elasticsearch  登録APIに10000件リクエストを送信し、スループットを計測 登録APIサーバはSQSにキューイングした時点でレスポンスを返 す。  50同時リクエスト  server: AWS m3.medium  Node.js v4.2.1 + Express4 vs Node.js v0.8.24 + Express3
  46. 46. Page 46 0 50 100 150 200 250 Node.js v0.8.24 Node.js v4.2.1 結果 197.8 q/sec 160.9 q/secq/sec
  47. 47. Page 47 Node.js v4.xの方が遅い...?  他のベンチマークを見るとv4.xの方が速い  ソースコードの修正点は特になし  エラーログなども出ていない
  48. 48. Page 48 確認  v0.8とv4.xで単純なexpressアプリでBenchmarkを とってみる (ついでなのでv0.10, v0.12, v5もとりま した)  スループットを比較  AWS m3.medium (client, serverともに) var express = require('express'); var app = express(); app.get('/', function(req, res) { res.send('Hello, World'); }); app.listen(3000);
  49. 49. Page 49 結果 0 200 400 600 800 1000 1200 1400 1600 v0.8.24 v0.10.40 v0.12.7 v4.2.2 v5.0.0 q/sec q/sec どのバージョンもスループットにあまり差はなし
  50. 50. Page 50 色々試した結果  aws-sdkのバージョンが関係していそう Node.js v0.8: aws-sdk@1.12 Node.js v4.2: aws-sdk@2.2.11  Node.js v4.xへのバージョンアップのついでにあげて いた  バージョンをaws-sdk@1.12に揃えて再評価
  51. 51. Page 51 aws-sdkを揃えた結果  デバイス登録シナリオで再実験 0 50 100 150 200 250 1 2 197.8 q/sec 197.3 q/secq/sec
  52. 52. Page 52 aws-sdk 2.2.11が遅いというわけではない  Node.js v0.8.24 + aws-sdk@1.12 と Node.js v0.8.24 + aws-sdk@2.2.11で比較 0 50 100 150 200 250 1 2 197.8 q/sec 194.3 q/secq/sec
  53. 53. Page 53 容疑者の1人  aws-sdk@1.17で追加されたProgressStreamの影響 S3などへのアップロード/ダウンロードをプログレスバーなど で表示できる機能が追加された  aws-sdk@2.2.11から該当コードをコメントアウト Node.js v0.8.24 + aws-sdk@1.12 と Node.js v4.2.1 + aws-sdk@2.2.11 を再度比較 ⇨Stream2以上で有効になる機能
  54. 54. Page 54 結果 0 50 100 150 200 250 1 2 3 Throughputの改善が見られた q/sec 197.8 q/sec 181 q/sec 160.9 q/sec
  55. 55. Page 55 今日のまとめ  Pusna-RSはNode.js製の大規模Push通知基盤 運用開始から2年、扱うデバイスが億を超えても 安定稼動中!  Pusna-RSのNode.jsをv0.8からv4.xにあげる 作業を実施中。 Streamを1から3にあげる  npmとの依存もあるがそのままのコードだとむしろ v0.8よりもパフォーマンスが下がることがある。
  56. 56. Page 56 おわりに
  57. 57. Page 57 リクルートテクノロジーズでは ”最新”のNodeを使って一緒に 仕事をする仲間を探していま す!
  58. 58. Page 58
  59. 59. Page 59 ありがとうございました!
  60. 60. Page 60
  61. 61. Page 62 ちょっとしたおまけ
  62. 62. Page 63 Node 0.8からNode 4のAPIの変更  細かい変更点はwikiのBreaking API Changesにのっ てる  実際に遭遇したケースを紹介  net / tls / http(s)などのネットワーク系のモジュール で大きな変更点があったという印象
  63. 63. Page 64 ケース1: Keep Alive Agent  v0.8のhttp.Agentは機能的に十分ではなかった SocketをPoolしないでremoveしてしまう実装 self.on('free', function(socket, host, port, localAddress) { ..................... if (!socket.destroyed && self.requests[name] && self.requests[name].length) { self.requests[name].shift().onSocket(socket); if (self.requests[name].length === 0) { // don't leak delete self.requests[name]; } } else { ..................... socket.destroy(); } });
  64. 64. Page 65 Pusna-RSの独自実装のKeep Alive Agent  freeがemitされたらSocketをプールするように  その他細かいオプションが取れるようなAgentを実装 self.on('free', function(socket, host, port, localAddress) { self.onFree(socket, host, port, localAddress); }); KeepAliveAgent.prototype.onFree = function(socket, host, port, localAddress) { ....................... var count = lengthOfArray(this.freeSockets, name) + lengthOfArray(this.sockets, name); if (count > this.options.maxSockets) { socket.destroy(); return; } pushToArray(this.freeSockets, name, socket); };
  65. 65. Page 66 http.Agent  当時はhttp.Agentの機能が足りなかったため、独自で 実装していた人も多いのでは。 keep-alive-agentといったnpmパッケージもある  Node 4.xではAgentの実装が変わっている// Node 0.8.x Agent.prototype.addRequest = function(req, host, port, localAddress) { ......................... // Node 4.x Agent.prototype.addRequest = function(req, options) { .........................
  66. 66. Page 67 ケース2: net系のイベントの発火タイミング (Socket.destroy) socket.on('close', function() { console.log('2: onclose'); }); async.waterfall([ function(next) { socket.connect(80, function() { console.log('1: connected'); next(null); }); },function(next) { socket.destroy(); async.nextTick(next); }], function(err, result) { if (err) return; console.log('3: end'); });
  67. 67. Page 68 SocketのIOイベントの発火タイミングが微妙に 変化  Node v0.8.x 1: connected 2: closed 3: end  Node v4.x 1: connected 3: end 2: closed
  68. 68. Page 69 socket.on(‘close’)がemitされるタイミング // Node 4.x this._handle.close(function() { debug('emit close'); self.emit('close', isException); }); ........................... // Node 0.8.x process.nextTick(function() { self.emit('close', exception ? true : false); }); ...........................
  69. 69. Page 70 ネットワーク系  関数のシグネチャが変わるといったわかりやすいもの から、イベント発火のタイミングが微妙に変わると いったわかりにくいものまで多種多様な変更があった  net.Agentなどの内部モジュールを独自で実装してい たケースや、古いネットワーク系のnpmパッケージな どを用いていると移行につまづく  request
  70. 70. Page 71 request  10702のdependentsがある (2015/10/28現在)  v2.54.0以前ではNode4では動かない 2015/03/24  requestに依存していて、Last Updateがここ以前の npmパッケージは少なくともNode4には上がらない
  71. 71. Page 72 Pusna-RSの場合  Elastical elasticsearch client 2013年からメンテナンスされていない  elasticsearchに移行
  72. 72. Page 73 うまくいくもの  メジャーアップデートがあっても動くものもある。  Node Stream 1のコードはNode4.xでも問題なく動く  npm aws-sdk • breaking changesを読む Express • 3.x -> 4.xへのMigrationは楽
  73. 73. Page 74 Pusna-RSをv4.x系に移行  ネットワーク系が大きく変わった ネットワーク系のCoreを独自拡張していた場合や、古い requestなどに依存しているnpmパッケージを使っている場合 は修正が必要  2年前は機能が不足していて、独自実装したモジュール でも現在は拡充されていることが多い Coreに寄せる npmパッケージも

×