alpine系统的网络诊断

排查kubernetes的timeout问题

Posted by Zeusro on May 11, 2019
👈🏻 Select language

kubernetes + alpine+ php 特别容易出现访问外网/解析外网地址的时候出现超时的问题.

原因

docker容器访问外网的时候,整个完整路径是这样的.

容器–>主机–>外网–>主机–>容器

容器到主机之间的流量要经过源地址转换(SNAT)才能顺利流通.

SNAT就像是一个搬运工,把砖(流量)从容器搬到主机

如果一个主机上面运行多个容器,并发访问外网(特别是PHP这种没有连接池的)时向系统申请可用端口(nf_nat_l4proto_unique_tuple),不可用时+1,然后再申请,再校验.这个过程一多,最终就会导致寻址超时.

说白了是个系统内核问题.

详细的解释见

记一次Docker/Kubernetes上无法解释的连接超时原因探寻之旅

解决方案

最优解

节点升级到 5.1的Linux内核.

iptables升级到1.6.2以上

用基于IPVS模式,尽量少做SNAT/DNAT,支持随机端口SNAT的网络插件启动kubernetes

或者用绕过SNAT的网络插件插件方案,比如阿里云的terway.但这个插件跟阿里云绑定得比较深入,需要每台机器额外购买一个弹性网卡.

次优解

用ds部署name sever,所有节点的DNS解析走节点上的name server,通过最小程度的SNAT+dns cache缓解此类问题.

伪解决方案(不能解决根本问题)

默认的pod的/etc/resolv.conf一般长这样

1
2
3
4
sh-4.2# cat /etc/resolv.conf
nameserver <kube-dns-vip>
search <namespace>.svc.cluster.local svc.cluster.local cluster.local localdomain
options ndots:5

这个配置的意思是,默认nameserver指向kube-dns/core-dns,所有查询中,如果.的个数少于5个,则会根据search中配置的列表依次搜索,如果没有返回,则最后再直接查询域名本 身。ndots就是n个.(dots)的意思

举个例子

1
2
3
4
5
6
7
sh-4.2# host -v baidu.com
Trying "baidu.com.<namespace>.svc.cluster.local"
Trying "baidu.com.svc.cluster.local"
Trying "baidu.com.cluster.local"
Trying "baidu.com.localdomain"
Trying "baidu.com"
......

不使用 alpine 镜像

使用 FQDN

由于域名是从右到左逐级解析的,比如 google.com ,实际上是 google.com.,com后面的.称之为根域名。解析的时候,先解析.,然后解析.com,.com称之为顶级域名,最后解析google。

使用 FQDN:(Fully Qualified Domain Name)全限定域名,是为了尽可能减少内部DNS(比如coreDNS,节点DNS)的解析压力

重开socket

1
2
3
4
5
6
7
        lifecycle:
          postStart:
            exec:
              command:
              - /bin/sh
              - -c 
              - "/bin/echo 'options single-request-reopen' >> /etc/resolv.conf"

设置重开socket是规避容器并发A,AAAA查询

2级域名直接走上层解析

参考kubernetes 使用基于 alpine 镜像无法正常解析外网DNS 做的

直接运行 sed -i 's/options ndots:5/#options ndots:5/g' /etc/resolv.conf 会报错

alpine的echo命令会吞换行符,而resolv.conf格式不对DNS解析会报错

1
2
3
4
5
  dnsConfig:
    options:
      - name: ndots
        value: "2"
      - name: single-request-reopen

去掉了options ndots:5,变会默认值1,这样的话,容器内部直接访问还是没问题的,走search列表,`..svc.cluster.local`,还是能够访问。

而解析Google.com,实际上是解析Google.com.,.的数量超过1个,这时不走search列表,直接用上层DNS

综上所述,去掉ndots/ndots设为1 降低了频繁DNS查询的可能性。对于外网IP的解析有“奇效”。

