HerokuでSocket.IOをやっていく
@shokai
京都の
で働いているが横浜に住んでます
右のStart Presentationからスライドにできる
今日の話
1. こんな構成でやってる
2. メモリリークと再起動
3. dashboardエラーまみれ
第一部 こんな構成でやってる
2017年3月〜4月ごろに調べた内容を思い出して資料作りました
socket.io 1.x系 + heroku cedar stack
今も同じ設定で動かしてる
発表15分に収まらなそう
飛ばしていく
この資料自体が
Cosense(WiKi)なので、キーワードリンク先が全部細かい説明のページになっている
より細かい話は資料内のリンク見てください
とは
クライアントが emit('eventname', data)
したら
サーバーは on('eventname', callback)
で受信できる
同様にサーバー→クライアントにも送信できる
Socket.IOの下にあるやつ
とにかく気合でwebsocketやhttp pollingなどの内から通るものを探す
その上にTCP接続のようなものを作る
後半に出てくる pingTimeout
や pingInterval
等の実装はコイツが持っている
今回の話で出てくるSocket.IOの話は実質全てEngine.IOの話
とは
アプリケーションコードを渡したらサーバーを建ててくれる
スケール方法
dyno数を増やす
dynoのメモリやCPUを増強
外からのリクエストをdynoにランダムに割り振ってくれる
ワンオペに向いてる
マネージドなDBやmemcacheやらがボタン押すだけで一瞬で追加できる
Herokuで動かせる設計はdocker imageにして配ったりしやすい
接続情報は環境変数に入れる、とか
Node.jsのクラスタ化
redisのメモリはあまり必要ない
ソース読んでみたけど右から左に流しているだけで、中で一切queueとか作ってないので平坦
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をずっと動かしているとおかしくなるらしい
いろんな人に言われた
まああるんだろうな(未確認)
結論
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走ってヤバイ
このエラーは後でまた出てくる重要なやつなので、覚えておいてください
SIGTERM
node.jsプロセスはSIGTERMを受けると即落ちする
graceful-shutdown.jsimport {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
すごく赤い
polling使っててもまともに見れるdashboardにしたい!
なんとかなった
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に耐えられるアプリになるぞという指針だと思う
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接続を無言で切断してたら大量発生してた
何か返事するようにしたら解決した
server.jssocket.on('disconnect', () => socket.emit('bye'))
発生条件
接続が55秒以上維持されているが、何もデータをやりとりしていない時
client側から切断された事にserverが気づけず、cometを握りっぱなしにしてると発生する
発生条件
appがレスポンスを返し、それをheroku routerがclientに返したが、clientのsocketが既に閉じていた時
polling(comet)使っている限り避けられない
無視する
感想
85秒はいくらなんでも長い
最終的にこんな感じの設定になった
server.jsconst options = {
cookie: false,
serveClient: false,
pingTimeout: 15000, // default: 60000
pingInterval: 13000 // default: 25000
// transports: ['polling']
}
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
だな
きれいになった
Incident
はHerokuのインフラで何かがあったらしい
毎日何かあるけど、影響感じたこと無いのがすごい
H27
はclient側がresponseを待たずに切断したという現象なので気にしない
青い点はdeployによるrestart
その下に H13
がある
SIGTERM受けたら行うシャットダウン処理で、 emit('bye')
しながらserver側から切断していったらこれも消せるかも?
pollingを受け入れる事で、socket.ioから発火したサーバー側の処理も30秒以内に終わるかどうか確かめれるようになったとも言える
おわり
Heroku Routerのエラーの原因はだいたい
Engine.IOの方なので、Engine.IOのソースコードを見れ