HerokuでSocket.IOをやっていく
heroku session-affinityをdisableにし、socket.ioのpollingを切ってwebsocket onlyにしたので、この記事に書いてある情報は全部古くなったshokai.icon (2018/5/20)
shokai.icon @shokai
https://scrapbox.io/shokai
https://gyazo.com/553aa070f8d8fcc794dcfc859200b451
京都の株式会社Helpfeel.iconで働いているが横浜に住んでます
様子 /remote/コミュニケーションを減らそう
これはKyoto.なんか#3の発表資料です
右のStart Presentationからスライドにできる
今日の話
1. こんな構成でやってる
2. メモリリークと再起動
3. dashboardエラーまみれ
第一部 こんな構成でやってる
CosenseというWiKiを作っている
https://gyazo.com/8668866112b6ce2a5537e20d9afdbf4c
同時編集をSocket.IOで実装してる
https://gyazo.com/851cf825696a02355a80f615554c6f7e
HerokuでのSocket.IO運用
2017年3月〜4月ごろに調べた内容を思い出して資料作りました
socket.io 1.x系 + heroku cedar stack
今も同じ設定で動かしてる
発表15分に収まらなそう
飛ばしていく
この資料自体がCosense(WiKi)なので、キーワードリンク先が全部細かい説明のページになっている
より細かい話は資料内のリンク見てください
Socket.IO.iconとは
Engine.IOの上に作られたメッセージングライブラリ
websocketとかhttp pollingとかから適当に選んでなんとかしてくれる
クライアントがemit('eventname', data)したら
サーバーはon('eventname', callback)で受信できる
同様にサーバー→クライアントにも送信できる
Node.jsのプロセスに組み込んで使う
Engine.ioとは
Socket.IOの下にあるやつ
とにかく気合でwebsocketやhttp pollingなどの内から通るものを探す
その上にTCP接続のようなものを作る
後半に出てくるpingTimeoutやpingInterval等の実装はコイツが持っている
今回の話で出てくるSocket.IOの話は実質全てEngine.IOの話
Heroku.iconとは
PaaS
アプリケーションコードを渡したらサーバーを建ててくれる
サーバーはdynoと呼ばれるコンテナ内で起動する
スケール方法
dyno数を増やす
dynoのメモリやCPUを増強
Heroku Router
外からのリクエストをdynoにランダムに割り振ってくれる
websocketが通る
sticky-sessionが使える
ワンオペに向いてる
マネージドなDBやmemcacheやらがボタン押すだけで一瞬で追加できる
Herokuで動かせる設計はdocker imageにして配ったりしやすい
強制的にTwelve-Factor Appになるので、将来別の場所に移動させやすい
接続情報は環境変数に入れる、とか
Node.jsのクラスタ化
socket.io-redis + Heroku Redisの上で使ってる
Redisのpubsubで実装されている
redisのメモリはあまり必要ない
ソース読んでみたけど右から左に流しているだけで、中で一切queueとか作ってないので平坦
https://gyazo.com/ec42dd14407e6fc6b1e4f48d7bcd2531
node.jsプロセス数 * 2 + 1の接続数がRedisに必要
サーバー5台なら11本
足りなくてnodeが起動失敗したりした事があった
sticky-session
あるクライアントからのリクエストを必ず同じサーバーに送るようにするHTTPロードバランサーの機能
cookieを食わせてルーティングする
Heroku Routerで有効にする
$ 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とpolling(comet)
今時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のメモリリークの噂
Socket.IOをずっと動かしているとおかしくなるらしい
いろんな人に言われた
まああるんだろうな(未確認)shokai.icon
結論
Javaでwebsocketサーバーを建てろとのこと
いやまてよ
俺がJava書いたらメモリリークが起こりそうな気がする
というかどんなにがんばってもメモリリークの無いプログラムを書ける自信がない!
アプリケーションはrestartする
そもそもずっと起動していたらメモリリークするといっても
毎日何回もdeployするんだから関係ない
途中で切断・再接続が起こってもどうにかなるように実装する
Scrapboxの開発 - React & Websocketで作るリアルタイムWiki#583538dc97c29100000f55ba
Herokuは毎日restartする
daily restart
24時間 + 216ランダム分毎にrestart
同一app内の全dynoが同時にrestartしないようにするため
Heroku dynoはしょっちゅうrestartする
daily restart
何もしてなくても1日1回restart
deploy
デプロイした時にrestart
user initiation
$ heroku ps:restart
config
$ heroku config:set KEY=VALUE
dynoのシャットダウン処理
1. dynoに向けてSIGTERMが送信され、新しいHTTPリクエストはルーティングされなくなる
2. プロセスが終了しない場合、何度もSIGTERMは送られる
3. 1の30秒後にSIGKILLが送信される
H12 - Request timeout
30秒以内に完了しないリクエストへのHeroku Error Code
これが出ないように実装しておかないと、リクエスト処理中に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は何度も送られてくる
後始末を多重にやらないようにlodash.onceを使う
RailsもSignal.trap("TERM")しておかないとHerokuでは即落ちする
なおrestartしたdynoは新しいのにすぐ繋ぎ変えしてくれる
自分でgraceful shutdownを全て書くより楽
まとめ:とにかく30秒以内に全ての処理を終わらせろ
第三部 dashboardエラーまみれ
Socket.IOをデフォルト値で運用したHeroku dashboard
https://gyazo.com/e7738cde56fd3956e715f00098857335
すごく赤い
だいたいpolling(comet)のせいです
Logentriesのエラー通知が酷い事になる
polling使っててもまともに見れるdashboardにしたい!
https://gyazo.com/c15216706f6b673e46134aef0946d98e
なんとかなった
Socket.IOのpingTimeoutとpingInterval
clientはpingInterval毎にpingを送信する
serverはpingを受けるとpongをすぐ返す
同時に、setTimeout(onClose, pingInterval + pingTimeout)して切断を検知しようとする
次のpingを受けるとclearTimeoutし、またsetTimeoutする
https://github.com/socketio/engine.io/blob/1.8.4/lib/socket.js#L134
ドキュメントと実装が違う
Socket.IO — Server API
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に怒られる原因
Heroku Error Code
H番号で表記される
これを守っているとdaily/deploy restartに耐えられるアプリになるぞという指針だと思うshokai.icon
HTTPに対してだけ発行される
websocketは対象外
Socket.IOでよく見るHeroku Error Code
H12 - Request timeout
発生条件
appがheroku routerからHTTPリクエストを受けて30秒以内にレスポンスを返さなかった時
Socket.IOをデフォルト設定で使っている場合、transport=pollingのclientがブラウザウィンドウを閉じた後に確実に発生する
serverが切断検知に85秒かけて、その間cometを返さない為
H13 - Connection closed without response
発生条件
heroku routerからappにTCP socketが接続されたが、何もレスポンスを返さない時
認証情報が無いsocket.io接続を無言で切断してたら大量発生してた
何か返事するようにしたら解決した
code:server.js
socket.on('disconnect', () => socket.emit('bye'))
H15 - Idle connection
発生条件
接続が55秒以上維持されているが、何もデータをやりとりしていない時
Socket.IOを接続しっぱなしで何も通信していないと発生し、切断される
client側から切断された事にserverが気づけず、cometを握りっぱなしにしてると発生する
H27 - Client Request Interrupted
発生条件
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
// 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だな
きれいになった
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のソースコードを見れ