LMAX'ın bozucu modeli nasıl çalışır?


205

Yıkıcı modelini anlamaya çalışıyorum . InfoQ videosunu izledim ve makalelerini okumaya çalıştım. Ben bir önbellek ilgili, önbellek yöreden yararlanmak, yeni bellek tahsisi ortadan kaldırmak için son derece büyük bir dizi olarak başlatıldığını anlıyorum.

Konumları takip eden bir veya daha fazla atomik tamsayı varmış gibi geliyor. Her 'olay' benzersiz bir kimlik kazanıyor gibi görünüyor ve halkadaki konumu, halkanın boyutuna vb. Göre modülünü bularak bulunur.

Ne yazık ki, nasıl çalıştığına dair sezgisel bir fikrim yok. Birçok ticaret uygulaması yaptım ve oyuncu modelini inceledim, SEDA'ya baktım vb.

Sunumlarında bu modelin temel olarak yönlendiricilerin nasıl çalıştığını; ancak yönlendiricilerin de nasıl çalıştığına dair iyi bir açıklama bulamadım.

Daha iyi bir açıklamaya bazı iyi işaretler var mı?

Yanıtlar:


210

Google Code projesi , halka arabelleğinin uygulanmasıyla ilgili teknik bir makaleye atıfta bulunur, ancak nasıl çalıştığını öğrenmek isteyen biri için biraz kuru, akademik ve zorlayıcıdır. Bununla birlikte, iç kısımları daha okunabilir bir şekilde açıklamaya başlayan bazı blog gönderileri vardır. Bir vardır halka tamponu açıklama bölücü deseninin çekirdeği, bir tüketici engellerin tanımı (bozucu okuma ilişkin kısım) ve bazı çok üreticileri taşıma modülleri kullanılabilir.

Disruptor'ın en basit açıklaması: İletiler arasında mümkün olan en verimli şekilde mesaj göndermenin bir yoludur. Bir kuyruğa alternatif olarak kullanılabilir, ancak aynı zamanda SEDA ve Aktörlerle bir dizi özelliği paylaşır.

Kuyruklarla karşılaştırıldığında:

Disruptor, bir mesajı başka bir iş parçacığına iletme ve gerekirse bir uyanma (bir BlockingQueue'ya benzer) sağlar. Bununla birlikte, 3 farklı fark vardır.

  1. Disruptor kullanıcısı, Entry sınıfını genişleterek ve ön-konumlandırma için bir fabrika sağlayarak mesajların nasıl saklanacağını tanımlar. Bu, belleğin yeniden kullanılmasına (kopyalanmasına) izin verir veya Giriş başka bir nesneye başvuru içerebilir.
  2. Disruptor'a mesaj koymak 2 aşamalı bir işlemdir, önce halka tamponunda kullanıcıya uygun verilerle doldurulabilen Giriş sağlayan bir yuva talep edilir. Daha sonra giriş taahhüt edilmelidir, bu iki fazlı yaklaşım, yukarıda belirtilen belleğin esnek kullanımına izin vermek için gereklidir. İletiyi tüketici konuları için görünür kılan taahhüttür.
  3. Halka tamponundan tüketilen mesajları takip etmek tüketicinin sorumluluğundadır. Bu sorumluluğu halka arabelleğinden uzaklaştırmak, her bir iş parçacığı kendi sayacını korurken yazma çekişme miktarını azaltmaya yardımcı oldu.

Aktörlere Göre

Actor modeli, özellikle sağlanan BatchConsumer / BatchHandler sınıflarını kullanıyorsanız, Disruptor'ı diğer birçok programlama modelinden daha yakındır. Bu sınıflar, tüketilen sıra numaralarını korumanın tüm karmaşıklıklarını gizler ve önemli olaylar meydana geldiğinde bir dizi basit geri arama sağlar. Bununla birlikte, birkaç ince fark vardır.

  1. Disruptor, 1 iş parçacığı - 1 tüketici modeli kullanır; burada Aktörler N: M modeli kullanır, yani istediğiniz sayıda aktöre sahip olabilirsiniz ve bunlar sabit sayıda iş parçacığına (genellikle çekirdek başına 1) dağıtılır.
  2. BatchHandler arabirimi ek (ve çok önemli) bir geri arama sağlar onEndOfBatch(). Bu, verimi artırmak için olayları birlikte toplu olarak G / Ç yapanların yavaş tüketicilere olanak tanır. Diğer Aktör çerçevelerinde toplu işlem yapmak mümkündür, ancak neredeyse tüm diğer çerçeveler toplu işin sonunda bir geri arama sağlamadığından, toplu işin sonunu belirlemek için bir zaman aşımı kullanmanız gerekir, bu da gecikme süresine neden olur.

SEDA ile karşılaştırıldığında

LMAX, SEDA tabanlı bir yaklaşımın yerini almak için Disruptor modelini oluşturdu.

  1. SEDA üzerinde sağladığı en büyük gelişme paralel olarak çalışma yeteneğiydi. Bunu yapmak için Disruptor aynı mesajların (aynı sırayla) birden fazla tüketiciye çoklu yayınlanmasını destekler. Bu, boru hattındaki çatal kademelerine olan ihtiyacı ortadan kaldırır.
  2. Ayrıca, tüketicilerin aralarında başka bir kuyruklama aşaması yapmak zorunda kalmadan diğer tüketicilerin sonuçlarını beklemelerine izin veriyoruz. Bir tüketici sadece bağımlı olduğu bir tüketicinin sıra numarasını izleyebilir. Bu, boru hattındaki birleştirme aşamalarına olan ihtiyacı ortadan kaldırır.

Bellek Engelleri ile karşılaştırıldığında

Bunu düşünmenin başka bir yolu da yapılandırılmış, düzenli bir bellek bariyeri. Üretici engelinin yazma engelini oluşturduğu ve tüketici engelinin okuma engelidir.


1
Teşekkürler Michael. Yazmanız ve sağladığınız bağlantılar, nasıl çalıştığını daha iyi anlamama yardımcı oldu. Gerisi, sanırım batmasına izin vermeliyim.
Shahbaz

Hala sorularım var: (1) 'taahhüt' nasıl çalışır? (2) Halka tamponu dolduğunda, üretici tüm tüketicilerin verileri gördüğünü nasıl tespit eder, böylece üretici girişleri tekrar kullanabilir?
Qwertie

@Qwertie, muhtemelen yeni bir soru göndermeye değer.
Michael Barker

1
Son mermi noktasının (2 sayısı) ilk cümlesini okumak yerine SEDA ile kıyaslamak yerine “Tüketicilerin diğer tüketicilerin sonuçlarını aralarında başka bir kuyruk aşaması koymak zorunda kalmasını beklemelerine izin veriyoruz” tüketiciler aralarında başka bir kuyruklandırma aşaması koymak zorunda kalmadan diğer tüketicilerin sonuçlarını beklemek "(yani. ile" yerine "olmadan")?
runeks

@ runeks, evet olmalı.
Michael Barker

135

İlk önce sunduğu programlama modelini anlamak istiyoruz.

Bir ya da daha fazla yazar var. Bir veya daha fazla okuyucu var. Tamamen eskiden yeniye doğru sıralanan bir giriş satırı vardır (soldan sağa olarak resmedilmiştir). Yazarlar sağ tarafa yeni girişler ekleyebilir. Her okuyucu girişleri soldan sağa sırayla okur. Okuyucular açıkçası geçmiş yazarları okuyamazlar.

Giriş silme kavramı yoktur. Tüketilen girdilerin görüntüsünü önlemek için "tüketici" yerine "okuyucu" kullanıyorum. Ancak, son okuyucunun solundaki girişlerin işe yaramaz hale geldiğini biliyoruz.

Genellikle okuyucular aynı anda ve bağımsız olarak okuyabilirler. Ancak, okuyucular arasında bağımlılıklar beyan edebiliriz. Okuyucu bağımlılıkları keyfi döngüsel olmayan bir grafik olabilir. B okuyucu A okuyucuya bağlıysa, B okuyucu A okuyucusunu okuyamaz.

Okuyucu bağımlılığı, A okuyucu bir girdiye ek açıklama ekleyebildiği ve B okuyucu bu ek açıklamaya bağlı olduğu için ortaya çıkar. Örneğin, A giriş üzerinde bazı hesaplamalar yapar ve sonucu agirişteki alanda saklar . A daha sonra devam edin ve şimdi B girişi ve aA'nın değerini kaydedebilir . C okuyucu A'ya bağlı değilse, C okumaya çalışmamalıdır a.

Bu gerçekten ilginç bir programlama modelidir. Performanstan bağımsız olarak, model tek başına birçok uygulamadan yararlanabilir.

