解剖Twitter:Twitter系统架构设计分析-2

 

比较有趣的事情是,通常把Varnish部署在Web Server之外,面向Internet的位置。这样,当用户访问网站时,实际上先访问Varnish,读取所需内容。只有在Varnish没有缓存相应内容时,用户请求才被转发到Web Server上去。而Twitter的部署,却是把Varnish放在Apache Web Server内侧[19]。原因是Twitter的工程师们觉得Varnish的操作比较复杂,为了降低Varnish崩溃造成整个网站瘫痪的可能性,他们便采取了这种古怪而且保守的部署方式。

Apache Web Server的主要任务,是解析HTTP,以及分发任务。不同的Mongrel Rails Server负责不同的任务,但是绝大多数Mongrel Rails Server,都要与Vector Cache和Row Cache联系,读取数据。Rails Server如何与MemCached联系呢?Twitter工程师们自行开发了一个Rails插件(Gem),称为CacheMoney。

虽然Twitter没有公开Varnish的命中率是多少,但是[17]声称,使用了Varnish以后,导致整个Twitter.com网站的负载下降了50%,参见Figure 3.

Figure 3. Cache decreases Twitter.com load by 50% [17]
Courtesy http://farm3.static.flickr.com/2537/4061273900_2d91c94374_o.png

Reference,

