สวัสดีทุกคนน้า ^ w ^ ~ ช่วงนี้ห่างหายไปสักพักเลย ไม่ค่อยได้เขียน Blog เท่าไร เพราะช่วงนี้ติดงานสุด ๆ เลยล่ะ 5555
วันนี้อยากมาเล่าเรื่องประสบการณ์ที่เพิ่งเจอได้เมื่อไม่นานนี้ แล้วเป็นสิ่งที่น่าสนใจพอตัวเลยล่ะ เผื่ออาจเป็นประโยชน์กับใครหลาย ๆ คนสำหรับที่กำลังทำ MongoDB TLS โดยใช้ Let’s Encrypt.
บอกเลยว่าเป็นอะไรที่ทำให้ผมหัวหมุนไปทั้งวัน จากทั้ง ๆ ที่ควรจะได้ไปทำ Project อื่นต่อ แต่กลับมางมอันนี้ทั้งวันเลยล่ะ (ถ้าไม่มี AI Claude ช่วยนี่น่าจะหาอีกนานเลย) = v =“
Disclaimer
เนื้อหาที่เขียนมาในรอบนี้อาจมีคำศัพท์ที่แปลกหูบ้าง เนื่องด้วยให้ Claude ช่วยเขียนบทความให้ และก็ผมขัดเกลาให้ทุกคนพอเข้าใจมากที่สุด = w =“
TL;DR สำหรับคนที่อยากอ่านสรุปไว ๆ
อยู่ ๆ createIndex บน collection MongoDB ที่มี Document ขนาด 1.1M record ก็ค้างไม่ยอม commit ไล่ดูแล้วเหมือนจะเป็นเรื่อง network timeout, แล้วเหมือนจะเป็น resumed index build ค้าง, แล้วเหมือนจะเป็น autoIndex ถล่ม, แล้วเหมือนจะเป็น write จาก cronjob ที่ทำให้ drain ไม่จบ
แต่… root cause จริงคือ Let’s Encrypt เอา clientAuth EKU ออกจาก certificate ตามนโยบายใหม่ (กุมภาพันธ์ 2026) ทำให้ single-node replica set ตรวจสอบ certificate ของตัวเองไม่ผ่าน → commit index build ไม่ได้ พอออก internal cluster certificate ที่มี clientAuth แยกมาใช้สำหรับ intra-cluster TLS อาการทั้งหมดก็หายไป และ index ที่เคยค้างเป็นวันก็ commit เสร็จใน 1-2 วินาที O-O?!!!
จุดเริ่มต้น: index ที่สร้างไม่เสร็จ
เริ่มจากงานธรรมดา — ในขณะที่ผมกำลัง setup compound index บน collection ที่มี ~1.1 ล้าน document เพื่อ optimize report query:
db.report_transaction.createIndex({ user_id: 1, group_id: 1, time_bucket: 1})รันใน MongoDB Compass แล้วได้:
PoolClearedOnNetworkError: Connection to mongo:27017 interrupteddue to server monitor timeoutเอ๊ะ ไง๋เป็นแบบนั้นไปได้กันนะ O-O????
ในสิ่งที่ผมนึกอย่างแรกคือ ข้อความนี้คิดว่าน่าจะเป็นปัญหา network/Compass แต่ความจริงคือ index build ฝั่ง server ยังทำงานต่อ
Compass แค่หลุดเพราะ heartbeat timeout ตอน server busy นี่คือ บทเรียนแรก: error ที่ Client เห็น มักไม่ใช่ปัญหาที่แท้จริง
Issue 1: build ที่ค้างมา 8 วัน
จากที่เมื่อกี้เกิดข้อผิดพลาดก็เลยมาลองลองพิมพ์คำสั่ง db.currentOp() ดู…
กลับเจอ index build 3 ตัวที่ไม่ได้สั่ง — และ secs_running อยู่ที่ 681,354 วินาที (ซึ่งหาก แปลงเป็นวันจะได้ 7 - 8 วัน):
msg: 'Index Build: draining writes received during build'numYields: 0secs_running: 681354// ไม่มี lsid, ไม่มี $dbOMG! ทำไมมันรันนานขนาดนั้น O-O?!!!!
แต่ทีนี้มาดูจุดสังเกตที่สำคัญก่อน ว่ามีอะไรที่มันแปลก ๆ บ้าง
numYields: 0— op ที่ทำงานจริงต้อง yield CPU เป็นระยะ ค่า 0 ที่ไม่ขยับ = ค้าง- ไม่มี
lsid/$db— แปลว่าไม่ได้มาจาก client session แต่เป็น build ที่ mongod resume ขึ้นมาเองตอน startup - ทั้ง 3 ตัวเริ่มพร้อม uptime พอดี
อันนี้คือ Log ตอนที่เช็คจาก MongoDB:
"Found unfinished index build to resume" ... "phase":"drain writes"สาเหตุ: ช่วงก่อนหน้ามีการย้าย database ข้ามเวอร์ชัน (MongoDB 7.0 ↔ 8.0) หลายรอบ ระหว่างนั้นมี index build ค้าง พอ mongod restart มัน resume build เหล่านั้นแต่ค้างที่ phase drain writes และเนื่องจาก MongoDB จำกัด maxNumActiveUserIndexBuilds = 3 build ที่ค้าง 3 ตัวนี้กิน slot จนเต็ม → build ใหม่ทุกตัวเข้าคิวไม่ได้
วิธีแก้: ใช้คำสั่ง killOp build ที่ค้างทิ้ง (build ที่ยังไม่ commit = index ยังไม่มีจริง ไม่กระทบ data)
db.adminCommand({ killOp: 1, op: <opid> })Issue ที่ 2: autoIndex ยิงสร้างจาก 34 microservices
พอเคลียร์ index เก่า กลับมีการพยายาม build index ใหม่ไหลเข้ามาจากหลาย database ไม่หยุด — ทั้งที่สั่ง index แค่ตัวเดียว build โผล่จาก database1, database2, database3, database4… ซึ่งไม่ทีทางหยุดได้เลย
อาการนี้มีอยู่อย่างเดียว คือ Mongoose ที่ถูกตั้งค่า autoIndex เป็น true เสมอ ทุกครั้งที่ service instance มีการ connect/reconnect (deploy, restart, scale, network blip) Mongoose จะรัน ensureIndex ให้ทุก schema เมื่อมี 34 microservices ต่อ MongoDB instance เดียวกัน แต่ละตัว re-deploy ก็ Request สรา้ง index ของมันเข้ามา ทำให้คิว index บานไม่หยุด @~@
วิธีแก้: ปิด autoIndex ใน production ทั้ง 34 repo แล้วย้ายไปจัดการ index แบบ controlled ตอน deploy แทน
// ทุก service ที่ต่อ MongoDBmongoose.set('autoIndex', false);เนื่องจาก 34 repo เป็น 2 pattern (Express + NestJS) ที่ copy โครงสร้าง connect กันมา จึงใช้ AST codemod (ts-morph) แทรก mongoose.set('autoIndex', false) อัตโนมัติทั้งหมด แทนการแก้มือทีละ repo และเพิ่ม script syncIndexes() กลางที่รันตอน deploy แทน autoIndex ที่ปิดไป
Issue 3: ความเข้าใจผิดเรื่อง write จาก CronJob
หลังปิด autoIndex กลับมาสร้าง index 1.1M ตัวเดิม โดยผ่าน phase scanning collection (numYields เดินจริง = 1165 ครั้ง แล้ว) ซึ่งเหมือนเป็นสัญญาณที่ดีใช่ไหมล่ะ (หรือเปล่านะ…)
แต่… พอถึง draining writes ก็ค้างที่นั่นอีก รันไป 3 ชั่วโมง numYields ค้างที่ 1165 ไม่ขยับ ขณะ secs_running เดินต่อ — definition ของค้าง
ความคิดในตอนนั้นคือ Collection report_transaction ถูกป้อนโดย aggregate cronjob ทุก 5 นาที จาก database collection อื่น ๆ หลายตัว เลยคิดว่า write ที่ไหลเข้าตลอดทำให้ drain ตามไม่ทัน
แต่ก็ยังสรุปไม่ได้อยู่ดี เพราะตอน index build ใน database อื่น ๆ ที่แทบไม่มี write traffic ก็ค้างเหมือนกัน** ถ้าค้างเพราะ write จริง staging ที่เงียบไม่ควรค้าง
มันสามารถบอกว่ายังมีอะไรลึกกว่าที่ระดับ server ไม่ใช่ระดับ write ของแต่ละ collection แล้วล่ะ
มาถึงตรงนี้แล้ว หัวก็เริ่มที่จะตื้อมาก ๆ และแทบทำอะไรไม่ถูกเลยตอนนั้น มึนหัวสุด ๆ ;w;
Issues 4: Certificate ที่ปฏิเสธตัวเอง
ระหว่างนั้นที่กำลังทำตาม Claude ไปทีละ Step-by-step จู่ ๆ ก็ให้ลองไปอ่าน Log index ดู ซึ่งก็ไม่มีอะไรเกิดขึ้น
จนผมเอะ สงสัยว่าด้านใน log มันมีอะไรแปลก ๆ หรือเปล่าก็เลยลอง tail -f -n 200 /var/log/mongodb/mongod.log ดู สิ่งที่เจอคือ…
SSLHandshakeFailed: SSL peer certificate validation failed:unsuitable certificate purpose...ReplicaSetMonitor ... Host failed in replica set ...HostUnreachable: stream truncatedและใช่ Remote IP ของ connection ที่พยายามเชื่อม แต่ fail คือ… ตัว MongoDB เอง
อ้าว ทำไมเป็นแบบนั้น?!
ก็เลยสงสัยถาม Claude ให้ลองเช็คดูว่าเช็คใบ Certificate ทีว่าทำไมเป็นแบบนั้นไป ได้มาแบบนี้
openssl x509 -in /etc/ssl/mongodb/db.pem -noout -ext extendedKeyUsage# X509v3 Extended Key Usage:# TLS Web Server Authentication ← มีแค่ serverAuth# ← ขาด TLS Web Client Authentication o_O?!Config:
net: tls: mode: requireTLS certificateKeyFile: /etc/ssl/mongodb/db.pem # ใช้ใบเดียวทั้ง server และ clientสิ่งที่ที่ทำให้ทุกอย่างค้าง เป็นที่ Certificate
โดย MongoDB ถูก deploy เป็น single-node replica set แต่ด้วย Mongoose ต้องใช้แบบ Muti-document transaction ในการ Insert / Update จึงจำเป็นต้องทำ Database ในรูปแบบ Replication เพื่อให้รองรับฟีเจอร์นี้ โดยถึงแม้มี Node เดียว แต่ตัว ReplicaSetMonitor ก็ยังต้อง “ต่อหาสมาชิก” เพื่อเช็ค Health ซึ่งสมาชิกเดียวคือตัวมันเอง
แต่ตอนต่อหาตัวเอง mongod ระบบก็ทำตัวเป็น TLS client และส่งใบ certificate ของมัน (db.pem) ไปให้ฝั่ง Server (ตัวเอง) ตรวจสอบ แต่ db.pem มี EKU แค่ serverAuth แต่ไม่มี clientAuth ทำให้ Server ปฏิเสธด้วย “unsuitable certificate purpose” นั่นเป็นสาเหตุว่าทำไม node มองว่าตัวเอง unreachable เป็นช่วง ๆ
และเนื่องจาก index build ต้อง commit ผ่าน replication state ที่ healthy เมื่อ Replica monitor มองว่า host ของตัวเอง unreachable ไม่เสถียร → commit phase รอ confirmation ที่ไม่มีวันมา นั่นเป็นสาเหตุว่าทำไมค้างที่ drain writes ทุกครั้งตอนที่สร้าง index
นี่คือสิ่งที่อธิบายได้ว่า:
- ทำไม staging ที่ไม่มี write ก็ค้าง = ไม่ใช่เรื่อง write แต่เป็น replication health
- ทำไม build commit ไม่ได้ทุกตัว = commit path ต้องผ่าน Replica ที่ปฏิเสธตัวเอง
- ทำไม
numYieldsค้าง = build รอ replication coordination ที่ไม่มา
แล้ว Let’s encrypt เกี่ยวอะไรกับ MongoDB?
เนื่อวด้วยใบ Certificate ถูกสร้างมาจาก Let’s Encrypt เพราะต้องวิ่งผ่าน Domain เพื่อ Verify TLS connection
ซึ่งในอดีต Let’s Encrypt ออกใบ certificate พร้อมกับ EKU ทั้ง serverAuth + clientAuth แต่ทว่า:
- ในวันที่ 11 กุมภาพันธ์ 2026: Let’s Encrypt ประกาศว่าจะเอา
clientAuthEKU ออกจาก default certificate profile (ตามข้อกำหนด CA/Browser Forum และ Chrome Root Program ที่บังคับแยก PKI ของ client/server authentication) ลิ้งก์ข่าว - แต่มี
tlsClientprofile ชั่วคราวให้ opt-in — ถ้าเริ่มใช้ก่อน 13 พฤษภาคม 2026 จะใช้ต่อได้ถึง 8 กรกฎาคม 2026 หลังจากนั้นจะไม่มีการออก certificate ที่มี clientAuth EKU อีกเลย
โดยตอนสร้าง Certificate ของ MongoDB ไปโดน auto-renew รอบนโยบายใหม่ ทำได้ certificate ที่มีแค่ serverAuth พอเอามาใช้ใน intra-cluster TLS (ที่ node ต้องทำตัวเป็น client) มันก็พังทันที โดยไม่มีใครแตะ config
เอาละ เราเจอปัญหาซักที มาแก้ดีกว่า ย๊ากกกก!!!!
วิธีแก้: แยก Certificate ตามบทบาท
ทางแก้ที่ถูกต้อง (และเป็นทิศทางที่ทั้ง industry กำลังบังคับ) คือ เลิกใช้ public certificate ใบเดียว และทำใบ Certificate ทั้ง server และ client ออก internal certificate แยกสำหรับ intra-cluster authentication
โดยจะสร้างใบ Certificate สำหรับใช้ Internal ใช้ OpenSSL ในการสร้างขึ้นมา:
# internal CA (อายุยาว ใช้ภายในเท่านั้น)openssl req -x509 -new -nodes -key internal-ca.key -sha256 -days 3650 \ -out internal-ca.pem -subj "/CN=internal-ca/O=Org"
# cluster cert ที่มีทั้ง serverAuth + clientAuthopenssl x509 -req -in cluster.csr -CA internal-ca.pem -CAkey internal-ca.key \ -out cluster.crt -days 3650 \ -extfile <(printf "extendedKeyUsage=serverAuth,clientAuth")config สุดท้าย — แยก certificate 2 ใบตามบทบาท:
net: tls: mode: requireTLS certificateKeyFile: /etc/ssl/mongodb/db.pem # Let's Encrypt — รับ external client (serverAuth พอ) CAFile: /etc/ssl/certs/ca-certificates.crt # validate external client (public CA bundle) clusterFile: /etc/ssl/mongodb/cluster.pem # internal — intra-cluster (มี clientAuth) clusterCAFile: /etc/ssl/mongodb/internal-ca.pem # validate cluster member cert allowConnectionsWithoutCertificates: trueสิ่งที่ควรจะรู้ที่เจอระหว่างแก้:
tlsClusterCAFileบังคับให้ต้องมีtlsCAFileด้วย (ใช้tlsUseSystemCAคู่กับclusterCAFileไม่ได้) — แก้โดยชี้CAFileไปที่ system CA bundle ตรง ๆCAFileกับtlsUseSystemCAเป็น mutually exclusive ต้องเลือกอย่างเดียว
ผลลัพธ์: index build ที่เคยค้างเป็นวัน — commit เสร็จใน 1-2 วินาที
ในที่สุด สิ่งที่แก้นั่งแก้มาทั้งวันมันได้ถูกแก้ไขแล้ว TwT
สิ่งที่ได้เรียนรู้จากการแก้ไขและหาต้นตอปัญหา
1. Error ที่ client เห็น มักจะไม่ใช่ root cause ที่แท้จริงเสมอไป “Compass network timeout” → server busy; “build draining” → replication commit ค้าง; ตัว symptom ที่ดังที่สุดมักอยู่ไกลจากต้นตอที่สุด
2. เชื่อ metric ที่วัดได้ มากกว่าชื่อ phase numYields ที่ freeze บอกว่าค้างได้ชัดกว่าการเดาจาก msg และ getIndexes() ที่เห็น index ขึ้น ไม่ได้ แปลว่า build commit แล้ว — ต้องเช็ค currentOp ว่างคู่กัน
3. เบาะแสที่ทฤษฎีอธิบายไม่ได้ คือหนทางไปสู่ root cause “staging ที่ไม่มี write ก็ค้าง” คือจุดที่ทำให้ทฤษฎี write-storm พังและพาไปเจอ TLS การไม่มองข้าม anomaly ที่ขัดกับสมมติฐานคือกุญแจ
4. dry run ก่อนแตะ production config การ start mongod ด้วย config ใหม่บน port + dbpath ชั่วคราว (--port 27099 --dbpath /tmp/...) ทำให้ได้เห็นว่า ทุกครั้งตอนรันจริงมันเกิดอะไรขึ้นบ้าง หากไปรันก่อน อาจทำให้ข้อมูลเสียหายได้
5. นโยบาย CA ที่เปลี่ยน เป็นความเสี่ยงที่มองไม่เห็น การ deprecate clientAuth EKU กระทบทุกระบบที่ใช้ public certificate ทำ mutual TLS / intra-cluster auth — ไม่ใช่แค่ MongoDB ถ้าระบบไหนใช้ Let’s Encrypt cert เป็น client cert ควรย้ายไป internal CA ก่อน 8 กรกฎาคม 2026
Timeline สรุป
| Issues | อาการ | root cause ที่แท้จริง | วิธีแก้ |
|---|---|---|---|
| 0 | Compass network timeout | server busy ตอน build | รันจาก local mongosh |
| 1 | build ค้าง 8 วัน | resumed build ตกค้างจากการย้าย DB ข้ามเวอร์ชัน | killOp |
| 2 | build ขยายไม่หยุด | Mongoose autoIndex ถล่มจาก 34 service | ปิด autoIndex (codemod) + syncIndexes |
| 3 | build prod ค้างที่ drain | (เข้าใจผิดว่าเป็น write จาก cronjob) | — |
| 4 | commit ไม่ได้ทุก collection | Let’s Encrypt ตัด clientAuth EKU → RS ปฏิเสธตัวเอง | internal cluster cert |
แต่ละ Issues เป็นปัญหาจริงที่ควรแก้ (resumed build, autoIndex ควรปิดใน prod) แต่ตัวที่ทำให้ commit ไม่ได้เลยคือชั้นล่างสุด — TLS เมื่อแก้แล้ว ทุกอย่างที่เหลือก็ลื่นทันที