Cách mở rộng thông lượng PgBouncer lên gấp 4 lần
(clickhouse.com)- ClickHouse Managed Postgres vận hành mặc định một cụm nhiều tiến trình tỷ lệ với số lõi, nhằm tránh giới hạn PgBouncer đơn luồng bị bó vào một lõi CPU
- Tất cả tiến trình bind vào cùng một cổng bằng
so_reuseportvà kernel phân phối các kết nối mới, nên phía client thấy như một endpoint duy nhất - Các yêu cầu hủy của Postgres đi qua kết nối riêng có thể đến một tiến trình không giữ session, nên chúng được chuyển tiếp đến chủ sở hữu session thực sự bằng peering giữa các tiến trình
- Trên 16-vCPU
c7i.4xlarge, một tiến trình đơn đạt đỉnh khoảng 87.000 TPS, còn cụm 16 tiến trình tăng lên khoảng 336.000 TPS, ghi nhận thông lượng xấp xỉ gấp 4 lần - Khi ít kết nối, một tiến trình đơn có thể tương đương hoặc nhanh hơn đôi chút, nhưng ở mức đồng thời cao, một lõi sẽ trở thành nút thắt; vì vậy cần chia ngân sách kết nối trên toàn cụm để mở rộng thông lượng và giới hạn kết nối mà không kết nối quá mức tới Postgres
Nút thắt CPU do tiến trình đơn tạo ra
- PgBouncer là đơn luồng, nên bất kể hệ thống có bao nhiêu CPU, một tiến trình chỉ dùng một lõi
- Ngay cả trên máy chủ 16-vCPU, tác vụ connection pooling cũng tập trung vào một lõi, còn 15 lõi còn lại ở trạng thái nhàn rỗi
- Vì vậy pooler giới hạn thông lượng tổng thể trước khi năng lực xử lý của Postgres bị dùng hết
- ClickHouse Managed Postgres vận hành một cụm tiến trình xác định số tiến trình PgBouncer theo tỷ lệ với số lõi khả dụng
Phân phối kết nối bằng so_reuseport
- Tất cả tiến trình trong cụm bật
so_reuseportvà bind vào cùng một cổng - Kernel phân phối các kết nối đến cho từng tiến trình, nên client chỉ kết nối tới một endpoint và không cần biết có nhiều PgBouncer đang chạy phía sau
- Tài liệu PgBouncer cũng giới thiệu
so_reuseportnhư một cách tận dụng nhiều lõi bằng nhiều tiến trình đơn luồng
Peering giữa các tiến trình để bảo đảm hủy truy vấn
- Yêu cầu hủy của Postgres đi kèm khóa hủy qua một kết nối mới, tách biệt với kết nối của truy vấn đang chạy
- Khi dùng
so_reuseport, kernel có thể chuyển kết nối hủy mới tới một tiến trình khác, không phải tiến trình đang giữ session tương ứng- Nếu tiến trình nhận yêu cầu không biết truy vấn đó, lệnh hủy sẽ không được thực thi
- Khi các tiến trình PgBouncer peering với nhau, yêu cầu đến sai tiến trình có thể được chuyển tiếp tới tiến trình thực sự sở hữu session
- Nhờ đó, dù tiến trình nào nhận yêu cầu, việc hủy truy vấn vẫn hoạt động bình thường trên toàn cụm
Transaction pooling và ngân sách kết nối
- Pooling hoạt động ở chế độ transaction, và kết nối server được trả về pool ngay khi transaction commit
max_client_connvàmax_db_connectionsđược chia theo số tiến trình và cấp phát cho từng tiến trình- Việc chia nhỏ ngân sách kết nối giúp nâng giới hạn kết nối tổng hợp trong khi vẫn giữ tổng số kết nối của toàn cụm trong ngưỡng an toàn của Postgres
- Một tiến trình đơn sẽ từ chối các client mới vượt quá
max_client_conncủa chính nó với lỗi sauFATAL: no more connections allowed (max_client_conn)
Benchmark trên cùng phần cứng
- Hai cấu hình được đo trong cùng môi trường AWS EC2
- Máy chủ pooler: 16-vCPU
c7i.4xlarge - Postgres: máy chủ riêng
- Bộ tạo tải: dùng
pgbenchtrên máy chủ thứ ba - Workload: chỉ đọc (select-only), chế độ transaction pooling
- Máy chủ pooler: 16-vCPU
- Đối tượng so sánh là PgBouncer một tiến trình và cụm 16 tiến trình; loại instance, Postgres và workload đều giống nhau
- Tăng số kết nối client từ 8 lên 256 và đo thông lượng cùng mức sử dụng CPU của máy chủ 16 lõi
| Client | TPS một tiến trình | CPU máy chủ một tiến trình | TPS cụm | CPU máy chủ cụm |
|---|---|---|---|---|
| 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% |
Kết quả thông lượng và mức sử dụng CPU
- Tiến trình đơn đạt đỉnh khoảng 87.000 TPS rồi hiệu năng giảm khi tải tăng, ghi nhận khoảng 77.000 TPS ở 256 client
- Cụm 16 tiến trình tăng lên khoảng 336.000 TPS nhờ tận dụng nhiều lõi hơn, cho thông lượng cao hơn khoảng 4 lần so với tiến trình đơn
- Theo đo đạc bằng
pidstat, trong lúc chịu tải, một tiến trình PgBouncer đơn dùng gần kín một lõi với khoảng 97% CPU, nhưng mức sử dụng của toàn bộ máy chủ 16-vCPU vẫn dưới 10% - Cụm phân tán công việc trên toàn máy chủ và dùng khoảng 8 lõi; ngay cả khi Postgres và bộ tạo tải chạm giới hạn, vẫn còn dư địa bổ sung
- Trong phép đo duy trì 256 client, máy chủ tiến trình đơn liên tục dùng khoảng 9% CPU, còn máy chủ cụm dùng khoảng 52% CPU
- Trong phép đo bên ngoài của EC2 CloudWatch, CPUUtilization trung bình của instance tiến trình đơn khoảng 16%, còn cụm khoảng 60%
- Số liệu CloudWatch cao hơn đôi chút so với đo đạc bên trong guest, nhưng khác biệt rằng PgBouncer đơn không tận dụng được phần lớn 16-vCPU vẫn như nhau
Chọn cấu hình theo mức đồng thời
- Khi số kết nối ít, không có nhiều công việc để song song hóa và các kết nối của cụm bị phân tán qua nhiều tiến trình, nên một tiến trình đơn có thể đã đủ hoặc nhanh hơn đôi chút
- Với 8 client, tiến trình đơn đạt 8.910 TPS, còn cụm đạt 6.450 TPS
- Khi mức đồng thời tăng, một lõi mà tiến trình đơn chiếm giữ trở thành bức tường thông lượng, và chênh lệch hiệu năng so với cụm lớn dần
- Trong môi trường mà Postgres chạm giới hạn trước PgBouncer, một tiến trình đơn cũng là giá trị mặc định phù hợp
- Khi pooler bắt đầu giới hạn thông lượng, cần dùng cùng lúc cụm tiến trình theo số lõi, chia sẻ cổng dựa trên
so_reuseportvà peering giữa các tiến trình để loại bỏ nút thắt - Tất cả máy chủ ClickHouse Managed Postgres đều cung cấp cấu hình này làm mặc định
1 bình luận
Ý kiến trên Hacker News
Chỉ cần dùng https://github.com/yandex/odyssey. Odyssey là một PgBouncer có khả năng mở rộng
Cũng có nhiều khách hàng đã mở rộng lên hơn 10.000 kết nối tới PostgreSQL, và trong tương lai chúng tôi cũng sẽ xem xét các lựa chọn thay thế như Odyssey hay pgdog. Dù vậy, cá nhân tôi không thích cách để PostgreSQL có hơn 10.000 kết nối, và cho rằng chỉ vài trăm kết nối cũng đủ để mở rộng tốt
Khi vận hành PgBouncer trên Kubernetes thì khá dễ để chạy nhiều tiến trình trên một máy, đồng thời cũng đơn giản để phân tán ra nhiều máy
Vì Azure đôi khi gây sự cố tuần tự trên cả cụm máy chủ trong quá trình bảo trì VM, nên cấu hình nhiều máy đặc biệt hữu ích
Tôi hiểu khái niệm peering ở đây, nhưng chưa từng dùng nó với PostgreSQL
A) PostgreSQL có chế độ hay cấu hình nào để thiết lập việc này dễ dàng không? Tôi đang hình dung một cấu trúc trong đó yêu cầu hủy sẽ được chuyển tuần tự cho các peer cho tới khi không còn lỗi, hoặc yêu cầu hủy chứa metadata để tiến trình nhận nhầm có thể xác định đúng tiến trình cần tìm
B) Nếu mọi tiến trình PostgreSQL đều nhận yêu cầu client bằng
so_reuseportthì có vẻ việc giao tiếp giữa các peer sẽ cần một cơ chế IPC riêng; trên thực tế người ta dùng cơ chế nào?Có vẻ sẽ khá đơn giản để triển khai nếu đưa ID tiến trình PgBouncer vào khóa hủy
Có thể xem từ slide 26 trở đi: https://www.pgevents.ca/events/pgconfdev2024/sessions/sessio...
Cũng có video bài trình bày của Jelte, người bảo trì PgBouncer: https://www.youtube.com/watch?v=X-nCHcZ6vQU
Tôi đang dùng pgdog và nó rất phù hợp với nhu cầu của tôi
Tôi thắc mắc vì sao PgBouncer phải bận tâm cả yêu cầu hủy. Chẳng phải chỉ cần chuyển nguyên xi cho PostgreSQL, rồi nếu PostgreSQL trả về lỗi thay vì phản hồi bình thường cho truy vấn đã bị hủy thì PgBouncer đang phụ trách kết nối đó xử lý là được sao?
PgBouncer đưa cho client PID hủy và secret giả nhưng có thể theo dõi được; khi yêu cầu hủy quay lại, nó sẽ tra ra PID và secret thật của máy chủ/tiến trình để chuyển tiếp. Ngoài ra còn có trường hợp client tin rằng truy vấn vẫn đang chạy nhưng PgBouncer biết nó đã kết thúc và kết nối đã được tái sử dụng, nên nó cũng phải kiểm tra liệu kết nối và PID đó có còn đang thực thi chính truy vấn ấy hay không
Có phải đây là để kiểm soát truy cập vào máy chủ PostgreSQL thông qua connection pool trong kiến trúc microservice không? Nếu là backend nguyên khối thì có vẻ không cần thiết
Hầu hết các framework backend tử tế đều có sẵn connection pool, và 98% các trường hợp sử dụng không cần hoặc không nên dùng microservice đều được giải quyết bằng cách đó
PgBouncer là phần mềm rất xuất sắc. Tôi dùng nó rất nhiều và nó giúp vận hành cơ sở dữ liệu đơn giản hơn đáng kể
Tôi mới biết đến
so_reuseportvà thấy khá thú vị. Có vẻ trọng tâm của cấu hình này là nó cùng với peering; peering có được tích hợp sẵn trong PgBouncer và cấu hình đơn giản không?Có thể dùng peering cả trên Kubernetes không? Trong môi trường này có lẽ không cần tái sử dụng cổng, nên tôi cũng thắc mắc liệu mỗi Pod có một connection pool riêng và hoạt động độc lập hay không
Giờ tôi 46 tuổi, và vẫn nhớ lúc 23 tuổi đã bị sốc khi thấy mô hình kết nối nặng nề của PostgreSQL. Đến giờ vẫn chưa cải thiện sao?
Tuy vậy, nếu không dùng connection pool thì PostgreSQL vẫn phải fork tiến trình, nên việc tạo kết nối mới luôn tốn hàng chục mili giây trở lên. Ứng dụng được viết mà không có connection pool cũng không lý tưởng, nhưng lại khá phổ biến
Các framework ứng dụng cũng đã thay đổi, và kiến trúc serverless có thể tạo ra hàng chục nghìn kết nối, mức mà PostgreSQL bắt đầu gặp vấn đề. Cá nhân tôi không thích quá vài trăm kết nối, nhưng hiện nay đó là tình huống hoàn toàn thực tế
Trong quá trình đạt được tính tương thích với Java, nhiều công ty đã phải thay đổi rất mạnh cách xử lý đồng thời. PostgreSQL và SQLite cũng được thiết kế vào thời kỳ tương tự, nhưng đó là những hệ thống do các cựu binh trong ngành xây dựng, những người đã triển khai các hệ thống tải cao từ trước khi các tranh luận về threading xuất hiện, đồng thời vẫn phải hỗ trợ cả người dùng trên phần cứng cũ