Eureka 源码 分析 以及常见问题

常见问题

1 Eureka 服务发现的主要功能(解答)
2 Eureka 服务有多少个实例(解答)
3 Eureka 服务同步每次都是全量吗(待研究)
4 Eureka 服务发现的数据一致性(解答)
5 Eureka 如何保证数据可靠性,如何确认是一致的(解答)
6 Eureka 如果实例数太多,会面临同步的问题吗(待研究)

7 部分源码分析(解答)
9 Eureka Client注册一个实例为什么这么慢 ,为什么服务上线了,Eureka Client 不能及时获取到(解答)
10 为什么服务下线了,Eureka Server 接口返回的信息还是存在?(解答)
11 为什么有时候会出现 如下提示: (解答)
  EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY’RE NOT. RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE INSTANCES ARE NOT BEING EXPIRED JUST TO BE SAFE.

Eureka 服务发现的主要功能

Register:服务注册
当Eureka客户端向Eureka Server注册时,它提供自身的元数据,比如IP地址、端口,运行状况指示符URL,主页等。
Renew:服务续约
Eureka客户会每隔30秒发送一次心跳来续约。 通过续约来告知Eureka Server该Eureka客户仍然存在,没有出现问题。 正常情况下,如果Eureka Server在90秒没有收到Eureka客户的续约,它会将实例从其注册表中删除。 建议不要更改续约间隔。
Fetch Registries:获取注册列表信息
Eureka客户端从服务器获取注册表信息,并将其缓存在本地。客户端会使用该信息查找其他服务,从而进行远程调用。该注册列表信息定期(每30秒钟)更新一次。每次返回注册列表信息可能与Eureka客户端的缓存信息不同, Eureka客户端自动处理。如果由于某种原因导致注册列表信息不能及时匹配,Eureka客户端则会重新获取整个注册表信息。 Eureka服务器缓存注册列表信息,整个注册表以及每个应用程序的信息进行了压缩,压缩内容和没有压缩的内容完全相同。Eureka客户端和Eureka 服务器可以使用JSON / XML格式进行通讯。在默认的情况下Eureka客户端使用压缩JSON格式来获取注册列表的信息。
Cancel:服务下线
Eureka客户端在程序关闭时向Eureka服务器发送取消请求。 发送请求后,该客户端实例信息将从服务器的实例注册表中删除。该下线请求不会自动完成,它需要调用以下内容:
DiscoveryManager.getInstance().shutdownComponent();
Eviction 服务剔除
在默认的情况下,当Eureka客户端连续90秒没有向Eureka服务器发送服务续约,即心跳,Eureka服务器会将该服务实例从服务注册列表删除,即服务剔除。

Eureka 高可用架构

在这里插入图片描述
从图可以看出在这个体系中,有2个角色,即Eureka Server和Eureka Client。而Eureka Client又分为Applicaton Service和Application Client,即服务提供者何服务消费者。 每个区域有一个Eureka集群,并且每个区域至少有一个eureka服务器可以处理区域故障,以防服务器瘫痪。

Eureka Client向Eureka Serve注册,并将自己的一些客户端信息发送Eureka Serve。然后,Eureka Client通过向Eureka Serve发送心跳(每30秒)来续约服务的。 如果客户端持续不能续约,那么,它将在大约90秒内从服务器注册表中删除。 注册信息和续订被复制到集群中的Eureka Serve所有节点。 来自任何区域的Eureka Client都可以查找注册表信息(每30秒发生一次)。根据这些注册表信息,Application Client可以远程调用Applicaton Service来消费服务

源码分析(服务注册,服务续约)

服务注册其实很简单,在Eureka Client启动的时候,将自身的服务的信息发送到Eureka Server。现在来简单的阅读下源码
在com.netflix.discovery包下有个DiscoveryClient类,该类包含了Eureka Client向Eureka Server的相关
其中DiscoveryClient实现了EurekaClient接口,并且它是一个单例模式,而EurekaClient继承了LookupService接口

在DiscoveryClient类有一个服务注册的方法register(),该方法是通过Http请求向Eureka Client注册

    boolean register() throws Throwable {
            logger.info(PREFIX + appPathIdentifier + ": registering service...");
            EurekaHttpResponse<Void> httpResponse;
            try {
                httpResponse = eurekaTransport.registrationClient.register(instanceInfo);
            } catch (Exception e) {
                logger.warn("{} - registration failed {}", PREFIX + appPathIdentifier, e.getMessage(), e);
                throw e;
            }
            if (logger.isInfoEnabled()) {
                logger.info("{} - registration status: {}", PREFIX + appPathIdentifier, httpResponse.getStatusCode());
            }
            return httpResponse.getStatusCode() == 204;
        }

在DiscoveryClient类继续追踪register()方法,它被InstanceInfoReplicator 类的run()方法调用,其中InstanceInfoReplicator实现了Runnable接口,run()方法代码如下:

     public void run() {
            try {
                discoveryClient.refreshInstanceInfo();

                Long dirtyTimestamp = instanceInfo.isDirtyWithTime();
                if (dirtyTimestamp != null) {
                    discoveryClient.register();// z这里
                    instanceInfo.unsetIsDirty(dirtyTimestamp);
                }
            } catch (Throwable t) {
                logger.warn("There was a problem with the instance info replicator", t);
            } finally {
                Future next = scheduler.schedule(this, replicationIntervalSeconds, TimeUnit.SECONDS);
                scheduledPeriodicRef.set(next);
            }
        }

