Xueqiu Engineering Blog

thoughts on creating xueqiu

GC 优化的一些总结

| Comments

合理堆大小的设置

基本上所有会写Java的人也都知道怎么设置堆相关的参数,会设置但并不意味着这个是一个简单事情。常见的OOM问题,大部分都是因为不恰当的堆设置引起的。

该给Java进程配置多大的堆?

换个方式问这个问题: 大堆、小堆对Java进程会有怎样的影响?

  • 对Java程序而言,堆大小最大的影响的在于垃圾回收STW的时间。因此,对在意吞吐量而不在意响应时间的应用,堆可以设置的很大。
  • 而对于响应时间要求比较高的应用,堆大小则需要仔细考虑。设小了,会导致频繁gc,gc效率太低;设大了,则STW的时间太长,无法满足时延要求。

响应时间优先的应用如何确定堆大小?

为方便讨论,做以下假设:

  • 假设响应时间要求99%小于100ms
  • 假设应用qps为1000
  • 平均响应时间为50ms
  • 忽略GC外其他方面对时延造成的影响
  • 假设应用是典型的无状态应用,进程内长生命周期对象主要为连接池、线程池、缓存等

由以上假设,可以做一点简单的计算:

  • 99%的请求100ms以内、每秒1000个请求、平均响应时间50ms,意味着5s内可以留给gc做暂停的时间,最长为100ms。
  • 100ms的暂停时间中,其中有50个请求受gc影响,暂停时间大于50ms,加上业务自身的50ms,超过了100ms;另外50ms,暂停时间小于50ms,因此没有超过100ms. 5s内5000个请求,其中50个超时,刚好99%在100ms以内

通过上面的计算,我们得出一个结论:

在以上假设的条件下,5s内超过可以留给gc做暂停的时间,最长为100ms

这个结论变换为以下说法: 5s内发发生一次young gc,最长时间为100ms, 同时,要求old区gc最长不超过100ms

继续:

  • 有了gc频率和gc最长停止时间,我们就有了推算堆大小的方法
  • 如果young区设置为4G时,刚好能保证5s一次young gc,平均100ms,那么4G的young区是一个能够满足业务目标的young区堆大小设置。

如何确定old区大小?

  • 如果使用并行收集器,那么full gc的时候多半会超过young gc时的最长时间,因为大家算法差不多,但是full gc的时候堆变大了。因此,在响应时间优先的业务中,并行收集器不被推荐,一次full gc就有可能影响整个业务。
  • 如果使用并发收集器(CMS),CMS在old gc时有两次短暂的暂停(initial阶段和remark阶段),能保证这两次暂停小于100ms的整个堆大小设置,是一个可以作为参考值的old区大小设置。

young区大小小于前面4G的合理值会不会更好呢?至少显而易见的是超时率更小了

  • 堆大小会直接影响的一个指标是gc效率,即: gc时间占整个系统运行时间的比例。一般来说堆越大效率会越高,堆越小效率会越底。因此,吞吐量优先的应用,尽可把堆设置的大。

  • gc效率低,说明应用很大部分的cpu时间和等待时间都在gc上,这种开销规模小的时候不明显,但是规模大了以后,需要尽可能减少。

并发收集器(CMS)的问题

通过前面部分可以看到,响应时间优先的应用,并发收集器(CMS)是一个好的选择,但是CMS也有自己的一些问题。这些问题会导致并发收集器长时间暂停,从而影响业务。

相对并行收集器,并发收集器设计为在进行垃圾回收时不会暂停应用,因此并发收集器存在几个根本的问题:

  • 不能暂停进行垃圾回收,意味着进行并发垃圾回收时,需要预留空间,保证垃圾回收进行时,有足够的空间能够装下回收过程中的新增对象
  • 不能暂停,同时CMS本身内存结构的限制,会导致内存碎片。碎片意味着当有大对象出现时,需要进行空间整理。

Concurrent Mode Failure

常见导致这个问题有两种情况:一种确实是内存泄露了,导致无法垃圾回收。这时会看到连续的Concurrent Mode Failure。另一种是预留空间不足,我们主要讨论后一种。

这个错误在CMS的gc日志中有可能会看到,这个问题对应开头第一点,即: 在进行old区垃圾回收的时候,old区空间没了,此时,Java进程会暂停应用,进行full gc

