Hbase系列-3、Hbase高级

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

大数据系列文章目录

Hbase的官网

HBase的高可用

考虑关于HBase集群的一个问题,在当前的HBase集群中,只有一个Master,一旦Master出现故障,将会导致HBase不再可用。所以,在实际的生产环境中,是非常有必要搭建一个高可用的HBase集群的。

Hbase高可用的简介

HBase的高可用配置其实就是HMaster的高可用。要搭建HBase的高可用,只需要再选择一个节点作为HMaster,在HBase的conf目录下创建文件backup-masters,然后再backup-masters添加备份Master的记录。 一条记录代表一个backup master,可以在文件配置多个记录

搭建Hbase高可用

1)在hbase的conf文件夹中创建 backup-masters 文件

cd /export/server/hbase-2.1.0/conf 
touch backup-masters

2)将node2和node3添加到该文件中

vim backup-masters

3)将backup-masters文件分发到所有的服务器节点中

scp backup-masters node2:$PWD 
scp backup-masters node3:$PWD

4)重新启动Hbase

stop-hbase.sh
start-hbase.sh

5)查看webui,检查Backup Masters中是否有node2、node3

http://node1:16010/master-status

HBase的架构说明

在这里插入图片描述

Client

客户端,例如:发出HBase操作的请求。例如:之前我们编写的Java API代码、以及HBase shell,都是CLient

Master Server

在HBase的Web UI中,可以查看到Master的位置
在这里插入图片描述

  • 监控RegionServer , 处理RegionServer故障转移
  • 处理元数据的变更 , 处理region的分配或移除
  • 在空闲时间进行数据的负载均衡
  • 通过Zookeeper发布自己的位置给客户端

Region Server

在这里插入图片描述

  • 处理分配给它的Region , 负责存储HBase的实际数据
  • 刷新缓存到HDFS , 维护HLog
  • 执行压缩 , 负责处理Region分片
  • RegionServer中包含了大量丰富的组件,如下:
    Write-Ahead logs , HFile(StoreFile) , Store , MemStore , Region
    在这里插入图片描述

Region

在HBASE中,表被划分为很多「Region」,并由Region Server提供服务
在这里插入图片描述

Stroe

Region按列族垂直划分为「Store」,存储在HDFS在文件中

MemStore

  • MemStore与缓存内存类似
  • 当往HBase中写入数据时,首先是写入到MemStore
  • 每个列族将有一个MemStore
  • 当MemStore存储快满的时候,整个数据将写入到HDFS中的HFile中

StoreFile

  • 每当任何数据被写入HBASE时,首先要写入MemStore
  • 当MemStore快满时,整个排序的key-value数据将被写入HDFS中的一个新的HFile中
  • 写入HFile的操作是连续的,速度非常快
  • 物理上存储的是HFile

WAL

  • WAL全称为Write Ahead Log,它最大的作用就是故障恢复
  • WAL是HBase中提供的一种高并发、持久化的日志保存与回放机制
  • 每个业务数据的写入操作(PUT/DELETE/INCR),都会保存在WAL中
  • 一旦服务器崩溃,通过回放WAL,就可以实现恢复崩溃之前的数据
  • 物理上存储是Hadoop的Sequence File

HBase的重要工作原理

HBase读取数据的流程

1)从zookeeper找到meta表的region的位置,然后读取meta表中的数据。而meta中又存储了用户表的region信息ZK:/hbase/meta-region-server,该节点保存了meta表的region server数据

2)根据namespace、表名和rowkey根据meta表中的数据找到对应的region信息, 查找对应的region

在这里插入图片描述
3)从MemStore找数据,再去BlockCache中找,如果没有,再到StoreFile上读

4)可以把MemStore理解为一级缓存,BlockCache为二级缓存,但注意scan的时候BlockCache意义不大,因为scan是顺序扫描

在这里插入图片描述

HBase存储数据的流程

客户端流程:

1)客户端发起写入数据的请求, 首先会先连接zookeeper, 从zookeeper获取 hbase:meta表所在的regionServer的地址

2)连接meta表对应的regionServer, 从meta表获取目标表对应要写入数据的region的地址(基于region的startkey 和endKey来确定)

3)连接对应region的regionServer的地址, 开始进行数据的写入

4)首先先将数据写入到这个regionServer的Hlog日志中, 然后在将数据写入到 对应的region中store模块的memStore中, 当这个两个地方都写入完成后, 客户端就会认为数据写入完成了

服务器端流程(异步):

5) 客户端不断的进行数据的写入工作, memStore数据也会不断的增多, 当memStore中数据达到一定的阈值(128M|1小时)后, 内部最终启动一个flush线程, 将数据刷新到HDFS上, 形成一个storeFile文件

6)随着memStore不断刷新数据到HDFS中, storeFile文件也会越来越多, 当storeFile的文件达到一定的阈值后(3个及以上), 启动compact线程, 将多个文件合并最终合并为一个大文件(Hfile)

7)随着不断的合并, 这个大的Hfile文件也会越来越大, 当这个大的Hfile达到一定的阈值(最终10GB)后, 启动split机制, 将大的Hfile一分为二的操作, 此时region也会进行分割操作, 变成两个新的region, 每个region管理每个分割后新的Hfile文件, 原有就得region就会被下线

8)随着不断的进行split, 表的region的数量也会越来越多的

HBase的memStore溢写合并

在这里插入图片描述
说明:

  • 当MemStore写入的值变多,触发溢写操作(flush),进行文件的溢写,成为一个StoreFile
  • 当溢写的文件过多时,会触发文件的合并(Compact)操作,合并有两种方式(major,minor)

