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について

  1. APNsとの通信をバックグラウンドに回す必要がある
    • APNsからのレスポンスに時間が掛かるため
    • 参考:APNsとの通信は0.15 sec程かかる
  2. APNsとのTCPコネクションをプールする必要がある
    • 短いスパンで接続と切断を繰り返すとDoS攻撃とみなされる
    • TCPコネクションを作りすぎてリソース等圧迫させたくない
  3. 無効なトークンを消込む必要がある
    • 無効なトークンより後ろのリクエストは破棄されてしまうため
    • APNsからEOFが送られて切断されることがあり送信漏れが発生するため
    • 2つの消込処理が必要
      • エラーレスポンス」の即時消込
      • フィードバックサービス」の定期消込

LobiのProviderの実装

  • AnyEvent::APNSを用いてPush Notificationを実装(Perl製)
    • TCP接続を持ち回しする
    • JSONの配列をHTTPのPOSTで受け取る
  • エラーレスポンス時に対象となる無効なトークンを即時削除
    • フィードバックでの削除はProviderにはつけない
  • 無効なトークンに巻き込まれて届かなかったデータを再送
    • 送信した履歴を持っておく
    • エラーレスポンスのリクエストよりも後のものを再送

レガシーAPNsの悩み

  • 独自のプロトコルが厳しい
  • 無効なトークンによる巻き込み事故
    • 再送処理が連続で発生すると一部届かなくなる
  • 無効なトークンの条件が完全に絞り込めない
    • すべての無効なトークンを消し込むことは難しい
    • 無効なトークンが蓄積されてしまい無駄に通信してしまう
    • 無効なトークンが原因でAPNsから切断される場合がある

New APNs

New APNs

  1. HTTP/2で通信する
  2. フィードバックサービスが不要になる
  3. APNsとの認証処理が簡単になる
  4. ペイロードの情報量が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": []
}

メモリが増え続ける問題

  1. HTTP/2のクライアントでメモリリークが発生していた (2/2-2/3)
  2. 暫定の対策として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にも対応させる?