使用内容分发网络的一点小经验

背景

近期公司连续承接了一些线上直播培训活动,直播资源我们是用的是腾讯云的云直播服务,直播期间,因为大部分的流量都是腾讯在抗,对我们的压力不是很大,而直播结束后的回放阶段,因为之前的经验,看回放的学员不是很多,所以为了节约成本,回放资源就是我们自己做好切片之后,放在我们自己的服务器上自己扛。

而这次由于学员人数激增,导致回放资源访问速度骤降,整个系统也濒临不可用的极限,经过讨论,决定开通CDN服务,缓解压力。

准备工作

开通服务

认准大厂就好,我们这边还是用的腾讯云,文档👉:https://cloud.tencent.com/product/cdn

这里要注意一下,大厂的CDN服务现在都有一个Pro版,各家叫法不一样(腾讯这边叫EdgeOne),主要是加强了安全性,可以更好的防止被盗刷流量,但价格普遍要比常规CDN贵不少。

实际上,常规CDN如果配置得当,也能做到防盗刷,这个就看公司实力了,不差钱,直接上Pro版就行。想勤俭持家,再搭配团队有一点使用经验和开发能力,常规版足够。

解析域名

开通CDN服务后,要想配置生效,还需要给我们的源站域名增加一个解析,一般都是cname类型。

解析完成后,云服务上的控制台会实时看到解析结果👇。

相关配置

前面的配置完成后,就可以配置一些其他的配置工作了,包括访问控制,缓存配置,https配置等;

这里我主要聊2个点,其他就根据各家云服务商提供的入口,自行配置即可。

访问控制

访问控制和安全相关,这里建议大家至少要把防盗链,访问频次配置上,而鉴权之类的高阶一点的安全配置,就看开发能力和实际情况了。

Http响应头配置

这个配置是影响浏览器行为的,我们的场景,因为回放资源都是以索引文件(m3u8)和分片文件(ts)提供,所以配置上CDN之后会在浏览器端有一个跨域问题,这个情况,就需要在控制台配置一下了;

注意事项

需要注意的一点是,要充分了解自己的使用场景,你要加速的资源是静态还是动态?是要进行全球加速,还是只在国内?加速的资源是大文件还是小文件等等。

还有就是,要多了解下各家平台的收费政策,一般来说,预付费的流量包还是比后付费的模式更加划算一点的。可如果你要加速的资源都是那种请求头过大的资源(mp4,flv,zip等),开通服务的时候,最好衡量一下选择哪种付费模式,因为这种资源一般流量消耗的都会非常快,如果我们不想修改自己的网站(比如让资源请求支持range参数,或者改成切片方式提供),那最好把计费模式改为按带宽计费,能最大限度避免出现流量刺客的现象。

