Linux性能优化-总结

目录

系统监控的综合思路

应用监控综合思路

分析性能问题的步骤

性能优化的步骤

性能工具速查


系统监控的综合思路

USE方法
USE(Utilization Saturation and Errors)
USE法把系统资源的性能指标,简化成了三个类别,使用率,饱和度,错误数

  • 使用率,表示资源用于服务的时间 或者容量的百分比,100%的使用率,笔试容器已经用尽或者全部时间都用于服务
  • 饱和度,表示资源的繁忙程度,通常与等待队列的长度相关,100%的饱和度,表示资源无法接受更多的请求
  • 错误数,表示发生错误的事件个数,错误数越多,表明系统的问题越严重

这三个类别的指标,涵盖了系统资源的常见性能瓶颈,所以常被用来快速定位系统资源的性能瓶颈,无论是对CPU,内存,
磁盘和文件系统,网络等硬件资源,还是对文件描述符,连接数,连接跟综数等软件资源,USE方法都可以快速定位,
是哪一种资源出现了性能瓶颈
下面是常见的性能指标

不过,需要注意的是,USE方法只关注能体现系统资源性能瓶颈的核心指标,但这并不是说其他指标不重要。诸如系统日志、进程资源使用量、缓存使用量等其他各类指标,也都需要我们监控起来。只不过,它们通常用作辅助性能分析,而 USE 方法的指标,则直接表明了系统的资源瓶颈。

监控系统
USE法之后,就是建立监控系统,把这些指标保存下来;然后,根据这些监控到的状态,自动分析和定位大致的瓶颈来源;最后,再通过告警系统,把问题及时汇报给相关团队处理。

可以看出,一个完整的监控系统通常由数据采集、数据存储、数据查询和处理、告警以及可视化展示等多个模块组成。所以,要从头搭建一个监控系统,其实也是一个很大的系统工程。

已经有大量现成的监工工具,最常见的 Zabbix、Nagios、Prometheus 等等。
如下图所示,就是 Prometheus 的基本架构:

第一个是数据采集模块。最左边的 Prometheus targets 就是数据采集的对象,而 Retrieval 则负责采集这些数据。Prometheus 同时支持 Push 和 Pull 两种数据采集模式。

  • Pull 模式,由服务器端的采集模块来触发采集。只要采集目标提供了 HTTP 接口,就可以自由接入(这也是最常用的采集模式)。
  • Push 模式,则是由各个采集目标主动向 Push Gateway(用于防止数据丢失)推送指标,再由服务器端从 Gateway 中拉取过去(这是移动应用中最常用的采集模式)。
  • 由于需要监控的对象通常都是动态变化的,Prometheus 还提供了服务发现的机制,可以自动根据预配置的规则,动态发现需要监控的对象。这在 Kubernetes 等容器平台中非常有效。

第二个是数据存储模块。为了保持监控数据的持久化,图中的 TSDB(Time series database)模块,负责将采集到的数据持久化到 SSD 等磁盘设备中。TSDB 是专门为时间序列数据设计的一种数据库,特点是以时间为索引、数据量大并且以追加的方式写入。

第三个是数据查询和处理模块。刚才提到的 TSDB,在存储数据的同时,其实还提供了数据查询和基本的数据处理功能,而这也就是 PromQL 语言。PromQL 提供了简洁的查询、过滤功能,并且支持基本的数据处理方法,是告警系统和可视化展示的基础。

第四个是告警模块。右上角的 AlertManager 提供了告警的功能,包括基于 PromQL 语言的触发条件、告警规则的配置管理以及告警的发送等。不过,虽然告警是必要的,但过于频繁的告警显然也不可取。所以,AlertManager 还支持通过分组、抑制或者静默等多种方式来聚合同类告警,并减少告警数量。

最后一个是可视化展示模块。Prometheus 的 web UI 提供了简单的可视化界面,用于执行 PromQL 查询语句,但结果的展示比较单调。不过,一旦配合 Grafana,就可以构建非常强大的图形界面了。

Prometheus的统计图

应用监控综合思路

对系统资源的监控,USE 法简单有效,却不代表其适合应用程序的监控。比如即使在 CPU 使用率很低的时候,也不能说明应用程序就没有性能瓶颈。因为应用程序可能会因为锁或者 RPC 调用等,导致响应缓慢。

