plantegg

java tcp mysql performance network docker Linux


  • 首页

  • 分类

  • 归档

  • 标签

  • 站点地图

  • 关于

netstat定位性能案例

发表于 2019-04-21 | 分类于 TCP

netstat定位性能案例

netstat 和 ss 都是小工具,但是在网络性能、异常的窥探方面真的是神器。ss用法见这里

下面的案例通过netstat很快就发现为什么系统总是压不上去了(主要是快速定位到一个长链条的服务调用体系中哪个节点碰到瓶颈了)

netstat 命令

netstat跟ss命令一样也能看到Send-Q、Recv-Q这些状态信息,不过如果这个连接不是Listen状态的话,Recv-Q就是指收到的数据还在缓存中,还没被进程读取,这个值就是还没被进程读取的 bytes;而 Send 则是发送队列中没有被远程主机确认的 bytes 数

$netstat -tn  
Active Internet connections (w/o servers)
Proto Recv-Q Send-Q Local Address   Foreign Address State  
tcp0  0 server:8182  client-1:15260 SYN_RECV   
tcp0 28 server:22    client-1:51708  ESTABLISHED
tcp0  0 server:2376  client-1:60269 ESTABLISHED

netstat -tn 看到的 Recv-Q 跟全连接半连接没有关系,这里特意拿出来说一下是因为容易跟 ss -lnt 的 Recv-Q 搞混淆。

Recv-Q 和 Send-Q 的说明

Recv-Q
Established: The count of bytes not copied by the user program connected to this socket.
Listening: Since Kernel 2.6.18 this column contains the current syn backlog.

Send-Q
Established: The count of bytes not acknowledged by the remote host.
Listening: Since Kernel 2.6.18 this column contains the maximum size of the syn backlog.

通过 netstat 发现问题的案例

自身太慢,比如如下netstat -t 看到的Recv-Q有大量数据堆积,那么一般是CPU处理不过来导致的:

image.png

下面的case是接收方太慢,从应用机器的netstat统计来看,也是client端回复太慢(本机listen 9108端口)

image.png

send-q表示回复从9108发走了,没收到对方的ack,基本可以推断client端到9108之间有瓶颈

实际确实是前端到9108之间的带宽被打满了,调整带宽后问题解决

netstat -s 统计数据

所有统计信息基本都有

Linux Module and make debug

发表于 2019-01-24 | 分类于 Linux

Linux Module and make debug

Makefile 中的 tab 键

$sudo make
Makefile:4: *** missing separator.  Stop.

Makefile 中每个指令前面必须是tab(不能是4个空格)!

pwd

$sudo make
make -C /lib/modules/4.19.48-002.ali4000.test.alios7.x86_64/build M= modules
make[1]: Entering directory `/usr/src/kernels/4.19.48-002.ali4000.test.alios7.x86_64'
make[2]: *** No rule to make target `arch/x86/entry/syscalls/syscall_32.tbl', needed by `arch/x86/include/generated/asm/syscalls_32.h'.  Stop.
make[1]: *** [archheaders] Error 2
make[1]: Leaving directory `/usr/src/kernels/4.19.48-002.ali4000.test.alios7.x86_64'
make: *** [all] Error 2

Makefile中的:
make -C /lib/modules/$(shell uname -r)/build M=$(pwd) modules

$(pwd) 需要修改成:$(shell pwd)

makefile调试的法宝

makefile调试的法宝1

$ make --debug=a,m SHELL="bash -x" > make.log  2>&1                # 可以获取make过程最完整debug信息
$ make --debug=v,m SHELL="bash -x" > make.log  2>&1                # 一个相对精简版,推荐使用这个命令
$ make --debug=v  > make.log  2>&1                                 # 再精简一点的版本
$ make --debug=b  > make.log  2>&1                                 # 最精简的版本

推荐版本(会输出执行的具体命令):
make --debug=b SHELL="bash -x"  > make.log.simple  2>&1
or
make V=1

makefile调试的法宝2

上面的法宝1更多的还是在整体工程的makefile结构、makefile读取和makefile内部的rule之间的关系方面有很好的帮助作用。但是对于makefile中rule部分之前的变量部分的引用过程则表现的不是很充分。在这里,我们有另外一个法宝,可以把变量部分的引用过程给出一个比较好的调试信息。具体命令如下。

$ make -p 2>&1 | grep -A 1 '^# makefile' | grep -v '^--' | awk '/# makefile/&&/line/{getline n;print $0,";",n}' | LC_COLLATE=C sort -k 4 -k 6n > variable.log
$ cat variable.log
# makefile (from `Makefile', line 1) ; aa := 11
# makefile (from `Makefile', line 3) ; cc := 11
# makefile (from `Makefile', line 4) ; bb := 9999
# makefile (from `cfg_makefile', line 1) ; MAKEFILE_LIST :=  Makefile cfg_makefile
# makefile (from `cfg_makefile', line 1) ; xx := 4444
# makefile (from `cfg_makefile', line 2) ; yy := 4444
# makefile (from `cfg_makefile', line 3) ; zz := 4444
# makefile (from `sub_makefile', line 1) ; MAKEFILE_LIST :=  sub_makefile
# makefile (from `sub_makefile', line 1) ; aaaa := 222222
# makefile (from `sub_makefile', line 2) ; bbbb := 222222
# makefile (from `sub_makefile', line 3) ; cccc := 222222

makefile调试的法宝3

法宝2可以把makefile文件中每个变量的最终值清晰的展现出来,但是对于这些变量引用过程中的中间值却没有展示。此时,我们需要依赖法宝3来帮助我们。

$(warning $(var123))

很多人可能都知道这个warning语句。我们可以在makefile文件中的变量引用阶段的任何两行之间,添加这个语句打印关键变量的引用过程。

make 时ld报找不到lib

make总是报找不到libc,但实际我执行 ld -lc –verbose 从debug信息看又能够正确找到libc,debug方法

image.png

image.png

实际原因是make的时候最后有一个参数 -static,这要求得装 ***-static lib库,可以去掉 -static

依赖错误

编译报错缺少的组件需要yum install一下(bison/flex)

hping3

构造半连接:

sudo hping3 -i u100 -S -p 3306 10.0.186.79

tcp sk_state

enum {
    TCP_ESTABLISHED = 1,
    TCP_SYN_SENT,
    TCP_SYN_RECV,
    TCP_FIN_WAIT1,
    TCP_FIN_WAIT2,
    TCP_TIME_WAIT,
    TCP_CLOSE,
    TCP_CLOSE_WAIT,
    TCP_LAST_ACK,
    TCP_LISTEN,
    TCP_CLOSING,    /* Now a valid state */

    TCP_MAX_STATES  /* Leave at the end! */
};

kdump

启动kdump(kexec-tools), 系统崩溃的时候dump 内核(/var/crash)

sudo systemctl start kdump

参考:Linux 系统内核崩溃分析处理简介

Kdump 的概念出现在 2005 左右,是迄今为止最可靠的内核转存机制,已经被主要的 linux™ 厂商选用。kdump是一种先进的基于 kexec 的内核崩溃转储机制。当系统崩溃时,kdump 使用 kexec 启动到第二个内核。第二个内核通常叫做捕获内核,以很小的内存启动以捕获转储镜像。

第一个内核保留了内存的一部分给第二个内核启动用。由于 kdump 利用 kexec 启动捕获内核,绕过了 BIOS,所以第一个内核的内存得以保留。这是内核崩溃转储的本质。

kdump 需要两个不同目的的内核,生产内核和捕获内核。生产内核是捕获内核服务的对象。捕获内核会在生产内核崩溃时启动起来,与相应的 ramdisk 一起组建一个微环境,用以对生产内核下的内存进行收集和转存。

什么是 kexec ?

Kexec 是实现 kdump 机制的关键,它包括 2 一是组成部分:一是内核空间的系统调用 kexec_load,负责在生产内核(production kernel 或 first kernel)启动时将捕获内核(capture kernel 或 sencond kernel)加载到指定地址。二是用户空间的工具 kexec-tools,他将捕获内核的地址传递给生产内核,从而在系统崩溃的时候能够找到捕获内核的地址并运行。

没有 kexec 就没有 kdump。先有 kexec 实现了在一个内核中可以启动另一个内核,才让 kdump 有了用武之地。kexec 原来的目的是为了节省时间 kernel 开发人员重启系统的时间,谁能想到这个“偷懒”的技术却孕育了最成功的内存转存机制呢?

crash

sudo yum install crash -y
//手动触发crash
#echo 1 > /proc/sys/kernel/sysrq
#echo c > /proc/sysrq-trigger
//系统crash,然后重启,重启后分析:
sudo crash /usr/lib/debug/lib/modules/4.19.57-15.1.al7.x86_64/vmlinux /var/crash/127.0.0.1-2020-04-02-14\:40\:45/vmcore

可以触发dump但是系统没有crash, 以下两个命令都可以

1
2
3
4
5
sudo crash /usr/lib/debug/usr/lib/modules/4.19.91-19.1.al7.x86_64/vmlinux /proc/kcore
sudo crash /usr/lib/debug/usr/lib/modules/4.19.91-19.1.al7.x86_64/vmlinux /dev/mem

写内存hack内核,那就在crash命令执行前,先执行下面的命令:
stap -g -e 'probe kernel.function("devmem_is_allowed").return { $return = 1 }'

内核函数替换

image.png

static int __init hotfix_init(void)
{
  unsigned char e8_call[POKE_LENGTH];
  s32 offset, i;

  addr = (void *)kallsyms_lookup_name("tcp_reset");
  if (!addr) {
    printk("一切还没有准备好!请先加载tcp_reset模块。\n");
    return -1;
  }

  _text_poke_smp = (void *)kallsyms_lookup_name("text_poke");
  _text_mutex = (void *)kallsyms_lookup_name("text_mutex");

  stub = (void *)test_stub1;

  offset = (s32)((long)stub - (long)addr - FTRACE_SIZE);

  e8_call[0] = 0xe8;
  (*(s32 *)(&e8_call[1])) = offset;
  for (i = 5; i < POKE_LENGTH; i++) {
    e8_call[i] = 0x90;
  }
  get_online_cpus();
  mutex_lock(_text_mutex);
  _text_poke_smp(&addr[POKE_OFFSET], e8_call, POKE_LENGTH);
  mutex_unlock(_text_mutex);
  put_online_cpus();

  return 0;
}

void test_stub1(void)
{
  struct sock *sk = NULL;
  unsigned long sk_addr = 0;
  char buf[MAX_BUF_SIZE];
  int size=0;
  asm ("push %rdi");

  asm ( "mov %%rdi, %0;" :"=m"(sk_addr) : :);
  sk = (struct sock *)sk_addr;

  printk("aaaaaaaa yes :%d  dest:%X  source:%X\n",
      sk->sk_state,
      sk->sk_rcv_saddr,
      sk->sk_daddr);
/*
  size = snprintf(buf, MAX_BUF_SIZE-1, "rst %lu %d %pI4:%u->%pI4:%u \n",
                     get_seconds(),
                     sk->sk_state,
                     &(inet_sk(sk)->inet_saddr),
                     ntohs(inet_sk(sk)->inet_sport),
                     ntohs(inet_sk(sk)->inet_dport),
                     &(inet_sk(sk)->inet_daddr));
*/
//  tcp_rt_log_output(buf,size,1);

  asm ("pop %rdi");
}

参考文档

https://blog.sourcerer.io/writing-a-simple-linux-kernel-module-d9dc3762c234

https://stackoverflow.com/questions/16710047/usr-bin-ld-cannot-find-lnameofthelibrary

Linux系统中如何彻底隐藏一个TCP连接

定制Linux Kernel

发表于 2019-01-24 | 分类于 Linux

定制 Linux Kernel

Linux 里面有一个工具,叫 Grub2,全称 Grand Unified Bootloader Version 2。顾名思义,就是搞系统启动的。

修改启动参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
$cat change_kernel_parameter.sh 
#cat /sys/devices/system/cpu/vulnerabilities/*
#grep '' /sys/devices/system/cpu/vulnerabilities/*
#https://help.aliyun.com/document_detail/102087.html?spm=a2c4g.11186623.6.721.4a732223pEfyNC

#cat /sys/kernel/mm/transparent_hugepage/enabled
#transparent_hugepage=always
#noibrs noibpb nopti nospectre_v2 nospectre_v1 l1tf=off nospec_store_bypass_disable no_stf_barrier mds=off mitigations=off
#追加nopti nospectre_v2到内核启动参数中
sudo sed -i 's/\(GRUB_CMDLINE_LINUX=".*\)"/\1 nopti nospectre_v2 nospectre_v1 l1tf=off nospec_store_bypass_disable no_stf_barrier mds=off mitigations=off transparent_hugepage=always"/' /etc/default/grub

//从修改的 /etc/default/grub 生成 /boot/grub2/grub.cfg 配置
//如果是uefi引导,则是 /boot/efi/EFI/redhat/grub.cfg
sudo grub2-mkconfig -o /boot/grub2/grub.cfg

#limit the journald log to 500M
sed -i 's/^#SystemMaxUse=$/SystemMaxUse=500M/g' /etc/systemd/journald.conf
#重启系统
#sudo reboot

## 选择不同的kernel启动
#sudo grep "menuentry " /boot/grub2/grub.cfg | grep -n menu
##grub认的index从0开始数的
#sudo grub2-reboot 0; sudo reboot
or
#grub2-set-default "CentOS Linux (3.10.0-1160.66.1.el7.x86_64) 7 (Core)" ; sudo reboot

GRUB 2 reads its configuration from the /boot/grub2/grub.cfg file on traditional BIOS-based machines and from the /boot/efi/EFI/redhat/grub.cfg file on UEFI machines. This file contains menu information.

The GRUB 2 configuration file, grub.cfg, is generated during installation, or by invoking the /usr/sbin/grub2-mkconfig utility, and is automatically updated by grubby each time a new kernel is installed. When regenerated manually using grub2-mkconfig, the file is generated according to the template files located in /etc/grub.d/, and custom settings in the /etc/default/grub file. Edits of grub.cfg will be lost any time grub2-mkconfig is used to regenerate the file, so care must be taken to reflect any manual changes in /etc/default/grub as well.

查看kernel编译参数

一般在 /boot/config-** 文件内放置所有内核编译参数

1
2
3
4
5
6
7
//启用 tcp_rt 模块
cat /boot/config-4.19.91-24.8.an8.x86_64 |grep TCP_RT
CONFIG_TCP_RT=y

//启用 RPS
cat /boot/config-4.19.91-24.8.an8.x86_64 |grep RPS
CONFIG_RPS=y

修改是否启用透明大页

1
2
$cat /sys/kernel/mm/transparent_hugepage/enabled
always [madvise] never

制作启动盘

Windows 上用 UltraISO、rufus 烧制,macOS 上就比较简单了,直接用 dd 就可以做好:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ diskutil list
/dev/disk6 (external, physical):
#: TYPE NAME SIZE IDENTIFIER
0: *31.5 GB disk6

# 找到 U 盘的那个设备,umount
$ diskutil unmountDisk /dev/disk3

# 用 dd 把 ISO 文件写进设备,注意这里是 rdisk3 而不是 disk3,在 BSD 中 r(IDENTIFIER)
# 代表了 raw device,会快很多
$ sudo dd if=/path/image.iso of=/dev/rdisk3 bs=1m

# 弹出 U 盘
$ sudo diskutil eject /dev/disk3

Linux 下制作步骤

1
2
3
umount /dev/sdn1
sudo mkfs.vfat /dev/sdn1
dd if=/data/uniontechos-server-20-1040d-amd64.iso of=/dev/sdn1 status=progress

iommu passthrough

在内核参数中加上iommu.passthrough=1 可以关闭iommu,iommu.strict=0是nostrict模式,iommu.strict=1是strict模式(这种性能较差),也是默认的模式。Strict和nostrict主要是处理 无效TLB中缓存的页表项 的方法不同, 一种是批量处理, 一种是一次处理一个。

在X86中加 intel_iommu=off 去关闭的。

IOMMU 硬件单元

DMA Remapping Feature 的工作是通过 CPU 硬件平台的 IOMMU(I/O MMU,Input/Output Memory Management Unit,I/O 内存管理硬件单元)来完成的。IOMMU 的出现,实现了地址空间上的隔离,使设备只能访问规定的内存区域。

image-20220718111233654

参考资料:https://lenovopress.lenovo.com/lp1467.pdf

image-20220729162624318

1
2
3
4
5
6
7
/*
* This variable becomes 1 if iommu=pt is passed on the kernel command line.
* If this variable is 1, IOMMU implementations do no DMA translation for
* devices and allow every device to access to whole physical memory. This is
* useful if a user wants to use an IOMMU only for KVM device assignment to
* guests and not for driver dma translation.
*/

说明配置了iommu=pt 的话函数iommu_no_mapping返回1,那么驱动就直接return paddr,并不会真正调用到domain_pfn_mapping,直接用了物理地址少了一次映射性能当然会高一些。如果是跑KVM建议 passthrough=0,物理机场景 passthrough=1

iommu=pt并不会影响kvm/dpdk/spdk的性能,这三者本质上都是用户态驱动,iommu=pt只会影响内核驱动,能让内核驱动设备性能更高。

SMMU:

ChatGPT:SMMU代表的是”System MMU”,是一种硬件单元,通常用于处理设备DMA(直接内存访问)请求,以允许安全而有效地使用设备,同时保护系统内存不受意外访问和恶意攻击。SMMU的主要功能是将设备发出的DMA请求映射到正确的物理内存地址,同时确保设备无法访问不属于其权限范围的内存区域。SMMU通常与ARM和其他芯片架构一起使用,以提高系统安全性和性能。

Google: SMMU(System Memory Management Unit)是Arm平台的IOMMU, SMMU为设备提供用设备可见的IOVA地址来访问物理内存的能力,体系结构中可能存在多个设备使用IOVA经过IOMMU来访问物理内存,IOMMU需要能够区分不同的设备,从而为每个设备引入了一个Stream ID,指向对应的STE(Stream Table Entry),所有的STE在内存中以数组的形式存在,SMMU记录STE数组的首地址。在操作系统扫描设备的时候会为其分配独有的Stream ID简称sid,设备通过IOMMU进行访存的所有配置都写在对应sid的STE中。

在非虚拟化场景下使能IOMMU/SMMU会带来性能衰减,主要是因为在DMA场景下要iova 到 pa的翻译,带来开销。当前集团的ARM机型,在非云化环境下都是SMMU OFF的,云化机型才是开启SMMU。

定制内存

物理内存700多G,要求OS只能用512G:

1
2
3
4
5
6
7
8
9
10
11
12
24条32G的内存条,总内存768G
# dmidecode -t memory |grep "Size: 32 GB"
Size: 32 GB
…………
Size: 32 GB
Size: 32 GB
root@uos15:/etc# dmidecode -t memory |grep "Size: 32 GB" | wc -l
24

# cat /boot/grub/grub.cfg |grep 512
linux /vmlinuz-4.19.0-arm64-server root=UUID=dbc68010-8c36-40bf-b794-271e59ff5727 ro splash quiet console=tty video=VGA-1:1280x1024@60 mem=512G DEEPIN_GFXMODE=$DEEPIN_GFXMODE
linux /vmlinuz-4.19.0-arm64-server root=UUID=dbc68010-8c36-40bf-b794-271e59ff5727 ro splash quiet console=tty video=VGA-1:1280x1024@60 mem=512G DEEPIN_GFXMODE=$DEEPIN_GFXMODE

高级版 按numa限制内存

每个numa 128G内存,总共1024G(32条*32G),8个numa node,需要将每个numa node内存限制在64G

在grub中cmdline中加入如下配置,每个node只用64G内存:

1
memmap=64G\$64G memmap=64G\$192G memmap=64G\$320G memmap=64G\$448G memmap=64G\$576G memmap=64G\$704G memmap=64G\$832G memmap=64G\$960G

或者:

1
2
3
4
5
6
7
8
9
10
11
#cat /etc/default/grub
GRUB_TIMEOUT=5
GRUB_DISTRIBUTOR="$(sed 's, release .*$,,g' /etc/system-release)"
GRUB_DEFAULT=saved
GRUB_DISABLE_SUBMENU=true
GRUB_TERMINAL_OUTPUT="console"
GRUB_CMDLINE_LINUX="crashkernel=1024M,high resume=/dev/mapper/klas-swap rd.lvm.lv=klas/root rd.lvm.lv=klas/swap video=efifb:on rhgb quiet quiet noibrs noibpb nopti nospectre_v2 nospectre_v1 l1tf=off nospec_store_bypass_disable no_stf_barrier mds=off tsx=on tsx_async_abort=off mitigations=off iommu.passthrough=1 memmap=64G\\\$64G memmap=64G\\\$192G memmap=64G\\\$320G memmap=64G\\\$448G memmap=64G\\\$576G memmap=64G\\\$704G memmap=64G\\\$832G memmap=64G\\\$960G"
GRUB_DISABLE_RECOVERY="true"

然后执行生成最终grub启动参数
sudo grub2-mkconfig -o /boot/grub2/grub.cfg

比如在一个4node的机器上,总共768G内存(32G*24),每个node使用64G内存

1
linux16 /vmlinuz-0-rescue-e91413f0be2c4c239b4aa0451489ae01 root=/dev/mapper/centos-root ro crashkernel=auto rd.lvm.lv=centos/root rd.lvm.lv=centos/swap rhgb quiet memmap=128G\$64G memmap=128G\$256G memmap=128G\$448G memmap=128G\$640G

128G表示相对地址,$64G是绝对地址,128G\$64G 的意思是屏蔽64G到(64+128)G的地址对应的内存

检查

检查正在运行的系统使用的grub参数:

1
cat /proc/cmdline

内存信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#dmidecode -t memory
# dmidecode 3.2
Getting SMBIOS data from sysfs.
SMBIOS 3.2.1 present.
# SMBIOS implementations newer than version 3.2.0 are not
# fully supported by this version of dmidecode.

Handle 0x0033, DMI type 16, 23 bytes
Physical Memory Array
Location: System Board Or Motherboard
Use: System Memory
Error Correction Type: Multi-bit ECC
Maximum Capacity: 2 TB //最大支持2T
Error Information Handle: 0x0032
Number Of Devices: 32 //32个插槽

