两个矢古纲之战,或者 Cassandra 与 HBase 之战。 俄罗斯联邦储蓄银行团队经验

这甚至不是一个玩笑,似乎这张特定的图片最准确地反映了这些数据库的本质,到最后就会清楚为什么:

两个矢古纲之战,或者 Cassandra 与 HBase 之战。 俄罗斯联邦储蓄银行团队经验

根据 DB-Engines 排名,最受欢迎的两种 NoSQL 列式数据库是 Cassandra(以下简称 CS)和 HBase(HB)。

两个矢古纲之战,或者 Cassandra 与 HBase 之战。 俄罗斯联邦储蓄银行团队经验

命运的安排,我们Sberbank的数据加载管理团队已经 早已 并与 HB 密切合作。 在这段时间里,我们很好地研究了它的优点和缺点,并学会了如何烹饪它。 然而,CS形式的替代方案的出现总是迫使我们用怀疑来折磨自己:我们做出了正确的选择吗? 而且,结果 对照由 DataStax 进行的测试显示,CS 以几乎压倒性的分数轻松击败 HB。 另一方面,DataStax 是一个利益相关方,您不应该相信他们的话。 我们也对测试条件的信息量太少感到困惑,所以我们决定自己找出谁是BigData NoSql的王者,得到的结果非常有趣。

然而,在继续讨论所执行的测试结果之前,有必要描述环境配置的重要方面。 事实上,CS 可以在允许数据丢失的模式下使用。 那些。 这是当只有一台服务器(节点)负责某个key的数据时,如果由于某种原因发生故障,那么这个key的值就会丢失。 对于许多任务来说,这并不重要,但对于银行业来说,这是例外而不是规则。 在我们的例子中,拥有多个数据副本以进行可靠存储非常重要。

因此,只考虑三重复制模式下的CS工作模式,即案例空间的创建是使用以下参数进行的:

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

接下来,有两种方法可以确保所需的一致性级别。 一般规则:
西北 + NR > 射频

这意味着写入时节点的确认数(NW)加上读取时节点的确认数(NR)必须大于复制因子。 在我们的例子中,RF = 3,这意味着以下选项是合适的:
2 + 2> 3
3 + 1> 3

由于尽可能可靠地存储数据对我们来说至关重要,因此选择了 3+1 方案。 此外,HB的工作原理类似,即这样的比较会更加公平。

应该指出的是,DataStax 在他们的研究中做了相反的事情,他们为 CS 和 HB 设置 RF = 1(对于后者,通过更改 HDFS 设置)。 这是一个非常重要的方面,因为在这种情况下对 CS 性能的影响是巨大的。 例如,下图显示了将数据加载到CS中所需时间的增加:

两个矢古纲之战,或者 Cassandra 与 HBase 之战。 俄罗斯联邦储蓄银行团队经验

在这里我们看到以下情况:竞争线程写入数据的次数越多,花费的时间就越长。 这是很自然的,但重要的是 RF=3 的性能下降明显更高。 换句话说,如果我们将 4 个线程分别写入 5 个表(总共 20 个),则 RF=3 会损失约 2 倍(RF=150 为 3 秒,RF=75 为 1 秒)。 但如果我们通过将数据加载到 8 个表中,每个表有 5 个线程(总共 40 个)来增加负载,那么 RF=3 的损失已经是 2,7 倍(375 秒 vs 138 秒)。

也许这就是 DataStax 为 CS 成功执行负载测试的秘密,因为对于我们展位上的 HB,将复制因子从 2 更改为 3 没有任何效果。 那些。 磁盘不是我们配置的 HB 瓶颈。 然而,这里还有很多其他的陷阱,因为应该注意的是,我们的 HB 版本略有修补和调整,环境完全不同等等。 还值得注意的是,也许我只是不知道如何正确准备 CS,并且有一些更有效的方法来使用它,我希望我们能在评论中找到答案。 但首先要说的是。

所有测试均在由 4 台服务器组成的硬件集群上进行,每台服务器的配置如下:

CPU:Xeon E5-2680 v4 @ 2.40GHz 64 线程。
磁盘:12块SATA HDD
java版本:1.8.0_111

CS版本:3.11.5

