ResourceManager详细组件及功能

       在Yarn框架设计中其采用了Master/Slave结构,其中Master实现为ResourceManager,负责整个集群资源的管理与调度;Slave实现为NodeManager,负责单个节点的资源管理与任务启动。

       ResourceManager是整个Yarn集群中最重要的组件之一,其主要的功能包括ApplicationMaster的管理(启动、停止等)、NodeManager管理、Application管理、状态机管理等;并且ResourceManager负责集群中所有资源的统一管理和分配,它接收来自各个节点的资源汇报信息,并把这些信息按照一定的策略分配给各个应用程序;

ResourceManager基本功能与其协议接口

ResourceManager协议接口

        在整个Yarn框架中主要涉及到7个协议,分别是ApplicationClientProtocol、MRClientProtocol、ContainerManagementProtocol、ApplicationMasterProtocol、ResourceTracker、LocalizationProtocol、TaskUmbilicalProtocol,这些协议封装了各个组件交互的信息。

        在ResourceManager组件的交互中,其需要和对应的NodeManager、ApplicationMaster以及对应的客户端进行信息交互,其中涉及到的RPC协议有ResourceTrackerProtocol、ApplicationMasterProtocol和ApplicationClientProtocol。具体如下:

  • ResourceTracker:NodeManager通过该RPC协议向ResourceManager中注册、汇报节点健康情况以及Container的运行状态,并且领取ResourceManager下达的重新初始化、清理Container等命令。NodeManager和ResourceManager这种RPC通信采用了和MRv1类似的“pull模型”(ResourceManager充当RPC server角色,NodeManager充当RPC client角色),NodeManager周期性主动地向ResourceManager发起请求,并且领取下达给自己的命令。
  • ApplicationMasterProtocol:应用程序的ApplicationMaster同过该协议向ResourceManager注册、申请和释放资源。该协议和上面协议同样也是采用了“pull模型”,其中在RPC机制中,ApplicationMaster充当RPC client角色,ResourceManager充当RPC server角色。
  • ApplicationClientProtocol:应用程序的客户端通过该协议向ResourceManager提交应用程序、控制应用程序(如杀死job)以及查询应用程序的运行状态等。在该RPC协议中应用程序客户端充当RPC client角色,ResourceManager充当RPC server角色。

ResourceManager基本功能

       在ResourceManager内部其分别用于响应该RPC协议的组件为ResourceTrackeServer、ApplicationMasterService以及ClientRMServer。ResourceManager的基本功能概括如下:

  • 与客户端进行交互,处理来自于客户端的请求,如查询应用的运行情况等。
  • 启动和管理各个应用的ApplicationMaster,并且为ApplicationMaster分配第一个Container并用于启动和在它运行失败时将它重新启动。
  • 管理NodeManager,接收来自NodeManager的资源和节点健康情况汇报,并向NodeManager下达管理资源命令,例如kill掉某个container。
  • 资源管理和调度,接收来自ApplicationMaster的资源申请,并且为其进行资源的分配。

 

ResourceManager内部组件架构

       ResourceManager内部不同的组件及其对应的功能模块以及部分代码实现如下:

1、用户交互模块

        用户交互模块即上图显示的User Service管理模块。ResourceManager分别针对普通用户、管理员和Web提供了三种对外服务来分别处理不同的请求,具体实现分别对应ClientRMService、AdminService和WebApp;具体部分实现代码(ClientRMService)如下:

  • ClientRMService:ClientRMService是为普通用户提供的服务,它处理来自客户端各种RPC请求,比如提交应用程序、终止应用程序、获取应用程序运行状态等
  • AdminService:ResourceManager为管理员提供了一套独立的服务接口,以防止大量的普通用户请求使管理员发送的管理命令饿死,管理员可通过这些接口管理集群,比如动态更新节点列表、更新ACL列表、更新队列信息等
  • WebApp:为了更加友好地展示集群资源使用情况和应用程序运行状态等信息,YARN对外提供了一个WEB界面,这一部分是YARN仿照Haml开发的一个轻量级嵌入式Web框架

ClientRMService是一个RPC Server,其主要用来响应来自于普通客户端的各种RPC请求。其实现了ApplicationClientProtocol协议接口如下:

