SSD Nodes Learn 🎉 VPS เริ่ม $5.50/เดือน
คู่มือ Matt Connorโดย Matt Connor

วิธีแก้ปัญหา df บอกดิสก์เต็มแต่ du ตรวจไม่พบไฟล์

แก้ปัญหาพื้นที่ดิสก์เต็มแต่หาไฟล์ไม่เจอด้วยคำสั่ง lsof เพื่อค้นหาไฟล์ที่ถูกลบแต่โปรเซสยังเปิดค้างอยู่ รวมถึงสาเหตุจาก inode เต็มและไฟล์ที่ซ่อนอยู่ใต้จุด mount

เหตุใด df จึงรายงานว่าดิสก์เต็มในขณะที่ du รายงานค่าที่ต่างออกไป

df รายงานว่าดิสก์เต็มในขณะที่ du ไม่พบพื้นที่ดังกล่าว เนื่องจากมีกระบวนการ (process) หนึ่งยังคงเปิดไฟล์ที่ถูกลบไปแล้วค้างไว้ การลบไฟล์จะเป็นการนำชื่อไฟล์ออกจากไดเรกทอรีเท่านั้น แต่บล็อกข้อมูลจะถูกปล่อยคืนก็ต่อเมื่อ file descriptor ลำดับสุดท้ายที่ชี้ไปยัง inode นั้นถูกปิดลง du ทำงานโดยการไล่ตามชื่อไฟล์ จึงไม่นับรวมไฟล์ที่ไม่มีชื่อแล้ว df สอบถามไปยังระบบไฟล์ว่ามีการจัดสรรบล็อกไปเท่าใด จึงยังคงนับรวมไฟล์ที่ไม่มีชื่อปรากฏอยู่

คู่มือนี้จะสาธิตสถานการณ์ดังกล่าวบน Ubuntu VPS ทั่วไปโดยใช้เครื่องมือที่มีอยู่แล้ว พร้อมทั้งค้นหากระบวนการที่ถือครองไฟล์นั้นผ่าน /proc และคืนพื้นที่ว่างโดยไม่ต้องรีบูตเครื่อง นอกจากนี้ยังมีสาเหตุอื่นที่ทำให้เกิดอาการเดียวกัน ได้แก่ ตาราง inode เต็ม, ไฟล์ที่ถูกซ่อนอยู่ภายใต้จุด mount, และบล็อกที่ถูกสำรองไว้สำหรับ root

ให้รันคำสั่งแต่ละคำสั่งและอ่านผลลัพธ์ที่ปรากฏ ค่าที่ได้จะขึ้นอยู่กับดิสก์ของคุณ ดังนั้นให้เปรียบเทียบค่าก่อนและหลังบนเครื่องของคุณเอง แทนการเปรียบเทียบกับตัวเลขที่ระบุไว้ในคู่มือ

ความแตกต่างระหว่างสิ่งที่ df และ du นับ

df (disk free) จะสอบถามระบบไฟล์ที่ถูก mount แต่ละรายการว่ามีสถานะอย่างไร เช่น มีบล็อกทั้งหมดเท่าใด ถูกจัดสรรไปแล้วเท่าใด และเหลือพื้นที่ว่างเท่าใด คำสั่งนี้จะไม่เปิดเข้าไปในไดเรกทอรีใดๆ เลย ผลลัพธ์ที่ได้จะครอบคลุมทุกบล็อกที่ถูกจัดสรรไว้ รวมถึงบล็อกที่อยู่ในไฟล์ซึ่งไม่มีชื่อปรากฏอยู่ในไดเรกทอรีใดๆ แล้ว

du (disk usage) ทำงานในทางตรงกันข้าม โดยจะเริ่มจาก path ที่คุณระบุ จากนั้นจะอ่านไดเรกทอรี ตรวจสอบสถานะ (stat) ของทุกรายการที่พบ แล้วนำจำนวนบล็อกมารวมกัน ดังนั้นไฟล์ที่ไม่มีชื่อจึงไม่ถูกนับรวมด้วย รวมถึงไดเรกทอรีใดๆ ที่คำสั่งนี้ไม่มีสิทธิ์อ่าน ซึ่งเป็นเหตุผลว่าทำไมผู้ใช้ทั่วไปจึงได้ผลรวมน้อยกว่า root ควรเรียกใช้ du ภายใต้สิทธิ์ sudo ก่อนที่คุณจะสรุปผลใดๆ จากการเปรียบเทียบ

มีสองตัวเลือกที่สำคัญทุกครั้งที่คุณเปรียบเทียบคำสั่งทั้งสอง:

  • -x จะจำกัดให้ du ทำงานอยู่บนระบบไฟล์เดียวเท่านั้น หากไม่มีตัวเลือกนี้ du / จะเข้าไปยังทุกระบบไฟล์ที่ถูก mount อยู่ภายใต้ / และสร้างผลรวมที่ df / ไม่เคยตรวจวัดมาก่อน
  • -s จะแสดงผลสรุปเพียงบรรทัดเดียวต่อหนึ่งอาร์กิวเมนต์ แทนที่จะแสดงผลทีละบรรทัดสำหรับทุกไดเรกทอรี

