2021-12-16 hadoop3:目录树

本文基于源码hadoop-3.3.0,个人理解欢迎指正。

目录

1 概述

2 FSDirectory源码

2.1 构造函数

2.2 变量

2.3 关于此类的方法

3 INode

3.1 构造函数

3.2 主要变量

3.3 操作权限

3.4 INodeDirectory

3.5 INodeFile


1 概述

namenode的一个重要作用就是维护集群中的文件目录树。对于hdfs的namespace的状态,Hadoop中提供了FSDirectory和FSNamesystem两个类进行管理,但是FSDirectory主要是管理内存中的文件系统,而FSNamesystem则是用于将集群的操作记录到磁盘之中,并且FSNamesystem在FSDirectory的基础上添加了editlog的相关操作。

2 FSDirectory源码

FSDirectory在实现上自然是参考是Linux上的设计,因此这里也有INode的概念。

2.1 构造函数

FSDirectory(FSNamesystem ns, Configuration conf) throws IOException {
    // 创建iNodeId,用于唯一标识iNode
    this.inodeId = new INodeId();
    // 创建rootDir,即 /
    rootDir = createRoot(ns);
    // 创建map: GSet<INode, INodeWithAdditionalFields>
    inodeMap = INodeMap.newInstance(rootDir);
    // 是否开启权限认证,dfs.permissions.enabled默认true
    this.isPermissionEnabled = conf.getBoolean(
        DFSConfigKeys.DFS_PERMISSIONS_ENABLED_KEY,
        DFSConfigKeys.DFS_PERMISSIONS_ENABLED_DEFAULT);
    // 是否递归检查子目录,dfs.permissions.ContentSummary.subAccess,默认false
    this.isPermissionContentSummarySubAccess = conf.getBoolean(
        DFSConfigKeys.DFS_PERMISSIONS_CONTENT_SUMMARY_SUBACCESS_KEY,
        DFSConfigKeys.DFS_PERMISSIONS_CONTENT_SUMMARY_SUBACCESS_DEFAULT);
    // 用户名
    this.fsOwnerShortUserName =
        UserGroupInformation.getCurrentUser().getShortUserName();
    // 超级用户组:dfs.permissions.superusergroup默认supergroup
    this.supergroup = conf.get(
        DFSConfigKeys.DFS_PERMISSIONS_SUPERUSERGROUP_KEY,
        DFSConfigKeys.DFS_PERMISSIONS_SUPERUSERGROUP_DEFAULT);
    // dfs.namenode.acls.enabled,是否开启acl认证,默认true
    this.aclsEnabled = conf.getBoolean(
        DFSConfigKeys.DFS_NAMENODE_ACLS_ENABLED_KEY,
        DFSConfigKeys.DFS_NAMENODE_ACLS_ENABLED_DEFAULT);
    LOG.info("ACLs enabled? " + aclsEnabled);
    // dfs.namenode.posix.acl.inheritance.enabled:true,子目录是否继承此目录权限
    this.posixAclInheritanceEnabled = conf.getBoolean(
        DFSConfigKeys.DFS_NAMENODE_POSIX_ACL_INHERITANCE_ENABLED_KEY,
        DFSConfigKeys.DFS_NAMENODE_POSIX_ACL_INHERITANCE_ENABLED_DEFAULT);
    LOG.info("POSIX ACL inheritance enabled? " + posixAclInheritanceEnabled);
    // dfs.namenode.xattrs.enabled: true,是否支持扩展属性(可以理解未自定义属性)
    this.xattrsEnabled = conf.getBoolean(
        DFSConfigKeys.DFS_NAMENODE_XATTRS_ENABLED_KEY,
        DFSConfigKeys.DFS_NAMENODE_XATTRS_ENABLED_DEFAULT);
    LOG.info("XAttrs enabled? " + xattrsEnabled);
    // 扩展属性最大大小dfs.namenode.fs-limits.max-xattr-size:16384(16KB)
    this.xattrMaxSize = (int) conf.getLongBytes(
        DFSConfigKeys.DFS_NAMENODE_MAX_XATTR_SIZE_KEY,
        DFSConfigKeys.DFS_NAMENODE_MAX_XATTR_SIZE_DEFAULT);
    Preconditions.checkArgument(xattrMaxSize > 0,
                                "The maximum size of an xattr should be > 0: (%s).",
                                DFSConfigKeys.DFS_NAMENODE_MAX_XATTR_SIZE_KEY);
    Preconditions.checkArgument(xattrMaxSize <=
                                DFSConfigKeys.DFS_NAMENODE_MAX_XATTR_SIZE_HARD_LIMIT,
                                "The maximum size of an xattr should be <= maximum size"
                                + " hard limit " + DFSConfigKeys.DFS_NAMENODE_MAX_XATTR_SIZE_HARD_LIMIT
                                + ": (%s).", DFSConfigKeys.DFS_NAMENODE_MAX_XATTR_SIZE_KEY);

    // dfs.namenode.accesstime.precision:默认1h, hdfs文件的访问时间,atime
    this.accessTimePrecision = conf.getLong(
        DFS_NAMENODE_ACCESSTIME_PRECISION_KEY,
        DFS_NAMENODE_ACCESSTIME_PRECISION_DEFAULT);

    // dfs.quota.by.storage.type.enabled,默认true,是否开启配置不同存储类型的quota配额
    this.quotaByStorageTypeEnabled =
        conf.getBoolean(DFS_QUOTA_BY_STORAGETYPE_ENABLED_KEY,
                        DFS_QUOTA_BY_STORAGETYPE_ENABLED_DEFAULT);

    // dfs.ls.limit : 1000
    int configuredLimit = conf.getInt(
        DFSConfigKeys.DFS_LIST_LIMIT, DFSConfigKeys.DFS_LIST_LIMIT_DEFAULT);
    this.lsLimit = configuredLimit>0 ?
        configuredLimit : DFSConfigKeys.DFS_LIST_LIMIT_DEFAULT;

    // dfs.content-summary.limit: 5000
    this.contentCountLimit = conf.getInt(
        DFSConfigKeys.DFS_CONTENT_SUMMARY_LIMIT_KEY,
        DFSConfigKeys.DFS_CONTENT_SUMMARY_LIMIT_DEFAULT);
    // 线程休眠的时间: dfs.content-summary.sleep-microsec, 500ms
    this.contentSleepMicroSec = conf.getLong(
        DFSConfigKeys.DFS_CONTENT_SUMMARY_SLEEP_MICROSEC_KEY,
        DFSConfigKeys.DFS_CONTENT_SUMMARY_SLEEP_MICROSEC_DEFAULT);

    // filesystem limits
    // 最大路径长度,dfs.namenode.fs-limits.max-component-length,255b
    this.maxComponentLength = (int) conf.getLongBytes(
        DFSConfigKeys.DFS_NAMENODE_MAX_COMPONENT_LENGTH_KEY,
        DFSConfigKeys.DFS_NAMENODE_MAX_COMPONENT_LENGTH_DEFAULT);
    // 最大目录树,1024 * 1024, dfs.namenode.fs-limits.max-directory-items
    this.maxDirItems = conf.getInt(
        DFSConfigKeys.DFS_NAMENODE_MAX_DIRECTORY_ITEMS_KEY,
        DFSConfigKeys.DFS_NAMENODE_MAX_DIRECTORY_ITEMS_DEFAULT);
    // 每个inode最大的扩展属性数,dfs.namenode.fs-limits.max-xattrs-per-inode,32
    this.inodeXAttrsLimit = conf.getInt(
        DFSConfigKeys.DFS_NAMENODE_MAX_XATTRS_PER_INODE_KEY,
        DFSConfigKeys.DFS_NAMENODE_MAX_XATTRS_PER_INODE_DEFAULT);

    this.protectedDirectories = parseProtectedDirectories(conf);

    Preconditions.checkArgument(this.inodeXAttrsLimit >= 0,
                                "Cannot set a negative limit on the number of xattrs per inode (%s).",
                                DFSConfigKeys.DFS_NAMENODE_MAX_XATTRS_PER_INODE_KEY);
    // We need a maximum maximum because by default, PB limits message sizes
    // to 64MB. This means we can only store approximately 6.7 million entries
    // per directory, but let's use 6.4 million for some safety.
    final int MAX_DIR_ITEMS = 64 * 100 * 1000;
    Preconditions.checkArgument(
        maxDirItems > 0 && maxDirItems <= MAX_DIR_ITEMS, "Cannot set "
        + DFSConfigKeys.DFS_NAMENODE_MAX_DIRECTORY_ITEMS_KEY
        + " to a value less than 1 or greater than " + MAX_DIR_ITEMS);

    // dfs.namenode.name.cache.threshold:10,访问次数超过此阈值的频繁访问文件缓存在 FSDirectory nameCache 中。
    int threshold = conf.getInt(
        DFSConfigKeys.DFS_NAMENODE_NAME_CACHE_THRESHOLD_KEY,
        DFSConfigKeys.DFS_NAMENODE_NAME_CACHE_THRESHOLD_DEFAULT);
    NameNode.LOG.info("Caching file names occurring more than " + threshold
                      + " times");
    nameCache = new NameCache<ByteArray>(threshold);
    namesystem = ns;
    this.editLog = ns.getEditLog();
    ezManager = new EncryptionZoneManager(this, conf);

    this.quotaInitThreads = conf.getInt(
        DFSConfigKeys.DFS_NAMENODE_QUOTA_INIT_THREADS_KEY,
        DFSConfigKeys.DFS_NAMENODE_QUOTA_INIT_THREADS_DEFAULT);

    initUsersToBypassExtProvider(conf);
}

