(图1:短视频去重当前现状)视频去重本身是基于用户实际观看过的视频进行过滤,但考虑到实际观看的视频是通过客户端埋点上报,存在一定的时延,因此服务端会保存用户最近100条下发记录用于去重,这样就保证了即使客户端埋点还未上报上来,也不会给用户推荐了已经看过的视频(即重复推荐)。而下发给用户的视频并不一定会被曝光,因此仅保存100条,使得未被用户观看的视频在100条下发记录之后仍然可以继续推荐。当前方案主要问题是占用Redis内存非常大,因为视频ID是以原始字符串形式存在Redis Zset中,为了控制内存占用并且保证读写性能,我们对每个用户的播放记录最大长度进行了限制,当前限制单用户最大存储长度为10000,但这会影响重度用户产品体验。
(图2:统一去重服务主要步骤)整个过程很清晰,但是考虑到需要支持千万级用户量,假设按照5000万用户目标设计,我们还需要考虑四个问题:
(图3:统一去重服务整体流程)磁盘KV写性能相比读性能差很多,尤其是在Value比较大的情况下写QPS会更差,考虑日活千万级情况下磁盘KV写性能没法满足直接写入要求,因此需要设计写流量汇聚方案,即将一段时间以内同一个用户的播放记录汇聚起来一次写入,这样就大大降低写入频率,降低对磁盘KV的写压力。
(图4:近实时写入方案)近实时写入的出发点很单纯,优势也很明显,可以近实时地将播放埋点中的视频ID写入到布隆过滤器中,而且时间比较短(N分钟),可以避免Redis Zset中暂存的数据过长。但是,仔细分析还需要考虑很多特殊的场景,主要如下:第一,Redis中保存一个Value其实相当于一个分布式锁,实际上很难保证这把“锁”是绝对安全的,因此可能会存在两次收到播放埋点均认为可以进行磁盘KV写操作,但这两次读到的暂存数据不一定一样,由于磁盘KV不支持布隆过滤器结构,写入操作需要先从磁盘KV中读出当前的布隆过滤器,然后将需要写入的视频ID更新到该布隆过滤器,最后再写回到磁盘KV,这样的话,写入磁盘KV后就有可能存在数据丢失。第二,最后一个N分钟的数据需要等到用户下次再使用的时候才能通过播放埋点触发写入磁盘KV,如果有大量不活跃的用户,那么就会存在大量暂存数据遗留在Redis中占用空间。此时,如果再采用定时任务来将这部分数据写入到磁盘KV,那么也会很容易出现第一种场景中的并发写数据丢失问题。如此看来,近实时写入方案虽然出发点很直接,但是仔细想来,越来越复杂,只能另寻其他方案。3.2.2 批量写入既然近实时写入方案复杂,那不妨考虑简单的方案,通过定时任务批量将暂存的数据写入到磁盘KV中。我们将待写的数据标记出来,假设我们每小时写入一次,那么我们就可以把暂存数据以小时值标记。但是,考虑到定时任务难免可能会执行失败,我们需要有补偿措施,常见的方案是每次执行任务的时候,都在往前多1~2个小时的数据上执行任务,以作补偿。但是,明显这样的方案并不够优雅,我们从时间轮得到启发,并基于此设计了布隆过滤器批量写入的方案。我们将小时值首尾相连,从而得到一个环,并且将对应的数据存在该小时值标识的地方,那么同一小时值(比如每天11点)的数据是存在一起的,如果今天的数据因任务未执行或执行失败未同步到磁盘KV,那么在第二天将会得到一次补偿。顺着这个思路,我们可以将小时值对某个值取模以进一步缩短两次补偿的时间间隔,比如图5所示对8取模,可见1:00~2:00和9:00~10:00的数据都会落在图中时间环上的点1标识的待写入数据,过8个小时将会得到一次补偿的机会,也就是说这个取模的值就是补偿的时间间隔。
(图5:批量写入方案)那么,我们应该将补偿时间间隔设置为多少呢?这是一个值得思考的问题,这个值的选取会影响到待写入数据在环上的分布。我们的业务一般都会有忙时、闲时,忙时的数据量会更大,根据短视频忙闲时特点,最终我们将补偿间隔设置为6,这样业务忙时比较均匀地落在环上的各个点。确定了补偿时间间隔以后,我们觉得6个小时补偿还是太长了,因为用户在6个小时内有可能会看过大量的视频,如果不及时将数据同步到磁盘KV,会占用大量Redis内存,而且我们使用Redis ZSet暂存用户播放记录,过长的话会严重影响性能。于是,我们设计每个小时增加一次定时任务,第二次任务对第一次任务补偿,如果第二次任务仍然没有补偿成功,那么经过一圈以后,还可以得到再次补偿(兜底)。细心一点应该会发现在图5中的“待写入数据”和定时任务并不是分布在环上的同一个点的,我们这样设计的考虑是希望方案更简单,定时任务只会去操作已经不再变化的数据,这样就能避免并发操作问题。就像Java虚拟机中垃圾回收一样,我们不能一边回收垃圾,一边却还在同一间屋子里扔着垃圾。所以,设计成环上节点对应定时任务只去处理前一个节点上的数据,以确保不会产生并发冲突,使方案保持简单。批量写入方案简单且不存在并发问题,但是在Redis Zset需要保存一个小时的数据,可能会超过最大长度,但是考虑到现实中一般用户一小时内不会播放非常大量的视频,这一点是可以接受的。最终,我们选择了批量写入方案,其简单、优雅、高效,在此基础上,我们需要继续设计暂存大量用户的播放视频ID方案。
(图6:数据分片方案)对应地,我们的定时任务也要进行分片,每个任务分片负责处理一定数目的数据分片。否则,如果两者一一对应的话,将分布式定时任务分成5000个分片,虽然对于失败重试是更好的,但是对于任务调度来说会存在压力,实际上公司的定时任务也不支持5000分分片。我们将定时任务分为了50个分片,任务分片0负责处理数据分片0~100,任务分片1负责处理数据分片100~199,以此类推。
(图7:数据淘汰方案)对于数据过期时间的设置我们也进行了精心考虑,数据按月存储,因此新数据产生时间一般在月初,如果仅将过期时间设置为6个月以后,那么会造成月初不仅产生大量新数据,也需要淘汰大量老数据,对数据库系统造成压力。所以,我们将过期时间进行了打散,首先随机到6个月后的那个月任意一天,其次我们将过期时间设置在业务闲时,比如:00:00~05:00,以此来降低数据库清理时对系统的压力。
(图8:整体方案流程)首先,从Kafka播放埋点监听到数据以后,我们根据用户ID将该条视频追加到用户对应的播放历史中暂存,同时根据当前时间和用户ID的Hash值确定对应时间环,并将用户ID保存到该时间环对应的用户列表中。然后,每个分布式定时任务分片去获取上一个时间环的播放用户数据分片,再获取用户的播放记录更新到读出的布隆过滤器,最后将布隆顾虑其序列化后写入磁盘KV中。
(图9:迁移方案一)但是,我们忽略了两个问题:第一,新的Redis仅用作暂存,因此比老的Redis容量小很多,没法一次性将数据迁移过去,需要分多批迁移;第二,迁移到新的Redis后的存储格式和老的Redis不一样,除了播放视频列表,还需要播放用户列表,咨询DBA得知这样迁移比较难实现。既然迁移数据比较麻烦,我们就考虑能不能不迁移数据呢,在去重的时候判断该用户是否已迁移,如未迁移则同时读取一份老数据一起用于去重过滤,并触发将该用户的老数据迁移到新Redis(含写入播放用户列表),三个月以后,老数据已可过期淘汰,此时就完成了数据迁移,如图10所示。这个迁移方案解决了新老Redis数据格式不一致迁移难的问题,而且是用户请求时触发迁移,也避免了一次性迁移数据对新Redis容量要求,同时还可以做到精确迁移,仅迁移了三个月内需要迁移数据的用户。
(图10:迁移方案二)于是,我们按照方案二进行了数据迁移,在上线测试的时候,发现由于用户首次请求的时候需要去迁移老的数据,造成去重接口耗时不稳定,而视频去重作为视频推荐重要环节,对于耗时比较敏感,所以就不得不继续思考新的迁移方案。我们注意到,在定时批量生成布隆过滤器的时候,读取到时间环对应的播放用户列表后,根据用户ID获取播放视频列表,然后生成布隆过滤器保存到磁盘KV,此时,我们只需要增加一个从老Redis读取用户的历史播放记录即可把历史数据迁移过来。为了触发将某个用户的播放记录生成布隆过滤器的过程,我们需要将用户ID保存到时间环上对应的播放用户列表,最终方案如图11所示。
(图11:最终迁移方案)首先,DBA帮助我们把老Redis中播放记录的Key(含有用户ID)都扫描出来,通过文件导出;然后,我们通过大数据平台将导出的文件导入到Kafka,启用消费者监听并消费文件中的数据,解析后将其写入到当前时间环对应的播放用户列表。接下来,分布式批量任务在读取到播放用户列表中的某个用户后,如果该用户未迁移数据,则从老Redis读取历史播放记录,并和新的播放记录一起更新到布隆过滤器并存入磁盘KV。很好奇,就使用rubyonrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提
我正在尝试使用ruby和Savon来使用网络服务。测试服务为http://www.webservicex.net/WS/WSDetails.aspx?WSID=9&CATID=2require'rubygems'require'savon'client=Savon::Client.new"http://www.webservicex.net/stockquote.asmx?WSDL"client.get_quotedo|soap|soap.body={:symbol=>"AAPL"}end返回SOAP异常。检查soap信封,在我看来soap请求没有正确的命名空间。任何人都可以建议我
我有一个模型:classItem项目有一个属性“商店”基于存储的值,我希望Item对象对特定方法具有不同的行为。Rails中是否有针对此的通用设计模式?如果方法中没有大的if-else语句,这是如何干净利落地完成的? 最佳答案 通常通过Single-TableInheritance. 关于ruby-on-rails-Rails-子类化模型的设计模式是什么?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.co
我想安装一个带有一些身份验证的私有(private)Rubygem服务器。我希望能够使用公共(public)Ubuntu服务器托管内部gem。我读到了http://docs.rubygems.org/read/chapter/18.但是那个没有身份验证-如我所见。然后我读到了https://github.com/cwninja/geminabox.但是当我使用基本身份验证(他们在他们的Wiki中有)时,它会提示从我的服务器获取源。所以。如何制作带有身份验证的私有(private)Rubygem服务器?这是不可能的吗?谢谢。编辑:Geminabox问题。我尝试“捆绑”以安装新的gem..
我将应用程序升级到Rails4,一切正常。我可以登录并转到我的编辑页面。也更新了观点。使用标准View时,用户会更新。但是当我添加例如字段:name时,它不会在表单中更新。使用devise3.1.1和gem'protected_attributes'我需要在设备或数据库上运行某种更新命令吗?我也搜索过这个地方,找到了许多不同的解决方案,但没有一个会更新我的用户字段。我没有添加任何自定义字段。 最佳答案 如果您想允许额外的参数,您可以在ApplicationController中使用beforefilter,因为Rails4将参数
最近,当我启动我的Rails服务器时,我收到了一长串警告。虽然它不影响我的应用程序,但我想知道如何解决这些警告。我的估计是imagemagick以某种方式被调用了两次?当我在警告前后检查我的git日志时。我想知道如何解决这个问题。-bcrypt-ruby(3.1.2)-better_errors(1.0.1)+bcrypt(3.1.7)+bcrypt-ruby(3.1.5)-bcrypt(>=3.1.3)+better_errors(1.1.0)bcrypt和imagemagick有关系吗?/Users/rbchris/.rbenv/versions/2.0.0-p247/lib/ru
在Rails4.0.2中,我使用s3_direct_upload和aws-sdkgems直接为s3存储桶上传文件。在开发环境中它工作正常,但在生产环境中它会抛出如下错误,ActionView::Template::Error(noimplicitconversionofnilintoString)在View中,create_cv_url,:id=>"s3_uploader",:key=>"cv_uploads/{unique_id}/${filename}",:key_starts_with=>"cv_uploads/",:callback_param=>"cv[direct_uplo
我想在Ruby中创建一个用于开发目的的极其简单的Web服务器(不,不想使用现成的解决方案)。代码如下:#!/usr/bin/rubyrequire'socket'server=TCPServer.new('127.0.0.1',8080)whileconnection=server.acceptheaders=[]length=0whileline=connection.getsheaders想法是从命令行运行这个脚本,提供另一个脚本,它将在其标准输入上获取请求,并在其标准输出上返回完整的响应。到目前为止一切顺利,但事实证明这真的很脆弱,因为它在第二个请求上中断并出现错误:/usr/b
您如何在Rails中的实时服务器上进行有效调试,无论是在测试版/生产服务器上?我试过直接在服务器上修改文件,然后重启应用,但是修改好像没有生效,或者需要很长时间(缓存?)我也试过在本地做“脚本/服务器生产”,但是那很慢另一种选择是编码和部署,但效率很低。有人对他们如何有效地做到这一点有任何见解吗? 最佳答案 我会回答你的问题,即使我不同意这种热修补服务器代码的方式:)首先,你真的确定你已经重启了服务器吗?您可以通过跟踪日志文件来检查它。您更改的代码显示的View可能会被缓存。缓存页面位于tmp/cache文件夹下。您可以尝试手动删除
我想为我的Rails网络应用程序提供推荐功能。特别是,我想向新注册的用户推荐他可能想要关注的其他用户。Rails中是否有用于此目的的引擎/gem?如果没有,我应该从哪里开始构建它?谢谢。 最佳答案 有Coletivogemhttps://github.com/diogenes/coletivo我试了一下。在MySQL上运行。Neo4jhttp://neo4j.org真的很容易实现一个“跟随谁”。事实上,大多数展示其能力的样本都涉及“跟随谁”。快速提示-只有在JRuby上运行时,Neo4j.rb才会很酷。如果不是-使用Neograph