Google 早期三驾马车之 BigTable 论文学习与研读

前言

Google,作为全球最大的搜索引擎公司,其伟大之处不仅在于建立了一个强大的搜索引擎,还在于它创造了3项革命性的技术,即:GFS、MapReduce 和 BigTable。作为 Google 早期三驾马车,这三项革命性的技术不仅在大数据领域广为人知,更直接或间接性的推动了大数据、云计算、乃至如今火爆的人工智能领域的发展。

2006年11月6—8日,Google 在美国西雅图召开的第7届操作系统设计与实现研讨会上,发表了论文《Bigtable: A Distributed Storage System for Structured Data》(BigTable:结构化数据的分布式存储系统),分析了设计用于处理海量数据的分布式结构化数据存储系统 BigTable 的工作原理。

关于 GFS 相关介绍与论文研读:查看 GFS

关于 MapReduce 相关介绍与论文研读:查看 MapReduce

随着这3篇重量级论文的发表,基于这3项技术的衍生技术与开源产品如雨后春笋般涌现(Hadoop 就是其中的一个)。至此,Google 以一种独特的方式,推动了大数据处理、云计算等技术的发展。

注:在后 Hadoop 时代,Google 推出新三驾马车 —— Caffeine、Pregel、Dremel

本文学习与研读最初的 BigTable 论文,中英双文版见:中英双文版

总览

BigTable 论文总览如图所示,主要包含十一个部分:介绍(概述)、数据模型、API、BigTable 构件、介绍、优化、性能评估、实际应用、经验教训、相关工作、结论。

一、介绍

        诞生背景:处理分布在数千台普通服务器上的 PB 级的海量数据。
        定义:一个分布式的结构化数据存储系统。
        目标:适用性广泛、可扩展、高性能和高可用性。
        特点:Bigtable 不支持完整的关系数据模型;Bigtable 提供简单的数据模型,用户可以动态的控制数据的分布和格式;数据的下标是行和列的名字,名字可以是任意的字符串;Bigtable 将存储的数据都视为字符串,但是 Bigtable 本身不去解析这些字符串,客户程序通常会在把各种结构化或者半结构化的数据串行化到这些字符串里;通过仔细选择数据的模式,客户可以控制数据的位置相关性;可以通过 BigTable 的模式参数来控制数据是存放在内存中、还是硬盘上。

二、数据模型

    1. 总览
        定义:Bigtable 是一个稀疏的、分布式的、持久化存储的多维度排序 Map。Map 的索引是行关键字、列关键字以及时间戳;Map 中的每个 value 都是一个未经解析的 byte 数组。(row:string, column:string,time:int64)->string 。
        例子:存储海量的网页及相关信息。

       在存储表 Webtable 里:使用 URL 作为行关键字;使用网页的某些属性作为列名;网页的内容存在“contents:”列中;并用获取该网页的时间戳作为标识。
       行名是一个反向 URL。 contents 列族存放的是网页的内容, anchor 列族存放引用该网页的超链接文本。
       CNN 的主页被 Sports Illustrator 和 MY-look 的主页引用,因此该行包含了名为“ anchor:cnnsi.com”和“anchhor:my.look.ca” 的列。每个超链接只有一个版本(时间戳标识区分);而 contents 列则有三个版本,分别由时间戳 t3, t5,和 t6 标识。

      2. 行
      定义:表中的行关键字可以是任意的字符串。对同一个行关键字的读或者写操作都是原子的。Bigtable 通过行关键字的字典顺序来组织数据。表中的每个行都可以动态分区。每个分区叫做一个”Tablet”,Tablet 是数据分布和负载均衡调整的最小单位。
      在 Webtable 里,通过反转 URL 中主机名的方式,可以把同一个域名下的网页聚集起来组织成连续的行。
      具体来说,我们可以把 maps.google.com/index.html 的数据存放在关键字 com.google.maps/index.html 下。把相同的域中的网页存储在连续的区域可以让基于主机和域名的分析更加有效。

      3. 列族
      定义:列关键字组成的集合叫做“列族“,列族是访问控制的基本单位。存放在同一列族下的所有数据通常都属于同一个类型。
      使用:列族在使用之前必须先创建,然后才能在列族中任何的列关键字下存放数据;列族创建后,其中的任何一个列关键字下都可以存放数据。一张表中的列族不能太多(最多几百个),并且列族在运行期间很少改变;一张表可以有无限多个列。
      命名:列关键字的命名语法如下:列族:限定词。 列族的名字必须是可打印的字符串,而限定词的名字可以是任意的字符串。

      4. 时间戳
      定义:在 Bigtable 中,表的每一个数据项都可以包含同一份数据的不同版本;不同版本的数据通过时间戳来索引。时间戳的类型是 64 位整型,可以给时间戳赋值,用来表示精确到毫秒的“实时”时间。数据项中,不同版本的数据按照时间戳倒序排序,即最新的数据排在最前面。
      垃圾回收:减轻多个版本数据的管理负担,每一个列族配有两个设置参数对废弃版本的数据自动进行垃圾收集。
      在 Webtable 的举例里,contents:列存储的时间戳信息是网络爬虫抓取一个页面的时间。垃圾收集机制可以让我们只保留最近三个版本的网页数据。

