詩と創作・思索のひろば

ドキドキギュンギュンダイアリーです!!!

ISUCON9予選敗退(H::W::A::Abyss::Abyss::Abyss) #isucon

ISUCON9 2日目結果と本選出場者決定のお知らせ : ISUCON公式Blog

日曜日に行われたISUCON9予選に参加し、敗退してきました。前掲の記事の「失格となったチーム」がわたしたちです。チームメンバーは id:t_kytid:mechairoi

去年も予選敗退していて、最近LINE本社に行ってないな~と思っていたので今年は勝ちたかった。今年もはてな京都オフィスで予選に参加。

f:id:motemen:20190909133951p:plain

事前の作戦が大事なので、「コミュニケーションが大切」「マニュアルを読む」を確認した。

だいたいハマり出すとコミュニケーションが減ったり同じことをやりだしたりしてチームとしての効率が下がってしまうので、一時間ごとにタイマーを鳴らしてそこで話そう、という作戦を立てていた。あとは alp と pprof と pt-query-digest でボトルネックを見ていくというシンプルな戦略。

あと、1日目の結果を見ていて

美顔器 on Twitter: "近年の傾向を分析した結果、今回のISUCON戦略は「学生気分」とします"

という気づきを得たので方針は「学生気分」となった。

やったこと

  • 10:53 最初のベンチが取れるまでがだいたい1時間くらい。デプロイは手元で build してバイナリだけ rsync するような方法で、これはサイクル早く回せてよかった。
    • ベンチを回していると isucari の CPU 使用がボトルネックになっているのが分かったので、pprof。http.ListenAndServe(":6060", nil) して待ち受ける練習をしてたんだけど、よく考えたらセキュリティポリシーで塞いでいたので ssh でポートフォワードした。
  • 11:49 pprof すると postLogin 内の bcrypt が重いことが明らかだったので、「学生(10年前の自分)だったらどうするか?」と考えて即座にパスワードを平文化することを思いつき、ベンチを回してパスワードを収集する実装を投入。還元率 1 で 5,230 イスコイン。
    • あとで書くけど、この初手で地獄への道が敷かれていた。笑える。
    • まあ気づいていたとしても MD5 化くらいはやったと思うので、あとはそうだと思って読んでください。
  • 13:28 当然スコアは上がるんだけど、ベンチマークを回してパスワードを収集するたびにパフォーマンスが上がり、どこかのタイミングで臨界して POST /buy が失敗するようになったので、どうしようかねー、と話して外部 API へのアクセスをリトライするようにしたところ、うまく行って還元率 2 で 8,030 イスコイン。
    • これなんでリトライでうまく行ったんだろ。今考えるとよくわからない。500 になってたのかな?
  • かりそめの暫定一位を取ったところでお昼ごはん。
  • 14:03 どうせ3台構成にするから今のうちにやっとこ、ということで 1 台を nginx+アプリ、1 台を MySQL にしたところ還元率 0 で 2,710 イスコイン。還元率を 1 にすると以下のエラーが出て FAIL するようになってしまった。この失敗理由が見えないので一同相当にハマってしまった。Revert したり構成戻したり、でかなり時間を取ってしまった。なんでなの……。
POST /buy: リクエストに失敗しました (item_id: 50030),
POST /buy: リクエストに失敗しました (item_id: 50036),
POST /buy: リクエストに失敗しました (item_id: 50043)
  • 17:13 トランザクションを分離する変更を id:mechairoi がしてくれたのでそれを入れて、id:t_kyt が nginx の雑なチューニングをしたのを入れて、ヤケクソで還元率を 4 にして 9,950 イスコイン。
    • あと、カテゴリのオンメモリ化もどこかのタイミングで入っている。
    • いろいろやった結果ここで落ち着いた。「リクエストに失敗しました」の謎は結局解けなかった……。
  • その後 nginx の HTTP/2 を有効にしたり、還元率を 3 に変更したりして最高 10,590 イスコイン の最終 10,070 イスコイン でフィニッシュ。
  • 終了 10 分前くらいに再起動試験をしたら、アプリの起動時に DB アクセスしていたのが失敗するせいでアプリが起動しなくなっていて、あわてて修正した。最後までがんばった。

