您现在的位置是:网站首页>技术百科技术百科

聊聊系统优化

小大寒2024-01-01[技术百科]博学多闻

聊聊系统优化系统优化的核心是提高系统性能,主要通过吞吐量和延迟两方面衡量。性能测试需关注延迟和吞吐量的平衡,使用工具测量并逐步增加负载。优化策略包括算法、代码、内存分配及网络调优。常见瓶颈包括IO、CPU、内存和网络资源的限制。优化方法有空间换时间、简化代码、并行处理等,且在多核环境下并行处理能显著提升性能。网络优化可通过调整TCP/UDP参数、网卡设置等提升数据传输效率。‌

聊聊系统优化

一、系统性能定义

首先,让我们明确什么是系统性能。这个定义至关重要,因为如果我们对系统性能没有清晰的认识,就无法有效地定位性能问题。很多人以为这个概念很简单,但实际上,他们往往缺乏系统化的理解方法。在此,我想介绍一种系统化的方法来分析性能问题。总体来说,系统性能包含以下两个方面:

  1. 吞吐量(Throughput):指系统每秒可以处理的请求数或任务数。
  2. 延迟(Latency):指系统在处理单个请求或任务时的响应时间。

一个系统的性能需要同时满足这两方面的要求。如果系统的吞吐量很高,但延迟达到几分钟,那这个高吞吐量就毫无意义。同样,如果延迟很低但吞吐量很小,也无法满足实际需求。因此,一个好的性能测试需要同时关注吞吐量和延迟。

  • 吞吐量越大,延迟可能越高。因为系统负载加重会导致响应速度降低。
  • 延迟越小,支持的吞吐量通常越大。因为响应速度快,系统可以处理更多请求。

二、系统性能测试

在了解了性能的基本定义后,接下来就是如何测试系统的性能,即收集吞吐量和延迟这两个指标。

  • 定义延迟要求:例如,对于一个网站,响应时间可能要求在5秒以内。对于某些实时系统,这个要求可能更严格,比如5毫秒以内。具体的定义需要根据业务需求确定。
  • 开发性能测试工具:需要一个工具来制造高强度的吞吐量,以及另一个工具来测量延迟。关于如何测量延迟,可以在代码中实现,但这可能只测得程序内部的延迟。实际延迟包括操作系统和网络的影响,可以通过抓包工具如Wireshark进行测量。
  • 进行性能测试:逐步增加吞吐量,观察系统的负载情况,同时记录延迟数据。通过这种方式,可以找到系统的最大负载能力及其对应的延迟表现。

一些补充说明:

  • 关于延迟:当吞吐量较小时,延迟通常比较稳定。但随着吞吐量增加,延迟可能会出现较大波动。因此,分析延迟时应关注其分布情况。例如,多少百分比的请求延迟在可接受范围内,多少超出,多少完全无法接受。
  • 关于测试时间:性能测试需要定义一个时间段。例如,在某个吞吐量下持续运行15分钟,以观察系统的稳定性。因为系统可能在短时间内正常,但运行一段时间后出现问题。
  • Soak测试:这是一种长时间稳定性测试,比如在某个吞吐量下持续运行一周,以验证系统的长期稳定性。

性能测试涉及的细节很多,例如突发测试(Burst Test)等。这些测试方法对于性能调优至关重要,是一项细致且耗时的工作。

三、定位性能瓶颈

在性能调优前,首先需要定位系统的性能瓶颈。很多人以为这很简单,但实际上需要系统的方法。

3.1 查看操作系统负载

出现性能问题时,不要急于检查代码,而应首先查看操作系统的负载情况,包括CPU利用率、内存使用率、磁盘和网络IO等。

常用工具有:

  • Linux工具:vmstat、sar、iostat、top、tcpdump等。
  • Windows工具:perfmon。

通过观察这些数据,可以初步判断问题所在:

  1. CPU利用率:如果CPU利用率较低,但吞吐量和延迟表现不佳,说明程序可能因IO等其他问题受阻。
  2. IO:查看磁盘文件IO、驱动程序IO(如网卡)、内存换页率,这些都会影响系统性能。
  3. 网络:使用iftop、iptraf等工具分析网络带宽使用情况。
  4. 阻塞问题:如果CPU、IO、内存和网络均正常,但性能依然上不去,可能是程序中存在阻塞,如锁争用或资源等待。

