

Eclipsys has helped McMaster University maximize its investment in the Oracle Exadata Cloud@Customer solution. Read the story here






There's a specific feeling every DBA knows: the one where you check on a long-running job that should be well underway by now, and the log timestamp hasn't moved in hours. You refresh. You check again, still nothing.
That's how this story started.
We were three days into a 16 TB Oracle physical migration from Exadata Cloud@Customer to Oracle Database@Azure using Zero Downtime Migration (ZDM 21.5). Everything had been running smoothly. RMAN was chugging along, restoring datafiles across a cross-region WAN. Then, silence.
The ZDM job status said EXECUTING. The current phase said ZDM_RESTORE_TGT. The RMAN process was still alive on the target. Datafiles were still being written… except they weren't.
For 35 hours.
This blog covers what happened, why it happened, and, more importantly, how to recover without losing 15 TB of restored data and starting from scratch.
Source: On-premises Exadata Cloud@Customer, 16 TB database (PRODDB_ONP) in primary role
Target: Oracle Database@Azure ExaDB-D in another region (PRODDB_AZ)
Method: ZDM 21.5 physical online migration with DATA_TRANSFER_MEDIUM=DIRECT and ZDM_RMAN_DIRECT_METHOD=RESTORE_FROM_SERVICE
RMAN channels: 10 parallel channels
Network path: Enterprise WAN with firewalls between source datacenter and Azure region
The migration had already restored approximately 15 TB. RMAN was working on the tail end with a few large datafiles remaining. Then activity died.
The ZDM job log had gone quiet. But before assuming ZDM was hung, we needed to understand what was actually happening at every layer.
[zdmuser@zdmhost01 ~]$ zdmcli query job -jobid 101zdmhost01.corp.example.com: Audit ID: 1031588Job ID: 101User: zdmuserJob Type: "MIGRATE"Current status: EXECUTINGCurrent Phase: "ZDM_RESTORE_TGT"Result file path: "/apps/zdm/zdmbase/chkbase/scheduled/job-101-2026-08-21-11:55:12.log"Job execution start time: 2026-08-21 11:55:12ZDM_GET_SRC_INFO .............. COMPLETEDZDM_GET_TGT_INFO .............. COMPLETEDZDM_PRECHECKS_SRC ............. COMPLETEDZDM_PRECHECKS_TGT ............. COMPLETEDZDM_SETUP_SRC ................. COMPLETEDZDM_SETUP_TGT ................. COMPLETEDZDM_VALIDATE_SRC .............. COMPLETEDZDM_VALIDATE_TGT .............. COMPLETEDZDM_DISCOVER_SRC .............. COMPLETEDZDM_COPYFILES ................. COMPLETEDZDM_PREPARE_TGT ............... COMPLETEDZDM_SETUP_TDE_TGT ............. COMPLETEDZDM_RESTORE_TGT ............... STARTEDZDM_RECOVER_TGT ............... PENDINGZDM_FINALIZE_TGT .............. PENDINGZDM_CONFIGURE_DG_SRC .......... PENDING |
ZDM believed it was still working. That was misleading.
On the target host, we mapped out the process tree:
[oracle@azexadb-node1 ~]$ pstree -p 55676perl(55676)-+-perl(60022) `-sh(60023)---su(60024)---rman(60025)-+-oracle_60241_prd(60241) |-oracle_60808_prd(60808) |-oracle_61124_prd(61124) |-oracle_61608_prd(61608) |-oracle_61854_prd(61854) |-oracle_62146_prd(62146) |-oracle_62573_prd(62573) |-oracle_62616_prd(62616) |-oracle_62666_prd(62666) `-oracle_62738_prd(62738) |
RMAN was alive. Ten channel processes were alive. Everything looked normal at the process level.
This is where the picture changed. We queried the target's session view:
SQL> SELECT sid, event, seconds_in_wait, state 2 FROM gv$session 3 WHERE program LIKE '%rman%' 4 ORDER BY seconds_in_wait DESC; SID EVENT SECONDS_IN_WAIT STATE------ ------------------------------- ---------------- -------- 7 remote db file read 127642 WAITING 264 remote db file read 128265 WAITING 265 remote db file read 127669 WAITING 525 remote db file read 127634 WAITING 781 remote db file read 127980 WAITING 1941 remote db file read 127977 WAITING 6 SQL*Net message from client 4597 WAITING 8 SQL*Net message from client 4561 WAITING 1040 SQL*Net message from client 4567 WAITING 1295 SQL*Net message from client 4600 WAITING 10 rows selected. |
The output was ominous.
127,642 seconds = 35 hours and 27 minutes.
Six RMAN channels had been waiting on remote db file read for over 35 hours. The wait event remote db file read is what RMAN reports when a channel is reading data from a source database over SQL*Net — it's the source-server-side equivalent of physical reads for a RESTORE FROM SERVICE operation.
The four other channels were sitting on SQL*Net message from client — the RMAN coordinator's polite way of saying "I finished my work, waiting for the boss to give me more." But the boss couldn't hand out new work until the stuck channels either completed or failed.
Here's where things got interesting. We connected to the source database and asked the reverse question: how many sessions are currently connected from the target?
[oracle@onpexa-node1 ~]$ sqlplus / as sysdba SQL> SELECT COUNT(*) 2 FROM v$session 3 WHERE machine LIKE '%azexadb%'; COUNT(*)---------- 0 SQL> |
The source had zero sessions from the target. But the target had six sessions actively waiting to receive data from the source. That's not a paradox; it's a definitive signature of a well-known problem.
Somewhere in the network path between the source and target, an intermediate device (a firewall, a NAT gateway, some stateful network appliance) silently terminated the TCP connections between RMAN and the source database.
Here's how it played out:
RMAN opened 10 TCP connections to the source database's listener.
Restore work flowed normally for hours.
At some point, a network device decided one or more connections had been idle too long, their state was purged from its tracking table, or some other opaque decision was made.
The connection was silently dropped.
Only one side of the conversation noticed.
The source database noticed because it had SQLNET.EXPIRE_TIME=10 set. This makes the Oracle server send periodic dead-connection-detection probes to clients. When those probes failed (because the connection was actually dead), the source cleanly terminated its side and released the session, which is why the source query showed zero sessions.
The target's RMAN process, however, was blocked on a socket read, waiting for data that would never come. And the target had no equivalent client-side detection mechanism kicking in aggressively enough. So it just waited. And waited… for 35 hours. And would have waited forever.
This is called a "half-open" TCP connection. One side is closed, the other side thinks it's still open. The application on the alive side is stuck waiting for the dead side to say something.
The Linux TCP keepalive mechanism was supposed to detect this eventually, but the defaults are cruel:
[oracle@azexadb-node1 ~]$ sysctl net.ipv4.tcp_keepalive_timenet.ipv4.tcp_keepalive_time = 7200 [oracle@azexadb-node1 ~]$ sysctl net.ipv4.tcp_keepalive_intvlnet.ipv4.tcp_keepalive_intvl = 75 [oracle@azexadb-node1 ~]$ sysctl net.ipv4.tcp_keepalive_probesnet.ipv4.tcp_keepalive_probes = 9 |
That's 2 hours of idle time before keepalive even starts probing, then 9 probes at 75-second intervals. Detection time in the best case: about 2 hours and 11 minutes. In the worst case, where an intermediate device is happily acknowledging keepalive probes on behalf of the vanished endpoint, never. However, we never wanted to change these parameters on either EXADATA system without proper consent from Oracle Support.
For a WAN migration running over an enterprise firewall, that's essentially useless.
The obvious impulse is to abort the ZDM job and start over. But when you're 15 TB into a 16 TB restore, that impulse costs you three days of migration time. So before going nuclear, we needed to understand what safer options existed.
Two ideas worth trying before abort:
The critical insight is that in both cases, the already-restored data on the target remains intact. The target's ASM has all the datafiles that were successfully written. Only RMAN's in-memory state and the stuck connections need cleanup.
But before killing anything, we needed absolute certainty about which RMAN process to kill. On a shared Exadata host with multiple databases, other RMAN processes could be running backups. Killing the wrong one would be very bad.
If we were going to escalate to killing RMAN, we needed forensic-level certainty. Below is the identification approach.
[oracle@azexadb-node1 ~]$ ps -ef | grep "mZDM_oss_standby_clone_tgt" \ | grep "zdm_PRODDB_AZ_101" | grep -v grep root 55676 55675 0 Aug21 ? perl mZDM_oss_standby_clone_tgt -sdbname PRODDB_ONP -tdbname PRODDB_AZ -dbid 464464767 -scn 148074410446 -rmannumchannels 10 -operation restoreDBroot 60022 55676 0 Aug21 ? perl mZDM_oss_standby_clone_tgt (helper) |
This shows the exact tree spawned by ZDM. The RMAN process will be at the bottom, unambiguously parented by ZDM's sudo/su chain.
SQL> SELECT p.spid AS rman_pid, s.sid, s.serial#, s.event 2 FROM gv$session s 3 JOIN gv$process p ON p.addr = s.paddr AND p.inst_id = s.inst_id 4 WHERE s.program LIKE '%rman%' 5 ORDER BY p.spid; RMAN_PID SID SERIAL# EVENT--------- ----- -------- --------------------------------60241 1295 15071 SQL*Net message from client60808 1683 46042 SQL*Net message from client61124 6 48336 SQL*Net message from client61608 7 4723 remote db file read61854 1941 7420 remote db file read62146 2201 48189 SQL*Net message from client62573 264 3036 remote db file read62616 525 59586 remote db file read62666 781 61252 remote db file read62670 1040 9474 SQL*Net message from client62692 1298 7265 SQL*Net message from client62730 8 48372 SQL*Net message from client62738 265 3244 remote db file read 13 rows selected. |
The SPID values from this query are the OS process IDs of the RMAN channel processes. If they match the children of the RMAN parent PID from pstree, you have bulletproof confirmation.
[oracle@azexadb-node1 ~]$ ps -ef | grep "rman" | grep "dbhome" | grep -v greporacle 60025 60024 0 Aug21 ? 00:03:11 rman target / |
If only one RMAN process exists on the host (as was the case for us), the identification is trivial. If multiple exist (typical for hosts hosting backup jobs for other databases), each needs to be traced back to its parent to confirm which one is ZDM's.
We spent 15 minutes verifying the correct PID before touching anything. That's cheap insurance.
This is where the story turned. We ran:
[zdmuser@zdmhost01 ~]$ zdmcli suspend job -jobid 101zdmhost01.corp.example.com: Audit ID: 1031602Operation "zdmcli suspend job" is complete. |
Within about 10 minutes, the job status changed:
[zdmuser@zdmhost01 ~]$ zdmcli query job -jobid 101zdmhost01.corp.example.com: Audit ID: 1031603Job ID: 101Current status: SUSPENDEDCurrent Phase: "ZDM_RESTORE_TGT" |
What happened under the hood: ZDM sent a signal that eventually made RMAN terminate. When RMAN died, its stuck socket reads unblocked (with errors). The stuck connections were torn down cleanly. Everything paused at a checkpoint.
[zdmuser@zdmhost01 ~]$ zdmcli resume job -jobid 101zdmhost01.corp.example.com: Audit ID: 1031604Operation "zdmcli resume job" scheduled with the job ID 101. |
Here's where it got beautiful. ZDM's resume didn't restart the restore from scratch. Instead, it triggered an incremental restore to catch up on the delta since the initial snapshot SCN, established fresh TCP connections that benefited from the new keepalive settings, and completed sync in a fraction of the time a full restore would have taken.
Watching the target session activity confirmed things were working correctly:
SQL> SELECT sid, event, seconds_in_wait 2 FROM gv$session 3 WHERE program LIKE '%rman%' 4 ORDER BY seconds_in_wait DESC; SID EVENT SECONDS_IN_WAIT------ ------------------------------ --------------- 550 SQL*Net more data to client 0 603 SQL*Net more data to client 0 596 SQL*Net more data to client 1 2434 SQL*Net more data to client 0 2872 SQL*Net more data to client 0 1151 SQL*Net message from client 23 105 SQL*Net message from client 21 126 SQL*Net message from client 20 2950 SQL*Net message from client 10 1910 SQL*Net message from client 9 1084 SQL*Net message from client 2 |
Compare those wait times to what we saw before. Instead of 127,000+ seconds stuck on one event, we saw 0-25 seconds cycling through work. Channels were actively transferring data. This is what a healthy RMAN restore looks like.
Within a few hours, source and target were in sync. DG apply lag: zero. The 35-hour stall was recovered without losing 15 TB of prior work.
The zdmcli suspend command proved more capable than expected. It's not just a checkpoint marker; it actually forces the current RMAN operation to terminate cleanly. This is exactly what was needed to break the deadlock.
Combined with resume, which understands the incremental restore case, this pattern gives you a real recovery option that sits between "wait forever" and "abort and lose everything."
If suspend hadn't worked, the fallback plan was to manually kill the RMAN process (identified via the paranoid procedure above), let ZDM detect the failure, then resume from the failed phase- same eventual outcome, more manual work.
SQLNET.EXPIRE_TIME on the source (server side) helped the source clean up dead sessions. But there's no direct client-side equivalent that RMAN uses. For the target (client) to detect dead connections, you need aggressive OS TCP keepalive settings.
Set both:
On both source and target: SQLNET.EXPIRE_TIME=10 (or lower) in sqlnet.ora, then reload the listener
On both source and target OS: aggressive tcp_keepalive_* settings via sysctl
Firewalls maintain connection state tables with finite entries and timeouts. Long-lived Oracle SQL*Net connections that transfer data in bursts (like RMAN restore of a large section) can look "idle" enough to be culled. Worse, some devices continue ACKing keepalive probes on behalf of dropped connections, defeating the detection mechanism entirely.
Get your network team involved early. Ask specifically:
What is the TCP idle timeout on firewalls in the path?
Can it be extended for the duration of the migration window?
Are there any devices proxying/state-preserving connections that might interfere with keepalive detection?
The single most useful query when RMAN restore appears stuck:
-- On target - are RMAN channels stuck?SELECT sid, event, seconds_in_waitFROM gv$sessionWHERE program LIKE '%rman%' AND event = 'remote db file read' AND seconds_in_wait > 300ORDER BY seconds_in_wait DESC; -- On source - do sessions from target actually exist?SELECT COUNT(*)FROM v$sessionWHERE machine LIKE '%<target_host_pattern>%'; |
Long remote db file read waits on target + zero corresponding sessions on source = half-open TCP. That's the signature.
If you find yourself with a stuck ZDM restore, remember the recovery ladder:
Diagnose: confirm it's actually stuck and understand why.
Try zdmcli suspend job: surprisingly effective for breaking stuck states.
zdmcli resume job: takes advantage of incremental restore.
Kill RMAN manually (if suspend fails): but identify the process with paranoid care.
Abort: only as a last resort, after everything else has failed.
The difference between suspend/resume and abort/restart on a 16 TB migration is roughly three days of clock time. That's worth trying suspend even if you're not sure it'll work.
We set ZDM_RMAN_SECTION_SIZE=16G (down from default 64G). Smaller sections mean shorter individual RMAN operations, so any single stall affects less work. But this is mitigation, not a fix. The underlying network problem needs to be addressed at the network layer.
Watching a 35-hour stall on a critical production migration is genuinely stressful. There's a temptation to do something, anything, to stop the sinking feeling.
Resist that temptation.
Take an hour to diagnose the problem. Cross-reference every data point you can. Verify assumptions before acting. The extra hour spent understanding what's happening is trivial compared to the days you could lose making the wrong recovery choice.
When we finally acted, we knew exactly:
Which RMAN PID to kill if needed
What state the data was in
What suspend would do (in theory)
What our fallback plans were
Why the problem happened
That confidence made the recovery decision easy. Suspend, wait, resume, monitor. The system did the rest.
If I were starting this migration over from scratch:
Set OS TCP keepalive on all nodes before beginning any migration, not after the first stall.
cat > /etc/sysctl.d/99-tcp-keepalive.conf <<'EOF'net.ipv4.tcp_keepalive_time = 120net.ipv4.tcp_keepalive_intvl = 30net.ipv4.tcp_keepalive_probes = 5EOFsysctl -p /etc/sysctl.d/99-tcp-keepalive.conf |
Engage the network team upfront. Get firewall timeouts documented and extended before RMAN starts.
Set ZDM_RMAN_SECTION_SIZE=16G or 8G by default. It reduces the blast radius of any single stall.
Test suspend/resume in a lower environment, so you know how it behaves when you need it.
Have the diagnostic queries ready. Script them, don't type them from memory at 2 AM.
This particular failure mode isn't a ZDM bug or an Oracle bug. It's the natural result of running long-lived TCP connections across enterprise WAN infrastructure that was never designed to hold connections open for days at a time. It's a networking problem masquerading as a database problem.
The good news is that both Oracle and ZDM give you the tools to detect it, understand it, and recover. You just need to know where to look and what to trust.
Suspend/resume saved this migration. Adequate keepalive settings would have prevented the stall entirely. And network-layer changes would eliminate the root cause forever.
FIf you're planning a large cross-region Oracle migration over enterprise WAN infrastructure, do your future self a favor: don't rely on Oracle's server-side dead-connection detection alone. Configure aggressive OS TCP keepalives. Talk to your network team. Set section sizes conservatively. And know your recovery commands cold, before you need them.
For more information contact us today and one of our experts will be in touch.
