東武鉄道「システム障害が帰宅ラッシュを直撃 排他制御のバグ、19年目で初めて発覚」について
色々回っていたら、久しぶりに日経コンピュータの「動かないコンピュータ」シリーズに行きつきました。
ソフトウェア品質を司る…と言ったらアレですけど、様々なソフトウェア品質について理解をしておかないといけない立場として、この問題は捨て置けないと思ったので、ちょいとメモ程度に残しておこうと思います。
私も多くのトラブルプロジェクトに参加してきましたが、その中には当然、多くの『納品後不良』と呼ばれるものもありました。小さな規模のシステムから、数百億を超える規模のシステムまで様々です(平均すれば9桁後半~10桁前半くらいの規模のものになると思いますけど)。
さらに、そうした関わってきた納品後不良トラブルの中でも、おそらく一番多かったのは
排他設計
の甘さです。これが非常に多いんです。
先に結論だけ言っておきますと、排他設計は全く難しくありません。やたらと面倒くさいだけです。するべきことを、多少手間でもしっかりやりさえすれば、排他で問題となるようなことは絶対にありません。
未然に防ぐ方法は、技術的には超簡単(面倒なだけで)。
すべての外部リソースに対し、最小単位の「CRUD」の関係性を明確にする図を作り、それぞれの関係性や順序性を無視せずにモノ作りすること、それだけです。
CRUDとは
作成(追加)の「Create(Insert)」
参照の「Reference(Select)」
更新の「Update」
削除の「Delete」
の頭文字を取ったもので、データベースのみならず、ファイルやメモリに至るまで、プログラムから見て「外部」となるデータ保持媒体すべてに対してどのようなアクセスをしているかを正確に把握するために作られます。
参考までに、一般的な書式を載せましたが、書き方は自由ですし、その粒度も自由で、アクセス元も「機能単位」、「処理単位」など、どこまでも細かくすることが可能ですし、アクセス先にしても、「媒体単位」「目的単位」「スキーマ単位」「テーブル単位」など自由に記載することが可能です。
私はこれを
アクセス元「イベント単位」
アクセス先「項目単位」
にすべて整理します。覚えてる範囲では、機能数25、イベント数40、対象テーブル数25、項目数700くらいのシステムに対して調査した時、およそ1週間くらいかかったでしょうか。あ、もちろん、私一人で実施した場合の実績値になります。ちなみに、引用元は500kstep(50万行)くらいのソースコードのみでした。
しかし、そうしさえすれば、絶対に排他で誤ることはありません(とりあえず、デッドロックの問題については、ここでは触れないでおきます(一気に技術的ハードルが上がりますので))。
アクセス先ごとに見た時、
「U」と「D」が別々の機能(イベント)で実装されている箇所
「U」や「D」と「R」が別々の機能(イベント)で実装されている箇所
を特定すれば、そこに排他の要否を確認するだけで十分だからです。ちなみに「C」はあまり心配ありません。だって、「C」によってデータそのものが作られないと、「U」も「R」も、そして「D」も実施できませんから。「C」だけは安全なんです(主キーや採番などの課題はありますけど、排他とは重複する問題ではありません)。
ですが、このCRUDをしっかりと押さえているプロジェクトと言うのは、成功プロジェクトの中でもあまり見ません。これは、非常に多くの会社の、多くのプロジェクトでも同じでした。
私がそう言う現場で解決する際は、当初の見積りが1000万円程度のプロジェクトでも、10億のプロジェクトでも、することは毎度同じで、
①CRUD図を作り、
②排他制御されていない箇所を特定し、
③処理が重複する可能性を一つひとつ検証し、
④必要な箇所に排他制御を組み込む
(必要のないところには組み込まない)
をするだけです。
対策としては、技術的に難しいことは全くしていません。「調べる」ことと「整理する」ことさえできれば、あとは普通に必要箇所へのプログラム追加を行うだけで十分な対策です。
それに、プログラムにおける排他制御それ自体も、決して技術的に難しいことはしていません。
・先に読みだした方に更新権を与え、
更新前に読みだした方には更新権を与えない(楽観排他)
・先に読みだした方が更新を終えるまで、
他の処理には一切触らせない(悲観排他)
のいずれかしかありませんし、前者は『フラグ』を使えば簡単な基礎制御文で作れますし、後者はそもそもそう言う機構や関数、制御命令文などが既に用意されています。
・SQL…SELECT FOR UPDATE(Oracle等)
・プロセス…セマフォやmutex
が良い例です。
でも、やらかすんですよね。
この問題は、プログラミング技術の問題ではなく、システムの設計思想に組み込まれているかどうかの問題です。すなわち、
技術(Techology)
ではなく
開発(Development)
のスキルの問題です。プログラミングすることが開発のすべてだと思い込んでいるエンジニアには一生解けない問題かもしれません。
たぶん、私が解決してきただけでも、30代の頃からの実績を足せば、排他関連だけに絞り込んでも、3~4億程度は解決することでコスト削減してきたんじゃないでしょうか(つまりはマイナスになるべきコストを抑えた…と言う意味での利益貢献)。
それくらい、排他制御に関する事故は、頻繁に起こる問題で、事故が顕在化した時のインパクトは大きく、しかもあとで対策するとなったら意外と面倒なテーマだったりします。
まったく対応できていなければ、全機能に手を加え、しかも全テストしなおさなければならなくなりますし、一部に対応が無かった場合は、そこだけ対応すれば他に一切影響しないことを、論理的に説明できなければなりません。
単純な労力、証明資料の作成、顧客の説得、精神的な負荷、etc.…
色々と摩耗しやすい問題なので、「排他」について必ず意識するようにしましょう。若手のエンジニアにはどうしても理解しづらい機構かもしれません。ですが、"新人に毛の生えた程度"から卒業し、中堅エンジニア以上になるためには、この『業界の常識』を理解することは必須だと思うべきです。
1つのリソースを関数間、処理間、機能間、システム間で「共有する」となった時点で、検討することが必須となるITリテラシーの基本です。若手もベテランも関係ありません。
この考え方がどのくらい「常識」かと言うと、昔から基本情報技術者試験でも出てくるレベルの考え方だと言えばわかるでしょうか。もちろん、ITパスポート試験でも出てきます。
基本情報技術者…すなわち、ITSSレベル1~2程度の実力ということです。
ITSSレベル1~2程度というと、学生でも取得されることが多い資格試験ですね。就職に有利…かどうかは微妙ですが、IT企業に就職することを目指すなら、無いよりはあった方がマシという知識群の1つです。
排他制御は、そう言う意味では、ITに携わるものであれば、知らないと恥ずかしい知識レベルなのかもしれません。