技术手记

饭团性能优化记

缘起

前年冬天在人人时, 为了方便组里一起吃饭的同学们互相算账, 参考以前度厂的饭团设置, 在团队里拉起一个饭团, 然后写了个小系统来记账

设计

一开始的想法是这样的

1. 饭团设置一个团长, 团长管饭团的钱, 出去吃饭时由团长付钱
2. 每顿饭按人均消费额, 扣除参团人的余额
3. 每个人把钱交给团长, 余额不足时由团长催促交钱

所以设计的数据模型是

    人 {
        id,
        姓名
    }
    饭 {
        id,
        付款人,    # 外键, 多对一
        参与人,    # 外键, 多对多
        消费额
    }

吃饭就在饭那个表里加一条记录, 充值也算一顿特殊的饭. 每顿饭后的账面和最终余额按时间遍历所有记录实时算, 这样一是省了记每顿饭后余额的存储开销, 二是避免有历史修改而需要更新余额表一堆数据的麻烦事. 考虑到饭团也就十来个人, 在可预见的未来数据量人最多到百级, 饭撑死也就是千级, 每次遍历的代价应该也不大 (事实上在我写本篇文章的时候, 饭团历史总人数不到 20, 算上充值转账等总共也不到 500 顿饭)

这个余额实时计算的思路和 BitCoin 的余额判断方法也挺像的, 正是因为我写饭团踩了不少坑, 所以我觉得 BitCoin 某些方面还是有很大问题的, 这个回头另外讨论

另外为了统计方便和可追查, 希望记录每顿饭是哪天在哪吃的, 新增和修改数据

    饭 {
        ...
        日期时间,
        店        # 外键, 多对多
    }
    店 {
        id,
        饭店名
    }

后来考虑未来可能有人因为转岗或离职离开饭团而饭团里还有余额需要退款, 新增两个特殊的店来记录充值退款操作, 自此数据模型设计完毕. 最终的 sqlite schema 如下

    CREATE TABLE "ft_people" (
        "id" integer NOT NULL PRIMARY KEY,
        "name" varchar(200) NOT NULL
    );

    CREATE TABLE "ft_deal" (
        "id" integer NOT NULL PRIMARY KEY,
        "restaurant_id" integer NOT NULL REFERENCES "ft_restaurant" ("id"),
        "pay_people_id" integer NOT NULL REFERENCES "ft_people" ("id"),
        "deal_date" datetime NOT NULL,
        "charge" real NOT NULL
    );
    CREATE INDEX "ft_deal_75ae3b0c" ON "ft_deal" ("pay_people_id");
    CREATE INDEX "ft_deal_be4c8f84" ON "ft_deal" ("restaurant_id");

    CREATE TABLE "ft_deal_peoples" (
        "id" integer NOT NULL PRIMARY KEY,
        "deal_id" integer NOT NULL,
        "people_id" integer NOT NULL REFERENCES "ft_people" ("id"),
        UNIQUE ("deal_id", "people_id")
    );
    CREATE INDEX "ft_deal_peoples_1a9336ea" ON "ft_deal_peoples" ("deal_id");
    CREATE INDEX "ft_deal_peoples_3cff102f" ON "ft_deal_peoples" ("people_id");

    CREATE TABLE "ft_restaurant" (
        "id" integer NOT NULL PRIMARY KEY,
        "name" varchar(200) NOT NULL
    );

因为懒得自己去管理数据的写和更新操作, 刚好那段时间看了下 django, 感觉自带 ORM 和 admin 组件的 django 会是开发的好选择, 于是对着 tutorial 学过去后就开工了. 很快写完, 框架用的 django1.5, 数据库用 sqlite, 页面是裸写的 html, 没有任何 javascript, 仅有的一点 css 也硬编码在 html 文件里了

功能和美化

用了一段时间后发现离一开始的设定有一些变化, 比如团长不一定每顿饭都出席, 那需要有另外的人付账, 然后团长又要给付账的人团费, 还不如直接让付账人的钱直接进饭团余额. 这个功能用最初的功能也可以做到, 只是让团费的作用没那么清晰了. 用到后来, 发现其实是不需要有饭团团长这个设定的, 每顿饭谁付钱就算谁的, 反正饭团记录的是每个人的帐户余额, 团费其实就是团长的帐户余额. 需要交团费或互相转账时直接添加一顿转出人付款, 参与人只有收款人的特殊虚拟饭就可以了, 于是又加了个叫转账的虚拟店来记录转账操作, 自此充值和退款两个虚拟店就变得毫无用处了

一开始所有饭团记录都只有一页, 后来应大家的统计需求, 按参与人/付款人/店分别做了个过滤器, 这个实现的很简单, 就是对不符合过滤器的记录, 只计算不输出就行了

当饭团运作了半年多后, 单页的饭团太长, 又将默认页面改成只看最近一个月的, 另外提供了个翻页的按钮和查看全部的选项. 另一个问题是饭团成立时的团员有人转去了其他团队不再一起吃饭, 这些人最近的记录都是空的, 放着一是不好看, 二是人多了页面宽度超过很多人显示器的大小, 于是给人加了一个 “是否活跃” 的属性, 默认不显示那些不活跃的人

前不久回头去看饭团的前端, 觉得虽然算不上丑死人, 但是也没好看到哪去, 刚好就用 bootstrap 套了下, 并把各种过滤器提供表单输入的功能弄成一个查询表单. 本打算把表单直接塞导航栏, 结果发现 bootstrap 原生的 select 什么的真心太丑, 放导航栏严重破坏美感, 后来找了个 bootstrap-select 的插件来支持, 这个就很赞了

用上 bootstrap 时一嫌自己管理 css/js 麻烦, 二怕又扯上被人盗用跑流量的狗血, 直接用了国内大公司的 cdn 内容. 后来用 bootstrap-select 时, 发现国外的 cdn 太慢, 国内又没找到靠谱的, 就只能在自己项目里拷贝了一份, 结果测试环境都 OK, 在线上的 fastcgi 环境里总显示有问题, 提示找不到文件, 怒了在 nginx 里对自己的 static 文件夹又加了一条 alias 才行. 后来想这么弱智的事情不会是 django 的问题, 就去找官方文档, 在 https://docs.djangoproject.com/en/dev/howto/static-files/ 里来回看了几次才发现最后有一段关于怎么 Deployment 的, 原来还要收集一次, 也还是要加 static alias 的嘛, 只是解决了为什么之前要给 static/admin 单加一条的问题. 从这个角度来说, django 还是略复杂蛋疼, flask 就简单的多, 完全交给你自己去弄, 而且 templates 和 static 都汇集放好管理, 或许 django 是为了给每个 app 单独的分发权?

性能优化

饭团弄好后先是架在了公司我跑 Ubuntu Server 的台式机上, 直接就用 runserver 的模式跑的. 后来因为台式机偶尔会掉电, 饭团没设开机自动启动, 偶尔也会忘了开, 加上内网 IP 不一定固定, 用起来还是有点小烦, 于是迁移到我的 VPS 上

我贪便宜 15$/yr 买的 buyvm VPS, 内存只有 128M, 之前曾经写过一篇各种压榨内存的优化记录, 饭团丢上去就发现这货居然还是内存大户, 搜了下改成用 flup 以 fastcgi 的模式跑, 并把实例压到只有一个, 反正访问也不频繁, 不用处理啥并发. 用了小半年后觉得偶尔有点卡, 不过一直认为是 buyvm 的机器烂加上服务器在美国多半是网络延迟, 就没再管他

等到去年冬天的时候, 发现这慢的已经完全不成样子了, 而且有报页面超过返回大小, 将默认页面改成只看最近一个月这也是个主要原因, 当时还以为是网络的问题导致卡 (我那个 vps 走联通线路只有不到 50KB/s 的速度)

今年过完年, 在想在新团队是不是也能搭个这货玩, 把之前的数据拷贝到本地去测试了下各项功能, 发现打开首页需要接近 10 秒, 这都是本地了, 不能再赖网络, 于是加各种 Debug 信息去看到底慢到哪里. 实时的余额计算流程大概是这样

    遍历所有饭:
        获取饭的信息, 包括关联的餐厅和付款人等
        遍历所有人
            判断是否参加了本顿饭, 如果没有
                直接沿用上条记录
            如果有
                判断是否是付款人, 如果是
                    增加本顿饭总额扣掉自己那份的进余额
                如果不是
                    从余额里扣掉本顿饭钱
        添加输出信息

这里面的参团判断是用 O(n^2) 遍历实现的, 一开始就十来个人, 就算是平方复杂度也能慢到哪里去, 结果一堆 debug 信息放下去那个地方还真是特别慢. 仔细想了下估计是那个遍历所有人做判断的地方, 每次都新做了一次 SQL 查询, 好吧, 把判断用的表先遍历一次提出来做个 dict, 果然快了一些. 经过这步后耗时从 10s 降到 1s, 感觉再快也快不过跨太平洋的网络耗时, 就没再继续压榨性能

上一个改动做完没几天笨狗折腾了个 digitalocean 的 VPS 玩, 这个延迟又低速度又快, 于是有想着对那个 1s 的性能做优化, 按说这么点数据要 0.1 秒都不正常. 继续琢磨, 猜是每次取一顿饭, 都做了若干次 SQL 查询去取外键数据, 于是把人和餐厅的数据都预先提取出来构建 dict, 然后查询的时候使用就好, 这样又能快一点. 再回头去看那个多出来的辅助表, 猜是那个表每遍历一顿饭又去做了一次查询, 干脆自己把 view 里的查询都裸写, 每次页面请求都把四个表数据都 select * 出来, 然后自己去拼, 反正数据也不复杂, 这样一次页面请求只用四次 SQL 查询, 果然速度就降到了 0.01s 内. 因为数据量不大, 对内存压力也几乎没有, 而且 digitalocean 的内存有 512M, 也不用那么抠内存

问题感想

我中间曾经想要不要换 flask 重写一次, 自己管数据库, 后来找到性能瓶颈后还是留在了 django 那, 能用就懒得去动, 而且自己写个 admin 还是略麻烦

跟熊吐槽 django 的 ORM 怎么这么烂, 深度插件控的熊表示你这个一定有合适插件来帮你干这事而不是靠自己裸写 SQL 的, 不过笨狗表示有找插件和配置的时间, 我裸写的东西早搞完了. 果然笨狗还是又笨又懒, 还好目前看也还没太多篓子

对了, 饭团的 github 开源地址在: https://github.com/whusnoopy/fantuan, 欢迎 fork 帮忙优化

最后挂个 DigitalOcean 的邀请链接: https://www.digitalocean.com/?refcode=8a3c1464993e 如果你通过这个注册并付款, 我会有返点支持我继续用 DO

Win7 64bit 上 VirtualBox 安装过程中卡死

前两天桌面上 VirtualBox 的图标异常, 加上被提示有新版, 就想要不下个新的装上, 结果安装过程中到更新 VirtualBox 那个虚拟网卡时就卡死, 连带把机器本身的无线也弄的不可使用, 重连也不行. 不行就取消 VirtualBox 安装呗, 但是点取消后过很久才有确认窗口, 而且不管点什么最终还是关不掉, 任务管理器杀进程都没用, 只能重启. 一开始以为下的 4.3.0 这个版本有问题, 去官网重下 4.2.8, 问题依旧

公司笔记本装的 Win7 64bit, 怀疑是不是这个问题, 下载的时候没发现有区分 x86/x64, 搜了下 “VirtualBox 安装过程中卡死”, 果然有人一样, 说解决办法有解压安装文件直接选 x64 的安装, 这个用 7zip 解开 .exe 安装文件后也没发现有区别, 倒是那个 GuestAddition.iso 里有, 没直接去试, 另外的解决办法是 “安装过程前先禁用所有的网络连接, 然后安装过程中添加 VirtualBox Host Only 的网卡那就可以顺利通过“, 一试果然

svn co 时控制目录层次

发现很多地方的 svn 还是用 http://svn.corp.com/team/project/ 下同时挂 branch, release, tags, trunk 的目录结构, 如果想把整个 team 的所有 project 都 checkout 下来, 显然数据量会大的想哭, 事实上大多数情况只要 co trunk 就行了, 土法就是挨个挨个 co 对应的 trunk 来看, 不过这样似乎有点土鳖

好在 svn 在 checkout 和 update 的时候是可以带一个 –depth 或 –set-depth 的参数来控制下载的目录层级. 比较简单的记录如下

# 仅目录
--set-depth emtpy
# 一层
--set-depth immediates
# 无限
--set-depth infinity

具体应用例子如下

$ svn co --depth immediates http://svn.corp.com/team
$ for proj_name in $(ls) do; svn up ${proj_name}/trunk; done

这样就能把每个 project 下的 trunk 弄到本地了

读书杂记

全球通史
http://book.douban.com/subject/10583099/

这书从买 kindle 开始看, 到最近两天才看完. 一句话感触: 历史大潮滚滚过, 你我其中或可知

感觉一直到现在, 历史的关键转折无外乎科技发展, 宗教冲突, 以及利益驱使. 科技发展没什么好说的, 攀科技树多一层, 对低级别的来说基本就是碾压. 宗教的问题在天朝似乎没那么夸张, 但是看整个欧洲和中东, 基本上都是因为宗教的原因, 基督教和伊斯兰教互相 PK, 以及内部各分支在互相 PK. 利益驱使是一个很好的去做改进的动机, 除了宗教这种太意识形态的事, 科技发展和扩张都是建立在利益驱使下, 天朝最近几百年科技发展不行, 就是没啥利益了, 天朝上国啥也不缺, 往外打也没啥好打的, 就慢慢耗死了

另一个感触就是越到现代, 历史发展速度越快, 最近一百年的发展可能超过了之前所有文明阶段总和, 而最近一二十年又还在加速前进. 回忆下我们的小时候和现在, 差异实在是太大了, 如果把一个古人放到现在, 他会不会因为完全无法适应这么快的变化而崩溃. 我们既然已经在这股汹涌的历史大潮中, 已经无法选择崩溃, 崩溃就挂了, 那剩下要考虑的就是怎么保证可以随波逐流, 有理想点的可以考虑怎么去成为弄潮的人. 计算机相关领域一直又是更大更猛的潮, 但是笨狗还是想闲着发呆怎么办… 希望能被推着走还在时代的尾巴上吧

量子物理史话
http://book.douban.com/subject/1467022/

这本书很早就听过, 但是一直没去看, 应该是今年年初跟阿牛提起来, 于是找了个周末花了大半天一口气看完, 里面不少章节应该在 BBS 上零零碎碎看过, 所以也没有触动到非常夸张的地步, 只是觉得: 物理真的好奇妙, 而且, 对于这个世界, 我们究竟知道多少?

在我的 Task List 上好像是很早就说要写个读书笔记, 不过拖了这么久, 好像也想不起来到底当时想说些啥. 只还是深深觉得对这个世界我们知道的还是太少了, 而且现在的所知未必是正确的, 不断有新的理论和证据来说明世界原来不是我们一开始认为的那样的. 科技大发展有时候也让人挺困惑的, 简单点大条点也好啊, 可惜人类就是这么的充满好奇心, 不知道到人类文明消亡之时, 是否能把奥秘探究完. 我一直认为时间和空间是无限的, 我们当前这个宇宙的时空间有限那是因为我们的宇宙只是更大尺度上的一小部分, 或者等我们弄明白了当前这个宇宙后, 就可以将文明升级一个大阶段, 去考虑上一层的问题了

deep learning 的 feature 问题

这个不是读书了, 只是对现在火的要死的 deep learning 做一点自己的理解笔记和记录点疑问

因为我没弄过神经网络, 所以对 DL 的很多基础都不了解, 只能以很傻的方式来理解. 最近听了 MSR 邓力和 Baidu 余凯两次讲座, 加上之前在人人小强给普及的, 大致说来我理解的 deep learning 就是这么回事: 把以前只有零次 (比如 LR 的直接特征到结果映射) 或一次 (比如 SVM 用核函数来做原特征和结果的映射) 的问题空间转换, 变成多层 (即更 deep), 从而在这个过程中自然筛选组合学习到对问题的更本质的特征描述

我理解 deep learning 最大的变化是把一层隐信息变成了多层, 那每一层是怎么映射的? 是已有特征的大杂烩? 还是有一些简单的人工 feature engineering 的工作在里面? 对这个问题一直没人给仔细讲讲, 像 SVM 的核函数, 也还是需要人工去选择, 按 http://deeplearning.net/tutorial/ 这个 tutor 上的简单例子, 就类似要找到某函数最终的表达式, 可以在每一层我们都提供基本运算, 然后看若干次组合后能匹配上那个多项式? 表示对学术界最大的抵触就像是 “怎样画马” 那个讽刺漫画, 最后那一步跳的忒大了…

抛开那个映射方法的问题, 我的另一个问题是: feature 是否会变得不可理解? 因为 DL 的过程中可能通过人无法理解的大量组合得到最终的特征, 那是否会导致人类无法理解或解释最终的特征? 那在某些应用场景下是否会有遗憾? 比如人脸识别现在能做的很好, 但是对于那些识别不出来的照片我们怎么去跟人解释怎样变得可识别, 告诉别人脸洗干净点? 或者正面一点会容易识别? 这些都可以让人类来理解, 也可以让人类配合优化, 但 DL 出来的 feature 如果没法理解会不会在用户愿意配合的情况下都无所适从? 特别是广告, 之前在度厂我们做个性化, 让广告主接受的最大障碍就是广告主表示 “换了这样的游戏规则后我们完全不知道怎么玩, 你好歹给点 guideline 让大家知道什么是好的什么是坏的, 然后对于极端 case 能跟我解释为什么, 以后怎么避免”. 现在度厂说已经在凤巢上了 DL 的 model, 我在 ADC 上问余凯可解释的问题, 他表示广告主的难处没反馈到他那, 所以他也不知道或没觉得是问题…