Handle 0x0041, DMI type 17, 84 bytes
Memory Device
Array Handle: 0x0033
Error Information Handle: 0x0040
Total Width: 72 bits
Data Width: 64 bits
Size: 32 GB
Form Factor: DIMM
Set: None
Locator: CPU0_DIMMA0
Bank Locator: P0 CHANNEL A
Type: DDR4
Type Detail: Synchronous Registered (Buffered)
Speed: 2933 MT/s //内存最大频率
Manufacturer: SK Hynix
Serial Number: 220F9EC0
Asset Tag: Not Specified
Part Number: HMAA4GR7AJR8N-WM
Rank: 2
Configured Memory Speed: 2400 MT/s //内存实际运行速度--比如内存条数插太多会给内存降频
Minimum Voltage: 1.2 V
Maximum Voltage: 1.2 V
Configured Voltage: 1.2 V
Memory Technology: DRAM
Memory Operating Mode Capability: Volatile memory
Module Manufacturer ID: Bank 1, Hex 0xAD
Non-Volatile Size: None
Volatile Size: 32 GB

#lshw
*-bank:19
description: DIMM DDR4 Synchronous Registered (Buffered) 2933 MHz (0.3 ns) //内存最大频率
product: HMAA4GR7AJR8N-WM
vendor: SK Hynix
physical id: 13
serial: 220F9F63
slot: CPU1_DIMMB0
size: 32GiB //实际所插内存大小
width: 64 bits
clock: 2933MHz (0.3ns)

内存速度对延迟的影响

左边两列是同一种机型和CPU、内存,只是最左边的开了numa,他们的内存Speed: 2400 MT/s,但是实际运行速度是2133;最右边的是另外一种CPU,内存速度更快,用mlc测试他们的延时、带宽。可以看到V52机型带宽能力提升特别大,时延变化不大

image-20220123094155595

image-20220123094928794

image-20220123100052242

对比一下V62,intel8269 机型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
./Linux/mlc
Intel(R) Memory Latency Checker - v3.9
Measuring idle latencies (in ns)...
Numa node
Numa node 0 1
0 77.9 143.2
1 144.4 78.4

Measuring Peak Injection Memory Bandwidths for the system
Bandwidths are in MB/sec (1 MB/sec = 1,000,000 Bytes/sec)
Using all the threads from each core if Hyper-threading is enabled
Using traffic with the following read-write ratios
ALL Reads : 225097.1
3:1 Reads-Writes : 212457.8
2:1 Reads-Writes : 210628.1
1:1 Reads-Writes : 199315.4
Stream-triad like: 190341.4

Measuring Memory Bandwidths between nodes within system
Bandwidths are in MB/sec (1 MB/sec = 1,000,000 Bytes/sec)
Using all the threads from each core if Hyper-threading is enabled
Using Read-only traffic type
Numa node
Numa node 0 1
0 113139.4 50923.4
1 50916.6 113249.2

Measuring Loaded Latencies for the system
Using all the threads from each core if Hyper-threading is enabled
Using Read-only traffic type
Inject Latency Bandwidth
Delay (ns) MB/sec
==========================
00000 261.50 225452.5
00002 263.79 225291.6
00008 269.02 225184.1
00015 261.96 225757.6
00050 260.56 226013.2
00100 264.27 225660.1
00200 130.61 195882.4
00300 102.65 133820.1
00400 95.04 101353.2
00500 91.56 81585.9
00700 87.94 58819.1
01000 85.54 41551.3
01300 84.70 32213.6
01700 83.14 24872.5
02500 81.74 17194.3
03500 81.14 12524.2
05000 80.74 9013.2
09000 80.09 5370.0
20000 78.92 2867.2

Measuring cache-to-cache transfer latency (in ns)...
Local Socket L2->L2 HIT latency 51.6
Local Socket L2->L2 HITM latency 51.7
Remote Socket L2->L2 HITM latency (data address homed in writer socket)
Reader Numa Node
Writer Numa Node 0 1
0 - 111.3
1 111.1 -
Remote Socket L2->L2 HITM latency (data address homed in reader socket)
Reader Numa Node
Writer Numa Node 0 1
0 - 175.8
1 176.7 -

[root@numaopen.cloud.et93 /home/admin]
#lscpu
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Byte Order: Little Endian
CPU(s): 104
On-line CPU(s) list: 0-103
Thread(s) per core: 2
Core(s) per socket: 26
Socket(s): 2
NUMA node(s): 2
Vendor ID: GenuineIntel
CPU family: 6
Model: 85
Model name: Intel(R) Xeon(R) Platinum 8269CY CPU @ 2.50GHz
Stepping: 7
CPU MHz: 3199.902
CPU max MHz: 3800.0000
CPU min MHz: 1200.0000
BogoMIPS: 4998.89
Virtualization: VT-x
L1d cache: 32K
L1i cache: 32K
L2 cache: 1024K
L3 cache: 36608K
NUMA node0 CPU(s): 0-25,52-77
NUMA node1 CPU(s): 26-51,78-103

#dmidecode -t memory
Handle 0x003C, DMI type 17, 40 bytes
Memory Device
Array Handle: 0x0026
Error Information Handle: Not Provided
Total Width: 72 bits
Data Width: 64 bits
Size: 32 GB
Form Factor: DIMM
Set: None
Locator: CPU1_DIMM_E1
Bank Locator: NODE 2
Type: DDR4
Type Detail: Synchronous
Speed: 2666 MHz
Manufacturer: Samsung
Serial Number: 14998029
Asset Tag: CPU1_DIMM_E1_AssetTag
Part Number: M393A4K40BB2-CTD
Rank: 2
Configured Clock Speed: 2666 MHz
Minimum Voltage: 1.2 V
Maximum Voltage: 1.2 V
Configured Voltage: 1.2 V

BIOS定制

ipmitool

直接在linux内设置主板bios,然后重启就可以生效:

1
2
3
4
5
6
7
//Hygon C86 7260 24-core Processor 设置numa node(hygon 7280 就不行了)
ipmitool raw 0x3e 0x5c 0x00 0x01 0x81
ipmitool raw 0x3e 0x5c 0x05 0x01 0x81

//查询是否生效, 注意是 0x5d
#ipmitool raw 0x3e 0x5d 0x00 0x01
01 81

ipmitool使用

基本语法:ipmitool raw 0x3e 0x5c index 0x01 value

raw 0x3e 0x5c 固定不需要改,

Index表示需要修改的配置项, 接下来的 0x01 固定不需要改

value 表示值,0x81表示enable; 0x80表示disable

中间件的vipclient服务在centos7上域名解析失败

发表于 2019-01-13 | 分类于 DNS

中间件的vipclient服务在centos7上域名解析失败

我们申请了一批ECS,操作系统是centos7,这些ECS部署了中间件的DNS服务(vipclient),但是发现这个时候域名解析失败,而同样的配置在centos6上就运行正确

抓包分析

分别在centos6、centos7上nslookup通过同一个DNS Server解析同一个域名,并抓包比较得到如下截图(为了方便我将centos6、7抓包做到了一张图上):

image.png

绿色部分是正常的解析(centos6),红色部分是解析,多了一个OPT(centos7)

赶紧Google一下OPT,原来DNS协议还有一个extention,参考这里:

而centos7默认启用edns,但是vipclient实现的时候没有支持edns,所以 centos7 解析域名就出了问题

通过 dig 命令来查看dns解析过程

在centos7上,通过命令 dig edas.console.cztest.com 解析失败,但是改用这个命令禁用edns后就解析正常了:dig +noedns edas.console.cztest.com

vipclient会启动一个53端口,在上面监听dns query,也就是自己就是一个DNS Service

img

分析vipclient域名解析返回的包内容

image.png

把上图中最后4个16进制翻译成10进制IP地址,这个IP地址正是域名所对应的IP,可见vipclient收到域名解析后,因为看不懂edns协议,就按照自己的理解返回了结果,客户端收到这个结果后按照edns协议解析不出来IP,也就是两个的协议不对等导致了问题

总结

centos7之前默认都不启用edns,centos7后默认启用edns,但是vipclient目前不支持edns
通过命令:dig +noedns edas.console.cztest.com 能解析到域名所对应的IP
但是命令:dig edas.console.cztest.com 解析不到IP,因为vipclient(相当于这里的dns server)没有兼容edns,实际返回的结果带了IP但是客户端不支持edns协议所以解析不到(vipclient返回的格式、规范不对)

Docker中的DNS解析过程

发表于 2019-01-12 | 分类于 DNS

Docker中的DNS解析过程

问题描述

同一个Docker集群中两个容器中通过 nslookup 同一个域名,这个域名是容器启动的时候通过net alias指定的,但是返回来的IP不一样

如图所示:

image.png

图中上面的容器中 nslookup 返回来了两个IP,但是只有146是正确的,155是多出来,不对的。

要想解决这个问题抓包就没用了,因为Docker 中的net alias 域名是 Docker Daemon自己来解析的,也就是在容器中做域名解析(nslookup、ping)的时候,Docker Daemon先看这个域名是不是net alias的,是的话返回对应的ip,如果不是(比如 www.baidu.com) ,那么Docker Daemon再把这个域名丢到宿主机上去解析,在宿主机上的解析过程就是标准的DNS,可以抓包分析。但是Docker Daemon内部的解析过程没有走DNS协议,不好分析,所以得先了解一下 Docker Daemon的域名解析原理

具体参考文章: http://www.jianshu.com/p/4433f4c70cf0 http://www.bijishequ.com/detail/261401?p=70-67

继续分析所有容器对这个域名的解析

继续分析所有容器对这个域名的解析发现只有某一台宿主机上的有这个问题,而且这台宿主机上所有容器都有这个问题,结合上面的文章,那么这个问题比较明朗了,这台有问题的宿主机的Docker Daemon中残留了一个net alias,你可以理解成cache中有脏数据没有清理。

进而跟业务的同学们沟通,确实155这个IP的容器做过升级,改动过配置,可能升级前这个155也绑定过这个域名,但是升级后绑到146这个容器上去了,但是Docker Daemon中还残留这条记录。

重启Docker Daemon后问题解决(容器不需要重启)

重启Docker Daemon的时候容器还在正常运行,只是这段时间的域名解析会不正常,其它业务长连接都能正常运行,在Docker Daemon重启的时候它会去检查所有容器的endpoint、重建sandbox、清理network等等各种事情,所以就把这个脏数据修复掉了。

在Docker Daemon重启过程中,会给每个容器构建DNS Resovler(setup-resolver),如果构建DNS Resovler这个过程中容器发送了大量域名查询过来同时这些域名又查询不到的话Docker Daemon在重启过程中需要等待这个查询超时,然后才能继续往下走重启流程,所以导致启动流程拉长问题严重的时候导致Docker Daemon长时间无法启动

Docker的域名解析为什么要这么做,是因为容器中有两种域名解析需求:

  1. 容器启动时通过 net alias 命名的域名
  2. 容器中正常对外网各种域名的解析(比如 baidu.com/api.taobao.com)

对于第一种只能由docker daemon来解析了,所以容器中碰到的任何域名解析都会丢给 docker daemon(127.0.0.11), 如果 docker daemon 发现这个域名不认识,也就是不是net alias命名的域名,那么docker就会把这个域名解析丢给宿主机配置的 nameserver 来解析【非常非常像 dns-f/vipclient 的解析原理】

容器中的域名解析

容器启动的时候读取宿主机的 /etc/resolv.conf (去掉127.0.0.1/16 的nameserver)然后当成容器的 /etc/resolv.conf, 但是实际在容器中看到的 /etc/resolve.conf 中的nameserver只有一个:127.0.0.11,因为如上描述nameserver都被代理掉了

容器 -> docker daemon(127.0.0.11) -> 宿主机中的/etc/resolv.conf 中的nameserver

如果宿主机中的/etc/resolv.conf 中的nameserver没有,那么daemon默认会用8.8.8.8/8.8.4.4来做下一级dns server,如果在一些隔离网络中(跟外部不通),那么域名解析就会超时,因为一直无法连接到 8.8.8.8/8.8.4.4 ,进而导致故障。

比如 vipserver 中需要解析 armory的域名,如果这个时候在私有云环境,宿主机又没有配置 nameserver, 那么这个域名解析会发送给 8.8.8.8/8.8.4.4 ,长时间没有响应,超时后 vipserver 会关闭自己的探活功能,从而导致 vipserver 基本不可用一样。

修改 宿主机的/etc/resolv.conf后 重新启动、创建的容器才会load新的nameserver

如果容器中需要解析vipserver中的域名

  1. 容器中安装vipclient,同时容器的 /etc/resolv.conf 配置 nameserver 127.0.0.1
  2. 要保证vipclient起来之后才能启动业务

kubernetes中dns解析偶尔5秒钟超时

dns解析默认会发出ipv4和ipv6,一般dns没有配置ipv6,会导致ipv6解析等待5秒超时后再发出ipv4解析得到正确结果。应用表现出来就是偶尔卡顿了5秒

img

(高亮行delay 5秒才发出查询,是因为高亮前一行等待5秒都没有等到查询结果)

解析异常的strace栈:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
不正常解析的strace日志:
1596601737.655724 socket(PF_INET, SOCK_DGRAM|SOCK_NONBLOCK, IPPROTO_IP) = 5
1596601737.655784 connect(5, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("10.68.0.2")}, 16) = 0
1596601737.655869 poll([{fd=5, events=POLLOUT}], 1, 0) = 1 ([{fd=5, revents=POLLOUT}])
1596601737.655968 sendmmsg(5, {{{msg_name(0)=NULL, msg_iov(1)=[{"\20\v\1\0\0\1\0\0\0\0\0\0\20redis-7164-b5lzv\7cluster\5local\0\0\1\0\1", 48}], msg_controllen=0, msg_flags=MSG_TRUNC|MSG_EOR|MSG_FIN|MSG_RST|MSG_ERRQUEUE|MSG_NOSIGNAL|MSG_MORE|MSG_WAITFORONE|MSG_FASTOPEN|0x1e340010}, 48}, {{msg_name(0)=NULL, msg_iov(1)=[{"\207\250\1\0\0\1\0\0\0\0\0\0\20redis-7164-b5lzv\7cluster\5local\0\0\34\0\1", 48}], msg_controllen=0, msg_flags=MSG_WAITALL|MSG_FIN|MSG_ERRQUEUE|MSG_NOSIGNAL|MSG_FASTOPEN|MSG_CMSG_CLOEXEC|0x156c0000}, 48}}, 2, MSG_NOSIGNAL) = 2
1596601737.656113 poll([{fd=5, events=POLLIN}], 1, 5000) = 1 ([{fd=5, revents=POLLIN}])
1596601737.659251 ioctl(5, FIONREAD, [141]) = 0
1596601737.659330 recvfrom(5, "\207\250\201\203\0\1\0\0\0\1\0\0\20redis-7164-b5lzv\7cluster\5local\0\0\34\0\1\7cluster\5local\0\0\6\0\1\0\0\0\10\0D\2ns\3dns\7cluster\5local\0\nhostmaster\7cluster\5local\0_*5T\0\0\34 \0\0\7\10\0\1Q\200\0\0\0\36", 2048, 0, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("10.68.0.2")}, [16]) = 141
=========
1596601737.659421 poll([{fd=5, events=POLLIN}], 1, 4996) = 0 (Timeout) //这里就是问题所在
=========
1596601742.657639 poll([{fd=5, events=POLLOUT}], 1, 0) = 1 ([{fd=5, revents=POLLOUT}])
1596601742.657735 sendto(5, "\20\v\1\0\0\1\0\0\0\0\0\0\20redis-7164-b5lzv\7cluster\5local\0\0\1\0\1", 48, MSG_NOSIGNAL, NULL, 0) = 48
1596601742.657837 poll([{fd=5, events=POLLIN}], 1, 5000) = 1 ([{fd=5, revents=POLLIN}])
1596601742.660929 ioctl(5, FIONREAD, [141]) = 0
1596601742.661038 recvfrom(5, "\20\v\201\203\0\1\0\0\0\1\0\0\20redis-7164-b5lzv\7cluster\5local\0\0\1\0\1\7cluster\5local\0\0\6\0\1\0\0\0\3\0D\2ns\3dns\7cluster\5local\0\nhostmaster\7cluster\5local\0_*5T\0\0\34 \0\0\7\10\0\1Q\200\0\0\0\36", 2048, 0, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("10.68.0.2")}, [16]) = 141
1596601742.661129 poll([{fd=5, events=POLLOUT}], 1, 4996) = 1 ([{fd=5, revents=POLLOUT}])
1596601742.661204 sendto(5, "\207\250\1\0\0\1\0\0\0\0\0\0\20redis-7164-b5lzv\7cluster\5local\0\0\34\0\1", 48, MSG_NOSIGNAL, NULL, 0) = 48
1596601742.661313 poll([{fd=5, events=POLLIN}], 1, 4996) = 1 ([{fd=5, revents=POLLIN}])
1596601742.664443 ioctl(5, FIONREAD, [141]) = 0
1596601742.664519 recvfrom(5, "\207\250\201\203\0\1\0\0\0\1\0\0\20redis-7164-b5lzv\7cluster\5local\0\0\34\0\1\7cluster\5local\0\0\6\0\1\0\0\0\3\0D\2ns\3dns\7cluster\5local\0\nhostmaster\7cluster\5local\0_*5T\0\0\34 \0\0\7\10\0\1Q\200\0\0\0\36", 65536, 0, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("10.68.0.2")}, [16]) = 141
1596601742.664600 close(5) = 0

原因分析

DNS client (glibc 或 musl libc) 会并发请求 A 和 AAAA 记录,跟 DNS Server 通信自然会先 connect (建立fd),后面请求报文使用这个 fd 来发送,由于 UDP 是无状态协议, connect 时并不会发包,也就不会创建 conntrack 表项, 而并发请求的 A 和 AAAA 记录默认使用同一个 fd 发包,send 时各自发的包它们源 Port 相同(因为用的同一个socket发送),当并发发包时,两个包都还没有被插入 conntrack 表项,所以 netfilter 会为它们分别创建 conntrack 表项,而集群内请求 kube-dns 或 coredns 都是访问的CLUSTER-IP,报文最终会被 DNAT 成一个 endpoint 的 POD IP,当两个包恰好又被 DNAT 成同一个 POD IP时,它们的五元组就相同了,在最终插入的时候后面那个包就会被丢掉,而single-request-reopen的选项设置为俩请求被丢了一个,会等待超时再重发 ,这个就解释了为什么还存在调整成2s就是2s的异常比较多 ,因此这种场景下调整成single-request是比较好的方式,同时k8s那边给的dns缓存方案是 nodelocaldns组件可以考虑用一下

关于recolv的选项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
single-request (since glibc 2.10) 串行解析,
Sets RES_SNGLKUP in _res.options. By default, glibc
performs IPv4 and IPv6 lookups in parallel since
version 2.9. Some appliance DNS servers cannot handle
these queries properly and make the requests time out.
This option disables the behavior and makes glibc
perform the IPv6 and IPv4 requests sequentially (at the
cost of some slowdown of the resolving process).
single-request-reopen (since glibc 2.9) 并行解析,少收到一个解析回复后,再开一个socket重新发起解析,因此看到了前面调整timeout是1s后,还是有挺多1s的解析
Sets RES_SNGLKUPREOP in _res.options. The resolver
uses the same socket for the A and AAAA requests. Some
hardware mistakenly sends back only one reply. When
that happens the client system will sit and wait for
the second reply. Turning this option on changes this
behavior so that if two requests from the same port are
not handled correctly it will close the socket and open
a new one before sending the second request.

getaddrinfo 关闭ipv6的解析

基本上所有测试下来,网上那些通过修改配置的基本都不能关闭ipv6的解析,只有通过在代码中指定

hints.ai_family = AF_INET; /* or AF_INET6 for ipv6 addresses */

来只做ipv4的解析

Prefer A (IPv4) DNS lookups before AAAA(IPv6) lookups

https://man7.org/linux/man-pages/man3/getaddrinfo.3.html:

1
2
3
4
5
6
7
8
9
If hints.ai_flags includes the AI_ADDRCONFIG flag, then IPv4
addresses are returned in the list pointed to by res only if the
local system has at least one IPv4 address configured, and IPv6
addresses are returned only if the local system has at least one
IPv6 address configured. The loopback address is not considered
for this case as valid as a configured address. This flag is
useful on, for example, IPv4-only systems, to ensure that
getaddrinfo() does not return IPv6 socket addresses that would
always fail in connect(2) or bind(2).

c code demo:

1
2
3
4
5
6
7
struct addrinfo hints, *result;
int s;

memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_INET; /* or AF_INET6 for ipv6 addresses */
s = getaddrinfo(NULL, "ftp", &hints, &result);
...

or

In the Wireshark capture, 172.25.50.3 is the local DNS resolver; the capture was taken there, so you also see its outgoing queries and responses. Note that only an A record was requested. No AAAA lookup was ever done.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <stdlib.h>
#include <netdb.h>
#include <stdio.h>

int main(void) {
struct addrinfo hints;
struct addrinfo *result, *rp;
int s;
char host[256];

memset(&hints, 0, sizeof(struct addrinfo));
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = 0;

s = getaddrinfo("www.facebook.com", NULL, &hints, &result);
if (s != 0) {
fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(s));
exit(EXIT_FAILURE);
}

for (rp = result; rp != NULL; rp = rp->ai_next) {
getnameinfo(rp->ai_addr, rp->ai_addrlen, host, sizeof(host), NULL, 0, NI_NUMERICHOST);
printf("%s\n", host);
}
freeaddrinfo(result);
}

or:https://unix.stackexchange.com/questions/9940/convince-apt-get-not-to-use-ipv6-method

/etc/gai.conf getaddrinfo的配置文件

Prefix Precedence Label Usage
::1/128 50 0 Localhost
::/0 40 1 Default unicast
::ffff:0:0/96 35 4 IPv4-mapped IPv6 address
2002::/16 30 2 6to4
2001::/32 5 5 Teredo tunneling
fc00::/7 3 13 Unique local address
::/96 1 3 IPv4-compatible addresses (deprecated)
fec0::/10 1 11 Site-local address (deprecated)
3ffe::/16 1 12 6bone (returned)

来源于维基百科

0:0:0:0:0:ffff:0:0/96 10 4 IPv4映射地址(这个地址网络上信息较少,地址范围::: ffff:0.0.0.0~:: ffff:255.255.255.255 地址数量2 128−96 = 2 32 = 4 294 967 296,用于软件,目的是IPv4映射的地址。 )

参考资料

Kubernetes >= 1.13 + kube-proxy IPVS mode 服务部署不平滑

linux ipv4 ipv6双栈 (优先ipv4而不使用ipv6配置)

windows7的wifi总是报DNS域名异常无法上网

发表于 2019-01-10 | 分类于 DNS

windows7的wifi总是报DNS域名异常无法上网

