Năm sinh viên và ba cửa hàng khóa-giá trị được phân phối

Hoặc cách chúng tôi viết thư viện C++ khách cho ZooKeeper, etcd và Consul KV

Trong thế giới của hệ thống phân tán, có một số nhiệm vụ điển hình: lưu trữ thông tin về thành phần của cụm, quản lý cấu hình của các nút, phát hiện các nút bị lỗi, chọn người dẫn đầu và những người khác. Để giải quyết những vấn đề này, các hệ thống phân tán đặc biệt đã được tạo ra - dịch vụ phối hợp. Bây giờ chúng ta sẽ quan tâm đến ba trong số chúng: ZooKeeper, etcd và Consul. Trong số tất cả các chức năng phong phú của Lãnh sự, chúng tôi sẽ tập trung vào Lãnh sự KV.

Năm sinh viên và ba cửa hàng khóa-giá trị được phân phối

Về bản chất, tất cả các hệ thống này đều là kho lưu trữ khóa-giá trị có khả năng tuyến tính hóa và có khả năng chịu lỗi. Mặc dù các mô hình dữ liệu của chúng có những khác biệt đáng kể mà chúng ta sẽ thảo luận sau, nhưng chúng giải quyết được các vấn đề thực tế giống nhau. Rõ ràng, mỗi ứng dụng sử dụng dịch vụ phối hợp đều được gắn với một trong số chúng, điều này có thể dẫn đến nhu cầu hỗ trợ một số hệ thống trong một trung tâm dữ liệu để giải quyết cùng một vấn đề cho các ứng dụng khác nhau.

Ý tưởng giải quyết vấn đề này bắt nguồn từ một cơ quan tư vấn của Úc và chúng tôi, một nhóm nhỏ sinh viên, thực hiện nó, đó là điều tôi sắp nói đến.

Chúng tôi đã quản lý để tạo một thư viện cung cấp giao diện chung để làm việc với ZooKeeper, etcd và Consul KV. Thư viện được viết bằng C++, nhưng có kế hoạch chuyển nó sang các ngôn ngữ khác.

Mô hình dữ liệu

Để phát triển giao diện chung cho ba hệ thống khác nhau, bạn cần hiểu chúng có điểm gì chung và chúng khác nhau như thế nào. Hãy tìm ra nó.

Vườn bách thú

Năm sinh viên và ba cửa hàng khóa-giá trị được phân phối

Các khóa được tổ chức thành một cây và được gọi là các nút. Theo đó, đối với một nút, bạn có thể nhận được danh sách các nút con của nó. Các hoạt động tạo znode (tạo) và thay đổi giá trị (setData) được tách riêng: chỉ có thể đọc và thay đổi các khóa hiện có. Đồng hồ có thể được gắn vào các hoạt động kiểm tra sự tồn tại của nút, đọc giá trị và nhận nút con. Watch là trình kích hoạt một lần, kích hoạt khi phiên bản của dữ liệu tương ứng trên máy chủ thay đổi. Các nút phù du được sử dụng để phát hiện lỗi. Chúng được gắn với phiên của khách hàng đã tạo ra chúng. Khi khách hàng đóng phiên hoặc ngừng thông báo cho ZooKeeper về sự tồn tại của nó, các nút này sẽ tự động bị xóa. Các giao dịch đơn giản được hỗ trợ - một tập hợp các hoạt động đều thành công hoặc thất bại nếu điều này không thể thực hiện được đối với ít nhất một trong số chúng.

vvd

Năm sinh viên và ba cửa hàng khóa-giá trị được phân phối

Các nhà phát triển hệ thống này rõ ràng đã lấy cảm hứng từ ZooKeeper và do đó đã làm mọi thứ một cách khác biệt. Không có hệ thống phân cấp các khóa nhưng chúng tạo thành một tập hợp được sắp xếp theo thứ tự từ điển. Bạn có thể lấy hoặc xóa tất cả các khóa thuộc một phạm vi nhất định. Cấu trúc này có vẻ lạ nhưng thực tế nó rất biểu cảm và có thể dễ dàng mô phỏng chế độ xem phân cấp thông qua nó.

