原文链接:http://highscalability.com/blog/2014/2/26/the-whatsapp-architecture-facebook-bought-for-19-billion.html

原文写于2014年2月26日,以下是译文:

在雅虎曾经用C++写过高性能信息通道的Rick Reed并不是高扩展性架构世界中的新手。跟他一起的创业者们都是有丰富可扩展系统经验的前雅虎人。所以WhatsApp在可扩展性上的实力非常强。而且由于他们的目标就是让世界上每一台智能机( 几年内总数将达到50亿)都装上WhatsApp,这一经验对他们来说至关重要。

在进入正题之前,我们先来花点时间看看:为什么WhatsApp对于脸书来说值190个亿?

作为一个编程人员,如果你问我WhatsApp是不是值这么多钱,我肯定回答不是!它只是在网络上传送一些东西。但是我也是一个认为我们不需要博客平台的人,因为远程登录你的服务器,用vi改改index.html文基恩,然后在HTML里写你的博文,能有多难呢?后来我认识到做这些事情的代码有多愚蠢不是关键,关键是让用户喜欢使用你的产品。你不能买到人们的喜爱。

那是什么让WhatsApp这么值钱呢?技术?忽视那些说他们可以在一周内用PHP写出一个WhatsApp来的人们。这是不可能的。WhatsApp里面确实用到了一些比较厉害的技术。但是如果脸书想的话,绝对有这个实力来实现一个WhatsApp。

让我们来看一看它的一些功能。我们知道WhatsApp是一个没有广告、花招和游戏,但是有来自全世界的忠实用户的产品。它在一个短信收费严重的世界里提供免费的短信服务。作为一个美国人,最让我惊讶的是有那么多真实的用户用WhatsApp来与家人朋友联系。当你登陆WhatsApp的时候,很可能发现你认识的很多人已经在使用它了。由于每个人都有一个手机,这也减少了有些人不使用社交网络的问题。WhatsApp积极的在不同的手机平台上发布,使得每个你认识的人都可以使用它,而且它just works。“just works”是一个常用的短语。它代表所有的含义(共享位置、视频、音频、图片、按住讲话、语音短信和照片、读收据、群聊、通过WIFI发送信息、以及不论收件人在不在线这一切都可以完成)。它可以支持各种语音,使用你的手机号码作为身份认证,你的手机联系人列表就是你的社交图。这一些都极其简单。不需要邮件认证、用户名和密码、也不需要信用卡号码。所以它just works。

这些都很厉害,但是也不知190亿美金。其他产品也可以做到这些功能。

一个原因可能是谷歌也想要买它,因为它是一个威胁,因为多一个用户可以赚上99美分。而脸书则是非常急切的需要它,为了用户手机中的联系人列表,也为了一些其他数据(即使WhatsApp什么也不存)。

这一切都是为了4亿5千万活跃用户,并且每天增加一百万用户,潜在用户数量达到10亿。脸书需要WhatsApp来获得它的下一个十亿用户。这肯定是一部分原因。况且一个用户40美金也不是很离谱,尤其是190亿里的大头是用股票来支付的。脸书收购Instagram时一个用户的价格是30美金。一个推特用户则价值110美金。

Benedict Evans曾经估算过,移动是一个超过一万亿美金的生意。WhatsApp颠覆的是这个生意里短信那一部分。全球短信系统年收入超过1千亿,一天发送200亿条短信,而WhatsApp一天发送180亿条。如今的大趋势是从PC端转到更普遍的智能机端,这个机遇的规模要比现在脸书所在的市场大得多。

但是脸书保证没有广告和干扰,那它能获得什么呢?

这里有些很有趣的移动端商业化的例子。WhatsApp被用来创建一些项目组的群聊,还有一些风投会用它来进行交易流程的对话。

Instagram在科威特用来卖羊。

微信,一个WhatsApp的竞争者,在一月开通了出租车叫车服务。第一个月就叫了2千1百万辆次出租车。

电子商务的未来看来会通过一些移动短信app来实现,但这仅仅只局限于电子商务吗?

其实不只有商业会使用WhatsApp来替代以前在电脑或网页应用。西班牙的警察使用WhatsApp来抓捕罪犯。意大利的人们用它来组织篮球赛。

商业和其他应用开始向移动端转移有很充足的理由。每个人都有手机,而且这些短信应用很强大、免费、用起来成本很低。你不再需要电脑或者网页应用来做这些事了。很多功能可以再一个短信app上实现。

