综合实战篇

倪朋飞 《Linux 性能优化实战》

46 | 案例篇:为什么应用容器化后,启动慢了很多?

容器慢案例:1.资源限制导致OOM机制被杀死;2.资源少导致服务启动慢
==============================================================================================================
基于 Docker 的微服务架构带来的各种优势,比如:
    使用 Docker ,把应用程序以及相关依赖打包到镜像中后,部署和升级更快捷;
    把传统的单体应用拆分成多个更小的微服务应用后,每个微服务的功能都更简单,并且可以单独管理和维护;
    每个微服务都可以根据需求横向扩展。即使发生故障,也只是局部服务不可用,而不像以前那样,导致整个服务不可用。

jq 工具专门用来在命令行中处理 json。为了更好的展示 json 数据,我们用这个工具,来格式化 json 输出。
-----------------------------------------------------------------------------------------------
案例的2个根因:
    1.对容器进行了资源限制 -m 512M;但是容器内部的真正限制却并不是512M,而是物理机资源额度;然后JVM的堆内存限制是基于物理机资源的,计算出的结果实际超过512M;最终导致了OOM杀死进程
    2.给予的CPU资源过少,导致容器服务启动慢
-----------------------------------------------------------------------------------------------
案例
server端:
$ docker run --name tomcat --cpus 0.1 -m 512M -p 8080:8080 -itd feisky/tomcat:8         # -m表示设置内存为512MB


[root@yefeng ~]# curl localhost:8080            #本地访问容器服务,响应很慢
    Hello, wolrd!
[root@yefeng ~]# curl localhost:8080            #过几分钟后,再次访问,服务挂了
    curl: (52) Empty reply from server

[root@yefeng ~]# docker ps -a                   #容器已经exit
    CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                            PORTS               NAMES
    440bcf0b5979        feisky/tomcat:8     "catalina.sh run"   5 minutes ago       Exited (137) About a minute ago                       tomcat


[root@yefeng ~]# docker logs -f 440bcf0b5979    #查看容器日志
    Using CATALINA_BASE:   /usr/local/tomcat
    Using CATALINA_HOME:   /usr/local/tomcat
    Using CATALINA_TMPDIR: /usr/local/tomcat/temp
    Using JRE_HOME:        /docker-java-home/jre
    Using CLASSPATH:       /usr/local/tomcat/bin/bootstrap.jar:/usr/local/tomcat/bin/tomcat-juli.jar
    09-Jul-2022 15:01:19.019 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Server version:        Apache Tomcat/8.5.38
    09-Jul-2022 15:01:19.120 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Server built:          Feb 5 2019 11:42:42 UTC
    09-Jul-2022 15:01:19.120 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Server number:         8.5.38.0
    09-Jul-2022 15:01:19.120 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log OS Name:               Linux
    09-Jul-2022 15:01:19.121 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log OS Version:            5.18.3-1.el7.elrepo.x86_64
    09-Jul-2022 15:01:19.121 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Architecture:          amd64
    09-Jul-2022 15:01:19.121 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Java Home:             /usr/lib/jvm/java-8-openjdk-amd64/jre
    09-Jul-2022 15:01:19.121 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log JVM Version:           1.8.0_181-8u181-b13-2~deb9u1-b13
    09-Jul-2022 15:01:19.122 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log JVM Vendor:            Oracle Corporation
    09-Jul-2022 15:01:19.122 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log CATALINA_BASE:         /usr/local/tomcat
    09-Jul-2022 15:01:19.122 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log CATALINA_HOME:         /usr/local/tomcat
    09-Jul-2022 15:01:19.123 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Djava.util.logging.config.file=/usr/local/tomcat/conf/logging.properties
    09-Jul-2022 15:01:19.123 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager
    09-Jul-2022 15:01:19.123 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Djdk.tls.ephemeralDHKeySize=2048
    09-Jul-2022 15:01:19.123 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Djava.protocol.handler.pkgs=org.apache.catalina.webresources
    09-Jul-2022 15:01:19.218 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Dorg.apache.catalina.security.SecurityListener.UMASK=0027
    09-Jul-2022 15:01:19.218 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Dignore.endorsed.dirs=
    09-Jul-2022 15:01:19.218 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Dcatalina.base=/usr/local/tomcat
    09-Jul-2022 15:01:19.219 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Dcatalina.home=/usr/local/tomcat
    09-Jul-2022 15:01:19.219 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Djava.io.tmpdir=/usr/local/tomcat/temp
    09-Jul-2022 15:01:19.219 INFO [main] org.apache.catalina.core.AprLifecycleListener.lifecycleEvent Loaded APR based Apache Tomcat Native library [1.2.21] using APR version [1.5.2].
    09-Jul-2022 15:01:19.219 INFO [main] org.apache.catalina.core.AprLifecycleListener.lifecycleEvent APR capabilities: IPv6 [true], sendfile [true], accept filters [false], random [true].
    09-Jul-2022 15:01:19.219 INFO [main] org.apache.catalina.core.AprLifecycleListener.lifecycleEvent APR/OpenSSL configuration: useAprConnector [false], useOpenSSL [true]
    09-Jul-2022 15:01:19.222 INFO [main] org.apache.catalina.core.AprLifecycleListener.initializeSSL OpenSSL successfully initialized [OpenSSL 1.1.0j  20 Nov 2018]
    09-Jul-2022 15:01:20.520 INFO [main] org.apache.coyote.AbstractProtocol.init Initializing ProtocolHandler ["http-nio-8080"]
    09-Jul-2022 15:01:20.623 INFO [main] org.apache.tomcat.util.net.NioSelectorPool.getSharedSelector Using a shared selector for servlet write/read
    09-Jul-2022 15:01:20.823 INFO [main] org.apache.coyote.AbstractProtocol.init Initializing ProtocolHandler ["ajp-nio-8009"]
    09-Jul-2022 15:01:20.919 INFO [main] org.apache.tomcat.util.net.NioSelectorPool.getSharedSelector Using a shared selector for servlet write/read
    09-Jul-2022 15:01:20.920 INFO [main] org.apache.catalina.startup.Catalina.load Initialization processed in 7199 ms
    09-Jul-2022 15:01:21.225 INFO [main] org.apache.catalina.core.StandardService.startInternal Starting service [Catalina]
    09-Jul-2022 15:01:21.225 INFO [main] org.apache.catalina.core.StandardEngine.startInternal Starting Servlet Engine: Apache Tomcat/8.5.38
    09-Jul-2022 15:01:21.422 INFO [localhost-startStop-1] org.apache.catalina.startup.HostConfig.deployDirectory Deploying web application directory [/usr/local/tomcat/webapps/ROOT]
    09-Jul-2022 15:01:27.318 INFO [localhost-startStop-1] org.apache.catalina.startup.HostConfig.deployDirectory Deployment of web application directory [/usr/local/tomcat/webapps/ROOT] has finished in [5,896] ms
    09-Jul-2022 15:01:27.319 INFO [localhost-startStop-1] org.apache.catalina.startup.HostConfig.deployDirectory Deploying web application directory [/usr/local/tomcat/webapps/docs]
    09-Jul-2022 15:01:27.721 INFO [localhost-startStop-1] org.apache.catalina.startup.HostConfig.deployDirectory Deployment of web application directory [/usr/local/tomcat/webapps/docs] has finished in [402] ms
    09-Jul-2022 15:01:27.724 INFO [localhost-startStop-1] org.apache.catalina.startup.HostConfig.deployDirectory Deploying web application directory [/usr/local/tomcat/webapps/examples]
    09-Jul-2022 15:01:31.823 INFO [localhost-startStop-1] org.apache.catalina.startup.HostConfig.deployDirectory Deployment of web application directory [/usr/local/tomcat/webapps/examples] has finished in [4,099] ms
    09-Jul-2022 15:01:31.823 INFO [localhost-startStop-1] org.apache.catalina.startup.HostConfig.deployDirectory Deploying web application directory [/usr/local/tomcat/webapps/host-manager]
    09-Jul-2022 15:01:32.121 INFO [localhost-startStop-1] org.apache.catalina.startup.HostConfig.deployDirectory Deployment of web application directory [/usr/local/tomcat/webapps/host-manager] has finished in [297] ms
    09-Jul-2022 15:01:32.121 INFO [localhost-startStop-1] org.apache.catalina.startup.HostConfig.deployDirectory Deploying web application directory [/usr/local/tomcat/webapps/manager]
    09-Jul-2022 15:01:32.520 INFO [localhost-startStop-1] org.apache.catalina.startup.HostConfig.deployDirectory Deployment of web application directory [/usr/local/tomcat/webapps/manager] has finished in [399] ms
    09-Jul-2022 15:01:32.621 INFO [main] org.apache.coyote.AbstractProtocol.start Starting ProtocolHandler ["http-nio-8080"]
    09-Jul-2022 15:01:32.918 INFO [main] org.apache.coyote.AbstractProtocol.start Starting ProtocolHandler ["ajp-nio-8009"]
    09-Jul-2022 15:01:33.022 INFO [main] org.apache.catalina.startup.Catalina.start Server startup in 12102 ms


[root@yefeng ~]# docker inspect tomcat -f '{{json .State}}' | jq        # 显示容器状态,jq用来格式化json输出
    {
      "Status": "exited",           #容器已经处于 exited 状态
      "Running": false,
      "Paused": false,
      "Restarting": false,
      "OOMKilled": true,            #OOMKilled 是 true
      "Dead": false,
      "Pid": 0,
      "ExitCode": 137,              #ExitCode 是 137
      "Error": "",
      "StartedAt": "2022-07-09T15:01:11.944397977Z",
      "FinishedAt": "2022-07-09T15:05:18.109777032Z"
    }

[root@yefeng ~]# docker inspect tomcat |grep "State" -A12       #上一个命令效果类似这个嘛。。。
        "State": {
            "Status": "exited",
            "Running": false,
            "Paused": false,
            "Restarting": false,
            "OOMKilled": true,
            "Dead": false,
            "Pid": 0,
            "ExitCode": 137,
            "Error": "",
            "StartedAt": "2022-07-09T15:01:11.944397977Z",
            "FinishedAt": "2022-07-09T15:05:18.109777032Z"
        },

***OOM 表示内存不足时,某些应用会被系统杀死;
我们的应用分配了 256 MB 的内存,而容器启动时,明明通过 -m 选项,设置了 512 MB 的内存,按说应该是足够的。
当 OOM 发生时,系统会把相关的 OOM 信息,记录到日志中。所以,接下来,我们可以在终端中执行 dmesg 命令,查看系统日志,并定位 OOM 相关的日志:

