《超大流量分布式系统架构解决方案-人人都是架构师2.0》读书笔记

持续更新中…


全书总结

本书对于技术的学习并没有很深的阐述,而是从系统的架构角度讲解相关知识,可以帮助同学们或工程师们更好的了解系统的架构知识。

第一章: 大题小做—大规模服务化架构

    本书第一章主要介绍了互联网领域分布式系统的架构演变过程,以及大规模服务化场景下企业应该如何实施服务治理。在此大家需要注意,如果用户规模及业务需求的复杂度还没有到量,那么最好保持现有架构不变,毕竟构建一个高性能、高可用、易扩展、可伸缩的分布式系统绝非一件简单的事情,需要解决的技术难题太多。而且,如果业务没有起色,一味地追寻大型网站架构并无任何意义。
    当然,随着用户规模的线性增长,以及业务需求越来越复杂,从单机系统逐渐演变为分布式系统,以更好地支撑业务发展似乎是必经之路。

1.1 架构演变过程

    任何一个网站在上线初期都不会拥有庞大的用户流量和海量数据,都是在不停的试错过程中一步一步演变其自身架构。 为了应对不断增加的用户流量和数据,系统的架构模式也在不断的升级。当然目前一些具备一定技术能力的公司,在成立一个新部门时,架构师直接选用微服务或分布式进行开发也是很常见的。常见的架构演变历程为: 单机架构 → 集群架构 → 微服务架构。
    单体架构:
在这里插入图片描述

集群(Cluster)技术:
       可以将多台独立的服务器通过网络相互连接组合起来,形成一个有效的整体对外提供服务,使用集群的意义就在于其目标收益远高于所付出的实际成本和代价。互联网领域存在一个共识,那就是当一台服务器的处理能力接近或已超出其容量上限时,通常的做法是采用集群技术,通过增加新的服务器来分散并发访问流量。只要业务系统能够随意支持服务器的横向扩容,那么从理论上来说就应该无惧任何挑战,从而实现可伸缩性和高可用架构。
如下图所示: 请求先进入Nginx反向代理服务器集群进行路由转发,使请求根据不同的路由规则转发到不同的服务器上。
在这里插入图片描述
当业务发展到一定程度时,还可以增加以下方案:

  1. 静态资源通过CDN加速。
    在这里插入图片描述

  2. 业务垂直化,降低耦合,从而实现分而治之的管理。
    架构师可以根据系统业务功能的不同拆分出多个业务模块(一般大型电商网站都会拆分出首页、用户、搜索、广告、购物、订单、商品、收益结算等子系统),再由不同的业务团队负责承建,分而治之,独立部署。
    在这里插入图片描述

服务化:
        随着用户规模逐渐庞大,需求更加复杂,一定会对耦合在一个Web容器中的单体应用进行垂直化改造,以业务功能为维度拆分出多个子系统(如上图),这样做就是为了能够更清晰地规划和体现出每个子系统的职责,降低业务耦合,以及提升容错性。
在这里插入图片描述

集群和分布式的区别:
       集群是指将多台服务器集中在一起,目的是实现同一业务;而分布式是指将不同的业务分布在不同的地方,目的是实现不同的业务;前者是串联工作,而后者是并联工作。在此大家需要注意,分布式架构中的每一个子节点都允许构成一个集群,但集群却并不一定就是分布式的。

举一个贴近生活的例子。假设你厨艺高超,声名远播,周末盛情邀约了几个小伙伴来你家聚餐,你一个人负责买菜、切菜、炒菜、上菜,这便是单机架构;而某一天更多朋友来你家做客时,你发现似乎有些力不从心,这时你需要几个人一起来协作帮忙,以便提升效率,这就是集群架构;假设你家大业大,有上百位朋友都相约你家吃饭时,你会需要更多的人来协作帮忙,并且相互之间需要明确职责分工,A组负责买菜,B组负责洗菜,C组负责炒菜,D组负责上菜,这就是分布式+集群架构。

前后端分离架构的演变:

演变前存在的问题:

  • 下游RPC接口发生变化时,上游依赖这个接口的各个端的WebServer都需要修改代码;
  • 前后端职责模糊,前端需要关心后端语法,后端需要维护模板代码
  • 前后端联调成本高

