はじめに
先日仕事でメンバーとの会話で「ここってN+1問題って大丈夫だよね?」と指摘を入れた際に「???」みたいな顔をされてしまいました。
全てのパターンを知る必要は無いと思いますが、コードの会話する上で最低でも知っていてもらいたいと思いました。
今回はSEなら当たり前な「N+1問題」的な事を並べて記載したいと思います。
パフォーマンス編
N+1問題
概要
1回で済むはずのSQL発行を、ループの中で何度も実行してしまい、アプリが激重になる問題
やらかし例
# 記事を全件取得したあと、ループの中で毎回ユーザー情報をDBに見に行く
articles = select_all_articles()
for article in articles:
user = select_user_by_id(article.user_id) # ここでN回SQLが走る!
対策
JOIN を使う、またはORMの Eager Loadingを使って、一発でまとめてデータを取得
デッドロック
概要
複数の処理(トランザクション)が、お互いに相手がロック(占有)しているデータの解放を待ち続けてしまい、処理が完全にストップ(フリーズ)する現象
やらかし例
処理Aが「テーブルX → Y」の順で更新し、同時に走った処理Bが「テーブルY → X」の順で更新しようとすると、お互いにロックが噛み合ってタイムアウトするまで完全に黙り込みます
対策
データを更新する順番をシステム全体で統一すること。処理Aも処理Bも「必ずXを更新してからYを更新する」というルールにすれば、デッドロックは防げる
フルスキャン(シーケンシャルスキャン)
概要
DBから特定のデータを探す際に、目次を使わずに「1ページ目から最後のページまで(N件すべて)」を順番にチェックしている非常に重い状態
やらかし例
検索条件(WHERE 句)に指定しているカラムに、インデックス(索引) が貼られていないケース。データが数百万件になると、検索のたびに全データをスキャンするため、DBのCPUが100%に張り付きます
対策
よく検索条件や結合条件(JOIN)に使われるカラムには、適切にインデックス(Index)を作成
計算量(ビッグオー記法 / O(N))
概要
データ量(N)が増えたときに、処理時間やメモリ消費量がどれくらい爆発するかを示す指標
やらかし例
配列の中に特定のデータがあるか探す際、安易に「二重ループ」を回してしまうケース。データが10倍になると、処理時間は 10の2乗 = 100倍になります(O(N^2))。開発環境(データ10件)では一瞬ですが、本番環境で破綻
対策
アルゴリズムを意識すること。二重ループを避けてハッシュマップ(連想配列)を使い、計算量を O(N) に落とすなどの工夫する
ネットワーク・通信編
チャッティ(Chatty)API問題
概要
1つの画面を表示するために、何度も(Chatty=おしゃべりに)別々のAPIを叩かないといけない設計
やらかし例
スマホアプリのマイページを開くために、「プロフィール取得API」「投稿一覧API」「通知数API」の3回を別々にリクエストするケース。モバイル回線だと通信遅延が目立つ
対策
その画面専用のデータを一発で返すAPIを作る(BFFパターン)、または GraphQL を導入して1回のリクエストで欲しいデータを指定して取得
CORSエラー(Cross-Origin Resource Sharing)
概要
ブラウザのセキュリティ機能により、異なるドメイン間(例: frontend.com から backend.com)の通信が、サーバー側の許可がないと遮断される現象
やらかし例
ローカル環境やテスト環境で、フロントエンドから新設したバックエンドAPIを叩いた瞬間、コンソールに赤文字のエラーが出て通信が全滅
対策
バックエンド側のサーバー設定(またはミドルウェア)で、許可するオリジン(ドメイン)を正しくレスポンスヘッダー(Access-Control-Allow-Origin)に含めるよう設定
冪等性(べきとうせい)
概要
ある処理を1回実行しても、誤って100回実行しても、システムの最終的な状態(結果)が全く同じになるという性質
やらかし例
決済APIや、データの新規登録処理。ネットワークが不安定な時にユーザーがボタンを「連打」してリクエストが2回飛んだ際、2重課金や重複データ登録が起きてしまう設計はNG
対策
リクエストに一意なキー(リクエストID等)を含め、サーバー側で「既に処理済みか」をチェックして2回目は同じ結果を返すだけの設計(冪等な設計)にする
セキュリティ・アプリケーション不具合編
SQLインジェクション(SQL Injection)
概要
ユーザーの入力フォームなどに悪意のあるSQL文(' OR '1'='1 など)を注入され、データベースを不正に操作されたり、情報を盗まれたりする超有名脆弱性
やらかし例
# 入力値をそのまま文字列結合してSQLを作ると、不正なSQLが実行されてしまう
sql = "SELECT * FROM users WHERE username = '" + user_input + "';"
対策
変数をSQLに直接埋め込まず、必ずプレースホルダー(バインド変数)を使用して、入力値をただの「文字列」として安全に処理
XSS(クロスサイトスクリプティング)
概要
掲示板などの入力欄に、悪意のあるJavaScriptコード()を書き込まれ、それを閲覧した他のユーザーのブラウザ上でスクリプトが勝手に実行されてしまう脆弱性
やらかし例
ユーザーが投稿したテキストを、そのままHTMLとして画面にレンダリングしてしまうケース。セッションID(Cookie)を盗まれてアカウントを乗っ取られる危険
対策
ユーザー入力を表示する際は、必ずエスケープ処理(サニタイズ)を行い、< や > を < や > に変換
ディレクトリトラバーサル(Directory Traversal)
概要
ファイルダウンロード機能などで、ユーザーが相対パス(..)を入力することで、公開してはいけないサーバー内のシステムファイルを覗き見られてしまう脆弱性
やらかし例
download?file=../../etc/passwd のようなリクエストをそのまま受け付け、サーバーの機密ファイルを出力してしまう
対策
ファイル名に / や .. が含まれていないかバリデーションをかける、またはファイルIDなどからシステム内部でパスを解決する
浮動小数点数の計算誤差(丸め誤差)
概要
コンピュータは 0.1 を2進数で正確に表現できないため、プログラミング言語で 0.1 + 0.2 を計算すると 0.30000000000000004 みたいに微妙にズレる問題
やらかし例
ECサイトや会計システムで、商品の消費税計算や割引計算を浮動小数点数(float や double)で行った結果、合計金額が帳簿と1円ズレて大問題
対策
お金の計算に浮動小数点数を利用しない。10進数を正確に扱える専用のデータ型(Pythonの Decimal、Javaの BigDecimal、C#の decimal など)を使うか、すべて「セント」や「円」単位の整数(int)に変換して計算する
インフラ・設計・運用編
キャッシュクラッシュ(キャッシュスラッシング / 雪崩)
概要
DBの負荷を下げるためのキャッシュ(Redisなど)が、有効期限切れなどで一斉に消えた瞬間、大量のアクセスが本番DBに直撃してシステムがダウンする現象
やらかし例
すべてのデータのキャッシュ有効期限(TTL)を一律「30分」に設定するケース。30分後に一斉にキャッシュが消え、DBの負荷がスパイクして画面がフリーズ
対策
キャッシュの有効期限にジッター(ランダムな数秒のバラつき)を追加し、有効期限が切れるタイミングを分散
スプリットブレイン(Split-Brain)
概要
高可用性(HA)構成をとっている2台のサーバー間でネットワークが断絶した際、お互いに「相手が死んだ!」と勘違いして、両方が同時にマスター(主系)として動き出してしまう現象
やらかし例
両方のサーバーが自分が主系だと思って同じデータに勝手に更新をかけるため、データの整合性がめちゃくちゃに破壊される
対策
奇数台のノードによる多数決システム(クォーラム)を取り入れる、または片方を強制終了させる仕組み(フェンシング)を導入
単一障害点(SPOF: Single Point of Failure)
概要
そこが1箇所でも壊れたら、システム全体が完全に停止してしまうような脆弱な箇所のこと
やらかし例
Webサーバーを5台並べて冗長化したものの、データを保存するDBサーバーが1台きり(冗長化なし)だったケース。そのDBが落ちた瞬間、システム全体が全滅
対策
サーバー、DB、ロードバランサー、ネットワーク経路など、すべてのレイヤーを冗長化(マルチAZ化など)し、SPOFを無くす
おわりに
改めて自分でまとめたが、まだ足らないと思うので、メンバーとの会話で話題に上がった際には追記して行きたい
参考(感謝)
- AIに聴きながら