但如果该主机运行其他容器(这不废话吗,一个节点不跑多个容器那还用啥kubernetes),其他容器也会并发地请求,SNAT的问题还是会出现,所以说修改/etc/resolv.conf文件并不能解决根本问题

歪门邪道1

1
2
3
4
5
6
7
          lifecycle:
            postStart:
              exec:
                command:
                - /bin/sh
                - -c 
                - "head -n 2 /etc/resolv.conf > /etc/temp.conf;cat /etc/temp.conf > /etc/resolv.conf;rm -rf /etc/temp.conf"

歪门邪道2

1
2
3
4
5
6
7
      initContainers:
      - name: alpine
        image: alpine
        command:
         - /bin/sh
         - -c 
         - "head -n 2 /etc/resolv.conf > /etc/temp.conf;cat /etc/temp.conf > /etc/resolv.conf;rm -rf /etc/temp.conf"

衍生的问题

DNAT

容器访问clusterIP(因为是虚拟IP所以需要DNAT)也有可能出现这类超时的问题

访问同 namespace svc 不要强行加戏

non-head service的 virtual domain 格式是<svc>.<namespace>.svc.cluster.local

如果我们容器直接访问<svc>.<namespace>.svc.cluster.local,因为默认DNS设置的问题,解析的次数反而更多。正确的方式是访问<svc>

例子:假设test下面有个s的svc

1
2
3
4
host -v s 
# 解析1次
host -v s.test.svc.cluster.local
# 解析4次

所以,访问同namespace其他svc,直接用svc名去访问即可,没必要装逼使用<svc>.<namespace>.svc.cluster.local这种格式。

其他知识

dns记录类型

  1. A记录:地址记录,用来指定域名的IPv4地址(如:8.8.8.8),如果需要将域名指向一个IP地址,就需要添加A记录。
  2. CNAME: 如果需要将域名指向另一个域名,再由另一个域名提供ip地址,就需要添加CNAME记录。
  3. TXT:在这里可以填写任何东西,长度限制255。绝大多数的TXT记录是用来做SPF记录(反垃圾邮件)。
  4. NS:域名服务器记录,如果需要把子域名交给其他DNS服务商解析,就需要添加NS记录。
  5. AAAA:用来指定主机名(或域名)对应的IPv6地址(例如:ff06:0:0:0:0:0:0:c3)记录。
  6. MX:如果需要设置邮箱,让邮箱能收到邮件,就需要添加MX记录。
  7. 显性URL:从一个地址301重定向到另一个地址的时候,就需要添加显性URL记录(注:DNSPod目前只支持301重定向)。
  8. 隐性URL:类似于显性URL,区别在于隐性URL不会改变地址栏中的域名。
  9. SRV:记录了哪台计算机提供了哪个服务。格式为:服务的名字、点、协议的类型,例如:_xmpp-server._tcp。

用到的命令

安装方法:

1
2
3
  yum install -y bind-utils
  sudo apt-get install -y dnsutils
  apk add bind-tools

dig

dig +trace +ndots=5 +search $host

host

host -v $host

参考链接:

  1. iptables中DNAT、SNAT和MASQUERADE的理解
  2. linux根文件系统 /etc/resolv.conf 文件详解
  3. kube-dns per node #45363
  4. DNS intermittent delays of 5s #56903
  5. Racy conntrack and DNS lookup timeouts
  6. /etc/resolv.conf
  7. /etc/resolv.conf search和ndots配置
  8. DNS for Services and Pods

kubernetes + alpine + php is particularly prone to timeout issues when accessing external networks/resolving external addresses.

Cause

When docker containers access external networks, the complete path is:

Container –> Host –> External Network –> Host –> Container

Traffic between containers and hosts needs to go through Source Network Address Translation (SNAT) to flow smoothly.

SNAT is like a porter, moving bricks (traffic) from containers to the host.

