つばくろぐ @takamii228

知は力なり

ISUCON10に参加した #isucon

先日行われたISUCON10に元同僚の id:int128 先生と id:translucens 先生と gosoudan2というチームで参加してきました。

isucon.net

事前準備

実はISUCON9に同じメンバーでgosoudanというチームで参加していて、このときは始めてだったので勝手が分からなかったものの無事スコアを残せてよかった、という形で終わってました。

参考順位的には594チーム中246位相当でした。

isucon.net

ISUCON10に向けては、各メンバーの事情やコロナもあって集まって練習したりということはせず、一週間くらい前から前回の振り返りをしつつ今回どういうTryをするかを話し合ってました。振り返りのKPTからは以下のようなものが上がっていました。

  • Keep
    • CDパイプラインが構築できた
    • 初参加でスコアが残せた
    • StackDriver Profilerでボトルネック分析のサイクルが回せた
  • Problem
    • 問題文の理解が不十分でスコアに影響する改善に時間がかけられなかった
    • 改善の計画や評価をうまく整理できず、行き当たりばったりになってしまった

前日のミーティングにて以下のTryをすることを決めました。

  • 問題文をまず精読してドメインをきちんと把握する
  • 最初のスコアを出す、最初の改善を出すまでにやることをリストアップしておく
  • 1時間単位で情報の同期をとる

また目標としては去年以上の順位をとること、またアルゴリズム、データベース、ネットワーク通信、インフラ構成の中から少なくとも3種類の性能改善を実現することを上げました。

利用する言語は全員Gopher道場卒業生なのでもちろんGoです。

当日スムーズに動けるように、競技が開始してからやることをTo DoリストにしてGoogle Docsで共有しておきました。また当日も各メンバーが気づいたことわかったことをGoogle Docsに集約する形ですすめることにしました。また競技中のコミュニケーションはZoomを繋ぎっぱなしにしてSlackでテキストコミュニケーションを取ることにしました。

当日の流れ

当日は開始が遅れて12時20からの開始になりましたが、事前に昼食を済ませて待機していました。

時系列でやったことを並べると以下のような試合展開でした。

  • 12時 ~ 13時台
    • 問題文の精読を各メンバーで実施し20分個人作業、15分共有のサイクルを回す
    • sshの設定を全員で共有し全員各サーバにsshできることを確認
    • サーバー構成を確認して上がっているプロセスや動いているアプリケーションの状況を確認
    • 標準構成でスコアを取りに行った => 13:15分ごろに初回ベンチ実行して499
    • ソースコードGitHubにコピー
    • インフラ系の設定ファイルをあとでロールバックできるようにバックアップを作成
    • メトリクス計測できるようにStack Driverの設定を実施
    • GitHub ActionsでSelf Hosted Runnerを使ったCDパイプラインの構築に着手
    • 業務シーケンスのわかったことを共有
  • 14時台
    • GitHub Actionを使ったCDパイプラインが出来上がる
    • 各画面から呼ばれるAPIマッピングを把握、改善できそうな処理にあたりを付ける
    • DBのperformance_profileを設定
    • AP2台構成、DB1台構成にして不要なプロセスを停止してProfileをオンにして2回目のベンチ => 322
    • DBのCPUが100%で張り付いててAPはすっかすか、DBにindexなし、nazotteのAPIコールが遅いことが判明
    • Stack Driverのプロファイルのオン・オフを環境変数で切り替えるように修正
  • 15時-16時台
    • DBにindexを付与
    • nginxのログにレスポンスタイムを追加、変なログが無いか確認
    • ベンチを回す => 683に向上
    • 検索画面でconditionsで毎回jsonファイルを読んでることに気づくもProfilerみてもそんなに負荷がかかってないので放置
    • nazotteのN+1にトライ開始
    • nginxのbot排除のフィルター処理を追加
    • APIの処理概要をSQL単位で精読
  • 17時台-18時台
    • nazotteのN+1とbotの排除でスコアが1350に
    • buffer_sizeのwarningが出てたので拡張するように修正
    • select * 部分を最適化しようとクエリのリストアップを開始
    • MySQLを8に上げたらどうなるかトライ開始
  • 19時台
    • MySQLを8にしたらスコアが激減、conf設定をいじり始める
    • いじってもスコアが改善しないのでMySQL5.6に切り戻す
    • select *を変えると初回チェックで弾かれるのではと思い断念
    • Topの low_priced にpriorityやstockのORDERをつけたらスコアが伸びるのではとトライ開始
    • データ登録のInsertをbulk insertに変更するように修正
    • featureの文字列カンマ結合のLIKE検索が重そうなのでTABLE追加に着手
  • 20時台
    • low_pricedの修正を入れたらレスポンス不正でアウトだったので断念
    • bulk insertの修正が入ってスコアが1472に
    • featureのTABLE追加はレスポンス不正の不具合が取れず断念
    • bulk insertのバージョンでサーバーにデプロイして再起動テストに備えて設定を最終確認

