一、背景
接入了阿里云后,很多公司都会购买阿里云的Lindorm(阿里版Hbase,下文Hbase都指Lindorm),宣称性能提升很多,但是在使用过程中,仍然避免不了各种超时的问题,如下所示:
java.lang.RuntimeException: DoNotRetrylOException: Failed doing WGET on
namespace=default, table=pick_all_label_data, operationTimeout=320, time took=322,
caused by com.alibaba.lindorm.client.exception.OperationTimeoutException: Failed after
322ms, operationTimeout=320, vmPauseTime=0, exceptions=, {12024-11-30
19:17:12(1732965432524) WGET},{321-321:WAIT_RESPONSE, [iddc2 Id
xxxxxx, Request
Invocation=[batchGet([{Attributes=[], totalColumns=0, row=0945697000000000,
families={], WOperation)] host=xxxxxx, requestlD=12060029, tinneout=320]}
at
com.alibaba.lindorm.client.core.ipc.Operation Context.getOperatiionTimeoutException(Ope
rationContext.java:172)
at
com.alibaba.lindorm.client.core.ipc.Retrying Caller.getTimeouteException(Retrying Caller.jav
a:146)
at
com.alibaba.lindorm.client.core.ipc.Retrying Caller.with Retries (Retrying Caller.java:387)
看似都是timeout引起,然后lindorm内部呢也通过RetryingCaller很努力的做了重试,最后通过DoNotRetryIOException结束了本次操作,尽管报错以后,一致认为是超时导致无可避免,然后也就忽略了。但这种定时炸弹指不定哪天爆炸了,后果肯定难以想象,有没有什么办法或者配置能优化或者解决这种“超时”。
二、超时是怎么引起的
先介绍一下Lindorm和原生Hbase的关系
1. Lindorm与Hbase
Lindorm是阿里开源的一个多模数据库,所谓多模,就是支持宽表、时序、对象、文本、队列、空间等多种数据模型,模型之间数据互融互通,具备数据接入、存储、检索、计算、分析等一体化融合处理与服务的能力,官方宣称:吞吐性能是开源HBase的3~7倍,P99时延为开源HBase的1/10,平均故障恢复时间相比开源HBase升10倍,支持冷热分离,压缩率比开源HBase提升一倍,综合存储成本为开源的1/2。
-
原生HBASE
只支持Rowkey索引。Scan请求可基于此rowkey索引高效的执行整行匹配、前缀匹配、范围查询等操作,但若需要使用rowkey之外的列进行查询,则只能使用filter在指定的rowkey范围内进行逐行过滤。若无法指定rowkey范围,则需进行全表扫描,不仅浪费大量资源,查询RT也无法保证。 -
Lindorm
在原生的基础能力上,额外扩展了其他能力。- 对于简单查询:
额外内置原生的全局二级索引功能,对于列较少且有固定查询模式的场景来说,阿里云的高性能二级索引方案能够完美解决此类问题,同时仍保持强大的吞吐与性能。 - 对于复杂查询:
Lindorm把宽表引擎+搜索引擎进行了结合,总结来说就是向上兼容了Hbase的开源API,向下基于数据自动分区+分区多副本+LSM的架构思想对Hbase进行了增强,使其具备全局二级索引、多维检索、动态列、TTL等查询处理能力。
- 对于简单查询:
2. 问题是怎么引入的
对于高并发、大数据量请求的操作,均有可能出现超时问题,如常见的增删改查和批量扫描:
- 写入Hbase–PUT操作
- 删除Hbase数据 --DELETE操作
- 查询或者校验是否存在 --GET操作
- 查询某个商品(produce_id)归属信息等 --SCAN操作
以上场景都会出现超时的情况,根据以往的经验或其他人的踩坑经历可知,HBase的查询超时通常由以下几个原因造成:
- 不合理的设计:表的设计不当,缺乏合适的主键和索引导致全表扫描;
- 数据量庞大:当表中的数据量达到数亿甚至数十亿行时,查询就可能变得缓慢;
- 网络延迟:由于网络带宽不够或延迟,数据读取会受到影响;
- 资源瓶颈:内存、CPU或I/O资源不足,无法支持高效访问;
- 负载过高:如果某个节点负载过高,可能会导致响应变慢和超时;
- 长耗时的事务:长时间的读写操作可能会占用资源,导致其他请求被阻塞;
- STRONG CONSISTENCY配置:设置了强一致性,那么在某些情况下,也可能导致查询变慢;
- 数据倾斜:数据的分布不均(例如热点数据集中在少数行或列上),可能会使某些查询变得非常慢;
- 客户端配置不合理:客户端配置中的超时设置(查询超时时间、RPC超时时间)可能不当,导致查询请求在超时之前未能完成;
- 硬件或者Lindorm版本的原因
3. 超时问题的分析方法
3.1 GET方法详解
客户端GET代码
// 举个简单的代码例子
String getByRowKey(Connection connection, String tableName, String rowKey, String column) {
try(Table table = connection.getTable(TableName.valueOf(tableName))) {
Get get = new Get(Bytes.toBytes(rowKey));
Result result = table.get(get);
byte[] valueBytes = result.getValue(Bytes.toBytes("cf"), Bytes.toBytes(column));
return new String(valueBytes, "utf-8");
} catch(Exception e) {
// do except
}
}
当我们get出现超时时,可能会出现如下问题:
java.lang.RuntimeException: DoNotRetryIOException: Failed doing WGET on namespace=default, table=xxx_label_relation_v2, operationTimeout=200, time took=202, caused by com.alibaba.lindorm.client.exception.OperationTimeoutException: Failed after 202ms, operationTimeout=200, vmPauseTime=0, exceptions=, {2024-12-26 13:37:46(1735191466852) WGET},{202-202:WAIT_RESPONSE,[idc1 xxxx.lindorm.rds.aliyuncs.com:30020, Request Invocation=[batchGet([{Attributes=[], totalColumns=0, row=S17383L000000000, families={}},{Attributes=[], totalColumns=0, row=S9922L0000000000, families={}},{Attributes=[], totalColumns=0, row=S17385L000000000, families={}},{Attributes=[], totalColumns=0, row=S17384L000000000, families={}}], WOperation)] host=xxxx.lindorm.rds.aliyuncs.com:30020, requestID=76981, timeout=200]}
at com.alibaba.lindorm.client.core.ipc.OperationContext.getOperationTimeoutException(OperationContext.java:172)
at com.alibaba.lindorm.client.core.ipc.RetryingCaller.getTimeoutException(RetryingCaller.java:146)
at com.alibaba.lindorm.client.core.ipc.RetryingCaller.withRetries(RetryingCaller.java:387)
at com.alibaba.lindorm.client.core.LindormWideColumnService$RequestSender.execute(LindormWideColumnService.java:254)
at com.alibaba.lindorm.client.core.LindormWideColumnService.batchGet(LindormWideColumnService.java:534)
at com.alibaba.hbase.client.AliHBaseAPIProxyDirectImpl.get(AliHBaseAPIProxyDirectImpl.java:827)
at org.apache.hadoop.hbase.client.AliHBaseUETable.get(AliHBaseUETable.java:275)
原因详解:
错误日志中关键信息:operationTimeout=200, time took=202,也就是查询操作时间配置的200ms,但是执行完查询花了202ms,所以就超时了。当进行GET的时候,客户端和服务端之间会建立RPC连接,后面构建WGET(WideColumn GET)对象的时候会出现两个参数:operationTimeout和glitchTimeout,先初始化,然后后面会进行重置:
operationTimeout:是指定一次操作(如读取、写入等)允许的最大执行时间。若操作在指定时间内未完成,将会抛出一个超时异常
glitchTimeout:是控制如何处理“故障”操作的超时设置。当客户端对 Lindorm 发起请求时,如果在指定时间内没有收到响应,系统会尝试重新发起请求。
我们看看Get的核心代码:
----------GET的核心代码------
org.apache.hadoop.hbase.client.AliHBaseUETable#get
@Override
public Result get(Get get) throws IOException {
//这里是做了一层负载代理--其实就是用Lindorm代理Hbase的API
return proxy.get(get);
}
com.alibaba.hbase.client.AliHBaseAPIProxyDirectImpl#get
--这里进入了lindorm的宽表处理方法
com.alibaba.lindorm.client.WideColumnService#get
@Override
public Result get(Get get) throws IOException {
try {
WResult result = wideColumnService.get(tableNameWithoutNamespace,
ElementConvertor.toLindormGet(get));
return ElementConvertor.toHBaseResult(result, get.isCheckExistenceOnly());
}catch(IOException e){
throw ElementConvertor.toHBaseIOException(e);
}
}
//这里的值会被重新赋值
public WGet(WGet get) throws IOException {
this.operationTimeout = -1;
this.glitchTimeout = -1;
}
//这里是进行GET的操作
public WResult[] batchGet(String tableName, final List<WGet> gets) throws LindormException {
if (gets != null && gets.size() != 0) {
//这里的Wget对象构造器,看上面的代码
WGet g = (WGet)gets.get(Threads.getFastRandom(gets.size()));
//这里是关键
WOperation wOperation = new WOperation(this, this.getNamespace(), tableName, g.getRowKey(), g.getOperationTimeout(), g.getGlitchTimeout());
OperationContext.OperationType operationType = OperationType.WGET;
RequestSender<WResult[], WMutationResult> requestSender = new RequestSender<WResult[], WMutationResult>(tableName, wOperation, operationType) {
protected LServerCallable<WMutationResult> buildCall() {
return LindormWideColumnService.this.buildBatchGetCallable(this.operationType, gets, this.wOperation);
}
protected WResult[] parseResult(WMutationResult response) {
return LindormWideColumnService.this.parseRResultArray(response);
}
protected int getNumberOfRowsAffected(WMutationResult wMutationResult) {
return gets.size();
}
};
//真正的查询操作
return (WResult[])requestSender.execute();
} else {
return null;
}
}
public Result execute() throws LindormException {
long start = System.currentTimeMillis();
LindormWideColumnService.this.startOperation(this.tableName, this.operationType);
//这是通过重试回调逻辑,先查询数据,然后再把数据返回,即便是查询有数据,但是超过操作时间也会抛异常
RetryingCaller<Response> retryingCaller = LindormWideColumnService.this.getLConnection().getDMLRetryingCaller(this.wOperation.getOperationTimeout(), this.wOperation.getGlitchTimeout(), LindormWideColumnService.this.doAsUser);
try {
//这里代码太多,主要就是查询完饭回一个Result结果集
} catch (Throwable var8) {
//这里就是我们超时异常出现的地方,有个计算逻辑System.currentTimeMillis() - start
String msg = this.wOperation.buildErrorMsg(this.operationType, t, System.currentTimeMillis() - start);
LindormException error = new LindormException(msg);
if (LindormWideColumnService.this.isInitCauseBy()) {
error.initCause(t);
}
LindormWideColumnService.this.getLConnection().getTableMetricsManager()
.onOperationError(LindormWideColumnService.this.namespace,
this.tableName, this.operationType, error);
LindormWideColumnService.this.endOperationExceptionally(
this.tableName, retryingCaller, t);
throw error;
}
}
所以这个问题,需要合理的设置glitchTimeout 和operationTimeout,修改配置主要是在构建socket连接的时候写进config中即可。
3.2 Hbase如何SCAN
客户端代码:
void scanByRows(Connection connection, String tableName, String startRowKey, String stopRowKey) {
Table table = connection.getTable(TableName.valueOf(tableName));
// 创建Scan对象
Scan scan = new Scan();
scan.setStartRow(Bytes.toBytes(startRowKey));
scan.setStopRow(Bytes.toBytes(stopRowKey));
// 获取ResultScanner对象
ResultScanner scanner = table.getScanner(scan);
// 遍历ResultScanner对象,获取查询结果
for (Result result : scanner) {
byte[] valueBytes = result.getValue(Bytes.toBytes("cf"), Bytes.toBytes("field_name"));
String valueStr = new String(valueBytes,"utf-8");
log.info("多key查询的结果:{}",valueStr);
}
scanner.close();
table.close();
}
SCAN具体的流程说明:
其中for循环中每次遍历ResultScanner对象获取一行记录,实际上在客户端层面都会调用一次next请求。next请求整个流程可以分为如下几个步骤:
- next请求首先会检查客户端缓存中是否存在还没有读取的数据行,如果有就直接返回,否则需要将next请求给HBase服务器端(RegionServer)。
- 如果客户端缓存已经没有扫描结果,就会将next请求发送给HBase服务器端。默认情况下,一次next请求仅可以请求100行数据(或者返回结果集总大小不超过2M)
- 服务器端接收到next请求之后就开始从BlockCache、HFile以及memcache中一行一行进行扫描,扫描的行数达到100行之后就返回给客户端,客户端将这100条数据缓存到内存并返回一条给上层业务。
特别说明:
HBase 每次 scan 的数据量可能会比较大,客户端不会一次性全部把数据从服务端拉回来。而是通过多次 rpc 分批次的拉取。控制每次 rpc 拉取的数据量,就可以通过三个参数来控制。
- setNumberOfRowsFetchSize (客户端每次 rpc fetch 的行数)
- setColumnsChunkSize (客户端每次获取的列数)
- setMaxResultByteSize (客户端缓存的最大字节数)
这里需要考虑的是如何合理的设置这几个参数,官网也提供了对应的配置策略:
- hbase.client.scanner.caching - (setCaching):
HBase-0.98 默认值为为 100,HBase-1.2 默认值为 2147483647,即 Integer.MAX_VALUE。
Scan.next() 的一次 RPC 请求 fetch 的记录条数。配置建议:这个参数与下面的setMaxResultSize配合使用,在网络状况良好的情况下,自定义设置不宜太小, 可以直接采用默认值,不配置。
setBatch() 配置获取的列数,假如表有两个列簇 cf,info,每个列簇5个列。这样每行可能有10列了,setBatch() 可以控制每次获取的最大列数,进一步从列级别控制流量。配置建议:当列数很多,数据量大时考虑配置此参数,例如100列每次只获取50列。一般情况可以默认值(-1 不受限)。 - hbase.client.scanner.max.result.size - (setMaxResultSize):
HBase-0.98 无该项配置,HBase-1.2 默认值为 210241024,即 2M。
Scan.next() 的一次 RPC 请求 fetch 的数据量大小,目前 HBase-1.2 在 Caching 为默认值(Integer Max)的时候,实际使用这个参数控制 RPC 次数和流量。
配置建议:如果网络状况较好(万兆网卡),scan 的数据量非常大,可以将这个值配置高一点。如果配置过高:则可能 loadCache 速度比较慢,导致 scan timeout 异常 - hbase.server.scanner.max.result.size:
服务端配置。HBase-0.98 无该项配置,HBase-1.2 新增,默认值为 10010241024,即 100M。
该参数表示当 Scan.next() 发起 RPC 后,服务端返回给客户端的最大字节数,防止 Server OOM。
要计算一次扫描操作的RPC请求的次数,用户需要先计算出行数和每行列数的乘积。然后用这个值除以批量大小和每行列数中较小的那个值。最后再用除得的结果除以扫描器缓存值。 用数学公式表示如下:
RPC 返回的个数 = (row数 * 每行的列数)/ Min(每行列数,Batch大小) / Caching大小
Result 返回的个数 =( row数 * 每行的列数 )/ Min(每行列数,Batch大小)
问题现象:
为什么scan的时候会出现:
java.lang.RuntimeException: DoNotRetryIOException: Failed doing WSCAN on namespace=default, table=pick_rule_result, operationTimeout=320, time took=321, caused by com.alibaba.lindorm.client.exception.OperationTimeoutException: Failed after 321ms, operationTimeout=320, vmPauseTime=0, exceptions=, {2024-11-05 19:30:19(1730806219322) WSCAN},{321-321:WAIT_RESPONSE}
解析:scan逻辑和get逻辑差不多,也是在构建WSCAN对象的时候会指定glitchTimeout 和operationTimeout,真正查询的时候会重新进行赋值,跟get的最大区别这是,scan前置的一些配置比较多:
public Scanner(Scan scan) throws IOException {
try {
if (scan.getCaching() <= 0) {
scan.setCaching(defaultScannerCaching);
} else if (scan.getCaching() == 1 && scan.isReversed()) {
scan.setCaching(scan.getCaching() + 1);
}
WScan wScan = ElementConvertor.toLindormScan(scan);
this.limit = scan.getLimit();
this.wScanner = wideColumnService
.getScanner(tableNameWithoutNamespace, wScan);
}catch(IOException e){
throw ElementConvertor.toHBaseIOException(e);
}
}
//这里很多参数是用来优化查询的,比如setCacheBlocks,所以scan的时候我们会建议,尽可能少的查询cf和column
//同时进行limit和过滤器的配置
public static WScan toLindormScan(Scan scan) throws IOException {
checkScanSupport(scan);
WScan wScan = new WScan(scan.getStartRow(), scan.getStopRow());
wScan.setFamilyMap(scan.getFamilyMap());
//很有必要
wScan.setCacheBlocks(scan.getCacheBlocks());
wScan.setCaching(scan.getCaching());
if (scan.getMaxResultSize() > 0) {
wScan.setMaxResultSize(scan.getMaxResultSize());
}
if (scan.getRowOffsetPerColumnFamily() > 0) {
wScan.setOffset(scan.getRowOffsetPerColumnFamily());
}
if (scan.getMaxResultsPerColumnFamily() > 0) {
wScan.setLimit(scan.getMaxResultsPerColumnFamily());
}
if (scan.getLimit() > 0) {
wScan.setLimit(scan.getLimit());
}
wScan.setReversed(scan.isReversed());
//多版本的情况下取最新的版本
if (scan.getMaxVersions() > 1) {
wScan.setMaxVersions(scan.getMaxVersions());
}
TimeRange timeRange = scan.getTimeRange();
if (!timeRange.isAllTime()) {
wScan.setTimeRange(timeRange.getMin(), timeRange.getMax());
}
wScan.setRaw(scan.isRaw());
wScan.setIsolationLevel(toLindormIsolationLevel(scan.getIsolationLevel()));
//如果有必要,加上过滤器,减少数据的扫描
if (scan.getFilter() != null) {
wScan.setFilter(toLindormFilter(scan.getFilter()));
}
if (scan.getAttribute(ALLOW_FULL_TALBE_SCAN) != null) {
boolean allowFilter = Bytes.toBoolean(scan.getAttribute(ALLOW_FULL_TALBE_SCAN));
wScan.setAllowFiltering(allowFilter);
}
if (isQueryHotOnly(scan)) {
wScan.setQueryHotOnly(true);
} else if (isHotColdAutoMerge(scan)) {
wScan.setHotColdAutoMerge(true);
}
copyAttributes(wScan, scan.getAttributesMap());
return wScan;
}
超时的逻辑也是一样,所以适当的配置glitchTimeout 和operationTimeout,以及代码中注释的参数,就能提高我们scan的性能,这里需要指出的时候,scan的数据量过大,如果数据很分散就会存在跨region查询,也会影响性能,这里是个矛盾点。
3.3 Hbase如何DELETE
客户端代码
略,同上类似
当删除后,通过scan查询,数据数据量比较大的情况下就会出现如下异常:
org.apache.hadoop.hbase.DoNotRetryIOException:
Timeout while execute wscan,
check if too many invalid result skippped, timeout 200 ms
为什么会这样呢,在HBase中,数据删除与清理主要涉及以下几个方面:
- 删除操作(Delete):用于标记数据行为删除,实际上数据仍然存在HBase表中,只是标记为删除。
- 过期操作(TTL,Time To Live):用于自动删除数据,根据数据创建时间加上TTL值,当数据超过TTL时间后自动删除。
- 数据压缩:用于减少存储空间占用,提高I/O性能。
- 数据清理:用于删除标记为删除的数据,以释放存储空间。
当执行DELETE操作后,会按照如下方式进行数据的删除:
- 逻辑删除: HBase不会立即在物理存储上删除数据,而是采用逻辑删除的方式。当执行删除操作时,HBase会将一条特殊的删除标记(Tombstone)插入到相应的数据单元中。这个删除标记指示这个数据单元已被删除,并且会在数据保留的时间后清理掉。
- Major Compaction(主要合并): HBase定期执行Major Compaction操作,它会合并和清理数据文件,删除标记和过期数据。Major Compaction将不再需要的数据清理掉,从而释放磁盘空间,并提高读取性能。
regionserver初始化的时候会初始化两个与compact相关的线程是:compactionChecker和compactSplitThread。
compactionChecker周期性地检查是否有compact请求,如果出现了compact请求,
那么将请求以及请求的周边信息一起包装成CompactionContext,
将CompactionContext交付给regionserver的compactSplitThread;
compactSplitThread会根据compact的类型为它分配合适的线程池,
并包装成compactSplitThread内部类CompactionRunner交给对应线程池,
线程池调度CompactionRunner,执行它的run方法,完成compact操作。
在 HBase 中,Major Compaction 通常会在以下条件触发:
1. 表达到一定的大小。
2. 规定的时间间隔达到。
3. 手动触发。执行命令:major_compact '表名'
-
Minor Compaction(次要合并): 在Major Compaction之外,HBase还执行Minor Compaction,它用于合并较小的数据文件以优化存储布局,但不会清理删除标记。
- Minor操作只用来做部分文件的合并操作以及包括minVersion=0并且设置ttl的过期版本清理,不做任何删除数据、多版本数据的清理工作。也即是说选取一些小的、相邻的StoreFile将他们合并成一个更大的StoreFile,在这个过程中不会处理已经Deleted或Expired的Cell。一次Minor Compaction的结果是更少并且更大的StoreFile。
- Major操作是对Region下的HStore下的所有StoreFile执行合并操作,最终的结果是整理合并出一个文件。这个过程还会清理三类无意义数据:被删除的数据、TTL过期数据、版本号超过设定版本号的数据。另外,一般情况下,Major Compaction时间会持续比较长,整个过程会消耗大量系统资源,对上层业务有比较大的影响。因此线上业务都会将关闭自动触发Major Compaction功能,改为手动在业务低峰期触发。
- chore方法中needsCompaction判断的是minor compact是否需要执行。从chore方法中可以直观的看到major compact是通过isMajorCompaction方法判断的这是很多判断条件的合成,其中最为重要的一个是hbase.hregion.majorcompaction设置的值,也就是判断上次进行majorCompaction到当前的时间间隔,如果超过设置值,则满足一个条件,同时另外一个条件是compactSelection.getFilesToCompact().size() < this.maxFilesToCompact。
- 通过设置hbase.hregion.majorcompaction = 0可以关闭CompactionChecke触发的major compaction,但是无法关闭用户调用级别的majorcompact。isMajorCompaction,最终实现的判断是来自RatioBasedCompactionPolicyd的isMajorCompaction方法
-
删除标记的清理: 当Major Compaction执行时,HBase会检查数据单元中的删除标记,如果数据的所有版本都已被标记为删除,则在Major Compaction中清理掉这些数据。
源码主要看下
org.apache.hadoop.hbase.regionserver.HRegionServer这个,核心方法是
org.apache.hadoop.hbase.regionserver.HStore.chore()
protected void chore() {
//遍历所有的region节点
for (HRegion hr : this.instance.onlineRegions.values()) {
// If region is read only or compaction is disabled at table level, there's no need to
// iterate through region's stores
if (hr == null || hr.isReadOnly() || !hr.getTableDescriptor().isCompactionEnabled()) {
continue;
}
//遍历所有region节点上的Hstore,主要是去判断eedsCompaction
for (HStore s : hr.stores.values()) {
try {
//HStore
long multiplier = s.getCompactionCheckMultiplier();
assert multiplier > 0;
if (iteration % multiplier != 0) {
continue;
}
//是否需要压缩
if (s.needsCompaction()) {
// Queue a compaction. Will recognize if major is needed.
this.instance.compactSplitThread.requestSystemCompaction(hr, s,
getName() + " requests compaction");
} else if (s.shouldPerformMajorCompaction()) {
s.triggerMajorCompaction();
if (
majorCompactPriority == DEFAULT_PRIORITY
|| majorCompactPriority > hr.getCompactPriority()
) {
this.instance.compactSplitThread.requestCompaction(hr, s,
getName() + " requests major compaction; use default priority", Store.NO_PRIORITY,
CompactionLifeCycleTracker.DUMMY, null);
} else {
this.instance.compactSplitThread.requestCompaction(hr, s,
getName() + " requests major compaction; use configured priority",
this.majorCompactPriority, CompactionLifeCycleTracker.DUMMY, null);
}
}
} catch (IOException e) {
LOG.warn("Failed major compaction check on " + hr, e);
}
}
}
iteration = (iteration == Long.MAX_VALUE) ? 0 : (iteration + 1);
}
}
需要注意的是,HBase的删除操作并不是实时的,而是通过Compaction过程逐步进行的。这意味着一条数据的删除标记可能会在Compaction之前存在一段时间,直到Compaction执行并将其清理。这种机制有助于保持HBase的高性能和高吞吐量,同时确保数据的持久性和一致性。总之,HBase通过逻辑删除和Compaction机制来处理数据的删除操作。
三、优化建议
针对前面提到的查询超时情况,通常我们会做以下优化操作:
- 大数据量:增加更多的预分区,限制查询的范围和结果集大小,通过添加条件过滤器来减少返回的数据量。尽量使用查询的时间范围、主键或其他索引来加速检索。
- 更合理的设计:检查数据表的索引设计,避免全表扫描,如果是需要点查,数据量大的情况下,通过MD5做哈希打散,或者加盐都可以,确保使用合适的索引以提高查询性能。
- 负载过高:监控 Lindorm 的 CPU、内存和 I/O 使用情况,考虑使用预分区(pre-split)和数据重均衡来分散负载;
- 长耗时的事务:优化事务逻辑,避免长时间持有锁,并实现更有效的数据访问模式。
- STRONG CONSISTENCY配置:使用 最终一致性 代替 强一致性 提高性能。
- 优化数据倾斜:优化数据模型,避免数据倾斜,确保数据分布均匀,这里一般和rowkey的设计、cf下的columns个数都有关系;
- 合理的客户端配置:查看并调整客户端超时配置,如查询超时、RPC 超时等,确保这些配置适合你的使用场景;