เข้าใจ Connection Pool จนกระจ่างแจ้งดุจบ้านทรายทอง

Screen Shot 2559-02-24 at 07.24.42

หมายเหตุ เพื่อไม่ให้ภาษาวิบัติมากไปกว่านี้ที่ คำว่า “ควายหรี่” ผมจะกลับมาใช้แอคเซ่นส์ภาษาไทยว่า “คิวรี่” ละนะ

…ความเดิมหลังจากการหัดนูบให้คิวรี่ฐานข้อมูล เราจะอาจไม่ทันสังเกตุว่ามีเรื่องราวมากมายซ่อนอยู่หลังรั้วบ้านหลังใหญ่ที่ชื่อว่า “Connection Pool” แปลตรงๆ ก็เป็น “บ่อกักเก็บ” หรือ “ถังพักการเชื่อมต่อ” เป็นความวิเศษของแพคเกจ “database/sql” ที่พึ่งพาไดร์ฟเวอร์ของมันมาแอบจัดการกับการเชื่อมต่อ (connection) จากไคลเอ็นท์ให้เราโดยอัตโนมัติ แต่ทำไมเราต้องสนใจะมันด้วยละ ก็เรื่องราวมันน่าสนุกสนานชวนติดตามดั่งละครน้ำเน่าหลังข่าวบ้านทรายทองที่เราท่านล้วนชอบเสพย์ดราม่ากัน อยากรู้รึยัง เอ้า! ตามมา…

ในตอนเริ่มต้นที่เราเปิด sql.Open() เราได้ออปเจกต์ *sql.DB  กลับมา connection pool ซึ่งต่อจากนี้เราจะเรียกมันว่า “บ้าน pool” จะยังว่างๆ ไม่มีคนอยู่​ เพราะไอ้เจ้า connection ของ Go จะถูกสร้างสไตล์จอมขี้เกียจ (lazily) คือจะสร้างเมื่อถูกทวงถามเท่านั้น นิสัยเหมือนปกม.จอมขี้เกียจที่จะไม่ออกงานจนกว่าจะถูกทวงด้วย Slack อย่างนั้นละครับคุ้นๆไหม ทีนี้การที่เราจะเข้าใจการทำงานของบ้าน pool เนี่ยเป็นเรื่องคอขาดบาดตายเลยนะ เพราะมันมีผลกระทบกับพฤติกรรมของโปรแกรมเรามหาศาล

วิธีที่ pool ทำงาน

Conection pool ทำงานโดยใช้แนวคิดง่ายๆ สมมุติตัวแปรให้ *sql.DB มีชื่อเล่นว่า “คุณชาย d” ต้องการเข้าถึงฐานข้อมูลข้างใต้ออปเจ็กต์ *sql.DB และการเชื่อมต่อหรือ connection ชื่อเล่นว่าน้อง c นะ

ชาย d จะร้องขอ connection หรือน้อง c จากบ้าน pool ก่อน ถ้ามีว่างเหลืออยู่ (idle) ก็จ่ายน้องมาให้ 1 คน ถ้าไม่มีก็จะปั๊มเพิ่มให้ใหม่ ถึงตอนนี้น้อง c จะถูกจับจองเป็นเจ้าของหัวใจ (ownership)โดยชาย d  เมื่อเค้าคนนั้นเสร็จกิจสมฤดี เค้าก็จะทำอย่างใดอย่างหนึ่งระหว่างส่ง น้อง c กลับไปยังบ้าน poll หรือไม่ก็ส่งต่อความเป็นเจ้าข้าวเจ้าของให้กับ object อื่นไปเวียนใช้เยี่ยงทาสก่อนจะส่งกลับบ้าน poll แค่นี้แหละ

 

database-152940_960_720

ทีนี้มีเมธอด…เอ้ยนิสัยอะไรบ้างที่ คุณชาย d มีและชอบทำมิดีมิร้ายกับน้อง c ยังไงบ้าง

  • คุณชาย db.Ping() เมื่อสั่ง Ping ปุ๊บจะส่งตัวน้อง c คืน pool ทันทีหลังเสร็จกิจล่มปากอ่าวยังไม่ทันทำอันใด
  • คุณชาย db.Exec() เมื่อเสร็จกิจจะส่งตัวน้อง c กลับ pool ทันที, แต่จะมีการคืนค่าออปเจกต์ผลลัพธ์ (result) ที่ยังคงอ้างอิงแอบกุ๊กกิ๊กกับน้อง c อยู่ให้กับเราด้วย, ดังนั้นมันอาจถูกเราเรียกมาจิกหัวใช้เมื่อใดก็ได้เพื่อการตรวจสอบผลลัพธ์ของฟังค์ชั่น Exec()
  • คุณชาย db.Query() จะส่งต่อความเป็นเจ้าของ (ownership) ของน้อง c ไปยังออปเจ็ค sql.Rows, ซึ่งจะคืนน้องกลับไปยัง pool เมื่อคุณสั่งให้เวียนเทียนเสร็จกิจทุกแถวแล้ว หรือไม่ก็สั่ง .Close() สั่งตัดเยื่อใยขึ้นมากลางคันก็ได้
  • คุณชาย db.QueryRow() จะส่งน้อง c ให้กับ ลูกน้องชื่อ sql.Row, ซึ่งจะปล่อยเธอไปเมื่อเค้าสั่ง .Scan() เรือนร่างเธอเสร็จแล้ว
  • คุณชาย db.Begin() จะส่งน้อง c ให้กับคนขับรถชื่อ sql.Tx, ซึ่งจะปลดปล่อยเธอเมื่อมีการสั่ง .Commit() หรือ .Rollback() เท่านั้น

