Performance Issue (Recap จากประสบการณ์)

สำหรับ Blog นี้มา Recap ปัญหาที่ทำให้ระบบช้า หรือ หน่วง่าจากที่ผมเข้าไปแก้ไขเนี่ยมีเคสอะไรบ้าง แล้วแก้ไขอย่างไรครับ จริง Blog ดองมานาน 2-3 ปี เขียนเติมมาเรื่อยๆ บางอันอาจจะเก่าแล้วก็ได้นะ หัวข้อที่เขียนประมาณนี้

ก่อนจะเริ่มจัดการ Performance Issue สิ่งที่เราควรมี

📌 พิมพ์เขียวของระบบ ไม่ว่าจะเป็น System Architecture หรือ Software Architecture เอาไว้เห็นการไหลของข้อมูล และความสัมพันธ์ของละ Sub System / Microservice ครับ

📌 ข้อมูลบันทึกการทำงานของระบบ สมัยนี้เค้าจะเรียกว่า Observability พวก Log / Tracing / Metric ในปัจจุบัน Software และ Framework หลายๆมี Endpoint ที่จะเอามา Feed เข้า Centralize อย่าง LGTM แล้วครับ แค่ถ้าไม่มีเอาแบบบ้านๆ Log ของพวก OS Perfmon (Windows) / SysLog (Linux) มีข้อมูล System Utilization (CPU , Memory Disk , Network )

📌 การเปลี่ยนแปลงที่เกิดขึ้นภายในระบบ พวก Ticket การแก้ไข จริงๆมันดู Commit ก็ได้แหละ แต่มันหาสรุปยาก เอาจริงคนที่แก้ควรจะสรุปมากกว่า เคสล่าสุดมีคนไปแก้ แล้วไม่เปิด Ticket > QA ไม่รับตรวจ เพราะไม่มี Owner เลยไหลไปที่ Prod ลูกค้าตรงแทน

เอาหละมาลอง Recap ดีกว่ามีประเด็นอะไรบ้าง

- Application Tier

📌 APP - เขียน Code แบบไม่เอ๊ะ หรือ Optimize ปัญหาที่เจอประจำพวก การเขียน Loop ซ้อนกัน หรือ Query แบบ N+1 แล้ว ถ้า Code นั้นมันต่อ DB / API ด้วย CPU วิ่งเป็นช่วงๆ

ถ้าระบบแบบ Cloud-Native / Distributed System มีเคสคล้ายๆกับ N+1 ในมุมของ Microservice เหมือนกัน ปัญหาพวก Chatty API นอกจากข้าใน Service ยังมีพวก Network Roundtrips แถมให้อัก ช้าช่วยช้าพลัส

คำแนะนำ

  • ลองจัด Flow งานเป็น Batch / หรือ อันไหนมันใหญ่บ้าง ไม่ได้แก้เลยทำเป็น Cache เอาง่ายๆ ฝากไว้ที่ตัวแปรนึงก่อน Loop หรือ ถ้า Distribute ฝากไว้ตัว Caching Service อย่างพวก Redis ก็ได้นะ
  • พวกงานที่ Distributed System ถ้า Service หลังบ้านช้า มี Flow Auto-Retry ถี่ๆ โดยไม่มี Exponential Backoff หรือ Circuit Breaker มันกลายเป็นการทำ DDOS ระบบตัวเองด้วยนะ ทำพวก Circuit Breaker / Retry distributed amplification pattern

📌 ไม่เข้าใจ Data Structure และพวก Helper Method ที่แต่ละภาษาออกแบบมาให้ใช้งาน ตอนแรก Data เล็กๆไม่เห็นภาพ แต่พอข้อมูลเยอะขึ้น อย่างเคสที่ผมช่วยให้คำแนะนำใน Module Compliance ล่าสุด เช่น

  • การ Access ข้อมูลใน List ถ้าข้อมูลเยอะขึ้นช้าลงเรื่อยๆ เป็น O(n)
    - ถ้าเรียกใช้บ่อย Key Value ควรเอาพวก Map หรือ Dictionary / ถ้ามาเดี๋ยวๆไปใช้ HashSet ก็ได้ ทำทดไว้
    - หรือ ย้ายไปใช้พวก tree ถ้ามันทำเป็นโครงสร้างได้
  • การใช้บาง Function Code dotnet และกัน
