Hiệu năng bị tác động thế nào bởi các kết nối PostgreSQL không hoạt động

Bài đăng đầu tiên trong loạt bài viết này, Tài nguyên tiêu thụ bởi các kết nối PostgreSQL không hoạt động, đã nói về cách PostgreSQL quản lý các kết nối và cách mà ngay cả các kết nối không hoạt động cũng tiêu thụ bộ nhớ và CPU. Trong bài đăng này, tôi sẽ thảo luận về cách mà các kết nối không hoạt động ảnh hưởng đến hiệu suất của PostgreSQL.

Tác động đến tỷ lệ giao dịch

Khi PostgreSQL cần dữ liệu, nó trước tiên tìm kiếm trang cần thiết trong bộ đệm chia sẻ của nó. Nếu nó không thể tìm thấy trang trong bộ đệm chia sẻ, nó sẽ lấy trang từ bộ đệm của hệ điều hành (OS), nếu có sẵn. Nếu trang không có sẵn trong bộ đệm của hệ điều hành, nó sẽ được đọc từ ổ đĩa lưu trữ. Việc tìm kiếm trang từ bộ đệm chia sẻ là nhanh nhất, tiếp đó là tìm kiếm bộ đệm của hệ điều hành. Việc lấy trang từ ổ đĩa lưu trữ là chậm nhất.

Khi số lượng kết nối PostgreSQL tăng lên, bộ nhớ khả dụng có sẵn cho bộ đệm OS giảm xuống. Điều này làm cho hệ điều hành loại bỏ các trang khỏi bộ đệm. Việc tìm kiếm các trang này lần kế tiếp sẽ dẫn đến việc đọc từ ổ đĩa lưu trữ và do đó sẽ chậm hơn.

Nếu trường hợp bộ nhớ khả dụng trong instance thấp, nó sẽ bắt đầu sử dụng không gian trao đổi (swap space), một lần nữa nằm trên ổ đĩa lưu trữ và do đó là chậm. Sử dụng không gian trao đổi giúp giải phóng một số bộ nhớ, nhưng nếu các trang đã bị trao đổi lại cần thiết bởi hệ điều hành, chúng phải được đọc lại, dẫn đến tăng sử dụng I/O. Để biết thêm thông tin, xem Quản lý trao đổi.

Tác động của bộ nhớ khả dụng đến hiệu suất phụ thuộc vào khối lượng công việc, kích thước bộ dữ liệu làm việc và tổng bộ nhớ có sẵn. Nếu kích thước bộ dữ liệu làm việc nhỏ hơn tổng bộ nhớ có sẵn, việc giảm bộ nhớ khả dụng sẽ không có bất kỳ tác động đáng kể nào. Tuy nhiên, nếu kích thước bộ dữ liệu làm việc lớn hơn bộ nhớ có sẵn, tác động sẽ rõ rệt.

Kiểm tra hiệu suất
Các phần sau đây sẽ trình bày kết quả kiểm tra hiệu suất được tạo ra bằng cách sử dụng công cụ đo hiệu suất đơn giản của PostgreSQL là pgbench. Trong các kiểm tra này, chúng tôi sử dụng instance loại db.m5.large trên Amazon RDS cho PostgreSQL, có 2 vCPU và 8GB bộ nhớ. Ổ đĩa EBS loại IO1 được sử dụng với 3000 IOPS.

Mỗi kiểm tra bao gồm hai giai đoạn. Trong giai đoạn đầu tiên, pgbench được chạy trong 600 giây trên máy chủ Amazon RDS cho PostgreSQL mà không có lưu lượng truy cập nào khác trên cơ sở dữ liệu. Điều này cung cấp một tỷ lệ giao dịch cơ sở.

