RabbitMQ集群/监控/Federation/Shovel

RabbitMQ运维

集群搭建

单台RabbitMQ服务器可以满足每秒1000条的消息吞吐量
RabbitMQ集群不能保证消息的万无一失,即使将消息、队列、交换器等都设置为可持久化,生产端和消费端都正确地使用了确认方式。
集群中所有节点都会备份所有的元数据信息,包括:队列、交换器、绑定关系、vhost元数据,但不会备份消息

基于存储空间和性能的考虑,在RabbitMQ集群中创建队列,集群只会在单个节点而不是所有节点上创建队列的进程并包含完整的队列信息(元数据、状态、内容)。这样只有队列的宿主节点,即所有者节点知道队列的所有信息,所有其他非所有者节点只知道队列的元数据和指向该队列存在的那个节点的指针。因此,在集群节点崩溃时,该节点的队列进程和关联的绑定都会消失。附加在那些队列上的消费都也会丢失所订阅的信息,并且任何匹配该队列绑定信息的新消息也都会消失

多机多节点配置

RabbitMQ集群对延迟非常敏感,应当只在本地局域网内使用,在广域网中不应该使用集群,而应该用Federation或者Shovel代替

0、配置主机名
hostname name1 #临时修改
hostnamectl set-hostname name1 #永久修改
1、配置各节点的hosts文件,让节点能互相识别对方的存在
192.168.0.2 node1
192.168.0.3 node2
192.168.0.4 node3
2、编辑RabbitMQ的cookie文件,确保各个节点的cookie文件使用的是同一个值
.erlang.cookie文件位置:rpm安装:/var/lib/rabbitmq/.erlang.cookie 编译安装/root/.erlang.cookie或者$HOME/.erlang.cookie
cookie相当于密钥令牌,集群中RabbitMQ节点需要通过交换密钥令牌以获得相互认证
3、配置集群。三种方式:rabbitmqctl方式;rabbitmq.config方式;rabbitmq-autocluster插件方式
启动node1、node2、node3节点的RabbitMQ服务
rabbitmq-server -detached
这样,三个节点都以独立节点存在的单个集群,通过rabbitmqctl cluster_status查看各节点状态
以node1为基准,将node2、node3节点加入node1的集群中,三个节点是平等的
将node2节点加入node1:
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl join_cluster rabbit@node1
rabbitmqctl start_app
node3加入集群命令相同,加入后,通过rabbitmqctl cluster_status查看状态

通过rabbitmqctl stop_app可以关闭某个节点。如果关闭了集群中的所有节点,则需要确保在启动时第一个启动最后关闭那个节点
若第一个启动的不是最后关闭的节点,那么这个节点会等待最后关闭的节点启动,默认30秒,重试10次,如果没有等到,这个先启动的节点会失败
若最后关闭的节点不能启动,可以通过rabbitmqctl forget_cluster_node命令将此节点剔出当前集群
rabbitmqctl force_boot可以强制启动一个节点:
rabbitmqctl force_boot
rabbitmq-server -detached

集群节点类型