触发条件:

  • 一旦MemStore达到128M时,则触发Flush溢出(Region级别)
    在这里插入图片描述
  • MemStore的存活时间超过1小时(默认),触发Flush溢写(RegionServer级别)
    在这里插入图片描述
    In-memory合并:
  • In-memory合并是HBase 2.0之后添加的。它与默认的MemStore的区别:实现了在内存中进行compaction(合并)
  • 在CompactingMemStore中,数据是以段(Segment)为单位存储数据的。MemStore包含了多个segment。
  • 当数据写入时,首先写入到的是Active segment中(也就是当前可以写入的segment段)
  • 在2.0之前,如果MemStore中的数据量达到指定的阈值时,就会将数据flush到磁盘中的一个StoreFile
  • 2.0的In-memory compaction,active segment满了后,将数据移动到pipeline中。这个过程跟以前不一样,以前是flush到磁盘,而这次是将Active segment的数据,移到称为pipeline的内存当中。一个pipeline中可以有多个segment。而In-memory compaction会将pipeline的多个segment合并为更大的、更紧凑的segment,这就是compaction
  • HBase会尽量延长CompactingMemStore的生命周期,以达到减少总的IO开销。当需要把CompactingMemStore flush到磁盘时,pipeline中所有的segment会被移动到一个snapshot中,然后进行合并后写入到HFile

compaction策略:
但Active segment flush到pipeline中后,后台会触发一个任务来合并pipeline中的数据。合并任务会扫描pipeline中所有的segment,将segment的索引合并为一个索引。有三种合并策略

  • basic(基础型)
    Basic compaction策略不清理多余的数据版本,无需对cell的内存进行考核。
    basic适用于所有大量写模式。

  • eager(饥渴型)
    eager compaction会过滤重复的数据,清理多余的版本,这会带来额外的开销。
    eager模式主要针对数据大量过期淘汰的场景,例如:购物车、消息队列等。

  • adaptive(适应型)
    adaptive compaction根据数据的重复情况来决定是否使用eager策略。
    该策略会找出cell个数最多的一个,然后计算一个比例,如果比例超出阈值,则使用eager策略,否则使用basic 策略。

compaction策略配置:

  1. 可以通过hbase-site.xml来配置默认In Memory Compaction方式
    在这里插入图片描述
  2. 在创建表的时候指定方式
create "test_memory_compaction", {NAME => 'C1', IN_MEMORY_COMPACTION => "BASIC"}

在这里插入图片描述

  • 当MemStore超过阀值的时候,就要flush到HDFS上生成一个StoreFile。因此随着不断写入,HFile的数量将会越来越多,根据前面所述,StoreFile数量过多会降低读性能
  • 为了避免对读性能的影响,需要对这些StoreFile进行compact操作,把多个HFile合并成一个HFile
  • compact操作需要对HBase的数据进行多次的重新读写,因此这个过程会产生大量的IO。可以看到compact操作的 本质就是以IO操作换取后续的读性能的提高

minor compaction: minor 合并

  • Minor Compaction操作只用来做部分文件的合并操作,包括minVersion=0并且设置ttl的过期版本清理,不做任何删除数据、多版本数据的清理工作

  • 小范围合并,默认是3-10个文件进行合并,不会删除其他版本的数据

  • Minor Compaction则只会选择数个StoreFile文件compact为一个StoreFile

  • Minor Compaction的过程一般较快,而且IO相对较低

minor compaction: minor 合并触发条件

  • 在打开Region或者MemStore时会自动检测是否需要进行Compact(包括Minor、Major)
  • minFilesToCompact由hbase.hstore.compaction.min控制,默认值为3
  • 即Store下面的StoreFile数量减去正在compaction的数量 >=3时,需要做compaction

http://node1:16010/conf
在这里插入图片描述
major compaction: major 合并

  • Major Compaction操作是对Region下的Store下的所有StoreFile执行合并操作,最终的结果是整理合并出一个文件
  • 一般手动触发,会删除其他版本的数据(不同时间戳的)

major compaction: major 合并触发条件

  • 如果无需进行Minor compaction,HBase会继续判断是否需要执行Major Compaction

  • 如果所有的StoreFile中,最老(时间戳最小)的那个StoreFile的时间间隔大于Major Compaction的时间间隔( hbase.hregion.majorcompaction——默认7天)
    在这里插入图片描述

  • 当region中的数据逐渐变大之后,达到某一个阈值,会进行裂变

    一个region等分为两个region,并分配到不同的RegionServer。

    原本的Region会下线,新Split出来的两个Region会被HMaster分配到相应的HRegionServer上,使得原先1个Region的压力得以分流到2个Region上。

  • HBase只是增加数据,所有的更新和删除操作,都是在Compact阶段做的

  • 用户写操作只需要进入到内存即可立即返回,从而保证I/O高性能读写

自动分区:

之前,我们在建表的时候,没有涉及过任何关于Region的设置,由HBase来自动进行分区。也就是Region达到一定 大小就会自动进行分区。最小的分裂大小和table的某个region server的region 个数有关,当store file的大小大于如下公式得出的值的时候就会split,公式如下:

Min (R^2 * “hbase.hregion.memstore.flush.size”, “hbase.hregion.max.filesize”) R为同一个table中在同一个region server中region的个数。

  • 如果初始时R=1,那么Min(128MB,10GB)=128MB,也就是说在第一个flush的时候就会触发分裂操作
  • 当R=2的时候Min(22128MB,10GB)=512MB ,当某个store file大小达到512MB的时候,就会触发分裂
  • 如此类推,当R=9的时候,store file 达到10GB的时候就会分裂,也就是说当R>=9的时候,store file 达到10GB的时候就会分裂
  • split 点都位于region中row key的中间点

手动分区:

在创建表的时候,就可以指定表分为多少个Region。默认一开始的时候系统会只向一个RegionServer写数据,系统不指定startRow和endRow,可以在运行的时候提前Split,提高并发写入。

HBase的Region管理

region分配:

  • 任何时刻,一个region只能分配给一个region server
  • Master记录了当前有哪些可用的region server,以及当前哪些region分配给了哪些region server,哪些region还没有分配。当需要分配的新的region,并且有一个region server上有可用空间时,master就给这个region server发送一个装载请求,把region分配给这个region server。region server得到请求后,就开始对此region提供服务。

region server上线:

  • Master使用ZooKeeper来跟踪region server状态
  • 当某个region server启动时
    首先在zookeeper上的server目录下建立代表自己的znode。
    由于Master订阅了server目录上的变更消息,当server目录下的文件出现新增或删除操作时,master可以得到来 自zookeeper的实时通知。
    一旦region server上线,master能马上得到消息。