#Code Before / AfterImprove
1Before
.Where(x => x.SecurityId == id) .FirstOrDefault()
After
.FirstOrDefault(x => x.SecurityId == id)
Loop จาก 2 เหลือ 1 ลด chain ที่ไม่จำเป็น, อ่านง่ายขึ้น
2Before
.Where(g => list .Select(p => p.PortfolioId) .Contains(...)) .FirstOrDefault()
After
.FirstOrDefault(g => list.Select(p => p.PortfolioId) .Contains(...))
รวม 2 ขั้น → 1 เหมือนข้อ 1 แต่ predicate ซับซ้อนกว่า
3Before
.Where(p => p.PortfolioId == id) .Select(q => q.ExpectedNAV) .Sum()
After
.Where(p => p.PortfolioId == id) .Sum(q => q.ExpectedNAV)
ลด iteration Sum() รับ selector ได้เลย ไม่ต้อง Select ก่อน
4Before
xx.Count() > 0
After
xx.Any()
Any() เร็วกว่า หยุดทันทีที่เจอ
Count() นับทั้งหมด

ถ้าพวกเรือง Data Structure / Perf ลองมาดู Blog: Back to Basic: Fundamental Data Structure in C#

📌 Memory Leak ที่อาจจะเกิดจากการทำเราเปิด Session ไว้นานเกินความจำเป็น พวก httpClient หรือ DB Connection อีกที่ทำให้ Leak พวก Disk IO เปิดแล้ว ไม่คืน

คำแนะนำ ต้องมาตั้งเวลาในการ Clear ให้เหมาะสม และ Review Code ให้คืน Resource อย่างพวก DB I/O ส่วนของ dotnet จะมี using ให้ครบบอก Scope ทำเสร็จมันจะ Clear ให้ หรือ ใข้ Pool จะได้ของเดิมมา Reuse ได้

📌 LOCK - ตรงตัวครับ มี Process นึงมันไปถือของของไว้นานเกินความจำเป็น ทำให้อีก Process เกิดข้อผิดพลาดได้ครับ เช่น Disk IO ตัวดีเลย ถ้าเขียนไม่ดี Excessive Logging

  • ทางแก้ คุมการคืน Resource หรือ จัดเรียง Logic ใหม่ ให้มีการ lock ได้ แต่ต้องไม่แย่งกัน มี Queue

📌 Thread Pool หลายภาษา มีแนวคิดว่าของมันสร้างมาแล้ว ใช้เวลาปั๊นพอสมควรเลย อย่างพวก DB Conn เอางี้ เรามา Reuse ใหม่ แต่ถ้าตั้งเยอะไป ตัว Pool มันไม่ได้ใช้ประโยชน์ แถมจอง Mem เกินไปด้วย

คำแนะนำ ต้องมาทดสอบทำ Perf Test แล้วมาปรับจูน อีกเคสที่เจอมีคนเขียน Pool เอง คนเขียนไม่ยอมสื่อสารว่าค่าที่ควรต้องเท่าไหร่ มันเลยได้ค่า Default ไป 3 ระบบช้าลง เพราะต้องรอของออกจาก Pool ครับ แก้มาแล้วสื่อสารให้ชาวโลกรู้ด้วย

📌 Garbage Collection (GC) - ถ้าดูจาก Monitor จะเจอ Memory spike และ CPU pause เพราะต้องรอ GC มัน Clear

