- Trong thử nghiệm Jepsen, đã xác nhận trường hợp cụm Amazon RDS for PostgreSQL Multi-AZ không tuân thủ Snapshot Isolation, mức cô lập mạnh nhất xét trên toàn bộ các node
- Nguyên nhân cốt lõi là thứ tự hiển thị giao dịch trên primary được quyết định bằng khóa trong bộ nhớ, trong khi secondary tuân theo thứ tự WAL, khiến hai thứ tự này có thể lệch nhau
- Ngay cả trong điều kiện dùng storage
gp3và instancedb.m6id.largemà không tiêm lỗi hay failover, G-nonadjacent cycle vẫn xuất hiện vài phút một lần ở mức khoảng 150 write TPS / 1600 read-only TPS - Hiện tượng bất thường này thuộc loại Long Fork, xuất hiện trong mọi phiên bản được thử nghiệm từ PostgreSQL 13.15 đến 17.4 do AWS hỗ trợ; không quan sát thấy Short Fork/Write Skew
- Với các giao dịch quan trọng về an toàn, khi dùng read-only secondary có thể nhìn thấy thứ tự thực thi khác nhau, nên cần cân nhắc chỉ dùng writer endpoint hoặc đưa vào ít nhất 1 thao tác write
Cập nhật nguyên nhân Long Fork
- Sergey Melnik của AWS cùng các thành viên bình luận trên HN là matashii và Ants Aasma đã xác định nguyên nhân Long Fork trong cụm PostgreSQL
- PostgreSQL primary quyết định thứ tự làm cho giao dịch trở nên nhìn thấy được bằng khóa trong bộ nhớ
- Secondary làm cho giao dịch trở nên nhìn thấy được theo thứ tự trong Write-Ahead Log (WAL)
- Nếu thứ tự khóa và thứ tự WAL khác nhau, primary và secondary có thể nhìn thấy thứ tự biểu kiến của giao dịch khác nhau
- Hành vi này từng được đề cập trong một bài trên mailing list PostgreSQL năm 2013, và Melnik đã viết bài trên blog AWS giải thích về transaction visibility trong cụm PostgreSQL và read replica
- Jepsen khuyến nghị AWS và PostgreSQL ghi tài liệu về vấn đề này cùng với công việc sửa lỗi
Mức cô lập và kiến trúc của RDS for PostgreSQL
- PostgreSQL là cơ sở dữ liệu SQL mã nguồn mở đa dụng, cung cấp ba mức cô lập giao dịch bằng MVCC
Read UncommittedvàRead Committedđều hoạt động như Read CommittedRepeatable Readthực tế không phải Repeatable Read mà cung cấp Snapshot IsolationSerializablecung cấp Serializability
- Amazon RDS for PostgreSQL là dịch vụ AWS cung cấp cụm PostgreSQL được quản lý
- Tự động hóa provisioning, quản lý storage, replication, backup, upgrade, v.v.
- Multi-AZ deployments phân tán các node cơ sở dữ liệu trên nhiều Availability Zone để giảm khả năng xảy ra lỗi tương quan
- RDS dùng synchronous replication để chỉ phản hồi sau khi độ bền giao dịch đã được bảo đảm trên cả primary và ít nhất 1 instance secondary
- Người dùng được cung cấp hai URL nói PostgreSQL wire protocol
- primary endpoint: dành cho giao dịch read-write
- reader endpoint: dành cho giao dịch read-only
- Primary endpoint hỗ trợ mọi mức cô lập của PostgreSQL, nhưng secondary không hỗ trợ Serializable
- Mức cô lập mạnh nhất có thể dùng trên toàn bộ node là Snapshot Isolation, thứ PostgreSQL gọi là
Repeatable Read
Thiết kế thử nghiệm
- Jepsen đã điều chỉnh thư viện thử nghiệm cho PostgreSQL để phù hợp với Amazon RDS for PostgreSQL, và dùng một wrapper program nhỏ
- Mỗi vòng thử nghiệm provision một cụm RDS bằng API CreateDBCluster của AWS
- Storage là
gp3 - Instance là
db.m6id.large
- Storage là
- Khởi chạy 1 node EC2 để chạy thử nghiệm và cung cấp main endpoint cùng read-only endpoint của cụm RDS
- Không tiêm lỗi và cũng không kích hoạt failover
- Workload chính gồm các giao dịch xử lý danh sách số nguyên duy nhất
- Mỗi danh sách được lưu trong một row duy nhất và được mã hóa dưới dạng trường
TEXTchứa các giá trị phân tách bằng dấu phẩy - Giao dịch đọc danh sách bằng primary key hoặc append số nguyên duy nhất vào danh sách bằng
CONCAT
- Mỗi danh sách được lưu trong một row duy nhất và được mã hóa dưới dạng trường
- Với workload này, Elle checker có thể suy luận phụ thuộc luồng dữ liệu giữa các giao dịch và tìm cycle trong đồ thị để kiểm chứng nhiều mức cô lập
Quan sát G-nonadjacent cycle
- Ngay cả trong điều kiện bình thường và mức concurrency trung bình, Amazon RDS for PostgreSQL 17.4 vẫn cho thấy G-nonadjacent cycle vài phút một lần
- Một lần chạy thử nghiệm 2 phút thực hiện khoảng 150 write TPS và 1600 read-only TPS, bao gồm một cycle 4 giao dịch
- Cycle ví dụ gồm bốn giao dịch
T1,T2,T3,T4T1append9vào row 89 để tạo danh sách[4 9], vàT2quan sát thấy điều nàyT3append11vào row 90 để tạo danh sách[11]T4append3vào row 90 và đọc danh sách kết quả[11, 3], ghi đè version củaT3T2quan sát thấy append củaT1ở row 89 nhưng không thấy append củaT3ở row 90- Ngược lại,
T4quan sát thấy append củaT3ở row 90 nhưng bỏ lỡ append củaT1ở row 89
- Cycle này bao gồm read-write dependency không liền kề với nhau, nên là G-nonadjacent cycle vi phạm Snapshot Isolation
- Trong
Repeatable Readcủa PostgreSQL chuẩn, hành vi như vậy không được xảy ra, và Jepsen không quan sát thấy nó trong PostgreSQL chuẩn
Vì sao xung đột với Snapshot Isolation
- Trong Snapshot Isolation, mọi giao dịch phải trông như hoạt động trên snapshot của cơ sở dữ liệu tại timestamp bắt đầu
s - Hiệu ứng của giao dịch sẽ trở nên nhìn thấy được với các giao dịch khác ở timestamp commit
csau đó - Nếu viết các quan sát trong cycle ví dụ thành quan hệ timestamp, chúng mâu thuẫn với nhau
- Vì
T2đọc append củaT1, thời điểm bắt đầu củaT2phải sau commit củaT1:c1 < s2 - Vì
T2không quan sát thấy append củaT3, nêns2 < c3 - Vì
T4ghi đè và quan sát thấyT3, nênc3 < s4 - Vì
T4không quan sát thấy append củaT1, nêns4 < c1
- Vì
- Các quan hệ này không thể đồng thời đúng, nên xung đột với mô hình timestamp của Snapshot Isolation
Long Fork và kết quả theo phiên bản
- Cycle này cũng là một ví dụ về Long Fork
- Giao dịch thứ nhất và thứ hai tạo thành một fork trạng thái logic
- Giao dịch thứ ba và thứ tư tạo thành fork thứ hai
- Hai fork cập nhật các row khác nhau nhưng không quan sát thấy hiệu ứng của nhau
- Không quan sát thấy Short Fork, tức Write Skew
- Kết quả này gợi ý rằng Amazon RDS for PostgreSQL có thể cung cấp Parallel Snapshot Isolation, yếu hơn Snapshot Isolation một chút
- Các bất thường G-nonadjacent xuất hiện đa dạng, từ trường hợp chỉ được nối bằng write-read edge đến trường hợp bao gồm hơn 4 giao dịch
- Cùng một loại bất thường xảy ra trong mọi phiên bản được thử nghiệm, từ PostgreSQL 13.15, phiên bản cũ nhất AWS hỗ trợ, đến phiên bản mới nhất 17.4
Những điểm người dùng cần kiểm tra
- Vì tồn tại Long Fork và các G-nonadjacent cycle khác, cụm Amazon RDS for PostgreSQL Multi-AZ không bảo đảm Snapshot Isolation
- Về điểm này, cụm RDS for PostgreSQL Multi-AZ cung cấp ngữ nghĩa an toàn yếu hơn PostgreSQL một node, vốn trong thử nghiệm Jepsen trước đây có vẻ cung cấp Strong Snapshot Isolation
- Người dùng có thể xem xét cấu trúc giao dịch của mình có dễ bị Long Fork hay không, hoặc xác minh bằng thử nghiệm rằng các điều kiện bất biến mong muốn có được duy trì không
- Các giao dịch read có thể nhìn thấy kết quả khác nhau so với các giao dịch khác về thứ tự thực thi giao dịch
- Vì hiện tượng bất thường có vẻ liên quan đến truy vấn vào read-only secondary, có thể khôi phục Snapshot Isolation bằng các cách sau
-
Chỉ dùng writer endpoint
- Đưa ít nhất 1 thao tác write vào mọi giao dịch quan trọng về an toàn
- Việc kiểm chứng của Jepsen là cách tiếp cận thực nghiệm; có thể chứng minh sự tồn tại của bug nhưng không thể chứng minh sự vắng mặt của bug
- Báo cáo này không phải là kết quả điều tra chi tiết hành vi của RDS for PostgreSQL, mà là sản phẩm của khảo sát sơ bộ
-
1 bình luận
Các ý kiến trên Hacker News
Ước gì các bài viết trong thế giới phần mềm thường xuyên theo kiểu này hơn: “Amazon RDS for PostgreSQL là một dịch vụ của Amazon Web Services (AWS) cung cấp các instance được quản lý của cơ sở dữ liệu PostgreSQL. Chúng tôi cho thấy các cluster multi-AZ của Amazon RDS for PostgreSQL vi phạm snapshot isolation, mô hình nhất quán mạnh nhất được hỗ trợ trên mọi endpoint…”
Thẳng thắn, đi vào trọng tâm, không tô vẽ, giống cách chia sẻ kết quả nghiên cứu trong các lĩnh vực STEM khác. Có thời tôi thích những bài blog dí dỏm giải thích bằng meme, nhưng giờ tôi lại nhớ kiểu viết plain và đơn giản
Khi tôi viết những bài kỹ thuật rất sâu, gần như không có like hay bình luận, đến mức một Staff Engineer còn nói “nên thu hẹp đối tượng độc giả hơn”. Ngược lại, khi thử nghiệm Kubecost thời kỳ đầu, tôi viết một bài nói rằng các khuyến nghị giúp tiết kiệm chi phí không nhiều và có thể gây vấn đề hiệu năng container; dù đó là bài khá kỹ thuật, bàn về CPU throttling và cgroups, nhưng khi chèn meme vào thì mọi người cực kỳ thích
Sau đó tôi viết một bài khô khan hơn về việc tạo một thư viện ngoài nhỏ cho Python bằng C, truy cập qua ctypes và so sánh cấp phát stack/heap; khi cũng chèn meme vào thì kết quả tương tự. Tôi không thích xu hướng này, nhưng để tiếp cận lượng độc giả rộng thì cũng không biết cách nào khác để tránh. Jensen không nhắm tới nhóm độc giả đó, và lối viết nghiêm ngặt, thuần túy như vậy đáng được vỗ tay
Không có trong tiêu đề và trong bài cũng không thật rõ ràng, nhưng vấn đề này chỉ giới hạn ở multi-AZ cluster, một tính năng tương đối mới của RDS. Nó khác với multi-AZ instance mà nhiều người quen thuộc
multi-AZ instance là tính năng cũ, trong đó DB chính được sao chép đồng bộ sang DB phụ ở một Availability Zone khác, và nếu DB chính lỗi thì RDS sẽ failover sang DB phụ
multi-AZ cluster có hai DB phụ và giao dịch được sao chép đồng bộ tới ít nhất một trong số đó. Khi một DB phụ lỗi hoặc hiệu năng giảm, nó vững chắc hơn multi-AZ instance, và cũng cho phép truy cập chỉ đọc vào DB phụ
Tuy nhiên multi-AZ cluster hẳn có thêm ma thuật bổ sung bên trong, không phải chức năng cơ bản của PostgreSQL, và có lẽ vì thế mà nó thất bại trong bài kiểm thử Jepsen
Tuy nhiên PostgreSQL vẫn còn một khiếm khuyết có thể tạo ra vấn đề tương tự mẫu này. Một giao dịch không được sao chép mà client biến mất trong lúc commit sẽ lập tức trở nên visible. Trong ví dụ, nếu T1 xảy ra trên leader bị tách mạng rồi kết nối bị ngắt trong lúc commit, T2 cũng xảy ra trên node bị tách, còn T3/T4 xảy ra về sau trên leader mới, thì có thể thấy cùng kết quả. Nhưng điều này không khớp lắm với mô tả rằng bài kiểm thử này không dùng fault injection
Sửa: Tôi đã không thấy bài viết giải thích mẫu này bằng sự không khớp thứ tự commit giữa replica và node chính. Hơi ngượng vì tôi từng trình bày cách sửa vấn đề này
Điều tra tốt. Dạo này nhiều lập trình viên phần mềm thậm chí còn không hiểu rõ bản thân transaction, chứ chưa nói đến các mô hình transaction khác nhau. Tôi còn từng gặp cả những lập trình viên CRUD được gọi là “senior developer” mà hoàn toàn không biết gì về transaction trong cơ sở dữ liệu.
Thực tế, khi có quy mô traffic và phần mềm giải quyết vấn đề không hề tầm thường, transaction và mô hình transaction rất quan trọng đối với hiệu năng và code không lỗi.
Ví dụ, trong một dự án lớn, sau nhiều phân tích, chúng tôi đã chuyển từ Read Committed mặc định của SQL Server sang Read Committed Snapshot Isolation, và tình trạng tranh chấp lock gần như biến mất, khiến người dùng rất hài lòng. Các kỹ sư phần mềm của dự án đó dùng transaction rất nhiều, nhưng trước khi được dạy các kiến thức cơ bản thì họ hoàn toàn không biết gì về mô hình transaction hay lock.
Vì chủ yếu làm trong lĩnh vực bán lẻ, tôi thường thấy các hệ thống đầy lỗi giống race condition; càng tiếc hơn vì những isolation level này có thể giúp ích rất nhiều trong các phần đó.
Tuy nhiên, những trường hợp như vậy chủ yếu thấy ở kỹ sư startup, còn các lập trình viên Oracle/MSSQL điển hình ở doanh nghiệp lớn thì ít nhất cũng nắm đúng kiến thức cơ bản, nên tôi đánh giá khá cao.
Trong sự nghiệp, vài lần tôi đã thấy cách này dẫn đến kết quả thật sự tệ.
Tuy nhiên, điểm cần chú ý khi chuyển đổi là mọi code phụ thuộc vào blocking read sẽ bị hỏng. Ví dụ, những đoạn như
select with existsphải được viết lại bằng explicit lock hoặc cách khác.Ở công ty cũ, khi đổi lệnh
pg_dumptrong script backup để bắt đầu dùng worker song song (cờ-j), trong lúc restore đôi khi hiếm gặp các lỗi ám chỉ bất nhất như lỗi trùng khóa và lỗi ràng buộc khóa ngoại.Khi đó tôi đã định báo cho AWS và mailing list PostgreSQL, nhưng không thể tái hiện dễ dàng nên không tiến triển được, cuối cùng bỏ cuộc và quay lại dump đơn luồng. Tôi tò mò không biết hiện tượng tôi thấy lúc đó có liên quan đến vấn đề này không.
Đọc bài này thì có vẻ tác động thực tế là khi một lần ghi vừa xảy ra trên cùng một hàng rồi ngay sau đó đọc rất nhanh, có thể trả về dữ liệu cũ. Transaction ghi được đánh dấu là hoàn tất, nhưng trước khi toàn bộ lớp phân tán của multi-AZ RDS instance được cập nhật hoàn toàn, nếu đọc ngay cùng hàng đó thì hàng có thể chưa tồn tại hoặc cột chưa được cập nhật đầy đủ, nên giá trị cũ được trả về.
Theo cơ chế snapshot của PostgreSQL, có vẻ điều này không có nghĩa là chỉ một phần byte của kiểu cột nhiều byte được cập nhật rồi đọc ra một giá trị vô nghĩa.
Rốt cuộc nó trông giống một race condition sẽ hội tụ theo thời gian. Hoặc tôi tò mò liệu có ai đọc thành nghĩa rằng các transaction về sau trong “long fork” có thể vĩnh viễn không hoàn tất ngay cả trong tình huống bình thường hay không.
Câu “Công việc này được Jepsen thực hiện độc lập mà không có thù lao” là nội dung mà các bên liên quan đến RDBMS sẽ không muốn thấy ngay cả vào một ngày đẹp trời. Chắc hẳn nội bộ đã có vài email bày tỏ lo ngại qua lại. Như mọi khi, xin bày tỏ sự kính trọng với aphyr.
Chưa hoàn toàn rõ liệu đây có phải là vấn đề trong các cụm PostgreSQL upstream nhiều instance hay không. Không biết có đúng khi hiểu rằng AWS đã làm gì đó trong cấu hình cụm, hoặc thêm một bản vá gây ra hành vi này hay không
Sao chép PostgreSQL nhìn chung có nhiều cách khác nhau và kết quả cũng khác nhau. Ví dụ có báo cáo Patroni của Bin Wang: https://www.binwang.me/2024-12-02-PostgreSQL-High-Availabili...
Điều được phát hiện ở đây cũng là PostgreSQL hiện không cung cấp hành vi snapshot nhất quán giữa node primary và replica. Có lẽ giao dịch chỉ đọc T2 đã chạy trên node secondary, còn các giao dịch thay đổi T1/T3/T4 chạy trên node primary
Về bối cảnh, snapshot trên node PostgreSQL secondary dựa vào thứ tự bền vững của giao dịch, tức vị trí bản ghi commit trong WAL, để quyết định giao dịch nào nhìn thấy được. Trong khi đó, thứ tự hiển thị trên node primary được quyết định bởi thời điểm backend đã phê duyệt giao dịch lần đầu nhận thông báo rằng giao dịch đã commit hoàn toàn, và thời điểm sau đó nó đánh dấu commit
Trên từng node primary và secondary, thứ tự commit giữa các backend được kết nối là nhất quán, nhưng thứ tự commit giữa primary và secondary có thể hơi khác nhau. Đang có công việc để cải thiện điều này, nhưng vẫn còn ở giai đoạn rất đang tiến hành
Có vẻ AWS đã vá PostgreSQL để sao chép sang hai instance, và xử lý là đủ nếu một trong hai xác nhận thay đổi. Việc xác nhận này xảy ra khi nào không phải thông tin công khai
Cá nhân tôi cho rằng với PostgreSQL, sao chép ở cấp filesystem kiểu drbd sẽ tốt hơn. Các instance AWS Multi-AZ kiểu cũ có lẽ dùng cách này. Tuy nhiên throughput sẽ thấp hơn và không thể đọc từ instance secondary
Đặc biệt là đoạn này: https://youtu.be/fLqJXTOhUg4?t=434
Tiêu đề được gửi lên đang hỏi đúng trọng tâm. RDS for PostgreSQL 17.4 không triển khai snapshot isolation một cách đúng đắn
Các cuộc thảo luận có thể khá căng về việc tiêu đề có quá khắt khe với hệ thống hay không, quá ưu ái hay không, có nêu được vấn đề ý nghĩa nhất trong hơn chục vấn đề đã phát hiện hay không, có công bằng theo tiêu chuẩn Jepsen muốn trở thành bên trung gian trung thực cho các kết quả an toàn dữ liệu của database hay không, và 10 năm sau khi mọi người vẫn tiếp tục link nhưng không còn áp dụng cho phiên bản mới thì nên hiểu thế nào
Sau vài lần thử gây bực bội, họ đang tránh vấn đề này bằng chính sách đặt mọi tiêu đề báo cáo theo dạng “Jepsen: ”. Nếu HN muốn văn bản link mang tính mô tả hơn hoặc nhiều màu sắc hơn thì tất nhiên có thể tự chọn
Dù vậy, vì đây là bài của Kyle Kingsbury, kiểu Chuck Norris của các bảo đảm giao dịch, AWS cần trả lời hoặc giải thích. Ngay cả khi có vẻ nó chỉ áp dụng cho cụm multi-AZ, một trong hai lựa chọn của RDS cho PostgreSQL, cũng vậy. Triển khai Multi-AZ có thể có một hoặc hai instance DB standby, và ở đây là về cấu hình có hai instance DB standby
Tài liệu AWS không có cam kết như vậy. Bộ hướng dẫn RDS dài 5494 trang cũng hầu như chỉ nhắc đến isolation hoặc serializable trong tài liệu tham số của từng engine
Cũng không có gì về tính nhất quán đọc toàn cục của cụm Multi-AZ. Dù là sao chép bán đồng bộ, writer chờ xác nhận bản ghi log từ một standby, nhưng hai reader có thể đang ở trên các snapshot khác nhau
[1] - "New Amazon RDS for MySQL & PostgreSQL Multi-AZ Deployment Option: Improved Write Performance & Faster Failover" - https://aws.amazon.com/blogs/aws/amazon-rds-multi-az-db-clus...
[2] - "Amazon RDS Multi-AZ with two readable standbys: Under the hood" - https://aws.amazon.com/blogs/database/amazon-rds-multi-az-wi...
Nếu lập trình viên giả định snapshot isolation nhưng Amazon RDS for PostgreSQL thực tế chỉ cung cấp parallel snapshot isolation, thì tôi tò mò sẽ có những lỗi an toàn hay lỗi ở cấp ứng dụng nào có thể phát sinh, đặc biệt trong cấu hình multi-AZ dùng endpoint read replica
git push. Bắt đầu transaction, đọc trạng thái hiện tại rồi kiểm tra xem có khớp với trạng thái kỳ vọng không, ghi trạng thái mới, rồi commit kèm hash của trạng thái mới. Trong trường hợp không may, có thể sinh ra một commit hash không khớp với bất kỳ trạng thái hợp lệ nàoChính việc khó suy luận về những thứ như vậy khiến vấn đề khó tránh. Vì thế giải pháp dễ nhất có lẽ gần với việc: nếu write phụ thuộc vào điều kiện từ read, thì “có thể khôi phục snapshot isolation bằng cách chỉ dùng writer endpoint”
Tuy vậy, khá bất ngờ là cách “chỉ dùng writer endpoint” này chưa được kiểm thử, nhất là trong tình huống mất tính sẵn sàng
User1 đăng bình luận, rồi User2 đăng bình luận; sau đó User1 trong một transaction riêng kiểm tra thấy chỉ có 1 bình luận nên nhận badge. User2 cũng thực hiện cùng kiểm tra trong một transaction riêng, chỉ thấy 1 bình luận của mình và cũng có thể nhận badge
Với snapshot isolation, điều này là không thể. Ít nhất một trong các transaction riêng đó phải thấy 2 bình luận
Bài báo gốc về parallel snapshot cũng đáng đọc: https://scispace.com/pdf/transactional-storage-for-geo-repli...
Khi thấy câu “hiện tượng này xảy ra trên tất cả các phiên bản đã thử nghiệm, từ 13.15 đến 17.4”, tôi đã lo rằng việc nâng major version là một lựa chọn sai lầm, nhưng có vẻ không phải vậy. Đây không phải là regression, mà gần với một yêu cầu tính năng hoặc một bug đã tồn tại từ lâu hơn