news 2026/10/12 1:28:20

UE5延迟渲染管线源码解析:从GBuffer到后处理的数据流与实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
UE5延迟渲染管线源码解析:从GBuffer到后处理的数据流与实战

1. 为什么值得花时间啃渲染管线源码

很多人第一次打开UE5的渲染模块源码,看到满屏的FSceneRenderer、FDeferredShadingSceneRenderer、FRDGBuilder,第一反应是关掉。我完全理解,因为我自己第一次也是这么干的。但如果你真的想搞清楚"为什么我的场景在某个角度突然变暗""为什么开了Lumen之后帧率掉了一半""为什么后处理材质在移动端表现和PC完全不一样",光靠调参数是永远调不明白的。你必须知道引擎在每一帧里到底做了什么,数据是怎么从场景描述一步步变成屏幕上那些像素的。

这篇是"UE5渲染管线源码解析"系列的第二篇,第一篇我们聊了整体框架和FSceneRenderer的调度逻辑,这一篇往深里走,重点拆解延迟渲染路径下从GBuffer到光照合成再到后处理的完整数据流。涉及的核心模块包括FDeferredShadingSceneRenderer::Render、FRDGBuilder的资源管理机制、GBuffer的布局策略、光照通道的Pass组织方式,以及后处理链的插入点。这些内容对于做渲染向TA、引擎工具开发、性能优化、自定义渲染Feature的工程师来说,是绕不过去的基本功。

我写这个系列的出发点很简单:官方文档对渲染管线的描述停留在概念层面,论坛里的帖子大多是片段式的,真正把源码逻辑串起来讲清楚的内容非常少。我自己在项目里踩过的坑、调试过的Bug、做过的自定义改动,都逼着我把这些代码翻了很多遍。所以这个系列不是翻译注释,而是把我实际理解到的管线运作逻辑,用能看懂的方式讲出来。适合有一定C++基础、用过UE渲染模块、想深入理解引擎内部机制的读者。如果你连FRDGBuilder是什么都还没概念,建议先去看第一篇,或者至少把RDG的基础用法过一遍。

2. 延迟渲染路径的整体数据流拆解

2.1 从FSceneRenderer到DeferredShading的调用链

UE5的渲染入口在FSceneRenderer::Render,这是一个静态工厂方法,根据当前平台和配置决定创建哪种渲染器实例。PC端默认走FDeferredShadingSceneRenderer,移动端前向渲染走FMobileSceneRenderer,还有光追路径的变体。我们这里只聊延迟渲染这条线。

调用链大致是这样的:FSceneRenderer::Render创建实例后,调用RenderViewFamily,里面会依次执行BeginRenderViewFamily、PreVisibilityFrameSetup、ComputeViewVisibility、RenderViewFamily_RenderThread等阶段。真正进入渲染循环是在FDeferredShadingSceneRenderer::Render里,这个方法体量非常大,我第一次看的时候光滚动就滚了好几分钟。

它内部的核心流程可以概括为几个阶段:首先是InitViews,做可见性剔除、计算View的各种矩阵和参数;然后是PrePass和DepthPrePass,提前写入深度;接着是GBufferPass,把材质属性写入多张RenderTarget;再是LightingPass,基于GBuffer做光照计算;之后是BasePass里的一些补充;最后是PostProcessing,做Bloom、ToneMapping、AA等后处理。

理解这个顺序非常重要,因为UE5的RDG(Render Dependency Graph)会把这些Pass组织成一个有向无环图,自动处理资源屏障和内存复用。你在写自定义Pass的时候,必须清楚自己的Pass应该插在哪个位置,依赖哪些资源,否则要么拿不到正确的数据,要么造成不必要的同步开销。

2.2 GBuffer布局策略与内存考量

延迟渲染的核心思想是把几何信息先写进一组RenderTarget,也就是GBuffer,然后在屏幕空间做光照。UE5的GBuffer布局经过多次演进,目前默认的布局在SceneRenderTargets和FGBufferParams里定义。

典型的GBuffer包含这几张:SceneColor(光照结果)、GBufferA(WorldNormal)、GBufferB(Metallic/Specular/Roughness/ShadingModelID)、GBufferC(BaseColor/AO)、GBufferD(自定义数据,比如次表面颜色)、GBufferE(预计算阴影因子)等。每张RT的格式和通道分配都是精心设计过的,目的是在精度和带宽之间找平衡。

