ISUCON12に出て予選敗退した

id:yashigani_wさん, id:wtatsuruさんと、「デジタルトランスフォーメーションズ」というチームでISUCON12に出たけど、振り返ってみたらほぼ初期データを弄って終了してしまったな、という振り返りエントリ。何を振り返ったらええねんと思いながら過ごしていたら時間が経ってしまった。 そういえばチーム名の最後に2022ってつけたいと思ってたのを今思い出した。

当日やったこと

我々のチームでは15分スプリントというのをやっていて、「15分作業 → 5分スプリント会」みたいなサイクルを回していた。これはwtaturuさんが去年のチームでやっていたらしく、今年もやることにした。ということでスプリント会の記録がissueに残っている

https://github.com/tatsuru/isucon12-yosen/issues/3

終盤少しグダッとした場面もあったが、それでも18スプリント回せていたのはよかった

初期データをつくる

手を動かしていた場面はほとんどこれで終わってしまった...

  • 初回のベンチを回した結果を眺めていて、rankingのエンドポイントが遅いので、そこから直そうとなった
  • とはいえどういう作戦にするかをパッと決めることはできなくて、どうやって手を入れるかの材料を集めるのに結構時間がかかってしまった。最終的には以下の作戦でよかろうという話になる。
    • 初期データではplayer_scoreから一つのユーザーが複数のスコアをもっていたので、これを各ユーザー最新の一件のみになるようにデータを作り直す
    • データを入れる時も最新の一件だけになるように
    • player_scoreをひく時は単純にORDER BY scoreでソートするように
  • rankingが遅いのでranking作るところだけ直すか、と話していたところ、結局データを入れるところも直さないと意味がないよねとなり、セットで修正することに
  • 最初は/initializeでこの辺りの処理全部やってしまうつもりで進めていたが、一度ベンチマークを試したら初期化処理に落ちてしまって、作戦を少し変える
    • 振り返ってみるとこの時どういう理由で落ちたかあんまり見てなかったのはよくなかった
  • スコアに関するデータはSQLiteのDBに乗っていて、実体としてはテナントごとに分割されたファイルなので、初期データを手元で作り直してそれをホストに置く作戦にした
  • 各ユーザーの最新のスコアだけ取り出すのは以下のSELECT文で取ってきていた(たぶん合ってると思う)
SELECT * from player_score WHERE competition_id = ? GROUP BY player_id HAVING MAX(row_num);
  • ちなみに初期データを作り直すのもGoで書いた。と言っても元データ取り出してSELECTでフィルタリングして別のDBに書き込むだけなので難しいことはしていない
  • これでplayer_scoreのレコード数が大幅に減った
    • 一番多いところで160万レコードくらいあったのが8万レコードくらいになった
  • 一方でこの辺りでミスが増え始める
    • player_scoreだけ新しいDBに移してしまい、その他のテーブルを移し忘れて/initializeに落ちる
    • 100テナントあることはわかっていたけど、forの条件普通にミスって99テナント分しか移しておらず、やはり/initializeに落ちる
      • 全テーブル移したのになんで落ちるんとか思っていたけど、ちゃんとログ出したら原因判明したし、わかった後はあまりにも恥ずかしくてすまん・・・すまん・・・となっていた
    • 他にもあった気がするけど忘れてしまった。ただ修正→確認のサイクルの時間が長くてだいぶ時間使ってしまった

メモリリークの修正

  • 初期データを作っている横でメモリ使用量が膨らんでパフォーマンスが落ちる問題が起きる
    • 完全に勘だったけど、メモリを使いまくるような処理はcsvの読み込みじゃないのと思ったので、アクセスの傾向を改めて眺めたところ、csvによるスコア登録のリクエスト数が初期実装と比較して増えていることに気づく
    • ここを少し修正してベンチが最後まで通るようになったあたりで概ね時間終了
    • github.com