Tabii ki, LMAX'ın ana hedefi performanstır. Önceden tahsis edilmiş bir kayıtlar halkası kullanır. Halka yeterince büyük, ancak sistem tasarım kapasitesinin ötesinde yüklenmeyecek şekilde sınırlandırıldı. Halka doluysa, yazar (lar) en yavaş okuyucular ilerleyip yer açana kadar bekleyecektir.

Çöp toplama maliyetini azaltmak için giriş nesneleri önceden tahsis edilir ve sonsuza kadar yaşar. Yeni giriş nesneleri eklemiyoruz veya eski giriş nesnelerini silmiyoruz, bunun yerine bir yazar önceden var olan bir giriş ister, alanlarını doldurur ve okuyucuları bilgilendirir. Bu görünür 2 fazlı eylem gerçekten basitçe atomik bir eylemdir

setNewEntry(EntryPopulator);

interface EntryPopulator{ void populate(Entry existingEntry); }

Girişleri önceden ayırmak aynı zamanda bitişik girişlerin (büyük olasılıkla) bitişik bellek hücrelerine yerleştirilmesi anlamına gelir ve okuyucular girişleri sırayla okuduğundan CPU önbelleklerini kullanmak önemlidir.

Ve kilit, CAS ve hatta bellek bariyerinden kaçınmak için çok çaba gösterin (örneğin, sadece bir yazar varsa geçici olmayan bir dizi değişkeni kullanın)

Okuyucu geliştiriciler için: Yazma çekişmesini önlemek için farklı not alan okuyucular farklı alanlara yazmalıdır. (Aslında farklı önbellek satırlarına yazmaları gerekir.) Açıklayıcı bir okuyucu, bağımlı olmayan diğer okuyucuların okuyabileceği hiçbir şeye dokunmamalıdır. Bu yüzden, bu okuyucuların değiştirmek yerine girişlere açıklama eklemelerini söylüyorum .


2
Bana iyi geliyor. Ek açıklama teriminin kullanılmasını seviyorum.
Michael Barker

21
+1, OP'nin istediği gibi, bozucu modelin gerçekte nasıl çalıştığını açıklamaya çalışan tek cevaptır.
G-Wiz

1
Halka doluysa, yazar (lar) en yavaş okuyucular ilerleyip yer açana kadar bekleyecektir. - derin FIFO kuyrukları ile ilgili problemlerden biri, dolduruluncaya ve gecikme süresi zaten yüksek olana kadar gerçekten baskı yapmaya çalışmadığından, yük altında çok kolay bir şekilde dolu hale getirmektir.
bestsss

1
@irreputable Yazar tarafı için de benzer açıklamalar yazabilir misiniz?
Buchi

Sevdim ama bunu buldum "bir yazar önceden var olan bir giriş ister, alanlarını doldurmak ve okuyucuları bilgilendirmek. Bu görünür 2-fazlı eylem gerçekten basit bir atom eylem" kafa karıştırıcı ve muhtemelen yanlış? "Bildirim" yok, değil mi? Ayrıca atomik değil, sadece tek bir etkili / görünür yazma, değil mi? Sadece belirsiz olan dilde harika bir cevap mı?
HaveAGuess


17

Aslında gerçek kaynağı, meraktan çıkarmak için zaman ayırdım ve arkasındaki fikir oldukça basit. Bu yazıyı yazarken en son sürüm 3.2.1'dir.

Tüketicilerin okuması için verileri tutacak önceden tahsis edilmiş olayları saklayan bir arabellek vardır.

Arabellek, arabellek yuvalarının kullanılabilirliğini açıklayan bir dizi bayrakla (tamsayı dizisi) desteklenir (ayrıntılar için daha fazla bilgi). Diziye bir java # AtomicIntegerArray gibi erişilir, bu nedenle bu açıklama için bir tane olduğunu varsayabilirsiniz.

