こんにちは、アスクルの いのだい です。
現在はLOHACOのエンジニア組織の部長職に就いていますが、元は運用エンジニアのチーム(以後、運用チーム)に所属してリリース作業などの運用業務を行っていました。そんな私が運用チームに所属していたときの失敗談を書きます。このお話を通して、同業の皆様が同じ過ちを繰り返さないような糧になれば嬉しいです。
!少々刺激の強い内容が出てきますのでご注意ください!
事故概要
発生日時:2016年8月16日 17時15分
復旧日時:2016年8月29日 5時00分
事象 :オペレーションミスによるOracleデータベース破損
事故の経緯
時は2016年の夏、大型案件の開発が進行中でした。運用担当である私は、当時まだローンチされていない本番環境でOracleデータベースのメンテナンスを行っていました。開発環境と本番環境でOracleの環境差異があったため、本番環境でデータベースの作成と削除を繰り返し行っていました(環境差異については後述します)なお、データベースの作成と削除は開発環境で検証済みのスクリプトを用いて実施していました。
作業後、Oracleサーバから見慣れぬエラーが出始めました。あれ?と思いサイトの動作を確認したりアプリケーションログを見たりするもエラーはなく、サービスは提供できていました。振り返ると、この時サービスが正常に稼働できていたのは本当に幸いでした。実はこの時、Oracleの重要な領域をDROPしてしまっていました。
実行したスクリプトはこちらです。
実行ユーザ:system
-- スキーマ設定 DEF NCB=******* -- -- DB名の設定 DEF NCI_BILL=******* -- spool DROP_ALL_=*******.log -- -- 接続 --CONN &NCB./Y_&NCB._h@&NCI_BILL. -- DECLARE type cursor_type is ref cursor; cur_objects cursor_type; cur_objects_val USER_OBJECTS%rowtype; flg BOOLEAN := false; BEGIN open cur_objects for 'SELECT * FROM USER_OBJECTS'; loop fetch cur_objects into cur_objects_val; exit when cur_objects%notfound; if cur_objects_val.object_type = 'TABLE' then flg := true; elsif cur_objects_val.object_type = 'SEQUENCE' then flg := true; elsif cur_objects_val.object_type = 'FUNCTION' then flg := true; elsif cur_objects_val.object_type = 'TYPE' then flg := true; elsif cur_objects_val.object_type = 'SYNONYM' then flg := true; elsif cur_objects_val.object_type = 'PACKAGE BODY' then flg := true; elsif cur_objects_val.object_type = 'PACKAGE' then flg := true; else flg := false; end if; if flg = true then EXECUTE IMMEDIATE 'drop ' || cur_objects_val.object_type || ' ' || cur_objects_val.object_name; end if; flg := false; end loop; EXCEPTION when OTHERS then null; END; / spool off DISCONN
実行したログはこちらです。
Oracleはどんな状態だったのか??
- USER領域1は普通に使用できていた=サービス影響なし
- 管理機能がうまく動かないことでアーカイブログ2が溜まり続ける状態だった →2日持たずディスクフルになる試算・・・
- SYSTEM領域3の復元を試みたが、うまくいかず
- 何で動いているのかわからない by Oracleサポート
事故当日の時系列
2016年8月16日
17:15 systemユーザで諸々DROPするスクリプトを実行 ★事故発生
19:34 退避テーブルから一部のテーブルを復元
20:30 Oracleのアーカイブログが削除できていない事象が発覚
2016年8月17日
1:30 アーカイブログを手動削除し、ディスクの空き容量を確保
どう対処したか
先に、当時運用していたOracleの構成を補足します。1データベース+2インスタンス4で構成され、Real Application Clusters(RAC)を用いてクラスタリングを行っています。この事故ではsystemユーザが保有するオブジェクトを削除してしまったので、インスタンスではなくデータベースを一部破壊したことになります。
『何で動いているのかわからない』by Oracleサポート
とあるように、現在動いているOracleはリカバリすることができない状態であったため、別途データベースとインスタンスを構築してデータを移し替える方法を採用しました。
3時間のサイト停止を設けて、無事復旧できました。
事故原因と再発防止策
実行時間3秒のスクリプトで運用停止寸前に陥ったことで背筋が凍る思いをし、ミスをしてしまった申し訳無さでつらい2週間を過ごしました。しかし、起こしてしまったことは仕方がない(サイトのお客様および関係者様、本当に申し訳ございません orz)ので、いかにして同じことを繰り返さないようにするかが重要です。復旧後に改めて原因を深堀りし、再発防止策を立てました。
1. なぜ本番環境でデータベースの作成と削除を繰り返していたのか?
開発環境と本番環境でOracleの環境差異があったため、本番環境でデータベースの作成と削除を繰り返し行っていました (事故の経緯 より抜粋)
データベースの運用経験がある方なら、通常やることは少ないはずです。なぜ実施していたのか。理由は、開発環境と本番環境でOracleのエディションが異なっていたことで、本番環境で実行するDDLを事前に検証できなかったため です。翌年のOracleバージョンアップにおいて開発環境と本番環境のエディションを統一することで、開発環境で実行したDDLをそのまま本番環境で実行できるようになりました。
2.systemユーザで自身のオブジェクトをDROPするスクリプトを誤って実行してしまったのはなぜか?
原因は2つあります。
1つ目は、開発環境で動いた実績があるスクリプトであることを過信したため です。
なお、データベースの作成と削除は開発環境で検証済みのスクリプトを用いて実施していました。 (事故の経緯 より抜粋)
データベースを一括削除するスクリプトは、事前に開発環境で検証されたものでした。そのため、しっかりとスクリプトの内容を読むことなく実行してしまいました。この点に関して、Oracleの本番環境ではじめて実行するDDLやクエリは事前レビューを必須としました。
2つ目は、開発環境と本番環境で作業ポリシーが異なっていたため です。
開発環境ではデータベースごとのユーザで操作し、本番環境では一律systemユーザを用いてデータベースの変更操作を行っていました。
例)
|データベース|開発環境の実行ユーザ|本番環境の実行ユーザ|
|-|-|-|
|hoge|hoge|system|
|sample|sample|system|
今回実行したスクリプトには、実行するユーザに紐づくオブジェクトを削除する処理が書かれていました。そのため実行するユーザがとても重要でしたが、元々あった作業ポリシーに則って盲目的に作業を進めていました。事故後すぐに作業ポリシーの見直しを行い、本番環境でもデータベースごとのユーザで実行する手順に修正しました。
素直に感じたこと、そして感謝
大型案件が進行する中で事故を起こしてしまったのですが、事故の復旧から2週間あまりで当初のスケジュールどおりリリースを迎えることができました。テストやリリースに向けた準備で忙しい中、LOHACOのエンジニアたちが集結して取り組んでくれたからこそ、乗り越えることができたと思います。本当にすごいことです。そして、感謝の気持ちでいっぱいです。
最後に
皆さんの現場でも
- 経緯はよくわからないけど違和感のあるポリシーが存在する
- 開発環境と本番環境で違う点が多々ある
といったものがあれば、ぜひ見直しましょう。
この記事がどこかの未来の事故を1つでも減らしてくれればと願いつつ、結びとします。読んでいただきありがとうございました。