Postgres: các ràng buộc phình to, pg_repack và trì hoãn

Postgres: các ràng buộc phình to, pg_repack và trì hoãn

Hiệu ứng phình to trên các bảng và chỉ mục được biết đến rộng rãi và không chỉ hiện diện trong Postgres. Có nhiều cách để giải quyết vấn đề này ngay lập tức, chẳng hạn như VACUUM FULL hoặc CLUSTER, nhưng chúng khóa các bảng trong khi hoạt động và do đó không phải lúc nào cũng có thể sử dụng được.

Bài viết sẽ chứa một số lý thuyết nhỏ về cách xảy ra tình trạng phình to, cách bạn có thể chống lại nó, về các hạn chế bị trì hoãn và các vấn đề mà chúng mang lại khi sử dụng tiện ích mở rộng pg_repack.

Bài viết này được viết dựa trên bài phát biểu của tôi tại PGConf.Nga 2020.

Tại sao sưng tấy xảy ra?

Postgres dựa trên mô hình nhiều phiên bản (MVCC). Bản chất của nó là mỗi hàng trong bảng có thể có một số phiên bản, trong khi các giao dịch chỉ nhìn thấy một trong các phiên bản này, nhưng không nhất thiết phải giống nhau. Điều này cho phép một số giao dịch hoạt động đồng thời và hầu như không có tác động lẫn nhau.

Rõ ràng, tất cả các phiên bản này cần phải được lưu trữ. Postgres hoạt động với bộ nhớ theo từng trang và một trang là lượng dữ liệu tối thiểu có thể được đọc hoặc ghi từ đĩa. Hãy xem một ví dụ nhỏ để hiểu điều này xảy ra như thế nào.

Giả sử chúng ta có một bảng mà chúng ta đã thêm một số bản ghi vào đó. Dữ liệu mới đã xuất hiện ở trang đầu tiên của tệp nơi bảng được lưu trữ. Đây là các phiên bản trực tiếp của các hàng có sẵn cho các giao dịch khác sau khi cam kết (để đơn giản, chúng tôi sẽ giả định rằng mức cô lập là Đã đọc đã cam kết).

Postgres: các ràng buộc phình to, pg_repack và trì hoãn

Sau đó, chúng tôi đã cập nhật một trong các mục, do đó đánh dấu phiên bản cũ là không còn phù hợp.

Postgres: các ràng buộc phình to, pg_repack và trì hoãn

Từng bước cập nhật và xóa các phiên bản hàng, chúng tôi đã kết thúc với một trang trong đó khoảng một nửa dữ liệu là “rác”. Dữ liệu này không được hiển thị cho bất kỳ giao dịch nào.

Postgres: các ràng buộc phình to, pg_repack và trì hoãn

Postgres có cơ chế KHOẢNG CHÂN KHÔNG, giúp loại bỏ các phiên bản lỗi thời và nhường chỗ cho dữ liệu mới. Nhưng nếu nó không được cấu hình đủ mạnh hoặc đang bận làm việc trong các bảng khác thì "dữ liệu rác" vẫn còn và chúng ta phải sử dụng các trang bổ sung cho dữ liệu mới.

Vì vậy, trong ví dụ của chúng tôi, tại một thời điểm nào đó, bảng sẽ bao gồm bốn trang nhưng chỉ một nửa trong số đó chứa dữ liệu trực tiếp. Kết quả là khi truy cập vào bảng, chúng ta sẽ đọc được nhiều dữ liệu hơn mức cần thiết.

Postgres: các ràng buộc phình to, pg_repack và trì hoãn

Ngay cả khi VACUUM hiện xóa tất cả các phiên bản hàng không liên quan thì tình hình cũng sẽ không được cải thiện đáng kể. Chúng tôi sẽ có không gian trống trong các trang hoặc thậm chí toàn bộ trang cho các hàng mới, nhưng chúng tôi vẫn sẽ đọc nhiều dữ liệu hơn mức cần thiết.
Nhân tiện, nếu một trang hoàn toàn trống (trang thứ hai trong ví dụ của chúng tôi) ở cuối tệp thì VACUUM sẽ có thể cắt bớt nó. Nhưng bây giờ cô ấy đang ở giữa nên chẳng thể làm gì được cô ấy.

Postgres: các ràng buộc phình to, pg_repack và trì hoãn

