中间件业务在网易轻舟容器平台的性能调优实践
在讨论容器化场景的性能调优之前,先谈一下性能调优中的“望闻问切”。对于性能问题,大部分人首先想到的是 CPU 利用率高,但这只是个现象,并不是症状。打个比方:感冒看医生时,病人跟大夫描述的是现象,包括头部发热、流鼻涕等;而大夫通过探查、化验,得到的医学症状是病人的白细胞较多、咽喉红肿等,并确诊为细菌性感冒,给开了 999 感冒灵。性能调优流程与此相似,也需要找到现象、症状和解法。回到 CPU 利用率高的例子:已知现象是 CPU 利用率高,我们通过 strace 检查,发现 futex_wait 系统调用占用了 80% 的 CPU 时间——这才是症状;根据这个症状,我们业务逻辑代码降低了线程切换,CPU 利用率随之降低。
大部分的性能调优都可以通过发现现象、探测症状、解决问题这三个步骤来完成,而这在容器的性能调优中就更为重要的,因为在主机的性能调优过程中,我们有很多的经验可以快速找到症状,但是在容器的场景中,很多客户只能描述问题的现象,因为他们并不了解使用的容器引擎的工作原理以及容器化架构的实现方式。
容器化场景中的性能调优主要面临 7 个方面的挑战。
我们借鉴 redhat tuned 思想,提出一种与业务相关的内核参数配置新框架,它可以把我们对 Linux 系统现有的一些调优手段(包括电源管理工具,CPU、内存、磁盘、网络等内核参数)整合到一个具体的策略 (profile) 中,业务场景不同,profile 不同,以此来快速实现云计算对不同业务进行系统的性能调节的需求。
在 VM 级别我们看到的即是所有,网络栈是完整暴露的,CPU、内存、磁盘等也是完全没有限制的。性能调优老司机的工具箱安个遍,诊断流程走一趟基本问题就查个八九不离十了,但是在容器中,很多时候,都是默认不自带诊断、调优工具的,很多时候连 ping 或者 telnet 等基础命令都没有,这导致大部分情况下我们需要以黑盒的方式看待一个容器,所有的症状只能从物理机或者云主机的链路来看。但是我们知道容器通过 namespace 的隔离,具备完整网络栈,CPU、内存等通过隔离,只能使用 limit 的资源,如果将容器当做黑盒会导致很多时候问题症状难以快速发现,排查问题变难了。
容器的场景带来很多酷炫的功能和技术,比如故障自动恢复、弹性伸缩、跨主机调度等,但是这一切的代价是需要依赖容器化的架构,比如 Kubernetes 网络中需要 FullNat 的方式完成两层网络的转发等,这会给排查问题带来更复杂的障碍,当你不清楚编排引擎的架构实现原理的时候,很难将问题指向这些平时不会遇到的场景。例如上面这个例子中,FullNat 的好处是降低了网络整体方案的复杂性,但是也引入了一些 NAT 场景下的常见问题,比如短连接场景中的 SNAT 五元组重合导致包重传的问题等等,排查问题的方位变大了。
容器本质是一种操作系统级虚拟化技术,不可避免涉及隔离性,虽然平时并不需要考虑隔离的安全性问题,但是当遇到性能调优的时候,内核的共享使我们不得不面对一个更复杂的场景。举个例子,由于内核的共享,系统的 proc 是以只读的方式进行挂载的,这就意味着系统内核参数的调整会带来的宿主机级别的变更。在性能调优领域经常有人提到 C10K 或者 C100K 等类似的问题,这些问题难免涉及到内核参数的调整,但是越特定的场景调优的参数越不同,有时会有“彼之蜜糖,我之毒药”的效果。因此同一个节点上的不同容器会出现非常离奇的现象。
这个问题在大多数场景下无需考虑,列在第四位是期望能够引起大家重视。网易轻舟在一次排查“ES 容器(使用 java 11)将 CPU requests 都配置成 8 时,其性能低于将 request CPU 都配置成 1”的问题时,发现是 Java 的标准库中对 cgroup 的支持不完全导致的,好在这点在大多数场景中没有任何影响。
提到容器架构避不开网络、存储和调度,网络是评判容器架构好坏的一个核心标准,不同的网络方案也会有不同的实现方式与问题。比如网易轻舟 Kubernetes 中使用了 Flannel 的 CNI 插件实现的网络方案,标准 Flannel 支持的 Vxlan 的网络方案,Docker 的 Overlay 的 macVlan,ipvlan 的方案,或者 OpenShift SDN 网路方案,还有网易轻舟自研的云内普通 VPC 和 SRIOV+VPC 方案等等。这些不同的网络方案无一例外都是跨宿主机的二层网络很多都会通过一些 vxlan 封包解包的方式来进行数据传输,这种方式难免会增加额外的 CPU 损耗,这是一种先天的缺欠,并不是调优能够解决的问题。有的时候排查出问题也只能绕过而不是调优。
应用容器化是一个需要特别注意的问题,很多公司并没有严格的配管流程,比如系统依赖的内核版本、语言的小版本等等,很多时候都是选择一个大概的版本,这会带来很多语言层级的 BUG,比如 Java 应用程序运行在容器中可能会遇到更长的应用程序暂停问题、Java 7 无法感知 CPU 个数导致 GC 线程过多问题和 PHP7.0 中 php-fpm 的诡异行为。环境的问题本就需要严格管控,但是当遇到了容器,很多时候我们会分不清哪些不经意的行为会带来严重的问题,警惕性因为容器镜像能够正常启动而降低了。
性能优化通常可以通过如表五个步骤完成:
调优管理变更和性能优化并不直接相关,但可能是性能调优成功最重要的因素。以下总结来源于网易轻舟团队的实践和经验:
在调优之前,实施合理的调优管理流程变更
永远不要在生产系统上调优
压测的环境要相对独立,不能受其他压力的影响,这里说的不仅是压测的服务器端,而且包括压测客户端
在调优过程中,每次只修改一个变量
反复测试提升性能的参数,有时候,统计来的结果更加可靠
把成功的参数调整整理成文档,和社区分享,即使你觉得它们微不足道
生产环境中获得的任何结果对 Linux 性能都有很大用处
对于容器业务来说,尽量让 CPU 访问本地内存,不要访问远端内存。
CPU 访问不同类型节点内存的速度是不相同的,访问本地节点的速度最快,访问远端节点的速度最慢,即访问速度与节点的距离有关,距离越远访问速度越慢,此距离称作 Node Distance。正是因为有这个特点,容器应用程序要尽量的减少不同 Node 模块之间的交互,也就是说,我们根据容器内存 Node 亲和性,选择容器使用的 CPU 固定在一个 Node 模块里,因此其性能将会有很大的提升。有一种特殊场景除外,如果一个容器申请的 request 大于单个 Node 上预留的 CPU 后,这种亲和性的绑定就会失效,此时回归到原始的跨 Node 范围绑定,对于之前已经做了亲和性的容器(申请的 request 小于单个 Node 上预留的 CPU)我们的策略是继续维持不变。
针对不同的场景,可以考虑以下参数着手进行调优。
与 CPU 相关的配置
cpu 相关的参数 | 说明 |
---|---|
force_latency | 如果为 1 的话,表示强制把 cstate 置为 c0 |
governor=performance | 表示开启 intel 睿频技术 |
energy_perf_bias=performance | 通过 x86_energy_perf_policy 工具允许管理员定义相关性能和能效,分为三种:1. performance: 默认设置,不会考虑节省能源 2. normal: 潜在到能耗节省,容忍部分性能妥协。3. powersave: 接收严重性能降级,最大能效。 |
min_perf_pct | 如果设置为 100 的话,其含义是尽量让 CPU 跑在 P0 就是 Turbo 的最高频率,对于 CPU,有一个标称频率(Base Freqency)。比如某台服务器的 CPU 的 Base Freqency 是 3.0GHz,也就是在普通状态下,BIOS 关闭 Turbo 开关的话,CPU 的最高频率不会超过 3.0GHz。Pn 指的是硬件上的最低频率,P1 是 Base Frequency。在这范围的频率就是 P1,P2,P3…Pn,后面的数字越小,就说明频率越高。那么 P1 以上的频率就称为 Turbo 频率,P0 就是 Turbo 的最高频率。Turbo 就是我们所说的超频,就是超出标称频率。 |
kernel.sched_min_granularity_ns | 表示进程最少运行时间,为了防止任务频繁的切换,可以设置得较大些。 |
kernel.sched_migration_cost_ns | 其值越大表示 task 更不容易被迁走,该变量用来判断一个进程是否还是 hot,如果进程的运行时间(now - p->se.exec_start)小于它,那么内核认为它的 code 还在 cache 里,所以该进程还是 hot,那么在迁移的时候就不会考虑它。 |
sysctl_sched_wakeup_granularity | 该变量表示进程被唤醒后至少应该运行的时间的基数,它只是用来判断某个进程是否应该抢占当前进程,并不代表它能够执行的最小时间(sysctl_sched_min_granularity),如果这个数值越小,那么发生抢占的概率也就越高(见 wakeup_gran、wakeup_preempt_entity 函数) |
与内存相关的配置
内存相关的参数 | 说明 |
---|---|
transparent_hugepages(默认值是 madvise) | 设置为 never 表示关闭透明大页 |
vm.dirty_ratio | 表示当系统脏页到达内存 X%,强制同步 WriteBack(WB),如果脏页越多,WB 会变慢,响应就会变慢,所以响应要快的话,此值可以设置得较小些。 |
vm.dirty_background_ratio | 当系统脏页到达内存 X%,强制异步 WB,在低水位的时候就刷的话,刷的就相对越快,所以响应要快的话,此值可以设置得较小些。 |
磁盘相关的参数
磁盘相关的参数 | 说明 |
---|---|
readahead(一般默认值是 256) | Linux 的文件预读 readahead,指 Linux 系统内核将指定文件的某区域预读进页缓存起来,便于接下来对该区域进行读取时,不会因缺页(page fault)而阻塞。因为从内存读取比从磁盘读取要快很多。预读可以有效的减少磁盘的寻道次数和应用程序的 I/O 等待时间,所以业务是磁盘密集型的,此值可以设置得较大些。 |
与 sysctl 相关的配置
sysctl 参数 | 说明 |
---|---|
net.core.busy_read net.core.busy_poll | 这两个参数默认都是 0,它表示是繁忙轮询,有助于减少网络接收路径中的延迟,使 socket 层代码查询网络设备的接收队列并禁用网络中断,这可以消除由于中断和由此产生的环境切换所造成的延误。但是,它会增加 CPU 的使用率。繁忙轮询可以防止 CPU 进入睡眠状态,睡眠状态会造成额外的功耗。 |
net.ipv4.tcp_fastopen | tcp_fastopen(TFO)提高性能的关键是省去了热请求的三次握手,适用于对小包的应用场景进行调优 |
vm.max_map_count kernel.pid_max kernel.threads-max | 这三个参数可以用于调节 os 支持的最大线程数量 |
fs.inotify.max_queued_events fs.inotify.max_user_instances fs.inotify.max_user_watches | max_queued_events 表示调用 inotify_init 时分配给 inotify instance 中可排队的 event 的数目的最大值,超出这个值的事件被丢弃,但会触发 IN_Q_OVERFLOW 事件 ;max_user_instances 表示每一个 real user ID 可创建的 inotify instatnces 的数量上限,默认 128;max_user_watches 表示同一用户同时可以添加的 watch 数目(watch 一般是针对目录,决定了同时同一用户可以监控的目录数量). 调大这三个参数是为了解决“当运行 docker 镜像时,报 Error: ENOSPC: System limit for number of file watchers reached”问题 |
net.core.somaxconn | 定义了系统中每一个端口最大的监听队列的长度 |
net.netfilter.nf_conntrack_max net.netfilter.nf_conntrack_tcp_be_liberal net.netfilter.nf_conntrack_checksum options nf_conntrack hashsize 配合使用 | 调大 netfilter ct 参数,防止连接数过多,导致“nf_conntrack: table full, dropping packet on...”问题 |
net.ipv4.ip_forward | 开启数据包转发的功能,所谓转发即当主机拥有多于一块的网卡时,其中一块收到数据包,根据数据包的目的 ip 地址将数据包发往本机另一块网卡,该网卡根据路由表继续发送数据包 |
net.bridge.bridge-nf-call-iptables net.bridge.bridge-nf-call-arptables net.bridge.bridge-nf-call-ip6tables | 这三个参数置为 1 表示二层的网桥在转发包时也会被 iptables/arptables/ip6tables 的 FORWARD 规则所过滤 |
net.core.netdev_max_backlog | 默认是 1000,在每个网络接口接收数据包的速率比内核处理这些包的速率快时,允许送到队列的数据包的最大数目 |
net.core.netdev_budget | 默认是 300,net_rx_action budget 表示一个 CPU 单次轮询(poll)所允许的最大收包数量,如果在内核缓存层面发现有丢包的话,可以调整 net.core.netdev_max_backlog 和 net.core.netdev_budget 这两个参数 |
net.ipv4.neigh.default.gc_thresh1 net.ipv4.neigh.default.gc_thresh2 net.ipv4.neigh.default.gc_thresh3 | 这三个参数是为了解决“neighbour table overflow”问题 |
net.ipv4.conf.eth0.arp_ignore | 1 表示只响应目的 IP 地址为接收网卡上的本地地址的 arp 请求 |
net.ipv4.tcp_rmem="P1 P2 P3" | 第一个值是为 socket 接收缓冲区分配的最少字节数;第二个值是默认值(该值会被 rmem_default 覆盖),缓冲区在系统负载不重的情况下可以增长到这个值;第三个值是接收缓冲区空间的最大字节数(该值会被 rmem_max 覆盖)。 |
net.ipv4.tcp_wmem="P1 P2 P3" | 第一个值是为 socket 发送缓冲区分配的最少字节数;第二个值是默认值(该值会被 wmem_default 覆盖),缓冲区在系统负载不重的情况下可以增长到这个值;第三个值是发送缓冲区空间的最大字节数(该值会被 wmem_max 覆盖)。 |
net.ipv4.udp_mem="P1 P2 P3" | UDP 的情况,含义同上 |
注:了解以上网络参数的具体含义,需学习 Linux 内核收发包原理,强烈推荐《Monitoring and Tuning the Linux Networking Stack》系列文章。详见文末参考文献 4-7。
软中断隔离:将容器网卡的软中断绑定到某几个专用 CPU 上,而容器业务进程绑定到其他 CPU 上,这样可以减少业务和网卡软中断之间的影响,避免频繁的上下文切换,特别适用于对网络性能要求极高的服务例如 Redis。对于云内普通 VPC 和云内 SR-IOV VPC 都适用:
云内普通 VPC:需要把容器使用的虚拟网卡 veth 的软中断绑定到预留的某几个专用 CPU 上
云内 SR-IOV VPC:需要把容器使用的 VF 网卡的软中断绑定到预留的某几个专用 CPU 上
SR-IOV 直通网卡:优化后端 DPDK PMD 线程配比,一般由 1 个调大至 2 个或者 4 个。
调优后 SR-IOV 下 QPS 接近 BGP 物理网络,99.99% 时延从 BGP 物理网络的 990ms 下降到 140ms。
调优后简单 ETL 任务 QPS 比 YARN 上高 20%,复杂 ETL 任务 QPS 比在 YARN 上高 30%。
对于 RDS MGR 集群,K8S 容器部署相比 RDS2.0 云主机 VM 部署,同等规格下性能提升可以达到 30%~170%;
经过优化后,与物理机部署相比,常规模式下,只写场景、只读场景和读写混合场景的性能差距保持在 5~10% 之间。
异步复制集群:普通容器单分片相比物理机性能有 40% 差距,增加生产消费者数量,集群整体性能有所提升,但依然与标准物理机有 25% 左右差距;增加生产消费者数量,容器调优后的性能基本持平标准物理机,差距 5% 以内。
同步复制集群:普通容器性能略差于标准物理机,差距在 10% 左右,容器调优后的性能基本持平标准物理机,差距缩小到 5%。
刘迎冬,网易杭州研究院轻舟云原生内核开发专家,十年以上虚拟化和内核经验,先后在华为、网易从事虚拟化和内核相关工作,目前在网易杭研主要负责虚拟化和轻舟容器内核的交付、运维工作和性能调优工作,主要关注运行容器的新一代操作系统技术、混合部署隔离技术以及内核性能调优技术。
点个在看少个 bug 👇