Trong giai đoạn thứ hai, 1.000 kết nối đã được mở trước khi chạy lại pgbench. Mỗi trong số 1.000 kết nối này đã lấy một hàng từ các bảng trong schema nội bộ của PostgreSQL có tên là information_schema. Sau đây là các bước thực hiện đối với mỗi trong số 1.000 kết nối này:

  1. Mở một kết nối.
  2. Tìm nạp tên của tất cả các bảng và dạng xem trong information_schema :
SELECT table_schema||'.'||table_name as relname from information_schema.tables WHERE table_schema='information_schema';
  1. Trong một vòng lặp, hãy chạy chọn trên từng bảng này với LIMIT 1 :
SELECT * FROM information_schema.columns LIMIT 1;
  1. Lặp lại các bước cho tất cả 1.000 kết nối.
  2. Để các kết nối ở trạng thái không hoạt động.

Khi bạn khởi động lại instance, nó không có bất kỳ trang nào được lưu trong bộ nhớ cache. Lần đầu tiên bạn chạy pgbench, nó sẽ tải các trang cần thiết từ đĩa lưu trữ. Các lần chạy pgbench tiếp theo có một số trang đã có sẵn trong cache nên chúng có thể sử dụng chúng thay vì tải từ lưu trữ.

Để giảm thiểu tác động của việc lưu trữ các trang, một bước khởi tạo được thực hiện, trong đó pgbench được chạy trước khi bắt đầu thực hiện kiểm tra thực tế. Kết quả của lần chạy ban đầu này không được sử dụng trong bất kỳ phân tích nào. Điều này đảm bảo rằng bất kỳ trang chung nào có thể được lưu trong cache đã có sẵn cho hai lần chạy kiểm tra.

Biểu đồ sau cho thấy cách bộ nhớ của instance giảm từ khoảng 4,88 GB xuống còn 90 MB khi 1.000 kết nối được mở ra.

Như đã giải thích trong bài trước của loạt bài này, mặc dù các kết nối này không hoạt động, nhưng chúng tiêu thụ bộ nhớ và tài nguyên CPU. Kết quả cho thấy có tác động đến hiệu suất do các kết nối không hoạt động này.

Transaction rate test # 1: Standard pgbench

Trong thử nghiệm đầu tiên, pgbench chạy ở cấu hình tiêu chuẩn với 100 kết nối máy khách. Đoạn mã sau là kết quả cho lần chạy pgbench này:

transaction type: <builtin: TPC-B (sort of)>
scaling factor: 1000
query mode: simple
number of clients: 100
number of threads: 2
duration: 600 s
number of transactions actually processed: 749572
latency average = 80.058 ms
tps = 1249.096708 (including connections establishing)
tps = 1249.116996 (excluding connections establishing)

Với 1.000 kết nối không hoạt động, kết quả thay đổi như sau:

transaction type: <builtin: TPC-B (sort of)>
scaling factor: 1000
query mode: simple
number of clients: 100
number of threads: 2
duration: 600 s
number of transactions actually processed: 684434
latency average = 87.686 ms
tps = 1140.430155 (including connections establishing)
tps = 1140.449899 (excluding connections establishing)

Kết quả cho thấy tốc độ giao dịch giảm từ 1.249 xuống 1.140 giao dịch mỗi giây. Đây là mức giảm 8,7% trong tỷ lệ giao dịch do các kết nối không hoạt động.

Transaction rate test # 2: Select-only pgbench

Bởi vì bộ nhớ được sử dụng bởi các kết nối không hoạt động nên làm giảm bộ nhớ có sẵn cho bộ đệm trang, tác động của các kết nối không hoạt động này dự kiến sẽ rõ ràng hơn đối với các truy vấn đọc. Để kiểm tra điều này, tôi đã chạy pgbench bằng cách sử dụng cấu hình -S, chạy một tập lệnh chỉ chọn tích hợp cho phép đo điểm chuẩn. Kết quả đầu ra sau đây là kết quả cho lần chạy thử nghiệm này:

