LASSIC Media らしくメディア
レースコンディションとは|競合状態の原因と対策
複数の利用者が同時にシステムを操作すると、まれに残高の数字が合わなくなる、同じ注文が二重に登録される、といった不具合が起きることがあります。エラーログには目立った記録が残らず、同じ手順を繰り返しても再現しないため、開発会社に調査を依頼しても原因の特定に時間がかかりがちです。
この記事のテーマは、こうした不具合の代表的な原因である「レースコンディション(競合状態)」です。発注者やプロジェクトマネージャーが、開発会社とのレビューで押さえておきたい発生の仕組みと対策の考え方を中心に扱います。似た名前で語られる「デッドロック」や「排他制御」との違いも含めて確認していきましょう。
目次
レースコンディション(競合状態)とは
レースコンディション(競合状態)とは、複数の処理が同時に同じ資源へアクセスし、実行タイミングの違いで結果が変わってしまう不具合のことです。米国MITRE社が公開する脆弱性分類CWE-362でも、共有リソースへの排他制御が不十分な並行実行として定義されています。ソフトウェア工学では、古くから知られる不具合のパターンといえるでしょう。
なお、複数の処理が互いの完了を待ち合って動けなくなる「デッドロック」は、レースコンディションと隣接する概念です。ロックによって同時アクセスを制限する対策手法である「排他制御」も同様に、扱う論点は別になります。本記事では対策手段の詳細には立ち入らず、なぜ競合状態が発生するのかという発生メカニズムに絞って整理します。
レースコンディションが厄介なのは、発生条件が特定のタイミングの重なりに依存する点にあります。開発環境やテスト環境では処理が順番に流れやすく、問題が表面化しないことが少なくありません。
本番環境でアクセスが集中する時間帯や、負荷の高い状況になって初めて不具合が顕在化します。再現条件を絞り込みにくいぶん原因調査に時間がかかり、修正が遅れるほど実害が積み上がる不具合だといえるでしょう。
実際に、放射線治療装置「Therac-25」では、競合状態が原因の不具合が知られています。操作パネルへの入力を素早く行うと本来の保護機構を回避できてしまい、過剰照射による死亡事故につながった事例です。この事例は、日本語版Wikipediaの「競合状態」の項目でも取り上げられています。
医療機器に限った話ではなく、タイミング次第で想定していた制御が無効化されかねない、という点は業務システムの設計でも参考になる教訓でしょう。
通常のプログラムの不具合は、同じ入力を与えれば同じ結果になるため、原因箇所を絞り込みやすいものです。レースコンディションは、同じ入力・同じコードであっても、処理の実行順序という制御しにくい要素が結果を左右する点で、通常の不具合とは性質が異なります。
名称の由来は、複数の処理が資源へたどり着く速さを競い合う「競走(レース)」に見立てたところにあります。どちらが先にゴールするかによって結果が変わってしまう、という比喩です。呼び名は独特ですが、扱っている現象自体は、並行処理を伴うシステムであれば起こり得るものといえます。
この記事のポイント
- レースコンディションは、複数処理の同時アクセスとタイミングのずれによって起きる不具合です。
- 銀行残高の更新や在庫カウントなど、共有データを読み書きする処理で発生しやすくなります。
- 対策には、アトミックな操作やトランザクション、キューによる直列化など複数の考え方があります。
なぜ起きるのか 共有資源とタイミングのずれ
レースコンディションは、複数の処理が同じ資源(データベースのレコード、ファイル、メモリ上の変数など)へ同時にアクセスする場面で発生します。とくに危険なのは、「値を読み込む」「計算する」「書き戻す」という一連の操作の途中に、別の処理が割り込むケースでしょう。
図のように、入金処理Aと出金処理Bが同じ口座残高を同時に読み込むと、後から書き込んだ処理の結果だけが残り、先に書き込んだ処理の更新が消えてしまいます。この現象は「lost update(更新の消失)」と呼ばれ、レースコンディションの典型例です。
もう一つの典型パターンが、TOCTOU(Time Of Check to Time Of Use、確認してから使うまでの間の競合)です。「在庫が1個残っているか確認する」処理と「実際に注文を確定する」処理の間に、別の注文が割り込む場合を考えます。確認時点では在庫があったのに、確定時にはすでに在庫が無い、という状態が生じてしまいます。
主な原因は、「読み込み」「計算」「書き込み」という一連の操作が、内部では複数のステップに分かれている点にあります。ひとまとまり(アトミック、不可分)に実行される保証はありません。途中に別の処理が割り込む余地がある限り、共有資源へのアクセスはつねに競合のリスクを抱えることになるのです。
共有資源は、データベースのレコードだけに限りません。ファイルやディレクトリ、キャッシュサーバー上の値、メモリ上の変数など、複数の処理から読み書きされ得るものはすべてが対象です。サーバーを複数台構成にして負荷を分散するシステムでは、1台のサーバー内だけでなく、サーバーをまたいだ競合にも注意を払う必要があります。
マルチコアCPUや並列処理が前提のシステムでは、同時に実行される処理の数そのものが増えます。処理を高速化するための工夫が、そのままレースコンディションの発生条件を増やす方向に働くこともある、という関係を押さえておきたいところです。
マイクロサービスやイベント駆動アーキテクチャを採用するシステムでは、複数のサービスが非同期にメッセージをやり取りしながら1つの処理を完成させる構成が一般的です。処理の完了順序を厳密に制御しにくい構成であるほど、レースコンディションを作り込みやすくなる点には注意が必要でしょう。
同時アクセスが起きる背景は、複数の利用者がほぼ同じ時刻にアクセスする場合だけではありません。夜間のバッチ処理とリアルタイムの利用者操作が重なる、通信エラー時の自動リトライが元のリクエストと同時に届く、といった場面でも発生します。
セキュリティの文脈では、「ファイルの権限を確認する」処理と「そのファイルを開く」処理の間に、対象を別のものにすり替えられてしまう手口の起点になることもあります。業務システムの内部処理どうしであっても、確認と実行の間にすり抜けを許す設計になっていないかは、確認しておきたい点でしょう。
典型的な不具合の具体例
レースコンディションが引き起こす不具合は、業務システムの複数の場面で見られます。代表的な症状と、発生しやすい典型的な場面を次の表に整理しました。
| 症状 | 典型的な発生シーン |
|---|---|
| 更新の消失(lost update) | 在庫数や残高など、同じ値を複数の担当者・処理が並行して更新する場面 |
| 二重登録・二重決済 | 確認から確定までの間に同じ操作が連続で送信される、または複数端末から同時に送信される場面 |
| カウンタの不整合 | アクセス数やポイント数の合計値を、ロックなしで加算処理する場面 |
| 在庫のオーバーブッキング | 残数チェックと確保処理の間に、別の注文が割り込む場面(TOCTOU) |
カウンタの不整合は、アクセス数やポイント残高のように、単純な加算・減算を大量に行う処理で起こりやすい不具合です。「現在の値を読む」「1を足す」「書き戻す」という3ステップの間に別の処理が割り込むと、本来加算されるはずだった分がまるごと消えてしまいます。処理件数が多いシステムほど、このずれは積み重なって目立ちやすくなるでしょう。
二重登録・二重決済は、通信の遅延や再送によって、ボタンを一度しか押していなくても同じリクエストが複数回サーバーに届くケースでも起こります。サーバー側で同じ注文かどうかを判定する仕組みがなければ、レースコンディションと同じ理屈で重複が発生するのです。
在庫のオーバーブッキングは、ECサイトや予約システムで典型的に見られます。「残り1点」と表示された商品を複数の利用者がほぼ同時に注文すると、在庫チェックの時点ではどちらの注文も成立して見えます。確保処理が終わって初めて、重複が発覚することになるでしょう。
いずれの不具合も、発生した直後にエラー画面が出るわけではありません。後日、経理担当者がデータの不一致に気付く、利用者からの問い合わせで発覚する、といった形で表面化しやすく、対応が後手に回りがちな点は共通しています。
金額や在庫数のように、後から辻褄合わせが難しい情報ほど、レースコンディションによる不整合の影響は大きくなります。会計処理や在庫管理のように、正確性が業務の前提となる領域では、とくに注意を払う価値があるといえるでしょう。
見つかりにくい理由
レースコンディションは、通常のテストで見つけにくい不具合の代表格です。原因は、発生条件がタイミングの重なりという再現しづらい要素に依存している点にあります。
開発時の動作確認は、たいていひとりの担当者が順番に操作するため、複数の処理が同時に走る状況そのものが生まれません。結合テストや受け入れテストでも、同時アクセスを意図的に再現しない限り不具合は顕在化しないでしょう。
本番環境でアクセスが集中する時間帯や、サーバーの負荷が高くタイミングがずれやすい状況になって初めて発現することも珍しくありません。障害報告では「まれに」としか説明できず、再現条件を絞り込む調査に工数がかかりやすい不具合だといえます。
ログに残る情報が乏しいことも、原因究明を難しくする要因です。処理自体はエラーを出さずに完了してしまうため、異常に気付くのは後になってデータの不整合を発見したときになりがちです。
負荷試験の環境が、本番のタイミングを完全に再現できるとは限らない点も見落とせません。サーバー台数やネットワークの遅延、利用者の操作間隔など、本番特有の条件がそろって初めて発現する場合もあります。テスト環境で問題なしと判定された機能が、本番で不具合を起こすこともあるのです。
同じ条件で試しても再現しないことが、かえって調査を難しくする面もあります。一度きりの発生だと、システムの不具合ではなく一時的な現象として片付けられてしまい、原因の特定そのものが先送りになるケースもあるでしょう。
クラウド環境でオートスケールを利用している場合、サーバーの台数が需要に応じて自動的に増減します。台数が変化するたびに処理の実行タイミングも変わるため、負荷が高まる局面でレースコンディションが表面化しやすくなる、という副次的な影響にも注意が必要でしょう。
コードレビューの観点から、共有資源への同時アクセス箇所をあらかじめ洗い出しておくことは有効な対策の一つです。ただし、この確認だけですべての競合パターンを見つけられるわけではなく、設計段階での配慮を代替するものではない点に注意が要るでしょう。
対策の考え方 ロックだけが手段ではない
レースコンディションへの対策は、いずれも目的が共通しています。複数の処理が同じ資源に同時に触れても、結果が壊れないようにすることです。ただし、具体的な手段はいくつかの考え方に分かれます。
- アトミックな操作にする:読み込みから書き込みまでを1つの不可分な処理としてデータベースに実行させ、途中で割り込まれる余地そのものをなくす方法です。
- 排他制御(ロック)で順番を強制する:同時にアクセスできる処理を1つに制限する対策で、仕組みや注意点は別の記事で詳しく扱っています。
- トランザクションの分離レベルを適切に設定する:データベースには、並行実行時に他の処理の影響をどこまで遮断するかを選べる仕組みがあり、設定次第で更新の消失を防げます。
- 冪等性(べきとうせい、同じ処理を何度実行しても結果が変わらない性質)を設計に組み込む:二重登録・二重決済の対策として、同じ注文IDのリクエストを二度目以降は無視する仕組みを持たせます。
- キューで処理を直列化する:同時に届いた複数の処理を一列に並べ、1件ずつ順番に処理することで、そもそも競合が起きない状態を作ります。
それぞれの対策には、向き不向きがあります。処理の性質に応じて使い分ける考え方を、次の表に整理しました。
| 対策の考え方 | 概要 | 向く場面 |
|---|---|---|
| アトミックな操作 | 読み込みから書き込みまでを1回の不可分な処理にする | 単純な加算・更新など、操作の内容が単純な場合 |
| 排他制御(ロック) | 同時にアクセスできる処理を1つに制限する | 複数ステップの処理をまとめて保護したい場合 |
| トランザクションの分離レベル | 並行実行時に他の処理の影響をどこまで遮断するかを設定する | データベースを使う処理全般 |
| 冪等性設計 | 同じリクエストを繰り返しても結果が変わらないようにする | 二重登録・二重決済の防止 |
| キューによる直列化 | 同時に届いた処理を一列に並べ、順番に処理する | 大量の同時リクエストをさばく場面 |
PostgreSQL公式ドキュメントでは、直列化可能(Serializable)と呼ばれる分離レベルを使うと、読み書きの依存関係を検出できると説明されています。この仕組みにより、こうした更新の消失を防げるのです。
MySQLの公式マニュアルでも、SELECT … FOR UPDATEのようなロック付き読み取りを使う方法が示されています。読み込みから書き込みまでを、他の処理に邪魔されずに一続きで行えるようになります。
ロックによる排他制御は確実性が高い一方で、他の処理の待ち時間が発生し、システム全体の応答速度に影響することがあります。キューによる直列化も、待ち行列が伸びれば応答までの時間が延びるため、性能要件とのバランスを見ながら選ぶことが実務上のポイントになるでしょう。
冪等性を持たせる代表的な実装が、リクエストごとに一意な「冪等キー」を発行する方法です。同じ冪等キーを持つリクエストが二度届いても、サーバー側では最初の1回分だけを処理として扱い、二度目以降は前回と同じ結果を返します。決済APIなどで広く採用されている考え方です。
実務では、これらの対策を1つだけ選ぶのではなく、処理の重要度に応じて組み合わせることが一般的です。金額を扱う処理には手厚い対策を、影響の小さい処理には軽量な対策を、というようにめりはりをつける考え方が現実的でしょう。
デッドロック(複数の処理が互いの完了を待ち続けて止まってしまう問題)は、排他制御を導入した結果として新たに注意すべき論点です。本記事の主題である「なぜ競合状態が起きるか」とは、別の切り口になります。ロックの具体的な設計や、楽観ロック・悲観ロックの使い分けは、既存の関連記事で詳しく扱っている内容です。
発注・レビューで確認すべき点
レースコンディションは設計段階での作り込みを避けることが基本方針になるため、発注者やPMが仕様確認・レビューの際に着眼したい点を整理します。
こうした確認を怠ると、リリース後にデータの不整合や二重決済が発覚し、原因調査・データ修正・利用者への説明対応まで含めた手戻りが発生します。設計段階での見直しに比べ、本番稼働後の是正は影響範囲の特定に時間がかかりやすく、対応コストも膨らみやすい点に注意が必要です。
同時実行を前提にした仕様になっているか
複数の利用者が同時に同じデータを操作したらどうなるかを、要件定義やレビューの段階で確認しておくことが欠かせません。仕様書に同時実行時の挙動が書かれていない場合、実装者の判断に委ねられ、対策が漏れるリスクが高まります。
データベースの一意制約を活用しているか
二重登録・二重決済の防止では、アプリケーション側のチェックだけに頼らず、データベースの一意制約(ユニークキー)を併用しているかがポイントになります。アプリケーション側の確認とデータベースへの書き込みの間にも、わずかな時間差が生じるためです。
負荷テストで同時アクセスを再現しているか
レースコンディションは負荷が高い状況で顕在化しやすい不具合です。単体の機能テストだけでなく、同時に大量のリクエストを送る負荷テストの観点を組み込んでいるかどうかを確認しましょう。テストシナリオに同時実行のケースが含まれているかは、開発会社への確認事項として有効でしょう。
仕様変更時に再検証する体制があるか
機能追加やアクセス増加にあわせてシステムを拡張する際、同時実行の前提が崩れていないかを再確認する体制があるかも重要な観点です。当初は問題がなかった処理が、利用者数の増加によって初めて競合を起こすことも珍しくありません。運用フェーズに入ってからも、この観点を継続して見ておく必要があるでしょう。
発生時に追跡できるログ設計になっているか
レースコンディションはエラーを出さずに完了することが多い不具合です。後から「いつ」「どの処理の組み合わせで」不整合が生じたのかを追跡できるログ設計になっているかも、確認しておきたい点でしょう。処理の開始・終了時刻や関連するIDを記録しておくかどうかは、発生時の調査にかかる時間を大きく左右します。
受託開発の契約形態によっては、同時実行対策の作り込みがどこまで契約範囲に含まれるかも、あらかじめすり合わせておきたいポイントです。要件定義書に同時実行時の挙動を明記しておけば、追加費用の発生や責任範囲をめぐる後々の行き違いを防ぎやすくなるでしょう。
これらの確認には、データベースの分離レベルや排他制御の実装パターンについての専門知識が求められます。自社の担当者だけで仕様書の妥当性を判断しにくい場合は、設計レビューの段階から外部の専門家に加わってもらうことで、見落としを減らせるはずです。
まとめ
- レースコンディション(競合状態)は、複数の処理が同じ資源に同時にアクセスし、タイミングの違いで結果が変わってしまう不具合である。
- 共有資源への同時アクセス、TOCTOU、非アトミックな読み書きが典型的な発生パターンとなる。
- 更新の消失(lost update)や二重登録・二重決済など、業務に直結する不具合として表面化する。
- 負荷が高い状況で初めて発現しやすく、通常のテストでは見つけにくい。
- アトミックな操作・トランザクション・冪等性設計・キューによる直列化など、複数の対策を組み合わせて防ぐ。
よくある質問
レースコンディションとデッドロックは同じものですか。
別の問題です。レースコンディションは同時アクセスとタイミングのずれで結果が変わる不具合を指します。デッドロックは複数の処理が互いの完了を待ち続けて止まってしまう不具合です。排他制御(ロック)を導入して競合状態を防ぐと、今度はデッドロックに注意が必要になるという関係にあります。
すべての同時アクセスにロックが必要ですか。
ロックだけが解決策とは限りません。データベースのトランザクション機能や、冪等性を持たせる設計、キューによる直列化など、処理の性質に応じて手段を選ぶことになります。ロックは代表的な対策の一つですが、実装や運用のコストもあわせて考えたうえで選ぶ必要があるでしょう。
一度発生すれば気づけますか。
気づけるとは限りません。処理自体はエラーを出さずに完了するため、データの不整合として後から発覚することがあります。定期的なデータ整合性チェックと、負荷テストでの再現確認を組み合わせておくことが有効です。
開発を依頼する際、契約前に確認しておくべきことはありますか。
仕様書に同時実行時の挙動が明記されているか、負荷テストの計画に同時アクセスのシナリオが含まれているかを、契約前のレビューで確認しておくとよいでしょう。仕様に抜けがある場合は、要件定義の段階で認識をすり合わせておくことをおすすめします。
対応を先送りにすると、どのような影響がありますか。
放置すると、金額や在庫数の不整合が利用者の目に触れる形で表面化し、問い合わせ対応や個別のデータ修正に追われることになりがちです。件数が積み重なるほど調査の切り分けが難しくなるため、仕様策定の早い段階で対策を組み込んでおくことが望ましいでしょう。
著者:テレリモ総研編集部 鈴木 亮佑
ご不明な点はお問い合わせフォームからもご連絡いただけます。
出典
- Wikipedia日本語版「競合状態」
- MITRE「CWE-362: Concurrent Execution using Shared Resource with Improper Synchronization (‘Race Condition’)」
- PostgreSQL公式ドキュメント「13.2. Transaction Isolation」
- MySQL 8.0 Reference Manual「Locking Reads」