几个月前,在我们终于收到了新 Cephcluster 的新磁盘后,我们决定对它们进行基准测试,这样我们就可以调整我们的集群以获得最佳性能,但这是另一个故事。在为我们的第一个基准设置一切并启动它之后,我们知道它需要大约一个半小时才能完成,所以与此同时,我们继续做其他事情。大约两个小时后回到测试,它仍然没有完成,它在开始时卡在某个地方。在我们准备它的过程中,我们遇到了这个问题,我们认为这只是工具配置错误。然而,在运行一个简单的 ceph 状态后,我们看到一些 Ceph 服务(在这个特殊情况下,一个监视器、一个管理器和两个 OSD)关闭了。 SSH 连接到运行这些服务的节点(以下称为节点 1)并没有什么乐趣,因为它不再应答。我们不得不使用机器的 iDRAC(嵌入服务器的远程控制界面)在其控制台上发现内核崩溃并强制重启机器。为了更好地理解内核恐慌的原因,我们开始恐慌,认为我们全新的服务器可能存在巨大问题。查看来自内核的调用跟踪并没有给我们提供很多信息,除了它可能与内存相关。成功重启后,我们从上次启动的内核日志中查看发生了什么。我们收到的第一个堆栈跟踪是这样的(为了保持简短,不包括一些行): kernel: Call Trace:kernel: __check_heap_object+0xe7/0x110kernel: __check_object_size.part.0+0x128/0x150kernel: __check_object_size+0x1c/0x_ittocopyernel +0x2b/0x50内核:__skb_datagram_iter+0x19f/0x2e0kernel:? zerocopy_sg_from_iter+0x50/0x50kernel:skb_copy_datagram_iter+0x40/0x90kernel:tcp_recvmsg+0x6a4/0xa10kernel:? _cond_resched+0x1a/0x50内核:? aa_sk_perm+0x43/0x1b0kernel:inet_recvmsg+0x5e/0x100kernel:sock_recvmsg+0x69/0x80kernel:__sys_recvfrom+0xe5/0x1d0kernel:? _cond_resched+0x1a/0x50内核:? ktime_get_ts64+0x4c/0xf0kernel: ? __prepare_exit_to_usermode+0x62/0xe0kernel: __x64_sys_recvfrom+0x29/0x30kernel: do_syscall_64+0x49/0xc0kernel: entry_SYSCALL_64_after_hwframe+0x44/0xa90703304/0xa904/0xa90703304/0xa90704304/0xa90703304/0xa907043030304/0xa9070304304/0xa9070303040304030300070307304030730403073030730403073030730307303073030730307303040730407334/之间清除网络调用堆栈和 inet_recvmsg.通过堆栈跟踪搜索更多一点(有很多,准确地说是 230),我们偶然发现了这个:内核:BUG:进程交换器/57 中的坏页状态 pfn:7d518ekernel:页面:ffffe4f71f546380 refcount:0 mapcount:0映射:0000000000000000指数:0xffff99fd5518fe00头:ffffe4f71f546380顺序:1 compound_mapcount:0kernel:标志:0x57ffffc0010200(板|头)内核:原料:0057ffffc0010200 dead000000000100 dead000000000122 ffff99fce07cce00kernel:原料:ffff99fd5518fe00 00000000802a0029 00000000ffffffff 0000000000000000kernel:页甩了,因为:PAGE_FLAGS_CHECK_AT_FREE标志( s) setkernel:模块链接在:cfg80211 xt_conntrack xt_MASQUERADE nf_conntrack_netlink xfrm_user xfrm_algo nft_counter xt_addrtype nft_compat nft_chain_nat nf_nat nf_conntrack nf_defrag_ipv6 nf_defrag_ipv4 nf_tables nfnetlink br_netfilter桥AUFS覆盖dell_rbu 8021q GARP MRP STP LLC接合nls_iso8859_1 dm_multipath scsi_dh_rdac scsi_dh_emc scsi_dh_alua amd64_edac_mod edac_mce_amd amd_energy kvm_amd KVM ipmi_ssif rapL遗传dell_smbios dcdbas dell_wmi_descriptor wmi_bmof joydev input_leds efi_pstore cdc_ether usbnet MII CCP k10temp acpi_ipmi ipmi_si ipmi_devintf ipmi_msghandler acpi_power_meter mac_hid sch_fq_codel ip_tables都是x_tables autofs4 BTRFS blake2b_generic RAID10 raid456 async_raid6_recov async_memcpy async_pq async_xor async_tx XOR raid6_pq libcrc32c RAID1 RAID0多径线性hid_generic USBHID HID crct10dif_pclmul crc32_pclmul mgag200 ghash_clmulni_intel i2c_algo_bit drm_kms_helper aesni_intel syscopyarea crypto_simd sysfillrect sysimgblt cryptd fb_sys_fops glue_helper nvmekernel:CEC AHCI TG3 nvme_core rc_core libahci DRM i40e (OE) xhci_pci xhci_pci_renesas i2c_piix4 wmikernel:CPU:57 PID:0 通讯:swapper/57 污染:GBD OE 5.8.0-33-generic #36-Ubuntukernel:硬件名称:Dell Inc. PowerEdge R6525/0GK70M,BIOS 1.4.8 05/06/2020 :调用跟踪:kernel:<IRQ>内核:show_stack+0x52/0x58kernel:dump_stack+0x70/0x8dkernel:bad_page.cold+0x63/0x94kernel:check_free_page_bad+0x66/0x70kernel:__free_pages/0x58kernel+0x70/0x8dkernel:__free_pages/0x5gf_0xfree_pages/0x0f_0xfree_pages/0xfrag_knel+0x80f_0x0f_knel +0x102/0x1b0kernel:__kfree_skb+0x26/0x40kernel:tcp_data_queue+0x1ce/0x570kernel:? tcp_init_transfer +量0x108 / 0x170kernel:tcp_rcv_state_process + 0x2c6 / 0x82fkernel:tcp_child_process +的0xA5 / 0x1a0kernel:tcp_v4_rcv + 0xa28 / 0xdd0kernel:ip_protocol_deliver_rcu +的0x30 / 0x1b0kernel:ip_local_deliver_finish + 0x48 / 0x50kernel:ip_local_deliver + 0xe3 / 0xf0kernel:? ip_protocol_deliver_rcu+0x1b0/0x1b0kernel: ip_sublist_rcv_finish+0x3d/0x50kernel: ip_list_rcv_finish.constprop.0+0x163/0x190kernel: ip_sublist_rcv+0x36/0xa? ip_rcv_finish_core.constprop.0+0x310/0x310内核:ip_list_rcv+0xf5/0x118内核:__netif_receive_skb_list_core+0x228/0x250内核:__netif_receive_skb_list+0x: tcp4_gro_receive+0x115/0x1a0kernel: netif_receive_skb_list_internal+0xc5/0x180kernel: napi_complete_done+0x7a/0x1a0kernel: i40e_napi_poll+0x137f/0x172e] check_preempt_curr +的0x84 / 0x90kernel:napi_poll + 0x96 / 0x1b0kernel:net_rx_action + 0xb8 / 0x1c0kernel:__do_softirq + 0xd0 / 0x2a1kernel:asm_call_irq_on_stack + 0×12 / 0x20kernel:</ IRQ>内核:do_softirq_own_stack +的0x3D / 0x50kernel:irq_exit_rcu +位0x95 / 0xd0kernel:common_interrupt + 0x7c/0x150内核:asm_common_interrupt+0x1e/0x40内核:RIP:0010:native_safe_halt+0xe/0x10内核:代码:e5 8b 74 d0 04 8b 3c d0 e8 6f b3 cc cc cc cc 3 cc cc 3 cc e9 07 00 00 00 0f 00 2d 66 ee 43 00 fb f4 <c3> 90 e9 07 00 00 00 0f 00 2d 56 ee 43 00 f4 c3 cc cc 0f 1f 407a 0704 07 072 EF 0704 072 EF 072 EF 072 EF 0704 07 072 EF 072 EF 0704 07 07 7 07 7 7 0 7 0 7 0 7 0 7 0 7 0 7 0 7 0 7 7 0 4070400000000000000000 ffffffff947cd3a0 RBX:ffff99fddb499780 RCX:ffff99fddf26d440kernel:RDX:0000000007b73c42 RSI:0000000000000039 RDI:ffff99fddf25fa80kernel:RBP:ffffa70240577e90 R08:00000066a171bc54 R09:0000000000000201kernel:R10:0000000000000029 R11:0000000000013400 R12:0000000000000039kernel:R13:0000000000000000 R14:0000000000000000 R15:0000000000000000kernel:? __cpuidle_text_start+0x8/0x8kernel: ? default_idle + 0×20 / 0x140kernel:arch_cpu_idle + 0×15 / 0x20kernel:default_idle_call + 0x2c上/ 0x30kernel:cpuidle_idle_call + 0x173 / 0x200kernel:do_idle + 0x7a / 0xe0kernel:cpu_startup_entry + 0×20 / 0x30kernel:start_secondary + 0xe6 / 0x100kernel:secondary_startup_64 + 0xb6 / 0xc0kernel:usercopy:从 SLUB 对象 'anon_vma_chain(882:init.scope)'(偏移量 2,大小 1448)检测到内核内存暴露尝试!内核:------------[ 在这里切]------ ------kernel: kernel BUG at mm/usercopy.c:99!kernel: invalid opcode: 0000 [#5] SMP NOPTI 这里首先要注意的是第一行:Bad page state in process swapper。一些进程,swapper,试图访问它不应该访问的内存页面。这个过程是什么,是任何人的猜测,但更重要的是我们有导致这个错误的调用跟踪,在其中,这一行:i40e_napi_poll+0x137f/0x1720 [i40e]。这里的括号指定了函数所在的内核模块。它只存在于树外模块中。事实上,我们已经安装了一个自定义的 i40e 驱动程序!在我们分析所有这些时,Ceph 集群仍在另外两个节点(以下称为 node-2 和 node-3)上运行,因此试图在剩余的 OSD 之间重新平衡数据,这也导致 node-2 崩溃。用于测试的客户端(以下称为 node-4 和 node-5)很好。
现在我们已经暴露了问题所在,让我们回过头来了解我们是如何到达那里的。如前所述,我们为 Cephcluster 获得了五台全新的服务器。有了这些,我们还得到了 10 个 NVMe SSD、10 个 Optane SSD 和 5 个 IntelX722-DA2 网卡。这些卡是支持 RDMA 的双端口 10GbE 适配器。简而言之,RDMA 是一个进程,它允许另一台计算机在不涉及操作系统的情况下访问我们计算机的内存,因此不涉及 CPU。这对于 Cephcluster 尤其有用,因为它减少了客户端的延迟。我们对此进行了一些测试,但同样,这是另一个故事。在这些测试中,我们注意到机器在一段时间后随机冻结,因此我们决定升级网卡的固件,这可能可以解决问题。这样做后,我们检查内核日志以查看升级后是否一切顺利,我们收到以下消息: i40e 0000:43:00.0: 设备驱动程序检测到比预期 v1.9 更新的 NVM 映像 v1.11 版本.请安装最新版本的网络驱动程序。 “没关系”,我们想,让我们升级驱动程序吧。哦,我们有没有提到 i40e 是用于 X722 卡的驱动程序?那时,我们正在使用内核中的驱动程序,就像在 v5.8 中的 in-tree 中一样,在其上应用了 Ubuntu 的补丁程序,尽管它们都不可能更改该驱动程序代码中的任何内容。所以我们去从 Intel 的网站下载了最新的 (2.13.10) 版本的驱动程序,安装它,加载它并继续我们的生活,即对 Ceph 进行基准测试。 固件和内核模块有一些升级/降级顺便说一句,因为最新的那些与我们的 RDMA 测试不兼容,但让我们忘记那些,只记得此时我们使用的是内核的 5.8 版本,有一个树外模块 i40e,版本2.13.10.我们不担心这些,因为在复制过程中,我们重新安装了操作系统几次,以从我们的 RDMA 测试中删除任何痕迹。直到我们到达本文开头所描述的崩溃和堆栈跟踪为止。找到这样的错误的第一步是重现它,并有明确的步骤来做到这一点。在将崩溃的节点恢复后,我们的第一次复制尝试是再次启动基准测试。很快,节点 3 崩溃了,随后是其他两个节点之一。他们的内核日志包含我们之前看到的相同堆栈跟踪。为什么 node-3 现在崩溃了,而不是像以前那样 node-1 或 node-2?正如我们之前确定的,问题与网络有关,因此我们决定启动另一个基准测试,但这次只使用网络。具体来说,我们要对我们的网络进行压力测试,因为我们的 Cephbenchmarks 是网络密集型的。为此,我们使用了 iperf3,这是一种在 TCP、UDP 和 SCTP 上运行的速度测试工具。它使用客户端-服务器方案工作,即您在一台机器上运行 iperf3 服务器,在另一台机器上运行 iperf3 客户端,它会告诉您两者之间的网络速度。由于 X722 卡有两个使用 LACP 设置的接口,我们将在每台机器上运行两个服务器和两个客户端,如下所示:
在开始这个测试之前,我们还决定重启所有节点。 iperf3clients 大致在同一时间启动,实际上,三分之二的机器崩溃了,这意味着我们能够轻松重现该问题!我们尝试不使用绑定(在我们的例子中,没有 LACP),这没有帮助。我们还尝试只进行传输/接收,并确定问题仅在接收流量时发生。上图可以这样更新:我们还排除了单个网卡的作用,因为问题发生在多台服务器上。由于设置 iperf 服务器和客户端的这个装置需要相当长的时间,我们决定使用一个工具在 tmux 会话中自动启动它们,因此我们可以快速启动我们的复制方法。有问题的工具是 tmuxp,它使用一个 json 或 YAML 文件来描述窗口和窗格,并从中创建一个 tmux 会话。在节点 1 上,该文件如下所示: session_name: 'ceph-bench'start_directory: /rootwindows: - window_name: htop panes: - shell_command: htop start_directory: /root - window_name: iperf3 ens1f1 layout: tiled panes: - shell_command: 'read && iperf3 -s -p 5201' focus: true - shell_command: 'read && iperf3 -s -p 5202' - shell_command: 'read && while true;做 iperf3 -c 192.168.215.102 -p 5201 -t 600 -M 9000 -P 10 -l 1048576 -Z;睡眠 1; done' - shell_command: 'read && while true;做 iperf3 -c 192.168.215.103 -p 5201 -t 600 -M 9000 -P 10 -l 1048576 -Z;睡眠 1; done' - window_name:iperf3 ens1f0 布局:平铺窗格: - shell_command:'read && iperf3 -s -p 5203' focus:true - shell_command:'read && iperf3 -s -p 5204' - shell_command:'read && while true;做 iperf3 -c 192.168.215.202 -p 5203 -t 600 -M 9000 -P 10 -l 1048576 -Z;睡眠 1; done' - shell_command: 'read && while true;做 iperf3 -c 192.168.215.203 -p 5203 -t 600 -M 9000 -P 10 -l 1048576 -Z;睡眠 1; done' - window_name:journalctl 转储窗格: - shell_command:'journalctl -fxke \> dump' focus:true - shell_command:tail -f dump - window_name:shell 窗格:- null 如您所见,我们调整了一些 iperf3 选项以使其充分利用配置的MTU(-M 9000),运行10分钟(-t 600),使用10个并行流(-P 10),使用零拷贝,即避免内存工作,能够更快地发送数据(-Z) 并增加读取缓冲区的大小以发送 -l 1048576。因此,我们很容易使服务器之间的网络饱和。作为我们标准机器安装程序的一部分,我们安装了 Netdata,这是一种监控工具,用于收集有关我们系统上许多事物的指标。因此,它是我们查看压力测试期间和崩溃前发生的情况的首选工具。由于问题与网络和内存相关(还记得我们看过的第一个堆栈跟踪吗?),我们正在查看内核内存(特别是平板)使用情况和网络中断。
如您所见,我们在大约 00:35 开始测试,当网络中断增加时,从那时起,内存使用量只会不断增加,直到服务器崩溃,大约在 02:07。那时我们开始认为该错误可能是内存泄漏。如果确实如此,我们会看到它以较慢的速度发生,但需要更长的时间来触发它。因此,我们重新运行压力测试,但这次我们通过更改交换机上的协商参数将接口速度限制为 1Gbps。如您所见,我们在slab使用曲线上观察到了相同的趋势,但这次触发错误需要8个多小时。由于我们摆弄了驱动程序的自定义(例如,不是 in-tree,但仍由制造商正式发布)版本,我们决定一起尝试多个版本,看看是哪个版本导致了问题。这是我们发现的: 因此,该错误仅在使用 Intel atversion 2.13.10 发布的 i40e 驱动程序时发生。由于我们有堆栈跟踪,我们认为我们可以缩小 2.12.6 和 2.13.10 之间引入错误的范围。尽管我们充满希望,但我们认为我们可以将一个 gitrepository git 平分,看看问题出在哪里。我们是傻瓜。事实上,Intel 只发布代码的明星球,即使是在开发发生的 SourceForgeprojet 上。将两个版本的源代码差异化给出了 3851 处更改,即实际代码更改,而不是自述文件、许可证或发行说明,这当然没有任何帮助。但是,由于我们知道哪个函数导致了问题(来自堆栈跟踪,i40e_napi_poll),我们能够找到问题来自的文件并进行比较。其中有 714 处更改。理解网络如何工作是很棘手的。了解内核如何处理它甚至更棘手。我们从 Red Hat Linux NetworkPerformance TuningGuide 开始,它概述了内核的许多(如果不是全部)与网络相关的功能。它还深入介绍了它实际上是如何工作的。这是一本非常有趣的读物,如果您甚至在 Linux 上远程使用网络,您应该看看。
回到 2.12.6 和 2.13.10 之间的差异,我们注意到很多变化都与称为 ntuple 过滤的东西有关。由于我们不知道那是什么,我们环顾四周,发现它是 Intel 称为 Ethernet FlowDirector 的功能。我们最终还阅读了 packagecloud 关于使用 Linux 网络堆栈接收数据的指南。他们有关于发送数据的类似指南。两者都是我们强烈推荐的好读物。基本上,ntuple 过滤允许将某些网络流定向到与特定 CPU 相关联的特定 RX 队列。例如,如果你有一个 web 服务器监听固定到 CPU 2 的端口 80,你可以配置你的网卡将目标端口 80 的 TCP 数据包排队到 CPU 2 上的 RX 队列,从而命中 CPU 缓存并减少处理时间(例如从以上指南)。我们在我们的网卡上禁用了这个功能并再次尝试,错误没有触发。终于有进步了!这仍然让我们很困惑,因为我们没有设置任何 ntuple 过滤规则,所以处理那些的代码不应该被执行,就像 ntuple 过滤被禁用一样。我们联系了 e1000-devel 邮件列表,这让我们可以直接与英特尔工程师沟通。完整的线程可以在这里找到。然后我们得到了两条有用的信息: 我们可以忽略关于驱动程序与固件过期的消息,这意味着我们可以毫无问题地运行 in-tree 版本。不幸的是,生产限制不允许我们测试最新版本,所以我们使用了树内版本。但我们正在超越自己。由于我们想发现问题的根源,也许还有一种解决方法,我们决定找出内存泄漏的来源。为此,我们使用了 Bcc,它是 eBPF kerneltracer 上的一个前端,允许您在 Python 环境中从 eBPF 程序中获取结果并在 Python 中处理它们,在本文中介绍给我们(fr)。具体来说,我们使用了memleak.pyutility,打印未完成的分配,即尚未释放的分配。它还通过堆栈跟踪对它们进行分组,这意味着您可以获得这些分配的代码路径。它定期输出如下内容:附加到内核分配器,Ctrl+C 退出。[22:16:33] 未完成分配的前 10 个堆栈:来自堆栈 __alloc_pages_nodemask+0x237 [内核] __alloc_pages_nodemask+0x 的 17423 个分配中的 71364608 字节alloc_pages_current+0x87 [kernel] pte_alloc_one+0x18 [kernel] __pte_alloc+0x1b [kernel] do_anonymous_page+0x3e4 [kernel] handle_pte_fault+0x22b [kernel] __handle_mm_9fault_0x59fault_xkernel+0x59fault_xkernel+0x59fault_xkernel+0x59fault_xkernel+0x59fault 0x86可以[内核] asm_exc_page_fault + 0X1E [内核]在18262个分配74801152个字节从堆栈__alloc_pages_nodemask + 0x237 [内核] __alloc_pages_nodemask + 0x237 [内核] alloc_pages_current + 0×87 [内核] __page_cache_alloc + 0x2B访问[内核] page_cache_readahead_unbounded + 0x93 [内核] __do_page_cache_readahead + 0x35 [内核] do_sync_mmap_readahead+0xf5 [内核] filemap_fault+0x566 [内核] ext4_filemap_fault+0x32 [内核] __do_fault+0x3c [内核] do_cow_fault+0x7e [k] ernel] do_fault+0x3b [kernel] handle_pte_fault+0x1e7 [kernel] __handle_mm_fault+0x599 [kernel] handle_mm_fault+0xc6 [kernel] do_user_addr_fault+0x1e2 [kernel] exc_page_fault_gault+0x1e2 [kernel] exc_page_fault_gault+0x1e exc_page_fault+0x10xkernel extract_crng_user+0xa7 [内核] urandom_read_nowarn.constprop.0+0x29 [内核] urandom_read+0x27 [内核] vfs_read+0xaa [内核] ksys_read+0x67 [内核] __x64_sys_read+0x1a [内核]+0x1a [x40_40_40_40_40_40_sys_4_40_40_40_40_40_40_40_sys_4_40_40_40_sys_4h_4_4_4_40_40_40_40_40_40_40_sys_4_4h_46内核] 8 ......