通过分析操作系统负载,可以定位问题并采取针对性调整。

3.2 使用Profiler测试

接下来,可借助Profiler工具分析程序的性能表现。例如:

  • Java工具:JProfiler、TPTP。
  • GNU工具:gprof。
  • Intel工具:VTune。
  • Linux工具:OProfile、perf。

这些工具可提供函数调用次数、运行时间、CPU利用率等详细数据,帮助我们发现性能热点。

四、常见的系统瓶颈

以下是一些常见的性能问题和优化策略,源于个人经验,可能并不全面,仅供参考。性能优化的核心思路可以总结为以下几个方面:

  • 用空间换时间。通过引入各种缓存机制,例如 CPU 的 L1/L2 缓存、RAM 缓存和硬盘缓存等,将计算结果存储下来,以减少重复计算的时间。例如数据缓冲、CDN 加速等。此外,这一策略也包括数据冗余,如数据镜像和负载均衡。
  • 用时间换空间。在某些情况下,牺牲少量时间可以显著节省空间。例如网络传输中采用压缩算法,尽管增加了计算耗时,却能显著减少传输时间。
  • 简化代码。代码越少,执行效率越高。例如减少循环嵌套、优化条件判断顺序、避免频繁分配和释放内存等。熟悉编程语言及其常用库是实现代码优化的关键。
  • 并行处理。在多核处理器环境下,多线程或多进程能够显著提高性能。关键在于确保程序具备扩展性,并减少线程间的调度和同步开销。

根据 2:8 原则,20% 的代码可能耗费了 80% 的性能资源。找到这些关键代码,并集中优化,可以取得显著成效。

4.1 算法调优

优化算法是提升系统性能的关键。以下是一些实际案例:

  • 过滤算法:通过对过滤条件排序,并采用二分查找替代线性遍历,使系统性能提高了 50%。
  • 哈希算法:选用适合业务场景的哈希算法,避免高碰撞率问题,可显著提升系统性能。例如,针对特定数据特性优化后的哈希算法提升了 150% 性能。
  • 分而治之与预处理:例如,将复杂报表生成任务拆分为每日增量计算,仅需 20 分钟完成每日数据处理,而整月数据一次性计算则需数小时。分而治之的策略对于处理大规模数据非常有效。
4.2 代码调优

以下是代码优化的一些实践:

  • 字符串操作:字符串操作通常性能开销较大。例如,将日期和状态码存储为整数代替字符串处理,可以大幅提升性能。在频繁调用的函数中,通过优化字符串操作,使系统性能提升约 30%。
  • 多线程优化:线程之间的锁和上下文切换是多线程性能瓶颈的主要来源。尽量减少锁的使用或选择读写锁等优化方案。在某些情况下,避免线程安全的智能指针也能显著提升性能。
  • 内存分配:频繁的小内存分配会引发内存碎片问题,影响系统性能。例如,利用内存池管理机制可以有效缓解此类问题,减少 malloc 等系统调用的开销。

总之,性能优化需要结合具体业务场景,有针对性地进行算法、代码和架构的调整,以达到理想的性能表现。

4.3)网络调优

关于网络调优,尤其是 TCP 参数优化(可通过搜索 "TCP Tuning" 找到许多相关文章),涉及内容广泛且深奥。Linux 提供了大量 TCP/IP 参数可供调整,使我们有机会深入优化系统性能。强烈推荐阅读《TCP/IP 详解 卷1:协议》这本书。以下内容主要讲述一些概念上的知识点。

A) TCP 调优

TCP 连接存在较高的资源开销,例如会占用文件描述符和内存缓存,因此系统能够支持的 TCP 连接数量是有限的。针对 TCP 的高资源消耗特点,许多攻击手段(如 SYN Flood 攻击)旨在耗尽系统资源。 为此,我们需要正确配置 KeepAlive 参数,用于定义连接的存活时间。当连接在指定时间内无数据传输时,系统会发送探测包检测连接状态,若未收到回应,TCP 将关闭连接以回收资源。这在 HTTP 等短连接场景中尤为重要,可有效降低 DoS 攻击风险。例如:

net.ipv4.tcp_keepalive_probes = 5
net.ipv4.tcp_keepalive_intvl = 20
net.ipv4.tcp_fin_timeout = 30

