Một trong những nguyên lý hướng dẫn cách chúng tôi xây dựng giải pháp tại Amazon là: Tránh bước vào những cánh cửa một chiều. Điều này có nghĩa là chúng tôi tránh xa các lựa chọn khó đảo ngược hoặc mở rộng. Chúng tôi áp dụng nguyên lý này trong tất cả các bước phát triển phần mềm—từ thiết kế sản phẩm, tính năng, API, hệ thống backend cho đến việc triển khai. Trong bài viết này, tôi sẽ mô tả cách chúng tôi áp dụng nguyên lý này vào việc triển khai phần mềm.

Việc triển khai sẽ chuyển môi trường phần mềm từ trạng thái (phiên bản) này sang trạng thái (phiên bản) khác. Phần mềm có thể hoạt động một cách hoàn hảo ở một trong hai trạng thái đó. Tuy nhiên, phần mềm có thể không hoạt động tốt trong hoặc sau quá trình chuyển đổi tiến (nâng cấp hoặc tăng tiến) hoặc chuyển đổi lùi (hạ cấp hoặc quay lui). Phần mềm không hoạt động tốt sẽ dẫn đến gián đoạn dịch vụ và khiến dịch vụ nên không đáng tin cậy đối với khách hàng. Trong bài viết này, tôi giả sử rằng cả hai phiên bản phần mềm đều hoạt động tốt như mong đợi. Tôi tập trung vào cách để đảm bảo việc chuyển đổi tiến hoặc lùi trong quá trình triển khai không xảy ra lỗi.

Trước khi phát hành phiên bản phần mềm mới, chúng tôi thực hiện kiểm thử trong môi trường kiểm thử beta hoặc gamma trên nhiều khía cạnh như hoạt động, tính đồng thời, hiệu suất, quy mô và xử lý lỗi xuôi chiều. Kiểm thử này giúp chúng tôi phát hiện mọi vấn đề phát sinh trong phiên bản mới và khắc phục chúng. Tuy nhiên, việc này không phải lúc nào cũng đủ để đảm bảo triển khai thành công. Chúng tôi có thể gặp phải các trường hợp bất ngờ hoặc hành vi phần mềm chưa tối ưu trong môi trường sản xuất. Tại Amazon, chúng tôi muốn tránh đặt mình vào tình huống khi việc quay lui triển khai có thể gây ra lỗi cho khách hàng. Để tránh tình trạng này xảy ra, chúng tôi phải sẵn sàng cho việc quay lui trước mỗi lần triển khai. Phiên bản phần mềm có thể được quay lui mà không gây ra lỗi hoặc gián đoạn cho chức năng khả dụng trong phiên bản trước được gọi là tương thích ngược. Chúng tôi lập kế hoạch và xác nhận phần mềm của mình tương thích ngược với mọi bản sửa đổi.

Trước khi tôi trình bày chi tiết về cách Amazon tiếp cận các bản cập nhật phần mềm, hãy thảo luận một số điểm khác biệt giữa triển khai phần mềm độc lập và phần mềm phân tán.

Triển khai phần mềm độc lập so với phần mềm phân tán

Đối với phần mềm độc lập hoạt động dưới dạng một quy trình trên một thiết bị, việc triển khai là triển khai nguyên tử. Hai phiên bản của phần mềm không bao giờ hoạt động đồng thời. Nếu phần mềm độc lập duy trì trạng thái thì phiên bản mới phải đọc (nghĩa là hủy nối tiếp hóa) dữ liệu được ghi (nghĩa là được nối tiếp hóa) bởi phiên bản cũ và ngược lại. Đáp ứng điều kiện này sẽ giúp việc triển khai trở nên an toàn khi chuyển đổi tiến và lùi.
 