นี่คือชุดคำสั่งสำหรับเรียกใช้งานควบคู่กันบนระบบไฟล์ที่คุณต้องการตรวจสอบ:

df -h /
sudo du -xhs / 2>/dev/null

df จะให้คำตอบได้ทันที ในขณะที่ du อาจใช้เวลาหลายนาทีบนระบบไฟล์ขนาดใหญ่ เพราะต้องตรวจสอบสถานะของทุกไฟล์ที่พบระหว่างทาง เมื่อผลรวมของทั้งสองคำสั่งแตกต่างกันมาก และ du ได้ถูกเรียกใช้ในฐานะ root พร้อมกับ -x แสดงว่าพื้นที่ที่หายไปนั้นถูกจัดสรรให้กับสิ่งที่ไม่มีชื่อเรียกนั่นเอง

สร้างสถานการณ์ข้อมูลไม่ตรงกันโดยเจตนา

ให้ดำเนินการบน VPS ทดสอบ คำสั่งทั้งหมดด้านล่างเป็น bash และ coreutils จึงไม่ต้องติดตั้งซอฟต์แวร์ใดๆ เพิ่มเติม

บันทึกสถานะเริ่มต้นของระบบไฟล์ที่เก็บ /var/tmp ไว้

cd /var/tmp
df -h .
df --output=used -B1 .

คำสั่งที่สองจะแสดงจำนวนไบต์ที่ใช้งานจริงโดยไม่มีการปัดเศษ ทำให้การตรวจสอบในตอนท้ายมีความแม่นยำ

จากนั้นสร้างไฟล์ขึ้นมา โดยขนาดของไฟล์จะอ้างอิงจากพื้นที่ว่างที่เครื่องรายงาน เพื่อให้การสาธิตนี้ใช้งานได้กับดิสก์ทุกขนาด

free=$(df --output=avail -B1 . | tail -n 1)
fallocate -l $((free / 10)) ghost.bin
ls -l ghost.bin
df -h .

$(...) คือการทำ command substitution: shell จะรันคำสั่งที่อยู่ภายใน และนำผลลัพธ์ที่ได้มาเป็นค่าของ free หากคุณยังไม่คุ้นเคยกับไวยากรณ์นี้ command substitution ใน bash มีคำอธิบายไว้อย่างครบถ้วน fallocate จะทำการจองบล็อกข้อมูลจริงโดยไม่ต้องเขียนข้อมูลลงไป ซึ่งเป็นเหตุผลว่าทำไมคำสั่งนี้จึงทำงานเสร็จทันที หากระบบไฟล์ไม่รองรับคำสั่งนี้จะล้มเหลว และคุณสามารถใช้ head -c $((free / 10)) /dev/zero > ghost.bin เพื่อทำงานเดียวกันโดยการเขียนไบต์ข้อมูลลงไปแทน

เปรียบเทียบค่า df -h . นี้กับค่าที่คุณบันทึกไว้ก่อนหน้า คอลัมน์ที่แสดงพื้นที่ใช้งานจะเพิ่มขึ้น และคอลัมน์ที่แสดงพื้นที่ว่างจะลดลง

จากนั้นให้เปิดไฟล์ค้างไว้จากอีกกระบวนการหนึ่ง แล้วทำการลบไฟล์นั้นทิ้ง

sleep infinity < ghost.bin &
holder=$!
rm ghost.bin
ls -l ghost.bin
df -h .
sudo du -xhs . 2>/dev/null

การทำ redirection คือหัวใจสำคัญของเทคนิคนี้ sleep infinity < ghost.bin & จะเริ่มกระบวนการเบื้องหลัง (background process) ที่มี standard input เป็นไฟล์ดังกล่าว shell จะเปิดไฟล์และส่ง file descriptor ให้กับ sleep ซึ่งจะทำให้ไฟล์ถูกเปิดค้างไว้ $! จะเก็บค่า process ID ของงานเบื้องหลังนั้นไว้ จากนั้น rm จะลบชื่อไฟล์ออกในขณะที่ descriptor ยังคงเปิดอยู่

อ่านผลลัพธ์ที่ได้ ls จะไม่พบไฟล์ดังกล่าวเนื่องจากชื่อไฟล์ถูกลบไปแล้ว du จะกลับไปใกล้เคียงกับค่าเริ่มต้นเนื่องจากเป็นการไล่ตรวจสอบตามชื่อไฟล์ ส่วน df จะไม่มีการเปลี่ยนแปลงเนื่องจากบล็อกข้อมูลยังคงถูกจองไว้ ระบบไฟล์และโครงสร้างไดเรกทอรีจึงไม่ตรงกันอีกต่อไป และช่องว่างระหว่างค่าทั้งสองนี้ก็คือไฟล์ที่คุณเพิ่งลบทิ้งไปนั่นเอง