执行rabbitmqctl cluster_status,返回的第一行中[{nodes,[{disc,[rabbit@localhost]}]},其中disc标注了节点的类型
节点类型分为内存节点和磁盘节点。内存节点将所有队列、交换器、绑定关系、用户、权限和vhost元数据定义存储在内存中,磁盘节点存在磁盘中
集群中,可以选择配置部分节点为内存节点,可以获得更高的性能
#将node2加入node1时,指定node2为内存节点
rabbitmqctl join_cluster rabbit@node1 --ram
若集群已搭建好,可以切换节点类型
格式:rabbitmqctl change_cluster_node_type {disc,ram}
#将node2节点由内存转变为磁盘
rabbitmqctl stop_app
rabbitmqctl change_cluster_type disc
rabbitmqctl start_app
rabbitmqctl cluster_status
在集群中创建队列、交换器或绑定关系时,直到所有节点都提交成功元数据变更后,才会返回
RabbitMQ只要求在集群中至少有一个磁盘节点
如果集群只有一个磁盘节点,凑巧它崩溃了,那么集群可以继续发送或接收消息,但不能执行创建队列、交换器、绑定关系、用户,以及更改权限、添加或删除集群节点的操作。所以在建立集群时,至少要保证有两个以上的磁盘节点
内存节点唯一存到磁盘的元数据是集群中磁盘节点的地址

剔除单个节点

方法一
如要剔除node2节点,则在node2上执行
rabbitmqctl stop_app 或rabbitmqctl stop
再在node1或node3上执行
rabbitmqctl forget_cluster_node rabbit@node2

集群关闭顺序node3、node2、node1,若要剔出node1,则可以在node2上执行
rabbitmqctl forget_cluster_node rabbit@node1 --offline
rabbitmq-server -detached

方法二
在node2上执行rabbitmqctl reset
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl start_app
当重设的节点是集群的一部分时,该命令会和集群中磁盘节点通信,告诉它们该节点离开集群,不然集群会以为该节点出了故障

集群节点的升级

节点升级仅需保留Mnesia数据即可

集群升级:
1、关闭所有节点的服务,rabbitmqctl stop
2、保存各个节点的Mnesia数据
3、解压新版本的RabbitMQ到指定目录
4、指定新版本的Mnesia路径为步骤2保存的Mnesia数据路径RABBITMQ_NMESIA_BASE=/opt/mnesia
5、启动新版本的服务,注意先启动最后关闭的那个节点

单机多节点配置

在一台机器上部署多个RabbitMQ服务节点,需要确保每个节点都有独立的名称、数据存储位置、端口号(包括插件的端口号)

在主机node1上部署rabbit1@localhost、rabbit2@localhost、rabbit3@localhost
RABBITMQ_NODE_PORT=5672 RABBITMQ_NODENAME=rabbit1 rabbitmq-server -detached
RABBITMQ_NODE_PORT=5673 RABBITMQ_NODENAME=rabbit2 rabbitmq-server -detached
RABBITMQ_NODE_PORT=5674 RABBITMQ_NODENAME=rabbit3 rabbitmq-server -detached
若开启了管理插件,可以这样启动
RABBITMQ_NODE_PORT=5672 RABBITMQ_NODENAME=rabbit1 RABBITMQ_SERVER_START_ARGS="-rabbitmq_management listener [{port,15672}]" rabbitmq-server -detached
RABBITMQ_NODE_PORT=5673 RABBITMQ_NODENAME=rabbit2 RABBITMQ_SERVER_START_ARGS="-rabbitmq_management listener [{port,15673}]" rabbitmq-server -detached
RABBITMQ_NODE_PORT=5674 RABBITMQ_NODENAME=rabbit3 RABBITMQ_SERVER_START_ARGS="-rabbitmq_management listener [{port,15674}]" rabbitmq-server -detached
将rabbit2@localhost、rabbit3@localhost加入rabbit1@localhost
rabbitmqctl -n rabbit2@localhost stop_app
rabbitmqctl -n rabbit2@localhost reset
rabbitmqctl -n rabbit2@localhost join_cluster rabbit1@localhost
rabbitmqctl -n rabbit2@localhost start_app
rabbitmqctl -n rabbit2@localhost cluster_status
单机多节点配置通常用于实验性认证rabbitmq-plugins -n rabbit2@localhost enable rabbitmq_management
rabbitmqctl -n rabbit2@localhost add_user admin admin
rabbitmqctl -n rabbit2@localhost set_user_tags admin administrator

查看服务日志

日志RABBITMQ_NODENAME-sasl.log和RABBITMQ_NODENAME.log
RABBITMQ_NODENAME-sasl.log日志中可以找到Erlang的崩溃报告,可以调试无法启动的MQ节点
RABBITMQ_NODENAME.log日志记录应用服务的日志

rabbitmq-server -detached 启动Erlang虚拟机和MQ应用服务
rabbitmqctl start_app 启动MQ应用服务
rabbitmqctl stop 关闭Erlang虚拟机和MQ应用服务
rabbitmqctl stop_app 关闭MQ应用服务

rabbitmqctl rotate_logs .bak 手动换换日志

amq.rabbitmq.log交换器是用来收集RabbitMQ日志的,集群中所有服务日志都会发往这个交换器中。该交换器类型为topic,可以收集debug/info/warning/error这4个级别的日志。创建四个队列queue.debug、queue.info、queue.warning、queue.error,分别采用路由键debug/info/warning/error。如果希望一个队列收集所有级别的日志,可以使用#这个路由键

单节点故障恢复

配置数据节点冗余(镜像队列)可以有效防止由于单点故障影响集群的可用性、可靠性

单节点故障包括:机器硬件故障、机器掉电、网络异常、服务进程异常

1、机器硬件故障后,不法从软件角度恢复,此时可在集群其他节点中执行rabbitmqctl forget_cluster_node {nodename}剔除,将故障节点的IP地址从连接列表里删除,并让客户端重新与集群节点建立连接
2、机器掉电,电力恢复后,不能盲目重启服务,可能引起网络分区。在其它节点执行rabbitmqctl forget_cluster_node {nodename},然后删除故障机吕的Mnesia数据,再重启,最后再将节点以新节点的方式加入到集群中

集群迁移

扩容进入集群的机器是没有队列创建的,只有后面新创建的队列才有可能进入这个节点。如果配置了镜像队列,可以将原先队列漂移到这个新的节点中

RabbitMQ集群迁移包括元数据重建、数据迁移,以及与客户端连接的切换
元数据重建是指在新的集群中创建原集群的队列、交换器、绑定关系、vhost、用户、权限和Parameter。通过Web管理界面可以重建,Overview->Export definitions。导入时,如果新集群中对于交换器、队列、绑定关系这类非可变对象相冲突时,会报错。
重建元数据的意义在于重建队列、交换器和绑定关系,因此在版本不兼容时,可以只处理这部分数据,手动修改json文件
如果采用上面的方式将元数据在新集群上重建,所有队列都只会落到同一个节点上,其他节点处理空置状态。处理方法一是通过HTTP API接口创建相应的数据;另一种是通过程序或者脚本在新集群上建立元数据

数据迁移和客户端连接的切换

将生产者与MQ集群的连接断开,然后再与新的集群建立新的连接
对消费者,一是等待原集群中的消息全部消费完再断开连接,然后与新集群建立连接进行消费作业。查看命令
rabbitmqctl list_queues name messages messages_ready messages_unacknowledged
二是数据迁移,原理是先从原集群将数据消费出来,然后存入一个缓存区中,另一个线程读缓存区中的消息再发布到新的集群中
RabbitMQ本身提供的Federation和Shovel插件可以实现RabbitMQ ForwardMaker的功能

自动化迁移

元数据保存到zookeeper中,生产者和消费者从zookeeper中获取连接信息

查看队列中是否有未被消费完的消息
rabbitmqctl list_queues -p / -q | awk '{if($2>0) print $0}'

集群监控

通过HTTP API接口提供监控数据

集群节点信息可以通过/api/nodes接口获取。定时调用API接口获取JSON数据,然后解析数据进行持久化处理,比如通过OpenTSDB进行存储。监控系统可以根据用户检索条件从OpenTSDB中获取相应数据并展示到页面中

交换器的数据可以从/api/exchanges/vhost/name获取
队列的数据可以从/api/queues/vhost/name获取

通过客户端提供监控数据

Java客户端、其他客户端获取数据

检测RabbitMQ服务是否健康

使用AMQP协议来构建一个类似于TCP协议中Ping的检测程序

通过API来判断
1、创建一个以aliveness-test为名称的队列来接收测试消息
2、用队列名称,即aliveness-test作为消息路由键,将消息发往默认交换器
3、到达队列时就消费该消息,否则就报错

/api/healthchecks/node
/api/healthchecks/node/node

元数据管理与监控

删除一个队列或交换器、或者修改了绑定信息、胡乱建立了一个队列绑定到现有交换器、没有消费者订阅消费此队列导致消息堆积

提供给业务方使用的用户只有可读和可写权限,收回配置权限

/api/definitions获取集群元数据令牌

动维涵盖:容量评估、资源分配、集群管控、系统调优、升级扩容、故障修复、监控告警、负载均衡

跨越集群的界限

RabbitMQ可以通过3种方式实现分布式部署:集群、Federation、Shovel。Federation和Shovel可以为RabbitMQ的分布式部署提供更高的灵活性,但也提高了部署的复杂性

Federation

Federation插件使RabbitMQ在不同Broker节点间进行消息传递而无须建立集群:
在不同管理域(不同的用户和vhost、不同版本的RabbitMQ)中的Broker或集群间传递消息
基于AMQP 0-9-1协议在不同的Broker之间通信,能容忍不稳定的网络连接情况
一个Broker节点中可以同时存在联邦交换器(或队列)或者本地交换器(或队列),只需对特定交换器(或队列)创建Federation连接

Federation插件可以让多个交换器或者多个队列进行联邦
一个联邦交换器federated exchange或者一个联邦队列federated queue接收上游upstream的消息,这里的上游指的是其他Broker上的交换器或者队列
联邦交换器能够将原本发送给上游交换器的消息路由到本地的某个队列中;联邦队列则允许本地消费者接收来自上游队列的消息

联邦交换器

解决异地的问题,生产者和消费者可以异地部署而感受不到过多的差异
一条Federation link是单向的

broker1->broker3的单向Federation link建立过程:
1、在broker1上创建同名(可配置,默认同名)交换器exchangeA
2、在broker1上建立内部交换器exchangeA->broker3 B,通过路由键rkA与第一步的exchangeA绑定;#broker3是集群名称
3、在broker1上建立队列federation:exchangeA->broker3 B,并与上步的交换器绑定
4、通过AMQP将broker1上的队列中的消息消费到borcker3中的exchangeA中

经过Federation link转发的消息会带有特殊的headers属性标记

默认交换器(没有名称的交换器)和内部交换器不能对其使用Federation功能
联邦交换器可以部署成多叉树、三角形、环形等拓扑

联邦队列

联邦队列可以在多个Broker节点(或集群)之间为单个队列提供均衡负载的功能。一个联邦队列可以连接一个或多个上游队列upstream queue

brocker2的消费者先消费broker2中的消息,直到消费完,才会到brocker1上去拉消息。消费者既可以消费broker2中的队列,又可以消费broker1中的队列,从而可以提升单个队列的容量,并可在一定程度上做到负载均衡

一条消息可以在联邦队列间转发无限次。互为联邦队列可以让有多余消费能力的消费者在brocker1和brocker2中切换
对于联邦队列只能使用Basic.Consume进行消费,而不能用Basic.Get,因为Basic.Get是异步的方法
联邦队列不具备传递性,不能级联
理论上可以将federated queue与federated exchange绑定使用,但这会导致不可预测的结果,若对结果评估不足,慎用这种搭配方式

Federation的使用

(1)配置一个或多个upstream,可通过运行时参数或federation management插件来完成
(2)定义匹配交换器或队列的一种/多种策略policy

启用federation插件
rabbitmq-server -detached
rabbitmq-plugins enable rabbitmq_federation
启用federation管理插件
rabbitmq-plugins enable rabbitmq_federation_management

需要在集群中使用Federation功能时,集群中所有节点都应该开启Federation插件

Federation中存在3种级别的配置
1)upstreams,定义与其他broker建立连接的信息
2)upstream sets,对一系列使用Federation功能的upstream进行分组
3)policies,每个policy会选定一组交换器,或队列,或两者进行限定,进而作用于一个单独的upsteam或upsteam set上

