Ceph Async RDMA网络通信性能优化

Ceph Async RDMA网络性能优化

济南友泉软件有限公司

 

目录

一、         现有代码分析

1.1.       Reactor模型

1.1.1.    Reactor模型的优点

1.1.2.    Reactor模型的优点.

1.2.       有限状态机模型

1.3.       Async模块

1.2.1.    Async工作原理

1.2.2.    Async主要组件

1.4.       RDMA

1.3.1.    RDMA三种不同的硬件实现

1.3.2.    相关组件

二、         性能优化工作汇总

2.1        RDMA网络通信配置

2.2        RoCE网络通信的实现

2.3        性能测试平台开发

2.2.1    aysnc_server/async_client

2.2.2    ceph_perf_msgr_server/ceph_perf_msgr_client

2.4        QueuePair发送队列

2.5        TCMalloc优化内存分配

2.6        工作线程数调优

2.7        多线程Reactor模型

2.8        消息接收缓存

附录(Ⅰ):连接过程状态迁移图

参阅资料

 

网络通信模块的实现在源代码src/msg的目录下,该目录主要包括Messenger、Connection、Message、Dispatch等类,这些类定义了网络通信的框架与接口。三个子目录simple、async、xio分别对应三种不同的网络通信模型。simple、xio在最新的版本中已经被废弃,async是目前系统默认的网络通信方式。因此,本次网络通信优化的工作主要在async基础之上开展。

Reactor模型

为了处理高并发的网络I/O流,async模块采用了Reactor模型。在Reactor中,每一种handler会出处理一种event。这里会有一个全局的管理者selector,我们需要把channel注册感兴趣的事件,那么这个selector就会不断在channel上检测是否有该类型的事件发生,如果没有,那么主线程就会被阻塞,否则就会调用相应的事件处理函数即handler来处理。

Reactor模型原理

Reactor模型主要组件

  1. Reactor模型的优点

响应快,不必为单个同步时间所阻塞,虽然Reactor本身依然是同步的;编程相对简单,可以最大程度的避免复杂的多线程及同步问题,并且避免了多线程/进程的切换开销; 可以方便的通过增加Reactor实例个数来充分利用CPU资源;reactor框架本身与具体事件处理逻辑无关,具有很高的复用性;

  1. Reactor模型的缺点

相比传统的简单模型,Reactor增加了一定的复杂性,因而有一定的门槛,并且不易于调试; Reactor模式需要底层的Synchronous Event Demultiplexer支持,比如Java中的Selector支持,操作系统的select系统调用支持,如果要自己实现Synchronous Event Demultiplexer可能不会有那么高效;Reactor模式在IO读写数据时还是在同一个线程中实现的,即使使用多个Reactor机制的情况下,那些共享一个Reactor的Channel如果出现一个长时间的数据读写,会影响这个Reactor中其他Channel的相应时间,比如在大文件传输时,IO操作就会影响其他Client的相应时间,因而对这种操作,使用传统的Thread-Per-Connection或许是一个更好的选择,或则此时使用Proactor模式。

  1. 有限状态机模型

有限状态机(Finite State Machine, FSM),是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型

FSM模型把模型的多状态、多状态间的转换条件解耦;可以使维护变得容易,代码也更加具有可读性。

AsyncConnection连接建立过程中地状态迁移图参阅附录(Ⅰ)

 

  1. Async模块
  1. Async工作原理

  1. Async主要组件

AsyncMessenger

管理网络连接

AsyncConnection

网路通信连接,定义网络通信应用层协议

NetworkStack

管理Worker对象及其对应地线程

Worker

网络I/O流处理单元,每个Worker对应一个工作线程

ServerSocket/ServerSocketImpl

C/S模式监听套接字,向上屏蔽了各种不同的网络编程接口

ConnectedSocket/ConnectedSocketImpl

C/S模式连接套接字,向上屏蔽了各种不同的网络编程接口

EventCenter

事件分发器,负责事件注册、事件分发

EventCallback

当对应的事件发生时,由EventCenter负责回调

EventEpoll

对epoll进行封装,轮询网络I/O事件

 

  1. RDMA