[12] Alphabetical List of Twitter Services and Applications.
(http://en.wikipedia.org/wiki/List_of_Twitter_services_and_applications)
[13] How flash changes the DBMS world.
(http://hansolav.net/blog/content/binary/HowFlashMemory.pdf)
[14] Improving running component of Twitter.
(http://qconlondon.com/london-2009/file?path=/qcon-london-2009/slides/
EvanWeaver_ImprovingRunningComponentsAtTwitter.pdf)
[15] A high-performance, general-purposed, distributed memory object caching system.
(http://www.danga.com/memcached/)
[16] Updating Twitter without service disruptions.
(http://gojko.net/2009/03/16/qcon-london-2009-upgrading-twitter-without-service-disruptions/)
[17] Fixing Twitter. (http://assets.en.oreilly.com/1/event/29/
Fixing_Twitter_Improving_the_Performance_and_Scalability_of_the_World_s_
Most_Popular_Micro-blogging_Site_Presentation%20Presentation.pdf)
[18] Varnish, a high-performance HTTP accelerator.
(http://varnish.projects.linpro.no/)
[19] How to use Varnish in Twitter.com?
(http://projects.linpro.no/pipermail/varnish-dev/2009-February/000968.html)
[20] CacheMoney Gem, an open-source write-through caching library.
(http://github.com/nkallen/cache-money)

 

【4】抗洪需要隔离

如果说如何巧用Cache是Twitter的一大看点,那么另一大看点是它的消息队列(Message Queue)。为什么要使用消息队列?[14]的解释是“隔离用户请求与相关操作,以便烫平流量高峰 (Move operations out of the synchronous request cycle, amortize load over time)”。

为了理解这段话的意思,不妨来看一个实例。2009年1月20日星期二,美国总统Barack Obama就职并发表演说。作为美国历史上第一位黑人总统,Obama的就职典礼引起强烈反响,导致Twitter流量猛增,如Figure 4 所示。

Figure 4. Twitter burst during the inauguration of Barack Obama, 1/20/2009, Tuesday
Courtesy http://farm3.static.flickr.com/2615/4071879010_19fb519124_o.png

其中洪峰时刻,Twitter网站每秒钟收到350条新短信,这个流量洪峰维持了大约5分钟。根据统计,平均每个Twitter用户被120人“追”,这就 是说,这350条短信,平均每条都要发送120次 [16]。这意味着,在这5分钟的洪峰时刻,Twitter网站每秒钟需要发送350 x 120 = 42,000条短信。

面对洪峰,如何才能保证网站不崩溃?办法是迅速接纳,但是推迟服务。打个比方,在晚餐高峰时段,餐馆常常客满。对于新来的顾客,餐馆服务员不是拒之门外,而是让这些顾客在休息厅等待。这就是[14] 所说的 “隔离用户请求与相关操作,以便烫平流量高峰”。

如何实施隔离呢?当一位用户访问Twitter网站时,接待他的是Apache Web Server。Apache做的事情非常简单,它把用户的请求解析以后,转发给Mongrel Rails Sever,由Mongrel负责实际的处理。而Apache腾出手来,迎接下一位用户。这样就避免了在洪峰期间,用户连接不上Twitter网站的尴尬局面。

虽然Apache的工作简单,但是并不意味着Apache可以接待无限多的用户。原因是Apache解析完用户请求,并且转发给 Mongrel Server以后,负责解析这个用户请求的进程(process),并没有立刻释放,而是进入空循环,等待Mongrel Server返回结果。这样,Apache能够同时接待的用户数量,或者更准确地说,Apache能够容纳的并发的连接数量(concurrent connections),实际上受制于Apache能够容纳的进程数量。Apache系统内部的进程机制参见Figure 5,其中每个Worker代表一个进程。

Apache能够容纳多少个并发连接呢?[22]的实验结果是4,000个,参见Figure 6。如何才能提高Apache的并发用户容量呢?一种思路是不让连接受制于进程。不妨把连接作为一个数据结构,存放到内存中去,释放进程,直到 Mongrel Server返回结果时,再把这个数据结构重新加载到进程上去。

事实上Yaws Web Server[24],就是这么做的[23]。所以,Yaws能够容纳80,000以上的并发连接,这并不奇怪。但是为什么Twitter用 Apache,而不用Yaws呢?或许是因为Yaws是用Erlang语言写的,而Twitter工程师对这门新语言不熟悉 (But you need in house Erlang experience [17])。

Figure 5. Apache web server system architecture [21]
Courtesy http://farm3.static.flickr.com/2699/4071355801_db6c8cd6c0_o.png

Figure 6. Apache vs. Yaws.
The horizonal axis shows the parallel requests,
the vertical one shows the throughput (KBytes/second).
The red curve is Yaws, running on NFS.
The blue one is Apache, running on NFS,
while the green one is also Apache but on a local file system.
Apache dies at about 4,000 parallel sessions,
while Yaws is still functioning at over 80,000 parallel connections. [22]
Courtesy http://farm3.static.flickr.com/2709/4072077210_3c3a507a8a_o.jpg

Reference,

[14] Improving running component of Twitter.
(http://qconlondon.com/london-2009/file?path=/qcon-london-
2009/slides/EvanWeaver_ImprovingRunningComponentsAtTwitter.pdf)
[16] Updating Twitter without service disruptions.
(http://gojko.net/2009/03/16/qcon-london-2009-upgrading-
twitter-without-service-disruptions/)
[17] Fixing Twitter. (http://assets.en.oreilly.com/1/event/29/Fixing_Twitter_
Improving_the_Performance_and_Scalability_of_the_World_s_Most_Popular_
Micro-blogging_Site_Presentation%20Presentation.pdf)
[21] Apache system architecture.
(http://www.fmc-modeling.org/download/publications/
groene_et_al_2002-architecture_recovery_of_apache.pdf)
[22] Apache vs Yaws. (http://www.sics.se/~joe/apachevsyaws.html)
[23] 质疑Apache和Yaws的性能比较. (http://www.javaeye.com/topic/107476)
[24] Yaws Web Server. (http://yaws.hyber.org/)
[25] Erlang Programming Language. (http://www.erlang.org/)

【5】数据流与控制流

通过让Apache进程空循环的办法,迅速接纳用户的访问,推迟服务,说白了是个缓兵之计,目的是让用户不至于收到“HTTP 503” 错误提示,“503错误” 是指 “服务不可用(Service Unavailable)”,也就是网站拒绝访问。

大禹治水,重在疏导。真正的抗洪能力,体现在蓄洪和泄洪两个方面。蓄洪容易理解,就是建水库,要么建一个超大的水库,要么造众多小水库。泄洪包括两个方面,1. 引流,2. 渠道。

对于Twitter系统来说,庞大的服务器集群,尤其是以MemCached为主的众多的缓存,体现了蓄洪的容量。引流的手段是Kestrel消息队列,用于传递控制指令。渠道是机器与机器之间的数据传输通道,尤其是通往MemCached的数据通道。渠道的优劣,在于是否通畅。

Twitter的设计,与大禹的做法,形相远,实相近。Twitter系统的抗洪措施,体现在有效地控制数据流,保证在洪峰到达时,能够及时把数据疏散到多个机器上去,从而避免压力过度集中,造成整个系统的瘫痪。

2009 年6月,Purewire公司通过爬Twitter网站,跟踪Twitter用户之间“追”与“被追”的关系,估算出Twitter用户总量在 7,000,000左右 [26]。在这7百万用户中,不包括那些既不追别人,也不被别人追的孤立用户。也不包括孤岛人群,孤岛内的用户只相互追与被追,不与外界联系。如果加上这 些孤立用户和孤岛用户群,目前Twitter的用户总数,或许不会超过1千万。

截止2009年3月,中国移动用户数已达 4.7亿户[27]。如果中国移动的飞信[28] 和139说客[29] 也想往Twitter方向发展,那么飞信和139的抗洪能力应该设计到多少呢?简单讲,需要把Twitter系统的现有规模,至少放大47倍。所以,有人 这样评论移动互联网产业,“在中国能做到的事情,在美国一定能做到。反之,不成立”。

但是无论如何,他山之石可以攻玉。这就是我们研究Twitter的系统架构,尤其是它的抗洪机制的目的。

Figure 7. Twitter internal flows
Courtesy  http://farm3.static.flickr.com/2766/4095392354_66bd4bcc30_o.png
下面举个简单的例子,剖析一下Twitter网站内部的流程,借此考察Twitter系统有哪些机制,去实现抗洪的三要素,“水库”,“引流”和“渠道”。

假设有两个作者,通过浏览器,在Twitter网站上发表短信。有一个读者,也通过浏览器,访问网站并阅读他们写的短信。

1. 作者的浏览器与网站建立连接,Apache Web Server分配一个进程(Worker Process)。作者登录,Twitter查找作者的ID,并作为Cookie,记忆在HTTP邮包的头属性里。

2. 浏览器上传作者新写的短信(Tweet),Apache收到短信后,把短信连同作者ID,转发给Mongrel Rails Server。然后Apache进程进入空循环,等待Mongrel的回复,以便更新作者主页,把新写的短信添加上去。

3. Mongrel收到短信后,给短信分配一个ID,然后把短信ID与作者ID,缓存到Vector MemCached服务器上去。

同时,Mongrel让Vector MemCached查找,有哪些读者“追”这位作者。如果Vector MemCached没有缓存这些信息,Vector MemCached自动去MySQL数据库查找,得到结果后,缓存起来,以备日后所需。然后,把读者IDs回复给Mongrel。

接着,Mongrel把短信ID与短信正文,缓存到Row MemCached服务器上去。

4. Mongrel通知Kestrel消息队列服务器,为每个作者及读者开设一个队列,队列的名称中隐含用户ID。如果Kestrel服务器中已经存在这些队列,那就延用以往的队列。

对应于每个短信,Mongrel已经从Vector MemCached那里知道,有哪些读者追这条短信的作者。Mongrel把这条短信的ID,逐个放进每位读者的队列,以及作者本人的队列。

5. 同一台Mongrel Server,或者另一台Mongrel Server,在处理某个Kestrel队列中的消息前,从这个队列的名称中解析出相应的用户ID,这个用户,既可能是读者,也可能是作者。

然后Mongrel从Kestrel队列中,逐个提取消息,解析消息中包含的短信ID。并从Row MemCached缓存器中,查找对应于这个短信ID的短信正文。

这时,Mongrel既得到了用户的ID,也得到了短信正文。接下去Mongrel就着手更新用户的主页,添加上这条短信的正文。

6. Mongrel把更新后的作者的主页,传递给正在空循环的Apache的进程。该进程把作者主页主动传送(push)给作者的浏览器。

如果读者的浏览器事先已经登录Twitter网站,建立连接,那么Apache给该读者也分配了一个进程,该进程也处于空循环状态。Mongrel把更新后的读者的主页,传递给相应进程,该进程把读者主页主动传递给读者的浏览器。

咋一看,流程似乎不复杂。“水库”,“引流”和“渠道”,这抗洪三要素体现在哪里呢?盛名之下的Twitter,妙处何在?值得细究的看点很多。

Reference,

[26] Twitter user statistics by Purewire, June 2009.
(http://www.nickburcher.com/2009/06/twitter-user-statistics-purewire-report.html)
[27] 截止2009年3月,中国移动用户数已达4.7亿户.
(http://it.sohu.com/20090326/n263018002.shtml)
[28] 中国移动飞信网. (http://www.fetion.com.cn/)
[29] 中国移动139说客网. (http://www.139.com/)

【6】流量洪峰与云计算

上一篇历数了一则短信从发表到被阅读,Twitter业务逻辑所经历的6个步骤。表面上看似乎很乏味,但是细细咀嚼,把每个步骤展开来说,都有一段故事。

美国年度橄榄球决赛,绰号超级碗(Super Bowl)。Super Bowl在美国的收视率,相当于中国的央视春节晚会。2008年2月3日,星期天,该年度Super Bowl如期举行。纽约巨人队(Giants),对阵波士顿爱国者队(Patriots)。这是两支实力相当的球队,决赛结果难以预料。比赛吸引了近一亿美国人观看电视实况转播。

对于Twitter来说,可以预料的是,比赛进行过程中,Twitter流量必然大涨。比赛越激烈,流量越高涨。Twitter无法预料的是,流量究竟会涨到多少,尤其是洪峰时段,流量会达到多少。

根据[31]的统计,在Super Bowl比赛进行中,每分钟的流量与当日平均流量相比,平均高出40%。在比赛最激烈时,更高达150%以上。与一周前,2008年1月27日,一个平静的星期天的同一时段相比,流量的波动从平均10%,上涨到40%,最高波动从35%,上涨到150%以上。

Figure 8. Twitter traffic during Super Bowl, Sunday, Feb 3, 2008 [31]. The blue line represents the percentage of updates per minute during the Super Bowl normalized to the average number of updates per minute during the rest of the day, with spikes annotated to show what people were twittering about. The green line represents the traffic of a “regular” Sunday, Jan 27, 2008.
Courtesy http://farm3.static.flickr.com/2770/4085122087_970072e518_o.png

由此可见,Twitter流量的波动十分可观。对于Twitter公司来说,如果预先购置足够的设备,以承受流量的变化,尤其是重大事件导致的洪峰流量,那么这些设备在大部分时间处于闲置状态,非常不经济。但是如果缺乏足够的设备,那么面对重大事件,Twitter系统有可能崩溃,造成的后果是用户流失。

怎么办?办法是变买为租。Twitter公司自己购置的设备,其规模以应付无重大事件时的流量压力为限。同时租赁云计算平台公司的设备,以应付重大事件来临时的洪峰流量。租赁云计算的好处是,计算资源实时分配,需求高的时候,自动分配更多计算资源。

Twitter公司在2008年以前,一直租赁Joyent公司的云计算平台。在2008年2月3日的Super Bowl即将来临之际,Joyent答应Twitter,在比赛期间免费提供额外的计算资源,以应付洪峰流量[32]。但是诡异的是,离大赛只剩下不到4天,Twitter公司突然于1月30日晚10时,停止使用Joyent的云计算平台,转而投奔Netcraft [33,34]。

Twitter弃Joyent,投Netcraft,其背后的原因是商务纠葛,还是担心Joyent的服务不可靠,至今仍然是个谜。

变买为租,应对洪峰,这是一个不错的思路。但是租来的计算资源怎么用,又是一个大问题。查看一下[35],不难发现Twitter把租赁来的计算资源,大部分用于增加Apache Web Server,而Apache是Twitter整个系统的最前沿的环节。

为什么Twitter很少把租赁来的计算资源,分配给Mongrel Rails Server,MemCached Servers,Varnish HTTP Accelerators等等其它环节?在回答这个问题以前,我们先复习一下前一章“数据流与控制流”的末尾,Twitter从写到读的6个步骤。

这6个步骤的前2步说到,每个访问Twitter网站的浏览器,都与网站保持长连接。目的是一旦有人发表新的短信,Twitter网站在500ms以内,把新短信push给他的读者。问题是在没有更新的时候,每个长连接占用一个Apache的进程,而这个进程处于空循环。所以,绝大多数Apache进程,在绝大多数时间里,处于空循环,因此占用了大量资源。

事实上,通过Apache Web Servers的流量,虽然只占Twitter总流量的10%-20%,但是Apache却占用了Twitter整个服务器集群的50%的资源[16]。所以,从旁观者角度来看,Twitter将来势必罢黜Apache。但是目前,当Twitter分配计算资源时,迫不得已,只能优先保证Apache的需求。

迫不得已只是一方面的原因,另一方面,也表明Twitter的工程师们,对其系统中的其它环节,太有信心了。

在第四章“抗洪需要隔离”中,我们曾经打过一个比方,“在晚餐高峰时段,餐馆常常客满。对于新来的顾客,餐馆服务员不是拒之门外,而是让这些顾客在休息厅等待”。对于Twitter系统来说,Apache充当的角色就是休息厅。只要休息厅足够大,就能暂时稳住用户,换句行话讲,就是不让用户收到HTTP-503的错误提示。

稳住用户以后,接下去的工作是高效率地提供服务。高效率的服务,体现在Twitter业务流程6个步骤中的后4步。为什么Twitter对这4步这么有信心?

Reference,

[16] Updating Twitter without service disruptions.
(http://gojko.net/2009/03/16/qcon-london-2009-upgrading-twitter-without-service-disruptions/)
[30] Giants and Patriots draws 97.5 million US audience to the Super Bowl. (http://www.reuters.com/article/topNews/idUSN0420266320080204)
[31] Twitter traffic during Super Bowl 2008.
(http://blog.twitter.com/2008/02/highlights-from-superbowl-sunday.html)
[32] Joyent provides Twitter free extra capacity during the Super Bowl 2008.
(http://blog.twitter.com/2008/01/happy-happy-joyent.html)
[33] Twitter stopped using Joyent’s cloud at 10PM, Jan 30, 2008. (http://www.joyent.com/joyeurblog/2008/01/31/twitter-and-joyent-update/)
[34] The hasty divorce for Twitter and Joyent.
(http://www.datacenterknowledge.com/archives/2008/01/31/hasty-divorce-for-twitter-joyent/)
[35] The usage of Netcraft by Twitter.
(http://toolbar.netcraft.com/site_report?url=http://twitter.com)

【7】作为一种进步的不彻底

不彻底的工作方式,对于架构设计是一种进步。

当一个来自浏览器的用户请求到达Twitter后台系统的时候,第一个迎接它的,是Apache Web Server。第二个出场的,是Mongrel Rails Server。Mongrel既负责处理上传的请求,也负责处理下载的请求。Mongrel处理上传和下载的业务逻辑非常简洁,但是简洁的表象之下,却蕴含着反常规的设计。这种反常规的设计,当然不是疏忽的结果,事实上,这正是Twitter架构中,最值得注意的亮点。

Figure 9. Twitter internal flows
Courtesy http://farm3.static.flickr.com/2766/4095392354_66bd4bcc30_o.png

所谓上传,是指用户写了一个新短信,上传给Twitter以便发表。而下载,是指Twitter更新读者的主页,添加最新发表的短信。Twitter下载的方式,不是读者主动发出请求的pull的方式,而是Twitter服务器主动把新内容push给读者的方式。先看上传,Mongrel处理上传的逻辑很简洁,分两步。

1. 当Mongrel收到新短信后,分配一个新的短信ID。然后把新短信的ID,连同作者ID,缓存进Vector MemCached服务器。接着,把短信ID以及正文,缓存进Row MemCached服务器。这两个缓存的内容,由Vector MemCached与Row MemCached在适当的时候,自动存放进MySQL数据库中去。

2. Mongrel在Kestrel消息队列服务器中,寻找每一个读者及作者的消息队列,如果没有,就创建新的队列。接着,Mongrel把新短信的ID,逐个放进“追”这位作者的所有在线读者的队列,以及作者本人的队列。

品味一下这两个步骤,感觉是Mongrel的工作不彻底。一,把短信及其相关IDs,缓存进Vector MemCached和Row Cached就万事大吉,而不直接负责把这些内容存入MySQL数据库。二,把短信ID扔进Kestrel消息队列,就宣告上传任务结束。Mongrel 没有用任何方式去通知作者,他的短信已经被上传。也不管读者是否能读到新发表的短信。

为什么Twitter采取了这种反常规的不彻底的工作方式?回答这个问题以前,不妨先看一看Mongrel处理下载的逻辑。把上传与下载两段逻辑联系起来,对比一下,有助于理解。Mongrel下载的逻辑也很简单,也分两步。

1. 分别从作者和读者的Kestrel消息队列中,获得新短信的ID。

2. 从Row MemCached缓存器那里获得短信正文。以及从Page MemCached那里获得读者以及作者的主页,更新这些主页,也就是添加上新的短信的正文。然后通过Apache,push给读者和作者。

对照Mongrel处理上传和下载的两段逻辑,不难发现每段逻辑都“不彻底”,合在一起才形成一个完整的流程。所谓不彻底的工作方式,反映了 Twitter架构设计的两个“分”的理念。一,把一个完整的业务流程,分割成几段相对独立的工作,每一个工作由同一台机器中不同的进程负责,甚至由不同的机器负责。二,把多个机器之间的协作,细化为数据与控制指令的传递,强调数据流与控制流的分离。

分割业务流程的做法,并不是Twitter的首创。事实上,三段论的架构,宗旨也是分割流程。Web Server负责HTTP的解析,Application Server负责业务逻辑,Database负责数据存储。遵从这一宗旨,Application Server的业务逻辑也可以进一步分割。

1996年,发明TCL语言的前伯克利大学教授John Ousterhout,在Usenix大会上做了一个主题演讲,题目是“为什么在多数情况下,多线程是一个糟糕的设计[36]”。2003年,同为伯克利大学教授的Eric Brewer及其学生们,发表了一篇题为“为什么对于高并发服务器来说,事件驱动是一个糟糕的设计[37]”。这两个伯克利大学的同事,同室操戈,他们在争论什么?

所谓多线程,简单讲就是由一根线程,从头到尾地负责一个完整的业务流程。打个比方,就像修车行的师傅每个人负责修理一辆车。而所谓事件驱动,指的是把一个完整的业务流程,分割成几个独立工作,每个工作由一个或者几个线程负责。打个比方,就像汽车制造厂里的流水线,有多个工位组成,每个工位由一位或者几位工人负责。

很显然,Twitter的做法,属于事件驱动一派。事件驱动的好处在于动态调用资源。当某一个工作的负担繁重,成为整个流程中的瓶颈的时候,事件驱动的架构可以很方便地调集更多资源,来化解压力。对于单个机器而言,多线程和事件驱动的两类设计,在性能方面的差异,并不是非常明显。但是对于分布式系统而言,事件驱动的优势发挥得更为淋漓尽致。

Twitter把业务流程做了两次分割。一,分离了Mongrel与MySQL数据库,Mongrel不直接插手MySQL数据库的操作,而是委托MemCached全权负责。二,分离了上传和下载两段逻辑,两段逻辑之间通过Kestrel队列来传递控制指令。

在John Ousterhout和Eric Brewer两位教授的争论中,并没有明确提出数据流与控制流分离的问题。所谓事件,既包括控制信号,也包括数据本身。考虑到通常数据的尺寸大,传输成本高,而控制信号的尺寸小,传输简便。把数据流与控制流分离,可以进一步提高系统效率。

在Twitter系统中,Kestrel消息队列专门用来传输控制信号,所谓控制信号,实际上就是IDs。而数据是短信正文,存放在Row MemCached中。谁去处理这则短信正文,由Kestrel去通知。

Twitter完成整个业务流程的平均时间是500ms,甚至能够提高到200-300ms,说明在Twitter分布式系统中,事件驱动的设计是成功。

Kestrel消息队列,是Twitter自行开发的。消息队列的开源实现很多,Twitter为什么不用现成的免费工具,而去费神自己研发呢?

Reference,

[36] Why threads are a bad idea (for most purposes), 1996.
(http://www.stanford.edu/class/cs240/readings/threads-bad-usenix96.pdf)
[37] Why events are a bad idea (for high-concurrency servers), 2003.
(http://www.cs.berkeley.edu/~brewer/papers/threads-hotos-2003.pdf)

 

 

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值