AWS云上关于Keyspaces开启CDC进行数据捕获并存储在S3使用Athena分析的实践

本文记录了在 Amazon Keyspaces上实测 变更数据捕获(CDC)流 的完整过程,验证了从数据变更捕获到分析查询的端到端链路。整体框架如下

flowchart LR %% 样式定义 classDef table fill:#e1f5fe,stroke:#01579b,stroke-width:1px classDef stream fill:#f3e5f5,stroke:#4a148c,stroke-width:1px classDef consumer fill:#fff3e0,stroke:#e65100,stroke-width:1px classDef storage fill:#e8f5e8,stroke:#1b5e20,stroke-width:1px classDef query fill:#fce4ec,stroke:#880e4f,stroke-width:1px %% 节点 A["workshop.users<br/>Keyspaces 表(启用 CDC)<br/>(INSERT/UPDATE/DELETE 操作)"]:::table B["CDC Stream<br/>(4 shards)<br/>保留 24 小时,按 shard 组织<br/>精确一次,顺序一致"]:::stream C["cdc_consumer.py<br/>Python 消费者脚本 (boto3)<br/>拉取记录 → JSON Lines"]:::consumer D["S3 Bucket<br/>s3://keyspaces-cdc-xxxxxxxxxxxx/<br/>year/month/day/hour 时间分区"]:::storage E["Athena<br/>keyspaces_cdc.users_cdc<br/>SQL 分析查询<br/>支持嵌套字段提取"]:::query %% 横向连线 A -->|"CDC 流(近实时捕获行级变更)"| B B -->|"keyspacesstreams API<br/>(get-stream/get-shard-iterator/get-records)"| C C -->|"按时间分区写入"| D D -->|"Glue Catalog + JSON SerDe"| E

cqlsh-expansion 初始化

项目结构

/home/ec2-user/keyspaceenv/
  ├── .venv/                    # Python 虚拟环境
  ├── main.py                   # 入口文件
  ├── pyproject.toml            # 依赖定义
  ├── cdc_consumer.py           # CDC 消费者脚本
  ├── test_cdc.cql              # 测试 CQL 脚本

相关依赖

[project]
requires-python = ">=3.10"
dependencies = [
    "cassandra-driver>=3.30.0",
    "cqlsh>=6.2.2",
    "cqlsh-expansion>=0.9.6",
]

cqlsh-expansion 是 AWS 提供的 cqlsh 扩展版本,在标准 Apache Cassandra cqlsh 基础上增加了 AWS SigV4 认证支持,专门用于连接 Amazon Keyspaces。标准 cqlsh通过用户名/密码认证,而cqlsh-expansion使用 IAM 凭证 + SigV4 签名认证

使用之前需要先init动作,cqlsh-expansion.init 是一次性的安装后初始化脚本,执行 3 个动作:

  1. 创建 ~/.cassandra/ 目录(如果不存在)
  2. 复制 SSL 证书 → ~/.cassandra/sf-class2-root.crt
  3. 复制 cqlshrc 模板 → ~/.cassandra/cqlshrc

证书 sf-class2-root.crt 打包在 PyPI 包内(Starfield Class 2 CA 根证书,Amazon Trust Services 的根 CA,有效期到 2034 年)。

当运行 cqlsh-expansion cassandra.cn-north-1.amazonaws.com.cn 9142 --ssl 时:

  • 读取配置文件 ~/.cassandra/cqlshrc
  • 加载 Python 模块(cassandra-driver, cassandra_sigv4)
  • 实例化 SigV4AuthProvider(region_name='cn-north-1')
  • 创建 SSL 上下文(加载 sf-class2-root.crt)
  • SigV4 签名握手(HMAC-SHA256 签名 → IAM 验证)
  • 建立 CQL 连接(端口 9142,非标准 9042)
  • 加载系统元数据(keyspaces, tables, columns)
  • 显示 cqlsh> 提示符

配置文件内容~/.cassandra/cqlshrc如下

[auth_provider]
module = cassandra_sigv4.auth
classname = SigV4AuthProvider
region_name = cn-north-1

[ssl]
certfile = ~/.cassandra/sf-class2-root.crt

[connection]
hostname = 127.0.0.1   ; 被命令行参数覆盖
port = 9042            ; 被命令行参数覆盖为 9142
ssl = false            ; 被 --ssl 参数覆盖

