Xây dựng các khối ứng dụng phân tán. Cách tiếp cận đầu tiên

Xây dựng các khối ứng dụng phân tán. Cách tiếp cận đầu tiên

Cuối cùng Bài viết Chúng tôi đã kiểm tra nền tảng lý thuyết của kiến ​​trúc phản ứng. Đã đến lúc nói về các luồng dữ liệu, các cách triển khai hệ thống Erlang/Elixir phản ứng và các mẫu thông báo trong đó:

  • Yêu cầu-phản hồi
  • Phản hồi theo yêu cầu
  • Phản hồi với yêu cầu
  • Theo dõi công khai
  • Đảo ngược xuất bản-đăng ký
  • Phân phối nhiệm vụ

SOA, MSA và nhắn tin

SOA, MSA là các kiến ​​trúc hệ thống xác định các quy tắc để xây dựng hệ thống, trong khi việc nhắn tin cung cấp các nguyên tắc cơ bản để triển khai chúng.

Tôi không muốn quảng bá kiến ​​trúc hệ thống này hay kiến ​​trúc hệ thống kia. Tôi ủng hộ việc sử dụng các phương pháp thực hành hiệu quả và hữu ích nhất cho một dự án và hoạt động kinh doanh cụ thể. Dù chúng ta chọn mô hình nào, tốt hơn hết là tạo các khối hệ thống theo hướng Unix: các thành phần có khả năng kết nối tối thiểu, chịu trách nhiệm về các thực thể riêng lẻ. Các phương thức API thực hiện các hành động đơn giản nhất có thể với các thực thể.

Nhắn tin, như tên cho thấy, là một nhà môi giới tin nhắn. Mục đích chính của nó là nhận và gửi tin nhắn. Nó chịu trách nhiệm về các giao diện gửi thông tin, hình thành các kênh logic để truyền thông tin trong hệ thống, định tuyến và cân bằng, cũng như xử lý lỗi ở cấp hệ thống.
Thông điệp chúng tôi đang phát triển không cố gắng cạnh tranh hoặc thay thế rabbitmq. Các tính năng chính của nó:

  • Phân bổ.
    Điểm trao đổi có thể được tạo trên tất cả các nút cụm, càng gần mã sử dụng chúng càng tốt.
  • Sự đơn giản.
    Tập trung vào việc giảm thiểu mã soạn sẵn và dễ sử dụng.
  • Hiệu suất tốt hơn.
    Chúng tôi không cố gắng lặp lại chức năng của rabbitmq mà chỉ làm nổi bật lớp kiến ​​​​trúc và vận chuyển mà chúng tôi đưa vào OTP một cách đơn giản nhất có thể, giảm thiểu chi phí.
  • Uyển chuyển.
    Mỗi dịch vụ có thể kết hợp nhiều mẫu trao đổi.
  • Khả năng phục hồi theo thiết kế.
  • Khả năng mở rộng.
    Tin nhắn phát triển cùng với ứng dụng. Khi tải tăng lên, bạn có thể di chuyển điểm trao đổi sang từng máy riêng lẻ.

Bình luận. Về mặt tổ chức mã, các siêu dự án rất phù hợp với các hệ thống Erlang/Elixir phức tạp. Tất cả mã dự án đều nằm trong một kho lưu trữ - một dự án ô. Đồng thời, microservice được cách ly tối đa và thực hiện các thao tác đơn giản chịu trách nhiệm cho một thực thể riêng biệt. Với cách tiếp cận này, dễ dàng duy trì API của toàn bộ hệ thống, dễ dàng thực hiện các thay đổi, thuận tiện khi viết các bài kiểm tra đơn vị và tích hợp.

Các thành phần hệ thống tương tác trực tiếp hoặc thông qua nhà môi giới. Từ góc độ nhắn tin, mỗi dịch vụ có một số giai đoạn hoạt động:

  • Khởi tạo dịch vụ.
    Ở giai đoạn này, quy trình và các phần phụ thuộc thực thi dịch vụ được định cấu hình và khởi chạy.
  • Tạo điểm trao đổi.
    Dịch vụ có thể sử dụng điểm trao đổi tĩnh được chỉ định trong cấu hình nút hoặc tạo điểm trao đổi động.
  • Đăng ký dịch vụ.
    Để dịch vụ có thể phục vụ các yêu cầu, nó phải được đăng ký tại điểm trao đổi.
  • Hoạt động bình thường.
    Dịch vụ này tạo ra công việc hữu ích.
  • Hoàn thành công việc.
    Có 2 kiểu tắt máy: bình thường và khẩn cấp. Trong quá trình hoạt động bình thường, dịch vụ bị ngắt kết nối với điểm trao đổi và dừng lại. Trong trường hợp khẩn cấp, tính năng nhắn tin sẽ thực thi một trong các tập lệnh chuyển đổi dự phòng.

Nó trông khá phức tạp, nhưng đoạn mã này không quá đáng sợ. Các ví dụ mã kèm theo nhận xét sẽ được đưa ra trong phần phân tích mẫu sau này.

Trao đổi

Điểm trao đổi là một quy trình nhắn tin triển khai logic tương tác với các thành phần trong mẫu nhắn tin. Trong tất cả các ví dụ được trình bày bên dưới, các thành phần tương tác thông qua các điểm trao đổi, sự kết hợp của chúng tạo thành thông điệp.

Các mẫu trao đổi tin nhắn (MEP)

Trên toàn cầu, mô hình trao đổi có thể được chia thành hai chiều và một chiều. Cái trước ngụ ý phản hồi cho một tin nhắn đến, cái sau thì không. Một ví dụ kinh điển về mẫu hai chiều trong kiến ​​trúc máy khách-máy chủ là mẫu Yêu cầu-phản hồi. Hãy nhìn vào mẫu và các sửa đổi của nó.

Yêu cầu-phản hồi hoặc RPC

RPC được sử dụng khi chúng ta cần nhận phản hồi từ một quy trình khác. Quá trình này có thể đang chạy trên cùng một nút hoặc nằm ở một lục địa khác. Dưới đây là sơ đồ tương tác giữa client và server thông qua tin nhắn.

Xây dựng các khối ứng dụng phân tán. Cách tiếp cận đầu tiên