三、API

      Bigtable 提供了建立和删除表以及列族、修改集群、表和列族的元数据、从每个行中查找值、或者遍历表中的一个数据子集的API。
      特性:支持单行上的事务处理,利用这个功能,用户可以对存储在一个行关键字下的数据进行原子性的读-更新-写操作;  允许把数据项用做整数计数器;允许用户在服务器的地址空间内执行脚本程序。

四、BigTable 构件

       1. 基础构件
       基础构件包括分布式文件系统(GFS) 存储日志文件和数据文件。BigTable 依赖集群管理系统来调度任务、管理共享的机器上的资源、处理机器的故障、以及监视机器的状态。

       2. 数据存储文件(Google SSTable 格式)
       特点:SSTable 是一个持久化的、排序的、不可更改的Map 结构;SSTable 是一系列的数据块,使用块索引(通常存储在 SSTable 的最后)来定位数据块;在打开 SSTable 时索引被加载到内存。

            每次查找通过一次磁盘搜索完成:
            1. 使用二分查找法在内存中的索引里找到数据块的位置;
            2. 从硬盘读取相应的数据块。

            也可以选择把整个 SSTable 都放在内存中,这样就不必访问硬盘了。

        3. Chubby
        定义:一个高可用的、序列化的分布式锁服务组件
        Chubby 服务:
         a.  一个 Chubby 服务包括了 5 个活动的副本,其中的一个副本被选为 Master 并处理请求。只有在大多数副本都是正常运行且彼此之间能够互相通信,Chubby 服务才是可用的。
         b. 当有副本失效的时候,Chubby 使用 Paxos 算法保证副本的一致性。
         c. Chubby 提供了一个名字空间,包括了目录和小文件。每个目录或者文件可以当成一个锁,读写操作是原子的。Chubby 客户程序库提供对 Chubby 文件的一致性缓存。
         d. 每个 Chubby 客户程序都维护一个与 Chubby 服务的会话。如果客户程序不能在租约到期的时间内重新签订会话的租约,这个会话就过期失效。当一个会话失效时,它拥有的锁和打开的文件句柄都失效了。
         e. Chubby 客户程序可以在文件和目录上注册回调函数,当文件或目录改变、或者会话过期时,回调函数会通知客户程序。

    Bigtable 使用 Chubby 完成任务内容:
    1.  确保在任何给定的时间内最多只有一个活动的 Master 副本
    2.  存储 BigTable 数据的自引导指令的位置
    3.  查找 Tablet 服务器,以及在 Tablet 服务器失效时进行善后
    4.  存储 BigTable 的模式信息(每张表的列族信息)
    5.  以及存储访问控制列表

    注:如果 Chubby 长时间无法访问,BigTable 就会失效

五、介绍

        1. 介绍
        三个重要组件
        a. 链接到客户程序中的库
        b. 一个 Master 服务器
        任务:为 Tablet 服务器分配 Tablets、检测新加入的或者过期失效的 Table 服务器、对 Tablet 服务器负载均衡、对保存在 GFS 上的文件进行垃圾收集、处理对模式的相关修改操作,例如建立表和列族。
        c. 多个 Tablet 服务器
        每个 Tablet 服务器都管理一个 Tablet 的集合(通常每个服务器有大约数十个至上千个 Tablet)、每个 Tablet 服务器负责处理它所加载的 Tablet 的读写操作,以及在 Tablets 过大时,对其进行分割。

