วิธีขยายปริมาณงานของ PgBouncer ได้ 4 เท่า
(clickhouse.com)- ClickHouse Managed Postgres ใช้งานค่าเริ่มต้นเป็น ฟลีตหลายโปรเซส ที่ปรับตามจำนวนคอร์ เพื่อหลีกเลี่ยงข้อจำกัดที่ PgBouncer ซึ่งเป็นแบบเธรดเดียวจะถูกผูกอยู่กับ CPU เพียงคอร์เดียว
- ทุกโปรเซส bind กับพอร์ตเดียวกันด้วย
so_reuseportและให้เคอร์เนลกระจายการเชื่อมต่อใหม่ จึงมองเห็นเป็น เอนด์พอยต์เดียว สำหรับฝั่งไคลเอนต์ - คำขอยกเลิกของ Postgres ที่เข้ามาผ่านการเชื่อมต่อแยกอาจไปถึงโปรเซสที่ไม่ได้ถือเซสชันนั้นอยู่ จึงใช้ การ peering ระหว่างโปรเซส เพื่อส่งต่อไปยังเจ้าของเซสชันจริง
- บน
c7i.4xlargeแบบ 16-vCPU โปรเซสเดียวทำได้สูงสุดราว 87,000 TPS แต่ฟลีต 16 โปรเซสขึ้นไปได้ถึงประมาณ 336,000 TPS หรือราว 4 เท่าของปริมาณงาน - เมื่อมีการเชื่อมต่อน้อย โปรเซสเดียวอาจใกล้เคียงหรือเร็วกว่าเล็กน้อย แต่เมื่อมี concurrency สูง หนึ่งคอร์จะกลายเป็นคอขวด จึงควรกระจายงบการเชื่อมต่อไปทั้งฟลีตเพื่อขยายทั้งปริมาณงานและเพดานจำนวนการเชื่อมต่อ โดยไม่ทำให้ Postgres มีการเชื่อมต่อมากเกินไป
คอขวด CPU ที่เกิดจากโปรเซสเดียว
- PgBouncer เป็นแบบ single-threaded ดังนั้นไม่ว่าระบบจะมี CPU กี่ตัว โปรเซสหนึ่งก็ใช้ได้เพียงคอร์เดียว
- แม้บนเซิร์ฟเวอร์ 16-vCPU งาน connection pooling ก็จะไปกองอยู่ที่คอร์เดียว ส่วนอีก 15 คอร์แทบว่างอยู่
- ทำให้ตัว pooler กลายเป็นตัวจำกัดปริมาณงานทั้งหมดก่อนที่ขีดความสามารถของ Postgres จะถูกใช้หมด
- ClickHouse Managed Postgres ใช้งาน ฟลีตโปรเซส ที่กำหนดจำนวนโปรเซสตามจำนวนคอร์ที่มีอยู่
การกระจายการเชื่อมต่อด้วย so_reuseport
- ทุกโปรเซสในฟลีตเปิดใช้
so_reuseportและ bind กับ พอร์ตเดียวกัน - เคอร์เนลจะกระจายการเชื่อมต่อขาเข้าไปยังแต่ละโปรเซส ทำให้ไคลเอนต์เชื่อมต่อกับเอนด์พอยต์เดียวได้โดยไม่จำเป็นต้องรู้ว่ามี PgBouncer หลายตัวทำงานอยู่เบื้องหลัง
- เอกสารของ PgBouncer ก็แนะนำ
so_reuseportเป็นวิธีใช้หลายโปรเซสแบบเธรดเดียวเพื่อดึงประโยชน์จากหลายคอร์
การ peering ระหว่างโปรเซสเพื่อรับประกันการยกเลิกคิวรี
- คำขอยกเลิก ของ Postgres จะเข้ามาพร้อม cancel key ผ่านการเชื่อมต่อใหม่ที่แยกจากการเชื่อมต่อของคิวรีที่กำลังรันอยู่
- เมื่อใช้
so_reuseportเคอร์เนลอาจส่งการเชื่อมต่อสำหรับการยกเลิกใหม่นี้ไปยังโปรเซสอื่นที่ไม่ใช่เจ้าของเซสชันนั้น- หากโปรเซสที่รับคำขอไม่รู้จักคิวรีนั้น การยกเลิกก็จะไม่เกิดขึ้น
- หากให้โปรเซสของ PgBouncer ทำ peering กัน ก็จะสามารถส่งต่อคำขอที่มาถึงผิดโปรเซสไปยังโปรเซสเจ้าของเซสชันจริงได้
- ดังนั้นไม่ว่าคำขอจะเข้ามาที่โปรเซสใด การยกเลิกคิวรีก็ยังทำงานได้ถูกต้องทั้งฟลีต
Transaction pooling และงบการเชื่อมต่อ
- การ pooling ทำงานในโหมด transaction และเมื่อ commit ธุรกรรมแล้ว การเชื่อมต่อฝั่งเซิร์ฟเวอร์จะถูกคืนกลับสู่พูลทันที
max_client_connและmax_db_connectionsจะถูกหารตามจำนวนโปรเซสแล้วจัดสรรให้แต่ละโปรเซส- การแบ่งงบการเชื่อมต่อเช่นนี้ช่วยให้จำนวนการเชื่อมต่อรวมทั้งฟลีตยังอยู่ในขอบเขตที่ปลอดภัยของ Postgres ขณะเดียวกันก็เพิ่มเพดานจำนวนการเชื่อมต่อรวมได้
- โปรเซสเดียวจะปฏิเสธไคลเอนต์ใหม่เมื่อเกิน
max_client_connของตัวเอง พร้อมข้อความผิดพลาดต่อไปนี้FATAL: no more connections allowed (max_client_conn)
เบนช์มาร์กบนฮาร์ดแวร์เดียวกัน
- ทั้งสองคอนฟิกวัดผลบนสภาพแวดล้อม AWS EC2 เดียวกัน
- เซิร์ฟเวอร์ pooler: 16-vCPU
c7i.4xlarge - Postgres: รันบนเซิร์ฟเวอร์แยก
- ตัวสร้างโหลด: ใช้
pgbenchจากเซิร์ฟเวอร์เครื่องที่สาม - เวิร์กโหลด: อ่านอย่างเดียว (select-only), โหมด transaction pooling
- เซิร์ฟเวอร์ pooler: 16-vCPU
- สิ่งที่นำมาเปรียบเทียบคือ PgBouncer แบบ โปรเซสเดียว กับฟลีต 16 โปรเซส โดยใช้ instance type, Postgres และเวิร์กโหลดเหมือนกัน
- เพิ่มจำนวนการเชื่อมต่อของไคลเอนต์จาก 8 ถึง 256 แล้ววัดทั้งปริมาณงานและการใช้ CPU ของเซิร์ฟเวอร์ 16 คอร์
| ไคลเอนต์ | TPS โปรเซสเดียว | CPU เซิร์ฟเวอร์โปรเซสเดียว | TPS ฟลีต | CPU เซิร์ฟเวอร์ฟลีต |
|---|---|---|---|---|
| 8 | 8,910 | 0.8% | 6,450 | 2.9% |
| 32 | 54,203 | 5.2% | 64,244 | 12.3% |
| 64 | 86,570 | 8.3% | 219,439 | 31.9% |
| 128 | 83,463 | 8.1% | 320,547 | 45.9% |
| 256 | 76,893 | 7.7% | 336,469 | 48.9% |
ผลลัพธ์ด้านปริมาณงานและการใช้ CPU
- โปรเซสเดียวแตะจุดสูงสุดราว 87,000 TPS แล้วประสิทธิภาพลดลงเมื่อโหลดเพิ่มขึ้น โดยที่ไคลเอนต์ 256 รายทำได้ราว 77,000 TPS
- ฟลีต 16 โปรเซสเพิ่มขึ้นได้ถึงประมาณ 336,000 TPS ด้วยการใช้คอร์ได้มากขึ้น คิดเป็นปริมาณงานราว 4 เท่าเมื่อเทียบกับโปรเซสเดียว
- จากการวัดด้วย
pidstatโปรเซส PgBouncer เดี่ยวใช้ CPU ราว 97% ระหว่างรับโหลด หรือแทบเต็มหนึ่งคอร์ แต่การใช้รวมของทั้งเซิร์ฟเวอร์ 16-vCPU ยังต่ำกว่า 10% - ฟลีตกระจายงานไปทั่วทั้งเซิร์ฟเวอร์และใช้ประมาณ 8 คอร์ โดยยังมีเผื่ออยู่แม้ตอนที่ Postgres และตัวสร้างโหลดเริ่มถึงขีดจำกัด
- ในการวัดที่คงจำนวนไคลเอนต์ไว้ที่ 256 เซิร์ฟเวอร์แบบโปรเซสเดียวใช้ CPU ต่อเนื่องราว 9% ส่วนเซิร์ฟเวอร์แบบฟลีตใช้ราว 52% CPU
- การวัดจากภายนอกด้วย EC2 CloudWatch พบว่า CPUUtilization เฉลี่ยของอินสแตนซ์โปรเซสเดียวอยู่ที่ราว 16% ส่วนฟลีตอยู่ที่ราว 60%
- แม้ตัวเลขของ CloudWatch จะสูงกว่าการวัดภายใน guest เล็กน้อย แต่ข้อสรุปเรื่อง PgBouncer โปรเซสเดียวที่ใช้ประโยชน์จาก 16-vCPU ได้ไม่เต็มส่วนก็ยังเหมือนเดิม
การเลือกคอนฟิกตามระดับ concurrency
- เมื่อจำนวนการเชื่อมต่อน้อย จะยังไม่มีงานให้ขนานมากนัก และการเชื่อมต่อของฟลีตถูกกระจายไปหลายโปรเซส จึงอาจเป็นไปได้ว่า โปรเซสเดียวเพียงพอหรือเร็วกว่าเล็กน้อย
- ที่ไคลเอนต์ 8 ราย โปรเซสเดียวทำได้ 8,910 TPS ส่วนฟลีตทำได้ 6,450 TPS
- เมื่อ concurrency สูงขึ้น คอร์เดียวที่โปรเซสเดียวยึดอยู่จะกลายเป็นกำแพงของปริมาณงาน และช่องว่างด้านประสิทธิภาพเมื่อเทียบกับฟลีตก็จะยิ่งกว้างขึ้น
- ในสภาพแวดล้อมที่ Postgres ถึงขีดจำกัดก่อน PgBouncer โปรเซสเดียวก็ยังเป็นค่าเริ่มต้นที่เหมาะสมได้
- หาก pooler เริ่มเป็นตัวจำกัดปริมาณงาน ต้องใช้ทั้งฟลีตโปรเซสตามจำนวนคอร์ การแชร์พอร์ตด้วย
so_reuseportและการ peering ระหว่างโปรเซสร่วมกันเพื่อคลายคอขวด - เซิร์ฟเวอร์ ClickHouse Managed Postgres ทุกเครื่องให้คอนฟิกนี้มาเป็นค่าเริ่มต้น
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
ใช้ https://github.com/yandex/odyssey ก็ได้ Odyssey คือ PgBouncer ที่ขยายสเกลได้
มีลูกค้าจำนวนมากที่ขยายการเชื่อมต่อ PostgreSQL ไปถึง มากกว่า 10,000 รายการ และในอนาคตก็จะพิจารณาทางเลือกอย่าง Odyssey หรือ pgdog ด้วย อย่างไรก็ตามโดยส่วนตัวไม่ค่อยชอบแนวทางที่ให้ PostgreSQL มีการเชื่อมต่อเกิน 10,000 รายการ และคิดว่าแค่หลักร้อยก็เพียงพอสำหรับการขยายสเกลแล้ว
ถ้าใช้งาน PgBouncer บน Kubernetes การรันหลายโปรเซสบนเครื่องเดียวก็ทำได้ง่าย และการกระจายไปหลายเครื่องก็ง่ายเช่นกัน
Azure มักทำให้เกิดความขัดข้องแบบไล่ลำดับในทั้งกลุ่มเซิร์ฟเวอร์ระหว่างการบำรุงรักษา VM ดังนั้นการตั้งค่าหลายเครื่องจึงมีประโยชน์เป็นพิเศษ
ผมเข้าใจแนวคิดของ peering ที่พูดถึงตรงนี้ แต่ไม่เคยใช้กับ PostgreSQL
A) PostgreSQL มีโหมดหรือการตั้งค่าที่ทำให้กำหนดสิ่งนี้ได้ง่ายหรือไม่? ผมนึกภาพว่า cancel request จะถูกส่งต่อไปยัง peer ทีละตัวจนกว่าจะไม่เกิดข้อผิดพลาด หรือใน cancel request มี metadata ที่ทำให้โปรเซสที่รับผิดสามารถระบุโปรเซสที่ถูกต้องได้
B) ถ้าโปรเซส PostgreSQL ทุกตัวรับคำขอจากไคลเอนต์ด้วย
so_reuseportก็น่าจะต้องมีวิธี inter-process communication (IPC) แยกต่างหากสำหรับการสื่อสารระหว่าง peer แล้วในทางปฏิบัติใช้กลไกอะไร?ถ้าใส่ process ID ของ PgBouncer ไว้ใน cancel key ก็น่าจะทำได้ค่อนข้างง่าย
ดูได้ตั้งแต่สไลด์ที่ 26: https://www.pgevents.ca/events/pgconfdev2024/sessions/sessio...
มีวิดีโอการนำเสนอของ Jelte ผู้ดูแล PgBouncer ด้วย: https://www.youtube.com/watch?v=X-nCHcZ6vQU
กำลังใช้ pgdog อยู่ และมันเหมาะกับงานที่ต้องการมาก
สงสัยว่าทำไม PgBouncer ต้องใส่ใจกับ cancel request ด้วย แค่ส่งต่อไปยัง PostgreSQL ตามเดิม แล้วถ้า PostgreSQL ส่ง error แทน response ปกติสำหรับ query ที่ถูก cancel PgBouncer ที่ดูแล connection นั้นก็จัดการต่อไม่ได้หรือ?
PgBouncer จะให้ cancel PID และ secret value ปลอมแต่ติดตามได้แก่ไคลเอนต์ และเมื่อ cancel request กลับมา ก็จะค้นหา PID และ secret value ของเซิร์ฟเวอร์/โปรเซสจริงแล้วส่งต่อไป นอกจากนี้ยังมีสถานการณ์ที่ไคลเอนต์เชื่อว่า query กำลังรันอยู่ แต่ PgBouncer รู้ว่าเสร็จไปแล้วและ connection ถูกนำกลับมาใช้ซ้ำ ดังนั้นจึงต้องตรวจสอบว่า connection และ PID นั้นยังคงรัน query นั้นอยู่จริงหรือไม่
ใช้เพื่อควบคุมการเข้าถึงเซิร์ฟเวอร์ PostgreSQL ผ่าน connection pool ใน microservices ใช่ไหม? ถ้าเป็น monolithic backend ก็ดูไม่จำเป็น
backend framework ที่ดีส่วนใหญ่มี connection pool ในตัว และ 98% ของ use case ที่ไม่จำเป็นและไม่แนะนำให้ใช้ microservices ก็แก้ได้ด้วยสิ่งนี้
PgBouncer เป็นซอฟต์แวร์ที่ยอดเยี่ยม ใช้งานอยู่เยอะ และช่วยให้ การดูแลฐานข้อมูล ง่ายขึ้นมาก
เพิ่งรู้จัก
so_reuseportเป็นครั้งแรก น่าสนใจดี แกนหลักของการตั้งค่าน่าจะเป็นสิ่งนี้กับ peering แล้ว peering มีอยู่ใน PgBouncer ในตัวและตั้งค่าได้ง่ายไหม?ใช้ peering บน Kubernetes ได้ไหม? ในสภาพแวดล้อมนี้คงไม่จำเป็นต้อง reuse port และก็สงสัยว่าแต่ละ Pod จะมี connection pool แยกกันและทำงานอย่างอิสระหรือเปล่า
ตอนนี้อายุ 46 แล้ว ยังจำได้ว่าตอนอายุ 23 เคยช็อกกับ โมเดล connection ที่หนัก ของ PostgreSQL หลังจากนั้นยังไม่ดีขึ้นเลยหรือ?
แต่ถ้าไม่ใช้ connection pool PostgreSQL ต้อง fork โปรเซส ดังนั้นการสร้าง connection ใหม่จึงมีต้นทุนหลายสิบมิลลิวินาทีหรือมากกว่านั้นเสมอ แอปพลิเคชันที่เขียนโดยไม่ใช้ connection pool ก็ไม่ใช่แบบที่เหมาะนัก แต่ก็พบได้ทั่วไป
application framework ก็เปลี่ยนไปแล้ว และ serverless architecture สามารถสร้าง connection จำนวนหลายหมื่นรายการ ซึ่งเป็นระดับที่ PostgreSQL เริ่มมีปัญหา โดยส่วนตัวไม่ชอบ connection เกินหลักร้อย แต่ทุกวันนี้เป็นสถานการณ์ที่เกิดขึ้นได้จริงมาก
ในกระบวนการทำให้เข้ากันได้กับ Java หลายบริษัทต้องเปลี่ยนวิธีจัดการ concurrency ครั้งใหญ่ PostgreSQL และ SQLite ก็ถูกออกแบบในช่วงเวลาใกล้เคียงกัน แต่เป็นระบบที่สร้างโดยผู้คร่ำหวอดในอุตสาหกรรมที่ปรับใช้ระบบโหลดสูงมาตั้งแต่ก่อนจะมีการถกเถียงเรื่อง threading แบบนี้ พร้อมกับยังรองรับผู้ใช้ฮาร์ดแวร์เก่าด้วย