[copy-from]
ingestrate = 1500      ; 每秒最大导入行数
maxbatchsize = 10
chunksize = 30

使用之前需要激活虚拟环境

source .venv/bin/activate

# 连接 Amazon Keyspaces
cqlsh-expansion cassandra.cn-north-1.amazonaws.com.cn 9142 --ssl

# 连接并指定 keyspace
cqlsh-expansion cassandra.cn-north-1.amazonaws.com.cn 9142 --ssl -k workshop

# 执行单条命令
cqlsh-expansion cassandra.cn-north-1.amazonaws.com.cn 9142 --ssl -e 'DESCRIBE KEYSPACES;'
Keyspace: workshop
  ├── users                 — PRIMARY KEY (user_id)
  ├── user_access_by_app    — PRIMARY KEY ((user_id, app_name))
  └── user_sessions         — PRIMARY KEY (user_id, session_id, access_time)
                              CLUSTERING ORDER BY (session_id ASC, access_time DESC)

此外keyspace在连接路径上进行了服务适配( https://catalog.workshops.aws/workshops/0c3409ee-c04f-4c87-b0bf-c5022526b0d2/en-US/setup/first-content000 )

image

When connecting to Amazon Keyspaces, the driver requires a seed endpoint to establish an initial connection. Amazon Keyspaces uses DNS to route the initial connection to one of the many available endpoints. The endpoints are attached to network load balancers that in turn establish a connection to one of the request handlers in the fleet. After the initial connection is established, the client driver gathers information about all available endpoints from the system.peers table. With this information, the client driver can create additional connections to the listed endpoints. The number of connections the client driver can create is limited by the number of local connections specified in the client driver settings. By default, most client drivers establish one connection per endpoint and establish a connection pool to Cassandra and load balance queries over that pool of connections. Although multiple connections can be established to the same endpoint, behind the network load balancer they may be connected to many different request handlers. When connecting through the public endpoint, establishing one connection to each of the nine endpoints listed in the system.peers table results in nine connections to different request handlers.

开源Cassandra通过 seed endpoint 建立初始连接,从 system.peers 表获取集群节点信息,客户端驱动建立连接池并做负载均衡。而keyspace则

  1. DNS 路由到多个可用端点 — 开源 Cassandra 的 seed 是直接指向具体节点 IP,而 Keyspaces 用 DNS 做初始路由
  2. Network Load Balancer 层 — 开源 Cassandra 是客户端直连节点,没有 NLB 中间层
  3. Request Handler Fleet — 背后是无状态的请求处理器集群,不是传统的 Cassandra 节点
  4. 固定 9 个 peers — system.peers 返回固定 9 个端点(公网),而开源 Cassandra 返回的是实际集群节点列表
  5. 同一端点的多连接可能连到不同 request handler — 因为有 NLB,这在直连节点的开源 Cassandra 中不存在

CDC 核心概念

CDC 是一种近实时记录数据库表行级变更的机制。Amazon Keyspaces CDC 会将每次 INSERT、UPDATE、DELETE 操作生成一条变更记录,存储在有序的日志流中。并且提供了如下核心保证:

  • 精确一次(Exactly Once):每条行级变更在 CDC 流中只出现一次
  • 顺序一致:同一主键上的变更按实际发生顺序排列
  • 零容量消耗:CDC 不消耗表的读写容量
  • 保留 24 小时:不可更改,过期自动删除

流的组织结构如下

  • Stream:表级别,一张表一个流
  • Shard:按主键范围分组,Amazon Keyspaces 自动管理(拆分/合并)
  • Record:单条行级变更,有唯一递增的 sequenceNumber
CDC Stream (ARN)
  ├── Shard 1 (按主键范围划分)
  │     ├── Record (sequenceNumber: 001) → INSERT user u001
  │     ├── Record (sequenceNumber: 002) → UPDATE user u001
  │     └── ...
  ├── Shard 2
  │     ├── Record → INSERT user u002
  │     └── ...
  ├── Shard 3
  └── Shard 4

注意:启用 CDC 时必须选择视图类型View Type),并且启用后不可更改

类型 记录内容 适用场景
NEW_AND_OLD_IMAGES 变更前后的完整行 审计、diff 分析
NEW_IMAGE 仅变更后的行 数据同步、搜索索引
OLD_IMAGE 仅变更前的行 回滚、补偿事务
KEYS_ONLY 仅主键 轻量通知、触发器

