微服务运维——CentOS的Tcp性能调优,支撑百万并发的关键

1,569 阅读12分钟

由于我们的微服务架构基于.net core 3.1,部署构建采用了Docker技术,因此它必然需要一款底层Linux操作系统。这里选用了CentOS,因为虚拟机采用的也是CentOS,而docker也基于此,可以更好的发现问题,不会因为操作系统的不同而导致奇奇怪怪的问题。里面会给出打包CentOS镜像的源码哦~~~

打包CentOS镜像

基于CentOS的.net core镜像,官方并没有给出来。因此,如果需要只能自己打包了,这里给出部分片段,抛转引玉。 镜像来自官方centos 7, 时区增加了 上海时区 Asia/Shanghai, 除了没有安装libgdiplus和字体外,几乎可以完美运行所有.net core程序。

FROM centos:7
# This image provides a .NET Core 3.1 environment you can use to run your .NET
# applications.

ENV HOME=/opt/app-root \
    PATH=/opt/app-root/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
    DOTNET_APP_PATH=/opt/app-root/app \
    DOTNET_DATA_PATH=/opt/app-root/data \
    DOTNET_DEFAULT_CMD=default-cmd.sh \
    DOTNET_CORE_VERSION=3.1 \
    DOTNET_FRAMEWORK=netcoreapp3.1 \
# Microsoft's images set this to enable detecting when an app is running in a container.
    DOTNET_RUNNING_IN_CONTAINER=true \
    DOTNET_SSL_CERT_DIR=/opt/app-root/ssl_dir

LABEL io.k8s.description="Platform for running .NET Core 3.1 applications" \
      io.k8s.display-name=".NET Core 3.1" \
      io.openshift.tags="runtime,.net,dotnet,dotnetcore,rh-dotnet31-runtime" \
      io.openshift.expose-services="8080:http" \
      io.openshift.s2i.scripts-url=image:///usr/libexec/s2i \
      io.s2i.scripts-url=image:///usr/libexec/s2i

# Labels consumed by Red Hat build service
LABEL name="dotnet/dotnet-31-runtime-centos7" \
      com.redhat.component="rh-dotnet31-runtime-container" \
      version="3.1.ums" \
      release="1" \
      architecture="x86_64"


# Don't download/extract docs for nuget packages
ENV NUGET_XMLDOC_MODE=skip
# add chinese timezone
ENV TZ Asia/Shanghai
RUN ln -fs /usr/share/zoneinfo/${TZ} /etc/localtime \
    && echo ${TZ} > /etc/timezone

# Make dotnet command available even when scl is not enabled.
RUN ln -s /opt/app-root/etc/scl_enable_dotnet /usr/bin/dotnet