etcd không có thao tác so sánh và thiết lập tiêu chuẩn, nhưng nó có một thứ tốt hơn: giao dịch. Tất nhiên, chúng tồn tại trong cả ba hệ thống, nhưng các giao dịch etcd đặc biệt tốt. Chúng bao gồm ba khối: kiểm tra, thành công, thất bại. Khối đầu tiên chứa một tập hợp các điều kiện, khối thứ hai và thứ ba - các hoạt động. Giao dịch được thực hiện nguyên tử. Nếu tất cả các điều kiện đều đúng thì khối thành công sẽ được thực thi, nếu không thì khối thất bại sẽ được thực thi. Trong API 3.3, các khối thành công và thất bại có thể chứa các giao dịch lồng nhau. Nghĩa là, có thể thực hiện nguyên tử các cấu trúc có điều kiện ở mức lồng nhau gần như tùy ý. Bạn có thể tìm hiểu thêm về những hoạt động kiểm tra và hoạt động tồn tại từ tài liệu.

Đồng hồ cũng tồn tại ở đây, mặc dù chúng phức tạp hơn một chút và có thể tái sử dụng được. Nghĩa là, sau khi cài đặt đồng hồ trên một phạm vi khóa, bạn sẽ nhận được tất cả các bản cập nhật trong phạm vi này cho đến khi bạn hủy đồng hồ chứ không chỉ cập nhật đầu tiên. Trong etcd, phiên tương tự của phiên khách ZooKeeper là hợp đồng thuê.

Lãnh sự K.V.

Ở đây cũng không có cấu trúc phân cấp nghiêm ngặt, nhưng Lãnh sự có thể tạo ra vẻ ngoài như nó tồn tại: bạn có thể lấy và xóa tất cả các khóa có tiền tố được chỉ định, nghĩa là làm việc với “cây con” của khóa. Các truy vấn như vậy được gọi là đệ quy. Ngoài ra, Lãnh sự chỉ có thể chọn các khóa không chứa ký tự được chỉ định sau tiền tố, tương ứng với việc thu được các “con” ngay lập tức. Nhưng điều đáng nhớ là đây chính xác là sự xuất hiện của một cấu trúc phân cấp: hoàn toàn có thể tạo khóa nếu khóa gốc của nó không tồn tại hoặc xóa khóa có con, trong khi các khóa con sẽ tiếp tục được lưu trữ trong hệ thống.

Năm sinh viên và ba cửa hàng khóa-giá trị được phân phối
Thay vì theo dõi, Lãnh sự đã chặn các yêu cầu HTTP. Về bản chất, đây là những lệnh gọi thông thường đến phương pháp đọc dữ liệu, trong đó, cùng với các tham số khác, phiên bản dữ liệu đã biết cuối cùng được chỉ định. Nếu phiên bản hiện tại của dữ liệu tương ứng trên máy chủ lớn hơn phiên bản được chỉ định, phản hồi sẽ được trả về ngay lập tức, nếu không thì - khi giá trị thay đổi. Ngoài ra còn có các phiên có thể được gắn vào khóa bất cứ lúc nào. Điều đáng lưu ý là không giống như etcd và ZooKeeper, trong đó việc xóa phiên dẫn đến xóa các khóa liên quan, có một chế độ trong đó phiên chỉ được hủy liên kết khỏi chúng. Có sẵn giao dịch, không có chi nhánh, nhưng có đủ loại séc.

Để tất cả chúng cùng nhau

