网络环路

网络环路

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

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

image.png

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

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

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

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

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

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

一个STP的Case

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

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

用tcpdump抓取stp包

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

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

交换机上看到的STP

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

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

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

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

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

image.png

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

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

在这种网络环路下后果

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

交换机的CPU状态:

image.png

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

image.png

其它网络环路

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

参考资料:

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

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

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

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

问题描述

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

image.png

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

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

image.png

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

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

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

server强行断开连接

image.png

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

疑难问题汇总

疑难问题汇总

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

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

证明问题

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

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


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

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

需找问题

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

screenshot

细化证明问题

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

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

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

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

先检查dns设置:

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

image.png

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

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

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

image.png

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

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

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

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

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

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

image.png

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

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

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

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

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

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

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

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

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

youku-mac-ip.gif

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

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

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

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

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

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

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

Linux环境变量问题汇总

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

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

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

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

image.png

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

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

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

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

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

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

信息更新:

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

直接原因:

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

解决问题的有效操作:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

image.png

image.png

image.png

image.png

拿如上证据求助内核开发

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

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

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



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

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

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

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

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

参考文章

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

Load很高,CPU使用率很低

Load很高,CPU使用率很低

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

先看CPU、Load情况

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

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

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

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

image.png

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

image.png

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

image.png

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

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

image.png

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

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

image.png

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

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

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

为什么会出现这种情况

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

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

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

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

image.png

关于这个Case的总结

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

关于Load高的总结

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

img

参考文章

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

部分机器网络不通

部分机器网络不通

问题

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

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

youku-mac-ip.gif

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

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

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

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

关于TCP连接的Keepalive和reset

关于TCP连接的Keepalive和reset

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

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

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

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

image.png

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

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

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

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

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

image.png

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

如何徒手撕Bug

如何徒手撕Bug

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

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

问题

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

image.png

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

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

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

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

跟踪具体拿到锁的进程

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

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

image.png

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

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

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

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

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

image.png

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

strace工作原理

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

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

image.png

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

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

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

两个术语:

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

tracer 跟踪 tracee 的过程:

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

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

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

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

strace 常用用法

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

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

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

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

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

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

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

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

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

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

我的分析

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

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

总结

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

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

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

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

参考资料

strace 是如何工作的(2016)

部分机器网络不通

方舟域名和服务

服务发布

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

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

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

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

服务发现

1.VIPServer-Client方式

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

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

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

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

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

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

简单原理:

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

  • QPS=并发/RT

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

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

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

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

L=λW

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

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

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

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

案例:

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

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

image-20211103175727900

RT

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

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

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

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

Thread CPU Time(简称CPU Time)

Thread Wait Time(简称Wait Time)

单线程QPS

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

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

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

最佳线程数

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

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

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

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

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

最大QPS

1.最大QPS公式推导

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

image

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

image

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

image

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

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

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

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

理解最佳线程数量

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

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

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

image-20220506121132920

IO

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

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

思路严谨

最难讲清楚

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

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

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

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

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

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

DK 效应

性能的本质

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

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

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

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

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

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

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

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

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

老中医经验不可缺少

量变到质变

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

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

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

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

案例

10+倍性能提升全过程

vxlan网络性能测试

vxlan网络性能测试


缘起

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

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

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

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

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

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

iperf3 下载和安装

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

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

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

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

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

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

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

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

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

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

两宿主机之间测试

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

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

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

两容器之间(跨宿主机)

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

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

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

PPS 压测

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

购买的 ECS PPS为 600 万

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

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

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

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

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

image-20231227174305030

对应的tsar pps 监控:

image-20231227174402374

client端的iperf 数据:

image-20231227174428881

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

image-20231227174725237

image-20231227174802828

image-20231227174829420

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

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

带宽压测

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

qperf 测试工具

  • sudo yum install qperf -y

两台宿主机之间

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

包的大小分别为1和128

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

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

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

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

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

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

包的大小分别为1和128

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

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

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

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

结论

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

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

网络方案性能

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

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

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

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

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

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

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

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

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

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

延迟数据汇总:

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

image.png

吞吐量数据汇总:

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

image.png

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

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

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

参考文章:

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

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

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

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

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

screenshot

再来看CPU消耗的火焰图:

screenshot

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

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

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

screenshot.png

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

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

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

doMatch 代码:

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

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

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

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

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

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

案例描述

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

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

问题的原因

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

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

什么是delay ack

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

来看一个例子:
image.png

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

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

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

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

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

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

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

什么是Nagle算法

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

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

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

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

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

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

这就是悲剧的核心原因。

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

这个案例的原始出处

案例核心奇怪的现象是:

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

原因就是:

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

99,900 bytes:

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

100,000 bytes:

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

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

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

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

回到前面的问题

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

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

这是修改后的C代码

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

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

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

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

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

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

跟速度相关的几个概念

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

image.png

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

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

image.png

TCP性能优化点

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

image.png

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

image.png

参数

net.ipv4.tcp_slow_start_after_idle

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

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

 sysctl net.ipv4.tcp_slow_start_after_idle=0

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

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

从系统cache中查看 tcp_metrics item

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

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

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

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

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

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

ssthresh 是如何降低的

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

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

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

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

tcp windows scale

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

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

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

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

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

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

RTT越大,传输速度越慢

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

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

