当前位置: 首页 > article >正文

Harness 中的事件溯源:以事件日志重建状态

Harness 中的事件溯源以事件日志重建全链路 DevOps 状态引言痛点引入作为全链路 DevOps 平台Harness 每天会处理数百万到数千万级别的用户/系统操作开发者点击“启动流水线”、Feature Flag 规则引擎执行批量开关切换、云成本扫描器发现异常资源并标记、应用性能监控APM自动触发性能测试……这些操作的背后是整个 DevOps 协作链路的状态变迁——流水线从“空闲”到“排队”再到“构建中”“部署失败”Feature Flag 从“仅内部灰度”到“全量启用”EC2 实例从“待释放”到“标记忽略”……但在没有可靠的状态重建机制时全链路 DevOps 工程师会遇到哪些噩梦灾难性回滚束手无策比如 Feature Flag 误全量了一个导致支付接口挂掉的新功能传统的“直接改 Flag 配置”回滚只能覆盖当前状态但要查“谁、什么时候、通过什么方式改的配置前几次修改是什么对哪些测试环境/生产环境生效了支付接口挂掉的时间点和 Flag 切换的时间戳差多少”——这些问题没有清晰的、不可篡改的历史记录根本答不上回滚决策只能靠猜耽误的每一分钟都是真金白银的损失。审计合规无从谈起金融、医疗、政府等行业的 DevOps 平台必须满足严格的审计要求比如 SOC 2、HIPAA、GDPR 中的“操作可追溯”“变更审批留痕”。如果只用数据库快照或覆盖式的 CRUD 存储状态根本无法提供完整的、带因果关系的操作链——比如“张三提交的 Flag 变更李四在 14:30 拒绝王五后来又在 14:35 强制通过并部署”传统存储只能看到最后一次“部署中”的 Flag 状态中间的审批/拒绝/强制操作全丢了审计报告肯定过不了。问题定位效率极低比如一个流水线构建失败排查原因时只看到“Docker 镜像构建超时”但要查“超时前的 30 分钟该流水线使用的 Jenkins 节点有什么操作节点 CPU 利用率是多少镜像仓库的拉取速度突然下降是从什么时候开始的有没有其他流水线同时在使用同一个节点”——这些跨系统的状态变迁如果没有统一的事件源就得去翻 Jenkins 日志、Prometheus 监控、Docker 镜像仓库日志、Kubernetes 节点事件日志格式不一样、时间戳可能对不上、还有可能被日志清理策略删掉排查一个简单的问题可能要花几个小时甚至几天。状态一致性难以保证Harness 是一个分布式微服务架构比如有 Pipeline Service、Feature Flag Service、Cloud Cost Management Service 等几十个微服务不同微服务之间通过 RPC 或消息队列通信。如果只用覆盖式的 CRUD 存储当某个微服务调用失败或网络分区发生时很容易出现状态不一致——比如 Pipeline Service 已经更新了数据库里的“流水线构建中”状态但 Feature Flag Service 还没收到“启动灰度测试”的消息导致灰度测试没有触发用户看到的“流水线已完成灰度测试”和实际的“灰度测试未启动”状态冲突。解决方案概述Harness 解决上述所有问题的核心技术方案之一就是事件溯源Event Sourcing——这是一种以“事件”为核心而非以“当前状态”为核心的架构模式不再直接存储当前状态而是将所有对系统状态产生影响的操作都以“不可变事件Immutable Event”的形式按发生时间顺序写入“事件日志Event Log”。当前状态由事件日志重建任何时候需要获取某个实体比如某个流水线、某个 Feature Flag、某个 EC2 实例的当前状态只需要按时间顺序重放该实体相关的所有历史事件逐步更新状态最终得到的结果就是当前状态。事件日志是唯一的真相源所有的审计、回滚、问题定位、状态一致性保证都基于这个“不可变、按时间顺序、带因果关系”的事件日志。事件溯源听起来很简单但在 Harness 这种高并发、高可用、跨多租户、跨微服务的全链路 DevOps 平台上落地需要解决很多复杂的技术问题——比如事件的序列化/反序列化、事件日志的存储与检索、事件的重放性能优化、事件的版本兼容性、事件的因果关系追踪、多租户隔离、分布式事务的替代方案因为事件溯源通常结合 CQRS 模式使用不需要传统的分布式事务等。最终效果展示可选在 Harness 中事件溯源带来了哪些直观的、让用户/工程师眼前一亮的效果我们可以先展示几个典型的场景场景1Feature Flag 的完整历史与一键回滚在 Harness Feature Flag 的控制台中任何一个 Feature Flag 都有一个“事件时间线Event Timeline”页面上面清晰地列出了该 Flag 从创建到现在的所有操作比如“202X-05-20 10:00:00 张三创建 Flag PAYMENT_GATEWAY_V2状态为 OFF”“202X-05-20 14:00:00 张三提交 Flag 变更将 PAYMENT_GATEWAY_V2 开启至 内部团队10% 流量”“202X-05-20 14:20:00 李四审批通过该变更”“202X-05-20 14:21:00 Flag 变更生效10% 内部用户开始使用 V2 支付网关”“202X-05-20 15:00:00 王五提交 Flag 变更将 PAYMENT_GATEWAY_V2 开启至 全量用户”“202X-05-20 15:01:00 赵六发现支付接口错误率从 0.01% 飙升至 10%”“202X-05-20 15:02:00 赵六点击‘一键回滚到李四审批通过后的状态’”“202X-05-20 15:02:30 Flag 回滚生效错误率迅速下降至 0.01%”。而且在“一键回滚”时Harness不是直接修改当前的 Flag 配置而是生成一个新的“回滚事件Rollback Event”该事件的内容是“将 Flag 的状态恢复到事件 ID 为 12345李四审批通过后的事件时的状态”然后将这个回滚事件写入事件日志——这样回滚操作本身也被记录下来审计合规仍然没问题而且如果回滚操作本身有问题还可以“回滚回滚操作”真正做到了“任何操作都是可逆的只要不删除事件日志”。场景2流水线的全链路问题定位在 Harness Pipeline 的控制台中任何一个失败的流水线执行都有一个“执行追踪Execution Trace”页面这个页面基于事件溯源重建了整个流水线执行的完整状态变迁链并且关联了跨系统的所有相关事件比如“202X-05-21 09:00:00 开发者点击启动流水线生成事件 PIPELINE_TRIGGERED关联的 Git 提交 ID 是 abc123”“202X-05-21 09:00:10 事件 QUEUEING_JOB流水线被分配到 Jenkins 集群的节点 node-123”“202X-05-21 09:00:20 事件 BUILD_STARTEDDocker 镜像构建开始关联的 Dockerfile 路径是 /app/Dockerfile”“202X-05-21 09:05:00 事件 BUILD_STEP_FAILED错误信息是‘npm install 超时无法连接到 registry.npmjs.org’”“同时该页面还关联了 Prometheus 的监控事件 NODE_CPU_HIGHnode-123 的 CPU 利用率从 09:04:30 开始飙升至 95%、Docker 镜像仓库的事件 REGISTRY_LATENCY_HIGH从 09:04:40 开始registry.npmjs.org 的拉取延迟从 100ms 飙升至 10s、Kubernetes 的事件 POD_RESTARTINGnode-123 上的其他 3 个 Jenkins 代理 Pod 从 09:04:50 开始重启”。有了这个“执行追踪”页面全链路 DevOps 工程师只需要5分钟就能定位到问题的根本原因是 node-123 的 CPU 利用率过高导致其他 Jenkins 代理 Pod 重启进而导致该流水线的 npm install 请求无法及时处理最后超时。解决办法也很简单重启 node-123 或者将该流水线分配到其他空闲的 Jenkins 节点。场景3云成本的异常追踪与预算恢复在 Harness Cloud Cost ManagementCCM的控制台中任何一个超过预算的资源组都有一个“成本异常时间线Cost Anomaly Timeline”页面这个页面基于事件溯源重建了该资源组的成本变化链并且关联了所有对资源组成本产生影响的操作比如“202X-05-1 00:00:00 事件 BUDGET_SET资源组 prod-web 的月度预算设置为 10000 美元”“202X-05-15 08:00:00 事件 RESOURCE_SCALED_UP开发者张三将 prod-web 的 Kubernetes Deployment 副本数从 10 扩容到 50”“202X-05-20 00:00:00 事件 COST_ALERT_TRIGGEREDprod-web 的当前成本8000 美元已达到月度预算的 80%”“202X-05-22 00:00:00 事件 COST_ALERT_TRIGGEREDprod-web 的当前成本9500 美元已达到月度预算的 95%”“202X-05-22 09:00:00 事件 RESOURCE_AUTO_SCALED_DOWNHarness CCM 的自动优化规则触发将 prod-web 的 Kubernetes Deployment 副本数从 50 缩容到 15基于过去 7 天的流量数据15 个副本已经足够处理峰值流量”“202X-05-22 09:01:00 事件 COST_BUDGET_RESTOREDprod-web 的预计月度成本9800 美元已降至月度预算以下”。有了这个“成本异常时间线”页面云成本管理员可以清晰地看到谁、什么时候、通过什么方式导致了成本异常并且可以验证 Harness CCM 的自动优化规则是否有效——如果自动优化规则没有触发还可以手动生成一个“缩容事件”写入事件日志或者修改自动优化规则后生成一个“规则更新事件”写入事件日志。准备工作环境/工具如果你想跟着本文的内容动手实现一个简化版的 Harness 事件溯源系统你需要准备以下开发环境、软件版本和依赖库开发环境操作系统Windows 10/11、macOS Monterey/Ventura/Sonoma、LinuxUbuntu 20.04/22.04、CentOS 7/8、Debian 11/12 等编程语言Python 3.9因为 Python 3.9 支持类型提示、数据类等特性非常适合编写事件溯源系统的原型数据库事件日志存储MongoDB 6.0因为 MongoDB 是文档型数据库非常适合存储不可变的、半结构化的事件数据而且 MongoDB 支持按时间顺序排序、按实体 ID 检索、按事件类型过滤完全满足事件溯源的存储与检索需求当前状态快照存储可选但推荐用于优化重放性能Redis 7.0因为 Redis 是内存型数据库读写速度非常快非常适合存储实体的当前状态快照而且 Redis 支持 TTLTime To Live可以定期清理旧的状态快照消息队列可选但推荐用于结合 CQRS 模式使用Kafka 3.5因为 Kafka 是分布式、高吞吐、持久化的消息队列非常适合作为事件溯源系统的“事件总线Event Bus”而且 Kafka 支持按主题Topic、分区Partition、偏移量Offset存储和检索事件完全满足事件溯源的“按时间顺序、按实体分区”的需求依赖库我们需要安装以下 Python 依赖库pymongo用于连接和操作 MongoDB 数据库redis用于连接和操作 Redis 数据库kafka-python用于连接和操作 Kafka 消息队列dataclasses用于定义不可变的事件类和实体状态类Python 3.7 已经内置不需要额外安装但可以使用dataclasses-json库来简化序列化/反序列化dataclasses-json用于将数据类序列化为 JSON 字符串或者将 JSON 字符串反序列化为数据类python-dotenv用于从.env文件中加载环境变量比如 MongoDB 的连接字符串、Redis 的连接地址、Kafka 的 Bootstrap Servers 等pytest用于编写单元测试和集成测试可选但推荐用于保证事件溯源系统的质量你可以使用pip命令一次性安装所有依赖库pipinstallpymongo redis kafka-python dataclasses-json python-dotenv pytest基础知识在深入学习 Harness 中的事件溯源之前你需要具备以下前置知识1. 什么是事件溯源Event Sourcing事件溯源是一种以“事件”为核心的架构模式由 Martin Fowler 在 2005 年的一篇博客文章《Event Sourcing》中首次提出后来 Greg Young 对事件溯源的落地和推广做出了很大的贡献尤其是结合 CQRS 模式和领域驱动设计DDD使用。事件溯源的核心思想可以用一句话概括“所有对系统状态产生影响的操作都以不可变事件的形式按时间顺序记录下来当前状态由这些事件的重放得到。”2. 什么是不可变事件Immutable Event不可变事件是事件溯源的核心概念之一它具有以下几个关键特征不可修改一旦事件被写入事件日志就永远不能被修改或删除这是事件溯源保证审计合规和数据一致性的关键。按时间顺序事件必须按发生的时间顺序写入事件日志通常使用全局递增的序列号、UUIDv1带时间戳的 UUID、或者数据库的自增 ID 来保证时间顺序。半结构化事件通常包含一些固定的字段比如事件 ID、实体 ID、事件类型、发生时间戳、触发用户/系统、因果关系 ID 等以及一些与事件类型相关的可变字段比如 Feature Flag 变更事件会包含“变更前的状态”“变更后的状态”流水线构建失败事件会包含“错误信息”“失败步骤”等。唯一标识每个事件都有一个全局唯一的 ID用于标识该事件通常使用 UUIDv4 或者 MongoDB 的 ObjectId。3. 什么是事件日志Event Log事件日志是事件溯源系统的唯一真相源Single Source of Truth它是一个按时间顺序存储所有不可变事件的持久化存储系统。事件日志的关键特征包括不可变只能追加Append-Only写入事件不能修改或删除已写入的事件除非是为了合规性要求的“数据擦除Data Erasure”但这通常需要特殊的处理比如用“删除标记事件”代替真正的删除或者使用加密技术将事件的数据加密然后删除加密密钥。高可用因为事件日志是唯一的真相源所以它必须是高可用的通常使用分布式存储系统比如 MongoDB 副本集、Kafka 集群、AWS S3 等。高吞吐因为 Harness 每天要处理数百万到数千万级别的事件所以事件日志必须是高吞吐的通常使用支持批量写入的分布式存储系统比如 Kafka 集群。可检索事件日志必须支持按实体 ID、事件类型、时间范围、触发用户/系统等维度快速检索事件通常使用数据库的索引或者搜索引擎比如 Elasticsearch来实现。4. 什么是状态重放State Replay状态重放是事件溯源系统获取实体当前状态的核心方法它的过程是从事件日志中检索出该实体相关的所有历史事件按时间顺序排序。初始化该实体的空状态Empty State比如 Feature Flag 的空状态是“未创建”流水线的空状态是“不存在”。按时间顺序逐个重放Replay这些历史事件每个事件都会更新实体的状态比如 Feature Flag 创建事件会将状态从“未创建”更新为“已创建状态为 OFF”Feature Flag 变更事件会将状态从“已创建状态为 OFF”更新为“已创建状态为 内部团队 10% 流量”。重放完所有历史事件后得到的状态就是该实体的当前状态。5. 什么是状态快照State Snapshot状态快照是事件溯源系统优化状态重放性能的关键技术它的核心思想是定期或在特定条件下将实体的当前状态保存到一个快照存储系统中这样当需要获取该实体的当前状态时只需要重放快照之后的所有历史事件而不需要重放该实体相关的所有历史事件如果一个实体有 10000 个历史事件快照每 1000 个事件保存一次那么只需要重放最多 1000 个事件就能得到当前状态重放性能提升了 10 倍。状态快照的关键特征包括可选但推荐对于历史事件数量较少的实体比如只有几十个历史事件的 Feature Flag可以不使用状态快照但对于历史事件数量较多的实体比如有几百万个历史事件的流水线执行必须使用状态快照否则重放性能会非常差。定期或条件触发状态快照可以定期触发比如每 1000 个事件保存一次或者每小时保存一次也可以条件触发比如当实体的状态发生重大变化时保存一次或者当用户请求获取该实体的当前状态时保存一次。与事件日志关联每个状态快照都必须关联该快照对应的最后一个事件的 ID这样当需要重放状态时只需要从事件日志中检索出该快照对应的最后一个事件之后的所有历史事件。6. 什么是 CQRS 模式CQRSCommand Query Responsibility Segregation命令查询职责分离是一种将系统的“写操作Command”和“读操作Query”分离的架构模式由 Greg Young 首次提出通常与事件溯源和领域驱动设计DDD结合使用。CQRS 的核心思想可以用一句话概括“写操作和读操作使用不同的模型和存储系统。”写操作Command负责修改系统的状态使用领域模型Domain Model和事件日志作为存储系统写操作的结果是生成一个或多个不可变事件写入事件日志。读操作Query负责查询系统的状态使用查询模型Query Model和查询存储系统Query Store作为存储系统查询模型由事件日志中的事件重放或异步更新得到查询存储系统通常是数据库的索引、搜索引擎比如 Elasticsearch、或者数据仓库比如 Snowflake。CQRS 模式为什么要和事件溯源结合使用因为事件溯源系统的写操作非常高效只能追加写入事件日志不需要修改或删除已有的数据但读操作相对较慢需要重放历史事件才能得到当前状态——而 CQRS 模式正好可以解决这个问题将读操作分离出来使用专门的查询模型和查询存储系统让读操作也变得非常高效。7. 什么是领域驱动设计DDD领域驱动设计Domain-Driven Design简称 DDD是一种以“领域”为核心的软件开发方法由 Eric Evans 在 2003 年的著作《领域驱动设计软件核心复杂性应对之道》中首次提出。DDD 的核心概念包括领域Domain软件系统要解决的业务问题的范围。子域Subdomain将大的领域拆分成多个小的、相对独立的子领域比如 Harness 的领域可以拆分成 CI/CD 子域、Feature Flag 子域、Cloud Cost Management 子域等。限界上下文Bounded Context每个子域对应的软件系统的边界在这个边界内领域模型的概念是统一的、明确的比如 Harness 的 CI/CD 限界上下文内的“流水线”概念和 Feature Flag 限界上下文内的“流水线”概念是不同的。聚合Aggregate限界上下文内的一组相关的领域对象的集合由一个“聚合根Aggregate Root”来统一管理聚合根是聚合内唯一可以被外部直接访问的对象其他对象只能通过聚合根来访问。实体Entity具有唯一标识的领域对象其标识不会随着时间的推移而改变比如 Harness 的 Feature Flag 是一个实体其唯一标识是 Flag ID流水线是一个实体其唯一标识是 Pipeline IDEC2 实例是一个实体其唯一标识是 Instance ID。值对象Value Object没有唯一标识的领域对象其值由其属性决定比如 Harness 的 Feature Flag 规则是一个值对象其值由“目标用户群体”“流量百分比”“生效时间范围”等属性决定如果两个规则的所有属性都相同那么这两个规则就是相等的。领域事件Domain Event限界上下文内发生的、对业务有意义的事情也就是事件溯源中的“不可变事件”比如 Harness 的 Feature Flag 创建事件、Feature Flag 变更事件、流水线启动事件、流水线构建失败事件等。事件溯源和领域驱动设计结合使用时聚合根就是事件溯源中的“实体”领域事件就是事件溯源中的“不可变事件”——这样事件溯源系统就可以完全基于领域模型来构建更加符合业务需求也更加容易维护和扩展。核心概念与问题背景在上一章的准备工作中我们已经简单介绍了事件溯源、不可变事件、事件日志、状态重放、状态快照、CQRS 模式、领域驱动设计等前置知识。在这一章中我们将深入讲解 Harness 中的事件溯源的核心概念并且分析 Harness 为什么选择事件溯源作为核心架构模式之一也就是 Harness 中的事件溯源的问题背景。Harness 中的事件溯源的核心概念Harness 中的事件溯源并不是一个简单的“理论实现”而是一个针对全链路 DevOps 平台的特点进行了大量优化和扩展的工程化实现。因此Harness 中的事件溯源除了包含事件溯源的通用核心概念比如不可变事件、事件日志、状态重放、状态快照等之外还包含一些Harness 特有的核心概念比如多租户事件隔离、跨微服务事件因果关系追踪、事件版本兼容性管理、事件的“命令式重放”与“声明式重放”、事件的“审计重放”与“业务重放”等。在这一节中我们将逐一讲解 Harness 中的事件溯源的所有核心概念包括通用核心概念和 Harness 特有的核心概念。通用核心概念1. 聚合根Aggregate Root在 Harness 中聚合根是事件溯源的基本单位——也就是说事件是按“聚合根 ID”来组织和存储的状态重放也是按“聚合根”来进行的。Harness 中的典型聚合根包括Pipeline流水线唯一标识是pipelineId属于 CI/CD 限界上下文。PipelineExecution流水线执行唯一标识是executionId属于 CI/CD 限界上下文。FeatureFlag功能开关唯一标识是flagId属于 Feature Flag 限界上下文。FeatureFlagEnvironment功能开关环境配置唯一标识是flagEnvironmentId属于 Feature Flag 限界上下文。CloudResource云资源唯一标识是cloudResourceId属于 Cloud Cost Management 限界上下文。CloudBudget云预算唯一标识是budgetId属于 Cloud Cost Management 限界上下文。每个聚合根都有自己的领域模型Domain Model领域模型定义了聚合根的当前状态结构和状态更新规则也就是每个事件如何更新聚合根的状态。2. 领域事件Domain Event在 Harness 中领域事件是对聚合根状态产生影响的、对业务有意义的事情——它是不可变的按时间顺序写入事件日志是事件溯源系统的唯一真相源。Harness 中的典型领域事件包括PipelineCreated流水线创建事件当用户创建一个新的流水线时触发包含pipelineId、pipelineName、createdBy、createdAt等字段。PipelineUpdated流水线更新事件当用户更新一个已有的流水线时触发包含pipelineId、oldPipelineConfig、newPipelineConfig、updatedBy、updatedAt等字段。PipelineExecutionTriggered流水线执行触发事件当用户点击“启动流水线”、或者 Git 提交触发流水线、或者定时任务触发流水线时触发包含executionId、pipelineId、triggerType、triggeredBy、triggeredAt等字段。PipelineExecutionStepStarted流水线执行步骤启动事件当流水线执行的某个步骤启动时触发包含executionId、stepId、stepName、startedAt等字段。PipelineExecutionStepSucceeded流水线执行步骤成功事件当流水线执行的某个步骤成功完成时触发包含executionId、stepId、stepName、succeededAt、stepOutput等字段。PipelineExecutionStepFailed流水线执行步骤失败事件当流水线执行的某个步骤失败时触发包含executionId、stepId、stepName、failedAt、errorMessage、errorStackTrace等字段。FeatureFlagCreated功能开关创建事件当用户创建一个新的功能开关时触发包含flagId、flagName、flagType、createdBy、createdAt等字段。FeatureFlagEnvironmentUpdated功能开关环境配置更新事件当用户更新某个环境下的功能开关配置时触发包含flagEnvironmentId、flagId、environmentId、oldFlagConfig、newFlagConfig、updatedBy、updatedAt等字段。CloudResourceDiscovered云资源发现事件当云成本扫描器发现一个新的云资源时触发包含cloudResourceId、cloudProvider、resourceType、resourceId、discoveredAt等字段。CloudBudgetExceeded云预算超支事件当某个云资源组的成本超过预算时触发包含budgetId、resourceGroupId、currentCost、budgetAmount、exceededAt等字段。每个领域事件都必须包含以下固定字段Harness 称为“事件元数据Event Metadata”eventId事件的全局唯一标识通常使用 UUIDv4 或者 MongoDB 的 ObjectId。aggregateType聚合根的类型比如Pipeline、PipelineExecution、FeatureFlag等。aggregateId聚合根的唯一标识比如pipelineId、executionId、flagId等。eventType事件的类型比如PipelineCreated、PipelineExecutionStepFailed、FeatureFlagEnvironmentUpdated等。version聚合根的版本号每次聚合根的状态发生变化也就是每次写入一个事件版本号都会递增 1版本号是保证聚合根状态一致性的关键我们会在后面的章节中详细讲解。timestamp事件发生的时间戳通常使用 UTC 时间精确到毫秒或者微秒取决于业务需求。tenantId租户的唯一标识Harness 是多租户平台这个字段非常重要我们会在后面的章节中详细讲解。userId触发事件的用户的唯一标识如果是系统触发的事件这个字段可以为空或者设置为一个特殊的“系统用户 ID”。systemId触发事件的系统的唯一标识如果是用户触发的事件这个字段可以为空或者设置为一个特殊的“用户系统 ID”。causationId导致该事件发生的“原因事件”的 ID也就是该事件是由哪个事件触发的我们会在后面的章节中详细讲解“跨微服务事件因果关系追踪”。correlationId关联该事件和其他相关事件的“关联 ID”比如同一个流水线执行的所有事件都有相同的correlationId同一个 Feature Flag 变更的所有事件都有相同的correlationId我们会在后面的章节中详细讲解。每个领域事件还可以包含一些可变字段Harness 称为“事件数据Event Data”这些字段的内容取决于事件的类型比如PipelineExecutionStepFailed事件会包含errorMessage、errorStackTrace等字段FeatureFlagEnvironmentUpdated事件会包含oldFlagConfig、newFlagConfig等字段。3. 事件日志Event Log在 Harness 中事件日志是唯一的真相源——所有的写操作命令都会生成一个或多个领域事件写入事件日志所有的读操作查询、审计合规、问题定位、状态一致性保证都基于这个事件日志。Harness 的事件日志不是一个单一的存储系统而是一个由多个存储系统组成的分层存储架构我们会在后面的章节中详细讲解 Harness 的事件日志的架构设计包括事件持久化层Event Persistence Layer用于永久存储所有的领域事件必须是高可用、高吞吐、不可变的 Append-Only 存储系统Harness 早期使用 MongoDB 副本集作为事件持久化层后来为了支持更高的吞吐和更好的跨微服务事件总线功能迁移到了 Kafka 集群 MongoDB 副本集的组合Kafka 集群作为“事件总线”和“短期事件存储”MongoDB 副本集作为“长期事件存储”和“事件索引层”。事件索引层Event Index Layer用于快速检索事件支持按tenantId、aggregateType、aggregateId、eventType、timestamp、correlationId、causationId等维度快速检索事件Harness 使用 MongoDB 副本集的索引功能作为事件索引层后来为了支持更复杂的全文检索和跨租户/跨聚合根的事件分析引入了 Elasticsearch 作为“事件分析层”。事件分析层Event Analysis Layer用于对事件进行复杂的分析和统计比如“某个租户的某个 Feature Flag 在过去 30 天内的所有变更事件”“某个租户的某个流水线在过去 7 天内的所有失败事件的失败原因统计”等Harness 使用 Elasticsearch 作为事件分析层后来为了支持更大规模的事件分析引入了 Snowflake 作为“事件数据仓库”。4. 状态重放State Replay在 Harness 中状态重放是获取聚合根当前状态的核心方法——也就是说Harness 不会直接存储聚合根的当前状态除非是为了优化重放性能而使用状态快照而是每次需要获取聚合根的当前状态时从事件日志中检索出该聚合根相关的所有历史事件按时间顺序逐个重放这些事件最终得到聚合根的当前状态。Harness 中的状态重放分为两种类型业务重放Business Replay用于获取聚合根的当前业务状态也就是对用户可见的状态重放时会应用所有的“业务规则”比如 Feature Flag 的规则引擎、流水线的执行规则等。审计重放Audit Replay用于获取聚合根的完整历史状态变迁链也就是对审计人员可见的状态重放时不会应用任何“业务规则”而是会保留所有的“原始事件”和“原始状态变化”。Harness 中的状态重放还支持时间旅行Time Travel——也就是说你可以指定一个“目标时间戳”然后重放该目标时间戳之前的所有历史事件最终得到聚合根在该目标时间戳时的状态比如你可以查询“某个 Feature Flag 在 202X-05-20 15:00:00 时的状态”或者“某个流水线在 202X-05-21 09:03:00 时的状态”。5. 状态快照State Snapshot在 Harness 中状态快照是优化状态重放性能的关键技术——因为如果一个聚合根有几百万个历史事件每次都重放所有历史事件的话重放性能会非常差用户体验也会非常糟糕。Harness 中的状态快照分为两种类型定期快照Periodic Snapshot按固定的时间间隔或者固定的事件数量间隔保存状态快照比如 Harness 对PipelineExecution聚合根的定期快照策略是“每 100 个事件保存一次或者每 5 分钟保存一次取两者中先满足的条件”。按需快照On-Demand Snapshot在特定条件下保存状态快照比如当用户第一次查询某个聚合根的当前状态时保存一次或者当聚合根的状态发生重大变化时保存一次或者当聚合根的历史事件数量超过某个阈值时保存一次。Harness 的状态快照存储系统是Redis 集群 MongoDB 副本集的组合Redis 集群用于存储最近的状态快照比如过去 24 小时内的状态快照读写速度非常快可以满足用户的实时查询需求。MongoDB 副本集用于存储所有的历史状态快照持久化保存可以满足用户的时间旅行查询需求和审计需求。每个状态快照都必须包含以下固定字段snapshotId快照的全局唯一标识通常使用 UUIDv4 或者 MongoDB 的 ObjectId。aggregateType聚合根的类型。aggregateId聚合根的唯一标识。version聚合根的版本号也就是该快照对应的最后一个事件的版本号。lastEventId该快照对应的最后一个事件的 ID。lastEventTimestamp该快照对应的最后一个事件的时间戳。snapshotTimestamp快照保存的时间戳。tenantId租户的唯一标识。state聚合根的当前状态通常使用 JSON 格式或者 Protocol Buffers 格式存储。Harness 特有的核心概念1. 多租户事件隔离Multi-Tenant Event IsolationHarness 是一个多租户Multi-Tenant平台——也就是说多个租户不同的公司或组织共享同一个 Harness 平台的基础设施但每个租户的数据是完全隔离的一个租户的用户无法访问另一个租户的数据。多租户事件隔离是 Harness 中的事件溯源的最基本要求之一——因为如果不同租户的事件没有隔离那么一个租户的用户就可以访问另一个租户的事件日志这会导致严重的数据泄露问题也无法满足审计合规要求。Harness 中的多租户事件隔离主要通过以下几种方式实现事件元数据中的tenantId字段每个事件都必须包含tenantId字段用于标识该事件属于哪个租户。事件持久化层的租户隔离如果使用MongoDB 副本集作为事件持久化层可以使用数据库级别的租户隔离每个租户有自己的数据库、或者集合级别的租户隔离每个租户有自己的集合、或者文档级别的租户隔离所有租户的事件都存储在同一个集合中但每个事件都有tenantId字段并且在集合上创建tenantId的索引。Harness 早期使用文档级别的租户隔离后来为了更好的性能和隔离性迁移到了集合级别的租户隔离 文档级别的租户隔离的组合每个租户有自己的“事件集合组”每个聚合根类型有自己的集合每个集合上都创建tenantId、aggregateId、version的复合索引。如果使用Kafka 集群作为事件持久化层可以使用主题级别的租户隔离每个租户有自己的主题、或者分区级别的租户隔离每个租户的事件都存储在同一个主题的特定分区中、或者消息级别的租户隔离所有租户的事件都存储在同一个主题中但每个事件的消息头或消息体中都有tenantId字段。Harness 使用主题级别的租户隔离 消息级别的租户隔离的组合每个租户有自己的“事件主题组”每个聚合根类型有自己的主题每个主题上的每个事件的消息头中都有tenantId字段。事件检索时的租户过滤所有的事件检索操作无论是业务重放、审计重放、还是事件分析都必须首先根据tenantId字段进行过滤确保只检索当前租户的事件。访问控制列表ACL除了事件元数据中的tenantId字段和事件持久化层的租户隔离之外Harness 还使用访问控制列表ACL来进一步限制租户内部的用户对事件的访问比如只有管理员用户才能访问所有的事件普通用户只能访问自己触发的事件或者自己所在团队的事件。2. 跨微服务事件因果关系追踪Cross-Microservice Event Causation TrackingHarness 是一个分布式微服务架构——也就是说Harness 由几十个微服务组成比如 Pipeline Service、Feature Flag Service、Cloud Cost Management Service、Git Service、Notification Service 等不同微服务之间通过 RPCgRPC或消息队列Kafka通信。在分布式微服务架构中一个用户操作可能会触发多个微服务的多个事件——比如用户点击“启动流水线”这个操作会触发Pipeline Service生成一个PipelineExecutionTriggered事件。Pipeline Service调用Git Service拉取代码Git Service 生成一个GitCodePulled事件。Pipeline Service调用Feature Flag Service启动灰度测试Feature Flag Service 生成一个FeatureFlagEnvironmentUpdated事件。Pipeline Service调用Notification Service发送流水线启动通知Notification Service 生成一个NotificationSent事件。在这种情况下跨微服务事件因果关系追踪就变得非常重要——因为如果没有因果关系追踪当某个微服务的某个事件出现问题时比如FeatureFlagEnvironmentUpdated事件失败你根本无法知道这个事件是由哪个微服务的哪个事件触发的也就无法定位问题的根本原因。Harness 中的跨微服务事件因果关系追踪主要通过以下两个事件元数据字段实现correlationId关联 ID用于标识同一个用户操作或同一个业务流程触发的所有事件比如用户点击“启动流水线”这个操作触发的所有事件都有相同的correlationId同一个 Feature Flag 变更的审批、生效、通知等所有事件都有相同的correlationId。correlationId通常由第一个触发事件的微服务生成比如 Pipeline Service 生成PipelineExecutionTriggered事件时会生成一个correlationId然后将这个correlationId传递给 Git Service、Feature Flag Service、Notification Service这些微服务生成的事件都会使用这个correlationId。causationId原因 ID用于标识导致该事件发生的直接原因事件比如GitCodePulled事件的causationId是PipelineExecutionTriggered事件的eventIdFeatureFlagEnvironmentUpdated事件的causationId是PipelineExecutionTriggered事件的eventIdNotificationSent事件的causationId是FeatureFlagEnvironmentUpdated事件的eventId。通过correlationId和causationId这两个字段Harness 可以构建出跨微服务的事件因果关系图Event Causality Graph——这个图可以直观地展示出“同一个用户操作或同一个业务流程触发的所有事件之间的因果关系”当某个事件出现问题时只需要沿着因果关系图往上追溯就能很快定位到问题的根本原因。我们可以使用Mermaid 流程图来展示 Harness 中“用户点击启动流水线”这个操作触发的跨微服务事件因果关系图渲染错误:Mermaid 渲染失败: Parse error on line 5: ...elationIdabc123