cassandra.yml 参数代币数量:256
hinted_handoff_enabled:真
hinted_handoff_throttle_in_kb:1024
最大提示交付线程数:2
提示目录:/data10/cassandra/hints
hins_flush_period_in_ms:10000
最大提示文件大小in_mb:128
batchlog_replay_throttle_in_kb:1024
验证器:AllowAllAuthenticator
授权者:AllowAllAuthorizer
role_manager:CassandraRoleManager
角色有效性(毫秒):2000
权限有效性以毫秒为单位:2000
凭证有效性(毫秒):2000
分区器:org.apache.cassandra.dht.Murmur3Partitioner
数据文件目录:
- /data1/cassandra/data # 每个dataN目录都是一个单独的磁盘
- /data2/cassandra/数据
- /data3/cassandra/数据
- /data4/cassandra/数据
- /data5/cassandra/数据
- /data6/cassandra/数据
- /data7/cassandra/数据
- /data8/cassandra/数据
commitlog_目录:/data9/cassandra/commitlog
cdc_启用:假
disk_failure_policy:停止
commit_failure_policy:停止
prepared_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
行缓存保存周期:0
counter_cache_size_in_mb:
counter_cache_save_period: 7200
保存的缓存目录:/data10/cassandra/saved_caches
commitlog_sync:定期
commitlog_sync_period_in_ms:10000
commitlog_segment_size_in_mb:32
种子提供者:
- 类名:org.apache.cassandra.locator.SimpleSeedProvider
参数:
— 种子:“*,*”
并发读取数: 256 # 尝试了 64 - 没有注意到差异
并发写入: 256 # 尝试了 64 - 没有注意到差异
concurrent_counter_writes: 256 # 尝试了 64 - 没有注意到差异
并发_物化_视图_写入:32
memtable_heap_space_in_mb: 2048 # 尝试了 16 GB - 速度较慢
memtable_allocation_type:heap_buffers
index_summary_capacity_in_mb:
index_summary_resize_interval_in_分钟:60
滴流同步:假
trickle_fsync_interval_in_kb:10240
存储端口:7000
ssl_存储_端口:7001
监听地址:*
广播地址:*
监听广播地址:true
internode_authenticator:org.apache.cassandra.auth.AllowAllInternodeAuthenticator
start_native_transport: true
本机传输端口:9042
启动rpc:true
rpc_地址:*
rpc_port:9160
rpc_keepalive: true
rpc_server_type:同步
thrift_framed_transport_size_in_mb:15
增量备份:假
snapshot_before_compaction: false
自动快照:真
列索引大小(in_kb):64
列_索引_缓存_大小_in_kb:2
并发压缩器:4
压缩吞吐量_mb_每秒:1600
sstable_preemptive_open_interval_in_mb:50
读取请求超时时间:100000
range_request_timeout_in_ms:200000
write_request_timeout_in_ms: 40000
counter_write_request_timeout_in_ms:100000
cas_contention_timeout_in_ms:20000
截断请求超时时间:60000
request_timeout_in_ms: 200000
Slow_query_log_timeout_in_ms: 500
cross_node_timeout: false
endpoint_snitch:GossipingPropertyFileSnitch
动态告密更新间隔毫秒:100
动态告密重置间隔毫秒:600000
动态告密坏度阈值:0.1
request_scheduler:org.apache.cassandra.scheduler.NoScheduler
服务器加密选项:
节点间加密:无
客户端加密选项:
启用:假
节点间压缩:dc
inter_dc_tcp_nodelay:假
跟踪类型_查询_ttl:86400
跟踪类型_修复_ttl:604800
启用用户定义函数: false
启用脚本用户定义函数: false
windows_timer_interval: 1
透明数据加密选项:
启用:假
tombstone_warn_threshold: 1000
tombstone_failure_threshold:100000
batch_size_warn_threshold_in_kb:200
批量大小失败阈值in_kb:250
unlogged_batch_across_partitions_warn_threshold:10
Compaction_large_partition_warning_threshold_mb:100
gc_warn_threshold_in_ms:1000
启用背压:假
启用物化视图:true
启用_sasi_indexes:true

气相色谱设置:

### CMS 设置-XX:+UseParNewGC
-XX:+ UseConcMarkSweepGC
-XX:+CMSParallelRemarkEnabled
-XX:幸存者比率=8
-XX:最大TenuringThreshold=1
-XX:CMSInitiatingOccupancyFraction=75
-XX:+使用CMSInitiatingOccupancyOnly
-XX:CMSWaitDuration=10000
-XX:+CMSParallelInitialMarkEnabled
-XX:+CMSEdenChunksRecordAlways
-XX:+CMSClassUnloading启用

jvm.options 内存分配为 16Gb(我们也尝试了 32Gb,没有注意到差异)。

这些表是使用以下命令创建的:

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

HB版本:1.2.0-cdh5.14.2(在org.apache.hadoop.hbase.regionserver.HRegion类中我们排除了MetricsRegion,当RegionServer上的region数量超过1000时会导致GC)

