用Go语言操作HBase2.x实现列查询结果过滤——基于github.com/pingcap/go-hbase的魔改实践

古翠码翁 2022-06-30 10:34:13

注:本文系原创,亦发表于作者微信公众号,转载请注明出处。

一、前言

上一篇文章《如何用Go语言快速方便操作HBase2.0.x —— 基于github.com/pingcap/go-hbase的Hack实践》(以下简称“Hack实践”)中笔者介绍了如何改造go-hbase库以使得Go语言开发应用程序兼容访问2.x版本的HBase系统。

最近在项目实践中,笔者又遇到了新的需求——对HBase检索的数据列结果按条件进行过滤,类似SQL语言的WHERE条件子句功能。

HBase是支持对扫描(等同于SQL的SELECT)结果进行过滤的,以下是一个命令行终端的例子:

假设我们有一个HBase数据表,名称'demo_tab',一共10行,对应行键是'row_1' ... 'row_10',两列分别称作'info:val_1', 'info:val_2'。现在要选取所有 val_1值为aa的记录。

 图1. HBase带有列过滤功能的数据扫描示例

由图1可见,红框是数据表中所有数据的记录,按照行键统计一共有10行。蓝色框部分是在执行前导入的组件,后文程序分析时候也会提到。星标的语句是带有列过滤的数据查询命令,具体目的是将'info:val_1'这一列等于'aa'的所有行输出。下面结果用绿色框标记了'info:val_1'这一列的值,可以看到结果符合命令行给出的条件。

本文就是介绍如何用Go语言开发一个程序,实现上述功能。继续用在《Hack实践》一文提到并修改的go-hbase包,在这个项目中,笔者发现go-hbase包提供的scan接口功能很简单,只有最直接的数据扫描,仅支持有限的几个参数,包括设定行键起止范围,时间区间范围等,并没有提供对扫描结果进行条件过滤的功能,也就是图1给出的列过滤功能无法直接实现。那么能否自己实现这个功能呢?在分析了这个程序包后,笔者发现有关列过滤等机制的协议部分已经包含在程序包中,只要适当改造程序接口就可以实现这个功能。下面笔者就介绍一下魔改go-hbase包实现列过滤查询的经验。

为方便起见,这里再次贴一下go-hbase程序包的代码树形结构。

├── action.go

├── action_test.go

├── admin.go

├── admin_test.go

├── call.go

├── client.go

├── client_ops.go

├── client_test.go

├── column.go

├── column_test.go

├── conn.go

├── del.go

├── del_test.go

├── get.go

├── get_test.go

├── iohelper

│   ├── multireader.go

│   ├── pbbuffer.go

│   └── utils.go

├── LICENSE

├── proto

│   ├── AccessControl.pb.go

│   ├── Admin.pb.go

│   ├── Aggregate.pb.go

│   ├── Authentication.pb.go

│   ├── Cell.pb.go

│   ├── Client.pb.go

│   ├── ClusterId.pb.go

│   ├── ClusterStatus.pb.go

│   ├── Comparator.pb.go

│   ├── Encryption.pb.go

│   ├── ErrorHandling.pb.go

│   ├── Filter.pb.go

│   ├── FS.pb.go

│   ├── HBase.pb.go

│   ├── HFile.pb.go

│   ├── LoadBalancer.pb.go

│   ├── MapReduce.pb.go

│   ├── Master.pb.go

│   ├── MultiRowMutation.pb.go

│   ├── RegionServerStatus.pb.go

│   ├── RowProcessor.pb.go

│   ├── RPC.pb.go

│   ├── SecureBulkLoad.pb.go

│   ├── Snapshot.pb.go

│   ├── Tracing.pb.go

│   ├── VisibilityLabels.pb.go

│   ├── WAL.pb.go

│   └── ZooKeeper.pb.go

├── protobuf

│   ├── AccessControl.proto

│   ├── Admin.proto

│   ├── Aggregate.proto

│   ├── Authentication.proto

│   ├── Cell.proto

│   ├── Client.proto

│   ├── ClusterId.proto

│   ├── ClusterStatus.proto

│   ├── Comparator.proto

│   ├── Encryption.proto

│   ├── ErrorHandling.proto

│   ├── Filter.proto

│   ├── FS.proto

│   ├── HBase.proto

│   ├── HFile.proto

│   ├── LoadBalancer.proto

│   ├── MapReduce.proto

│   ├── Master.proto

│   ├── MultiRowMutation.proto

│   ├── RegionServerStatus.proto

│   ├── RowProcessor.proto

│   ├── RPC.proto

│   ├── SecureBulkLoad.proto

│   ├── Snapshot.proto

│   ├── Tracing.proto

│   ├── VisibilityLabels.proto

