背景
近期公司连续承接了一些线上直播培训活动,直播资源我们是用的是腾讯云的云直播服务,直播期间,因为大部分的流量都是腾讯在抗,对我们的压力不是很大,而直播结束后的回放阶段,因为之前的经验,看回放的学员不是很多,所以为了节约成本,回放资源就是我们自己做好切片之后,放在我们自己的服务器上自己扛。
而这次由于学员人数激增,导致回放资源访问速度骤降,整个系统也濒临不可用的极限,经过讨论,决定开通 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。而笔者之前也写过批量自动处理视频的一个博客(传送门👉:)
至于具体怎么搭配,按实际情况来就好了,有时候踩踩坑才能记得住啊哈哈。
*项目代码
这点实际上也是可选的,我这里要修改一下代码,主要还是走的勤俭持家路线。我的一个资源如果开启了加速,那我不能一直让它加着,毕竟流量消耗在那摆着,所以过了需要加速的阶段,就要把它停掉。
所以我的代码主要是启停加速,支持自动启停和手动启停 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 左右的分片文件,响应时间是在秒级,注意,这是还是在负载不高的情况下,如果在高负载情况下,这个响应时长会更久,看视频卡顿也会非常明显。
缓存预热
当我们的接入工作验证生效以后,要根据情况进行预热和刷新。
我这里因为加速的视频都是小的分片文件,就需要及时的进行预热,避免负载上来以后,频繁的发送回源请求,这里腾讯云也提供了方便的预热接口,只需要把索引文件提交上去,然后勾选“预热 TS 分片”选项即可。
收尾啦
好了,至此,我们的项目就对接完成了。
最后,看一下这几天站点的监控数据,真的是不上 CDN 不行啦。
数据分析
此次我们自己 ElasticSearch 集群记录的请求日志在过去一周达到了千万级的规模,比其他项目的日志加起来还要多得多。
再来看下直播流量的消耗,一周内的规模达到了 28T,带宽峰值也来到了 13G,比我们以前承担的线上活动规模都大了几个数量级。
最后,我想说,接入一个 CDN 只能是分担一些静态资源的访问负载,除了静态资源,网站的动态资源响应能力也能考验系统并发能力,所以归根结底,还是考验的系统架构是否能弹性的支撑这种高并发的场景。
这里我还是提倡服务分层,不要怕引入一些中间件就造成系统复杂性提升而踌躇不前,以我这个案例来说,Redis 中间层的引入,起了非常大的作用,如果没有缓存层承接流量洪峰,单靠几台硬件服务器,靠系统自己,靠单个的数据库实例来抗,那早崩溃了不知道多少回了。(说到这里,笔者曾经写过一篇关于 Redis 集群部署的博客,传送门👉:https://xie.infoq.cn/article/97570be6f0bff9084a6c6e938)
这种高并发场景下,系统的表现能力,也能反应开发者系统架构的设计能力,我们应该不停的突破,去更加关注系统的质量属性。
好了,就这些吧,最后没头没尾的说了一堆,哈哈。
评论