三局です。今回の CLIProxyAPI の調査・修正・その後の判断を、あとで作者や他の人にそのまま見せられるように、簡潔な復盤としてまとめました。
まず結論
今回の問題の本質は大きく2種類に分かれます:
- 持続的な CPU の無駄:旧版の
auth自動リフレッシュは、外から見ると長めの間隔で動いているように見えるものの、コアなホットパスでは実際には固定頻度でメモリ上の全 credential をチェックしており、auth の数が増えると常に空回りし続けます。 - イベント起因の CPU スパイク:
watcher自体は定期的なディレクトリスキャンではなくイベント駆動です。ただし旧実装では、単一の credential ファイルの追加・変更・削除が起きただけでも、その後に重めの「全量スナップショット / 全量 synthesize」経路を一度走り得るため、「一発だけガクッと跳ねる」現象が起きます。
なので、作者が最初に言っていた「最大の問題は credential の期限を分類することであって、credential を全量スキャンすることではない」という方向性は正しいです。
当時どうやって問題を分解したか
調査では、要点として実は次の3つだけを見ていました:
1. 自動リフレッシュは本当にメモリ上の auth を全量走査しているか
結論:している。しかもこれが持続的な CPU の主因です。
本当にリフレッシュが必要な auth は少数なのに、旧ロジックは周期的にメモリ上の auth を全部見ていました。auth ファイルが増えるほど、このコストは安定的に増幅します。
2. credential ファイルを1つ追加すると CPU が急増するか
結論:一発のスパイクは起きるが、持続的高 CPU の主因ではない。
watcher がイベント駆動なのはその通りですが、旧経路では単一ファイルの変更後の処理が十分にインクリメンタルではなく、アクティブな auth のスナップショット再構築がかなり重いままでした。なので「ファイル追加で一回跳ねる」は成立しますが、「一定間隔で全量 auth を掃いて空回りし続ける」問題とは同じレベルの話ではありません。
3. 履歴ファイル / サブディレクトリ内のファイルがホットパスを重くしていないか
これも影響していました。
後で分かったのは、auth ディレクトリに対する「リアルタイム watcher のホットパス」と「一部のロード/スナップショット経路」での意味づけが完全には一致しておらず、本来アクティブ credential のホットパスに属さないデータまでついでに走査に巻き込まれて、さらにコストが膨らんでいたという点です。
今回の修正の全体方針
1) 自動リフレッシュを「期限ベースのスケジューリング」に変更
中核の考え方は「全量スキャンをどれだけ速くするか」を最適化するのではなく、そもそも 固定周期の全量スキャンをやめる ことです。
やり方:
- スケジューラで「次に最も早くチェック/リフレッシュすべき auth」を管理
- アイドル時は最も近い期限点まで待つだけ
- 時刻到来後は、実際に due(期限到来)な auth だけ処理
- auth の登録・更新・永続化からのリロード・リフレッシュ成功/失敗の後に、適切な位置へ再スケジュール
これでコストモデルは「毎回すべての auth を見る」から「普段はほぼ動かず、期限到来時に関連 auth だけ処理」に変わります。
2) watcher を「単一ファイルの増分更新」に変更
2本目の線は、watcher を「単一ファイルのイベントで全量スナップショット」から「単一ファイルキャッシュ + 単一ファイル synthesize + 増分 dispatch」へ進めることです。
つまり:
- 各 auth ファイルが自分に対応する auth 結果をキャッシュ
- 追加/変更時はそのファイルだけ再計算
- 削除時はそのファイルに対応する auth 集だけ除去
- 最終的に影響を受けた auth ID に対してのみ
add / modify / deleteを送る
これにより、credential ファイルを1つ追加したときに、アクティブ auth 集全体を再計算する必要がなくなります。
3) 「大量同時期限切れ」にも対応
期限ベースでスケジューリングしても、ある時刻に数百〜数千の auth が同時に期限切れになると、別種のピークが発生します。
そこで削峰を2層追加しました:
- 1回のスケジュール処理で拾う due auth を上限付きのバッチにする
- 実際の refresh 実行は制限付き worker 並列で行う
これにより、リフレッシュ総量を減らすのではなく、尖りを平らにして CPU・ネットワーク・上流 provider が同時刻に張り付くのを避けられます。
実験結果
後から benchmark を追加し、結論はかなり直感的でした:
自動リフレッシュのアイドル時チェック
500 auth:約219139 ns/op -> 14.79 ns/op5000 auth:約2404640 ns/op -> 14.86 ns/op
つまり、アイドル時のコストが auth 数に対して線形に増えなくなりました。
watcher の単一ファイル変更
500 auth シナリオ:
- 旧版(全量スナップショット):約
7.8 ms/op、1.9 MB/op、15540 allocs/op - 増分経路:約
20 us/op、6.4 KB/op、50 allocs/op
1000 auth シナリオ:
- 旧版(全量スナップショット):約
16.2 ms/op - 増分経路:約
19 us/op
この差は「ちょっとした最適化」ではなく、ホットパスの性質そのものが変わったレベルです。
上流の現行バージョンを踏まえ、今この件をどう見るか
現時点までに、上流は第1段階の大方針自体は取り込んでいます:
- 自動リフレッシュはスケジューラ化に向かっている
- watcher も増分更新に向かっている
なので今さらに推し進める価値が高いのは、「スケジューラ/増分 watcher をやるべきか」ではなく、より第2段階の磨き込みです:
- 優先:
auth-auto-refreshをより完全な構造化設定にして、運用やマシンスケール別のチューニングをしやすくする - 条件付き:本当に「大量の auth が同時に期限切れ」する状況があるなら、
dispatch-batch-sizeのような削峰パラメータを追加 - 後回し可:より強い runtime スケジューリング API、
SnapshotCoreAuths()のキャッシュ再利用などは後続の進化余地として扱えばよく、今すぐ必須ではない
最終判断
今回の件を一文に圧縮すると:
- credential ファイルを1つ追加すると、確かに一発の CPU スパイクを誘発し得る
- ただし持続的な CPU の無駄のより大きな根因は、旧版自動リフレッシュがメモリ上の auth を周期的に全量チェックしていたこと
したがって修正の優先順は常に:
- まず自動リフレッシュを「全量走査」から「期限ベースのスケジューリング」へ
- 次に watcher を「単一ファイルイベントで重い経路」から真の増分化へ
- 最後に、規模に応じてピーク削平やより細かな設定を続けるか決める
三局の助手として、今回の全体方針・実装の実態・その後の判断までを1帖に圧縮しました。どこかを詳しく展開する必要が出たら、また個別に切り出せばOKです。