草庐IT

最易懂的Prometheus告警原理详解

轻风博客 2023-03-28 原文

通俗易懂的一篇文章,主要介绍了 Prometheus 什么时候告警,什么时候不会告警。同时介绍了 Prometheus 告警原理。

 

警报是监控系统中必不可少的一块, 当然了, 也是最难搞的一块. 我们乍一想, 警报似乎很简单一件事:

 

假如发生了异常情况, 发送或邮件/消息通知给某人或某频道。

 

一把梭搞起来之后,就不免有一些小麻烦:

 

  • 这个啊…一天中总有那么几次波动,也难修难查了,算了算了不看了;

     

  • 警报太多了,实在看不过来,屏蔽/归档/放生吧…

     

  • 有毒吧,这个阈值也太低了;

     

  • 卧槽,这些警报啥意思啊,发给我干嘛啊?

     

  • 卧槽卧槽卧槽,怎么一下子几十百来条警报, 哦…原来网络出问题了全崩了。

 

玩笑归玩笑,但至少我们能看出,警报不是一个简单的计算+通知系统。只是,”做好警报”这件事本身是个综合问题,代码能解决的也只是其中的一小部分,更多的事情要在组织、人事和管理上去做。

 

从 for 参数开始

 

我们首先需要一些背景知识:Prometheus 是如何计算并产生警报的?

 

看一条简单的警报规则:

alert: KubeAPILatencyHigh
  annotations:
    message: The API server has a 99th percentile latency of {{ $value }} seconds
      for {{ $labels.verb }} {{ $labels.resource }}.
  expr: |
    cluster_quantile:apiserver_request_latencies:histogram_quantile{job="apiserver",quantile="0.99",subresource!="log"} > 4
  for: 10m
  labels:
    severity: critical

这条警报的大致含义是,假如 kube-apiserver 的 P99 响应时间大于 4 秒,并持续 10 分钟以上,就产生报警。

 

首先要注意的是由 for 指定的 Pending Duration。这个参数主要用于降噪,很多类似响应时间这样的指标都是有抖动的,通过指定 Pending Duration,我们可以 过滤掉这些瞬时抖动,让 on-call 人员能够把注意力放在真正有持续影响的问题上。

 

那么显然,下面这样的状况是不会触发这条警报规则的,因为虽然指标已经达到了警报阈值,但持续时间并不够长:

 

 

但偶尔我们也会碰到更奇怪的事情。

 

问题 1 为什么不报警?

 

 

类似上面这样持续超出阈值的场景,为什么有时候会不报警呢?

 

问题 2 为什么报警?

 

 

类似上面这样并未持续超出阈值的场景,为什么有时又会报警呢?

 

采样间隔

 

这其实都源自于 Prometheus 的数据存储方式与计算方式。

 

首先,Prometheus 按照配置的抓取间隔(scrape_interval)定时抓取指标数据,因此存储的是形如 (timestamp, value) 这样的采样点。

 

对于警报, Prometheus 会按固定的时间间隔重复计算每条警报规则,因此警报规则计算得到的只是稀疏的采样点,而警报持续时间是否大于 for 指定的 Pending Duration 则是由这些稀疏的采样点决定的。

 

而在 Grafana 渲染图表时,Grafana 发送给 Prometheus 的是一个 Range Query,其执行机制是从时间区间的起始点开始,每隔一定的时间点(由 Range Query 的 step 请求参数决定) 进行一次计算采样。

 

这些结合在一起,就会导致警报规则计算时“看到的内容”和我们在 Grafana 图表上观察到的内容不一致,比如下面这张示意图:

 

 

上面图中,圆点代表原始采样点:

 

  • 40s 时,第一次计算,低于阈值。

     

  • 80s 时,第二次计算,高于阈值,进入 Pending 状态。

     

  • 120s 时,第三次计算,仍然高于阈值,90s 处的原始采样点虽然低于阈值,但是警报规则计算时并没有”看到它“。

     

  • 160s 时,第四次计算,高于阈值,Pending 达到 2 分钟,进入 firing 状态 持续高于阈值。

     

  • 直到 360s 时,计算得到低于阈值,警报消除。

 

由于采样是稀疏的,部分采样点会出现被跳过的状况,而当 Grafana 渲染图表时,取决于 Range Query 中采样点的分布,图表则有可能捕捉到 被警报规则忽略掉的”低谷“(图三)或者也可能无法捕捉到警报规则碰到的”低谷“(图二)。如此这般,我们就被”图表“给蒙骗过去,质疑起警报来了。

 