此外,TCP 连接的主动关闭方会进入 TIME_WAIT 状态,该状态持续两个 MSL(最大分段生存期,默认为 4 分钟),期间资源无法回收。HTTP 服务器上通常会出现大量 TIME_WAIT 状态的连接。可通过以下参数优化:

net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_tw_recycle = 1

其中,前者表示重用 TIME_WAIT 状态连接,后者则允许快速回收资源。

另一个重要概念是 RWIN(TCP 接收窗口大小),表示接收端未发送 ACK 前可以接收的最大数据量。丢包会导致发送端停止发送数据并等待超时重传,而丢包率的增加会显著降低带宽利用率。以下参数可以调优:

net.core.wmem_default = 6155309
net.core.rmem_default = 6155309
net.core.rmem_max = 23126675
net.core.wmem_max = 23126675

理论上,RWIN 应与网络吞吐量和延迟成比例。对于高延迟网络,需要配置较大的 buffer 以减少 ACK 次数;而对丢包率较高的网络,应避免过大的 buffer 以降低重传的性能影响。

最后,当网络条件非常稳定且不在意少量数据丢失时,可考虑使用 UDP 来提升传输效率。

B)UDP 调优

在 UDP 优化中,MTU(最大传输单元)尤为重要。MTU 决定了单个数据包的最大尺寸,例如以太网的 MTU 为 1500 字节。考虑到协议头部占用(IP 20 字节,UDP 8 字节),实际数据载荷最大为 1472 字节。在发送数据时,尽量填满 MTU 以最大化带宽利用率。

某些硬件支持超出 MTU 的数据包分片和重组,可进一步提升性能。此外,使用 setsockopt() 方法可调整 SO_SNDBUFSO_RCVBUF 缓存大小、TTL 以及 KeepAlive 设置。

UDP 的另一个优点是支持多播(multi-cast),适合在内网中通知多台节点,且便于水平扩展。

C)网卡调优

网卡调优对于千兆及以上的网络尤为关键。在 Linux 下,可使用 ifconfig 查看网卡统计信息。当 overrun 值较高时,可通过增加 txqueuelen 缓冲队列长度(默认为 1000)来改善性能,例如:

ifconfig eth0 txqueuelen 6600

此外,ethtool 命令也可用于调整网卡的缓冲区大小。在 Windows 系统中,可在网卡属性的高级选项卡中调整参数(如接收缓冲区和发送缓冲区大小)。这些调整对于大数据量传输非常有效。

D)其他网络性能优化

在多路复用技术中,一个线程管理多个 TCP 连接是关键。常见的三种系统调用是:

  • select:支持最多 1024 个连接,性能受限。
  • poll:突破连接限制,但仍采用轮询机制,效率较低。
  • epoll:仅在连接活跃时由操作系统触发,效率较高,适用于 Linux Kernel 2.6 及以上版本。

最后,针对可能阻塞的系统调用(如 gethostbyname)需格外小心。这类调用可能因为 DNS 查询延迟严重影响性能。在多线程环境中,建议使用性能更优的替代函数(如 gethostbyname_r),或通过配置 hosts 文件或内存管理解决方案优化。

4.4 系统调优

A) I/O模型

前面提到过 select/poll/epoll 这三个系统调用。我们知道,Unix/Linux 将所有设备都视为文件进行 I/O 操作,所以,这三个调用其实属于 I/O 相关的系统调用。关于 I/O 模型,优化 I/O 性能是非常关键的。以下是常见的 I/O 模型:

1. 同步阻塞式 I/O(无需赘述)。

2. 同步非阻塞 I/O,通过 fcntl 设置 O_NONBLOCK 实现。

3. 使用 select/poll/epoll 的 I/O 模型:I/O 操作非阻塞,但事件处理阻塞。这是一种 I/O 异步、事件同步的方式。

4. 异步 I/O(AIO)模型:
这种模型让 I/O 和处理并行进行。I/O 请求立即返回,表示请求已成功发起。后台完成 I/O 操作时,通过信号或基于线程的回调函数通知应用程序完成状态。由于没有任何阻塞,无论是 I/O 本身还是事件通知,因此 AIO 可以充分利用 CPU 性能,避免了同步非阻塞方式的轮询开销。

