就是要你懂TCP队列--通过实战案例来展示问题

就是要你懂TCP队列–通过实战案例来展示问题

详细理论和实践部分可以看这篇

再写这篇原因是,即使我在上篇文章里将这个问题阐述的相当清晰,但是当我再次碰到这个问题居然还是费了一些周折,所以想再次总结下。

问题描述

使用其他团队的WEBShell 调试问题的时候非常卡,最开始怀疑是定时任务导致压力大,然后重启Server端的Tomcat就恢复了,当时该应用的开发同学看到机器磁盘、cpu、内存、gc等都正常,实在不知道为什么会这么卡

分析问题

因为每天都是上午出现问题,拿到权限后,也跟着先检查一遍定时任务,没发现什么异常。

既然在客户端表现出来卡顿,那么tsar先看看网络吧,果然大致是卡顿的时候网络重传率有点高,不过整个问题不是一直出现,只是间歇性的。

抓包、netstat -s 看重传、reset等都还好、ss -lnt 看也没有溢出,我看了很多次当前队列都是0

重启Tomcat

问题恢复,所以基本觉得问题还是跟Tomcat非常相关,抓包看到的重传率非常低(不到0.01%—被这里坑了一下),因为中间链路还有nginx等,一度怀疑是不是抓包没抓到本地回环网卡导致的,要不不会tsar看到的重传率高,而tcpdump抓下来的非常低。

重启后 jstack 看看tomcat状态,同时跟正常的server对比了一下,发现明显有一个线程不太对,一直在增加

image.png

所以到这里大概知道问题的原因了,只是还不能完全确认。

应该是Tomcat里面的线程越来越多导致Tomcat越来越慢,这个慢不是表现在gc、cpu等上,所以开发同学发现卡顿上去也没看出端倪来。

那么对于网络很熟悉的同学,上去看到网络重传很高也没找到原因有点不太应该,主要是问题出现的时候间歇性非常低,通过ss -lnt去看溢出队列和netstat -s |grep -i listen 的时候基本都没什么问题,就忽视了,再说在tcpdump抓包只看到很少的几个重传,反倒是几百个reset包干扰了问题(几百个reset肯定不对,但是没有影响我所说的应用)。

调整参数,加速问题重现

因为总是每天上午一卡顿、有人抱怨、然后重启恢复,第二天仍是这个循环,也就是问题轻微出现后就通过重启解决了

故意将全连接队列从当前的128改成16,重启后运行正常,实际并发不是很高的时候16也够了,改成16是为了让问题出现的时候如果是全连接队列不够导致的,那么会影响更明显一些,经过一天的运行后,可以清晰地观察到:

image.png

tsar的重传率稳定的很高,ss -lnt也能明显地看到全连接队列完全满了,这个满不是因为压力大了,压力一直还是差不多的,所以只能理解是Tomcat处理非常慢了,同时netstat -s 看到 overflowed也稳定增加

这个时候客户端不只是卡顿了,是完全连不上。

Tomcat jstack也能看到这几个线程创建了2万多个:

image.png

抓包(第二次抓包的机会,所以这次抓了所有网卡而不只是eth0)看到 Tomcat的8080端口上基本是这样的:

image.png

而看所有网卡的所有重传的话,这次终于可以看到重传率和tsar看到的一致,同时也清晰的看到主要127.0.0.1的本地流量,也就是Nginx过来的,而之前的抓包只抓了eth0,只能零星看到几个eth0上的重传包,跟tsar对不上,也导致问题跑偏了(重点去关注reset了)

image.png

或者这个异常状态的截图

image-20230315143053615

一些疑问

为什么之前抓包看不到这些重传

因为对业务部署的不了解只抓了eth0, 导致没抓到真正跟客户端表现出来的卡顿相关的重传。比如这是只抓eth0上的包,看到的重传:

image.png

可以看到明显非常少,这完全不是问题。

为什么 ss -lnt / netstat -s 都没发现问题

当时抱怨的时候都是间歇性的,所以 ss -lnt看了10多次都是当前连接0, netstat -s 倒是比较疏忽没仔细比较

为什么线程暴涨没有监控到

边缘业务,本身就是监控管理其它服务的,本身监控不健全。

网络重传和业务的关系

一般我们通过tsar等看到的是整个机器的重传率,而实际影响我们业务的(比如这里的8080端口)只是我这个端口上的重传率,有时候tsar看到重传率很高,那可能是因为机器上其他无关应用拉高的,所以这里需要一个查看具体业务(或者说具体端口上的重传率的工具)

如何快速定位网络重传发生的端口

bcc、bpftrace或者systemtap等工具都提供了观察网络重传包发生的时候的网络四元组以及发生重传的阶段(握手、建立连接后……),这样对我们定位问题就很容易了

image.png

image.png

image.png

总结

问题的根本原因不是因为TCP连接队列不够,而是 Tomcat中线程泄露,导致Tomcat反应越来越慢,进而导致TCP连接队列溢出,然后网络重传率升高,最终导致了client端操作卡顿。

这种问题最快的是 jstack 发现,但是因为这只是一个后台Manager,所以基本没有监控,当时也漏看了jstack,所以导致问题定位花的时间长一些。当然通过tcpdump(漏抓了 lo 网卡,主要重传都是本地nginx和本地tomcat的,所以没有发现问题),通过 ss -lnt 和 netstat -s 本来也应该可以发现的,但是因为干扰因素太多而导致也没有发现,这个时候tcp_retrans等工具可以帮我们看的更清楚。

当然从发现连接队列不够到Tomcat处理太慢这个是紧密联系的,一般应用重启的时候也会短暂连接队列不够,那是因为重启的时候Tomcat前累积了太多连接,这个时候Tomcat重启中,需要热身,本身处理也慢,所以短暂会出现连接队列不够,等Tomcat启动几分钟后就正常了。

NIO和epoll

NIO、EPOLL和协程

从IO说起

用户线程发起IO操作后(比如读),网络数据读取过程分两步:

  • 用户线程等待内核将数据从网卡拷贝到内核空间
  • 内核将数据从内核空间拷贝到用户空间

同步阻塞IO

用户线程发起read后让出CPU一直阻塞直到内核把网卡数据读到内核空间,然后再拷贝到用户空间,然后唤醒用户线程

同步非阻塞IO

用户线程发起read后,不阻塞,反复尝试读取,直到内核把网卡数据读到内核空间,用户线程继续read,这时进入阻塞直到数据拷贝到用户空间

undefined

阻塞和非阻塞指的是发起IO操作后是等待还是返回,同步和异步指的是应用程序与内核通信时数据从内核空间拷贝到用户空间的操作是内核主动触发(异步)还是应用程序触发(同步)

IO多路复用、Epoll

一个进程虽然任一时刻只能处理一个请求,但是处理每个请求的事件时,耗时控制在 1 毫秒以内,这样 1 秒内就可以处理上千个请求,把时间拉长来看,多个请求复用了一个进程,这就是多路复用,这种思想很类似一个 CPU 并发多个进程,所以也叫做时分多路复用。

epoll作用:进程内同时刻找到缓冲区或者连接状态变化的所有TCP连接,主要是基于同一时刻活跃连接只在总连接中占一小部分

image.png

image.png

用户线程读取分成两步,用户线程先发起select调用(确认内核是否准备好数据),准备好后才调用read,将数据从内核空间读取到用户空间(read这里还是阻塞)。主要是一个select线程可以向内核查多个数据通道的状态

undefined

IO多路复用和同步阻塞、非阻塞的区别主要是用户线程发起read的时机不一样,IO多路复用是等数据在内核空间准备好了再通过同步read去读取;而阻塞和非阻塞因为没法预先知道数据是否在内核空间准备好,所以早早触发了read然后等待,只是阻塞会一直等,而非阻塞是指触发read后不用等,反复read直到read到数据。

Tomcat中的NIO指的是同步非阻塞,但是触发时机又是通过Java中的Selector,可以理解成通过Selector跳过了前面的阻塞和非阻塞,实际用户线程在数据Ready前没有触发read操作,数据到了才出发read操作。

阻塞IO和NIO的主要区别是:NIO面对的是Buffer,可以做到读取完毕后再一次性处理;而阻塞IO面对的是流,只能边读取边处理

多路复用 API 返回的事件并不一定可读写的,如果使用阻塞 I/O, 那么在调用 read/write 时则会发生程序阻塞,因此最好搭配非阻塞 I/O,以便应对极少数的特殊情况

epoll JStack 堆栈

像Redis采取的是一个进程绑定一个core,然后处理所有连接的所有事件,因为redis主要是内存操作,速度比较快,这样做避免了加锁,权衡下来更有利(实践上为了利用多核会部署多个redis实例;另外新版本的redis也开始支持多线程了)。但是对大多服务器就不可取了,毕竟单核处理能力是瓶颈,另外就是IO速度和CPU速度的差异非常大,所以不能采取Redis的设计。

Nginx采取的是多个Worker通过reuseport来监听同一个端口,一个Worker对应一个Epoll红黑树,上面挂着所有这个Worker负责处理的连接。默认多个worker是由OS来调度,可以通过 worker_cpu_affinity 来指定某个worker绑定到哪个core。

eg: 启动4个worker,分别绑定到CPU0~CPU3上

1
2
worker_processes    4;
worker_cpu_affinity 0001 0010 0100 1000;

or
启动2个worker;worker 1 绑定到CPU0/CPU2上;worker 2 绑定到CPU1/CPU3上

1
2
worker_processes    2;
worker_cpu_affinity 0101 1010;

or 自动绑定(推荐方式)

1
2
3
4
5
worker_processes auto;
worker_cpu_affinity auto; //自动绑核

或者 限制CPU资源的使用,只将nginx worker绑定到特定的一些cpu核心上:
worker_cpu_affinity auto 01010101;

分析worker和core的绑定关系(psr–当前进程跑在哪个core上,没有绑定就会飘来飘去,没有意义)

1
ps -eo pid,ni,pri,pcpu,psr,comm|grep nginx|awk '{++s[$(NF-1)]}END{for (i in s)print "core-id",i,"\t",s[i]}'|sort -nr -k 3

而Tomcat等服务器会专门有一个(或多个)线程处理新连接IO(Accept),然后老的连接全部交给一个线程池(Reactor)来处理,这个线程池的线程数量可以根据机器CPU core数量来调整

Image

完整的NIO中Acceptor逻辑JStack:

//3306 acceptor端口
"HTTPServer" #32 prio=5 os_prio=0 tid=0x00007fb76cde6000 nid=0x4620 runnable [0x00007fb6db5f6000]
   java.lang.Thread.State: RUNNABLE
        at sun.nio.ch.EPollArrayWrapper.epollWait(Native Method)
        at sun.nio.ch.EPollArrayWrapper.poll(EPollArrayWrapper.java:275)
        at sun.nio.ch.EPollSelectorImpl.doSelect(EPollSelectorImpl.java:93)
        at sun.nio.ch.SelectorImpl.lockAndDoSelect(SelectorImpl.java:86)
        - locked <0x000000070007fde0> (a sun.nio.ch.Util$3)
        - locked <0x000000070007fdc8> (a java.util.Collections$UnmodifiableSet)
        - locked <0x000000070002cbc8> (a sun.nio.ch.EPollSelectorImpl)
        at sun.nio.ch.SelectorImpl.select(SelectorImpl.java:97)
        at com.alibaba.cobar.net.NIOAcceptor.run(NIOAcceptor.java:63)

   Locked ownable synchronizers:
        - None

Acceptor Select Java源代码

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
 33     public NIOAcceptor(String name, int port, FrontendConnectionFactory factory, boolean online) throws IOException{
34 super.setName(name);
35 this.port = port;
36 this.factory = factory;
37 if (online) {
38 this.selector = Selector.open();
39 this.serverChannel = ServerSocketChannel.open();
40 this.serverChannel.socket().bind(new InetSocketAddress(port), 65535);
41 this.serverChannel.configureBlocking(false);
42 this.serverChannel.register(selector, SelectionKey.OP_ACCEPT);
43 }
44 }
53
54 public void setProcessors(NIOProcessor[] processors) {
55 this.processors = processors;
56 }
57
58 @Override
59 public void run() {
60 for (;;) {
61 ++acceptCount;
62 try {
63 selector.select(1000L);
64 Set<SelectionKey> keys = selector.selectedKeys();
65 try {
66 for (SelectionKey key : keys) {
67 if (key.isValid() && key.isAcceptable()) {
68 accept();
69 } else {
70 key.cancel();
71 }
72 }
73 } finally {
74 keys.clear();
75 }
76 } catch (Throwable e) {
77
91 }
92 }
93 }
94
95 private void accept() {
96 SocketChannel channel = null;
97 try {
98 channel = serverChannel.accept();
99 channel.setOption(StandardSocketOptions.TCP_NODELAY, true);
100 channel.configureBlocking(false);
101 FrontendConnection c = factory.make(channel);
102 c.setAccepted(true);
103
104 NIOProcessor processor = nextProcessor();
105 c.setProcessor(processor);
106 processor.postRegister(c);
107 } catch (Throwable e) {
108 closeChannel(channel);
109 logger.info(getName(), e);
110 }
111 }

synchronized public void online() {
if (this.serverChannel != null && this.serverChannel.isOpen()) {
return;
}

try {
this.selector = Selector.open();
this.serverChannel = ServerSocketChannel.open();
this.serverChannel.socket().bind(new InetSocketAddress(port));
this.serverChannel.configureBlocking(false);
//NIOAccept 只处理accept事件
this.serverChannel.register(selector, SelectionKey.OP_ACCEPT);
statusLogger.info(this.getName() + " is started and listening on " + this.getPort());
} catch (IOException e) {
logger.error(this.getName() + " online error", e);
throw GeneralUtil.nestedException(e);
}
}

创建server(Listen端口)就是创建一个NIOAcceptor,监听在特定端口上,NIOAcceptor有多个(一般和core一致) NIOProcessor 线程,一个NIOProcessor 中还可以有一个 NIOReactor

NIOAcceptor(一般只有一个,可以有多个)是一个Thread,只负责处理新建连接(建立新连接会设置这个Socket的Options,比如buffer size、keepalived等),将新建连接绑定到一个NIOProcessor(NIOProcessor数量一般和CPU Core数量一致,一个NIOProcessor对应一个NIOReactor),连接上的收发包由NIOReactor来处理。也就是一个连接(Socket)创建后就绑定到了一个固定的 NIOReactor来处理,每个NIOReactor 有一个 R线程和一个 W线程(写不走epoll的话用这个W线程按queue写出)。这个 R线程一直阻塞在selector,等待新连接或者读写事件的到来。

新连接进来后NIOAcceptor.select 阻塞解除,执行accept逻辑,accept返回一个channel(对socket封装),设置channel TCP options,将这个channel和一个 NIOProcessor绑定(一个NIOProcessor可以绑定多个channel,反之一个channel只能绑定到一个NIOProcessor),同时将这个channel插入(offer)到NIOProcessor里面的NIOReactor的队列中,并唤醒NIOReactor的selector,将新连接注册到 NIOReactor的selector中(进行连接的mysql协议认证)。然后阻塞在这个selector等待事件中,等待读写事件的到来

也就是只有Acceptor阶段会有惊群(但是上面的代码只有一个Acceptor,所以也没有惊群了),收发数据阶段因为Socket已经绑定到了一个固定的Thread,所以不会有惊群了,但是可能会存在某个Thread有慢处理导致新进来的请求长时间得不到响应。

Select 触发 read/write 堆栈:

"Processor2-R" #26 prio=5 os_prio=0 tid=0x00007fb76cc9a000 nid=0x4611 runnable [0x00007fb6dbdfc000]
   java.lang.Thread.State: RUNNABLE
        at sun.nio.ch.EPollArrayWrapper.epollWait(Native Method)
        at sun.nio.ch.EPollArrayWrapper.poll(EPollArrayWrapper.java:275)
        at sun.nio.ch.EPollSelectorImpl.doSelect(EPollSelectorImpl.java:93)
        at sun.nio.ch.SelectorImpl.lockAndDoSelect(SelectorImpl.java:86)
        - locked <0x000000070006e090> (a sun.nio.ch.Util$3)
        - locked <0x000000070006cd68> (a java.util.Collections$UnmodifiableSet)
        - locked <0x00000007000509e0> (a sun.nio.ch.EPollSelectorImpl)
        at sun.nio.ch.SelectorImpl.select(SelectorImpl.java:97)
        at com.alibaba.cobar.net.NIOReactor$R.run(NIOReactor.java:88)
        at java.lang.Thread.run(Thread.java:852)

NIOReactor.java:

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
 82         @Override
83 public void run() {
84 final Selector selector = this.selector;
85 for (;;) {
86 ++reactCount;
87 try {
88 selector.select(1000L);
89 register(selector);
90 Set<SelectionKey> keys = selector.selectedKeys();
91 try {
92 for (SelectionKey key : keys) {
93 Object att = key.attachment();
94 if (att != null && key.isValid()) {
95 int readyOps = key.readyOps();
96 if ((readyOps & SelectionKey.OP_READ) != 0) {
97 read((NIOConnection) att); //读
98 } else if ((readyOps & SelectionKey.OP_WRITE) != 0) {
99 write((NIOConnection) att); //写
100 } else {
101 key.cancel();
102 }
103 } else {
104 key.cancel();
105 }
106 }
107 } finally {
108 keys.clear();
109 }
110 } catch (Throwable e) {
111 logger.warn(name, e);
112 }
113 }
114 }

Socket是一个阻塞的IO,一个Socket需要一个Thread来读写;SocketChannel对Socket进行封装,是一个NIO的Socket超集,一个Select线程就能处理所有的SocketChannel(也就是所有的Socket)

Java的Netty框架和 Corba的NIOProcessor 就是基于java的NIO库,用的(多)selector形式