如何应对

 

首先嘛, Prometheus 作为一个指标系统天生就不是精确的——由于指标本身就是稀疏采样的,事实上所有的图表和警报都是”估算”,我们也就不必 太纠结于图表和警报的对应性,能够帮助我们发现问题解决问题就是一个好监控系统。当然,有时候我们也得证明这个警报确实没问题,那可以看一眼 ALERTS 指标。ALERTS 是 Prometheus 在警报计算过程中维护的内建指标,它记录每个警报从 Pending 到 Firing 的整个历史过程,拉出来一看也就清楚了。

 

但有时候 ALERTS 的说服力可能还不够,因为它本身并没有记录每次计算出来的值到底是啥,而在我们回头去考证警报时,又无法选取出和警报计算过程中一模一样的计算时间点, 因此也就无法还原警报计算时看到的计算值究竟是啥。这时候终极解决方案就是把警报所要计算的指标定义成一条 Recording Rule,计算出一个新指标来记录计算值,然后针对这个 新指标做阈值报警。kube-prometheus 的警报规则中就大量采用了这种技术。

 

到此为止了吗?

 

Prometheus 警报不仅包含 Prometheus 本身,还包含用于警报治理的 Alertmanager,我们可以看一看上面那张指标计算示意图的全图:

 

 

在警报产生后,还要经过 Alertmanager 的分组、抑制处理、静默处理、去重处理和降噪处理最后再发送给接收者。而这个过程也有大量的因素可能会导致警报产生了却最终 没有进行通知。

 

为什么要 Alertmanager?

 

我们先介绍一点背景知识,Prometheus 生态中的警报是在 Prometheus Server 中计算警报规则(Alert Rule)并产生的,而所谓计算警报规则,其实就是周期性地执行一段 PromQL,得到的查询结果就是警报,比如:

node_load5 > 20

 

这个 PromQL 会查出所有”在最近一次采样中,5分钟平均 Load 大于 20”的时间序列。这些序列带上它们的标签就被转化为警报。

 

只是,当 Prometheus Server 计算出一些警报后,它自己并没有能力将这些警报通知出去,只能将警报推给 Alertmanager,由 Alertmanager 进行发送。

 

这个切分,一方面是出于单一职责的考虑,让 Prometheus “do one thing and do it well”, 另一方面则是因为警报发送确实不是一件”简单”的事,需要一个专门的系统来做好它。可以这么说,Alertmanager 的目标不是简单地”发出警报”,而是”发出高质量的警报”。它提供的高级功能包括但不限于:

 

Go Template 渲染警报内容;

 

  • 管理警报的重复提醒时机与消除后消除通知的发送;

     

  • 根据标签定义警报路由,实现警报的优先级、接收人划分,并针对不同的优先级和接收人定制不同的发送策略;

     

  • 将同类型警报打包成一条通知发送出去,降低警报通知的频率;

     

  • 支持静默规则: 用户可以定义一条静默规则,在一段时间内停止发送部分特定的警报,比如已经确认是搜索集群问题,在修复搜索集群时,先静默掉搜索集群相关警报;

     

  • 支持”抑制”规则(Inhibition Rule): 用户可以定义一条”抑制”规则,规定在某种警报发生时,不发送另一种警报,比如在”A 机房网络故障”这条警报发生时,不发送所有”A 机房中的警报”;

 

假如你很忙,那么读到这里就完全 OK 了,反正这类文章最大的作用就是让我们”知道有 X 这回事,大概了解有啥特性,当有需求匹配时,能想到试试看 X 合不合适“,其中 X = Alertmanager。当然,假如你是个好奇宝宝,那么还可以看看下面的解析。

 

Alertmanager 内部架构

 

先看官方文档中的架构图:

 

 

  • 从左上开始,Prometheus 发送的警报到 Alertmanager;

     

  • 警报会被存储到 AlertProvider 中,Alertmanager 的内置实现就是包了一个 map,也就是存放在本机内存中,这里可以很容易地扩展其它 Provider;

     

  • Dispatcher 是一个单独的 goroutine,它会不断到 AlertProvider 拉新的警报,并且根据 YAML 配置的 Routing Tree 将警报路由到一个分组中;

     

  • 分组会定时进行 flush (间隔为配置参数中的 group_interval), flush 后这组警报会走一个 Notification Pipeline 链式处理;

     

  • Notification Pipeline 为这组警报确定发送目标,并执行抑制逻辑,静默逻辑,去重逻辑,发送与重试逻辑,实现警报的最终投递;

 

