HBase性能调优

46 篇文章 6 订阅
13 篇文章 0 订阅

在线的OLTP系统对响应时间的要求非常高。当HBase为OLTP系统提供在线实时的数据存储时,响应时间以及吞吐量尤为重要。某一个配置项的不妥当可能直接造成线上HBase集群整体响应超时,然后应用服务器线程池耗尽,最终导致服务不可用,而一些简单的配置改动可能会让HBase集群性能提升数倍,因此HBase在线调优对HBase在企业生产环境的应用非常重要。

一、客户端调优

1,设置客户端写入缓存

如果业务能够容忍数据丢失,如一些日志数据,那么客户端写入HBase表时可以采取批量缓存的方式,数据先缓存在客户端,当达到配置的阈值时再批量提交到服务器端。注意,如果客户端重启或者宕机,则这部分缓存的数据会丢失。

(1)HBase 1.x版本API只需要通过设置HTable.setAutoFlush(false),再设置缓存大小,即可在实现客户端写入时先在客户端缓存。

表级别设置写入缓存如下:

HTable.setAutoFlush(false);
HTable.setWriteBufferSize(1024 * 1024 * 10);// 缓存大小10MB

(2) HBase 2.x版本API则需要使用BufferedMutator来实现客户端的缓存、异步写入分区服务器

  package com.mt.hbase.chpt09.client;

  import com.mt.hbase.chpt05.rowkeydesign.RowKeyUtil;
  import com.mt.hbase.connection.HBaseConnectionFactory;
  import com.mt.hbase.constants.Constants;
  import org.apache.hadoop.hbase.TableName;
  import org.apache.hadoop.hbase.client.BufferedMutator;
  import org.apache.hadoop.hbase.client.BufferedMutatorParams;
  import org.apache.hadoop.hbase.client.Connection;
  import org.apache.hadoop.hbase.client.Put;
  import org.apache.hadoop.hbase.client.RetriesExhaustedWithDetailsException;
  import org.apache.hadoop.hbase.util.Bytes;
 
  import java.io.IOException;
  importjava.util.ArrayList;
  import java.util.List;
 
  /**
    * 批量、异步向HBase写入数据
    */
  private static void doBufferedMutator() {
      final BufferedMutator.ExceptionListener listener = new BufferedMutator.    ExceptionListener() {
         @Override
         public void onException(RetriesExhaustedWithDetailsException e,    BufferedMutator mutator) {
            for (int i = 0; i < e.getNumExceptions(); i++){
              System.out.println("error mutator: " + e.getRow(i));
           }
        }
     };
     BufferedMutatorParams params = new BufferedMutatorParams(TableName.valueOf    (Constants.TABLE)).listener(listener);
     params.writeBufferSize(10*1024*1024);
     try {
        Connection conn = HBaseConnectionFactory.getConnection();
        BufferedMutator mutator = conn.getBufferedMutator(params);
        List<Put> actions = new ArrayList<Put>();
        Put put = new Put(Bytes.toBytes("rowkey1"));
        put.addColumn(Bytes.toBytes(Constants.CF_PC), Bytes.toBytes(Constants.    COLUMN_VIEW),Bytes.toBytes("value1"));
        actions.add(put);
       mutator.mutate(actions);
        mutator.close();
        conn.close();
     } catch (IOException e1) {
        e1.printStackTrace();
     }
  }

2,设置合适的扫描缓存

Scan操作一般需要查询大量的数据,如果一次RPC请求就将所有数据都加载到客户端,则请求时间会比较长。同时,由于数据量大,网络传输也容易出错,因此HBase Scan API提供了一个分批拉取数据然后缓存到客户端的功能。每次ResultScanner.next()被调用时,只要当前客户端扫描缓存数据为空,HBase客户端就会去服务器端拉取下一批数据。如果缓存值设置得过大,每次获取的数据过多,那么容易造成请求超时,甚至由于数据过大造成内存OutOfMemory异常;如果缓存值设置得过小,就会增加一些额外的RPC请求。因此一般会根据业务需求进行平衡,设置一个最适合业务的值,如1000等。设置批量拉取数据的代码如下:

Scan scan = new Scan();
scan.setCaching(1000);//表示一个RPC请求读取的数据条数

