Bitva dvou yakozuna nebo Cassandra vs HBase. Zkušenosti týmu Sberbank

To není ani vtip, zdá se, že tento konkrétní obrázek nejpřesněji odráží podstatu těchto databází a nakonec bude jasné proč:

Bitva dvou yakozuna nebo Cassandra vs HBase. Zkušenosti týmu Sberbank

Podle hodnocení DB-Engines Ranking jsou dvě nejoblíbenější sloupcové databáze NoSQL Cassandra (dále CS) a HBase (HB).

Bitva dvou yakozuna nebo Cassandra vs HBase. Zkušenosti týmu Sberbank

Náš tým pro správu načítání dat ve Sberbank již z vůle osudu udělal dávno a úzce spolupracuje s HB. Během této doby jsme docela dobře prostudovali jeho silné a slabé stránky a naučili se ho vařit. Přítomnost alternativy v podobě CS nás však vždy nutila trochu se potrápit pochybnostmi: vybrali jsme správně? Navíc výsledky srovnání, v podání DataStax, řekli, že CS snadno porazí HB s téměř zdrcujícím skóre. Na druhou stranu je DataStax zainteresovanou stranou a neměli byste ji brát za slovo. Zmátlo nás také poměrně malé množství informací o podmínkách testování, a tak jsme se rozhodli zjistit na vlastní kůži, kdo je králem BigData NoSql, a získané výsledky se ukázaly jako velmi zajímavé.

Než však přejdeme k výsledkům provedených testů, je nutné popsat významné aspekty konfigurací prostředí. Faktem je, že CS lze používat v režimu, který umožňuje ztrátu dat. Tito. v tomto případě je za data určitého klíče zodpovědný pouze jeden server (uzel), a pokud z nějakého důvodu selže, hodnota tohoto klíče bude ztracena. Pro mnoho úkolů to není kritické, ale pro bankovní sektor je to spíše výjimka než pravidlo. V našem případě je důležité mít několik kopií dat pro spolehlivé uložení.

Proto byl uvažován pouze provozní režim CS v režimu trojité replikace, tzn. Vytvoření casespace bylo provedeno s následujícími parametry:

CREATE KEYSPACE ks WITH REPLICATION = {'class' : 'NetworkTopologyStrategy', 'datacenter1' : 3};

Dále existují dva způsoby, jak zajistit požadovanou úroveň konzistence. Obecné pravidlo:
SZ + NR > RF

Což znamená, že počet potvrzení z uzlů při zápisu (NW) plus počet potvrzení z uzlů při čtení (NR) musí být větší než faktor replikace. V našem případě RF = 3, což znamená, že jsou vhodné následující možnosti:
2 + 2 > 3
3 + 1 > 3

Protože je pro nás zásadně důležité ukládat data co nejspolehlivější, bylo zvoleno schéma 3+1. HB navíc funguje na podobném principu, tzn. takové srovnání bude spravedlivější.

Nutno podotknout, že DataStax ve své studii udělali opak, nastavili RF = 1 pro CS i HB (u druhého jmenovaného změnou nastavení HDFS). To je opravdu důležitý aspekt, protože dopad na výkon CS je v tomto případě obrovský. Například obrázek níže ukazuje nárůst času potřebného k načtení dat do CS:

Bitva dvou yakozuna nebo Cassandra vs HBase. Zkušenosti týmu Sberbank

Zde vidíme následující: čím více konkurenčních vláken zapisuje data, tím déle to trvá. To je přirozené, ale je důležité, aby degradace výkonu pro RF=3 byla výrazně vyšší. Jinými slovy, pokud napíšeme 4 vláken do 5 tabulek (celkem 20), pak RF=3 prohraje asi 2krát (150 sekund pro RF=3 oproti 75 pro RF=1). Pokud ale zvýšíme zátěž načtením dat do 8 tabulek po 5 vláknech (celkem 40), pak ztráta RF=3 je již 2,7násobná (375 sekund oproti 138).

