背景

之前使用 cass-operator 在 k8s 上搭建了一个 cassandra 集群用来做存储,随着业务数据的增长,之前申请的 100 G 的 PVC 大小已经是捉襟见肘

急需更换到更大的磁盘空间上

最开始的时候其实有考虑到 pvc 扩容的可能,特地申请了一个允许扩展容量的存储类型(storage class)

但是后来发现,如果直接修改对应 pvc, 整个集群的资源实际上还是由 cass-operator 管理的,这意味着一旦重启、修改 datacenter 配置项,可能会把容量给改回去,有不小的可能性直接翻车。

cass-operator 本身提供添加 node 来横向扩容的特性,我们可以直接添加集群中 cassandra 实例的数量, cass-operator 会帮我们把数据重新平衡到新的节点上,并在数据移动完成后自动执行 nodetool cleanup 任务,从而清理出磁盘空间。

不幸的是,目前 cass-operator 还没有提供一个简单的方式来扩容 pvc

但这个方法只是缓兵之计,因为这会造成 cpu/内存 资源上的浪费,这两个资源并没有到达瓶颈。当我们调整完磁盘的容量后,其实是可以通过这个方式来扩容的。

当然我们也可以通过手动导出 sstable 的方式,在同一拓扑的其他集群上装载数据,但是这个方法有需要先停机维护,可能需要几个小时的时间,会影响生产环境的正常使用

所以,经过广泛的查阅文档 / google / stackoverflow ,对比后发现还是使用迁移数据中心的方式比较轻松

  • 创建新 dc 集群
  • 修改keyspace配置参数, 迁移数据
  • 修改客户端连接参数
  • 验证数据,清理旧集群

开始迁移

首先准备新 dc 文件,和旧 dc 文件基本相同,修改的部分只有 metadata 中的 datacenter 名称,以及 pvc 申请的容量

部分和业务相关的信息用中括号标明,实际使用时请按自己的情况做替换

这里旧集群是 dc1, 新集群是 dc2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
apiVersion: cassandra.datastax.com/v1beta1
kind: CassandraDatacenter
metadata:
name: dc2
namespace: cassandra
spec:
podTemplateSpec:
spec:
containers:
- name: "cassandra"
image: "<private-domain>/k8ssandra/cass-management-api:4.0.1"
terminationMessagePath: "/dev/other-termination-log"
terminationMessagePolicy: "File"
clusterName: <my-cluster-name>
serverType: cassandra
serverVersion: "4.0.1"
managementApiAuth:
insecure: {}
size: 3
resources:
requests:
memory: 24Gi
cpu: 3
limits:
memory: 24Gi
cpu: 3
storageConfig:
cassandraDataVolumeClaimSpec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1T
config:
cassandra-yaml:
authenticator: org.apache.cassandra.auth.PasswordAuthenticator
authorizer: org.apache.cassandra.auth.CassandraAuthorizer
role_manager: org.apache.cassandra.auth.CassandraRoleManager
jvm-server-options:
initial_heap_size: 12G
max_heap_size: 22G

将上述文件 kubectl applly -f 应用到集群中,等待 cassandra 新的实例都启动后,我们就可以尝试连接新 dc 的实例了。

用户名和密码都是使用之前 secret 里面的同一套

直接连接的话,大概率会报错

1
Cannot achieve consistency level LOCAL_ONE info={required_replicas 1 alive_replicas: 0, consistency: QUORUM }

参见对应文档:https://docs.datastax.com/en/security/5.1/security/secSystemKeyspace.html

我们需要修改系统认证用到的 keyspace 的副本分布

1
2
3
4
ALTER KEYSPACE system_auth
WITH REPLICATION= {'class' : 'NetworkTopologyStrategy',
'dc1' : 3,
'dc2' : 3};

修改完成后,需要手动运行 repair 任务

1
nodetool repair --full system_auth

这样我们就能正常登录到新 dc 的实例了。

迁移数据

将自己定义的 keyspace 分配副本的策略进行修改,我是在 akka projection 场景下使用,所以会涉及到这么几个 keyspace 需要修改

原来的分布是在 dc1 集群上存储有3个副本,现在需要在 dc2 上面也存储 3 个副本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ALTER KEYSPACE akka
WITH REPLICATION= {'class' : 'NetworkTopologyStrategy',
'dc1' : 3,
'dc2' : 3};

ALTER KEYSPACE akka_projection
WITH REPLICATION= {'class' : 'NetworkTopologyStrategy',
'dc1' : 3,
'dc2' : 3};

ALTER KEYSPACE akka_snapshot
WITH REPLICATION= {'class' : 'NetworkTopologyStrategy',
'dc1' : 3,
'dc2' : 3};

同样地,我们在修改完 keyspace 之后也需要做修复工作

1
2
3
nodetool repair --full -tr akka
nodetool repair --full -tr akka_projection
nodetool repair --full -tr akka_snapshot

当然也可以直接 nodetool repair --full -tr 修复所有 keyspace, 但是在实际过程中,修复的时候老是有 k8s node 重启,引发修复失败,所以我拆成较小的部分修复。

修复过程中可以通过 nodetool status 观察各个节点加载的数据大小,用来检验数据是否被正常加载到新集群。

数据修复的时间,和数据大小已经网速有关,我这里迁移 240 G 数据大约花费了一个小时

客户端配置修改

通过 java-driver 连接需要修改连接的数据中心

对于 datastax-java-driver 需要修改

datastax-java-driver.basic.load-balancing-policy.local-datacenter

对于 quill 需要修改

session.basic.load-balancing-policy.local-datacenter

这两个配置项需要指定到新的 dc2 ,配置修改后重启 deployment 以生效

重启完成后,保险起见可以再次运行一遍 nodetool repair 以确保数据完整

此时可以观察到 dc1 的网络流量,cpu使用情况应该都比较低了,因为所有的请求都是从 dc2 走

验证和清理

我们首先把修改业务表副本拓扑分配

1
2
3
4
5
6
7
8
9
10
11
ALTER KEYSPACE akka
WITH REPLICATION= {'class' : 'NetworkTopologyStrategy',
'dc2' : 3};

ALTER KEYSPACE akka_projection
WITH REPLICATION= {'class' : 'NetworkTopologyStrategy',
'dc2' : 3};

ALTER KEYSPACE akka_snapshot
WITH REPLICATION= {'class' : 'NetworkTopologyStrategy',
'dc2' : 3};

这次修改就不用运行修复任务了,因为之后我们会把整个旧集群都直接删除。

临时停用旧集群节点以验证服务可用性,这里我没有想到好办法能不删除 pvc 停用,用的是手动替换镜像的方式,比如把 /cass-management-api:4.0.1 替换成一个可用的 aws-cli:2.4.17 镜像,这样 pod 能启动,但是对应的健康检查过不去,外部请求到旧集群实际不可用

临时停用节点

1
2
3
<docker-registry-url>/k8ssandra/cass-management-api:4.0.1 

<docker-registry-url>/bitnami/aws-cli:2.4.17

在此情况下调用接口以验证数据是否正常

验证完毕后,修改认证使用到的 keyspace ,移除 dc1 的副本

1
2
3
ALTER KEYSPACE system_auth
WITH REPLICATION= {'class' : 'NetworkTopologyStrategy',
'dc2' : 3};

以上验证完毕后就可以回收资源了

使用 kubectl delete -f删除对应的 dc

使用 nodetool 删除节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
cassandra@dc2-default-sts-0:/$ nodetool status
Datacenter: dc1
===============
Status=Up/Down
|/ State=Normal/Leaving/Joining/Moving
-- Address Load Tokens Owns (effective) Host ID Rack
DN 10.0.45.205 46.74 GiB 1 0.0% 821d2534-0ea9-4c7c-a312-0e6047569503 default
DN 10.0.59.211 65.04 GiB 1 0.0% acceeaa7-2342-430b-82e1-32d08a558fb4 default
DN 10.0.56.165 67.34 GiB 1 0.0% 966fb0fb-2079-41cf-a0d9-9329bc3a53a6 default
Datacenter: dc2
===============
Status=Up/Down
|/ State=Normal/Leaving/Joining/Moving
-- Address Load Tokens Owns (effective) Host ID Rack
UN 10.0.57.90 139.74 GiB 1 100.0% da7a2578-6089-4065-a1e7-8db91e2dc045 default
UN 10.0.58.35 147.21 GiB 1 100.0% cccfbcac-35eb-4d2a-aff4-263b98bb7c1d default
UN 10.0.62.14 148.9 GiB 1 100.0% b2059b41-7899-430a-a386-fa43f3805a8e default
1
2
3
nodetool removenode 821d2534-0ea9-4c7c-a312-0e6047569503
nodetool removenode acceeaa7-2342-430b-82e1-32d08a558fb4
nodetool removenode 966fb0fb-2079-41cf-a0d9-9329bc3a53a6

至此,所有迁移工作完成撒花

2023 更新

之后由于其他需求,需要把部分 cassandra 里面的数据垂直迁移,又做了一遍迁移的步骤。
发现这里其实不应该使用 nodetool repair,因为 repair 会把对比新旧两个数据中心的数据以后再开始数据传输。
然而,新的数据中心里面的数据是空的,根本没有需要进行数据对比的必要
这里的数据量比较大,所以会很慢,而且会占用很多的网络带宽

所以这里应该使用 nodetool rebuild,这个命令只会把缺失的数据读取过来,不会读取已经存在的数据,所以会快很多

1
2
# 注意此处一定要指定 nodetool rebuild 的数据中心
$ nodetool rebuild -- old_datacenter_name

还有一点需要注意的是,在执行迁移操作时,一定要确保客户端的读、写一致性级别正确被设置

要防止客户端过早连接到新数据中心,并确保读取或写入的一致性级别不会查询新数据中心:

确保客户端配置为使用 DCAwareRoundRobinPolicy

确保客户端指向现有数据中心,这样他们就不会尝试访问新的数据中心,因为新数据中心可能没有任何数据。

如果使用QUORUM一致性级别,需要更改为LOCAL_QUORUM

如果使用ONE一致性级别,需要设置为LOCAL_ONE

如果客户端应用程序未正确配置,它们可能会在数据中心就绪之前连接到新的数据中心。这会导致连接异常、超时和/或数据不一致。
以 akka projection 的默认配置为例, 默认的一致性级别是 QUORUM, 就需要设置成 LOCAL_QUORUM

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
datastax-java-driver {
basic {
contact-points = ["{{ .Values.cassandra.host }}:9042"]
load-balancing-policy.local-datacenter = "{{ .Values.cassandra.datacenter }}"
request.consistency = LOCAL_QUORUM
}
profiles {
akka-persistence-cassandra-profile {
basic.request {
consistency = LOCAL_QUORUM
}
}
akka-projection-cassandra-profile {
basic.request {
consistency = LOCAL_QUORUM
}
}
}
}

数据迁移完成后,可以手动执行 nodetool compact 来压实数据,这样可以节省一些磁盘空间

参考链接

  1. cass-operator 文档: 零停机迁移 cassandra 数据库
  2. datastax 文档: 在集群中添加新的数据中心
  3. medium: 在 k8s 集群间迁移 cassandra