私がアプリの深い仕様理解やスコア改善に効果がありそうな改善のリストアップを、 id:translucens 先生がプロファイル設定やDB周り改善の実施、id:int128先生がデプロイパイプライン含めた開発の下回り整備とGoのアプリ実装という役割分担でうまく3人で協力しながら立ち回れたかなと思います。

スコア推移

サブミットしたスコアの推移は以下の通りでした。最終的な参考順位は1399で92位 / 468チームだったようです。nazotteの改善ができたあたりでは途中は30位内くらいだった気がしますが、そこからスコアが伸びなかったのが悔やまれます。

f:id:takamii228:20200915093319p:plain

isucon.net

振り返り

前回は初回参加で右も左も分からなかったですが、前回の反省を踏まえた改善アクションも取れましたし、ある程度の性能改善アクションも取れてスコアも伸ばすことができました。

競技終了後の振り返りでは事前準備がうまくいったこと、tag pushによるGitHub ActionのCDパイプラインができたこと、問題を十分理解できたことなど前回の反省が生かされたKeepが上がりました。一方でAP2台+DB1台の構成に固執してたねってのが上がって、事前に想定してないアクションはなかなか本番ではとれないなと感じました。

個人的にはGoを1年以上まともに書いてなかったので、アプリの改善を自信を持って書けなかったのは悔しかったです。でも業務理解やタイムキーパー、nginxの設定等でチームには貢献できたかなと思います。

来年もISUCONがあるかわからないですが、3回目は機会があれば本戦出場を目指して戦えたらいいなと思います。参加した皆さん、運営の皆さん本当にお疲れさまでした!!!

Androidを学びたい人はcodelabsを一通りやるといい

先日こんな記事が話題になっていた。

android-developers.googleblog.com

ノンプログラマ向けのAndroidアプリ開発の学習コンテンツらしい。

早速やってみたら動画とcodelabs形式で起動してクリックイベントを拾って画面を切り替えるアプリが出来上がった。動画で講師陣がやたらテンションが高いのはアメリカらしい。

ものの数時間で終えたので、すでに開発業務にやってる人にとっては物足りないなーと思ったらブログの末尾に3つコースが紹介されていた。

developer.android.com

developer.android.com

developer.android.com

1つめはKotlinの言語についてのcodelabs、2つめはKotlinをつかったAndroid開発の基礎編のcodelabs、最後は応用編といった形だ。

せっかくなので流れでKotlinと基礎編を全部やってみたら案外量が多くて、土日を3回ほど費やしたが自粛で暇だったのでいい学習題材になった。

コンテンツは以下の内容を含んでいて、かなり満足いくものだった。

  • Layout
  • Navigation
  • ActivityとFragment
  • Architecture Components
  • Room
  • Recycle View
  • Internet connection(Retrofit)
  • Repository
  • Design(ThemeとStyle)

進め方は穴埋め形式でガイドに従ってソースを補完していくとアプリが出来上がるという流れ。コピペでもとりあえず動くものが出来上がる。

一通りやるだけでArchitecture Componentsを使ってMVVMなAndroidアプリの作りが理解できた。すごい。

GitHubに演習前と回答のソースもすべて公開されている。

github.com

github.com

Androidを扱った研修コンテンツを作る仕事を少しやってたけど、もうこれでいいじゃんってなった。テスト技法とか通知とかがあるといいかなーって思ったらAdvancedコースの方にある模様。さすがである。

とりあえずAndroidを学びたい人はこれを一通り理解するとよいと思いました。

またこれを終えた人は実際に自分でアプリを作ってみたり、以下の本を読んで理解が深められたらもう晴れてAndroidエンジニアになれるのではなかろうかと錯覚した。

peaks.cc

peaks.cc

iOSもこういうのあるといいんだけどなー。探してみよう。

