きょこみのーと

技術に関係ないほうのブログ

clusterのserverについて(入門編)

これはcluster Advent Calendar 2019 17日目の記事です。

adventar.org

前日は ∞∞hk∞∞ひきこもり衆さん の「新しい世界で出会ったお姫様|hk|note」でした。

めちゃくちゃエモいですね。いい話...!

note.com

はじめに

自分のことを知らないひとが結構多いと思うので、簡単に自己紹介します。

2016年10月に入社してたので、クラスター社で働くようになってどうやら4年目に突入していました 😙

現在は、サーバーの開発をメインにやってます。 初期の方の5人とかだったときはUnityクライアント側とかも何でもやってたんですが、今は社員がたくさんいるのでサーバーの担当となってます 🎉🎉🎉

今回は、clusterのサーバーサイドの全体構成についてざっくりとした説明と、clusterでは実際どういう通信が行われているのかをいくつかピックアップして紹介していきたいと思います。

全体構成

f:id:kyokomi:20191216204203p:plain
ざっくりとした全体構成

  • MQTTサーバー: MQTTというプロトコルでpub/subを行いリアルタイムの通信で使うサーバー(位置情報や音声、体の身ぶり手ぶりなどの同期)
  • APIサーバー: ユーザーの作成、イベントの作成、イベントの入室記録などなど
  • Webサイト: ブラウザー上で動いてる部分(ログイン、チケット購入、入室までの流れなどなど)
  • Unityクライアント: 入室後のVR空間内で操作するクライアントアプリ(VR/Desktop版)
  • Stripe: 決済代行の外部サービス (クレジットカード情報などを安全に扱う)
  • Firebase Authentication: さまざまなプロバイダー(Twitter/Facebook/Googleなど)を使用した認証を行うことができ、ユーザー ID を識別できるようにする外部サービス

ログイン

f:id:kyokomi:20191216204419p:plain
ログインのフロー

上記の図のような流れで、TwitterFacebookなどの認証でclusterにログインする部分でFirebase Authenticationを使ってます。

Firebase Authenticationを使うことによって パスワード電話番号、一般的なフェデレーション ID プロバイダ(GoogleFacebookTwitter) などを使用した認証を行うことができます。

そのため将来的には他のプロバイダー追加やアカウントの移行とかが簡単にできるはずなので、乞うご期待...!という感じです。

firebase.google.com

チケット決済

f:id:kyokomi:20191216205550p:plain
カード登録・チケット購入のフロー

Webブラウザ ーで入力したクレジットカード情報は、Stripeに送信してStripe側で生成されたトークンをclusterのサーバーに送ってもらっています。

このトークンからcluster社がクレジットカード情報を参照したり、トークンを他ユーザーが推測したりすることができないようになっているため、ユーザーの皆様は安全にクレジットカード決済を行うことができるようになっています。

stripe.com

ルーム内でのリアルタイム通信

音声/位置/3点・6点トラッキング情報など送信

f:id:kyokomi:20191216210434p:plain
MQTTサーバーと直接やりとりする系

音声や、位置情報、3点・6点トラッキングによる体の向きや身ぶり手ぶりの情報はAPIサーバーを経由せず、直接MQTTサーバーに送信しています。

これが、いわゆるMO体験な部分を実現しているリアルタイム通信です。

コメント送信

f:id:kyokomi:20191216211537p:plain
APIサーバーを経由してMQTTサーバーに送信する系

コメントの送信は、1回サーバーを経由して送信するパターンです。

こちらも音声や位置情報のように直接MQTTサーバーに送信しちゃってもいいんですが、後から入室した人でも過去のコメントが読めるようにすることなど考えると、データベースに保存する必要があるためAPIサーバーを経由しています。

一定周期ごとに送信

f:id:kyokomi:20191217001407p:plain
一定周期にpublishする系

あとは、一定周期で実行され今の部屋の状態をまとめて計算して送信するバッチのような処理があります。

誰がいま入室していてどういうアバターの見た目で、誰がいまスタッフなのか?誰がいまゲストなのか?など、さまざまな情報を送信しています。

おわり

サーバー側ってなんか大変そうだな〜と思っていた人も、何をやってるか全然わからない〜と思っていた人も、興味を持っていただけたのであれば幸いです 😌