private void initUsersToBypassExtProvider(Configuration conf) {
    // dfs.namenode.inode.attributes.provider.bypass.users
    // 所有操作都将绕过扩展属性检查的用户主体(在安全集群中)或用户名(在不安全集群中)的列表。
    // 这意味着存储在 HDFS 而不是外部提供程序中的文件属性将用于权限检查并在请求时返回。
    String[] bypassUsers = conf.getTrimmedStrings(
        DFSConfigKeys.DFS_NAMENODE_INODE_ATTRIBUTES_PROVIDER_BYPASS_USERS_KEY,
        DFSConfigKeys.DFS_NAMENODE_INODE_ATTRIBUTES_PROVIDER_BYPASS_USERS_DEFAULT);
    for(int i = 0; i < bypassUsers.length; i++) {
        String tmp = bypassUsers[i].trim();
        if (!tmp.isEmpty()) {
            if (usersToBypassExtAttrProvider == null) {
                usersToBypassExtAttrProvider = new HashSet<String>();
            }
            LOG.info("Add user " + tmp + " to the list that will bypass external"
                     + " attribute provider.");
            usersToBypassExtAttrProvider.add(tmp);
        }
    }
}

2.2 变量

此类中比较重要的变量都在构造函数中有所涉及,这里不足更多的赘述。

  • rootDir在此做一点解释,这个变量是INodeDirectory类型,在这里直接初始化为"/",这是整个文件系统的根目录。
  • inodeMap: 记录根目录下所有的INode,并维护INodeId ->INode的映射关系。