默认有一种名为all的upstream set,为隐式定义。upstreams和upstream sets属于运行时参数。Federation相关的运行时参数和策略都可以通过这3种方式进行设置:
通过rabbitmqctl;通过HTTPAPI接口;通过rabbitmq_federation_management的Web管理界面。基于Web管理界面的方式不能提供全部功能,如无法对upstream set进行管理


部署步骤:
#依赖
yum install openssl
#安装
rpm -ivh erlang-20.3-1.el7.centos.x86_64.rpm
rpm -ivh socat-1.7.3.2-2.el7.x86_64.rpm
rpm -ivh rabbitmq-server-3.7.4-1.el7.noarch.rpm
#启用
systemctl start rabbitmq-server
rabbitmq-plugins enable rabbitmq_management
rabbitmqctl add_user admin admin
rabbitmqctl set_user_tags admin administrator
1、启用插件
在broker1和broker3中开启rabbitmq_federation插件,最好同时开启rabbitmq_federation_management插件
rabbitmq-plugins enable rabbitmq_federation
rabbitmq-plugins enable rabbitmq_federation_management
2、定义upstream
在broker3中定义一个upstream
#ack-mode可以是no-ack,on-publish,on-confirm
方法1:rabbitmqctl set_parameter federation-upstream f1 '{"uri":"amqp://admin:admin@192.168.158.100:5672","ack-mode":"on-confirm"}'
方法2:curl -i -u admin:admin -XPUT -d'{"value":{"uri":"amqp://admin:admin@192.168.158.100:5672","ack-mode":"on-confirm"}}' http://192.168.158.139:15672/api/parameters/federation-upstream/%2F/f1
方法3:在Web上操作
参数说明:
Name,名称
URI,uri,定义AMQP连接
Prefetch count,prefetch_count,Federation内部缓存消息条数,收到上游消息之后且发到下游之前缓存的消息条数
Reconnect delay,reconnect-delay,若连接断开后,需等待多少秒开始重新建立连接
Acknowledgement Mode,ack-mode,消息确认方式,on-confirm(默认),on-publish,no-ack。on-confirm接收到下游确认消息后再向上游发送消息确认,可以确保网络失败或Broker宕机时不丢失消息,但处理速度最慢;on-publish消息发送到下游后,再向上游发送消息确认,可以确保网络失败时不丢失消息,不能确保Broker宕机时的消息丢失;no-ack无须进行消息确认,速度最快,最容易丢失消息
Trust User-ID,trust-user-id,为false时,忽略消息中user_id属性;为true时,只转发user_id为上游任意有效用户的消息
只适合联邦交换器的参数:
Exchange,exchange,指定upstream exchange名称,默认和federated exchange同名
Max hops,max-hops,消息被丢弃前最大跳转次数,默认为1
Expires,expires,连接断开后,上游队列的超时时间,默认为none,表示不删除,单位为ms。相当于设置队列的x-expires参数,设置该值可以避免连接断开后,生产者一直向上游队列发送消息,造成上游大量消息堆积
Message TTL,message-ttl,上游队列的x-message-ttl参数,默认为none,表示没有超时时间
HA Policy,ha-policy,上游队列的x-ha-policy参数,默认为none,表示队列中没有任何HA
只适合联邦队列的参数
Queue,queue,执行upstream queue的名称,默认和federated queue同名

