Oracle 23ai 发布之后,我前后画过三版架构图,前两版都被自己推翻了。第一版几乎是照着官方文档的示意图改了个标题,画完自己都不好意思拿出来——那玩意儿贴到工位上,排障的时候一点用没有,因为图里没有我实际环境中的容器分布,也没有那块新多出来的向量内存。第二版倒是把向量相关的东西堆上去了,结果线条乱得像蜘蛛网,PDB 级的参数和 CDB 级的参数混在一层,越看越糊涂。到第三版,我把"画图"这件事拆成了两个独立视图:一张回答"实例里的东西摆在哪",一张回答"一次向量检索的数据怎么流"。这两张图现在成了我们团队新人入职第一周的必读材料,也是我自己半夜被叫起来排查慢查询时第一眼要看的东西。下面就把这两张图从动机、结构、组件取舍到画完之后的验证过程,完整地摊开讲一遍,包括我自己在画的过程中被纠正的四个认知偏差。
1. 这两张图不是文档插图,是排障时随手能翻的地图
先说清楚我为什么非要自己画。官方文档里的架构图信息量是够的,但它是"通用视图"——为了覆盖所有部署形态,它必须画得抽象。抽象到你在真实环境里看到一条慢 SQL,图上找不到任何一个可以直接点击去查的落点。比如文档图里 SGA 就是一个大框,里面分了 Buffer Cache、Shared Pool、Large Pool 几块,你在图上根本看不出"我这个 PDB 到底分到了多少向量内存",因为那玩意儿在 23ai 之前压根不存在。
架构图对我而言有三个具体用途,按重要性排序:第一是排障时的索引,看到某个现象能立刻定位到"这属于哪一层的问题",缩小排查范围;第二是给团队做知识传递,一张画得准的图能省掉大量口头解释;第三是自我检查,画的过程会强迫你把每个组件之间的依赖关系过一遍,很多想当然的地方在落笔时就会露馅。第三个用途是意外收获,也是我后来坚持每上一个新版本就重画一版的原因。
这两张图的定位也不一样。我把第一张叫 A 图,运行时视图,静态的,回答"东西在哪、占多大、谁管着谁";第二张叫 B 图,数据流视图,动态的,回答"一行文本从进来到变成 Top-K 结果,中间经过哪些环节、每个环节的输入输出是什么"。两张图放一起看,A 图是地形,B 图是路线。只看地形不知道路怎么走,只看路线不知道哪里有坑。
提示:如果你也想画自己环境的架构图,先别急着开工具。拿一张 A4 纸,用铅笔把"你最怕出问题的那条链路"画出来,画不下去的地方就是你需要补知识的地方。这一步比后面调颜色、调对齐重要得多。
还有一个现实因素:23ai 引入的东西太多太散。向量检索、JSON 关系二元性、库内 JavaScript、True Cache、SQL Domain、Boolean 类型、优先级事务,这些东西在文档里是一章一章分开讲的,但实际上它们在运行时是叠在同一个实例上的。你如果脑子里没有一个统一的视图,很容易出现"A 特性开了之后 B 特性的表现变了"这种连锁困惑。我画这两张图,本质上是在给自己建一个能装下所有新特性的容器。
2. A 图:把向量内存池塞进 SGA 之后,实例视图变了哪里
2.1 先画存储与容器层,再回头画内存
我第一版是从 SGA 开始画的,画到一半卡住了,因为我不知道向量内存到底该挂在哪个节点下。后来把顺序反过来:先从最底下的数据文件画起,往上依次是表空间、数据文件所属的 PDB、PDB 所在的 CDB、CDB 对应的实例、实例的内存与进程。这个自下而上的顺序看着反直觉,但它有一个好处——每一层都是上一层"物理上真实存在的东西",你不会画出一个悬空的概念框。
画到容器层的时候,我特意把 CDB$ROOT 和各个 PDB 用了不同的底色区分,并且在 PDB 框的右上角标了这个 PDB 的业务归属和大致数据量。这个细节在正式文档图里肯定不会有,但它对我极其有用——半夜看监控告警说某个 PDB 的临时表空间涨了,我一眼就能知道这是哪个业务线在跑什么批处理。
存储层我画得比较粗,只标了数据文件、在线重做日志、归档路径和控制文件四类,另外单独拉了一条线指向备份介质。粗是有意的,因为 A 图的目的是建立整体位置感,不是做存储规划。真要算容量,我有另外一套表格,不在这张图上。
2.2 SGA 新成员:向量内存池在图上的位置与边界
23ai 里 SGA 的结构变化,核心就是多了一块给向量索引和向量运算用的内存区域,由VECTOR_MEMORY_SIZE这个参数控制大小。我在 A 图上把它画成和 Buffer Cache、Shared Pool 并列的一块独立区域,而不是塞在 Shared Pool 里面。为什么这么画?因为它的生命周期和管理方式跟 Shared Pool 完全不同——Shared Pool 里放的是游标、执行计划这些可以随时淘汰的东西,而向量池里放的是索引结构,淘汰重建的代价非常高。
这里有个细节值得单独说:向量池的大小是可以按 PDB 分配的。也就是说在 CDB 层面设一个总盘子,然后各个 PDB 各自申领一块。我在图上用两种线型表达了这件事——实线表示 CDB 级的参数设置,虚线表示 PDB 级的覆盖。这个表达方式是我在图被自己看糊涂两次之后才定下来的,前面说了,第二版就是因为把这两级混在一层画,导致我排查时误判了参数生效位置。
参数的实际值我建议你直接查,不要记:
SHOW PARAMETER vector_memory_size SELECT con_id, name, value, ispdb_modifiable FROM v$parameter WHERE name = 'vector_memory_size';ispdb_modifiable这一列很关键,它告诉你这个参数能不能在 PDB 级别单独设。不同小版本上这个值可能会有变化,所以别背结论,现场查。
2.3 后台进程那一栏我没有照抄官方图
官方图里后台进程画得挺全,DBWn、LGWR、CKPT、SMON、PMON、ARCn 一应俱全。我在自己的图上做了删减和增补:删掉了几个平时根本不会去关注的,增补了几个跟 23ai 新特性相关但我一开始完全没意识到的。
其中一个是被我漏掉又补上的:向量索引的构建过程会消耗大量内存和临时空间,这个构建动作不是一个独立进程完成的,它是在服务进程里跑的,但它对内存池的申请行为跟普通查询完全不一样。我在图上加了一条从"会话服务进程"到"向量内存池"的粗箭头,旁边标注了"索引构建期峰值占用",用来提醒自己:建索引的时候别在同一实例上跑大查询。
另一个是参数文件的位置。我把 SPFILE 单独画了一个小框,用虚线连到实例,旁边写了一行备注"PDB 级参数修改需要落到 CDB 的 SPFILE"。这行备注是血泪——有一次在一个测试 PDB 里改完参数,重启之后发现没生效,就是因为没搞清楚参数最终落在哪。
2.4 CDB 与 PDB 的参数继承,用两种线型表达
这一小节单拎出来讲,因为它是我认为 A 图里最容易画错、也最容易在排查时误导人的地方。
Oracle 多租户的参数机制是:一部分参数只能在 CDB 根上设,一部分可以在 PDB 上覆盖,还有一部分是 PDB 设了也不生效的。这三类如果只用一种线画,图就废了。我的做法是三条规矩:
| 参数类型 | 图上表达 | 典型例子 |
|---|---|---|
| 仅 CDB 级可设 | 红色实线连到 CDB 框 | 与实例整体资源相关的参数 |
| PDB 可覆盖 | 蓝色虚线连到对应 PDB 框 | VECTOR_MEMORY_SIZE、各类 PDB 级限制 |
| PDB 设置不生效 | 灰色点线,旁边打叉 | 部分与 SGA 结构强绑定的参数 |
这张表我直接贴在图的右下角,作为图例的一部分。这样任何一个人看图的时候,不需要再问我"这条虚线什么意思"。
顺便说一个实操经验:改 PDB 级参数的时候,我习惯先用ALTER SESSION SET CONTAINER = xxx切进去,改完之后立刻在当前会话里SHOW PARAMETER验证一次,然后再去 CDB 根上看一眼。两次都要看,只看一次容易产生"已经生效了"的错觉,实际上可能只是会话级别的临时值。
3. B 图:一行文本走到 Top-K 的完整链路
3.1 嵌入在库外生成还是库内生成,图上要标清楚
B 图从左边开始画,起点是一个"文档进来源"。这里第一个分叉点就是:向量(embedding)在哪生成。有两条路,一条是在应用侧用外部模型算好了再写进数据库,另一条是把模型加载进数据库,用库内函数直接在 SQL 里算。
我在图上把这两条路都画了,用不同颜色区分,并且标注了各自的代价。库外生成的好处是灵活,模型想换就换,不占数据库资源;坏处是多一次数据搬运,而且写入时要注意向量维度和数据类型必须与列定义严格对齐,对不齐会直接报错。库内生成的好处是链路短,插入的同时就把向量算好了,适合那种"数据进来马上就要能检索"的场景;坏处是模型加载和推理会吃数据库的 CPU 和内存,得提前评估。
-- 库内生成向量的大致形状 INSERT INTO doc_chunks (content, emb) VALUES (:txt, VECTOR_EMBEDDING(my_embed_model USING :txt AS DATA));这段 SQL 里my_embed_model是提前注册好的模型名,不同版本的注册方式略有差异,具体用法以你手上版本的文档为准。我之所以在图上把库内这条路也画出来,是因为它直接决定了 A 图里向量池的压力来源——如果你全部走库内生成,那向量池和 CPU 的规划就完全是另一个量级。
3.2 VECTOR 列的维度与精度,是图上最容易被省略的关键信息
很多架构图把表就画成一个矩形写个表名完事,我在这张图上特意把关键的向量列单独标了出来,写清楚三件事:维度、元素类型、距离度量。
这三件事为什么重要?维度决定了单个向量占多少字节,进而决定了索引的内存占用;元素类型(FLOAT32、FLOAT64、INT8之类)同样影响存储和计算精度;距离度量(余弦、欧氏、点积等)必须和你的业务语义匹配,最重要的是——查询时用的距离函数必须和建索引时声明的一致,不一致会导致索引失效,退化成全表扫描。
CREATE TABLE doc_chunks ( chunk_id NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, tenant_id NUMBER NOT NULL, content CLOB, emb VECTOR(768, FLOAT32) );我在图的这个节点旁边算了一笔账:768 维、FLOAT32,单个向量固定占 3KB 左右,一百万行就是 3GB 的裸数据,加上索引结构通常会翻倍甚至更多。这笔账不算清楚,VECTOR_MEMORY_SIZE设多少就完全是拍脑袋。
提示:维度一定要在项目初期定死。中途换模型导致维度变化,意味着整表的向量列要重建、所有索引要重建。我见过因为这事儿返工两周的案例,代价极高。
3.3 HNSW 与 IVF 在图上长得不一样:内存结构与构建期
23ai 支持不止一种向量索引结构,最常见的是基于图的 HNSW 和基于分区的 IVF。这两种在 B 图上我用了完全不同的画法,因为它们的运行时行为差别很大。
HNSW 我画成一张网状的图结构,节点之间有多层连线,旁边标注"全内存、构建期内存峰值高、查询快、插入有代价"。IVF 我画成一堆被切成扇区的圆盘,旁边标注"先聚类再分桶、构建期需要采样、内存占用相对可控、查询时要扫多个桶"。
-- 图结构索引的大致写法 CREATE VECTOR INDEX idx_chunk_emb ON doc_chunks (emb) ORGANIZATION INMEMORY NEIGHBOR GRAPH DISTANCE COSINE WITH TARGET ACCURACY 90; -- 分区式索引的大致写法 CREATE VECTOR INDEX idx_chunk_emb_ivf ON doc_chunks (emb) ORGANIZATION NEIGHBOR PARTITIONS DISTANCE COSINE WITH TARGET ACCURACY 90;WITH TARGET ACCURACY这个子句是 23ai 向量索引里我觉得最值得在图上单独标出来的东西。它表达的是一个权衡:目标精度越高,索引需要维护的结构越复杂、内存越大、构建越慢。图结构索引在精度上的表现通常更好,但构建期的内存峰值也更高——这就是为什么我在 A 图上专门画了那条"构建期峰值占用"的箭头。
3.4 查询侧的过滤条件与混合检索怎么画
B 图的右半边是查询侧。这里的核心问题是:向量相似度检索经常要和普通的结构化过滤条件组合使用,比如"在这个租户下、这个时间段内、类型是合同的文档里,找最相似的十条"。这种组合查询的执行路径,跟纯向量检索完全不同,图上必须画出来。
我画的做法是把查询侧拆成三个并行的输入:向量输入、标量过滤条件、返回条数,然后汇入一个"执行计划选择"节点,最后分两条路径出去——一条走向量索引再回表过滤,一条走标量索引先缩小范围再做向量比较。哪条路径被选中,取决于数据分布和优化器的判断。这个分叉在图上是用菱形表达的。
实践中最容易出问题的是过滤条件的选择性。如果过滤之后剩下的行数很少,优化器很可能干脆放弃向量索引,直接全表扫一遍再排序——在小数据量下这反而更快,但数据量一大就崩了。我在图上这个菱形旁边写了一行备注:"关注执行计划里是否真的走了向量索引"。
4. 画图这半天的工具选择和图层规矩
4.1 我为什么放弃自动生成、回到手动矢量绘图
一开始我想偷懒,试着用各种自动生成工具从配置文件或者字典视图里直接吐出架构图。结论是:省下来的时间全部赔在修改上。自动生成的图有两个致命问题——它不知道你想强调什么,也不知道哪些细节可以省略;它把所有东西画得一样大,导致图没有视觉重心。
后来我回到手动矢量绘图,用的是最普通的那类工具,能导出 SVG 就行。手动的代价是要花时间摆位置,好处是每一个框的大小、每一条线的走向都是我自己决定的,而我决定它们的过程本身就是一次梳理。
这里有个取舍:手动画的图,环境一变就得改。我的应对是把图拆成"稳定层"和"易变层"——容器层次、数据流向这些相对稳定,画在主干上;具体的 IP、端口、数据量、参数值这些易变的,全部画成独立的标注框,改了直接换标注框就行,不用动主干。
4.2 三条图层规矩:颜色管生命周期、线型管数据流向、命名管可检索
这三条规矩是被返工逼出来的。第一版图我用了七八种颜色,纯凭感觉配的,结果过了一个月自己都看不懂哪种颜色代表什么。
现在的规矩是这样的:颜色只用来表达生命周期和归属,比如蓝色系是常驻内存的、绿色系是持久化存储的、橙色系是一次性的临时对象。线型只用来表达数据流向和依赖关系,实线是同步调用、虚线是异步、点线是配置依赖。命名规矩最土但最有用——每个框都有一个唯一的名字,这个名字和我在监控系统、在字典视图查询里用的名字保持一致,这样从图到命令行是无缝的。
| 图层元素 | 表达含义 | 反例 |
|---|---|---|
| 蓝色填充 | 常驻内存结构 | 用蓝色表示"重要" |
| 绿色填充 | 持久化存储 | 用绿色表示"正常状态" |
| 橙色填充 | 临时或过渡对象 | 用橙色表示"警告" |
| 实线箭头 | 同步调用路径 | 用实线表示"关系密切" |
| 虚线箭头 | 异步或后台路径 | 随意切换虚实 |
| 点线 | 配置继承或依赖 | 用来表示数据流 |
最后一行那个反例是我真犯过的错:用点线画数据流,结果看图的人以为是配置关系,排查方向直接跑偏。
4.3 版本落款和变更记录:一个几乎没人做但极省事的习惯
每张图的右下角我都强制写三行:版本号、绘制日期、对应的数据库小版本号。第三行最关键——数据库打个小补丁,某些视图或者参数行为就可能变,图上标的结论就未必成立了。
变更记录我单独维护了一个短表格,只记"这一版改了什么、为什么改",不记细节。比如"第三版:新增向量内存池节点,因为排查时发现参数生效位置判断错误"。这个表格的价值在于,半年后你看到某个结论觉得奇怪,翻一下变更记录就知道当时的判断依据是什么。
图 A 变更记录 v1 初版,照搬通用结构,实用价值低 v2 加入向量相关组件,线条混乱,参数层级混淆 v3 拆分为运行时视图,明确 CDB/PDB 两级参数表达 v4 补充备份路径,修正向量池边界描述这四行字,比图上任何一个漂亮的圆角矩形都值钱。
5. 落笔之后暴露的四个认知偏差
5.1 向量内存池够大,不代表索引就全在内存里
这是我第一个被纠正的想法。我最初以为把VECTOR_MEMORY_SIZE调大,索引就会老老实实待在内存里。实际情况是,这个池子有它自己的管理策略,索引在池子里的驻留与否还跟访问频率、并发情况有关。池子不够大肯定出问题,但池子够大不等于万事大吉。
排查的时候需要看的视图和普通内存组件不太一样,我一般会先把所有跟向量内存相关的固定视图列出来:
SELECT name FROM v$fixed_table WHERE name LIKE '%VECTOR%';这个查询很有用,因为不同小版本之间视图名会有增删,先列出来再挑着查,比死记硬背强。查到之后再去看实际的占用和命中情况。
5.2 同一个列上建两个索引,账要合起来算
测试环境里我为了对比效果,在同一个向量列上先后建了两种不同结构的索引。当时觉得反正测试库内存大,无所谓。后来把配置往预生产搬的时候才发现,两个索引加起来的内存需求远超预期,参数按单索引估算的根本不够。
这件事在图上的体现是:B 图里同一个向量列节点出来了两条线,分别指向两个不同的索引结构。第一版图里我只画了一条线,因为当时脑子里默认"一列一索引"。改图的时候我在这个节点旁边加了个小小的乘号标注,提醒自己索引数量要乘上去算。
实践中我的建议是:生产环境同一个向量列上只保留一个索引,需要对比效果就在独立环境里做,别在同一个实例上堆。
5.3 参数生效位置画反了,后来排查多花了两小时
第二版 A 图里,我把VECTOR_MEMORY_SIZE的实线连到了 PDB 框上,意思是"在 PDB 里设的"。实际排查一个内存不足的问题时,我一直在 PDB 里查、在 PDB 里改,改完没反应,折腾了快两个小时才想起来去 CDB 根上看——原来总盘子是在根上设的,PDB 只是分配。
这个坑的教训被我用最笨的方式固化下来了:在 A 图上,凡是我曾经判断错过的节点,旁边加一个小小的三角形标记。现在图上有五个三角形,每一个背后都是一次真实的排查耗时。这个做法看起来很土,但它是这张图区别于任何官方示意图的地方——它是被真实问题打磨过的。
5.4 备份恢复路径在图上是缺的
前三个版本我完全没画备份。理由很自然:备份是运维的事,跟架构有什么关系。直到有一次要做恢复演练,我对着图琢磨"这个 PDB 的恢复会影响到哪些东西",才发现图上根本没有这条路径,只能临时翻文档。
第四版我补上了:从归档路径到备份介质的一条线,从备份介质回到 CDB 根和各个 PDB 的两条线,旁边标注了恢复的粒度和大致耗时。补完之后我才意识到,这条路径其实反过来约束了 A 图的其他部分——比如 PDB 的分布方式会直接影响单 PDB 恢复的可行性。
提示:架构图上如果找不到"出事了怎么退回去"的路径,这张图就只完成了一半。备份恢复这条线不难画,但特别容易被跳过。
6. 用几条 SQL 把图上的每个框点一遍
图画完之后我做了一件事:把图上每一个能查的节点,都用一条 SQL 点一遍,确认图上的描述和实际运行状态一致。这个动作我叫它"点框",做完之后图的可靠度会完全不一样。
6.1 内存与参数自检
先确认参数值和图上标的一致:
SELECT con_id, name, value, isdefault, ispdb_modifiable FROM v$parameter WHERE name IN ('vector_memory_size', 'sga_target', 'sga_max_size') ORDER BY con_id, name;这条查询返回的con_id列能直接告诉你当前是在哪个容器里看的值,比切来切去看SHOW PARAMETER更清楚。我一般在 CDB 根上跑一次,然后切到每个 PDB 再跑一次,把结果对着图逐行核对。
接着看向量内存的实际使用:
SELECT * FROM v$vector_memory_pool;如果这个视图在你手上版本里查不到,别慌,先回上一步那个v$fixed_table的查询里找找带 VECTOR 字样的视图名。视图名在小版本之间调整过,属于正常现象。
6.2 索引与执行计划对照
确认索引确实建在你想的那个列上,并且结构类型和图上画的一致:
SELECT index_name, index_type, status, -- 视图列名以实际版本为准 tablespace_name FROM user_indexes WHERE index_type LIKE '%VECTOR%';然后是最关键的一步——把一条真实的近似检索查询跑一遍,看执行计划:
EXPLAIN PLAN FOR SELECT chunk_id, VECTOR_DISTANCE(emb, :q, COSINE) AS dist FROM doc_chunks WHERE tenant_id = 42 ORDER BY dist FETCH FIRST 10 ROWS ONLY; SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);计划里是否真的用上了向量索引,是 B 图那个菱形节点的实际答案。如果发现走了全表扫描,先别急着调参数,先看数据量和过滤条件的选择性——小表全扫往往是最优解,强行让它走索引反而更慢。
6.3 一次近似查询走全表的定位复现
我把一次真实的排查过程完整记下来了,因为它很好地验证了 B 图的价值。
现象:一个原本几百毫秒的查询突然变成七八秒。按 B 图的路径从上往下走,第一站看查询语句本身——距离函数和建索引时声明的是不是同一个。这次是同一个,排除。第二站看过滤条件——业务侧新加了一个过滤字段,选择性很高。第三站看执行计划——果然退化成全表扫描加排序。
原因清楚了:过滤条件的新增让优化器判断"先过滤再算向量距离"比"走向量索引再回表"更划算,但实际数据分布下这个判断是错的。应对方式是把过滤条件和向量检索拆成两步,或者调整查询写法引导优化器。这个过程如果没有 B 图,我大概率会先去查内存参数、查索引状态,绕一大圈才回到执行计划上。
7. 这两张图接下来我打算怎么长
现在这两张图还缺几块我觉得迟早要补的内容。一块是库内 JavaScript 相关的执行环境,因为它在运行时也会占用一部分资源,跟向量池之间存在资源竞争的可能;另一块是 JSON 关系二元性视图的访问路径,这块目前在我们的业务里用得还不多,但趋势看是要用的。
另外一个打算是把 A 图做成两个版本:一个"逻辑版",去掉所有具体数值,只保留结构和依赖,用来做培训;一个"实例版",把当前环境的真实参数、真实数据量、真实 IP 都标上去,用来排障。现在这两个用途挤在一张图上,培训的时候要遮遮掩掩,排障的时候又嫌不够细。
我个人在实际操作中的体会是,架构图这件事的价值不在图本身,而在画图和点框的过程中被迫做的那些核对。图会过时,但"每上一个新版本就重画一遍、每画一个框就去查一次真实状态"这个习惯不会过时。我见过太多团队把架构图当成一次性的交付物,画完往文档库里一扔,两年后新人打开一看,里面的组件名字都对不上当前环境了,那张图就彻底死了。真正活着的架构图,是那种边角上有三角形标记、右下角有版本落款、旁边还挂着一张变更记录表的图——它看起来不完美,但每一处都对应着一个真实踩过的坑。