transaction type: <builtin: select only>
scaling factor: 1000
query mode: simple
number of clients: 100
number of threads: 2
duration: 600 s
number of transactions actually processed: 1181937
latency average = 50.778 ms
tps = 1969.344251 (including connections establishing)
tps = 1969.377751 (excluding connections establishing)

Với 1.000 kết nối không hoạt động, kết quả thay đổi như sau:

transaction type: <builtin: select only>
scaling factor: 1000
query mode: simple
number of clients: 100
number of threads: 2
duration: 600 s
number of transactions actually processed: 966656
latency average = 62.095 ms
tps = 1610.440842 (including connections establishing)
tps = 1610.470585 (excluding connections establishing)

Kết quả cho thấy tỷ lệ giao dịch giảm từ 1.969 xuống 1.610 giao dịch mỗi giây (giảm 18,2%).

Kiểm tra tỷ lệ giao dịch # 3: Custom pgbench

Để xem tác động của các kết nối không hoạt động này đối với khối lượng công việc cần đọc nhiều, tôi đã thực hiện một thử nghiệm khác với truy vấn pgbench tùy chỉnh đang đọc một số lượng lớn hàng. Đoạn mã sau là nội dung của tập lệnh pgbench tùy chỉnh chạy bằng pgbench:

\set nbranches :scale

\set naccounts 100000 * :scale

\set aid random(1, :naccounts)

\set bid random(1, :nbranches)

BEGIN;

SELECT * FROM pgbench_accounts WHERE aid >= :aid AND aid < (:aid + 5000) AND bid=:bid LIMIT 1;

END;

Mỗi giao dịch của tập lệnh pgbench tùy chỉnh này đọc 5.000 hàng từ bảng pgbench_accounts và chỉ trả về một trong các hàng này. Ý tưởng đó là làm cho mỗi giao dịch đọc được nhiều trang hơn.

Pgbench chạy với tập lệnh tùy chỉnh và không có bất kỳ kết nối nhàn rỗi nào cho thấy kết quả sau:

transaction type: pgbench_script.sql

scaling factor: 5000

query mode: simple

number of clients: 100

number of threads: 2

duration: 600 s

number of transactions actually processed: 227484

latency average = 264.140 ms

tps = 378.586790 (including connections establishing)

tps = 378.592772 (excluding connections establishing)

Với 1.000 kết nối không hoạt động chạy các truy vấn dựa trên lược đồ thông tin, kết quả đã thay đổi thành như sau:

transaction type: pgbench_script.sql

scaling factor: 5000

query mode: simple

number of clients: 100

number of threads: 2

duration: 600 s

number of transactions actually processed: 124114

latency average = 484.485 ms

tps = 206.404854 (including connections establishing)

tps = 206.507645 (excluding connections establishing)

Kết quả cho thấy tốc độ giao dịch giảm từ 378 xuống 206 giao dịch mỗi giây (giảm 46%).

Bạn có thể sử dụng Amazon RDS Performance Insights để xem chi tiết các sự kiện chờ động cơ trong hai lần chạy pgbench. Hình sau cho thấy rằng phần lớn thời gian được dành cho sự kiện đợi DataFileRead trong cả hai lần chạy. Trong lần chạy thứ hai với 1.000 kết nối không hoạt động, hầu như toàn bộ thời gian được sử dụng bởi sự kiện chờ này. Sự kiện DataFileRead làm nổi bật việc chờ đọc từ tệp dữ liệu quan hệ.

Biểu đồ sau đây cho thấy chỉ số thông lượng đọc của Amazon CloudWatch cho hai lần chạy pgbench.

Trong lần chạy đầu tiên, tỷ lệ đọc đạt khoảng 87 MB/s, trong khi trong lần chạy thứ hai với 1.000 kết nối mở, tỷ lệ đọc tăng lên khoảng 117 MB/s. Điều này xảy ra vì các kết nối không hoạt động đã tiêu thụ bộ nhớ OS miễn phí dẫn đến bộ nhớ cache OS nhỏ hơn. Vì vậy, cần phải tải thêm nhiều trang hơn từ đĩa lưu trữ thay vì đọc từ cache OS. Vì đọc từ đĩa lưu trữ chậm hơn nhiều so với đọc từ bộ nhớ, nó dẫn đến sự suy giảm hiệu suất truy vấn.

