返回博客

我们的 Ontology 内核:一切操作必须经过本体层

TL;DR

Coomia发布于 2025年6月27日25 分钟阅读
分享本文Twitter / X

我们的 Ontology 内核:一切操作必须经过本体层

系列:S2 架构全景 · 第 4 篇 | 难度:中级 | 阅读时间:18 分钟

TL;DR

  • 平台严格禁止任何服务绕过 Ontology 层直接访问底层数据库。这条技术红线看似限制了开发自由度,却是保证数据一致性、权限安全和审计可追溯的基石。
  • OntologyRuntimeService 是整个平台的"数据宪法"——所有数据读写必须通过它的 gRPC 接口完成。它在每次操作中执行 Schema 验证、权限检查、审计记录和事件发布,形成不可绕过的 4 层防护。
  • "Ontology Tax"(本体税)是指每次数据操作因通过本体层而增加的约 2-5ms 延迟。这个代价换来了统一的数据治理、自动化的血缘追踪、零代码的权限继承和完整的审计日志——投资回报率远超预期。

#1. 引言:一条看似不合理的红线

在 coomia-dip 项目的 CLAUDE.md 文件中,有一条技术红线写得非常清楚:

Code
❌ 绕过 Ontology 直接查库

第一次看到这条规则的开发者,通常会产生这样的疑问:

  • "我只是读一下数据,为什么不能直接查 Doris?"
  • "走 Ontology 层多了一次 gRPC 调用,性能不会变差吗?"
  • "这是不是过度设计?"

这些疑问都很合理。事实上,在项目早期我们内部也有过激烈的讨论。本文将深入解释:为什么我们最终坚持了这条红线,以及 OntologyRuntimeService 如何成为平台的"数据宪法"。

#2. 什么是 Ontology 内核

#2.1 传统架构 vs Ontology 驱动架构

在传统的微服务架构中,每个服务直接管理自己的数据库。这种模式有一个被广泛接受的原则——"每个服务拥有自己的数据"(Database per Service)。

Code
传统微服务架构:

Service A ──── Database A (MySQL)
Service B ──── Database B (PostgreSQL)
Service C ──── Database C (MongoDB)
Service D ──── Database D (Redis)

问题:数据孤岛、缺乏统一语义、跨服务查询困难

coomia-dip 采用了截然不同的方式——Ontology 驱动架构

Code
Ontology 驱动架构:

                    ┌──────────────────────┐
                    │  OntologyRuntimeSvc  │
Service A ──gRPC──> │  (Schema 验证)        │
Service B ──gRPC──> │  (权限检查)           │──> Doris / Iceberg / ...
Service C ──gRPC──> │  (审计记录)           │
Service D ──gRPC──> │  (事件发布)           │
                    └──────────────────────┘

所有数据访问的唯一入口

#2.2 核心概念:本体即模式

在 coomia-dip 中,"本体"(Ontology)不是一个抽象的哲学概念,而是一套严格定义的运行时数据模式。它包含:

概念说明类比
ObjectType对象类型定义(属性、约束、索引)数据库表的 DDL
RelationType关系类型定义(方向、基数、属性)外键 + 关联表
ActionType动作类型定义(输入、输出、副作用)存储过程签名
Property属性定义(类型、验证规则、派生规则)列定义 + CHECK 约束
DerivedProperty派生属性(计算规则、依赖关系)计算列 + 物化视图

关键区别在于:这些定义不是静态的 DDL,而是运行时可查询、可版本化、可继承的一等公民。

#2.3 OntologyRuntimeService 的定位

OntologyRuntimeService 是 Control Layer (Control Layer) 中最核心的服务。它不是一个简单的 CRUD 代理,而是平台的数据宪法执行者

PROTOBUF
service OntologyRuntimeService {
  // 实例生命周期
  rpc CreateObject(CreateObjectRequest) returns (CreateObjectResponse);
  rpc GetObject(GetObjectRequest) returns (GetObjectResponse);
  rpc UpdateObject(UpdateObjectRequest) returns (UpdateObjectResponse);
  rpc DeleteObject(DeleteObjectRequest) returns (DeleteObjectResponse);

  // 批量操作
  rpc BatchGetObjects(BatchGetRequest) returns (BatchGetResponse);
  rpc SearchObjects(SearchObjectsRequest) returns (SearchObjectsResponse);

  // 关系操作
  rpc CreateLink(CreateLinkRequest) returns (CreateLinkResponse);
  rpc GetLinkedObjects(GetLinkedRequest) returns (GetLinkedResponse);

  // 聚合查询
  rpc AggregateObjects(AggregateRequest) returns (AggregateResponse);

  // 派生属性
  rpc GetDerivedProperty(DerivedPropertyRequest) returns (DerivedPropertyResponse);
}