Khi số lượng trang trống hoặc rất thưa thớt như vậy trở nên lớn, được gọi là quá tải, nó bắt đầu ảnh hưởng đến hiệu suất.

Tất cả mọi thứ được mô tả ở trên là cơ chế xuất hiện sự phình to trong các bảng. Trong các chỉ mục, điều này xảy ra theo cách tương tự.

Tôi có bị đầy hơi không?

Có một số cách để xác định xem bạn có bị đầy hơi hay không. Ý tưởng đầu tiên là sử dụng số liệu thống kê nội bộ của Postgres, chứa thông tin gần đúng về số lượng hàng trong bảng, số lượng hàng “trực tiếp”, v.v. Bạn có thể tìm thấy nhiều biến thể của tập lệnh tạo sẵn trên Internet. Chúng tôi lấy làm cơ sở kịch bản từ các Chuyên gia PostgreSQL, những người có thể đánh giá các bảng phình to cùng với các chỉ mục btree toast và bloat. Theo kinh nghiệm của chúng tôi, sai số của nó là 10-20%.

Một cách khác là sử dụng phần mở rộng pgstattuple, cho phép bạn xem bên trong các trang và nhận được cả giá trị phình to ước tính và chính xác. Nhưng trong trường hợp thứ hai, bạn sẽ phải quét toàn bộ bảng.

Chúng tôi coi giá trị tăng vọt nhỏ, lên tới 20% là có thể chấp nhận được. Nó có thể được coi là một chất tương tự của fillfactor cho những cái bàn и chỉ số. Ở mức 50% trở lên, các vấn đề về hiệu suất có thể bắt đầu.

Các cách chống đầy hơi

Postgres có một số cách để giải quyết tình trạng cồng kềnh ngay lập tức, nhưng chúng không phải lúc nào cũng phù hợp với tất cả mọi người.

Định cấu hình AUTOVACUUM để không xảy ra tình trạng đầy hơi. Hay chính xác hơn là giữ nó ở mức độ bạn có thể chấp nhận được. Đây có vẻ giống lời khuyên của “thuyền trưởng”, nhưng thực tế không phải lúc nào cũng dễ dàng đạt được điều này. Ví dụ: bạn có sự phát triển tích cực với những thay đổi thường xuyên đối với lược đồ dữ liệu hoặc một số loại di chuyển dữ liệu đang diễn ra. Do đó, hồ sơ tải của bạn có thể thay đổi thường xuyên và thường sẽ khác nhau tùy theo từng bảng. Điều này có nghĩa là bạn cần phải liên tục làm việc trước một chút và điều chỉnh AUTOVACUUM theo cấu hình thay đổi của mỗi bảng. Nhưng rõ ràng điều này không dễ thực hiện.

Một lý do phổ biến khác khiến AUTOVACUUM không thể theo kịp các bảng là do có các giao dịch kéo dài khiến nó không thể dọn sạch dữ liệu có sẵn cho các giao dịch đó. Khuyến nghị ở đây cũng rất rõ ràng - loại bỏ các giao dịch “lơ lửng” và giảm thiểu thời gian thực hiện các giao dịch. Nhưng nếu tải trên ứng dụng của bạn là sự kết hợp giữa OLAP và OLTP, thì bạn có thể đồng thời có nhiều cập nhật thường xuyên và truy vấn ngắn, cũng như các hoạt động dài hạn - ví dụ: xây dựng báo cáo. Trong tình huống như vậy, đáng để suy nghĩ về việc phân bổ tải trọng trên các cơ sở khác nhau, điều này sẽ cho phép tinh chỉnh từng cơ sở tốt hơn.

Một ví dụ khác - ngay cả khi cấu hình đồng nhất, nhưng cơ sở dữ liệu chịu tải rất cao, thì ngay cả AUTOVACUUM mạnh mẽ nhất cũng có thể không đối phó được và tình trạng phình to sẽ xảy ra. Chia tỷ lệ (dọc hoặc ngang) là giải pháp duy nhất.

Phải làm gì trong tình huống bạn đã thiết lập AUTOVACUUM nhưng tình trạng phình to vẫn tiếp tục gia tăng.