明日は、よしおか こうさん の「clusterデフォルトアバターのデザイン話」です。 アバターのデザインは、たくさんの歴史があるので楽しみです。

AnnictのAndroidアプリを久しぶりに機能追加などした(技術編)

だいぶ間が空いてしまったが、前回の続きで技術的な話を書いていきます。

Android開発久しぶりなのでリハビリって感じです。

kyokomi.hatenablog.com

ライブラリ更新

久しぶりだったのでライブラリ等を全部最新にupdateしました

  • com.android.support -> androidx
  • android.arch -> androidx

Gradleのバージョンとかも上げたりしてたので、なんか丸1日くらいかかった記憶です。

Activity を Fragment化しBottomNavigationを導入

f:id:kyokomi:20191123160832p:plain

Activityで全部処理してたんですが、JetpackのNavigationを見て今期アニメ一覧をBottomNavigationで実装するためFragment化しました。 もともと単純なRecyclerViewだったのと、ViewModelなどしっかりArchitecture componentsに従って作ってたのでそんなに大変ではなかったです。

ハマった点としては、 KotterKnifeを使っていたのですが以下の問題がありViewBindingを使うように改修しました。

ただし、一度、findViewById()で見つけたViewはキャッシュ(メモ化)して、それ以後はキャッシュしたViewを返すようになっています。 Fragmentでは、Viewを生成するのは一回だけとは限りません。

FragmentではKotterKnifeをうかつに使えないという話 - Qiita より

Debug/Canaryアプリのリボン

f:id:kyokomi:20191123160811p:plain

開発用、リリース前確認用アプリに gradle-android-ribbonizer-plugin でリボンをつけていたが、CIだとうまく動いてなかったりでした。(これは自分の設定が悪い説ですが) またライブラリを最新にしようとしたら、かなり古くにつくってからメンテされていない様子でした。

github.com

なので代わりのものを探し、easylauncherを導入しました。

github.com

とりあえず動かすまでが、とにかく楽でした。色々位置とかも調整できて便利そう。

Android Architecture components

Jetpackという呼び名が存在する前?のAndroid Architecture components は、ほぼそのまま移行できて助かりました。 RoomやPagingやViewModelなど。

developer.android.com

flipperを導入

fbflipper.com

roomの保存先であるSQLiteのdatabaseを見たかったので。

基本的にGetting Started見てやればいいんですが、noopでいい感じにreleaseビルドにいれなくて済むのか〜と思ったら、すべてのプラグインには対応してないということだったので、以下を参考に対応しました。

Databaseはこんな感じでみれる便利。

f:id:kyokomi:20191123161854p:plain

Networkはこんな感じでみれる便利。

f:id:kyokomi:20191123162200p:plain

Aboutページ、ライセンス表記

Aboutページとライセンス表記を com.mikepenz:aboutlibraries で作っていたのですが、minifyされた際にうまく生成できておらず、設定が面倒すぎて挫折しました。

github.com

簡素ですが、導入も楽なので ライセンスは play-services-oss-licenses を導入しました。

github.com

Aboutページは、自分で書いてもまあ良かったんですが com.github.medyo:android-about-page で十分ほしいものが楽に作れそうだったのでで導入しました。

github.com

CI周り

BitriseのCIとか設定が生きていたので、ストアへの公開が自動化されてて過去の自分に感動しました。

www.bitrise.io

