Một số giải pháp để xử lý distributed transaction trong hệ thống phân tán

 Transaction và Distributed Transaction là các vấn đề phải đối mặt rất nhiều trong xây dựng các hệ thống lớn Enterprise Software. Nó ảnh hưởng rất lớn tới độ ổn định của hệ thống. Nhưng đây là lại là một vấn đề mà có thể vì điều kiện nên rất nhiều người không để ý khi bắt đầu xây dựng hệ thống. Note này mình tổng hợp lại một số kiến thức và giải pháp cơ bản để xử lý vấn đề này.

1. Thế nào là Transaction?

Đây là một câu hỏi mà rất nhiều người trả lời sai vì không đọc cặn kẽ các định nghĩa về tính chất của một transaction.

Một transaction là một quá trình xử lý từ khi bắt đầu tới khi kết thúc thỏa mãn bốn tính chất ACID. Bốn chữ đó viết tắt của bốn tính chất quan trọng sau:

- Atomicity: tính “nguyên tử” của giao dịch. Nghĩa là mọi giao dịch chỉ thành công khi tất cả các phần thành công All or Nothings.

Consistency: tính nhất quán. Nghĩa là mọi dữ liệu được thao tác đều nhất quán với tất cả các quy tắc (rules), các rằng buộc (constraint)... trong toàn bộ quá trình xử lý từ khi bắt đầu tới khi kết thúc.

- Isolation: tính cô lập. Nó đảm bảo việc thực thi đồng thời của các giao dịch chỉ có thể có kết quả khi các giao dịch được thực hiện tuần tự. Ví dụ: hai giao dịch cùng sửa một table, thì các giao dịch đó phải được thực hiện tuần tự, giao dịch này xong mới tới giao dịch kia.

- Durability: tính bền. Nghĩa là mọi giao dịch khi commit thì kết quả nó phải được đảm bảo, cho dù ứng dụng bị tắt, mất điện server.

Thông thường mọi người chỉ hiểu qua tính chất đầu, thường không để ý các tính chất sau.

Với tính chất đầu thì xét một vị dụ đơn giản sau:

Client gửi một câu lệnh update tới CSDL

CSDL thực thi và trả lại kết quả cho Client

Client nhận kết quả và kết thúc một quá trình xử lý. 

Giả code: VAR Result = EXECUTE “Insert Into Table” PROCESS Result; Như trên có phải là một giao dịch? Câu trả lời là không. Bởi trong trường hợp trên client chỉ yêu cầu DB thực thi một lệnh sửa đổi dữ liệu nhưng không hề yêu cầu tạo một transaction giữa client và DB. Do đó ngay sau khi server nhận được câu lệnh thực thi thì client không còn vai trò gì đối với việc thay đổi dữ liệu của DB nữa rồi. Lúc này, nếu việc trả lại kết quả quá trình xử lý không phụ thuộc vào tính chất client ra sao, cũng như đường truyền giữa client và server ra sao. Cần phải sửa lại đoạn code xử lý trên bằng cạch đặt trong một giao dịch: Begin Transaction VAR Result = EXECUTE “Insert Into Table” PROCESS Result; End Transaction Trong trường hợp này một giao dịch đã được tạo ra giữa client và server. Chỉ khi nào End Transaction được thực thi thì server mới commit dữ liệu. Nghĩa là nếu client không nhận được kết quả thì dữ liệu sẽ không bị thay đổi. Tương tự nếu bạn xây dựng một web service (ví dụ một restful service) để cập nhật DB. Thì khi client gửi một http request cho tới khi client nhận được kết quả server trả về cũng không tạo thành một giao dịch. Tiếp tục phân tích sâu thêm các tính chất khác. Cho dù client và server kết nối bình thường, client không chết hay gặp vấn đề xử lý gì đó thì quá trình xử lý trên vẫn không được đảm bảo. Bởi ngay khi Server kết thúc một quá trình xử lý và dữ liệu đang được trả về, hoặc client đã nhận được kết quả trả về và thực hiện nốt quá trình xử lý để kết thúc thì có thể dữ liệu trên server đã có thể đã bị sửa đổi. Như vậy tính chất consistency không đảm bảo. Đồng thời tính Isolation cũng không được đảm bảo. Còn tính Durability thì có vẻ trực quan hơn nhỉ? Cũng không đơn giản lắm đâu! Cứ cho là client đã đặt đoạn thực thi của mình trong một transaction bao gồm là update CSDL trên server và sau đó khi nhận một kết quả nào đó thì cập nhật cái gì đó ví dụ ghi vào file dữ liệu một dòng log. Nhưng ngay khi ghi xong vào một dòng log thì client bị tắt ngóm. Vậy trên DB server rollback lại dữ liệu về trạng thái ban đầu, nhưng trong dữ liệu log của client lại có thêm một dòng mới. Vây là dữ liệu giữa client và server lại bị lệch và phải rà soát lại. Nếu như đây không phải là một file log mà là một DB khác thì nghiêm trọng đấy nhỉ. Ví dụ bạn cập nhất dữ liệu kho hàng và sau đó xác nhận dữ liệu đơn hàng. Hàng có thể đã không bị trừ thành công nhưng đơn hàng thì vẫn cứ đổi trạng thái. Như vậy cho thấy rằng việc cài đặt transaction không hề đơn giản chút nào. Nó không mấy khi sinh vấn đề khi hệ thống của bạn còn nhỏ, lượng người dùng, lượng tranh chấp dữ liệu không lớn. Nhưng khi hệ thống bạn lớn, lượng thao tác dữ liệu lớn, các giao dịch phức tạp, móc nối với nhau chằng chịt, thì nếu không kiểm soát tốt sẽ khiến mọi thứ bị đảo lộn trên toàn hệ thống. 