การค้นหาโพรเซสที่ถือครองไฟล์ที่ถูกลบ

File descriptor ที่เปิดอยู่ทั้งหมดจะปรากฏภายใต้ /proc/<pid>/fd/ ในรูปแบบ symbolic link ที่ชี้ไปยังไฟล์นั้นๆ เมื่อไฟล์ถูก unlinked เคอร์เนลจะทำเครื่องหมายที่ปลายทางของลิงก์นั้นว่าถูกลบไปแล้ว ดังนั้นการค้นหาผู้ถือครองไฟล์จึงหมายถึงการค้นหาลิงก์ที่มีปลายทางติดเครื่องหมายดังกล่าว

sudo find /proc/[0-9]*/fd -lname '*(deleted)' -printf '%p -> %l\n' 2>/dev/null

-lname จะจับคู่กับปลายทางของ symbolic link แทนที่จะเป็นชื่อของมัน ส่วน %p จะพิมพ์พาธของ descriptor ออกมา และ %l จะพิมพ์สิ่งที่ลิงก์นั้นชี้ไป โดยเลข PID จะเป็นองค์ประกอบที่สองของพาธที่พิมพ์ออกมา ให้รันคำสั่งด้วย sudo เพราะหากไม่ทำเช่นนั้น คุณจะสามารถอ่านได้เพียง /proc/<pid>/fd สำหรับโพรเซสของคุณเองเท่านั้น การ redirect stderr จะช่วยตัดข้อความแจ้งเตือนจากโพรเซสที่จบการทำงานไปในระหว่างที่ find กำลังทำงานอยู่

เซิร์ฟเวอร์ที่มีภาระงานสูงมักถือครองไฟล์ที่ถูกลบอยู่หลายไฟล์ตลอดเวลา ซึ่งส่วนใหญ่มีขนาดเล็กและไม่เป็นอันตราย ให้เรียงลำดับตามขนาดเพื่อให้ไฟล์ที่น่าสนใจที่สุดอยู่ด้านบน

sudo bash -c 'for fd in /proc/[0-9]*/fd/*; do
  target=$(readlink "$fd" 2>/dev/null) || continue
  case "$target" in
    *"(deleted)") echo "$(stat -Lc %s "$fd" 2>/dev/null) $fd $target" ;;
  esac
done' | sort -rn | head

stat -L จะติดตามลิงก์ไปยัง inode โดยตรง ดังนั้น %s จะรายงานขนาดของไฟล์ที่ไม่มีชื่อเรียกอีกต่อไป การเรียงลำดับด้วยตัวเลขนี้จะทำให้ไฟล์ที่มีขนาดใหญ่ที่สุดขึ้นมาอยู่ลำดับแรก

จากนั้นให้ระบุโพรเซสที่อยู่เบื้องหลัง descriptor ที่พบ พาธที่อยู่ด้านบนสุดของรายการจะมีตัวเลขทั้งสองที่คุณต้องการอยู่ ให้กำหนดค่าเหล่านั้นลงในตัวแปร โดยแทนที่ PID และ N ด้วยค่าที่คำสั่งของคุณแสดงผลออกมา

pid=PID
n=N
ps -o pid,user,etime,args -p "$pid"
sudo stat -L "/proc/$pid/fd/$n"

ps จะระบุชื่อโปรแกรมและแสดงระยะเวลาที่รันอยู่ ส่วน stat -L จะพิมพ์ขนาดและจำนวนบล็อกที่ถูกจัดสรรของ inode ที่ถูกลบ ข้อมูลเหล่านี้จะช่วยตอบคำถามสำคัญว่าบริการใดที่กำลังรักษาไฟล์นี้ไว้ไม่ให้ถูกลบออกไปจริง

หากเครื่องมี lsof ติดตั้งอยู่แล้ว sudo lsof +L1 จะแสดงรายการไฟล์ที่เปิดอยู่ซึ่งมีค่า link count ลดลงเหลือศูนย์และแสดงขนาดในตารางเดียว เครื่องมือนี้ไม่มีอยู่ใน Ubuntu รุ่น minimal และการติดตั้งแพ็กเกจบนระบบไฟล์ที่ไม่มีพื้นที่ว่างอาจล้มเหลวได้ ดังนั้นการใช้ /proc จึงเป็นวิธีที่ใช้งานได้เสมอในทุกสถานการณ์

การคืนพื้นที่โดยไม่ต้องรีบูต