Trong một hệ thống phân tán, việc triển khai có phần phức tạp hơn. Việc triển khai được thực hiện thông qua các bản cập nhật để tính khả dụng không bị ảnh hưởng. Phiên bản mới được triển khai cùng lúc trên một tập hợp con các máy chủ để những máy chủ còn lại có thể tiếp tục phục vụ các yêu cầu. Thông thường, các máy chủ này giao tiếp với nhau thông qua một lệnh gọi thủ tục từ xa (RPC) hoặc trạng thái nhất quán chia sẻ (ví dụ: siêu dữ liệu hoặc các điểm kiểm tra). Cách giao tiếp hoặc trạng thái chia sẻ như vậy có thể tạo ra những thách thức mới. Trình ghi và trình đọc có thể chạy các phiên bản phần mềm khác nhau. Kết quả là chúng có thể diễn giải dữ liệu theo cách khác nhau. Trình đọc thậm chí có thể không đọc được toàn bộ dữ liệu, dẫn đến sự cố ngừng hoạt động.

Các vấn đề đối với thay đổi trong giao thức

Chúng tôi nhận thấy lý do phổ biến nhất cho việc không thể quay lui là thay đổi trong giao thức. Ví dụ như một thay đổi về mã bắt đầu nén và lưu dữ liệu vào ổ đĩa cùng lúc. Sau khi phiên bản mới ghi một số dữ liệu nén, bạn không thể quay lui được nữa. Phiên bản cũ không biết mình phải giải nén dữ liệu sau khi đọc từ ổ đĩa. Nếu dữ liệu được lưu trữ trong blob hoặc kho tài liệu thì các máy chủ khác sẽ không đọc được dữ liệu đó, ngay cả khi quá trình triển khai đang diễn ra. Nếu dữ liệu này được truyền giữa hai quy trình hoặc máy chủ thì bên nhận sẽ không đọc được dữ liệu.

Đôi khi, các thay đổi trong giao thức có thể rất tinh vi. Ví dụ: Hãy xem xét hai máy chủ giao tiếp không đồng bộ qua một kết nối. Để cho nhau biết cả hai vẫn tồn tại, chúng đồng ý gửi một tín hiệu cho nhau mỗi năm giây. Nếu một máy chủ không nhận được tín hiệu trong thời gian quy định, nó sẽ giả định rằng máy chủ kia đã ngừng hoạt động và ngắt kết nối.

Bây giờ, hãy xem xét một triển khai khiến chu kỳ gửi tín hiệu tăng lên 10 giây. Cam kết mã trông thì rất nhỏ—chỉ là thay đổi về con số. Tuy nhiên, lúc này lựa chọn chuyển đổi tiến và lùi đều không an toàn. Trong khi triển khai, máy chủ đang chạy phiên bản mới sẽ gửi tín hiệu mỗi 10 giây. Do đó, máy chủ đang chạy phiên bản cũ sẽ không nhận được tín hiệu sau hơn năm giây và chấm dứt kết nối với máy chủ đang chạy phiên bản mới. Trong một nhóm lớn, tình huống này có thể xảy ra với một số kết nối, dẫn đến giảm tính khả dụng.

Những thay đổi tinh vi như vậy rất khó để phân tích qua việc đọc mã hay tài liệu thiết kế. Do đó, chúng tôi xác nhận rõ ràng rằng mỗi triển khai đều an toàn để chuyển đổi tiến và lùi.

Kỹ thuật triển khai hai giai đoạn

Một cách để chúng tôi đảm bảo mình có thể quay lui an toàn là sử dụng kỹ thuật thường được gọi là triển khai hai giai đoạn. Xem xét tình huống giả định sau đây với một dịch vụ quản lý dữ liệu (ghi vào, đọc từ) trên Amazon Simple Storage (Amazon S3). Dịch vụ này chạy trên một nhóm các máy chủ trên nhiều Vùng sẵn sàng để tăng quy mô và tính khả dụng.

Hiện tại, dịch vụ này sử dụng định dạng XML để lưu trữ dữ liệu. Như được thể hiện trên sơ đồ sau trong phiên bản V1, tất cả các máy chủ đều ghi và đọc XML. Vì lý do kinh doanh, chúng tôi muốn lưu trữ dữ liệu ở định dạng JSON. Nếu chúng tôi thực hiện thay đổi này trong một lần triển khai, các máy chủ đã nhận sự thay đổi sẽ ghi bằng JSON. Nhưng các máy chủ khác vẫn chưa biết cách đọc JSON. Tình trạng này gây ra lỗi. Do đó, chúng tôi chia sự thay đổi đó thành hai phần và thực hiện triển khai hai giai đoạn.