3、定义policy
定义Policy用于匹配交换器exchangeA,并使用第二步所创建的upstream
方法1:rabbitmqctl set_policy --apply-to exchanges p1 "^exchange" '{"federation-upstream":"f1"}'
方法2:curl -i -u admin:admin -XPUT -d'{"pattern":"^exchange","definition":{"federati},"apply-to":"exchanges"}' http://192.168.158.139:15672/api/policies/%2F/p1
方法3:在Web上操作
4、查看状态
rabbitmqctl eval 'rabbit_federation_status:status().'
定义联邦队列
先定义一个upstream,之后在定义policy时,将apply to指定为queues即可,如
rabbitmqctl set_policy --apply-to queues p2 "^queue" '{"federation-upstream":"f1"}'

Shovel

和Federation的数据转发功能类似,Shovel能可靠、持续地从一个Broker的队列(源,source)拉取数据至另一个Broker中的交换器(目的,destination)。源和目的可以是同一个Broker
Shovel优势:
松耦合,解决不同Broker、集群、用户、vhost、MQ和Erlang版本移动消息
支持广域网,可以容忍糟糕的网络,能保证消息的可靠性
高度定制,当Shovel成功连接后,可以配置

Shovel的原理

若在配置Shovel link时设置了add_forward_headers参数为true,则最后消息会有特殊headers属性标记

