本文选自“字节跳动基础架构实践”系列文章。html
“字节跳动基础架构实践”系列文章是由字节跳动基础架构部门各技术团队及专家倾力打造的技术干货内容,和你们分享团队在基础架构发展和演进过程当中的实践经验与教训,与各位技术同窗一块儿交流成长。git
“延迟突刺”、“性能抖动”等问题一般会受到多方因素影响不便排查,本文以线上问题为例,详解 TLB shootdown,最终使得 CPU 的消耗下降 2% 左右,并消除了抖动突刺,变得更加稳定。github
在互联网业务运行的过程当中,不免遇到“延迟突刺”、“性能抖动”等问题,而一般这类问题会受到多种软件环境甚至硬件环境的影响,缘由较为隐晦,解决起来相对棘手。数据库
本文以一个线上问题为例子,深刻 x86 体系结构,结合内核内存管理的知识,辅以多种 Linux 平台上的 Debug 工具,详解 TLB shootdown 问题,最终解决掉该问题,提高了业务性能。api
Kernel: 本文中特指 Linux-4.14。缓存
KVM: Kernel-based Virtual Machine。如今主流的虚拟机技术之一。bash
Host: 指虚拟化场景下的宿主机。服务器
Guest: 指虚拟化场景下的虚拟机。多线程
APIC: Advanced Programmable Interrupt Controller 。Intel CPU 使用的中断控制器。架构
LAPIC: Local Advanced Programmable Interrupt Controller 。
IPI: Inter-Processor Interrupt。CPU 之间相互通知使用。
MMU: Memory Management Unit。Kernel 用来管理虚拟地址和物理地址映射的硬件。
TLB: Translation Lookaside Buffer。MMU 为了加速查找页表,使用的 cache。用来加速 MMU 的转化速度。
PTE: Page Table Entry 。管理页表使用的页表项。
jemalloc: 一个用户态内存管理的库,在多线程并发的场景下,malloc/free 的性能好于 glibc 默认的实现。
如上图所示,一个进程有 4 个 thread并行执行。因为 4 个 thread 共享同一个进程的页表,在执行的过程当中,经过把 pgd 加载到 cr3 的方式,每一个 CPU 的 TLB 中加载了相同的 page table。
若是 CPU0 上,想要修改 page table,尤为是想要释放一些内存,那么须要修改 page table,同时修改本身的 TLB(或者从新加载 TLB)。
然而,这还不够。例如,CPU0 上释放了 page A,而且 page A 被 kernel 回收,颇有可能被其余的进程使用。可是,CPU一、CPU2 以及 CPU3 的 TLB 中仍是缓存了对应的 PTE 表项,依然能够访问到 page A。
为了防止这个事情发生,CPU0 须要通知 CPU一、CPU2 和 CPU3,也须要在 TLB 中禁用掉对应的 PTE。通知的方式就是使用IPI (Inter-Processor Interrupt)。
在虚拟化的场景下,IPI 的成本比较高。若是 Guest 中有大量的 IPI,就会看到 Guest 的 CPU sys 暴涨。同时,在 Host 上能够发现虚拟机发生 vmexit 突增,其中主要是 wrmsr 的 ICR Request 产生。(熟悉 x86 的同窗知道,x2apic 模式下,x86 上 IPI 的实现即经过 wrmsr 指令请求 ICR)
#watch -d -n 1 "cat /proc/interrupts | grep TLB"
复制代码
若是看到数据上涨比较厉害,那么基本就能够看到问题了。
#perf top
复制代码
若是看到 smp_call_function_many,那么很不幸,就是在批量发送 IPI。
好消息是这个场景并不常见,比较特定的状况下才会发生。典型的就是用户态进程中调用了系统调用:
int madvise(void *addr, size_t length, MADV_DONTNEED);
复制代码
# ls /proc/*/maps | xargs grep jemalloc
复制代码
# strace -f -p 1510 2>&1 | grep madvise
复制代码
确认上述的 TLB shootdown 问题以后,咱们再来回顾一下,系统调用 madvise 到底起了什么做用呢?
int madvise(void *addr, size_t length, MADV_DONTNEED);
复制代码
若是使用了 DONTNEED,就会释放对应的 page。若是下一次再访问到,就会重复上述的 2 和 3。
效果就是短暂的 page 归还 kernel 以后,下次访问从新分配。
例如 state 0 所示,用户态进程分配了 VMA0 和 VMA1 两个虚拟机地址空间。有的地址上已经分配了物理页面(例如 0x800000),有的尚未分配(例如 0x802000)。
如 state 1 所示,用户态进程第一次访问到了例如 0x802000 地址的时候,触发了 page fault,内核为用户态进程的 0x802000 分配了物理页面(地址是 0x202000)。
如 state 2 所示,执行了:
madvise(0x800000, 8192, MADV_DONTNEED)
复制代码
以后,内核释放了对应的物理页面。那么下一次访问到 0x800000 ~ 0x801fff 的时候,就会触发 page fault。处理过程相似 state 1。
如 state 3 所示,执行了:
munmap(0x800000, 16384);
复制代码
就把对应的 VMA 释放了。那么下次访问到 0x800000 ~ 0x803fff 的时候,就会触发 segment fault。由于地址已经释放,属于非法地址,内核会给进程发送 signal 11。大部分状况下,会杀掉进程。
问题产生自 jemalloc,因此尝试从 jemalloc 自己入手解决问题。
尝试去社区,问 jemalloc 的 maintainer,是否有办法解决 TLB shootdown 引发的问题,maintainer 建议经过 jemalloc 环境变量(MALLOC_CONF)动态控制 jemalloc 是否启动 madvise。问题和答复见:
https://github.com/jemalloc/jemalloc/issues/1422
复制代码
在本地写测试代码,实际测试 jemalloc(比较靠近 upstream 的 5.0版本)和 maintainer 给出来的建议,在进程启动前导入环境变量:
MALLOC_CONF=dirty_decay_ms:-1,muzzy_decay_ms:-1
复制代码
能够验证能够成功避免问题。该环境变量能够解决 tlb 问题,详细参数做用请参看手册:
http://jemalloc.net/jemalloc.3.html#opt.dirty_decay_ms
复制代码
某业务一样使用了 jemalloc,可是测试没有效果。
对业务实际使用的 so 动态连接库进行:
#strings libjemalloc.so.2 | grep -i version
复制代码
能够发现实际使用的版本是:
JEMALLOC_VERSION "4.2.0-0-gf70a254d44c8d30af2cd5d30531fb18fdabaae6d"
复制代码
经过阅读 jemalloc 的源代码发现,在 4.2 版本的时候,还不支持 maintainer 给出来的变量参数。可是能够经过以下变量来达到相似的效果:
MALLOC_CONF=purge:decay,decay_time:-1
复制代码
设置了 jemalloc 的参数以后,业务的表现获得了明显的提高。以下图所示,最后一个零点和前一个零点进行对比,CPU 的抖动状况获得了很大的改善,从以前的 6% 左右抖动到低于 4% 的稳定运行,且 CPU 的消耗曲线更加稳定平滑。
与此同时,业务上的延迟也更加稳定,PCT99 也下降了延迟突刺状况。
在解决问题的过程当中,也并不是如文章所写的通常有序进行。期间也屡次使用 perf 观察热点函数的变化;使用 atop 对比先后的业务表现和系统指标;也观察虚拟化的监控数据(wrmsr 的数量)等等手段,一步一步排除干扰,锁定问题。
随着当代操做系统的复杂度的提升,问题的难度也在提升。在解决问题的过程当中,咱们也在进步!
最后,欢迎加入字节跳动基础架构团队,一块儿探讨、解决问题,一块儿变强!
字节跳动基础架构团队是支撑字节跳动旗下包括抖音、今日头条、西瓜视频、火山小视频在内的多款亿级规模用户产品平稳运行的重要团队,为字节跳动及旗下业务的快速稳定发展提供了保证和推进力。
公司内,基础架构团队主要负责字节跳动私有云建设,管理数以万计服务器规模的集群,负责数万台计算/存储混合部署和在线/离线混合部署,支持若干 EB 海量数据的稳定存储。
文化上,团队积极拥抱开源和创新的软硬件架构。咱们长期招聘基础架构方向的同窗,具体可参见 job.bytedance.com,感兴趣能够联系邮箱 arch-graph@bytedance.com 。
欢迎关注字节跳动技术团队