คำแนะนำ ลองมาตรวจก่อน ว่ามีอะไรที่ไปกระตุ้นให้ GC มันทำงานบ่อย เช่น Memory Leak หรือ Config Thread Pool มากเกินไป

📌 Thundering Herd - หลาย task/process แย่งทรัพยากรพร้อมกัน แล้วมันจะลากปัญหาอีกหลายเรื่อยตามมาอย่าง Resource Exhaustion ของมันไม่พอ พวก CPU / Memory / Network จนทำให้เกิด Thread Starvation ต้องรอ Thread

คำแนะนำ ต้องมาทำความเข้าใจ Blocking กับ Non-blocking เอางานไหนที่ต้องรอ Sync (Blocking - รอทำให้จบ) งานไหน Async (Non-blocking ที่สั่งแล้วปล่อยได้) ที้เราต้องมาดูในแต่ละภาษาและเลือกใช้ให้ถูกต้องกับงาน

  • Parallel.ForEach with Limit MaxDegreeOfParallelism (sync) ควรใช้กับ ThreadPool / DB Conn Pool
  • Task.WhenAll + Semaphore (sync)
  • Parallel.ForEachAsync (async)

📌 Connection Pool Starvation - หลายครั้ง Code ของเรามันจอง/ยึด Resource ไว้ก่อน แต่ยังไม่ถึงเวลาที่ต้องใช้งาน หรือ จัดลำดับไม่เหมาะสม เช่น

  • แบบแรก Get DB Conn > SELECT > Call 3RD API > Update > Release DB Conn จะเห็นว่าช่วง Call 3RD API มันทำจอง DB Con โดยไม่จำเป็น ถ้ามีเยอะ งานอื่นๆจะช้า Timeout
  • แบบสอง Get DB Conn > SELECT > Release DB Conn > Call 3RD API > Get DB Conn > Update > Release DB Conn ตัว DB Conn ไม่ถึงยึดว่าช่วง Call 3RD API เอาไปทำอย่างอื่นๆได้

คำแนะนำ

  • Review และตกลงลำดับการทำงานให้เหมาะสม ดูพวก consistency model แต่ต้องคุยกับ Business ด้วยว่ามันเป็น critical section หรือ ป่าว
  • บางที่เค้า Lock ไว้ เพื่อแก้ Stale read / lost update พอไปแยกแบบที่สองคุณสมบัติตรงนี้หายไปด้วย

📌 งานเร่ง แต่ requirement เปลี่ยนแล้ว ลืมเอา code ตรวนั้นออก มัน call เงียบๆไปหลายปี 555

คำแนะนำ Refactor / ยอมตัดใจเอาออก ใช้ Git แล้วไปดูจาก History เอาสิ งานพวกนี้ ถ้าทำต้องมี Note สักนิดนะ

📌 การจัดเก็บข้อมูลพวกไฟลฺฺ์ ลง App Server

  • ถ้าสบายหน่อยใช้ Blob Storage ที่เหลือการจัดการเรื่อง Structure / HA ก็ฝากไว้กับมัน
  • แต่ถ้ายังเก็บไว้ใน Path ของ App Server อันนี้ต้องระวังอย่างไปทำแบบว่าสร้าง folder ชื่อ uploads แล้วกองทุกอย่างเป็นใน path นั้นนะ พอไฟล์ยิ่งเยอะ ระบบมันจะยิ่งช้า ช้าจากตัว OS ที่จะหาไฟล์แบบ Linear Search หรือ มี B-Tree (ใน Ext4) มันมี Limit มันทดลง RAM ถ้ายิ่งเยอะ จะยิ่งช้า

คำแนะนำ แบ่งข้อมูล เช่น

  • แยก folder ตาม YEAR-MONTH ตัวอย่างใกล้ตัว WordPress ใช้นะ เมื่อก่อนกองทีเดียวกันแหละ แต่เค้าปรับมาเกิน 8-9 ปีแล้ว Blog เก่าๆผมยังมีซากกองที่ root ค่อยย้ายแก้มา
  • หรือ ใช้ Hash-based shading เอาไฟล์มา hash จากชื่อ หรือตัดมา แล้วกระจายเก็บ แต่อย่างให้ Folder มันลึกเกินไปนะ ตัวอย่างประมาณนี้
