Tại sao nhà quản trị hệ thống phải xem xét việc cân bằng giữa hiệu năng và độ trong suốt

Ngay khi chúng tôi thiết lập máy chủ thứ hai, các hệ thống phân tán đã trở thành một mô hình quen thuộc ở Amazon. Khi bắt đầu làm việc ở Amazon vào năm 1999, chúng tôi có ít máy chủ đến nỗi có thể đặt mấy cái tên dễ nhớ cho chúng như “fishy” hoặc “online-01”. Tuy nhiên, ngay cả vào năm 1999 thì điện toán phân tán cũng là một lĩnh vực không mấy dễ hiểu. Ngày nay cũng vậy, thách thức đặt ra với các hệ thống phân tán là độ trễ, thay đổi quy mô, tìm hiểu API kết nối mạng, đóng gói (marshalling) và mở gói (unmarshalling) dữ liệu, cùng độ phức tạp của các thuật toán như Paxos. Khi hệ thống nhanh chóng mở rộng và phân tán nhiều hơn, những trường hợp biên từng được coi là lý thuyết lại xảy ra thường xuyên.

Việc phát triển các dịch vụ điện toán phân tán theo nhu cầu, chẳng hạn như mạng điện thoại đường dài đáng tin cậy hoặc dịch vụ Amazon Web Services (AWS) là rất khó khăn. Điện toán phân tán cũng khó hiểu và kém trực quan hơn các loại hình điện toán khác vì hai vấn đề liên quan với nhau. Sự cố đơn lẻ và tính không tất định gây ra các vấn đề có ảnh hưởng lớn nhất đến hệ thống phân tán. Ngoài các sự cố điển hình về điện toán mà hầu như kỹ sư nào cũng từng gặp phải, sự cố trong hệ thống phân tán còn có thể xảy ra theo nhiều cách khác nữa. Tệ hơn nữa là không phải lúc nào ta cũng biết là đang xảy ra sự cố.

Trên Thư viện cho nhà xây dựng của Amazon, chúng tôi giới thiệu cách AWS xử lý các vấn đề phức tạp về phát triển và vận hành, phát sinh từ các hệ thống phân tán. Trước khi đi sâu tìm hiểu các kỹ thuật này trong những bài viết khác, ta nên xem lại các khái niệm giải thích vì sao điện toán phân tán lại khó hiểu đến vậy. Đầu tiên, hãy xem xét các loại hệ thống phân tán.

Thực tế, hệ thống phân tán có nhiều mức độ triển khai khác nhau. Đầu tiên, ta có hệ thống phân tán ngoại tuyến. Đây là các hệ thống xử lý theo lô, các cụm phân tích dữ liệu lớn, hệ thống kết xuất hình ảnh, cuộn gập protein, v.v. Mặc dù quá trình triển khai không hề đơn giản, hệ thống phân tán ngoại tuyến nhận được hầu hết mọi lợi ích của điện toán phân tán (khả năng thay đổi quy mô và dung sai) và gần như không có nhược điểm (các kiểu sự cố phức tạp và tính không tất định).

Tiếp đến là hệ thống phân tán thời gian thực mềm (soft real-time). Đây là những hệ thống quan trọng phải liên tục tạo ra hoặc cập nhật kết quả, nhưng cũng dành ra khá nhiều thời gian để thực thi điều đó. Ví dụ về các hệ thống như vậy bao gồm một số trình dựng chỉ mục tìm kiếm, hệ thống tìm kiếm máy chủ bị suy giảm, các vai trò cho Amazon Elastic Compute Cloud (Amazon EC2), v.v. Trình lập chỉ mục tìm kiếm có thể ở trạng thái ngoại tuyến (tùy vào ứng dụng) từ 10 phút đến nhiều giờ mà không ảnh hưởng quá nhiều đến khách hàng. Các vai trò cho Amazon EC2 phải cập nhật thông tin chứng nhận cho (hầu hết) mọi phiên bản EC2, nhưng phải mất hàng giờ cho việc này bởi thông tin chứng nhận cũ thường không hết hạn nhanh.

Cuối cùng và cũng khó khăn nhất chính là hệ thống phân tán thời gian thực cứng (hard real-time). Đây thường là các dịch vụ yêu cầu/phản hồi được gọi. Tại Amazon, khi nói đến xây dựng hệ thống phân tán, chúng tôi nghĩ ngay đến hệ thống thời gian thực cứng. Tiếc rằng các hệ thống thời gian thực cứng là khó xây dựng chính xác nhất. Lý do là vì các yêu cầu thường đến bất ngờ và đòi hỏi phản hồi nhanh chóng (như khi khách hàng đang chủ động chờ phản hồi). Ví dụ bao gồm máy chủ web nhận yêu cầu (front-end), quy trình đặt hàng, giao dịch bằng thẻ tín dụng, mọi API AWS, liên lạc qua điện thoại, v.v. Bài viết này chủ yếu tập trung vào hệ thống phân tán thời gian thực cứng.

