Khôi phục file sau khi chạy nhầm rm -rf
Chạy nhầm rm -rf? Dừng ghi và unmount filesystem ngay. Xem thứ tự khôi phục thực tế trên ext4, cùng các giới hạn khiến file không thể lấy lại.
Việc cần làm trong 60 giây đầu tiên
Hai việc quyết định khả năng khôi phục các file đã xóa bằng rm -rf, và cả hai đều phải thực hiện trước khi bạn mở công cụ tìm kiếm. Dừng ghi vào filesystem đó. Sau đó ngừng sử dụng nó bằng cách unmount hoặc remount ở chế độ chỉ đọc.
rm không xóa dữ liệu. Nó xóa directory entry, sau đó đánh dấu inode và các block dữ liệu của file là còn trống. Các byte vẫn nằm trên thiết bị. Chúng vẫn ở đó cho đến khi block allocator cấp các block đó cho một đối tượng khác và đối tượng đó ghi đè lên chúng. Mỗi giây filesystem vẫn được mount và đang hoạt động, một daemon có thể ghi thêm một dòng log hoặc database có thể flush một page. Một trong hai thao tác ghi đó có thể rơi đúng vào các block bạn muốn khôi phục.
Vì vậy, các lệnh đầu tiên phải là lệnh dừng ghi, không phải lệnh khôi phục file.
sudo systemctl stop nginx postgresql
sudo umount /mnt/dataNếu umount trả về umount: /mnt/data: target is busy., hãy tìm tiến trình đang giữ filesystem mở.
sudo fuser -vm /mnt/data
sudo lsof +D /mnt/dataNếu không thể giải phóng filesystem, hãy remount nó ở chế độ chỉ đọc. Mount chỉ đọc sẽ ngăn các lần cấp phát mới, và đó là phần lớn việc bạn cần làm.
sudo mount -o remount,ro /mnt/dataNếu path đã xóa nằm trên root filesystem thì việc này khó hơn. sudo mount -o remount,ro / thường sẽ fail với mount: /: cannot remount /dev/vda1 read-only. vì các tiến trình đang chạy vẫn giữ file để ghi, còn kernel sẽ không buộc chúng đóng file. Trên VPS, cách thực tế là dùng rescue mode hoặc recovery mode của nhà cung cấp: hệ thống sẽ boot vào một live system riêng, với disk của bạn được gắn vào nhưng chưa được mount. Khi đó, mọi lệnh bên dưới đều chạy trên một thiết bị mà không có tiến trình nào ghi vào.
Một quy tắc áp dụng cho toàn bộ hướng dẫn này: Không bao giờ ghi các file đã khôi phục, disk image hoặc tool mới cài vào filesystem mà bạn đang khôi phục. Hãy gắn thêm một volume thứ hai hoặc gửi output sang máy khác qua SSH.
Vì sao việc khôi phục sau rm -rf trên ext4 gần như không thể
Hãy xác định đúng kỳ vọng trước khi cài bất kỳ thứ gì. Xác nhận bạn đang dùng filesystem nào:
lsblk -fTrên ext4, filesystem mặc định của gần như mọi VPS image, vị trí dữ liệu của file được lưu trong inode dưới dạng cây extent. Một extent là một bản ghi cho biết block logic N của file bắt đầu tại block vật lý M và kéo dài L block. File nhỏ lưu tối đa 4 bản ghi ngay trong inode. File lớn hơn trỏ đến các block bổ sung chứa phần còn lại của cây.
Khi link cuối cùng đến một file bị xóa, ext4 duyệt cây đó, trả lại mọi extent cho bộ cấp phát block rồi xóa cây khỏi inode. Sau đó inode được đánh dấu là rảnh và ghi thời điểm xóa. Dữ liệu vẫn chưa bị thay đổi. Bản ghi duy nhất về vị trí của dữ liệu đã bị xóa.
Đây là điểm khác với ext3, nơi inode đã xóa vẫn giữ đủ thông tin để công cụ như ext3grep lần theo. Trên ext4, bạn vẫn có thể liệt kê các inode đã xóa:
sudo debugfs -R lsdel /dev/vdb1debugfs mở device ở chế độ chỉ đọc trừ khi bạn truyền -w, nên lệnh này an toàn trên device chưa được mount và không mất gì khi thử. Các inode sẽ được liệt kê. Việc dump một inode là điểm quá trình dừng lại, vì block map mà inode từng chứa đã bị xóa, nên dump không còn gì để lần theo.
Có 2 công cụ cố gắng khắc phục vấn đề này bằng cách đọc journal. Journal là một ring có kích thước cố định mà ext4 dùng để giữ metadata nhất quán sau sự cố, và nó vẫn có thể chứa bản sao cũ của inode trước khi bị xóa. extundelete và ext4magic đều tìm kiếm trong đó. Kiểm tra kích thước bạn đang làm việc:
sudo dumpe2fs -h /dev/vdb1 | grep -i journalJournal chỉ chứa metadata và có kích thước nhỏ, nên hoạt động ghi thông thường sẽ nhanh chóng ghi đè toàn bộ vòng này. Trên một server đang chạy, khoảng thời gian inode trước khi xóa còn tồn tại chỉ tính bằng phút. Cả 2 công cụ đều không còn được duy trì tích cực, và không phải distribution nào cũng đóng gói chúng. Hãy xem đây là phương án ít khả thi, chạy chúng trên device chưa được mount hoặc trên disk image, và đừng ngạc nhiên nếu chúng không trả về kết quả nào.
Nếu lsblk -f báo xfs, tình hình cũng không khả quan hơn, vì XFS cũng không có công cụ undelete được hỗ trợ. Thứ tự các option bên dưới không thay đổi.
File vẫn đang mở trong một tiến trình đang chạy?
Đây là cách khôi phục duy nhất trên trang này có khả năng thành công cao. Vì vậy, bạn không nên khởi động lại service đang sử dụng file.
Một file chỉ thực sự biến mất khi 2 số đếm đều về 0: số directory entry trỏ đến inode của file và số file descriptor đang mở. rm đưa số đếm thứ nhất về 0. Nếu một tiến trình vẫn đang mở file, số đếm thứ hai chưa về 0. Khi đó inode và các block của file vẫn được cấp phát, nên dữ liệu vẫn có thể đọc được.
Tìm các file đang mở nhưng link count đã về 0:
sudo lsof +L1+L1 có nghĩa là liệt kê các file đang mở với link count nhỏ hơn 1. Mỗi kết quả cho biết tiến trình, số file descriptor, một NLINK của 0 và một đường dẫn kết thúc bằng (deleted). Lấy PID và số descriptor rồi dùng chúng với /proc:
sudo ls -l /proc/1234/fdMột entry có dạng 3 -> /var/log/app/events.log (deleted). Liên kết đó vẫn truy cập được dữ liệu. Hãy sao chép dữ liệu sang một filesystem khác:
sudo cp /proc/1234/fd/3 /mnt/rescue/events.logDùng cp, không dùng mv. Mở /proc/1234/fd/3 sẽ tạo một handle mới trên cùng inode và bắt đầu tại offset 0. Nhờ đó, bạn nhận được toàn bộ file thay vì chỉ phần dữ liệu sau vị trí hiện tại của tiến trình ghi.
Có 2 giới hạn cần biết. Cách này không khôi phục được cả directory tree đã bị xóa, vì chỉ những file riêng lẻ mà tiến trình đang mở mới được giữ lại. Ngoài ra, file database được sao chép khi engine đang ghi dở là bản sao nhất quán với trạng thái crash. Vì vậy, hãy dự kiến chạy cơ chế recovery riêng của engine trên bản sao đó, thay vì coi nó là bản sao sạch. Các entry mà lsof hiển thị mem thay cho số descriptor là các vùng được memory map. Những entry này không có /proc/<pid>/fd để sao chép dữ liệu.
Bạn có snapshot trên btrfs, ZFS hoặc LVM không?
Nếu filesystem hỗ trợ snapshot, các file đã xóa vẫn còn nguyên bên trong một snapshot. Cách này chỉ có tác dụng nếu snapshot đã tồn tại trước khi file bị xóa. Snapshot bạn tạo bây giờ không thể khôi phục trạng thái trong quá khứ.
btrfs lưu snapshot dưới dạng subvolume:
sudo btrfs subvolume list /Duyệt snapshot rồi dùng cp -a để sao chép các đường dẫn cần khôi phục. Nên sao chép từng đường dẫn thay vì rollback toàn bộ subvolume, vì rollback cũng loại bỏ mọi dữ liệu được ghi từ thời điểm tạo snapshot đến hiện tại.
ZFS cung cấp mỗi snapshot dưới dạng một thư mục chỉ đọc:
zfs list -t snapshot
ls /tank/data/.zfs/snapshot/Thư mục .zfs bị ẩn và không xuất hiện trong kết quả ls đơn giản tại thư mục gốc của dataset, nhưng bạn có thể truy cập bằng tên thư mục này. Hãy sao chép file ra từ đó. zfs rollback đưa toàn bộ dataset về trạng thái của snapshot được chỉ định và xóa mọi snapshot mới hơn snapshot đó, vì vậy chỉ dùng khi không còn cách nào khác.
Snapshot LVM là volume copy-on-write có kích thước cố định:
sudo lvs
sudo mount -o ro /dev/vg0/data-snap /mnt/snapMount snapshot ở chế độ chỉ đọc rồi sao chép dữ liệu ra. Kiểm tra lvs trước khi sử dụng, vì snapshot LVM sẽ bị kernel vô hiệu hóa nếu hết không gian được cấp phát. Khi đã xảy ra việc đó, dữ liệu bên trong snapshot sẽ mất.
Snapshot không phải là backup. Nó nằm trên cùng disk hoặc cùng pool với dữ liệu gốc, nên cũng chịu mọi lỗi có thể xảy ra với dữ liệu gốc. Snapshot rất phù hợp để hoàn tác một thao tác nhầm xảy ra cách đây hai phút, đúng với mục đích cần dùng ở đây.
Khôi phục bằng PhotoRec trên image, không bao giờ chạy trên đĩa đang hoạt động
Nếu không cách nào ở trên áp dụng được, lựa chọn còn lại là carving: quét thiết bị raw để tìm các mẫu byte đánh dấu phần đầu của một loại file đã biết, rồi ghi phần dữ liệu theo sau ra ngoài. Carving chỉ đọc dữ liệu file. Tên file, cấu trúc thư mục, timestamp và quyền sở hữu đều là metadata của filesystem. Chính metadata đó đã bị rm phá hủy, nên không thể khôi phục lại. Bạn sẽ nhận được các file có tên dạng f0384512.jpg trong một thư mục output được đánh số, rồi phải tự phân loại chúng.
Có 2 quy tắc quyết định việc này có hoạt động hay không.
Thứ nhất, hãy tạo image của thiết bị trước khi cho bất kỳ công cụ nào khác truy cập vào nó. Trên Debian và Ubuntu, package là gddrescue và binary mà package này cài đặt là ddrescue.
sudo apt update && sudo apt install -y gddrescue testdisk
sudo ddrescue -n /dev/vdb1 /mnt/rescue/vdb1.img /mnt/rescue/vdb1.map/mnt/rescue phải nằm trên một thiết bị khác, với dung lượng trống ít nhất bằng dung lượng của partition. lsblk -b in chính xác kích thước theo byte. File map cho phép tiếp tục copy sau khi bị gián đoạn thay vì phải bắt đầu lại. Sau khi có image, bạn có thể dùng một công cụ khác để thử lại trên đúng cùng một tập byte. Điều này không thể làm được nếu công cụ đầu tiên đã ghi đè lên đĩa.
Thứ hai, hãy trỏ công cụ khôi phục vào file image.
sudo photorec /d /mnt/rescue/recup /mnt/rescue/vdb1.imgphotorec mở một menu dạng text. Chọn partition, sau đó chọn loại filesystem, các file signature cần tìm và thư mục đích. Hãy thu hẹp danh sách signature xuống các loại file bạn thực sự bị mất trước khi bắt đầu. Danh sách mặc định sẽ tìm mọi loại file và trả về hàng chục nghìn fragment để bạn sàng lọc.
testdisk, cùng thuộc package này, có chức năng undelete riêng và chỉ hỗ trợ FAT, exFAT, NTFS và ext2. Với ext4, điều đó khiến photorec.
Hãy dự đoán rằng các file bị phân mảnh sẽ được khôi phục không đầy đủ. Carving giả định các block của file nằm liền nhau. Vì vậy, file bị allocator chia ra ở nhiều vị trí trên đĩa có thể bị ghép sai hoặc bị bỏ sót hoàn toàn. Các file media thường được carving khá tốt vì có header rõ ràng. File text thuần, file cấu hình và source code thường được carving kém, vì không có byte signature đánh dấu phần đầu của một shell script.
Khoảng trắng thừa: đường dẫn sai bị xóa như thế nào
Hầu hết sự cố với rm -rf đều bắt nguồn từ shell. rm nhận một danh sách đường dẫn rồi xóa từng đường dẫn theo thứ tự. Nó không biết ý định của bạn là gì.
Trường hợp kinh điển là một khoảng trắng:
rm -rf /home/deploy/app /old
rm -rf /home/deploy/app/oldDòng đầu tiên có 2 đối số. Nó xóa ứng dụng, rồi xóa /old. Nếu /old không tồn tại, rm hoàn toàn không in gì vì -f ẩn lỗi không tìm thấy file. Không có thông báo không có nghĩa là thao tác đã an toàn.
Dạng thứ hai là một biến chưa được đặt trong dấu ngoặc kép và chứa khoảng trắng:
dir="/srv/my app"
rm -rf $dirShell tách giá trị tại khoảng trắng, nên rm nhận /srv/my và app là 2 đường dẫn riêng biệt. Khi được ghi là rm -rf "$dir", đó là một đường dẫn duy nhất.
Dạng thứ ba là một biến rỗng, thường do lệnh đáng lẽ phải gán giá trị cho biến đã fail:
rm -rf "$TARGET"/*Khi TARGET chưa được set, biểu thức đó được mở rộng thành rm -rf /*. GNU rm từ chối dạng trống này: rm -rf / in rm: it is dangerous to operate recursively on '/' rồi dừng. Dạng dùng glob không được bảo vệ như vậy, vì shell thay /* bằng danh sách các đường dẫn cấp cao nhất thực tế trước khi rm chạy; còn / không nằm trong danh sách đó nên guard không bao giờ được kích hoạt.
Các thói quen giúp tránh lặp lại sự cố
- Đặt mọi biến được dùng làm path trong dấu ngoặc kép. Luôn viết
"$dir", kể cả bên trong các phép kiểm tra và vòng lặp. - Dừng khi biến rỗng.
rm -rf "${TARGET:?TARGET is not set}"/*khiến shell dừng và in thông báo của bạn trước khirmchạy nếuTARGETchưa được đặt hoặc đang rỗng. Đặtset -euo pipefailở đầu mọi script có thao tác xóa. - Thêm
--one-file-system. Tùy chọn này yêu cầurmbỏ qua mọi thư mục nằm trên filesystem khác với đối số bạn cung cấp. Nhờ đó, thao tác xóa đệ quy không đi vào volume backup đã mount hoặc bind mount. - Không xóa bằng root. Service account chỉ có thể xóa dữ liệu mà nó sở hữu. Đây là lý do chính để chạy từng service bằng một user không có quyền đặc biệt. Nếu không chắc một account có thể truy cập đến đâu, đọc các bit quyền trong output của ls sẽ trả lời chỉ bằng một lệnh.
- In danh sách trước khi thao tác. Trong script, hãy tạo các path,
printf '%s\n'chúng, đọc output, rồi xóa trong lượt thứ hai. - Để sẵn một lệnh chuyển file vào trash.
sudo apt install trash-clicung cấp cho bạntrash-put,trash-list,trash-restorevàtrash-empty. File đã xóa được chuyển vào~/.local/share/Trash, còntrash-empty 30sẽ xóa mọi thứ cũ hơn thirty days.
Alias rm thành trash-put nghe như bước tiếp theo hiển nhiên, nhưng đó là một cái bẫy. Alias tạo ra một phản xạ sẽ không hoạt động trên server tiếp theo nếu server đó không có alias này. Alias cũng không áp dụng bên trong script, trong khi các lỗi tốn kém thường xảy ra ở đó. Hãy chủ động gõ trash-put.
Khôi phục là cách duy nhất luôn hiệu quả
Mọi cách ở trên đều chỉ là khả năng. Backup thì không.
Có 2 điều khiến một backup trở nên đáng tin cậy. Backup phải chạy theo lịch mà bạn không cần nhớ để kích hoạt. Bạn cũng phải khôi phục từ backup đó ít nhất 1 lần. Một repository chưa từng được dùng để khôi phục chỉ là sự tin tưởng, vì những lỗi khiến nó vô dụng, như sai path trong danh sách include hoặc password của repository không ai ghi lại, chỉ xuất hiện vào ngày bạn cần dùng.
Với restic, khôi phục chỉ cần 2 lệnh.
restic -r /srv/restic-repo snapshots
restic -r /srv/restic-repo restore latest --target /mnt/rescue --include /srv/appdataKhôi phục vào một directory trống thay vì ghi đè lên path đang chạy, để bạn có thể so sánh 2 bên trước khi đưa dữ liệu vào vị trí chính thức. Thiết lập backup restic trên VPS trình bày cách thiết lập repository và systemd timer để chạy backup.
Với Borg:
borg list /srv/borg-repo
borg extract /srv/borg-repo::daily-2026-08-08 srv/appdataCác path bên trong Borg archive được lưu mà không có dấu slash ở đầu, nên srv/appdata sẽ khớp còn /srv/appdata sẽ không khớp gì. borg extract ghi dữ liệu vào working directory hiện tại, vì vậy trước tiên hãy cd vào một scratch directory.
Nếu bạn vẫn chưa chọn giữa 2 công cụ, so sánh restic và Borg trình bày về deduplication và append-only repository. Đây là thuộc tính ngăn một server bị breach xóa lịch sử backup của chính nó. Dùng công cụ nào cũng được. Câu trả lời sai là không chạy công cụ nào.
Server mới là thời điểm rẻ nhất để thiết lập việc này, trước khi trên server có dữ liệu đáng mất. 10 phút đầu tiên trên VPS mới là phần bạn nên xử lý việc đó, cùng với thiết lập SSH và firewall.
Sau đó, hãy tạo một lịch định kỳ trong calendar: mỗi tháng khôi phục 1 directory từ repository vào /tmp rồi đọc các file trong đó. Một thói quen đơn giản này có giá trị hơn mọi công cụ trên trang này.
FAQ
Tôi có thể khôi phục một file đã xóa trên ext4 không?
Thường là không. Khi link cuối cùng trỏ đến một file bị xóa, ext4 xóa extent tree khỏi inode, nên trên disk không còn thông tin nào ghi lại vị trí của dữ liệu. extundelete và ext4magic tìm trong journal của ext4 một bản sao cũ hơn của inode đó. Cách này chỉ hữu ích nếu file vừa bị xóa vài phút trước và filesystem không có hoạt động ghi nào kể từ đó. Cả hai project đều không còn được duy trì tích cực. Hãy chạy một trong hai tool trên device đã unmount hoặc disk image, không chạy trên filesystem đang mount. Trước tiên, dùng sudo dumpe2fs -h /dev/vdb1 | grep -i journal để kiểm tra đúng device hoặc image đang làm việc.
Một service vẫn đang mở file đã xóa. Tôi có thể lấy lại file đó không?
Có. Đây là trường hợp thuận lợi nhất. Khi một process vẫn giữ file ở trạng thái open, inode và các data block của file vẫn được cấp phát, nên dữ liệu vẫn đọc được. Không restart service, vì khi descriptor cuối cùng bị đóng, thao tác xóa sẽ hoàn tất. Chạy sudo lsof +L1 để liệt kê các file đang mở có link count bằng 0. Ghi lại PID và số file descriptor, rồi copy dữ liệu qua /proc bằng sudo cp /proc/1234/fd/3 /mnt/rescue/events.log. Ghi bản sao sang một filesystem khác. Các entry hiển thị mem thay cho số descriptor là các file được memory map, nên không có đường dẫn /proc/<pid>/fd để copy.
Tại sao nên tạo disk image thay vì chạy recovery tool trực tiếp trên disk?
Vì mọi tool đều phải ghi output vào đâu đó. Nếu ghi vào filesystem đang được khôi phục, dữ liệu có thể được ghi lên các free block vẫn còn chứa dữ liệu cần lấy lại. Trước tiên, dùng sudo ddrescue -n /dev/vdb1 /mnt/rescue/vdb1.img /mnt/rescue/vdb1.map để copy partition sang một device khác. Sau đó trỏ photorec đến file image. Disk image cũng cho phép bạn thử tool thứ hai trên đúng cùng một tập byte vào lúc khác. Điều này không thể thực hiện sau khi dữ liệu trên bản gốc đã bị ghi đè.
rm -rf / còn phá hủy được Linux system không?
Lệnh nguyên bản thì không. GNU rm từ chối thực thi và in rm: it is dangerous to operate recursively on '/'. Các dạng lệnh nguy hiểm là những dạng được tạo ra qua một cách khác. rm -rf "$TARGET"/* khi TARGET chưa được set sẽ được mở rộng thành rm -rf /*. Khi đó shell truyền cho rm danh sách các directory cấp cao nhất thực tế. Danh sách này không chứa /, nên cơ chế bảo vệ không được kích hoạt. Thay vào đó, hãy viết "${TARGET:?TARGET is not set}" để shell dừng lại trước khi rm chạy.
Filesystem snapshot có phải là backup không?
Không. Snapshot của btrfs hoặc ZFS nằm trên cùng pool với dữ liệu mà nó bảo vệ, nên khi disk hỏng hoặc pool bị phá hủy thì cả hai đều mất. LVM snapshot còn có vấn đề về kích thước cố định: khi snapshot đầy, kernel sẽ invalidate snapshot và nội dung của nó biến mất. Snapshot rất phù hợp để hoàn tác một thao tác xóa vừa xảy ra 2 phút trước. Với mọi trường hợp khác, hãy lưu một repository trên hardware riêng.