非默认 HBase 参数动物园管理员.会话.超时:120000
hbase.rpc.timeout: 2 分钟
hbase.client.scanner.timeout.period: 2 分钟
hbase.master.handler.count:10
hbase.regionserver.lease.period,hbase.client.scanner.timeout.period:2分钟
hbase.regionserver.handler.count:160
hbase.regionserver.metahandler.count:30
hbase.regionserver.logroll.period: 4 小时
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.majorcompaction:1 天
hbase-site.xml 的 HBase 服务高级配置片段(安全阀):
hbase.regionserver.wal.codecorg.apache.hadoop.hbase.regionserver.wal.IndexedWALEditCodec
hbase.master.namespace.init.timeout3600000
hbase.regionserver.可选cacheflushinterval18000000
hbase.regionserver.thread.compaction.large12
hbase.regionserver.wal.enablecompressiontrue
hbase.hstore.compaction.max.size1073741824
hbase.server.compactchecker.interval.multiplier200
HBase RegionServer 的 Java 配置选项:
-XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=70 -XX:+CMSParallelRemarkEnabled -XX:ReservedCodeCacheSize=256m
hbase.snapshot.master.timeoutMillis:2 分钟
hbase.snapshot.region.timeout:2 分钟
hbase.snapshot.master.timeout.millis:2分钟
HBase REST 服务器最大日志大小:100 MiB
HBase REST 服务器最大日志文件备份:5
HBase Thrift 服务器最大日志大小:100 MiB
HBase Thrift 服务器最大日志文件备份:5
主最大日志大小:100 MiB
主最大日志文件备份:5
RegionServer 最大日志大小:100 MiB
RegionServer 最大日志文件备份:5
HBase Active Master 检测窗口:4 分钟
dfs.client.hedged.read.threadpool.size:40
dfs.client.hedged.read.threshold.millis:10毫秒
hbase.rest.threads.min:8
hbase.rest.threads.max:150
最大进程文件描述符:180000
hbase.thrift.minWorkerThreads:200
hbase.master.executor.openregion.线程:30
hbase.master.executor.closeregion.线程:30
hbase.master.executor.serverops.线程:60
hbase.regionserver.thread.compaction.small:6
hbase.ipc.server.read.threadpool.size: 20
区域移动器线程:6
客户端 Java 堆大小(以字节为单位):1 GiB
HBase REST 服务器默认组:3 GiB
HBase Thrift 服务器默认组:3 GiB
HBase Master 的 Java 堆大小(以字节为单位):16 GiB
HBase RegionServer 的 Java 堆大小(以字节为单位):32 GiB

+动物园管理员
最大客户端Cnxns:601
最大会话超时:120000
创建表:
hbase org.apache.hadoop.hbase.util.RegionSplitter ns:t1 UniformSplit -c 64 -f cf
更改 'ns:t1', {NAME => 'cf', DATA_BLOCK_ENCODING => 'FAST_DIFF', COMPRESSION => 'GZ'}

这里有一个重要的一点 - DataStax 描述没有说明使用了多少个区域来创建 HB 表,尽管这对于大容量至关重要。 因此,对于测试,选择数量 = 64,这允许存储最多 640 GB,即中等尺寸的桌子。

测试时,HBase 有 22 个表和 67 个区域(如果没有上面提到的补丁,这对于 1.2.0 版本来说是致命的)。

现在来看代码。 由于尚不清楚哪种配置对特定数据库更有利,因此以各种组合进行了测试。 那些。 在某些测试中,同时加载4个表(所有4个节点都用于连接)。 在其他测试中,我们使用了 8 个不同的表。 在某些情况下,批量大小为 100,在其他情况下为 200(批量参数 - 请参阅下面的代码)。 value 的数据大小为 10 字节或 100 字节 (dataSize)。 每次向每个表写入和读取总共 5 万条记录。 同时,5 个线程写入/读取每个表(线程号 - thNum),每个线程使用自己的键范围(计数 = 1 万):

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);
    }
}

因此,为 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);
    }
}

由于在 HB 中客户端必须处理数据的均匀分布,因此关键的加盐函数如下所示:

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);
}

现在是最有趣的部分 - 结果:

两个矢古纲之战,或者 Cassandra 与 HBase 之战。 俄罗斯联邦储蓄银行团队经验

同样的事情以图表形式表示:

两个矢古纲之战,或者 Cassandra 与 HBase 之战。 俄罗斯联邦储蓄银行团队经验

HB的优势如此惊人,以至于有人怀疑CS设置中存在某种瓶颈。 然而,谷歌搜索并搜索最明显的参数(如并发写入或memtable_heap_space_in_mb)并没有加快速度。 同时,日志很干净,不会咒骂任何东西。

数据均匀分布在节点上,所有节点的统计数据大致相同。