Như được thể hiện trên sơ đồ trước, chúng tôi gọi giai đoạn đầu là Chuẩn bị. Trong giai đoạn này, chúng tôi chuẩn bị cho tất cả các máy chủ đọc JSON (bên cạnh XML) nhưng vẫn tiếp tục ghi XML bằng cách triển khai phiên bản V2. Theo quan điểm vận hành, thay đổi này không biến đổi bất cứ điều gì. Tất cả các máy chủ vẫn có thể đọc XML và mọi dữ liệu vẫn được ghi bằng XML. Nếu chúng tôi quyết định quay lui thay đổi này, các máy chủ sẽ trở về tình trạng không thể đọc được JSON. Đây không phải là trở ngại vì chưa có dữ liệu nào được ghi bằng JSON.

Như được thể hiện trên sơ đồ trước, chúng tôi gọi giai đoạn hai là Kích hoạt. Trong giai đoạn này, chúng tôi kích hoạt các máy chủ để sử dụng định dạng JSON trong việc ghi bằng cách triển khai phiên bản V3. Khi mỗi máy chủ nhận thay đổi này, máy chủ đó sẽ bắt đầu ghi bằng JSON. Các máy chủ chưa nhận thay đổi này vẫn có thể đọc JSON vì chúng đã được chuẩn bị trong giai đoạn đầu. Nếu chúng tôi quyết định quay lui thay đổi này, tất cả dữ liệu được ghi bởi các máy chủ tạm thời nằm trong giai đoạn Kích hoạt sẽ ở dạng JSON. Dữ liệu được ghi bởi các máy chủ không nằm trong giai đoạn Kích hoạt sẽ ở dạng XML. Tình huống này vẫn ổn vì, như thể hiện trên V2, các máy chủ vẫn có thể đọc cả XML và JSON sau khi quay lui.

Mặc dù sơ đồ trước cho thấy sự thay đổi định dạng nối tiếp hóa từ XML sang JSON nhưng kỹ thuật thông thường có thể áp dụng cho tất cả các tình huống được mô tả ở phần Thay đổi trong giao thức trước đó. Ví dụ: Hãy nhớ lại tình huống trước đó, khi chu kỳ gửi tín hiệu giữa các máy chủ phải tăng từ 5 lên 10 giây. Trong giai đoạn Chuẩn bị, chúng tôi có thể cho mọi máy chủ tạm giãn chu kỳ gửi tín hiệu dự kiến thành 10 giây mặc dù chúng vẫn tiếp tục gửi tín hiệu sau mỗi 5 giây. Trong giai đoạn Kích hoạt, chúng tôi thay đổi tần suất thành mỗi 10 giây.

Các biện pháp đề phòng đối với triển khai hai giai đoạn

Bây giờ, tôi sẽ mô tả các biện pháp đề phòng mà chúng tôi áp dụng trong khi thực hiện kỹ thuật triển khai hai giai đoạn. Mặc dù tôi đã đề cập đến tình huống ví dụ được mô tả trong phần trước nhưng các biện pháp đề phòng này sẽ áp dụng cho hầu hết các triển khai hai giai đoạn.

Nhiều công cụ triển khai cho phép người dùng coi việc triển khai là thành công nếu một số lượng máy chủ tối thiểu nhận thay đổi và tự báo cáo rằng chúng đang ở tình trạng tốt. Ví dụ: AWS CodeDeploy có một cấu hình triển khai được gọi là minimumHealthyHosts.

