orange723

orange723

4 posts

TCP 连接的建立

实验流程来自 知识星球:程序员踩坑案例分享

创建连接

sudo tcpdump -s0 -X -nn "tcp port 9527" -w vm-1-tcp.pcap --print

vm-1-tcp.pcap

当时碰到个问题,在 vm-1 用 "nc -k -l vm-1 9527",vm-2 连接 vm-1 时 vm-1 窗口收不到消息

在两台 vm 里 hosts 文件加了对端的机器名和 ip

vm-1
198.19.249.151 vm-2

vm-2
198.19.249.111 vm-1

vm-1 上抓包看下

sudo tcpdump -s0 -X -nn "tcp port 9527" -w vm-1-tcp-nc-localhost.pcap --print

vm-1-tcp-nc-localhost.pcap

在看 vm-1 监听的情况

sudo netstat -anpt

监听 127.0.0.1 去了,另外一块网卡没监听

vm-2 发 syn 给 vm-1,vm-1 直接回了个 rst,然后 vm-1 根据 net.ipv4.tcp_syn_retries 不停的重试

sudo sysctl -a|grep net.ipv4.tcp_syn_retries
net.ipv4.tcp_syn_retries = 6

但是我抓包发现会重传 10 次 共 11 个包

再次抓包验证

sudo tcpdump -s0 -X -nn "tcp port 9527" -w vm-1-tcp-nc-localhost-retries.pcap --print

vm-1-tcp-nc-localhost-retries.pcap

很奇怪,和 net.ipv4.tcp_syn_linear_timeouts=6 的现象不一样,正常只应该有 7 个包,一个正常 syn 和 6 个重试

这时还有一个现象,正常来说 “指数退避” 应该是 1 2 4 8,但抓包前 4 次均是相隔 1s,第5个重试包才相隔 2s,根据这个现象和当前内核版本查询到

net.ipv4.tcp_syn_linear_timeouts

tcp_syn_linear_timeouts - INTEGER
The number of times for an active TCP connection to retransmit SYNs with a linear backoff timeout before defaulting to an exponential backoff timeout. This has no effect on SYNACK at the passive TCP side.

With an initial RTO of 1 and tcp_syn_linear_timeouts = 4 we would expect SYN RTOs to be: 1,1,1,1,1,2,4,... (4 linear timeouts,and the first exponential backoff using 2^0 * initial_RTO). Default: 4

这就对了,后面更改 net.ipv4.tcp_syn_linear_timeouts 在继续测试

sudo sysctl -w net.ipv4.tcp_syn_retries=6 net.ipv4.tcp_syn_linear_timeouts=1

正常是 10 次,现在应该是 7 次

sudo tcpdump -s0 -X -nn "tcp port 9527" -w vm-1-tcp-nc-localhost-retries-syn-linear.pcap --print

vm-1-tcp-nc-localhost-retries-syn-linear.pcap

while true;do sudo netstat -anpo|grep 9527;sleep 1;done

没错到7次自动停了

后面改成 nc -k -l 192.168.139.111 9527 直接就通了

观测 SYN_SENT

vm-1 使用 iptables drop vm-2 发来的 syn 包

sudo iptables -A INPUT -p tcp --dport 9527 -j DROP
sudo tcpdump -s0 -X -nn "tcp port 9527" -w vm-1-tcp-iptables-drop-9527.pcap --print

vm-1-tcp-iptables-drop-9527.pcap

能看到这回是 tcp retransmission,重传了 10 次 依旧是这两个参数控制

net.ipv4.tcp_syn_retries
net.ipv4.tcp_syn_linear_timeouts

能看到 vm-2 连接状态 SYN_SENT

观测 SYN_RECV

需要在 vm-2 drop 从 vm-1 传过来的 SYN+ACK 包,这样 vm-2 收不到 SYN+ACK 就没办法回 ACK,vm-1 也没办法将三次握手完成

sudo iptables -A INPUT -p tcp --sport 9527 -j DROP

改用 nmap 测试连接

sudo nmap -sS 192.168.139.111 -p 9527

vm-1 查看连接状态

while true;do sudo netstat -anpo|grep 9527;sleep 1;done
sudo tcpdump -s0 -X -nn "tcp port 9527" -w vm-1-tcp-iptables-vm2-drop-9527.pcap --print

vm-1-tcp-iptables-vm2-drop-9527.pcap

能看到 vm-2 >(SYN) vm-1,vm-1 >(SYN+ACK) vm-2,然后 vm-1 一直在重试,试了5次

net.ipv4.tcp_synack_retries 默认是5

tcp_synack_retries - INTEGER
Number of times SYNACKs for a passive TCP connection attempt will be retransmitted. Should not be higher than 255. Default value is 5, which corresponds to 31seconds till the last retransmission with the current initial RTO of 1second. With this the final timeout for a passive TCP connection will happen after 63seconds.

文中提到:SYN FLOOD