/uploads/
  ab/c1/abc123.jpg  >> ตัด ab ชั้นแรก และ c1 ชั้น 2
  de/f4/def456.jpg
  xy/z7/xyz789.jpg
- Database Tier

📌 ออกแบบ schema ผิด อันนี้น่าจะเคยที่เจอกันประจำเลย โดยมันมีสาเหตุแตกย่อยลงไปได้ ประมาณนี้เท่าที่นึกได้

  • เคยเจอว่า Database เก็บเรื่องเดียวกัน ไม่ได้ Sync กัน เลยแยก Table / Field ของตัวเอง
  • Assumption เปลี่ยน - ตอนแรก Architecture ที่ Design มา รองรับ ตัวเลข X Tx และมี Growth Y% แต่ทว่าพอเวลาผ่านไป Z ปี ปรากฏว่า Assumption ที่เราคาดการณ์ไว้มันเปลี่ยน
    ** อันนี้จริงๆ ต้องเอา business มาคุยด้วย แล้วมาคาดการณ์ Trend ล่วงหน้า

📌 เลือกรูปแบบฐานข้อมูลผิด หลักๆ มันมี 2 แบบ SQL / NoSQL แต่ละแบบมันมีงานที่เหมาะสมกับมันอยู่นะ ถ้าเลือกผิดมันมีผลกับ Perf ได้

  • SQL - งานทืต้องการ ACID / ข้อมูลมี Structure ที่แน่นนอน แต่มีข้อจำกัดเรื่องการ Scale
  • NoSQL - ข้อมูล Unstructured/Semi-structured และยอมรับว่าขาด ACID ไปบางอัน ตาม CAP Theorem) แต่ได้ Scalability กับ Speed ที่เพิ่มขึ้นนะ

ปัญหากลุ่ม - ไม่ได้ปรับการตั้งค่า Index - มีผลกับ App ผลสรุปเป็นตารางตามนี้

📌 ปัญหา Missing Index / Too Many Index ขอสรุปลงตารางนะ

ปัญหาReadWriteDiskMemory
Missing Indexช้าปกติหนักสูง
Too Many Indexปกติ/เร็วช้าหนักสูง

คำแนะนำ Review Index เพื่อลบ ตามการใช้งานจริง

ปกติพวก DBMS มันจะมีตัว Query Analyzer 0มาให้แล้ว ของ Azure SQL มีตัวนี้ เราสามารถสร้าง ปรับ index ออกตามที่มันแนะนำได้ และ Review Index อันไหนที่เคย Work ปัจจุบันอาจจะไม่ใช่แล้วก็ได้ ถ้าปล่อยไว้มันจะไปเกิด overhead ตอน insert / update หรือ เปิด Fragmentation

📌 ปัญหา Fragmentation เกิดจาก Index มันใช้พื้นที่ทด ถ้ามันอยู่กระจายไป มันช้าลง Disk I/O หน่วง แม้ว่าเราจะลบ Index ไปแล้ว พื้นที่มันไม่จัดใหม่ให้นะ

คำแนะนำ rebuild/reorg ตัว index

📌 Stats ไม่อัปเดต - ทำให้ตัว DBMS Query Optimizer เลือก plan ไม่ดี ส่งผลให้ CPU และ TempDB (MSSQL) หนัก

คำแนะนำ

ปัญหากลุ่ม - Transactions

📌 Uncommitted Transactions ค้าง - Lock ค้าง → Memory/Transaction log โต
📌 Large Commit → flush ทีเดียวใหญ่ → disk burst / Lock ค้าง → Memory/Transaction log โต
📌 Min Commit → force flush → High IOPs
📌 เลือกใช้ Transaction ให้ถูก - Lock Contention (Lock wait) , Lock Escalation , Deadlock