这里有个很多人忽略的细节:UE5会根据当前 shading path 和平台能力动态调整GBuffer的格式。比如在支持PF_FloatRGBA的平台上,GBufferA可能用16位浮点存法线;在不支持的平台上,会退化成PF_A2B10G10R2这种打包格式。这个逻辑在FSceneRenderTargets::AllocateGBufferTargets里,你可以看到一堆条件分支。

为什么这件事重要?因为如果你在做自定义渲染Feature,需要往GBuffer里写额外数据,就必须考虑格式兼容性。我曾经在一个项目里想往GBufferD里塞自定义的材质ID,结果在某个移动平台上因为格式不支持直接崩了。后来改成用Stencil通道传递,才解决问题。所以理解GBuffer的布局策略,不只是看懂代码,更是避免实际项目踩坑。

2.3 RDG资源管理与Pass依赖关系

UE5的RDG是整个渲染管线的骨架。它的核心思想是:你声明你要读什么、写什么,RDG自动帮你安排执行顺序、插入屏障、复用内存。听起来很美好,但实际用起来有很多细节要注意。

FRDGBuilder是构建图的入口,你在Render方法里会看到大量GraphBuilder.CreateTexture、GraphBuilder.AddPass这样的调用。每个Pass通过FRDGPassParameterStruct声明依赖,RDG在Execute阶段会做拓扑排序,然后逐个执行。

这里的关键点是:RDG的资源生命周期是自动管理的,但前提是你正确声明了依赖。如果你在Pass A里写了一张纹理,在Pass B里读它,但没有通过参数结构声明这个依赖,RDG可能会把Pass B排到Pass A前面,或者提前释放纹理内存,导致花屏或崩溃。我调试过一个案例,自定义Pass的输出在编辑器里正常,打包后随机出现黑块,查了两天才发现是依赖声明漏了一个RDG_TEXTURE_ACCESS宏。

另外,RDG的调试功能非常有用。你可以在控制台输入r.RDG.Debug 1打开调试模式,然后在r.RDG.DumpGraph里导出当前帧的依赖图。这个图能直观看到每个Pass的输入输出和资源复用情况,排查性能问题时特别管用。

3. 光照通道的核心实现细节

3.1 标准延迟光照的Pass组织

GBuffer写完之后,就进入光照阶段。UE5的延迟光照在FDeferredShadingSceneRenderer::RenderLights里组织,核心思路是把所有光源分成几类:平行光、点光、聚光、矩形光,然后分别用不同的Pass处理。

平行光的处理最特殊,因为它影响整个场景,通常用一个全屏Pass计算。点光和聚光则用光源体积(Light Volume)的方式,只在实际影响范围内做计算,减少overdraw。这个分类逻辑在FDeferredShadingSceneRenderer::RenderLights里通过FLightSceneInfo的Proxy数据判断。

每个光源的着色逻辑在DeferredLightPixelShaders.usf里,核心函数是GetDynamicLighting。它做的事情是:从GBuffer读取材质属性,计算光源方向的BRDF,累加到SceneColor上。这里有个优化点:UE5会把多个小光源合并到一个Pass里处理,减少DrawCall和RT切换。这个合并逻辑在FDeferredShadingSceneRenderer::RenderLights的bUseLightingChannels分支里。

理解这个组织方式的意义在于:如果你要加自定义光源类型,或者修改光照模型,就必须知道在哪个Pass里改,以及怎么和现有的光源分类逻辑兼容。我见过有人直接在DeferredLightPixelShaders.usf里改BRDF,结果所有光源都受影响,包括不该受影响的环境光。正确的做法是通过ShadingModel或者LightFunction来区分。

3.2 Lumen与动态GI的介入点

Lumen是UE5渲染管线里最复杂的新增模块之一。它的核心是在光照阶段之前,先计算出间接光的贡献,然后作为额外的光照项叠加到SceneColor上。

