在微服务架构下基于 Prometheus 构建一体化监控平台的最佳实践

本文涉及的产品
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
简介: 个人认为将来可观测性一定是标准化且由开源驱动的。现在整个软件架构体系变得越来越复杂,我们要监控的对象越来越多,场景也越来越广。封闭的单一厂商很难面面俱到的去实现全局可观测能力,需要社区生态共同参与,用开放、标准的方法来构建云原生可观测性。

作者 | 图恩


随着 Prometheus 逐渐成为云原生时代的可观测事实标准,那么今天为大家带来在微服务架构下基于 Prometheus 构建一体化监控平台的最佳实践和一些相关的思考,内容主要包括以下几个部分:


  • 微服务、容器化技术演进的监控之痛
  • 云原生时代,为什么是 Prometheus
  • 阿里云 Prometheus 在微服务场景的落地实践
  • 大规模落地实践挑战和解决方案
  • 云原生可观测性的发展趋势和展望


微服务、容器化技术演进的监控之痛

image.gif1.png

1、第一个挑战:监控对象动态化


容器化部署使得我们的监控对象变得动态化。随着 K8s 这种服务编排框架大规模落地,应用部署单元从原来的主机变成一个 Pod。Pod 在每次发布时销毁重建,IP 也发生变化。在微服务体系下,我们讲究快速迭代、持续集成,这使得发布变得愈发频繁,Pod 生命周期变得非常短暂。据统计,平均每个 Pod 的生命周期只有两三天,然后就会被销毁,再去重建。而且随着 DevOps 普及,负责应用发布的角色发生变化,应用发布变得弱管控和更加敏捷,一个应用会不断的进行滚动发布,从而达成快速迭代的目标。


所以说,随着软件生产流程的变化和相关技术的成熟,我们的监控对象处于一个不断频繁变化的状态之中。


第二个挑战:监控层次/对象多样化


首先,Kubernetes 相关的 kube 组件以及容器层是我们必须要监控的新对象。其次,微服务拆分之后,以及行业在中间件,DB 等领域的精细化发展使我们发现依赖的 PaaS 层组件越来越多样化,对这些应用强依赖的 PaaS 组件也需要进行监控。最后就是多语言。当微服务拆分之后,每个团队都可以选择自己擅长的语言去进行应用开发。这就造成一个问题,即这些应用的监控指标需要支持各种语言的 client library 才能去生产和暴露。


第三个挑战:监控内容复杂化


监控内容复杂化来源于以下几点,第一个是复杂的应用依赖。第二个是在高度分布式环境下,我们需要非常复杂,细颗粒度的指标才能描绘整个系统状态。
image.gif2.png

上面这张图我们可以更直观的感受到以上挑战是如何产生的,左边是传统单体应用的部署架构,右边是微服务部署的架构。原来只需监控一个应用对象,现在变成了几十、上百个且不停的动态的发布,IP 地址不停变化。传统监控工具可能会采用静态配置的方式去发现这些监控目标。但在微服务场景下,这种方式已经无法实施。原来单体应用可能只需要依赖 MySQL 就可以了。但现在依赖的组件越来越多。传统监控工具是没有办法全面支持这种庞大的监控需求,且传统监控工具缺乏在容器层的监控能力。


为了解决上述问题,我们发现 Prometheus 或许是一个理想的解决方案。


云原生时代,为什么是 Prometheus