[root@yefeng ~]# dmesg
    [80608.709252] java invoked oom-killer: gfp_mask=0xcc0(GFP_KERNEL), order=0, oom_score_adj=0
    [80608.709279] CPU: 0 PID: 29118 Comm: java Tainted: G        W   E     5.18.3-1.el7.elrepo.x86_64 #1
    [80608.709295] Hardware name: VMware, Inc. VMware Virtual Platform/440BX Desktop Reference Platform, BIOS 6.00 11/12/2020
    [80608.709310] Call Trace:
    [80608.709325]  <TASK>
    [80608.709354]  dump_stack_lvl+0x49/0x5f
    [80608.725685]  dump_stack+0x10/0x12
    [80608.725687]  dump_header+0x4f/0x200
    [80608.725912]  oom_kill_process.cold+0xb/0x10
    [80608.725965]  out_of_memory+0x1cf/0x540
    [80608.726084]  mem_cgroup_out_of_memory+0xe4/0x100             
    [80608.726108]  try_charge_memcg+0x6a1/0x730
    [80608.726110]  ? __alloc_pages+0x17b/0x330
    [80608.726148]  charge_memcg+0x8b/0x100
    [80608.726150]  __mem_cgroup_charge+0x2d/0x80
    [80608.726153]  do_anonymous_page+0x108/0x390
    [80608.726174]  __handle_mm_fault+0x824/0x880
    [80608.726177]  handle_mm_fault+0xdb/0x2c0
    [80608.726178]  do_user_addr_fault+0x1be/0x650
    [80608.726252]  exc_page_fault+0x6c/0x150
    [80608.726275]  ? asm_exc_page_fault+0x8/0x30
    [80608.726533]  asm_exc_page_fault+0x1e/0x30
    [80608.726536] RIP: 0033:0x7fc93e31620d
    [80608.726575] Code: 01 00 00 48 83 fa 40 77 77 c5 fe 7f 44 17 e0 c5 fe 7f 07 c5 f8 77 c3 66 0f 1f 44 00 00 c5 f8 77 48 89 d1 40 0f b6 c6 48 89 fa <f3> aa 48 89 d0 c3 0f 1f 00 66 2e 0f 1f 84 00 00 00 00 00 48 39 d1
    [80608.726595] RSP: 002b:00007fc8ef1509e8 EFLAGS: 00010206
    [80608.726615] RAX: 0000000000000000 RBX: 00007fc9381a8800 RCX: 0000000008aa7090
    [80608.726616] RDX: 0000000755a28090 RSI: 0000000000000000 RDI: 000000075cf81000
    [80608.726617] RBP: 00007fc8ef150a60 R08: 0000000002000002 R09: 00007fc93e1c68a1
    [80608.726636] R10: 00007fc938018b90 R11: 0000000000000206 R12: 00000007c0000768
    [80608.726637] R13: 00007fc9381a8800 R14: 00000007c0000768 R15: 0000000010000000
    [80608.726640]  </TASK>
    [80608.726738] memory: usage 524284kB, limit 524288kB, failcnt 29761
    [80608.726740] memory+swap: usage 1048576kB, limit 1048576kB, failcnt 32
    [80608.726741] kmem: usage 3900kB, limit 9007199254740988kB, failcnt 0
    [80608.726742] Memory cgroup stats for /system.slice/docker-440bcf0b5979c56858162e816a779c3dc71647f104674d2ae51cee422e239a3a.scope:
    [80608.808074] anon 532013056
    file 16384
    kernel 3952640
    kernel_stack 720896
    pagetables 2510848
    percpu 4680
    sock 0
    vmalloc 12288
    shmem 0
    file_mapped 8192
    file_dirty 8192
    file_writeback 0
    swapcached 568717312
    anon_thp 0
    file_thp 0
    shmem_thp 0
    inactive_anon 530817024
    active_anon 2039808
    inactive_file 16384
    active_file 0
    unevictable 0
    slab_reclaimable 168760
    slab_unreclaimable 500840
    slab 669600
    workingset_refault_anon 4922
    workingset_refault_file 6
    workingset_activate_anon 0
    workingset_activate_file 6
    workingset_restore_anon 0
    workingset_restore_file 0
    workingset_nodereclaim 0
    pgfault 160831
    pgmajfault 3653
    pgrefill 14
    pgscan 449216
    pgsteal 136014
    pgactivate 505
    pgdeactivate 14
    pglazyfree 0
    pglazyfreed 0
    thp_fault_alloc 225
    thp_collapse_alloc 0
    [80608.808094] Tasks state (memory values in pages):
    [80608.808094] [  pid  ]   uid  tgid total_vm      rss pgtables_bytes swapents oom_score_adj name
    [80608.808102] [  28962]     0 28962  1282795   134081  2523136   131569             0 java
    [80608.808120] oom-kill:constraint=CONSTRAINT_MEMCG,nodemask=(null),cpuset=docker-440bcf0b5979c56858162e816a779c3dc71647f104674d2ae51cee422e239a3a.scope,mems_allowed=0,oom_memcg=/system.slice/docker-440bcf0b5979c56858162e816a779c3dc71647f104674d2ae51cee422e239a3a.scope,task_memcg=/system.slice/docker-440bcf0b5979c56858162e816a779c3dc71647f104674d2ae51cee422e239a3a.scope,task=java,pid=28962,uid=0
    [80608.808166] Memory cgroup out of memory: Killed process 28962 (java) total-vm:5131180kB, anon-rss:516256kB, file-rss:20068kB, shmem-rss:0kB, UID:0 pgtables:2464kB oom_score_adj:0
    [80609.409454] docker0: port 1(veth1a194a0) entered disabled state
    [80609.410704] vethe2b94bf: renamed from eth0
    [80609.434731] docker0: port 1(veth1a194a0) entered disabled state
    [80609.436548] device veth1a194a0 left promiscuous mode
    [80609.436555] docker0: port 1(veth1a194a0) entered disabled state
    [root@yefeng ~]#

$ dmesg
    [193038.106393] java invoked oom-killer: gfp_mask=0x14000c0(GFP_KERNEL), nodemask=(null), order=0, oom_score_adj=0
    [193038.106396] java cpuset=0f2b3fcdd2578165ea77266cdc7b1ad43e75877b0ac1889ecda30a78cb78bd53 mems_allowed=0
    [193038.106402] CPU: 0 PID: 27424 Comm: java Tainted: G  OE    4.15.0-1037 #39-Ubuntu
    [193038.106404] Hardware name: Microsoft Corporation Virtual Machine/Virtual Machine, BIOS 090007  06/02/2017
    [193038.106405] Call Trace:
    [193038.106414]  dump_stack+0x63/0x89
    [193038.106419]  dump_header+0x71/0x285
    [193038.106422]  oom_kill_process+0x220/0x440
    [193038.106424]  out_of_memory+0x2d1/0x4f0
    [193038.106429]  mem_cgroup_out_of_memory+0x4b/0x80
    [193038.106432]  mem_cgroup_oom_synchronize+0x2e8/0x320
    [193038.106435]  ? mem_cgroup_css_online+0x40/0x40
    [193038.106437]  pagefault_out_of_memory+0x36/0x7b
    [193038.106443]  mm_fault_error+0x90/0x180
    [193038.106445]  __do_page_fault+0x4a5/0x4d0
    [193038.106448]  do_page_fault+0x2e/0xe0
    [193038.106454]  ? page_fault+0x2f/0x50
    [193038.106456]  page_fault+0x45/0x50
    [193038.106459] RIP: 0033:0x7fa053e5a20d
    [193038.106460] RSP: 002b:00007fa0060159e8 EFLAGS: 00010206
    [193038.106462] RAX: 0000000000000000 RBX: 00007fa04c4b3000 RCX: 0000000009187440
    [193038.106463] RDX: 00000000943aa440 RSI: 0000000000000000 RDI: 000000009b223000
    [193038.106464] RBP: 00007fa006015a60 R08: 0000000002000002 R09: 00007fa053d0a8a1
    [193038.106465] R10: 00007fa04c018b80 R11: 0000000000000206 R12: 0000000100000768
    [193038.106466] R13: 00007fa04c4b3000 R14: 0000000100000768 R15: 0000000010000000
    [193038.106468] Task in /docker/0f2b3fcdd2578165ea77266cdc7b1ad43e75877b0ac1889ecda30a78cb78bd53 killed as a result of limit of /docker/0f2b3fcdd2578165ea77266cdc7b1ad43e75877b0ac1889ecda30a78cb78bd53
    [193038.106478] memory: usage 524288kB, limit 524288kB, failcnt 77
    [193038.106480] memory+swap: usage 0kB, limit 9007199254740988kB, failcnt 0
    [193038.106481] kmem: usage 3708kB, limit 9007199254740988kB, failcnt 0
    [193038.106481] Memory cgroup stats for /docker/0f2b3fcdd2578165ea77266cdc7b1ad43e75877b0ac1889ecda30a78cb78bd53: cache:0KB rss:520580KB rss_huge:450560KB shmem:0KB mapped_file:0KB dirty:0KB writeback:0KB inactive_anon:0KB active_anon:520580KB inactive_file:0KB active_file:0KB unevictable:0KB
    [193038.106494] [ pid ]   uid  tgid total_vm      rss pgtables_bytes swapents oom_score_adj name
    [193038.106571] [27281]     0 27281  1153302   134371  1466368        0             0 java
    [193038.106574] Memory cgroup out of memory: Kill process 27281 (java) score 1027 or sacrifice child
    [193038.148334] Killed process 27281 (java) total-vm:4613208kB, anon-rss:517316kB, file-rss:20168kB, shmem-rss:0kB
    [193039.607503] oom_reaper: reaped process 27281 (java), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB
                                                                                                                    

    第一,被杀死的是一个 java 进程。从内核调用栈上的 mem_cgroup_out_of_memory 可以看出,它是因为超过 cgroup 的内存限制,而被 OOM 杀死的。
    第二,java 进程是在容器内运行的,而容器内存的使用量和限制都是 512M(524288kB)。目前使用量已经达到了限制,所以会导致 OOM。
    第三,被杀死的进程,PID 为 27281,虚拟内存为 4.3G(total-vm:4613208kB),匿名内存为 505M(anon-rss:517316kB),页内存为 19M(20168kB)。换句话说,匿名内存是主要的内存占用。而且,匿名内存加上页内存,总共是 524M,已经超过了  512M 的限制。
综合这几点,可以看出,Tomcat 容器的内存主要用在了匿名内存中,而匿名内存,其实就是主动申请分配的堆内存。