คำแนะนำ

  • ต้องมาปรับที่ตัว App ด้วย ว่าจะคุม Transactions เล็ก-ใหญ่ แค่ไหน ง
  • มีเคสที่เคยเจอ ตัว Core มีคนไปปรับ Transactions จาก Default Read Commit มาเป็น Serializable ผลที่ได้ Operation Read เละเลย เพราะ Table Lock กระจุย ต้องระวังด้วย
  • ถ้าลืมพวก Transaction ลองมาดู Blog เก่าผมได้ แนวข้อสอบเกี่ยวกับ Isolation level / [DB2] WORKING WITH ISOLATION LEVEL

ปัญหากลุ่ม - storage/memory

📌 Table ใหญ่มาก - Full Scan → Disk/CPU หน่วง

คำแนะนำ

📌 Too Few Prefetchers - ตัว Prefetcher ไม่ได้คาดการณ์ข้อมูล Page ที่ใช้ล่วงหน้า แล้วเตรียมของมาลง memory รอไว้ buffer pool อาการที่พบ ให้ CPU idle, Disk I/O หนัก

คำแนะนำ

  • ตรวจสอบ Execution Plan และ Update Stats
  • สำหรับ DB บางตัว มี Parameter กำหนดไว้อย่างตัว DB2 มี NUM_IOCLEANERS กำหนดจำนวน Thread ไปอ่านได้เลย หรือ pg_prewarm ของ PostgreSQL 

📌 Too Few Cleaners - Prefetchers มากไป จนเต็ม buffer pool / memory เกิดปัญหา memory pressure + disk flush burst

คำแนะนำ ตรวจก่อน แต่ละ DB จะวิธีการตรวจ ต้องมากำหนดให้ตัว DBMS Clear Page ออกไป แต่ละค่ายจะมี Config ต่างกันไปอีก

  • DB2 ปรับได้เยอะ NUM_IOCLEANERS / CHNGPGS_THRESH / LOGBUFSZ เป็นต้น
  • PostgreSQL - ปรับจากตัว bgwriter_lru_maxpages / checkpoint_completion_target / max_wal_size
  • MSSQL ไม่ค่อยจะมีให้ปรับ Prefetchers / Cleaners

📌Append Contention - หลาย transaction เขียนข้อมูล (INSERT/UPDATE) ไปที่ Page เดียวกัน/พร้อมกัน Disk bottleneck อาจจะเกิดการจอ PK

คำแนะนำ

  • ฝั่ง APP ลดการขอ Primary Key
    - พวก Max+1 (ที่ชอบเรียกกัน System Counter) เลวร้ายสุด - เขียนเอง เจอ Race Condition / Lock กันเละ
    - ใช้ Auto Increment ของ DB
    - หรือไปใช้ค่าที่ Random อย่างพวก GUID / UUID (v7 มี time-ordered แล้ว) แต่ UUID ต้องระวังด้วยนะ V4 จะเจอ Page Splits / Fragmentation ตอนทำ Index ใช้ UUID V7 จะแก้เรืองนี้ให้แล้ว

สำหรับ UUIDv7 Postgres, MySQL, DB2 ใช้งานได้เลย แต่ Microsoft SQL Server ต้องมาเลือก Data Type ก่อน ควรใช้ BINARY(16) ถ้าอยากใช้ UNIQUEIDENTIFIER มาทำ COMB GUID สลับเอาส่วน Timestamp 48 บิตแรก ย้ายไปไว้ที่ 6 Byte สุดท้าย

  • หรือมาปรับค่าของแต่ละ DB ผมสรุปสั้นๆๆไว้ตามนี้ เดี๋ยวถ้าว่างๆจะเขียน Blog แยก