而InstanceInfoReplicator类是在DiscoveryClient初始化过程中使用的其中有一个initScheduledTasks()方法。该方法主要开启了获取服务注册列表的信息如果需要向Eureka Server注册,则开启注册同时开启了定时向Eureka Server服务续约的定时任务,具体代码如下

    private void initScheduledTasks() {
           ...//省略了任务调度获取注册列表的代码
            if (clientConfig.shouldRegisterWithEureka()) {
             ...
                // Heartbeat timer
                scheduler.schedule(
                        new TimedSupervisorTask(
                                "heartbeat",
                                scheduler,
                                heartbeatExecutor,
                                renewalIntervalInSecs,
                                TimeUnit.SECONDS,
                                expBackOffBound,
                                new HeartbeatThread()
                        ),
                        renewalIntervalInSecs, TimeUnit.SECONDS);

                // InstanceInfo replicator
                instanceInfoReplicator = new InstanceInfoReplicator(
                        this,
                        instanceInfo,
                        clientConfig.getInstanceInfoReplicationIntervalSeconds(),
                        2); // burstSize

                statusChangeListener = new ApplicationInfoManager.StatusChangeListener() {
                    @Override
                    public String getId() {
                        return "statusChangeListener";
                    }

                    @Override
                    public void notify(StatusChangeEvent statusChangeEvent) {

                        instanceInfoReplicator.onDemandUpdate();
                    }
                };
              ...
        }

在看 Eureka Server 方面
Maven的eureka-core:1.6.2的jar包下。打开com.netflix.eureka包,很轻松的就发现了又一个EurekaBootStrap的类,BootStrapContext具有最先初始化的权限,所以先看这个类

   protected void initEurekaServerContext() throws Exception {

     ...//省略代码
       PeerAwareInstanceRegistry registry;
            if (isAws(applicationInfoManager.getInfo())) {
               ...//省略代码,如果是AWS的代码
            } else {
                registry = new PeerAwareInstanceRegistryImpl(
                        eurekaServerConfig,
                        eurekaClient.getEurekaClientConfig(),
                        serverCodecs,
                        eurekaClient
                );
            }

            PeerEurekaNodes peerEurekaNodes = getPeerEurekaNodes(
                    registry,
                    eurekaServerConfig,
                    eurekaClient.getEurekaClientConfig(),
                    serverCodecs,
                    applicationInfoManager
            );
     }

重点关注
其中PeerAwareInstanceRegistryImpl和PeerEurekaNodes两个类看其命名,应该和服务注册以及Eureka Server高可用有关。先追踪PeerAwareInstanceRegistryImpl类,在该类有个register()方法,该方法提供了注册,并且将注册后信息同步到其他的Eureka Server服务。代码如下:

    public void register(final InstanceInfo info, final boolean isReplication) {
            int leaseDuration = Lease.DEFAULT_DURATION_IN_SECS;
            if (info.getLeaseInfo() != null && info.getLeaseInfo().getDurationInSecs() > 0) {
                leaseDuration = info.getLeaseInfo().getDurationInSecs();
            }
            super.register(info, leaseDuration, isReplication);
            replicateToPeers(Action.Register, info.getAppName(), info.getId(), info, null, isReplication); // 向其他节点传播
        }

其中 super.register(info, leaseDuration, isReplication)方法,点击进去到子类AbstractInstanceRegistry可以发现更多细节,其中注册列表的信息被保存在一个Map中。replicateToPeers()方法,即同步到其他Eureka Server的其他Peers节点,追踪代码,发现它会遍历循环向所有的Peers节点注册,最终执行类PeerEurekaNodes的register()方法,该方法通过执行一个任务向其他节点同步该注册信息,代码如下:

      public void register(final InstanceInfo info) throws Exception {
            long expiryTime = System.currentTimeMillis() + getLeaseRenewalOf(info);
            batchingDispatcher.process(
                    taskId("register", info),
                    new InstanceReplicationTask(targetHost, Action.Register, info, null, true) {
                        public EurekaHttpResponse<Void> execute() {
                            return replicationClient.register(info);
                        }
                    },
                    expiryTime
            );
        }

经过一系列的源码追踪,可以发现PeerAwareInstanceRegistryImpl的register()方法实现了服务的注册,并且向其他Eureka Server的Peer节点同步了该注册信息,那么register()方法被谁调用了呢?之前在Eureka Client的分析可以知道,Eureka Client是通过 http来向Eureka Server注册的,那么Eureka Server肯定会提供一个注册的接口给Eureka Client调用,那么PeerAwareInstanceRegistryImpl的register()方法肯定最终会被暴露的Http接口所调用。在Idea开发工具,按住alt+鼠标左键,可以很快定位到ApplicationResource类的addInstance ()方法,即服务注册的接口,其代码如下:

    @POST
        @Consumes({"application/json", "application/xml"})
        public Response addInstance(InstanceInfo info,
                                    @HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication) {

        ...//省略代码                 
                   registry.register(info, "true".equals(isReplication));
            return Response.status(204).build();  // 204 to be backwards compatible
        }

服务续约 源码分析:
服务注册在Eureka Client程序启动之后开启,并同时开启服务续约的定时任务。在eureka-client-1.6.2.jar的DiscoveryClient的类下有renew()方法,其代码如下:

      /**
         * Renew with the eureka service by making the appropriate REST call
         */
        boolean renew() {
            EurekaHttpResponse<InstanceInfo> httpResponse;
            try {
                httpResponse = eurekaTransport.registrationClient.sendHeartBeat(instanceInfo.getAppName(), instanceInfo.getId(), instanceInfo, null);
                logger.debug("{} - Heartbeat status: {}", PREFIX + appPathIdentifier, httpResponse.getStatusCode());
                if (httpResponse.getStatusCode() == 404) {
                    REREGISTER_COUNTER.increment();
                    logger.info("{} - Re-registering apps/{}", PREFIX + appPathIdentifier, instanceInfo.getAppName());
                    return register();
                }
                return httpResponse.getStatusCode() == 200;
            } catch (Throwable e) {
                logger.error("{} - was unable to send heartbeat!", PREFIX + appPathIdentifier, e);
                return false;
            }
        }

另外服务端的续约接口在eureka-core:1.6.2.jar的 com.ne

tflix.eureka包下的InstanceResource类下,接口方法为renewLease(),它是REST接口。为了减少类篇幅,省略了大部分代码的展示。其中有个registry.renew()方法,即服务续约,代码如下:

    @PUT
    public Response renewLease(...参数省略){
         ...  代码省略
        boolean isSuccess=registry.renew(app.getName(),id, isFromReplicaNode);
           ...  代码省略
     }

读者可以跟踪registry.renew的代码一直深入研究。在这里就不再多讲述。另外服务续约有2个参数是可以配置,即Eureka Client发送续约心跳的时间参数和Eureka Server在多长时间内没有收到心跳将实例剔除的时间参数,在默认的情况下这两个参数分别为30秒和90秒,官方给的建议是不要修改,如果有特殊要求还是可以调整的,只需要分别在Eureka Client和Eureka Server修改以下参数:

    eureka.instance.leaseRenewalIntervalInSeconds
    eureka.instance.leaseExpirationDurationInSeconds

最后,服务注册列表的获取、服务下线和服务剔除就不在这里进行源码跟踪解读,因为和服务注册和续约类似,总的来说,通过读源码,可以发现,整体架构与前面小节的eureka 的高可用架构图完全一致

Eureka 集群原理

Eureka Server 集群相互之间通过 Replicate 来同步数据,相互之间不区分主节点和从节点,
所有的节点都是平等的。在这种架构中,节点通过彼此互相注册来提高可用性
每个节点需要添加一个或多个有效的 serviceUrl 指向其他节点。

如果某台 Eureka Server 宕机,Eureka Client 的请求会自动切换到新的 Eureka Server 节点。当宕机的服务器重新恢复后,Eureka 会再次将其纳入到服务器集群管理之中。当节点开始接受客户端请求时,所有的操作都会进行节点间复制,将请求复制到其它 Eureka Server 当前所知的所有节点中。

另外 Eureka Server 的同步遵循着一个非常简单的原则:只要有一条边将节点连接,就可以进行信息传播与同步。所以,如果存在多个节点,只需要将节点之间两两连接起来形成通路,那么其它注册中心都可以共享信息。每个 Eureka Server 同时也是 Eureka Client,多个 Eureka Server 之间通过 P2P 的方式完成服务注册表的同步。

Eureka Server 集群之间的状态是采用异步方式同步的,所以不保证节点间的状态一定是一致的,不过基本能保证最终状态是一致的。

保障了 AP
Eureka Server 各个节点都是平等的,几个节点挂掉不会影响正常节点的工作,剩余的节点依然可以提供注册和查询服务。而 Eureka Client 在向某个 Eureka 注册时,如果发现连接失败,则会自动切换至其它节点。只要有一台 Eureka Server 还在,就能保证注册服务可用(保证可用性),只不过查到的信息可能不是最新的(不保证强一致性)。

Eureka 为了保障注册中心的高可用性,容忍了数据的非强一致性
服务节点间的数据可能不一致
Client-Server 间的数据可能不一致
比较适合跨越多机房对注册中心服务可用性要求较高的使用场景。

Eureka 分区概念

Eureka 提供了 Region 和 Zone 两个概念来进行分区,这两个概念均来自于亚马逊的 AWS:

region:可以理解为地理上的不同区域,比如亚洲地区,中国区或者深圳等等。没有具体大小的限制。根据项目具体的情况,可以自行合理划分 region

zone:可以简单理解为 region 内的具体机房,比如说 region 划分为深圳,然后深圳有两个机房,就可以在此 region 之下划分出 zone1、zone2 两个 zone。

上图中的 us-east-1c、us-east-1d、us-east-1e 就代表了不同的 Zone。
Zone 内的 Eureka Client 优先和 Zone 内的 Eureka Server 进行心跳同步
同样调用端优先在 Zone 内的 Eureka Server 获取服务列表
当 Zone 内的 Eureka Server 挂掉之后,才会从别的 Zone 中获取信息

Erueka 工作流程

1、Eureka Server 启动成功,等待服务端注册。在启动过程中如果配置了集群,集群之间定时通过 Replicate 同步注册表,每个 Eureka Server 都存在独立完整的服务注册表信息

2、Eureka Client 启动时根据配置的 Eureka Server 地址注册中心注册服务

3、Eureka Client 会每 30s 向 Eureka Server 发送一次心跳请求,证明客户端服务正常

4、当 Eureka Server 90s 内没有收到 Eureka Client 的心跳,注册中心则认为该节点失效,会注销该实例

5、单位时间内 Eureka Server 统计到有大量的 Eureka Client 没有上送心跳,则认为可能为网络异常,进入自我保护机制,不再剔除没有上送心跳的客户端

6、当 Eureka Client 心跳请求恢复正常之后,Eureka Server 自动退出自我保护模式

7、Eureka Client 定时全量或者增量从注册中心获取服务注册表,并且将获取到的信息缓存到本地

8、服务调用时,Eureka Client 会先从本地缓存找寻调取的服务。如果获取不到,先从注册中心刷新注册表,再同步到本地缓存

9、Eureka Client 获取到目标服务器信息,发起服务调用

10、Eureka Client 程序关闭时向 Eureka Server 发送取消请求,Eureka Server 将实例从注册表中删除

Eureka 集群是怎么保持数据一致的

从两个方面考虑:
1 Eureka 是弱数据一致性
2 Eureka是如何同步数据的

首先明确的是Eureka 是若数据一致性的
CAP 理论:
Consistency 数据一致性:
分布式系统中,数据会存在多个副本中,有一些问题会导致写入数据时,一部分副本成功、一部分副本失败,造成数据不一致。满足一致性就要求对数据的更新操作成功后,多副本的数据必须保持一致。

Availiability可用性:
在任何时候客户端对集群进行读写操作时,请求能够正常响应。

PartitionTolerance 分区容错性:
发生通信故障时,集群被分割为多个无法通信的分区时,集群仍然可用。

ZooKeeper 选择了C,就是尽可能的保证数据一致性,某些情况下可以牺牲可用性
Eureka 选择了A,所以Eureka 具有高可用,在任何时候,服务消费者能正常获取服务列表,但不能保证数据的强一致性,消费者可能会拿到过期的服务列表

Eureka 的数据同步方式:
1 复制方式:
分布式系统的数据在多个副本质检的复制方式,主要有:
主从复制:
就是 Master-Slave 模式,有一个主副本,其他为从副本,所有写操作都提交到主副本,再由主副本更新到其他从副本。
写压力都集中在主副本上,是系统的瓶颈,从副本可以分担读请求

对等复制:
就是 Peer to Peer 模式,副本间不分主从,任何副本都可以接收写操作,然后每个副本间互相进行数据更新。
对等复制模式,任何副本都可以接收写请求不存在写压力瓶颈,但各个副本间数据同步时可能产生数据冲突

Eureka 采用的就是Peer to Peer 模式

2 同步过程:
Eureka Server 启动后,会通过EurekaClient 请求 其他Eureka Server 节点中的一个节点,获取注册的服务信息,然后复制到其他peer节点。

Eureka Server 每当自己的信息变更后,例如Client 向自己发起注册,续约,注销请求,就会把自己的最新信息通知给其他Eureka Server,保持数据同步
在这里插入图片描述
如果自己的信息变更是另一个Eureka Server 同步过来的,这是再同步回去的话就会出现数据同步死循环了。
在这里插入图片描述

Eureka Server 在执行复制操作的时候,使用 HEADER_REPLICATION 这个 http header 来区分普通应用实例的正常请求,说明这是一个复制请求,这样其他 peer 节点收到请求时,就不会再对其进行复制操作,从而避免死循环。

还有一个问题,就是数据冲突,比如 server A 向 server B 发起同步请求,如果 A 的数据比 B 的还旧,B 不可能接受 A 的数据,那么 B 是如何知道 A 的数据是旧的呢?这时 A 又应该怎么办呢?

数据的新旧一般是通过版本号来定义的,Eureka 是通过 lastDirtyTimestamp 这个类似版本号的属性来实现的

lastDirtyTimestamp 是注册中心里面服务实例的一个属性,表示此服务实例最近一次变更时间
在这里插入图片描述

还有一个重要机制: heartbeat 心跳,既续约操作,来进行数据的最终修复,因为节点间的复制可能会出错,通过心跳就可以发现错误,进行弥补。

小结

  1. Eureka 是弱数据一致性,选择了 CAP 中的 AP。
  2. Eureka 采用 Peer to Peer 模式进行数据复制。
  3. Eureka 通过 lastDirtyTimestamp 来解决复制冲突。
  4. Eureka 通过心跳机制实现数据修复。

Eureka Client 注册一个实例为什么这么慢

1 Eureka Client一启动(不是启动完成),不是立即向Eureka Server注册,它有一个延迟向服务端注册的时间,通过跟踪源码,可以发现默认的延迟时间为40秒,源码在eureka-client-1.6.2.jar的DefaultEurekaClientConfig类下,代码如下:

    public int getInitialInstanceInfoReplicationIntervalSeconds() {
        return configInstance.getIntProperty(
            namespace + INITIAL_REGISTRATION_REPLICATION_DELAY_KEY, 40).get();
     }

解决方案:
将实例信息变更同步到 Eureka Server的初始延迟时间,从默认的40秒修改到10秒,即修改或添加以下配置:

eureka:
   client:
     ## InstanceInfoReplicator 将实例信息变更同步到 Eureka Server的初始延迟时间 ,默认为40秒
     initial-instance-info-replication-interval-seconds: 10

2 Eureka Server的响应缓存
Eureka Server维护每30秒更新的响应缓存,可通过更改配置eureka.server.responseCacheUpdateIntervalMs来修改。 所以即使实例刚刚注册,它也不会出现在调用/ eureka / apps REST端点的结果中。

3 Eureka Server刷新缓存
Eureka客户端保留注册表信息的缓存。 该缓存每30秒更新一次(如前所述)。 因 此,客户端决定刷新其本地缓存并发现其他新注册的实例可能需要30秒。

解决方案:

可以根据情况考虑在Eureka-Server服务中关闭readOnlyCacheMap,即修改或添加以下配置:

eureka:
    server:
         use-read-only-response-cache: false
   或者调整 readWriteCacheMap 的过期时间,即修改或添加以下配置:
eureka:
   server:
     response-cache-auto-expiration-in-seconds: 60

4 LoadBalancer Refresh
Ribbon的负载平衡器从本地的Eureka Client获取服务注册列表信息。Ribbon本身还维护本地缓存,以避免为每个请求调用本地客户端。 此缓存每30秒刷新一次(可由ribbon.ServerListRefreshInterval配置)。 所以,可能需要30多秒才能使用新注册的实例。

解决方案:

   在测试环境下,可以在Eureka-Client服务中适当提高 Eureka-Client端拉取 Server注册信息的频率,比如将默认频率由30秒改为5秒,即修改或添加以下配置:
eureka:
  client:
    registry-fetch-interval-seconds: 5

综上几个因素,一个新注册的实例,特别是启动较快的实例(默认延迟40秒注册),不能马上被Eureka Server发现。另外,刚注册的Eureka Client也不能立即被其他服务调用,因为调用方因为各种缓存没有及时的获取到新的注册列表

为什么服务下线了,Eureka Server 接口返回的信息还是存在?

1、Eureka_Client应用实例异常挂掉了,但是没有能在挂掉前告知Eureka-Server服务,所以Eureka-Server服务没有下线挂掉的Eureka-Client服务实例信息。解决该问题可以依赖Eureka-Server 的 EvictionTask 去剔除已下线的Eureka-client服务实例信息。

解决方案:

   可以在Eureka-Server服务中调整EvictionTask的调度频率,比如将调度间隔从默认的60秒,调整为5秒,即添加以下配置:
eureka:
  server:
     eviction-interval-timer-in-ms: 5000 

2.Eureka-Client应用实例下线时告知Eureka-Server了,但是 Eureka-Server 的 REST API 有 response cache 缓存,所以需要等待缓存过期后才能更新。

解决方案:

可以根据情况考虑在Eureka-Server服务中关闭readOnlyCacheMap,即修改或添加以下配置:

eureka:
    server:
         use-read-only-response-cache: false
   或者调整 readWriteCacheMap 的过期时间,即修改或添加以下配置:
eureka:
   server:
     response-cache-auto-expiration-in-seconds: 60

3.Eureka-Server 服务由于开启了Self Preservation 模式(自我保护模式),导致注册列表(registry)的信息不会因为过期而被剔除,直到退出自我保护模式(Self Preservation)。
解决方案:

   在测试环境中可在Eureka-Server服务中关闭自我保护模式,即修改或添加以下配置:
eureka:
   server:
      enable-self-preservation: false
   在生产环境下可以在Eureka-Server服务中把leaseRenewalIntervalInSeconds 和 renewal-percent-threshold 参数调小,从而提高触发自我保护机制的门槛,即修改或添加以下配置:
eureka:
  server:
      renewal-percent-threshold: 0.49  ## 指定每分钟需要收到的续约次数的阀值,默认值为0.85  
   以及
eureka:
   instance:
     leaseRenewalIntervalInSeconds: 10  # 默认值为30 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值