You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hbase.apache.org by bb...@apache.org on 2023/12/01 19:58:50 UTC

(hbase) branch branch-2.6 updated: HBASE-25549 Provide a switch that allows avoiding reopening all regions when modifying a table to prevent RIT storms (#5549)

This is an automated email from the ASF dual-hosted git repository.

bbeaudreault pushed a commit to branch branch-2.6
in repository https://gitbox.apache.org/repos/asf/hbase.git


The following commit(s) were added to refs/heads/branch-2.6 by this push:
     new edadaec0f8a HBASE-25549 Provide a switch that allows avoiding reopening all regions when modifying a table to prevent RIT storms (#5549)
edadaec0f8a is described below

commit edadaec0f8a6ff63ab6be352403b80acf27887e9
Author: Gourab Taparia <it...@gmail.com>
AuthorDate: Sat Dec 2 01:10:01 2023 +0530

    HBASE-25549 Provide a switch that allows avoiding reopening all regions when modifying a table to prevent RIT storms (#5549)
    
    Signed-off-by: Bryan Beaudreault <bb...@apache.org>
    Signed-off-by: GeorryHuang <hu...@apache.org>
---
 .../java/org/apache/hadoop/hbase/client/Admin.java |  40 +++++++-
 .../org/apache/hadoop/hbase/client/AsyncAdmin.java |  15 ++-
 .../hadoop/hbase/client/AsyncHBaseAdmin.java       |   7 +-
 .../org/apache/hadoop/hbase/client/HBaseAdmin.java |   7 +-
 .../hadoop/hbase/client/RawAsyncHBaseAdmin.java    |   7 +-
 .../hbase/shaded/protobuf/RequestConverter.java    |   4 +-
 .../src/main/protobuf/Master.proto                 |   1 +
 .../src/main/protobuf/MasterProcedure.proto        |   1 +
 .../org/apache/hadoop/hbase/master/HMaster.java    |  15 ++-
 .../hadoop/hbase/master/MasterRpcServices.java     |   3 +-
 .../apache/hadoop/hbase/master/MasterServices.java |  13 ++-
 .../master/procedure/ModifyTableProcedure.java     |  88 ++++++++++++++--
 .../hbase/master/MockNoopMasterServices.java       |   6 ++
 .../master/procedure/TestModifyTableProcedure.java | 114 +++++++++++++++++++++
 hbase-shell/src/main/ruby/hbase/admin.rb           |  28 ++++-
 hbase-shell/src/main/ruby/hbase_constants.rb       |   1 +
 hbase-shell/src/main/ruby/shell/commands/alter.rb  |  21 ++++
 .../hadoop/hbase/thrift2/client/ThriftAdmin.java   |   2 +-
 18 files changed, 346 insertions(+), 27 deletions(-)

diff --git a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/Admin.java b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/Admin.java
index 871c1b82149..8d8a2c90479 100644
--- a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/Admin.java
+++ b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/Admin.java
@@ -1535,7 +1535,21 @@ public interface Admin extends Abortable, Closeable {
       throw new IllegalArgumentException("the specified table name '" + tableName
         + "' doesn't match with the HTD one: " + td.getTableName());
     }
-    modifyTable(td);
+    modifyTable(td, true);
+  }
+
+  /**
+   * Modify an existing table, more IRB friendly version.
+   * @param td            modified description of the table
+   * @param reopenRegions By default, 'modifyTable' reopens all regions, potentially causing a
+   *                      RIT(Region In Transition) storm in large tables. If set to 'false',
+   *                      regions will remain unaware of the modification until they are
+   *                      individually reopened. Please note that this may temporarily result in
+   *                      configuration inconsistencies among regions.
+   * @throws IOException if a remote or network exception occurs
+   */
+  default void modifyTable(TableDescriptor td, boolean reopenRegions) throws IOException {
+    get(modifyTableAsync(td, reopenRegions), getSyncWaitTimeout(), TimeUnit.MILLISECONDS);
   }
 
   /**
@@ -1559,7 +1573,7 @@ public interface Admin extends Abortable, Closeable {
    * @return the result of the async modify. You can use Future.get(long, TimeUnit) to wait on the
    *         operation to complete
    * @deprecated since 2.0 version and will be removed in 3.0 version. use
-   *             {@link #modifyTableAsync(TableDescriptor)}
+   *             {@link #modifyTableAsync(TableDescriptor, boolean)}
    */
   @Deprecated
   default Future<Void> modifyTableAsync(TableName tableName, TableDescriptor td)
@@ -1582,7 +1596,27 @@ public interface Admin extends Abortable, Closeable {
    * @return the result of the async modify. You can use Future.get(long, TimeUnit) to wait on the
    *         operation to complete
    */
-  Future<Void> modifyTableAsync(TableDescriptor td) throws IOException;
+  default Future<Void> modifyTableAsync(TableDescriptor td) throws IOException {
+    return modifyTableAsync(td, true);
+  }
+
+  /**
+   * Modify an existing table, more IRB (ruby) friendly version. Asynchronous operation. This means
+   * that it may be a while before your schema change is updated across all of the table. You can
+   * use Future.get(long, TimeUnit) to wait on the operation to complete. It may throw
+   * ExecutionException if there was an error while executing the operation or TimeoutException in
+   * case the wait timeout was not long enough to allow the operation to complete.
+   * @param td            description of the table
+   * @param reopenRegions By default, 'modifyTableAsync' reopens all regions, potentially causing a
+   *                      RIT(Region In Transition) storm in large tables. If set to 'false',
+   *                      regions will remain unaware of the modification until they are
+   *                      individually reopened. Please note that this may temporarily result in
+   *                      configuration inconsistencies among regions.
+   * @throws IOException if a remote or network exception occurs
+   * @return the result of the async modify. You can use Future.get(long, TimeUnit) to wait on the
+   *         operation to complete
+   */
+  Future<Void> modifyTableAsync(TableDescriptor td, boolean reopenRegions) throws IOException;
 
   /**
    * Change the store file tracker of the given table.
diff --git a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/AsyncAdmin.java b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/AsyncAdmin.java
index bde7e950e56..3742fffc256 100644
--- a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/AsyncAdmin.java
+++ b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/AsyncAdmin.java
@@ -193,7 +193,20 @@ public interface AsyncAdmin {
    * Modify an existing table, more IRB friendly version.
    * @param desc modified description of the table
    */
-  CompletableFuture<Void> modifyTable(TableDescriptor desc);
+  default CompletableFuture<Void> modifyTable(TableDescriptor desc) {
+    return modifyTable(desc, true);
+  }
+
+  /**
+   * Modify an existing table, more IRB friendly version.
+   * @param desc          description of the table
+   * @param reopenRegions By default, 'modifyTable' reopens all regions, potentially causing a RIT
+   *                      (Region In Transition) storm in large tables. If set to 'false', regions
+   *                      will remain unaware of the modification until they are individually
+   *                      reopened. Please note that this may temporarily result in configuration
+   *                      inconsistencies among regions.
+   */
+  CompletableFuture<Void> modifyTable(TableDescriptor desc, boolean reopenRegions);
 
   /**
    * Change the store file tracker of the given table.
diff --git a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/AsyncHBaseAdmin.java b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/AsyncHBaseAdmin.java
index cf473291319..448cfc9c36e 100644
--- a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/AsyncHBaseAdmin.java
+++ b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/AsyncHBaseAdmin.java
@@ -148,7 +148,12 @@ class AsyncHBaseAdmin implements AsyncAdmin {
 
   @Override
   public CompletableFuture<Void> modifyTable(TableDescriptor desc) {
-    return wrap(rawAdmin.modifyTable(desc));
+    return modifyTable(desc, true);
+  }
+
+  @Override
+  public CompletableFuture<Void> modifyTable(TableDescriptor desc, boolean reopenRegions) {
+    return wrap(rawAdmin.modifyTable(desc, reopenRegions));
   }
 
   @Override
diff --git a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/HBaseAdmin.java b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/HBaseAdmin.java
index 601f13b4889..0eb1b970cbd 100644
--- a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/HBaseAdmin.java
+++ b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/HBaseAdmin.java
@@ -392,7 +392,8 @@ public class HBaseAdmin implements Admin {
   }
 
   @Override
-  public Future<Void> modifyTableAsync(TableDescriptor td) throws IOException {
+  public Future<Void> modifyTableAsync(TableDescriptor td, boolean reopenRegions)
+    throws IOException {
     ModifyTableResponse response = executeCallable(
       new MasterCallable<ModifyTableResponse>(getConnection(), getRpcControllerFactory()) {
         long nonceGroup = ng.getNonceGroup();
@@ -401,8 +402,8 @@ public class HBaseAdmin implements Admin {
         @Override
         protected ModifyTableResponse rpcCall() throws Exception {
           setPriority(td.getTableName());
-          ModifyTableRequest request =
-            RequestConverter.buildModifyTableRequest(td.getTableName(), td, nonceGroup, nonce);
+          ModifyTableRequest request = RequestConverter.buildModifyTableRequest(td.getTableName(),
+            td, nonceGroup, nonce, reopenRegions);
           return master.modifyTable(getRpcController(), request);
         }
       });
diff --git a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/RawAsyncHBaseAdmin.java b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/RawAsyncHBaseAdmin.java
index cf103f401fc..f8fb625eab4 100644
--- a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/RawAsyncHBaseAdmin.java
+++ b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/RawAsyncHBaseAdmin.java
@@ -661,9 +661,14 @@ class RawAsyncHBaseAdmin implements AsyncAdmin {
 
   @Override
   public CompletableFuture<Void> modifyTable(TableDescriptor desc) {
+    return modifyTable(desc, true);
+  }
+
+  public CompletableFuture<Void> modifyTable(TableDescriptor desc, boolean reopenRegions) {
+    // TODO fill the request with reopenRegions
     return this.<ModifyTableRequest, ModifyTableResponse> procedureCall(desc.getTableName(),
       RequestConverter.buildModifyTableRequest(desc.getTableName(), desc, ng.getNonceGroup(),
-        ng.newNonce()),
+        ng.newNonce(), reopenRegions),
       (s, c, req, done) -> s.modifyTable(c, req, done), (resp) -> resp.getProcId(),
       new ModifyTableProcedureBiConsumer(this, desc.getTableName()));
   }
diff --git a/hbase-client/src/main/java/org/apache/hadoop/hbase/shaded/protobuf/RequestConverter.java b/hbase-client/src/main/java/org/apache/hadoop/hbase/shaded/protobuf/RequestConverter.java
index cd6fd2bb9c4..6ab4817b172 100644
--- a/hbase-client/src/main/java/org/apache/hadoop/hbase/shaded/protobuf/RequestConverter.java
+++ b/hbase-client/src/main/java/org/apache/hadoop/hbase/shaded/protobuf/RequestConverter.java
@@ -1306,12 +1306,14 @@ public final class RequestConverter {
    * @return a ModifyTableRequest
    */
   public static ModifyTableRequest buildModifyTableRequest(final TableName tableName,
-    final TableDescriptor tableDesc, final long nonceGroup, final long nonce) {
+    final TableDescriptor tableDesc, final long nonceGroup, final long nonce,
+    boolean reopenRegions) {
     ModifyTableRequest.Builder builder = ModifyTableRequest.newBuilder();
     builder.setTableName(ProtobufUtil.toProtoTableName(tableName));
     builder.setTableSchema(ProtobufUtil.toTableSchema(tableDesc));
     builder.setNonceGroup(nonceGroup);
     builder.setNonce(nonce);
+    builder.setReopenRegions(reopenRegions);
     return builder.build();
   }
 
diff --git a/hbase-protocol-shaded/src/main/protobuf/Master.proto b/hbase-protocol-shaded/src/main/protobuf/Master.proto
index ce736cd799e..a70587d34c3 100644
--- a/hbase-protocol-shaded/src/main/protobuf/Master.proto
+++ b/hbase-protocol-shaded/src/main/protobuf/Master.proto
@@ -203,6 +203,7 @@ message ModifyTableRequest {
   required TableSchema table_schema = 2;
   optional uint64 nonce_group = 3 [default = 0];
   optional uint64 nonce = 4 [default = 0];
+  optional bool reopen_regions = 5 [default = true];
 }
 
 message ModifyTableResponse {
diff --git a/hbase-protocol-shaded/src/main/protobuf/MasterProcedure.proto b/hbase-protocol-shaded/src/main/protobuf/MasterProcedure.proto
index b81507269ae..856cf668f18 100644
--- a/hbase-protocol-shaded/src/main/protobuf/MasterProcedure.proto
+++ b/hbase-protocol-shaded/src/main/protobuf/MasterProcedure.proto
@@ -82,6 +82,7 @@ message ModifyTableStateData {
   required TableSchema modified_table_schema = 3;
   required bool delete_column_family_in_modify = 4;
   optional bool should_check_descriptor = 5;
+  optional bool reopen_regions = 6;
 }
 
 enum TruncateTableState {
diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/HMaster.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/HMaster.java
index c92b3559a90..92c65da1600 100644
--- a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/HMaster.java
+++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/HMaster.java
@@ -1130,7 +1130,7 @@ public class HMaster extends HRegionServer implements MasterServices {
           procedureExecutor.submitProcedure(new ModifyTableProcedure(
             procedureExecutor.getEnvironment(), TableDescriptorBuilder.newBuilder(metaDesc)
               .setRegionReplication(replicasNumInConf).build(),
-            null, metaDesc, false));
+            null, metaDesc, false, true));
         }
       }
     }
@@ -2720,6 +2720,13 @@ public class HMaster extends HRegionServer implements MasterServices {
   private long modifyTable(final TableName tableName,
     final TableDescriptorGetter newDescriptorGetter, final long nonceGroup, final long nonce,
     final boolean shouldCheckDescriptor) throws IOException {
+    return modifyTable(tableName, newDescriptorGetter, nonceGroup, nonce, shouldCheckDescriptor,
+      true);
+  }
+
+  private long modifyTable(final TableName tableName,
+    final TableDescriptorGetter newDescriptorGetter, final long nonceGroup, final long nonce,
+    final boolean shouldCheckDescriptor, final boolean reopenRegions) throws IOException {
     return MasterProcedureUtil
       .submitProcedure(new MasterProcedureUtil.NonceProcedureRunnable(this, nonceGroup, nonce) {
         @Override
@@ -2738,7 +2745,7 @@ public class HMaster extends HRegionServer implements MasterServices {
           // checks. This will block only the beginning of the procedure. See HBASE-19953.
           ProcedurePrepareLatch latch = ProcedurePrepareLatch.createBlockingLatch();
           submitProcedure(new ModifyTableProcedure(procedureExecutor.getEnvironment(),
-            newDescriptor, latch, oldDescriptor, shouldCheckDescriptor));
+            newDescriptor, latch, oldDescriptor, shouldCheckDescriptor, reopenRegions));
           latch.await();
 
           getMaster().getMasterCoprocessorHost().postModifyTable(tableName, oldDescriptor,
@@ -2755,14 +2762,14 @@ public class HMaster extends HRegionServer implements MasterServices {
 
   @Override
   public long modifyTable(final TableName tableName, final TableDescriptor newDescriptor,
-    final long nonceGroup, final long nonce) throws IOException {
+    final long nonceGroup, final long nonce, final boolean reopenRegions) throws IOException {
     checkInitialized();
     return modifyTable(tableName, new TableDescriptorGetter() {
       @Override
       public TableDescriptor get() throws IOException {
         return newDescriptor;
       }
-    }, nonceGroup, nonce, false);
+    }, nonceGroup, nonce, false, reopenRegions);
 
   }
 
diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/MasterRpcServices.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/MasterRpcServices.java
index 2924ebef000..86c16f80a25 100644
--- a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/MasterRpcServices.java
+++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/MasterRpcServices.java
@@ -1484,7 +1484,8 @@ public class MasterRpcServices extends RSRpcServices
     throws ServiceException {
     try {
       long procId = master.modifyTable(ProtobufUtil.toTableName(req.getTableName()),
-        ProtobufUtil.toTableDescriptor(req.getTableSchema()), req.getNonceGroup(), req.getNonce());
+        ProtobufUtil.toTableDescriptor(req.getTableSchema()), req.getNonceGroup(), req.getNonce(),
+        req.getReopenRegions());
       return ModifyTableResponse.newBuilder().setProcId(procId).build();
     } catch (IOException ioe) {
       throw new ServiceException(ioe);
diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/MasterServices.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/MasterServices.java
index 35044fb761a..a288bd351c3 100644
--- a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/MasterServices.java
+++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/MasterServices.java
@@ -152,8 +152,19 @@ public interface MasterServices extends Server {
    * @param tableName  The table name
    * @param descriptor The updated table descriptor
    */
+  default long modifyTable(final TableName tableName, final TableDescriptor descriptor,
+    final long nonceGroup, final long nonce) throws IOException {
+    return modifyTable(tableName, descriptor, nonceGroup, nonce, true);
+  }
+
+  /**
+   * Modify the descriptor of an existing table
+   * @param tableName     The table name
+   * @param descriptor    The updated table descriptor
+   * @param reopenRegions Whether to reopen regions after modifying the table descriptor
+   */
   long modifyTable(final TableName tableName, final TableDescriptor descriptor,
-    final long nonceGroup, final long nonce) throws IOException;
+    final long nonceGroup, final long nonce, final boolean reopenRegions) throws IOException;
 
   /**
    * Modify the store file tracker of an existing table
diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/procedure/ModifyTableProcedure.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/procedure/ModifyTableProcedure.java
index f85fb493fdf..6b86b4fbf02 100644
--- a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/procedure/ModifyTableProcedure.java
+++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/procedure/ModifyTableProcedure.java
@@ -20,6 +20,7 @@ package org.apache.hadoop.hbase.master.procedure;
 import java.io.IOException;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 import java.util.stream.Collectors;
@@ -33,11 +34,13 @@ import org.apache.hadoop.hbase.TableNotFoundException;
 import org.apache.hadoop.hbase.client.RegionInfo;
 import org.apache.hadoop.hbase.client.RegionReplicaUtil;
 import org.apache.hadoop.hbase.client.TableDescriptor;
+import org.apache.hadoop.hbase.client.TableDescriptorBuilder;
 import org.apache.hadoop.hbase.master.MasterCoprocessorHost;
 import org.apache.hadoop.hbase.master.zksyncer.MetaLocationSyncer;
 import org.apache.hadoop.hbase.procedure2.ProcedureStateSerializer;
 import org.apache.hadoop.hbase.regionserver.storefiletracker.StoreFileTrackerValidationUtils;
 import org.apache.hadoop.hbase.replication.ReplicationException;
+import org.apache.hadoop.hbase.rsgroup.RSGroupInfo;
 import org.apache.hadoop.hbase.util.Bytes;
 import org.apache.hadoop.hbase.util.ServerRegionReplicaUtil;
 import org.apache.yetus.audience.InterfaceAudience;
@@ -56,6 +59,7 @@ public class ModifyTableProcedure extends AbstractStateMachineTableProcedure<Mod
   private TableDescriptor modifiedTableDescriptor;
   private boolean deleteColumnFamilyInModify;
   private boolean shouldCheckDescriptor;
+  private boolean reopenRegions;
   /**
    * List of column families that cannot be deleted from the hbase:meta table. They are critical to
    * cluster operation. This is a bit of an odd place to keep this list but then this is the tooling
@@ -77,14 +81,15 @@ public class ModifyTableProcedure extends AbstractStateMachineTableProcedure<Mod
 
   public ModifyTableProcedure(final MasterProcedureEnv env, final TableDescriptor htd,
     final ProcedurePrepareLatch latch) throws HBaseIOException {
-    this(env, htd, latch, null, false);
+    this(env, htd, latch, null, false, true);
   }
 
   public ModifyTableProcedure(final MasterProcedureEnv env,
     final TableDescriptor newTableDescriptor, final ProcedurePrepareLatch latch,
-    final TableDescriptor oldTableDescriptor, final boolean shouldCheckDescriptor)
-    throws HBaseIOException {
+    final TableDescriptor oldTableDescriptor, final boolean shouldCheckDescriptor,
+    final boolean reopenRegions) throws HBaseIOException {
     super(env, latch);
+    this.reopenRegions = reopenRegions;
     initialize(oldTableDescriptor, shouldCheckDescriptor);
     this.modifiedTableDescriptor = newTableDescriptor;
     preflightChecks(env, null/* No table checks; if changing peers, table can be online */);
@@ -104,6 +109,60 @@ public class ModifyTableProcedure extends AbstractStateMachineTableProcedure<Mod
         }
       }
     }
+    if (!reopenRegions) {
+      if (this.unmodifiedTableDescriptor == null) {
+        throw new HBaseIOException(
+          "unmodifiedTableDescriptor cannot be null when this table modification won't reopen regions");
+      }
+      if (
+        !this.unmodifiedTableDescriptor.getTableName()
+          .equals(this.modifiedTableDescriptor.getTableName())
+      ) {
+        throw new HBaseIOException(
+          "Cannot change the table name when this modification won't " + "reopen regions.");
+      }
+      if (
+        this.unmodifiedTableDescriptor.getColumnFamilyCount()
+            != this.modifiedTableDescriptor.getColumnFamilyCount()
+      ) {
+        throw new HBaseIOException(
+          "Cannot add or remove column families when this modification " + "won't reopen regions.");
+      }
+      if (
+        this.unmodifiedTableDescriptor.getCoprocessorDescriptors().hashCode()
+            != this.modifiedTableDescriptor.getCoprocessorDescriptors().hashCode()
+      ) {
+        throw new HBaseIOException(
+          "Can not modify Coprocessor when table modification won't reopen regions");
+      }
+      final Set<String> s = new HashSet<>(Arrays.asList(TableDescriptorBuilder.REGION_REPLICATION,
+        TableDescriptorBuilder.REGION_MEMSTORE_REPLICATION, RSGroupInfo.TABLE_DESC_PROP_GROUP));
+      for (String k : s) {
+        if (
+          isTablePropertyModified(this.unmodifiedTableDescriptor, this.modifiedTableDescriptor, k)
+        ) {
+          throw new HBaseIOException(
+            "Can not modify " + k + " of a table when modification won't reopen regions");
+        }
+      }
+    }
+  }
+
+  /**
+   * Comparing the value associated with a given key across two TableDescriptor instances'
+   * properties.
+   * @return True if the table property <code>key</code> is the same in both.
+   **/
+  private boolean isTablePropertyModified(TableDescriptor oldDescriptor,
+    TableDescriptor newDescriptor, String key) {
+    String oldV = oldDescriptor.getValue(key);
+    String newV = newDescriptor.getValue(key);
+    if (oldV == null && newV == null) {
+      return false;
+    } else if (oldV != null && newV != null && oldV.equals(newV)) {
+      return false;
+    }
+    return true;
   }
 
   private void initialize(final TableDescriptor unmodifiedTableDescriptor,
@@ -125,7 +184,13 @@ public class ModifyTableProcedure extends AbstractStateMachineTableProcedure<Mod
           break;
         case MODIFY_TABLE_PRE_OPERATION:
           preModify(env, state);
-          setNextState(ModifyTableState.MODIFY_TABLE_CLOSE_EXCESS_REPLICAS);
+          // We cannot allow changes to region replicas when 'reopenRegions==false',
+          // as this mode bypasses the state management required for modifying region replicas.
+          if (reopenRegions) {
+            setNextState(ModifyTableState.MODIFY_TABLE_CLOSE_EXCESS_REPLICAS);
+          } else {
+            setNextState(ModifyTableState.MODIFY_TABLE_UPDATE_TABLE_DESCRIPTOR);
+          }
           break;
         case MODIFY_TABLE_CLOSE_EXCESS_REPLICAS:
           if (isTableEnabled(env)) {
@@ -135,7 +200,11 @@ public class ModifyTableProcedure extends AbstractStateMachineTableProcedure<Mod
           break;
         case MODIFY_TABLE_UPDATE_TABLE_DESCRIPTOR:
           updateTableDescriptor(env);
-          setNextState(ModifyTableState.MODIFY_TABLE_REMOVE_REPLICA_COLUMN);
+          if (reopenRegions) {
+            setNextState(ModifyTableState.MODIFY_TABLE_REMOVE_REPLICA_COLUMN);
+          } else {
+            setNextState(ModifyTableState.MODIFY_TABLE_POST_OPERATION);
+          }
           break;
         case MODIFY_TABLE_REMOVE_REPLICA_COLUMN:
           removeReplicaColumnsIfNeeded(env);
@@ -143,7 +212,11 @@ public class ModifyTableProcedure extends AbstractStateMachineTableProcedure<Mod
           break;
         case MODIFY_TABLE_POST_OPERATION:
           postModify(env, state);
-          setNextState(ModifyTableState.MODIFY_TABLE_REOPEN_ALL_REGIONS);
+          if (reopenRegions) {
+            setNextState(ModifyTableState.MODIFY_TABLE_REOPEN_ALL_REGIONS);
+          } else {
+            return Flow.NO_MORE_STATE;
+          }
           break;
         case MODIFY_TABLE_REOPEN_ALL_REGIONS:
           if (isTableEnabled(env)) {
@@ -238,7 +311,7 @@ public class ModifyTableProcedure extends AbstractStateMachineTableProcedure<Mod
         .setUserInfo(MasterProcedureUtil.toProtoUserInfo(getUser()))
         .setModifiedTableSchema(ProtobufUtil.toTableSchema(modifiedTableDescriptor))
         .setDeleteColumnFamilyInModify(deleteColumnFamilyInModify)
-        .setShouldCheckDescriptor(shouldCheckDescriptor);
+        .setShouldCheckDescriptor(shouldCheckDescriptor).setReopenRegions(reopenRegions);
 
     if (unmodifiedTableDescriptor != null) {
       modifyTableMsg
@@ -260,6 +333,7 @@ public class ModifyTableProcedure extends AbstractStateMachineTableProcedure<Mod
     deleteColumnFamilyInModify = modifyTableMsg.getDeleteColumnFamilyInModify();
     shouldCheckDescriptor =
       modifyTableMsg.hasShouldCheckDescriptor() ? modifyTableMsg.getShouldCheckDescriptor() : false;
+    reopenRegions = modifyTableMsg.hasReopenRegions() ? modifyTableMsg.getReopenRegions() : true;
 
     if (modifyTableMsg.hasUnmodifiedTableSchema()) {
       unmodifiedTableDescriptor =
diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/master/MockNoopMasterServices.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/master/MockNoopMasterServices.java
index a0951480124..8075c558dee 100644
--- a/hbase-server/src/test/java/org/apache/hadoop/hbase/master/MockNoopMasterServices.java
+++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/master/MockNoopMasterServices.java
@@ -258,6 +258,12 @@ public class MockNoopMasterServices implements MasterServices {
     return -1;
   }
 
+  @Override
+  public long modifyTable(TableName tableName, TableDescriptor descriptor, long nonceGroup,
+    long nonce, boolean reopenRegions) throws IOException {
+    return -1;
+  }
+
   @Override
   public long enableTable(final TableName tableName, final long nonceGroup, final long nonce)
     throws IOException {
diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/master/procedure/TestModifyTableProcedure.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/master/procedure/TestModifyTableProcedure.java
index 81ec43402a4..91ce3866f63 100644
--- a/hbase-server/src/test/java/org/apache/hadoop/hbase/master/procedure/TestModifyTableProcedure.java
+++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/master/procedure/TestModifyTableProcedure.java
@@ -25,20 +25,24 @@ import java.io.IOException;
 import org.apache.hadoop.hbase.ConcurrentTableModificationException;
 import org.apache.hadoop.hbase.DoNotRetryIOException;
 import org.apache.hadoop.hbase.HBaseClassTestRule;
+import org.apache.hadoop.hbase.HBaseIOException;
 import org.apache.hadoop.hbase.HColumnDescriptor;
 import org.apache.hadoop.hbase.HTableDescriptor;
 import org.apache.hadoop.hbase.InvalidFamilyOperationException;
 import org.apache.hadoop.hbase.TableName;
 import org.apache.hadoop.hbase.client.ColumnFamilyDescriptor;
 import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder;
+import org.apache.hadoop.hbase.client.CoprocessorDescriptorBuilder;
 import org.apache.hadoop.hbase.client.PerClientRandomNonceGenerator;
 import org.apache.hadoop.hbase.client.RegionInfo;
 import org.apache.hadoop.hbase.client.TableDescriptor;
 import org.apache.hadoop.hbase.client.TableDescriptorBuilder;
+import org.apache.hadoop.hbase.io.compress.Compression;
 import org.apache.hadoop.hbase.master.procedure.MasterProcedureTestingUtility.StepHook;
 import org.apache.hadoop.hbase.procedure2.Procedure;
 import org.apache.hadoop.hbase.procedure2.ProcedureExecutor;
 import org.apache.hadoop.hbase.procedure2.ProcedureTestingUtility;
+import org.apache.hadoop.hbase.regionserver.HRegion;
 import org.apache.hadoop.hbase.testclassification.LargeTests;
 import org.apache.hadoop.hbase.testclassification.MasterTests;
 import org.apache.hadoop.hbase.util.Bytes;
@@ -569,4 +573,114 @@ public class TestModifyTableProcedure extends TestTableDDLProcedureBase {
     t2.join();
     assertFalse("Expected ConcurrentTableModificationException.", (t1.exception || t2.exception));
   }
+
+  @Test
+  public void testModifyWillNotReopenRegions() throws IOException {
+    final boolean reopenRegions = false;
+    final TableName tableName = TableName.valueOf(name.getMethodName());
+    final ProcedureExecutor<MasterProcedureEnv> procExec = getMasterProcedureExecutor();
+
+    MasterProcedureTestingUtility.createTable(procExec, tableName, null, "cf");
+
+    // Test 1: Modify table without reopening any regions
+    TableDescriptor htd = UTIL.getAdmin().getDescriptor(tableName);
+    TableDescriptor modifiedDescriptor = TableDescriptorBuilder.newBuilder(htd)
+      .setValue("test" + ".hbase.conf", "test.hbase.conf.value").build();
+    long procId1 = ProcedureTestingUtility.submitAndWait(procExec, new ModifyTableProcedure(
+      procExec.getEnvironment(), modifiedDescriptor, null, htd, false, reopenRegions));
+    ProcedureTestingUtility.assertProcNotFailed(procExec.getResult(procId1));
+    TableDescriptor currentHtd = UTIL.getAdmin().getDescriptor(tableName);
+    assertEquals("test.hbase.conf.value", currentHtd.getValue("test.hbase.conf"));
+    // Regions should not aware of any changes.
+    for (HRegion r : UTIL.getHBaseCluster().getRegions(tableName)) {
+      Assert.assertNull(r.getTableDescriptor().getValue("test.hbase.conf"));
+    }
+    // Force regions to reopen
+    for (HRegion r : UTIL.getHBaseCluster().getRegions(tableName)) {
+      getMaster().getAssignmentManager().move(r.getRegionInfo());
+    }
+    // After the regions reopen, ensure that the configuration is updated.
+    for (HRegion r : UTIL.getHBaseCluster().getRegions(tableName)) {
+      assertEquals("test.hbase.conf.value", r.getTableDescriptor().getValue("test.hbase.conf"));
+    }
+
+    // Test 2: Modifying region replication is not allowed
+    htd = UTIL.getAdmin().getDescriptor(tableName);
+    long oldRegionReplication = htd.getRegionReplication();
+    modifiedDescriptor = TableDescriptorBuilder.newBuilder(htd).setRegionReplication(3).build();
+    try {
+      ProcedureTestingUtility.submitAndWait(procExec, new ModifyTableProcedure(
+        procExec.getEnvironment(), modifiedDescriptor, null, htd, false, reopenRegions));
+      Assert.fail(
+        "An exception should have been thrown while modifying region replication properties.");
+    } catch (HBaseIOException e) {
+      assertTrue(e.getMessage().contains("Can not modify"));
+    }
+    currentHtd = UTIL.getAdmin().getDescriptor(tableName);
+    // Nothing changed
+    assertEquals(oldRegionReplication, currentHtd.getRegionReplication());
+
+    // Test 3: Adding CFs is not allowed
+    htd = UTIL.getAdmin().getDescriptor(tableName);
+    modifiedDescriptor = TableDescriptorBuilder.newBuilder(htd)
+      .setColumnFamily(ColumnFamilyDescriptorBuilder.newBuilder("NewCF".getBytes()).build())
+      .build();
+    try {
+      ProcedureTestingUtility.submitAndWait(procExec, new ModifyTableProcedure(
+        procExec.getEnvironment(), modifiedDescriptor, null, htd, false, reopenRegions));
+      Assert.fail("Should have thrown an exception while modifying CF!");
+    } catch (HBaseIOException e) {
+      assertTrue(e.getMessage().contains("Cannot add or remove column families"));
+    }
+    currentHtd = UTIL.getAdmin().getDescriptor(tableName);
+    Assert.assertNull(currentHtd.getColumnFamily("NewCF".getBytes()));
+
+    // Test 4: Modifying CF property is allowed
+    htd = UTIL.getAdmin().getDescriptor(tableName);
+    modifiedDescriptor =
+      TableDescriptorBuilder
+        .newBuilder(htd).modifyColumnFamily(ColumnFamilyDescriptorBuilder
+          .newBuilder("cf".getBytes()).setCompressionType(Compression.Algorithm.SNAPPY).build())
+        .build();
+    ProcedureTestingUtility.submitAndWait(procExec, new ModifyTableProcedure(
+      procExec.getEnvironment(), modifiedDescriptor, null, htd, false, reopenRegions));
+    for (HRegion r : UTIL.getHBaseCluster().getRegions(tableName)) {
+      Assert.assertEquals(Compression.Algorithm.NONE,
+        r.getTableDescriptor().getColumnFamily("cf".getBytes()).getCompressionType());
+    }
+    for (HRegion r : UTIL.getHBaseCluster().getRegions(tableName)) {
+      getMaster().getAssignmentManager().move(r.getRegionInfo());
+    }
+    for (HRegion r : UTIL.getHBaseCluster().getRegions(tableName)) {
+      Assert.assertEquals(Compression.Algorithm.SNAPPY,
+        r.getTableDescriptor().getColumnFamily("cf".getBytes()).getCompressionType());
+    }
+
+    // Test 5: Modifying coprocessor is not allowed
+    htd = UTIL.getAdmin().getDescriptor(tableName);
+    modifiedDescriptor =
+      TableDescriptorBuilder.newBuilder(htd).setCoprocessor(CoprocessorDescriptorBuilder
+        .newBuilder("any.coprocessor.name").setJarPath("fake/path").build()).build();
+    try {
+      ProcedureTestingUtility.submitAndWait(procExec, new ModifyTableProcedure(
+        procExec.getEnvironment(), modifiedDescriptor, null, htd, false, reopenRegions));
+      Assert.fail("Should have thrown an exception while modifying coprocessor!");
+    } catch (HBaseIOException e) {
+      assertTrue(e.getMessage().contains("Can not modify Coprocessor"));
+    }
+    currentHtd = UTIL.getAdmin().getDescriptor(tableName);
+    assertEquals(0, currentHtd.getCoprocessorDescriptors().size());
+
+    // Test 6: Modifying REGION_REPLICATION is not allowed.
+    htd = UTIL.getAdmin().getDescriptor(tableName);
+    modifiedDescriptor = TableDescriptorBuilder.newBuilder(htd).setRegionReplication(3).build();
+    try {
+      ProcedureTestingUtility.submitAndWait(procExec, new ModifyTableProcedure(
+        procExec.getEnvironment(), modifiedDescriptor, null, htd, false, reopenRegions));
+      Assert.fail("Should have thrown an exception while modifying REGION_REPLICATION!");
+    } catch (HBaseIOException e) {
+      System.out.println(e.getMessage());
+      assertTrue(e.getMessage().contains("Can not modify REGION_REPLICATION"));
+    }
+  }
 }
diff --git a/hbase-shell/src/main/ruby/hbase/admin.rb b/hbase-shell/src/main/ruby/hbase/admin.rb
index 2110e72d155..1f2e684dce9 100644
--- a/hbase-shell/src/main/ruby/hbase/admin.rb
+++ b/hbase-shell/src/main/ruby/hbase/admin.rb
@@ -717,6 +717,7 @@ module Hbase
       # Get table descriptor
       htd = org.apache.hadoop.hbase.HTableDescriptor.new(@admin.getTableDescriptor(table_name))
       hasTableUpdate = false
+      reopen_regions = true
 
       # Process all args
       args.each do |arg|
@@ -726,9 +727,18 @@ module Hbase
         # Normalize args to support shortcut delete syntax
         arg = { METHOD => 'delete', NAME => arg['delete'] } if arg['delete']
 
+        if arg.key?(REOPEN_REGIONS)
+          if !['true', 'false'].include?(arg[REOPEN_REGIONS].downcase)
+            raise(ArgumentError, "Invalid 'REOPEN_REGIONS' for non-boolean value.")
+          end
+          reopen_regions = JBoolean.valueOf(arg[REOPEN_REGIONS])
+          arg.delete(REOPEN_REGIONS)
+        end
+
         # There are 3 possible options.
         # 1) Column family spec. Distinguished by having a NAME and no METHOD.
         method = arg.delete(METHOD)
+
         if method.nil? && arg.key?(NAME)
           descriptor = hcd(arg, htd)
           column_name = descriptor.getNameAsString
@@ -841,11 +851,23 @@ module Hbase
 
       # Bulk apply all table modifications.
       if hasTableUpdate
-        @admin.modifyTable(table_name, htd)
+        future = @admin.modifyTableAsync(htd, reopen_regions)
+        if reopen_regions == false
+          puts("WARNING: You are using REOPEN_REGIONS => 'false' to modify a table, which will
+          result in inconsistencies in the configuration of online regions and other risks. If you
+          encounter any issues, use the original 'alter' command to make the modification again!")
+        end
 
         if wait == true
-          puts 'Updating all regions with the new schema...'
-          alter_status(table_name_str)
+          # If the changes(reopen_regions=false) are not getting applied immediately -
+          # showing the below can be confusing, hence we only show it when reopen_regions is true.
+          if reopen_regions == true
+            puts 'Updating all regions with the new schema...'
+          end
+          future.get
+          if reopen_regions == true
+            alter_status(table_name_str)
+          end
         end
       end
     end
diff --git a/hbase-shell/src/main/ruby/hbase_constants.rb b/hbase-shell/src/main/ruby/hbase_constants.rb
index 76631c91cf9..ee6b8f22f42 100644
--- a/hbase-shell/src/main/ruby/hbase_constants.rb
+++ b/hbase-shell/src/main/ruby/hbase_constants.rb
@@ -99,6 +99,7 @@ module HBaseConstants
   VALUE = 'VALUE'.freeze
   VERSIONS = org.apache.hadoop.hbase.HConstants::VERSIONS
   VISIBILITY = 'VISIBILITY'.freeze
+  REOPEN_REGIONS = 'REOPEN_REGIONS'.freeze
 
   # aliases
   ENDKEY = STOPROW
diff --git a/hbase-shell/src/main/ruby/shell/commands/alter.rb b/hbase-shell/src/main/ruby/shell/commands/alter.rb
index 9d95bffbdda..85881fb41b1 100644
--- a/hbase-shell/src/main/ruby/shell/commands/alter.rb
+++ b/hbase-shell/src/main/ruby/shell/commands/alter.rb
@@ -72,6 +72,27 @@ You can also set configuration settings specific to this table or column family:
   hbase> alter 't1', CONFIGURATION => {'hbase.hregion.scan.loadColumnFamiliesOnDemand' => 'true'}
   hbase> alter 't1', {NAME => 'f2', CONFIGURATION => {'hbase.hstore.blockingStoreFiles' => '10'}}
 
+You can also set configuration setting with REOPEN_REGIONS=>'false' to avoid regions RIT, which
+let the modification take effect after regions was reopened (Be careful, the regions of the table
+may be configured inconsistently If regions are not reopened after the modification)
+
+  hbase> alter 't1', REOPEN_REGIONS => 'false', MAX_FILESIZE => '134217728'
+  hbase> alter 't1', REOPEN_REGIONS => 'false', CONFIGURATION => {'hbase.hregion.scan
+    .loadColumnFamiliesOnDemand' => 'true'}
+
+However, be aware that:
+1. Inconsistency Risks: If the regions are not reopened after the modification, the table's regions
+may become inconsistently configured. Ensure that you manually reopen the regions as soon as
+possible to apply the changes consistently across the entire table.
+2. If changes are made to the table without reopening the regions, we currently only allow
+lightweight operations. The following types of changes, which may lead to unknown situations,
+will throw an exception:
+    a. Adding or removing CFs, coprocessors.
+    b. Modifying the table name.
+    c. Changing region replica related configurations such as 'REGION_REPLICATION'
+       and 'REGION_MEMSTORE_REPLICATION'.
+    d. Changing the rsgroup.
+
 You can also unset configuration settings specific to this table:
 
   hbase> alter 't1', METHOD => 'table_conf_unset', NAME => 'hbase.hregion.majorcompaction'
diff --git a/hbase-thrift/src/main/java/org/apache/hadoop/hbase/thrift2/client/ThriftAdmin.java b/hbase-thrift/src/main/java/org/apache/hadoop/hbase/thrift2/client/ThriftAdmin.java
index 76deb6fdd6a..94da27933cf 100644
--- a/hbase-thrift/src/main/java/org/apache/hadoop/hbase/thrift2/client/ThriftAdmin.java
+++ b/hbase-thrift/src/main/java/org/apache/hadoop/hbase/thrift2/client/ThriftAdmin.java
@@ -922,7 +922,7 @@ public class ThriftAdmin implements Admin {
   }
 
   @Override
-  public Future<Void> modifyTableAsync(TableDescriptor td) {
+  public Future<Void> modifyTableAsync(TableDescriptor td, boolean reopenRegions) {
     throw new NotImplementedException("modifyTableAsync not supported in ThriftAdmin");
   }