Možná je to částečně tajemství úspěšného zátěžového testování provedeného DataStaxem pro CS, protože pro HB na našem stánku změna replikačního faktoru z 2 na 3 neměla žádný efekt. Tito. disky nejsou pro naši konfiguraci úzkým hrdlem HB. Je zde však mnoho dalších úskalí, protože je třeba poznamenat, že naše verze HB byla mírně záplatována a upravena, prostředí jsou zcela jiná atd. Za zmínku také stojí, že možná jen nevím, jak správně připravit CS a existují nějaké efektivnější způsoby, jak s tím pracovat, a doufám, že se to dozvíme v komentářích. Ale nejdřív.

Všechny testy byly provedeny na hardwarovém clusteru sestávajícím ze 4 serverů, každý s následující konfigurací:

CPU: Xeon E5-2680 v4 @ 2.40 GHz 64 vláken.
Disky: 12 kusů SATA HDD
Java verze: 1.8.0_111

CS verze: 3.11.5

parametry cassandra.ymlpočet_tokenů: 256
hinted_handoff_enabled: true
hinted_handoff_throttle_in_kb: 1024
max_hints_delivery_threads: 2
adresář_nápověd: /data10/cassandra/hints
hints_flush_period_in_ms: 10000 XNUMX
max_hints_file_size_in_mb: 128
batchlog_replay_throttle_in_kb: 1024
autentizátor: AllowAllAuthenticator
autorizátor: AllowAllAuthorizer
role_manager: CassandraRoleManager
roles_validity_in_ms: 2000
permits_validity_in_ms: 2000
credentials_validity_in_ms: 2000
partitioner: org.apache.cassandra.dht.Murmur3Partitioner
adresáře_datových_souborů:
- /data1/cassandra/data # každý adresář dataN je samostatný disk
- /data2/cassandra/data
- /data3/cassandra/data
- /data4/cassandra/data
- /data5/cassandra/data
- /data6/cassandra/data
- /data7/cassandra/data
- /data8/cassandra/data
Commitlog_directory: /data9/cassandra/commitlog
cdc_enabled: false
disk_failure_policy: stop
commit_failure_policy: stop
ready_statements_cache_size_mb:
thrift_prepared_statements_cache_size_mb:
key_cache_size_in_mb:
key_cache_save_period: 14400
row_cache_size_in_mb: 0
row_cache_save_period: 0
counter_cache_size_in_mb:
counter_cache_save_period: 7200
adresář uložených_mezipamětí: /data10/cassandra/saved_caches
commitlog_sync: periodický
commitlog_sync_period_in_ms: 10000
commitlog_segment_size_in_mb: 32
poskytovatel_semen:
- class_name: org.apache.cassandra.locator.SimpleSeedProvider
parametry:
— semena: "*,*"
concurrent_reads: 256 # zkusil 64 - žádný rozdíl nebyl zaznamenán
concurrent_writes: 256 # zkusil 64 - žádný rozdíl nebyl zaznamenán
concurrent_counter_writes: 256 # zkusil 64 - žádný rozdíl nebyl zaznamenán
concurrent_materialized_view_writes: 32
memtable_heap_space_in_mb: 2048 # zkusil 16 GB - bylo to pomalejší
memtable_allocation_type: heap_buffers
index_summary_capacity_in_mb:
index_summary_resize_interval_in_minutes: 60
trickle_fsync: false
trickle_fsync_interval_in_kb: 10240
storage_port: 7000
ssl_storage_port: 7001
adresa_poslechu: *
vysílací_adresa: *
listen_on_broadcast_address: true
internode_authenticator: org.apache.cassandra.auth.AllowAllInternodeAuthenticator
start_native_transport: true
native_transport_port: 9042
start_rpc: true
adresa_rpc: *
rpc_port: 9160
rpc_keepalive: true
rpc_server_type: synchronizace
thrift_framed_transport_size_in_mb: 15
incremental_backups: false
snapshot_before_compaction: false
auto_snapshot: true
column_index_size_in_kb: 64
column_index_cache_size_in_kb: 2
souběžné kompaktory: 4
compaction_throughput_mb_per_sec: 1600
sstable_preemptive_open_interval_in_mb: 50
read_request_timeout_in_ms: 100000 XNUMX
range_request_timeout_in_ms: 200000 XNUMX
write_request_timeout_in_ms: 40000
counter_write_request_timeout_in_ms: 100000 XNUMX
cas_contention_timeout_in_ms: 20000
truncate_request_timeout_in_ms: 60000
request_timeout_in_ms: 200000 XNUMX
slow_query_log_timeout_in_ms: 500
cross_node_timeout: false
endpoint_snitch: GossipingPropertyFileSnitch
dynamic_snitch_update_interval_in_ms: 100
dynamic_snitch_reset_interval_in_ms: 600000 XNUMX
dynamic_snitch_badness_threshold: 0.1
request_scheduler: org.apache.cassandra.scheduler.NoScheduler
server_encryption_options:
internode_encryption: žádné
client_encryption_options:
povoleno: false
internode_compression: dc
inter_dc_tcp_nodelay: nepravda
tracetype_query_ttl: 86400
tracetype_repair_ttl: 604800
enable_user_defined_functions: false
enable_scripted_user_defined_functions: false
windows_timer_interval: 1
transparent_data_encryption_options:
povoleno: false
tombstone_warn_threshold: 1000
tombstone_failure_threshold: 100000 XNUMX
batch_size_warn_threshold_in_kb: 200
batch_size_fail_threshold_in_kb: 250
unlogged_batch_across_partitions_warn_threshold: 10
compaction_large_partition_warning_threshold_mb: 100
gc_warn_threshold_in_ms: 1000
back_pressure_enabled: false
enable_materialized_views: true
enable_sasi_indexes: true

Nastavení GC:

### Nastavení CMS-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:+CMSParallelRemarkEnabled
-XX:Poměr přežití=8
-XX:MaxTenuringThreshold=1
-XX:CMSIitiatingOccupancyFraction=75
-XX:+UseCMSIitiatingOccupancyOnly
-XX:CMSWaitDuration=10000
-XX:+CMSParallelInitialMarkEnabled
-XX:+CMSEdenChunksRecordAlways
-XX:+CMSCclassUnloadingEnabled

Bylo přiděleno 16 Gb paměti jvm.options (zkusili jsme i 32 Gb, žádný rozdíl nebyl zaznamenán).

Tabulky byly vytvořeny příkazem:

CREATE TABLE ks.t1 (id bigint PRIMARY KEY, title text) WITH compression = {'sstable_compression': 'LZ4Compressor', 'chunk_length_kb': 64};

Verze HB: 1.2.0-cdh5.14.2 (ve třídě org.apache.hadoop.hbase.regionserver.HRegion jsme vyloučili MetricsRegion, což vedlo ke GC, když byl počet regionů na RegionServeru více než 1000)

Jiné než výchozí parametry HBasečasový limit.relace.zookeeper: 120000 XNUMX
hbase.rpc.timeout: 2 minuty
hbase.client.scanner.timeout.period: 2 minuty
hbase.master.handler.count: 10
hbase.regionserver.lease.period, hbase.client.scanner.timeout.period: 2 minuty
hbase.regionserver.handler.count: 160
hbase.regionserver.metahandler.count: 30
hbase.regionserver.logroll.period: 4 hodiny
hbase.regionserver.maxlogs: 200
hbase.hregion.memstore.flush.size: 1 GiB
hbase.hregion.memstore.block.multiplier: 6
hbase.hstore.compactionThreshold: 5
hbase.hstore.blockingStoreFiles: 200
hbase.hregion.majorkompakce: 1 dní
HBase Service Advanced Configuration Snippet (Safety Valve) pro hbase-site.xml:
hbase.regionserver.wal.codecorg.apache.hadoop.hbase.regionserver.wal.IndexedWALEditCodec
hbase.master.namespace.init.timeout3600000
hbase.regionserver.optionalcacheflushinterval18000000
hbase.regionserver.thread.compaction.large12
hbase.regionserver.wal.enablecompressiontrue
hbase.hstore.compaction.max.size1073741824
hbase.server.compactchecker.interval.multiplier200
Možnosti konfigurace Java pro HBase RegionServer:
-XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSIitiatingOccupancyFraction=70 -XX:+CMSParallelRemarkEnabled -XX:ReservedCodeCacheSize=256m
hbase.snapshot.master.timeoutMillis: 2 minuty
hbase.snapshot.region.timeout: 2 minuty
hbase.snapshot.master.timeout.millis: 2 minuty
Maximální velikost protokolu HBase REST Server: 100 MiB
Maximální zálohy souborů protokolu HBase REST Server: 5
Maximální velikost protokolu HBase Thrift Server: 100 MiB
Maximální zálohy souborů protokolu HBase Thrift Server: 5
Maximální velikost protokolu Master: 100 MiB
Hlavní maximální zálohy souborů protokolu: 5
Maximální velikost protokolu RegionServer: 100 MiB
RegionServer Maximální zálohy souborů protokolu: 5
Okno aktivní hlavní detekce HBase: 4 minuty
dfs.client.hedged.read.threadpool.size: 40
dfs.client.hedged.read.threshold.millis: 10 milisekund
hbase.rest.threads.min: 8
hbase.rest.threads.max: 150
Maximální počet deskriptorů procesního souboru: 180000 XNUMX
hbase.thrift.minWorkerThreads: 200
hbase.master.executor.openregion.threads: 30
hbase.master.executor.closeregion.threads: 30
hbase.master.executor.serverops.threads: 60
hbase.regionserver.thread.compaction.small: 6
hbase.ipc.server.read.threadpool.size: 20
Vlákna pro přesun regionu: 6
Velikost klientské Java haldy v bajtech: 1 GiB
Výchozí skupina serveru HBase REST: 3 GiB
Výchozí skupina serveru HBase Thrift Server: 3 GiB
Velikost Java haldy HBase Master v bajtech: 16 GiB
Velikost Java haldy HBase RegionServer v bajtech: 32 GiB