│   ├── WAL.proto

│   └── ZooKeeper.proto

├── put.go

├── put_test.go

├── README.md

├── result.go

├── result_test.go

├── scan.go

├── scan_test.go

├── service_call.go

├── types.go

└── utils.go

图2. 代码树形结构

 

二、程序包代码修改

首先开始研究的就是代码根目录下scan.go文件,从名字就可以猜测到这个是scan功能的实现的入口。这个接口工作的机制大致是:

  1. 用NewScan()方法生成一个Scan结构体;
  2. 通过一系列属性设置接口,设置一些必要的基础参数;
  3. 通过Next() 方法开始与HBase交互,发送查询请求,接收结果并返回;

对外开放的工作接口最重要的就是Next(),这里面实际触发了Scan操作。进一步挖掘,发现Next()方法内部调用的getData()方法才是核心, 顺便提到一点,Go语言中,程序包对外开放访问的方法或者数据变量都以大写首字母开头,小写的方法、数据和定义都仅仅包内可见。getData()代码片段由图3所示。

 图3. getData() 方法代码片段

图3 红色方框标记出的部分看上去好像是与HBase交互的RPC协议部分,事实也是如此。到现在,整个scan.go文件中,还没有发现任何关于过滤器的内容。不过按图索骥,去查看RPC协议的实现,特别是proto.ScanRequest的定义,在proto/Client.pb.go中。

图4. ScanRequest数据结构

 ScanRequest中有一个成员,叫做Scan,是一个指向proto.Scan结构的指针(见图4,在包内proto包名被省略),而且根据代码中的注解,这个结构明显要用于protobuf序列化与远程服务进行交互的。那么再看proto.Scan的定义(图5)。

 图5. proto.Scan数据结构

由图5可见,我用红色方框做了标记,这里赫然有一个 Filter成员。那么是不是我们要找的过滤器接口呢?按照线索,找到Filter定义,然后我把proto/Filter.go以及相关的proto/Comparator.go实现读过后,基本可以确定,这里就是过滤器的入口。而且,相关的过滤器使用协议部分都已经定义好,只要正确调用接口就可以了。

再回到图3,req := &proto.ScanRequest 这句赋值,就是连接外部请求和底层协议的桥梁,设定好一个过滤器,通过里面的 Scan: &proto.Scan{} 把过滤器传递给RPC,就可以实现想要的效果。

通过以上分析,就有了实现路径,只需要修改2处 代码:

1. 修改scan.go文件中,关于Scan结构体的定义,这个结构是开放给外部的接口,让用户将过滤器传递进来。图6 是scan.go中定义的Scan结构体修改前后代码对比,用红色框出。

   图6. Scan结构体修改前(左)和修改后(右)对比

2.  修改scan.go文件中,getData()方法内ScanRequest的赋值,将用户定义好的,在Scan结构体内部的过滤器传递给ScanRequest中的那个Scan指针,再通过底层协议机制把过滤器发送给HBase。图7 是修改后的getData()方法,可与图3对比。

 图7. 修改后的scan.go文件中getData()方法,修改用红框标出

至此,go-hbase程序包修改就已完成,看上去很简单,不过真正定位要修改的地方,还是要花一番功夫深入分析的。

 

三、程序示例

按照笔者实践的经验,修改程序包只完成了工作一小部分,真正难点是如何正确使用它,在应用程序中将过滤器各个参数设置好,并正确序列化后传递给服务器端。为此,在这里给出两个例子,一个简单的完全对应图1应用的例子,用一个单列过滤器对数据进行等值判定过滤;另一个是复杂一些的例子,通过2个过滤器对2列数据进行联合过滤。通过这两个例子,用户可以在其上演变更复杂的应用。

这两个例子都在笔者工作环境中编译通过并正确运行,用户也可以将代码复制下来,搭建好一样的环境进行测试。

3.1 简单例子

这里假设读者已经熟悉Go语言的语法,涉及编程语法细节不做过多介绍。proto.Comparator和proto.Filter是两个容器接口,可以认为是抽象的过滤器和比较器。根据具体实现,将实际设设置好的过滤器(这个例子中是SingleColumnValueFilter)和实际的比较器(例子中是BinarComparator)序列化后装入这两个容器,然后发出扫描请求。由于HBase中数据都是按照字节流存放的,没有数据类型概念,因此对于字符串数据,按照二进制字节串方式比较,这也是为什么例子中的比较接口用的ByteArrayComparable。另外,在Comparator和Fitler中,对应的实际比较器和过滤器的名字不要写错,要写全路径名。比如BinaryComparaotor要写成org.apache.hadoop.hbase.filter.BinaryComparator,我猜测服务器要根据传入的这个名称,找到具体的java组件包,也就是图1给出的例子中对应的import操作。这是一个非常容易出错的点,在实践中,笔者经过多方摸索,才知道这个名字正确设置方式。

