32 điểm bởi xguru 2024-06-27 | Chưa có bình luận nào. | Chia sẻ qua WhatsApp
  • Cơ sở dữ liệu Postgres sử dụng một lượng lớn RAM. Khi tạo tập kết quả, nó đi qua các bước như khớp chỉ mục, tìm các hàng liên quan trong bảng, hợp nhất/lọc/tổng hợp/sắp xếp tuple, và tất cả các bước này đều phụ thuộc vào bộ nhớ
  • Để tối ưu hóa mức sử dụng bộ nhớ của Postgres, cần tận dụng tối đa lượng RAM sẵn có, đồng thời điều chỉnh hiệu quả các kiểu cấp phát bộ nhớ khác nhau và ngăn OS kết thúc tiến trình do dùng bộ nhớ quá mức

Sharing is Caring

  • Phần RAM lớn nhất liên quan đến Postgres được gọi là shared_buffers, đại diện cho các hàng của mọi bảng và chỉ mục được truy xuất thường xuyên nhất. Nó được hỗ trợ bởi một heuristic chấm điểm theo tần suất sử dụng
    • shared_buffers là giá trị cố định được cấp phát khi Postgres khởi động và không góp phần gây ra các sự cố bộ nhớ bất ngờ
    • Giá trị mặc định là 128MB
    • Tuy nhiên, OS có thể không xem đây là bộ nhớ được cấp phát trước, nên việc đặt giá trị rất cao, tới mức bằng lượng RAM của instance, có thể rủi ro
  • Trên hệ thống production, khuyến nghị phổ biến nhất cho shared_buffers là 25% RAM khả dụng. Đây là điểm khởi đầu phù hợp với hầu hết hệ thống vì đã được điều chỉnh theo phần cứng
    • Kết quả benchmark cho thấy khuyến nghị 25% thường là đủ, nhưng vẫn có thể thay đổi tùy cách cơ sở dữ liệu được sử dụng
    • Ví dụ, hệ thống báo cáo có thể có tỷ lệ cache hit thấp do các truy vấn ad hoc phức tạp, nên đôi khi lại cho hiệu năng tốt hơn đôi chút với cấu hình thấp hơn
  • Có thể dùng extension pg_buffercache để xác định chính xác bảng và chỉ mục nào được cấp phát trong shared buffer. Có thể điều chỉnh giá trị shared_buffers bằng cách kiểm tra số trang đã được sử dụng trong buffer
    • Nếu buffer cache không được sử dụng tới 100%, cấu hình có thể đang quá cao, nên có thể giảm kích thước instance hoặc giá trị shared_buffers
    • Nếu đạt 100% và chỉ nhiều phần nhỏ của các bảng được cache, có thể có lợi khi tăng dần giá trị cho đến khi lợi ích bắt đầu giảm dần
  • View pg_stat_io mới của Postgres 16 cũng có thể hữu ích khi tinh chỉnh shared_buffers. Có thể xem tỷ lệ hit cũng như hoạt động đọc/ghi của client backend
    • Nếu tỷ lệ đọc so với ghi gần bằng 1, điều đó có thể cho thấy Postgres đang liên tục xoay vòng cùng một trang trong shared_buffers. Khi đó nên tăng shared_buffers để giảm hiện tượng thrashing này
  • Khi bắt đầu vượt quá 50% RAM hệ thống, nên cân nhắc tăng kích thước instance, vì Postgres vẫn cần bộ nhớ cho các phiên người dùng và các truy vấn liên quan