ZooKeeper có mô hình dữ liệu nghiêm ngặt nhất. Các truy vấn phạm vi biểu cảm có sẵn trong etcd không thể được mô phỏng một cách hiệu quả trong ZooKeeper hoặc Consul. Cố gắng kết hợp những gì tốt nhất từ ​​tất cả các dịch vụ, chúng tôi đã tạo ra một giao diện gần như tương đương với giao diện ZooKeeper với các ngoại lệ quan trọng sau:

  • các nút trình tự, vùng chứa và TTL không được hỗ trợ
  • ACL không được hỗ trợ
  • phương thức set tạo khóa nếu nó không tồn tại (trong ZK setData trả về lỗi trong trường hợp này)
  • các phương thức set và cas được tách biệt (trong ZK về cơ bản chúng giống nhau)
  • phương thức xóa sẽ xóa một nút cùng với cây con của nó (trong ZK xóa trả về lỗi nếu nút có con)
  • Đối với mỗi khóa chỉ có một phiên bản - phiên bản giá trị (trong ZK Có ba trong số họ)

Việc từ chối các nút tuần tự là do etcd và Consul không có hỗ trợ tích hợp cho chúng và người dùng có thể dễ dàng triển khai chúng trên giao diện thư viện kết quả.

Việc triển khai hành vi tương tự như ZooKeeper khi xóa một đỉnh sẽ yêu cầu duy trì một bộ đếm con riêng cho từng khóa trong etcd và Consul. Vì chúng tôi đã cố gắng tránh lưu trữ thông tin meta nên đã quyết định xóa toàn bộ cây con.

Sự tinh tế của việc thực hiện

Chúng ta hãy xem xét kỹ hơn một số khía cạnh của việc triển khai giao diện thư viện trong các hệ thống khác nhau.

Hệ thống phân cấp trong etcd

Duy trì chế độ xem phân cấp trong etcd hóa ra lại là một trong những nhiệm vụ thú vị nhất. Truy vấn phạm vi giúp dễ dàng truy xuất danh sách các khóa có tiền tố được chỉ định. Ví dụ: nếu bạn cần mọi thứ bắt đầu bằng "/foo", bạn đang yêu cầu một phạm vi ["/foo", "/fop"). Nhưng điều này sẽ trả về toàn bộ cây con của khóa, điều này có thể không được chấp nhận nếu cây con lớn. Lúc đầu, chúng tôi dự định sử dụng cơ chế dịch khóa, được triển khai trong zetcd. Nó liên quan đến việc thêm một byte vào đầu khóa, bằng độ sâu của nút trong cây. Tôi sẽ cho bạn một ví dụ.

"/foo" -> "u01/foo"
"/foo/bar" -> "u02/foo/bar"

Sau đó lấy tất cả các con ngay lập tức của khóa "/foo" có thể bằng cách yêu cầu một phạm vi ["u02/foo/", "u02/foo0"). Có, trong ASCII "0" đứng ngay sau "/".

Nhưng làm thế nào để thực hiện việc loại bỏ một đỉnh trong trường hợp này? Hóa ra bạn cần xóa tất cả các phạm vi thuộc loại ["uXX/foo/", "uXX/foo0") cho XX từ 01 đến FF. Và sau đó chúng tôi gặp phải giới hạn số hoạt động trong một giao dịch.

Kết quả là, một hệ thống chuyển đổi khóa đơn giản đã được phát minh, giúp thực hiện hiệu quả cả việc xóa khóa và lấy danh sách con. Chỉ cần thêm một ký tự đặc biệt trước mã thông báo cuối cùng là đủ. Ví dụ:

"/very" -> "/u00very"
"/very/long" -> "/very/u00long"
"/very/long/path" -> "/very/long/u00path"

Sau đó xóa chìa khóa "/very" chuyển sang xóa "/u00very" và phạm vi ["/very/", "/very0")và nhận tất cả trẻ em - trong yêu cầu về khóa từ phạm vi ["/very/u00", "/very/u01").

Xóa khóa trong ZooKeeper