前后端分离后的系统架构图:
在这里插入图片描述

API网关服务

  1. Dubbo 目前并没有为开发人员提供成熟、易用的网关服务,因此我们通常会在服务上游构建一层WebServer用以满足如下3个需求:
    • 将外部的HTTP协议适配为内部的二进制协议;
    • 聚合操作;
    • 集成一些公共逻辑。

    由 WebServer 来负责处理网关业务,但随着业务发展到一定阶段后,WebServer变得越来越重,架构团队不得不考虑引入网关层来解决现有痛点。将接入层Nginx 改造为网关层,Lua 脚本的维护成本和实现难度较大,因此,选择了将一些流控、灰度发布、安全防护,以及日志记录等一些相对简单的网关逻辑放在接入层,尽可能将流量挡在系统上游,而对于一些复杂的网关逻辑,则单独引入了一层API网关,替换掉原本的WebServer,将聚合操作下潜至服务层,API网关通过优化后的泛化调用方式调用下游目标服务。
在这里插入图片描述
关于如何在API网关中实现流控逻辑,可以直接继续阅读后续内容。

    随着敏捷开发的日益普及,互联网企业的项目发布也随之变得越发频繁,每次新版本的发布都可能伴随着诸多不可控风险,因此我们需要一种发布机制,来降低风险的影响范围。 灰度发布,简单来说,就是通过一系列的规则和策略,先将一小部分的用户作为“金丝雀”,让其请求路由到新版本应用上进行观察,出现异常,影响范围也比较小。待运行正常后,再逐步导流更多的用户到灰度环境中。

    如果是以白名单作为灰度策略的,那么当用户发起HTTP请求后,如嵌入在Nginx中的Lua脚本会首先从Header中获取出Token并解析出用户的UserId,再请求Redis验证当前的UserId是否包含在白名单中,只有那些包含在白名单中的用户,才允许访问灰度环境中的新版本应用。
    网关层的引入并非是必需的,但是业务越复杂,网关的好处就会越明显。
在这里插入图片描述

分布式多活数据中心:
    出于对灾备、高可用等方面考虑,稍微有些实力的企业都会选择构建多个数据中心,由主数据中心负责核心业务的正常运转,其他数据中心负责数据备份,以及一些边缘业务。
两地三中心架构:
在这里插入图片描述
尽管"两地三中心"架构具备良好的容灾性和容错性,但存在一个弊端,就是会产生大量的资源浪费。只有不幸降临时,灾备数据中心才能派上用场和体现它的价值。

分布式多活架构:
在这里插入图片描述

    分布式多活架构存在三个难题:

  • 多数据中心之间需要打通内网专线通道
  • RPC调用需要做到就进调用
  • 数据同步问题
1.2 服务治理需求

服务化和RPC协议:
        服务化框架的核心就是RPC(远程过程调用),常见的RPC框架有很多,例如: Dubbo。成熟的RPC实现方案也有很多,比如: Java RMI、Web Service、Hessian及Finagle等。当然不同的RPC实现对序列化和反序列化的处理也不尽相同,比如将对象序列成XML/JSON等文本格式,尽管可读性、扩展性高,但却过于笨重,不仅报文体积大,解析过程也较为缓慢。因此在一些特别注重性能的场景下,采用二进制协议更合适。
        RPC调用主要经历三个步骤:
        ☆: 底层网络通信协议处理
        ☆: 解决寻址问题
        ☆: 请求/响应过程中参数的序列化和反序列化工作。

服务治理框架Dubbo实现服务化:
        Provider作为服务提供方对外提供服务,当JVM启动时Provider会被自动加载和启动,当Provider启动成功后,会向注册中心(Registry)注册指定的服务,这样作为服务调用方的Customer在启动后便可以向注册中心订阅目标服务(服务提供者的地址列表),然后在本地根据负载均衡算法从地址列表中选择其中一个可用服务节点进行RPC调用,如果调用失败则自动Failover到其他服务节点上。