今思うと、

  • ShipmentService の状態はこの条件以降は変わらないよね~、ということを昼飯のときに id:mechairoi が言っていたのを拾えなかったのは少し惜しかった。こういう気づきを、いかに精度よくテンポよく実装に移せるか、が ISUCON ではいつも大事。
  • ベンチの終了時に外部 API が 502 を返していたのに気づいてなかった。途中で 502 が出てて、ログがバッファリングされているのかな……されてるわけないんだけど、と思ってしまっていたのはヘンだった。。
  • 還元率でユーザの行動傾向変わるのに気づいてもよかったな~。新着をいじってもよかったな~。
  • rsync でバイナリ(と *.sql のみ)デプロイしたのはよかった。自然と motemen がデプロイ担当になっていて、そこで整流できたのもよかったかな。

試合終了、そして

参加された皆さまはすでにお気付きのとおり、パスワードを平文で保存するのはレギュレーション違反である。

終了時にはそのことを知らず、それなりにやることはやった感もあったので、18 時ごろに再起動試験を終え、少し伸びた終了時刻までの間にビールを買ってきてやれやれ、と、同じくオフィスで予選に参加していた他のチームと感想戦をしながら Twitter や Discord を眺めているうちに、違和感が……。あれ? と思ってレギュレーションを再確認したらそれに気づいてしまった。実装した当の自分が最初に気づいていたわけで、なかなかショックがでかい。

チームメンバーに告げ、当然のお通夜ムードになった中、2日目の予選通過チームが発表されると全体の上位20チームの一番下に弊チームの名前が載っていた。どうやってチェックするつもりだったかはわからないけど、運営による追試をどうやらすり抜けてしまっていたらしい。メンバーの間では、報告しよう、ですぐに意見が一致したので 941 さんに DM を送って無事失格となりました。100万円は惜しかったけど、正直に告げることを選択できるチームだったのはよかった。

これでこの話は終わりです。

カレンダーでタスクを管理することとその実装

昔からタスクを次々こなしていくことはすごく苦手で、Todoist とか Remember The Milk とか Google タスクとか Hiveminder とか! を使ってみることはあってもタスクがどんどん溜まっていく一方で、一向に捌ける様子はなく、そういう状態を続けていると TODO リストは腐っていてしまって、開くことすら億劫になってしまう。そうやっていくつものタスク管理ツールを荒廃したまま捨てていった結果、久しぶりに Remember The Milk を開いてみると学生のときのタスクがまだ残っていてウッとなったりするのもよくあることです。

そういうタスク管理ツールの何がよくないのかというと、ツールはタスクの期限を管理してくれるものは多いけれど、どのタスクをいつやるべきかということに関しては管理できないというか指示してくれない、というのが自分の性格においては問題なのらしい。自分は意志の力がないので、この並んだタスクの中から何をどんな順番でやるべきかなんてことをいちいち考えるのは嫌なのです。そうした結果、重要ではないが手っ取り早く面白そうなタスクばかりが先に消化されて、大きくて重たい問題というのはどんどん先送りにされていくアンチパターンがある。

カレンダーでタスクを管理する

一方で日々のタスクのうち達成できていることもあって、それはどういう類のものかというと、これはもう単純にミーティング。いつ始まっていつ終わるかの時間も場所も決まっているし、他人を巻き込んでいる強制力もある。どんなに意志力がないときでもカレンダーの通知があればそれにしたがって決まった場所に行くだけである。意志力はすぐに枯渇する。