Đội CHÂN KHÔNG ĐẦY ĐỦ xây dựng lại nội dung của các bảng và chỉ mục và chỉ để lại dữ liệu liên quan trong đó. Để loại bỏ sự phình to, nó hoạt động hoàn hảo, nhưng trong quá trình thực thi, một khóa độc quyền trên bảng sẽ bị bắt (AccessExclusiveLock), khóa này sẽ không cho phép thực hiện các truy vấn trên bảng này, thậm chí là chọn. Nếu bạn có đủ khả năng để dừng dịch vụ hoặc một phần dịch vụ của mình trong một thời gian (từ hàng chục phút đến vài giờ tùy thuộc vào kích thước cơ sở dữ liệu và phần cứng của bạn), thì tùy chọn này là tốt nhất. Thật không may, chúng tôi không có thời gian để chạy VACUUM FULL trong quá trình bảo trì theo lịch trình, vì vậy phương pháp này không phù hợp với chúng tôi.

Đội Cụm Xây dựng lại nội dung của các bảng theo cách tương tự như VACUUM FULL, nhưng cho phép bạn chỉ định một chỉ mục theo đó dữ liệu sẽ được sắp xếp vật lý trên đĩa (nhưng trong tương lai thứ tự này không được đảm bảo cho các hàng mới). Trong một số trường hợp nhất định, đây là cách tối ưu hóa tốt cho một số truy vấn - với việc đọc nhiều bản ghi theo chỉ mục. Nhược điểm của lệnh cũng giống như VACUUM FULL - nó khóa bàn trong khi hoạt động.

Đội REINDEX tương tự như hai phần trước nhưng xây dựng lại một chỉ mục cụ thể hoặc tất cả các chỉ mục của bảng. Khóa yếu hơn một chút: ShareLock trên bảng (ngăn sửa đổi nhưng cho phép chọn) và AccessExclusiveLock trên chỉ mục đang được xây dựng lại (chặn các truy vấn sử dụng chỉ mục này). Tuy nhiên ở phiên bản thứ 12 của Postgres xuất hiện một tham số KIÊM NHIỆM, cho phép bạn xây dựng lại chỉ mục mà không chặn việc bổ sung, sửa đổi hoặc xóa các bản ghi đồng thời.

Trong các phiên bản trước của Postgres, bạn có thể đạt được kết quả tương tự như REINDEX ĐỒNG thời bằng cách sử dụng TẠO CHỈ SỐ ĐỒNG HÀNH. Nó cho phép bạn tạo một chỉ mục mà không cần khóa nghiêm ngặt (ShareUpdateExclusiveLock, không can thiệp vào các truy vấn song song), sau đó thay thế chỉ mục cũ bằng một chỉ mục mới và xóa chỉ mục cũ. Điều này cho phép bạn loại bỏ tình trạng phình chỉ mục mà không can thiệp vào ứng dụng của bạn. Điều quan trọng cần lưu ý là khi xây dựng lại các chỉ mục sẽ có tải bổ sung trên hệ thống con đĩa.

Vì vậy, nếu đối với các chỉ mục có nhiều cách để loại bỏ tình trạng phình to “nhanh chóng” thì không có cách nào đối với các bảng. Đây là nơi các tiện ích mở rộng bên ngoài khác nhau phát huy tác dụng: pg_repack (trước đây là pg_reorg), pgcompact, pgcompacttable và những người khác. Trong bài viết này, tôi sẽ không so sánh chúng và sẽ chỉ nói về pg_repack, sau một số sửa đổi, chúng tôi sử dụng chính chúng tôi.

Cách pg_repack hoạt động

Postgres: các ràng buộc phình to, pg_repack và trì hoãn
Giả sử chúng ta có một bảng hoàn toàn bình thường - với các chỉ mục, các hạn chế và thật không may, có sự phình to. Bước đầu tiên của pg_repack là tạo bảng nhật ký để lưu trữ dữ liệu về tất cả các thay đổi trong khi nó đang chạy. Trình kích hoạt sẽ sao chép những thay đổi này cho mỗi lần chèn, cập nhật và xóa. Sau đó, một bảng được tạo, tương tự như bảng ban đầu về cấu trúc, nhưng không có chỉ mục và hạn chế để không làm chậm quá trình chèn dữ liệu.

Tiếp theo, pg_repack chuyển dữ liệu từ bảng cũ sang bảng mới, tự động lọc ra tất cả các hàng không liên quan và sau đó tạo chỉ mục cho bảng mới. Trong quá trình thực hiện tất cả các thao tác này, các thay đổi sẽ tích lũy trong bảng nhật ký.