在服务的调用过程中还存在一个不容忽视的问题,即监控系统。监控系统来帮助开发人员分析和定位问题,Dubbo 为开发人员提供了一套完善的监控中心,使我们能够非常清楚地知道指定服务的状态信息(如服务调用成功次数、服务调用失败次数、平均响应时间等)。
在这里插入图片描述
服务接口需要同时包含在服务提供方和服务调用方两端,而服务实现对于服务调用方来说是隐藏的,因此它仅仅需要包含在服务提供方即可。

警惕因超时和重试引起的系统雪崩:
        Dubbo的Consumer会在本地根据负载均衡算法从地址中选择某一服务节点进行RPC调用,如果调用失败则自动Failover到其他服务节点上,默认重试次数为两次,服务调用超时就意味着调用失败,需要重试。 在大流量的场景下,系统的负载压力将被逐步方法,产生蝴蝶效应。假设某时刻有10000个请求调用服务A,都调用失败则重试,就会产生30000次的并发请求,处于峰值流量则会更加糟糕,很有可能引发系统雪崩。
        并不是任何服务都适合Failover的,比如写服务,由于要考虑幂等性,因此失败后不应该重试,否则将导致数据重复写入。只有读服务开启Failover才显得有意义。

服务治理
        服务治理所涉及的范围较广,包含但不限于: 服务注册/发现,服务限流、服务熔断、负载均衡、服务路由、配置管理、服务监控等。 服务治理的主要作用是: 改变运行时服务的行为和选址逻辑,达到限流,权重配置等目的。
        以服务动态注册/发现为例,当服务变得越来越多时,如果把服务的调用地址(URL)配置在服务调用方,那么URL的配置管理将变得非常麻烦。引入注册中心的目的就是实现服务的动态注册和发现,让服务的位置更加透明,这样服务调用方将得到解脱,并且在客户端实现负载均衡和Failover将会大大降低对硬件负载均衡器的依赖,从而减少企业的支出成本。
        服务端服务发现模式,除注册中心外,还需引入代理中心(路由器)当Provider成功启动后,会向注册中心注册指定的服务,由代理中心来处理服务发现;Consumer 的职责很简单,只需要连接代理中心并向其发送请求即可,代理中心会根据负载均衡算法从地址列表中选择一个可用的服务节点进行RPC调用。
在这里插入图片描述

        对于客 户 端 服 务 发 现 模 式,Dubbo 整体架构其实也基于的是此
模式。基础的服务注册步骤和服务端模式是一致的,只是将服务发现交给了Consumer来负责实现,无须再引入代理中心,由Consumer根据指定的负载均衡算法从地址列表中选择一个可用的服务节点进行RPC调用。
在这里插入图片描述

注册中心性能瓶颈
        企业常用的注册中心式Zookeeper,其存在两个棘手问题:
        - 服务扩容时,应用启动异常缓慢。
        - 冗余的服务配置项会增加存储压力和扩大网络开销。

        zookeeper是一个典型的CP系统,是基于ZAB(ZookeeperAtomic Broadcast,原子广播)协议的强一致性中间件,它的写操作存在单点问题,无法通过水平扩容来解决。当客户端发送写请求时,集群中的其他节点会优先转发给Leader节点,由Leader节点来负责具体的写入操作,只有当集群中>=N/2+1个节点都同步成功后,一次写操作才算完成。当服务扩容时,TPS 越高,服务注册时的写入效率就越低,这会导致上游产生大量的请求排队,表象就是服务启动变得异常缓慢。
        注册中心的优化改造:
        1. 扩展ObSerer节点,提升Zookeeper集群的读取速度和带宽压力。
        2. 精简Provider注册时的配置项,减少冗余配置,降低网络开销。
        3. 采取去中心话、最终一致性交的注册中心方案。