Hệ thống phân tán thời gian thực cứng rất khó hiểu

Trong một phần của bộ truyện tranh Superman, Siêu nhân phải chiến đấu với một bản ngã khác có tên Bizarro. Hắn sống trên một hành tinh (Thế giới Bizarro), nơi mà mọi thứ đều đảo ngược. Bizarro trông khá giống Siêu nhân nhưng lại có bản chất xấu xa. Hệ thống phân tán thời gian thực cứng cũng vậy. Chúng trông có vẻ giống điện toán thông thường, nhưng lại khác về bản chất và cũng gây hại phần nào.

Hệ thống phân tán thời gian thực cứng rất khó hiểu vì một lý do: kết nối mạng yêu cầu/phản hồi. Đó không phải là những thông tin cốt lõi của TCP/IP, DNS, socket hay các giao thức tương tự. Dù khó hiểu nhưng các chủ đề đó cũng giống các vấn đề hóc búa khác trong điện toán.

Điều khiến các hệ thống phân tán thời gian thực cứng rất khó hiểu là: mạng cho phép gửi thông điệp từ miền lỗi này sang miền lỗi khác. Việc gửi một thông điệp nghe thì có vẻ vô thưởng vô phạt. Nhưng trên thực tế, gửi thông điệp chính là khởi điểm khiến mọi thứ trở nên phức tạp hơn bình thường.

Lấy ví dụ đơn giản như sau: hãy nhìn vào đoạn mã sau đây được lấy từ một lần triển khai Pac-Man. Vì chỉ chạy trên một máy nên nó không gửi thông điệp qua bất kỳ mạng nào.

board.move(pacman, user.joystickDirection())
ghosts = board.findAll(":ghost")
for (ghost in ghosts)
  if board.overlaps(pacman, ghost)
    user.slayBy(":ghost")
    board.remove(pacman)
    return

Bây giờ, giả sử ta đang phát triển một phiên bản kết nối mạng của mã này, trong đó trạng thái của đối tượng board được duy trì trên một máy chủ riêng biệt. Mọi lệnh gọi gửi đến đối tượng board, như findAll(), đều dẫn đến việc gửi và nhận thông điệp giữa hai máy chủ.

Mỗi khi thông điệp yêu cầu/phản hồi được gửi giữa hai máy chủ thì lúc nào cũng có 8 bước xảy ra. Để hiểu về mã Pac-Man có kết nối mạng, hãy cùng xem lại thông tin cơ bản về thông điệp yêu cầu/phản hồi. 

Thông điệp yêu cầu/phản hồi trên mạng

Một hành động yêu cầu/phản hồi hai chiều luôn bao gồm các bước giống nhau. Trong sơ đồ sau đây, máy khách CLIENT gửi MESSAGE yêu cầu qua mạng NETWORK đến máy chủ SERVER. Máy chủ này phản hồi bằng thông điệp REPLY cũng qua mạng NETWORK.

Tại sao nhà quản trị hệ thống phải xem xét việc cân bằng giữa hiệu năng và độ trong suốt

Trong trường hợp mọi thứ đều hoạt động trơn tru, các bước sau sẽ xảy ra:

1. ĐĂNG YÊU CẦU: CLIENT đưa MESSAGE yêu cầu lên NETWORK.
2. GỬI YÊU CẦU: NETWORK gửi MESSAGE đến SERVER.
3. XÁC THỰC YÊU CẦU: SERVER xác thực MESSAGE.
4. CẬP NHẬT TRẠNG THÁI MÁY CHỦ: SERVER cập nhật trạng thái dựa trên MESSAGE (nếu cần).
5. ĐĂNG PHẢN HỒI: SERVER đưa REPLY lên NETWORK.
6. GỬI PHẢN HỒI: NETWORK gửi REPLY đến CLIENT.
7. XÁC THỰC PHẢN HỒI: CLIENT xác thực REPLY.
8. CẬP NHẬT TRẠNG THÁI MÁY KHÁCH: CLIENT cập nhật trạng thái dựa trên REPLY (nếu cần).

Từng đó là quá nhiều bước cho một quy trình hai chiều có quy mô nhỏ! Dù vậy, những bước trên là định nghĩa của giao tiếp yêu cầu/phản hồi trên mạng. Bởi vậy, không thể bỏ qua bước nào. Chẳng hạn, không thể bỏ qua bước 1. Máy khách phải tìm cách đưa MESSAGE lên mạng NETWORK. Về mặt vật lý, điều này có nghĩa là gửi gói tin qua bộ điều hợp mạng, từ đó truyền các tín hiệu điện qua dây diện, qua một loạt các bộ định tuyến bao gồm mạng nối CLIENT với SERVER. Bước này tách biệt với bước 2, bởi bước 2 có thể gặp sự cố vì các lý do riêng biệt, chẳng hạn như SERVER đột nhiên mất điện và không thể nhận các gói tin đến. Logic này cũng có thể áp dụng cho các bước còn lại.

