Node.js v0.8からv4.xへのバージョンアップ
~ 大規模Push通知基盤の運用事例 ~
株式会社リクルートテクノロジーズ APソリューショングループ
伊藤 瑛
Page 2
自己紹介
■名前
伊藤 瑛
■所属
リクルートテクノロジーズ アプリケーションソリューションG
2015年度新卒入社 / Node歴 3ヶ月
■やっていること
Node製の大規模Push基盤Pusna-RSの運用開発
Page 3
アジェンダ
1. Pusna-RSについて
2. Node.js v4.xへのアップデートについて
1. Pusna-RSのStream
2. アップデートの途中経過
3. まとめ
Page 4
Pusna-RSとは
 Push Notification Aggregator Realtime & Scalable
モバイルアプリのためのPush通知基盤
 Node.jsを用いて高いリアルタイム性
AWSの各種機能を用いたスケーラビリティ
双方の確保
 リクルートグループの主要なスマホアプリで
用いられている
 運用開始から2年経ち、億のデバイスを扱う規模に
なっても無停止で安定稼動中!
Page 5
システム概要
開発期間
• 2013年9月~2013年12月
サービス概要
• 2013年12月末 (約2年間稼
働)
• 億を超えるデバイスにPush
通知を配信
• 100を超えるアプリで多様な
使われ方をしている。
PusnaRS
プラットフォーム
• Node.js v0.8.24
フレームワーク
• Express 3
• Rendr
システム構成概要
Page 6
Node.js v0.8からv4.xへ
Page 7
バージョンアップの検討
セキュリティリスクの排除
新技術への追従
Node.jsとIo.jsの関係性が落ち着いた
システム上大きな問題は起きていないにもかかわらず、
いまこのタイミングでやる理由
Page 8
セキュリティリスク
 今年に入ってNode.js / Io.jsに2件の脆弱性
CVE-2015-5380
V8起因のバグ。DoSの危険性
CVE-2015-7384
Dosの可能性
旧バージョンに脆弱性が見つかった場合対応できない
Page 9
新技術への追従
 新規格への対応
APNsがiOS9からHTTP/2対応に。将来的にHTTP/2対応必須
になるかもしれない
 ES2015などの新技術の開拓と知見の集積
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ヶ月サポートされる
今が移行に最適なタイミング!
Page 11
今日の主旨
バージョンアップのなかで
見えてきたものを
共有したいと思います!
Page 12
全体構成
デバイス管理
プッシュ配信管理
APNs
GCM
スマホ リクルート
DynamoDB elasticsearch
登録API 登録WorkerSQS
配信worker
操作用Web UI
管理API
SQS
配信担当者
データ基盤
事業サーバ
Page 13
全体構成
デバイス管理
プッシュ配信管理
APNs
GCM
スマホ リクルート
DynamoDB elasticsearch
登録API 登録WorkerSQS
配信worker
操作用Web UI
管理API
SQS
配信担当者
データ基盤
事業サーバ
Page 14
全体構成
デバイス管理
プッシュ配信管理
APNs
GCM
スマホ リクルート
DynamoDB elasticsearch
登録API 登録WorkerSQS
配信worker
操作用Web UI
管理API
SQS
配信担当者
データ基盤
事業サーバ
Page 15
全体構成
デバイス管理
プッシュ配信管理
APNs
GCM
スマホ リクルート
DynamoDB elasticsearch
登録API 登録WorkerSQS
配信worker
操作用Web UI
管理API
SQS
配信担当者
データ基盤
事業サーバ
Node Application
Page 16
どう移行させるか
正常動作させる
• Node.js v4.xへアップグ
レードすることにより
動かなくなるコードの修
正
• Node Core API
• npmパッケージのアッ
プグレード
新技術の取り入れ
• 古い実装を新しい実装
に書き換える
• Stream3
• ES2015
• npmパッケージのリプ
レース
• etc...
Page 17
どう移行させるか
正常動作させる
• Node.js v4.xへアップグ
レードすることにより、
動かなくなるコードの修
正
• Node Core API
• npmパッケージのアッ
プグレード
新技術の取り入れ
• 古い実装を新しい実装
に書き換える
• Stream3
• ES2015
• npmパッケージのリプ
レース
• etc...
Page 18
どう移行させるか
正常動作させる
• Node.js v4.xへアップグ
レードすることにより、
動かなくなるコードの修
正
• Node Core API
• npmパッケージのアッ
プグレード
新技術の取り入れ
• 古い実装を新しい実装
に書き換える
• Stream3
• ES2015
• npmパッケージのリプ
レース
• etc...
Pusna-RSにおけるStreamの移行の話をします!
Page 19
Streamが使われている部分
デバイス管理
プッシュ配信管理
APNs
GCM
スマホ リクルート
DynamoDB elasticsearch
登録API 登録WorkerSQS
配信worker
操作用Web UI
管理API
SQS
配信担当者
データ基盤
事業サーバ
Push配信の実行workerでStreamを活用
Page 20
Stream API
 データの”流れ”を綺麗に扱うためのAPI
 データを一括で読み込むのではなく、破片ごとに読み
