Cassandra 索引重建实践

Cassandra简介

Apache Cassandra 是一种分布式 NoSQL 数据库,属于宽列存储(Wide Column Store)模型。它最初由 Facebook 开发用于 Inbox 搜索,随后在2008年开源并进入 Apache 基金会孵化 。Cassandra 以其高可用性和线性可扩展性著称,能够在多节点集群中实现无单点故障的数据存储。它遵循 CAP 定理中 AP(Partition Tolerance 和 Availability)原则,提供可调的最终一致性。在 Cassandra 中,数据按照 Keyspace(密钥空间) 来划分,类似于关系型数据库中的数据库;Keyspace 下包含多个 Table(表),每个表通过 Primary Key(主键) 定义数据分区和排序方式。Cassandra 采用分布式哈希将数据分散到集群各节点上,支持在多数据中心部署下的容灾和跨地域复制。凭借其高吞吐和可伸缩性,Cassandra 被广泛应用于日志收集、物联网、社交网络等对海量数据和高可用要求严苛的场景。

Cassandra二级索引原理

在 Cassandra 中,每张表的主键决定了数据的分区和查询模式。如果需要通过非主键列进行查询,通常有两种办法:冗余建模(为新查询模式创建一张反范式表),或使用 二级索引。二级索引(Secondary Index) 是 Cassandra 提供的一种机制,允许在非主键列上建立索引,从而支持通过该列进行查询。使用 CREATE INDEX 语句即可为已有表添加二级索引;之后,对该列的新写入会自动更新索引,而已有的数据则会被后台异步索引。

二级索引的实现原理与关系型数据库不同:索引数据是本地的、非全局的。Cassandra 在每个存储数据的节点上为被索引列维护一个隐式的索引表(类似于隐藏的索引列族) 。这个索引表通常以索引列的值作为键,指向拥有该值的分区的主键。查询包含二级索引时,协调节点会将查询下发到每个节点,由各节点利用本地索引筛选出满足条件的主键,然后再由协调节点汇总结果。这意味着如果索引列不是查询的分区键的一部分,可能需要遍历集群中多个节点才能完成查询。高基数(High Cardinality) 或分布很广的列上建立二级索引通常效率低下 :因为几乎每个节点都可能持有匹配的数据,需要全局扫描;相反,对于基数较低且选择性较好的列(例如状态标志、类别等),二级索引可以有效地定位数据而无需手动创建冗余表。

值得注意的是,Cassandra 二级索引并不适合所有查询场景。如果滥用,可能导致写入性能下降(因为每次写入还需更新索引表),查询延迟增大,甚至给集群增加负担。在 Cassandra 3.x 中,引入了 SAI(Storage-Attached Indexing)等改进的索引机制,但经典的二级索引仍然被广泛使用。理解其工作原理有助于判断何时该使用索引、何时应考虑其他方案。

为什么需要手动重建索引

理想情况下,Cassandra 的二级索引会随着数据更新自动维护,不需要额外干预。然而,在实际生产环境中,可能出现索引异常或性能问题,需要我们手动干预重建索引。以下是常见的原因:

  • 索引不一致或损坏:由于集群故障、Bug 或其它异常情况,索引数据可能与实际数据不同步。例如,某些写入在索引表中未正确记录,导致通过索引查询无法找到实际存在的数据,或者已经删除的数据仍通过索引查询出现。这种索引数据不一致的情况需要重建索引来修复。
  • 性能下降:长时间运行后,索引表可能积累大量过期或无效条目(例如对应已删除或过期的数据),从而充斥大量墓碑(tombstone)记录,影响查询性能。如果索引列上的数据分布发生显著变化(例如大量插入删除),索引的查询效率可能下降。这时,重新索引可以清理无效数据、压缩索引结构,从而提升性能。
  • 数据批量导入或迁移:在通过 SSTable 离线导入数据、节点扩容/缩容或灾备数据恢复等操作后,二级索引可能没有及时构建完全。例如,将某节点数据文件拷贝恢复到新集群时,索引数据不会自动生成,需要手动重建索引以涵盖所有导入的数据。
  • 索引设计调整:有时为了优化查询,我们可能修改索引策略或列。在删除旧索引、更换新索引时,手动删除和重建索引是必要步骤。此外,Cassandra 提供了 nodetool rebuild_index 命令,可以在不删除索引定义的情况下重建索引,但在某些版本中可能不稳定。所以,删除后重新创建 often 是更直接可靠的做法。