所以短信对谷歌和脸书来说是一个威胁。电脑正在不流行,web也在不流行。短信加移动端构建了绕过传统渠道的一个完整生态系统。短信取代搜索成了移动端的中心,改变了事物被发现的途径,也决定了什么样的应用能在未来胜出。

脸书想不想要进入这个市场?

随着转向移动端,我们可以看到脸书的去门户化。脸书的电脑网页界面是门户风格的,提供了所有后端所能提供的功能的入口。它很大、很复杂、很容易出问题。谁真的喜欢脸书的UI?

当脸书像移动端倾斜时,他们也尝试过门户风格,但行不通。所以他们采用了一系列更小更专注的不同目的的app。移动为先!你在这么小的屏幕上只能做有限的事情。在移动端更容易找到一个特定的app,而不是在一个复杂的门户式应用的重重菜单中找到你想要的东西。

但是脸书走的更远。他们不仅仅提供各种app,他们还提供多个互相竞争的功能类似的app,而这些app可能都不使用相同的后端基础设施。比如Messenger和WhatsApp,Instagram和脸书的照片app。Paper是另一个脸书的界面,只提供很少的功能,但是在这些功能上能做的很好。

康威定律在这里也成立。它的大意是“设计系统的大型机构会受到自身限制而做出和自身的通信构架相似的设计”。有一个大块头的后端基础架构,我们就会有一个城堡一样的前端门户设计。转向移动端把大公司从这种思路中解放了出来。如果应用可以只提供一部分脸书基础架构的功能,那应用就可以完全不依赖与脸书的基础架构。如果它们不需要脸书的基础架构,那它们也不需要有脸书来创建。那这个时候脸书还是脸书吗?

脸书的CEO Mark Zuckerberg有自己的理解。在移动端世界大会上他说脸书收购WhatsApp是与Internet.org的愿景高度相关的:

我们的宗旨是发展一系列免费的基础互联网服务 -- “一个互联网的911”。这些服务可以使像脸书一样的社交网络服务、一个短信服务、搜索、或者像天气一类的服务。 向用户提供这些免费服务就像是一种容易上瘾的药  -- 能够承担的起数据服务和手机的用户就不会想要花钱来获得这些服务。这会使得他们觉得自己很重要,从而花钱购买其它类似的服务。 -- 或者我们希望如此。

这是一个长期的目标,需要有巨大的值钱的股票才能玩得起。

那我们有结论了吗?不见得。这毕竟是一大笔钱,而直接的好处却不是很明显,尽管长期目标的解释有一定的道理。我们仍然实在移动的早期。没人知道未来是什么样子的,所以为了让未来不会像过去那样是需要付出代价的。脸书正在这么做。

这个话题就到此为止了。让我们来看看32个工程师是怎么支持4亿5千万活跃用户的。

信息来源:

预警:我们并不知道很多关于WhatsApp的整体架构。只有从很多来源得到的一星半点信息。Rick Reed的演讲是关于如何用Erlang来让每台服务器可以支持2百万连接的优化过程,非常有趣,但是不是一个关于整体架构的演讲。我们的来源有:

  • Rick Reed 2012 的演讲 Scaling to Millions of Simultaneous Connections
  • Rick Reed 的Erlang Factory interview
  • An interview with Eugene Fooksmam from WhatsApp
  • DLD14 - What's up WhatsApp? (Jan Koum, David Rowan)
  • yowsup是一个WhatsApp的API的开源版。因为DMCA的关闭,这个代码库已经无法下载了。但是他们确实记录了一些WhatsApp的内部工作。比如,所有事情都会搞多样化。
  • 一些在相关文献中提到的来源。

数据:

这些数据大部分来自现有系统,而不是演讲时的那个系统。那个演讲会包括更多的数据存储的改动、消息、大集群和更多的BEAM/OTP的补丁。

  • 4亿5千万活跃用户,比任何公司更快的达到了这一数字
  • 32个工程师,每个人支持1千4百万活跃用户
  • 七个平台上每天500亿条信息(收+发)
  • 每天超过1百万新增用户
  • 广告投入为0
  • Sequoia Capital投了6千万;回报是34亿
  • 脸书35%的现金用在了这单交易中
  • 数百个节点
  • 大于8000个核
  • 数百TB的RAM
  • 每秒超过7千万条Erlang消息
  • 2011年WhatsApp在单一服务器上有1百万tcp会话连接,还能有内存和CPU的剩余;在2012年可以有2百万tcp连接;2013年WhatsApp在推特上公告,12月31号产生了一个新纪录:收了70亿短信+发了110亿短信=总的一天180亿条短信!2013年快乐!!!