问题1根因:JVM 根据系统的内存总量,来自动管理堆内存,不明确配置的话,堆内存的默认限制是物理内存的四分之一。不过,前面我们已经限制了容器内存为 512 M,java 的堆内存到底是多少呢?
重新启动 tomcat 容器,并调用 java 命令行来查看堆内存大小:
    $ docker rm -f tomcat       # 重新启动容器
    $ docker run --name tomcat --cpus 0.1 -m 512M -p 8080:8080 -itd feisky/tomcat:8

    $ docker exec tomcat java -XX:+PrintFlagsFinal -version | grep HeapSize     # 查看堆内存,注意单位是字节
        uintx ErgoHeapSizeLimit                         = 0                                   {product}
        uintx HeapSizePerGCThread                       = 87241520                            {product}
        uintx InitialHeapSize                          := 132120576                           {product}
        uintx LargePageHeapSizeThreshold                = 134217728                           {product}
        uintx MaxHeapSize                              := 2092957696  

    可以看到,初始堆内存的大小(InitialHeapSize)是 126MB,而最大堆内存则是 1.95GB,这可比容器限制的 512 MB 大多了。

之所以会这么大,其实是因为,容器内部看不到 Docker 为它设置的内存限制。
虽然在启动容器时,我们通过 -m 512M 选项,给容器设置了 512M 的内存限制。但实际上,从容器内部看到的限制,却并不是 512M。

$ docker exec tomcat free -m
                  total        used        free      shared  buff/cache   available
    Mem:           7977         521        1941           0        5514        7148
    Swap:             0           0           0


    [root@yefeng ~]# docker run --name tomcat --cpus 0.1 -m 512M -p 8080:8080 -itd feisky/tomcat:8
    WARNING: IPv4 forwarding is disabled. Networking will not work.
    d09d189952a89e16d6f2190444bc2915f1a1a0399d9ef94512b62fe188605e3c
    [root@yefeng ~]# docker exec -it d09d189952a89e16d6f2190444bc2915f1a1a0399d9ef94512b62fe188605e3c free -h
                  total        used        free      shared  buff/cache   available
    Mem:           9.7G        996M        7.4G         36M        1.3G        8.4G
    Swap:          2.0G        604K        2.0G

容器内部看到的内存,仍是主机内存

执行下面的命令,通过环境变量 JAVA_OPTS=’-Xmx512m -Xms512m’ ,把 JVM 的初始内存和最大内存都设为 512MB:
# 删除问题容器
$ docker rm -f tomcat
# 运行新的容器
$ docker run --name tomcat --cpus 0.1 -m 512M -e JAVA_OPTS='-Xmx512m -Xms512m' -p 8080:8080 -itd feisky/tomcat:8


[root@yefeng ~]# for ((i=0;i<30;i++)); do curl localhost:8080; sleep 1; done
Hello, wolrd!
Hello, wolrd!
Hello, wolrd!
Hello, wolrd!
Hello, wolrd!
Hello, wolrd!   #再次测试,除了第一次的响应特别慢,其他都正常了
Hello, wolrd!
Hello, wolrd!
Hello, wolrd!
Hello, wolrd!
Hello, wolrd!
Hello, wolrd!
Hello, wolrd!
Hello, wolrd!

----------------------------------------------------------------------------------------------------
处理问题2:启动慢
[root@yefeng ~]# docker logs  tomcat |tail      
09-Jul-2022 15:36:07.118 INFO [localhost-startStop-1] org.apache.catalina.startup.HostConfig.deployDirectory Deployment of web application directory [/usr/local/tomcat/webapps/docs] has finished in [597] ms
09-Jul-2022 15:36:07.119 INFO [localhost-startStop-1] org.apache.catalina.startup.HostConfig.deployDirectory Deploying web application directory [/usr/local/tomcat/webapps/examples]
09-Jul-2022 15:36:10.619 INFO [localhost-startStop-1] org.apache.catalina.startup.HostConfig.deployDirectory Deployment of web application directory [/usr/local/tomcat/webapps/examples] has finished in [3,499] ms
09-Jul-2022 15:36:10.619 INFO [localhost-startStop-1] org.apache.catalina.startup.HostConfig.deployDirectory Deploying web application directory [/usr/local/tomcat/webapps/host-manager]
09-Jul-2022 15:36:10.923 INFO [localhost-startStop-1] org.apache.catalina.startup.HostConfig.deployDirectory Deployment of web application directory [/usr/local/tomcat/webapps/host-manager] has finished in [303] ms
09-Jul-2022 15:36:10.923 INFO [localhost-startStop-1] org.apache.catalina.startup.HostConfig.deployDirectory Deploying web application directory [/usr/local/tomcat/webapps/manager]
09-Jul-2022 15:36:11.218 INFO [localhost-startStop-1] org.apache.catalina.startup.HostConfig.deployDirectory Deployment of web application directory [/usr/local/tomcat/webapps/manager] has finished in [295] ms
09-Jul-2022 15:36:11.319 INFO [main] org.apache.coyote.AbstractProtocol.start Starting ProtocolHandler ["http-nio-8080"]
09-Jul-2022 15:36:11.619 INFO [main] org.apache.coyote.AbstractProtocol.start Starting ProtocolHandler ["ajp-nio-8009"]
09-Jul-2022 15:36:11.722 INFO [main] org.apache.catalina.startup.Catalina.start Server startup in 11003 ms      #查看日志,发现服务初始化时间需要11秒。。。



通过不断地删除容器,启动新容器,来观察容器启动慢的原因
$ docker rm -f tomcat       # 删除旧容器
$ docker run --name tomcat --cpus 0.1 -m 512M -e JAVA_OPTS='-Xmx512m -Xms512m' -p 8080:8080 -itd feisky/tomcat:8        # 运行新容器



$ top       #使用top进行观察
    top - 12:57:18 up 2 days,  5:50,  2 users,  load average: 0.00, 0.02, 0.00
    Tasks: 131 total,   1 running,  74 sleeping,   0 stopped,   0 zombie
    %Cpu0  :  3.0 us,  0.3 sy,  0.0 ni, 96.6 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st         #整理的指标看起来都不高
    %Cpu1  :  5.7 us,  0.3 sy,  0.0 ni, 94.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
    KiB Mem :  8169304 total,  2465984 free,   500812 used,  5202508 buff/cache
    KiB Swap:        0 total,        0 free,        0 used.  7353652 avail Mem
      PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
    29457 root      20   0 2791736  73704  19164 S  10.0  0.9   0:01.61 java                #但是实际上10%的使用率已经跑满了,因为该容器对资源进行了限制"--cpus 0.1";但是这里选择了无视
    27349 root      20   0 1121372  96760  39340 S   0.3  1.2   4:20.82 dockerd
    27376 root      20   0 1031760  43768  21680 S   0.3  0.5   2:44.47 docker-containe              
    29430 root      20   0    7376   3604   3128 S   0.3  0.0   0:00.01 docker-containe
        1 root      20   0   78132   9332   6744 S   0.0  0.1   0:16.12 systemd

这里只是觉得java占用cpu偏高,觉得可疑
删除并启动新容器,并使用pidstat进行观察
$ docker rm -f tomcat       # 删除旧容器
$ docker run --name tomcat --cpus 0.1 -m 512M -e JAVA_OPTS='-Xmx512m -Xms512m' -p 8080:8080 -itd feisky/tomcat:8        # 运行新容器
$ PID=$(docker inspect tomcat -f '{{.State.Pid}}')      # 查询新容器中进程的Pid
$ pidstat -t -p $PID 1      # 执行 pidstat;-t表示显示线程,-p指定进程号
    12:59:28      UID      TGID       TID    %usr %system  %guest   %wait    %CPU   CPU  Command
    12:59:29        0     29850         -   10.00    0.00    0.00    0.00   10.00     0  java
    12:59:29        0         -     29850    0.00    0.00    0.00    0.00    0.00     0  |__java
    12:59:29        0         -     29897    5.00    1.00    0.00   86.00    6.00     1  |__java
    ...
    12:59:29        0         -     29905    3.00    0.00    0.00   97.00    3.00     0  |__java
    12:59:29        0         -     29906    2.00    0.00    0.00   49.00    2.00     1  |__java
    12:59:29        0         -     29908    0.00    0.00    0.00   45.00    0.00     0  |__java

虽然 CPU 使用率(%CPU)很低,但等待运行的使用率(%wait)却非常高,最高甚至已经达到了 97%。这说明,这些线程大部分时间都在等待调度,而不是真正的运行。
为什么 CPU 使用率这么低,线程的大部分时间还要等待 CPU 呢?由于这个现象因 Docker 而起,自然的,你应该想到,这可能是因为 Docker 为容器设置了限制。
再回顾一下,案例开始时容器的启动命令。我们用 --cpus 0.1 ,为容器设置了 0.1 个 CPU 的限制,也就是 10% 的 CPU。这里也就可以解释,为什么 java 进程只有 10% 的 CPU 使用率,也会大部分时间都在等待了。

# 删除旧容器
$ docker rm -f tomcat
# 运行新容器 #增大了CPU资源!!!
$ docker run --name tomcat --cpus 1 -m 512M -e JAVA_OPTS='-Xmx512m -Xms512m' -p 8080:8080 -itd feisky/tomcat:8
# 查看容器日志
$ docker logs -f tomcat
...
18-Feb-2019 12:54:02.139 INFO [main] org.apache.catalina.startup.Catalina.start Server startup in 2001 ms       #启动时间由22秒缩短为2秒


如果你在 Docker 容器中运行 Java 应用,一定要确保,在设置容器资源限制的同时,配置好 JVM 的资源选项(比如堆内存等)。当然,如果你可以升级 Java 版本,那么升级到 Java 10 ,就可以自动解决类似问题了。



容器化后的性能分析,跟前面内容稍微有些区别,比如下面这几点。
    容器本身通过 cgroups 进行资源隔离,所以,在分析时要考虑 cgroups 对应用程序的影响。
    容器的文件系统、网络协议栈等跟主机隔离。虽然在容器外面,我们也可以分析容器的行为,不过有时候,进入容器的命名空间内部,可能更为方便。
    容器的运行可能还会依赖于其他组件,比如各种网络插件(比如 CNI)、存储插件(比如 CSI)、设备插件(比如 GPU)等,让容器的性能分析更加复杂。如果你需要分析容器性能,别忘了考虑它们对性能的影响。
容器慢案例:1.资源限制导致OOM机制被杀死;2.资源少导致服务启动慢

47 | 案例篇:服务器总是时不时丢包,我该怎么办?(上)
48 | 案例篇:服务器总是时不时丢包,我该怎么办?(下)

容器化后,应用程序会通过命名空间进行隔离。所以,你在分析时,不要忘了结合命名空间、cgroups、iptables 等来综合分析。比如:
    cgroups 会影响容器应用的运行;
    iptables 中的 NAT,会影响容器的网络性能;
    叠加文件系统,会影响应用的 I/O 性能等。