额外说明
1. 客户端读取的数据都不经过 Master 服务器,直接和 Tablet 服务器通信进行读写操作;

2. 由于 BigTable 的客户程序不必通过 Master 服务器来获取 Tablet 的位置信息,因此,大多数客户程序甚至完全不需要和 Master 服务器通信。

3. 一个 BigTable 集群存储了很多表,每个表包含了一个 Tablet 的集合,而每个 Tablet 包含了某个范围内的行的所有相关数据。

4. 初始状态下,一个表只有一个 Tablet。随着表中数据的增长,它被自动分割成多个 Tablet,缺省情况下,每个 Tablet 的尺寸大约是 100MB 到 200MB。

        2. Table 的位置
            存储结构如图:


                a. 第一层(从左到右)
                第一层是一个包含 Root Tablet 的位置信息、存储在 Chubby 中的文件。Root Tablet 包含了一个特殊的 METADATA 表里所有的 Tablet 的位置信息。METADATA 表的每个 Tablet 包含了一个用户 Tablet 的集合。
                特性:Root Tablet 实际上是 METADATA 表的第一个 Tablet,由于 Root Tablet 永远不会被分割,保证了 Tablet 的位置信息存储结构不会超过三层。
                b. 第二层
                在 METADATA 表里面,每个 Tablet 的位置信息都存放在一个行关键字下面,而这个行关键字是由 Tablet 所在的表的标识符和 Tablet 的最后一行编码而成的。
                特性:METADATA 的每一行都存储了大约 1KB 的内存数据。在 128MB 的 METADATA Tablet 中,三层结构的存储模式共标识 2^34 个 Tablet 的地址。
                c. 第三层
                第三层是具体的 UserTable。
                说明:
                I. 客户程序使用的库会缓存 Tablet 的位置信息。
                II. 若无缓存或信息不正确,客户程序就在树状的存储结构中递归查询 Tablet 位置信息;
                III. 如果客户端缓存是空,寻址算法需要通过三次网络来回通信寻址,其中包括一次 Chubby 读操作;
                IV. 如果客户端缓存的地址信息过期,寻址算法可能需要最多6次网络来回通信才能更新数据,因为只有在缓存中没有查到数据的时候才发现数据过期。

        3. Table 的分配
            a. 介绍
            任何一个时刻,一个Tablet只能分配给一个Tablet服务器;Master 服务器记录了当前 Tablet 服务器的活跃信息、分配信息;BigTable 使用 Chubby 跟踪记录 Tablet 服务器的状态。
            b. 独占锁
            当一个 Tablet 服务器启动时,它在 Chubby 的一个指定目录下建立一个有唯一性名字的文件,并且获取该文件的独占锁。若 Tablet 服务器丢失了 Chubby 上的独占锁,它就停止对 Tablet 提供服务。当 Tablet 服务器终止时,尝试释放持有的文件锁
            c. Master 服务器检测
            Master 服务器负责检查一个 Tablet 服务器是否已经不再为它的 Tablet 提供服务了,并且要尽快重新分配它加载的 Tablet。Master 服务器通过轮询 Tablet 服务器文件锁的状态来检测何时 Tablet 服务器不再为 Tablet 提供服务。Master 服务器在它的 Chubby 会话过期后主动退出。Master 服务器的故障不会改变现有 Tablet 在 Tablet 服务器上的分配状态。
            d. Master 服务器扫描 Tablet 分配状态
            详细步骤如下:
            I.  Master 服务器从 Chubby 获取一个唯一的 Master 锁,用来阻止创建其它的 Master 服务器实例;
            II.  Master 服务器扫描 Chubby 的服务器文件锁存储目录,获取当前正在运行的服务器列表;
            III.  Master 服务器和所有的正在运行的 Tablet 表服务器通信,获取每个 Tablet 服务器上 Tablet 的分配信息;
            IV.  Master 服务器扫描 METADATA 表获取所有的 Tablet 的集合。

        特殊情况:
        Master 服务器发现了一个还没有分配的 Tablet,Master 服务器就将这个 Tablet 加入未分配的 Tablet 集合等待合适的时机分配。在 METADATA 表的 Tablet 还没有被分配之前是不能够扫描它的。因此,在开始扫描之前(步骤 4),如果在第三步的扫描过程中发现 Root Tablet 还没有分配,Master 服务器就把 Root Tablet 加入到未分配的 Tablet 集合。这个附加操作确保了 Root Tablet 会被分配。由于 Root Tablet 包括了所有 METADATA 的 Tablet 的名字,因此 Master 服务器扫描完 Root Tablet 以后,就得到了所有的 METADATA 表的 Tablet 的名字。

        4. Table 服务
            a. 结构如图:


            b. 介绍
            Tablet 的持久化状态信息保存在 GFS 上。更新操作提交到 REDO 日志中。最近提交的那些存放在一个排序的缓存 memtable 中,较早的更新存放在一系列 SSTable 中。
            c. 恢复 Table 过程
            Tablet 服务器首先从 METADATA 表中读取它的元数据(包含组成这个 Tablet 的 SSTable 的列表以及一系列的 Redo Point);Tablet 服务器把 SSTable 的索引读进内存,之后通过重复 Redo Point 之后提交的更新来重建 memtable。

           对 Tablet 服务器写操作
           1. Tablet 服务器首先要检查这个操作格式是否正确、操作发起者是否有执行这个操作的权限。
           2. 成功的修改操作会记录在提交日志里。可以采用批量提交方式提高包含大量小的修改操作的应用程序的吞吐量。
           3. 当一个写操作提交后,写的内容插入到 memtable。

          对 Tablet 服务器读操作
          1. Tablet 服务器会作类似的完整性和权限检查。
          2. 一个有效的读操作在一个由一系列 SSTable 和 memtable 合并的视图里执行。
          3. 由于 SSTable 和 memtable 是按字典排序的数据结构,因此可以高效生成合并视图。

        5. 空间收缩
            背景:写操作使得 memtable 的不断增大,当 memtable 的尺寸到达一个门限值的时候,这个 memtable 就会被冻结,然后创建一个新的 memtable;被冻结住 memtable 会被转换成 SSTable,然后写入 GFS。
            Major Compaction
            定义:合并所有的 SSTable 并生成一个新的 SSTable 的 Merging Compaction 过程
            特点:Major Compaction 过程生成的 SSTable 不包含已经删除的信息或数据。Bigtable 循环扫描它所有的 Tablet,并且定期对它们执行 Major Compaction。Major Compaction 机制允许 Bigtable 回收已经删除的数据占有的资源,并且确保 BigTable 能及时清除已经删除的数据,这对存放敏感数据的服务是非常重要。