下面就分开讲一讲核心的两块:

 

  • Dispatcher 中的 Routing Tree 的实现与设计意图

     

  • Notification Pipeline 的实现与设计意图

 

Routing Tree

 

Routing Tree 的是一颗多叉树,节点的数据结构定义如下:


// 节点包含警报的路由逻辑
type Route struct {
    // 父节点
    parent *Route
    // 节点的配置,下文详解
    RouteOpts RouteOpts
    // Matchers 是一组匹配规则,用于判断 Alert 与当前节点是否匹配
    Matchers types.Matchers
    // 假如为 true, 那么 Alert 在匹配到一个节点后,还会继续往下匹配
    Continue bool
    // 子节点
    Routes []*Route
}

 

具体的处理代码很简单,深度优先搜索:警报从 root 开始匹配(root 默认匹配所有警报),然后根据节点中定义的 Matchers 检测警报与节点是否匹配,匹配则继续往下搜索,默认情况下第一个”最深”的 match (也就是 DFS 回溯之前的最后一个节点)会被返回。特殊情况就是节点配置了 Continue=true,这时假如这个节点匹配上了,那不会立即返回,而是继续搜索,用于支持警报发送给多方这种场景(比如”抄送”)

 

深度优先搜索

func (r *Route) Match(lset model.LabelSet) []*Route {
    if !r.Matchers.Match(lset) {
    return nil
    }

    var all []*Route
    for _, cr := range r.Routes {
        // 递归调用子节点的 Match 方法
        matches := cr.Match(lset)

        all = append(all, matches...)

        if matches != nil && !cr.Continue {
          break
        }
    }
    // 假如没有任何节点匹配上,那就匹配根节点
    if len(all) ==0 {
        all = append(all, r)
    }
    return all
}

 

为什么要设计一个复杂的 Routing Tree 逻辑呢?我们看看 Prometheus 官方的配置例子:为了简化编写,Alertmanager 的设计是根节点的所有参数都会被子节点继承(除非子节点重写了这个参数)


route:
  # 根节点的警报会发送给默认的接收组
  # 该节点中的警报会按’cluster’和’alertname’做 Group,每个分组中最多每5分钟发送一条警报,同样的警报最多4小时发送一次
  receiver:’default-receiver’
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 4h
  group_by: [cluster, alertname]
  # 没有匹配到子节点的警报,会默认匹配到根节点上
  # 接下来是子节点的配置:
  routes:
    # 所有 service 字段为 mysql 或 cassandra 的警报,会发送到’database-pager’这个接收组
    # 由于继承逻辑,这个节点中的警报仍然是按’cluster’和’alertname’做 Group 的
  - receiver:’database-pager’
    group_wait: 10s
    match_re:
    service: mysql|cassandra
    # 所有 team 字段为 fronted 的警报,会发送到’frontend-pager’这个接收组
    # 很重要的一点是,这个组中的警报是按’product’和’environment’做分组的,因为’frontend’面向用户,更关心哪个’产品’的什么’环境’出问题了
  - receiver:’frontend-pager’
    group_by: [product, environment]
    match:
    team: frontend

 

总结一下,Routing Tree 的设计意图是让用户能够非常自由地给警报归类,然后根据归类后的类别来配置要发送给谁以及怎么发送:

 

发送给谁?上面已经做了很好的示例,’数据库警报’和’前端警报’都有特定的接收组,都没有匹配上那么就是’默认警报’, 发送给默认接收组

 

怎么发送?对于一类警报,有个多个字段来配置发送行为:

 

  • group_by

 

决定了警报怎么分组,每个 group 只会定时产生一次通知,这就达到了降噪的效果,而不同的警报类别分组方式显然是不一样的,举个例子:配置中的 ‘数据库警报’ 是按 ‘集群’ 和 ‘规则名’ 分组的,这表明对于数据库警报,我们关心的是“哪个集群的哪个规则出问题了”,比如一个时间段内,’华东’集群产生了10条 ‘API响应时间过长’ 警报,这些警报就会聚合在一个通知里发出来;配置中的 ‘前端警报’ 是按 ‘产品’ 和 ‘环境’ 分组的, 这表明对于前端警报,我们关心的是“哪个产品的哪个环境出问题了”。

 

  • group_interval 和 group_wait

 

