👦 Bottlenecks of Software

อยู่ๆแอพที่ทำก็ช้าเป็นเต่าเฉยเลย เกิดจากอะไรและแก้ไงดี ?

😢 ปัญหา

เชื่อว่าหลายๆคนที่ได้เขียนโปรแกรมที่ใช้จริงมาซักระดับนึงน่าจะเคยมีประสบการณ์ว่า แอพที่เคยทำงานได้รวดเร็วดุจสายฟ้า แต่อยู่มาก็ดันช้าเป็นเต่าซะงั้น พยายามเปลี่ยนโค้ดจุดนั้นนู้นนี่ก็แล้ว จัดการ database ก็แล้ว (ทำทุกๆอย่างให้เธอแล้ว) แต่ก็ยังเป็นเต่าอยู่ดี หรือบางทีก็อาการดีขึ้น แต่ซักพักก็วนกลายร่างเป็นเต่าเช่นเคย เฮ่อ...เหนื่อยใจ ตอนนี้เลยได้แต่พึ่งเจ้าพ่อนั่งจุดธูปเช้าเย็นหวังว่ามันจะไม่ล่มก็พอใจละ

จากปัญหาที่ว่ามามันเกิดจากอะไรได้บ้าง แล้วเราจะแก้ยังไงดี ?

😄 วิธีแก้ปัญหา

ออกตัวไว้ก่อนเลยว่า ไม่สามารถบอกวิธีการแก้ปัญหาแบบรวบรัดได้ เพราะการแก้ปัญหาในเรื่องนี้ มันต้องไปนั่งวิเคราะห์เป็นเคสๆเลย เพราะ Software & Hardware Architecture ของแต่ละเคสมันไม่เหมือนกันยังไงล่ะ เลยไม่มียาเทพที่กินเม็ดเดียวแล้วหาย ดังนั้นในบทความนี้จะชี้แนะว่า จะวิเคราะห์ปัญหานี้ทำยัง และ มันเกิดจากอะไรได้บ้าง ยังไงละ

🤔 ทำไมแอพถึงช้า ?

ก่อนเฉลยผมอยากให้เข้าใจตรงกันก่อนว่า ตอนที่โปรแกรมมันของเราทำงานเรื่องอะไรก็ตาม มันจะประมวลผลเป็นรูปแบบที่เรียกว่า Pipeline หรือทำงานกันเป็นทอดๆนั่นเอง เช่น เราสั่งให้มันไปคำนวณให้หน่อยดิ๊ว่านักเรียนแต่ละคนได้เกรดอะไร ซึ่งภายใน pipeline นั้นก็จะไปทำงานประมาณนี้

  1. ดึงรายชื่อนักเรียนจาก database

  2. คำนวณเกรดของนักเรียนแต่ละคน

  3. แสดงผลออกทาง View

สมมุติว่าแอพของเรามันช้า สิ่งที่เราต้องไปไล่ดูคือ

หาว่ามันช้าจากการทำงานเรื่องอะไรบ้าง แล้วค่อยไปไล่ดู Pipeline ของมันต่อ

จากที่ว่ามาทั้งหมด ประเด็นสำคัญที่สุดในการแก้ปัญหาเรื่องแอพหน่วงคือการหา คอขวด หรือ Bottleneck นั่นเอง ซึ่งเจ้า คอขวดนี้แหละมันจะซ่อนตัวอยู่ภายใน Pipeline ของเราอีกที

คอขวด หรือ Bottleneck ที่ซ่อนอยู่ภายใน Pipeline

การจัดการคอขวด เป็นหลักพื้นฐานในการทำ Scalable เลย ซึ่งเป็นจุดชี้เป็นชี้ตายว่าตัวโปรแกรมของเราจะรับโหลดได้สูงสุดเท่าไหร่ เช่นจะรับ concurrent user เป็นแสนๆต่อวินาทีได้หรือเปล่า บลาๆ

🤔 ทำไมต้องต้องไล่แก้คอขวดเพียงอย่างเดียว ?

หลายคนอาจจะสงสัยว่า ทำไมไม่ไปเขียนโค้ดให้มันทำ performance ดีๆ ไม่ก็จัดการเคลีย database จัด index, cleaning บลาๆ ไปเลยล่ะ จะไปไล่หาคอขวดมันทำไม?