六、优化

        1. 局部性群组
        客户程序可以将多个列族组合成一个局部性群族。对 Tablet 中的每个局部性群组都会生成一个单独的 SSTable。将通常不会一起访问的列族分割成不同的局部性群组可以提高读取操作的效率。 
        以局部性群组为单位,设定一些有用的调试参数。
        a. Tablet 服务器依照惰性加载的策略将设定为放入内存的局部性群组的 SSTable 装载进内存。
        b. 加载完成之后,访问属于该局部性群组的列族的时候就不必读取硬盘了。

        这个特性对于需要频繁访问的小块数据特别有用:在 Bigtable 内部,我们利用这个特性提高 METADATA 表中具有位置相关性的列族的访问速度。

        2. 压缩
        客户程序可以控制一个局部性群组的 SSTable 是否需要压缩;如果需要压缩,以什么格式来压缩?
        答案:“两遍”的、可定制的压缩方式。第一遍采用 Bentley and McIlroy’s 方式,这种方式在一个很大的扫描窗口里对常见的长字符串进行压缩;第二遍是采用快速压缩算法,即在一个 16KB 的小扫描窗口中寻找重复数据。两个压缩的算法都很快,在现在的机器上,压缩的速率达到 100-200MB/s,解压的速率达到 400-1000MB/s。
        在 Bigtable 中存储同一份数据的多个版本时,压缩效率会更高。

        3. 通过缓存提高读操作的性能
        a. Bloom 过滤器
        我们通过允许客户程序对特定局部性群组的 SSTable 指定 Bloom 过滤器,来减少硬盘访问的次数。我们可以使用 Bloom 过滤器查询一个 SSTable 是否包含了特定行和列的数据。对于某些特定应用程序,我们只付出了少量的、用于存储 Bloom 过滤器的内存的代价,就换来了读操作显著减少的磁盘访问的次数。使用 Bloom 过滤器也隐式的达到了当应用程序访问不存在的行或列时,大多数时候我们都不需要访问硬盘的目的。
        为了提高读操作的性能,Tablet 服务器使用二级缓存的策略。扫描缓存是第一级缓存,主要缓存 Tablet 服务器通过 SSTable 接口获取的 Key-Value 对;Block 缓存是二级缓存,缓存的是从 GFS 读取的 SSTable 的 22 相比于对整个 SSTable 进行压缩,分块压缩压缩率较低 Block。
        对于经常要重复读取相同数据的应用程序来说,扫描缓存非常有效;对于经常要读取刚刚读过的数据附近的数据的应用程序来说,Block 缓存更有用(例如,顺序读,或者在一个热点的行的局部性群组中随机读取不同的列)。

        b. Commit 日志的实现
        避免生成多个不同磁盘的日志文件,设置每个 Tablet 服务器一个 Commit 日志文件,把修改操作的日志以追加方式写入同一个日志文件。因此一个实际的日志文件中混合了对多个 Tablet 修改的日志记录。
        避免多次读取日志文件,首先把日志按照关键字(table,row name,log sequence number)排序,使得同一个 Tablet 的修改操作的日志记录连续存放;为了并行排序,将日志分割成 64MB 的段,之后在不同的 Tablet 服务器对段进行并行排序。   

        c. Table 恢复提速
        当 Master 服务器将一个 Tablet 从一个 Tablet 服务器移到另外一个 Tablet 服务器时,源 Tablet 服务器会对这个 Tablet 做一次 Minor Compaction(压缩),从而减少 Tablet 服务器的日志文件中没有归并的记录,减少恢复的时间。

       d. 利用不变性
       除了 SSTable 缓存之外的其它部分产生的 SSTable 都是不变的,Master 服务器采用“标记-删除”的垃圾回收方式删除 SSTable 集合中废弃的 SSTable;METADATA 表则保存了 Root SSTable 的集合。SSTable 的不变性使得分割 Tablet 的操作非常快捷。不必为每个分割出来的 Tablet 建立新的 SSTable 集合,而是共享原来的 Tablet 的 SSTable 集合。

