Rails系统性能优化之路

这篇文章讲述的是我们在一个Rails on Jruby系统的性能优化之路上披荆斩棘的故事。

优化之前

在开始性能优化之前,有几点必须明确:

1. 性能优化的对象:并不是所有页面都需要优化,而且首先应该选择那些访问率最高、性能瓶颈最大的页面来进行优化。

2. 性能优化的目标:性能优化必须有一个具体的目标,即要达到的响应时间和吞吐量。有了目标,我们就知道目前离目标的距离,需要优化的力度;同时,也知道在何时停止优化。

3. 伴随优化的测量:没有测量的性能优化很可能让你缘木求鱼。

优化之路

下面,分别从多个方面看一下性能优化中的一些实践。

缓存

缓存是性能优化的一大利器。Rails本身对缓存机制有很好的支持。

客户端缓存

一般而言,对于客户端缓存的原则是:对动态html页面不作任何缓存,永久缓存任何其它类型的文件。

Rails很好地支持了这个原则。比如:

stylesheet_link_tag(”application”)

生成的页面元素是:

<link href=”/stylesheets/application.css?1232285206″ media=”screen” rel=”stylesheet” type=”text/css”/>

大家都注意到在css文件后面有个时间戳,这其实是一个小计策:当文件发生改变的时候,时间戳也发生了改变;而客户端会认为这是一个新链接,就会重新获取文件。有了这种机制的支持,我们可以很放心地在客户端对静态文件进行永久缓存,而不用担心过期问题。

服务端缓存

Rails提供了三种服务器端缓存方式:page cache,action cache和fragment cache。对于动态页面,fragment缓存使用的机会会比较多。

让我们看一个例子:一个产品页面片段,片段上面的部分信息对于所有用户都是一致的,但另一部分对于不同权限用户是不一致的。在admin登录时,能看到a链接,b按钮和c复选框;在一般用户登录时,能看到b按钮和c复选框;未登录用户只能看到c复选框。

一种很简便的缓存策略是根据用户标志进行缓存,即对用户p1缓存,对p2也进行缓存,那么我们就可以根据用户的标识以及产品的标志来定位缓存。

但是,这种缓存策略存在一个问题,即缓存数量太大:同一个产品片段存在n份不同的拷贝,n = admin用户的数量 + 一般用户的数量 + 1 (未登录用户)。而且,对于同一种权限的用户来说,他们对同一个产品的缓存是一样的。

消除这种重复的策略是根据用户的类型来对缓存进行分类,那么对于一个产品而言,它只会有3份并且不重复的缓存,3 = 1 (admin权限) + 1 (一般权限) + 1 (未登录用户)。

这 可能是一个比较简单的例子,比较容易想到。但在一些很复杂的情况下,可能就会迷惑。其实原则就是:消除重复,根据片段的根本特征差别来对缓存进行分类。但 这里引起了另外一个问题:如果根本特征的区别需要对很多数据进行大量的计算,那么缓存就失去了它的意义。所以,要把握好权衡。缓存,最重要的是为了减少数 据的重复获取,减少重复计算。

另外,缓存的清理策略是至关重要的:何时清除缓存,以及如何定位失效缓存。对于要求实时性的页面,可以使用 rails提供的sweeper机制来进行缓存清理。但sweeper有个不方便的地方是需要对相关的action都逐一进行声明。我们的系统巧妙地利用 了rails的observer:在observer检测到数据的更新时对相关缓存进行清理。

计算结果缓存

在一个request的生命周期之内,有些数据不会改变,或者我们不关心改变,则可以通过对结果缓存以避免重复计算。

def length
@length ||= end – start
end

内存

来看个例子:我们要在一个页面中显示一百个产品的信息,产品信息从一个信息搜索平台获取。下面的代码是从搜索结果数据集创建产品对象:

records.map { |record| Product.new record }

但这里有个问题,Product是一个ActiveRecord类,但页面显示结果并不需要这么“重量级”的对象。取而代之以轻量级的对象会减少内存的消耗,并提升速度。

数据库

性能的瓶颈往往不在语言级别,而在IO上。对数据库的优化是重中之重。

在数据库的优化上有一些耳熟能详的方法,比如:

  • 增加适当的索引以加快查询。有些很好的工具可以帮助我们找到性能的瓶颈,比如分析execution plan。
  • 缩小transaction的粒度以减少锁的见时间。
  • 避免多次创建transaction的开销(如下代码)