每一个 gRPC 方法背后,都隐含着一系列不可跳过的检查和处理逻辑。

#3. 4 层防护:每次操作发生了什么

当一个服务调用 OntologyRuntimeService.GetObject() 时,请求会经过 4 个层次的处理。这 4 层共同构成了 coomia-dip 的"数据宪法"。

#3.1 第一层:Schema 验证

Code
请求到达 → 从 SchemaRegistry 获取 ObjectType 定义
           → 验证请求的字段是否存在于 Schema 中
           → 验证字段类型是否匹配
           → 验证约束条件(必填、范围、枚举值)
           → 验证是否有不被允许的未知字段

这一层确保不合法的数据永远无法进入系统。与传统方式不同,Schema 验证不依赖数据库约束(如 NOT NULL、CHECK),而是在应用层由本体定义驱动。

好处是:当本体定义变更时(例如新增一个必填属性),所有经过 OntologyRuntimeService 的请求会立即感知到变更,无需修改任何服务代码或执行数据库 migration。

#3.2 第二层:权限检查

Code
Schema 验证通过 → 从请求上下文中提取 WorldContext
                → 确定当前租户/组织/空间/项目/世界
                → 从 PolicyEngine 获取 RBAC + ABAC 规则
                → 逐属性检查读/写权限
                → 行级过滤条件注入

coomia-dip 采用 5 级隔离模型(Tenant → Org → Space → Project → World),权限检查需要在每个层级上进行。直接查库意味着绕过了所有权限检查——这在多租户环境中是不可接受的安全漏洞。

特别重要的是属性级权限控制。例如:

Code
ObjectType: Employee(员工)
  - name: 所有人可读
  - department: 所有人可读
  - salary: 仅 HR 角色可读
  - ssn: 仅 Compliance 角色可读

直接查库 → SELECT * FROM entity_common WHERE type='Employee'
         → 泄露了 salary 和 ssn 字段

通过 Ontology → OntologyRuntimeService 根据调用者角色
              → 自动过滤掉无权访问的属性
              → 返回的数据中不包含 salary 和 ssn

#3.3 第三层:审计记录

Code
权限检查通过 → 记录操作审计日志
             → 谁(principal)在什么时间
             → 对哪个 World 的哪个对象
             → 执行了什么操作
             → 结果是什么
             → 发送到 Kafka audit 主题

coomia-dip 使用 3 个 Kafka 审计主题来分类存储审计日志:

主题用途保留期
audit.data-access数据读取操作90 天
audit.data-mutation数据写入操作365 天
audit.admin-operation管理操作(Schema 变更等)永久

直接查库的操作完全不会出现在审计日志中。在金融、医疗等受监管行业,这意味着合规性的彻底崩塌。

#3.4 第四层:事件发布

Code
审计记录完成 → 发布数据变更事件到 Kafka
             → 触发订阅者通知
             → 触发派生属性级联计算
             → 触发物化视图增量更新
             → 触发搜索索引增量同步

这一层是平台响应式能力的基础。如果一个服务直接修改了 Doris 中的数据,以下功能将全部失效:

  • 派生属性不会重新计算
  • 物化视图不会增量更新
  • 其他服务的订阅不会收到通知
  • 数据血缘链路出现断裂
  • 搜索索引与实际数据不一致

#4. "Ontology Tax":代价与收益分析

#4.1 量化代价

我们称通过 Ontology 层产生的额外开销为"Ontology Tax"(本体税)。让我们精确量化它:

Code
直接查 Doris:
  网络延迟: ~0.5ms
  查询执行: ~1-5ms (取决于数据量)
  总延迟:   ~1.5-5.5ms