もし今からやるならGithubActionsとかなんですかね〜?(知らないけどw

おわり

大体こんな感じでした。

Jetpackデファクトな感じでAndroidアプリ開発がだいぶ楽にできそうでいい時代になったな〜と思いました。

AnnictのAndroidアプリを久しぶりに機能追加などした

はじめに

約2年くらい前につくって、作りっぱなしだったAnnictterを長い時を経て改修しました。

(作ったときの記事は以下)

kyokomi.hatenablog.com

実は自分は作ってからずっと利用しているヘビーユーザーだったのですが、Androidのバージョンも10に上がって久しぶりに開発したくなったので色々足りてない機能を追加しようと決意し、とりあえずそこそこ機能追加してリリースし、今に至ります。

play.google.com

今回の記事で話すこと

技術的なことやアプリの内容についてなど結構色々話したいこともあり長くなりそうなので、技術的な話は別記事をあとで書きます。今回話すことは以下です。

  • 結局どういう風に利用していたか?
  • 不満とか改善したかった点
  • 今回改修した内容
  • 今後の予定

結局どういう風に利用していたか?

f:id:kyokomi:20191112213147p:plain
リリース時のblogより

とのことだが、こんな感じでした。

  • リストを同じ作品の複数話で埋めてしまい、途中で視聴を一時中断や視聴中止したくなる(これは想定どおり)
    • 一時中断や視聴中止したいが、ブラウザを起動してログイン -> 視聴中の一覧から探す -> ステータスを変更する のが面倒だった(なんかひと手間を感じてしまい、あとでまとめてやろっと〜って思ってしまいズルズルと...)
  • 結局四半期に1回棚卸し的な感じで来期のアニメをチェックしながら、今期はこれ途中で離脱してしまったな〜とかを振り返りながら、まとめてステータスを更新することになっていた(これはこれでいいんですが...)

f:id:kyokomi:20191112213737p:plain:w200
実質視聴が一時中断してる様子
(これはまだ視聴再開の見込みがある方、1話〜10話くらいまで全く見ないで並んでることもあった)

  • 結果、見たい -> 視聴中止見てる -> 視聴中止 をあとでまとめてステータス更新する運用になってしまい、 一時中断 を使いこなせていなかった。

不満とか改善したかった点

  • 一時中断のステータスをもっと活用して、やっぱ続きを見ようという流れを作りたい(思考停止して過去の名作に手を伸ばしがちなので...)
  • 来期これくるのか〜ってのをもっと早い段階で知って、準備(?)したい
  • 未視聴の話リストは、常に空リストになる感じの運用にしたい(一生タスクが整理できてない感)

今回改修した内容

作品一覧の表示・ステータス更新画面を追加

現在時刻を元に、前期、今期、来期をタブレイアウトで表示。 また、作品のステータスを一覧で確認できて、そこから編集できるようにしました。

f:id:kyokomi:20191112215137p:plain:w200f:id:kyokomi:20191112215300p:plain:w200
作品の一覧とステータス更新

BottomNavigationを追加し、視聴記録画面 -> 今期作品一覧を行き来しやすく

とりあえずサッと作品視聴ステータスを更新して、すぐに視聴記録の一覧に反映されるというのが最高です。

f:id:kyokomi:20191112220247g:plain:w250
ステータス更新して、視聴記録一覧へ反映

今後の予定

基本的に自分のAnnictの用途がスマホで、完結できるようにするのが目標です。

作品のチャンネル設定できるようにする

実はこれをちゃんとやらないと、未視聴話の一覧に反映されません。基本的には自動で設定されるの大体大丈夫なのですが、たまにWebサービスの視聴が設定されてしまい放送予定が未定だと一覧出てこないという状況になります。

f:id:kyokomi:20191112220727p:plain:w300
Webのチャンネル一覧
https://annict.jp/channel/works

見たい、見てるの一覧を見ながら以下を設定できる画面を実装する

  • 実は見なかった(見たい -> 未設定)
  • 数話みたが、もう見なさそう(見てる -> 視聴中止)
  • 今期〜来期の狭間で見るかも?(見たい・見てる -> 一時中断)

視聴記録時に「Twitterに投稿する」を選択できるようにする

Twitter見てる感じ、最近アニメあんまり見てない?」って昔からのアニメ仲間に言われたので、そんなことないよ!ってアピールをできるように(笑)

これはすぐ実装終わりそうなのでさっさとやる。誤爆しないようなUIにするのだけ注意する。

おわり

次回は、技術面について掘り下げて話していければと。 Androidアプリ開発久しぶりすぎて色々学びが多かったので。

では、みなさん良いアニメライフを!!!!

goa v3をそろそろ検証してみる

はじめに

github.com

今回は以下について

  • とりあえず触ってみた & 感想
  • Middlewareについて調査
  • Panic時の挙動とPanicをMiddlewareでハンドリングする

とりあえずGetting Startedを参考に触ってみた

https://goa.design/learn/getting-started/ 見ながらやっていく。

スッと動いた。良き!!

 [calcapi] 16:16:15 HTTP "Add" mounted on GET /add/{a}/{b}
 [calcapi] 16:16:15 HTTP "./gen/http/openapi.json" mounted on GET /openapi.json
 [calcapi] 16:16:15 serving gRPC method calc.Calc/Add
 [calcapi] 16:16:15 HTTP server listening on "localhost:8000"
 [calcapi] 16:16:15 gRPC server listening on "localhost:8080"
 [calcapi] 16:16:17  id=sGdiU14f req=GET /add/1/2 from=127.0.0.1

generateされたclientもあるけど、あえてcurlで叩いてみる。

 curl -i -X GET http://localhost:8000/add/1/2
 HTTP/1.1 200 OK
 Content-Type: application/json
 Date: Sun, 20 Oct 2019 07:20:22 GMT
 Content-Length: 2
 
 3

よさそう

v1 -> v3の移行手順はこれらしい

https://goa.design/learn/upgrading/

designは基本的には、これにしたがって移行していけばよさそう。

また今度実際に移行したら色々書く予定

midlewareについて

goa.design

gRPCに対応したことで、プロトコル関係なく実行するmiddlewareとプロトコル単位で設定できるmiddlewareの2種類になったようす。

Endpoint Middleware

Useを使えば今まで通りすべてのpathに対してのmiddlewareになるみたい。 calcEndpoints.Add に対してだけ設定するみたいなやりかたもあるみたい。

calc/main.go.diff

     // Wrap the services in endpoints that can be invoked from other services
  // potentially running in different processes.
  var (
      calcEndpoints *calc.Endpoints
  )
  {
      calcEndpoints = calc.NewEndpoints(calcSvc)
 
      // Apply ErrorLogger to all endpoints.
      calcEndpoints.Use(middleware.ErrorLogger(logger, "calc"))
 
      // Or apply ErrorLogger to specific endpoint.
      //calcEndpoints.Add = middleware.ErrorLogger(logger, "add")(calcEndpoints.Add)
  }

Transport MIddleware

HTTPだけgRPCだけ実行みたいなのがやりたいときに使うぽい。

calc/middleware/auth.go

 package middleware
 
 import (
  "context"
  "net/http"
  "strings"
 )
 
 // WithAuthToken is a HTTP server middleware that reads the value of the
 // Authorization header and if present writes it in the request context.
 func WithAuthToken() func(http.Handler) http.Handler {
  return func(h http.Handler) http.Handler {
      // A HTTP handler is a function.
      return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
          req := r
          // Grab Authorization header and initialize request context with it.
          if bearerToken := r.Header.Get("Authorization"); bearerToken != "" {
              ctx := context.WithValue(r.Context(), AuthTokenKey, strings.Replace(bearerToken, "Bearer ", "", 1))
              req = r.WithContext(ctx)
          }
 
          // Call initial handler.
          h.ServeHTTP(w, req)
      })
  }
 }