Bước tiếp theo là chuyển các thay đổi sang bảng mới. Quá trình di chuyển được thực hiện qua nhiều lần lặp và khi chỉ còn lại ít hơn 20 mục trong bảng nhật ký, pg_repack sẽ có được khóa mạnh, di chuyển dữ liệu mới nhất và thay thế bảng cũ bằng bảng mới trong các bảng hệ thống Postgres. Đây là khoảng thời gian duy nhất và rất ngắn khi bạn không thể làm việc với cái bàn. Sau đó, bảng cũ và bảng có nhật ký sẽ bị xóa và dung lượng được giải phóng trong hệ thống tệp. Quá trình này đã hoàn tất.

Về lý thuyết mọi thứ có vẻ tuyệt vời, nhưng điều gì xảy ra trong thực tế? Chúng tôi đã thử nghiệm pg_repack khi không tải và đang tải, đồng thời kiểm tra hoạt động của nó trong trường hợp dừng sớm (nói cách khác là sử dụng Ctrl+C). Tất cả các xét nghiệm đều dương tính.

Chúng tôi đến cửa hàng thực phẩm - và rồi mọi thứ không diễn ra như chúng tôi mong đợi.

Bánh pancake đầu tiên được bán

Trên cụm đầu tiên, chúng tôi đã nhận được lỗi về việc vi phạm ràng buộc duy nhất:

$ ./pg_repack -t tablename -o id
INFO: repacking table "tablename"
ERROR: query failed: 
    ERROR: duplicate key value violates unique constraint "index_16508"
DETAIL:  Key (id, index)=(100500, 42) already exists.

Giới hạn này có tên được tạo tự động index_16508 - nó được tạo bởi pg_repack. Dựa trên các thuộc tính có trong thành phần của nó, chúng tôi đã xác định ràng buộc “của chúng tôi” tương ứng với nó. Vấn đề hóa ra là đây không phải là một hạn chế hoàn toàn bình thường mà là một hạn chế bị trì hoãn (hạn chế trì hoãn), I E. Việc xác minh của nó được thực hiện muộn hơn lệnh sql dẫn đến hậu quả không mong muốn.

Các ràng buộc trì hoãn: tại sao chúng cần thiết và chúng hoạt động như thế nào

Một chút lý thuyết về hạn chế trì hoãn.
Hãy xem xét một ví dụ đơn giản: chúng ta có một bảng tham khảo về ô tô với hai thuộc tính - tên và thứ tự của ô tô trong thư mục.
Postgres: các ràng buộc phình to, pg_repack và trì hoãn

create table cars
(
  name text constraint pk_cars primary key,
  ord integer not null constraint uk_cars unique
);



Giả sử chúng ta cần đổi chiếc xe thứ nhất và thứ hai. Giải pháp đơn giản là cập nhật giá trị đầu tiên thành giá trị thứ hai và giá trị thứ hai thành giá trị đầu tiên:

begin;
  update cars set ord = 2 where name = 'audi';
  update cars set ord = 1 where name = 'bmw';
commit;

Nhưng khi chạy mã này, chúng tôi dự đoán sẽ có sự vi phạm ràng buộc vì thứ tự của các giá trị trong bảng là duy nhất:

[23305] ERROR: duplicate key value violates unique constraint “uk_cars”
Detail: Key (ord)=(2) already exists.

Làm thế nào tôi có thể làm điều đó khác đi? Tùy chọn thứ nhất: thêm giá trị thay thế bổ sung vào đơn hàng được đảm bảo không tồn tại trong bảng, ví dụ: “-1”. Trong lập trình, điều này được gọi là “trao đổi giá trị của hai biến thông qua một biến thứ ba”. Hạn chế duy nhất của phương pháp này là cập nhật bổ sung.

Tùy chọn hai: Thiết kế lại bảng để sử dụng kiểu dữ liệu dấu phẩy động cho giá trị đơn hàng thay vì số nguyên. Sau đó, khi cập nhật giá trị từ 1 chẳng hạn lên 2.5, mục đầu tiên sẽ tự động “đứng” giữa mục thứ hai và thứ ba. Giải pháp này hoạt động, nhưng có hai hạn chế. Đầu tiên, nó sẽ không hiệu quả với bạn nếu giá trị được sử dụng ở đâu đó trong giao diện. Thứ hai, tùy thuộc vào độ chính xác của kiểu dữ liệu, bạn sẽ có một số lượng hạn chế các lần chèn có thể có trước khi tính toán lại giá trị của tất cả các bản ghi.