Working Memory

  • Nửa còn lại của bộ nhớ mà Postgres thực sự dùng để thực hiện công việc là working memory, được điều khiển bởi tham số work_mem
    • Giá trị mặc định là 4MB và đây là một trong những giá trị đầu tiên người dùng có thể thay đổi để tăng tốc thực thi truy vấn
    • Tuy nhiên, nếu OS đang kết thúc Postgres do báo lỗi “hết bộ nhớ”, bạn có thể muốn tăng work_mem, nhưng điều này chỉ làm vấn đề tệ hơn. Nó làm tăng lượng RAM Postgres sử dụng và khiến việc bị kết thúc càng dễ xảy ra hơn
  • Nhiều người hiểu “working memory” là một khoản cấp phát duy nhất dành cho mọi thao tác Postgres có thể thực hiện trong lúc xử lý truy vấn, nhưng thực tế có thể nhiều hơn thế
    • Mỗi bước (node) được cấp một instance riêng của work_mem. Ví dụ, nếu dùng giá trị mặc định 4MB của work_mem, một truy vấn cần 4 node có thể tiêu thụ tới 16MB RAM
    • Trên máy chủ bận rộn, nếu có 100 truy vấn như vậy chạy đồng thời, riêng việc tính kết quả đã có thể dùng tới 1.6GB RAM. Truy vấn phức tạp hơn có thể cần nhiều RAM hơn tùy vào số node cần cho việc thực thi
  • Dùng lệnh EXPLAIN để xem execution plan của truy vấn sẽ cho thấy Postgres thực thi truy vấn như thế nào và mọi node cần để tạo đầu ra
    • Khi dùng cùng extension pg_stat_statements, có thể tách ra những truy vấn hoạt động nhiều nhất và ước tính tổng mức dùng bộ nhớ do work_mem gây ra
  • Nếu work_mem được đặt quá thấp, các hàng hoặc kết quả trung gian không vừa trong RAM sẽ tràn ra đĩa, khiến tốc độ chậm đi rất nhiều
    • Có thể kiểm tra view pg_stat_database để xem tổng kích thước và số lượng các file tạm đã ghi ra đĩa, và nếu kích thước trung bình là hợp lý thì có thể tăng work_mem thêm đúng bằng mức đó
  • Để có ước lượng sơ bộ về lượng RAM khả dụng cho mỗi phiên, dùng công thức sau: (80% tổng RAM - shared_buffers) / (max_connections)
    • Ví dụ, với 16GB RAM, 4GB shared buffer và 100 kết nối tối đa, mỗi phiên có thể dùng khoảng 88MB
    • Chia giá trị này cho số node trung bình trong execution plan để có một cấu hình work_mem hợp lý

Ongoing Maintenance

  • Phần cuối cùng có thể tinh chỉnh trong mức dùng RAM của Postgres tương tự working memory nhưng dành riêng cho bảo trì, với tên tham số tương ứng là maintenance_work_mem
    • Giá trị mặc định là 64MB và xác định lượng RAM dành riêng cho các thao tác như VACUUM, CREATE INDEX, ALTER TABLE ADD FOREIGN KEY
  • Vì bị giới hạn ở một thao tác trên mỗi phiên và khả năng có nhiều thao tác đồng thời là thấp, nên việc dùng giá trị cao hơn thường được xem là khá an toàn
    • Các tác vụ bảo trì này có thể dùng rất nhiều bộ nhớ và có thể hoàn thành nhanh hơn nhiều nếu chạy hoàn toàn trong RAM, vì vậy cấu hình tới 1GB hoặc 2GB là rất phổ biến
  • Điểm cần lưu ý quan trọng là tiến trình autovacuum của Postgres, vốn đánh dấu các tuple chết để tái sử dụng sau này
    • Autovacuum khởi chạy các tác vụ nền tới giới hạn autovacuum_max_workers, và mỗi tác vụ có thể dùng toàn bộ một instance của maintenance_work_mem
    • Máy chủ còn nhiều RAM thường an toàn với 1GB working memory cho bảo trì, nhưng nếu RAM hạn chế thì cần cẩn trọng hơn
    • Đặc biệt, để giới hạn worker autovacuum thì có tham số riêng là autovacuum_work_mem
    • Worker autovacuum của Postgres không thể dùng quá 1GB, nên cấu hình autovacuum_work_mem cao hơn mức này sẽ không có tác dụng