Windows7笔记本+公司wifi(dhcp)环境下,用着用着dns服务不可用(无法通过域名上网,通过IP地址可以访问),这里有个一模一样的Case了:https://superuser.com/questions/629559/why-is-my-computer-suddenly-using-nbns-instead-of-dns 一样的环境,看来这个问题也不只是我一个人碰到了。

其实之前一直有,一个月偶尔出来一两次,以为是其他原因就没管,这次换了新电脑还是这个毛病有点不能忍,于是决定彻底解决一下。

这个问题出现后,通过下面三个办法都可以让DNS恢复正常:

  1. 重启系统大法,恢复正常
  2. 禁用wifi驱动再启用,恢复正常
  3. 不用DHCP,而是手工填入一个DNS服务器,比如114.114.114.114【公司域名就无法解析了】

如果只是停用一下wifi再启用问题还在。

找IT升级了网卡驱动不管用

重现的时候抓包看看

image.png

这肯定不对了,254上根本就没有跑DNS服务,可是当时没有检查 ipconfig,看看是否将网关IP动态配置到dns server里面去了,等下次重现后再确认吧。

第二次重现后抓包,发现不一样了:

image.png

出来一个 NBNS 的鬼东西,赶紧查了一下,把它禁掉,如下图所示:

image.png

把NBNS服务关了就能上网了,同时也能抓到各种DNS Query包

事情没有这么简单

过一段时间后还是会出现上面的症状,但是因为NBNS关闭了,所以这次 ping www.baidu.com 的时候没有任何包了,没有DNS Query包,也没有NBNS包,这下好尴尬。

尝试Enable NBNS,又恢复了正常,看来开关 NBNS 仍然只是一个workaround,他不是导致问题的根因,开关一下没有真正解决问题,只是临时相当于重启了dns修复了问题而已。

继续在网络不通的时候尝试直接ping dns server ip,发现一个奇怪的现象,丢包很多,丢包的时候还总是从 192.168.0.11返回来的,这就奇怪了,我的笔记本基本IP是30开头的,dns server ip也是30开头的,route 路由表也是对的,怎么就走到 192.168.0.11 上了啊(参考我的另外一篇文章,网络到底通不通),赶紧 ipconfig /all | grep 192

image.png

发现这个IP是VirtualBox虚拟机在笔记本上虚拟出来的网卡IP,这下我倒是能理解为啥总是我碰到这个问题了,因为我的工作笔记本一拿到后第一件事情就是装VirtualBox 跑虚拟机。

VirtualBox为啥导致了这个问题就是一个很偏的方向,我实在无能为力了,尝试找到了一个和VirtualBox的DNS相关的开关命令,只能死马当活马医了(像极了算命大师和老中医)

./VBoxManage.exe  modifyvm "ubuntu" --natdnshostresolver1 on

执行完上面的命令观察了3个月了,暂时没有再出现这个问题,相对于以前轻则一个月2、3次,重则一天出现5、6次,应该算是解决了,同时升级 VirtualBox 也无法解决这个问题。

route 信息:

$route PRINT -4
===========================================================================
接口列表
 23...00 ff c1 57 7f 12 ......Sangfor SSL VPN CS Support System VNIC
 18...f6 96 34 38 76 06 ......Microsoft Virtual WiFi Miniport Adapter #2
 17...f6 96 34 38 76 07 ......Microsoft Virtual WiFi Miniport Adapter
 15...00 ff 1f 24 e6 6c ......Sophos SSL VPN Adapter
 12...f4 96 34 38 76 06 ......Intel(R) Dual Band Wireless-AC 8260
 11...54 ee 75 d4 99 ae ......Intel(R) Ethernet Connection I219-V
 14...0a 00 27 00 00 0e ......VirtualBox Host-Only Ethernet Adapter
  1...........................Software Loopback Interface 1
 25...00 00 00 00 00 00 00 e0 Microsoft ISATAP Adapter
 19...00 00 00 00 00 00 00 e0 Microsoft ISATAP Adapter #9
 26...00 00 00 00 00 00 00 e0 Microsoft ISATAP Adapter #2
 27...00 00 00 00 00 00 00 e0 Microsoft ISATAP Adapter #3
 22...00 00 00 00 00 00 00 e0 Microsoft ISATAP Adapter #7
 21...00 00 00 00 00 00 00 e0 Microsoft ISATAP Adapter #5
 20...00 00 00 00 00 00 00 e0 Microsoft 6to4 Adapter #2
 16...00 00 00 00 00 00 00 e0 Teredo Tunneling Pseudo-Interface
 24...00 00 00 00 00 00 00 e0 Microsoft ISATAP Adapter #8
===========================================================================

IPv4 路由表
===========================================================================
活动路由:
网络目标网络掩码  网关   接口   跃点数
  0.0.0.0  0.0.0.0192.168.0.250169.254.24.89266
  0.0.0.0  0.0.0.030.27.115.254 30.27.112.21 20
  30.27.112.0255.255.252.0在链路上  30.27.112.21276
 30.27.112.21  255.255.255.255在链路上  30.27.112.21276
30.27.115.255  255.255.255.255在链路上  30.27.112.21276
127.0.0.0255.0.0.0在链路上 127.0.0.1306
127.0.0.1  255.255.255.255在链路上 127.0.0.1306
  127.255.255.255  255.255.255.255在链路上 127.0.0.1306
  169.254.0.0  255.255.0.0在链路上 169.254.24.89266
169.254.24.89  255.255.255.255在链路上 169.254.24.89266
  169.254.255.255  255.255.255.255在链路上 169.254.24.89266
224.0.0.0240.0.0.0在链路上 127.0.0.1306
224.0.0.0240.0.0.0在链路上 169.254.24.89266
224.0.0.0240.0.0.0在链路上  30.27.112.21276
  255.255.255.255  255.255.255.255在链路上 127.0.0.1306
  255.255.255.255  255.255.255.255在链路上 169.254.24.89266
  255.255.255.255  255.255.255.255在链路上  30.27.112.21276
===========================================================================
永久路由:
  网络地址  网络掩码  网关地址  跃点数
  0.0.0.0  0.0.0.0192.168.0.250 默认
  0.0.0.0  0.0.0.0192.168.0.250 默认
===========================================================================

另外DHCP也许可以做一些事情,至少同样的用法在以前的公司网络环境没有出过问题

下面是来自微软官方的建议:

One big advise – do not disable the DHCP Client service on any server, whether the machine is a DHCP client or statically configured. Somewhat of a misnomer, this service performs Dynamic DNS registration and is tied in with the client resolver service. If disabled on a DC, you’ll get a slew of errors, and no DNS queries will get resolved.