说到这里,我个人建议我们线上网站上的视频类资源,最好是以分片的形式,或者flv搭配Range参数的方式提供,不要直接上大的mp4文件。而笔者之前也写过批量自动处理视频的一个博客(传送门👉:https://xie.infoq.cn/article/9a8abebacf858783c67166623)

至于具体怎么搭配,按实际情况来就好了,有时候踩踩坑才能记得住啊哈哈。

*项目代码

这点实际上也是可选的,我这里要修改一下代码,主要还是走的勤俭持家路线。我的一个资源如果开启了加速,那我不能一直让它加着,毕竟流量消耗在那摆着,所以过了需要加速的阶段,就要把它停掉。

所以我的代码主要是启停加速,支持自动启停和手动启停2种方式。

开启加速

 /// <summary>
 /// 加速视频
 /// </summary>
 /// <param name="liveId"></param>
 /// <param name="minutes">加速的分钟数,默认360分钟(6小时)</param>
 /// <returns></returns>
[HttpPost,ValidateAntiForgeryToken]
public async Task<IActionResult> TurboTs(string liveId,int minutes=360)
{
    var live = await _context.CourseLive.Where(u => u.LiveID == Guid.Parse(liveId)).FirstOrDefaultAsync();
    
    if (string.IsNullOrEmpty(live.FileAddress))
        return Json(_resp.error("加速失败,回放文件为空"));

    string originAddress = live.FileAddress.ToLower();
    string turboAddress = "";
    if (!originAddress.EndsWith("m3u8"))
        return Json(_resp.error("不能加速非HLS协议的回访文件"));
    if (originAddress.StartsWith("http") && originAddress.Contains(Common.ConfigurationHelper.GetSectionValue("turboHost")))
    {
        return Json(_resp.ret(1, "已加速"));
    }
    if (originAddress.StartsWith("http"))
    {
        return Json(_resp.error("不能加速外链文件"));
    }
    turboAddress = Common.ConfigurationHelper.GetSectionValue("turboHost") + originAddress;
    live.FileAddress = turboAddress;
    live.Updated_at = DateTime.Now;
    _context.CourseLive.Update(live);
    DateTime expireTime = DateTime.Now.AddMinutes(minutes);
    await _redisProvider.HashSetAsync("turboFiles", live.LiveID.ToString(), $"{expireTime}|{originAddress}|{turboAddress}");
    await _context.SaveChangesAsync();
    return Json(_resp.success(turboAddress));
}

手动关停

/// <summary>
/// 取消加速
/// </summary>
/// <param name="liveId"></param>
/// <returns></returns>
[HttpPost, ValidateAntiForgeryToken]
public async Task<IActionResult> CancleTurbo(string liveId)
{
    try
    {
        if (!await _redisProvider.HashExistsAsync("turboFiles", liveId))
        {
            return Json(_resp.error("视频未加速或加速时长已过期,无需取消"));
        }
        var live = await _context.CourseLive.Where(u => u.LiveID == Guid.Parse(liveId)).FirstOrDefaultAsync();
        if (string.IsNullOrEmpty(live.FileAddress))
        {
            return Json(_resp.error("无回放文件"));
        }
        live.FileAddress = live.FileAddress.ToLower().Replace(Common.ConfigurationHelper.GetSectionValue("turboHost"), "");
        live.Updated_at = DateTime.Now;
        await _redisProvider.HashDeleteAsync("turboFiles", liveId);
        _context.CourseLive.Update(live);
        await _context.SaveChangesAsync();
        return Json(_resp.success(live.FileAddress, "操作成功"));
    }
    catch (Exception ex)
    {
        return Json(_resp.error($"取消加速失败,{ex.Message}"));
    }
}

自动关停

 public async Task Invoke()
 {
     try
     {
         if (!await _redisProvider.KeyExistsAsync("turboFiles"))
         {
             NLogUtil.fileLogger.Warn("没有配置的加速文件");
             return;
         }
         StringBuilder stringBuilder = new StringBuilder();
         var turboFiles = await RedisConfigure.db.HashGetAllAsync("turboFiles");
         foreach (var item in turboFiles)
         {
             if (!item.Name.HasValue)
             {
                 continue;
             }
             Guid liveId = Guid.Parse(item.Name);
             var currLive = await _courseliveRepo.getOneAsync(u => u.LiveID == liveId);
             string[] parts = item.Value.ToString().Split('|');
             DateTime expireTime = DateTime.Parse(parts[0]);
             if (DateTime.Now < expireTime)
             {
                 continue;
             }
             currLive.FileAddress = parts[1];
             currLive.Updated_at = DateTime.Now;
             await _courseliveRepo.updateItemAsync(currLive);
             string msg = $"<p>课程【{currLive.Title}】加速结束(Id:{currLive.LiveID}),原始文件地址{parts[1]},加速地址{parts[2]}---{DateTime.Now};</p>";
             stringBuilder.Append(msg);
             NLogUtil.fileLogger.Warn(msg);
             Console.WriteLine(msg);
             await RedisConfigure.db.HashDeleteAsync("turboFiles", liveId.ToString());                    
         }
         if (stringBuilder.Length > 0 && DateTime.Now.Hour<22 && DateTime.Now.Hour>7) {
             await _emailKitHelper.SendEmailKitDevAsync("{被通知人邮箱}", "课程回放视频加速结束", stringBuilder.ToString());
         }
     }
     catch (Exception ex) {
         NLogUtil.fileLogger.Error("自动清理失败" + ex.Message);
     }
 }
//...注入其他服务
//...
services.AddTransient<AutoJobs.CacheHandle.ResetTurboVideo>();

//...启动中间件
//...
app.Services.UseScheduler(scheduler =>
{
        scheduler.OnWorker(nameof(ResetTurboVideo));
    scheduler
        .Schedule<ResetTurboVideo>()
        .Hourly()
        .PreventOverlapping(nameof(ResetTurboVideo))
    ;
});

自动关停的代码是一个计划任务类的功能,我这里使用的是Coravel,逻辑代码写完之后,要把计划任务注入到项目当中才会生效。

整个的启停加速逻辑就是,开启加速时,预估好要加速的时长,配置完成后,如果需要随时关停,那自行操作即可,如果没有自行操作,那到时间也会自己关停,来节约流量消耗。当然如果需要持续加速,那自动关停后,再次手动开启即可。

完成后的页面效果如下

加速效果

好了,接入工作完成后,可以看一下配置了加速的资源和没配置加速的资源访问速度对比

配置了加速

可以看到,配置完加速后,排除浏览器缓存的文件,访问加速后的,1M左右的分片文件,响应速度也是在毫秒级别,而且从请求头信息也可以看出,确实是命中了缓存,看视频丝滑流畅,加载几乎无延时;

没配置加速

而没配置加速的情况,同样访问1M左右的分片文件,响应时间是在秒级,注意,这是还是在负载不高的情况下,如果在高负载情况下,这个响应时长会更久,看视频卡顿也会非常明显。

加速监控

看一下加速监控到的数据情况👇,命中率基本99%以上,而且从流量曲线看,夜间和凌晨没有流量或者流量很低,而白天流量相对高,也能侧面反应出没有出现盗刷流量的情况。
在这里插入图片描述

缓存预热

当我们的接入工作验证生效以后,要根据情况进行预热和刷新。

我这里因为加速的视频都是小的分片文件,就需要及时的进行预热,避免负载上来以后,频繁的发送回源请求,这里腾讯云也提供了方便的预热接口,只需要把索引文件提交上去,然后勾选“预热TS分片”选项即可。

收尾啦

好了,至此,我们的项目就对接完成了。

最后,看一下这几天站点的监控数据,真的是不上CDN不行啦。

数据分析

此次我们自己ElasticSearch集群记录的请求日志在过去一周达到了千万级的规模,比其他项目的日志加起来还要多得多。

再来看下直播流量的消耗,一周内的规模达到了28T,带宽峰值也来到了13G,比我们以前承担的线上活动规模都大了几个数量级。

最后,我想说,接入一个CDN只能是分担一些静态资源的访问负载,除了静态资源,网站的动态资源响应能力也能考验系统并发能力,所以归根结底,还是考验的系统架构是否能弹性的支撑这种高并发的场景。

这里我还是提倡服务分层,不要怕引入一些中间件就造成系统复杂性提升而踌躇不前,以我这个案例来说,Redis中间层的引入,起了非常大的作用,如果没有缓存层承接流量洪峰,单靠几台硬件服务器,靠系统自己,靠单个的数据库实例来抗,那早崩溃了不知道多少回了。(说到这里,笔者曾经写过一篇关于Redis集群部署的博客,传送门👉:https://xie.infoq.cn/article/97570be6f0bff9084a6c6e938

这种高并发场景下,系统的表现能力,也能反应开发者系统架构的设计能力,我们应该不停的突破,去更加关注系统的质量属性。

好了,就这些吧,最后没头没尾的说了一堆,哈哈。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

为自己_带盐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值