所以,应用程序的核心指标,不再是资源的使用情况,而是
请求数、错误率和响应时间
这些指标不仅直接关系到用户的使用体验,还反映应用整体的可用性和可靠性。

  1. 是应用进程的资源使用情况,比如进程占用的 CPU、内存、磁盘 I/O、网络等。使用过多的系统资源,导致应用程序响应缓慢或者错误数升高,是一个最常见的性能问题。
  2. 是应用程序之间调用情况,比如调用频率、错误数、延时等。由于应用程序并不是孤立的,如果其依赖的其他应用出现了性能问题,应用自身性能也会受到影响。
  3. 是应用程序内部核心逻辑的运行情况,比如关键环节的耗时以及执行过程中的错误等。由于这是应用程序内部的状态,从外部通常无法直接获取到详细的性能数据。所以,应用程序在设计和开发时,就应该把这些指标提供出来,以便监控系统可以了解其内部运行状态。

有了应用进程的资源使用指标

  • 可以把系统资源的瓶颈跟应用程序关联起来,从而迅速定位因系统资源不足而导致的性能问题
  • 有了应用程序之间的调用指标,可以迅速分析出一个请求处理的调用链中,到底哪个组件才是导致性能问题的罪魁祸首;
  • 有了应用程序内部核心逻辑的运行性能,可以更进一步,直接进入应用程序的内部,定位到底是哪个处理环节的函数导致了性能问题。

基于这些思路,将这些指标纳入监控系统(比如 Prometheus + Grafana)中,就可以跟系统监控一样,一方面通过告警系统,把问题及时汇报给相关团队处理;另一方面,通过直观的图形界面,动态展示应用程序的整体性能。

除此之外,由于业务系统通常会涉及到一连串的多个服务,形成一个复杂的分布式调用链。为了迅速定位这类跨应用的性能瓶颈,你还可以使用 Zipkin、Jaeger、Pinpoint 等各类开源工具,来构建全链路跟踪系统。

比如,下图就是一个 Jaeger 调用链跟踪的示例。

全链路跟踪可以迅速定位出,在一个请求处理过程中,哪个环节才是问题根源。比如,从上图中,可以很容易看到,这是 Redis 超时导致的问题。
还可以生成线上系统的调用拓扑图。这些直观的拓扑图,在分析复杂系统(比如微服务)时尤其有效。

日志监控

性能指标的监控,可以让你迅速定位发生瓶颈的位置,不过只有指标的话往往还不够。比如,同样的一个接口,当请求传入的参数不同时,就可能会导致完全不同的性能问题。所以,除了指标外,我们还需要对这些指标的上下文信息进行监控,而日志正是这些上下文的最佳来源。

  • 指标是特定时间段的数值型测量数据,通常以时间序列的方式处理,适合于实时监控。
  • 而日志则完全不同,日志都是某个时间点的字符串消息,通常需要对搜索引擎进行索引后,才能进行查询和汇总分析。
  • 对日志监控来说,最经典的方法,就是使用 ELK 技术栈,即使用 Elasticsearch、Logstash 和 Kibana 这三个组件的组合。

这其中

  • Logstash 负责对从各个日志源采集日志,然后进行预处理,最后再把初步处理过的日志,发送给 Elasticsearch 进行索引。
  • Elasticsearch 负责对日志进行索引,并提供了一个完整的全文搜索引擎,这样就可以方便你从日志中检索需要的数据。
  • Kibana 则负责对日志进行可视化分析,包括日志搜索、处理以及绚丽的仪表板展示等。

下面这张图,就是一个 Kibana 仪表板的示例,它直观展示了 Apache 的访问概况。

值得注意的是,ELK 技术栈中的 Logstash 资源消耗比较大。所以,在资源紧张的环境中,我们往往使用资源消耗更低的 Fluentd,来替代 Logstash(也就是所谓的 EFK 技术栈)。
 

分析性能问题的步骤

系统资源瓶颈
系统资源的瓶颈,可以通过 USE 法,即使用率、饱和度以及错误数这三类指标来衡量。系统的资源,可以分为硬件资源和软件资源两类。

如 CPU、内存、磁盘和文件系统以及网络等,都是最常见的硬件资源。
而文件描述符数、连接跟踪数、套接字缓冲区大小等,则是典型的软件资源。
在到监控系统告警时,就可以对照这些资源列表,再根据指标的不同来进行定位。

