考虑到项目中本身微服务较多且依赖较多基础服务,实际运行时服务总量在25+,如果没有一个自动化代码构建方式的话,仅依靠手动运行,步骤繁琐且不能很好的控制代码质量。
除了借助GitLabRunner进行持续集成外,如果需要完整在本地运行项目,还采用DockerCompose进行容器编排,
DockerCompose会根据配置快速完成镜像构建,并使用预先设置的配置进行构建容器运行,方便测试。
为了方便部署,提供了DockerCompose一键部署脚本,下面是通过DockerCompose一键启动所有服务流程:
下图为启动所有服务后docker运行状态:
前端使用Vue,后端使用go-zero作为微服务框架,包括API层和RPC层。API层与前端交互,提供功能中间件。RPC层实现业务逻辑,使用Consul进行服务注册和发现。存储方面,使用MySQL持久化、Redis作为缓存、Elasticsearch为搜索引擎和Kafka作为消息队列。七牛云提供视频存储和音视频分析。算法支持包括推荐算法和语言大模型。服务可观测性通过链路追踪和服务监控实现,可在Grafana展示。
以七牛云为存储服务器,基于Gorse推荐系统给用户推视频。
视频摘要总结Prompt:
您将获得一段视频内容的文本,您的任务是给出2个简体中文句子来总结视频。下面是视频文本内容:视频关键词提取Prompt:
您将获得一段视频内容的文本,您的任务是从下面可选的标签列表中为视频选择5个简体中文标签,以吸引观众。请直接输出这5个标签,下面是可选的标签的列表:美食|动漫|游戏|网络小说|科学|编程|文学|化学|计算机|哲学|经济学|政治|法律|艺术|健康|音乐|影视|综艺|直播|明星|搞笑|脱口秀|魔术|舞蹈|相声|运动下面是视频文本内容:视频摘要及关键词提取实例返回:
{"status":200,"message":"success","timestamp":"2023-11-0414:01:45","data":{"keywords":["搞笑","脱口秀"],"summary":"1.专家指出烧节盖烟太黑会污染空气,于是主人公用大塑料袋装起来,展示了聪明的一面。\n2.主人公用塑料袋装满了烧节盖烟,但面临如何处理的问题,引发了思考。","text":"专家说烧节盖烟太黑了容易污染空气于是我就找了个大塑料袋把烟给装起来了嘿嘿还是我聪明啊哎呀还是你小子机灵呢不过你这塑料袋装满了之后扔哪啊这塑料袋小红书","duration":15.666667}}4.3.3基于语音识别模型的视频文字提取基于Whisper模型进行视频文字提取,通过Whisper模型把视频中的音频转化成文字,供视频摘要及关键词提取。
下图为系统中视频在Gorse推荐系统中收到的反馈
在视频流的推送方面,我们避免向用户推送已经观看过的视频,从而提供更加个性化的推荐体验。每个用户的视频推荐结果都基于其过去的浏览偏好,从而确保推荐内容与用户的兴趣和偏好相匹配。
当用户查询直播列表时,将调用七牛云的StreamLists接口,获取正在活跃的直播流,并呈现给用户。
man:=pili.ManagerConfig{ AccessKey:l.svcCtx.Config.AccessKey, SecretKey:l.svcCtx.Config.SecretKey, Transport:nil, } manger:=pili.NewManager(man) list,err:=manger.GetStreamsList(l.ctx,pili.GetStreamListRequest{ Hub:"scooter", LiveOnly:true, }) iferr!=nil{ returnnil,err }在后台,我们也可以对所有的直播进行实时流量与人数监控。
搜索功能在后端业务中扮演着至关重要的角色。能够帮助用户快速查找和过滤数据,提供高效的数据检索,保持实时更新和即时性,满足复杂的查询需求,并最终提升用户体验。无论是处理大量数据还是提供个性化的数据访问,搜索功能都是不可或缺的工具之一。
当用户发布视频信息或语言模型算法为视频生成分类标签时,这些操作会触发对数据库的操作。对于更新或创建操作,会将相应的变更写入数据库的binlog中。为了保证搜索引擎能够获取到最新的数据并提供给用户搜索,我们采用Elasticsearch作为存储搜索视频的关键信息的解决方案。
为了实现数据的同步更新,我们引入了canal来监听数据库的binlog变化。canal会将产生的变更消息传递到Kafka消息队列中。然后,我们使用Kafka消费者来消费这些数据,并将其更新到Elasticsearch中。
这么设计的优点在于:首先,使用binlog可以捕捉到数据库的每个变更操作,包括更新和创建操作,确保了数据的完整性和一致性。其次,通过使用Kafka作为中间消息队列,可以实现高效的异步数据传输和解耦。最重要的是,将数据更新到Elasticsearch中,使搜索引擎能够及时获取到最新的数据,提供给用户快速而准确的搜索结果。
通过这种实现我们能够保证搜索引擎与数据库之间的数据同步,并提供可靠的搜索功能。无论是用户发布新的视频信息还是语言模型算法生成新的分类标签,都能够迅速反映在搜索引擎中,为用户提供最新、全面的搜索体验。
服务解耦和流量削峰是指将一个系统中的服务进行解耦,使其能够独立运行和扩展,并通过一些策略来平衡系统的负载,减少系统的压力和资源消耗。这两个概念的出现是为了应对现代高流量和高并发的网络环境下,系统面临的挑战和需求。
从后端架构设计出发,我们的后端采用Go-Zero框架,它是一个基于Go语言的微服务框架,它提供了一种服务解耦的设计思路和实现方式。通过Go-Zero,我们可以将一个复杂的系统拆分成多个独立的微服务,每个微服务都有自己的职责和功能,并且可以独立部署和扩展。Go-Zero采用了领域驱动设计(DDD)的思想,将业务逻辑划分为多个领域模型,每个领域模型对应一个微服务。这种方式使得不同的团队可以独立开发和维护各个微服务,减少了团队之间的依赖和协调成本。同时,Go-Zero提供了一套完善的服务注册和发现机制,使得各个微服务可以方便地进行通信和协作。
同时,在视频上传层面,通过在用户视频上传路径设置消息队列Kafka,实现了服务解耦和削峰的效果,后面我们会详细介绍这一过程。服务解耦将上传服务和视频处理服务解耦开发,提高了系统的可维护性和可扩展性。削峰则通过异步处理视频任务,避免了服务器过载,确保系统的稳定性和性能。
下图展示了Prometheus与Grafana的接入,通过该接入可以实现对各个服务的指标和性能情况进行监控和可视化。
当服务启动时,可以将其配置为被Prometheus监听。通过在服务中集成Prometheus客户端库,服务可以向Prometheus暴露自身的监控指标。这些指标可以是关于服务的性能、资源利用情况、错误率等方面的信息。
微服务架构中,由于调用链可能涉及多个环节,例如从HTTP到RPC,再从RPC到HTTP,开发人员通常需要了解每个环节的调用情况和性能。为了实现这一目的,我们采取的方案是全链路跟踪。go-zero集成了强大的链路追踪功能,其实现如下所示:
全链路跟踪的方法是在请求开始时生成一个唯一的SpanID,并随着整个请求链路传递。通过使用这个SpanID,开发人员可以查看整个链路的情况和性能问题,从而更好地分析和优化系统性能。
同时,我们也为go-zero继承ORM框架Gorm,这一环节也接入到了go-zero的链路追踪:
iferr:=db.Callback().Create().Before("gorm:createBefore").Register("gorm:createBefore:metric:trace",func(db*gorm.DB){ctx:=db.Statement.Contexttracer:=trace.TracerFromContext(ctx)_,span:=tracer.Start(ctx,"gorm:create",oteltrace.WithSpanKind(oteltrace.SpanKindClient),)db.InstanceSet("gorm:create_span",span)});err!=nil{returnerr}iferr:=db.Callback().Create().After("gorm:createAfter").Register("gorm:createAfter:metric:trace",func(db*gorm.DB){v,ok:=db.InstanceGet("gorm:create_span")if!ok{return}span:=v.(oteltrace.Span)ifdb.Statement.Error!=nil{span.RecordError(db.Statement.Error)}span.SetAttributes(semconv.DBSQLTableKey.String(db.Statement.Table),semconv.DBStatementKey.String(db.Statement.SQL.String()),)span.End()});err!=nil{returnerr}上述代码在使用GORM框架时集成Go-Zero链路追踪功能。通过注册两个Hook函数,在数据库创建操作的前后创建和记录跟踪Span,以便监控和分析数据库操作的性能和执行情况。
我们通过将Jaeger链路追踪与Go-Zero框架集成,能够实时监视系统请求和服务调用的链路,从而更好地实时监控、故障排查和性能优化。
视频截帧功能使得我们的系统能够截取视频的一帧作为视频封面,提供吸引人的预览图像。为用户呈现有吸引力和信息性的视频内容。这种方式可以增强用户对视频的兴趣和理解,提升用户浏览和选择视频的效率,为用户带来更好的使用体验。
在上传视频时,我们调用了七牛云的vframe/jpg接口截取视频的第一秒画面生成视频的封面,为用户提供更具吸引力和信息性的预览图像。这项功能允许我们从视频中提取并展示最有代表性的画面,以便在视频列表和播放器中展示。通过单帧截取,我们能够捕捉到视频的关键内容,并为用户提供一个直观的视觉引导。
在上传视频时,我们将视频的信息传送到Kafka消息队列中。后台运行的消费者会订阅这些消息,并调用七牛云的/v3/video/censor接口对视频进行审核。如果视频通过审核,系统将保留该视频并进行后续处理;如果视频未通过审核,系统将调用删除视频的RPC服务,将其从系统中移除。这个审核流程具有多个优势。首先,通过将视频信息发送到Kafka,我们实现了异步处理,提高了系统的并发性和响应能力。其次,利用七牛云的审核接口,我们能够自动化地对视频内容进行审核,减轻了人工审核的工作量,并提高了审核的准确性和效率。
视频转码技术用于将视频文件从一种格式转换为另一种格式。它可以提高视频的兼容性,使其能够在不同的设备和平台上播放。其次,视频转码可以减小文件大小,节省存储空间和带宽。我们能够对上传的音频文件进行高效的mp4转码,并在上传视频文件时自动触发。
在上传视频时,会进行视频转码并异步生成带水印的视频。在用户上传视频后,系统会调用七牛云的接口异步生成一份带有我们团队logo的水印视频。这种异步生成的方式不会对用户的上传体验造成延迟,保证了系统的高效性和响应性。通过异步生成水印视频,我们能够充分利用七牛云的强大处理能力,提供快速且高质量的水印视频生成。用户无需等待水印处理完成,即可继续进行其他操作,提高了用户上传的效率和流畅度。
写策略的步骤:首先更新数据库中的数据,然后删除缓存中相应的数据。读策略的步骤:如果所需数据已经缓存在内存中,则直接返回缓存中的数据;如果所需数据未缓存,则从数据库中获取数据,将其写入缓存,并将数据返回给用户。缓存可以将经常访问的数据存储在快速访问的介质中(Redis数据库),以实现快速的数据检索和返回,从而显著提高性能和响应速度。同时可以减轻后端数据库的负载,将大量的访问请求转移到缓存中处理,从而降低了后端数据处理的压力。
您将获得一段文本,您的任务是判断文本的情感倾向,正向文本请返回正向,负向文本请返回负向。请直接输出情感倾向;下面是文本内容:4.3.16错误码设计采用了一种统一的方式来处理返回的HTTP状态码,始终返回200OK。而业务错误和服务内部错误则通过自定义的错误码来进行区分。同时,我们保持了API服务和RPC服务之间的业务错误码的一致性。如果在RPC服务中定义了一个业务错误码,它可以透传到API服务,并最终通过接口返回给客户端。
当发生错误时,仍然会返回JSON格式的数据,其中code字段表示业务自定义的错误码,message字段提供了详细的描述信息。客户端在接收到这些数据后,可以根据业务错误码进行逻辑处理,并可以将message信息用于错误弹窗显示。
在上图所示的流程中,业务API和服务RPC之间包含两个拦截器。靠近RPC的是服务端拦截器,其功能是将业务自定义的错误转换为gRPC错误,并将详细的错误信息封装在消息体的detail字段中。然后,客户端拦截器从gRPC错误中提取消息体的detail,将其中的错误信息转换为业务自定义的错误码,并返回自定义的错误响应。这样便实现了将业务自定义错误码在服务端和客户端之间进行转换的机制。
在RPC服务中,当我们遇到数据库查询错误或者其他意料之外的错误时,我们可以在RPC里面直接返回错误,交给API层处理。
同时,可以自定义一些错误码,当遇到意料之内的错误,比如不能够重复点赞,RPC层可以根据自定义错误码将错误返回给API层。
var( FavoriteUserIdEmptyError=xcode.New(30001,"点赞用户id为空") FavoriteVideoIdEmptyError=xcode.New(30002,"点赞视频id为空") FavoriteServiceDuplicateError=xcode.New(30003,"不能重复点赞") FavoriteServiceCancelError=xcode.New(30004,"点赞记录不存在,无法取消点赞") FavoriteInvalidActionTypeError=xcode.New(30005,"无效的点赞操作") FavoriteLimitError=xcode.New(30006,"点赞频繁,请稍后再试!"))当RPC的错误返回到API层,API层的错误处理就会变得很简单。我们在API层自定义了错误处理函数。
funcSetErrorHandler(handlerfunc(error)(int,any)){ errorLock.Lock() defererrorLock.Unlock() errorHandler=func(_context.Context,errerror)(int,any){ returnhandler(err) }}API层处理RPC服务的错误:
通过这种方式,实现了简洁、明确的错误处理机制,并同时保持了错误码在API服务和RPC服务之间的一致性。这样,客户端可以根据返回的数据准确地处理业务逻辑,并根据需要展示错误信息给用户。
具体实现采用了基于Redis的令牌桶算法。每个用户在每个业务上每秒钟只允许执行3次操作。当用户发送请求时,系统会从Redis中获取一个令牌,如果成功获取到令牌,表示用户未超过限制,请求将被处理;否则,说明用户的操作频率超过了限制,系统将拒绝此次访问。
limiter:=redis_rate.NewLimiter(l.svcCtx.RedisClient) limiterKey:=strconv.FormatInt(userId,10)+FavoriteLimitKey limiterRes,err:=limiter.Allow(l.ctx,limiterKey,redis_rate.PerSecond(FavorActionMaxQPS)) iferr!=nil{ l.Logger.Errorf("[favoritelimiter]err",err) } iflimiterRes.Allowed==0{ l.Logger.Errorf("[favoritelimiter]err",err) returnnil,code.FavoriteLimitError }4.3.18Mock模拟数据Mock模拟数据使得开发和测试团队能够并行工作,而无需等待真实数据的准备。开发人员可以使用模拟数据来构建和调试前端或后端功能,而测试人员可以使用模拟数据进行测试用例的编写和执行,从而实现快速构建原型和演示版本,加快开发迭代的速度。
在开发阶段,我们充分利用vite-plugin-mock插件,通过自定义数据模板以生成随机化数据,并且通过接口请求的方式注入到前端应用程序中。在开发过程中,前端通过接口请求获取模拟数据,模拟实际接口的返回结果,从而在前端开发过程中能够独立高效地进行工作。
下图展示了利用Mock随机数据生成的页面内容。
由于各个分类的视频页面包含大量媒体内容,我们将各个分类视频的图片通过懒加载载入,加快初始页面加载速度。我们通过LazyLoad来具体实现,将图片标记为懒加载,并且监听滚动事件,判断图片是否进入可视区域,当图片进入可视区域时,触发加载图片的操作,代码如下:
可能有读者疑惑为什么至少要三个实例,实际上如果两个节点之间的通信中断,每个节点可能会认为另一个节点已经失败,并试图接管服务,这样会导致“脑裂”(split-brain)现象,即两个节点同时认为自己是主节点,可能会导致数据不一致或服务中断。三个或更多节点的集群可以通过多数投票机制来避免这个问题。
InnoDB集群使用了以下MySQL技术:
这是一个为MySQL设计的高级客户端和代码编辑器,可以说是dba的神器。启动很简单,mysqlshxxx就能连上,如下图。
mysql-js>dba.help('getCluster')RetrievesaclusterfromtheMetadataStore.SYNTAXdba.getCluster([name][,options])WHEREname:Parametertospecifythenameoftheclustertobereturned.options:Dictionarywithadditionaloptions....这段代码是关于如何在MySQL数据库中使用JavaScript命令行接口(mysql-js)来获取一个集群的信息。简单来说,这段代码提供了一个命令dba.getCluster,使用这个命令从MySQL的元数据存储中获取一个集群。
这里有几个关键点:
dba.getCluster()是你要执行的命令。
这个命令可以带两个参数:
name(可选):这个参数用来指定你想要获取哪个集群的信息。如果你不提供这个参数,可能就会返回默认的或者所有集群的信息。
options(可选):这是一个字典(就像一个键值对的集合),你可以在这里面放入一些额外的选项,来告诉命令如何执行。比如,你可以设置一些特殊的配置或者选择返回某些特定的信息。
当你输入这个命令并且提供必要的参数后,MySQL的命令行接口就会去查找并且返回你请求的集群的信息。
后者使得一组MySQL实例能够提供高可用性。InnoDB集群提供了一种替代的、易于使用的程序化方式来使用GroupReplication。
我们用MySQLShell来实现集群的复制。
这是一个轻量级中间件,它为您的应用程序和InnoDB集群之间的透明路由提供支持,如下图。
我们来看看如何操作MySQLrouter
首先配置MySQLrouter
#启动/home/cluster/mysql-router/start.shPID16819writtento/home/cluster/mysql-router/mysqlrouter.pid#默认通过route连接mysql后,23306端口连接后可以进行读写操作.23307端口连接后只能进行只读操作.$netstat-nltp|grep2330tcp000.0.0.0:233060.0.0.0:*LISTENoff(0.00/0/0)tcp000.0.0.0:233070.0.0.0:*LISTENoff(0.00/0/0)tcp000.0.0.0:233080.0.0.0:*LISTENoff(0.00/0/0)tcp000.0.0.0:233090.0.0.0:*LISTENoff(0.00/0/0)这样就可以使用MySQL客户端连接router了.下面验证下连接router:
#管理节点本机mysql-shell连接:[cluster@node-1~]$mysqlsh--uriroot@node-1:23306#管理节点本机mysql连接:[cluster@node-2~]$mysql-uroot-hnode-1-P23306-p#远程客户机通过route连接mysql[root@db-node01~]#mysql-uroot-h127.0.0.1-P23306-p小结下图展示了这些技术是如何协同工作的:
port7000cluster-enabledyescluster-config-filenodes.confcluster-node-timeout5000appendonlyyes我们创建6个文件夹,把配置文件复制到每个文件夹内
mkdircluster-testcdcluster-testmkdir700070017002700370047005cd7000redis-server./redis.conf[82462]26Nov11:56:55.329*Noclusterconfigurationfound,I'm97a3a64667477371c4479320d683e4c8db5858b1接着使用命令create,创建一个新集群。该选项意味着我们希望为创建的每个主节点提供一个副本。--cluster-replicas1
其他参数是实例的IP地址列表。
redis-cli--clustercreate127.0.0.1:7000127.0.0.1:7001\127.0.0.1:7002127.0.0.1:7003127.0.0.1:7004127.0.0.1:7005\--cluster-replicas1