If multiple containers run on one host and concurrently access external networks (especially PHP which has no connection pool), they request available ports from the system (nf_nat_l4proto_unique_tuple). If unavailable, +1, then request again, then verify. When this process happens too much, it ultimately leads to addressing timeouts.

Simply put, it’s a system kernel issue.

For detailed explanation, see:

Remembering a Journey to Find the Unexplained Connection Timeout on Docker/Kubernetes

Solutions

Optimal Solution

Upgrade nodes to Linux kernel 5.1.

Upgrade iptables to 1.6.2 or above.

Use network plugins based on IPVS mode, minimize SNAT/DNAT, support random port SNAT to start kubernetes.

Or use network plugin solutions that bypass SNAT, such as Alibaba Cloud’s terway. But this plugin is deeply bound to Alibaba Cloud and requires purchasing an additional elastic network interface for each machine.

Suboptimal Solution

Deploy name server with ds, all nodes’ DNS resolution goes through the name server on the node, alleviating this type of problem through minimal SNAT + dns cache.

Pseudo-Solution (Cannot Solve Root Problem)

The default pod’s /etc/resolv.conf usually looks like this:

1
2
3
4
sh-4.2# cat /etc/resolv.conf
nameserver <kube-dns-vip>
search <namespace>.svc.cluster.local svc.cluster.local cluster.local localdomain
options ndots:5

This configuration means the default nameserver points to kube-dns/core-dns. In all queries, if the number of dots is less than 5, it will search according to the list configured in search. If no result is returned, it will finally directly query the domain name itself. ndots means n dots.

For example:

1
2
3
4
5
6
7
sh-4.2# host -v baidu.com
Trying "baidu.com.<namespace>.svc.cluster.local"
Trying "baidu.com.svc.cluster.local"
Trying "baidu.com.cluster.local"
Trying "baidu.com.localdomain"
Trying "baidu.com"
......

Don’t Use alpine Images

Use FQDN

Since domain names are resolved level by level from right to left, for example google.com, it’s actually google.com., the . after com is called the root domain. When resolving, first resolve ., then resolve .com, .com is called the top-level domain, finally resolve google.

Using FQDN: (Fully Qualified Domain Name) is to minimize the resolution pressure on internal DNS (like coreDNS, node DNS) as much as possible.

Reopen Socket

1
2
3
4
5
6
7
        lifecycle:
          postStart:
            exec:
              command:
              - /bin/sh
              - -c 
              - "/bin/echo 'options single-request-reopen' >> /etc/resolv.conf"

Setting reopen socket is to avoid concurrent A, AAAA queries in containers.

2-Level Domain Directly Goes to Upper-Level Resolution

Reference kubernetes using alpine-based images cannot properly resolve external DNS

Running sed -i 's/options ndots:5/#options ndots:5/g' /etc/resolv.conf directly will error.

alpine’s echo command swallows newlines, and if resolv.conf format is wrong, DNS resolution will error.

1
2
3
4
5
  dnsConfig:
    options:
      - name: ndots
        value: "2"
      - name: single-request-reopen

Removed options ndots:5, changed to default value 1. This way, containers directly accessing is still fine, going through search list, `..svc.cluster.local`, can still be accessed.

When resolving Google.com, it’s actually resolving Google.com., the number of dots exceeds 1, so it doesn’t go through the search list, directly uses upper-level DNS.

In summary, removing ndots/setting ndots to 1 reduces the possibility of frequent DNS queries. It has “miraculous effects” for resolving external IPs.

But if this host runs other containers (isn’t this nonsense, if a node doesn’t run multiple containers, why use kubernetes), other containers will also request concurrently, the SNAT problem will still appear, so modifying the /etc/resolv.conf file cannot solve the root problem.

Workaround 1

1
2
3
4
5
6
7
          lifecycle:
            postStart:
              exec:
                command:
                - /bin/sh
                - -c 
                - "head -n 2 /etc/resolv.conf > /etc/temp.conf;cat /etc/temp.conf > /etc/resolv.conf;rm -rf /etc/temp.conf"