CDC数据捕获流程

启用 CDC 流

CDC 可以在创建表时启用,也可以为现有表启用。启用时 Amazon Keyspaces 会自动创建服务关联角色 AWSServiceRoleForAmazonKeyspacesCDC 用于发布 CloudWatch 指标。

我们选择为已有的 workshop.users 表启用 CDC:

aws keyspaces update-table \
  --keyspace-name workshop \
  --table-name users \
  --cdc-specification status=ENABLED,viewType=NEW_AND_OLD_IMAGES \
  --region cn-north-1
  
outpur:
{
    "resourceArn": "arn:aws-cn:cassandra:cn-north-1:xxxxxxxxxxxx:/keyspace/workshop/table/users"
}

启用是异步操作,需要等待状态变为 ACTIVE:

aws keyspaces get-table --keyspace-name workshop --table-name users --region cn-north-1

output
status: "UPDATING", cdcSpecification.status: "ENABLING"   (约 15 秒后)
status: "ACTIVE",   cdcSpecification.status: "ENABLED"

查看流信息

aws keyspacesstreams list-streams \
  --keyspace-name workshop --table-name users --region cn-north-1
 
output
{
    "streams": [{
        "streamArn": "arn:aws-cn:cassandra:cn-north-1:xxxxxxxxxxxx:/keyspace/workshop/table/users/stream/2026-05-12T07:08:20.735",
        "keyspaceName": "workshop",
        "tableName": "users",
        "streamLabel": "2026-05-12T07:08:20.735"
    }]
}

控制台信息如下

image-20260512161737385

触发 CDC 事件

如下操作会触发cdc时间,并且行为各不一致

操作 CDC 行为
INSERT 生成一条记录,newImage 包含完整行
UPDATE 生成一条记录,newImage 为更新后的行,oldImage 为更新前的行
DELETE 生成一条记录,newImage 为空(KEYS_ONLY 时仅有主键)
BATCH 分解为多条独立的行级记录
TTL 过期 生成记录,标明 TTL 是变更来源

例如如下表结构

CREATE TABLE workshop.users (
    user_id text PRIMARY KEY,
    access_date date,
    age int,
    app_name text,
    name text,
    state text
)

执行的测试脚本(test_cdc.cql):

CONSISTENCY LOCAL_QUORUM;
-- 3 条 INSERT
INSERT INTO workshop.users (user_id, name, age, state, app_name, access_date)
  VALUES ('u001', 'Alice', 28, 'BJ', 'portal', '2026-05-12');
INSERT INTO workshop.users (user_id, name, age, state, app_name, access_date)
  VALUES ('u002', 'Bob', 35, 'SH', 'dashboard', '2026-05-12');
INSERT INTO workshop.users (user_id, name, age, state, app_name, access_date)
  VALUES ('u003', 'Charlie', 42, 'GZ', 'api', '2026-05-12');
-- 1 条 UPDATE
UPDATE workshop.users SET age = 29, state = 'SZ' WHERE user_id = 'u001';
-- 1 条 DELETE
DELETE FROM workshop.users WHERE user_id = 'u003';

执行日志结果

Consistency level set to LOCAL_QUORUM.

 user_id | access_date | age | app_name  | name     | state
---------+-------------+-----+-----------+----------+-------
    u001 |  2026-05-12 |  29 |    portal |    Alice |    SZ
    u002 |  2026-05-12 |  35 | dashboard |      Bob |    SH

(2 rows)

预期产生 5 个 CDC 事件(3 INSERT + 1 UPDATE + 1 DELETE)。

消费 CDC 流

CDC 流的消费遵循 拉取模型(Pull)

  • get-stream 获取流中所有 shard 列表
  • get-shard-iterator获取某个 shard 的起始位置(迭代器)
  • get-records用迭代器拉取记录,返回下一个迭代器
  • 重复步骤 3 持续消费

迭代器类型分为如下几种

  • TRIM_HORIZON:从 shard 最早可用记录开始
  • LATEST:从当前时刻之后的新记录开始
  • AT_SEQUENCE_NUMBER:从指定序列号开始
  • AFTER_SEQUENCE_NUMBER:从指定序列号之后开始

Python 消费者脚本(cdc_consumer.py)如下

