Làm thế nào để mô hình gây rối của LMAX hoạt động?


205

Tôi đang cố gắng để hiểu mô hình gây rối . Tôi đã xem video InfoQ và cố gắng đọc bài báo của họ. Tôi hiểu có một bộ đệm vòng liên quan, nó được khởi tạo như một mảng cực lớn để tận dụng lợi thế của bộ nhớ cache, loại bỏ phân bổ bộ nhớ mới.

Có vẻ như có một hoặc nhiều số nguyên tử theo dõi các vị trí. Mỗi 'sự kiện' dường như có một id duy nhất và vị trí của nó trong vòng được tìm thấy bằng cách tìm mô-đun của nó đối với kích thước của vòng, v.v., v.v.

Thật không may, tôi không có cảm giác trực quan về cách thức hoạt động của nó. Tôi đã thực hiện nhiều ứng dụng giao dịch và nghiên cứu mô hình diễn viên , xem SEDA, v.v.

Trong bài trình bày của họ, họ đã đề cập rằng mẫu này về cơ bản là cách các bộ định tuyến hoạt động; tuy nhiên tôi không tìm thấy bất kỳ mô tả hay nào về cách thức hoạt động của bộ định tuyến.

Có một số gợi ý tốt để giải thích tốt hơn?

Câu trả lời:


210

Dự án Google Code tham chiếu một bài viết kỹ thuật về việc triển khai bộ đệm vòng, tuy nhiên nó hơi khô khan, hàn lâm và khó khăn cho ai đó muốn tìm hiểu cách thức hoạt động của nó. Tuy nhiên, có một số bài viết trên blog đã bắt đầu giải thích nội bộ theo cách dễ đọc hơn. Có một lời giải thích về bộ đệm vòng là cốt lõi của mẫu người gây rối, mô tả về các rào cản của người tiêu dùng (phần liên quan đến việc đọc từ người gây rối) và một số thông tin về việc xử lý nhiều nhà sản xuất có sẵn.

Mô tả đơn giản nhất về Disruptor là: Đó là cách gửi tin nhắn giữa các luồng theo cách hiệu quả nhất có thể. Nó có thể được sử dụng thay thế cho hàng đợi, nhưng nó cũng chia sẻ một số tính năng với SEDA và Actors.

So với hàng đợi:

Disruptor cung cấp khả năng truyền thông điệp đến các luồng khác, đánh thức nó nếu cần (tương tự như BlockingQueue). Tuy nhiên, có 3 sự khác biệt rõ ràng.

  1. Người dùng Disruptor định nghĩa cách các thông điệp được lưu trữ bằng cách mở rộng lớp Entry và cung cấp một nhà máy để thực hiện việc phân bổ. Điều này cho phép tái sử dụng bộ nhớ (sao chép) hoặc Entry có thể chứa tham chiếu đến đối tượng khác.
  2. Đưa tin nhắn vào Disruptor là quy trình 2 pha, đầu tiên, một vị trí được yêu cầu trong bộ đệm vòng, cung cấp cho người dùng Mục nhập có thể được điền với dữ liệu thích hợp. Sau đó, mục nhập phải được cam kết, cách tiếp cận 2 pha này là cần thiết để cho phép sử dụng linh hoạt bộ nhớ được đề cập ở trên. Đó là cam kết làm cho thông điệp hiển thị cho các chủ đề của người tiêu dùng.
  3. Người tiêu dùng có trách nhiệm theo dõi các thông điệp đã được sử dụng từ bộ đệm vòng. Việc chuyển trách nhiệm này ra khỏi bộ đệm vòng đã giúp giảm lượng tranh chấp khi mỗi luồng duy trì bộ đếm riêng.

So với diễn viên