通过 OntologyRuntimeService:
  gRPC 调用开销:     ~0.3ms
  Schema 验证:       ~0.2ms (缓存后)
  权限检查:          ~0.5ms (缓存后)
  审计记录 (异步):   ~0.1ms (不阻塞主流程)
  事件发布 (异步):   ~0.1ms (不阻塞主流程)
  Doris 查询:        ~1-5ms
  gRPC 响应序列化:   ~0.2ms
  总延迟:            ~2.4-6.4ms

Ontology Tax ≈ 1-2ms,约增加 15-40% 的延迟。

#4.2 为什么这个代价是值得的

收益直接查库通过 Ontology额外工作量
Schema 一致性手动保证自动保证0 行代码
属性级权限需自行实现内置省去数百行代码
审计日志需自行接入自动省去所有审计中间件
数据血缘不可能自动省去专门的血缘系统
派生属性级联需自行触发自动省去所有触发器逻辑
搜索索引同步需自行同步自动省去同步 pipeline
版本回溯需自行记录Nessie 自动省去版本管理代码

如果每个服务都自行实现这些功能,代码量保守估计增加 3000-5000 行/服务。对于一个有 20+ 微服务的平台,这意味着 60000-100000 行的重复代码——以及 60000-100000 个可能的 bug。

#4.3 性能优化:把 Tax 降到最低

我们通过以下措施将 Ontology Tax 降低到可接受的范围:

Schema 缓存(本地 + Redis 两级缓存)

Code
首次请求:
  SchemaRegistry.GetObjectType() → Redis → PostgreSQL
  缓存时间: 5 分钟 (Redis), 60 秒 (本地内存)
  Schema 变更时通过 Kafka 事件主动失效缓存

后续请求:
  本地内存缓存命中 → ~0.01ms
  命中率: >99.5%

权限规则预编译

Code
PolicyEngine 将 RBAC + ABAC 规则预编译为决策树
首次编译: ~5ms
后续评估: ~0.1ms (二叉树遍历)
规则变更时增量重编译

审计和事件的异步化

Code
审计记录 → Kafka Producer (异步, 不等待 ACK)
事件发布 → Kafka Producer (异步, 批量发送)

对主流程的阻塞: <0.2ms
通过 Kafka 的 acks=1 配置保证至少一次投递

批量操作优化

Code
单次请求 100 个对象:
  直接查库: 100 * 5ms = 500ms (逐条)
            或 5-10ms (WHERE IN 批量)

  Ontology 层: Schema 验证 1 次 (共享 ObjectType)
               权限检查 1 次 (共享 WorldContext)
               Doris 查询 1 次 (WHERE IN)
               审计 1 条 (批量操作记录)
               总延迟: ~8-15ms

  批量场景下 Ontology Tax 被分摊到接近 0

#5. OntologyRuntimeService 的内部架构

#5.1 组件结构

Code
┌─────────────────────────────────────────────────────┐
│              OntologyRuntimeService                  │
│                                                      │
│  ┌──────────┐  ┌──────────┐  ┌──────────────────┐  │
│  │ gRPC     │  │ Schema   │  │ Permission       │  │
│  │ Endpoint │→ │ Resolver │→ │ Evaluator        │  │
│  └──────────┘  └──────────┘  └──────────────────┘  │
│                                      │               │
│                                      ▼               │
│  ┌──────────────────────────────────────────────┐   │
│  │           Query Planner                       │   │
│  │  ┌───────┐ ┌────────┐ ┌──────────────────┐  │   │
│  │  │ Doris │ │Iceberg │ │ DerivedProperty   │  │   │
│  │  │ Query │ │ Query  │ │ Calculator        │  │   │
│  │  └───────┘ └────────┘ └──────────────────┘  │   │
│  └──────────────────────────────────────────────┘   │
│                       │                              │
│                       ▼                              │
│  ┌──────────┐  ┌──────────┐  ┌──────────────────┐  │
│  │ Audit    │  │ Event    │  │ Response          │  │
│  │ Logger   │  │Publisher │  │ Assembler         │  │
│  └──────────┘  └──────────┘  └──────────────────┘  │
│                                                      │
└─────────────────────────────────────────────────────┘

#5.2 Schema Resolver

