Teleport DynamoDB 审计事件存储演进:RFD 24 按天分区全局二级索引(timesearchV2)设计解析
2026/9/21 19:21:57 网站建设 项目流程
  • 网络安全
  • 认证鉴权
  • 运维
  • 后端

【免费下载链接】teleport

The easiest, and most secure way to access and protect all of your infrastructure.

项目地址:https://gitcode.com/gh_mirrors/tel/teleport
点击查看免费下载

Teleport 将审计事件(audit events)持久化到 AWS DynamoDB 时,会面临 DynamoDB 单个分区 10 GB 容量上限带来的写入停摆风险。本文围绕仓库内已实现的设计文档 rfd/0024-dynamo-event-overflow.md(RFD 24)展开,剖析"在全局二级索引(GSI)上按事件日期yyyy-mm-dd分区"这一方案的前因后果,并结合 lib/events/dynamoevents/dynamoevents.go 的源码与测试,讲清timesearchV2索引的建表、写入、查询与兼容细节。读完本文,你将理解 DynamoDB 分区热点的成因、Teleport 事件表与事件索引的物理结构,以及大规模审计日志场景下时间范围检索为何必须按日期切分分区键。

背景:Teleport 的审计事件如何落到 DynamoDB

Teleport 的审计日志(auth 服务侧的audit_events_uri)支持多种后端,其中 DynamoDB 是生产环境常用的高可用选项。官方配置样例见 docs/pages/includes/config-reference/auth-service.yaml:

teleport: storage: # 集群状态存储使用 DynamoDB(独立于事件表) type: dynamodb region: us-east-1 table_name: Example_TELEPORT_DYNAMO_TABLE_NAME # 审计事件写入 DynamoDB 事件表(表结构与状态表完全不同) audit_events_uri: - "dynamodb://events_table_name" - "file:///var/lib/teleport/audit/events" - "stdout://"

需要注意两点(docs/pages/reference/deployment/backends.mdx 中有明确警告):

  • 事件表与状态表必须分开table_namedynamodb://中的events_table_name不能指向同一张表,二者的 schema 不同,混用会报错;
  • 事件表与状态表使用不同代码路径:状态存储在 lib/backend/dynamo/dynamodbbk.go,事件存储则在 lib/events/dynamoevents/dynamoevents.go,两者各自建表、各自维护索引。

RFD 24 讨论的正是事件表(audit events 表)的物理设计。事件表的主键为:

  • 分区键(Hash Key):SessionID(若事件不属于任何会话,则生成随机 UUID,见getSessionID);
  • 排序键(Range Key):EventIndex(会话内事件序号)。

由于分区键是会话 ID,写入会天然分散到大量分区,因此主表不会触碰 10 GB 单分区上限。

问题:全局二级索引的"单分区陷阱"

DynamoDB 的存储单元是分区(partition),单个分区最多容纳10 GB数据。Teleport 在主表之上维护了一个只读的全局二级索引(GSI),作为按时间检索审计事件的"物化视图"(materialized view)。RFD 24 指出旧索引存在两个致命设计缺陷:

1. 分区键是硬编码字符串default

旧 GSI 不以会话 ID 分区,而是以namespace 字段(硬编码字符串default)作为分区键。这意味着:

  • 索引只有一个分区,全部历史审计事件都堆积在该分区内;
  • 在生产部署中该分区逼近 10 GB 上限
  • 一旦达到 10 GB,索引会停止从主表同步数据,新的审计事件将无法被读取——即审计查询功能整体失效。

2. 单分区数据过多导致 B-Tree 过深

即使不触及 10 GB 硬限制,把海量事件塞进一个分区也会让 DynamoDB 内部用于排序的B-Tree 索引层级不断加深,从而拖慢检索性能。RFD 24 明确将"搜索时间受影响"列为需要解决的第二个隐患。

问题的本质

旧 GSI 的失败在于分区键基数太低:namespace 恒为default,等价于把所有事件压进同一个分区。RFD 24 的解决思路非常直接——把分区键换成每天变化的值,让"天"成为天然的分区单位。