3,跳过WAL写入

预写入日志(WAL)可用于分区服务器异常恢复,在第7章有详细介绍。数据的写入操作需要等待WAL刷新写入文件系统,因此,对于一些能够容忍部分数据丢失的业务,如日志系统等,可以跳过WAL写入以提高写入速度,代码如下:

Put.setDurability(Durability.SKIP_WAL);
Delete.setDurability(Durability.SKIP_WAL);

4,设置重试次数与间隔

当 HBase 客户端请求在服务器端出错并抛出异常后,如果抛出的异常不是DoNotRetryIOException类的子类,那么客户端会发起重试。客户端超时时间、重试的间隔与次数需要配置合理,否则容易造成分区服务器请求雪崩,进而导致应用服务器线程池线程耗尽,系统无法正常响应。以下两个配置项决定了重试次数以及重试间隔。
(1)hbase.client.pause:重试的休眠时间系数。
(2)hbase.client.retries.number:最大重试次数,默认为35,建议减少,如5。
重试间隔为休眠时间系数乘A,其中A=RETRY_BACKOFF[重试次数],RETRY_BACKOFF是一个常数数组,代码如下:

public static final int [] RETRY_BACKOFF = {1, 2, 3, 5, 10, 20, 40, 100, 10 0,
100, 100, 200, 200};

如果设置hbase.client.pause=1000,hbase.client.retries.number=10,那么10次重试间隔为1、2、3、5、10、20、40、100、100、100,单位为秒(s)。
如果重试次数超过了RETRY_BACKOFF数组大小,则A=200(数组最后一个元素)。

HBase 2.4.9源代码中描述了如下几种可重试的异常:
● NotServingRegionException;
● RegionServerStoppedException;
● OutOfOrderScannerNextException;
● UnknownScannerException;
● ScannerResetException。

5,选用合适的过滤器

Scan请求通常需要扫描大量的数据行,过滤器可以用来在服务器端过滤掉一部分不需要的数据,从而减少在服务器端和客户端之间传输的数据量。

使用过滤器来提升Scan请求性能的手段:
(1)组合使用KeyOnlyFilter、FirstKeyOnlyFilter。KeyOnlyFilter可以使得服务器端返回的数据量只包含行键,FirstKeyOnlyFilter可以减少服务器端扫描的数据量,只需要扫描到每行的第一列。
(2)避免使用包含大量Filter的FilterList。假如在用户行为日志管理系统中查询出商品ID从1001到9999的订单数据,此时使用包含多个SingleColumnValueFilter的FilterList可以满足需求(多个过滤器的关系为Operator.MUST_PASS_ONE,类似于MySQL中的OR),但是性能可能会非常低,此时采用扫描用户所有的订单数据到客户端过滤的性能反而会更好。我试过在7000多行(每行数据大小约200字节)的用户数据中使用SingleColumnValueFilter,100个以内使用FilterList的过滤操作速度会很快,一旦超过100个,使用Scan设置开始行键和结束行键扫描用户所有的数据到内存再过滤反而会更快。当然,这与用户的数据量大小有关,并不是说100就是一个最优数字,不同业务可以通过实验得到不同的最优数字。
(3)过滤器尽量使用字节比较器,因为HBase数据以字节形式存储。

二、服务器端调优

1,创建表语句的优化

(1)使用数据块编码

DATA_BLOCK_ENCODING表示针对行键使用的数据块编码格式。常用的一种数据块编码格式为PREFIX,当开启PREFIX编码后HBase数据存储文件中会添加一个保存着当前行行键与上一行行键具有的相同前缀字符的列。
除了PREFIX编码格式,HBase还支持以下几种数据块编码格式。
(1)DIFF:对PREFIX的一种扩展,将PREFIX对键前缀的缩略扩展到值与时间戳(存储偏移量而不是相同前缀字符数量)。DIFF对读写会有比较大的影响,但是可以缓存更多数据,一般不启用。
(2)FAST_DIFF:一种对DIFF更快的实现,与DIFF区别不大。
(3)ROW_INDEX_V1:针对提升随机读性能的一种编码格式,尤其适用键值对占用空间较小的场景,ROW_INDEX_V1主要为了提升搜索性能,会在数据块的头部添加一些索引信息,使得落在数据块上的行键可以使用二分查找,所以其编码后占用的空间更大,类似于数据库索引,可以提升随机Get性能,但是对大批数据的Scan操作无提升效果。