Connection poolers
Các connection pooler giúp giảm thiểu tác động của kết nối cơ sở dữ liệu bằng cách duy trì một pool kết nối có thể được chia sẻ cho tất cả các kết nối khách hàng. Bạn có thể sử dụng một connection pooler ứng dụng là một phần của mã ứng dụng của bạn hoặc một connection pooler độc lập như pgbouncer hoặc Amazon RDS Proxy. Những connection pooler này giúp giới hạn số lượng kết nối bạn có thể mở và tái sử dụng các kết nối để mỗi kết nối khách hàng mới không cần phải mở kết nối cơ sở dữ liệu mới. Các phần sau cung cấp thông tin chi tiết hơn về pgbouncer và RDS Proxy.

pgbouncer
pgbouncer là một connection pooler nhẹ cho phép ba chế độ sau:

  • Session mode – Mỗi kết nối ứng dụng được gắn với một kết nối cơ sở dữ liệu. Nếu kết nối chuyển sang trạng thái không hoạt động, pgbouncer không thể sử dụng lại nó cho các kết nối ứng dụng khác.
  • Transaction mode – pgbouncer có thể sử dụng lại kết nối đang mở ngay sau khi kết nối ứng dụng hoàn tất giao dịch.
  • Statement mode – Kết nối có thể được sử dụng lại cho các máy khách khác ngay sau khi một câu lệnh SQL được hoàn thành.

Trong hầu hết các ứng dụng, sử dụng pooling chế độ giao dịch sẽ đem lại kết quả tốt nhất. Để kiểm tra lợi ích của connection pooler, tôi đã cài đặt pgbouncer trên máy chủ chạy pgbench. pgbouncer được cấu hình để cho phép tối đa 5.000 kết nối khách hàng nhưng chỉ mở tối đa 200 kết nối đến thí nghiệm RDS PostgreSQL của chúng tôi. Sau đó, pgbench được chạy chống lại pooler pgbouncer này.

Đoạn mã sau đây là kết quả thử nghiệm của pgbench tùy chỉnh chạy với pgbouncer:

transaction type: pgbench_script.sql

scaling factor: 5000

query mode: simple

number of clients: 100

number of threads: 2

duration: 600 s

number of transactions actually processed: 227064

latency average = 264.600 ms

tps = 377.928241 (including connections establishing)

tps = 377.928476 (excluding connections establishing)

Trong khi quá trình chạy pgbench đang diễn ra, bạn có thể nhìn vào nhóm pgbouncer để xem trạng thái kết nối:

pgbouncer=# show pools;

-[ RECORD 1 ]-----------

database   | pgbench

user       | postgres

cl_active  | 100

cl_waiting | 0

sv_active  | 100

sv_idle    | 0

sv_used    | 0

sv_tested  | 0

sv_login   | 0

maxwait    | 0

maxwait_us | 0

pool_mode  | transaction

Đầu ra trạng thái nhóm này cho thấy rằng có 100 kết nối máy khách (cl_active) dẫn đến 100 kết nối máy chủ hoạt động (sv_active).

Trong lần chạy thử nghiệm thứ hai, tôi đã mở 1.000 kết nối và để chúng ở trạng thái nhàn rỗi. Trình tổng hợp không cần duy trì bất kỳ kết nối máy chủ nào đối với các kết nối máy khách không hoạt động này. Thống kê nhóm sau cho thấy có 1.000 kết nối máy khách nhưng chỉ có một kết nối PostgreSQL nhàn rỗi:

pgbouncer=# show pools;

-[ RECORD 1 ]-----------

database   | pgbench

user       | postgres

cl_active  | 1000

cl_waiting | 0