Vì vậy, một yêu cầu/phản hồi qua mạng không chỉ cần một bước (hay còn gọi là phương thức) mà đến tám bước. Tệ hơn nữa, như đã nói ở trên, CLIENT, SERVER và NETWORK có thể gặp sự cố riêng biệt. Mã của kỹ sư phải xử lý được bất kỳ bước nào gặp sự cố trong các bước được mô tả ở trên. Điều đó hiếm khi đúng trong kỹ thuật thông thường. Để biết lý do, hãy xem lại biểu thức sau đây, lấy từ phiên bản mã dành cho một máy.

Về mặt kỹ thuật, mã này có thể bị trục trặc vào thời gian chạy theo một số cách bất thường, ngay cả khi bản thân việc triển khai board.find là không có lỗi. Ví dụ: CPU có thể tự động bị quá nhiệt trong thời gian chạy. Nguồn cấp điện cho máy có thể tự động gặp sự cố. Bộ phận cốt lõi có thể bị hỏng hóc. Bộ nhớ có thể bị đầy và có một số đối tượng mà board.find cố gắng tạo nhưng không được. Ngoài ra, ổ đĩa trên máy có thể bị đầy dữ liệu và board.find không cập nhật được tệp thống kê nào đó nên sẽ trả về lỗi, ngay cả khi đáng ra không nên làm vậy. Tia gamma có thể chiếu vào máy chủ và thay đổi một bit trong RAM. Dù vậy, thường thì các kỹ sư không bận tâm đến những điều này. Chẳng hạn, quá trình kiểm thử đơn vị không bao giờ có kịch bản “nếu CPU gặp sự cố” và chỉ đôi khi tính đến kịch bản hết bộ nhớ.

Trong kỹ thuật thông thường, những kiểu sự cố này chỉ xảy ra trên một máy, tức là một miền lỗi. Ví dụ, nếu phương thức board.find không thực hiện được do CPU tự động nóng lên, thì có thể nhận định rằng toàn bộ máy đã ngừng hoạt động. Ngay cả về mặt khái niệm thì lỗi đó cũng không xử lý được. Có thể đưa ra nhận định tương tự về các kiểu lỗi khác được nêu ở trên. Bạn có thể viết một số trường hợp kiểm thử nhưng chúng không mấy hữu ích với kỹ thuật thông thường. Nếu những sự cố đó xảy ra thật, thì có thể nhận định rằng toàn bộ hệ thống cũng sẽ gặp sự cố. Trong kỹ thuật, ta gọi đó là mô hình chung số phận (share fate). Mô hình “chung số phận” góp phần giảm đáng kể các kiểu sự cố khác nhau mà một kỹ sư phải xử lý.

Xử lý các kiểu sự cố trong hệ thống phân tán thời gian thực cứng

Các kỹ sư làm việc trên hệ thống phân tán thời gian thực cứng phải kiểm thử tất cả các sự cố mạng, vì máy chủ và mạng không “chung số phận”. Không giống như trường hợp một máy, tức là máy khách vẫn tiếp tục hoạt động khi mạng gặp sự cố. Nếu máy từ xa gặp sự cố, máy khách sẽ hoạt động tiếp.

Để kiểm thử toàn bộ trường hợp sự cố của các bước yêu cầu/phản hồi nêu trên, kỹ sư phải giả định tình huống từng bước không thực hiện được. Đồng thời, họ cũng cần đảm bảo rằng mã (trên cả máy khách lẫn máy chủ) luôn hoạt động chính xác sau sự cố.
Hãy xem xét một hành động yêu cầu/phản hồi hai chiều khi có sự cố:

1. Không thể ĐĂNG YÊU CẦU: MẠNG không thể gửi thông điệp (ví dụ như bộ định tuyến trung gian bị lỗi vào sai thời điểm) hoặc MÁY CHỦ hoàn toàn từ chối thông điệp.
2. Không thể GỬI YÊU CẦU: NETWORK gửi thành công MESSAGE đến SERVER nhưng SERVER bị lỗi ngay sau khi nhận MESSAGE.
3. Không thể XÁC THỰC YÊU CẦU: SERVER quyết định MESSAGE không hợp lệ. Điều này có thể do bất cứ nguyên nhân nào. Ví dụ: gói tin bị hỏng, phiên bản phần mềm không tương thích, lỗi trên máy khách hoặc máy chủ.
4. Không thể CẬP NHẬT TRẠNG THÁI MÁY CHỦ: SERVER cố gắng cập nhật trạng thái nhưng không được.
5. Không thể ĐĂNG PHẢN HỒI: Bất kể phản hồi thành công hay thất bại, SERVER không thể đăng phản hồi. Ví dụ: thẻ mạng có thể bị quá nhiệt vào sai thời điểm.
6. Không thể GỬI PHẢN HỒI: NETWORK có thể không gửi được REPLY cho CLIENT như đã nêu ở trên, mặc dù NETWORK vẫn hoạt động trong bước trước đó.
7. Không thể XÁC THỰC PHẢN HỒI: CLIENT quyết định REPLY không hợp lệ.
8. Không thể CẬP NHẬT TRẠNG THÁI MÁY KHÁCH: CLIENT có thể nhận được thông điệp REPLY nhưng không thể cập nhật trạng thái của mình, không hiểu được thông điệp (do không tương thích) hoặc bị trục trặc vì lý do nào đó.