goa.Context じゃなくて普通の context.Context がくるのでそこからGetすればOK

calc.go

 s.logger.Print("Token = ", ctx.Value(middleware.AuthTokenKey))

PanicHandler

httpパッケージのserver.goのserveメソッド側のrecoverでhandlingされる。

 2019/10/20 21:16:16 http: panic serving 127.0.0.1:57525: test
 goroutine 21 [running]:
 net/http.(*conn).serve.func1(0xc00017e280)
         /usr/local/opt/go/libexec/src/net/http/server.go:1767 +0x139
 panic(0x1507000, 0x168e4c0)
         /usr/local/opt/go/libexec/src/runtime/panic.go:679 +0x1b2
 calc.(*calcsrvc).Add(0xc0000b0550, 0x16a6860, 0xc0001d43f0, 0xc0001d2130, 0xc0001ec101, 0xc0001d2130, 0xc0000eb760)
         /Users/kyokomi/workspace/ghq/github.com/kyokomi-sandbox/sandbox/golang/goav3/calc/calc.go:29 +0x11c

これはこれでとりあえず助かるんですが、responseも以下みたいな感じになってしまい...

共通の500エラーとか返したいなぁと...

 $ curl -i -X GET -H "Authorization: Bearer hogehogehogehoge" http://localhost:8000/add/1/2
 curl: (52) Empty reply from server

