孙宇的技术专栏 大数据/机器学习

Feed架构设计

2018-05-17

阅读:


Feed

微博,微信朋友圈,Pinterest是典型的feed流业务,系统中的每一条消息就是一个feed。

这类业务的特点是:

  • 有好友关系,例如关注,粉丝
  • 我们的主页由别人发布的feed组成

这类业务的典型动作是:

  • 关注,取关
  • 发布feed
  • 拉取自己的主页feed流

这类业务的核心元数据是:

  • 关系数据
  • feed数据

feed流业务最大的特点是“我们的主页由别人发布的feed组成”,获得朋友圈消息feed流集合,从技术上说,主要有“拉取”与“推送”两种方式。

拉流

某feed系统里有ABCD四个用户,其中:

  • A关注了BC,D关注了B
  • B发布过四条feed:msg1, msg3, msg5, msg10
  • C发布过两条feed:msg2, msg8

其关系存储又包含关注关系与粉丝关系,“A关注了BC,D关注了B”的潜台词是“B有两个粉丝AD,C有一个粉丝A”。

每一个用户,都有一个feed队列,记录自己曾经发布的所有feed数据。在拉模式中,发布一条feed的流程非常简单,例如C新发布了一条msg12:只需往C的feed队列里加入一条feed即可。

取消关注的流程也非常简单,例如A取消关注C:只需要在A的关注列表里删除C,并在C的粉丝列表里删除A即可。

在拉模式中,用户A获取“由别人发布的feed组成的主页”的过程及其复杂,此时需要:

  • 获取A的关注列表
  • 获取所关注列表中,所有用户发布的feed
  • 对消息进行rank排序(假设按照发布时间排序),分页取出对应的一页feeds

feed流的拉模式的优点是:

  • 存储结构简单,数据存储量较小,关系数据与feed数据都只存一份
  • 关注,取关,发布feed的业务流程非常简单
  • 存储结构,业务流程都比较容易理解,适合项目早期用户量、数据量、并发量不大时的快速实现

缺点也显而易见:

  • 拉取朋友圈feed流列表的业务流程非常复杂
  • 有多次数据访问,并且要进行大量的内存计算,网络传输,性能较低

推流

推模式,关系数据的存储与拉模式完全一样。feed数据,每个用户也存储自己发布的feed。feed数据存储,与拉流不同的是,每个用户还需要存储自己收到的feed流。如:

  • B曾经发布过 1,3,5,10
  • C曾经发布过 2,8
  • A关注了BC,所以A的接收队列是 1,2,3,5,8,10
  • D关注了B,所以D的接受队列是 1,3,5,10

在推模式中,获取“由别人发布的feed组成的主页”会变得异常简单,假设一页消息为3条feed,A如果要看自己朋友圈的第二页消息,直接返回1,2,3即可。第一页朋友圈是最新的消息,即5,8,10。

在推模式中,发布一条feed的流程会更复杂一点。

例如B新发布了一条msg12:

  • 在B的发布feed存储里加入消息12
  • 查询B全部粉丝AD
  • 在粉丝AD的接收feed存储里也加入消息12

之所以该方案称为推模式,就是因为,用户发布feed的时候, 直接将feed推到了粉丝的接收列表里,故称为“推模式”。不止写发布feed存储,而且要写多个粉丝的接收feed存储

在推模式中,添加关注的流程也会变得复杂:

例如D新增关注C:

  • 在D的关注存储里添加C
  • 在C的粉丝存储里添加D
  • 在D的接收feed存储里加入C发布的feed

在推模式(写扩散)中,取消关注的流程:

例如A取消关注C:

  • 在A的关注存储里删除C
  • 在C的粉丝存储里删除A
  • 在A的接收feed存储里删除C发布的feed

feed流的推模式(写扩散)的优点是:

  • 消除了拉模式的IO集中点,每个用户都读自己的数据,高并发下锁竞争少。
  • 拉取朋友圈feed流列表的业务流程异常简单,速度很快
  • 拉取朋友圈feed流列表,不需要进行大量的内存计算,网络传输,性能很高

其缺点是:

  • 极大极大消耗存储资源,feed数据会存储很多份,例如杨幂5KW粉丝,她每次一发博文,消息会冗余5KW份
  • 新增关注,取消关注,发布feed的业务流会更复杂

feed流业务的推拉模式小结:

  • 拉模式,读扩散,feed存一份,存储小,用户集中访问数据,性能差
  • 推模式,写扩散,feed存多份,用冗余存储换锁冲突,性能高

综合

通常为了实时性,都会采用推流的方式。但推流的方式缺点很明显就是会冗余存储。而且,如果用户的粉丝特别多,就会出现延迟。这里首先想到的肯定是异步处理。明星发帖后肯定是第一时间显示,然后使用异步任务去设置粉丝的 feed 流内容。

为了进一步提升效率,需要对粉丝进行筛选。我们做一下改进把用户分成有效和无效的用户。比如说有一百个粉丝,我发一条微博的时候不需要推给一百个粉丝,因为可能有50个粉丝不会马上来看。这样同步推送给他们,相当于做无用功。

比如当天登陆过的人标识为有效用户,我们只需要发送给这些粉丝,这样压力马上就减轻了,投递的延迟也减小了。

另外,还可以根据互动程度、关系密切程度进行一些排序,对某些用户先推。另外,还可以根据其它算法对用户进行分类,比如按注册时用户选的爱好、兴趣等,或者他之前对哪些内容进行过评论(通常系统内会对用户打上很多个标签,以此为参照)。

https://blog.csdn.net/einsteinlike/article/details/45579351

技术实现-反向 Ajax