Product.transaction do
search_results.each do |search_result|
Product.create(
search_result)
end
end


大型查询

对于像报表类的大型查询,要绕过ActiveRecord,而直接使用数据库驱动。同时,对于多表连接的查询,可以考虑几种方法来优化:

  • 引入中间表,让另外一个进程定时把查询结果插入中间表。但这会牺牲一定的实时性。
  • 避免重复结果的获取,减少结果集。这往往出现在一对多表之间,连接会导致“一”这边出现多条重复结果。可以考虑把一个查询分拆成多个查询。
  • 使用存储过程或者functions。当然,这失去的是业务逻辑的透明性和灵活性。

Rails的finder

接下来,探讨一下如何正确使用Rails的finder。Rails的finder是很容易误用或者不合理使用的地方,往往有很多性能问题因此而起。

在使用finder时,要搞清楚几个问题:

  • 是否需要结果集中的所有数据?
  • 是否需要预先加载?

表的扫描是邪恶的,很吃性能。一定要减少获取的结果集,用:select指定需要的字段。配合以创建正确的数据库索引,并在索引上挂载其它需要的字段,避免表的扫描。

正确使用预先加载可以避免n+1查询:

Company.all(:include => :products:conditions => “company.kind = ‘toy’”)

产生的sql查询是:

SELECTFROM companies WHERE kind = ‘toy’

SELECTFROM products WHERE products.company_id IN (12, 423, 431…)

但错误使用预先加载是个很危险的事情,它可能不会影响结果的正确性,但会引起很严重的性能问题:

Company.all(:include => :products:conditions => “products.id IS NOT NULL AND products.weight > 10″)

其实写这个查询的人的目的是为了找出拥有products,并且products的weight大于10的company。但这个语句导致的sql查询是性能低下的:

SELECT companies.id AS t0_r0, …., products.id as t1_r0, … FROM companies
LEFT
OUTER JOIN products ON products.company_id = companies.id
WHERE products.id IS NOT NULL AND products.weight > 10

这个sql查询有两个问题:

  • 结果集中的products信息是不需要的
  • LEFT OUTER JOIN的性能劣于INNER JOIN

我们可以使用如下的语句来避免这两个问题:

Company.all(:joins=> “INNER JOIN products ON products.company_id = companies.id, :conditions => products.weight > 10)

它生成的sql是:

SELECT companies.* FROM companies
INNER
JOIN products ON products.company_id = companies.id
WHERE products.weight > 10

这个查询的效率会高很多。所以,正确使用Rails的finder是至关重要的。

页面

View的helper方法生成html元素,比如:

link_to “My Company”, company_path(@company)

# => <a href=”/companies/1″>My Company</a>

过度使用helper方法会引起性能问题。比如上面这个方法,每次都会调用link_to和company_path — 从routes中解析path。

但注意,只有在过度使用的时候,才需要考虑是否可以减少helper方法的使用来提升性能。同时,也可以用一些工具替代ERB来提升性能,比如erubis:它通过预处理避免每次调用的重复开销。

多线程

版本2.2之后,Rails终于是线程安全的了,意味着我们可以开启多线程模式,这绝对是一个巨大的进步。

非线程安全的Rails,无法有效利用共享资源。在以前的版本中,假如我们的应用部署在mongrel上,那么对于n个并发请求,就需要n份rails,n份application,n个数据库链接等等。几乎所有的东西都无法共享。

多 线程模式,对于Rails on JRuby带来的改变尤其巨大,因为jruby的线程是native thread(相对于ruby的green thread)。比如我们的系统只开启了一个应用实例来处理所有的请求(当然,当瓶颈出现在一些共享资源上时,可以考虑增加实例)。

还是用数字来说话吧:Rails on JRuby的内存使用是原先非线程安全时的1/n (n是并发请求的数量),是Rails on Ruby的线程安全模式的1/m(m是cpu的数量)。

那么,开启多线程模式要注意什么?如果你的应用中有类变量的使用,请注意它们是否会在并发下出现问题。

服务器

服务器的配置优化对性能的影响很大。我们的系统以war包的形式部署在tomcat上,主要的配置优化在于对JVM的内存分配上。可以从这几个方面看:

xms — 初始内存大小是否合适?如果你的系统在一开始的时候就需要较大的内存分配,就可以设置一个合理的xms值。
xmx — 最大内存大小是否合适?如果太小,会导致OutOfMomery,会导致持续的GC;反之,则会导致一次GC时间过长,在这段时间内,系统的性能将会受到影响(当然,这跟GC的算法有关)。

性能优化的几个原则

更近

数据库,应用服务器,web服务器以及客户端,这是信息传输的一条链(当然,有些系统的链会更长,更复杂)。那么,减少响应时间的一个原则就是:让数据离客户端更近。

一个最能体现更近原则的优化就是客户端缓存–客户端是距离用户最近的地方。

更加细节的一些例子,比如sql server的nonclustered index现在可以把非键的数据挂载在索引的叶子节点上,这样就不需要再去表上扫描获取这些数据。这也是更近原则的一个体现。

更快

更加快速的响应,需要更加快速的计算。前文中提到的加速页面元素的生成,就属于更快原则。

这方面的实践包括有算法优化,数据库索引,使用更加高效的方法,使用正则表达式匹配等等。

举个Rails中的例子:使用Model.find_by_*方法是很低效的,因为它需要调用method_missing来动态生成方法。而Model.find_by_sql方法的效率高很多。

更少

传输过多的数据,进行过多的操作,可能都会影响性能。

减少数据的传输量有很多例子:压缩静态文件,以减少服务器和客户端之间的传输量;避免在action中滥用实例变量,以减少实例变量在action和view之间的传输;正确合理使用finder,以减少从数据库中获取的数据量等等。

减少操作次数的例子有:合理运用预先加载,减少查询次数;减少transaction的不必要重复创建等等

其它还有:缩小transaction的粒度;缩小并行运算锁的粒度等等。

平衡

在追求更近、更快、更少的时候,要注意平衡。

比如给JVM分配一个合理大小的内存,而不是过大或者过小;不要滥用数据库索引,要考虑是否会影响插入数据的速度;平衡一个运算在空间和时间上的消耗;保持性能在长时间内的平稳等等。

性能优化是一个不断实践、不断调优的过程。比如前文中提到的服务器端缓存,最后并没有被采用,因为我们发现相比而言缓存命中的开销反而更大。

小结

在经过一系列的优化之后,我们的系统很好地满足了客户对性能的要求。下面是几点总结:

  1. 大多数性能问题都出在IO上,IO应是关注的重点。
  2. 性能优化是一个实践的过程,空讲理论是没有意义的。
  3. 出现性能问题,先不要怪罪于平台、语言、框架,大多数性能问题都产生于错误或者不合理的实现。
  4. 性能优化过程并不一定需要贯穿整个项目的始终,但一定要时刻保持对性能问题的关注:从刚开始的架构设计,到项目开发中的代码编写、重构等等,性能都应该是关注的一个方面。
————————————————————————————————————–
此文是三个月之前的旧文了,刚发表于2010年1月刊的《程序员》杂志。

无关敏捷,关乎责任

JJG在《The Elements of User Experience》特别强调,要让每一个人参与到网站设计中:高层管理人员,市场人员,销售人员,等等。不过这里,我想他忽略了一个很重要的群体,就是开发团队

The Elements of User Experience》把用户体验分为五个要素: Strategy, Scope, Structure, Skeleton, Surface。

其中最根本的是strategy,因为它是用户的需求和网站的目标

在我们的开发过程中,拿到一个story并不意味着开发的开始,而往往很多时候我们会花很多时间论证这个story的价值所在。开发团队经常会向客户提很多问题,探究这个story的起源和目的;开发团队经常和客户一起讨论甚至争论一个story的功能或者设计,因为随着开发的深入,对项目的了解,我们有义务告诉客户我们所想,帮助客户找到真正所需。

讨论的结果可能证明开发团队是错的,也可能证明客户是错的。但双方都在讨论中对story的价值越加清晰。

因为通过争论,客户会发现

  • 其实这才是我们真正想要的:经过向公司相关人员咨询,发现这果然是更好的方案。
  • 原来可以通过这种更简单的方式得到我们想要的,得到用户所需要的。
  • 应该丢弃这个功能,这样做是错的,这样的设计不仅对我们未来的业务发展没有好处,而且还可能成为一个束缚。
  • ……