socket send/rcv buf

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

image.png

细化看下问题所在:

image.png

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

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

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

image.png

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

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

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

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

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

就是要你懂TCP相关文章:

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

MSS和MTU导致的悲剧

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

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


总结

影响性能的几个点:

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

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

参考文章:

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

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

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

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

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

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

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

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

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

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

如何在工作中学习V1.1

如何在工作中学习V1.1

2021年0705更新了两个案例和慢就是快的理念,尽量将案例扩大化,不只是程序员,增加了高中数学题的案例。

本文被网友翻译的英文版 (medium 需要梯子)

先说一件值得思考的事情:高考的时候大家都是一样的教科书,同一个教室,同样的老师辅导,时间精力基本差不多,可是最后别人考的是清华北大或者一本,而你的实力只能考个三本,为什么? 当然这里主要是智商的影响,那么其他因素呢?智商解决的问题能不能后天用其他方式来补位一下?

大家平时都看过很多方法论的文章,看的时候很爽觉得非常有用,但是一两周后基本还是老样子了。其中有很大一部分原因那些方法对脑力有要求、或者方法论比较空缺少落地的步骤和案例。 下文中描述的方式方法是不需要智商也能学会的,非常具体的。

关键问题点

为什么你的知识积累不了?

有些知识看过就忘、忘了再看,实际碰到问题还是联系不上这个知识,这其实是知识的积累出了问题,没有深入的理解自然就不能灵活运用,也就谈不上解决问题了。这跟大家一起看相同的高考教科书但是高考结果不一样是一个原因。问题出在了理解上,每个人的理解能力不一样(智商),绝大多数人对知识的理解要靠不断地实践(做题)来巩固。

同样实践效果不一样?

同样工作一年碰到了10个问题(或者说做了10套高考模拟试卷),但是结果不一样,那是因为在实践过程中方法不够好。或者说你对你为什么做对了、为什么做错了没有去复盘

假如碰到一个问题,身边的同事解决了,而我解决不了。那么我就去想这个问题他是怎么解决的,他看到这个问题后的逻辑和思考是怎么样的,有哪些知识指导了他这么逻辑推理,这些知识哪些我也知道但是我没有想到这么去运用推理(说明我对这个知识理解的不到位导致灵活运用缺乏);这些知识中又有哪些是我不知道的(知识缺乏,没什么好说的快去Google什么学习下–有场景案例和目的加持,学习理解起来更快)。

等你把这个问题基本按照你同事掌握的知识和逻辑推理想明白后,需要再去琢磨一下他的逻辑推理解题思路中有没有不对的,有没有啰嗦的地方,有没有更直接的方式(对知识更好地运用)。

我相信每个问题都这么去实践的话就不应该再抱怨灵活运用、举一反三,同时知识也积累下来了,这种场景下积累到的知识是不会那么容易忘记的。

这就是向身边的牛人学习,同时很快超过他的办法。这就是为什么高考前你做了10套模拟题还不如其他人做一套的效果好

知识+逻辑 基本等于你的能力,知识让你知道那个东西,逻辑让你把东西和问题联系起来

这里的问题你可以理解成方案、架构、设计等

img

系统化的知识哪里来?

知识之间是可以联系起来的并且像一颗大树一样自我生长,但是当你都没理解透彻,自然没法产生联系,也就不能够自我生长了。

真正掌握好的知识点会慢慢生长连接最终组成一张大网

但是我们最容易陷入的就是掌握的深度、系统化(工作中碎片时间过多,学校里缺少时间)不够,所以一个知识点每次碰到花半个小时学习下来觉得掌握了,但是3个月后就又没印象了。总是感觉自己在懵懵懂懂中,或者一个领域学起来总是不得要领,根本的原因还是在于:宏观整体大图了解不够(缺乏体系,每次都是盲人摸象);关键知识点深度不够,理解不透彻,这些关键点就是这个领域的骨架、支点、抓手。缺了抓手自然不能生长,缺了宏观大图容易误入歧途。

我们有时候发现自己在某个领域学起来特别快,但是换个领域就总是不得要领,问题出在了上面,即使花再多时间也是徒然。这也就是为什么学霸看两个小时的课本比你看两天效果还好,感受下来还觉得别人好聪明,是不是智商比我高啊。

所以新进入一个领域的时候要去找他的大图和抓手。

好的同事总是能很轻易地把这个大图交给你,再顺便给你几个抓手,你就基本入门了,这就是培训的魅力,这种情况肯定比自学效率高多了。但是目前绝大部分的培训都做不到这点

好的逻辑又怎么来?

实践、复盘

讲个前同事的故事

有一个前同事是5Q过来的,负责技术(所有解决不了的问题都找他),这位同学从chinaren出道,跟着王兴一块创业5Q,5Q在学校靠鸡腿打下大片市场,最后被陈一舟的校内收购(据说被收购后5Q的好多技术都走了,最后王兴硬是呆在校内网把合约上的所有钱都拿到了)。这位同学让我最佩服的解决问题的能力,好多问题其实他也不一定就擅长,但是他就是有本事通过Help、Google不停地验证尝试就把一个不熟悉的问题给解决了,这是我最羡慕的能力,在后面的职业生涯中一直不停地往这个方面尝试。

