首页 > 其他分享 >【安全架构】监控告警:Prometheus体系中告警的那些事

【安全架构】监控告警:Prometheus体系中告警的那些事

时间:2024-11-06 18:21:23浏览次数:1  
标签:架构 labels instance Prometheus 规则 告警 vmalert

原创 小斐Lab 网络小斐

在原生的 Prometheus 体系中,告警引擎评估模块是集成在 Prometheus 时序数据库中的,把告警规则定义好,放在 Prometheus 告警配置模块路径下即可实现对实例告警评估和触发。然后使用 Alertmanager 做告警路由和告警消息转发。
如下消息通知简单原理示意图:

而上面的告警规则: Alert rule 在 Prometheus 和 Alertmanager 结合的场景中所在位置和流程如下所示:

而在 VictoriaMetrics 体系中, VictoriaMetrics 时序库并没有集成告警引擎评估模块,而是把该模块单独拧出来,用单独的程序进行实现。也就是今天要讲的 vmalert 程序,它可以完全替代 Prometheus 时序库集成的的告警引擎评估模块,并且可以定义记录规则,根据需要可以把并记录规则的数据回写到 vmagent 或者 VictoriaMetrics 时序库中,最终的情况就是 vmalert 全面接管告警引擎评估模块,实现对告警规则做评估和发送告警通知,最终架构如下:

alertmanager 介绍

首先需要明白一个基本概念,时序库自身一般都不带告警功能(去重、抑制、静默、告警路由、发送告警)等功能,一般都需要使用单独的告警组件实现,不同的架构实现方式有点差异,这里我单独讲讲使用 Prometheus 时序库的架构和使用 VictoriaMetrics 时序库架构。

一般情况下触发一条告警的基本过程是什么样子的呢,这里以一副图来呈现:

在这些架构中告警都有 Alertmanager 配合,那 Alertmanager 在其中都起到什么作用呢?

Alertmanager 是一个专门用于实现告警的工具或组件,可以实现接收 Prometheus 或 vmalert 等其它应用发出的告警信息,并且可以对这些告警信息进行 分组 、 抑制 以及 静默 等操作,然后通过 路由 的方式,根据不同的告警规则配置,分发到不同的告警路由策略中。

除此之外, Alertmanager 还支持 邮件 、 "企业微信 、 Slack 、 WebHook 等多种方式发送告警信息,并且其中 WebHook 这种方式可以将告警信息转发到我们自定义的应用中,使我们可以对告警信息进行处理,所以使用 Alertmanager 进行告警,非常方便灵活、简单易用。

Alertmanager 常用的功能主要有:

抑制: 抑制是一种机制,指的是当某一告警信息发送后,可以停止由此告警引发的其它告警,避免相同的告警信息重复发送。
静默: 静默也是一种机制,指的是依据设置的标签,对告警行为进行静默处理。如果 AlertManager 接收到的告警符合静默配置,则 Alertmanager 就不会发送该告警通知。
发送告警: 支持配置多种告警规则,可以根据不同的路由配置,采用不同的告警方式发送告警通知。
告警分组: 分组机制可以将详细的告警信息合并成一个通知。在某些情况下,如系统宕机导致大量的告警被同时触发,在这种情况下分组机制可以将这些被触发的告警信息合并为一个告警通知,从而避免一次性发送大量且属于相同问题的告警,导致无法对问题进行快速定位。

那 Alertmanager 在 Prometheus 或 VictoriaMetrics 使用的时序库架构中是什么样的关系呢,下面用两张图去呈现?

两者区别就是 Prometheus 内部集成告警规则引擎,而 VictoriaMetrics 是通过外部单独组件 vmalert 实现告警规则引擎。

Prometheus 架构下的记录规则直接写入到 Prometheus 时序库中,而 VictoriaMetrics 架构下的记录规则可以直接写入 vmalert 定义的参数 -remoteWrite.url 中,一般配置为 vmagent 地址或 VictoriaMetrics 地址即可。

其实这里讲的告警只是一个很简单的告警通知逻辑,成熟的商业化告警有一系列的后续处理,比如:多渠道分级通知、告警静默、抑制、收敛聚合、降噪、排班、认领升级、协同闭环处理等等,习惯称呼为 OnCall 平台,如果有这一块需求,应该去了解商业化告警产品。

配置告警

按照上面的两个不同时序库的告警架构,我们可以看到首先需要定义告警规则,然后把定义好的告警规则放入到一个文件中,让不同的告警引擎可以加载这些规则,实现对时序库中的需要关注的指标做告警评估。

告警规则

告警规则的简单示例:

groups:  # 定义一个告警规则组
- name: Instances  # 规则组组名,可以将同一类型的报警放到一个分组中
  rules:  # 定义告警规则,可以有多个
  - alert: 主机宕机  # 告警名称(alertname),也就是告警信息的标题,一个alert代表一个告警规则
    expr: up == 0  # 表达式,根据表达式的值进行匹配
    for: 5m  # 表达式评估触发报警,发送告警消息前的等待时间,如果在这个实际内恢复,状态恢复到健康状态
    labels:  # 定义标签,可自定义多个标签
      severity: Critical  # 告警级别
    annotations:  # 定义告警内容
      # 消息内容,$labels.instance 就是监控项中的标签变量
      summary: 主机 {{ $labels.instance }} 停止工作
      # 详细描述
      description: "{{ $labels.instance }} job {{ $labels.job }} 已经宕机5分钟以上!"

详细介绍一条告警规则的字段:

alert :告警规则的名称( alertname ),在 Alertmanager 喜欢使用 alertname 进行告警分组。
expr :定义告警条件的 PromQL/MetricsQL 表达式,这里是判断主机状态表达式。
for :规定持续多长时间满足条件后触发告警。
labels :添加到告警标签中的标签,例如,定义告警的严重性。
annotations :提供有关告警的描述性信息,一般定义 summary (摘要信息)和 description (详细描述)。

如何针对这些字段定义呢?

alert 是指某条告警规则的名称,可以自定义,比如:内存使用率超限可以定义为 HostOutOfMemory ,名称支持中文定义。 expr 就是告警条件表达式,主要是查询表达式加逻辑符号组成。 for 和告警规则状态有关,一共有三种告警规则状态,下面会具体说下。 labels 是指在告警消息触发时加的告警标签,可作为后续告警消息处理使用。 annotations 就是具体告警内容,可以在内容中通过标签引用和指标样本值引用把标签值和样本值插入告警内容中。

告警规则状态:

Inactive :无任何报警,一切正常
Pending :已触发阈值,但未满足告警持续时间,也就是在告警规则中写的 for ,在 for 规定的时间内触发都不会发送给 AlertManager ,当 for 持续时间一过会立即发送给 AlertManager
Firing :已触发阈值且满足告警持续时间,告警发送给接收者

告警分级:

告警分级:告警规则文件中 labels 下的 severity 定义

critical(临界) :表示最严重的问题,需要立即采取紧急措施,通常涉及到重大的系统故障或安全问题。
high(高) :表示高级别的告警,需要尽快处理,通常是性能下降、服务不稳定等问题。
warning(警告) :表示一般警告,通常表示存在潜在的问题或异常情况,需要引起关注,但不需要立即采取紧急措施。
info(信息) :用于提供一般信息,通常不是告警,而是关于系统运行状态或事件的通知。
debug(调试) :用于调试目的,通常不会在生产环境中使用,而是用于开发和故障排除。

一般用前面: Critical 、 High 、 Warning 这三个,主要评估告警规则的严重程度,后续可以通过告警分级来处理告警信息,比如不同的告警级别路由发送给不同的机器人,夜莺中把告警分为(S1、S2、S3级别),告警分级可以根据各自组织实际情况去做定义。

下面演示下 Prometheus 下和 vmalert 下如何加载告警规则配置文件:

在 /etc/prometheus/rules/ 目录下新建 Prometheus 告警规则文件 alerts-prometheus.yml ,实现对 Prometheus 时序库的告警:


groups:
- name: prometheus.rules
  rules:
  - alert: PrometheusJobMissing
    expr: absent(up{job="prometheus"})
    for: 0m
    labels:
      severity: warning
    annotations:
      summary: Prometheus job missing (instance {{ $labels.instance }})
      description: "A Prometheus job has disappeared\n  VALUE = {{ $value }}\n  LABELS = {{ $labels }}"

  - alert: PrometheusTargetMissing
    expr: up == 0
    for: 0m
    labels:
      severity: critical
    annotations:
      summary: Prometheus target missing (instance {{ $labels.instance }})
      description: "A Prometheus target has disappeared. An exporter might be crashed.\n  VALUE = {{ $value }}\n  LABELS = {{ $labels }}"

  - alert: PrometheusAllTargetsMissing
    expr: sum by (job) (up) == 0
    for: 0m
    labels:
      severity: critical
    annotations:
      summary: Prometheus all targets missing (instance {{ $labels.instance }})
      description: "A Prometheus job does not have living target anymore.\n  VALUE = {{ $value }}\n  LABELS = {{ $labels }}"

  - alert: PrometheusTargetMissingWithWarmupTime
    expr: sum by (instance, job) ((up == 0) * on (instance) group_right(job) (node_time_seconds - node_boot_time_seconds > 600))
    for: 0m
    labels:
      severity: critical
    annotations:
      summary: Prometheus target missing with warmup time (instance {{ $labels.instance }})
      description: "Allow a job time to start up (10 minutes) before alerting that it's down.\n  VALUE = {{ $value }}\n  LABELS = {{ $labels }}"
  
  - alert: PrometheusConfigurationReloadFailure
    expr: prometheus_config_last_reload_successful != 1
    for: 0m
    labels:
      severity: warning
    annotations:
      summary: Prometheus configuration reload failure (instance {{ $labels.instance }})
      description: "Prometheus configuration reload error\n  VALUE = {{ $value }}\n  LABELS = {{ $labels }}"

  - alert: PrometheusTooManyRestarts
    expr: changes(process_start_time_seconds{job=~"prometheus|pushgateway|alertmanager"}[15m]) > 2
    for: 0m
    labels:
      severity: warning
    annotations:
      summary: Prometheus too many restarts (instance {{ $labels.instance }})
      description: "Prometheus has restarted more than twice in the last 15 minutes. It might be crashlooping.\n  VALUE = {{ $value }}\n  LABELS = {{ $labels }}"

接下来打开 Prometheus 主配置文件,编辑 prometheus.yml 文件,如下所示:

global:
  scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute.
  evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.
  # scrape_timeout is set to the global default (10s).

# Alertmanager configuration
alerting:
  alertmanagers:
    - static_configs:
        - targets:
          # 指定 Alertmanager 的服务所在主机与端口
          - 172.17.40.25:9093

# Load rules once and periodically evaluate them according to the global 'evaluation_interval'.
rule_files:
  # 指定告警规则文件所在目录路径,最终文件可以通过通配符匹配或者单独指定具体文件
  - "/etc/prometheus/rules/*.yml"
  # - "second_rules.yml"

# A scrape configuration containing exactly one endpoint to scrape:
# Here it's Prometheus itself.
scrape_configs:
  # The job name is added as a label `job=<job_name>` to any timeseries scraped from this config.
  - job_name: "prometheus"

    # metrics_path defaults to '/metrics'
    # scheme defaults to 'http'.

    static_configs:
      - targets: ["localhost:9090"]

配置修改完成后,我们可以在 Prometheus 服务主机上执行命令,实现配置的热加载:

curl -X POST http://localhost:9090/-/reload 

打开 Prometheus Web UI 查看配置规则是否加载成功:

上面是针对 Prometheus 架构,如果是使用 VictoriaMetrics 架构,我们就需要部署 vmalert 告警引擎,然后在 vmalert 中配置相关参数关联 VictoriaMetrics 时序库和 Alertmanager 组件。

打开 vmalert 启动配置参数,修改启动配置文件,在我的脚本中都是放在 /etc/victoriametrics/vmalert/ 目录下的 vmalert.conf ,如下所示:

# -rule 指定告警规则文件路径,可使用通配符匹配整个目录的告警规则文件
-rule=/etc/victoriametrics/vmalert/alerts.yml
# -datasource.url 指定数据源,也就是时序库 API 地址,这里填写 VictoriaMetrics 数据源
-datasource.url=http://172.17.40.25:8428
# -notifier.url 指定告警触发后消息通知地址,也就是 Alertmanager 地址
-notifier.url=http://172.17.40.25:9093
# -notifier.url=http://172.17.40.26:9093 如果 Alertmanager 是集群部署可以指定多个 Alertmanager 地址
# 远程写入兼容的时序库,主要应用在记录规则中使用,这里可以指定 VictoriaMetrics 时序库地址也可以指定 vmagent 地址
-remoteWrite.url=http://172.17.40.25:8428
# 重启后从时序库查询中恢复内存中的告警状态
-remoteRead.url=http://172.17.40.25:8428