django 入门小试

对 django 这个框架早有耳闻, 最近因为想给组内写个饭团系统, 想是不是可以刚好学习用下这样的框架

先按官方教程学了下基本的内容, 很简单: https://docs.djangoproject.com/en/1.5/intro/

然后就自己操练了下, 记几个小细节或坑

1. 简单应用时, 数据库辅助表不要自己建, 否则维护关系很麻烦
2. 用 shell 来做本地测试, 用 dbshell 来修复数据库问题
3. 对不同的模块, 功能, 善用辅助字段, 各种 Google 查询就是了
4. 调试完成关闭 DEBUG 模式发布后如果还有错, 那就在 setting.py 里配好管理员邮件等着收邮件看报告分析错误

过程中还经常参考的是 The Django Book, 不过没找到哪里有比较好的全本或翻译版下, 我都是从新浪爱问等地方找到的半成品

目前已经完成饭团, 架在内网的一台台式机上, 源码和说明在
https://github.com/whusnoopy/fantuan

这个系统在我们组内已经用了好几个月, 目前看来工作良好 :P 我是用的 fastcgi 模式在跑, 使用 unix socket 通信, 配好 nginx 的转发规则就可以了, 注意对应 socket 文件的读写权限, 我一开始被这个坑了半天, 其他的看上面那个 github 工程的 README 应该都说清楚了

补一句, 关于饭团, 之前支付宝有 AA 收账其实就能满足需求, 只是没法回溯查看, 对我们团队的问题则是不知道大家的支付宝帐号, 统计收集也麻烦, 加上我们还想记录下来做点 data mining 玩, 比如偏好餐厅等, 就还是有单独饭团的需求. 另外, 最新版的 QQ 客户端里, 群也有 AA 收账功能, 看了下, 跟支付宝类似, 发起会更方便, 但考虑到财付通的覆盖面远不如支付宝, 估计也够呛, 另外一些我们想要的记录似乎也是没有的 (比如餐厅, 付款人等)

为什么要预估点击率

背景

想到这个题目是因为 @lijiefei 某天跟我说他有师弟面淘宝时被问到 “点击率预估的目标到底是什么”, 笨狗当时胡乱扯了一通, 发现要把这个似乎已经是真理的事情掰清楚还没那么容易, 于是有此念想写文一篇详细分析下原因

我和 jiefei 认识是在百度做搜索广告的时候, 那就从搜索广告开始说为什么要预估点击率, 以及预估点击率的目标. 先申明一些名词和假定:
1) 每个广告 (Ad) 有一个出价 (Bid), 并有其在某情形下实际的点击率 (Click-Through-Rate, CTR)
2) 广告按点击收费 (Charge per Click, CPC), 下面我们会分别讨论一价计费 (First-Price, FP, 即广告出价多少则一次点击计费多少) 和二价计费 (Second-Price, SP, 即广告按下一位出价来支付点击价格, 更普遍的是 GSP)
3) 千次展现收费 (Cost Per Mille, CPM, 或 RPM, R for Revenue), 即对点击付费广告其展示一千次情况下的收入 (一价计费下等价于 1000*CTR*Bid), 或是展示广告的千次展现固定价格
4) 预估点击率 (predict CTR, pCTR) 是指对某个广告将要在某个情形下展现前, 系统预估其可能的点击概率

目标分类

搜索广告跟自然结果一个很大的区别就是自然结果只要有一点相关就应该放到所有结果里去, 至于先后位置那个再说, 而广告, 是有个相关性的准入门槛的, 不相关的广告出价再高, 丢出来还是会被骂死. 那怎么判断相关? 用户会用鼠标点击来对结果投票, 相关的广告会被点击, 不相关的广告不会被点击, 那很自然就能得出 “点击率和相关性正相关” 这个结论 (至于描述里写 “二十五岁以下免进” 但实际是钢材广告的这种诱骗行为后面再说怎么处理). 那对于这种相关性准入的场景, 预估点击率就是预估广告是否相关, 最朴素情况下这是个二分类问题, 那不管预估成怎样, 只要有一种分割方法能分开是否相关就行了. 此时预估点击率的目标是能对广告按相关与否分类 (或说按相关性排序并给出一个截断值). 评估分类问题好坏, 一般都是看准确和召回两个指标, 用人工打分的记录来做回归验证就行

目标排序

判断相关与否只是点击率预估对广告的一个小辅助, 我们来看看广告的目标是什么? 没错, 是赚钱. (我曾经在其他场合说过广告的目标是维持用户体验下持续赚钱, 不过跟赚钱这一简化目标这不冲突, 前面相关性上已经保证了维持用户体验, 那只要能让广告主还有的赚, 就能持续赚钱) 我们再把问题简化下, 如果广告都是一样的固定价格, 且就以这个价格按点击计费, 那在 PV 一定且预算充分的情况下, 更高的点击率则意味着更赚钱. 这样目标可以等价于怎么挑出更赚钱的广告, 就是那些点击率最高的广告, 我们只要能弄明白广告实际点击率的高低关系就能取得收益最大化, 预估点击率在这时候又是个排序问题, 我们只要弄对广告之间的序关系, 就可以收益最大. 评估排序问题的好坏, 一个经典方法是对 pCTR 的 ROC 曲线算 AUC (曲线下面积), 实际上我见过的做法也都是通过评估 AUC 的高低来判断点击率预估模型的好坏

目标带权排序

上一段里对广告这个业务做了很多简化, 比如大家价格都是一样的, 如果我们考虑价格不一样的情况, 那预期收益就会变成 (价格Bid*点击率CTR), 这个值很多地方也叫 CPM 或 RPM. 如果是对 CPM 排序, 那就需要我们预估的点击率在维持序关系正确的前提下, 还要保证相互之间的缩放比是一样的. 比如有广告 A, B, C, 实际点击率是 5%, 3%, 1%, 那在价格一致的情况下, 我预估成 5-3-1 还是 5-4-3 是没关系的, 但在价格不一样的情况下, 比如 1, 1.5, 3, 这时候 5-4-3 的预估点击率值会让他们的预估排名和实际排名刚好颠倒过来, 不过预估 5-3-1 或 10-6-2 (放大一倍) 倒没关系. 为了评估这个结果, 可以在描 ROC 曲线时把价格乘上去, 那最后还是判断排序问题的好坏, 加了价格的 AUC 我们可以叫 wAUC (weighted-AUC), 这个离线评估和在线效果依然可以对等

目标准确

从准确召回到 AUC 再到 wAUC, 看起来对已有问题可以完美解决了? 不过广告计费显然不是 FP 这么简单, 在 Google 的带领下几乎所有的搜索引擎都使用了 GSP (Generalized Second Price) 来对广告点击进行计费, 这里再简单解释下最简版 GSP 是怎么回事:
1) 所有广告按 CPM 排序 (即 CTR*Bid)
2) 排最后的广告收底价 (Reserved Price, RP)
3) 其他广告按他的下一位 CPM_i+1 除以他的 CTR_i 并加一个偏移量来计费, 并保证比底价高, 即 Price_i = max(CPM_i+1/CTR_i + delta_price, RP)

至于 GSP 的细节和为什么这么做能保证收入和体验的平衡等可以详见相关论文, 我们只讨论在 GSP 模式下, 点击率预估的作用和关键点. 根据介绍 GSP 时最后那个公式, 如果把 CPM_i+1 拆成 CTR_i+1*Bid_i+1, 看起来只要保证同比缩放还是会没问题? 但是, 凡事怕但是, 在搜索广告里, 不同的展现位置对点击率还有影响, 比如广告 A, B 在第一位点击率是 5%, 3%, 而在第二位是 3%, 2%, 那只是同比缩放就很难保证最终比较是一致的问题了, 所以最好还是保证预估值跟实际值尽可能接近的好, 这样才能在预估时获得更实际用时完全一样的场景. 评估准确度, 我们有 MAE 和 MSE 等一堆指标, 也是现成的工作的比较好的东西

扩展和吐槽

有行家可能会吐槽说我刚那个不同广告在不同位置的衰减不一致这个说法, 跟公开论文说的不一样, Yahoo 的 paper 里说不同广告在同位置的衰减是一样的. 我只能说, 骚年, 你太天真了… 衰减因子怎么可能只是 f(pos) 这样一个简单函数, 从实际情况来看, 衰减函数和广告是有关的, 但我们又不能对每个广告都去估一个 f(pos, ad), 好在, 我们发现可以把不同的广告做聚类后得到一个 f(pos, type) 的函数簇, 事实上, 最后的衰减函数不仅仅有 pos 和 type 两个因子, 而且里面的因子可以极度简化, 最后的衰减用简单函数就能很好拟合, 我说的够多了, 再说估计要被前东家找麻烦, 你们来感受一下就好