应用访问数据库比较慢,但又不是慢查询

  1. 这位同学的解决办法是通过tcpdump来分析网络包,看网络包的时间戳和网络包的内容,然后找到了具体卡在了哪里。
  2. 如果是专业的DBA可能会通过show processlist 看具体连接在做什么,比如看到这些连接状态是 authentication 状态,然后再通过Google或者对这个状态的理解知道创建连接的时候MySQL需要反查IP、域名这里比较耗时,通过配置参数 skip-name-resolve 跳过去就好了。
  3. 如果是MySQL的老司机,一上来就知道连接慢的话跟 skip-name-resolve 关系最大。

在我眼里这三种方式都解决了问题,最后一种最快但是纯靠积累和经验,换个问题也许就不灵了;第一种方式是最牛逼和通用的,只需要最少的知识就把问题解决了,而且跨领域仍然可以适用(这也是基础知识的威力)。

我当时跟着他从sudo、ls等linux命令开始学起。当然我不会轻易去打搅他问他,每次碰到问题我尽量让他在我的电脑上来操作,解决后我再自己复盘,通过history调出他的所有操作记录,看他在我的电脑上用Google搜啥了,然后一个个去学习分析他每个动作,去想他为什么搜这个关键字,复盘完还有不懂的再到他面前跟他面对面的讨论他为什么要这么做,指导他这么做的知识和逻辑又是什么。

慢就是快

往往我们很容易求多,一个知识点一本书看下来当时觉得掌握了,实际还是没有,这就是对自己的理解能力高估了,要学会慢下来,打透一个知识点比对10个知识点懵懵懂懂重要多了,因为你掌握一个知识点后,很容易发散掌握其它知识点。

学习不是走斜坡,不是你学了就掌握了(掌握指的知识能用来解决问题);学习更像走阶梯,每一阶有每一阶的难点,学物理有物理的难点,学漫画有漫画的难点,你没有克服难点,再怎么努力都是原地跳。所以当你克服难点,你跳上去就不会下来了。

这里的克服难点可以理解成真正掌握知识点,大多时候的学习只是似是而非,所以一直在假学习,只有真正掌握后才像是上了个台阶。

人跟人的差别就是爬台阶的能力,有人碰到台阶了绕过去,或者别人把他拉上去了,他哦一下就完事了,这种很快还是会掉下去(不能解决问题、或是很快遗忘);有的人爬上去然后反复琢磨刚刚怎么爬上去的,甚至再下来,然后重新爬试试,还有没有不同的爬法。这两种人经过一两年就天差地别了。因为把事情做到位一次,就能获得几十倍于把事情普通完成后得到的经验。

其实高中备考三年的高中生最应该注意这个方法(跟大家推荐的错题本非常类似),比如从做了一道数学几何题 这个案例里面可以看到对一道题型所包含的知识点的理解、运用吃透,远远超过做更多的题目。

如何向身边的同学学习

微信、钉钉提问的技巧

我进现在的公司的时候是个网络小白,但是业务需要我去解决这些问题,于是我就经常在钉钉上找内部的专家来帮请教一些问题,首先要感谢他们的耐心,同时我觉得跟他们提问的时候的方法大家可以参考一下。

首先,没有客套直奔主题把问题描述清楚,微信、钉钉消息本来就不是即时的,就不要问在不在、能不能问个问题、你好(因为这些问题会浪费他一次切换,真要客套把 你好 写在问题前面在一条消息中发出去)。

其次,我会截图把现象接下来,关键部分红框标明。如果是内部机器还会帮对方申请登陆账号,打通ssh登陆,然后把ssh登陆命令和触发截图现象命令的文字一起钉钉发过去。也就是对方收到我的消息,看到截图的问题后,他只要复制粘贴我发给他的文字信息就看到现象了。

为什么要帮他申请账号,有时候账号要审批,要找人,对方不知道到哪里申请等等;这么复杂对方干脆就装作没看见你的消息好了。

为什么还要把ssh登陆命令、重现文字命令发给他呢,怕他敲错啊,敲错了还得来问你,一来一回时间都浪费了。你也许会说我截图上有重现命令啊,那么凭什么他帮你解决问题他还要瞪大眼睛看你的截图把你的命令抄下来?比如容器ID一长串,你是截图了,结果他把b抄成6了,重现不了,还得问你,又是几个来回……

提完问题后有几种情况:抱歉,我也不知道;这个问题你要问问谁,他应该知道;沉默

如果你跟我上面一样给出的信息完整,能直接复制粘贴重现,沉默是极少极少的

没关系钉钉的优势是复制粘贴方便,你就换个人再问,可能问到第三个人终于搞定了。那么我会回来把结果告诉前面我问过的同学,即使他是沉默的那个。因为我骚扰过人家,要回来填这个坑,另外也许他真的不知道,那么同步给他也可以帮到他。结果就是他觉得我很靠谱,信任度就建立好了,下次再有问题会更卖力地一起来解决。

一些不好的网络提问

有个同学看了我的文章(晚上11点看的),马上发了钉钉消息过来问文章中用到的工具是什么。我还没睡觉但是躺床上看东西,有钉钉消息提醒,但没有切过去回复(不想中断我在看的东西)。5分钟后这个同学居然钉了我一下,我当时是很震惊的,这是你平时学习,不是我的产品出了故障,现在晚上11点,因个人原因骚扰别人完全没有边界。