RDMA是Remote Direct Memory Access的缩写,通俗的说可以看成是远程的DMA技术,为了解决网络传输中服务器端数据处理的延迟而产生的。

                         RDMA工作原理

  1. RDMA三种不同的硬件实现

目前,有三种RDMA协议的实现:Infiniband、RoCE、iWARP。由于RoCE具备明显性能和成本优势,将逐渐成为市场主流。

软件栈对比

 

 

Infiniband (IB)

iWARP

RoCE

标准组织

IBTA

IETF

IBTA

性能

最好

稍差

与IB相当

成本

网卡厂商

Mellanox

Chelsio

Mellanox

Emulex

性能、成本对比

 

Infiniband网络最好,但网卡和交换机是价格也很高,然而RoCEv2和iWARP仅需使用特殊的网卡就可以了,价格也相对便宜很多。

  1. 相关组件

Device/DeviceList

抽象RDMA网卡,根据icfs.conf配置网卡参数

Infiniband

封装IB Verbs网络编程接口及组件

RDMAConnectedSocketImpl

仿socket连接套接字,采用伪fd实现网络I/O流的数据读写

RDMAConnTCP

为RDMAConnectedSocketImpl服务,利用利用TCP/IP协议建立RDMA连接

RDMAServerSocketImpl

仿socket服务套接字,定义服务接口

RDMAServerConnTCP

实现RDMAServerSocketImpl接口,利用TCP/IP协议建立RDMA连接

 

RDMADispatcher

轮询RDMA网络I/O流可读事件,将网络I/O流可读数据分发到对应RDMAConnectedSocketImpl

轮询RDMA网络I/O流可写事件,将网络I/O流可写数据分发到某个RDMAWorker

RDMAWorker

网络I/O流处理单元,每个RDMAWorker对应一个工作线程

RDMAStack

管理RDMAWorker对象及其对应地线程

 

 

 

  1. RDMA网络通信配置

在安装完网卡及其驱动之后,需要启动openibd,运行以下命令

service openibd start

chkconfig openibd on

对于IB网络,还需要启动opensmd,

service opensmd start

chkconfig opensmd on

网络启动之后,通过ibstat可以查看当前网络设备状态,

[root@server42 ~]# ibstat

CA 'mlx5_0'

     CA type: MT4119

     Number of ports: 1

     Firmware version: 16.25.1020

     Hardware version: 0

     Node GUID: 0xb8599f0300bd417a

     System image GUID: 0xb8599f0300bd417a

     Port 1:

              State: Active

              Physical state: LinkUp

              Rate: 40

              Base lid: 0

              LMC: 0

              SM lid: 0

              Capability mask: 0x04010000

              Port GUID: 0xba599ffffebd417a

              Link layer: Ethernet

CA 'mlx5_1'

     CA type: MT4119

     Number of ports: 1

     Firmware version: 16.25.1020

     Hardware version: 0

     Node GUID: 0xb8599f0300bd417b

     System image GUID: 0xb8599f0300bd417a

     Port 1:

              State: Active

              Physical state: LinkUp

              Rate: 40

              Base lid: 0

              LMC: 0

              SM lid: 0

              Capability mask: 0x04010000

              Port GUID: 0xba599ffffebd417b

              Link layer: Ethernet

通过ib_send_bw、ib_send_lat等工具可以测试网络带宽、延迟等性能。

Async提供了posix、rdma两种底层网络通信的方式,为了使用RDMA协议实现高带宽、低延迟的网络通信,需要配置rdma网络及软件定义参数。

使用rdma verbs创建QueuePair时,需要通信双方rdma设备的硬件信息,通常利用TCP/IP完成rdma连接双方的硬件参数的交换,因此需要配置集群网段,即

public_network = 100.7.45.0/20

cluster_network = 188.188.44.0/20

Async模块默认采用TCP/IP协议进行网络通信,需要改成rdma协议

ms_type = async

ms_async_transport_type = rdma

RDMA Verbs API按照设备名对设备进行操作,为了兼容Linux操作系统的命名,需要进行设备网络名到设备名的转换,Mellanox驱动提供了以下命令用于获取设备名与网络名之间的映射关系:

[root@server42 ~]# ibdev2netdev

i40iw0 port 1 ==> eno1 (Up)

mlx5_0 port 1 ==> enp59s0f0 (Up)

mlx5_1 port 1 ==> enp59s0f1 (Up)

据此,可以在配置环境中设置网络通信设备的名称,即

ms_async_rdma_public_device_name = enp59s0f0

ms_async_rdma_cluster_device_name = enp59s0f1

  1. RoCE网络通信的实现

由于Infiniband与RoCE网络开发采用相同上层Verbs API,因此,IB网络通信代码可以完全在RoCE硬件上运行,整个代码几乎不需要改动。

  1. 性能测试平台开发

为了能够对网络模块通信性能及优化效果进行定性、定量地深入研究,需要一套相对独立地RDMA网络通信性能测试工具。

  1. aysnc_server/async_client

async_client向async_server发送MSG_DATA_PING类型地数据包,async_server当受到2000个数据包之后会自动关闭连接,async_client监测到async_server端关闭之后,async_client会停止发送数据包,同时输出网络通信性能地统计信息。

async_server命令参数

--addr X ip to listen

--port X port to bind

 

async_client命令参数:

--msgs X number of msg to transport

--dszie X size of each msg to transport

--addr X ip of the server

--port X port of the server

这种测试工具其实是利用async_server端连接关闭作为消息数据包发送结束的标志,因为async_client感知到async_server连接关闭需要一定的时间,从而导致不能够准确地测试网络性能。

  1. ceph_perf_msgr_server/ceph_perf_msgr_client

采用“请求-应答”模式,具体实现上与实际的OSD业务通信流程比较相似,因此可以较好的反映网络通信性能。

client向server端发送指定数量的MOSDOp消息,server端对于收到的每个MOSDOp消息,都会向client端发送MOSDOpReply消息。