No DNS Name Resolution If DHCP Client Service Is Not Running. When you try to resolve a host name using Domain Name Service (DNS), the attempt is unsuccessful. Communication by Internet Protocol (IP) address (even to …

http://support.microsoft.com/kb/268674

from: https://blogs.msmvps.com/acefekay/2009/11/29/dns-wins-netbios-amp-the-client-side-resolver-browser-service-disabling-netbios-direct-hosted-smb-directsmb-if-one-dc-is-down-does-a-client-logon-to-another-dc-and-dns-forwarders-algorithm/#section4

NBNS也许会导致nslookup OK but ping fail的问题

https://www.experts-exchange.com/questions/28894006/NetBios-name-resolution-instead-of-DNS.html

The Windows Client Resolver(ping dns流程)

  1. Windows checks whether the host name is the same as the local host name.
  2. If the host name and local host name are not the same, Windows searches the DNS client resolver cache.
  3. If the host name cannot be resolved using the DNS client resolver cache, Windows sends DNS Name Query Request messages to its configured DNS servers.
  4. If the host name is a single-label name (such as server1) and cannot be resolved using the configured DNS servers, Windows converts the host name to a NetBIOS name and checks its local NetBIOS name cache.
  5. If Windows cannot find the NetBIOS name in the NetBIOS name cache, Windows contacts its configured WINS servers.
  6. If Windows cannot resolve the NetBIOS name by querying its configured WINS servers, Windows broadcasts as many as three NetBIOS Name Query Request messages on the directly attached subnet.
  7. If there is no reply to the NetBIOS Name Query Request messages, Windows searches the local Lmhosts file.
    Ping

windows下nslookup 流程

  1. Check the DNS resolver cache. This is true for records that were cached via a previous name query or records that are cached as part of a pre-load operation from updating the hosts file.
  2. Attempt NetBIOS name resolution.
  3. Append all suffixes from the suffix search list.
  4. When a Primary Domain Suffix is used, nslookup will only take devolution 3 levels.

总结

碰到问题绕过去也不是长久之计,还是要从根本上了解问题的本质,这个问题在其它公司没有碰到过,我觉得跟公司的DNS、DHCP的配置也有点关系吧,但是这个我不好确认,应该还有好多用Windows本本的同学同样会碰到这个问题的,希望对你们有些帮助

https://support.microsoft.com/en-us/help/172218/microsoft-tcp-ip-host-name-resolution-order

http://www.man7.org/linux/man-pages/man5/resolv.conf.5.html


本文附带鸡汤:

有些技能初学很难,大家水平都差不多,但是日积月累就会形成极强的优势,而且一旦突破某个临界点,它就会突飞猛进,这种技能叫指数型技能,是值得长期投资的,比如物理学就是一种指数型技能。

那么抓包算不算呢?​​

nslookup-OK-but-ping-fail

发表于 2019-01-09 | 分类于 DNS

nslookup 域名结果正确,但是 ping 域名 返回 unknown host

2018-02 update : 最根本的原因 https://access.redhat.com/solutions/1426263

下面让我们来看看这个问题的定位过程

先Google一下: nslookup ok but ping fail, 这个关键词居然被Google自动提示了,看来碰到这个问题同学的好多

Google到的帖子大概有如下原因:

  • 域名最后没有加 . 然后被自动追加了 tbsite.net aliyun.com alidc.net,自然 ping不到了
  • /etc/resolv.conf 配置的nameserver 要保证都是正常服务的
  • /etc/nsswitch.conf 中的这行:hosts: files dns 配置成了 hosts: files mdns dns,而server不支持mdns
  • 域名是单标签的(domain 单标签; domain.com 多标签),单标签在windows下走的NBNS而不是DNS协议

检查完我的环境不是上面描述的情况,比较悲催,居然碰到了一个Google不到的问题

抓包看为什么解析不了

DNS协议是典型的UDP应用,一来一回就搞定了查询,效率比TCP三次握手要高多了,DNS Server也支持TCP,不过一般不用TCP

sudo tcpdump -i eth0 udp and port 53 

抓包发现ping 不通域名的时候都是把域名丢到了 /etc/resolv.conf 中的第二台nameserver,或者根本没有发送 dns查询。

这里要多解释一下我们的环境, /etc/resolv.conf 配置了2台 nameserver,第一台负责解析内部域名,另外一台负责解析其它域名,如果内部域名的解析请求丢到了第二台上自然会解析不到。

所以这个问题的根本原因是挑选的nameserver 不对,按照 /etc/resolv.conf 的逻辑都是使用第一个nameserver,失败后才使用第二、第三个备用nameserver。

比较奇怪,出问题的都是新申请到的一批ECS,仔细对比了一下正常的机器,发现有问题的 ECS /etc/resolv.conf 中放了一个词rotate,赶紧查了一下rotate的作用(轮询多个nameserver),然后把rotate去掉果然就好了。

风波再起

本来以为问题彻底解决了,结果还是有一台机器ping仍然是unknow host,眼睛都看瞎了没发现啥问题,抓包发现总是把dns请求交给第二个nameserver,或者根本不发送dns请求,这就有意思了,跟我们理解的不太一样。

看着像有cache之类的,于是在正常和不正常的机器上使用 strace ,果然发现了点不一样的东西:

image.png

ping的过程中访问了 nscd(name service cache daemon) 同时发现 nscd返回值图中红框的 0,跟正常机器比较发现正常机器红框中是 -1,于是检查 /var/run/nscd/ 下面的东西,kill 掉 nscd进程,然后删掉这个文件夹,再ping,一切都正常了。

从strace来看所有的ping都会尝试看看 nscd 是否在运行,在的话找nscd要域名解析结果,如果nscd没有运行,那么再找 /etc/resolv.conf中的nameserver做域名解析

而nslookup和dig这样的命令就不会尝试找nscd,所以没有这个问题。

如下文字摘自网络:

NSCD(name service cache daemon)是GLIBC关于网络库的一个组件,服务基于glibc开发的各类网络服务,基本上来讲我们能见到的一些编程语言和开发框架最终均会调用到glibc的网络解析的函数(如GETHOSTBYNAME or GETHOSTBYADDR等),因此绝大部分程序能够使用NSCD提供的缓存服务。当然了如果是应用端自己用socker编写了一个网络client就无法使用NSCD提供的缓存服务,比如DNS领域常见的dig命令不会使用NSCD提供的缓存,而作为对比ping得到的DNS解析结果将使用NSCD提供的缓存

connect函数返回值的说明:

RETURN VALUE
   If  the  connection or binding succeeds, zero is returned.  On error, -1 is returned,and errno is set appropriately.

Windows下客户端是默认有dns cache的,但是Linux Client上默认没有dns cache,DNS Server上是有cache的,所以忽视了这个问题。这个nscd是之前看ping不通,google到这么一个命令,但是应该没有搞明白它的作用,就执行了一个网上的命令,把nscd拉起来,然后ping 因为rotate的问题,还是不通,同时nscd cache了这个不通的结果,导致了新的问题

域名解析流程(或者说 glibc的 gethostbyname() 函数流程–背后是NameServiceSwitch)

  • DNS域名解析的时候先根据 /etc/nsswitch.conf 配置的顺序进行dns解析(name service switch),一般是这样配置:hosts: files dns 【files代表 /etc/hosts ; dns 代表 /etc/resolv.conf】(ping是这个流程,但是nslookup和dig不是)
  • 如果本地有DNS Client Cache,先走Cache查询,所以有时候看不到DNS网络包。Linux下nscd可以做这个cache,Windows下有 ipconfig /displaydns ipconfig /flushdns
  • 如果 /etc/resolv.conf 中配置了多个nameserver,默认使用第一个,只有第一个失败【如53端口不响应、查不到域名后再用后面的nameserver顶上】
  • 如果 /etc/resolv.conf 中配置了rotate,那么多个nameserver轮流使用. 但是因为glibc库的原因用了rotate 会触发nameserver排序的时候第二个总是排在第一位

nslookup和dig程序是bind程序包所带的工具,专门用来检测DNS Server的,实现上更简单,就一个目的,给DNS Server发DNS解析请求,没有调gethostbyname()函数,也就不遵循上述流程,而是直接到 /etc/resolv.conf 取第一个nameserver当dns server进行解析

glibc函数

glibc 的解析器(revolver code) 提供了下面两个函数实现名称到 ip 地址的解析, gethostbyname 函数以同步阻塞的方式提供服务, 没有超时等选项, 仅提供 IPv4 的解析. getaddrinfo 则没有这些限制, 同时支持 IPv4, IPv6, 也支持 IPv4 到 IPv6 的映射选项. 包含 Linux 在内的很多系统都已废弃 gethostbyname 函数, 使用 getaddrinfo 函数代替. 不过从现实的情况来看, 还是有很多程序或网络库使用 gethostbyname 进行服务.

备注:
线上开启 nscd 前, 建议做好程序的测试, nscd 仅支持通过 glibc, c 标准机制运行的程序, 没有基于 glibc 运行的程序可能不支持 nscd. 另外一些 go, perl 等编程语言网络库的解析函数是单独实现的, 不会走 nscd 的 socket, 这种情况下程序可以进行名称解析, 但不会使用 nscd 缓存. 不过我们在测试环境中使用go, java 的常规网络库都可以正常连接 nscd 的 socket 进行请求; perl 语言使用 Net::DNS 模块, 不会使用 nscd 缓存; python 语言使用 python-dns 模块, 不会使用 nscd 缓存. python 和 perl 不使用模块的时候进行解析还是遵循上述的过程, 同时使用 nscd 缓存.

下面是glibc中对rotate的处理:

这是glibc 2.2.5(2010年的版本),如果有rotate逻辑就是把第一个nameserver总是丢到最后一个去(为了均衡nameserver的负载,保护第一个nameserver):

image.png

在2017年这个代码逻辑终于改了,不过还不是默认用第一个,而是随机取一个,rotate搞成random了,这样更不好排查问题了

image.png

image.png

也就是2010年之前的glibc版本在rotate模式下都是把第一个nameserver默认挪到最后一个(为了保护第一个nameserver),这样rotate模式下默认第一个nameserver总是/etc/resolov.conf配置文件中的第二个,到2017年改掉了这个问题,不过改成随机取nameserver, 作者不认为这是一个bug,他觉得配置rotate就是要平衡多个nameserver的性能,所以random最公平,因为大多程序都是查一次域名缓存好久,不随机轮询的话第一个nameserver压力太大

参考 glibc bug

Linux内核与glibc

getaddrinfo() is a newer function that was introduced to replace gethostbyname() and provides a more flexible and robust way of resolving hostnames. getaddrinfo() can resolve both IPv4 and IPv6 addresses, and it supports more complex name resolution scenarios, such as service name resolution and name resolution with specific protocol families.

linux主要是基于glibc,分析glibc源码(V2.17)getaddrinfo 逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// search1、search2等是resolv.conf中配置的search域,会根据resolv.conf中ndots配置和qname的级数来决定
// 先查加search域的域名还是后查加search域的域名,除没有配置nameserver外,任何错误都会重试下一个
foreach domain(qname、qname.search1、qname.search2...)
foreach retry // resolv.conf中配置的retry数目
foreach ns // resolv.conf中配置的nameserver,最多有效的只有前三个,增加rotate 的option后,会从
//nameserver中(最多前三个)随机选择一个进行请求
foreach qtype(A、AAAA)
// 根据请求显试指定和系统是双栈还是单栈v4、or单栈v6来决定,如果两个
// 类型的话会同时并发请求,不会等另外一个返回。如果在resov.conf中指定
// single-request-reopen,则会每个类型请求新建一个链接,否则复用同一个
foreach (without_edns0, with_edns0 )
// 注意:默认不支持edns0,如果要支持的话需要设置宏RES_USE_EDNS0,并重编译
if (timeout || rcode == servfail
|| rcode == notimp || rcode == refuse)
// timeout时间第一次为resolv.conf中配置,后面的依据下面公式
//计算:(timeout<<index_of_dns_server)/num_of_dns_server
conftinue;

还有一种情况,开了easyconnect这样的vpn软件,同时又配置了自己的dns server

这个时候ping 某个自己的dns server中才有的域名是没法解析到的,即使你配置了自己的dns server,如果这个时候你通过 nslookup 自己的域名, 自己的dns-server-ip 确实是能够解析到的。但是你只是 nslookup 自己的域名 就不行,明显可以看到这个时候nslookup把域名发给了127.0.0.1:53来解析,而这个端口正是easyconnect这个软件在监听,你也可以理解easyconnect这样的软件的工作方式就是必须要挟持你的dns解析,可以理解的是这个时候nslookup肯定解析不到你的域名(只把dns解析丢给第一个nameserver–127.0.0.1),但是不能理解的是还是ping不通域名,正常ping的逻辑丢给第一个127.0.0.1解析不到域名的话,会丢给第二个dns-server继续尝试解析,但是这里的easyconnect没有进行第二次尝试,这也许是它实现上没有考虑到或者故意这样实现的。

其他情况

resolv.conf中search只能支持最多6个后缀(代码中写死了): This cannot be modified for RHEL 6.x and below and is resolved in RHEL7 glibc package versions at or exceeding glibc-2.17-222.el7.

nameserver:指定nameserver,必须配置,每行指定一个nameserver,最多只能生效3行

总结

  • /etc/resolv.conf rotate参数的关键作用
  • nscd对域名解析的cache
  • nslookup背后执行原理和ping不一样(前者不调glibc的gethostbyname() 函数), nslookup不会检查 /etc/hosts、/etc/nsswitch.conf, 而是直接从 /etc/resolv.conf 中取nameserver; 但是ping或者我们在程序一般最终都是通过调glibc的gethostbyname() 函数对域名进行解析的,也就是按照 /etc/nsswitch.conf 指示的来
  • 在没有源代码的情况下strace和抓包能够看到问题的本质
  • resolv.conf中最多只能使用前六个搜索域

下一篇介绍《在公司网下,我的windows7笔记本的wifi总是报dns域名异常无法上网(通过IP地址可以上网)》困扰了我两年,最近换了新笔记本还是有这个问题才痛下决心咬牙解决

参考资料

https://superuser.com/questions/495759/why-is-ping-unable-to-resolve-a-name-when-nslookup-works-fine

https://stackoverflow.com/questions/330395/dns-problem-nslookup-works-ping-doesnt

来自redhat上原因的描述,但是从代码的原作者的描述来看,他认为rotate下这个行为是合理的

Linux 系统如何处理名称解析

网络丢包

发表于 2018-12-26 | 分类于 network

网络丢包

查看网卡是否丢包,一般是ring buffer太小

ethtool -S eth0 | grep rx_ | grep errors

当驱动处理速度跟不上网卡收包速度时,驱动来不及分配缓冲区,NIC接收到的数据包无法及时写到sk_buffer(由网卡驱动直接在内核中分配的内存,并存放数据包,供内核软中断的时候读取),就会产生堆积,当NIC内部缓冲区写满后,就会丢弃部分数据,引起丢包。这部分丢包为rx_fifo_errors,在 /proc/net/dev中体现为fifo字段增长,在ifconfig中体现为overruns指标增长。

查看ring buffer的大小设置

ethtool ‐g eth0  

Socket buffer太小导致的丢包(一般不多见)

内核收到包后,会给对应的socket,每个socket会有 sk_rmem_alloc/sk_wmem_alloc/sk_omem_alloc、sk_rcvbuf(bytes)来存放包

When sk_rmem_alloc >
sk_rcvbuf the TCP stack will call a routine which “collapses” the receive queue

查看collapses:

netstat -sn | egrep "prune|collap"; sleep 30; netstat -sn | egrep "prune|collap"
17671 packets pruned from receive queue because of socket buffer overrun
18671 packets pruned from receive queue because of socket buffer overrun

测试发现在小包情况下,这两个值相对会增大且比较快。增大 net.ipv4.tcp_rmem 和 net.core.rmem_max、net.core.rmem_default 后没什么效果 – 需要进一步验证

net.core.netdev_budget

sysctl net.core.netdev_budget //默认300, The default value of the budget is 300. This will
cause the SoftIRQ process to drain 300 messages from the NIC before getting off the CPU
如果 /proc/net/softnet_stat 第三列一直在增加的话需要,表示SoftIRQ 获取的CPU时间太短,来不及处理足够多的网络包,那么需要增大这个值
net/core/dev.c->net_rx_action 函数中会按netdev_budget 执行softirq,budget每次执行都要减少,一直到没有了,就退出softirq

一般默认软中断只绑定在CPU0上,如果包的数量巨大的话会导致 CPU0利用率 100%(主要是si),这个时候可以检查文件 /proc/net/softnet_stat 的第三列 或者 RX overruns 是否在持续增大

net.core.netdev_max_backlog

enqueue_to_backlog函数中,会对CPU的softnet_data 实例中的接收队列(input_pkt_queue)进行判断,如果队列中的数据长度超过netdev_max_backlog ,那么数据包将直接丢弃,这就产生了丢包。

参数net.core.netdev_max_backlog指定的,默认大小是 1000。

netdev_max_backlog 接收包队列(网卡收到还没有进行协议的处理队列),每个cpu core一个队列,如果/proc/net/softnet_stat第二列增加就表示这个队列溢出了,需要改大。

/proc/net/softnet_stat:(第一列和第三列的关系?)
The 1st column is the number of frames received by the interrupt handler. (第一列是中断处理程序接收的帧数)
The 2nd column is the number of frames dropped due to netdev_max_backlog being exceeded. netdev_max_backlog
The 3rd column is the number of times ksoftirqd ran out of netdev_budget or CPU time when there was still work to be done net.core.netdev_budget

rp_filter

https://www.yuque.com/plantegg/weyi1s/uc7a5g

关于ifconfig的种种解释

  • RX errors: 表示总的收包的错误数量,这包括 too-long-frames 错误,Ring Buffer 溢出错误,crc 校验错误,帧同步错误,fifo overruns 以及 missed pkg 等等。
  • RX dropped: 表示数据包已经进入了 Ring Buffer,但是由于内存不够等系统原因,导致在拷贝到内存的过程中被丢弃。
  • RX overruns: 表示了 fifo 的 overruns,这是由于 Ring Buffer(aka Driver Queue) 传输的 IO 大于 kernel 能够处理的 IO 导致的,而 Ring Buffer 则是指在发起 IRQ 请求之前的那块 buffer。很明显,overruns 的增大意味着数据包没到 Ring Buffer 就被网卡物理层给丢弃了,而 CPU 无法及时地处理中断是造成 Ring Buffer 满的原因之一,上面那台有问题的机器就是因为 interruprs 分布的不均匀(都压在 core0),没有做 affinity 而造成的丢包。
  • RX frame: 表示 misaligned 的 frames。

dropped数量持续增加,建议增大Ring Buffer ,使用ethtool ‐G 进行设置。

txqueuelen:1000 对应着qdisc队列的长度(发送队列和网卡关联着)

而对应的接收队列由内核参数来设置:

net.core.netdev_max_backlog

Adapter buffer defaults are commonly set to a smaller size than the maximum//网卡进出队列大小调整 ethtool -G eth rx 8192 tx 8192

image.png

核心流程

image.png

接收数据包是一个复杂的过程,涉及很多底层的技术细节,但大致需要以下几个步骤:

  1. 网卡收到数据包。
  2. 将数据包从网卡硬件缓存转移到服务器内存中。
  3. 通知内核处理。
  4. 经过TCP/IP协议逐层处理。
  5. 应用程序通过read()从socket buffer读取数据。

通过 dropwatch来查看丢包点

dropwatch -l kas (-l 加载符号表) // 丢包点位置等于 ip_rcv地址+ cf(偏移量)

image.png

一个典型的接收包调用堆栈:

 0xffffffff8157af10 : tcp_may_send_now+0x0/0x160 [kernel]
 0xffffffff815765f8 : tcp_fastretrans_alert+0x868/0xb50 [kernel]
 0xffffffff8157729d : tcp_ack+0x8bd/0x12c0 [kernel]
 0xffffffff81578295 : tcp_rcv_established+0x1d5/0x750 [kernel]
 0xffffffff81582bca : tcp_v4_do_rcv+0x10a/0x340 [kernel]
 0xffffffff81584411 : tcp_v4_rcv+0x831/0x9f0 [kernel]
 0xffffffff8155e114 : ip_local_deliver_finish+0xb4/0x1f0 [kernel]
 0xffffffff8155e3f9 : ip_local_deliver+0x59/0xd0 [kernel]
 0xffffffff8155dd8d : ip_rcv_finish+0x7d/0x350 [kernel]
 0xffffffff8155e726 : ip_rcv+0x2b6/0x410 [kernel]
 0xffffffff81522d42 : __netif_receive_skb_core+0x582/0x7d0 [kernel]
 0xffffffff81522fa8 : __netif_receive_skb+0x18/0x60 [kernel]
 0xffffffff81523c7e : process_backlog+0xae/0x180 [kernel]
 0xffffffff81523462 : net_rx_action+0x152/0x240 [kernel]
 0xffffffff8107dfff : __do_softirq+0xef/0x280 [kernel]
 0xffffffff8163f61c : call_softirq+0x1c/0x30 [kernel]
 0xffffffff81016fc5 : do_softirq+0x65/0xa0 [kernel]
 0xffffffff8107d254 : local_bh_enable_ip+0x94/0xa0 [kernel]
 0xffffffff81634f4b : _raw_spin_unlock_bh+0x1b/0x40 [kernel]
 0xffffffff8150d968 : release_sock+0x118/0x170 [kernel]

如果客户端建立连接的时候抛异常,可能的原因(握手失败,建不上连接):

  • 网络不通,诊断:ping ip
  • 端口不通, 诊断:telnet ip port
  • rp_filter 命中(rp_filter=1, 多网卡环境), 诊断: netstat -s | grep -i filter ;
  • snat/dnat的时候宿主机port冲突,内核会扔掉 syn包。 troubleshooting: sudo conntrack -S | grep insert_failed //有不为0的
  • 全连接队列满的情况,诊断: netstat -s | egrep “listen|LISTEN”
  • syn flood攻击, 诊断:同上
  • 若远端服务器的内核参数 net.ipv4.tcp_tw_recycle 和 net.ipv4.tcp_timestamps 的值都为 1,则远端服务器会检查每一个报文中的时间戳(Timestamp),若 Timestamp 不是递增的关系,不会响应这个报文。配置 NAT 后,远端服务器看到来自不同的客户端的源 IP 相同,但 NAT 前每一台客户端的时间可能会有偏差,报文中的 Timestamp 就不是递增的情况。nat后的连接,开启timestamp。因为快速回收time_wait的需要,会校验时间该ip上次tcp通讯的timestamp大于本次tcp(nat后的不同机器经过nat后ip一样,保证不了timestamp递增),诊断:是否有nat和是否开启了timestamps
  • NAT 哈希表满导致 ECS 实例丢包 nf_conntrack full

iptables和tcpdump

sudo iptables -A INPUT -p tcp –destination-port 8089 -j DROP

tcpdump 是直接从网卡驱动拿包,也就是包还没进入内核tcpdump就拿到了,而iptables是工作在内核层,也就是即使被DROP还是能tcpdump到8089的packet。

参考资料:

https://mp.weixin.qq.com/s?__biz=MjM5NjQ5MTI5OA==&mid=2651747704&idx=3&sn=cd76ad912729a125fd56710cb42792ba&chksm=bd12ac358a6525235f51e3937d99ea113ed45542c51bc58bb9588fa1198f34d95b7d13ae1ae2&mpshare=1&scene=1&srcid=07047U4tN9Y3m97WQUJSLENt#rd

http://blog.hyfather.com/blog/2013/03/04/ifconfig/

网络环路

发表于 2018-12-26 | 分类于 network

网络环路

本文主要探讨网络环路的成因,危害以及预防

交换机之间多条网线导致环路

image.png

如图sw1/2/3 三个交换机形成一个环路,一个arp广播包从sw1出来到sw2,然后到sw3,再然后又从sw3回到sw1,形成一个环路,这个arp包会重复前面的传播过程进而导致这个包一直在三个交换机之间死循环,进而把三个交换机的CPU、带宽全部打满,整个网络瘫痪

对这种网络环路网络工程师们非常忌惮,因为一旦形成非常不好排查,并且整个网络瘫痪,基本上是严防死守。同时交换机也提供了各种功能(算法、策略)来自动检测网络环路并阻断网络环路。

比如上图中交换机能检测到虚线形成了环路,并自动把这个交换机口Down掉以阻止成环。

交换机对环路的阻断–STP(Spanning TreeProtocol)协议

STP协议的基本思想十分简单。大家知道,自然界中生长的树是不会出现环路的,如果网络也能够像一棵树一样生长就不会出现环路。于是,STP协议中定义了根桥(RootBridge)、根端口(RootPort)、指定端口(DesignatedPort)、路径开销(PathCost)等概念,目的就在于通过构造一棵自然树的方法达到裁剪冗余环路的目的,同时实现链路备份和路径最优化。用于构造这棵树的算法称为生成树算法SPA(Spanning TreeAlgorithm)。(摘自:http://network.51cto.com/art/201307/404013.htm)

STP是通过BPDU的网络包来在交换机之间交换信息、判断是否成环

一个STP的Case

下图是抓到的STP网络包
image.png

STP协议的后果就是带宽效率低,所以出现了PVST、PVST+、RSTP、MISTP、MSTP,这些协议可能不同厂家的交换机都不一样,互相之间也不一定兼容,所以是否生效要以实际测试为准

用tcpdump抓取stp包

$ sudo tcpdump -vvv -p -n -i eth1 stp
tcpdump: listening on eth1, link-type EN10MB (Ethernet), capture size 65535 bytes

15:44:10.772423 STP 802.1d, Config, Flags [none], bridge-id  8000.MAC.8687, length 43
message-age 0.00s, max-age 20.00s, hello-time 2.00s, forwarding-delay 15.00s
root-id 8000.MAC, root-pathcost 0
15:44:12.768245 STP 802.1d, Config, Flags [none], bridge-id 8000.MAC8.8687, length 43
message-age 0.00s, max-age 20.00s, hello-time 2.00s, forwarding-delay 15.00s
root-id 8000.MAC, root-pathcost 0
15:44:14.766513 STP 802.1d, Config, Flags [none], bridge-id 8000.MAC.8687, length 43
message-age 0.00s, max-age 20.00s, hello-time 2.00s, forwarding-delay 15.00s
root-id 8000.MAC, root-pathcost 0
15:44:16.766478 STP 802.1d, Config, Flags [none], bridge-id 8000.MAC.8687, length 43
message-age 0.00s, max-age 20.00s, hello-time 2.00s, forwarding-delay 15.00s
root-id 8000.MAC, root-pathcost 0
15:44:18.767851 STP 802.1d, Config, Flags [none], bridge-id 8000.MAC.8687, length 43
message-age 0.00s, max-age 20.00s, hello-time 2.00s, forwarding-delay 15.00s
root-id 8000.MAC, root-pathcost 0	

交换机上看到的STP

C4948-D2-08-36U#show run int g1/31
Building configuration...

Current configuration : 482 bytes
!
interface GigabitEthernet1/31
 description to D2-9-09/10U-GWR730-eth1
 switchport access vlan 270
 switchport mode access
 switchport port-security maximum 50
 switchport port-security
 switchport port-security aging time 2
 switchport port-security violation restrict
 switchport port-security aging type inactivity
 switchport port-security aging static
 storm-control broadcast level 20.00
 spanning-tree portfast
 spanning-tree bpduguard enable
 spanning-tree guard root
end

SDN或者说OVS对网络环路的影响

前面讨论的都是硬件交换机之间的网络环路以及硬件交换机对这些环路的处理,那么在SDN和OVS的场景下有没有可能成环呢? 成环后硬件交换机能不能检测到,或者软交换机自己能否检测到并阻止这些环路呢?

来看一个OVS场景下的成环Case

image.png

上图中红色虚线部分组成了一个环路,是为了组成环路而人为构造的场景,同时发现OVS只支持STP算法,打开也没有用,因为OVS和硬件交换机之间没法通过BPDU来协商判断环路(物理交换机丢掉了硬件交换机的BPDU包)。

也就是在硬件网络环境固定的情况下,我们可以在Linux环境下鼓捣出来一个网络环路,同时让Linux所在的物理二层网络瘫痪掉(好屌)

在这种网络环路下后果

  • 整个二层网络瘫痪,所有交换机CPU 100%,带宽100%
  • 连接在交换机上的所有服务器SYS CPU飙升到 30%左右(没有啥意义了,服务器没法跟外部做任何交流了)

交换机的CPU状态:

image.png

成环后抓到的arp广播风暴网络包(实际我只发了一个arp包):

image.png

其它网络环路

  • 直接把两个交换机用两根网线连接起来就是个环路
  • 拿一根网线两头连接在同一个交换机的两个网口上(短路) 2006年的一个Case: https://www.zhihu.com/question/49545070,不过现在的交换机基本上都能识别这种短路
  • 两个交换机之间做bond失败,导致环路或者三角形(三角形的话会导致多个网口对应同一个mac地址,进而导致这个mac地址网络不通,三角形不会形成网络风暴)

参考资料:

https://www.zhihu.com/question/49545070

http://network.51cto.com/art/201307/404013.htm

一个没有遵守tcp规则导致的问题

发表于 2018-11-26 | 分类于 troubleshooting

一个没有遵守tcp规则导致的问题

问题描述

应用连接数据库一段时间后,执行SQL的时候总是抛出异常,通过抓包分析发现每次发送SQL给数据的时候,数据库总是Reset这个连接

image.png

注意图中34号包,server(5029)发了一个fin包给client ,想要断开连接。client没断开,接着发了一个查询SQL给server。

进一步分析所有断开连接(发送第一个fin包)的时间点,得到如图:

image.png

基本上可以猜测,server(5029端口)在建立连接100秒终止后如果没有任何请求过来就主动发送fin包给client,要断开连接,但是这个时候client比较无耻,收到端口请求后没搭理(除非是故意的),这个时候意味着server准备好关闭了,也不会再给client发送数据了(ack除外)。

但是client虽然收到了fin断开连接的请求不但不理,过一会还不识时务发SQL查询给server,server一看不懂了(server早就申明连接关闭,没法发数据给client了),就只能回复reset,强制告诉client断开连接吧,client这时才迫于无奈断开了这次连接(图一绿框)

client的应用代码层肯定会抛出异常。

server强行断开连接

image.png

18745号包,client发了一个查询SQL给server,server先是回复ack 18941号包,然后回复fin 19604号包,强行断开连接,client端只能抛异常了

疑难问题汇总

发表于 2018-10-24 | 分类于 Linux

疑难问题汇总

一网通客户 vxlan 网络始终不通,宿主机能抓到发出去的包,但是抓不到回复包。对端容器所在的宿主机抓不到进来的包

一定是网络上把这个包扔掉了

证明问题

  • 先选择两台宿主机,停掉上面的 ovs 容器(腾出4789端口)
  • 一台宿主机上执行: nc -l -u 4789 //在4789端口上启动udp服务
  • 另外一台主机上执行: nc -u 第一台宿主机的IP 4789 //从第二台宿主机连第一台的4789端口 nc -u -z -v ip 4789
  • 从两边都发送一些内容看看,看是否能到达对方

如果通过nc发送的东西也无法到达对方(跟方舟没有关系了)那么就是链路上的问题


一网通客户 vxlan 网络能通,但是pca容器初始化的时候失败

通过报错信息发现pca容器访问数据库SocketTimeout,同时看到异常信息都是Timeout大于15分钟以上了。

需找问题

  • 先在 pca容器和数据库容器互相 ping 证明网络没有问题,能够互通
  • 在 pca 容器中通过mysql 命令行连上 mysql,并创建table,insert一些记录,结果也没有问题
  • 抓包发现pca容器访问数据库的时候在重传包(以往经验)

screenshot

细化证明问题

  • ping -s -M 尝试发送1460大小的包
  • 检查宿主机、容器MTU设置

确认问题在宿主机网卡MTU设置为1350,从而导致容器发出的包被宿主机网卡丢掉

新零售客户通过vpn部署好中间件后,修改笔记本的dns设置后通过浏览器来访问中间件的console,但是报找不到server。同时在cmd中ping 这个域名能通,但是nslookup解析不了这个域名

ping 这个域名能通,但是nslookup不行,基本可以确认网络没有大问题,之所以ping可以nslookup不行,是因为他们底层取dns server的逻辑不一样。

先检查dns设置:

image.png
如上图,配置的填写

image.png

多出来一个127.0.0.1肯定有问题,明明配置的时候只填了114.114.114.114. nslookup、浏览器默认把域名解析丢给了127.0.0.1,但是 ping丢给了114.114.114.114,所以看到如上描述的结果。

经过思考发现应该是本机同时运行了easyconnect(vpn软件),127.0.0.1 是他强行塞进来的。马上停掉easyconnect再ipconfig /all 验证一下这个时候的dns server,果然127.0.0.1不见了, nslookup 也正常了。

某航空客户 windows下通过方舟dns域名解析不了方舟域名,但是宿主机上可以。windows机器能ping通dns server ip, 但是nslookup 解析不了域名,显示request time out

image.png

能ping通说明网络能通,但是dns域名要能解析依赖于:

  • 网络能通
  • dns server上有dns服务(53udp端口)
  • 中间的防火墙对这个udp53端口限制了

如上图,这里的问题非常明显是中间的防火墙没放行 udp 53端口

方舟环境在ECS底座上DNS会继承rotate模式,导致域名解析不正常,ping 域名不通,但是nslookup能通

nslookup 域名结果正确,但是 ping 域名失败

某银行 POC 环境物理机搬迁到新机房后网络不通,通过在物理机上抓包,抓不到任何容器的包

image.png

如图所示容器中发了 arp包(IP 10.100.2.2 寻找10.100.2.1 的mac地址),这个包从bond0 网卡发出去了,也是带的正确的 vlanid 1011,但是交换机没有回复,那么就一定是交换机上vlan配置不对,需要找分配这个vlan的网工来检查交换机的配置

能抓到进出的容器包–外部环境正确,方舟底座的问题

不能抓到出去的容器包–方舟底座的问题

能抓到出去的容器包,抓不到回来的包–外部环境的问题

所以这里是方舟底座的问题。检查ovs、vlan插件一切都正常,见鬼了

检查宿主机网卡状态,发现没插网线,如果容器所用的宿主机网卡没有插网线,那么ovs就不会转发任何包到宿主机网卡。

一台应用服务器无法访问部分drds-server

应用机器: 10.100.10.201 这台机器抛502异常比较多,进一步诊断发现 ping youku.tddl.tbsite.net 的时候解析到 10.100.53.15/16就不通

直接ping 10.100.53.15/16 也不通,经过诊断发现是交换机上记录了两个 10.100.10.201的mac地址导致网络不通。

youku-mac-ip.gif

上图是不通的IP,下图是正常IP

经过调查发现是土豆业务也用了10.100.10.201这个IP导致交换机的ARP mac table冲突,土豆删除这个IP后故障就恢复了。

当时交换机上发现的两条记录:

00:18:51:38:b1:cd 10.100.10.201 
8c:dc:d4:b3:af:14 10.100.10.201

某个客户默认修改了umask导致黑屏脚本权限不够,部署中途不断卡壳,直接在黑屏脚本中修复了admin这个用户的umask

  1. 客户环境的 umask 是 0027 会导致所有copy文件的权限都不对了
  2. 因为admin没权限执行 /bin/jq 导致daemon.json是空的
  3. /etc/docker/daemon.json 文件是空的,docker启动报错

修复centos下udp和批量处理脚本因为环境变量的确实不能执行modprobe和ping等等命令的问题,同时将alios的这块修复逻辑放到了方舟安装脚本中,init的时候会先把这个问题修复

Linux环境变量问题汇总

Centos系统重启后 /etc/resolv.conf总是被还原,开始以为是系统Bug,研究后发现是可以配置的,dhcp默认会每次重启后拉取DNS自动更新 /etc/resolv.conf

MonkeyKing burn cpu: mkt-burncpu.sh 脚本在方舟服务器上运行一段时间后,进程不见了,MK团队认为是方舟杀掉了他们。

好奇心迫使我去看代码、看openssl测试输出日志(MonkeyKing burn cpu内部调用 openssl speed 测试cpu的速度),这个测试一轮跑完了opessl就结束了,本身就不是死循环一直跑, 不是方舟杀掉的。

另外说明这个问题一直存在开发、测试MonkeyKing功能的团队就没有发现,或者之前一直只需要跑不到10分钟就自己主动把它杀掉让出CPU。

image.png

某汽车客户 部署过程中愚公不能正常启动,怀疑是依赖的zk问题,zk网络访问正常

尝试telnet zk发现不通,客户现场安装了kerberos导致telnet测试有问题(telnet被kerberos替换过),换一个其他环境的telnet 二进制文件就可以了(md5sum、telnet –help)

开发反应两个容器之间的网络不稳定,偶尔报连不上某些容器

主要是出现在tlog-console访问hbase容器的时候报连接异常

  1. 在 task_1114_g0_tlog-console_tlog_1(10.16.11.131) 的56789 端口上启动了一个简单的http服务,然后从 task_1114_g0_tlog-hbase_tlog(10.16.11.108) 每秒钟去访问一下10.16.11.131:56789 , 如果丢包率很高的时候服务 10.16.11.131:56789 也很慢或者访问不到就是网络问题,否则就有可能是hbase服务不响应导致的丢包、网络不通(仅仅是影响hbase服务)
  2. 反过来在hbase上同样启动http服务,tlog-console不停地去get
  3. 整个过程我的http服务响应非常迅速稳定,从没出现过异常
  4. 在重现问题侯,贺飞发现 是tlog线程数目先增多,retran才逐渐增高的, retran升高,并没有影响在那台机器上ping 或者telnet hbase的服务
  5. 通过以上方式证明跟容器、网络无关,是应用本身的问题,交由产品开发继续解决

最终开发确认网络没有问题后一门心思闷头自查得出结论:

信息更新:

问题:
tlog-console进程线程数多,卡在连接hbase上的问题

直接原因:

  1. tlog-console有巡检程序,每m分钟会检查运行超过n秒的线程,并且中断这个线程; 这个操作直接导致hbase客户端在等待hbaseserver返回数据的时候被中断,这种中断会经常发生,累积久了,就会打爆tlog-console服务的线程数目,这时候,tlogconsole机器的retran就会变多,连接hbaseserver就会出问题, 具体的机理不明

解决问题的有效操作:

  1. 停止对tlog-console的巡检程序后,问题没有发生过

其他潜在问题,这些问题是检查问题的时候,发现的其他潜在问题,已经反馈给tlog团队:

  1. Htable实例不是线程安全,有逻辑多线程使用相同的htable实例
  2. 程序中有new HTable 不close的路径

某化工私有云DRDS扩容总是报资源不足,主要是因为有些drds-server容器指定了–cpu-shares=128(相当于4Core–1024/物理核数 等于每个核对应的cpu-shares ), 导致物理机CPU不够。现场将所有容器的–cpu-shares改成2后修复这个问题,但是最终需要产品方

主要是swarm对cpu-shares的判断上有错误,swarm默认认定每台机器的总cpu-shares是1024,也就是 1024/物理核数 等于每个核对应的cpu-shares

如果需要精细化CPU控制,cpu-shares比cpu-set之类的要精确,利用率更高。但是也更容易出现问题

mq-diamond的异常日志总是打爆磁盘。mq-diamond 容器一天输出500G日志的问题,本质是调用的依赖不可用了,导致mq-diamond 频繁输出日志,两天就用掉了1T磁盘.

这里有两个问题需要处理:

  1. mq-diamond 依赖的服务可用;
  2. mq-diamond 自身保护,不要被自己的日志把磁盘撑爆了

对于问题二修改log4j来保护;对于问题1查看异常内容,mq-diamond尝试连接server:ip1,ip2,ip3 正常这里应该是一个ip而不是三个ip放一起。判断是mq-diamond从mq-cai获取diamond iplist有问题,这个iplist应该放在三行,但是实际被放到了1行,用逗号隔开

手工修改这个文件,放到三行,问题没完,还是异常,我自己崩溃没管。最后听mq-diamond的开发讲他们取iplist的url比较特殊,是自己定义的,所以我修改的地方不起作用。反思,为什么修改不起作用的时候不去看看Nginx的access日志? 这样可以证明我修改的文件实际没有被使用,同时还能找到真正被使用的配置文件

内核migration进程bug导致宿主机Load非常高,同时CPU idle也很高(两者矛盾)

内核migration进程bug导致对应的CPU核卡死(图一),这个核上的所有进程得不到执行(Load高,CPU没有任何消耗, 图二),直到内核进程 watchdog 发现这个问题并恢复它。

出现这个bug后的症状,通过top命令看到CPU没有任何消耗但是Load偏高,如果应用进程恰好被调度到这个出问题的CPU核上,那么这个进程会卡住(大概20秒)没有任何响应,比如 ping 进程(图三图四),watchdog恢复这个问题后,多个网络包在同一时间全部通。其实所影响的不仅仅是网络卡顿,中间件容器里面的服务如果调度到这个CPU核上同样得不到执行,从外面就是感觉容器不响应了

image.png

image.png

image.png

image.png

拿如上证据求助内核开发

关键信息在这里:
代码第297行
2017-09-15T06:52:37.820783+00:00 ascliveedas4.sgdc kernel: [598346.499872] WARNING: at net/sched/sch_generic.c:297 dev_watchdog+0x270/0x280()
2017-09-15T06:52:37.820784+00:00 ascliveedas4.sgdc kernel: [598346.499873] NETDEV WATCHDOG: ens2f0 (ixgbe): transmit queue 28 timed out

kernel version: kernel-3.10.0-327.22.2.el7.src.rpm

265 static void dev_watchdog(unsigned long arg)
266 {
267 struct net_device *dev = (struct net_device *)arg;
268
269 netif_tx_lock(dev);
270 if (!qdisc_tx_is_noop(dev)) {
271 if (netif_device_present(dev) &&
272 netif_running(dev) &&
273 netif_carrier_ok(dev)) {
274 int some_queue_timedout = 0;
275 unsigned int i;
276 unsigned long trans_start;
277
278 for (i = 0; i < dev->num_tx_queues; i++) {
279 struct netdev_queue *txq;
280
281 txq = netdev_get_tx_queue(dev, i);
282 /*
283  * old device drivers set dev->trans_start
284  */
285 trans_start = txq->trans_start ? : dev->trans_start;
286 if (netif_xmit_stopped(txq) &&
287 time_after(jiffies, (trans_start +
288  dev->watchdog_timeo))) {
289 some_queue_timedout = 1;
290 txq->trans_timeout++;
291 break;
292 }
293 }
294
295 if (some_queue_timedout) {
296 WARN_ONCE(1, KERN_INFO "NETDEV WATCHDOG: %s (%s): transmit queue %u timed out\n",
297dev->name, netdev_drivername(dev), i);
298 dev->netdev_ops->ndo_tx_timeout(dev);
299 }
300 if (!mod_timer(&dev->watchdog_timer,
301round_jiffies(jiffies +
302  dev->watchdog_timeo)))
303 dev_hold(dev);
304 }



$ cat  kernel_log.0915
2017-09-15T02:19:55.975310+00:00 ascliveedas4.sgdc kernel: [582026.288227] openvswitch: netlink: Key type 62 is out of range max 22
2017-09-15T03:49:41.312168+00:00 ascliveedas4.sgdc kernel: [587409.546584] md: md0: data-check interrupted.
2017-09-15T06:52:37.820782+00:00 ascliveedas4.sgdc kernel: [598346.499865] ------------[ cut here ]------------
2017-09-15T06:52:37.820783+00:00 ascliveedas4.sgdc kernel: [598346.499872] WARNING: at net/sched/sch_generic.c:297 dev_watchdog+0x270/0x280()
2017-09-15T06:52:37.820784+00:00 ascliveedas4.sgdc kernel: [598346.499873] NETDEV WATCHDOG: ens2f0 (ixgbe): transmit queue 28 timed out
2017-09-15T06:52:37.820784+00:00 ascliveedas4.sgdc kernel: [598346.499916] Modules linked in: 8021q garp mrp xt_nat veth xt_addrtype ipt_MASQUERADE nf_nat_masquerade_ipv4 iptable_nat nf_conntrack_ipv4 nf_defrag_ipv4 nf_nat_ipv4 iptable_filter xt_conntrack nf_nat nf_conntrack bridge stp llc tcp_diag udp_diag inet_diag binfmt_misc overlay() vfat fat intel_powerclamp coretemp intel_rapl kvm_intel kvm crc32_pclmul ghash_clmulni_intel aesni_intel lrw gf128mul glue_helper ablk_helper cryptd raid10 ipmi_devintf iTCO_wdt iTCO_vendor_support sb_edac lpc_ich hpwdt edac_core hpilo i2c_i801 ipmi_si sg mfd_core pcspkr ioatdma ipmi_msghandler acpi_power_meter shpchp wmi pcc_cpufreq openvswitch libcrc32c nfsd auth_rpcgss nfs_acl lockd grace sunrpc ip_tables ext4 mbcache jbd2 sd_mod crc_t10dif crct10dif_generic mgag200 syscopyarea sysfillrect sysimgblt drm_kms_helper ixgbe crct10dif_pclmul ahci ttm crct10dif_common igb crc32c_intel mdio libahci ptp drm pps_core i2c_algo_bit libata i2c_core dca dm_mirror dm_region_hash dm_log dm_mod
2017-09-15T06:52:37.820786+00:00 ascliveedas4.sgdc kernel: [598346.499928] CPU: 10 PID: 123 Comm: migration/10 Tainted: G L ------------ T 3.10.0-327.22.2.el7.x86_64#1
2017-09-15T06:52:37.820787+00:00 ascliveedas4.sgdc kernel: [598346.499929] Hardware name: HP ProLiant DL160 Gen9/ProLiant DL160 Gen9, BIOS U20 12/27/2015
2017-09-15T06:52:37.820788+00:00 ascliveedas4.sgdc kernel: [598346.499935]  ffff88207fc43d88 000000001cdfb0f1 ffff88207fc43d40 ffffffff816360fc
2017-09-15T06:52:37.820789+00:00 ascliveedas4.sgdc kernel: [598346.499939]  ffff88207fc43d78 ffffffff8107b200 000000000000001c ffff881024660000
2017-09-15T06:52:37.820790+00:00 ascliveedas4.sgdc kernel: [598346.499942]  ffff881024654f40 0000000000000040 000000000000000a ffff88207fc43de0
2017-09-15T06:52:37.820791+00:00 ascliveedas4.sgdc kernel: [598346.499943] Call Trace:
2017-09-15T06:52:37.820792+00:00 ascliveedas4.sgdc kernel: [598346.499952]  <IRQ>  [<ffffffff816360fc>] dump_stack+0x19/0x1b
2017-09-15T06:52:37.820794+00:00 ascliveedas4.sgdc kernel: [598346.499956]  [<ffffffff8107b200>] warn_slowpath_common+0x70/0xb0
2017-09-15T06:52:37.820795+00:00 ascliveedas4.sgdc kernel: [598346.499959]  [<ffffffff8107b29c>] warn_slowpath_fmt+0x5c/0x80
2017-09-15T06:52:37.820795+00:00 ascliveedas4.sgdc kernel: [598346.499964]  [<ffffffff8154d4f0>] dev_watchdog+0x270/0x280
2017-09-15T06:52:37.820796+00:00 ascliveedas4.sgdc kernel: [598346.499966]  [<ffffffff8154d280>] ? dev_graft_qdisc+0x80/0x80
2017-09-15T06:52:37.820797+00:00 ascliveedas4.sgdc kernel: [598346.499972]  [<ffffffff8108b0a6>] call_timer_fn+0x36/0x110
2017-09-15T06:52:37.820798+00:00 ascliveedas4.sgdc kernel: [598346.499974]  [<ffffffff8154d280>] ? dev_graft_qdisc+0x80/0x80
2017-09-15T06:52:37.820799+00:00 ascliveedas4.sgdc kernel: [598346.499977]  [<ffffffff8108dd97>] run_timer_softirq+0x237/0x340
2017-09-15T06:52:37.820800+00:00 ascliveedas4.sgdc kernel: [598346.499980]  [<ffffffff81084b0f>] __do_softirq+0xef/0x280
2017-09-15T06:52:37.820801+00:00 ascliveedas4.sgdc kernel: [598346.499985]  [<ffffffff81103360>] ? cpu_stop_should_run+0x50/0x50
2017-09-15T06:52:37.820801+00:00 ascliveedas4.sgdc kernel: [598346.499988]  [<ffffffff8164819c>] call_softirq+0x1c/0x30
2017-09-15T06:52:37.820802+00:00 ascliveedas4.sgdc kernel: [598346.499994]  [<ffffffff81016fc5>] do_softirq+0x65/0xa0
2017-09-15T06:52:37.820803+00:00 ascliveedas4.sgdc kernel: [598346.499996]  [<ffffffff81084ea5>] irq_exit+0x115/0x120
2017-09-15T06:52:37.820804+00:00 ascliveedas4.sgdc kernel: [598346.499999]  [<ffffffff81648e15>] smp_apic_timer_interrupt+0x45/0x60
2017-09-15T06:52:37.820805+00:00 ascliveedas4.sgdc kernel: [598346.500003]  [<ffffffff816474dd>] apic_timer_interrupt+0x6d/0x80
2017-09-15T06:52:37.820813+00:00 ascliveedas4.sgdc kernel: [598346.500007]  <EOI>  [<ffffffff811033df>] ? multi_cpu_stop+0x7f/0xf0
2017-09-15T06:52:37.820815+00:00 ascliveedas4.sgdc kernel: [598346.500010]  [<ffffffff81103666>] cpu_stopper_thread+0x96/0x170

某银行客户RAID阵列坏掉,导致物理机重启后容器的net-alias域名解析不到

docker daemon 的endpoint用的容器名存在zk中,如果创建一个重复名字的容器,那么会失败,然后回滚,回滚动作会把zk中别人的endpoint删掉,从而导致域名不通。

物理机异常后,我们的调度程序会在其它物理机重新调度生成这个容器,但是当原来的物理机回来后,这里有两个一样的容器会自动删掉宕机的物理机上的这个容器,从而误删net-alias,进而域名无法解析

某快递客户PHP短连接访问DRDS会导致极低概率出现连接被reset、货运快递客户事务没生效导致数据库写入的金额对不上账

参考文章

https://mp.weixin.qq.com/s?__biz=MzU5Mzc0NDUyNg==&mid=2247483793&idx=1&sn=c7b4ec96d186dd74689482077522337f&scene=21#wechat_redirect

Load很高,CPU使用率很低

发表于 2018-09-26 | 分类于 troubleshooting

Load很高,CPU使用率很低

第一次碰到这种Case:物理机的Load很高,CPU使用率很低

先看CPU、Load情况

如图一:
vmstat显示很有多任务等待排队执行(r)top都能看到Load很高,但是CPU idle 95%以上
image.png
image.png

这个现象不太合乎常规,也许是在等磁盘IO、也许在等网络返回会导致CPU利用率很低而Load很高

贴个vmstat 说明文档(图片来源于网络N年了,找不到出处)
image.png

检查磁盘状态,很正常(vmstat 第二列也一直为0)

image.png

再看Load是在5号下午15:50突然飙起来的:

image.png

同一时间段的网络流量、TCP连接相关数据很平稳:

image.png

所以分析到此,可以得出:Load高跟磁盘、网络、压力都没啥关系

物理机上是跑的Docker,分析了一下CPUSet情况:

image.png

发现基本上所有容器都绑定在CPU1上(感谢 @辺客 发现这个问题)

进而检查top每个核的状态,果然CPU1 的idle一直为0

image.png

看到这里大致明白了,虽然CPU整体很闲但是因为很多进程都绑定在CPU1上,导致CPU1上排队很长,看前面tsar的–load负载截图的 等待运行进程排队长度(runq)确实也很长。

物理机有32个核,如果100个任务同时进来,Load大概是3,这是正常的。如果这100个任务都跑在CPU1上,Load还是3(因为Load是所有核的平均值)。但是如果有源源不断的100个任务进来,前面100个还没完后面又来了100个,这个时候CPU1前面队列很长,其它31个核没事做,这个时候整体Load就是6了,时间一长很快Load就能到几百。

这是典型的瓶颈导致积压进而高Load。

为什么会出现这种情况

检查Docker系统日志,发现同一时间点所有物理机同时批量执行docker update 把几百个容器都绑定到CPU1上,导致这个核忙死了,其它核闲得要死(所以看到整体CPU不忙,最忙的那个核被平均掩盖掉了),但是Load高(CPU1上排队太长,即使平均到32个核,这个队列还是长,这就是瓶颈啊)。

如下Docker日志,Load飙升的那个时间点有人批量调docker update 把所有容器都绑定到CPU1上:
image.png

检查Docker集群Swarm的日志,发现Swarm没有发起这样的update操作,似乎是每个Docker Daemon自己的行为,谁触发了这个CPU的绑定过程的原因还没找到,求指点。

手动执行docker update, 把容器打散到不同的cpu核上,恢复正常:

image.png

关于这个Case的总结

  • 技术拓展商业边界,同样技能、熟练能力能拓展解决问题的能力。 开始我注意到了Swarm集群显示的CPU绑定过多,同时也发现有些容器绑定在CPU1上。所以我尝试通过API: GET /containers/json 拿到了所有容器的参数,然后搜索里面的CPUSet,结果这个API返回来的参数不包含CPUSet,那我只能挨个 GET /containers/id/json, 要写个循环,偷懒没写,所以没发现这个问题。
  • 这种多个进程绑定到同一个核然后导致Load过高的情况确实很少见,也算是个教训
  • 自己观察top 单核的时候不够仔细,只是看到CPU1 的US 60%,没留意idle,同时以为这个60%就是偶尔一个进程在跑,耐心不够(主要也是没意识到这种极端情况,疏忽了)

关于Load高的总结

  • Load高一般对应着CPU高,就是CPU负载过大,检查CPU具体执行任务是否合理
  • 如果Load高,CPU使用率不高的检查一下IO、网络等是否比较慢
  • 如果是虚拟机,检查是否物理机超卖或者物理机其它ECS抢占CPU、IO导致的
  • 如果两台一样的机器一样的流量,Load有一台偏高的话检查硬件信息,比如CPU被降频了,QPI,内存效率等等(https://www.atatech.org/articles/12201),这个时候可能需要硬件相关同学加入一起排查了,当然牛逼的工程师能把这块也Cover了排查效率自然更高
  • load计算是看TASK_RUNNING(R)或者TASK_UNINTERRUPTIBLE(D–不可中断的睡眠进程)的数量,R肯定会占用CPU,但是D一般就不占用CPU了

Linux 下load 高主要是因为R/D 两个状态的线程多了,排查套路:

img

参考文章

浅谈 Linux 高负载的系统化分析

部分机器网络不通

发表于 2018-08-26 | 分类于 troubleshooting

部分机器网络不通

问题

应用机器: 10.100.10.201 这台机器抛502异常比较多,进一步诊断发现 ping youku.tddl.tbsite.net 的时候解析到 10.100.53.15/16就不通

直接ping 10.100.53.15/16 也不通,经过诊断发现是交换机上记录了两个 10.100.10.201的mac地址导致网络不通。

youku-mac-ip.gif

上图是不通的IP,下图是正常IP

经过调查发现是土豆业务也用了10.100.10.201这个IP导致交换机的ARP mac table冲突,土豆删除这个IP后故障就恢复了。

当时交换机上发现的两条记录:

00:18:51:38:b1:cd 10.100.10.201 
8c:dc:d4:b3:af:14 10.100.10.201

关于TCP连接的Keepalive和reset

发表于 2018-08-26 | 分类于 TCP

关于TCP连接的Keepalive和reset

先来看一个现象,下面是测试代码:

Server: socat -dd tcp-listen:2000,keepalive,keepidle=10,keepcnt=2,reuseaddr,keepintvl=1 -
Client: socat -dd - tcp:localhost:2000,keepalive,keepidle=10,keepcnt=2,keepintvl=1

Drop Connection (Unplug Cable, Shut down Link(WiFi/Interface)): sudo iptables -A INPUT -p tcp --dport 2000 -j DROP

server监听在2000端口,支持keepalive, client连接上server后每隔10秒发送一个keepalive包,一旦keepalive包得不对对方的响应,每隔1秒继续发送keepalive, 重试两次,如果一直得不到对方的响应那么这个时候client主动发送一个reset包,那么在client这边这个socket就断开了。server上会一直傻傻的等,直到真正要发送数据了才抛异常。

image.png

假如client连接层是一个Java应用的连接池,那么这个socket断开后Java能感知吗?

https://stackoverflow.com/questions/10240694/java-socket-api-how-to-tell-if-a-connection-has-been-closed

Java对Socket的控制比较弱,比如只能指定是否keepalive,不能用特定的keepalive参数(intvl/cnt等),除非走JNI,不推荐。

如下图(dup ack其实都是keepalive包,这是因为没有抓到握手包导致wireshark识别错误而已)
image.png

如上图,client 21512在多次keepalive server都不响应后,发送了reset断开这个连接(server没收到),server以为还连着,这个时候当server正常发数据给client,如果防火墙还在就丢掉,server不停地重传,如果防火墙不在,那么对方os收到这个包后知道21512这个端口对应的连接已经关闭了,再次发送reset给server,这时候server抛异常,中断这个连接。

image.png

os层面目前看起来除了用socket去读数据感知到内核已经reset了连接外也没什么好办法检测到。

如何徒手撕Bug

发表于 2018-08-25 | 分类于 troubleshooting

如何徒手撕Bug

经常碰到bug,如果有源代码,或者源代码比较简单一般通过bug现象结合读源代码,基本能比较快解决掉。但是有些时候源代码过于复杂,比如linux kernel,比如 docker,复杂的另一方面是没法比较清晰地去理清源代码的结构。

所以不到万不得已不要碰复杂的源代码

问题

docker daemon重启,上面有几十个容器,重启后daemon基本上卡死不动了。 docker ps/exec 都没有任何响应,同时能看到很多这样的进程:

image.png

这个进程是docker daemon在启动的时候去设置每个容器的iptables,来实现dns解析。

这个时候执行 sudo iptables -L 也告诉你有其他应用锁死iptables了:
image.png

$sudo fuser /run/xtables.lock 
/run/xtables.lock:1203  5544 10161 14451 14482 14503 14511 14530 14576 14602 14617 14637 14659 14664 14680 14698 14706 14752 14757 14777 14807 14815 14826 14834 14858 14872 14889 14915 14972 14973 14979 14991 15006 15031 15067 15076 15104 15127 15155 15176 15178 15179 15180 16506 17656 17657 17660 21904 21910 24174 28424 29741 29839 29847 30018 32418 32424 32743 33056 33335 59949 64006

通过上面的命令基本可以看到哪些进程在等iptables这个锁,之所以有这么多进程在等这个锁,应该是拿到锁的进程执行比较慢所以导致后面的进程拿不到锁,卡在这里

跟踪具体拿到锁的进程

$sudo lsof  /run/xtables.lock | grep 3rW
iptables 36057 root3rW  REG   0,190 48341 /run/xtables.lock

通过strace这个拿到锁的进程可以看到:

image.png

发现在这个配置容器dns的进程同时还在执行一些dns查询任务(容器发起了dns查询),但是这个时候dns还没配置好,所以这个查询会超时

看看物理机上的dns服务器配置:

$cat /etc/resolv.conf   
options timeout:2 attempts:2   
nameserver 10.0.0.1  
nameserver 10.0.0.2
nameserver 10.0.0.3

尝试将 timeout 改到20秒、1秒分别验证一下,发现如果timeout改到20秒strace这里也会卡20秒,如果是1秒(这个时候attempts改成1,后面两个dns去掉),那么整体没有感知到任何卡顿,就是所有iptables修改的进程都很快执行完毕了

strace某个等锁的进程,拿到锁后非常快

image.png

拿到锁后如果这个时候没有收到 dns 查询,那么很快iptables修改完毕,也不会导致卡住

strace工作原理

strace -T -tt -ff -p pid -o strace.out

注意:对于多进线程序需要加-f 参数,这样会trace 进程下的所有线程,-t 表示打印时间精度默认为秒,-tt -ttt 分别表示ms us 的时间精度。

image.png

我们从图中可以看到,对于正在运行的进程而言,strace 可以 attach 到目标进程上,这是通过 ptrace 这个系统调用实现的(gdb 工具也是如此)。ptrace 的 PTRACE_SYSCALL 会去追踪目标进程的系统调用;目标进程被追踪后,每次进入 syscall,都会产生 SIGTRAP 信号并暂停执行;追踪者通过目标进程触发的 SIGTRAP 信号,就可以知道目标进程进入了系统调用,然后追踪者会去处理该系统调用,我们用 strace 命令观察到的信息输出就是该处理的结果;追踪者处理完该系统调用后,就会恢复目标进程的执行。被恢复的目标进程会一直执行下去,直到下一个系统调用。

你可以发现,目标进程每执行一次系统调用都会被打断,等 strace 处理完后,目标进程才能继续执行,这就会给目标进程带来比较明显的延迟。因此,在生产环境中我不建议使用该命令,如果你要使用该命令来追踪生产环境的问题,那就一定要做好预案。

假设我们使用 strace 跟踪到,线程延迟抖动是由某一个系统调用耗时长导致的,那么接下来我们该怎么继续追踪呢?这就到了应用开发者和运维人员需要拓展分析边界的时刻了,对内核开发者来说,这才算是分析问题的开始。

两个术语:

  1. tracer:跟踪(其他程序的)程序
  2. tracee:被跟踪程序

tracer 跟踪 tracee 的过程:

首先,attach 到 tracee 进程:调用 ptrace,带 PTRACE_ATTACH 及 tracee 进程 ID 作为参数。

之后当 tracee 运行到系统调用函数时就会被内核暂停;对 tracer 来说,就像 tracee 收到了 SIGTRAP 信号而停下来一样。接下来 tracer 就可以查看这次系统调 用的参数,打印相关的信息。

然后,恢复 tracee 执行:再次调用 ptrace,带 PTRACE_SYSCALL 和 tracee 进程 ID。 tracee 会继续运行,进入到系统调用;在退出系统调用之前,再次被内核暂停。

以上“暂停-采集-恢复执行”过程不断重复,tracer 就可以获取每次系统调用的信息,打印 出参数、返回值、时间等等。

strace 常用用法

  1. sudo strace -tt -e poll,select,connect,recvfrom,sendto nc www.baidu.com 80 //网络连接不上,卡在哪里

  2. 如何确认一个程序为什么卡住和停止在什么地方?

有些时候,某个进程看似不在做什么事情,也许它被停止在某个地方。

$ strace -p 22067 Process 22067 attached - interrupt to quit flock(3, LOCK_EX

这里我们看到,该进程在处理一个独占锁(LOCK_EX),且它的文件描述符为3,so 这是一个什么文件呢?

$ readlink /proc/22067/fd/3 /tmp/foobar.lock

aha, 原来是 /tmp/foobar.lock。可是为什么程序会被停止在这里呢?

$ lsof | grep /tmp/foobar.lock command 21856 price 3uW REG 253,88 0 34443743 /tmp/foobar.lock command 22067 price 3u REG 253,88 0 34443743 /tmp/foobar.lock

原来是进程 21856 hold住了锁。此时,真相大白 21856 和 22067 读到了相同的锁。

strace -cp // strace 可以按操作汇总时间

我的分析

docker启动的时候要修改每个容器的dns(iptables规则),如果这个时候又收到了dns查询,但是查询的时候dns还没配置好,所以只能等待dns默认超时,等到超时完了再往后执行修改dns动作然后释放iptables锁。这里会发生恶性循环,导致dns修改时占用iptables的时间非常长,进而看着像把物理机iptables锁死,同时docker daemon不响应任何请求。

这应该是docker daemon实现上的小bug,也就是改iptables这里没加锁,如果修改dns的时候同时收到了dns查询,要是让查询等锁的话就不至于出现这种恶性循环

总结

其实这个问题还是挺容易出现的,daemon重启,上面有很多容器,容器里面的任务启动的时候都要做dns解析,这个时候daemon还在修改dns,冲进来很多dns查询的话会导致修改进程变慢

这也跟物理机的 /etc/resolv.conf 配置有关

暂时先只留一个dns server,同时把timeout改成1秒(似乎没法改成比1秒更小),同时 attempts:1 ,也就是加快dns查询的失败,当然这会导致应用启动的时候dns解析失败,最终还是需要从docker的源代码修复这个问题。

解决过程中无数次想放弃,但是反复在那里strace,正是看到了有dns和没有dns查询的两个strace才想清楚这个问题,感谢自己的坚持和很多同事的帮助,手撕的过程中必然有很多不理解的东西,需要请教同事

参考资料

strace 是如何工作的(2016)

部分机器网络不通

发表于 2018-08-25 | 分类于 DNS

方舟域名和服务

服务发布

  • 通过Docker方式指定需要发布的服务名称和对应端口

~:docker run -d -it –name HTTP_Provider –net=vlan701 -l alimw.domain=chengji.test.com -l alimw.port=8090 reg.docker.alibaba-inc.com/middleware.udp

说明:这里docker容器的名称是HTTP_Provider ,通过alimw.domain=chengji.test.com -l alimw.port=8090 指定了服务名为:chengji.test.com,端口:8090

  • 启动后,进入VIPServer的OPS平台查询域名:chengji.test.com,可以看到注册的服务IP和端口,以及健康状态。
    说明:由于只是通过Docker方式注册了服务,但是内部服务并没有启动,可以看到健康程度标注为差,健康检查为false。
  • 部署相关的HTTP服务,再次进入VIPServer的OPS平台查询域名:chengji.test.com,将可以看到健康检查状态正常。

服务发现

1.VIPServer-Client方式

任意启动一个Docker环境,部署好HTTP服务的消费者,采用标准的VS的Client订阅方式即可

2.DNS-F方式(跨语言)

需要提前部署好DNS-F客户端,需要保证DNS-F服务高可用,可直接通过curl方式进行测试

3.方舟提供DNS Server,负责这些域名的解析

性能优化,从老中医到科学理论指导

发表于 2018-08-24 | 分类于 performance

性能优化,从老中医到科学理论指导

简单原理:

  • 追着RT去优化,哪个环节、节点RT高,哪里就值得优化,CPU、GC等等只是导致RT高的因素,RT才是结果;

  • QPS=并发/RT

利特尔法则[[编辑](https://zh.wikipedia.org/w/index.php?title=利特爾法則&action=edit&section=0&summary=/* top */ )]

利特尔法则(英语:Little’s law),基于等候理论,由约翰·利特尔在1954年提出。利特尔法则可用于一个稳定的、非占先式的系统中。其内容为:

在一个稳定的系统中,长期的平均顾客人数(L),等于长期的有效抵达率(λ),乘以顾客在这个系统中平均的等待时间(W)

或者,我们可以用一个代数式来表达:

L=λW

利特尔法则可用来确定在途存货的数量。此法则认为,系统中的平均存货等于存货单位离开系统的比率(亦即平均需求率)与存货单位在系统中平均时间的乘积。

虽然此公式看起来直觉性的合理,它依然是个非常杰出的推导结果,因为此一关系式“不受到货流程分配、服务分配、服务顺序,或任何其他因素影响”。

此一理论适用于所有系统,而且它甚至更适合用于系统中的系统。举例来说,在一间银行里,顾客等待的队伍就是一个子系统,而每一位柜员也可以被视为一个等待的子系统,而利特尔法则可以套用到任何一个子系统,也可以套用到整个银行的等待队伍之母系统。

唯一的条件就是,这个系统必须是长期稳定的,而且不能有插队抢先的情况发生,这样才能排除换场状况的可能性,例如开业或是关厂。

案例:

需要的线程数 = qps * latency(单位秒)。 依据是little’s law,类似的应用是tcp中的bandwidth-delay product。如果这个数目远大于核心数量,应该考虑用异步接口。
举例:

  • qps = 2000,latency = 10ms,计算结果 = 2000 * 0.01s = 20。和常见核数在同一个数量级,用同步。
  • qps = 100, latency = 5s, 计算结果 = 100 * 5s = 500。和常见核数不在同一个数量级,用异步。
  • qps = 500, latency = 100ms,计算结果 = 500 * 0.1s = 50。和常见核数在同一个数量级,可用同步。如果未来延时继续增长,考虑异步。

image-20211103175727900

RT

什么是 RT ?是概念还是名词还是理论?

RT其实也没那么玄乎,就是 Response Time,只不过看你目前在什么场景下,也许你是c端(app、pc等)的用户,响应时间是你请求服务器到服务器响应你的时间间隔,对于我们后端优化来说,就是接受到请求到响应用户的时间间隔。这听起来怎么感觉这不是在说废话吗?这说的不都是服务端的处理时间吗?不同在哪里?其实这里有个容易被忽略的因素,叫做网络开销。
所以客户端RT ≈ 网络开销 + 服务端RT。也就是说,一个差的网络环境会导致两个RT差距的悬殊(比如,从深圳访问上海的请求RT,远大于上海本地内的请求RT)

客户端的RT则会直接影响客户体验,要降低客户端RT,提升用户的体验,必须考虑两点,第一点是服务端的RT,第二点是网络。对于网络来说常见的有CDN、AND、专线等等,分别适用于不同的场景,有机会写个blog聊一下这个话题。

对于服务端RT来说,主要看服务端的做法。
有个公式:RT = Thread CPU Time + Thread Wait Time
从公式中可以看出,要想降低RT,就要降低 Thread CPU Time 或者 Thread Wait Time。这也是马上要重点深挖的一个知识点。

Thread CPU Time(简称CPU Time)

Thread Wait Time(简称Wait Time)

单线程QPS

我们都知道 RT 是由两部分组成 CPU Time + Wait Time 。那如果系统里只有一个线程或者一个进程并且进程中只有一个线程的时候,那么最大的 QPS 是多少呢?
假设 RT 是 199ms (CPU Time 为 19ms ,Wait Time 是 180ms ),那么 1000s以内系统可以接收的最大请求就是
1000ms/(19ms+180ms)≈5.025。

所以得出单线程的QPS公式:

单线程𝑄𝑃𝑆=1000𝑚𝑠/𝑅𝑇单线程QPS=1000ms/RT

最佳线程数

还是上面的那个话题 (CPU Time 为 19ms ,Wait Time 是 180ms ),假设CPU的核数1。假设只有一个线程,这个线程在执行某个请求的时候,CPU真正花在该线程上的时间就是CPU Time,可以看做19ms,那么在整个RT的生命周期中,还有 180ms 的 Wait Time,CPU在做什么呢?抛开系统层面的问题(这里不考虑什么时间片轮循、上下文切换等等),可以认为CPU在这180ms里没做什么,至少对于当前的业务来说,确实没做什么。

  • 一核的情况
    由于每个请求的接收,CPU只需要工作19ms,所以在180ms的时间内,可以认为系统还可以额外接收180ms/19ms≈9个的请求。由于在同步模型中,一个请求需要一个线程来处理,因此,我们需要额外的9个线程来处理这些请求。这样,总的线程数就是:

(180𝑚𝑠+19𝑚𝑠)/19𝑚𝑠≈10个(180ms+19ms)/19ms≈10个

​ 多线程之后,CPU Time从19ms变成了20ms,这1ms的差值代表多线程之后上下文切换、GC带来的额外开销(对于我们java来说是jvm,其他语言另外计算),这里的1ms只是代表一个概述,你也可以把它看做n。

  • 两核的情况
    一核的情况下可以有10个线程,那么两核呢?在理想的情况下,可以认为最佳线程数为:2 x ( 180ms + 20ms )/20ms = 20个
  • CPU利用率
    我们之前说的都是CPU满载下的情况,有时候由于某个瓶颈,导致CPU不得不有效利用,比如两核的CPU,因为某个资源,只能各自使用一半的能效,这样总的CPU利用率就变成了50%,再这样的情况下,最佳线程数应该是:50% x 2 x( 180ms + 20ms )/20ms = 10个
    这个等式转换成公式就是:最佳线程数 = (RT/CPU Time) x CPU 核数 x CPU利用率
    当然,这不是随便推测的,在收集到的很多的一些著作或者论坛的文档里都有这样的一些实验去论述这个公式或者这个说法是正确的。

最大QPS

1.最大QPS公式推导

假设我们知道了最佳线程数,同时我们还知道每个线程的QPS,那么线程数乘以每个线程的QPS既这台机器在最佳线程数下的QPS。所以我们可以得到下图的推算。

image

我们可以把分子和分母去约数,如下图。

image

于是简化后的公式如下图.

image

从公式可以看出,决定QPS的时CPU Time、CPU核数和CPU利用率。CPU核数是由硬件做决定的,很难操纵,但是CPU Time和CPU利用率与我们的代码息息相关。

虽然宏观上是正确的,但是推算的过程中还是有一点小小的不完美,因为多线程下的CPU Time(比如高并发下的GC次数增加消耗更多的CPU Time、线程上下文切换等等)和单线程的CPU Time是不一样的,所以会导致推算出来的结果有误差。

尤其是在同步模型下的相同业务逻辑中,单线程时的CPU Time肯定会比大量多线程的CPU Time小,但是对于异步模型来说,切换的开销会变得小很多,为什么?这里先卖个葫芦吧,看完本篇就知道了。

既然决定QPS的是CPU Time和CPU核数,那么这两个因子又是由谁来决定的呢?

理解最佳线程数量

最佳线程数量 单线程压测,总rt(total),下游依赖rt(IO), rt(CPU)=rt(total)-rt(IO)

最佳线程数量 rt(total)/rt(cpu)

从单线程跑出QPS、各个环节的RT、CPU占用等数据,然后加并发直到QPS不再增加,然后看哪个环境RT增加最大,瓶颈就在哪里

image-20220506121132920

IO

IO耗时增加的RT一般都不影响QPS,最终通过加并发来提升QPS

每次测试数据都是错的,我用RT、并发、TPS一计算数据就不对。现场的人基本不理解RT和TPS同时下降是因为压力不够了(前面有瓶颈,压力打不过来),电话会议讲到半夜

思路严谨

最难讲清楚

前美国国防部长拉姆斯菲尔德:

Reports that say that something hasn’t happened are always interesting to me, because as we know, there are known knowns; there are things we know we know. We also know there are known unknowns; that is to say we know there are some things we do not know. But there are also unknown unknowns—the ones we don’t know we don’t know. And if one looks throughout the history of our country and other free countries, it is the latter category that tend to be the difficult ones.

这句话总结出了人们对事物认知的三种情况:

  1. known knowns(已知的已知)
  2. known unknowns(已知的未知)
  3. unknown unknowns(未知的未知)

这三种情况几乎应证了我学习工作以来面对的所有难题。当我们遇到一个难题的时候,首先我们对这个问题会有一定的了解(否则你都不会遇到这个问题:)),这就是已知的已知部分;在解决这个问题的时候,我们会遇到困难,困难又有两类,一类是你知道困难的点是什么,但是暂时不知道怎么解决,需要学习,这就是已知的未知;剩下的潜伏在问题里的坑,你还没遇到的,就是未知的未知。

性能调优的优先条件是,性能分析,只有分析出系统的瓶颈,才能进行调优。而分析一个系统的性能,就要面对上面提到的三种情况。计算机系统是非常庞大的,包含了计算机体系结构、操作系统、网络、存储等,单单拎出任何一个方向都值得我们去研究很久,因此,我们在分析系统性能的时候,是无法避免地会遇到很多未知的未知问题,而我们要做的事情就是要将它们变成已知的未知,再变成已知的已知。

DK 效应

性能的本质

IPC:insns per cycle ,每个时钟周期执行的指令数量,越大越好

一个程序固定后,指令数量就是固定的(假设同一平台,编译后),那性能之和需要多少个时钟周期才能把这一大堆指令给执行完

如果一个程序里面没必要的循环特别多,那指令总数就特别多,必然会慢;

有的指令效率很高,一个时钟周期就能执行完比如nop(不需要读写任何变量,特快),有的指令需要多个时钟周期(比如 CAS、pause),像pause需要140个时钟周期,一般的intel跑 nop IPC 可以达到4(4条流水线并行),同样的CPU跑pause可能只有 4/140, 相差巨大

但不管怎么样,绝大多时候我们都是在固定的指令下去优化,所以我们重点关注IPC够不够高

经验:一般的程序基本都是读写内存瓶颈,所以IPC大多低于1,能到0.7 以上算是比较优秀了,这种我们把它叫做内存型业务,比如数据库、比如Nginx 都是这种;还有一些是纯计算,内存访问比较少,比如加密解密,他们的IPC大多时候会高于1.

练习:写一个能把IPC跑到最高的代码(可以试试跑一段死循环行不行);写一个能把IPC跑到最低的程序。然后用perf 去看他们的 IPC,用 top 去看他们的CPU使用率

进一步同时把这样的程序跑两份,但是将他们绑到一对超线程上,然后再看他们的IPC以及 top, 然后请思考

答案:写nop将IPC 跑到4, 写 pause 将 IPC 跑到 0.03? 两个nop跑到一对超线程上IPC打折,两个pause跑到一对超线程上,IPC不受影响

老中医经验不可缺少

量变到质变

找瓶颈,先干掉瓶颈才能优化其它

没有找到瓶颈,所做的其它优化会看不出效果,误入歧途,瞎蒙

全栈能力,一文钱难倒英雄好汉

因为关键是找瓶颈,作为java程序员如果只能看jstack、jstat可能发现的不是瓶颈

案例

10+倍性能提升全过程

vxlan网络性能测试

发表于 2018-08-21 | 分类于 network

vxlan网络性能测试


缘起

Docker集群中需要给每个容器分配一个独立的IP,同时在不同宿主机环境上的容器IP又要能够互相联通,所以需要一个overlay的网络(vlan也可以解决这个问题)

overlay网络就是把容器之间的网络包重新打包在宿主机的IP包里面,传到目的容器所在的宿主机后,再把这个overlay的网络包还原成容器包交给容器

这里多了一次封包解包的过程,所以性能上必然有些损耗

封包解包可以在应用层(比如Flannel的UDP封装),但是需要将每个网络包从内核态复制到应用态进行封包,所以性能非常差

比较新的Linux内核带了vxlan功能,也就是将网络包直接在内核态完成封包,所以性能要好很多,本文vxlan指的就是这种方式

本文主要是比较通过vxlan实现的overlay网络之间的性能(相对宿主机之间而言)

iperf3 下载和安装

  • wget http://downloads.es.net/pub/iperf/iperf-3.9.tar.gz (https://downloads.es.net/pub/iperf/)
  • tar zxvf iperf-3.0.6.tar.gz
  • cd iperf-3.0.6
  • ./configure
  • make install

测试环境宿主机的基本配置情况

conf:
loc_node   =  e12174.bja
loc_cpu=  2 Cores: Intel Xeon E5-2430 0 @ 2.20GHz
loc_os =  Linux 3.10.0-327.ali2010.alios7.x86_64
loc_qperf  =  0.4.9
rem_node   =  e26108.bja
rem_cpu=  2 Cores: Intel Xeon E5-2430 0 @ 2.20GHz
rem_os =  Linux 3.10.0-327.ali2010.alios7.x86_64
rem_qperf  =  0.4.9

容器到自身宿主机之间, 跟两容器在同一宿主机,速度差不多

$iperf3 -c 192.168.6.6 
Connecting to host 192.168.6.6, port 5201
[  4] local 192.168.6.1 port 21112 connected to 192.168.6.6 port 5201
[ ID] Interval   Transfer Bandwidth   Retr
[  4]   0.00-10.00  sec  13.9 GBytes  11.9 Gbits/sec1 sender
[  4]   0.00-10.00  sec  13.9 GBytes  11.9 Gbits/sec  receiver

[ ID] Interval   Transfer Bandwidth   Retr
[  4]   0.00-10.00  sec  14.2 GBytes  12.2 Gbits/sec  139 sender
[  4]   0.00-10.00  sec  14.2 GBytes  12.2 Gbits/sec  receiver

[ ID] Interval   Transfer Bandwidth   Retr
[  4]   0.00-10.00  sec  13.9 GBytes  11.9 Gbits/sec   96 sender
[  4]   0.00-10.00  sec  13.9 GBytes  11.9 Gbits/sec  receiver

从宿主机A到宿主机B上的容器

$iperf3 -c 192.168.6.6
Connecting to host 192.168.6.6, port 5201
[  4] local 192.168.6.1 port 47940 connected to 192.168.6.6 port 5201
[ ID] Interval   Transfer Bandwidth   Retr
[  4]   0.00-10.00  sec   409 MBytes   343 Mbits/sec0 sender
[  4]   0.00-10.00  sec   405 MBytes   340 Mbits/sec  receiver

[ ID] Interval   Transfer Bandwidth   Retr
[  4]   0.00-10.00  sec   389 MBytes   326 Mbits/sec   14 sender
[  4]   0.00-10.00  sec   386 MBytes   324 Mbits/sec  receiver

[ ID] Interval   Transfer Bandwidth   Retr
[  4]   0.00-10.00  sec   460 MBytes   386 Mbits/sec7 sender
[  4]   0.00-10.00  sec   458 MBytes   384 Mbits/sec  receiver

两宿主机之间测试

$iperf3 -c 10.125.26.108
Connecting to host 10.125.26.108, port 5201
[  4] local 10.125.12.174 port 24309 connected to 10.125.26.108 port 5201
[ ID] Interval   Transfer Bandwidth   Retr
[  4]   0.00-10.00  sec   471 MBytes   395 Mbits/sec0 sender
[  4]   0.00-10.00  sec   469 MBytes   393 Mbits/sec  receiver

[ ID] Interval   Transfer Bandwidth   Retr
[  4]   0.00-10.00  sec   428 MBytes   359 Mbits/sec0 sender
[  4]   0.00-10.00  sec   426 MBytes   357 Mbits/sec  receiver

[ ID] Interval   Transfer Bandwidth   Retr
[  4]   0.00-10.00  sec   430 MBytes   360 Mbits/sec0 sender
[  4]   0.00-10.00  sec   427 MBytes   358 Mbits/sec  receiver

两容器之间(跨宿主机)

$iperf3 -c 192.168.6.6
Connecting to host 192.168.6.6, port 5201
[  4] local 192.168.6.5 port 37719 connected to 192.168.6.6 port 5201
[ ID] Interval   Transfer Bandwidth   Retr
[  4]   0.00-10.00  sec   403 MBytes   338 Mbits/sec   18 sender
[  4]   0.00-10.00  sec   401 MBytes   336 Mbits/sec  receiver

[ ID] Interval   Transfer Bandwidth   Retr
[  4]   0.00-10.00  sec   428 MBytes   359 Mbits/sec   15 sender
[  4]   0.00-10.00  sec   425 MBytes   356 Mbits/sec  receiver

[ ID] Interval   Transfer Bandwidth   Retr
[  4]   0.00-10.00  sec   508 MBytes   426 Mbits/sec   11 sender
[  4]   0.00-10.00  sec   506 MBytes   424 Mbits/sec  receiver

PPS 压测

必须到这里下载最新版的iperf2 才有增强的 pps 测试能力

购买的 ECS PPS为 600 万

1
2
3
4
5
6
7
iperf -c 10.0.1.2 -t 600 -u -i 1 -l 16 -b 500kpps -P 16 //实际outgoing 有丢包

调整参数刚好压到 ECS的标称 600万 PPS,同时通过netstat -s 观察没有丢包
iperf -c 10.0.1.2 -t 600 -u -i 1 -l 16 -b 250kpps -P 24 -e
[SUM] 25.00-26.00 sec 91.6 MBytes 768 Mbits/sec 6002993/1 6002999 pps

iperf -c 100.69.170.17 -u -i 1 -l 16 -b 2000kpps -e

压测机器内核 3.10,故意不打满,160万pps

server收包端的top,几乎看不到si 和ksoftirq:

image-20231227174305030

对应的tsar pps 监控:

image-20231227174402374

client端的iperf 数据:

image-20231227174428881

在如上基础上加大压力,可以看到si% 快速被打爆,和流量不成正比,大量丢包

image-20231227174725237

image-20231227174802828

image-20231227174829420

实际流量只是从160万pps 增加到320万pps,但是si CPU的增加可不是翻倍,而是出现了踩踏,丢包也大量出现(因为si% 达到100%)

在4.19/5.10的内核上进行如上验证,也是一样出现了软中断CPU踩踏

带宽压测

netperf 安装依赖 automake-1.14, 环境无法升级,放弃

qperf 测试工具

  • sudo yum install qperf -y

两台宿主机之间

$qperf -t 10  10.125.26.108 tcp_bw tcp_lat
tcp_bw:
bw  =  50.5 MB/sec
tcp_lat:
latency  =  332 us

包的大小分别为1和128

$qperf  -oo msg_size:1   10.125.26.108 tcp_bw tcp_lat
tcp_bw:
bw  =  1.75 MB/sec
tcp_lat:
latency  =  428 us

$qperf  -oo msg_size:128   10.125.26.108 tcp_bw tcp_lat
tcp_bw:
bw  =  57.8 MB/sec
tcp_lat:
latency  =  504 us

两台宿主机之间,包的大小从一个字节每次翻倍测试

$qperf  -oo msg_size:1:4K:*2 -vu  10.125.26.108 tcp_bw tcp_lat 
tcp_bw:
bw=  1.86 MB/sec
msg_size  = 1 bytes
tcp_bw:
bw=  3.54 MB/sec
msg_size  = 2 bytes
tcp_bw:
bw=  6.43 MB/sec
msg_size  = 4 bytes
tcp_bw:
bw=  14.3 MB/sec
msg_size  = 8 bytes
tcp_bw:
bw=  27.1 MB/sec
msg_size  =16 bytes
tcp_bw:
bw=  42.3 MB/sec
msg_size  =32 bytes
tcp_bw:
bw=  51.8 MB/sec
msg_size  =64 bytes
tcp_bw:
bw=  49.7 MB/sec
msg_size  =   128 bytes
tcp_bw:
bw=  48.2 MB/sec
msg_size  =   256 bytes
tcp_bw:
bw=   58 MB/sec
msg_size  =  512 bytes
tcp_bw:
bw=  54.6 MB/sec
msg_size  = 1 KiB (1,024)
tcp_bw:
bw=  48.7 MB/sec
msg_size  = 2 KiB (2,048)
tcp_bw:
bw=  53.6 MB/sec
msg_size  = 4 KiB (4,096)
tcp_lat:
latency   =  432 us
msg_size  =1 bytes
tcp_lat:
latency   =  480 us
msg_size  =2 bytes
tcp_lat:
latency   =  441 us
msg_size  =4 bytes
tcp_lat:
latency   =  487 us
msg_size  =8 bytes
tcp_lat:
latency   =  404 us
msg_size  =   16 bytes
tcp_lat:
latency   =  335 us
msg_size  =   32 bytes
tcp_lat:
latency   =  338 us
msg_size  =   64 bytes
tcp_lat:
latency   =  401 us
msg_size  =  128 bytes
tcp_lat:
latency   =  496 us
msg_size  =  256 bytes
tcp_lat:
latency   =  684 us
msg_size  =  512 bytes
tcp_lat:
latency   =  534 us
msg_size  =1 KiB (1,024)
tcp_lat:
latency   =  681 us
msg_size  =2 KiB (2,048)
tcp_lat:
latency   =  701 us
msg_size  =4 KiB (4,096)

两个容器之间(分别在两台宿主机上)

$qperf -t 10  192.168.6.6 tcp_bw tcp_lat 
tcp_bw:
bw  =  44.4 MB/sec
tcp_lat:
latency  =  512 us

包的大小分别为1和128

$qperf -oo msg_size:1  192.168.6.6 tcp_bw tcp_lat 
tcp_bw:
bw  =  1.13 MB/sec
tcp_lat:
latency  =  630 us

$qperf -oo msg_size:128  192.168.6.6 tcp_bw tcp_lat 
tcp_bw:
bw  =  44.2 MB/sec
tcp_lat:
latency  =  526 us

两个容器之间,包的大小从一个字节每次翻倍测试

$qperf -oo msg_size:1:4K:*2  192.168.6.6 -vu tcp_bw tcp_lat 
tcp_bw:
bw=  1.06 MB/sec
msg_size  = 1 bytes
tcp_bw:
bw=  2.29 MB/sec
msg_size  = 2 bytes
tcp_bw:
bw=  3.79 MB/sec
msg_size  = 4 bytes
tcp_bw:
bw=  7.66 MB/sec
msg_size  = 8 bytes
tcp_bw:
bw=  14 MB/sec
msg_size  =  16 bytes
tcp_bw:
bw=  24.4 MB/sec
msg_size  =32 bytes
tcp_bw:
bw=  36 MB/sec
msg_size  =  64 bytes
tcp_bw:
bw=  46.7 MB/sec
msg_size  =   128 bytes
tcp_bw:
bw=   56 MB/sec
msg_size  =  256 bytes
tcp_bw:
bw=  42.2 MB/sec
msg_size  =   512 bytes
tcp_bw:
bw=  57.6 MB/sec
msg_size  = 1 KiB (1,024)
tcp_bw:
bw=  52.3 MB/sec
msg_size  = 2 KiB (2,048)
tcp_bw:
bw=  41.7 MB/sec
msg_size  = 4 KiB (4,096)
tcp_lat:
latency   =  447 us
msg_size  =1 bytes
tcp_lat:
latency   =  417 us
msg_size  =2 bytes
tcp_lat:
latency   =  503 us
msg_size  =4 bytes
tcp_lat:
latency   =  488 us
msg_size  =8 bytes
tcp_lat:
latency   =  452 us
msg_size  =   16 bytes
tcp_lat:
latency   =  537 us
msg_size  =   32 bytes
tcp_lat:
latency   =  712 us
msg_size  =   64 bytes
tcp_lat:
latency   =  521 us
msg_size  =  128 bytes
tcp_lat:
latency   =  450 us
msg_size  =  256 bytes
tcp_lat:
latency   =  442 us
msg_size  =  512 bytes
tcp_lat:
latency   =  630 us
msg_size  =1 KiB (1,024)
tcp_lat:
latency   =  519 us
msg_size  =2 KiB (2,048)
tcp_lat:
latency   =  621 us
msg_size  =4 KiB (4,096)

​

结论

  • iperf3测试带宽方面vxlan网络基本和宿主机一样,没有什么损失
  • qperf测试vxlan的带宽只相当于宿主机的60-80%
  • qperf测试一个字节的小包vxlan的带宽只相当于宿主机的60-65%
  • 由上面的结论猜测:物理带宽更大的情况下vxlan跟宿主机的差别会扩大

qperf安装更容易; iperf3 可以多连接并发测试,可以控制包的大小、nodelay等等

网络方案性能

OS Host Docker_Host Docker_NAT_IPTABLES Docker_NAT_PROXY Docker_BRIDGE_VLAN Docker_OVS_VLAN Docker_HAVS_VLAN
TPS 6U 118727.5 115962.5 83281.08 29104.33 57327.15 55606.37 54686.88
TPS 7U 117501.4 110010.7 101131.2 34795.39 108857.7 107554.3 105021
6U BASE -2.38% -42.56% -307.94% -107.11% -113.51% -117.10%
7U BASE -6.81% -16.19% -237.69% -7.94% -9.25% -11.88%
RT 6U(ms) 0.330633 0.362042 0.505125 1.423767 0.799308 0.763842 0.840458
RT 7U(ms) 0.3028 0.321267 0.346325 1.183225 0.325333 0.335708 0.33535
6U(us) BASE 31.40833 174.4917 1093.133 468.675 433.2083 509.825
7U(us) BASE 18.46667 43.525 880.425 22.53333 32.90833 32.55
  • Host:是指没有隔离的情况下,D13物理机;
  • Docker_Host:是指Docker采用Host网络模式;
  • Docker_NAT_IPTABLES:是指Docker采用NAT网络模式,通过IPTABLES进行网络转发。
  • Docker_NAT_PROXY:是指Docker采用NAT网络模式,通过docker-proxy进行网络转发。
  • Docker_BRIDGE:是指Docker采用Bridge网络模式,并且配置静态IP和VLAN701,这里使用VLAN。
  • Docker_OVS_VLAN:是指Docker采用VSwitch网络模式,通过OpenVSwitch进行网络通信,使用ACS VLAN Driver。
  • Docker_HAVS_VLAN:是指Docker采用VSwitch网络模式,通过HAVS进行网络通信,使用VLAN。

通过测试,汇总测试结论如下

  1. Docker_Host网络模式在6U和7U环境下,性能比物理机方案上性能降低了26%左右,RT增加了1830us左右。

  2. Docker_NAT_IPTABLES网络模式在6U环境下,性能比物理机方案上性能降低了43%左右,RT增加了174us;在7U环境下,性能比物理机方案上性能降低了16%左右,RT增加了44us;此外,可以明显看出,7U环境比6U环境性能上优化了20%,RT上减少了130us左右。

  3. Docker_NAT_PROXY网络模式在6U环境下,性能比物理机方案性能降低了300%,RT增加了1ms以上;在7U环境下,性能比物理机方案性能降低了237%,RT增加了880us以上;此外,可以明显看出,7U环境比6U环境性能上优化了20%,RT上减少了200us左右。

  4. Docker_BRIDGE_VLAN网络模式在6U环境下,性能比物理机方案性能降低了107%,RT增加了469us;在7U环境下,性能比物理机方案性能降低了8%左右,RT增加了23us左右;此外,可以明显看出,7U环境比6U环境性能上优化了90%,RT上减少了446us。从诊断上来看,6U和7U的性能差异主要在VLAN的处理上的spin_lock,详细可以参考之前的测试验证。

  5. Docker_OVS_VLAN网络模式在6U环境下,性能比物理机方案性能降低了114%,RT增加了433us;在7U环境下,性能比物理机方案性能降低了9%左右,RT增加了33us;此外,可以明显看出,7U环境比6U环境性能上优化了93%,RT上减少了400us。从诊断上来看,6U和7U的性能差异主要在VLAN的处理上的spin_lock。并且发现,OVS与Bridge网络模式性能上基本持平,无较大性能上的差异。

  6. Docker_HAVS_VLAN网络模式在6U环境下,性能比物理机方案性能降低了117%,RT增加了510us;在7U环境下,性能比物理机方案性能降低了12%左右,RT增加了33us;此外,可以明显看出,7U环境比6U环境性能上优化了92%,RT上减少了477us。从诊断上来看,6U和7U的性能差异主要在VLAN的处理上的spin_lock。并且发现,HAVS与Bridge网络模式性能上基本持平,无较大性能上的差异;HAVS与OVS的性能上差异也较小,无较大性能上的差异。

  7. SR-IOV网络模式由于存在OS、Docker、网卡等要求,非通用化方案,将作为进一步的优化方案进行探索。

网络性能结果分析(rama等同方舟vlan网络方案)

延迟数据汇总:

host rama不开启mac nat rama开启mac nat calico-bgp flannel-vxlan
64 0.041 0.041 0.041 0.042 0.041
512 0.041 0.041 0.043 0.041 0.043
1024 0.045 0.045 0.045 0.046 0.048
2048 0.073 0.072 0.072 0.073 0.073
4096 0.072 0.070 0.073 0.071 0.079
16384 0.148 0.144 0.149 0.242 0.200
32678 0.244 0.335 0.242 0.320 0.352
64512 0.300 0.481 0.419 0.437 0.541

image.png

吞吐量数据汇总:

host rama不开启mac nat rama开启mac nat calico-bgp flannel-vxlan
64 386 381 381 377 359
512 2660 2370 2530 2580 1840
1024 5170 4590 4880 4510 2610
2048 7710 7350 7040 7420 3310
4096 9410 8750 8220 8440 3830
16384 9410 8850 8460 8580 5080
32678 9410 8810 8580 8550 4950
65507 9410 8660 8410 8540 4920

image.png

从延迟上来看,rama与calico-bgp相差不大,从数据上略低于host性能,略高于flannel-vxlan;从吞吐量上看,区别会明显一些,当报文长度大于4096 KB 时,均观察到各网络插件的吞吐量达到最大值,从最大值上来看可以初步得出以下结论:

host > rama不开启mac nat > rama开启mac nat ≈ calico-bgp > flannel-vxlan

rama不开启mac nat时性能最高,开启mac nat功能,性能与calico-bgp基本相同,并且性能大幅度高于flannel-vxlan;虽然rama开启mac nat之后的性能与每个节点上的pod数量直接相关,但由于测试 rama开启mac nat方案 的时候,取的是两个个节点上50个pod中预计性能最差的pod,基本可以反映一般情况

参考文章:

https://linoxide.com/monitoring-2/install-iperf-test-network-speed-bandwidth/

http://blog.yufeng.info/archives/2234

双11全链路压测中通过Perf发现的一个SpringMVC 的性能问题

发表于 2018-07-26 | 分类于 troubleshooting

双11全链路压测中通过Perf发现的一个SpringMVC 的性能问题

在最近的全链路压测中TPS不够理想,然后通过perf 工具(perf record 采样, perf report 展示)看到(可以点击看大图):

screenshot

再来看CPU消耗的火焰图:

screenshot

图中CPU的消耗占21%,不太正常。

可以看到Spring框架消耗了比较多的CPU,具体原因就是在Spring MVC中会大量使用到
@RequestMapping
@PathVariable
带来使用上的便利

业务方修改代码去掉spring中的methodMapping解析后的结果(性能提升了40%):

screenshot.png

图中核心业务逻辑能抢到的cpu是21%(之前是15%)。spring methodMapping相关的也在火焰图中找不到了

Spring收到请求URL后要取出请求变量和做业务运算,具体代码(对照第一个图的调用堆栈):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
170	public RequestMappingInfo More ...getMatchingCondition(HttpServletRequest request) {
171 RequestMethodsRequestCondition methods = methodsCondition.getMatchingCondition(request);
172 ParamsRequestCondition params = paramsCondition.getMatchingCondition(request);
173 HeadersRequestCondition headers = headersCondition.getMatchingCondition(request);
174 ConsumesRequestCondition consumes = consumesCondition.getMatchingCondition(request);
175 ProducesRequestCondition produces = producesCondition.getMatchingCondition(request);
176
177 if (methods == null || params == null || headers == null || consumes == null || produces == null) {
178 return null;
179 }
180
181 PatternsRequestCondition patterns = patternsCondition.getMatchingCondition(request);
182 if (patterns == null) {
183 return null;
184 }
185
186 RequestConditionHolder custom = customConditionHolder.getMatchingCondition(request);
187 if (custom == null) {
188 return null;
189 }
190
191 return new RequestMappingInfo(patterns, methods, params, headers, consumes, produces, custom.getCondition());
192 }

doMatch 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
96 
97 protected boolean More ...doMatch(String pattern, String path, boolean fullMatch,
98 Map<String, String> uriTemplateVariables) {
99
100 if (path.startsWith(this.pathSeparator) != pattern.startsWith(this.pathSeparator)) {
101 return false;
102 }
103
104 String[] pattDirs = StringUtils.tokenizeToStringArray(pattern, this.pathSeparator, this.trimTokens, true);
105 String[] pathDirs = StringUtils.tokenizeToStringArray(path, this.pathSeparator, this.trimTokens, true);
106
107 int pattIdxStart = 0;
108 int pattIdxEnd = pattDirs.length - 1;
109 int pathIdxStart = 0;
110 int pathIdxEnd = pathDirs.length - 1;
111
112 // Match all elements up to the first **
113 while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) {
114 String patDir = pattDirs[pattIdxStart];
115 if ("**".equals(patDir)) {
116 break;
117 }
118 if (!matchStrings(patDir, pathDirs[pathIdxStart], uriTemplateVariables)) {
119 return false;
120 }
121 pattIdxStart++;
122 pathIdxStart++;
123 }
124
125 if (pathIdxStart > pathIdxEnd) {
126 // Path is exhausted, only match if rest of pattern is * or **'s
127 if (pattIdxStart > pattIdxEnd) {
128 return (pattern.endsWith(this.pathSeparator) ? path.endsWith(this.pathSeparator) :
129 !path.endsWith(this.pathSeparator));
130 }
131 if (!fullMatch) {
132 return true;
133 }
134 if (pattIdxStart == pattIdxEnd && pattDirs[pattIdxStart].equals("*") && path.endsWith(this.pathSeparator)) {
135 return true;
136 }
137 for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
138 if (!pattDirs[i].equals("**")) {
139 return false;
140 }
141 }
142 return true;
143 }
144 else if (pattIdxStart > pattIdxEnd) {
145 // String not exhausted, but pattern is. Failure.
146 return false;
147 }
148 else if (!fullMatch && "**".equals(pattDirs[pattIdxStart])) {
149 // Path start definitely matches due to "**" part in pattern.
150 return true;
151 }
152
153 // up to last '**'
154 while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) {
155 String patDir = pattDirs[pattIdxEnd];
156 if (patDir.equals("**")) {
157 break;
158 }
159 if (!matchStrings(patDir, pathDirs[pathIdxEnd], uriTemplateVariables)) {
160 return false;
161 }
162 pattIdxEnd--;
163 pathIdxEnd--;
164 }
165 if (pathIdxStart > pathIdxEnd) {
166 // String is exhausted
167 for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
168 if (!pattDirs[i].equals("**")) {
169 return false;
170 }
171 }
172 return true;
173 }
174
175 while (pattIdxStart != pattIdxEnd && pathIdxStart <= pathIdxEnd) {
176 int patIdxTmp = -1;
177 for (int i = pattIdxStart + 1; i <= pattIdxEnd; i++) {
178 if (pattDirs[i].equals("**")) {
179 patIdxTmp = i;
180 break;
181 }
182 }
183 if (patIdxTmp == pattIdxStart + 1) {
184 // '**/**' situation, so skip one
185 pattIdxStart++;
186 continue;
187 }
188 // Find the pattern between padIdxStart & padIdxTmp in str between
189 // strIdxStart & strIdxEnd
190 int patLength = (patIdxTmp - pattIdxStart - 1);
191 int strLength = (pathIdxEnd - pathIdxStart + 1);
192 int foundIdx = -1;
193
194 strLoop:
195 for (int i = 0; i <= strLength - patLength; i++) {
196 for (int j = 0; j < patLength; j++) {
197 String subPat = pattDirs[pattIdxStart + j + 1];
198 String subStr = pathDirs[pathIdxStart + i + j];
199 if (!matchStrings(subPat, subStr, uriTemplateVariables)) {
200 continue strLoop;
201 }
202 }
203 foundIdx = pathIdxStart + i;
204 break;
205 }
206
207 if (foundIdx == -1) {
208 return false;
209 }
210
211 pattIdxStart = patIdxTmp;
212 pathIdxStart = foundIdx + patLength;
213 }
214
215 for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
216 if (!pattDirs[i].equals("**")) {
217 return false;
218 }
219 }
220
221 return true;
222 }

最后补一个找到瓶颈点后 Google到类似问题的文章,并给出了具体数据和解决方法:http://www.cnblogs.com/ucos/articles/5542012.html

以及这篇文章中给出的优化前后对比图:
screenshot

就是要你懂TCP--TCP性能问题

发表于 2018-06-14 | 分类于 TCP

就是要你懂TCP–TCP性能问题

先通过一个案例来看TCP 性能点

案例描述

某个PHP服务通过Nginx将后面的redis封装了一下,让其他应用可以通过http协议访问Nginx来get、set 操作redis

上线后测试一切正常,每次操作几毫秒. 但是有个应用的value是300K,这个时候set一次需要300毫秒以上。 在没有任何并发压力单线程单次操作也需要这么久,这个操作需要这么久是不合理和无法接受的。

问题的原因

因为TCP协议为了对带宽利用率、性能方面优化,而做了一些特殊处理。比如Delay Ack和Nagle算法。

这个原因对大家理解TCP基本的概念后能在实战中了解一些TCP其它方面的性能和影响。

什么是delay ack

由我前面的TCP介绍文章大家都知道,TCP是可靠传输,可靠的核心是收到包后回复一个ack来告诉对方收到了。

来看一个例子:
image.png

截图中的Nignx(8085端口),收到了一个http request请求,然后立即回复了一个ack包给client,接着又回复了一个http response 给client。大家注意回复的ack包长度66,实际内容长度为0,ack信息放在TCP包头里面,也就是这里发了一个66字节的空包给客户端来告诉客户端我收到你的请求了。

这里没毛病,逻辑很对,符合TCP的核心可靠传输的意义。但是带来的一个问题是:性能不够好(用了一个空包用于特意回复ack,有点浪费)。那能不能优化呢?

这里的优化方法就是delay ack。

**delay ack **是指收到包后不立即ack,而是等一小会(比如40毫秒)看看,如果这40毫秒内有其它包(比如上面的http response)要发给client,那么这个ack包就跟着发过去(顺风车,http reponse包不需要增加任何大小和包的数量),这样节省了资源。 当然如果超过这个时间还没有包发给client(比如nginx处理需要 40毫秒以上),那么这个ack也要发给client了(即使为空,要不client以为丢包了,又要重发http request,划不来)。

假如这个时候ack包还在等待延迟发送的时候,又收到了client的一个包,那么这个时候server有两个ack包要回复,那么os会把这两个ack包合起来立即回复一个ack包给client,告诉client前两个包都收到了。

也就是delay ack开启的情况下:ack包有顺风车就搭;如果凑两个ack包那么包个车也立即发车;再如果等了40毫秒以上也没顺风车或者拼车的,那么自己打个专车也要发车。

截图中Nginx没有开delay ack,所以你看红框中的ack是完全可以跟着绿框(http response)一起发给client的,但是没有,红框的ack立即打车跑了

什么是Nagle算法

下面的伪代码就是Nagle算法的基本逻辑,摘自wiki:

if there is new data to send
  if the window size >= MSS and available data is >= MSS
        send complete MSS segment now
  else
    if there is unconfirmed data still in the pipe
          enqueue data in the buffer until an acknowledge is received
    else
          send data immediately
    end if
  end if
end if

这段代码的意思是如果接收窗口大于MSS 并且 要发送的数据大于 MSS的话,立即发送。
否则:
看前面发出去的包是不是还有没有ack的,如果有没有ack的那么我这个小包不急着发送,等前面的ack回来再发送

我总结下Nagle算法逻辑就是:如果发送的包很小(不足MSS),又有包发给了对方对方还没回复说收到了,那我也不急着发,等前面的包回复收到了再发。这样可以优化带宽利用率(早些年带宽资源还是很宝贵的),Nagle算法也是用来优化改进tcp传输效率的。

如果client启用Nagle,并且server端启用了delay ack会有什么后果呢?

假如client要发送一个http请求给server,这个请求有1600个bytes,通过握手协商好的MSS是1460,那么这1600个bytes就会分成2个TCP包,第一个包1460,剩下的140bytes放在第二个包。第一个包发出去后,server收到第一个包,因为delay ack所以没有回复ack,同时因为server没有收全这个HTTP请求,所以也没法回复HTTP response(server的应用层在等一个完整的HTTP请求然后才能回复,或者TCP层在等超过40毫秒的delay时间)。client这边开启了Nagle算法(默认开启)第二个包比较小(140<MSS),第一个包的ack还没有回来,那么第二个包就不发了,等!互相等!一直到Delay Ack的Delay时间到了!

这就是悲剧的核心原因。

再来看一个经典例子和数据分析

这个案例的原始出处

案例核心奇怪的现象是:

  • 如果传输的数据是 99,900 bytes,速度5.2M/秒;
  • 如果传输的数据是 100,000 bytes 速度2.7M/秒,多了10个bytes,不至于传输速度差这么多。

原因就是:

 99,900 bytes = 68 full-sized 1448-byte packets, plus 1436 bytes extra
100,000 bytes = 69 full-sized 1448-byte packets, plus   88 bytes extra

99,900 bytes:

68个整包会立即发送(都是整包,不受Nagle算法的影响),因为68是偶数,对方收到最后两个包后立即回复ack(delay ack凑够两个也立即ack),那么剩下的1436也很快发出去(根据Nagle算法,没有没ack的包了,立即发)

100,000 bytes:

前面68个整包很快发出去也收到ack回复了,然后发了第69个整包,剩下88bytes(不够一个整包)根据Nagle算法要等一等,server收到第69个ack后,因为delay ack不回复(手里只攒下一个没有回复的包),所以client、server两边等在等,一直等到server的delay ack超时了。

挺奇怪和挺有意思吧,作者还给出了传输数据的图表:

这是有问题的传输图,明显有个平台层,这个平台层就是两边在互相等,整个速度肯定就上不去。

如果传输的都是99,900,那么整个图形就很平整:

回到前面的问题

服务写好后,开始测试都没有问题,rt很正常(一般测试的都是小对象),没有触发这个问题。后来碰到一个300K的rt就到几百毫秒了,就是因为这个原因。

另外有些http post会故意把包头和包内容分成两个包,再加一个Expect 参数之类的,更容易触发这个问题。

这是修改后的C代码

    struct curl_slist *list = NULL;
    //合并post包
    list = curl_slist_append(list, "Expect:");  

    CURLcode code(CURLE_FAILED_INIT);
    if (CURLE_OK == (code = curl_easy_setopt(curl, CURLOPT_URL, oss.str().c_str())) &&
            CURLE_OK == (code = curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, timeout)) &&
            CURLE_OK == (code = curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &write_callback)) &&
            CURLE_OK == (code = curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L)) &&
            CURLE_OK == (code = curl_easy_setopt(curl, CURLOPT_POST, 1L)) &&
            CURLE_OK == (code = curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, pooh.sizeleft)) &&
            CURLE_OK == (code = curl_easy_setopt(curl, CURLOPT_READFUNCTION, read_callback)) &&
            CURLE_OK == (code = curl_easy_setopt(curl, CURLOPT_READDATA, &pooh)) &&                
            CURLE_OK == (code = curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L)) && //1000 ms curl bug
            CURLE_OK == (code = curl_easy_setopt(curl, CURLOPT_HTTPHEADER, list))                
            ) {

            //这里如果是小包就不开delay ack,实际不科学
            if (request.size() < 1024) {
                    code = curl_easy_setopt(curl, CURLOPT_TCP_NODELAY, 1L);
            } else {
                    code = curl_easy_setopt(curl, CURLOPT_TCP_NODELAY, 0L);
            }
            if(CURLE_OK == code) {
                    code = curl_easy_perform(curl);
            }

上面中文注释的部分是后来的改进,然后经过测试同一个300K的对象也能在几毫秒以内完成get、set了。

尤其是在 Post请求将HTTP Header 和Body内容分成两个包后,容易出现这种延迟问题。

一些概念和其它会导致TCP性能差的原因

跟速度相关的几个概念

  • CWND:Congestion Window,拥塞窗口,负责控制单位时间内,数据发送端的报文发送量。TCP 协议规定,一个 RTT(Round-Trip Time,往返时延,大家常说的 ping 值)时间内,数据发送端只能发送 CWND 个数据包(注意不是字节数)。TCP 协议利用 CWND/RTT 来控制速度。这个值是根据丢包动态计算出来的
  • SS:Slow Start,慢启动阶段。TCP 刚开始传输的时候,速度是慢慢涨起来的,除非遇到丢包,否则速度会一直指数性增长(标准 TCP 协议的拥塞控制算法,例如 cubic 就是如此。很多其它拥塞控制算法或其它厂商可能修改过慢启动增长特性,未必符合指数特性)。
  • CA:Congestion Avoid,拥塞避免阶段。当 TCP 数据发送方感知到有丢包后,会降低 CWND,此时速度会下降,CWND 再次增长时,不再像 SS 那样指数增,而是线性增(同理,标准 TCP 协议的拥塞控制算法,例如 cubic 是这样,很多其它拥塞控制算法或其它厂商可能修改过慢启动增长特性,未必符合这个特性)。
  • ssthresh:Slow Start Threshold,慢启动阈值。当数据发送方感知到丢包时,会记录此时的 CWND,并计算合理的 ssthresh 值(ssthresh <= 丢包时的 CWND),当 CWND 重新由小至大增长,直到 sshtresh 时,不再 SS 而是 CA。但因为数据确认超时(数据发送端始终收不到对端的接收确认报文),发送端会骤降 CWND 到最初始的状态。
  • tcp_wmem 对应send buffer,也就是滑动窗口大小

image.png

上图一旦发生丢包,cwnd降到1 ssthresh降到cwnd/2,一夜回到解放前,太保守了,实际大多情况下都是公网带宽还有空余但是链路过长,非带宽不够丢包概率增大,对此没必要这么保守(tcp诞生的背景主要针对局域网、双绞线来设计,偏保守)。RTT越大的网络环境(长肥管道)这个问题越是严重,表现就是传输速度抖动非常厉害。

所以改进的拥塞算法一旦发现丢包,cwnd和ssthresh降到原来的cwnd的一半。

image.png

TCP性能优化点

  • 建连优化:TCP 在建立连接时,如果丢包,会进入重试,重试时间是 1s、2s、4s、8s 的指数递增间隔,缩短定时器可以让 TCP 在丢包环境建连时间更快,非常适用于高并发短连接的业务场景。
  • 首包优化:此优化其实没什么实质意义,若要说一定会有意义的话,可能就是满足一些评测标准的需要吧,例如有些客户以首包时间作为性能评判的一个依据。所谓首包时间,简单解释就是从 HTTP Client 发出 GET 请求开始计时,到收到 HTTP 响应的时间。为此,Server 端可以通过 TCP_NODELAY 让服务器先吐出 HTTP 头,再吐出实际内容(分包发送,原本是粘到一起的),来进行提速和优化。据说更有甚者先让服务器无条件返回 “HTTP/“ 这几个字符,然后再去 upstream 拿数据。这种做法在真实场景中没有任何帮助,只能欺骗一下探测者罢了,因此还没见过有直接发 “HTTP/“ 的,其实是一种作弊行为。

image.png

  • 平滑发包:如前文所述,在 RTT 内均匀发包,规避微分时间内的流量突发,尽量避免瞬间拥塞,此处不再赘述。
  • 丢包预判:有些网络的丢包是有规律性的,例如每隔一段时间出现一次丢包,例如每次丢包都连续丢几个等,如果程序能自动发现这个规律(有些不明显),就可以针对性提前多发数据,减少重传时间、提高有效发包率。
  • RTO 探测:如前文讲 TCP 基础时说过的,若始终收不到 ACK 报文,则需要触发 RTO 定时器。RTO 定时器一般都时间非常长,会浪费很多等待时间,而且一旦 RTO,CWND 就会骤降(标准 TCP),因此利用 Probe 提前与 RTO 去试探,可以规避由于 ACK 报文丢失而导致的速度下降问题。
  • 带宽评估:通过单位时间内收到的 ACK 或 SACK 信息可以得知客户端有效接收速率,通过这个速率可以更合理的控制发包速度。
  • 带宽争抢:有些场景(例如合租)是大家互相挤占带宽的,假如你和室友各 1Mbps 的速度看电影,会把 2Mbps 出口占满,而如果一共有 3 个人看,则每人只能分到 1/3。若此时你的流量流量达到 2Mbps,而他俩还都是 1Mbps,则你至少仍可以分到 2/(2+1+1) * 2Mbps = 1Mbps 的 50% 的带宽,甚至更多,代价就是服务器侧的出口流量加大,增加成本。(TCP 优化的本质就是用带宽换用户体验感)
  • 链路质量记忆(后面有反面案例):如果一个 Client IP 或一个 C 段 Network,若已经得知了网络质量规律(例如 CWND 多大合适,丢包规律是怎样的等),就可以在下次连接时,优先使用历史经验值,取消慢启动环节直接进入告诉发包状态,以提升客户端接收数据速率。

image.png

参数

net.ipv4.tcp_slow_start_after_idle

内核协议栈参数 net.ipv4.tcp_slow_start_after_idle 默认是开启的,这个参数的用途,是为了规避 CWND 无休止增长,因此在连接不断开,但一段时间不传输数据的话,就将 CWND 收敛到 initcwnd,kernel-2.6.32 是 10,kernel-2.6.18 是 2。因此在 HTTP Connection: keep-alive 的环境下,若连续两个 GET 请求之间存在一定时间间隔,则此时服务器端会降低 CWND 到初始值,当 Client 再次发起 GET 后,服务器会重新进入慢启动流程。

这种友善的保护机制,对于 CDN 来说是帮倒忙,因此我们可以通过命令将此功能关闭,以提高 HTTP Connection: keep-alive 环境下的用户体验感。

 sysctl net.ipv4.tcp_slow_start_after_idle=0

运行中每个连接 CWND/ssthresh(slow start threshold) 的确认

#for i in {1..1000}; do ss -i dst 172.16.250.239:22 ; sleep 0.2; done
Netid      State      Recv-Q      Send-Q             Local Address:Port              Peer Address:Port     Process
tcp        ESTAB      0           2068920           192.168.99.211:43090           172.16.250.239:ssh
     cubic wscale:7,7 rto:224 rtt:22.821/0.037 ato:40 mss:1448 pmtu:1500 rcvmss:1056 advmss:1448 cwnd:3004 ssthresh:3004 bytes_sent:139275001 bytes_acked:137206082 bytes_received:46033 segs_out:99114 segs_in:9398 data_segs_out:99102 data_segs_in:1203 send 1524.8Mbps lastrcv:4 pacing_rate 1829.8Mbps delivery_rate 753.9Mbps delivered:97631 app_limited busy:2024ms unacked:1472 rcv_rtt:23 rcv_space:14480 rcv_ssthresh:64088 minrtt:22.724
Netid      State      Recv-Q      Send-Q             Local Address:Port              Peer Address:Port     Process
tcp        ESTAB      0           2036080           192.168.99.211:43090           172.16.250.239:ssh
     cubic wscale:7,7 rto:224 rtt:22.814/0.022 ato:40 mss:1448 pmtu:1500 rcvmss:1056 advmss:1448 cwnd:3004 ssthresh:3004 bytes_sent:157304161 bytes_acked:155284502 bytes_received:51685 segs_out:111955 segs_in:10597 data_segs_out:111943 data_segs_in:1360 send 1525.3Mbps pacing_rate 1830.3Mbps delivery_rate 745.7Mbps delivered:110506 app_limited busy:2228ms unacked:1438 rcv_rtt:23 rcv_space:14480 rcv_ssthresh:64088 notsent:16420 minrtt:22.724
Netid      State      Recv-Q      Send-Q             Local Address:Port              Peer Address:Port     Process
tcp        ESTAB      0           1970400           192.168.99.211:43090           172.16.250.239:ssh
     cubic wscale:7,7 rto:224 rtt:22.816/0.028 ato:40 mss:1448 pmtu:1500 rcvmss:1056 advmss:1448 cwnd:3004 ssthresh:3004 bytes_sent:174955661 bytes_acked:172985262 bytes_received:57229 segs_out:124507 segs_in:11775 data_segs_out:124495 data_segs_in:1514 send 1525.2Mbps pacing_rate 1830.2Mbps delivery_rate 746.7Mbps delivered:123097 app_limited busy:2432ms unacked:1399 rcv_rtt:23 rcv_space:14480 rcv_ssthresh:64088 minrtt:22.724

从系统cache中查看 tcp_metrics item

$sudo ip tcp_metrics show | grep  100.118.58.7
100.118.58.7 age 1457674.290sec tw_ts 3195267888/5752641sec ago rtt 1000us rttvar 1000us ssthresh 361 cwnd 40 metric_5 8710 metric_6 4258

每个连接的ssthresh默认是个无穷大的值,但是内核会cache对端ip上次的ssthresh(大部分时候两个ip之间的拥塞窗口大小不会变),这样大概率到达ssthresh之后就基本拥塞了,然后进入cwnd的慢增长阶段。

如果因为之前的网络状况等其它原因导致tcp_metrics缓存了一个非常小的ssthresh(这个值默应该非常大),ssthresh太小的话tcp的CWND指数增长阶段很快就结束,然后进入CWND+1的慢增加阶段导致整个速度感觉很慢

清除 tcp_metrics, sudo ip tcp_metrics flush all 
关闭 tcp_metrics 功能,net.ipv4.tcp_no_metrics_save = 1
sudo ip tcp_metrics delete 100.118.58.7

tcp_metrics会记录下之前已关闭TCP连接的状态,包括发送端CWND和ssthresh,如果之前网络有一段时间比较差或者丢包比较严重,就会导致TCP的ssthresh降低到一个很低的值,这个值在连接结束后会被tcp_metrics cache 住,在新连接建立时,即使网络状况已经恢复,依然会继承 tcp_metrics 中cache 的一个很低的ssthresh 值。

对于rt很高的网络环境,新连接经历短暂的“慢启动”后(ssthresh太小),随即进入缓慢的拥塞控制阶段(rt太高,CWND增长太慢),导致连接速度很难在短时间内上去。而后面的连接,需要很特殊的场景之下(比如,传输一个很大的文件)才能将ssthresh 再次推到一个比较高的值更新掉之前的缓存值,因此很有很能在接下来的很长一段时间,连接的速度都会处于一个很低的水平。

ssthresh 是如何降低的

在网络情况较差,并且出现连续dup ack情况下,ssthresh 会设置为 cwnd/2, cwnd 设置为当前值的一半,
如果网络持续比较差那么ssthresh 会持续降低到一个比较低的水平,并在此连接结束后被tcp_metrics 缓存下来。下次新建连接后会使用这些值,即使当前网络状况已经恢复,但是ssthresh 依然继承一个比较低的值。

ssthresh 降低后为何长时间不恢复正常

ssthresh 降低之后需要在检测到有丢包的之后才会变动,因此就需要机缘巧合才会增长到一个比较大的值。
此时需要有一个持续时间比较长的请求,在长时间进行拥塞避免之后在cwnd 加到一个比较大的值,而到一个比较
大的值之后需要有因dup ack 检测出来的丢包行为将 ssthresh 设置为 cwnd/2, 当这个连接结束后,一个
较大的ssthresh 值会被缓存下来,供下次新建连接使用。

也就是如果ssthresh 降低之后,需要传一个非常大的文件,并且网络状况超级好一直不丢包,这样能让CWND一直慢慢稳定增长,一直到CWND达到带宽的限制后出现丢包,这个时候CWND和ssthresh降到CWND的一半那么新的比较大的ssthresh值就能被缓存下来了。

tcp windows scale

网络传输速度:单位时间内(一个 RTT)发送量(再折算到每秒),不是 CWND(Congestion Window 拥塞窗口),而是 min(CWND, RWND)。除了数据发送端有个 CWND 以外,数据接收端还有个 RWND(Receive Window,接收窗口)。在带宽不是瓶颈的情况下,单连接上的速度极限为 MIN(cwnd, slide_windows)*1000ms/rt

1
2
#修改初始拥塞窗口
sudo ip route change default via ip dev eth0 proto dhcp src ip metric 100 initcwnd 20

tcp windows scale用来协商RWND的大小,它在tcp协议中占16个位,如果通讯双方有一方不支持tcp windows scale的话,TCP Windows size 最大只能到2^16 = 65535 也就是64k

如果网络rt是35ms,滑动窗口<CWND,那么单连接的传输速度最大是: 64K*1000/35=1792K(1.8M)

如果网络rt是30ms,滑动窗口>CWND的话,传输速度:CWND*1500(MTU)*1000(ms)/rt

一般通讯双方都是支持tcp windows scale的,但是如果连接中间通过了lvs,并且lvs打开了 synproxy功能的话,就会导致 tcp windows scale 无法起作用,那么传输速度就被滑动窗口限制死了(rt小的话会没那么明显)。

RTT越大,传输速度越慢

RTT大的话导致拥塞窗口爬升缓慢,慢启动过程持续越久。RTT越大、物理带宽越大、要传输的文件越大这个问题越明显
带宽B越大,RTT越大,低带宽利用率持续的时间就越久,文件传输的总时间就会越长,这是TCP慢启动的本质决定的,这是探测的代价。
TCP的拥塞窗口变化完全受ACK时间驱动(RTT),长肥管道对丢包更敏感,RTT越大越敏感,一旦有一个丢包就会将CWND减半进入避免拥塞阶段

RTT对性能的影响关键是RTT长了后丢包的概率大,一旦丢包进入拥塞阶段就很慢了。如果一直不丢包,只是RTT长,完全可以做大增加发送窗口和接收窗口来抵消RTT的增加

socket send/rcv buf

有些应用会默认设置 socketSendBuffer 为16K,在高rt的环境下,延时20ms,带宽100M,如果一个查询结果22M的话需要25秒

image.png

细化看下问题所在:

image.png

这个时候也就是buf中的16K数据全部发出去了,但是这16K不能立即释放出来填新的内容进去,因为tcp要保证可靠,万一中间丢包了呢。只有等到这16K中的某些ack了,才会填充一些进来然后继续发出去。由于这里rt基本是20ms,也就是16K发送完毕后,等了20ms才收到一些ack,这20ms应用、OS什么都不能做。

调整 socketSendBuffer 到256K,查询时间从25秒下降到了4秒多,但是比理论带宽所需要的时间略高

继续查看系统 net.core.wmem_max 参数默认最大是130K,所以即使我们代码中设置256K实际使用的也是130K,调大这个系统参数后整个网络传输时间大概2秒(跟100M带宽匹配了,scp传输22M数据也要2秒),整体查询时间2.8秒。测试用的mysql client短连接,如果代码中的是长连接的话会块300-400ms(消掉了慢启动阶段),这基本上是理论上最快速度了

image.png

$sudo sysctl -a | grep --color wmem
vm.lowmem_reserve_ratio = 256   256     32
net.core.wmem_max = 131071
net.core.wmem_default = 124928
net.ipv4.tcp_wmem = 4096        16384   4194304
net.ipv4.udp_wmem_min = 4096

如果指定了tcp_wmem,则net.core.wmem_default被tcp_wmem的覆盖。send Buffer在tcp_wmem的最小值和最大值之间自动调节。如果调用setsockopt()设置了socket选项SO_SNDBUF,将关闭发送端缓冲的自动调节机制,tcp_wmem将被忽略,SO_SNDBUF的最大值由net.core.wmem_max限制。

默认情况下Linux系统会自动调整这个buf(net.ipv4.tcp_wmem), 也就是不推荐程序中主动去设置SO_SNDBUF,除非明确知道设置的值是最优的。

这个buf调到1M有没有帮助,从理论计算BDP(带宽时延积) 0.02秒*(100MB/8)=250Kb 所以SO_SNDBUF为256Kb的时候基本能跑满带宽了,再大实际意义也不大了。

ip route | while read p; do sudo ip route change $p initcwnd 30 ; done

就是要你懂TCP相关文章:

关于TCP 半连接队列和全连接队列

MSS和MTU导致的悲剧

双11通过网络优化提升10倍性能

就是要你懂TCP的握手和挥手


总结

影响性能的几个点:

  • nagle,影响主要是针对响应时间;
  • tcp_metrics(缓存 ssthresh), 影响主要是传输大文件时速度上不去或者上升缓慢,明明带宽还有余;
  • tcp windows scale(lvs介在中间,不生效,导致接受窗口非常小), 影响主要是传输大文件时速度上不去,明明带宽还有余。

Nagle这个问题确实经典,非常隐晦一般不容易碰到,碰到一次决不放过她。文中所有client、server的概念都是相对的,client也有delay ack的问题。 Nagle算法一般默认开启的。

参考文章:

https://access.redhat.com/solutions/407743

http://www.stuartcheshire.org/papers/nagledelayedack/

https://en.wikipedia.org/wiki/Nagle%27s_algorithm

https://en.wikipedia.org/wiki/TCP_delayed_acknowledgment

https://www.kernel.org/doc/Documentation/networking/ip-sysctl.txt

https://www.atatech.org/articles/109721

https://www.atatech.org/articles/109967

https://www.atatech.org/articles/27189

https://www.atatech.org/articles/45084

高性能网络编程7–tcp连接的内存使用

1…78910
twitter @plantegg

twitter @plantegg

191 日志
18 分类
282 标签
RSS
© 2025 twitter @plantegg
由 Hexo 强力驱动
主题 - NexT.Mist
本站总访问量次 本站访客数人次
访问人数 人 次