public class ClientRMService extends AbstractService implements
    ApplicationClientProtocol {
  private static final ArrayList<ApplicationReport> EMPTY_APPS_REPORT = new ArrayList<ApplicationReport>();
 
  private static final Log LOG = LogFactory.getLog(ClientRMService.class);
 
  final private AtomicInteger applicationCounter = new AtomicInteger(0);
  final private YarnScheduler scheduler; // 调度器
  final private RMContext rmContext; // RM上下文对象,其包含了RM大部分运行时信息,如节点列表、队列列表、应用程序列表等
  private final RMAppManager rmAppManager; // app管理对象
 
  private Server server; // RPC Server
  protected RMDelegationTokenSecretManager rmDTSecretManager;
 
  private final RecordFactory recordFactory = RecordFactoryProvider.getRecordFactory(null);
  InetSocketAddress clientBindAddress;
  private final ApplicationACLsManager applicationsACLsManager; // 控制访问列表ACL
  private final QueueACLsManager queueACLsManager;
  
  // ......
  @Override
  protected void serviceStart() throws Exception {
    Configuration conf = getConfig();
    YarnRPC rpc = YarnRPC.create(conf);
    this.server =   // 实现RPC协议ApplicationClientProtocol 
      rpc.getServer(ApplicationClientProtocol.class, this,
            clientBindAddress,
            conf, this.rmDTSecretManager,
            conf.getInt(YarnConfiguration.RM_CLIENT_THREAD_COUNT, 
                YarnConfiguration.DEFAULT_RM_CLIENT_THREAD_COUNT));
    
    this.server.start();
    // ......
  }
}

2、NodeManager管理

         NodeManager主要是通过NMLivelinessMonitor、ResourceTrackerService和NodeListManager等组件服务来对NodeManager的生命周期、心跳处理以及黑名单进行响应处理。

  • NMLivelinessMonitor:NMLivelinessMonitor主要用来监控心跳异常请求。该服务会周期性地遍历集群中的所有NodeManager,如果某个NodeManager在一定时间内(默认10min,可以有参数yarn.nm.liveness-monitor.expiry-interval-ms配置)没有进行心跳汇报,那么则认为它已经死掉,同时在该节点上运行的Container也会被置为运行失败释放资源。那么这些被置为失败的Container是不会直接被RM分配执行的,RM只是负责将这些被置为失败的Container信息告诉它们所对应的ApplicationMaster,由ApplicationMaster进行任务失败重试or丢弃的处理,如果需要重新运行的话,该ApplicationMaster要重新向RM申请资源,然后由ApplicationMaster与对应的NodeManager通信以重新运行之前失败的Container。
  • NodesListManager:NodeListManager主要分管黑名单(include列表)和白名单(exlude列表)管理功能,分别有yarnresouecemanager.nodes.include-path和yarnresourcemanager.nodes.exclude-path指定。黑名单列表中的nodes不能够和RM直接通信(直接抛出RPC异常),管理员可以对这两个列表进行编辑,然后使用$HADOOP_HOME/bin/yarn rmadmin -refreshNodes动态加载修改后的列表,使之生效。
  • ResourceTrackerService:处理来自NodeManager的请求,主要包括注册和心跳两种请求,其中,注册时NodeManager启动时发生的行为,请求包中包含节点ID、可用的资源上限等信息;而心跳时周期性行为,包含各个Container运行状态,运行的Application列表、节点资源状况等信息,作为请求的应答,ResourceTrackerService可为NodeManager返回待释放的Container列表、Application列表等信息

       ResourceTrackerService是RPC协议接口ResourceTracker的实现,其主要用于处理来自NodeManager的RPC请求,请求主要包含2种信息:注册NodeManager和处理心跳信息。NodeManger启动后会向ResourceManager进行注册,注册时NodeManager会向ResourceTrackerService发送RPC请求信息,其请求信息封装构造为RegisterNodeManagerRequest对象,主要包含NodeManager所在节点的节点ID、可用资源总量、对外开放的http端口、节点的host和port等信息,具体代码ResourceTrackerService#registerNodeManager如下:

public RegisterNodeManagerResponse registerNodeManager(
    RegisterNodeManagerRequest request) throws YarnException,
    IOException {
  NodeId nodeId = request.getNodeId(); // 唯一性NodeID
  String host = nodeId.getHost(); // NodeManager所在节点的host
  int cmPort = nodeId.getPort(); // NodeManager所在节点的port
  int httpPort = request.getHttpPort(); // 对外开放的http端口
  Resource capability = request.getResource(); // NodeManager节点的资源

  RegisterNodeManagerResponse response = recordFactory
      .newRecordInstance(RegisterNodeManagerResponse.class);

  // 构造RMNode状态机来维护NodeManager的生命周期
  RMNode rmNode = new RMNodeImpl(nodeId, rmContext, host, cmPort, httpPort,
        resolve(host), capability, nodeManagerVersion);

  // ......
  // 检查节点注册信息的正确性
  
  String message =
    "NodeManager from node " + host + "(cmPort: " + cmPort + " httpPort: "
        + httpPort + ") " + "registered with capability: " + capability
        + ", assigned nodeId " + nodeId;
  LOG.info(message);
  response.setNodeAction(NodeAction.NORMAL);
  response.setRMIdentifier(ResourceManager.getClusterTimeStamp());
  response.setRMVersion(YarnVersionInfo.getVersion());
  return response;
}

        ResourceTrackerService还会负责处理来自NodeManager的周期性心跳,当NodeManager启动后,它会周期性地调用RPC接口ResourceTracker#nodeHeartbeat向ResourceManager汇报心跳,心跳信息主要包含该节点的各个Container的运行状态、正在运行的Application列表、节点的健康状况等,随后ResourceManager为该NodeManager返回需要释放的Container列表、Application列表等信息。其中心跳信息的处理如下:

  1. 从NodeManager的心跳请求信息中获得节点的状态信息NodeStatus
  2. 检测该节点是否已经注册过
  3. 检测该节点的host名称是否合法,例如是否在excluded列表中
  4. 检测该次心跳是不是最后一次响应的心跳信息,避免心跳的重复发送与应答
  5. 为NodeManager返回心跳应答信息,向NodeManager发送RMNodeStatusEvent事件通知其更新对应NodeManager的节点负载信息并保存最近一次心跳应答信息
public NodeHeartbeatResponse nodeHeartbeat(NodeHeartbeatRequest request)
    throws YarnException, IOException {
  // 从请求信息中获得nodeManager所在节点的健康状况
  NodeStatus remoteNodeStatus = request.getNodeStatus();
  /**
   * Here is the node heartbeat sequence...
   * 1. Check if it's a registered node
   * 2. Check if it's a valid (i.e. not excluded) node 
   * 3. Check if it's a 'fresh' heartbeat i.e. not duplicate heartbeat 
   * 4. Send healthStatus to RMNode
   */

  NodeId nodeId = remoteNodeStatus.getNodeId();

  // 1. Check if it's a registered node
  RMNode rmNode = this.rmContext.getRMNodes().get(nodeId);
  if (rmNode == null) {
    /* node does not exist */
    String message = "Node not found resyncing " + remoteNodeStatus.getNodeId();
    LOG.info(message);
    resync.setDiagnosticsMessage(message);
    return resync;
  }

  // Send ping
  this.nmLivelinessMonitor.receivedPing(nodeId);

  // 2. Check if it's a valid (i.e. not excluded) node
  if (!this.nodesListManager.isValidNode(rmNode.getHostName())) {
    String message =
        "Disallowed NodeManager nodeId: " + nodeId + " hostname: "
            + rmNode.getNodeAddress();
    LOG.info(message);
    shutDown.setDiagnosticsMessage(message);
    this.rmContext.getDispatcher().getEventHandler().handle(
        new RMNodeEvent(nodeId, RMNodeEventType.DECOMMISSION));
    return shutDown;
  }
   
  // 3. Check if it's a 'fresh' heartbeat i.e. not duplicate heartbeat
  NodeHeartbeatResponse lastNodeHeartbeatResponse = rmNode.getLastNodeHeartBeatResponse();
  if (remoteNodeStatus.getResponseId() + 1 == lastNodeHeartbeatResponse
      .getResponseId()) {
    LOG.info("Received duplicate heartbeat from node "
        + rmNode.getNodeAddress());
    return lastNodeHeartbeatResponse;
  } else if (remoteNodeStatus.getResponseId() + 1 < lastNodeHeartbeatResponse
      .getResponseId()) {
    String message =
        "Too far behind rm response id:"
            + lastNodeHeartbeatResponse.getResponseId() + " nm response id:"
            + remoteNodeStatus.getResponseId();
    LOG.info(message);
    resync.setDiagnosticsMessage(message);
    // TODO: Just sending reboot is not enough. Think more.
    this.rmContext.getDispatcher().getEventHandler().handle(
        new RMNodeEvent(nodeId, RMNodeEventType.REBOOTING));
    return resync;
  }

  // Heartbeat response
  NodeHeartbeatResponse nodeHeartBeatResponse = YarnServerBuilderUtils
      .newNodeHeartbeatResponse(lastNodeHeartbeatResponse.
          getResponseId() + 1, NodeAction.NORMAL, null, null, null, null,
          nextHeartBeatInterval);
  rmNode.updateNodeHeartbeatResponseForCleanup(nodeHeartBeatResponse);

  populateKeys(request, nodeHeartBeatResponse);

  // 4. Send status to RMNode, saving the latest response.
  this.rmContext.getDispatcher().getEventHandler().handle(
      new RMNodeStatusEvent(nodeId, remoteNodeStatus.getNodeHealthStatus(),
          remoteNodeStatus.getContainersStatuses(), 
          remoteNodeStatus.getKeepAliveApplications(), nodeHeartBeatResponse));

  return nodeHeartBeatResponse;
}

3、ApplicationMaster管理模块

  ApplicationMaster的管理主要由以下组件来协作完成:

  • AMLivelinessMonitor:监控AM是否活着,如果一个ApplicationMaster在一定时间内未汇报心跳信息,则认为它死掉了,它上面所有正在运行的Container将被置为失败状态,而AM本身会被重新分配到另外一个节点上执行
  • ApplicationMasterLauncher:与某个NodeManager通信,要求它为某个应用程序启动ApplicationMaster
  • ApplicationMasterService:处理来自ApplicationMaster的请求,主要包括注册和心跳两种请求,其中,注册是ApplicationMaster启动时发生的行为,注册请求包中包含ApplicationMaster启动节点;对外RPC端口号和trackingURL等信息;而心跳而是周期性行为,汇报信息包含所需资源描述、待释放的Container列表、黑名单列表等,而AMS则为之返回新分配的Container、失败的Container、待抢占的Container列表等信息

其中ApplicationMaster和ResourceManager的交互启动流程如下:

  1. 当ResourceManager接收到来自客户端的提交应用程序请求时就会向资源管理器申请一个container资源用于启动该应用程序所对应的ApplicationMaster,申请到资源后由ApplicationMasterLaucher与对应的NodeManager进行通信,要求该NodemManager在其所在节点启动该ApplicationMaster。
  2. ApplicationMaster启动完毕后,ApplicationMasterLuacher通过事件的形式将刚刚启动的ApplicationMaster注册到AMLivelinessMonitor,以启动心跳监控。
  3. ApplicationMaster启动后主动向ApplicationMasterService注册,并将自己所在host、端口等信息向其汇报。
  4. ApplicationMaster在运行的过程中不断向ApplicationMasterService发送心跳。心跳信息中包含需要申请的资源描述。
  5. ApplicationMasterService每次收到ApplicationMaster的心跳信息后,会同时AMLivelinessMonitor更新其最近一次发送心跳的时间。
  6. 当应用程序运行完毕后,ApplicationMaster向ApplicationMasterService请求注销自己。
  7. ApplicationMasterService收到注销请求后,会将该应用程序的运行状态标注为完成,并且同时AMLivelinessMonitor移除对该ApplicationMaster的心跳监控。

 其组件具体运行原理如下:

  • ApplicationMasterLauncher:ApplicationMasterLauncher是以线程池方式实现的一个事件处理器,其主要处理AMLaucherEvent类型的事件,包括启动(LAUNCH)和清除(CLEANUP)对应ApplicationMaster。
    • 当接收到LAUNCH类型的事件,ApplicationMasterLauncher立马会和对应的NodeManager进行通信,封装启动该ApplicationMaster所需要的各种信息,包括:启动命令、JAR包、环境变量等信息为StartContainerRequest请求对象。并通过RPC函数ContainerManagementProtocol#startContainers将请求信息发送给对应的NodeManager;NodeManager接收到来自ApplicationMasterLauncher的启动命令就会启动对应的ApplicationMaster。
    • 当接收到CLEANUP类型事件,ApplicationMasterLauncher立马会和对应的NodeManager进行通信,要求NodeManager杀死该ApplicationMaster,并释放掉资源。
