generated at
HerokuでSocket.IOをやっていく

heroku session-affinityをdisableにし、socket.ioのpollingを切ってwebsocket onlyにしたので、この記事に書いてある情報は全部古くなったshokai (2018/5/20)

shokai @shokai

京都の株式会社Helpfeelで働いているが横浜に住んでます

右のStart Presentationからスライドにできる

今日の話
1. こんな構成でやってる
2. メモリリークと再起動
3. dashboardエラーまみれ


第一部 こんな構成でやってる

CosenseというWiKiを作っている

同時編集をSocket.IOで実装してる


HerokuでのSocket.IO運用
2017年3月〜4月ごろに調べた内容を思い出して資料作りました
socket.io 1.x系 + heroku cedar stack
今も同じ設定で動かしてる

発表15分に収まらなそう
飛ばしていく
この資料自体がCosense(WiKi)なので、キーワードリンク先が全部細かい説明のページになっている
より細かい話は資料内のリンク見てください

Socket.IOとは
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とは
アプリケーションコードを渡したらサーバーを建ててくれる
サーバーはdynoと呼ばれるコンテナ内で起動する
スケール方法
dyno数を増やす
dynoのメモリやCPUを増強
外からのリクエストをdynoにランダムに割り振ってくれる
websocketが通る
sticky-sessionが使える

ワンオペに向いてる
マネージドなDBやmemcacheやらがボタン押すだけで一瞬で追加できる
Herokuで動かせる設計はdocker imageにして配ったりしやすい
強制的にTwelve-Factor Appになるので、将来別の場所に移動させやすい
接続情報は環境変数に入れる、とか

Node.jsのクラスタ化
socket.io-redis + Heroku Redisの上で使ってる
Redisのpubsubで実装されている
redisのメモリはあまり必要ない
ソース読んでみたけど右から左に流しているだけで、中で一切queueとか作ってないので平坦

node.jsプロセス数 * 2 + 1 の接続数がRedisに必要
サーバー5台なら11本
足りなくてnodeが起動失敗したりした事があった

あるクライアントからのリクエストを必ず同じサーバーに送るようにする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
結論
Javaでwebsocketサーバーを建てろとのこと

いやまてよ
俺がJava書いたらメモリリークが起こりそうな気がする
というかどんなにがんばってもメモリリークの無いプログラムを書ける自信がない!

アプリケーションはrestartする
そもそもずっと起動していたらメモリリークするといっても
毎日何回もdeployするんだから関係ない
途中で切断・再接続が起こってもどうにかなるように実装する

Herokuは毎日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

1. dynoに向けてSIGTERMが送信され、新しいHTTPリクエストはルーティングされなくなる
2. プロセスが終了しない場合、何度もSIGTERMは送られる
3. 1の30秒後にSIGKILLが送信される

30秒以内に完了しないリクエストへのHeroku Error Code
これが出ないように実装しておかないと、リクエスト処理中にdyno restart走ってヤバイ
このエラーは後でまた出てくる重要なやつなので、覚えておいてくださいshokai

SIGTERM
node.jsプロセスはSIGTERMを受けると即落ちする
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
すごく赤い
だいたいpolling(comet)のせいです
Logentriesのエラー通知が酷い事になる
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に耐えられるアプリになるぞという指針だと思うshokai
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.js
socket.on('disconnect', () => socket.emit('bye'))
発生条件
接続が55秒以上維持されているが、何もデータをやりとりしていない時
Socket.IOを接続しっぱなしで何も通信していないと発生し、切断される
client側から切断された事にserverが気づけず、cometを握りっぱなしにしてると発生する
発生条件
appがレスポンスを返し、それをheroku routerがclientに返したが、clientのsocketが既に閉じていた時
polling(comet)使っている限り避けられない
無視する

感想
85秒はいくらなんでも長い


最終的にこんな感じの設定になった
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 だな

きれいになった
Incident はHerokuのインフラで何かがあったらしい
毎日何かあるけど、影響感じたこと無いのがすごい
H27 はclient側がresponseを待たずに切断したという現象なので気にしない
青い点はdeployによるrestart
その下に H13 がある
SIGTERM受けたら行うシャットダウン処理で、 emit('bye') しながらserver側から切断していったらこれも消せるかも?

pollingを受け入れる事で、socket.ioから発火したサーバー側の処理も30秒以内に終わるかどうか確かめれるようになったとも言える

おわり
Heroku Routerのエラーの原因はだいたいEngine.IOの方なので、Engine.IOのソースコードを見れ