开发团队:

  • 的确客户是对的,我们在实现的东西是有价值的。
  • 又一次不仅帮助了客户找到了真正的价值,也避免了让自己花很多时间做一个用户不会喜欢的功能。
  • ……

印象特别深刻的是在项目结束之后,客户的BA诚挚地对我们说:谢谢团队的每一个人,谢谢你们不停地问问题

后面的那句话,我想,是他们意外得到的。所以在感谢的时候特别地提了一下。

开发团队保证正确实现客户所要的,就够了么?不,开发团队要保证正确实现客户真正想要的

这里无关敏捷,这里关乎责任

The Elements of User Experience

发布之后

release

发布之后,系统才开始在真实的数据、环境上运行,才开始经受真实用户的考验。发布,不意味着项目的结束,却是挑战的到来。如何在发布之后,快速修复影响到 系统使用的bug;如何在发布之后,快速改进在真实环境中无法承受的性能问题;如何在发布之后,快速调整用户体验较差的界面设计或者功能实现。开发团队或 者维护团队,如果不能快速响应这些突然袭来的变化,就会给客户带来损失。

同时,从发布之后出现的问题,可以反思开发过程中某些方面的不足。以下会列出我们在发布之后遇到的一系列问题,以及对这些问题的思考。

难以重现的问题

当发现bug的时候,立即想到的就是在测试环境或者UAT上重现问题,以便快速定位问题的根源。

场景

系 统发布一个小时之后,客户一封邮件过来,告诉我们有一个重要功能在产品环境上不工作:一个公司无法为她的包月套餐付费。于是,我们立即在测试环境上试图重 现问题,但失败了;UAT上也如是。这是一个重要的功能,经过详尽的测试,却在产品环境上突然出现问题,令人匪夷所思。于是,只好到产品环境上测试,果然 重现了bug。首先排除了环境的区别,那么肯定就是数据的区别。在仔细观察数据的时候,敏感地注意到一个特征:被测公司的所有员工都没有接受网站的条款。 马上在测试环境中建立同样特征的测试数据,果然重现了bug,并找到了问题的根源。

反思

有些bug会很难重现,而之所以难,根本原因在于忽略了出现bug的测试场景(包括数据、环境等因素)。导致这个bug出现的根源在于测试不够全面。如果在测试中尽量覆盖边际情况,就可以避免多数类似问题的出现。

产品环境中仅有的问题

产品环境和测试环境的差异在哪里?

场景1

这不是一个功能性bug:有一个页面在产品环境中出现了不应该有的滚动条,却十分影响页面的美观。对照测试环境和产品环境之后,发现是因为在产品环境中一个链接的url过长导致信息框出现了滚动条。

场景2

页面上有个回退链接,会回退到之前访问的页面。有些时候却回退到了网站的首页,在测试环境中无论如何也不能重现这个bug。在产品环境下,发现出现问题的页面都是从https跳转过来。查看代码,果然没有考虑这种情况。

场景3

在用某些关键词搜索的时候,会出现500错误。而在测试环境中却无法重现。非常幸运的是客户发来了产品环境的log,经过分析,发现问题在于产品环境中集成的第三方工具提供的有些数据会导致程序错误。

反思

这 些都是典型的由于测试环境和产品环境数据或者环境不一致引发的问题。如果能在测试环境中尽量保持数据的拟真性、环境的真实性,则可以尽量避免这些问题在发 布之后才被发现。但从另一方面看,出现这些问题的根本原因在于代码不够完善。场景一中前端代码的包容性不够好;场景二中引起问题的代码有一些比如hard code的bad smell,却没有被及时修复;而场景三的代码是有漏洞的,它没有很优雅地处理数据获取失败时的情况。

无法获知起因的问题

有些用户遇到了问题,但我们无法或者没有时间去一一获取这些用户的信息。

场景

新系统上线之后,所有老用户都得重新接受新的网站条款。但有些用户无法点击接受条款的按钮,严重阻碍了这些用户的回访。

反思

这 个问题可能只出现在千分之一的用户里面,试图去获取所有这些用户的客户端环境(浏览器版本)很困难,而客户又要求我们立即解决问题。所以,试图去重现问题 已经不可能了。不如凭着经验审查一下原有的代码是否有瑕疵,前端代码是否浏览器敏感。然后给出一个更通用、更完美的方案。这类问题是无法避免的,除非花费 大量时间对所有客户端环境都进行测试。但这类问题是可以解决的,比如上面那个bug我们就通过采用兼容性更好的代码解决了。