Những kiểu sự cố này là lý do khiến điện toán phân tán khó hiểu đến vậy. Tôi gọi đó là 8 kiểu sự cố nghiêm trọng. Để tìm hiểu thêm về các kiểu sự cố này, hãy xem lại biểu thức từ mã Pac-Man.

Biểu thức này mở rộng ra các hoạt động phía máy khách sau đây:

1. Đăng thông điệp như {action: "find", name: "pacman", userId: "8765309"} lên mạng, được gửi đến máy Board.
2. Nếu mạng không khả dụng hoặc kết nối với máy Board bị từ chối hoàn toàn, tiến hành báo cáo lỗi. Trường hợp này có phần đặc biệt vì máy khách biết trước rằng máy chủ chắc chắn không nhận được yêu cầu.
3. Chờ phản hồi.
4. Hệ thống hết thời gian chờ nếu không nhận được phản hồi. Trong bước này, hết thời gian chờ cũng có nghĩa kết quả của yêu cầu là UNKNOWN. Việc này có thể xảy ra hoặc không. Máy khách phải xử lý trường hợp UNKNOWN đúng cách.
5. Nếu nhận được phản hồi, xác định xem đó là phản hồi thành công, bị lỗi hay bị hỏng/không hiểu được.
6. Nếu đó là lỗi, mở dữ liệu của phản hồi và biến nó thành một đối tượng mà mã hiểu được.
7. Nếu phản hồi bị lỗi hoặc không hiểu được, đưa ra trường hợp ngoại lệ.
8. Bất cứ quy trình nào xử lý trường hợp ngoại lệ này đều phải xác định xem nên thử lại yêu cầu hay bỏ qua và dừng lại.

Biểu thức cũng bắt đầu các hoạt động phía máy chủ sau đây:

1. Nhận yêu cầu (bước này có thể không xảy ra).
2. Xác thực yêu cầu.
3. Tra cứu thông tin người dùng xem còn hoạt động không. (Máy chủ có thể đã bỏ qua người dùng vì không nhận được thông điệp nào từ phía họ trong thời gian quá dài.)
4. Cập nhật bảng duy trì hoạt động cho người dùng để máy chủ biết là họ (có lẽ) vẫn hoạt động.
5. Tra cứu thông tin vị trí của người dùng.
6. Đăng phản hồi chứa các thông tin như {xPos: 23, yPos: 92, clock: 23481984134}.
7. Bất kỳ logic nào khác của máy chủ đều phải xử lý chính xác các tác động của máy khách trong tương lai. Ví dụ: không nhận được thông điệp, nhận được nhưng không hiểu được, nhận được và bị lỗi, hoặc xử lý thành công.

Tóm lại, một biểu thức trong mã thông thường lại phát sinh thêm đến 15 bước trong mã của hệ thống phân tán thời gian thực cứng. Đó là do có 8 điểm khác nhau mà tại đó mỗi hoạt động giao tiếp hai chiều giữa máy chủ và máy khách có thể gặp sự cố. Mọi biểu thức đại diện cho giao tiếp hai chiều qua mạng, chẳng hạn như board.find("pacman") có thể dẫn đến các vấn đề sau.

(error, reply) = network.send(remote, actionData)
switch error
  case POST_FAILED:
    // handle case where you know server didn't get it
  case RETRYABLE:
    // handle case where server got it but reported transient failure
  case FATAL:
    // handle case where server got it and definitely doesn't like it
  case UNKNOWN: // i.e., time out
    // handle case where the *only* thing you know is that the server received
    // the message; it may have been trying to report SUCCESS, FATAL, or RETRYABLE
  case SUCCESS:
    if validate(reply)
      // do something with reply object
    else
      // handle case where reply is corrupt/incompatible

Sự phức tạp này là khó tránh khỏi. Nếu mã không xử lý tất cả các trường hợp đúng cách, dịch vụ sẽ gặp sự cố theo cách bất thường. Thử tưởng tượng cảnh ta phải viết trường hợp kiểm thử cho tất cả các kiểu sự cố mà hệ thống máy khách/máy chủ như ví dụ Pac-Man trên đây có thể gặp phải mà xem!

Kiểm thử hệ thống phân tán thời gian thực cứng