คำตอบคือ สมมุติว่าเราไล่แก้โค้ดให้มันเทพทุกตัวจริงๆ หรือจัดการ database จนลื่นหัวแตกจริงๆ แต่คอขวดมันยังมีอยู่ สุดท้ายโดยรวมกันก็ช้าอยู่ดีนั่นแหละ ไม่เชื่อลองดูภาพปลากรอบจิ

จากภาพจะเห็นว่า ต่อให้เราทำ performance ที่จุดต่างๆที่เราคิดว่าควรทำหมดแล้ว แต่เราไม่ได้แก้ คอขวด สุดท้ายโดยรวมความเร็วที่ทำได้ทั้งหมดก็เท่ากับที่คอขวดทำได้อยู่ดีนั่นแหละ

🤔 อะไรบ้างที่ทำให้เกิดคอขวด ?

เยอะม๊วกกกกกกๆๆๆ แต่ขอลิสต์แค่ตัวหลักๆก่อนนะ ดูได้จากรายการด้านล่างเลย ซึ่งแค่เห็นก็ปวดตับละ

ชื่อ

ความหมาย

OS

ตัว Operating System แต่ละตัวก็จะเก่งในงานไม่เหมือนกัน

การจัดการแต่ละเรื่องก็ไม่เท่ากัน

Software

ตัวโปรแกรมต่างๆที่ติดตั้งไว้ในเซิฟเวอร์ก็มีผล เช่นลองติดตั้ง Antivirus เยอะๆเข้าไปดูจิ

Hardware

อุปกรณ์ต่างๆที่ใช้ เช่น ฮาร์ดดิส แบบ SSD ก็เห็นผลชัดแล้ว

Environments

สภาพแวดล้อมของเซิฟเวอร์ก็มีผลนะ ร้อนไปเย็นไป หรือ มีแอพติดตั้งในเซิฟเวอร์นั้นเป็นร้อยตัวดูดิ

Programming

ประสิทธิภาพของโค้ดที่เขียนให้กับโปรแกรมนั้นๆ

Database

การออกแบบและเลือกใช้ฐานข้อมูล

Network

การเชื่อมต่อต่างๆของเซิฟเวอร์ เช่นเน็ทกาก หรือแบ่ง subnet ไม่ดีใช้อุปกรณ์ไม่เหมาะสมไรงี้

Limitations

ขีดจำกัดทางสายเลือดบางประการ

จากรายการด้านบนผมสรุปสั้นๆว่า คอขวดไม่ได้เกิดจากแค่เรื่องเดียวเท่านั้น ดังนั้นการแก้คอขวดเลยไม่ใช่เรื่องที่จะมาแก้แค่โค้ดให้ทำงานได้เร็วๆ หรือไปแก้ที่ database ให้ไหลลื่นปูดปราด แล้วมันจะหายนั่นเอง!

แก้คอขวด 1 เรื่อง ไม่ได้หมายความว่าปัญหาจะหายไปเพราะมันอยู่กันเป็นฝูง

🤔 คอขวดเต็มไปหมดแล้วเอาไง ?

ในบทความนี้ผมจะลงรายละเอียดเพียงแค่ 3 เรื่องเท่านั้น ไม่งั้นมันจะไม่ได้อยู่ในหมวดความรู้พื้นฐานสำหรับมือใหม่ละ ฮ่าๆ ซึ่งทั้ง 3 เรื่องที่ว่านั้นคือ Programmin, Database และ Limitationsดังนั้นไปดูแต่ละเรื่องเลยว่ามันมีอะไรที่เกี่ยวกับมันบ้าง

🔥 Programming

ปัจจัยของการเขียนโค้ดแล้วทำให้เกิด คอขวด นั้นมีหลายอย่างเลย ซึ่งผมขอยกตัวอย่างเท่าที่นึกออกก่อนนะ

🔹 Algorithms and data structures

การเลือกใช้คำสั่งที่เหมาะสมนั้นมีผลสูงมากต่อ performance ของโปรแกรม ถ้าต้องทำในระยะยาว ดังนั้นเวลาที่เราจะทำ Refactor Code เพื่อทำ performance เราจะต้องเลือกใช้คำสั่งที่เหมาะสมกับงานนั้นๆด้วย