RUN yum install -y centos-release-dotnet && \
    INSTALL_PKGS="rh-dotnet31-aspnetcore-runtime-3.1 nss_wrapper tar unzip" && \
    yum install -y --setopt=tsflags=nodocs $INSTALL_PKGS && \
    rpm -V $INSTALL_PKGS && \
    yum clean all -y && \
	# yum cache files may still exist (and quite large in size)
    rm -rf /var/cache/yum/*

# Get prefix path and path to scripts rather than hard-code them in scripts
ENV CONTAINER_SCRIPTS_PATH=/opt/app-root \
    ENABLED_COLLECTIONS="rh-dotnet31"

# When bash is started non-interactively, to run a shell script, for example it
# looks for this variable and source the content of this file. This will enable
# the SCL for all scripts without need to do 'scl enable'.
ENV BASH_ENV=${CONTAINER_SCRIPTS_PATH}/etc/scl_enable \
    ENV=${CONTAINER_SCRIPTS_PATH}/etc/scl_enable \
    PROMPT_COMMAND=". ${CONTAINER_SCRIPTS_PATH}/etc/scl_enable"

# Add default user
RUN mkdir -p ${DOTNET_APP_PATH} ${DOTNET_DATA_PATH} && \
    useradd -u 1001 -r -g 0 -d ${HOME} -s /sbin/nologin \
      -c "Default Application User" default
# Run container by default as user with id 1001 (default)
USER 0  # admin

有了镜像以后,便可以自由发布基于centos的.net core微服务了,奥利给。

TCP网络性能调优只术语

  1. NIC : 网卡
  2. DMA: 直接内存访问,一般指从网络设备将数据直接移到到内存区,可以节省CPU的开销。
  3. Ring Buffer: 环形缓冲区,NIC 在系统启动过程中会向系统注册自己的各种信息,系统会分配 Ring Buffer 队列也会分配一块专门的内核内存区域给 NIC 用于存放传输上来的数据包.

性能调节的指导方针

首先明确一点,性能调节是非常复杂的,因此为了优化性能,需要考虑多种因素的综合影响,要面对很多复杂的问题,因此没有一个完全的现成方案,供你复制。

在性能调节方面有这些因素的制约:

  • 网络、网卡的能力
  • 驱动的特性和配置
  • 计算机的硬件配置
  • CPU到内存的架构
  • CPU的核数
  • 内核版本

因为有诸多因素的影响,你应该明白了为啥出厂操作系统后,仍需要我们设定诸多参数来进行调优,而非出厂就设置好!

网卡收包

  • Ring Buffer 从名称即可看出来,其是一个圆形缓冲队列,因此,如果大小不够的话,很有可能会覆盖之前的数据。在网卡设备上,其分配RX Ring Buffer接收数据,TX Ring Buffer来发送数据。其采用SoftIRQs方式(包含硬件中断、软件中断两种方式)。 在这里插入图片描述

  • 中断和中断处理 硬件中断是最顶级的中断,当NIC接收到数据,它通过DMA拷贝数据到内核缓冲区,NIC使用硬件中断通知内核数据来了,然后中断处理进行接管后续处理,当然前提它需要中断其他的任何任务处理,以备自己使用资源,因此中断时非常按昂贵的开销。 硬件中断一般会按照处理逻辑,把自己分发为软件中断或 SoftIRQs,这样就可以更温和的处理任务。

# 查看硬件中断
cat /proc/interrupts
  • SoftIRQs SoftIRQs也是顶层中断,运行时不能被中断。其被设计用来负责处理 Ring Buffer。
# 查看下RX和TX SoftIRQs
cat  /proc/softirqs | grep RX
cat  /proc/softirqs | grep TX
  • NAPI Polling 新的api,被设计为拉模式,可以有效的避免使用硬件中断来监控数据,以减轻系统的负担。一图胜千言,参看下图。 在这里插入图片描述

  • 网络协议栈 一旦数据已经从NIC被接收到内核层,接下来的处理就被不同的协议栈接管了:Ethernet、ICMP、IPV4、IPV6、TCP、UDP、SCTP等等。 最后,数据被传送到socket缓冲区,上层应用程序可以接收数据,其从内核空间移动到用户空间,结束掉内核层的参与。

  • 网络诊断工具 netstat: 其主要从网络文件获取网络的统计信息

cat /proc/net/dev
cat /proc/net/tcp
cat /proc/net/unix

dropwatch:监控内存到内核的释放情况。 ip:管理和监控路由、设备等信息 ethtool:显示和改变NIC参数配置

  • TCP参数调节的持久化需要通过重启实现 许多网络参数都是内核控制的,因此采用sysctl来读取和改变,然而其只针对当前的运行环境,系统其重启又返回到内核默认设定。因此需要修改 /etc/sysctl.conf文件。

  • 查看瓶颈 查看硬件参数瓶颈的合适的工具是 ethtool

 ethtool -S eth3
 # 显示如下
 tx_scattered: 0
 tx_no_memory: 0
 tx_no_space: 0
 tx_too_big: 0
 tx_busy: 0
 tx_send_full: 0
 rx_comp_busy: 0
 rx_no_memory: 0
 stop_queue: 0
 wake_queue: 0
 vf_rx_packets: 0
 vf_rx_bytes: 0
 vf_tx_packets: 0
 vf_tx_bytes: 0
 vf_tx_dropped: 0
 tx_queue_0_packets: 43783950
 tx_queue_0_bytes: 31094532492
 rx_queue_0_packets: 91969116
 rx_queue_0_bytes: 69733870231
 tx_queue_1_packets: 48375299
 tx_queue_1_bytes: 32469935392
 rx_queue_1_packets: 80277415
 rx_queue_1_bytes: 65208029029
 
 netstat -s
 #分析协议层错误

性能调节

  • SoftIRQ 丢失 如果SoftIRQs得不到足够的时间运行,可能导致收数据异常,NIC缓冲溢出和数据丢失。这时候需要增加SoftIRQs的处理时间。
#  sysctl net.core.netdev_budget 
net.core.netdev_budget = 300

如果你使用命令 cat /proc/net/softnet_stat查看第三列是大幅增加的,则表示SoftIRQs没有足够的Cpu处理时间,这时候可以翻倍这处理时间。

0aa72ed3 00000000 000006a9 00000000 00000000 00000000 00000000 00000000 00000000 00000000
0b70c1b6 00000000 00000636 00000000 00000000 00000000 00000000 00000000 00000000 00000000
sysctl -w net.core.netdev_budget=600
  • Tuned Tuned 是一个非常推荐的性能调节服务,其可以根据场景自动调节cpu、IO、内核等性能,安装也非常简单。
# yum -y install tuned
# service tuned start
# chkconfig tuned on
# tuned-adm list
# tuned-adm profile throughput-performance
Switching to profile 'throughput-performance' 
  • backlog 队列 查看NIC接收的存储队列,采用
cat /proc/net/softnet_stat
# 数据第2列如果有数据并增加,则需要增大backlog队列
# 命令查询的行数为cpu核心数
 sysctl -w net.core.netdev_max_backlog=X
  • 调节RX、TX缓冲队列
ethtool -g eth0
Ring parameters for eth0:
Pre-set maximums:
RX:             18811
RX Mini:        0
RX Jumbo:       0
TX:             2560
Current hardware settings:
RX:             9709
RX Mini:        0
RX Jumbo:       0
TX:             170

上述数据是说 RX有18K左右的大小,仅仅使用了9K的样子,TX有2K大小,仅仅使用了170个。 可以修改RX、TX大小

 ethtool -G eth0 rx 20490  tx 8192
 # 持久化保存修改 /sbin/ifup-local
  • 调节传输队列长度 传输队列默认为1000,可以使用命令ip -s link查询,如果有丢包现象,可以增加队列
# ip link set dev em1 txqueuelen 2000
# ip link
# 持久化更改需要重启机器,修改文件  /sbin/ifup-local 
  • tcp 时戳 时间戳选项发送方在每个报文段中放置一个时间戳值。接收方在确认中返回这个数值,从而允许发送方为每一个收到的ACK计算RTT(我们必须说“每一个收到的ACK”而不是“每一个收到的报文段”,是因为TCP通常用一个ACK来确认多个报文段)。我们提到过目前很多实现为每个窗口值计算一个RTT,对于包含8个报文段的窗口而言这是正确的。然而,较大的窗口大小则需要进行更好的RTT计算。 因此我们需要保证tcp时戳已经启动,大部分系统默认是启动的。
#  sysctl net.ipv4.tcp_timestamps 
#  sysctl -w net.ipv4.tcp_timestamps=1
  • tcp sack 它使得接收方能告诉发送方哪些报文段丢失,哪些报文段重传了,哪些报文段已经提前收到等信息。根据这些信息TCP就可以只重传哪些真正丢失的报文段。因此如果您的网络性能特别好,几乎不丢包,则关闭sack,可以提升tcp的性能。
 sysctl -w net.ipv4.tcp_sack=0
  • tcp窗口缩放 其是tcp协议为高性能的一项扩展。因此我们需要保证其已经打开
# sysctl net.ipv4.tcp_window_scaling
net.ipv4.tcp_window_scaling = 1
  • 调节tcp_rmem (socker 内存) 默认有三个值可以调节,最小值、默认值、最大值。 默认最大值一般为4MB,
# sysctl net.ipv4.tcp_rmem
4096 87380 4194304
# sysctl -w net.ipv4.tcp_rmem=“16384 349520 16777216”
# sysctl net.core.rmem_max
4194304
# sysctl -w net.core.rmem_max=16777216
  • tcp listen backlog 监听仅有服务器调用,其做两件事。 (1)当socket创建一个套接字的时候,它被假设为一个主动套接字,也就是说,它是一个将调用connect发起连接的客户套接字。listen函数把一个未连接的套接字转换成一个被动套接字,指示内核应该接受指向该套接字的连接请求;调用listen使得套接字从CLOSED状态转换到LISTEN状态; (2)backlog规定了内核应该为相应的套接字排队的最大连接个数; 内核为给定的监听套接字维护两个队列,两个队列项之和不超过backlog: 在这里插入图片描述 如果应用很慢,或者连接数很多,则需要增加backlog个数。
# sysctl net.core.somaxconn
net.core.somaxconn = 128
# sysctl -w net.core.somaxconn=2048
net.core.somaxconn = 2048
# sysctl net.core.somaxconn
net.core.somaxconn = 2048
  • time_wait优化 在这里插入图片描述 通信双方建立TCP连接后,主动关闭连接的一方就会进入TIME_WAIT状态。 客户端主动关闭连接时,会发送最后一个ack后,然后会进入TIME_WAIT状态,再停留2个MSL时间,进入CLOSED状态。 上图就是客户端主动关闭连接的例子。 TCP/IP协议为什么这样设计的?,主要是由于(1)可靠地实现TCP全双工连接的终止,(2)允许老的重复分节在网络中消逝 。TCP协议在关闭连接的四次握手过程中,最终的ACK是由主动关闭连接的一端(后面统称A端)发出的,如果这个ACK丢失,对方(后面统称B端)将重发出最终的FIN,因此A端必须维护状态信息(TIME_WAIT)允许它重发最终的ACK。如果A端不维持TIME_WAIT状态,而是处于CLOSED 状态,那么A端将响应RST分节,B端收到后将此分节解释成一个错误。因而,要实现TCP全双工连接的正常终止,必须处理终止过程中四个分节任何一个分节的丢失情况,主动关闭连接的A端必须维持TIME_WAIT状态 。
# 查看当前系统的所有连接状态的统计
# netstat -n|awk '/^tcp/{++S[$NF]}END{for (key in S) print key,S[key]}'

如果系统有大量的TIME_WAIT,则需要调整内核参数

# 开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭;
net.ipv4.tcp_syncookies = 1
# 开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭;
net.ipv4.tcp_tw_reuse = 1
# 开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭。
net.ipv4.tcp_tw_recycle = 1
# 修改系默认的 TIMEOUT 时间 10
net.ipv4.tcp_fin_timeout = 10
# 用于向外连接的端口范围。缺省情况下很小:32768到61000,改为1024到65000。
net.ipv4.ip_local_port_range = 1024 65000 
# SYN队列的长度,默认为1024,加大队列长度为8192,可以容纳更多等待连接的网络连接数
net.ipv4.tcp_max_syn_backlog = 8192 
# 当keepalive起用的时候,TCP发送keepalive消息的频度。缺省是2小时,改为20分钟。
net.ipv4.tcp_keepalive_time = 1200 

结语

tcp优化是一件复杂的事情,并没有一个包含所有场景的万能方案,因此,这里也仅仅是分析下各类场景的调节。 底下给出部分优化参考。 编辑 /etc/sysctl.conf,千兆网环境:

net.ipv4.tcp_mem= 98304 131072 196608
net.ipv4.tcp_window_scaling=1
net.core.wmem_default = 65536
net.core.rmem_default = 65536
net.core.wmem_max=8388608

重载配置使用 sysctl -p 不限制带宽:

net.ipv4.tcp_window_scaling=1
net.ipv4.tcp_timestamps=0
net.ipv4.tcp_sack=0
net.ipv4.tcp_rmem=10000000 10000000 10000000
net.ipv4.tcp_wmem=10000000 10000000 10000000
net.ipv4.tcp_mem=10000000 10000000 10000000
net.core.rmem_max=524287
net.core.wmem_max=524287
net.core.rmem_default=524287
net.core.wmem_default=524287
net.core.optmem_max=524287
net.core.netdev_max_backlog=300000