บทความ › เซิร์ฟเวอร์และ DevOps

ดิสก์เต็มจนเซิร์ฟเวอร์ล่ม: หาตัวการ แล้วจำกัด journald, logrotate, Docker logs

เซิร์ฟเวอร์หยุดเพราะดิสก์เต็ม หาว่าอะไรกินที่ด้วย df, du, ncdu ตัวการที่เจอบ่อย วิธีตั้งเพดาน journald, logrotate, Docker log และตั้งเตือนที่ 80%

เริ่มต้น อ่าน 11 นาที 22 ตุลาคม 2569

อ่านจบแล้วจะรู้
  • ไล่จากบนลงล่าง: 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 /

ตัวการที่เจอบ่อย

ตัวการดูขนาดค่าเริ่มต้นที่ทำให้โต
journaldjournalctl --disk-usageSystemMaxUse = 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.logmax-size = -1 ไม่จำกัด
Docker image, build cachedocker system dfไม่ลบเองเลย
kernel เก่าdpkg -l 'linux-image-*' | grep ^iiค้างทุกครั้งที่อัปเดต kernel
ไฟล์ที่ลบแล้วแต่ยังถูกเปิดsudo lsof +L1process ยังถือพื้นที่

แถวสุดท้ายหลอกคนบ่อยที่สุด 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 + delaycompressgzip ชุดเก่า แต่เว้นชุดล่าสุดไว้หนึ่งรอบ เผื่อแอปยังเขียนอยู่
missingok, notifemptyไม่มีไฟล์ไม่ error ไฟล์ว่างไม่หมุน
copytruncatecopy แล้ว 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
journal50 MB
รวม500 MB/วัน
  1. พื้นที่ว่าง = 50 − 28 = 22 GB
  2. ถึงเตือน 80% (= 40 GB) เหลือ 40 − 28 = 12 GB → 12 GB ÷ 0.5 GB/วัน = 24 วัน
  3. เต็ม 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 ทุกแหล่งต้องมีเพดาน ถ้ายังมีตัวที่ไม่มี ดิสก์จะเต็มสักวัน คำถามคือวันไหน

วันนี้28 GBวันที่ 24: แตะ 80%40 GBวันที่ 44: เต็ม50 GBตั้งเพดาน log ทุกแหล่ง รวมไม่เกินราว 5.1 GB
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 เซิร์ฟเวอร์

ข้อผิดพลาดที่เจอบ่อย

  • rm log ที่ยังถูกเขียน พื้นที่ไม่คืน ใช้ 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)

คำสั่งและค่าตั้งในบทความทดสอบบนระบบตัวอย่าง ลองบนเครื่องทดสอบก่อนใช้กับระบบจริงเสมอ

อ่านต่อ

#Serverกลาง

รันฐานข้อมูล cache แอป และ HTTPS บนเครื่อง 2GB ให้รอด ตั้งเพดานทุกอย่างที่โตได้

เครื่องเล็กไม่ได้พังเพราะเล็ก แต่พังเพราะมีอะไรสักอย่างโตไม่มีเพดาน ค่าตั้ง PostgreSQL Redis log ของ Docker และ swap ที่ใช้จริงบนเครื่อง 2GB พร้อมเหตุผลของแต่ละตัว

อ่าน 8 นาที
#Serverกลาง

PostgreSQL 18: ใช้ uuidv7() แทน UUID v4 และเปิด async I/O ให้ถูก

PostgreSQL 18 มี uuidv7() ที่เรียงตามเวลา ทำให้ B-tree index เล็กและร้อนน้อยกว่า v4 และมี async I/O ผ่าน io_method เลือก worker หรือ io_uring พร้อมตัวอย่างคำนวณขนาด index

อ่าน 9 นาที
#Serverกลาง

รันแอปด้วย systemd แทน screen และ nohup: unit file, restart, timer แทน cron

เลิกเปิดแอปค้างไว้ใน screen หรือ nohup เขียน systemd unit ให้รันด้วย user แยก restart เองเมื่อพัง อ่าน env จากไฟล์ ล็อกสิทธิ์ และใช้ timer แทน cron

อ่าน 12 นาที