真实用户体验的问题

真实用户才是真正的测试人员。

场景1

一个页面有分页功能。用户来到了第n页,进行了一个操作之后,重定向到了第一页。而用户显然希望能继续回到第n页。

场景2

几个按钮应该排成一行,但当用户输入一个很长的名字之后,出现了换行。

反思

这类问题不算是bug,但却影响了用户的体验。测试人员虽然会站在用户的角度去测试功能,但最好的测试人员其实就是真实用户。如果有条件能在发布之前,让一些真实用户参与测试,是发现这类问题的最好方法。

遗留的疑难问题

既然是遗留的问题,肯定是难以解决的问题。

场景

系统中有几个暂时不影响发布系统使用的bug,它们被一直拖到了发布之后,因为之前“难以解决”。

反思

虽 然这些bug最终都被修复,但这是一种不正确的方式。疑难问题不应该留到发布之后:在发布之后能解决的问题,那么在发布之前也一样可以解决;如果在发布之 前确实无法解决,那么就应该选择其它方案。如果在发布之后花了很长时间也解决不了这些bug,就进退两难了。同时,维护团队一定要保证部分核心开发成员的 继续留任。如果在交付产品之后,开发团队立即全部撤离,而把系统的维护交给一群对业务和实现完全不熟悉的人,是一种很不负责任的态度。

不会有问题的问题

这个功能不是已经经过验收了么?

场景

第一眼看到客户报的有些bug时,脑中飘过的第一个想法是:这个功能肯定经过详尽的测试,而且肯定经过了客户的验收,怎么可能有问题呢?但在测试环境中确实重现了bug。

反思

之 所以在看到这些bug的时候比较难以置信,是因为我们以为这些功能已经被测试过,或者我们知道这些功能曾经被测试过。但事实是,有些功能可能根本就没有被 测过;而有些功能曾经被测过,但没有被自动化测试覆盖。同时,我发现后一类问题往往都是在last mile中被引入:在发布之前,为了解决性能问题,我们需要对设计或者实现进行一些大规模的改动;而到后期,测试人员因为功能性需求的结束而退出了团队。 当时非常担心大规模的改动会引起一些问题,在发布之后验证了这种担心。这类问题其实是可以尽量避免的:首先,更早开始性能测试和性能优化,以避免在 last mile进行大量改动;其次,last mile可能会出现赶工的情况,有大量功能的改动或者设计的变更,这时候是测试人员最不应该离开团队的时候。

不是问题的问题

客户坚持说,这里有问题。

场景1

客户说:在使用firefox浏览器的回退按钮时,能看到不实时的信息。

场景2

客户说:有一个bug… 过了一会儿,客户又说:bug好像没了。不过,总之问题出现过,你得去修复。

反思

这 些不是问题的问题,浪费了维护团队的大量时间。发布之后,维护团队需要及时地修复很多bug,应该尽量地排除不必要的干扰。在一个维护团队中,也需要维护 一个流程。我们的维护团队,由两个开发人员和一个测试人员构成。测试人员负责接纳并验证所有的bug,开发人员负责修复bug,再经测试人员测试,最后经 由客户验收。通过坚持这个流程,让团队的每个成员,都关心自己最应该关心的问题,而不应该被其它事情干扰。在发布之后,需要更加快速的反馈,而严格地执行 流程,能明显地提高团队的效率。

总结

系统在发布之后经历了一段时间的考验,bug不多,并且基本没有出现影响系统使用的bug;同时,维护团队保持了高效的反馈,及时地修复了大部分bug。高质量的代码和快速地反馈得到了客户的认可。

现在回头看这些在发布之后出现的问题,我相信,没有一个系统能确保全部避免。不过从我的反思中可以看到,这类问题大多是开发过程中的“不完美”而遗留下来的隐患,如果能做得更好,我们就可以尽早发现这些问题,以避免这些问题在发布之后才出现。

新博客,新种子

鉴于blogspot彻底被GFW,下定决心买了托管服务,搭建真正由自己控制的博客。