2.3 关于此类的方法

FSDirectory类中的方法相当多, 主要是封装了对文件系统目录树的操作, 例如增、删、 改、 查等。 基本上ClientProtocol中的方法, 都能在FSDirectory中找到对应的方法。

3 INode

在HDFS中,无论是目录还是文件,在文件系统目录树中都被看做是一个INode节点,如果是目录,则对应的类是INodeDirectory,如果是文件,则对应的类是INodeFile;INodeDirectory类以及INodeFile类都是INode的子类。其中INodeDirectory中包含一个成员集合变量children,如果该目录下有子目录或者文件,其子目录或文件的INode引用就会被保存在children集合中。HDFS就是通过这种方式维护整个文件系统的目录结构。

3.1 构造函数

/** Directory INode class. */
public INodeDirectory(long id, byte[] name, PermissionStatus permissions,
                      long mtime) {
    super(id, name, permissions, mtime, 0L);
}
/**
 * {@link INode} with additional fields including id, name, permission,
 * access time and modification time.
 */
private INodeWithAdditionalFields(INode parent, long id, byte[] name,
                                  long permission, long modificationTime, long accessTime) {
    super(parent);
    this.id = id;
    this.name = name;
    this.permission = permission;
    this.modificationTime = modificationTime;
    this.accessTime = accessTime;
}
INode(INode parent) {
    this.parent = parent;
}