iOSのPush通知でAPNsとの連携を証明書と認証キーでそれぞれやってみた

iOSアプリのPush通知を実現するには、APNs(Apple Push Notification Service)と連携する必要があります。この連携方式について調べてみたので備忘録としてまとめておきます。

APNsとの連携方式

APNsとの連携はPush通知を受け取るアプリケーションとの連携と、アプリケーションにPush通知を送るようにAPNsへ依頼するアプリケーションとの連携 の2種類行う必要があります。

https://docs-assets.developer.apple.com/published/1fe29f6177/4ebaf4d8-031d-4eb5-b975-07373dfa6eb6.png

https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server

前者のPush通知を受け取る側の設定についてはiOSのアプリケーションのApp IDをApple Developer Console上で発行するときにPush Notificationsを有効にし、APNsの証明書と紐付けることで実現できます。証明書の発行の手順はやや複雑ですが、ググればいろんな記事がヒットします。

qiita.com

一方で後者のPush通知を発行する側の連携方式については、実際はAPNsと直接やりとりするケースはあまりなく、Firebase Cloud MessagingやニフティクラウドなどMBaaS製品を経由することが多いと思います。今回はこちらの連携方式について詳しく見ていきます。

Push通知をAPNsへ依頼するアプリケーションとの連携方式

Push通知をAPNsへ依頼するアプリケーションとの連携方式は、調べてみると現在は認証キー(p8ファイル)での認証方式と証明書(p12ファイル)での認証方式の2種類があるようです。

認証キーでの認証方式の方が新しく、証明書での認証方式は以前からある方式のようです。それぞれのメリット・デメリットを比較すると以下のようになります。

連携方式 メリット デメリット
認証キー(p8ファイル)での認証 キーの有効期限がなく更新の運用がいらない Apple Developer Programに対して2つまでしか認証キーが発行できない
・App ID単位で管理ができない
・開発環境・本番環境と区別ができない
証明書(p12ファイル)での認証 ・App ID単位で管理ができる
・開発環境・本番環境と区別ができる
1年単位で更新が必要となる

メリット・デメリットはそれぞれの裏返しになっています。

認証キーの方が便利そうに見えますが、キーが2つしか発行できないので、同一アカウントで複数のアプリを開発・配信している場合は鍵の原本管理に注意が必要です。とはいえ証明書のケースでも管理は大変なので、管理対象の数が減って更新運用が不要になるのはだいぶ楽ですね。 今後はどうなっていくかについてはAppleのご都合次第ですが、恐らく認証キーの方に流れていくのではないかと予想しています。

Legacy Binary Protocolでの連携は2020年11月以降サポートされない

ちなみにもっと昔にはLegacy Binary Protocolというものもあったようです。こちらは2020年の11月以降はサポートしないとAppleから発表されています。

少し混乱しますが、ここでサポートが切れるのはLegacy Binary Protocolの連携方式であって、証明書ベースの認証方式自体は2020年11月以降も利用できると考えられます(証明書でもhttp2で通信できるますし)(でも違ったらごめんなさい...)。

各種MBaaSにおけるAPNsとの連携方式

ここで各種MBaaSのツールのAPNsとの認証方式が現在どうなっているか見てみましょう。

Firebase

Firebaseの設定手順を見ると、authentication keyとあるので認証キーでの設定が推奨されているようです。

firebase.google.com

FCMの設定画面ではAuthentication Keyが推奨と出ていますが、APNs CertificatesのアップロードUIも用意されているので今現在は両方利用できるようです。

f:id:takamii228:20200713164341p:plain

ニフティクラウド

ニフティクラウドはp12ファイルのアップロードとあるので証明書での認証方式のようです。

mbaas.nifcloud.com

f:id:takamii228:20200713165100p:plain

AWS Amplify

AWSのBaaSツールであるAmplifyはp12ファイルのアップロードとあるので証明書での認証方式のようです。

docs.amplify.aws

Salesforce

セールスフォースはp12ファイルの利用をやめてp8ファイルへの以降に舵を切ったようです。

org62.my.salesforce.com

証明書での認証と認証キーでの認証は共存できる?

