https://github.com/sgp2004/JavaTools 代码地址

HBase客户端的行锁会对相同rowkey的读写造成很大影响,同一个进程并发更新rowkey的计数有可能造成阻塞(场景 热门短链点击增加 热门微博评论数).

例如一个线上问题:

转发微博

抱歉,此微博已被作者删除。查看帮助:http://t.cn/zWSudZc

| 转发| 收藏| 评论

所有被删除的微博里短链被引用的计数要减一,结果因为微博内容删除,只剩一个帮助短链,计数都减到帮助短链里,导致服务器响应缓慢

分析行锁关键代码总结一下:

client端:

1 HTable类代码,发现 lockRow 和 unlockRow方法都没有被使用到,0.96某个jira说到的client端去除lock不知道有什么用?只是移除无用代码?

2 HRegionServer类的lockRow方法只在HTable中调用。但是在测试中并没有执行到这个lockRow方法。

推测应用调用jar包时,在client端并不存在锁的问题。

server端:

HRegion 自行生成lockId并阻塞同一行的操作  ,去掉lockid从客户端的传递,增加MVCC,优化请求。

所以只是去掉了显式锁调用。

废话不说,上测试代码

@Test
    public void testMultiAdd() throws InterruptedException {
        for (int i=0;i<100;i++){
            final int finalI = i;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println("start thread"+finalI);
                    long  time = System.currentTimeMillis();
                    int loop=0;
                    while (loop++<1000)
                        //commonDao.insert("test1","f1","key","value"+ finalI);         //for one same key test1   1000 loop,100 threads cost 53s   ;if increase to 10000 loop cost 530s and may be timeout
                        //commonDao.insert("test1"+finalI+loop,"f1","key","value"+ finalI); // for different key  1000 loop,100 threads cost 14s,10000loop cost 113s
                        //commonDao.delete("test1");         //for one same key test1   1000 loop,100 threads cost 53s
                        //commonDao.delete("test1"+finalI+loop); // for different key  1000 loop,100 threads cost 13s
                        //commonDao.incr("test2","f1","key",1l);  // for  one same key test2  1000 loop,100 threads cost 59s
                       // commonDao.incr("test2"+finalI+loop,"f1","key",1l);  // for different key  1000 loop,100 threads cost 15s
                        commonDao.getStrValue("test1","f1","key");  // for  one same key test1  100 loop,100 threads cost 59s ??? why is it so slow?
                       // commonDao.getStrValue("test1"+finalI+loop,"f1","key");  // for different key  1000 loop,100 threads cost 12s
                    System.out.println(finalI+"thread stop,use time:"+(System.currentTimeMillis()-time));
                }
            }) .start();
        }

        TimeUnit.DAYS.sleep(3l);
    }
commonDao是对原始HBase client的简单封装,隐藏表名,对常用字符串 整数 长整数进行封装bytes操作,

运行耗时表

100个线程 1000次循环,耗时(单位s):

操作

单rowkey

变化的rowkey

insert

53

14

delete

53

13

计数加incr

59

15

get

600

12

对单key的写操作会出现超时,get操作比其他要慢10倍。并且get操作必须在delete之后,insert之后可以在10s左右运行完毕。

https://issues.apache.org/jira/browse/HBASE-7263 中 描述了 HBase的read/updates 流程: 

(1)  Acquire RowLock

(1a) BeginMVCC + Finish MVCC

(2)  Begin MVCC

(3)  Do work

(4)  Release RowLock

(5)  Append to WAL

(6)  Finish MVCC

Write-only operations (e.g. puts) 除了步骤1a,与上相同。

疑问:update和write有何区别?

Remove explicit RowLocks in 0.96

 

一、insert分析

先分析insert  ,重点步骤在 HConnnectionManager 的 processBatchCallback方法

在retry 次数内进行一个循环

1 寻找对应region  HRegionLocation loc = locateRegion(tableName, row.getRow());   