Mô hình Actor gần với Disruptor hơn hầu hết các mô hình lập trình khác, đặc biệt nếu bạn sử dụng các lớp BatchConsumer / BatchHandler được cung cấp. Các lớp này ẩn tất cả các phức tạp của việc duy trì số thứ tự đã tiêu thụ và cung cấp một tập hợp các cuộc gọi lại đơn giản khi các sự kiện quan trọng xảy ra. Tuy nhiên, có một vài sự khác biệt tinh tế.

  1. Disruptor sử dụng mô hình tiêu dùng 1 luồng - 1, trong đó Diễn viên sử dụng mô hình N: M tức là bạn có thể có bao nhiêu diễn viên tùy thích và họ sẽ được phân phối trên một số luồng cố định (thường là 1 trên mỗi lõi).
  2. Giao diện BatchHandler cung cấp một cuộc gọi lại bổ sung (và rất quan trọng) onEndOfBatch(). Điều này cho phép người tiêu dùng chậm, ví dụ những người thực hiện I / O cho các sự kiện hàng loạt cùng nhau để cải thiện thông lượng. Có thể thực hiện việc tạo khối trong các khung Actor khác, tuy nhiên vì gần như tất cả các khung khác không cung cấp một cuộc gọi lại vào cuối đợt, bạn cần sử dụng thời gian chờ để xác định kết thúc đợt, dẫn đến độ trễ kém.

So với SEDA

LMAX đã xây dựng mô hình Disruptor để thay thế cách tiếp cận dựa trên SEDA.

  1. Cải tiến chính mà nó cung cấp so với SEDA là khả năng thực hiện công việc song song. Để làm điều này, Disruptor hỗ trợ truyền nhiều tin nhắn giống nhau (theo cùng một thứ tự) cho nhiều người tiêu dùng. Điều này tránh sự cần thiết cho các giai đoạn ngã ba trong đường ống.
  2. Chúng tôi cũng cho phép người tiêu dùng chờ đợi kết quả của những người tiêu dùng khác mà không phải đặt một giai đoạn xếp hàng khác giữa họ. Một người tiêu dùng có thể chỉ cần xem số thứ tự của một người tiêu dùng mà nó phụ thuộc vào. Điều này tránh sự cần thiết cho các giai đoạn tham gia trong đường ống.

So với rào cản bộ nhớ

Một cách khác để suy nghĩ về nó là một hàng rào bộ nhớ có cấu trúc, có trật tự. Trong đó rào cản nhà sản xuất tạo thành rào cản ghi và rào cản người tiêu dùng là rào cản đọc.


1
Cảm ơn Michael. Bài viết của bạn và các liên kết bạn cung cấp đã giúp tôi hiểu rõ hơn về cách thức hoạt động của nó. Phần còn lại, tôi nghĩ rằng tôi chỉ cần để nó chìm vào.
Shahbaz

Tôi vẫn có câu hỏi: (1) 'cam kết' hoạt động như thế nào? (2) Khi bộ đệm vòng đầy, nhà sản xuất phát hiện ra rằng tất cả người tiêu dùng đã xem dữ liệu để nhà sản xuất có thể sử dụng lại các mục?
Qwertie

@Qwertie, có lẽ đáng để đăng một câu hỏi mới.
Michael Barker

1
Không nên là câu đầu tiên của dấu đầu dòng cuối cùng (số 2) trong So với SEDA thay vì đọc "Chúng tôi cũng cho phép người tiêu dùng chờ đợi kết quả của những người tiêu dùng khác khi phải đặt một giai đoạn xếp hàng khác giữa họ" đọc "Chúng tôi cũng cho phép người tiêu dùng chờ đợi kết quả của những người tiêu dùng khác mà không phải đặt một giai đoạn xếp hàng khác giữa họ "(nghĩa là" bằng "nên được thay thế bằng" không có ")?
runeks

@runeks, đúng vậy.
Michael Barker

135

Đầu tiên chúng tôi muốn hiểu mô hình lập trình mà nó cung cấp.

Có một hoặc nhiều nhà văn. Có một hoặc nhiều độc giả. Có một dòng các mục, hoàn toàn theo thứ tự từ cũ đến mới (hình từ trái sang phải). Nhà văn có thể thêm các mục mới ở phía bên phải. Mỗi người đọc đọc các mục liên tục từ trái sang phải. Độc giả không thể đọc các nhà văn trong quá khứ, rõ ràng.

Không có khái niệm xóa mục nhập. Tôi sử dụng "trình đọc" thay vì "người tiêu dùng" để tránh hình ảnh của các mục được tiêu thụ. Tuy nhiên, chúng tôi hiểu rằng các mục bên trái của người đọc cuối cùng trở nên vô dụng.