public class ApplicationMasterLauncher extends AbstractService implements
    EventHandler<AMLauncherEvent> {
  private final ThreadPoolExecutor launcherPool;
  private LauncherThread launcherHandlingThread;
  
  private final BlockingQueue<Runnable> masterEvents
    = new LinkedBlockingQueue<Runnable>();
  
  protected final RMContext context;
    
  protected Runnable createRunnableLauncher(RMAppAttempt application, 
      AMLauncherEventType event) {
    Runnable launcher = new AMLauncher(context, application, event, getConfig());
    return launcher;
  }
  
  private void launch(RMAppAttempt application) {
    Runnable launcher = createRunnableLauncher(application, AMLauncherEventType.LAUNCH);
    masterEvents.add(launcher);
  }

  private void cleanup(RMAppAttempt application) {
    Runnable launcher = createRunnableLauncher(application, AMLauncherEventType.CLEANUP);
    masterEvents.add(launcher);
  } 

  @Override
  public synchronized void  handle(AMLauncherEvent appEvent) {
    AMLauncherEventType event = appEvent.getType();
    RMAppAttempt application = appEvent.getAppAttempt();
    switch (event) {
    case LAUNCH:
      launch(application);
      break;
    case CLEANUP:
      cleanup(application);
    default:
      break;
    }
  }
}
//AMLauncher#launch()函数
private void launch() throws IOException, YarnException {
  connect();
  ContainerId masterContainerID = masterContainer.getId();
  ApplicationSubmissionContext applicationContext =
    application.getSubmissionContext();
  LOG.info("Setting up container " + masterContainer
      + " for AM " + application.getAppAttemptId());  
  ContainerLaunchContext launchContext =
      createAMContainerLaunchContext(applicationContext, masterContainerID);

  // 启动container信息封装
  StartContainerRequest scRequest =
      StartContainerRequest.newInstance(launchContext,
        masterContainer.getContainerToken());
  List<StartContainerRequest> list = new ArrayList<StartContainerRequest>();
  list.add(scRequest);
  StartContainersRequest allRequests =
      StartContainersRequest.newInstance(list);

  // 调用RPC接口代理containerMgrProxy,发送startContainers请求
  StartContainersResponse response =
      containerMgrProxy.startContainers(allRequests);
}
  • AMLivelinessMonitor:AMLivelinessMonitor会周期性地遍历集群中的所有ApplicationMaster,如果某个ApplicationMaster在一定时间内(默认10min,可以有参数yarn.am.liveness-monitor.expiry-interval-ms配置)没有进行心跳汇报,那么则认为它已经死掉,同时该ApplicationMaster关联运行的Container也会被置为运行失败释放资源。如果Application运行失败,则有ResourceManager重新为它申请资源,并且在另外的节点上启动它(AM启动尝试次数由参数yarn.resourcemanager.am.max-attempts控制,默认2)。那么这些被置为失败的Container是不会直接被RM分配执行的,RM只是负责将这些被置为失败的Container信息告诉它们所对应的ApplicationMaster,由它进行失败任务的重试or丢弃,如果需要重新运行的话,该ApplicationMaster要从新向RM申请资源,然后由ApplicationMaster与对应的NodeManager通信以从新运行之前失败的Container。
  • ApplicationMasterService:ApplicationMasterService的主要职能是处理来自ApplicationMaster的心跳请求,另外也还处理Application的注册和清理请求。
    • 注册是Application启动完成后发生的,它向ApplicationMasterService发送注册请求包,包含:ApplicationMaster所在的节点、RPC端口、tracking url等信息。
    • 心跳信息的周期性处理。ApplicationMaster向ApplicationMasterService发送的周期性的心跳请求包AllocateResponse allocate(AllocateRequest request);其中包含信息:请求资源类型的描述、待释放的container列表等。ApplicationMasterService返回的心跳应答信息包含:ApplicationMasterService为之分配的Container、失败的Container等信息。
    • 清理请求是在ApplicationMaster运行完毕后,向ApplicationMasterService发送的,主要是叫其回收释放资源。