七、性能评估

注:此部分中包括多处 Google N 台 Tablet 服务器的 Bigtable 集群的基准数据,如需详细了解可查询原文,此处仅归纳总结。       

1. 单个 Table 服务器的性能
       随机读的性能比其它操作慢一个数量级或以上;内存中的随机读操作速度快很多(内存读取,不读取 GFS 中的 64KB 的 Block);随机和序列写操作的性能比随机读要好些;直接把写入操作的内容追加到一个Commit 日志文件的尾部,并且采用批量提交的方式,通过把数据以流的方式写入到 GFS 来提高性能;随机写和序列写在性能上没有太大的差异;序列读的性能好于随机读。

        2. 性能提升
        性能的提升还不是线性的,随机读的性能随 Tablet 服务器数量增加的提升幅度最小。

八、实际应用

1. Google Analytics 

Google Analytics 是用来帮助 Web 站点的管理员分析他们网站的流量模式的服务,提供了整体状况的统计数据、用户使用网站的行为报告。

2. Google Earth 

Google 通过一组服务为用户提供了高分辨率的地球表面卫星图像,访问的方式可以使通过基于 Web 的 Google Maps 访问接口(maps.google.com),也可以通过 Google Earth 定制的客户端软件访问。

3. 个性化查询 

个性化查询(www.google.com/psearch)是一个双向服务;这个服务记录用户的查询和点击,涉及到各种 Google 的服务,比如 Web 查询、图像和新闻。用户可以浏览他们查询的历史,重复他们之前的查询和点击;用户也可以定制基于 Google 历史使用习惯模式的个性化查询结果。 

九、经验教训

1. 很多类型的错误都会导致大型分布式系统受损,这些错误不仅仅是通常的网络中断、或者很多分布式协议中设想的 fail-stop 类型的错误。包括:内存数据损坏、网络中断、时钟偏差、机器挂起、扩展的和非对称的网络分区、我们使用的其它系统的
Bug(比如 Chubby)、 GFS 配额溢出、计划内和计划外的硬件维护等。我们通过修改协议来解决这些问题。比如,我们在我们的 RPC 机制中加入了 Checksum。我们在设计系统的部分功能时,不对其它部分功能做任何的假设,这样的做法解决了其它的一些问题。比如,我们不再假设一个特定的 Chubby 操作只返回错误码集合中的一个值。