Tùy chọn ba: trì hoãn ràng buộc để nó chỉ được kiểm tra tại thời điểm cam kết:

create table cars
(
  name text constraint pk_cars primary key,
  ord integer not null constraint uk_cars unique deferrable initially deferred
);

Vì logic của yêu cầu ban đầu của chúng tôi đảm bảo rằng tất cả các giá trị là duy nhất tại thời điểm cam kết nên nó sẽ thành công.

Tất nhiên, ví dụ được thảo luận ở trên rất tổng hợp, nhưng nó tiết lộ ý tưởng. Trong ứng dụng của mình, chúng tôi sử dụng các ràng buộc trì hoãn để triển khai logic chịu trách nhiệm giải quyết xung đột khi người dùng làm việc đồng thời với các đối tượng tiện ích được chia sẻ trên bảng. Việc sử dụng các hạn chế như vậy cho phép chúng tôi làm cho mã ứng dụng đơn giản hơn một chút.

Nói chung, tùy thuộc vào loại ràng buộc, Postgres có ba mức độ chi tiết để kiểm tra chúng: mức hàng, mức giao dịch và mức biểu thức.
Postgres: các ràng buộc phình to, pg_repack và trì hoãn
Nguồn: người ăn xin

CHECK và NOT NULL luôn được kiểm tra ở cấp hàng; đối với các hạn chế khác, như có thể thấy từ bảng, có các tùy chọn khác nhau. Bạn có thể đọc thêm đây.

Tóm lại, các ràng buộc trì hoãn trong một số trường hợp cung cấp mã dễ đọc hơn và ít lệnh hơn. Tuy nhiên, bạn phải trả giá cho điều này bằng cách làm phức tạp quá trình gỡ lỗi, vì thời điểm xảy ra lỗi và thời điểm bạn phát hiện ra lỗi đó khác nhau về thời gian. Một vấn đề khác có thể xảy ra là bộ lập lịch không phải lúc nào cũng có thể xây dựng một kế hoạch tối ưu nếu yêu cầu liên quan đến ràng buộc trì hoãn.

Cải thiện pg_repack

Chúng ta đã đề cập đến ràng buộc trì hoãn là gì, nhưng chúng liên quan như thế nào đến vấn đề của chúng ta? Hãy nhớ lại lỗi chúng tôi đã nhận được trước đó:

$ ./pg_repack -t tablename -o id
INFO: repacking table "tablename"
ERROR: query failed: 
    ERROR: duplicate key value violates unique constraint "index_16508"
DETAIL:  Key (id, index)=(100500, 42) already exists.

Nó xảy ra khi dữ liệu được sao chép từ bảng nhật ký sang bảng mới. Điều này có vẻ kỳ lạ vì... dữ liệu trong bảng nhật ký được cam kết cùng với dữ liệu trong bảng nguồn. Nếu chúng thỏa mãn các ràng buộc của bảng gốc thì làm sao chúng có thể vi phạm các ràng buộc tương tự trong bảng mới?

Hóa ra, gốc rễ của vấn đề nằm ở bước trước của pg_repack, bước này chỉ tạo các chỉ mục chứ không tạo ra các ràng buộc: bảng cũ có một ràng buộc duy nhất và thay vào đó, bảng mới tạo một chỉ mục duy nhất.

Postgres: các ràng buộc phình to, pg_repack và trì hoãn

Điều quan trọng cần lưu ý ở đây là nếu ràng buộc là bình thường và không bị trì hoãn thì chỉ mục duy nhất được tạo thay vào đó sẽ tương đương với ràng buộc này, bởi vì Các ràng buộc duy nhất trong Postgres được triển khai bằng cách tạo một chỉ mục duy nhất. Nhưng trong trường hợp ràng buộc trì hoãn, hành vi không giống nhau, vì chỉ mục không thể trì hoãn và luôn được kiểm tra tại thời điểm lệnh sql được thực thi.

Vì vậy, bản chất của vấn đề nằm ở “độ trễ” của quá trình kiểm tra: trong bảng gốc, nó xảy ra tại thời điểm cam kết và trong bảng mới tại thời điểm lệnh sql được thực thi. Điều này có nghĩa là chúng ta cần đảm bảo rằng việc kiểm tra được thực hiện giống nhau trong cả hai trường hợp: luôn bị trì hoãn hoặc luôn ngay lập tức.