---------------------------------------------------------
容器丢包案例:1.链路层tc规则随机丢包;2.iptable3规则随机丢包;3.mtu问题导致http GET请求被丢包
==============================================================================================================================
本案例问题汇总:
    1.tc命令配置随机丢弃,导致了hping3测试丢包
    2.iptables配置了随机的drop规则,导致了hping3测试丢包
    3.mtu 100,导致了tcp3次握手正常,但是业务数据包丢包
排查过程小结:
    hping3 -c 10 -S -p 80 192.168.0.30  测试,发现丢包现象

    链路层
        1.netstat -i/ifconfig/ip a    #查看数据(RX: bytes  packets  errors  dropped overrun)
        2.tc -s qdisc show dev eth0   #查看tc规则是否导致丢包

    网络层和传输层
        1.netstat -s                  #查看ip、tcp、udp等的传输数据

    排查 iptables 和内核的连接跟踪机制
        1.sysctl net.netfilter.nf_conntrack_max
        sysctl net.netfilter.nf_conntrack_count     #连接跟踪数不能超过最大连接跟踪数
        
        2.iptables -t filter -nvL                   #查看各条规则的统计信息,此处重点关注drop规则的匹配数量
    -----------------------------------------
    修复tc规则和iptables导致的丢包问题后,hping3测试无问题,但是curl访问却超时
        1.netstat -i/ifconfig/ip a    #查看数据(RX: bytes  packets  errors  dropped overrun)
        发现dropped数据在增长
    最后发现网卡mtu配置为100

-----------------------------------------------------------------------------------------------------------
启动容器,并观察到容器已经up
$ docker run --name nginx --hostname nginx --privileged -p 80:80 -itd feisky/nginx:drop
$ docker ps
    CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS                NAMES
    dae0202cc27e        feisky/nginx:drop   "/start.sh"         4 minutes ago       Up 4 minutes        0.0.0.0:80->80/tcp   nginx


执行hping3 命令,进一步验证 Nginx 是否可以正常访问了
$ hping3 -c 10 -S -p 80 192.168.0.30    # -c表示发送10个请求,-S表示使用TCP SYN,-p指定端口为80
    HPING 192.168.0.30 (eth0 192.168.0.30): S set, 40 headers + 0 data bytes
    len=44 ip=192.168.0.30 ttl=63 DF id=0 sport=80 flags=SA seq=3 win=5120 rtt=7.5 ms
    len=44 ip=192.168.0.30 ttl=63 DF id=0 sport=80 flags=SA seq=4 win=5120 rtt=7.4 ms
    len=44 ip=192.168.0.30 ttl=63 DF id=0 sport=80 flags=SA seq=5 win=5120 rtt=3.3 ms
    len=44 ip=192.168.0.30 ttl=63 DF id=0 sport=80 flags=SA seq=7 win=5120 rtt=3.0 ms
    len=44 ip=192.168.0.30 ttl=63 DF id=0 sport=80 flags=SA seq=6 win=5120 rtt=3027.2 ms
    --- 192.168.0.30 hping statistic ---
    10 packets transmitted, 5 packets received, 50% packet loss     #10 个请求包,却只收到了 5 个回复,50% 的包都丢了。
    round-trip min/avg/max = 3.0/609.7/3027.2 ms            #部分延迟很高

发现2个问题:
    1.50%丢包
    2.部分延迟高


----------------------------------------------------------------------------
全程都有丢包的可能。比如我们从下往上看:
    在两台 VM 连接之间,可能会发生传输失败的错误,比如网络拥塞、线路错误等;
    在网卡收包后,环形缓冲区可能会因为溢出而丢包;
    在链路层,可能会因为网络帧校验失败、QoS 等而丢包;
    在 IP 层,可能会因为路由失败、组包大小超过 MTU 等而丢包;
    在传输层,可能会因为端口未监听、资源占用超过内核限制等而丢包;
    在套接字层,可能会因为套接字缓冲区溢出而丢包;
    在应用层,可能会因为应用程序异常而丢包;
    此外,如果配置了 iptables 规则,这些网络包也可能因为 iptables 过滤规则而丢包。

在client端、server端、server端veth pair设备抓包,结果显示一致,说明了中间链路、宿主机的因素可以排除

----------------------------------------------------------------------------
查看容器的链路层
    root@nginx:/# netstat -i
    Kernel Interface table
    Iface      MTU    RX-OK RX-ERR RX-DRP RX-OVR    TX-OK TX-ERR TX-DRP TX-OVR Flg
    eth0       100       31      0      0 0             8      0      0      0 BMRU
    lo       65536        0      0      0 0             0      0      0      0 LRU
        RX-OK       总包数
        RX-ERR      总错误数
        RX-DRP      进入 Ring Buffer 后因其他原因(如内存不足)导致的丢包数    
        RX-OVR      Ring Buffer 溢出导致的丢包数
        
        其实这几个参数,ifconfig,ip -s a 都可以看到
        ethtool -S可以看到更详细的统计信息
        但是veth 驱动并没有实现网络统计的功能,所以使用 ethtool -S 命令,无法得到veth pair网卡收发数据的汇总信息
        
    查看 eth0 上是否配置了 tc 规则,并查看有没有丢包
    root@nginx:/# tc -s qdisc show dev eth0
        qdisc netem 800d: root refcnt 2 limit 1000 loss 30%
         Sent 432 bytes 8 pkt (dropped 4, overlimits 0 requeues 0)  ###tc规则丢弃了4个包
         backlog 0b 0p requeues 0
    从 tc 的输出中可以看到, eth0 上面配置了一个网络模拟排队规则(qdisc netem),并且配置了丢包率为 30%(loss 30%)。再看后面的统计信息,发送了 8 个包,但是丢了 4 个。
    解决方法也就很简单了,直接删掉 netem 模块就可以了。
    root@nginx:/# tc qdisc del dev eth0 root netem loss 30%


    清除tc规则后,重新测试
    $ hping3 -c 10 -S -p 80 192.168.0.30
        HPING 192.168.0.30 (eth0 192.168.0.30): S set, 40 headers + 0 data bytes
        len=44 ip=192.168.0.30 ttl=63 DF id=0 sport=80 flags=SA seq=0 win=5120 rtt=7.9 ms
        len=44 ip=192.168.0.30 ttl=63 DF id=0 sport=80 flags=SA seq=2 win=5120 rtt=1003.8 ms
        len=44 ip=192.168.0.30 ttl=63 DF id=0 sport=80 flags=SA seq=5 win=5120 rtt=7.6 ms
        len=44 ip=192.168.0.30 ttl=63 DF id=0 sport=80 flags=SA seq=6 win=5120 rtt=7.4 ms
        len=44 ip=192.168.0.30 ttl=63 DF id=0 sport=80 flags=SA seq=9 win=5120 rtt=3.0 ms
        --- 192.168.0.30 hping statistic ---
        10 packets transmitted, 5 packets received, 50% packet loss
        round-trip min/avg/max = 3.0/205.9/1003.8 ms
    测试结果显示仍然存在丢包,但是高延迟似乎有好转,由3s下降为1s


----------------------------------------------------------------------------
网络层和传输层
    执行netstat -s 命令,就可以看到协议的收发汇总,以及错误信息:
    netstat 汇总了 IP、ICMP、TCP、UDP 等各种协议的收发统计信息。
    root@nginx:/# netstat -s
    Ip:
        Forwarding: 1          //开启转发
        31 total packets received    //总收包数
        0 forwarded            //转发包数
        0 incoming packets discarded  //接收丢包数
        25 incoming packets delivered  //接收的数据包数
        15 requests sent out      //发出的数据包数
    Icmp:
        0 ICMP messages received    //收到的ICMP包数
        0 input ICMP message failed    //收到ICMP失败数
        ICMP input histogram:
        0 ICMP messages sent      //ICMP发送数
        0 ICMP messages failed      //ICMP失败数
        ICMP output histogram:
    Tcp:
        0 active connection openings  //主动连接数
        0 passive connection openings  //被动连接数
        11 failed connection attempts  //失败连接尝试数        #11 次连接失败重试
        0 connection resets received  //接收的连接重置数
        0 connections established    //建立连接数
        25 segments received      //已接收报文数
        21 segments sent out      //已发送报文数
        4 segments retransmitted    //重传报文数                 #4 次重传
        0 bad segments received      //错误报文数
        0 resets sent          //发出的连接重置数
    Udp:
        0 packets received
        ...
    TcpExt:
        11 resets received for embryonic SYN_RECV sockets  //半连接重置数 #11 次半连接重置
        0 packet headers predicted
        TCPTimeouts: 7    //超时数                     #7 次超时
        TCPSynRetrans: 4  //SYN重传数                  #4 次 SYN 重传
      ...

    这个结果告诉我们,TCP 协议有多次超时和失败重试,并且主要错误是半连接重置。换句话说,主要的失败,都是三次握手失败。

-------------------------------------------------------------------------------------------------------------
48 | 案例篇:服务器总是时不时丢包,我该怎么办?(下)

#排查 iptables 和内核的连接跟踪机制

除了网络层和传输层的各种协议,iptables 和内核的连接跟踪机制也可能会导致丢包。


我已经在 如何优化 NAT 性能  文章中,给你讲过连接跟踪的优化思路。
要确认是不是连接跟踪导致的问题,其实只需要对比当前的连接跟踪数和最大连接跟踪数即可。

连接跟踪在 Linux 内核中是全局的(不属于网络命名空间),我们需要退出容器终端,回到主机中来查看。

# 主机终端中查询内核配置
$ sysctl net.netfilter.nf_conntrack_max
    net.netfilter.nf_conntrack_max = 262144
$ sysctl net.netfilter.nf_conntrack_count
    net.netfilter.nf_conntrack_count = 182      #连接跟踪数只有 182,而最大连接跟踪数则是 262144。显然,这里的丢包,不可能是连接跟踪导致的。


iptables 
更简单的方法,就是直接查询 DROP 和 REJECT 等规则的统计信息,看看是否为 0。如果统计值不是 0 ,再把相关的规则拎出来进行分析。

iptables -nvL 命令,查看各条规则的统计信息。


root@nginx:/# iptables -t filter -nvL       # 在容器中执行
    Chain INPUT (policy ACCEPT 25 packets, 1000 bytes)
     pkts bytes target     prot opt in     out     source               destination
        6   240 DROP       all  --  *      *       0.0.0.0/0            0.0.0.0/0            statistic mode random probability 0.29999999981
    Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
     pkts bytes target     prot opt in     out     source               destination
    Chain OUTPUT (policy ACCEPT 15 packets, 660 bytes)
     pkts bytes target     prot opt in     out     source               destination
        6   264 DROP       all  --  *      *       0.0.0.0/0            0.0.0.0/0            statistic mode random probability 0.29999999981

    可以看到,两条 DROP 规则的统计数值不是 0,它们分别在 INPUT 和 OUTPUT 链中。这两条规则实际上是一样的,指的是使用 statistic 模块,进行随机 30% 的丢包。
    再观察一下它们的匹配规则。0.0.0.0/0 表示匹配所有的源 IP 和目的 IP,也就是会对所有包都进行随机 30% 的丢包。