提问题的时间要考虑对方大概率在电脑前,打字快。否则要紧的话就提选择题类型的问题

问题要尽量是封闭的,比如钉钉上不适合问的问题:

  • 为什么我们应用的TPS压不上去,即使CPU还有很多空闲(不好的原因:太开放,原因太多,对方要打字2000才能给你解释清楚各种可能的原因,你要不是他老板就不要这样问了)
  • 用多条消息来描述一个问题,一次没把问题描述清楚,需要对方中断多次

场景式学习、体感的来源、面对问题学习

前面提到的对知识的深入理解这有点空,如何才能做到深入理解?我下面通过几个非常具体的例子来解释下

学习TCP三次握手例子

经历稍微丰富点的工程师都觉得TCP三次握手看过很多次、很多篇文章了,但是文章写得再好似乎当时理解了,但是总是过几个月就忘了或者一看就懂,过一阵子被人一问就模模糊糊了,或者两个为什么就答不上了,自己都觉得自己的回答是在猜或者不确定

为什么会这样呢?而学其它知识就好通畅多了,我觉得这里最主要的是我们对TCP缺乏体感,比如没有几个工程师去看过TCP握手的代码,也没法想象真正的TCP握手是如何在电脑里运作的(打电话能给你一些类似的体感,但是细节覆盖面不够)。

如果这个时候你一边学习的时候一边再用wireshark抓包看看三次握手具体在干什么,比抽象的描述实在多了,你能看到具体握手的一来一回,并且看到一来一回带了哪些内容,这些内容又是用来做什么、为什么要带,这个时候你再去看别人讲解的理论顿时会觉得好理解多了,以后也很难忘记。

但是这里很多人执行能力不强,想去抓包,但是觉得要下载安装wireshark,要学习wireshark就放弃了。只看不动手当然是最舒适的,但是这个最舒适给了你在学习的假象,没有结果。

这是不是跟你要解决一个难题非常像,这个难题需要你去做很多事,比如下载源代码(翻不了墙,放弃);比如要编译(还要去学习那些编译参数,放弃);比如要搭建环境(太琐屑,放弃)。你看这中间九九八十一难你放弃了一难都取不了真经。这也是为什么同样学习、同样的问题,他能学会,他能解决,你不可以。

学习网络路由的案例

我第一次看RFC1180(这个RFC对网络路由描述的太好了)的时候是震惊的,觉得讲述的太好了,2000字就把一本教科书的知识阐述的无比清晰、透彻。但是实际上我发现很快就忘了,而且大部分程序员基本都是这样

写的确实很好,清晰简洁,图文并茂,结构逻辑合理,但是对于95%的程序员没有什么用,当时看的时候很爽、也觉得自己理解了、学会了,实际上看完几周后就忘得差不多了。问题出在这种RFC偏理论多一点看起来完全没有体感无法感同身受,所以即使似乎当时看懂了,但是忘得也快,需要一篇结合实践的文章来帮助理解

在这个问题上,让我深刻地理解到:

一流的人看RFC就够了,差一些的人看《TCP/IP卷1》,再差些的人要看一个个案例带出来的具体知识的书籍了,比如《wireshark抓包艺术》,人和人的学习能力有差别必须要承认。

也就是我们要认识到每个个人的学习能力的差异,我超级认同这篇文章中的一个评论

看完深有感触,尤其是后面的知识效率和工程效率型的区别。以前总是很中二的觉得自己看一遍就理解记住了,结果一次次失败又怀疑自己的智商是不是有问题,其实就是把自己当作知识效率型来用了。一个不太恰当的形容就是,有颗公主心却没公主命!

嗯,大部分时候我们都觉得自己看一遍就理解了记住了能实用解决问题了,实际上了是马上忘了,停下来想想自己是不是这样的?在网络的相关知识上大部分看RFC、TCP卷1等东西是很难实际理解的,还是要靠实践来建立对知识的具体的理解,而网络相关的东西基本离大家有点远(大家不回去读tcp、ip源码,纯粹是靠对书本的理解),所以很难建立具体的概念,所以这里有个必杀技就是学会抓包和用wireshark看包,同时针对实际碰到的文题来抓包、看包分析。

比如我的这篇《从计算机知识到落地能力,你欠缺了什么?》就对上述问题最好的阐述,程序员最常碰到的网络问题就是为啥为啥不通?

这是最好建立对网络知识具体理解和实践的机会,你把《从计算机知识到落地能力,你欠缺了什么?》实践完再去看RFC1180 就明白了。

再来看一个解决问题的例子

会员系统双11优化这个问题对我来说,我是个外来者,完全不懂这里面的部署架构、业务逻辑。但是在问题的关键地方(会员认为自己没问题–压力测试正常的;淘宝API更是认为自己没问题,alimonitor监控显示正常),结果就是会员的同学说我们没有问题,淘宝API肯定有问题,然后就不去思考自己这边可能出问题的环节了。思想上已经甩包了,那么即使再去review流程、环节也就不会那么仔细,自然更是发现不了问题了。