(2)使用布隆过滤器

BLOOMFILTER表示使用布隆过滤器。布隆过滤器可以用来提高随机读的性能。HBase支持ROW与ROWCOL这两种类型的布隆过滤器,ROWCOL只对指定列的随机读(Get操作)有效,如果应用中的随机读没有指定读哪个列限定符,那么设置ROWCOL是没有效果的,这种场景下就应该使用ROW。
布隆过滤器的原理是,当一个元素被加入集合时,通过K
个散列函数将这个元素映射成一个位数组中的K
个点,把它们的值置为1。检索时,我们只要看这些点的值是不是都是1就(大约)知道集合中有没有它了:如果这些点中有任何一个0,则被检元素一定不在;如果都是1,则被检元素很可能在。
布隆过滤器的数据存在存储文件的元数据中,开启布隆过滤器会有一定的存储以及内存开销。在生产环境中观察到一个开启了Snappy压缩的表,20 GB左右的Store(4~5个存储文件),布隆过滤器占用的空间在80 MB左右,因此这个开销还是可以接受的。

(3)开启数据压缩

压缩/解压是一个消耗CPU的操作,因此启用HBase压缩会导致CPU使用率上升。数据压缩在写入MemStore之后,MemStore刷新输出到磁盘之前,对写性能影响不大,但是读取数据时需要将数据块解压后才能读取以及放入缓存,因此对读性能有负面影响。

开启压缩很简单,只需以下两步:
1)在Hadoop配置文件core-site.xml中添加以下配置项:

<property>
    <name>io.compression.codecs</name>    
    <value>        
    org.apache.hadoop.io.compress.GzipCodec,        
    org.apache.hadoop.io.compress.DefaultCodec,        
    com.hadoop.compression.lzo.LzoCodec,        
    com.hadoop.compression.lzo.LzopCodec,        
    org.apache.hadoop.io.compress.BZip2Codec,        
    org.apache.hadoop.io.compress.SnappyCodec    
    </value>
</property>

2)在创建表的语句中增加如下属性:

COMPRESSION => 'SNAPPY'

(4)设置合理的数据块大小

BLOCKSIZE属性决定了HBase读取数据的最小块大小。为了提升性能,理想情况下每次查询所需扫描的数据都能够放到一个数据块或者整数倍的数据块中。例如,用户的行为数据、同一个用户的所有数据能够集中地放在几个或者多个数据块当中。HFile.java源代码中有一段注释:我们推荐将数据块的大小设置为8 KB~1 MB。大的数据块比较适合顺序查询(如Scan操作),但不适合随机查询(因为需要解压缩一个大的数据块)。小的数据块适合随机查询,但是需要更多的内存来保存数据块的索引,而且创建文件的时候也可能比较慢,因为在每个数据块的结尾我们都要把压缩的数据流刷新到文件中去(引起更多的Flush操作)。此外,由于压缩编解码器还需要一定的缓存,因此最小的数据块大小应该在20 KB~30 KB,默认的数据块大小为64 KB。

创建表的语句将表的数据块大小设置为128 KB:

BLOCKSIZE => '131072'

一般情况下建议根据数据行的大小以及业务使用的模式来设置合理的块大小,默认的64 KB是在随机读与顺序读之间比较平衡的一个设置。

(5)预分区

预分区的好处是可以负载均衡以充分利用集群中分区服务器的能力,最终提升整体的性能。每个表在设计时应该结合表的大小以及分区服务器的数量,来决定分区的多少,一般一个分区的存储文件的大小约为10 GB~50 GB,太大的存储文件合并或者拆分会对集群性能造成灾难性的影响。一个分区服务器上分区的数量也最好不要太多,假如一个表预计存储数据1 TB左右,那么比较合适的预分区方案是将表分为50~100个分区,这样最终每个分区的存储文件大小约为10 GB~20 GB。当然,如果集群中分区服务器的数量非常大,例如上百台,那么将表分为更多的分区(超过集群分区服务器数量的1倍)会是一个更优的方案。

