基于websocket的简单广播系统

在年初的时候,我们有点儿小迷茫,于是也跟风去做了一些轻娱乐类的小游戏。
那时为了实战对战,想到需要一个实时性很强的技术实现,于是我去实现了一个websocket server,没想到后来这些小程序没有成,但是我们的这个web socket server 演化得无处不在。下面介绍一下这个技术实现。

看理论肯定会有点拗口是不是,我们直接上代码就得了。我们现在假设有这么一个用户付款的逻辑,在写用户付款事件时,我们事先并不知道以后还需要加什么逻辑,于是我们先把这个行为广播出去。以下是伪代码:

    req := httplib.Post("https://ws.app.12zan.net/eventcast/user/5905e89db43fec42e3055df05ff72afe")
    text, er := zanjson.Encode(order)
    if er != nil {  
        log.Println(ev)
        return
    }
    req.Param("data", string(text))
    resp,_ = req.Response()

好了,现在,每当有用户付款时,这个用户系统都会往/eventcast/user/5905e89db43fec42e3055df05ff72afe这个频道广播一条消息。但是很遗憾,目前没有客户端订阅这类消息,所有的消息都被丢弃了。

有一天,我们英明神武的老板决定要加一个通知,每当有一个新的用户付款时,都给公司的同胞们发一个邮件通知一下,我们获得了新的付费用户,好让大家小开心一把,尤其是第一个试用客户付费的时候,我们肯定都要开心地跳起来。这时我们如果去改线上运行好的付款系统,还是有点儿风险的,一旦有修改,我们就得走一下测试流程,不然万一有问题不是影响公司发财了吗。没关系,我们之前不是已经把付款事件广播出来了吗,我们现在用起来。写这么一段js,线上运行起来,就好了。

const webSocket = require('ws');
let ws = new webSocket("wss://ws.app.12zan.net/eventcast/user/5905e89db43fec42e3055df05ff72afe");
ws.on('open', function open() {
    console.log("connected");
});
ws.on('message', function incoming(data) {
    let user = JSON.parse(data);
    Mail.send("一个叫"+user.name+"的好心人支付了"+user.amount+"元,让主赞美他!");
});

好了,现在一旦有人付款,我们全公司都能收到一个邮件,及时得到这一好消息了。让我们小小地庆祝一下吧。

接下来又过了几天,我们想改进一下体验,用户一旦付款成功,就发送一条短信,告知用户他的有效期和我们的24小时客服电话;只需要这么一段代码部署起来运行就好了, 之前的任何代码都不用动:

const webSocket = require('ws');
let ws = new webSocket("wss://ws.app.12zan.net/eventcast/user/5905e89db43fec42e3055df05ff72afe");
ws.on('open', function open() {
    console.log("connected");
});
ws.on('message', function incoming(data) {
    let user = JSON.parse(data);
    let expiresAt = (zan.Date.now().add("+365 day").format("YYYY-mm-dd"));
    SMS.send(user.Mobile,"尊敬的"+user.name+",您成功购买了十二赞旗舰版,有效期至"+expiresAt+",请登陆:https://www.12zan.cn 查看,如有任何疑问,欢迎致电4006681102");
});

发送通知邮件和发送告知短信,都基于用户付款动作,但是发邮件和发短信的代码完全隔离,相互之间出完全不知道对方的存在。

是不是很赞?那我们接下来梳理一下逻辑。

概念及主要逻辑

也许我们来不及去翻看websocket的定义,但是我们可以简单地理解,Websocket是对HTTP协议的一个扩展升级,在发起连接时,HTTP部分都是有效的,只是连接成功以后,服务端和客户端的连接不断,双方可以双向数据传输,且服务端可以主动向客户端推送数据。

我们看一次Websocket发起连接的过程(来自维基百科):

客户端向服务端发起连接:

GET / HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Host: example.com
Origin: http://example.com
Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ==
Sec-WebSocket-Version: 13

服务端的返回:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: fFBooB7FAkLlXgRSz0BT3v4hq5s=
Sec-WebSocket-Location: ws://example.com/

在HTTP协议中常见的字段,如Cookies,Host等,依然有效。