region server下线:

  • 当region server下线时,它和zookeeper的会话断开,ZooKeeper而自动释放代表这台server的文件上的独占锁

  • Master就可以确定
    region server和zookeeper之间的网络断开了
    region server挂了

  • 无论哪种情况,region server都无法继续为它的region提供服务了,此时master会删除server目录下代表这台region server的znode数据,并将这台region server的region分配给其它还活着的节点

HBase的master工作机制

Master上线:

Master启动进行以下步骤:

  1. 从zookeeper上获取唯一一个代表active master的锁,用来阻止其它master成为master
  2. 一般hbase集群中总是有一个master在提供服务,还有一个以上的‘master’在等待时机抢占它的位置。
  3. 扫描zookeeper上的server父节点,获得当前可用的region server列表
  4. 和每个region server通信,获得当前已分配的region和region server的对应关系
  5. 扫描.META.region的集合,计算得到当前还未分配的region,将他们放入待分配region列表

Master下线:

  • 由于master只维护表和region的元数据,而不参与表数据IO的过程,master下线仅导致所有元数据的修改被冻结
     无法创建删除表
     无法修改表的schema
     无法进行region的负载均衡
     无法处理region 上下线
     无法进行region的合并
     唯一例外的是region的split可以正常进行,因为只有region server参与
     表的数据读写还可以正常进行

  • 因此master下线短时间内对整个hbase集群没有影响。

  • 从上线过程可以看到,master保存的信息全是可以冗余信息(都可以从系统其它地方收集到或者计算出来)

HBase批量加载操作

Bulk Load 基本介绍

很多时候,我们需要将外部的数据导入到HBase集群中,例如:将一些历史的数据导入到HBase做备份。我们之前已经学习了HBase的Java API,通过put方式可以将数据写入到HBase中,我们也学习过通过MapReduce编写代码将HDFS中的数据导入到HBase。但这些方式都是基于HBase的原生API方式进行操作的。这些方式有一个共同点,就是需要与HBase连接,然后进行操作。HBase服务器要维护、管理这些连接,以及接受来自客户端的操作,会给HBase的存储、计算、网络资源造成较大消耗。此时,在需要将海量数据写入到HBase时,通过Bulk load(大容量加载)的方式,会变得更高效。可以这么说,进行大量数据操作,Bulk load是必不可少的。

我们知道,HBase的数据最终是需要持久化到HDFS。HDFS是一个文件系统,那么数据可定是以一定的格式存储到里面的。例如:Hive我们可以以ORC、Parquet等方式存储。而HBase也有自己的数据格式,那就是HFile。Bulk Load就是直接将数据写入到StoreFile(HFile)中,从而绕开与HBase的交互,HFile生成后,直接一次性建立与HBase 的关联即可。使用BulkLoad,绕过了Write to WAL,Write to MemStore及Flush to disk的过程

更多可以参考官方对Bulk load的描述:https://hbase.apache.org/book.html#arch.bulk.load
在这里插入图片描述
结论: 使用bulkload的方式将我们的数据直接生成HFile格式,然后直接加载到HBase的表当中去

Bulk Load Map Reduce 程序开发

需求说明:

银行每天都产生大量的转账记录,超过一定时期的数据,需要定期进行备份存储。本案例,在MySQL中有大量转账记录数据,需要将这些数据保存到HBase中。因为数据量非常庞大,所以采用的是Bulk Load方式来加载数据。

  • 项目组为了方便数据备份,每天都会将对应的转账记录导出为CSV文本文件,并上传到HDFS。我们需要做的就 将HDFS上的文件导入到HBase中。
  • 因为我们只需要将数据读取出来,然后生成对应的Store File文件。所以,我们编写的MapReduce程序,只有Mapper,而没有Reducer。

数据说明:
在这里插入图片描述
项目的准备工作:

1)在Hbase中创建银行转账记录表

create "TRANSFER_RECORD", { NAME => "C1"}

2)创建项目

groupidartifactid
com.lwhbankrecord_bulkload

3)添加依赖

 <repositories><!-- 代码库 -->
        <repository>
            <id>aliyun</id>
            <url>http://maven.aliyun.com/nexus/content/groups/public/</url>
            <releases>
                <enabled>true</enabled>
            </releases>
            <snapshots>
                <enabled>false</enabled>
                <updatePolicy>never</updatePolicy>
            </snapshots>
        </repository>
    </repositories>
    <dependencies>

        <dependency>
            <groupId>org.apache.hbase</groupId>
            <artifactId>hbase-client</artifactId>
            <version>2.1.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.hbase</groupId>
            <artifactId>hbase-mapreduce</artifactId>
            <version>2.1.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.hadoop</groupId>
            <artifactId>hadoop-mapreduce-client-jobclient</artifactId>
            <version>2.7.5</version>
        </dependency>
        <dependency>
            <groupId>org.apache.hadoop</groupId>
            <artifactId>hadoop-common</artifactId>
            <version>2.7.5</version>
        </dependency>
        <dependency>
            <groupId>org.apache.hadoop</groupId>
            <artifactId>hadoop-mapreduce-client-core</artifactId>
            <version>2.7.5</version>
        </dependency>
        <dependency>
            <groupId>org.apache.hadoop</groupId>
            <artifactId>hadoop-auth</artifactId>
            <version>2.7.5</version>
        </dependency>
        <dependency>
            <groupId>org.apache.hadoop</groupId>
            <artifactId>hadoop-hdfs</artifactId>
            <version>2.7.5</version>
        </dependency>
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.6</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.1</version>
                <configuration>
                    <target>1.8</target>
                    <source>1.8</source>
                </configuration>
            </plugin>
        </plugins>
    </build>

4)创建表结构

说明
com.lwh.bank_record.bulkload.mrMapReduce相关代码
com.lwh.bank_record.entity实体类

5)导入 log4j.properties

6)将数据上传到HDFS中:

将数据集 bank_record.csv 上传到HDFS的/hbase/bulkload/output 目录。该文件中包含50W条的转账记录数据。

hadoop fs -mkdir -p /hbase/bulkload/output
hadoop fs -put bank_record.csv /hbase/bulkload/output

项目的开发:MP的map程序

public class BulkLoadMapper extends Mapper<LongWritable, Text, ImmutableBytesWritable, Put> {
        private ImmutableBytesWritable k2 = new ImmutableBytesWritable();

