eumnq8 发表于 2021-1-12 19:37:31

服务器单机性能优化

【CPU篇】
性能优化涉及的基础知识:cpu缓存 内存 网络编程 文件系统
CPU缓存:
核的概念
处理器:物理芯片。
多核:一个独立cpu实例,一个处理器可以包含多个cpu实例。
硬件线程(逻辑核):一个核上同时执行多个线程(包括intel的超线程HT)

缓存行(cache line):
单次cpu读取缓存的长度为缓存行。
x86处理典型的缓存大小是64字节。

L1/L2/L3 缓存:
------MEMORY-------
------CacheL3------
-CacheL2---CacheL2-
-CacheL1---CacheL1-
-Core0-----Core1---
-------CPU---------

上下文切换:
进程上下文切换
线程上下文切换
中断上下文切换
自愿上下文切换(VCX/cswch)
    是指进程无法获取所需资源,导致的上下文切换。比如说, I/O、内存等系统资源不足时,   
    就会发生自愿上下文切换。
非自愿上下文切换(ICX/nvcswch)
    是指进程由于时间片已到等原因,被系统强制调度,进而发生的上下文切换。比如说,大量
    进程都在争抢 CPU 时,就容易发生非自愿上下文切换。
       
缓存一致性(伪共享)
两个线程分别跑在不同核心上,访问的内存同一个资源,但是cpu访问cache时使用的却是不同的缓存。

性能问题
性能指标有哪些?
cpu负载情况
cpu利用率
应用程序、线程的cpu利用率
cpu中断性能:软中断、硬中断、上下文切换
CPU性能常见问题表现有哪些?
中断
   各个核中断不均衡,导致cpu超载或空闲
cpu超载
   多核之间负载没有平衡;cpu做了无用功;应用程序设计需要优化
cpu空闲
   太多内存操作,导致cpu停顿;太多的分支预测错误
       
性能分析常用工具
top
uptime
mpstat/pidstat
vmstat
Perf
processxp

优化方法
减少上下文切换
for 循环时使用顺序、连续读取
减少结构体大小
注意伪共享问题

【内存篇】
虚拟内存、物理内存、交换分区、换页
tcmalloc和ptmalloc2区别
ptmalloc2:
申请时需要加锁
通用性更好,对于任意内存大小申请,性能一致
tcmalloc:
小内存:1~256K
中等内存:256K~1M
大内存:>1M
无锁
对于小内存(<256K) 性能优异

常见内存问题:内存碎片、内存泄漏、内存分配器性能

【网络篇】
建连接三次握手
客户端         服务端
                SYN->
          SYN+ACK<-
                ACK->
数据传输一来一回 ACK->ACK
客户端         服务端
                ACK->
          ACK<-
断开连接四次挥手
客户端         服务端
          FIN+ACK->
          ACK<-
                FIN<-
                ACK->

半连接状态(syn攻击)
1、服务端接收到SYN,放入SYN半连接队列(tcp_max_syn_backlog控制大小)
2、正常情况下服务端还会接收到ACK,放入accept连接队列(backlog控制大小)
3、服务端调用accept函数给进程建立socket连接

syn攻击就是只触发第一步消耗服务器的syn队列。从而搞死tcp服务器
syn队列一旦耗尽,正常玩家也无法建立连接。

TCP拥塞控制算法介绍
Reno、Bio、Cubic、tahoe
基于丢包策略
延时不可控
GCC
webrtc默认,基于时延+丢包
带宽估计不准确,低带宽过降
与tcp竞争,过度退让
时延抖动场景,带宽估计波动大
BBR
google提出,演进到第二版
算法核心是尽量不排队、低延时
带宽估计准确与tcp竞争,不吃亏

I/O多路复用 & 异步I/O
I/O多路复用
Select模型
Poll模型
Epoll模型(linux特有)
异步I/O
Windows下的IOCP模型

以在外面吃饭举例
在一个小餐馆里,你点了菜,然后坐在位置,当一个菜好了之后,服务员就会挨桌的问,是不是你的菜?(Select/Poll模型)
在kfc点了汉堡,取了一个号,然后坐在位置上,等服务员叫到你的号,你再去取。(Epoll模型)
在kfc位置上扫码点了汉堡,然后等服务员直接送汉堡到你的位置。(IOCP模型)

select模型中进程受阻于select调用,等待可能多个套接字中的任意一个变为可读

如何编写单机10w并发服务?
1、I/O 模型选择
2、多进程/多线程模型选择
在这种方式下,所有的进程/线程都监听相同的接口,并且开启 SO_REUSEPORT 选项,由内核负责将请求负载均衡到这些监听进程中去,由于内核确保了只有一个进程/线程被唤醒,就不会出现惊群问题了。
nginx设计比较好的解决了惊群问题
参考https://blog.csdn.net/zhwenx3/article/details/88673802
要实现单机10w还是比较容易的。