CPU 性能分析

利用 top、vmstat、pidstat、strace 以及 perf 等几个最常见的工具,获取 CPU 性能指标后,
再结合进程与 CPU 的工作原理,就可以迅速定位出 CPU 性能瓶颈的来源。

实际上,top、pidstat、vmstat 这类工具所汇报的 CPU 性能指标,都源自 /proc 文件系统(比如 /proc/loadavg、/proc/stat、/proc/softirqs 等)。
这些指标,都应该通过监控系统监控起来。虽然并非所有指标都需要报警,但这些指标却可以加快性能问题的定位分析。
当收到系统的用户 CPU 使用率过高告警时,从监控系统中直接查询到,导致 CPU 使用率过高的进程;
然后再登录到进程所在的 Linux 服务器中,分析该进程的行为。
可以使用 strace,查看进程的系统调用汇总;也可以使用 perf 等工具,找出进程的热点函数;甚至还可以使用动态追踪的方法,来观察进程的当前执行过程,直到确定瓶颈的根源。
 

内存性能分析

下图是一个迅速定位内存瓶颈的流程。通过 free 和 vmstat 输出的性能指标,确认内存瓶颈;
然后,再根据内存问题的类型,进一步分析内存的使用、分配、泄漏以及缓存等,最后找出问题的来源。

同 CPU 性能一样,很多内存的性能指标,也来源于 /proc 文件系统(比如 /proc/meminfo、/proc/slabinfo 等),
它们也都应该通过监控系统监控起来。这样,当你收到内存告警时,就可以从监控系统中,
直接得到上图中的各项性能指标,从而加快性能问题的定位过程。
比如说,当你收到内存不足的告警时,首先可以从监控系统中。找出占用内存最多的几个进程。
然后,再根据这些进程的内存占用历史,观察是否存在内存泄漏问题。确定出最可疑的进程后,再登录到进程所在的 Linux 服务器中,
分析该进程的内存空间或者内存分配,最后弄清楚进程为什么会占用大量内存。

磁盘和文件系统 I/O 性能分析

下面这张图。使用 iostat ,发现磁盘 I/O 存在性能瓶颈(比如 I/O 使用率过高、响应时间过长或者等待队列长度突然增大等)
后,可以再通过 pidstat、 vmstat 等,确认 I/O 的来源。接着,再根据来源的不同,进一步分析文件系统和磁盘的使用率、
缓存以及进程的 I/O 等,从而揪出 I/O 问题的真凶。

同 CPU 和内存性能类似,很多磁盘和文件系统的性能指标,也来源于 /proc 和 /sys 文件系统(比如 /proc/diskstats、/sys/block/sda/stat 等)。
自然,它们也应该通过监控系统监控起来。这样,当你收到 I/O 性能告警时,就可以从监控系统中,直接得到上图中的各项性能指标,从而加快性能定位的过程。
当发现某块磁盘的 I/O 使用率为 100% 时,首先可以从监控系统中,找出 I/O 最多的进程。然后,再登录到进程所在的 Linux 服务器中,
借助 strace、lsof、perf 等工具,分析该进程的 I/O 行为。最后,再结合应用程序的原理,找出大量 I/O 的原因。

网络性能分析

网络性能含两类资源,即网络接口和内核资源。网络性能的分析,要从 Linux 网络协议栈的原理来切入。
下面这张图,就是 Linux 网络协议栈的基本原理,包括应用层、套机字接口、传输层、网络层以及链路层等。

而要分析网络的性能,自然也是要从这几个协议层入手,通过使用率、饱和度以及错误数这几类性能指标,观察是否存在性能问题。比如 :

  • 在链路层,可以从网络接口的吞吐量、丢包、错误以及软中断和网络功能卸载等角度分析;
  • 在网络层,可以从路由、分片、叠加网络等角度进行分析;
  • 在传输层,可以从 TCP、UDP 的协议原理出发,从连接数、吞吐量、延迟、重传等角度进行分析;
  • 在应用层,可以从应用层协议(如 HTTP 和 DNS)、请求数(QPS)、套接字缓存等角度进行分析。

