Google Cloud 技术支持提供的有关 DNS 数据包丢失的故事

来自谷歌博客编辑器: 您是否想知道 Google Cloud 技术解决方案 (TSE) 工程师如何处理您的支持请求? TSE 技术支持工程师负责识别和纠正用户报告的问题来源。 其中一些问题非常简单,但有时您会遇到需要多个工程师同时关注的问题。 在这篇文章中,一位东证员工将向我们讲述他最近实践中遇到的一个非常棘手的问题—— DNS数据包丢失的情况。 在这个故事中,我们将看到工程师如何设法解决这个问题,以及他们在修复错误时学到了哪些新东西。 我们希望这个故事不仅能让您了解一个根深蒂固的错误,还能让您深入了解向 Google Cloud 提交支持请求的流程。

Google Cloud 技术支持提供的有关 DNS 数据包丢失的故事

故障排除既是一门科学,也是一门艺术。 这一切都从建立关于系统非标准行为原因的假设开始,然后对其进行强度测试。 然而,在提出假设之前,我们必须清楚地定义并精确地表述问题。 如果问题听起来太模糊,那么你就必须仔细分析一切; 这就是故障排除的“艺术”。

在 Google Cloud 下,此类流程变得更加复杂,因为 Google Cloud 会尽力保证用户的隐私。 因此,TSE 工程师无法编辑您的系统,也无法像用户一样广泛地查看配置。 因此,为了检验我们的任何假设,我们(工程师)无法快速修改系统。

一些用户认为我们会修复所有东西,就像汽车服务中的机械师一样,只需向我们发送虚拟机的 ID,而实际上该过程以对话形式进行:收集信息,形成和确认(或反驳)假设,最后,决策问题是基于与客户的沟通。

有问题的问题

今天我们要讲的故事有一个美好的结局。 所提出的案例得以成功解决的原因之一是对问题的描述非常详细和准确。 您可以在下面看到第一张票的副本(经过编辑以隐藏机密信息):
Google Cloud 技术支持提供的有关 DNS 数据包丢失的故事
此消息包含许多对我们有用的信息:

  • 指定的特定VM
  • 问题本身已表明 - DNS 不起作用
  • 它指出了问题出现的地方 - 虚拟机和容器
  • 指示了用户为识别问题所采取的步骤。

该请求被注册为“P1:严重影响 - 服务在生产中无法使用”,这意味着根据“Follow the Sun”方案持续监控情况 24/7(您可以阅读更多关于 用户请求的优先级),随着每个时区的变化,它从一个技术支持团队转移到另一个技术支持团队。 事实上,当这个问题到达我们苏黎世团队时,它已经传遍了全球。 此时,用户已采取缓解措施,但由于尚未找到根本原因,担心生产中的情况重演。

当机票到达苏黎世时,我们已经掌握了以下信息:

  • 内容 /etc/hosts
  • 内容 /etc/resolv.conf
  • 结论 iptables-save
  • 由团队组装 ngrep .pcap 文件

有了这些数据,我们就可以开始“调查”和故障排除阶段。

我们的第一步

首先,我们检查了元数据服务器的日志和状态,并确保其正常工作。 元数据服务器响应 IP 地址 169.254.169.254,并负责控制域名。 我们还仔细检查了防火墙是否能够与虚拟机正常工作并且不会阻止数据包。

这是某种奇怪的问题:nmap 检查驳斥了我们关于 UDP 数据包丢失的主要假设,因此我们在心里想出了更多的选项和方法来检查它们:

  • 数据包是否有选择地丢弃? =>检查iptables规则
  • 是不是太小了? MTU? => 检查输出 ip a show
  • 该问题仅影响 UDP 数据包还是也影响 TCP? => 开车离开 dig +tcp
  • dig 生成的数据包是否返回? => 开车离开 tcpdump
  • libdns 工作正常吗? => 开车离开 strace 检查双向数据包的传输

这里我们决定给用户打电话现场排查问题。

在通话过程中,我们可以检查几件事:

  • 经过多次检查后,我们从原因列表中排除了 iptables 规则
  • 我们检查网络接口和路由表,并仔细检查 MTU 是否正确
  • 我们发现 dig +tcp google.com (TCP)按其应有的方式工作,但是 dig google.com (UDP) 不起作用
  • 开车离开后 tcpdump покаработает dig,我们发现UDP数据包正在返回
  • 我们开车离开 strace dig google.com 我们看到 dig 如何正确调用 sendmsg() и recvms(),但是第二个被超时中断