自分のこの性質を利用して編み出したのは、ストレートに、カレンダーを使ってタスクを管理すること。Google カレンダーに30分単位でやることを予定しておき、あとはカレンダー通知に従って決まっていたことをやる。終わったら予定に ✓ をつけとく。それだけ。

f:id:motemen:20190620191620p:plain

青がメインのカレンダー。紫が自分専用タスクカレンダー。こんな感じになるわけです。

Slack と Fitbit の通知でケツを叩かれるイメージ。

f:id:motemen:20190620191447p:plain

運用

すばらしいアイデアだが、これをうまくやっていくにはいくつかコツが必要だった。

ひとつはメインのカレンダーを使わないこと。メインのカレンダーを使ってしまうと自分の予定を完全にブロックすることになるので、他人から見るとモテメンさん忙しそうですね……となってしまう。マネージャとしては自分の時間はできるだけ外に解放しておきたいので、他の人に見えない自分用のカレンダーにタスクを積んでいた。積んだところに新しく予定が入ったらタスクのほうをずらす。もちろん延期することができないタスクであればメインのカレンダー入れて明に自分の時間を確保する。

もうひとつは、時間の余裕を持っておくこと。調子よく30分単位で仕事を詰めていくとたしかに進んでいくんだけど、結果めちゃくちゃ疲れる。これは30分くらいで終わりそうだなーってタスクは60分で確保しておいて、余った時間は自分の自由時間にしちゃう。この自由時間ができることで、やりたくないタスクを棚上げにして、面白そうなことに時間を使ってしまうその衝動をなだめることもできる。

そして、タスクのゴールでなく細かなアクションを先に設定しておくこと。タスクを作ったときの脳内コンテキストは失われているので、〇〇を決める、じゃなくてこの URL を見て〇〇をする、などと予定の詳細に書いておく。そうすると考えるより先に行動から始められるので、初速を稼げる。そしてこの初速こそが重要である。

実装

ベストプラクティスやノウハウはソフトウェアにするというのは原理原則なので今回もそのように行った。もちろんプラットホームは Google Apps Script である。

f:id:motemen:20190620191529p:plain:h400

ウェブアプリとして実装していて、以下の簡単な機能をもっている。

  • カレンダーに追加したタスクの一覧
  • タスクの追加
    • メインのカレンダーの予定を見つつ、空いている時間にタスクの予定を追加する。
  • タスクの ✓
  • タスクのリスケジュール
    • タスクの予定を未来に設定しなおす。
    • あとからメインのカレンダーに予定が入ったときとか、タスクを消化しきれなかったときとか。

https://github.com/motemen/gas-TaskCal

デプロイ方法はREADMEにシュッと書いているとおり。

