继插件化后,热补丁技术在2015年开始爆发,目前已经是非常热门的Android开发技术。其中比较著名的有淘宝的Dexposed、支付宝的AndFix以及Qzone的超级热补丁方案。微信对热补丁技术的研究并不算早,大约开始于2015年6月。经过研究与尝试现有的各个方案,我们发现它们都有着自身的一些局限性。微信最终采用不同于它们的技术方案,走出了自己的实践演进之路。

另外一方面,技术应当只是热补丁方案中的一环。随着对热补丁的多次尝试与应用,微信建立起自身的流程规范,同时也不断的尝试拓展它的应用场景。通过本文,我希望大家不仅能够全面的了解各项热补丁技术的优缺点,同时也能对它的应用场景有着更加全面的认识。在此基础上,大家或许能更容易的决定是否在自己的项目中使用热补丁技术,以及应当如何使用它。

为什么需要热补丁

热补丁:让应用能够在无需重新安装的情况实现更新,帮助应用快速建立动态修复能力。

从上面的定义来看,热补丁节省Android大量应用市场发布的时间。同时用户也无需重新安装,只要上线就能无感知的更新。看起来很美好,这是否可以意味我们可以尽量使用补丁来代替发布呢?事实上,热补丁技术当前依然存在它的局限性,主要表现在以下几点:

  1. 补丁只能针对单一客户端版本,随着版本差异变大补丁体积也会增大;

  2. 补丁不能支持所有的修改,例如AndroidManifest;

  3. 补丁无论对代码还是资源的更新成功率都无法达到100%。

既然补丁技术无法完全代替升级,那它适合使用在哪些场景呢?

一. 轻量而快速的升级

热补丁技术也可以理解为一个动态修改代码与资源的通道,它适合于修改量较少的情况。以微信的多次发布为例,补丁大小均在300K以内,它相对于传统的发布有着很大的优势。

以Android用户的升级习惯,即使是相对活跃的微信也需要10天以上的时间去覆盖50%的用户。使用补丁技术,我们能做到1天覆盖70%以上。这也是基于补丁体积较小,可以直接使用移动网络下载更新。

正因如此,补丁技术非常适合使用在灰度阶段。在过去,我们需要在正式发布前保证所有严重的问题都已经得到修复,这通常需要我们经过三次以上的灰度过程,而且无法快速的验证这些问题在同一批用户的修复效果。利用热补丁技术,我们可以快速对同一批用户验证修复效果,这大大缩短我们的发布流程。

若发布版本出现问题或紧急漏洞,传统方式需要单独灰度验证修改,然后重新发布新的版本。利用补丁技术,我们只需要先上线小部分用户验证修改的效果,最后再全量上线即可。但是此种发布对线上用户影响较大, 我们需要谨慎而为。本着对用户负责的态度, 发布补丁等同于发布版本 ,它也应该严格执行完整的测试与上线流程。

总的来说,补丁技术可以降低开发成本,缩短开发周期,实现轻量而快速的升级。

二. 远端调试

一入Android深似海,Android开发的另外一个痛是机型的碎片化。我们也许都会遇到"本地不复现","日志查不出","联系用户不鸟你"的烦恼。所以补丁机制非常适合使用在远端调试上。即我们需要具备只特定用户发送补丁的能力,这对我们查找问题非常有帮助。

利用补丁技术,我们避免了骚扰用户而默默的为用户解决问题。当然这也需要非常严格的权限管理,以防恶意或随意使用。

三. 数据统计

数据统计在微信中也占据着非常重要的位置,我们也非常希望将热补丁与数据统计结合的更好。事实上,热补丁无论在普通的数据统计还是ABTest都有着非常大的优势。例如若我想对同一批用户做两种test, 传统方式无法让这批用户去安装两个版本。使用补丁技术,我们可以方便的对同一批用户更换补丁版本。在数据统计之路,如何与补丁技术结合的更好,更加精准的控制样本人数与比例,这也是微信当前努力发展的一个方向。

四. 其他

事实上,Android官方也使用热补丁技术实现Instant Run。它分为Hot Swap、Warm Swap与Cold Swap三种方式,大家可以参考 英文介绍 ,也可以看参考文章中的翻译稿。最新的Instant App应该也是采用类似的原理,但是Google Play是不允许下发代码的,这个海外App需要注意一下。

微信热补丁技术的演进之路

在了解补丁技术可以与适合做什么之后,我们回到技术本身。由于 Dexposed 无法支持全平台,并不适合应用到商业产品中。所以这里我们只简单介绍Andfix、Qzone、微信几套方案的实现,以及它们方案面临着的问题,大家也可以参考资料中的 各大热补丁方案分析和比较 一文。

一. AndFix

AndFix 采用native hook的方式,这套方案直接使用 dalvik_replaceMethod 替换class中方法的实现。由于它并没有整体替换class, 而field在class中的相对地址在class加载时已确定,所以AndFix无法支持新增或者删除filed的情况(通过替换 init 与clinit 只可以修改field的数值)。

也正因如此,Andfix可以支持的补丁场景相对有限,仅仅可以使用它来修复特定问题。结合之前的发布流程,我们更希望补丁对开发者是不感知的,即他不需要清楚这个修改是对补丁版本还是正式发布版本(事实上我们也是使用git分支管理+cherry-pick方式)。另一方面,使用native替换将会面临比较复杂的兼容性问题。

相比其他方案,AndFix的最大优点在于立即生效。事实上,AndFix的实现与 Instant Run的热插拔 有点类似,但是由于使用场景的限制,微信在最初期已排除使用这一方案。

二. Qzone

Qzone方案并没有开源,但在github上的 Nuwa 采用了相同的方式。这个方案使用classloader的方式,能实现更加友好的类替换。而且这与我们加载Multidex的做法相似,能基本保证稳定性与兼容性。具体原理在这里不再细说,大家可以参考"安卓App热补丁动态修复技术介绍"这篇文章。

本方案为了解决 unexpected DEX problem 异常而采用插桩的方式,从而规避问题的出现。事实上,Android系统的这些检查规则是非常有意义的,这会导致Qzone方案在Dalvik与Art都会产生一些问题。

  • Dalvik; 在dexopt过程,若class verify通过会写入pre-verify标志,在经过optimize之后再写入odex文件。这里的optimize主要包括inline以及quick指令优化等。

若采用插桩导致所有类都非preverify,这导致verify与optimize操作会在加载类时触发。这会有一定的性能损耗,微信分别采用插桩与不插桩两种方式做过两种测试,一是连续加载700个50行左右的类,一是统计微信整个启动完成的耗时。

平均每个类verify+optimize(跟类的大小有关系)的耗时并不长,而且这个耗时每个类只有一次。但由于启动时会加载大量的类,在这个情况影响还是比较大。

  • Art; Art采用了新的方式,插桩对代码的执行效率并没有什么影响。但是若补丁中的类出现修改类变量或者方法,可能会导致出现内存地址错乱的问题。为了解决这个问题我们需要将修改了变量、方法以及接口的类的父类以及调用这个类的所有类都加入到补丁包中。这可能会带来补丁包大小的急剧增加。

这里是因为在dex2oat时 fast* 已经将类能确定的各个地址写死。如果运行时补丁包的地址出现改变,原始类去调用时就会出现地址错乱。这里说的可能不够详细,事实上微信当时为了查清这两个问题,也花费了一定的时间将Dalvik跟Art的流程基本搞透。若大家对这里感兴趣,后续在单独的文章详细论述。

总的来说,Qzone方案好处在于开发透明,简单,这一套方案目前的应用成功率也是最高的, 但在补丁包大小与性能损耗上有一定的局限性。特别是无论我们是否真正应用补丁,都会因为插桩导致对程序运行时的性能产生影响。微信对于性能要求较高,所以我们也没有采用这套方案。

三. 微信热补丁方案

有没有那么一种方案,能做到开发透明,但是却没有Qzone方案的缺陷呢?Instant Run的冷插拔与buck的 exopackage 或许能给我们灵感,它们的思想都是全量替换新的Dex。即我们完全使用了新的Dex,那样既不出现Art地址错乱的问题,在Dalvik也无须插桩。当然考虑到补丁包的体积,我们不能直接将新的Dex放在里面。但我们可以将新旧两个Dex的差异放到补丁包中,最简单我们可以采用BsDiff算法。