前面也提到介绍的 GSP 是最简版的, 那如果是正常版会有什么不一样? 那就是排序时用的是 Quality*Bid, 这个 Quality 百度叫质量度, 也就是广大广告主望眼欲穿的那几颗星. Quality 和 CTR 有什么不一样? 最简情况下这两个当然是一样的, 但是我们可能还要考虑广告主的信誉度, 目标网页的质量等因素 (比如前面提到过的那种描述欺诈或诱导的广告在这个 Quality 因子里被调整掉), 最后这个 Quality 就会是包含了 CTR 的一个多因子复合值, 那要准确估计这个复合值, 当然也要求其中的每个因子的值尽可能准确. 这里在炒冷饭说准确度, 以及 MAE/MSE 的作用

实际上据我所知各搜索广告平台用的是比正常版的 GSP 还加强或改造过的版本, 里面的因子, 公式, 逻辑更复杂. 在这种情况下还是需要继续强调 CTR 预估的准, 才能做更精确的预估, 从而带来更大的收益

广告之外

呼~ 说完了搜索广告, 我们再简单说下内容广告. 搜索广告几乎都是点击付费, 而内容广告同时存在按点击付费和按展现付费, 那怎么比较一个按点击付费的广告和一个按展现付费的广告哪个预期收益更高? 同样是 CPM, 按展现付费的广告给的是确定值, 而按点击付费的是一个预估值, 通过 Bid*pCTR 得到, 如果 pCTR 不准, 就会导致点击付费广告的预期收益计算不准, 则不一定受益最大. 继续强调预估的准的好处

说完广告我们就能说说其他的, 比如搜索, 比如推荐, 这几个的优化目标如果是带来的量, 因为总体 PV 我们没法人工干预, 且每个点击是等价的, 那最后的优化就变成了点击率, 预估的序关系越接近真实, 可获得的收益更高. 如果不同的点击价值不一样, 那就可以把这个点击换做价格代入广告的模型, 因为没有二价计费那么讨厌的变换, 所以就按一价计费来考虑, 保证序正确且等比缩放就能保证收益最大. 如果再激进一点, 评估收益时还加入更复杂的因子而不仅仅是价格这个独立因素, 那自然就要求点击率预估准确, 从而保证做决策时和实际情况一致, 继而保证最终的优化目标最大化

总结

1) 点击率预估是为产品的最终目标服务的, 最终目标可以是广告的收入, 广告的相关性, 推荐的接受率等, 看具体场景
2) 点击率预估的直接目标根据需求场景不同, 分别是保证预估值和实际值分类正确, 预估序和实际序正确, 预估值和实际值是等比缩放的, 预估值等于实际值
3) 要保证离线评估点击率预估的效果, 分别可用分类的准确率和召回率, 排序的 AUC, 带权排序的 wAUC, 相似度 MAE/MSE 来评估

奇怪流量的终结

曾经有爆流量记这一篇博文提到之前租的空间被下 Win7 的请求爆流量, 在迁到自己的 VPS 后发现还是有对 Win7 的请求, 还附加了一个奇怪的隔一小会就发个 HEAD 请求, UA 中带 QQDownload 的奇怪来源 (BuyVM VPS 安装优化记)

作为日志洁癖者看着 nginx 的 access.log 里都是这种请求那自然是各种不爽, 之前一直怀疑 Win7 的请求来自旋风或迅雷, 但苦于木有证据, 这次特意又去查了下旋风和迅雷的离线服务器列表 (比如百度空间上用于 eMule 避免吸血的这篇: http://hi.baidu.com/asp502010/item/44ac169b289dd2d91a49df6d), 对比了下 IP 范围, 还是不在里面, 有点失望

不过这次有意外发现, 那就是 UA 里带 QQDownload 的那个, 虽然我知道这个 UA 串未必就是旋风发来的, 可能也只是老版本旋风 (这次 UA 是里版本号是 713, 我看了下最新的旋风应该是 783) 安装后改了系统的 UA 串, 但不管怎样总是有可以胡搅蛮缠的由头, 于是通过之前在旋风实习过的 momodi 联系上旋风的工程师, 直接吐槽

叶文/Snoopy阿排 15:43:29
hi, 非常抱歉冒昧打扰

我碰到的问题是这样的:
我自己有一个域名 yewen.us, 曾经指向我前公司内网里的一台机器, 提供前公司可激活的 windows 7 iso 文件下载, 可能被前同事用不同的下载工具 (如旋风, 迅雷等) 下载过, 并被计入链接库, 于是一直有外部请求来下这个文件
但是我一年多前就移除了这个文件, 对应的下载请求都返回 404, 但一直还在被抓 (后来没办法还改过 302, 416 等错误返回码, 都无效)

最近我仔细看了下我 nginx 的日志, 最近有大量带 QQDownload 标识的请求, 同时请求那个 win7 iso 的量也很大, 所以怀疑是不是 QQ 旋风没有正确处理错误返回码, 一直没把这个已失效的地址去除

之前写过一篇 blog 来分析: http://www.yewen.us/blog/2012/02/overflow/

从昨天晚上到今天上午的 nginx 日志分析:
$ grep “QQDownload” vyewenus.access.log | awk ‘{cnt[$1]++};END{for(ip in cnt){print ip, cnt[ip]}}’ | sort
101.226.68.137 196
140.207.54.139 195
183.195.232.138 196
$ grep “cn_windows_7_professional_x86_dvd_x15-65790” vyewenus.access.log | awk ‘{cnt[$1]++};END{for(ip in cnt){print ip, cnt[ip]}}’ | sort
122.141.67.50 329
123.185.52.73 54
14.114.226.18 150
14.114.226.194 14677
14.115.129.55 4040
180.117.68.185 361
59.33.63.137 1476

抽几条完整日志如下:
101.226.68.137 – – [21/Apr/2013:19:44:18 +0800] “HEAD / HTTP/1.1” 200 0 “-” “Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; QQDownload 713; .NET CLR 2.0.50727; InfoPath.2)”
14.115.129.55 – – [21/Apr/2013:19:44:18 +0800] “GET /ftp/Win7_rtm_with_loader/cn_windows_7_professional_x86_dvd_x15-65790.iso HTTP/1.1” 416 615 “http://yewen.us/” “Mozilla/4.0 (compatible; MSIE 9.0; Windows
NT 6.1; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729)”
14.115.129.55 – – [21/Apr/2013:19:44:22 +0800] “GET /ftp/Win7_rtm_with_loader/cn_windows_7_professional_x86_dvd_x15-65790.iso HTTP/1.1” 416 615 “http://yewen.us/” “Mozilla/4.0 (compatible; MSIE 9.0; Windows
NT 6.1; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729)”
14.115.129.55 – – [21/Apr/2013:19:44:25 +0800] “GET /ftp/Win7_rtm_with_loader/cn_windows_7_professional_x86_dvd_x15-65790.iso HTTP/1.1” 416 615 “http://yewen.us/” “Mozilla/4.0 (compatible; MSIE 9.0; Windows
NT 6.1; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729)”
叶文/Snoopy阿排 15:44:00

请帮验证下那几个大量请求的 IP 地址是否是旋风离线服务器发起的

QQ旋风 15:44:19
你能否自己屏蔽了?

叶文/Snoopy阿排 15:46:01
我现在已经返回 416 错误码了, 流量也不是太大问题, 只是很奇怪为什么我都返回错误码一年多了还在被抓, 而且 IP 不一定固定 (我怀疑可能存在 “某下载工具的地址库存了这个链接, 用户可能会直接请求” 的情况)

后面的沟通基本就是礼节性的再提供点证据, 旋风的工程师没直说这几个请求是否是旋风自己的, 只说可能库里确实有脏数据, 给我看看, 至于老版本如果有缓存了这个信息, 那就没办法了. 隔天早上联系我说确实从库里找到这么一条记录, 已经删除, 让我再看看日志, 这次过去看了下, 果然对 win7 的所有请求都没了

这个事应该就算解决了, 之前虽然有怀疑但一直没去付诸行动, 果然有问题直接找到工程师才算比较快的解决方法, 像我这种 “刁民” 应该也会给他们带来计划外工作量, 不过确实是自家 bug 那也没什么好说的, 上次提到那个多说评论显示错误的问题, 最后也是找多说工程师直接解决. 旋风的哥们帮解决这个问题后还很好奇的问了句, 你还关注日志啊, er, 可以说这已经成职业病了么, 互相呵呵了下也就结了