不幸的是,轮班结束了,我们被迫将问题升级到下一个时区。 然而,这个请求引起了我们团队的兴趣,一位同事建议使用 scrapy Python 模块创建初始 DNS 包。

from scapy.all import *

answer = sr1(IP(dst="169.254.169.254")/UDP(dport=53)/DNS(rd=1,qd=DNSQR(qname="google.com")),verbose=0)
print ("169.254.169.254", answer[DNS].summary())

该片段创建一个 DNS 数据包并将请求发送到元数据服务器。

用户运行代码,返回 DNS 响应,应用程序接收它,确认网络级别没有问题。

又经过一次“环球旅行”后,请求返回到我们团队,我完全转移给自己,认为请求不再到处循环,对用户来说会更方便。

同时,用户同意提供系统映像的快照。 这是一个非常好的消息:能够自己测试系统使故障排除变得更快,因为我不再需要要求用户运行命令、向我发送结果并进行分析,我可以自己完成所有事情!

我的同事们开始有点羡慕我了。 午餐时我们讨论了转换,但没有人知道发生了什么。 幸运的是,用户本人已经采取了减轻后果的措施,并不着急,所以我们有时间来剖析这个问题。 由于我们有了图像,我们可以运行任何我们感兴趣的测试。 伟大的!

退一步

系统工程师职位最常见的面试问题之一是:“当你 ping 时会发生什么? www.google.com? 这个问题很好,因为候选人需要描述从 shell 到用户空间,到系统内核,然后到网络的所有内容。 我笑了:有时候面试问题在现实生活中很有用……

我决定将这个人力资源问题应用于当前的问题。 粗略地说,当您尝试确定 DNS 名称时,会发生以下情况:

  1. 应用程序调用系统库,例如 libdns
  2. libdns 检查它应该联系的 DNS 服务器的系统配置(在图中是 169.254.169.254,元数据服务器)
  3. libdns 使用系统调用创建 UDP 套接字 (SOKET_DGRAM) 并双向发送带有 DNS 查询的 UDP 数据包
  4. 通过 sysctl 接口您可以在内核级别配置 UDP 堆栈
  5. 内核与硬件交互,通过网络接口在网络上传输数据包
  6. 管理程序在与元数据服务器联系时捕获数据包并将其传输到元数据服务器
  7. 元数据服务器通过其魔力确定 DNS 名称并使用相同的方法返回响应

Google Cloud 技术支持提供的有关 DNS 数据包丢失的故事
让我提醒您我们已经考虑过哪些假设:

假设:损坏的图书馆

  • 测试1:在系统中运行strace,检查dig是否调用了正确的系统调用
  • 结果:调用了正确的系统调用
  • 测试 2:使用 srapy 检查是否可以绕过系统库确定名称
  • 结果:我们可以
  • 测试 3:对 libdns 包和 md5sum 库文件运行 rpm –V
  • 结果:库代码与工作操作系统中的代码完全相同
  • 测试 4:在没有此行为的 VM 上挂载用户的根系统映像,运行 chroot,查看 DNS 是否工作
  • 结果:DNS 工作正常

根据测试得出的结论: 问题不在图书馆

假设:DNS 设置有错误

  • 测试1:运行dig后检查tcpdump,看看DNS数据包是否正确发送和返回
  • 结果:数据包传输正确
  • 测试2:在服务器上仔细检查 /etc/nsswitch.conf и /etc/resolv.conf
  • 结果:一切正确

根据测试得出的结论: 问题不在于 DNS 配置

假设:核心损坏

  • 测试:安装新内核,检查签名,重启
  • 结果:类似的行为

根据测试得出的结论: 内核没有损坏

假设:用户网络(或管理程序网络接口)的不正确行为

  • 测试 1:检查您的防火墙设置
  • 结果:防火墙在主机和 GCP 上传递 DNS 数据包
  • 测试二:拦截流量并监控DNS请求的传输和返回的正确性
  • 结果:tcpdump确认主机已收到返回数据包

