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、脚本、代码及数据等均为演示用途,不含真实业务数据,也不具备直接运行或复现的完整上下文。
    • 读者若需在实际项目中参考本文方案,请结合自身业务场景及数据安全规范,使用符合内部命名和权限控制的配置。

Data Desensitization Notice: All table names, field names, API endpoints, variable names, IP addresses, and sample data appearing in this article are fictitious and intended solely to illustrate technical concepts and implementation steps. The sample code is not actual company code. The proposed solutions are not complete or actual company solutions but are summarized from the author's memory for technical learning and discussion.
    • Any identifiers shown in the text do not correspond to names or numbers in any actual production environment.
    • Sample SQL, scripts, code, and data are for demonstration purposes only, do not contain real business data, and lack the full context required for direct execution or reproduction.
    • Readers who wish to reference the solutions in this article for actual projects should adapt them to their own business scenarios and data security standards, using configurations that comply with internal naming and access control policies.

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

Copyright Notice: The copyright of this article belongs to the original author. Without prior written permission from the author, no entity or individual may copy, reproduce, excerpt, or use it for commercial purposes in any way.
    • For non-commercial citation or reproduction of this content, attribution must be given, and the integrity of the content must be maintained.
    • The author reserves the right to pursue legal action against any legal disputes arising from the commercial use, alteration, or improper citation of this article's content.

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