过了两天发现那个奇怪的 UA 串发来的 HEAD 请求还有, 这次再问旋风那边他们就也不知道怎么回事了, 放 Google 搜了下找到 http://www.postsila.com/thread-193359-1-1.html 这么一篇, 跟我遇到一样的问题, 也不明所以, 不过还好这个请求来源比较固定, 从 access.log 里搜了下对应 IP 除了发 HEAD 请求没有任何正常用户行为, 那就开 iptables 屏蔽掉就行了

$ sudo /sbin/iptables -I INPUT -s 101.226.68.137 -j DROP

封了几天再看日志, 还在请求, 本来还想 ws 的通过 dnspod 能不能直接拒绝掉, 研究了下 dns 真干不了这事. 算了算了, 自己都屏蔽掉了那就这样吧, 看下日志还在涨

$ sudo /sbin/iptables -L -n -v --line-numbers
Chain INPUT (policy ACCEPT 529K packets, 835M bytes)
num   pkts bytes target     prot opt in     out     source               destination
1     3311  199K DROP       all  --  *      *       183.195.232.138      0.0.0.0/0
2     3229  194K DROP       all  --  *      *       140.207.54.139       0.0.0.0/0
3     3304  198K DROP       all  --  *      *       101.226.68.137       0.0.0.0/0

除掉这俩后再看日志, 最大的来源就是 dnspod 的定时监控和各搜索引擎和 Feed 订阅器的抓站. 想了下小破站挂就挂, 没那么严苛的可用时间要求, 去 dnspod 把监控间隔调到最长, 搞定

最后, 看了下之前租的空间还有流量, 再打开日志, 发现都是来自 youdao 爬虫的数据, 话说我都换了域名 IP 指向一周了, 怎么你们还在抓以前 IP 啊, DNS 不更新么

BuyVM VPS 安装优化记

BuyVM 的 VPS

早两周忘了是谁提醒了下, 说 buyvm 家 15$/yr 的 VPS 有货, 非常值得去抢, 当时估算了下这个价格确实算良心价, 虽然只有 128M 内存, 但作为一台自己的 vps 折腾点东西完全都够了的, 果断下手一台

这台机器打算拿来放一些自己玩的小程序, 同时把个人的空间和 blog 迁过来, 本还想拿这个机器做自己私有的翻墙中继, 不过试了下到本地的速度最高也没超过 30KB/s 过, 遂放弃, 等啥时候有烧包需要时去买 linode 在日本的节点好了

系统选择和基本设置

之前自己用 debian-server 做纯服务端的东西感觉很爽, 占资源少文档丰富, 出点问题也能很容易在 Google 帮助下搞定, 所以系统也选的 debian, 然后因为只有 128M 内存, 上 64bit 没意义, 而且会更耗内存一点, 所以选了 32bit 的版本

debian 有几个比较坑爹的地方, 首先就是新建用户默认不建 home 目录, 非要在 useradd 的时候强加 -m 参数指定, 不然还要人肉新建 home 目录兵把 /etc/skel/ 下的几个隐藏文件拷过来做初始化

然后就是如果不是 root 用户, /sbin 和 /usr/sbin 默认是不在 PATH 里的, 需要手动将这些路径打开, 不然连 ifconfig 什么的都用不了. 修改 /etc/profile 里, 将 PATH 设成全用户一致 (此处参考: http://techpoet.blogspot.jp/2010/01/add-sbin-to-user-path-in-debian.html)

自己有一堆常用的配置库放在 github 上, 直接 git 取下来放到 home 目录, 把一堆 rc 结尾的文件前面加点让对应的程序能读到. 特别的是 vimrc, 最好还是放到 /etc/vim/vimrc.local 下, 不然 sudo 的时候碰上的还是没配过的 vim, 挺不爽的

系统自带应用内存优化

因为只有 128M 的内存, 要做很多勒紧腰带过日子的准备, 先就是卸载一堆自带的服务, 释放宝贵的内存, 此处参考了 http://www.yyphs.com/post/475.html, 不过我只去掉了会启动的组件, 即如下操作

# 系统升级
apt-get update && apt-get upgrade

# 去除多余软件
apt-get -y purge apache2-* bind9-* xinetd samba-* nscd-* portmap sendmail-* sasl2-bin

# 清理软件包
apt-get autoremove && apt-get clean

LEMP 环境安装

Web 应用从早两年的 LAMP 现在都在转 LEMP, 主要还是把 Apache 这个巨无霸换成了相对小巧又耐操的 nginx, 自己之前用 nginx 感觉也相当好, 这次也把系统默认的 Apache 干掉换这个

一开始参考的 http://www.howtoforge.com/installing-nginx-with-php5-and-mysql-support-on-debian-squeeze, 安装 mysql, nginx, php 都很正常, 但是这里面提到 debian 没有 php-fpm, 所以用 lighttpd, 不过我装 lighttpd 后内存爆掉, 随便搜了下优化方案都搞不定, 卸掉, 还是用习惯的 php-fpm 方式跑 fastcgi

http://www.webhostingtalk.com/showthread.php?t=1025286 这个提示装上 php5-fpm, 从装 nginx 开始就用不上了, 后面的优化也先不用管, 一会一起处理 (wordpress 似乎还依赖一个叫 php5-apc 的包, 装的时候一起带上)

内存优化

上面那一堆装好后如果不做优化, 坨坨的爆内存, 所以该关的功能要关, 该调的参要调. mysql 的一个优化关键是要关 innodb, 然后 nginx/php-fpm/mysql 都要做的事就是降低同时运行的实例数, 减少连接数, 同时严格控制各处内存大小. 具体怎么配的也忘了, 主要参考的是搜到的这两篇里关于参数的配置

http://blog.log4d.com/2011/11/vps-lnmp-setup-config/ (主要参考其参数配置)

http://keithscode.com/blog/23-running-mysql-on-a-small-128mb-vps.html (主要参考其参数配置)

最后的内存占用如下 (目前运行状态, 装了 wordpress)

$ free
             total       used       free     shared    buffers     cached
Mem:        262144      88972     173172          0          0          0
-/+ buffers/cache:      88972     173172
Swap:            0          0          0

安装迁移 wordpress

先在 mysql 里建对应的数据库, 这个随便都能搜到, 自己碰到个坑是 mysql 分配数据库权限时用已有用户无法登陆, 最后还是删掉用户在分配权限时再新建用户, 用 identify 来设置密码

mysql> create database wp;
mysql> grant all privileges on wp.* to wp_user@localhost identified by 'wp_pass';

装 wordpress, 一切正常, 用老的文件, 改名备份 wp-config.php 直接访问 wp-admin/install.php
用导出的文件再导入 (大于 2M 的话需要修改 php 的上传限制: 修改 /etc/php5/fpm/php.ini 文件修改 upload_max_filesize 项)

之前用的是多说的评论系统, 把插件重新启用, 并绑定原来注册过的站点就能将评论恢复, 一切搞定

奇怪的问题

上面写的好像很容易, 实际上对一个新手 OP 来说还是非常磕磕碰碰的, 最后在 OP 之外还有些奇怪的问题

先是多说, 绑定站点时临时性卡死, 然后再导数据时有延迟, 再后面就有重复的评论出现, 但是又不是所有的评论都被重复一遍, 研究了半天没弄明白, 自己手动删掉了重复的

然后删重复数据时发现有一条奇葩的回复显示在了另一篇文章下, 还是一条二级评论, 但是在后台看对应的又是正确的文章, 而且文章下的评论数是对的, 就是不显示, 自己折腾半天都没弄好, 无奈去联系多说的技术客服, 周末不在线, 留言后觉得不放心, 又去 dev.duoshuo.com/wordpress-plugin 报了一遍才安心

今天终于找上人, 帮着看了下, 先让我把评论模式从嵌套改成盖楼, 发现果然显示在正确的文章下了, 但是错误的引用了一条另一篇文章下的评论, 而且引用的那条评论还在此评论后好几个月才发出来, 顺手看了下 HTML 源码, 发现多说也是按 评论(post-id), 文章(thread-id), 引用(root-id) 来组织的数据 (这跟 BBS 上的 pid, gid, rid 一样一样啊), 然后这条出错的评论应该是两边哪里没同步好, 导致其 root-id 出错, 而 root-id 对应的评论在另一篇文章下, 所以嵌套模式时就显示到那边去了, 而正确的文章下因为找不到对应的父评论所以也没显示, 而盖楼模式下不判断父评论所以能显示, 但是存在错误引用关系, 最后让帮忙手动将出错评论的 root-id 置零, 总算搞定

第二件事是一个陈年老问题还在, 一年多前就写过一篇爆流量记, 里面提到有人抓 win7.iso, 但是当时找不到证据说是谁, 迁到新站后还在被抓, 期间 404/302/416 轮着报了一年多错居然还抓, 怒了去看看到底都哪来的请求, 意外发现有很多标识 QQDownload 的请求, 从 momodi 那联系上 QQ 旋风的人, 跟他们 blabla 说了一堆, 结果估计他们也从来没碰到过这样的事, 说要不你先自己屏蔽掉? 我哭笑不得说现在对我倒没啥影响了, 反正就算屏蔽也耗系统资源, 而且现在每次给个 416 也费不了我多少流量, 只是觉得很不爽而已. 不过后来自己去找了下迅雷和旋风离线下载的 IP 段, 这几个好像又都不在里面, 贴下原始日志, 大家看看有啥想法没:

$ awk '/QQDownload/{cnt[$1]++};END{for(ip in cnt){print ip, cnt[ip]}}' access.log
101.226.68.137 196
140.207.54.139 195
183.195.232.138 196
$ awk '/cn_windows_7/{cnt[$1]++};END{for(ip in cnt){print ip, cnt[ip]}}' access.log
122.141.67.50 329
123.185.52.73 54
14.114.226.18 150
14.114.226.194 14677
14.115.129.55 4040
180.117.68.185 361
59.33.63.137 1476

抽几条完整日志如下:

101.226.68.137 – – [21/Apr/2013:19:44:18 +0800] “HEAD / HTTP/1.1” 200 0 “-” “Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; QQDownload 713; .NET CLR 2.0.50727; InfoPath.2)”
14.115.129.55 – – [21/Apr/2013:19:44:18 +0800] “GET /ftp/Win7_rtm_with_loader/cn_windows_7_professional_x86_dvd_x15-65790.iso HTTP/1.1” 416 615 “http://yewen.us/” “Mozilla/4.0 (compatible; MSIE 9.0; Windows NT 6.1; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729)”
14.115.129.55 – – [21/Apr/2013:19:44:22 +0800] “GET /ftp/Win7_rtm_with_loader/cn_windows_7_professional_x86_dvd_x15-65790.iso HTTP/1.1” 416 615 “http://yewen.us/” “Mozilla/4.0 (compatible; MSIE 9.0; Windows NT 6.1; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729)”
14.115.129.55 – – [21/Apr/2013:19:44:25 +0800] “GET /ftp/Win7_rtm_with_loader/cn_windows_7_professional_x86_dvd_x15-65790.iso HTTP/1.1” 416 615 “http://yewen.us/” “Mozilla/4.0 (compatible; MSIE 9.0; Windows NT 6.1; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729)”