如何编写单机100w并发服务?
首先从物理资源使用上来说,100万个请求需要大量的系统资源,硬件要先跟上(主要带宽、内存、CPU)。
比如,假设每个请求需要 16KB 内存的话,那么总共就需要大约 15 GB 内存。而从带宽上来说,假设只有 20% 活跃连接,即使每个连接只需要 1KB/s 的吞吐量,总共也需要 1.6 Gb/s 的吞吐量。千兆网卡显然满足不了这么大的吞吐量,所以还需要配置万兆网卡,或者基于多网卡 Bonding 承载更大的吞吐量。
其次,从软件资源上来说,大量的连接也会占用大量的软件资源。
比如,文件描述符的数量、连接状态的跟踪(CONNTRACK)、网络协议栈的缓存大小(比如套接字读写缓存、TCP 读写缓存)等等。
最后,大量请求带来的中断处理,也会带来非常高的处理成本。
这样,就需要多队列网卡、中断负载均衡、CPU 绑定、RPS/RFS(软中断负载均衡到多个 CPU 核上),以及将网络包的处理卸载(Offload)到网络设备(如 TSO/GSO、LRO/GRO、VXLAN OFFLOAD)等各种硬件和软件的优化。
C1000K 的解决方法,本质上还是构建在 epoll 的非阻塞 I/O 模型上。
只不过,除了 I/O 模型之外,还需要从应用程序到 Linux 内核、再到 CPU、内存和网络等各个层次的深度优化,特别是需要借助硬件,来卸载那些原来通过软件处理的大量功能。   
比如跳过内核协议栈的冗长路径,把网络包直接送到要处理的应用程序那里去。这里有两种常见的机制,DPDK 和 XDP。
结论,理论上linux上实现单机100w可行,比较有挑战。

优化方法分享:
1、应用层优化
优化报文大小。比如压缩、pb序列化。
报文合并,减少交互次数。比如登陆时多个消息可以合并为一次请求消息。
最近1公里。将接入网关部署在用户最近的机房、云主机,通过域名解析、运营商匹配、大数据分析、客户端测试、负载均衡等策略将用户按照最近原则分配到最近的接入网关。
ssl 协议是很耗性能的。应当了解接入网关最大的访问量。
使用httpdns、powerdns。 普通的dns解析也会导致连接出现异常。
2、TCP层的调优
tcp缓存区设置 :tcp_moderate_revbuf、ipv4.tcp_rmem、ipv4.tcp_wmem
tcp的两个积压队列:tcp_max_syn_backlog、net.core.somaxconn
开启syncookies字段,但会占用tcp头的timestamps字段
Google 提出了 TCP fast open 方案( FTO )
tcp阻塞控制算法: ipv4.tcp_congestion_control
重传次数优化:tcp_syn_retries、tcp_synack_retries
选择性确认( SACK/FACK ):允许tcp确认非连续的包,以减少需要重传输的数量
延迟ack:推迟最多500ms发送ack,从而可以合并多个ack;减少网络包的数量
慢启动窗口:设置为10mss
Nagle算法
time_wait重复利用:设置tcp_tw_reuse=1,tcp_timestamps=1,使用tcp新版本的时间戳字段,它允许作为客户端的新连接,在安全条件下使用 TIME_WAIT 状态下的端口。


【文件系统篇】
1、顺序读写和随机读写
2、文件缓存
3、高I/O服务器分开部署

【应用程序的优化】
1、数据结构选择
数组
链表

队列
优先队列
散列表
跳表
平衡二叉树


字典树
B+树

2、并发编程
进程、线程、协程
进程间上下文切换:进程的上下文不仅包括虚拟内存、栈、全局变量等用户空间的资源,
还包括内核堆栈、寄存器等内核空间的状态。

进程1执行|进程1上下文保存|加载进程2上下文|进程2执行
---------------------------------------------------->时钟
         |       进程上下文切换          |
线程上下文切换:需要切换线程的私有数据、寄存器等不共享数据

利用好锁:
互斥锁、
自旋锁(专为防止多处理器并发而引入的一种锁,它在内核中大量应用于中断处理等部分)、
自适应锁、
读写锁、
悲观锁(独占锁)[导致其它所有未持有锁的线程阻塞,而等待持有锁的线程释放锁]、
乐观锁(Compare And Set cas)
更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。
线程1重新获取内存地址V的当前值,并重新计算想要修改的新值。这个重新尝试的过程被称为自旋。
从思想上来说,Synchronized属于悲观锁,悲观地认为程序中的并发情况严重,所以严防死守。CAS属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去尝试更新。
有的时候并发高的情况下使用悲观锁反而性能更高
CAS也有些缺点:
1、并发高的时候,cpu压力比较大,因为许多线程反复尝试更新某一个变量,却一直更新不成功。
2、不能保证代码块的原子性,只能一个变量原子性。
3、ABA问题,可以添加前缀版本号解决。

自旋锁可以参考 ngx_spinlock实现。

3、善用缓存
        1、内存缓存 (boost_lru)
        2、进程缓存(redis、 memcache)

4、零拷贝(linux中零拷贝)
零拷贝主要的任务就是避免CPU将数据从一块存储拷贝到另外一块存储,主要就是利用各种零拷贝技术,避免让CPU做大量的数据拷贝任务,减少不必要的拷贝,或者让别的组件来做这一类简单的数据传输任务,让CPU解脱出来专注于别的任务。这样就可以让系统资源的利用更加有效。
1、使用mmap
buf = mmap(diskfd, len);
write(sockfd, buf, len);
2、sendfile只适用于将数据从文件拷贝到套接字上,限定了它的使用范围
3、splice(有一方必须是管道设备)
https://www.jianshu.com/p/fad3339e3448

5、其他优化
处理器绑定
非阻塞I/O
线程池
编译器优化
资源优先级

归纳下:
**** Hidden Message *****

页: [1]
查看完整版本: 服务器单机性能优化