Schema Resolver 负责在每次请求时解析目标 ObjectType 的完整定义。它的工作不仅仅是"查找 Schema",还包括:

  • 继承解析:如果 ObjectType A 继承自 ObjectType B,解析完整的属性集合
  • 版本匹配:根据 WorldContext 确定使用哪个版本的 Schema(不同 World 可能有不同版本)
  • 约束合并:合并基类和子类的约束条件
  • 索引信息:确定哪些属性有索引,用于查询优化
Code
SchemaResolver.resolve("Equipment", worldContext)
  → 查找本地缓存
  → 未命中 → 查找 Redis 缓存
  → 未命中 → 查询 SchemaRegistry (gRPC)
  → 获取 Equipment ObjectType 定义
  → 检查是否有继承: Equipment extends Asset
  → 递归解析 Asset 定义
  → 合并属性集: Asset.properties + Equipment.properties
  → 合并约束: Asset.constraints + Equipment.constraints
  → 缓存结果
  → 返回完整的 ResolvedSchema

#5.3 Permission Evaluator

Permission Evaluator 是权限检查的核心组件。它与 Policy Engine(策略引擎)协作,执行两种类型的权限检查:

RBAC(基于角色的访问控制)

Code
检查流程:
  1. 从 WorldContext 提取用户身份
  2. 查询用户在当前 Org/Space/Project/World 中的角色
  3. 查询角色对目标 ObjectType 的权限
  4. 权限继承: World → Project → Space → Org → Tenant
  5. 结果: ALLOW / DENY / PARTIAL (属性级过滤)

ABAC(基于属性的访问控制)

Code
检查流程:
  1. 获取 ABAC 策略规则
  2. 评估条件表达式:
     - 用户属性 (department, level, clearance)
     - 资源属性 (classification, owner, region)
     - 环境属性 (time, ip, device)
  3. 生成行级过滤条件注入到查询中

两者的结果会被合并——RBAC 确定用户是否有基本访问权,ABAC 进一步细化为"可以看到哪些行的哪些列"。

#5.4 Query Planner

Query Planner 将经过验证和授权的请求转化为具体的存储查询。它需要处理的场景包括:

Code
场景 1: 简单属性查询
  → 生成 Doris SQL: SELECT col1, col2 FROM entity_common WHERE ...
  → 注入权限过滤条件
  → 执行

场景 2: 包含派生属性
  → 分离普通属性和派生属性
  → 普通属性 → Doris 查询
  → 派生属性 → ComputationCoordinator
  → 合并结果

场景 3: 包含关系遍历
  → 生成 entity_edge 表的 JOIN 查询
  → 或生成多步查询计划 (广度优先遍历)
  → 对每步遍历结果执行权限检查

场景 4: 聚合查询
  → 生成 Doris 聚合 SQL
  → 考虑权限过滤对聚合结果的影响
  → 如果有行级过滤,先过滤再聚合

#5.5 Response Assembler

Response Assembler 的职责是将来自多个数据源的结果合并成统一的 gRPC 响应。它还负责:

  • 属性过滤:根据权限检查结果,移除无权访问的属性
  • 格式转换:将 Doris 的行数据转换为 Protobuf 消息
  • 空值处理:对于没有值的可选属性,区分"没有权限看到"和"确实没有值"
  • 分页处理:封装分页游标(基于 Doris 的 LIMIT/OFFSET 或基于排序键的 keyset pagination)

#6. 一个具体例子:从 API 请求到数据返回

让我们追踪一个具体的请求,看看它如何流经整个 Ontology 内核。

#6.1 场景描述

一个工厂管理应用需要查询所有状态为"运行中"的设备,显示设备名称、位置和当前利用率(利用率是一个派生属性,需要实时计算)。

#6.2 外部 API 入口

Code
REST API (外部):
GET /api/v1/ontology/objects/Equipment?
  filter=status eq 'RUNNING'&
  select=name,location,utilization&
  pageSize=20

→ API Gateway 转换为 gRPC 调用
→ OntologyRuntimeService.SearchObjects()

#6.3 完整处理流程

Code
Step 1: gRPC 请求到达 (t=0ms)
  ├── 解析 SearchObjectsRequest
  ├── 提取 WorldContext (从 gRPC metadata)
  └── 记录请求开始时间