Session Pooling

  • Cách dễ nhất để giảm tiêu thụ bộ nhớ là đặt giới hạn logic lên các khoản cấp phát tiềm năng
    • Postgres hiện là engine dựa trên tiến trình, nên mỗi phiên người dùng được gán một tiến trình vật lý chứ không phải thread
    • Vì vậy, mọi kết nối đều mang theo một phần overhead RAM nhất định và góp phần làm tăng context switch
    • Kết quả là khuyến nghị phổ biến là đặt max_connections không quá 4 lần số thread CPU khả dụng. Điều này giảm thời gian chuyển đổi các phiên hoạt động giữa các CPU và đồng thời tự nhiên giới hạn tổng RAM các phiên có thể tiêu thụ
  • Nếu mọi phiên đều đang chạy truy vấn và mỗi node đại diện cho một lần cấp phát work_mem, thì mức dùng working memory tối đa về lý thuyết là connections * nodes * work_mem
    • Không phải lúc nào cũng có thể giảm độ phức tạp của truy vấn, nhưng thường có thể giảm số lượng kết nối
    • Điều này có thể không dễ nếu ứng dụng luôn mở sẵn một lượng phiên lớn cố định hoặc nhiều microservice riêng lẻ cùng phụ thuộc vào Postgres
  • Công thức work_mem * max_connections * 5 là một ước tính thô về lượng RAM tối đa mà instance Postgres có thể cấp cho các phiên người dùng để xử lý truy vấn cơ bản, giả sử mọi kết nối đều hoạt động
    • Nếu máy chủ không có đủ RAM cho giá trị này, nên cân nhắc giảm một trong các yếu tố hoặc tăng RAM
    • Ước tính 5 node trung bình cho mỗi truy vấn có thể không phù hợp với ứng dụng của bạn, nên cần điều chỉnh khi đã hiểu rõ hơn về execution plan của truy vấn
  • Bước tiếp theo là đưa vào một connection pooler như PgBouncer
    • Nó tách kết nối client khỏi cơ sở dữ liệu và tái sử dụng các session Postgres đắt đỏ giữa nhiều client
    • Nếu cấu hình phù hợp, hàng trăm client có thể chia sẻ vài chục kết nối Postgres mà không ảnh hưởng tới ứng dụng
    • Đã ghi nhận PgBouncer có thể multiplex hơn 1000 kết nối xuống còn 40-50 theo cách này, nhờ đó giảm mạnh tổng mức tiêu thụ bộ nhớ do overhead tiến trình

Reducing Bloat

  • Khía cạnh khó theo dõi nhất của mức dùng bộ nhớ có lẽ là table bloat
    • Postgres dùng cơ chế điều khiển tương tranh đa phiên bản (MVCC) để biểu diễn dữ liệu trong hệ thống lưu trữ
    • Nghĩa là mỗi khi một hàng trong bảng bị sửa đổi, Postgres sẽ tạo một bản sao khác của hàng đó ở đâu đó trong bảng và đánh dấu nó bằng số phiên bản mới
    • Tiến trình VACUUM của Postgres đánh dấu các phiên bản hàng cũ là vùng “không còn dùng” để có thể đặt các phiên bản hàng mới vào đó
  • Postgres có tiến trình nền autovacuum liên tục tìm các vùng cấp phát có thể tái sử dụng này để bảng không tăng mãi không giới hạn
    • Nhưng đôi khi, đặc biệt với hệ thống lớn, cấu hình mặc định cho việc này có thể không đủ và các tác vụ bảo trì có thể bị tụt lại phía sau
    • Kết quả là bảng có thể chứa nhiều hàng chết hơn hàng sống và trở thành bảng bị “phình to” bởi dữ liệu cũ
  • Nếu bảng bị phình quá mức, cần xem xét tác động của nó lên shared buffer
    • Nếu mỗi trang chỉ chứa một hàng đang dùng và nhiều hàng chết, thì khi một truy vấn cần 10 hàng cụ thể, nó phải nạp 10 trang vào shared buffer, từ đó lãng phí nhiều bộ nhớ vốn có thể dùng cho việc khác
    • Nếu nhu cầu với các hàng này đặc biệt cao, số lần sử dụng sẽ giữ chúng lại trong shared buffer, làm hiệu quả cache giảm đi đáng kể
  • Có nhiều truy vấn ước tính table bloat được chia sẻ trên Internet, nhưng cách duy nhất để xem cụ thể các trang của bảng trông như thế nào là dùng extension pgstattuple
  • Nếu free_percent lớn hơn 30%, có thể cần tinh chỉnh autovacuum theo hướng tích cực hơn. Nếu lớn hơn nhiều so với 30%, nên loại bỏ bloat hoàn toàn
    • Hiện tại, cách duy nhất được hỗ trợ để làm điều này là dùng lệnh VACUUM FULL để về bản chất tái xây dựng lại bảng. Lệnh này di chuyển mọi hàng đang dùng sang vị trí mới và loại bỏ bản sao bị phình trước đó
    • Quá trình này áp dụng khóa truy cập độc quyền trong suốt thời gian thực hiện, nên trong gần như mọi trường hợp sẽ cần một dạng downtime nào đó
  • Một lựa chọn khác là extension pg_repack được Tembo hỗ trợ
    • Công cụ dòng lệnh này có thể tổ chức lại bảng để loại bỏ bloat hoàn toàn online mà không cần khóa độc quyền
    • Vì công cụ này tồn tại ngoài Postgres core và sửa đổi vùng lưu trữ bảng cũng như chỉ mục, nó thường được xem là một kỹ thuật nâng cao
    • Nên thử nghiệm kỹ trong môi trường không phải production trước khi sử dụng
  • Có thể đi xa hơn bằng cách sắp xếp lại thứ tự cột để tối đa hóa số hàng trên mỗi trang, giống như chơi Tetris với cột
    • Đây có lẽ là mức tối ưu hóa khá cực đoan, nhưng trong môi trường có quyền tự do tái xây dựng bảng theo cách đó thì vẫn là một chiến lược khả thi