但是我的经验告诉我要有证据地甩包,或者说拿着证据优雅地甩包,这迫使我去找更多的细节证据(证据要给力哦,不能让人家拍回来)。如果我是这么说的,这个问题在淘宝API这里,你看理由是…………,我做了这些实验,看到了这些东东。那么淘宝API那边想要证明我的理由错了就会更积极地去找一些数据。

事实上我就是做这些实验找证据过程中发现了会员的问题,这就是态度、执行力、知识、逻辑能力综合下来拿到的一个结果。我最不喜欢的一句话就是我的程序没问题,因为我的逻辑是这样的,不会错的。你当然不会写你知道的错误逻辑,程序之所以有错误都是在你的逻辑、意料之外的东西。有很多次一堆人电话会议中扯皮的时候,我一般把电话静音了,直接上去人肉一个个过对方的逻辑,一般来说电话会议还没有结束我就给出来对方逻辑之外的东西。

场景式学习

我带2岁的小朋友看刷牙的画本的时候,小朋友理解不了喝口水含在嘴里咕噜咕噜不要咽下去,然后刷牙的时候就都喝下去了。我讲到这里的时候立马放下书把小朋友带到洗手间,先开始我自己刷牙了,示范一下什么是咕噜咕噜(放心,他还是理解不了的,但是至少有点感觉了,水在口里会响,然后水会吐出来)。示范完然后辅导他刷牙,喝水的时候我和他一起直接低着头,喝水然后立马水吐出来了,让他理解了到嘴里的东西不全是吞下去的。然后喝水晃脑袋,有点声音了(离咕噜咕噜不远了)。训练几次后小朋友就理解了咕噜咕噜,也学会了咕噜咕噜。这就是场景式学习的魅力。

很多年前我有一次等电梯,边上还有一个老太太,一个年轻的妈妈带着一个4、5岁的娃。应该是刚从外面玩了回来,妈妈在教育娃娃刚刚在外面哪里做错了,那个小朋友也是气嘟嘟地。进了电梯后都不说话,小朋友就开始踢电梯。这个时候那个年轻的妈妈又想开始教育小朋友了。这时老太太教育这个妈妈说,这是小朋友不高兴,做出的反抗,就是想要用这个方式抗议刚刚的教育或者挑逗起妈妈的注意。这个时候要忽视他,不要去在意,他踢几下后(虽然没有公德这么小懂不了这么多)脚也疼还没人搭理他这个动作,就觉得真没劲,可能后面他都不踢电梯了,觉得这是一个非常无聊还挨疼的事情。那么我在这个场景下立马反应过来,这就是很多以前我对一些小朋友的行为不理解的原因啊,这比书上看到的深刻多了。就是他们生气了在那里做妖挑逗你骂他、打他或者激怒你来吸引大人的注意力。

钉子式学习方法和系统性学习方法

系统性就是想掌握MySQL,那么搞几本MySQL专著和MySQL 官方DOC看下来,一般课程设计的好的话还是比较容易普遍性地掌握下来,绝大部分时候都是这种学习方法,可是问题在于在种方式下学完后当时看着似乎理解了,但是很容易忘记,一片一片地系统性的忘记。还是一般人对知识的理解没那么容易真正理解。

钉子式的学习方式,就是在一大片知识中打入几个桩,反复演练将这个桩不停地夯实,夯温,做到在这个知识点上用通俗的语言跟小白都能讲明白,然后在这几个桩中间发散像星星之火燎原一样把整个一片知识都掌握下来。这种学习方法的缺点就是很难找到一片知识点的这个点,然后没有很好整合的话知识过于零散。

我们常说的一个人很聪明,就是指系统性的看看书就都理解了,是真的理解那种,还能灵活运用,但是大多数普通人就不是这样的,看完书似乎理解了,实际几周后基本都忘记了,真正实践需要用的时候还是用不好。

这个钉子就是我前面讲慢就是快中间提到的:完整地掌握一个知识点,比懵懵懂懂懂了10个知识点还重要,被你掌握的这个知识点就是你的钉子,钉入到一大片位置的知识中,成为一个有力的抓手来帮助理解相关的知识。

举个Open-SSH的例子

为了做通 SSH 的免密登陆,大家都需要用到 ssh-keygen/ssh-copy-id, 如果我们把这两个命令当一个小的钉子的话,会去了解ssh-keygen做了啥(生成了密钥对),或者ssh-copy-id 的时候报错了(原来是需要秘钥对),然后将 ssh-keygen 生成的pub key复制到server的~/.ssh/authorized_keys 中。

然后你应该会对这个原理要有一些理解(更大的钉子),于是理解了密钥对,和ssh验证的流程,顺便学会怎么看ssh debug信息,那么接下来网络上各种ssh攻略、各种ssh卡顿的解决都是很简单的事情了。

比如你通过SSH可以解决这些问题:

  • 免密登陆
  • ssh卡顿
  • 怎么去掉ssh的时候需要手工多输入yes
  • 我的ssh怎么很快就断掉了
  • 我怎么样才能一次通过跳板机ssh到目标机器
  • 我怎么样通过ssh科学上网
  • 我的ansible(底层批量命令都是基于ssh)怎么这么多问题,到底是为什么
  • 我的git怎么报网络错误了
  • X11 forward我怎么配置不好
  • https为什么需要随机数加密,还需要签名
  • …………

这些问题都是一步步在扩大ssh的外延,让这个钉子变成一个巨大的桩。