การรีบูตช่วยแก้ปัญหาได้จริง แต่ไม่ใช่ขั้นตอนแรกที่ควรทำ เพราะจะทำให้บริการหยุดทำงานและทำลายหลักฐานไปทั้งหมด มีทางเลือกที่นุ่มนวลกว่า 4 วิธี โดยควรลองทำตามลำดับดังนี้

ขั้นแรก ให้คัดลอกข้อมูลออกมาหากคุณยังต้องการใช้งานอยู่ การอ่านจาก path ของ descriptor จะเป็นการอ่านจาก inode ที่ยังทำงานอยู่จริง

sudo cp /proc/<pid>/fd/<n> /root/recovered.log

นี่เป็นกรณีเดียวที่ไฟล์ซึ่งถูกลบไปแล้วสามารถกู้คืนได้ง่าย ซึ่งเป็นเหตุผลว่าทำไม การกู้คืนไฟล์ที่ถูกลบด้วย rm -rf จึงเริ่มต้นด้วยการตรวจสอบว่ายังมี process ใดที่เปิดไฟล์นั้นค้างไว้อยู่หรือไม่ เมื่อ descriptor สุดท้ายถูกปิด เส้นทางนี้จะหายไปทันที

ขั้นที่สอง ให้ล้างเนื้อหาในไฟล์ผ่าน descriptor นั้น path /proc จะชี้ไปยัง inode เดียวกัน ดังนั้นการสั่ง truncate จะเป็นการคืนพื้นที่บล็อกในขณะที่ process ยังคงทำงานอยู่

sudo truncate -s 0 "/proc/$pid/fd/$n"
df -h /

วิธีนี้จะได้ผลดีเมื่อผู้เขียนไฟล์เปิดไฟล์ในโหมด append เพราะการเขียนทุกครั้งจะไปต่อท้ายไฟล์ปัจจุบันเสมอ หากไม่ใช่โหมดดังกล่าว process จะยังคงใช้ write offset เดิม ทำให้การเขียนครั้งถัดไปกระโดดไปไกลและสร้างไฟล์ขึ้นมาใหม่โดยมีช่องว่าง (hole) อยู่ด้านหน้า ช่องว่างเหล่านี้ไม่ได้ถูกจองพื้นที่ไว้ บล็อกจึงยังคงว่างอยู่และ df จะยังคงแสดงพื้นที่ที่เพิ่งคืนกลับมา สิ่งที่กลับมามีเพียงขนาดไฟล์เท่านั้น หากรัน sudo stat -L "/proc/$pid/fd/$n" อีกครั้งหลังจาก process เขียนข้อมูล มันจะแสดงขนาดไฟล์เดิมคู่กับจำนวนบล็อกที่ไม่สัมพันธ์กัน ให้รีสตาร์ท process หากคุณต้องการให้ขนาดไฟล์เริ่มต้นจากศูนย์ใหม่

ขั้นที่สาม ให้สั่งให้ service เปิดไฟล์ log ใหม่ daemon ที่ไฟล์ log ถูกลบไปในขณะที่ยังทำงานอยู่เป็นปัญหาที่พบบ่อยที่สุด daemon หลายตัวจะเปิดไฟล์ log ใหม่เมื่อได้รับสัญญาณ (signal): nginx ใช้ SIGUSR1 และ rsyslog ใช้ SIGHUP ให้ตรวจสอบเอกสารประกอบของ daemon นั้นๆ แทนการเดา เพราะการส่งสัญญาณผิดประเภทไปยัง daemon อาจทำให้มันหยุดทำงานได้

sudo systemctl kill -s USR1 nginx

ขั้นที่สี่ ให้รีสตาร์ท unit นั้น sudo systemctl restart <unit> จะปิด descriptor ทุกตัวที่ process เก่าถือครองอยู่ ทำให้บล็อกถูกคืนกลับมาอย่างแน่นอน สำหรับตัวอย่างข้างต้น ผู้ถือครองไฟล์คือ sleep ที่คุณเริ่มขึ้นมาเอง ดังนั้นการหยุดการทำงานของมันจึงเพียงพอแล้ว

kill $holder
df -h .
df --output=used -B1 .
sudo find /proc/[0-9]*/fd -lname '*(deleted)' -printf '%p -> %l\n' 2>/dev/null

เปรียบเทียบจำนวนไบต์ที่ใช้กับค่าที่คุณบันทึกไว้ก่อนสร้างไฟล์ ทั้งสองค่าจะกลับมาตรงกันอีกครั้ง และ find จะไม่แสดง descriptor ของคุณอีกต่อไป การตรวจสอบด้วยคำสั่งเดิมที่ใช้ค้นหาปัญหาเป็นนิสัยที่ดีควรทำ