Quá trình kiểm thử phiên bản đoạn mã Pac-Man dành cho máy đơn rất đơn giản. Chỉ cần tạo một vài đối tượng Board khác nhau, đặt trạng thái riêng cho chúng rồi tạo một số đối tượng Người dùng ở các trạng thái khác nhau, v.v. Kỹ sư có thể suy nghĩ nhiều nhất về các điều kiện biên và có thể sử dụng phương pháp kiểm thử tái tạo hoặc kiểm thử mờ (fuzzer).

Trong mã Pac-Man, có 4 vị trí mà đối tượng board được sử dụng. Trong Pac-Man phân tán, có 4 điểm trong mã đó cho ra 5 kết quả khác nhau, như minh họa ở trên (POST_FAILED, RETRYABLE, FATAL, UNKNOWN hoặc SUCCESS). Điều đó khiến các trường hợp kiểm thử tăng lên rất nhiều. Ví dụ: kỹ sư hệ thống phân tán thời gian thực cứng phải xử lý rất nhiều hoán vị. Giả sử lệnh gọi đến board.find() không thực hiện được và cho kết quả POST_FAILED. Sau đó, bạn phải kiểm thử điều sẽ xảy ra khi lệnh gọi này không thành công và cho kết quả RETRYABLE, tiếp đó là kiểm thử trường hợp không thành công với kết quả FATAL và cứ tiếp tục như vậy.

Thế nhưng cách kiểm thử như vậy vẫn chưa đủ. Trong mã điển hình, kỹ sư có thể giả định rằng nếu board.find() hoạt động thì lệnh gọi tiếp theo đến board là board.move() cũng sẽ hoạt động. Trong kỹ thuật về hệ thống phân tán thời gian thực cứng thì không có trường hợp chắc chắn như vậy. Máy chủ có thể tự gặp sự cố riêng biệt bất cứ lúc nào. Do đó, kỹ sư phải viết kịch bản kiểm thử cho cả 5 trường hợp đối với mỗi lệnh gọi đến board. Chẳng hạn, kỹ sư chỉ cần viết 10 kịch bản để kiểm thử phiên bản mã Pac-Man cho một máy. Tuy nhiên, trong phiên bản hệ thống phân tán, họ phải kiểm thử 20 lần với mỗi kịch bản đó. Thế nghĩa là ma trận kiểm thử tăng từ 10 lên 200 lần!

Chưa hết đâu nhé. Kỹ sư cũng có thể sở hữu mã máy chủ. Cho dù có lỗi chung nào xảy ra ở phía máy khách, mạng và máy chủ thì họ cũng phải kiểm thử để máy khách, máy chủ không bị hư hỏng. Mã máy chủ có thể trông như sau.

handleFind(channel, message)
  if !validate(message)
    channel.send(INVALID_MESSAGE)
    return
  if !userThrottle.ok(message.user())
    channel.send(RETRYABLE_ERROR)
    return
  location = database.lookup(message.user())
  if location.error()
    channel.send(USER_NOT_FOUND)
    return
  else
    channel.send(SUCCESS, location)

handleMove(...)
  ...

handleFindAll(...)
  ...

handleRemove(...)
  ...

Có 4 chức năng phía máy chủ cần kiểm thử. Giả sử mỗi chức năng trên một máy có 5 kịch bản kiểm thử. Vậy là có tổng cộng 20 kịch bản. Vì máy khách gửi nhiều thông điệp đến cùng một máy chủ, kịch bản kiểm thử phải mô phỏng trình tự của các yêu cầu khác nhau để đảm bảo máy chủ luôn hoạt động mạnh mẽ. Ví dụ về yêu cầu: find, move, remove, findAll.

Giả sử một cấu trúc có 10 kịch bản khác nhau, mỗi kịch bản có trung bình 3 lệnh gọi. Như vậy là thêm 30 lần kiểm thử nữa. Tuy nhiên, mỗi kịch bản cũng cần kiểm thử các trường hợp sự cố. Đối với mỗi lần kiểm thử như vậy, bạn cần mô phỏng điều sẽ xảy ra khi máy khách nhận được một trong 4 kiểu sự cố (POST_FAILED, RETRYABLE, FATAL, UNKNOWN), rồi gửi lại lệnh gọi có yêu cầu hợp lệ đến máy chủ. Ví dụ: máy khách có thể gửi được yêu cầu find, nhưng đôi khi lại nhận kết quả là UNKNOWN khi gửi yêu cầu move. Sau đó, máy khách có thể gửi lại yêu cầu find vì lý do nào đó. Máy chủ có xử lý chính xác trường hợp này không? Có lẽ, nhưng bạn phải kiểm thử thì mới biết chắc được. Vậy là đối với mã phía máy khách, ma trận kiểm thử phía máy chủ cũng tăng độ phức tạp lên rất nhiều.

Xử lý trường hợp unknown (không xác định)