通常源为队列,目的为交换器,但是,也可以源为队列,目的为队列。实际也是由交换器转发,只不过这个交换器是默认交换器

配置交换器做为源也是可行的。实际上会在源端自动新建一个队列,消息先存在这个队列,再被Shovel移走

源和目的端的交换器和队列都可以在建立Shovel之后再创建。Shovel可以为源端或目的端配置多个Broker地址,这样可以在一个连接失败后随机挑选其他地址建立连接

Shovel的使用

#启用插件
rabbitmq-plugins enable rabbitmq_shovel
#启用管理插件
rabbitmq-plugins enable rabbitmq_shovel_management

Shovel既可以部署在源端,也可以部署在目的端。部署方式有静态方式static和动态方式dynamic,静态方式指在rabbitmq.config配置文件中设置,动态方式指运行时Parameter

1、静态方式

单条Shovel条目构成(shovels部分的下一层)
{rabbitmq_shovel,[{shovels,[{shovel_name,[...]},...]} ]}
其中shovel_name名称需要唯一,每条shovel定义如下:

sources、destination和queue必需,其余都可以默认

配置示例:

其中,broker配置URI。若源或目标是集群,可以使用brokers,内容为列表,如
{brokers,["amqp://root:root@192.168.0.2:5672","amqp://root:root@192.168.0.3:5672"]}
当一个节点故障时,可以转移到另一个节点上
声明declarations是可选的
queue表示源端的队列名称,可以设置为<<>>表示匿名队列,队列名称由MQ自动生成
prefetch_count表示Shovel内部缓存的消息条数
ack_mode表示在完成转发时的确认模式,有no_ack、on_publish、on_confirm。官方推荐on_confirm,即使用publisher confirm机制,收到目的端的消息确认后,再向源发送消息确认
publish_properties消息发往目的端时需要特别设置的属性列表。默认被转发的消息的各属性是被保留的,此处设置的值会覆盖原来的值。属性列表有:content_type、content_encoding、headers、delivery_mode、priority、correlation_id、reply_to、expiration、message_id、timestamp、type、user_id、app_id、cluster_id
add_forward_headers若设置为true,会在转发的消息内添加x-shovelled的header属性
publish_fields定义消息发往目的端交换器的路由键。若交换器和路由键没有定义,则Shovel会从原始消息上复制
reconnect_delay在连接失效后,重建连接前的等待时间,单位为秒,若设置为0,则不重连,默认为5秒