但是具体到我们的应用上,十二赞的这个websocket server实现了两个小目标【多遗憾了,并没有赚到两个亿】:
1. 我们实现的是一个广播系统,一个广播系统意味着一个地方去发送数据,n多个接受端来接受数据。要支持非常多的客户端同时连上数据来实时接受数据。我们最终的server端的实现,全内存实现,没有用redis或是MySQL类似的数据库,就是为了实现超多客户端的支持。
2. 我们希望采用最简单、最通用的文案,并且,非常高效,支持非常多的客户端同时连接,我们认为http协议更简单,所以在发送的时候,我们是走http协议来发送数据的。并且,没有任何安全上的设计,如果数据很重要,请自行加密之后发送。

当然我们也有一些遗憾:
1. 允许数据丢失。有得必有失,我们允许一个比例的信息丢失。产生数据丢失时,不影响主逻辑。就像刚才的例子,发送邮件通知我们有新付款的这个事件没有触发并没有关系,我们到下午才发现有新用户付款,这时再去开香槟也不迟:(。
2. 容忍时序错乱。像刚才的例子,有新用户付款时,是先告诉我们全体同事有新付款,还是先给用户发送一条短信,并不那么重要。

好了,回到我们的系统,我们给一点点总结。

我们定义,每个websocket的入口,都是一个URL;去掉协议和HOST部分,剩下的PATH部分代表了不同的频道。比如,发起websocket时连接到ws://ws.app.12zan.net/channel/hello,那么这个频道地址就是/channel/hello;所有连接到ws.app.12zan.net/channel/hello的websocket客户端,他们会收到一模一样的消息,我们称之为订阅。

同时,为了简化发起数据的过程,我们还在websocket server中定义:当一个http 的客户端,以POST方式请求某一个地址时,我们截取URL中的PATH部分,得到频道名,并取POST的数据中的data域,作为要广播的数据,将之广播到相应的频道。

在十二赞的应用:

这个广播系统,在十二赞的整个技术架构中,后来应用的特别广。
比如,我们的部署系统zeus,在网页端实现了一个客户端,当服务端有应用重启、关闭、启动时,都会弹出消息通知。任何在打开了这个系统的网页的人都能看到。比如我和同事小王都正在zeus的网页上,我新建了一个search系统的一个节点,启动完毕的时候,我和小王会收到通知,在第三号服务器上新启了一个search系统的节点。我在操作,很关心这个,所心这时我可以放心去继续我的工作。小王正要在三号机器上新部署一个系统,他收到这个通知后,觉得这个机器可能会很忙,于是把自己的新实例部署在了四号机器上。

再比如,我们的日志服务器,担负着收集所有服务器上日志的使命。但是如果它挂掉了呢?于是我们在这个日志服务器上跑了一个定时器,每5秒钟向某个频道广播一条心跳消息,告诉世界自己还活着。然后另行跑了一个进程,收听这个频道的广播,如果连续30秒没有收到这个心跳包,证明这个日志服务器挂掉了,就发一条报警短信,通知同学去看看这个服务。

再比如,我们在日志服务上的应用,参见这里:十二赞日志系统简介

十二赞的商品搜索实现

其实要说很惭愧,虽然我是搜索出身人士,但是十二赞的搜索功能其实在目前是做得非常弱鸡的。不过还是介绍一下这个弱鸡的系统。

十二赞的搜索是基于elasticsearch的。因为业务量较小,所以,到目前为止,还没有用上elasticsearch强大可怕的集群功能。之所以觉得可以拿出来分享一下,是因为这个文案的投入是比较小的,效果尚可接受。

对于在小程序上开店的商家来说,一般商品数不多,很少于超过200的,所以在召加回率和精度上,我们只需要满足召回率就行了。即便搜索结果不够精准,因为商品少,用户一眼扫一下也能找到他想要的商品,搜索只是帮他从原有的上百个商品中把范围缩小到十分之一,就已经很有效了。
另外,也是因为团队小,我们的方案也特别注重精简,能不自己去写代码实现的,就不自己去写代码实现。

好了,上方案。
我们的商品数据是存在MySQL数据库中的,就用的阿里云的RDS。我们运行了一个go-mysql-elasticsearch,这个程序启动时会运行一次mysql_dump,把数据导出进入elasticsearch,然后接下来就会作为MySQL的一个slave,不断地读取MySQL的binlog,同步进elastichsearch。跟我们其他的服务一样,我们把这个elasticsearch和go-mysql-elasticsearch也封装进了一个docker 之中,只对外暴露9200端口来提供服务。

我们之前的ppt介绍过,十二赞的各种内部业务都已经微服务化,这个elasticsearch的服务,也不例外,它工作在es-sec.z.12zan.net:3000上,后端的实例可以有多个es的实例,通过http接口来请求elastichsearch的http接口时,网关 server会自动定向到负载最低的elasticsearch实例来提供服务。

这个方案运行不久,我们就发现一个问题,当我们的MySQL数据库的商品表新增了字段时,所有的后续的数据更新都无法同步的elasticsearch中去了。因为MySQL表的字段比elasticsearch中的字段多了(字段减少啊变更什么的都会导致这个问题)。

谢天谢地,因为Docker化,我们可以轻而易举地解决这个问题。我们的方案,当MySQL的数据表schema变化时,老的elasticsearch实例不变,继续提供服务,但是有缺陷,因为新增的数据没有同步进来;同时,我们新启一个Docker实例,这个实例启动约1分钟之后就同步进来了所有的商品数据,这时是两个elasticsearch的实例都在es-sec.z.12zan.net:3000上对外提供http接口服务,其中一个的数据是老的,一个实例的数据是全的。这时候我们关掉老的Docker上的实例就好了,就实现了索引的切换。用户可能会感受到新商品没有马上在搜索结果里体现,感觉搜索里的商品更新有延迟,但是基本上发现这个延迟的概率很小。

其实,淘宝的搜索的索引切换也有这么一个过程,不过要复杂的多,每天夜里要生成一个全量的索引,切换掉前一天的索引;然后另外有一个实时的搜索引擎,数据全放在内存里,只同步当天的数据变更,对外提供服务的接口将两份数据合并。

十二赞日志收集与报警系统简介

先快速介绍一下十二赞的日志收集系统:十二赞的日志收集系统,分为两块,一块是线上系统的各种报错、异常的日志收集,主要是各种线上代码运行期间产生,我们称之为log-collect,一块是用户行为操作的日志收集,主要是由各个业务系统根据用户的行为来上报,比如用户A访问了xx页面,用户B收藏了某某商品等,我们称之为eventdb。

基于这两块的日志收集,我们实现了一些自己非常自豪的特性。比如,基于log-collect,我们做到了能够主动去发现问题,抢在大多数客户发现异常之前,就把问题处理掉,从而做到不断地提高Saas系统的可用率和稳定性;基于eventdb,我们能做到非常完善的行为收集,将我们的返利模块、分销模块的准确度、实时性大幅度提高。

下面我们介绍一下系统的架构。

从需求上,我们认为log-collect是为了及时发现问题,并马上解决掉。但是这些日志,在我们解决掉问题之后,是不需要再保留这个日志的。比如,举个例子,用户注册的时候,可能输了一个12012345678的号码,这个号码是不对的,导致我们的验证短信发不出去,短信模块就会报错。我们的log-collect会收集到这条报错日志,马上告警。开发同学收到告警通知时,就马上去处理这个问题,用户输入120这个号段时,提示用户该号段是不被支持的,以后就再也不需要处理这个了,因为这条告警日志,我们是不存的,只存档15天就丢弃掉。

但是对于eventdb,我们的目标是为了对这些数据做分析,这些行为一般会跟财务相关,比如用户A通过用户B分享的链接进到了系统,5分钟之后有户A购买了商品付款了200元,2天后用户A退掉了其中的80元。这些数据,会影响到商家给用户B结算cps款项。类似这些数据,我们是永久存储的,不会抛弃。同时,这类数据,我们是要在保证准确性的基础上不断提高实时性的。所以对这类数据,我们有两条线来处理,一条是在线实时,一条是离线的一个小时跑一次数据的。

log-Collect

基于这种差异,我们在架构上也有不同。下面是log-collect的架构图:
https://ylpicture.oss-cn-beijing.aliyuncs.com/201811/48370500.8048.png

我们每一台服务端机器上都有一个live tail,实时监控日志文件,一旦日志文件有新的写入,就立刻发送到http的一个日志网关。这个网关就立刻把这条件日志推送给一个广播服务器,并写入到一个数据库(数据库会清掉7天之前的数据。)这个数据丢给广播服务器了之后,会在特定的频道进行广播。我写了一些客户端,订阅广播,根据日志内容的不同,将日志发给倍洽上不同的告警频道。(关于bearychat/中文名倍洽,大家可以自行去其官网上了解)。手机上装了倍洽,就可以随时接受告警通知了:
https://ylpicture.oss-cn-beijing.aliyuncs.com/201811/82177300.jpeg

eventDB

下图是eventDB的架构图:
https://ylpicture.oss-cn-beijing.aliyuncs.com/201811/87788400.6795.png

与log-collect相同的,收到新的行为事件后,网关也会在一个特定的频道进行广播。不同的有两点,一点是另一条链路先把行为事件写入到阿里云的oss存储起来,然后写了crontab每小时、每天定期从oss文件里导入到eventDB这个数据库;另一点是广播客户端工作的事情也变成了实时写入到eventDB这个数据库。

在事件收集上,也不一样,log-collect是在所有的服务器上部署了LiveTail来从日志文件中读取,而eventDB是需要各个业务系统自己向日志网关来汇报事件的。

存入数据库之后,后续就是再对这些数据进行分析,查找用户的来源渠道,计算佣金等等操作了。

 

由「根据用户手机壳颜色变换APP皮肤主题」说起

转载自公众号:1鸣说。作者:一鸣十二赞产品经理。

前几天的某互联网公司产品经理和程序员打架事件引起热议,据传原因是产品经理提了需求,要求APP程序员可以做到根据用户的手机壳颜色来改变软件主题颜色。这个原因当然是假的,只是用老梗来增加趣味。同时网上了引起了一轮新的讨论,程序员吐槽遇到的「奇葩需求」。

实际上,就这个需求本身,并不是十分荒诞。

我先把结论放在前面:这个过程中,最大的问题是产品经理和程序员之间的沟通不畅。

在这个需求真实存在的情况下,我们可以大致来模拟一下整个需求的流程。

第一步:用户反馈提出,APP的主题颜色太单一了,像QQ有那么多的主题皮肤,希望这款APP也能多弄点好看的皮肤,最好能弄一款和自己手机壳颜色一样的的皮肤就更好了。

第二步:产品经理经过一定范围的用户调研,确认了需求确实存在,有非常多的用户确实希望能够让APP皮肤颜色和自己的手机壳一致。

第三步:产品经理确认了该需求,同时出于为了方便用户的考虑(让用户少操作的角度),提出需求「根据用户手机壳颜色变换APP皮肤主题」

第四步:程序员只关注到需求的前半句,「根据手机壳颜色」,认为自动识别手机壳颜色做不到,直接驳回需求。

第五步:产品经理认为「变更APP皮肤主题」是一件非常容易的事情,两者引发争议。

你能看出问题出在哪里吗?这个问题在绝大多数的互联网公司中经常出现,就是产品经理和开发团队之间的沟通不畅导致的信息不对称。这个需求的核心是,也就是产品需求的最终结果是什么?是「皮肤颜色和手机壳颜色一致」。整个需求流程有两步,第一步:获取手机壳颜色。第二步:将皮肤颜色变更为该颜色。在这个过程中,程序员认为第一步是重点,而实际上,重点是第二步。程序员认为自动识别手机壳颜色做不到,所以一直纠结在这个点。然而第一步的这个过程,换个方式完全不影响整个需求本身。

在传递需求过程中,最忌讳的双方不愿意沟通了。以下是我希望的沟通流程:

产品经理:这里有个需求,是「根据用户手机壳颜色变换APP皮肤主题」。

程序员:我总结这个需求可以分成两步,第一步:我们的APP自动识别手机壳颜色。第二步:将皮肤颜色设置为这个颜色。是吗?

产品经理:是滴。

程序员:这个需求第二步比较容易做到,至于第一步,有点困难……

产品经理:哪里困难?

程序员:自动识别手机壳颜色做不到,这个技术我们不具备。

产品经理:没事,这个是小问题,我们换个方式获取手机壳颜色就好了。

程序员:怎么获取手机壳颜色?这个无论怎么想,都是很困难啊。

产品经理:哈哈,你想复杂了,我们让用户自动输入就好了呀。

程序员:还能这样,我真没想到。

产品经理:所以在第一步获取用户手机壳颜色的环节,最好的方案当然是自动获取啦,这个做不到换个方案,让用户输入也是不错滴。这个需求核心是让皮肤颜色和手机壳颜色一致,至于怎么获取手机壳颜色,这是个小问题,关系不大。

程序员:明白了,那我们怎么做。

产品经理:这个简单,在用户打开APP的时候,让用户输入自己的手机壳颜色,然后确认后自动变换皮肤颜色。

程序员:明白了,这就开始做。

最差的沟通方式就是:

产品经理:这里有个需求,是「根据用户手机壳颜色变换APP皮肤主题」。

程序员:SB,滚粗。

这个问题在知乎上讨论的热火朝天,实际上打错了靶子。一方面是产品经理不够专业,另一方面是程序员关注错了重点,不具备产品思维同时拒绝沟通导致的误解。

一般比较有经验的程序员面对需求时,会问明白,这个需求的「产品目标」是什么?产品目标才是核心,不要纠结于产品经理的提出的怎么实现这个产品目标的过程,这个过程是完全可以协商的,对于产品目标而言是次要的。

这一次讨论就到这里了,晚安。

千人千面,漫谈「个性化推荐

转载自公众号:1鸣说。作者:一鸣十二赞产品经理。

摘要:
个性化推荐功能在资讯类、搜索类以及电商类应用中均有广泛应用,本文简单谈谈笔者对个性化推荐的看法,以及抛砖引玉地提出了一种简单但自认为可能有效的个性化推荐算法。

在上一篇文章中,我讲了社交电商的五个重点环节,本文就五个重点环节中的「导购」环节,谈谈个性化推荐的应用。

我在上一篇文章中已经提到,导购环节指的是买家从进入商城到进入商品详情页的过程,这一过程中最重要的是展现买家最感兴趣的商品并让他点击进入该商品的详情页中,如何展现买家最感兴趣的商品,这就是个性化推荐的使用场景。下面的内容不仅仅针对于电商,这些内容套在其他行业也完全通用。

什么是个性化推荐

个性化推荐,指的是通过一定的算法,向用户展现他感兴趣的内容。准确地讲,个性化推荐更高效的连接了人与信息,降低了人获取信息的成本。

以电商为例,我举一个简单的例子,小明是一个数码爱好者,买了一个CPU,实际上小明是想组装一台新电脑,他接下去要买主板、显卡等配件,系统根据以往的情况,推测了这种情况,同时根据小明的经济水平,向小明推荐了一款中档显卡,小明第二次进入商城,发现首页出现的这款商品刚好是自己考虑买的,然后小明直接查看该商品,下单付款完成整个流程。

实际上,以电商为例,并不能很准确地阐述,因为一个东西即便用户再喜欢,也不可能无限制地去购买。以资讯类应用为例,可能更容易理解。谈到个性化推荐,最有名的莫过于今日头条,通过做更精准的个性化推荐,让这款产品成功地成为了一个时间黑洞。还是以小明为例,新用户小明初次打开该应用,系统是不了解小明的,然后小明用了一段时间这个应用后,系统发现当推送数码类文章给小明的时候,小明的打开率非常高,系统就判定小明喜欢此类文章,后续就会增加此类文章的曝光率。

为什么要做个性化推荐

互联网发展从PC端到移动端,人们的时间越来碎片化,然而同时信息的爆炸式增长,增加了用户获取有价值的信息的难度。如果一款产品不能给用户带来价值,那么被卸载是可以被预料到的结果。

对于资讯类产品,想要增加用户驻留的时间。对于搜索类应用,想要更准确地展现用户搜索的内容。对于电商类应用,想要提升用户的转化率。这些都是客观存在的要求,大数据的诞生,也为个性化推荐提供了生存土壤。

从南到北,从东往西,我们面临的用户实在太多了,每个人的爱好和需求都不同,彼之蜜糖汝之砒霜,A用户特别感兴趣的内容,B用户可能闻所未闻。千人一面展现内容的旧互联网模式已经过去,新互联网模式要求千人千面,只有针对每个不同的用户,展现更精准的内容,才能给其提供价值。

个性化推荐的原理

个性化推荐就是更高效的连接匹配人和信息,这其中就两个因素,「人」和「信息」,用户A和用户B属于人,信息C和信息D属于信息:用户A对信息C感兴趣,用户B和用户A的相似度非常高,那么用户B极有可能也对信息C感兴趣。同时信息C和信息D的相似度非常高,那么用户A和B对信息D感兴趣的可能性也非常高。

其实个性化推荐的基本原理非常简单,就是基于相似度的基础上,这里面所有的东西都是「可能」,都存在概率。即便A和B有99%的相似度,也存在A非常感兴趣的信息,对于B来说非常讨厌的可能。我们做个性化推荐,就基于这个原则,我们认为相似的人,可能有相同的兴趣,如果抛开这个前提,那么个性化推荐就非常难做。

个性化推荐是一个非常依赖数据和算法的功能,离开数据,一切推测都是空中楼阁,离开算法,再多数据也难以派上用场。

一个简单的个性化推荐算法

从上面介绍的原理看出,算法的核心就是相似度的判定,而判定相似度的一个非常简单粗暴的方法就是「贴标签」。虽然每个人都是有血有肉的个体,无论用多少文字,都难以完整地阐释一个特定的人。但是为了这个算法的实现,我们假定两个标签完全一样的人相似度为 100% 。

举个简单的例子,小明的标签是:90后,中产家庭,月入2万,男性,数码爱好者,手机发烧友,狂热果粉。小明在刚出粪叉的时候第一时间买了,然后这时候有个小张,具有和小明同样的标签:90后,中产家庭,月入2万,男性,数码爱好者,手机发烧友,狂热果粉,那么我们系统判定小明和小张的相似度为 100% ,同时认定小张也会在这个时候买粪叉。如果这时候小张进入商城,给他推荐粪叉准没错。

为方便讨论,也为了更严谨地阐述这个算法,我们由定性到定量进行分析。

用数字来表示,用 V (Value) 来表示信息E (Information)(备注:防止大写字母I被误认为1,我用E表示)对用户 U(User)的价值。 其中

V越大表示用户对该条信息越感兴趣。我们假定 V=1=100% 表示用户的该条信息非常该兴趣,转换到资讯类应用就是用户必定会打开该条资讯,转换到电商类应用就是用户必定会买这件商品。上述情况下该件商品对小张的价值可以表述为

算法1.1

以上就是算法1.0版本,事实上,两个用户相似度为100%的情况非常少见,还是用上面这个例子,小明(U1)和小张(U2)每个人都有10个标签,他们前9个标签都完全一致,只有最后一个标签不一样。某个信息E对小明的价值是 1 ,即

那么我们认为 小明和小张的相似度为 90% ,也就是0.9,即

我们计算信息E对小张的价值为

算法1.2

用以上策略考虑相似度基本可行,但是忽略了特定标签的价值的影响。还是用上面的例子,小明和小张的其他所有9个标签完全一致,最后一个标签小明是狂热果粉,而小张是狂热果黑,那么信息E对小张的价值很可能是0而不是0.9,问题出在哪里?我们忽略特定标签对信息价值的影响,实际上我们应该给信息也贴上标签,然后再匹配用户和信息之间的关联度(用D表示)。我们给信息E贴上狂热果粉这个标签,那么小明匹配到这个标签,

小张没有匹配到这个标签,那么

后续算法

由于时间有限(我准备睡觉了(~﹃~)~zZ),先推理到这里,我们下一期再续。使用贴标签的方式,是一种简单粗暴但是有效的判定信息是否有价值的算法,后续我们再讨论以下几种情况(逐级递进):

(1)信息E存在多个标签

(2)用户U1和U2的标签数量不一样多

(3)标签增加权重W(Weight),以及动态变更的算法

(4)多个信息分别有不同标签,分别对用户的价值

(5)信息的标签增加权重,以及动态变更

最终达到的效果是,用户U和信息E的标签以及权重是会随着他的操作动态变更的,最后根据一个公式计算E对U的价值: