Skip to content

Commit 7c999c1

Browse files
briaugenreichbbeaudreault
authored andcommitted
HBASE-27487: Slow meta can create pathological feedback loop with multigets (apache#4900)
Signed-off-by: Bryan Beaudreault <bbeaudreault@apache.org> Signed-off-by: Duo Zhang <zhangduo@apache.org>
1 parent 6c19e78 commit 7c999c1

4 files changed

Lines changed: 162 additions & 21 deletions

File tree

hbase-client/src/main/java/org/apache/hadoop/hbase/client/AsyncRequestFutureImpl.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,14 @@ public void run() {
207207
// Cancelled
208208
return;
209209
}
210+
} catch (OperationTimeoutExceededException e) {
211+
// The operation has timed out before executing the actual callable. This may be due to
212+
// slow/hotspotted meta or the operation timeout set too low for the number of requests.
213+
// Circumventing the usual failure flow ensure the meta cache is not cleared and will not
214+
// result in a doomed feedback loop in which the meta continues to be hotspotted.
215+
// See HBASE-27487
216+
failAll(multiAction, server, numAttempt, e);
217+
return;
210218
} catch (IOException e) {
211219
// The service itself failed . It may be an error coming from the communication
212220
// layer, but, as well, a functional error raised by the server.
@@ -676,6 +684,25 @@ Retry manageError(int originalIndex, Row row, Retry canRetry, Throwable throwabl
676684
return canRetry;
677685
}
678686

687+
/**
688+
* Fail all the actions from this multiaction after an OperationTimeoutExceededException
689+
* @param actions the actions still to do from the initial list
690+
* @param server the destination
691+
* @param numAttempt the number of attempts so far
692+
* @param throwable the throwable that caused the failure
693+
*/
694+
private void failAll(MultiAction actions, ServerName server, int numAttempt,
695+
Throwable throwable) {
696+
int failed = 0;
697+
for (Map.Entry<byte[], List<Action>> e : actions.actions.entrySet()) {
698+
for (Action action : e.getValue()) {
699+
setError(action.getOriginalIndex(), action.getAction(), throwable, server);
700+
++failed;
701+
}
702+
}
703+
logNoResubmit(server, numAttempt, actions.size(), throwable, failed, 0);
704+
}
705+
679706
/**
680707
* Resubmit all the actions from this multiaction after a failure.
681708
* @param rsActions the actions still to do from the initial list

hbase-client/src/main/java/org/apache/hadoop/hbase/client/CancellableRegionServerCallable.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919

2020
import java.io.IOException;
2121
import java.io.InterruptedIOException;
22-
import org.apache.hadoop.hbase.DoNotRetryIOException;
2322
import org.apache.hadoop.hbase.ServerName;
2423
import org.apache.hadoop.hbase.TableName;
2524
import org.apache.yetus.audience.InterfaceAudience;
@@ -64,7 +63,10 @@ public T call(int operationTimeout) throws IOException {
6463
int remainingTime = tracker.getRemainingTime(operationTimeout);
6564
if (remainingTime <= 1) {
6665
// "1" is a special return value in RetryingTimeTracker, see its implementation.
67-
throw new DoNotRetryIOException("Operation rpcTimeout");
66+
throw new OperationTimeoutExceededException(
67+
"Timeout exceeded before call began. Meta requests may be slow, the operation "
68+
+ "timeout is too short for the number of requests, or the configured retries "
69+
+ "can't complete in the operation timeout.");
6870
}
6971
return super.call(Math.min(rpcTimeout, remainingTime));
7072
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package org.apache.hadoop.hbase.client;
19+
20+
import org.apache.hadoop.hbase.DoNotRetryIOException;
21+
import org.apache.yetus.audience.InterfaceAudience;
22+
23+
/**
24+
* Thrown when a batch operation exceeds the operation timeout
25+
*/
26+
@InterfaceAudience.Public
27+
public class OperationTimeoutExceededException extends DoNotRetryIOException {
28+
29+
public OperationTimeoutExceededException() {
30+
super();
31+
}
32+
33+
public OperationTimeoutExceededException(String msg) {
34+
super(msg);
35+
}
36+
37+
public OperationTimeoutExceededException(String msg, Throwable t) {
38+
super(msg, t);
39+
}
40+
41+
}

hbase-server/src/test/java/org/apache/hadoop/hbase/TestClientOperationTimeout.java renamed to hbase-server/src/test/java/org/apache/hadoop/hbase/client/TestClientOperationTimeout.java

Lines changed: 90 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,21 @@
1515
* See the License for the specific language governing permissions and
1616
* limitations under the License.
1717
*/
18-
package org.apache.hadoop.hbase;
18+
package org.apache.hadoop.hbase.client;
19+
20+
import static org.apache.hadoop.hbase.client.MetricsConnection.CLIENT_SIDE_METRICS_ENABLED_KEY;
1921

2022
import java.io.IOException;
2123
import java.net.SocketTimeoutException;
2224
import java.util.ArrayList;
2325
import java.util.List;
2426
import org.apache.hadoop.conf.Configuration;
25-
import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder;
26-
import org.apache.hadoop.hbase.client.Connection;
27-
import org.apache.hadoop.hbase.client.ConnectionFactory;
28-
import org.apache.hadoop.hbase.client.Get;
29-
import org.apache.hadoop.hbase.client.Put;
30-
import org.apache.hadoop.hbase.client.ResultScanner;
31-
import org.apache.hadoop.hbase.client.RetriesExhaustedException;
32-
import org.apache.hadoop.hbase.client.Scan;
33-
import org.apache.hadoop.hbase.client.Table;
34-
import org.apache.hadoop.hbase.client.TableDescriptorBuilder;
27+
import org.apache.hadoop.hbase.HBaseClassTestRule;
28+
import org.apache.hadoop.hbase.HBaseTestingUtility;
29+
import org.apache.hadoop.hbase.HConstants;
30+
import org.apache.hadoop.hbase.MiniHBaseCluster;
31+
import org.apache.hadoop.hbase.StartMiniClusterOption;
32+
import org.apache.hadoop.hbase.TableName;
3533
import org.apache.hadoop.hbase.ipc.CallTimeoutException;
3634
import org.apache.hadoop.hbase.regionserver.HRegionServer;
3735
import org.apache.hadoop.hbase.regionserver.RSRpcServices;
@@ -78,7 +76,8 @@ public class TestClientOperationTimeout {
7876
private static int DELAY_GET;
7977
private static int DELAY_SCAN;
8078
private static int DELAY_MUTATE;
81-
private static int DELAY_BATCH_MUTATE;
79+
private static int DELAY_BATCH;
80+
private static int DELAY_META_SCAN;
8281

8382
private static final TableName TABLE_NAME = TableName.valueOf("Timeout");
8483
private static final byte[] FAMILY = Bytes.toBytes("family");
@@ -112,7 +111,8 @@ public void setUp() throws Exception {
112111
DELAY_GET = 0;
113112
DELAY_SCAN = 0;
114113
DELAY_MUTATE = 0;
115-
DELAY_BATCH_MUTATE = 0;
114+
DELAY_BATCH = 0;
115+
DELAY_META_SCAN = 0;
116116
}
117117

118118
@AfterClass
@@ -157,12 +157,12 @@ public void testPutTimeout() {
157157
}
158158

159159
/**
160-
* Tests that a batch mutate on a table throws {@link SocketTimeoutException} when the operation
161-
* takes longer than 'hbase.client.operation.timeout'.
160+
* Tests that a batch mutate and batch get on a table throws {@link SocketTimeoutException} when
161+
* the operation takes longer than 'hbase.client.operation.timeout'.
162162
*/
163163
@Test
164-
public void testMultiPutsTimeout() {
165-
DELAY_BATCH_MUTATE = 600;
164+
public void testMultiTimeout() {
165+
DELAY_BATCH = 600;
166166
Put put1 = new Put(ROW);
167167
put1.addColumn(FAMILY, QUALIFIER, VALUE);
168168
Put put2 = new Put(ROW);
@@ -176,6 +176,72 @@ public void testMultiPutsTimeout() {
176176
} catch (Exception e) {
177177
Assert.assertTrue(e instanceof SocketTimeoutException);
178178
}
179+
180+
Get get1 = new Get(ROW);
181+
get1.addColumn(FAMILY, QUALIFIER);
182+
Get get2 = new Get(ROW);
183+
get2.addColumn(FAMILY, QUALIFIER);
184+
185+
List<Get> gets = new ArrayList<>();
186+
gets.add(get1);
187+
gets.add(get2);
188+
try {
189+
TABLE.batch(gets, new Object[2]);
190+
Assert.fail("should not reach here");
191+
} catch (Exception e) {
192+
Assert.assertTrue(e instanceof SocketTimeoutException);
193+
}
194+
}
195+
196+
/**
197+
* Tests that a batch get on a table throws
198+
* {@link org.apache.hadoop.hbase.client.OperationTimeoutExceededException} when the region lookup
199+
* takes longer than the 'hbase.client.operation.timeout'
200+
*/
201+
@Test
202+
public void testMultiGetMetaTimeout() throws IOException {
203+
204+
Configuration conf = new Configuration(UTIL.getConfiguration());
205+
206+
// the operation timeout must be lower than the delay from a meta scan to etch region locations
207+
// of the get requests. Simply increasing the meta scan timeout to greater than the
208+
// HBASE_CLIENT_SCANNER_TIMEOUT_PERIOD will result in SocketTimeoutException on the scans thus
209+
// avoiding the simulation of load on meta. See: HBASE-27487
210+
conf.setLong(HConstants.HBASE_CLIENT_OPERATION_TIMEOUT, 400);
211+
conf.setBoolean(CLIENT_SIDE_METRICS_ENABLED_KEY, true);
212+
try (Connection specialConnection = ConnectionFactory.createConnection(conf);
213+
Table specialTable = specialConnection.getTable(TABLE_NAME)) {
214+
215+
MetricsConnection metrics =
216+
((ConnectionImplementation) specialConnection).getConnectionMetrics();
217+
long metaCacheNumClearServerPreFailure = metrics.metaCacheNumClearServer.getCount();
218+
219+
DELAY_META_SCAN = 400;
220+
List<Get> gets = new ArrayList<>();
221+
// we need to ensure the region look-ups eat up more time than the operation timeout without
222+
// exceeding the scan timeout.
223+
for (int i = 0; i < 100; i++) {
224+
gets.add(new Get(Bytes.toBytes(i)).addColumn(FAMILY, QUALIFIER));
225+
}
226+
try {
227+
specialTable.get(gets);
228+
Assert.fail("should not reach here");
229+
} catch (Exception e) {
230+
RetriesExhaustedWithDetailsException expected = (RetriesExhaustedWithDetailsException) e;
231+
Assert.assertEquals(100, expected.getNumExceptions());
232+
233+
// verify we do not clear the cache in this situation otherwise we will create pathological
234+
// feedback loop with multigets See: HBASE-27487
235+
long metaCacheNumClearServerPostFailure = metrics.metaCacheNumClearServer.getCount();
236+
Assert.assertEquals(metaCacheNumClearServerPreFailure, metaCacheNumClearServerPostFailure);
237+
238+
for (Throwable cause : expected.getCauses()) {
239+
Assert.assertTrue(cause instanceof OperationTimeoutExceededException);
240+
}
241+
242+
}
243+
}
244+
179245
}
180246

181247
/**
@@ -240,7 +306,12 @@ public ClientProtos.MutateResponse mutate(RpcController rpcc,
240306
public ClientProtos.ScanResponse scan(RpcController controller,
241307
ClientProtos.ScanRequest request) throws ServiceException {
242308
try {
243-
Thread.sleep(DELAY_SCAN);
309+
String regionName = Bytes.toString(request.getRegion().getValue().toByteArray());
310+
if (regionName.contains(TableName.META_TABLE_NAME.getNameAsString())) {
311+
Thread.sleep(DELAY_META_SCAN);
312+
} else {
313+
Thread.sleep(DELAY_SCAN);
314+
}
244315
} catch (InterruptedException e) {
245316
LOG.error("Sleep interrupted during scan operation", e);
246317
}
@@ -251,7 +322,7 @@ public ClientProtos.ScanResponse scan(RpcController controller,
251322
public ClientProtos.MultiResponse multi(RpcController rpcc, ClientProtos.MultiRequest request)
252323
throws ServiceException {
253324
try {
254-
Thread.sleep(DELAY_BATCH_MUTATE);
325+
Thread.sleep(DELAY_BATCH);
255326
} catch (InterruptedException e) {
256327
LOG.error("Sleep interrupted during multi operation", e);
257328
}

0 commit comments

Comments
 (0)