Step 2: Schema 解析 (t=0.1ms)
  ├── SchemaResolver.resolve("Equipment")
  ├── 本地缓存命中 ✓
  ├── 验证: "name" → STRING ✓
  ├── 验证: "location" → STRING ✓
  ├── 验证: "utilization" → DERIVED_DOUBLE ✓ (标记为派生属性)
  ├── 验证: "status" → ENUM[RUNNING,STOPPED,MAINTENANCE] ✓
  └── 验证: filter 表达式语法合法 ✓

Step 3: 权限检查 (t=0.5ms)
  ├── 用户角色: FactoryOperator
  ├── RBAC 检查: FactoryOperator 对 Equipment 有 READ 权限 ✓
  ├── ABAC 检查: 用户 department=Manufacturing
  │   ├── 策略: "Manufacturing 部门只能看到自己工厂的设备"
  │   └── 生成行级过滤: AND factory_id IN ('F001', 'F002')
  ├── 属性级检查:
  │   ├── name → 允许 ✓
  │   ├── location → 允许 ✓
  │   └── utilization → 允许 ✓
  └── 完成

Step 4: 查询规划 (t=0.8ms)
  ├── 分离普通属性: name, location (Doris 查询)
  ├── 分离派生属性: utilization (需要计算)
  ├── 生成 Doris SQL:
  │   SELECT object_id, attributes->>'name' AS name,
  │          attributes->>'location' AS location
  │   FROM entity_common
  │   WHERE type = 'Equipment'
  │     AND attributes->>'status' = 'RUNNING'
  │     AND factory_id IN ('F001', 'F002')  -- ABAC 注入
  │     AND world_id = 'W-2024-001'
  │   ORDER BY object_id
  │   LIMIT 20
  └── 准备 ComputationCoordinator 调用

Step 5: 执行查询 (t=1.0ms → t=4.5ms)
  ├── Doris 查询执行: 返回 20 条记录
  └── 对 20 个 object_id 调用 ComputationCoordinator
      ├── utilization 策略路由: EXPRESSION (简单计算)
      ├── 公式: running_hours / total_hours * 100
      ├── 需要 running_hours 和 total_hours → 查询 Doris
      └── 计算完成: 返回 20 个 utilization 值

Step 6: 结果组装 (t=5.0ms)
  ├── 合并 Doris 结果和派生属性结果
  ├── 属性过滤: 移除无权属性 (本次全部允许)
  ├── 构建 gRPC Response
  └── 设置分页游标

Step 7: 审计 + 事件 (异步, t=5.1ms)
  ├── 发送审计日志到 Kafka audit.data-access
  │   { principal: "user-123", action: "SEARCH",
  │     objectType: "Equipment", resultCount: 20,
  │     worldId: "W-2024-001", timestamp: "..." }
  └── (搜索操作不触发数据变更事件)

Step 8: 响应返回 (t=5.2ms)
  └── 总延迟: ~5.2ms (其中 Ontology Tax ≈ 1.7ms)

#7. 如果绕过 Ontology 会怎样:5 种灾难场景

#7.1 场景一:权限泄露

Code
开发者 A 为了"性能优化",直接查 Doris:

SELECT * FROM entity_common
WHERE type = 'Employee'
AND attributes->>'department' = 'Engineering'

结果: 返回了所有工程部员工的所有属性
      包括 salary、ssn、performance_review
      这些信息本应只有 HR 才能看到

影响: 数据泄露 → 合规违规 → 法律风险

#7.2 场景二:派生属性不一致

Code
开发者 B 直接修改了 Doris 中的库存数量:

UPDATE entity_common
SET attributes = jsonb_set(attributes, '{quantity}', '100')
WHERE object_id = 'INV-001'

结果: quantity 变了,但派生属性 total_value (= quantity * unit_price)
      没有重新计算,仍然显示旧值
      库存报表出现数据不一致

影响: 业务决策基于错误数据 → 财务差异 → 审计问题

#7.3 场景三:审计断裂

Code
开发者 C 直接删除了 Doris 中的敏感记录:

DELETE FROM entity_common
WHERE object_id = 'CASE-007'

结果: 记录消失了,但审计日志中没有任何删除记录
      监管审查时无法解释数据去向
      Nessie 版本历史中也没有对应的变更

影响: 审计不合规 → 监管处罚 → 信誉损失

#7.4 场景四:事件链断裂

Code
开发者 D 直接更新了订单状态:

UPDATE entity_common
SET attributes = jsonb_set(attributes, '{status}', '"SHIPPED"')
WHERE object_id = 'ORDER-123'

结果: 订单状态变了,但:
  - 下游的"发货通知"Action 没有触发
  - 客户没有收到物流提醒
  - 库存的"已发货数量"派生属性没有更新
  - 运营仪表板上的"待发货订单数"没有减少
  - 关联的物流 World 没有同步

影响: 整个业务流程断裂 → 客户投诉 → 运营混乱

#7.5 场景五:多租户数据泄露

Code
开发者 E 写了一个跨租户的统计查询:

SELECT type, COUNT(*) FROM entity_common
GROUP BY type

结果: 返回了所有租户的数据统计
      租户 A 可以推断租户 B 的业务规模
      违反了租户间的数据隔离承诺

影响: 租户信任危机 → 合同违约 → 客户流失

#8. Ontology 内核的设计原则

#8.1 单一入口原则(Single Point of Access)

所有数据操作只有一个入口:OntologyRuntimeService。没有"用于只读的快速通道",没有"用于批量导入的后门"。

这条原则借鉴了数据库事务日志的设计思想——所有变更必须通过 WAL(Write-Ahead Log)才能生效。OntologyRuntimeService 就是 coomia-dip 的"WAL"。

#8.2 零信任原则(Zero Trust)

OntologyRuntimeService 不信任任何调用者。即使是平台内部的服务,每次调用仍然需要:

  • 提供有效的 WorldContext
  • 通过 Schema 验证
  • 通过权限检查
  • 被审计记录

这与网络安全中的"零信任架构"理念一致——永远验证,永远不信任。

#8.3 声明式操作原则

调用者不需要关心数据存储在哪里(Doris?Iceberg?Redis 缓存?),也不需要关心如何执行权限检查。调用者只需要声明:

PROTOBUF
message SearchObjectsRequest {
  string object_type = 1;           // 我要查什么类型
  string filter = 2;                // 筛选条件
  repeated string select_properties = 3; // 我需要哪些属性
  int32 page_size = 4;              // 每页多少条
  string page_token = 5;            // 分页游标
  WorldContext context = 6;         // 在哪个世界
}

OntologyRuntimeService 负责将声明式请求转化为具体的存储操作、权限过滤和结果组装。

#8.4 可观测性原则

通过 Ontology 层的每一次操作都是可观测的:

  • Metrics:请求量、延迟分布、错误率、缓存命中率
  • Traces:分布式追踪(OpenTelemetry),可以精确定位每一层的耗时
  • Logs:结构化审计日志,支持事后查询和分析

直接查库的操作对平台是"黑盒"——你不知道谁在什么时候查了什么数据,性能瓶颈在哪里。

#9. 与 Palantir Foundry 的对比

Palantir Foundry 采用了类似的设计理念,但实现方式有所不同:

维度Palantir Foundrycoomia-dip
数据访问入口Ontology API (REST)OntologyRuntimeService (gRPC)
Schema 管理Ontology Manager UISchemaRegistry (gRPC + UI)
权限模型基于 Marking 的 ABACRBAC + ABAC 混合
审计Audit ServiceKafka 审计主题
版本管理Dataset 级别版本Nessie 分支 + Iceberg 快照
派生属性TypeScript OSDK 计算7 种计算策略路由
多租户Enrollment 级隔离5 级隔离模型

两者的核心共识是相同的:Ontology 不是装饰,它是数据访问的唯一合法路径。

Palantir 在其官方文档中明确指出:"The Ontology is the single source of truth for how your data should be understood, accessed, and acted upon." coomia-dip 继承了这一哲学,并通过 gRPC 实现了更低延迟的内部通信。

#10. 何时 Ontology Tax 不可接受:例外与对策

#10.1 批量数据导入

当需要导入数百万条数据时,逐条通过 OntologyRuntimeService 效率太低。对策:

Code
解决方案: Batch Import Pipeline

1. 数据先写入 Staging Area (MinIO 上的 Parquet 文件)
2. SchemaValidator 批量验证整个文件
3. PermissionChecker 一次性检查导入权限
4. BulkLoader 直接写入 Doris (绕过逐条 gRPC)
5. AuditLogger 记录一条批量导入审计
6. EventPublisher 发布批量变更事件