总之,当出现索引无法正常服务(查询结果不正确或超时)或者索引本身导致写入/读取性能显著下降时,手动重建索引是一种有效的修复手段。

索引重建的实践步骤

下面以实际案例为背景,介绍 Cassandra 二级索引重建的一般步骤和实践方法。假设我们在 Keyspace keyspace_xxx 的表 table_xxx 上有一个索引 index_xxx,索引目标列为 field_xxx。近期监控发现,针对该索引列的查询出现异常(查询不到应有的数据),同时写入延迟升高,怀疑索引出现问题。我们计划在维护时间窗对其进行重建。大体步骤如下:

确认索引状况

在动手之前,先评估索引的当前状态。例如,可以登录节点服务器检查索引文件占用的磁盘大小,以评估索引规模。Cassandra 的数据目录通常位于 .../data/data/<keyspace>/<table> 下,其中二级索引数据会存储在类似 <table>.<index_name> 的子目录中。通过如下一条命令可以查看索引相关目录大小:

cd /path/to/cassandra-data/data/data/keyspace_xxx
du -h --max-depth=1 | grep index_xxx

如果发现索引数据文件异常庞大(例如数十GB),或与预期不符,说明索引可能累积了大量历史数据。这进一步佐证了需要重建索引的判断。

通知并停止相关应用

因为重建索引需要删除并重新创建索引,在此过程中依赖该索引的查询将不可用。为避免影响业务,应该提前通知相关方并在维护窗口执行。关闭或暂停使用该索引的应用服务(例如我们的案例中相关的 service_xxx 等微服务)以避免在索引重建期间收到错误查询结果。同时停止写入该表的数据也有助于保证重建期间数据一致性(虽然Cassandra允许在线重建索引,但静默数据环境下操作风险更低)。

连接到 Cassandra 集群

使用 CQL 工具或驱动连接到 Cassandra 执行索引重建操作。可以通过 cqlsh 连接,或者如下一样使用 Python Driver 脚本连接。这里以 cqlsh 为例:

$ cqlsh 10.x.x.x    # 连接到集群某节点的 CQL 接口,IP已脱敏为10.x.x.x

连接后,切换到目标 Keyspace:

USE keyspace_xxx;

备份表模式(可选)

在删除索引前,建议使用 DESCRIBE TABLE table_xxx; 保存表的 schema 定义。这有助于了解表结构和索引信息,防止误操作。如果需要,也可以备份当前索引的定义。

删除索引

执行索引删除语句,将异常的索引移除:

DROP INDEX IF EXISTS index_xxx;

使用 IF EXISTS 可以避免索引不存在时的错误。删除索引会从系统中移除索引的元数据,并触发集群各节点删掉本地索引数据(这个过程可能需要一点时间来清理磁盘上的索引SSTable文件)。

创建索引

确认索引已删除后,重新创建索引:

CREATE INDEX index_xxx ON table_xxx (field_xxx);

Cassandra 会为已有的数据异步重建索引。在我们案例中,索引重建开始后,集群各节点会扫描表 table_xxx 上的 field_xxx 列数据并插入索引条目。这个过程对大表来说可能比较耗时。在重建过程中,新的索引查询可能仍然无法返回完整结果,直到重建完成。

监控重建进度

索引创建语句提交后,需等待索引重建完成。可以通过以下方式监控:

  • 日志观察:检查各节点的 Cassandra 日志(system.log),搜索关于索引重建的消息。例如 Cassandra 会记录索引建立完成的时间点。我们的实践中,从开始到索引构建完毕耗时约数小时,需耐心等待。
  • 目录大小变化:再次查看数据目录中索引目录的大小增长情况。如果索引文件大小趋于稳定,且接近原表数据体量的预期比例,表明重建接近完成。
  • 性能监控:重建索引期间,节点 CPU 和 IO 可能显著升高。待这些指标恢复正常水平,也暗示重建结束。

验证索引

