僕が SE になって初めて起こした本番障害のバグ
今年2023年の4月をもって、僕は SE として丸10年働いたことになる。転職や休職も経験したが、2013年の4月に新卒入社してから10年間、一応はプログラマ・SE として仕事をしてきた。
今回は、そんな僕が10年前、新卒1年目の時にやらかした、本番障害とバグの話を紹介する。
僕は2013年4月に新卒入社し、新人研修を3ヶ月ミッチリやって、参画する案件が決まるまでさらに1ヶ月間研修をして、8月から現場配属になった。最初の現場では客先社内の業務管理用のウェブアプリみたいなモノの要件定義フェーズから携わった。僕はウェブサイトを作ってきた経験があったので、HTML・CSS・簡単な JS を書いて「動く画面イメージ」をこしらえて、新卒ながら顧客にプレゼンしたりしてた。
それからも自分はその開発案件の主要メンバとして、複数の画面の設計・開発・テストをこなした。チーム全体としては順調にスケジュール遅延して炎上したりしながら、翌年2014年の3月頭に本番リリースへとこぎつけた。自分が作った画面が実際に本番稼動し、お客様が使い始めているというのは、ワクワク半分、ドキドキ半分といった感じだった。
リリースから1週間ほどして、その「ドキドキ」が現実のモノとなってしまった。
ある朝、お客様から問合せがあった。
「○○画面にある『添付ファイル』欄から、対象のファイルをダウンロードできず違うファイルがダウンロードできてしまう」というモノだった。
状況をイメージするには、Redmine や Backlog などのチケット管理システム、もしくは社内 Wiki みたいなモノを思い浮かべてもらいたい。1つのチケットに、添付できるファイルの上限が9つと決まっていて、そのファイルリンクが 3×3 マスの表に表示されている、という UI だった。以下に当時の UI とバグを再現したデモ画面を作ってみたので、触ってみてほしい。
ちなみに、当時の現物は JSP (Java サーブレット) で作っていたが、今回のデモは JavaScript で再現している。バグの本質は言語の違いではないので気になさらず。
- デモ:My First Mistake
- コード:frontend-sandboxes/index.html at master · Neos21/frontend-sandboxes · GitHub
表の左上、「File 1」のマスにカーソルを当てると、選択された実際のファイル名は file-1.txt
と表示される。コレは正しい動きだ。
しかし、「File 2」のマスにカーソルを当てると、選択されるファイル名は file-4.txt
となり、表示されている番号と実際のファイル名の番号がズレてしまっている。
じゃあ file-2.txt
はどこにあるのかというと、「File 4」のマスにカーソルを当てると表示される。
表のセルに表示されている番号と、カーソルを当てて取得できる実際のファイル名とが一致しているのは、「File 1」「File 5」「File 9」の時だけで、後はセルの番号と実際のファイル名がチグハグになってしまっている。
コレは元々「添付ファイル欄」だったので、ファイルを2つしかアップロードしていない時は、「File 2」をクリックすると file-4.txt
を探しに行ってしまい 404 となり、「File 4」の欄は空欄になっていたので、file-2.txt
をダウンロードする術がない、というような状態になっていた。サーバ上にはアップロードされたファイル自体は残っているのだが、お客様からしてみれば「アップロードしたファイルがダウンロードできない」という挙動になる。
さて、それではこのバグが発生する実際のコードを見てみよう。
// セルに表示するタイトルと、実際のファイル名とを宣言したオブジェクトの配列
const items = [
{ title: 'File 1', fileName: 'file-1.txt' },
{ title: 'File 2', fileName: 'file-2.txt' },
{ title: 'File 3', fileName: 'file-3.txt' },
{ title: 'File 4', fileName: 'file-4.txt' },
{ title: 'File 5', fileName: 'file-5.txt' },
{ title: 'File 6', fileName: 'file-6.txt' },
{ title: 'File 7', fileName: 'file-7.txt' },
{ title: 'File 8', fileName: 'file-8.txt' },
{ title: 'File 9', fileName: 'file-9.txt' }
];
// セルに触れた時の処理をとりあえず実装しておく
window.onSelect = (item) => {
document.getElementById('selected-item').innerText = item;
};
// テーブル HTML を組み立てる
let tableHtml = '<table>';
for(let row = 0; row < 3; row++) {
tableHtml += '<tr>';
for(let col = 0; col < 3; col++) {
tableHtml += `<td onmouseover="onSelect('${items[3 * col + row].fileName}')" onmouseleave="onSelect('')">${items[3 * row + col].title}</td>`; // この行にバグがあります
}
tableHtml += '</tr>';
}
tableHtml += '</table>';
// テーブル HTML を画面に出力する
document.getElementById('container').innerHTML = tableHtml;
tableHtml
という変数に、文字列結合でテーブルの HTML を組み立てているワケだが、コレが実際には JSP で書いていた、という違いだけ。バグの本質はそこではない。
table
・tr
・td
要素の構成に従って、二重の for
ループがあり、外側のループで行を、内側のループで列 (セル) を組み立てている。添付ファイルの上限は9つなので、3列・3行となるように 3
は決め打ちでループを回している。
そしてバグが潜む問題の行は、二重の for
ループ内で td
要素を組み立てているところ。選択したセルのファイル番号と、実際に取得できるファイル名の番号が異なってしまうバグの原因が分かっただろうか?
まず、セルに表示するタイトルは、配列 items
の添字を指定して次のように参照している。
items[3 * row + col].title
当然ながら for
ループは 0
始まりなので、1行目・2列目のセルは items[3 * 0 + 1].title
となり、items[1].title
、つまり File 2
という文字列が取得できる。
そして <td onmouseover="">
部分で指定している、実際のファイル名を取得する部分は、配列 items
の添字を次のように指定している。
items[3 * col + row].fileName
よーく読み比べてみてほしい。そう、3
に掛け算する値が row
と col
とで違ってしまっているのだ。コチラの場合、1行目・2列目のセルでは items[3 * 1 + 0].fileName
となり、items[3].fileName
、つまり file-4.txt
というファイル名を取得してしまうワケだ。
改めてデモ画面でカーソルを動かしてみてほしいのだが、「File 1」「File 4」「File 7」と順にカーソルを移動させると、file-1.txt
・file-2.txt
・file-3.txt
とファイル名は順番に取得できていることが分かる。セルに対する並び順が横並びなのか縦並びなのか、というのが、添字の指定の仕方によってチグハグになってしまったのである。
最初はどうしてこういうバグが発生しているのか全く分からなかった。ファイルを4つだけなど、9つまでアップロードしていないタイミングでは余計に意味不明な挙動に感じられたので、調査から原因特定までに半日ほどかかった。
この画面の設計書上は、タイトルの表示どおり横並びに表示されるのが正。つまりファイル名を取得する部分の添字が間違っていたのだ。
items[3 * col + row].fileName; // ← コレが誤り
items[3 * row + col].fileName; // ← こうすべきだった
単純なコーディングミスだったが、細かすぎるあまり、実装工程での上長による静的コードレビューはすり抜けてしまっていた。また、テスト時は
- 最大9つのファイルがアップロードできること (アップロード機能としての簡単な機能確認)
- 1つ目のファイルがダウンロードできること (ダウンロード機能としての簡単な機能確認)
というテスト項目だけにしてしまっていたので、2・3・4・6・7・8個目のファイルが正常にダウンロードできないことに気付けなかった。
アップロード処理を含めて類似の機能を持った既存画面があったので、そこからコードをツギハギして作った結果、こんなショボいミスを生み、不十分なテストケースでテストをすり抜けてしまったワケだ。
アップロードしたファイルが消えてしまったワケではないので、リンク部分のコードを直せば済む程度のバグだったのが不幸中の幸い。先輩や上司達は「しゃーない、こんなミスよくあること」とフォローしてくれたし、お客様も「すぐ直せるなら直しといてー」という感じで楽観的だった。
しかし、こんな些細なミスでも PM は「障害報告書」を書いて、お客様に頭を下げていた。僕は PM に頭を下げさせるようなことをしてしまったのだ。バグの程度の問題ではない。なんならショボい程度のバグで再リリースになってしまって、自分のミスが本当に恥ずかしく、悔しく、周りに申し訳ない気持ちでいっぱいだった。
結局このバグは、前述のとおりコードを修正し、
- ファイルを1つアップロードする → ファイルを1つダウンロードし、期待どおりのファイルか確認する
- ファイルを2つアップロードする → ファイルを2つダウンロードし、それぞれ期待どおりのファイルか確認する
- ……
- ファイルを9つアップロードする → ファイルを9つダウンロードし、それぞれ期待どおりのファイルか確認する
…という形で、大きく9パターンのテストケースをサボらずキッチリこなし、再リリースすることで不具合解消できた。
「初の本番障害がこの程度で良かったじゃないか〜」とフォローしてくれた上司の発言のとおり、この後もっとひどいバグに悩まされるのだが、それは以前話した。w
- 過去記事 : 2018-08-03 とある社内業務システムで起こったバグの話 1
- 過去記事 : 2018-08-06 とある社内業務システムで起こったバグの話 2
- 過去記事 : 2018-08-07 とある社内業務システムで起こったバグの話 3
- 過去記事 : 2018-08-08 とある社内業務システムで起こったバグの話 4
このバグを通じて、僕は新人なりに良い教訓を得た。
- 同じ値を2回以上使うなら変数として定義する
- 今回の場合なら
const item = items[3 * row + col];
があれば全然結果は違った
- 今回の場合なら
- 多重のループは書かないで済むように構成を考える
- 今回のような場合、
<tr>
・</tr>
を挟み込む必要があるが、その部分をif
文にしてでも、「9回回すループ」にした方が良かったかも
- 今回のような場合、
// 例えばこんな感じ
let tableHtml = '<table>';
for(let i = 0; i < 9; i++) {
const item = items[i];
const isRowBegin = (i === 0 || i === 3 || i === 6); // ココをマジックナンバーにしない方法もあるけど
const isRowEnd = (i === 2 || i === 5 || i === 8); // コレでも二重ループよりはまだ分かりやすいかも
if(isRowBegin) tableHtml += '<tr>';
tableHtml += `<td onmouseover="onSelect('${item.fileName}')" onmouseleave="onSelect('')">${item.title}</td>`;
if(isRowEnd) tableHtml += '</tr>';
}
tableHtml += '</table>';
- レビュアーが見落とす可能性も大いにある。コードは可読性 (レビュアーが読んで理解できること) が命
- 今回の場合、レビュアーは SE 歴15年以上で Java の資格も持っているベテランだった。しかし分かりにくいコードは見落としを生みやすい
- レビュアーの責任も問われるが、基本的には「やらかしたヤツ」が記憶されている
- コピペで実装した場合は特に要注意。パッと見で動いていても、細かな操作バリエーションで抜け漏れがあり得る
- コピペした後に一部を調整する際にバグが混じる
- 今回の場合、「1列・9行」を表示するテーブルが別画面に存在したので、それをコピペして調整して組んでいた
- テストサボるな。Excel 管理でスクショを貼り付けるのはダルいが、テストケースを書く段階ではとりあえず盛り込んでおけ
- できるだけテストするバリエーションやパターンを「増やそう」という意識で考えてみる
- 多すぎるテストケースを「やらないことにする」という判断は上司にさせればいい。そうすれば「テストしておけばよかったのに」と言いやすくなるw
- 「本番障害」は、程度によらず起こしてはならない。起こすとクッソ面倒な修正作業を無償でやる羽目になり皆が迷惑するから
- 人事評価だとか、「人からよく見られたい」みたいな見栄を気にする以前の段階で、個人の心の中でメチャクチャ恥ずかしい
- 「レビュアーもいたのだし、チーム全員の責任だ」などと表面的には言ってくれるが、皆キッチリ「誰が起こしたバグか」は覚えていて、飲み会の度に冗談にされるw
- 本番障害に至らないようにするために、設計書を細かく書き、実装を慎重に行い、テストをサボらず入念に行うのである
確かに、データが破損・欠落するような重大なバグではなくて良かった。簡単なミスで、被害も少ないバグで「本番障害」の重たさを経験できて、運が良かったのだと思う。
だが、僕はこのミスで一気に血の気が引いたし、それ以来システム開発を「ミスなく」「確実に」行おうという意識が強くなった。
中小企業に転職してみて、この時の現場のようなキチッとした案件など皆無で、「なんとなくイケるっしょ!」みたいな大学生的なノリで皆が仕事していてウンザリした。だがそれも数年在籍すると、単純接触効果で慣れてしまったところがある。僕が思う水準の品質はこの会社では求められておらず、抜け漏れや失敗があっても別にいい。この会社はそういう場所であり、皆それで良しとされているのだ。
郷に入っては郷に従え、で良いのか。そこそこの品質でバグの不安がある成果物を納品する、そんな今の仕事のやり方に合わせていけば良いのだろうか。コレから先、どういう風に働いていこうかのう。