The Balancing Act

  • Việc cấu hình hợp lý tất cả các tham số và tài nguyên này vừa là nghệ thuật vừa là khoa học
    • Chúng ta đã xem cách đo mức sử dụng thực tế của shared buffer và cách xác định khi nào working memory đang quá thấp
    • Nhưng nếu, như trong hầu hết trường hợp, phần cứng hoặc ngân sách khả dụng có giới hạn thì sao? Đó là lúc phần “nghệ thuật” phát huy tác dụng
  • Trong tình huống thiếu bộ nhớ, có thể cần giảm nhẹ shared_buffers để tạo chỗ cho nhiều work_mem hơn. Hoặc cũng có thể phải giảm cả hai
    • Nếu ứng dụng cần rất nhiều phiên, có thể hợp lý hơn khi giảm work_mem hoặc đưa vào connection pooling để tránh các phiên đồng thời cộng dồn các khoản cấp phát RAM lớn
    • Nếu trước đây bạn đã tăng maintenance_work_mem với giả định RAM đủ cho mọi thứ, thì lúc này có thể hợp lý hơn khi giảm nó xuống. Có rất nhiều điều cần cân nhắc
  • Trên các instance ít bộ nhớ, ngay cả các khuyến nghị trên cũng có thể chưa đủ. Trong những trường hợp này, nên làm theo thứ tự sau để tối đa hóa việc sử dụng bộ nhớ và tránh cạn kiệt tài nguyên:
    1. Thêm connection pooler và giảm max_connections. Đây là cách nhanh và dễ nhất để giảm mức tiêu thụ tài nguyên tối đa
    2. Dùng EXPLAIN cho các truy vấn xuất hiện thường xuyên nhất theo báo cáo từ pg_stat_statements để tìm số node tối đa của truy vấn, thay vì số trung bình. Sau đó đặt work_mem không vượt quá (80% tổng RAM - shared_buffers) / (max_connections * số node kế hoạch tối đa)
    3. Đưa maintenance_work_memautovacuum_work_mem về mặc định 64MB. Nếu tác vụ bảo trì quá chậm và có thể dùng thêm RAM, hãy cân nhắc tăng theo từng bước 8MB
    4. Dùng extension pg_buffercache để xem lượng bảng được lưu trong shared_buffers. Xem xét kỹ từng bảng và chỉ mục để tìm cách giảm chúng, như lưu trữ archive dữ liệu, sửa truy vấn để dùng ít thông tin hơn, v.v. Việc này có thể bao gồm VACUUM FULL hoặc pg_repack để nén lại các trang đang bị chiếm bởi bảng phình to
    5. Nếu pg_buffercache cho thấy shared_buffers đã đầy và không thể giảm thêm nếu không loại bỏ các trang đang hoạt động, hãy dùng cột usagecount để ưu tiên các trang hoạt động nhất. Giá trị của cột này nằm trong khoảng 1-5, vì vậy tập trung vào các trang được dùng 3-5 lần có thể giúp giảm shared_buffers mà không ảnh hưởng lớn đến hiệu năng
    6. Cuối cùng, cấp phát phần cứng mạnh hơn. Nếu cơ sở dữ liệu thực sự cần nhiều RAM hơn cho workload hiện tại và việc giảm các tham số trên gây tác động xấu quá lớn tới hiệu năng hệ thống, thì nâng cấp thường là lựa chọn hợp lý hơn

Chưa có bình luận nào.

Chưa có bình luận nào.