Như tôi đã đề cập, trong ZooKeeper bạn không thể xóa nút nếu nó có nút con. Chúng tôi muốn xóa khóa cùng với cây con. Tôi nên làm gì? Chúng tôi làm điều này với sự lạc quan. Đầu tiên, chúng ta duyệt cây con một cách đệ quy, thu được các đỉnh con của mỗi đỉnh bằng một truy vấn riêng biệt. Sau đó, chúng tôi xây dựng một giao dịch cố gắng xóa tất cả các nút của cây con theo đúng thứ tự. Tất nhiên, những thay đổi có thể xảy ra giữa việc đọc cây con và xóa nó. Trong trường hợp này, giao dịch sẽ thất bại. Hơn nữa, cây con có thể thay đổi trong quá trình đọc. Một yêu cầu dành cho nút con của nút tiếp theo có thể trả về lỗi nếu, ví dụ, nút này đã bị xóa. Trong cả hai trường hợp, chúng tôi lặp lại toàn bộ quá trình một lần nữa.

Cách tiếp cận này làm cho việc xóa một khóa trở nên rất kém hiệu quả nếu nó có khóa con và thậm chí còn hơn thế nữa nếu ứng dụng tiếp tục hoạt động với cây con, xóa và tạo khóa. Tuy nhiên, điều này cho phép chúng tôi tránh làm phức tạp việc triển khai các phương pháp khác trong etcd và Consul.

lấy bối cảnh trong ZooKeeper

Trong ZooKeeper, có các phương thức riêng biệt hoạt động với cấu trúc cây (tạo, xóa, getChildren) và hoạt động với dữ liệu trong các nút (setData, getData). Hơn nữa, tất cả các phương thức đều có điều kiện tiên quyết nghiêm ngặt: tạo sẽ trả về lỗi nếu nút đó đã thực hiện đã được tạo, xóa hoặc setData – nếu nó chưa tồn tại. Chúng tôi cần một phương thức thiết lập có thể được gọi mà không cần nghĩ đến sự hiện diện của khóa.

Một lựa chọn là thực hiện một cách tiếp cận lạc quan, chẳng hạn như xóa bỏ. Kiểm tra xem một nút có tồn tại không. Nếu tồn tại, hãy gọi setData, nếu không thì tạo. Nếu phương thức cuối cùng trả về lỗi, hãy lặp lại tất cả. Điều đầu tiên cần lưu ý là việc kiểm tra sự tồn tại là vô nghĩa. Bạn có thể gọi ngay create. Hoàn tất thành công có nghĩa là nút không tồn tại và nó đã được tạo. Nếu không, create sẽ trả về lỗi thích hợp, sau đó bạn cần gọi setData. Tất nhiên, giữa các lệnh gọi, một đỉnh có thể bị xóa bởi lệnh gọi cạnh tranh và setData cũng sẽ trả về lỗi. Trong trường hợp này, bạn có thể làm lại từ đầu, nhưng liệu nó có đáng không?

Nếu cả hai phương pháp đều trả về lỗi thì chúng tôi biết chắc chắn rằng việc xóa cạnh tranh đã diễn ra. Hãy tưởng tượng rằng việc xóa này xảy ra sau khi gọi set. Khi đó bất kỳ ý nghĩa nào mà chúng ta đang cố gắng thiết lập đều đã bị xóa bỏ. Điều này có nghĩa là chúng ta có thể cho rằng tập hợp đó đã được thực thi thành công, ngay cả khi trên thực tế không có gì được viết.

Thêm chi tiết kỹ thuật

Trong phần này chúng ta sẽ tạm dừng các hệ thống phân tán và nói về mã hóa.
Một trong những yêu cầu chính của khách hàng là đa nền tảng: ít nhất một trong các dịch vụ phải được hỗ trợ trên Linux, MacOS và Windows. Ban đầu, chúng tôi chỉ phát triển cho Linux và sau đó bắt đầu thử nghiệm trên các hệ thống khác. Điều này gây ra rất nhiều vấn đề mà trong một thời gian, người ta hoàn toàn không biết phải giải quyết như thế nào. Do đó, cả ba dịch vụ điều phối hiện đều được hỗ trợ trên Linux và MacOS, trong khi chỉ Consul KV được hỗ trợ trên Windows.