処理することができる
 各Streamをpipe()で連結することができる
Readable
• I/Oなどからの
読み込み
Readable /
Writable
(Transform)
• データの整形
Writable
• I/Oなどへの書
き出し
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
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したい
Page 23
StreamによるPush通知配信処理の実装
SQS
配信準備ストリーム
配信タイプ・デバ
イスを判断
配信準備ストリームと配信実行ストリームにより構成
 準備ストリーム
SQSからのメッセージを受信して、配信タイプ・対象デバイス
ごとに適切なストリームをインスタンス化し、配信実行スト
リームを組み立てる
 実行ストリーム
実際の配信処理を行うストリーム
実行ストリーム
Android:
全デバイス
実行ストリーム
iOS:
指定デバイス
実行ストリーム
iOS:
個別デバイス
Page 24
配信準備ストリームの構成
配信処理を組み立てるためのストリーム
SQS
Readable Stream
pollingしてメッ
セージを受信
dataイベントを
emit
Execlusive Executor
pipe()
pipe()
Readable/Writable
二重配信を防
ぐためDynamo
のステータスを
更新
Writable
配信タイプ・対
象デバイスご
とにストリーム
を組み立て、
実行
データ受信
ステータス更新
配信処理組み立て
実際の処理へ
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段階の処理の流れ
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
からデータ抽出
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段階の処理の流れ
データ整形
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段階の処理の流れ
配信依頼
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段階の処理の流れ
結果を保存
未達デバイス
の削除
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全端末配信の場合
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指定端末の配信
Page 32
配信処理実装上のポイント
Stream1 Pusna-RSでの工夫 Stream3
データの
取りこぼし
バッファリングを
独自実装
(Stream2と
同等の機能)
Bufferingを
標準実装
Streamの作り方
が煩雑
through
(by @dominictarr)
使いやすい
標準API
Page 33
配信処理実装上のポイント
Stream1 Pusna-RSでの工夫 Stream3
データの
取りこぼし
バッファリングを
独自実装
(Stream2と
同等の機能)
Bufferingを
標準実装
Streamの作り方
が煩雑
through
(by @dominictarr)
使いやすい
標準API
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;
};
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);
};
Page 36
配信処理実装上のポイント
Stream1 Pusna-RSでの工夫 Stream3
データの
取りこぼし
バッファリングを
独自実装
(Stream2と
同等の機能)
Bufferingを
標準実装
Streamの作り方
が煩雑
through
(by @dominictarr)
使いやすい
標準API
Page 37
Stream1では
 throughを使ってシンプルにかける
var stream = through(function (data) {
// データを加工
...............
this.push(data);
});
Page 38
Stream3では
 標準APIでthroughとほぼ同じ