2. 另外一个教训是,我们明白了在彻底了解一个新特性会被如何使用之后,再决定是否添加这个新特性是非常重要的。

3. 还有一个具有实践意义的经验:我们发现系统级的监控对 Bigtable 非常重要(比如,监控 Bigtable 自身以及使用 Bigtable 的客户程序)。

4. 对我们来说,最宝贵的经验是简单设计的价值。考虑到我们系统的代码量(大约 100000 行生产代码),以及随着时间的推移,新的代码以各种难以预料的方式加入系统,我们发现简洁的设计和编码给维护和调试带来的巨大好处。

十、相关工作

Bigtable 的局部性群组提供了类似于基于列的存储方案在压缩和磁盘读取方面具有的性能;

Bigtable 采用 memtable 和 SSTable 存储对表的更新的方法与 Log-Structured Merge Tree【26】存储索引数据更新的方法类似。这两个系统中,排序的数据在写入到磁盘前都先存放在内存中,读取操作必须从内存和磁盘中合并数据产生最终的结果集。 

C-Store 和 Bigtable 对比:

两个系统都采用 Shared-nothing 架构,都有两种不同的数据结构,一种用于当前的写操作,另外一种存放“长时间使用”的数据,并且提供一种机制在两个存储结构间搬运数据。

两个系统在 API 接口函数上有很大的不同:C-Store 操作更像关系型数据库,而 Bigtable 提供了低层次的读写操作接口,并且设计的目标是能够支持每台服务器每秒数千次操作。C-Store 同时也是个“读性能优化的关系型数据库”,而 Bigtable 对读和写密集型应用都提供了很好的性能。 

Bigtable 也必须解决所有的 Shared-nothing 数据库需要面对的、类型相似的一些负载和内存均衡方面的难题,我们的问题在某种程度上简单一些: 
        1. 我们不需要考虑同一份数据可能有多个拷贝的问题,同一份数据可能由于视图或索引的原因以不同的形式表现出来; 
        2. 我们让用户决定哪些数据应该放在内存里、哪些放在磁盘上,而不是由系统动态的判断; 
        3. 我们的系统中没有复杂的查询执行或优化工作。

十一、结论

  注:结束语作为文章的总结部分,原文如下。

我们已经讲述完了 Bigtable,Google 的一个分布式的结构化数据存储系统。Bigtable 的集群从 2005 年 4 月开始已经投入使用了,在此之前,我们花了大约 7 人年设计和实现这个系统。截止到 2006 年 4 月,已经有超过 60 个项目使用 Bigtable 了。我们的用户对 Bigtable 提供的高性能和高可用性很满意,随着时间的推移,他们可以根据自己的系统对资源的需求增加情况,通过简单的增加机器,扩展系统的承载能力。 

由于 Bigtable 提供的编程接口并不常见,一个有趣的问题是:我们的用户适应新的接口有多难?新的使用者有时不太确定使用 Bigtable 接口的最佳方法,特别是在他们已经习惯于使用支持通用事务的关系型数据库的接口的情况下。但是,Google 内部很多产品都成功的使用了 Bigtable 的事实证明了,我们的设计在实践中行之有效。 

我们现在正在对 Bigtable 加入一些新的特性,比如支持二级索引,以及支持多 Master 节点的、跨数据中心复制的 Bigtable 的基础构件。我们现在已经开始将 Bigtable 部署为服务供其它的产品团队使用,这样不同的产品团队就不需要维护他们自己的 Bigtable 集群了。随着服务集群的扩展,我们需要在 Bigtable 系统内部处理更多的关于资源共享的问题了。

最后,我们发现,建设 Google 自己的存储解决方案带来了很多优势。通过为 Bigtable 设计我们自己的数据模型,是我们的系统极具灵活性。另外,由于我们全面控制着 Bigtable 的实现过程,以及 Bigtable 使用到的其它的 Google 的基础构件,这就意味着我们在系统出现瓶颈或效率低下的情况时,能够快速的解决这些问题。 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值