相关文章:

Harness 中的事件溯源:以事件日志重建状态

Harness 中的事件溯源:以事件日志重建全链路 DevOps 状态 引言 痛点引入 作为全链路 DevOps 平台,Harness 每天会处理 数百万到数千万级别的用户/系统操作:开发者点击“启动流水线”、Feature Flag 规则引擎执行批量开关切换、云成本扫描器…...

智能车竞赛独轮组信标灯系统全解析:从硬件选型到实战调试技巧

智能车竞赛独轮组信标灯系统全解析:从硬件选型到实战调试技巧 信标灯系统作为智能车竞赛独轮组的核心模块,直接决定了车模的导航精度和比赛成绩。一套稳定高效的信标灯系统需要硬件选型、信号处理、算法优化和实战调试的完美配合。本文将深入剖析信标灯系…...

彻底告别OpenClaw使用焦虑:我给他装上了“透视眼”和“批量克隆模组贾

指令替换 项目需求:将加法指令替换为减法 项目目录如下 /MyProject ├── CMakeLists.txt # CMake 配置文件 ├── build/ #构建目录 │ └── test.c #测试编译代码 └── mypass2.cpp # pass 项目代码 一,测试代码示例 test.c // test.c #includ…...

从ReLU到GELU:非线性投影如何提升注意力机制的效果?