同前面几种资源类似,网络的性能指标也都来源于内核,包括 /proc 文件系统(如 /proc/net)、网络接口以及 conntrack 等内核模块。
这些指标同样需要被监控系统监控。这样,当你收到网络告警时,就可以从监控系统中,查询这些协议层的各项性能指标,从而更快定位出性能问题。
比如,当收到网络不通的告警时,就可以从监控系统中,查找各个协议层的丢包指标,确认丢包所在的协议层。然后,从监控系统的数据中,
确认网络带宽、缓冲区、连接跟踪数等软硬件,是否存在性能瓶颈。最后,再登录到发生问题的 Linux 服务器中,借助 netstat、tcpdump、bcc 等工具,
分析网络的收发数据,并且结合内核中的网络选项以及 TCP 等网络协议的原理,找出问题的来源。


应用程序瓶颈

除了以上这些来自网络资源的瓶颈外,还有很多瓶颈,其实直接来自应用程序。比如,最典型的应用程序性能问题,就是吞吐量(并发请求数)下降、错误率升高以及响应时间增大。
这些应用程序性能问题虽然各种各样,但就其本质来源,实际上只有三种,也就是资源瓶颈、依赖服务瓶颈以及应用自身的瓶颈。

  1. 资源瓶颈,其实还是指刚才提到的 CPU、内存、磁盘和文件系统 I/O、网络以及内核资源等各类软硬件资源出现了瓶颈,从而导致应用程序的运行受限。对于这种情况,我们就可以用前面系统资源瓶颈模块提到的各种方法来分析。
  2. 依赖服务的瓶颈,也就是诸如数据库、分布式缓存、中间件等应用程序,直接或者间接调用的服务出现了性能问题,从而导致应用程序的响应变慢,或者错误率升高。这说白了就是跨应用的性能问题,使用全链路跟踪系统,就可以帮你快速定位这类问题的根源。
  3. 应用程序自身的性能问题,包括了多线程处理不当、死锁、业务算法的复杂度过高等等。对于这类问题,在我们前面讲过的应用程序指标监控以及日志监控中,观察关键环节的耗时和内部执行过程中的错误,就可以帮你缩小问题的范围。

不过,由于这是应用程序内部的状态,外部通常不能直接获取详细的性能数据,所以就需要应用程序在设计和开发时,就提供出这些指标,以便监控系统可以了解应用程序的内部运行状态。
如果这些手段过后还是无法找出瓶颈,你还可以用系统资源模块提到的各类进程分析工具,来进行分析定位。比如:

  • 使用 strace,观察系统调用;
  • 使用 perf 和火焰图,分析热点函数;
  • 甚至使用动态追踪技术,来分析进程的执行状态。

当然,系统资源和应用程序本来就是相互影响、相辅相成的一个整体。实际上,很多资源瓶颈,也是应用程序自身运行导致的。比如,进程的内存泄漏,会导致系统内存不足;进程过多的 I/O 请求,会拖慢整个系统的 I/O 请求等。

很多情况下,资源瓶颈和应用自身瓶颈,其实都是同一个问题导致的,并不需要我们重复分析。

性能优化的步骤

CPU 优化
CPU 性能优化的核心,在于排除所有不必要的工作、充分利用 CPU 缓存并减少进程调度对性能的影响。

  1. 把进程绑定到一个或者多个 CPU 上,充分利用 CPU 缓存的本地性,并减少进程间的相互影响。
  2. 为中断处理程序开启多 CPU 负载均衡,以便在发生大量中断时,可以充分利用多 CPU 的优势分摊负载。
  3. 使用 Cgroups 等方法,为进程设置资源限制,避免个别进程消耗过多的 CPU。同时,为核心应用程序设置更高的优先级,减少低优先级任务的影响。

内存优化

  1. 除非有必要,Swap 应该禁止掉。这样就可以避免 Swap 的额外 I/O ,带来内存访问变慢的问题。
  2. 使用 Cgroups 等方法,为进程设置内存限制。这样就可以避免个别进程消耗过多内存,而影响了其他进程。对于核心应用,还应该降低 oom_score,避免被 OOM 杀死。
  3. 使用大页、内存池等方法,减少内存的动态分配,从而减少缺页异常。