+ZooKeeper
maxClientCnxns: 601
maxSessionTimeout: 120000 XNUMX
Vytváření tabulek:
hbase org.apache.hadoop.hbase.util.RegionSplitter ns:t1 UniformSplit -c 64 -f cf
alter 'ns:t1', {NAME => 'cf', DATA_BLOCK_ENCODING => 'FAST_DIFF', COMPRESSION => 'GZ'}

Je zde jeden důležitý bod - popis DataStax neříká, kolik regionů bylo použito k vytvoření HB tabulek, ačkoli to je kritické pro velké objemy. Proto bylo pro testy zvoleno množství = 64, které umožňuje uložit až 640 GB, tzn. středně velký stůl.

V době testu měl HBase 22 tisíc tabulek a 67 tisíc regionů (to by bylo pro verzi 1.2.0 smrtelné, nebýt výše zmíněného patche).

Nyní ke kódu. Protože nebylo jasné, které konfigurace jsou pro konkrétní databázi výhodnější, byly testy prováděny v různých kombinacích. Tito. v některých testech byly načteny 4 tabulky současně (pro připojení byly použity všechny 4 uzly). V dalších testech jsme pracovali s 8 různými tabulkami. V některých případech byla velikost dávky 100, v jiných 200 (parametr dávky - viz kód níže). Velikost dat pro hodnotu je 10 bajtů nebo 100 bajtů (dataSize). Celkem bylo pokaždé zapsáno a načteno 5 milionů záznamů do každé tabulky. Současně bylo do každé tabulky zapsáno/čteno 5 vláken (číslo vlákna - thNum), z nichž každé používalo svůj vlastní rozsah klíčů (počet = 1 milion):

if (opType.equals("insert")) {
    for (Long key = count * thNum; key < count * (thNum + 1); key += 0) {
        StringBuilder sb = new StringBuilder("BEGIN BATCH ");
        for (int i = 0; i < batch; i++) {
            String value = RandomStringUtils.random(dataSize, true, true);
            sb.append("INSERT INTO ")
                    .append(tableName)
                    .append("(id, title) ")
                    .append("VALUES (")
                    .append(key)
                    .append(", '")
                    .append(value)
                    .append("');");
            key++;
        }
        sb.append("APPLY BATCH;");
        final String query = sb.toString();
        session.execute(query);
    }
} else {
    for (Long key = count * thNum; key < count * (thNum + 1); key += 0) {
        StringBuilder sb = new StringBuilder("SELECT * FROM ").append(tableName).append(" WHERE id IN (");
        for (int i = 0; i < batch; i++) {
            sb = sb.append(key);
            if (i+1 < batch)
                sb.append(",");
            key++;
        }
        sb = sb.append(");");
        final String query = sb.toString();
        ResultSet rs = session.execute(query);
    }
}

V souladu s tím byla podobná funkce poskytnuta pro HB:

Configuration conf = getConf();
HTable table = new HTable(conf, keyspace + ":" + tableName);
table.setAutoFlush(false, false);
List<Get> lGet = new ArrayList<>();
List<Put> lPut = new ArrayList<>();
byte[] cf = Bytes.toBytes("cf");
byte[] qf = Bytes.toBytes("value");
if (opType.equals("insert")) {
    for (Long key = count * thNum; key < count * (thNum + 1); key += 0) {
        lPut.clear();
        for (int i = 0; i < batch; i++) {
            Put p = new Put(makeHbaseRowKey(key));
            String value = RandomStringUtils.random(dataSize, true, true);
            p.addColumn(cf, qf, value.getBytes());
            lPut.add(p);
            key++;
        }
        table.put(lPut);
        table.flushCommits();
    }
} else {
    for (Long key = count * thNum; key < count * (thNum + 1); key += 0) {
        lGet.clear();
        for (int i = 0; i < batch; i++) {
            Get g = new Get(makeHbaseRowKey(key));
            lGet.add(g);
            key++;
        }
        Result[] rs = table.get(lGet);
    }
}

Protože se v HB musí klient postarat o rovnoměrnou distribuci dat, funkce solení kláves vypadala takto:

public static byte[] makeHbaseRowKey(long key) {
    byte[] nonSaltedRowKey = Bytes.toBytes(key);
    CRC32 crc32 = new CRC32();
    crc32.update(nonSaltedRowKey);
    long crc32Value = crc32.getValue();
    byte[] salt = Arrays.copyOfRange(Bytes.toBytes(crc32Value), 5, 7);
    return ArrayUtils.addAll(salt, nonSaltedRowKey);
}

Nyní to nejzajímavější - výsledky:

Bitva dvou yakozuna nebo Cassandra vs HBase. Zkušenosti týmu Sberbank

Totéž ve formě grafu:

Bitva dvou yakozuna nebo Cassandra vs HBase. Zkušenosti týmu Sberbank

Výhoda HB je tak překvapivá, že existuje podezření, že je v nastavení CS nějaké úzké hrdlo. Nicméně googlování a hledání nejviditelnějších parametrů (jako concurrent_writes nebo memtable_heap_space_in_mb) věci neurychlilo. Zároveň jsou polena čistá a na nic nenadávají.

Data byla distribuována rovnoměrně mezi uzly, statistiky ze všech uzlů byly přibližně stejné.

Takto vypadá statistika tabulky z jednoho z uzlůKeyspace: ks
Počet přečtení: 9383707
Latence čtení: 0.04287025042448576 ms
Zápis: 15462012
Latence zápisu: 0.1350068438699957 ms
Čekající spláchnutí: 0
Tabulka: t1
Počet SSTable: 16
Využitý prostor (živě): 148.59 MiB
Využitý prostor (celkem): 148.59 MiB
Prostor využitý snímky (celkem): 0 bajtů
Využitá paměť mimo haldu (celkem): 5.17 MiB
SSTable kompresní poměr: 0.5720989576459437
Počet oddílů (odhad): 3970323
Počet zapamatovatelných buněk: 0
Velikost památných dat: 0 bajtů
Použitá paměť mimo haldu paměti: 0 bajtů
Počet paměťových spínačů: 5
Místní počet čtení: 2346045
Místní latence čtení: NaN ms
Místní počet zápisů: 3865503
Místní latence zápisu: NaN ms
Čekající spláchnutí: 0
Procento opravených: 0.0
Falešně pozitivní filtr Bloom: 25
Falešný poměr Bloomova filtru: 0.00000
Použitý prostor Bloomova filtru: 4.57 MiB
Použitá paměť haldy Bloom filter off: 4.57 MiB
Souhrn indexu mimo použitou paměť haldy: 590.02 KiB
Použitá kompresní metadata z paměti haldy: 19.45 kB
Minimální počet bajtů zkomprimovaného oddílu: 36
Maximální počet bajtů zkomprimovaného oddílu: 42
Střední počet bajtů zkomprimovaného oddílu: 42
Průměr živých buněk na řez (posledních pět minut): NaN
Maximální počet živých buněk na řez (posledních pět minut): 0
Průměrný počet náhrobků na plátek (posledních pět minut): NaN
Maximální počet náhrobků na plátek (posledních pět minut): 0
Zahozené mutace: 0 bajtů