SNS 背后的技术: 消息流的推拉模式选择

上一篇扯到 SNS 本质上还是要满足沟通需要, 既然有沟通就涉及到信息本体的传播, 曾经的各种传播方式多半采用把信息从消息源推送到接收者, 由接收者用收件箱保存并查看的方式实现, 比如短信, 比如电子邮件等等

这种推送模式在传统信息沟通中运作的还不错, 每个人维护一个收件箱, 消息发完就不用管了, 因为一条消息一般不会发给非常多的人, 所以发送过程几乎瞬时完成, 同时对接收者来说单位时间收到的消息不会太多, 所以收件箱也不用非常大, 定期清理或提供更好的查询方式就行了 (前者比如功能手机的短信收件箱, 后者比如 Gmail)

而在 SNS 上这个模式就不那么适用, 从消息获取机制上来看, 应该是 “我去看我的朋友们都在干什么” 而不是 “我的朋友们有什么事情要分享给我”, 从技术上来说, 因为 SNS 的消息非常多, 为每个人维护一个足够大的收件箱是非常耗资源的, 另外, 对 SNS 上的超级用户 (比如有上千万好友的人人公共主页或小站, 几千万粉丝的微博大号) 来说, 推送消息这个过程也非常耗资源和时间. 所以很多 SNS 采用的是所谓的拉模式, 即对每个用户维护一份发件箱, 用户需要获取信息时主动发起对所有其收听的好友一个拉取的操作, 从不同好友的发件箱拉回所有消息, 并实时组织这些消息

至此, 所谓消息的推模式和拉模式都简单说完了, 听起来都各有各的道理, 到底要怎么用才合适?

从存储的角度来说, 拉模式一定会节省资源, 因为消息都是一个源, 而接收者可能有多个, 如果收件箱和发件箱同时存在, 则所有的收件箱大小应该等于发件箱乘上单条消息的平均接受人数, 按一般理论上 SNS 平均好友数量是 150~200 的规模算, 拉模式比推模式节省 150~200 倍的存储空间, 另外考虑到好友更多的用户活跃度更高, 因而这个数字会更大, 再者人人网存在公共主页和小站小组等超大规模接收者的实体, 微博只限制单个用户收听其他人的数量而不限制单个用户被多少人收听, 所以这个差异还会更大

不过从网络流量的角度来说则两种方式各有利弊, 看具体的用户分布是怎样的, 如果那些收听了很多人的用户频繁的获取消息, 即意味着会有非常频繁的消息获取操作, 而这些操作都需要进行远程调用并带来网络流量开销. 不过这个问题应该可以用临时收件箱来解决, 把用户获取到的信息缓存起来, 下次有拉操作时只需要判断对应的好友那是否有更新的内容产生, 如果有则把新的信息拉过来, 否则只是一次远程查询的操作. 缓存只要维持一个不那么长的失效时间, 就应该可以在 cache 命中率和占用内容容量上获得一个比较好的折中

说来说去似乎都是拉模式更好, 那为什么还会有推模式存在的意义呢? 当然推模式也并不是一无是处, 从信息的异步角度出发推是比拉要好的, 试想如果别人说给你发了条短信, 等你要看的时候还必须对方在联网状态你才能看到他发的什么, 那不是坑爹么, 但是在 SNS 上这个问题基本不存在, 因为所有的信息发送源也都是同一公司下的服务器存储, 不可能不在线, 只是对于第三方应用来说, 需要把在第三方产生的消息推到 SNS 上来, 至少推到活动人的发件箱, 这样看推还是没有特别大的优势? 从用户体验来说, 推模式可以更快的拿到信息, 省去了多一层的信息获取流程, 登陆后就可以立即看到自己收到的消息无疑更有吸引力, 而且如果用户存在短时间密集登陆的行为, 则推模式下只要直接取收件箱就行了, 不会在后面还有 1*n 的拉取操作. 不过话说回来, 对用户密集登陆的情况, 只要高峰流量不大的离谱, 拉模式还是应该能扛住, 按上一段我们的优化方案, 不还有一层缓存么, 而且拉模式所谓登陆后要过一会才能看到信息, 这个 “过一会” 的时间绝大部分情况下应该只有不到一秒, 甚至不到零点一秒, 那这个延迟对用户而言完全感觉不到或不觉得有问题, 用户的宽带接入或手机带宽反而更可能是打开慢的原因

说到这里某狗作为拉模式的铁杆粉丝对推模式的不屑之意已经表露无疑, 估计有很多推模式的拥趸已经在准备找数据找架构要拍死我了, 那最后我再补一个拉模式更好的理由来对推模式一剑封喉, 那就是, 隐私

从本质上来说, 推模式是在维护一个巨大的面向所有用户的 cache, 而天底下几乎所有的 cache 设计思想都应该是满足超大的读请求, 一定的追加操作, 以及极少的修改操作 (如果是 FIFO 或 LRU 删除以保持空间大小这个应该是 cache 内部的事情不算调用者的操作, 所谓的修改是指改 cache 里某个 key 对应的内容), 如果是普通的消息发送-收件人阅读的模式, 推模式没有任何问题, 短信邮件活了这么久都好好的. 但是, 凡事就怕但是, 如果消息流有大量的垃圾信息, 那就会崩溃掉, 除非在收件箱里另开一块用于处理垃圾, 比如电子邮件一般都有个垃圾邮件分类, 自动帮你判别, 但在 SNS 上, 对垃圾信息的判断无法做到那么有效, 就算能判断的比较准, 也没有另一个信息流来存放 (从这点来说 Facebook 的 ticker 其实就能起到垃圾信息收件箱的作用, 信息不丢, 但是也基本不会干扰用户, 如果要找回来也有地方可以去找), 而且 SNS 上太多用户的随意行为想做撤销, 比如发了不该发的照片, 或是 spammer 被举报后要将起发送的垃圾信息收回或做过滤, 如果再考虑上因隐私范围修改而要对已发出去的消息做限制, 这一大堆修改操作会让 cache 痛不欲生欲仙欲死

考虑下 SNS 上好友关系的频繁变化, 如果有新增好友关系或收听关系, 则推模式下后台应该立即对 Social Graph 上这条新的边做消息推送, 这时候还要依赖发件箱, 即推模式无法抛弃发件箱模块. 而如果是删除好友关系, 也应该立即把这条边做个逆操作, 这个操作更重, 需要扫收件箱的所有内容并删除特定的消息. 万一还有用户注销, 或是 spammer 被封需要收回其发出去的各种垃圾信息, 或是被 XSS/CSRF 攻击后要做清理, 都需要对大量用户的收件箱做扫描和选择性删除, 这些操作既重又要求很高的时效性, 否则就可能是安全事故, 这些都让推模式下 cache 的设计初衷被破坏的一塌糊涂