ふりかえり

  • id:wtatsuruさんも書いていたが、とにかく手が遅い。去年も遅いなと思っていたけど改めて痛感した
  • 与えられた環境で勝負しすぎた。仕事でやったとしてもさっさとSQLiteからMySQLに移行しましょうと言うと思う
    • DB移行は頭には浮かんだものの時間かかりそうだからって後回しにしてしまった
    • 最終的にはどうせやるだろう作業は最初にやるのがよい 
  • ログ出すのも困ったらすぐにやるべきだった
  • 三人で集まって会話しながら作業できたのはよかった
    • ディスプレイを2台置いて、常に片方がペアプロしてるような状況を作れた
    • ホワイトボードで絵を描いて意見を擦り合わせたりがやりやすかった
    • 15分スプリント、5分振り返りで18スプリント回したけど、そうすると会話の時間は90分あることになるが、これは少し長いかもしれないし、そんなことないかもしれない
  • 悔しい結果とはなったけどシンプルに楽しかった
    • 今回は一番ネックになっていそうな部分を順に直していきましょうというのだけ決めていて、その通りに動けたのはよかった

来年また出直します...運営の皆様お疲れ様でした & ありがとうございました。 

メンバーの振り返り

yashigani.hatenablog.com

wtatsuru.hatenadiary.com

2021年

良くも悪くも子供が中心だった一年だったように思う。子供が楽しそうにしてるのを見たくて休みにあれこれ計画したり、子供が体調を崩すと仕事ができなくて家の中がめちゃくちゃになったりというのを繰り返していたら一年が終わった。

本当は住む場所を変えたりしたかったのだけど、これも子供の都合で少し先送りすることにした。つぎに引っ越すときは地元へ帰ることも検討しているけど、まあもう少し先になりそう。

2022年にはなにか新しいことも始めてみたい。

AWS S3バッチオペレーションのちょっとしたtipsなどのご紹介

このエントリは、はてなエンジニアAdvent Calendarの9日目の記事としてかかれました。

AWS S3にはバッチオペレーションというマネージドサービスがあって、これは指定したバケット/オブジェクトに対して一括で何かしらの操作ができる。例えば「バケット内のすべてのオブジェクトを別バケットにコピーしたい」とかそういう時に使うと便利。

aws.amazon.com

その一括操作ではLambdaを利用することもできる。Lambdaを使うとかなり柔軟な操作ができるようになるが、ドキュメントを見ただけでは最初どうしたらいいかわからなかった上に、利用する機会もそんなに無いので覚えられない。その他にも最初に知ってたらよかったみたいなのが細々とあるので、そういうのを少しまとめておく。

なお、このエントリではS3 バッチオペレーション自体のジョブの登録のやり方自体は割愛する。まずS3バッチオペレーションがどういうものか知りたいという人には、以下のエントリが雰囲気を掴むのに良いと思う。

dev.classmethod.jp

マニフェストファイル

バッチオペレーションのジョブを登録するにはどのオブジェクトを操作するのかを指定するためのマニフェストファイルが必要になる。マニフェストファイルにはS3インベントリレポートというのを使うと良いというのをよく見るけど、実は自分で作ったCSVを使うこともできる。フォーマットとしては以下のような感じ。

{bucket},{object_key}

つまりバケットと操作したいオブジェクトのキーの一覧だけでよい。最初から操作したいオブジェクトがわかっている場合はインベントリレポートを出力せずとも上のフォーマットのCSVを作ればよいし、インベントリレポートを全部チェックした上で、一部の操作対象を出力する、とかでもよい。初回のインベントリレポートの出力には少し時間がかかる(24h程度?)ので、操作対象が完全にわかっている場合は自分でCSVファイルを作ったほうが実は手早いなどはありそう。

S3 バッチオペレーションからLambdaへのリクエス

Lambdaへは以下のようなJSONがリクエストされる(ドキュメント から引用)

{
    "invocationSchemaVersion": "1.0",
    "invocationId": "YXNkbGZqYWRmaiBhc2RmdW9hZHNmZGpmaGFzbGtkaGZza2RmaAo",
    "job": {
        "id": "f3cc4f60-61f6-4a2b-8a21-d07600c373ce"
    },
    "tasks": [
        {
            "taskId": "dGFza2lkZ29lc2hlcmUK",
            "s3Key": "customerImage1.jpg",
            "s3VersionId": "1",
            "s3BucketArn": "arn:aws:s3:us-east-1:0123456788:awsexamplebucket1"
        }
    ]
}

