Logging และ Metrics
ไอเดียในหนึ่งประโยค
หัวข้อที่มีชื่อว่า “ไอเดียในหนึ่งประโยค”Kubernetes ให้ logging, live debugging, และ metrics มาพอสำหรับสืบหาปัญหาของ Pod เดียวตอนนี้เลย แต่ไม่มีอะไร aggregate หรือเก็บข้อมูลไว้ทั่ว cluster — นั่นเป็น stack แยกที่คุณต้องเพิ่มเอง
kubectl logs: หน้าต่างสด ไม่ใช่หนังสือประวัติศาสตร์
หัวข้อที่มีชื่อว่า “kubectl logs: หน้าต่างสด ไม่ใช่หนังสือประวัติศาสตร์”kubectl logs อ่าน stdout/stderr ของ container ตรงจาก node ที่มันรันอยู่ เป็นวิธีเร็วที่สุดในการดูว่า Pod กำลังทำอะไรอยู่ตอนนี้เลย
# Stream logs live as they are writtenkubectl logs -f deploy/checkout
# A Pod with more than one container: pick which onekubectl logs my-pod -c sidecar
# The container just crashed and restarted: see its last words before it diedkubectl logs my-pod --previous--previous เป็นตัวที่คนลืมบ่อยที่สุดแล้วก็ต้องใช้บ่อยที่สุด: ตอน container crash-loop log ของ instance ปัจจุบัน อาจผ่านช่วงที่น่าสนใจไปแล้ว แต่ --previous โชว์ log ของ instance ที่เพิ่ง crash พิสูจน์จริง container ที่พิมพ์บรรทัดเดียวแล้ว exit ด้วย code ไม่ใช่ศูนย์
apiVersion: v1kind: Podmetadata: name: crasher namespace: log-demospec: restartPolicy: Always containers: - name: c image: busybox command: ["sh", "-c", "echo 'boot message, crashing now'; sleep 2; exit 1"]$ kubectl get pod crasherNAME READY STATUS RESTARTS AGEcrasher 0/1 Error 1 (2s ago) 12s
$ kubectl logs crasher --previousboot message, crashing nowkubelet เก็บ container ที่ terminate ไว้ให้แค่จำนวนจำกัดก่อนจะ garbage-collect log ของมันทิ้ง ดังนั้น --previous ต้องรันภายในเวลาไม่นานหลัง crash — รอนานเกินไปผ่านหลาย restart cycle แม้แต่ --previous ก็จะรายงาน unable to retrieve container logs เพราะ container instance ที่ terminate ตัวนั้นหายไปแล้วจริง ๆ
เลือก log จากหลาย Pod พร้อมกัน และตัดให้เหลือแค่ที่ต้องการ
$ kubectl logs -l app=web -n log-demo --tail=2 --prefix[pod/web-57787d5f95-dwbnf/c] tick 2 from web-57787d5f95-dwbnf[pod/web-57787d5f95-dwbnf/c] tick 3 from web-57787d5f95-dwbnf[pod/web-57787d5f95-g6qrh/c] tick 2 from web-57787d5f95-g6qrh[pod/web-57787d5f95-g6qrh/c] tick 3 from web-57787d5f95-g6qrh[pod/web-57787d5f95-q9hkw/c] tick 1 from web-57787d5f95-q9hkw[pod/web-57787d5f95-q9hkw/c] tick 2 from web-57787d5f95-q9hkw
$ kubectl logs -l app=web -n log-demo --since=5s --prefix[pod/web-57787d5f95-g6qrh/c] tick 2 from web-57787d5f95-g6qrh[pod/web-57787d5f95-g6qrh/c] tick 3 from web-57787d5f95-g6qrh[pod/web-57787d5f95-q9hkw/c] tick 2 from web-57787d5f95-q9hkw[pod/web-57787d5f95-dwbnf/c] tick 2 from web-57787d5f95-dwbnf[pod/web-57787d5f95-dwbnf/c] tick 3 from web-57787d5f95-dwbnf-l (label selector) บวก -f จะ stream ทุก Pod ที่ match พร้อมกัน แต่ละบรรทัดติด tag ด้วย --prefix ให้รู้ว่ามาจาก Pod ไหน — เป็น built-in เทียบเท่ากับที่ tool ของ third-party อย่าง stern ให้ พร้อมสีสวยกว่าและ selector ข้าม namespace ได้ สำหรับ case ทั่วไปคือ “tail ทุกอย่างที่ match label นี้ตอนนี้เลย”
ข้อจำกัดที่สำคัญคือ Kubernetes เองไม่ aggregate หรือเก็บ log พวกนี้ ถ้า Pod ถูกลบ log ของมันก็มักจะหายไปด้วย ไม่มี “search log ทั่ว cluster” built-in นั่นเป็นหน้าที่ของ logging stack แยก — ปกติเป็น node-level agent อย่าง Fluent Bit (หรือ Fluentd/Vector) ที่ tail ไฟล์ log ของทุก container แล้วส่งไปที่ backend อย่าง Loki หรือ Elasticsearch ที่ search และเก็บระยะยาวได้
# A minimal illustration: Fluent Bit runs as a DaemonSet so one agent per node# tails every container's logs and forwards them to a backendapiVersion: apps/v1kind: DaemonSetmetadata: name: fluent-bitspec: selector: matchLabels: app: fluent-bit template: metadata: labels: app: fluent-bit spec: containers: - name: fluent-bit image: fluent/fluent-bit:5.1 volumeMounts: - name: varlog mountPath: /var/log volumes: - name: varlog hostPath: path: /var/logkubectl exec ไม่ได้ทำงานเสมอไป — ephemeral container กับ kubectl debug
หัวข้อที่มีชื่อว่า “kubectl exec ไม่ได้ทำงานเสมอไป — ephemeral container กับ kubectl debug”image แบบ minimal หรือ distroless (build จาก scratch หรือ gcr.io/distroless/static หรือ utility image อย่าง registry.k8s.io/pause) ไม่มี shell หรือ coreutils มาให้เลย — มีแค่ static binary ตัวเดียวที่ container รัน kubectl exec ต้องการ อะไรสักอย่าง ให้รันข้างใน container ดังนั้นมันจะพังด้วย error ที่เจาะจงจริง ไม่ใช่ปัญหา permission ระดับ Kubernetes
$ kubectl exec distroless-app -n log-demo -- sherror: Internal error occurred: Internal error occurred: error executing command in container: failed to exec in container: failed to start exec "28673a297...": OCI runtime exec failed: exec failed: unable to start container process: exec: "sh": executable file not found in $PATHEphemeral container (GA/stable ตั้งแต่ Kubernetes 1.25) แก้ปัญหานี้ได้: kubectl debug แนบ container ใหม่ที่ใช้แล้วทิ้งเข้ากับ Pod ที่กำลังรันอยู่แล้ว แชร์ process namespace กับ target โดยไม่ต้อง restart หรือแทนที่อะไรใน Pod spec เดิมเลย
$ kubectl debug -it distroless-app -n log-demo --image=busybox --target=app -- ps auxTargeting container "app". If you don't see processes from this container it may be because the container runtime doesn't support this feature.Defaulting debug container name to debugger-988qv.PID USER TIME COMMAND 1 65535 0:00 /pause 17 root 0:00 ps aux--target=app คือสิ่งที่ทำให้มันมีประโยชน์กว่าแค่ “Pod busybox ข้าง ๆ ของฉัน”: ephemeral container แชร์ process namespace กับ target container ที่ระบุไว้ ดังนั้น ps aux ที่รัน ข้างใน debug container เห็น PID 1 เป็น /pause — process ของ target แบบ distroless เอง — ไม่ใช่แค่ shell ของตัวมันเอง ไม่มีอะไรใน Pod spec เดิมเปลี่ยนเลย kubectl get pod distroless-app -o yaml ยังโชว์แค่ container เดิมตัวเดียวบวก entry ephemeralContainers ใหม่
kubectl debug ยังไปถึงทั้ง Node ได้ด้วย เวลาปัญหาอยู่ที่ node เองไม่ใช่ Pod ตัวใดตัวหนึ่งบนนั้น — มัน schedule debug Pod แบบ privileged ที่ mount root filesystem ของ node ไว้ที่ /host
$ kubectl debug node/observability-worker -it --image=busybox -- chroot /host cat /etc/os-releaseVERSION_ID="13"VERSION="13 (trixie)"ID=debianEvent เป็นสัญญาณ ไม่ใช่แค่ note ข้างเคียง
หัวข้อที่มีชื่อว่า “Event เป็นสัญญาณ ไม่ใช่แค่ note ข้างเคียง”Warning event ทุกตัว (FailedScheduling, BackOff, Unhealthy, และอื่น ๆ) query ได้โดยตรง กรองฝั่ง server ได้เลย
$ kubectl get events -n log-demo --field-selector type=Warning --sort-by=.lastTimestampLAST SEEN TYPE REASON OBJECT MESSAGE37s Warning BackOff pod/crasher Back-off restarting failed container c in pod crasher_log-demo(607c53af-...)3s Warning BackOff pod/crasher Back-off restarting failed container c in pod crasher_log-demo(91d78655-...)
$ kubectl get events -n log-demo --field-selector involvedObject.name=crasherLAST SEEN TYPE REASON OBJECT MESSAGE31s Normal Scheduled pod/crasher Successfully assigned log-demo/crasher to observability-worker29s Normal Pulled pod/crasher Successfully pulled image "busybox" in 1.59s ...5s Normal Started pod/crasher Container started3s Warning BackOff pod/crasher Back-off restarting failed container c in pod crasher_log-demo(...)--field-selector กรองให้เหลือ object เดียวหรือ event type เดียวฝั่ง server (ถูกกว่า pipe ผ่าน grep) และ kubectl get events --watch stream event ใหม่สด ๆ ตามที่ API server ปล่อยออกมา ใช้กลไก watch เดียวกับ flag -w/--watch ของ kubectl ที่เหลือ event ก็มีอายุสั้นด้วย (default retention ประมาณหนึ่งชั่วโมง ควบคุมด้วย --event-ttl บน API server) ดังนั้นมันตอบ “เพิ่งเกิดอะไรขึ้น” ได้ดีกว่า “เมื่อวานเกิดอะไรขึ้น” มาก — อีกเหตุผลที่ต้องมี logging/metrics stack จริง
metrics-server: พอสำหรับ kubectl top และ autoscaling เท่านั้น
หัวข้อที่มีชื่อว่า “metrics-server: พอสำหรับ kubectl top และ autoscaling เท่านั้น”metrics-server เป็น cluster add-on เบา ๆ ที่เก็บ CPU/memory usage จากทุก kubelet และเก็บไว้ ในหน่วยความจำเท่านั้น — ไม่มี history ไม่มี persistence ไม่มี alerting มันไม่ได้เป็นส่วนหนึ่งของ kind โดย default และต้อง patch จริงหนึ่งจุดเพื่อทน self-signed certificate ของ kubelet บน kind
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yamlkubectl -n kube-system patch deployment metrics-server --type=json \ -p='[{"op":"add","path":"/spec/template/spec/containers/0/args/-","value":"--kubelet-insecure-tls"}]'kubectl -n kube-system rollout status deploy/metrics-serverมันมีไว้ทำสองอย่างเท่านั้น พิสูจน์ด้วยตัวเลขจริงจาก kind cluster 1.37 ที่รันอยู่จริง
$ kubectl top nodesNAME CPU(cores) CPU(%) MEMORY(bytes) MEMORY(%)observability-control-plane 166m 1% 806Mi 9%observability-worker 41m 0% 222Mi 2%observability-worker2 43m 0% 244Mi 2%
$ kubectl top pods -n log-demoNAME CPU(cores) MEMORY(bytes)distroless-app 0m 0Miweb-57787d5f95-dwbnf 1m 0Miweb-57787d5f95-g6qrh 1m 0Miweb-57787d5f95-q9hkw 1m 0Mi…และ HorizontalPodAutoscaler ที่ poll metrics-server เพื่อตัดสินใจว่าจะ scale Deployment ขึ้นหรือลง (พูดถึงลึกกว่านี้ในโมดูล Scheduling and Scaling)
apiVersion: autoscaling/v2kind: HorizontalPodAutoscalermetadata: name: checkoutspec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: checkout minReplicas: 2 maxReplicas: 10 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 70metrics-server ไม่ใช่ monitoring solution ชัด ๆ — ถ้าถามว่า “CPU usage เมื่อสามชั่วโมงก่อนเป็นยังไง” หรือ “แจ้งเตือนตอน memory เกิน 90%” metrics-server ตอบไม่ได้เลย มันรู้แค่ snapshot ปัจจุบัน
Prometheus + Grafana: stack observability มาตรฐานของวงการ
หัวข้อที่มีชื่อว่า “Prometheus + Grafana: stack observability มาตรฐานของวงการ”สำหรับ observability จริง ๆ — historical data, dashboard, alerting — combination มาตรฐานคือ Prometheus กับ Grafana
Prometheus ทำงานด้วย scraping: มันดึง metrics จาก HTTP endpoint /metrics ที่ workload เปิดไว้ในรูปแบบ text ของ Prometheus เอง เป็นระยะ ๆ application และ sidecar หลายตัวเปิด endpoint นี้เอง สำหรับ state ของ object ระดับ cluster (replica count ของ Deployment, phase ของ Pod, condition ของ node และอื่น ๆ) Prometheus scrape kube-state-metrics ซึ่งเป็น service แยกที่แปลง object ของ Kubernetes API เป็น metrics ของ Prometheus
apiVersion: v1kind: Podmetadata: name: checkout annotations: prometheus.io/scrape: "true" prometheus.io/port: "9090"spec: containers: - name: checkout image: checkout:2.3.0 ports: - containerPort: 9090# A workload's own /metrics endpoint, in Prometheus text formatcurl http://checkout-pod:9090/metricsPrometheus เก็บข้อมูลที่ scrape มาเป็น time series แล้วประเมิน alerting rule กับมัน Grafana อยู่ชั้นบนเป็น visualization layer query Prometheus เพื่อ render dashboard — trend ของ CPU/memory, request latency, error rate — และขับเคลื่อน alert notification
OpenTelemetry Collector: pipeline ที่เป็นกลางต่อ vendor ไม่ใช่ตัวแทน Prometheus
หัวข้อที่มีชื่อว่า “OpenTelemetry Collector: pipeline ที่เป็นกลางต่อ vendor ไม่ใช่ตัวแทน Prometheus”OpenTelemetry Collector เป็น pipeline ที่เป็นกลางต่อ vendor รับ, process, และ export log, metric, และ trace พร้อมกันได้ แทนที่จะต้องมี agent แยกต่อ signal ต่อ vendor ใน cluster Kubernetes มันมักรันเป็น DaemonSet (เก็บระดับ node คล้าย Fluent Bit ด้านบน) หรือ sidecar รับข้อมูลจาก application แล้ว route ไปยัง backend หนึ่งตัวหรือมากกว่า พร้อม batch, retry, และ filter ระหว่างทาง
ที่สำคัญคือ Collector เสริม Prometheus แทนที่จะแทนที่มัน: มัน รับ metrics ในรูปแบบ scrape ของ Prometheus เองได้แล้ว export ต่อไป ทำให้ cluster หนึ่งใช้ collection pipeline เดียวได้ ในขณะที่ยังเลือก (หรือเปลี่ยน) backend ได้ — Prometheus, vendor APM เชิงพาณิชย์, หรือทั้งสอง — โดยไม่ต้อง instrument application ใหม่ทั้งหมด บทนี้พูดถึงมันแค่ภาพรวม ไม่ได้ติดตั้งจริง
flowchart LR
subgraph Debug
exec["kubectl exec"] -.->|"fails, no shell"| distroless["Distroless container"]
debug["kubectl debug --target"] --> distroless
end
subgraph Logs
stdout["Pod stdout/stderr"] --> agent["Node-level log agent\n(Fluent Bit)"]
agent --> backend["Log backend\n(Loki / Elasticsearch)"]
end
subgraph Metrics
cadvisor["kubelet cAdvisor"] --> ms["metrics-server\n(in-memory)"]
ms --> top["kubectl top / HPA"]
workload["Workload /metrics\n+ kube-state-metrics"] --> prom["Prometheus"]
prom --> grafana["Grafana dashboards"]
end Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”ทุกอย่างในบทนี้คือ Kubernetes ให้ หน้าต่างสด ไม่ใช่ ประวัติศาสตร์ kubectl logs/exec/debug/top/get events ทั้งหมดตอบว่า “อะไรเป็นจริงกับ Pod หรือ Node นี้ตอนนี้ (หรือเมื่อกี้นี้)” — และทุกตัวสูญเสียข้อมูลทันทีที่ Pod ถูกลบ container ถูก garbage-collect หรือ event TTL หมดอายุ observability จริง (Fluent Bit → Loki/Elasticsearch, Prometheus → Grafana, หรือ OpenTelemetry Collector pipeline ที่ป้อนตัวใดตัวหนึ่ง) มีไว้เพื่ออยู่รอดจากตรงนั้นโดยเฉพาะ — มัน copy สัญญาณสดออกไปที่มี retention ก่อน ที่ Kubernetes จะทิ้งมัน ephemeral container ก็อยู่ในเรื่องเดียวกันแต่มองจากมุมอื่น: image แบบ distroless จงใจไม่มี tooling สำหรับ live-window ติดมาเลย ดังนั้น kubectl debug เอาหน้าต่างชั่วคราวของตัวเองมาแทนที่จะสมมติว่ามีอยู่แล้วข้างใน container
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ทางเลือก | ข้อดี | ต้นทุน |
|---|---|---|
kubectl logs/exec/debug/top | ไม่ต้องมี component เพิ่ม ใช้งานได้ทันทีที่ cluster มีอยู่ (พิสูจน์แล้วด้านบน) | ไม่มี retention, ไม่มี history, ไม่ aggregate ข้าม Pod หรือเวลา |
kubectl debug --target (ephemeral container) | debug container แบบ distroless/minimal ได้โดยไม่ต้อง rebuild image หรือ restart Pod (พิสูจน์แล้วด้านบน) | ต้องการ container runtime ที่รองรับ ephemeral container/shared process namespace; เพิ่ม container พิเศษที่มองเห็นได้ใน Pod ระหว่างที่มันรัน |
| Fluent Bit + Loki/Elasticsearch | log ที่ search ได้และเก็บไว้ทั่วทุก Pod และผ่านการลบ | infrastructure จริงที่ต้องรัน กำหนดขนาด และดูแล availability |
| metrics-server | footprint น้อยที่สุด พอสำหรับ kubectl top/HPA เป๊ะ (พิสูจน์แล้วด้านบน) | ไม่มี history เลย — ตอบ “เมื่อชั่วโมงก่อนเป็นยังไง” ไม่ได้ |
| Prometheus + Grafana | historical, query ได้, alert ได้ — มาตรฐานของวงการ | service ที่ต้อง stateful เพิ่มอีกตัวที่ต้อง operate (storage, retention, scrape config) |
| OpenTelemetry Collector | pipeline เดียวสำหรับ log+metric+trace เป็นกลางต่อ vendor ป้อน Prometheus ได้ | อีกชิ้นส่วนที่เคลื่อนไหวได้; ไม่ตัดความจำเป็นของ backend (Prometheus, Loki, หรือ vendor) ที่ต้องเก็บข้อมูลจริง ๆ ออกไป |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “
kubectl execควรทำงานได้เสมอเพื่อเปิด shell ใน container ไหนก็ได้” พิสูจน์แล้วด้านบนด้วย error ที่เจาะจงจริง: image แบบ distroless/minimal ไม่มี shell binary เลย ดังนั้นexecพังด้วยexecutable file not found in $PATH— วิธีแก้คือkubectl debug --targetไม่ใช่การสมมติว่า container พัง - “Kubernetes เก็บ log ไว้ที่ไหนสักแห่งอย่างถาวรโดย default” พิสูจน์แล้วด้านบน: log ถูกอ่านสดจาก node และหลังผ่าน restart cycle มากพอ แม้แต่
kubectl logs --previousก็ดึง log ของ container ที่ terminate แล้วไม่ได้อีก — ไม่มีอะไรถาวรถ้าไม่มี logging stack แยก - “metrics-server เป็น Prometheus แบบเบา ๆ” พิสูจน์แล้วด้านบน: มันเก็บแค่ snapshot ปัจจุบันในหน่วยความจำ ไม่มี history เลย มีไว้แค่ตอบ
kubectl topและป้อน HPA ไม่ใช่ให้ query หรือ alert - “
kubectl debug --targetrestart หรือแทนที่ container ของ Pod” พิสูจน์แล้วด้านบน: container เดิม (/pause, PID 1) รันต่อเนื่องตลอด ephemeral container เป็น process แยกเพิ่มเข้ามาที่เห็นได้ในps auxoutput เดียวกัน ไม่ใช่ตัวแทนที่ - “OpenTelemetry Collector แทนที่ Prometheus” ตามเอกสารของ OpenTelemetry Collector เป็น pipeline ที่เป็นกลางต่อ vendor ที่ รับจากและ export ไปยัง Prometheus ได้ในบรรดา backend อื่น ๆ — มันเป็น routing/processing layer ไม่ใช่ storage backend ที่แข่งกับ Prometheus
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”ตอน review คำสั่ง debug, คำสั่งดู log, หรือ manifest ด้าน observability ที่ AI สร้าง ให้เช็ค
kubectl exec -it <pod> -- /bin/shที่แนะนำเป็นขั้นแรกในการ debug image แบบ distroless หรือFROM scratch— พิสูจน์แล้วด้านบน มันพังทันทีด้วย error “executable file not found” จริง tool ที่ถูกต้องคือkubectl debug --targetkubectl logs <pod>(ไม่มี--previous) ที่แนะนำเพื่อวินิจฉัยว่า ทำไม container ถึง crash เมื่อกี้ — ถ้า container restart ไปแล้ว log ปัจจุบันอาจเริ่มหลัง crash--previousต่างหากที่โชว์คำพูดสุดท้ายของ instance ที่กำลังจะตาย พิสูจน์แล้วด้านบน- doc troubleshooting ที่สมมติว่า log search ได้ทั่ว cluster หรือทั่วเวลาโดยไม่ต้องทำอะไรเพิ่ม — พิสูจน์แล้วด้านบน Kubernetes เองไม่เก็บ history เลย ความสามารถนั้นมีก็ต่อเมื่อ deploy logging stack (Fluent Bit/Loki ฯลฯ) แล้วจริง ๆ
metrics-serverที่เสนอเป็นตัวแทน “เราต้องการ monitoring/alerting” — พิสูจน์แล้วด้านบน มันไม่มี history และ alert ไม่ได้ ต้องใช้ Prometheus (หรือเทียบเท่า) บวก Grafana หรือ alerting layer อื่นhostPath: /var/logmount ที่แนะนำเป็นวิธีเร็ว ๆ “เก็บ log” จากข้างใน application Pod เอง แทนที่จะเป็น DaemonSet agent ระดับ node เฉพาะ — วิธีนี้ผูก application เข้ากับ path เฉพาะของ node และพลาด log ของ Pod อื่นทุกตัวบน node นั้น pattern มาตรฐานคือ DaemonSet อย่าง Fluent Bit ไม่ใช่โค้ดเก็บ log ต่อ application
🐛 หา bug ในโค้ดนี้
apiVersion: v1kind: Podmetadata:name: distroless-appnamespace: log-demospec:containers: - name: app image: registry.k8s.io/pause:3.10เฉลย
manifest นี้ไม่มีอะไรผิดในตัวมันเอง — bug อยู่ที่วิธีที่ AI assistant มักแนะนำในการ debug มัน: kubectl exec distroless-app -- sh พิสูจน์แล้วด้านบน มันพังทันทีด้วย OCI runtime exec failed: ... exec: "sh": executable file not found in $PATH เพราะ registry.k8s.io/pause (เหมือน image แบบ distroless ทุกตัว) ไม่มี shell มาให้เลย วิธีที่ถูกต้อง ก็พิสูจน์แล้วด้านบนเช่นกัน คือ kubectl debug -it distroless-app --image=busybox --target=app -- ps aux ซึ่งแนบ container ที่ใช้แล้วทิ้งเข้าไปแชร์ process namespace กับ target แทนที่จะพยายามรัน shell ที่ไม่เคยมีอยู่