Một giả định quan trọng trong ví dụ về triển khai hai giai đoạn là ở cuối giai đoạn đầu, tất cả các máy chủ đã được nâng cấp để đọc XML và JSON. Nếu một hoặc nhiều máy chủ không được nâng cấp trong giai đoạn đầu thì chúng sẽ không thể đọc dữ liệu trong và sau giai đoạn hai. Do đó, chúng tôi xác nhận rõ ràng rằng tất cả các máy chủ đã nhận sự thay đổi trong giai đoạn Chuẩn bị.

Khi tôi làm về Amazon DynamoDB, chúng tôi đã quyết định thay đổi giao thức giao tiếp giữa một số lượng lớn máy chủ trải rộng trên nhiều vi dịch vụ. Tôi đã điều phối việc triển khai giữa tất cả các vi dịch vụ để tất cả các máy chủ đạt đến giai đoạn Chuẩn bị trước và sau đó tiến hành giai đoạn Kích hoạt. Như một biện pháp đề phòng, tôi xác nhận rõ ràng rằng việc triển khai đã thành công trên mỗi máy chủ đơn lẻ vào cuối mỗi giai đoạn.

Mặc dù mỗi giai đoạn riêng lẻ đều có thể được quay lui an toàn nhưng chúng tôi không thể quay lui cả hai sự thay đổi. Trong ví dụ trước, vào cuối giai đoạn Kích hoạt, các máy chủ ghi dữ liệu bằng JSON. Phiên bản phần mềm được sử dụng trước khi nhận thay đổi từ giai đoạn Chuẩn bị và Kích hoạt không biết cách đọc JSON. Do đó, để đề phòng, chúng tôi dành ra một khoảng thời gian đáng kể giữa hai giai đoạn Chuẩn bị và Kích hoạt. Chúng tôi gọi đây là giai đoạn bake và giai đoạn này thường kéo dài một vài ngày. Chúng tôi chờ đợi để đảm bảo rằng mình sẽ không phải quay lui về phiên bản trước đó.

Sau giai đoạn Kích hoạt, chúng tôi không thể loại bỏ khả năng đọc XML của phần mềm một cách an toàn. Không thể loại bỏ khả năng này một cách an toàn vì tất cả dữ liệu trước giai đoạn Chuẩn bị đều được ghi bằng XML. Chúng tôi chỉ có thể loại bỏ khả năng đọc XML sau khi chắc chắn rằng mọi đối tượng đơn lẻ đều đã được ghi lại bằng JSON. Chúng tôi gọi quá trình này là lấp lại. Việc này có thể đòi hỏi công cụ bổ sung có thể chạy đồng thời trong khi dịch vụ đang ghi và đọc dữ liệu.

Các biện pháp tốt nhất trong nối tiếp hóa

Hầu hết các phần mềm đều thực hiện việc nối tiếp hóa dữ liệu—cho dù là để lưu trữ hay truyền qua mạng. Logic nối tiếp hóa sẽ thay đổi trong quá trình phát triển. Các thay đổi có thể dàn trải từ việc thêm một trường mới đến thay đổi hoàn toàn định dạng. Sau nhiều năm, chúng tôi đã rút ra các biện pháp tốt nhất để áp dụng cho nối tiếp hóa:

• Chúng tôi thường tránh phát triển các định dạng nối tiếp hóa tùy chỉnh.

Logic ban đầu cho nối tiếp hóa tùy chỉnh có thể không đáng kể và còn cung cấp hiệu suất tốt hơn. Tuy nhiên, các lần lặp lại tiếp theo của định dạng này tạo ra những thách thức đã được giải quyết bằng các framework được thiết lập tốt như JSON, Protocol Buffers, Cap’n Proto và FlatBuffers. Khi được sử dụng một cách thích hợp, các framework này cung cấp các tính năng an toàn như thoát, tương thích ngược và theo dõi sự tồn tại của thuộc tính (nghĩa là theo dõi xem một trường được thiết lập rõ ràng hay được ngầm chỉ định giá trị mặc định).

• Với mỗi thay đổi, chúng tôi chỉ định rõ một phiên bản riêng biệt cho các bộ nối tiếp hóa.