磁盘和文件系统 I/O 的优化

  1. 最简单的方法,通过 SSD 替代 HDD、或者使用 RAID 等方法,提升 I/O 性能。
  2. 针对磁盘和应用程序 I/O 模式的特征,选择最适合的 I/O 调度算法。比如,SSD 和虚拟机中的磁盘,通常用的是 noop 调度算法;而数据库应用,更推荐使用 deadline 算法。
  3. 优化文件系统和磁盘的缓存、缓冲区,比如优化脏页的刷新频率、脏页限额,以及内核回收目录项缓存和索引节点缓存的倾向等等。
  4. 使用不同磁盘隔离不同应用的数据、优化文件系统的配置选项、优化磁盘预读、增大磁盘队列长度等,也都是常用的优化思路。

网络优化
这些优化方法都是从 Linux 的网络协议栈出发,针对每个协议层的工作原理进行优化

首先,从内核资源和网络协议的角度来说,我们可以对内核选项进行优化,比如:

  • 增大套接字缓冲区、连接跟踪表、最大半连接数、最大文件描述符数、本地端口范围等内核资源配额;
  • 减少 TIMEOUT 超时时间、SYN+ACK 重传数、Keepalive 探测时间等异常处理参数;
  • 开启端口复用、反向地址校验,并调整 MTU 大小等降低内核的负担。
  • 这些都是内核选项优化的最常见措施。

其次,从网络接口的角度来说,我们可以考虑对网络接口的功能进行优化,比如:

  • 将原来 CPU 上执行的工作,卸载到网卡中执行,即开启网卡的 GRO、GSO、RSS、VXLAN 等卸载功能;
  • 开启网络接口的多队列功能,这样,每个队列就可以用不同的中断号,调度到不同 CPU 上执行;
  • 增大网络接口的缓冲区大小以及队列长度等,提升网络传输的吞吐量。

在极限性能情况(比如 C10M)下,内核的网络协议栈可能是最主要的性能瓶颈,所以,一般会考虑绕过内核协议栈。

  • 可以使用 DPDK 技术,跳过内核协议栈,直接由用户态进程用轮询的方式,来处理网络请求。同时,再结合大页、CPU 绑定、内存对齐、流水线并发等多种机制,优化网络包的处理效率。
  • 还可以使用内核自带的 XDP 技术,在网络包进入内核协议栈前,就对其进行处理。这样,也可以达到目的,获得很好的性能。


应用程序优化
说完了系统软硬件资源的优化,再来看看应用程序的优化思路。

虽然系统的软硬件资源,是保证应用程序正常运行的基础,但性能优化的最佳位置,还是应用程序内部

  • 系统 CPU 使用率(sys%)过高的问题。有时候出现问题,虽然表面现象是系统 CPU 使用率过高,但待你分析过后,很可能会发现,应用程序的不合理系统调用才是罪魁祸首。这种情况下,优化应用程序内部系统调用的逻辑,显然要比优化内核要简单也有用得多。
  • 数据库的 CPU 使用率高、I/O 响应慢,也是最常见的一种性能问题。这种问题,一般来说,并不是因为数据库本身性能不好,而是应用程序不合理的表结构或者 SQL 查询语句导致的。这时候,优化应用程序中数据库表结构的逻辑或者 SQL 语句,显然要比优化数据库本身,能带来更大的收益。

所以,在观察性能指标时,应该先查看应用程序的响应时间、吞吐量以及错误率等指标,因为它们才是性能优化要解决的终极问题。
以终为始,从这些角度出发,推荐下面几种方法。

  1. 从 CPU 使用的角度来说,简化代码、优化算法、异步处理以及编译器优化等,都是常用的降低 CPU 使用率的方法,这样可以利用有限的 CPU 处理更多的请求。
  2. 从数据访问的角度来说,使用缓存、写时复制、增加 I/O 尺寸等,都是常用的减少磁盘 I/O 的方法,这样可以获得更快的数据处理速度。
  3. 从内存管理的角度来说,使用大页、内存池等方法,可以预先分配内存,减少内存的动态分配,从而更好地内存访问性能。
  4. 从网络的角度来说,使用 I/O 多路复用、长连接代替短连接、DNS 缓存等方法,可以优化网络 I/O 并减少网络请求数,从而减少网络延时带来的性能问题。
  5. 从进程的工作模型来说,异步处理、多线程或多进程等,可以充分利用每一个 CPU 的处理能力,从而提高应用程序的吞吐能力。
  6. 可以使用消息队列、CDN、负载均衡等各种方法,来优化应用程序的架构,将原来单机要承担的任务,调度到多台服务器中并行处理。这样也往往能获得更好的整体性能。
     