Rất khó để xem xét tất cả các hoán vị sự cố mà một hệ thống phân tán có thể gặp phải, nhất là khi có nhiều yêu cầu. Chúng tôi tìm ra một cách để tiếp cận kỹ thuật phân tán, đó là không tin tưởng bất cứ thứ gì. Mọi dòng mã, trừ khi không đóng vai trò gì trong giao tiếp mạng, đều có thể không hoạt động đúng chức năng.

Có lẽ trường hợp khó xử lý nhất là loại lỗi UNKNOWN được nêu trong phần trên đây. Không phải lúc nào máy khách cũng biết là yêu cầu đã thực hiện thành công. Có lẽ nó đã gửi được yêu cầu move Pac-Man (hay trong dịch vụ ngân hàng là rút tiền từ tài khoản ngân hàng của người dùng), hoặc không. Các kỹ sư giải quyết việc đó ra sao? Khó mà nói được, bởi kỹ sư cũng là con người, mà con người thường gặp khó khăn trước tình huống bất định. Con người đã quen nhìn vào mã như dưới đây.

bool isEven(number)
  switch number % 2
    case 0
      return true
    case 1
      return false

Con người hiểu được mã này bởi nó hoạt động giống như vẻ bên ngoài. Con người gặp khó khăn với phiên bản phân tán của mã. Phiên bản này làm nhiệm vụ phân tán một số công việc cho dịch vụ.

bool distributedIsEven(number)
  switch mathServer.mod(number, 2)
    case 0
      return true
    case 1
      return false
    case UNKNOWN
      return WHAT_THE_FARG?

Họ gần như không biết làm sao để xử lý trường hợp UNKNOWN cho đúng. UNKNOWN thực sự có nghĩa là gì? Mã có thử lại không? Nếu có thì bao nhiêu lần? Thời gian đợi giữa các lần thử lại là bao lâu? Hiệu ứng phụ mà mã gây ra khiến mọi chuyện trở nên tệ hơn nữa. Bên trong một ứng dụng quản lý ngân sách chạy trên một máy, thao tác rút tiền từ tài khoản là rất dễ dàng, như trong ví dụ sau đây.

class Teller
  bool doWithdraw(account, amount)
    switch account.withdraw(amount)
      case SUCCESS
        return true
      case INSUFFICIENT_FUNDS
        return false

Tuy nhiên, phiên bản phân tán của ứng dụng đó lại hoạt động bất thường do lỗi UNKNOWN.

class DistributedTeller
  bool doWithdraw(account, amount)
    switch this.accountService.withdraw(account, amount)
      case SUCCESS
        return true
      case INSUFFICIENT_FUNDS
        return false
      case UNKNOWN
        return WHAT_THE_FARG?

Việc tìm cách xử lý kiểu lỗi UNKNOWN là lý do khiến mọi thứ không như vẻ bề ngoài trong mảng kỹ thuật phân tán.

Tập hợp hệ thống phân tán thời gian thực cứng

8 kiểu sự cố nghiêm trọng có thể xảy ra ở bất kỳ mức độ trừu tượng nào trong một hệ thống phân tán. Các ví dụ trên đây chỉ giới hạn ở một máy khách, một mạng và một máy chủ. Kể cả trong kịch bản đơn giản như vậy thì ma trận trạng thái sự cố cũng có thể tăng độ phức tạp. Các hệ thống phân tán thực tế có ma trận trạng thái sự cố tinh vi hơn so với một máy khách như trong ví dụ. Các hệ thống phân tán thực tế bao gồm nhiều máy, có thể được xem xét ở nhiều mức độ trừu tượng khác nhau:

1. Từng máy
2. Nhóm máy
3. Tập hợp nhóm máy
4. Và hơn thế nữa (có thể)

Ví dụ: dịch vụ được xây dựng trên AWS có thể gộp lại các máy chuyên về xử lý tài nguyên trong một Vùng sẵn sàng cụ thể. Ngoài ra, còn có 2 nhóm máy xử lý hai Vùng sẵn sàng khác. Sau đó, những nhóm này có thể gộp lại thành một nhóm Khu vực AWS. Nhóm Khu vực đó có thể giao tiếp (theo logic) với các nhóm Khu vực khác. Tiếc rằng ngay cả ở cấp độ cao hơn, logic hơn như vậy nhưng các vấn đề tương tự vẫn xảy ra.

Giả sử một dịch vụ đã nhóm một vài máy chủ thành một nhóm logic duy nhất, gọi là GROUP1. Nhóm GROUP1 đôi khi có thể gửi thông điệp cho nhóm máy chủ khác (GROUP2). Đây là ví dụ về kỹ thuật phân tán đệ quy. Các kiểu sự cố mạng nêu trên cũng có thể áp dụng trong trường hợp này. Giả sử GROUP1 muốn gửi yêu cầu cho GROUP2. Trong sơ đồ sau đây, tương tác yêu cầu/phản hồi giữa hai máy cũng giống như với một máy nêu ở phần trên.

Tại sao nhà quản trị hệ thống phải xem xét việc cân bằng giữa hiệu năng và độ trong suốt