2、动态方式

动态配置的信息会存入Mnesia数据库中
Shovel仅在一端启用即可
#源192.168.158.100,目的192.168.158.139,源queue:queue1,目的交换器:
方式一:rabbitmqctl set_parameter shovel hidden_shovel '{"src-uri":"amqp://admin:admin@192.168.158.100:5672","src-queue":"queue1","dest-uri":"amqp://admin:admin@192.168.158.139:5672","src-exchange-key":"rk2","prefetch-count":64,"reconnect-delay":5,"publish-properties":[],"add-forward-headers":true,"ack-mode":"on-confirm"}'
方式二:curl -i -u admin:admin -XPUT -d'{"value":{"src-uri":"amqp://admin:admin@192.168.158.100:5672","src-queue":"queue1","dest-uri":"amqp://admin:admin@192.168.158.139:5672","src-exchange-key":"rk2","prefetch-count":64,"reconnect-delay":5,"publish-properties":[],"add-forward-headers":true,"ack-mode":"on-confirm"}}' http://192.168.158.139:15672/api/parameters/shovel/%2F/hidden_shovel
方式三:Web界面

查看状态
rabbitmqctl eval 'rabbit_shovel_status:status().'

案例:消息堆积的治理

单个队列中堆积10万条消息不会有丝毫影响,但超过1千万时,将引起严重问题,如内存、磁盘告警、connection阻塞等

可以使用Shovel,当某个队列消息堆积严重时,可以使用Shovel将队列中消息移交给另一个集群

1、当检测到集群cluster1中队列queue1中有严重消息堆积,如通过/api/queues/vhost/name获取消息个数messages超过2千万或者消息占用大小messages_bytes超过10GB时,启用shovel1将queue1中消息转发至备份集群cluster2中队列queue2
2、当检测到queue1中消息个数低于1百万或者消息大小低于1GB时,停止shovel1,让原本队列消化剩余堆积
3、当检测到queue1消息个数低于10万个或占用大小低于100MB时,开启shovel2将queue2队列中暂存的消息返还队列queue1
4、当检测到queue1消息个数超过1百万,或消息占用高于1GB时,将shovel2停掉

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值