石器时代1 📅 2026/7/2 2:54:57 整个系统的框架如下整个系统由客户端 web服务器 数据库 文件存储服务器构成。文件服务器使用的是亚马逊的S3对于小公司来说选择亚马逊比自建存储的成本要低得多。我们要求系统要尽可能及时的上传报警视频。一个报警视频大概录制30s及时意味着报警一旦触发就要开始上传而不是等报警视频录制结束了再上传录制下来的报警文件。而且在有些设备上如摄像头是可以没有存储卡的但是也得能上传所以选择上传报警视频文件的方式就不可取了。而在s3服务使用的是http协议上传文件必须在上传文件之前告诉服务器文件的大小即http头里面的content-length信息。为了解决这个问题我们使用了分片上传的方式。就是首先根据视频的分辨率大小计算出一个文件size这个大约能存储10s左右的视频。在上传过程中计算已经上传的数据量大小当一个分片存储满之后再开始另一个分片。在最后一个分片时可能报警视频已经录制结束了但是分片还没存满这时候就用空数据填充。当然空数据的位置也得记录下来这样观看端在播放时就不至于把空数据当作正常数据导致播放失败。除了正常的视频数据在每段报警视频的最后还得记录视频中的I帧位置信息主要是用于在播放时拖动寻找位置信息。这一点是参考mp4文件的录制方式由于我们使用的并不是标准的mp4格式所以在上传视频的过程中得将I帧的位置信息记录下来待整个视频上传结束后将位置信息存储在视频的尾部最后不足一个分片的部分再用空数据填上。整个采集端来说上传文件到亚马逊S3的过程就是如此那么跟web服务器又是怎么交互的呢第一步采集端在触发了一个报警时要向web服务器申请一个EVENTID作为这个报警事件的唯一标识在之后上传文件都跟这个EVENTID绑定。观看端在播放时根据这个EVENTID查到它对应的视频文件然后去亚马逊S3上下载播放。第二步当采集端向亚马逊上传一个分片文件时需要生成一个uri然后才能向这个uri PUT数据。uri的生成采集端可以直接向亚马逊申请但是考虑到申请uri需要携带亚马逊的账户秘钥放在客户端做不安全所以申请uri还是放在web服务器上。当采集端需要上传文件向web服务器去申请。每次采集端申请uri时带上EVENTID以及一个分片index即告诉web服务器你要申请的是哪个eventid的第一个分片。生成的uri格式如下http://xxxxxxxxxxxxxxxxxxxxxxxx/eventid/index.avi。前面的xxxx表示你在 s3上面创建的存储桶index即是第几个文件 avi是文件的后缀名这里是一个假设叫什么都可以。每开始一个新的分片index自动加1这样在只需要记录一个最终的index即可。下载时根据最终的index大小就可以把所有的文件都下载下来。当申请到uri之后采集端就可以通过http协议向这个uri上传数据了。第三步在每个uri上传结束之后向web服务器report一次 event信息。这个event信息即是第一步开始时申请的eventid。汇报的信息包括这个event 的触发时间类型视频时长视频分辨率音频的采样率以及index。可以看到每个uri上传结束都汇报一次的信息其实也只有index的值不同其他的值都一样。本来是可以等到在一个视频完全上传结束之后一次性汇报一次event信息就OK了。但是考虑到当一个视频正在上传的过程中采集端软件crash了或者小偷进来后里面将监控设备砸了所以要每上传一个分片都要汇报一次。这样观看端查看时就可以看到一个未完成的视频了。除了这点外也要注意到可能一个分片都没上传上去就发生意外所以我们在每次报警一触发就立即抓一幅图片上传到S3上。上面基本就是整个系统上传部分的流程。web服务器负责生成eventid, 申请uri以及写数据库。数据库只要存储一张event表项就可以了表项里面记录了这个event 的详细信息。在2.0版本中虽然使用了redis缓存用来降低mysql的访问压力但是缓存的使用很简单仅仅存储了一个采集端每天的event个数。这样观看端查询时可以一次性获取到最近30天每天的event个数。因为我们只给用户保留最近30天的数据在redis上做了个数统计就不用再去数据库读表统计了。接下来再说说观看端的查询流程首先就是去查询采集端最近一个月每天的event个数。然后再具体查看某一天的报警时带上日期起 始时间段去服务器查询event列表。在返回结果之后将event信息作本地缓存。如果下次再查询先查看本地缓存中是否存在如果有就直接返回。最后根据web服务器返回的event信息包括了这个event对应着亚马逊服务器上的uri通过uri下载视频数据播放。同时也将视频数据缓存到本地文件中供下次查看时使用。3.0 青铜时代2.0版本完成了0到1 的跨越但是整个系统与服务还处于初级阶段。在刚上线之后就开始了3.0的开发工作。3.0版本的主要目的是完成视频数据与事件的分离。在2.0 版本中我们以事件为单位向AWS 上传文件这种业务模型有着一定局限性文件数据强依赖事件。理想的状态应该是文件数据应该是一个整体而不应该按照事件来划分。事件只需要记录其对应的文件数据即可。对于一个事件我们只需要在数据库保存它的一些基本信息(比如时间类型等等)然后记录下这个事件对应的数据在云端的位置。这样做有两个好处1 数据与事件解耦云端存储的只是一堆文件易于维护2 数据可以复用比如两个事件发生的时间有重叠在2.0版本重叠的数据就要上传两次浪费了存储空间如图所示我们在上传本地数据文件时依然使用分片方式上传。每读取一帧数据判断一下数据的时间戳有没有到达事件的开始时间。如果到达那么就向web服务器汇报一次事件信息并且记录下这个事件的开始在该分片文件中所处的位置。同样判断当前正在处理的事件比较时间戳是否已经达到结束时间。如果已经结束同样记录一个结束位置。一个分片文件可能对应多个event有些event在这个分片文件的某个地方开始有些event在这个分片文件的某个地方结束还有些event可能占有整个分片文件。当一个分片文件上传结束时需要向web服务器汇报分片文件信息包括一些基本信息大小媒体参数以及文件的uri等以及分片文件与event的映射关系即event的位置信息。在数据库的设计中event存储一个表项分片文件存储一个表项映射关系存储一个表项。关系如下图所示在event与file的映射表项中存储了event与file id以及这个event的开始位于file的位置(start_pos)以及结束位置file中的位置(end_pos)。如果这个event不在这个file中开始也不在这个file中结束那么说明这个file处于这个event的中间既不是第一个分片也不是最后一个分片那么start_pos就是0end_pos就是分片文件大小即分片的结束。index就是这个分片文件是该event的第几个分片文件。当我们观看某个云视频时只需要在数据库中按照event进行查找即可以返回这个event的所有分片文件。观看端拿到这些分片文件信息去亚马逊S3下载就行播放。对于数据库的影响2.0版本中对于一个event在上传一个分片文件之后就要向web服务器汇报一次。web服务器判断该event是否是第一次汇报如果是在数据库插入一行新的表项如果不是则要更新之前插入的表项3.0版本中分片文件每次汇报只需要插入表项即可没有更新操作。event信息在开始的时候汇报一次在结束的时候需要更新一次。整体来说3.0版本中减少了数据库的update操作。搞过数据库的人都知道更新操作比插入对数据库的消耗大得多从某种意义上来说也变相减轻了数据库的负载。在3.0版本中我们修改了redis的使用策略。2.0版本仅仅用redis来统计每天的event数量但是其实在查询的时候我们并不需要关心有多个数量。移动端查询时是按业来查询的每次查询10个每次向下翻页就再查询10个无法再翻页时就说明已经查询出当天所有数据了。为了提高查询性能我们将event的信息存储在redis里面。包括event 的触发时间时长icon信息。按照日期cid(采集端的id唯一标识)type(event类型)作为key, value是一个list类型的值保存当天所有的event id信息。然后再用eventid作key, value保存event的详细信息。这样在查询时先按照cid日期类型找到列表key从里面读取一页的数据。然后再根据这一页的数据去查询里面每个event的详细信息。这样在查询列表时就不要再访问数据库了。浓缩视频压倒数据库的最后一根稻草3.0版本上线三个月之后系统运行的还算良好但是我们发现数据库表项在飞速膨胀。我们的云服务用户已经有几万个每个采集端每天平均都要上传几十条视频所以按照这种速度单表记录很快就来到了将近1000w。在mysql上1000万几乎就是单表记录上限了。搞web的兄弟发现这一趋势后做了分表方案。按照采集端的cid尾数 即(0-9)将eventfile以及映射表分成了10张表。虽然是解决了存储方面的问题但是随着使用云服务的用户在不断增加数据库的访问压力也在渐增。在3.0版本我们新增了浓缩视频功能就是将一天中的视频变化压缩成很短的几分钟。由于短视频每天才产生一个所以我们在当天录制完之后第二天的0点之后开始上传前一天产生的浓缩视频。这个功能在3.0版本上运行了一段时间刚开始没有问题。但是在不知不觉中却为自己刨了一个大坑。那段时间运营部门搞促销活动用户登录送积分用积分赠送云服务。突然有一天测试人员早上过来后发现前一天的浓缩视频没有上传翻开采集端日志一看在凌晨0点之后那段时间所有的web请求全部失败了。让运维同学查看了下凌晨那段时间发生了啥一看惊呆了在0点0分0秒那一刻瞬间涌入了上万的请求。web服务器还好有负载均衡但是数据库只有一台1s之内成千上万的请求数据库不死才怪。由于在采集端做了失败重试请求失败之后又会接着再次请求数据库几乎一直在卧倒状态。幸好的是采集端做了重试次数限制所以基本在凌晨1点之后请求数也就慢慢降下来了。而这一切都是由于浓缩视频集中在凌晨那段时间上传导致的。做促销活动的那几天每天都会送出1w多的云服务一下子就把数据库压垮了。其实解决这个问题的方法很简单对于浓缩视频来说我们只要保证上传了就可以没必要非得全部挤在0点这个时间。我们把上传的时间随机延长至0~5点之间任何一个时间点保证用户在早上起来后能查看到即可。很快就出了更新版本服务器的访问压力随即降了下来服务也回归正常。但是还是有一种隐约的不安因为用户还在快速增长不知道哪一天服务器又会遇到类似的问题。4.0 火炮时代3.0版本告一段落之后随即开始了4.0版本的规划。4.0版本主要要解决的就是服务器的访问压力包括web服务器以及数据库。主要的性能瓶颈还在数据库上 web服务器作水平扩容很简单因为在web服务器前面有nginx作为接入层做负载均衡新增一台web服务器直接在nginx上加个配置就行了。但是数据库因为还没有做分库所以只能先优化单台数据库的性能。使用Innodb引擎写性能每秒几百个还能再撑一段时间。运行云存储服务的采集端大约有几万台每秒钟的并发请求量还没那么大。但是数据量增长太快却是一个问题虽然已经按照采集端的cid做了分表但是表项的数据按照现在的增长速度很快又会到千万。分表也不可能这样无限制的做下去但是分表策略却是可以调整的。其实我们的云服务有一个特点就是数据只保存30天查询的时候也是按天来查询所以优先应该选择按天来分表才对。30天过后直接删除掉老的表项这样数据就不会无限量的膨胀。每天建一张表数据量也不会达到单表上限。仅仅是这样实现一下其实也不复杂但是考虑到版本兼容就没那么简单了。数据库还是只有一台用户如果还是使用3.0的版本我们也得按照新的分表方式来写表。这样就带来一个问题即按时间分表到底是按照event的触发时间来分表还是按照event的上传时间来分表这到底有什么区别呢。一般情况下采集端在触发报警时要立马上传视频。但是如果当时断网了我们也会缓存在本地等到网络恢复了再上传。所以有可能在当天触发的报警视频在第二天才能上传也有可能更晚。刚开始想按照event的上传时间来做分表这样做只要在服务器端判断下当前时间将请求直接插入到对应日期的表项中就行了。但是这种做法查询性能就比较差了。查询的时候按日期查询