Vậy chúng tôi đã có ý tưởng gì?

Tạo một chỉ mục tương tự như deferred

Ý tưởng đầu tiên là thực hiện cả hai lần kiểm tra ở chế độ ngay lập tức. Điều này có thể tạo ra một số hạn chế dương tính giả, nhưng nếu có ít hạn chế trong số đó thì điều này sẽ không ảnh hưởng đến công việc của người dùng vì những xung đột như vậy là tình huống bình thường đối với họ. Ví dụ: chúng xảy ra khi hai người dùng bắt đầu chỉnh sửa cùng một tiện ích cùng một lúc và ứng dụng khách của người dùng thứ hai không có thời gian để nhận thông tin rằng tiện ích đó đã bị người dùng đầu tiên chặn chỉnh sửa. Trong tình huống như vậy, máy chủ từ chối người dùng thứ hai và máy khách của nó sẽ khôi phục các thay đổi và chặn tiện ích. Một lát sau, khi người dùng đầu tiên hoàn tất chỉnh sửa, người dùng thứ hai sẽ nhận được thông tin rằng tiện ích không còn bị chặn và có thể lặp lại hành động của họ.

Postgres: các ràng buộc phình to, pg_repack và trì hoãn

Để đảm bảo rằng các kiểm tra luôn ở chế độ không trì hoãn, chúng tôi đã tạo một chỉ mục mới tương tự như ràng buộc trì hoãn ban đầu:

CREATE UNIQUE INDEX CONCURRENTLY uk_tablename__immediate ON tablename (id, index);
-- run pg_repack
DROP INDEX CONCURRENTLY uk_tablename__immediate;

Trong môi trường thử nghiệm, chúng tôi chỉ nhận được một số lỗi dự kiến. Thành công! Chúng tôi đã chạy lại pg_repack trong quá trình sản xuất và gặp 5 lỗi trên cụm đầu tiên sau một giờ làm việc. Đây là một kết quả có thể chấp nhận được. Tuy nhiên, ở cụm thứ hai, số lỗi đã tăng lên đáng kể và chúng tôi phải dừng pg_repack.

Tại sao nó lại xảy ra? Khả năng xảy ra lỗi phụ thuộc vào số lượng người dùng đang làm việc với cùng một widget cùng một lúc. Rõ ràng, tại thời điểm đó, có ít thay đổi mang tính cạnh tranh hơn đối với dữ liệu được lưu trữ trên cụm đầu tiên so với các cụm khác, tức là. chúng tôi chỉ “may mắn” thôi.

Ý tưởng đó không thành công. Tại thời điểm đó, chúng tôi thấy hai giải pháp khác: viết lại mã ứng dụng của chúng tôi để loại bỏ các ràng buộc bị trì hoãn hoặc “dạy” pg_repack cách làm việc với chúng. Chúng tôi đã chọn cái thứ hai.

Thay thế các chỉ mục trong bảng mới bằng các ràng buộc trì hoãn từ bảng gốc

Mục đích của việc sửa đổi rất rõ ràng - nếu bảng gốc có một ràng buộc trì hoãn, thì đối với bảng mới, bạn cần tạo một ràng buộc như vậy chứ không phải một chỉ mục.

Để kiểm tra những thay đổi của chúng tôi, chúng tôi đã viết một bài kiểm tra đơn giản:

  • bảng có ràng buộc hoãn lại và một bản ghi;
  • chèn dữ liệu vào vòng lặp xung đột với bản ghi hiện có;
  • thực hiện cập nhật – dữ liệu không còn xung đột nữa;
  • cam kết những thay đổi.

create table test_table
(
  id serial,
  val int,
  constraint uk_test_table__val unique (val) deferrable initially deferred 
);

INSERT INTO test_table (val) VALUES (0);
FOR i IN 1..10000 LOOP
  BEGIN
    INSERT INTO test_table VALUES (0) RETURNING id INTO v_id;
    UPDATE test_table set val = i where id = v_id;
    COMMIT;
  END;
END LOOP;

Phiên bản gốc của pg_repack luôn bị lỗi trong lần chèn đầu tiên, phiên bản sửa đổi hoạt động không có lỗi. Tuyệt vời.