控制分组的细节,不细谈,其中 group_interval 控制了这个分组最快多久执行一次 Notification Pipeline。

 

  • repeat_interval

 

假如一个相同的警报一直 FIRING,Alertmanager 并不会一直发送警报,而会等待一段时间,这个等待时间就是 repeat_interval,显然,不同类型警报的发送频率也是不一样的。

 

group_interval 和 repeat_interval 的区别会在下文中详述。

 

Notification Pipeline

 

由 Routing Tree 分组后的警报会触发 Notification Pipeline:

 

当一个 AlertGroup 新建后,它会等待一段时间(group_wait 参数),再触发第一次 Notification Pipeline 假如这个 AlertGroup 持续存在,那么之后每隔一段时间(group_interval 参数),都会触发一次 Notification Pipeline 每次触发 Notification Pipeline,AlertGroup 都会将组内所有的 Alert 作为一个列表传进 Pipeline, Notification Pipeline 本身是一个按照责任链模式设计的接口,MultiStage 这个实现会链式执行所有的 Stage:

// A Stage processes alerts under the constraints of the given context.
type Stage interface {
    Exec(ctx context.Context, l log.Logger, alerts …*types.Alert) (context.Context, []*types.Alert, error)
}

// A MultiStage executes a series of stages sequencially.
type MultiStage []Stage
// Exec implements the Stage interface.
func (ms MultiStage) Exec(ctx context.Context, l log.Logger, alerts …*types.Alert) (context.Context, []*types.Alert, error) {
    var err error
    for _, s := range ms {
        if len(alerts) ==0{
            return ctx, nil, nil
        }

        ctx, alerts, err = s.Exec(ctx, l, alerts…)
        if err != nil {
            return ctx, nil, err
        }
    }
    return ctx, alerts, nil
}

 

MultiStage 里塞的就是开头架构图里画的 InhibitStage、SilenceStage…这么一条链式处理的流程,这里要提一下,官方的架构图画错了,RoutingStage 其实处在整个 Pipeline 的首位,不过这个顺序并不影响逻辑。要重点说的是DedupStage和NotifySetStage它俩协同负责去重工作,具体做法是:

 

  • NotifySetStage 会为发送成功的警报记录一条发送通知,key 是’接收组名字’+’GroupKey 的 key 值’,value 是当前 Stage 收到的 []Alert (这个列表和最开始进入 Notification Pipeline 的警报列表有可能是不同的,因为其中有些 Alert 可能在前置 Stage 中已经被过滤掉了)

     

  • DedupStage 中会以’接收组名字’+’GroupKey 的 key 值’为 key 查询通知记录,假如:

 

查询无结果,那么这条通知没发过,为这组警报发送一条通知;

 

查询有结果,那么查询得到已经发送过的一组警报 S,判断当前的这组警报 A 是否为 S 的子集:

 

假如 A 是 S 的子集,那么表明 A 和 S 重复,这时候要根据 repeat_interval 来决定是否再次发送:

 

距离 S 的发送时间已经过去了足够久(repeat_interval),那么我们要再发送一遍;

 

距离 S 的发送时间还没有达到 repeat_interval,那么为了降低警报频率,触发去重逻辑,这次我们就不发了;

 

假如 A 不是 S 的子集,那么 A 和 S 不重复,需要再发送一次;上面的表述可能有些抽象,最后表现出来的结果是:

 

  • 假如一个 AlertGroup 里的警报一直发生变化,那么虽然每次都是新警报,不会被去重,但是由于 group_interval (假设是5分钟)存在,这个 AlertGroup 最多 5 分钟触发一次 Notification Pipeline,因此最多也只会 5 分钟发送一条通知;

 

  • 假如一个 AlertGroup 里的警报一直不变化,就是那么几条一直 FIRING 着,那么虽然每个 group_interval 都会触发 Notification Pipeline,但是由于 repeate_interval(假设是1小时)存在,因此最多也只会每 1 小时为这个重复的警报发送一条通知;再说一下 Silence 和 Inhibit,两者都是基于用户主动定义的规则的:

 

  • Silence Rule:静默规则用来关闭掉部分警报的通知,比如某个性能问题已经修复了,但需要排期上线,那么在上线前就可以把对应的警报静默掉来减少噪音;

 

  • Inhibit Rule:抑制规则用于在某类警报发生时,抑制掉另一类警报,比如某个机房宕机了,那么会影响所有上层服务,产生级联的警报洪流,反而会掩盖掉根本原因,这时候抑制规则就有用了;因此 Notification Pipeline 的设计意图就很明确了:通过一系列逻辑(如抑制、静默、去重)来获得更高的警报质量,由于警报质量的维度很多(剔除重复、类似的警报,静默暂时无用的警报,抑制级联警报),因此 Notification Pipeline 设计成了责任链模式,以便于随时添加新的环节来优化警报质量。

 