Chúng tôi làm việc này độc lập với mã nguồn hoặc việc tạo phiên bản cho bản dựng. Chúng tôi cũng lưu trữ phiên bản bộ nối tiếp hóa với dữ liệu được nối tiếp hóa hoặc trong siêu dữ liệu. Các phiên bản bộ nối tiếp hóa cũ hơn tiếp tục hoạt động trong phần mềm mới. Chúng tôi nhận thấy việc phát hành số liệu về phiên bản dữ liệu được ghi hoặc đọc thường khá hữu ích. Việc này cung cấp cho người vận hành khả năng quan sát và thông tin về việc khắc phục sự cố nếu phát sinh lỗi. Tất cả những điều này cũng áp dụng cho các phiên bản RPC và API.

• Chúng tôi tránh việc nối tiếp hóa các cấu trúc dữ liệu mà mình không thể kiểm soát.

Ví dụ: Chúng tôi có thể nối tiếp hóa các đối tượng trong bộ dữ liệu Java bằng cách sử dụng phản xạ. Nhưng khi chúng tôi cố gắng nâng cấp JDK, việc triển khai nền tảng các lớp này có thể thay đổi và khiến quá trình hủy nối tiếp hóa thất bại. Rủi ro này cũng áp dụng cho các lớp từ những thư viện được chia sẻ giữa các nhóm.

• Thông thường, chúng tôi thiết kế các bộ nối tiếp hóa chấp nhận sự hiện diện của các thuộc tính không xác định.
 
Nếu khả thi, các bộ nối tiếp hóa của chúng tôi sẽ giữ nguyên những thuộc tính không xác định trong khi ghi lại dữ liệu. Với sự điều tiết này, ngay cả khi máy chủ đang chạy phiên bản phần mềm mới, chứa các thuộc tính dữ liệu mới trong dữ liệu trong khi nối tiếp hóa thì các máy chủ đang chạy phiên bản cũ cũng không loại bỏ các thuộc tính đó trong khi cập nhật cùng loại dữ liệu. Do đó, việc triển khai hai giai đoạn là không cần thiết.

Chúng tôi luôn chia sẻ các biện pháp tốt nhất của mình một cách thận trọng, rằng các hướng dẫn của chúng tôi không áp dụng cho tất cả các ứng dụng và tình huống.

Xác nhận một thay đổi có thể được quay lui an toàn

Thông thường, chúng tôi xác nhận rõ ràng rằng có thể chuyển đối tiến và lùi hoạt động thay đổi phần mềm một cách an toàn thông qua kiểm thử nâng cấp-hạ cấp. Đối với quá trình này, chúng tôi thiết lập một môi trường kiểm thử đại diện cho các môi trường sản xuất. Sau nhiều năm, chúng tôi đã xác định được một vài mẫu hình cần tránh khi thiết lập các môi trường kiểm thử.

Tôi đã trải qua các tình huống khi mà việc triển khai một thay đổi trong quy trình sản xuất dẫn đến lỗi, mặc dù thay đổi đã vượt qua mọi kiểm thử trong môi trường kiểm thử. Có một lần, mỗi dịch vụ trong môi trường kiểm thử chỉ có một máy chủ duy nhất. Do đó, mọi triển khai đều ở dạng nguyên tử, nghĩa là loại trừ đi khả năng chạy các phiên bản phần mềm khác nhau cùng lúc. Bây giờ, ngay cả khi các môi trường kiểm thử không có nhiều lưu lượng như môi trường sản xuất, chúng tôi vẫn sử dụng nhiều máy chủ từ các Vùng sẵn sàng khác nhau cho mỗi dịch vụ, như vậy sẽ giống như trong môi trường sản xuất. Chúng tôi yêu thích sự đơn sơ của Amazon nhưng điều này không áp dụng trong việc đảm bảo chất lượng.

