Помните опыт тайм-аута при вызове внутренней службы K8S для разрешения доменного имени.

Kubernetes

предисловие

В последнее время онлайн-k8s время от времени сталкивался с некоторыми проблемами тайм-аута между внутренними службами.Из журнала мы можем узнать, что все причины тайм-аута кроются в域名解析На вышеизложенном и разрешении доменного имени внутри k8s истекло время ожидания, поэтому непосредственно замените внутреннее доменное имя IP-адресом службы k8s, понаблюдайте в течение определенного периода времени и обнаружите, что тайм-аута нет, но поскольку использование IP-адреса службы не является долгосрочным решением, поэтому я должен найти решение.

повторяющийся

Вначале коллеги по эксплуатации и техническому обслуживанию использовали в вызывающем модулеabИнструмент провел несколько стресс-тестов на целевом сервисе и не обнаружил запросов на тайм-аут. После моего вмешательства я проанализировал его.abЭтот тип инструмента стресс-тестирования http должен иметь кэш DNS, и мы в основном хотим проверить производительность службы DNS, поэтому мы напрямую запустили инструмент стресс-тестирования, который выполняет только разрешение доменных имен.Код выглядит следующим образом:

package main

import (
	"context"
	"flag"
	"fmt"
	"net"
	"sync/atomic"
	"time"
)

var host string
var connections int
var duration int64
var limit int64
var timeoutCount int64

func main() {
	// os.Args = append(os.Args, "-host", "www.baidu.com", "-c", "200", "-d", "30", "-l", "5000")

	flag.StringVar(&host, "host", "", "Resolve host")
	flag.IntVar(&connections, "c", 100, "Connections")
	flag.Int64Var(&duration, "d", 0, "Duration(s)")
	flag.Int64Var(&limit, "l", 0, "Limit(ms)")
	flag.Parse()

	var count int64 = 0
	var errCount int64 = 0
	pool := make(chan interface{}, connections)
	exit := make(chan bool)
	var (
		min int64 = 0
		max int64 = 0
		sum int64 = 0
	)

	go func() {
		time.Sleep(time.Second * time.Duration(duration))
		exit <- true
	}()
endD:
	for {
		select {
		case pool <- nil:
			go func() {
				defer func() {
					<-pool
				}()
				resolver := &net.Resolver{}
				now := time.Now()
				_, err := resolver.LookupIPAddr(context.Background(), host)
				use := time.Since(now).Nanoseconds() / int64(time.Millisecond)
				if min == 0 || use < min {
					min = use
				}
				if use > max {
					max = use
				}
				sum += use
				if limit > 0 && use >= limit {
					timeoutCount++
				}
				atomic.AddInt64(&count, 1)
				if err != nil {
					fmt.Println(err.Error())
					atomic.AddInt64(&errCount, 1)
				}
			}()
		case <-exit:
			break endD
		}
	}

	fmt.Printf("request count:%d\nerror count:%d\n", count, errCount)
	fmt.Printf("request time:min(%dms) max(%dms) avg(%dms) timeout(%dn)\n", min, max, sum/count, timeoutCount)
}

Скомпилированная бинарная программа напрямую закидывается в соответствующий pod-контейнер для опрессовки:

# 200个并发,持续30秒
./dns -host {service}.{namespace} -c 200 -d 30

На этот раз можно обнаружить, что максимальная трудоемкость5sРезультаты многих тестов схожи:

А тайм-аут HTTP-вызовов между нашими внутренними сервисами обычно устанавливается в3sИз этого следует, что это должно быть то же самое, что и ситуация с тайм-аутом онлайн.В случае высокого параллелизма возникнут некоторые тайм-ауты разрешения доменных имен, что приведет к сбою HTTP-запроса.

причина

Сначала я подумал, что этоcorednsПроблема, поэтому я нашел обновление эксплуатации и обслуживания.corednsЗатем версия проверяется под давлением, и обнаруживается, что проблема все еще существует, что указывает на то, что это не проблема версии, не так ли?corednsЭто из-за плохой работы? Думать об этом не возможно.Параллелизм всего 200 не выдерживает, да и производительность слабовата. В сочетании с предыдущими данными стресс-теста средний ответ вполне нормальный (82мс), но некоторые запросы будут задерживаться , и все они около 5 секунд, так что я беруk8s dns 5sИскал в гугле по ключевому слову .Не знал, если не знал.Оказалась большая дыра в k8s (на самом деле к k8s никакого отношения не имеет, но решения не предусмотрено на уровне k8s).