"""Keyspaces CDC -> S3 consumer. Dumps raw records for inspection."""
import json
import time
from datetime import datetime, timezone
import boto3

REGION = "cn-north-1"
STREAM_ARN = "arn:aws-cn:cassandra:cn-north-1:xxxxxxxxxxxx:/keyspace/workshop/table/users/stream/2026-05-12T07:08:20.735"
BUCKET = "keyspaces-cdc-xxxxxxxxxxxx" # s3 桶名称
PREFIX = "cdc/workshop/users"

client = boto3.client("keyspacesstreams", region_name=REGION)
s3 = boto3.client("s3", region_name=REGION)


def get_shards():
    resp = client.get_stream(streamArn=STREAM_ARN)
    return resp.get("shards", [])


def get_records_from_shard(shard_id):
    resp = client.get_shard_iterator(
        streamArn=STREAM_ARN, shardId=shard_id, shardIteratorType="TRIM_HORIZON"
    )
    iterator = resp["shardIterator"]
    all_records = []
    for _ in range(10):
        resp = client.get_records(shardIterator=iterator, maxResults=100)
        records = resp.get("changeRecords", [])
        if records:
            all_records.extend(records)
        iterator = resp.get("nextShardIterator")
        if not iterator or not records:
            break
        time.sleep(0.5)
    return all_records


def write_to_s3(records):
    if not records:
        return
    now = datetime.now(timezone.utc)
    partition = now.strftime("year=%Y/month=%m/day=%d/hour=%H")
    key = f"{PREFIX}/{partition}/{now.strftime('%Y%m%dT%H%M%S')}_{len(records)}.json"
    lines = [json.dumps(r, ensure_ascii=False, default=str) for r in records]
    body = "\n".join(lines)
    s3.put_object(Bucket=BUCKET, Key=key, Body=body.encode("utf-8"),
                  ContentType="application/json")
    print(f"Wrote {len(records)} records to s3://{BUCKET}/{key}")


def main():
    shards = get_shards()
    print(f"Found {len(shards)} shards")
    all_records = []
    for shard in shards:
        records = get_records_from_shard(shard["shardId"])
        all_records.extend(records)
    print(f"Total records collected: {len(all_records)}")
    write_to_s3(all_records)


if __name__ == "__main__":
    main()

首次尝试(INSERT 后立即消费)会失败

Found 4 shards
Total records collected: 0

等待 2 分钟后重试,表明CDC 传播延迟约 1-2 分钟,实际需要考虑这个延迟。

Found 4 shards
  Shard shardId-000000017785... got 2 records
  Shard shardId-000000017785... got 1 records
  Shard shardId-000000017785... got 3 records

Total records collected: 6
Wrote 6 records to s3://keyspaces-cdc-xxxxxxxxxxxx/cdc/workshop/users/year=2026/month=05/day=12/hour=07/20260512T072409_6.json

如果使用KCL的将获取如下优势

  • 自动 shard 管理和血统跟踪
  • Worker 间负载均衡
  • DynamoDB 检查点(断点续传)
  • 容错和故障恢复
  • 自动适应 shard 拆分/合并

其中KCL 使用 DynamoDB 表作为分布式锁 + 进度追踪机制,协调多个消费者 Worker

信息 作用
Shard → Worker 映射 哪个 Worker 负责消费哪个 Shard
检查点(Checkpoint) 每个 Shard 消费到了哪个 sequenceNumber
租约过期时间 Worker 挂了后,其他 Worker 何时可以接管
Worker 心跳 判断 Worker 是否存活

KCL 自动创建的 DynamoDB 表包括

表名 用途
<APP_NAME> 主租赁表(shard 分配 + 检查点)
<APP_NAME>-WorkerMetricStats Worker 性能指标
<APP_NAME>-CoordinatorState 协调器状态

注意: 本次测试使用直接 API 调用(无检查点、无负载均衡),适合开发验证。生产环境应使用 KCL 以获得上述能力。

实际捕获的一条 INSERT 记录(u001 Alice)

{
  "eventVersion": "1.1",
  "createdAt": "2026-05-12 07:10:52.907000+00:00",
  "origin": "USER",
  "partitionKeys": {
    "user_id": {
      "textT": "u001"
    }
  },
  "clusteringKeys": {},
  "newImage": {
    "valueCells": {
      "user_id": { "value": { "textT": "u001" } },
      "access_date": { "value": { "dateT": "2026-05-12" } },
      "age": { "value": { "intT": "28" } },
      "app_name": { "value": { "textT": "portal" } },
      "name": { "value": { "textT": "Alice" } },
      "state": { "value": { "textT": "BJ" } }
    },
    "staticCells": {}
  },
  "sequenceNumber": "17353800000765995285078678"
}

