7
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

/(スラッシュ)1つのミスで大変な事態を引き起こしてしまった話

Posted at

はじめに

エンジニアになってから、今までで一番大きな障害を引き起こしてしまいました。
下手したら、賠償責任レベルです。

その原因は、/ によるものでした。

今後、同じミスをしない、させないために、戒めとして書き残したいと思います。

事象

ある日、利用者からファイルがないという連絡を受け、調査したところ、S3に保存してあるファイルが消失していました。このとき、S3はバージョン管理をしておらず、復元できない状態でした。
バックアップ取らなかったの?というご意見もあると思いますが、今回の場合、削除しなければいけなく、バックアップを取ったところでその削除が必要になります。しかも、バックアップをとる場合、保存されているファイルを全てを対象にしなければ、復元は難しい状態でした。

他の利用者のファイルも確認したところ、一部消失していました。
プログラムと見比べても規則性なく、特定できませんでした。

あと、発覚したのが発生してから2週間後ということもあり、調査に時間を要しました。

原因

原因は、最近追加したバッチ処理であることが判明しました。

まず、以下のコードを見てください。

問題のコード
try {
    // 削除対象のファイルを抽出
    // bucketNameはS3のバケット、folderNameはキープレフィックスを指定
    List<String> keys = s3client.listObjects(bucketName, folderName)
                            .getObjectSummaries()
                            .stream()
                            .map(s -> s.getKey())
                            .collect(Collectors.toList());

    if (keys.isEmpty()) {
        return;
    }

    long skip = 0;
    do {
        // 一度に1000件しか削除できないので、上記で取得した結果から1000件のみ抽出
        List<String> deleteKeys = keys.stream()
                                      .skip(skip)
                                      .limit(1000)
                                      .collect(Collectors.toList());
 
        // ファイル削除
        s3client.deleteObjects(new DeleteObjectsRequest(bucketName)
                .withKeys(deleteKeys.toArray(new String[deleteKeys.size()])));
                
        skip += 1000;
    } while (keys.size() > skip);

} catch (AmazonServiceException e) {
    throw e;
}

やりたいことは、あるバケット配下の指定したフォルダを削除するということでした。
正しくは、S3にはフォルダと概念がなく、バケット直下は /区切りのファイル名を保存します。

Bucket
 └ AAA
   ├ BBB
   | └ CCC.txt
   └ DDD.txt

ではなく、

Bucket
 ├ AAA/BBB/CCC.txt
 └ AAA/DDD.txt

ということになります。S3のコンソール上はディレクトリ階層のように表現されてますが、実態はこれです。

上記のコード上には、変数bucketNamefolderNameというものがあり、それぞれ
削除対象ファイルがあるバケット名、削除したいフォルダ名をパラメータとして渡しています。

具体的には、bucketName=hogefolderName=1という感じです。(値は仮です)

S3には以下のようなファイルが存在していました。

hoge
 └ 1/A.txt
 └ 2/B.txt
 └ 10/C.txt

この状態で上記のコードが実行されると、

hoge
 └ 2/B.txt

という結果になりました。

あれ?

10/C.txtは???

そもそも、folderName という変数名が勘違いを引き起こしていました。

普通に考えたら、1/A.txtのみ削除されると思うかもしれませんが、実はここはS3の落とし穴だったのです。

先ほど、申し上げた通り、S3にはディレクトリ階層という概念がないということです。つまり、folderName にしていたのは、ファイルのプレフィックスだったのです。

そうです。

先頭に、1がついているファイルが削除されてしまったのです。
なので、実際は先頭がマッチしてしまったファイルが多く存在していました。

どうすればよかったのか

folderName に指定するのは、1ではなく、1/でした。

この/ で命運が別れたわけです。

削除されてないファイルがあったのはなぜか

先頭がマッチしたにも関わらず、削除されていないファイルが存在していました。
このせいで原因の特定までに時間がかかってしまいました。

原因は、S3のSDKの仕様です。原因というと言い方悪いですが、この仕様のおかげで助かった部分もあります。

この仕様というのは、特定のバケットから一度に取得できるファイル数は1000件となっています。

// 最大でもこの結果は1000件になる
List<String> keys = s3client.listObjects(bucketName, folderName)
                            .getObjectSummaries()
                            .stream()
                            .map(s -> s.getKey())
                            .collect(Collectors.toList());

もしこの制限がなければ、全て削除されていたことでしょう。
想像したくないですね。開発者に感謝します。

今後の対策

二度とファイル消失を発生させないために今後の対策をまとめます。

  • S3のバージョン管理を有効にする
  • S3のサーバアクセスログを有効にする
  • コンソール上から削除する可能性がある場合はオブジェクトレベルのログも有効にする
  • SDKの仕様を理解する。←基本ですね。。。
7
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?