причина тайм-аута 5s

в линуксglibcТайм-аут резолвера по умолчанию составляет 5 с, а причиной тайм-аута является ядро.conntrackБаги модуля.

Инженер ткацкого производства Мартинас Пумпутис дал очень подробный анализ проблемы:woohoo.weave.works/blog/race - от…

Цитировать еще раз здесьIM ROC.IO/posts/Cool Belle…Пояснение в статье:

DNS-клиент (glibc или musl libc) будет одновременно запрашивать записи A и AAAA. Связь с DNS-сервером будет естественно сначала подключаться (устанавливать fd), а затем использовать этот fd для отправки пакетов запроса. Поскольку UDP является протоколом без сохранения состояния, он не подключается при подключении. Пакет будет отправлен, и запись conntrack не будет создана, а записи A и AAAA параллельных запросов будут использовать один и тот же fd для отправки пакетов по умолчанию. Если два пакета не были вставлены в запись conntrack , netfilter создаст для них записи conntrack соответственно, а запрос kube-dns или coredns в кластере — это доступ к CLUSTER-IP, и пакет в конечном итоге будет преобразован в один IP-адрес POD конечной точки, когда два пакета DNAT в один и тот же IP-адрес POD, их пятерка одинакова, и последний пакет будет отброшен, когда он будет окончательно вставлен, если есть только один экземпляр pod-копии dns. Ситуация очень проста (это всегда DNAT на тот же IP-адрес POD), явление заключается в том, что запрос DNS истекает, политика клиента по умолчанию заключается в том, чтобы ждать 5 с для автоматического повтора попытки, если повторная попытка успешна, мы видим явление, что запрос DNS имеет время задержки 5 с.

решение

Решение (1): Отправьте DNS-запрос по протоколу TCP.

пройти черезresolv.confизuse-vcвозможность включить протокол TCP

тестовое задание

  1. Исправлять/etc/resolv.confфайла, добавьте строку текста в конце:

    options use-vc
    
  2. Для стресс-теста:

    # 200个并发,持续30秒,记录超过5s的请求个数
    ./dns -host {service}.{namespace} -c 200 -d 30 -l 5000
    

    Результат выглядит следующим образом:

В заключение

не появился5sПроблема тайм-аута решена, но время выполнения некоторых запросов по-прежнему относительно велико.4sСреднее время выше, чем у протокола UPD, и эффект не очень хороший.

Решение (2). Избегайте параллелизма одного и того же DNS-запроса из пяти кортежей.

пройти черезresolv.confизsingle-request-reopenа такжеsingle-requestВарианты, которых следует избегать:

  • повторное открытие с одним запросом (glibc>=2.9) Отправляйте запросы типа A и запросы типа AAAA, используя разные исходные порты. Таким образом, два запроса не занимают одну и ту же запись в таблице conntrack, что позволяет избежать конфликта.
  • одиночный запрос (glibc>=2.10) Избегайте параллелизма, вместо этого отправляйте запросы типов A и AAAA последовательно, параллелизм отсутствует, что позволяет избежать конфликтов.

проверка одиночного запроса-повторного открытия

  1. Исправлять/etc/resolv.confфайла, добавьте строку текста в конце:

    options single-request-reopen
    
  2. Для стресс-теста:

    # 200个并发,持续30秒,记录超过5s的请求个数
    ./dns -host {service}.{namespace} -c 200 -d 30 -l 5000
    

    Результат выглядит следующим образом:

тестовый одиночный запрос

  1. Исправлять/etc/resolv.confфайла, добавьте строку текста в конце:

    options single-request
    
  2. Для стресс-теста:

    # 200个并发,持续30秒,记录超过5s的请求个数
    ./dns -host {service}.{namespace} -c 200 -d 30 -l 5000
    

    Результат выглядит следующим образом:

В заключение

По результатам испытаний под давлением видно, чтоsingle-request-reopenа такжеsingle-requestОпция действительно может значительно сократить время разрешения доменного имени.

Об этапах реализации и недостатках схемы (1) и схемы (2)

Этапы реализации

На самом деле, это для контейнера/etc/resolv.confВарианты добавления файлов, на данный момент есть два подходящих решения:

  1. Установить, изменив хук postStart пода
lifecycle:
  postStart:
    exec:
      command:
        - /bin/sh
        - -c
        - "/bin/echo 'options single-request-reopen' >> /etc/resolv.conf"
  1. Установите, изменив template.spec.dnsConfig модуля.
template:
  spec:
    dnsConfig:
      options:
        - name: single-request-reopen