Ajax、反向 Ajax 异步的 JavaScript 和 XML (Ajax),一种可通过 JavaScript 来访问的浏览器功能特性,其允许脚本向幕后的网站发送一个 HTTP 请求而又无需重新加载页面。 Ajax 的出现已经超过了十年,尽管其名字中包含了 XML,但您几乎可以在 Ajax 请求中传送任何的东西。最常使用的数据是 JSON,它与 JavaScript 语法非常接近且消耗更少的带宽。

反向 Ajax (Reverse Ajax) 本质上则是这样的一种概念:能够从服务器端向客户端发送数据。在一个标准的 HTTP Ajax 请求中,数据是发送给服务器端的,反向 Ajax 可以某些特定的方式来模拟发出一个 Ajax 请求,这样,服务器就可以尽可能快地向客户端发送事件(低延迟通信)。

反向 Ajax 的目的是让服务器将信息推送到客户端。Ajax 请求默认情况下是无状态的,且只能从客户端向服务器端发出请求。可以通过使用技术模拟服务器端和客户端之间的响应式通信来绕过这一限制。方法有如下几种:

HTTP 轮询

轮询 (Polling) 涉及了从客户端向服务器端发出请求以获取一些数据,这显然就是一个纯粹的 Ajax HTTP 请求。为了尽快地获得服务器端事件,轮询的间隔(两次请求相隔的时间)必须尽可能地小。

但有这样的一个缺点存在:如果间隔减小的话,客户端浏览器就会发出更多的请求,这些请求中的许多都不会返回任何有用的数据,而这将会白白地浪费掉带宽和处理资源。

用 JavaScript 实现的轮询的优点和缺点:

  • 优点:很容易实现,不需要任何服务器端的特定功能,且在所有的浏览器上都能工作。
  • 缺点:这种方法很少被用到,因为它是完全不具伸缩性的。试想一下,在 100 个客户端每个都发出 2 秒钟的轮询请求的情况下,所损失的带宽和资源数量,在这种情况下 30% 的请求没有返回数据。

Comet

Comet 请求被发送到服务器端并保持一个很长的存活期,直到超时或是有服务器端事件发生。 在该请求完成后,另一个长生存期的 Ajax 请求就被送去等待另一个服务器端事件。 使用 Comet 的话,Web 服务器就可以在无需显式请求的情况下向客户端发送数据。

Comet 的一大优点是: 每个客户端始终都有一个向服务器端打开的通信链路

服务器端可以通过在事件到来时立即提交(完成)响应来把事件推给客户端,或者它甚至可以累积再连续发送。因为请求长时间保持打开的状态,故服务器端需要特别的功能来处理所有的这些长生存期请求。

XMLHttpRequest 长轮询

打开一个到服务器端的 Ajax 请求然后等待响应。服务器端需要一些特定的功能来允许请求被挂起,只要一有事件发生,服务器端就会在挂起的请求中送回响应并关闭该请求。然后客户端就会使用这一响应并打开一个新的到服务器端的长生存期的 Ajax 请求

  • 优点:客户端很容易实现良好的错误处理系统和超时管理。这一可靠的技术还允许在与服务器端的连接之间有一个往返,即使连接是非持久的(当您的应用有许多的客户端时,这是一件好事)。它可用在所有的浏览器上;只需要确保所用的 XMLHttpRequest 对象发送到了简单的 Ajax 请求就可以了。
  • 缺点:相比于其他技术来说,不存在什么重要的缺点,像所有我们已经讨论过的技术一样,该方法依然依赖于无状态的 HTTP 连接,其要求服务器端有特殊的功能来临时挂起连接。

建议

反向 Ajax 实现和使用 Comet 的最好方法是通过 XMLHttpRequest 对象,它提供了一个真正的连接句柄和错误处理。因此建议选择经由 HTTP 长轮询使用 XMLHttpRequest 对象(在服务器端挂起的一个简单的 Ajax 请求)的 Comet 模式,所有支持 Ajax 的浏览器也都支持该种做法。

示例: index.php:

var timestamp = 0; 
    var url = 'backend.php'; 
    var error = false; 
    function connect(){ 
        $.ajax({ 
            data : {'timestamp' : timestamp}, 
            url : url, 
            type : 'get', 
            timeout : 0, 
            success : function(response){ 
                var data = eval_r('('+response+')'); 
                error = false; 
                timestamp = data.timestamp; 
                $("#content").append('
' + data.msg + '
'); 
            }, 
            error : function(){ 
                error = true; 
                setTimeout(function(){ connect();}, 5000); 
            }, 
            complete : function(){ 
                if (error) 
                    setTimeout(function(){connect();}, 5000); 
                else 
                    connect(); 
            } 
        }) 
    } 
    function send(msg){ 
        $.ajax({ 
            data : {'msg' : msg}, 
            type : 'get', 
            url : url 
        }) 
    } 
    $(document).ready(function(){ 
        connect(); 
    }) 

backend.php:

date_default_timezone_set('Etc/GMT-8');
set_time_limit(0);
error_reporting(0);
$filename  = 'data.txt'; 
  
$msg = isset($_GET['msg']) ? $_GET['msg'] : ''; 
if ($msg != '') { 
 file_put_contents($filename,$msg); 
 die(); 
} 
$lastmodif    = isset($_GET['timestamp']) ? $_GET['timestamp'] : 0; 
$currentmodif = filemtime($filename); 
while ($currentmodif <= $lastmodif){ 
 usleep(10000); 
 clearstatcache(); 
 $currentmodif = filemtime($filename); 
} 
$response = array(); 
$response['msg']       = file_get_contents($filename); 
$response['timestamp'] = $currentmodif; 
echo json_encode($response); 
flush(); 

评论

文章