根据测试得出的结论: 问题不在网络上

假设:元数据服务器不工作

  • 测试1:检查元数据服务器日志是否存在异常
  • 结果:日志没有异常
  • 测试 2:通过绕过元数据服务器 dig @8.8.8.8
  • 结果:即使不使用元数据服务器,分辨率也会被破坏

根据测试得出的结论: 问题不在于元数据服务器

底线: 我们测试了所有子系统,除了 运行时设置!

深入了解内核运行时设置

要配置内核执行环境,可以使用命令行选项 (grub) 或 sysctl 界面。 我调查了 /etc/sysctl.conf 想一想,我发现了几个自定义设置。 感觉好像抓住了什么,我放弃了所有非网络或非 TCP 设置,保留了山设置 net.core。 然后我转到虚拟机中主机权限所在的位置,开始对损坏的虚拟机进行一项又一项应用设置,直到找到罪魁祸首:

net.core.rmem_default = 2147483647

这就是一个破坏 DNS 的配置! 我找到了凶器但为什么会出现这种情况呢? 我还需要一个动机。

基本 DNS 数据包缓冲区大小通过配置 net.core.rmem_default。 典型值约为 200KiB,但如果您的服务器收到大量 DNS 数据包,您可能需要增加缓冲区大小。 如果新数据包到达时缓冲区已满,例如因为应用程序处理速度不够快,那么您将开始丢失数据包。 我们的客户正确地增加了缓冲区大小,因为他担心数据丢失,因为他正在使用通过 DNS 数据包收集指标的应用程序。 他设置的值是可能的最大值:231-1(如果设置为231,内核将返回“INVALID ARGUMENT”)。

突然我意识到为什么 nmap 和 scapy 可以正常工作:它们使用的是原始套接字! 原始套接字与常规套接字不同:它们绕过 iptables,并且不进行缓冲!

但为什么“缓冲区太大”会导致问题呢? 它显然没有按预期工作。

此时我可以在多个内核和多个发行版上重现该问题。 该问题已经出现在 3.x 内核上,现在也出现在 5.x 内核上。

确实,启动时

sysctl -w net.core.rmem_default=$((2**31-1))

DNS 停止工作。

我开始通过简单的二分搜索算法寻找工作值,发现系统可以使用2147481343,但这个数字对我来说是一组毫无意义的数字。 我建议客户尝试这个号码,他回答说系统可以在 google.com 上运行,但在其他域上仍然出错,所以我继续调查。

我已经安装了 水滴表,一个应该早先使用的工具:它准确地显示了数据包在内核中的最终位置。 罪魁祸首是函数 udp_queue_rcv_skb。 我下载了内核源代码并添加了一些 功能 printk 跟踪数据包的确切结束位置。 我很快就找到了合适的条件 if,只是盯着它看了一会儿,因为就在那时,一切终于组合成一幅完整的图画:231-1,一个无意义的数字,一个不起作用的域......它是一段代码 __udp_enqueue_schedule_skb:

if (rmem > (size + sk->sk_rcvbuf))
		goto uncharge_drop;

请注意:

  • rmem 是 int 类型
  • size 类型为 u16(无符号 XNUMX 位 int)并存储数据包大小
  • sk->sk_rcybuf 是 int 类型并存储缓冲区大小,根据定义,该大小等于中的值 net.core.rmem_default

何时 sk_rcvbuf 接近 231,对数据包大小求和可能会导致 整数溢出。 由于它是一个 int,它的值变为负数,因此当它应该为 false 时条件变为 true(您可以在以下位置阅读更多相关信息) 链接).

该错误可以通过一种简单的方式纠正:通过强制转换 unsigned int。 我应用了修复程序并重新启动了系统,DNS 再次工作。

胜利的滋味

我将我的发现转发给客户并发送 LKML 内核补丁。 我很高兴:拼图的每一块都拼凑在一起,我可以准确解释为什么我们观察到我们所观察到的现象,最重要的是,由于我们的团队合作,我们能够找到问题的解决方案!

值得注意的是,这种情况很少见,幸运的是我们很少收到用户如此复杂的请求。

Google Cloud 技术支持提供的有关 DNS 数据包丢失的故事


来源: habr.com

添加评论