image.gif3.png

  • 动态化:Prometheus 具有先发优势。Kubernetes 诞生之初,标配监控工具就是 Prometheus,天然契合 Kubernetes 的架构与技术特征,可以去自动发现监控目标。在大规模、监控目标不停变化的监控场景下,根据实践经验,主动拉取采集是一种比较好的实现方式,可以避免监控目标指标漏采,监控目标需要解决维护采集点配置以及 push 模式实现成本较大等一系列问题。其次,动态化的容器指标通过 Kubernetes 的 Kubelet/VK 组件采集,它们天然采用 Prometheus 格式生产和暴露指标数据。


  • 多样化:因为 Kubernetes 有很多的控制面组件,比如 API server 等组件,也是天然通过 Prometheus 数据格式来暴露监控指标,这使得 Prometheus 采集这些组件的监控指标非常标准和简单。其次,Prometheus 是一个开放性社区,有 100+ 个官方或非官方 exporter 可以供大家使用。比如,你想监控数据库、消息队列、分布式存储、注册中心、网关,各种各样的 exporter 开箱即用,可以把原组件非 Prometheus 标准的数据格式转化成 Prometheus 的数据格式供采集器进行采集。再者,Prometheus 支持 go、Python、Java 等 20 多种语言,可以非常简单的为应用生成和暴露监控的 metric。最后,Prometheus 可扩展性非常强,如果上面都满足不了应用需求,它也有强大的工具可以帮助业务方轻松的写出自己的 exporter。


  • 复杂化:Prometheus 定义了一个多维模型。多维模型可以简单理解为我可以给任何事情都打上标签,通过标签的方式来描述对象的系统状态。多维模型听起来比较简单,但很多监控工具最开始无法用这种方式去描述它的监控目标。通过多维模型,我们可以很容易刻画出整个监控目标的复杂状态,还可以刻画出应用之间的依赖关系。

4.png

其次,Prometheus 实现了一种称为 PromQL 的查询语言,非常强大,可以将复杂的指标进行过滤、聚合、计算。它内置有 20-30 种计算函数和算子,包括常见的累加,差值,平均值、最大最小值,P99,TopK 等,可以基于这些计算能力直接绘制指标视图和配置告警,这可以省去原本需要的代码开发工作,非常容易的得到想要的业务结果。


可以看到上图中一个真实的 PromQL 语句,http_request_duration_seconds_bucket 是一个 Histogram 类型的指标,它有多个 bucket,通过上面的 PromQL 语句,不用编写任何代码就可以计算出 RT 超过 500ms 和 1200ms 的请求占比,从而得出 Apdex Score,评价某个接口的服务能力。

Prometheus 落地实践方案


接下来,我们看一个完整的落地实践方案。其核心就是如何围绕 Prometheus 来构建可观测平台,也就是如何把描述系统状态的各个层次的指标数据都汇聚到 Prometheus 数据监控数据平台上。


之前我们几乎是不可能完成这种将各类指标进行汇聚的工作的。因为每个监控工具专注的领域不一样,数据格式不一样,工具之间的数据无法打通,即使汇聚在一起也无法产生 1+1>2 的效果。


但通过 Prometheus 可以把 IaaS、PaaS 层的各种组件的监控指标都汇聚在一起。如果需要采集一些业务指标、应用的健康状态、云上应用所依赖的云产品是否正常,Kubernetes 组件有没有在正常运行,容器的 CPU,内存水位是否符合预期,Node 节点 tcp 连接数分配有没有风险,或者需要将 tracing 转成 metric,log 转成 metric,甚至一些 CI/CD 事件想要和核心监控视图关联起来,从而快速发现到底监控视图的数据异常是不是由某次变更引起的。我们都有成熟的工具以将以上描述我们系统运行状态的指标采集到 Prometheus 平台。


另外,我们还可以把一些把无意义的计算单元,通过标记的方式标记成有意义的业务语义,然后把这些指标也汇聚到 Prometheus 上面来。


所以这里的核心思想就是打破数据边界,把所有能够真实反映我们当前运行系统状态的指标汇聚在一起,无代码编写成本,就可以将这些数据关联,生产出有效的监控视图和告警。在具体实施方面我们总结出三个层次,以便更好的实施。
image.gif5.png

第一个层次:从业务视角定义核心目标首先,我们需要定义核心目标,这些监控的指标一定是为业务服务,指标本身并无实际意义,只是一个数值,只有确定的业务目标才能赋予这些指标生命力。


第二个层次:聚焦核心指标&提供角色视图在制定核心目标后,我们需要确定核心目标可以被量化。需要特别注意,核心指标一定是动态变化的,因为微服务的特点就是要不停快速迭代。今天可能还没有依赖某组件,可能下个迭代就依赖了,如果你没有被依赖组件的指标,会非常痛苦。因为没有办法通过这些核心指标去完整映射核心目标到底有没有异常。另外,需要根据角色去提供视图,一个组织里不同角色关心的视图是不一样的,当出现问题时,角色视图可以帮助更快地排查问题。