証明書での連携方式を今後どこかのタイミングで認証キーへ移行していくケースを考えてみます。証明書(p12ファイル)と認証キー(p8ファイル)は、いずれもPush通知の発行を依頼するサービスがAPNsへリクエストを投げる時の認証に使うものです。そのため、基本的にはAPNsの受け口が空いている限り共存できるのでは思いましたが、少し不安になったので同一アプリに対してp12ファイルで認証したPushのリクエストとp8ファイルで認証したPushのリクエストがいずれも受信できるか実際に検証してみました。

証明書一式を準備する

検証用のアプリを作るにあたっての一式をApple Developer Program上で準備します。

  • 開発用証明書
  • AppID(Push Notificationsをオンにする)
  • provision profile(検証につかう端末を含む)
  • APNs証明書(P12ファイル)
  • AuthKey(p8ファイル)

iOSのサンプルプロジェクトを作る

次にPush通知を受け取るサンプルアプリを作ります。今回はPush通知が受け取れればいいので、デフォルトのSingle Page Viewアプリプロジェクトを作り、AppDelegate.swiftに最低限の設定のみを記述します。PushのCapabilitiesをオンにするのを忘れないようにします。

import UIKit
import UserNotifications

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Push通知の許可ダイアログを出す
        UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
            guard granted else { return }
            DispatchQueue.main.async {
                UIApplication.shared.registerForRemoteNotifications()
            }
        }
        UNUserNotificationCenter.current().delegate = self
        
        return true
    }
   
   ...
    // Push通知の用のデバイストークンをログに吐き出す
    func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        let token = deviceToken.map { String(format: "%.2hhx", $0) }.joined()
        print("Device token: \(token)")
    }
    
    // Push通知のペイロードの受信を確認する
    func application(_ application: UIApplication, didReceiveRemoteNotification payload: [AnyHashable: Any]) {
        print(payload)
    }
            
    // フォワグラウンドで通知バナーを出す
    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
         completionHandler([.alert, .sound, .badge])
    }
}

この状態でアプリを実機でビルドし、Push通知の許可ダイアログを許可するとログからデバイストークンが取得できます。このデバイストークンを使ってPush通知のリクエストを組みます。

証明書を使ってAPNsへPush通知のリクエストを投げる

p12ファイルを使ったAPNsへのリクエストはcurlコマンドを使って組むことができます。ググってみたらちょうどcurlコマンドがあったのでこれを参考にして組んでみます。

https://gist.github.com/greencoder/16d1f8d7b0fed5b49cf64312ce2b72cc

#!/bin/sh

set -x

P12_CERT_FILE_PATH="xxxxx.p12"
APP_BUNDLE_INDENTIFER="com.takamiii.push-test"
DEVICE_TOKEN="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
API_ENDPOINT="https://api.development.push.apple.com/3/device/"

curl -v \
-d '{"aps":{"alert":{"title":"Test","body":"Hello from request with P12 certificates"}}}' \
-H "Content-Type: application/json" \
-H "apns-topic: ${APP_BUNDLE_INDENTIFER}" \
-H "apns-priority: 10" \
--http2 \
--cert-type P12 --cert ${P12_CERT_FILE_PATH} \
${API_ENDPOINT}${DEVICE_TOKEN}

curlコマンドを実行すると無事200で成功し、Push通知を受信できました。ログからPushが受け取れていることがわかります。

< HTTP/2 200 
< apns-id: 66AF09FA-154B-F68F-CAB3-5CBD49104122
< 
* Connection #0 to host api.development.push.apple.com left intact
* Closing connection 0
[AnyHashable("aps"): {
    alert =     {
        body = "Hello from request with P12 certificates";
        title = Test;
    };
}]

通知バナーも出ています。

f:id:takamii228:20200713174627p:plain

認証キーを使ってAPNsへPush通知のリクエストを投げる

p12ファイルの設定はそのままに、p8ファイルの認証キーを使ったPush通知のリクエストを投げてみます。こちらはjwtの文字列を組む必要があって少し複雑なのですが、こちらのqiita記事を参考を参考にリクエストを組んでみます。

qiita.com

#!/bin/bash

set -x

AUTH_KEY_FILE_PATH="AuthKey_xxxxxx.p8"
AUTH_KEY_ID="xxxxxxxx"
TEAM_ID="xxxxxxxxx"

APP_BUNDLE_INDENTIFER="com.takamiii.push-test"
DEVICE_TOKEN="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
API_ENDPOINT="https://api.development.push.apple.com/3/device/"

base64() {
   openssl base64 -e -A | tr -- '+/' '-_' | tr -d =
}

