目的
GPTとかClaudeに教えてもらったり、いろいろ調査したことで、勉強になったことをメモして、将来見返す。
FQ制限
直近1時間以内に送った人には、配信をしないみたいな制約がたまにある。
これは、Redis(SET NX EX) で配信直前に、存在有無の確認を行う。TTLは1時間にしておく。
そこまで、高負荷でない & 運用楽にしたい場合は、DynamoDBの ConditionalCheck でOK。
in-flight request deduplication
例えば、DuckDB の インスタンスを、処理の中で作りたいけど、そこに並列で同じようなリクエストが来て、作成が競合するってことがある。
そんな時に以下の実装をすると、最初のリクエスト以外は、最初のリクエストが終わるまで待機して、終わったら最初のリクエストが作ったインスタンスを使うみたいなことができる。
let p = loadingTables.get(key);
if (!p) {
p = (async () => { await createTable(entry, csvPath, tableName); })()
.finally(() => loadingTables.delete(key));
loadingTables.set(key, p);
}
await p;
テナント移動時のアラート
SaaSで複数テナントを持つユーザーが、二つのタブを開いていて、片方のタブでテナントを変更した場合、もう片方のタブでの表示は、変更前のテナントが表示されて、運用ミスるってことがよくある。
その場合、BroadcastChannel を使って、別タブに状態を伝えるのが手っ取り早い。すごく便利。
export const useAuthChannel = () => {
// 認証状態を管理するstate。実際のアプリではContextやReduxなどから取得する
const [isAuthenticated, setIsAuthenticated] = useState(true);
// ログアウト処理
const logout = useCallback(() => {
console.log('ログアウト処理を実行し、メッセージを送信します。');
// ここでAPIサーバーへのログアウト通知などの本来の処理を行う
// 他のタブにログアウトを通知
channel.postMessage('logout');
// このタブの認証状態も更新
setIsAuthenticated(false);
}, []);
useEffect(() => {
// 他のタブからメッセージを受信したときの処理
const handleMessage = (event) => {
if (event.data === 'logout') {
console.log('別タブからログアウト通知を受信しました。');
setIsAuthenticated(false);
}
};
// イベントリスナーを登録
channel.addEventListener('message', handleMessage);
// コンポーネントがアンマウントされるときにクリーンアップする
return () => {
console.log('クリーンアップ:イベントリスナーを削除します。');
channel.removeEventListener('message', handleMessage);
// 注: アプリケーション全体で一つのチャンネルを使い続ける場合、
// ここで channel.close() を呼ぶと他のコンポーネントで使えなくなる可能性があります。
// アプリが完全に閉じられるときに一度だけcloseするのが理想です。
};
}, []); // 空の依存配列で、初回レンダリング時に一度だけ実行
return { isAuthenticated, logout };
};
DynamoDBの注意点
書き込みが同一Partitionだと、1,000 WCUs/秒が限界
読み込みも、同一Partitionだと、約3,000 RCUsが限界
例えば、上記のConditionalCheckのように、partitionKeyが分散できるものは便利だけど、SaaSで提供している場合、siteIdでPartitionKeyを設定したいと言った時に設計が難しい。
KafkaConsumerの高速化
KafkaのMessageを処理する際は、1件づつ取得する方法もあるが、もちろんBatchで取得して、Promise.all で並列処理すれば大体早い。
けど、それでも足りない場合がある。外部のAPIにアクセスする時など。
その場合は、外部のAPIにアクセスする部分だけを、await をつけずに渡して、次のメッセージを処理しにいくと早い。
ただ、これの問題として、サーバーが落ちた時に、後ろに溜まった APIアクセス部分が落ちたり、メモリが増加したりするデメリットがありうる。
また、APIアクセスをテスト環境で、Sleepなどで代替しようとすると、並列処理数が、実際のAPIアクセスの時と異なる値になる可能性が高い。
そのため、同時並列数を抑えるか、実APIにアクセスできるようにするなど、パフォーマンステストの方法を考える必要がある。
Kafkaの失敗時
Kafkaで処理失敗時に、ロジック内のリトライで済めば良いが、そうでない場合が難しい。
GPTに聞いたところ、DLQに入れて、後から再処理というのが一般的とのことだが、順序が決まっているものであれば、失敗したからと言ってRetryしていいわけでもない。
この辺り、考えたがいい答えが出なかったけど、難しいことだけはわかった。
カラム数の増加によるSQL速度の違い
TiDB に対して、Update対象のカラム数を変更して、速度を測ってみた。
一度に1000行更新するとして、カラム数が1違うとだいたい40msぐらい違った。
TiDBは、特にカラムが増えると、複数レプリカに書き込みが必要であったり、TiDB→TiKVへのネットワーク通信があるので、影響が大きいとのこと。
BatchUpdateするときは、カラム数を抑えた方が良さそう。
レコメンド機能を作る時のDB選定について
基本的に、いろんな種類のアイテムカタログから、レコメンド機能を作る際は、ElasticSearch がお勧めされる。
最初は、1インデックスで、複数のアイテムカタログのデータを保持して、検索をするのが最初はおすすめっぽい。
機会があったので、S3 + DuckDB + ECS(インメモリにDuckDBをキャッシュして速度あげる)を試してみた。
結論、カタログサイズが小さくて、更新頻度が1日に1回程度であれば、問題なく動いた。
メモリの問題さえ対処できれば、ここ以外でも有効に使えそうな感じがあった。
LLMの使い方
RAGみたいなめっちゃ分かりやすい使い道を除くと、あんまりいい使い方ができなかった。
一方で、Mastraのworkflowみたいに、ルールベースのロジックと組み合わせながら作ってもらったら、一定利用に耐えうるものはできてきた。
ただ、やはり処理速度の問題があるため、業務システム以外での使い道が難しい。
例えば、LLMで文字列をベクトル化して、近似値をとってレコメンド条件を柔軟にするみたいなのは良さそうだけど、突き詰めればここも機械学習でいいじゃんってなるから、難しい。
React-router / Remix
Remixを使ってみたけど、使ってすぐReactRouterに変わることがわかり、LLMを使って1日で変更した。
やはり、LLMがあるので、フレームワークの変更や、バージョンアップ作業自体はすぐ終わる。
一方で、そのときはリリース前だから良かったけど、今だとフロントの自動テストがもう少し充実しないと、怖くてできない。
テストが大事。ただ、フロントのE2Eは、あんまり評判よくないと聞いた。なんかいい方法がないんだろうか。
コードからドキュメント作り、ドキュメントからテスト作り、テストからマニュアル作り、マニュアルとドキュメントをRAGで検索みたいなことできないかな。
負荷テストのconsole.log
負荷テスト時に、1秒あたりの同時実行数に制限かけながら、console.logで実行ログだして、それをCloudwatch Insight で計測して、秒間実行数を計算してた。
が、Node.js で CPU 100% になった時に、想定と違った挙動を示した。
おそらくだが、これは console.log が CPU 100% になったことにより、CPUの実行待ち行列に入り、ログが出力される時間がずれたからではないかと考えられる。
最後
GPT-5が端的にアドバイスくれるので、短時間ですごく知識が伸びてる気がする。
コード書くのはClaudeCodeに任せるけど、もっぱら質問はGPT-5にしてる。
知識と経験は、LLMのおかげで、方向性もあんま違えなくなったため、やれば伸びてくから、体力が一番大事と感じてきた...