Pokus o zmenšení velikosti dávky (i zaslání jednotlivě) neměl žádný efekt, pouze se to zhoršilo. Je možné, že ve skutečnosti jde skutečně o maximální výkon pro CS, protože výsledky získané pro CS jsou podobné těm, které byly získány pro DataStax – asi stovky tisíc operací za sekundu. Pokud se navíc podíváme na využití zdrojů, uvidíme, že CS využívá mnohem více CPU a disků:

Bitva dvou yakozuna nebo Cassandra vs HBase. Zkušenosti týmu Sberbank
Obrázek ukazuje vytížení při běhu všech testů za sebou pro obě databáze.

Ohledně silné čtenářské výhody HB. Zde vidíte, že u obou databází je vytížení disku při čtení extrémně nízké (testy čtení jsou závěrečnou částí testovacího cyklu pro každou databázi, například u CS je to od 15:20 do 15:40). V případě HB je důvod jasný – většina dat visí v paměti, v memstore a část se ukládá do mezipaměti v blockcache. Pokud jde o CS, není příliš jasné, jak to funguje, ale recyklace disku také není vidět, ale pro každý případ byl učiněn pokus povolit mezipaměť row_cache_size_in_mb = 2048 a nastavit mezipaměť = {'keys': 'ALL', 'rows_per_partition': ' 2000000'}, ale tím se to ještě trochu zhoršilo.

Za zmínku také ještě jednou stojí důležitý bod o počtu krajů v HB. V našem případě byla hodnota zadána jako 64. Pokud ji snížíme a rovnáme např. 4, tak při čtení rychlost klesne 2x. Důvodem je, že memstore se bude rychleji zaplňovat a soubory budou častěji splachovány a při čtení bude potřeba zpracovat více souborů, což je pro HB poměrně komplikovaná operace. V reálných podmínkách to lze ošetřit tak, že si promyslíme strategii předběžného dělení a zhutnění; konkrétně používáme vlastnoručně napsanou utilitu, která sbírá odpadky a neustále na pozadí komprimuje HFiles. Je docela možné, že pro testy DataStax alokovali pouze 1 region na tabulku (což není správné) a to by poněkud objasnilo, proč byl HB ve svých testech čtení tak podřadný.

Z toho jsou vyvozeny následující předběžné závěry. Za předpokladu, že během testování nedošlo k žádným zásadním chybám, pak Cassandra vypadá jako kolos s hliněnými nohami. Přesněji, zatímco balancuje na jedné noze, jako na obrázku na začátku článku, předvádí poměrně dobré výsledky, ale v boji za stejných podmínek prohrává na plné čáře. Zároveň jsme se s ohledem na nízké využití CPU na našem hardwaru naučili osadit dva RegionServer HB na hostitele a tím zdvojnásobili výkon. Tito. S ohledem na využití zdrojů je situace pro CS ještě tristnější.

Tyto testy jsou samozřejmě značně syntetické a množství dat, které zde bylo použito, je poměrně skromné. Je možné, že pokud bychom přešli na terabajty, byla by situace jiná, ale zatímco u HB můžeme terabajty načíst, u CS se to ukázalo jako problematické. Často to házelo OperationTimedOutException i u těchto svazků, ačkoli parametry pro čekání na odpověď byly již několikrát navýšeny oproti výchozím.

Doufám, že společným úsilím najdeme úzká hrdla CS a pokud to dokážeme urychlit, tak na konci příspěvku určitě přidám informaci o konečných výsledcích.

UPD: Díky radám soudruhů se mi podařilo urychlit čtení. bylo:
159 644 ops (4 tabulky, 5 streamů, dávka 100).
Přidáno:
.withLoadBalancingPolicy(new TokenAwarePolicy(DCAwareRoundRobinPolicy.builder().build()))
A hrál jsem si s počtem vláken. Výsledek je následující:
4 tabulky, 100 vláken, dávka = 1 (kus po kusu): 301 969 ops
4 tabulky, 100 vláken, dávka = 10: 447 608 operací
4 tabulky, 100 vláken, dávka = 100: 625 655 operací

Později použiji další tipy pro ladění, spustím celý testovací cyklus a výsledky přidám na konec příspěvku.

Zdroj: www.habr.com

Přidat komentář