        @Override
        protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
            //1. 获取一行数据
            String line = value.toString();
            //2. 判断是否接收到数据
            if (line != null && !"".equals(line.trim())) {
                //3. 对这一行数据执行切割操作
                String[] fields = line.split(",");

                //4. 封装 k2 和 v2的数据
                byte[] rowkey = fields[0].getBytes();
                k2.set(rowkey);
                Put v2 = new Put(rowkey);

                v2.addColumn("C1".getBytes(), "code".getBytes(), fields[1].getBytes());
                v2.addColumn("C1".getBytes(), "rec_account".getBytes(), fields[2].getBytes());
                v2.addColumn("C1".getBytes(), "rec_bank_name".getBytes(), fields[3].getBytes());
                v2.addColumn("C1".getBytes(), "rec_name".getBytes(), fields[4].getBytes());
                v2.addColumn("C1".getBytes(), "pay_account".getBytes(), fields[5].getBytes());
                v2.addColumn("C1".getBytes(), "pay_name".getBytes(), fields[6].getBytes());
                v2.addColumn("C1".getBytes(), "pay_comments".getBytes(), fields[7].getBytes());
                v2.addColumn("C1".getBytes(), "pay_channel".getBytes(), fields[8].getBytes());
                v2.addColumn("C1".getBytes(), "pay_way".getBytes(), fields[9].getBytes());
                v2.addColumn("C1".getBytes(), "status".getBytes(), fields[10].getBytes());
                v2.addColumn("C1".getBytes(), "timestamp".getBytes(), fields[11].getBytes());
                v2.addColumn("C1".getBytes(), "money".getBytes(), fields[12].getBytes());
                //4. 写出去
                context.write(k2, v2);
            }
        }
    }

项目的开发: MR的驱动类

public class BulkLoadDriver extends Configured implements Tool {
        @Override
        public int run(String[] args) throws Exception {


            //1. 创建 Job对象
            Job job = Job.getInstance(super.getConf(), "BulkLoadDriver");
            // 设置提交yarn 必备参数
            job.setJarByClass(BulkLoadDriver.class);
            //2. 设置 天龙八部
            //2.1: 设置 输入类 和输入路径
            job.setInputFormatClass(TextInputFormat.class);
            TextInputFormat.addInputPath(job, new Path("hdfs://node1:8020/hbase/bulkload/input"));
            //2.2: 设置 mapper类和 map输出k2和v2的类型job.setMapperClass(BulkLoadMapper.class); job.setMapOutputKeyClass(ImmutableBytesWritable.class); job.setMapOutputValueClass(Put.class);

            //2.3: 设置 shuffle操作: 分区 排序 规约 分组

            //2.7: 设置 reduce类 和 reduce 输出 k3和v3的类型job.setNumReduceTasks(0); // 没有reduce

            job.setOutputKeyClass(ImmutableBytesWritable.class);
            job.setOutputValueClass(Put.class);

            //2.8: 设置输出类, 及输出路径 Hfile


            job.setOutputFormatClass(HFileOutputFormat2.class);
            // 获取 连接对象 和 table对象
            Connection hbConn = ConnectionFactory.createConnection(super.getConf());
            Table table = hbConn.getTable(TableName.valueOf("TRANSFER_RECORD"));
            HFileOutputFormat2.configureIncrementalLoad(job, table,
                    hbConn.getRegionLocator(TableName.valueOf("TRANSFER_RECORD")));
            return flag ? 0 : 1;
        }

        public static void main(String[] args) throws Exception {
            Configuration conf = HBaseConfiguration.create();
            conf.set("hbase.zookeeper.quorum", "node1:2181,node2:2181,node3:2181");
            int i = ToolRunner.run(conf, new BulkLoadDriver(), args);
            System.exit(i);
        }

执行测试:查看结果
在这里插入图片描述
项目的开发: 加载数据到HBase

语法说明:

hbase org.apache.hadoop.hbase.tool.LoadIncrementalHFiles 数据路径 HBase表名

例子:

hbase org.apache.hadoop.hbase.tool.LoadIncrementalHFiles /bank/output TRANSFER_RECORD

HBase与Hive的集成

HBase与Hive的对比

基本说明:

Hive:

  1. 是一个数据仓库的工具: 其本质将HDFS中已经存储的文件在Mysql中做了一个双射关系,以方便使用HQL去管理查询
  2. 用于数据分析、清洗: Hive适用于离线的数据分析和清洗,延迟较高
  3. 基于HDFS、MapReduce: Hive存储的数据依旧在DataNode上,编写的HQL语句终将是转换为MapReduce代码执行。

Hbase:

  1. 是一个数据库: 是一种面向列存储的非关系型数据库
  2. 用于存储结构化和非结构话的数据 : 适用于单表非关系型数据的存储,不适合做关联查询,类似JOIN等操作
  3. 基于HDFS: 数据持久化存储的体现形式是Hfile,存放于DataNode中,被ResionServer以region的形式进行管理。
  4. 延迟较低,接入在线业务 : 面对大量的企业数据,HBase可以实现单表大量数据的存储,同时提供了高效的访问速度

总结

Hive和Hbase是两种基于Hadoop的不同技术,Hive是一种类SQL的引擎,并且运行MapReduce任务,Hbase是一种在Hadoop之上的NoSQL 的Key/vale数据库。这两种工具是可以同时使用的。就像用Google来搜索,用FaceBook进行社交一样, Hive可以用来进行统计查询,HBase可以用来进行实时查询,数据也可以从Hive写到HBase,或者从HBase写回Hive。

HBase与hive的整合

hive与我们的HBase各有千秋,各自有着不同的功能,但是归根接地,hive与hbase的数据最终都是存储在hdfs上面的,一般的我们为了存储磁盘的空间,不会将一份数据存储到多个地方,导致磁盘空间的浪费,我们可以 直接将数据存入hbase,然后通过hive整合hbase直接使用sql语句分析hbase里面的数据即可,非常方便。

整合环境准备工作:

1、拷贝hive中用于集成hbase的通信包, 放置于hbase的lib目录下

将hive的lib下中用于和hbase通信的包,拷贝到hbase的lib目录下: hive-hbase-handler-2.1.1.jar

cp /export/server/hive-2.1.0/lib/hive-hbase-handler-2.1.1.jar /export/server/hbase-2.1.0/lib/

2、修改hive的配置文件
编辑node03服务器上面的hive的配置文件hive-site.xml添加以下两行配置

cd /export/server/hive-2.1.0/conf 
vim hive-site.xml
<property>
	<name>hive.zookeeper.quorum</name>
	<value>node1,node2,node3</value>
</property>
<property>
	<name>hbase.zookeeper.quorum</name>
	<value>node1,node2,node3</value>
</property>
<property>
	<name>hive.server2.enable.doAs</name>
	<value>false</value>
</property>

3、修改hive-env.sh 配置文件添加以下配置
编辑node03服务器上面的hive的配置文件hive-site.xml添加以下两行配置

cd /export/servers/hive-2.1.0/conf 
vim hive-env.sh
# 内 容 如 下 : 
HADOOP_HOME=/export/server/hadoop-2.7.5 
export HBASE_HOME=/export/server/hbase-2.1.0
export HIVE_CONF_DIR=/export/server/hive-2.1.0/conf

需求: 将HBase的表数据在hive中进行映射

1、hbase当中创建表并手动插入加载一些数据
进入HBase的shell客户端,手动创建一张表,并插入加载一些数据进去

create 'hbase_hive_score',{ NAME =>'cf'}
put 'hbase_hive_score','1','cf:name','zhangsan' 
put 'hbase_hive_score','1','cf:score', '95'
put 'hbase_hive_score','2','cf:name','lisi' 
put 'hbase_hive_score','2','cf:score', '96'
put 'hbase_hive_score','3','cf:name','wangwu' 
put 'hbase_hive_score','3','cf:score', '97'

2、建立hive的外部表, 映射hbase当中的表已经字段

在hive当中建立外部表,进入hive客户端,然后执行以下命令进行创建hive外部表,就可以实现映射HBase当中的表数据

CREATE external TABLE course.hbase2hive( id int,
name string,
score int
) STORED BY 'org.apache.hadoop.hive.hbase.HBaseStorageHandler' 
WITH SERDEPROPERTIES ("hbase.columns.mapping" = ":key,cf:name,cf:score") TBLPROPERTIES("hbase.table.name"="hbase_hive_score");

HBase的协处理器

起源:
Hbase 作为列族数据库最经常被人诟病的特性包括:

  • 无法轻易建立“二级索引”
  • 难以执行求和、计数、排序等操作

比如,在旧版本的(<0.92)Hbase 中,统计数据表的总行数,需要使用 Counter 方法,执行一次 MapReduce Job 才能得到。虽然 HBase 在数据存储层中集成了 MapReduce,能够有效用于数据表的分布式计算。然而在很多情况下, 做一些简单的相加或者聚合计算的时候, 如果直接将计算过程放置在 server 端,能够减少通讯开销,从而获得很好的性能提升。

于是, HBase 在 0.92 之后引入了协处理器(coprocessors),实现一些激动人心的新特性:能够轻易建立二次索引、复杂过滤器(谓词下推)以及访问控制等。

协处理器主要的分类:

- obServer

- endpoint

Observer 类似于传统数据库中的触发器,当发生某些事件的时候这类协处理器会被 Server 端调用。Observer Coprocessor 就是一些散布在 HBase Server 端代码中的 hook 钩子, 在固定的事件发生时被调用。比如: put 操作之前有钩子函数 prePut,该函数在 put 操作执行前会被 Region Server 调用;在 put 操作之后则有 postPut 钩子函数

以 Hbase2.0.0 版本为例,它提供了三种观察者接口:

  • RegionObserver:提供客户端的数据操纵事件钩子: Get、 Put、 Delete、 Scan 等
  • WALObserver:提供 WAL 相关操作钩子。
  • MasterObserver:提供 DDL-类型的操作钩子。如创建、删除、修改数据表等。
  • 到 0.96 版本又新增一个 RegionServerObserver

下面是以 RegionObserver 为例子讲解 Observer 这种协处理器的原理:

  • 比如客户端发起get请求
  • 该请求被分派给合适的RegionServer和Region
  • coprocessorHost拦截该请求,然后在该表上登记的每个RegionObserer上调用 preGet()
  • 如果没有被preGet拦截,该请求继续送到Region,然后进行处理
  • Region产生的结果再次被coprocessorHost拦截,调用posGet()处理
  • 加入没有postGet()拦截该响应,最终结果被返回给客户端
  • Endpoint 协处理器类似传统数据库中的存储过程,客户端可以调用这些 Endpoint 协处理器执行一段 Server 端代码,并将 Server 端代码的结果返回给客户端进一步处理,最常见的用法就是进行聚集操作。
  • 如果没有协处理器,当用户需要找出一张表中的最大数据,即max 聚合操作,就必须进行全表扫描,在客户端代码内遍历扫描结果,并执行求最大值的操作。这样的方法无法利用底层集群的并发能力,而将所有计算都集中到 Client 端统一执 行,势必效率低下。
  • 利用 Coprocessor,用户可以将求最大值的代码部署到 HBase Server 端,HBase 将利用底层 cluster 的多个节点并发执行求最大值的操作。即在每个 Region 范围内 执行求最大值的代码,将每个 Region 的最大值在 Region Server 端计算出,仅仅将该 max 值返回给客户端。在客户端进一步将多个 Region 的最大值进一步处理而找到其中的最大值。这样整体的执行效率就会提高很多。

下图是 EndPoint 的工作原理:
在这里插入图片描述
Hbase的协处理器-概念总结

  • Observer 允许集群在正常的客户端操作过程中可以有不同的行为表现
  • Endpoint 允许扩展集群的能力,对客户端应用开放新的运算命令
  • observer 类似于 RDBMS 中的触发器,主要在服务端工作
  • endpoint 类似于 RDBMS 中的存储过程,主要在 服务器端、client 端工作
  • observer 可以实现权限管理、优先级设置、监控、 ddl 控制、 二级索引等功能
  • endpoint 可以实现 min、 max、 avg、 sum、 distinct、 group by 等功能

Hbase协处理器-加载的方式

静态加载