1.3 服务治理之调用链

        调用链在系统中起着十分重要的作用,在分布式和微服务系统中,许多共享业务被拆分成独立的服务,一次用户请求可能涉及后端多个服务间的调用,那些分散在各个服务器上的孤立日志对于排查问题就会非常不利。因此构建 分布式链路跟踪系统(即调用链)十分重要。
        分布式调用跟踪系统其实就是一个监控平台,能够以可视化的方式展现跟踪到的每一个请求的完整调用链,以及采集调用链上每个服务的执行耗时、整合孤立日志等。目前,一些大型的互联网企业内部都构建有适用于自身业务特点的分布式调用跟踪系统。
        众所周知的一些分布式调用跟踪系统大都脱胎于 Google 的论文Dapper。了解调用链首先要知道 TranceID、SpanID、ParentSpanID。Trace表示对一次请求的完整调用链追踪,而Span则可以理解为 Trace 的组成结构,比如服务 A 和服务 B 的请求/响应过程就是一次Span。在生产环境中,一次用户请求可能涉及后端多个服务之间的调用,那么 Span就用于体现服务之间具体的依赖关系。每一次请求都应该被分配一个全局唯一的TraceID,并且整个调用链中所有的Span过程都应该获取到同一个TraceID,以表示这些服务调用过程是发生在同一个Trace上的。

调用链的实现方案
        Dubbo实现分布式链路跟踪系统:
        Dubbo预留了足够多接口,方便开发人员二次开发,在Dubbo框架上实现调用跟踪就显得顺理成章。同时,TranceID嵌入Dubbo中可以有效降低对业务代码的侵入,不需要在业务代码中硬编码实现数据采集上报工作。 一般,在开发过程中使用Filter来拦截HTTP请求,Dubbo也提供了专门用于拦截RPC请求Filter接口,以便开发人员实现服务的调用跟踪和数据采集上报等功能扩展。
        实 现 Filter 接 口 后 , 开 发 人 员 还 需 要 重 写 其invoke()方法,该方法中包含两个参数,其中 Invoker 接口提供用于执行目标服务方法的 invoke()方法,Invocation 接口可以向服务提供方传递当前 Trace 的上下文信息,其派生为RpcInvocation类。在此需要注意,由于Dubbo的Filter并没有纳入Spring的IoC容器中 进 行 管 理,因此我们需要手动将其交由Ioc管理

成功配置好Filter后,当服务调用方向服务提供方发起RPC请求时,Filter将会对其进行拦截,开发人员便可以在远程服务方法的执行前后实现自定义的埋点上报逻辑。Dubbo提供的Filter既可以对服务提供方进行拦截,也可以拦截服务调用方,只要在服务调用方和服务提供方的Spring配置信息中配置好Filter即可。在Filter中可以使用Dubbo提供的一个临时状态记录器RpcContext类,通过调用RpcContext提供的一系列方法,可以非常方便地采集到当前Span过程中包含的一些非常有价值的数据信息.
实现调用跟踪,首先需要根据TraceID将一次请求中涉及的所有后端服务调用完整地串联起来形成一个Trace,然后再考虑如何明确各个服务之间的调用顺序和依赖关系等问题

在这里插入图片描述
        SpanID用于标记每一个Span过程,代表着服务的调用顺序。而ParentSpanID则用于明确 Trace 中服务的依赖关系。SpanID 的值会随每一次服务调用递增,而ParentSpanID的值则来源上一个Span过程的SpanID.
        通过TraceID、SpanID和ParentSpanID,便能够快速地梳理不同的Trace中服务之间的调用顺序和依赖关系,但是服务调用方如何将这些Trace上下文信息向下传递给服务提供方呢?如下图所示,当Filter对服务调用方进行拦截时,可以将Trace上下文信息Set进由Dubbo提供的RpcInvocation接口中;当Filter对服务提供方进行拦截时,再从中获取出之前由服务调用方传递过来的Trace上下文信息即可。
在这里插入图片描述
采集其他指标 完成网络耗时的监控

        除了需要采集TraceID、SpanID、ParentSpanID和RpcContext中包含的数据信息,以及服务执行异常时的堆栈信息,还需要采集Trace中各个Span过程的开始时间和结束时间,以便跟踪系统后续根据这些采集到的时间参数进行服务的执行耗时运算。简单来说,当 Filter 拦截到RPC 请求时,需要记录一个开始时间,当服务调用完成后还需要记录一个结束时间。那么服务调用方和服务提供方就是四个不同维度的时间戳,如下所示:

  • Client Send Time (CS,客户端发送时间)
  • Client Receive Time (CR, 客户端接收时间)
  • Server Receive Time (SR,服务端接收时间)
  • Server Send Time (SS, 服务端发送时间)