: требуется версия k8s >=1.9

недостаток

не поддерживаетсяalpineКонтейнер базового образа, т.к.aplineиспользуется в нижней частиmusl libcБиблиотека не поддерживает эти параметры resolv.conf, поэтому при использованииalpineПриложения, созданные с использованием базовых образов, по-прежнему не могут избежать проблемы тайм-аута.

Решение (3): локальный кэш DNS

На самом деле, официальные лица k8s также поняли, что эта проблема относительно распространена, и дали решение для развертывания coredns в режиме кэширования в качестве набора демонов:GitHub.com/Это так особенно/…

Общий принцип таков:

Локальный DNS-кэш развертывает Pod с помощью hostNetwork на каждом узле в режиме DaemonSet, создает сетевую карту и привязывает IP-адрес локального DNS, DNS-запрос локального Pod направляется в локальный DNS, а затем извлекает кэш или продолжает работу. использовать TCP для запроса DNS-анализа вышестоящего кластера (из-за использования TCP один и тот же сокет будет выполнять только три рукопожатия один раз, и не будет одновременного создания записей conntrack, поэтому не будет конфликтов conntrack)

развертывать

  1. получить текущийkube-dns serviceIP-адрес кластера
# kubectl -n kube-system get svc kube-dns -o jsonpath="{.spec.clusterIP}"
10.96.0.10
  1. Загрузите официальный шаблон yaml для замены ключевых слов
wget -O nodelocaldns.yaml "https://github.com/kubernetes/kubernetes/raw/master/cluster/addons/dns/nodelocaldns/nodelocaldns.yaml" && \
sed -i 's/__PILLAR__DNS__SERVER__/10.96.0.10/g' nodelocaldns.yaml && \
sed -i 's/__PILLAR__LOCAL__DNS__/169.254.20.10/g' nodelocaldns.yaml && \
sed -i 's/__PILLAR__DNS__DOMAIN__/cluster.local/g' nodelocaldns.yaml && \
sed -i 's/__PILLAR__CLUSTER__DNS__/10.96.0.10/g' nodelocaldns.yaml && \
sed -i 's/__PILLAR__UPSTREAM__SERVERS__/\/etc\/resolv.conf/g' nodelocaldns.yaml
  1. Окончательный файл yaml выглядит следующим образом:
# Copyright 2018 The Kubernetes Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

apiVersion: v1
kind: ServiceAccount
metadata:
  name: node-local-dns
  namespace: kube-system
  labels:
    kubernetes.io/cluster-service: "true"
    addonmanager.kubernetes.io/mode: Reconcile
---
apiVersion: v1
kind: Service
metadata:
  name: kube-dns-upstream
  namespace: kube-system
  labels:
    k8s-app: kube-dns
    kubernetes.io/cluster-service: "true"
    addonmanager.kubernetes.io/mode: Reconcile
    kubernetes.io/name: "KubeDNSUpstream"
spec:
  ports:
    - name: dns
      port: 53
      protocol: UDP
      targetPort: 53
    - name: dns-tcp
      port: 53
      protocol: TCP
      targetPort: 53
  selector:
    k8s-app: kube-dns
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: node-local-dns
  namespace: kube-system
  labels:
    addonmanager.kubernetes.io/mode: Reconcile
data:
  Corefile: |
    cluster.local:53 {
        errors
        cache {
                success 9984 30
                denial 9984 5
        }
        reload
        loop
        bind 169.254.20.10 10.96.0.10
        forward . 10.96.0.10 {
                force_tcp
        }
        prometheus :9253
        health 169.254.20.10:8080
        }
    in-addr.arpa:53 {
        errors
        cache 30
        reload
        loop
        bind 169.254.20.10 10.96.0.10
        forward . 10.96.0.10 {
                force_tcp
        }
        prometheus :9253
        }
    ip6.arpa:53 {
        errors
        cache 30
        reload
        loop
        bind 169.254.20.10 10.96.0.10
        forward . 10.96.0.10 {
                force_tcp
        }
        prometheus :9253
        }
    .:53 {
        errors
        cache 30
        reload
        loop
        bind 169.254.20.10 10.96.0.10
        forward . /etc/resolv.conf {
                force_tcp
        }
        prometheus :9253
        }
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: node-local-dns
  namespace: kube-system
  labels:
    k8s-app: node-local-dns
    kubernetes.io/cluster-service: "true"
    addonmanager.kubernetes.io/mode: Reconcile