性能工具速查

关于工具手册的查看,man 应该是我们最熟悉的方法,我在专栏中多次介绍过。实际上,除了 man 之外,还有另外一个查询命令手册的方法,也就是 info。
info 可以理解为 man 的详细版本,提供了诸如节点跳转等更强大的功能。相对来说,man 的输出比较简洁,而 info 的输出更详细。所以,我们通常使用 man 来查询工具的使用方法,只有在 man 的输出不太好理解时,才会再去参考 info 文档。

有些工具不需要额外安装,就可以直接使用,比如内核的 /proc 文件系统;
而有些工具,则需要安装额外的软件包,比如 sar、pidstat、iostat 等。
所以,在选择性能工具时,除了要考虑性能指标这个目的外,还要结合待分析的环境来综合考虑。比如,实际环境是否允许安装软件包,是否需要新的内核版本等。

首先还是要推荐下面这张图,也就是 Brendan Gregg 整理的性能工具谱图。

这张图从 Linux 内核的各个子系统出发,汇总了对各个子系统进行性能分析时,你可以选择的工具。不过,虽然这个图是性能分析最好的参考资料之一,它其实还不够具体。
比如,当你需要查看某个性能指标时,这张图里对应的子系统部分,可能有多个性能工具可供选择。但实际上,并非所有这些工具都适用,具体要用哪个,还需要你去查找每个工具的手册,对比分析做出选择。
建议从性能指标出发,根据性能指标的不同,将性能工具划分为不同类型。比如,最常见的就是可以根据 CPU、内存、磁盘 I/O 以及网络的各类性能指标,将这些工具进行分类。

CPU 性能工具
首先,从 CPU 的角度来说,主要的性能指标就是 CPU 的使用率、上下文切换以及 CPU Cache 的命中率等。下面这张图就列出了常见的 CPU 性能指标。

从这些指标出发,再把 CPU 使用率,划分为系统和进程两个维度,我们就可以得到,下面这个 CPU 性能工具速查表。注意,因为每种性能指标都可能对应多种工具

内存性能工具

接着我们来看内存方面。从内存的角度来说,主要的性能指标,就是系统内存的分配和使用、进程内存的分配和使用以及 SWAP 的用量。下面这张图列出了常见的内存性能指标。

从这些指标出发,我们就可以得到如下表所示的内存性能工具速查表。

注:最后一行 pcstat 的源码链接为 https://github.com/tobert/pcstat

磁盘 I/O 性能工具

接下来,从文件系统和磁盘 I/O 的角度来说,主要性能指标,就是文件系统的使用、缓存和缓冲区的使用,以及磁盘 I/O 的使用率、吞吐量和延迟等。下面这张图列出了常见的 I/O 性能指标。

从这些指标出发,我们就可以得到,下面这个文件系统和磁盘 I/O 性能工具速查表。同 CPU 和内存性能工具一样,我也梳理出了这些工具的特点和注意事项。

网络性能工具

从网络的角度来说,主要性能指标就是吞吐量、响应时间、连接数、丢包数等。根据 TCP/IP 网络协议栈的原理,我们可以把这些性能指标,进一步细化为每层协议的具体指标。这里我同样用一张图,分别从链路层、网络层、传输层和应用层,列出了各层的主要指标。

从这些指标出发,我们就可以得到下面的网络性能工具速查表。

基准测试工具

除了性能分析外,很多时候,我们还需要对系统性能进行基准测试。比如,

  • 在文件系统和磁盘 I/O 模块中,我们使用 fio 工具,测试了磁盘 I/O 的性能。
  • 在网络模块中,我们使用 iperf、pktgen 等,测试了网络的性能。
  • 而在很多基于 Nginx 的案例中,我们则使用 ab、wrk 等,测试 Nginx 应用的性能。
  • 还有最基本的压测工具 stress,sysbench

除了专栏里介绍过的这些工具外,对于 Linux 的各个子系统来说,还有很多其他的基准测试工具可能会用到。下面这张图,是 Brendan Gregg 整理的 Linux 基准测试工具图谱

猜你喜欢

转载自blog.csdn.net/hixiaoxiaoniao/article/details/88913127