Nói chung độc giả có thể đọc đồng thời và độc lập. Tuy nhiên chúng tôi có thể tuyên bố sự phụ thuộc giữa các độc giả. Phụ thuộc độc giả có thể là đồ thị chu kỳ tùy ý. Nếu người đọc B phụ thuộc vào người đọc A, người đọc B không thể đọc người đọc A.

Phụ thuộc người đọc phát sinh vì người đọc A có thể chú thích một mục và người đọc B phụ thuộc vào chú thích đó. Ví dụ: A thực hiện một số tính toán trên một mục nhập và lưu trữ kết quả trong trường atrong mục nhập. A sau đó tiếp tục và bây giờ B có thể đọc mục nhập và giá trị của aA được lưu trữ. Nếu người đọc C không phụ thuộc vào A, C không nên cố đọc a.

Đây thực sự là một mô hình lập trình thú vị. Bất kể hiệu suất, một mình mô hình có thể mang lại lợi ích cho rất nhiều ứng dụng.

Tất nhiên, mục tiêu chính của LMAX là hiệu suất. Nó sử dụng một vòng được phân bổ trước. Chiếc nhẫn đủ lớn, nhưng nó được giới hạn để hệ thống sẽ không được tải vượt quá khả năng thiết kế. Nếu chiếc nhẫn đầy, nhà văn sẽ đợi cho đến khi những người đọc chậm nhất tiến lên và nhường chỗ.

Các đối tượng nhập cảnh được phân bổ trước và sống mãi mãi, để giảm chi phí thu gom rác. Chúng tôi không chèn các đối tượng mục nhập mới hoặc xóa các đối tượng mục nhập cũ, thay vào đó, một nhà văn yêu cầu một mục nhập có sẵn, điền vào các trường của nó và thông báo cho độc giả. Hành động 2 pha rõ ràng này thực sự chỉ đơn giản là một hành động nguyên tử

setNewEntry(EntryPopulator);

interface EntryPopulator{ void populate(Entry existingEntry); }

Các mục nhập phân bổ trước cũng có nghĩa là các mục liền kề (rất có thể) định vị trong các ô nhớ liền kề và bởi vì các trình đọc đọc các mục liên tục, điều này rất quan trọng để sử dụng bộ đệm CPU.

Và rất nhiều nỗ lực để tránh khóa, CAS, thậm chí là rào cản bộ nhớ (ví dụ: sử dụng biến chuỗi không biến động nếu chỉ có một người viết)

Đối với các nhà phát triển độc giả: Độc giả chú thích khác nhau nên viết vào các lĩnh vực khác nhau, để tránh tranh chấp viết. (Trên thực tế họ nên ghi vào các dòng bộ đệm khác nhau.) Một trình đọc chú thích không nên chạm vào bất cứ thứ gì mà các trình đọc không phụ thuộc khác có thể đọc. Đây là lý do tại sao tôi nói những độc giả này chú thích các mục, thay vì sửa đổi các mục.


2
Có vẻ ổn với tôi. Tôi thích việc sử dụng thuật ngữ chú thích.
Michael Barker

21
+1 đây là câu trả lời duy nhất cố gắng mô tả cách thức mẫu gây rối thực sự hoạt động, như OP đã hỏi.
G-Wiz

1
Nếu chiếc nhẫn đầy, nhà văn sẽ đợi cho đến khi những người đọc chậm nhất tiến lên và nhường chỗ. - một trong những vấn đề với hàng đợi FIFO sâu là khiến chúng quá dễ dàng đầy tải, vì chúng không thực sự cố gắng gây áp lực cho đến khi chúng bị nhồi và độ trễ đã cao.
bestsss

1
@irreputable Bạn cũng có thể viết lời giải thích tương tự cho phía nhà văn không?
Buchi

Tôi thích nó nhưng tôi thấy điều này "một nhà văn yêu cầu một mục có sẵn, điền vào các lĩnh vực của nó và thông báo cho độc giả. Hành động 2 pha rõ ràng này thực sự chỉ đơn giản là một hành động nguyên tử" khó hiểu và có thể sai? Không có "thông báo" phải không? Ngoài ra, nó không phải là nguyên tử, nó chỉ là một cách viết hiệu quả / có thể nhìn thấy, đúng không? Câu trả lời tuyệt vời chỉ là ngôn ngữ mơ hồ?
HaveAGuess

