HerokuでSocket.IOをやっていく
shokai.icon @shokai
https://gyazo.com/553aa070f8d8fcc794dcfc859200b451
京都の株式会社Helpfeel.iconで働いているが横浜に住んでます
右のStart Presentationからスライドにできる
今日の話
1. こんな構成でやってる
2. メモリリークと再起動
3. dashboardエラーまみれ
第一部 こんな構成でやってる
https://gyazo.com/8668866112b6ce2a5537e20d9afdbf4c
https://gyazo.com/851cf825696a02355a80f615554c6f7e
2017年3月〜4月ごろに調べた内容を思い出して資料作りました
socket.io 1.x系 + heroku cedar stack
今も同じ設定で動かしてる
発表15分に収まらなそう
飛ばしていく
この資料自体がCosense(WiKi)なので、キーワードリンク先が全部細かい説明のページになっている より細かい話は資料内のリンク見てください
Socket.IO.iconとは
クライアントがemit('eventname', data)したら
サーバーはon('eventname', callback)で受信できる
同様にサーバー→クライアントにも送信できる
Socket.IOの下にあるやつ
とにかく気合でwebsocketやhttp pollingなどの内から通るものを探す
その上にTCP接続のようなものを作る
後半に出てくるpingTimeoutやpingInterval等の実装はコイツが持っている
今回の話で出てくるSocket.IOの話は実質全てEngine.IOの話
Heroku.iconとは
アプリケーションコードを渡したらサーバーを建ててくれる
スケール方法
dyno数を増やす
dynoのメモリやCPUを増強
外からのリクエストをdynoにランダムに割り振ってくれる
ワンオペに向いてる
マネージドなDBやmemcacheやらがボタン押すだけで一瞬で追加できる
Herokuで動かせる設計はdocker imageにして配ったりしやすい
接続情報は環境変数に入れる、とか
Node.jsのクラスタ化
redisのメモリはあまり必要ない
ソース読んでみたけど右から左に流しているだけで、中で一切queueとか作ってないので平坦
https://gyazo.com/ec42dd14407e6fc6b1e4f48d7bcd2531
node.jsプロセス数 * 2 + 1の接続数がRedisに必要
サーバー5台なら11本
足りなくてnodeが起動失敗したりした事があった
cookieを食わせてルーティングする
$ heroku features:enable http-session-affinity
こういうhttp headerを返すようになる
Set-Cookie: heroku-session-affinity=(長いHASH); Version=1; Expires=Tue, 15-Nov-2016 11:51:24 GMT; Max-Age=86400; Domain=staging.scrapbox.io; Path=/; HttpOnly
expireは24時間後
routerはheroku-session-affinityの値を見て、毎回同じdynoにルーティングする
sticky-sessionが無い場合
socket.ioは接続時の最初の数回はhttp pollingでhandshakeしようとする HerokuRouterがリクエスト毎に別のdynoにルーティングする
同じdynoにリクエストが行かず、handshake失敗してしまう
なおwebsocketしか通さないならsticky-sessionは不要です
今時websocket通らない環境の人いるの?
けっこういる
SFCっていう大学とか
ずっとwebsoket接続維持してるとpollingに落とされる人がいる気がする
ちゃんと調べてないけど
夜中〜明け方に増えてるような感じする
polling接続はHTTP2本使う
client→serverはHTTP-POST
server→client用に、client側からcometをかける
まとめ
後ろにRedisを置いたnodejsプロセスでsocket.ioクラスタを組む
HeorkuRouterのsticky-sessionでルーティング
polling(comet)必要
Socket.IOをずっと動かしているとおかしくなるらしい
いろんな人に言われた
まああるんだろうな(未確認)shokai.icon
結論
Javaでwebsocketサーバーを建てろとのこと
いやまてよ
俺がJava書いたらメモリリークが起こりそうな気がする
というかどんなにがんばってもメモリリークの無いプログラムを書ける自信がない!
アプリケーションはrestartする
そもそもずっと起動していたらメモリリークするといっても
毎日何回もdeployするんだから関係ない
途中で切断・再接続が起こってもどうにかなるように実装する
Herokuは毎日restartする
24時間 + 216ランダム分毎にrestart
同一app内の全dynoが同時にrestartしないようにするため
daily restart
何もしてなくても1日1回restart
deploy
デプロイした時にrestart
user initiation
$ heroku ps:restart
config
$ heroku config:set KEY=VALUE
1. dynoに向けてSIGTERMが送信され、新しいHTTPリクエストはルーティングされなくなる 2. プロセスが終了しない場合、何度もSIGTERMは送られる
3. 1の30秒後にSIGKILLが送信される
これが出ないように実装しておかないと、リクエスト処理中にdyno restart走ってヤバイ
このエラーは後でまた出てくる重要なやつなので、覚えておいてくださいshokai.icon
SIGTERM
node.jsプロセスはSIGTERMを受けると即落ちする
code:graceful-shutdown.js
import {once} from 'lodash'
const startShutdown = once(function () {
// ここで後始末処理してから
process.exit(0)
})
process.on('SIGTERM', startShutdown)
SIGTERMは何度も送られてくる
RailsもSignal.trap("TERM")しておかないとHerokuでは即落ちする
なおrestartしたdynoは新しいのにすぐ繋ぎ変えしてくれる
自分でgraceful shutdownを全て書くより楽
まとめ:とにかく30秒以内に全ての処理を終わらせろ
第三部 dashboardエラーまみれ
Socket.IOをデフォルト値で運用したHeroku dashboard
https://gyazo.com/e7738cde56fd3956e715f00098857335
すごく赤い
polling使っててもまともに見れるdashboardにしたい!
https://gyazo.com/c15216706f6b673e46134aef0946d98e
なんとかなった
Socket.IOのpingTimeoutとpingInterval
clientはpingInterval毎にpingを送信する
serverはpingを受けるとpongをすぐ返す
同時に、setTimeout(onClose, pingInterval + pingTimeout)して切断を検知しようとする
次のpingを受けるとclearTimeoutし、またsetTimeoutする
ドキュメントと実装が違う
pingTimeout (Number): how many ms without a pong packet to consider the connection closed (60000)
pingInterval (Number): how many ms before sending a new ping packet (25000).
実際は、サーバーは85秒後にdisconnectを検知する
これがHeorku Routerに怒られる原因
H番号で表記される
これを守っているとdaily/deploy restartに耐えられるアプリになるぞという指針だと思うshokai.icon
HTTPに対してだけ発行される
websocketは対象外
Socket.IOでよく見るHeroku Error Code
発生条件
appがheroku routerからHTTPリクエストを受けて30秒以内にレスポンスを返さなかった時
Socket.IOをデフォルト設定で使っている場合、transport=pollingのclientがブラウザウィンドウを閉じた後に確実に発生する
serverが切断検知に85秒かけて、その間cometを返さない為
発生条件
heroku routerからappにTCP socketが接続されたが、何もレスポンスを返さない時
認証情報が無いsocket.io接続を無言で切断してたら大量発生してた
何か返事するようにしたら解決した
code:server.js
socket.on('disconnect', () => socket.emit('bye'))
発生条件
接続が55秒以上維持されているが、何もデータをやりとりしていない時
client側から切断された事にserverが気づけず、cometを握りっぱなしにしてると発生する
発生条件
appがレスポンスを返し、それをheroku routerがclientに返したが、clientのsocketが既に閉じていた時
polling(comet)使っている限り避けられない
無視する
感想
85秒はいくらなんでも長い
最終的にこんな感じの設定になった
code:server.js
const options = {
cookie: false,
serveClient: false,
pingTimeout: 15000, // default: 60000
pingInterval: 13000 // default: 25000
}
const io = SocketIO(server, options)
client側から切断された場合、serverは30秒以内にそれを知りたい
pingTimeout + pingIntervalが切断検知にかかる
とりあえず15+13 = 28秒ぐらいにしておくか
client側のネットワークが不調かもしれない
1回pingがserverに届かなくても、2回目のpingが届けば切断が起きないようにする
pingInterval * 2 < pingTimeout + pingIntervalであればいい
13 * 2 < 13 + 15だな
きれいになった
https://gyazo.com/f8ccc75a5c6735a4fbd20570969cf06c
IncidentはHerokuのインフラで何かがあったらしい
毎日何かあるけど、影響感じたこと無いのがすごい
H27はclient側がresponseを待たずに切断したという現象なので気にしない
青い点はdeployによるrestart
その下にH13がある
SIGTERM受けたら行うシャットダウン処理で、emit('bye')しながらserver側から切断していったらこれも消せるかも?
pollingを受け入れる事で、socket.ioから発火したサーバー側の処理も30秒以内に終わるかどうか確かめれるようになったとも言える
おわり
Heroku Routerのエラーの原因はだいたいEngine.IOの方なので、Engine.IOのソースコードを見れ