然后就会学习到一些高级一些的ssh配置,比如干掉经常ssh的时候要yes一下(StrictHostKeyChecking=no), 或者怎么配置一下ssh就不会断线了(ServerAliveInterval=15),或者将 ssh跳板机->ssh server的过程做成 ssh server一步就可以了(ProxyCommand),进而发现用 ssh的ProxyCommand很容易科学上网了,或者git有问题的时候轻而易举地把ssh debug打开,对git进行debug了……

这基本都还是ssh的本质范围,像ansible、git在底层都是依赖ssh来通讯的,你会发现学、调试X11、ansible和git简直太容易了。

另外理解了ssh的秘钥对,也就理解了非对称加密,同时也很容易理解https流程(SSL),同时知道对称和非对称加密各自的优缺点,SSL为什么需要用到这两种加密算法了。

你看一个简单日常的知识我们只要沿着它用钉子精神,深挖细挖你就会发现知识之间的连接,这个小小的知识点成为你知识体系的一根结实的柱子。

我见过太多的老的工程师、年轻的工程师,天天在那里ssh 密码,ssh 跳板机,ssh 目标机,一小会ssh断了,重来一遍;或者ssh后卡住了,等吧……

在这个问题上表现得没有求知欲、没有探索精神、没有一次把问题搞定的魄力,所以就习惯了

空洞的口号

很多文章都会教大家:举一反三、灵活运用、活学活用、多做多练。但是只有这些口号是没法落地的,落地的基本原则就是前面提到的,却总是被忽视了。

什么是工程效率,什么是知识效率

有些人纯看理论就能掌握好一门技能,还能举一反三,这是知识效率,这种人非常少;

大多数普通人都是看点知识然后结合实践来强化理论,要经过反反复复才能比较好地掌握一个知识,这就是工程效率,讲究技巧、工具来达到目的。

肯定知识效率最牛逼,但是拥有这种技能的人毕竟非常少(天生的高智商吧)。从小我们周边那种不怎么学的学霸型基本都是这类,这种学霸都还能触类旁通非常快的掌握一个新知识,非常气人。剩下的绝大部分只能拼时间+方法+总结等也能掌握一些知识

非常遗憾我就是工程效率型,只能羡慕那些知识效率型的学霸。但是这事又不能独立看待有些人在某些方向上是工程效率型,有些方向就又是知识效率型(有一种知识效率型是你掌握的实在太多也就比较容易触类旁通了,这算灰色知识效率型)

使劲挖掘自己在知识效率型方面的能力吧,两者之间当然没有明显的界限,知识积累多了逻辑训练好了在别人看来你的智商就高了

知识分两种

一种是通用知识(不是说对所有人通用,而是说在一个专业领域去到哪个公司都能通用);另外一种是跟业务公司绑定的特定知识

通用知识没有任何疑问碰到后要非常饥渴地扑上去掌握他们(受益终生,这还有什么疑问吗?)。对于特定知识就要看你对业务需要掌握的深度了,肯定也是需要掌握一些的,特定知识掌握好的一般在公司里混的也会比较好

167211888bc4f2a368df3d16c68e6d51.png

如何在工作中学习

如何在工作中学习

大家平时都看过很多方法论的文章,看的时候很爽觉得非常有用,但是一两周后基本还是老样子了。其中有很大一部分原因那些方法对脑力有要求、或者方法论比较空缺少落地的步骤。 下文中描述的方式方法是不需要智商也能学会的,非常具体可以复制。

先说一件值得思考的事情:高考的时候大家都是一样的教科书,同一个教室,同样的老师辅导,时间精力基本差不多,可是最后别人考的是清华北大或者一本,而你的实力只能考个三本,为什么? 当然这里主要是智商的影响,那么其他因素呢?智商解决的问题能不能后天用其他方式来补位一下?

思考10秒钟再往下看

关键问题点

解决问题的能力就是从你储蓄的知识中提取到方案,差别就是知识储存能力和运用能力的差异

为什么你的知识积累不了?

有些知识看过就忘、忘了再看,实际碰到问题还是联系不上这个知识,这其实是知识的积累出了问题,没有深入理解好自然就不能灵活运用,也就谈不上解决不了问题。这跟大家一起看相同的高考教科书但是高考结果不一样。问题出在了理解上,每个人的理解能力不一样(智商),绝大多数人对知识的理解要靠不断地实践(做题)来巩固。

同样实践效果不一样?

同样工作一年碰到了10个问题(或者说做了10套高考模拟试卷),但是结果不一样,那是因为在实践过程中方法不够好。或者说你对你为什么做对了、为什么做错了没有去分析,存在一定的瞎蒙成分。

假如碰到一个问题,身边的同事解决了,而我解决不了。那么我就去想这个问题他是怎么解决的,他看到这个问题后的逻辑和思考是怎么样的,有哪些知识指导了他这么逻辑推理,这些知识哪些我也知道但是我没有想到这么去运用推理(说明我对这个知识理解的不到位导致灵活运用缺乏);这些知识中又有哪些是我不知道的(知识缺乏,没什么好说的快去Google什么学习下–有场景案例和目的加持,学习理解起来更快)。

等你把这个问题基本按照你同事掌握的知识和逻辑推理想明白后,需要再去琢磨一下他的逻辑推理解题思路中有没有不对的,有没有啰嗦的地方,有没有更直接的方式(对知识更好地运用)。