一般来说,增加预留空间就可以解决这个问题,可以选择:

  • 预留空间比例不变的情况下,增加old区堆大小。
  • 堆大小不变的情况下,增加预留比例。即: 缩小-XX:CMSInitiatingOccupancyFraction=70。比如:整个堆4G,young区2G,因此old区有2G。如果设置 -XX:CMSInitiatingOccupancyFraction=70,即表示,old区使用2Gx0.7=1.4G时,触发old区gc,预留空间为600m。如果缩小这个值到-XX:CMSInitiatingOccupancyFraction=50,表示old区使用2Gx0.5=1G时,触发old区gc,预留空间为1g

Promotion Failed

这个问题对应最开始的第二点,碎片问题。

  • 这个问题在young gc时有大对象需要迁移到old区,但是old区没有连续空间,随后会暂停应用,进行空间整理。
  • 因为是由于碎片造成,因此,这个问题出现会比较随机。与业务相关,与业务产生的对象相关。
  • 这个问题基本无解,因为,不进行full gc或者空间整理,那么碎片只会越来越多。
  • 可以考虑增加堆大小,这样能减缓碎片产生,或者在流量小的时候,主动触发一次full gc,减少影响。

碎片问题算是CMS的硬伤,也才有了G1。

以上两个问题,均与业务产生的对象有较大关系,有些业务再大的量也不见得会有上面问题,而有的业务,在很小量的时候就会出现。

CMS参数优化的大致流程

GC问题排查流程

关于G1

G1也是并发收集器,G1解决了CMS碎片的硬伤,同时增加了更多响应时间优先的特性。

  • G1之所以能解决CMS存在的问题,是因为对内存结构进行了彻底的改造。把堆进行了分块处理,可以按照内存块为单位计算其使用率等。
  • G1也使用分代的垃圾回收方式,这点与以前是一样的.
  • G1的优化很多是围绕内存块的优化

G1的垃圾回收log

关于G1的日志可以看Oracle的这篇g1gc logs - basic - how to print and how to understand,写的很详细了。

其中需要注意的是,G1也是并发的时候,与CMS类似分了一些阶段。其中G1收集器在remark阶段和cleanup阶段STW,而CMS在initial阶段和remark阶段STW。

G1涉及到的一些优化参数

  • -XX:G1HeapRegionSize=4m: 设置内存分块的大小。范围是1MB~32MB。是一个很常用的参数。当系统中存在大量大对象的时候,大的Region会提升GC效率。

  • XX:InitiatingHeapOccupancyPercent=50: 设置使用整个对的x%时,系统开始进行并行GC。注意是整个堆的百分比。这与CMS收集器的类似参数不同。

  • -XX:MaxGCPauseMillis=200: 单位为毫秒。此值为建议JVM的最长暂停时间。只是建议值,G1只能尽量保证,而无法完全保证。

  • -XX:ParallelGCThreads: 进行young区和old区垃圾回收时的并发线程数。默认线程数由CPU数量决定(通常小于CPU个数)。当有较大的Update RSet时间时,可以尝试调整此值。

  • -XX:ConcGCThreds: 并发GC时的线程数。mixed GC情况下,较长的cycle start时间,可以尝试调整此值。

  • -XX:+ParallelRefProcEnabled: 当看到有较长的Ref Proc建议配置此值。之前我们从JDK6升级到JDK8时,发现Ref Proc时间有较长的提升,CMS收集器和G1收集器均由这个问题。配置以后暂停明显缩小。

对响应时间优先的应用,G1从目前的使用看情况是比较理想的。即没有CMS存在的硬伤,在优化层面也相对简单。从G1内存结构看,G1实际使用时可以配置比较大的堆。可以预见,G1将会逐步替换目前CMS收集器。

参考文献

1
2
3
我们在招聘 “Java工程师 / 运维开发工程师 / 测试开发工程师” 等职位
详细JD可以参考 http://xueqiu.com/about/jobs
感兴趣的同学欢迎投简历到 wangdong@xueqiu.com

Redis集群的数据划分与扩容探讨

| Comments

首先界定一下,本文探讨的标的不是官方即将发布的Redis 3.0 Cluster,而是自行实现的一个生产环境中的Redis集群。这是一个典型的分布式系统,以下讨论将围绕CAP原理中,与P相关的数据划分和扩容两个问题展开。

目标

对这样一个系统,在数据划分和扩容方面最理想的目标是:

  1. 数据划分针对客户端透明
  2. 支持在线热扩容,扩容针对客户端透明
  3. 支持纵向扩容、横向扩容