ตัวอย่าง สมมุติว่าผมต้องการให้โค้ดหาผลรวมตั้งแต่เลข 1 จนถึงเลขที่ผมใส่เข้าไป เช่น ผมใส่เลข 3 คำตอบคือ 6 เพราะเกิดจาก 1+2+3 แล้วผมเขียนโค้ดออกมาแบบนี้

var sum = 0;
for (int i = 1; i <= N; ++i)
{
sum += i;
}

แล้วลองเปรียบเทียบกับผมเขียนโค้ดแบบนี้

var sum = N * (1 + N) / 2;

แม้ว่าโค้ดทั้ง 2 แบบจะให้คำตอบที่ถูกได้ทั้งคู่ก็ตาม แต่ BigO ของทั้ง 2 ตัวต่างกันคนละโลกเลย (ตัวที่ 2 เร็วกว่า) หรือลองคิดแบบนี้ดูก็ได้ว่า ถ้าเราเลือกใช้ของที่ไม่เหมาะสมกับงาน มันก็เหมือนกับการเอาค้อนไปเลื่อยไม้นั่นแหละ ซึ่งมันก็อาจจะเลื่อยได้(มั้ง) แต่มันก็ไม่ได้เร็วเท่าที่มันควรจะเป็นยังไงล่ะ

🔹 Strategies

การคิดวิธีการรับมือของงานแต่ละแบบก็มีผลสูงมากนะ ผมขอยกตัวอย่างให้เห็นภาพตามนี้

ตัวอย่าง 1 ถ้าเราไปดึงรายการสินค้าที่มีเป็นแสนๆรายการมาแสดงผลในหน้าเดียวมันจะเกิดอะไรขึ้น? แอพอาจจะเด้งเลยก็ได้ อีกทั้งเสียเวลาทั้ง server ทั้ง bandwidth ที่ไปดาวโหลดไฟล์มาอีกด้วย การแก้ปัญหา แทนที่จะโหลดมาตูมเดียวเราก็แบ่งโหลดก็ได้นิ หรือเราเรียกว่าการทำ Paging

ตัวอย่าง 2 ถ้าโค้ดมีการสร้าง database connection ใหม่เรื่อยๆโดยไม่คืน resource เลยสุดท้าย database ก็ตายแอพก็ตายตามกันไป การแก้ปัญหา เปิด connection แล้วปิดด้วย หรือใช้พวกที่มีการจัดการ connection pooling ที่เหมาะสม

ตัวอย่าง 3 การทำงานต่างๆ ถ้ามันมีเวลาในการทำงานที่สูงมากก็อาจจะเกิดปัญหาภายหลังได้ การแก้ปัญหา พยายามให้ Roundtrip มันสั้นเข้าไว้

🔹 Language limitations

ในภาษาแต่ละภาษามันจะมีขีดจำกัดทางสายเลือดของมันอยู่ ดังนั้นเราก็ควรจะต้องไปศึกษาว่ามันมีข้อจำกัดอะไรบ้าง และจะจัดการกับของพวกนั้นยังไง ยกตัวอย่างเช่น

Recursive function - บางภาษาการเขียน recursive function ไม่ใช่เรื่องดีนัก แต่ในขณะเดียวกันบางภาษาจะเก่งกับการทำ recursive function เอามากๆ

Structural & Unstructured - บางภาษาเวลาที่เราจะใช้มันต้องสร้างโครงสร้างให้มันก่อนถึงจะทำงานได้ เช่น Class แต่ในขณะเดียวกันบางภาษาสามารถเรียกใช้งานโดยไม่ต้องสร้างโครงสร้างให้มันก็ได้ เช่น Javascript

🔹 Build & Compile level

บางภาษาพอเขียนเสร็จปุ๊ปก็เอาไปใช้งานได้เลย เช่น php แต่ก็มีอีกหลายๆภาษาที่ต้องทำการ compile เสียก่อนถึงจะใช้งานได้ เช่น C# ซึ่งในการ build ก็มีหลายระดับ เช่น debug, release ไรพวกนี้ ซึ่งประสิทธิภาพของพวกนี้ก็ไม่เหมือนกันด้วย

🔥 Database

ปัจจัยที่เกิด คอขวด ของ database ก็มีหลายอย่างเช่นกัน ขอยกตัวอย่างเท่าที่นึกออกเช่นกัน (เพราะอันที่นึกไม่ออกก็ไม่รู้จะเขียนไร ฮา)

🔹 Poor design

การออกแบบที่ไม่เหมาะสมนี่เจอบ่อยฝุดๆ เช่นกำหนดขนาดที่ไม่เหมาะสม varchar, nvarchar หรือไปกำหนดของให้มันเป็น text ทั้งๆที่มันไม่ควรเป็น text เลวร้ายกว่านั้นคือไปใช้ char แทน boolean ไรงี้ (พูดเรื่องพวกนี้แล้วปวดใจ) และรวมถึงการตั้งชื่อหัวตารางที่ไม่เหมาะสมด้วย N1, N2, N3, N4 ... เฮ่อเหนื่อยใจ

🔹 Normalization

การทำ normalization มากจนเกินไปบางทีก็ไม่ดีนะจ๊ะ และอย่าเมากาวเอาแต่ยึดติดกับการทำ normalize ด้วย เพราะเป้าหมายของ database ไม่ใช่การทำ normalize แต่เป็นการเก็บและรักษาข้อมูลได้ต่างหาก ซึ่งในบางทีเราอาจจะไม่ต้องทำ normalize ถึงระดับ 3 ก็ได้ ขึ้นอยู่กับความเหมาะสมของหน้างานที่เจอ ว่าคนเอาไปใช้ในเคสนั้นๆเหมาะกับแบบไหน

🔹 Redundancy

ตรงตัวเลยจะเก็บข้อมูลซ้ำๆกันไปทำไม ? แต่ถ้ามันจำเป็นที่จะต้องทำก็ทำไปเถอะ ขึ้นกับความเหมาะสมของหน้างาน

🔹 Bad Referential Integrity

การเชื่อมตารางแบบต่างๆ เช่น 1-1, 1-M และ M-M ไรพวกนี้ก็มีผลกับ performance นะ ซึ่งยิ่งเยอะ performance ยิ่งตก

🔹 Not Taking Advantage of DB Engine Features

ตัว database แต่ละตัวมันก็มีความสามารถที่ติดมากับมันอยู่แล้วนะ ลองไปศึกษาใช้งานมันดูบ้างก็จะทำ performance กลับมาได้เยอะพอตัวเลย เช่น การใช้ Views, Indexes, Stored procedures, Constraints, Triggers อะไรเทือกนี้

🔹 No limitations on table or column name size

คือการที่เราปล่อยให้เขามาดึงข้อมูลได้โดยไม่ใส่ข้อจำกัดอะไรเลย เช่นดึงไปทีเดียวเป็นล้าน records เลย แล้วถ้าเขาส่งมาขอแบบนี้รัวๆ database มันจะไม่ล่มได้ไง? ดังนั้นจัดการกำหนด limit มันบ้างซะ

🔹 One table to hold all domain values

เคยเจอตารางมหาเทพไหม ที่มีทุกอย่างอยู่ในตารางนั้นเลย ข้อมูลลูกค้า ข้อมูลสินค้า ที่อยู่จัดส่ง บลาๆ จะบ้าตาย แค่คิดยินนี่ก็ปวดหัวที่จะไปเขียน query ด้วยละ นี่ยังไม่รวมว่าถ้าจะไป maintenance มันด้วยนี่ .... เอิ่ม

🔹 Lack of testing

ตัว database เราทำเทสครั้งสุดท้ายเมื่อไหร่ ? แล้วจะรู้ได้ไงว่าที่ออกแบบมามันออกแบบได้เหมาะสมกับงานมันแล้ว ? หรือรอให้มันขึ้น production แล้วให้ user จริงมาเทส ?

ตัวอย่างที่จะเกิดคอขวดที่ database

  • เขียน query ที่ทำให้มันรอนานมากๆ เช่น ดึงข้อมูลจากตาราง A แล้วเอาไป Join กับตาราง B แล้วก็ intersect กับตาราง C .... Z ไรงี้ อย่าทำ เสียทรัพยากรเครื่องอันมีค่าโดยใช่เหตุ

  • ออกแบบกฏที่อาจจะมีปัญหา เช่น A reference B และ B ก็ reference A ส่วนถ้าจะลบ A ต้องลบ B ด้วย และถ้าจะลบ B ก็ต้องลบ A ด้วยไรงี้

🔹 Limitation