方案总览:RFD 24 的三步迁移

RFD 24 的完整迁移路径如下:

  1. 新增日期字段:在每条审计事件上新增CreatedAtDate字段,取值为事件 UTC 时间的yyyy-mm-dd字符串;
  2. 新建按日期分区的 GSI:创建与旧索引等价的timesearchV2全局二级索引,但分区键改为CreatedAtDate(排序键仍为CreatedAt);
  3. 历史数据回填 + 删除旧索引:由 Auth 服务在 DynamoDB 后端初始化时触发一次性的后台任务,为存量事件补齐日期字段;回填完成后删除旧索引,迁移结束。

其中回填阶段有一个重要的可用性窗口:新索引建立后,尚未补上CreatedAtDate的旧事件在回填完成前不可见、不可检索。RFD 24 预期后台任务执行较快,事件会"很快重新出现",因此该窗口对用户影响有限。

从当前源码看,RFD 24 的主体已经落地:timesearchV2是仓库中唯一的按时间检索索引,旧索引在代码中已不再出现。

实现细节:从源码看 timesearchV2

下面结合 lib/events/dynamoevents/dynamoevents.go 逐项印证 RFD 24 的设计。

1. 新增日期字段与表结构

事件表的属性定义(tableSchema)在代码注释中明确标注了 RFD 24 的痕迹:

// lib/events/dynamoevents/dynamoevents.go var tableSchema = []dynamodbtypes.AttributeDefinition{ // Existing attributes pre RFD 24. {AttributeName: aws.String(keySessionID), AttributeType: dynamodbtypes.ScalarAttributeTypeS}, {AttributeName: aws.String(keyEventIndex), AttributeType: dynamodbtypes.ScalarAttributeTypeN}, {AttributeName: aws.String(keyCreatedAt), AttributeType: dynamodbtypes.ScalarAttributeTypeN}, // New attribute in RFD 24. {AttributeName: aws.String(keyDate), AttributeType: dynamodbtypes.ScalarAttributeTypeS}, }

对应常量:

// keyDate identifies the date the event was created at in UTC. // The date takes the format `yyyy-mm-dd` as a string. // Specified in RFD 24. keyDate = "CreatedAtDate"

日期格式使用 ISO 8601 的2006-01-02(即yyyy-mm-dd),该格式字符串在代码中以iso8601DateFormat定义。选择字符串日期作为分区键的原因在于:它可直接从查询时间范围生成,无需任何换算或二次查询。

2. 建表:GSI 的分区键从 namespace 换成日期

createTable创建的 GSI 定义与 RFD 24 提案完全一致:

GlobalSecondaryIndexes: []dynamodbtypes.GlobalSecondaryIndex{ { IndexName: aws.String(indexTimeSearchV2), // "timesearchV2" KeySchema: []dynamodbtypes.KeySchemaElement{ { // Partition by date instead of namespace. AttributeName: aws.String(keyDate), // CreatedAtDate(Hash Key) KeyType: dynamodbtypes.KeyTypeHash, }, { AttributeName: aws.String(keyCreatedAt), // CreatedAt(Range Key) KeyType: dynamodbtypes.KeyTypeRange, }, }, Projection: &dynamodbtypes.Projection{ProjectionType: dynamodbtypes.ProjectionTypeAll}, ProvisionedThroughput: &provisionedThroughput, }, },

代码注释 "Partition by date instead of namespace" 直接点明了 RFD 24 的核心改动,而常量声明处也写着:

// indexTimeSearchV2 is the new secondary global index proposed in RFD 24. // Allows searching events by time. indexTimeSearchV2 = "timesearchV2"

3. 写入路径:每条事件都带上日期

事件写入时(createPutItem),日期由事件自身的时间戳格式化而来:

e := event{ EventKey: EventKey{ SessionID: sessionID, EventIndex: in.GetIndex(), CreatedAt: in.GetTime().Unix(), CreatedAtDate: in.GetTime().Format(iso8601DateFormat), }, ... }

也就是说,新写入的事件天然携带分区键,主表数据会实时同步到timesearchV2的对应日期分区中。这正是"GSI 是主表物化视图"的体现。

4. 查询路径:把时间范围翻译成分区键集合

RFD 24 指出"在该新索引上搜索非常 trivial,因为分区键就是从查询起止时间生成的一组日期"。源码把这一思想拆成两步:

第一步,生成日期列表daysBetween按天生成yyyy-mm-dd字符串:

func daysBetween(start, end time.Time) []string { var days []string oneDay := time.Hour * time.Duration(24) startDay := daysSinceEpoch(start) // start.Unix() / (60*60*24) endDay := daysSinceEpoch(end) for startDay <= endDay { days = append(days, start.Format(iso8601DateFormat)) startDay++ start = start.Add(oneDay) } return days }

第二步,按日期逐个分区查询searchEventsRaw生成日期列表后,若查询按时间倒序(EventOrderDescending)则反转列表,然后交由eventsFetcher.QueryByDateIndex逐天发起 DynamoDB Query:

query := "CreatedAtDate = :date AND CreatedAt BETWEEN :start and :end" // ... for _, date := range l.dates { l.checkpoint.Date = date input := dynamodb.QueryInput{ KeyConditionExpression: aws.String(query), IndexName: aws.String(indexTimeSearchV2), // 走 timesearchV2 ... ScanIndexForward: aws.Bool(l.forward), } ... }

每个日期分区内的CreatedAt排序键再配合BETWEEN :start and :end做秒级裁剪,即可精确命中目标时间窗内的事件。由于查询是"分区键等值 + 排序键范围",这是 DynamoDB 中成本最低、延迟最稳定的访问模式。

值得注意的是,SearchSessionEvents(按会话查事件)仍然走主表的SessionID索引(IndexName: nil),与按时间检索各司其职。

5. 分页与断点续传(checkpoint)

跨天查询天然需要分页。实现用checkpointKey记录"当前查询到哪一天 + 该天内的 DynamoDB 游标":

type checkpointKey struct { Date string `json:"date,omitempty"` // 查询对应的日期 Iterator string `json:"iterator,omitempty"` // 恢复部分查询的 DynamoDB 游标 }
  • 每次 Query 返回后,若LastEvaluatedKey非空,会把当前事件的EventKey(含CreatedAtDate)序列化为游标存入checkpoint.Iterator
  • 下一轮调用通过StartKey传入 checkpoint,searchEventsRaw会先裁剪日期列表到 checkpoint 所在日期(dates[0] != checkpoint.Date就弹出队头),再从该日期继续查询;
  • 若 checkpoint 日期落在新的查询窗口之外,代码会做护栏检查:窗口前移则重置游标,窗口后移则直接空返回(对应测试TestCheckpointOutsideOfWindow)。

旧版本兼容:由于 checkpoint 会作为长期运行的导出任务的状态落盘,仓库专门保留了legacyCheckpointKey(旧格式的 raw DynamoDB 属性值游标),在解析StartKey失败时回退到旧格式解码,并标注DELETE IN: 19.0.0(见 legacy.go 与getCheckpointFromLegacyStartKey)。这说明按日期分区改造并未破坏旧客户端的中途续传。

6. 容量管理:自动扩缩容同样覆盖索引

auto_scaling: true时,configureTable会同时为主表和timesearchV2索引注册可扩缩目标与目标跟踪策略(dynamoevents.go 第 480-570 行):

resourceID: fmt.Sprintf("table/%s/index/%s", l.Tablename, indexTimeSearchV2), // read/write 目标跟踪策略:DynamoDBIndexReadCapacityUtilization / WriteCapacityUtilization

这意味着按日期分区后,热门的"今天"分区即使查询量大,其读容量也能被自动扩缩容覆盖,不会因为索引侧容量不足而限流。

配置与运维实践

事件表的完整配置参考

以下配置片段综合自 docs/pages/reference/deployment/backends.mdx 与 auth-service.yaml:

teleport: storage: type: dynamodb region: us-east-1 table_name: Example_TELEPORT_DYNAMO_TABLE_NAME # 状态表 audit_events_uri: - 'dynamodb://events_table_name' # 事件表(表名必须与状态表不同) audit_sessions_uri: s3://Example_TELEPORT_S3_BUCKET/records # 审计事件 TTL:默认 1 年;设为 0 则禁用 TTL。 # 注意:只有 DynamoDB 事件后端尊重该字段,其他后端通过 URI 查询参数配置。 retention_period: 365d billing_mode: "pay_per_request" # 或 provisioned continuous_backups: true

dynamodb://URI 支持的查询参数:

dynamodb://events_table_name?region=us-east-1&endpoint=dynamo.example.com&use_fips_endpoint=true
  • region:指定 AWS 区域;
  • endpoint:指向自建/兼容 DynamoDB 的端点(非 AWS 场景);
  • use_fips_endpoint:启用 FIPS 端点(优先级为 URI 参数 >--fips启动标志 >AWS_USE_FIPS_ENDPOINT环境变量)。

TTL 的实现同样与 RFD 24 的分区设计兼容:事件写入时按retention_period设置Expires属性(setExpiry),DynamoDB 会异步删除过期项,每个日期分区都会按 TTL 自然收缩,长期运行也不会让历史分区无限膨胀。

高可用部署限制

事件后端依赖 DynamoDB Streams 实现事件监听。官方文档明确警告:AWS 会对同时读取同一 stream 分片的进程限流,因此部署读取 DynamoDB 后端的 Auth 服务实例不能超过两个(见 backends.mdx 中的 danger 提示)。这也是在规划 HA 集群时与分区设计同等重要的约束。

测试验证:分区与分页逻辑的可信度

仓库在 dynamoevents_test.go 中为按日期分区方案提供了直接测试:

  • TestIndexExists:建表后断言timesearchV2索引存在且处于 Active/Updating 状态,验证新 GSI 是建表流程的固定组成部分;
  • TestDateRangeGenerator:验证daysBetween能正确处理跨月日期区间(如2021-08-302021-09-01),这是分区键集合正确性的基石;
  • TestCheckpointOutsideOfWindow:验证 checkpoint 日期落在查询窗口外时不会 panic,能安全返回空结果;
  • TestSizeBreak:用 200 KB 大事件模拟分页边界,验证processQueryOutput在响应超过events.MaxEventBytesInResponse时能保存 checkpoint、下一轮继续拉取,确保跨天查询不会丢事件;
  • TestSearchSessionEventsBySessionID等套件则覆盖主键索引与会话检索路径,保证按日期索引改造没有破坏既有查询。

总结

RFD 24 是 Teleport 在 DynamoDB 审计日志场景下的一次关键存储架构修正,其核心可归纳为三点:

  1. 问题本质是分区键基数过低:旧 GSI 用硬编码default分区,单分区数据逼近 10 GB 后索引停止同步、审计读取失效;
  2. 解法是"按天切分分区":新增CreatedAtDateyyyy-mm-dd)字段并作为timesearchV2GSI 的分区键,事件写入即带上分区键,查询时用daysBetween把时间范围展开成分区键集合,逐个分区 Query,兼顾容量扩展与检索性能;
  3. 迁移是平滑的:通过一次性的历史回填补齐旧事件,并以 checkpoint 兼容逻辑(DELETE IN: 19.0.0)保证长期导出任务在升级过程中不中断。

对于正在规划大规模审计日志存储的团队,这份设计文档与其源码落地(rfd/0024-dynamo-event-overflow.md、dynamoevents.go、dynamoevents_test.go)提供了一个可复用的范式:当 DynamoDB 索引的热点无法消除时,把分区键换成业务上有界、可推导的时间维度,是性价比最高的一步

  • 网络安全
  • 认证鉴权
  • 运维
  • 后端

【免费下载链接】teleport

The easiest, and most secure way to access and protect all of your infrastructure.

项目地址:https://gitcode.com/gh_mirrors/tel/teleport
点击查看免费下载

相关推荐

上一篇:Mobile-Agent架构深度解析:跨平台智能调度引擎的技术突破与实践指南
下一篇:Cryptozombies项目解析:ZombieFactory智能合约深度剖析

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询