平台:

后端:

  • Erlang
  • FreeBSD
  • Yaws, lighttpd
  • PHP
  • BEAM的特制补丁(BEAM就像JAVA的JVM,但是是Erlang的)
  • 特制XMPP
  • 可能在Softlayer中运行

前端:

  • 七个客户端平台:iPhone、安卓、黑莓、诺基亚塞班S60、诺基亚S40、Windows Phone、?
  • SQLite

硬件:

标准的涉及用户的服务器:

  • 两个Westmere Hex-core(24个逻辑CPU)
  • 100GB RAM, SSD;
  • 双NIC(一个公有连接用户的网络,一个私有与后端相连的网络)

产品:

  • 关注消息。连接全球的人们,不管他们在哪里,也不需要他们花很多钱。创立人Jan Koum还记得在1992年时要和全世界不同地方的家人们保持联系有多难。
  • 私密性。这与Jan Koum在乌克兰长大有关,那里没什么东西是保密的。消息不会被存在服务器上;聊天记录也不被保存;目标是对用户了解的越少越好;不知道用户的姓名和性别;聊天记录只在用户的手机上。

概述:

1. WhatsApp的服务器几乎完全基于Erlang:

  • 后端短息路由系统是由Erlang实现的
  • 一个巨大的成就是只有很少的服务器就管理了这么多的活跃用户。WhatsApp团队一致认为这很大程度上市Erlang的功劳。
  • 有趣的是2009年时Facebook Chat也是用Erlang写的,但是后来他们放弃了,因为很难找到合适的程序员。

2. WhatsApp的服务器从ejabberd启动:

  • Ejabberd是一个著名的开源Jabber 服务器的Erlang版
  • 一开始选它是因为它是开源的,代码经过很多程序员审核,容易入手,并且从长远来看Erlang对大型通信系统十分合适
  • 接下来的几年花在了重写和改变ejabberd的部分代码上,包括从XMPP换到一个内部开发的协议、修改代码结构、重新设计一些核心组件、以及对Erlang VM做出了许多重要改变来提高服务器性能

3. 一天处理5百亿条消息的重点在于有一个可靠、可用的系统。赚钱是以后的事情,还有很远的路要走。

4. 一个主要的衡量一个系统是否健康的标准就是消息队列的长度。一个服务器上所有进程的消息队列长度都被实时监控着,一旦超过某一个预设的值就会发出警报。一旦一个或多个进程发出警报,那就说明这是下一个需要解决的瓶颈。

5. 发送多媒体消息时,图片、音频或视频会被上传到一个HTTP服务器,然后这个内容的连接以及一个用Base64编码的预显示内容(如果有的话)会被发送出去。

6. 有些代码每天都会被改变。经常是一天几次,虽然通常会避开高峰时刻。Erlang有助于积极地上线修复和新的功能。热加载意味着更新可以在不用重启或者转移负荷的情况下启用。错误经常很快就能通过热加载来纠正。系统设计地比较松耦合,使得递增地推出新的东西变得很容易的。

7. WhatsApp用了什么协议?SSL通信管道通向WhatsApp的服务器池。所有的消息都在服务器的队列上,直到客户端下一次连接然后取走消息。成功获取一条消息的状态会被送回WhatsApp的服务器,然后被转发个发送消息的那个人(他会看到这条消息旁边出现了一个勾)。一旦客户端接收的这些消息,它们就会在服务器的内存中被删除。

8. WhatsApp内部的注册过程是怎么实现的?WhatsApp曾经基于手机的IMEI号码创建一个用户名密码。但是最近修改了这一机制。WhatsApp的客户端现在通过一个通用API发送一个独特的5位PIN。WhatsApp然后会发送一条短信到这个手机号码(这意味着WhatsApp的用户不在需要使用同一部手机了)。根据这个PIN,客户端会请求一个独特的键。这个键就被当做未来所有请求的密码。(这个永久的键被存在手机中。)这也意味着注册一台新的设备会使得老设备上的键失效。

9. 安卓平台上使用了谷歌推送服务。

10. 安卓平台上有更多的用户。安卓开发也更友好一些。开发者可以开发一个功能的初始版,然后一个晚上就可以把它推送到亿万用户的机器上。如果有什么问题就可以马上修复。在iOS上就不行。

每台服务器超过2百万连接:

1. 巨大的用户增长,这是一个幸福的烦恼,但这也意味着要花很多钱买更多的设备以及增加管理这些设备的复杂程度。

2. 需要为负荷的激增做好计划。例如西班牙或者墨西哥有足球比赛或者地震的时候。这些通常在本来就是高峰的时候发生,所以需要有足够的空闲带宽来处理高峰+激增。一次最近的足球比赛在原本的高峰期基础上又产生了35%的发送增量。

3. 原本的服务器可以同时处理200个连接:

  • 可以推断需要很多的服务器来满足增长需要
  • 服务器在突然到来的负荷面前很脆弱。网络故障和其他问题会出现。需要解耦各个部件使它们在高峰期不那么脆弱
  • 目标是一台服务器1百万个连接。这是在他们可以实现20万连接时定下的极有野心的目标。还要让服务器有多余的容量来处理世界性活动、硬件故障以及其它各种问题意味着要有足够的弹性来处理高负荷和故障。

用来增加可扩展性的工具和技术:

1. 系统主动汇报工具(wsar):

  • 记录一个系统的各种数据,包括OS数据、硬件数据、BEAM数据。它被设计成可以很容易从其他系统插入测量数据的模式,例如虚拟内存、跟踪CPU使用率、总体使用率、用户时间、系统时间、中断时间、上下文转换、系统调用、陷阱、收/发包、所有进程队列中的消息总数、繁忙端口事件、通讯负荷率、输入输出比特数、调度数据、垃圾回收数据、收集到的文字、等等
  • 原来每分钟跑一次。随着系统性能提升,需要每秒运行一次,不然有些事件在一分钟的精确度上是发现不了的。现在数据非常精确,可以看到所有部件是如何工作的。

2. CPU中的硬件性能计数器(pmcstat):

  • 可以看到CPU在哪里花了多少比例的时间。可以看到多少时间被用在了仿真器的循环上。在他们的例子上,只有16%的时间用来执行仿真器中的代码上了。也就是说即使他们一点也不运行Erlang代码,也就只能节省16%的时间。意味着他们应该在其他地方想办法提升系统的性能。

3. dtrace、内核锁计数、fprof:

  • Dtrace主要用于debug,而不是性能
  • 在FreeBSD上给BEAM打补丁来获得CPU时间戳
  • 写了一些脚本,把所有进程的信息汇总到一个地方来显示时间都花在了哪些步骤上
  • 最大的收获是在编译仿真器的时候开启锁计数

4. 一些问题:

  • 早期看到很多时间或在垃圾回收上,这个已经降低了
  • 看到一些网络堆栈上的问题,也被解决了
  • 最大的问题在于仿真器中的锁竞争,可以从锁计数的输出中发现

5. 测量:

  • 合成负荷,那些从你自己的测试脚本中产生的负荷,在用户规模非常大的时候,对于用户会接触到的系统的测试作用很有限:

    • 对于简单的接口很有用,比如一个用户表格,尽快的产生插入和读取
    • 如果要测试一台服务器支持1百万个连接,需要让30台机器打开足够的IP端口,才能产生足够的连接。对于2百万连接就需要60台机器。这么大的规模就很难测试。
    • 产品线上可以看到的负荷的类型也很难产生。可以假设一个正常的负荷是什么类型的,但是其实因为社交活动、世界性的活动、在不同的平台上用户的行为不同、不同的国家,这些因素都会导致负荷的类型变化很大
  • Tee‘d负荷:
    • 把正常的产品线上的负荷复制一部分到一个分离的系统
    • 有效地把副作用限制住了。我们不希望复制负荷的同时改变一个用户的永久状态或者导致发送重复的信息给用户
    • Erlang支持热加载,所以在满负荷下,如果有一个新想法,可以在程序运行的同时编译并加载这个变化,并且马上看到这个变化时变好了还是变坏了
    • 增加一些旋钮来动态调节产品线负荷,并观察性能会如何收到影响。可以一边输出CPU使用率、VM使用率、监听队列溢出等数据,一边调节旋钮看整个系统如何反应
  • 真实产品线负荷:
    • 终极测试,同时输入和输出。
    • 在DNS中重复写入一个服务器的信息,所以它会得到两倍或三倍于正常数值的负荷。制造一些TTL的问题来模拟客户端不适用DNS规定的TTL或者出现了延时,所以不能很快地针对负荷过载进行处理。
    • IPFW。把负载从一台服务器转移到另一台,这样可以使一台机器正好有那么多客户端的连接。但是一个bug可能引起内核恐慌,所以不是很行的通。

6. 结果:

  • 从一台服务器有20万个并发连接开始
  • 第一个瓶颈是42万5千个连接。系统一直处于竞争状态,真正的活没人干了。测量调度器数值来观察多少有用的工作正在执行、或睡眠、或循环。发现在负荷下,它一直试图获取睡眠锁,所以整个系统CPU使用率只有35%-45%,但是调度器有98%的使用率
  • 第一轮修复就达到了1百万的连接:
    • VM利用率在76%。CPU在73%。BEAM仿真器的利用率为45%,与用户进程比例很接近。这是一个好事,因为仿真器就是以用户权限运行的。
    • 通常CPU利用率并不是一个很好地衡量一个系统有多忙的数据,因为调度器也是用CPU
  • 一个月后,遇上了2百万连接的瓶颈
    • BEAM的利用率在80%,很接近FreeBSD开始换页的极限。CPU也差不多。调度器开始竞争状态,不过总的还是运行的比较稳定。
  • 似乎不太需要继续优化Erlang代码了:
    • 原来一个连接有两个Erlang进程,把数量降到了1个
    • 对计时器做了同样的事情
  • 最高峰时每台服务器有2百80万连接:
    • 每秒57万1千个包,超过20万条消息
    • 做了一些内存优化,把VM负荷降到了70%
  • 试过3百万连接,但是失败了
    • 当系统出问题时,发现了很长的消息队列,不管是一个消息队列还是消息队列的总和
    • 把每个进程的消息队列数据加到了BEAM的管理界面中,显示多少消息被接收和发送,以多快的速度
    • 每十秒钟采样一次,可以看到一个进程的消息队列中有60万消息,每秒处理4万条消息需要15秒。算上期间接收的新消息,总的消化时间需要41秒。

7. 发现:

  • Erlang + BEAM + 他们的修复 -- 拥有很牛逼的SMP可扩展性。近乎线性的扩展性。非常厉害。一个24通道的机器运行这套系统处理产品线的负荷会占用85%的CPU,它可以一直运行下去而不出什么问题:

    • 这是对Erlang编程模型的一个证明
    • 一个服务器运行的时间越长,就会有越多长时间的连接,这些连接几乎没有负荷,所以它就可以接受更多的连接
  • 竞争是最大的问题:
    • 他们的Erlang代码里有一些针对BEAM竞争问题的修复
    • 一些BEAM的补丁
    • 分割负载,这样每个CPU的负载就小了
    • 当前时间锁。每次一条消息离开一个端口的时候它会更新当前时间。这是一个所有调度器共享的锁,意味着所有的CPU都会用到这个锁。
    • 优化计时器的使用。不使用bif计时器。
    • 检查IO时间表会导致指数增长。创建的VM有一个哈希表,会被重复分配。使用几何分配这个表可以优化性能。
    • 增加一个文件来记录所有你已经打开的端口可以减少端口竞争
    • Mseg分配原本是一个所有分配器之间的竞争。把它的竞争范围所减到每个调度器的范围。
    • 在接受一个连接的时候有很多端口之间的交互。修改一些选项可以减少昂贵的端口间交换。
    • 当消息队列变大时,垃圾回收会使整个系统不稳定。所以暂停垃圾回收知道信息队列没有那么大时才进行。
  • 避免一些常犯的错误:
    • 一个从FreeBSD 9引入FreeBSD 8的TSE时间计数器。这是一个更快的读计数器。可以更快的拿到当前时间,比查询一个芯片要快。
    • 从FreeBSD 9引入igp网络驱动,以解决多个网卡上队列查询的问题
    • 增加文件和网络管道的数量
    • pmcstat显示很多时间花在网络堆栈查询PCB上。所以扩大哈希表的容量可以加快查询速度
  • BEAM补丁:
    • 之前提到的工具补丁。增加调度器仪表盘来获取使用率信息、消息队列的数据、睡眠次数、发送速率、消息计数、等等。这些可以通过Erlang代码和procinfo来做,但是在1百万连接时太慢了
    • 数据收集是很有效的,因为它们可以运行在产品线中
    • 数据有三个不同时间间隔:1、10、100秒。这样可以观察到不同的问题。
    • 让锁计数可以在更大的异步线程中工作
    • 增加debug选项来debug锁计数器
  • 调试:
    • 设置一个较低的调度器醒来阈值,否则调度器会一直睡眠
    • 优先mseg分配器,而不是malloc
    • 每个调度器的每个实例都有一个分配器
    • 把页的值设的比较大。让FreeBSD使用超级页。减少TLB找不到的概率,提升同一个CPU的吞吐量
    • 把BEAM运行在实时优先级上,这样其他例如cron的任务就不会打扰调度,可以防止导致重要用户负荷堆积的小问题
    • 降低空循环次数,所以调度器不会空循环
  • Mnesia
    • 优先os:timestamp,而不是erlang:now
    • 不要使用交易,但是远程复制会堆积起来。并行复制表格可以增加吞吐量
  • 有很多改变都没有在这里列举出来