Lumen的介入点在FDeferredShadingSceneRenderer::RenderLumenSceneLighting和RenderLumenSceneDirectLighting里。它会先构建一个简化的场景表示(Lumen Scene),然后在上面做光线追踪或者距离场追踪,计算出间接光。这个过程涉及大量的Compute Shader和RDG Pass。

从源码角度看,Lumen最值得关注的是它的资源管理策略。它需要维护多级Surface Cache、Radiance Cache、Probe数据等,这些资源的更新频率和生命周期管理非常复杂。在LumenSceneRendering.cpp里,你可以看到UpdateLumenScene、RenderLumenSceneLighting、RenderLumenSceneDirectLighting等函数的调用顺序和依赖关系。

实际项目中,Lumen的性能开销主要来自两个方面:一是Surface Cache的更新,二是最终Gather Pass的采样。前者可以通过r.LumenScene.SurfaceCache.CardCaptureRefreshFraction控制更新频率,后者可以通过r.Lumen.ScreenProbeGather.DownsampleFactor降低采样分辨率。这些参数在源码里都有对应的CVar定义,理解它们的实现逻辑,才能知道调参的边界在哪里。

3.3 阴影与光照的交互逻辑

阴影在UE5里是一个独立的Pass,但和光照紧密耦合。FDeferredShadingSceneRenderer::RenderShadowDepthMaps负责渲染阴影深度图,然后在光照Pass里通过GetShadowTerms采样。

这里有个容易混淆的点:UE5的阴影分为静态阴影(Static Shadow)和动态阴影(Dynamic Shadow)。静态阴影通过预计算的Shadowmap或者Distance Field Shadow实现,动态阴影通过实时渲染的Shadow Depth Map实现。两者的混合逻辑在ShadowRendering.cpp里,核心函数是RenderShadowDepthMaps和RenderVirtualShadowMaps。

Virtual Shadow Map(VSM)是UE5的新特性,它用虚拟纹理的方式管理阴影贴图,大幅提升了阴影精度和性能。VSM的实现涉及Page Table管理、物理Page分配、缓存失效等机制,代码量很大。如果你在做阴影相关的优化,建议先理解FVirtualShadowMapArray和FVirtualShadowMapCacheManager这两个类的职责。

我在实际项目里遇到过一个VSM相关的Bug:场景里某个物体的阴影在相机移动时闪烁。查了很久发现是Page的缓存失效策略问题,当物体移动速度超过某个阈值时,VSM的缓存没有及时更新。后来通过调整r.Shadow.Virtual.Cache.MaxPageAge参数缓解了这个问题。这个经历告诉我,理解源码不只是为了改代码,更是为了在出问题时知道去哪里找原因。

4. 后处理链的插入与自定义扩展

4.1 后处理链的标准顺序

UE5的后处理链在FDeferredShadingSceneRenderer::RenderPostProcessing里组织,标准顺序大致是:Bloom、ToneMapping、ColorGrading、AntiAliasing(TAA/TSR)、MotionBlur、DOF、ChromaticAberration、Vignette等。

这个顺序不是随便定的,每一步都有其物理意义。比如Bloom必须在ToneMapping之前,因为Bloom是基于HDR亮度提取的;ToneMapping之后再做ColorGrading,是因为调色通常在LDR空间进行更符合美术直觉。理解这个顺序,才能知道自定义后处理应该插在哪里。

后处理的实现方式在UE5里有两套:一套是传统的PostProcessMaterial,通过材质编辑器编写;另一套是C++里的FPostProcessPass,用于引擎内置的后处理效果。两者最终都会通过RDG的Pass执行。

4.2 自定义后处理Pass的插入方法

如果你想加一个自定义的后处理效果,比如一个特殊的屏幕空间效果,有几种方式:

第一种是写一个PostProcessMaterial,在材质里用SceneTexture节点采样SceneColor,然后做处理。这种方式最简单,但灵活性有限,不能访问RDG资源,也不能做多Pass处理。

第二种是在C++里通过FSceneViewExtension插入自定义Pass。这是UE5推荐的扩展方式,你可以在SubscribeToPostProcessingPass里指定插入点,比如EPostProcessingPass::Tonemap之前或之后。然后在PrePostProcessPass_RenderThread里构建你的RDG Pass。