Dù bằng cách nào thì một máy trong GROUP1 cũng phải đưa thông điệp lên mạng NETWORK và gửi (theo logic) đến GROUP2. Một máy trong GROUP2 phải xử lý yêu cầu, v.v. Việc GROUP1 và GROUP2 bao gồm các nhóm máy không thay đổi quy luật căn bản. GROUP1, GROUP2 và NETWORK vẫn có thể gặp sự cố tách biệt với nhau.

Tuy nhiên, đó chỉ là góc nhìn ở cấp độ nhóm. Ngoài ra còn có tương tác ở cấp độ giữa các máy trong mỗi nhóm. Ví dụ: GROUP2 có thể có cấu trúc như trong sơ đồ sau.

Tại sao nhà quản trị hệ thống phải xem xét việc cân bằng giữa hiệu năng và độ trong suốt

Đầu tiên, một thông điệp được gửi đến GROUP2 qua cân bằng tải, đến một máy (có thể là S20) trong nhóm. Các nhà thiết kế hệ thống biết rằng S20 có thể gặp sự cố trong giai đoạn UPDATE STATE (cập nhật trạng thái). Do đó, S20 có thể cần chuyển thông điệp cho ít nhất một máy khác là máy ngang hàng hoặc máy trong nhóm khác. Thực sự thì S20 thực hiện điều này ra sao? Bằng cách gửi thông điệp yêu cầu/phản hồi cho một máy khác, như S25 trong sơ đồ dưới đây.

Tại sao nhà quản trị hệ thống phải xem xét việc cân bằng giữa hiệu năng và độ trong suốt

Do vậy, S20 đang thực hiện kết nối mạng theo cách đệ quy. Một lần nữa, cả 8 sự cố đều có thể xảy ra tách biệt với nhau. Kỹ thuật phân tán xảy ra 2 lần thay vì 1. Ở cấp độ logic, thông điệp gửi từ GROUP1 đến GROUP2 có thể gặp sự cố theo cả 8 cách. Thông điệp đó dẫn đến một thông điệp khác (mà có thể gặp sự cố tách biệt theo cả 8 cách nêu trên). Việc kiểm thử kịch bản này cần bao gồm ít nhất là các tình huống sau:

• Kiểm thử cả 8 cách mà thông điệp cấp độ nhóm gửi từ GROUP1 đến GROUP2 có thể gặp sự cố.
• Kiểm thử cả 8 cách mà thông điệp cấp độ máy chủ gửi từ S20 đến S25 có thể gặp sự cố.

Trong ví dụ về thông điệp yêu cầu/phản hồi này, ta thấy lý do việc kiểm thử các hệ thống phân tán vẫn là một bài toán cực kỳ nan giải, kể cả khi ta có hơn 20 năm kinh nghiệm về mảng này. Công việc kiểm thử rất khó khăn do có quá nhiều trường hợp biên, nhưng nó đặc biệt quan trọng trong các hệ thống này. Phải mất một thời gian dài để lỗi xuất hiện sau khi hệ thống được triển khai. Đồng thời, lỗi có thể gây tác động lớn đến bất ngờ đối với một hệ thống và các hệ thống có liên quan.

Lỗi phân tán thường xuất hiện muộn

Nếu sự cố là điều không tránh khỏi, thì theo lẽ thường, nó nên xảy ra càng sớm càng tốt. Ví dụ: khi một dịch vụ gặp sự cố về thay đổi quy mô (mà thường mất 6 tháng để khắc phục), thì tốt hơn hết nên phát hiện ra sự cố đó ít nhất 6 tháng trước khi nó đạt được quy mô lớn như vậy. Tương tự, tốt hơn hết nên phát hiện lỗi trước giai đoạn sản xuất. Nếu lỗi xảy ra trong giai đoạn sản xuất, cần nhanh chóng tìm ra lỗi đó trước khi nó ảnh hưởng đến nhiều khách hàng hoặc gây ra tác động xấu khác.

Các lỗi phân tán (tức các lỗi do không xử lý được toàn bộ hoán vị của 8 kiểu sự cố) thường rất nghiêm trọng. Có nhiều sự cố ghi nhận được ở các hệ thống phân tán lớn, từ hệ thống viễn thông đến hệ thống Internet cốt lõi. Tình trạng đình trệ như vậy không chỉ lan rộng, tốn kém mà còn có thể xuất phát từ các lỗi trong giai đoạn sản xuất từ nhiều tháng trước đó. Sau đó, phải mất một thời gian để kích hoạt các kịch bản cho biết nguyên nhân thực sự của những lỗi này (và lý do chúng lan rộng trên toàn hệ thống).

Lỗi phân tán lan rộng với tốc độ chóng mặt

Tôi muốn nêu một vấn đề khác rất quan trọng khi tìm hiểu về lỗi phân tán:

1. Hầu hết các lỗi phân tán đều bao gồm việc sử dụng mạng.
2. Vì vậy, lỗi phân tán thường có khả năng phát tán đến các máy (hoặc nhóm máy) khác, bởi theo định nghĩa thì chúng đã có sẵn yếu tố duy nhất liên kết các máy lại với nhau.

Amazon cũng từng gặp phải các lỗi phân tán như vậy. Một ví dụ tuy cũ nhưng vẫn rất điển hình là sự cố trên toàn hệ thống www.amazon.com. Nguyên nhân của sự cố là do ổ đĩa của một máy chủ bị đầy bộ nhớ, gây ra trục trặc trong dịch vụ danh mục từ xa.

Do xử lý sai tình trạng lỗi, máy chủ danh mục từ xa bắt đầu trả về phản hồi trống với mọi yêu cầu nhận được. Tốc độ trả về cũng rất nhanh, do yêu cầu được trả về không chứa dữ liệu (chí ít là trong trường hợp này). Trong khi đó, cân bằng tải giữa trang web và dịch vụ danh mục từ xa không nhận thấy rằng tất cả phản hồi có độ dài bằng 0. Nhưng cân bằng tải có nhận thấy rằng tốc độ trả về nhanh hơn nhiều so với các máy chủ danh mục từ xa. Vì vậy, nó gửi lưu lượng khổng lồ từ www.amazon.com đến một máy chủ danh mục từ xa mà có ổ đĩa đã đầy. Hậu quả là toàn bộ trang web phải ngừng hoạt động chỉ vì một máy chủ từ xa không hiển thị được thông tin sản phẩm nào.

Chúng tôi nhanh chóng tìm thấy máy chủ bị lỗi và loại nó ra khỏi dịch vụ để khôi phục trang web. Tiếp đó, chúng tôi thực hiện quy trình thông thường là tìm ra căn nguyên và xác định vấn đề để tình huống này không xảy ra lần nữa. Chúng tôi đã chia sẻ bài học kinh nghiệm đó trong Amazon để ngăn các hệ thống khác gặp phải sự cố như vậy. Ngoài việc rút ra bài học cụ thể về kiểu sự cố này, sự việc nêu trên cũng là một ví dụ điển hình về khả năng hình thành nhanh chóng và bất ngờ của các kiểu sự cố trong hệ thống phân tán.

Tóm tắt các vấn đề trong hệ thống phân tán

Tóm lại, thiết kế kỹ thuật cho hệ thống phân tán là công việc khó khăn vì:

• Kỹ sư không thể kết hợp các tình trạng lỗi. Thay vào đó, họ phải xem xét nhiều hoán vị của sự cố. Hầu hết các lỗi có thể xảy ra bất cứ lúc nào theo cách tách biệt (và có thể là kết hợp với) bất kỳ tình trạng lỗi nào khác.
• Kết quả của một hoạt động mạng bất kỳ có thể là UNKNOWN, trong trường hợp đó, yêu cầu có thể đã thành công, không thực hiện được hoặc nhận được nhưng không xử lý được.
• Các vấn đề về phân tán có thể xảy ra ở mọi cấp độ logic của hệ thống phân tán, không chỉ ở các máy vật lý cấp thấp.
• Do tính chất đệ quy, càng ở cấp độ cao của hệ thống thì vấn đề về phân tán càng nghiêm trọng.
• Lỗi phân tán thường xuất hiện vào thời điểm rất lâu sau khi đã triển khai vào hệ thống.
• Lỗi phân tán có thể lan ra toàn bộ hệ thống.
• Nhiều sự cố nêu trên là do định luật vật lý về kết nối mạng và là bất di bất dịch.

Tính chất khó hiểu và bất thường của điện toán phân tán không có nghĩa là không có cách giải quyết các vấn đề này. Trong Thư viện cho nhà xây dựng của Amazon, chúng tôi đi sâu vào cách thức AWS quản lý các hệ thống phân tán. Mong rằng các bài học của chúng tôi có ích với bạn phần nào trong quá trình xây dựng cho khách hàng.


Jacob Gabrielson là Kỹ sư chính cấp cao tại Amazon Web Services. Ông đã làm việc tại Amazon trong 17 năm, chủ yếu về các nền tảng vi dịch vụ nội bộ. Trong 8 năm qua, ông đã làm việc về EC2 và ECS, bao gồm hệ thống triển khai phần mềm, dịch vụ mặt phẳng kiểm soát, thị trường Spot, Lightsail và gần đây nhất là bộ chứa. Đam mê của Jacob là lập trình hệ thống, ngôn ngữ lập trình và điện toán phân tán. Sở đoản lớn nhất của ông là hành vi hệ thống hai phương thức, đặc biệt là trong các điều kiện lỗi. Ông có bằng Cử nhân khoa học máy tính của trường đại học Washington ở Seattle.

Nội dung liên quan

Thời gian chờ, thử lại và rút lại với phương sai độ trễ Tránh dự phòng trong hệ thống phân tán