บทความ › เซิร์ฟเวอร์และ DevOps
ดิสก์เต็มจนเซิร์ฟเวอร์ล่ม: หาตัวการ แล้วจำกัด journald, logrotate, Docker logs
เซิร์ฟเวอร์หยุดเพราะดิสก์เต็ม หาว่าอะไรกินที่ด้วย df, du, ncdu ตัวการที่เจอบ่อย วิธีตั้งเพดาน journald, logrotate, Docker log และตั้งเตือนที่ 80%
- ไล่จากบนลงล่าง: df -h ดูว่าเต็มที่ไหน แล้ว du หรือ ncdu เจาะทีละโฟลเดอร์
- ตัวการที่เจอบ่อยคือ log ที่ไม่มีเพดาน: Docker json-file ค่าเริ่มต้นไม่หมุนเลย ส่วน journald ใช้ได้ถึง 10% ของดิสก์
- ตั้งเพดานทุกแหล่ง แล้ว log จะหยุดโต พื้นที่ที่ใช้กลายเป็นค่าคงที่แทนเส้นที่ขึ้นทุกวัน
- ตั้งเตือนที่ 80% และคำนวณว่าเหลือกี่วันจากอัตราโตต่อวัน
เซิร์ฟเวอร์ที่ดิสก์เต็ม 100% ไม่ได้แค่ช้า database เขียนไม่ได้ แอปที่ต้องเขียนไฟล์พังตาม และส่วนใหญ่ตัวการไม่ใช่ข้อมูลจริง แต่เป็น log ที่โตเรื่อย ๆ โดยไม่มีใครตั้งเพดาน บทความนี้ไล่วิธีหาว่าอะไรกินที่ ล้างให้ถูก และตั้งเพดานทุกแหล่งให้เรื่องนี้ไม่เกิดซ้ำ
ทดสอบบน Ubuntu 24.04 (systemd 255, logrotate 3.21) และ Docker Engine ตัวเลข default ตรวจกับ man page ของ systemd 255, logrotate และเอกสาร Docker
ขั้นที่ 1: เต็มที่ไหน
df -h # พื้นที่แต่ละ filesystem
df -i # inode ถ้า df -h ยังว่างแต่สร้างไฟล์ไม่ได้ ให้ดูตัวนี้
ดูคอลัมน์ Use% หา filesystem ที่ใกล้ 100% ส่วนใหญ่คือ / แล้วเจาะลงทีละชั้น
sudo du -xh --max-depth=1 / 2>/dev/null | sort -h | tail -15
sudo du -xh --max-depth=1 /var 2>/dev/null | sort -h | tail -15
-x ไม่ข้ามไป filesystem อื่น sort -h เรียงตามขนาดที่อ่านแบบ K/M/G ได้ ถ้าลงได้ ใช้ ncdu สะดวกกว่า เดินเข้าออกโฟลเดอร์ได้ด้วยลูกศร
sudo apt install ncdu
sudo ncdu -x /
ตัวการที่เจอบ่อย
| ตัวการ | ดูขนาด | ค่าเริ่มต้นที่ทำให้โต |
|---|---|---|
| journald | journalctl --disk-usage | SystemMaxUse = 10% ของ filesystem (ไม่เกิน 4G) |
| log ของแอป | du -sh /var/log/* /srv/*/logs | ไม่มี logrotate config = ไม่หมุนเลย |
| Docker container log (json-file) | sudo du -sh /var/lib/docker/containers/*/*-json.log | max-size = -1 ไม่จำกัด |
| Docker image, build cache | docker system df | ไม่ลบเองเลย |
| kernel เก่า | dpkg -l 'linux-image-*' | grep ^ii | ค้างทุกครั้งที่อัปเดต kernel |
| ไฟล์ที่ลบแล้วแต่ยังถูกเปิด | sudo lsof +L1 | process ยังถือพื้นที่ |
แถวสุดท้ายหลอกคนบ่อยที่สุด rm log ขนาด 10 GB ที่แอปเขียนอยู่ ชื่อไฟล์หายแต่พื้นที่ไม่คืน จนกว่า process จะปิดไฟล์ ถ้าต้องล้างไฟล์ที่ยังถูกเขียน ใช้ sudo truncate -s 0 ไฟล์ แทน
ล้างของเฉพาะหน้า
sudo journalctl --rotate --vacuum-size=500M # archive ไฟล์ปัจจุบัน แล้วเหลือไม่เกิน 500 MB
docker system df # ดูก่อนว่าอะไรกินที่
docker image prune -a # ลบ image ที่ไม่มี container ใช้ (ถามยืนยันก่อน)
docker builder prune # ล้าง build cache
sudo apt autoremove --purge # kernel และแพ็กเกจที่ไม่ต้องใช้แล้ว
sudo apt clean # ไฟล์ .deb ที่ดาวน์โหลดค้างไว้
--vacuum-size ลบเฉพาะไฟล์ journal ที่ archive แล้ว จึงต้องมี --rotate นำหน้า ระวัง docker image prune -a ลบ image ทุกตัวที่ไม่มี container อ้างถึง รวมตัวที่จะใช้ rollback ด้วย ถ้า deploy ด้วย tag เก่าสำรองไว้ ใช้ docker image prune (ไม่มี -a) ที่ลบเฉพาะ dangling image ก่อน
ล้างแล้วไม่ตั้งเพดาน = นัดเจอกันใหม่อีกเดือน
ตั้งเพดาน 1: journald
ค่าเริ่มต้นให้ journal ใช้ได้ถึง 10% ของ filesystem สูงสุด 4G บนดิสก์ 50 GB คือ 5 GB → เพดานจริง 4 GB ซึ่งเยอะไปสำหรับเซิร์ฟเวอร์เล็ก ตั้งผ่าน drop-in แทนการแก้ไฟล์หลัก
sudo mkdir -p /etc/systemd/journald.conf.d
sudo tee /etc/systemd/journald.conf.d/size.conf >/dev/null <<'EOF'
[Journal]
SystemMaxUse=500M
EOF
sudo systemctl restart systemd-journald
journalctl --disk-usage
ตั้งเพดาน 2: logrotate สำหรับ log ของแอป
แพ็กเกจของระบบมี config ของตัวเองใน /etc/logrotate.d/ อยู่แล้ว log ที่แอปเราเขียนเองต้องเพิ่มเอง
# /etc/logrotate.d/myapp
/srv/myapp/logs/*.log {
daily
rotate 14
maxsize 200M
compress
delaycompress
missingok
notifempty
copytruncate
}
| คำสั่ง | ความหมาย |
|---|---|
daily + rotate 14 | หมุนวันละครั้ง เก็บ 14 ชุด ที่เก่ากว่าลบทิ้ง |
maxsize 200M | ถ้าไฟล์เกิน 200 MB หมุนก่อนครบวันได้ (มีผลเมื่อ logrotate ได้รัน) |
compress + delaycompress | gzip ชุดเก่า แต่เว้นชุดล่าสุดไว้หนึ่งรอบ เผื่อแอปยังเขียนอยู่ |
missingok, notifempty | ไม่มีไฟล์ไม่ error ไฟล์ว่างไม่หมุน |
copytruncate | copy แล้ว truncate ไฟล์เดิม ใช้เมื่อแอปเปิดไฟล์ใหม่เองไม่ได้ แลกกับ log ที่อาจหายช่วงสั้น ๆ ระหว่าง copy |
ถ้าแอปรู้จัก reopen log (เช่นรับ signal) ใช้ create + postrotate แทน copytruncate ทดสอบก่อนเสมอ
sudo logrotate -d /etc/logrotate.d/myapp # dry run ไม่แก้อะไร
sudo logrotate -f /etc/logrotate.d/myapp # บังคับหมุนจริงหนึ่งครั้ง
ทางที่ง่ายกว่า: ถ้าแอปรันเป็น systemd service ให้เขียน log ออก stdout แล้วปล่อยให้ journald เก็บ จะเหลือเพดานที่ต้องดูแลตัวเดียว (วิธีเขียน service อยู่ในบทความ systemd)
ตั้งเพดาน 3: Docker log
driver json-file ที่เป็นค่าเริ่มต้น ไม่หมุน log เลย container ที่ log เยอะจะโตไม่หยุด ตั้งใน /etc/docker/daemon.json
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
sudo systemctl restart docker
ค่าใน log-opts ต้องเป็น string (ใส่ quote ให้ "3") และ ใช้กับ container ที่สร้างใหม่หลัง restart เท่านั้น container เดิมต้องสร้างใหม่ เอกสาร Docker แนะนำ driver local ด้วย เพราะหมุนเองโดย default (20 MB × 5 ไฟล์ บีบอัด ≈ 100 MB ต่อ container) และใช้รูปแบบไฟล์ที่ประหยัดกว่า (อ่านผ่าน docker logs เหมือนเดิม) ตั้งค่า Docker อื่น ๆ บนเครื่องเล็กดูในบทความ Docker hardening
ตัวอย่างคำนวณ: เหลือกี่วันก่อนเต็ม
เซิร์ฟเวอร์ดิสก์ 50 GB ใช้อยู่ 28 GB วัดอัตราโตด้วยการจด df วันละครั้งไว้หนึ่งสัปดาห์ ได้ดังนี้
| แหล่ง | โตต่อวัน |
|---|---|
| log ของแอป (ไม่มี logrotate) | 300 MB |
| Docker container log (json-file ไม่จำกัด) | 150 MB |
| journal | 50 MB |
| รวม | 500 MB/วัน |
- พื้นที่ว่าง = 50 − 28 = 22 GB
- ถึงเตือน 80% (= 40 GB) เหลือ 40 − 28 = 12 GB → 12 GB ÷ 0.5 GB/วัน = 24 วัน
- เต็ม 100% → 22 GB ÷ 0.5 GB/วัน = 44 วัน
หลังตั้งเพดานทั้งสามจุด ขนาดสูงสุดของ log กลายเป็นค่าคงที่
- journal ≤ 500 MB
- Docker 10 MB × 3 ไฟล์ = 30 MB ต่อ container สมมติ 4 container = 120 MB
- log ของแอป 14 ชุด + ไฟล์ปัจจุบัน = 15 × 300 MB = 4.5 GB กรณีแย่สุดที่ไม่นับการบีบอัด (gzip กับ log ข้อความมักเล็กลงอีกมาก)
รวมไม่เกินราว 5.1 GB แล้ว หยุดโต เส้นกราฟพื้นที่ดิสก์จะแบนแทนที่จะขึ้นทุกวัน ที่เหลือที่โตจริงคือข้อมูลของแอป ซึ่งต้องวางแผน retention แยก
log ทุกแหล่งต้องมีเพดาน ถ้ายังมีตัวที่ไม่มี ดิสก์จะเต็มสักวัน คำถามคือวันไหน
ตั้งเตือนที่ 80%
script สั้น ๆ เช็ก / ส่งเตือนเมื่อเกิน 80% รันด้วย systemd timer หรือ cron ทุก 15 นาที
#!/bin/sh
# /usr/local/bin/disk-alert.sh
LIMIT=80
USE=$(df --output=pcent / | tail -1 | tr -dc '0-9')
if [ "$USE" -ge "$LIMIT" ]; then
logger -p user.warning "disk / at ${USE}%"
# ส่งต่อไป Telegram/อีเมล ตามที่ใช้อยู่
fi
ที่ 80% ตามตัวอย่างข้างบนยังเหลือ 20 วันก่อนเต็ม พอให้แก้ในเวลางาน ไม่ใช่ตีสาม วิธีส่งเตือนเข้า Telegram อยู่ในบทความ monitor เซิร์ฟเวอร์
ข้อผิดพลาดที่เจอบ่อย
rmlog ที่ยังถูกเขียน พื้นที่ไม่คืน ใช้truncate -s 0หรือ restart process- ลบไฟล์ใน
/var/log/journalด้วยมือ ใช้journalctl --vacuum-*แทน ให้ journald จัดการเอง - ตั้ง daemon.json แล้วไม่สร้าง container ใหม่ เพดานไม่มีผลกับ container เดิม
- ลืมเช็ก inode
df -hยังว่างแต่สร้างไฟล์ไม่ได้ มักเป็นไฟล์เล็กจำนวนมาก (session, cache) ดูด้วยdf -i - ไล่ล้างโดยไม่หาสาเหตุ ล้างเสร็จแล้วไม่รู้ว่าอะไรโต จดอัตราโตต่อวันไว้ จะรู้ว่าต้องตั้งเพดานที่ไหน
แหล่งอ้างอิง
คำถามที่พบบ่อย
ลบไฟล์ log ไปแล้ว ทำไม df ยังบอกว่าเต็ม
process ที่เปิดไฟล์นั้นค้างไว้ยังถือพื้นที่อยู่ ดูด้วย sudo lsof +L1 แล้ว restart process นั้น ครั้งหน้าให้ล้างด้วย truncate -s 0 แทน rm
journalctl --vacuum-size ทำไมลดได้ไม่เท่าที่สั่ง
vacuum ลบได้เฉพาะไฟล์ journal ที่ archive แล้ว ไฟล์ที่กำลังเขียนอยู่ไม่ถูกแตะ ใส่ --rotate ไปด้วยเพื่อ archive ไฟล์ปัจจุบันก่อน
ตั้ง max-size ใน daemon.json แล้ว container เดิมยังโตไม่หยุด
ค่าใหม่ใช้กับ container ที่สร้างหลัง restart Docker เท่านั้น container เดิมต้องลบแล้วสร้างใหม่ (เช่น docker compose up -d --force-recreate)
คำสั่งและค่าตั้งในบทความทดสอบบนระบบตัวอย่าง ลองบนเครื่องทดสอบก่อนใช้กับระบบจริงเสมอ