Một cái bạn cần lưu tâm nữa là giao dịch không phải chỉ là ở Database, cần phải hiểu giao dịch là từ khi bắt đầu tới khi kết thúc nghĩa nó phải bao gồm cả client và tất cả các node tham gia vào quá trình xử lý đó. Vì khi hệ thống lớn thì việc cập nhật sửa đổi dữ liệu nó nằm rải ra trên rất nhiều node từ client tới server.

Vậy có các giải pháp xử lý nào. Có hai nhóm giai pháp để giải quyết vấn đề này:

Giải pháp giải quyết triệt để, đảm bảo tất cả các tính chất của một transaction

Giải pháp giải quyết không triệt để nhưng vẫn đảm bảo nghiệp vụ chạy đúng trong phần lớn các trường hợp.

1. Two Phase Commit.

Two phase commit là giải pháp duy nhất đảm bảo các tính chất ACID của distributed transaction. Như tên gọi, quá trình thực thi giao dịch sẽ chia làm hai giai đoạn:

Giai đoạn một, Request Commit Phase: giai đoạn này từ client sẽ gửi lệnh ghi tới các resources. Đồng thời ghi client sẽ ghi log Undo và Redo.

Giai đoạn hai, Commint Phase: sau khi nhận được từ response ghi thành công từ tất cả các resources, thì từ client gửi lệnh Commit tới tất cả resources.

Nếu có bất cứ request nào thực thi bị lỗi thì sẽ tiến hành gửi lệnh undo tới tất cả các resources 

Qua đây, các bạn thấy có hai điểm chú ý:

Các resources phải hỗ trợ cơ chế lock trong quá trình chờ lệnh commit từ client

Client phải có khả năng phối hợp quá trình xử lý ở các resources khác nhau. Nó còn gọi là các cordinator. 

Hiện các DB như MySQL, Oracle, SQL Server đều hỗ trợ thực hiện Distributed Transaction – XA qua giao thức two phase commint. Với Microsoft thì có công nghệ MSDTC – Microsoft Distributed Transaction Cordinator. Các framework web service theo chuẩn SOAP cũng hỗ trợ giao thức này. 

Các bạn có thể tìm kiếm thêm với các từ khóa liên quan ở trên, hoặc đọc thêm tại đây: https://en.wikipedia.org/wiki/Two-phase_commit_protocol 

Với phương pháp này, tất cả các tính chất ACID của giao dịch được đảm bảo. Nhưng nó sẽ đòi hỏi các resources phải bị lock trong quá trình xử lý. Điều này sẽ có tác động tới quá trình thiết kế hệ thống khi bạn phải cân bằng giữa ba tính chất CAP của hệ phân tán (tìm hiểu thêm về CAP Theorem).

2. Các giải pháp đảm bảo Eventually Consistency.

Nếu các bạn đã tìm hiểu về nguyên lý CAP, thì các bạn sẽ thấy là chỉ đạt được 2 trên ba tính chất trên. 

Do đó khi đảm bảo khả năng Consistency thì một trong hai tính chất Availability (khả năng hoạt động của hệ thống khi một trong các node bị ngừng hoạt động) và Partion Tolerance (khả năng hoạt động của hệ thống khi đường mạng giữa các node bị đứt) khó đảm bảo. 

Do vậy để có hệ thống hoạt động ổn định cao, và hiệu năng lớn thì người ta thường hi sinh tính chất consistency. 

Như vậy khi thiết kế để giải quyết distributed transaction thì sẽ phải thiết kế sao cho việc không đảm bảo consistency không ảnh hưởng tới tính chính xác của nghiệp vụ. Điều đó có nghĩa là sẽ có khoảng thời gian trạng thái giữa các node trong hệ thống không nhất quán, hay tính chất của hệ thống là eventually consistency.

2.1. Phương pháp lưu log kết quả giao dịch theo transaction id.

