一提起Oracle多租户容器数据库(CDB/PDB),很多人第一反应是"这不就是把多个库塞进一个大库的壳里嘛"。真用起来才发现,里面的门道比想象的深:连接串怎么配、克隆为什么要先设只读、RMAN恢复时为什么不能随便对某个PDB做不完全恢复、资源管理计划到底管住了什么——这些问题只看概念图是看不明白的,实操一遍才会真正理解。
这篇文章我把从创建CDB、建第一个PDB,到日常管理、克隆迁移、备份恢复、资源隔离这一条线上容易踩的坑和值得记住的细节整理出来。适合刚接触多租户架构的人,也适合那些已经用了一段时间但总在某几个报错上反复折腾的同路人。
1. 为什么需要多租户架构:一堆独立库带来的混乱
1.1 传统模式下的资源浪费与管理成本
早些年的数据库部署方式基本是一套业务一套独立实例:A系统的库占一台机器,B系统的库再占一台机器。每个库都要吃一块专属的内存、SGA、一组后台进程,哪怕业务量很小,机器配置也得按最大峰值去留;更麻烦的是,补丁和版本升级要一台一台打,十套库就是十份重复劳动。
多租户架构把"一个实例对应一套库"的模式,改成了"一套CDB(容器数据库)里装多个PDB(可插拔数据库)"的模式。CDB是壳,PDB是真正面向业务的数据容器。对于业务应用来说,PDB仍然是一个完整独立的数据库,有自己的数据字典、用户、表空间、权限体系;对于DBA来说,底层只需要维护一个数据库实例,资源可以共享,补丁只需打一次,备份也能按PDB粒度走。
1.2 CDB、PDB和根容器之间的关系
CDB里除了PDB,还有一个Root容器(CDB$ROOT),它保存了SPFILE、控制文件、UNDO等实例级元数据,以及Oracle提供的字典表和公共对象。PDB则像一个个"租户",它们共享CDB实例的后台进程和SGA,但在逻辑层面彼此隔离。
我经常用公寓来打比方:CDB是整栋楼,楼里的水电总闸、电梯、公共走廊是Root;每间房子是PDB,住户只关心自己屋里的装修和家具,不用管大楼的管线。如果大楼水管维修,整栋楼都会受影响,这就是为什么CDB级操作(比如重启实例、改控制文件)会影响所有PDB;而房间里怎么折腾,只要不破坏承重墙,邻居不会知道。
1.3 多租户带来的实际收益和取舍
收益最直观的几点:
- 整合成本:一台服务器跑十几个PDB,机器数量和许可成本都下降。
- 维护提速:补丁、升级针对CDB做一遍,所有PDB同时受益。
- 快速复制:PDB克隆可以秒级生成开发库,配合快照克隆能做到分钟级交付。
- 备份灵活:可以只备份某一个PDB,不完全恢复也能限定在PDB范围。
但代价也存在:同一CDB里的PDB共用一套实例资源,一个"吵闹的邻居"可能吃掉大量CPU和IO;CDB级别的故障(比如实例崩溃)会影响内部所有PDB。所以多租户不是白拿好处,资源管理和监控的功夫不能省。
2. 从创建到连接:手把手拉起第一个CDB和PDB
2.1 创建CDB时的关键选项
安装Oracle 19c时,选择创建CDB还是非CDB是第一个分水岭。新建实例时勾选"Create as Container Database",指定CDB名字,其他选项包括:
- EM(Enterprise Manager):建议开启,多租户的管理比以前复杂,图形界面看PDB状态更方便。
- 字符集:这是后面排查问题最头疼的点。CDB的字符集最好与你预期的所有PDB兼容,尽量用AL32UTF8,后面克隆PDB时对字符集的一致性要求比较高。
- 本地UNDO:从12cR2开始支持每个PDB单独配UNDO表空间,这个选项建议开启。它带来的好处是PDB故障时可以独立清理UNDO,不至于影响CDB里其他PDB。
安装完成后可以查询一下基本信息:
show con_name; show con_id;不出意外会看到CON_NAME为CDB$ROOT,CON_ID为1。
2.2 用种子PDB创建第一个业务库
CDB创建好之后,里面会有一个种子PDB(PDB$SEED),它是所有新PDB的模板。创建新PDB最标准的做法就是从种子克隆:
create pluggable database salespdb admin user sales_admin identified by "your_password" file_name_convert = ('/u01/oradata/orcl/pdbseed', '/u01/oradata/orcl/salespdb') path_prefix = '/u01/oradata/orcl/salespdb';这里两个关键点:
admin user:创建PDB管理员用户,后续在这个PDB里创建业务用户、表空间都要靠它。file_name_convert:把种子PDB的数据文件路径替换成新PDB的路径。如果CDB的OMF(Oracle Managed Files)配置正确,有时可以省略,但建议还是显式写清楚,路径混乱是管理灾难。
创建完成后,PDB的状态是MOUNTED,并不能直接连接。需要打开它:
alter pluggable database salespdb open; alter pluggable database salespdb save state;save state这条命令容易忽略。它的作用是让CDB下次启动时,自动把这个PDB恢复到关闭前的状态。如果不加,CDB重启后PDB默认是MOUNTED,业务连接串会直接报"服务不存在"。
查看PDB状态:
select name, open_mode, con_id from v$pdbs;2.3 监听注册与service_name:连接配置最容易翻车
刚玩多租户的人,十有八九在连接上栽过跟头。核心原因是:PDB有自己的service_name,CDB也有自己的service_name,同一个监听端口上挂了好几个服务。
一个典型的tnsnames.ora配置:
SALESPDB = (DESCRIPTION = (ADDRESS = (PROTOCOL = TCP)(HOST = dbserver01)(PORT = 1521)) (CONNECT_DATA = (SERVICE_NAME = salespdb) ) )注意这里SERVICE_NAME填的是PDB的名字salespdb,不是CDB名orcl。很多人在命令行里用sqlplus system/oracle@//host:1521/salespdb提示密码错误或ORA-01017,其实不一定是密码问题,很可能连的是CDB而不是PDB。
还有一点要特别提醒:想通过sys登录某个PDB,有两种方式。一种是用PDB的service_name连接后,输入sys/pwd@salespdb as sysdba;另一种是连接到CDB后执行alter session set container=salespdb。很多时候应用的连接账号密码完全正确,但sysdba身份只能从CDB管理PDB,我吃过好几次类似教训。
2.4 验证连接与常见报错
创建完成后,推荐先在命令行验证连接:
sqlplus sales_admin/"your_password"@//dbhost:1521/salespdb如果报ORA-12514,先检查监听状态:
lsnrctl status确认PDB的service_name是否出现在服务列表中。如果没出现,执行alter system register强制注册,或者等监听动态注册。这类问题绝大多数不是配置写错,而是监听还没拿到最新服务名。
3. PDB的日常管理:切换、启停、表空间与用户隔离
3.1 在不同容器之间切换
日常管理PDB,最核心的一条命令是切换容器:
alter session set container = salespdb; show con_name;在CDB中执行这条命令后,当前会话就进入salespdb。注意,多数PDB级管理操作,比如创建用户、表空间,都需要先进到对应PDB再执行。CDB$ROOT里不应该创建业务对象,虽然Oracle不强制,但这是一种好习惯。
如果嫌每次手动切换麻烦,可以给每个PDB配一个专用连接串直接连过去,再执行DDL。两种方式没有本质区别,只是习惯问题。我比较推荐管理脚本里先显式声明容器,避免脚本跑错库造成误操作。
3.2 每个PDB是否需要独立UNDO和TEMP
多租户环境下,TEMP表空间默认每个PDB自己有一个临时表空间;UNDO则要看安装时是否启用了本地UNDO,启用了本地UNDO后,每个PDB都有独立的UNDO表空间。
查询PDB的UNDO和TEMP:
select tablespace_name, contents from dba_tablespaces; -- 在对应PDB中执行独立UNDO意味着如果一个PDB里有长事务,UNDO膨胀影响能控制在单个PDB内。但如果你用的是共享UNDO模式(12cR1遗留场景),一个PDB的大事务可能把CDB级UNDO撑爆,那影响就是全局的。所以新版本环境下,能启用本地UNDO就启用。
3.3 在PDB中管理表空间与用户
进入PDB后,创建表空间和用户的方式与非CDB几乎一样:
create tablespace salests datafile '/u01/oradata/orcl/salespdb/salests01.dbf' size 2g autoextend on next 512m maxsize 32g; create user app_sales identified by "app_pwd" default tablespace salests quota unlimited on salests; grant connect, resource, create session to app_sales;需要留意的是,PDB中的用户是隔离的。A用户在所有PDB里完全相同吗?不是。每个PDB有自己独立的用户命名空间,所以salespdb里的app_sales和anotherpdb里的app_sales是两个不同的用户。这在迁移PDB时更容易体现:拔插件PDB到另一台CDB,原来PDB里的用户直接跟着走,不受新CDB公共用户影响。
3.4 启动关闭:save state 和 all 操作
管理多个PDB时,批量操作命令能省很多时间:
alter pluggable database all open; alter pluggable database all close immediate;关闭PDB时,建议用immediate还是normal?和普通实例类似的逻辑,开发测试环境无所谓,生产环境要尽量先通知应用端切换流量,再执行关闭,避免会话被强行中断。这里有个小坑:CDB重启后,如果某个PDB没有save state,它会停留在MOUNTED状态,业务报警马上就会来。所以新创建PDB后,save state应该写成固定操作步骤之一。
4. PDB克隆与迁移:本地克隆、远程克隆和拔插玩法
4.1 本地克隆:快速生成一套开发库
多租户最令DBA兴奋的功能之一就是PDB克隆。想在几秒内生成一套和生产结构一样的开发库?
-- 源PDB需要处于open read only alter pluggable database salespdb open read only; create pluggable database devpdb from salespdb file_name_convert = ('/u01/oradata/orcl/salespdb', '/u01/oradata/orcl/devpdb'); alter pluggable database devpdb open;克隆的原理是基于文件的拷贝,所以速度很快,但前提是源PDB必须是open read only状态。很多人第一次执行会报ORA-65081,就是因为源库不是只读打开的。
4.2 远程克隆:跨CDB复制PDB
远程克隆适用于把某台CDB里的PDB,克隆到另一台CDB中。前提条件:
- 源PDB
open read only。 - 目标CDB和源CDB都开启了本地UNDO。
- 需要有数据库链接(dblink)连接源库,且dblink用户有权限读取源PDB的数据字典(通常要求有
CREATE PLUGGABLE DATABASE权限)。
-- 在目标CDB中执行 create pluggable database remote_dev from salespdb@link_to_source file_name_convert = ('/u01/oradata/orcl/salespdb', '/u01/oradata/orcl/remote_dev');远程克隆适合两地环境同步,比如从生产CDB克隆到灾备CDB做演练,或者从生产克隆到预发环境。但在生产上执行前,一定要确认网络延迟和传输时间,数据量大的时候克隆任务跑很久是正常的。
4.3 拔插PDB:把"房间"搬到另一栋楼
拔插(unplug/plug)是真正的PDB迁移方式。它不像克隆会生成一个对照副本,而是把PDB从原CDB中摘下来,再挂到目标CDB上。
拔出的步骤:
alter pluggable database salespdb close; alter pluggable database salespdb unplug into '/backup/salespdb.xml'; drop pluggable database salespdb keep datafiles;注意keep datafiles,它告诉Oracle保留数据文件不删除,这样你才能把文件拷到目标服务器。然后到目标CDB上执行:
create pluggable database salespdb using '/backup/salespdb.xml' file_name_convert = ('/u01/oradata/orcl/salespdb', '/u01/oradata/orcl/salespdb');拔插最需要注意版本兼容性,源CDB和目标CDB的版本必须满足一定条件,低版本CDB拔出的PDB不能插到更高版本CDB?不是,是低版本可插到高版本,反向不行。另外,字符集、时区设置不一致也可能导致插不进去。实操时,我建议先把xml文件和数据文件完整备份到共享存储,再开始拔插流程。
4.4 快照克隆和刷新克隆的适用场景
19c及之后版本支持快照克隆和刷新克隆。快照克隆依赖底层存储快照能力,生成PDB瞬间完成,适合开发测试环境从生产PDB快速派生环境;刷新克隆则可以让克隆出来的PDB周期性地从源PDB刷新数据,适合测试库定期同步生产数据。
这些玩法听起来美好,但对底层存储、版本、以及备份策略都有额外要求。不要在没有测试过的环境里直接上生产,我见过有人把快照克隆挂在普通文件系统上,结果快照无法创建,最后数据文件全是稀疏文件,差点把存储空间占满。这类高级功能,先搭一套实验环境验证成功了再考虑引用。
5. RMAN备份恢复:CDB级别与PDB级别的差异
5.1 CDB备份与PDB备份的区别
多租户环境下,RMAN既可以备份整个CDB,也可以单独备份某个PDB。两者的区别要理解清楚:
- CDB级备份包含CDB$ROOT、种子PDB、所有PDB,以及控制文件和SPFILE。
- PDB级备份只包含某一个PDB的数据文件,以及该PDB的UNDO表空间(如果启用了本地UNDO),归档日志还是要的。
常用的备份命令:
-- 备份整个CDB backup database plus archivelog; -- 备份单个PDB backup pluggable database salespdb;日常策略推荐:控制文件和SPFILE每次自动备份;全库备份频率根据恢复目标定;针对重要PDB,可以额外做单独备份,这样在恢复时不必动用整个CDB的全量备份。
5.2 单独恢复某个PDB的完整流程
当某个PDB崩溃或者数据文件损坏,可以单独恢复:
-- 进入CDB(必须以sysdba身份) alter pluggable database salespdb close; -- RMAN中执行还原和恢复 restore pluggable database salespdb; recover pluggable database salespdb; alter pluggable database salespdb open;这一步最怕的是恢复到一半发现归档日志断了。所以在恢复前,先确认该PDB的归档日志连续性。如果归档日志有缺失,RMAN会自动报错,这时候可能需要恢复到更早的备份点,或者接受数据丢失。
5.3 不完全恢复在PDB中为什么麻烦
这里有一个容易想当然的地方:对单个PDB做不完全恢复,能不能恢复到过去某个时间点?严格来说,如果你只是单独恢复一个PDB的数据文件到过去时点,但CDB里的redo log是所有PDB共享的,后续日志里还可能包含其他PDB的事务,所以单独做时间点恢复很可能因日志序列不匹配而失败,或者与CDB整体数据不一致。
实践中,对某个PDB做不完全恢复通常做法是:
- 先将整个CDB恢复到某个时间点(或SCN)。
- 再用
alter database open resetlogs打开整个CDB。 - 此时所有PDB都会回到这个时间点。
也就是说,不可能只说"我只想把salespdb恢复到昨天下午3点",其他PDB完全不动——因为CDB是共日志的。要规避这种窘境,最好给关键PDB设计独立的恢复策略,比如高频单独备份,这样全量恢复最快的粒度和数据丢失量都可控。
5.4 备份策略建议
从我实际运维的经验来看,多租户的备份策略至少要分两层:
- CDB层:控制文件、SPFILE,每天自动备份,保留最近若干天。
- PDB层:按业务重要性区分备份频率。核心PDB每天单独备份,普通PDB每周一次全备加每日归档即可。
- 归档日志统一备份,因为PDB恢复依赖归档。
另外,RMAN在CDB模式下如果执行delete obsolete,要小心它可能根据备份策略标记PDB备份失效。建议先report obsolete看要删除哪些,再执行确认。
6. 多租户下的性能与资源控制:别让邻居互相打扰
6.1 CPU、IO与内存的隔离手段
共享实例的好处是省资源,坏处是某个PDB可能把CPU打满,影响同CDB里的其他业务。Oracle提供了CDB资源管理计划(CDB Resource Manager Plan)来控制各PDB的资源使用。
创建资源管理计划的思路:
begin dbms_resource_manager.create_cdb_plan( plan => 'cdb_plan', comment => 'Control PDB resource usage' ); dbms_resource_manager.create_cdb_plan_directive( plan => 'cdb_plan', pluggable_database => 'salespdb', shares => 2, utilization_limit => 80 ); dbms_resource_manager.create_cdb_plan_directive( plan => 'cdb_plan', pluggable_database => 'otherpdb', shares => 4, utilization_limit => 50 ); end; / alter system set resource_manager_plan = 'cdb_plan' scope = both;这里的shares是权重分配,utilization_limit是CPU使用率上限。这样能限制某个PDB最多用多少CPU,不至于把整个CDB拖垮。
内存方面,PDB不能独立设置SGA,但可以通过alter system set pga_aggregate_limit在PDB级别限制PGA。如果遇到单个PDB消耗大量内存导致邻居PDB响应变慢,可以给有问题的PDB设一个PGA上限,缓冲一下影响。
6.2 PDB级参数覆盖
多租户允许在PDB级别覆盖某些实例参数,让不同PDB用不同的优化器模式、并行度等:
alter system set optimizer_mode = first_rows container = current scope = both;需要注意,并非所有参数都支持PDB覆盖。一个简单的判断办法:登录到PDB后执行alter system set 参数名 scope=both,如果报ORA-65040,说明该参数不允许在PDB级设置。遇到这类情况,只能在CDB级统一调整,或者考虑把该PDB迁到另一个CDB中。
6.3 监控PDB资源消耗
日常监控,以下几个视图值得常看:
v$pdbs:各PDB的打开模式、状态。v$con_container_stats:提供PDB级别的CPU、IO等统计信息。v$resource_limit:观察资源限额。v$session里的con_id列:定位某个会话属于哪个PDB。
有一次客户反馈整个实例慢,查AWR发现是某个PDB跑了个超大规模并行查询,把CPU全占了。如果没有按PDB维度看统计,很难快速定位。所以监控面板里一定要有PDB维度的CPU、会话数、IO指标。
6.4 常见的性能干扰与优化经验
- 大查询并行度太高:在PDB级限制
parallel_max_servers,或用资源管理计划限制并行。 - 定时任务冲突:多个PDB都在整点跑批,CPU峰值叠加。错开调度比硬扛更好。
- 归档风暴:某个PDB数据量暴增,导致整个CDB归档日志暴涨。通过监控
v$con_container_stats找到元凶。
资源管理计划的配置不需要过于复杂,先把最重要的几个PDB加进去,后面再根据监控数据微调。
7. 实战中的坑:多租户架构最容易翻车的地方
7.1 连接串里把PDB名字当service_name用
这个问题前面提过,但值得再展开。Oracle数据库里每个PDB会注册一个与该PDB名称相同的默认服务名,所以很多应用直接拿PDB名当服务名用没什么问题。但如果你自定义了服务名,或者通过svcrctl创建了别的服务,默认服务名也可能存在;此时应用如果连了非期望的服务名,可能连到了CDB或另一个PDB。检查应用实际连接的服务名:
select sys_context('USERENV', 'DB_NAME'), sys_context('USERENV', 'CON_NAME') from dual;如果返回的CON_NAME不是预期PDB,赶紧改连接串。这类问题隐蔽在应用层,排查起来最费时间。
7.2 PDB默认创建后处于MOUNTED状态
新PDB创建后默认MOUNTED,很多人以为是创建流程有问题,各种查log。其实只要执行:
alter pluggable database xx open; alter pluggable database xx save state;就解决了。建议把这两条命令固化到新建PDB的操作清单里,否则下一次还是要踩。
7.3 克隆时忘记将源PDB设为只读
执行克隆命令频繁报ORA-65081(无法从非只读状态克隆)。解决办法:
alter pluggable database salespdb open read only; -- 克隆完成后 alter pluggable database salespdb open read write;注意,如果在克隆期间源PDB要承接业务,那就不能直接只读。这时可以考虑用刷新克隆,或者通过备库做克隆:先把源库相关归档拷贝到备库,再用备库的一致状态做源。
7.4 字符集与时区不一致造成拔插失败
拔插或远程克隆时,源PDB和目标CDB的字符集、时区不匹配,可能在创建阶段报错。所以迁移规划阶段一定要先做兼容性检查:
select value from nls_database_parameters where parameter = 'NLS_CHARACTERSET'; select dbtimezone from dual;如果字符集不一致,最稳妥的办法是先把源PDB的数据导出再导入到目标环境,而不是直接拔插。硬拔插有时表面成功了,但业务数据出现乱码,那是更难解决的事。
7.5 大事务导致UNDO暴涨在多租户下的连锁反应
共享UNDO模式下(未启用本地UNDO),某个PDB跑了一个没有提交的大事务,UNDO段会被撑大,可能直接影响CDB里所有PDB的查询一致性和性能。即便启用了本地UNDO,如果一个CDB里有多个PDB同时对UNDO表空间发起扩展,也可能把磁盘空间吃满。
应对办法包括:监控v$undostat,关注各PDB的UNDO增长速度;限制长事务源头的业务逻辑;必要时给利库PDB单独设置更大的UNDO表空间。这些动作不能靠事后补救,提前监控更重要。
这些经验背后的个人体会
多租户架构本身不难理解,但实际操作里最考验人的是对"共享与隔离"边界的把握。CDB与PDB的关系、UNDO是否独立、日志共享导致的不完全恢复限制、连接串里那一点点服务名差异——每一条看起来很小,一旦在真实环境里出问题,影响面往往比传统架构更大。我个人的建议是:任何操作之前,先确认当前会话在哪个容器,任何PDB级操作先进PDB,任何CDB级操作先想清楚会不会误伤邻居。
最后再分享一个小技巧:写管理脚本时,每个关键动作都先输出show con_name,再在脚本里用alter session set container显式指定目标PDB。别小看这一行,它已经帮我挡住了好几次"脚本跑错库"的事故。