การเฝ้าดูค่าที่เปลี่ยนแปลงนั้นง่ายกว่าการรัน df ด้วยตนเองซ้ำๆ watch จะทำซ้ำคำสั่งตามช่วงเวลาที่กำหนด และแสดงผลลัพธ์ใหม่ในตำแหน่งเดิม ดังนั้น watch df -h / จะแสดงให้เห็นว่าคอลัมน์ที่ใช้พื้นที่เปลี่ยนแปลงไปอย่างไรเมื่อพื้นที่ถูกคืนกลับมา

เมื่อผลรวมตรงกันแต่ดิสก์ยังคงเต็ม

หาก df และ root du -x แสดงค่าตรงกัน แสดงว่าไม่มีไฟล์ที่ถูกลบตกค้างอยู่ สาเหตุที่เหลือจะมีลักษณะแตกต่างออกไปและต้องตรวจสอบแยกกันในแต่ละกรณี

พื้นที่ inode เต็ม แต่พื้นที่ block ยังเหลือ

inode ทำหน้าที่เก็บข้อมูล metadata ของไฟล์หนึ่งไฟล์ ระบบไฟล์ ext4 จะกำหนดจำนวน inode ไว้คงที่ตั้งแต่ตอนสร้างระบบไฟล์ ดังนั้นระบบไฟล์อาจเกิดอาการ inode เต็มในขณะที่ยังมีพื้นที่ block เหลืออยู่ ทำให้ไม่สามารถสร้างไฟล์ใหม่ได้แม้ว่า df -h จะแสดงว่ายังมีพื้นที่ว่างเหลืออยู่ก็ตาม

df -h /
df -i /

คำสั่งแรกใช้สำหรับนับจำนวน block และคำสั่งที่สองใช้สำหรับนับจำนวน inode ให้เปรียบเทียบค่าในคอลัมน์การใช้งานของแต่ละคำสั่ง หากการใช้งาน block อยู่ในระดับต่ำแต่การใช้งาน inode เต็มขีดจำกัด แสดงว่าปัญหาเกิดจากการมีไฟล์ขนาดเล็กจำนวนมหาศาล

df ไม่ยอมรับการใช้ -i และ --output พร้อมกันในคำสั่งเดียว ดังนั้นหากคุณต้องการค่าจำนวนไฟล์ดิบเพื่อนำไปอ่านหรือส่งต่อให้คำสั่งอื่น ให้เลือกเฉพาะฟิลด์ inode ตามชื่อและไม่ต้องใส่ -i

df --output=itotal,iused,iavail,ipcent /

คอลัมน์เหล่านั้นแสดงข้อมูลการนับแบบเดียวกับที่ df -i แสดงผล ในรูปแบบที่คุณสามารถนำไปประมวลผลต่อได้

ค้นหาไฟล์เหล่านั้นด้วยการนับจำนวนรายการแทนการนับจำนวนไบต์

sudo du --inodes -x -d 1 / 2>/dev/null | sort -rn | head

ทำซ้ำคำสั่งเดิมในระดับไดเรกทอรีที่พบว่ามีจำนวนไฟล์มากที่สุดไปเรื่อยๆ จนกว่าจะพบโครงสร้างไดเรกทอรีที่เป็นต้นเหตุของการสร้างไฟล์เหล่านั้น หาก du ของคุณไม่รองรับ --inodes คุณสามารถใช้ sudo find /var -xdev -type f | wc -l เพื่อค่อยๆ นับจำนวนไฟล์ในโครงสร้างไดเรกทอรีย่อยได้

วิธีแก้ไขคือการลบหรือย้ายไฟล์เหล่านั้นออกไป คุณไม่สามารถเพิ่มจำนวน inode ให้กับระบบไฟล์ ext4 ที่มีอยู่เดิมได้ เนื่องจากจำนวน inode ถูกกำหนดไว้ตายตัวตั้งแต่ตอน mkfs ดังนั้นหากต้องการเพิ่มจำนวน inode คุณต้องสร้างระบบไฟล์ขึ้นมาใหม่และกู้คืนข้อมูลจากสำรอง สำหรับ XFS นั้นจะจัดสรร inode ตามความจำเป็น จึงไม่มีข้อจำกัดเรื่องจำนวน inode คงที่เหมือนกับ ext4 เครื่องเซิร์ฟเวอร์ที่รัน container มักจะประสบปัญหาพื้นที่เต็มเร็วกว่าปกติ เนื่องจากเลเยอร์ของ image ประกอบด้วยไฟล์ขนาดเล็กจำนวนมาก สำหรับเครื่องประเภทนี้ การล้างข้อมูลดิสก์ของ Docker บน VPS คือวิธีแก้ไขที่ตรงจุดที่สุด ซึ่งจะช่วยคืนพื้นที่ได้มากกว่าการไล่ลบไฟล์ทั่วไปในระบบไฟล์

พื้นที่ที่ถูกซ่อนอยู่ใต้จุด mount