Workaround 2

1
2
3
4
5
6
7
      initContainers:
      - name: alpine
        image: alpine
        command:
         - /bin/sh
         - -c 
         - "head -n 2 /etc/resolv.conf > /etc/temp.conf;cat /etc/temp.conf > /etc/resolv.conf;rm -rf /etc/temp.conf"

Derived Problems

DNAT

Containers accessing clusterIP (because it’s a virtual IP, DNAT is needed) may also have this type of timeout problem.

Don’t Forcefully Add Drama When Accessing Same Namespace svc

The virtual domain format for non-head service is <svc>.<namespace>.svc.cluster.local

If our container directly accesses <svc>.<namespace>.svc.cluster.local, because of default DNS settings, the number of resolutions is actually more. The correct way is to access <svc>

Example: Assume there’s an s svc under test

1
2
3
4
host -v s 
# Resolve 1 time
host -v s.test.svc.cluster.local
# Resolve 4 times

So, when accessing other svc in the same namespace, just use the svc name directly, no need to show off using the <svc>.<namespace>.svc.cluster.local format.

Other Knowledge

DNS Record Types

  1. A record: Address record, used to specify the IPv4 address of a domain name (e.g., 8.8.8.8). If you need to point a domain name to an IP address, you need to add an A record.
  2. CNAME: If you need to point a domain name to another domain name, which then provides an IP address, you need to add a CNAME record.
  3. TXT: You can fill in anything here, length limit 255. The vast majority of TXT records are used for SPF records (anti-spam).
  4. NS: Name server record. If you need to hand over subdomain resolution to other DNS service providers, you need to add an NS record.
  5. AAAA: Used to specify the IPv6 address corresponding to a hostname (or domain name) (e.g., ff06:0:0:0:0:0:0:c3) record.
  6. MX: If you need to set up email so emails can be received, you need to add an MX record.
  7. Explicit URL: When you need to 301 redirect from one address to another, you need to add an explicit URL record (Note: DNSPod currently only supports 301 redirect).
  8. Implicit URL: Similar to explicit URL, the difference is that implicit URL doesn’t change the domain name in the address bar.
  9. SRV: Records which computer provides which service. Format: service name, dot, protocol type, e.g., _xmpp-server._tcp.

Commands Used

Installation method:

1
2
3
  yum install -y bind-utils
  sudo apt-get install -y dnsutils
  apk add bind-tools

dig

dig +trace +ndots=5 +search $host

host

host -v $host

  1. Understanding DNAT, SNAT and MASQUERADE in iptables
  2. Detailed Explanation of Linux Root File System /etc/resolv.conf File
  3. kube-dns per node #45363
  4. DNS intermittent delays of 5s #56903
  5. Racy conntrack and DNS lookup timeouts
  6. /etc/resolv.conf
  7. /etc/resolv.conf search and ndots configuration
  8. DNS for Services and Pods

kubernetes + alpine + php особенно склонны к проблемам с таймаутом при доступе к внешним сетям/разрешении внешних адресов.

Причина

Когда docker-контейнеры обращаются к внешним сетям, полный путь таков:

Контейнер –> Хост –> Внешняя сеть –> Хост –> Контейнер

Трафик между контейнерами и хостами должен проходить через преобразование сетевых адресов источника (SNAT), чтобы течь плавно.

SNAT похож на носильщика, перемещающего кирпичи (трафик) из контейнеров на хост.

Если на одном хосте запущено несколько контейнеров и они одновременно обращаются к внешним сетям (особенно PHP, у которого нет пула соединений), они запрашивают доступные порты у системы (nf_nat_l4proto_unique_tuple). Если недоступно, +1, затем снова запрос, затем проверка. Когда этот процесс происходит слишком часто, это в конечном итоге приводит к таймаутам адресации.

Проще говоря, это проблема ядра системы.

Подробное объяснение см.:

Запись путешествия по поиску необъяснимой причины таймаута соединения на Docker/Kubernetes