package main

import (
    "fmt"
    "log"
    hbase "github.com/pingcap/go-hbase"  // 改造后的HBase程序包
    pb "github.com/golang/protobuf/proto"  // protobuf序列化工具
    proto "github.com/pingcap/go-hbase/proto"  // HBase程序包中的协议模块
)

func main() {
    zkHosts := []string{"zk-node1:2181", "zk-node2:2181", "zk-node3:2181"} // zookeeper集群地址
    dbCli, err := hbase.NewClient(zkHosts, "/hbase")
  // 连接HBase
    if err != nil {
        log.Fatal("Failed to connect HBaser sever", err)
    }
    tblName, fmName, colName := "demo_tab", "info", "val_1" // 表名,列族,列名
    filterVal := "aa"  // 过滤值
    myComparator := &proto.BinaryComparator{Comparable: &proto.ByteArrayComparable{Value: []byte(filterVal)}}    // 实际的比较器,用于按照二进制字节比较 filterVal
    srzComp, err1 := pb.Marshal(myComparator)
    if err1 != nil {
        log.Fatal("Failed to serialize myComparator ", err1)
    }
    valComparator := proto.Comparator{Name: pb.String("org.apache.hadoop.hbase.filter.BinaryComparator"), SerializedComparator: srzComp}  // 序列化实际比较器,然后装入容器,注意名字
    myFilter := &proto.SingleColumnValueFilter{
                        ColumnFamily: []byte(fmName),  // 列族 
                        ColumnQualifier: []byte(colName),  // 列名
                        CompareOp:  (*proto.CompareType)(pb.Int32(proto.CompareType_value["EQUAL"])),
                        Comparator: &valComparator,  // 比较器
                        FilterIfMissing: pb.Bool(true),
                        LatestVersionOnly: pb.Bool(true),
                    } // 设置具体过滤器,这里比较操作使用 等于"EQUAL",其他操作参看go-hbase中代码
     srzFilter, err2 := pb.Marshal(myFilter)
     if err2 != nil {
        log.Fatal("Failed to serialize myFilter", err2)
     }
     valFilter := &proto.Filter{Name: pb.String("org.apache.hadoop.hbase.filter.SingleColumnValueFilter"), SerializedFilter: srzFilter}  // 序列化具体过滤器,然后转入容器Filter,注意过滤器名字
     // 开始扫描并过滤数据
     s := hbase.NewScan([]byte(tblName), 1000, dbCli)  // 设置Scan
     s.AddStringColumn(fmName, colName)  // 添加列族和列名,这里只扫描1列,与图1扫全列略有不同
     s.Filter = valFilter  // 设定过去器,图6修改的接口
     defer s.Close()
     defer dbCli.Close()
     for {  // 输出扫描结果
         res := s.Next() 
         if res == nil {
             break
         }
         for col, kv := range res.Columns {
             fmt.Println("row:", string(res.Row), "col: ", col, "val: ", string(kv.Value))
         }
     }
}

图8. 简单例子:单过滤器的应用

这个简单例子编译后运行,结果输入如图9所示,可见'info:val_1'这列值为aa的3行数据都被扫描输出。

 图9. 单过滤器程序示例的输出结果

3.2 一个复杂的例子

这节我们给出一个略微复杂的例子。按照'info:val_1' = 'aa' 且 'info:val_2' <> 'bb'的列输出,即val_1列等于'aa'并且'val_2列不等于'bb'的结果。这个预期输出应该是row_7,对应列值是'aa'和'kk'。

实现这个复杂例子,需要用到的是过滤器列表FilterList,其实就是在Filter容器之上,再加一层容器,将一个个Filter再序列化后装入到FilterList中,然后把FilterList再封一层Filter容器发送给服务器。具体实现见图10,理解代码可以自行阅读注释内容,这里不再进一步文字解释。为节省篇幅,复杂例子省去了错误处理,所有错误变量都略去不再处理。