关键: 不是"绕过 Ontology",而是"Ontology 的批量模式"
     Schema 验证和权限检查仍然执行
     只是从逐条改为批量

#10.2 数据分析查询

数据分析师需要执行复杂的 SQL 查询来探索数据。对策:

Code
解决方案: Ontology-Aware SQL Gateway

1. 分析师编写 SQL 查询
2. SQL Gateway 解析 SQL
3. 注入权限过滤条件 (基于用户的 RBAC/ABAC)
4. 转发到 Doris 执行
5. 记录审计日志
6. 返回结果

关键: 允许 SQL 的灵活性
     但权限和审计仍然由 Ontology 层保证

#10.3 实时流处理

当 Kafka 消费者需要实时处理事件流时,每条事件都通过 gRPC 会成为瓶颈。对策:

Code
解决方案: Embedded Ontology Validator

1. 流处理器启动时预加载相关的 ObjectType Schema
2. 在进程内执行 Schema 验证 (避免网络调用)
3. 权限检查通过预计算的规则缓存执行
4. 审计日志批量异步发送
5. Schema 变更通过 Kafka 事件实时同步到流处理器

关键: 将 Ontology 逻辑嵌入到流处理器中
     消除网络调用开销
     但 Ontology 的语义保证不变

#11. 实施路径:如何在团队中推行 Ontology 内核

#11.1 代码层面

Python
# ❌ 禁止: 直接使用 Doris 客户端
from doris_client import DorisConnection
conn = DorisConnection("doris:9030")
result = conn.query("SELECT * FROM entity_common WHERE type='Equipment'")

# ✅ 正确: 通过 SDK 访问 Ontology
from ontology_sdk import OntologyClient
client = OntologyClient(world_context=ctx)
result = client.search("Equipment", filter="status eq 'RUNNING'")

#11.2 架构层面

Code
所有服务的依赖图:

Service A ──> ontology-sdk ──> OntologyRuntimeService ──> Doris
Service B ──> ontology-sdk ──> OntologyRuntimeService ──> Doris
Service C ──> ontology-sdk ──> OntologyRuntimeService ──> Doris

没有任何服务直接依赖 Doris 客户端库

#11.3 CI/CD 层面

YAML
# .gitlab-ci.yml 中的依赖审计
dependency-audit:
  script:
    - |
      # 检查是否有服务直接依赖数据库客户端
      if grep -r "doris_client\|mysql.connector\|psycopg2" src/; then
        echo "ERROR: Direct database access detected!"
        echo "Use ontology-sdk instead."
        exit 1
      fi

#11.4 代码审查清单

每次 Code Review 时,检查以下项目:

  • 没有直接的数据库连接或 SQL 查询
  • 所有数据操作通过 ontology-sdkOntologyRuntimeService
  • 提供了有效的 WorldContext
  • 没有在代码中硬编码 ObjectType 名称(使用常量或配置)
  • 批量操作使用了 BatchGetObjects 而非循环调用 GetObject

#Key Takeaways

  1. "Ontology 内核"不是过度设计,而是必要的架构约束。 通过将所有数据访问统一到 OntologyRuntimeService,我们用 1-2ms 的延迟换来了自动化的 Schema 验证、权限检查、审计记录和事件发布。这 4 层防护消除了数十万行的重复代码和无数的安全漏洞。

  2. "Ontology Tax"是可以优化到接近零的。 通过两级缓存、规则预编译、异步审计和批量操作,单次请求的额外延迟仅 1-2ms。在批量场景下,分摊后的 Tax 接近 0。关键是优化手段足够多,而架构的收益是恒定的。

  3. 绕过 Ontology 的代价远大于 Ontology Tax。 权限泄露、数据不一致、审计断裂、事件链断裂、多租户数据泄露——任何一个问题造成的损失,都远超那 1-2ms 的延迟。这就是为什么"禁止直接查库"是技术红线而不是建议。

下一篇预告: [S2-05] 多租户架构:从租户到世界的 5 级隔离模型——深入理解 Tenant → Org → Space → Project → World 的 5 级隔离如何在共享基础设施上实现数据和计算的严格隔离。

Tags: #ontology #ontology-kernel #data-governance #access-control #audit #schema-validation #grpc #coomia-dip #智策平台