Chúng tôi bắt đầu sản xuất và lại gặp lỗi ở cùng giai đoạn sao chép dữ liệu từ bảng nhật ký sang bảng mới:

$ ./pg_repack -t tablename -o id
INFO: repacking table "tablename"
ERROR: query failed: 
    ERROR: duplicate key value violates unique constraint "index_16508"
DETAIL:  Key (id, index)=(100500, 42) already exists.

Tình huống điển hình: mọi thứ đều hoạt động trong môi trường thử nghiệm, nhưng không hoạt động trong môi trường sản xuất?!

APPLY_COUNT và điểm nối của hai lô

Chúng tôi bắt đầu phân tích mã theo từng dòng theo nghĩa đen và phát hiện ra một điểm quan trọng: dữ liệu được chuyển từ bảng nhật ký sang bảng mới theo lô, hằng số APPLY_COUNT cho biết kích thước của lô:

for (;;)
{
num = apply_log(connection, table, APPLY_COUNT);

if (num > MIN_TUPLES_BEFORE_SWITCH)
     continue;  /* there might be still some tuples, repeat. */
...
}

Vấn đề là dữ liệu từ giao dịch ban đầu, trong đó một số thao tác có thể vi phạm ràng buộc, khi được truyền, có thể kết thúc ở điểm giao nhau của hai đợt - một nửa số lệnh sẽ được thực hiện trong đợt đầu tiên và nửa còn lại trong lần thứ hai. Và ở đây, tùy thuộc vào vận may của bạn: nếu các đội không vi phạm bất cứ điều gì trong đợt đầu tiên thì mọi thứ đều ổn, nhưng nếu vi phạm thì sẽ xảy ra lỗi.

APPLY_COUNT bằng 1000 bản ghi, điều này giải thích tại sao các thử nghiệm của chúng tôi thành công - chúng không bao gồm trường hợp "giao tiếp hàng loạt". Chúng tôi đã sử dụng hai lệnh - chèn và cập nhật, vì vậy chính xác 500 giao dịch của hai lệnh luôn được đặt thành một đợt và chúng tôi không gặp bất kỳ sự cố nào. Sau khi thêm bản cập nhật thứ hai, bản chỉnh sửa của chúng tôi ngừng hoạt động:

FOR i IN 1..10000 LOOP
  BEGIN
    INSERT INTO test_table VALUES (1) RETURNING id INTO v_id;
    UPDATE test_table set val = i where id = v_id;
    UPDATE test_table set val = i where id = v_id; -- one more update
    COMMIT;
  END;
END LOOP;

Vì vậy, nhiệm vụ tiếp theo là đảm bảo rằng dữ liệu từ bảng gốc, đã được thay đổi trong một giao dịch, cũng sẽ xuất hiện trong bảng mới trong một giao dịch.

Từ chối từ đợt

Và một lần nữa chúng tôi có hai giải pháp. Đầu tiên: hãy từ bỏ hoàn toàn việc phân vùng thành từng đợt và truyền dữ liệu trong một giao dịch. Ưu điểm của giải pháp này là tính đơn giản - số lượng thay đổi mã cần thiết là tối thiểu (nhân tiện, trong các phiên bản cũ hơn, pg_reorg hoạt động chính xác như vậy). Nhưng có một vấn đề - chúng tôi đang tạo ra một giao dịch kéo dài và điều này, như đã nói trước đây, là mối đe dọa cho sự xuất hiện của một đợt phình to mới.

Giải pháp thứ hai phức tạp hơn nhưng có lẽ đúng hơn: tạo một cột trong bảng nhật ký với mã định danh của giao dịch đã thêm dữ liệu vào bảng. Sau đó, khi sao chép dữ liệu, chúng tôi có thể nhóm dữ liệu theo thuộc tính này và đảm bảo rằng các thay đổi liên quan sẽ được chuyển cùng nhau. Lô sẽ được hình thành từ một số giao dịch (hoặc một giao dịch lớn) và kích thước của nó sẽ thay đổi tùy thuộc vào lượng dữ liệu đã được thay đổi trong các giao dịch này. Điều quan trọng cần lưu ý là vì dữ liệu từ các giao dịch khác nhau được đưa vào bảng nhật ký theo thứ tự ngẫu nhiên nên sẽ không thể đọc nó một cách tuần tự như trước đây nữa. seqscan cho mỗi yêu cầu có tính năng lọc theo tx_id quá đắt, cần có chỉ mục, nhưng nó cũng sẽ làm chậm phương thức do chi phí cập nhật nó. Nói chung, như mọi khi, bạn cần phải hy sinh một cái gì đó.