ไดเรกทอรีสามารถเก็บไฟล์ไว้ได้ก่อนที่จะมีการ mount อุปกรณ์ใดๆ ลงไป เมื่อคุณ mount ระบบไฟล์ทับลงบนไดเรกทอรีนั้น ไฟล์ที่อยู่ข้างใต้จะยังคงอยู่ที่เดิมทุกประการ ทั้งยังถูกจัดสรรพื้นที่ไว้ และยังถูกนับโดย df แต่จะไม่สามารถเข้าถึงได้ผ่านชื่อไฟล์อีกต่อไป du จะมองไม่เห็นไฟล์เหล่านั้นเนื่องจากจุด mount ได้บังไว้

สาธิตการทำงานนี้ด้วย tmpfs ซึ่งไม่จำเป็นต้องใช้ดิสก์สำรอง ขั้นตอนนี้ต้องทำบนเครื่องที่คุณมีสิทธิ์ในการ mount เช่น KVM VPS

sudo mkdir -p /srv/covered
sudo cp /etc/services /srv/covered/
ls /srv/covered
sudo mount -t tmpfs tmpfs /srv/covered
ls /srv/covered
sudo umount /srv/covered
ls /srv/covered

ls ตรงกลางแสดงให้เห็นไดเรกทอรีที่ว่างเปล่า การคัดลอกไฟล์ไม่ได้หายไปไหน แต่ยังคงอยู่บน root filesystem และจะปรากฏกลับมาทันทีที่คุณ unmount ลองจินตนาการถึงบริการที่เขียน log ลงใน path นั้นมาเป็นเดือน ก่อนที่จะมีคนนำ volume มา mount ทับ

หากต้องการค้นหาไฟล์ที่แท้จริงบนเซิร์ฟเวอร์ที่กำลังทำงานอยู่ ให้ทำการ mount root filesystem ซ้ำอีกครั้งในตำแหน่งอื่น การทำ bind mount จะแสดงระบบไฟล์หนึ่งโดยไม่รวมระบบไฟล์ที่ถูก mount ซ้อนอยู่ภายใน