简单来说,在编译时通过新旧两个Dex生成差异patch.dex。在运行时,将差异patch.dex重新跟原始安装包的旧Dex还原为新的Dex。这个过程可能比较耗费时间与内存,所以我们是单独放在一个后台进程:patch中。为了补丁包尽量的小,微信自研了DexDiff算法,它深度利用Dex的格式来减少差异的大小。它的粒度是Dex格式的每一项,可以充分利用原本Dex的信息,而BsDiff的粒度是文件,AndFix/Qzone的粒度为class。

这块后面我希望后面用单独的文章来讲述,这里先做一个铺垫,大致的效果如下图。在最极端的情况,由于利用了原本dex的信息完全替换一个13M的Dex,我们的补丁大小也仅仅只有6.6M。

但是这套方案并非没有缺点,它带来的问题有两个:

  1. 占用Rom体积;这边大约是你所修改Dex大小的1.5倍( Dex压缩成jar的大小加上 生成的dexopt文件大小)。

  2. 一个额外的合成过程;虽然我们单独放在一个进程上处理,但是合成时间的长短与内存消耗也会影响最终的成功率(与修改Dex大小、补丁大小相关)。

微信的热补丁方案叫做Tinker,也算缅怀一下Dota中的地精修补匠,希望能做到无限刷新。

限于篇幅,这里对Dex、library以及资源的更多技术细节并没有详细的论述,这里希望放在后面的单独文章中。我们最后从整体比较一下这几种方案:

若不care性能损耗与补丁包大小,Qzone方案是最简单且成功率最高的方案(没有单独的合成过程)。相对Tinker来说,它的占用Rom体积也更小。另一方面,Qzone与Tinker的成功率当前大约相差3%左右。

事实上,一个完整的框架应该也是一个容易使用的框架。Tinker对补丁版本管理、进程管理、安全校验等都有着很好的支持。同时我们也支持gradle与命名行两种接入方式。希望在不久的将来,它可以很快的跟大家见面。

微信的热补丁应用现状

上一章节我们简单比较了各个热补丁的技术方案,它们解决了如何生成与加载补丁包的问题。但一个完善的热补丁系统不应该仅限于此,它还需要包括以下几个方面:

  • 网络通道;这里要解决的问题是决定补丁以何种方式推送给哪部分的用户。

  • 上线与后台管理平台;这里主要包括热补丁的上线管理,历史管理以及上报分析,报警监控等;

一. 网络通道现状

网络通道负责的将补丁包交付给用户,这个包括特定用户与全量用户两种情况。事实上,微信当前针对热补丁有以下三种通道更新:

  • pull通道; 在登陆/24小时等时机,通过pull方式查询后台是否有对应的补丁包更新,这也是我们最常用的方式;

  • 指定版本的push通道; 针对版本的通道,在紧急情况下,我们可以在一个小时内向所有用户下发补丁包更新。

  • 指定特定用户的push通道;对特定用户或用户组做远程调试。

事实上,对于大部分的应用来说,假设不实现push通道,CDN+pull通道实现起来还是较为容易。

二. 上线与管理平台现状

上线与管理平台主要为了快速上线,管理历史记录,以及监控补丁的运行情况等(界面比较丑陋,因为我们木有美工啊)。

事实上,微信发布热补丁是非常慎重的。它整个发布流程与升级版本是保持一致的,也必须修改版本号、经过严格的完整测试流程等。我们也会通过灰度的方式上线,同时监控补丁版本的各个指标。这里的为了完整的监控补丁的情况,我们做的工作有:

  • 1分钟粒度的每小时/每天的各版本累积用户,及时监控补丁版本的人数与活跃;

  • 3分钟粒度的Crash统计,基准版本与补丁版本的Crash每小时/每天的两个维度对照;

  • 10分钟粒度的补丁监控信息上报。

三. 补丁成功率现状

应用成功率= 补丁版本人数/补丁发布前该版本人数

由于可能存在基准或补丁版本用户安装了其他版本,所以本统计结果应略为偏低,但它能现实的反应补丁的线上覆盖情况。

使用Qzone方案,微信补丁在10天后的应用成功率大约在98.5%左右。使用Tinker大约只有95.5%左右,主要原因在于空间不足以及后台进程被杀。在这里我们也在尝试使用重试的方式以及降低合成的耗时与内存,从而提升成功率。

热补丁技术发展的很快,Android推出的Instant App也令人期待。但是在国内,似乎我们还是指望自己更靠谱一点。每一个的应用的需求都不太一致,这里大致讲了一些微信的实践经验,希望对大家有帮助。

未来工作

随着微信部门内从“单APP”向“多APP”演进,微信也正在迈入开源化的开发实践。我们希望将各个功能组件化,从而做可以到快速复制与应用。微信的热补丁框架“Tinker”当前也在经历从微信分离,又合入到微信的过程。希望在不久的将来,我们也可以将“Tinker”以及微信中一些其他的组件开源出去。

我们也希望可以找一些App作为内测,给我们提供宝贵的意见。若对微信的Tinker方案感兴趣的用户,可以单独发消息或在文章末留言注明姓名、所在公司以及负责的App,我们希望挑选部分产品作为内测。

参考文章

