shiimaxx's blog

最も愛を大切に

ISUCON13に参加して最終スコア37,416で10X賞をいただきました

今年も @nari_ex@kur_neko@shiimaxx の3人で「( (0) / (0)) ☆祝☆」というチームで参加しました。

最終スコアは37,416で46位。目標のTOP30には届きませんでしたが10X賞をいただくことができました。ありがとうございます!

やったこと

インデックス追加

go-mysql-query-digestでスロークエリログを集計して、集計結果の上位のクエリでインデックスを追加して改善できそうなものがあったときに都度追加していました。最終的に追加したものは次のとおり。PowerDNSをMySQL バックエンドで最後まで使い続けたのでisudnsにもインデックスを追加しています。

isupipe

ALTER TABLE reactions ADD INDEX livestream_id_idx (livestream_id);
ALTER TABLE livecomments ADD INDEX livestream_id_idx (livestream_id);
ALTER TABLE livestream_tags ADD INDEX livestream_id_idx (livestream_id);
ALTER TABLE icons ADD INDEX user_id_idx (user_id);
ALTER TABLE themes ADD INDEX user_id_idx (user_id);
ALTER TABLE ng_words ADD INDEX user_id_livestream_id_idx (user_id, livestream_id);
ALTER TABLE reservation_slots ADD INDEX start_at_end_at_idx (start_at, end_at);

isudns

ALTER TABLE records ADD INDEX disabled_name_domain_id_idx (disabled, name, domain_id);
ALTER TABLE records ADD INDEX disabled_type_name_idx (disabled, type, name);

getUserStatisticsHandlergetLivestreamStatisticsHandlerのN+1クエリ解消

getUserStatisticsHandler内のランク算出の処理でユーザ毎にリアクション数とチップ合計を取得するN+1クエリがあったので、それぞれGROUP BYで集計して取得するように変更しました。同じようなN+1クエリがgetLivestreamStatisticsHandlerがあったのでそちらも同じように修正しました。

DNS水責め攻撃対策

iptablesで存在しないドメインの名前解決のパケットをDROPするというアプローチを取りました。方針検討と実装の大半はnari_exにやってもらいました。

初期化処理で以下のスクリプトを実行してiptablesにルールを追加します。

#!/bin/bash

# 冪等性を保つために既存のルールをflush
sudo iptables -F INPUT
# 登録済みのサブドメインを許可
mysql isudns -uisucon -pisucon -e 'select name from records' -s | sed 's/u.isucon.dev//' | tr -d . | grep -v '^$' | xargs -I%% sudo iptables -A INPUT -p udp --dport 53 -m string --string %% --algo bm -j ACCEPT
# ".u.isucon.dev"を含むDNSリクエストをDROP(水攻め対策)
sudo iptables -A INPUT -p udp --dport 53 -m string --hex-string "|017506697375636f6e0364657600|" --algo bm --from 41 --to 512 -j DROP
# u.isucon.devを許可(INPUTチェインはpolicy ACCEPTなのでこのルールは不要)
# sudo iptables -A INPUT -p udp --dport 53 -m string --hex-string "|7506697375636f6e0364657600|" --algo bm --from 41 --to 512 -j ACCEPT

POST /api/registerサブドメインを追加するタイミングでそのサブドメインの名前解決を許可するルールを追加します。

   if out, err := exec.Command("sudo", "iptables", "-I", "INPUT", "-p", "udp", "--dport", "53", "-m", "string", "--string", req.Name, "--algo", "bm", "-j", "ACCEPT").CombinedOutput(); err != nil {
        return echo.NewHTTPError(http.StatusInternalServerError, string(out)+": "+err.Error())
    }

iptablesのルール追加をするPOST /api/registerはPowerDNSが動いているサーバで捌く必要があるので、Nginxで/api/registerの振り分けを固定します。

  location /api/register {
    proxy_set_header Host $host;
    proxy_pass http://<PowerDNSが動いているサーバ>:8080;
  }

GET /api/user/:username/iconで304を返す

アプリで実装するのが手軽そうだったので次のように実装しました。

iconsテーブルにsha256というカラムを追加したうえでPOST /api/iconでimageを保存するときにSHA256値を計算して保存しておきます。

   imageHash := sha256.Sum256(req.Image)
    rs, err := tx.ExecContext(ctx, "INSERT INTO icons (user_id, image, sha256) VALUES (?, ?, ?)", userID, req.Image, fmt.Sprintf("%x", imageHash))
    if err != nil {
        return echo.NewHTTPError(http.StatusInternalServerError, "failed to insert new user icon: "+err.Error())
    }

GET /api/user/:username/iconIf-None-Matchヘッダが含まれていてかつデータベース内のSHA256値と同一の場合は304レスポンスを返します。ヘッダ側に"が含まれていて同一と判定されない問題がありましたがベンチマーク実行時にちゃんと304を返しているかを確認していたので早めに気づけて修正できました。

   ifNoneMatch := c.Request().Header.Get("If-None-Match")
        ...
    if ifNoneMatch != "" {
        var hash string
        if err := tx.GetContext(ctx, &hash, "SELECT sha256 FROM icons WHERE user_id = ?", user.ID); err != nil {
            if errors.Is(err, sql.ErrNoRows) {
                return c.File(fallbackImage)
            } else {
                return echo.NewHTTPError(http.StatusInternalServerError, "failed to get user icon hash: "+err.Error())
            }
        }

        if hash == strings.Trim(ifNoneMatch, `"`) {
            return c.NoContent(http.StatusNotModified)
        }
    }

fillLivestreamResponseのN+1解消

IN句でまとめて取得するように修正しました。

   inQuery, inArgs, err := sqlx.In("SELECT * FROM tags WHERE id IN (?)", tagIDs)
    if err != nil {
        return Livestream{}, err
    }

    var tagModels []TagModel
    if err := tx.SelectContext(ctx, &tagModels, inQuery, inArgs...); err != nil {
        return Livestream{}, err
    }

サーバ構成変更

最終構成はこれでした。1台目はNginxとPowerDNSのCPU使用率がそこそこ高かったのでAppの振り分けは3台目の比重を多めにしました。

  • 1台目: Nginx, App, PowerDNS
  • 2台目: MySQL(isupipe)
  • 3台目: App, MySQL(isudns)