package main
import (
    "fmt"
    "log"
    hbase "github.com/pingcap/go-hbase"
    pb "github.com/golang/protobuf/proto"
    proto "github.com/pingcap/go-hbase/proto"
)
func main() {
    zkHosts := []string{"zk-node1:2181", "zk-node2:2181", "zk-node3:2181"} // zookeeper集群地址
    dbCli, _ := hbase.NewClient(zkHosts, "/hbase")
  // 连接HBase
    tblName, fmName, colName1, colName2 := "demo_tab", "info", "val_1", "val_2"  // 两个列val_1, val_2
    filterVal1, filterVal2 := "aa", "bb"
    // 定义两个比较值,分别比较val_1, val_2的值,如果两列用同一个值比较,只需要定义一个
    myComparator1 := &proto.BinaryComparator{Comparable: &proto.ByteArrayComparable{Value: []byte(filterVal1)}}
    myComparator2 := &proto.BinaryComparator{Comparable: &proto.ByteArrayComparable{Value: []byte(filterVal2)}}
    // 分别序列化
    srzComp1, _  := pb.Marshal(myComparator1)
    srzComp2, _  := pb.Marshal(myComparator2)
    valComparator1 := proto.Comparator{Name: pb.String("org.apache.hadoop.hbase.filter.BinaryComparator"), SerializedComparator: srzComp1}  // 比较器1
    valComparator2 := proto.Comparator{Name: pb.String("org.apache.hadoop.hbase.filter.BinaryComparator"), SerializedComparator: srzComp2}  // 比较器2
    myFilter1 := &proto.SingleColumnValueFilter{
                        ColumnFamily: []byte(fmName),
                        ColumnQualifier: []byte(colName1),
                        CompareOp:  (*proto.CompareType)(pb.Int32(proto.CompareType_value["EQUAL"])),
                        Comparator: &valComparator1,
                        FilterIfMissing: pb.Bool(true),
                        LatestVersionOnly: pb.Bool(true),
                    }  // 过滤器1,使用“等于”条件
    myFilter2 := &proto.SingleColumnValueFilter{
                        ColumnFamily: []byte(fmName),
                        ColumnQualifier: []byte(colName2),
                        CompareOp:  (*proto.CompareType)(pb.Int32(proto.CompareType_value["NOT_EQUAL"])),
                        Comparator: &valComparator2,
                        FilterIfMissing: pb.Bool(true),
                        LatestVersionOnly: pb.Bool(true),
                    } // 过滤器2,使用“不等于”条件
     srzFilter1, _ := pb.Marshal(myFilter1)
     srzFilter2, _ := pb.Marshal(myFilter2)
     valFilter1 := &proto.Filter{Name: pb.String("org.apache.hadoop.hbase.filter.SingleColumnValueFilter"), SerializedFilter: srzFilter1}  // 装入Filter
     valFilter2 := &proto.Filter{Name: pb.String("org.apache.hadoop.hbase.filter.SingleColumnValueFilter"), SerializedFilter: srzFilter2}  // 装入Filter
     filterList := &proto.FilterList{
            Operator: (*proto.FilterList_Operator)(pb.Int32(proto.FilterList_Operator_value["MUST_PASS_ALL"])),
              // 这里MUST_PASS_ALL 相当于AND,列表中过滤条件必须全部满足
            Filters: []*proto.Filter{valFilter1, valFilter2},
        }  // 定义FitlerList 
     srzFilterList, _ := pb.Marshal(filterList) // 序列化
     topFilter := &proto.Filter{Name: pb.String("org.apache.hadoop.hbase.filter.FilterList"), SerializedFilter: srzFilterList} // 将FilterList装入Filter,使用Filter统一接口传递给服务器,注意这里名称
     s := hbase.NewScan([]byte(tblName), 1000, dbCli)
     s.AddStringColumn(fmName, colName1)  // 将val_1加入扫描
     s.AddStringColumn(fmName, colName2)
  // 将val_2加入扫描
     s.Filter = topFilter
     defer s.Close()
     defer dbCli.Close()
     for {  // 输出结果
         res := s.Next() 
         if res == nil {
             break
         }
         for col, kv := range res.Columns {
             fmt.Println("row:", string(res.Row), "col: ", col, "val: ", string(kv.Value))
         }
     }
}

图10.  复杂例子:多列多条件过滤

上面例子编译后运行,结果输入如图11所示,可见输出只有row_6这行,输出两列值分别是aa和kk,满足过滤条件。

 图11. 多列多值过滤器输出结果

四、结语

笔者结合项目经验,总结了如何通过修改go-hbase包,实现列过滤器的方法,并给了两个实际应用样例。经过实际测试,可以验证方案是有效的。随着使用深入,实践经验的逐步积累,笔者会将更多使用Go语言进行大数据开发的经验分享出来。

...全文
583 1 打赏 收藏 转发到动态 举报
AI 作业
写回复
用AI写文章
1 条回复
切换为时间正序
请发表友善的回复…
发表回复
叶落留潇 2022-06-30
  • 打赏
  • 举报
回复

受益匪浅

46

社区成员

发帖
与我相关
我的任务
社区描述
这里是CSDN讨论Web产品技术和产业发展的大本营, 欢迎所有Web3业内和关心Web3的朋友们.
社区管理员
  • Web3天空之城
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