Ngay từ đầu, chúng tôi đã cố gắng sử dụng các thư viện có sẵn để truy cập các dịch vụ. Trong trường hợp của ZooKeeper, sự lựa chọn thuộc về Trình quản lý vườn thú C++, cuối cùng không thể biên dịch được trên Windows. Tuy nhiên, điều này không có gì đáng ngạc nhiên: thư viện được định vị là chỉ dành cho linux. Đối với Lãnh sự, lựa chọn duy nhất là lãnh sự. Hỗ trợ đã được thêm vào nó phiên họp и giao dịch. Đối với etcd, không tìm thấy thư viện chính thức hỗ trợ phiên bản mới nhất của giao thức, vì vậy chúng tôi chỉ đơn giản là khách hàng grpc đã tạo.

Lấy cảm hứng từ giao diện không đồng bộ của thư viện ZooKeeper C++, chúng tôi quyết định triển khai giao diện không đồng bộ. ZooKeeper C++ sử dụng các nguyên hàm tương lai/lời hứa cho việc này. Thật không may, trong STL, chúng được triển khai rất khiêm tốn. Ví dụ, không thì phương pháp, áp dụng hàm đã truyền cho kết quả của tương lai khi nó có sẵn. Trong trường hợp của chúng tôi, phương pháp như vậy là cần thiết để chuyển đổi kết quả sang định dạng của thư viện của chúng tôi. Để giải quyết vấn đề này, chúng tôi phải triển khai nhóm luồng đơn giản của riêng mình, vì theo yêu cầu của khách hàng, chúng tôi không thể sử dụng các thư viện nặng của bên thứ ba như Boost.

Việc triển khai sau đó của chúng tôi hoạt động như thế này. Khi được gọi, một cặp lời hứa/tương lai bổ sung sẽ được tạo. Tương lai mới được trả về và tương lai đã qua được đặt cùng với chức năng tương ứng và một lời hứa bổ sung trong hàng đợi. Một chuỗi từ nhóm sẽ chọn một số hợp đồng tương lai từ hàng đợi và thăm dò chúng bằng cách sử dụng Wait_for. Khi có kết quả, hàm tương ứng sẽ được gọi và giá trị trả về của nó được chuyển cho lời hứa.

Chúng tôi đã sử dụng cùng một nhóm luồng để thực hiện các truy vấn tới etcd và Consul. Điều này có nghĩa là các thư viện cơ bản có thể được truy cập bởi nhiều luồng khác nhau. ppconsul không an toàn cho luồng, vì vậy các cuộc gọi tới nó được bảo vệ bằng khóa.
Bạn có thể làm việc với grpc từ nhiều luồng, nhưng có một số điều cần lưu ý. Đồng hồ trong etcd được triển khai thông qua luồng grpc. Đây là các kênh hai chiều cho các tin nhắn thuộc một loại nhất định. Thư viện tạo một luồng duy nhất cho tất cả đồng hồ và một luồng duy nhất xử lý tin nhắn đến. Vì vậy grpc cấm ghi song song vào luồng. Điều này có nghĩa là khi khởi tạo hoặc xóa đồng hồ, bạn phải đợi cho đến khi yêu cầu trước đó gửi xong trước khi gửi yêu cầu tiếp theo. Chúng tôi sử dụng để đồng bộ hóa biến có điều kiện.

Tổng

Xem cho chính mình: liboffkv.

Đội của chúng tôi: Raed Romanov, Ivan Glushenkov, Dmitry Kamaldinov, Victor Krapivensky, Vitaly Ivanin.

Nguồn: www.habr.com

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