大家可以点击下方的阅读原文,即可直接点击跳转。

  1. Dexposed github ( https://github.com/alibaba/dexposed )

  2. AndFix github ( https://github.com/alibaba/AndFix )

  3. Nuwa github ( https://github.com/jasonross/Nuwa )

  4. Qzone实现原理解析 ( https://mp.weixin.qq.com/s?__biz=MzI1MTA1MzM2Nw==&mid=400118620&idx=1&sn=b4fdd5055731290eef12ad0d17f39d4a )

  5. Instant Run英文原文 ( https://medium.com/google-developers/instant-run-how-does-it-work-294a1633367f#.c088qhdxu )

  6. Instant Run工作原理及用法中文翻译稿 ( http://www.jianshu.com/p/2e23ba9ff14b )

  7. Buck exopackage 介绍 ( https://buckbuild.com/article/exopackage.html )

  8. 各大热补丁方案分析和比较 (http://blog.zhaiyifan.cn/2015/11/20/HotPatchCompare/ )

posted @ 2016-07-07 16:10 小马歌 阅读(220) | 评论 (0)编辑 收藏
 
     摘要: from:http://mp.weixin.qq.com/s?__biz=MzAwNDY1ODY2OQ==&mid=207243549&idx=1&sn=4ebe4beb8123f1b5ab58810ac8bc5994&scene=0#rd前言:在13年11月中旬时,因为基础组件组人手紧张,Leo安排我和春哥去广州轮岗支援。刚到广州的时候,Ray让我和春哥对Line...  阅读全文
posted @ 2016-07-07 16:09 小马歌 阅读(296) | 评论 (0)编辑 收藏
 

from:http://www.ibm.com/developerworks/cn/web/1103_zhaoct_recommstudy1/


“探索推荐引擎内部的秘密”系列将带领读者从浅入深的学习探索推荐引擎的机制,实现方法,其中还涉及一些基本的优化方法,例如聚类和分类的应用。同时在理论讲解的基础上,还会结合 Apache Mahout 介绍如何在大规模数据上实现各种推荐策略,进行策略优化,构建高效的推荐引擎的方法。本文作为这个系列的第一篇文章,将深入介绍推荐引擎的工作原理,和其中涉及的各种推荐机制,以及它们各自的优缺点和适用场景,帮助用户清楚的了解和快速构建适合自己的推荐引擎。

信息发现

如今已经进入了一个数据爆炸的时代,随着 Web 2.0 的发展, Web 已经变成数据分享的平台,那么,如何让人们在海量的数据中想要找到他们需要的信息将变得越来越难。

在这样的情形下,搜索引擎(Google,Bing,百度等等)成为大家快速找到目标信息的最好途径。在用户对自己需求相对明确的时候,用搜索引擎很方便的通过关键字搜索很快的找到自己需要的信息。但搜索引擎并不能完全满足用户对信息发现的需求,那是因为在很多情况下,用户其实并不明确自己的需要,或者他们的需求很难用简单的关键字来表述。又或者他们需要更加符合他们个人口味和喜好的结果,因此出现了推荐系统,与搜索引擎对应,大家也习惯称它为推荐引擎。

随着推荐引擎的出现,用户获取信息的方式从简单的目标明确的数据的搜索转换到更高级更符合人们使用习惯的信息发现。

如今,随着推荐技术的不断发展,推荐引擎已经在电子商务 (E-commerce,例如 Amazon,当当网 ) 和一些基于 social 的社会化站点 ( 包括音乐,电影和图书分享,例如豆瓣,Mtime 等 ) 都取得很大的成功。这也进一步的说明了,Web2.0 环境下,在面对海量的数据,用户需要这种更加智能的,更加了解他们需求,口味和喜好的信息发现机制。

回页首

推荐引擎

前面介绍了推荐引擎对于现在的 Web2.0 站点的重要意义,这一章我们将讲讲推荐引擎到底是怎么工作的。推荐引擎利用特殊的信息过滤技术,将不同的物品或内容推荐给可能对它们感兴趣的用户。

 


图 1. 推荐引擎工作原理图
图 1. 推荐引擎工作原理图 

图 1 给出了推荐引擎的工作原理图,这里先将推荐引擎看作黑盒,它接受的输入是推荐的数据源,一般情况下,推荐引擎所需要的数据源包括:

  • 要推荐物品或内容的元数据,例如关键字,基因描述等;
  • 系统用户的基本信息,例如性别,年龄等
  • 用户对物品或者信息的偏好,根据应用本身的不同,可能包括用户对物品的评分,用户查看物品的记录,用户的购买记录等。其实这些用户的偏好信息可以分为两类:
  • 显式的用户反馈:这类是用户在网站上自然浏览或者使用网站以外,显式的提供反馈信息,例如用户对物品的评分,或者对物品的评论。
  • 隐式的用户反馈:这类是用户在使用网站是产生的数据,隐式的反应了用户对物品的喜好,例如用户购买了某物品,用户查看了某物品的信息等等。

显式的用户反馈能准确的反应用户对物品的真实喜好,但需要用户付出额外的代价,而隐式的用户行为,通过一些分析和处理,也能反映用户的喜好,只是数据不是很精确,有些行为的分析存在较大的噪音。但只要选择正确的行为特征,隐式的用户反馈也能得到很好的效果,只是行为特征的选择可能在不同的应用中有很大的不同,例如在电子商务的网站上,购买行为其实就是一个能很好表现用户喜好的隐式反馈。

推荐引擎根据不同的推荐机制可能用到数据源中的一部分,然后根据这些数据,分析出一定的规则或者直接对用户对其他物品的喜好进行预测计算。这样推荐引擎可以在用户进入的时候给他推荐他可能感兴趣的物品。

推荐引擎的分类

推荐引擎的分类可以根据很多指标,下面我们一一介绍一下:

  1. 推荐引擎是不是为不同的用户推荐不同的数据

    根据这个指标,推荐引擎可以分为基于大众行为的推荐引擎和个性化推荐引擎

    • 根据大众行为的推荐引擎,对每个用户都给出同样的推荐,这些推荐可以是静态的由系统管理员人工设定的,或者基于系统所有用户的反馈统计计算出的当下比较流行的物品。
    • 个性化推荐引擎,对不同的用户,根据他们的口味和喜好给出更加精确的推荐,这时,系统需要了解需推荐内容和用户的特质,或者基于社会化网络,通过找到与当前用户相同喜好的用户,实现推荐。

    这是一个最基本的推荐引擎分类,其实大部分人们讨论的推荐引擎都是将个性化的推荐引擎,因为从根本上说,只有个性化的推荐引擎才是更加智能的信息发现过程。

  2. 根据推荐引擎的数据源

    其实这里讲的是如何发现数据的相关性,因为大部分推荐引擎的工作原理还是基于物品或者用户的相似集进行推荐。那么参考图 1 给出的推荐系统原理图,根据不同的数据源发现数据相关性的方法可以分为以下几种:

    • 根据系统用户的基本信息发现用户的相关程度,这种被称为基于人口统计学的推荐(Demographic-based Recommendation)
    • 根据推荐物品或内容的元数据,发现物品或者内容的相关性,这种被称为基于内容的推荐(Content-based Recommendation)
    • 根据用户对物品或者信息的偏好,发现物品或者内容本身的相关性,或者是发现用户的相关性,这种被称为基于协同过滤的推荐(Collaborative Filtering-based Recommendation)。
  3. 根据推荐模型的建立方式

    可以想象在海量物品和用户的系统中,推荐引擎的计算量是相当大的,要实现实时的推荐务必需要建立一个推荐模型,关于推荐模型的建立方式可以分为以下几种:

    • 基于物品和用户本身的,这种推荐引擎将每个用户和每个物品都当作独立的实体,预测每个用户对于每个物品的喜好程度,这些信息往往是用一个二维矩阵描述的。由于用户感兴趣的物品远远小于总物品的数目,这样的模型导致大量的数据空置,即我们得到的二维矩阵往往是一个很大的稀疏矩阵。同时为了减小计算量,我们可以对物品和用户进行聚类, 然后记录和计算一类用户对一类物品的喜好程度,但这样的模型又会在推荐的准确性上有损失。
    • 基于关联规则的推荐(Rule-based Recommendation):关联规则的挖掘已经是数据挖掘中的一个经典的问题,主要是挖掘一些数据的依赖关系,典型的场景就是“购物篮问题”,通过关联规则的挖掘,我们可以找到哪些物品经常被同时购买,或者用户购买了一些物品后通常会购买哪些其他的物品,当我们挖掘出这些关联规则之后,我们可以基于这些规则给用户进行推荐。
    • 基于模型的推荐(Model-based Recommendation):这是一个典型的机器学习的问题,可以将已有的用户喜好信息作为训练样本,训练出一个预测用户喜好的模型,这样以后用户在进入系统,可以基于此模型计算推荐。这种方法的问题在于如何将用户实时或者近期的喜好信息反馈给训练好的模型,从而提高推荐的准确度。

其实在现在的推荐系统中,很少有只使用了一个推荐策略的推荐引擎,一般都是在不同的场景下使用不同的推荐策略从而达到最好的推荐效果,例如 Amazon 的推荐,它将基于用户本身历史购买数据的推荐,和基于用户当前浏览的物品的推荐,以及基于大众喜好的当下比较流行的物品都在不同的区域推荐给用户,让用户可以从全方位的推荐中找到自己真正感兴趣的物品。


深入推荐机制

这一章的篇幅,将详细介绍各个推荐机制的工作原理,它们的优缺点以及应用场景。

基于人口统计学的推荐

基于人口统计学的推荐机制(Demographic-based Recommendation)是一种最易于实现的推荐方法,它只是简单的根据系统用户的基本信息发现用户的相关程度,然后将相似用户喜爱的其他物品推荐给当前用户,图 2 给出了这种推荐的工作原理。


图 2. 基于人口统计学的推荐机制的工作原理
图 2. 基于人口统计学的推荐机制的工作原理 

从图中可以很清楚的看到,首先,系统对每个用户都有一个用户 Profile 的建模,其中包括用户的基本信息,例如用户的年龄,性别等等;然后,系统会根据用户的 Profile 计算用户的相似度,可以看到用户 A 的 Profile 和用户 C 一样,那么系统会认为用户 A 和 C 是相似用户,在推荐引擎中,可以称他们是“邻居”;最后,基于“邻居”用户群的喜好推荐给当前用户一些物品,图中将用户 A 喜欢的物品 A 推荐给用户 C。

这种基于人口统计学的推荐机制的好处在于:

  1. 因为不使用当前用户对物品的喜好历史数据,所以对于新用户来讲没有“冷启动(Cold Start)”的问题。
  2. 这个方法不依赖于物品本身的数据,所以这个方法在不同物品的领域都可以使用,它是领域独立的(domain-independent)。

那么这个方法的缺点和问题是什么呢?这种基于用户的基本信息对用户进行分类的方法过于粗糙,尤其是对品味要求较高的领域,比如图书,电影和音乐等领域,无法得到很好的推荐效果。可能在一些电子商务的网站中,这个方法可以给出一些简单的推荐。另外一个局限是,这个方法可能涉及到一些与信息发现问题本身无关却比较敏感的信息,比如用户的年龄等,这些用户信息不是很好获取。

基于内容的推荐

基于内容的推荐是在推荐引擎出现之初应用最为广泛的推荐机制,它的核心思想是根据推荐物品或内容的元数据,发现物品或者内容的相关性,然后基于用户以往的喜好记录,推荐给用户相似的物品。图 3 给出了基于内容推荐的基本原理。


图 3. 基于内容推荐机制的基本原理
图 3. 基于内容推荐机制的基本原理 

图 3 中给出了基于内容推荐的一个典型的例子,电影推荐系统,首先我们需要对电影的元数据有一个建模,这里只简单的描述了一下电影的类型;然后通过电影的元数据发现电影间的相似度,因为类型都是“爱情,浪漫”电影 A 和 C 被认为是相似的电影(当然,只根据类型是不够的,要得到更好的推荐,我们还可以考虑电影的导演,演员等等);最后实现推荐,对于用户 A,他喜欢看电影 A,那么系统就可以给他推荐类似的电影 C。

这种基于内容的推荐机制的好处在于它能很好的建模用户的口味,能提供更加精确的推荐。但它也存在以下几个问题:

  1. 需要对物品进行分析和建模,推荐的质量依赖于对物品模型的完整和全面程度。在现在的应用中我们可以观察到关键词和标签(Tag)被认为是描述物品元数据的一种简单有效的方法。
  2. 物品相似度的分析仅仅依赖于物品本身的特征,这里没有考虑人对物品的态度。
  3. 因为需要基于用户以往的喜好历史做出推荐,所以对于新用户有“冷启动”的问题。

虽然这个方法有很多不足和问题,但他还是成功的应用在一些电影,音乐,图书的社交站点,有些站点还请专业的人员对物品进行基因编码,比如潘多拉,在一份报告中说道,在潘多拉的推荐引擎中,每首歌有超过 100 个元数据特征,包括歌曲的风格,年份,演唱者等等。

基于协同过滤的推荐

随着 Web2.0 的发展,Web 站点更加提倡用户参与和用户贡献,因此基于协同过滤的推荐机制因运而生。它的原理很简单,就是根据用户对物品或者信息的偏好,发现物品或者内容本身的相关性,或者是发现用户的相关性,然后再基于这些关联性进行推荐。基于协同过滤的推荐可以分为三个子类:基于用户的推荐(User-based Recommendation),基于项目的推荐(Item-based Recommendation)和基于模型的推荐(Model-based Recommendation)。下面我们一个一个详细的介绍着三种协同过滤的推荐机制。

基于用户的协同过滤推荐

基于用户的协同过滤推荐的基本原理是,根据所有用户对物品或者信息的偏好,发现与当前用户口味和偏好相似的“邻居”用户群,在一般的应用中是采用计算“K- 邻居”的算法;然后,基于这 K 个邻居的历史偏好信息,为当前用户进行推荐。下图 4 给出了原理图。


图 4. 基于用户的协同过滤推荐机制的基本原理
图 4. 基于用户的协同过滤推荐机制的基本原理 

上图示意出基于用户的协同过滤推荐机制的基本原理,假设用户 A 喜欢物品 A,物品 C,用户 B 喜欢物品 B,用户 C 喜欢物品 A ,物品 C 和物品 D;从这些用户的历史喜好信息中,我们可以发现用户 A 和用户 C 的口味和偏好是比较类似的,同时用户 C 还喜欢物品 D,那么我们可以推断用户 A 可能也喜欢物品 D,因此可以将物品 D 推荐给用户 A。

基于用户的协同过滤推荐机制和基于人口统计学的推荐机制都是计算用户的相似度,并基于“邻居”用户群计算推荐,但它们所不同的是如何计算用户的相似度,基于人口统计学的机制只考虑用户本身的特征,而基于用户的协同过滤机制可是在用户的历史偏好的数据上计算用户的相似度,它的基本假设是,喜欢类似物品的用户可能有相同或者相似的口味和偏好。

基于项目的协同过滤推荐

基于项目的协同过滤推荐的基本原理也是类似的,只是说它使用所有用户对物品或者信息的偏好,发现物品和物品之间的相似度,然后根据用户的历史偏好信息,将类似的物品推荐给用户,图 5 很好的诠释了它的基本原理。

假设用户 A 喜欢物品 A 和物品 C,用户 B 喜欢物品 A,物品 B 和物品 C,用户 C 喜欢物品 A,从这些用户的历史喜好可以分析出物品 A 和物品 C 时比较类似的,喜欢物品 A 的人都喜欢物品 C,基于这个数据可以推断用户 C 很有可能也喜欢物品 C,所以系统会将物品 C 推荐给用户 C。

与上面讲的类似,基于项目的协同过滤推荐和基于内容的推荐其实都是基于物品相似度预测推荐,只是相似度计算的方法不一样,前者是从用户历史的偏好推断,而后者是基于物品本身的属性特征信息。


图 5. 基于项目的协同过滤推荐机制的基本原理
图 5. 基于项目的协同过滤推荐机制的基本原理 

同时协同过滤,在基于用户和基于项目两个策略中应该如何选择呢?其实基于项目的协同过滤推荐机制是 Amazon 在基于用户的机制上改良的一种策略,因为在大部分的 Web 站点中,物品的个数是远远小于用户的数量的,而且物品的个数和相似度相对比较稳定,同时基于项目的机制比基于用户的实时性更好一些。但也不是所有的场景都是这样的情况,可以设想一下在一些新闻推荐系统中,也许物品,也就是新闻的个数可能大于用户的个数,而且新闻的更新程度也有很快,所以它的形似度依然不稳定。所以,其实可以看出,推荐策略的选择其实和具体的应用场景有很大的关系。

基于模型的协同过滤推荐

基于模型的协同过滤推荐就是基于样本的用户喜好信息,训练一个推荐模型,然后根据实时的用户喜好的信息进行预测,计算推荐。

基于协同过滤的推荐机制是现今应用最为广泛的推荐机制,它有以下几个显著的优点:

  1. 它不需要对物品或者用户进行严格的建模,而且不要求物品的描述是机器可理解的,所以这种方法也是领域无关的。
  2. 这种方法计算出来的推荐是开放的,可以共用他人的经验,很好的支持用户发现潜在的兴趣偏好

而它也存在以下几个问题:

  1. 方法的核心是基于历史数据,所以对新物品和新用户都有“冷启动”的问题。
  2. 推荐的效果依赖于用户历史偏好数据的多少和准确性。
  3. 在大部分的实现中,用户历史偏好是用稀疏矩阵进行存储的,而稀疏矩阵上的计算有些明显的问题,包括可能少部分人的错误偏好会对推荐的准确度有很大的影响等等。
  4. 对于一些特殊品味的用户不能给予很好的推荐。
  5. 由于以历史数据为基础,抓取和建模用户的偏好后,很难修改或者根据用户的使用演变,从而导致这个方法不够灵活。

混合的推荐机制

在现行的 Web 站点上的推荐往往都不是单纯只采用了某一种推荐的机制和策略,他们往往是将多个方法混合在一起,从而达到更好的推荐效果。关于如何组合各个推荐机制,这里讲几种比较流行的组合方法。

  1. 加权的混合(Weighted Hybridization): 用线性公式(linear formula)将几种不同的推荐按照一定权重组合起来,具体权重的值需要在测试数据集上反复实验,从而达到最好的推荐效果。
  2. 切换的混合(Switching Hybridization):前面也讲到,其实对于不同的情况(数据量,系统运行状况,用户和物品的数目等),推荐策略可能有很大的不同,那么切换的混合方式,就是允许在不同的情况下,选择最为合适的推荐机制计算推荐。
  3. 分区的混合(Mixed Hybridization):采用多种推荐机制,并将不同的推荐结果分不同的区显示给用户。其实,Amazon,当当网等很多电子商务网站都是采用这样的方式,用户可以得到很全面的推荐,也更容易找到他们想要的东西。
  4. 分层的混合(Meta-Level Hybridization): 采用多种推荐机制,并将一个推荐机制的结果作为另一个的输入,从而综合各个推荐机制的优缺点,得到更加准确的推荐。

推荐引擎的应用

介绍完推荐引擎的基本原理,基本推荐机制,下面简要分析几个有代表性的推荐引擎的应用,这里选择两个领域:Amazon 作为电子商务的代表,豆瓣作为社交网络的代表。

推荐在电子商务中的应用 – Amazon

Amazon 作为推荐引擎的鼻祖,它已经将推荐的思想渗透在应用的各个角落。Amazon 推荐的核心是通过数据挖掘算法和比较用户的消费偏好于其他用户进行对比,借以预测用户可能感兴趣的商品。对应于上面介绍的各种推荐机制,Amazon 采用的是分区的混合的机制,并将不同的推荐结果分不同的区显示给用户,图 6 和图 7 展示了用户在 Amazon 上能得到的推荐。


图 6. Amazon 的推荐机制 - 首页
图 6. Amazon 的推荐机制 - 首页 

图 7. Amazon 的推荐机制 - 浏览物品
图 7. Amazon 的推荐机制 - 浏览物品 

Amazon 利用可以记录的所有用户在站点上的行为,根据不同数据的特点对它们进行处理,并分成不同区为用户推送推荐:

  • 今日推荐 (Today's Recommendation For You): 通常是根据用户的近期的历史购买或者查看记录,并结合时下流行的物品给出一个折中的推荐。
  • 新产品的推荐 (New For You): 采用了基于内容的推荐机制 (Content-based Recommendation),将一些新到物品推荐给用户。在方法选择上由于新物品没有大量的用户喜好信息,所以基于内容的推荐能很好的解决这个“冷启动”的问题。
  • 捆绑销售 (Frequently Bought Together): 采用数据挖掘技术对用户的购买行为进行分析,找到经常被一起或同一个人购买的物品集,进行捆绑销售,这是一种典型的基于项目的协同过滤推荐机制。
  • 别人购买 / 浏览的商品 (Customers Who Bought/See This Item Also Bought/See): 这也是一个典型的基于项目的协同过滤推荐的应用,通过社会化机制用户能更快更方便的找到自己感兴趣的物品。

值得一提的是,Amazon 在做推荐时,设计和用户体验也做得特别独到:

Amazon 利用有它大量历史数据的优势,量化推荐原因。

  • 基于社会化的推荐,Amazon 会给你事实的数据,让用户信服,例如:购买此物品的用户百分之多少也购买了那个物品;
  • 基于物品本身的推荐,Amazon 也会列出推荐的理由,例如:因为你的购物框中有 ***,或者因为你购买过 ***,所以给你推荐类似的 ***。

另外,Amazon 很多推荐是基于用户的 profile 计算出来的,用户的 profile 中记录了用户在 Amazon 上的行为,包括看了那些物品,买了那些物品,收藏夹和 wish list 里的物品等等,当然 Amazon 里还集成了评分等其他的用户反馈的方式,它们都是 profile 的一部分,同时,Amazon 提供了让用户自主管理自己 profile 的功能,通过这种方式用户可以更明确的告诉推荐引擎他的品味和意图是什么。

推荐在社交网站中的应用 – 豆瓣

豆瓣是国内做的比较成功的社交网站,它以图书,电影,音乐和同城活动为中心,形成一个多元化的社交网络平台,自然推荐的功能是必不可少的,下面我们看看豆瓣是如何推荐的。


图 8 . 豆瓣的推荐机制 - 豆瓣电影
图 8 . 豆瓣的推荐机制 - 豆瓣电影 

当你在豆瓣电影中将一些你看过的或是感兴趣的电影加入你看过和想看的列表里,并为它们做相应的评分,这时豆瓣的推荐引擎已经拿到你的一些偏好信息,那么它将给你展示如图 8 的电影推荐。


图 9 . 豆瓣的推荐机制 - 基于用户品味的推荐
图 9 . 豆瓣的推荐机制 - 基于用户品味的推荐 

豆瓣的推荐是通过“豆瓣猜”,为了让用户清楚这些推荐是如何来的,豆瓣还给出了“豆瓣猜”的一个简要的介绍。

你的个人推荐是根据你的收藏和评价自动得出的,每个人的推荐清单都不同。你的收藏和评价越多,豆瓣给你的推荐会越准确和丰富。
每天推荐的内容可能会有变化。随着豆瓣的长大,给你推荐的内容也会越来越准。

这一点让我们可以清晰明了的知道,豆瓣必然是基于社会化的协同过滤的推荐,这样用户越多,用户的反馈越多,那么推荐的效果会越来越准确。

相对于 Amazon 的用户行为模型,豆瓣电影的模型更加简单,就是“看过”和“想看”,这也让他们的推荐更加专注于用户的品味,毕竟买东西和看电影的动机还是有很大不同的。

另外,豆瓣也有基于物品本身的推荐,当你查看一些电影的详细信息的时候,他会给你推荐出“喜欢这个电影的人也喜欢的电影”, 如图 10,这是一个基于协同过滤的应用。


图 10 . 豆瓣的推荐机制 - 基于电影本身的推荐
图 10 . 豆瓣的推荐机制 - 基于电影本身的推荐 

 

 

 

总结

在网络数据爆炸的年代,如何让用户更快的找到想要的数据,如何让用户发现自己潜在的兴趣和需求,无论是对于电子商务还是社会网络的应用都是至关重要的。推荐引擎的出现,使得这个问题越来越被大家关注。但对大多数人来讲,也许还在惊叹它为什么总是能猜到你到底想要些什么。推荐引擎的魔力在于你不清楚在这个推荐背后,引擎到底记录和推理了些什么。

通过这篇综述性的文章,你可以了解,其实推荐引擎只是默默的记录和观察你的一举一动,然后再借由所有用户产生的海量数据分析和发现其中的规律,进而慢慢的了解你,你的需求,你的习惯,并默默的无声息的帮助你快速的解决你的问题,找到你想要的东西。

其实,回头想想,很多时候,推荐引擎比你更了解你自己。

通过第一篇文章,相信大家对推荐引擎有一个清晰的第一印象,本系列的下一篇文章将深入介绍基于协同过滤的推荐策略。在现今的推荐技术和算法中,最被大家广泛认可和采用的就是基于协同过滤的推荐方法。它以其方法模型简单,数据依赖性低,数据方便采集,推荐效果较优等多个优点成为大众眼里的推荐算法“No.1”。本文将带你深入了解协同过滤的秘密,并给出基于 Apache Mahout 的协同过滤算法的高效实现。Apache Mahout 是 ASF 的一个较新的开源项目,它源于 Lucene,构建在 Hadoop 之上,关注海量数据上的机器学习经典算法的高效实现。

感谢大家对本系列的关注和支持。

posted @ 2016-07-04 15:48 小马歌 阅读(205) | 评论 (0)编辑 收藏
 

这是关于 C1000K 序列文章的第二篇, 在前一篇文章 构建C1000K的服务器(1) – 基础 中, 介绍了支持 C1000K 的 Linux 系统的内核参数调整和系统设置. 在本篇文章中, 将对一个真正的应用服务器做 C1000K 测试.

Comet 服务器是一类逻辑相对简单, 需要高并发连接的服务器. Comet 在网站系统中的应用非常广泛, 可以见这篇日志的介绍: http://www.ideawu.net/blog/archives/737.html.

HTTP 协议处理

要开发一个支持百万并发连接的 Comet 服务器, 我选择 C/C++ 语言, 当然还有其它的选择如 Erlang, Java 等. 对于一个只支持 long-polling Comet 服务器, 首先要具备解析 HTTP 协议的能力, 我选择 libevent 来处理 HTTP 协议.

通道和订阅者管理

服务器在启动的时候, 就预先分配了 100 万个通道对象的空间, 但订阅者对象是按需分配的, 通过内存池方式. 100 万个通道对象和程序的其它数据占用了 24MB 的内存.

Benchmark

启动 icomet 服务器:

./icomet 

服务器监听了 100 个端口, 是为了测试方便, 原因见前一篇文章的分析: 构建C1000K的服务器(1) – 基础.

然后启动 benchmark 客户端:

./tools/benchmark 127.0.0.1 8100 

benchmark 程序每创建十万个连接, 就会暂停, 按回车后继续. 通过 top/ps 查看 icomet 进程的内存占用. 最终, 得出如下数据.

连接数进程VIRT进程RES
039m24m
100000302m288m
200000579m565m
5000001441m1427m
10000002734m2720m

可以看到, 每一个 Comet 连接大约占用了 2.7KB 的内存. 此时, 服务器空闲, 进程占用 CPU 为 0%.

项目的代码在: https://github.com/ideawu/icomet, 欢迎大家试用, 并反馈你的测试数据.

Related posts:

  1. HTTP 长连接技术 Comet
  2. 构建C1000K的服务器(1) – 基础
  3. iComet 的一个应用场景
  4. 长连接技术的应用
posted @ 2016-05-26 11:17 小马歌 阅读(298) | 评论 (0)编辑 收藏
 
from:http://www.ideawu.net/blog/archives/740.html?cp=2#comments

著名的 C10K 问题提出的时候, 正是 2001 年, 到如今 12 年后的 2013 年, C10K 已经不是问题了, 任何一个普通的程序员, 都能利用手边的语言和库, 轻松地写出 C10K 的服务器. 这既得益于软件的进步, 也得益于硬件性能的提高.

现在, 该是考虑 C1000K, 也就是百万连接的问题的时候了. 像 Twitter, weibo, Facebook 这些网站, 它们的同时在线用户有上千万, 同时又希望消息能接近实时地推送给用户, 这就需要服务器能维持和上千万用户的 TCP 网络连接, 虽然可以使用成百上千台服务器来支撑这么多用户, 但如果每台服务器能支持一百万连接(C1000K), 那么只需要十台服务器.

有很多技术声称能解决 C1000K 问题, 例如 Erlang, Java NIO 等等, 不过, 我们应该首先弄明白, 什么因素限制了 C1000K 问题的解决. 主要是这几点:

  1. 操作系统能否支持百万连接?
  2. 操作系统维持百万连接需要多少内存?
  3. 应用程序维持百万连接需要多少内存?
  4. 百万连接的吞吐量是否超过了网络限制?

下面来分别对这几个问题进行分析.

1. 操作系统能否支持百万连接?

对于绝大部分 Linux 操作系统, 默认情况下确实不支持 C1000K! 因为操作系统包含最大打开文件数(Max Open Files)限制, 分为系统全局的, 和进程级的限制.

全局限制

在 Linux 下执行:

cat /proc/sys/fs/file-nr 

会打印出类似下面的一行输出:

5100	0	101747 

第三个数字 101747 就是当前系统的全局最大打开文件数(Max Open Files), 可以看到, 只有 10 万, 所以, 在这台服务器上无法支持 C1000K. 很多系统的这个数值更小, 为了修改这个数值, 用 root 权限修改 /etc/sysctl.conf 文件:

fs.file-max = 1020000 net.ipv4.ip_conntrack_max = 1020000 net.ipv4.netfilter.ip_conntrack_max = 1020000 

需要重启系统服务生效:

# Linux $ sudo sysctl -p /etc/sysctl.conf  # BSD $ sudo /etc/rc.d/sysctl reload 

进程限制

执行:

ulimit -n 

输出:

1024 

说明当前 Linux 系统的每一个进程只能最多打开 1024 个文件. 为了支持 C1000K, 你同样需要修改这个限制.

临时修改

ulimit -n 1020000 

不过, 如果你不是 root, 可能不能修改超过 1024, 会报错:

-bash: ulimit: open files: cannot modify limit: Operation not permitted 

永久修改

编辑 /etc/security/limits.conf 文件, 加入如下行:

# /etc/security/limits.conf work         hard    nofile      1020000 work         soft    nofile      1020000 

第一列的 work 表示 work 用户, 你可以填 *, 或者 root. 然后保存退出, 重新登录服务器.

注意: Linux 内核源码中有一个常量(NR_OPEN in /usr/include/linux/fs.h), 限制了最大打开文件数, 如 RHEL 5 是 1048576(2^20), 所以, 要想支持 C1000K, 你可能还需要重新编译内核.

2. 操作系统维持百万连接需要多少内存?

解决了操作系统的参数限制, 接下来就要看看内存的占用情况. 首先, 是操作系统本身维护这些连接的内存占用. 对于 Linux 操作系统, socket(fd) 是一个整数, 所以, 猜想操作系统管理一百万个连接所占用的内存应该是 4M/8M, 再包括一些管理信息, 应该会是 100M 左右. 不过, 还有 socket 发送和接收缓冲区所占用的内存没有分析. 为此, 我写了最原始的 C 网络程序来验证:

服务器

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <arpa/inet.h> #include <netinet/tcp.h> #include <sys/select.h>  #define MAX_PORTS 10  int main(int argc, char **argv){     struct sockaddr_in addr;     const char *ip = "0.0.0.0";     int opt = 1;     int bufsize;     socklen_t optlen;     int connections = 0;     int base_port = 7000;     if(argc > 2){         base_port = atoi(argv[1]);     }      int server_socks[MAX_PORTS];      for(int i=0; i<MAX_PORTS; i++){         int port = base_port + i;         bzero(&addr, sizeof(addr));         addr.sin_family = AF_INET;         addr.sin_port = htons((short)port);         inet_pton(AF_INET, ip, &addr.sin_addr);          int serv_sock;         if((serv_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1){             goto sock_err;         }         if(setsockopt(serv_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) == -1){             goto sock_err;         }         if(bind(serv_sock, (struct sockaddr *)&addr, sizeof(addr)) == -1){             goto sock_err;         }         if(listen(serv_sock, 1024) == -1){             goto sock_err;         }          server_socks[i] = serv_sock;         printf("server listen on port: %d\n", port);     }      //optlen = sizeof(bufsize);     //getsockopt(serv_sock, SOL_SOCKET, SO_RCVBUF, &bufsize, &optlen);     //printf("default send/recv buf size: %d\n", bufsize);      while(1){         fd_set readset;         FD_ZERO(&readset);         int maxfd = 0;         for(int i=0; i<MAX_PORTS; i++){             FD_SET(server_socks[i], &readset);             if(server_socks[i] > maxfd){                 maxfd = server_socks[i];             }         }         int ret = select(maxfd + 1, &readset, NULL, NULL, NULL);         if(ret < 0){             if(errno == EINTR){                 continue;             }else{                 printf("select error! %s\n", strerror(errno));                 exit(0);             }         }          if(ret > 0){             for(int i=0; i<MAX_PORTS; i++){                 if(!FD_ISSET(server_socks[i], &readset)){                     continue;                 }                 socklen_t addrlen = sizeof(addr);                 int sock = accept(server_socks[i], (struct sockaddr *)&addr, &addrlen);                 if(sock == -1){                     goto sock_err;                 }                 connections ++;                 printf("connections: %d, fd: %d\n", connections, sock);             }         }     }      return 0; sock_err:     printf("error: %s\n", strerror(errno));     return 0; } 

注意, 服务器监听了 10 个端口, 这是为了测试方便. 因为只有一台客户端测试机, 最多只能跟同一个 IP 端口创建 30000 多个连接, 所以服务器监听了 10 个端口, 这样一台测试机就可以和服务器之间创建 30 万个连接了.

客户端

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <arpa/inet.h> #include <netinet/tcp.h>  int main(int argc, char **argv){     if(argc <=  2){         printf("Usage: %s ip port\n", argv[0]);         exit(0);     }      struct sockaddr_in addr;     const char *ip = argv[1];     int base_port = atoi(argv[2]);     int opt = 1;     int bufsize;     socklen_t optlen;     int connections = 0;      bzero(&addr, sizeof(addr));     addr.sin_family = AF_INET;     inet_pton(AF_INET, ip, &addr.sin_addr);      char tmp_data[10];     int index = 0;     while(1){         if(++index >= 10){             index = 0;         }         int port = base_port + index;         printf("connect to %s:%d\n", ip, port);          addr.sin_port = htons((short)port);          int sock;         if((sock = socket(AF_INET, SOCK_STREAM, 0)) == -1){             goto sock_err;         }         if(connect(sock, (struct sockaddr *)&addr, sizeof(addr)) == -1){             goto sock_err;         }          connections ++;         printf("connections: %d, fd: %d\n", connections, sock);          if(connections % 10000 == 9999){             printf("press Enter to continue: ");             getchar();         }         usleep(1 * 1000);         /*            bufsize = 5000;            setsockopt(serv_sock, SOL_SOCKET, SO_SNDBUF, &bufsize, sizeof(bufsize));            setsockopt(serv_sock, SOL_SOCKET, SO_RCVBUF, &bufsize, sizeof(bufsize));          */     }      return 0; sock_err:     printf("error: %s\n", strerror(errno));     return 0; } 

我测试 10 万个连接, 这些连接是空闲的, 什么数据也不发送也不接收. 这时, 进程只占用了不到 1MB 的内存. 但是, 通过程序退出前后的 free 命令对比, 发现操作系统用了 200M(大致)内存来维护这 10 万个连接! 如果是百万连接的话, 操作系统本身就要占用 2GB 的内存! 也即 2KB 每连接.

可以修改

/proc/sys/net/ipv4/tcp_wmem /proc/sys/net/ipv4/tcp_rmem 

来控制 TCP 连接的发送和接收缓冲的大小(多谢 @egmkang).

3. 应用程序维持百万连接需要多少内存?

通过上面的测试代码, 可以发现, 应用程序维持百万个空闲的连接, 只会占用操作系统的内存, 通过 ps 命令查看可知, 应用程序本身几乎不占用内存.

4. 百万连接的吞吐量是否超过了网络限制?

假设百万连接中有 20% 是活跃的, 每个连接每秒传输 1KB 的数据, 那么需要的网络带宽是 0.2M x 1KB/s x 8 = 1.6Gbps, 要求服务器至少是万兆网卡(10Gbps).

总结

Linux 系统需要修改内核参数和系统配置, 才能支持 C1000K. C1000K 的应用要求服务器至少需要 2GB 内存, 如果应用本身还需要内存, 这个要求应该是至少 10GB 内存. 同时, 网卡应该至少是万兆网卡.

当然, 这仅仅是理论分析, 实际的应用需要更多的内存和 CPU 资源来处理业务数据.

参考:

http://www.cyberciti.biz/faq/linux-increase-the-maximum-number-of-open-files/
http://www.lognormal.com/blog/2012/09/27/linux-tcpip-tuning/

下一篇: 构建C1000K的服务器(2) – 实现

Related posts:

  1. 要记得清除 sockaddr_in
  2. 构建C1000K的服务器(2) – 实现百万连接的comet服务器
  3. 数据传输中的停止等待机制的实现
  4. Libevent 2 HTTP 客户端示例
  5. 有趣的 main 函数参数
Posted by ideawu at 2013-09-16 22:01:16 Tags: 
posted @ 2016-05-26 11:16 小马歌 阅读(253) | 评论 (0)编辑 收藏
 
from:http://www.ideawu.net/blog/archives/533.html

"因为TCP端口号是16位无符号整数, 最大65535, 所以一台服务器最多支持65536个TCP socket连接." - 一个非常经典的误解! 即使是有多年网络编程经验的人, 也会持有这个错误结论.

要戳破这个错误结论, 可以从理论和实践两方面来.

理论

系统通过一个四元组来唯一标识一条TCP连接. 这个四元组的结构是{local_ip, local_port, remote_ip, remote_port}, 对于IPv4, 系统理论上最多可以管理2^(32+16+32+16), 2的96次方个连接.

因为对于同一台服务器来说, 一般只有一个 local_ip, 那么, 同一台服务器可以管理 2^(16+32+16) 个连接. 而一个服务(进程, 如 Nginx 进程)一般只监听一个 local_port, 那么, 同一台服务就可以管理 2^(32+16) 个连接. 而如果从一台远端机器(所谓的 client)来连接这台服务器上的一个服务, 那么 local_ip, local_port, remote_ip 这3个变量是固定的, 那么, 就只能建立 2^16=65536 个连接了. 这就是经典的误解的来源!

如果不仅仅考虑TCP, 则是一个五元组, 加上协议号(TCP, UDP或者其它).

实践

服务器绑定一个ip:port, 然后accept连接, 所有accept的连接使用的本地地址也是同样的ip:port.

扩展内容

如果某个客户端向同一个TCP端点(ip:port)发起主动连接, 那么每一条连接都必须使用不同的本地TCP端点, 如果客户端只有一个IP则是使用不同的本地端口, 该端口的范围在*nix系统上的一个例子是32768到61000, 可以通过如下命令查看:

[root@benegg.com ~]# cat /proc/sys/net/ipv4/ip_local_port_range  32768   61000 

也就是说, 一个客户端连接同一个服务器的同一个ip:port(比如进行压力测试), 最多可以发起30000个左右的连接.

TCP客户端(TCP的主动发起者)可以在同一ip:port上向不同的服务器发起主动连接, 只需在bind之前对socket设置SO_REUSEADDR选项.

系统支持的最大打开文件描述符数(包括socket连接):

[root@benegg.com ~]# cat /proc/sys/fs/file-max 580382 

单个进程所能打开的最大文件描述符数:

[root@benegg.com ~]# ulimit -n 1024 

结论

无论是对于服务器还是客户端, 认为"一台机器最多建立65536个TCP连接"是没有根据的, 理论上远远超过这个值.

另外, 对于client端, 操作系统会自动根据不同的远端 ip:port, 决定是否重用本地端口.

Related posts:

  1. 连连看游戏开发实践(1) – 算法
  2. 对P2P应用不友好的NAT
  3. Tomcat网站server.xml设置
  4. P2P穿透NAT的思路
  5. SSDB 的双主和多主配置
Posted by ideawu at 2010-07-16 16:44:50
posted @ 2016-05-26 11:15 小马歌 阅读(285) | 评论 (0)编辑 收藏
 

在几个月前改造dubbo时,netty4已经稳定很久了,一时手痒,按照netty3-rpc的源码克隆了一套netty4,在修正了大量的包、类型不同之后,基本保持了netty3的风格,并发量小或者数据包很小时,一切都很ok, 在进行大并发测试时,结果和netty3完全不同,基本用惨不忍睹来形容。由于当时急于开发php客户端,就把netty4-rpc当做一个失败的组件存档起来, 前几天php-dubbo开发基本完成之后,返回过来思考netty4-rpc的问题,经过仔细分析数据包的解析过程,单步跟踪源码

NettyCodecAdapter, TelnetCodec, ExchangeCoedec,发现ByteBuf的缓冲区为1024,当数据超过1024时,会调用多次Decoder.messageReceived函数,第一次分析dubbo的协议头时,是正确的,第二次之后数据就错误了,然后dubbo内部缓冲区的数据越来越长,但是仍然分析不到一个完整的dubbo数据包

因此去看netty4的源码,发现AbstractNioByteChannel中有网络数据接收的代码时这么处理ByteBuf的

  ByteBuf byteBuf = null;
            int messages = 0;
            boolean close = false;
            try {
                int totalReadAmount = 0;
                boolean readPendingReset = false;
                do {
                    byteBuf = allocHandle.allocate(allocator);
                    int writable = byteBuf.writableBytes();
                    int localReadAmount = doReadBytes(byteBuf);
                    if (localReadAmount <= 0) {
                        // not was read release the buffer
                        byteBuf.release();
                        close = localReadAmount < 0;
                        break;
                    }
                    if (!readPendingReset) {
                        readPendingReset = true;
                        setReadPending(false);
                    }
                    pipeline.fireChannelRead(byteBuf);
                    byteBuf = null;

看见没,内核是需要ByteBuf.release的,继续通过byteBuf的一个实现PooledByteBuf分析源码,原来是实现了一个基于简单计数应用计数的循环使用的缓冲区,一旦计数变为1,该缓冲区被归还到netty4内核,被后面的数据读取线程重新使用

而我们InternalDecoder的代码为

      message = com.alibaba.dubbo.remoting.buffer.ChannelBuffers.wrappedBuffer(
                    input.toByteBuffer());

直接引用了ByteBuf.toByteBuffer,继续查看源码UnpooledHeapByteBuf, 其toByteBuffer实际是对内部数据的

一个nio封装而已,因此,使用上述函数时,导致dubbo的decode保存了一个某一个ByteBuffer的内部数据,但是虽有该

buffer被归还到netty4缓冲区中被循环引用,下一次可能被其他读写线程重新改写数据,因此,高并发下当缓冲区被重复使用时,bytebuf将由于计数问题不断被使用,而解码器中缺傻傻等待。

解决方案

1.通过byteBuf的retain和release函数保证计数的有效性,通过程序例外或者缓冲区被使用完成时候归还ByteBuf到netty4内核

2.拷贝数据到dubbo的缓冲区中

思考:

netty3 是否也有该问题呢???

posted @ 2016-05-17 15:25 小马歌 阅读(1492) | 评论 (1)编辑 收藏
 
     摘要: from:http://hittyt.iteye.com/blog/1691772Hessian反序列化问题众所周知,Hessian框架提供的序列化方式,在性能上要优于Java自己的序列化方式。他将对象序列化,生成的字节数组的数量要相对于Java自带的序列化方式要更简洁。目前公司的一个项目中,有RPC调用的需要,这里我们使用了公司自己的开源RPC框架Dubbo作为远程调用框架,进行业务方法的调用和...  阅读全文
posted @ 2016-05-17 15:24 小马歌 阅读(1340) | 评论 (0)编辑 收藏
 
from:http://my.oschina.net/aruan/blog/351594

同事刘阳使用dubbo服务器中配置mina作为网络传输层,发现大并发情况下,解码发生如下异常

014-12-01 18:00:44,652 [DubboServerHandler-10.1.19.13:20880-thread-164] WARN  alibaba.dubbo.remoting.exchange.codec.ExchangeCodec (ExchangeCodec.java:596) -  [DUBBO] Fail to encode response: Response [id=8119, version=2.0.0, status=40, event=false, error=Fail to decode request due to: RpcInvocation [methodName=null, parameterTypes=null, arguments=null, attachments={input=242}, headers=null], result=null], send bad_response info instead, cause: null, dubbo version: 2.5.5, current host: 127.0.0.1
java.lang.NullPointerException
    at com.alibaba.dubbo.rpc.protocol.dubbo.DubboCodec.encodeResponseData(DubboCodec.java:301)
    at com.alibaba.dubbo.remoting.exchange.codec.ExchangeCodec.encodeResponse(ExchangeCodec.java:560)
    at com.alibaba.dubbo.remoting.exchange.codec.ExchangeCodec.encode(ExchangeCodec.java:104)
    at com.alibaba.dubbo.rpc.protocol.dubbo.DubboCountCodec.encode(DubboCountCodec.java:39)
    at com.alibaba.dubbo.remoting.transport.mina.MinaCodecAdapter$InternalEncoder.encode(MinaCodecAdapter.java:79)
    at org.apache.mina.filter.codec.ProtocolCodecFilter.filterWrite(ProtocolCodecFilter.java:214)
    at org.apache.mina.common.support.AbstractIoFilterChain.callPreviousFilterWrite(AbstractIoFilterChain.java:361)
    at org.apache.mina.common.support.AbstractIoFilterChain.access$1300(AbstractIoFilterChain.java:53)
    at org.apache.mina.common.support.AbstractIoFilterChain$EntryImpl$1.filterWrite(AbstractIoFilterChain.java:659)
    at org.apache.mina.common.support.AbstractIoFilterChain$TailFilter.filterWrite(AbstractIoFilterChain.java:587)
    at org.apache.mina.common.support.AbstractIoFilterChain.callPreviousFilterWrite(AbstractIoFilterChain.java:361)
    at org.apache.mina.common.support.AbstractIoFilterChain.fireFilterWrite(AbstractIoFilterChain.java:355)
    at org.apache.mina.transport.socket.nio.SocketSessionImpl.write0(SocketSessionImpl.java:166)
    at org.apache.mina.common.support.BaseIoSession.write(BaseIoSession.java:177)
    at org.apache.mina.common.support.BaseIoSession.write(BaseIoSession.java:168)
    at com.alibaba.dubbo.remoting.transport.mina.MinaChannel.send(MinaChannel.java:95)
    at com.alibaba.dubbo.remoting.transport.AbstractPeer.send(AbstractPeer.java:51)
    at 

经过对比netty3和netty4作为传输层,却都没有发现类似的问题。

首先排除不是mina本身的问题,mina也没有爆出有这个问题,初步判断dubbo在使用mina时机制有问题

经过对比发现

1.netty是为每一个channel分配了一个NettyCodecAdapter, mina确实在服务器监听前配置了MinaCodecAdapter

2.也就是说,netty的每一个独立的通道的Codec(encoder/decoder)是通道安全的

3.mina的所有通道是共享相同的codec(encoder/decoder)的,因此,解码器中的实例数据时非channel安全的

因此解码器中与netty相同的解码器的缓冲数据算法在并发情况下将会产生数据覆盖问题。

4.解决方案

        1.配置acceptor的监听器

codecAdapter = new MinaCodecAdapter(getCodec(), getUrl(), this);
        acceptor.getFilterChain().addLast("codec", new ProtocolCodecFilter(codecAdapter));

    acceptor.addListener(new IoServiceListener(){
            @Override
            public void serviceActivated(IoService service,
                    SocketAddress serviceAddress, IoHandler handler,
                    IoServiceConfig config) {
            }

            @Override
            public void serviceDeactivated(IoService service,
                    SocketAddress serviceAddress, IoHandler handler,
                    IoServiceConfig config) {
            }

            @Override
            public void sessionCreated(IoSession session) {
                codecAdapter.sessionCreated(session);
            }

            @Override
            public void sessionDestroyed(IoSession session) {
                codecAdapter.sessionDestroyed(session);
            }
        });

    2.监听session的create和destroy事件,传递到decoder中,decoder中,通过session和buffer的键值对保存对不同通道的数据的缓存,

private Map<IoSession, ChannelBuffer> buffers = new ConcurrentHashMap<IoSession, ChannelBuffer>();

        // ChannelBuffers.EMPTY_BUFFER;

        public void sessionCreated(IoSession session) {
            buffers.put(session, ChannelBuffers.EMPTY_BUFFER);
        }

        public void sessionDestroyed(IoSession session) {
            buffers.remove(session);
        }

    3.解码时通过session获得当前channel的数据

ChannelBuffer buffer = buffers.get(session);
            if(buffer == null) return;


经过测试,问题得以解决

posted @ 2016-05-17 15:24 小马歌 阅读(1190) | 评论 (0)编辑 收藏
 

其实这个不算是一个坑,阿里实现的字符串协议挺好的,但是由于我个人的强迫症和在编写php客户端过程中对字符串的输出和解析感觉很别扭,尤其是字符串数据很大时,必须一个字节一个字节的判断处理,让我很郁闷,明显和我当年编写汇编时的哪种精致不符。体现在两个方面

1.字符串的格式定义为 字母S或者R + 两个字节的数据长度(MSB)+ utf8格式的字节数组,S表示最后一个块,上面那个长度是unicode 

字符串的长度,不是自己数组的长度,即"中国"这个词,长度是2,utf8表示的字节数组确实6个字节

2.我的php客户端是用c混合c++编写的php扩展,输出字符串时首先调用libmbfl库计算unicode字符串的长度,然后输出utf8数据,因为在php中,我们默认采用utf8格式,字符串zval已经是utf8格式了,并且附带一个utf8长度的整数,

3.读取应答分析字符串时,根据上面的长度并不知道该分配多少内存来接受整个字符串,因为长度和utf8的字节长度根本没有关系,还有多个节时防止utf8字母被分配到多个不同的chunk上面

4.因此,我们重新定义了一个字符串数据协议, E +4字节UTF8字节长度(MSB)+ utf8表示一个完整的字符串,4字节时,表示的数据有2^31-1个utf8字节,可以表示好几百兆的汉字,够用了,这样我们可以使用c语言的memcpy函数进行快速数据拷贝和数据缓冲区分配了

5.经过测试,大量字符串的数据传输性能能提高5%

posted @ 2016-05-17 15:24 小马歌 阅读(277) | 评论 (0)编辑 收藏
仅列出标题
共95页: First 上一页 3 4 5 6 7 8 9 10 11 下一页 Last