3.2 主要变量

INode基础抽象类,其内部保存了hdfs文件和目录共有的基本属性;包括当前节点的父节点的INode对象的引用、文件、目录名、用户组、访问权限等。需要注意的是,INode类的设计采用了模板模式。INode类定义的方法多为2个,一个是final的接口方法,用于规范接口的调用;另一个是abstract抽象方法,抽象方法留给子类具体实现。INode类中只有一个字段,就是parent,表名当前INode的父目录。

而在其子类定义了以下公共属性字段对应的方法:

  • userName:文件/目录所属用户名
  • groupName:文件/目录所属用户组
  • fsPermission:文件或者目录权限
  • modificationTime:上次修改时间
  • accessTime:上次访问时间

以及INode的元数据信息对应的方法:

  • id:INode的id
  • name:文件/目录的名称
  • fullPathName:文件/目录的完整路径
  • parent:文件/目录的父节点
/**
   * @param snapshotId
   *          if it is not {@link Snapshot#CURRENT_STATE_ID}, get the result
   *          from the given snapshot; otherwise, get the result from the
   *          current inode.
   * @return user name
   */
abstract String getUserName(int snapshotId);

/** The same as getUserName(Snapshot.CURRENT_STATE_ID). */
@Override
public final String getUserName() {
    return getUserName(Snapshot.CURRENT_STATE_ID);
}

/**
   * @param snapshotId
   *          if it is not {@link Snapshot#CURRENT_STATE_ID}, get the result
   *          from the given snapshot; otherwise, get the result from the
   *          current inode.
   * @return group name
   */
abstract String getGroupName(int snapshotId);

/** The same as getGroupName(Snapshot.CURRENT_STATE_ID). */
@Override
public final String getGroupName() {
    return getGroupName(Snapshot.CURRENT_STATE_ID);
}
/**
   * @param snapshotId
   *          if it is not {@link Snapshot#CURRENT_STATE_ID}, get the result
   *          from the given snapshot; otherwise, get the result from the
   *          current inode.
   * @return permission.
   */
abstract FsPermission getFsPermission(int snapshotId);

/** The same as getFsPermission(Snapshot.CURRENT_STATE_ID). */
@Override
public final FsPermission getFsPermission() {
    return getFsPermission(Snapshot.CURRENT_STATE_ID);
}
...
// 略

3.3 操作权限

INode作为基础的抽象类,其内部的permission字段表示用户的操作权限,在INode中,permission的数据类型为long长整型,其将permission的64字节分为三段,分别用于保存访问权限、用户组标识符和用户名标识符,并巧妙地利用了Java的枚举,建立长整型上的分段操作,实现了上述三个文件属性的操作。

enum PermissionStatusFormat implements LongBitFormat.Enum {
    MODE(null, 16),
    GROUP(MODE.BITS, 24),
    USER(GROUP.BITS, 24);

    final LongBitFormat BITS;

    private PermissionStatusFormat(LongBitFormat previous, int length) {
        BITS = new LongBitFormat(name(), previous, length, 0);
    }

    // 提取最后23个bit的user信息
    static String getUser(long permission) {
        final int n = (int)USER.BITS.retrieve(permission);
        String s = SerialNumberManager.USER.getString(n);
        assert s != null;
        return s;
    }

    // 提取中间25个bit的group信息
    static String getGroup(long permission) {
        final int n = (int)GROUP.BITS.retrieve(permission);
        return SerialNumberManager.GROUP.getString(n);
    }

    // 提取前16个bit的mode信息
    static short getMode(long permission) {
        return (short)MODE.BITS.retrieve(permission);
    }

    /** Encode the {@link PermissionStatus} to a long. */
    // 将其转化为一个long型的权限信息
    static long toLong(PermissionStatus ps) {
        long permission = 0L;
        final int user = SerialNumberManager.USER.getSerialNumber(
            ps.getUserName());
        assert user != 0;
        permission = USER.BITS.combine(user, permission);
        // ideally should assert on group but inodes are created with null
        // group and then updated only when added to a directory.
        final int group = SerialNumberManager.GROUP.getSerialNumber(
            ps.getGroupName());
        permission = GROUP.BITS.combine(group, permission);
        final int mode = ps.getPermission().toShort();
        permission = MODE.BITS.combine(mode, permission);
        return permission;
    }

