高性能Cassandra多语言客户端

本文涉及的产品
云原生多模数据库 Lindorm,多引擎 多规格 0-4节点
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
云数据库 MongoDB,通用型 2核4GB
简介: 作为全球范围内最流行的宽表数据库,Apache Cassandra具备诸多优点:海量数据存储;简洁易上手的类SQL语法;总是在线;扩容灵活等。除了服务端的各种优点之外,Cassandra对各种语言客户端(driver)的高性能支持也是其实现易用性和良好性能的重要环节。

本文根据10月24日在群里直播的内容整理,方便习惯看文字的同学。如果更习惯看视频,可以点击观看直播回放

为何选择Cassandra

作为全球范围内最流行的宽表数据库,Apache Cassandra具备诸多优点:海量数据存储;简洁易上手的类SQL语法;总是在线;扩容灵活等。除了服务端的各种优点之外,Cassandra对各种语言客户端(driver)的高性能支持也是其实现易用性和良好性能的重要环节。Cassandra支持几乎所有流行语言的原生客户端:Java/Python/C++/C#/NodeJS/PHP/Ruby/Go/Perl/Scala...(详细列表参见Cassandra文档) 。下图列出了Cassandra支持的开发语言:
image
同时,Cassandra客户端良好的架构设计保证了在易于使用的同时能够从服务端获取最大的性能。下图是Cassandra和一些其他引擎的性能对比(出自https://www.datastax.com/nosql-databases/benchmarks-cassandra-vs-mongodb-vs-hbase
image
接下来的内容中,我们会从分布式数据库客户端要解决的一般问题出发,分析和比较几种常见的客户端实现方案,理解Cassandra高性能客户端背后的实现原理和架构,并以java driver为例,介绍Cassandra高性能客户端的异步接口、连接池、负载均衡、重试策略等重要特性的原理和配置参数。

分布式数据库客户端常见方案

要为分布式数据库设计一个客户端,可以简化为如下模型:
image
数据分布在不同的服务器节点上面,要如何设计客户端和服务器的交互方式?
一般来说,这包含了如下问题:

  • 客户端如何知道数据在哪个节点?这涉及meta数据如何存储、交互的问题。
  • 如何减少不必要的网络通信? 一般要用到一些数据本地化的设计。
  • 负载均衡:包括proxy之间、多个副本之间的均衡。
  • 重客户端 vs 轻客户端,需要做一些取舍。
  • 如何实现多语言的支持?有些引擎会为每种语言实现一个Native的客户端,也有一些引擎(例如HBase)会使用IDL的方式来定义跨语言的接口。
  • 其他设计上的考虑,如连接池/线程调度/异步…

Mongodb方案

Mongodb Sharding集群的架构参见下图
image
这是一个比较典型的proxy方式实现分布式数据库客户端的方案。在这种方案里面,meta数据单独存储在config server里面,前端通过一个proxy(mongos)来将客户端的请求路由到实际的数据节点(Shard)。

HBase方案

下图同时画出了HBase通过Java和其他语言访问的架构:
image
HBase的Thrift Server也是类似proxy的工作方式。

Cassandra方案

image
Cassandra与其他引擎不同的地方首先体现在每个服务端节点上都有meta信息,同时客户端也会在启动时拉取meta。这意味着:
(1)Cassandra在把客户端请求路由到数据节点时不需要从远端节点查找meta。
(2)Cassandra客户端的策略可以很灵活,既可以把任意一个节点作为proxy来发送请求,也可以像重客户端一样把请求发到数据所在的节点减少网络转发的请求,如下图:
image

比较

了解了这几种架构之后,我们可以再回到这一小节前面提到的问题,见下表:

比较内容 MongoDB HBase Cassandra
meta存储 中心化 中心化 分布式
数据本地化
proxy负载均衡
副本负载均衡
重客户端? 轻客户端 重客户端 策略灵活,介于二者之间
多语言支持 Native Thrift Native

可见,Cassandra客户端的设计在每个点上几乎都做了最有利于性能的选择。

Cassandra客户端深入

了解了Cassandra客户端和服务端交互的方式之后,我们再来深入看一下Cassandra客户端的内部结构(以DataStax Java Driver为例):
image
关于这个图的进一步描述可以参考https://beyondthelines.net/databases/the-cassandra-java-driver ,中文版本是https://yq.aliyun.com/articles/719645 ,这里只做简单的解读:
(1)应用通过session对象和driver交互,session对象中管理着一系列连接池(pool)。
(2)客户端会针对服务器的每个节点创建一个连接池,每个连接池中有若干到节点的网络连接(Connection)。
(3)请求都是异步发送的,这也意味着每个连接可以并行发送若干请求。
各种对象之间的关系如下图:
image

Quick Start

一个简单的Java客户端示例代码如下:

public class Demo {

  public static void main(String[] args) {
    // 此处填写数据库连接点地址(公网或者内网的),控制台有几个就填几个。
    // 实际上SDK最终只会连上第一个可连接的连接点并建立控制连接,填写多个是为了防止单个节点挂掉导致无法连接数据库。
    // 此处无需关心连接点的顺序,因为SDK内部会先打乱连接点顺序避免不同客户端的控制连接总是连一个点。
    // 千万不要把公网和内网的地址一起填入。
    String[] contactPoints = new String[]{
        "$host1",
        "$host2"
    };
    Cluster cluster = Cluster.builder()
        .addContactPoints(contactPoints)
        // 填写账户名密码(如果忘记可以在 帐号管理 处重置)
        .withAuthProvider(new PlainTextAuthProvider("$username", "$Password"))
        // 如果进行的是公网访问,需要在帐号名后面带上 @public 以切换至完全的公网链路。
        // 否则无法在公网连上所有内部节点,会看到异常或者卡顿,影响本地开发调试。
        // 后续会支持网络链路自动识别(即无需手动添加@public)具体可以关注官网Changelog。
        //.withAuthProvider(new PlainTextAuthProvider("cassandra@public", "123456"))
        .build();
    // 初始化集群,此时会建立控制连接(这步可忽略,建立Session时候会自动调用)
    cluster.init();
    // 连接集群,会对每个Cassandra节点建立长连接池。
    // 所以这个操作非常重,不能每个请求创建一个Session。合理的应该是每个进程预先创建若干个。
    // 通常来说一个够用了,你也可以根据自己业务测试情况适当调整,比如把读写的Session分开管理等。
    Session session = cluster.connect();
    //查询
    ResultSet res = session.execute("SELECT release_version FROM system.local");
    // ResultSet 实现了 Iterable 接口,我们直接将每行信息打印到控制台
    res.forEach(System.out::println);
    // 关闭Session
    session.close();
    // 关闭Cluster
    cluster.close();
  }

}

更多例子可以参加阿里云帮助文档直播演示的Demo及各种语言Driver的文档。

主要特性

Statements

这里主要介绍如下三种statement

SimpleStatement

如下代码创建了一个SimpleStatement并执行:

session.execute("SELECT value FROM application_params WHERE name = 'greeting_message'");

这个语句执行过程中与服务端的交互过程如下图:
image

PreparedStatement

PreparedStatement prepared = session.prepare(
  "insert into product (sku, description) values (?, ?)");

BoundStatement bound = prepared.bind("234827", "Mouse");
session.execute(bound);

session.execute(prepared.bind("987274", "Keyboard"));

PreparedStatement的执行过程分为两个步骤,prepare阶段服务器会解析CQL语句并缓存:
image
之后每次调用execute,服务器并不需要重新解析语句,而是从缓存中取出解析的结果来执行,因此减少了解析的时间:
image

BatchStatement

PreparedStatement preparedInsertExpense =
    session.prepare(
        "INSERT INTO cyclist_expenses (cyclist_name, expense_id, amount, description, paid) "
            + "VALUES (:name, :id, :amount, :description, :paid)");
SimpleStatement simpleInsertBalance =
    new SimpleStatement("INSERT INTO cyclist_expenses (cyclist_name, balance) VALUES (?, 0) IF NOT EXISTS",
        "Vera ADRIAN");

BatchStatement batch = new BatchStatement(BatchStatement.Type.UNLOGGED)
    .add(simpleInsertBalance)
    .add(preparedInsertExpense.bind("Vera ADRIAN", 1, 7.95f, "Breakfast", false));

session.execute(batch);

关于BatchStatement的原理和注意事项参见简析Cassandra的BATCH操作一文。

总结

  • SimpleStatement适用于只执行一次(或几次)的语句
  • PreparedStatement适用于经常执行的语句,可以节省parse的时间
  • BatchStatement适用于有原子性要求的批量语句;或者对同一个partition key的批量操作。

异步API

异步API的示例如下:

import com.google.common.util.concurrent.*;

ListenableFuture<Session> session = cluster.connectAsync();

// Use transform with an AsyncFunction to chain an async operation after another:
ListenableFuture<ResultSet> resultSet = Futures.transform(session,
    new AsyncFunction<Session, ResultSet>() {
        public ListenableFuture<ResultSet> apply(Session session) throws Exception {
            return session.executeAsync("select release_version from system.local");
        }
    });

// Use transform with a simple Function to apply a synchronous computation on the result:
ListenableFuture<String> version = Futures.transform(resultSet,
    new Function<ResultSet, String>() {
        public String apply(ResultSet rs) {
            return rs.one().getString("release_version");
        }
    });
   

// Use a callback to perform an action once the future is complete:
Futures.addCallback(version, new FutureCallback<String>() {
    public void onSuccess(String version) {
        System.out.printf("Cassandra version: %s%n", version);
    }

    public void onFailure(Throwable t) {
        System.out.printf("Failed to retrieve the version: %s%n",
            t.getMessage());
    }
});

由于异步API不会阻塞应用执行,因此可以提高效率。异步API+TokenAwarePolicy配合使用可以实现很好的写入性能。

连接池

如下代码示例了如何配置连接池相关参数:

PoolingOptions poolingOptions = new PoolingOptions();
poolingOptions
    .setConnectionsPerHost(HostDistance.LOCAL,  4, 10)
    .setConnectionsPerHost(HostDistance.REMOTE, 2, 4);
    .setMaxRequestsPerConnection(HostDistance.LOCAL, 32768)
    .setMaxRequestsPerConnection(HostDistance.REMOTE, 2000);

Cluster cluster = Cluster.builder()
    .withContactPoints("127.0.0.1")
    .withPoolingOptions(poolingOptions)
    .build();

和连接池有关的配置内容主要有以下这几个方面:

pool大小

pool大小的配置决定了和服务端每个host建立多少个连接。默认值为LOCAL hosts: core = max = 1,REMOTE hosts: core = max = 1。如果core != max,cassandra会根据负载情况动态调整连接数,具体的策略如下:

  • 若连接数n < max 且 并发请求数 > (n - 1) * maxRequestsPerConnection + NewConnectionThreshold,则新建连接
  • 每10s执行一次清理。若n > core 且 需要的连接数 < n,则清理连接。需要的连接数由 实际请求数,maxRequestsPerConnection,NewConnectionThreshold三者共同决定。
    可根据应用的实际负载情况适当调大CoreConnections/MaxConnections的值。NewConnectionThreshold一般情况下不需要配置。

maxRequestsPerConnection

这个配置项的含义是in flight(已发到服务器但还没收到响应)的最大请求数。超出这个配置的请求会排队或报错(取决于排队配置)。默认值为LOCAL hosts:1024,REMOTE hosts:256,还是比较小的,可以根据应用实际情况适当调大。

排队配置

主要包括MaxQueueSize和PoolTimeoutMillis。如果所有连接都在忙(超过maxRequestsPerConnection个请求),则新的请求会排队一段时间以等待连接可用,队列最大长度为MaxQueueSize,等待的最长时间为PoolTimeoutMillis。

另外,在生产环境上面如果需要监测连接池的使用情况,Driver里面也提供了Session.getState这个方法。

负载均衡策略

如下代码示例了如何配置负载均衡策略:

Cluster cluster = Cluster.builder()
        .addContactPoint("127.0.0.1")
        .withLoadBalancingPolicy(new RoundRobinPolicy())
        .build();

负载均衡策略决定新的请求发往哪个coordinator,以及failover时选择哪个coordinator。默认的策略是new TokenAwarePolicy(DCAwareRoundRobinPolicy.builder().build());,表示会把请求尽量发到LOCAL节点,并切会根据token分布把请求发到数据对应的节点上面去。
除默认策略外,其他策略还包括:

  • RoundRobinPolicy
  • DCAwareRoundRobinPolicy
  • TokenAwarePolicy
  • LatencyAwarePolicy
  • WhiteListPolicy
  • HostFilterPolicy

重试策略

如下代码示例了如何配置重试策略:

Cluster cluster = Cluster.builder()
        .addContactPoint("127.0.0.1")
        .withRetryPolicy(new MyCustomPolicy())
        .build();

如下这些重试行为是hard-code的,应用无法配置(实际上也没有理由要去配置……):

  • 网络实际写入发生前,有任何错误都会在另一个host上重试
  • prepared statement未编译,在同一个host上编译并重试
  • 若节点处于bootstrapping状态,在另一个节点重试请求
    默认策略:
  • OnReadTimeout:如果收到足够多副本数的响应但没有读到数据,则在同一个host重试一次;其他情况直接抛异常。
  • onWriteTimeout:只在写batchlog失败时重试一次;其他情况抛异常。
  • onUnavailable:在其他host重试一次。
  • onRequestError:WriteFailure/ReadFailure不重试,客户端超时等请求错误会在其他host重试一次。
    如果默认策略不满足需求,也可以通过实现RetryPolicy接口来自定义重试策略。

总结

可以看到,Cassandra不仅支持多种开发语言访问,而且客户端的设计兼顾了易用性和高性能,灵活而且高效,还提供了很多有用的特性可以根据应用的实际场景从服务端获取最大的性能。

入群邀约

为了营造一个开放的 Cassandra 技术交流,我们建立了微信群公众号和钉钉群,为广大用户提供专业的技术分享及问答,定期开展专家技术直播,欢迎大家加入。另外阿里云提供免费Cassandra试用:https://www.aliyun.com/product/cds

8c677711345d2450b80800d50aab3fa0b5a95ded

钉钉群入群链接:https://c.tb.cn/F3.ZRTY0o

微信群公众号:
2a9d5b6c3b9030d06ddb59131aab7287945ead53

目录
相关文章
|
SQL NoSQL 网络协议
高性能分布式No SQL数据库Aerospike(二)——安装部署
高性能分布式No SQL数据库Aerospike(二)——安装部署
676 0
高性能分布式No SQL数据库Aerospike(二)——安装部署
|
5天前
|
存储 负载均衡 Cloud Native
【专栏】Minio是一款高性能分布式对象存储服务,以其易用性和可扩展性著称
【4月更文挑战第28天】Minio是一款高性能分布式对象存储服务,以其易用性和可扩展性著称,适用于存储多媒体内容。通过组建Minio集群,可实现高可用性、高性能、可扩展性和数据保护。搭建集群包括安装Minio、配置集群参数、启动节点、验证集群状态、设置访问权限及可选的数据迁移步骤。Minio集群是实现可靠且高性能存储解决方案的理想选择,适合各种应用场景。
|
5天前
|
消息中间件 监控 负载均衡
Kafka 最佳实践:构建可靠、高性能的分布式消息系统
Apache Kafka 是一个强大的分布式消息系统,被广泛应用于实时数据流处理和事件驱动架构。为了充分发挥 Kafka 的优势,需要遵循一些最佳实践,确保系统在高负载下稳定运行,数据可靠传递。本文将深入探讨 Kafka 的一些最佳实践,并提供丰富的示例代码,帮助大家更好地应用这一强大的消息系统。
|
存储 固态存储 算法
高性能分布式No SQL数据库Aerospike(一)——介绍和架构
高性能分布式No SQL数据库Aerospike(一)——介绍和架构
1082 1
高性能分布式No SQL数据库Aerospike(一)——介绍和架构
|
存储 SQL 缓存
高性能分布式No SQL数据库Aerospike(三)——常用工具使用
高性能分布式No SQL数据库Aerospike(三)——常用工具使用
339 0
高性能分布式No SQL数据库Aerospike(三)——常用工具使用
|
存储 SQL NoSQL
高性能分布式No SQL数据库Aerospike(四)——经验总结和最佳实践
高性能分布式No SQL数据库Aerospike(四)——经验总结和最佳实践
310 0
|
分布式计算 NoSQL MongoDB
MongoDB分布式计算的优点
分布式计算的优点
129 0
|
存储 JSON 缓存
MongDB是一个高性能,开源,无模式的文档型NosQL数据库。
MongDB是一个高性能,开源,无模式的文档型NosQL数据库。
225 0
|
存储 运维 NoSQL
开源OpenIM:高性能、可伸缩、易扩展的即时通讯架构
网上有很多关于IM的教程和技术博文,有亿级用户的IM架构,有各种浅谈原创自研IM架构,也有微信技术团队分享的技术文章,有些开发者想根据这些资料自研IM。理想很丰满,现实很骨感,最后做出来的产品很难达到商用标准。事实上,很多架构没有经过海量用户的考验,当然我们也不会评判某种架构的好坏,如果开发者企图根据网上教程做出一个商用的IM,可能有点过于乐观了。本文主要从我个人角度深度剖析100%开源的OpenIM架构。当然,世界上没有最完美的架构,只有最合适的架构,也没有所谓的通用方案,不同的解决方案都有其优缺点,只有最满足业务的系统才是一个好的系统。而且,在有限的人力、物力,综合考虑时间成本,通常需要做
1060 0
开源OpenIM:高性能、可伸缩、易扩展的即时通讯架构
|
存储 NoSQL Linux
Cassandra技术介绍之配置
cassandra之基础配置
3309 0