结语

 

Alertmanager 整体的设计意图就是奔着治理警报(通知)去的,首先它用 Routing Tree 来帮助用户定义警报的归类与发送逻辑,然后再用 Notification Pipeline 来做抑制、静默、去重以提升警报质量。这些功能虽然不能解决”警报”这件事中所有令人头疼的问题,但确实为我们着手去解决”警报质量”相关问题提供了趁手的工具。

 

作者丨alei

来源丨网址:

https://aleiwu.com/post/alertmanager/  

https://aleiwu.com/post/prometheus-alert-why/

有关最易懂的Prometheus告警原理详解的更多相关文章

  1. ruby - 有人可以解释一下在 Ruby 中注入(inject)的真实、通俗易懂的用法吗? - 2

    我正在学习Ruby,遇到了inject。我正处于理解它的风口浪尖,但当我是那种需要真实世界的例子来学习一些东西的人时。我遇到的最常见的例子是人们使用inject来添加一个(1..10)范围的总和,我不太关心这个。这是一个任意的例子。在实际程序中我会用它做什么?我正在学习,所以我可以继续使用Rails,但我不必有一个以Web为中心的示例。我只需要一些我可以全神贯注的目标。谢谢大家。 最佳答案 inject有时可以通过它的“其他”名称reduce更好地理解。它是一个对Enumerable进行操作(迭代一次)并返回单个值的函数。它有许多有

  2. 物联网MQTT协议详解 - 2

    一、什么是MQTT协议MessageQueuingTelemetryTransport:消息队列遥测传输协议。是一种基于客户端-服务端的发布/订阅模式。与HTTP一样,基于TCP/IP协议之上的通讯协议,提供有序、无损、双向连接,由IBM(蓝色巨人)发布。原理:(1)MQTT协议身份和消息格式有三种身份:发布者(Publish)、代理(Broker)(服务器)、订阅者(Subscribe)。其中,消息的发布者和订阅者都是客户端,消息代理是服务器,消息发布者可以同时是订阅者。MQTT传输的消息分为:主题(Topic)和负载(payload)两部分Topic,可以理解为消息的类型,订阅者订阅(Su

  3. Tcl脚本入门笔记详解(一) - 2

    TCL脚本语言简介•TCL(ToolCommandLanguage)是一种解释执行的脚本语言(ScriptingLanguage),它提供了通用的编程能力:支持变量、过程和控制结构;同时TCL还拥有一个功能强大的固有的核心命令集。TCL经常被用于快速原型开发,脚本编程,GUI和测试等方面。•实际上包含了两个部分:一个语言和一个库。首先,Tcl是一种简单的脚本语言,主要使用于发布命令给一些互交程序如文本编辑器、调试器和shell。由于TCL的解释器是用C\C++语言的过程库实现的,因此在某种意义上我们又可以把TCL看作C库,这个库中有丰富的用于扩展TCL命令的C\C++过程和函数,所以,Tcl是

  4. 【Unity游戏破解】外挂原理分析 - 2

    文章目录认识unity打包目录结构游戏逆向流程Unity游戏攻击面可被攻击原因mono的打包建议方案锁血飞天无限金币攻击力翻倍以上统称内存挂透视自瞄压枪瞬移内购破解Unity游戏防御开发时注意数据安全接入第三方反作弊系统外挂检测思路狠人自爆实战查看目录结构用il2cppdumper例子2-森林whoishe后记认识unity打包目录结构dll一般很大,因为里面是所有的游戏功能编译成的二进制码游戏逆向流程开发人员代码被编译打包到GameAssembly.dll中使用il2ppDumper工具,并借助游戏名_Data\il2cpp_data\Metadata\global-metadata.dat

  5. 【详解】Docker安装Elasticsearch7.16.1集群 - 2

    开门见山|拉取镜像dockerpullelasticsearch:7.16.1|配置存放的目录#存放配置文件的文件夹mkdir-p/opt/docker/elasticsearch/node-1/config#存放数据的文件夹mkdir-p/opt/docker/elasticsearch/node-1/data#存放运行日志的文件夹mkdir-p/opt/docker/elasticsearch/node-1/log#存放IK分词插件的文件夹mkdir-p/opt/docker/elasticsearch/node-1/plugins若你使用了moba,直接右键新建即可如上图所示依次类推创建

  6. 【Elasticsearch基础】Elasticsearch索引、文档以及映射操作详解 - 2

    文章目录概念索引相关操作创建索引更新副本查看索引删除索引索引的打开与关闭收缩索引索引别名查询索引别名文档相关操作新建文档查询文档更新文档删除文档映射相关操作查询文档映射创建静态映射创建索引并添加映射概念es中有三个概念要清楚,分别为索引、映射和文档(不用死记硬背,大概有个印象就可以)索引可理解为MySQL数据库;映射可理解为MySQL的表结构;文档可理解为MySQL表中的每行数据静态映射和动态映射上面已经介绍了,映射可理解为MySQL的表结构,在MySQL中,向表中插入数据是需要先创建表结构的;但在es中不必这样,可以直接插入文档,es可以根据插入的文档(数据),动态的创建映射(表结构),这就

  7. 最强Http缓存策略之强缓存和协商缓存的详解与应用实例 - 2

    HTTP缓存是指浏览器或者代理服务器将已经请求过的资源保存到本地,以便下次请求时能够直接从缓存中获取资源,从而减少网络请求次数,提高网页的加载速度和用户体验。缓存分为强缓存和协商缓存两种模式。一.强缓存强缓存是指浏览器直接从本地缓存中获取资源,而不需要向web服务器发出网络请求。这是因为浏览器在第一次请求资源时,服务器会在响应头中添加相关缓存的响应头,以表明该资源的缓存策略。常见的强缓存响应头如下所述:Cache-ControlCache-Control响应头是用于控制强制缓存和协商缓存的缓存策略。该响应头中的指令如下:max-age:指定该资源在本地缓存的最长有效时间,以秒为单位。例如:Ca

  8. Streampark集成Cloudera Flink、ldap、告警,以及部署常见问题 - 2

    集成背景我们当前集群使用的是ClouderaCDP,Flink版本为ClouderaVersion1.14,整体Flink安装目录以及配置文件结构与社区版本有较大出入。直接根据Streampark官方文档进行部署,将无法配置FlinkHome,以及后续整体Flink任务提交到集群中,因此需要进行针对化适配集成,在满足使用需求上,尽量提供完整的Streampark使用体验。集成步骤版本匹配问题解决首先解决无法识别Cloudera中的FlinkHome问题,根据报错主要明确到的事情是无法读取到Flink版本、lib下面的jar包名称无法匹配。修改对象:修改源码:(解决无法匹配clouderajar

  9. IDEA 2022 创建 Spring Boot 项目详解 - 2

    如何用IDEA2022创建并初始化一个SpringBoot项目?目录如何用IDEA2022创建并初始化一个SpringBoot项目?0. 环境说明1.  创建SpringBoot项目 2.编写初始化代码0. 环境说明IDEA2022.3.1JDK1.8SpringBoot1.  创建SpringBoot项目        打开IDEA,选择NewProject创建项目。        填写项目名称、项目构建方式、jdk版本,按需要修改项目文件路径等信息。        选择springboot版本以及需要的包,此处只选择了springweb。        此处需特别注意,若你使用的是jdk1

  10. 详解Unity中的粒子系统Particle System (二) - 2

    前言上一篇我们简要讲述了粒子系统是什么,如何添加,以及基本模块的介绍,以及对于曲线和颜色编辑器的讲解。从本篇开始,我们将按照模块结构讲解下去,本篇主要讲粒子系统的主模块,该模块主要是控制粒子的初始状态和全局属性的,以下是关于该模块的介绍,请大家指正。目录前言本系列提要一、粒子系统主模块1.阅读前注意事项2.参考图3.参数讲解DurationLoopingPrewarmStartDelayStartLifetimeStartSpeed3DStartSizeStartSize3DStartRotationStartRotationFlipRotationStartColorGravityModif

随机推荐