Vì vậy, chúng tôi quyết định bắt đầu với tùy chọn đầu tiên vì nó đơn giản hơn. Đầu tiên, cần phải hiểu liệu một giao dịch kéo dài có phải là vấn đề thực sự hay không. Vì quá trình chuyển dữ liệu chính từ bảng cũ sang bảng mới cũng diễn ra trong một giao dịch dài nên câu hỏi được chuyển thành “chúng ta sẽ tăng giao dịch này lên bao nhiêu?” Thời lượng của giao dịch đầu tiên phụ thuộc chủ yếu vào kích thước của bảng. Thời lượng của một cái mới phụ thuộc vào số lượng thay đổi tích lũy trong bảng trong quá trình truyền dữ liệu, tức là. về cường độ của tải. Quá trình chạy pg_repack diễn ra trong thời gian tải dịch vụ tối thiểu và khối lượng thay đổi nhỏ một cách không tương xứng so với kích thước ban đầu của bảng. Chúng tôi quyết định rằng chúng tôi có thể bỏ qua thời gian của một giao dịch mới (để so sánh, trung bình là 1 giờ 2-3 phút).

Các thí nghiệm đều tích cực. Bắt đầu sản xuất quá. Để rõ ràng, đây là hình ảnh có kích thước của một trong các cơ sở dữ liệu sau khi chạy:

Postgres: các ràng buộc phình to, pg_repack và trì hoãn

Vì chúng tôi hoàn toàn hài lòng với giải pháp này nên chúng tôi đã không thử triển khai giải pháp thứ hai mà đang xem xét khả năng thảo luận về nó với các nhà phát triển tiện ích mở rộng. Rất tiếc, bản sửa đổi hiện tại của chúng tôi vẫn chưa sẵn sàng để xuất bản vì chúng tôi chỉ giải quyết được vấn đề với các hạn chế trì hoãn duy nhất và để có một bản vá chính thức, cần phải cung cấp hỗ trợ cho các loại khác. Chúng tôi hy vọng có thể làm được điều này trong tương lai.

Có lẽ bạn có một câu hỏi, tại sao chúng tôi thậm chí còn tham gia vào câu chuyện này với việc sửa đổi pg_repack, và chẳng hạn như không sử dụng các chất tương tự của nó? Tại một thời điểm nào đó, chúng tôi cũng đã nghĩ về điều này, nhưng trải nghiệm tích cực khi sử dụng nó trước đó, trên các bảng không có ràng buộc trì hoãn, đã thúc đẩy chúng tôi cố gắng hiểu bản chất của vấn đề và khắc phục nó. Ngoài ra, việc sử dụng các giải pháp khác cũng cần có thời gian để tiến hành thử nghiệm, vì vậy chúng tôi quyết định rằng trước tiên chúng tôi sẽ cố gắng khắc phục sự cố trong đó và nếu chúng tôi nhận ra rằng chúng tôi không thể thực hiện việc này trong thời gian hợp lý thì chúng tôi sẽ bắt đầu xem xét các giải pháp tương tự. .

Những phát hiện

Những gì chúng tôi có thể đề xuất dựa trên kinh nghiệm của bản thân:

  1. Theo dõi sự đầy hơi của bạn. Dựa trên dữ liệu giám sát, bạn có thể hiểu tính năng tự động hút chân không được cấu hình tốt như thế nào.
  2. Điều chỉnh AUTOVACUUM để giữ độ phồng ở mức chấp nhận được.
  3. Nếu tình trạng sưng tấy vẫn ngày càng gia tăng và bạn không thể khắc phục nó bằng các công cụ sẵn có, đừng ngại sử dụng các tiện ích mở rộng bên ngoài. Điều chính là kiểm tra mọi thứ tốt.
  4. Đừng ngại sửa đổi các giải pháp bên ngoài cho phù hợp với nhu cầu của bạn - đôi khi điều này có thể hiệu quả hơn và thậm chí còn dễ dàng hơn việc thay đổi mã của riêng bạn.

Nguồn: www.habr.com

Thêm một lời nhận xét