第三种是直接修改引擎源码,在RenderPostProcessing里插入你的Pass。这种方式最灵活,但维护成本高,升级引擎版本时容易冲突。

我个人的建议是优先用FSceneViewExtension,它的接口设计得比较清晰,而且不侵入引擎代码。只有在需要修改引擎内置后处理逻辑时,才考虑直接改源码。

4.3 TSR与TAA的实现差异

UE5的TSR(Temporal Super Resolution)是TAA的升级版,两者都是时域抗锯齿,但实现差异很大。TAA的核心是:用上一帧的颜色和当前帧的Jittered采样做混合,通过历史帧累积来消除锯齿。TSR则在此基础上增加了更智能的历史帧重投影、更精确的运动矢量处理、以及基于深度的采样权重计算。

TSR的实现主要在TemporalSuperResolution.cpp和TSRShaders.usf里。核心步骤包括:运动矢量重投影、历史帧验证、邻域采样、权重计算、最终合成。其中历史帧验证是最关键的一步,它决定了哪些历史像素是可信的,哪些需要丢弃。这个逻辑在TSRShaders.usf的ValidateHistory函数里。

实际使用中,TSR的画质明显优于TAA,但性能开销也更高。在r.TSR.开头的CVar里,你可以找到各种控制参数,比如r.TSR.History.ScreenPercentage控制历史帧的分辨率,r.TSR.ShadingRejection.Flickering控制闪烁抑制的强度。理解这些参数的实现逻辑,才能根据项目需求做针对性调优。

5. 性能优化与调试实战

5.1 用RDG调试工具定位瓶颈

RDG的调试工具是我用得最多的性能分析手段之一。打开r.RDG.Debug 1后,你可以在ProfileGPU或者Stat GPU里看到每个Pass的耗时。更强大的是r.RDG.DumpGraph,它会导出当前帧的完整依赖图,包括每个Pass的输入输出资源和执行时间。

我通常的做法是:先用Stat GPU找到耗时最高的几个Pass,然后用r.RDG.DumpGraph看这些Pass的依赖关系,判断是否有不必要的资源同步或者内存拷贝。有一次我发现一个自定义Pass耗时异常,查图之后发现它和另一个Pass共享了一张纹理,但依赖声明写错了,导致RDG插入了一个额外的Copy Pass。修正依赖声明后,耗时直接降了一半。

5.2 常见性能陷阱与规避策略

延迟渲染管线里有很多性能陷阱,我列几个最常见的:

第一个是GBuffer的过度写入。有些材质会往所有GBuffer通道写数据,即使某些通道根本用不到。这会造成带宽浪费。正确的做法是在材质里用ShadingModel和MaterialAttributes精确控制哪些通道需要写入。

第二个是光源的过度绘制。当场景里有很多小光源时,如果每个光源都用一个全屏Pass处理,overdraw会非常严重。UE5的光源体积机制就是为了解决这个问题,但前提是光源的AttenuationRadius设置合理。如果半径设得太大,光源体积会覆盖大量屏幕空间,优化效果就打折扣了。

第三个是后处理的重复采样。有些后处理效果会多次采样SceneColor,如果每次都用全分辨率采样,开销会很大。UE5的后处理链里有很多降分辨率的优化,比如Bloom就是用多级降采样实现的。你在写自定义后处理时,也应该考虑是否可以用降分辨率的方式减少采样开销。

5.3 移动端与PC端的管线差异

移动端的渲染管线和PC端差异很大。移动端默认走前向渲染,GBuffer不存在,光照直接在BasePass里计算。这意味着很多PC端的优化手段在移动端不适用,比如延迟光照的光源体积优化。

移动端的后处理链也做了大量简化。Bloom用的是更低分辨率的降采样,ToneMapping的精度也降低了,TSR被替换成了更轻量的AA方案。这些差异在MobileShadingRenderer.cpp和PostProcessMobile.cpp里可以看到。

如果你在做跨平台项目,理解这些差异非常重要。我曾经在一个项目里把PC端的后处理参数直接搬到移动端,结果帧率直接掉到个位数。后来查源码才发现,移动端的后处理链根本不支持某些高级效果,强行开启会走fallback路径,性能极差。

6. 自定义渲染Feature的完整实现流程

6.1 从需求到Pass设计的思路