tasksは配列になっているが、実際には要素は一つしかない。Lambdaでどういう操作をするかにもよるが、操作するにあたって重要なのはs3Keyとなる。これを利用して、例えばObjectをGetしてきて加工して別のバケットにPUTしたりする。Lambdaからのレスポンスは以下のようなものを返す(こちらもドキュメントから引用)

{
  "invocationSchemaVersion": "1.0",
  "treatMissingKeysAs" : "PermanentFailure",
  "invocationId" : "YXNkbGZqYWRmaiBhc2RmdW9hZHNmZGpmaGFzbGtkaGZza2RmaAo",
  "results": [
    {
      "taskId": "dGFza2lkZ29lc2hlcmUK",
      "resultCode": "Succeeded",
      "resultString": "[\"Mary Major", \"John Stiles\"]"
    }
  ]
}

こちらもresultsは配列となっているが、実際には要素を一つだけ返せばよい。results内のresultCodeにはSucceeded/TemporaryFailure/PermanentFailureのどれかを指定する。ジョブのレポートを出力して、オブジェクトごとに処理の成否を確認したいような場合は成否に合わせてSucceeded/PermanentFailureを指定し、resultStringに成功した場合は成功メッセージを、失敗した場合は失敗理由がわかるようなメッセージを埋めておき、レポートを精査するのが良い。

個人的にはresultStringにはJSON文字列を埋めておくのがよいと考えている。大量のオブジェクトを操作した結果について精査する場合、レポートのCSVを何かしらのスクリプトで処理した上でresultStringを確認することになるはずで、その時にJSONであれば扱いやすいと思う。

また、レスポンスの以下のフィールドにはリクエストのJSONに含まれる値をそのまま利用するとよい。あと特に詳細には触れなかったけど、バッチオペレーションからLambdaを実行するとか、LambdaからS3にアクセスするための権限設定はそれぞれ必要になる。

  • invocationSchemaVersion
  • invocationId
  • results.taskId

特にresults.taskIdはバッチオペレーションのジョブで一意になっていないと失敗してしまうので、リクエストされたJSONに含まれている値をそのまま使うのが一番安全。 これ以外にtreatMissingKeysAsフィールドがあり、これは特にドキュメントに記載されていないが、例えば先程のresults.taskIdがジョブで一意になっておらず失敗した場合、ここで指定されたレスポンスコードが利用される。下のメッセージはresults.taskIdが一意ではなかった場合にレポートに出力されていたもの。

"Lambda function didn't return the tasks/keys in the function response, using ""treatMissingKeysAs"" (default to TemporaryFailure) as error code: PERMANENT_FAILURE"

ここにあるように、デフォルトだとTemporaryFailureが使われるけど、ここにはPermanentFailureを指定しておけば良いと思う。

任意の値をLambdaに渡したい

任意の値をLambdaに渡す方法も用意されている。バッチオペレーションのジョブを登録する時に操作するオブジェクトのバケットとキーを指定するためのCSVファイルを渡すことになるのだけど、そのCSVのキーの位置にURLエンコードしたJSON文字列を入れるとよい。以下のドキュメントに記載されている。

docs.aws.amazon.com

例えば次のようなJSONをLambdaに渡したいとする。

{
  "s3Key": "1aaaa.txt",
  "newKey": "new-1aaaa.txt"
}

この場合、バッチオペレーションのジョブに渡すCSVファイルは以下のような感じになる。

tkzwtks-s3,%7B%22s3Key%22%3A%221aaaa.txt%22%2C%22newKey%22%3A%22new-1aaaa.txt%22%7D
tkzwtks-s3,%7B%22s3Key%22%3A%222aaaa.txt%22%2C%22newKey%22%3A%22new-2aaaa.txt%22%7D
tkzwtks-s3,%7B%22s3Key%22%3A%223aaaa.txt%22%2C%22newKey%22%3A%22new-3aaaa.txt%22%7D
tkzwtks-s3,%7B%22s3Key%22%3A%224aaaa.txt%22%2C%22newKey%22%3A%22new-4aaaa.txt%22%7D
tkzwtks-s3,%7B%22s3Key%22%3A%229aaaa.txt%22%2C%22newKey%22%3A%22new-9aaaa.txt%22%7D

