Gunfish
Push Provider Server with HTTP/2
Push Provider Server with HTTP/2
Yokohama\.(pm6?|go) #14
Takuya Yoshimura (takyoshi)
Takuya Yoshimura (takyoshi)
About me
- 吉村卓也 (Takuya Yoshimura)
- @takyoshi (moulin) (github)
- KAYAC Inc.
- Lobi, サーバーサイド
Lobi - Chat & Game Community
同じアカウントでログイン
APNS
おっ、届いた
1. 通知が飛ぶイベント
4. デバイストークンに対応する端末に通知を送信
2. Bさんのアカウントに紐づく
全端末のデバイストークンを取得
3. Bさんアカウントに紐づくすべての
デバイストークンに対して通知を送信
Aさん
Bさん
LobiのiOSへのPush Notification
LobiのiOSへのPush Notification
- Appleが提供するAPNs(後述)を利用したPush Notification
- APNs Provider APIを利用する(レガシー版)
- アカウントに紐づくすべての端末に同時に通知が届く
Push Notification
同じアカウントでログイン
APNS
おっ、届いた
1. Bさんに通知が飛ぶイベント
4. デバイストークンに対応する端末に通知を送信
2. Bさんのアカウントに紐づく
全端末のデバイストークンを取得
3. Bさんアカウントに紐づくすべての
デバイストークンに対して通知を送信
Aさん
Bさん
APNsについて
- Apple Push Notification serviceの略
- iOS端末にPush Notificationを届けるためのサービス
- 開発者側はAPNs Provider APIを用いてAPNsを利用できる
APNsについて
-
APNsとの通信をバックグラウンドに回す必要がある
- APNsからのレスポンスに時間が掛かるため
- 参考:APNsとの通信は0.15 sec程かかる
-
APNsとのTCPコネクションをプールする必要がある
- 短いスパンで接続と切断を繰り返すとDoS攻撃とみなされる
- TCPコネクションを作りすぎてリソース等圧迫させたくない
-
無効なトークンを消込む必要がある
- 無効なトークンより後ろのリクエストは破棄されてしまうため
- APNsからEOFが送られて切断されることがあり送信漏れが発生するため
-
2つの消込処理が必要
- 「エラーレスポンス」の即時消込
- 「フィードバックサービス」の定期消込
LobiのProviderの実装
- AnyEvent::APNSを用いてPush Notificationを実装(Perl製)
- TCP接続を持ち回しする
- JSONの配列をHTTPのPOSTで受け取る
- エラーレスポンス時に対象となる無効なトークンを即時削除
- フィードバックでの削除はProviderにはつけない
- 無効なトークンに巻き込まれて届かなかったデータを再送
- 送信した履歴を持っておく
- エラーレスポンスのリクエストよりも後のものを再送
レガシーAPNsの悩み
- 独自のプロトコルが厳しい
- 無効なトークンによる巻き込み事故
- 再送処理が連続で発生すると一部届かなくなる
- 無効なトークンの条件が完全に絞り込めない
- すべての無効なトークンを消し込むことは難しい
- 無効なトークンが蓄積されてしまい無駄に通信してしまう
- 無効なトークンが原因でAPNsから切断される場合がある
New APNs
New APNs
- HTTP/2で通信する
- フィードバックサービスが不要になる
- APNsとの認証処理が簡単になる
- ペイロードの情報量が4096バイトに拡張される
WWDC-2015 (8/26) 「What's New in Notifications」で発表
HTTP/2の通信
- 無効なトークンを送っても巻き込み事故が発生しない
- HTTP/2の多重送信の特性によるもの
- パフォーマンス面も向上する
- 多重送信できるため1つのHTTPクライアントを有効利用できる
フィードバックサービスが不要になる
- New APNsはエラーの種類が増えている
- フィードバックサービスに該当するエラーもレスポンスとして返ってくる
- 即時消込ができるため無効なトークンをすぐ削除できる
- 定期削除が不要になる
ちょうどその頃…
Go 1.6からhttp/2がはいる
Go 1.6が2月に出るらしい...
Gunfishへ
Gunfish
- Push Notificationを漏れなく送ることが目的
- Go1.6で作られたPush Notification Providerサーバ
- 現在New APNs Provider APIのみ対応
- HTTPでPOSTするだけでPush通知が届く仕組み
- 旧バージョンとの互換性をもたせる
- エラーレスポンス時のhookとcustom error handeler
- 無効なトークン削除用の機能
- graceful restart
- パフォーマンスチューニングが可能
GunfishのArchitechture
server
supervisor
worker
sender
APNS
Lobi
Gunfish
: chan
: goroutine
: write
: read
①
②
③
④
⑤
⑥
⑦
WorkerとSender
Worker
- 1worker, 1 http/2 client
- SenderからAPNsのレスポンスを非同期で受け取る
Sender
- 1 worker, N sender
- APNsにリクエストする
- WorkerにAPNsからのレスポンスを渡す
- Sender数はhttp/2 clientの最大多重送信数になる
- memo: Sender数100以上になるとhttp/2 clientでエラー
Gunfishの起動
$ gunfish -c /etc/gunfish/config.toml
config.tomlの例
[provider]
port = 38003
worker_num = 4 # http/2クライアント接続を持つworkerの数
queue_size = 2000 # 最大同時リクエスト数
max_request_size = 1000 # POSTされるJSON配列の最大長
max_connections = 1000 # Gunfishが受け付ける最大コネクション数
[apns]
cert_file = "/path/to/cert_file.pem"
key_file = "/path/to/key_file.pem"
request_per_sec = 2000 # 1秒間にpushが送信される流量を設定
sender_num = 30 # http/2クライアントの送信の多重度
error_hook = "your_hook_cmd.sh" # errorレスポンス時のhook
Gunfishを使ったPush Notification
$ curl -X POST -H "Content-type: application/json" \
-d '[{"token":"83fa0eb8a743c118fca80b0136bfee0", \
"payload": {"aps": {"alert": "hoge", "sound": "default", "badge": 1}}}]' \
http://localhost:38103/apns/push
{"result": "ok"}
// POSTするjson配列の構造
[
{
"token": "apnsから発行されるデバイストークン",
"payload": "APNsが指定するPayload"
}
]
// Payloadの構造
{
"aps": {
"alert": {
"title": "foo",
"body": "bar"
},
"sound": "default",
"badge": 1
},
"option1": "任意のプロパティ"
}
Custom Error Response Handler
// ResponseHandlerインターフェース
type ResponseHandler interface {
OnResponse(*Request, *Response, error)
HookCmd() string
}
- Errorレスポンス時の処理をgo言語レベルで定義可能
- ResponseHandlerインタフェースを実装してsetする
InitErrorResponseHandler(YourCustomErrorHandler{})
Graceful Restart
- queueに入っているデータが渡りきるまで落とさない
- queueの長さが0の状態が一定時間続いたとき終了
- 強制終了タイムアウトの設定(2分)
- Server::Starterに対応している
$ start_server --port 38003 --interval 5 -- gunfish -c /etc/gunfish/config.toml
- Server::Starter対応の詳細
Go言語でGraceful Restartをする
http://shogo82148.github.io/blog/2015/05/03/golang-graceful-restart/
Go言語でGraceful Restartをするときに取りこぼしを少なくする
http://shogo82148.github.io/blog/2015/11/23/golang-graceful-restart-2nd/
パフォーマンスチューニング
問題設定
- ピークタイム時の5000(notification/sec)を捌きたい
設定項目
- request_per_sec = 5000
- worker数
- sender数
パフォーマンスチューニング
- h2oのAPNs Mockサーバ ( mruby )
- wrkコマンドによるベンチマーク
- luaでPOSTのスクリプトを作成
- 1 POST, 200 Notification
- ベンチ用でgunfishを起動する
$ h2o -c conf/h2o/h2o.conf
$ wrk2 -t2 -c20 -d10 -s bench/scripts/err_and_success.lua -L -R25 http://localhost:38103
$ gunfish -c /etc/gunfish/config.toml -E test 2> gunfish.log
パフォーマンスチューニング
# wrk2の結果
#[Mean = 23.171, StdDeviation = 22.064]
#[Max = 160.384, Total count = 240]
#[Buckets = 27, SubBuckets = 2048]
----------------------------------------------------------
242 requests in 10.01s, 31.43KB read
Requests/sec: 24.18
Transfer/sec: 3.14KB
# logの中身を確認
38542 msg:"Succeeded to send a notification", type:worker # 送信成功
9067 msg:"Response queue is full.", type:sender # 処理が追いついていない場合のメッセージ
968 msg:"Catch error response.", type:"http/2-client" # エラーレスポンスを受け取る
506 msg:MissingTopic, type:worker
190 msg:BadDeviceToken, type:worker
95 msg:Unregistered, type:worker
8 msg:"Worker Queue size: 2083", type:
8 msg:"Succeeded to establish new connection.", type:worker
8 msg:"Response queue size: 2083", type:
1 msg:, type:
242リクエスト * 10 * 200 = 48400 を送信
38542 + 9067 + 506 + 190 + 95 = 48400
リリース
リリース
- 本番環境1台のみに対してリリース
- 数日間稼働させて経過を観測
- zabbixにて監視する
- 観測するための2つのAPI
{
"pid": 19184,
"debug_port": 17889,
"uptime": 14,
"start_at": 1458276491,
"su_at": 0,
"period": 1,
"retry_after": 10,
"workers": 8,
"senders": 400,
"queue_size": 0,
"retry_queue_size": 0,
"workers_queue_size": 0,
"cmdq_queue_size": 0,
"retry_count": 0,
"req_count": 0,
"sent_count": 0,
"err_count": 0
}
{
"time": 1458276536967232300,
"go_version": "go1.6",
"go_os": "linux",
"go_arch": "amd64",
"cpu_num": 4,
"goroutine_num": 424,
"gomaxprocs": 4,
"cgo_call_num": 5,
"memory_alloc": 7276200,
"memory_total_alloc": 9109144,
"memory_sys": 13641976,
"memory_lookups": 104,
"memory_mallocs": 41548,
"memory_frees": 24228,
"memory_stack": 2228224,
"heap_alloc": 7276200,
"heap_sys": 8257536,
"heap_idle": 352256,
"heap_inuse": 7905280,
"heap_released": 0,
"heap_objects": 17320,
"gc_next": 8999555,
"gc_last": 1458276491942982000,
"gc_num": 2,
"gc_per_second": 0,
"gc_pause_per_second": 0,
"gc_pause": []
}
メモリが増え続ける問題
- HTTP/2のクライアントでメモリリークが発生していた (2/2-2/3)
- 暫定の対策として30分定期で再起動する (2/3)
メモリが増え続ける問題
- pprofによる解析
$ go tool pprof gunfish http://localhost:2412/debug/pprof/profile
(pprof) > top
(pprof) > list
- pprofを使って問題となっている箇所を調べる
- goの公式リポジトリからissueになっているかどうかを確認する
メモリが増え続ける問題
- h2のTransportがRequest.BodyをGCしないで保持され続ける
- 既にissue化されていて、問題も解決済みとなっていた
- Go1.6rc2でfixされる内容で、rc2でbuildしたら改善した
net/http: http2 Transport retains Request.Body after request is complete, not GCed #14084
メモリが増え続ける問題
問題が解決されたので無事全台に投入
まとめ
まとめ
- Golangのhttp/2を使ったAPNs Providerを開発
- Gunfishの設計思想は通知の漏れがないようにすること
- 最新のもの使うときは迅速にキャッチアップすること
- チューニング作業をもっと楽にできるようにしたかった
- 今後は…GCMにも対応させる?