  • 通过修改 hbase-site.xml 这个文件来实现
  • 启动全局 aggregation,能过操纵所有的表上的数据。只需要添加如下代码:
    在这里插入图片描述
    注意: 为所有 table 加载了一个 cp class,可以用” ,”分割加载多个 class

动态加载

  • 启用表 aggregation,只对特定的表生效
  • 通过 HBase Shell 来实现,disable 禁用表
  • 添加 aggregation , 添加后启用表即可
 hbase> alter 'mytable', METHOD => 'table_att','coprocessor'=> '|org.apache.Hadoop.hbase.coprocessor.AggregateImplementation||'

Hbase协处理器-卸载的方式

  1. 禁用表:
disable 'test'
  1. 修改表: 删除协处理器的配置信息
alter ‘test’, METHOD => 'table_att_unset', NAME => 'coprocessor$1
  1. 启动表
enable 'test'

HBase的表结构设计

Hbase表的名称空间

基本说明

  • 在一个项目中,需要使用HBase保存多张表,这些表会按照业务域来划分
  • 为了方便管理,不同的业务域以名称空间(namespace)来划分,这样管理起来会更加容易
  • 类似于Hive中的数据库,不同的数据库下可以放不同类型的表
  • HBase默认的名称空间是「default」,默认情况下,创建表时表都将创建在 default 名称空间下
  • HBase中还有一个命名空间「hbase」,用于存放系统的内建表(namespace、meta)

语法说明

  • 创建命名空间 : create_namespace ‘DAY02_SPACE’
  • 查看命名空间列表: list_namespace
  • 查看命名空间: describe_namespace ‘DAY02_SPACE’
  • 命名空间创建表: create ‘DAY02_SPACE :MSG’,’C1’
    注意: 带有命名空间的表, 使用冒号将命名空间和表名连接到一起
  • 删除命名空间: drop_namespace ‘DAY02_SPACE’
    注意: 删除命名空间, 命名空间中必须没有表, 否则无法删除

Hbase表的列族设计

HBase列蔟的数量应该越少越好

  • 两个及以上的列蔟HBase性能并不是很好
  • 一个列蔟所存储的数据达到flush的阈值时,表中所有列蔟将同时进行flush操作
  • 这将带来不必要的I/O开销,列蔟越多,对性能影响越大

Hbase表的版本设计

数据版本确界

在Hbase当中,我们可以为数据设置上界和下界,其实就是定义数据的历史版本保留多少个,通过自定义历史版本保存的数量,我们可以实现历史多个版本的数据的查询。

  1. 版本的下界

默认的版本下界是0,即禁用。row版本使用的最小数目是与生存时间(TTL Time To Live)相结合的,并且我们根据实际需求可以有0或更多的版本,使用0,即只有1个版本的值写入cell。

  1. 版本的上界

之前默认的版本上界是3,也就是一个row保留3个副本(基于时间戳的插入)。该值不要设计的过大,一般的业务不会超过100。如果cell中存储的数据版本号超过了3个,再次插入数据时,最新的值会将最老的值覆盖。(现版本已默认为1)

数据的TTL

在实际工作当中经常会遇到有些数据过了一段时间我们可能就不需要了,那么这时候我们可以使用定时任务去定时的删除这些数据,或者我们也可以使用Hbase的TTL(Time To Live)功能,让我们的数据定期的会进行清除

使用代码来设置数据的确界以及设置数据的TTL如下:

 // 测试TTL与版本的确界
    @Test
    public void test07() throws Exception {
        // 1) 创建HBase连接对象:
        Configuration conf = HBaseConfiguration.create();
        conf.set("hbase.zookeeper.quorum", "node1:2181,node2:2181,node3:2181");
        Connection hbConn = ConnectionFactory.createConnection(conf);

        // 2) 从连接对象中获取相关的管理对象: Admin(对表的操作) 和 Table(对表数据操作) Admin admin = hbConn.getAdmin();
        // 3) 执行相关的操作
        boolean flag = admin.tableExists(TableName.valueOf("version_hbase")); // 为true 表示存在, 否则为不存在
        if (!flag) {// 说明表不存在
            //3.1: 创建表的构建器对象
            TableDescriptorBuilder descBuilder = TableDescriptorBuilder.newBuilder(TableName.valueOf(“version_hbase”));
            //3.2: 在构建器设置表的列族信息
            ColumnFamilyDescriptorBuilder familyDesc = ColumnFamilyDescriptorBuilder.newBuilder(“f1”.getBytes());

            familyDesc.setMaxVersions(5);
            familyDesc.setMinVersions(3);
            familyDesc.setTimeToLive(30);

            descBuilder.setColumnFamily(familyDesc.build());
            //3.3: 基于表构建器 构建表基本信息封装对象
            TableDescriptor desc = descBuilder.build();
            admin.createTable(desc);
        }
        //3.4 获取table对象, 进行添加数据添加一次, 修改5次
        Table version_hbase = hbConn.getTable(TableName.valueOf("version_hbase"));
        for (int i = 1; i <= 6; i++) {
            Put put1 = new Put("1".getBytes());
            //put.setTTL(3000); // 针 对 某 一 条 具 体 的 数 据 设 置 TTL put1.addColumn("f1".getBytes(), "name".getBytes(), ("zhangsan"+i).getBytes());
            version_hbase.put(put1);
            Thread.sleep(1000);
        }
        // 4) 处理结果集
        Get get = new Get("1".getBytes());
        get.readAllVersions(); //读取所有的版本数据Result result = version_hbase.get(get);

        Cell[] rawCells = result.rawCells();
        for (Cell cell : rawCells) {

            byte[] name = CellUtil.cloneValue(cell);
            System.out.println(Bytes.toString(name));
        }
        // 5) 释放资源
        admin.close();
        hbConn.close();
    }

注意: 在列族上同时设定TTL也是迟早有用的。如果当前存储的所有时间版本都早于TTL,至少MIN_VERSION个最新版本会保留下来。这样确保在你的查询以及数据早于TTL时有结果返回。

Hbase表的数据压缩

压缩算法