假设你要做一个自定义的渲染Feature,比如一个屏幕空间的描边效果。第一步是明确需求:描边是基于深度的,还是基于法线的,还是基于自定义的Stencil?不同的需求决定了你要在哪个Pass之后插入,以及需要哪些输入资源。

如果基于深度和法线,你需要在GBuffer Pass之后、光照Pass之前插入,因为光照之后SceneColor已经被修改,深度和法线信息可能不再准确。如果基于Stencil,你需要在BasePass里写入Stencil,然后在后处理阶段读取。

这个决策过程在源码里体现为:你需要找到合适的EPostProcessingPass插入点,或者直接在Render方法里找到合适的调用位置。UE5的FSceneViewExtension提供了SubscribeToPostProcessingPass接口,可以指定在哪个后处理Pass之前或之后执行。

6.2 RDG Pass的编写与资源声明

写一个RDG Pass的基本模板是这样的:先创建输出纹理,然后通过GraphBuilder.AddPass添加Pass,在Lambda里执行实际的渲染命令。关键是正确声明依赖:

FRDGTextureRef OutputTexture = GraphBuilder.CreateTexture( FRDGTextureDesc::Create2D(Extent, PF_FloatRGBA, FClearValueBinding::Black, TexCreate_RenderTargetable | TexCreate_ShaderResource), TEXT("CustomOutlineOutput")); GraphBuilder.AddPass( RDG_EVENT_NAME("CustomOutlinePass"), PassParameters, ERDGPassFlags::Raster, [PassParameters](FRHICommandList& RHICmdList) { // 设置RenderTarget,绑定Shader,Draw });

这里PassParameters是一个FShaderParameters结构体,里面声明了所有输入输出资源。RDG会根据这些声明自动插入屏障和安排执行顺序。如果你漏声明了某个资源,RDG不会报错,但运行时可能出现花屏或崩溃。

6.3 与现有管线的兼容性处理

自定义Feature最大的挑战是兼容性。你的Pass可能和引擎内置的Pass有资源冲突,或者在某些平台上不支持。处理兼容性的几个原则:

第一,尽量使用RDG提供的抽象接口,不要直接操作RHI资源。RDG会帮你处理平台差异。

第二,在Pass的ShouldCompilePermutation里检查平台和Shader Model支持情况,不支持时直接跳过。

第三,在FSceneViewExtension的IsActiveThisFrame里判断当前View是否需要执行你的Feature,避免不必要的开销。

第四,做好Fallback。如果某个平台不支持你的Feature,要有降级方案,而不是直接崩溃。

我在项目里做自定义Feature时,最常犯的错误是忘记处理Editor和Game的差异。Editor里View的构造流程和Game不一样,有些资源在Editor里可能还没准备好。后来我养成了一个习惯:在IsActiveThisFrame里加一个GIsEditor的判断,Editor下走简化路径,Game下走完整路径。

7. 源码阅读的方法论与工具链

7.1 如何高效定位关键代码

UE5的渲染代码量巨大,盲目阅读效率极低。我的方法是:从问题出发,而不是从代码出发。比如你想知道"Bloom是怎么实现的",就直接搜Bloom相关的CVar和函数名,找到Bloom.cpp,然后顺着调用链往上往下看。

另一个技巧是利用RenderDoc或者PIX抓帧,然后在抓帧结果里找到对应的DrawCall,看它的Shader和资源绑定。再回到源码里搜Shader文件名,就能快速定位到对应的C++代码。这个方法特别适合理解某个具体效果的实现。

7.2 调试工具与日志的配合使用

UE5的渲染调试工具非常丰富。除了前面提到的RDG调试,还有r.ShaderPrint可以在Shader里打印变量,r.DumpGPU可以导出GPU状态,ProfileGPU可以看每个Pass的耗时。

日志方面,UE_LOG(LogRenderer, ...)是常用的调试手段。你可以在关键路径上加日志,观察执行顺序和参数值。不过要注意,渲染代码大多在RenderThread上执行,日志输出要用UE_LOG的RenderThread版本,否则会有线程安全问题。

7.3 版本升级时的代码迁移经验

UE5的渲染代码在版本之间变化很大。从5.0到5.3,RDG的API改了好几次,Lumen的实现也重构过。如果你有自定义的渲染代码,升级引擎版本时大概率需要迁移。

我的经验是:不要试图一次性迁移所有代码,而是按功能模块逐个迁移。每迁移一个模块,就用r.RDG.Debug和ProfileGPU验证一遍,确保没有引入新的性能问题。另外,尽量把自定义代码和引擎代码解耦,用FSceneViewExtension而不是直接改引擎源码,这样升级时只需要适配接口变化,不需要重新理解引擎内部逻辑。

8. 几个实际项目中的踩坑记录

8.1 GBuffer格式不兼容导致的崩溃

前面提过,我在一个项目里想往GBufferD里写自定义数据,结果在某个移动平台上崩溃。原因是那个平台的GBufferD格式是PF_B8G8R8A8,不支持浮点写入。后来改成用Stencil通道传递,才解决问题。

这个坑的教训是:永远不要假设GBuffer的格式在所有平台上都一样。在写自定义GBuffer数据之前,先查FSceneRenderTargets::AllocateGBufferTargets里的平台分支,确认目标平台的格式支持情况。

8.2 RDG依赖声明错误导致的随机花屏

另一个坑是RDG依赖声明错误。我在一个自定义Pass里读了一张纹理,但忘记在PassParameters里声明。在Editor里运行正常,打包后随机出现花屏。查了两天才发现是RDG把Pass的执行顺序排错了。

这个坑的教训是:RDG的依赖声明必须完整且准确。每次写完Pass,都要用r.RDG.DumpGraph检查一遍依赖关系,确认没有遗漏。

8.3 后处理顺序错误导致的画质问题

还有一个坑是后处理顺序。我在一个项目里加了一个自定义的ColorGrading Pass,插在了ToneMapping之前。结果画面过曝严重,因为ToneMapping之前是HDR空间,ColorGrading的参数是按LDR空间调的。

这个坑的教训是:后处理的插入点必须符合物理意义。在插入自定义后处理之前,先理解当前后处理链的顺序和每个Pass的输入输出空间,确保你的Pass在正确的空间里执行。

8.4 移动端Shader编译失败的排查

移动端Shader编译失败是另一个常见问题。我在一个项目里写了一个自定义Shader,PC上正常,移动端编译报错。原因是用了移动端不支持的Shader Model特性。后来通过ShouldCompilePermutation加了平台判断,才解决。

这个坑的教训是:移动端的Shader能力有限,写Shader时要考虑平台兼容性。在ShouldCompilePermutation里做好平台检查,不支持时走Fallback路径。

9. 后续可以深入的方向

渲染管线源码解析这个系列还有很多可以展开的方向。比如Nanite的Cluster管理和光栅化流程、Virtual Shadow Map的Page管理机制、Lumen的Surface Cache更新策略、Substrate材质系统的实现细节等。每一个方向都值得单独写一篇甚至几篇。

我个人的计划是接下来先写Nanite的源码解析,因为它在实际项目里的性能影响最大,而且很多团队在用它的时候遇到了各种问题。如果你对某个特定模块特别感兴趣,也可以告诉我,我会优先安排。

另外,源码阅读这件事,光看是不够的。我强烈建议你在看的同时动手改一改,比如改一个光照模型、加一个后处理效果、优化一个Pass的性能。只有真正动手了,才能把源码里的逻辑变成自己的理解。我在项目里带新人的时候,也是用这种方式:先让他们读一遍FDeferredShadingSceneRenderer::Render,然后让他们加一个简单的自定义Pass,最后让他们优化一个性能瓶颈。经过这个流程,基本就能独立处理渲染相关的问题了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/12 1:26:41

C++ 小病毒让鼠标锁死:用 TaoToken 统一 Key 复现与防御实验

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/12 1:22:51

Cortex 安全部署指南:多租户认证、TLS 加固与漏洞披露机制

可观测性时序数据库后端指标监控 【免费下载链接】cortex A horizontally scalable, highly available, multi-tenant, long term Prometheus. 项目地址: https://gitcode.com/gh_mirrors/cortex6/cortex 点击查看 免费下载 本篇指南围绕开源时序数据库 Cortex 的官…

作者头像 李华