qzsee opened a new pull request, #69642:
URL: https://github.com/apache/doris/pull/69642

   ### What problem does this PR solve?
   
   Issue Number: close #xxx
   
   Related PR: None
   
   Problem Summary:
   
   A running FE runs bdbje `NetworkRestore` concurrently from several threads 
into the same bdb directory. The restores corrupt the log files, and the FE can 
never start again until the bdb directory is rebuilt by hand.
   
   #### How it happens
   
   The problem was hit on an observer during a rolling upgrade.
   
   1. The old master sends some txns to the observer and is then stopped for 
upgrade before a majority of electable nodes acks these txns. An observer 
(`SECONDARY`) never acks, so it is not counted in the majority. Now the 
observer has txns that the new master does not have.
   2. While there is no master, the cleaner of the observer keeps running, and 
a checkpoint with cleaned files is written after these txns.
   3. A follower is elected as the new master. The observer syncs up with it 
and has to roll back the extra txns. The matchpoint is before a checkpoint end, 
so bdbje can not roll back in place. The replica thread invalidates the 
environment with `InsufficientLogException`:
      ```
      matchpointVLSN of xxx precedes a checkpoint end, needs network restore.
      InsufficientLogException ... at ReplicaFeederSyncup.verifyRollback(...)
      ```
      Up to here this is expected bdbje behavior.
   4. From now on, **every** thread that touches the invalidated environment 
gets the same `InsufficientLogException`. Three places run `NetworkRestore` 
inside the running process when they catch it:
      - `BDBJEJournal.getDatabaseNames()` -> `reSetupBdbEnvironment()`. It is 
reached through `getMaxJournalId()`, which is called by the replayer thread and 
also by http / rpc / metric threads (e.g. `HaController`, 
`FrontendServiceImpl`, `MetricRepo`).
      - `BDBJEJournal.open()` -> `reSetupBdbEnvironment()`.
      - The `catch (InsufficientLogException)` of the replayer thread in `Env`.
   
      As a result, several restores run at the same time on the same directory 
(in the case above, two restores started within one second):
      ```
      Started network restore      <- restore 1
      Started network restore      <- restore 2
      Rename from: xxx.jdb.tmp to xxx.jdb failed  UNEXPECTED_STATE
      Missing file: xxx.jdb                        UNEXPECTED_STATE
      ```
   5. Why concurrent restores corrupt the directory:
      - Each restore picks its own log provider. The `.jdb` files of different 
nodes are independent, so files with the same name have different contents on 
different nodes.
      - `setRetainLogFiles(false)` deletes every local file that is not in the 
file list of the chosen provider. One restore therefore deletes or overwrites 
the `.tmp` and `.jdb` files that the other restore just downloaded.
      - The invalidated environment is not closed during the restore, and its 
bdbje daemon threads are still alive.
   
      The directory ends up as a mix of files from different nodes, and some 
referenced files are missing.
   6. On the next start, recovery fails with `EnvironmentFailureException ... 
LOG_FILE_NOT_FOUND` (or `LOG_CHECKSUM` if someone copies a single `.jdb` file 
from another node). This is **not** an `InsufficientLogException`, so `setup()` 
never triggers a restore again. The FE keeps failing to start until the bdb 
directory is rebuilt by hand.
   
   There is also a minor issue: if `setup()` still gets 
`InsufficientLogException` in its last retry, the loop ends and `setup()` 
returns with a `null` environment.
   
   #### How to fix
   
   The idea: **never run `NetworkRestore` in a running process. Exit, and 
restore once, single-threaded, while opening the environment at startup.**
   
   1. Add `BDBEnvironment.NETWORK_RESTORE_LOCK`.
   2. `BDBEnvironment.setup()` is the only place that runs `NetworkRestore`. 
When opening the environment throws `InsufficientLogException`, it first 
`close()`s the environment, then runs the restore while holding the lock, then 
retries the open. This is safe because the environment failed to open: there is 
no environment handle and no bdbje daemon thread, and no other thread uses the 
environment yet.
   3. Add `BDBEnvironment.exitForNetworkRestore()`. While holding the lock, it 