npmの依存を減らせる!
const Transform = require(‘stream’).Transform;
new Transform {
transform: function(chunk, enc, cb) {
// ロジックを実装
}
})).....
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) {
}
}
Page 40
Pusna-RSのStream移行のまとめ
Stream1の時代から
内部バッファを実装
throughなどの
パッケージの活用
標準実装へ
Page 41
Node.js v4.x移行
進捗状況
Page 42
Node.js v4.x対応進捗
デバイス管理
プッシュ配信
APNs
GCM
スマホ リクルート
DynamoDB elasticsearch
登録API 登録WorkerSQS
配信worker
操作用Web UI
管理API
SQS
配信担当者
データ基盤
事業サーバ
Page 43
Node.js v4.x対応進捗
デバイス管理
プッシュ配信
APNs
GCM
スマホ リクルート
DynamoDB elasticsearch
登録API 登録WorkerSQS
配信worker
操作用Web UI
管理API
SQS
配信担当者
データ基盤
事業サーバ
Page 44
Node.js v4.xへの移行状況
 各アプリケーションをv4.x系に移行中!
 細かいCore APIの変更やnpmパッケージの問題などは
あったが、正常動作させるのは難しくない
 移行が終わったものは順次デプロイし動作試験中!
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
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
Page 47
Node.js v4.xの方が遅い...?
 他のベンチマークを見るとv4.xの方が速い
 ソースコードの修正点は特になし
 エラーログなども出ていない
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);
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
どのバージョンもスループットにあまり差はなし
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に揃えて再評価
Page 51
aws-sdkを揃えた結果
 デバイス登録シナリオで再実験
0
50
100
150
200
250
1 2
197.8 q/sec 197.3 q/secq/sec
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
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以上で有効になる機能
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
Page 55
今日のまとめ
 Pusna-RSはNode.js製の大規模Push通知基盤
運用開始から2年、扱うデバイスが億を超えても
安定稼動中!
 Pusna-RSのNode.jsをv0.8からv4.xにあげる
作業を実施中。
Streamを1から3にあげる
 npmとの依存もあるがそのままのコードだとむしろ
v0.8よりもパフォーマンスが下がることがある。
Page 56
おわりに
Page 57
リクルートテクノロジーズでは
”最新”のNodeを使って一緒に
仕事をする仲間を探していま
す!
Page 58
Page 59
ありがとうございました!
Page 60
Page 62
ちょっとしたおまけ
Page 63
Node 0.8からNode 4のAPIの変更
 細かい変更点はwikiのBreaking API Changesにのっ
てる
 実際に遭遇したケースを紹介
 net / tls / http(s)などのネットワーク系のモジュール
で大きな変更点があったという印象
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();
}
});
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);
};
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) {
.........................
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');
});
Page 68
SocketのIOイベントの発火タイミングが微妙に
変化
 Node v0.8.x
1: connected
2: closed
3: end
 Node v4.x
1: connected
3: end
2: closed
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);
});
...........................
Page 70
ネットワーク系
 関数のシグネチャが変わるといったわかりやすいもの
から、イベント発火のタイミングが微妙に変わると
いったわかりにくいものまで多種多様な変更があった
 net.Agentなどの内部モジュールを独自で実装してい
たケースや、古いネットワーク系のnpmパッケージな
どを用いていると移行につまづく
 request
Page 71
request
 10702のdependentsがある (2015/10/28現在)
 v2.54.0以前ではNode4では動かない
2015/03/24
 requestに依存していて、Last Updateがここ以前の
npmパッケージは少なくともNode4には上がらない
Page 72
Pusna-RSの場合
 Elastical
elasticsearch client
2013年からメンテナンスされていない
 elasticsearchに移行
Page 73
うまくいくもの
 メジャーアップデートがあっても動くものもある。
 Node
Stream 1のコードはNode4.xでも問題なく動く
 npm
aws-sdk
• breaking changesを読む
Express
• 3.x -> 4.xへのMigrationは楽
Page 74
Pusna-RSをv4.x系に移行
 ネットワーク系が大きく変わった
ネットワーク系のCoreを独自拡張していた場合や、古い
requestなどに依存しているnpmパッケージを使っている場合
は修正が必要
 2年前は機能が不足していて、独自実装したモジュール
でも現在は拡充されていることが多い
Coreに寄せる
npmパッケージも

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