문제
자동 백업에서 복원하는 경로에는 거의 항상 게이트가 하나 붙는다. 망가진 파일을 살아 있는 데이터 위에 덮어쓰지 않기 위해서다. 가장 자연스러운 선택은 엔진이 제공하는 무결성 검사를 돌리고 ok가 나오면 진행하는 것이다. 이 게이트는 실제로 손상된 파일을 잡아낸다. 문제는 가장 흔한 사고 형태, 즉 아무것도 들어 있지 않은 백업 파일을 그대로 통과시킨다는 데 있다.
빈 파일은 유효한 데이터베이스다
길이가 0인 파일을 열면 엔진은 이를 손상으로 보지 않는다. 아직 페이지가 하나도 없는 새 데이터베이스로 간주하고 정상적으로 연다. 그 상태에서 무결성 검사를 돌리면 검사할 페이지가 없으므로 위반도 없고, 결과는 ok다. 백업 작업이 디스크가 꽉 차서 중간에 죽었든, 잘못된 경로를 가리켰든, 스냅샷이 시작만 하고 끝나지 않았든, 남는 산출물의 형태는 똑같다. 게이트 입장에서 이 파일은 완벽하게 건강하다.
검사는 구조를 보지 내용을 보지 않는다
무결성 검사가 답하는 질문은 "이 파일의 페이지와 인덱스가 자기 자신과 일관적인가"이다. "이 파일이 내가 잃어버린 데이터를 담고 있는가"가 아니다. 두 질문은 자주 함께 참이기 때문에 하나가 다른 하나를 대신할 수 있다고 착각하기 쉽다. 복원은 정확히 둘이 갈라지는 순간에 실행되는 작업이다. 그래서 복원 게이트는 검증 도구의 계약을 읽고, 그 도구가 답하지 않는 질문을 따로 물어야 한다.
복원 전에 무엇을 확인할 것인가
최소 네 가지를 순서대로 확인한다. 첫째, 파일 크기와 페이지 수가 0보다 큰가. 둘째, 기대하는 스키마 객체가 실제로 존재하는가. 셋째, 핵심 테이블의 행 수가 미리 정해 둔 하한 이상인가. 넷째, 실제 읽기 질의 하나가 그럴듯한 값을 돌려주는가. 이 검사는 원본 위가 아니라 복사본 위에서 수행하고, 통과한 뒤에야 파일을 교체한다. 교체 대상 파일은 지우지 말고 옆으로 옮겨 둔다. 되돌릴 경로가 없는 복원은 복원이 아니라 두 번째 사고다.
픽스처는 진짜 산출물이어야 한다
이 게이트를 테스트할 때 헤더 몇 바이트만 흉내 낸 가짜 파일을 픽스처로 쓰면, 나중에 진짜 검증이 추가되는 순간 그 테스트가 먼저 깨진다. 더 나쁜 경우에는 깨지지 않고, 게이트가 실제로는 통과시키지 않을 입력을 통과시킨다고 주장한다. 픽스처는 실제 엔진으로 만든 진짜 파일이어야 한다. 정상 백업 하나, 0바이트 하나, 중간에서 잘린 것 하나, 스키마는 맞지만 비어 있는 것 하나. 네 개를 모두 만들어 두면 이 결함군 전체가 한 번에 고정된다.
회귀를 고정한다
고친 다음에는 사고 모양 그 자체를 테스트로 남긴다. 길이 0인 백업 파일을 복원 경로에 넣었을 때 거부되어야 하고, 거부 사유가 "무결성 실패"가 아니라 "내용 없음"으로 구분되어 기록되어야 한다. 이 구분이 없으면 다음 사람은 같은 알람을 보고 엉뚱한 곳을 디버깅한다. 알람 문구까지가 테스트 범위다.
완료 기준
복원 게이트가 무결성과 존재 여부를 각각 독립적으로 검사하고, 빈 백업이 명시적 사유와 함께 거부되며, 픽스처가 손으로 만든 바이트가 아니라 실제 엔진 산출물이고, 교체된 원본이 되돌릴 수 있는 위치에 남아 있을 때 완료다. 그 전까지 이 게이트는 백업이 존재한다는 사실만 확인해 주고 있다.
The trap
Any automated restore path acquires a gate sooner or later, because nobody wants to overwrite live data with a broken file. The natural choice is to run the engine-provided integrity check and proceed when it answers ok. That gate genuinely catches corrupted files. What it does not catch is the single most common backup failure: a file with nothing in it.
An empty file is a valid database
Open a zero-length file and the engine does not treat it as damage. It treats it as a brand-new database that has no pages yet, and opens it cleanly. Run the integrity check against that and there are no pages to inspect, therefore no violations to report, therefore ok. A backup job that died halfway when the disk filled up, one that wrote to the wrong path, and one whose snapshot started but never finished all leave behind artifacts of exactly this shape. To the gate, the file looks perfectly healthy.
The check validates structure, not presence
An integrity check answers "are this file's pages and indexes internally consistent?" It does not answer "does this file contain the data I lost?" Those two questions are true together often enough that one starts standing in for the other. A restore runs at precisely the moment they come apart. So a restore gate has to read the contract of the verification tool it leans on and separately ask the question that tool does not answer.
What to assert before restoring
Assert four things, in order. That the file size and page count are greater than zero. That the schema objects you expect actually exist. That the row counts of the tables you care about clear a floor you decided in advance. That one real read query returns a plausible value. Run all of it against a copy, never against the live file, and swap only after every assertion passes. Move the file you are replacing aside instead of deleting it — a restore with no way back is not a restore, it is the second incident.
Fixtures have to be real artifacts
If you test this gate with a fixture that only imitates a few header bytes, that test is the first thing to break the day a real validation is added. In the worse case it does not break, and it goes on asserting that the gate accepts an input the gate would in fact reject. Build fixtures with the real engine: one healthy backup, one zero-byte file, one truncated mid-write, one that is schema-correct but empty. Those four cover the entire family, and they keep covering it after the implementation changes.
Pin the regression
Leave behind a test shaped like the incident itself. A zero-length backup handed to the restore path must be rejected, and the rejection must be recorded as "no content" rather than "integrity failure". Without that distinction the next person reads the alert and debugs the wrong layer — the wording of the failure is part of what you are testing, not decoration on top of it.
Completion bar
You are done when the restore gate checks integrity and presence as independent assertions, an empty backup is rejected with an explicit reason, the fixtures are real engine output rather than hand-assembled bytes, and the replaced original is still somewhere you can walk back to. Until then the gate is confirming that a backup file exists, which was never the question.
问题
自动恢复路径迟早都会加上一道门禁,因为没人愿意用坏掉的文件覆盖线上数据。最自然的做法是跑一次引擎自带的完整性检查,返回 ok 就继续。这道门禁确实能拦住真正损坏的文件,却拦不住最常见的那种备份故障:一个什么都没有的文件。
空文件是合法的数据库
打开一个长度为零的文件,引擎并不把它当作损坏,而是当作一个还没有任何页面的新数据库,并正常打开。对它跑完整性检查,没有页面可查,也就没有违规可报,结果是 ok。磁盘写满而中途死掉的备份作业、写错路径的作业、开始了却没结束的快照,留下的产物形状完全一样。在门禁看来,这个文件健康得无可挑剔。
检查看的是结构,不是内容
完整性检查回答的是“这个文件的页面和索引彼此自洽吗”,而不是“这个文件装着我丢掉的数据吗”。这两个问题经常同时成立,于是人们习惯用一个替代另一个。恢复恰恰发生在两者分岔的那一刻。所以恢复门禁必须先读清所依赖工具的契约,再单独去问那个工具不回答的问题。
恢复之前要断言什么
按顺序断言四件事:文件大小与页面数大于零;期望的库表对象确实存在;关心的表的行数达到事先定好的下限;一条真实的读取查询返回了合理的值。所有断言都对副本执行,绝不对线上文件执行,全部通过之后才做替换。被替换的文件要移到一旁而不是删除——没有退路的恢复不是恢复,而是第二次事故。
夹具必须是真实产物
如果用只模仿了几个头部字节的假文件来测这道门禁,那么真正的校验一旦加上,最先崩掉的就是这个测试。更糟的情况是它不崩,于是继续断言门禁会接受一个实际上会被拒绝的输入。夹具要用真实引擎生成:一个健康备份,一个零字节文件,一个写到一半被截断的,一个结构正确但内容为空的。这四个覆盖了整个缺陷族,并且在实现变动之后仍然覆盖得住。
把回归钉住
留下一个与事故同形的测试:把零长度备份交给恢复路径必须被拒绝,而且拒绝原因要记成“没有内容”,不是“完整性失败”。少了这个区分,下一个人看到告警就会去调错层。失败信息的措辞属于测试范围,不是锦上添花。
完成标准
恢复门禁把完整性与内容存在性作为两条独立断言分别检查;空备份被带着明确原因拒绝;夹具是真实引擎产物而非手工拼出来的字节;被替换的原件仍留在可以退回的位置——满足这四条才算完成。在那之前,这道门禁只是在确认备份文件存在,而那从来不是问题所在。
問題
自動復元の経路には遅かれ早かれゲートが付く。壊れたファイルで生きているデータを上書きしたい人はいないからだ。もっとも自然な選択は、エンジンが用意した整合性チェックを走らせ、ok なら進めることである。このゲートは実際に壊れたファイルを止める。止められないのは、もっともありふれたバックアップ障害、すなわち中身が空のファイルだ。
空のファイルは正当なデータベースである
長さ 0 のファイルを開いても、エンジンはそれを破損とは扱わない。ページがまだ一つも無い新しいデータベースとみなし、問題なく開く。その状態で整合性チェックを走らせれば、検査すべきページが無いので違反も無く、結果は ok になる。ディスクが満杯で途中で落ちたバックアップ、書き先を間違えたバックアップ、開始しただけで終わらなかったスナップショット——残る成果物の形はどれも同じだ。ゲートから見れば、このファイルは完全に健全である。
チェックは構造を見ていて中身を見ていない
整合性チェックが答えるのは「このファイルのページとインデックスは自分自身と整合しているか」であって、「このファイルは失われたデータを保持しているか」ではない。二つは同時に真であることが多く、だから一方が他方の代わりになると錯覚する。復元はまさに両者が食い違う瞬間に走る作業だ。だから復元ゲートは、頼っている検証ツールの契約を読み、そのツールが答えない問いを別途立てなければならない。
復元の前に何を確かめるか
順に四つを確かめる。ファイルサイズとページ数が 0 より大きいこと。期待するスキーマ objects が実在すること。主要テーブルの行数が事前に決めた下限を超えること。実際の読み取りクエリが一つ、もっともらしい値を返すこと。これらはすべて原本ではなく複製に対して実行し、全部通ってから入れ替える。置き換えられる側のファイルは削除せず脇に退避させる。戻る道の無い復元は復元ではなく二度目の事故だ。
フィクスチャは本物の成果物でなければならない
ヘッダ数バイトを真似ただけの偽ファイルでこのゲートを試験すると、本物の検証が追加された日に真っ先に壊れるのはその試験である。さらに悪い場合は壊れず、ゲートが実際には拒否する入力を受け入れると主張し続ける。フィクスチャは実エンジンで作る。健全なバックアップ一つ、0 バイト一つ、書き込み途中で切れたもの一つ、スキーマは正しいが空のもの一つ。この四つで欠陥の族全体が固定され、実装が変わったあとも固定され続ける。
回帰を固定する
事故そのものの形をした試験を残す。長さ 0 のバックアップを復元経路に渡したら拒否されること、そして拒否理由が「整合性失敗」ではなく「中身が無い」として記録されること。この区別が無いと、次の担当者は警告を読んで別の層をデバッグする。失敗メッセージの文言は試験の範囲であって、飾りではない。
完了の基準
復元ゲートが整合性と中身の存在を独立した表明として別々に検査し、空のバックアップが明示的な理由とともに拒否され、フィクスチャが手組みのバイト列ではなく実エンジンの出力であり、置き換えた原本が戻れる場所に残っている。これらが揃って完了だ。それまでこのゲートは、バックアップファイルが存在することだけを確認している。