public AllocateResponse allocate(AllocateRequest request)
    throws YarnException, IOException {

  // 先更新amLivelinessMonitor中App的心跳时间
  ApplicationAttemptId appAttemptId =
      amrmTokenIdentifier.getApplicationAttemptId();
  ApplicationId applicationId = appAttemptId.getApplicationId();
  this.amLivelinessMonitor.receivedPing(appAttemptId);

  synchronized (lock) {
    // 注册检查、心跳请求信息检查
    // ......

    // Send the status update to the appAttempt.
    // 根据请求信息更新对应的RMAppAttempt状态机
    this.rmContext.getDispatcher().getEventHandler().handle(
        new RMAppAttemptStatusupdateEvent(appAttemptId, request
            .getProgress()));

    // 请求与释放的资源列表
    List<ResourceRequest> ask = request.getAskList();
    List<ContainerId> release = request.getReleaseList();

    RMApp app = this.rmContext.getRMApps().get(applicationId);
    
    // 尝试使用资源调度器YarnScheduler进行资源的调度分配
    // Send new requests to appAttempt.
    Allocation allocation =
        this.rScheduler.allocate(appAttemptId, ask, release, 
            blacklistAdditions, blacklistRemovals);

    //
    RMAppAttempt appAttempt = app.getRMAppAttempt(appAttemptId);
    AllocateResponse allocateResponse =
        recordFactory.newRecordInstance(AllocateResponse.class);
    if (!allocation.getContainers().isEmpty()) {
      allocateResponse.setNMTokens(allocation.getNMTokens());
    }

    // update the response with the deltas of node status changes
    // 增量更新对应的RMNode状态机

    // 根据申请到的containers列表构造返回信息
    allocateResponse.setAllocatedContainers(allocation.getContainers());
    allocateResponse.setCompletedContainersStatuses(appAttempt
        .pullJustFinishedContainers());
    allocateResponse.setResponseId(lastResponse.getResponseId() + 1);
    allocateResponse.setAvailableResources(allocation.getResourceLimit());
    allocateResponse.setNumClusterNodes(this.rScheduler.getNumClusterNodes());
    
    lock.setAllocateResponse(allocateResponse);
    return allocateResponse;
  }    
}

4、状态机管理模块

        ResourceManager使用有限状态机维护有状态对象的生命周期,状态机的引入使得YARN设计架构更加清晰。ResourceManager共维护了四类状态机,分别是RMApp、RMAppAttempt、RMContainer和RMNode。

  • RMApp:RMApp维护了一个应用程序的整个运行周期,包括从启动到运行结束整个过程。由于一个Application的生命周期可能会启动多个Application运行实例,因此可认为,RMApp维护的是同一个Application启动的所有运行实例的生命周期。
  • RMAppAttempt:一个应用程序可能启动多个实例,即一个实例运行失败后,可能再次启动一个重新运行,而每次启动称为一个运行尝试,用“RMAppAttempt”描述,RMAppAttempt维护了一次运行尝试的整个生命周期。
  • RMContainer:RMContainer维护了一个Container的运行周期,包括从创建到运行结束整个过程。RM将资源封装成Container发送给应用程序的ApplicationMaster,而ApplicationMaster则会在Container描述的运行环境中启动任务,因此,从这个层面上讲,Container和任务的生命周期是一致的。
  • RMNode:RMNode维护了一个NodeManager的生命周期,包括启动到运行结束整个过程。

5、安全管理模块

        ResourceManager自带了非常全面的权限管理机制,主要由ClientTOAMSecretManager、ContainerTokenSecretManager、ApplicationTokenSecretManager等模块完成

6、资源分配模块

        该模块主要涉及一个组件--ResourceScheduler。ResourceScheduler是资源调度器,它按照一定的约束条件将集群中的资源分配给各个应用程序,当前主要考虑内存和CPU资源。ResourceScheduler是一个插拔式模块,YARN自带了一个批处理资源调度器 -- FIFO和两个多用户调度器 -- Fair Scheduler和Capacity Scheduler。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值