41

Martin Fowler đã viết một bài báo về LMAX và mô hình kẻ gây rối, Kiến trúc LMAX , có thể làm rõ hơn nữa.


17

Tôi thực sự đã dành thời gian để nghiên cứu nguồn thực tế, vì tò mò và ý tưởng đằng sau nó khá đơn giản. Phiên bản gần đây nhất tại thời điểm viết bài này là 3.2.1.

Có một bộ đệm lưu trữ các sự kiện được phân bổ trước sẽ giữ dữ liệu cho người tiêu dùng đọc.

Bộ đệm được hỗ trợ bởi một mảng các cờ (mảng số nguyên) có độ dài mô tả sự sẵn có của các vị trí đệm (xem thêm để biết chi tiết). Mảng được truy cập như java # AtomicIntegerArray, vì vậy với mục đích khám phá này, bạn cũng có thể giả sử nó là một.

Có thể có bất kỳ số lượng các nhà sản xuất. Khi nhà sản xuất muốn ghi vào bộ đệm, một số dài được tạo ra (như khi gọi AtomicLong # getAndIncrement, Disruptor thực sự sử dụng triển khai riêng của mình, nhưng nó hoạt động theo cách tương tự). Chúng ta hãy gọi điều này được tạo ra từ lâu là nhà sản xuấtCallId. Theo cách tương tự, ConsumerCallId được tạo khi người tiêu dùng KẾT THÚC đọc một vị trí từ bộ đệm. Người tiêu dùng gần đây nhất được truy cập.

(Nếu có nhiều người tiêu dùng, cuộc gọi có id thấp nhất sẽ được chọn.)

Các id này sau đó được so sánh và nếu sự khác biệt giữa hai id nhỏ hơn bên bộ đệm, nhà sản xuất được phép viết.

(Nếu nhà sản xuấtCallId lớn hơn ConsumerCallId + bufferSize gần đây, điều đó có nghĩa là bộ đệm đã đầy và nhà sản xuất buộc phải chờ xe buýt cho đến khi có chỗ.)

Sau đó, nhà sản xuất được chỉ định vị trí trong bộ đệm dựa trên callId của anh ta (đó là prducerCallId modulo bufferSize, nhưng vì bộ đệm kích thước luôn có sức mạnh bằng 2 (giới hạn được thực thi khi tạo bộ đệm), nên thao tác Actuall được sử dụng là ProducCallId & (bufferSize - 1 )). Sau đó, nó là miễn phí để sửa đổi sự kiện trong khe đó.

(Thuật toán thực tế phức tạp hơn một chút, liên quan đến bộ nhớ cache của người tiêu dùng gần đây trong một tài liệu tham khảo nguyên tử riêng biệt, cho mục đích tối ưu hóa.)

Khi sự kiện được sửa đổi, thay đổi là "xuất bản". Khi xuất bản vị trí tương ứng trong mảng cờ được điền với cờ đã cập nhật. Giá trị cờ là số vòng lặp (managerCallId chia cho đệmSize (một lần nữa vì đệmSize là lũy thừa của 2, hoạt động thực tế là một ca đúng).

Theo cách tương tự có thể có bất kỳ số lượng người tiêu dùng. Mỗi khi người tiêu dùng muốn truy cập vào bộ đệm, một ConsumerCallId được tạo ra (tùy thuộc vào cách người tiêu dùng được thêm vào người gây rối, nguyên tử được sử dụng trong thế hệ id có thể được chia sẻ hoặc tách riêng cho từng người trong số họ). ConsumerCallId này sau đó được so sánh với producentCallId gần đây nhất và nếu nó kém hơn hai, người đọc được phép tiến bộ.

(Tương tự nếu nhà sản xuấtCallId thậm chí là với ConsumerCallId, điều đó có nghĩa là bộ đệm là hợp lý và người tiêu dùng buộc phải chờ đợi. Cách chờ đợi được xác định bởi WaitStrargety trong quá trình tạo ra kẻ gây rối.)

Đối với người tiêu dùng cá nhân (những người có trình tạo id riêng), điều tiếp theo được kiểm tra là khả năng tiêu thụ hàng loạt. Các vị trí trong bộ đệm được kiểm tra theo thứ tự từ một vị trí tương ứng với ConsumerCallId (chỉ số được xác định theo cách tương tự như đối với nhà sản xuất), đến vị trí tương ứng với nhà sản xuất gần đây của nhà sản xuất.

Chúng được kiểm tra trong một vòng lặp bằng cách so sánh giá trị cờ được viết trong mảng cờ, so với giá trị cờ được tạo cho ConsumerCallId. Nếu các cờ trùng khớp, điều đó có nghĩa là các nhà sản xuất điền vào các vị trí đã cam kết thay đổi của họ. Nếu không, vòng lặp bị hỏng và thay đổi được cam kết cao nhất được trả về. Các vị trí từ ConsumerCallId đến nhận được trong ChangeId có thể được tiêu thụ theo đợt.

Nếu một nhóm người tiêu dùng đọc cùng nhau (những người có trình tạo id dùng chung), mỗi người chỉ nhận một cuộc gọi duy nhất và chỉ có vị trí cho cuộc gọi duy nhất đó được kiểm tra và trả lại.


7

Từ bài viết này :

Mẫu ngắt là một hàng đợi được sao lưu bởi một mảng tròn (tức là bộ đệm vòng) chứa đầy các đối tượng chuyển được phân bổ trước, sử dụng các rào cản bộ nhớ để đồng bộ hóa nhà sản xuất và người tiêu dùng thông qua các chuỗi.

Rào cản bộ nhớ là loại khó giải thích và blog của Trisha đã thực hiện nỗ lực tốt nhất theo ý kiến ​​của tôi với bài đăng này: http://mechanitis.blogspot.com/2011/08/dissecting-disruptor-why-its-so-fast. html

Nhưng nếu bạn không muốn đi sâu vào các chi tiết cấp thấp, bạn có thể biết rằng các rào cản bộ nhớ trong Java được thực hiện thông qua volatiletừ khóa hoặc thông qua java.util.concurrent.AtomicLong. Trình tự mô hình của kẻ gây rối là AtomicLongs và được truyền đạt qua lại giữa các nhà sản xuất và người tiêu dùng thông qua các rào cản bộ nhớ thay vì khóa.

Tôi tìm thấy nó dễ dàng hơn để hiểu một khái niệm thông qua mã, do đó mã dưới đây là một đơn giản helloworld từ CoralQueue , mà là một thực hiện mô hình gây rối loạn thực hiện bằng cách CoralBlocks mà tôi đang liên kết. Trong đoạn mã dưới đây, bạn có thể thấy cách mẫu ngắt thực hiện việc tạo khối và cách bộ đệm vòng (tức là mảng tròn) cho phép giao tiếp không có rác giữa hai luồng:

package com.coralblocks.coralqueue.sample.queue;

import com.coralblocks.coralqueue.AtomicQueue;
import com.coralblocks.coralqueue.Queue;
import com.coralblocks.coralqueue.util.MutableLong;

public class Sample {

    public static void main(String[] args) throws InterruptedException {

        final Queue<MutableLong> queue = new AtomicQueue<MutableLong>(1024, MutableLong.class);

        Thread consumer = new Thread() {

            @Override
            public void run() {

                boolean running = true;

                while(running) {
                    long avail;
                    while((avail = queue.availableToPoll()) == 0); // busy spin
                    for(int i = 0; i < avail; i++) {
                        MutableLong ml = queue.poll();
                        if (ml.get() == -1) {
                            running = false;
                        } else {
                            System.out.println(ml.get());
                        }
                    }
                    queue.donePolling();
                }
            }

        };

        consumer.start();

        MutableLong ml;

        for(int i = 0; i < 10; i++) {
            while((ml = queue.nextToDispatch()) == null); // busy spin
            ml.set(System.nanoTime());
            queue.flush();
        }

        // send a message to stop consumer...
        while((ml = queue.nextToDispatch()) == null); // busy spin
        ml.set(-1);
        queue.flush();

        consumer.join(); // wait for the consumer thread to die...
    }
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.