修复:删除这两条 DROP 规则
root@nginx:/# iptables -t filter -D INPUT -m statistic --mode random --probability 0.30 -j DROP
root@nginx:/# iptables -t filter -D OUTPUT -m statistic --mode random --probability 0.30 -j DROP




$ hping3 -c 10 -S -p 80 192.168.0.30    #修复后测试,hping3结果已经ok
HPING 192.168.0.30 (eth0 192.168.0.30): S set, 40 headers + 0 data bytes
len=44 ip=192.168.0.30 ttl=63 DF id=0 sport=80 flags=SA seq=0 win=5120 rtt=11.9 ms
len=44 ip=192.168.0.30 ttl=63 DF id=0 sport=80 flags=SA seq=1 win=5120 rtt=7.8 ms
...
len=44 ip=192.168.0.30 ttl=63 DF id=0 sport=80 flags=SA seq=9 win=5120 rtt=15.0 ms
--- 192.168.0.30 hping statistic ---
10 packets transmitted, 10 packets received, 0% packet loss
round-trip min/avg/max = 3.3/7.9/15.0 ms



-------------------------------------------------------------------------------------------------------------
接下来用curl命令验证业务情况,结果发现超时
$ curl --max-time 3 http://192.168.0.30
curl: (28) Operation timed out after 3000 milliseconds with 0 bytes received