索引重建完成后,使用查询验证索引是否正常工作。可以使用之前存在的一些实际值进行测试。例如,我们知道在表 table_xxx 中存在一条记录,其 field_xxx 字段值为 ‘some_value’(这里用示例值代替实际值)。我们尝试用索引列来查询它:

SELECT * 
FROM table_xxx 
WHERE field_xxx = 'some_value' 
LIMIT 1;

如果返回结果包含该记录(或至少不再超时),说明索引查询功能恢复正常。对于数值类型的字段查询时无需加引号,这里要根据字段类型调整查询语句。

恢复应用服务

确认索引功能恢复后,重新启动先前暂停的应用服务。例如:

# 重启相关应用服务
$ cd /path/to/service_xxx1 && sh startup.sh start
$ cd /path/to/service_xxx2 && sh startup.sh start
$ cd /path/to/service_xxx3 && sh startup.sh start

让应用重新与 Cassandra 建立连接,并验证业务查询正常。此时索引重建流程全部完成。

经过上述步骤,我们成功修复并重建了 Cassandra 的二级索引 index_xxx。在实践中,我们的索引在 DROP 时释放了大量磁盘空间,重建后查询恢复正常、写入性能也有所改善。这印证了手动重建索引在解决索引异常问题上的有效性。

Python索引重建自动化小工具

上述过程可以通过人工逐步执行,但在某些情况下,我们希望将其工具化,以便更快捷且可重复地对索引进行操作。下面提供一个使用 Python 和 DataStax Cassandra Driver (cassandra-driver) 的小工具示例,用于自动化地连接集群并重建指定索引。这个脚本可以执行以下工作:

  • 连接 Cassandra 集群(支持指定节点地址列表)。
  • 删除指定索引(若存在)。
  • 创建指定索引。
  • (可选)使用给定的测试值查询索引列,验证索引是否有效。

在使用该脚本之前,请确保已通过 pip install cassandra-driver 安装 Cassandra Python 驱动,并根据需要配置好 Cassandra 集群的网络连通性和认证信息(如果集群启用了用户名/密码验证,需要在代码中添加认证支持,例如使用 PlainTextAuthProvider)。