Решения

Оптимальное решение

Обновить узлы до ядра Linux 5.1.

Обновить iptables до 1.6.2 или выше.

Использовать сетевые плагины на основе режима IPVS, минимизировать SNAT/DNAT, поддерживать случайный порт SNAT для запуска kubernetes.

Или использовать решения сетевых плагинов, обходящие SNAT, такие как terway от Alibaba Cloud. Но этот плагин глубоко привязан к Alibaba Cloud и требует покупки дополнительного эластичного сетевого интерфейса для каждой машины.

Субоптимальное решение

Развернуть сервер имен с ds, разрешение DNS всех узлов проходит через сервер имен на узле, облегчая этот тип проблемы через минимальный SNAT + dns кэш.

Псевдо-решение (не может решить корневую проблему)

Файл /etc/resolv.conf подов по умолчанию обычно выглядит так:

1
2
3
4
sh-4.2# cat /etc/resolv.conf
nameserver <kube-dns-vip>
search <namespace>.svc.cluster.local svc.cluster.local cluster.local localdomain
options ndots:5

Эта конфигурация означает, что сервер имен по умолчанию указывает на kube-dns/core-dns. Во всех запросах, если количество точек меньше 5, он будет искать согласно списку, настроенному в search. Если результат не возвращен, он в конечном итоге напрямую запросит само доменное имя. ndots означает n точек.

Например:

1
2
3
4
5
6
7
sh-4.2# host -v baidu.com
Trying "baidu.com.<namespace>.svc.cluster.local"
Trying "baidu.com.svc.cluster.local"
Trying "baidu.com.cluster.local"
Trying "baidu.com.localdomain"
Trying "baidu.com"
......

Не использовать образы alpine

Использовать FQDN

Поскольку доменные имена разрешаются уровень за уровнем справа налево, например google.com, это на самом деле google.com., точка после com называется корневым доменом. При разрешении сначала разрешается ., затем разрешается .com, .com называется доменом верхнего уровня, наконец разрешается google.

Использование FQDN: (Полностью квалифицированное доменное имя) предназначено для минимизации давления разрешения на внутренний DNS (например, coreDNS, DNS узла) насколько возможно.

Переоткрыть сокет

1
2
3
4
5
6
7
        lifecycle:
          postStart:
            exec:
              command:
              - /bin/sh
              - -c 
              - "/bin/echo 'options single-request-reopen' >> /etc/resolv.conf"

Установка переоткрытия сокета предназначена для избежания одновременных A, AAAA запросов в контейнерах.

2-уровневый домен напрямую идет на разрешение верхнего уровня

Ссылка kubernetes использует образы на основе alpine не может правильно разрешить внешний DNS

Запуск sed -i 's/options ndots:5/#options ndots:5/g' /etc/resolv.conf напрямую выдаст ошибку.

Команда echo alpine проглатывает символы новой строки, и если формат resolv.conf неправильный, разрешение DNS выдаст ошибку.

1
2
3
4
5
  dnsConfig:
    options:
      - name: ndots
        value: "2"
      - name: single-request-reopen

Удален options ndots:5, изменено на значение по умолчанию 1. Таким образом, контейнеры, напрямую обращающиеся к , все еще в порядке, проходя через список search, `..svc.cluster.local`, все еще могут быть доступны.

При разрешении Google.com фактически разрешается Google.com., количество точек превышает 1, поэтому оно не проходит через список search, напрямую использует DNS верхнего уровня.

В итоге, удаление ndots/установка ndots в 1 снижает возможность частых DNS-запросов. Это имеет “чудесные эффекты” для разрешения внешних IP.

Но если этот хост запускает другие контейнеры (разве это не ерунда, если узел не запускает несколько контейнеров, зачем использовать kubernetes), другие контейнеры также будут запрашивать одновременно, проблема SNAT все еще появится, поэтому изменение файла /etc/resolv.conf не может решить корневую проблему.