在服务端的容器veth pair设备上抓包,抓包现象比较明显:3次握手没问题,但是发送http GET请求,服务端一直无响应(wireshark查看更清晰)
[root@yefeng ~]# tcpdump -nnvvi veth524ff9a host 192.168.1.88 and tcp port 80
    tcpdump: listening on veth524ff9a, link-type EN10MB (Ethernet), capture size 262144 bytes
    21:42:29.052572 IP (tos 0x0, ttl 63, id 37145, offset 0, flags [DF], proto TCP (6), length 60)
        192.168.1.88.60802 > 172.17.0.2.80: Flags [S], cksum 0x1bba (correct), seq 1719104784, win 64240, options [mss 1460,sackOK,TS val 2898873629 ecr 0,nop,wscale 7], length 0
    21:42:29.052779 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 60)
        172.17.0.2.80 > 192.168.1.88.60802: Flags [S.], cksum 0x6e42 (incorrect -> 0x30b5), seq 2397574152, ack 1719104785, win 65392, options [mss 256,sackOK,TS val 2607000787 ecr 2898873629,nop,wscale 7], length 0
    21:42:29.053012 IP (tos 0x0, ttl 63, id 37146, offset 0, flags [DF], proto TCP (6), length 52)
        192.168.1.88.60802 > 172.17.0.2.80: Flags [.], cksum 0x5847 (correct), seq 1, ack 1, win 502, options [nop,nop,TS val 2898873630 ecr 2607000787], length 0
    21:42:29.053014 IP (tos 0x0, ttl 63, id 37147, offset 0, flags [DF], proto TCP (6), length 129)
        192.168.1.88.60802 > 172.17.0.2.80: Flags [P.], cksum 0xefad (correct), seq 1:78, ack 1, win 502, options [nop,nop,TS val 2898873630 ecr 2607000787], length 77: HTTP, length: 77
            GET / HTTP/1.1
            User-Agent: curl/7.29.0
            Host: 192.168.1.150
            Accept: */*

    21:42:29.260432 IP (tos 0x0, ttl 63, id 37148, offset 0, flags [DF], proto TCP (6), length 129)
        192.168.1.88.60802 > 172.17.0.2.80: Flags [P.], cksum 0xeede (correct), seq 1:78, ack 1, win 502, options [nop,nop,TS val 2898873837 ecr 2607000787], length 77: HTTP, length: 77
            GET / HTTP/1.1
            User-Agent: curl/7.29.0
            Host: 192.168.1.150
            Accept: */*

    21:42:29.468972 IP (tos 0x0, ttl 63, id 37149, offset 0, flags [DF], proto TCP (6), length 129)
        192.168.1.88.60802 > 172.17.0.2.80: Flags [P.], cksum 0xee0e (correct), seq 1:78, ack 1, win 502, options [nop,nop,TS val 2898874045 ecr 2607000787], length 77: HTTP, length: 77
            GET / HTTP/1.1
            User-Agent: curl/7.29.0
            Host: 192.168.1.150
            Accept: */*

    21:42:29.876434 IP (tos 0x0, ttl 63, id 37150, offset 0, flags [DF], proto TCP (6), length 129)
        192.168.1.88.60802 > 172.17.0.2.80: Flags [P.], cksum 0xec76 (correct), seq 1:78, ack 1, win 502, options [nop,nop,TS val 2898874453 ecr 2607000787], length 77: HTTP, length: 77
            GET / HTTP/1.1
            User-Agent: curl/7.29.0
            Host: 192.168.1.150
            Accept: */*

    21:42:30.740512 IP (tos 0x0, ttl 63, id 37151, offset 0, flags [DF], proto TCP (6), length 129)
        192.168.1.88.60802 > 172.17.0.2.80: Flags [P.], cksum 0xe916 (correct), seq 1:78, ack 1, win 502, options [nop,nop,TS val 2898875317 ecr 2607000787], length 77: HTTP, length: 77
            GET / HTTP/1.1
            User-Agent: curl/7.29.0
            Host: 192.168.1.150
            Accept: */*

    21:42:32.054218 IP (tos 0x0, ttl 63, id 37152, offset 0, flags [DF], proto TCP (6), length 52)
        192.168.1.88.60802 > 172.17.0.2.80: Flags [F.], cksum 0x4c40 (correct), seq 78, ack 1, win 502, options [nop,nop,TS val 2898876631 ecr 2607000787], length 0
    21:42:32.054310 IP (tos 0x0, ttl 64, id 33311, offset 0, flags [DF], proto TCP (6), length 64)
        172.17.0.2.80 > 192.168.1.88.60802: Flags [.], cksum 0x6e46 (incorrect -> 0x5ec0), seq 1, ack 1, win 511, options [nop,nop,TS val 2607003789 ecr 2898873630,nop,nop,sack 1 {78:79}], length 0
    21:42:32.054507 IP (tos 0x0, ttl 63, id 37153, offset 0, flags [DF], proto TCP (6), length 129)
        192.168.1.88.60802 > 172.17.0.2.80: Flags [P.], cksum 0xd83a (correct), seq 1:78, ack 1, win 502, options [nop,nop,TS val 2898876631 ecr 2607003789], length 77: HTTP, length: 77
            GET / HTTP/1.1
            User-Agent: curl/7.29.0
            Host: 192.168.1.150
            Accept: */*

    21:42:32.261066 IP (tos 0x0, ttl 63, id 37154, offset 0, flags [DF], proto TCP (6), length 129)
        192.168.1.88.60802 > 172.17.0.2.80: Flags [P.], cksum 0xd76b (correct), seq 1:78, ack 1, win 502, options [nop,nop,TS val 2898876838 ecr 2607003789], length 77: HTTP, length: 77
            GET / HTTP/1.1
            User-Agent: curl/7.29.0
            Host: 192.168.1.150
            Accept: */*

    21:42:32.668406 IP (tos 0x0, ttl 63, id 37155, offset 0, flags [DF], proto TCP (6), length 129)
        192.168.1.88.60802 > 172.17.0.2.80: Flags [P.], cksum 0xd5d4 (correct), seq 1:78, ack 1, win 502, options [nop,nop,TS val 2898877245 ecr 2607003789], length 77: HTTP, length: 77
            GET / HTTP/1.1
            User-Agent: curl/7.29.0
            Host: 192.168.1.150
            Accept: */*

    21:42:33.492459 IP (tos 0x0, ttl 63, id 37156, offset 0, flags [DF], proto TCP (6), length 129)
        192.168.1.88.60802 > 172.17.0.2.80: Flags [P.], cksum 0xd29c (correct), seq 1:78, ack 1, win 502, options [nop,nop,TS val 2898878069 ecr 2607003789], length 77: HTTP, length: 77
            GET / HTTP/1.1
            User-Agent: curl/7.29.0
            Host: 192.168.1.150
            Accept: */*

再次查看网卡的链路层数据,发现dorpped有增长
[root@yefeng ~]# docker exec -it nginx netstat -i
    Kernel Interface table
    Iface      MTU    RX-OK RX-ERR RX-DRP RX-OVR    TX-OK TX-ERR TX-DRP TX-OVR Flg
    eth0       100      316      0     42 0           147      0      0      0 BMRU
    lo       65536        0      0      0 0             0      0      0      0 LRU
    [root@yefeng ~]# docker exec -it nginx ip -s a s eth0
    28: eth0@if29: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 100 qdisc noqueue state UP group default qlen 1000
        link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
        inet 172.17.0.2/16 scope global eth0
           valid_lft forever preferred_lft forever
        RX: bytes  packets  errors  dropped overrun mcast
        18003      316      0       42      0       0
        TX: bytes  packets  errors  dropped carrier collsns
        8422       147      0       0       0       0
    [root@yefeng ~]# docker exec -it nginx ifconfig
    eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 100
            inet 172.17.0.2  netmask 255.255.0.0  broadcast 0.0.0.0
            ether 02:42:ac:11:00:02  txqueuelen 1000  (Ethernet)
            RX packets 316  bytes 18003 (17.5 KiB)
            RX errors 0  dropped 42  overruns 0  frame 0
            TX packets 147  bytes 8422 (8.2 KiB)
            TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0



总结一下:奇怪的现象,hping3测试不丢包,curl测试却丢包了
    根据抓包分析,也就是HTTP GET请求丢包了,而且稳定丢
然后神器的发现了eth0的mtu只有100...

[root@yefeng ~]# docker exec -it nginx ifconfig eth0 mtu 1500
修复mtu后再次curl测试,问题彻底修复
容器丢包案例:1.链路层tc规则随机丢包;2.iptable3规则随机丢包;3.mtu问题导致http GET请求被丢包

 49 | 案例篇:内核线程 CPU 利用率太高,我该怎么办?

内核线程概述;常见的内核线程
===================================================================================
在排查网络问题时,我们还经常碰到的一个问题,就是内核线程的 CPU 使用率很高。
    比如,在高并发的场景中,内核线程 ksoftirqd 的 CPU 使用率通常就会比较高。

要分析 ksoftirqd 这类 CPU 使用率比较高的内核线程,一般需要借助于其他性能工具,进行辅助分析。
    以 ksoftirqd 为例,如果你怀疑是网络问题,就可以用 sar、tcpdump 等分析网络流量,进一步确认网络问题的根源。
    
    本案例推荐使用perf+火焰图在综合分析
    
----------------------------
内核线程
Linux 在启动过程中,有三个特殊的进程,也就是 PID 号最小的三个进程。
    0 号进程为 idle 进程,这也是系统创建的第一个进程,它在初始化 1 号和 2 号进程后,演变为空闲任务。当 CPU 上没有其他任务执行时,就会运行它。
    1 号进程为 init 进程,通常是 systemd 进程,在用户态运行,用来管理其他用户态进程。
    2 号进程为 kthreadd 进程,在内核态运行,用来管理内核线程。

 ps 命令,来查找 kthreadd 的子进程
    $ ps -f --ppid 2 -p 2   #方式1
    UID         PID   PPID  C STIME TTY          TIME CMD
    root          2      0  0 12:02 ?        00:00:01 [kthreadd]
    root          9      2  0 12:02 ?        00:00:21 [ksoftirqd/0]
    root         10      2  0 12:02 ?        00:11:47 [rcu_sched]
    root         11      2  0 12:02 ?        00:00:18 [migration/0]
    ...
    root      11094      2  0 14:20 ?        00:00:00 [kworker/1:0-eve]
    root      11647      2  0 14:27 ?        00:00:00 [kworker/0:2-cgr]

    $ ps -ef | grep "\[.*\]"    #方式2,因为内核线程的名称(CMD)都在中括号里
    root         2     0  0 08:14 ?        00:00:00 [kthreadd]
    root         3     2  0 08:14 ?        00:00:00 [rcu_gp]
    root         4     2  0 08:14 ?        00:00:00 [rcu_par_gp]
    ...

-------------------------------------------------------------------
常见的内核线程:
    kswapd0:用于内存回收。在  Swap 变高 案例中,我曾介绍过它的工作原理。
    kworker:用于执行内核工作队列,分为绑定 CPU (名称格式为 kworker/CPU86330)和未绑定 CPU(名称格式为 kworker/uPOOL86330)两类。
    migration:在负载均衡过程中,把进程迁移到 CPU 上。每个 CPU 都有一个 migration 内核线程。
    jbd2/sda1-8:jbd 是 Journaling Block Device 的缩写,用来为文件系统提供日志功能,以保证数据的完整性;名称中的 sda1-8,表示磁盘分区名称和设备号。每个使用了 ext4 文件系统的磁盘分区,都会有一个 jbd2 内核线程。
    pdflush:用于将内存中的脏页(被修改过,但还未写入磁盘的文件页)写入磁盘(已经在 3.10 中合并入了 kworker 中)。
内核线程概述;常见的内核线程
DOS攻击导致内核线程 CPU 利用率太高的案例:perf工具;FlameGraph(火焰图)
==============================================================================================================================
运行案例容器
$ docker run -itd --name=nginx -p 80:80 nginx   # 运行Nginx服务并对外开放80端口

[root@yefeng ~]# curl http://192.168.1.150      #curl命令进行测试,返回正常结果

使用hping3命令发起攻击
[root@yefeng ~]# hping3 -S -p 80 -i u1 192.168.1.150    
    # -S参数表示设置TCP协议的SYN(同步序列号),-p表示目的端口为80
    # -i u10表示每隔10微秒发送一个网络帧
    # 注:如果你在实践过程中现象不明显,可以尝试把10调小,比如调成5甚至1

--------------------------------------------------------------------------------
排查过程:
    回到server端,你应该就会发现异常-----系统的响应明显变慢了

$ top   
    top - 08:31:43 up 17 min,  1 user,  load average: 0.00, 0.00, 0.02
    Tasks: 128 total,   1 running,  69 sleeping,   0 stopped,   0 zombie
    %Cpu0  :  0.3 us,  0.3 sy,  0.0 ni, 66.8 id,  0.3 wa,  0.0 hi, 32.4 si,  0.0 st     #软中断数据比较高
    %Cpu1  :  0.0 us,  0.3 sy,  0.0 ni, 65.2 id,  0.0 wa,  0.0 hi, 34.5 si,  0.0 st
    KiB Mem :  8167040 total,  7234236 free,   358976 used,   573828 buff/cache
    KiB Swap:        0 total,        0 free,        0 used.  7560460 avail Mem
      PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
        9 root      20   0       0      0      0 S   7.0  0.0   0:00.48 ksoftirqd/0     #
       18 root      20   0       0      0      0 S   6.9  0.0   0:00.56 ksoftirqd/1
     2489 root      20   0  876896  38408  21520 S   0.3  0.5   0:01.50 docker-containe
     3008 root      20   0   44536   3936   3304 R   0.3  0.0   0:00.09 top
        1 root      20   0   78116   9000   6432 S   0.0  0.1   0:11.77 systemd
    #两个 CPU 的软中断使用率都超过了 30%;而 CPU 使用率最高的进程,正好是软中断内核线程 ksoftirqd/0 和 ksoftirqd/1。

    [root@yefeng ~]# awk '{print $1,$2,$3}' /proc/softirqs  #可以看到NET_RX数据是变更较大的


***对于普通进程,我们要观察其行为有很多方法,比如 strace、pstack、lsof 等等。但这些工具并不适合内核线程,
比如,如果你用 pstack ,或者通过 /proc/pid/stack 查看 ksoftirqd/0(进程号为 9)的调用栈时,分别可以得到以下输出:
    $ pstack 9          #pstack 报出的是不允许挂载进程的错误;
    Could not attach to target 9: Operation not permitted.
    detach: No such process
    $ cat /proc/9/stack     # /proc/9/stack 方式虽然有输出,但输出中并没有详细的调用栈情况。
    [<0>] smpboot_thread_fn+0x166/0x170
    [<0>] kthread+0x121/0x140
    [<0>] ret_from_fork+0x35/0x40
    [<0>] 0xffffffffffffffff


perf 可以对指定的进程或者事件进行采样,并且还可以用调用栈的形式,输出整个调用链上的汇总信息。

$ perf record -a -g -p 9 -- sleep 30        #-p采集指定进程,采样30s后退出
$ perf report                               #进行分析



Samples: 1K of event 'cpu-clock', Event count (approx.): 342750000
  Children      Self  Command      Shared Object      Symbol
+  100.00%     0.00%  ksoftirqd/1  [kernel.kallsyms]  [k] ret_from_fork                                      
-  100.00%     0.00%  ksoftirqd/1  [kernel.kallsyms]  [k] kthread                                            
   - kthread                                                                                                 
      - 99.93% smpboot_thread_fn                                                                             
         - 99.27% run_ksoftirqd                                                                              
            - 99.20% __softirqentry_text_start                                                               
               - net_rx_action              #接收数据包                                                      
                  - 99.12% __napi_poll                                                                       
                     - 95.26% process_backlog                                                                
                        - 95.04% __netif_receive_skb                                                         
                           - __netif_receive_skb_one_core                                                    
                              - 87.24% __netif_receive_skb_core                                              
                                 - 86.94% br_handle_frame            #数据包经过了网桥                       
                                    - br_handle_frame_finish                                                 
                                       - 86.36% br_pass_frame_up     #表明网桥处理后,再交给桥接的其他桥接网卡进一步处理。比如,在新的网卡上接收网络包、执行 netfilter 过滤规则等等。
                                          - 86.21% br_netif_receive_skb                                  
                                               netif_receive_skb     #接收数据包                         
                                             + __netif_receive_skb                                       
                              + 7.51% ip_rcv                                                             
                     + 3.65% e1000_clean                                                                 
         + 0.66% schedule                                                                                
+   99.93%     0.00%  ksoftirqd/1  [kernel.kallsyms]  [k] smpboot_thread_fn  




从这个图中,你可以清楚看到 ksoftirqd 执行最多的调用过程。虽然你可能不太熟悉内核源码,但通过这些函数,我们可以大致看出它的调用栈过程。
    net_rx_action 和 netif_receive_skb,表明这是接收网络包(rx 表示 receive)。
    br_handle_frame ,表明网络包经过了网桥(br 表示 bridge)。
    br_nf_pre_routing ,表明在网桥上执行了 netfilter 的 PREROUTING(nf 表示 netfilter)。而我们已经知道 PREROUTING 主要用来执行 DNAT,所以可以猜测这里有 DNAT 发生。
        可能内核版本不一致吧,没有找到案例中的"br_nf_pre_routing"函数
    br_pass_frame_up,表明网桥处理后,再交给桥接的其他桥接网卡进一步处理。比如,在新的网卡上接收网络包、执行 netfilter 过滤规则等等。



虽然perf工具很好,但是火焰图观察汇总结果更为直观
    要理解火焰图,我们最重要的是区分清楚横轴和纵轴的含义。
        1.横轴表示采样数和采样比例。一个函数占用的横轴越宽,就代表它的执行时间越长。同一层的多个函数,则是按照字母来排序。
        2.纵轴表示调用栈,由下往上根据调用关系逐个展开。换句话说,上下相邻的两个函数中,下面的函数,是上面函数的父函数。这样,调用栈越深,纵轴就越高。
        3.另外,要注意图中的颜色,并没有特殊含义,只是用来区分不同的函数。
    火焰图是动态的矢量图格式,所以它还支持一些动态特性。比如,鼠标悬停到某个函数上时,就会自动显示这个函数的采样数和采样比例。而当你用鼠标点击函数时,火焰图就会把该层及其上的各层放大,方便你观察这些处于火焰图顶部的调用栈的细节。


如果我们根据性能分析的目标来划分,火焰图可以分为下面这几种。
    1.on-CPU 火焰图:表示 CPU 的繁忙情况,用在 CPU 使用率比较高的场景中。
    2.off-CPU 火焰图:表示 CPU 等待 I/O、锁等各种资源的阻塞情况。
    3.内存火焰图:表示内存的分配和释放情况。
    4.热 / 冷火焰图:表示将 on-CPU 和 off-CPU 结合在一起综合展示。
    5.差分火焰图:表示两个火焰图的差分情况,红色表示增长,蓝色表示衰减。差分火焰图常用来比较不同场景和不同时期的火焰图,以便分析系统变化前后对性能的影响情况。




安装FlameGraph
    $ git clone https://github.com/brendangregg/FlameGraph
    $ cd FlameGraph
生成火焰图,其实主要需要三个步骤:
    1.执行 perf script ,将 perf record 的记录转换成可读的采样记录;
    2.执行 stackcollapse-perf.pl 脚本,合并调用栈信息;
    3.执行 flamegraph.pl 脚本,生成火焰图。
    #在 Linux 中,我们可以使用管道,来简化这三个步骤的执行过程。假设刚才用 perf record 生成的文件路径为 /root/perf.data,执行下面的命令,你就可以直接生成火焰图:
    perf script -i /root/perf.data | ./stackcollapse-perf.pl --all |  ./flamegraph.pl > ksoftirqd.svg
使用浏览器打开 ksoftirqd.svg ,你就可以看到生成的火焰图了
DOS攻击导致内核线程 CPU 利用率太高的案例:perf工具;FlameGraph(火焰图)

50 | 案例篇:动态追踪怎么用?(上)
51 | 案例篇:动态追踪怎么用?(下)

strace和ptrace
===========================================
 strace 简单易用的好处,却忽略了它对进程性能带来的影响。从原理上来说,strace 基于系统调用 ptrace 实现,这就带来了两个问题。
    1.由于 ptrace 是系统调用,就需要在内核态和用户态切换。当事件数量比较多时,繁忙的切换必然会影响原有服务的性能;
    2.ptrace 需要借助 SIGSTOP 信号挂起目标进程。这种信号控制和进程挂起,会影响目标进程的行为。
所以,在性能敏感的应用(比如数据库)中,我并不推荐你用 strace (或者其他基于 ptrace 的性能工具)去排查和调试。
strace和ptrace(不推荐)
动态追踪技术概述
============================================================================
动态追踪技术,通过探针机制,来采集内核或者应用程序的运行信息,从而可以不用修改内核和应用程序的代码,就获得丰富的信息,帮你分析、定位想要排查的问题。
    相比以往的进程级跟踪方法(比如 ptrace),动态追踪往往只会带来很小的性能损耗(通常在 5% 或者更少)。

根据事件类型的不同,动态追踪所使用的事件源,可以分为静态探针、动态探针以及硬件事件等三类。
    硬件事件通常由性能监控计数器 PMC(Performance Monitoring Counter)产生,包括了各种硬件的性能情况,比如 CPU 的缓存、指令周期、分支预测等等。
    静态探针,是指事先在代码中定义好,并编译到应用程序或者内核中的探针。
    动态探针,则是指没有事先在代码中定义,但却可以在运行时动态添加的探针
    
ftrace 最早用于函数跟踪,后来又扩展支持了各种事件跟踪功能。
perf 是我们的老朋友了,我们在前面的好多案例中,都使用了它的事件记录和分析功能,这实际上只是一种最简单的静态跟踪机制。你也可以通过 perf ,来自定义动态事件(perf probe),只关注真正感兴趣的事件。
eBPF 则在 BPF(Berkeley Packet Filter)的基础上扩展而来,不仅支持事件跟踪机制,还可以通过自定义的 BPF 代码(使用 C 语言)来自由扩展。所以,eBPF 实际上就是常驻于内核的运行时,可以说就是 Linux 版的 DTrace。
最常见的就是前面提到的 SystemTap,我们之前多次使用过的 BCC(BPF Compiler Collection),以及常用于容器性能分析的 sysdig 等。
动态追踪技术概述
ftrace 和 trace-cmd工具;perf 和 perf trace;eBPF 和 BCC;SystemTap 和 sysdig(待学习)
ftrace 和 trace-cmd工具;perf 和 perf trace;eBPF 和 BCC;SystemTap 和 sysdig(待学习)

 52 | 案例篇:服务吞吐量下降很厉害,怎么分析?

服务吞吐量下降的案例:net.netfilter.nf_conntrack_count;内存、CPU资源过少;net.core.somaxconn;net.ipv4.ip_local_port_range;net.ipv4.tcp_tw_reuse
==============================================================================================================================
本案例问题汇总:
1.连接跟踪数太少
2.内存、CPU资源过少
3.套接字监听队列太短
4.设备client时的临时端口太少
5.火焰图分析,系统函数__init_check_established消耗了大量的CPU资源,需要开启端口号的重用

----------------------------------------------
$ docker run --name nginx --network host --privileged -itd feisky/nginx-tp
$ docker run --name phpfpm --network host --privileged -itd feisky/php-fpm-tp


$ docker ps     #容器已经处于运行状态(Up)
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS               NAMES
09e0255159f0        feisky/php-fpm-tp   "php-fpm -F --pid /o…"   28 seconds ago      Up 27 seconds                           phpfpm
6477c607c13b        feisky/nginx-tp     "/init.sh"               29 seconds ago      Up 28 seconds                           nginx

$ curl http://192.168.0.30


$ wrk --latency -c 1000 http://192.168.0.30     ## 默认测试时间为10s,请求超时2s
    Running 10s test @ http://192.168.0.30
      2 threads and 1000 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency    14.82ms   42.47ms 874.96ms   98.43%
        Req/Sec   550.55      1.36k    5.70k    93.10%
      Latency Distribution
         50%   11.03ms
         75%   15.90ms
         90%   23.65ms
         99%  215.03ms
      1910 requests in 10.10s, 573.56KB read    #一共1910个请求
      Non-2xx or 3xx responses: 1910            #所有 1910 个请求收到的都是异常响应(非 2xx 或 3xx)
    Requests/sec:    189.10     #吞吐量(也就是每秒请求数)只有 189
    Transfer/sec:     56.78KB
    可以看到wrk的测试结果,数据很差
    
----------------------------------------------------------------------------------------------
开始测试并优化
$ wrk --latency -c 1000 -d 1800 http://192.168.0.30 ## 测试时间30分钟
    
server端观察连接数
$ ss -s
    Total: 177 (kernel 1565)
    TCP:   1193 (estab 5, closed 1178, orphaned 0, synrecv 0, timewait 1178/0), ports 0
    Transport Total     IP        IPv6
    *    1565      -         -
    RAW    1         0         1
    UDP    2         2         0
    TCP    15        12        3
    INET    18        14        4
    FRAG    0         0         0
    #wrk 并发 1000 请求时,建立连接数只有 5,而 closed 和 timewait 状态的连接则有 1100 多 
    #一个是建立连接数太少了
    #一个是 timewait 状态连接太多了


先来看第二个关于 timewait 的问题
内核中的连接跟踪模块,有可能会导致 timewait 问题。
我们今天的案例还是基于 Docker 运行,而 Docker 使用的 iptables ,就会使用连接跟踪模块来管理 NAT。
确认是不是连接跟踪导致的问题的方法:
    通过 dmesg 查看系统日志,如果有连接跟踪出了问题,应该会看到 nf_conntrack 相关的日志。
$ dmesg | tail
[88356.354329] nf_conntrack: nf_conntrack: table full, dropping packet
[88356.354374] nf_conntrack: nf_conntrack: table full, dropping packet  #说明正是连接跟踪导致的问题

$ sysctl net.netfilter.nf_conntrack_max     #连接跟踪数的最大限制
net.netfilter.nf_conntrack_max = 200
$ sysctl net.netfilter.nf_conntrack_count   #当前的连接跟踪数
net.netfilter.nf_conntrack_count = 200

$ sysctl -w net.netfilter.nf_conntrack_max=1048576  #将 nf_conntrack_max 增大


-----------------------------------------
server端再次进行测试
$ wrk --latency -c 1000 http://192.168.0.30     # 默认测试时间为10s,请求超时2s
    ...
      54221 requests in 10.07s, 15.16MB read
      Socket errors: connect 0, read 7, write 0, timeout 110
      Non-2xx or 3xx responses: 45577   #10s 内的总请求数虽然增大到了 5 万,但是有 4 万多响应异常,说白了,真正成功的只有 8000 多个(54221-45577=8644)。
    Requests/sec:   5382.21             #连接跟踪的优化效果非常好,吞吐量已经从刚才的 189 增大到了 5382。看起来性能提升了将近 30 倍
    Transfer/sec:      1.50MB

响应的状态码并不是我们期望的 2xx (表示成功)或 3xx(表示重定向)
这种情况下,搞清楚 Nginx 真正的响应就很重要了。
查询 Nginx 容器日志
$ docker logs nginx --tail 3
    192.168.0.2 - - [15/Mar/2019:2243:27 +0000] "GET / HTTP/1.1" 499 0 "-" "-" "-"  #499 并非标准的 HTTP 状态码,而是由 Nginx 扩展而来,表示服务器端还没来得及响应时,客户端就已经关闭连接了。
    192.168.0.2 - - [15/Mar/2019:22:43:27 +0000] "GET / HTTP/1.1" 499 0 "-" "-" "-"
    192.168.0.2 - - [15/Mar/2019:22:43:27 +0000] "GET / HTTP/1.1" 499 0 "-" "-" "-"
    #问题在于服务器端处理太慢,客户端因为超时(wrk 超时时间为 2s),主动断开了连接。


案例本身是 Nginx+PHP 的应用,那是不是可以猜测,是因为 PHP 处理过慢呢?
$ docker logs phpfpm --tail 5
    [15-Mar-2019 22:28:56] WARNING: [pool www] server reached max_children setting (5), consider raising it
    [15-Mar-2019 22:43:17] WARNING: [pool www] server reached max_children setting (5), consider raising it
    #从这个日志中,我们可以看到两条警告信息,server reached max_children setting (5),并建议增大 max_children。
    max_children 表示 php-fpm 子进程的最大数量,当然是数值越大,可以同时处理的请求数就越多。
    不过由于这是进程问题,数量增大,也会导致更多的内存和 CPU 占用。所以,我们还不能设置得过大。

一般来说,每个 php-fpm 子进程可能会占用 20 MB 左右的内存。所以,你可以根据内存和 CPU 个数,估算一个合理的值。
这儿我把它设置成了 20,并将优化后的配置重新打包成了 Docker 镜像。你可以执行下面的命令来执行它:
# 停止旧的容器
$ docker rm -f nginx phpfpm     
# 使用新镜像启动Nginx和PHP
$ docker run --name nginx --network host --privileged -itd feisky/nginx-tp:1
$ docker run --name phpfpm --network host --privileged -itd feisky/php-fpm-tp:1

那么问题2其实是PHP容器的子进程数量过少,应付不了这么高的并发导致的


-----------------------------------------
server端再次进行测试
$ wrk --latency -c 1000 http://192.168.0.30     # 默认测试时间为10s,请求超时2s
    ...
      47210 requests in 10.08s, 12.51MB read
      Socket errors: connect 0, read 4, write 0, timeout 91
      Non-2xx or 3xx responses: 31692       #但是测试期间成功的请求数却多了不少,从原来的 8000,增长到了 15000(47210-31692=15518)。
    Requests/sec:   4683.82         #吞吐量只有 4683,比刚才的 5382 少了一些;
    Transfer/sec:      1.24MB
    虽然性能有所提升,可 4000 多的吞吐量显然还是比较差的,并且大部分请求的响应依然还是异常。


重新运行测试,这次还是要用 -d 参数延长测试时间,以便模拟性能瓶颈的现场
$ wrk --latency -c 1000 -d 1800 http://192.168.0.30     ## 测试时间30分钟


# 只关注套接字统计
$ netstat -s | grep socket
    73 resets received for embryonic SYN_RECV sockets
    308582 TCP sockets finished time wait in fast timer
    8 delayed acks further delayed because of locked socket
    290566 times the listen queue of a socket overflowed
    290566 SYNs to LISTEN sockets dropped
# 稍等一会,再次运行
$ netstat -s | grep socket
    73 resets received for embryonic SYN_RECV sockets
    314722 TCP sockets finished time wait in fast timer
    8 delayed acks further delayed because of locked socket
    344440 times the listen queue of a socket overflowed        #有大量的套接字丢包,并且丢包都是套接字队列溢出导致的
    344440 SYNs to LISTEN sockets dropped                       #有大量的套接字丢包,并且丢包都是套接字队列溢出导致的

这说明要么测试并发过高,要么服务端的tcp半连接队列太短
接下来,我们应该分析连接队列的大小是不是有异常。
$ ss -ltnp
State     Recv-Q     Send-Q            Local Address:Port            Peer Address:Port
LISTEN    10         10                      0.0.0.0:80                   0.0.0.0:*         users:(("nginx",pid=10491,fd=6),("nginx",pid=10490,fd=6),("nginx",pid=10487,fd=6))
LISTEN    7          10                            *:9000                       *:*         users:(("php-fpm",pid=11084,fd=9),...,("php-fpm",pid=10529,fd=7))
Nginx 和 php-fpm 的监听队列 (Send-Q)只有 10,而 nginx 的当前监听队列长度 (Recv-Q)已经达到了最大值,php-fpm 也已经接近了最大值。
很明显,套接字监听队列的长度太小了,需要增大。

关于套接字监听队列长度的设置,既可以在应用程序中,通过套接字接口调整,也支持通过内核选项来配置。


$ docker exec nginx cat /etc/nginx/nginx.conf | grep backlog        #
        listen       80  backlog=10;
$ docker exec phpfpm cat /opt/bitnami/php/etc/php-fpm.d/www.conf | grep backlog     ## 查询php-fpm监听队列长度
    ; Set listen(2) backlog.
    ;listen.backlog = 511
$ sysctl net.core.somaxconn     ## somaxconn是系统级套接字监听队列上限
    net.core.somaxconn = 10

从输出中可以看到,Nginx 和 somaxconn 的配置都是 10,而 php-fpm 的配置也只有 511,显然都太小了。
解决办法:优化方法就是增大这三个配置,比如,可以把 Nginx 和 php-fpm 的队列长度增大到 8192,而把 somaxconn 增大到 65536# 停止旧的容器
$ docker rm -f nginx phpfpm
# 使用新镜像启动Nginx和PHP
$ docker run --name nginx --network host --privileged -itd feisky/nginx-tp:2
$ docker run --name phpfpm --network host --privileged -itd feisky/php-fpm-tp:2

--------------------------------------------------------------------------
$ wrk --latency -c 1000 http://192.168.0.30
    ...
      62247 requests in 10.06s, 18.25MB read
      Non-2xx or 3xx responses: 62247
    Requests/sec:   6185.65
    Transfer/sec:      1.81MB
    #吞吐量已经增大到了 6185,并且在测试的时候,如果你在终端一中重新执行 netstat -s | grep socket,还会发现,现在已经没有套接字丢包问题了。
    # Nginx 的响应,再一次全部失败了,都是 Non-2xx or 3xx。

查看 Nginx 日志
$ docker logs nginx --tail 10
2019/03/15 16:52:39 [crit] 15#15: *999779 connect() to 127.0.0.1:9000 failed (99: Cannot assign requested address) while connecting to upstream, client: 192.168.0.2, server: localhost, request: "GET / HTTP/1.1", upstream: "fastcgi://127.0.0.1:9000", host: "192.168.0.30"
    #Nginx 报出了无法连接 fastcgi 的错误,错误消息是 Connect 时, Cannot assign requested address。这个错误消息对应的错误代码为 EADDRNOTAVAIL,表示 IP 地址或者端口号不可用。


显然只能是端口号的问题。接下来,我们就来分析端口号的情况
根据网络套接字的原理,当客户端连接服务器端时,需要分配一个临时端口号,而  Nginx 正是 PHP-FPM 的客户端。端口号的范围并不是无限的,最多也只有 6 万多。
查询系统配置的临时端口号范围
$ sysctl net.ipv4.ip_local_port_range       #临时端口的范围只有 50 个,显然太小了
net.ipv4.ip_local_port_range=20000 20050
$ sysctl -w net.ipv4.ip_local_port_range="10000 65535"      #把端口号范围扩展为 “10000 65535”
net.ipv4.ip_local_port_range = 10000 65535


------------------------------------------------
$ wrk --latency -c 1000 http://192.168.0.30/
...
  32308 requests in 10.07s, 6.71MB read
  Socket errors: connect 0, read 2027, write 0, timeout 433
  Non-2xx or 3xx responses: 30
Requests/sec:   3208.58
Transfer/sec:    682.15KB
    #异常的响应少多了 ,不过,吞吐量也下降到了 3208。并且,这次还出现了很多 Socket read errors。显然,还得进一步优化。





前面我们已经优化了很多配置。这些配置在优化网络的同时,却也会带来其他资源使用的上升。这样来看,是不是说明其他资源遇到瓶颈了呢?
$ wrk --latency -c 1000 -d 1800 http://192.168.0.30     # 测试时间30分钟


$ top       #sever端观察
    ...
    %Cpu0  : 30.7 us, 48.7 sy,  0.0 ni,  2.3 id,  0.0 wa,  0.0 hi, 18.3 si,  0.0 st
    %Cpu1  : 28.2 us, 46.5 sy,  0.0 ni,  2.0 id,  0.0 wa,  0.0 hi, 23.3 si,  0.0 st
    KiB Mem :  8167020 total,  5867788 free,   490400 used,  1808832 buff/cache
    KiB Swap:        0 total,        0 free,        0 used.  7361172 avail Mem
      PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
    20379 systemd+  20   0   38068   8692   2392 R  36.1  0.1   0:28.86 nginx
    20381 systemd+  20   0   38024   8700   2392 S  33.8  0.1   0:29.29 nginx
     1558 root      20   0 1118172  85868  39044 S  32.8  1.1  22:55.79 dockerd
    20313 root      20   0   11024   5968   3956 S  27.2  0.1   0:22.78 docker-containe
    13730 root      20   0       0      0      0 I   4.0  0.0   0:10.07 kworker/u4:0-ev
    从 top 的结果中可以看到,可用内存还是很充足的,但系统 CPU 使用率(sy)比较高,两个 CPU 的系统 CPU 使用率都接近 50%,
    且空闲 CPU 使用率只有 2%。再看进程部分,CPU 主要被两个 Nginx 进程和两个 docker 相关的进程占用,使用率都是 30% 左右。

CPU 使用率上升了,该怎么进行分析呢?我想,你已经还记得我们多次用到的 perf,再配合前两节讲过的火焰图,很容易就能找到系统中的热点函数。

# 执行perf记录事件
$ perf record -g
# 切换到FlameGraph安装路径执行下面的命令生成火焰图
$ perf script -i ~/perf.data | ./stackcollapse-perf.pl --all | ./flamegraph.pl > nginx.svg

查看火焰图进行分析
根据我们讲过的火焰图原理,这个图应该从下往上、沿着调用栈中最宽的函数,来分析执行次数最多的函数。
中间的 do_syscall_64、tcp_v4_connect、inet_hash_connect 这个堆栈,很明显就是最需要关注的地方。
inet_hash_connect() 是 Linux 内核中负责分配临时端口号的函数。
所以,这个瓶颈应该还在临时端口的分配上。
    在上一步的“端口号”优化中,临时端口号的范围,已经优化成了 “10000 65535”。这显然是一个非常大的范围,那么,端口号的分配为什么又成了瓶颈呢?
再顺着 inet_hash_connect 往堆栈上面查看,下一个热点是 __init_check_established 函数
__init_check_established 函数的目的,是检查端口号是否可用。

结合这一点,你应该可以想到,如果有大量连接占用着端口,那么检查端口号可用的函数,不就会消耗更多的 CPU 吗?


$ ss -s #查看连接状态统计
TCP:   32775 (estab 1, closed 32768, orphaned 0, synrecv 0, timewait 32768/0), ports 0
...
有大量连接(这儿是 32768)处于 timewait 状态,而 timewait 状态的连接,本身会继续占用端口号。如果这些端口号可以重用,那么自然就可以缩短 __init_check_established 的过程。

$ sysctl net.ipv4.tcp_tw_reuse  #tcp_tw_reuse 选项,用来控制端口号的重用
net.ipv4.tcp_tw_reuse = 0       #tcp_tw_reuse 是 0,也就是禁止状态。

优化方法也很容易,把它设置成 1 就可以开启了。



# 停止旧的容器
$ docker rm -f nginx phpfpm
# 使用新镜像启动Nginx和PHP
$ docker run --name nginx --network host --privileged -itd feisky/nginx-tp:3
$ docker run --name phpfpm --network host --privileged -itd feisky/php-fpm-tp:3
-------------------------------------------------------------------
最终测试
$ wrk --latency -c 1000 http://192.168.0.30/
...
  52119 requests in 10.06s, 10.81MB read
  Socket errors: connect 0, read 850, write 0, timeout 0
Requests/sec:   5180.48
Transfer/sec:      1.07MB
吞吐量已经达到了 5000 多,并且只有少量的 Socket errors,也不再有 Non-2xx or 3xx 的响应了。说明一切终于正常了。
服务吞吐量下降的案例:net.netfilter.nf_conntrack_count;内存、CPU资源过少;net.core.somaxconn;net.ipv4.ip_local_port_range;net.ipv4.tcp_tw_reuse

 53 | 套路篇:系统监控的综合思路

USE 法把系统资源的性能指标,简化成了三个类别,即使用率、饱和度以及错误数。
    使用率,表示资源用于服务的时间或容量百分比。100% 的使用率,表示容量已经用尽或者全部时间都用于服务。
    饱和度,表示资源的繁忙程度,通常与等待队列的长度相关。100% 的饱和度,表示资源无法接受更多的请求。
    错误数表示发生错误的事件个数。错误数越多,表明系统的问题越严重。
使用率、饱和度以及错误数

 

54 | 套路篇:应用监控的一般思路
55 | 套路篇:分析性能问题的一般步骤
56 | 套路篇:优化性能问题的一般方法
57 | 套路篇:Linux 性能工具速查

 

posted @ 2022-07-11 10:56  雲淡風輕333  阅读(195)  评论(0编辑  收藏  举报