Herhangi bir sayıda üretici olabilir. Üretici arabelleğe yazmak istediğinde, uzun bir sayı üretilir (AtomicLong # getAndIncrement çağırırken olduğu gibi, Disruptor aslında kendi uygulamasını kullanır, ancak aynı şekilde çalışır). Buna uzun bir üreticiCallId adı verelim. Benzer bir şekilde, bir tüketici bir tampondan bir yuva okurken ENDS oluşturulur. En son ConsumerCallId öğesine erişilir.

(Çok sayıda tüketici varsa, en düşük kimliğe sahip çağrı seçilir.)

Bu kimlikler daha sonra karşılaştırılır ve ikisi arasındaki fark tampon tarafına göre daha azsa, üreticinin yazmasına izin verilir.

(ProducerCallId, yeni customerCallId + bufferSize değerinden büyükse, bu arabellek dolu demektir ve üretici bir nokta bulunana kadar veri yolunu beklemeye zorlanır.)

Üreticiye daha sonra callId (prducerCallId modulo bufferSize olan) arabellekte yuva atanır, ancak bufferSize her zaman 2 (tampon oluşturmada uygulanan sınır) gücü olduğundan, kullanılan etkin işlem manufacturerCallId & (bufferSize - 1'dir) )). Daha sonra o yuvadaki etkinliği değiştirmek ücretsizdir.

(Gerçek algoritma, optimizasyon amacıyla son tüketici kimliği ayrı bir atomik referansta önbelleğe almayı içeren biraz daha karmaşıktır.)

Etkinlik değiştirildiğinde, değişiklik "yayınlanır". Bayrak dizisindeki ilgili yuva yayınlanırken güncel bayrakla doldurulur. Bayrak değeri döngü sayısıdır (producerCallId, bufferSize değerine bölünür (yine bufferSize 2'nin gücü olduğundan, gerçek işlem sağdaki bir kaydırmadır).

Benzer bir şekilde, herhangi bir sayıda tüketici olabilir. Bir tüketici arabelleğe her erişmek istediğinde, bir customerCallId üretilir (tüketicilerin, kimlik oluşturmada kullanılan atomik her biri için paylaşılabilir veya ayrı olabilir. Bu ConsumCallId daha sonra en son producCCallId ile karşılaştırılır ve eğer ikisinden daha azsa, okuyucunun ilerlemesine izin verilir.

(Benzer şekilde manufacturerCallId, customerCallId için bile olsa, arabellek boştur ve tüketicinin beklemeye zorlandığı anlamına gelir. Bekleme şekli, yıkıcı oluşturma sırasında WaitStrategy tarafından tanımlanır.)

Bireysel tüketiciler için (kendi kimlik üreteçlerine sahip olanlar), kontrol edilen bir sonraki şey, toplu tüketim yeteneğidir. Tampondaki yuvalar, customerCallId ile ilgili olandan (endeks, üreticiler için olanla aynı şekilde belirlenir), en son manufacturerCallId ile ilgili olana göre incelenir.

Bayrak dizisinde yazılan bayrak değeri, customerCallId için oluşturulan bir bayrak değeri ile karşılaştırılarak bir döngüde incelenir. Bayraklar eşleşirse, yuvaları dolduran üreticiler değişikliklerini taahhüt etmiş demektir. Değilse, döngü bozulur ve taahhüt edilen en yüksek changeId döndürülür. ConsumerCallId ile changeId içinde alınan yuvalar toplu olarak tüketilebilir.

Bir grup tüketici birlikte okursa (paylaşılan kimlik oluşturucusuna sahip olanlar), her biri yalnızca tek bir callId alır ve yalnızca o tek callId için yuva kontrol edilir ve döndürülür.


7

Gönderen bu makalede :

Kesici desen, üreticileri ve tüketicileri diziler arasında senkronize etmek için bellek engellerini kullanan önceden tahsis edilmiş transfer nesneleri ile doldurulmuş dairesel bir dizi (yani halka tamponu) ile desteklenen bir yığın kuyruğudur.

Bellek engellerini açıklamak biraz zor ve Trisha'nın blogu benim görüşüme göre bu yazı ile en iyi girişimde bulundu: http://mechanitis.blogspot.com/2011/08/dissecting-disruptor-why-its-so-fast. html

Ancak, düşük düzeyli ayrıntılara dalmak istemiyorsanız, Java'daki bellek engellerinin volatileanahtar kelime veya java.util.concurrent.AtomicLong. Disruptor örüntü dizileri AtomicLongs'dir ve üreticiler ve tüketiciler arasında kilitler yerine bellek engelleri aracılığıyla ileri geri iletilir.

Aşağıda kod basit bir nedenle daha kolay, kod sayesinde bir kavram anlamak bulmak helloworld gelen CoralQueue ben bağlı ediyorum hangi CoralBlocks tarafından yapılan bir bölücü desen uygulamasıdır. Aşağıdaki kodda, ayırıcı modelin toplu işlemi nasıl gerçekleştirdiğini ve halka arabelleğinin (yani dairesel dizi) iki iş parçacığı arasında çöpsüz iletişime nasıl izin verdiğini görebilirsiniz:

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...
    }
}
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.