再说隐私控制, 最早的 SNS 压根就没有隐私这回事, 发出去的东西谁都可以看可以评论可以分享可以做任何演绎操作, 只是我的好友能有个简单快捷的入口找到, 后来就有了仅好友可见, 发送给指定分组 (其实是发送给特定的某些人的简化版), 不可分享不可回复等各种要求, 如果这个要求一直都是固定的也还好, 再消息发送的时候加个限制, 对不该收到此消息的人不做推送, 然后消息本身自带权限标记来说明收件人可以对此消息做什么动作. 可惜的是咱们亲爱的用户永远不会这么想, 发送对象会一直变, 今天是密友的明天可能是工作同事, 那推送时的判断就是一个一直在变的表达式, 维护这个表达式和查询判断又是个很重的操作, 而用户如果再发送完成后再改权限, 那就涉及到更多的对已推送消息的修改, 比如用户从所有人可见改为仅好友可见且不能被分享, 那其实是要重复一次消息的发送过程, 万一这中间有延迟或用户过了段时间才做这个操作导致以及有好友把该消息分享了出去, 那整个更新操作就不是一步而是一个 BFS 了, 对网络调用对 cache 都是莫大的伤害

如果我们把权限收回到消息本体, 每个人的收件箱或发件箱只是一个索引, 是不是就能解决问题呢? 好像是? 但是推模式下每次从收件箱给用户显示消息时还加一次判断, 这不还是在做拉操作么? 带了拉操作的推模式? 那为什么不直接改拉模式, 还节省 cache

再回到推模式更快的用户体验, 既然加了权限验证等操作, 那这个用户体验估计也省不到哪去, 而且 发送-验证-到收件箱 和 拉取-验证-取发件箱 都是三步操作, 谁也没有更简单, 反倒是拉模式的验证过程只在拉操作时发生, 而推模式下有修改时还要补发验证流. 另外因为收件箱容量不可能非常大, 不管是 cache 的用户数还是单个用户 cache 的消息条数都有限, 那 cache 不命中时还是必须走拉模式取消息, 如果把收件箱变成拉模式触发的短时小 cache, 或用推拉结合对热点用户做一些预处理, 整个用户体验应该还是可以保证的了的, 对于 cache 里可能的消息权限改动, 只要有一次通知, 让此 cache 失效, 下次还走纯拉模式应该就能搞定

上面都是理论扯淡, 具体应用还是要看 SNS 模型和消息模型来决定怎么用好. 以下根据公开资料和个人猜测来八卦下各家的做法

Twitter 公开资料说他们走的推拉结合, 有 cache, 但是大部分还是用拉的操作, 很多调用来源是移动端或 API, 慢点就慢点大家都无所谓, 第三方 API 本身就是在定期做拉操作以变相完成推模式, 而且发出去的东西容易收不回

Facebook 对离线用户应该是拉模式加收件箱 cache, 用不经常登陆的小号很容易验证, 只要稍微有几天没登陆, 一上去 news feed 也一定是空的, 要有取的等待时间, 而且明显不是因为翻墙带来的延迟. 而且 FB 对 news feed 默认不采用时间序, 如果用推模式用户查看时从收件箱取还是要做一次排序, 且这个排序操作不是非常轻, 那再前面套一层拉操作影响也不会大, 还不如直接走拉模式, 就算考虑热点用户被拉的操作会很频繁机器扛不住, 那同步几份发件箱对拉操作做负载均衡就可以了, 比起用推模式 100 倍以上的机器需求, 如果真的大量存在热点, 拉模式增加十倍机器随便就能解决这个性能问题. 不过如果用户在线, 则推模式生效, 有新消息会立马推送到 news feed 或 ticker 里来, 只是新来的消息只能按时间序放最新的地方了. 另外删除还是一个老大难问题, 经常发现有好友被攻击后发的垃圾消息要过很久才会被干掉 (退出后 cache 失效下一次登陆改拉操作才去掉的?)

新浪微博号称他们用推拉结合, 不过据我观察和小道消息应该还是纯拉加很小的 cache, 因为新浪微博活跃的都是一群公知大号, 给他们的消息做推操作会把网络压死 (另外吐个槽草根大号的运营人员都是准点上班的, 他们会几乎同时发大量内容, 这一瞬间如果用推估计就把全网给搞嗝屁了). 对用户而言, 登陆时拉出一整页的内容放 cache, 就新浪那个前端加载速度, 绝对不比后台做一次拉操作快, 所以用户几乎感觉不到, 等用户往下滚动页面时候依次吐出 cache 内容, 速度飞快体验完美, 吐完了就该手动翻页了, 不过有多少人会往后翻? 根据我在搜索上的经验, 应该不到 10%, 而这时候慢点大家也还是可以忍, 都主动翻了还在乎这一两秒? 在天朝政治正确很重要, 所以微博删帖的效率也一定要高, 以微博的删帖量, 走推模式估计还是会把网络拖挂, 而拉模式下只要判断下最原始微博是否不存在或属被证实的谣言就 OK

QQ 空间走的应该是推, 大部分时间都一亿多用户在线, 空间上有点新动态还是很快会在 QQ 面板上跳数字, 而且腾讯一直做的是实时通讯, 在实时过滤黄反方面经验好的很, 基本不用担心有太多类似删帖的更新操作把网络弄的很崩溃. 不过具体到 QQ 空间里的一些模块, 比如个人主页等, 那就看应用场景是推还是拉了

至于人人, 可能会比较敏感不便多说, 之前号称是推拉结合, 不过在我看来就是个比较单纯的推模式, 且目前看来推模式的各种坑爹问题都存在, 比如 cache 占用太多资源, 删除操作多且难做到实时等等都有. 还因为是推模式, 整个隐私框架也难有大的改进

最后给大家瞎掰一些数据算算不同模式的 cache 需求 (包括总 cache 大小和为负载均衡用的机器数) 和网络开销 (包括调用量和带宽), 看到底怎样好 (以下涉及到平均的数字多半都是长尾分布):
A. 某网, 日活跃用户 10M, 月活跃用户 50M, 总用户 100M, 平均每个人有 150 个双向好友, 每个活跃用户在活跃天内平均有 10 次查看操作, 平均每次操作取 20 条消息, 并有平均 3 次消息发布操作, 每条消息索引 1K, 消息本体 20K
B. 某网, 日活跃用户 15M, 月活跃用户 60M, 总用户 200M, 平均每个人有 100 个单向出好友边和 200 个单向入好友边, 每个活跃用户在活跃天内平均有 20 次查看操作, 平均每次操作取 10 条消息, 并有平均 2 次消息发布操作, 每条消息索引 1K, 消息本体 2K

极端情况 1: 如果消息发送者都是出边数在 M 这个级别且其每天发送量要比平均值高一两个数量级的怎么办? (比如新浪微博的各种大号, 算完后估计你也会理解除了商业原因, 某些架构下技术原因也还是对他们做点限制好)
极端情况 2: 如果消息查看者的入边数都在 K 这个级别且其每天的查看操作比平均值高一两个数量级怎么办? (比如新浪微博的 VIP 用户, 最多关注大几千人, 而且频繁刷)
极端情况 3: 考虑下活跃用户的各项数据平均值会比全局平均值高一倍

用沟通模型分析社交网络用户诉求

关于在社交网络 (Social Network Service, SNS) 上, 用户到底想要什么, 这个问题一直在想并且想了很久, 最近对个人认识体系有个相对完整的想法, 感觉还是用沟通模型来解释比较好

在得到这个想法前, 翻看了很多别的地方的讨论, 包括知乎上被转比较多那个 “上人人网就是为了泡妞” 的说法, 如果抛开这些非持续性的稳定需求, SNS 也好, IM 等也好, 最后一定还是回到沟通的本质上来. 虽然人人的早期 (当时还是校内和 5Q 在 PK), 以及其他的很多 SNS (比如早期的 51.com, 还有陌陌) 的第一波增长都是约炮, 但是约炮的人毕竟还是有限, 做成十万或百万规模还行, 而且有道德和法律风险, 要想继续做大, 必须还是走正路, 就还是沟通

在这里把不同的沟通模型拿进来看, 个人感觉主要的差异性在: 1) 实时与否; 2) 反馈与否; 3) 私密与否; 4) 重要与否. 当然这个划分方法不一定能很好区别, 不过大体来看可以如此. 实时性是指是否可以异步进行, 还是必须参与方必须实时参与; 反馈是说沟通是否单向, 收到消息的人是否需要反馈, 在这如果加个强要求就是是否必须反馈; 私密是说这个沟通是否有限制参与方, 非预期内的人是否也能参与; 重要表示对参与方的重视程度, 比如是否需要当面或书面等方式表达. 按我这个定义, 把常用的方式列举如下 (后面符号分别表示是否 实时/反馈/私密/重要, o 表是 x 表否 ? 表可能)