  • 在HBase可以使用多种压缩编码,包括LZO、SNAPPY、GZIP。只在硬盘压缩,内存中或者网络传输中没有压缩
压缩算法压缩后占比压缩解压缩
GZIP13.4%21 MB/s118 MB/s
LZO20.5%135 MB/s410 MB/s
Zippy/Snappy22.2%172 MB/s409 MB/s
  • GZIP的压缩率最高,但是其实CPU密集型的,对CPU的消耗比其他算法要多,压缩和解压速度也慢
  • LZO的压缩率居中,比GZIP要低一些,但是压缩和解压速度明显要比GZIP快很多,其中解压速度快的更多
  • Zippy/Snappy的压缩率最低,而压缩和解压速度要稍微比LZO要快一些

查看表的压缩算法

  • 通过以下输出可以看出,HBase创建表默认是没有指定压缩算法的
    在这里插入图片描述

设置数据压缩
语法如下:

  • 创建新的表,并指定数据压缩算法 : create “DAY02_SPACE:MSG", {NAME => “C1”, COMPRESSION => “GZ”}
  • 修改已有的表,并指定数据压缩算法 : alter " DAY02_SPACE :MSG", {NAME => “C1”, COMPRESSION => “GZ”}

说明

  • 默认情况,一个HBase的表只有一个Region,被托管在一个RegionServer中
  • 那么在这种情况下,如果有大量的写入操作 以及大量的读取的操作, 都要去操作这张表, 而由于这个表只有一个region,也就说只在一个regionserver上,
    此时这个regionServer就会面临高并发读写操作,此时有可能导致服务器出现宕机的问题
  • 每个Region有两个重要的属性:Start Key、End Key,表示这个Region维护的ROWKEY范围
  • 如果只有一个Region,那么Start Key、End Key都是空的,没有边界。所有的数据都会放在这个Region中,但当数据越来越大时,会将Region分裂,取一个Mid Key来分裂成两个Region

Hbase表的预分区设计

预分区指的是在初始化创建表的时候, 提前预先设置多个region到Hbase中, 而多个region可以均匀落在各个regionServer上,从而分担高并发的读写操作