sudo mkdir -p /mnt/rootcheck
sudo mount --bind / /mnt/rootcheck
sudo du -xhs /mnt/rootcheck/* 2>/dev/null | sort -h
sudo umount /mnt/rootcheck

ไฟล์ใดก็ตามที่ปรากฏในรายการนั้นแต่ไม่ปรากฏใน path ปกติ แสดงว่าไฟล์เหล่านั้นถูกฝังอยู่ใต้จุด mount ให้ unmount bind mount ออกเมื่อเสร็จสิ้น มิฉะนั้นการเรียกใช้ du โดยไม่มีแฟล็ก -x จะนับไฟล์ชุดเดียวกันซ้ำสองครั้ง

บล็อกที่สงวนไว้สำหรับ root

ext4 จะกันพื้นที่บล็อกส่วนหนึ่งไว้สำหรับผู้ใช้ root เพื่อป้องกันไม่ให้ดิสก์เต็มจน root ไม่สามารถล็อกอินเข้ามาซ่อมแซมระบบได้ กระบวนการที่รันด้วยสิทธิ์ผู้ใช้ทั่วไปจะพบข้อจำกัดนี้ก่อน ในขณะที่ df ยังคงแสดงพื้นที่ว่างเหลืออยู่เล็กน้อย ให้ตรวจสอบการตั้งค่าบนระบบไฟล์ของคุณเองแทนการคาดเดาจากค่าเริ่มต้น

dev=$(df --output=source / | tail -n 1)
sudo tune2fs -l "$dev" | grep -i 'block count'

คำสั่งดังกล่าวจะแสดงจำนวนบล็อกทั้งหมดและจำนวนบล็อกที่ถูกสงวนไว้ในหน่วยเดียวกัน ทำให้สามารถคำนวณอัตราส่วนระหว่างกันได้โดยตรง df จะรายงานคอลัมน์ available เป็นพื้นที่ที่ผู้ใช้ทั่วไปสามารถใช้งานได้จริง ซึ่งเป็นเหตุผลว่าทำไมผลรวมของ used และ available จึงน้อยกว่าขนาดพื้นที่รวม ช่องว่างที่หายไปนั้นคือส่วนที่ถูกสงวนไว้

คุณสามารถปรับเปลี่ยนค่านี้ได้ด้วย sudo tune2fs -m <percent> "$dev" การเปลี่ยนแปลงจะมีผลทันทีโดยไม่จำเป็นต้อง remount การลดพื้นที่สงวนบนระบบไฟล์สำหรับเก็บข้อมูลแยกต่างหากถือเป็นเรื่องสมเหตุสมผล แต่สำหรับระบบไฟล์ root ควรเหลือพื้นที่ไว้เพียงพอเพื่อให้ root ยังสามารถเขียนข้อมูลได้ เนื่องจากระบบไฟล์ root ที่ไม่มีพื้นที่ว่างเหลืออยู่เลยจะซ่อมแซมได้ยากกว่ามาก tune2fs สามารถใช้ได้กับ ext2, ext3 และ ext4 ส่วน XFS ไม่มีการตั้งค่าในลักษณะเดียวกันนี้

กรณีที่ du อาจทำให้คุณเข้าใจผิด

นิสัย 4 ประการของ du ทำให้ผลรวมที่ได้ดูไม่ถูกต้อง

  • Hard links: du จะนับ inode เพียงครั้งเดียวแม้จะมีหลายชื่อที่ชี้ไปยังไฟล์เดียวกัน ดังนั้นโครงสร้างไดเรกทอรีที่มี hard links จำนวนมากจะรายงานขนาดรวมน้อยกว่าผลรวมขนาดของไฟล์จริง
  • Sparse files: du จะรายงานเฉพาะบล็อกที่มีการจัดสรรจริง ในขณะที่ ls -l จะรายงานขนาดตามที่ปรากฏ ให้เพิ่มแฟล็ก --apparent-size เพื่อดูตัวเลขอีกค่าหนึ่ง
  • Permissions: หากรันในฐานะผู้ใช้ทั่วไป du จะข้ามไฟล์ที่ไม่สามารถอ่านได้และรายงานขนาดต่ำกว่าความเป็นจริง ข้อผิดพลาดที่แสดงออกมามักถูกผู้ใช้เปลี่ยนเส้นทางไปยัง /dev/null และไม่ได้อ่านมัน
  • Filesystem boundaries: หากไม่มี -x ตัว du / จะนับทุกไฟล์ระบบที่ถูก mount อยู่ภายใต้ / ทำให้ผลรวมอาจมากกว่าที่ df / รายงาน

df ยังมีนิสัยอีกประการที่ควรทราบ โดยมันจะรายงานแต่ละไฟล์ระบบแยกกัน ดังนั้นควรเรียกใช้คำสั่งกับ path ที่เกิดปัญหาการเขียนข้อมูลโดยตรง ไฟล์ระบบ /boot แยกต่างหากอาจเต็มได้ตามรอบการทำงานของมันเมื่อมีแพ็กเกจเคอร์เนลสะสมมากขึ้น และ การลบเคอร์เนลเก่าบน Ubuntu เป็นงานที่แยกต่างหากจากการเคลียร์พื้นที่บน /

ลำดับขั้นตอนการทำงานเมื่อเกิดเหตุการณ์จริง

  1. เรียกใช้ df -h <path> และ df -i <path> บนระบบไฟล์ที่การเขียนข้อมูลล้มเหลว ไม่ใช่เรียกใช้บน / ตามความเคยชิน
  2. เรียกใช้ sudo du -xh -d 1 <mountpoint> 2>/dev/null | sort -h จากนั้นไล่ตรวจสอบลงไปในไดเรกทอรีที่มีขนาดใหญ่ที่สุด
  3. หาก du ไม่สามารถระบุที่มาของพื้นที่ที่ถูกใช้งานตามที่ df รายงานได้ ให้ค้นหาใน /proc เพื่อหาไฟล์ที่ถูกลบไปแล้วแต่ยังคงถูกเปิดใช้งานอยู่
  4. หากทั้งสองค่าตรงกัน ให้ทำ bind mount ระบบไฟล์นั้นไปยังตำแหน่งอื่น แล้วตรวจสอบหาไฟล์ที่อาจซ่อนอยู่ภายใต้จุด mount point
  5. หากปัญหาเกิดจากจำนวน inode เต็ม ให้ใช้วิธีนับจำนวนไฟล์แทนการนับขนาดเป็นไบต์

ทุกขั้นตอนข้างต้นคือคำสั่งที่คุณสามารถอ่านผลลัพธ์ได้ นี่คือความแตกต่างระหว่างการแก้ไขปัญหาอย่างมีหลักการกับการคาดเดาไปเรื่อยๆ

FAQ

ทำไม df ถึงแสดงว่าดิสก์เต็มทั้งที่ du พบข้อมูลน้อยกว่ามาก?

สาเหตุทั่วไปคือไฟล์ถูกลบไปแล้วในขณะที่ยังมี process เปิดใช้งานอยู่ การลบไฟล์จะทำให้ entry ใน directory หายไป ดังนั้น du จึงไม่มีชื่อไฟล์ให้ไล่ตรวจสอบและหยุดนับไฟล์นั้น แต่ inode และ block ของไฟล์จะยังคงถูกจองไว้จนกว่า descriptor ตัวสุดท้ายจะถูกปิด ซึ่ง df จะนับรวม block ที่ถูกจองไว้ทั้งหมด ให้ค้นหาใน /proc/<pid>/fd เพื่อหา symbolic link ที่มีเป้าหมายเป็นไฟล์ที่ถูกลบไปแล้ว คุณจะพบทั้งตัวไฟล์และ process ที่ถือไฟล์นั้นอยู่ ก่อนจะเชื่อผลการเปรียบเทียบ ให้ยืนยันว่าคุณได้รัน du ในฐานะ root และใช้แฟล็ก -x แล้ว เพราะผู้ใช้ทั่วไปจะข้าม directory ที่ไม่มีสิทธิ์อ่านไปโดยไม่แจ้งเตือน

ฉันจะหาไฟล์ที่ถูกลบไปแล้วแต่ยังเปิดใช้งานอยู่โดยไม่ใช้ lsof ได้อย่างไร?

ให้ใช้บันทึก open descriptor ของ kernel โดยตรง sudo find /proc/[0-9]*/fd -lname '*(deleted)' -printf '%p -> %l\n' 2>/dev/null จะแสดงรายการ descriptor ทุกตัวที่ชี้ไปยังไฟล์ที่ไม่มีชื่อ และ process ID จะปรากฏอยู่ใน path ที่แสดงผล การใช้ sudo stat -Lc %s บน path ของ descriptor เหล่านั้นจะรายงานขนาดไฟล์ ทำให้คุณสามารถเรียงลำดับและเลือกไฟล์ที่สำคัญได้ วิธีนี้ไม่จำเป็นต้องติดตั้งแพ็กเกจใดๆ ซึ่งสำคัญมากเพราะการติดตั้งซอฟต์แวร์บนระบบไฟล์ที่ไม่มีพื้นที่ว่างมักจะล้มเหลว