会议 o/o/o/o
聊天 o/o/x/x
电话 o/o/o/o
短信 x/o/o/o
电邮 x/o/o/o
IM ?/o/o/x
SNS x/o/?/x
博客 x/?/x/?
微信 ?/o/o/?

基本上越重要的事情应该是需要当面说, 并且最好是有记录的, 而一些随便的东西就不一定要当面, 而且不一定要是同步通讯, 再随便一点就也可以不要求私密性, 博客谁都可以看, 在轻松环境下的聊天也可以任何人都来参与. 另外一个比较大的差异就是是否需要反馈, 这事是你自说自话就行了, 还是必须有其他人反馈你才能继续下去, 或有动力持续下去.

扯回到 SNS (特别是人人) 上, 这四个特征对应的分别是:
1) 实时性. 信息是否能被实时获取, 甚至实时的推送到收件人那并有明显提示. 更严格对应到人人上, 就是新鲜事的到达速度, 以及导航栏气泡/聊天窗等信息到达速度. 应该说人人对实时性要求没那么高, 最多也就是数分钟这个实时性, 有爆炸性事件能保证可以被传播, 热点内容的传播足够快就行, 类似 IM 那样的实时性要求并无太多必要
2) 反馈性. 用户行为是否需要有反馈, 且反馈是否需要有明确通知. 对应到人人上, 就是新鲜事是否可以被回复, 被分享, 被喜欢 (或其他的快速情感表达, 赞/踩等都算), 反馈的通知就还是气泡或其他推送信息了. 不过很多反馈会要求实时, 比如两个人版聊起来了, 这时候微观交流就会被从 SNS 变成 IM. 其他的反馈可以不那么实时, 但是一定要有
3) 私密性. 用户行为是否需要限制可见人或参与人. 对应到人人上, 就类似个人主页是否任何人可见所有内容, 相片等内容是否只有好友可见或更复杂的隐私模型 (这个近期也想写点东西说下)
4) 重要性. 是否需要区分不同重要性的内容? 比如我女朋友的新鲜事我一定会关注, 而一些公共主页, 类似我们爱讲冷笑话这样的, 可能就是兴趣来了看一下, 没兴趣时就不看了, 来自这些地方的新鲜事丢了就丢了我也不在乎

SNS 的默认模型不需要那么实时, 所以太过频繁的消息推送会让用户觉得反感, 获取信息的主动权应该由用户来拉, 除非是用户非常关注的信息值得去推送 (比如某屌丝暗恋关注的女神有新动态, 他可能需要去抢沙发以获得更多好感)
另外用户的行为需要获得反馈来提升 TA 的黏度, 所以当用户的信息是被朋友或陌生人看到且作出相应回应 (我看过了, 我觉得赞, 我来说几句, 我来骂醒你) 后, 这个消息一定要推送给 TA, 至于实时性与否可以分情况讨论. 另外如果是太频繁且无用的提醒, 那此反馈渠道基本就算废了

来看看现在人人网上的用户都是啥样:
1) 有人喜欢显摆, 发个照片或日志希望大家都来看看我有多牛逼, 这种人需要更多更实时的反馈, 所以消息应该尽快推给他的朋友或其他人, 并让他的朋友尽快反馈 (哪怕只是很简单的一个赞按钮), 这样能促使他更多的活跃
2) 有人喜欢观察, 就是看自己订阅的信息, 这时候需要让他的信息流的实时性和完整性更好, 同时因为其拉信息足够频繁, 所以可以让其自己决定查看方式 (新鲜事是否按时间序这时候就很重要, 同时是否支持分好友组或分类型查看也很有用)
3) 有人就是 “我跟大家说个事, 我说完了, 再见” 这样的风格, 以工作了发婚纱照和娃的照片居多, 不过其实他们还是希望有反馈的, 比如 “婚纱照真漂亮” “娃好可爱” 等, 但是他对反馈的时效性不敏感, 只要定期有一个汇总就好, 并且他很可能不会主动来看反馈, 需要我们去推送 (一天一次? 一周一次?)
4) 有人就是 “今儿大爷心情好上来看看朋友们有啥好玩的”, 以工作了偶尔上来看的居多, 这些人需要尽可能快的让其获取关键信息 (主要是为了完成反馈操作), 然后再用起感兴趣的内容做扩散阅读希望能让其有更多的黏性

第一种和第二种人会是重度用户, 但第三种第四种才会是更多人的存活状态, 而且大部分人同时兼有 34 两种特性
第一种人需要的是更好的发布器, 让其发布内容更方便更爽, 同时把反馈尽可能快的推送给他并让他获得满足感, 并能跟其他人互动起来
第二种人需要的是更合理的信息获取方式, 优化新鲜事, 相册分享等浏览方式 (比如严格按时间序, 指定用户范围或新鲜事类型), 并给他足够简单高效的反馈让 13 两种用户能持续活动
第三种人更需要一个合适频度的反馈渠道 (邮箱? 合并了的手机通知?), 让他们能从发布信息获得成就感, 并保持活跃度继续发布内容或跟人交互, 过频的通知反倒会逼走他们
第四种人的信息流组织是个很有挑战性的活, TA 的收件箱可能压了一堆没读的东西, 那怎么把重要的东西挑出来让他不要错过, 同时也满足 13 两种人的反馈需求, 这个事情很重要, 那新鲜事就不应该按纯时间序而是按重要性组织, 或单独提供一个要闻模块, 以在可能很短的访问时间里更多完成他的信息获取需求和让他完成其他人需要的反馈行为

具体到一些产品上, 个人感觉应该做的
1) 消息本体, 保证其能被进行回复/分享/喜欢等操作, 并且消息展现的不同地方体验一致 (不管是新鲜事, 还是个人主页或时间轴, 以及终端页)
2) 新鲜事, 保证信息不会因为消息传递阻塞或蹩脚的排序/折叠/聚合方式而丢失或有很大延迟, 同时提供多种查看方式让用户自己选 (时间序/分组/分类/特别关注等)
3) 分享/收藏, 有算法聚合或人工推荐的公开信息聚合页供随便逛的用户打发时间, 也有多维度的查询方式让用户定位自己看过或收藏过的特定信息
4) 搜索, 类 Facebook Graph Search 那样的有点科幻, 而且实用性不高, 能按关键词搜人, 个体的新鲜事中的文本标题或正文, 以及全站公开信息的文本部分就 OK (比如状态, 相片/相册描述或标题, 分享的视频标题和评论等, 特别是用户自己的新鲜事内部搜索)
5) 通知系统, 更精准的消息推送, 现在导航栏上有消息气泡, 加好友气泡, 提醒气泡, 这仨应该被合成两个, 如果需要有实时的聊天需求, 类似私信或站内信这样的可以单拉一个气泡, 且无关紧要的进 ticker (这是个神器, 后面会讨论), 如果导航栏上的通知做的足够优, 网页内的右下角弹窗应该也能被整合
6) Ticker, 不得不说 Facebook 的这个东西是我在其他地方都没见过且我觉得非常赞的神器, 这里面糅合了不重要的新鲜事和通知, 保证信息的完整性, 且因为其占据的位置不大且不那么醒目, 又不会干扰用户正常行为, 同时 ticker 可以作为实验田, 我们猜测不重要的新鲜事可以试着先移到这, 如果用户还持续交互, 说明用户对此消息源或此类型的消息还是感兴趣的, 应该被更重视用更重的产品来展现, 通知反馈也是一样 (比如我的好友 x 跟 y 加为好友, 我要不要也跟 y 加为好友), 另外如果要对信息流做货币化, 这里显然也是个很好的切入点(做广告做久了的职业病, 啥都想到卖钱上去). 总之, 这个东西是少数几个我觉得一定要从别人那腆着脸抄过来并将其好好优化的功能之一
7) 隐私体系, 大部分人当这个地方是公开场合 (谁都能看, 比如微博), 或有限开放的公共场合 (大家对人人的默认定位, 好友可见, 其他人看我的内容被分享的情况了), 如果一旦有不适合按这个规模公开的内容, 用户又找不到方便的手段限制, 那势必会流失或遗憾 (比如我有一些以前的猥琐照片想分享给大学同宿舍那几个哥们, 现在就是放哪都不好, 人人上的隐私体系管不住, QQ 群别人不一定看到, 大家的交流也是时间序的而不是围绕这张照片进行, 不过 QQ 空间似乎有这样的功能了?), 而相反, 如果给用户提供隐私选项让其对部分内容加限制, 反倒应该能促进他更多的发布公开内容, 并让全站良性循环