sv_active  | 0

sv_idle    | 1

sv_used    | 0

sv_tested  | 0

sv_login   | 0

maxwait    | 0

maxwait_us | 0

pool_mode  | transaction

Trình đánh cắp kết nối có các cài đặt khác nhau để kiểm soát cách chúng hoạt động. Để biết thêm thông tin, hãy xem pgbouncer Configuration.

Với 1.000 kết nối máy khách nhàn rỗi này vẫn mở, việc chạy pgbench bằng pgbouncer sẽ dẫn đến kết quả như sau:

transaction type: pgbench_script.sql

scaling factor: 5000

query mode: simple

number of clients: 100

number of threads: 2

duration: 600 s

number of transactions actually processed: 226827

latency average = 264.935 ms

tps = 377.451418 (including connections establishing)

tps = 377.451655 (excluding connections establishing)

Điều này cho thấy rằng khi sử dụng bộ gộp kết nối, không có tác động đến hiệu suất khi có hoặc không có 1.000 kết nối không hoạt động. Pgbouncer hiển thị trạng thái sau trong khi pgbench đang diễn ra:

pgbouncer=# show pools;

-[ RECORD 1 ]-----------

database   | pgbench

user       | postgres

cl_active  | 1100

cl_waiting | 0

sv_active  | 100

sv_idle    | 0

sv_used    | 0

sv_tested  | 0

sv_login   | 0

maxwait    | 0

maxwait_us | 0

pool_mode  | transaction

Có tổng số 1.100 kết nối máy khách (cl_active) nhưng chỉ có 100 kết nối máy chủ (sv_active) đang được sử dụng.

Trong thử nghiệm này, cá thể RDS chỉ có 2 vCPU, do đó 100 quy trình chạy song song có thể dẫn đến rất nhiều chuyển đổi ngữ cảnh, gây ra một số suy giảm hiệu suất. Tùy thuộc vào khối lượng công việc, bạn có thể hưởng lợi từ việc giảm số lượng kết nối tối đa xuống một số lượng nhỏ hơn. Đoạn mã sau là kết quả kiểm tra với pgbouncer được định cấu hình để cho phép tối đa 20 kết nối cơ sở dữ liệu:

transaction type: pgbench_script.sql

scaling factor: 5000

query mode: simple

number of clients: 100

number of threads: 2

duration: 600 s

number of transactions actually processed: 256267

latency average = 234.286 ms

tps = 426.828543 (including connections establishing)

tps = 426.828801 (excluding connections establishing)

Bạn nhận được tỷ lệ giao dịch cao hơn với kích thước nhóm kết nối nhỏ hơn. pgbouncer hiển thị trạng thái sau trong khi pgbench đang diễn ra:

pgbouncer=# show pools;

-[ RECORD 1 ]-----------

database   | pgbench

user       | postgres

cl_active  | 20

cl_waiting | 80

sv_active  | 20

sv_idle    | 0

sv_used    | 0

sv_tested  | 0

sv_login   | 0

maxwait    | 0

maxwait_us | 125884

pool_mode  | transaction

Tại bất kỳ thời điểm nào, chỉ có 20 kết nối khách hàng (cl_active) được kích hoạt. 80 kết nối còn lại (cl_waiting) đợi lượt của mình và sẽ được gán kết nối ngay khi một trong các kết nối cơ sở dữ liệu có sẵn. Kết quả này cho thấy rằng, số lượng kết nối nhiều không đồng nghĩa với khả năng xử lý nhiều hơn. Số lượng kết nối cơ sở dữ liệu nhỏ hơn này giúp giảm thiểu sự chuyển đổi bối cảnh và xung đột tài nguyên, và do đó cải thiện hiệu suất tổng thể cho khối lượng công việc tùy chỉnh này. Điều này đúng chung cho các cơ sở dữ liệu khác cũng

RDS Proxy