我相信每个问题都这么去实践的话就不会再抱怨为什么自己做不到灵活运用、举一反三,同时知识也积累下来了,实战场景下积累到的知识是不容易忘记的。

这就是向身边的牛人学习,同时很快超过他的办法。这就是为什么高考前你做了10套模拟题还不如其他人做一套的效果好的原因

知识+逻辑 基本等于你的能力,知识让你知道那个东西,逻辑让你把东西和问题联系起来。碰到问题如果你连相关知识都没有就谈不上解决问题,有时候碰到问题被别人解决后你才发现有相应的知识贮备,但还不能转化成能力,那就是你只是知道那个知识点,但理解不到位、不深,也就无法实战了。

这里的问题你可以理解成方案、架构、设计等

逻辑可以理解为:元认知能力(思考方式、思路,像教练一样反复在大脑里追问为什么)

我们说能力强的人比如在读书的时候,他们读到的不仅仅是文字以及文字所阐述的道理,他们更多注意到的是作者的“思考方式” ,作者的“思考方式”与自己的“思考方式”之间的不同,以及,若是作者的“思考方式”有可取之处的话,自己的“思考方式”要做出哪些调整?于是,一本概率论读完,大多数人就是考个试也不一定能及格,而另外的极少数人却成了科学家——因为他们改良了自己的思考方式,从此可以“像一个科学家一样思考”……

系统化的知识哪里来?

知识之间是可以联系起来的并且像一颗大树一样自我生长,但是当你都没理解透彻,自然没法产生联系,也就不能够自我生长了。当我们讲到入门了某块的知识的时候一般是指的对关键问题点理解清晰,并且能够自我生长,也就是滚雪球一样可以滚起来了。

但是我们最容易陷入的就是掌握的深度、系统化(工作中碎片时间过多,学校里缺少实践)不够,所以一个知识点每次碰到花半个小时学习下来觉得掌握了,但是3个月后就又没印象了。总是感觉自己在懵懵懂懂中,或者一个领域学起来总是不得要领,根本的原因还是在于:宏观整体大图了解不够(缺乏体系,每次都是盲人摸象);关键知识点深度不够,理解不透彻,这些关键点就是这个领域的骨架、支点、抓手。缺了抓手自然不能生长,缺了宏观大图容易误入歧途。

我们有时候发现自己在某个领域学起来特别快,但是换个领域就总是不得要领,问题出在了上面,即使花再多时间也是徒然。这也就是为什么学霸看两个小时的课本比你看两天效果还好,感受下来还觉得别人好聪明,是不是智商比我高啊。

所以新进入一个领域的时候要去找他的大图和抓手。

好的书籍或者培训总是能很轻易地把这个大图交给你,再顺便给你几个抓手,你就基本入门了,这就是培训的魅力,这种情况肯定比自学效率高多了。但是目前绝大部分的书籍和培训都做不到这点

好的逻辑又怎么来?

实践、复盘

img

讲个前同事的故事

有一个前同事是5Q过来的,负责技术(所有解决不了的问题都找他),这位同学从chinaren出道,跟着王兴一块创业5Q,5Q在学校靠鸡腿打下大片市场,最后被陈一舟的校内收购(据说被收购后5Q的好多技术都走了,最后王兴硬是呆在校内网把合约上的所有钱都拿到了)。这位同学让我最佩服的解决问题的能力,好多问题其实他也不一定就擅长,但是他就是有本事通过Help、Google不停地验证尝试就把一个不熟悉的问题给解决了,这是我最羡慕的能力,在后面的职业生涯中一直不停地往这个方面尝试。

应用刚启动连接到数据库的时候比较慢,但又不是慢查询

  1. 这位同学的解决办法是通过tcpdump来分析网络通讯包,看具体卡在哪里把这个问题硬生生地给找到了。
  2. 如果是专业的DBA可能会通过show processlist 看具体连接在做什么,比如看到这些连接状态是 authentication 状态,然后再通过Google或者对这个状态的理解知道创建连接的时候MySQL需要反查IP、域名这里比较耗时,通过配置参数 skip-name-resolve 跳过去就好了。
  3. 如果是MySQL的老司机,一上来就知道 skip-name-resolve 这个参数要改改默认值。

在我眼里这三种方式都解决了问题,最后一种最快但是纯靠积累和经验,换个问题也许就不灵了;第一种方式是最牛逼和通用的,只需要最少的业务知识+方法论就可以更普遍地解决各种问题。

我当时跟着他从sudo、ls等linux命令开始学起。当然我不会轻易去打搅他问他,每次碰到问题我尽量让他在我的电脑上来操作,解决后我再自己复盘,通过history调出他的所有操作记录,看他在我的电脑上用Google搜啥了,然后一个个去学习分析他每个动作,去想他为什么搜这个关键字,复盘完还有不懂的再到他面前跟他面对面的讨论他为什么要这么做,指导他这么做的知识和逻辑又是什么(这个动作没有任何难度吧,你照着做就是了,实际我发现绝对不会有10%的同学会去分析history的,而我则是通过history 搞到了各种黑科技 :) )。

场景式学习、体感的来源、面对问题学习

前面提到的对知识的深入理解这有点空,如何才能做到深入理解?