数据Sharding分片

Redis在部署时的拓扑结构完全取决于其适用的业务场景。基本上可以划分为两类:

a) Cache场景

对Cache场景,服务器端最常见的部署拓扑是“一致性哈希”。一致性哈希可以极大的优化机器增删时带来的哈希目标漂移问题。同时对于Hash目标漂移时产生的严重的数据倾斜,可以利用虚拟节点来优化。

基本上,物理节点有了一定规模后,只要不是同时挂多个节点,或者同时扩容多个节点,数据分片不会有太大的扰动。穿透过Cache的请求后端存储可以抗住即可。

基本可以认为,在Cache的场景下,Redis是可以比较完美的实现前述横向扩容目标的。

b) Storage场景

Storage场景下的数据分片则会复杂一些。原因是既然作为存储就要保证数据的高可用,要实现高可用,Redis自身提供了主从Replication机制,通过多副本来保证。Redis在主从复制高可用方面也经历了较长的迭代,从最初2.4版本备受诟病的全量同步,到2.6版本终于实现了增量同步,到2.8版本的Sentinel代理。

高可用有了保证后,我们回过头来看数据分片,最简单的方式是对Key哈希后按照分片数“取模”

略微复杂一点的话可以使用“预分片(Pre-Sharding)”的方案,有人也称呼为按“桶”进行数据划分。阿里的Tair(http://tair.taobao.org/)框架,以及豌豆荚最近放出的Codis(https://github.com/wandoulabs/codis)对数据的划分都使用了Pre-Sharding方案。

Pre-Sharding方案实际上可以理解为预先分配一个相当大的集合,对Key哈希的结果落在这个集合中,集合的每个元素又与具体的物理节点存在多对一的路由映射关系,这张路由表由一个配置中心进行维护。阿里的Tair把它叫做ConfigServer中,豌豆荚的Codis把它叫做ConfigManager,一个意思。

回过头来再细想下,一致性哈希中的虚拟节点,实际上也可以归类到Pre-Sharding方案中。换句话说,只要是key经过两次哈希,第一次Hash到虚拟节点,第二次Hash到物理节点,都可以算作Pre-Sharding。只不过区别在于,一致性哈希的第二次Hash其路由表是按照算法固定的,Tair/Codis的第二次Hash其路由表是第三方可配的。

纵向扩容 Scale Up

Redis的优雅之处其中就包括对在线纵向扩容的支持,直接一系列config set maxmemory完事(当然别忘了同步改从库,以及rewrite conf文件)。不过config set maxmemory在2.4版本有个小bug就是不支持以K/M/G为单位,不知道后续版本修复了没有。

横向扩容 Scale Out

前已述,Cache场景下横向扩容是没有问题的。

Storage场景下,我们分别根据两种Sharding方式探讨两种方案

a) 取模Sharding

假如扩容前是模N,我们提出的方案:扩容后,对N的倍数进行模运算。

以扩容后模2N为例,具体操作为:

  1. 首先对N个Master节点(如A、B、C),以1:1建立N个Slave同步(如D、E、F)
  2. 然后关闭Slave库的ReadOnly,但主从关系和顺序保持不变,客户端改为模2N(A、B、C、D、E、F)
  3. 然后断开主从
  4. 最后异步清理掉2N个库中多余的50%的Key

取模Sharding扩容

这个方案的可行之处在于

  1. 扩容前后,虽然取模的结果变了,但是目标节点的数据仍在
  2. 在第2步中,允许多个客户端加载模2N有先后,因为不存在Race关系。(反证法:如果存在Race关系,那么在没扩容没重启时也存在Race关系)

最后一步清理Key的方式有两种

  1. 如果有定期做RDB备份的话,可以异步解析RDB,挑出其中冗余的50% Key,在低峰期删除。这种方式比较彻底。
  2. 如果没有定期RDB备份的话,可以在低峰其起异步的工具不断randomkey(),并检查其是否冗余,若冗余则删除。一段时间后冗余Key的数量一定会大大下降,但是不彻底。
b) Pre-Sharding

Pre-Sharding中,横向扩容只需要去修改Hash路由表即可,增加物理节点仍然需要保证数据可访问,类似模倍数方案。

或者如果有Proxy中间件的话让中间件进行路由。在这里值得一提的是Codis通过patch Redis实现了以key为粒度的原子migrate操作,使得通过中间件进行路由极为便捷。