ตัว database เองก็มีขีดจำกัดทางสายเลือดของมันเหมือนกัน เช่น database บางตัวทำงานกับกับข้อมูลที่ไม่เกินระดับ GB เท่านั้น หรือบางตัวต้องออกแบบโครงสร้างก่อนถึงจะใช้งานได้ แต่บางตัวไม่ต้องมีโครงสร้างเลย บลาๆ ดังนั้นไปศึกษาให้ดีเสียก่อนที่จะเลือกใช้ database

🤔 เรื่องเยอะจุงขอสั้นๆได้ไหม ?

เอาง่ายๆนะแค่ทำตาม Design guideline & Best practices ของภาษาหรือ database ที่เราใช้เพียงแค่นี้ก็ช่วยได้เยอะแล้ว เพราะ 90% ของ Developer และ DBA บ้านเราไม่ยอมไปอ่านของพวกนี้กัน แล้วก็มาจับงานจริงเลยทำให้ คอขวด กระจายเต็มไปหมดเบย (ขอบคุณครับผมจะได้มีงานไปบรรยายเรื่องพวกนี้หากินต่อ ผมคิดในใจนะไม่ได้พูดออกมา ฮี่ๆ) ซึ่งในพวก guideline พวกนั้นก็จะแนะนำไว้หลายเรื่องเลย เช่น เรื่องของ database

  • Caching

  • Hot & Cold data

  • Vertical & Horizontal Scaling

  • Federation - Splitting into multiple DBs based on function

  • Sharding - Splitting one dataset up across multiple hosts

  • Moving some functionality to other types of DBs

😁 แถมของ database ให้ไหนๆก็ยาวละ

ตัว database ในโลกนี้ (ณ ตอนที่เขียนบทความนี้) มีทั้งหมด 2 ตระกูล หลักคือ

🔹 Relational database

ก็ที่เราใช้ๆกันมามีเส้นโยงยั้วเยี้ยนั่นแหละ ซึ่งของพวกนี้ต้องมีโครงสร้างก่อนถึงจะสามารถใช้งานได้ พูดง่ายๆต้องกำหนดก่อนว่าตารางนี้จะเก็บข้อมูลกี่ฟิล์อะไรบ้างและแต่ละฟิล์จะเป็นชนิดข้อมูลอะไร บลาๆ

🔹 Non-Relational database (NoSQL)

คือตรงข้ามกับตระกูลแรกเลย ซึ่งเจ้าตัวนี้จะเก็บข้อมูลได้ยืดหยุ่นและหลากหลายกว่า เพราะมันไม่ต้องประกาศโครงสร้างก่อนใช้งาน เช่น ตารางเดียวกันก็มีหลายโครงสร้างได้ และมันไม่ได้มีแค่แบบตารางเท่านั้น

ซึ่งเจ้าตระกูลนี้จะแตกย่อยออกเป็น 4 สายตามนี้เลย

ชื่อประเภท database

ตัวอย่าง product

Graph

neo4J, OrientDB, Titan

Key-Value store

Redis, Amazon DynamoDB

Document database

MongoDB, Couchbase

Column store

Apache HBase, Cassandra

ตัวสีน้ำเงินสามารถกดไปลองเล่นได้นะ ส่วนรายละเอียดแต่ละตัวเป็นยังไง เดี๋ยวว่างๆจะมาเขียนให้ละกัน ลองติดตามดูได้จาก side menu ละกัน

🎯 บทสรุป

การที่โปรแกรมมันช้านั้นเพราะมันมี คอขวด ในระบบเกิดขึ้น ซึ่งมันจะแอบซ่อนอยู่ภายใน Pipeline ของระบบ โดยเจ้าคอขวดมันมากับผองเพื่อนของมัน ดังนั้นถ้าเราจะเอามันออกเราต้องไปไล่ดูว่ามีจุดไหนที่เป็นขอควดแล้วไปไล่เก็บผองเพื่อนที่ทำให้ระบบเราช้า เพียงเท่านี้ระบบเราก็จะกลับมาเร็วขึ้นแล้ว

เกร็ดความรู้ ของทุกอย่างที่เรียนที่รู้มา อย่ายึดมั่นถือมั่นเมากาวตะบี้ตะบันบังคับให้มันเป็นไปตามที่ได้เรียนมา เพราะของทุกอย่างมีดีมีเสียเสมอ ดังนั้นให้เลือกทำให้เหมาะสมกับหน้างานที่เกิดขึ้นด้วย