spec:
  updateStrategy:
    rollingUpdate:
      maxUnavailable: 10%
  selector:
    matchLabels:
      k8s-app: node-local-dns
  template:
    metadata:
      labels:
        k8s-app: node-local-dns
    spec:
      priorityClassName: system-node-critical
      serviceAccountName: node-local-dns
      hostNetwork: true
      dnsPolicy: Default # Don't use cluster DNS.
      tolerations:
        - key: "CriticalAddonsOnly"
          operator: "Exists"
      containers:
        - name: node-cache
          image: k8s.gcr.io/k8s-dns-node-cache:1.15.7
          resources:
            requests:
              cpu: 25m
              memory: 5Mi
          args:
            [
              "-localip",
              "169.254.20.10,10.96.0.10",
              "-conf",
              "/etc/Corefile",
              "-upstreamsvc",
              "kube-dns-upstream",
            ]
          securityContext:
            privileged: true
          ports:
            - containerPort: 53
              name: dns
              protocol: UDP
            - containerPort: 53
              name: dns-tcp
              protocol: TCP
            - containerPort: 9253
              name: metrics
              protocol: TCP
          livenessProbe:
            httpGet:
              host: 169.254.20.10
              path: /health
              port: 8080
            initialDelaySeconds: 60
            timeoutSeconds: 5
          volumeMounts:
            - mountPath: /run/xtables.lock
              name: xtables-lock
              readOnly: false
            - name: config-volume
              mountPath: /etc/coredns
            - name: kube-dns-config
              mountPath: /etc/kube-dns
      volumes:
        - name: xtables-lock
          hostPath:
            path: /run/xtables.lock
            type: FileOrCreate
        - name: kube-dns-config
          configMap:
            name: kube-dns
            optional: true
        - name: config-volume
          configMap:
            name: node-local-dns
            items:
              - key: Corefile
                path: Corefile.base

Несколько деталей можно увидеть через yaml:

  1. Используется тип развертыванияDaemonSet, т.е. запускать DNS-сервис на каждом узле k8s
  2. hostNetworkсобственностьtrue, то есть напрямую использовать сетевую карту физической машины узла для привязки портов, чтобы pod в этом узле узла мог получить прямой доступ к службе dns, без пробрасывания через службу, не будет никакого DNAT
  3. dnsPolicyсобственностьDefault, не использовать кластерный DNS, напрямую использовать локальные настройки DNS при разрешении внешних доменных имен
  4. привязать к узлу узел169.254.20.10а также10.96.0.10IP, так что модулям под узлом нужно только установить DNS на169.254.20.10Вы можете напрямую получить доступ к службе DNS на хосте.

тестовое задание

  1. Исправлять/etc/resolv.confсервер имён в файле:

    nameserver 169.254.20.10
    
  2. Для стресс-теста:

    # 200个并发,持续30秒,记录超过5s的请求个数
    ./dns -host {service}.{namespace} -c 200 -d 30 -l 5000
    

    Результат выглядит следующим образом:

В заключение

В результате стресс-теста установлено, что проблема тайм-аута не решена.conntrackСитуация, которую должен показать конфликт, похожа на ситуацию плана (2), а может быть, поза, которую я использовал, неверна, но хотя эта проблема все еще существует, но черезDaemonSetДавление распределяется между каждым DNS-узлом, запрашивающим узел, что может эффективно облегчить проблемы с тайм-аутом DNS.

воплощать в жизнь

  • Решение (1): Установите, изменив template.spec.dnsConfig модуля, и установитеdnsPolicyУстановить какNone
template:
  spec:
    dnsConfig:
      nameservers:
        - 169.254.20.10
      searches:
        - public.svc.cluster.local
        - svc.cluster.local
        - cluster.local
      options:
        - name: ndots
        value: "5"
    dnsPolicy: None
  • Вариант (2): изменить значение по умолчаниюcluster-dns, на узле узел будет/etc/systemd/system/kubelet.service.d/10-kubeadm.confв файле--cluster-dnsЗначение параметра изменено на169.254.20.10, затем перезагрузитеkubelet
systemctl restart kubelet

: путь к файлу конфигурации также может быть/etc/kubernetes/kubelet

окончательное решение

В итоге решил использовать方案(二)+方案(三)Используются вместе, чтобы оптимизировать эту проблему в наибольшей степени, и заменить все базовые изображения в строке на нестандартные.aplineЗеркальная версия, пока что проблема в основном решена, и я надеюсь, что официальные лица K8S смогут напрямую интегрировать в нее эту функцию как можно скорее.