v1のようにEndpoint Middlewareでrecoverを入れてみる

注意点としては、recoverしたときにreturnのerrorをちゃんとdefer内で差し替えないと、正常処理に入ってしまいそっちで別のpanic等が起きてしまうことがあります(encodeとかdecodeのあたりで)

calc/middleware/panic_handler.go

 package middleware

 import (
  "context"
  "fmt"
  "log"
 
  goa "goa.design/goa/v3/pkg"
 )
 
 // PanicRecover panic recover middleware
 func PanicRecover(l *log.Logger) func(goa.Endpoint) goa.Endpoint {
  return func(e goa.Endpoint) goa.Endpoint {
      return func(ctx context.Context, req interface{}) (response interface{}, err error) {
          defer func() {
              if exception := recover(); exception != nil {
                  panicErr, ok := exception.(error)
                  if !ok {
                      panicErr = fmt.Errorf("recover error %v", exception)
                  }
                  l.Printf("[PANIC] %v", panicErr)
                  err = panicErr
              }
          }()
 
          response, err = e(ctx, req)
          return
      }
  }
 }

適当にhandlerでpanicさせてときのログ

 [calcapi] 21:19:33 calc.add
 [calcapi] 21:19:33 Token = hogehogehogehoge
 [calcapi] 21:19:33 [PANIC] recover error test
 [calcapi] 21:19:33 [ERROR] calc: recover error test
 [calcapi] 21:19:33  id=IGTAuYo3 status=500 bytes=111 time=247.777µs

curlの結果もこんな感じになる

$ curl -i -X GET -H "Authorization: Bearer hogehogehogehoge" http://localhost:8000/add/1/2
HTTP/1.1 500 Internal Server Error
Content-Type: application/json
Date: Sun, 20 Oct 2019 12:19:33 GMT
Content-Length: 111

{"name":"fault","id":"Aghc9GSY","message":"recover error test","temporary":false,"timeout":false,"fault":true}

Go言語による並行処理

Go言語による並行処理

Go言語による並行処理

感想だけシュッと書きます。

  • 今までなんとなく使ってたgoroutineやcontextのCancelなどを改めて学ぶことができてよかった
  • いつも、 sync.WaitGroup で事足りてしまい、あまりチャネルを使えてなかったなぁという学びもあった
  • goroutineでアクセスする系で、気軽にsync.RWMutex を使いがちだったなぁと反省した

とはいえ、じゃあ毎回チャネルを使うのか?というのも正解ではなく、使うことによってパフォーマンスや処理が最適化されるが、コードの可読性などがトレードオフになりえる点などを気をつけて、書籍の例にあるように書き込みと読み込みの責務をしっかり分けるなどやっていければと思いました。

monorepoでCircleCIを使ってherokuへのdeployをする

はじめに

かなり特殊な構成なので自分以外には役に立たないかもしれませんが...

個人開発のプロジェクトをmonorepoで開発しているのですが、apiサーバーがherokuなので単純にmasterをそのままherokuにpushするとめちゃ不要なファイルなどが含まれてしまうという問題がありました。

gitignoreすればいいとは思いつつも、heroku用のgitignoreとgithubで管理するコード用のgitignoreみたいなことをするのも辛いので、 git worktree を使ってheroku用のブランチへのpushを行い、それをherokuにもpushするというアプローチにしました。(以下の図のようなイメージ)

herokuまでのdeployの流れ

f:id:kyokomi:20191019212317p:plain
herokuまでのdeployの流れ

CircleCIの各jobの説明

  • build jobは普通にgolangのbuildやtestなどを行う
  • git_push jobは deploy.sh を実行して、 heroku用のworktreeを作成し github にpushします
    • deploy.sh の中から呼び出している deploy_generate_file.sh で必要なファイルだけをコピーするようにしてます
    • githubheroku ブランチへapiサーバーのdeployに必要なファイルだけがpushされ、またCircleCIが呼び出されます
  •   一度ここでCircleCIのworkflowは途切れてしまいます
  • heroku_deploy jobによって herokuのgitリポジトリに対してpushを行います
    • このpushでheroku側でdeployが行われます