step1 locateRegion 时首先会加锁 regionLockObject

This block guards against two threads trying to load the meta
          // region at the same time. The first will load the meta region and
          // the second will use the value that the first one found.

 

 

step2 生成一个metakey

byte [] metaKey = HRegionInfo.createRegionName(tableName, row,
        HConstants.NINES, false);

step3 查询metakey所在region

// Query the root or meta region for the location of the meta region
            regionInfoRow = server.getClosestRowBefore(
            metaLocation.getRegionInfo().getRegionName(), metaKey,
            HConstants.CATALOG_FAMILY);

得到的regionInfoRow 信息,Result类型,打印为kv:keyvalues={.META.,,1/info:regioninfo/1353046230286/Put/vlen=34/ts=0, .META.,,1/info:server/1353046237800/Put/vlen=40/ts=0, .META.,,1/info:serverstartcode/1353046237800/Put/vlen=8/ts=0, .META.,,1/info:v/1353046230286/Put/vlen=2/ts=0}

 

转换server信息为region server的ip和端口

value = regionInfoRow.getValue(HConstants.CATALOG_FAMILY,
              HConstants.SERVER_QUALIFIER);
              ipAndPort:75-25-171-yf-core.jpool.sinaimg.cn:60020


这样就得到了row对应regionServer的地址

 

我们再回到processBatchCallback 的 locateRegion

if (useCache) {
        location = getCachedLocation(tableName, row);
        if (location != null) {
          return location;
        }
      }

 

第二次获取时会从cache中获取,不存在以上的锁的问题。所以第二次调用时可以回到processBatchCallback方法 往下进行

1 生成action

Action<R> action = new Action<R>(row, i);
            lastServers[i] = loc;
            actions.add(regionName, action);

2 发送请求

Map<HRegionLocation, Future<MultiResponse>> futures =
            new HashMap<HRegionLocation, Future<MultiResponse>>(
                actionsByServer.size());

        for (Entry<HRegionLocation, MultiAction<R>> e: actionsByServer.entrySet()) {
          futures.put(e.getKey(), pool.submit(createCallable(e.getKey(), e.getValue(), tableName)));
        }

3 收集结果

没有发现有rowLock使用

HTablePool代码:

class PooledHTable implements HTableInterface {

private HTableInterface table; // actual table implementation

@Override
public RowLock lockRow(byte[] row) throws IOException {
return table.lockRow(row);
}

搜索lockRow  只找到在 “return table.lockRow(row);” 中调用,搜索HTable的lockRow方法也只在PooledHTable中使用,没发现外部使用,困惑。

lockRow方法调用了HRegionServer的lockRow方法。两个方法都在“1 Remove rowlocks as a client side API (https://issues.apache.org/jira/browse/HBASE-7315 )”  被移除。测试屏蔽掉这部分代码也没有任何异常,debug也没有打印,说明没有执行到。

client.Put

public Put(byte [] row, long ts, RowLock rowLock) {
if(row == null || row.length > HConstants.MAX_ROW_LENGTH) {
throw new IllegalArgumentException("Row key is invalid");
}
this.row = Arrays.copyOf(row, row.length);
this.ts = ts;
if(rowLock != null) {
this.lockId = rowLock.getLockId();
}
}

去掉rowLock.getLockId(); 也没有影响

至此看出在client端是没有锁的,只会设置lockId,也需要传入RowLock才设置。生成Get时,我们的调用代码默认也只生成无锁的Get对象。0.96中计划把无用代码去除。

那么对客户端设置lockId是否有用?

服务器端代码  HRegion.java:

public Integer getLock(Integer lockid, byte [] row, boolean waitForLock)
  throws IOException {
    Integer lid = null;
    if (lockid == null) {
      lid = internalObtainRowLock(row, waitForLock);
    } else {
      if (!isRowLocked(lockid)) {
        throw new IOException("Invalid row lock");
      }
      lid = lockid;
    }
    return lid;
  }

传入的lockid需要在服务器端lockIds注册,传入null时服务器端会生成id,存入lockIds,传入lockid则会因为没有入口存入lockIds抛异常,经试验测试

 Get get = new Get(row,new RowLock(row,1l));

Put put = new Put(Bytes.toBytes(rowkey),new RowLock(Bytes.toBytes(rowkey),1l));

确实是会抛异常,很坑爹的public 构造方法, 不过没有在HRegion抛,而是在HRegionServer?lockid在服务器端何时被初始化的?

Caused by: org.apache.hadoop.ipc.RemoteException: org.apache.hadoop.hbase.UnknownRowLockException: Invalid row lock
    at org.apache.hadoop.hbase.regionserver.HRegionServer.getLockFromId(HRegionServer.java:2349)
    at org.apache.hadoop.hbase.regionserver.HRegionServer.delete(HRegionServer.java:2259)
    at sun.reflect.GeneratedMethodAccessor30.invoke(Unknown Source)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
    at java.lang.reflect.Method.invoke(Method.java:597)
    at org.apache.hadoop.hbase.ipc.WritableRpcEngine$Server.call(WritableRpcEngine.java:364)
    at org.apache.hadoop.hbase.ipc.HBaseServer$Handler.run(HBaseServer.java:1326)

    at org.apache.hadoop.hbase.ipc.HBaseClient.call(HBaseClient.java:1021)
    at org.apache.hadoop.hbase.ipc.WritableRpcEngine$Invoker.invoke(WritableRpcEngine.java:150)
    at $Proxy6.delete(Unknown Source)
    at org.apache.hadoop.hbase.client.HTable$4.call(HTable.java:714)
    at org.apache.hadoop.hbase.client.HTable$4.call(HTable.java:712)
    at org.apache.hadoop.hbase.client.ServerCallable.withRetries(ServerCallable.java:163)

 HRegionServer.java:

Integer getLockFromId(long lockId) throws IOException {
    if (lockId == -1L) {
      return null;
    }
    String lockName = String.valueOf(lockId);
    Integer rl = rowlocks.get(lockName);
    if (rl == null) {
      throw new UnknownRowLockException("Invalid row lock");
    }
    this.leases.renewLease(lockName);
    return rl;
  }

 

我们接下来看,server端 HRegion的put方法(未完待续)

 

 

HBase 0.96进行了很大的变动,rpc调用通过hbase-protocol模块实现,在其中重写了锁方法

Over in HBASE-7263 there has been some discussion about removing support

for explicit RowLocks in 0.96.  This would involve the following:

- Remove lockRow/unlockRow functions in HTable and similar   。 replaces instances of RowLock with NullType.

- Remove constructors for Put/Delete/Increment/Get that take RowLocks

- functions in HRegion no longer take lockIds (checkAndPut, append,

increment, etc).  This would affect coprocessors that call directly into

those functions.

1 Remove rowlocks as a client side API (https://issues.apache.org/jira/browse/HBASE-7315 )

2. Remove rowlocks from server code and replace it with better mechanism (https://issues.apache.org/jira/browse/HBASE-7263 )

 

 The reasoning is as follows:

1) RowLocks are broken

 They are only kept in the memory associated with the region, so on a

split, region move, RS crash, they just disappear

 

2) 0.96 is special

Now seems like a good time to clean things up since we've made some

incompatible changes already (e.g. protobufing) and we could have a cleaner

client implementation

 

3) RowLocks have been deprecated "in spirit" for awhile

Here's a post from 2009 cautioning against their use:

http://bb10.com/java-hadoop-hbase-user/2009-09/msg00239.html

and a more recent example:

http://permalink.gmane.org/gmane.comp.java.hadoop.hbase.user/23488

 

4) RowLocks are hard to use effectively

Clients can deadlock or starve themselves, either by forgetting to release

the RowLocks or by starving other non-contending row operations by

occupying server handlers stuck waiting to acquire the locks.