创建表的语句代码片段可以用来将表预分为10个区:

SPLITS =>['1' ,'2','3' ,'4' ,'5','6','7','8','9']

2,禁止分区自动拆分与合并

默认情况下HBase自动管理分区的拆分和大合并,这样可以减少一些运维工作。在行键设计合理的情况下,分区基本上都是均衡地增长的。如果采用自动拆分与大合并,很容易造成拆分和合并风暴,在生产环境中一个30 GB左右的Store大合并时间可以超过20分钟。如果是离线集群,则可能不会太关注拆分和合并对集群性能的影响,但是对实时在线集群来说,这个影响可能就无法接受了,尤其是大合并需要合并所有存储文件、清理过期数据、清理删除状态的数据以及将数据本地化,这对网络(对分区服务器的网络带宽占用可以超过1 Gbit/s)和磁盘I/O都有很大的压力,因此对在线集群一般需要禁止自动拆分和大合并,然后在合适的时候进行手动拆分和手动合并(一般在业务非高峰期做拆分和合并),同时将拆分和大合并操作错开,将这些操作产生的网络与磁盘I/O负载均衡,最小化对线上服务的影响。
手动拆分还有一个优势是线上分区的名称与数量不会变化,这样一些自动化的监控程序或者调试脚本可以很好地工作,而自动化分区拆分会给分区重命名、监控或者运维带来麻烦。

(1)禁止自动拆分

参数hbase.regionserver.regionSplitLimit控制HBase表最大的分区数量,超过则不能进行自动拆分。建议在建表的时候先预分区,然后在需要的时候(例如某个分区的存储文件超过50 GB)选择集群空闲时间执行手动拆分。

为了禁止分区自动拆分需要将以下配置项添加到配置文件hbase-site.xml中:

<property>
    <name>hbase.regionserver.regionSplitLimit</name>    
    <value>1</value>
</property

(2)禁止自动大合并

与合并相关的配置项。
1)hbase.hregion.majorcompaction:该参数控制大合并间隔的时间,默认为604 800 000 ms,即7天,设置为0表示禁止自动大合并。
2)hbase.hstore.blockingStoreFiles:当Store的存储文件数量超过该参数配置的值时,需要在刷新MemStore前先进行拆分或者合并,除非等待超过hbase.hstore.blockingWaitTime配置的时间,默认的配置时间为90 000 ms。因此需要适当调大该参数,以免MemStore刷新被阻塞,进而影响写入操作,导致整个分区服务器服务异常。
3)hbase.hstore.compactionThreshold:当Store的存储文件数量大于等于该参数配置的值时,可能会触发合并。默认值为3,如果配置值过大,可以推迟触发合并的时间,但是会造成Store的存储文件数量过大,影响查询的性能,一般设置为5以内。

为了禁止自动大合并需要将以下配置项添加到配置文件hbase-site.xml中:

<property>
    <name>hbase.hregion.majorcompaction</name>    
    <value>0</value>
</property>
<property>
    <name>hbase.hstore.blockingStoreFiles</name>    
    <value>500</value>
</property>
<property>
    <name>hbase.hstore.compactionThreshold</name>    
    <value>5</value>
</property>

3,开启机柜感知

机柜感知实际上是HDFS的一个优化。HDFS为了保证数据的安全,数据文件默认会在HDFS上保存3份副本,存储策略是本地存一份,另外一个机柜A上的机器X存一份,机柜A上的另外一台机器Y存一份。这样一方面能够保证数据就近读取,另一方面即使某个机柜上面的所有机器出现断电或者网络异常,HDFS也能够在另外一个机柜上找到数据的备份。那么Hadoop是怎样判断两台不同的机器是否在同一个机柜呢?
这就需要配置机柜感知了,默认情况下机柜感知是关闭的。如果集群中所有的机器都在一个默认的机柜下,就可能会造成网络流量的浪费。假设集群中总共有6台物理机,机器A、B、C、D、E和F分属于两个机柜,机器A、B和C属于机柜M,机器D、E和F属于机柜N,Hadoop配置的数据副本数为3。在没有配置机柜感知的情况下,当机器A有数据写入时,第一份数据副本存放在A,另外两份副本可能存放在B、C、D、E和F中的任意两台机器,有可能第二份副本存放在机器E,第三份副本存放在机器B。这样,数据的流向是A→E→B,也就是说,数据从机柜M流向机柜N,然后又流回M,显然从N流回M是不必要的。