WebHostingPad,它号称不限空间不限流量。买了之后你会发现空间和流量都有限制,这叫上了贼船下不来了。不过10G空间和每月100G的流量对我来说已经太多了。价格很便宜,推荐购买三年的,每个月1.9美金。使用webhostingtop这个coupon code,可以优惠25美刀,最后只需要付46美刀。

发现WebHostingPad支持Rails,惊喜一下。以后可以在这里很方便的搭建一些实验站点了。

用wordpress作为博客的搭建工具,基本上是一键安装(cPanel这个控制工具很多托管服务商都在用么?)。选了一个博客模板,花了一些时间去掉广告,调整了一下布局,并fix了它的一些bug。但在IE6上显示不正常。强势地制止你使用IE6:http://www.ie6nomore.com/。每个人都应该主动放弃使用IE6,这个世界为IE6浪费的时间和金钱已经远远大于它在现在带来的价值了。

搬到了新博客之后,虽然保持了原有域名,但因为RSS地址不再一致,导致过去订阅的同学不能再看到更新。试图在.htaccess里通过rewrite使老的rss地址重定向到新的rss地址,但无耻地失败了。不想再花时间整这个。以后奔向feedburner去了:http://feeds.feedburner.com/andyhu1007 ,生活从此实现小康~~

感谢webhostingpad救助穷人,感谢wordpress救助懒人,感谢feedburner救助普罗大众,感谢我自己:一个勤劳多金的男人。

”框计算“的“框算计”

百度在2009百度技术创新技术大会上提出了一个叫做“框计算”的全新概念。

提出两点疑问:

第一个疑问:专注做购物的淘宝都没能让用户一键找到自己想要的商品,凭什么是你百度?专注做游戏的盛大都没有把所有玩家的需求覆盖,凭什么是你百度?

第二个疑问:搜索引擎的精神是开放和自由,给用户最优结果的同时,让用户保有选择的权利。百度如果变成一个路霸或者黑店,谁还愿过你这条路,谁还敢进你这家店?

百度的“框计算”不要最后反而成了“框算计”,算的不是别人,是自己。别让起点,成了终点

Agile China 之 腾讯

腾讯的研发部经理作了精彩的演讲,虽然前段部分广告内容太多。但有几点新鲜的东西:

1. 现在石油价格不断攀升,能源问题引起了各个国家不断的政治风波。原来以为能源问题跟网络公司似乎没太大关系,但其实随着腾讯的服务器数量即将达到10万之巨,电力开支已经快要超越人力成本,成为腾讯最大的成本开支。

2. 灰度发布:所谓灰度发布,就是新产品逐步放量发布的过程。其主要思想就是把影响集中到一个点,然后再发散到一个面,出现意外情况后很容易就回退。刚开始可能仅仅放量给100个用户,你可能就是那个最先使用到腾讯新产品的人哦。

3. 原来国内最大的互联网公司,也已经敏捷了。

SES

写写上次去南京参加SES大会的一些趣事和心得。

印象最深的当然是马云,marketing确实很强,一上台就扛起了民族主义的大旗。掌声不断,笑声也不断。Google的周韶云在造势方面跟他比起来就差远了,受媒体的关注程度当然也不同,从会后记者的对比就可以看出来。有些人说,马云太浮,但我觉得其实像阿里巴巴这样的电子商务网站,需要马云这样一个marketing超牛的人。市场的竞争无非就是渠道和代理的竞争,马云誓称要为渠道商创造一百个亿的收益,收拢人心的举动众人皆知。如今很多B2B的网站进军中国,阿里巴巴的竞争压力与日俱增,如何在服务上真的让用户满意,才是关键。

Google在中国同样受到高层次人群的欢迎,主要用于搜索生活和技术知识,google的用户也相对成熟,客户忠诚度明显比百度高出不少。

其实目前很多中国的网站,都应该改变思路,不应该以一些花俏的如music download, mm pic等东西来吸引用户了,而应该真正站在用户方面着想,为用户创造真正有价值的服务。虽然百度在娱乐方面的优势保持了其在用户数量上的领先,但谁都知道,未来的天平将倾向哪方。

共享,参与,对话,互动是Web 2.0的特征。随着Web 2.0概念的兴起,越来越多的Web 2.0网站涌现。

即使是搜索引擎也开始出现Web 2.0的特征。从search info –> search Action + search Behavior –> Social Search。

结合自己做Intenet的体会,10年之前一个点子创造一个公司,而如今,要想生存,必须做深,做好!