sign() {
   printf "$1"| openssl dgst -binary -sha256 -sign "${AUTH_KEY_FILE_PATH}" | base64
}

TIME=$(date +%s)
HEADER=$(printf '{ "alg": "ES256", "kid": "%s" }' "${AUTH_KEY_ID}" | base64)
CLAIMS=$(printf '{ "iss": "%s", "iat": %d }' "${TEAM_ID}" "${TIME}" | base64)
JWT="${HEADER}.${CLAIMS}.$(sign ${HEADER}.${CLAIMS})"

curl -v \
   -d '{"aps":{"alert":{"title":"Test","body":"Hello from request with P8 certificates"}}}' \
   -H "Content-Type: application/json" \
   -H "Authorization: Bearer ${JWT}" \
   -H "apns-topic: ${APP_BUNDLE_INDENTIFER}" \
   --http2 \
   ${API_ENDPOINT}${DEVICE_TOKEN}

こちらも無事200で成功し、Push通知を受信できました。ログからPushが受け取れていることがわかります。

< HTTP/2 200 
< apns-id: 09393B36-E1CE-4F9A-5A29-2FC7FF73D424
< 
* Connection #0 to host api.development.push.apple.com left intact
* Closing connection 0
[AnyHashable("aps"): {
    alert =     {
        body = "Hello from request with P8 certificates";
        title = Test;
    };
}]

通知バナーも出ています。

f:id:takamii228:20200713175350p:plain

以上から、p12ファイルとp8ファイルを使ったリクエストは併用できているので、証明書と認証キーの設定は共存できていると考えられます。

なのでもともとニフティクラウドでp12ファイルでPushを送っていたところからp8ファイルを使ったFirebaseでのPushに移行する、なんてことは問題なくできそうということですね。

まとめ

iOSのPush通知についてAPNsとの連携方式ついて調べた結果をまとめてみました。Push通知の実現にはアプリの実装に加えて証明書連携が必要で、その手順は複雑ですが一つ一つ紐解いていけば理解できるものだと思いました。

またAPNsの証明書と認証キーの移行については今のところどちらでも利用できているので、Pushを実際に発行するアプリケーションの連携方式に合わせて利用していけばよいでしょう。認証キーでの連携は今後は増えていくと思われるので、鍵ファイルの管理をきちんとしていく必要があると感じました。

おまけ

Push通知のリクエストのエンドポイントを調査している中でapi.sandbox.push.apple.comapi.development.push.apple.comの2つがあって混乱したので、digで掘ってみました。

$ dig api.sandbox.push.apple.com

; <<>> DiG 9.10.6 <<>> api.sandbox.push.apple.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 32012
;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
;; QUESTION SECTION:
;api.sandbox.push.apple.com.  IN  A

;; ANSWER SECTION:
api.sandbox.push.apple.com. 269   IN  CNAME   api.sandbox.push-apple.com.akadns.net.
api.sandbox.push-apple.com.akadns.net. 185 IN A   17.188.138.73

;; Query time: 14 msec
;; SERVER: 2404:1a8:7f01:b::3#53(2404:1a8:7f01:b::3)
;; WHEN: Mon Jul 13 18:40:18 JST 2020
;; MSG SIZE  rcvd: 122
$ dig api.development.push.apple.com

; <<>> DiG 9.10.6 <<>> api.development.push.apple.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 45266
;; flags: qr rd ra; QUERY: 1, ANSWER: 5, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
;; QUESTION SECTION:
;api.development.push.apple.com.  IN  A

;; ANSWER SECTION:
api.development.push.apple.com.   242 IN  CNAME   api.sandbox.push-apple.com.akadns.net.
api.sandbox.push-apple.com.akadns.net. 554 IN A   17.188.165.218
api.sandbox.push-apple.com.akadns.net. 554 IN A   17.188.166.29
api.sandbox.push-apple.com.akadns.net. 554 IN A   17.188.165.219
api.sandbox.push-apple.com.akadns.net. 554 IN A   17.188.166.27

;; Query time: 17 msec
;; SERVER: 2404:1a8:7f01:b::3#53(2404:1a8:7f01:b::3)
;; WHEN: Mon Jul 13 18:40:36 JST 2020
;; MSG SIZE  rcvd: 174

どっちも最終的には同じapi.sandbox.push-apple.com.akadns.netを向いてるようなので、どちらでもよさそうです。