ClickHouse kümesini yüksek erişilebilirlik için kurmak

Selamlar, bu yazımda ClickHouse'u tek node olmaktan çıkarıp gerçek anlamda kümeli, replikalı bir analitik motora dönüştürmeye bakacağız. Tek makineli ClickHouse geliştirme için yeter de artar bile; ama prod'da bir node düştüğü an analitik pipeline'ınız sessizce duruyor. Bunu kabullenmek istemiyorsak işin biraz daha derinine inmemiz gerekiyor.

Neden kümeleme?

ClickHouse'un cazibesi tek başına milyarlarca satırı tarayabilmesi. Gel gör ki tek node demek tek arıza noktası demek. Kümeleme iki şeyi birden veriyor: shard ile veriyi yatayda parçalıyoruz, replica ile her parçanın kopyasını tutuyoruz. Birincisi ölçeklenme, ikincisi dayanıklılık.

Karıştırılan nokta şu: shard farklı veri, replica aynı veri. Bence ilk başta zihinsel modelinizde bunu net oturtmak en kritik adım, gerisi konfigürasyon detayı.

Koordinasyon: Keeper'ı tercih edin

Replikalar arası senkronizasyon için bir koordinasyon servisi şart. Eskiden ZooKeeper standarttı, ama açıkçası ben yeni kuruluyorsa artık ClickHouse Keeper diyorum. Aynı protokolü konuşur, JVM yükü yok, daha az bellek yer ve operasyonu çok daha sade.

3 node'luk Keeper grubu çoğu yük için yeter. Her birinde benzer bir konfig dosyası oluyor:

<clickhouse>
    <keeper_server>
        <tcp_port>9181</tcp_port>
        <server_id>1</server_id>
        <log_storage_path>/var/lib/clickhouse-keeper/log</log_storage_path>
        <snapshot_storage_path>/var/lib/clickhouse-keeper/snapshots</snapshot_storage_path>
        <raft_configuration>
            <server><id>1</id><hostname>keeper-1.local</hostname><port>9234</port></server>
            <server><id>2</id><hostname>keeper-2.local</hostname><port>9234</port></server>
            <server><id>3</id><hostname>keeper-3.local</hostname><port>9234</port></server>
        </raft_configuration>
    </keeper_server>
</clickhouse>

Her node'da server_id farklı; gerisi aynı. Çalışıp çalışmadığını dört harfli klasik komutla doğrularız:

echo ruok | nc keeper-1.local 9181
# imok dönmesi gerekiyor

Küme tanımı

Şimdi her ClickHouse node'una cluster.xml koyacağız. İki shard, her birinde iki replica gibi düşünelim:

<clickhouse>
    <remote_servers>
        <analytics_cluster>
            <shard>
                <internal_replication>true</internal_replication>
                <replica><host>ch-1a.local</host><port>9000</port></replica>
                <replica><host>ch-1b.local</host><port>9000</port></replica>
            </shard>
            <shard>
                <internal_replication>true</internal_replication>
                <replica><host>ch-2a.local</host><port>9000</port></replica>
                <replica><host>ch-2b.local</host><port>9000</port></replica>
            </shard>
        </analytics_cluster>
    </remote_servers>
</clickhouse>

internal_replication=true çok önemli; bu olmadan Distributed tablo aynı veriyi her replikaya tek tek yazmaya çalışır, ReplicatedMergeTree'nin işini bozar. Yani hem yazımı çiftler hem de tutarlılığı tehlikeye atarsınız.

Her node kendini tanısın diye bir de macros.xml lazım. Mesela ch-1a için:

<clickhouse>
    <macros>
        <shard>01</shard>
        <replica>ch-1a</replica>
    </macros>
</clickhouse>

Tablolar: önce yerel, sonra dağıtık

Kalıp şu: her node'da bir ReplicatedMergeTree tablosu (yerel), sonra üzerinde bir Distributed tablo (sorgu cephesi).

CREATE TABLE events_local ON CLUSTER analytics_cluster
(
    event_id UUID,
    event_type String,
    user_id UInt64,
    event_time DateTime
)
ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/events', '{replica}')
PARTITION BY toYYYYMM(event_time)
ORDER BY (event_type, user_id, event_time);

CREATE TABLE events ON CLUSTER analytics_cluster AS events_local
ENGINE = Distributed('analytics_cluster', 'default', 'events_local', xxHash64(user_id));

{shard} ve {replica} makroları az önce yazdığımız macros.xml'den geliyor. Sharding anahtarı için rand() yerine xxHash64(user_id) tercih ettim çünkü aynı kullanıcının verisi aynı shard'da kalsın istiyorum; yan yana veri analitikte hayatı kolaylaştırıyor.

Failover testi

Replikalardan birini kapatın:

sudo systemctl stop clickhouse-server

SELECT count() FROM events hâlâ dönüyor mu? Dönüyorsa yarısı oldu. O sırada INSERT da atın. Sonra node'u tekrar açın ve replication kuyruğuna bakın:

SELECT table, num_tries, last_exception
FROM system.replication_queue
WHERE table = 'events_local';

Birkaç saniye içinde kuyruk boşalıyorsa replica leader'a yetişti demektir.

Sık karşılaşılan tuzaklar

  • internal_replication'ı false bırakmak: Distributed tablo veriyi her replikaya kendisi yazmaya çalışır, ReplicatedMergeTree de aynı veriyi tekrar replike etmeye uğraşır. Sonuç: çift kayıt ya da çakışma.
  • Keeper'ı ClickHouse node'larıyla aynı sunucuda tutmak: Kısa vadede çalışır, ağır sorgu anında Keeper aç kalır, oturumlar düşer. Bence küçük kümelerde bile Keeper'ı ayrı tutmaya değer.
  • Çok fazla partition: PARTITION BY toYYYYMMDD(...) cazip görünür ama tablo başına 1000 partition'ı geçince merge'ler yavaşlar. Aylık ya da haftalık yeter.
  • Session timeout'unun varsayılana takılı kalması: Ağ titrek bir ortamdaysanız session_timeout_ms'i 30-60 saniyeye çekmek ZooKeeper session expired hatalarının yarısını eritir.

Kapanış

Bu yazımızda ClickHouse kümesini Keeper koordinasyonu, ReplicatedMergeTree ve Distributed tablolarla nasıl kuracağımıza, failover'ı nasıl test edeceğimize baktık. Şahsi kanaatim, 2 shard + her shard'da 2 replica çoğu ekip için sağlam bir başlangıç; veri büyüdükçe shard eklemek replica eklemekten her zaman daha verimli oluyor. Umarım faydalı olur, bir sonraki yazıda görüşmek üzere.