    // 将long值id转化为权限对象
    static PermissionStatus toPermissionStatus(long id,
                                               SerialNumberManager.StringTable stringTable) {
        int uid = (int)USER.BITS.retrieve(id);
        int gid = (int)GROUP.BITS.retrieve(id);
        return new PermissionStatus(
            SerialNumberManager.USER.getString(uid, stringTable),
            SerialNumberManager.GROUP.getString(gid, stringTable),
            new FsPermission(getMode(id)));
    }

    @Override
    public int getLength() {
        return BITS.getLength();
    }
}

枚举PermissionStatusFormat有三个值,MODE、GROUP、USER,分别用于处理访问权限、用户组标识符和用户名标识符。上述三个枚举值创建时,都会调用PermissionStatusFormat的构造函数,构造函数需要链各个参数,分别是枚举值对应的属性在长整形permission中的偏移量和长度。

在使用INode.getUser(permission)获取用户名的时候,由于用户名标识符保存在permission的41~63位,其会使用枚举USER.retrieve(),它会把成员变量permission和掩码USER.MASK进行与运算,然后右移,获得标识符的值;然后再保存标识符和用户名对应关系的SerialNumberManager实例中进行查找,以得到字符串形式的用户名。INode.setUser()用于设置节点的用户名,它的实现也利用了PermissionStatusFormat,使用USER.combine()设置文件所有者标识符对应位。在HDFS中,用户名和用户标识的影射、用户组名和用户组标识符的影射保存在SerialNumberManager对象中。通过SerialNumberManaer,名字节点不必在INode对象中保存字符串形式的用户名和用户组名,节省了对象对内存的占用。

INode中的方法大多比较简单,提供了对成员变量的访问/设置能力。这些方法中需要额外注意的就是isRoot()判断当前节点是否是目录树的根节点,目录树的根节点是HDFS中最重要的一个目录,所有目录都由根目录衍生。如果INode的成员属性name长度为0,约定这是HDFS的根节点,INode.isRoot()返回true,否则返回false,该节点便不是HDFS的根。

3.4 INodeDirectory

INodeDirectory抽象了HDFS中的目录,目录里面保存了文件和其他子目录。在INodeDirectory实现中的体现是其成员变量children,它是一个用于保存INode的列表。INodeDirectory中的大部分方法都是在操作这个列表,如创建子目录项、查询或遍历子目录项、替换子目录项等,它们的实现都比较简单。

构造方法(继承关系可参见上图):

/** constructor */
public INodeDirectory(long id, byte[] name, PermissionStatus permissions,
                      long mtime) {
    super(id, name, permissions, mtime, 0L);
}

/**
   * Copy constructor
   * @param other The INodeDirectory to be copied
   * @param adopt Indicate whether or not need to set the parent field of child
   *              INodes to the new node
   * @param featuresToCopy any number of features to copy to the new node.
   *              The method will do a reference copy, not a deep copy.
   */
public INodeDirectory(INodeDirectory other, boolean adopt,
                      Feature... featuresToCopy) {
    super(other);
    this.children = other.children;
    if (adopt && this.children != null) {
        for (INode child : children) {
            child.setParent(this);
        }
    }
    this.features = featuresToCopy;
    AclFeature aclFeature = getFeature(AclFeature.class);
    if (aclFeature != null) {
        // for the de-duplication of AclFeature
        removeFeature(aclFeature);
        addFeature(AclStorage.addAclFeature(aclFeature));
    }
}

3.5 INodeFile

在文件系统目录树中,使用INodeFile抽象成一个hdfs文件,它也是INode的子类。INodeFile中包含了两个文件特有的属性:文件信息头header和文件对应的数据块信息blocks。

  • hearder