开启机柜感知:

(1)增加core-site.xml的配置项

在Hadoop的配置文件core-site.xml中增加以下配置项,指定了配置机柜感知的脚本文件路径:

<property>
    <name>topology.script.file.name</name>    
    <value>/data/hadoop-2.10.1/etc/hadoop/topology.sh</value>
</property>

(2)增加机柜感知脚本topology.sh,需要读取配置文件topology.data来录入机柜信息

#!/bin/bash
HADOOP_CONF=/data/hadoop-2.10.1/etc/hadoop
while [ $# -gt 0 ] ; do  
	nodeArg=$1  
	exec<${HADOOP_CONF}/topology.data  
	result=""  
	while read line ; do    
	ar=( $line )    
	if [ "${ar[0]}" = "$nodeArg" ]||[ "${ar[1]}" = "$nodeArg" ]; then      
		result="${ar[2]}"    
	fi  
	done  
	shift  
	if [ -z "$result" ] ; then    
		echo -n "/default-rack"  
	else    
		echo -n "$result"  
	fi    
		done

(3)增加机柜配置信息topology.data。

该文件每行描述了一个Hadoop机器节点的机房机柜信息,格式为“IP+主机名+/机房/机柜”,如下配置表示一个6台机器的集群,其中master1、slave1和slave3属于同一个机柜rack1,另外3台属于机柜rack2。

192.168.172.1 master1 /dc1/rack1
192.168.172.2 master2 /dc1/rack2
192.168.172.3 slave1 /dc1/rack1
192.168.172.4 slave2 /dc1/rack2
192.168.172.5 slave3 /dc1/rack1
192.168.172.6 slave4 /dc1/rack2

(4)重启名称节点使机柜感知生效。

开启机柜感知后需要重启名称节点,重启之后执行下面的命令即可查看配置是否生效:

./hadoop dfsadmin –printTopology
Rack: /dc1/rack1  
192.168.172.1:50010 (master1)  
192.168.172.3:50010 (slave1)  
192.168.172.5:50010 (slave3)

Rack: /dc1/rack2  
192.168.172.2:50010 (master2)  
192.168.172.4:50010 (slave2)  
192.168.172.6:50010 (slave4)

要添加新节点到集群中,则只需将新节点配置添加到topology.data。
调用下面的命令即可使机柜感知在新节点生效:

./hadoop dfsadmin –refreshNodes

4,开启短路本地读

HBase集群中的每个节点上一般都会同时部署Hadoop数据节点以及HBase分区服务器。HBase分区服务器在大合并过程中会将多个小的存储文件合并成一个大的存储文件,同时这个大的存储文件会存放在负责该分区的分区服务器,这就是分区的本地化。
移动“计算”比移动“数据”容易。如果读取数据的Hadoop DFS客户端与数据节点在同一个节点,则称之为“本地读”,与之相对的是“远程读”,也就是Hadoop DFS客户端与数据节点不在同一个节点,那么读取数据就需要一个RPC。默认情况下不管是本地读还是远程读,其实都需要经过一层数据节点的RPC转调。当开启短路本地读(Short Circuit Local Read)配置后,相当于Hadoop DFS客户端直接读取本地文件,而无须经过数据节点的中转。
短路本地读用到了Unix域套接字(Unix Domain Socket),它是一种进程间的通信方式,使得同一台机器上的两个进程能以Socket的方式通信。它带来的另一大好处是,利用它的两个进程间除可以传递普通数据外,还可以传递文件描述符。
假设机器上的两个用户A和B,A拥有访问某个文件的权限而B没有,而B又需要访问这个文件。借助Unix域套接字,可以让A打开文件得到一个文件描述符,然后把文件描述符传递给B,B就能读取文件里面的内容了,即使B没有相应的权限。在HDFS的场景中,A就是数据节点,B就是DFS客户端,需要读取的文件就是数据节点数据目录中的某个文件。

添加到Hadoop配置文件hdfs-site.xml即可开启短路本地读的配置项:

<property>
    <name>dfs.client.read.shortcircuit</name>    
    <value>true</value>
</property>
<property>
    <name>dfs.domain.socket.path</name>    
    <value>/data/hadoop-2.10.1/dn_socket</value>
</property>

注意,需要重启数据节点和分区服务器后配置才会生效,并且需要创建一个空文件/data/ hadoop-2.10.1/dn_socket用来在进程间通信。

5,开启补偿重试读

HBase数据文件基于HDFS存储,为了保证数据的安全可靠,一般会存储多个备份,默认是3个。当开启了短路本地读后,HBase会优先从本地读取数据。在某些情况下,本地磁盘或者网络问题可能会导致短时间内的本地读失败,因此,为了应对这些情况,HBase社区对这种情况提出了补偿重试读
(hedged read)。
开启补偿重试机制后,当客户端发起一个本地读时,如果超过配置的时间还没返回,客户端就会向数据副本所在的其他数据节点发送相同的数据请求。哪一个请求先返回,另一个就会被丢弃。

将下面的配置项添加到hbase-site.xml文件即可开启HBase补偿重试读:

<property>
  <name>dfs.client.hedged.read.threadpool.size</name>  
  <value>20</value>  
  <!—20个线程-->
</property>
<property>
  <name>dfs.client.hedged.read.threshold.millis</name>  
  <value>5000</value>  
  <!—5000 ms -->
</property>
两个配置项:
(1)dfs.client.hedged.read.threadpool.size:并发补偿重试读的线程池大小。
(2)dfs.client.hedged.read.threshold.millis:补偿重试读开始前等待的时间,即如果一个请求在该配置的时间内还未返回,则发起重试读。

6,JVM内存调优

当一个在线应用的访问压力较小的时候,程序性能通常能够达到预期,此时应用管理员都会用一套通用的JVM启动参数来应对这些应用。一旦每秒查询数(Query Per Second,RPS)达到一定的数量级,应用的响应时间(Response Time,RT)通常会因为各种资源瓶颈而直线上升,JVM 垃圾回收时间就是一个影响RT的重要因素。
为了更好地阅读本节内容,读者需要对JVM垃圾回收有一定了解,包括了解垃圾回收的工作原理和常用的垃圾回收算法,理解新生代、老生代等术语。
Oracle公司的Hotspot虚拟机提供了两个并发程度很高的JVM垃圾回收。
(1)并发标记清除回收器(Concurrent Mark Sweep Collector,简称CMS垃圾回收器):“分代回收”算法中老生代的一种回收算法,适用于对停顿时间要求较短、可以为垃圾回收线程共享CPU资源的应用,通常与新生代回收算法“并行新生代回收器”(Parallel New Collector)一起使用。
(2)第一垃圾回收器(Garbage-First Garbage Collector,简称G1垃圾回收器):JDK 7u4发行时G1被正式推出,其设计目标是用来替代CMS垃圾回收器。G1回收器能够更好地预测垃圾回收停顿时间,同时完成高吞吐量的目标,与CMS回收器相比具有多个优点,例如能够进行内存整理,不会产生很多内存碎片;新生代大小与老生代大小不再固定,内存上使用更为灵活;在停顿时间上加了预测机制,用户可以指定停顿时间以免应用雪崩。
目前实际工作中大多数应用系统都在使用CMS垃圾回收器,但G1垃圾回收器是未来JDK垃圾回收器算法的方向。
JVM垃圾回收调优的目标有以下两个。
(1)缩短Minor GC时间,因为Minor GC并发复制会使JVM处于STW(Stop-The-World)状态,整个应用都暂停而无法响应。
(2)减少Full GC次数,每次时间越短越好,因为除了标记阶段的STW状态,Full GC会产生大量内存碎片(使用CMS垃圾回收算法时),并且如果单次垃圾回收时间过长且超过心跳阈值,则会导致HBase分区服务器被ZooKeeper认为已经死亡而从集群中移除。

文章来源:《HBase入门与实践(第2版)》 作者:彭旭

文章内容仅供学习交流,如有侵犯,联系删除哦!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

晓之以理的喵~~

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值