字段说明如下

字段 说明
eventVersion 事件格式版本(1.1)
createdAt 变更发生时间(UTC)
origin 变更来源(USER = 用户操作,SYSTEM = TTL 等)
partitionKeys 分区键值,类型化表示(textT, intT 等)
clusteringKeys 聚簇键值(本表无聚簇键,为空)
newImage 变更后的行数据(DELETE 时为空)
oldImage 变更前的行数据(INSERT 时为空,本次未出现因为是首次写入)
sequenceNumber 分片内唯一递增序列号

将 CDC 数据写入 S3 时,采用 Hive 风格分区(key=value/),这样 Athena 可以自动识别分区并进行分区裁剪(Partition Pruning),只扫描需要的数据。

分区路径设计如下,本次使用 JSON Lines,生产环境建议使用 Parquet + Snappy 压缩。

s3://keyspaces-cdc-xxxxxxxxxxxx/
  cdc/workshop/users/
    year=2026/
      month=05/
        day=12/
          hour=07/
            20260512T072409_6.json    ← JSON Lnes 格式

验证 S3 数据如下

aws s3 ls s3://keyspaces-cdc-xxxxxxxxxxxx/cdc/ --recursive --region cn-north-1
2026-05-12 07:24:10  3323  cdc/workshop/users/year=2026/month=05/day=12/hour=07/20260512T072409_6.json

CDC数据结果查询

我们用Athena 直接对 S3 中的数据执行 SQL 查询:

创建 Glue Database

aws glue create-database \
  --database-input '{"Name":"keyspaces_cdc","Description":"Keyspaces CDC data"}' \
  --region cn-north-1

创建 Athena 外部表,CDC 记录是深度嵌套的 JSON,需要用 struct 类型映射:

CREATE EXTERNAL TABLE IF NOT EXISTS keyspaces_cdc.users_cdc (
  eventVersion string,
  createdAt string,
  origin string,
  partitionKeys struct<user_id:struct<textT:string>>,
  clusteringKeys string,
  newImage struct<valueCells:struct<
    user_id:struct<value:struct<textT:string>>,
    access_date:struct<value:struct<dateT:string>>,
    age:struct<value:struct<intT:string>>,
    app_name:struct<value:struct<textT:string>>,
    name:struct<value:struct<textT:string>>,
    state:struct<value:struct<textT:string>>
  >,staticCells:string>,
  sequenceNumber string
)
PARTITIONED BY (year string, month string, day string, hour string)
ROW FORMAT SERDE 'org.openx.data.jsonserde.JsonSerDe'
LOCATION 's3://keyspaces-cdc-xxxxxxxxxxxx/cdc/workshop/users/'
TBLPROPERTIES ('has_encrypted_data'='false');

加载分区

MSCK REPAIR TABLE keyspaces_cdc.users_cdc;

分析查询

SELECT
  createdAt,
  origin,
  partitionKeys.user_id.textT as user_id,
  newImage.valueCells.name.value.textT as name,
  newImage.valueCells.age.value.intT as age,
  newImage.valueCells.state.value.textT as state,
  newImage.valueCells.app_name.value.textT as app_name,
  sequenceNumber
FROM keyspaces_cdc.users_cdc
ORDER BY createdAt;

查询结果如下

createdAt origin user_id name age state app_name 事件分析
2026-05-12 07:10:52.907 USER u001 Alice 28 BJ portal INSERT
2026-05-12 07:10:52.912 USER u002 Bob 35 SH dashboard INSERT
2026-05-12 07:10:52.918 USER u003 Charlie 42 GZ api INSERT
2026-05-12 07:10:52.924 USER u001 Alice 29 SZ portal UPDATE(age 28→29, state BJ→SZ)
2026-05-12 07:10:52.928 USER u003 (空) (空) (空) (空) DELETE(newImage 为空)
2026-05-12 07:13:50.960 USER u004 David 30 CD mobile INSERT

参考资料

posted @ 2026-05-12 16:20  zhaojie10  阅读(18)  评论(0)    收藏  举报