通过采集这四个不同维度的时间戳,便可以在一次请求完成后计算出整个Trace的执行耗时、网络耗时,以及Trace中每个Span过程的执行耗时、网络耗时等结果数据。关于服务执行耗时的运算规则,如下所示:

  • 服务调用耗时 = CR - CS;
  • 服务处理耗时 = SS - SR;
  • 网络耗时 = 服务调用耗时 - 服务处理耗时;
  • 前置网络耗时 = SR - CS;
  • 后置网络耗时 = CR - SS;

调用链概述
        当服务调用方向 服务提供方发起RPC请求时,Filter会对服务调用方进行拦截,然后试图从ThreadLocal中获取当前线程的Trace上下文信息,如果不存在则说明这是一次根调用,需要生成TraceID,然后将生成的TraceID、SpanID设置在Invocation中传递给服务提供方,接着执行前置数据采集上报(开始时间维度为 Client Send Time)。当调用Invoker接口的invoke()方法执行完远程服务方法后,再执行后置数据采集上报(结束时间维度为Client Receive Time)。
        当RPC请求到达服务提供方后,Filter会对其进行拦截,然后从Invocation中获取由服务调用方传递过来的Trace上下文信息,并将其存储到当前线程的ThreadLocal中,然后执行前置数据采集上报(开始时 间 维 度 为 Server Receive Time ) 。 当 调 用 Invoker 接 口 的invoke()方法执行完服务方法后,再执行后置数据采集上报(结束时 间 维 度 为 Server Send Time ) , 最 后 还 需 要 删 除 存 储 在ThreadLocal 中当前线程的Trace上下文信息。
        如果服务提供方内部还调用了其他服务,Filter 会对调用方进行拦截,然后从ThreadLocal中获取当前线程的Trace上下文信息,修改SpanID和设置ParentSpanID后,再将其设置到Invocation中传递给服务提供方,接着执行前置数据采集上报(开始时间维度为Client Send
Time)。当调用Invoker接口的invoke()方法执行完远程服务方法后,再执行后置数据采集上报(结束时间维度为Client ReceiveTime)。

第二章: 大促备战核弹—全链路压测

2.1 为什么要在线上实施全链路压测

        当用户规模较小时,开发和测试同学随便在线下环境做一把功能测试,只需要确保系统具备可靠性(检验系统是否能够无差错地执行预期的操作)即可,但随着用户规模的线性上升,流量会越来越大,这时再光依靠常规的功能测试已无法满足,流量上来了,就必须重视系统性能了,毕竟谁也不希望自己的系统被流量无情击垮而导致低可用性。
        因此到了这个阶段,大多数企业都会选择在线下对各个子系统、中间件、存储系统实施压测,明确其吞吐量,但是因差异性问题,这样的压测结果数据,与线上还是有较大差距的,毕竟绝大多数企业的压测环境并不会按照线上环境的机器数量1:1扩容部署,所以线下环境的压测结果数据基本上仅供参考,并不能够作为线上环境的指导数据,只有直接在线上环境实施压测才是唯一的明道。
        线上实施全链路压测的 4 个关键核心点
        - 业务系统、中间件如何配合改造升级;
        - 如何将压测数据引流到隔离环境中;
        - 压测数据如何构造;
        - 超大规模的压测流量如何发起。

        系统中任何一个接口都不会独立存在,假设A接口可以压测出1w/s的QPS,那么当A接口和B接口同时施压时,A接口的QPS势必会下降,原因其实很简单,因为受限于一些共享资源;全链路压测其实指的就是在特定的业务场景下,将相关的链路完整地串联起来同时施压,尽可能模拟出真实的用户行为,当系统整站流量都被打上来的时候,必定会暴露出性能瓶颈,才能够探测出系统整体的真实容量水位,以及有指导地在大促前进行容量规划和性能优化,这便是线上实施全链路压测的真正目的。

2.2 业务系统如何区分压测流量

压测流量打标方案

        前面提到,使用线上生成环境压测才能更加准确和发现瓶颈,那么如何区分出真实流量和压测流量,就必然需要对压测的数据进行特殊标记,并且这个压测标记还要能够顺利在一次请求涉及的所有后端服务调用的上下文中进行传递,当最终数据落盘或者调用第三方接口的时候才能做到不影响和污染线上数据。
