วิธีจัดการ Log บน VPS เครื่องเดียวให้ประหยัดทรัพยากร
เรียนรู้วิธีจัดการ Log บน VPS เครื่องเดียวอย่างมีประสิทธิภาพ ตั้งแต่การใช้ journald และ logrotate ไปจนถึงการติดตั้ง Loki หรือ OpenSearch พร้อมข้อจำกัดด้าน RAM และการตั้งค่า Retention
ต้นทุนที่แท้จริงของการจัดการ log แบบ self-hosted บน VPS หนึ่งเครื่อง
การจัดการ log แบบ self-hosted บน VPS (virtual private server) หนึ่งเครื่องสรุปได้ด้วยคำถามเดียวคือ คุณต้องการคลัสเตอร์สำหรับค้นหา หรือต้องการเพียงการหมุนเวียนไฟล์ (rotation) และการใช้ grep เท่านั้น คู่มือจากผู้ให้บริการส่วนใหญ่มักตอบคำถามนี้โดยเริ่มที่การใช้โหนด 3 โหนดและ RAM 12 GB ก่อนที่จะเริ่มรับ log แม้แต่บรรทัดเดียว ซึ่งคำตอบนี้ไม่มีประโยชน์สำหรับเซิร์ฟเวอร์เพียงเครื่องเดียว ดังนั้นการเปรียบเทียบด้านล่างนี้จึงพิจารณาจากสิ่งที่แต่ละตัวเลือกต้องการจากเซิร์ฟเวอร์ขนาดเล็กก่อนที่จะเริ่มจัดเก็บข้อมูลใดๆ
หากคุณรันเซิร์ฟเวอร์เพียงหนึ่งหรือสองเครื่องและต้องการทราบเหตุการณ์ที่เกิดขึ้นเมื่อวันอังคารที่แล้ว systemd-journald และ logrotate ก็ทำงานนั้นได้อยู่แล้ว และคุณสามารถหยุดอ่านได้หลังจากส่วนถัดไป หากคุณมีเครื่องหลายเครื่องที่ต้องส่ง log มาไว้ที่จุดเดียวและต้องการค้นหาย้อนหลังเป็นสัปดาห์ Grafana Loki จะเหมาะกับเซิร์ฟเวอร์ขนาดเล็ก เพราะใช้วิธีทำดัชนี (index) เฉพาะ label ไม่ใช่เนื้อหาในบรรทัด ส่วน Elasticsearch และ OpenSearch นั้นให้ความสามารถในการค้นหาแบบ full text อย่างแท้จริง แต่ต้องแลกมาด้วยการใช้หน่วยความจำ เนื่องจาก JVM (Java virtual machine) heap มีข้อกำหนดขั้นต่ำที่คุณไม่สามารถลดลงไปกว่านั้นได้
เริ่มต้นที่ journald เพราะคนส่วนใหญ่หยุดอยู่แค่ตรงนี้
systemd-journald ทำงานอยู่แล้วบนเซิร์ฟเวอร์ Ubuntu หรือ Debian ในปัจจุบัน มันทำหน้าที่บันทึก standard output ของทุก service unit, ข้อความจาก kernel และทุกอย่างที่ส่งไปยัง syslog คำสั่ง 4 รายการครอบคลุมเหตุการณ์ส่วนใหญ่
journalctl -u nginx.service --since "2026-08-14 09:00" --until "2026-08-14 10:00"
journalctl -p err -b
journalctl -f -u ssh.service
journalctl --disk-usageคำสั่งสุดท้ายจะแสดงบรรทัดที่คล้ายกับ Archived and active journals take up 1.1G in the file system. นั่นคือตัวเลขที่ตัดสินว่าคุณจำเป็นต้องทำอะไรเพิ่มเติมหรือไม่ หากมันแสดงผลเป็นไม่กี่ร้อยเมกะไบต์และคุณสามารถค้นหาสิ่งที่ต้องการได้ด้วย -u และ --since แสดงว่าคุณทำเสร็จแล้ว
การที่ journal จะคงอยู่หลังการรีบูตหรือไม่นั้นขึ้นอยู่กับ Storage= และการมีอยู่ของ /var/log/journal ด้วยการตั้งค่า Storage=auto ทั่วไป journald จะเขียนลงใน /var/log/journal เมื่อไดเรกทอรีนั้นมีอยู่จริง และเขียนลงใน /run/log/journal เมื่อไม่มี /run เป็นหน่วยความจำชั่วคราว ดังนั้นบนเครื่องที่ไม่มีไดเรกทอรีดังกล่าว บันทึกทั้งหมดจะถูกลบเมื่อรีบูต ซึ่งเป็นช่วงเวลาที่คุณต้องการอ่านมันมากที่สุด อิมเมจของ Ubuntu มักจะมีไดเรกทอรีนี้มาให้ แต่อิมเมจแบบ Minimal หรือแบบ Container มักจะไม่มี
ls -d /var/log/journal
sudo mkdir -p /var/log/journal
sudo systemctl restart systemd-journald
journalctl --disk-usageหลังจากรีสตาร์ท journalctl --disk-usage ควรรายงานขนาดที่ต่ำกว่า /var/log/journal แทนที่จะเป็น /run ค่าเริ่มต้นมีการจำกัดไว้แล้ว ซึ่งเป็นเหตุผลหลักที่ทำให้ journald เป็นคำตอบที่จริงจังไม่ใช่แค่ตัวเลือกสำรอง หน้า man page ของ journald.conf กำหนดให้ SystemMaxUse= เป็น 10% ของขนาดระบบไฟล์ และ SystemKeepFree= เป็น 15% โดยจำกัดค่าเริ่มต้นที่คำนวณได้ไว้ที่ 4G ส่วน SystemMaxFileSize= มีค่าเริ่มต้นเป็นหนึ่งในแปดของ SystemMaxUse= โดยจำกัดไว้ที่ 128M ดังนั้นโดยปกติคุณจะเก็บไฟล์ที่หมุนเวียน (rotated) ไว้ 7 ไฟล์ MaxRetentionSec= มีค่าเริ่มต้นเป็น 0 ซึ่งเป็นการปิดการลบตามอายุ อ่านค่าเริ่มต้นสุดท้ายนั้นอีกครั้ง: โดยค่าเริ่มต้น journal จะถูกจำกัดด้วยขนาดเท่านั้น ไม่ใช่ด้วยอายุ
[Journal]
Storage=persistent
SystemMaxUse=2G
MaxRetentionSec=30dayเขียนค่าดังกล่าวลงใน /etc/systemd/journald.conf.d/99-size.conf, รีสตาร์ท journald จากนั้นตรวจสอบว่า journalctl --disk-usage ขยับไปสู่เพดานใหม่ของคุณแล้ว เพื่อคืนพื้นที่จัดเก็บทันทีโดยไม่ต้องรอการหมุนเวียนครั้งถัดไป ให้รัน sudo journalctl --vacuum-size=500M หรือ sudo journalctl --vacuum-time=14d ทั้งสองคำสั่งจะแสดงรายการไฟล์ที่ถูกลบออกมา ดังนั้นหากรันแล้วเงียบแสดงว่าไม่มีอะไรให้ลบ
ทุกอย่างที่อยู่นอกเหนือจาก journal เช่น /var/log/nginx/access.log เป็นหน้าที่ของ logrotate ซึ่งทำงานทุกวันผ่าน systemd timer มีความล้มเหลวหนึ่งอย่างที่ควรทราบเพราะมันดูเหมือนบั๊กใน df หลังจากมีการหมุนเวียนไฟล์ ไฟล์เก่าจะหายไปจากรายการในไดเรกทอรีในขณะที่ daemon ยังคงเปิดไฟล์นั้นค้างไว้ ทำให้ df -h รายงานว่าดิสก์เต็มในขณะที่ du -sh /var/log รายงานพื้นที่ที่น้อยกว่ามาก พื้นที่จะกลับมาก็ต่อเมื่อ process เปิดไฟล์ log นั้นใหม่อีกครั้ง ซึ่งเป็นเหตุผลที่มีบรรทัด reload ใน postrotate ในไฟล์ config sudo lsof -nP +L1 จะแสดงรายการไฟล์ที่ถูกลบแต่ยังคงถูกเปิดค้างไว้ พร้อมระบุชื่อ process ที่ถือไฟล์นั้นอยู่ ทดสอบกฎโดยไม่ต้องแก้ไขอะไรด้วยการใช้ sudo logrotate -d /etc/logrotate.d/nginx
การส่ง log จากหลายเซิร์ฟเวอร์ไปยังตัวรวบรวมข้อมูลส่วนกลาง
เมื่อมีเซิร์ฟเวอร์มากกว่าหนึ่งเครื่อง การจัดการเซิร์ฟเวอร์ Linux หลายเครื่องพร้อมกัน จะทำได้ง่ายขึ้นเมื่อ log ทั้งหมดถูกส่งมารวมไว้ที่จุดเดียว rsyslog ถูกติดตั้งมาพร้อมกับ Linux ส่วนใหญ่แล้ว ดังนั้นวิธีที่ประหยัดที่สุดในการทำตัวรวบรวมข้อมูลส่วนกลางคือการใช้ไฟล์คอนฟิกบนเครื่องส่งแต่ละเครื่อง
*.* action(type="omfwd" target="logs.example.com" port="514" protocol="tcp")บันทึกไฟล์ดังกล่าวเป็น /etc/rsyslog.d/50-forward.conf ตรวจสอบความถูกต้องด้วย sudo rsyslogd -N1 ซึ่งจะตรวจสอบคอนฟิกและจบการทำงานโดยไม่เริ่มบริการใดๆ จากนั้นให้ restart rsyslog สำหรับเครื่องที่เป็นตัวรวบรวมข้อมูล ให้เปิดใช้งาน TCP input
module(load="imtcp")
input(type="imtcp" port="514")มีข้อควรระวัง 2 ประการในเชิงเทคนิค ประการแรก syslog แบบปกติไม่มีการเข้ารหัสและไม่มีการยืนยันตัวตน ดังนั้นใครก็ตามที่สามารถเข้าถึงพอร์ต 514 ได้จะสามารถส่ง log ปลอมที่ดูเหมือนมาจากระบบของคุณได้ ให้ผูกบริการนี้ไว้กับเครือข่ายส่วนตัวหรือ VPN และตั้งค่า firewall ที่พอร์ตดังกล่าว ประการที่สอง คิวการทำงานเริ่มต้นจะอยู่ในหน่วยความจำ ดังนั้นเมื่อตัวรวบรวมข้อมูลไม่สามารถติดต่อได้ คิวจะเต็มและข้อความจะถูกทิ้งโดยไม่มีการสำรองข้อมูล rsyslog มีเอกสารอธิบายการทำ disk assisted queue สำหรับกรณีนี้ไว้ใน คู่มือการส่งข้อมูลที่เชื่อถือได้
เหตุผลที่ ELK stack ไม่เหมาะกับ VPS ขนาดเล็ก
ELK ประกอบด้วย Elasticsearch สำหรับการจัดเก็บและค้นหา, Logstash สำหรับไปป์ไลน์การนำเข้าข้อมูล และ Kibana สำหรับส่วนติดต่อผู้ใช้ โดยมีพื้นฐานสำคัญคือ JVM heap ซึ่งต้องถูกกำหนดค่าไว้ก่อนที่จะมี log ใดๆ เข้ามา
เอกสารของ Elastic ระบุให้ตั้งค่า heap ไม่เกิน 50% ของหน่วยความจำทั้งหมดที่มีให้แต่ละโหนดของ Elasticsearch เนื่องจากกระบวนการนี้ยังใช้บัฟเฟอร์นอก heap (off-heap) และต้องพึ่งพา file cache ของระบบปฏิบัติการเพื่ออ่านไฟล์ดัชนีอย่างรวดเร็ว ดังนั้น heap ขนาด 2 GB จึงหมายถึงต้องใช้เครื่องที่มี RAM 4 GB เป็นอย่างน้อย โดยยังไม่รวม Kibana และแอปพลิเคชันหลักที่เซิร์ฟเวอร์นั้นถูกสร้างมาเพื่อรัน นอกจากนี้ Elastic ยังระบุว่า Elasticsearch จะปรับขนาด heap โดยอัตโนมัติตามบทบาทของโหนดและหน่วยความจำรวม ซึ่งหมายความว่าหากรันบนเครื่องขนาดเล็ก ระบบจะได้ heap ขนาดเล็กและต้องเสียเวลาไปกับการทำ garbage collection ตลอดเวลา
Logstash คือส่วนที่ทำให้งบประมาณสำหรับเซิร์ฟเวอร์ขนาดเล็กพังทลายลงทันที หน้าการตั้งค่า JVM ของ Elastic เองแนะนำให้ใช้ heap ไม่ต่ำกว่า 4 GB และไม่เกิน 8 GB สำหรับการนำเข้าข้อมูลทั่วไป ซึ่งนั่นเท่ากับ RAM ทั้งหมดของ VPS ขนาด 4 GB สำหรับกระบวนการเดียวที่อยู่ตรงกลางไปป์ไลน์
The data behind this chart
[
{
"label": "Loki plus Alloy (no JVM)",
"documented_heap_mb": 0
},
{
"label": "OpenSearch demo compose",
"documented_heap_mb": 512
},
{
"label": "OpenSearch production example",
"documented_heap_mb": 2048
},
{
"label": "Logstash recommended minimum",
"documented_heap_mb": 4096
}
]ค่าเหล่านี้คือสิ่งที่แต่ละโปรเจกต์ระบุไว้ในเอกสารของตนเอง ไม่ใช่การวัดผลจากเครื่องทดสอบ และปริมาณงานของคุณจะส่งผลต่อค่าเหล่านี้ ไฟล์ compose ตัวอย่างของ OpenSearch กำหนดค่า 512 MB ต่อโหนดสำหรับการสาธิต และ 2048 MB ในตัวอย่างสำหรับการใช้งานจริง ในขณะที่ค่าต่ำสุดที่แนะนำสำหรับ Logstash คือ 4096 MB ส่วนคอลัมน์ heap ของ Loki และ Alloy จะแสดงค่า 0 เนื่องจากเป็นโปรแกรมที่เขียนด้วยภาษา Go ซึ่งไม่มี JVM heap ให้ต้องจองไว้ล่วงหน้า นี่คือความแตกต่างทั้งหมดในตัวเลขเดียว: ส่วนประกอบที่เป็น JVM จะต้องจองหน่วยความจำไว้เสมอไม่ว่าจะมี log เข้ามาหรือไม่ก็ตาม
หากคุณยังต้องการรัน Elastic stack บนเซิร์ฟเวอร์ขนาดเล็ก ให้ตัด Logstash ออกแล้วส่งข้อมูลตรงเข้า Elasticsearch ด้วย collector ที่มีน้ำหนักเบาแทน Logstash มีไว้เพื่อแยกวิเคราะห์และแปลงข้อมูลในปริมาณมาก ซึ่งหากรันบนเครื่องเดียว คุณสามารถทำงานเหล่านั้นที่ต้นทางหรือข้ามขั้นตอนไปได้เลย
ทั้ง Elasticsearch และ OpenSearch ยังจำเป็นต้องเพิ่มค่า vm.max_map_count เป็น 262144 เนื่องจากมีการทำ memory map ไฟล์ดัชนี และค่าเริ่มต้นของ Linux นั้นต่ำเกินไปสำหรับโปรแกรมเหล่านี้ คอนเทนเนอร์ที่หยุดทำงานหลังจากเริ่มระบบได้เพียงไม่กี่วินาทีบนเครื่องที่เพิ่งติดตั้งใหม่ มักเกิดจากสาเหตุนี้เพียงอย่างเดียว
OpenSearch หรือ Elasticsearch: คุณควรเลือกติดตั้งตัวไหน?
ประวัติโดยย่อของสัญญาอนุญาต (licence) มีความสำคัญเพราะเป็นตัวกำหนดว่าคุณสามารถใช้งานซอฟต์แวร์ได้อย่างไร ในเดือนมกราคม 2021 Elastic ได้เปลี่ยนสัญญาอนุญาตของ Elasticsearch และ Kibana จาก Apache 2.0 ไปเป็นรูปแบบคู่ระหว่าง SSPL (Server Side Public License) และ Elastic License 2.0 ทางด้าน AWS ได้ทำการ fork โค้ดเวอร์ชัน Apache 2.0 ล่าสุดออกมาเป็น OpenSearch ซึ่งยังคงใช้สัญญาอนุญาต Apache 2.0 ต่อไป ในเดือนกันยายน 2024 Elastic ได้เพิ่ม AGPLv3 (GNU Affero General Public License version 3) เป็นอีกหนึ่งทางเลือกสำหรับซอฟต์แวร์โอเพนซอร์สฟรี สำหรับผู้ที่ทำ self-hosting บน VPS เพียงเครื่องเดียว สัญญาอนุญาตเหล่านี้ทั้งหมดอนุญาตให้คุณใช้งานได้ตามปกติ ข้อจำกัดของสัญญาอนุญาตจะมีผลก็ต่อเมื่อคุณนำซอฟต์แวร์ไปให้บริการแก่ผู้อื่นในรูปแบบ managed service เท่านั้น
ความแตกต่างในทางปฏิบัติบนเซิร์ฟเวอร์ขนาดเล็กนั้นมีน้อยกว่าที่ประวัติความเป็นมาบ่งบอกไว้ เนื่องจากทั้งสองตัวใช้ engine พื้นฐานเดียวกัน ชื่อเรียกฟังก์ชันอาจต่างกัน เช่น การจัดการวงจรชีวิตของ index ใน OpenSearch เรียกว่า ISM (Index State Management) ส่วนใน Elasticsearch เรียกว่า ILM (Index Lifecycle Management) ณ เดือนสิงหาคม 2026 เป็นต้นไป OpenSearch เวอร์ชัน 2.12 ขึ้นไปจะไม่ยอมเริ่มทำงานหากไม่มีการตั้งรหัสผ่านผู้ดูแลระบบในการรันครั้งแรก
sudo sysctl -w vm.max_map_count=262144
printf 'vm.max_map_count = 262144\n' | sudo tee /etc/sysctl.d/99-opensearch.conf
docker run -d -p 9200:9200 -p 9600:9600 -e "discovery.type=single-node" \
-e "OPENSEARCH_INITIAL_ADMIN_PASSWORD=<custom-admin-password>" \
opensearchproject/opensearch:latestคำสั่ง sysctl -w จะนำการตั้งค่าไปใช้ทันที และไฟล์ใน /etc/sysctl.d/ คือส่วนที่จะคงอยู่หลังการรีบูต ตรวจสอบว่าคอนเทนเนอร์ทำงานขึ้นมาแล้วด้วย curl -k -u admin:<password> https://localhost:9200 โดยบริการจะตอบกลับผ่าน https โดยใช้ใบรับรองแบบ demo ดังนั้น -k จึงใช้เพื่อข้ามการตรวจสอบความถูกต้อง และการตอบกลับที่ปกติจะเป็นบล็อก JSON ขนาดเล็กที่ระบุชื่อคลัสเตอร์และเวอร์ชัน หน้าการติดตั้งของ OpenSearch ยังระบุให้ผู้ใช้ Docker Desktop จัดสรรหน่วยความจำให้โฮสต์อย่างน้อย 4 GB ซึ่งเป็นสัญญาณที่ชัดเจนว่ากระบวนการทำงานของซอฟต์แวร์ต้องการทรัพยากรในระดับใด
วิธีที่ Loki ใช้รักษาขนาดให้เล็ก: การใช้ labels แทนการทำ full text index
Loki เก็บ index ชุดเดียวสำหรับ labels และจัดเก็บ log lines ในรูปแบบ chunks ที่ถูกบีบอัด การสืบค้นจะเลือก streams ก่อนแล้วจึงกรองข้อความในภายหลัง {unit="ssh.service"} |= "Failed password" จะเลือก stream ตาม label จากนั้นจึงสแกน chunks เหล่านั้นเพื่อหาข้อความที่ต้องการ เนื่องจากไม่มีการทำ index เนื้อหาภายในบรรทัด การนำเข้าข้อมูล (ingestion) จึงมีต้นทุนต่ำและไม่ต้องใช้ inverted index ในหน่วยความจำ ต้นทุนจะไปอยู่ที่ช่วงเวลาการสืบค้น (query time) ซึ่งเป็นการแลกเปลี่ยนที่คุ้มค่าเมื่อคุณทราบอยู่แล้วว่ากำลังตรวจสอบบริการใดอยู่
เอกสารของ Grafana ระบุว่าโหมด monolithic ซึ่งหมายถึงการรัน Loki ทั้งหมดในกระบวนการเดียวด้วย -target=all นั้น เหมาะสำหรับปริมาณการอ่านและเขียนข้อมูลขนาดเล็กที่ระดับไม่เกินประมาณ 20GB ต่อวัน ซึ่ง VPS หนึ่งเครื่องสามารถรองรับปริมาณงานนี้ได้อย่างสบาย
กับดักที่สำคัญคือเรื่อง label cardinality การรวมกันของค่า label ที่แตกต่างกันแต่ละชุดจะถือเป็นหนึ่ง stream และจำนวน stream จะเป็นตัวกำหนดขนาดหน่วยความจำและ index ของ Loki การใช้ label ที่เก็บค่า IP address ของไคลเอนต์หรือตัวระบุคำขอ (request identifier) จะสร้าง stream ขึ้นมาหนึ่งรายการต่อหนึ่งค่า ดังนั้นเว็บเซิร์ฟเวอร์ที่มีงานชุกอาจสร้าง stream ได้หลายหมื่นรายการต่อวัน และกระบวนการจะขยายตัวจนกระทั่ง kernel สั่งยุติการทำงาน ควรจำกัด label ไว้เฉพาะค่าที่คุณสามารถนับได้ด้วยมือ เช่น unit, host, job, level ส่วนรายละเอียดที่เป็นตัวแปรให้ใส่ไว้ในบรรทัด log โดยตรง ซึ่ง filter expression จะสามารถค้นหาข้อมูลนั้นได้ในระหว่างการสืบค้น
การติดตั้ง Loki และ Alloy บน VPS เดียวกัน
กระบวนการทำงานแบ่งเป็นสองส่วน Loki ทำหน้าที่จัดเก็บและตอบสนองต่อการสืบค้นข้อมูล ส่วน Grafana Alloy ทำหน้าที่อ่าน log และส่งข้อมูลไปยัง Loki ในอดีตเราใช้ Promtail เป็นตัวส่งข้อมูล แต่ได้สิ้นสุดอายุการใช้งาน (End of Life) ไปเมื่อวันที่ 2 มีนาคม 2026 ดังนั้นการติดตั้งใหม่ทั้งหมดจึงต้องใช้ Alloy และตัวอย่าง Docker ของ Loki ในปัจจุบันก็ได้รวมการตั้งค่าสำหรับ Alloy ไว้แล้ว
wget https://raw.githubusercontent.com/grafana/loki/v3.7.0/cmd/loki/loki-local-config.yaml -O loki-config.yamlโปรดอ่านไฟล์ดังกล่าวก่อนใช้งาน โดยค่าเริ่มต้นจะตั้งค่า path_prefix: /tmp/loki ให้เก็บข้อมูล chunks ไว้ภายใต้ /tmp/loki/chunks ซึ่งเหมาะสมสำหรับการทดสอบ แต่ไม่ถูกต้องสำหรับการใช้งานจริงบนเซิร์ฟเวอร์ เนื่องจากข้อมูลภายใต้ /tmp ของคอนเทนเนอร์จะสูญหายเมื่อมีการสร้างคอนเทนเนอร์ใหม่ ทำให้ประวัติการใช้งานของคุณหายไปทุกครั้งที่มีการอัปเดตอิมเมจ คุณควรเปลี่ยนไปชี้ยัง path ที่คุณ mount ไว้แทน
common:
instance_addr: 127.0.0.1
path_prefix: /loki
storage:
filesystem:
chunks_directory: /loki/chunks
rules_directory: /loki/rules
replication_factor: 1
ring:
kvstore:
store: inmemorydocker volume create loki-data
docker run --name loki -d \
-v $(pwd):/mnt/config -v loki-data:/loki \
-p 127.0.0.1:3100:3100 \
grafana/loki:3.7.0 -config.file=/mnt/config/loki-config.yaml
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:3100/readyคำสั่งสุดท้ายควรแสดงผลลัพธ์เป็น 200 เนื่องจาก /ready จะตอบกลับด้วย HTTP 200 เมื่อ Loki พร้อมรับข้อมูล หากแสดงผลเป็นอย่างอื่น หมายความว่ากระบวนการกำลังเริ่มทำงานหรือการตั้งค่าถูกปฏิเสธ ซึ่ง docker logs loki จะระบุสาเหตุให้ทราบ รายละเอียดสองประการในคำสั่ง run ถูกกำหนดไว้อย่างตั้งใจ ประการแรก พอร์ตถูกเปิดไว้ที่ 127.0.0.1 เท่านั้น เนื่องจากตัวอย่างการตั้งค่ามี auth_enabled: false และ Loki ไม่มีระบบยืนยันตัวตนในตัว ดังนั้นใครก็ตามที่เข้าถึงพอร์ต 3100 ได้จะสามารถอ่าน log ทั้งหมดและเขียนข้อมูลปลอมเข้าไปได้ คุณควรจำกัดการเข้าถึงไว้ที่ loopback หรือใช้งานผ่าน VPN หรือ reverse proxy ที่มีการยืนยันตัวตน ประการที่สอง การใช้ named volume มีความสำคัญเนื่องจากอิมเมจทำงานด้วยผู้ใช้ loki ที่มี UID 10001 ดังนั้นไดเรกทอรีบนโฮสต์ที่ใช้ bind mount และเป็นเจ้าของโดย root จะไม่สามารถเขียนข้อมูลได้โดยคอนเทนเนอร์
sudo apt-get update && sudo apt-get install -y gpg wget
sudo mkdir -p /etc/apt/keyrings/
sudo wget -O /etc/apt/keyrings/grafana.asc https://apt.grafana.com/gpg-full.key
echo "deb [signed-by=/etc/apt/keyrings/grafana.asc] https://apt.grafana.com stable main" \
| sudo tee /etc/apt/sources.list.d/grafana.list
sudo apt-get update
sudo apt-get install -y alloyAlloy จะอ่านค่าจาก /etc/alloy/config.alloy ซึ่งจะดึงข้อมูลจาก system journal และชุดไฟล์ที่กำหนด แล้วส่งข้อมูลทั้งสองส่วนไปยัง Loki ในเครื่อง
loki.write "local" {
endpoint {
url = "http://127.0.0.1:3100/loki/api/v1/push"
}
}
loki.relabel "journal" {
forward_to = []
rule {
source_labels = ["__journal__systemd_unit"]
target_label = "unit"
}
}
loki.source.journal "read" {
forward_to = [loki.write.local.receiver]
relabel_rules = loki.relabel.journal.rules
labels = {job = "systemd-journal", host = "app-01"}
}
local.file_match "nginx" {
path_targets = [{"__path__" = "/var/log/nginx/*.log", "job" = "nginx", "host" = "app-01"}]
}
loki.source.file "nginx" {
targets = local.file_match.nginx.targets
forward_to = [loki.write.local.receiver]
}กฎ relabel จะคัดลอกฟิลด์ __journal__systemd_unit จาก journal ไปยัง label ที่ชื่อ unit ซึ่งเป็นส่วนที่ทำให้ {unit="ssh.service"} ทำงานได้ในภายหลัง หากไม่มีกฎนี้ ชื่อ unit จะอยู่ในเนื้อหาของ log แทนที่จะเป็น label ทำให้คุณไม่สามารถเลือกกรองข้อมูลได้ และการสืบค้นทุกครั้งจะต้องไล่ค้นหาข้อมูลทั้งหมด
sudo systemctl reload alloy
systemctl show -p User alloy
sudo journalctl -n 5
sudo -u alloy journalctl -n 5นี่คือจุดที่การตั้งค่าส่วนใหญ่มักติดขัด Alloy ทำงานด้วย service account ของตัวเองไม่ใช่ root และการอ่าน system journal จำเป็นต้องเป็นสมาชิกของกลุ่ม systemd-journal ในขณะที่ไฟล์ภายใต้ /var/log/nginx จะเป็นของกลุ่ม adm บน Debian และ Ubuntu ให้แทนที่บัญชีผู้ใช้ที่ systemctl show แสดงผลในคำสั่งสุดท้าย หากผลลัพธ์ที่ได้มีจำนวนรายการน้อยกว่าการรันด้วย root อย่างมาก แสดงว่าบัญชีนั้นไม่สามารถอ่าน system journal ได้ และ Loki จะว่างเปล่าไม่ว่าการตั้งค่าของคุณจะถูกต้องเพียงใด ให้เพิ่มกลุ่มผู้ใช้และเริ่มการทำงานใหม่ด้วย sudo usermod -aG systemd-journal,adm alloy ตามด้วย sudo systemctl restart alloy
curl -G -s "http://127.0.0.1:3100/loki/api/v1/query_range" \
--data-urlencode 'query={job="systemd-journal"}' \
--data-urlencode 'limit=5' | jq '.data.result | length'ตัวเลขที่มากกว่า 0 หมายความว่ามี stream ที่มี label นั้นและมีข้อมูลอยู่ ส่วนเลข 0 หมายความว่ายังไม่มีข้อมูลส่งเข้ามาภายใต้ label นั้น ค่าเริ่มต้นหนึ่งอย่างที่อธิบายเหตุการณ์แจ้งเตือนผิดพลาดที่พบบ่อยคือ loki.source.journal ซึ่งตั้งค่า max_age ไว้ที่ 7h ดังนั้นการเริ่มทำงานใหม่จะอ่านข้อมูลย้อนหลังเพียง 7 ชั่วโมงจาก journal และจะไม่ดึงข้อมูลที่เก่ากว่านั้น สำหรับการใช้งานผ่านหน้าจอผู้ใช้ ให้รัน Grafana บนเครื่องเดียวกันและชี้ Loki data source ไปที่ http://127.0.0.1:3100. สำหรับ log ของคอนเทนเนอร์จำเป็นต้องใช้ source อีกแบบหนึ่ง โดย Alloy จะค้นหาคอนเทนเนอร์ Docker ที่กำลังทำงานอยู่และติดตาม log ซึ่งเป็นสิ่งที่ตัวอย่างการเริ่มต้นใช้งานของ Loki ทำ และบน คลัสเตอร์ k3s โหนดเดียวบน VPS งานดังกล่าวจะเปลี่ยนไปอ่านที่ไดเรกทอรี log ของ pod ที่ kubelet เขียนไว้แทน
การเก็บรักษาข้อมูล: กำหนดวันหมดอายุของ log
แทบไม่มีใครกำหนดระยะเวลาการเก็บรักษาข้อมูลจนกว่าดิสก์จะเต็ม และมักจะมาทำตอนตี 3 ในขณะที่บริการหยุดทำงาน ให้ตัดสินใจเรื่องนี้ตั้งแต่วันแรกโดยพิจารณาจากสองคำถาม: คุณย้อนกลับไปดูข้อมูลจริงไกลแค่ไหน และคุณยังต้องใช้ข้อมูลอะไรบ้างระหว่างการตรวจสอบเหตุการณ์ในเดือนหน้า สำหรับเซิร์ฟเวอร์เดี่ยว ระยะเวลา 14 ถึง 30 วันเพียงพอสำหรับทั้งสองกรณี
Loki จะไม่ลบข้อมูลใดๆ จนกว่าคุณจะเปิดใช้งาน compactor โดยค่าเริ่มต้นการเก็บรักษาข้อมูลจะถูกปิดไว้ ซึ่งมักสร้างความประหลาดใจให้กับผู้ที่พบว่าพื้นที่จัดเก็บข้อมูลเต็มในขณะที่ retention_period ยังคงอยู่ในไฟล์ config โดยไม่ได้ทำงานใดๆ
limits_config:
retention_period: 744h
compactor:
working_directory: /loki/retention
compaction_interval: 10m
retention_enabled: true
retention_delete_delay: 2h
retention_delete_worker_count: 150
delete_request_store: filesystem744h เท่ากับ 31 วัน มีกฎที่บันทึกไว้ 4 ข้อที่ควบคุมบล็อกดังกล่าว:
- การเก็บรักษาข้อมูลจะถูกดำเนินการโดย compactor และเอกสารของ Grafana ระบุให้รัน compactor เป็นอินสแตนซ์เดียว สำหรับ VPS หนึ่งเครื่อง สิ่งนี้จะเกิดขึ้นโดยอัตโนมัติ
- ระยะเวลาการเก็บรักษาขั้นต่ำคือ 24h และการเก็บรักษาจะทำงานได้ก็ต่อเมื่อระยะเวลาของ index คือ 24h ตัวอย่าง
schema_configใช้period: 24hอยู่แล้ว ดังนั้นไม่ต้องแก้ไข delete_request_storeจำเป็นต้องมีเมื่อretention_enabledเป็น true โดยระบุที่จัดเก็บคำขอลบข้อมูล ดังนั้นบนโหนดเดี่ยวที่ใช้ระบบไฟล์เป็นฐาน ค่านี้จะตรงกับobject_store: filesystemที่อยู่ใน schema อยู่แล้ว- Chunks จะถูกทำเครื่องหมายก่อนและถูกลบหลังจากผ่านไป
retention_delete_delayซึ่งในที่นี้คือ 2h ดังนั้นพื้นที่ว่างจะกลับคืนมาภายหลังนโยบายที่กำหนดไว้ อย่าประเมินการตั้งค่าด้วยdfทันทีหลังจากโหลดใหม่เพียงห้านาที
OpenSearch จะลบทั้ง index แทนที่จะลบทีละบรรทัด ซึ่งเป็นเหตุผลว่าทำไม log index จึงถูกสร้างขึ้นเป็นรายวัน นโยบาย ISM จะนำ index ผ่านสถานะต่างๆ และลบออกเมื่อเก่าพอ และ ism_template จะแนบนโยบายเข้ากับ index ใหม่โดยอัตโนมัติเพื่อให้คุณไม่ต้องคอยจัดการเอง
นโยบาย ISM ที่ลบ log index หลังจาก 14 วัน
{
"policy": {
"description": "delete log indexes after 14 days",
"default_state": "hot",
"states": [
{
"name": "hot",
"actions": [],
"transitions": [
{ "state_name": "delete", "conditions": { "min_index_age": "14d" } }
]
},
{
"name": "delete",
"actions": [ { "delete": {} } ],
"transitions": []
}
],
"ism_template": { "index_patterns": ["logs-*"], "priority": 100 }
}
}สร้างนโยบายด้วยการส่ง PUT ไปยัง _plugins/_ism/policies/logs-retention เทมเพลตนี้จะมีผลกับ index ที่สร้างขึ้นหลังจากมีนโยบายอยู่แล้ว ดังนั้น index ใดๆ ที่อยู่บนดิสก์อยู่ก่อนแล้วจะต้องแนบนโยบายด้วยตนเอง
ไม่ว่าคุณจะใช้ระบบใด ตัวเลขการเก็บรักษาข้อมูลจะมีประสิทธิภาพก็ต่อเมื่อมีการตรวจสอบพื้นที่ว่างรองรับอยู่เบื้องหลัง การตั้งค่าลบข้อมูลที่ 14 วันจะไม่ช่วยคุณหาก log เพียง 10 วันก็เต็มพื้นที่จัดเก็บแล้ว ดังนั้นควรจับคู่นโยบายนี้เข้ากับ การตรวจสอบสุขภาพของดิสก์บน VPS และตั้งค่าการแจ้งเตือนเมื่อมีการใช้งานถึง 80%
พื้นที่ดิสก์ที่ต้องใช้ต่อ GB ของ log
คำตอบที่ตรงไปตรงมาขึ้นอยู่กับจำนวนบรรทัดและฟิลด์ข้อมูลของคุณ ดังนั้นควรวัดค่าจากข้อมูลจริงของคุณเองแทนที่จะเชื่ออัตราส่วนที่เผยแพร่ทั่วไป กลไกการทำงานของแต่ละระบบมีความแตกต่างกันมากพอที่จะคาดการณ์แนวโน้มได้ OpenSearch และ Elasticsearch จะเขียน inverted index ลงในทุกฟิลด์ที่ทำดัชนีควบคู่ไปกับเอกสารที่จัดเก็บ ทำให้ขนาดข้อมูลบนดิสก์ใหญ่กว่าข้อความดิบ และทุก replica จะเพิ่มขนาดขึ้นเป็นทวีคูณ ในกรณีที่เป็นโหนดเดี่ยว ให้ตั้งค่า replica count เป็น 0 เพราะ shard สำรองที่อยู่บนโหนดเดียวกันไม่สามารถช่วยกู้คืนข้อมูลได้หากโหนดนั้นล้มเหลว การปล่อยไว้ที่ 1 จะทำให้เปลืองพื้นที่ดิสก์เป็นสองเท่าและทำให้สถานะสุขภาพของคลัสเตอร์ติดอยู่ที่ yellow ตลอดไป ส่วน Loki จะเขียนข้อมูลเป็น compressed chunks ร่วมกับดัชนี label ขนาดเล็ก ทำให้ขนาดที่ใช้จริงแปรผันตามขนาดของบรรทัดที่ถูกบีบอัดแล้ว
sudo du -sh /var/lib/docker/volumes/loki-data/_data
curl -k -u admin:<password> "https://localhost:9200/_cat/indices?v&h=index,docs.count,store.size"ให้รันคำสั่งที่เกี่ยวข้องกับระบบของคุณเป็นเวลา 2 วันติดต่อกัน ผลต่างที่ได้คืออัตราการเติบโตต่อวัน ให้นำไปคูณกับจำนวนวันที่ต้องการเก็บข้อมูล (retention days) แล้วบวกพื้นที่สำรองเพิ่มอีกประมาณ 30% สำหรับการทำ compaction และ merges จากนั้นจึงนำไปเปรียบเทียบกับขนาดของ volume หากพื้นที่ไม่เพียงพอ ให้ลดระยะเวลาการเก็บข้อมูลลงก่อนที่จะตัดสินใจซื้อดิสก์เพิ่ม เพราะการขยาย volume เพียงอย่างเดียวจะช่วยแก้ปัญหาได้เพียงไม่กี่สัปดาห์เท่านั้น
สิ่งที่มักจะล้มเหลวก่อนบนเซิร์ฟเวอร์ขนาดเล็ก
หน่วยความจำมักจะหมดก่อนเป็นอันดับแรก กลไก OOM (out of memory) killer ของ kernel จะเลือกยุติกระบวนการที่ใช้หน่วยความจำสูง และกระบวนการที่ใหญ่ที่สุดบนเซิร์ฟเวอร์ที่ทำหน้าที่เก็บ log มักจะเป็น JVM โดย journalctl -k | grep -i "killed process" จะแสดงการยุติกระบวนการพร้อมชื่อกระบวนการในวงเล็บ เหยื่อที่ถูกเลือกอาจไม่ใช่ stack ของระบบ log เสมอไป แต่อาจเป็น sshd หรือฐานข้อมูลของคุณ ซึ่งเป็นสาเหตุที่ทำให้การทดลองเก็บ log ส่งผลให้แอปพลิเคชันที่คุณต้องการตรวจสอบ log ล่มลงไปด้วย คุณควรจำกัดเพดานการใช้ทรัพยากรของคอนเทนเนอร์ให้ชัดเจนเพื่อให้ความล้มเหลวเกิดขึ้นในจุดที่คุณกำหนดไว้ ซึ่งเป็นหน้าที่ของ memory limits in Docker Compose
ดิสก์มักจะเต็มเป็นอันดับที่สอง และ search engine จะล้มเหลวในรูปแบบที่เฉพาะเจาะจงและสังเกตได้ง่าย Elasticsearch และ OpenSearch จะตรวจสอบการใช้งานดิสก์ในหลายระดับ โดย low watermark จะอยู่ที่ 85% และ high watermark อยู่ที่ 90% เมื่อถึงระดับ flood stage ที่ 95% ดัชนีทุกตัวที่มี shard อยู่บนโหนดนั้นจะได้รับสถานะ block index.blocks.read_only_allow_delete และการเขียนข้อมูลจะล้มเหลวด้วยข้อผิดพลาด blocked by: [FORBIDDEN/12/index read-only / allow delete (api)] สถานะ block จะถูกปลดออกเมื่อการใช้งานลดลงต่ำกว่าระดับ high watermark ให้เพิ่มพื้นที่ว่างก่อน แล้วจึงค่อยปลด block ด้วยตนเองหากสถานะยังคงค้างอยู่
curl -k -u admin:<password> -X PUT "https://localhost:9200/_all/_settings" \
-H 'Content-Type: application/json' \
-d '{"index.blocks.read_only_allow_delete": null}'Loki จะล้มเหลวแบบเงียบกว่า เนื่องจากไม่มีโหมด read-only ให้เปลี่ยนไปใช้ ดังนั้นเมื่อ volume เต็ม จะปรากฏเป็นความล้มเหลวในการ push ข้อมูลที่ฝั่งผู้ส่งและเกิดช่องว่างในผลลัพธ์การสืบค้น ส่วนปัญหา cardinality จะแสดงออกมาเป็นการค่อยๆ เพิ่มขึ้นของหน่วยความจำแทนที่จะเป็นข้อผิดพลาด คุณควรตรวจสอบขนาดของไดเรกทอรี chunks ตามกำหนดเวลา ไม่ใช่รอให้เกิดเหตุการณ์ก่อน
ความล้มเหลวสุดท้ายคือการนำข้อมูลที่ไม่เหมาะสมเข้ามาเก็บ ระบบ log ไม่ใช่ระบบ metrics การเก็บค่า CPU load ที่สุ่มตัวอย่างทุก 10 วินาทีในรูปแบบข้อความนั้นสิ้นเปลืองทรัพยากรและนำไปสร้างกราฟได้ยาก งานดังกล่าวควรเป็นหน้าที่ของเครื่องมืออย่าง a Zabbix monitoring server on Ubuntu 24.04 สำหรับ application exceptions นั้นต้องการการจัดกลุ่ม การลบข้อมูลซ้ำ และการแสดงผล stack trace ซึ่งเป็นหน้าที่ของ a self-hosted error tracker ส่วนการทราบว่าเว็บไซต์ล่มหรือไม่เป็นงานแยกต่างหาก ซึ่งจัดการได้ด้วย an uptime and status page such as Uptime Kuma ควรใช้ระบบ log สำหรับบรรทัดข้อความที่มนุษย์จะอ่านเท่านั้น
FAQ
ฉันจำเป็นต้องใช้ Elasticsearch เพื่อค้นหา log ของเซิร์ฟเวอร์หรือไม่?
ไม่จำเป็นสำหรับเซิร์ฟเวอร์เพียงหนึ่งหรือสองเครื่อง journalctl สามารถกรองข้อมูลตาม unit, priority, boot และช่วงเวลาได้อยู่แล้ว และไฟล์ที่ถูกหมุนเวียน (rotated files) ก็สามารถตรวจสอบได้ด้วย grep และ zgrep การใช้คลัสเตอร์สำหรับค้นหาจะคุ้มค่ากับหน่วยความจำที่เสียไปก็ต่อเมื่อคุณมีเครื่องจำนวนมาก เมื่อคุณต้องการค้นหาข้อความแบบอิสระ (free text search) จากทุกเครื่องพร้อมกัน หรือเมื่อมีผู้ใช้งานหลายคนต้องการอินเทอร์เฟซร่วมกัน หากต่ำกว่าเกณฑ์ดังกล่าว การใช้ journald โดยกำหนดขนาดจำกัดและระยะเวลาการเก็บรักษาจะให้ผลลัพธ์เดียวกันโดยไม่กิน RAM เพิ่มเติม
ฉันต้องใช้ RAM เท่าไรสำหรับการจัดการ log แบบ self-hosted?
ให้ใช้ตัวเลขที่แต่ละโปรเจกต์ระบุไว้แทนการกะเกณฑ์ด้วยความรู้สึก Loki และ Alloy เป็นโปรแกรมที่เขียนด้วย Go ซึ่งไม่มีการจอง heap ไว้ล่วงหน้า และ Grafana ได้ระบุว่า Loki แบบ monolithic สามารถรองรับข้อมูลได้สูงสุดประมาณ 20GB ต่อวัน ตัวอย่างไฟล์ compose ของ OpenSearch กำหนด heap ไว้ที่ 512 MB สำหรับการทดสอบ และ 2 GB สำหรับตัวอย่างการใช้งานจริง ส่วน Elastic ระบุว่า heap ต้องไม่เกิน 50% ของหน่วยความจำทั้งหมด ดังนั้น heap ขนาด 2 GB จึงหมายถึงต้องใช้เครื่องที่มี RAM อย่างน้อย 4 GB ก่อนจะนับรวม Kibana ส่วนเอกสารของ Logstash แนะนำให้ใช้ heap ไม่ต่ำกว่า 4GB ด้วยตัวเอง ค่าเหล่านี้เป็นค่าที่ระบุไว้ในเอกสาร ไม่ใช่ผลการทดสอบประสิทธิภาพ ดังนั้นควรวัดปริมาณโหลดของคุณเองก่อนวางแผนขนาดของระบบ
ความแตกต่างที่แท้จริงระหว่าง Loki และ OpenSearch สำหรับ log คืออะไร?
คือรูปแบบการทำดัชนี (index model) Loki จะทำดัชนีเฉพาะ label และเก็บเนื้อหาของ log ไว้เป็นก้อนข้อมูลที่ถูกบีบอัดซึ่งจะถูกสแกนในขณะที่ทำการ query ดังนั้นการเขียนข้อมูลจึงใช้ทรัพยากรน้อย แต่การ query ในวงกว้างจะมีต้นทุนสูงกว่า ส่วน OpenSearch จะทำดัชนีเนื้อหาภายในฟิลด์ต่างๆ ทำให้การค้นหาข้อความแบบเต็ม (full text search) ทำได้อย่างรวดเร็ว แต่ทั้งหน่วยความจำและดิสก์จะต้องถูกใช้เพื่อเก็บดัชนีดังกล่าว เลือกใช้ Loki เมื่อคุณทราบแน่ชัดว่าต้องการดูบริการใดและในช่วงเวลาใด เลือกใช้ OpenSearch เมื่อคุณต้องการค้นหาข้อความที่คุณไม่สามารถคาดเดาได้ล่วงหน้า
ฉันควรเก็บ log ไว้บน VPS นานเท่าไร?
ให้กำหนดจำนวนวันที่ต้องการก่อนที่ดิสก์จะเต็มจนบังคับให้คุณต้องลบเอง โดยกำหนดไว้ที่จุดเดียวต่อระบบ: MaxRetentionSec= และ SystemMaxUse= สำหรับ journald, retention_period โดยเปิดใช้งาน compactor สำหรับ Loki และนโยบาย ISM ด้วย min_index_age สำหรับ OpenSearch สำหรับการตั้งค่าเซิร์ฟเวอร์เดี่ยวส่วนใหญ่ ระยะเวลา 14 ถึง 30 วันเพียงพอสำหรับการแก้ไขปัญหาและการตรวจสอบเหตุการณ์ย้อนหลัง สิ่งใดก็ตามที่คุณจำเป็นต้องเก็บไว้นานกว่านั้นควรถูกสำรองข้อมูลไว้นอกเซิร์ฟเวอร์ เพราะ log ที่เก็บไว้เฉพาะบนเซิร์ฟเวอร์ที่เกิดความเสียหายจะไม่ถือเป็นบันทึกที่ปลอดภัย
Promtail ยังคงเป็นวิธีที่ใช้ส่ง log ไปยัง Loki อยู่หรือไม่?
ไม่แล้ว Promtail สิ้นสุดอายุการใช้งาน (end of life) เมื่อวันที่ 2 มีนาคม 2026 และ Grafana Alloy ได้เข้ามาแทนที่ ตัวอย่างการติดตั้ง Loki ผ่าน Docker ในปัจจุบันได้เปลี่ยนมาใช้การตั้งค่าของ Alloy แล้ว และ Grafana ได้จัดเตรียมเครื่องมือแปลงที่สามารถเปลี่ยนการตั้งค่า Promtail เดิมให้เป็นไวยากรณ์ของ Alloy ได้ การติดตั้ง Promtail เดิมจะยังคงทำงานต่อไปได้ แต่จะไม่ได้รับการแก้ไขข้อผิดพลาดใดๆ อีก ดังนั้นให้ถือว่าการย้ายระบบเป็นงานบำรุงรักษาที่จำเป็น ไม่ใช่การอัปเกรดที่คุณจะเลื่อนออกไปเรื่อยๆ ได้อย่างไม่มีกำหนด