从ReLU到GELU:非线性投影如何重塑注意力机制的边界? 在Transformer架构席卷NLP领域的今天,注意力机制已成为深度学习模型的标准组件。但鲜少有人讨论的是,那些隐藏在QKV投影层后的非线性激活函数,才是真正决定注意力&q…...

04-Java JDK, JRE和JVM

Java JDK, JRE和JVM 在本教程中,您将了解JDK,JRE和JVM。您还将学习它们之间的主要区别。 什么是JVM? JVM(Java虚拟机)是使您的计算机运行Java程序的抽象机。 运行Java程序时,Java编译器首先将Java代码编…...

从零实现富文本编辑器#-React可编辑节点的组件预设渤

1. 智能软件工程的范式转移:从库集成到原生框架演进 在生成式人工智能(Generative AI)从单纯的文本生成向具备自主规划与执行能力的“代理化(Agentic)”系统跨越的过程中,.NET 生态系统正在经历一场自该平…...

Windows 11系统优化终极指南:Win11Debloat一键清理与隐私保护工具

Windows 11系统优化终极指南:Win11Debloat一键清理与隐私保护工具 【免费下载链接】Win11Debloat A simple, lightweight PowerShell script that allows you to remove pre-installed apps, disable telemetry, as well as perform various other changes to declu…...

