上周接到一个需求:新项目要一套独立数据库,数据隔离是硬性要求。我手头正好有一台跑在Linux上的Oracle 19c,CDB已经在线运行,里面还挂着两个正在用的PDB。这种场景下显然没必要再装一套完整实例——Oracle 19c多租户架构本身就是为这种情况设计的:在已有CDB里新增一个PDB,实例资源共享,数据字典和应用数据完全隔离,连接串独立,备份恢复也是各自独立的。
整个流程沿着“建PDB库 → 建表空间 → 建用户指定表空间 → 表空间扩容”这条链路走下来,顺利的话半小时内就能把一套新数据库环境交付出去。但每一步都有细节,尤其是刚从非多租户架构转过来的DBA,容易在容器切换、文件路径、Quota这些地方栽跟头。这篇文章就把我这次操作的全过程、用到的命令、以及遇到的坑完整记录下来。
1. 动手前先确认三件事:CDB状态、磁盘空间和PDB规划
在已有CDB里新增PDB,最大的风险不是命令不会写,而是对当前环境心里没数。CDB还挂着两个生产PDB,一个不小心就会影响它们,所以我的习惯是动手之前先做一轮状态检查和资源评估。
1.1 三句话确认CDB与PDB$SEED状态
先用系统管理员身份登录CDB,执行下面几条最基本的查询:
sqlplus / as sysdba SELECT name, cdb, open_mode FROM v$database; SELECT con_id, name, open_mode FROM v$pdbs; SELECT pdb_name, status, con_id FROM cdb_pdbs;解释一下输出怎么看:
v$database里cdb列如果是YES,说明当前库就是以多租户方式运行的,后面所有操作才成立;如果显示NO,那就是传统非CDB架构,整套方案都要换掉,得先把库转成CDB或者用DBCA重建。v$pdbs会列出当前CDB里的所有容器,包括CDB$ROOT、PDB$SEED和已经创建的PDB。正常情况下,已有PDB处于READ WRITE,PDB$SEED显示MOUNTED是正常的——seed本质是一个只读模板,不是让你连上去干活的。cdb_pdbs.status显示NORMAL表示PDB状态健康。
这里想提醒一个很多人忽略的点:即使你在CDB root里执行CREATE USER,只要用户名不带C##前缀,19c会直接报ORA-65096。这就是多租户和传统库最直观的差异。所以后续所有针对业务的操作,都要明确“我现在在哪个容器里”。
1.2 磁盘和参数资源要提前算清楚
CDB是共享同一个实例的,新增PDB不会单独分配SGA、PGA,只是在这个实例上多跑一套数据。所以资源规划主要看两件事:磁盘和连接/文件数。
先看磁盘:
df -h /u01/app/oracle/oradata这一步特别重要。很多人建PDB时只顾写命令,结果数据文件把磁盘撑爆。我再补一句:Oracle在创建PDB时会把seed里的数据文件复制一份到新PDB目录,这部分空间是立即占用的,不是后续增长才占,所以预估空间时要把 seed 文件大小也算进去。
再确认几个参数:
show parameter db_files; show parameter processes; show parameter sessions;db_files决定整个CDB能容纳的数据文件总数。PDB一多、每个PDB再挂好几个表空间,文件数涨得很快。如果这个参数是默认的200左右,建议提前调大,免得将来加数据文件时碰到数量上限。注意这个参数是实例级别的,改动要重启才生效,所以要提前规划,不要等生产环境扩容时才想起来。
顺手做一张规划表,这是我在操作前必填的清单:
| 规划项 | 用途说明 | 本次实例取值 |
|---|---|---|
| PDB名称 | 一眼看出归属,别起无关名字 | app_pdb |
| PDB管理员账号 | 管理PDB内部对象的本地管理员 | app_admin |
| 业务账号 | 应用连接数据库用的账号 | app_user |
| 默认表空间 | 业务数据最终落盘位置 | app_ts |
| 临时表空间 | 排序、hash等临时段使用 | temp(PDB自带) |
| 数据文件路径 | 最好跟随PDB目录 | /u01/app/oracle/oradata/ORCLCDB/app_pdb/ |
| 初始大小策略 | 预留近期增长空间 | 512M起步,autoextend开上限8G |
把这张表填完,后面每一步命令里的变量都可以直接对号入座,不容易写错。
2. 从PDB$SEED创建新PDB:一条命令和它的两个关键点
创建PDB主要有三种方式:从PDB$SEED直接创建(全新数据库,最常用);克隆已有PDB(适合复制一套现成环境当测试库);unplug/plug方式把PDB从别的CDB插进来(适合迁移场景)。这次新项目是全新业务,没有现成环境可复制,所以最合适的是从seed创建。
2.1 从seed建还是克隆已有PDB?先分清场景
这部分值得单独说一下,因为很多人一听到“复制PDB”就问能不能直接克隆。我的建议是:
- 如果业务是全新的,表结构、数据都要从头建,那就老老实实从seed创建,干净利落。
- 如果是要快速拉一套跟现有环境一模一样的数据(比如测试环境复刻生产),本地克隆更好用。克隆时源PDB必须处于
OPEN READ ONLY或者关闭状态,克隆完成再切回去。 - 如果是整库迁移到新CDB,unplug/plug更合适。
做一个简单对照:
| 方式 | 适用场景 | 源状态要求 | 复杂度 |
|---|---|---|---|
| 从PDB$SEED创建 | 全新项目、空库 | seed自动就绪 | 低 |
| 克隆已有PDB | 复制环境、测试库 | OPEN READ ONLY或关闭 | 中 |
| unplug/plug | 跨CDB迁移 | 源端关闭后拔出 | 中高 |
2.2 实操命令:FILE_NAME_CONVERT、ADMIN USER与STORAGE限制
确认环境没问题、方案也定了,执行创建命令:
CREATE PLUGGABLE DATABASE app_pdb ADMIN USER app_admin IDENTIFIED BY "Admin#123" FILE_NAME_CONVERT = ( '/u01/app/oracle/oradata/ORCLCDB/pdbseed', '/u01/app/oracle/oradata/ORCLCDB/app_pdb' ) STORAGE (MAXSIZE 10G) PATH_PREFIX = '/u01/app/oracle/oradata/ORCLCDB/app_pdb/';几个关键点,我逐个说清楚:
FILE_NAME_CONVERT是文件路径映射规则。它告诉Oracle:seed目录下的所有数据文件,都复制到新PDB对应的目录里。如果你不指定,Oracle会尝试自动生成,但文件可能分散在别的地方,后面管理很头疼。所以我的建议是永远显式指定转换规则,做不做OMF都先把这个写上。
ADMIN USER是PDB内部的本地管理员。这个账号可以登录PDB做管理,但它是PDB本地用户,没有CDB root的权限,跟sys这种common user不是一个层级。
STORAGE (MAXSIZE 10G)是可选的,但建议加上。它给这个PDB设一个总存储上限。没有上限的话,一个失控的报表查询就可能把磁盘填满。10G只是示例,按需调整。
PATH_PREFIX和FILE_NAME_CONVERT搭配使用,限制这个PDB所有文件都放在同一个目录下,对后续备份策略和巡检都比较友好。
命令执行成功后,查一下:
SELECT name, open_mode FROM v$pdbs WHERE name = 'APP_PDB';这时新PDB通常是MOUNTED状态,因为创建本身还没把它打开。
2.3 打开PDB并Save State,别等重启后才后悔
新PDB创建完之后不能直接用,还得打开并保存启动状态:
ALTER PLUGGABLE DATABASE app_pdb OPEN; ALTER PLUGGABLE DATABASE app_pdb SAVE STATE;SAVE STATE这条命令是我强烈建议每一次创建PDB后都执行的。它的作用是告诉CDB:以后整个实例重启时,自动把这个PDB恢复到当前打开状态。如果不执行,CDB一旦重启,所有PDB默认回到MOUNTED,应用连接直接失败。这个坑真不是一次两次在凌晨重启后遇到了——人还没醒,告警先响。
如果CDB里PDB已经很多,我习惯直接:
ALTER PLUGGABLE DATABASE ALL SAVE STATE;一次性把现有PDB的启动状态都固化下来,省得一个个写。
最后再执行SHOW PDBS看一眼,确认新PDB已经是READ WRITE,状态正常,这一步才算完。
3. 建表空间前先切换容器:文件路径与段管理方式的选择
PDB开起来了,下一个动作是建表空间。但在建表空间之前,有一个最常见也最伤人的操作顺序问题:容器切换。
3.1 容器切错了,表空间就建到CDB根里了
从CDB root登录后,当前会话默认还是在CDB$ROOT容器里,哪怕你明明感觉自己已经“连到了新PDB”。如果你没切容器就直接执行:
CREATE TABLESPACE app_ts ...;这条表空间会建到CDB$ROOT下面,也就是整个CDB共享的那一层。这在多租户架构里是大忌,轻则表空间混乱,重则影响其他PDB的字典空间。所以建任何业务对象之前,第一件事永远是:
ALTER SESSION SET CONTAINER = app_pdb; SELECT SYS_CONTEXT('USERENV', 'CON_NAME') FROM dual;第二条查询会返回当前容器名,用来确认切换是否成功。我习惯把这条放进所有脚本模板的开头,防止后面操作在错误的容器里执行。
另外,多租户下PDB里不能创建UNDO表空间——UNDO是整个CDB共享的,不用也不应该自己建。如果需要单独的表空间,就建普通业务表空间;临时表空间PDB自带了一套TEMP,绝大多数情况下直接用默认的就行。
3.2 数据文件路径规划与表空间参数取舍
确认在正确的容器里之后,创建业务表空间:
CREATE TABLESPACE app_ts DATAFILE '/u01/app/oracle/oradata/ORCLCDB/app_pdb/app_ts01.dbf' SIZE 512M AUTOEXTEND ON NEXT 64M MAXSIZE 8G EXTENT MANAGEMENT LOCAL AUTOALLOCATE SEGMENT SPACE MANAGEMENT AUTO;先说路径。我建议数据文件放在PDB自己的目录下,也就是xxx/app_pdb/这种结构。这样将来做RMAN备份、排查文件归属、甚至迁移PDB,都能一眼认出这套文件属于哪个容器,非常省心。
这里有个真实的坑:Oracle不会替你创建不存在的子目录。如果你在路径里写了一个没建过的目录,执行时会直接报错。所以要么用已经存在的目录,要么先mkdir -p建好目录再执行。别让这种小事卡住。
再说参数。
EXTENT MANAGEMENT LOCAL是本地管理表空间,这个在19c基本是默认共识了,不用考虑字典管理表空间。关键是AUTOALLOCATE和UNIFORM SIZE的选择:
AUTOALLOCATE让Oracle根据段的大小自动选择合适的区大小,适合OLTP场景,表大小不一、增长模式不可预测。UNIFORM SIZE 1M所有区固定大小,适合大量小对象或者某些特殊运维场景,空间利用率更可控,但需要你比较了解对象的增长特征。
绝大多数业务系统,直接用AUTOALLOCATE是最省事且合理的。
SEGMENT SPACE MANAGEMENT AUTO同样是现代Oracle的默认选项,段内的空闲空间管理交给位图自动完成,比老式MANUAL手工管理链表的方式高效很多,不需要特殊理由,保持默认就好。
关于AUTOEXTEND ON NEXT 64M MAXSIZE 8G,我单独解释一下。我的建议很简单:自动扩展要打开,但必须设上限。开自动扩展是为了避免每次空间不足时手工介入;设上限是为了防止某个异常SQL把磁盘写满。NEXT设置多大也讲究:太小,频繁扩展消耗性能;太大,容易浪费空间。64M到128M对大多数OLTP系统是合理的起点。
3.3 建完表空间立刻检查文件和大小
建表空间这种DDL,执行成功不代表一切正常。我习惯马上查一遍文件信息:
SELECT tablespace_name, file_name, bytes/1024/1024 size_mb, autoextensible, maxbytes/1024/1024 max_mb, status FROM dba_data_files WHERE tablespace_name = 'APP_TS';重点看这几个字段:文件路径是否落在预期目录、autoextensible是不是YES、max_mb是不是设置的8G上限、status是不是AVAILABLE。全部正常再继续往下走。
这一步花不了一分钟,但能避免很多后续排查的麻烦。
4. 创建用户并指定表空间:三个高频坑,一次性全避开
表空间就绪,接着创建业务用户。表面上看就是一条CREATE USER,但这里有三个高频坑,我一个一个复盘。
4.1 完整建用户语句:默认表空间、临时表空间、Quota一个都不能少
先看完整语句:
ALTER SESSION SET CONTAINER = app_pdb; CREATE USER app_user IDENTIFIED BY "App#2024" DEFAULT TABLESPACE app_ts TEMPORARY TABLESPACE temp QUOTA UNLIMITED ON app_ts PROFILE default;第一坑:DEFAULT TABLESPACE不指定。新建的PDB里默认没有USERS表空间,如果创建用户时不指定默认表空间,用户的默认表空间很可能会落到SYSTEM上。应用产生的所有段对象全部写到SYSTEM表空间,这对数据库的危害不用我多说。所以必须显式指定业务表空间。
第二坑:TEMPORARY TABLESPACE不指定。不指定会沿用数据库默认临时表空间,一般也是TEMP,问题不大,但显式写出来更严谨,尤其是将来PDB里如果有多个临时表空间、想给不同用户区分临时空间时,这一项就必须显式控制了。
第三坑:QUOTA没给。这个坑在“4.3”里专门讲,因为它的报错特别容易误导人。
还有个细节,密码为什么要用双引号包起来?因为Oracle的密码如果不加双引号,会默认转成大写。你设的App#2024可能实际变成了APP#2024,应用层连接串里对不上,半天查不出原因。用双引号写密码,Oracle会严格按大小写处理。
这里还要注意一下用户命名。在PDB中创建的业务用户是本地用户,不需要也不能加C##前缀。C##前缀只用于CDB root下创建的common user。如果你在CDB root下写一条不带C##的CREATE USER,19c会报ORA-65096;同样地,带着C##在PDB里建用户也不合适,会产生一个你未必想要的全局账号。正确做法非常明确:业务本地用户就在PDB容器里创建。
4.2 权限授予:Connect+Resource够用,但别滥用DBA
用户创建好之后,权限授予是下一步。我的推荐起步是:
GRANT CONNECT, RESOURCE TO app_user;CONNECT提供最基本的连接能力,RESOURCE提供创建表、序列、视图等常用对象的能力,大多数内部业务系统这个组合就够了。如果应用还涉及外部表、调优包、特定包的执行,再按需要补GRANT EXECUTE ON ...或者对应的系统权限。
关键是:除非是开发人员的个人管理账号,否则不要直接给应用账号DBA角色。DBA权限在19c里同时在CDB和所有PDB生效,它的系统权限列表太大,一旦应用被注入或者账号泄密,损失范围是整个数据库而不是一个PDB。
4.3 两个报错复盘:ORA-01950和容器方向错误
第一个报错:ORA-01950: no privileges on tablespace 'APP_TS'。
这个报错特别有迷惑性。表面看是权限问题,实际上80%的情况是用户在该表空间的Quota配额没给。前面CREATE USER时如果没有写QUOTA UNLIMITED ON app_ts,用户在这个表空间上没有任何空间份额,自然一块数据都插不进去。
修复其实简单:
ALTER USER app_user QUOTA UNLIMITED ON app_ts;如果你想把应用用户在某个表空间上限制得更细,也可以给具体大小,比如QUOTA 2G ON app_ts。但注意:如果用户被授了UNLIMITED TABLESPACE系统权限(比如有人图省事给了DBA角色),那么不管Quota怎么设置都等于没有限制。所以最小权限原则下,Quota要认真管理。
第二个报错:ORA-65096: invalid common user or role name。
这个报错出现在你人在CDB root却想创建不带C##前缀的用户时。报错信息长得很奇怪,网上搜索一堆解释,但本质就是容器方向错了。解决方法是先切回目标PDB再执行创建。如果你是连接串登录的,确认连接字符串里服务名是PDB服务名,而不是CDB的。
5. 表空间扩容:Resize、加数据文件、Autoextend怎么用
表空间建好了,用户在用了。但数据库这东西没人敢保证空间永远够用,扩容是绕不开的日常操作。这次我把扩容的几种姿势一次讲透。
5.1 先看使用率:dba_tablespace_usage_metrics的局限
扩容之前先明确要不要扩、扩到多少。很多人上来就写SQL查dba_tablespace_usage_metrics:
SELECT tablespace_name, used_space, tablespace_size, used_percent FROM dba_tablespace_usage_metrics WHERE tablespace_name = 'APP_TS';这个视图用起来确实方便,但它有一个关键局限:tablespace_size用的是当前所有数据文件已经分配的大小,而不是MAXSIZE上限。也就是说,如果一个表空间的数据文件配了AUTOEXTEND ON MAXSIZE 8G,而现在刚分配了512M,使用率可能只有10%,但磁盘空间理论上还能膨胀到8G。这时候看使用率可能低估风险。
所以我的建议是,判断扩容需求看两个指标结合:一是当前已用空间,二是所有文件的MAXSIZE上限。前者决定“现在够不够用”,后者决定“未来还能撑多久”。对应的查询:
SELECT tablespace_name, file_name, bytes/1024/1024 current_mb, maxbytes/1024/1024 max_mb, autoextensible FROM dba_data_files WHERE tablespace_name = 'APP_TS';再配合:
SELECT tablespace_name, ROUND(SUM(bytes)/1024/1024) free_mb FROM dba_free_space WHERE tablespace_name = 'APP_TS' GROUP BY tablespace_name;结合起来,就能比较准确地判断当前的剩余空间了。
5.2 三种扩容方式的具体命令与取舍
确定了要扩容,通常有三条路。
第一条:直接RESIZE现有数据文件。适用于文件在创建时规格偏低、磁盘上还有足够连续空间、并且还没到文件数量上限的场景。
ALTER DATABASE DATAFILE '/u01/app/oracle/oradata/ORCLCDB/app_pdb/app_ts01.dbf' RESIZE 2G;注意:RESIZE不能小于该数据文件已使用的空间,否则直接报ORA-03297: file contains used data beyond requested RESIZE value。所以扩容前先查一下文件的实际使用量,留出至少20%的余量。
第二条:给表空间增加一个新的数据文件。适用于原文件MAXSIZE已经快到顶、想进一步提升容量同时分散I/O的场景。
ALTER TABLESPACE app_ts ADD DATAFILE '/u01/app/oracle/oradata/ORCLCDB/app_pdb/app_ts02.dbf' SIZE 1G AUTOEXTEND ON NEXT 128M MAXSIZE 16G;加文件的好处是可以在多个文件之间分摊写入,缺点是文件数量增加、管理成本上升,而且如果有文件损坏,恢复时需要处理的对象也更多。所以“能通过resize解决就优先resize”是我个人的习惯。
第三条:调整AUTOEXTEND的MAXSIZE上限。适用于当前文件还有增长空间,只是上限设小了。
ALTER DATABASE DATAFILE '/u01/app/oracle/oradata/ORCLCDB/app_pdb/app_ts01.dbf' AUTOEXTEND ON NEXT 64M MAXSIZE 16G;我个人对自动扩展的态度是:生产环境可以开,但一定要设上限。上限的作用是防止失控SQL把磁盘耗尽,导致整个CDB所有PDB一起出问题——因为磁盘是共享的。
三种方式对照:
| 方式 | 适用场景 | 关键命令 | 注意点 |
|---|---|---|---|
| RESIZE | 单文件扩容 | ALTER DATABASE DATAFILE ... RESIZE | 不能小于已用空间 |
| ADD DATAFILE | 分散I/O、原文件到顶 | ALTER TABLESPACE ... ADD DATAFILE | 文件数量与备份范围增加 |
| AUTOEXTEND上限调整 | 文件还有空间 | ALTER DATABASE DATAFILE ... AUTOEXTEND | 必须设MAXSIZE防失控 |
5.3 扩容后的两个验证动作
扩容命令执行成功不等于结束。我每次扩完都做两件事。
第一,再查一次dba_data_files,确认新文件或者resize后的文件状态是AVAILABLE,大小和预期一致。
第二,检查操作系统磁盘余量。这个听起来蠢,但真有人加完文件后现场发现磁盘早就满了,虽然DDL执行成功,但后续写操作全在报警。这时候看df -h比看数据库内部视图更直接。
6. 交付后验证和运维习惯:让新增PDB稳稳上线
前面几步做完,数据库环境已经成型了。但交付前还需要做最后的连接验证,以及调整后续运维习惯。
6.1 连接验证与监听服务名
PDB创建后,Oracle的监听会自动注册PDB对应的服务名,通常就是PDB名称,可能会有域名后缀。先用命令确认:
lsnrctl services看输出里有没有app_pdb相关的service。如果没看到,多半是动态注册还没刷新,可以等一会儿,或者手动触发:
ALTER SYSTEM REGISTER;然后从应用层视角连一次,模拟真实连接:
sqlplus app_user/"App#2024"@//localhost:1521/app_pdb输入几条查询,比如SELECT sys_context('USERENV', 'CON_NAME') FROM dual;,确认返回的是APP_PDB而不是CDB root。这一步能同时验证监听、服务名、用户密码、默认表空间几个环节,省得后面应用接入时出问题再层层排查。
6.2 固定脚本模板与监控阈值调整
这套流程我重复过太多次,现在已经固化成脚本模板了。每个新项目来了,只需要改四个变量:PDB名称、表空间名称、用户名、路径前缀,跑一遍基本不会出错。模板里固定包含环境检查、创建PDB、开PDB并保存状态、切容器、建表空间、建用户授权、扩容预案和最终的连接验证,每一段都有明确的输出检查点。
另外,新PDB上线后一定要记得把它纳入日常监控和备份策略。RMAN方面,多租户下可以对PDB单独做备份,不需要整个CDB一把梭;监控方面,把app_ts表空间使用率、v$pdbs.open_mode、磁盘剩余空间这几个指标加进告警阈值。我在实际操作中发现,很多环境出了问题不是没监控,而是告警阈值设得太宽,比如表空间到95%才告警,而应用在90%时已经开始大量报错。建议根据业务写入速度把阈值设得保守一些。
最后说一句个人体会:多租户不是把多个库塞进同一个实例就完事了,它真正考验的是你对“共享什么、隔离什么”的理解。资源是共享的,所以磁盘、内存、文件数都要全局看;数据和权限是隔离的,所以容器切换、本地用户、Quota一样都不能马虎。把这套流程理顺,以后新项目加数据库就是一件十分钟左右的事。