构建和运维HBase集群是一个非常有挑战性的工作。HBase凭借其在海量数据的良好的扩展性和高效的读写能力,受到越来越多公司的重视。
在公司里,HBase越来越受欢迎。希望通过HBase读写数据的产品越来越多,在兴奋之余,头疼的问题也来了。毕竟,作为线上的产品,我们不希望过多人随意的访问,会照成很多潜在的风险,比如误删,误操作。但是,如果所有事情都有管理员处理,沟通的代价就会很高,而且管理员不得不处理一些Application相关的工作,导致管理低效。访问控制,就成为了一个很重要的需求。
此外,现在部署在云计算公有云的HBase集群也在日益增多,如果没有访问控制,存储其中的数据可以随意删改,对企业而言,也是无法接受的。那么HBase 的访问控制就成为了一个迫切紧急的任务。
Apache Hadoop 在0.20版本之后开始添加Kerberos认证技术。然而,直到0.21版本,这项工作仍未结束。因此,在0.22版本之前,这项特性的有效性和稳定性均不成熟。此外,Yahoo! Distribution of Hadoop 的0.20.S版本和Cloudera的第一个稳定的CDH3发布中也会加入这项安全支持。
HBase的访问控制是也是基于Kerberos的,现在,在我们的产品环境中,已经集成了Hadoop和HBase的访问控制功能。在后面的文章里,我将详细阐述HBase访问控制的具体功能和实现机制。供大家参考。
Secure HBase的特征。
1. 和Yahoo!Distribution of Hadoop 一样,基于Kerberos。在我们的环境中,我们同时部署了Secure Hadoop和Secure HBase, 关于Secure Hadoop,可以参考(Hadoop-4487)
2. 实现Coprocessor Framework, 允许HBase 管理员在Regionserver中载入定制的代码,关于Coprocessor,请参考(HBase-2001)
3. 实现了一个AccessController的Coprocessor,在代码中加入了Access Control。请参考(HBase-3025)
4. 支持基于表和column family的访问控制,不支持row级别。
认证机制
认证机制利用了Java的JAAS。 当HBase接受到透过RPC过来的请求时,将调用JAAS的doAs函数执行这个命令。具体的代码变动,可以看下面的代码比较。
1. diff --git a/src/main/java/org/apache/hadoop/hbase/ipc/HBaseServer.java b/src/main/java/org/apache/hadoop/hbase/ipc/HBaseServer.java
2.
3. index 40473e3..2dec6b5 100644
4.
5. --- a/src/main/java/org/apache/hadoop/hbase/ipc/HBaseServer.java
6.
7. +++ b/src/main/java/org/apache/hadoop/hbase/ipc/HBaseServer.java
8.
9. @@ -58,6 +58,7 @@
10.
11. import java.util.concurrent.ConcurrentHashMap;
12.
13. import java.util.concurrent.ExecutorService;
14.
15. import java.util.concurrent.Executors;
16.
17. import java.util.concurrent.LinkedBlockingQueue;
18.
19. +import java.security.PrivilegedExceptionAction;
20.
21. import org.apache.commons.logging.Log;
22.
23. import org.apache.commons.logging.LogFactory;
24.
25. @@ -1060,7 +1061,7 @@ public abstract class HBaseServer implements RpcServer {
26.
27. ByteArrayOutputStream buf = new ByteArrayOutputStream(buffersize);
28.
29. while (running) {
30.
31. try {
32.
33. - Call call = myCallQueue.take(); // pop the queue; maybe blocked here
34.
35. + final Call call = myCallQueue.take(); // pop the queue; maybe blocked here
36.
37. if (LOG.isDebugEnabled())
38.
39. LOG.debug(getName() + ": has #" + call.id + " from " +
40.
41. @@ -1073,7 +1074,32 @@ public abstract class HBaseServer implements RpcServer {
42.
43. CurCall.set(call);
44.
45. // TODO: simple auth -- store user in context
46.
47. try {
48.
49. - value = call(call.connection.protocol, call.param, call.timestamp); // make the call
50.
51. + Object obj = (Writable)call.connection.ticket.doAs(new PrivilegedExceptionAction<Object>() {
52.
53. + public Object run() throws IOException, InterruptedException {
54.
55. + return call(call.connection.protocol, call.param,
56.
57. + call.timestamp);
58.
59. + }
60.
61. + });
62.
63. +
64.
65. + if (obj instanceof Writable) {
66.
67. + value = (Writable)obj;
68.
69. + }
70.
71. + else {
72.
73. + // doAs() return value could not be converted to Writable:
74.
75. + // probably should throw an exception here in that case.
76.
77. + }
78.
79. } catch (Throwable e) {
80.
81. LOG.debug(getName()+", call "+call+": error: " + e, e);
82.
83. errorClass = e.getClass().getName();
权限和授权
HBase的权限定义比较简单。比如我们不会区分是insert还是update,而统一是Put。
HBase的权限可以分为基于行和基于schema。基于行包括对表的读写。基于schema的包括了增加一列或者增加一个family,删除表之类的操作。前者是在Regionserver中运行,但是后者是在master中执行。在master中运行的不会在Coprocessor Framework中运行,但是我们上面的代码将运行在master和Regionserver中,所以不论用户是执行基于行的操作还是基于schema的操作,都可以实现访问控制。
表的所属权
所属权定义在表的TableDescriptor,对应于 .META. 中的regioninfo:owner这一栏。表的所有者有权利读写表和删除表。任何人有权利读 –ROOT— 和 .META. , 但只有HBase 管理员修改表的所属权属性。比如,如果有一个客户端希望能读表的信息,但是你担心他会误删表的数据,就可以建立一个对某个表只读权限的用户。
Coprocessor
Coprocessor已经融合到了HBase 0.90 的版本中。如果enable了Coprocessor,RegionServer会从配置文件hbase-site.xml 中读取Coprocessor的配置。下面是Access Control coprocessor的配置片段。
<property>
<name>hbase.coprocessor.default.classes</name>
<value>org.apache.hadoop.hbase.security.rbac.AccessController</value>
</property>
Coprocessor的接口定义了一组pre- 和post- 的函数,每个对应于客户端的一个request。
比如,客户端的函数Get(),在Coprocessor中就会定义preGet()和postGet(), preX()和postX()会抛出CoprocessorException。可以定义任意数量的Coprocessor。其实这个和动态代理类似。如图,定义了三个preGet()和三个postGet(),如果遇到异常,异常将直接抛给用户终止后面的进行。
利用pre-X方法,我们可以实现访问控制。比如,如果一个客户端发送Put(T,Row)到一个Regionserver,Regionserver会首先调用用于认证的Coprocessor函数prePut检测该用户是否对该表有写权限。如果有,则写入,否则,抛出AccessDeniedException异常。
上图是preX和permission的对应关系。
吃外,我们用preX做permission 检测的同时,也使用了两个postX方法——postOpen()和postPut() 来存储Permission信息。
如何存储Permissions
表Table1的Permission会存储在三个地方。
1. Table1的第一个region在 .META.表所在的行
2. Zookeeper中/hbase/acl/table1节点
3. 所有管理table1的RegionServer在内存中的权限镜像。
.META.
我们在.META.上面加了一个colum family,acl:。对于任意一个表T, Permission信息存储在该表在.META.的第一行,存储在acl:U列中(U表示username),值可以是R,W,RW。这是Permission信息存储的原始位置,Zookeeper和regionServer的信息都是从这里拷贝的。当Secure HBase第一次启动的时候,.META. regions 首先被load到RegionServer,之后拷贝到Zookeeper和Regionserver的镜像。当权限被管理员改变,也是先修改.META. 然后到Regionserver 的mirrors。
Zookeeper
Zookeeper在.META和Regionserver镜像中起到一个桥梁的作用。在Zookeeper中,Permission 信息会存放在/hbase/acl/T, 所有含有表T region的Regionserver都会设置watch来监视Permission信息的改变。
Regionserver的内存镜像
每一个Regionserver都会为他服务的regions维护一个内存权限镜像。镜像的内容来自于拷贝相应Zookeeper节点的Permission信息得到。Zookeeper和Regionserver的镜像之间的一致性是靠Zookeeper Watches保证的。
Zookeeper起的作用
当一个Regionserver打开一个.META. region时,AccessController:postOpen() 扫描出表在.META.中所有的第一行,建立Zookeeper 节点/hbase/acl/T,acl:columns的每行都被拷贝到Zookeeper的节点上。形成一个user=>Permission的映射表。
维护一致性: 从.META.到Zookeeper
当管理员修改了Permission信息时,实际上是对.META.的第一行出发了put操作。这会触发Coprocessor的PostPut 方法。在PostPut里,我们将修改的权限保存到对应的/hbase/acl/T节点中。
维护一致性: 从Zookeeper到Regionserver 权限镜像
下图表示两个Regionserver,每一个都有一个ZKPermissionWatcher,实现了nodeCreated()和nodeChanged()方法。对于每一个watcher 方法,改变对应的Zookeeper节点都会触发Permission镜像的刷新。
注意,Regionserver仅仅使用本地的权限镜像,而不是使用Zookeeper或者.META.来认证客户端的请求。这是处于性能的考虑。但是,如何确保每次都执行正确的认证信息?这要求Regionserver的镜像和.META.中的Permission完全一致。因为表T它的第一个region对应的.META.的那个region被打开之后才能被打开,这就保证了regionserver总是先于任何客户端之前导入Permission信息。
总结
1. META中的acl: 这个family用于存储Permission信息。Zookeeper能够自动快速地将权限的改动更新到本地的权限缓存中。
2. 一个用户的操作既不需要查询META也不需要查询Zookeeper。最新的信息能从本地缓存中获得。
3. 修改权限不需要disable表,对表权限的更新将立即并自动地通过Zookeeper从META表更新到regionserver
实例分析
现在,我们来看一个权限处理的例子
S代表处理的Regionserver, 而U代表用户。
在第三步,doAs()将HBase的系统用户转化为一个普通的U用户,类似unix的sudo su,
第五步,如果认证失败,就会返回AccessDeniedException。