TypeCheckFixed
MSSQLตรวจ sys.dm_exec_requests ดูจาก wait_type
กลุ่ม PAGELATCH
ปรับขนาด tempdb
DB2db2pd -db MYDB -logsปรับค่า เพิ่ม LOGBUFSZ / MINCOMMIT
PostgreSQLpg_stat_activity ดู WALWrite, WALInsertปรับค่า เพิ่ม wal_buffers

ปัญหากลุ่ม - HA ถ้าตั้งค่าไม่สอดคล้อง กระทบการทำงานของระบบได้

📌Over-Replication - กำหนดจำนวน Tx ที่จะ Replicate มาจนเกินไป ปลายทางรับไม่ได้ เลยกลายเป็นว่าตัว Tx Log ต้องถูก Hold จนกว่าปลายทางจะยืนยันว่าได้รับ ผลทำให้ Log file โตเร็ว, I/O สูง

📌Logging Delay - มันมาคู่ Over-Replication มี log เยอะ มันเกิด latency พุ่ง

คำแนะนำ

  • ปรับ polling interval ให้เหมาะสม / เลือก replicate ที่จำเป็น พวก Table Temp ไม่ต้องเอาก็ได้
  • ปรับ Config ให้เหมาะสม

Recap อีกที

ถ้าต้องเข้ามาแก้ไข ต้องทำอะไรบ้าง ลองมาดูกันครับ

  1. เอา System Architecture ออกมากางและทำความเข้าใจภาพรวม การเชื่อมโยงระบบต่างๆ
  2. ตรวจสอบ System Utilization (CPU , Memory Disk , Network )ของแต่ละจุด ถ้าระบบมีทำพวก Observability อย่างน้อยพวก log
  3. กรณีที่ ดูภาพรวมแล้ว เจอจุดที่ใช้ ทรัพยากร 100% ได้จริงๆ โอเค รอดตัวไป Bottleneck เห็นชัด ก็ไล่ดู ไล่แก้กันไปทีละจุด
  4. กรณีที่ ใช้ทรัพยากรได้แค่ 10-20% Lazy System ที่เกิดจากขาดประสบการณ์ ให้ความสำคัญ Non-Functional - Performance ของระบบตั้งแต่แรก ต้องมาดูอาจจะเกิดจากการไม่เข้าใน Framework / Data Flow หรือ หนักๆ ก็เป็นการ Coding มักง่าย ได้ BigO แย่
  5. ในการทำ Performance Tuning เราต้องพึ่งพา ผู้เชี่ยวชาญในแต่ละจุด ผู้ที่สามารถเข้าใจจุดนั้นๆ ได้อย่างถ่องแท้ เข้าใจว่าแต่ละส่วนทำหน้าที่อะไร มีความสัมพันธ์กันอย่างไร ไม่ใช่แค่นั่งถาม AI หรือ ไปอ่าน Stackoverflow มาตอบ

สุดท้ายการที่จะแก้ไขต้องมีคนทุกสาย ย้ำว่าทุกสาย ทั้งในส่วน Technical (Dev / DBA / Infra) และ Business สำหรับผมการแก้จะเอา Business มากางก่อน แล้วเทียบกับ Flow และถาม BA เลยตอนนั้นว่าใช้ไหม เพราะงานเคยเจองานบางอัน Process แทบตายมันก็ได้ค่า 0 แต่มันใช้เวลา Portfolio ละ 1 นาที ถ้าคุยแค่ Dev มันจะแบบใส Thread แยกกันคิด มัน overengineer ไปนะ แล้วเสี่ยงค่าเพี้ยนอีก ผมเดินไปคุยกับ Business เค้าบอกข้ามได้ แล้วเรามาหาจุด Trigger ที่จะข้าม เช่น ไม่ได้ map master a b c ลดเวลาได้เยอะ


Discover more from naiwaen@DebuggingSoft

Subscribe to get the latest posts sent to your email.