Như đã thể hiện trong phần trước, việc sử dụng công cụ quản lý kết nối có thể giúp cải thiện hiệu suất. Tùy thuộc vào khối lượng công việc và tài nguyên máy chủ có sẵn, cải tiến hiệu suất có thể đáng kể nếu sử dụng công cụ quản lý kết nối để xử lý một số lượng lớn kết nối.

Tuy nhiên, vì tất cả các kết nối hiện nay đều thông qua công cụ quản lý kết nối, nó có thể trở thành một điểm thất bại duy nhất. Việc đảm bảo bạn thiết lập nó cho tính sẵn sàng cao là rất quan trọng. Điều này có thể khó khăn. AWS giải quyết vấn đề này bằng cách cung cấp một proxy cơ sở dữ liệu hoàn toàn quản lý, có tính sẵn sàng cao: RDS Proxy.

RDS Proxy cho phép bạn thiết lập và quản lý bộ kết nối với chỉ vài cú nhấp chuột. RDS Proxy cho phép bạn đặt ngưỡng tối đa cho số lượng kết nối mở đến cơ sở dữ liệu. Tất cả các kết nối khách hàng sau đó sử dụng bộ kết nối này. Điều này giúp giảm thiểu các kết nối không hoạt động và đảm bảo rằng bất kỳ sự gia tăng đột ngột nào về số lượng kết nối cũng không làm giảm hiệu suất cơ sở dữ liệu.

Hình sau đây thể hiện thống kê sử dụng RDS Proxy cho một lần chạy pgbench mở 100 kết nối khách hàng. Trong bài kiểm tra này, RDS Proxy được cấu hình để chỉ cho phép 1% số kết nối tối đa được phép bởi cơ sở dữ liệu.

Trong một bài kiểm tra khác, đã mở 1.000 kết nối chờ trước khi bắt đầu chạy pgbench. Hình sau cho thấy chỉ có hai kết nối cơ sở dữ liệu đã được mở bởi RDS Proxy được sử dụng cho 1.000 kết nối mới đang chờ. Khi chạy pgbench bắt đầu, RDS Proxy mở thêm các kết nối mới.

RDS Proxy cũng cung cấp tùy chọn để định cấu hình thời gian chờ kết nối máy khách nhàn rỗi. Theo mặc định, proxy sẽ đóng bất kỳ kết nối máy khách nào không hoạt động trong 30 phút. Bạn có thể thay đổi tham số này theo yêu cầu khối lượng công việc của mình. Để biết thêm thông tin, hãy xem Tạo Proxy RDS.

Tóm tắt

Kết quả trong bài viết này cho thấy số lượng kết nối cơ sở dữ liệu cao hơn không đồng nghĩa với khả năng xử lý cao hơn. Khi bạn tăng số lượng kết nối cơ sở dữ liệu, sự chuyển đổi ngữ cảnh và tranh chấp tài nguyên cũng tăng, gây ảnh hưởng đến hiệu suất.

Bài viết cũng chỉ ra rằng kết nối PostgreSQL tiêu thụ tài nguyên ngay cả khi chúng không hoạt động, vì vậy giả thuyết thông thường rằng kết nối không hoạt động không có bất kỳ ảnh hưởng nào đến hiệu suất là không chính xác.

Nếu ứng dụng của bạn được thiết kế một cách dẫn đến số lượng kết nối cao, bất kể chúng có hoạt động hay không, bạn nên cân nhắc thay đổi để tài nguyên bộ nhớ và CPU không bị lãng phí để quản lý các kết nối này. Thay đổi có thể nằm trong ứng dụng để giới hạn số lượng kết nối hoặc giải pháp có thể sử dụng một connection pooler. Bạn có thể xem xét sử dụng pgbouncer hoặc sử dụng RDS Proxy, một dịch vụ quản lý cho phép bạn thiết lập connection pooling chỉ với một vài cú nhấp chuột.


Bài được dịch từ bài viết trên AWS Blogs, bạn có thể xem bài viết gốc tại đây.