ฉันสามารถคืนพื้นที่โดยไม่ต้องสั่ง kill process ได้หรือไม่?

ทำได้ในบางกรณี sudo truncate -s 0 /proc/<pid>/fd/<n> สามารถเข้าถึง inode เดียวกันผ่าน descriptor และปลดปล่อย block ของไฟล์ในขณะที่ process ยังทำงานอยู่ วิธีนี้สะอาดที่สุดหาก process เปิดไฟล์ในโหมด append เพราะการเขียนข้อมูลจะไปต่อท้ายไฟล์เสมอ หากไม่ใช่เช่นนั้น write offset จะค้างอยู่ที่เดิม และการเขียนครั้งถัดไปจะสร้างไฟล์ขึ้นใหม่โดยมีช่องว่าง (hole) อยู่ที่ส่วนหน้า ทำให้ขนาดไฟล์ที่รายงานกลับมาเท่าเดิมในขณะที่ block ใต้ช่องว่างนั้นยังคงว่างอยู่ การรีสตาร์ท unit หรือส่งสัญญาณให้ process เปิดไฟล์ log ใหม่ตามที่ระบุในเอกสารประกอบของซอฟต์แวร์นั้น เป็นวิธีแก้ไขที่ไม่ทิ้งไฟล์แบบ sparse ไว้

df แสดงว่ามีพื้นที่ว่าง แต่ยังเขียนไฟล์ไม่ได้ เกิดจากอะไรได้บ้าง?

ให้ตรวจสอบ inode ด้วย df -i บน path เดียวกัน เนื่องจากระบบไฟล์ที่มี block ว่างแต่ไม่มี inode เหลืออยู่จะไม่สามารถสร้างไฟล์ใหม่ได้ ให้ตรวจสอบว่าการเขียนไฟล์นั้นทำโดยผู้ใช้ที่ไม่ใช่ root บนระบบไฟล์ ext4 ที่เหลือเพียง block สำรองหรือไม่ ซึ่งสามารถดูได้จาก sudo tune2fs -l บนอุปกรณ์นั้นๆ และตรวจสอบให้แน่ใจว่าคุณกำลังอ่านค่าระบบไฟล์ที่เขียนข้อมูลลงไปจริงๆ เพราะ /boot หรือ /var ที่แยกออกมาจะมีพื้นที่เต็มแยกจาก /

ทำไม du ถึงรายงานผลรวมใหญ่กว่า df?

การรัน du โดยไม่มี -x จะข้ามเข้าไปในทุกระบบไฟล์ที่ mount อยู่ภายใต้ path ที่คุณระบุ ทำให้เป็นการรวมผลจากหลายระบบไฟล์ ในขณะที่ df จะแสดงผลเฉพาะระบบไฟล์เดียว การทำ bind mount จะทำให้ปัญหานี้แย่ลงเพราะไฟล์เดียวกันจะถูกนับซ้ำในทุก path ที่ปรากฏ ให้เพิ่ม -x เพื่อให้ du ทำงานอยู่บนระบบไฟล์เดียว และระบุ path เดียวกันให้กับ df เพื่อให้ทั้งสองคำสั่งอ้างอิงถึงสิ่งเดียวกัน