plantegg

java tcp mysql performance network docker Linux

一个没有遵守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 -u -z -v ip 4789
  • 从两边都发送一些内容看看,看是否能到达对方

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


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

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

需找问题

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

screenshot

细化证明问题

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

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

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

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

先检查dns设置:

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

image.png

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

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

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

image.png

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

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

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

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

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

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

image.png

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

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

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

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

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

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

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

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

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

youku-mac-ip.gif

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

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

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

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

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

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

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

Linux环境变量问题汇总

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

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

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

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

image.png

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

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

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

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

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

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

信息更新:

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

直接原因:

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

解决问题的有效操作:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

image.png

image.png

image.png

image.png

拿如上证据求助内核开发

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

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

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



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

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

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

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

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

参考文章

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

Load很高,CPU使用率很低

第一次碰到这种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

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

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现象结合读源代码,基本能比较快解决掉。但是有些时候源代码过于复杂,比如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网络性能测试


缘起

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 的性能问题

在最近的全链路压测中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

0%