見どころはサーバサイドの関数定義をフロントエンドでも参照してるところと、カレンダー詳細へのパーマリンクの生成(https://stackoverflow.com/a/33302612)かな~。

考察

とは言うものの、いかに仕組みを整えてもそこに血が通わなければ価値がないのは世の常で、つまり元気がないと何もできない。それはもはや前提というほかなくて、それはとにかく睡眠を取ることでだけ実現できる。睡眠重要……。

参考文献として挙げるなら、以下。

ヒトはなぜ先延ばしをしてしまうのか

ヒトはなぜ先延ばしをしてしまうのか

  • 作者: ピアーズ・スティール,池村千秋
  • 出版社/メーカー: CCCメディアハウス
  • 発売日: 2012/06/28
  • メディア: 単行本(ソフトカバー)
  • 購入: 2人 クリック: 4回
  • この商品を含むブログ (8件) を見る

自分の課題に対しては半分くらいしか参考にできなかった本だけど、ここで紹介した方法はこの本の言葉で言うとこうである。

  • プレコミットメント
    • 先に自分を縛っておく方法。時間を取って、やることを決めておくのはまさにこれ。
  • 非スケジュール
    • あらかじめ余暇の時間を取っておく方法。この時間があることで、今の仕事に集中できる。ちょっと違うけど、余った時間を好きに使うことを許すのは、これに近いかな(先のタスクを余った時間にこなして、丸々自由時間にしたりもする)。

Go の関数を context 対応するツール

最初から完璧な設計と実装ができているなら苦労はないわけだけど、実際にはそうもいかない。具体的にはある程度の規模になってくると「あーこの関数 context.Context 対応したい!」みたいな気持ちが湧いてくるわけです。context 対応ってのは、第一引数に ctx context.Context を追加することですね。

そういうことをやるツールを書きました。

GitHub - motemen/go-ctxize: Rewrite functions to have "Context"s

go get github.com/motemen/go-ctxize/cmd/goctxize で、goctxize というバイナリが手に入ります。

サンプル

README やテストにある例だけど、

// $GOPATH/src/example.com/foo/foo.go
package foo

func F() {
}

// $GOPATH/src/example.com/foo/foo_test.go
package foo

import "testing"

func TestF(t *testing.T) {
    F()
}

// $GOPATH/src/example.com/bar/bar.go
package bar

import (
    "example.com/foo"
)

func bar() {
    foo.F()
}

という感じのコードにおいて、foo.F() のシグネチャが foo.F(ctx context.Context) であってほしい! という場合。

goctxize example.com/foo.F

としてやると foo.go が以下のように書き換わります。

// $GOPATH/src/example.com/foo/foo.go
package foo

import "context"

func F(ctx context.Context) {
}

// $GOPATH/src/example.com/foo/foo_test.go
package foo

import (
    "context"
    "testing"
)

func TestF(t *testing.T) {
    ctx := context.TODO()

    F(ctx)
}

ほんとは example.com/foo.F を呼び出しているほかのパッケージも書き換えたいわけなので、第2引数以降に追加のパッケージを指定してやると:

goctxize example.com/foo.F example.com/bar

bar.go もこうなります。

// $GOPATH/src/example.com/bar/bar.go
package bar

import (
    "context"
    "example.com/foo"
)

func bar() {
    ctx := context.TODO()

    foo.F(ctx)
}

コンテキストの推測

呼び出し側のファイルを書き換える際、*gin.Context みたいに context.Context を実装している変数がすでに呼び出し側のスコープに存在しているときはその変数を使ってくれます。かしこいですね。スコープ周辺の判定はけっこう雑。

// before
package web

import (
    "example.com/foo"
    "github.com/gin-gonic/gin"
)

func web(c *gin.Context) {
    foo.F()
}

// after
package web

import (
    "example.com/foo"
    "github.com/gin-gonic/gin"
)

func web(c *gin.Context) {
    foo.F(c)
}

また、コンテキスト化される関数の中に context.TODO() で初期化されている変数があったら、その変数を消し去ることもしてくれます。

// before
package bar

import (
        "context"

        "example.com/foo"
)

func alreadyHasCtxInside() { // これを ctx 化する
        ctx := context.TODO()
        _ = ctx
}

// after
package bar

import (
        "context"

        "example.com/foo"
)

func alreadyHasCtxInside(ctx context.Context) {

        _ = ctx
}

パッケージの指定

フルパスでパッケージを指定するのは煩雑なので、相対パスによる指定もできます:

# PWD=$GOPATH/src/example.com
goctxize ./foo.F ./bar ./baz

とか。go list と同じ解釈をするので、./... もいけます。

これは x/tools/go/packages パッケージによって実現されていて、作ってる途中で浦島太郎的にこの存在を知ったんだけど(Go 1.11 からの登場)、これがすげー便利。これまで静的解析をおこなうツールを書くとき、複数のパッケージにまたがって型情報を読み込みたいときには x/tools/go/loader を使うのが定石だった(と思う)のだけど、これは ./... 的な引数をうまく解釈することができない不都合があって、やや物足りなかったのです。Go モジュールに対応していることもあり、今後はこれを使うのが標準となる予定みたいですね。

どうぞご利用ください。

はてなで一緒に働きませんか?