举个学习TCP三次握手例子

经历稍微丰富点的工程师都觉得TCP三次握手看过很多次、很多篇文章了,但是文章写得再好似乎当时理解了,但是总是过几个月就忘了或者一看就懂,过一阵子被人一问就模模糊糊了,或者多问两个为什么就答不上了,自己都觉得自己的回答是在猜或者不确定。

为什么会这样呢?而学其它知识就好通畅多了,我觉得这里最主要的是我们对TCP缺乏体感,比如没有几个工程师去看过TCP握手的代码,也没法想象真正的TCP握手是如何在电脑里运作的(打电话能给你一些类似的体感,但是细节覆盖面不够)。

如果这个时候你一边学习的时候一边再用wireshark抓包看看三次握手具体在干什么、交换了什么信息,比抽象的描述具象实在多了,你能看到握手的一来一回,并且看到一来一回带了哪些内容,这些内容又是用来做什么、为什么要带,这个时候你再去看别人讲解的理论顿时会觉得好理解多了,以后也很难忘记。

但是这里很多人执行能力不强,想去抓包,但是觉得要下载安装wireshark,要学习wireshark就放弃了。只看不动手当然是最舒适的,但是这个最舒适给了你在学习的假象,没有结果

这是不是跟你要解决一个难题非常像,这个难题需要你去做很多事,比如下载源代码(翻不了墙,放弃);比如要编译(还要去学习那些编译参数,放弃);比如要搭建环境(太琐屑,放弃)。你看这中间九九八十一难你放弃了一难都取不了真经。这也是为什么同样学习、同样的问题,他能学会,他能解决,你不可以。

167211888bc4f2a368df3d16c68e6d51.png

空洞的口号

很多文章都会教大家:举一反三、灵活运用、活学活用、多做多练。但是只有这些口号是没法落地的,落地的基本原则就是前面提到的,却总是被忽视了。

还有些人做事情第六感很好,他自己也不一定能阐述清楚合理的逻辑,就是感觉对了,让他给你讲道理,你还真学不来。

我这里主要是在描述能复制的一些具体做法,少喊些放哪里都正确的口号。不要那些抽象的套路,主要是不一定适合你和能复制。

什么是工程效率,什么是知识效率

有些人纯看理论就能掌握好一门技能,还能举一反三,这是知识效率,这种人非常少;

大多数普通人都是看点知识然后结合实践来强化理论,要经过反反复复才能比较好地掌握一个知识,这就是工程效率,讲究技巧、工具来达到目的。

肯定知识效率最牛逼,但是拥有这种技能的人毕竟非常少(天生的高智商吧)。从小我们周边那种不怎么学的学霸型基本都是这类,这种学霸都还能触类旁通非常快的掌握一个新知识,非常气人。剩下的绝大部分只能拼时间+方法+总结等也能掌握一些知识

非常遗憾我就是工程效率型,只能羡慕那些知识效率型的学霸。但是这事又不能独立看待有些人在某些方向上是工程效率型,有些方向就又是知识效率型(有一种知识效率型是你掌握的实在太多也就比较容易触类旁通了,这算灰色知识效率型)

使劲挖掘自己在知识效率型方面的能力吧,两者之间当然没有明显的界限,知识积累多了逻辑训练好了在别人看来你的智商就高了

知识分两种

一种是通用知识(不是说对所有人通用,而是说在一个专业领域去到哪个公司都能通用);另外一种是跟业务公司绑定的特定知识

通用知识没有任何疑问碰到后要非常饥渴地扑上去掌握他们(受益终生,这还有什么疑问吗?)。对于特定知识就要看你对业务需要掌握的深度了,肯定也是需要掌握一些的,特定知识掌握好的一般在公司里混的也会比较好

案例学习的例子

通过一个小问题,花上一周看源代码、做各种实验反复验证,把这里涉及到的知识全部拿下,同时把业务代码、内核配置、出问题的表征、监控指标等等都连贯起来,要么不做要么一杆到底就是要你懂TCP–性能和发送接收Buffer的关系:发送窗口大小(Buffer)、接收窗口大小(Buffer)对TCP传输速度的影响,以及怎么观察窗口对传输速度的影响。BDP、RT、带宽对传输速度又是怎么影响的

进一步阅读

如果喜欢本文的话,你也会喜欢我亲身经历的:《三个故事》

如果你觉得看完对你很有帮助可以通过如下方式找到我

find me on twitter: @plantegg

知识星球:https://t.zsxq.com/0cSFEUh2J

开了一个星球,在里面讲解一些案例、知识、学习方法,肯定没法让大家称为顶尖程序员(我自己都不是),只是希望用我的方法、知识、经验、案例作为你的垫脚石,帮助你快速、早日成为一个基本合格的程序员。

争取在星球内:

  • 养成基本动手能力
  • 拥有起码的分析推理能力–按我接触的程序员,大多都是没有逻辑的
  • 知识上教会你几个关键的知识点
image-20230407232314969

通过案例来理解MSS、MTU等相关TCP概念

就是要你懂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

引文

对于那种出现概率非常低,很难重现的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使用

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 实用教程

iptables监控reset的连接信息

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)

Linux环境变量问题汇总

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 HTTP Proxy and SSH Proxy

如何设置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/