このCSVは1つ目のフィールドにバケット名、二つ目のフィールドにURLエンコードしたJSON文字列が入っている。これをジョブの登録時に渡すと、Lambdaには以下のようなJSONがリクエストされる。

{
    "invocationSchemaVersion": "1.0",
    "invocationId": "YXNkbGZqYWRmaiBhc2RmdW9hZHNmZGpmaGFzbGtkaGZza2RmaAo",
    "job": {
        "id": "f3cc4f60-61f6-4a2b-8a21-d07600c373ce"
    },
    "tasks": [
        {
            "taskId": "dGFza2lkZ29lc2hlcmUK",
            "s3Key": "%7B%22s3Key%22%3A%221aaaa.txt%22%2C%22newKey%22%3A%22new-1aaaa.txt%22%7D",
            "s3VersionId": "1",
            "s3BucketArn": "arn:aws:s3:us-east-1:0123456788:awsexamplebucket1"
        }
    ]
}

JSON内のs3KeyにはCSVファイルの2つ目のフィールドのURLエンコードされたJSON文字列が入っているため、これをデコードすることで外から任意の値を渡すことができる。実際のコードは以下の通り(このコードではオブジェクトに対して実際に何か操作をしているわけではない点には注意)。

console.log('Loading function');

const _sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
exports.handler = async (event, context) => {

    var date = new Date();
    console.log('invoke: ' + date.toISOString());

    const task = event.tasks[0];
    const payload = JSON.parse(decodeURIComponent(task.s3Key));
    await _sleep(3000);

    const message = payload.newKey;

    return {
        invocationSchemaVersion: event.invocationSchemaVersion,
        treatMissingKeysAs: 'PermanentFailure',
        invocationId: event.invocationId,
        results: [{
            taskId: task.taskId,
            resultCode: 'Succeeded',
            resultString: message
        }]
    };
};

使い所が難しいが、例えばオブジェクトを全然違う名前にリネームしたい場合に、元のkeyと新しいkeyを渡してリネームする、みたいな場合に使うと良いと思う(実際そういう場面で使った)。リネームに近いけど、オブジェクトを整理するために、移動先のディレクトリを指定する、みたいな使い方もできると思う。

余談

ここまで試していて気づいたのだけど、実はS3 バッチオペレーションでLambdaを使うような場合、(このエントリを書いている時点では)実際にオブジェクトが存在しなくてもよく、マニフェストファイルさえ作れたらよい。つまりS3オブジェクトとは全く関係のない、任意のJSONをURLエンコードしてマニフェストファイルを作ってバッチオペレーションのジョブを登録するだけで大量にLambdaを実行させまくることができる。どういう場面で使えるかは全然わからないけど・・・

Lambdaの同時実行数

実際にLambdaを使うようなジョブを実行する場合、Lambdaの同時実行数には注意が必要。Lambdaからレスポンスが返って来たら次のを実行するという感じではなく、レスポンスが返る前に次から次にLambdaを実行していくため、油断していたらLambdaの同時実行数の制限に引っかかってしまう、ということも起こりそう。特にS3 バッチオペレーション以外でもLambdaを普通に使っているようなサービスの場合、通常のサービスに影響しうるので注意したほうが良いと思う。ちなみに上のコードで実際に10000件のオブジェクトに対してバッチオペレーションを実行した場合、LambdaのConcurrent executionsがMAX250くらいまで行った。

バッチオペレーションの使用感

「大量のオブジェクト操作をできるだけ素早く終わらせたい」というのがこれを使う一番のモチベーションだったため、実際に使ったときの処理時間をざっくり書く。

  • 27000件のオブジェクトコピーはほぼ一瞬で終わる
  • 2000万件のオブジェクトコピーは7時間弱で終わる
  • 500万件のLambdaを利用したオブジェクトのリネームは2時間程度で終わる

