001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018package org.apache.hadoop.hbase.backup.replication; 019 020import com.google.errorprone.annotations.RestrictedApi; 021import java.io.FileNotFoundException; 022import java.io.IOException; 023import java.io.UncheckedIOException; 024import java.util.List; 025import java.util.Map; 026import java.util.UUID; 027import java.util.concurrent.ConcurrentHashMap; 028import java.util.concurrent.TimeUnit; 029import java.util.concurrent.locks.ReentrantLock; 030import java.util.stream.Collectors; 031import org.apache.hadoop.conf.Configuration; 032import org.apache.hadoop.fs.FileStatus; 033import org.apache.hadoop.fs.FileSystem; 034import org.apache.hadoop.fs.FileUtil; 035import org.apache.hadoop.fs.Path; 036import org.apache.hadoop.hbase.HBaseConfiguration; 037import org.apache.hadoop.hbase.HConstants; 038import org.apache.hadoop.hbase.backup.impl.BackupSystemTable; 039import org.apache.hadoop.hbase.backup.util.BackupFileSystemManager; 040import org.apache.hadoop.hbase.backup.util.BackupUtils; 041import org.apache.hadoop.hbase.backup.util.BulkLoadProcessor; 042import org.apache.hadoop.hbase.client.Connection; 043import org.apache.hadoop.hbase.client.ConnectionFactory; 044import org.apache.hadoop.hbase.io.asyncfs.monitor.StreamSlowMonitor; 045import org.apache.hadoop.hbase.regionserver.wal.WALUtil; 046import org.apache.hadoop.hbase.replication.BaseReplicationEndpoint; 047import org.apache.hadoop.hbase.replication.regionserver.ReplicationSourceInterface; 048import org.apache.hadoop.hbase.util.CommonFSUtils; 049import org.apache.hadoop.hbase.util.EnvironmentEdgeManager; 050import org.apache.hadoop.hbase.wal.FSHLogProvider; 051import org.apache.hadoop.hbase.wal.WAL; 052import org.apache.yetus.audience.InterfaceAudience; 053import org.slf4j.Logger; 054import org.slf4j.LoggerFactory; 055 056/** 057 * ContinuousBackupReplicationEndpoint is responsible for replicating WAL entries to a backup 058 * storage. It organizes WAL entries by day and periodically flushes the data, ensuring that WAL 059 * files do not exceed the configured size. The class includes mechanisms for handling the WAL 060 * files, performing bulk load backups, and ensuring that the replication process is safe. 061 */ 062@InterfaceAudience.Private 063public class ContinuousBackupReplicationEndpoint extends BaseReplicationEndpoint { 064 private static final Logger LOG = 065 LoggerFactory.getLogger(ContinuousBackupReplicationEndpoint.class); 066 public static final String CONF_PEER_UUID = "hbase.backup.wal.replication.peerUUID"; 067 public static final String CONF_BACKUP_ROOT_DIR = "hbase.backup.root.dir"; 068 069 private final Map<Long, FSHLogProvider.Writer> walWriters = new ConcurrentHashMap<>(); 070 private final ReentrantLock lock = new ReentrantLock(); 071 072 private ReplicationSourceInterface replicationSource; 073 private Configuration conf; 074 private BackupFileSystemManager backupFileSystemManager; 075 private UUID peerUUID; 076 private String peerId; 077 078 private long latestWALEntryTimestamp = -1L; 079 080 public static final long ONE_DAY_IN_MILLISECONDS = TimeUnit.DAYS.toMillis(1); 081 public static final String WAL_FILE_PREFIX = "wal_file."; 082 083 @Override 084 public void init(Context context) throws IOException { 085 super.init(context); 086 this.replicationSource = context.getReplicationSource(); 087 this.peerId = context.getPeerId(); 088 this.conf = HBaseConfiguration.create(context.getConfiguration()); 089 090 initializePeerUUID(); 091 initializeBackupFileSystemManager(); 092 LOG.info("{} Initialization complete", Utils.logPeerId(peerId)); 093 } 094 095 private void initializePeerUUID() throws IOException { 096 String peerUUIDStr = conf.get(CONF_PEER_UUID); 097 if (peerUUIDStr == null || peerUUIDStr.isEmpty()) { 098 throw new IOException("Peer UUID is not specified. Please configure " + CONF_PEER_UUID); 099 } 100 try { 101 this.peerUUID = UUID.fromString(peerUUIDStr); 102 LOG.info("{} Peer UUID initialized to {}", Utils.logPeerId(peerId), peerUUID); 103 } catch (IllegalArgumentException e) { 104 throw new IOException("Invalid Peer UUID format: " + peerUUIDStr, e); 105 } 106 } 107 108 private void initializeBackupFileSystemManager() throws IOException { 109 String backupRootDir = conf.get(CONF_BACKUP_ROOT_DIR); 110 if (backupRootDir == null || backupRootDir.isEmpty()) { 111 throw new IOException( 112 "Backup root directory is not specified. Configure " + CONF_BACKUP_ROOT_DIR); 113 } 114 115 try { 116 this.backupFileSystemManager = new BackupFileSystemManager(peerId, conf, backupRootDir); 117 LOG.info("{} BackupFileSystemManager initialized successfully for {}", 118 Utils.logPeerId(peerId), backupRootDir); 119 } catch (IOException e) { 120 throw new IOException("Failed to initialize BackupFileSystemManager", e); 121 } 122 } 123 124 private void flushWriters() throws IOException { 125 LOG.info("{} Flushing {} WAL writers", Utils.logPeerId(peerId), walWriters.size()); 126 for (Map.Entry<Long, FSHLogProvider.Writer> entry : walWriters.entrySet()) { 127 FSHLogProvider.Writer writer = entry.getValue(); 128 if (writer != null) { 129 LOG.debug("{} Closing WAL writer for day: {}", Utils.logPeerId(peerId), entry.getKey()); 130 try { 131 writer.close(); 132 LOG.debug("{} Successfully closed WAL writer for day: {}", Utils.logPeerId(peerId), 133 entry.getKey()); 134 } catch (IOException e) { 135 LOG.error("{} Failed to close WAL writer for day: {}. Error: {}", Utils.logPeerId(peerId), 136 entry.getKey(), e.getMessage(), e); 137 throw e; 138 } 139 } 140 } 141 walWriters.clear(); 142 143 // All received WAL entries have been flushed and persisted successfully. 144 // At this point, it's safe to record the latest replicated timestamp, 145 // as we are guaranteed that all entries up to that timestamp are durably stored. 146 // This checkpoint is essential for enabling consistent Point-in-Time Restore (PITR). 147 updateLastReplicatedTimestampForContinuousBackup(); 148 149 LOG.info("{} WAL writers flushed and cleared", Utils.logPeerId(peerId)); 150 } 151 152 @Override 153 public UUID getPeerUUID() { 154 return peerUUID; 155 } 156 157 @Override 158 public void start() { 159 LOG.info("{} Starting ContinuousBackupReplicationEndpoint", Utils.logPeerId(peerId)); 160 startAsync(); 161 } 162 163 @Override 164 protected void doStart() { 165 LOG.info("{} ContinuousBackupReplicationEndpoint started successfully.", 166 Utils.logPeerId(peerId)); 167 notifyStarted(); 168 } 169 170 @Override 171 public boolean replicate(ReplicateContext replicateContext) { 172 final List<WAL.Entry> entries = replicateContext.getEntries(); 173 if (entries.isEmpty()) { 174 LOG.debug("{} No WAL entries to replicate", Utils.logPeerId(peerId)); 175 return false; 176 } 177 178 LOG.debug("{} Received {} WAL entries for replication", Utils.logPeerId(peerId), 179 entries.size()); 180 181 Map<Long, List<WAL.Entry>> groupedEntries = groupEntriesByDay(entries); 182 LOG.debug("{} Grouped WAL entries by day: {}", Utils.logPeerId(peerId), 183 groupedEntries.keySet()); 184 185 lock.lock(); 186 try { 187 for (Map.Entry<Long, List<WAL.Entry>> entry : groupedEntries.entrySet()) { 188 LOG.debug("{} Backing up {} WAL entries for day {}", Utils.logPeerId(peerId), 189 entry.getValue().size(), entry.getKey()); 190 backupWalEntries(entry.getKey(), entry.getValue()); 191 } 192 193 // Capture the timestamp of the last WAL entry processed. This is used as the replication 194 // checkpoint so that point-in-time restores know the latest consistent time up to which 195 // replication has 196 // occurred. 197 latestWALEntryTimestamp = entries.get(entries.size() - 1).getKey().getWriteTime(); 198 199 LOG.debug("{} Replication submitted successfully", Utils.logPeerId(peerId)); 200 return true; 201 } catch (IOException e) { 202 LOG.error("{} Replication failed. Error details: {}", Utils.logPeerId(peerId), e.getMessage(), 203 e); 204 return false; 205 } finally { 206 lock.unlock(); 207 } 208 } 209 210 /** 211 * Persists the latest replicated WAL entry timestamp in the backup system table. This checkpoint 212 * is critical for Continuous Backup and Point-in-Time Restore (PITR) to ensure restore operations 213 * only go up to a known safe point. The value is stored per region server using its ServerName as 214 * the key. 215 * @throws IOException if the checkpoint update fails 216 */ 217 private void updateLastReplicatedTimestampForContinuousBackup() throws IOException { 218 try (final Connection conn = ConnectionFactory.createConnection(conf); 219 BackupSystemTable backupSystemTable = new BackupSystemTable(conn)) { 220 backupSystemTable.updateBackupCheckpointTimestamp(replicationSource.getServerWALsBelongTo(), 221 latestWALEntryTimestamp); 222 } 223 } 224 225 private Map<Long, List<WAL.Entry>> groupEntriesByDay(List<WAL.Entry> entries) { 226 return entries.stream().collect( 227 Collectors.groupingBy(entry -> (entry.getKey().getWriteTime() / ONE_DAY_IN_MILLISECONDS) 228 * ONE_DAY_IN_MILLISECONDS)); 229 } 230 231 private void backupWalEntries(long day, List<WAL.Entry> walEntries) throws IOException { 232 LOG.debug("{} Starting backup of {} WAL entries for day {}", Utils.logPeerId(peerId), 233 walEntries.size(), day); 234 235 try { 236 FSHLogProvider.Writer walWriter = walWriters.computeIfAbsent(day, this::createWalWriter); 237 238 for (WAL.Entry entry : walEntries) { 239 walWriter.append(entry); 240 } 241 242 walWriter.sync(true); 243 } catch (UncheckedIOException e) { 244 String errorMsg = Utils.logPeerId(peerId) + " Failed to get or create WAL Writer for " + day; 245 LOG.error("{} Backup failed for day {}. Error: {}", Utils.logPeerId(peerId), day, 246 e.getMessage(), e); 247 throw new IOException(errorMsg, e); 248 } 249 250 List<Path> bulkLoadFiles = BulkLoadProcessor.processBulkLoadFiles(walEntries); 251 252 if (LOG.isTraceEnabled()) { 253 LOG.trace("{} Processed {} bulk load files for WAL entries", Utils.logPeerId(peerId), 254 bulkLoadFiles.size()); 255 LOG.trace("{} Bulk load files: {}", Utils.logPeerId(peerId), 256 bulkLoadFiles.stream().map(Path::toString).collect(Collectors.joining(", "))); 257 } 258 259 uploadBulkLoadFiles(day, bulkLoadFiles); 260 } 261 262 private FSHLogProvider.Writer createWalWriter(long dayInMillis) { 263 String dayDirectoryName = BackupUtils.formatToDateString(dayInMillis); 264 265 FileSystem fs = backupFileSystemManager.getBackupFs(); 266 Path walsDir = backupFileSystemManager.getWalsDir(); 267 268 try { 269 // Create a directory for the day 270 Path dayDir = new Path(walsDir, dayDirectoryName); 271 fs.mkdirs(dayDir); 272 273 // Generate a unique WAL file name 274 long currentTime = EnvironmentEdgeManager.getDelegate().currentTime(); 275 String walFileName = WAL_FILE_PREFIX + currentTime + "." + UUID.randomUUID(); 276 Path walFilePath = new Path(dayDir, walFileName); 277 278 // Initialize the WAL writer 279 FSHLogProvider.Writer writer = 280 ObjectStoreProtobufWalWriter.class.getDeclaredConstructor().newInstance(); 281 writer.init(fs, walFilePath, conf, true, WALUtil.getWALBlockSize(conf, fs, walFilePath), 282 StreamSlowMonitor.create(conf, walFileName)); 283 284 LOG.info("{} WAL writer created: {}", Utils.logPeerId(peerId), walFilePath); 285 return writer; 286 } catch (Exception e) { 287 throw new UncheckedIOException( 288 Utils.logPeerId(peerId) + " Failed to initialize WAL Writer for day: " + dayDirectoryName, 289 new IOException(e)); 290 } 291 } 292 293 @Override 294 public void stop() { 295 LOG.info("{} Stopping ContinuousBackupReplicationEndpoint...", Utils.logPeerId(peerId)); 296 stopAsync(); 297 } 298 299 @Override 300 protected void doStop() { 301 close(); 302 LOG.info("{} ContinuousBackupReplicationEndpoint stopped successfully.", 303 Utils.logPeerId(peerId)); 304 notifyStopped(); 305 } 306 307 private void close() { 308 LOG.info("{} Closing WAL replication component...", Utils.logPeerId(peerId)); 309 lock.lock(); 310 try { 311 flushWriters(); 312 } catch (IOException e) { 313 LOG.error("{} Failed to Flush Open Wal Writers: {}", Utils.logPeerId(peerId), e.getMessage(), 314 e); 315 } finally { 316 lock.unlock(); 317 LOG.info("{} WAL replication component closed.", Utils.logPeerId(peerId)); 318 } 319 } 320 321 @RestrictedApi( 322 explanation = "Package-private for test visibility only. Do not use outside tests.", 323 link = "", 324 allowedOnPath = "(.*/src/test/.*|.*/org/apache/hadoop/hbase/backup/replication/ContinuousBackupReplicationEndpoint.java)") 325 void uploadBulkLoadFiles(long dayInMillis, List<Path> bulkLoadFiles) 326 throws BulkLoadUploadException { 327 if (bulkLoadFiles.isEmpty()) { 328 LOG.debug("{} No bulk load files to upload for {}", Utils.logPeerId(peerId), dayInMillis); 329 return; 330 } 331 332 LOG.debug("{} Starting upload of {} bulk load files", Utils.logPeerId(peerId), 333 bulkLoadFiles.size()); 334 335 if (LOG.isTraceEnabled()) { 336 LOG.trace("{} Bulk load files to upload: {}", Utils.logPeerId(peerId), 337 bulkLoadFiles.stream().map(Path::toString).collect(Collectors.joining(", "))); 338 } 339 String dayDirectoryName = BackupUtils.formatToDateString(dayInMillis); 340 Path bulkloadDir = new Path(backupFileSystemManager.getBulkLoadFilesDir(), dayDirectoryName); 341 try { 342 backupFileSystemManager.getBackupFs().mkdirs(bulkloadDir); 343 } catch (IOException e) { 344 throw new BulkLoadUploadException( 345 String.format("%s Failed to create bulkload directory in backupFS: %s", 346 Utils.logPeerId(peerId), bulkloadDir), 347 e); 348 } 349 350 for (Path file : bulkLoadFiles) { 351 Path sourcePath; 352 try { 353 sourcePath = getBulkLoadFileStagingPath(file); 354 } catch (FileNotFoundException fnfe) { 355 throw new BulkLoadUploadException( 356 String.format("%s Bulk load file not found: %s", Utils.logPeerId(peerId), file), fnfe); 357 } catch (IOException ioe) { 358 throw new BulkLoadUploadException( 359 String.format("%s Failed to resolve source path for: %s", Utils.logPeerId(peerId), file), 360 ioe); 361 } 362 363 Path destPath = new Path(bulkloadDir, file); 364 365 try { 366 LOG.debug("{} Copying bulk load file from {} to {}", Utils.logPeerId(peerId), sourcePath, 367 destPath); 368 369 copyWithCleanup(CommonFSUtils.getRootDirFileSystem(conf), sourcePath, 370 backupFileSystemManager.getBackupFs(), destPath, conf); 371 372 LOG.info("{} Bulk load file {} successfully backed up to {}", Utils.logPeerId(peerId), file, 373 destPath); 374 } catch (IOException e) { 375 throw new BulkLoadUploadException( 376 String.format("%s Failed to copy bulk load file %s to %s on day %s", 377 Utils.logPeerId(peerId), file, destPath, BackupUtils.formatToDateString(dayInMillis)), 378 e); 379 } 380 } 381 382 LOG.debug("{} Completed upload of bulk load files", Utils.logPeerId(peerId)); 383 } 384 385 /** 386 * Copy a file with cleanup logic in case of failure. Always overwrite destination to avoid 387 * leaving corrupt partial files. 388 */ 389 @RestrictedApi( 390 explanation = "Package-private for test visibility only. Do not use outside tests.", 391 link = "", 392 allowedOnPath = "(.*/src/test/.*|.*/org/apache/hadoop/hbase/backup/replication/ContinuousBackupReplicationEndpoint.java)") 393 static void copyWithCleanup(FileSystem srcFS, Path src, FileSystem dstFS, Path dst, 394 Configuration conf) throws IOException { 395 try { 396 if (dstFS.exists(dst)) { 397 FileStatus srcStatus = srcFS.getFileStatus(src); 398 FileStatus dstStatus = dstFS.getFileStatus(dst); 399 400 if (srcStatus.getLen() == dstStatus.getLen()) { 401 LOG.info("Destination file {} already exists with same length ({}). Skipping copy.", dst, 402 dstStatus.getLen()); 403 return; // Skip upload 404 } else { 405 LOG.warn( 406 "Destination file {} exists but length differs (src={}, dst={}). " + "Overwriting now.", 407 dst, srcStatus.getLen(), dstStatus.getLen()); 408 } 409 } 410 411 // Always overwrite in case previous copy left partial data 412 FileUtil.copy(srcFS, src, dstFS, dst, false, true, conf); 413 } catch (IOException e) { 414 try { 415 if (dstFS.exists(dst)) { 416 dstFS.delete(dst, true); 417 LOG.warn("Deleted partial/corrupt destination file {} after copy failure", dst); 418 } 419 } catch (IOException cleanupEx) { 420 LOG.warn("Failed to cleanup destination file {} after copy failure", dst, cleanupEx); 421 } 422 throw e; 423 } 424 } 425 426 private Path getBulkLoadFileStagingPath(Path relativePathFromNamespace) throws IOException { 427 FileSystem rootFs = CommonFSUtils.getRootDirFileSystem(conf); 428 Path rootDir = CommonFSUtils.getRootDir(conf); 429 Path baseNSDir = new Path(HConstants.BASE_NAMESPACE_DIR); 430 Path baseNamespaceDir = new Path(rootDir, baseNSDir); 431 Path hFileArchiveDir = 432 new Path(rootDir, new Path(HConstants.HFILE_ARCHIVE_DIRECTORY, baseNSDir)); 433 434 LOG.debug("{} Searching for bulk load file: {} in paths: {}, {}", Utils.logPeerId(peerId), 435 relativePathFromNamespace, baseNamespaceDir, hFileArchiveDir); 436 437 Path result = 438 findExistingPath(rootFs, baseNamespaceDir, hFileArchiveDir, relativePathFromNamespace); 439 LOG.debug("{} Bulk load file found at {}", Utils.logPeerId(peerId), result); 440 return result; 441 } 442 443 private static Path findExistingPath(FileSystem rootFs, Path baseNamespaceDir, 444 Path hFileArchiveDir, Path filePath) throws IOException { 445 if (LOG.isTraceEnabled()) { 446 LOG.trace("Checking for bulk load file at: {} and {}", new Path(baseNamespaceDir, filePath), 447 new Path(hFileArchiveDir, filePath)); 448 } 449 450 for (Path candidate : new Path[] { new Path(baseNamespaceDir, filePath), 451 new Path(hFileArchiveDir, filePath) }) { 452 if (rootFs.exists(candidate)) { 453 return candidate; 454 } 455 } 456 457 throw new FileNotFoundException("Bulk load file not found at either: " 458 + new Path(baseNamespaceDir, filePath) + " or " + new Path(hFileArchiveDir, filePath)); 459 } 460 461 @Override 462 public void beforePersistingReplicationOffset() throws IOException { 463 lock.lock(); 464 try { 465 flushWriters(); 466 } finally { 467 lock.unlock(); 468 } 469 } 470}