テック技報最新号へ →
コード

Go: Argon2id のハッシュ計算はセマフォで同時実行数を絞る(ログイン集中でメモリが溢れるのを防ぐ)

Argon2id はハッシュ1回ごとにパラメータ m 分のメモリを確保する。ログインが同時に来るとその分だけ積み上がるので、バッファ付きチャネルで同時に計算する数に上限を付ける。

@amitsinha__07 · 投稿 2026-11-02
#go#argon2#password#concurrency#semaphore#memory

何が起きるか

m=64 MiB で設定した場合、同時に処理中のログイン1件につき 64 MiB を使う。投稿者の負荷試験では、128 件を並列に流すと約 7.5 GB のメモリを使った一方、処理できたのは毎秒約 75 件で、8 件並列のときと変わらなかった(数値は投稿者の環境での測定)。

CPU を使い切った時点で、それ以上並列にしても速くならず、メモリだけが増える。ログインの集中やパスワード総当たり攻撃のときに OOM でプロセスが落ちる原因になる。

やり方

バッファ付きチャネルをセマフォとして使い、ハッシュ計算の入口で枠を取る。待ちが長くなりすぎないよう、リクエストの context でも抜けられるようにする。

go
import (
    "context"
    "runtime"

    "golang.org/x/crypto/argon2"
)

// 同時に計算する数。目安は NumCPU/4 程度から負荷試験で決める
var hashSlots = make(chan struct{}, max(1, runtime.NumCPU()/4))

func hashPassword(ctx context.Context, password, salt []byte) ([]byte, error) {
    select {
    case hashSlots <- struct{}{}: // 枠を取る
    case <-ctx.Done():
        return nil, ctx.Err() // 待ちきれなければ 503 などで返す
    }
    defer func() { <-hashSlots }() // 枠を返す

    // time=1, memory=64*1024 KiB(64 MiB), threads=4, keyLen=32
    return argon2.IDKey(password, salt, 1, 64*1024, 4, 32), nil
}
  • ピーク時のメモリは「上限 × m」で見積もれるようになる(上の例なら NumCPU/4 × 64 MiB)。
  • golang.org/x/sync/semaphore の Weighted を使っても同じことができる(Acquire(ctx, 1) / Release(1))。

注意点

  • 上限の目安(NumCPU/4)は投稿者の提案。threads(並列度 p)の値やほかの処理との兼ね合いで変わるので、自分の環境で負荷試験をして決める。
  • 枠待ちが長いとログイン全体が遅くなる。待ち時間の上限(context のタイムアウト)と、ログイン試行のレート制限を合わせて入れる。
  • パスワード照合(保存済みハッシュとの比較)も同じ関数を通すこと。照合側だけ素通しだと意味がない。比較には subtle.ConstantTimeCompare を使う。
  • パラメータ(m / t / p)の選び方は security 分野の パスワードハッシュは「アルゴリズム+コストパラメータ」まで決める(Argon2id / scrypt / bcrypt / PBKDF2) を参照。
  • Go 1.21 以降は組み込みの max が使える。それより前なら自前で書く。

出典

X の元の投稿を開く