但是,ceph_perf_msgr_client在ClientThread::entry()中存在一个Bug,即

    void *entry() {

      lock.Lock();

      for (int i = 0; i < ops; ++i) {

        if (inflight > uint64_t(concurrent)) {

          cond.Wait(lock);

        }

        MOSDOp *m = new MOSDOp(client_inc.read(), 0, oid, oloc, pgid, 0, 0, 0);

        m->write(0, msg_len, data);

        inflight++;

        conn->send_message(m);

        //cerr << __func__ << " send m=" << m << std::endl;

      }

由于调用write()函数之后,data内的数据会被清空,所以第一次调用之后,后面发送的数据包其实没有数据,需要改成

    void *entry() {

      lock.Lock();

      for (int i = 0; i < ops; ++i) {

        if (inflight > uint64_t(concurrent)) {

          cond.Wait(lock);

        }

        MOSDOp *m = new MOSDOp(client_inc.read(), 0, oid, oloc, pgid, 0, 0, 0);

        /*

        m->write(0, msg_len, data);

        */

        bufferlist msg_data(data);

        m->write(0, msg_len, msg_data);

 

        inflight++;

        conn->send_message(m);

        //cerr << __func__ << " send m=" << m << std::endl;

      }

      lock.Unlock();

      msgr->shutdown();

      return 0;

}

 

server 41

server 42

Client性能

Server配置
RoCE,event

Client
RoCE,event

并行数
worker threads

并行数
numjobs

队列深度
concurrency

request个数
ios

耗时(us)

IOPS

延时(us)

1

1

32

100K

4324564

23123.72

43.24564

1

2

32

100K

3464919

57721.41

34.64919

1

4

32

100K

4003939

99901.62

40.03939

1

8

32

100K

5313240

150567.3

53.1324

1

16

32

100K

11167830

143268.7

111.6783

1

32

32

100K

27079705

118169.7

270.7971

1

64

32

100K

68204271

93835.77

682.0427

1

64

64

100K

66653653

96018.74

666.5365

 

 

 

 

server 41

server 42

Client性能

Server配置
RoCE,polling

Client
RoCE,polling

并行数
worker threads

并行数
numjobs

队列深度
concurrency

request个数
ios

耗时(us)

IOPS

延时(us)

1

1

32

100K

4952843

20190.424

49.52843

1

2

32

100K

3712582

53870.864

37.12582

1

4

32

100K

3664009

109170.038

36.64009

1

8

32

100K

5526721

144751.291

55.26721

1

16

32

100K

11834255

135200.737

118.3426

1

32

32

100K

33805670

94658.6771

338.0567

1

64

32

100K

67214894

95216.9916

672.1489

1

64

64

100K

68273589

93740.4946

682.7359

 

从以上测试结果来看,主要有以下结论:

  • 无论采用polling还是event轮询模式,网络性能几乎一样。
  • 随着连接数的增大,网络性能逐渐达到性能瓶颈,最大IOPS为14万左右。
  • 当连接数增大到一定程度,IOPS维持在9万左右。
  1. QueuePair发送队列

通过读取ms_async_rdma_receive_buffer与ms_async_rdma_send_buffers来配置注册内存大小,在Device::create_queue_pair()中,会根据ms_async_rdma_send_buffers来创建QueuePair.,即

Infiniband::QueuePair* Device::create_queue_pair(IcfsContext *cct, ibv_qp_type type)

{

  Infiniband::QueuePair *qp = new QueuePair(

      cct, *this, type, active_port->get_port_num(), srq, rx_cq, rx_cq, max_send_wr, max_recv_wr);

 

  if (qp->init()) {

    delete qp;

    return NULL;

  }

  return qp;

}

 

但是ms_async_rdma_send_buffers设置较大会导致创建QueuePair失败,需要独立地设置注册内存以及QueuePair的创建,

Infiniband::QueuePair* Device::create_queue_pair(IcfsContext *cct, ibv_qp_type type)

{

//<nene>: use the "ms_async_rdma_qp_max_send_wr" instead of "max_send_wr"

/*

  Infiniband::QueuePair *qp = new QueuePair(

      cct, *this, type, active_port->get_port_num(), srq, rx_cq, rx_cq, max_send_wr, max_recv_wr);

*/

  uint32_t qp_max_send_wr = cct->_conf->ms_async_rdma_qp_max_send_wr;

  Infiniband::QueuePair *qp = new QueuePair(

      cct, *this, type, active_port->get_port_num(), srq, rx_cq, rx_cq, qp_max_send_wr, max_recv_wr);

//</nene>

  if (qp->init()) {

    delete qp;

    return NULL;

  }

  return qp;

}

经过修改之后,达到了以下效果,

  • 注册内存buffer大小(ms_async_rdma_buffer_size)可由4096增加到131072
  • 注册内存buffer数量(ms_async_rdma_send_buffers/ms_async_receive_buffers)可由1024增加到10240
  • 解决了1M大小数据块测试过程中数据断流问题
  1. TCMalloc优化内存分配

TCMalloc全称Thread-Caching Malloc,即线程缓存的malloc,实现了高效的多线程内存管理,用于替代系统的内存分配相关的函数(malloc、free,new,new[]等)。

icfs_perf_msgr_server/icfs_perf_msgr_client测试工具没有采用TCMalloc,但是msg模块却使用了TCMAlloc进行优化,为了更加准确地描述网络模块地性能,需要对测试程序配置对TCMalloc的支持。

在测试程序中采用TCMalloc分配内存,测试结果如下,

server 41

server 42

Client性能

Server配置
RoCE,event,tcmalloc

Client
RoCE,event,tcmalloc

并行数
worker threads

并行数
numjobs

队列深度
concurrency

request个数
ios

耗时(us)

IOPS

延时(us)

1

1

32

100K

3208947

31162.87

32.08947

1

2

32

100K

3432609

58264.72

34.32609

1

4

32

100K

3349781

119410.8

33.49781

1

8

32

100K

4502944

177661.5

45.02944

1

16

32

100K

6317459

253266.4

63.17459

1

32

32

100K

12766794

250650.2

127.6679

1

64

32

100K

25002414

255975.3

250.0241

1

64

64

100K

25310469

252859.8

253.1047

从上面地优化结果可以看出,

  • 经过TCMalloc内存分配优化,最大IOPS增加近160%
  • 连接数增大到一定程度,整体性能不再提高,1S1C情况下,最大IOPS25万左右。
  1. 工作线程数调优

每个AsyncMessenger根据ms_async_op_threads生成Worker线程,每个Worker线程包含一个事件分发器EventCenter来处理网络I/O流事件及其回调函数分发。

对于单个AsyncMessenger,增大ms_async_op_threads,生成多个Worker线程,研究不同情况地网络通信性能。

 

 

 

server 41

server 42

Client性能

CPU占有率

Server配置
RoCE,event,tcmalloc
ms_async_op_threads=3

Client
RoCE,event,tcmalloc
ms_async_op_threads=3

并行数
worker threads

并行数
numjobs

队列深度
concurrency

request个数
ios

耗时(us)

IOPS

延时(us)

1

1

32

100K

3331462

30016.85

33.31462

69.1%

1

2

32

100K

3372494

59303.29

33.72494

133.4%

1

4

32

100K

3927981

101833.5

39.27981

231.1%

1

8

32

100K

6795892

117718.2

67.95892

284.8%

1

16

32

100K

11972282

133642

119.7228

343%

1

32

32

100K

19545797

163718.1

195.458

342.9%

1

64

32

100K

34377666

186167.4

343.7767

362.8%

1

64

64

100K

29780075

214908.8

297.8008

369.5%

 

 

server 41

server 42

Client性能

CPU占有率

Server配置
RoCE,event,tcmalloc
ms_async_op_threads=10

Client
RoCE,event,tcmalloc
ms_async_op_threads=10

并行数
worker threads

并行数
numjobs

队列深度
concurrency

request个数
ios

耗时(us)

IOPS

延时(us)

1

1

32

100K

3208947

31162.87

32.08947

53.5%

1

2

32

100K

3432609

58264.72

34.32609

114.6%

1

4

32

100K

3349781

119410.8

33.49781

249%

1

8

32

100K

4502944

177661.5

45.02944

356%

1

16

32

100K

6317459

253266.4

63.17459

616%

1

32

32

100K

12766794

250650.2

127.6679

654%

1

64

32

100K

25002414

255975.3

250.0241

649%

1

64

64

100K

25310469

252859.8

253.1047

691%

 

从结果来看,Worker线程数由3增加到10,最大IOPS增加19%,但是相应地CPU占有率增加近87%

  1. 多线程Reactor模型

Async模块采用Reactor模型,当网络I/O流事件发生时,EventCenter会调用对应对应地事件回调函数EventCallback进行处理,由于同一EventCenter内地事件回调函数地执行是顺序地,所以当存在较耗时地回调函数调用时,EventCenter::process_events就成为了整个网络通信性能瓶颈。

为了改进这种高性能网络I/O流模型,主要有两种思路:

  • 增加EventCenter地数量,达到降低单个EventCenter内地事件回调数量地目的。
  • 采用多线程模型,异步地执行同一EventCenter内的事件回调。

经过测试分析,多线程Reactor模型并未达到预期地效果,性能没有提升。

主要代码如下:

ThreadPool cb_tp;

  class EventCallbackWQ : public ThreadPool::WorkQueue<EventCallback> {

    list<EventCallback*> callbacks;

 

   public:

    EventCallbackWQ(time_t timeout, time_t suicide_timeout, ThreadPool *tp)

      : ThreadPool::WorkQueue<EventCallback>("EventCenter::EventCallback", timeout, suicide_timeout, tp) {}

 

    bool _enqueue(EventCallback *cb) {

      auto iter = std::find(callbacks.begin(), callbacks.end(), cb);

      if (iter == callbacks.end()) {

        callbacks.push_back(cb);

      }

      return true;

    }

    void _dequeue(EventCallback *cb) {

      assert(0);

    }

    bool _empty() {

      return callbacks.empty();

    }

    EventCallback *_dequeue() {

      if (callbacks.empty())

        return NULL;

      EventCallback *cb = callbacks.front();

      callbacks.pop_front();

      return cb;

    }   

    void _process(EventCallback *cb, ThreadPool::TPHandle &handle) override {

      if (cb) {

        cb->do_request(cb->fd_or_id);

      } else {

        assert(0);

      }

    }

    void _process_finish(EventCallback *cb) { }

    void _clear() {

      assert(callbacks.empty());

    }

  } cb_wq;

 

  1. 消息接收缓存

当网络I/O流存在可读数据的时候,EventCenter::process_events()会调用AsyncConnection::process()函数来读取消息数据。

在读取消息的data部分的时候,会不断地调用alloc_aligned_buffer()来申请内存,从而严重地影响程序地性能。为了提高内存分配地利用效率,通过封装boost::pool内存池来完成bufferlist中内存分配。

目前这项工作还在进行中,需要进一步地分析验证。

主要代码如下:

  class buffer::boost_buffer : public buffer::raw {

    boost::pool<> &mempool;

    unsigned chunk_size = 0;

    unsigned chunk_num = 0;

  public:

    explicit boost_buffer(unsigned l, boost::pool<> &p) : raw(l), mempool(p) {

      if (len) {

        chunk_size = p.get_requested_size();

        chunk_num = len/chunk_size+1;

        if (len%chunk_size==0) {

          --chunk_num;

        }

        data = static_cast<char *>(mempool.ordered_malloc(chunk_num));

      }

 

      assert(data != nullptr);

 

      inc_total_alloc(len);

      bdout << "boost_buffer" << this << " alloc " << (void *)data << " " << l << " " << buffer::get_total_alloc() << bendl;

    }

    ~boost_buffer() {

      mempool.ordered_free(data, chunk_num);

 

      dec_total_alloc(len);

      bdout << "boost_buffer " << this << " free " << (void *)data << " " << buffer::get_total_alloc() << bendl;

    }

    raw* clone_empty() {

      return new boost_buffer(len, mempool);

    }

  };

  buffer::raw* buffer::create_boost_buffer(unsigned len, boost::pool<> &p) {

    return new buffer::boost_buffer(len, p);

  }

 

static void alloc_boost_buffer(boost::pool<> &p, unsigned len, bufferlist &data)

{

  // create a buffer to read into that matches the data alignment

  assert(len != 0);

 

  bufferptr ptr

  (

    buffer::create_boost_buffer

    (

      len, p

    )

  );

  data.push_back(std::move(ptr));

}

 

      case STATE_OPEN_MESSAGE_READ_DATA_PREPARE:

        {

          // read data

          unsigned data_len = le32_to_cpu(current_header.data_len);

          unsigned data_off = le32_to_cpu(current_header.data_off);

          if (data_len) {

            // get a buffer

            map<icfs_tid_t,pair<bufferlist,int> >::iterator p = rx_buffers.find(current_header.tid);

            if (p != rx_buffers.end()) {

              ldout(async_msgr->cct,10) << __func__ << " seleting rx buffer v " << p->second.second

                                  << " at offset " << data_off

                                  << " len " << p->second.first.length() << dendl;

              data_buf = p->second.first;

              // make sure it's big enough

              if (data_buf.length() < data_len)

                data_buf.push_back(buffer::create(data_len - data_buf.length()));

              data_blp = data_buf.begin();

            } else {

              ldout(async_msgr->cct,20) << __func__ << " allocating new rx buffer at offset " << data_off << dendl;

              //<nene>: Use the memepool

              //alloc_aligned_buffer(data_buf, data_len, data_off);

              alloc_boost_buffer(mempool, data_len, data_buf);

              data_blp = data_buf.begin();

            }

          }

 

          msg_left = data_len;

          state = STATE_OPEN_MESSAGE_READ_DATA;

        }

 

 

附录(Ⅰ):连接过程状态迁移图

 

 

参阅资料

  1. 罗军舟.  <TCP/IP协议及网络编程技术>, 清华大学出版社.
  2. 游双. <Linux高性能服务器编程>, 机械工业出版社.
  3. 陈硕. <Linux多线程服务端编程>, 电子工业出版社。
  4. 谢希仁. <计算机网络>, 电子工业出版社。
  5. Mellanox. RDMA Aware Networks Programming User Manual.
  6. 罗剑锋. Boost程序库完全开发指南, 电子工业出版社.
  7. Douglas C. Schmidt. An Object Behavioral Pattern for Demultiplexing and Dispatching Handles for Synchronous Events.
  8. Stephen Prata. C++ Primer Plus, 人民邮电出版社.
  9. 严蔚敏. 数据结构(C语言描述), 清华大学出版社.
  • 10
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值