例如,Nginx 的高效性部分得益于采用了 epoll 和 AIO 的方式。

再看 Windows 下的 I/O 模型:

a) WriteFile 系统调用:可同步阻塞,也可同步非阻塞(取决于是否以 Overlapped 模式打开)。若为 Overlapped 模式,需要通过 WaitForSingleObject 检测操作是否完成。

b) WriteFileEx 系统调用:支持异步 I/O,并允许传入回调函数,操作完成后会调用该函数。然而,Windows 会将回调函数放入 APC(Asynchronous Procedure Calls)队列,仅当线程处于 Alterable 状态时才会执行回调。

c) IOCP(I/O Completion Port):
IOCP 使用线程池模型,将 I/O 结果存放到队列中,由专门的线程(或线程池)处理。与 Linux 的 AIO 模型类似,但实现方式和使用方式完全不同。

提升 I/O 性能的根本方法是尽量减少与外设交互的次数。内存缓存可以显著提高性能,但也需要在响应速度和写入次数之间做出平衡。

B) 多核 CPU 调优

在多核 CPU 环境下,CPU0 通常具有特殊的重要性,例如负责调整功能。因此,避免过度使用 CPU0 是优化的关键。
对于 Windows,可以通过任务管理器的“设置相关性”功能指定进程运行的 CPU 核。
对于 Linux,可使用 taskset 命令实现类似功能。例如:

taskset -c 1,2 mydemo

此外,多核 CPU 通常支持 NUMA(Non-Uniform Memory Access)架构。在 NUMA 模式下,各处理器节点拥有独立的本地内存,这可以有效避免 SMP 架构中的一致性问题。Linux 下可以通过 numactl 命令对 NUMA 进行优化:

numactl --cpubind=0 --membind=0,1 mydemo

最佳实践是让程序仅访问所在节点的本地内存:

numactl --cpunodebind=1 --membind=1 --localalloc mydemoapp

C) 文件系统调优

文件系统性能优化的首要任务是分配足够的内存。在 Linux 下,可通过 free 命令检查 free/used/buffers/cached 状态,理想情况下 buffers 和 cached 占比应在 40% 左右。

文件系统优化建议包括:

  • /etc/fstab 文件中添加 noatime 参数,避免频繁更新文件访问时间。
  • 根据需要启用 dealloc 参数,以优化写入操作。
  • 合理选择日志模式(data=journal, data=ordered, data=writeback)。默认设置 data=ordered 通常能在性能和数据保护之间取得平衡。

此外,可以使用 iotop 工具监控各进程的磁盘读写负载。

4.5)数据库调优

数据库调优并不是我的强项,因此我会基于自己有限的知识简单讨论一下。请注意,下面的内容并不一定适用于所有情况,因为不同的业务场景和数据库设计可能会得出完全不同的结论。所以,这里仅是一些一般性的说明,具体问题需要具体分析。

A)数据库引擎调优

我对数据库引擎并不是非常熟悉,但有几个问题我认为是必须了解的。

  • 数据库的锁机制:这是非常重要的。在并发情况下,锁会显著影响性能。各种隔离级别、行锁、表锁、页锁、读写锁、事务锁以及读优先或写优先机制等都会影响性能。最理想的情况是避免锁定,因此,通过分库分表、冗余数据和减少一致性事务处理等手段,可以显著提升性能。NoSQL就是通过牺牲一致性和事务处理,并使用冗余数据,来实现分布式和高性能。
  • 数据库的存储机制:不仅要了解各种字段类型的存储方式,更要搞清楚数据库如何进行数据存储、如何分区和管理数据。例如,Oracle的数据库文件、表空间、段等。了解这些存储机制有助于减轻I/O负载。比如,在MySQL中,通过show engines;命令可以查看不同存储引擎的支持情况。不同的存储引擎有不同的特点,根据具体业务需求和数据库设计选择合适的引擎,可以提高性能。
  • 数据库的分布式策略:最简单的分布式策略是复制或镜像。要理解分布式的一致性算法,如主主同步或主从同步。了解这些技术的原理,可以实现数据库层面的水平扩展。

B)SQL语句优化

