plantegg

java tcp mysql performance network docker Linux

就是要你懂TCP–通过案例来学习MSS、MTU

问题的描述

  • 最近要通过Docker的方式把产品部署到客户机房, 过程中需要部署一个hbase集群,hbase总是部署失败(在我们自己的环境没有问题)
  • 发现hbase卡在同步文件,人工登上hbase 所在的容器中看到在hbase节点之间scp同步一些文件的时候,同样总是失败(稳定重现)
  • 手工尝试scp那些文件,发现总是在传送某个文件的时候scp卡死了
  • 尝试单独scp这个文件依然卡死
  • 在这个容器上scp其它文件没问题(小文件)
  • 换一个容器scp这个文件没问题

分析过程

实在很难理解为什么单单这个文件在这个容器上scp就卡死了,既然scp网络传输卡死,那么就同时在两个容器上tcpdump抓包,想看看为什么传不动了

在客户端抓包如下:(33端口是服务端的sshd端口,10.16.11.108是客户端ip)

image-20240506090810608

从抓包中可以得到这样一些结论:

  • 从抓包中可以明显知道scp之所以卡死是因为丢包了,客户端一直在重传,图中绿框
  • 图中篮框显示时间间隔,时间都是花在在丢包重传等待的过程
  • 奇怪的问题是图中橙色框中看到的,网络这时候是联通的,客户端跟服务端在这个会话中依然有些包能顺利到达(Keep-Alive包)
  • 同时注意到重传的包长是1442,包比较大了,看了一下tcp建立连接的时候MSS是1500,应该没有问题
  • 查看了scp的两个容器的网卡mtu都是1500,正常
1
基本上看到这里,能想到是因为丢包导致的scp卡死,因为两个容器mtu都正常,包也小于mss,那只能是网络路由上某个环节mtu太小导致这个1442的包太大过不去,所以一直重传,看到的现状就是scp卡死了

接下来分析网络传输链路

scp传输的时候实际路由大概是这样的

1
容器A---> 宿主机1 ---> ……中间的路由设备 …… ---> 宿主机2 ---> 容器B  
  • 前面提过其它容器scp同一个文件到容器B没问题,所以我认为中间的路由设备没问题,问题出在两台宿主机上
  • 在宿主机1上抓包发现抓不到丢失的那个长度为 1442 的包,也就是问题出在了 容器A—> 宿主机1 上

查看宿主机1的dmesg看到了这样一些信息

2016-08-08T08:15:27.125951+00:00 server kernel: openvswitch: ens2f0.627: dropped over-mtu packet: 1428 > 1400
2016-08-08T08:15:27.536517+00:00 server kernel: openvswitch: ens2f0.627: dropped over-mtu packet: 1428 > 1400

验证方法

-D Set the Don’t Fragment bit.
-s packetsize
Specify the number of data bytes to be sent. The default is 56, which translates into 64
ICMP data bytes when combined with the 8 bytes of ICMP header data. This option cannot be
used with ping sweeps.

ping 测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
✘ ren@mac  ~/Downloads  ping -c 1 -D -s 1500 www.baidu.com
PING www.a.shifen.com (110.242.68.4): 1500 data bytes
ping: sendto: Message too long
^C
--- www.a.shifen.com ping statistics ---
1 packets transmitted, 0 packets received, 100.0% packet loss
✘ ren@mac  ~/Downloads  ping -c 1 -D -s 1400 www.baidu.com
PING www.a.shifen.com (110.242.68.4): 1400 data bytes
1408 bytes from 110.242.68.4: icmp_seq=0 ttl=49 time=21.180 ms

--- www.a.shifen.com ping statistics ---
1 packets transmitted, 1 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 21.180/21.180/21.180/0.000 ms
ren@mac  ~/Downloads 

一些结论

到这里问题已经很明确了 openvswitch 收到了 一个1428大小的包因为比mtu1400要大,所以扔掉了,接着查看宿主机1的网卡mtu设置果然是1400,悲催,马上修改mtu到1500,问题解决。

正常分片是ip层来操作,路由器工作在3层,有分片能力,从容器到宿主机走的是bridge,没有进行分片,或者是因为收到这个IP包的时候里面带了 Don’t Fragment标志,路由器就不进行分片了,那为什么IP包要带这个标志呢?当然是为了有更好的性能,都经过TCP握手协商出了一个MSS,就不要再进行分片了。

当然这里TCP协商MSS的时候应该经过 PMTUD( This process is called “Path MTU discovery”.) 来确认整个路由上的所有最小MTU,但是有些路由器会因为安全的原因过滤掉ICMP,导致PMTUD不可靠,所以这里的PMTUD形同虚设,比如在我们的三次握手中会协商一个MSS,这只是基于Client和Server两方的MTU来确定的,链路上如果还有比Client和Server的MTU更小的那么就会出现包超过MTU的大小,同时设置了DF标志而不再进行分片被丢掉。

centos或者ubuntu下:

$cat /proc/sys/net/ipv4/tcp_mtu_probing //1 表示开启路径mtu检测
0

$sudo sysctl -a |grep -i pmtu
net.ipv4.ip_forward_use_pmtu = 0
net.ipv4.ip_no_pmtu_disc = 0 //默认似乎是没有启用PMTUD
net.ipv4.route.min_pmtu = 552

IPv4规定路由器至少要能处理576bytes的包,Ethernet规定的是1500 bytes,所以一般都是假设链路上MTU不小于1500

TCP中的MSS总是在SYN包中设置成下一站的MTU减去HeaderSize(40)。

image.png

一般终端只有收到PATH MTU 调整报文才会去调整mss报文大小,PATH MTU是封装在ICMP报文里面。所以重新在ECS上抓包,抓取数据交互报文和ICMP报文。

image-20221125133218008

上图可以看到当服务端(3717端口)发送1460 payload的报文的时候,中间链路上的ECS会返回一个ICMP报文,此ICMP报文作用是告诉服务端,ECS的链路MTU只有1476,当服务端收到这个ICMP报文的时候,服务端就会知道中间链路只能允许payload 1436的报文通过,自然就会缩小发送的mss大小。

这个ICMP包在链路上有可能会被丢掉,比如:

Intel网卡驱动老版本RSS使用的是vxlan外层报文, 在新版本切到了内层RSS; 用的外层RSS, 对于GRE代理访问模式没有问题; 新版本用的内层RSS, 看到的源地址是192.168.0.64, 但实际发icmp包的是gre那台ecs-ip, 所以icmp跟session按内层rss策略落不到一个核去了,所以后端服务器无法收到ICMP报文,从而无法自动调整报文MSS大小。
简单说, 就是gre代理回icmp的这种场景, 在内层rss版本上不支持了。

总结

  • 因为这是客户给的同一批宿主机默认想当然的认为他们的配置到一样,尤其是mtu这种值,只要不是故意捣乱就不应该乱修改才对,我只检查了两个容器的mtu,没看宿主机的mtu,导致诊断中走了一些弯路
  • 通过这个案例对mtu/mss等有了进一步的了解
  • 从这个案例也理解了vlan模式下容器、宿主机、交换机之间的网络传输链路
  • 其实抓包还发现了比1500大得多的包顺利通过,反而更小的包无法通过,这是因为网卡基本都有拆包的功能了
  • 设置由系统主动允许分片的参数 sysctl -w net.ipv4.ip_no_pmtu_disc=1 可以解决这种问题

常见问题

Q: 传输的包超过MTU后表现出来的症状?
A:卡死,比如scp的时候不动了,或者其他更复杂操作的时候不动了,卡死的状态。

Q: mtu 如何配置
ifconfig eth1 mtu 9000 up
vi /etc/sysconfig/network-scripts/ifcfg-eth0

Q: 为什么我的MTU是1500,但是抓包看到有个包2700,没有卡死?
A: 有些网卡有拆包的能力,具体可以Google:LSO、TSO,这样可以减轻CPU拆包的压力,节省CPU资源。

Q: 到哪里可以设置MSS

A: 网卡配置–ifconfig;ip route在路由上指定;iptables中限制

# Add rules
$ sudo iptables -I OUTPUT -p tcp -m tcp –tcp-flags SYN,RST SYN -j TCPMSS –set-mss 48
# delete rules
$ sudo iptables -D OUTPUT -p tcp -m tcp –tcp-flags SYN,RST SYN -j TCPMSS –set-mss 48

# show router information
$ route -ne
$ ip route show
192.168.11.0/24 dev ens33 proto kernel scope link src 192.168.11.111 metric 100
# modify route table
$ sudo ip route change 192.168.11.0/24 dev ens33 proto kernel scope link src 192.168.11.111 metric 100 advmss 48

如何定位上亿次调用才出现一次的Bug

引文

对于那种出现概率非常低,很难重现的bug有时候总是感觉有力使不上,比如这个问题

正好最近也碰到一个极低概率下的异常,我介入前一大帮人花了几个月,OS、ECS、网络等等各个环节都被怀疑一遍但是又都没有实锤,所以把过程记录下。

问题背景

客户会调用我们的一个服务,正常都是client request -> server response 如此反复直到client主动完成,然后断开tcp连接。但是就是在这个过程中,有极低的概率client 端抛出连接非正常断开的异常堆栈,由于这个业务比较特殊,客户无法接受这种异常,所以要求一定要解决这个问题。

重现麻烦,只能在客户环境,让客户把他们的测试跑起来才能一天重现1-2次,每次跟客户沟通成本很高。出现问题的精确时间点不好确定

tcpdump 抓包所看到的问题表现

在client 和 server上一直进行tcpdump 抓包,然后压力测试不停地跑,一旦client抛了连接异常,根据时间点、端口信息在两边的抓包中分析当时的tcp会话

比如,通过tcpdump分析到的会话是这样的:
screenshot.png

如上图所示,正常都是client发送request,server返回response,但是出问题的时候(截图红框)server收到了client的request,也回复了ack给client说收到请求了,但是很快server又回复了一个fin包(server主动发起四次挥手断开连接),这是不正常的。

到这里可以有一个明确的结论:出问题都是因为server主动发起连接断开的fin包,即使刚收到client的request请求还没有返回response

开发增加debug日志

在server端的应用中可能会调用 socket.close 的地方都增加了日志,但是实际发生异常的时候没有任何日志输出,所以到此开发认为应用代码没有问题(毕竟没有证据–实际不能排除)

怀疑ECS网络抖动(是个好背锅侠,什么锅都可以背)

申请单独的物理机资源给客户,保证没有其它应用来争抢网络和其它资源,前三天一次异常也没有发生(在ECS上一天发生1-2次),非常高兴以为找到问题了。结果第四天异常再次出现,更换物理机也只是好像偶然性地降低了发生频率而已。

去底层挖掘tcp协议,到底什么条件下会出现主动断开连接

实际也没有什么进展

用strace、pstack去监控 socket.close 这个事件

但实际可能在上亿次正常的 socket.close (查询全部结束,client主动请求断开连接)才会出现一次不正常的 socket.close .量太大,还没发在这么多事件中区分那个是不正常的close

应用被 OOM kill

调查过程中为了更快地重现异常,将客户端连接都改成长连接,这样应用不再去调 socket.close ,除非超时、异常之类的,这样一旦出现不正常的 socket.close 就更容易定位了。

实际跑了一段时间后,发现确实 tcpdump 能抓到很多 server在接收到request还没有返回response的时候主动发送 fin包来断开连接的情况,跟前面的症状是一模一样的。但是最终发现这个时候应用被杀掉了,只是说明应用被杀的情况下 server会主动去掉 socket.close关闭连接,但这只是充分条件,而不是必要条件。实际生产线上也没有被 OOM kill过。

给力的开发同学

分析了这个异常后,开发简化了整个测试,实现client上跑一行PHP代码反复调用就能够让这个bug触发,这一下把整个测试重现bug的过程简化了,终于不再需要客户配合了,让问题的定位效率快了一个数量级。

为了快速地定位到异常的具体连接,实现脚本来自动分析tcpdump结果找到异常close的连接

快速在tcpdump包中找到出问题的那个stream(这个命令行要求tshark的版本为1.12及以上,默认的阿里服务器上的版本都太低,解析不了_ws.col.Info列):

tshark -r capture.pcap135 -T fields -e frame.number -e frame.time_epoch -e ip.addr -e tcp.port  -e tcp.stream   -e _ws.col.Info | egrep "FIN|Request Quit" | awk '{ print $5, $6 $7 }' | sort -k1n | awk '{ print $1 }' | uniq -c | grep -v "^      3" | less

在这一系列的工具作用下,稳定跑上一天,异常能发生3、4次,产生的日志和网络包有几百G。

出现问题的后,通过上面的脚本分析连接异常断开的client ip+port和时间,同时拿这三个信息到下面的异常堆栈中搜索匹配找到调用 socket.close()的堆栈。

上Btrace 监听所有 socket.close 事件

	@OnMethod(clazz="+java.net.Socket", method="close")
	public static void onSocketClose(@Self Object me) {
      println("\n==== java.net.Socket#close ====");
      BTraceUtils.println(BTraceUtils.timestamp() );
      BTraceUtils.println(BTraceUtils.Time.millis() );
      println(concat("Socket closing:", str(me)));
      println(concat("thread: ", str(currentThread())));
      printFields(me);
      jstack();
}

终于在出现异常的时候btrace抓到了异常的堆栈,在之前代码review看来不可能的逻辑里server主动关闭了连接

screenshot.png

图左是应用代码,图右是关闭连接的堆栈,有了这个堆栈就可以去修复问题了

实际上这里可能有几个问题:

  1. buffer.position 是不可能为0的;
  2. 即使buffer.position 等于0 也不应该直接 socket.close, 可能发送error信息给客户端更好;

总结

  • 最终原因是因为NIO过程中buffer有极低的概率被两个socket重用,从而导致出现正在使用的buffer被另外一个socket拿过去并且设置了buffer.position为0,进而导致前一个socket认为数据异常赶紧close了。
  • 开发简化问题的重现步骤非常关键,同时对异常进行分类分析,加快了定位效率
  • 能够通过tcpdump去抓包定位到具体问题大概所在点这是比较关键的一步,同时通过btrace再去监控出问题的调用堆栈从而找到具体代码行。
  • 过程看似简单,实际牵扯了一大波工程师进来,经过几个月才最终定位到出问题的代码行,确实不容易

iptables监控reset的连接信息

需求

如果连接被reset需要记录下reset包是哪边放出来的,并记录reset连接的四元组信息

iptables规则

1
2
3
4
5
6
7
8
9
10
11
12
# Generated by iptables-save v1.4.21 on Wed Apr  1 11:39:31 2020
*filter
:INPUT ACCEPT [557:88127]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [527:171711]
# 不监听3406上的reset,日志前面添加 [drds]
-A INPUT -p tcp -m tcp ! --sport 3406 --tcp-flags RST RST -j LOG --log-prefix "[drds] " --log-level 7 --log-tcp-sequence --log-tcp-options --log-ip-options
# -A INPUT -p tcp -m tcp ! --dport 3406 --tcp-flags RST RST -j LOG --log-prefix "[drds] " --log-level7 --log-tcp-sequence --log-tcp-options --log-ip-options
-A OUTPUT -p tcp -m tcp ! --sport 3406 --tcp-flags RST RST -j LOG --log-prefix "[drds] " --log-level 7 --log-tcp-sequence --log-tcp-options --log-ip-options
COMMIT
# Completed on Wed Apr 1 11:39:31 2020

将如上配置保存在 drds_filter.conf中,设置开机启动:

1
2
3
//注意,tee 命令的 "-a" 选项的作用等同于 ">>" 命令,如果去除该选项,那么 tee 命令的作用就等同于 ">" 命令。
//echo -1 | sudo tee /proc/sys/kernel/perf_event_paranoid //sudo强行修改写入
echo "sudo iptables-restore < drds_filter.conf" | sudo tee -a /etc/rc.d/rc.local

单独记录到日志文件中

默认情况下 iptables 日志记录在 dmesg中不方便查询,可以修改rsyslog.d规则将日志存到单独的文件中:

1
2
# cat /etc/rsyslog.d/drds_filter_log.conf
:msg, startswith, "[drds]" -/home/admin/logs/drds-tcp.log

将 [drds] 开头的日志存到对应的文件

将如上配置放到: /etc/rsyslog.d/ 目录下, 重启 rsyslog 就生效了

1
2
3
sudo cp /home/admin/drds-worker/install/drds_filter_log.conf /etc/rsyslog.d/drds_filter_log.conf
sudo chown -R root:root /etc/rsyslog.d/drds_filter_log.conf
sudo systemctl restart rsyslog

防止日志打满磁盘

配置 logrotate, 保留最近30天的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#cat /etc/logrotate.d/drds
/home/admin/logs/drds-tcp.log
{
daily
rotate 30
copytruncate
compress
dateext
#size 1k
prerotate
/usr/bin/chattr -a /home/admin/logs/drds-tcp.log
endscript
postrotate
/usr/bin/chattr +a /home/admin/logs/drds-tcp.log
endscript
}

执行:
sudo /usr/sbin/logrotate --force --verbose /etc/logrotate.d/drds
debug:
sudo /usr/sbin/logrotate -d --verbose /etc/logrotate.d/drds
查看日志:
cat /var/lib/logrotate/logrotate.status

logrotate操作的日志需要权限正常,并且上级目录权限也要对,解决方案参考:https://chasemp.github.io/2013/07/24/su-directive-logrotate/ 报错信息:

1
2
3
4
5
6
7
rotating pattern: /var/log/myapp/*.log  weekly (4 rotations)
empty log files are rotated, old logs are removed
considering log /var/log/myapp/default.log

error: skipping "/var/log/myapp/default.log" because parent directory has insecure permissions
(It's world writable or writable by group which is not "root") Set "su" directive in
config file to tell logrotate which user/group should be used for rotation

最终效果

1
2
3
4
5
$tail -10 logs/drds-tcp.log
Apr 26 15:27:36 vb kernel: [drds] IN= OUT=eth0 SRC=10.0.186.75 DST=10.0.175.109 LEN=40 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=8182 DPT=9366 SEQ=0 ACK=1747027778 WINDOW=0 RES=0x00 ACK RST URGP=0
Apr 26 15:27:36 vb kernel: [drds] IN= OUT=eth0 SRC=10.0.186.75 DST=10.0.175.109 LEN=40 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=8182 DPT=9368 SEQ=0 ACK=3840170438 WINDOW=0 RES=0x00 ACK RST URGP=0
Apr 26 15:27:36 vb kernel: [drds] IN= OUT=eth0 SRC=10.0.186.75 DST=10.0.175.109 LEN=40 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=8182 DPT=9370 SEQ=0 ACK=3892381139 WINDOW=0 RES=0x00 ACK RST URGP=0
Apr 26 15:27:38 vb kernel: [drds] IN= OUT=eth0 SRC=10.0.186.75 DST=10.0.171.173 LEN=40 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=8182 DPT=38225 SEQ=0 ACK=1436910913 WINDOW=0 RES=0x00 ACK RST URGP=0

NetFilter Hooks

下面几个 hook 是内核协议栈中已经定义好的:

  • NF_IP_PRE_ROUTING: 接收到的包进入协议栈后立即触发此 hook,在进行任何路由判断 (将包发往哪里)之前
  • NF_IP_LOCAL_IN: 接收到的包经过路由判断,如果目的是本机,将触发此 hook
  • NF_IP_FORWARD: 接收到的包经过路由判断,如果目的是其他机器,将触发此 hook
  • NF_IP_LOCAL_OUT: 本机产生的准备发送的包,在进入协议栈后立即触发此 hook
  • NF_IP_POST_ROUTING: 本机产生的准备发送的包或者转发的包,在经过路由判断之后, 将触发此 hook

IPTables 表和链(Tables and Chains)

下面可以看出,内置的 chain 名字和 netfilter hook 名字是一一对应的:

  • PREROUTING: 由 NF_IP_PRE_ROUTING hook 触发
  • INPUT: 由 NF_IP_LOCAL_IN hook 触发
  • FORWARD: 由 NF_IP_FORWARD hook 触发
  • OUTPUT: 由 NF_IP_LOCAL_OUT hook 触发
  • POSTROUTING: 由 NF_IP_POST_ROUTING hook 触发

tracing_point 监控

对于 4.19内核的kernel,可以通过tracing point来监控重传以及reset包

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
# grep tcp:tcp /sys/kernel/debug/tracing/available_events
tcp:tcp_probe
tcp:tcp_retransmit_synack
tcp:tcp_rcv_space_adjust
tcp:tcp_destroy_sock
tcp:tcp_receive_reset
tcp:tcp_send_reset
tcp:tcp_retransmit_skb

#开启本机发出的 reset 监控,默认输出到:/sys/kernel/debug/tracing/trace_pipe
# echo 1 > /sys/kernel/debug/tracing/events/tcp/tcp_send_reset/enable

#如下是开启重传以及reset的记录,本机ip 10.0.186.140
# cat trace_pipe
//重传
<idle>-0 [002] ..s. 9520196.657431: tcp_retransmit_skb: sport=3306 dport=62460 saddr=10.0.186.140 daddr=10.0.186.70 saddrv6=::ffff:10.0.186.140 daddrv6=::ffff:10.0.186.70
Wisp-Root-Worke-540 [000] .... 9522308.074233: tcp_destroy_sock: sport=3306 dport=20594 saddr=10.0.186.140 daddr=10.0.186.70 saddrv6=::ffff:10.0.186.140 daddrv6=::ffff:10.0.186.70 sock_cookie=51a
C2 CompilerThre-543 [002] ..s. 9522308.074296: tcp_destroy_sock: sport=3306 dport=20670 saddr=10.0.186.140 daddr=10.0.186.70 saddrv6=::ffff:10.0.186.140 daddrv6=::ffff:10.0.186.70 sock_cookie=574

// 被reset
Wisp-Root-Worke-540 [002] ..s. 9522519.353756: tcp_receive_reset: sport=33822 dport=8080 saddr=10.0.186.140 daddr=10.0.171.193 saddrv6=::ffff:10.0.186.140 daddrv6=::ffff:10.0.171.193 sock_cookie=5dd
// 主动reset
DragoonAgent-28297 [002] .... 9522433.144611: tcp_send_reset: sport=8182 dport=61783 saddr=10.0.186.140 daddr=10.0.171.174 saddrv6=::ffff:10.0.186.140 daddrv6=::ffff:10.0.171.174
Wisp-Root-Worke-540 [002] ..s. 9522519.353773: tcp_send_reset: sport=33822 dport=8080 saddr=10.0.186.140 daddr=10.0.171.193 saddrv6=::ffff:10.0.186.140 daddrv6=::ffff:10.0.171.193

// 3306对端中断
cat-28727 [000] ..s. 9524341.650740: inet_sock_set_state: family=AF_INET protocol=IPPROTO_TCP sport=3306 dport=23262 saddr=10.0.186.140 daddr=10.0.186.70 saddrv6=::ffff:10.0.186.140 daddrv6=::ffff:10.0.186.70 oldstate=TCP_SYN_RECV newstate=TCP_ESTABLISHED
<idle>-0 [000] .ns. 9524397.184608: inet_sock_set_state: family=AF_INET protocol=IPPROTO_TCP sport=3306 dport=23262 saddr=10.0.186.140 daddr=10.0.186.70 saddrv6=::ffff:10.0.186.140 daddrv6=::ffff:10.0.186.70 oldstate=TCP_ESTABLISHED newstate=TCP_CLOSE

//8182 主动关闭
<idle>-0 [000] .Ns. 9525499.045236: inet_sock_set_state: family=AF_INET protocol=IPPROTO_TCP sport=8182 dport=25448 saddr=10.0.186.140 daddr=10.0.171.174 saddrv6=::ffff:10.0.186.140 daddrv6=::ffff:10.0.171.174 oldstate=TCP_SYN_RECV newstate=TCP_ESTABLISHED
DragoonAgent-6240 [001] .... 9525499.118092: inet_sock_set_state: family=AF_INET protocol=IPPROTO_TCP sport=8182 dport=25448 saddr=10.0.186.140 daddr=10.0.171.174 saddrv6=::ffff:10.0.186.140 daddrv6=::ffff:10.0.171.174 oldstate=TCP_ESTABLISHED newstate=TCP_FIN_WAIT1
<idle>-0 [000] ..s. 9525499.159032: inet_sock_set_state: family=AF_INET protocol=IPPROTO_TCP sport=8182 dport=25448 saddr=10.0.186.140 daddr=10.0.171.174 saddrv6=::ffff:10.0.186.140 daddrv6=::ffff:10.0.171.174 oldstate=TCP_FIN_WAIT1 newstate=TCP_FIN_WAIT2
<idle>-0 [000] .Ns. 9525499.159056: inet_sock_set_state: family=AF_INET protocol=IPPROTO_TCP sport=8182 dport=25448 saddr=10.0.186.140 daddr=10.0.171.174 saddrv6=::ffff:10.0.186.140 daddrv6=::ffff:10.0.171.174 oldstate=TCP_FIN_WAIT2 newstate=TCP_CLOSE

//3306 被动关闭
<idle>-0 [002] .Ns. 9524484.864509: inet_sock_set_state: family=AF_INET protocol=IPPROTO_TCP sport=3306 dport=23360 saddr=10.0.186.140 daddr=10.0.186.70 saddrv6=::ffff:10.0.186.140 daddrv6=::ffff:10.0.186.70 oldstate=TCP_SYN_RECV newstate=TCP_ESTABLISHED
cat-28568 [002] ..s. 9524496.913199: inet_sock_set_state: family=AF_INET protocol=IPPROTO_TCP sport=3306 dport=23360 saddr=10.0.186.140 daddr=10.0.186.70 saddrv6=::ffff:10.0.186.140 daddrv6=::ffff:10.0.186.70 oldstate=TCP_ESTABLISHED newstate=TCP_CLOSE_WAIT
Wisp-Root-Worke-540 [003] .... 9524496.915450: inet_sock_set_state: family=AF_INET protocol=IPPROTO_TCP sport=3306 dport=23360 saddr=10.0.186.140 daddr=10.0.186.70 saddrv6=::ffff:10.0.186.140 daddrv6=::ffff:10.0.186.70 oldstate=TCP_CLOSE_WAIT newstate=TCP_LAST_ACK
Wisp-Root-Worke-539 [002] .Ns. 9524496.915572: inet_sock_set_state: family=AF_INET protocol=IPPROTO_TCP sport=3306 dport=23360 saddr=10.0.186.140 daddr=10.0.186.70 saddrv6=::ffff:10.0.186.140 daddrv6=::ffff:10.0.186.70 oldstate=TCP_LAST_ACK newstate=TCP_CLOSE

iptables 打通网络

1
2
//本机到 172.16.0.102 不通,但是和 47.100.29.16能通(阿里云弹性ip)
iptables -t nat -A OUTPUT -d 172.16.0.102 -j DNAT --to-destination 47.100.29.16

参考资料

深入理解 iptables 和 netfilter 架构

NAT - 网络地址转换(2016)

iptables使用

结构

image-20220608093532338

包流

img

iptables监控reset的连接信息

如果连接被reset需要记录下reset包是哪边发出来的,并记录reset连接的四元组信息

iptables规则

1
2
3
4
5
6
7
8
9
10
11
12
# Generated by iptables-save v1.4.21 on Wed Apr  1 11:39:31 2020
*filter
:INPUT ACCEPT [557:88127]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [527:171711]
# 不监听3406上的reset,日志前面添加 [plantegg]
-A INPUT -p tcp -m tcp ! --sport 3406 --tcp-flags RST RST -j LOG --log-prefix "[plantegg] " --log-level 7 --log-tcp-sequence --log-tcp-options --log-ip-options
# -A INPUT -p tcp -m tcp ! --dport 3406 --tcp-flags RST RST -j LOG --log-prefix "[plantegg] " --log-level7 --log-tcp-sequence --log-tcp-options --log-ip-options
-A OUTPUT -p tcp -m tcp ! --sport 3406 --tcp-flags RST RST -j LOG --log-prefix "[plantegg] " --log-level 7 --log-tcp-sequence --log-tcp-options --log-ip-options
COMMIT
# Completed on Wed Apr 1 11:39:31 2020

将如上配置保存在 plantegg_filter.conf中,设置开机启动:

1
2
3
//注意,tee 命令的 "-a" 选项的作用等同于 ">>" 命令,如果去除该选项,那么 tee 命令的作用就等同于 ">" 命令。
//echo -1 | sudo tee /proc/sys/kernel/perf_event_paranoid //sudo强行修改写入
echo "sudo iptables-restore < plantegg_filter.conf" | sudo tee -a /etc/rc.d/rc.local

单独记录到日志文件中

默认情况下 iptables 日志记录在 dmesg中不方便查询,可以修改rsyslog.d规则将日志存到单独的文件中:

1
2
# cat /etc/rsyslog.d/plantegg_filter_log.conf
:msg, startswith, "[plantegg]" -/home/admin/logs/plantegg-tcp.log

将 [plantegg] 开头的日志存到对应的文件

将如上配置放到: /etc/rsyslog.d/ 目录下, 重启 rsyslog 就生效了

1
2
3
sudo cp /home/admin/plantegg-worker/install/plantegg_filter_log.conf /etc/rsyslog.d/plantegg_filter_log.conf
sudo chown -R root:root /etc/rsyslog.d/plantegg_filter_log.conf
sudo systemctl restart rsyslog

防止日志打满磁盘

配置 logrotate, 保留最近30天的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#cat /etc/logrotate.d/drds
/home/admin/logs/drds-tcp.log
{
daily
rotate 30
copytruncate
compress
dateext
#size 1k
prerotate
/usr/bin/chattr -a /home/admin/logs/drds-tcp.log
endscript
postrotate
/usr/bin/chattr +a /home/admin/logs/drds-tcp.log
endscript
}

执行:
sudo /usr/sbin/logrotate --force --verbose /etc/logrotate.d/drds
debug:
sudo /usr/sbin/logrotate -d --verbose /etc/logrotate.d/drds
查看日志:
cat /var/lib/logrotate/logrotate.status

logrotate操作的日志需要权限正常,并且上级目录权限也要对,解决方案参考:https://chasemp.github.io/2013/07/24/su-directive-logrotate/ 报错信息:

1
2
3
4
5
6
7
rotating pattern: /var/log/myapp/*.log  weekly (4 rotations)
empty log files are rotated, old logs are removed
considering log /var/log/myapp/default.log

error: skipping "/var/log/myapp/default.log" because parent directory has insecure permissions
(It's world writable or writable by group which is not "root") Set "su" directive in
config file to tell logrotate which user/group should be used for rotation

最终效果

1
2
3
4
5
$tail -10 logs/drds-tcp.log
Apr 26 15:27:36 vb kernel: [drds] IN= OUT=eth0 SRC=10.0.186.75 DST=10.0.175.109 LEN=40 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=8182 DPT=9366 SEQ=0 ACK=1747027778 WINDOW=0 RES=0x00 ACK RST URGP=0
Apr 26 15:27:36 vb kernel: [drds] IN= OUT=eth0 SRC=10.0.186.75 DST=10.0.175.109 LEN=40 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=8182 DPT=9368 SEQ=0 ACK=3840170438 WINDOW=0 RES=0x00 ACK RST URGP=0
Apr 26 15:27:36 vb kernel: [drds] IN= OUT=eth0 SRC=10.0.186.75 DST=10.0.175.109 LEN=40 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=8182 DPT=9370 SEQ=0 ACK=3892381139 WINDOW=0 RES=0x00 ACK RST URGP=0
Apr 26 15:27:38 vb kernel: [drds] IN= OUT=eth0 SRC=10.0.186.75 DST=10.0.171.173 LEN=40 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=8182 DPT=38225 SEQ=0 ACK=1436910913 WINDOW=0 RES=0x00 ACK RST URGP=0

tracing_point 监控

对于 4.19内核的kernel,可以通过tracing point来监控重传以及reset包

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
# grep tcp:tcp /sys/kernel/debug/tracing/available_events
tcp:tcp_probe
tcp:tcp_retransmit_synack
tcp:tcp_rcv_space_adjust
tcp:tcp_destroy_sock
tcp:tcp_receive_reset
tcp:tcp_send_reset
tcp:tcp_retransmit_skb

//开启本机发出的 reset 监控,默认输出到:/sys/kernel/debug/tracing/trace_pipe
# echo 1 > /sys/kernel/debug/tracing/events/tcp/tcp_send_reset/enable

//如下是开启重传以及reset的记录,本机ip 10.0.186.140
# cat trace_pipe
//重传
<idle>-0 [002] ..s. 9520196.657431: tcp_retransmit_skb: sport=3306 dport=62460 saddr=10.0.186.140 daddr=10.0.186.70 saddrv6=::ffff:10.0.186.140 daddrv6=::ffff:10.0.186.70
Wisp-Root-Worke-540 [000] .... 9522308.074233: tcp_destroy_sock: sport=3306 dport=20594 saddr=10.0.186.140 daddr=10.0.186.70 saddrv6=::ffff:10.0.186.140 daddrv6=::ffff:10.0.186.70 sock_cookie=51a
C2 CompilerThre-543 [002] ..s. 9522308.074296: tcp_destroy_sock: sport=3306 dport=20670 saddr=10.0.186.140 daddr=10.0.186.70 saddrv6=::ffff:10.0.186.140 daddrv6=::ffff:10.0.186.70 sock_cookie=574

// 被reset
Wisp-Root-Worke-540 [002] ..s. 9522519.353756: tcp_receive_reset: sport=33822 dport=8080 saddr=10.0.186.140 daddr=10.0.171.193 saddrv6=::ffff:10.0.186.140 daddrv6=::ffff:10.0.171.193 sock_cookie=5dd
// 主动reset
DragoonAgent-28297 [002] .... 9522433.144611: tcp_send_reset: sport=8182 dport=61783 saddr=10.0.186.140 daddr=10.0.171.174 saddrv6=::ffff:10.0.186.140 daddrv6=::ffff:10.0.171.174
Wisp-Root-Worke-540 [002] ..s. 9522519.353773: tcp_send_reset: sport=33822 dport=8080 saddr=10.0.186.140 daddr=10.0.171.193 saddrv6=::ffff:10.0.186.140 daddrv6=::ffff:10.0.171.193

// 3306对端中断
cat-28727 [000] ..s. 9524341.650740: inet_sock_set_state: family=AF_INET protocol=IPPROTO_TCP sport=3306 dport=23262 saddr=10.0.186.140 daddr=10.0.186.70 saddrv6=::ffff:10.0.186.140 daddrv6=::ffff:10.0.186.70 oldstate=TCP_SYN_RECV newstate=TCP_ESTABLISHED
<idle>-0 [000] .ns. 9524397.184608: inet_sock_set_state: family=AF_INET protocol=IPPROTO_TCP sport=3306 dport=23262 saddr=10.0.186.140 daddr=10.0.186.70 saddrv6=::ffff:10.0.186.140 daddrv6=::ffff:10.0.186.70 oldstate=TCP_ESTABLISHED newstate=TCP_CLOSE

//8182 主动关闭
<idle>-0 [000] .Ns. 9525499.045236: inet_sock_set_state: family=AF_INET protocol=IPPROTO_TCP sport=8182 dport=25448 saddr=10.0.186.140 daddr=10.0.171.174 saddrv6=::ffff:10.0.186.140 daddrv6=::ffff:10.0.171.174 oldstate=TCP_SYN_RECV newstate=TCP_ESTABLISHED
DragoonAgent-6240 [001] .... 9525499.118092: inet_sock_set_state: family=AF_INET protocol=IPPROTO_TCP sport=8182 dport=25448 saddr=10.0.186.140 daddr=10.0.171.174 saddrv6=::ffff:10.0.186.140 daddrv6=::ffff:10.0.171.174 oldstate=TCP_ESTABLISHED newstate=TCP_FIN_WAIT1
<idle>-0 [000] ..s. 9525499.159032: inet_sock_set_state: family=AF_INET protocol=IPPROTO_TCP sport=8182 dport=25448 saddr=10.0.186.140 daddr=10.0.171.174 saddrv6=::ffff:10.0.186.140 daddrv6=::ffff:10.0.171.174 oldstate=TCP_FIN_WAIT1 newstate=TCP_FIN_WAIT2
<idle>-0 [000] .Ns. 9525499.159056: inet_sock_set_state: family=AF_INET protocol=IPPROTO_TCP sport=8182 dport=25448 saddr=10.0.186.140 daddr=10.0.171.174 saddrv6=::ffff:10.0.186.140 daddrv6=::ffff:10.0.171.174 oldstate=TCP_FIN_WAIT2 newstate=TCP_CLOSE

//3306 被动关闭
<idle>-0 [002] .Ns. 9524484.864509: inet_sock_set_state: family=AF_INET protocol=IPPROTO_TCP sport=3306 dport=23360 saddr=10.0.186.140 daddr=10.0.186.70 saddrv6=::ffff:10.0.186.140 daddrv6=::ffff:10.0.186.70 oldstate=TCP_SYN_RECV newstate=TCP_ESTABLISHED
cat-28568 [002] ..s. 9524496.913199: inet_sock_set_state: family=AF_INET protocol=IPPROTO_TCP sport=3306 dport=23360 saddr=10.0.186.140 daddr=10.0.186.70 saddrv6=::ffff:10.0.186.140 daddrv6=::ffff:10.0.186.70 oldstate=TCP_ESTABLISHED newstate=TCP_CLOSE_WAIT
Wisp-Root-Worke-540 [003] .... 9524496.915450: inet_sock_set_state: family=AF_INET protocol=IPPROTO_TCP sport=3306 dport=23360 saddr=10.0.186.140 daddr=10.0.186.70 saddrv6=::ffff:10.0.186.140 daddrv6=::ffff:10.0.186.70 oldstate=TCP_CLOSE_WAIT newstate=TCP_LAST_ACK
Wisp-Root-Worke-539 [002] .Ns. 9524496.915572: inet_sock_set_state: family=AF_INET protocol=IPPROTO_TCP sport=3306 dport=23360 saddr=10.0.186.140 daddr=10.0.186.70 saddrv6=::ffff:10.0.186.140 daddrv6=::ffff:10.0.186.70 oldstate=TCP_LAST_ACK newstate=TCP_CLOSE

iptables 打通网络

1
2
//本机到 172.16.0.102 不通,但是和 47.100.29.16能通(阿里云弹性ip)
iptables -t nat -A OUTPUT -d 172.16.0.102 -j DNAT --to-destination 47.100.29.16

ipset 组合iptables使用

ipset是iptables的扩展,它允许创建匹配地址集合的规则。普通的iptables链只能单IP匹配, 进行规则匹配时,是从规则列表中从头到尾一条一条进行匹配,这像是在链表中搜索指定节点费力。ipset 提供了把这个 O(n) 的操作变成 O(1) 的方法:就是把要处理的 IP 放进一个集合,对这个集合设置一条 iptables 规则。像 iptable 一样,IP sets 是 Linux 内核提供,ipset 这个命令是对它进行操作的一个工具。
另外ipset的一个优势是集合可以动态的修改,即使ipset的iptables规则目前已经启动,新加的入ipset的ip也生效。

ipset可以以set的形式管理大批IP以及IP段,set可以有多个,通过 ipset修改set后可以立即生效。不用再次修改iptables规则。k8s也会用ipset来管理ip集合

ipset is an extension to iptables that allows you to create firewall rules that match entire “sets” of addresses at once. Unlike normal iptables chains, which are stored and traversed linearly, IP sets are stored in indexed data structures, making lookups very efficient, even when dealing with large sets.

接下来用一个ip+port的白名单案例来展示他们的用法,ipset负责白名单,iptables负责拦截规则:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  240  [2021-11-30 19:57:10] ipset list drds_whitelist_ips |grep "^127.0."
241 [2021-11-30 19:57:27] ipset del drds_whitelist_ips 127.0.0.1 //从set删除ip
248 [2021-11-30 19:58:50] ipset list drds_whitelist_ips |grep "^11.1.2"
249 [2021-11-30 19:59:05] ipset del drds_whitelist_ips 11.1.2.30

#timeout 259200是集合内新增的IP有三天的寿命
ipset create myset hash:net timeout 259200

ipset list drds_whitelist_ips //列出set中的所有ip、ip段
ipset add drds_whitelist_ips 100.1.2.0/24 //从set中增加ip段

iptables -I INPUT 1 -p tcp -j drds_whitelist //创建新规则链drds_whitelist,所有tcp流入的包都跳转到 drds_whitelist规则
//有了以上drds_whitelist_ips这个名单, 接下来可以在iptables规则中使用这个set了
//在第一行增加规则:访问端口1234的tcp请求走规则 drds_whitelist
iptables -I INPUT 1 -p tcp --dport 1234 -j drds_whitelist

//规则drds_whitelist 添加如下三条
//第一条白名单中的来源ip访问1234就ACCEPT,不再走后面的. 关键的白名单列表就取自ipset中的drds_whitelist_ips
iptables -A drds_whitelist -m set --match-set drds_whitelist_ips src -p tcp --dport 1234 -j ACCEPT

//同规则1,记录日志,走到这里说明规则1没生效,那么就是黑名单要拦截的了
iptables -A drds_whitelist -p tcp --dport 1234 -j LOG --log-prefix '[drds_reject] ' --log-level 7 --log-tcp-sequence --log-tcp-options --log-ip-options
//拦截
iptables -A drds_whitelist -p tcp --dport 1234 -j REJECT --reject-with icmp-host-prohibited

经过如上操作后,可以得到iptables规则如下

1
2
3
4
5
6
7
8
9
10
#iptables -L -n --line-numbers
Chain INPUT (policy ACCEPT)
target prot opt source destination
drds_whitelist tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:1234

Chain drds_whitelist (1 references)
target prot opt source destination
1 ACCEPT tcp -- 0.0.0.0/0 0.0.0.0/0 match-set drds_whitelist_ips src tcp dpt:80
2 LOG tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:1234 LOG flags 7 level 7 prefix `[drds_reject] ` --log-level 7 --log-tcp-sequence --log-tcp-options --log-ip-options '
3 REJECT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:1234 reject-with icmp-host-prohibited

从以上Chain drds_whitelist中删除第三条规则

1
iptables -D drds_whitelist 3 

block ip 案例

模拟断网测试的时候可以通过iptables固定屏蔽某几个ip来实现。

创建ipset,存放好需要block的ip列表

1
2
ipset create block_ips hash:net timeout 259200
ipset add block_ips 10.176.2.245

添加iptables过滤规则,规则中不需要列出一堆ip,只需要指定上一步创建好的ipset,以后屏蔽、放开某些ip不需要修改iptables规则了,只需要往ipset添加、删除目标ip

1
2
3
4
5
6
7
iptables -N drds_rule //创建新规则链

iptables -I INPUT 1 -m set --match-set block_ips src -p tcp -j drds_rule //命中就跳转到drds_rule
//这条可有可无,记录日志,方便调试
iptables -I drds_rule -m set --match-set block_ips src -j LOG --log-prefix '[drds_reject] ' --log-level 7 --log-tcp-sequence --log-tcp-options --log-ip-options

iptables -A drds_rule -m set --match-set block_ips src -p tcp -j REJECT --reject-with icmp-host-prohibited

iptables记录日志

记录每个新连接创建的时间,日志在/var/log/kern或者/var/log/dmesg中:

1
2
3
4
5
iptables -I INPUT -m state --state NEW -j LOG --log-prefix "Connection In: "
iptables -I OUTPUT -m state --state NEW -j LOG --log-prefix "Connection Out: "

//检查包,记录invalid包到日志中
iptables -A INPUT -m conntrack --ctstate INVALID -m limit --limit 1/sec -j LOG --log-prefix "invalid: " --log-level 7

在宿主机上执行,然后在dmesg中能看到包的传递流程。只有raw有TRACE能力,nat、filter、mangle都没有。这个方式对性能影响非常大,时延高(增加1秒左右)

1
2
iptables -t raw -A OUTPUT -p icmp -j TRACE
iptables -t raw -A PREROUTING -p icmp -j TRACE

端口转发

iptables

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
iptables -t nat -A PREROUTING -d 10.176.7.5 -p tcp --dport 8507 -j DNAT --to-destination 10.176.7.6:3307
iptables -t nat -D PREROUTING -p tcp --dport 18080 -j DNAT --to-destination 10.176.7.245:8080

#将访问8022端口的进出流量转发到22端口
iptables -t nat -A PREROUTING -p tcp --dport 8022 -j REDIRECT --to-ports 22
iptables -t nat -A PREROUTING -p tcp --dport 8507 -j REDIRECT --to-ports 3307

#将本机的端口转发到其他机器
iptables -t nat -A PREROUTING -d 192.168.172.130 -p tcp --dport 8000 -j DNAT --to-destination 192.168.172.131:80
#将192.168.172.131:80 端口将数据返回给客户端时,将源ip改为192.168.172.130
iptables -t nat -A POSTROUTING -d 192.168.172.131 -p tcp --dport 80 -j SNAT --to 192.168.172.130

#ip 转发,做完转发后netstat能看到两条连接
sudo iptables -t nat -A OUTPUT -d 100.69.170.27 -j DNAT --to-destination 127.0.0.1

/sbin/iptables -t nat -I PREROUTING -d 23.27.6.15 -j DNAT --to-destination 45.61.255.176
/sbin/iptables -t nat -I POSTROUTING -d 45.61.255.176 -j SNAT --to-source 23.27.6.15
/sbin/iptables -t nat -I POSTROUTING -s 45.61.255.176 -j SNAT --to-source 23.27.6.15


#清空nat表的所有链
iptables -t nat -F PREROUTING

#禁止访问某个端口
iptables -A OUTPUT -p tcp --dport 31165 -j DROP

iptables 工作图如下,进来的包走1、2;出去的包走4、5;转发的包走1、3、5

Image

ncat端口转发

1
2
监听本机 9876 端口,将数据转发到 192.168.172.131的 80 端口
ncat --sh-exec "ncat 192.168.172.131 80" -l 9876 --keep-open

scat

1
2
在本地监听12345端口,并将请求转发至192.168.172.131的22端口。
socat TCP4-LISTEN:12345,reuseaddr,fork TCP4:192.168.172.131:22

iptables 屏蔽IP

一分钟内新建22端口连接超过 4 次,不分密码对错, 直接 block.

1
2
3
4
5
6
7
8
9
10
11
12
iptables -A INPUT -p tcp -m tcp --dport 22 -m state --state NEW -m recent --set --name SSH --rsource
iptables -A INPUT -p tcp -m tcp --dport 22 -m state --state NEW -m recent --update --seconds 60 --hitcount 4 --name SSH --rsource -j DROP

或者 block 掉暴力破解 ssh 的 IP
grep "Failed" /var/log/auth.log | \
awk '{print $(NF-3)}' | \
sort | uniq -c | sort -n | \
awk '{if ($1>100) print $2}' | \
xargs -I {} iptables -A INPUT -s {} -j DROP

iptables -A INPUT -p tcp --sport 3306 -j DROP
iptables -A OUTPUT -p tcp --dport 3306 -j DROP

Per-IP rate limiting with iptables

iptables 扔掉指定端口的 ack 包

1
2
//扔掉发给 8000 端口的 ack 包,但是不要扔 rst包
sudo iptables -A OUTPUT -p tcp --dport 8000 -m tcp ! --tcp-flags RST,ACK RST,ACK -m tcp --tcp-flags ACK ACK -j DROP

将 ack delay 1 秒中,故意制造乱序:

1
2
3
4
5
6
tc qdisc add dev eth0 root handle 1: prio
tc qdisc add dev eth0 parent 1:3 handle 30: netem delay 1000ms

iptables -t mangle -A OUTPUT -p tcp --dport 8000 -m tcp ! --tcp-flags RST,ACK RST,ACK -m tcp --tcp-flags ACK ACK -j MARK --set-mark 3

tc filter add dev eth0 protocol ip parent 1:0 prio 3 handle 3 fw flowid 1:3

执行效果(在 172.26.137.120.17922 端抓包):

1
2
3
4
5
6
7
15:31:57.815944 IP 172.26.137.120.17922 > 172.26.137.130.8000: Flags [S], seq 2408440162, win 29200, options [mss 1460,sackOK,TS val 1952321086 ecr 0,nop,wscale 7], length 0
15:31:57.815963 IP 172.26.137.130.8000 > 172.26.137.120.17922: Flags [S.], seq 1188559140, ack 2408440163, win 65160, options [mss 1460,sackOK,TS val 1076070423 ecr 1952321086,nop,wscale 7], length 0
15:31:57.816236 IP 172.26.137.120.17922 > 172.26.137.130.8000: Flags [R.], seq 1, ack 1, win 229, options [nop,nop,TS val 1952321086 ecr 1076070423], length 0 //本来是先 ack,再 RST,但因为 ack delay 了导致先抓到 RST

//1 秒后 ack 发出
15:31:58.816225 IP 172.26.137.120.17922 > 172.26.137.130.8000: Flags [.], ack 1, win 229, options [nop,nop,TS val 1952321086 ecr 1076070423], length 0
15:31:58.816264 IP 172.26.137.130.8000 > 172.26.137.120.17922: Flags [R], seq 1188559141, win 0, length 0

iptables 常用参数

-I : Insert rule at given rule number

-t : Specifies the packet matching table such as nat, filter, security, mangle, and raw.

-L : List info for specific chain (such as INPUT/FORWARD/OUTPUT) of given packet matching table

–line-numbers : See firewall rules with line numbers

-n : Do not resolve names using dns i.e. only show numeric output for IP address and port numbers.

-v : Verbose output. This option makes the list command show the interface name, the rule options (if any), and the TOS masks

NetFilter Hooks

下面几个 hook 是内核协议栈中已经定义好的:

  • NF_IP_PRE_ROUTING: 接收到的包进入协议栈后立即触发此 hook,在进行任何路由判断 (将包发往哪里)之前
  • NF_IP_LOCAL_IN: 接收到的包经过路由判断,如果目的是本机,将触发此 hook
  • NF_IP_FORWARD: 接收到的包经过路由判断,如果目的是其他机器,将触发此 hook
  • NF_IP_LOCAL_OUT: 本机产生的准备发送的包,在进入协议栈后立即触发此 hook
  • NF_IP_POST_ROUTING: 本机产生的准备发送的包或者转发的包,在经过路由判断之后, 将触发此 hook

IPTables 表和链(Tables and Chains)

下面可以看出,内置的 chain 名字和 netfilter hook 名字是一一对应的:

  • PREROUTING: 由 NF_IP_PRE_ROUTING hook 触发
  • INPUT: 由 NF_IP_LOCAL_IN hook 触发
  • FORWARD: 由 NF_IP_FORWARD hook 触发
  • OUTPUT: 由 NF_IP_LOCAL_OUT hook 触发
  • POSTROUTING: 由 NF_IP_POST_ROUTING hook 触发

如果没有匹配到任何规则那么执行默认规则。下面括号中的policy

1
2
3
4
#iptables -L | grep policy
Chain INPUT (policy ACCEPT)
Chain FORWARD (policy ACCEPT)
Chain OUTPUT (policy ACCEPT)

If you would rather deny all connections and manually specify which ones you want to allow to connect, you should change the default policy of your chains to drop. Doing this would probably only be useful for servers that contain sensitive information and only ever have the same IP addresses connect to them.

1
2
3
iptables --policy INPUT DROP`
`iptables --policy OUTPUT DROP`
`iptables --policy FORWARD DROP

iptables规则对性能的影响

蓝色是iptables规则数量,不过如果规则内容差不多,只是ip不一样,完全可以用ipset将他们合并到一条或者几条规则,从而提升性能

image-20220521141020452

iptables 丢包监控

1
2
3
4
5
6
7
8
//每 2 秒中 diff 一下因为命中 iptables 导致的 Drop 包的数量,并高亮差异(丢包数), -v :Verbose  output
watch -d "iptables -nvL | grep DROP "

//输出如下:
Chain OUTPUT (policy ACCEPT 19G packets, 3618G bytes)
pkts bytes target prot opt in out source destination
15 600 DROP tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:8000 option=!8 flags:0x04/0x04
160 6400 DROP tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:22345 option=!8 flags:0x04/0x04

参考资料

深入理解 iptables 和 netfilter 架构

NAT - 网络地址转换(2016)

通过iptables 来控制每个ip的流量

iptables 实用教程

Linux环境变量问题汇总

测试好的脚本放到 crontab 里就报错: 找不到命令

写好一个脚本,测试没有问题,然后放到crontab 想要定时执行,但是总是报错,去看日志的话显示某些命令找不到,这种一般都是因为PATH环境变量变了导致的

自己在shell命令行下测试的时候当前环境变量就是这个用户的环境变量,可以通过命令:env 看到,脚本放到crontab 里面后一般都加了sudo 这个时候 env 变了。比如你可以在命令行下执行 env 和 sudo env 比较一下就发现他们很不一样

sudo有一个参数 -E (–preserver-env)就是为了解决这个问题的。

这个时候再比较一下

  • env
  • sudo env
  • sudo -E env

大概就能理解这里的区别了。

本文后面的回复中有同学提到了:

第一个问题,sudo -E在集团的容器中貌似是不行的,没有特别好的解,我们最后是通过在要执行的脚本中手动source “/etc/profile.d/dockerenv.sh”才行

我也特意去测试了一下官方的Docker容器,也有同样的问题,/etc/profile.d/dockerenv.sh 中的脚本没有生效,然后debug看了看,主要是因为bashrc中的 . 和 source 不同导致的,不能说没有生效,而是加载 /etc/profile.d/dockerenv.sh 是在一个独立的bash 进程中,加载完毕进程结束,所有加载过的变量都完成了生命周期释放了,类似我文章中的export部分提到的。我尝试把 ~/.bashrc 中的 . /etc/bashrc 改成 source /etc/bashrc , 同时也把 /etc/bashrc 中的 . 改成 source,就可以了,再次进到容器不需要任何操作就能看到所有:/etc/profile.d/dockerenv.sh 中的变量了,所以我们制作镜像的时候考虑改改这里

crontab

docker 容器中admin取不到env参数

docker run的时候带入一堆参数,用root能env中能看到这些参数,admin用户也能看见这些参数,但是通过crond用admin就没法启动应用了,因为读不到这些env。

同样一个命令ssh执行不了, 报找不到命令

比如:

ssh user@ip “ ip a “ 报错: bash: ip: command not found

但是你要是先执行 ssh user@ip 连上服务器后,再执行 ip a 就可以,这里是同一个命令通过两种不同的方式使用,但是环境变量也不一样了。

同样想要解决这个问题的话可以先 ssh 连上服务器,再执行 which ip ; env | grep PATH

$ which ip
/usr/sbin/ip
$ env | grep PATH
PATH=/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin

很明显这里因为 ip在/usr/sbin下,而/usr/sbin又在PATH变量中,所以可以找到。

那么接下来我们看看

$ssh user@ip "env | grep PATH"
PATH=/usr/local/bin:/usr/bin

很明显这里的PATH比上面的PATH短了一截,/usr/sbin也没有在里面,所以/usr/sbin 下的ip命令自然也找不到了,这里虽然都是同一个用户,但是他们的环境变量还不一样,有点出乎我的意料之外。

主要原因是我们的shell 分为login shell 和 no-login shell , 先ssh 登陆上去再执行命令就是一个login shell,Linux要为这个终端分配资源。

而下面的直接在ssh 里面放执行命令实际上就不需要login,所以这是一个no-login shell.

login shell 和 no-login shell又有什么区别呢?

  • login shell加载环境变量的顺序是:① /etc/profile ② ~/.bash_profile ③ ~/.bashrc ④ /etc/bashrc
  • 而non-login shell加载环境变量的顺序是: ① ~/.bashrc ② /etc/bashrc

也就是nog-login少了前面两步,我们先看后面两步。

下面是一个 .bashrc 的内容:

$ cat .bashrc 
# .bashrc

# Source global definitions
if [ -f /etc/bashrc ]; then
	. /etc/bashrc
fi

基本没有什么内容,它主要是去加载 /etc/bashrc 而他里面也没有看到sbin相关的东西

那我们再看non-login少的两步: ① /etc/profile ② ~/.bash_profile

cat /etc/profile :
if [ “$EUID” = “0” ]; then
pathmunge /usr/sbin
pathmunge /usr/local/sbin
else
pathmunge /usr/local/sbin after
pathmunge /usr/sbin after
fi

这几行代码就是把 /usr/sbin 添加到 PATH 变量中,正是他们的区别决定了这里的环境变量不一样。

用一张图来表述他们的结构,箭头代表加载顺序,红框代表不同的shell的初始入口
image.png

像 ansible 这种自动化工具,或者我们自己写的自动化脚本,底层通过ssh这种non-login的方式来执行的话,那么都有可能碰到这个问题,如何修复呢?

在 /etc/profile.d/ 下创建一个文件:/etc/profile.d/my_bashenv.sh 内容如下:

$cat /etc/profile.d/my_bashenv.sh 

pathmunge () {
if ! echo $PATH | /bin/egrep -q "(^|:)$1($|:)" ; then
   if [ "$2" = "after" ] ; then
  PATH=$PATH:$1
   else
  PATH=$1:$PATH
   fi
fi
}
 
pathmunge /sbin
pathmunge /usr/sbin
pathmunge /usr/local/sbin
pathmunge /usr/local/bin
pathmunge /usr/X11R6/bin after
 
unset pathmunge

complete -cf sudo
 
    alias chgrp='chgrp --preserve-root'
    alias chown='chown --preserve-root'
    alias chmod='chmod --preserve-root'
    alias rm='rm -i --preserve-root'
    
HISTTIMEFORMAT='[%F %T] '
HISTSIZE=1000
export EDITOR=vim    
export PS1='\n\e[1;37m[\e[m\e[1;32m\u\e[m\e[1;33m@\e[m\e[1;35m\H\e[m \e[4m`pwd`\e[m\e[1;37m]\e[m\e[1;36m\e[m\n\$'

通过前面我们可以看到 /etc/bashrc 总是会去加载 /etc/profile.d/ 下的所有 *.sh 文件,同时我们还可以在这个文件中修改我们喜欢的 shell 配色方案和环境变量等等

脚本前增加如下一行是好习惯

1
#!/bin/bash --login

image-20220505213833017

BASH

1、交互式的登录shell (bash –il xxx.sh)
载入的信息:
/etc/profile
~/.bash_profile( -> ~/.bashrc -> /etc/bashrc)
~/.bash_login
~/.profile

2、非交互式的登录shell (bash –l xxx.sh)
载入的信息:
/etc/profile
~/.bash_profile ( -> ~/.bashrc -> /etc/bashrc)
~/.bash_login
~/.profile
$BASH_ENV

3、交互式的非登录shell (bash –i xxx.sh)
载入的信息:
~/.bashrc ( -> /etc/bashrc)

4、非交互式的非登录shell (bash xxx.sh)
载入的信息:
$BASH_ENV

SH

1、交互式的登录shell
载入的信息:
/etc/profile
~/.profile

2、非交互式的登录shell
载入的信息:
/etc/profile
~/.profile

3、交互式的非登录shell
载入的信息:
$ENV

练习验证一下bash、sh和login、non-login

  • sudo ll 或者 sudo cd 是不是都报找不到命令
  • 先sudo bash 然后执行 ll或者cd就可以了
  • 先sudo sh 然后执行 ll或者cd还是报找不到命令
  • sudo env | grep PATH 然后 sudo bash 后再执行 env | grep PATH 看到的PATH环境变量不一样了

找不到ll、cd命令不是因为login/non-login而是因为这两个命令是bash内部定义的,所以sh找不到,通过type -a cd 可以看到一个命令到底是哪里来的

4、非交互式的非登录shell
载入的信息:
nothing

history 为什么没有输出

1
2
3
4
5
#cat test.sh
echo $$
pwd
echo "abc"
history | tail -5

执行如上测试文件,为什么pwd 的内容有输出,但是第五行的 history 确是空的?效果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#bash test.sh  //为什么这样 history 没有输出
31147
/root
abc

#./test.sh //这样 history 有输出
31151
/root
abc
8596 17/04/24 16:02:35 strace -p 30643
8597 17/04/24 16:15:18 cat test.sh
8598 17/04/24 16:15:22 vi test.sh
8599 17/04/24 16:15:44 bash test.sh
8600 17/04/24 16:15:48 ./test.sh

#source ./test.sh //这样 history 有输出
25179
/root
abc
8589 17/04/24 16:15:18 cat test.sh
8590 17/04/24 16:15:22 vi test.sh
8591 17/04/24 16:15:44 bash test.sh
8592 17/04/24 16:15:48 ./test.sh
8593 17/04/24 16:15:56 source ./test.sh

history 是 bash 的内部命令,在非交互环境里默认关闭,可以通过 set -o history 开启。bash test.sh 执行的时候是新启动了一个非交互的bash,然后读入test.sh再执行,在这个新的bash 执行环境下 history 默认是关闭的

1
2
3
4
5
#echo "set -o | grep history" | bash
history off

#set -o |grep history
history on

如果上面的脚本修改一下:

1
2
3
4
5
6
#cat test.sh
set -o history #打开history 功能
echo $$
pwd
echo "abc"
history | tail -5

然后即使用 bash ./test.sh 也能看到history 的输出了

1
2
3
4
5
6
7
#bash test.sh
31463
/root
abc
1 17/04/24 16:24:25 echo $$
2 17/04/24 16:24:25 echo "abc"
3 17/04/24 16:24:25 history | tail -5

export命令的作用

Linux 中export是一种命令工具通过export命令把shell变量中包含的用户变量导入给子程序.默认情况下子程序仅会继承父程序的环境变量,子程序不会继承父程序的自定义变量,所以需要export让父程序中的自定义变量变成环境变量,然后子程序就能继承过来了。

我们来看一个例子, 有一个变量,名字 abc 内容123 如果没有export ,那么通过bash创建一个新的shell(新shell是之前bash的子程序),在新的shell里面就没有abc这个变量, export之后在新的 shell 里面才可以看到这个变量,但是退出重新login后(产生了一个新的bash,只会加载env)abc变量都不在了

$echo $abc
$abc="123"
$echo $abc
123
$bash
$echo $abc

$exit
exit

$export abc

$echo $abc
123

$bash

$echo $abc
123

一些常见问题

执行好好地shell 脚本换台服务器就:source: not found

source 是bash的一个内建命令(所以你找不到一个/bin/source 这样的可执行文件),也就是他是bash自带的,如果我们执行脚本是这样: sh shell.sh 而shell.sh中用到了source命令的话就会报 source: not found

这是因为bash 和 sh是两个东西,sh是 POSIX shell,你可以把它看成是一个兼容某个规范的shell,而bash是 Bourne-Again shell script, bash是 POSIX shell的扩展,就是bash支持所有符合POSIX shell的规范,但是反过来就不一定了,而这里的 source 恰好就是 bash内建的,不符合 POSIX shell的规范(POSIX shell 中用 . 来代替source)

. (a period)

. filename [arguments]

Read and execute commands from the filename argument in the current shell context. If filename does not contain a slash, the PATH variable is used to find filename. When Bash is not in POSIX mode, the current directory is searched if filename is not found in $PATH. If any arguments are supplied, they become the positional parameters when filename is executed. Otherwise the positional parameters are unchanged. If the -T option is enabled, source inherits any trap on DEBUG; if it is not, any DEBUG trap string is saved and restored around the call to source, and source unsets the DEBUG trap while it executes. If -T is not set, and the sourced file changes the DEBUG trap, the new value is retained when source completes. The return status is the exit status of the last command executed, or zero if no commands are executed. If filename is not found, or cannot be read, the return status is non-zero. This builtin is equivalent to source.

在centos执行好好的脚本放到Ubuntu上就不行了,报语法错误

同上,如果到ubuntu上用 bash shell.sh是可以的,但是sh shell.sh就报语法错误,但是在centos上执行:sh或者bash shell.sh 都可以通过。 在centos上执行 ls -lh /usr/bin/sh 可以看到 /usr/bin/sh link到了 /usr/bin/bash 也就是sh等同于bash,所以都可以通过不足为奇。

但是在ubuntu上执行 ls -lh /usr/bin/sh 可以看到 /usr/bin/sh link到了 /usr/bin/dash , 这就是为什么ubuntu上会报错

source shell.sh 和 bash shell.sh以及 ./shell.sh的区别

source shell.sh就在本shell中展开执行
bash shell.sh表示在本shell启动一个子程序(bash),在子程序中执行 shell.sh (shell.sh中产生的一些环境变量就没法带回父shell进程了), 只需要有读 shell.sh 权限就可以执行
./shell.sh 跟bash shell.sh类似,但是必须要求shell.sh有rx权限,然后根据shell.sh前面的 #! 后面的指示来确定用bash还是sh

$cat test.sh 
echo $$

$echo $$
2299

$source test.sh 
2299

$bash test.sh 
4037

$./test.sh 
4040

如上实例,只有source的时候进程ID和bash进程ID一样,其它方式都创建了一个新的bash进程,所以ID也变了。

bash test.sh 产生一个新的bash,但是这个新的bash中不会加载 .bashrc 需要加载的话必须 bash -l test.sh.

通过ssh 执行命令(命令前有sudo)的时候报错:sudo: sorry, you must have a tty to run sudo

这是因为 /etc/sudoers (Linux控制sudo行为、权限的配置文件)中指定了 requiretty(Redhat、Fedora默认行为),但是 通过ssh远程执行命令是没有tty的(不需要交互)。
解决办法可以试试 ssh -t or -tt (强制分配tty)或者先修改 /etc/sudoers把 requiretty 删掉或者改成 !requiretty

cp 命令即使使用了 -f force参数,overwrite的时候还是弹出交互信息,必须手工输入Y、yes等

Google搜索一下别人给出的方案是这样 echo yes | cp -rf xxx yyy 算是笨办法,但是没有找到这里为什么-f 不管用。
type -a cp 先确认一下 cp到底是个什么东西:

	#type -a cp
	cp is aliased to `cp -i'
	cp is /usr/bin/cp

这下算是有点清楚了,原来默认cp 都是-i了(-i, –interactive prompt before overwrite (overrides a previous -n option)),看起来就是默认情况下为了保护我们的目录不经意间被修改了。所以真的确认要overwrite的话直接用 /usr/bin/cp -f 就不需要每次yes确认了

重定向

sudo docker logs swarm-agent-master >master.log 2>&1 输出重定向http://www.kissyu.org/2016/12/25/shell%E4%B8%AD%3E%20:dev:null%202%20%3E%20&1%E6%98%AF%E4%BB%80%E4%B9%88%E9%AC%BC%EF%BC%9F/

>/dev/null 2>&1 标准输出丢弃 错误输出丢弃
2>&1 >/dev/null 标准输出丢弃 错误输出屏幕

http://kodango.com/bash-one-liners-explained-part-three

umask

创建文件的默认权限是 666 文件夹是777 但是都要跟 umask做运算(按位减法) 一般umask是002
所以创建出来文件最终是664,文件夹是775,如果umask 是027的话最终文件是 640 文件夹是750
『尽量不要以数字相加减啦!』你应该要这样想(-rw-rw- rw-) – (——–wx)=-rw-rw-r–这样就对啦!不要用十进制的数字喔!够能力的话,用二进制来算,不晓得的话,用 rwx 来算喔!

其它

echo $-   // himBH 

“$-” 中含有“i”代表“交互式shell”
“$0”的显示结果为“-bash”,bash前面多个“-”,代表“登录shell”.
没有“i“和“-”的,是“非交互式的非登录shell”

set +o histexpand (! 是history展开符号, histexpand 可以打开或者关闭这个展开符)
alias 之后,想要用原来的命令:+alias (命令前加)

bash程序执行,当“$0”是“sh”的时候,则要求下面的代码遵循一定的规范,当不符合规范的语法存在时,则会报错,所以可以这样理解,“sh”并不是一个程序,而是一种标准(POSIX),这种标准,在一定程度上(具体区别见下面的“Things bash has that sh does not”)保证了脚本的跨系统性(跨UNIX系统)

Linux 分 shell变量(set),用户变量(env), shell变量包含用户变量,export是一种命令工具,是显式那些通过export命令把shell变量中包含的用户变量导入给用户变量的那些变量.

set -euxo pipefail //-u unset -e 异常退出 http://www.ruanyifeng.com/blog/2017/11/bash-set.html

引号

shell 中:单引号的处理是比较简单的,被单引号包括的所有字符都保留原有的意思,例如’$a’不会被展开, ‘cmd‘也不会执行命令;而双引号,则相对比较松,在双引号中,以下几个字符 $, `, \ 依然有其特殊的含义,比如$可以用于变量展开, 反引号`可以执行命令,反斜杠\可以用于转义。但是,在双引号包围的字符串里,反斜杠的转义也是有限的,它只能转义$, `, “, \或者newline(回车)这几个字符,后面如果跟着的不是这几个字符,只不会被黑底,反斜杠会被保留 http://kodango.com/simple-bash-programming-skills-2

su 和 su - 的区别

su命令和su -命令最大的本质区别就是:前者只是切换了root身份,但Shell环境仍然是普通用户的Shell;而后者连用户和Shell环境一起切换成root身份了。只有切换了Shell环境才不会出现PATH环境变量错误。su切换成root用户以后,pwd一下,发现工作目录仍然是普通用户的工作目录;而用su -命令切换以后,工作目录变成root的工作目录了。用echo $PATH命令看一下su和su -以后的环境变量有何不同。以此类推,要从当前用户切换到其它用户也一样,应该使用su -命令。

比如:
su admin 会重新加载 ~/.bashrc ,但是不会切换到admin 的home目录。
但是 su - admin 不会重新加载 ~/.bashrc ,但是会切换admin的home目录。

The su command is used to become another user during a login session. Invoked without a username, su defaults to becoming the superuser. The optional argument - may be used to provide an environment similar to what the user would expect had the user logged in directly.

后台任务执行

将任务放到后台,断开ssh后还能运行:
“ctrl-Z”将当前任务挂起(实际是发送 SIGTSTP 信号),父进程ssh退出时会给所有子进程发送 SIGHUP;

jobs -l 查看所有job

“disown -h %序号” 让该任务忽略SIGHUP信号(不会因为掉线而终止执行),序号为 Jobs -l 看到的顺序号;
“bg”让该任务在后台恢复运行。

shell 调试与参数

为了方便 Debug,有时在启动 Bash 的时候,可以加上启动参数。

  • -n:不运行脚本,只检查是否有语法错误。
  • -v:输出每一行语句运行结果前,会先输出该行语句。
  • -x:每一个命令处理完以后,先输出该命令,再进行下一个命令的处理。
1
2
3
$ bash -n scriptname
$ bash -v scriptname
$ bash -x scriptname

shell 数值运算

bash中数值运算要这样 $(( $a+$b )) // declare -i 才是定义一个整型变量

  • 在中括号 [] 内的每个组件都需要有空白键来分隔;
  • 在中括号内的变量,最好都以双引号括号起来;
  • 在中括号内的常数,最好都以单或双引号括号起来。

在bash中为变量赋值的语法是foo=bar,访问变量中存储的数值,其语法为 $foo。 需要注意的是,foo = bar (使用空格隔开)是不能正确工作的,因为解释器会调用程序foo 并将 =bar作为参数。 总的来说,在shell脚本中使用空格会起到分割参数的作用,有时候可能会造成混淆,请务必多加检查。

其它

  • 系统合法的 shell 均写在 /etc/shells 文件中;
  • 用户默认登陆取得的 shell 记录于 /etc/passwd 的最后一个字段;
  • type 可以用来找到运行命令为何种类型,亦可用于与 which 相同的功能 [type -a];
  • 变量主要有环境变量与自定义变量,或称为全局变量与局部变量
  • 使用 env 与 export 可观察环境变量,其中 export 可以将自定义变量转成环境变量;
  • set 可以观察目前 bash 环境下的所有变量;
  • stty -a
  • $? 亦为变量,是前一个命令运行完毕后的回传值。在 Linux 回传值为 0 代表运行成功;
  • bash 的配置文件主要分为 login shell 与 non-login shell。login shell 主要读取 /etc/profile 与 ~/.bash_profile, non-login shell 则仅读取 ~/.bashrc

在bash中进行比较时,尽量使用双方括号 [[ ]] 而不是单方括号 [ ]这样会降低犯错的几率,尽管这样并不能兼容 sh

type

执行顺序(type -a ls 可以查看到顺序):

  1. 以相对/绝对路径运行命令,例如『 /bin/ls 』或『 ./ls 』;
  2. 由 alias 找到该命令来运行;
  3. 由 bash 内建的 (builtin) 命令来运行;
  4. 透过 $PATH 这个变量的顺序搜寻到的第一个命令来运行。

tldr 可以用来查询命令的常用语法,比man简短些,偏case型

参考文章:

关于ansible远程执行的环境变量问题

Bash和Sh的区别

什么是交互式登录 Shell what-is-interactive-and-login-shell

Shell 默认选项 himBH 的解释

useful-documents-about-shell

linux cp实现强制覆盖

https://wangdoc.com/bash/startup.html

编写一个最小的 64 位 Hello World

计算机教育中缺失的一课

macOS设置环境变量path/paths的完全总结 很详细

如何设置git Proxy

git http proxy

首先你要有一个socks5代理服务器,从 github.com 拉代码的话海外的代理速度才快,可以用阿里郎的网络加速,也可以自己配置shadowsocks这样的代理。

Windows 阿里郎会在本地生成socks5代理:127.0.0.1:13658

下面的例子假设你的socks5代理是: 127.0.0.1:13658

配置git http proxy

git config --global http.proxy socks5h://127.0.0.1:13658 //或者 socks5://127.0.0.1:13658

上面的命令实际上是修改了 .gitconfig:

$cat ~/.gitconfig   
[http]
	proxy = socks5h://127.0.0.1:13658

现在git的http代理就配置好了, git clone https://github.com/torvalds/linux.git 速度会快到你流泪(取决于你的代理速度),我这里是从每秒10K到了3M 。

注意:

  • http.proxy就可以了,不需要配置https.proxy
  • 这个http代理仅仅针对 git clone https:// 的方式生效
  • socks5 本地解析域名;socks5h 将域名也发到远程代理来解析(推荐使用,比如 github.com 在 2024 走 socks5 都无法拉取)

配置git ssh proxy

如果想要 git clone **git@**github.com:torvalds/linux.git 也要快起来的话 需要配置 ssh proxy

这里要求你有一台海外的服务器,能ssh登陆,做好免密码,假设这台服务器的IP是:2.2.2.2

修改(如果没有就创建这个文件)~/.ssh/config, 内容如下:

$cat ~/.ssh/config 
host github.com
#LogLevel DEBUG3
ProxyCommand ssh -l root 2.2.2.2 exec /usr/bin/nc %h %p

然后 git clone git@github.com:torvalds/linux.git 也能飞起来了

需要注意你的代理服务器2.2.2.2上nc有没有安装,没有的话yum装上,装上后再检查一下安装的位置,对应配置中的 /usr/bin/nc
写这些主要是从Google上搜索到的一些文章,http的倒还是靠谱,但是ssh的就有点乱,还要在本地安装东西,对nc版本有要求之类的,于是就折腾了一下,上面的方式都是靠谱的。

整个原理还是穿墙术。 可以参考 :SSH 高级用法和技巧大全

配置git 走socks

如果没有海外服务器,但是本地已经有了socks5 服务那么也可以直接走socks5来proxy所有git 流量

1
2
3
4
cat ~/.ssh/config
host github.com
ProxyCommand /usr/bin/nc -X 5 -x 127.0.0.1:12368 %h %p //走本地socks5端口来转发代理流量
#ProxyCommand ssh -l root jump exec /usr/bin/nc %h %p //这个是走 jump

nc 代理参数-X proxy_version 指定 nc 请求时使用代理服务的协议

  • proxy_version4 : 表示使用的代理为 SOCKS4 代理
  • proxy_version5 : 表示使用的代理为 SOCKS5 代理
  • proxy_versionconnect : 表示使用的代理为 HTTPS 代理
  • 如果不指定协议, 则默认使用的代理为 SOCKS5 代理

-X proxy_version
Requests that nc should use the specified protocol when talking to the proxy server. Supported protocols are ‘’4’’ (SOCKS v.4), ‘’5’’ (SOCKS v.5) and ‘’connect’’ (HTTPS proxy). If the protocol is not specified, SOCKS version 5 is used.

我的拉起代理自动脚本

下面的脚本总共拉起了三个socks5代理,端口13657-13659,其中13659是阿里郎网络加速的代理
最后还启动了一个8123的http 代理(有些场景只支持http代理)

macOS:

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
listPort=`/usr/sbin/netstat -ant |grep "127.0.0.1.13658" |grep LISTEN`
if [[ "$listPort" != tcp4* ]]; then
#sh ~/ssh-jump.sh
nohup ssh -qTfnN -D 13658 root@jump vmstat 10 >/dev/null 2>&1
echo "start socks5 on port 13658"
fi

listPort=`/usr/sbin/netstat -ant |grep "127.0.0.1.13657" |grep LISTEN`
if [[ "$listPort" != tcp4* ]]; then
nohup ssh -qTfnN -D 13657 azureuser@yu2 vmstat 10 >/dev/null 2>&1
echo "start socks5 on port 13657"
fi

listPort=`/usr/sbin/netstat -ant |grep "127.0.0.1.13659" |grep LISTEN`
#if [ "$listPort" != "tcp4 0 0 127.0.0.1.13659 *.* LISTEN " ]; then
if [[ "$listPort" != tcp4* ]]; then
Applications/AliLang.app/Contents/Resources/AliMgr/AliMgrSockAgent -bd 参数1 -wd 工号 -td 参数2 >~/jump.log 2>&1
echo "start listPort $listPort"
fi

listPort=`/usr/sbin/netstat -ant |grep "127.0.0.1.8123 " |grep LISTEN`
if [[ "$listPort" != tcp4* ]]; then
polipo socksParentProxy=127.0.0.1:13659 1>~/jump.log 2>1&
echo "start polipo http proxy at 8123"
fi

#分别测试http和socks5代理能工作
#curl --proxy http://127.0.0.1:8123 https://www.google.com
#curl -x socks5h://localhost:13657 http://www.google.com/

Docker 常见问题

启动

docker daemon启动的时候如果报 socket错误,是因为daemon启动参数配置了: -H fd:// ,但是 docker.socket是disable状态,启动daemon依赖socket,但是systemctl又拉不起来docker.socket,因为被disable了,先 sudo systemctl enable docker.socket 就可以了。

如果docker.socket service被mask后比disable更粗暴,mask后手工都不能拉起来了,但是disable后还可以手工拉起,然后再拉起docker service。 这是需要先 systemctl unmask

1
2
$sudo systemctl restart docker.socket
Failed to restart docker.socket: Unit docker.socket is masked.

另外 docker.socket 启动依赖环境的要有 docker group这个组,可以添加: groupadd docker

failed to start docker.service unit not found. rhel 7.7

systemctl list-unit-files |grep docker.service 可以看到docker.service 是存在并enable了

实际是redhat 7.7的yum仓库所带的docker启动参数变了, 如果手工启动的话也会报找不到docker-runc 手工:

1
ln -s /usr/libexec/docker/docker-runc-current /usr/bin/docker-runc

https://access.redhat.com/solutions/2876431 https://stackoverflow.com/questions/42754779/docker-runc-not-installed-on-system

yum安装docker会在 /etc/sysconfig 下放一些配置参数(docker.service 环境变量)

Docker 启动报错: Error starting daemon: Error initializing network controller: list bridge addresses failed: no available network

这是因为daemon启动的时候缺少docker0网桥,导致启动失败,手工添加:

1
2
ip link add docker0 type bridge
ip addr add dev docker0 172.30.0.0/24

启动成功后即使手工删除docker0,然后再次启动也会成功,这次会自动创建docker0 172.30.0.0/16 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#systemctl status docker -l
● docker.service - Docker Application Container Engine
Loaded: loaded (/etc/systemd/system/docker.service; enabled; vendor preset: disabled)
Active: failed (Result: exit-code) since Fri 2021-01-22 17:21:45 CST; 2min 12s ago
Docs: http://docs.docker.io
Process: 68318 ExecStartPost=/sbin/iptables -I FORWARD -s 0.0.0.0/0 -j ACCEPT (code=exited, status=0/SUCCESS)
Process: 68317 ExecStart=/opt/kube/bin/dockerd (code=exited, status=1/FAILURE)
Main PID: 68317 (code=exited, status=1/FAILURE)

Jan 22 17:21:43 l57f12112.sqa.nu8 dockerd[68317]: time="2021-01-22T17:21:43.991179104+08:00" level=warning msg="failed to load plugin io.containerd.snapshotter.v1.aufs" error="modprobe aufs failed: "modprobe: FATAL: Module aufs not found.\n": exit status 1"
Jan 22 17:21:43 l57f12112.sqa.nu8 dockerd[68317]: time="2021-01-22T17:21:43.991371956+08:00" level=warning msg="could not use snapshotter btrfs in metadata plugin" error="path /var/lib/docker/containerd/daemon/io.containerd.snapshotter.v1.btrfs must be a btrfs filesystem to be used with the btrfs snapshotter"
Jan 22 17:21:43 l57f12112.sqa.nu8 dockerd[68317]: time="2021-01-22T17:21:43.991381620+08:00" level=warning msg="could not use snapshotter aufs in metadata plugin" error="modprobe aufs failed: "modprobe: FATAL: Module aufs not found.\n": exit status 1"
Jan 22 17:21:43 l57f12112.sqa.nu8 dockerd[68317]: time="2021-01-22T17:21:43.991388991+08:00" level=warning msg="could not use snapshotter zfs in metadata plugin" error="path /var/lib/docker/containerd/daemon/io.containerd.snapshotter.v1.zfs must be a zfs filesystem to be used with the zfs snapshotter: skip plugin"
Jan 22 17:21:44 l57f12112.sqa.nu8 systemd[1]: Stopping Docker Application Container Engine...
Jan 22 17:21:45 l57f12112.sqa.nu8 dockerd[68317]: failed to start daemon: Error initializing network controller: list bridge addresses failed: PredefinedLocalScopeDefaultNetworks List: [172.17.0.0/16 172.18.0.0/16 172.19.0.0/16 172.20.0.0/16 172.21.0.0/16 172.22.0.0/16 172.23.0.0/16 172.24.0.0/16 172.25.0.0/16 172.26.0.0/16 172.27.0.0/16 172.28.0.0/16 172.29.0.0/16 172.30.0.0/16 172.31.0.0/16 192.168.0.0/20 192.168.16.0/20 192.168.32.0/20 192.168.48.0/20 192.168.64.0/20 192.168.80.0/20 192.168.96.0/20 192.168.112.0/20 192.168.128.0/20 192.168.144.0/20 192.168.160.0/20 192.168.176.0/20 192.168.192.0/20 192.168.208.0/20 192.168.224.0/20 192.168.240.0/20]: no available network
Jan 22 17:21:45 l57f12112.sqa.nu8 systemd[1]: docker.service: main process exited, code=exited, status=1/FAILURE
Jan 22 17:21:45 l57f12112.sqa.nu8 systemd[1]: Stopped Docker Application Container Engine.
Jan 22 17:21:45 l57f12112.sqa.nu8 systemd[1]: Unit docker.service entered failed state.
Jan 22 17:21:45 l57f12112.sqa.nu8 systemd[1]: docker.service failed.

参考:https://github.com/docker/for-linux/issues/123

或者这样解决:https://stackoverflow.com/questions/39617387/docker-daemon-cant-initialize-network-controller

This was related to the machine having several network cards (can also happen in machines with VPN)

The solution was to start manually docker like this:

1
/usr/bin/docker daemon --debug --bip=192.168.y.x/24

where the 192.168.y.x is the MAIN machine IP and /24 that ip netmask. Docker will use this network range for building the bridge and firewall riles. The –debug is not really needed, but might help if something else fails.

After starting once, you can kill the docker and start as usual. AFAIK, docker have created a cache config for that –bip and should work now without it. Of course, if you clean the docker cache, you may need to do this again.

本机网络信息默认保存在:/var/lib/docker/network/files/local-kv.db 想要清理bridge网络的话,不能直接 docker network rm bridge 因为bridge是预创建的受保护不能直接删除,可以删掉:/var/lib/docker/network/files/local-kv.db 并且同时删掉 docker0 然后重启dockerd就可以了

alios下容器里面ping不通docker0

alios上跑docker,然后启动容器,发现容器里面ping不通docker0, 手工重新brctl addbr docker0 , 然后把虚拟网卡加进去就可以了。应该是系统哪里bug了.

image.png

非常神奇的是不通的时候如果在宿主机上对docker0抓包就瞬间通了,停掉抓包就不通

docker0-tcpdump.gif

猜测是 alios 的bug

systemctl start docker

Failed to start docker.service: Unit not found.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
UNIT LOAD PATH
Unit files are loaded from a set of paths determined during
compilation, described in the two tables below. Unit files found
in directories listed earlier override files with the same name
in directories lower in the list.

Table 1. Load path when running in system mode (--system).
┌────────────────────────┬─────────────────────────────┐
│Path │ Description │
├────────────────────────┼─────────────────────────────┤
│/etc/systemd/system │ Local configuration │
├────────────────────────┼─────────────────────────────┤
│/run/systemd/system │ Runtime units │
├────────────────────────┼─────────────────────────────┤
│/usr/lib/systemd/system │ Units of installed packages │
└────────────────────────┴─────────────────────────────┘

systemd 设置path环境变量,可以设置

[Service]
Type=notify
Environment=PATH=/opt/kube/bin:/sbin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/usr/X11R6/bin:/opt/satools:/root/bin

容器没有systemctl

Failed to get D-Bus connection: Operation not permitted: systemd容器中默认无法启动,需要启动容器的时候

1
docker run -itd --privileged --name=ren drds_base:centos init //init 必须要或者systemd

1号进程需要是systemd(init 是systemd的link),才可以使用systemctl,推荐用这个来解决:https://github.com/gdraheim/docker-systemctl-replacement

systemd是用来取代init的,之前init管理所有进程启动,是串行的,耗时久,也不管最终状态,systemd主要是串行并监控进程状态能反复重启。

新版本init link向了systemd

busybox/Alpine/Scratch

busybox集成了常用的linux工具(nc/telnet/cat……),保持精细,方便一张软盘能装下。

Alpine一个精简版的Linux 发行版,更小更安全,用的musl libc而不是glibc

scratch一个空的框架,什么也没有

找不到shell

Dockerfile 中(https://www.ardanlabs.com/blog/2020/02/docker-images-part1-reducing-image-size.html):

1
2
3
CMD ./hello OR RUN 等同于 /bin/sh -c "./hello", 需要shell,
改用:
CMD ["./hello"] 等同于 ./hello 不需要shell

entrypoint VS cmd

dockerfile中:CMD 可以是命令、也可以是参数,如果是参数, 把它传递给:ENTRYPOINT

在写Dockerfile时, ENTRYPOINT或者CMD命令会自动覆盖之前的ENTRYPOINT或者CMD命令

从参数中传入的ENTRYPOINT或者CMD命令会自动覆盖Dockerfile中的ENTRYPOINT或者CMD命令

copy VS add

COPY指令和ADD指令的唯一区别在于是否支持从远程URL获取资源。 COPY指令只能从执行docker build所在的主机上读取资源并复制到镜像中。 而ADD指令还支持通过URL从远程服务器读取资源并复制到镜像中。

满足同等功能的情况下,推荐使用COPY指令。ADD指令更擅长读取本地tar文件并解压缩

Digest VS Image ID

pull镜像的时候,将docker digest带上,即使黑客使用手段将某一个digest对应的内容强行修改了,docker也能check出来,因为docker会在pull下镜像的时候,只要根据image的内容计算sha256

1
docker images --digests
  • The “digest” is a hash of the manifest, introduced in Docker registry v2.
  • The image ID is a hash of the local image JSON configuration. 就是inspect 看到的 RepoDigests

容器中抓包和调试 – nsenter

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
获取pid:docker inspect -f {{.State.Pid}} c8f874efea06

进入namespace:nsenter --target 17277 --net --pid –mount

//只进入network namespace,这样看到的文件还是宿主机的,能直接用tcpdump,但是看到的网卡是容器的
nsenter --target 17277 --net

// ip netns 获取容器网络信息
1022 [2021-04-14 15:53:06] docker inspect -f '{{.State.Pid}}' ab4e471edf50 //获取容器进程id
1023 [2021-04-14 15:53:30] ls /proc/79828/ns/net
1024 [2021-04-14 15:53:57] ln -sfT /proc/79828/ns/net /var/run/netns/ab4e471edf50 //link 以便ip netns List能访问

// 宿主机上查看容器ip
1026 [2021-04-14 15:54:11] ip netns list
1028 [2021-04-14 15:55:19] ip netns exec ab4e471edf50 ifconfig

//nsenter调试网络
Get the pause container's sandboxkey:
root@worker01:~# docker inspect k8s_POD_ubuntu-5846f86795-bcbqv_default_ea44489d-3dd4-11e8-bb37-02ecc586c8d5_0 | grep SandboxKey
"SandboxKey": "/var/run/docker/netns/82ec9e32d486",
root@worker01:~#
Now, using nsenter you can see the container's information.
root@worker01:~# nsenter --net=/var/run/docker/netns/82ec9e32d486 ip addr show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
3: eth0@if7: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP group default
link/ether 0a:58:0a:f4:01:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 10.244.1.2/24 scope global eth0
valid_lft forever preferred_lft forever
Identify the peer_ifindex, and finally you can see the veth pair endpoint in root namespace.
root@worker01:~# nsenter --net=/var/run/docker/netns/82ec9e32d486 ethtool -S eth0
NIC statistics:
peer_ifindex: 7
root@worker01:~#
root@worker01:~# ip -d link show | grep '7: veth'
7: veth5e43ca47@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master cni0 state UP mode DEFAULT group default
root@worker01:~#

nsenter相当于在setns的示例程序之上做了一层封装,使我们无需指定命名空间的文件描述符,而是指定进程号即可,详细case

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#docker inspect cb7b05d82153 | grep -i SandboxKey   //根据 pause 容器id找network namespace
"SandboxKey": "/var/run/docker/netns/d6b2ef3cf886",

[root@hygon252 19:00 /root]
#nsenter --net=/var/run/docker/netns/d6b2ef3cf886 ip addr show
3: eth0@if496: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP group default //496对应宿主机上的veth编号
link/ether 1e:95:dd:d9:88:bd brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 192.168.3.22/24 brd 192.168.3.255 scope global eth0
valid_lft forever preferred_lft forever
#nsenter --net=/var/run/docker/netns/d6b2ef3cf886 ethtool -S eth0
NIC statistics:
peer_ifindex: 496

#ip -d -4 addr show cni0
475: cni0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP group default qlen 1000
link/ether 8e:34:ba:e2:a4:c6 brd ff:ff:ff:ff:ff:ff promiscuity 0 minmtu 68 maxmtu 65535
bridge forward_delay 1500 hello_time 200 max_age 2000 ageing_time 30000 stp_state 0 priority 32768 vlan_filtering 0 vlan_protocol 802.1Q bridge_id 8000.8e:34:ba:e2:a4:c6 designated_root 8000.8e:34:ba:e2:a4:c6 root_port 0 root_path_cost 0 topology_change 0 topology_change_detected 0 hello_timer 0.00 tcn_timer 0.00 topology_change_timer 0.00 gc_timer 43.31 vlan_default_pvid 1 vlan_stats_enabled 0 group_fwd_mask 0 group_address 01:80:c2:00:00:00 mcast_snooping 1 mcast_router 1 mcast_query_use_ifaddr 0 mcast_querier 0 mcast_hash_elasticity 4 mcast_hash_max 512 mcast_last_member_count 2 mcast_startup_query_count 2 mcast_last_member_interval 100 mcast_membership_interval 26000 mcast_querier_interval 25500 mcast_query_interval 12500 mcast_query_response_interval 1000 mcast_startup_query_interval 3124 mcast_stats_enabled 0 mcast_igmp_version 2 mcast_mld_version 1 nf_call_iptables 0 nf_call_ip6tables 0 nf_call_arptables 0 numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
inet 192.168.3.1/24 brd 192.168.3.255 scope global cni0
valid_lft forever preferred_lft forever

创建虚拟网卡

1
2
3
4
5
6
7
8
To make this interface you'd first need to make sure that you have the dummy kernel module loaded. You can do this like so:
$ sudo lsmod | grep dummy
$ sudo modprobe dummy
$ sudo lsmod | grep dummy
dummy 12960 0
With the driver now loaded you can create what ever dummy network interfaces you like:

$ sudo ip link add eth10 type dummy

修改网卡名字

1
2
3
4
5
6
7
8
9
ip link set ens33 down
ip link set ens33 name eth0
ip link set eth0 up

mv /etc/sysconfig/network-scripts/ifcfg-{ens33,eth0}
sed -ire "s/NAME=\"ens33\"/NAME=\"eth0\"/" /etc/sysconfig/network-scripts/ifcfg-eth0
sed -ire "s/DEVICE=\"ens33\"/DEVICE=\"eth0\"/" /etc/sysconfig/network-scripts/ifcfg-eth0
MAC=$(cat /sys/class/net/eth0/address)
echo -n 'HWADDR="'$MAC\" >> /etc/sysconfig/network-scripts/ifcfg-eth0

OS版本

搞Docker就得上el7, 6的性能太差了 Docker 对 Linux 内核版本的最低要求是3.10,如果内核版本低于 3.10 会缺少一些运行 Docker 容器的功能。这些比较旧的内核,在一定条件下会导致数据丢失和频繁恐慌错误。

清理mount文件

删除 /var/lib/docker 目录如果报busy,一般是进程在使用中,可以fuser查看哪个进程在用,然后杀掉进程;另外就是目录mount删不掉问题,可以 mount | awk ‘{ print $3 }’ |grep overlay2| xargs umount 批量删除

No space left on device

OSError: [Errno 28] No space left on device

​ 大部分时候不是真的磁盘没有空间了还有可能是inode不够了(df -ih 查看inode使用率)

​ 尝试用 fallocate 来测试创建文件是否成功

​ 尝试fdisk-l / tune2fs -l 来确认分区和文件系统的正确性

​ fallocate 创建一个文件名很长的文件失败(也就是原始报错的文件名),同时fallocate 创建一个短文件名的文件成功

​ dmesg 查看系统报错信息

1
2
[13155344.231942] EXT4-fs warning (device sdd): ext4_dx_add_entry:2461: Directory (ino: 3145729) index full, reach max htree level :2
[13155344.231944] EXT4-fs warning (device sdd): ext4_dx_add_entry:2465: Large directory feature is not enabled on this filesystem

img

看起来是小文件太多撑爆了ext4的BTree索引,通过 tune2fs -l /dev/nvme1n1p1 验证下

1
2
3
4
5
6
7
8
#tune2fs -l /dev/nvme1n1p1 |grep Filesystem
Filesystem volume name: /flash2
Filesystem revision #: 1 (dynamic)
Filesystem features: has_journal ext_attr resize_inode dir_index filetype needs_recovery extent 64bit flex_bg sparse_super large_file huge_file uninit_bg dir_nlink extra_isize
Filesystem flags: signed_directory_hash
Filesystem state: clean
Filesystem OS type: Linux
Filesystem created: Fri Mar 6 17:08:36 2020

​ 执行 tune2fs -O large_dir /dev/nvme1n1p1 打开 large_dir 选项

1
2
tune2fs -l /dev/nvme1n1p1 |grep -i large
Filesystem features: has_journal ext_attr resize_inode dir_index filetype needs_recovery extent flex_bg large_dir sparse_super large_file huge_file uninit_bg dir_nlink extra_isize

如上所示,开启后Filesystem features 多了 large_dir,不过4.13以上内核才支持这个功能

CPU 资源分配

对于cpu的限制,Kubernetes采用cfs quota来限制进程在单位时间内可用的时间片。当独享和共享实例在同一台node节点上的时候,一旦实例的工作负载增加,可能会导致独享实例工作负载在不同的cpu核心上来回切换,影响独享实例的性能。所以,为了不影响独享实例的性能,我们希望在同一个node上,独享实例和共享实例的cpu能够分开绑定,互不影响。

内核的默认cpu.shares是1024,也可以通过 cpu.cfs_quota_us / cpu.cfs_period_us去控制容器规格(除后的结果就是核数)

cpu.shares 多层级限制后上层有更高的优先级,可能会经常看到 CPU 多核之间不均匀的现象,部分核总是跑不满之类的。 cpu.shares 是用来调配争抢用,比如离线、在线混部可以通过 cpu.shares 多给在线业务

给容器限制16core的quota:

1
docker update --cpu-quota=1600000 --cpu-period=100000 c1 c2

sock

docker有两个sock,一个是dockershim.sock,一个是docker.sock。dockershim.sock是由实现了CRI接口的一个插件提供的,主要把k8s请求转换成docker请求,最终docker还是要 通过docker.sock来管理容器。

kubelet —CRI—-> docker-shim(kubelet内置的CRI-plugin) –> docker

docker image api

1
2
3
4
5
获取所有镜像名字: GET /v2/_catalog   
curl registry:5000/v2/_catalog

获取某个镜像的tag: GET /v2/<name>/tags/list
curl registry:5000/v2/drds/corona-server/tags/list

从registry中删除镜像

默认registry仓库不支持删除镜像,修改registry配置来支持删除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#cat config.yml
version: 0.1
log:
fields:
service: registry
storage:
delete: //增加如下两行,默认是false,不能删除
enabled: true
cache:
blobdescriptor: inmemory
filesystem:
rootdirectory: /var/lib/registry
http:
addr: :5000
headers:
X-Content-Type-Options: [nosniff]
health:
storagedriver:
enabled: true
interval: 10s
threshold: 3

#docker cp ./config.yml registry:/etc/docker/registry/config.yml
#docker restart registry

然后通过API来查询要删除镜像的id:

1
2
3
4
5
6
7
8
9
//查询要删除镜像的tag
curl registry:5000/v2/drds/corona-server/tags/list
//根据tag查找Etag
curl -v registry:5000/v2/drds/corona-server/manifests/2.0.0_3012622_20220214_4ca91d96-arm64 -H 'Accept: application/vnd.docker.distribution.manifest.v2+json'
//根据前一步返回的Etag来删除对应的tag
curl -X DELETE registry:5000/v2/drds/corona-server/manifests/sha256:207ec19c1df6a3fa494d41a1a8b5332b969a010f0d4d980e39f153b1eaca2fe2 -v

//执行垃圾回收
docker exec -it registry bin/registry garbage-collect /etc/docker/registry/config.yml

检查是否restart能支持只重启deamon,容器还能正常运行

1
2
3
$sudo docker info | grep Restore
Live Restore Enabled: true

参考资料

https://www.ardanlabs.com/blog/2020/02/docker-images-part1-reducing-image-size.html

Linux LVM使用

LVM是 Logical Volume Manager(逻辑卷管理)的简写, 用来解决磁盘分区大小动态分配。LVM不是软RAID(Redundant Array of Independent Disks)。

从一块硬盘到能使用LV文件系统的步骤:

硬盘—-分区(fdisk)—-PV(pvcreate)—-VG(vgcreate)—-LV(lvcreate)—-格式化(mkfs.ext4 LV为ext文件系统)—-挂载

img

LVM磁盘管理方式

image-20220725100705140

lvreduce 缩小LV

先卸载—>然后减小逻辑边界—->最后减小物理边界—>在检测文件系统 ==谨慎用==

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[aliyun@uos15 15:07 /dev/disk/by-label]
$sudo e2label /dev/nvme0n1p1 polaru01 //给磁盘打标签

[aliyun@uos15 15:07 /dev/disk/by-label]
$lsblk -f
NAME FSTYPE LABEL UUID FSAVAIL FSUSE% MOUNTPOINT
sda
├─sda1 vfat EFI D0E3-79A8 299M 0% /boot/efi
├─sda2 ext4 Boot f204c992-fb20-40e1-bf58-b11c994ee698 1.3G 6% /boot
├─sda3 ext4 Roota dbc68010-8c36-40bf-b794-271e59ff5727 14.8G 61% /
├─sda4 ext4 Rootb 73fe0ac6-ff6b-46cc-a609-c574be026e8f
├─sda5 ext4 _dde_data 798fce56-fc82-4f59-bcaa-d2ed5c48da8d 42.1G 54% /data
├─sda6 ext4 Backup 267dc7a8-1659-4ccc-b7dc-5f2cd80f4e4e 3.7G 57% /recovery
└─sda7 swap SWAP 7a5632dc-bc7b-410e-9a50-07140f20cd13 [SWAP]
nvme0n1
└─nvme0n1p1 ext4 polaru01 762a5700-8cf1-454a-b385-536b9f63c25d 413.4G 54% /u01
nvme1n1 xfs u02 8ddf19c4-fe71-4428-b2aa-e45acf08050c
nvme2n1 xfs u03 2b8625b4-c67d-4f1e-bed6-88814adfd6cc
nvme3n1 ext4 u01 cda85750-c4f7-402e-a874-79cb5244d4e1

LVM 创建、扩容

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
sudo vgcreate vg1 /dev/nvme0n1 /dev/nvme1n1 //两块物理磁盘上创建vg1
如果报错:
Can't open /dev/nvme1n1 exclusively. Mounted filesystem?
Can't open /dev/nvme0n1 exclusively. Mounted filesystem?
是说/dev/nvme0n1已经mounted了,需要先umount

vgdisplay
sudo lvcreate -L 5T -n u03 vg1 //在虚拟volume-group vg1上创建一个5T大小的分区or: sudo lvcreate -l 100%free -n u03 vg1
sudo mkfs.ext4 /dev/vg1/u03
sudo mkdir /lvm
sudo fdisk -l
sudo umount /lvm
sudo lvresize -L 5.8T /dev/vg1/u03 //lv 扩容,但此时还未生效;缩容的话风险较大,且需要先 umount
sudo e2fsck -f /dev/vg1/u03
sudo resize2fs /dev/vg1/u03 //触发生效
sudo mount /dev/vg1/u03 /lvm
cd /lvm/
lvdisplay
sudo vgdisplay vg1
lsblk -l
lsblk
sudo vgextend vg1 /dev/nvme3n1 //vg 扩容, 增加一块磁盘到vg1
ls /u01
sudo vgdisplay
sudo fdisk -l
sudo pvdisplay
sudo lvcreate -L 1T -n lv2 vg1 //从vg1中再分配一块1T大小的磁盘
sudo lvdisplay
sudo mkfs.ext4 /dev/vg1/lv2
mkdir /lv2
ls /
sudo mkdir /lv2
sudo mount /dev/vg1/lv2 /lv2
df -lh

//手工创建lvm
1281 18/05/22 11:04:22 ls -l /dev/|grep -v ^l|awk '{print $NF}'|grep -E "^nvme[7-9]{1,2}n1$|^df[a-z]$|^os[a-z]$"
1282 18/05/22 11:05:06 vgcreate -s 32 vgbig /dev/nvme7n1 /dev/nvme8n1 /dev/nvme9n1
1283 18/05/22 11:05:50 vgcreate -s 32 vgbig /dev/nvme7n1 /dev/nvme8n1 /dev/nvme9n1
1287 18/05/22 11:07:59 lvcreate -A y -I 128K -l 100%FREE -i 3 -n big vgbig
1288 18/05/22 11:08:02 df -h
1289 18/05/22 11:08:21 lvdisplay
1290 18/05/22 11:08:34 df -lh
1291 18/05/22 11:08:42 df -h
1292 18/05/22 11:09:05 mkfs.ext4 /dev/vgbig/big -m 0 -O extent,uninit_bg -E lazy_itable_init=1 -q -L big -J size=4000
1298 18/05/22 11:10:28 mkdir -p /big
1301 18/05/22 11:12:11 mount /dev/vgbig/big /big

创建LVM

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function create_polarx_lvm_V62(){
vgremove vgpolarx

#sed -i "97 a\ types = ['nvme', 252]" /etc/lvm/lvm.conf
parted -s /dev/nvme0n1 rm 1
parted -s /dev/nvme1n1 rm 1
parted -s /dev/nvme2n1 rm 1
parted -s /dev/nvme3n1 rm 1
dd if=/dev/zero of=/dev/nvme0n1 count=10000 bs=512
dd if=/dev/zero of=/dev/nvme1n1 count=10000 bs=512
dd if=/dev/zero of=/dev/nvme2n1 count=10000 bs=512
dd if=/dev/zero of=/dev/nvme3n1 count=10000 bs=512

#lvmdiskscan
vgcreate -s 32 vgpolarx /dev/nvme0n1 /dev/nvme1n1 /dev/nvme2n1 /dev/nvme3n1
lvcreate -A y -I 16K -l 100%FREE -i 4 -n polarx vgpolarx
mkfs.ext4 /dev/vgpolarx/polarx -m 0 -O extent,uninit_bg -E lazy_itable_init=1 -q -L polarx -J size=4000
sed -i "/polarx/d" /etc/fstab
mkdir -p /polarx
echo "LABEL=polarx /polarx ext4 defaults,noatime,data=writeback,nodiratime,nodelalloc,barrier=0 0 0" >> /etc/fstab
mount -a
}

create_polarx_lvm_V62

-I 64K 值条带粒度,默认64K,mysql pagesize 16K,所以最好16K

默认创建的是 linear,一次只用一块盘,不能累加多快盘的iops能力:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#lvcreate -h
lvcreate - Create a logical volume

Create a linear LV.
lvcreate -L|--size Size[m|UNIT] VG
[ -l|--extents Number[PERCENT] ]
[ --type linear ]
[ COMMON_OPTIONS ]
[ PV ... ]

Create a striped LV (infers --type striped).
lvcreate -i|--stripes Number -L|--size Size[m|UNIT] VG
[ -l|--extents Number[PERCENT] ]
[ -I|--stripesize Size[k|UNIT] ]
[ COMMON_OPTIONS ]
[ PV ... ]

Create a raid1 or mirror LV (infers --type raid1|mirror).
lvcreate -m|--mirrors Number -L|--size Size[m|UNIT] VG
[ -l|--extents Number[PERCENT] ]
[ -R|--regionsize Size[m|UNIT] ]
[ --mirrorlog core|disk ]
[ --minrecoveryrate Size[k|UNIT] ]
[ --maxrecoveryrate Size[k|UNIT

remount

正常使用中的文件系统是不能被umount的,如果需要修改mount参数的话可以考虑用mount 的 -o remount 参数

1
2
3
4
5
6
[root@ky3 ~]# mount -o lazytime,remount /polarx/  //增加lazytime参数
[root@ky3 ~]# mount -t ext4
/dev/mapper/vgpolarx-polarx on /polarx type ext4 (rw,noatime,nodiratime,lazytime,nodelalloc,nobarrier,stripe=128,data=writeback)
[root@ky3 ~]# mount -o rw,remount /polarx/ //去掉刚加的lazytime 参数
[root@ky3 ~]# mount -t ext4
/dev/mapper/vgpolarx-polarx on /polarx type ext4 (rw,noatime,nodiratime,nodelalloc,nobarrier,stripe=128,data=writeback)

remount 时要特别小心,会大量回收 slab 等导致sys CPU 100% 打挂整机,remount会导致slab回收等,请谨慎执行

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
[2023-10-26 15:04:49][kernel][info]EXT4-fs (dm-0): re-mounted. Opts: lazytime,data=writeback,nodelalloc,barrier=0,nolazytime
[2023-10-26 15:04:49][kernel][info]EXT4-fs (dm-1): re-mounted. Opts: lazytime,data=writeback,nodelalloc,barrier=0,nolazytime
[2023-10-26 15:05:16][kernel][warning]Modules linked in: ip_tables tcp_diag inet_diag venice_reduce_print(OE) bianque_driver(OE) 8021q garp mrp bridge stp llc ip6_tables tcp_rds_rt_j(OE) tcp_rt_base(OE) slb_vctk(OE) slb_vtoa(OE) hookers slb_ctk_proxy(OE) slb_ctk_session(OE) slb_ctk_debugfs(OE) loop nf_conntrack fuse btrfs zlib_deflate raid6_pq xor vfat msdos fat xfs libcrc32c ext3 jbd dm_mod khotfix_D902467(OE) kpatch_D537536(OE) kpatch_D793896(OE) kpatch_D608634(OE) kpatch_D629788(OE) kpatch_D820113(OE) kpatch_D723518(OE) kpatch_D616841(OE) kpatch_D602147(OE) kpatch_D523456(OE) kpatch_D559221(OE) ipflt(OE) kpatch_D656712(OE) kpatch_D753272(OE) kpatch_D813404(OE) i40e kpatch_D543129(OE) kpatch_D645707(OE) kpatch(OE) rpcrdma(OE) xprtrdma(OE) ib_isert(OE) ib_iser(OE) ib_srpt(OE) ib_srp(OE) ib_ipoib(OE) ib_addr(OE) ib_sa(OE)
[2023-10-26 15:05:16][kernel][warning]ib_mad(OE) bonding iTCO_wdt iTCO_vendor_support intel_powerclamp coretemp intel_rapl kvm_intel kvm crc32_pclmul ghash_clmulni_intel aesni_intel lrw gf128mul glue_helper ablk_helper cryptd ipmi_devintf pcspkr sg lpc_ich mfd_core i2c_i801 shpchp wmi ipmi_si ipmi_msghandler acpi_pad acpi_power_meter binfmt_misc aocblk(OE) mlx5_ib(OE) ext4 mbcache jbd2 crc32c_intel ast(OE) syscopyarea mlx5_core(OE) sysfillrect sysimgblt ptp i2c_algo_bit pps_core drm_kms_helper aocnvm(OE) vxlan ttm aocmgr(OE) ip6_udp_tunnel udp_tunnel drm i2c_core sd_mod crc_t10dif crct10dif_generic crct10dif_pclmul crct10dif_common ahci libahci libata rdma_ucm(OE) rdma_cm(OE) iw_cm(OE) ib_umad(OE) ib_ucm(OE) ib_uverbs(OE) ib_cm(OE) ib_core(OE) mlx_compat(OE) [last unloaded: ip_tables]
[2023-10-26 15:05:16][kernel][warning]CPU: 85 PID: 105195 Comm: mount Tainted: G W OE K------------ 3.10.0-327.ali2017.alios7.x86_64 #1
[2023-10-26 15:05:16][kernel][warning]Hardware name: Foxconn AliServer-Thor-04-12U-v2/Thunder2.0 2U, BIOS 1.0.PL.FC.P.026.05 03/04/2020
[2023-10-26 15:05:16][kernel][warning]task: ffff8898016c5b70 ti: ffff88b2b5094000 task.ti: ffff88b2b5094000
[2023-10-26 15:05:16][kernel][warning]RIP: 0010:[<ffffffff81656502>] [<ffffffff81656502>] _raw_spin_lock+0x12/0x50
[2023-10-26 15:05:16][kernel][warning]RSP: 0018:ffff88b2b5097d98 EFLAGS: 00000202
[2023-10-26 15:05:16][kernel][warning]RAX: 0000000000160016 RBX: ffffffff81657696 RCX: 007d44c33c3e3c3e
[2023-10-26 15:05:16][kernel][warning]RDX: 007d44c23c3e3c3e RSI: 00000000007d44c3 RDI: ffff88b0247b67d8
[2023-10-26 15:05:16][kernel][warning]RBP: ffff88b2b5097d98 R08: 0000000000000000 R09: 0000000000000007
[2023-10-26 15:05:16][kernel][warning]R10: ffff88b0247a7bc0 R11: 0000000000000000 R12: ffffffff81657696
[2023-10-26 15:05:16][kernel][warning]R13: ffff88b2b5097d80 R14: ffffffff81657696 R15: ffff88b2b5097d78
[2023-10-26 15:05:16][kernel][warning]FS: 00007ff7d3f4f880(0000) GS:ffff88bd6a340000(0000) knlGS:0000000000000000
[2023-10-26 15:05:16][kernel][warning]CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[2023-10-26 15:05:16][kernel][warning]CR2: 00007fff5286b000 CR3: 0000008177750000 CR4: 00000000003607e0
[2023-10-26 15:05:16][kernel][warning]DR0: 0000000000000000 DR1: 0000000000000000 DR2: 0000000000000000
[2023-10-26 15:05:16][kernel][warning]DR3: 0000000000000000 DR6: 00000000fffe0ff0 DR7: 0000000000000400
[2023-10-26 15:05:16][kernel][warning]Call Trace:
[2023-10-26 15:05:16][kernel][warning][<ffffffff812085df>] shrink_dentry_list+0x4f/0x480
[2023-10-26 15:05:16][kernel][warning][<ffffffff81208a9c>] shrink_dcache_sb+0x8c/0xd0
[2023-10-26 15:05:16][kernel][warning][<ffffffff811f3a7c>] do_remount_sb+0x4c/0x1a0
[2023-10-26 15:05:16][kernel][warning][<ffffffff81212519>] do_mount+0x6a9/0xa40
[2023-10-26 15:05:16][kernel][warning][<ffffffff8117830e>] ? __get_free_pages+0xe/0x50
[2023-10-26 15:05:16][kernel][warning][<ffffffff81212946>] SyS_mount+0x96/0xf0
[2023-10-26 15:05:16][kernel][warning][<ffffffff816600fd>] system_call_fastpath+0x16/0x1b
[2023-10-26 15:05:16][kernel][warning]Code: f6 47 02 01 74 e5 0f 1f 00 e8 a6 17 ff ff eb db 66 0f 1f 84 00 00 00 00 00 0f 1f 44 00 00 55 48 89 e5 b8 00 00 02 00 f0 0f c1 07 <89> c2 c1 ea 10 66 39 c2 75 02 5d c3 83 e2 fe 0f b7 f2 b8 00 80
[2023-10-26 15:05:44][kernel][emerg]BUG: soft lockup - CPU#85 stuck for 23s! [mount:105195]

复杂版创建LVM

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
function disk_part(){
set -e
if [ $# -le 1 ]
then
echo "disk_part argument error"
exit -1
fi
action=$1
disk_device_list=(`echo $*`)

echo $disk_device_list
unset disk_device_list[0]

echo $action
echo ${disk_device_list[*]}
len=`echo ${#disk_device_list[@]}`
echo "start remove origin partition "
for dev in ${disk_device_list[@]}
do
#echo ${dev}
`parted -s ${dev} rm 1` || true
dd if=/dev/zero of=${dev} count=100000 bs=512
done
#替换98行,插入的话r改成a
sed -i "98 r\ types = ['aliflash' , 252 , 'nvme' ,252 , 'venice', 252 , 'aocblk', 252]" /etc/lvm/lvm.conf
sed -i "/flash/d" /etc/fstab

if [ x${1} == x"split" ]
then
echo "split disk "
#lvmdiskscan
echo ${disk_device_list}
#vgcreate -s 32 vgpolarx /dev/nvme0n1 /dev/nvme2n1
vgcreate -s 32 vgpolarx ${disk_device_list[*]}
#stripesize 16K 和MySQL pagesize适配
#lvcreate -A y -I 16K -l 100%FREE -i 2 -n polarx vgpolarx
lvcreate -A y -I 16K -l 100%FREE -i ${#disk_device_list[@]} -n polarx vgpolarx
#lvcreate -A y -I 128K -l 75%VG -i ${len} -n volume1 vgpolarx
#lvcreate -A y -I 128K -l 100%FREE -i ${len} -n volume2 vgpolarx
mkfs.ext4 /dev/vgpolarx/polarx -m 0 -O extent,uninit_bg -E lazy_itable_init=1 -q -L polarx -J size=4000
sed -i "/polarx/d" /etc/fstab
mkdir -p /polarx
opt="defaults,noatime,data=writeback,nodiratime,nodelalloc,barrier=0"
echo "LABEL=polarx /polarx ext4 ${opt} 0 0" >> /etc/fstab
mount -a
else
echo "unkonw action "
fi
}

function format_nvme_mysql(){

if [ `df |grep flash|wc -l` -eq $1 ]
then
echo "check success"
echo "start umount partition "
parttion_list=`df |grep flash|awk -F ' ' '{print $1}'`
for partition in ${parttion_list[@]}
do
echo $partition
umount $partition
done
else
echo "check host fail"
exit -1
fi

disk_device_list=(`ls -l /dev/|grep -v ^l|awk '{print $NF}'|grep -E "^nvme[0-9]{1,2}n1$|^df[a-z]$|^os[a-z]$"`)
full_disk_device_list=()
for i in ${!disk_device_list[@]}
do
echo ${i}
full_disk_device_list[${i}]=/dev/${disk_device_list[${i}]}
done
echo ${full_disk_device_list[@]}
disk_part split ${full_disk_device_list[@]}
}

if [ ! -d "/polarx" ]; then
umount /dev/vgpolarx/polarx
vgremove -f vgpolarx
dmsetup --force --retry --deferred remove vgpolarx-polarx
format_nvme_mysql $1
else
echo "the lvm exists."
fi

LVM性能还没有做到多盘并行,也就是性能和单盘差不多,盘数多读写性能也一样

查看 lvcreate 使用的参数:

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
#lvs -o +lv_full_name,devices,stripe_size,stripes
LV VG Attr LSize Pool Origin Data% Meta% Move Log Cpy%Sync Convert LV Devices Stripe #Str
drds vg1 -wi-ao---- 5.37t vg1/drds /dev/nvme0n1p1(0) 0 1
drds vg1 -wi-ao---- 5.37t vg1/drds /dev/nvme2n1p1(0) 0 1

# lvs -v --segments
LV VG Attr Start SSize #Str Type Stripe Chunk
polarx vgpolarx -wi-ao---- 0 11.64t 4 striped 128.00k 0

# lvdisplay -m
--- Logical volume ---
LV Path /dev/vgpolarx/polarx
LV Name polarx
VG Name vgpolarx
LV UUID Wszlwf-SCjv-Txkw-9B1t-p82Z-C0Zl-oJopor
LV Write Access read/write
LV Creation host, time ky4, 2022-08-18 15:53:29 +0800
LV Status available
# open 1
LV Size 11.64 TiB
Current LE 381544
Segments 1
Allocation inherit
Read ahead sectors auto
- currently set to 2048
Block device 254:0

--- Segments ---
Logical extents 0 to 381543:
Type striped
Stripes 4
Stripe size 128.00 KiB
Stripe 0:
Physical volume /dev/nvme1n1
Physical extents 0 to 95385
Stripe 1:
Physical volume /dev/nvme3n1
Physical extents 0 to 95385
Stripe 2:
Physical volume /dev/nvme2n1
Physical extents 0 to 95385
Stripe 3:
Physical volume /dev/nvme0n1
Physical extents 0 to 95385

==要特别注意 stripes 表示多快盘一起用,iops能力累加,但是默认 stripes 是1,也就是只用1块盘,也就是linear==

安装LVM

1
sudo yum install lvm2 -y

dmsetup查看LVM

管理工具dmsetup是 Device mapper in the kernel 中的一个

1
2
dmsetup ls
dmsetup info /dev/dm-0

reboot 失败

在麒麟下OS reboot的时候可能因为mount: /polarx: 找不到 LABEL=/polarx. 导致OS无法启动,可以进入紧急模式,然后注释掉 /etc/fstab 中的polarx 行,再reboot

这是因为LVM的label、uuid丢失了,导致挂载失败。

查看设备的label

1
sudo lsblk -o name,mountpoint,label,size,uuid  or lsblk -f

修复:

紧急模式下修改 /etc/fstab 去掉有问题的挂载; 修改标签

1
2
3
4
5
6
7
8
#blkid   //查询uuid、label
/dev/mapper/klas-root: UUID="c4793d67-867e-4f14-be87-f6713aa7fa36" BLOCK_SIZE="512" TYPE="xfs"
/dev/sda2: UUID="8DCEc5-b4P7-fW0y-mYwR-5YTH-Yf81-rH1CO8" TYPE="LVM2_member" PARTUUID="4ffd9bfa-02"
/dev/nvme0n1: UUID="nJAHxP-d15V-Fvmq-rxa3-GKJg-TCqe-gD1A2Z" TYPE="LVM2_member"
/dev/sda1: UUID="29f59517-91c6-4b3c-bd22-0a47c800d7f4" BLOCK_SIZE="512" TYPE="xfs" PARTUUID="4ffd9bfa-01"
/dev/mapper/vgpolarx-polarx: LABEL="polarx" UUID="025a3ac5-d38a-42f1-80b6-563a55cba12a" BLOCK_SIZE="4096" TYPE="ext4"

e2label /dev/mapper/vgpolarx-polarx polarx

比如,下图右边的是启动失败的

image-20211228185144635

软RAID

mdadm(multiple devices admin)是一个非常有用的管理软raid的工具,可以用它来创建、管理、监控raid设备,当用mdadm来创建磁盘阵列时,可以使用整块独立的磁盘(如/dev/sdb,/dev/sdc),也可以使用特定的分区(/dev/sdb1,/dev/sdc1)

mdadm使用手册

mdadm –create device –level=Y –raid-devices=Z devices
-C | –create /dev/mdn
-l | –level 0|1|4|5
-n | –raid-devices device [..]
-x | –spare-devices device [..]

创建 -l 0表示raid0, -l 10表示raid10

1
2
3
4
5
6
7
8
9
10
mdadm -C /dev/md0 -a yes -l 0 -n2 /dev/nvme{6,7}n1  //raid0
mdadm -D /dev/md0
mkfs.ext4 /dev/md0
mkdir /md0
mount /dev/md0 /md0

//条带
mdadm --create --verbose /dev/md0 --level=linear --raid-devices=2 /dev/sdb /dev/sdc
检查
mdadm -E /dev/nvme[0-5]n1

删除

1
2
umount /md0 
mdadm -S /dev/md0

监控raid

1
2
3
4
5
6
7
8
9
#cat /proc/mdstat
Personalities : [raid0] [raid6] [raid5] [raid4]
md6 : active raid6 nvme3n1[3] nvme2n1[2] nvme1n1[1] nvme0n1[0]
7501211648 blocks super 1.2 level 6, 512k chunk, algorithm 2 [4/4] [UUUU]
[=>...................] resync = 7.4% (280712064/3750605824) finish=388.4min speed=148887K/sec
bitmap: 28/28 pages [112KB], 65536KB chunk //raid6一直在异步刷数据

md0 : active raid0 nvme7n1[3] nvme6n1[2] nvme4n1[0] nvme5n1[1]
15002423296 blocks super 1.2 512k chunks

控制刷盘速度

1
2
3
#sysctl -a |grep raid
dev.raid.speed_limit_max = 0
dev.raid.speed_limit_min = 0

nvme-cli

1
2
3
4
nvme id-ns /dev/nvme1n1 -H
for i in `seq 0 1 2`; do nvme format --lbaf=3 /dev/nvme${i}n1 ; done //格式化,选择不同的扇区大小,默认512,可选4K

fuser -km /data/

raid硬件卡

raid卡外观

image.png

mount 参数对性能的影响

推荐mount参数:defaults,noatime,data=writeback,nodiratime,nodelalloc,barrier=0 这些和 default 0 0 的参数差别不大,但是如果加了lazytime 会在某些场景下性能很差

比如在mysql filesort 场景下就可能触发 find_inode_nowait 热点,MySQL filesort 过程中,对文件的操作时序是 create,open,unlink,write,read,close; 而文件系统的 lazytime 选项,在发现 inode 进行修改了之后,会对同一个 inode table 中的 inode 进行修改,导致 file_inode_nowait 函数中,spin lock 的热点。

所以mount时注意不要有 lazytime

img

img

如果一个SQL 要创建大量临时表,而 /tmp/ 挂在参数有lazytime的话也会导致同样的问题,如图堆栈:

img

对应的内核代码:

image-20231027170529214

image-20231027170707041

另外一个应用,也经常因为find_inode_nowait 热点把CPU 爆掉:

img

image-20231027165103176

lazytime 的问题可以通过代码复现:

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
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include "pthread.h"
#include "stdio.h"
#include "stdlib.h"
#include <atomic>
#include <string>
typedef unsigned long long ulonglong;
static const char *FILE_PREFIX = "stress";
static const char *FILE_DIR = "/flash4/tmp/";
static std::atomic<ulonglong> f_num(0);
static constexpr size_t THREAD_NUM = 128;
static constexpr ulonglong LOOP = 1000000000000;

void file_op(const char *file_name) {
int f;
char content[1024];
content[0] = 'a';
content[500] = 'b';
content[1023] = 'c';
f = open(file_name, O_RDWR | O_CREAT);
unlink(file_name);
for (ulonglong i = 0; i < 1024 * 16; i++) {
write(f, content, 1024);
}
close(f);
}
void *handle(void *data) {
char file[1024];
ulonglong current_id;
for (ulonglong i = 0; i < LOOP; i++) {
current_id = f_num++;
snprintf(file, 1024, "%s%s_%d.txt", FILE_DIR, FILE_PREFIX, current_id);
file_op(file);
}
}
int main(int argc, char** args) {
for (std::size_t i = 0; i < THREAD_NUM; i++) {
pthread_t tid;
int ret = pthread_create(&tid, NULL, handle, NULL);
}
}

主动;工具、生产效率;面向故障、事件

LVM 异常修复

文件系统损坏,是导致系统启动失败比较常见的原因。文件系统损坏,比较常见的原因是分区丢失和文件系统需要手工修复。

1、分区表丢失,只要重新创建分区表即可。因为分区表信息只涉及变更磁盘上第一个扇区指定位置的内容。所以只要确认有分区情况,在分区表丢失的情况下,重做分区是不会损坏磁盘上的数据的。但是分区起始位置和尺寸需要正确 。起始位置确定后,使用fdisk重新分区即可。所以,问题的关键是如何确定分区的开始位置。

确定分区起始位置:

MBR(Master Boot Record)是指磁盘第一块扇区上的一种数据结构,512字节,磁盘分区数据是MBR的一部分,可以使用通过dd if=/dev/vdc bs=512 count=1 | hexdump -C 以16进制列出扇区0的裸数据:

image-20240118103305675

可以看出磁盘的分区类型ID、分区起始扇区和分区包含扇区数量,通过这几个数值可以确定分区位置。后面LVM可以通过LABELONE计算出起始位置。

参考资料

https://www.tecmint.com/manage-and-create-lvm-parition-using-vgcreate-lvcreate-and-lvextend/

pvcreate error : Can’t open /dev/sdx exclusively. Mounted filesystem?

软RAID配置方法参考这里

Linux LVS 配置

NAT

  • Enable IP forwarding. This can be done by adding the following to

    1
    net.ipv4.ip_forward = 1 

then

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
ipvsadm -A -t 172.26.137.117:9376 -s rr //创建了一个rr lvs
// -m 表示nat模式,不加的话默认是route模式
ipvsadm -a -t 172.26.137.117:9376 -r 172.20.22.195:9376 -m //往lvs中添加一个RS
ipvsadm -ln
ipvsadm -a -t 172.26.137.117:9376 -r 172.20.22.196:9376 -m //往lvs中添加另外一个RS
ipvsadm -ln

//删除realserver
ipvsadm -d -t 100.81.131.221:18507 -r 100.81.131.237:8507 -m

//连接状态查看
#ipvsadm -L -n --connection
IPVS connection entries
pro expire state source virtual destination
TCP 15:00 ESTABLISHED 127.0.0.1:40630 127.0.0.1:3001 127.0.0.1:3306
TCP 14:59 ESTABLISHED 127.0.0.1:40596 127.0.0.1:3001 127.0.0.1:3306
TCP 14:59 ESTABLISHED 127.0.0.1:40614 127.0.0.1:3001 127.0.0.1:3307
TCP 15:00 ESTABLISHED 127.0.0.1:40598 127.0.0.1:3001 127.0.0.1:3307

#流量统计
ipvsadm -L -n --stats -t 192.168.1.10:28080 //-t service-address
Prot LocalAddress:Port Conns InPkts OutPkts InBytes OutBytes
-> RemoteAddress:Port
TCP 192.168.1.10:28080 39835 1030M 863494K 150G 203G
-> 172.20.62.78:3306 774 46173852 38899725 6575M 9250M
-> 172.20.78.79:3306 781 45106566 37997254 6421M 9038M
-> 172.20.81.80:3306 783 45531236 38387112 6479M 9128M

#清空统计数据
#ipvsadm --zero
#列出所有连接信息
#/sbin/ipvsadm -L -n --connection

#ipvsadm -L -n
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
-> RemoteAddress:Port Forward Weight ActiveConn InActConn
TCP 11.197.140.20:18089 wlc
-> 11.197.140.20:28089 Masq 1 0 0
-> 11.197.141.110:28089 Masq 1 0 0

ipvsadm常用参数

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
添加虚拟服务器
语法:ipvsadm -A [-t|u|f] [vip_addr:port] [-s:指定算法]
-A:添加
-t:TCP协议
-u:UDP协议
-f:防火墙标记
-D:删除虚拟服务器记录
-E:修改虚拟服务器记录
-C:清空所有记录
-L:查看
添加后端RealServer
语法:ipvsadm -a [-t|u|f] [vip_addr:port] [-r ip_addr] [-g|i|m] [-w 指定权重]
-a:添加
-t:TCP协议
-u:UDP协议
-f:防火墙标记
-r:指定后端realserver的IP
-g:DR模式
-i:TUN模式
-m:NAT模式
-w:指定权重
-d:删除realserver记录
-e:修改realserver记录
-l:查看
通用:
ipvsadm -ln:查看规则
service ipvsadm save:保存规则

查看连接对应的RS ip和端口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# ipvsadm -Lcn |grep "10.68.128.202:1406"
TCP 15:01 ESTABLISHED 10.68.128.202:1406 10.68.128.202:3306 172.20.188.72:3306

# ipvsadm -Lcn | head -10
IPVS connection entries
pro expire state source virtual destination
TCP 15:01 ESTABLISHED 10.68.128.202:1390 10.68.128.202:3306 172.20.185.132:3306
TCP 15:01 ESTABLISHED 10.68.128.202:1222 10.68.128.202:3306 172.20.165.202:3306
TCP 15:01 ESTABLISHED 10.68.128.202:1252 10.68.128.202:3306 172.20.222.65:3306
TCP 15:01 ESTABLISHED 10.68.128.202:1328 10.68.128.202:3306 172.20.149.68:3306

ipvsadm -Lcn
IPVS connection entries
pro expire state source virtual destination
TCP 00:57 NONE 110.184.96.173:0 122.225.32.142:80 122.225.32.136:80
TCP 01:57 FIN_WAIT 110.184.96.173:54568 122.225.32.142:80 122.225.32.136:80

当一个client访问vip的时候,ipvs或记录一条状态为NONE的信息,expire初始值是persistence_timeout的值,然后根据时钟主键变小,在以下记录存在期间,同一client ip连接上来,都会被分配到同一个后端。

FIN_WAIT的值就是tcp tcpfin udp的超时时间,当NONE的值为0时,如果FIN_WAIT还存在,那么NONE的值会从新变成60秒,再减少,直到FIN_WAIT消失以后,NONE才会消失,只要NONE存在,同一client的访问,都会分配到统一real server。

通过keepalived来检测RealServer的状态

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
# cat /etc/keepalived/keepalived.conf
global_defs {
notification_email {
}
router_id LVS_DEVEL
vrrp_skip_check_adv_addr
vrrp_strict
vrrp_garp_interval 0
vrrp_gna_interval 0
}
#添加虚拟服务器
#相当于 ipvsadm -A -t 172.26.137.117:9376 -s wrr
virtual_server 172.26.137.117 9376 {
delay_loop 3 #服务健康检查周期,单位是秒
lb_algo wrr #调度算法
lb_kind NAT #模式
# persistence_timeout 50 #会话保持时间,单位是秒
protocol TCP #TCP协议转发

#添加后端realserver
#相当于 ipvsadm -a -t 172.26.137.117:9376 -r 172.20.56.148:9376 -w 1
real_server 172.20.56.148 9376 {
weight 1
TCP_CHECK { # 通过TcpCheck判断RealServer的健康状态
connect_timeout 2 # 连接超时时间
nb_get_retry 3 # 重连次数
delay_before_retry 1 # 重连时间间隔
connect_port 9376 # 检测端口
}
}

real_server 172.20.248.147 9376 {
weight 1
HTTP_GET {
url {
path /
status_code 200
}
connect_timeout 3
nb_get_retry 3
delay_before_retry 3
}
}
}

修改keepalived配置后只需要执行reload即可生效

systemctl reload keepalived

timeout

LVS的持续时间有2个

  1. 把同一个cip发来请求到同一台RS的持久超时时间。(-p persistent)
  2. 一个链接创建后空闲时的超时时间,这个超时时间分为3种。
    • tcp的空闲超时时间。
    • lvs收到客户端tcp fin的超时时间
    • udp的超时时间

连接空闲超时时间的设置如下:

1
2
3
4
5
6
7
[root@poc117 ~]# ipvsadm -L --timeout
Timeout (tcp tcpfin udp): 900 120 300
[root@poc117 ~]# ipvsadm --set 1 2 1
[root@poc117 ~]# ipvsadm -L --timeout
Timeout (tcp tcpfin udp): 1 2 1

ipvsadm -Lcn //查看

persistence_timeout

用于保证同一ip client的所有连接在timeout时间以内都发往同一个RS,比如ftp 21port listen认证、20 port传输数据,那么希望同一个client的两个连接都在同一个RS上。

persistence_timeout 会导致负载不均衡,timeout时间越大负载不均衡越严重。大多场景下基本没什么意义

PCC用来实现把某个用户的所有访问在超时时间内定向到同一台REALSERVER,这种方式在实际中不常用

1
2
3
ipvsadm -A -t 192.168.0.1:0 -s wlc -p 600(单位是s)     //port为0表示所有端口
ipvsadm -a -t 192.168.0.1:0 -r 192.168.1.2 -w 4 -g
ipvsadm -a -t 192.168.0.1:0 -r 192.168.1.3 -w 2 -g

此时测试一下会发现通过HTTP访问VIP和通过SSH登录VIP的时候都被定向到了同一台REALSERVER上面了

lvs 管理

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
257  [2021-09-13 22:11:26] lscpu
258 [2021-09-13 22:11:34] dmidecode | grep Ser
259 [2021-09-13 22:11:53] dmidecode | grep FT
260 [2021-09-13 22:11:58] dmidecode | grep 2500
261 [2021-09-13 22:12:03] dmidecode
262 [2021-09-13 22:12:27] lscpu
263 [2021-09-13 22:12:37] ipvsadm -ln
264 [2021-09-13 22:12:59] ipvsadm -lnvt 166.100.128.234:3306 --in-vid 1560537
265 [2021-09-13 22:14:37] base_admin --help
266 [2021-09-13 22:14:44] base_admin --cpu-usage
267 [2021-09-13 22:14:56] ip link
268 [2021-09-13 22:16:04] base_admin --cpu-usage
269 [2021-09-13 22:16:28] cat /usr/local/etc/nf-var-config
270 [2021-09-13 22:16:43] base_admin --cpu-usage
271 [2021-09-13 22:17:35] ipvsadm -lnvt 166.100.128.234:3306 --in-vid 1560537
272 [2021-09-13 22:18:17] base_admin --cpu-usage
273 [2021-09-13 22:22:02] ls
274 [2021-09-13 22:22:06] ps -aux
275 [2021-09-13 22:22:17] tsar --help
276 [2021-09-13 22:22:24] ipvsadm -lnvt 166.100.128.234:3306 --in-vid 1560537
277 [2021-09-13 22:22:31] ipvsadm -lnvt 166.100.128.234:3306 --in-vid 1560537 --stat
278 [2021-09-13 22:22:33] ipvsadm -lnvt 166.100.128.234:3306 --in-vid 1560537 --stats
279 [2021-09-13 22:23:10] tsar --lvs -li1 -D|awk '{print $1," ",($6)*8.0}'
280 [2021-09-13 22:24:29] ipvsadm -lnvt 166.100.128.234:3306 --in-vid 1560537 --stats
281 [2021-09-13 22:25:26] tsar --lvs -li1 -D
282 [2021-09-13 22:25:46] ipvsadm -lnvt 166.100.128.234:3306 --in-vid 1560537
283 [2021-09-13 22:26:37] appctl -cas | grep conns
284 [2021-09-13 22:31:16] ipvsadm -ln
286 [2021-09-13 22:31:43] ipvsadm -lnvt 166.100.128.234:3306 --in-vid 1560537 --stats
292 [2021-09-13 22:38:16] rpm -qa | grep slb
293 [2021-09-13 22:42:30] appctl -cas | grep conns
294 [2021-09-13 22:43:03] base_admin --cpu-usage
295 [2021-09-13 22:45:42] tsar --lvs -li1 -D|awk '{print $1," ",($6)*8.0}'
296 [2021-09-13 22:57:20] base_admin --cpu-usage
297 [2021-09-13 22:58:16] tsar --lvs -li1 -D|awk '{print $1," ",($6)*8.0}'
298 [2021-09-13 22:59:38] ipvsadm -lnvt 166.100.128.234:3306 --in-vid 1560537 --stats
299 [2021-09-13 23:00:16] appctl -a | grep conn
300 [2021-09-13 23:00:24] base_admin --cpu-usage
301 [2021-09-13 23:00:50] appctl -cas | grep conns
302 [2021-09-13 23:01:15] base_admin --cpu-usage
303 [2021-09-13 23:01:21] ipvsadm -lnvt 166.100.128.234:3306 --in-vid 1560537 --stats
304 [2021-09-13 23:02:09] appctl -cas | grep conns
305 [2021-09-13 23:03:12] base_admin --cpu-usage
306 [2021-09-13 23:04:43] ipvsadm -lnvt 166.100.128.234:3306 --in-vid 1560537 --stats | head -3
307 [2021-09-13 23:05:38] base_admin --cpu-usage
308 [2021-09-13 23:06:10] tsar --lvs -li1 -D|awk '{print $1," ",($6)*8.0}'
309 [2021-09-13 23:06:39] base_admin --cpu-usage
310 [2021-09-13 23:15:59] appctl -a | grep conn_limit_enable
311 [2021-09-13 23:15:59] appctl -a | grep cps_limit_enable
312 [2021-09-13 23:15:59] appctl -a | grep inbps_limit_enable
313 [2021-09-13 23:15:59] appctl -a | grep outbps_limit_enable
314 [2021-09-13 23:17:13] appctl -w conn_limit_enable=0
315 [2021-09-13 23:17:13] appctl -w cps_limit_enable=0
316 [2021-09-13 23:17:13] appctl -w inbps_limit_enable=0
317 [2021-09-13 23:17:13] appctl -w outbps_limit_enable=0
318 [2021-09-13 23:17:43] appctl -cas | grep conn
319 [2021-09-13 23:17:44] appctl -cas | grep conns
320 [2021-09-13 23:19:30] last=0;while true;do pre=`ipvsadm -lnvt 166.100.128.234:3306 --in-vid 1560537 --stats| grep TCP|awk '{print $4}'`;let cut=pre-last;echo $cut;last=$pre;sleep 1;done
321 [2021-09-13 23:19:56] ipvsadm -lnvt 166.100.128.234:3306 --in-vid 1560537 --stats| grep TCP|awk '{print $4}'
322 [2021-09-13 23:20:01] last=0;while true;do pre=`ipvsadm -lnvt 166.100.128.234:3306 --in-vid 1560537 --stats| grep TCP|awk '{print $4}'`;let cut=pre-last;echo $cut;last=$pre;sleep 1;done
323 [2021-09-13 23:20:55] base_admin --cpu-usage
324 [2021-09-13 23:22:05] ipvsadm -lnvt 166.100.129.249:3306 --in-vid 1560537
325 [2021-09-13 23:22:05] ipvsadm -lnvt 166.100.128.219:3306 --in-vid 1560537
326 [2021-09-13 23:22:05] ipvsadm -lnvt 166.100.129.40:80 --in-vid 1560537
327 [2021-09-13 23:24:22] base_admin --cpu-usage
328 [2021-09-13 23:24:29] last=0;while true;do pre=`ipvsadm -lnvt 166.100.128.234:3306 --in-vid 1560537 --stats| grep TCP|awk '{print $4}'`;let cut=pre-last;echo $cut;last=$pre;sleep 1;done
329 [2021-09-13 23:24:50] ipvsadm -lnvt 166.100.129.249:3306 --in-vid 1560537
332 [2021-09-13 23:25:38] ipvsadm -lnvt 166.100.128.234:3306 --in-vid 1560537 —stats
333 [2021-09-13 23:25:57] ipvsadm -lnvt 166.100.129.249:3306 --in-vid 1560537 --stats
334 [2021-09-13 23:25:58] ipvsadm -lnvt 166.100.128.219:3306 --in-vid 1560537 --stats
335 [2021-09-13 23:25:58] ipvsadm -lnvt 166.100.129.40:80 --in-vid 1560537 --stats
336 [2021-09-13 23:26:45] last=0;while true;do pre=`ipvsadm -lnvt 166.100.129.40:80 --in-vid 1560537 --stats| grep TCP|awk '{print $4}'`;let cut=pre-last;echo $cut;last=$pre;sleep 1;done

LVS 工作原理

1.当客户端的请求到达负载均衡器的内核空间时,首先会到达PREROUTING链。

2.当内核发现请求数据包的目的地址是本机时,将数据包送往INPUT链。

3.LVS由用户空间的ipvsadm和内核空间的IPVS组成,ipvsadm用来定义规则,IPVS利用ipvsadm定义的规则工作,IPVS工作在INPUT链上,当数据包到达INPUT链时,首先会被IPVS检查,如果数据包里面的目的地址及端口没有在规则里面,那么这条数据包将被放行至用户空间。

4.如果数据包里面的目的地址及端口在规则里面,那么这条数据报文将被修改目的地址为事先定义好的后端服务器,并送往POSTROUTING链。

5.最后经由POSTROUTING链发往后端服务器。

image.png

netfilter 原理

Netfilter 由多个表(table)组成,每个表又由多个链(chain)组成(此处可以脑补二维数组的矩阵了),链是存放过滤规则的“容器”,里面可以存放一个或多个iptables命令设置的过滤规则。目前的表有4个:raw table, mangle table, nat table, filter table。Netfilter 默认的链有:INPUT, OUTPUT, FORWARD, PREROUTING, POSTROUTING,根据的不同功能需求,不同的表下面会有不同的链,链与表的关系可用下图直观表示:

image.png

OSPF + LVS

OSPF:Open Shortest Path First 开放最短路径优先,SPF算法也被称为Dijkstra算法,这是因为最短路径优先算法SPF是由荷兰计算机科学家狄克斯特拉于1959年提出的。

通过OSPF来替换keepalived,解决两个LVS节点的高可用,以及流量负载问题。keepalived两个节点只能是master-slave模式,而OSPF两个节点都是master,同时都有流量

img

这个架构与LVS+keepalived 最明显的区别在于,两台Director都是Master 状态,而不是Master-Backup,如此一来,两台Director 地位就平等了。剩下的问题,就是看如何在这两台Director 间实现负载均衡了。这里会涉及路由器领域的一个概念:等价多路径

ECMP(等价多路径)

ECMP(Equal-CostMultipathRouting)等价多路径,存在多条不同链路到达同一目的地址的网络环境中,如果使用传统的路由技术,发往该目的地址的数据包只能利用其中的一条链路,其它链路处于备份状态或无效状态,并且在动态路由环境下相互的切换需要一定时间,而等值多路径路由协议可以在该网络环境下同时使用多条链路,不仅增加了传输带宽,并且可以无时延无丢包地备份失效链路的数据传输。

ECMP最大的特点是实现了等值情况下,多路径负载均衡和链路备份的目的,在静态路由和OSPF中基本上都支持ECMP功能。

参考资料

http://www.ultramonkey.org/papers/lvs_tutorial/html/

https://www.jianshu.com/p/d4222ce9b032

https://www.cnblogs.com/zhangxingeng/p/10595058.html

lvs持久性工作原理和配置

10+倍性能提升全过程–优酷账号绑定淘宝账号的TPS从500到5400的优化历程

背景说明

2016年的双11在淘宝上买买买的时候,天猫和优酷土豆一起做了联合促销,在天猫双11当天购物满XXX元就赠送优酷会员,这个过程需要用户在优酷侧绑定淘宝账号(登录优酷、提供淘宝账号,优酷调用淘宝API实现两个账号绑定)和赠送会员并让会员权益生效(看收费影片、免广告等等)

这里涉及到优酷的两个部门:Passport(在上海,负责登录、绑定账号,下文中的优化过程主要是Passport部分);会员(在北京,负责赠送会员,保证权益生效)

在双11活动之前,Passport的绑定账号功能一直在运行,只是没有碰到过大促销带来的挑战


整个过程分为两大块:

  1. 整个系统级别,包括网络和依赖服务的性能等,多从整个系统视角分析问题;
  2. 但服务器内部的优化过程,将CPU从si/sy围赶us,然后在us从代码级别一举全歼。

系统级别都是最容易被忽视但是成效最明显的,代码层面都是很细致的力气活。

整个过程都是在对业务和架构不是非常了解的情况下做出的。

会员部分的架构改造

  • 接入中间件DRDS,让优酷的数据库支持拆分,分解MySQL压力
  • 接入中间件vipserver来支持负载均衡
  • 接入集团DRC来保障数据的高可用
  • 对业务进行改造支持Amazon的全链路压测

主要的压测过程

screenshot.png

上图是压测过程中主要的阶段中问题和改进,主要的问题和优化过程如下:

- docker bridge网络性能问题和网络中断si不均衡    (优化后:500->1000TPS)
- 短连接导致的local port不够                   (优化后:1000-3000TPS)
- 生产环境snat单核导致的网络延时增大             (优化后生产环境能达到测试环境的3000TPS)
- Spring MVC Path带来的过高的CPU消耗           (优化后:3000->4200TPS)
- 其他业务代码的优化(比如异常、agent等)          (优化后:4200->5400TPS)

优化过程中碰到的比如淘宝api调用次数限流等一些业务原因就不列出来了


概述

由于用户进来后先要登录并且绑定账号,实际压力先到Passport部分,在这个过程中最开始单机TPS只能到500,经过N轮优化后基本能达到5400 TPS,下面主要是阐述这个优化过程

Passport部分的压力

Passport 核心服务分两个:

  • Login 主要处理登录请求
  • userservice 处理登录后的业务逻辑,比如将优酷账号和淘宝账号绑定

为了更好地利用资源每台物理加上部署三个docker 容器,跑在不同的端口上(8081、8082、8083),通过bridge网络来互相通讯

Passport机器大致结构

screenshot.png

userservice服务网络相关的各种问题


太多SocketConnect异常(如上图)

在userservice机器上通过netstat也能看到大量的SYN_SENT状态,如下图:
image.png

因为docker bridge通过nat来实现,尝试去掉docker,让tomcat直接跑在物理机上

这时SocketConnect异常不再出现
image.png

从新梳理一下网络流程

docker(bridge)—-短连接—>访问淘宝API(淘宝open api只能短连接访问),性能差,cpu都花在si上;

如果 docker(bridge)—-长连接到宿主机的某个代理上(比如haproxy)—–短连接—>访问淘宝API, 性能就能好一点。问题可能是短连接放大了Docker bridge网络的性能损耗

当时看到的cpu si非常高,截图如下:

image.png

去掉Docker后,性能有所提升,继续通过perf top看到内核态寻找可用的Local Port消耗了比较多的CPU,gif动态截图如下(可以点击看高清大图):

perf-top-netLocalPort-issue.gif

注意图中ipv6_rcv_saddr_equal和inet_csk_get_port 总共占了30%的CPU (系统态的CPU使用率高意味着共享资源有竞争或者I/O设备之间有大量的交互。)

一般来说一台机器默认配置的可用 Local Port 3万多个,如果是短连接的话,一个连接释放后默认需要60秒回收,30000/60 =500 这是大概的理论TPS值【这里只考虑连同一个server IP:port 的时候】

这500的tps算是一个老中医的经验。不过有些系统调整过Local Port取值范围,比如从1024到65534,那么这个tps上限就是1000附近。

同时观察这个时候CPU的主要花在sy上,最理想肯定是希望CPU主要用在us上,截图如下:
image.png

规则:性能优化要先把CPU从SI、SY上的消耗赶到US上去(通过架构、系统配置);然后提升 US CPU的效率(代码级别的优化)

sy占用了30-50%的CPU,这太不科学了,同时通过 netstat 分析连接状态,确实看到很多TIME_WAIT:
localportissue-time-wait.png

cpu要花在us上,这部分才是我们代码吃掉的

于是让PE修改了tcp相关参数:降低 tcp_max_tw_buckets和开启tcp_tw_reuse,这个时候TPS能从1000提升到3000

到这里总结下:因为短链接导致端口不够,进而使得内核在循环搜索可用端口的内核态

鼓掌,赶紧休息,迎接双11啊

image.png

测试环境优化到3000 TPS后上线继续压测

居然性能又回到了500,太沮丧了,其实最开始账号绑定慢,Passport这边就怀疑taobao api是不是在大压力下不稳定,一般都是认为自己没问题,有问题的一定是对方。我不觉得这有什么问题,要是知道自己有什么问题不早就优化掉了,但是这里缺乏证据支撑,也就是如果你觉得自己没有问题或者问题在对方,一定要拿出证据来(有证据那么大家可以就证据来讨论,而不是互相苍白地推诿)。

这个时候Passport更加理直气壮啊,好不容易在测试环境优化到3000,怎么一调taobao api就掉到500呢,这么点压力你们就扛不住啊。 但是taobao api那边给出调用数据都是1ms以内就返回了(alimonitor监控图表–拿证据说话)。

看到alimonitor给出的api响应时间图表后,我开始怀疑从优酷的机器到淘宝的机器中间链路上有瓶颈,但是需要设计方案来证明这个问题在链路上,要不各个环节都会认为自己没有问题的,问题就会卡死。但是当时Passport的开发也只能拿到Login和Userservice这两组机器的权限,中间的负载均衡、交换机都没有权限接触到。

在没有证据的情况下,肯定机房、PE配合你排查的欲望基本是没有的(被坑过很多回啊,你说我的问题,结果几天配合排查下来发现还是你程序的问题,凭什么我要每次都陪你玩?),所以我要给出证明问题出现在网络链路上,然后拿着这个证据跟网络的同学一起排查。

讲到这里我禁不住要插一句,在出现问题的时候,都认为自己没有问题这是正常反应,毕竟程序是看不见的,好多意料之外逻辑考虑不周全也是常见的,出现问题按照自己的逻辑自查的时候还是没有跳出之前的逻辑所以发现不了问题。但是好的程序员在问题的前面会尝试用各种手段去证明问题在哪里,而不是复读机一样我的逻辑是这样的,不可能出问题的。即使目的是证明问题在对方,只要能给出明确的证据都是负责任的,拿着证据才能理直气壮地说自己没有问题和干净地甩锅。

在尝试过tcpdump抓包、ping等各种手段分析后,设计了场景证明问题在中间链路上。

设计如下三个场景证明问题在中间链路上:

  1. 压测的时候在userservice ping 依赖服务的机器;
  2. 将一台userservice机器从负载均衡上拿下来(没有压力),ping 依赖服务的机器;
  3. 从公网上非我们机房的机器 ping 依赖服务的机器;

这个时候奇怪的事情发现了,压力一上来场景1、2的两台机器ping淘宝的rt都从30ms上升到100-150ms,场景1 的rt上升可以理解,但是场景2的rt上升不应该,同时场景3中ping淘宝在压力测试的情况下rt一直很稳定(说明压力下淘宝的机器没有问题),到此确认问题在优酷到淘宝机房的链路上有瓶颈,而且问题在优酷机房出口扛不住这么大的压力。于是从上海Passport的团队找到北京Passport的PE团队,确认在优酷调用taobao api的出口上使用了snat,PE到snat机器上看到snat只能使用单核,而且对应的核早就100%的CPU了,因为之前一直没有这么大的压力所以这个问题一直存在只是没有被发现。

于是PE去掉snat,再压的话 TPS稳定在3000左右


到这里结束了吗? 从3000到5400TPS

优化到3000TPS的整个过程没有修改业务代码,只是通过修改系统配置、结构非常有效地把TPS提升了6倍,对于优化来说这个过程是最轻松,性价比也是非常高的。实际到这个时候也临近双11封网了,最终通过计算(机器数量*单机TPS)完全可以抗住双11的压力,所以最终双11运行的版本就是这样的。 但是有工匠精神的工程师是不会轻易放过这么好的优化场景和环境的(基线、机器、代码、工具都具备配套好了)

优化完环境问题后,3000TPS能把CPU US跑上去,于是再对业务代码进行优化也是可行的了

进一步挖掘代码中的优化空间

双11前的这段封网其实是比较无聊的,于是和Passport的开发同学们一起挖掘代码中的可以优化的部分。这个过程中使用到的主要工具是这三个:火焰图、perf、perf-map-java。相关链接:http://www.brendangregg.com/perf.html ; https://github.com/jrudolph/perf-map-agent

通过Perf发现的一个SpringMVC 的性能问题

这个问题具体参考我之前发表的优化文章。 主要是通过火焰图发现spring mapping path消耗了过多CPU的性能问题,CPU热点都在methodMapping相关部分,于是修改代码去掉spring中的methodMapping解析后性能提升了40%,TPS能从3000提升到4200.

著名的fillInStackTrace导致的性能问题

代码中的第二个问题是我们程序中很多异常(fillInStackTrace),实际业务上没有这么多错误,应该是一些不重要的异常,不会影响结果,但是异常频率很高,对这种我们可以找到触发的地方,catch住,然后不要抛出去(也就是别触发fillInStackTrace),打印一行error日志就行,这块也能省出10%的CPU,对应到TPS也有几百的提升。

screenshot.png

部分触发fillInStackTrace的场景和具体代码行(点击看高清大图):
screenshot.png

对应的火焰图(点击看高清大图):
screenshot.png

screenshot.png

解析useragent 代码部分的性能问题

整个useragent调用堆栈和cpu占用情况,做了个汇总(useragent不启用TPS能从4700提升到5400)
screenshot.png

实际火焰图中比较分散:
screenshot.png

最终通过对代码的优化勉勉强强将TPS从3000提升到了5400(太不容易了,改代码过程太辛苦,不如改配置来得快)

优化代码后压测tps可以跑到5400,截图:

image.png

最后再次总结整个压测过程的问题和优化历程

- docker bridge网络性能问题和网络中断si不均衡    (优化后:500->1000TPS)
- 短连接导致的local port不够                   (优化后:1000-3000TPS)
- 生产环境snat单核导致的网络延时增大             (优化后能达到测试环境的3000TPS)
- Spring MVC Path带来的过高的CPU消耗           (优化后:3000->4200TPS)
- 其他业务代码的优化(比如异常、agent等)         (优化后:4200->5400TPS)

image.png

0%