客户端发了 1 个 SYN 到服务端,如果客户端不响应那服务端就会重试 5 次,一台机器是 5 次如果机器多服务端资源很快就会被消耗

文中提到:如果只使用 iptables 拦截第二次握手包的话,会导致源端协议栈 SYN 重传的,这样就没法测试 SYN+ACK 重传了。所以发送端在发完 SYN 包后不能有其他逻辑。nc 做不到只发送 SYN 包就退出,改用 nmap 来进行实验。

复现下 用 nc 然后抓包

sudo tcpdump -s0 -X -nn "tcp port 9527" -w vm-1-tcp-iptables-vm2-drop-9527-nc.pcap --print

vm-1-tcp-iptables-vm2-drop-9527-nc.pcap

果然 vm-2 在重传

SYN Queue

借用下文中的图

(图片来自:https://www.emqx.com/en/blog/emqx-performance-tuning-tcp-syn-queue-and-accept-queue)

验证下半连接队列长度,修改相关的内核参数

sudo sysctl -w net.ipv4.tcp_syncookies=0 net.ipv4.tcp_max_syn_backlog=4 net.core.somaxconn=8

vm-2 测试

while true;do sudo nmap -sS 192.168.139.111 -p 9527;done

vm-1 查看状态,又和修改的内核参数对应不上

$ sudo netstat -anpo|grep RECV
tcp        0      0 192.168.139.111:9527    192.168.139.151:35013   SYN_RECV    -                    on (1.82/2/0)
tcp        0      0 192.168.139.111:9527    192.168.139.151:57984   SYN_RECV    -                    on (1.76/2/0)

半连接取值的规则是这样

min(backlog, net.core.somaxconn, net.ipv4.tcp_max_syn_backlog)

syn_backlog 和 somaxconn 设置的都不是 2,唯一有关系的就是 backlog,backlog没有改直接用的 nc

nc -k -l 192.168.139.111 9527
$ sudo ss -anpt
State     Recv-Q    Send-Q         Local Address:Port       Peer Address:Port   Process
LISTEN    0         1            192.168.139.111:9527            0.0.0.0:*       users:(("nc",pid=37710,fd=3))

关于 ss 的 Send-Q 解释

High Send-Q means the data is put on TCP/IP send buffer, but it is not sent or it is sent but not ACKed

表示数据在 tcp/ip 发送缓存中,但未发送或已发送但未 ack

对比我们情况就是 vm-2 拦截了 vm-1 发过来的 syn+ack,未回复 ack

也就是 nc 的 backlog 设置的是 1,server 的半连接队列只允许有1个等待

用 go 写一个

package main

import (
	"fmt"
	"log"
	"net"
	"time"
)

func main() {
	fmt.Print("h")
	conn,err := net.Listen("tcp4","0.0.0.0:9527")
	if err != nil {
		panic(err)
	}
	defer conn.Close()

	log.Println("listen :9527 success")

	for {
		time.Sleep(time.Second * 10)
	}
}
$ sudo ss -anpt
State     Recv-Q     Send-Q         Local Address:Port         Peer Address:Port    Process
LISTEN    0          8                    0.0.0.0:9527              0.0.0.0:*        users:(("s",pid=37717,fd=4))

能看到 send-q 是 8,根据公示 min(backlog, net.core.somaxconn, net.ipv4.tcp_max_syn_backlog),somaxconn 是 8,syn_backlog 是 4

我们把内核参数恢复默认看下 go server 的默认 backlog

$ sudo sysctl -a|grep tcp_syncookies;sudo sysctl -a|grep max_syn_backlog;sudo sysctl -a|grep net.core.somaxconn
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_max_syn_backlog = 512
net.core.somaxconn = 4096

$ sudo ss -anpt
State     Recv-Q     Send-Q         Local Address:Port         Peer Address:Port    Process
LISTEN    0          4096                 0.0.0.0:9527              0.0.0.0:*        users:(("s",pid=318,fd=4))

现在唯一的问题是最小应是4,通过 ss -anpt 查看显示是8,我们访问测试下,改完内核参数记得重新运行服务

$ while true;do sudo nmap -sS 192.168.139.111 -p 9527;done

$ sudo ss -anpt
State     Recv-Q     Send-Q         Local Address:Port         Peer Address:Port    Process
LISTEN    0          8                    0.0.0.0:9527              0.0.0.0:*        users:(("s",pid=344,fd=4))

$ sudo netstat -anpo | grep SYN_RECV | wc -l
4

$ sudo ss -anpt|grep 9527
LISTEN   0      8              0.0.0.0:9527         0.0.0.0:*     users:(("s",pid=344,fd=4))
SYN-RECV 0      0      192.168.139.111:9527 192.168.139.151:53165
SYN-RECV 0      0      192.168.139.111:9527 192.168.139.151:33241
SYN-RECV 0      0      192.168.139.111:9527 192.168.139.151:50404
SYN-RECV 0      0      192.168.139.111:9527 192.168.139.151:46060

能看到队列里是4,那上面的就是取值问题

netstat -s 能看到丢弃了多少 syn

$ sudo netstat -s | grep -E "LISTEN|overflowed"
    85 SYNs to LISTEN sockets dropped

Accept Queue

全连接队列最大长度
min(backlog, net.core.somaxconn)

vm-1

import socket
import time

def start_server(host, port, backlog):
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.bind((host, port))
    server.listen(backlog)

    while True:
        time.sleep(1)

if __name__ == '__main__':
    start_server('192.168.139.111', 9527, 8)

vm-2

import socket
import time

def connect_and_hold(host, port, count):
    cli_list = []
    try:
        for i in range(count):
            cli = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            cli.connect((host, port))
            cli_list.append(cli)
    except Exception as e:
        print(f"Failed to connect: {e}")

    while True:
        time.sleep(1)

if __name__ == '__main__':
    connect_and_hold('192.168.139.111', 9527, 10)

清理掉之前的 iptables 规则,分别启动测试

$ sudo netstat -s|grep -E "LISTEN|overflow"
    6 times the listen queue of a socket overflowed # 全连接丢弃的包
    91 SYNs to LISTEN sockets dropped
vm-1

$ sudo netstat -anpo|grep -E "Recv-Q|9527"
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name     Timer
tcp        9      0 192.168.139.111:9527    0.0.0.0:*               LISTEN      407/python3          off (0.00/0/0)
tcp        0      0 192.168.139.111:9527    192.168.139.151:54468   ESTABLISHED -                    off (0.00/0/0)
tcp        0      0 192.168.139.111:9527    192.168.139.151:54524   ESTABLISHED -                    off (0.00/0/0)
tcp        0      0 192.168.139.111:9527    192.168.139.151:54516   ESTABLISHED -                    off (0.00/0/0)
tcp        0      0 192.168.139.111:9527    192.168.139.151:54478   ESTABLISHED -                    off (0.00/0/0)
tcp        0      0 192.168.139.111:9527    192.168.139.151:54464   ESTABLISHED -                    off (0.00/0/0)
tcp        0      0 192.168.139.111:9527    192.168.139.151:54494   ESTABLISHED -                    off (0.00/0/0)
tcp        0      0 192.168.139.111:9527    192.168.139.151:54508   ESTABLISHED -                    off (0.00/0/0)
tcp        0      0 192.168.139.111:9527    192.168.139.151:54536   ESTABLISHED -                    off (0.00/0/0)
tcp        0      0 192.168.139.111:9527    192.168.139.151:54496   ESTABLISHED -                    off (0.00/0/0)

vm-2

$ sudo netstat -anpo|grep -E "Recv-Q|9527"
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name     Timer
tcp        0      0 192.168.139.151:54464   192.168.139.111:9527    ESTABLISHED 33260/python3        off (0.00/0/0)
tcp        0      0 192.168.139.151:54468   192.168.139.111:9527    ESTABLISHED 33260/python3        off (0.00/0/0)
tcp        0      0 192.168.139.151:54478   192.168.139.111:9527    ESTABLISHED 33260/python3        off (0.00/0/0)
tcp        0      0 192.168.139.151:54494   192.168.139.111:9527    ESTABLISHED 33260/python3        off (0.00/0/0)
tcp        0      0 192.168.139.151:54496   192.168.139.111:9527    ESTABLISHED 33260/python3        off (0.00/0/0)
tcp        0      0 192.168.139.151:54508   192.168.139.111:9527    ESTABLISHED 33260/python3        off (0.00/0/0)
tcp        0      0 192.168.139.151:54516   192.168.139.111:9527    ESTABLISHED 33260/python3        off (0.00/0/0)
tcp        0      0 192.168.139.151:54524   192.168.139.111:9527    ESTABLISHED 33260/python3        off (0.00/0/0)
tcp        0      0 192.168.139.151:54536   192.168.139.111:9527    ESTABLISHED 33260/python3        off (0.00/0/0)
tcp        0      1 192.168.139.151:54538   192.168.139.111:9527    SYN_SENT    33260/python3        on (0.81/7/0)

没错,vm-2 的第10个包 SYN_SENT 在重传

也就是全连接满了 半连接是不接收直接drop掉的

观测下全连接不满,半连接什么情况

vm-2 的连接改成6

vm-1

$ sudo netstat -anpo|grep -E "Recv-Q|9527"
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name     Timer
tcp        6      0 192.168.139.111:9527    0.0.0.0:*               LISTEN      463/python3          off (0.00/0/0)
tcp        0      0 192.168.139.111:9527    192.168.139.151:51454   ESTABLISHED -                    off (0.00/0/0)
tcp        0      0 192.168.139.111:9527    192.168.139.151:51402   ESTABLISHED -                    off (0.00/0/0)
tcp        0      0 192.168.139.111:9527    192.168.139.151:51440   ESTABLISHED -                    off (0.00/0/0)
tcp        0      0 192.168.139.111:9527    192.168.139.151:51426   ESTABLISHED -                    off (0.00/0/0)
tcp        0      0 192.168.139.111:9527    192.168.139.151:51418   ESTABLISHED -                    off (0.00/0/0)
tcp        0      0 192.168.139.111:9527    192.168.139.151:51450   ESTABLISHED -                    off (0.00/0/0)

vm-2 拦截 vm-1 过来的 syn+ack

$ sudo iptables -A INPUT -p tcp --sport 9527 -j DROP

$ nc 192.168.139.111 9527

vm-1 能看到这个 SYN_RECV 在重试,也就是进了半连接队列,因为 vm-2 拦截了 vm-1 过来的包,vm-2 不会给 vm-1 发送 ack,vm-1 就会一直重试

$ sudo netstat -anpo|grep -E "Recv-Q|9527"
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name     Timer
tcp        6      0 192.168.139.111:9527    0.0.0.0:*               LISTEN      463/python3          off (0.00/0/0)
tcp        0      0 192.168.139.111:9527    192.168.139.151:51454   ESTABLISHED -                    off (0.00/0/0)
tcp        0      0 192.168.139.111:9527    192.168.139.151:40638   SYN_RECV    -                    on (12.04/4/0)
tcp        0      0 192.168.139.111:9527    192.168.139.151:51402   ESTABLISHED -                    off (0.00/0/0)
tcp        0      0 192.168.139.111:9527    192.168.139.151:51440   ESTABLISHED -                    off (0.00/0/0)
tcp        0      0 192.168.139.111:9527    192.168.139.151:51426   ESTABLISHED -                    off (0.00/0/0)
tcp        0      0 192.168.139.111:9527    192.168.139.151:51418   ESTABLISHED -                    off (0.00/0/0)
tcp        0      0 192.168.139.111:9527    192.168.139.151:51450   ESTABLISHED -                    off (0.00/0/0)

当全连接没满,半连接是可以接收的

文中描述

net.ipv4.tcp_abort_on_overflow
此值为 0 表示握手到第三步时全连接队列满时则扔掉客户端发过来的 ACK 包。但是客户端那边因为握手包已经发出,已经自动进入 ESTABLISHED 状态准备传输数据了。服务端丢弃了 ACK 包后这个链接还是处于 SYN_RECV 状态的(如果此时客户端发数据,服务端会直接丢弃。客户端就开始重传,此时的重传次数受内核的 net.ipv4.tcp_retries2 参数控制);

此值为 1 则直接给客户端发送 RST 包直接断开连接。

这里强调下,这个参数只在半连接队列往全连接队列移动时才有效。而全连接队列已经满的情况下,内核的默认行为只是丢弃新的 SYN 包(而且目前没有参数可以控制这个行为),这会导致客户端 SYN 不断重传。

默认 net.ipv4.tcp_abort_on_overflow 是 0,要想测试很难,只在半连接向全连接移动时有效。

另外握手到第三步,就是 vm-2 向 vm-1 发 ack,既要满足发送 ack 又要叫全连接是满的,也就是发送 syn+ack 时候全连接还没满,回 ack 时 vm-1 恰巧有一个比当前请求还快的握手,让 vm-1 的全连接队列满。

我尝试在 vm-1 全连接队列满的时候,发送一个正常包到 vm-1,看看 vm-1 和 vm-2 的状态

$ sudo tcpdump -s0 -X -nn "tcp port 9527" -w vm-1-tcp_abort_on_overflow.pcap --print

vm-1

$ sudo netstat -anpo|grep -E "Recv-Q|9527"
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name     Timer
tcp        9      0 192.168.139.111:9527    0.0.0.0:*               LISTEN      538/python3          off (0.00/0/0)
tcp        0      0 192.168.139.111:9527    192.168.139.151:56822   ESTABLISHED -                    off (0.00/0/0)
tcp        0      0 192.168.139.111:9527    192.168.139.151:56802   ESTABLISHED -                    off (0.00/0/0)
tcp        0      0 192.168.139.111:9527    192.168.139.151:56786   ESTABLISHED -                    off (0.00/0/0)
tcp        0      0 192.168.139.111:9527    192.168.139.151:56840   ESTABLISHED -                    off (0.00/0/0)
tcp        0      0 192.168.139.111:9527    192.168.139.151:56838   ESTABLISHED -                    off (0.00/0/0)
tcp        0      0 192.168.139.111:9527    192.168.139.151:56796   ESTABLISHED -                    off (0.00/0/0)
tcp        0      0 192.168.139.111:9527    192.168.139.151:56790   ESTABLISHED -                    off (0.00/0/0)
tcp        0      0 192.168.139.111:9527    192.168.139.151:56780   ESTABLISHED -                    off (0.00/0/0)
tcp        0      0 192.168.139.111:9527    192.168.139.151:56818   ESTABLISHED -                    off (0.00/0/0)

-----------------------------------------------------------------
vm-2

$ sudo netstat -anpo|grep -E "Recv-Q|9527"
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name     Timer
tcp        0      1 192.168.139.151:42424   192.168.139.111:9527    SYN_SENT    33284/nc             on (1.58/6/0)
tcp        0      0 192.168.139.151:56780   192.168.139.111:9527    ESTABLISHED 33283/python3        off (0.00/0/0)
tcp        0      0 192.168.139.151:56786   192.168.139.111:9527    ESTABLISHED 33283/python3        off (0.00/0/0)
tcp        0      0 192.168.139.151:56790   192.168.139.111:9527    ESTABLISHED 33283/python3        off (0.00/0/0)
tcp        0      0 192.168.139.151:56796   192.168.139.111:9527    ESTABLISHED 33283/python3        off (0.00/0/0)
tcp        0      0 192.168.139.151:56802   192.168.139.111:9527    ESTABLISHED 33283/python3        off (0.00/0/0)
tcp        0      0 192.168.139.151:56818   192.168.139.111:9527    ESTABLISHED 33283/python3        off (0.00/0/0)
tcp        0      0 192.168.139.151:56822   192.168.139.111:9527    ESTABLISHED 33283/python3        off (0.00/0/0)
tcp        0      0 192.168.139.151:56838   192.168.139.111:9527    ESTABLISHED 33283/python3        off (0.00/0/0)
tcp        0      0 192.168.139.151:56840   192.168.139.111:9527    ESTABLISHED 33283/python3        off (0.00/0/0)
tcp        0      1 192.168.139.151:56844   192.168.139.111:9527    SYN_SENT    33283/python3        on (48.94/7/0)

能看到 vm-1 建立9个连接后这边就停止了,没有 SYN_RECV,也就是全连接满了 半连接的请求直接被 drop

而 vm-2 通过抓包能看到 56844 python 在发送 SYN_SENT

nc 的 42424 也是,全部都在重试,试了7次,正常现象 我的 net.ipv4.tcp_syn_retries = 6 net.ipv4.tcp_syn_linear_timeouts = 1

重传这里还能看到个现象:vm-1 使用 iptables 拒绝 vm-2 过来的 syn 包和全连接满了直接拒绝半连接反应的抓包是一样的,区别是一个是用户行为一个是系统行为

我将 vm-1 重启内核参数恢复默认,又启动一个nginx,能看到默认半连接 511

$ sudo ss -lnt
State     Recv-Q    Send-Q       Local Address:Port       Peer Address:Port    Process
LISTEN    0         511                0.0.0.0:80              0.0.0.0:*
LISTEN    0         511                   [::]:80                 [::]:*

这时候如果你的nginx无法处理连接,状况大致可分为几种

  1. 监听了lo网卡,导致无法处理外部请求,访问会拒绝。客户端走tcp重试
  2. 监听了正确的网卡,但有 iptables 或安全组等拦截。客户端走tcp重试
  3. 监听了正确的网卡 iptables 或安全组都放行,全连接满了。系统级别直接drop连接
  4. 监听了正确的网卡 iptables 或安全组都放行,全连接没满半连接也没满。但新机器上来就把内核参数改了,导致半连接过小,高并发情况下 系统基本指标都正常 这会让请求处理异常吗?(这一点存在疑问后面测试下)
  5. 监听了正确的网卡 iptables 或安全组都放行,全连接没满半连接也没满。但这台机器的基本指标都异常比如CPU内存使用100%,这样全连接就会一直堆积 accept 很慢,导致半连接也满了。你的机器最终也就不可用了

4 问题测试 会异常 从 server 观测到 vm-2 发送了大量的 tcp 重试,同时半连接队列从系统层又drop掉很多请求

我发现这个抓包少了并不全,但也不碍事,系统层drop掉请求是对的

$ sudo sysctl -w net.ipv4.tcp_syncookies=0 net.ipv4.tcp_max_syn_backlog=4 net.core.somaxconn=8
net.ipv4.tcp_syncookies = 0
net.ipv4.tcp_max_syn_backlog = 4
net.core.somaxconn = 8
vm-2

$ wrk -t4 -c400 -d60s http://101.200.150.26
$ netstat -anpo|grep -E "Recv|80"
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name     Timer
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN      28539/nginx: master  off (0.00/0/0)
tcp        0    862 172.22.7.89:80          x.x.x.x:55481    ESTABLISHED 28540/nginx: worker  on (0.31/0/0)
tcp        0    862 172.22.7.89:80          x.x.x.x:56121    ESTABLISHED 28541/nginx: worker  on (6.32/6/0)

$ netstat -s|grep -E "LISTEN|overflow"
    5517 times the listen queue of a socket overflowed
    78079 SYNs to LISTEN sockets dropped
$ netstat -s|grep -E "LISTEN|overflow"
    5517 times the listen queue of a socket overflowed
    78388 SYNs to LISTEN sockets dropped

还能看到在tcp连接建立以后 nginx 也做了重传,同时 Send-Q 部分为 862 byte,通过抓包分析862 恰好是 tcp 层的 tcp segment len,这个请求是 server 发往 vm-2 的响应请求,server 发给了 vm-2 还在等待 vm-2 的 ack,所以能看到 Send-Q 是 862

不设置内核参数,在压测下

tcpdump -s0 -X -nn "tcp port 80" -w cloudserver-wrk-no-sysctl.pcap --print

这个包是全的

再来看系统是否有drop请求,空的 netstat -s|grep -E "LISTEN|overflow" 过滤直接没有

TcpExt:
    2 invalid SYN cookies received
    10 resets received for embryonic SYN_RECV sockets
    42 TCP sockets finished time wait in fast timer
    273 packets rejected in established connections because of timestamp
    19 delayed acks sent
    Quick ack mode was activated 13960 times
    630 packet headers predicted
    64905 acknowledgments not containing data payload received
    16255 predicted acknowledgments
    TCPSackRecovery: 1741
    18 congestion windows recovered without slow start after partial ack
阅读全文 →

ssh 使用

server

禁用密码登录

/etc/ssh/sshd_config

UsePAM no
PasswordAuthentication no

/etc/ssh/sshd_config.d/50-cloud-init.conf

# 示例,禁用密码登录要改成 no,cloudcone 使用 cloud-init 添加文件启用了密码登录
PasswordAuthentication yes

有些服务商会不修改 sshd_config 文件,而是在 sshd_config.d 目录下添加新文件进行覆盖。

如果改了 sshd_config 文件重启发现没有生效,要检查 sshd_config.d 目录

client

~/.ssh/config

# 通过跳板机做转发
Host beszel
  Hostname localhost
  User root
  ProxyJump orange723
  LocalForward 8090 localhost:8090

Host *
  IdentityAgent agent.sock
  UserKnownHostsFile /dev/null
  StrictHostKeyChecking no
  ServerAliveInterval 15

正常连接 server 后长时间不操作,就会卡住然后只能重新连接,根据 plantegg 看到

"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

client 会每隔多少s 发送一个 packet 到 server

抓包看看

no-time

会发现连接后,长时间不操作 client 再去连接 server 已经连不上了,重新传输 server 都无响应,跟我们碰到的情况一样,只能重新连接

time

设置的 15s 可以看到连接后,在 16.923s 是 ssh 协议 client 发送一个包到 server,server 也响应了

ssh-local-forward

ssh -f -N -L 8090:localhost:8090 orange723
阅读全文 →

深入剖析 Kubernetes

01

云计算已经蜕变成 虚拟机和账单

改变一切的是 docker 镜像 解决了本地和云端环境一致的问题

05

容器技术的核心功能,就是通过约束和修改进程的动态表现,从而为其创造出一个边界

Cgroups技术是用来制造约束的主要手段,而Namespace技术则是用来修改进程视图的主要方法

Docker 容器这个听起来玄而又玄的概念,实际上是在创建容器进程时,指定了这个进程所需要启用的一组 Namespace 参数。这样,容器就只能“看”到当前 Namespace 所限定的资源、文件、设备、状态,或者配置。而对于宿主机以及其他不相关的程序,它就完全看不到了。

容器,其实是一种特殊的进程而已。

06

容器是一个 “单进程” 模型

一个正在运行的 Docker 容器,其实就是一个启用了多个 Linux Namespace 的应用进程,而这个进程能够使用的资源量,则受 Cgroups 配置的限制。

07

rootfs

08

默认情况下,Docker 会为你提供一个隐含的 ENTRYPOINT,即:/bin/sh -c。所以,在不指定 ENTRYPOINT 时,比如在我们这个例子里,实际上运行在容器里的完整进程是:/bin/sh -c "python app.py",即 CMD 的内容就是 ENTRYPOINT 的参数。

09

Kubernetes 项目的架构, 都由 Master 和 Node 两种节点组成,而这两种角色分别对应着控制节点和计算节点。

其中,控制节点,即 Master 节点,由三个紧密协作的独立组件组合而成,它们分别是负责 API 服务的 kube-apiserver、负责调度的 kube-scheduler,以及负责容器编排的 kube-controller-manager。整个集群的持久化数据,则由 kube-apiserver 处理后保存在 Etcd 中。

而计算节点上最核心的部分,则是一个叫作 kubelet 的组件。

在 Kubernetes 项目中,kubelet 主要负责同容器运行时(比如 Docker 项目)打交道。而这个交互所依赖的,是一个称作 CRI(Container Runtime Interface)的远程调用接口,这个接口定义了容器运行时的各项核心操作,比如:启动一个容器需要的所有参数。

而 kubelet 的另一个重要功能,则是调用网络插件和存储插件为容器配置网络和持久化存储。这两个插件与 kubelet 进行交互的接口,分别是 CNI(Container Networking Interface)和 CSI(Container Storage Interface)。

运行在大规模集群中的各种任务之间,实际上存在着各种各样的关系。这些关系的处理,才是作业编排和管理系统最困难的地方。

14

Pod 生命周期的变化,主要体现在 Pod API 对象的 Status 部分,这是它除了 Metadata 和 Spec 之外的第三个重要字段。其中,pod.status.phase,就是 Pod 的当前状态,它有如下几种可能的情况:

  1. Pending。这个状态意味着,Pod 的 YAML 文件已经提交给了 Kubernetes,API 对象已经被创建并保存在 Etcd 当中。但是,这个 Pod 里有些容器因为某种原因而不能被顺利创建。比如,调度不成功。
  2. Running。这个状态下,Pod 已经调度成功,跟一个具体的节点绑定。它包含的容器都已经创建成功,并且至少有一个正在运行中。
  3. Succeeded。这个状态意味着,Pod 里的所有容器都正常运行完毕,并且已经退出了。这种情况在运行一次性任务时最为常见。
  4. Failed。这个状态下,Pod 里至少有一个容器以不正常的状态(非 0 的返回码)退出。这个状态的出现,意味着你得想办法 Debug 这个容器的应用,比如查看 Pod 的 Events 和日志。
  5. Unknown。这是一个异常状态,意味着 Pod 的状态不能持续地被 kubelet 汇报给 kube-apiserver,这很有可能是主从节点(Master 和 Kubelet)间的通信出现了问题。

更进一步地,Pod 对象的 Status 字段,还可以再细分出一组 Conditions。这些细分状态的值包括:PodScheduled、Ready、Initialized,以及 Unschedulable。它们主要用于描述造成当前 Status 的具体原因是什么。

15

Kubernetes 支持的 Projected Volume 一共有四种:

  1. Secret;
  2. ConfigMap;
  3. Downward API;
  4. ServiceAccountToken。

其实,Secret、ConfigMap,以及 Downward API 这三种 Projected Volume 定义的信息,大多还可以通过环境变量的方式出现在容器里。但是,通过环境变量获取这些信息的方式,不具备自动更新的能力。所以,一般情况下,我都建议你使用 Volume 文件的方式获取这些信息。

而作为用户,你还可以通过设置 restartPolicy,改变 Pod 的恢复策略。除了 Always,它还有 OnFailure 和 Never 两种情况:

  1. Always:在任何情况下,只要容器不在运行状态,就自动重启容器;
  2. OnFailure: 只在容器 异常时才自动重启容器;
  3. Never: 从来不重启容器。

只要记住如下两个基本的设计原理即可:

  1. 只要 Pod 的 restartPolicy 指定的策略允许重启异常的容器(比如:Always),那么这个 Pod 就会保持 Running 状态,并进行容器重启。否则,Pod 就会进入 Failed 状态 。
  2. 对于包含多个容器的 Pod,只有它里面所有的容器都进入异常状态后,Pod 才会进入 Failed 状态。在此之前,Pod 都是 Running 状态。此时,Pod 的 READY 字段会显示正常容器的个数

livenessProbe 检查服务是否健康,状态是否为running
readnessProbe 是否能被Service使用

PodPreset 可以写通用的字段,给pod用,只会更改pod,而不会修改deployment

17

18

StatefulSet 的设计其实非常容易理解。它把真实世界里的应用状态,抽象为了两种情况:

  1. 拓扑状态。这种情况意味着,应用的多个实例之间不是完全对等的关系。这些应用实例,必须按照某些顺序启动,比如应用的主节点 A 要先于从节点 B 启动。而如果你把 A 和 B 两个 Pod 删除掉,它们再次被创建出来时也必须严格按照这个顺序才行。并且,新创建出来的 Pod,必须和原来 Pod 的网络标识一样,这样原先的访问者才能使用同样的方法,访问到这个新 Pod。
  2. 存储状态。这种情况意味着,应用的多个实例分别绑定了不同的存储数据。对于这些应用实例来说,Pod A 第一次读取到的数据,和隔了十分钟之后再次读取到的数据,应该是同一份,哪怕在此期间 Pod A 被重新创建过。这种情况最典型的例子,就是一个数据库应用的多个存储实例。

StatefulSet 这个控制器的主要作用之一,就是使用 Pod 模板创建 Pod 的时候,对它们进行编号,并且按照编号顺序逐一完成创建工作。而当 StatefulSet 的“控制循环”发现 Pod 的“实际状态”与“期望状态”不一致,需要新建或者删除 Pod 进行“调谐”的时候,它会严格按照这些 Pod 编号的顺序,逐一完成这些操作。

19

首先,StatefulSet 的控制器直接管理的是 Pod。这是因为,StatefulSet 里的不同 Pod 实例,不再像 ReplicaSet 中那样都是完全一样的,而是有了细微区别的。比如,每个 Pod 的 hostname、名字等都是不同的、携带了编号的。而 StatefulSet 区分这些实例的方式,就是通过在 Pod 的名字里加上事先约定好的编号。

其次,Kubernetes 通过 Headless Service,为这些有编号的 Pod,在 DNS 服务器中生成带有同样编号的 DNS 记录。只要 StatefulSet 能够保证这些 Pod 名字里的编号不变,那么 Service 里类似于 web-0.nginx.default.svc.cluster.local 这样的 DNS 记录也就不会变,而这条记录解析出来的 Pod 的 IP 地址,则会随着后端 Pod 的删除和再创建而自动更新。这当然是 Service 机制本身的能力,不需要 StatefulSet 操心。

最后,StatefulSet 还为每一个 Pod 分配并创建一个同样编号的 PVC。这样,Kubernetes 就可以通过 Persistent Volume 机制为这个 PVC 绑定上对应的 PV,从而保证了每一个 Pod 都拥有一个独立的 Volume。

26

而在 Kubernetes 项目中,负责完成授权(Authorization)工作的机制,就是 RBAC:基于角色的访问控制(Role-Based Access Control)。

如果你直接查看 Kubernetes 项目中关于 RBAC 的文档的话,可能会感觉非常复杂。但实际上,等到你用到这些 RBAC 的细节时,再去查阅也不迟。

而在这里,我只希望你能明确三个最基本的概念。

  1. Role:角色,它其实是一组规则,定义了一组对 Kubernetes API 对象的操作权限。
  2. Subject:被作用者,既可以是“人”,也可以是“机器”,也可以是你在 Kubernetes 里定义的“用户”。
  3. RoleBinding:定义了“被作用者”和“角色”的绑定关系。

33

Flannel 项目是 CoreOS 公司主推的容器网络方案。事实上,Flannel 项目本身只是一个框架,真正为我们提供容器网络功能的,是 Flannel 的后端实现。目前,Flannel 支持三种后端实现,分别是:

  1. VXLAN;
  2. host-gw;
  3. UDP。

35

所谓 BGP,就是在大规模网络中实现节点路由信息共享的一种协议。

Calico 项目的架构

  1. Calico 的 CNI 插件。这是 Calico 与 Kubernetes 对接的部分。
  2. Felix。它是一个 DaemonSet,负责在宿主机上插入路由规则(即:写入 Linux 内核的 FIB 转发信息库),以及维护 Calico 所需的网络设备等工作。
  3. BIRD。它就是 BGP 的客户端,专门负责在集群里分发路由规则信息。
阅读全文 →

容器实战高手课

01

在容器中,1 号进程永远不会响应 SIGKILL 和 SIGSTOP 这两个特权信号;

对于其他的信号,如果用户自己注册了 handler,1 号进程可以响应。

SIGTERM 可以程序自己注册,所以 1 号进程是可以响应的

02

进程 “活着” 的时候只有两个状态:运行态(TASK_RUNNING)和睡眠态(TASK_INTERRUPTIBLE,TASK_UNINTERRUPTIBLE)

如果你想让容器里的进程避免过多僵尸进程,1需要限制容器的pids.max 是容器需要做的,2是程序的父进程要处理子进程进行回收

进程退出会调用do_exit() 它有两个状态 EXIT_DEAD EXIT_ZOMBIE,僵尸进程会处于EXIT_ZOMBIE

03

如何实现 graceful shutdown

容器的第一个进程 也就是 init 进程,如果你做docker stop时 init进程收到的是 SIGTERM 你的进程注册了 SIGTERM 他就会自动退出,但是在容器里你的子进程收到的是 SIGKILL 这个signal是不允许被注册的,所以当你的init收到SIGTERM要转发给子进程让他们去退出

08

通过查看 syslog 看是否有 oom-kill

grep -C 10 oom-kill syslog

系统总的可用页面数乘以进程的 OOM 校准值 oom_score_adj,再加上进程已经使用的物理页面数,计算出来的值越大,那么这个进程被 OOM Kill 的几率也就越大。

要么调大内存,要么修bug

09

在linux里 内存 rss 是程序真正使用的内存,page cache是文件缓存,当内存达到max时 也可以继续申请,会释放page的内存

cgroup v2 memory.stat anon 是rss ,file是page cache

11

overlayfs 就是 unionfs 的一种实现,overlayfs主要是给容器镜像使用,分为 lowerlay 和 upperlay 还有workerlay,lowerlay仅仅可读,一般写操作都放在 upperlay

12

overlayfs 没有专门限制磁盘使用量的实现,如果你的磁盘是xfs是可以通过xfs的quota来限制,docker 自己也有磁盘使用限制

15

容器中有些网络参数是默认继承宿主机,而有些是会写入默认值,所以当你需要变更网络参数时 要在你的程序没启动时进行修改,如果你的应用已经启动,变更后已经连接的tcp要重新连接才会生效,容器提供了 sysctl 来在容器里应用启动之前修改,这样就会是你想要的网络参数

16

容器和宿主机网络通信是走docker0这个网卡,宿主机和容器是通过一对网卡pair来连接,容器pair->宿主机pai—>docker0->eth0 转发

19

其实 Linux capabilities 就是把 Linux root 用户原来所有的特权做了细化,可以更加细粒度地给进程赋予不同权限

getcap $(which ping)
setcap -r $(which ping)

阅读全文 →