共计 3918 个字符,预计需要花费 10 分钟才能阅读完成。
Cassandra Java 驱动程序
本文翻译至:https://beyondthelines.net/databases/the-cassandra-java-driver/
同时也加上了作者阅读源码后的观后感,丰富了很多细节。
Cassandra 驱动程序不是将 CQL 字符串发送到 Cassandra 节点并等待响应的傻瓜程序
它们实际上很聪明,并且以某种方式组织的,使您易于使用,工作更开心,同时仍然尝试从 Cassandra 中获得最大的性能。
在本文中,我将重点介绍 Java 驱动程序,快速了解其体系结构及其提供的某些功能。
快速使用
3.x 版本
Cluster cluster = Cluster.builder().addContactPoints(contactPoints).withPort(port).build();
session = cluster.connect();
ResultSet results = session.execute(query);
for (Row row : results) {//TODO: access row;}
4.x 版本
session = CqlSession.builder().build();
ResultSet results = session.execute(query);
for (Row row : results) {//TODO: access row;}
配置 application.conf, 放在 java 进程的 classpath 下
datastax-java-driver {basic.contact-points = ["127.0.0.1:9042"]
basic {
load-balancing-policy {local-datacenter = datacenter1}
}
可以看到 4.x 完全移除了 Cluster 这个类,一个会话会创建 n 个 pool(n=node 个数),一个 pool 就是一个连接池,拥有若干个连接,请求都是异步的,所以一个连接也是可以同时发送多个 request,这种我们称之为 inFlight
因为目前主流的客户端还是 3.x,下面我们重点介绍 3.x 版本
架构
Cassandra Java 驱动程序提供了一个异步 API。请注意,它还提供了一个同步 API,但由于它是基于异步 API 的,并且我不想在我的应用程序线程与 Cassandra 交互时夯住,因此我不准备介绍它。
让我们自底向上研究一下驱动程序各个组件
连接
最底部是与 Cassandra 节点的连接。Cassandra 协议是完全异步的。这意味着我们可以通过同一连接发送多个请求。在发送下一个请求之前,我们不必等待单个请求完成。每个请求都由流 ID 标识,并且在响应中也设置了该 ID,以便 driver 可以将响应与相应的请求进行关联。
该驱动程序依靠 Netty 执行异步 IO 操作。
一旦将请求发送到连接会话,executeAsync 将返回 Future,然后在接收到相应的响应(或发生超时异常)时使用 Promise 完成。
正在进行的请求(也称为“in-flight”请求)存储在队列中。队列已满时,您将无法再将查询发送到 Cassandra。executeAsync 将返回失败的 future(jdk8 异步作业句柄)。在版本 3.1 之前,调用线程处于阻塞状态,等待有可用的连接。当然,队列大小可以在 poolingOptions 中配置。
val poolingOptions = new PoolingOptions()
poolingOptions
.setMaxRequestsPerConnection(HostDistance.LOCAL, 32768)
.setMaxRequestsPerConnection(HostDistance.REMOTE, 2000)
val cluster = Cluster.builder()
.withContactPoints("127.0.0.1")
.withPoolingOptions(poolingOptions)
.build()
默认值非常低(本地连接为 1024,远程连接为 256)。256 在生产环境很容易就用满,因此,我建议您根据需要调整这些值。
使用 TCP 保持活动状态或发送应用程序心跳以保持连接打开,以保持连接打开。
连接池
连接属于连接池。驱动程序为每个 Cassandra 节点维护一个连接池。连接池也可以通过 poolingOptions 进行配置。
poolingOptions
.setConnectionsPerHost(HostDistance.LOCAL, 4, 10)
.setConnectionsPerHost(HostDistance.REMOTE, 2, 4)
主要配置是池大小。可用连接的数量可以根据负载在核心和最大数量之间变化。我们还可以为本地或远程数据中心设置不同的设置。当连接闲置时间过长时,连接将关闭,直到池大小达到其核心大小为止。
会话
连接池属于会话。
会话也是应用程序用于与 Cassandra 通信的对象。
该层为应用程序抽象所有连接管理。
val session = cluster.connect()
Session 提供了所有与 Cassandra 通信的 API,例如 session.executeAsync,它允许应用程序向 Cassandra 发送请求,或者 session.getState 允许我们监控后端主机和进行中的查询。
cluster
群集是顶层抽象。在这里,我们可以配置所有内容,例如指定池选项,负载平衡策略,重试策略或默认一致性级别。
val cluster = Cluster.builder()
.withContactPoints("127.0.0.1")
.withPoolingOptions(poolingOptions)
.withLoadBalancingPolicy(new RoundRobinPolicy())
.withQueryOptions(new QueryOptions().setConsistencyLevel(ConsistencyLevel.LOCAL_QUORUM)
)
.build()
Bootstrapping
当驱动程序首次连接到种子节点之一时,它会建立一个控制连接,用于发现群集拓扑。基本上,它查询 Cassandra 中的系统表。
引导启动时,从种子节点列表中随机选择种子节点,以避免在初始群集拓扑中始终使用相同的节点。
负载均衡
负载平衡负责建立与整个 Cassandra 集群(不仅在一个节点上)的连接,并维护与集群中每个主机的连接池。
它具有将某些请求发送到某些节点的逻辑。与哪些主机建立连接以及向哪些主机发送请求由负载平衡策略确定。
实际上,对每个请求都会算出一个查询计划。查询计划确定向哪个主机发送请求以及以哪个顺序发送(取决于推测执行策略和重试策略)。
负载平衡还确定主机是本地主机还是远程主机(跟客户端 DCAware 配置有关)。
如果默认策略不够用,可以编写自定义负载平衡策略。
驱动程序从请求中提取 partitionKey, 并使用正确的哈希算法路由到持有该分区的 Cassandra 节点。
默认策略是 DatacenterAwareLoadBalancingPolicy。拥有如下两特性
- 数据中心感知:确定哪些节点属于本地数据中心,哪些节点属于远程数据中心。然后,驱动程序仅将请求发送到本地数据中心,并将远程数据中心用作备用。
- 令牌感知:查找请求的分区键,并使用与群集相同的算法对其进行哈希处理。然后,它将请求发送到负责令牌的节点(在该分区的副本中随机选择)。
使用 DDCAwareRoundRobinPolicy 时可以指定本地数据中心:
Cluster cluster = Cluster.builder()
.addContactPoint("127.0.0.1")
.withLoadBalancingPolicy(DCAwareRoundRobinPolicy.builder()
.withLocalDc("myLocalDC")
.withUsedHostsPerRemoteDc(2)
.allowRemoteDCsForLocalConsistencyLevel()
.build())
)
.build()
容错能力
错误主要有 3 种:
- 无效的请求:错误直接返回应用上层,因为驱动程序无法知道如何处理此类请求
- 服务器错误:驱动程序可以根据负载平衡策略尝试下一个节点
- 网络超时:如果请求被标记为幂等,则驱动程序可以重试该请求。默认情况下,请求不被认为是幂等的,因此在可能的情况下将请求尽量标记是一个好习惯。
对于幂等请求,如果在一定的延迟内没有来自第一节点的响应,则驱动程序可以将请求发送到第二节点。这称为“推测重试”,用 SpeculativeExecutionPolicy 进行配置。
val cluster = Cluster.builder()
.addContactPoint("127.0.0.1")
.withSpeculativeExecutionPolicy(
new ConstantSpeculativeExecutionPolicy(
500, // delay before a new execution is launched
2 // maximum number of executions
)
)
.build()
结论
感谢 datastax 为我们提供了这么强大的客户端,Java 驱动程序值得花一些时间来了解其体系结构以及如何正确配置它(每个连接的最大请求尤为重要,因为我发现默认值不是很合适–配置本地数据中心也很重要,否则驱动程序可能会连接到远程数据中心)。
本文作者:陈江 @阿里
阅读原文
本文为云栖社区原创内容,未经允许不得转载。