Trong một lần khác, môi trường kiểm thử lại có nhiều máy chủ. Tuy nhiên, việc triển khai đã được thực hiện đồng thời trên tất cả các máy chủ để tăng tốc kiểm thử. Cách tiếp cận này cũng ngăn các phiên bản cũ và mới của phần mềm chạy cùng lúc. Vấn đề đối với chuyển đối tiến không được phát hiện. Chúng tôi hiện sử dụng cùng một cấu hình triển khai cho tất cả các môi trường kiểm thử và sản xuất.

Đối với những thay đổi liên quan đến việc điều phối các vi dịch vụ, chúng tôi duy trì cùng một thứ tự triển khai trên các vi dịch vụ trong môi trường kiểm thử và sản xuất. Tuy nhiên, thứ tự để chuyển đổi tiến và lùi có thể khác nhau. Ví dụ: Chúng tôi thường tuân theo một thứ tự cụ thể trong bối cảnh nối tiếp hóa. Đó là trình đọc trước trình ghi khi chuyển đổi tiến và trình ghi trước trình đọc khi chuyển đổi lùi. Thứ tự phù hợp cũng thường được áp dụng trong môi trường kiểm thử và sản xuất.

Khi thiết lập môi trường kiểm thử giống với môi trường sản xuất, chúng tôi sẽ mô phỏng lưu lượng sản xuất theo cách sát thực nhất. Ví dụ: Chúng tôi tạo và đọc một số bản ghi (hoặc tin nhắn) liên tiếp. Tất cả các API đều được sử dụng liên tục. Sau đó, chúng tôi đưa môi trường qua ba giai đoạn, mỗi giai đoạn kéo dài trong một khoảng thời gian hợp lý để xác định các lỗi tiềm ẩn. Khoảng thời gian này đủ dài để tất cả các API, quy trình backend và tác vụ theo lô được chạy ít nhất một lần.

Đầu tiên, chúng tôi triển khai sự thay đổi cho khoảng một nửa nhóm để đảm bảo phiên bản phần mềm cùng tồn tại. Thứ hai, chúng tôi hoàn thành việc triển khai. Thứ ba, chúng tôi bắt đầu triển khai chuyển đổi lùi và tuân theo các bước tương tự cho đến khi mọi máy chủ đều chạy phần mềm cũ. Nếu không xuất hiện lỗi hay hành vi bất ngờ trong các giai đoạn này thì chúng tôi coi như kiểm thử đã thành công.

Kết luận

Việc đảm bảo rằng chúng tôi có thể quay lui một triển khai mà không gây ra bất kỳ gián đoạn nào cho khách hàng là yếu tố rất quan trọng để khiến dịch vụ trở nên đáng tin cậy. Kiểm thử rõ ràng về khả năng quay lui an toàn giúp loại bỏ sự phụ thuộc vào phân tích thủ công dễ bị lỗi. Khi chúng tôi phát hiện một thay đổi không thể được quay lui một cách an toàn, thông thường chúng tôi có thể chia nhỏ thay đổi đó thành hai thay đổi, mỗi thay đổi đó có thể được chuyển đổi tiến và lùi một cách an toàn.

Đọc thêm

Để biết thêm thông tin về cách Amazon cải thiện tính bảo mật và độ sẵn sàng của dịch vụ trong khi gia tăng sự hài lòng của khách hàng và năng suất của nhà phát triển, hãy xem Phát triển nhanh hơn khi liên tục tạo ra thành phẩm


Giới thiệu về tác giả

Sandeep Pokkunuri là Kỹ sư chính tại AWS. Từ khi gia nhập Amazon vào năm 2011, ông đã làm việc tại nhiều bộ phận dịch vụ bao gồm Amazon DynamoDB và Amazon Simple Queue Service (SQS). Ông hiện đang tập trung vào các công nghệ ML liên quan đến ngôn ngữ con người (ví dụ: ASR, NLP, NLU và Dịch máy) và là kỹ sư chính của Amazon Lex. Trước khi gia nhập AWS, ông đã làm việc tại Google về các vấn đề ML như phát hiện nội dung rác và lạm dụng trên mạng xã hội cũng như phát hiện bất thường trong nhật ký truy cập mạng.

Tiến bước nhanh hơn nhờ phân phối liên tục