教训:

  • 优化是一个又黑暗又肮脏的活,只适合巨怪和工程师。当Rick做出这些改变使服务器可以接受2百万连接时,有很多工作要做:写工具、运行测试、引入代码、在技术堆栈的每一层加入仪表盘、调试系统、观察数据、与很底层的细节打交道以及试图理解所有的事情。这就是去除瓶颈,把性能和可扩展性推到极致索要付出的代价。
  • 获取你需要的数据。写工具。给工具打补丁。添加旋钮。Ken不停的修改这个系统来获取他们需要的数据,不停地写工具和脚本来管理他们的数据,优化这个系统。做任何需要的事情。
  • 测量。去除瓶颈。测试。重复这一过程。这是你该做的。
  • Erlang太棒了!Erlang不断证明它可用来写一个多功能的、可靠地、高性能的平台的能力。尽管我个人对这个说法有一定的保留,毕竟它还需要这么多的调试和打补丁。
  • 解决有问题的代码就可以获利。
  • 现在价值和员工数量正式没有关系了。现在有很多可利用的工具。一个先进的全球通信基础设施使WhatsApp这样的应用成为可能。如果WhatsApp需要先建立一个网络或造一个手机,这就不可能发生。强大又便宜的硬件和开源软件当然又是一大利器。这些正确的产品在正确的时间正确的地点出现在了正确的购买者面前。
  • 关注用户很重要。WhatsApp致力于成为一个简单的消息应用,而不是一个游戏网络,或者一个广告网络,或者一个消失的照片网络。这对他们很重要。这让他们不打广告、在增加新功能的时候保持应用的简单、以及在任何手机上实现“just works”这一理念。
  • 由于简单而导致的限制是可以接受的。你的身份与手机号码绑定。所以如果你换了手机号码,你的身份也没了。这个不像电脑。但是这让整个系统在设计上简单了很多。
  • 年龄什么也不是。如果2009年的时候WhatsApp的创始人Brian Acton是因为年龄歧视而没有获得推特和脸书工作的话,那真是一种讽刺。
  • 开始要简单,然后再变化。一开始服务器端是基于ejabberd。它后来被重写了,但是那只是Erlang方向的第一步。后来Erlang的可扩展性、可靠性和可维护性得到了越来越多的应用。
  • 保持很少的服务器。一直保持尽可能少的服务器数量,同时又保证足够的冗余来处理一些活动引起的短期陡升的消息量。分析和优化直到它们不起作用,然后部署更多的硬件。
  • 有意提供冗余的硬件。这样可以保证用户在节日期间有不间断的服务,而雇员也可以享受假日,不用把整个假日用来修复过载问题。
  • 当你收钱时增长就会放缓。当WhatsApp免费时,增长超级快,一天有1万个下载。当收费之后就下降到1千一天。在年底,当有了图片消息之后,他们先是收一个一次性的下载费,后来改成年费。
  • 创意来自最奇怪的地方。经常忘记Skype账号用户名和密码的经历导致了做一个“just work”的应用的热情。
05-11 13:03