登場するファイル(参考までに

deploy.sh

#!/bin/bash

set -x

# shellcheck disable=SC2006
if [ "`git status -s`" ]
then
    echo "The working directory is dirty. Please commit any pending changes."
    exit 1;
fi

echo "Deleting old publication"
rm -rf heroku
mkdir heroku
git worktree prune
rm -rf .git/worktrees/heroku/

echo "Checking out heroku branch into heroku"
git worktree add -B heroku heroku origin/heroku

echo "Removing existing files"
rm -rf heroku/*

echo "Generating site"
sh ./deploy_generate_file.sh

echo "Updating heroku branch"
cd heroku && git add --all && git commit -m "Publishing to heroku (deploy.sh)"

echo "Pushing to github"
git push -u origin heroku

deploy_generate_file.sh

#!/bin/bash

set -x

cp -r .circleci heroku       # circleCIが動くようにするため
cp -r api heroku             # このディレクトリにapiサーバーのコードが入ってます(main.goとかとか)
cp go.mod go.sum heroku      # 訳あってgo.modが上位階層にあるのでそれもコピー
cp app.json Procfile heroku  # herokuのdeployに必要なファイル
cd heroku && go mod vendor   # heroku側でfetchしなくていいように(deployを早くするため)にgo mod vendor

circleci/config.yml

# Golang CircleCI 2.1 configuration file
version: 2.1

jobs:
  build:
    docker:
      - image: circleci/golang:1.13
    environment:
      GO111MODULE:     "on"
    working_directory: /go/src/github.com/kyokomi/webapp-sandbox
    steps:
      - checkout
      - run: go get -u golang.org/x/lint/golint
      - run: go get -u golang.org/x/tools/cmd/goimports
      - run: make test_all
      - run: make build_api

  git_push:
    docker:
      - image: circleci/golang:1.13
    environment:
      GO111MODULE:     "on"
    working_directory: /go/src/github.com/kyokomi/webapp-sandbox
    steps:
      - checkout
      - run:
          name: git settings
          command: |
            git config --global user.email kyokomidev@gmail.com
            git config --global user.name circleci

      - run: sh deploy.sh

  heroku_deploy:
    docker:
      - image: buildpack-deps:trusty
    environment:
        HEROKU_APP_NAME: "kyokomi-webapp-sandbox"
    steps:
      - checkout
      - run:
          name: Deploy Master to Heroku
          command: |
            git push https://heroku:$HEROKU_API_KEY@git.heroku.com/$HEROKU_APP_NAME.git master

workflows:
  api_build_and_deploy:
    jobs:
      - build:
          filters:
            branches:
              ignore: heroku
      - git_push:
          requires:
            - build
          filters:
            branches:
              only:
                - master
      - heroku_deploy:
          filters:
            branches:
              only:
                - heroku

ISUCON9予選にk02で参加して完全敗北しました #isucon

いつものメンバーでISUCONに参加して完全敗北...! いつもどおりGo書く人を担当。 最大スコアは3,120で、最終結果は fail 。

以下メンバーの感想。

alligatorswamp.hatenablog.com

事前にやったこと

  • scrapboxに色々使いそうな記事をまとめたり
  • ISUCON8予選の過去問の環境を構築して模擬したり
  • 使うツールの選定など

ISUCON9予選当日の作業の流れ(kyokomi目線)

  • 10:00 開始
    • ドキュメントを読む -> 読んでる途中にAlibabaでの環境構築を先にやったほうが良いと判断
  • 10:15 〜 10:15
    • Alibabaの環境構築完了
    • sshできることと、アプリケーションにログインできることを確認した https://gyazo.com/b8705539ea9092849d86721548d54399
    • 速攻でベンチマークを実行する image
    • ベンチ実行によってクレジットカードの認証が通らなくなったことに気が付かずメンバーの二人がわちゃわちゃしてた
      • しばらくjsのコードとか見た後にレギュレーションに書いてあったこと気がつく...
  • 10:00 〜 10:30
    • githubにpushする
    • scrapboxにメモってた手順のpush先が事前に素振りしたときのrepositoryになってて無駄に混乱した
    • image
    • github tokenでシュッとpushできるようにする手順書いてたのは正解だった
  • 10:30 〜 10:50
    • ファイル構成把握しながら、不要なファイル削除したり、gitignoreの設定を追加
    • ローカルで起動するようにflag追加、docker-composeの追加など
    • 初期データがローカルでなくて動かないことに気が付き120MBもあることからgitignoreされてるというやさしさに気がつく
  • 10:50 〜 11:30
    • テーブル構成を把握したかったのでER図作ろうとして外部キー貼ったら、初期処理でエラーになって「う〜ん?」って調べてたが本質ではないな...と30分くらい無駄にしてホワイトボードに手書きした
  • 11:30 〜 12:10
    • main.goのコードが長く、目的のコードがさっと見つけられず大まかな機能単位でfileを分けるリファクタに手をだした
    • 見通しはよくなったし改修時の対象を探すのも楽になった感じはある
  • 12:10 〜 12:30
    • Prometheusで監視できるようにmetricsのhandlerを仕込んだり
    • image
    • 結局ほとんど使わなかった...
  • 12:30 〜 13:30
    • マイページのtransactionsの取得がボトルネックぽいということで見始める
    • `status IN (?,?,?,?,?) AND ` がサクッと見て無駄そうなので消した(全ステータス指定してたので)
    • indexを貼った。queryの負荷はそこそこ減ったぽい(スロークエリ曰く) image
  • 13:30 〜 14:00
    • categoriesが32万回くらいselectされてたのでサクッとcacheした
    • 100〜1000回くらい?まではselect回数が減った(レコード数は40くらいなんだけど)
      • これはinitで全レコードcacheしたほうがよかったなとあとから思った
      • 一旦、できるだけ既存の実装から離れないように〜というポリシーでこうしてた
  • 14:00 〜 14:40
    • usersもcacheした
    • この時点の3120点になってた(そしてこれが最終的に最大スコア...)
  • 14:40 〜 15:00
    • なんかスコア上がらねーなと思い、このままgetTransactionを見ていっていいのか???と不安になり
    • キャンペーンを有効にしてみる?という話になって1に設定した
    • 結果なんかめっちゃエラーになる
    • そしてAPIGatewayErrorとtoo many connectionsがでまくる
    • DBのCPU使用率も落ち着く(これはミスリード...)
    • これを解消するのが鍵では!!!!!と作戦を変更(これが失敗だった...)
    • MaxConectionはアプリ側は一旦120に設定し、DB側(3号機)で300くらいに設定して一旦でなくなった
    • APIGatewayの502エラーが自分たちのNginx?と勘違いしてulimitか!!とか結構時間無駄にした
      • よく考えたらアプリログでNginxのログでるのおかしくね???ってなり、そこでようやく外部サービスやん...って気がつく
  • 15:00 〜 17:00
    • とりあえずログやmeasureでどれくらい外部APIが叩かれてるのかを可視化したり
    • statusの取得が無駄っぽいから wait_done 以外は shippings のテーブルの結果を見てresponseに設定したりを入れてリクエストを減らしたり
    • item購入時にreserveするの不要そう?(実際にshipするときでいいのでは)と判断し、shippingsテーブルのreserve_idとかをNULL許可にしてうまいことshipするやつだけreserveの外部APIを叩くようにしてリクエストを減らしたり image
  • 17:00 〜 18:00 終了
    • ずっとキャンペーン有効による外部APIのGatewayTimeoutをなんとかせねば〜と試行錯誤していてFailしたままなのでなんとかしなければと焦り始める
    • 502エラー速攻で返すくらいならタイムアウトギリギリまでRetryしたほうがいいのでは?と、適当に1秒sleepして3回までRetryするbackoff的な実装を強引にGOTO文で実装したりしたがしかし...
    • 結局最後は10回Retryしてタイムアウトになり、じゃあ5回だ!!とか完全にヤケクソ状態の山賊状態だった...
    • 10ってコメントのpushなのにコードは10 -> 5に変更っていう...もうむちゃくちゃ...
    • もう何と戦ってるか本人達もわかっていないまま試合終了してしまった... 〜完〜

まとめ

  • 想像を上回る良い出題ですごく楽しかったです
  • レギュレーション大事だよな...!って毎回わかってるつもりだけど終わるまで色々気づけてなくて悔しい
  • なんかいっぱいAPI叩かれてるね。なんかめっちゃCPU上がってるね。の深堀りができる状況になってなかった
  • 悔しい...!来年こそは...!