Vì việc nhắn tin hoàn toàn không đồng bộ nên việc trao đổi đối với khách hàng được chia thành 2 giai đoạn:

  1. gửi yêu cầu

    messaging:request(Exchange, ResponseMatchingTag, RequestDefinition, HandlerProcess).

    Sàn giao dịch ‒ tên duy nhất của điểm trao đổi
    Phản hồiMatchingTag ‒ nhãn cục bộ để xử lý phản hồi. Ví dụ: trong trường hợp gửi một số yêu cầu giống hệt nhau của những người dùng khác nhau.
    Định nghĩa yêu cầu - nội dung yêu cầu
    Trình xử lý ‒ PID của bộ xử lý. Quá trình này sẽ nhận được phản hồi từ máy chủ.

  2. Xử lý phản hồi

    handle_info(#'$msg'{exchange = EXCHANGE, tag = ResponseMatchingTag,message = ResponsePayload}, State)

    Phản hồiTải trọng - phản hồi của máy chủ.

Đối với máy chủ, quy trình cũng bao gồm 2 giai đoạn:

  1. Khởi tạo điểm trao đổi
  2. Xử lý các yêu cầu nhận được

Hãy minh họa mẫu này bằng mã. Giả sử chúng ta cần triển khai một dịch vụ đơn giản cung cấp một phương thức thời gian chính xác duy nhất.

Mã máy chủ

Hãy xác định API dịch vụ trong api.hrl:

%% =====================================================
%%  entities
%% =====================================================
-record(time, {
  unixtime :: non_neg_integer(),
  datetime :: binary()
}).

-record(time_error, {
  code :: non_neg_integer(),
  error :: term()
}).

%% =====================================================
%%  methods
%% =====================================================
-record(time_req, {
  opts :: term()
}).
-record(time_resp, {
  result :: #time{} | #time_error{}
}).

Hãy xác định bộ điều khiển dịch vụ trong time_controller.erl

%% В примере показан только значимый код. Вставив его в шаблон gen_server можно получить рабочий сервис.

%% инициализация gen_server
init(Args) ->
  %% подключение к точке обмена
  messaging:monitor_exchange(req_resp, ?EXCHANGE, default, self())
  {ok, #{}}.

%% обработка события потери связи с точкой обмена. Это же событие приходит, если точка обмена еще не запустилась.
handle_info(#exchange_die{exchange = ?EXCHANGE}, State) ->
  erlang:send(self(), monitor_exchange),
  {noreply, State};

%% обработка API
handle_info(#time_req{opts = _Opts}, State) ->
  messaging:response_once(Client, #time_resp{
result = #time{ unixtime = time_utils:unixtime(now()), datetime = time_utils:iso8601_fmt(now())}
  });
  {noreply, State};

%% завершение работы gen_server
terminate(_Reason, _State) ->
  messaging:demonitor_exchange(req_resp, ?EXCHANGE, default, self()),
  ok.

Mã khách hàng

Để gửi yêu cầu đến dịch vụ, bạn có thể gọi API yêu cầu nhắn tin ở bất kỳ đâu trong ứng dụng khách:

case messaging:request(?EXCHANGE, tag, #time_req{opts = #{}}, self()) of
    ok -> ok;
    _ -> %% repeat or fail logic
end

Trong hệ thống phân tán, cấu hình của các thành phần có thể rất khác nhau và tại thời điểm yêu cầu, việc nhắn tin có thể chưa bắt đầu hoặc bộ điều khiển dịch vụ sẽ không sẵn sàng phục vụ yêu cầu. Vì vậy, chúng ta cần kiểm tra phản hồi tin nhắn và xử lý trường hợp lỗi.
Sau khi gửi thành công, khách hàng sẽ nhận được phản hồi hoặc lỗi từ dịch vụ.
Hãy xử lý cả hai trường hợp trong hand_info:

handle_info(#'$msg'{exchange = ?EXCHANGE, tag = tag, message = #time_resp{result = #time{unixtime = Utime}}}, State) ->
  ?debugVal(Utime),
  {noreply, State};

handle_info(#'$msg'{exchange = ?EXCHANGE, tag = tag, message = #time_resp{result = #time_error{code = ErrorCode}}}, State) ->
  ?debugVal({error, ErrorCode}),
  {noreply, State};

Phản hồi theo yêu cầu

Tốt nhất là tránh gửi những tin nhắn lớn. Khả năng đáp ứng và hoạt động ổn định của toàn bộ hệ thống phụ thuộc vào điều này. Nếu phản hồi cho một truy vấn chiếm nhiều bộ nhớ thì việc chia nó thành nhiều phần là bắt buộc.

Xây dựng các khối ứng dụng phân tán. Cách tiếp cận đầu tiên

Hãy để tôi cho bạn một vài ví dụ về những trường hợp như vậy:

  • Các thành phần trao đổi dữ liệu nhị phân, chẳng hạn như các tập tin. Việc chia phản hồi thành các phần nhỏ giúp bạn làm việc hiệu quả với các tệp có kích thước bất kỳ và tránh tràn bộ nhớ.
  • Danh sách. Ví dụ: chúng ta cần chọn tất cả các bản ghi từ một bảng lớn trong cơ sở dữ liệu và chuyển chúng sang thành phần khác.

Tôi gọi những phản ứng này là đầu máy. Trong mọi trường hợp, 1024 tin nhắn có dung lượng 1 MB sẽ tốt hơn một tin nhắn có dung lượng 1 GB.

Trong cụm Erlang, chúng tôi nhận được một lợi ích bổ sung - giảm tải cho điểm trao đổi và mạng vì phản hồi được gửi ngay lập tức đến người nhận, bỏ qua điểm trao đổi.

Phản hồi với yêu cầu

Đây là một sửa đổi khá hiếm của mẫu RPC để xây dựng hệ thống hộp thoại.

Xây dựng các khối ứng dụng phân tán. Cách tiếp cận đầu tiên

Xuất bản-đăng ký (cây phân phối dữ liệu)

Các hệ thống hướng sự kiện sẽ cung cấp chúng cho người tiêu dùng ngay khi dữ liệu sẵn sàng. Do đó, các hệ thống thiên về mô hình đẩy hơn là mô hình kéo hoặc thăm dò. Tính năng này cho phép bạn tránh lãng phí tài nguyên bằng cách liên tục yêu cầu và chờ đợi dữ liệu.
Hình minh họa quá trình phân phối tin nhắn đến người tiêu dùng đã đăng ký một chủ đề cụ thể.

Xây dựng các khối ứng dụng phân tán. Cách tiếp cận đầu tiên

Các ví dụ kinh điển về việc sử dụng mô hình này là sự phân bổ trạng thái: thế giới trò chơi trong trò chơi máy tính, dữ liệu thị trường trên các sàn giao dịch, thông tin hữu ích trong nguồn cấp dữ liệu.

Hãy xem mã thuê bao:

init(_Args) ->
  %% подписываемся на обменник, ключ = key
  messaging:subscribe(?SUBSCRIPTION, key, tag, self()),
  {ok, #{}}.

handle_info(#exchange_die{exchange = ?SUBSCRIPTION}, State) ->
  %% если точка обмена недоступна, то пытаемся переподключиться
  messaging:subscribe(?SUBSCRIPTION, key, tag, self()),
  {noreply, State};

%% обрабатываем пришедшие сообщения
handle_info(#'$msg'{exchange = ?SUBSCRIPTION, message = Msg}, State) ->
  ?debugVal(Msg),
  {noreply, State};

%% при остановке потребителя - отключаемся от точки обмена
terminate(_Reason, _State) ->
  messaging:unsubscribe(?SUBSCRIPTION, key, tag, self()),
  ok.

Nguồn có thể gọi hàm để xuất bản tin nhắn ở bất kỳ nơi nào thuận tiện:

messaging:publish_message(Exchange, Key, Message).

Sàn giao dịch - tên của điểm trao đổi,
Key - phím định tuyến
Tin nhắn - khối hàng

Đảo ngược xuất bản-đăng ký

Xây dựng các khối ứng dụng phân tán. Cách tiếp cận đầu tiên

Bằng cách mở rộng pub-sub, bạn có thể có được một mẫu thuận tiện cho việc ghi nhật ký. Tập hợp các nguồn và người tiêu dùng có thể hoàn toàn khác nhau. Hình vẽ cho thấy một trường hợp với một người tiêu dùng và nhiều nguồn.

Mẫu phân bổ nhiệm vụ

Hầu hết mọi dự án đều liên quan đến các tác vụ xử lý bị trì hoãn, chẳng hạn như tạo báo cáo, gửi thông báo và truy xuất dữ liệu từ hệ thống của bên thứ ba. Thông lượng của hệ thống thực hiện các tác vụ này có thể dễ dàng được điều chỉnh bằng cách thêm các trình xử lý. Tất cả những gì còn lại đối với chúng tôi là hình thành một cụm bộ xử lý và phân bổ đều các nhiệm vụ giữa chúng.

Hãy xem xét các tình huống phát sinh bằng ví dụ về 3 trình xử lý. Ngay cả ở giai đoạn phân công nhiệm vụ, câu hỏi về tính công bằng trong phân phối và sự tràn ngập của người xử lý cũng nảy sinh. Việc phân phối vòng tròn sẽ đảm bảo tính công bằng và để tránh tình trạng quá tải người xử lý, chúng tôi sẽ đưa ra một hạn chế tìm nạp trước_limit. Trong điều kiện nhất thời tìm nạp trước_limit sẽ ngăn một người xử lý nhận tất cả các nhiệm vụ.

Nhắn tin quản lý hàng đợi và mức độ ưu tiên xử lý. Bộ xử lý nhận nhiệm vụ khi chúng đến. Nhiệm vụ có thể hoàn thành thành công hoặc thất bại:

  • messaging:ack(Tack) - được gọi nếu tin nhắn được xử lý thành công
  • messaging:nack(Tack) - được gọi trong mọi tình huống khẩn cấp. Sau khi tác vụ được trả về, tin nhắn sẽ chuyển nó cho người xử lý khác.

Xây dựng các khối ứng dụng phân tán. Cách tiếp cận đầu tiên

Giả sử xảy ra một lỗi phức tạp trong khi xử lý ba tác vụ: bộ xử lý 1 sau khi nhận tác vụ đã bị hỏng mà không kịp báo cáo bất cứ điều gì cho điểm trao đổi. Trong trường hợp này, điểm trao đổi sẽ chuyển nhiệm vụ sang một trình xử lý khác sau khi hết thời gian chờ xác nhận. Vì lý do nào đó, người xử lý 3 đã từ bỏ nhiệm vụ và gửi nack; kết quả là nhiệm vụ cũng được chuyển cho một người xử lý khác đã hoàn thành thành công nó.

Tóm tắt sơ bộ

Chúng ta đã tìm hiểu các khối xây dựng cơ bản của hệ thống phân tán và hiểu biết cơ bản về cách sử dụng chúng trong Erlang/Elixir.

Bằng cách kết hợp các mẫu cơ bản, bạn có thể xây dựng các mô hình phức tạp để giải quyết các vấn đề mới nổi.

Trong phần cuối của loạt bài này, chúng ta sẽ xem xét các vấn đề chung về tổ chức dịch vụ, định tuyến và cân bằng, đồng thời nói về khía cạnh thực tế của khả năng mở rộng và khả năng chịu lỗi của hệ thống.

Hết phần thứ hai.

Hình ảnh Marius Christensen
Hình minh họa được chuẩn bị bằng websequencediagrams.com

Nguồn: www.habr.com

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