如何在6小时内将小说变成爆款推文视频?TaleStreamAI完整指南

如何在6小时内将小说变成爆款推文视频?TaleStreamAI完整指南 【免费下载链接】TaleStreamAI AI小说推文全自动工作流,自动从ID到视频 项目地址: https://gitcode.com/gh_mirrors/ta/TaleStreamAI 你是否曾想过,将一本精彩的小说在短短…...

和AI一起搞事情#:边剥龙虾边做个中医技能来起号图

1. 核心概念 在 Antigravity 中,技能系统分为两层: Skills (全局库):实际的代码、脚本和指南,存储在系统级目录(如 ~/.gemini/antigravity/skills)。它们是“能力”的本体。 Workflows (项目级)&#xff1a…...

Mysql的行级锁到底是怎么加的?醒

1. 架构背景与演进动力 1.1 从单体到碎片化:.NET 的开源征程 在.NET Framework 时代,构建系统主要围绕 Windows 操作系统紧密集成,采用传统的封闭式开发模式。然而,随着.NET Core 的推出,微软开启了彻底的开源与跨平台…...

一个简洁易用的 Delphi JSON 封装库,基于 System.JSON`单元封装,提供更直观的 API文

一、前言:什么是 OFA VQA 模型? OFA(One For All)是字节跳动提出的多模态预训练模型,支持视觉问答、图像描述、图像编辑等多种任务,其中视觉问答(VQA)是最常用的功能之一——输入一张…...

TCLB(CUDA Lattice Boltzmann)项目介绍

文章目录TCLB 项目介绍核心特性1. 高性能计算架构2. 核心功能与耦合能力3. 跨平台支持快速使用流程1. 安装与编译2. 运行仿真依赖要求核心依赖可选依赖快速安装依赖学术引用开发与贡献许可证TCLB应用领域1. 流体动力学基础与工程仿真2. 流固耦合(LBM-DEM&#xff09…...

.NET 诊断技巧 | 日志框架原理、手写日志框架学习汕

一、 什么是 AI Skills:从工具级到框架级的演化 AI Skills(AI 技能) 的概念最早在 Claude Code 等前沿 Agent 实践中被强化。最初,Skills 被视为“工具级”的增强,如简单的文件读写或终端操作,方便用户快速…...

OpenMatrix 架构解析:基于 Harness 思想的 AI 任务编排系统

引言:AI 编码的信任危机 AI 编码工具已经非常强大,但用户仍然不敢完全信任。为什么? 第一层:AI 补全代码(Copilot)→ 解决「写」的问题 第二层:AI 对话编程(Claude Code&#xff0…...

C语言入门:秒懂数据类型

刚接触C语言,我们总会遇到int、char、float这些关键词,很多同学觉得麻烦,甚至想只用一种类型写完全部代码。其实数据类型是编程的基础,理解它,才能写出规范、少出错的程序。简单来说,数据类型就是给变量规定…...

本周补题 4/5 -- 4/12

Pta 天梯赛:9 10 11 14 13牛客138:A B C D E F...

C 语言数据类型全解析:从基础到实战

数据类型是 C 语言的基石,也是每个 C 语言初学者必须掌握的核心概念。它决定了变量在内存中占用的空间大小、存储方式以及取值范围。本文将从最基础的概念讲起,系统梳理 C 语言所有数据类型,包含详细的语法、示例代码和实战技巧,帮…...

2026年杭州AI搜索营销新宠横评:5大系统供应商性能实测+选型建议

各位老板,2026年了,还在为传统SEO流量下滑发愁吗?AI搜索的流量入口已经明牌,但市面上的GEO(生成式引擎优化)工具和服务商五花八门,选谁合作才能不踩坑、不掉队?今天,我们…...

Vivado FFT IP核避坑指南:从Matlab数据生成到FPGA验证的完整流程

Vivado FFT IP核实战避坑指南:从Matlab数据生成到FPGA验证的全链路解析 在FPGA信号处理领域,FFT(快速傅里叶变换)是实现频域分析的核心运算单元。Xilinx Vivado提供的FFT IP核虽然功能强大,但在实际工程落地过程中&…...

动态数码管鬼影问题全攻略:从51单片机消影代码到TM1637芯片方案

动态数码管鬼影现象深度解析与工程实践指南 1. 数码管显示原理与鬼影成因 数码管作为嵌入式系统中最常见的显示器件之一,其工作原理直接影响着显示质量。我们先从基础结构说起: 数码管内部构造: 7段LED排列成"8"字形(部…...

基于STM32F407与W5500的HAL库TCP通信实战指南

1. 硬件准备与连接 搞嵌入式开发的朋友都知道,硬件连接是第一步也是最容易出错的地方。我刚开始用STM32F407和W5500时,就因为SPI接线问题折腾了好几天。这里分享下我的经验,帮你少走弯路。 首先说说W5500这个模块,它是一款全硬件T…...

【IIC通信】Chap.2 从“线与”到“时序”:I2C总线协议深度解析与实战信号分析

1. 从线与逻辑看I2C总线冲突的本质 第一次用示波器抓取I2C波形时,我盯着那条"不听话"的SDA线陷入了沉思——为什么总线上某个设备拉低电平后,其他设备输出的高电平就消失了?这个现象背后正是I2C最精妙的"线与"设计。所有…...

【实战】ESP32 + LN298N 驱动编码器推杆:从零搭建行程闭环控制系统

1. 硬件选型与系统架构设计 这个项目最核心的硬件就是ESP32开发板、LN298N电机驱动模块和带编码器的电动推杆。先说ESP32,我强烈推荐使用ESP32-S3系列,相比S2多了几个硬件PWM通道,对于多电机控制特别友好。实测下来,ESP32的硬件PW…...

告别物理JTAG:手把手在KV260 PYNQ上配置XVC远程调试接口(含Vivado Block Design)

告别物理JTAG:KV260 PYNQ环境下的XVC远程调试实战指南 调试Zynq平台PL逻辑时,传统JTAG连接常受限于物理接触和线缆长度。去年在开发一个工业视觉项目时,产线设备与调试台相距30米,来回插拔JTAG不仅效率低下,还导致多次…...

【青少年CTF S1·2026 公益赛】easy_php

<?php // 屏蔽报错&#xff0c;增加一点黑盒难度 error_reporting(0); // TIPS: FLAG在根目录下class Monitor {private $status;private $reporter;public function __construct() {$this->status "normal";$this->reporter new Logger();}public funct…...

Serilog:从结构化日志认知到 .NET 工程落地何

1. 前言 本文详细介绍如何使用 kylin v10 iso 文件构建出 docker image&#xff0c;docker 版本为 20.10.7。 2. 构建 yum 离线源 2.1. 挂载 ISO 文件 mount Kylin-Server-V10-GFB-Release-030-ARM64.iso /media 2.2. 添加离线 repo 文件 在/etc/yum.repos.d/下创建kylin-local…...

Spring Cloud进阶--分布式权限校验OAuth淄

一、核心问题及解决方案&#xff08;按踩坑频率排序&#xff09; 问题 1&#xff1a;误删他人持有锁——最基础也最易犯的漏洞 成因&#xff1a;释放锁时未做身份校验&#xff0c;直接执行 DEL 命令删除键。典型场景&#xff1a;服务 A 持有锁后&#xff0c;业务逻辑耗时超过锁…...

ANARCI抗体序列编号终极指南:从零基础到实战应用的完整教程

ANARCI抗体序列编号终极指南&#xff1a;从零基础到实战应用的完整教程 【免费下载链接】ANARCI Antibody Numbering and Antigen Receptor ClassIfication 项目地址: https://gitcode.com/gh_mirrors/an/ANARCI ANARCI&#xff08;Antibody Numbering and Antigen Rece…...

嵌入式轻量级调试库:零开销DEBUG_PRINT实现原理

1. 项目概述 Utils 是一个面向嵌入式系统的轻量级调试输出工具库&#xff0c;核心目标是为资源受限的MCU平台&#xff08;如ESP32、STM32、nRF52等&#xff09;提供类 printf() 的格式化打印能力&#xff0c;同时严格控制运行时开销与代码体积。该库不依赖标准C库的 stdio.…...

警惕AI患上“讨好症”:来自图灵奖得主Bengio的揭秘

来源&#xff1a;星云创联AI智库在人工智能的万神殿中&#xff0c;“深度学习三巨头”的晚年画像构成了某种充满张力的隐喻。Yann LeCun 活成了永远愤怒的乐观派&#xff0c;在社交媒体上不知疲倦地战斗&#xff0c;坚信AI不过是手中的工具&#xff1b;Geoffrey Hinton 则化身为…...