Redo 日志是 Oracle 的命脉,没有 Redo,数据库的持久性和崩溃恢复就无从谈起。但就是这么一个基础机制,很多 DBA 对它的理解停留在"LGWR 写日志"六个字上。我问过几个工作五年的 DBA:LGWR 什么时候触发?Log Buffer 多大合适?Log File Sync 等待高怎么治?十个里有八个答不上来,还有两个答的是错的。这篇文章把 Redo 的生成、缓冲、写入、同步整条链路拆清楚,顺便讲一个我因为调错 LOG_BUFFER 把库搞挂的真实案例。
1.1 Redo 不是日志文件,是内存里的故事
很多新手以为 Redo 就是"操作日志",记录"谁改了哪行数据"。实际上 Redo 记录的是"如何重做这个变更"——不是记录 UPDATE salary=1000,而是记录"把第 5 号数据文件的第 1234 号块的第 3 号槽位的列 2 改成 1000"。这种物理+逻辑混合的格式,让 Recovery 进程可以在崩溃后精确重演变更,而不需要理解业务语义。Redo 记录的最小单位叫 Redo Record,每个 Redo Record 包含多个 Change Vector,每个 Change Vector 对应一个数据块的变更。比如一条 UPDATE 语句,可能产生 3 个 Change Vector:UNDO 块的变更(记录旧值)、数据块的变更(写入新值)、索引块的变更(维护索引)。
这些 Redo Record 首先在 PGA 里组装(服务器进程的私有内存),然后拷贝到 SGA 的 Redo Log Buffer(共享内存)。拷贝过程需要获取 Redo Allocation Latch 和 Redo Copy Latch,这是 Redo 生成路径上的第一个瓶颈。如果并发会话多,Redo Allocation Latch 的争用会很严重,AWR 里表现为 latch: redo allocation 等待事件。我们给一个高并发 OLTP 客户调优时,发现 latch: redo allocation 占了 DB Time 的 8%,后来把 LOG_BUFFER 从 64MB 加到 256MB,争用降到 1% 以下。原理很简单:LOG_BUFFER 大了,每个会话分配的空间更充裕,分配频率降低,latch 争用自然减少。但 LOG_BUFFER 不是越大越好,后面会讲一个反例。
1.2 LGWR 的触发条件:不是每秒写,是事件驱动
LGWR 不是定时器驱动的后台进程,它是事件驱动的。触发 LGWR 写盘的条件有四个,按优先级排序:第一,事务提交(COMMIT)。当一个会话执行 COMMIT 时,它会在 Redo Log Buffer 里生成一个 Commit Record,然后通知 LGWR 把该事务的所有 Redo Record 刷到磁盘。这是最常见的触发原因,也是 Log File Sync 等待的来源。第二,Redo Log Buffer 满三分之一。当 Log Buffer 的使用量超过 1/3 时,LGWR 被唤醒写盘,防止 Buffer 溢出。第三,DBWn 触发 Checkpoint 前。DBWn 写脏块之前,必须确保这些脏块对应的 Redo 已经落盘(WAL 原则),所以 DBWn 会触发 LGWR 写盘。第四,每 3 秒的超时唤醒。即使没有任何事件,LGWR 每 3 秒也会醒来检查一次 Buffer,如果有未写的 Redo,就写盘。这个 3 秒超时是硬编码的,不能改。
注意:LGWR 写盘时,不是只写触发它的那个事务的 Redo,而是把 Redo Log Buffer 里所有未写的 Redo 一起刷盘。这意味着,如果你的系统并发很高,会话 A 的 COMMIT 触发了 LGWR,会话 B、C、D 的未提交事务的 Redo 也会被一起刷盘。这种"搭便车"机制减少了 LGWR 的写盘次数,但也意味着未提交事务的 Redo 可能提前落盘——没关系,Recovery 时会根据 Commit Record 判断哪些变更需要应用。没有 Commit Record 的 Redo,会被忽略。
1.3 Log File Sync:COMMIT 的代价与批量提交
Log File Sync 是 OLTP 系统最常见的等待事件之一,它表示会话从发出 COMMIT 到收到 LGWR 的写盘确认之间的等待时间。这个时间 = LGWR 写盘时间 + OS 同步时间(fsync/fdatasync)+ 网络延迟(如果是 RAC)。在单实例本地盘上,Log File Sync 通常在 1-5ms;在 SAN 存储上,可能 5-15ms;如果存储负载高或者用了 NFS,可能飙到 50ms 以上。
我们有一个电商客户,大促期间 Log File Sync 的 P95 延迟飙到 80ms,订单提交接口超时,用户疯狂点击"提交",导致重复下单。排查发现,存储阵列的 write cache 被备份任务占满,LGWR 的 fdatasync 等待存储确认,延迟暴涨。应急措施:把在线日志文件移到本地 SSD(延迟 <1ms),归档日志留在 SAN。这个方案牺牲了部分可靠性(本地盘没有 RAID),但换来了订单接口的可用性。事后,客户给在线日志配了独立的 RAID10 SSD 组,Log File Sync 稳定在 2ms。
除了硬件优化,应用层的批量提交也能大幅降低 Log File Sync 开销。比如 ETL 任务,每 1000 行提交一次,比每行提交一次,Log File Sync 次数减少 1000 倍。代价是:如果批量提交中途崩溃,这 1000 行需要重新处理。但对于幂等操作(比如 INSERT IGNORE),这不是问题。我们给一个日志采集系统做调优,把提交频率从每 10 行改成每 5000 行,吞吐量从 3000 TPS 提升到 18000 TPS,Log File Sync 的 DB Time 占比从 45% 降到 6%。
1.4 实验:LOG_BUFFER 调优的边界与陷阱
实验环境:Oracle 19c EE,单实例,64G 内存,SATA 盘。测试表 orders,1000 万行,并发 100 会话,每会话循环 INSERT + COMMIT 1000 次。我们测试了 LOG_BUFFER 从 8MB 到 512MB 的梯度,观察吞吐量和等待事件。
-- 查看当前 LOG_BUFFER
SHOW PARAMETER LOG_BUFFER;
-- 默认值:64MB
-- 调整 LOG_BUFFER(需重启实例)
ALTER SYSTEM SET LOG_BUFFER = 256M SCOPE = SPFILE;
STARTUP FORCE;
-- 测试脚本:100 并发 INSERT+COMMIT
DECLARE
BEGIN
FOR i IN 1..1000 LOOP
INSERT INTO orders(order_id, amount, status)
VALUES (seq_orders.NEXTVAL, DBMS_RANDOM.VALUE(100,10000), 'NEW');
COMMIT;
END LOOP;
END;
/
-- 监控等待事件
SELECT event, total_waits, time_waited_micro/1000 time_ms, avg_wait_micro/1000 avg_ms
FROM v$system_event
WHERE event IN ('log file sync', 'log file parallel write', 'latch: redo allocation');
实验结果:LOG_BUFFER=8MB 时,吞吐量 4200 TPS,latch: redo allocation 占 DB Time 的 22%,log file sync avg=12ms。瓶颈在 Redo Allocation Latch,Buffer 太小,频繁分配。LOG_BUFFER=64MB(默认)时,吞吐量 8500 TPS,latch 占 6%,log file sync avg=5ms。LOG_BUFFER=256MB 时,吞吐量 9200 TPS,latch 占 1%,log file sync avg=4.5ms。LOG_BUFFER=512MB 时,吞吐量 9100 TPS(反而降了!),log file sync avg=4.8ms。为什么 512MB 反而降了?因为 LOG_BUFFER 太大,LGWR 每次写盘的数据量也大了,虽然写盘次数减少,但每次写的 I/O 时间变长,而且大 Buffer 的内存管理开销(扫描未写区域)增加了。甜点区在 128-256MB,超过 256MB 边际效应递减。
【踩坑笔记】LOG_BUFFER 超过 256MB 后,性能反而可能下降。而且 11g 以后,Oracle 会自动调整 LOG_BUFFER(基于 CPU 数和内存总量),手动设置过大可能覆盖自动优化的结果。建议:除非 AWR 明确显示 latch: redo allocation 争用高,否则不要手动调 LOG_BUFFER。
1.5 那个把我库搞挂的反例:LOG_BUFFER 设成 1GB
三年前,我接手一个"性能很差"的库,AWR 里 latch: redo allocation 占 15%。我当时年轻气盛,直接把 LOG_BUFFER 从 64MB 改成 1GB,想着"越大越好,一劳永逸"。重启后, latch 争用确实没了,但实例启动时间从 2 分钟变成 20 分钟——因为实例启动时要初始化 1GB 的 Redo Log Buffer,在虚拟化环境(VMware)里,这个大内存分配触发了内存气球回收(ballooning),ESXi 从虚拟机回收了 800MB 内存给其他 VM,导致 SGA 其他组件(Buffer Cache)被压缩,性能暴跌。更惨的是,这个库是 RAC,节点 2 启动时跟节点 1 的 Cache Fusion 同步,因为内存被气球压缩,同步超时,节点 2 被驱逐了三次才起来。最后灰溜溜改回 256MB,发誓再也不瞎调 LOG_BUFFER。
这个教训让我明白:任何参数都有场景假设。LOG_BUFFER 1GB 在物理机、大内存、高并发 OLTP 上可能没问题,但在虚拟化、内存紧张、中等并发的环境上就是灾难。调参之前,先看硬件、再看负载、最后看版本,别照搬网上的"最佳实践"。网上说 LOG_BUFFER 设 1GB 的,他的环境可能是 Exadata,你的环境可能是虚拟机,能一样吗?
1.6 总结:Redo 调优的三层漏斗
Redo 调优不是调一个参数,是三层漏斗:第一层,应用层:减少不必要的 COMMIT,改成批量提交;避免短事务(比如循环里每行都 COMMIT),这是最大的收益来源。第二层,数据库层:LOG_BUFFER 设到甜点区(通常 128-256MB),在线日志文件放在低延迟存储(SSD),日志组大小匹配写入速率(通常 1-2GB)。第三层,OS/存储层:确保 fdatasync 延迟 < 10ms,存储 write cache 不被其他任务(备份、归档)挤占,多路径策略不会导致 I/O 排队。三层都通了,Log File Sync 才能压到 2ms 以内。只调一层,效果有限。最后送一句话:Redo 是数据库的心跳,LGWR 是心电图,Log File Sync 是心率和血压。别等心跳停了才想起看心电图。