ということで、何をやるにも結構早く終わらせることができる。大量のオブジェクトを操作する時に自分でバッチ処理を書いたりすると考えることが多い。並列実行の仕組みを用意しない場合、量が多ければ多いほど処理時間は増えていくだし、失敗した場合のリカバー方法も検討する必要がある。S3バッチオペレーションを利用する場合、そういう心配が完全には消えないものの、バッチ処理を書かなくてもよくなるため、だいぶ緩和されると思う。特に処理時間が短くなれば精神的にもだいぶ楽になる。レポートを出力しておけば、後からは失敗したものだけ再実行する、というのも簡単にできる。標準でできる処理は少ないが、Lambdaを使って様々操作できるのはいい。準備は若干面倒な部分もあるし、常用するのには向いていないが、いざというときには検討してみてもいいと思う。

まとめ

ということでS3バッチオペレーションを使う上で最初に知ってたらよかったみたいなポイントをまとめた。S3バッチオペレーション自体は大技って感じで使うタイミングはなかなか無いけど、大量のS3オブジェクトに何かしたい場合にはぜひ検討してみてほしい。

明日のはてなエンジニアアドベントカレンダーの担当はid:stefafafanさんです。よろしくおねがいします!

このゴール、というよりはゴールセレブレーションのことははっきり覚えている。 当時珍しくテレビで放送されていたので(今みたいに全試合生放送されていたわけではなく、見に行かなかった試合のゴールはダイジェストで見るしかなかった)かじり付きで観ていた。 たぶん家で夕食を食べる直前くらいだったと思う。「もう夕食だぞ」と言われたけど、食卓につくとテレビの位置的に試合が観られなくなってしまうので、生返事しながら観ていたと思う。

ぼんやりと覚えているのは、このペットボトルの水が「実は名古屋の選手用の水ではない」というエピソードで、たぶん翌日の新聞に書いてあったか、その日の実況が言っていたような記憶がある。 しかしこれが25年前なの衝撃的だな・・・あとピクシーをはじめこの試合に出ている選手が例えば監督として何年もやってたりして歴史を感じる。

普通の日記

4月に入って以降、週末はかなりゆっくり過ごせている。

例年だったらサッカーシーズン真っ只中で、土日のどちらかは家でサッカー見る、もしくは遠征してるとかのはずだけど、今はサッカーがない。 3月までは仕事がはちゃめちゃに忙しくて、これが終わったらどこか旅行でも行くんやと思ってたけどそれもできずで、ずっと家にこもっている。

3月まで全然休みを取っていなかったこともあり、有給とか少しづつ使うとでめちゃくちゃ時間が余るので、積んでた本をちょっと読んでみるとか、部屋の片付けするとか、そういうことに時間が使えている。 これはこれでいい。

雨が降ってなければ一日一度は散歩することにしていて、家の近所をぶらぶらしてる。 気になっていた店がどんどんテイクアウト始めていて、正直大変そうだなと思うけど、個人としては気軽に試せて嬉しい、という気持ちもある。 少しづつ開拓したい。

isucon9の予選に出て敗退した

isucon9の予選に参加して、今年も予選敗退してしまったのでその記録を残しておく。今年で4回目で、id:hitode909 さん、id:side_tanaさんと「ミッシングマグネティックストレージ」という名前のチームで出た。最高スコアは6720イスコインで、最終スコアは5120イスコインだった

振り返ってみたけど自分はほとんど何もやってない。もう少し手を動かす必要があった。

準備

締め切り1週間くらい前にチームを組んで、予選1週間くらい前に練習をした。レギュレーションを読んだり、言語どうしましょうねとか、あと最初にこういうことやりましょうとかそういうことを話して、isucon8の予選問題で軽く試したりした。ここ数年はAzureとかConoHaとか使われているものの、だいたい「サーバにSSHでログインしてあとはいい感じにする」みたいなムーブになってしまい、出題環境となるクラウドサービスを特に触らずに終わってしまう。今年はなにか触ってみたいなと思って、どうせNginxのログとか集めたくなるのでLogServiceというのを使ってみることにした。それ以外は「デプロイはrsyncでやりましょう」「sshの鍵を事前に集めておきましょう」という話をして、Alibabaのテスト用インスタンス無事立てられましたね〜とかをやった。