  1. 方式一: 指定 start key,end key 来分区
    hbase> create ‘ns1:t1’, ‘f1’, SPLITS => [‘10’, ‘20’, ‘30’, '40’]
    hbase> create ‘t1’, ‘f1’, SPLITS => [‘10’, ‘20’, ‘30’, '40’]
    hbase> create ‘t1’, ‘f1’, SPLITS_FILE => ‘splits.txt’, OWNER => 'johndoe’

  2. 方式二: hash分区方案
    hbase> create ‘t1’, ‘f1’, {NUMREGIONS => 15, SPLITALGO => ‘HexStringSplit’}

Hbase表的rowkey设计

Hbase官方推荐的设计原则:

  • 避免使用递增行键/时序数据
  • 如果ROWKEY设计的都是按照顺序递增(例如:时间戳),这样会有很多的数据写入时,负载都在一台机器上。我们尽量应当将写入大压力均衡到各个RegionServer
  • 避免rowkey和列的长度过大
  • 在HBase中,要访问一个Cell(单元格),需要有ROWKEY、列蔟、列名,如果ROWKEY、列名太大,就会占用较大内存空间。所以ROWKEY
    和列的长度应该尽量短小
  • ROWKEY的最大长度是64KB,建议越短越好
  • 使用Long等类型比String类型更省空间
  • long类型为8个字节,8个字节可以保存非常大的无符号整数,例如:18446744073709551615。如果是字符串,是按照一个字节一个字符方式保存,需要快3倍的字节数存储
  • 保证ROWKEY的唯一性
  • 设计ROWKEY时,必须保证RowKey的唯一性
  • 由于在HBase中数据存储是Key-Value形式,若向HBase中同一张表插入相同RowKey的数据,则原先存在的数据会被新的数据覆盖。

避免数据出现热点问题:

热点是指大量的客户端(client)直接访问集群的一个或者几个节点(可能是读、也可能是写), 大量地访问量可能会使得某个服务器节点超出承受能力,导致整个RegionServer的性能下降,其他的Region也会受影响

主要的处理方案如下:

  • 反转策略
  • 如果设计出的ROWKEY在数据分布上不均匀,但ROWKEY尾部的数据却呈现出了良好的随机性,可以考虑将ROWKEY的翻转,或者直接将尾部的bytes提前到ROWKEY的开头。
  • 反转策略可以使ROWKEY随机分布,但是牺牲了ROWKEY的有序性
  • 缺点:利于Get操作,但不利于Scan操作,因为数据在原ROWKEY上的自然顺序已经被打乱
  • 加盐策略
  • Salting(加盐)的原理是在原ROWKEY的前面添加固定长度的随机数,也就是给ROWKEY分配一个随机前缀使它和之间的ROWKEY的开头不同
  • 随机数能保障数据在所有Regions间的负载均衡
  • 缺点:因为添加的是随机数,基于原ROWKEY查询时无法知道随机数是什么,那样在查询的时候就需要去各个可能的Regions中查找,加盐对比读取是无力的
  • 哈希策略
  • 基于 ROWKEY的完整或部分数据进行 Hash,而后将Hashing后的值完整替换或部分替换原ROWKEY的前缀部分
  • 这里说的 hash 包含 MD5、sha1、sha256 或 sha512 等算法
  • 缺点:Hashing 也不利于 Scan,因为打乱了原RowKey的自然顺序

其他一些建议:

  • 尽量减少行键和列族的大小在HBase中,value永远和它的key一起传输的。当具体的值在系统间传输时,它的rowkey,列名,时间戳也会一起传输。如果你的rowkey和列名很大,这个时候它们将会占用大量的存储空间。

  • 列族尽可能越短越好,最好是一个字符。

  • 冗长的属性名虽然可读性好,但是更短的属性名存储在HBase中会更好。

HBase的调优

通用调优

  • NameNode的元数据备份使用SSD
  • 定时备份NameNode上的元数据
  • 每小时或者每天备份,如果数据极其重要,可以5~10分钟备份一次。 备份可以通过定时任务复制元数据目录即可
  • 为NameNode指定多个元数据目录
  • 使用dfs.name.dir或者dfs.namenode.name.dir指定。一个指定本地磁盘,一个指定网络磁盘。这样可以提供元数据的冗余和健壮性,以免发生故障。
  • 设置dfs.namenode.name.dir.restore为true,允许尝试恢复之前失败的dfs.namenode.name.dir目录,在创建checkpoint时做此尝试,如果设置了多个磁盘,建议允许。
  • NameNode节点配置为RAID1(镜像盘)结构
  • 保持NameNode日志目录有足够的空间,有助于帮助发现问题。
  • Hadoop是IO密集型框架,所以尽量提升存储的速度和吞吐

Linux调优

  • 开启文件系统的预读缓存可以提高读取速度
 $ sudo blockdev --setra 32768 /dev/sda	(尖叫提示:ra是readahead的缩写)
  • 最大限度使用物理内存
 $ sudo sysctl -w vm.swappiness=0
  • swappiness,Linux内核参数,控制换出运行时内存的相对权重
  • swappiness参数值可设置范围在0到100之间,低参数值会让内核尽量少用交换,更高参数值会使内核更多的 去使用交换空间
  • 默认值为60(当剩余物理内存低于40%(40=100-60)时,开始使用交换空间)
  • 对于大多数操作系统,设置为100可能会影响整体性能,而设置为更低值(甚至为0)则可能减少响应延迟
  • 调整ulimit上限, 默认值为比较小的数字
$ ulimit -n 查看允许最大进程数
$ ulimit -u 查看允许打开最大文件数
  • 开启集群的时间同步NTP

HDFS调优

  • 保证RPC调用会有较多的线程

属性:dfs.namenode.handler.count
解释:该属性是NameNode服务默认线程数,的默认值是10,根据机器的可用内存可以调整为50~100
属性:dfs.datanode.handler.count
解释:该属性默认值为10,是DataNode的处理线程数,如果HDFS客户端程序读写请求比较多,可以调高到15至20,设置的值越大,内存消耗越多,不要调整的过高,一般业务中,5~10即可。

  • 副本数量的调整

属性:dfs.replication
解释:如果数据量巨大,且不是非常之重要,可以调整为2~3,如果数据非常之重要,可以调整为3至5。

  • 文件块大小的调整

属性:dfs.blocksize
解释:块大小定义,该属性应该根据存储的大量的单个文件大小来设置,如果大量的单个文件都小于100M,建议设置
成64M块大小,对于大于100M或者达到GB的这种情况,建议设置成256M,一般设置范围波动在64M~256M之间。

HBase调优

  • 优化DataNode允许的最大文件数

属性:dfs.datanode.max.transfer.threads
文件:hdfs-site.xml
解释:HBase一般都会同一时间操作大量的文件,根据集群的数量和规模以及数据动作,设置为4096或者更高。默认值:4096

  • 优化延迟高的数据操作的等待时间

属性:dfs.image.transfer.timeout
文件:hdfs-site.xml
解释:如果对于某一次数据操作来讲,延迟非常高,socket需要等待更长的时间,建议把该值设置为更大的值(默认
60000毫秒),以确保socket不会被timeout掉。

  • 优化数据的写入效率

属性:
mapreduce.map.output.compress mapreduce.map.output.compress.codec
文件:mapred-site.xml
解释:开启这两个数据可以大大提高文件的写入效率,减少写入时间。第一个属性值修改为true,第二个属性值修改为:
org.apache.hadoop.io.compress.GzipCodec

  • 优化DataNode存储

属性:dfs.datanode.failed.volumes.tolerated 文件:hdfs-site.xml
解释:默认为0,意思是当DataNode中有一个磁盘出现故障,则会认为该DataNode shutdown了。如果修改为1,则一个磁盘出现故障时,数据会被复制到其他正常的DataNode上。

  • 设置RPC监听数量

属性:hbase.regionserver.handler.count
文件:hbase-site.xml
解释:默认值为30,用于指定RPC监听的数量,可以根据客户端的请求数进行调整,读写请求较多时,增加此值。

  • 优化HStore文件大小

属性:hbase.hregion.max.filesize
文件:hbase-site.xml
解释:默认值10737418240(10GB),如果需要运行HBase的MR任务,可以减小此值,因为一个region对应一个map任 务,如果单个region过大,会导致map任务执行时间过长。该值的意思就是,如果HFile的大小达到这个数值,则这个region会被切分为两个Hfile。

  • 优化hbase客户端缓存

属 性 :hbase.client.write.buffer 文件:hbase-site.xml
解释:用于指定HBase客户端缓存,增大该值可以减少RPC调用次数,但是会消耗更多内存,反之则反之。一般我们需要
设定一定的缓存大小,以达到减少RPC次数的目的。

  • 指定scan.next扫描HBase所获取的行数

属性:hbase.client.scanner.caching
文件:hbase-site.xml
解释:用于指定scan.next方法获取的默认行数,值越大,消耗内存越大。

内存优化

HBase操作过程中需要大量的内存开销,毕竟Table是可以缓存在内存中的,一般会分配整个可用内存的70%给HBase的 Java堆。但是不建议分配非常大的堆内存,因为GC过程持续太久会导致RegionServer处于长期不可用状态,一般16~48G内存就可以了,如果因为框架占用内存过高导致系统内存不足,框架一样会被系统服务拖死。

  • JVM优化
  • 并行GC

参 数 :-XX:+UseParallelGC 解释:开启并行GC

  • 同时处理垃圾回收的线程数

参数:-XX:ParallelGCThreads=cpu_core – 1
解释:该属性设置了同时处理垃圾回收的线程数。

  • 禁用手动GC

参数:-XX:DisableExplicitGC
解释:防止开发人员手动调用GC

Zookeeper调优

参数:zookeeper.session.timeout

文件:hbase-site.xml

解释:In hbase-site.xml, set zookeeper.session.timeout to 30 seconds or less to bound failure detection (20-30 seconds is a good start).该值会直接关系到master发现服务器宕机的最大周期,默认值为30秒,如果该值过小,会在HBase在写入大量数据发生而GC时,导致RegionServer短暂的不可用,从而没有向ZK发送心跳包,最终导致认为从节点shutdown。一般20台左右的集群需要配置5台zookeeper。

总结

Hbase高级的是真多,手写了好久,要吐了,看了的不要白嫖,点赞再跑。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

技术武器库

一句真诚的谢谢,胜过千言万语

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值