ตัวอย่างท่ามาตรฐานหลังเปิด sql.Open() เพื่อให้แน่ใจว่าต่อสายติดแล้วให้สั่ง db.Ping() ดังนี้

db, err := sql.Open("driverName", "dataSourceName")
if err != nil {
    log.Fatal(err)
}
defer db.Close()
err = db.Ping()
if err != nil {
    log.Fatal(err)
}

โน๊ตว่าบรรทัด log.Fatal(err) เมื่อเจอปัญหาคุณสามารถเปลี่ยนให้มันทำอย่างอื่นที่ฉลาดกว่านี้ได้เสมอนะ อันนี้ใส่ไว้แบบง่ายๆเท่านั้น

Consequence

ผลบุญจากการใช้บ้าน connection pool ก็คือ คุณไม่จำเป็นจะต้องคอยเช็คหรือรับมือจัดการกับการเชื่อมต่อที่อาจล้มเหลวขึ้นมากลางคันอีกเลย ตัว database/sql จะจัดการให้เราเอง โดยหลังบ้านมันจะแอบพยายามส่งเชื่อมต่อ  10 ครั้งหลังพบว่าการเชื่อมต่อหลุด แล้วมันก็เพียงแค่จะดึงน้อง c2 จากคนอื่นมาส่งให้ หรือปั๊มน้อง c3 ใหม่ออกมา นั่นหมายถึงว่าโค้ดของคุณจะสะอาดปราศจากโรค…เอ้อ…โลจิกการตรวจสอบคอนเนคชั่นน่ารำคาญ

การตั้งค่า Connection pool

  • db.SetMaxOpenConns(n int)

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

  • db.SetMaxIdleConns(n int)

    กำหนดจำนวน  connection ที่สามารถเปิดทิ้งไว้ว่างๆ (idle) หลังจากถูกปลดปล่อย ค่าดีฟอลท์คือ 0 ซึ่งหมายถึงตัวการเชื่อมต่อจะไม่ถูกเก็บไว้ใน pool เลย นี่อาจนำไปสู่สถานการณ์ที่มี connection จำนวนมากถูกปิดๆ เปิดๆ บ่อยๆอย่างรวดเร็ว ซึ่งอาจไม่ใช้แบบที่คุณต้องการก็ได้

กุญแจสำคัญที่ควรสังเกตุเกี่ยวกับ connection pool คือมันขึ้นอยู่กับว่าคุณจิกใช้น้อง c อย่างไร และคุณตั้งค่าบ้าน pool ไว้อย่างไร? ซึ่งมันอาจทำให้เกิดพฤติกรรมไม่พึงประสงค์บางอย่างได้เช่น

  1. มีน้อง c จำนวนมากโดนเฆี่ยน…ห๊ะ (Lots of connection thrash คืออัลลัย?) นำไปสู่อาการงานเข้าและอืดช้าเป็นเกลือ
  2. เปิด connection กับ database มากเกินไป นำไปสู่ error ได้มากมาย
  3. เกิดการ Block ในขณะที่รอการเชื่อมต่อ
  4. Operation สามารถล้มเหลวได้เมื่อ pool มีการเชื่อมต่อที่ล้มเหลวเกินกว่า 10 ครั้ง ซึ่งเป็นผลจากข้อจำกัดตั้งต้นที่ให้ลองต่อใหม่ไม่เกิน 10 ครั้ง

แต่ส่วนใหญ่ สิ่งที่มีอิทธิพลต่อพฤติกรรมจะเกิดจากว่าคุณจิกใช้ sql.DB อย่างไรมากกว่าว่าคุณตั้งค่า pool อย่างไร

ชีวิตในบ้าน pool ก็เรียบง่ายวุ่นวายเช่นนี้แล

อ้างอิงจาก “The Ultimate Guide to Building Database – Driven in Go” by VividCortex