下面是脚本源码:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Cassandra 二级索引重建工具
"""
import argparse
from cassandra.cluster import Cluster
# 如果需要认证,可启用以下导入并在 Cluster 时加入 auth_provider
# from cassandra.auth import PlainTextAuthProvider

# 解析命令行参数
parser = argparse.ArgumentParser(description="Cassandra index rebuild tool")
parser.add_argument('--hosts', required=True, help="Cassandra contact points (comma separated IP list)")
parser.add_argument('--keyspace', required=True, help="Keyspace name")
parser.add_argument('--table', required=True, help="Table name")
parser.add_argument('--column', required=True, help="Column name to index on")
parser.add_argument('--index', required=True, help="Index name")
parser.add_argument('--test-value', help="Optional test value to verify the index query")
args = parser.parse_args()

contact_points = args.hosts.split(',')
keyspace = args.keyspace
table = args.table
column = args.column
index = args.index
test_value = args.test_value

# 建立集群连接(默认端口9042,如需其它端口可在Cluster参数中指定)
# 如需认证: e.g., auth_provider = PlainTextAuthProvider(username='user', password='pass')
cluster = Cluster(contact_points)
session = cluster.connect(keyspace)
print(f"Connected to cluster at {contact_points}, keyspace {keyspace}")

# 删除已有索引
drop_cql = f"DROP INDEX IF EXISTS {index}"
session.execute(drop_cql)
print(f"Index {index} dropped (if existed).")

# 创建新索引
create_cql = f"CREATE INDEX {index} ON {table} ({column})"
session.execute(create_cql)
print(f"Index {index} created on {table}({column}).")

# 验证索引(如果提供了测试值)
if test_value:
    query = f"SELECT {column} FROM {table} WHERE {column}=%s LIMIT 1"
    rows = session.execute(query, [test_value])
    if rows:
        print(f"Index query successful, found {column} = {test_value}.")
    else:
        print(f"No results for {column} = {test_value}. The value might not exist in table.")
else:
    print("No test value provided for verification, skipping index query.")

# 关闭连接
session.shutdown()
cluster.shutdown()
print("Index rebuild completed.")

上述脚本通过命令行参数指定要连接的节点、Keyspace、表、列和索引名称,并执行重建操作。我们使用 DROP INDEX IF EXISTS 确保在索引存在时删除它,随后用 CREATE INDEX 重建。cassandra-driver 会等待集群元数据同步(Schema agreement),确保索引在整个集群中创建。注意:创建索引会立即返回,但索引的数据重建在后台异步进行,因此脚本返回后索引可能还在构建中;如果数据量大,可能需要等待一段时间才能查询出结果。

在验证环节中,若提供了 —test-value 参数,脚本会使用该值对索引列执行一次查询(等价于 SELECT … WHERE column = ‘value’ LIMIT 1),并输出是否查找到结果。这可以简单地检验索引查询是否生效。但需要注意,这个验证依赖于提供的值确实存在于表中。如果值不存在或者索引尚未完全重建,查询可能得不到结果。

示例:使用Python脚本重建索引

假设我们希望对前述的 index_xxx 索引执行重建,索引目标列 field_xxx 我们已知存在值 ‘some_value’ 用于测试。集群的某节点 IP 为 10.x.x.x(已脱敏)。我们可以按照如下方式运行脚本:

$ python cass_rebuild_index.py \
    --hosts 10.x.x.x \
    --keyspace keyspace_xxx \
    --table table_xxx \
    --column field_xxx \
    --index index_xxx \
    --test-value some_value

运行后,脚本会依次输出连接信息、索引删除/创建进度以及验证结果。例如,预期输出如下:

Connected to cluster at ['10.x.x.x'], keyspace keyspace_xxx  
Index index_xxx dropped (if existed).  
Index index_xxx created on table_xxx(field_xxx).  
Index query successful, found field_xxx = some_value.  
Index rebuild completed.  

从输出可以看到索引已成功重建,并且能够通过索引查询到测试值。此时,我们还需要留意实际集群中索引重建的完成情况。如果数据量很大,脚本可能在索引完全建好之前就返回了“查询成功”(因为刚好测试值所在的部分已经建索引完成)。对于生产环境中的大型索引,建议在脚本执行后,通过监控或日志进一步确认索引重建的完成,然后再恢复业务流量。

结论

在 Cassandra 集群中,二级索引的异常修复与重建是一项需要谨慎对待的维护操作。本文通过介绍 Cassandra 和二级索引的原理,阐述了手动重建索引的原因和重要性,并给出了实际操作步骤和自动化脚本示例。在实际应用中,我们应尽量避免二级索引失效或性能问题的发生,例如合理评估索引列的基数和查询模式,定期关注索引相关的监控指标。一旦索引出现问题,按照上述流程重建索引能够有效恢复集群的查询能力。但需要注意的是,重建索引对集群有一定性能影响,应在业务低谷期进行,并做好应用停机或流量切换的准备。

通过对 Cassandra 索引重建实践 的深入了解,我们可以更从容地应对分布式数据库运维中的索引挑战,保障数据库服务的稳定可靠运行。

Ge Yuxu • AI & Engineering

脱敏说明:本文所有出现的表名、字段名、接口地址、变量名、IP地址及示例数据等均非真实, 仅用于阐述技术思路与实现步骤,示例代码亦非公司真实代码。 示例方案亦非公司真实完整方案,仅为本人记忆总结,用于技术学习探讨。
    • 文中所示任何标识符并不对应实际生产环境中的名称或编号。
    • 示例 SQL、脚本、代码及数据等均为演示用途,不含真实业务数据,也不具备直接运行或复现的完整上下文。
    • 读者若需在实际项目中参考本文方案,请结合自身业务场景及数据安全规范,使用符合内部命名和权限控制的配置。

版权声明:本文版权归原作者所有,未经作者事先书面许可,任何单位或个人不得以任何方式复制、转载、摘编或用于商业用途。
    • 若需非商业性引用或转载本文内容,请务必注明出处并保持内容完整。
    • 对因商业使用、篡改或不当引用本文内容所产生的法律纠纷,作者保留追究法律责任的权利。

Copyright © 1989–Present Ge Yuxu. All Rights Reserved.