前日は明日最初にだれが何をやるのかというのを決めたり、RAMユーザーをチームメンバー分発行したり、あとはテンション上がりそうなご飯はどこで注文しましょうかとか話して解散した。前日は飲酒禁止ですとか言っていたのに、その後うっかりビールを飲んでしまった。

LogService

なんとなくLogServiceのドキュメント眺めてたら便利そうだったので使った。この辺りにNginxのログを集めて分析する方法が書いてあったので導入時に困ることもなかった。

jp.alibabacloud.com

「よく使う集計クエリ」を保存しておけるのがよかった。ベンチマーク走らせたあとに各自がそれぞれの画面でログの集計結果を眺めたり、好きなクエリで集計できそうだなと思ったので導入した。もちろん、こういうのはkibanaとか使ってもいいのだと思う。とにかく今年はいつもと違うことをやってみたかった。 f:id:tkzwtks:20190909225629p:plain

当日

当日どういうことをしたかはhitode909さんのブログにある通り。

blog.sushi.money

今年はアプリケーションの改善に集中したい、ということを練習時から話していて、実際みんなでそういう動きをできたのは良かった。hitode909さんの細かい修正は少しづつスコアを上昇させたし、side_tanaさんが導入したCloud Traceはアプリケーションのボトルネックを探すにはかなり役に立った。

tana.hatenablog.com

最初にも書いたけど、僕はあまり手を動かすことができずに、最初やる予定だったタスクをやったあとは、このサービスはどういうアプリケーションか、みたいなことをずっと考えたり、ペアプロに参加したりしていた。/buy のエンドポイントが遅いものの、ここを改善しないとポイントが上がらないと考えていたので、どう改善するかということを考えていたけど、結局解決案を見つけられなかった。サービスで実際にユーザーがどういう動きをするのだろうかというモデルをうまく作れたらもう少し改善できたのかもしれない。普段から触れているアプリケーションなら「この画面のあとはここに遷移するはずだから、この処理はあとでもいい」みたいな解決案は出せるかもしれない。しかしisuconみたいに初めて触れるアプリケーションでそういうことをやるのは難しい、というのは改めて実感した。こういうのが得意な人もいるのかもしれない。次回は(ベンチマーカーが模している)ユーザーがどういう動きをしているか、というのを最初に考えるといいかもしれない。

/buyにこだわりすぎたのもよくなかった。「/loginが遅い」という話はしていたものの、今回のスコア計算は「売ったイスの総額」になるということだったので、とにかく購入エンドポイントを解決するのが先決だと思っていた。が、/loginを素早く捌けるようになればより多くのユーザーが購入までたどり着けるようになるはずで、もしかしたらスコアを上げることができたかもしれない。ここを改善するための案はそこそこ早く話し合えたものの、タイムアップまでの時間があまりなくて試すこともできなかった。

アプリケーションの改善にこだわりすぎて、結局サーバーを1台しか使えなかったのも反省点だと思う。まずはアプリケーションのボトルネックをある程度解消しないとホストは増やせないと思っていたが、最終的には増やしたいはずだったので、最初から3台でやればよかったのかもしれない。同様にキャンペーンレベルを最初からMAXでやればよかったと思う(キャンペーンレベルは一度MAXにしてみたが、その結果スコアががくんと下がったのでその後日和ってしまったのだった)。あと最初に書いたけど、やっぱりもう少し手を動かす必要があった...

反省点はあるものの、今年は「どういうアプリケーションか考えながら改善したい」というつもりだったので、一貫してそういう話ができたのは満足感がある。問題のボリュームは多いなと思ったけど、考えることもたくさんあって楽しかった。あとisuconは普段の仕事と違う人とチームを組めるのがよい。普段一緒に仕事していないメンバーと課題に取り組むのは新鮮な感じがして楽しかった。出題者、運営の皆様お疲れさまでした。来年こそは本戦に出たいと思いつつ、しばらくは反省の日々を過ごします...