SQL语句优化首先要使用工具,例如:MySQL SQL Query AnalyzerOracle SQL Performance Analyzer,或微软的SQL Query Analyzer。大多数RDBMS都提供类似的工具,可以帮助你诊断SQL性能问题。也可以通过EXPLAIN命令查看SQL语句的执行计划。

还有一个重要的方面是,数据库操作需要大量内存,尤其是对于多表查询的SQL语句,这会消耗大量内存。

接下来,根据我有限的SQL知识,列出几个可能会影响性能的SQL:

  • 全表扫描:例如,执行`select * from user where lastname = "xxxx"`这样的SQL语句,会对整个表进行扫描,线性复杂度为O(n),记录越多,性能越差(例如:100条记录需要50ms,一百万条记录需要5分钟)。为了解决这个问题,有两种优化方法:一种是分表,减少单表记录数量,另一种是为`lastname`字段建立索引。索引类似于key-value数据结构,key是查询字段,value是物理行号,索引的查询复杂度大约为O(log(n)),即使用B-Tree实现索引(例如:100条记录需要50ms,一百万条记录需要100ms)。
  • 索引使用:在索引字段上,避免进行计算、类型转换、函数调用、空值判断或字段连接操作,这些都会影响索引的性能。索引通常出现在`WHERE`或`ORDER BY`子句中,因此在这些子句中避免进行复杂的计算操作,尽量不使用`NOT`或函数。
  • 多表查询:关系型数据库中常见的操作是多表查询,主要包括EXISTS、IN和JOIN三种关键字(关于各种JOIN的使用,参见相关文献)。现代数据库引擎对这些语句的优化已经非常到位,通常性能差异不大。有观点认为,EXISTS的性能优于IN,IN又优于JOIN。实际应用中,性能差异可能与数据量、数据库结构和SQL复杂度有关,一般来说,在简单的查询中,差异不大。避免过多的嵌套查询,尽量使用简单的SQL语句。
  • JOIN操作:有观点认为JOIN的顺序会影响性能,但只要JOIN的结果集一致,性能与JOIN的顺序无关。因为数据库引擎会自动优化JOIN顺序。JOIN有三种实现算法:嵌套循环、排序归并和哈希JOIN(MySQL只支持嵌套循环)。
    • 嵌套循环:类似于多重嵌套循环,查询复杂度为O(log(n)) * O(log(m))。
    • 哈希JOIN:通过使用临时哈希表,优化嵌套循环的O(log(n))复杂度。
    • 排序归并:将两个表按照查询字段排序后进行合并。

具体的优化方法应根据数据特点和SQL语句的复杂度来选择。

  • 部分结果集:通过`LIMIT`(MySQL)、`rownum`(Oracle)、`Top`(SQL Server)等方式限制返回的记录数,这为数据库引擎提供了优化的空间。通常,为了保证性能,`ORDER BY`的字段需要建立索引。使用分页显示数据时,MySQL采用`OFFSET`,SQL Server使用`FETCH NEXT`。如果能够知道分页的起始值,可以使用`>=`来代替`FETCH`,这种技术称为seek,性能比`fetch`要高。
  • 字符串操作:字符串操作对性能的影响非常大,尽量使用数字代替字符串,如时间、工号等。
  • 全文检索:不要使用`LIKE`进行全文检索。如果需要全文检索,可以尝试使用Sphinx
  • 其他优化建议
    • 避免使用`SELECT *`,明确指定需要的字段。如果涉及多表查询,确保为每个字段指定表名。
    • 避免使用`HAVING`,因为它会遍历所有记录,性能较差。
    • 尽量使用`UNION ALL`替代`UNION`,因为后者会进行去重,性能较差。
    • 索引过多会影响`INSERT`和`DELETE`的性能。`UPDATE`涉及多个索引时也会变慢,但如果只更新一个索引,影响较小。

关于SQL优化,网上有很多资源,不同的数据库引擎有不同的优化技巧,大家可以通过搜索找到更多详细的技术文章。

这篇文章的目的是分享一些常见的优化技巧,欢迎大家提出改进意见和补充。

注:这篇文章包含了许多常见的数据库优化技术,具体的优化方法应根据具体的业务需求和数据库环境来选择。本文的写作灵感来源于看到一些相关文章后,决定分享自己的理解。

阅读完毕,很棒哦!

文章评论

站点信息

  • 网站地址:www.xiaodahan.com
  • 我的QQ: 3306916637