NIO 多路复用Java example

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
128
129
130
131
132
133
134
135
136
137
public class SelectorNIO {
/**
* linux 多路复用器 默认使用epoll,可通过启动参数指定使用select poll或者epoll ,
*/
private Selector selector = null;
int port = 3306;

public static void main(String[] args) {
SelectorNIO service = new SelectorNIO();
service.start();
}

public void initServer() {
try {
ServerSocketChannel server = ServerSocketChannel.open();
server.configureBlocking(false);
server.bind(new InetSocketAddress(port));

//epoll模式下 open会调用一个调用系统调用 epoll_create 返回文件描述符 fd3
selector = Selector.open();

/**
*对应系统调用
*select,poll模式下:jvm里开辟一个文件描述符数组,并吧 fd4 放入
*epoll模式下: 调用内核 epoll_ctl(fd3,ADD,fd4,EPOLLIN)
*/
server.register(selector, SelectionKey.OP_ACCEPT);
} catch (IOException e) {
e.printStackTrace();
}
}

public void start() {
initServer();
System.out.println("server start");
while (true) {
try {
Set<SelectionKey> keys = selector.keys();
System.out.println("可处理事件数量 " + keys.size());
/**
*对应系统调用
*1,select,poll模式下: 调用 内核 select(fd4) poll(fd4)
*2,epoll: 调用内核 epoll_wait()
*/
while (selector.select() > 0) {
//返回的待处理的文件描述符集合
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
//使用后需移除,否则会被一直处理
iterator.remove();
if (key.isAcceptable()) {
/**
* 对应系统调用
* select,poll模式下:因为内核未开辟空间,那么在jvm中存放fd4的数组空间
* epoll模式下: 通过epoll_ctl把新客户端fd注册到内核空间
*/
acceptHandler(key);
} else if (key.isReadable()) {
/**
* 处理读事件
*/
readHandler(key);
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

public void acceptHandler(SelectionKey key) {
try {
ServerSocketChannel ssc = (ServerSocketChannel)key.channel();
//接受新客户端
SocketChannel client = ssc.accept();
//重点,设置非阻塞
client.configureBlocking(false);
ByteBuffer buffer = ByteBuffer.allocate(1024);

/**
* 调用系统调用
* select,poll模式下:jvm里开辟一个数组存入 fd7
* epoll模式下: 调用 epoll_ctl(fd3,ADD,fd7,EPOLLIN
*/
client.register(selector, SelectionKey.OP_READ, buffer);
System.out.println("client connected:" + client.getRemoteAddress());
} catch (IOException e) {
e.printStackTrace();
}
}

public void readHandler(SelectionKey key) {
try {
// 可读事件,读取数据并向客户端发送响应
SocketChannel socketChannel = (SocketChannel)key.channel();
/**
* 简单介绍下Buffer
* Buffer本质上是一个内存块,要弄懂它首先要了解以下三个属性
* capacity(容量)、position(读写位置)和limit(读写的限制)。两种模式,读模式和写模式,
* capacity在读写模式下不变,但position和limit在读写模式下值是会变的
* 举个例子,
* 1.创建一个capacity为1024的Buffer,刚开始position=0,limit=capacity=1024
* 2.往Buffer写数据,每写一个数据,position指针向后移动一个位置,其值加一,limit则减1。比如
* 写入24个字节后,position=24(已经写入24),limit=1000(还可写入1024)
* 3.假设我们已经写完了,那我从哪里读?读多少呢?所以Buffer提供了一个读写模式翻转的方法flip方法
* 把写模式转换成读模式,底层就是把position和limit的值改成从哪里读,读多少,所以调用该方法后,我们
* 就能得到position=0 从0位置开始读,limit=24读24个位置
*/
ByteBuffer buffer = ByteBuffer.allocate(1024);
int len = socketChannel.read(buffer);
System.out.println("readHandler len" + len);
if (len > 0) {
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
String message = new String(bytes, "UTF-8");
System.out.println("Received message from " + socketChannel.getRemoteAddress() + ": " + message);
// 向客户端发送响应
String response = "Hello, client!";
ByteBuffer responseBuffer = ByteBuffer.wrap(response.getBytes("UTF-8"));
socketChannel.write(responseBuffer);
}
//If the peer closes the socket:
//read() returns -1; readLine() returns null; readXXX() throws EOFException, for any other X.
//As InputStream only has read() methods, it only returns -1: it doesn't throw an IOException at EOS.
if(len==-1){
socketChannel.close();
throw new EOFException("read eof exception");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

Channel

Channel 类位于 java.nio.channels 包中,但并不是 Channel 仅仅支持 NIO,其分为两种类型:

  • FileChannel:完全不支持 NIO;
  • SocketChannel/ServerSocketChannel 等 Channel 默认情况下并不支持 NIO,只有显式地调用配置方法才能够进入非阻塞模式(ServerSocketChannel.configBlocking(false))。

下面主要以 SocketChannel 的角度来介绍 Channel 类。

Channel 我们可以理解为对应于 BIO 中的 Socket,也可以理解为 Scoket.inputStream/SocketOutputStream。如果认为是流,那么我们做一个比较:

  • 传统 Socket:我们调用 Socket 的 getInputStream() 以及 getOutputStream() 进行数据的读和写。
  • Channel:我们不再需要得到输入输出流进行读和写,而是通过 Channel 的 read() 以及 write() 方法进行读和写。

Channel 如此实现也付出了代价(如下图所示):

  • 读写模式需要调用 flip() 方法进行切换,读模式下调用 write() 试图进行写操作会报错。
  • 读写不再能够接受一个简单的字节数组,而是必须是封装了字节数组的 Buffer 类型。

image-20200516195346349

目前已知 Channel 的实现类有:

  • FileChannel 一个用来写、读、映射和操作文件的通道。

  • DatagramChannel

  • SocketChannel

    SocketChannel 可以看做是具有非阻塞模式的 Socket。其可以运行在阻塞模式,也可以运行在非阻塞模式。其只能依靠 ByteBuffer 进行读写,而且是尽力读写,尽力的含义是:

    • ByteBuffer 满了就不能再读了;
    • 即使此次 Socket 流没有传输完毕,但是一旦 Channel 中的数据读完了,那么就返回了,这就是非阻塞读。所以读的方法有 -1(EOF),0(Channel 中的数据读完了,但是整个数据流本身没有消耗完),其他整数,此次读的数据(因为 ByteBuffer 并不是每次都是空的,原来就有数据时只能够尽力装满)。
  • ServerSocketChannel 这个类似于 ServerSocket 起到的作用。

一个比喻比较他们的不同

打个不是极其恰当的比方:假如你去餐馆吃饭,厨师(内核)给你准备饭菜(数据)

  • 阻塞IO:老板,饭好了吗?于是你傻傻在窗口等着。等着厨师把饭做好给你。干等着,不能玩手机。
  • 非阻塞IO:老板,饭好了吗?没好?那我玩手机。哈哈,刷个微博。十分钟过去了,你又去问,饭好了吗?还没好,那我再斗个地主吧。过了一会儿,你又去问。。。。等饭的过程中可以玩手机,不过你得时不时去问一下好了没。
  • IO多路复用:你们一帮人一口气点了十几个菜,其他人坐着该做啥做啥,派一个人等着厨房的通知。。。问厨师,这么多个菜,有哪几个菜好了呢?厨师告诉你A、C、E好了,你可以取了;又过了一会儿,你去问厨师,有哪些菜好了呢?厨师告诉你D、F好了,可以取了。。。
  • 异步IO:老板,饭好了麻烦通知我一下。我去看电视,不用再去问饭好了没有了,饭好厨师会给你的。等饭的过程中当然可以玩手机。完全托管的机制。
  • 同步:端菜上桌过程必须是阻塞,异步相当于厨师将菜送到桌子上后通知你吃

Tomcat中的NIO+多路复用的实现

NIOEndpoint组件实现了NIO和IO多路复用,IO多路复用指的是Poller通过Selector处理多个Socket(SocketChannel)

undefined

  • LimitLatch 是连接控制器,负责控制最大连接数,NIO模式下默认是10000,达到阈值后新连接被拒绝
  • Acceptor 跑在一个单独的线程里,一旦有新连接进来accept方法返回一个SocketChannel对象,接着把SocketChannel对象封装在一个PollerEvent对象中,并将PollerEvent对象压入Poller的Queue里交给Poller处理。 Acceptor和Poller之间是典型的生产者-消费者模式
  • Poller的本质是一个Selector,内部维护一个Channel数组,通过一个死循环不断地检测Channel中的数据是否就绪,一旦就绪就生成一个 SocketProcessor任务对象扔给 Executor处理。同时Poller还会循环遍历自己所管理的SocketChannel是否已经超时,如果超时就关闭这个SocketChannel
  • Executor是线程池,主要处理具体业务逻辑,Poller主要处理读取Socket数据逻辑。Executor主要负责执行 SocketProcessor对象中的run方法,SocketProcessor对象的run方法用 Http11Processor来读取和解析请求数据。
  • Http11Processor是应用层协议的封装,他会调用容器获得请求(ServletRequest),再将响应通过Channel写出给请求

因为Tomcat支持同步非阻塞IO模型和异步IO模型,所以Http11Processor不是直接读取Channel。针对不同的IO模型在Java API中对Channel有不同的实现,比如:AsynchronousSocketChannel 和 SocketChannel,为了对 Http11Processor屏蔽这些差异,Tomcat设计了一个包装类SocketWrapper,Http11Processor只需要调用SocketWrapper的读写方法

Tomcat核心参数

  • acceptorThreadCount: Acceptor线程数量,多核情况下充分利用多核来应对大量连接的创建,默认值是1
  • acceptCount: TCP全连接队列大小,默认值是100,这个值是交给内核,由内核来维护这个队列的大小,满了后Tomcat无感知
  • maxConnections: NIO模式默认10000,最大同时处理的连接数量。如果是BIO,一个connections需要一个thread来处理,不应设置太大。
  • maxThreads: 专门处理IO操作的Worker线程数量,默认值是200

Acceptor

Acceptor实现了Runnable接口,可以跑在单线程里,一个Listen Port只能对应一个ServerSocketChannel,因此这个ServerSocketChannel是在多个Acceptor线程之间共享

serverSock = ServerSocketChannel.open();
serverSock.socket().bind(addr,getAcceptCount());
serverSock.configureBlocking(true);
  • bind方法的第二个参数是操作系统的等待队列长度,也就是TCP的全连接队列长度,对应着Tomcat的 acceptCount 参数配置,默认是100
  • ServerSocketChannel被设置成阻塞模式,也就是连接创建的时候是阻塞的方式。

多路复用–多个socket共用同一个线程来读取socket中的数据

多路复用可以是对accept,也可以是read,一般而言对于accept一个listen port就是一个线程,但是对于read,如果是高并发情况下,一个线程来读取N多socket肯定性能不够好,同时也没用利用上物理上的多核,所以一般是core+1或者2*core数量的线程来读取N多socket,因为有些read还做一些其它逻辑所以会设置的比core数量略微多些。

正常一个连接一个线程(tomcat的BIO模型),导致的问题连接过多的话线程也过多,而大部分连接都是空闲的。如果活跃连接数比较多的话,导致CPU主要用在了线程调度、切换以及过高的内存消耗上(C10K)。而对于NIO即使活跃连接数非常多,但是实际处理他们的线程也就几个(一般设置跟core数差不多),所以也不会有太高的上下文切换(参考后面阐述的协程的原理)。

Select和epoll本质是为了IO多路复用(多个连接共用一个线程–监听是否连接有数据到达)。有报文进来的时候触发Select,Select轮询所有连接确认是哪个连接有报文进来了。连接过多放大了这种无用轮询。
epoll通过一颗红黑树维护所有连接,同时将有数据进来的连接通过回调更新到一个队列中,那么epoll每次检查的时候只需要检查队列而不是整个红黑树,效率大大提高了。

事件驱动适合于I/O密集型服务,多进程或线程适合于CPU密集型服务
多路复用有很多种实现,在linux上,2.4内核前主要是select和poll,现在主流是epoll
select解决了一个线程监听多个socket的问题,但是因为依靠fd_set结构体记录所监控的socket,带来了能监听的socket数量有限(不超过1024)
poll在select的基础上解决了1024个的问题,但是还是要依次轮询这1024个socket,效率太低
epoll 异步非阻塞多路复用

闲置线程或进程不会导致系统上下文切换过高(但是每个线程都会消耗内存)。只有ready状态过多时上下文切换才不堪重负。对于CPU连说调度10M的线程、进程不现实,这个时候适合用协程

image.png

netty自带telnet server的example中,一个boss epoll负责listen新连接,新连接分配给多个worker epoll(worker则使用默认的CPU数*2.),每个连接之后的读写都由固定的一个worker来处理

以上netty结构中:

  • BOSS负责accept连接(通过BOSS监听的channel的read事件),然后实例化新连接的channel

  • 该channel绑定到worker线程组下的某个eventloop上,后续所有该channel的事件、任务 均有该eventloop执行。这是单个channel无锁的关键

  • BOSS 提交Channel.regist任务到worker线程组,之后BOSS任务结束,转入继续listen

协程

协程是一种轻量级的,用户态的执行单元。相比线程,它占用的内存非常少,在很多实现中(比如 Go 语言)甚至可以做到按需分配栈空间。

它主要有三个特点:

  • 占用的资源更少 ;
  • 所有的切换和调度都发生在用户态;
  • 它的调度是协商式的,而不是抢占式的。

协程的全部精神就在于控制流的主动让出和恢复,工程实现得考虑如何让协程的让出与恢复高效。一般在协程中调用 yield_to 来主动把执行权从本协程让给另外一个协程。yield_to 机器码:

1
2
3
4
5
6
7
8
9
10
11
12
000000000040076d <_Z8yield_toP9coroutineS0_>:
40076d: 55 push %rbp
40076e: 48 89 e5 mov %rsp,%rbp
400771: 48 89 7d f8 mov %rdi,-0x8(%rbp)
400775: 48 89 75 f0 mov %rsi,-0x10(%rbp)
400779: 48 8b 45 f0 mov -0x10(%rbp),%rax
40077d: 48 8b 00 mov (%rax),%rax
400780: 48 8b 55 f8 mov -0x8(%rbp),%rdx
400784: 48 89 22 mov %rsp,(%rdx)
400787: 48 89 c4 mov %rax,%rsp
40078a: 5d pop %rbp
40078b: c3 retq

yield_to 中,参数 old_co 指向老协程,co 则指向新的协程,也就是我们要切换过去执行的目标协程。

这段代码的作用是,首先,把当前 rsp 寄存器的值存储到 old_co 的 stack_pointer 属性(第 9 行),并且把新的协程的 stack_pointer 属性更新到 rsp 寄存器(第 10 行),然后,retq 指令将会从栈上取出调用者的地址,并跳转回调用者继续执行(第 12 行)。

当调用这一次 yield_to 时,rsp 寄存器刚好就会指向新的协程 co 的栈,接着就会执行”pop rbp”和”retq”这两条指令。这里你需要注意一下,栈的切换,并没有改变指令的执行顺序,因为栈指针存储在 rsp 寄存器中,当前执行到的指令存储在 IP 寄存器中,rsp 的切换并不会导致 IP 寄存器发生变化。

这个协程切换过程并没有使用任何操作系统的系统调用,就实现了控制流的转移。也就是说,在同一个线程中,我们真正实现了两个执行单元。这两个执行单元并不像线程那样是抢占式地运行,而是相互主动协作式执行,所以,这样的执行单元就是协程。我们可以看到,协程的切换全靠本执行单元主动调用 yield_to 来把执行权让渡给其他协程。

每个协程都拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方(上述例子中,保存在 coroutine 对象中),在切回来的时候,恢复先前保存的寄存器上下文和栈。

多进程和多线程优劣的比较

把进程看做是资源分配的单位,把线程才看成一个具体的执行实体。

进程间内存难以共享,多线程可以共享内存;多进程内核管理成本高。

每个线程消耗内存过多, 比如,64 位的 Linux 为每个线程的栈分配了 8MB 的内存,还预分配了 64MB 的内存作为堆内存池;切换请求是内核通过切换线程实现的,什么时候会切换线程呢?不只时间片用尽,当调用阻塞方法时,内核为了让 CPU 充分工作,也会切换到其他线程执行。一次上下文切换的成本在几十纳秒到几微秒间,当线程繁忙且数量众多时,这些切换会消耗绝大部分的 CPU 运算能力。

协程把内核态的切换工作交由用户态来完成.

目前主流语言基本上都选择了多线程作为并发设施,与线程相关的概念是抢占式多任务(Preemptive multitasking),而与协程相关的是协作式多任务。不管是进程还是线程,每次阻塞、切换都需要陷入系统调用 (system call),先让 CPU 执行操作系统的调度程序,然后再由调度程序决定该哪一个进程 (线程) 继续执行。

由于抢占式调度执行顺序无法确定,我们使用线程时需要非常小心地处理同步问题,而协程完全不存在这个问题。因为协作式的任务调度,是要用户自己来负责任务的让出的。如果一个任务不主动让出,其他任务就不会得到调度。这是协程的一个弱点,但是如果使用得当,这其实是一个可以变得很强大的优点。

同步、异步、协程的比较

同步调用

切换请求是内核通过切换线程实现的,什么时候会切换线程呢?不只时间片用尽,当调用阻塞方法时,内核为了让 CPU 充分工作,也会切换到其他线程执行。一次上下文切换的成本在几十纳秒到几微秒间,当线程繁忙且数量众多时,这些切换会消耗绝大部分的 CPU 运算能力。

image.png

改成异步化后:

把上图中本来由内核实现的请求切换工作,交由用户态的代码来完成就可以了,异步化编程通过应用层代码实现了请求切换,降低了切换成本和内存占用空间。异步化依赖于 IO 多路复用机制,比如 Linux 的 epoll 或者 Windows 上的 iocp,同时,必须把阻塞方法更改为非阻塞方法,才能避免内核切换带来的巨大消耗。Nginx、Redis 等高性能服务都依赖异步化实现了百万量级的并发。

image.png

然而,写异步化代码很容易出错。因为所有阻塞函数,都需要通过非阻塞的系统调用拆分成两个函数。虽然这两个函数共同完成一个功能,但调用方式却不同。第一个函数由你显式调用,第二个函数则由多路复用机制调用。这种方式违反了软件工程的内聚性原则,函数间同步数据也更复杂。特别是条件分支众多、涉及大量系统调用时,异步化的改造工作会非常困难。

用协程来实现

协程与异步编程相似的地方在于,它们必须使用非阻塞的系统调用与内核交互,把切换请求的权力牢牢掌握在用户态的代码中。但不同的地方在于,协程把异步化中的两段函数,封装为一个阻塞的协程函数。这个函数执行时,会使调用它的协程无感知地放弃执行权,由协程框架切换到其他就绪的协程继续执行。当这个函数的结果满足后,协程框架再选择合适的时机,切换回它所在的协程继续执行。

img

实际上,用户态的代码切换协程,与内核切换线程的原理是一样的。内核通过管理 CPU 的寄存器来切换线程,我们以最重要的栈寄存器和指令寄存器为例,看看协程切换时如何切换程序指令与内存。

每个线程有独立的栈,而栈既保留了变量的值,也保留了函数的调用关系、参数和返回值,CPU 中的栈寄存器 SP 指向了当前线程的栈,而指令寄存器 IP 保存着下一条要执行的指令地址。因此,从线程 1 切换到线程 2 时,首先要把 SP、IP 寄存器的值为线程 1 保存下来,再从内存中找出线程 2 上一次切换前保存好的寄存器值,写入 CPU 的寄存器,这样就完成了线程切换。(其他寄存器也需要管理、替换,原理与此相同,不再赘述。)

协程的切换

协程的切换与此相同,只是把内核的工作转移到协程框架实现而已,下图是协程切换前的状态:

image.png

从协程 1 切换到协程 2 后的状态如下图所示:

image.png

协程就是用户态的线程。然而,为了保证所有切换都在用户态进行,协程必须重新封装所有的阻塞系统调用,否则,一旦协程触发了线程切换,会导致这个线程进入休眠状态,进而其上的所有协程都得不到执行。比如,普通的 sleep 函数会让当前线程休眠,由内核来唤醒线程,而协程化改造后,sleep 只会让当前协程休眠,由协程框架在指定时间后唤醒协程。再比如,线程间的互斥锁是使用信号量实现的,而信号量也会导致线程休眠,协程化改造互斥锁后,同样由框架来协调、同步各协程的执行。

非阻塞+epoll+同步编程 = 协程

协程主要是将IO Wait等场景自动识别然后以非常小的代价切换到其它任务处理,一旦Wait完毕再切换回来。

协程在实现上都是试图用一组少量的线程来实现多个任务,一旦某个任务阻塞,则可能用同一线程继续运行其他任务,避免大量上下文的切换。每个协程所独占的系统资源往往只有栈部分。而且,各个协程之间的切换,往往是用户通过代码来显式指定的(跟各种 callback 类似),不需要内核参与,可以很方便的实现异步

这个技术本质上也是异步非阻塞技术,它是将事件回调进行了包装,让程序员看不到里面的事件循环。程序员就像写阻塞代码一样简单。比如调用 client->recv() 等待接收数据时,就像阻塞代码一样写。实际上是底层库在执行recv时悄悄保存了一个状态,比如代码行数,局部变量的值。然后就跳回到EventLoop中了。什么时候真的数据到来时,它再把刚才保存的代码行数,局部变量值取出来,又开始继续执行。

协程是异步非阻塞的另外一种展现形式。Golang,Erlang,Lua协程都是这个模型。

协程的优点是它比系统线程开销小,缺点是如果其中一个协程中有密集计算,其他的协程就不运行了。操作系统进程、线程切换的缺点是开销大,优点是无论代码怎么写,所有进程都可以并发运行。
协程也叫做用户态进程/用户态线程。区别就在于进程/线程是操作系统充当了EventLoop调度,而协程是自己用Epoll进行调度。

Erlang解决了协程密集计算的问题,它基于自行开发VM,并不执行机器码。即使存在密集计算的场景,VM发现某个协程执行时间过长,也可以进行中止切换。Golang由于是直接执行机器码的,所以无法解决此问题。所以Golang要求用户必须在密集计算的代码中,自行Yield。

操作系统调用不知道内部具体实现,代价包含:上下文切换(几百个指令?)、PageCache
语言自己调度(协程)一般是执行完,基于栈的切换只需要保存栈指针;一定是在同一个线程/进程内切换,各种Cache还有效。

多线程下的真正开销代价

系统调用开销其实不大,上下文切换同样也是数十条cpu指令可以完成

多线程调度下的热点火焰图:

image.png

多线程下真正的开销来源于线程阻塞唤醒调度,系统调用和上下文切换伴随着多线程,所以导致大家一直认为系统调用和上下文切换过多导致了多线程慢。

以ajdk的Wisp2协程为例

对于很快的锁,Wisp2可以很好地解决,因为任务切换不频繁,最多也就CPU核数量的任务在切换,拿到锁的协程会很快执行完然后释放锁,所以其他协程再执行的时候容易拿到锁。

但是对于像logback日志同步输出doAppend()的锁(比较慢,并发度高)Wisp2就基本无能为力了。

Wisp2的主线程跟CPU数量一致,Wisp1的时候碰到CPU执行很长的任务就容易卡主,Wisp2解决了这个问题,超过一定时间会让出这个协程。如果主线程比较闲的时候会尝试从其它主线程 steal 协程(任务)过来, steal的时候需要加锁(自旋锁)来尝试steal成功。如果碰到其他主线程也在steal就可能会失败,steal尝试几次加锁不成功(A线程尝试steal B线程的协程-任务,会尝试锁住A和B,但是比如C线程也在偷的话可能会导致A偷取失败)就放弃。

Wisp2碰到执行时间比较长的任务的话,有个线程会过一段时间去监控,如果超过100ms,就触发一个safepoint,触发抢占。

Node.js

Node.js:基于事件的异步非阻塞框架,基于V8,上层跑JavaScript应用。默认只有一个eventLoop导致也只能用一个核。

Node.js 只有一个 EventLoop,也就是只占用一个 CPU 内核,当 Node.js 被CPU 密集型任务占用,导致其他任务被阻塞时,却还有 CPU 内核处于闲置状态,造成资源浪费。

比喻

关于JAVA的网络,之前有个比喻形式的总结,分享给大家:

有一个养鸡的农场,里面养着来自各个农户(Thread)的鸡(Socket),每家农户都在农场中建立了自己的鸡舍(SocketChannel)

  • 1、BIO:Block IO,每个农户盯着自己的鸡舍,一旦有鸡下蛋,就去做捡蛋处理;
  • 2、NIO:No-Block IO-单Selector,农户们花钱请了一个饲养员(Selector),并告诉饲养员(register)如果哪家的鸡有任何情况(下蛋)均要向这家农户报告(select keys);
  • 3、NIO:No-Block IO-多Selector,当农场中的鸡舍逐渐增多时,一个饲养员巡视(轮询)一次所需时间就会不断地加长,这样农户知道自己家的鸡有下蛋的情况就会发生较大的延迟。怎么解决呢?没错,多请几个饲养员(多Selector),每个饲养员分配管理鸡舍,这样就可以减轻一个饲养员的工作量,同时农户们可以更快的知晓自己家的鸡是否下蛋了;
  • 4、Epoll模式:如果采用Epoll方式,农场问题应该如何改进呢?其实就是饲养员不需要再巡视鸡舍,而是听到哪间鸡舍的鸡打鸣了(活跃连接),就知道哪家农户的鸡下蛋了;
  • 5、AIO:Asynchronous I/O, 鸡下蛋后,以前的NIO方式要求饲养员通知农户去取蛋,AIO模式出现以后,事情变得更加简单了,取蛋工作由饲养员自己负责,然后取完后,直接通知农户来拿即可,而不需要农户自己到鸡舍去取蛋。

参考文章

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

LVS 20倍的负载不均衡,原来是内核的这个Bug

LVS 20倍的负载不均衡,原来是内核的这个Bug

姊妹篇:就是要你懂负载均衡–lvs和转发模式

问题由来

最近用 sysbench 做压测的时候,sysbench每次创建100个长连接,lvs后面两台RS(通过wlc来均衡),发现每次都是其中一台差不多95个连接,另外一台大概5个连接,不均衡得太离谱了,并且稳定重现,所以想要搞清楚为什么会出现20倍的不均衡。

前面是啰嗦的基础知识部分,bug直达文章末尾

几个术语和缩写

vip:Virtual IP,LVS实例IP
RS: Real Server 后端真正提供服务的机器
LB: Load Balance 负载均衡器
LVS: Linux Virtual Server

负载均衡调度算法

LVS的负载调度算法有10种,其它不常用的就不说了,凑数没有意义。基本常用的如下四种,这四种又可以分成两大种:rr轮询调度和lc最小连接调度。

rr轮询调度(Round-Robin Scheduling)

轮询调度(Round Robin Scheduling)算法就是以轮询的方式依次将请求调度到不同的服务器,即每次调度执行i = (i + 1) mod n,并选出第i台服务器。算法的优点是其简洁性,它无需记录当前所有连接的状态,不管服务器上实际的连接数和系统负载,所以它是一种无状态调度。

wrr加权轮询调度(Weighted Round-Robin Scheduling)

加权轮询调度(Weighted Round-Robin Scheduling)算法可以解决服务器间性能不一的情况,它用相应的权值表示服务器的处理性能,服务器的缺省权值为1。假设服务器A的权值为1,B的权值为2,则表示服务器B的处理性能是A的两倍。加权轮叫调度算法是按权值的高低和轮叫方式分配请求到各服务器。权值高的服务器先收到的连接,权值高的服务器比权值低的服务器处理更多的连接,相同权值的服务器处理相同数目的连接数。

lc最小连接调度(Least-Connection Scheduling)

最小连接调度(Least-Connection Scheduling)算法是把新的连接请求分配到当前连接数最小的服务器。最小连接调度是一种动态调度算法,它通过服务器当前所活跃的连接数来估计服务器的负载情况。调度器需要记录各个服务器已建立连接的数目,当一个请求被调度到某台服务器,其连接数加1;当连接中止或超时,其连接数减一。

如果集群系统的真实服务器具有相近的系统性能,采用”最小连接”调度算法可以较好地均衡负载。

特别注意:这种调度算法还需要考虑active(权重*256)和inactive连接的状态,这个实现考量实际会带来严重的不均衡问题。

wlc加权最小连接调度(Weighted Least-Connection Scheduling)

加权最小连接调度(Weighted Least-Connection Scheduling)算法是最小连接调度的超集,各个服务器用相应的权值表示其处理性能。服务器的缺省权值为1,系统管理员可以动态地设置服务器的权值。加权最小连接调度在调度新连接时尽可能使服务器的已建立连接数和其权值成比例。

调度器可以自动问询真实服务器的负载情况,并动态地调整其权值。

其中wlc和lc可以看成一种,wrr和rr可以看成另外一种。下面只重点说wrr和wlc为什么不均衡

为什么会不均衡

wrr算法

非常简单,来了新连接向各个RS转发就行,比如一段时间内创建100个连接,那这100个连接能基本均匀分布在后端所有RS上。

长连接

如果所有请求都是长连接,如果后端有RS重启(宕机、OOM服务不响应、日常性重启等等),那么其上面的连接一般会重建,重建的新连接会均匀分布到其它RS上,当重启的RS正常加入到LVS后,它上面的连接是最少的,即使后面大批量建新的连接,也只是新连接在这些RS上均匀分布,重新加入的RS没法感知到历史已经存在的老连接所以容易导致负载不均衡。

批量重启所有RS(升级等,多个RS进入服务状态肯定有先后),第一个起来的RS最容易获取到更多的连接,压力明显比其它RS要大,这肯定也是不符合预期的。

总之wrr/rr算法因为不考虑已存在的连接问题,在长连接的情况下对RS重启、扩容(增加新的RS)十分不友好,容易导致长连接的不均衡。

当然对于短连接不存在这个问题,所以可以考虑让应用端的连接不要那么长,比如几个小时候断开重新连接一下。升级的时候等所有RS都启动好后再让LVS开始工作等

LVS 集群下不均衡

一般一个负载均衡由多个(4个)LVS 机器组成。

假设每批请求发来四个新连接,经过4台负载均衡机器每个机器一个连接,这一个连接都打到了 RS 的第一个节点上。主要是4台负载均衡机器之间没有协商机制,互相没有同步信息。

可以的解决方案:LVS 机器加入随机因子,让每个LVS 认为的第一个节点不一样

权值相等的WRR算法是否与RR算法等效?

不等效。原因是由于RR调试算法加入了初始随机因子,而WRR由于算法的限制没有此功能。因此在新建连接数少,同时并发连接少,也没有预热的情况下,RR算法会有更好的均衡性表现。

WRR在每一次健康检查抖动的时候,会重置调度器,从头开始WRR的逻辑,因此可能会导致调度部分调度不均匀。

案例

比如如下这个负载不均衡,因为第一个RS CPU特别忙,QPS的不均衡大致能说明工作连接的差异

image-20210422171244718

  1. 连接数差距大有一部分是因为机器忙,断开慢。lvs监控的累积连接数是200:250的差距, 流量差距是1:2
  2. wrr会经常重置调度逻辑,经常从第一台开始轮询,导致第一台压力更大

和lvs负载监控数据对比来看是一致的:

image-20210422171155401

wlc算法

针对wrr对长连接的上述不均衡,所以wlc算法考虑当前已存在的连接数,尽量把新连接发送到连接数较少的RS上,看起来比较完美地修复了wrr的上述不均衡问题。

wlc将连接分成active(ESTABLISHED)和inactive(syn/fin等其它状态),收到syn包后LVS按照如下算法判定该将syn发给哪个RS

static inline int
ip_vs_dest_conn_overhead(struct ip_vs_dest *dest)
{
        /* We think the overhead of processing active connections is 256
         * times higher than that of inactive connections in average. (This
         * 256 times might not be accurate, we will change it later) We
         * use the following formula to estimate the overhead now:
         *                dest->activeconns*256 + dest->inactconns
         */
        return (atomic_read(&dest->activeconns) << 8) +
                atomic_read(&dest->inactconns);
}

也就是一个active状态的连接权重是256,一个inactive权重是1,然后将syn发给总连接负载最轻的RS。

这里会导致不均衡过程: 短时间内有一批syn冲过来(同时并发创建一批连接),必然有一个RS(假如这里总共两个RS)先建立第一个active的连接,在第二个RS也建立第一个active连接之前,后面的syn都会发给第二个RS,那么最终会看到第二个RS的连接远大于第一个RS,这样就导致了最终连接数的负载不均衡。

主要是因为这里对inactive 连接的判定比较糙,active连接的权重直接256就更糙了(作者都说了是拍脑袋的)。实际握手阶段的连接直接都判定为active比较妥当,挥手阶段的连接判定为inactive是可以的,但是active的权重取4或者8就够了,256有点夸张。

这个不均衡场景可以通过 sysbench 稳定重现,如果两个RS的rt差异大一点会更明显

RS到LVS之间的时延差异会放大这个不均衡,这个差异必然会存在,再就是vpc网络环境下首包延时很大(因为overlay之类的网络,连接的首包都会去网关拉取路由信息,所以首包都很慢),差异会更明显,因为这些都会影响第一个active连接的建立。

What is an ActiveConn/InActConn (Active/Inactive) connnection?

  • ActiveConn in ESTABLISHED state
  • InActConn any other state

只对NAT模式下有效:

With LVS-NAT, the director sees all the packets between the client and the realserver, so always knows the state of tcp connections and the listing from ipvsadm is accurate. However for LVS-DR, LVS-Tun, the director does not see the packets from the realserver to the client.

Example with my Apache Web server.

Client  	    <---> Server

A client request an object on the web server on port 80 :

SYN REQUEST     ---->
SYN ACK 	    <----
ACK             ----> *** ActiveConn=1 and 1 ESTABLISHED socket on realserver.
HTTP get        ----> *** The client request the object
HTTP response   <---- *** The server sends the object
APACHE closes the socket : *** ActiveConn=1 and 0 ESTABLISHED socket on realserver
The CLIENT receives the object. (took 15 seconds in my test)
ACK-FIN         ----> *** ActiveConn=0 and 0 ESTABLISHED socket on realserver

slb下的wlc

阿里slb集群下多台LVS服务器之间是开启的session同步功能,因此WLC在计算后端RS的连接权重时会将其它LVS服务器同步的连接计算进来,所以说实际上是一个准全局的调度算法,因此它的调度均衡性最好

WLC由于要计算所有连接的权重,因此消耗的CPU最多,性能最差。由于Session同步不是实时的,同时WLC算法对完成三次握手连接与半开连接的计算权重不同,因此WLC算法不适合突发新建连接的场景。

sysbench验证wlc均衡逻辑

lvs(多个LVS节点的集群)后面总共两个RS,如果一次性同时创建100个连接,那么基本上这个100个连接都在第一个RS上,如果先创建50个,这时这50个基本在第一个RS上,休息几秒钟,再创建50个,那么第二批的50个基本落在第二个RS上。

如果先创建50个,这时这50个基本在第一个RS上,休息几秒钟,再创建100个,那么第二批的100个中前50个基本落在第二个RS上,后面50个又都跑到第一个RS上了。

Session Persistence 导致的负载不均衡

LB 上开启了“会话保持”(Session Persistence),会将会话黏在某个RS上,如果刚好某个请求端访问量大,就会导致这个RS访问量大,从而不均衡

LVS的会话保持有这两种

  1. 把同一个client的请求信息记录到lvs的hash表里,保存时间使用persistence_timeout控制,单位为秒。 persistence_granularity 参数是配合persistence_timeout的,在某些情况特别有用,他的值是子网掩码,表示持久连接的粒度,默认是 255.255.255.255,也就是单独的client ip,如果改成,255.255.255.0就是client ip一个网段的都会被分配到同一个real server。

  2. 一个连接创建后空闲时的超时时间,这个时间为3种

    • tcp的空闲超时时间

    • lvs收到客户端tcp fin的超时时间

    • udp的超时时间

总结

  • wrr/rr在长连接下,RS比较害怕动态扩容、重启机器、升级应用等场景
  • wrr会因为没有随机因子在小流量、探活失败重置场景下导致第一个RS 压力大,总的来说推荐rr而不是wrr
  • wlc/lc在长连接下,如果同时创建的大量连接(比如sysbench压测),因为内核的lvs逻辑对active和inactive判定不太合理导致了这种场景下连接会严重不均衡。
  • 如果是druid这种连接池一个个创建的连接在wlc/lc算法是不会触发不均衡
  • 如果lvs到两个RS的rt差异越大会加剧wlc/lc的不平衡(rt差异肯定是会存在的)

参考文章

What is an ActiveConn/InActConn (Active/Inactive) connnection?

就是要你懂抓包--WireShark之命令行版tshark

玩转TShark(Wireshark的命令行版)

在我感叹Wireshark图形界面的强大时候,有时候也抱怨有点慢,或者感叹下要是有命令行界面版该多好啊,实际上TShark就是WireShark的命令行版,WireShark的功能基本都有,还能组合grep/awk等编程处理分析抓包文件。

下面让我们通过一些例子来学习TShark的常用功能,所有用到的*.cap/*.pcap等都是通过tcpdump抓到的包。请收藏好,下次碰到类似问题直接用文章中的命令跑一下。

wireshark问题

配置文件路径

macOS 下

1
~/.config/wireshark

查看有哪些plugins 以及路径

image-20240614105158403

不再展示协议内容

比如,info列不再显示mysql 的request、response,但是下方的二进制解析能看到select等语句,这种一般是配置文件中 disable 了mysql协议。

配置文件名:C:\Users\admin\AppData\Roaming\Wireshark\disabled_protos

如果抓包缺失很大(比如进出走两个网卡,实际只抓了一个网卡),那么协议解析后也不会正确显示。

IO graph图表无法展示数据

一般是缺数据,先把IO graph关掉再重新打开就可以了,注意图表title显示

tcp segment of a reassembled pdu

这个提示是指,wireshark需要将多个tcp协议包重新组合成特定协议内容(比如MySQL,HTTP),但是因为包缺失(或者每个包大小截断了)导致reassembled失败。实际上wireshark已经成功检测到该协议,只是在解析这个协议的时候缺失包导致解析不好。

这个时候可以试试将指定协议的reassembled属性关掉

image.png

PDU:Protocol Data Unit

If the reassembly is successful, the TCP segment containing the last part of the packet will show the packet.
The reassembly might fail if some TCP segments are missing.

TCP segment of a reassembled PDU means that:

  1. Wireshark/TShark thinks it knows what protocol is running atop TCP in that TCP segment;
  2. that TCP segment doesn’t contain all of a “protocol data unit” (PDU) for that higher-level protocol, i.e. a packet or protocol message for that higher-level protocol, and doesn’t contain the last part of that PDU, so it’s trying to reassemble the multiple TCP segments containing that higher-level PDU.

常用命令

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
#parse 8507/4444 as mysql protocol, default only parse 3306 as mysql.
sudo tshark -i eth0 -d tcp.port==8507,mysql -T fields -e mysql.query 'port 8507'

sudo tshark -i any -c 50 -d tcp.port==4444,mysql -Y " ((tcp.port eq 4444 ) )" -o tcp.calculate_timestamps:true -T fields -e frame.number -e frame.time_epoch -e frame.time_delta_displayed -e ip.src -e tcp.srcport -e tcp.dstport -e ip.dst -e tcp.time_delta -e tcp.stream -e tcp.len -e mysql.query

#query time
sudo tshark -i eth0 -Y " ((tcp.port eq 3306 ) and tcp.len>0 )" -o tcp.calculate_timestamps:true -T fields -e frame.number -e frame.time_epoch -e frame.time_delta_displayed -e ip.src -e tcp.srcport -e tcp.dstport -e ip.dst -e tcp.time_delta -e tcp.stream -e tcp.len -e mysql.query

#每隔3秒钟生成一个新文件,总共生成5个文件后(15秒后)终止抓包,然后包名也按时间规范好了
sudo tcpdump -t -s 0 tcp port 3306 -w 'dump_%Y-%m-%d_%H:%M:%S.pcap' -G 3 -W 5 -Z root

#每隔30分钟生成一个包并压缩
nohup sudo tcpdump -i eth0 -t -s 0 tcp and port 3306 -w 'dump_%Y-%m-%d_%H:%M:%S.pcap' -G 1800 -W 48 -Z root -z gzip &

#file size 1000M
nohup sudo tcpdump -i eth0 -t -s 0 tcp and port 3306 -w 'dump_' -C 1000 -W 300 -Z root -z gzip &

#抓取详细SQL语句, 快速确认client发过来的具体SQL内容:
sudo tshark -i any -f 'port 8527' -s 0 -l -w - |strings
sudo tshark -i eth0 -d tcp.port==3306,mysql -T fields -e mysql.query 'port 3306'
sudo tshark -i eth0 -R "ip.addr==11.163.182.137" -d tcp.port==3306,mysql -T fields -e mysql.query 'port 3306'
sudo tshark -i eth0 -R "tcp.srcport==62877" -d tcp.port==3001,mysql -T fields -e tcp.srcport -e mysql.query 'port 3001'

#直接展示,省掉wireshark
$tshark -i bond0 port 3306 -T fields -e frame.number -e frame.time_delta -e col.Source -e col.Destination -e col.Protocol -e ip.len -e col.Info -e mysql.query
$tshark -i bond0 port 3306 -T fields -e frame.number -e frame.time_delta -e tcp.srcport -e tcp.dstport -e col.Info -e mysql.query
$tshark -i bond0 port 3306 -T fields -E separator=, -E quote=d -e frame.number -e frame.time_delta -e tcp.srcport -e tcp.dstport -e col.Info
"1","0.000000000","1620","3306","faxportwinport > mysql [SYN] Seq=0 Win=42340 Len=0 MSS=1460 SACK_PERM=1 WS=512"
"2","0.000026993","3306","1620","mysql > faxportwinport [SYN, ACK] Seq=0 Ack=1 Win=29200 Len=0 MSS=1460 SACK_PERM=1 WS=128"

分析mysql的每个SQL响应时间

应用有输出的日志显示DB慢,DB监控到的日志显示自己很快,经常扯皮,如果直接在应用机器的网卡抓包,然后分析到每个SQL的响应时间,那么DB、网络都可以甩锅了(有时候应用统计的时间包含了应用自身的时间、取连接的时间等)

tshark -r 213_php.cap -Y "mysql.query or (  tcp.srcport==3306)" -o tcp.calculate_timestamps:true -T fields -e frame.number -e frame.time_epoch  -e frame.time_delta_displayed  -e ip.src -e tcp.srcport -e tcp.dstport -e ip.dst -e tcp.time_delta -e tcp.stream -e tcp.len -e mysql.query |sort -nk9 -nk1
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
34143	1475902394.645073000	0.000342000	10.100.53.17	3306	40383	10.100.10.213	0.000153000	2273	0	
34145 1475902394.645333000 0.000260000 10.100.53.17 3306 40383 10.100.10.213 0.000253000 2273 77
34150 1475902394.645537000 0.000204000 10.100.53.17 3306 40383 10.100.10.213 0.000146000 2273 0
34151 1475902394.645706000 0.000169000 10.100.53.17 3306 40383 10.100.10.213 0.000169000 2273 11
34153 1475902394.645737000 0.000031000 10.100.10.213 40383 3306 10.100.53.17 0.000031000 2273 21 SET NAMES 'utf8'
34161 1475902394.646390000 0.000158000 10.100.53.17 3306 40383 10.100.10.213 0.000653000 2273 11
34162 1475902394.646418000 0.000028000 10.100.10.213 40383 3306 10.100.53.17 0.000028000 2273 22 START TRANSACTION
34164 1475902394.646713000 0.000295000 10.100.53.17 3306 40383 10.100.10.213 0.000295000 2273 11
34166 1475902394.646776000 0.000063000 10.100.10.213 40383 3306 10.100.53.17 0.000063000 2273 46 select AUTO_SEQ_t_order.nextval from dual
34194 1475902394.651468000 0.000909000 10.100.53.17 3306 40383 10.100.10.213 0.004692000 2273 100
34195 1475902394.651782000 0.000314000 10.100.10.213 40383 3306 10.100.53.17 0.000314000 2273 576 insert into t_order (`out_order_no`,`pk_order`,`uid`,`ytid`,`platform`,`origin_price`,`price`,`partner_id`,`ip`,`sources`,`pay_state`,`type`,`product_type`,`device`,`extension`,`spm`,`ext2`,`createtime`,`pay_channel`,`use_ytid`,`updatetime`) values ('2016100822003361672230261573284','261573284','336167223','336167223','1','500','500','100000','42.49.141.142','2','1','1','2','3','{\"showid\":\"286083\",\"play_url\":\"http:\\/\\/v.youku.com\\/v_show\\/id_XMTczOTM5NjU1Mg==.html\",\"permit_duration\":172800}','','','2016-10-08 12:53:14','201','0','2016-10-08 12:53:14')
34196 1475902394.653275000 0.001493000 10.100.53.17 3306 40383 10.100.10.213 0.001493000 2273 19
34197 1475902394.653410000 0.000135000 10.100.10.213 40383 3306 10.100.53.17 0.000135000 2273 370 insert into t_order_product (`fk_order`,`product_id`,`origin_price`,`price`,`discount`,`deliver_state`,`product_url`,`product_name`,`amount`,`ytid`,`sub_product_id`,`createtime`) values ('2016100822003361672230261573284','4000010000','500','500','0','1','http://vip.youku.com','���������������������2:���������������','1','336167223','286083','2016-10-08 12:53:14')
34198 1475902394.658326000 0.004916000 10.100.53.17 3306 40383 10.100.10.213 0.004916000 2273 19
34199 1475902394.658407000 0.000081000 10.100.10.213 40383 3306 10.100.53.17 0.000081000 2273 11 commit
34200 1475902394.659626000 0.001219000 10.100.53.17 3306 40383 10.100.10.213 0.001219000 2273 11
34201 1475902394.659811000 0.000185000 10.100.10.213 40383 3306 10.100.53.17 0.000185000 2273 22 START TRANSACTION
34202 1475902394.660054000 0.000243000 10.100.53.17 3306 40383 10.100.10.213 0.000243000 2273 11
34203 1475902394.660126000 0.000072000 10.100.10.213 40383 3306 10.100.53.17 0.000072000 2273 125 SELECT * FROM t_order where ( out_order_no = '2016100822003361672230261573284' ) AND ( ytid = '336167223' ) FOR UPDATE
34209 1475902394.661970000 0.001844000 10.100.53.17 3306 40383 10.100.10.213 0.001844000 2273 2214
34211 1475902394.662069000 0.000099000 10.100.10.213 40383 3306 10.100.53.17 0.000089000 2273 122 update t_order set `pay_state`='2',`updatetime`='2016-10-08 12:53:14' where pk_order='261573284' and ytid='336167223'
34213 1475902394.662917000 0.000848000 10.100.53.17 3306 40383 10.100.10.213 0.000848000 2273 19
34216 1475902394.663049000 0.000088000 10.100.10.213 40383 3306 10.100.53.17 0.000132000 2273 11 commit
34225 1475902394.664204000 0.000264000 10.100.53.17 3306 40383 10.100.10.213 0.001155000 2273 11
34226 1475902394.664269000 0.000065000 10.100.10.213 40383 3306 10.100.53.17 0.000065000 2273 115 SELECT * FROM t_order where ( out_order_no = '2016100822003361672230261573284' ) AND ( ytid = '336167223' )
34235 1475902394.665694000 0.000061000 10.100.53.17 3306 40383 10.100.10.213 0.001425000 2273 2214
34354 1475902394.681464000 0.000157000 10.100.53.17 3306 40383 10.100.10.213 0.000187000 2273 0
34174 1475902394.648046000 0.001123000 10.100.53.19 3306 33471 10.100.10.213 0.000151000 2275 0
34176 1475902394.648331000 0.000285000 10.100.53.19 3306 33471 10.100.10.213 0.000278000 2275 77
34179 1475902394.648482000 0.000151000 10.100.53.19 3306 33471 10.100.10.213 0.000127000 2275 0
34180 1475902394.648598000 0.000116000 10.100.53.19 3306 33471 10.100.10.213 0.000116000 2275 11
34181 1475902394.648606000 0.000008000 10.100.10.213 33471 3306 10.100.53.19 0.000008000 2275 21 SET NAMES 'utf8'
34182 1475902394.648846000 0.000240000 10.100.53.19 3306 33471 10.100.10.213 0.000240000 2275 11
34183 1475902394.648885000 0.000039000 10.100.10.213 33471 3306 10.100.53.19 0.000039000 2275 380 select pk_auto_renew_account as account_id,fk_user as uid,platform,ytid,fk_member_conf_id as member_id,fk_product_id as product_id,price,fk_pay_channel as pay_channel,renew_type,fk_order,fk_auto_renew_subscribe_log as fk_subscribe_log,state,memo,nexttime,createtime,updatetime from t_auto_renew_account where ( ytid = '354295193' ) AND ( platform = '1' ) AND ( state <> '3' )
34184 1475902394.650040000 0.001155000 10.100.53.19 3306 33471 10.100.10.213 0.001155000 2275 1727
34189 1475902394.650559000 0.000519000 10.100.53.19 3306 33471 10.100.10.213 0.000198000 2275 0

或者:
tshark -r gege_drds.pcap -Y “ ((tcp.srcport eq 3306 ) and tcp.len>0 )” -o tcp.calculate_timestamps:true -T fields -e frame.number -e frame.time_epoch -e frame.time_delta_displayed -e ip.src -e tcp.srcport -e tcp.dstport -e ip.dst -e tcp.time_delta -e tcp.stream -e tcp.len -e tcp.analysis.ack_rtt

这个命令跑出来,倒数第四列基本就是rt

967     1548148159.346612000    0.000442000     192.168.4.18    3306    44026   192.168.100.30  0.005255000     17      1576    0.005255000
969     1548148159.346826000    0.000214000     192.168.4.18    3306    44090   192.168.100.30  0.005425000     15      1576    0.005425000
973     1548148159.347428000    0.000602000     192.168.4.18    3306    44070   192.168.100.30  0.005517000     8       2500    0.005517000
979     1548148159.348640000    0.001212000     192.168.4.18    3306    44048   192.168.100.30  0.005517000     22      2462    0.005517000
981     1548148159.348751000    0.000111000     192.168.4.18    3306    44066   192.168.100.30  0.005855000     21      2692    0.005855000
983     1548148159.348844000    0.000093000     192.168.4.18    3306    44046   192.168.100.30  0.004589000     3       2692    0.004589000
985     1548148159.348981000    0.000137000     192.168.4.18    3306    44012   192.168.100.30  0.004885000     19      2443    0.004885000
990     1548148159.349293000    0.000312000     192.168.4.18    3306    44074   192.168.100.30  0.005923000     5       2692    0.005923000
994     1548148159.349671000    0.000378000     192.168.4.18    3306    44080   192.168.100.30  0.004889000     4       2730    0.004889000
1009    1548148159.350591000    0.000920000     192.168.4.18    3306    44022   192.168.100.30  0.004187000     14      1448    0.004187000
1010    1548148159.350592000    0.000001000     192.168.4.18    3306    44022   192.168.100.30  0.000001000     14      1052    
1013    1548148159.350790000    0.000198000     192.168.4.18    3306    44002   192.168.100.30  0.005998000     0       1576    0.005998000
1026    1548148159.352207000    0.001417000     192.168.4.18    3306    44026   192.168.100.30  0.005348000     17      1448    0.005348000
1027    1548148159.352217000    0.000010000     192.168.4.18    3306    44026   192.168.100.30  0.000010000     17      1052    
1036    1548148159.352973000    0.000756000     192.168.4.18    3306    44090   192.168.100.30  0.005940000     15      2500    0.005940000
1041    1548148159.353683000    0.000710000     192.168.4.18    3306    44070   192.168.100.30  0.005190000     8       2692    0.005190000
1043    1548148159.353737000    0.000054000     192.168.4.18    3306    44066   192.168.100.30  0.004635000     21      1448    0.004635000
1044    1548148159.353749000    0.000012000     192.168.4.18    3306    44066   192.168.100.30  0.000012000     21      128     
1051    1548148159.354289000    0.000540000     192.168.4.18    3306    44046   192.168.100.30  0.004911000     3       1576    0.004911000
1054    1548148159.354511000    0.000222000     192.168.4.18    3306    44080   192.168.100.30  0.004515000     4       1576    0.004515000
1055    1548148159.354530000    0.000019000     192.168.4.18    3306    44074   192.168.100.30  0.004909000     5       1576    0.004909000
1065    1548148159.355412000    0.000882000     192.168.4.18    3306    44012   192.168.100.30  0.005217000     19      2692    0.005217000
1067    1548148159.355496000    0.000084000     192.168.4.18    3306    44048   192.168.100.30  0.005231000     22      2610    0.005231000
1072    1548148159.356111000    0.000615000     192.168.4.18    3306    44052   192.168.100.30  0.005830000     24      2730    0.005830000
1076    1548148159.356545000    0.000434000     192.168.4.18    3306    44022   192.168.100.30  0.005615000     14      2692    0.005615000
1079    1548148159.357012000    0.000467000     192.168.4.18    3306    44002   192.168.100.30  0.005966000     0       2462    0.005966000
1082    1548148159.357235000    0.000223000     192.168.4.18    3306    44072   192.168.100.30  0.004817000     23      2692    0.004817000
1093    1548148159.359244000    0.002009000     192.168.4.18    3306    44070   192.168.100.30  0.005188000     8       1576    0.005188000

MySQL响应时间直方图【第八列的含义– Time since previous frame in this TCP stream: seconds】

tshark -r gege_drds.pcap -Y "mysql.query or (tcp.srcport==3306  and tcp.len>60)" -o tcp.calculate_timestamps:true -T fields -e frame.number -e frame.time_epoch  -e frame.time_delta_displayed  -e ip.src -e tcp.srcport -e tcp.dstport -e ip.dst -e tcp.time_delta -e tcp.stream -e tcp.len | awk 'BEGIN {sum0=0;sum3=0;sum10=0;sum30=0;sum50=0;sum100=0;sum300=0;sum500=0;sum1000=0;sumo=0;count=0;sum=0} {rt=$8; if(rt>=0.000) sum=sum+rt; count=count+1; if(rt<=0.000) sum0=sum0+1; else if(rt<0.003) sum3=sum3+1 ; else if(rt<0.01) sum10=sum10+1; else if(rt<0.03) sum30=sum30+1; else if(rt<0.05) sum50=sum50+1; else if(rt < 0.1) sum100=sum100+1; else if(rt < 0.3) sum300=sum300+1; else if(rt < 0.5) sum500=sum500+1; else if(rt < 1) sum1000=sum1000+1; else sum=sum+1 ;} END{printf "-------------\n3ms:\t%s \n10ms:\t%s \n30ms:\t%s \n50ms:\t%s \n100ms:\t%s \n300ms:\t%s \n500ms:\t%s \n1000ms:\t%s \n>1s:\t %s\n-------------\navg: %.6f \n" , sum3,sum10,sum30,sum50,sum100,sum300,sum500,sum1000,sumo,sum/count;}'
 
 -------------
3ms:	145037 
10ms:	78811 
30ms:	7032 
50ms:	2172 
100ms:	1219 
300ms:	856 
500ms:	449 
1000ms:118
>1s:	0
-------------
avg: 0.005937 

对于rt分析,要注意一个query多个response情况(response结果多,分包了),分析这种rt的时候只看query之后的第一个response,其它连续response需要忽略掉。

有时候应用说修改库存的代码都加了事务,但是数据库里库存对不上,这锅压力好大,抓个包看看应用发过来的SQL是啥

开发测试环境上通过如下命令也可以直接用tshark抓包分析SQL语句:

sudo tshark -i eth0 -d tcp.port==3306,mysql -T fields -e mysql.query 'port 3306'

这样就直接看到发出的SQL是否是autocommit=1了

HTTP响应时间分析

1
2
3
4
5
//按秒汇总每个http response 耗时
tshark -r dump.pcap -Y 'http.time>0 ' -T fields -e frame.number -e frame.time_epoch -e frame.time_delta_displayed -e ip.src -e ip.dst -e tcp.stream -e http.request.full_uri -e http.response.code -e http.time | awk '{ print int($2), $8 }' | awk '{ sum[$1]+=$2; count[$1]+=1 ;} END { for (key in count) { printf "time= %s \t count=%s \t avg=%.6f \n", key, count[key], sum[key]/count[key] } }' | sort -k2n | awk '{ print strftime("%c",$2), $0 }'

//on macOS
tshark -r dump.pcap -Y 'http.response_for.uri contains "health" ' -T fields -e frame.number -e frame.time_epoch -e frame.time_delta_displayed -e ip.src -e ip.dst -e tcp.stream -e http.request.full_uri -e http.response_for.uri -e http.time | awk '{ print int($2/10), $8 }' | awk '{ sum[$1]+=$2; count[$1]+=1 ;} END { for (key in count) { printf "time= %s \t count=%s \t avg=%.6f \n", key, count[key], sum[key]/count[key] } }' | sort -k2n | gawk '{ print strftime("%c",$2), $0 }'

按http response分析响应时间

第三列是RT,倒数第二列是stream,同一个stream是一个连接。对应http response 200的是请求响应结果的RT

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
# tshark -nr 10.cap -o tcp.calculate_timestamps:true  -Y "http.request or http.response" -T fields -e frame.number -e frame.time_epoch  -e tcp.time_delta  -e ip.src -e ip.dst -e tcp.stream  -e http.request.full_uri -e http.response.code -e http.response.phrase | sort -nk6 -nk1

82579 1631791992.105383000 0.000113000 172.26.2.13 172.26.13.107 1198 http://plantegg/ajax.sword
83167 1631791992.261663000 0.156042000 172.26.13.107 172.26.2.13 1198 200
84917 1631791992.775011000 0.513106000 172.26.2.13 172.26.13.107 1198 http://plantegg/ajax.sword
86388 1631791993.188458000 0.413018000 172.26.13.107 172.26.2.13 1198 200
87391 1631791993.465156000 0.276608000 172.26.2.13 172.26.13.107 1198 http://plantegg/ajax.sword
88067 1631791993.645780000 0.179832000 172.26.13.107 172.26.2.13 1198 200
89364 1631791993.994322000 0.348324000 172.26.2.13 172.26.13.107 1198 http://plantegg/ajax.sword
89843 1631791994.140131000 0.145169000 172.26.13.107 172.26.2.13 1198 200
91387 1631791994.605527000 0.465245000 172.26.2.13 172.26.13.107 1198 http://plantegg/ajax.sword
92271 1631791994.920607000 0.314639000 172.26.13.107 172.26.2.13 1198 200
93491 1631791995.323424000 0.402724000 172.26.2.13 172.26.13.107 1198 http://plantegg/ajax.sword
93860 1631791995.403614000 0.079834000 172.26.13.107 172.26.2.13 1198 200
97221 1631791996.347307000 0.943423000 172.26.2.13 172.26.13.107 1198 http://plantegg/ajax.sword
97862 1631791996.544563000 0.196448000 172.26.13.107 172.26.2.13 1198 200
99613 1631791997.065735000 0.521095000 172.26.2.13 172.26.13.107 1198 http://plantegg/ajax.sword
82714 1631791992.141943000 0.000122000 172.26.2.13 172.26.12.147 1199 http://plantegg/ajax.sword
83055 1631791992.235637000 0.093471000 172.26.12.147 172.26.2.13 1199 200
84789 1631791992.739133000 0.503423000 172.26.2.13 172.26.12.147 1199 http://plantegg/ajax.sword
85525 1631791992.946220000 0.206860000 172.26.12.147 172.26.2.13 1199 200
88208 1631791993.677995000 0.731490000 172.26.2.13 172.26.12.147 1199 http://plantegg/ajax.sword
88638 1631791993.800956000 0.122637000 172.26.12.147 172.26.2.13 1199 200
91010 1631791994.476918000 0.675911000 172.26.2.13 172.26.12.147 1199 http://plantegg/ajax.sword
92079 1631791994.874566000 0.397357000 172.26.12.147 172.26.2.13 1199 200
94480 1631791995.581990000 0.707200000 172.26.2.13 172.26.12.147 1199 http://plantegg/ajax.sword
94764 1631791995.665365000 0.082906000 172.26.12.147 172.26.2.13 1199 200
96241 1631791996.090803000 0.425378000 172.26.2.13 172.26.12.147 1199 http://plantegg/ajax.sword
96731 1631791996.215406000 0.124276000 172.26.12.147 172.26.2.13 1199 200
98832 1631791996.818172000 0.602695000 172.26.2.13 172.26.12.147 1199 http://plantegg/ajax.sword
99735 1631791997.105453000 0.286845000 172.26.12.147 172.26.2.13 1199 200
83462 1631791992.351494000 0.000042000 172.26.2.13 172.26.9.77 1200 http://plantegg/ajax.sword
84309 1631791992.558541000 0.206305000 172.26.9.77 172.26.2.13 1200 200
86253 1631791993.152426000 0.593767000 172.26.2.13 172.26.9.77 1200 http://plantegg/ajax.sword
86740 1631791993.270402000 0.117311000 172.26.9.77 172.26.2.13 1200 200
89775 1631791994.112908000 0.842414000 172.26.2.13 172.26.9.77 1200 http://plantegg/ajax.sword
90429 1631791994.312254000 0.199015000 172.26.9.77 172.26.2.13 1200 200
92840 1631791995.086191000 0.773857000 172.26.2.13 172.26.9.77 1200 http://plantegg/ajax.sword
93262 1631791995.257123000 0.170488000 172.26.9.77 172.26.2.13 1200 200

改进版本,每10秒钟统计http response耗时,最后按时间排序输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
tshark -r 0623.pcap -Y 'http.time>0 ' -T fields -e frame.number -e frame.time_epoch  -e frame.time_delta_displayed  -e ip.src -e ip.dst -e tcp.stream  -e http.request.full_uri -e http.response_for.uri  -e http.time  | awk '{ print int($2/10), $8 }' | awk '{ sum[$1]+=$2; count[$1]+=1 ;} END { for (key in count) {  printf  "time= %s  \t count=%s   \t avg=%.6f \n", key,  count[key], sum[key]/count[key] } }' | sort -k2n | gawk '{ print strftime("%c",$2*10), $0 }'
四 6/23 14:17:30 2022 time= 165596505 count=15289 avg=0.012168
四 6/23 14:17:40 2022 time= 165596506 count=38725 avg=0.013669
四 6/23 14:17:50 2022 time= 165596507 count=42545 avg=0.014140
四 6/23 14:18:00 2022 time= 165596508 count=45613 avg=0.016915
四 6/23 14:18:10 2022 time= 165596509 count=49033 avg=0.018768
四 6/23 14:18:20 2022 time= 165596510 count=49797 avg=0.025015
四 6/23 14:18:30 2022 time= 165596511 count=49670 avg=0.034057
四 6/23 14:18:40 2022 time= 165596512 count=49524 avg=0.040647
四 6/23 14:18:50 2022 time= 165596513 count=49204 avg=0.034251
四 6/23 14:19:00 2022 time= 165596514 count=48024 avg=0.037120
四 6/23 14:19:10 2022 time= 165596515 count=49301 avg=0.041453
四 6/23 14:19:20 2022 time= 165596516 count=42174 avg=0.049191
四 6/23 14:19:30 2022 time= 165596517 count=49437 avg=0.050924
四 6/23 14:19:40 2022 time= 165596518 count=49563 avg=0.050709
四 6/23 14:19:50 2022 time= 165596519 count=49517 avg=0.047916
四 6/23 14:20:00 2022 time= 165596520 count=48256 avg=0.057453
四 6/23 14:20:10 2022 time= 165596521 count=49412 avg=0.053587
四 6/23 14:20:20 2022 time= 165596522 count=51361 avg=0.053422
四 6/23 14:20:30 2022 time= 165596523 count=45610 avg=0.067171
四 6/23 14:20:40 2022 time= 165596524 count=54 avg=2.886536

解析已知协议与IP域名映射

以下技巧抄自:https://www.ilikejobs.com/posts/wireshark/

wireshark

查询当前已经解析了哪些域名

wireshark

设置私有IP名称

先确认下图红框选项是选上的:

image-20240315110305420

wireshark

查看刚设置自定义的名称:

wireshark

保存文件(含host对应名称)

wireshark

分析包的总概览

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ capinfos rsb2.cap 
File name: rsb2.cap
File type: Wireshark/tcpdump/... - pcap
File encapsulation: Ethernet
Packet size limit: file hdr: 65535 bytes
Number of packets: 510 k
File size: 143 MB
Data size: 135 MB
Capture duration: 34 seconds
Start time: Tue Jun 7 11:15:31 2016
End time: Tue Jun 7 11:16:05 2016
Data byte rate: 3997 kBps
Data bit rate: 31 Mbps
Average packet size: 265.62 bytes
Average packet rate: 15 kpackets/sec
SHA1: a8367d0d291eab6ba78732d092ae72a5305756a2
RIPEMD160: ec991772819f316d2f629745d4b58fb861e41fc6
MD5: 53975139fa49581eacdb42bd967cbd58
Strict time order: False

分析每两个IP之间的流量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ tshark -r retrans.cap -q -z 'conv,ip' 
================================================================================
IPv4 Conversations
Filter:<No Filter>
| <- | | -> | | Total | Relative | Duration |
| Frames Bytes | | Frames Bytes | | Frames Bytes | Start | |
100.98.50.214 <-> 10.117.41.213 425 60647 544 350182 969 410829 0.856983000 88.7073
10.252.138.13 <-> 10.117.41.213 381 131639 451 45706 832 177345 3.649894000 79.5370
10.168.127.178 <-> 10.117.41.213 335 118164 390 39069 725 157233 3.456698000 81.2639
10.168.246.105 <-> 10.117.41.213 435 23490 271 14634 706 38124 0.000000000 89.7614
10.117.49.244 <-> 10.117.41.213 452 24408 221 11934 673 36342 0.289990000 89.6024
100.97.197.0 <-> 10.117.41.213 45 4226 107 7310 152 11536 0.538867000 88.0736
100.97.196.0 <-> 10.117.41.213 48 4576 102 6960 150 11536 0.524268000 89.0840
100.97.196.128 <-> 10.117.41.213 39 3462 90 6116 129 9578 0.573839000 88.0728
100.97.197.128 <-> 10.117.41.213 27 1998 81 5562 108 7560 1.071232000 87.0382
100.98.148.129 <-> 10.117.41.213 55 3630 37 2442 92 6072 0.571963000 86.7362
================================================================================

分析每个会话的流量

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
$ tshark -r retrans.cap -q -z 'conv,tcp' 
================================================================================
TCP Conversations
Filter:<No Filter>
| <- | | -> | | Total | Relative | Duration |
| Frames Bytes | | Frames Bytes | | Frames Bytes | Start | |
10.117.41.213:33362 <-> 100.98.50.214:3306 143 107183 108 17345 251 124528 9.556973000 79.9993
10.117.41.213:32695 <-> 100.98.50.214:3306 131 95816 118 17843 249 113659 3.464596000 54.7814
10.117.41.213:33737 <-> 100.98.50.214:3306 107 67199 82 11842 189 79041 69.539519000 13.0781
10.117.41.213:33736 <-> 100.98.50.214:3306 58 37851 31 4895 89 42746 69.539133000 8.2015
10.117.41.213:33735 <-> 100.98.50.214:3306 51 37654 27 3338 78 40992 69.538573000 20.0257
10.117.41.213:33681 <-> 100.98.50.214:3306 22 2367 15 2480 37 4847 58.237482000 0.0082
10.252.138.13:17926 <-> 10.117.41.213:3306 13 3454 17 1917 30 5371 77.462089000 0.2816
10.168.127.178:21250 <-> 10.117.41.213:3306 13 4926 17 2267 30 7193 77.442197000 0.6282
10.252.138.13:17682 <-> 10.117.41.213:3306 13 5421 17 2267 30 7688 34.945805000 0.7274
10.168.127.178:21001 <-> 10.117.41.213:3306 18 9872 11 1627 29 11499 21.220800000 35.0242
10.252.138.13:17843 <-> 10.117.41.213:3306 13 4453 15 1510 28 5963 59.176447000 10.8169
10.168.127.178:20927 <-> 10.117.41.213:3306 12 4414 15 1510 27 5924 13.686763000 0.1860
10.252.138.13:17481 <-> 10.117.41.213:3306 11 4360 16 1564 27 5924 3.649894000 0.1810
10.252.138.13:17928 <-> 10.117.41.213:3306 11 3077 15 1461 26 4538 77.467248000 0.6720
10.168.127.178:21241 <-> 10.117.41.213:3306 11 3077 15 1461 26 4538 77.376858000 0.4669
10.168.127.178:21201 <-> 10.117.41.213:3306 12 3971 14 2571 26 6542 64.890147000 5.4010
10.168.127.178:21184 <-> 10.117.41.213:3306 12 6775 14 1794 26 8569 64.073021000 5.6804
10.252.138.13:17545 <-> 10.117.41.213:3306 11 4379 15 1510 26 5889 13.940379000 0.1845
10.168.127.178:20815 <-> 10.117.41.213:3306 11 4360 15 1510 26 5870 3.456698000 0.1901
10.252.138.13:17864 <-> 10.117.41.213:3306 12 2985 12 1129 24 4114 59.855131000 9.7005
10.252.138.13:17820 <-> 10.117.41.213:3306 11 5529 13 1740 24 7269 49.537379000 0.1669
10.252.138.13:17757 <-> 10.117.41.213:3306 11 6006 13 1740 24 7746 45.507148000 0.7587
10.252.138.13:17677 <-> 10.117.41.213:3306 11 5529 13 1740 24 7269 34.806484000 0.5017
10.168.127.178:21063 <-> 10.117.41.213:3306 11 3848 13 1390 24 5238 29.902032000 0.0133
10.252.138.13:17516 <-> 10.117.41.213:3306 11 5985 13 1740 24 7725 11.505585000 0.1494
10.252.138.13:17507 <-> 10.117.41.213:3306 11 3570 13 1424 24 4994 9.652955000 0.0151
10.252.138.13:17490 <-> 10.117.41.213:3306 11 5985 13 1740 24 7725 4.865639000 0.1275

分析每个包的response time

$ tshark -r rsb2.cap -o tcp.calculate_timestamps:true -T fields -e frame.number -e frame.time_epoch -e ip.src -e ip.dst -e tcp.stream -e tcp.len -e tcp.analysis.initial_rtt -e tcp.time_delta

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
1481	1465269331.308138000	100.98.199.36	10.25.92.13	302	0		0.002276000
1482 1465269331.308186000 10.25.92.13 100.98.199.36 361 11 0.000063000
1483 1465269331.308209000 100.98.199.36 10.25.92.13 496 0 0.004950000
1484 1465269331.308223000 100.98.199.36 10.25.92.13 513 0 0.000000000
1485 1465269331.308238000 100.98.199.36 10.25.92.13 326 0 0.055424000
1486 1465269331.308246000 100.98.199.36 10.25.92.13 514 0 0.000000000
1487 1465269331.308261000 10.25.92.71 10.25.92.13 48 0 0.000229000
1488 1465269331.308277000 100.98.199.36 10.25.92.13 254 0 0.055514000
1489 1465269331.308307000 100.98.199.36 10.25.92.13 292 0 0.002096000
1490 1465269331.308383000 100.98.199.36 10.25.92.13 308 0 0.055406000
1491 1465269331.308403000 100.98.199.36 10.25.92.13 75 0 0.041664000
1492 1465269331.308421000 100.98.199.36 10.25.92.13 291 0 0.001973000
1493 1465269331.308532000 100.98.199.36 10.25.92.13 509 0 0.002100000
1494 1465269331.308567000 100.98.199.36 10.25.92.13 123 0 0.041560000
1495 1465269331.308576000 100.98.199.36 10.25.92.13 232 11 0.063317000
1496 1465269331.308584000 100.98.199.36 10.25.92.13 465 655 0.018121000
1497 1465269331.308626000 100.98.199.36 10.25.92.13 61 655 0.042409000
1498 1465269331.308637000 100.98.199.36 10.25.92.13 146 0 0.001520000
1499 1465269331.308639000 100.98.199.36 10.25.92.13 510 0 0.001460000
1500 1465269331.308645000 100.98.199.36 10.25.92.13 237 11 0.063273000

分析有问题的包、概览

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
$ tshark -r retrans.cap -q -z 'expert,note'

Errors (22)
=============
Frequency Group Protocol Summary
22 Malformed MySQL Malformed Packet (Exception occurred)

Warns (749)
=============
Frequency Group Protocol Summary
538 Sequence TCP ACKed segment that wasn't captured (common at capture start)
192 Sequence TCP Connection reset (RST)
19 Sequence TCP Previous segment not captured (common at capture start)

Notes (1162)
=============
Frequency Group Protocol Summary
84 Sequence TCP TCP keep-alive segment
274 Sequence TCP Duplicate ACK (#1)
37 Sequence TCP ACK to a TCP keep-alive segment
23 Sequence TCP This frame is a (suspected) retransmission
262 Sequence TCP Duplicate ACK (#2)
259 Sequence TCP Duplicate ACK (#3)
141 Sequence TCP Duplicate ACK (#4)
69 Sequence TCP Duplicate ACK (#5)
7 Sequence TCP Duplicate ACK (#6)
5 Sequence TCP This frame is a (suspected) spurious retransmission
1 Sequence TCP Duplicate ACK (#7)

分析rtt、丢包、deplicate等等

$ tshark -r retrans.cap -q -z io,stat,1,”AVG(tcp.analysis.ack_rtt)tcp.analysis.ack_rtt”,”COUNT(tcp.analysis.retransmission) tcp.analysis.retransmission”,”COUNT(tcp.analysis.fast_retransmission) tcp.analysis.fast_retransmission”,”COUNT(tcp.analysis.duplicate_ack) tcp.analysis.duplicate_ack”,”COUNT(tcp.analysis.lost_segment) tcp.analysis.lost_segment”,”MIN(tcp.window_size)tcp.window_size”

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

===================================================================================
| IO Statistics |
| |
| Duration: 89.892365 secs |
| Interval: 2 secs |
| |
| Col 1: AVG(tcp.analysis.ack_rtt)tcp.analysis.ack_rtt |
| 2: COUNT(tcp.analysis.retransmission) tcp.analysis.retransmission |
| 3: COUNT(tcp.analysis.fast_retransmission) tcp.analysis.fast_retransmission |
| 4: COUNT(tcp.analysis.duplicate_ack) tcp.analysis.duplicate_ack |
| 5: COUNT(tcp.analysis.lost_segment) tcp.analysis.lost_segment |
| 6: AVG(tcp.window_size)tcp.window_size |
|---------------------------------------------------------------------------------|
| |1 |2 |3 |4 |5 |6 | |
| Interval | AVG | COUNT | COUNT | COUNT | COUNT | AVG | |
|-------------------------------------------------------------| |
| 0 <> 2 | 0.001152 | 0 | 0 | 0 | 0 | 4206 | |
| 2 <> 4 | 0.002088 | 0 | 0 | 0 | 1 | 6931 | |
| 4 <> 6 | 0.001512 | 0 | 0 | 0 | 0 | 7099 | |
| 6 <> 8 | 0.002859 | 0 | 0 | 0 | 0 | 7171 | |
| 8 <> 10 | 0.001716 | 0 | 0 | 0 | 0 | 6472 | |
| 10 <> 12 | 0.000319 | 0 | 0 | 0 | 2 | 5575 | |
| 12 <> 14 | 0.002030 | 0 | 0 | 0 | 0 | 6922 | |
| 14 <> 16 | 0.003371 | 0 | 0 | 0 | 2 | 5884 | |
| 16 <> 18 | 0.000138 | 0 | 0 | 0 | 1 | 3480 | |
| 18 <> 20 | 0.000999 | 0 | 0 | 0 | 4 | 6665 | |
| 20 <> 22 | 0.000682 | 0 | 0 | 41 | 2 | 5484 | |
| 22 <> 24 | 0.002302 | 2 | 0 | 19 | 0 | 7127 | |
| 24 <> 26 | 0.000156 | 1 | 0 | 22 | 0 | 3042 | |
| 26 <> 28 | 0.000000 | 1 | 0 | 19 | 1 | 152 | |
| 28 <> 30 | 0.001498 | 1 | 0 | 24 | 0 | 5615 | |
| 30 <> 32 | 0.000235 | 0 | 0 | 44 | 0 | 1880 | |

分析丢包、duplicate ack

$ tshark -r retrans.cap -q -z io,stat,5,”COUNT(tcp.analysis.retransmission) tcp.analysis.retransmission”,”COUNT(tcp.analysis.fast_retransmission) tcp.analysis.fast_retransmission”,”COUNT(tcp.analysis.duplicate_ack) tcp.analysis.duplicate_ack”,”COUNT(tcp.analysis.lost_segment) tcp.analysis.lost_segment”

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
===================================================================================
| IO Statistics |
| |
| Duration: 89.892365 secs |
| Interval: 5 secs |
| |
| Col 1: COUNT(tcp.analysis.retransmission) tcp.analysis.retransmission |
| 2: COUNT(tcp.analysis.fast_retransmission) tcp.analysis.fast_retransmission |
| 3: COUNT(tcp.analysis.duplicate_ack) tcp.analysis.duplicate_ack |
| 4: COUNT(tcp.analysis.lost_segment) tcp.analysis.lost_segment |
|---------------------------------------------------------------------------------|
| |1 |2 |3 |4 | |
| Interval | COUNT | COUNT | COUNT | COUNT | |
|------------------------------------------| |
| 0 <> 5 | 0 | 0 | 0 | 1 | |
| 5 <> 10 | 0 | 0 | 0 | 0 | |
| 10 <> 15 | 0 | 0 | 0 | 4 | |
| 15 <> 20 | 0 | 0 | 0 | 5 | |
| 20 <> 25 | 3 | 0 | 67 | 2 | |
| 25 <> 30 | 2 | 0 | 58 | 1 | |
| 30 <> 35 | 0 | 0 | 112 | 0 | |
| 35 <> 40 | 1 | 0 | 156 | 0 | |
| 40 <> 45 | 0 | 0 | 127 | 2 | |
| 45 <> 50 | 1 | 0 | 91 | 0 | |
| 50 <> 55 | 0 | 0 | 63 | 0 | |
| 55 <> 60 | 0 | 0 | 65 | 2 | |
| 60 <> 65 | 2 | 0 | 41 | 0 | |
| 65 <> 70 | 3 | 0 | 34 | 2 | |
| 70 <> 75 | 7 | 0 | 55 | 0 | |
| 75 <> 80 | 3 | 0 | 68 | 0 | |
| 80 <> 85 | 1 | 0 | 46 | 0 | |
| 85 <> Dur| 0 | 0 | 30 | 0 | |
===================================================================================

分析rtt 时间

$ tshark -r ~/ali/metrics/tcpdump/rsb2.cap -q -z io,stat,1,”MIN(tcp.analysis.ack_rtt)tcp.analysis.ack_rtt”,”MAX(tcp.analysis.ack_rtt)tcp.analysis.ack_rtt”,”AVG(tcp.analysis.ack_rtt)tcp.analysis.ack_rtt”

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
========================================================
| IO Statistics |
| |
| Duration: 33.914454 secs |
| Interval: 1 secs |
| |
| Col 1: MIN(tcp.analysis.ack_rtt)tcp.analysis.ack_rtt |
| 2: MAX(tcp.analysis.ack_rtt)tcp.analysis.ack_rtt |
| 3: AVG(tcp.analysis.ack_rtt)tcp.analysis.ack_rtt |
|------------------------------------------------------|
| |1 |2 |3 | |
| Interval | MIN | MAX | AVG | |
|-------------------------------------------| |
| 0 <> 1 | 0.000005 | 0.248840 | 0.009615 | |
| 1 <> 2 | 0.000004 | 0.458952 | 0.009601 | |
| 2 <> 3 | 0.000002 | 0.251274 | 0.009340 | |
| 3 <> 4 | 0.000006 | 0.290993 | 0.010843 | |
| 4 <> 5 | 0.000004 | 0.390800 | 0.008995 | |
| 5 <> 6 | 0.000008 | 0.407525 | 0.011133 | |
| 6 <> 7 | 0.000004 | 0.239225 | 0.008763 | |
| 7 <> 8 | 0.000003 | 0.177203 | 0.009211 | |
| 8 <> 9 | 0.000007 | 0.265505 | 0.010294 | |
| 9 <> 10 | 0.000007 | 0.354278 | 0.008475 | |
| 10 <> 11 | 0.000005 | 5.337388 | 0.011211 | |
| 11 <> 12 | 0.000004 | 0.320651 | 0.008231 | |
| 12 <> 13 | 0.000008 | 0.272029 | 0.008526 | |
| 13 <> 14 | 0.000005 | 0.663421 | 0.014589 | |
| 14 <> 15 | 0.000005 | 0.277754 | 0.009128 | |
| 15 <> 16 | 0.000002 | 0.260320 | 0.010388 | |
| 16 <> 17 | 0.000006 | 0.429298 | 0.009155 | |
| 17 <> 18 | 0.000005 | 0.668089 | 0.010008 | |
| 18 <> 19 | 0.000005 | 0.452897 | 0.009574 | |
| 19 <> 20 | 0.000006 | 0.850698 | 0.010345 | |
| 20 <> 21 | 0.000007 | 0.270671 | 0.012368 | |
| 21 <> 22 | 0.000005 | 0.295439 | 0.008660 | |
| 22 <> 23 | 0.000008 | 0.710938 | 0.010321 | |
| 23 <> 24 | 0.000003 | 0.269014 | 0.010238 | |
| 24 <> 25 | 0.000005 | 0.287966 | 0.009604 | |
| 25 <> 26 | 0.000009 | 0.661160 | 0.010807 | |
| 26 <> 27 | 0.000006 | 0.310515 | 0.009439 | |
| 27 <> 28 | 0.000003 | 0.346298 | 0.011302 | |
| 28 <> 29 | 0.000004 | 0.375117 | 0.008333 | |
| 29 <> 30 | 0.000006 | 1.323647 | 0.008799 | |
| 30 <> 31 | 0.000006 | 0.283616 | 0.010187 | |
| 31 <> 32 | 0.000007 | 0.649273 | 0.008613 | |
| 32 <> 33 | 0.000004 | 0.440265 | 0.010663 | |
| 33 <> Dur| 0.000004 | 0.337023 | 0.011477 | |
========================================================

计算window size

$ tshark -r rsb-single2.cap -q -z io,stat,5,”COUNT(tcp.analysis.retransmission) tcp.analysis.retransmission”,”AVG(tcp.window_size) tcp.window_size”,”MAX(tcp.window_size) tcp.window_size”,”MIN(tcp.window_size) tcp.window_size”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
=========================================================================
| IO Statistics |
| |
| Duration: 30.776061 secs |
| Interval: 5 secs |
| |
| Col 1: COUNT(tcp.analysis.retransmission) tcp.analysis.retransmission |
| 2: AVG(tcp.window_size) tcp.window_size |
| 3: MAX(tcp.window_size) tcp.window_size |
| 4: MIN(tcp.window_size) tcp.window_size |
|-----------------------------------------------------------------------|
| |1 |2 |3 |4 | |
| Interval | COUNT | AVG | MAX | MIN | |
|------------------------------------------| |
| 0 <> 5 | 0 | 4753 | 15744 | 96 | |
| 5 <> 10 | 0 | 8067 | 431616 | 96 | |
| 10 <> 15 | 0 | 5144 | 18688 | 96 | |
| 15 <> 20 | 0 | 11225 | 611072 | 81 | |
| 20 <> 25 | 0 | 5104 | 24448 | 96 | |
| 25 <> 30 | 0 | 10103 | 506880 | 96 | |
| 30 <> Dur| 0 | 5716 | 12423 | 96 | |
=========================================================================

有用的命令(这些命令也都是安装WireShark就装好了的):

capinfos rsb2.cap

tshark -q -n -r rsb2.cap -z “conv,ip” 分析流量总况

tshark -q -n -r rsb2.cap -z “conv,tcp” 分析每一个连接的流量、rtt、响应时间、丢包率、重传率等等

editcap -c 100000 ./rsb2.cap rsb00.cap //把大文件rsb2.cap按每个文件100000个package切成小文件

常用排错过滤条件:

对于排查网络延时/应用问题有一些过滤条件是非常有用的:

  • tcp.analysis.lost_segment:表明已经在抓包中看到不连续的序列号。报文丢失会造成重复的ACK,这会导致重传。
  • tcp.analysis.duplicate_ack:显示被确认过不止一次的报文。大量的重复ACK是TCP端点之间高延时的迹象。
  • tcp.analysis.retransmission:显示抓包中的所有重传。如果重传次数不多的话还是正常的,过多重传可能有问题。这通常意味着应用性能缓慢和/或用户报文丢失。
  • tcp.analysis.window_update:将传输过程中的TCP window大小图形化。如果看到窗口大小下降为零,这意味着发送方已经退出了,并等待接收方确认所有已传送数据。这可能表明接收端已经不堪重负了。
  • tcp.analysis.bytes_in_flight:某一时间点网络上未确认字节数。未确认字节数不能超过你的TCP窗口大小(定义于最初3此TCP握手),为了最大化吞吐量你想要获得尽可能接近TCP窗口大小。如果看到连续低于TCP窗口大小,可能意味着报文丢失或路径上其他影响吞吐量的问题。
  • tcp.analysis.ack_rtt:衡量抓取的TCP报文与相应的ACK。如果这一时间间隔比较长那可能表示某种类型的网络延时(报文丢失,拥塞,等等)。

抓包常用命令

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
#tshark 解析MySQL协议
tshark -r ./mysql-compress.cap -o tcp.calculate_timestamps:true -T fields -e mysql.caps.cp -e frame.number -e frame.time_epoch -e frame.time_delta_displayed -e ip.src -e tcp.srcport -e tcp.dstport -e ip.dst -e tcp.time_delta -e frame.time_delta_displayed -e tcp.stream -e tcp.len -e mysql.query

#用tcpdump抓取并保存包:
sudo tcpdump -i eth0 port 3306 -w drds.cap

#每隔3秒钟生成一个新文件,总共生成5个文件后(15秒后)终止抓包,然后包名也按时间规范好了
sudo tcpdump -t -s 0 tcp port 3306 -w 'dump_%Y-%m-%d_%H:%M:%S.pcap' -G 3 -W 5 -Z root

#每隔30分钟��成一个包并压缩
nohup sudo tcpdump -i eth0 -t -s 0 tcp and port 3306 -w 'dump_%Y-%m-%d_%H:%M:%S.pcap' -G 1800 -W 48 -Zroot -z gzip &

#file size 1000M
nohup sudo tcpdump -i eth0 -t -s 0 tcp and port 3306 -w 'dump_' -C 1000 -W 300 -Z root -z gzip &

#抓取详细SQL语句, 快速确认client发过来的具体SQL内容:
sudo tshark -i any -f 'port 8527' -s 0 -l -w - |strings
sudo tshark -i eth0 -d tcp.port==3306,mysql -T fields -e mysql.query 'port 3306'
sudo tshark -i eth0 -R "ip.addr==11.163.182.137" -d tcp.port==3306,mysql -T fields -e mysql.query 'port 3306'
sudo tshark -i eth0 -R "tcp.srcport==62877" -d tcp.port==3001,mysql -T fields -e tcp.srcport -e mysql.query 'port 3001'

#如果RDS开启了SSL,那么抓包后的内容tshark/wireshark分析不到MySQL的具体内容,可以强制关闭:connectionProperties里加上useSSL=false

tshark -r ./manager.cap -o tcp.calculate_timestamps:true -Y " tcp.analysis.retransmission " -T fields-e tcp.stream -e frame.number -e frame.time -e ip.src -e tcp.srcport -e tcp.dstport -e ip.dst | sort

#分析MySQL rt,倒数第四列基本就是rt
tshark -r gege_drds.pcap -Y " ((tcp.srcport eq 3306 ) and tcp.len>0 )" -o tcp.calculate_timestamps:true -T fields -e frame.number -e frame.time_epoch -e frame.time_delta_displayed -e ip.src -e tcp.srcport -e tcp.dstport -e ip.dst -e tcp.time_delta -e tcp.stream -e tcp.len -e tcp.analysis.ack_rtt

#或者排序一下
tshark -r 213_php.cap -Y "mysql.query or ( tcp.srcport==3306)" -o tcp.calculate_timestamps:true -T fields -e frame.number -e frame.time_epoch -e frame.time_delta_displayed -e ip.src -e tcp.srcport -e tcp.dstport -e ip.dst -e tcp.time_delta -e tcp.stream -e tcp.len -e mysql.query |sort -nk9 -nk1

Wireshark 插件安装

1
2
3
4
5
6
7
8
9
插件是使用lua开发的,安装比较简单,以OS X平台为例:
1. 将协议解析脚本copy到/Applications/Wireshark.app/Contents/Resources/share/wireshark/ 目录
2. 编辑init.lua文件,设置disable_lua = false,确保lua支持打开
3. 在init.lua文件末尾增加
dofile("hsf2.lua")

再次启动Wireshark,会对12200端口的数据流使用脚本解析,已经可以识别HSF协议了。

补充下Windows平台下的安装,步骤类似,将hsf2.lua拷贝到wireshark的根目录,例如c:\Program Files\Wireshark\,在这个目录下也有init.lua,然后参照上面的步骤2和3。

一个案例

问题:客户现场不管怎么样增加应用机器,tps就是上不去,同时增加机器后,增加的机器CPU还都能被用完,但是tps没有变化(这点比较奇怪) 整体服务调用慢,数据库没有慢查询,不知道到具体时间花在哪里,各个环节都尝试过增加服务器(或提升配置),但是问题一直得不到解决

tshark分析抓包文件数据库服务器网卡中断瓶颈导致rtt非常高,进一步导致每个Query的ResponseTime非常高(图中左边都是出问题、右边都是问题解决后的响应时间)

下面两个图是吧tshark解析结果丢到了数据库中好用SQL可以进一步分析

image.png

** 问题修复后数据库每个查询的平均响应时间从47毫秒下降到了4.5毫秒 **

image.png

从wireshark中也可以看到类似的rtt不正常(超过150ms的比较多)

image.png

从wireshark中也可以看到类似的rtt正常(99%都在10ms以内)

image.png

tcprstat

推荐一个快速统计rt的工具tcprstat,实测在CPU打满的高压力情况下会丢失大量请求数据,但是作为统计平均值这问题不大。支持http、mysql协议等。实际测试在每秒2万个SQL的时候,对于一台4C的机器,只能采集到70%的请求。

或者看这个支持设置RT阈值的统计改进版

tcprstat 统计抓包离线文件

1
2
3
4
5
# -l 166.100.128.148  向目标端口80发起请求的地址
# -p 80 发出response端口
# -t 10 间隔10s一次汇总统计
# -f 后面的分位置可以随便指定(90%、95%、99%等)
tcprstat -r pts.pcap -p 80 -l 166.100.128.148 -t 10 -f "%T\t%n\t%M\t%m\t%a\t%h\t%S\t%95M\t%90a\t%95S\t%99M\t%99a\t%90S\n"

其它工具 packetdrill

https://github.com/google/packetdrill

https://mp.weixin.qq.com/s/CcM3rINPn54Oean144kvMw

http://beta.computer-networking.info/syllabus/default/exercises/tcp-2.html

https://segmentfault.com/a/1190000019193928

就是要你懂TCP--性能优化大全

TCP性能优化大全

先从一个问题看起,客户通过专线访问云上的DRDS,专线100M,时延20ms,一个SQL查询了22M数据,结果花了大概25秒,这慢得不太正常,如果通过云上client访问云上DRDS那么1-2秒就返回了。如果通过http或者scp传输这22M的数据大概两秒钟也传送完毕了,所以这里问题的原因基本上是DRDS在这种网络条件下有性能问题,需要找出为什么。

抓包 tcpdump+wireshark

这个查询结果22M的需要25秒,如下图(wireshark 时序图),横轴是时间纵轴是sequence number:

image.png

粗一看没啥问题,把这个图形放大看看

image.png

换个角度,看看窗口尺寸图形:

image.png

从bytes in flight也大致能算出来总的传输时间 16K*1000/20=800Kb/秒

DRDS会默认设置 socketSendBuffer 为16K:

socket.setSendBufferSize(16*1024) //16K send buffer

来看一下tcp包发送流程:

(图片来自:https://www.atatech.org/articles/9032)

如果sendbuffer不够就会卡在上图中的第一步 sk_stream_wait_memory, 通过systemtap脚本可以验证:

#!/usr/bin/stap
# Simple probe to detect when a process is waiting for more socket send
# buffer memory. Usually means the process is doing writes larger than the
# socket send buffer size or there is a slow receiver at the other side.
# Increasing the socket's send buffer size might help decrease application
# latencies, but it might also make it worse, so buyer beware.
#
# Typical output: timestamp in microseconds: procname(pid) event
#
# 1218230114875167: python(17631) blocked on full send buffer
# 1218230114876196: python(17631) recovered from full send buffer
# 1218230114876271: python(17631) blocked on full send buffer
# 1218230114876479: python(17631) recovered from full send buffer

probe kernel.function("sk_stream_wait_memory")
{
	printf("%u: %s(%d) blocked on full send buffer\n",
		gettimeofday_us(), execname(), pid())
}

probe kernel.function("sk_stream_wait_memory").return
{
	printf("%u: %s(%d) recovered from full send buffer\n",
		gettimeofday_us(), execname(), pid())
}

如果tcp发送buffer也就是SO_SNDBUF只有16K的话,这些包很快都发出去了,但是这16K不能立即释放出来填新的内容进去,因为tcp要保证可靠,万一中间丢包了呢。只有等到这16K中的某些ack了,才会填充一些进来然后继续发出去。由于这里rt基本是20ms,也就是16K发送完毕后,等了20ms才收到一些ack,这20ms应用、内核什么都不能做,所以就是如第二个图中的大概20ms的等待平台。这块请参考这篇文章

sendbuffer相当于发送仓库的大小,仓库的货物都发走后,不能立马腾出来发新的货物,而是要等发走的获取对方确认收到了(ack)才能腾出来发新的货物, 仓库足够大了之后接下来的瓶颈就是高速公路了(带宽、拥塞窗口)

如果是UDP,就没有send buffer的概念,有数据统统发出去,根本不关心对方是否收到。

几个发送buf相关的内核参数

vm.lowmem_reserve_ratio = 256   256     32
net.core.wmem_max = 1048576
net.core.wmem_default = 124928
net.ipv4.tcp_wmem = 4096        16384   4194304
net.ipv4.udp_wmem_min = 4096

net.ipv4.tcp_wmem 默认就是16K,而且是能够动态调整的,只不过我们代码中这块的参数是很多年前从Corba中继承过来的,一直没有修改。代码中设置了这个参数后就关闭了内核的动态调整功能,所以能看到http或者scp都很快。

接收buffer是有开关可以动态控制的,发送buffer没有开关默认就是开启,关闭只能在代码层面来控制

net.ipv4.tcp_moderate_rcvbuf

优化

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

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

image.png

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

BDP 带宽时延积

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

因为BDP是250K,也就是拥塞窗口即将成为新的瓶颈,所以调大buffer没意义了。

用tc构造延时和带宽限制的模拟重现环境

sudo tc qdisc del dev eth0 root netem delay 20ms
sudo tc qdisc add dev eth0 root tbf rate 500kbit latency 50ms burst 15kb

这个案例的结论

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

平时看到的一些理论在实践中用起来比较难,最开始看到抓包结果的时候比较怀疑发送、接收窗口之类的,没有直接想到send buffer上,理论跟实践的鸿沟

需要调整tcp_rmem 的问题 Case

发送和接收Buffer对性能的完整影响参考这篇

总结下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 到最初始的状态。
  • SO_SNDBUF、SO_RCVBUF 发送、接收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

这些经验都来自CDN @辟拾 的 网络优化 - TCP 是如何做到提速 20 倍的

重要参数

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 后,服务器会重新进入慢启动流程。

这种友善的保护机制,但是对于目前的网络坏境没必要这么谨慎和彬彬有礼,建议将此功能关闭,以提高长连接环境下的用户体验感。

 sysctl net.ipv4.tcp_slow_start_after_idle=0

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

ss -itn dst  11.163.187.32 |grep -v "Address:Port" | xargs -L 1 | grep ssthresh

ESTAB 0 0 11.163.187.33:33833 11.163.187.32:2181 cubic wscale:7,7 rto:201 rtt:0.16/0.186 ato:40 mss:1448 cwnd:10 ssthresh:7 send 724.0Mbps lastsnd:2813 lastrcv:2813 lastack:2813 pacing_rate 1445.7Mbps rcv_rtt:52081.5 rcv_space:29344
ESTAB 0 0 11.163.187.33:2376 11.163.187.32:46793 cubic wscale:7,7 rto:201 rtt:0.169/0.137 ato:40 mss:1448 cwnd:59 ssthresh:48 send 4044.1Mbps lastsnd:334 lastrcv:409 lastack:334 pacing_rate 8052.5Mbps retrans:0/759 reordering:34 rcv_rtt:50178 rcv_space:137603
ESTAB 0 0 11.163.187.33:33829 11.163.187.32:2181 cubic wscale:7,7 rto:201 rtt:0.065/0.002 ato:40 mss:1448 cwnd:10 ssthresh:7 send 1782.2Mbps lastsnd:2825 lastrcv:2825 lastack:2825 pacing_rate 3550.7Mbps rcv_rtt:51495.8 rcv_space:29344
ESTAB 0 0 11.163.187.33:33828 11.163.187.32:2181 cubic wscale:7,7 rto:201 rtt:0.113/0.061 ato:40 mss:1448 cwnd:10 ssthresh:7 send 1025.1Mbps lastsnd:2826 lastrcv:2826 lastack:2826 pacing_rate 2043.5Mbps rcv_rtt:54801.8 rcv_space:29344
ESTAB 0 0 11.163.187.33:2376 11.163.187.32:47047 cubic wscale:7,7 rto:206 rtt:5.977/9.1 ato:40 mss:1448 cwnd:10 ssthresh:51 send 19.4Mbps lastsnd:522150903 lastrcv:522150906 lastack:522150903 pacing_rate 38.8Mbps retrans:0/44 reordering:31 rcv_rtt:86067 rcv_space:321882
ESTAB 0 0 11.163.187.33:2376 11.163.187.32:46789 cubic wscale:7,7 rto:201 rtt:0.045/0.003 ato:40 mss:1448 cwnd:10 ssthresh:9 send 2574.2Mbps lastsnd:522035639 lastrcv:1589957951 lastack:522035639 pacing_rate 5077.9Mbps retrans:0/12 reordering:20 rcv_space:28960
ESTAB 0 0 11.163.187.33:33831 11.163.187.32:2181 cubic wscale:7,7 rto:201 rtt:0.071/0.01 ato:40 mss:1448 cwnd:10 ssthresh:7 send 1631.5Mbps lastsnd:2825 lastrcv:2825 lastack:2825 pacing_rate 3263.1Mbps rcv_rtt:54805.8 rcv_space:29344

从系统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

如果因为之前的网络状况等其它原因导致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

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的增加

以上经验来自 @俞青 同学的 tcp metrics 在长肥网络下引发性能问题

经典的 nagle 和 dalay ack对性能的影响

请参考这篇文章:就是要你懂 TCP– 最经典的TCP性能问题

最后的经验

抓包解千愁


就是要你懂TCP相关文章:

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

MSS和MTU导致的悲剧

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

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

参考文章:

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

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

tcp_rmem case

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

就是要你懂负载均衡--lvs和转发模式

基础知识的力量–lvs和转发模式

本文希望阐述清楚LVS的各种转发模式,以及他们的工作流程和优缺点,同时从网络包的流转原理上解释清楚优缺点的来由,并结合阿里云的slb来说明优缺点。

大家都背过LVS的几种转发模式,DR模式性能最好但是部署不灵活;NAT性能差部署灵活多了…… 实际都是没理解好这几个模式背后代表的网络连通性的原理和网络包路由原理,导致大多时候都是死背那几个概念。

如果我们能从网络包背后流转的流程和原理来看LVS的转发模式,那么那些优缺点简直就太直白了,这就是基础知识的力量。

如果对网络包是怎么流转的不太清楚,推荐先看这篇基础:程序员的网络知识 – 一个网络包的旅程 ,对后面理解LVS的各个转发模式非常有帮助。

几个术语和缩写

cip:Client IP,客户端地址
vip:Virtual IP,LVS实例IP
rip:Real IP,后端RS地址
RS: Real Server 后端真正提供服务的机器
LB: Load Balance 负载均衡器
LVS: Linux Virtual Server
sip: source ip
dip: destination

LVS的几种转发模式

  • DR模型 – (Director Routing-直接路由)
  • NAT模型 – (NetWork Address Translation-网络地址转换)
  • fullNAT – (full NAT)
  • ENAT – (enhence NAT 或者叫三角模式/DNAT,阿里云提供)
  • IP TUN模型 – (IP Tunneling - IP隧道)

DR模型(Director Routing–直接路由)

image.png

如上图所示基本流程(假设 cip 是200.200.200.2, vip是200.200.200.1):

  1. 请求流量(sip 200.200.200.2, dip 200.200.200.1) 先到达 LVS(图中Director)
  2. 然后LVS,根据负载策略挑选众多 RS中的一个,然后将这个网络包的MAC地址修改成这个选中的RS的MAC
  3. 然后丢给Director,Director将这个包丢给选中的RS
  4. 选中的RS看到MAC地址是自己的、dip也是自己的,愉快地收下并处理、回复
  5. 回复包(sip 200.200.200.1, dip 200.200.200.2)
  6. 经过交换机直接回复给client了(不再走LVS)

我们看到上面流程,请求包到达LVS后,LVS只对包的目的MAC地址作了修改,回复包直接回给了client。

同时要求多个RS和LVS(Director)都配置的是同一个IP地址,但是用的不同的MAC。这就要求所有RS和LVS在同一个子网,在二层路由不需要IP,他们又在同一个子网,所以这里联通性没问题。

RS上会将vip配置在lo回环网卡上,同时route中添加相应的规则,这样在第四步收到的包能被os正常处理。

image.png

优点:

  • DR模式是性能最好的一种模式,入站请求走LVS,回复报文绕过LVS直接发给Client

缺点:

  • 要求LVS和rs在同一个子网,扩展性不够好;
  • RS需要配置vip同时特殊处理arp;
  • 配置比较复杂;
  • 不支持端口映射。

为什么要求LVS和RS在同一个vlan(或者说同一个二层网络里)

因为DR模式依赖多个RS和LVS共用同一个VIP,然后依据MAC地址来在LVS和多个RS之间路由,所以LVS和RS必须在一个vlan或者说同一个二层网络里

DR 模式为什么性能最好

因为回复包不走LVS了,大部分情况下都是请求包小,回复包大,LVS很容易成为流量瓶颈,同时LVS只需要修改进来的包的MAC地址。

DR 模式为什么回包不需要走LVS了

因为RS和LVS共享同一个vip,回复的时候RS能正确地填好sip为vip,不再需要LVS来多修改一次(后面讲的NAT、Full NAT都需要)

总结下 DR的结构

image.png

绿色是请求包进来,红色是修改过MAC的请求包,SW是一个交换机。

NAT模型(NetWork Address Translation - 网络地址转换)

nat模式的结构图如下:

image.png

基本流程:

  1. client发出请求(sip 200.200.200.2,dip 200.200.200.1)
  2. 请求包到达LVS(图中Director),LVS修改请求包为(sip 200.200.200.2, dip rip)
  3. 请求包到达rs, rs回复(sip rip,dip 200.200.200.2)
  4. 这个回复包不能直接给client,因为rip不是VIP会被reset掉(client看到的连接是vip,突然来一个rip就reset)
  5. 但是因为lvs是网关,所以这个回复包先走到网关,网关有机会修改sip
  6. 网关修改sip为VIP,修改后的回复包(sip 200.200.200.1,dip 200.200.200.2)发给client

image.png

优点:

  • 配置简单
  • 支持端口映射(看名字就知道)
  • RIP一般是私有地址,主要用户LVS和RS之间通信

缺点:

  • LVS和所有RS必须在同一个vlan
  • 进出流量都要走LVS转发
  • LVS容易成为瓶颈
  • 一般而言需要将VIP配置成RS的网关

为什么NAT要求lvs和RS在同一个vlan

因为回复包必须经过lvs再次修改sip为vip,client才认,如果回复包的sip不是client包请求的dip(也就是vip),那么这个连接会被reset掉。如果LVS不是网关,因为回复包的dip是cip,那么可能从其它路由就走了,LVS没有机会修改回复包的sip

总结下NAT结构

image.png

注意这里LVS修改进出包的(sip, dip)的时候只改了其中一个,所以才有接下来的full NAT。当然NAT最大的缺点是要求LVS和RS必须在同一个vlan,这样限制了LVS集群和RS集群的部署灵活性,尤其是在阿里云这种对外售卖的公有云环境下,NAT基本不实用。

full NAT模型(full NetWork Address Translation-全部网络地址转换)

基本流程(类似NAT):

  1. client发出请求(sip 200.200.200.2 dip 200.200.200.1)
  2. 请求包到达lvs,lvs修改请求包为**(sip 200.200.200.1, dip rip)** 注意这里sip/dip都被修改了
  3. 请求包到达rs, rs回复(sip rip,dip 200.200.200.1)
  4. 这个回复包的目的IP是VIP(不像NAT中是 cip),所以LVS和RS不在一个vlan通过IP路由也能到达lvs
  5. lvs修改sip为vip, dip为cip,修改后的回复包(sip 200.200.200.1,dip 200.200.200.2)发给client

优点:

  • 解决了NAT对LVS和RS要求在同一个vlan的问题,适用更复杂的部署形式

缺点:

  • RS看不到cip(NAT模式下可以看到)
  • 进出流量还是都走的lvs,容易成为瓶颈(跟NAT一样都有这个问题)

为什么full NAT解决了NAT中要求的LVS和RS必须在同一个vlan的问题

因为LVS修改进来的包的时候把(sip, dip)都修改了(这也是full的主要含义吧),RS的回复包目的地址是vip(NAT中是cip),所以只要vip和rs之间三层可通就行,这样LVS和RS可以在不同的vlan了,也就是LVS不再要求是网关,从而LVS和RS可以在更复杂的网络环境下部署。

为什么full NAT后RS看不见cip了

因为cip被修改掉了,RS只能看到LVS的vip,在阿里内部会将cip放入TCP包的Option中传递给RS,RS上一般部署自己写的 toa(Tcp Option as Address)模块来从Options中读取的cip,这样RS能看到cip了, 当然这不是一个开源的通用方案。

总结下full NAT的结构

image.png

注意上图中绿色的进包和红色的出包他们的地址变化

那么到现在full NAT解决了NAT的同vlan的要求,基本上可以用于公有云了,但是还是没解决进出流量都走LVS的问题(LVS要修改进出的包)。

比较下NAT和Full NAT

两者进出都要走LVS,NAT必须要求vip是RS的网关,这个限制在公有云这种应用场景下不能忍,于是Full NAT通过修改请求包的source ip,将原来的source ip从cip改成vip,这样RS回复的时候回复包的目标IP也是vip,所以LVS和RS之间不再要求是同一vlan的关系了。当然带来了新的问题,RS看不见cip了(这个可以通过自定义的vtoa模块来复原)

那么有没有一个方案能够像full NAT一样不限制lvs和RS之间的网络关系,同时出去的流量跟DR模式一样也不走LVS呢?

比较下DR、NAT和Full NAT

DR只修改目标Mac地址;
NAT只修改目标IP,LVS做网关得到修改回包的机会,RS能看到client ip;
Full-NAT同时修改 源ip和 目标ip, LVS通过三层路由和RS相通,RS看到的源ip是LVS IP。

阿里云的ENAT模式(enhence NAT) 或者叫 三角模式

前后端都是经典类型,属于NAT模式的特例,LVS转发给RS报文的源地址是客户端的源地址。

与NAT模式的差异在于 RS响应客户端的报文不再经过LVS机器,而是直接发送给客户端(源地址是VIP的地址, 后端RS需要加载一个ctk模块, lsmod | grep ctk 确认 ,主要是数据库产品使用)

优点:

  • 不要求LVS和RS在同一个vlan
  • 出去的流量不需要走LVS,性能好

缺点:

  • 阿里集团内部实现的自定义方案,需要在所有RS上安装ctk组件(类似full NAT中的vtoa)

基本流程:

  1. client发出请求(cip,vip)
  2. 请求包到达lvs,lvs修改请求包为(vip,rip),并将cip放入TCP Option中
  3. 请求包根据ip路由到达rs, ctk模块读取TCP Option中的cip
  4. 回复包(RIP, vip)被ctk模块截获,并将回复包改写为(vip, cip)
  5. 因为回复包的目的地址是cip所以不需要经过lvs,可以直接发给client

ENAT模式在内部也会被称为 三角模式或者DNAT/SNAT模式

为什么ENAT的回复包不需要走回LVS了

因为之前full NAT模式下要走回去是需要LVS 再次改写回复包的IP,而ENAT模式下,这件事情在RS上被ctk模块提前做掉了

为什么ENAT的LVS和RS可以在不同的vlan

跟full NAT一样

总结下 ENAT的结构

image.png

最后说一下不太常用的 TUN模型

IP TUN模型(IP Tunneling - IP隧道)

基本流程:

  1. 请求包到达LVS后,LVS将请求包封装成一个新的IP报文
  2. 新的IP包的目的IP是某一RS的IP,然后转发给RS
  3. RS收到报文后IPIP内核模块解封装,取出用户的请求报文
  4. 发现目的IP是VIP,而自己的tunl0网卡上配置了这个IP,从而愉快地处理请求并将结果直接发送给客户

优点:

  • 集群节点可以跨vlan
  • 跟DR一样,响应报文直接发给client

缺点:

  • RS上必须安装运行IPIP模块
  • 多增加了一个IP头
  • LVS和RS上的tunl0虚拟网卡上配置同一个VIP(类似DR)

DR模式中LVS修改的是目的MAC

为什么IP TUN不要求同一个vlan

因为IP TUN中不是修改MAC来路由,所以不要求同一个vlan,只要求lvs和rs之间ip能通就行。DR模式要求的是lvs和RS之间广播能通

IP TUN性能

回包不走LVS,但是多做了一次封包解包,不如DR好

总结下 IP TUN的结构

image.png

图中红线是再次封装过的包,ipip是操作系统的一个内核模块。

DR可能在小公司用的比较多,IP TUN用的少一些,相对而言NAT、FullNAT、ENAT这三种在集团内部比较类似,用的也比较多,他们之间的可比较性很强,所以放在一块了。

阿里云 SLB 的 FNAT

本质就是前面所讲的 fullnat模式,为了解决RS看不到真正的client ip问题,在阿里云公网上的物理机/宿主机默认都会帮你将source-ip(本来是lvs ip)替换成真正的client ip,这样当包进到ecs的时候source ip已经是client ip了,所以slb默认的fnat模式会让你直接能拿到client ip。回包依然会经过lvs(虽然理论上可以不需要了,但是要考虑rs和client不能直接通,以及管理方便等)

这个进出的替换过程在物理机/宿主机上是avs来做,如果没有avs就得安装slb的toa模块来做了。

这就是为什么slb比直接用lvs要方便些,也就是云服务商提供这种云产品的价值所在。

但是进出流量都走lvs,导致lvs流量过大,大象流容易打挂单core(目前限制单流不超过5GB),时延有所增加

所以推出NGLB来解决这个问题

阿里云的NGLB

下一代负载均衡,只有首包经过slb节点,后续client和RS直接通信,只支持RS是物理机的场景。这个模块slb基本没有负载,性能最好。

NGLB_pic.png

SLB模块简介

  1. toa模块主要用在Classic网络SLB/ALB的FNAT场景下后端RS(NC)获取实际Client端的真实地址和端口(FNAT模式下SLB/ALB发送给后端RS的报文中源IP已经被替换为SLB/ALB的localIP,将ClientIP[后续简写为cip]通过tcp option携带到RS),用户通过特定getpeername接口获取cip。toa模块已经内置到ali内核版本中,无需再单独安装(见/lib/modules/uname -r/kernel/net/toa/toa.ko)。
  2. vtoa模块属于增强版toa,同时支持VPC网络和Classic网络SLB/ALB的FNAT场景下后端RS获取实际客户端的真实地址和端口(FNAT模式下SLB/ALB发送给后端RS的报文中源IP已经被替换为SLB/ALB的localIP,将cip通过tcp option携带到RS),用户通过特定getsockopt接口获取vid:vip:vport和cip:cport,兼容toa接口。
  3. ctk: 包括ALB_ctk_debugfs.ko,ALB_ctk_session.ko,ALB_ctk_proxy.ko模块。ctk是一个NAT模块,对于ENAT场景,从ALB过来的带tcp option的tcp流量(cip:cport<->rip:rport带vip:vport opt)做了DNAT和反向DNAT转换,使得到上层应用层时看到的流被恢复为原始形态(cip:cport<->vip:vport)
  4. vctk:VPC NGLB模式下,只有建立TCP连接的首包(SYN包)经过ALB转发,后端vctk做Local的SNAT(避免VPC间地址冲突)和DNAT, 返回包做反向SNAT和DNAT转换,再做VXLAN封装,直接返回Source NC。

!!注意:一般来说,ctk与toa/vtoa模块不同时使用,toa和vtoa不同时使用:

vtoa模块的功能是toa模块的超集,也就是说toa提供的功能在vtoa模块中都是提供的,并且接口,功能都是保持不变的。所以加载了vtoa之后,就不需要加载toa模块,如果加载了vtoa后再加载toa,获取vpcid以及cip/vip可能失败。

当toa/vtoa单独工作时,toa/vtoa模块工作在tcp层,通过修改内核把tcp opt中的cip,rip保存在sock结构中,并通过getpeername/getsockname系统接口给用户提供服务。

如果同时加载ctk和toa/vtoa模块,FNAT场景下ctk不起作用;ENAT场景下, 因ctk工作在IP层(NAT),tcp opt先被ctk处理并去除并保存在session中,vtoa接口依赖ctk的session获取toa/vtoa信息。

阿里云 SLB 的双机房高可用

主备模式,备用机房没有流量。

SLB 的双机房容灾主要通过lvs机器和网络设备lsw之间通过动态路由协议(OSPF、ECMP、BGP)发布大小段路由实现主备机房容灾。10G集群采用ospf协议,40G集群采用bgp协议。

案例

主机房通过bgp协议发送 /27 的路由到lsw,csr,备机房发布 /26 路由到lsw, csr。

正常情况下,如果应用访问192.168.0.2的话,路由器会选择掩码最长的路由为最佳路由,获选进入路由表,也就是会选择192.168.0.1/27这条路由。从而实现流量主要在主机房,备机房冷备的效果。
当主机房发生故障,仅当主机房所有lvs机器都不能提供服务,即ABTN中无法收到主机房的/27明细路由时,流量才会发生主备切换,切换到备机房,实现主备机房容灾。

image.png

LVS节点之间的均衡

内核版的lvs 最开始就采用集群化的部署,每个group 4台lvs 机器,支持group 级别横向扩展。使用ospf 作为引流方式。每台lvs机器有两块10G 网卡T1、T2口,分别上联lsw1 和 lsw2,通过ospf 动态路由协议与lsw 之间建立邻居关系,四台lvs机器发布相同的network 给lsw,实现流量转发的ecmp。lsw 打开multicast 以支持4台lvs机器之间的session 同步。通过session 同步保证当单台lvs机器宕机或者下线时,长连接 rehash 到其他lvs 机器时能够继续转发而不产生中断。

LVS节点单机高可用

每台lvs机器有两块10G网卡,每块网卡上联一台lsw,单机双上联容灾;

LVS Group

每个lvs_group 4台lvs 机器,同group机器提供对等服务,同时4台lvs机器之间有实时的session 同步,发生单机宕机的场景,流量会均摊到同组其他lvs机器上,长连接可以保持不断;

一些数据

内核版的lvs只支持10G带宽,采用dpdk后能支持25、40G带宽。

dpdk基于内核的uio机制,提供了PMD(Poll Mode Driver)的收包模式,uio旁路了内核,主动轮询去掉硬中断,DPDK从而可以在用户态做收发包处理。带来Zero Copy、无系统调用的好处,同步处理减少上下文切换带来的Cache Miss。

另外dpdk也采用了hugepage,LVS使用单页内存1G,基本上避免了TLB MISS,对于LVS这种内存大户来说,对性能提升非常有利。并且dpdk提供了一系列高质量的基础库比如内存池(Mempool)、MBuf、无锁环(Ring),便于开发者迅速构建自己的包转发平台。

性能优化

LVS侧针对内存的访问所做的优化如下:

1.session/svc 数据结构调整 热点字段聚集到同个cache line

2.结合性能测试数据,调整session的prefecth

3.消除false sharing。

目前LVS 40G机型,单机4块 40G网卡。

平均包长1k的情况下能跑满4个网口(160G)

64bytes小包的转发pps为4200W,kernel版本为1000W。

限流对性能的影响

通过令牌桶限流的话令牌桶加锁就是瓶颈

lvs的优化方案为大小桶算法:

per core维护一个小的令牌桶,当小桶中的令牌取完之后,才会加锁从大桶中获取,如果大桶中也拿不到令牌,本周期(令牌更新间隔)内也不会再次访问大桶。

从而去除每包必须加锁访问令牌桶,降低中心化限速对性能的影响。

单流瓶颈

四层负载均衡lvs作为阿里云的核心产品已经走过了10个年头,在这期间lvs不断的进行技术的革新和演进,从最初的单机10g内核版本、10g用户态到现在主流的线上40g的版本,机器的带宽越来越大,cpu核数越来越多处理能力也越来越强,但存在一个问题一直没有解,对于同一条流会hash分到同一个cpu上,如果是单流的流量比较大超过lvs单核的处理能力,就会导致lvs的单cpu使用率飙高从而导致丢包。mellnex cx5 100g网卡平台提供了流offload的能力,lvs基于该硬件的特性开发了offload的功能,可以将大象流offload到网卡中防止单流消耗cpu的性能。

经测试offload后最高性能单卡单流可以达到2800wpps,具备应对大象流的能力。

经过十年来的不断演进,目前SLB四层监听的单LVS集群,已经可以达到PPS 4亿,网卡单向带宽1.6T,单集群新建连接8000w,并发13.4亿以及Offload单流2800万PPS的处理能力。

参考资料

LVS 20倍的负载不均衡,原来是内核的这个Bug

章文嵩(正明)博士和他背后的负载均衡(LOAD BANLANCER)帝国

https://yizhi.ren/2019/05/03/lvs/

就是要你懂DNS--一文搞懂域名解析相关问题

一文搞懂域名解析DNS相关问题

本文希望通过一篇文章解决所有域名解析中相关的问题

最后会通过实际工作中碰到的不同场景下几个DNS问题的分析过程来理解DNS

这几个Case描述如下:

  1. 一批ECS nslookup 域名结果正确,但是 ping 域名 返回 unknown host
  2. Docker容器中的域名解析过程和原理
  3. 中间件的VipClient服务在centos7上域名解析失败
  4. 在公司网下,我的windows7笔记本的wifi总是报dns域名异常无法上网(通过IP地址可以上网)

因为这些问题都不一样,但是都跟DNS服务相关所以打算分四篇文章挨个介绍,希望看完后能加深对DNS原理的理解并独立解决任何DNS问题。

下面我们就先开始介绍下DNS解析原理和流程。

Linux下域名解析流程

  • DNS域名解析的时候先根据 /etc/host.conf、/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顶上】

image.png

上述描述主要是阐述的图中 stub resolver部分的详细流程。这部分流程出问题才是程序员实际中更多碰到的场景

所以默认的nsswitch流程是

image.png

以下是一个 /etc/nsswitch.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# cat /etc/nsswitch.conf |grep -v "^#" |grep -v "^$"
passwd: files sss
shadow: files sss
group: files sss
hosts: files dns myhostname <<<<< 重点是这一行三个值的顺序
bootparams: nisplus [NOTFOUND=return] files
ethers: files
netmasks: files
networks: files
protocols: files
rpc: files
services: files sss
netgroup: nisplus sss
publickey: nisplus
automount: files nisplus sss
aliases: files nisplus

这个配置中的解析顺序是:files->dns->myhostname, 这个顺序可以调整和配置。

Linux下域名解析流程需要注意的地方

Linux下域名解析诊断工具

  • ping
  • nslookup (nslookup domain @dns-server-ip)
  • dig (dig +trace domain)
  • tcpdump (tcpdump -i eth0 host server-ip and port 53 and udp)
  • strace

img

1
2
//指定本地 ip 端口:192.168.0.201#20202,将 dns 解析任务发送给 172.21.0.10 
dig +retry=0 -b192.168.0.201#20202 aliyun.com @172.21.0.10

案例

如下,向 /etc/hosts 中添加两条记录,一条是test.unknow.host 无法解析到,但是另一条 test.localhost 可以解析到,为啥呢?

$head -2 /etc/hosts
127.0.0.1  test.unknow.host
127.0.0.1   test.localhost
$ping test.unknow.host
ping: unknown host test.unknow.host
$ping -c 1 test.localhost
PING test.localhost (127.0.0.1) 56(84) bytes of data.
64 bytes from test.localhost (127.0.0.1): icmp_seq=1 ttl=64 time=0.016 ms

为什么 test.unknow.host 没法解析到? 可能有哪些因素导致这种现象?尝试 ping -c 1 test.localhost 的目的是做什么?

看完前面的理论我的猜测是两种可能导致这种情况:

  • /etc/hosts 没有启用
  • 有本地缓存记录了一个unknow host记录

验证

strace -e trace=open -f ping -c 1 test.localhost

可以通,说明 /etc/hosts 是在起作用的,所以最好验证 /etc/hosts 在起作用的方法是往其中添加一条新纪录,然后验证一下

那接下来只能看本地有没有启动 nscd 这样的缓存了,见后发现也没有,这个时候就可以上 strace 追踪ping的流程了
undefined

从上图可以清晰地看到读取了 /etc/host.conf, 然后读了 /etc/hosts, 再然后读取到我们添加的那条记录,似乎没问题,仔细看这应该是 ip地址后面带的是一个中文字符的空格,这就是问题所在。

到这里可能的情况要追加第三种了:

  • /etc/hosts 中添加的记录没生效(比如中文符号)

dhcp

如果启用了dhcp,那么dhclient会更新在Network Manager启动的时候更新 /etc/resolv.conf

dnsmasq

一般会在127.0.0.1:53上启动dns server服务,配置文件对应在:/run/dnsmasq/resolv.conf。集团内部的vipclient就是类似这个原理。

微服务下的域名解析、负载均衡

微服务中多个服务之间一般都是通过一个vip或者域名之类的来做服务发现和负载均衡、弹性伸缩,所以这里也需要域名解析(一个微服务申请一个域名)

域名解析通过jar、lib包

基本与上面的逻辑没什么关系,jar包会去通过特定的协议联系server,解析出域名对应的多个ip、机房、权重等

域名解析通过dns server

跟前面介绍逻辑一致,一般是/etc/resolv.conf中配置的第一个nameserver负责解析微服务的域名,解析不到的(如baidu.com)再转发给上一级通用的dns server,解析到了说明是微服务自定义的域名,就可以返回来了

如果这种情况下/etc/resolv.conf中配置的第一个nameserver是127.0.0.1,意味着本地跑了一个dns server, 这个服务使用dns协议监听本地udp 53端口

验证方式: nslookup 域名 @127.0.0.1 看看能否解析到你想要的地址

kubernetes 和 docker中的域名解析

一般是通过iptables配置转发规则来实现,这种用iptables和tcpdump基本都可以看清楚。如果是集群内部的话可以通过CoreDNS来实现,通过K8S动态向CoreDNS增删域名,增删ip,所以这种域名肯定只能在k8s集群内部使用

nginx 中的域名解析

nginx可以自定义resolver,也可以通过读取 /etc/resolv.conf转换而来,要注意对 /etc/resolv.conf中 注释的兼容

https://github.com/blacklabelops-legacy/nginx/issues/36 可能是nginx读取 /etc/resolv.conf没有处理好 # 注释的问题

进一步的Case学习:

  1. 一批ECS nslookup 域名结果正确,但是 ping 域名 返回 unknown host
  2. Docker容器中的域名解析过程和原理
  3. 中间件的VipClient服务在centos7上域名解析失败
  4. 在公司网下,我的windows7笔记本的wifi总是报dns域名异常无法上网(通过IP地址可以上网)

参考文章:

GO DNS 原理解析

Linux 系统如何处理名称解析

Anatomy of a Linux DNS Lookup – Part I

史上最全 SSH 暗黑技巧详解

史上最全 SSH 暗黑技巧详解

我见过太多的老鸟、新手对ssh 基本只限于 ssh到远程机器,实际这个命令我们一天要用很多次,但是对它的了解太少了,他的强大远远超出你的想象。当于你也许会说够用就够了,确实没错,但是你考虑过效率没有,或者还有哪些脑洞大开的功能会让你爱死他,这些功能又仅仅是一行命令就够了。

疫情期间一行SSH命令让我节省了70%的出差时间,来,让我们一起走一遍,看看会不会让你大开眼界

本文试图解决的问题

  • 如何通过ssh命令科学上网
  • docker 镜像、golang仓库总是被墙怎么办
  • 公司跳板机要输入动态token,太麻烦了,如何省略掉这个token;
  • 比如多机房总是要走跳板机,如何绕过跳板机直连;
  • 我的开发测试机器如何免打通、免密码、直达;
  • 如何访问隔离环境中(k8s)的Web服务 – 将隔离环境中的web端口映射到本地
  • 如何让隔离环境的机器用上yum、apt
  • 如何将服务器的图形界面映射到本地(类似vnc的作用)
  • ssh如何调试诊断,这才是终极技能……

注意事项

  • ssh是指的openSSH 命令工具
  • 本文适用于各种Linux、macOS下命令行操作,Windows的话各种可视化工具都可以复制session、配置tunnel来实现类似功能。
  • 如果文章中提到的文件、文件夹不存在可以直接创建出来。
  • 所有配置都是在你的笔记本上(相当于ssh client上)

科学上网

有时候科学上网还得靠自己,一行ssh命令来科学上网:

1
nohup ssh -qTfnN -D 127.0.0.1:38080 root@1.1.1.1 "vmstat 10" 2>&1 >/dev/null &

上面的 1.1.1.1 是你在境外的一台服务器,已经做好了免密登陆(免密见后面,要不你还得输一下密码),这句话的意思就是在本地启动一个38080的端口,上面收到的任何东西都会转发给 1.1.1.1:22(做了ssh加密),1.1.1.1:22 会解密收到的东西,然后把他们转发给google之类的网站(比如你要访问的是google),结果依然通过原路返回

127.0.0.1:38080 socks5 就是要填入到你的浏览器中的代理服务器,什么都不需要装,非常简单

image.png

原理图如下(灰色矩形框就是你本地ssh命令,ssh 线就是在穿墙, 国外服务器就是命令中的1.1.1.1):
undefined

科学上网之http特殊代理–利用ssh 本地转发是HTTP协议

前面所说的代理是socks5代理,一般浏览器都有插件支持,但是比如你的docker(或者其他程序)需要通过http去拉取镜像就会出现如下错误:

Sending build context to Docker daemon 8.704 kB
Step 1 : FROM k8s.gcr.io/kube-cross:v1.10.1-1
Get https://k8s.gcr.io/v1/_ping: dial tcp 108.177.125.82:443: i/o timeout

如果是git这样的应用内部可以配置socks5和http代理服务器,请参考另外一篇文章,但是有些应用就不能配置了,当然最终通过ssh大法还是可以解决这个问题:

sudo ssh -L 443:108.177.125.82:443 root@1.1.1.1 //在本地监听443,转发给远程108.177.125.82的443端口

然后再在 /etc/hosts 中将域名 k8s.gcr.io 指向 127.0.0.1, 那么本来要访问 k8s.gcr.io:443的,变成了访问本地 127.0.0.1:443 而 127.0.0.1:443 又通过ssh重定向到了 108.177.125.82:443 这样就实现了http代理或者说这种特殊情况下的科学上网。这个方案不需要装任何东西,但是每个访问目标都要这样处理,好在这种情况不多

内部堡垒机、跳板机都需要密码+动态码,太复杂了,怎么解?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ cat ~/.ssh/config 
#reuse the same connection --关键配置
ControlMaster auto
ControlPath ~/tmp/ssh_mux_%h_%p_%r

#查了下ControlPersist是在OpenSSH5.6加入的,5.3还不支持
#不支持的话直接把这行删了,不影响功能
#keep one connection in 72hour
#ControlPersist 72h
#复用连接的配置到这里,后面的配置与复用无关

#其它也很有用的配置
GSSAPIAuthentication=no
#这个配置在公网因为安全原因请谨慎关闭
StrictHostKeyChecking=no
TCPKeepAlive=yes
CheckHostIP=no
# "ServerAliveInterval [seconds]" configuration in the SSH configuration so that your ssh client sends a "dummy packet" on a regular interval so that the router thinks that the connection is active even if it's particularly quiet
ServerAliveInterval=15
#ServerAliveCountMax=6
ForwardAgent=yes

UserKnownHostsFile /dev/null

在你的ssh配置文件增加上述参数,意味着72小时内登录同一台跳板机只有第一次需要输入密码,以后都是重用之前的连接,所以也就不再需要输入密码了。

加了如上参数后的登录过程就有这样的东东(默认没有,这是debug信息):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
debug1: setting up multiplex master socket
debug3: muxserver_listen: temporary control path /home/ren/tmp/ssh_mux_10.16.*.*_22_corp.86g3C34vy36tvCtn
debug2: fd 3 setting O_NONBLOCK
debug3: fd 3 is O_NONBLOCK
debug3: fd 3 is O_NONBLOCK
debug1: channel 0: new [/home/ren/tmp/ssh_mux_10.16.*.*_22_corp]
debug3: muxserver_listen: mux listener channel 0 fd 3
debug1: control_persist_detach: backgrounding master process
debug2: control_persist_detach: background process is 15154
debug2: fd 3 setting O_NONBLOCK
debug1: forking to background
debug1: Entering interactive session.
debug2: set_control_persist_exit_time: schedule exit in 259200 seconds
debug1: multiplexing control connection

/home/ren/tmp/ssh_mux_10.16.._22_corp 这个就是保存好的socket,下次可以重用,免密码。 in 259200 seconds 对应 72小时

我有很多不同机房(或者说不同客户)的机器都需要跳板机来登录,能一次直接ssh上去吗?

比如有一批客户机房的机器IP都是192.168.., 然后需要走跳板机100.10.1.2才能访问到,那么我希望以后在笔记本上直接 ssh 192.168.1.5 就能直接连上

$ cat /etc/ssh/ssh_config

Host 192.168.*.*
ProxyCommand ssh -l ali-renxijun 100.10.1.2 exec /usr/bin/nc %h %p

上面配置的意思是执行 ssh 192.168.1.5的时候命中规则 Host 192.168.. 所以执行 ProxyCommand 先连上跳板机再通过跳板机连向192.168.1.5 。这样在你的笔记本上就跟192.168.. 的机器仿佛在一起,ssh可以上去,但是ping不通这个192.168.1.5的ip

划重点:公司的线上跳板机做了特殊限制,限制了这个技能。日常环境跳板机支持这个功能

比如我的跳板配置:

#到美国的机器用美国的跳板机速度更快
Host 10.74.*
ProxyCommand ssh -l user us.jump exec /bin/nc %h %p 2>/dev/null
#到中国的机器用中国的跳板机速度更快
Host 10.70.*
ProxyCommand ssh -l user cn.jump exec /bin/nc %h %p 2>/dev/null
   
Host 192.168.0.*
ProxyCommand ssh -l user 1.1.1.1 exec /usr/bin/nc %h %p

其实我的配置文件里面还有很多规则,懒得一个个隐藏IP了,这些规则是可以重复匹配的

来看一个例子

ren@ren-VirtualBox:/$ ping -c 1 10.16.1.*
        PING 10.16.1.* (10.16.1.*) 56(84) bytes of data.^C
    --- 10.16.1.* ping statistics ---
    1 packets transmitted, 0 received, 100% packet loss, time 0ms
    
ren@ren-VirtualBox:~$ ssh -l corp 10.16.1.* -vvv
OpenSSH_6.7p1 Ubuntu-5ubuntu1, OpenSSL 1.0.1f 6 Jan 2014
debug1: Reading configuration data /home/ren/.ssh/config
debug1: Reading configuration data /etc/ssh/ssh_config
debug1: /etc/ssh/ssh_config line 28: Applying options for *
debug1: /etc/ssh/ssh_config line 44: Applying options for 10.16.*.*
debug1: /etc/ssh/ssh_config line 68: Applying options for *
debug1: auto-mux: Trying existing master
debug1: Control socket "/home/ren/tmp/ssh_mux_10.16.1.*_22_corp" does not exist
debug1: Executing proxy command: exec ssh -l corp 139.*.*.* exec /usr/bin/nc 10.16.1.* 22

本来我的笔记本跟 10.16.1.* 是不通的(ping 不通),但是ssh可以直接连上,实际ssh登录过程中自动走跳板机139...* 就连上了

-vvv 参数是debug,把ssh登录过程的日志全部打印出来。

将隔离环境中的web端口映射到本地(本地代理)

远程机器部署了WEB Server(端口 8083),需要通过浏览器来访问这个WEB服务,但是server在隔离环境中,只能通过ssh访问到。一般来说会在隔离环境中部署一个windows机器,通过这个windows机器来访问到这个web server。能不能省掉这个windows机器呢?

现在我们试着用ssh来实现本地浏览器直接访问到这个隔离环境中的WEB Server。

假设web server是:10.1.1.123:8083, ssh账号是:user

先配置好本地直接 ssh user@10.1.1.123 (参考前面的 ProxyCommand配置过程,最好是免密也配置好),然后在你的笔记本上执行:

ssh -CNfL 0.0.0.0:8088:10.1.1.123:8083 user@10.1.1.123

或者:(root@100.1.2.3 -p 54900 是可达10.1.1.123的代理服务器)

ssh -CNfL 0.0.0.0:8089:10.1.1.123:8083 root@100.1.2.3 -p 54900

这表示在本地启动一个8088的端口,将这个8088端口映射到10.1.1.123的8083端口上,用的ssh账号是user

然后在笔记本上的浏览器中输入: 127.0.0.1:8088 就看到了如下界面:

image.png

反过来,也可以让隔离环境机器通过代理上网,比如安装yum

为什么有时候ssh 比较慢,比如总是需要30秒钟后才能正常登录

先了解如下知识点,在 ~/.ssh/config 配置文件中:

GSSAPIAuthentication=no

禁掉 GSSAPI认证,GSSAPIAuthentication是个什么鬼东西请自行 Google(多一次没必要的授权认证过程,然后等待超时)。 这里要理解ssh登录的时候有很多种认证方式(公钥、密码等等),具体怎么调试请记住强大的命令参数 ssh -vvv 上面讲到的技巧都能通过 -vvv 看到具体过程。

比如我第一次碰到ssh 比较慢总是需要30秒后才登录,不能忍受,于是登录的时候加上 -vvv明显看到控制台停在了:GSSAPIAuthentication 然后Google了一下,禁掉就好了

当然还有去掉每次ssh都需要先输入yes

批量打通所有机器之间的ssh登录免密码

Expect在有些公司是被禁止的

ssh免密码的原理是将本机的pub key复制到目标机器的 ~/.ssh/authorized_keys 里面。可以手工复制粘贴,也可以 ssh-copy-id 等

如果有100台机器,互相两两打通还是比较费事(大概需要100*99次copy key)。 下面通过 expect 来解决输入密码,然后配合shell脚本来批量解决这个问题。

这个脚本需要四个参数:目标IP、用户名、密码、home目录,也就是ssh到一台机器的时候帮我们自动填上yes,和密码,这样就不需要人肉一个个输入了。

再在外面写一个循环对每个IP执行如下操作:

if代码部分检查本机~/.ssh/下有没有id_rsa.pub,也就是是否以前生成过密钥对,没生成的话就帮忙生成一次。

for循环部分一次把生成的密钥对和authorized_keys复制到所有机器上,这样所有机器之间都不需要输入密码就能互相登陆了(当然本机也不需要输入密码登录所有机器)

最后一行代码:

ssh $user@$n "hostname -i"

验证一下没有输密码是否能成功ssh上去。

思考一下,为什么这么做就可以打通两两之间的免密码登录,这里没有把所有机器的pub key复制到其他所有机器上去啊

答案:其实这个脚本做了一个取巧投机的事,那就是让所有机器共享一套公钥、私钥。
有时候我也会把我的windows笔记本和我专用的某台虚拟机共享一套秘钥,这样任何新申请的机器打通一次账号就可以在两台机器上随便登录。请保护好自己的私钥

如果免密写入 authorized_keys 成功,但是通过ssh pubkey认证的时候还是有可能失败,这是因为pubkey认证要求:

  • authorized_keys 文件权限要对
  • .ssh 文件夹权限要对
  • /home/user 文件夹权限要对 —-这个容易忽视掉

留个作业:第一次ssh某台机器的时候总是出来一个警告,需要yes确认才能往下走,怎么干掉他?

StrictHostKeyChecking=no
UserKnownHostsFile=/dev/null

如果按照文章操作不work,推荐就近问身边的同学。问我的话请cat 配置文件 然后把ssh -vvv user@ip (user、ip请替换成你的),再截图发给我。**

测试成功的同学也请留言说下什么os、版本,以及openssl版本,我被问崩溃了


这里只是帮大家入门了解ssh,掌握好这些配置文件和-vvv后有好多好玩的可以去挖掘,同时也请在留言中说出你的黑技能

~/.ssh/config 参考配置

下面是我个人常用的ssh config配置

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
$cat ~/.ssh/config
#GSSAPIAuthentication=no
StrictHostKeyChecking=no
#TCPKeepAlive=yes
CheckHostIP=no
# "ServerAliveInterval [seconds]" configuration in the SSH configuration so that your ssh client sends a "dummy packet" on a regular interval so that the router thinks that the connection is active even if it's particularly quiet
ServerAliveInterval=15
#ServerAliveCountMax=6
ForwardAgent=yes

UserKnownHostsFile /dev/null

#reuse the same connection
ControlMaster auto
ControlPath /tmp/ssh_mux_%h_%p_%r

#keep one connection in 72hour
ControlPersist 72h

Host 192.168.1.*
ProxyCommand ssh user@us.jump exec /usr/bin/nc %h %p 2>/dev/null
Host 192.168.2.*
ProxyCommand ssh user@cn.jump exec /usr/bin/nc %h %p 2>/dev/null
#ProxyCommand /bin/nc -x localhost:12346 %h %p

Host 172
HostName 10.172.1.1
Port 22
User root
ProxyJump root@1.2.3.4:12345

Host 176
HostName 10.176.1.1
Port 22
User root
ProxyJump admin@1.2.3.4:12346

Host 10.5.*.*, 10.*.*.*
port 22
user root
ProxyJump plantegg@1.2.3.4:12347

ProxyJump完全可以取代 ProxyCommand,比如ProxyJump 不再依赖nc、也更灵活一些

/etc/ssh/ssh_config 参考配置

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
Host *
Protocol 2
ServerAliveInterval 30
User admin

host 10.10.55.*
ProxyCommand ssh -l admin admin.jump exec /usr/bin/nc %h %p

# uos is a hostname
Host 10.10.1.13* 192.168.2.133 uos
ProxyCommand ssh -l root -p 54900 1.1.1.1 exec /usr/bin/nc %h %p

#debug for git proxy
Host github.com
# LogLevel DEBUG3
# ProxyCommand ssh -l root gfw.jump exec /usr/bin/nc %h %p
# ProxyCommand ssh -oProxyCommand='ssh -l admin gfw.jump:22' -l root gfw.jump2 exec /usr/bin/nc %h %p


ForwardAgent yes
ForwardX11 yes
ForwardX11Trusted yes

SendEnv LANG LC_*
HashKnownHosts yes
GSSAPIAuthentication no
GSSAPIDelegateCredentials no
Compression yes

其他知识点

参数的优先级是:命令行配置选项 > ~/.ssh/config > /etc/ssh/ssh_config

在SSH的**身份验证阶段,SSH只支持服务端保留公钥,客户端保留私钥的方式,**所以方式只有两种:客户端生成密钥对,将公钥分发给服务端;服务端生成密钥对,将私钥分发给客户端。只不过出于安全性和便利性,一般都是客户端生成密钥对并分发公钥(阿里云服务器秘钥对–服务器将一对密钥中的公钥放在 authorized_keys, 私钥给client登陆用)

服务器上的 /etc/ssh/ssh_host* 是用来验证服务器身份的秘钥对(对应client的 known_hosts), 在主机验证阶段,服务端持有的是私钥,客户端保存的是来自于服务端的公钥。注意,这和身份验证阶段密钥的持有方是相反的。

SSH支持多种身份验证机制,它们的验证顺序如下:gssapi-with-mic,hostbased,publickey,keyboard-interactive,password,但常见的是密码认证机制(password)和公钥认证机制(public key). 当公钥认证机制未通过时,再进行密码认证机制的验证。这些认证顺序可以通过ssh配置文件(注意,不是sshd的配置文件)中的指令PreferredAuthentications改变。

永久隧道

大多时候隧道会失效,或者断开,我们需要有重连机制,一般可以通过autossh(需要单独安装)搞定自动重连,再配合systemd或者crond搞定永久自动重连

比如以下代码在gf开启2个远程转发端口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
remote_port=(30081 30082)
for port in "${remote_port[@]}"
do
line=`ps aux |grep ssh |grep $port | wc -l`
if [[ "$line" -lt 1 ]]; then
#等价: ssh -fN -R gf:$port:127.0.0.1:22 root@gf
autossh -M 0 -fNR gf:$port:127.0.0.1:22 root@gf
fi;
done

line=`ps aux |grep ssh |grep 13129 | wc -l`
if [[ "$line" -lt 1 ]]; then
nohup ssh -fNR gf:13129:172.16.1.2:3129 root@gf
fi;

#cat /etc/cron.d/jump
#* * * * * root sh /root/drds_private_cloud/jump.sh

或者另外创建一个service服务

1
2
3
4
5
6
7
8
9
10
[Unit]
Description=AutoSSH tunnel on 31081 to gf server
After=network.target

[Service]
Environment="AUTOSSH_GATETIME=0"
ExecStart=/usr/bin/autossh -M 0 -q -N -o "ServerAliveInterval 60" -o "ServerAliveCountMax 3" -NR gf:31081:172.16.1.2:22 -i /root/.ssh/id_rsa root@gf

[Install]
WantedBy=multi-user.target

调试ssh–终极大招

好多问题都是可以 debug 发现的

  • 客户端增加参数 -vvv 会把所有流程在控制台显示出来。卡在哪个环节;密码不对还是key不对一看就知道
  • server端还可以:/usr/sbin/sshd -ddd -p 2222 在2222端口对sshd进行debug,看输出信息验证为什么pub key不能login等. 一般都是权限不对,/root 以及 /root/.ssh 文件夹的权限和owner都要对,更不要说 /root/.ssh/authorized_keys 了
1
/usr/sbin/sshd -ddd -p 2222 

ssh 提示信息

可以用一下脚本生成一个彩色文件,放到 /etc/motd 中就行

Basic colors are numbered:

  • 1 – Red
  • 2 – Green
  • 3 – Yellow
  • 4 – Blue
  • 5 – Magenta
  • 6 – Cyan
  • 7 – White
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
#!/bin/sh
export TERM=xterm-256color

read one five fifteen rest < /proc/loadavg
echo "$(tput setaf 2)
Kernel: `uname -v | awk -v OFS=' ' '{print $4, $5}'`

\\ ^__^
\\ (oo)\\_______
(__)\\ )\\\/\\
||----w |
|| ||

本机器为长稳测试环境, 千万不要kill进程, 不要跑负载过重的任务

有任何需要请联系 ** 多谢!

$(tput setaf 4)Load Averages......: ${one}, ${five}, ${fifteen} (1, 5, 15 min)
$(tput setaf 5)
______________
本机器为长稳测试环境, 千万不要kill进程, 不要跑负载过重的任务

有任何需要请联系 ** 多谢!
--------------
\\ ^__^
\\ (oo)\\_______
(__)\\ )\\\/\\
||----w |
|| ||

$(tput sgr0)"

以上脚本运行结果

image-20210902224011450

sshd Banner

Banner指定用户登录后,sshd 向其展示的信息文件(Banner /usr/local/etc/warning.txt),默认不展示任何内容。

或者配置:

1
2
3
4
5
cat /etc/ssh/sshd_config
# no default banner path
#Banner none
#在配置文件末尾添加Banner /etc/ssh/my_banner这一行内容:
Banner /etc/ssh/my_banner

/etc/ssh/my_banner 中可以放置提示内容

验证秘钥对

-y Read a private OpenSSH format file and print an OpenSSH public key to stdout.

cd ~/.ssh/ ; ssh-keygen -y -f id_rsa | cut -d’ ‘ -f 2 ; cut -d’ ‘ -f 2 id_rsa.pub

ssh-keygen -y -e -f <private key>获取一个私钥并打印相应的公钥,该公钥可以直接与您可用的公钥进行比较

github 上你的公钥

github可以取到你的公钥,如果别人让你查看他的服务器,直接给 https://github.com/plantegg.keys 这个链接,让他把下载的key 加到 ~/.ssh/authorized_keys 里面就行了

ssh-keygen

静默生成

1
2
3
4
5
6
7
8
9
ssh-keygen -q -t rsa -N '' -f ~/.ssh/id_rsa <<<y

ssh-keygen -q -t rsa -N '' -f ~/.ssh/id_rsa <<<y >/dev/null 2>&1

//修改 passphrase
ssh-keygen -p -P "12345" -N "abcde" -f .ssh/id_rsa
//ssh-keygen -p [-P old_passphrase] [-N new_passphrase] [-f keyfile]
//或者直接通过提示一步步修改:
ssh-keygen -p

删除或者修改 passphrase

run ssh-keygen -p in a terminal. It will then prompt you for a keyfile (defaulted to the correct file for me, ~/.ssh/id_rsa), the old passphrase (enter what you have now) and the new passphrase (enter nothing).

要注意openssh 不同版本使用的不同 format,用openssh 8.0 默认用 “RFC4716” 格式,老的 4.0 默认是 PKCS8 格式

去修改dsa密钥后 openssh 4.0 不认

-m key_format
Specify a key format for the -i (import) or -e (export) conversion options. The sup‐
ported key formats are: “RFC4716” (RFC 4716/SSH2 public or private key), “PKCS8” (PEM
PKCS8 public key) or “PEM” (PEM public key). The default conversion format is
“RFC4716”.

如果用 8.0 去修改 PKCS8 格式的 key 可以指定格式参数

1
ssh-keygen -p  -m "PKCS8" -f ./id_dsa

ssh-agent

私钥设置了密码以后,每次使用都必须输入密码,有时让人感觉非常麻烦。比如,连续使用scp命令远程拷贝文件时,每次都要求输入密码。

ssh-agent命令就是为了解决这个问题而设计的,它让用户在整个 Bash 对话(session)之中,只在第一次使用 SSH 命令时输入密码,然后将私钥保存在内存中,后面都不需要再输入私钥的密码了。

第一步,使用下面的命令新建一次命令行对话。

1
$ eval `ssh-agent`

上面命令中,ssh-agent会先自动在后台运行,并将需要设置的环境变量输出在屏幕上,类似下面这样。

1
2
3
4
$ ssh-agent
SSH_AUTH_SOCK=/tmp/ssh-barrett/ssh-22841-agent; export SSH_AUTH_SOCK;
SSH_AGENT_PID=22842; export SSH_AGENT_PID;
echo Agent pid 22842;

eval命令的作用,就是运行上面的ssh-agent命令的输出,设置环境变量。

第二步,在新建的 Shell 对话里面,使用ssh-add命令添加默认的私钥(比如~/.ssh/id_rsa,或~/.ssh/id_dsa,或~/.ssh/id_ecdsa,或~/.ssh/id_ed25519)。

1
2
3
$ ssh-add
Enter passphrase for /home/you/.ssh/id_dsa: ********
Identity added: /home/you/.ssh/id_dsa (/home/you/.ssh/id_dsa)

上面例子中,添加私钥时,会要求输入密码。以后,在这个对话里面再使用密钥时,就不需要输入私钥的密码了,因为私钥已经加载到内存里面了。

如果添加的不是默认私钥,ssh-add命令需要显式指定私钥文件。

1
$ ssh-add my-other-key-file

上面的命令中,my-other-key-file就是用户指定的私钥文件。

SSH agent 程序能够将您的已解密的私钥缓存起来,在需要的时候用它来解密key chanllge返回给 SSHD https://webcache.googleusercontent.com/search?q=cache:7OfvSBFki10J:https://www.ibm.com/developerworks/cn/linux/security/openssh/part2/+&cd=7&hl=en&ct=clnk&gl=hk keychain介绍

安装sshd和debug

sshd 有自己的一对或多对密钥。它使用密钥向客户端证明自己的身份。所有密钥都是公钥和私钥成对出现,公钥的文件名一般是私钥文件名加上后缀.pub

DSA 格式的密钥文件默认为/etc/ssh/ssh_host_dsa_key(公钥为ssh_host_dsa_key.pub),RSA 格式的密钥为/etc/ssh/ssh_host_rsa_key(公钥为ssh_host_rsa_key.pub)。如果需要支持 SSH 1 协议,则必须有密钥/etc/ssh/ssh_host_key

如果密钥不是默认文件,那么可以通过配置文件sshd_configHostKey配置项指定。默认密钥的HostKey设置如下。

1
2
3
4
5
6
# HostKey for protocol version 1
# HostKey /etc/ssh/ssh_host_key

# HostKeys for protocol version 2
# HostKey /etc/ssh/ssh_host_rsa_key
# HostKey /etc/ssh/ssh_host_dsa_ke

注意,如果重装 sshd,/etc/ssh下的密钥都会重新生成(这些密钥对用于验证Server的身份),导致客户端重新 ssh 连接服务器时,会跳出警告,拒绝连接。为了避免这种情况,可以在重装 sshd 时,先备份/etc/ssh目录,重装后再恢复这个目录。

调试:非后台(-D)和debug(-d)模式启动sshd,同时监听2222和3333端口

sshd -D -d -p 2222 -p 3333

sshd config 配置多端口

1
2
3
4
5
#cat /etc/ssh/sshd_config
Port 22022
Port 22
#AddressFamily any
#ListenAddress 0.0.0.0

scp设置socks代理

scp -o “ProxyCommand=nc -X 5 -x [SOCKS_HOST]:[SOCKS_PORT] %h %p” [LOCAL/FILE/PATH] [REMOTE_USER]@[REMOTE_HOST]:[REMOTE/FILE/PATH]

其中[SOCKS_HOST]和[SOCKS_PORT]是socks代理的LOCAL_ADDRESS和LOCAL_PORT。[LOCAL/FILE/PATH]、[REMOTE_USER]、[REMOTE_HOST]和[REMOTE/FILE/PATH]分别是要复制文件的本地路径、要复制到的远端主机的用户名、要复制到的远端主机名、要复制文件的远端路径,这些参数与不使用代理时一样。“ProxyCommand=nc”表示当前运行命令的主机上需要有nc命令。

ProxyCommand

Specifies the proxy command for the connection. This command is launched prior to making the connection to Hostname. %h is replaced with the host defined in HostName and %p is replaced with 22 or is overridden by a Port directive.

在ssh连接目标主机前先执行ProxyCommand中的命令,比如 .ssh/config 中有如下配置

1
2
3
4
5
6
7
8
9
host remote-host
ProxyCommand ssh -l root -p 52146 1.2.3.4 exec /usr/bin/nc %h %p

//以上配置等价下面的命令
ssh -o ProxyCommand="ssh -l root -p 52146 1.2.3.4 exec /usr/bin/nc %h %p" remote-host
//or 等价
ssh -o ProxyCommand="ssh -l root -p 52146 -W %h:%p 1.2.3.4 " remote-host
//or 等价 debug1: Setting implicit ProxyCommand from ProxyJump: ssh -l root -p 52146 -vvv -W '[%h]:%p' 1.2.3.4
ssh -J root@1.2.3.4:52146 remote-host

如上配置指的是,如果执行ssh remote-host 命中host规则,那么先执行命令 ssh -l root -p 52146 1.2.3.4 exec /usr/bin/nc 同时把remote-host和端口(默认22)传给nc

ProxyCommand和ProxyJump很类似,ProxyJump使用:

1
2
//ssh到centos8机器上,走的是gf这台跳板机,本地一般和centos8不通
ssh -J gf:22 centos8

ProxyJump

需要 OpenSSH 7.3 以上版本才可以使用 ProxyJump, 相对 ProxyCommand 更简洁方便些

1
2
3
4
5
6
7
8
9
10
11
12
#ssh 116 就可以通过 jumpserver:50023 连上 root@1.116.2.1:22
Host 116
HostName 1.116.2.1
Port 22
User root
ProxyJump admin@jumpserver:50023

#ssh 1.112.任意ip 都会默认走 jumpserver 跳转过去
Host 1.112.*.*
Port 22
User root
ProxyJump root@jumpserver

加密算法

列出本地所支持默认的加密算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#ssh -Q key                                                            
ssh-ed25519
ssh-ed25519-cert-v01@openssh.com
ssh-rsa
ssh-dss
ecdsa-sha2-nistp256
ecdsa-sha2-nistp384
ecdsa-sha2-nistp521
ssh-rsa-cert-v01@openssh.com
ssh-dss-cert-v01@openssh.com
ecdsa-sha2-nistp256-cert-v01@openssh.com
ecdsa-sha2-nistp384-cert-v01@openssh.com
ecdsa-sha2-nistp521-cert-v01@openssh.com

ssh -Q cipher # List supported ciphers
ssh -Q mac # List supported MACs
ssh -Q key # List supported public key types
ssh -Q kex # List supported key exchange algorithms

比如连服务器报如下错误:

1
2
debug1: kex: algorithm: (no match)
Unable to negotiate with server port 22: no matching key exchange method found. Their offer: diffie-hellman-group1-sha1,diffie-hellman-group14-sha1

表示服务端支持 diffie-hellman-group1-sha1,diffie-hellman-group14-sha1 加密,但是client端不支持,那么可以指定算法来强制client端使用某种和server一致的加密方式

1
2
3
4
5
ssh -oKexAlgorithms=+diffie-hellman-group14-sha1 -l user

或者config中配置:
host server_ip
KexAlgorithms +diffie-hellman-group1-sha1

如果仍然报以下错误:

1
2
3
4
5
debug2: first_kex_follows 0
debug2: reserved 0
debug1: kex: algorithm: diffie-hellman-group14-sha1
debug1: kex: host key algorithm: (no match)
Unable to negotiate with server_ip port 22: no matching host key type found. Their offer: ssh-rsa

那么可以配置来解决:

1
2
3
Host *
HostKeyAlgorithms +ssh-rsa
PubkeyAcceptedKeyTypes +ssh-rsa

When an SSH client connects to a server, each side offers lists of connection parameters to the other. These are, with the corresponding ssh_config keyword:

  • KexAlgorithms: the key exchange methods that are used to generate per-connection keys
  • HostkeyAlgorithms: the public key algorithms accepted for an SSH server to authenticate itself to an SSH client
  • Ciphers: the ciphers to encrypt the connection
  • MACs: the message authentication codes used to detect traffic modification

无所不能的 SSH 三大转发模式

了解完前面的一些小知识,再来看看无所不能的三大杀招。上面的各种代理基本都是由这三种转发模式实现的。

SSH能够做动态转发、本地转发、远程转发。先简要概述下他们的特点和使用场景

三个转发模式的比较

  • 动态转发完全可以代替本地转发,只是动态转发是socks5协议,当科学上网用,本地转发是tcp协议
  • 本地转发完全是把动态转发特例化到访问某个固定目标的转发,类似 iptable 的 port forwarding
  • 远程转发是启动转端口的机器同时连上两端的两个机器,把本来不连通的两端拼接起来,中间显得多了个节点。
  • 三个转发模式可以串联使用

动态转发常用来科学上网,本地转发用来打洞,这两种转发启动的端口都是在本地;远程转发也是打洞的一种,只不过启用的端口在远程机器上。

img

动态转发 (-D) SOCKS5 协议

动态转发指的是,本机与 SSH 服务器之间创建了一个加密连接,然后本机内部针对某个端口的通信,都通过这个加密连接转发。它的一个使用场景就是,访问所有外部网站,都通过 SSH 转发。

动态转发需要把本地端口绑定到 SSH 服务器。至于 SSH 服务器要去访问哪一个网站,完全是动态的,取决于原始通信,所以叫做动态转发

动态的意思就是:需要访问的目标、端口还不确定。后面要讲的本地转发、远程转发都是针对具体IP、port的转发。

1
2
3
$ ssh -D 4444 ssh-server -N
//或者如下方式:
nohup ssh -qTfnN -D *:13658 root@jump vmstat 10 >/dev/null 2>&1

注意,这种转发采用了 SOCKS5 协议。访问外部网站时,需要把 HTTP 请求转成 SOCKS5 协议,才能把本地端口的请求转发出去。-N参数表示,这个 SSH 连接不能执行远程命令,只能充当隧道。

image-20210913143129749

下面是 ssh 隧道建立后的一个使用实例

1
2
3
curl -x socks5://localhost:4444 http://www.example.com
or
curl --socks5-hostname localhost:4444 https://www.twitter.com

上面命令中,curl 的-x参数指定代理服务器,即通过 SOCKS5 协议的本地3000端口,访问http://www.example.com

官方文档关于 -D的介绍

-D [bind_address:]port
Specifies a local “dynamic” application-level port forwarding. This works by allocat‐
ing a socket to listen to port on the local side, optionally bound to the specified
bind_address. Whenever a connection is made to this port, the connection is forwarded
over the secure channel, and the application protocol is then used to determine where
to connect to from the remote machine. Currently the SOCKS4 and SOCKS5 protocols are
supported, and ssh will act as a SOCKS server. Only root can forward privileged ports.
Dynamic port forwardings can also be specified in the configuration file.

特别注意,如果ssh -D 要启动的本地port已经被占用了是不会报错的,但是实际socks代理会没启动成功

本地转发 (-L)

本地转发(local forwarding)指的是,SSH 服务器作为中介的跳板机,建立本地计算机与特定目标网站之间的加密连接。本地转发是在本地计算机的 SSH 客户端建立的转发规则。

典型使用场景就是,打洞,经过跳板机访问无法直接连通的服务。

它会指定一个本地端口(local-port),所有发向那个端口的请求,都会转发到 SSH 跳板机(ssh-server),然后 SSH 跳板机作为中介,将收到的请求发到目标服务器(target-host)的目标端口(target-port)。

1
$ ssh -L :local-port:target-host:target-port ssh-server  //target-host是ssh-server的target-host, target-host 域名解析、路由都是由ssh-server完成

上面命令中,-L参数表示本地转发,local-port是本地端口,target-host是你想要访问的目标服务器,target-port是目标服务器的端口,ssh-server是 SSH 跳板机。当你访问localhost:local-port 的时候会通过ssh-server把请求转给target-host:target-port

img

上图对应的命令是:

1
ssh -L 53682:remote-server:53682 ssh-server

然后,访问本机的53682端口,就是访问remote-server的53682端口.

1
$ curl http://localhost:53682

注意,本地端口转发采用 HTTP 协议,不用转成 SOCKS5 协议。如果需要HTTP的动态代理,可以先起socks5动态代理,然后再起一个本地转发给动态代理的socks5端口,这样就有一个HTTP代理了,能给yum、docker之类的使用。

这个命令最好加上-N参数,表示不在 SSH 跳板机执行远程命令,让 SSH 只充当隧道。另外还有一个-f参数表示 SSH 连接在后台运行。

如果经常使用本地转发,可以将设置写入 SSH 客户端的用户个人配置文件。

1
2
Host test.example.com
LocalForward client-IP:client-port server-IP:server-port

远程转发(-R)

远程端口指的是在远程 SSH 服务器建立的转发规则。主要是执行ssh转发的机器别人连不上,所以需要一台client能连上的机器当远程转发端口,要不就是本地转发了。

由于本机无法访问内网 SSH 跳板机,就无法从外网发起 SSH 隧道,建立端口转发。必须反过来,从 SSH 跳板机发起隧道,建立端口转发,这时就形成了远程端口转发。

1
ssh -fNR 30.1.2.3:30081:166.100.64.1:3128 root@30.1.2.3 -p 2728

上面的命令,首先需要注意,不是在30.1.2.3 或者166.100.64.1 上执行的,而是找一台能联通 30.1.2.3 和166.100.64.1的机器来执行,在执行前Remote clients能连上 30.1.2.3 但是 30.1.2.3 和 166.100.64.1 不通,所以需要一个中介将 30.1.2.3 和166.100.64.1打通,这个中介就是下图中的MobaXterm所在的机器,命令在MobaXterm机器上执行

image-20210913163036410

执行上面的命令以后,跳板机30.1.2.3 到166.100.64.1的隧道已经建立了,这个隧道是依赖两边都能连通的MobaXterm机器。然后,就可以从Remote Client访问目标服务器了,即在Remote Client上执行下面的命令。

1
$ curl http://30.1.2.3:30081

执行上面的命令以后,命令就会输出服务器 166.100.64.1 的3128端口返回的内容。

如果经常执行远程端口转发,可以将设置写入 SSH 客户端的用户个人配置文件。

1
2
Host test.example.com
RemoteForward local-IP:local-port target-ip:target-port

注意远程转发需要:

  1. sshd_config里要打开AllowTcpForwarding选项,否则-R远程端口转发会失败。
  2. 默认转发到远程主机上的端口绑定的是127.0.0.1如要绑定0.0.0.0需要打开sshd_config里的GatewayPorts选项(然后ssh -R 后加上*:port )。这个选项如果由于权限没法打开也有办法,可配合ssh -L将端口绑定到0.0.0.0

开通远程转发后,如果需要动态代理(比如访问所有web服务),那么可以在30081端口机器上(30.1.2.3)执行:

1
nohup ssh -qTfnN -D *:13658 root@127.0.0.1 -p 30081 vmstat 10  >/dev/null 2>&1

表示在30081机器上(30.1.2.3)启动了一个socks5动态代理服务

调试转发、代理是否能联通

命令行调试

1
2
ssh -o 'ProxyCommand nc -X 5 -x 127.0.0.1:1080 %h %p' admin@1.1.1.1
ssh -o 'ProxyCommand ssh root@5.5.5.5 nc -X 5 -x 127.0.0.1:1088 %h %p' root@1.1.1.1

curl

curl -I –socks5-hostname localhost:13659 twitter.com

curl -x socks5://localhost:13659 twitter.com

Suppose you have a socks5 proxy running on localhost:13659 .

In curl >= 7.21.7, you can use

1
curl -x socks5h://localhost:13659 http://www.google.com/

In a proxy string, socks5h:// and socks4a:// mean that the hostname is
resolved by the SOCKS server. socks5:// and socks4:// mean that the
hostname is resolved locally. socks4a:// means to use SOCKS4a, which is
an extension of SOCKS4. Let’s make urllib3 honor it.

In curl >= 7.18.0, you can use

1
curl --socks5-hostname localhost:13659 http://www.google.com/

–proxy 参数含义如下:

1
The --socks5 option is basically considered obsolete since curl 7.21.7. This is because starting in that release, you can now specify the proxy protocol directly in the string that you specify the proxy host name and port number with already. The server you specify with --proxy. If you use a socks5:// scheme, curl will go with SOCKS5 with local name resolve but if you instead use socks5h:// it will pick SOCKS5 with proxy-resolved host name.

wget

指定命令行参数,通过命令行指定HTTP代理服务器的方式如下:

wget -Y on -e “http_proxy=http://[HTTP_HOST]:[HTTP_PORT]http://facebook.com/其中:[HTTP_HOST]和[HTTP_PORT]是http proxy的ADDRESS和PORT。

-Y表示是否使用代理,on表示使用代理。

-e执行后面跟的命令,相当于在.wgetrc配置文件中添加了一条命令,将http_proxy设置为需要使用的代理服务器。

wget –limit-rate=2.5k 限制下载速度,进行测试

PKI (Public Key Infrastructure)证书

X.509 只是一种常用的证书格式,一般以PEM编码,PEM 编码的证书通常以 .pem.crt.cer 为后缀。再次提醒,这只是“通常”情况,实际上某些工具可能并不遵循这些惯例。通过pem证书可以访问需要认证的https服务(比如etcd、apiserver等)

  • ASN.1 用于定义数据类型,例如证书(certificate)和秘钥(key)——就像用 JSON 定义一个 request body —— X.509 用 ASN.1 定义。
  • DER 是一组将 ASN.1 编码成二进制(比特和字节)的编码规则(encoding rules)。
  • PKCS#7 and PKCS#12 是比 X.509 更大的数据结构(封装格式),也用 ASN.1 定义,其中能包含除了证书之外的其他东西。二者分别在 Java 和 Microsoft 产品中使用较多。
  • DER 编码之后是二进制数据,不方便复制粘贴,因此大部分证书都是用 PEM 编码的,它用 base64 对 DER 进行编码,然后再加上自己的 label。
  • 私钥通常用是 PEM 编码的 PKCS#8 对象,但有时也会用密码来加密。

通过命令 cat /etc/kubernetes/pki/ca.crt | openssl x509 -text 也可以得到下图信息

image

公钥、私钥常见扩展名

  • 公钥:.pub or .pemca.crt
  • 私钥:.prv, .key, or .pem , ca.key

证书生成过程演示

并不是所有的场景都需要向这些大型的 CA 机构申请公钥证书,在任何一个企业,组织或是团体内都可以自己形这样的“小王国”,也就是说,你可以自行生成这样的证书,只需要你自己保证自己的生成证书的私钥的安全,以及不需要扩散到整个互联网。下面,我们用 openssl命令来演示这个过程。

1)生成 CA 机构的证书(公钥) ca.crt 和私钥 ca.key

1
2
3
4
5
6
openssl req -newkey rsa:2048 \
-new -nodes -x509 \
-days 365 \
-out ca.crt \
-keyout ca.key \
-subj "/C=SO/ST=Earth/L=Mountain/O=CoolShell/OU=HQ/CN=localhost"
  1. 生成 alice 的私钥
1
openssl genrsa -out alice.key 2048

3)生成 Alice 的 CSR – Certificate Signing Request

1
2
openssl req -new -key alice.key -days 365 -out alice.csr \
-subj "/C=CN/ST=Beijing/L=Haidian/O=CoolShell/OU=Test/CN=localhost.alice"

4)使用 CA 给 Alice 签名证书

1
2
3
4
5
openssl x509  -req -in alice.csr \
-extfile <(printf "subjectAltName=DNS:localhost.alice") \
-CA ca.crt -CAkey ca.key \
-days 365 -sha256 -CAcreateserial \
-out alice.crt

参考资料:

http://docs.corp-inc.com/pages/editpage.action?pageId=203555361
https://wiki.archlinux.org/index.php/SSH_keys_(%E7%AE%80%E4%BD%93%E4%B8%AD%E6%96%87)

https://wangdoc.com/ssh/key.html

https://robotmoon.com/ssh-tunnels/

通过SSH动态转发来建立Socks代以及各种场景应用案例

https://daniel.haxx.se/blog/2020/05/26/curl-ootw-socks5/

SSH Performance

Why when I transfer a file through SFTP, it takes longer than FTP?

一行代码解决scp在Internet传输慢的问题

关于证书(certificate)和公钥基础设施(PKI)的一切

网络数字身份认证术

MySQL知识体系的三驾马车

MySQL知识体系的三驾马车

在我看来要掌握好MySQL的话要理解好这三个东西:

  • 索引(B+树)
  • 日志(WAL)
  • 事务(可见性)

索引决定了查询的性能,也是用户感知到的数据库的关键所在,日常使用过程中抱怨最多的就是查询太慢了;

而日志是一个数据库的灵魂,他决定了数据库为什么可靠,还要保证性能,核心原理就是将随机写转换成顺序写;

事务则是数据库的皇冠。

索引

索引主要是解决查询性能的问题,数据一般都是写少查多,而且要满足各种查,所以使用数据库过程中最常见的问题就是索引的优化。

MySQL选择B+树来当索引的数据结构,是因为B+树的树干只有索引,能使得索引保持比较小,更容易加载到内存中;数据全部放在B+树的叶节点上,整个叶节点又是个有序双向链表,这样非常合适区间查找。

如果用平衡二叉树当索引,想象一下一棵 100 万节点的平衡二叉树,树高 20。一次查询可能需要访问 20 个数据块。在机械硬盘时代,从磁盘随机读一个数据块需要 10 ms 左右的寻址时间。也就是说,对于一个 100 万行的表,如果使用二叉树来存储,单独访问一个行可能需要 20 个 10 ms 的时间,这个查询可真够慢的

对比一下 InnoDB 的一个整数字段B+数索引为例,B+树的杈数一般是 1200。这棵树高是 4 的时候,就可以存 1200 的 3 次方个值,这已经 17 亿了。考虑到树根的数据块总是在内存中的,一个 10 亿行的表上一个整数字段的索引,查找一个值最多只需要访问 3 次磁盘。其实,树的第二层也有很大概率在内存中,那么访问磁盘的平均次数就更少了。

明确以下几点:

  • B+树是N叉树,以一个整数字段索引来看,N基本等于1200。数据库里的树高一般在2-4层。
  • 索引的树根节点一定在内存中,第二层大概率也在内存,再下层基本都是在磁盘中。
  • 每往下读一层就要进行一次磁盘IO。 从B+树的检索过程如下图所示:

image.png

每往下读一层就会进行一次磁盘IO,然后会一次性读取一些连续的数据放入内存中。

一个22.1G容量的表, 只需要高度为3的B+树就能存储,如果拓展到4层,可以存放25T的容量。但主要占内存的部分是叶子节点中的整行数据,非叶子节点全部加载到内存只需要18.8M。

B+树

MySQL的索引结构主要是B+树,也可以选hash

B+树特点:

  • 叶子结点才有数据,这些数据形成一个有序链表
  • 非叶子节点只有索引,导致非叶子节点小,查询的时候整体IO更小、更稳定(相对B数)
  • 删除相对B树快,因为数据有大量冗余,大部分时候不需要改非叶子节点,删除只需要从叶子节点中的链表中删除
  • B+树是多叉树,相对二叉树二分查找效率略低,但是树高度大大降低,减少了磁盘IO
  • 因为叶子节点的有序链表存在,支持范围查找

B+树的标准结构:

Image

innodb实现的B+树用了双向链表,节点内容存储的是页号(每页16K)

Image

联合索引

对于多个查询条件的复杂查询要正确建立多列的联合索引来尽可能多地命中多个查询条件,过滤性好的列要放在联合索引的前面。

MySQL一个查询只能用一个索引。

索引下推(index condition pushdown )

对于多个where条件的话,如果索引只能命中一个,剩下的那个条件过滤还是会通过回表来获取到后判断是否符合,但是MySQL5.6后,如果剩下的那个条件在联合索引上(但是因为第一个条件是模糊查询,没法用全联合索引),会将这个条件下推到索引判断上,来减少回表次数。这叫索引下推优化(index condition pushdown )

覆盖索引

要查询的列(select后面的列)如果都在索引上,那么这个查询的最终结果都可以直接从索引上读取到,这样读一次索引(数据小、顺序读)性能非常好。否则的话需要回表去获取别的列

前缀索引用不上覆盖索引对查询性能的优化,每次索引命中可能需要做一次回表,确认完整列值

回表

什么是回表?

select id, name from t where id>1 and id<10; 假设表t的id列是一个非主键的普通索引,那么这个查询就需要回表。查询执行的时候根据索引条件 id>1 and id<10 找到符合条件的行地址(主键),因为id索引上肯定有id的值,但是没有name,这里需要返回id,name 所以找到这些记录的地址后还需要回表(按主键)去取到name的值;

对应地如果select id from t where id>1 and id<10; 就不需要回表了,假设命中5条记录,这5个id的值都在索引上就能取到为啥还额外去回表呢?回表大概率是很慢的,因为你取到的行地址不一定连续,可能需要多次磁盘read

搞清楚概念后再来看count(*) 要不要回表?既然是统计数据,直接count主键(没有主键会自动添加一个默认隐藏的主键)就好了,多快好省。所以问题的本质是对回表不理解。count(*) 要不要回表不太重要,重要的是理解好什么是回表

那 select id, name from t where id>1 and id<10; 怎么样才能不回表呢?肯定是建立id name的联合索引就可以了

select * from table order by id limit 150000,10 这样limit后偏移很大一个值的查询,会因为回表导致非常慢。

这是因为根据id列上索引去查询过滤,但是select *要求查所有列的内容,但是索引上只有id的数据,所以导致每次对id索引进行过滤都要求去回表(根据id到表空间取到这个id行所有列的值),每一行都要回表导致这里出现了150000+10次随机磁盘读。

可以通过先用一个子查询(select id from order by id limit 150000,10),子查询中只查id列,而id的值都在索引上,用上了覆盖索引来避免回表。

先查到这10个id(扫描行数还是150000+10, 这里的limit因为有deleted记录、每行大小不一样等因素影响,没法一次跳到150000处。但是这次扫描150000行的时候不需要回表,所以速度快多了),然后再跟整个表做jion(join的时候只需要对这10个id行进行回表),来提升性能。

索引的一些其它知识点

多用自增主键是因为自增主键保证的是主键一直是增加的,也就是不会在索引中间插入,这样的话避免的索引页的分裂(代价很高)

写数据除了记录redo-log之外还会在内存(change buffer)中记录下修改后的数据,这样再次修改、读取的话不需要从磁盘读取数据,非唯一索引才能用上change buffer,因为唯一索引一定需要读磁盘验证唯一性,既然读过磁盘这个change buffer的意义就不大了。

1
mysql> insert into t(id,k) values(id1,k1),(id2,k2);//假设k1页在buffer中,k2不在

image.png

Buffer POOL

(1)缓冲池(buffer pool)是一种常见的降低磁盘访问的机制;

(2)缓冲池通常以页(page)为单位缓存数据;

(3)缓冲池的常见管理算法是LRU,memcache,OS,InnoDB都使用了这种算法;

(4)InnoDB对普通LRU进行了优化:

- 将缓冲池分为老生代和新生代,入缓冲池的页,优先进入老生代,页被访问,才进入新生代,以解决预读失效的问题

- 页被访问(预读的丢到old区),且在老生代**停留时间超过配置阈值(innodb_old_blocks_time)**的,才进入新生代,以解决批量数据访问,大量热数据淘汰的问题

图片

只有同时满足「被访问」与「在 old 区域停留时间超过 1 秒」两个条件,才会被插入到 young 区域头部

日志

数据库的关键瓶颈在于写,因为每次更新都要落盘防止丢数据,而磁盘最怕的就是随机写。

Write-Ahead logging(WAL)

写磁盘前先写日志,这样不用担心丢数据问题,写日志又是一个顺序写,性能比随机写好多了,这样将性能很差的随机写转换成了顺序写。然后每过一段时间将这些日志合并后真正写入到表空间,这次是随机写,但是有机会将多个写合并成一个,比如多个写在同一个Page上。

这是数据库优化的关键。

bin-log

MySQL Server用来记录执行修改数据的SQL,Replication基本就是复制并重放这个日志。有statement、row和混合模式三种。

bin-log保证不了表空间和bin-log的一致性,也就是断电之类的场景下是没法保证数据的一致性。

MySQL 日志刷新策略通过 sync_binlog 参数进行配置,其有 3 个可选配置:

  1. sync_binlog=0:MySQL 应用将完全不负责日志同步到磁盘,将缓存中的日志数据刷新到磁盘全权交给操作系统来完成;
  2. sync_binlog=1:MySQL 应用在事务提交前将缓存区的日志刷新到磁盘;
  3. sync_binlog=N:当 N 不为 0 与 1 时,MySQL 在收集到 N 个日志提交后,才会将缓存区的日志同步到磁盘。

redo-log

INNODB引擎用来保证事务的完整性,也就是crash-safe。MySQL 默认是保证不了不丢数据的,如果写了表空间还没来得及写bin-log就会造成主从数据不一致;或者在事务中需要执行多个SQL,bin-log保证不了完整性。

而在redo-log中任何修改都会先记录到redo-log中,即使断电MySQL重启后也会先检查redo-log将redo-log中记录了但是没有提交到表空间的数据进行提交(刷脏)

redo-log和bin-log的比较:

  • redo log 是 InnoDB 引擎特有的;binlog 是 MySQL 的 Server 层实现的,所有引擎都可以使用。redo-log保证了crash-safe的问题,binlog只能用于归档,保证不了safe。
  • redo log 是物理日志,记录的是“在某个数据页上做了什么修改”;binlog 是逻辑日志,记录的是这个语句的原始逻辑,比如“给 ID=2 这一行的 c 字段加 1 ”。
  • redo log 是循环写的,空间固定会用完;binlog 是可以追加写入的。“追加写”是指 binlog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。

redo-log中记录的是对页的操作,而不是修改后的数据页,buffer pool(或者说change buffer)中记录的才是数据页。正常刷脏是指的将change buffer中的脏页刷到表空间的磁盘,如果没来得及刷脏就崩溃了,那么就只能从redo-log来将没有刷盘的操作再执行一次让他们真正落盘。buffer pool中的任何变化都会写入到redo-log中(不管事务是否提交)

只有当commit(非两阶段的commit)的时候才会真正把redo-log写到表空间的磁盘上(不一定是commit的时候刷到表空间)。

如果机器性能很好(内存大、innodb_buffer_pool设置也很大,iops高),但是设置了比较小的innodb_logfile_size那么会造成redo-log很快会被写满,这个时候系统会停止所有更新,全力刷盘去推进ib_logfile checkpoint(位点),这个时候磁盘压力很小,但是数据库性能会出现间歇性下跌(select 反而相对更稳定了–更少的merge)。

redo-log要求数据量尽量少,这样写盘IO小;操作幂等(保证重放幂等)。实际逻辑日志(Logical Log, 也就是bin-log)的特点就是数据量小,而幂等则是基于Page的Physical Logging特点。最终redo-log的形式是Physiological Logging的方式,来兼得二者的优势。

所谓Physiological Logging,就是以Page为单位,但在Page内以逻辑的方式记录。举个例子,MLOG_REC_UPDATE_IN_PLACE类型的REDO中记录了对Page中一个Record的修改,方法如下:

(Page ID,Record Offset,(Filed 1, Value 1) … (Filed i, Value i) … )

其中,PageID指定要操作的Page页,Record Offset记录了Record在Page内的偏移位置,后面的Field数组,记录了需要修改的Field以及修改后的Value。

Innodb的默认Page大小是16K,OS文件系统默认都是4KB,对16KB的Page的修改保证不了原子性,因此Innodb又引入Double Write Buffer的方式来通过写两次的方式保证恢复的时候找到一个正确的Page状态。

InnoDB给每个REDO记录一个全局唯一递增的标号LSN(Log Sequence Number)。Page在修改时,会将对应的REDO记录的LSN记录在Page上(FIL_PAGE_LSN字段),这样恢复重放REDO时,就可以来判断跳过已经应用的REDO,从而实现重放的幂等。

binlog和redo-log一致性的保证

bin-log和redo-log的一致性是通过两阶段提交来保证的,bin-log作为事务的协调者,两阶段提交过程中prepare是非常重的,prepare一定会持久化(日志),记录如何commit和rollback,一旦prepare成功就一定能commit和rollback,如果其他节点commit后崩溃,恢复后会有一个协商过程,其它节点发现崩溃节点已经commit,所以会跟随commit;如果崩溃节点还没有prepare那么其它节点只能rollback。

实际崩溃后恢复时MySQL是这样保证redo-log和bin-log的完整性的:

  1. 如果redo-log里面的事务是完整的,也就是有了commit标识,那么直接提交
  2. 如果redo-log里面事务只有完整的prepare,则去检查事务对应的binlog是否完整
    1. 如果binlog完整则提交事务
    2. 如果不完整则回滚事务
  3. redo-log和binlog有一个共同的数据字段叫XID将他们关联起来

组提交

在没有开启binlog时,Redo log的刷盘操作将会是最终影响MySQL TPS的瓶颈所在。为了缓解这一问题,MySQL使用了组提交,将多个刷盘操作合并成一个,如果说10个事务依次排队刷盘的时间成本是10,那么将这10个事务一次性一起刷盘的时间成本则近似于1。

但是开启binlog后,binlog作为事务的协调者每次commit都需要落盘,这导致了Redo log的组提交失去了意义。

image-20211108152328424

Group Commit的方案中,其正确性的前提在于一个group内的事务没有并发冲突,因此即便并行也不会破坏事务的执行顺序。这个方案的局限性在于一个group 内的并行度仍然有限

刷脏

在内存中修改了,已经写入到redo-log中,但是还没来得及写入表空间的数据叫做脏页,MySQL过一段时间就需要刷脏,刷脏最容易造成MySQL的卡顿。

  • redo-log写满后,系统会停止所有更新操作,把checkpoint向前推进也就是将数据写入到表空间。这时写性能跌0,这个场景对性能影响最大
  • 系统内存不够,也需要将内存中的脏页释放,释放前需要先刷入到表空间。
  • 系统内存不够,但是redo-log空间够,也会刷脏,也就是刷脏不只是脏页写到redo-log,还要考虑读取情况。刷脏页后redo-log位点也一定会向前推荐
  • 系统空闲的时候也会趁机刷脏
  • 刷脏的时候默认还会连带刷邻居脏页(innodb_flush_neighbors)

当然如果一次性要淘汰的脏页太多,也会导致查询卡顿严重,可以通过设置innodb_io_capacity(一般设置成磁盘的iops),这个值越小的话一次刷脏页的数量越小,如果刷脏页速度还跟不上脏页生成速度就会造成脏页堆积,影响查询、更新性能。

在 MySQL 5.5 及以前的版本,回滚日志是跟数据字典一起放在 ibdata 文件里的,即使长事务最终提交,回滚段被清理,文件也不会变小。我见过数据只有 20GB,而回滚段有 200GB 的库。最终只好为了清理回滚段,重建整个库。

长事务意味着系统里面会存在很老的事务视图。由于这些事务随时可能访问数据库里面的任何数据,所以这个事务提交之前,数据库里面它可能用到的回滚记录都必须保留,这就会导致大量占用存储空间。除了对回滚段的影响,长事务还占用锁资源,也可能拖垮整个库。

表空间会刷进去没有提交的事务(比如大事务change buffer和redo-log都不够的时候),这个修改虽然在表空间中,但是通过可见性来控制是否可见。

落盘

innodb_flush_method 参数目前有 6 种可选配置值:

  1. fdatasync;
  2. O_DSYNC
  3. O_DIRECT
  4. O_DIRECT_NO_FSYNC
  5. littlesync
  6. nosync

其中,littlesync 与 nosync 仅仅用于内部性能测试,并不建议使用。

  • fdatasync,即取值 0,这是默认配置值。对 log files 以及 data files 都采用 fsync 的方式进行同步;
  • O_DSYNC,即取值 1。对 log files 使用 O_SYNC 打开与刷新日志文件,使用 fsync 来刷新 data files 中的数据;
  • O_DIRECT,即取值 4。利用 Direct I/O 的方式打开 data file,并且每次写操作都通过执行 fsync 系统调用的方式落盘;
  • O_DIRECT_NO_FSYNC,即取值 5。利用 Direct I/O 的方式打开 data files,但是每次写操作并不会调用 fsync 系统调用进行落盘;

为什么有 O_DIRECT 与 O_DIRECT_NO_FSYNC 配置的区别?

首先,我们需要理解更新操作落盘分为两个具体的子步骤:①文件数据更新落盘②文件元数据更新落盘。O_DIRECT 的在部分操作系统中会导致文件元数据不落盘,除非主动调用 fsync,为此,MySQL 提供了 O_DIRECT 以及 O_DIRECT_NO_FSYNC 这两个配置。

如果你确定在自己的操作系统上,即使不进行 fsync 调用,也能够确保文件元数据落盘,那么请使用 O_DIRECT_NO_FSYNC 配置,这对 MySQL 性能略有帮助。否则,请使用 O_DIRECT,不然文件元数据的丢失可能会导致 MySQL 运行错误。

Double Write

MySQL默认数据页是16k,而操作系统内核的页目前为4k。因此当一个16k的MySQL页写入过程中突然断电,可能只写入了一部分,即数据存在不一致的情况。MySQL为了防止这种情况,每写一个数据页时,会先写在磁盘上的一个固定位置,然后再写入到真正的位置。如果第二次写入时掉电,MySQL会从第一次写入的位置恢复数据。开启double write之后数据被写入两次,如果能将其优化掉,对用户的性能将会有不小的提升。

MySQL 8.0关掉Double Write能有5%左右的性能提升

事务

在 MySQL/InnoDB 中,使用MVCC(Multi Version Concurrency Control) 来实现事务。每个事务修改数据之后,会创建一个新的版本,用事务id作为版本号;一行数据的多个版本会通过指针连接起来,通过指针即可遍历所有版本。

当事务读取数据时,会根据隔离级别选择合适的版本。例如对于 Read Committed 隔离级别来说,每条SQL都会读取最新的已提交版本;而对于Repeatable Read来说,会在事务开始时选择已提交的最新版本,后续的每条SQL都会读取同一个版本的数据。

img

Postgres用Old to New,INNODB使用的是New to Old, 即主表存最新的版本,用链表指向旧的版本。当读取最新版本数据时,由于索引直接指向了最新版本,因此较低;与之相反,读取旧版本的数据代价会随之增加,需要沿着链表遍历。

INNODB中旧版本的数据存储于undo log中。这里的undo log起到了几个目的,一个是事务的回滚,事务回滚时从undo log可以恢复出原先的数据,另一个目的是实现MVCC,对于旧的事务可以从undo 读取旧版本数据。

可见性

是基于事务的隔离级别而言的,常用的事务的隔离级别有可重复读RR(Repeatable Read,MySQL默认的事务隔离级别)和读已提交RC(Read Committed)。

可重复读

读已提交:A事务能读到B事务已经commit了的结果,即使B事务开始时间晚于A事务

重复读的定义:一个事务启动的时候,能够看到所有已经提交的事务结果。但是之后,这个事务执行期间,其他事务的更新对它不可见。

指的是在一个事务中先后两次读到的结果是一样的,当然这两次读的中间自己没有修改这个数据,如果自己修改了就是当前读了。

如果两次读过程中,有一个别的事务修改了数据并提交了,第二次读到的还是别的事务修改前的数据,也就是这个修改后的数据不可见,因为别的事务在本事务之后。

如果一个在本事务启动之后的事务已经提交了,本事务会读到最新的数据,但是因为隔离级别的设置,会要求MySQL判断这个数据不可见,这样只能按照undo-log去反推修改前的数据,如果有很多这样的已经提交的事务,那么需要反推很多次,也会造成卡顿。

总结下,可见性的关键在于两个事务开始的先后关系:

  • 如果是可重复读RR(Repeatable Read),后开始的事务提交的结果对前面的事务可见
  • 如果是读已提交RC(Read Committed),后开始的事务提交的结果对前面的事务可见

当前读

更新数据都是先读后写的,而这个读,只能读当前的值,称为”当前读“(current read)。除了 update 语句外,select 语句如果加锁,也是当前读。

事务的可重复读的能力是怎么实现的?

可重复读的核心就是一致性读(consistent read);而事务更新数据的时候,只能用当前读。如果当前的记录的行锁被其他事务占用的话,就需要进入锁等待。

而读提交的逻辑和可重复读的逻辑类似,它们最主要的区别是:

  • 在可重复读隔离级别下,只需要在事务开始的时候创建一致性视图,之后事务里的其他查询都共
    用这个一致性视图;
  • 在读提交隔离级别下,每一个语句执行前都会重新算出一个新的视图。

幻读

幻读指的是一个事务中前后两次读到的数据不一致(读到了新插入的行)

可重复读是不会出现幻读的,但是更新数据时只能用当前读,当前读要求读到其它事务的修改(新插入行)

Innodb 引擎为了解决「可重复读」隔离级别使用「当前读」而造成的幻读问题,就引出了 next-key 锁,就是记录锁和间隙锁的组合。

  • 记录锁,锁的是记录本身;
  • 间隙锁,锁的就是两个值之间的空隙,以防止其他事务在这个空隙间插入新的数据,从而避免幻读现象。

可重复读、当前读以及行锁案例

案例表结构

1
2
3
4
5
6
7

mysql> CREATE TABLE `t` (
`id` int(11) NOT NULL,
`k` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
insert into t(id, k) values(1,1),(2,2);

上表执行如下三个事务

img

begin/start transaction 命令并不是一个事务的起点,在执行到它们之后的第一个操作 InnoDB 表的语句,事务才真正启动。如果你想要马上启动一个事务,可以使用 start transaction with consistent snapshot 这个命令。

“start transaction with consistent snapshot; ”的意思是从这个语句开始,创建一个持续整个事务的一致性快照

在读提交隔离级别(RC)下,这个用法就没意义了,等效于普通的 start transaction。

因为以上案例是RR(start transaction with consistent snapshot;), 也就是可重复读隔离级别。

那么事务B select到的K是3,因为事务C已提交,事务B update的时候不会等锁了,同时update必须要做当前读,这是因为update不做当前读而是可重复性读的话读到的K是1,这样覆盖了事务C的提交!也就是更新数据伴随的是当前读。

事务A开始在事务C之前, 而select是可重复性读,所以事务C提交了但是对A不可见,也就是select要保持可重复性读仍然读到的是1.

如果这个案例改成RC,事务B看到的还是3,事务A看到的就是2了(这个2是事务C提交的),因为隔离级别是RC。select 执行时间点事务才开始。

MySQL和PG事务实现上的差异

这两个数据库对MVCC实现上选择了不同方案,上面讲了MySQL选择的是redo-log去反推多个事务的不同数据,这个方案实现简单。但是PG选择的是保留多个不同的数据版本,优点就是查询不同版本数据效率高,缺点就是对这些数据要做压缩、合并之类的。

总结

理解好索引是程序员是否掌握数据库的最关键知识点,理解好索引才会写出更高效的SQL,避免慢查询搞死MySQL。

对日志的理解可以看到一个数据库为了提升性能(刷磁盘的瓶颈)采取的各种手段。也是最重要的一些设计思想所在。

事务则是数据库皇冠。

参考资料

https://explainextended.com/2009/10/23/mysql-order-by-limit-performance-late-row-lookups/ 回表

https://stackoverflow.com/questions/1243952/how-can-i-speed-up-a-mysql-query-with-a-large-offset-in-the-limit-clause

Linux Network Stack

Linux Network Stack

文章目标

从一个网络包进到网卡后续如何流转,进而了解中间有哪些关键参数可以控制他们,有什么工具能帮忙可以看到各个环节的一些指征,以及怎么调整他们。

接收流程

接收流程大纲

在开始收包之前,也就是OS启动的时候,Linux要做许多的准备工作:

  1. 创建ksoftirqd线程,为它设置好它自己的线程函数,用来处理软中断
  2. 协议栈注册,linux要实现许多协议,比如arp,icmp,ip,udp,tcp,每一个协议都会将自己的处理函数注册一下,方便包来了迅速找到对应的处理函数
  3. 网卡驱动初始化,每个驱动都有一个初始化函数,内核会让驱动也初始化一下。在这个初始化过程中,把自己的DMA准备好,把NAPI的poll函数地址告诉内核
  4. 启动网卡,分配RX,TX队列,注册中断对应的处理函数

以上是内核准备收包之前的重要工作,当上面都ready之后,就可以打开硬中断,等待数据包的到来了。

当数据到来了以后,第一个迎接它的是网卡:

  1. 网卡将数据帧DMA到内存的RingBuffer中,然后向CPU发起中断通知
  2. CPU响应中断请求,调用网卡启动时注册的中断处理函数
  3. 中断处理函数几乎没干啥,就发起了软中断请求
  4. 内核线程ksoftirqd线程发现有软中断请求到来,先关闭硬中断
  5. ksoftirqd线程开始调用驱动的poll函数收包
  6. poll函数将收到的包送到协议栈注册的ip_rcv函数中
  7. ip_rcv函数再讲包送到udp_rcv函数中(对于tcp包就送到tcp_rcv)

详细接收流程

  1. 网络包进到网卡,网卡驱动校验MAC,看是否扔掉,取决是否是混杂 promiscuous mode
  2. 网卡在启动时会申请一个接收ring buffer,其条目都会指向一个skb的内存。
  3. DMA完成数据报文从网卡硬件到内存到拷贝
  4. 网卡发送一个中断通知CPU。
  5. CPU执行网卡驱动注册的中断处理函数,中断处理函数只做一些必要的工作,如读取硬件状态等,并把当前该网卡挂在NAPI的链表中;
  6. Driver “触发” soft IRQ(NET_RX_SOFTIRQ (其实就是设置对应软中断的标志位)
  7. CPU中断处理函数返回后,会检查是否有软中断需要执行。因第6步设置了NET_RX_SOFTIRQ,则执行报文接收软中断。
  8. 在NET_RX_SOFTIRQ软中断中,执行NAPI操作,回调第5步挂载的驱动poll函数。
  9. 驱动会对interface进行poll操作,检查网卡是否有接收完毕的数据报文。
  10. 将网卡中已经接收完毕的数据报文取出,继续在软中断进行后续处理。注:驱动对interface执行poll操作时,会尝试循环检查网卡是否有接收完毕的报文,直到系统设置的net.core.netdev_budget上限(默认300),或者已经就绪报文。
  11. net_rx_action
  12. 内核分配 sk_buff 内存
  13. 内核填充 metadata: 协议等,移除 ethernet 包头信息
  14. 将skb 传送给内核协议栈 netif_receive_skb
  15. __netif_receive_skb_core:将数据送到抓包点(tap)或协议层(i.e. tcpdump)// 出抓包点:dev_queue_xmit_nit
  16. 进入到由 netdev_max_backlog 控制的qdisc
  17. 开始 ip_rcv 处理流程,主要处理ip协议包头相关信息
  18. 调用内核 netfilter 框架(iptables PREROUTING)
  19. 进入L4 protocol tcp_v4_rcv
  20. 找到对应的socket
  21. 根据 tcp_rmem 进入接收缓冲队列
  22. 内核将数据送给接收的应用

http://arthurchiao.art/blog/linux-net-stack-implementation-rx-zh:

image-20220725164331535

TAP 处理点就是 tcpdump 抓包、流量过滤。

注意:netfilter 或 iptables 规则都是在软中断上下文中执行的, 数量很多或规则很复杂时会导致网络延迟

软中断:可以把软中断系统想象成一系列内核线程(每个 CPU 一个),这些线程执行针对不同 事件注册的处理函数(handler)。如果你用过 top 命令,可能会注意到 ksoftirqd/0 这个内核线程,其表示这个软中断线程跑在 CPU 0 上。

硬中断发生在哪一个核上,它发出的软中断就由哪个核来处理。可以通过加大网卡队列数,这样硬中断工作、软中断工作都会有更多的核心参与进来。

__napi_schedule干两件事情,一件事情是把struct napi_struct 挂到struct softnet_data 上,注意softnet_data是一个per cpu变量,换句话说,软中断结构是挂在触发硬中断的同一个CPU上;另一件事情是调用__raise_softirq_irqoff 把irq_stat的__softirq_pending 字段置位,irq_stat 也是个per cpu 变量,表示当前这个cpu上有软中断待处理。

Image

从上图可以看到tcpdump在协议栈之前,也就是netfilter过滤规则对tcpdump无效,发包则是反过来:

Image

img

典型的接收堆栈

img

undefined

从四层协议栈来看收包流程

image.png

DMA驱动部分流程图

DMA是一个硬件逻辑,数据传输到系统物理内存的过程中,全程不需要CPU的干预,除了占用总线之外(期间CPU不能使用总线),没有任何额外开销。

img

img

image.png

  1. 驱动在内存中分配一片缓冲区用来接收数据包,叫做sk_buffer;
  2. 将上述缓冲区的地址和大小(即接收描述符),加入到rx ring buffer。描述符中的缓冲区地址是DMA使用的物理地址;
  3. 驱动通知网卡有一个新的描述符;
  4. 网卡从rx ring buffer中取出描述符,从而获知缓冲区的地址和大小;
  5. 网卡收到新的数据包;
  6. 网卡将新数据包通过DMA直接写到sk_buffer中。

Linux network queues overview

linux network queues

可以通过perf来监控包的堆栈:

1
perf trace --no-syscalls --event 'net:*' ping baidu.com -c1

buffer和流控

影响发送的速度的几个buffer和queue,接收基本一样

img

网卡传递数据包到内核的流程图及参数

软中断NET_TX_SOFTIRQ的处理函数为net_tx_action,NET_RX_SOFTIRQ的为net_rx_action

image.png

在网络子系统初始化中为NET_RX_SOFTIRQ注册了处理函数net_rx_action。所以net_rx_action函数就会被执行到了。

image.png

这里需要注意一个细节,硬中断中设置软中断标记,和ksoftirq的判断是否有软中断到达,都是基于smp_processor_id()的。这意味着只要硬中断在哪个CPU上被响应,那么软中断也是在这个CPU上处理的。所以说,如果你发现你的Linux软中断CPU消耗都集中在一个核上的话,做法是要把调整硬中断的CPU亲和性,来将硬中断打散到不同的CPU核上去。

软中断(也就是 Linux 里的 ksoftirqd 进程)里收到数据包以后,发现是 tcp 的包的话就会执行到 tcp_v4_rcv 函数。如果是 ESTABLISHED 状态下的数据包,则最终会把数据拆出来放到对应 socket 的接收队列中。然后调用 sk_data_ready 来唤醒用户进程。

对应的堆栈(本堆栈有问题,si%打满):

image-20211210160634705

igb_fetch_rx_bufferigb_is_non_eop的作用就是把数据帧从RingBuffer上取下来。为什么需要两个函数呢?因为有可能帧要占多个RingBuffer,所以是在一个循环中获取的,直到帧尾部。获取下来的一个数据帧用一个sk_buff来表示。收取完数据以后,对其进行一些校验,然后开始设置sbk变量的timestamp, VLAN id, protocol等字段。接下来进入到napi_gro_receive中,里面还会调用关键的 netif_receive_skb, 在netif_receive_skb中,数据包将被送到协议栈中,上图中的tcp_v4_rcv就是其中之一(tcp协议)

发送流程

  1. 应用调 sendmsg
  2. 数据拷贝到sk_write_queue上的最后一个skb中,如果该skb指向的数据区已经满了,则调用sk_stream_alloc_skb创建一个新的skb,并挂到这个sk_write_queue上
  3. TCP 分片 skb_buff
  4. 根据 tcp_wmem 缓存需要发送的包
  5. 构造TCP包头(src/dst port)
  6. ipv4 调用 tcp_write_xmit 和 tcp_transmit_skb
  7. ip_queue_xmit, 构建 ip 包头(获取目标ip和port,找路由)
  8. 进入 netfilter 流程 nf_hook(),iptables规则在这里生效
  9. 路由流程 POST_ROUTING,iptables 的nat和mangle表会在这里设置规则,对数据包进行一些修改
  10. ip_output 分片
  11. 进入L2 dev_queue_xmit,tc 网络流控在这里
  12. 填入 txqueuelen 队列
  13. 进入发送 Ring Buffer tx
  14. 驱动触发软中断 soft IRQ (NET_TX_SOFTIRQ)

在传输层的出口函数tcp_transmit_skb中,会对这个skb进行克隆(skb_clone),克隆得到的子skb和原先的父skb 指向共同的数据区。并且会把struct skb_shared_info的dataref 的计数加一。

传输层以下各层处理的skb 实际就是这个克隆出来的skb,而原先的skb保留在TCP连接的发送队列上。

克隆skb再经过协议栈层层处理后进入到驱动程序的RingBuffer 中。随后网卡驱动真正将数据发送出去,当发送完成时,由硬中断通知 CPU,然后由中断处理程序来清理 RingBuffer中指向的skb。注意,这里只释放了这个skb结构本身,而skb指向的数据区,由于dataref而不会被释放。要等到TCP层接收到ACK后,再释放父skb的同时,释放数据区。

比如ip_queue_xmit发现无法路由到目标地址,就会丢弃发送包,这里丢弃的是克隆包,原始包还在发送队列里,所以TCP层就会在定时器到期后进行重传

发包卡顿

内核从3.16开始有这样一个机制,在生成的一个新的重传报文前,先判断之前的报文的是否还在qdisc里面,如果在,就避免生成一个新的报文。

也就是对内核而言这个包发送了但是没收到ack,但实际这个包还在本机qdisc queue或者driver queue里,所以没必要重传

对应的监控计数:

1
2
#netstat -s |grep -i spur
TCPSpuriousRtxHostQueues: 4163

这个发包过程在发送端实际抓不到这个包,因为还没有真正发送,而是在发送端的queue里排队,但是对上层应用来说包发完了(回包ack也不需要应用来感知),所以抓包看起来正常,就是应用感觉卡了(卡的原因还是包在发送端内核 queue 排队,一般是 pfifo_fast bug bug2

关于 TCPSpuriousRtxHostQueues 指标的作用:

Host queues (Qdisc + NIC) can hold packets so long that TCP can
eventually retransmit a packet before the first transmit even left
the host.

Its not clear right now if we could avoid this in the first place :

  • We could arm RTO timer not at the time we enqueue packets, but
    at the time we TX complete them (tcp_wfree())

  • Cancel the sending of the new copy of the packet if prior one
    is still in queue.

This patch adds instrumentation so that we can at least see how
often this problem happens.

TCPSpuriousRtxHostQueues SNMP counter is incremented every time
we detect the fast clone is not yet freed in tcp_transmit_skb()

发包卡死

一个Linux 内核 bug 导致的 TCP连接卡死

从四层协议栈来看发包流程

image.png

发包流程对应源代码:

Image

net.core.dev_weight 用来调整 __qdisc_run 的循环处理权重,调大后也就是 __netif_schedule 更多的被调用执

另外发包默认是系统调用完成的(占用 sy cpu),只有在包太多,为了避免系统调用长时间占用 CPU 导致应用层卡顿,这个时候内核给了发包时间一个quota(net.core.dev_weight 参数来控制),用完后即使包没发送完也退出发包的系统调用,队列中未发送完的包留待 tx-softirq 来发送(这是占用 si cpu)

tcp在做tcp_sendmsg 的时候会将应用层msg做copy到内核层的skb,在调用网络层执行tcp_transmit_skb 会将这个 skb再次copy交给网络层,内核态的skb继续保留直到收到ack。

tcp_transmit_skb 还会设置tcp头,在skb中 tcp头、ip头内存都预留好了,只需要填写内容。

然后就是ip层,主要是分包、路由控制,然后就是netfilter(比如iptables规则匹配)。再然后进入neighbour(arp) , 获取mac后进入网络层

sudo ifconfig eth0 txqueuelen ** 来控制qdisc 发送队列长度

image-20210714204347862

粗略汇总一下进出堆栈:

image.png

http://docshare02.docshare.tips/files/21804/218043783.pdf 中也有描述:

img

软中断

一般net_rx 远大于net_tx, 如下所示,这是因为每个包发送完成后还需要清理回收内存(释放 skb),这是通过硬中断触发 rx-softirq 来完成的,无论是收包、还是发送包完毕都是触发这个rx-softirq。

1
2
3
4
5
6
#cut /proc/softirqs -c 1-70
CPU0 CPU1 CPU2 CPU3 CPU4
HI: 3 0 0 0 0
TIMER: 1616454419 1001992045 1013647869 1366481348 884639123
NET_TX: 168326 1717390 7000 6083 5748
NET_RX: 771543422 132283770 96912580 77533029 85143572

发送的时候如果 net.core.dev_weight 配额够的话直接通过系统调用就将包发送完毕,不需要触发软中断

内核相关参数

Ring Buffer

Ring Buffer位于NIC和IP层之间,是一个典型的FIFO(先进先出)环形队列。Ring Buffer没有包含数据本身,而是包含了指向sk_buff(socket kernel buffers)的描述符。
可以使用ethtool -g eth0查看当前Ring Buffer的设置:

$sudo ethtool -g eth0
Ring parameters for eth0:
Pre-set maximums:
RX:		256
RX Mini:	0
RX Jumbo:	0
TX:		256
Current hardware settings:
RX:		256
RX Mini:	0
RX Jumbo:	0
TX:		256

上面的例子是一个小规格的ECS,接收队列、传输队列都为256。

$sudo ethtool -g eth0
Ring parameters for eth0:
Pre-set maximums:
RX:		4096
RX Mini:	0
RX Jumbo:	0
TX:		4096
Current hardware settings:
RX:		4096
RX Mini:	0
RX Jumbo:	0
TX:		512

这是一台物理机,接收队列为4096,传输队列为512。接收队列已经调到了最大,传输队列还可以调大。队列越大丢包的可能越小,但数据延迟会增加

调整 Ring Buffer 队列数量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ethtool -l eth0
Channel parameters for eth0:
Pre-set maximums:
RX: 0
TX: 0
Other: 1
Combined: 8
Current hardware settings:
RX: 0
TX: 0
Other: 1
Combined: 8

sudo ethtool -L eth0 combined 8
sudo ethtool -L eth0 rx 8

网卡多队列就是指的有多个RingBuffer,每个RingBufffer可以由一个core来处理

image.png

网卡各种统计数据查看

ethtool -S eth0 | grep errors

ethtool -S eth0 | grep rx_ | grep errors //查看网卡是否丢包,一般是ring buffer太小

//监控
ethtool -S eth0 | grep -e "err" -e "drop" -e "over" -e "miss" -e "timeout" -e "reset" -e "restar" -e "collis" -e "over" | grep -v "\: 0"

网卡进出队列大小调整

//查看目前的进出队列大小
ethtool -g eth0
//修改进出队列
ethtool -G eth0 rx 8192 tx 8192

要注意如果设置的值超过了允许的最大值,用默认的最大值,一些ECS之类的虚拟机、容器就不允许修改这个值。

txqueuelen

ifconfig 看到的 txqueuelen 跟Ring Buffer是两个东西,IP协议下面就是 txqueuelen,txqueuelen下面才到Ring Buffer.

常用的tc qdisc、netfilter就是在txqueuelen这一环节。 qdisc 的队列长度是我们用 ifconfig 来看到的 txqueuelen

发送队列就是指的这个txqueuelen,和网卡关联着。 而每个Core接收队列由内核参数: net.core.netdev_max_backlog来设置

1
2
3
4
5
//当前值通过ifconfig可以查看到,修改:
ifconfig eth0 txqueuelen 2000
//监控
ip -s link
ip -s link show enp2s0f0

如果txqueuelen 太小导致数据包被丢弃的情况,这类问题可以通过下面这个命令来观察:

1
2
3
4
5
6
7
8
9
10
11
$ ip -s -s link ls dev eth0
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP mode DEFAULT group default qlen 1000
link/ether 00:16:3e:12:9b:c0 brd ff:ff:ff:ff:ff:ff
RX: bytes packets errors dropped overrun mcast
13189414480980 22529315912 0 0 0 0
RX errors: length crc frame fifo missed
0 0 0 0 0
TX: bytes packets errors dropped carrier collsns
15487121408466 12925733540 0 0 0 0
TX errors: aborted fifo window heartbeat transns
0 0 0 0 2

如果观察到 dropped 这一项不为 0,那就有可能是 txqueuelen 太小导致的。当遇到这种情况时,你就需要增大该值了,比如增加 eth0 这个网络接口的 txqueuelen:

$ ifconfig eth0 txqueuelen 2000

Interrupt Coalescence (IC) - rx-usecs, tx-usecs, rx-frames, tx-frames (hardware IRQ)

可以通过降低终端的频率,也就是合并硬中断来提升处理网络包的能力,当然这是以增大网络包的延迟为代价。

	//检查
	$ethtool -c eth0
	Coalesce parameters for eth0:
Adaptive RX: off  TX: off
stats-block-usecs: 0
sample-interval: 0
pkt-rate-low: 0
pkt-rate-high: 0

rx-usecs: 1
rx-frames: 0
rx-usecs-irq: 0
rx-frames-irq: 0

tx-usecs: 0
tx-frames: 0
tx-usecs-irq: 0
tx-frames-irq: 256

rx-usecs-low: 0
rx-frame-low: 0
tx-usecs-low: 0
tx-frame-low: 0

rx-usecs-high: 0
rx-frame-high: 0
tx-usecs-high: 0
tx-frame-high: 0
	//修改, 
	ethtool -C eth0 rx-usecs value tx-usecs value
	//监控
	cat /proc/interrupts

我们来说一下上述结果的大致含义

  • Adaptive RX: 自适应中断合并,网卡驱动自己判断啥时候该合并啥时候不合并

  • rx-usecs:当过这么长时间过后,一个RX interrupt就会被产生。How many usecs to delay an RX interrupt after a packet arrives.

  • rx-frames:当累计接收到这么多个帧后,一个RX interrupt就会被产生。Maximum number of data frames to receive before an RX interrupt.

  • rx-usecs-irq: How many usecs to delay an RX interrupt while an interrupt is being serviced by the host.

  • rx-frames-irq: Maximum number of data frames to receive before an RX interrupt is generated while the system is servicing an interrupt.

Ethtool 绑定端口

ntuple filtering for steering network flows

一些网卡支持 “ntuple filtering” 特性。该特性允许用户(通过 ethtool )指定一些参数来在硬件上过滤收到的包,然后将其直接放到特定的 RX queue。例如,用户可以指定到特定目端口的 TCP 包放到 RX queue 1。

Intel 的网卡上这个特性叫 Intel Ethernet Flow Director,其他厂商可能也有他们的名字,这些都是出于市场宣传原因,底层原理是类似的。

我们后面会看到,ntuple filtering 是一个叫 Accelerated Receive Flow Steering(aRFS) 功能的核心部分之一,后者使得 ntuple filtering 的使用更加方便。

这个特性适用的场景:最大化数据本地性(data locality),以增加 CPU 处理网络数据时的缓存命中率。例如,考虑运行在 80 口的 web 服务器:

  1. webserver 进程运行在 80 口,并绑定到 CPU 2
  2. 和某个 RX queue 关联的硬中断绑定到 CPU 2
  3. 目的端口是 80 的 TCP 流量通过 ntuple filtering 绑定到 CPU 2
  4. 接下来所有到 80 口的流量,从数据包进来到数据到达用户程序的整个过程,都由 CPU 2 处理
  5. 仔细监控系统的缓存命中率、网络栈的延迟等信息,以验证以上配置是否生效

检查 ntuple filtering 特性是否打开:

1
2
3
4
5
$ sudo ethtool -k eth0
Offload parameters for eth0:
...
ntuple-filters: off
receive-hashing: on

可以看到,上面的 ntuple 是关闭的。

打开:

1
$ sudo ethtool -K eth0 ntuple on

打开 ntuple filtering 功能,并确认打开之后,可以用 ethtool -u 查看当前的 ntuple
rules:

1
2
3
$ sudo ethtool -u eth0
40 RX rings available
Total 0 rules

可以看到当前没有 rules。

我们来加一条:目的端口是 80 的放到 RX queue 2

1
2
3
4
$ sudo ethtool -U eth0 flow-type tcp4 dst-port 80 action 2

删除
ethtool -U eth0 delete 1023

你也可以用 ntuple filtering 在硬件层面直接 drop 某些 flow 的包。当特定 IP 过来的流量太大时,这种功能可能会派上用场。更多关于 ntuple 的信息,参 考 ethtool man page。

软中断合并 GRO

GRO和硬中断合并的思想很类似,不过阶段不同。硬中断合并是在中断发起之前,而GRO已经到了软中断上下文中了。

如果应用中是大文件的传输,大部分包都是一段数据,不用GRO的话,会每次都将一个小包传送到协议栈(IP接收函数、TCP接收)函数中进行处理。开启GRO的话,Linux就会智能进行包的合并,之后将一个大包传给协议处理函数。这样CPU的效率也是就提高了。

1
2
# ethtool -k eth0 | grep generic-receive-offload
generic-receive-offload: on

如果你的网卡驱动没有打开GRO的话,可以通过如下方式打开。

1
# ethtool -K eth0 gro on

这是收包,发包对应参数是GSO

ifconfig 监控指标

  • RX overruns: overruns意味着数据包没到Ring Buffer就被网卡物理层给丢弃了,而CPU无法及时的处理中断是造成Ring Buffer满的原因之一,例如中断分配的不均匀。或者Ring Buffer太小导致的(很少见),overruns数量持续增加,建议增大Ring Buffer ,使用ethtool ‐G 进行设置。
  • RX dropped: 表示数据包已经进入了Ring Buffer,但是由于内存不够等系统原因,导致在拷贝到内存的过程中被丢弃。如下四种情况导致dropped:Softnet backlog full(pfmemalloc && !skb_pfmemalloc_protocol(skb)–分配内存失败);Bad / Unintended VLAN tags;Unknown / Unregistered protocols;IPv6 frames
  • RX errors:表示总的收包的错误数量,这包括 too-long-frames 错误,Ring Buffer 溢出错误,crc 校验错误,帧同步错误,fifo overruns 以及 missed pkg 等等。

overruns

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

监控指标 /proc/net/softnet_stat

Important details about /proc/net/softnet_stat:

  • Each line of /proc/net/softnet_stat corresponds to a struct softnet_data structure, of which there is 1 per CPU.
  • The values are separated by a single space and are displayed in hexadecimal
  • The first value, sd->processed, is the number of network frames processed. This can be more than the total number of network frames received if you are using ethernet bonding. There are cases where the ethernet bonding driver will trigger network data to be re-processed, which would increment the sd->processed count more than once for the same packet.
  • The second value, sd->dropped, is the number of network frames dropped because there was no room on the processing queue. More on this later.
  • The third value, sd->time_squeeze, is (as we saw) the number of times the net_rx_action loop terminated because the budget was consumed or the time limit was reached, but more work could have been. Increasing the budget as explained earlier can help reduce this. time_squeeze 计数在内核中只有一个地方会更新(比如内核 5.10),如果看到监控中有 time_squeeze 升高, 那一定就是执行到了以上 budget 用完的逻辑
  • The next 5 values are always 0.
  • The ninth value, sd->cpu_collision, is a count of the number of times a collision occurred when trying to obtain a device lock when transmitting packets. This article is about receive, so this statistic will not be seen below.
  • The tenth value, sd->received_rps, is a count of the number of times this CPU has been woken up to process packets via an Inter-processor Interrupt
  • The last value, flow_limit_count, is a count of the number of times the flow limit has been reached. Flow limiting is an optional Receive Packet Steering feature that will be examined shortly.

对应的代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
// https://github.com/torvalds/linux/blob/v5.10/net/core/net-procfs.c#L172
static int softnet_seq_show(struct seq_file *seq, void *v) {
struct softnet_data *sd = v;

seq_printf(seq,
"%08x %08x %08x %08x %08x %08x %08x %08x %08x %08x %08x %08x %08x\n",
sd->processed, sd->dropped, sd->time_squeeze, 0,
0, 0, 0, 0, /* was fastroute */
0, /* was cpu_collision */
sd->received_rps, flow_limit_count,
softnet_backlog_len(sd), (int)seq->index);
return 0;
}

net.core.netdev_budget

一次软中断(ksoftirqd进程)能处理包的上限,有就多处理,处理到300个了一定要停下来让CPU能继续其它工作。单次poll 收包是所有注册到这个 CPU 的 NAPI 变量收包数量之和不能大于这个阈值。

1
sysctl net.core.netdev_budget //3.10 kernel默认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时间太短,来不及处理足够多的网络包,那么需要增大这个值,当这个值太大的话有可能导致包到了内核但是应用(userspace)抢不到时间片来读取这些packet。

增大和查看 net.core.netdev_budget

sysctl -a | grep net.core.netdev_budget
sysctl -w net.core.netdev_budget=400 //临时性增大

早期的时候网卡一般是10Mb的,现在基本都是10Gb的了,还是每一次软中断、上下文切换只处理一个包的话代价太大,需要改进性能。于是引入的NAPI,一次软中断会poll很多packet

image.png

来源 This is much faster, but brings up another problem. What happens if we have so many packets to process that we spend all our time processing packets from the NIC, but we never have time to let the userspace processes actually drain those queues (read from TCP connections, etc.)? Eventually the queues would fill up, and we’d start dropping packets. To try and make this fair, the kernel limits the amount of packets processed in a given softirq context to a certain budget. Once this budget is exceeded, it wakes up a separate thread called ksoftirqd (you’ll see one of these in ps for each core) which processes these softirqs outside of the normal syscall/interrupt path. This thread is scheduled using the standard process scheduler, which already tries to be fair.

于是在Poll很多packet的时候有可能网卡队列一直都有包,那么导致这个Poll动作无法结束,造成应用一直在卡住状态,于是可以通过netdev_max_backlog来设置Poll多少Packet后停止Poll以响应用户请求。

image.png

一旦出现slow syscall(如上图黄色部分慢)就会导致packet处理被延迟。

发送包的时候系统调用循环发包,占用sy,只有当发包系统quota用完的时候,循环退出,剩下的包通过触发软中断的形式继续发送,此时占用si

netdev_max_backlog

The netdev_max_backlog is a queue within the Linux kernel where traffic is stored after reception from the NIC, but before processing by the protocol stacks (IP, TCP, etc). There is one backlog queue per CPU core.

如果 /proc/net/softnet_stat 第二列一直在增加的话表示netdev backlog queue overflows. 需要增大 netdev_max_backlog

增大和查看 netdev_max_backlog:
sysctl -a |grep netdev_max_backlog
sysctl -w net.core.netdev_max_backlog=1024 //临时性增大

netdev_max_backlog(接收)和txqueuelen(发送)相对应

softnet_stat

关于/proc/net/softnet_stat 的重要细节:

  1. 每一行代表一个 struct softnet_data 变量。因为每个 CPU core 都有一个该变量,所以每行 其实代表一个 CPU core
  2. 每列用空格隔开,数值用 16 进制表示
  3. 第一列 sd->processed,是处理的网络帧的数量。如果你使用了 ethernet bonding, 那这个值会大于总的网络帧的数量,因为 ethernet bonding 驱动有时会触发网络数据被 重新处理(re-processed)
  4. 第二列,sd->dropped,是因为处理不过来而 drop 的网络帧数量。后面会展开这一话题
  5. 第三列,sd->time_squeeze,前面介绍过了,由于 budget 或 time limit 用完而退出 net_rx_action 循环的次数
  6. 接下来的 5 列全是 0
  7. 第九列,sd->cpu_collision,是为了发送包而获取锁的时候有冲突的次数
  8. 第十列,sd->received_rps,是这个 CPU 被其他 CPU 唤醒去收包的次数
  9. 最后一列,flow_limit_count,是达到 flow limit 的次数。flow limit 是 RPS 的特性, 后面会稍微介绍一下

TCP协议栈Buffer

	sysctl -a | grep net.ipv4.tcp_rmem   // receive
	sysctl -a | grep net.ipv4.tcp_wmem   // send
	//监控
	cat /proc/net/sockstat

接收Buffer

$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

如果 “pruning” 一直在增加很有可能是程序中调用了 setsockopt(SO_RCVBUF) 导致内核关闭了动态调整功能,或者压力大,缓存不够了。具体Case:https://blog.cloudflare.com/the-story-of-one-latency-spike/

nstat也可以看到比较多的数据

1
2
3
4
5
6
7
8
9
10
11
$nstat -z |grep -i drop
TcpExtLockDroppedIcmps 0 0.0
TcpExtListenDrops 0 0.0
TcpExtTCPBacklogDrop 0 0.0
TcpExtPFMemallocDrop 0 0.0
TcpExtTCPMinTTLDrop 0 0.0
TcpExtTCPDeferAcceptDrop 0 0.0
TcpExtTCPReqQFullDrop 0 0.0
TcpExtTCPOFODrop 0 0.0
TcpExtTCPZeroWindowDrop 0 0.0
TcpExtTCPRcvQDrop 0 0.0

总体简略接收包流程

image-20210511114834433

带参数版收包流程:

image.png

总体简略发送包流程

带参数版发包流程:

image.png

网络包流转结构图

跨机器网络IO

Image

lo 网卡

127.0.0.1(lo)本机网络 IO ,无需走到物理网卡,也不用进入RingBuffer驱动队列,但是还是要走内核协议栈,直接把 skb 传给接收协议栈(经过软中断)

Image

总的来说,本机网络 IO 和跨机 IO 比较起来,确实是节约了一些开销。发送数据不需要进 RingBuffer 的驱动队列,直接把 skb 传给接收协议栈(经过软中断)。但是在内核其它组件上,可是一点都没少,系统调用、协议栈(传输层、网络层等)、网络设备子系统、邻居子系统整个走了一个遍。连“驱动”程序都走了(虽然对于回环设备来说只是一个纯软件的虚拟出来的东东)。所以即使是本机网络 IO,也别误以为没啥开销,实际本机访问自己的eth0 ip也是走的lo网卡和访问127.0.0.1是一样的,测试用ab分别走127.0.0.1和eth0压nginx,在nginx进程跑满,ab还没满两者的nginx单核都是7万TPS左右,跨主机压nginx的单核也是7万左右(要调多ab的并发数)。

如果是同一台宿主机走虚拟bridge通信的话(同一宿主机下的不容docker容器通信):

Image

ab 压 nginx单核(intel 8163 绑核)
127.0.0.1 Requests per second: 69498.96 [#/sec] (mean)
Time per request: 0.086 [ms] (mean)
Eth0 Requests per second: 70261.93 [#/sec] (mean)
Time per request: 0.085 [ms] (mean)
跨主机压 Requests per second: 70119.05 [#/sec] (mean)
Time per request: 0.143 [ms] (mean)

ab不支持unix domain socket,如果增加ab和nginx之间的时延,QPS急剧下降,但是增加ab的并发数完全可以把QPS拉回去。

Unix Domain Socket工作原理

接收connect 请求的时候,会申请一个新 socket 给 server 端将来使用,和自己的 socket 建立好连接关系以后,就放到服务器正在监听的 socket 的接收队列中。这个时候,服务器端通过 accept 就能获取到和客户端配好对的新 socket 了。

Image

主要的连接操作都是在这个函数中完成的。和我们平常所见的 TCP 连接建立过程,这个连接过程简直是太简单了。没有三次握手,也没有全连接队列、半连接队列,更没有啥超时重传。

直接就是将两个 socket 结构体中的指针互相指向对方就行了。就是 unix_peer(newsk) = sk 和 unix_peer(sk) = newsk 这两句。

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
//file: net/unix/af_unix.c
static int unix_stream_connect(struct socket *sock, struct sockaddr *uaddr,
int addr_len, int flags)
{
struct sockaddr_un *sunaddr = (struct sockaddr_un *)uaddr;

// 1. 为服务器侧申请一个新的 socket 对象
newsk = unix_create1(sock_net(sk), NULL);

// 2. 申请一个 skb,并关联上 newsk
skb = sock_wmalloc(newsk, 1, 0, GFP_KERNEL);
...

// 3. 建立两个 sock 对象之间的连接
unix_peer(newsk) = sk;
newsk->sk_state = TCP_ESTABLISHED;
newsk->sk_type = sk->sk_type;
...
sk->sk_state = TCP_ESTABLISHED;
unix_peer(sk) = newsk;

// 4. 把连接中的一头(新 socket)放到服务器接收队列中
__skb_queue_tail(&other->sk_receive_queue, skb);
}

//file: net/unix/af_unix.c
#define unix_peer(sk) (unix_sk(sk)->peer)

收发包过程和复杂的 TCP 发送接收过程相比,这里的发送逻辑简单简单到令人发指。申请一块内存(skb),把数据拷贝进去。根据 socket 对象找到另一端,直接把 skb 给放到对端的接收队列里了

Image

Unix Domain Socket和127.0.0.1通信相比,如果包的大小是1K以内,那么性能会有一倍以上的提升,包变大后性能的提升相对会小一些。

Image

Image

再来一个整体流转矢量图:

image-20211116101345648

案例

snat/dnat 宿主机port冲突,丢包

image-20230726101807001

  1. snat 就是要把 192.168.1.10和192.168.1.11的两个连接替换成宿主机的ip:port

  2. 主要是在宿主机找可用port分别给这两个连接用

  3. 找可用port分两步

    • 找到可用port
      
    • 将可用port写到数据库,后面做连接追踪用(conntrack)
      
  4. 上述两步不是事务,可能两个连接同时找到一个相同的可用port,但是只有第一个能写入成功,第二个fail,fail后这个包被扔掉

  5. 1秒钟后被扔掉的包重传,后续正常

症状:

  • 问题发生概率不高,跟压力没有关系,跟容器也没有关系,只要有snat/dnat和并发就会发生,只发生在创建连接的第一个syn包

  • 可以通过conntrack工具来检查fail的数量

  • 实际影响只是请求偶尔被拉长了1秒或者3秒

  • snat规则创建的时候增加参数:NF_NAT_RANGE_PROTO_RANDOM_FULLY 来将冲突降低几个数量级—-可以认为修复了这个问题

      sudo conntrack -L -d ip-addr
    

来自:https://tech.xing.com/a-reason-for-unexplained-connection-timeouts-on-kubernetes-docker-abd041cf7e02

容器(bridge)通过udp访问宿主机服务失败

image.png

这个案例主要是讲述回包的逻辑,如果是tcp,那么用dest ip当自己的source ip,如果是UDP,无连接状态信息,那么会根据route来选择一块网卡(上面的IP) 来当source ip

来自:http://cizixs.com/2017/08/21/docker-udp-issue
https://github.com/moby/moby/issues/15127

参考资料

The Missing Man Page for ifconfig–关于ifconfig的种种解释

Linux数据报文的来龙去脉

linux-network-performance-parameters

Linux之TCPIP内核参数优化

https://access.redhat.com/sites/default/files/attachments/20150325_network_performance_tuning.pdf

Linux 网络协议栈收消息过程-Ring Buffer : 支持 RSS 的网卡内部会有多个 Ring Buffer,NIC 收到 Frame 的时候能通过 Hash Function 来决定 Frame 该放在哪个 Ring Buffer 上,触发的 IRQ 也可以通过操作系统或者手动配置 IRQ affinity 将 IRQ 分配到多个 CPU 上。这样 IRQ 能被不同的 CPU 处理,从而做到 Ring Buffer 上的数据也能被不同的 CPU 处理,从而提高数据的并行处理能力。

Linux 网络栈监控和调优:发送数据

Linux 网络栈监控和调优:接收数据(2016) 英文版

收到包后内核层面的处理:从网卡注册软中断处理函数到收包逻辑

收到包后应用和协议层面的处理:图解 | 深入理解高性能网络开发路上的绊脚石 - 同步阻塞网络 IOhttps://mp.weixin.qq.com/s/cIcw0S-Q8pBl1-WYN0UwnA 当软中断上收到数据包时会通过调用 sk_data_ready 函数指针(实际被设置成了 sock_def_readable()) 来唤醒在 sock 上等待的进程

http://docshare02.docshare.tips/files/21804/218043783.pdf

https://wiki.linuxfoundation.org/networking/kernel_flow

https://upload.wikimedia.org/wikipedia/commons/thumb/3/37/Netfilter-packet-flow.svg/2000px-Netfilter-packet-flow.svg.png

https://wiki.nix-pro.com/view/Packet_journey_through_Linux_kernel

https://blog.packagecloud.io/eng/2017/02/06/monitoring-tuning-linux-networking-stack-sending-data/

美团redis丢包,调整多队列,绑核,软中断绑定node0

网络通不通是个大问题–半夜鸡叫

网络通不通是个大问题–半夜鸡叫

半夜鸡叫

凌晨啊,还有同学在为网络为什么不通的问题搏斗着:

undefined

问题描述大概如下:

slb后面配了一台realserver(就叫172吧), 在172上通过curl http://127.0.0.1:80/ 是正常的(说明服务自身是正常的)
如果从开发同学的笔记本直接curl slb-ip 就卡住了,进一步发现如果从北京的办公网curl slb-ip就行,但是从杭州的curl slb-ip就不行。

从杭州curl的时候在172上抓包如下:
undefined

明显可以看到tcp握手包正确到达了172,但是172一直没有回复。也就是如果是杭州访问服务的话,服务端收到握手请求后直接扔掉没有任何回复(回想下哪些场景会扔包)

问题排查

先排除了iptables的问题

略过

route 的嫌疑

因为北京可以杭州不行,明显是某些IP可以,于是检查route 表,解决问题的必杀技(基础知识)都在这里

发现杭州的ip和北京的ip确实命中了不同的路由规则,简单说就是172绑在eth0上,机器还有另外一块网卡eth1. 而回复杭州ip的时候要走eth1. 至于为什么没有从eth1回复等会再说

知道原因就好说了,修改一下route,让eth0成为默认路由,这样北京、杭州都能走eth0进出了

所以到这里,问题描述如下:
undefined

机器有两块网卡,请求走eth0 进来(绿线),然后走 eth1回复(路由决定的,红线),但是实际没走eth1回复,像是丢包了。

解决办法

修改一下route,让eth0成为默认路由,这样北京、杭州都能走eth0进出了

为什么5U的机器可以

开发同学接着反馈,出问题的172是7U2的系统,但是还有一些5U7的机器完全正常,5U7的机器上也是两块网卡,route规则也是一样的。

这确实诡异,看着像是7U的内核行为跟5U不一致,咨询了内核同学,让检查一下 rp_filter 参数。果然看到7U2的系统默认 rp_filter 开着,而5U7是关着的,于是反复开关这个参数稳定重现了问题

1
sysctl -w net.ipv4.conf.eth0.rp_filter=0 

rp_filter 原理和监控

rp_filter参数用于控制系统是否开启对数据包源地址的校验, 收到包后根据source ip到route表中检查是否否和最佳路由,否的话扔掉这个包【可以防止DDoS,攻击等】

​ 0:不开启源地址校验。
​ 1:开启严格的反向路径校验。对每个进来的数据包,校验其反向路径是否是最佳路径。如果反向路径不是最佳路径,则直接丢弃该数据包。
​ 2:开启松散的反向路径校验。对每个进来的数据包,校验其源地址是否可达,即反向路径是否能通(通过任意网口),如果反向路径不通,则直接丢弃该数据包。

那么对于这种丢包,可以打开日志:/proc/sys/net/ipv4/conf/eth0/log_martians 来监控到:

undefined

rp_filter: IP Reverse Path Filter, 在ip层收包的时候检查一下反向路径是不是最优路由,代码位置:

ip_rcv->NF_HOOK->ip_rcv_finish->ip_rcv_finish_core

也就是rp_filter在收包的流程中检查每个进来的包,是不是符合rp_filter规则,而不是回复的时候来做判断,这也就是为什么抓包只能看到进来的syn就没有然后了

开启rp_filter参数的作用

  • 减少DDoS攻击: 校验数据包的反向路径,如果反向路径不合适,则直接丢弃数据包,避免过多的无效连接消耗系统资源。
  • 防止IP Spoofing: 校验数据包的反向路径,如果客户端伪造的源IP地址对应的反向路径不在路由表中,或者反向路径不是最佳路径,则直接丢弃数据包,不会向伪造IP的客户端回复响应。

通过netstat -s来观察IPReversePathFilter

$netstat -s | grep -i filter
	IPReversePathFilter: 35428
$netstat -s | grep -i filter
	IPReversePathFilter: 35435

能明显看到这个数字在增加,如果没开rp_filter 就看不到这个指标或者数值不变

undefined

问题出现的时候,尝试过用 watch -d -n 3 ‘netstat -s’ 来观察过哪些指标在变化,只是变化的指标太多,留意不过来,或者只是想着跟drop、route有关的参数

1
2
3
4
$netstat -s |egrep -i "drop|route"
12 dropped because of missing route
30 SYNs to LISTEN sockets dropped
InNoRoutes: 31

当时观察到这几个指标,都没有变化,实际他们看着像但是都跟rp_filter没关系,最后我打算收藏如下命令保平安:

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
$netstat -s |egrep -i "drop|route|overflow|filter|retran|fails|listen"
12 dropped because of missing route
30 times the listen queue of a socket overflowed
30 SYNs to LISTEN sockets dropped
IPReversePathFilter: 35435
InNoRoutes: 31

$nstat -z -t 1 | egrep -i "drop|route|overflow|filter|retran|fails|listen"
IpOutNoRoutes 0 0.0
TcpRetransSegs 20 0.0
Ip6InNoRoutes 0 0.0
Ip6OutNoRoutes 0 0.0
Icmp6InRouterSolicits 0 0.0
Icmp6InRouterAdvertisements 0 0.0
Icmp6OutRouterSolicits 0 0.0
Icmp6OutRouterAdvertisements 0 0.0
TcpExtLockDroppedIcmps 0 0.0
TcpExtArpFilter 0 0.0
TcpExtListenOverflows 0 0.0
TcpExtListenDrops 0 0.0
TcpExtTCPPrequeueDropped 0 0.0
TcpExtTCPLostRetransmit 0 0.0
TcpExtTCPFastRetrans 0 0.0
TcpExtTCPForwardRetrans 0 0.0
TcpExtTCPSlowStartRetrans 0 0.0
TcpExtTCPBacklogDrop 0 0.0
TcpExtTCPMinTTLDrop 0 0.0
TcpExtTCPDeferAcceptDrop 0 0.0
TcpExtIPReversePathFilter 2 0.0
TcpExtTCPTimeWaitOverflow 0 0.0
TcpExtTCPReqQFullDrop 0 0.0
TcpExtTCPRetransFail 0 0.0
TcpExtTCPOFODrop 0 0.0
TcpExtTCPFastOpenListenOverflow 0 0.0
TcpExtTCPSynRetrans 10 0.0
IpExtInNoRoutes 0 0.0

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

  • 网络不通,诊断: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

总结

本质原因就是服务器开启了 rp_filter 为1,严格校验进出包是否走同一块网卡,而如果请求从杭州机房发过来的话,回复包的路由走的是另外一块网卡,触发了内核的rp_filter扔包逻辑。

改server的路由可以让杭州的包也走同一块网卡,就不扔掉了。当然将 rp_filter 改成0 关掉这个校验逻辑也可以完全避免这个扔包。

从问题的解决思路来说,基本都可以认定是握手的时候服务器扔包了。只有知道 rp_filter 参数的内核老司机可以直接得出是这里的原因。如果对于一个新手的话还是得掌握如何推理分析得到原因。

就是要你懂网络--一个网络包的旅程

就是要你懂网络–一个网络包的旅程


写在最前面的

我相信你脑子里关于网络的概念都在下面这张图上,但是乱成一团麻,这就是因为知识没有贯通、没有实践、没有组织

image.png

上面的概念在RFC1180中讲的无比的通熟易懂和连贯,但是抱歉,当时你也许看懂了,但是一个月后又忘记了,或者碰到问题才发现之前即使觉得看懂了的东西实际没懂,我发现大多人看 RFC1180、教科书基本当时都能看到,但就是一到实践就不会了,这里的鸿沟我分析应该就是缺少实践认知。

所以这篇文章希望解决书本知识到实践的贯通,希望把网络概念之间的联系通过实践来组织起来

从一个网络ping不通的问题开始

当时的网络链路是(大概是这样,略有简化):

容器1->容器1所在物理机1->交换机->物理机2
  • 从容器1 ping 物理机2 不通;
  • 从物理机1上的容器2 ping物理机2 通;
  • 物理机用一个vlan,容器用另外一个vlan
  • 交换机都做了trunk,让两个vlan都允许通过(肯定没问题,因为容器2是通的)
  • 同时发现即使是通的,有的容器 ping物理机1只需要0.1ms,有的容器需要200ms以上(都在同一个交换机下),不合理
  • 所有容器 ping 其它外网IP反而是通的

这个问题扯了一周是因为容器的网络是我们自己配置的,交换机我们没有权限接触,由客户配置。出问题的时候都会觉得自己没问题对方有问题,另外就是对网络基本知识认识不够所以都觉得自己没问题。

这个问题的答案在大家看完本文的基础知识后会总结出来。

开始前大家先想想,假如有个面试题是:输入 ping IP后 敲回车,然后发生了什么?

route 路由表

$route -n
Kernel IP routing table
Destination Gateway Genmask Flags Metric RefUse Iface
0.0.0.0     10.125.15.254   0.0.0.0 UG0  00 eth0
10.0.0.0    10.125.15.254   255.0.0.0   UG0  00 eth0
10.125.0.0  0.0.0.0 255.255.240.0   U 0  00 eth0
11.0.0.0    10.125.15.254   255.0.0.0   UG0  00 eth0
30.0.0.0    10.125.15.254   255.0.0.0   UG0  00 eth0
100.64.0.0  10.125.15.254   255.192.0.0 UG0  00 eth0
169.254.0.0 0.0.0.0 255.255.0.0 U 1002   00 eth0
172.16.0.0  10.125.15.254   255.240.0.0 UG0  00 eth0
172.17.0.0  0.0.0.0 255.255.0.0 U 0  00 docker0
192.168.0.0 10.125.15.254   255.255.0.0 UG0  00 eth0

假如你现在在这台机器上ping 172.17.0.2 根据上面的route表得出 172.17.0.2这个IP匹配到下面这条路由:

172.17.0.0  0.0.0.0 255.255.0.0 U 0  00 docker0

那么ping 包会从docker0这张网卡发出去。

但是如果是ping 10.125.4.4 根据路由规则应该走eth0这张网卡。

也就是:route/路由表 来帮我们匹配目标地址(一个目标地址只能匹配一条路由,匹配不到就报no route to host 错误)

现在根据路由我们已经知道目标ip将要走哪个网卡出去,接下来就要判断目标IP是否在同一个子网了

ifconfig

首先来看看这台机器的网卡情况:

$ifconfig
docker0: flags=4099<UP,BROADCAST,MULTICAST>  mtu 1500
    inet 172.17.42.1  netmask 255.255.0.0  broadcast 0.0.0.0
    ether 02:42:49:a7:dc:ba  txqueuelen 0  (Ethernet)
    RX packets 461259  bytes 126800808 (120.9 MiB)
    RX errors 0  dropped 0  overruns 0  frame 0
    TX packets 462820  bytes 103470899 (98.6 MiB)
    TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
    inet 10.125.3.33  netmask 255.255.240.0  broadcast 10.125.15.255
    ether 00:16:3e:00:02:67  txqueuelen 1000  (Ethernet)
    RX packets 280918095  bytes 89102074868 (82.9 GiB)
    RX errors 0  dropped 0  overruns 0  frame 0
    TX packets 333504217  bytes 96311277198 (89.6 GiB)
    TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
    inet 127.0.0.1  netmask 255.0.0.0
    loop  txqueuelen 0  (Local Loopback)
    RX packets 1077128597  bytes 104915529133 (97.7 GiB)
    RX errors 0  dropped 0  overruns 0  frame 0
    TX packets 1077128597  bytes 104915529133 (97.7 GiB)
    TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

这里有三个IP,三个子网掩码(netmask),根据目标路由走哪张网卡,得到这个网卡的子网掩码,来计算目标IP是否在这个子网内。

如果目标ip在子网内,那就是大家说的在同一个二层网络,直连可以通;如果目标ip和本机不在同一个子网那么本机只管将网络包发给本机网关,剩下的由网关按照以上逻辑不停地往外走直到发送给目标机器(也就是网关拿到这个包后先查自己的路由,然后按照路由扔给下一跳)

直连可通的意思是:本机发广播包对方能收到,这个时候就要来到ARP 广播找对方机器的Mac地址了(如果不是同一个二层,就是转发给网关,那么这里同样也是ARP 广播找网关机器的Mac–本机和网关一定在同一个子网)

ARP协议

网络包在物理层传输的时候依赖的mac 地址而不是上面目的的IP地址,也就是根据mac地址来决定把包发到哪里去。

ARP 协议就是查询某个IP地址的mac地址是多少,由于这种对应关系一般不太变化,所以每个os都有一份arp缓存(一般15分钟过期),也可以手工清理,下面是arp缓存的内容:

$arp -a
e010125011202.bja.tbsite.net (10.125.11.202) at 00:16:3e:01:c2:00 [ether] on eth0
? (10.125.15.254) at 0c:da:41:6e:23:00 [ether] on eth0
v125004187.bja.tbsite.net (10.125.4.187) at 00:16:3e:01:cb:00 [ether] on eth0
e010125001224.bja.tbsite.net (10.125.1.224) at 00:16:3e:01:64:00 [ether] on eth0
v125009121.bja.tbsite.net (10.125.9.121) at 00:16:3e:01:b8:ff [ether] on eth0
e010125009114.bja.tbsite.net (10.125.9.114) at 00:16:3e:01:7c:00 [ether] on eth0
v125012028.bja.tbsite.net (10.125.12.28) at 00:16:3e:00:fb:ff [ether] on eth0
e010125005234.bja.tbsite.net (10.125.5.234) at 00:16:3e:01:ee:00 [ether] on eth0

进入正题,ping后回车后发生什么

首先 OS需要把ping命令封成一个icmp包,需要填上包头(包括IP、mac地址),那么OS先根据目标IP和本机的route规则计算使用哪个interface(网卡),每条路由规则基本都包含目标IP范围、网关、网卡这样几个基本元素。

如果目标IP在同一子网

如果目标IP和本机IP是同一个子网(根据本机ifconfig上的每个网卡的netmask来判断),并且本机arp缓存没有这条IP对应的mac记录,那么给整个子网的所有机器广播发送一个 arp查询

比如我ping 10.125.3.42,然后tcpdump抓包看到的arp请求:

$sudo tcpdump -i eth0  arp
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 65535 bytes
16:22:01.792501 ARP, Request who-has e010125003042.bja.tbsite.net tell e010125003033.bja, length 28
16:22:01.792566 ARP, Reply e010125003042.bja.tbsite.net is-at 00:16:3e:01:8d:ff (oui Unknown), length 28

Host to Host through a Switch - Switch Functions animation

上面就是本机发送广播消息,10.125.3.42的mac地址是多少,很快10.125.3.42回复了自己的mac地址。
收到这个回复后,先缓存起来,下个ping包就不需要再次arp广播了。
然后将这个mac地址填写到ping包的包头的目标Mac(icmp包),然后发出这个icmp request包,同一个子网,按照MAC地址,正确到达目标机器,然后对方正确回复icmp reply【对方回复也要查路由规则,arp查发送放的mac,这样回包才能正确路由回来,略过】。

来看一次完整的ping 10.125.3.43,tcpdump抓包结果:

$sudo tcpdump -i eth0  arp or icmp
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 65535 bytes
16:25:15.195401 ARP, Request who-has e010125003043.bja.tbsite.net tell e010125003033.bja, length 28
16:25:15.195459 ARP, Reply e010125003043.bja.tbsite.net is-at 00:16:3e:01:0c:ff (oui Unknown), length 28
16:25:15.211505 IP e010125003033.bja > e010125003043.bja.tbsite.net: ICMP echo request, id 27990, seq 1, length 64
16:25:15.212056 IP e010125003043.bja.tbsite.net > e010125003033.bja: ICMP echo reply, id 27990, seq 1, length 64

接着再 ping 一次同一个IP地址,arp有缓存了就看不到arp广播查询过程了。

如果目标IP不是同一个子网

arp只是同一子网广播查询,如果目标IP不是同一子网的话就要经过本IP网关就行转发,如果本机没有缓存网关mac(一般肯定缓存了),那么先发送一次arp查询网关的mac,然后流程跟上面一样,只是这个icmp包发到网关上去了(mac地址填写的是网关的mac)

从本机10.125.3.33 ping 11.239.161.60的过程,因为不是同一子网按照路由规则匹配,根据route表应该走10.125.15.254这个网关,如下截图:

image.png

首先是目标IP 11.239.161.60 符合最上面红框中的路由规则,又不是同一子网,所以查找路由规则中的网关10.125.15.254的Mac地址,arp cache中有,于是将 0c:da:41:6e:23:00 填入包头,那么这个icmp request包就发到10.125.15.254上了,虽然包头的mac是 0c:da:41:6e:23:00,但是IP还是 11.239.161.60.

看看目标IP 11.239.161.60 的真正mac信息(跟ping包包头的Mac是不同的):

eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
    inet 11.239.161.60  netmask 255.255.252.0  broadcast 11.239.163.255
    ether 00:16:3e:00:04:c4  txqueuelen 1000  (Ethernet)

这个包根据Mac地址路由到了网关上

网关接下来怎么办

为了简化问题,假设两个网关直连

网关收到这个包后(因为mac地址是她的),打开一看IP地址是 11.239.161.60,不是自己的,于是继续查自己的route和arp缓存,发现11.239.161.60这个IP的网关是11.239.163.247,于是把包的目的mac地址改成11.239.163.247的mac继续发出去。

11.239.163.247这个网关收到包后,一看 11.239.161.60是自己同一子网的IP,于是该arp广播找mac就广播,cache有就拿cache的,然后这个包才最终到达目的11.239.161.60上。

整个过程中目标mac地址每一跳都在变,IP地址不变,每经过一次变化可以简单理解从一跳。

实际上可能要经过多个网关多次跳跃才能真正到达目标机器

目标收到这个icmp包后的回复过程一样,略过。

arp广播风暴和arp欺骗

广播风暴:如果一个子网非常大,机器非常多,每次arp查询都是广播的话,也容易因为N*N的问题导致广播风暴。

arp欺骗:同样如果一个子网中的某台机器冒充网关或者其他机器,当收到arp查询的时候总是把自己的mac冒充目标机器的mac发给你,然后你的包先走到他,为了不被发现达到自己的目的后再转发给真正的网关或者机器,所以在里面都点什么手脚,看看你发送的内容都还是很容易的

讲完基础再来看开篇问题的答案

分别在两个物理机上抓包

在物理机2上抓包:

image.png

tcpdump: listening on em1, link-type EN10MB (Ethernet), capture size 65535 bytes
f4:0f:1b:ae:15:fb > 18:66:da:f0:15:90, ethertype 802.1Q (0x8100), length 102: vlan 134, p 0, ethertype IPv4, (tos 0x0, ttl 63, id 5654, offset 0, flags [DF], proto ICMP (1), length 84)
10.159.43.162 > 10.159.43.1: ICMP echo request, id 6285, seq 1, length 64
18:66:da:f0:15:90 > 00:00:0c:9f:f0:86, ethertype 802.1Q (0x8100), length 102: vlan 134, p 0, ethertype IPv4, (tos 0x0, ttl 64, id 21395, offset 0, flags [none], proto ICMP (1), length 84)
10.159.43.1 > 10.159.43.162: ICMP echo reply, id 6285, seq 1, length 64

这个抓包能看到核心证据,ping包有到达物理机2,同时物理机2也正确回复了(mac、ip都对)

同时在物理机1上抓包只能看到ping包出去,回包没有到物理机1(所以回包肯定不会到容器里了)

所以问题的核心在交换机没有正确把物理机2的回包送到物理机1上面。

同时观察到的不正常延时:
image.png

过程中的其它测试:

  1. 新拿出一台物理机配置上不通的容器的IP,这是通的,所以客户坚持是容器网络的配置;
  2. 怀疑不通的IP所使用的mac地址冲突,在交换机上清理了交换机的arp缓存,没有帮助,还是不通

对于1能通,我认为这个测试不严格,新物理机所用的mac不一样,并且所接的交换机口也不一样,影响了测试结果。

最终的原因

最后在交换机上分析包没正确发到物理机1上的原因跟客户交换机使用了HSRP(热备份路由器协议,就是多个交换机HA高可用,也就是同一子网可以有多个网关的IP),停掉HSRP后所有IP容器都能通了,并且前面的某些容器延时也恢复正常了。

通俗点说就是HSRP把回包拐跑了,有些回包拐跑了又送回来了(延时200ms那些)

至于HSRP为什么会这么做,要厂家出来解释了。

大概结构如下图:

undefined

关于HSRP和VRRP

VRRP是虚拟路由冗余协议的简称,这个协议的目的是为了让多台路由器共同组成一个虚拟路由器,从而解决单点故障。

使用VRRP的网络架构大致如上面这个图所示,其中Master和Slave共同组成了一个虚拟路由器,这台虚拟路由器的IP是1.1.1.1,同时还会有一个虚拟的mac地址,所有主机的默认网关IP都将设置成1.1.1.1。

假设主机H1需要对外发送数据,在发送IP数据包时主机H1需要知道1.1.1.1这个IP对应的物理地址,因此H1会向外广播一个ARP请求,询问1.1.1.1这个IP数据包对应的物理地址。此时,Master将会负责响应这个APR请求,将虚拟的mac地址报告给主机H1,主机H1就用这个物理地址发送IP数据包。

当IP数据包到达交换机Switch A的时候,Switch A需要知道应该把这个数据包转发到哪条链路去,这个时候Switch A也会广播一个ARP请求,看看哪条链路会响应这个ARP请求。同样,Master会响应这个ARP请求,从而Switch A就知道了应该把数据包从自己的eth0对应的这条链路转发出去。此时,Master就是真正负责整个网络对外通信的路由器。

当Master出现故障的时候,通过VRRP协议,Slave可以感知到这个故障(通过类似于心跳的方式),这个时候Slave会主动广播一个ARP消息,告诉Switch A应该从eth1对应的链路转发物理地址是虚拟mac地址的数据包。这样就完成了主备路由器的切换,这个过程对网络中的主机来说是透明的。

通过VRRP不仅可以实现1主1备的部署,还可以实现1主多备的部署。在1主多备的部署结构下,当Master路由器出现故障,多个Backup路由器会通过选举的方式产生一个新的Master路由器,由这个Master路由器来响应ARP请求。

除了利用VRRP屏蔽单点故障之外,还可以实现负载均衡。在主备部署的情况下,Backup路由器其实是空转的,并不负责数据包的路由工作,这样显然是有点浪费的。此时,为了让Backup也负责一部分的路由工作,可以将两台路由器配制成互为主备的模式,这样就形成了两台虚拟路由器,网络中的主机可以选择任意一台作为默认网关。这种互为主备的模式也可以应用到1主多备的部署方式下。比如由3台路由器,分别是R1,R2和R3,用这3台路由器可以组成3台虚拟路由器,一台虚拟路由器以R1为Master,R2和R3为Backup路由器,另外一台以R2为Master,R1和R3为Backup路由器,第三台则以R3为Master,R1和R2为Backup路由器。

通过VRRP,可以实现LVS的主备部署,屏蔽LVS单点故障对应用服务器的影响。

网络到底通不通是个复杂的问题

讲这个过程的核心目的是除了真正的网络不通,有些是服务不可用了也怪网络。很多现场的同学根本讲不清自己的服务(比如80端口上的tomcat服务)还在不在,网络通不通,网络不通的话该怎么办?

实际这里涉及到四个节点(以两个网关直连为例),srcIP -> src网关 -> dest网关 -> destIP.如果ping不通(也有特殊的防火墙限制ping包不让过的),那么分段ping(二分查找程序员应该最熟悉了)。 比如前面的例子就是网关没有把包转发回来

抓包看ping包有没有出去,对方抓包看有没有收到,收到后有没有回复。

ping自己网关能不能通,ping对方网关能不能通

接下来说点跟程序员日常相关的

如果网络能ping通,服务无法访问

那么尝试telnet IP port 看看你的服务监听的端口是否还在,在的话是否能正常响应新的连接。有时候是进程挂掉了,端口也没人监听了。有时候是进程还在但是死掉了,所以端口也不响应新的请求了。

如果端口还在也是正常的话,telnet应该是好的:

$telnet 11.239.161.60 2376
Trying 11.239.161.60...
Connected to 11.239.161.60.
Escape character is '^]'.
^C
Connection closed by foreign host.

假如我故意换成一个不存在的端口,目标机器上的OS直接就拒绝了这个连接(抓包的话一般是看到reset标识):

$telnet 11.239.161.60 2379
Trying 11.239.161.60...
telnet: connect to address 11.239.161.60: Connection refused

一个服务不响应,然后首先怀疑网络不通、丢包的Case

当时的反馈应用代码抛SocketTimeoutException,怀疑网络问题:

  1. tsar检查,发现retran率特别高,docker容器(tlog-console)内达到50,物理机之间的retran在1-2之间。
  2. Tlog连接Hbase,出现大量连接断开,具体日志见附件,Hbase服务器完全正常,Hbase同学怀疑retran比较高导致。
  3. 业务应用连接Diamond 偶尔会出现超时异常,具体日志见附件。
  4. 业务很多这样的异常日志:[Diamond SocketTimeoutException]
  5. 有几台物理机io偶然情况下会飙升到80多。需要定位解决。

其实当时看到tsar监控retran比较高,我也觉得网络有问题,但是我去看的时候网络又非常好,于是我看了一下出问题时间段的网卡的流量信息也非常正常:

image.png

上图是通过sar监控到的9号 10.16.11.138(v24d9e0f23d40) 这个网卡的流量,看起来也是正常,流量没有出现明显的波动(10.16.11.138 出问题容器对应的网卡名:v24d9e0f23d40)

为了监控网络到底有没有问题,接着在出问题的两个容器上各启动一个http server,然后在对方每1秒钟互相发一次发http get请求,基本认识告诉我们如果网络丢包、卡顿,那么我这个http server的监控日志时间戳也会跳跃,如果应用是因为网络出现异常那么我启动的http服务也会出现异常。

实际监控来看,应用出异常的时候我的http服务是正常的(写了脚本判断日志的连续性,没问题):

image.png

这也强有力地证明了网络没问题,所以大家集中火力查看应用的问题。后来的实际调查发现是应用假死掉了(内部线程太多,卡死了),服务端口不响应请求了。

TCP建连接过程跟前面ping一样,只是把ping的icmp协议换成TCP协议,也是要先根据route,然后arp。

总结

网络丢包,卡顿,抖动很容易做背包侠,找到正确的原因解决问题才会更快,要不在错误的路径上怎么发力都不对。准的方向要靠好的基础知识和正确的逻辑以及证据来支撑,而不是猜测

  • 有重传的时候(或者说重传率高的时候),ping有可能是正常的(icmp包网卡直接返回);
  • 重传高,一般是tcp retrans,可能应用不响应,可能操作系统软中断太高等
  • ping只是保证网络链路是否通畅

这些原理基本都在RFC1180中阐述的清晰简洁,图文并茂,结构逻辑合理,但是对于90%的程序员没有什么卵用,因为看完几周后就忘得差不多。对于普通人来说还是要通过具体的案例来加深理解。

一流的人看RFC就够了,差一些的人看《TCP/IP卷1》,再差些的人要看一个个案例带出来的具体知识的书籍了,比如《wireshark抓包艺术》,人和人的学习能力有差别必须要承认。


参考文章:

https://tools.ietf.org/html/rfc1180

https://www.practicalnetworking.net/series/packet-traveling/packet-traveling/

Computer Networking Introduction - Ethernet and IP (Heavily Illustrated) 这篇凑合吧,其实没我这篇写得好,不过这个博客还有些别的文章也不错

netstat定位性能案例

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

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

定制 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上域名解析失败

中间件的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解析过程

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域名异常无法上网

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

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和抓包能够看到问题的本质
  • 根因: https://access.redhat.com/solutions/1426263
  • 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 系统如何处理名称解析

网络丢包

网络丢包

查看网卡是否丢包,一般是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/