/** 
   * Bit format:
   * [4-bit storagePolicyID][12-bit BLOCK_LAYOUT_AND_REDUNDANCY]
   * [48-bit preferredBlockSize]
   *
   * BLOCK_LAYOUT_AND_REDUNDANCY contains 12 bits and describes the layout and
   * redundancy of a block. We use the highest 1 bit to determine whether the
   * block is replica or erasure coded. For replica blocks, the tail 11 bits
   * stores the replication factor. For erasure coded blocks, the tail 11 bits
   * stores the EC policy ID, and in the future, we may further divide these
   * 11 bits to store both the EC policy ID and replication factor for erasure
   * coded blocks. The layout of this section is demonstrated as below.
   *
   * Another possible future extension is for future block types, in which case
   * the 'Replica or EC' bit may be extended into the 11 bit field.
   *
   * +---------------+-------------------------------+
   * |     1 bit     |             11 bit            |
   * +---------------+-------------------------------+
   * | Replica or EC |Replica factor or EC policy ID |
   * +---------------+-------------------------------+
   *
   * BLOCK_LAYOUT_AND_REDUNDANCY format for replicated block:
   * 0 [11-bit replication]
   *
   * BLOCK_LAYOUT_AND_REDUNDANCY format for striped block:
   * 1 [11-bit ErasureCodingPolicy ID]
   */
enum HeaderFormat {
    PREFERRED_BLOCK_SIZE(null, 48, 1),
    BLOCK_LAYOUT_AND_REDUNDANCY(PREFERRED_BLOCK_SIZE.BITS,
                                HeaderFormat.LAYOUT_BIT_WIDTH + 11, 0),
    STORAGE_POLICY_ID(BLOCK_LAYOUT_AND_REDUNDANCY.BITS,
                      BlockStoragePolicySuite.ID_BIT_LENGTH, 0);

    private final LongBitFormat BITS;
    ...
}
  • blocks

保存了文件拥有的所有数据块信息,数组元素类型是BlockInfo。BlockInfo类继承自Block类,他保存了数据块与文件、数据块与数据节点的对应关系。

类中对数据块的操作也是针对blocks,如:

/**
 * For a given block (or an erasure coding block group), BlockInfo class
 * maintains 1) the {@link BlockCollection} it is part of, and 2) datanodes
 * where the replicas of the block, or blocks belonging to the erasure coding
 * block group, are stored.
 */
@InterfaceAudience.Private
public abstract class BlockInfo extends Block
    implements LightWeightGSet.LinkedElement {...}

@Override
public BlockInfo getLastBlock() {
    return blocks.length == 0 ? null: blocks[blocks.length-1];
}
/**
   * add a block to the block list
   */
void addBlock(BlockInfo newblock) {
    Preconditions.checkArgument(newblock.isStriped() == this.isStriped());
    if (this.blocks.length == 0) {
        this.setBlocks(new BlockInfo[]{newblock});
    } else {
        int size = this.blocks.length;
        BlockInfo[] newlist = new BlockInfo[size + 1];
        System.arraycopy(this.blocks, 0, newlist, 0, size);
        newlist[size] = newblock;
        this.setBlocks(newlist);
    }
}
  • INodeFile的构造函数
INodeFile(long id, byte[] name, PermissionStatus permissions, long mtime,
          long atime, BlockInfo[] blklist, short replication,
          long preferredBlockSize) {
    this(id, name, permissions, mtime, atime, blklist, replication, null,
         preferredBlockSize, (byte) 0, CONTIGUOUS);
}

INodeFile(long id, byte[] name, PermissionStatus permissions, long mtime,
          long atime, BlockInfo[] blklist, Short replication, Byte ecPolicyID,
          long preferredBlockSize, byte storagePolicyID, BlockType blockType) {
    super(id, name, permissions, mtime, atime);
    final long layoutRedundancy = HeaderFormat.getBlockLayoutRedundancy(
        blockType, replication, ecPolicyID);
    header = HeaderFormat.toLong(preferredBlockSize, layoutRedundancy,
                                 storagePolicyID);
    if (blklist != null && blklist.length > 0) {
        for (BlockInfo b : blklist) {
            Preconditions.checkArgument(b.getBlockType() == blockType);
        }
    }
    setBlocks(blklist);
}

public INodeFile(INodeFile that) {
    super(that);
    this.header = that.header;
    this.features = that.features;
    setBlocks(that.blocks);
}

public INodeFile(INodeFile that, FileDiffList diffs) {
    this(that);
    Preconditions.checkArgument(!that.isWithSnapshot());
    this.addSnapshotFeature(diffs);
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值