Обходной путь 1

1
2
3
4
5
6
7
          lifecycle:
            postStart:
              exec:
                command:
                - /bin/sh
                - -c 
                - "head -n 2 /etc/resolv.conf > /etc/temp.conf;cat /etc/temp.conf > /etc/resolv.conf;rm -rf /etc/temp.conf"

Обходной путь 2

1
2
3
4
5
6
7
      initContainers:
      - name: alpine
        image: alpine
        command:
         - /bin/sh
         - -c 
         - "head -n 2 /etc/resolv.conf > /etc/temp.conf;cat /etc/temp.conf > /etc/resolv.conf;rm -rf /etc/temp.conf"

Производные проблемы

DNAT

Контейнеры, обращающиеся к clusterIP (поскольку это виртуальный IP, требуется DNAT), также могут иметь этот тип проблемы с таймаутом.

Не принудительно добавлять драму при доступе к svc того же namespace

Формат виртуального домена для не-head service: <svc>.<namespace>.svc.cluster.local

Если наш контейнер напрямую обращается к <svc>.<namespace>.svc.cluster.local, из-за настроек DNS по умолчанию количество разрешений фактически больше. Правильный способ - обращаться к <svc>

Пример: предположим, что под test есть svc s

1
2
3
4
host -v s 
# Разрешить 1 раз
host -v s.test.svc.cluster.local
# Разрешить 4 раза

Итак, при доступе к другим svc в том же namespace просто используйте имя svc напрямую, нет необходимости хвастаться использованием формата <svc>.<namespace>.svc.cluster.local.

Другие знания

Типы записей DNS

  1. A-запись: Запись адреса, используется для указания IPv4-адреса доменного имени (например, 8.8.8.8). Если нужно указать доменное имя на IP-адрес, нужно добавить A-запись.
  2. CNAME: Если нужно указать доменное имя на другое доменное имя, которое затем предоставляет IP-адрес, нужно добавить CNAME-запись.
  3. TXT: Здесь можно заполнить что угодно, ограничение длины 255. Подавляющее большинство TXT-записей используется для SPF-записей (антиспам).
  4. NS: Запись сервера имен. Если нужно передать разрешение поддомена другим поставщикам DNS-услуг, нужно добавить NS-запись.
  5. AAAA: Используется для указания IPv6-адреса, соответствующего имени хоста (или доменному имени) (например, ff06:0:0:0:0:0:0:c3) запись.
  6. MX: Если нужно настроить почту, чтобы письма могли быть получены, нужно добавить MX-запись.
  7. Явный URL: Когда нужно 301-редирект с одного адреса на другой, нужно добавить явную URL-запись (Примечание: DNSPod в настоящее время поддерживает только 301-редирект).
  8. Неявный URL: Аналогично явному URL, разница в том, что неявный URL не изменяет доменное имя в адресной строке.
  9. SRV: Записывает, какой компьютер предоставляет какую службу. Формат: имя службы, точка, тип протокола, например, _xmpp-server._tcp.

Используемые команды

Способ установки:

1
2
3
  yum install -y bind-utils
  sudo apt-get install -y dnsutils
  apk add bind-tools

dig

dig +trace +ndots=5 +search $host

host

host -v $host

Ссылки:

  1. Понимание DNAT, SNAT и MASQUERADE в iptables
  2. Подробное объяснение файла /etc/resolv.conf корневой файловой системы Linux
  3. kube-dns per node #45363
  4. Прерывистые задержки DNS на 5 секунд #56903
  5. Гонка conntrack и таймауты поиска DNS
  6. /etc/resolv.conf
  7. Конфигурация search и ndots в /etc/resolv.conf
  8. DNS для служб и подов


💬 讨论 / Discussion

对这篇文章有想法?欢迎在 GitHub 上发起讨论。
Have thoughts on this post? Start a discussion on GitHub.

在 GitHub 参与讨论 / Discuss on GitHub