第三个层次:全量收集&提前定义&提前聚合/过滤想要实现上面两层的核心基础其实就是是全量收集,即能采集的指标一定要应采尽采。全量采集的技术基础是 metric 指标的存储成本是相对于 log 还有 trace 而言最小,即使全量采集也不会让成本膨胀太多,却能让你的核心目标度量效果即使在快速迭代的过程也不受损。


在全量采集前提下,我们要尽早去聚合或过滤掉高基数的 label。高基数问题是时序场景常遇到的问题,我们会看到采集的容器层指标带一些 Pod ID,但这种 label 是没有实际业务意义,再比如 URL path 会发散,带上了 uid 或者 order id 之类的业务 id,我们可能关心的是整个接口的健康状态,而不是某一个 path 的,这时就需要把 path 聚合一下,通过这种聚合可以减少存储成本,提升存储稳定性,也不会影响核心目标的达成。


另外,在我们采集指标时,尽量找出多层之间有关联关系的 label。比如在采集一些应用指标时,我们可以通过 Prometheus relabel 的功能把 pod name 一起采集过来,这样就可以直接建立应用和容器层的关联视图,在排查问题时通过关联分析减少 MTTD 时间。
image.gif6.png

接下来,我们讲一下如何利用 Tag 细化监控范围。Pod 本身是没有实际的业务语义的,但打上一些标签后,比如 某个 Pod 属于登录服务还是支付服务,属于生产环境还是测试、预发环境。就使得 Pod 这个计算单元有了实际业务语义。当有了业务语义之后,就可以配置出低噪音的告警。比如当我们支付成功率低于三个 9 的时候,认为核心目标已经受损了,需要马上告警出来。告警之后通过 Tag,就可以迅速定位到底是生产环境还是测试环境的问题。


如果是生产环境就要马上去处理,可以定位到底在北京哪个可用区,告警的原因到底是因为哪个服务的接口出现了异常。所以说告警的有效性也非常重要,因为我们在实践中都知道,如果告警一直是处于流量轰炸状态,告警最后就会变得没有意义。


通过 Tag,我们可以提供场景化的视图,假设老板想要去捞取生产环境下支付服务 CPU 利用率 Top5 的 Pod 列表,通过 Tag 加上 PromQL 语言,我们一条语句就可以把这个视图马上捞取出来。
image.gif7.png

讲一个真实的电商场景如何通过 Prometheus 构建统一监控平台的。从图中看到,电商系统的主体已经迁移到阿里云上,分两部分,一部分是在 Kubernetes 集群,另一部分是在 ECS 的虚机集群。每个服务依赖不同的中间件或 DB 组件,有些依赖云产品,还有一些依赖原有自建 DB 或组件。


我们可以看到使用之前的监控方案,很难实现全面的监控指标采集。应用层的同学通常只关心应用层正不正常,有哪些指标可以反映健康状态,他们可能会选择一些 APM 工具,或者其使用的开发语言相关的特定监控工具,比如使用 Spring Boot 的开发同学会通过 Actuator 监控应用状态。而负责 SRE 的同学通常会关心基础设施正不正常,怎么监控 Kubernetes 组件,容器的黄金指标水位是否正常。他们可能是不同的部门,会通过不同监控工具实现不同的监控系统,这些系统之间是割裂的,所以你去排查一个问题,我们都有一个体感,很多时候可能是网络问题,有些时候可能是某一台主机有问题,影响了应用性能,如果只看应用层,会觉得应用代码没有问题,但是有了这样一个全局的视图,就会很快排查到影响你应用的问题点到底在哪。


我们可以通过 node exporter 去采集 VM 层面 CPU、内存、 IO 黄金三指标,也可以通过云监控的 exporter,监控应用依赖的云服务健康状态。当然 Kubernetes 和容器这一层 Prometheus 提供的能力更加全面,比如 kube-state-metrics 可以监控 Kubernetes 的元信息,通过 cadvisor 可以采集容器运行时指标,还有各种 kube 组件的监控,动动手指头配置几个采集 job,或者直接用开源或者云产品,开箱即用。


另外我们团队提供了 ARMS APM,以无侵入的方式去生产,暴露应用的指标,全面监控应用健康状态。如果不能满足需求的话,你也可以使用 Prometheus 官方的多语言 client library 或者三方提供的一些 client library 很方便的去生产和暴露你的指标。还有很多官方或者三方的 exporter 可以用来监控 mysql,redis,nginx,kafka 等 DB 和中间件。除此之外,针对特定语言开发,比如 JVM,还有 JMX exporter 可以使用,查看堆内存有使用正不正常,GC 是不是在频繁的发生。


通过 Prometheus 及其生态,规范化,统一的指标可以很容易的汇聚在一起,接下来我们就可以定义 SLO。在电商系统场景下,以支付成功率为例,这是很要命的一个指标,如果你的支付成功率低了,可能今天 gaap 就会损失很大。通过 SLI 去准确衡量核心目标是否受损。比如 SLI 是应用层面的接口 error 这种指标,可能还需要关注应用运行的容器,其内存、CPU 是否在健康水位,如果超出健康水位,这可能就是预警,在接下来某段时间就会发生故障。有了这些指标,使用 Grafana 和 AlertManager,就可以轻松完成可视化和告警配置,在应用异常不符合预期时及时告警,快速定位问题范围。
image.gif8.png

如图,我们可以看到 Grafana 全面的展示了应用层,依赖的中间件、容器层、以及主机层的全量视图。
image.gif9.png

基于 Prometheus,我们还可以衍生出非常多的应用。举个例子,Kubernetes 上的网络拓扑是很难刻画出来的,但基于 epbf 技术可以采集 Kubernetes 工作负载间的各种关联关系,可以把这种关联关系转换成 metrics,结合 Prometheus 采集 Kubernetes 集群元信息指标,可以非常方便的刻画出整个网络拓扑,方便定位集群内的网络问题。我们已经提供了相关的云产品,并且由于基于 ebpf 实现,也是多语言适用且完全无侵入的。
image.gif10.png

还有一个例子和资源使用成本相关,由于云原生架构的弹性和动态化,我们很难计量各个应用消耗了多少资源,付出多少成本。但通过 Prometheus 加上自己的账单系统,定义好每个资源的计费,很容易去刻画出来一个部门和各个应用的成本视图。当应用出现资源消耗不合理时,我们还可以给出优化建议。

大规模落地实践挑战和解决方案


接下来我们讨论下,落地 Prometheus 有哪些技术上的挑战以及相应的解决方案,这其中包括:

  • 多云、多租场景
  • 规模化运维
  • 可用性降低,MTTD 和 MTTR 时间长
  • 大数据量、大时间跨度查询性能差
  • GlobalView
  • 高基数问题


image.gif11.png

为了应对以上挑战,我们对采集和存储进行了分离。这样的好处就是采集端做的尽量轻,存储端可用性做的足够强,这样就可以支持公有云、混合云、边缘或者多元环境。在分离之后,我们针对采集、存储分别进行可用性优化,并保持与开源一致的使用方式。
image.gif12.png
接下来,可以看到部署拓扑图,采集端就是部署在业务方的集群里面,所以天然就是多租的,存储端我们用超大规模 Kubernetes 集群进行多租部署,计算存储分离,这些租户共享资源池,在容器层物理隔离,通过云盘和 NAS 存储索引和指标数值文件,可以保证一定弹性和单租户水平扩容能力,同时我们对每个租户使用的资源又有限制,避免某个租户对资源的消耗影响到其他租户。

最后为了解决多租问题,我们做了中心化的元信息管理,保证租户数据的最终一致性。在进行故障调度时,可以通过中心化的元信息管理,非常方便进行故障转移。改造之后,当 node 发生故障,在一分钟之内就可以恢复,Pod 发生故障,10s 可以恢复。

13.png

我们对采集侧进行了可用性改造,因为开源 Prometheus 是一个单体应用,单副本是无法保障高可用的。我们把采集端改造成一个多副本模型,共享同样的采集配置,根据采集量将采集目标调度到不同的副本上。当采集量发生变化或有新增的采集目标时,会计算副本的采集水位,进行动态扩容,保证采集可用性。


在存储侧,我们也做了一些可用性保障措施。在写的时候,可以根据时间线的数量来动态扩容存储节点,也可以根据索引所使用的 pagecache 有没有超限来进行扩容。在读的时候,也会根据时间线数量,还有时间点的数量进行限制,保证查询和存储节点的可用性。另外我们知道时序数据库都会做压缩,如果集中压缩的话,IO 抖动非常厉害,所以我们做了一个调度算法把节点进行分批压缩,这样就可以减少抖动。
image.gif14.png

在大数据量查询性能方面,我们可以看一个比较典型的 PromQL 查询案例。总数据量有 6 亿个时间点,600 万个时间线。如果使用开源系统进行查询,要占用 25G 的带宽,查询耗时可能是三分钟。但我们做了一些优化,比如 DAG 执行优化,简单讲就是对执行语句进行解析,如果发现有重复的子查询,就去重,然后并行化查询降低 RT。


还有算子下推,将一些算子计算逻辑从查询节点下推到存储节点实现,可以减少原始指标数据的传输,大幅度降低 RT。


针对大促场景,应用开发者或 SRE 在大促之前频繁查询大盘,执行的 PromQL 除了起止时间有些微差别,其他都是一样的,因此,我们做了场景化设计,将计算结果缓存,对超出起止时间缓存范围的部分进行增量查询。


最后通过 Gorilla 压缩算法结合流式响应,避免批量一次性的加载到内存里面进行计算。

15.png

经过优化之后,对大规模、大数据量的查询性能优化到 8~10 秒,并对 70% 场景都可以提升 10 倍以上性能。image.gif这一部分简单聊一下安全问题。云上应用是非常注重安全的,有些指标数据比较敏感,可能不希望被无关业务方抓取到。因此,我们设计了租户级别的鉴权机制,会对生成 Token 的密钥进行租户级别加密,加密流程是企业级安全的。如果出现 Token 泄露,可以收敛影响范围到租户级别,只需要受影响的租户换一下加密密钥生成新的 Token,废弃掉旧 Token 就可以消除安全风险。


除了以上部分,我们也做了一些其他技术优化,下面简单介绍一下。


  • 高基数问题

通过预聚合,把发散指标进行收敛,在减少存储成本的同时,一定程度上缓解高基数问题。另外我们做了全局索引的优化,将时间线索引拆分到 shard 级别,当 shard 过期之后,索引也会随之删除,减少了短时间跨度查询时需要加载的时间线数量。


  • 大时间跨度查询

实现 Downsampling,牺牲一定精度来换取查询性能与可用性。


  • 采集能力

提升单副本采集能力,可以减少 agent 用户侧的资源消耗。
image.gif16.png

最后是阿里云 Proemtheus 监控与开源版本的对比。


在可用性方面,开源版本到了百万级时间线,内存消耗会出现暴涨,基本上是不可用的。而且因为是单副本,如果出现一些网络异常或者所在的宿主机出现问题,整个监控系统就是不可用的。


虽然开源的 Thanos、Cortex 做了一些可用性增强,但总体来讲他们并没有完全解决可用性问题。我们做了采集存储架构分离,采集存储端理论上可以无限水平扩容,可用性比较高。而且存储时长理论上也没有上限,而开源版本存储一个月指标,时间线就会膨胀得非常厉害,查询和写入基本上都是不可用的。

云原生可观测性的发展趋势和展望


最后聊一聊云原生可观测性的发展趋势,个人认为将来可观测性一定是标准化且由开源驱动的。现在整个软件架构体系变得越来越复杂,我们要监控的对象越来越多,场景也越来越广。封闭的单一厂商很难面面俱到的去实现全局可观测能力,需要社区生态共同参与,用开放、标准的方法来构建云原生可观测性。

17.png

我们可以看一下 metric、log、tracing 的关系,这三者在不同维度上从低到高,各有特长。


在告警有效性上来说,metric 是最有效的,因为 metric 最能真实反映系统状态,不会因为偶发抖动造成告警轰炸,告警平台完全失效的问题。但在排查问题的深度上,肯定还是需要去看 tracing 和 log。另外在单条记录存储成本上,metric 远低于 tracing 和 log。所以基于此,个人认为将来会以 metric 为接入点,再去关联 tracing 和 log,tracing 和 log 只在 metric 判定系统异常时才需要采集存储。这样就可以既保证问题抛出的有效性,又能降低资源使用成本,这样的形态是比较理想合理,符合未来发展趋势的。


更多 ARMS 相关信息,钉钉扫码进群了解!


18.png

相关文章
|
3天前
|
消息中间件 监控 API
构建微服务架构:从理论到实践的全面指南
本文将深入探讨微服务架构的设计原则、实施步骤和面临的挑战。与传统的单体架构相比,微服务通过其独立性、可伸缩性和灵活性,为现代应用开发提供了新的视角。文章将介绍如何从零开始规划和部署一个微服务系统,包括选择合适的技术栈、处理数据一致性问题以及实现服务间通信。此外,我们还将讨论在迁移至微服务架构过程中可能遇到的技术和组织挑战,以及如何克服这些难题以实现顺利过渡。
|
1天前
|
监控 API 数据库
构建高效后端:微服务架构的实战指南
【6月更文挑战第14天】在数字化浪潮下,后端开发面临着前所未有的挑战和机遇。本文将深入探讨微服务架构的设计理念、实现方式及其在现代软件开发中的重要性,为读者提供一份全面而实用的微服务实战手册。
|
2天前
|
运维 Cloud Native 开发者
云原生技术:构建未来软件架构的基石
【6月更文挑战第13天】随着云计算的不断演进,云原生技术已成为推动现代软件开发、部署和运维的关键力量。本文深入探讨了云原生的核心概念、优势以及它在企业中的应用,旨在揭示如何借助云原生技术实现更高效、灵活和可靠的软件解决方案。
12 2
|
5天前
|
消息中间件 缓存 负载均衡
构建高效可靠的后端系统架构
本文将探讨如何构建一种高效可靠的后端系统架构,以满足不断增长的技术需求和用户期望。我们将重点介绍架构设计原则、分布式系统、容错机制和性能优化等关键概念,并提供实际案例和最佳实践,帮助开发者在后端开发中取得成功。
|
9天前
|
弹性计算 运维 监控
构建高效可扩展的后端服务架构
在当今数字化时代,构建高效可扩展的后端服务架构是企业成功的关键之一。本文将探讨如何设计和实施一种可靠、高性能的后端架构,以满足不断增长的用户需求和复杂的业务逻辑。通过采用合适的技术栈、优化数据库设计、实现弹性伸缩和监控等关键策略,我们能够打造出稳定可靠、高效可扩展的后端服务系统。
|
10天前
|
运维 Cloud Native 持续交付
构建未来:云原生架构在企业数字化转型中的关键作用
【5月更文挑战第37天】 随着企业加速迈向数字化,云原生架构已成为实现敏捷性、可扩展性和创新的基石。本文将探讨云原生技术如何赋能组织快速响应市场变化,优化资源利用,并最终促进业务增长。通过深入分析云原生的核心组件,如容器化、微服务和持续集成/持续部署(CI/CD),以及它们如何协同工作以提高开发效率和运维灵活性,我们将揭示企业如何利用这些技术来构建一个更加灵活和弹性的IT环境。
|
11天前
|
运维 Kubernetes 持续交付
构建高效后端:微服务架构的设计与实践
本文深入探讨了微服务架构的设计原则和实践方法,旨在为读者提供一套完整的微服务开发指南。通过分析微服务的核心优势,如灵活性、可扩展性与独立部署能力,文章详细阐述了如何有效规划服务边界、选择合适的通信协议以及确保服务的高可用性和弹性。此外,还讨论了在微服务实施过程中可能遇到的挑战,包括数据一致性和服务发现机制,以及如何通过现代技术栈和最佳实践来克服这些挑战。
|
16天前
|
Kubernetes 负载均衡 开发者
构建高效后端服务:从微服务到容器化部署
【5月更文挑战第31天】本文深入探讨了现代后端开发中的关键概念,包括微服务的架构设计、容器化技术的运用以及它们如何共同提升应用的可扩展性、可靠性和性能。通过具体案例分析,我们将揭示这些技术是如何在实际开发中被实施的,并讨论它们对后端开发流程的影响。
|
16天前
|
消息中间件 数据库 网络架构
构建高效后端:微服务架构的优化策略
【5月更文挑战第31天】在这篇文章中,我们将深入探讨如何通过采用微服务架构来提升后端开发的效率和性能。我们将分析微服务架构的关键优势,并讨论如何克服实施过程中的挑战。通过具体的案例研究,我们将展示如何优化微服务架构以实现最佳的性能和可维护性。无论你是后端开发的新手还是经验丰富的专业人士,这篇文章都将为你提供有价值的见解和实用的技巧。
|
1月前
|
编解码 Prometheus 运维
Prometheus 的监控方法论
【1月更文挑战第24天】