logs the reason and the thread name and exits the process. The three runtime 
places above now call it instead of running a restore:
      - only the first thread exits;
      - the other threads block on the lock until the process is gone;
      - the in-memory meta may already contain replayed journals that are now 
rolled back, so it has to be rebuilt anyway.
   4. After restart, opening the environment throws `InsufficientLogException` 
again, because bdbje persists the "restore required" state in the log. 
`setup()` then restores in a clean state. This is verified with a real 
replication group in the new UT.
   5. If `setup()` still can not open the environment after all retries, it 
exits instead of returning a `null` environment.
   6. `exitHook` (default `System::exit`) is added only so that UTs can replace 
it.
   
   `reSetupBdbEnvironment()` and the in-process `NetworkRestore` calls in 
`BDBJEJournal` and `Env` are removed.
   
   #### Tests
   
   - `BDBNetworkRestoreTest` (mocked):
     - 8 threads (replayer / http / rpc / metric) call `getMaxJournalId()` on 
an invalidated environment at the same time. Asserts: no `NetworkRestore` runs, 
exactly one thread exits while holding the lock, and the other 7 threads are 
blocked on `NETWORK_RESTORE_LOCK` (checked with `ThreadMXBean`).
     - When opening the environment throws `InsufficientLogException`, 
`setup()` runs `NetworkRestore` exactly once, with the lock held and the 
environment closed, and then opens a usable environment.
     - If the environment still can not be opened after all retries, `setup()` 
exits.
   - `BDBNetworkRestoreClusterTest` (a real 4-node bdbje group: 3 electable 
nodes + 1 secondary, no mocked exception):
     1. All nodes are in sync.
     2. The old master writes txns that only the observer receives.
     3. The observer cleans log files and makes a checkpoint, then the old 
master stops. The observer **keeps running**.
     4. The other two followers elect a new master, which writes new txns.
     5. The running observer is invalidated by its own replica thread. 
`je.info` contains `precedes a checkpoint end, needs network restore`.
     6. 8 FE threads call the real `BDBJEJournal.getMaxJournalId()` 
concurrently. Asserts: no restore runs, exactly one thread exits, and `je.info` 
has no `Started network restore` / `Missing file:` / `Rename from:` / 
`LOG_FILE_NOT_FOUND` / `LOG_CHECKSUM`.
     7. "Restart": opening the observer without a restore throws 
`InsufficientLogException` again. `setup()` then runs the **real** 
`NetworkRestore` exactly once. Afterwards the observer has the same data as the 
new master: the uncommitted txns are rolled back, the new master's txns are 
present, and `je.info` contains no corruption messages.
   
     The exception stack in step 5 (`ReplicaFeederSyncup.verifyRollback` -> 
`setupLogRefresh`) is the same as the one in the production case.
   
   #### Note for a node that is already corrupted
   
   This PR can not repair a directory that was already corrupted by concurrent 
restores. To recover such a node:
   1. Move `meta/bdb` away and keep `meta/image`.
   2. Start the FE with this fix. Opening the empty environment triggers a 
single restore from another member.
   3. Alternatively, drop the node and add it again.
   
   ### Release note
   
   Fix the bdb directory of an FE (usually an observer) being corrupted by 
concurrent bdbje NetworkRestore after the master changed. Before this fix, the 
FE could not start again until its bdb directory was rebuilt.
   
   ### Check List (For Author)
   
   - Test
       - [ ] Regression test
       - [x] Unit Test
       - [ ] Manual test (add detailed scripts or steps below)
       - [ ] No need to test or manual test.
   
   - Behavior changed:
       - [ ] No.
       - [x] Yes. When the bdb environment of a running FE gets 
`InsufficientLogException`, the FE now exits immediately and restores the log 
files the next time it starts. Before, it ran `NetworkRestore` inside the 
running process. The FE needs to be restarted by its supervisor (or manually), 
which is the same as the old replayer path, which also exited after the restore.
   
   - Does this need documentation?
       - [x] No.
       - [ ] Yes.
   
   ### Check List (For Reviewer who merge this PR)
   
   - [ ] Confirm the release note
   - [ ] Confirm test coverage
   - [ ] Confirm behavior changed
   - [ ] Confirm whether need to document
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to