这是来自其中一个节点的表统计信息的样子键空间:ks
阅读次数:9383707
读取延迟:0.04287025042448576 毫秒
写入次数:15462012
写入延迟:0.1350068438699957 毫秒
待刷新:0
表:t1
SS表数:16
使用空间(实时):148.59 MiB
已用空间(总计):148.59 MiB
快照使用的空间(总计):0 字节
使用的堆外内存(总计):5.17 MiB
SSTable压缩比:0.5720989576459437
分区数量(估计):3970323
内存表单元数:0
Memtable数据大小:0字节
使用的 Memtable 堆外内存:0 字节
内存表开关数量:5
本地读取计数:2346045
本地读取延迟:NaN ms
本地写入次数:3865503
本地写入延迟:NaN ms
待刷新:0
修复百分比:0.0
布隆过滤器误报:25
布隆过滤器错误率:0.00000
使用的布隆过滤器空间:4.57 MiB
使用的布隆过滤器堆外内存:4.57 MiB
使用的堆外内存索引摘要:590.02 KiB
压缩元数据使用的堆外内存:19.45 KiB
压缩分区最小字节:36
压缩分区最大字节数:42
压缩分区平均字节数:42
每片平均活细胞(最后五分钟):NaN
每片最大活细胞数(最后五分钟):0
每片平均墓碑数(最后五分钟):NaN
每片最大墓碑数(最后五分钟):0
丢弃的突变:0 字节

尝试减少批次的大小(甚至单独发送)没有效果,只会变得更糟。 事实上,这可能确实是 CS 的最大性能,因为 CS 获得的结果与 DataStax 获得的结果类似 - 每秒大约数十万次操作。 此外,如果我们查看资源利用率,我们会发现 CS 使用更多的 CPU 和磁盘:

两个矢古纲之战,或者 Cassandra 与 HBase 之战。 俄罗斯联邦储蓄银行团队经验
该图显示了两个数据库连续运行所有测试期间的利用率。

关于HB强大的阅读优势。 在这里您可以看到,对于这两个数据库,读取期间的磁盘利用率都非常低(读取测试是每个数据库测试周期的最后部分,例如对于 CS,这是从 15:20 到 15:40)。 就 HB 而言,原因很明确 - 大部分数据挂在内存中、memstore 中,还有一些缓存在 blockcache 中。 至于CS,不太清楚它是如何工作的,但是磁盘回收也是不可见的,但为了以防万一,尝试启用缓存 row_cache_size_in_mb = 2048 并设置 caching = {'keys': 'ALL', 'rows_per_partition': '2000000'},但这让情况变得更糟。

还值得再次提及的是关于 HB 区域数量的重要一点。 在我们的例子中,该值被指定为64。如果将其减小并使其等于例如4,那么读取时,速度会下降2倍。 原因是memstore会更快填满,文件会更频繁地刷新,并且在读取时需要处理更多文件,这对于HB来说是一个相当复杂的操作。 在实际情况下,可以通过考虑预分割和压缩策略来解决这个问题;特别是,我们使用一个自行编写的实用程序,在后台不断收集垃圾并压缩 HFile。 对于 DataStax 测试,他们很可能只为每个表分配 1 个区域(这是不正确的),这在一定程度上澄清了为什么 HB 在读取测试中如此差劲。

由此得出以下初步结论。 假设在测试过程中没有犯下重大错误,那么 Cassandra 看起来就像是一个泥足巨人。 更准确地说,当她用单腿保持平衡时,如文章开头的图片所示,她表现出了相对较好的成绩,但在相同条件下的战斗中,她彻底失败了。 同时,考虑到我们硬件上的CPU利用率较低,我们学会了在每台主机上放置两个RegionServer HB,从而使性能提高一倍。 那些。 考虑到资源的利用,CS的情况就更加惨淡了。

当然,这些测试是相当综合的,并且这里使用的数据量相对较小。 如果我们切换到 TB,情况可能会有所不同,但是对于 HB,我们可以加载 TB,但对于 CS,这却是有问题的。 即使对于这些卷,它也经常抛出OperationTimedOutException,尽管等待响应的参数与默认参数相比已经增加了几倍。

我希望通过共同的努力,我们能够找到CS的瓶颈,如果我们能够加快速度,那么在文章的最后我一定会补充关于最终结果的信息。

UPD:多亏了同志们的建议,我的阅读速度才得以加快。 曾是:
159 个操作(644 个表、4 个流、批次 5)。
添加者:
.withLoadBalancingPolicy(new TokenAwarePolicy(DCAwareRoundRobinPolicy.builder().build()))
我玩弄了线程的数量。 结果如下:
4 个表,100 个线程,批次 = 1(逐个):301 次操作
4 个表,100 个线程,批次 = 10:447 次操作
4 个表,100 个线程,批次 = 100:625 次操作

稍后我将应用其他调整技巧,运行完整的测试周期并将结果添加到帖子末尾。

来源: habr.com

添加评论