浏览器打开 vmalert 我们可以直接访问 http://172.17.40.25:8880/ 可以发现 vmalert 正常打开,但是每次单独打开都比较麻烦,我们可以设置 VictoriaMetrics 时序库代理地址打开:

打开 VictoriaMetrics 时序库的启动配置文件,我的安装文档都是在 /etc/victoriametrics/single/ 目录下的 vmsingle.conf 下:


-storageDataPath=/var/lib/victoria-metrics-data
-retentionPeriod=90d
-httpListenAddr=:8428
-selfScrapeInterval=15s
-vmui.defaultTimezone=Local
# 这里添加 -vmalert.proxyURL 为 vmalert 的地址与端口即可
-vmalert.proxyURL=http://172.17.40.25:8880

然后我们重加载 VictoriaMetrics 后,直接在浏览器中打开 VictoriaMetrics 地址加 vmalert ,也就是 http://:8428/vmalert/ 既可直接访问 vmalert 的 Web UI ,如下所示:

接下来需要把告警规则文件写好,这里我写针对 VictoriaMetrics 、 vmagent 以及 vmalert 这三个组件的告警规则文件,然后加载告警规则文件:

在目录 /etc/victoriametrics/vmalert 下新建 rules 目录,然后修改启动配置文件:

-rule=/etc/victoriametrics/vmalert/rules/*.yml

解析来在目录 /etc/victoriametrics/vmalert/rules 下新建告警规则文件即可,如下所示:

# File contains default list of alerts for VictoriaMetrics single server.
# The alerts below are just recommendations and may require some updates
# and threshold calibration according to every specific setup.
groups:
  # Alerts group for VM single assumes that Grafana dashboard
  # https://grafana.com/grafana/dashboards/10229 is installed.
  # Pls update the `dashboard` annotation according to your setup.
  - name: vmsingle
    interval: 30s
    concurrency: 2
    rules:
      - alert: DiskRunsOutOfSpaceIn3Days
        expr: |
          vm_free_disk_space_bytes / ignoring(path)
          (
            rate(vm_rows_added_to_storage_total[1d])
            * scalar(
              sum(vm_data_size_bytes{type!~"indexdb.*"}) /
              sum(vm_rows{type!~"indexdb.*"})
             )
          ) < 3 * 24 * 3600 > 0
        for: 30m
        labels:
          severity: critical
        annotations:
          dashboard: "http://172.17.40.25:3000/d/wNf0q_kZk?viewPanel=73&var-instance={{ $labels.instance }}"
          summary: "Instance {{ $labels.instance }} will run out of disk space soon"
          description: "Taking into account current ingestion rate, free disk space will be enough only
            for {{ $value | humanizeDuration }} on instance {{ $labels.instance }}.\n
            Consider to limit the ingestion rate, decrease retention or scale the disk space if possible."

      - alert: DiskRunsOutOfSpace
        expr: |
          sum(vm_data_size_bytes) by(job, instance) /
          (
           sum(vm_free_disk_space_bytes) by(job, instance) +
           sum(vm_data_size_bytes) by(job, instance)
          ) > 0.8
        for: 30m
        labels:
          severity: critical
        annotations:
          dashboard: "http://172.17.40.25:3000/d/wNf0q_kZk?viewPanel=53&var-instance={{ $labels.instance }}"
          summary: "Instance {{ $labels.instance }} (job={{ $labels.job }}) will run out of disk space soon"
          description: "Disk utilisation on instance {{ $labels.instance }} is more than 80%.\n
            Having less than 20% of free disk space could cripple merge processes and overall performance.
            Consider to limit the ingestion rate, decrease retention or scale the disk space if possible."

      - alert: RequestErrorsToAPI
        expr: increase(vm_http_request_errors_total[5m]) > 0
        for: 15m
        labels:
          severity: warning
        annotations:
          dashboard: "http://172.17.40.25:3000/d/wNf0q_kZk?viewPanel=35&var-instance={{ $labels.instance }}"
          summary: "Too many errors served for path {{ $labels.path }} (instance {{ $labels.instance }})"
          description: "Requests to path {{ $labels.path }} are receiving errors.
            Please verify if clients are sending correct requests."

      - alert: RowsRejectedOnIngestion
        expr: rate(vm_rows_ignored_total[5m]) > 0
        for: 15m
        labels:
          severity: warning
        annotations:
          dashboard: "http://172.17.40.25:3000/d/wNf0q_kZk?viewPanel=58&var-instance={{ $labels.instance }}"
          summary: "Some rows are rejected on \"{{ $labels.instance }}\" on ingestion attempt"
          description: "VM is rejecting to ingest rows on \"{{ $labels.instance }}\" due to the
            following reason: \"{{ $labels.reason }}\""

      - alert: TooHighChurnRate
        expr: |
          (
             sum(rate(vm_new_timeseries_created_total[5m])) by(instance)
             /
             sum(rate(vm_rows_inserted_total[5m])) by (instance)
           ) > 0.1
        for: 15m
        labels:
          severity: warning
        annotations:
          dashboard: "http://172.17.40.25:3000/d/wNf0q_kZk?viewPanel=66&var-instance={{ $labels.instance }}"
          summary: "Churn rate is more than 10% on \"{{ $labels.instance }}\" for the last 15m"
          description: "VM constantly creates new time series on \"{{ $labels.instance }}\".\n
            This effect is known as Churn Rate.\n
            High Churn Rate tightly connected with database performance and may
            result in unexpected OOM's or slow queries."

      - alert: TooHighChurnRate24h
        expr: |
          sum(increase(vm_new_timeseries_created_total[24h])) by(instance)
          >
          (sum(vm_cache_entries{type="storage/hour_metric_ids"}) by(instance) * 3)
        for: 15m
        labels:
          severity: warning
        annotations:
          dashboard: "http://172.17.40.25:3000/d/wNf0q_kZk?viewPanel=66&var-instance={{ $labels.instance }}"
          summary: "Too high number of new series on \"{{ $labels.instance }}\" created over last 24h"
          description: "The number of created new time series over last 24h is 3x times higher than
            current number of active series on \"{{ $labels.instance }}\".\n
            This effect is known as Churn Rate.\n
            High Churn Rate tightly connected with database performance and may
            result in unexpected OOM's or slow queries."

      - alert: TooHighSlowInsertsRate
        expr: |
          (
             sum(rate(vm_slow_row_inserts_total[5m])) by(instance)
             /
             sum(rate(vm_rows_inserted_total[5m])) by (instance)
           ) > 0.05
        for: 15m
        labels:
          severity: warning
        annotations:
          dashboard: "http://172.17.40.25:3000/d/wNf0q_kZk?viewPanel=68&var-instance={{ $labels.instance }}"
          summary: "Percentage of slow inserts is more than 5% on \"{{ $labels.instance }}\" for the last 15m"
          description: "High rate of slow inserts on \"{{ $labels.instance }}\" may be a sign of resource exhaustion
            for the current load. It is likely more RAM is needed for optimal handling of the current number of active time series.
            See also https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3976#issuecomment-1476883183"

      - alert: LabelsLimitExceededOnIngestion
        expr: increase(vm_metrics_with_dropped_labels_total[5m]) > 0
        for: 15m
        labels:
          severity: warning
        annotations:
          dashboard: "http://172.17.40.25:3000/d/wNf0q_kZk?viewPanel=74&var-instance={{ $labels.instance }}"
          summary: "Metrics ingested in ({{ $labels.instance }}) are exceeding labels limit"
          description: "VictoriaMetrics limits the number of labels per each metric with `-maxLabelsPerTimeseries` command-line flag.\n
           This prevents ingestion of metrics with too many labels. Please verify that `-maxLabelsPerTimeseries` is configured
           correctly or that clients which send these metrics aren't misbehaving."

重新加载 vmalert 即可看到告警规则文件加载成功:

告警通知方也一样加载出来了:

告警规则评估触发告警,但还未达到持续时间,未立马发送告警,如果是已发送的告警,状态会呈现 Firing

那肯定很多人问我告警知道怎么弄了后,但是告警规则表达式有点难搞,我应该如何写,其实很多告警规则在开源平台中已经写好了,我们可以直接套用,具体可以看如下所示:

告警规则:(https://samber.github.io/awesome-prometheus-alerts/)

下一篇具体讲讲 Alertmanager 的使用。

参考

标签:架构,labels,instance,Prometheus,规则,告警,vmalert
From: https://www.cnblogs.com/o-O-oO/p/18523618

相关文章

  • 网络安全架构:如何理解P2DR模型
    1.P2DR模型是什么P2DR模型是由美国ISS公司提出的,它是动态网络安全体系的代表模型。根据风险分析产生的安全策略描述了系统中哪些资源要得到保护,以及如何实现对它们的保护等。P2DR模型包括四个主要部分:Policy(安全策略)、Protection(防护)、Detection(检测)和Response(响应)。......
  • 【安全架构】权限控制模型
    原创大袤宏图不同的权限模型提供了灵活的访问控制策略,本文绘制了不同模型的ER图,探讨这些模型的原理及适用场景。一、访问控制列表模型访问控制列表模型(AccessControlList,ACL)基于资源的访问控制列表,每个资源都有一个列表记录哪些用户可以对其进行哪些操作,适用于小型系......
  • 分布式追踪与告警系统:保障分布式系统稳定运行的利器
    在复杂的分布式系统环境中,分布式追踪与告警系统扮演着至关重要的角色。它们能够帮助开发人员和运维人员快速定位问题、提高系统的可靠性和稳定性。那么,分布式追踪与告警系统的作用是什么?又该如何设计实现呢?一、分布式追踪与告警系统的作用1.快速定位问题在分布式系统中,一个请......
  • 用户注册案例--mvc架构的实现
    用户注册案例--浅谈servlet本案例为用户注册案例,同时介绍一部分之前经常用得到servlet的知识.servlet是javaEE的技术规范之一.基于MCV架构的分析1.dao层在mapper代理文件中写入insert的操作,对于成功注册的用户直接写入数据库,以及查询操作判断用户名是否已经存在UsergetUs......
  • 科普文:软件架构Linux系列之【图解存储 IO性能优化与瓶颈分析】
    概叙科普文:软件架构Linux系列之【Linux的文件预读readahead】-CSDN博客科普文:软件架构Linux系列之【并发问题的根源:CPU缓存模型详解】-CSDN博客从上面冯诺依曼结构下的cpu、内存、外存之间的延迟就可以看出,磁盘I/O性能的发展远远滞后于CPU和内存,因而成为现代计算机系统的......
  • LPC1100 系列_2.架构与硬件特性
    2.架构与硬件特性2.1架构概述LPC1100系列是NXP公司基于ARMCortex-M0内核的微控制器系列。ARMCortex-M0是ARM公司推出的一款低功耗、高性能的32位RISC处理器内核,适用于嵌入式系统和微控制器应用。LPC1100系列微控制器采用了Cortex-M0内核,具有以下主要......
  • 架构师之路-学渣到学霸历程-43
    NGXIN综合理解Location的作用今天早上我们一块分享了nginx的location的意义,大概也都了解了;今晚我们整理一个综合的实验来验证这个location的具体作用;1、部署综合实验;结合所有的修饰符进行测试;这里在开始之前,添加一个echo模块;实验之前最好就是恢复成最初的快照或者是你......
  • 二十三、Mysql8.0高可用集群架构实战
    文章目录一、MySQLInnoDBCluster1、基本概述2、集群架构3、搭建一主两从InnoDB集群3.1、安装3个数据库实例3.2、安装mysqlrouter和安装mysqlshell3.2.1、安装mysql-router3.2.2、安装mysql-shell3.3、InnoDBCluster初始化3.3.1、参数及权限配置预需求检测3.3.2、初......
  • 静态库、动态库、framework、xcframework、use_frameworks!的作用、关联核心SDK工程和
    1.1库的概念库:程序代码的集合,编译好的二进制文件加上头文件供使用,共享程序代码的一种方式。1.2库的分类根据开源情况分为:开源库(能看到具体实现)、闭源库(只公开调用的的接口,是编译后的二进制文件,看不到具体实现,使用时链接即可。)闭源库分为:动态库.td(之前叫.dylib)或.framework......
  • 软件架构中对前后端分离的学习
    前后端分离架构目录前后端分离架构前端后端前端MVVM架构MVVM(Model-View-ViewModel)架构模式是一种广泛应用于软件开发中的设计模式,特别是在现代前端开发和移动应用开发中。它旨在通过将应用程序分为三个核心部分——模型(Model)、视图(View)和视图模型(ViewModel)——来简化用户界面......