Đây là phương pháp cơ bản được sử dụng rộng rãi. Theo đó khi thực hiện một giao dịch, client sẽ gửi đi một transactionid kèm theo. Server sẽ lưu log giao dịch, kết quả thực thi theo transactionid đó. Nếu quá trình trả lại kết quả bị lỗi, client thực hiện lại giao dịch vời cùng transactionid, thì server sẽ trả lại kết quả tương ứng với transactionid đã lưu log đó. Khi cần rollback thì cũng dựa trên transactionid và log để thực hiện các xử lý tương ứng.

Nhưng với cách thiết kế này thì cần lưu tâm hai điểm:

Resources thực sự không bị lock, nên trong quá trình chờ kết quả trả về thì có thể resouces đã bị sửa đổi rồi. Do đó khi thiết kế cần lưu tâm, quản lý chặt chẽ các nghiệp vụ có giao dịch liên quan tới resouces tương ứng. Nếu tồn tại các giao dịch có độ tranh chấp cao, đòi hỏi tính chính xác lớn thì cách thiết kế này không đảm bảo. Trong trường hợp đó buộc phải cài đặt Two Phase Commit.

Việc rollback không đơn giản, nó phụ thuộc vào tính chất nghiệp vụ. Vì nhiều khi trong quá trình client bắt đầu rollback thì thực sự dữ liệu đã bị thay đổi bởi nghiệp vụ khác rồi. Do đó việc rollback lại có thể làm sai hoàn toàn nghiệp vụ, rất khó để truy vết lại. Vì vậy cần hết sức cẩn thận khi thực hiện rollback. Ưu điểm lớn nhất của phương pháp này chính là nó đảm bảo tính chất AP của hệ thống.

2.1. Sử dụng cặp queue Request và Response.

Phương pháp lưu log trên đảm bảo việc eventually consistency, nhưng nó chưa thật sự perfect lắm trong trường hợp xử lý các sự cố: đường mạng bị đứt, rồi client server lúc sống lúc chết. Ví dụ khi client gửi một request tới server xử lý, client gửi xong thì đường truyền bị đứt. Lúc này client nên làm gỉ? Gửi lại để nhận kết quả, hay gửi lệnh rollback cho server? Client không rõ là server có nhận được hay không, đã xử lý hay chưa xử lý, đã xử lý đúng hay sai?

Trong trường hợp đòi hỏi phải có tính chặt chẽ cao hơn thì có thể thiết kế hệ thống để giải quyết distributed transaction bằng hai queue là:

- Queue Request: dùng để lưu các request gửi đi

- Queue Response: dùng để lưu các kết quả xử lý xong. Khi client cần thực hiện một request nào đó thì gửi một request vào queue request, Server sẽ nhận request từ queue request và xử lý, sau đó gửi kết quả vào queue response, Client sẽ lắng nghe queue response để nhận kết quả tương ứng. Bằng cách làm như vậy thì cả client và server không cần tồn tại tại cùng một thời điểm. Client gửi xong, client có thể chết, Server nhận xử lý xong và trả kết quả rồi chết… 

Kết quả quá trình xử lý vẫn được đảm bảo do lưu trữ trong queue. 

Ngoài các vấn đề của việc thiết kế eventually consistency thì có hai điểm cần lưu ý khi thiết kế dạng này là:

Do queue response có chứa rất nhiều kết quả của các giao dịch khác nhau được server trả về, nên client phải có cách lọc lấy chỉ response tương ứng với request gửi đi. 

Như RabbitMQ thì phải tạo queue tạm tương ứng với request gửi đi, khi server nhận được request sẽ nhận được tên queue mà mình sẽ trả lại kết quả. Với một số loại message queue khác Windows Service Bus thì nó có hỗ trợ fillter message theo sessionid. Khi đó chỉ cần filter response kết quả tương ứng với transactionid gửi đi là được.

Do các message queue chứa các request và response nên nó phải có độ ổn định cao, bằng không nếu nó có vấn đề dẫn tới mất dữ liệu request và response thì sẽ rất nguy hiểm. Không thể biết là có giao dịch nào đã xử lý, giao dịch nào chưa, kết quả các giao dịch ra sao… 

Việc phụ hồi hệ thống trong trường hợp sự cố sẽ rất đau đầu. Trên đây là một số giải pháp để giải quyết vấn đề distributed transaction. 

Không có giải pháp nào là tuyệt đối, cần phải hiểu bản chất của transaction, tính chất của các nghiệp vụ mà có sự lựa chọn cho phù hợp. Các giải pháp này đều đã được áp dụng từ lâu trong các hệ thống lớn trên thế giới, cũng như trong hệ thống mình xây dựng.

Nguồn: Kipalog

#ntechdevelopers



Ntech Developers

Programs must be written for people to read, and only incidentally for machines to execute.

Post a Comment

Previous Post Next Post