另外还有个情况要考虑,假如万一路由表用满了,Pre-Sharding也就退化为取模Sharding的模式了,还可以再采用模倍数方案。

横向扩容客户端透明

客户端与Redis物理节点间有两种方式的连接

a) 直连

直连的话,客户端必须从配置中心订阅到所需的节点变动通知。这个配置中心可以是Tair的配置中心,也可以是ZooKeeper/etcd等等,也可以是Sentinel。

其中Sentinel有个天生的问题,就是它的监控粒度是一套主从节点。如果在Storage场景中,按照取模的方式进行Shard,并使用Sentinel做配置管理。这时横向扩容的话Sentinel并不能有效的通知客户端节点Sharding数发生了变化,解决这个问题需要一部分hack工作。而ZooKeeper/etcd等通用配置中心则可定制化程度较高。

b) 通过Proxy中间件透传

类似Twitter的twemproxy(https://github.com/twitter/twemproxy),或者百度的的bdrp(https://github.com/ops-baidu/bdrp)或者前述的豌豆荚codis,以及京东的Redis集群都是使用了一层Proxy进行透传请求。这样只需要Proxy能够订阅到物理节点的变更,并自动加载即可。订阅方式同样可以走各种配置中心。

总结

针对Cache和Storage两种场景,对数据划分和扩容有不同的方案和取舍点,可以在便捷性和成本等各个方面进行权衡。

另外也可以留心下即将发布的Redis 3.0,在线下环境中折腾玩下。

1
2
3
我们在招聘 “高级架构师 / DBA / Java工程师” 等职位
详细JD可以参考 http://xueqiu.com/about/jobs
感兴趣的同学欢迎投简历到 gaolei@xueqiu.com

2013,一个「移动」公司

| Comments

雪球, 是一个投资者的社交网络;雪球公司,是一家互联网金融服务提供商。

关于雪球

2012年,雪球组建了移动产品开发团队。大部分时间里,这个团队由一个 产品经理,一个 iOS 工程师,一个 Android 工程师构成,主要的工作是把雪球的web产品移动化。10月后,我们又迎来了第二位 iOS 和第二位 Android 工程师。

在经过2个大版本和平均每个月一个用户可见的小版本迭代后,雪球的移动和网页产品基本处在了相同的水准线。移动产品和团队,在雪球也经历了两种生产和合作模式。

  1. 2012年1-9月,网页产品移动化

    网页产品移动化,这是任何一家想做移动互联网产品的互联网公司直觉的做法。

    我们把雪球产品分为投资和社交两个方向,由移动产品经理和设计师将线上产品转化成 iPhone 和 Android 产品原型;由移动工程师将后端 API 实现 iPhone 和 Android 的封装。

  2. 2012年9月至今,移动优先

    一个好的移动产品,并不是简单实现平台迁移的事情。我们开始做一些符合移动场景下的用户界面和交互优化;开始做一些移动产品独有的功能。

    但这种模式同样有明显的缺点,移动团队和原来的网页产品处于平行线;移动产品在通用功能开发时间上基本都落后于网页产品。移动产品经理需要对所有产品都完全了解,需要处理产品每个细小迭代和改动,随着产品的演进,这几乎是不可能的事情。

    这可以做一款合格的移动产品,但还是做不了优秀的产品。

回到Big-Picture,一个稍微神志清醒的公司和团队都知道移动互联网意味着什么;看雪球自身的数据,最近一个月,雪球的DAU用户中大约有45%使用移动产品,移动客户端各种指标都在更高速增长。

所以,2013,我们主要有什么变化呢?做一家移动互联网公司。

变化

  1. 我们在讨论任何一个功能的时候,网页和移动都是同时考虑必选项;
  2. 产品开发的优先级不是先网页后移动的区分,只有对功能实现的优先级区分;
  3. 产品经理的分工,主要从「平台不同分工」变成「功能不同分工」。原来的网页产品经理负责每个feature的网页和移动端;原来的移动产品经理专注移动产品整体界面、交互与 iOS 和 Android 的本地化体验,专注移动自身场景下独有feature的产品设计和开发。
  4. 移动开发工程师对产品功能和系统本地化有更优的实现,与web工程师一样逐步按业务模块分工。

挑战

  1. 产品经理:每个产品经理需要对移动平台更熟悉。
  2. 后端工程师:后端在程序设计上考虑跨平台实现。还好,雪球的后端团队,从一开始就面向多终端实现。后端与web前端和mobile前端都是通过 API 接口通信。当然,明年后端工场团队需要为移动网络下做更多的接口优化。
  3. web 工程师:雪球的移动产品,采取了 native 为主,html5 为辅的方式。随着移动OS和html5的发展,web工程师在移动开发过程中的作用会越来越重要。
  4. mobile 工程师: 更早参与到产品设计过程,持续对业务模块迭代;对系统本地化的交互、体验做的更优。

优秀的团队,每个成员能很快找到新方式下的位置。 在明年这个时候,雪球也可以自信在本文第一句前加上「移动」二字。

ResysChina 2012 Conference Note

| Comments

2012.12.1日,我们参加了ResysChina 2012推荐系统大会。 大会是由ResysChina(一个面向推荐系统领域的专业社区)主办,一年一次的推荐系统行业质量比较高的大会。 大会链接:http://www.resyschina.com/2012/ (有部分slides可下载)

本次会议分为主题嘉宾演讲和创业团队分享两部分。

嘉宾分享:

Facebook推荐系统–石言心

2008年加入Facebook, 就一直从事机器学习、推荐相关工作。感觉Facebook这块还是很强的。

他们把各种网站内容(例如,照片,朋友,游戏)和广告合在一起进行推荐,就是说一个推荐api返回的内容揉合了网站内容和广告。 每种内容都有非常不同的特性需要机器学习模型来学习。 各用户的各不同内容之间通过一定规则转换一个标准分。

推荐的哲学:新广告的出现占用了系统和其他的广告的展现机会。为了各方的利益均衡,广告主应该支付一个费用。具体竞价公式忘记了。 有个index引擎,索引用户的所有特征。用推荐器从索引里选取相应物料进行投放。选择1%的用户进行训练和实验,后验和预测的误差,决定新的模型是否会被广泛应用。小时级别的模型训练。

同时讲到了他们大规模数据的存储与处理,及快速的进行机器学习和反馈。

Recommendation anytime, anywhere in Hulu–项亮

Hulu是一家美国视频网站,现为美国第三大视频网站。有一半多的技术在北京, 北京的团队主要负责搜索引擎、推荐系统、web播放器等。 Hulu的mission: 用户:提供个性化体验,帮他们快速找到和发现符合其兴趣的内容。 内容合作方:更好的把内容展现给目标用户。 Hulu:提高用户参与度。

历史:使用推荐技术的历史。从个性化视频推荐,到整站个性化。

推荐产品的两种类型: 内容发现:帮助用户找到感兴趣的内容。 方便导航:让用户最快到达用户感兴趣的内容。

数据驱动产品决策: 目标:帮用户简单、快速的跟之前看过的剧。(比如:你昨天看了越狱的第4集,今天你如果登陆我们会提醒你看第5集)。 排序:最早通过最后观看时间排序。 但需要考虑各种情况(比如: 是否要考虑用户对每个视频的完成度。是否看完了,看了多少分钟。 每天发布的剧和每周发布的剧怎么排序。 ) 需要加很多规则来解决每个问题。 系统变得越来越复杂: 有很多冲突的规则 每个人有自己的规则 所以,转为采用机器学习的方法和使用数据来解决 从观看历史中抽取特征 建立模型来预测用户要看的下一个视频

基于规则vs机器学习: 基于规则: 容易理解,基于领域知识,白盒。 很难定义适合的规则,不是基于数据,不同规则经常冲突。 机器学习: 基于数据。 难以解释,基本上是黑盒. 基于机器学习的系统: 一个好得基于机器学习的系统应该: 基于数据进行决策 能对其怎么运作的能提供详尽的解释 能很容易地增加领域知识

腾讯精准用户定向和效果广告–王益

分享介绍技术团队如何在腾讯严格的用户隐私保护政策下,从多种用户数据中精准挖掘用户兴趣;以及利用挖掘结果,实现推荐和广告系统中的实时获取和排序。

效果广告: 搜索广告 上下文广告 用户定向广告 无线 社交

语义分析: 问题:查询和广告描述都是短文本,信息量少,有歧义,直接文本内容匹配效果不好。 apple pie iphone crack

解决: *从海量文本数据中归纳“知识”,帮助理解语义。 知识:同一个主题的词聚成一类(topic) topic 1 = { apple, tree, pie, fruit, …} topic 2 = { computer, iphone, ipod, crack, apple, …} 理解:词空间到topic空间的投影 “apple pie” topic 1: 99%, topic 2: 1% “iphone crack”topic 1: 1%,topic 2: 99%

搜索引擎扩展: 对一个query,用该query在搜索引擎进行查询,用搜索引擎中蕴含的“知识”补足信息。 隐含语义分析: LSA pLSA NMF LDA Peacock

用户意图分析: 问题: 用户行为数据覆盖率低 搜搜queries 拍拍transactions 覆盖5%用户

解决: 用 user feature 扩展用户兴趣 QQ profile:性别、年龄段、职业、学历 用户关系:微博fan了谁、QQ聊了谁、QQmail写给谁

算法和系统: 新的模型 用co-clustering思路,发掘用户行为模式,得到意图 用regression思路,得到用户feature和意图的关联。 新的并行算法 目前的并行co-clustering算法不能学习大量意图; 新算法同时 shard 训练数据和意图。 Go语言 语言支持concurrency,代码量是C++版本的40%。 实时推演算法

百度推荐系统的探索过程–刘其文

1:介绍了百度统一推荐平台CoreEngine 什么是CoreEngine: 是一个巨大的信息库 TA了解你是谁 TA能够判断什么是对你有价值的 TA能够知道你在什么时间、场景下需要

2:主要讲了三个关键问题: 如何判断内容质量: 用户决定一切:根据用户行为来判断内容质量。 后验:通过后验数据来判断效果。 先验:主要通过基础质量、热度、时效性来判断。

如何为用户建模: 用户反馈:正反馈 负反馈

如何给推荐结果排序 根据相关性、多样性、惊喜度等排序,没有一个统一的排序标准。 机器学习排序:

如何把多层次的需求转化成单一优化指标: 离线评估: 供参考的baseline 人工评估: 太依赖于主观经验 在线评估: A/B test是最客观的评价

3: CoreEngine的启示: 没有“银弹” 需要根据产品目标、数据现状、资源现状选择算法 不要手握锤子就把一切看做钉子 推荐只是实现产品目标的方式,不是产品目标本身

介绍了百度在推荐系统数据、架构、策略和产品方面的一些探索过程。搭建了统一的基础推荐平台,且进行了跨产品推荐。 百度推荐系统组据说有大概100人的团队。

创业团队:

S购物党

口碑搜索、正品比价,购物党通过商品、评价、价格等因素帮消费者解决买什么、去哪买的问题。 就是介绍了下自己的产品,没讲啥和推荐有关的内容。

豌豆猜:

是豌豆荚推出的个性化应用推荐引擎。 通过分析用户的下载、安装和卸载行为,豌豆们为您挑选贴心、聪明、有趣 的应用。豌豆猜会记住您「不喜欢」的应用,并变得更加智能。 效果:据说下载量提升不少,具体数字没记。 方法:基于内容推荐和基于协同过滤综合。 实现:目前用的mahout,据说以后可能换(mahout大而全,使用的部分算法实现为考虑通用性效率不太高)。

说他们做新产品的原则是 “糙,快,猛”!先做个东西出来,不用太好看,看用户反馈,反馈明显的,再优化,否则放弃。

微博寻人:

做了一个人–人,人–微博关键字,人–微博关系链。 数据源是爬的weibo的,专门有个产品叫中国爬萌(专业爬weibo,数据量仅次于sina)。

总设计师:梁斌,此人灰常幽默。 语录: 世界不公平,不平等的根源在于信息不对称,我的理想是让信息对称,人人平等。 大家对现在微博寻人的用户量可能没概念,大概就是盘古搜索的量。 有人提问如何赚钱,梁总说:为人民群众服务的时候,薅羊毛不能薅的太早!

各大公司大规模数据处理、机器学习、推荐系统这块还是比较重视的,并不同程度上使用在其产品上。 近几年越来越多的公司推出了推荐系统相关的功能,以提升用户体验、增加公司收入(个性化广告)。 还出现了不少以推荐为核心的公司和产品,比如简网、无觅、今晚看啥、百分点等。

我们的推荐系统目前也就实现了从无到有的阶段,以后在推荐质量反馈、大数据量处理、多种内容推荐、提高学习和反馈速度等各方面需要不断提升。

用Backbone.js绑住服务端生成的html

| Comments

去年做雪球的timeline模块时我正深受 #newTwitter 的影响,倾向于把尽可能多的逻辑放到客户端去做,最后实现的时候选择了Backbone.js。使用Backbone.js的好处就不说了,这一两年它火的一塌糊涂,到处都是介绍的文章,而且这篇文章的重点也不是这个。

下面我假设您已经了解Backbone.js的作用和实现方式。

在页面初始化的时候,与发起一个ajax请求去取初始数据相比,把初始数据输出到页面里是一个更好的方案。Backbone.js提供了一个Loading Bootstrapped Models的FAQ,雪球也正是这样做的。把初始数据的json输出到页面里,然后Backbone.js用这个json来渲染页面。

但是这一年的实践中陆续发现一些问题:接口输出的timeline json里某些字段里偶尔出现一些不可见的换行符,导致浏览器解析json的时候出错。输出json字符串有injection可能(后来今年三月份的时候backbone特意在文档里加上了提示)。另外,随着业务复杂性的增长,接口直接输出的json体积在膨胀,很多属性已经不是页面展示所必须的,json的体积已经接近甚至已经超过了生成的html的体积。

同时我还在思考另外一个问题,backbone的使用场景其实是app,DocumentCloudTrello这种需要反复对页面元素操作,应用要处理好数据和UI的一致性,初始化的时候稍微慢一点也没有关系,用backbone再好不过了。但是雪球其实更像是page,用户打开页面希望尽早的看到数据,当然我们也需要经常操作页面的元素,也需要处理数据和UI的一致性问题。

在服务端把html就拼好然后传给浏览器似乎是最直接的答案。那么接下来就面临了这篇文章要处理的问题了,如果既用服务端渲染html,又能够继续使用现有的基于backbone的客户端程序,甚至可以随时切换渲染位置。

backbone的文档似乎没有明确的给出这样的建议,但是稍微思考一下backbone View的实现方式应该可以想到,既然view的events是绑在一个根el上面的,那么这个el是一个空的wrapper或者已经渲染好的html片段并不影响事件的delegate。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var TimelineView = Backbone.View.extend({
  events: {
    "click .comment": "comment"
  }.
  comment: function(e){
   var $elm = $(e.target)
     , statusModel = this.collection.get($elm.data("id"))
   renderComment(statusModel)
  },
  render: function(){
    var timelineHtml = ''
    // some code to generate html from models
    $(this.el).html(timelineHtml)
  }
})
var statusCollection = new StatusCollection(statusList)
var timelineView = new TimelineView({
  el: $("#timeline"),
  collection: statusCollection
})
timelineView.render()

假设上面的一段代码是已有的客户端渲染的实现方式,需要说明的是statusList正是我们之前输出到页面里的timeline json,$("#timeline")是准备放timeline的wrapper。

现在改成服务端渲染之后发生的变化的后果,$("#timeline")变成了已经塞满status的列表,statusList不再存在。我们挨个解决。

$("#timeline")既然已经填满了,就不用再render啦。最后一行就改成了:

1
2
3
if ($("#timeline").html().trim()){
  timelineView.render()
}

statusList是空的,那么statusCollection也是空的,comment的时候就找不到status的model。本来作为json输出来的数据其实被塞到了dom里,那我们就应该找一个合适的时候把status model从dom里读出来。我选择在view初始化的时候获取,给TimelineView加上initialize方法。

1
2
3
4
5
6
7
8
9
10
var TimelineView = Backbone.View.extend({
  initialize: function(){
    if (this.collection.length) {
      var models = []
      // some code to read models from timeline dom
      this.collection.add(models)
    }
  },
  ...
})

好了,客户端的代码没有任何其他要改的了,所有的backbone的功能都会跟原来一样的工作,还可以吧?

最后完整的代码改造成了这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
var TimelineView = Backbone.View.extend({
  initialize: function(){
    if (this.collection.length) {
      var models = []
      // some code to read models from timeline dom
      this.collection.add(models)
    }
  },
  events: {
    "click .comment": "comment"
  }.
  comment: function(e){
   var $elm = $(e.target)
     , statusModel = this.collection.get($elm.data("id"))
   renderComment(statusModel)
  },
  render: function(){
    var timelineHtml = ''
    // some code to generate html from models
    $(this.el).html(timelineHtml)
  }
})
var statusCollection = new StatusCollection(statusList)
var timelineView = new TimelineView({
  el: $("#timeline"),
  collection: statusCollection
})
if ($("#timeline").html().trim()){
  timelineView.render()
}

后记

  • 这种处理对于需要SEO,或者特殊设备支持的应用来说更有意义。
  • 服务端渲染html会给服务端带来额外的cpu消耗,但是很小。不过我们还是做了适配,可以随时切换渲染方式。
  • 服务端渲染也需要模板,客户端渲染也需要模板,这个模板如何复用?对于使用node.js的雪球来说,很好处理。这个留给以后说。

开篇再加上2012的一点小总结

| Comments

欢迎来到雪球工程师团队博客!

马上就到年底了,也不知道这第一篇博客会不会成为最后一篇 (-_-#) 不管了,来一篇今年的总结作为开篇顺便展望一下未来 …

团队

今年人员上最大的变化是补充了移动团队,从无到有已经形成了比较完整的队伍。各个根据项目组合而成的小组也有了自己比较专注的目标,而且项目上多少都会有一些挑战而不是简单的重复性工作。总体上上看我们的工程师团队是按照计划完成了我们每一个阶段的开发任务。

开发实践

产品开发的组织上我们从年初的半年一个里程碑,逐渐缩短,后来是三个月,现在已经是每个月一个里程碑,这其中的原因是因为针对一些遇到的问题我们引入了一些方法和工具有效的提升了效率。

站立早会

一开始我们也没有特别的流程打到哪指到那,渐渐的为了团队之间的沟通更顺利,我们增加了开发之前的沟通与设计,还增加了每日(或隔日)的站立早会。早会上大家会回答三个问题:我之前做了什么?我现在在做什么?有什么需要讨论的问题?早会的效果可以说是很不错的,可以及时的发现问题然后有针对性的去解决掉,还可以督促工程师每天对已经完成的和将要做的事情提前有个计划。

Code Review

在我们的几个核心项目上,之前一直都是谁写的代码谁知道,其他人可能并不知道实现细节,也不知道是否实现的过程中有什么隐藏的问题。一直想找一个能够比较好进行 Code Review 的办法,不得法。找来找去发现其实 github 的 pull request 方式是最符合我们需要的方式,不过 github 的服务器对我们来说有点慢,弄个 Enterprise 版本又太贵,后来开窍了我们一直在用 Redmine 做项目管理,那我们干脆就山寨一个 Pull Request 好了。

成果在这里 https://github.com/xueqiu/redmine_pull_requests 欢迎 Fork 。使用半年来我们一共开启了将近 1000 个 request 了!通过引入方便的工具降低大家对改变的畏惧心理是个很有用的方法,一旦用上了就体会到好处变成习惯了。用这个工具我们还是提前发现了不少问题,更多的同学对其他人的代码也有了了解,因为有人要 review 自己的代码,也自觉的不能写的太差劲了 (-__-) 对提高代码质量还是很有帮助的。

测试

到今天我们的核心的项目都有了自己完整的、容易编写的自动化测试框架,这些项目包括 Java Web Service / NodeJS / iOS / Android ,虽然还没有很高的覆盖率,不过从新做(修改)的功能开始已经逐步开始补上和完善。测试代码不能解决逻辑问题,但可以避免人为出现的低级错误,谁用谁知道啊。

未来雪球可能也会有测试团队,但是测试团队的任务绝对不是手工点界面测试甚至不是写业务的测试代码,而是维护各种测试工具、推广测试的最佳实践,最适合写测试的人是开发者自己。

运维

对于我们现在的规模谈架构可能比较虚,我们把这项工作叫做运维吧,今年做了几个调整

  • 集中数据库,方便维护与调优
  • 独立缓存集群,扩大容量,隔离与其他系统的交叉
  • 合并服务,把尽合并的服务合并在一起(同一个项目或者同一台服务器),充分利用服务器资源

展望

最近看书看到个说法(大意)历史有两种,一种是著名的,一种是不著名的,著名的历史背后都是由不著名的历史组成和引发的。

对雪球的工程师团队来说,我们还是一支很年轻的队伍,看看这一年的改进都是非常基础性的,但对未来一定会有深远的影响。所以未来在各种提高效率的方法、工具投入任何精力都是不过分的,这也是让工程师有自我满足感的最重要因素,我个人认为没有之一。沿着这个思路,我们的目标是没有目的地的,不过方向是明确的清晰的,只要耐心踏实的坚持走下去,年轻的团队也可以很牛逼的。