在这里插入图片描述
        对压测数据进行打标的方式有许多种:
        1. 在URL上进行打标 ;(在url中增加一个参数)
        2. 在HTTP的header中进行打标。
        正常URL: http://renbz.com/qbc/xx.json
        打标URL: http://renbz.com/qbc/xx.json?st=true
        当在URL上添加打标参数(st=true)并向后端业务系统成功发起请求后,如果业务系统检测到请求中已包含压测标记,那么就会把压测标记在一次请求生命周期中一直流转下去。除可以在URL上进行流量打标外,在HTTP请求的header中打标似乎也是一个不错的选择。

在链路上下文信息中传递压测标记

        在业务代码中,调整代码加几个判断条件,理论上可行,但一般不这样做。抛开成本不谈,最棘手的问题就是连负责相关业务的研发同学都没有完全的把握能够确保所有的改造不存在遗漏。
        为了避免高侵入性问题带来的困扰,业界通常采用的做法是直接在中间件、组件中动手脚,将流量区分和压测标记传递的任务交由具体的中间件、组件来负责,业务系统中唯一需要配合改动的就是对所依赖的相关中间件、组件版本的升级。
        第一章介绍了在大规模RPC服务调用场景下利用分布式调用跟踪系统来实施治理的,既然一次请求中涉及的所有后端服务调用都能够被完整地串联起来,那么在分布式跟踪系统中进行相应的全链路压测的升级改造就显得顺理成章。简单来说,我们采用的做法是,当对业务系统发起一次HTTP请求调用时,嵌套在分布式调用跟踪系统中负责流量区分的拦截器会优先对请求进行拦截,然后尝试从URL或者HTTP的header中获取出压测标记,如果确认为压测流量,就将压测标记放进当前线程的ThreadLocal中,待调用下游服务时,再从中获取出压测标记并放入Dubbo的RpcInvocation中,逐层向下传递即可。
        注意: 程序中如果直接使用 new Thread()的方式创建线程并发起异步 RPC 请求,应该使用ThreadLocal 的派生类 InheritableThreadLocal,如果采用线程池的方式,那么可以使用阿里的TransmittableThreadLocal来替代JDK原生的ThreadLocal,并设置相关环境变量。

外部第三方接口走Mock

        当业务系统需要调用外部接口时,嵌套在分布式调用跟踪系统中的 MockFilter 会对其进行拦截,如果检测为压测流量,则匹配目标接口是否是外部接口,匹配规则可以直接在配置中心内添加黑名单设置,如果满足匹配,就直接调用Mock服务,Mock的具体返回结果同样也可以在配置中心内设置,如果压测流量较大,运维同学仍然需要扩容较多的Mock服务来支撑压测请求,由此可见 Mock 服务的运维成本相对还是相对较高的,因此我们不得不对此方案进行改进,不再单独部署相关的Mock服务,而是直接将Mock逻辑封装在MockFilter内,除可以避免网络损耗提升压测质量外,更重要的是,有效控制住了成本开销。
在这里插入图片描述
压测数据的隔离方案
        在设计压测数据的隔离方案时,考虑了物理隔离和逻辑隔离2种方案,前者的优点很明显,但缺点就是增加了运维成本,加重了企业的负担。我们线上存储系统的规模是比较庞大的,如果全部都按照1:1的方式扩容出一套影子库出来,这几乎不现实,而且在非压测时段这些扩容出来的资源纯粹就是一种浪费。
        对于那些真正需要落盘的数据,以MySQL为例,我们选择了在同一套生产库实例下新建一个影子库来存储压测数据,避免定时任务在统计的时候把压测数据也计算其中。而对于那些中间数据,比如写入Redis、消息队列中的每一条压测数据,都会统一加上特定的压测标识,并设置较短的存活时间,让其自动失效。在此大家需要注意,由于压测流量请求的是线上存储系统,因此底层连接资源的占用、释放等问题需要重点关注,千万不能在压测已经结束了,仍然还影响正常的用户访问。

持续更新中…

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值