我们的 Ontology 内核:一切操作必须经过本体层
TL;DR
我们的 Ontology 内核:一切操作必须经过本体层
“系列:S2 架构全景 · 第 4 篇 | 难度:中级 | 阅读时间:18 分钟
TL;DR
- 平台严格禁止任何服务绕过 Ontology 层直接访问底层数据库。这条技术红线看似限制了开发自由度,却是保证数据一致性、权限安全和审计可追溯的基石。
- OntologyRuntimeService 是整个平台的"数据宪法"——所有数据读写必须通过它的 gRPC 接口完成。它在每次操作中执行 Schema 验证、权限检查、审计记录和事件发布,形成不可绕过的 4 层防护。
- "Ontology Tax"(本体税)是指每次数据操作因通过本体层而增加的约 2-5ms 延迟。这个代价换来了统一的数据治理、自动化的血缘追踪、零代码的权限继承和完整的审计日志——投资回报率远超预期。
#1. 引言:一条看似不合理的红线
在 coomia-dip 项目的 CLAUDE.md 文件中,有一条技术红线写得非常清楚:
❌ 绕过 Ontology 直接查库
第一次看到这条规则的开发者,通常会产生这样的疑问:
- "我只是读一下数据,为什么不能直接查 Doris?"
- "走 Ontology 层多了一次 gRPC 调用,性能不会变差吗?"
- "这是不是过度设计?"
这些疑问都很合理。事实上,在项目早期我们内部也有过激烈的讨论。本文将深入解释:为什么我们最终坚持了这条红线,以及 OntologyRuntimeService 如何成为平台的"数据宪法"。
#2. 什么是 Ontology 内核
#2.1 传统架构 vs Ontology 驱动架构
在传统的微服务架构中,每个服务直接管理自己的数据库。这种模式有一个被广泛接受的原则——"每个服务拥有自己的数据"(Database per Service)。
传统微服务架构:
Service A ──── Database A (MySQL)
Service B ──── Database B (PostgreSQL)
Service C ──── Database C (MongoDB)
Service D ──── Database D (Redis)
问题:数据孤岛、缺乏统一语义、跨服务查询困难
coomia-dip 采用了截然不同的方式——Ontology 驱动架构:
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 代理,而是平台的数据宪法执行者。
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 验证
请求到达 → 从 SchemaRegistry 获取 ObjectType 定义
→ 验证请求的字段是否存在于 Schema 中
→ 验证字段类型是否匹配
→ 验证约束条件(必填、范围、枚举值)
→ 验证是否有不被允许的未知字段
这一层确保不合法的数据永远无法进入系统。与传统方式不同,Schema 验证不依赖数据库约束(如 NOT NULL、CHECK),而是在应用层由本体定义驱动。
好处是:当本体定义变更时(例如新增一个必填属性),所有经过 OntologyRuntimeService 的请求会立即感知到变更,无需修改任何服务代码或执行数据库 migration。
#3.2 第二层:权限检查
Schema 验证通过 → 从请求上下文中提取 WorldContext
→ 确定当前租户/组织/空间/项目/世界
→ 从 PolicyEngine 获取 RBAC + ABAC 规则
→ 逐属性检查读/写权限
→ 行级过滤条件注入
coomia-dip 采用 5 级隔离模型(Tenant → Org → Space → Project → World),权限检查需要在每个层级上进行。直接查库意味着绕过了所有权限检查——这在多租户环境中是不可接受的安全漏洞。
特别重要的是属性级权限控制。例如:
ObjectType: Employee(员工)
- name: 所有人可读
- department: 所有人可读
- salary: 仅 HR 角色可读
- ssn: 仅 Compliance 角色可读
直接查库 → SELECT * FROM entity_common WHERE type='Employee'
→ 泄露了 salary 和 ssn 字段
通过 Ontology → OntologyRuntimeService 根据调用者角色
→ 自动过滤掉无权访问的属性
→ 返回的数据中不包含 salary 和 ssn
#3.3 第三层:审计记录
权限检查通过 → 记录操作审计日志
→ 谁(principal)在什么时间
→ 对哪个 World 的哪个对象
→ 执行了什么操作
→ 结果是什么
→ 发送到 Kafka audit 主题
coomia-dip 使用 3 个 Kafka 审计主题来分类存储审计日志:
| 主题 | 用途 | 保留期 |
|---|---|---|
audit.data-access | 数据读取操作 | 90 天 |
audit.data-mutation | 数据写入操作 | 365 天 |
audit.admin-operation | 管理操作(Schema 变更等) | 永久 |
直接查库的操作完全不会出现在审计日志中。在金融、医疗等受监管行业,这意味着合规性的彻底崩塌。
#3.4 第四层:事件发布
审计记录完成 → 发布数据变更事件到 Kafka
→ 触发订阅者通知
→ 触发派生属性级联计算
→ 触发物化视图增量更新
→ 触发搜索索引增量同步
这一层是平台响应式能力的基础。如果一个服务直接修改了 Doris 中的数据,以下功能将全部失效:
- 派生属性不会重新计算
- 物化视图不会增量更新
- 其他服务的订阅不会收到通知
- 数据血缘链路出现断裂
- 搜索索引与实际数据不一致
#4. "Ontology Tax":代价与收益分析
#4.1 量化代价
我们称通过 Ontology 层产生的额外开销为"Ontology Tax"(本体税)。让我们精确量化它:
直接查 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 两级缓存)
首次请求:
SchemaRegistry.GetObjectType() → Redis → PostgreSQL
缓存时间: 5 分钟 (Redis), 60 秒 (本地内存)
Schema 变更时通过 Kafka 事件主动失效缓存
后续请求:
本地内存缓存命中 → ~0.01ms
命中率: >99.5%
权限规则预编译
PolicyEngine 将 RBAC + ABAC 规则预编译为决策树
首次编译: ~5ms
后续评估: ~0.1ms (二叉树遍历)
规则变更时增量重编译
审计和事件的异步化
审计记录 → Kafka Producer (异步, 不等待 ACK)
事件发布 → Kafka Producer (异步, 批量发送)
对主流程的阻塞: <0.2ms
通过 Kafka 的 acks=1 配置保证至少一次投递
批量操作优化
单次请求 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 组件结构
┌─────────────────────────────────────────────────────┐
│ 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 可能有不同版本)
- 约束合并:合并基类和子类的约束条件
- 索引信息:确定哪些属性有索引,用于查询优化
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(基于角色的访问控制)
检查流程:
1. 从 WorldContext 提取用户身份
2. 查询用户在当前 Org/Space/Project/World 中的角色
3. 查询角色对目标 ObjectType 的权限
4. 权限继承: World → Project → Space → Org → Tenant
5. 结果: ALLOW / DENY / PARTIAL (属性级过滤)
ABAC(基于属性的访问控制)
检查流程:
1. 获取 ABAC 策略规则
2. 评估条件表达式:
- 用户属性 (department, level, clearance)
- 资源属性 (classification, owner, region)
- 环境属性 (time, ip, device)
3. 生成行级过滤条件注入到查询中
两者的结果会被合并——RBAC 确定用户是否有基本访问权,ABAC 进一步细化为"可以看到哪些行的哪些列"。
#5.4 Query Planner
Query Planner 将经过验证和授权的请求转化为具体的存储查询。它需要处理的场景包括:
场景 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 入口
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 完整处理流程
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 场景一:权限泄露
开发者 A 为了"性能优化",直接查 Doris:
SELECT * FROM entity_common
WHERE type = 'Employee'
AND attributes->>'department' = 'Engineering'
结果: 返回了所有工程部员工的所有属性
包括 salary、ssn、performance_review
这些信息本应只有 HR 才能看到
影响: 数据泄露 → 合规违规 → 法律风险
#7.2 场景二:派生属性不一致
开发者 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 场景三:审计断裂
开发者 C 直接删除了 Doris 中的敏感记录:
DELETE FROM entity_common
WHERE object_id = 'CASE-007'
结果: 记录消失了,但审计日志中没有任何删除记录
监管审查时无法解释数据去向
Nessie 版本历史中也没有对应的变更
影响: 审计不合规 → 监管处罚 → 信誉损失
#7.4 场景四:事件链断裂
开发者 D 直接更新了订单状态:
UPDATE entity_common
SET attributes = jsonb_set(attributes, '{status}', '"SHIPPED"')
WHERE object_id = 'ORDER-123'
结果: 订单状态变了,但:
- 下游的"发货通知"Action 没有触发
- 客户没有收到物流提醒
- 库存的"已发货数量"派生属性没有更新
- 运营仪表板上的"待发货订单数"没有减少
- 关联的物流 World 没有同步
影响: 整个业务流程断裂 → 客户投诉 → 运营混乱
#7.5 场景五:多租户数据泄露
开发者 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 缓存?),也不需要关心如何执行权限检查。调用者只需要声明:
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 Foundry | coomia-dip |
|---|---|---|
| 数据访问入口 | Ontology API (REST) | OntologyRuntimeService (gRPC) |
| Schema 管理 | Ontology Manager UI | SchemaRegistry (gRPC + UI) |
| 权限模型 | 基于 Marking 的 ABAC | RBAC + ABAC 混合 |
| 审计 | Audit Service | Kafka 审计主题 |
| 版本管理 | 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 效率太低。对策:
解决方案: 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 查询来探索数据。对策:
解决方案: Ontology-Aware SQL Gateway
1. 分析师编写 SQL 查询
2. SQL Gateway 解析 SQL
3. 注入权限过滤条件 (基于用户的 RBAC/ABAC)
4. 转发到 Doris 执行
5. 记录审计日志
6. 返回结果
关键: 允许 SQL 的灵活性
但权限和审计仍然由 Ontology 层保证
#10.3 实时流处理
当 Kafka 消费者需要实时处理事件流时,每条事件都通过 gRPC 会成为瓶颈。对策:
解决方案: Embedded Ontology Validator
1. 流处理器启动时预加载相关的 ObjectType Schema
2. 在进程内执行 Schema 验证 (避免网络调用)
3. 权限检查通过预计算的规则缓存执行
4. 审计日志批量异步发送
5. Schema 变更通过 Kafka 事件实时同步到流处理器
关键: 将 Ontology 逻辑嵌入到流处理器中
消除网络调用开销
但 Ontology 的语义保证不变
#11. 实施路径:如何在团队中推行 Ontology 内核
#11.1 代码层面
# ❌ 禁止: 直接使用 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 架构层面
所有服务的依赖图:
Service A ──> ontology-sdk ──> OntologyRuntimeService ──> Doris
Service B ──> ontology-sdk ──> OntologyRuntimeService ──> Doris
Service C ──> ontology-sdk ──> OntologyRuntimeService ──> Doris
没有任何服务直接依赖 Doris 客户端库
#11.3 CI/CD 层面
# .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-sdk或OntologyRuntimeService - 提供了有效的 WorldContext
- 没有在代码中硬编码 ObjectType 名称(使用常量或配置)
- 批量操作使用了
BatchGetObjects而非循环调用GetObject
#Key Takeaways
-
"Ontology 内核"不是过度设计,而是必要的架构约束。 通过将所有数据访问统一到 OntologyRuntimeService,我们用 1-2ms 的延迟换来了自动化的 Schema 验证、权限检查、审计记录和事件发布。这 4 层防护消除了数十万行的重复代码和无数的安全漏洞。
-
"Ontology Tax"是可以优化到接近零的。 通过两级缓存、规则预编译、异步审计和批量操作,单次请求的额外延迟仅 1-2ms。在批量场景下,分摊后的 Tax 接近 0。关键是优化手段足够多,而架构的收益是恒定的。
-
绕过 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 #智策平台