предисловие
В последнее время онлайн-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
тестовое задание
-
Исправлять
/etc/resolv.conf
файла, добавьте строку текста в конце:options use-vc
-
Для стресс-теста:
# 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 последовательно, параллелизм отсутствует, что позволяет избежать конфликтов.
проверка одиночного запроса-повторного открытия
-
Исправлять
/etc/resolv.conf
файла, добавьте строку текста в конце:options single-request-reopen
-
Для стресс-теста:
# 200个并发,持续30秒,记录超过5s的请求个数 ./dns -host {service}.{namespace} -c 200 -d 30 -l 5000
Результат выглядит следующим образом:
тестовый одиночный запрос
-
Исправлять
/etc/resolv.conf
файла, добавьте строку текста в конце:options single-request
-
Для стресс-теста:
# 200个并发,持续30秒,记录超过5s的请求个数 ./dns -host {service}.{namespace} -c 200 -d 30 -l 5000
Результат выглядит следующим образом:
В заключение
По результатам испытаний под давлением видно, чтоsingle-request-reopen
а такжеsingle-request
Опция действительно может значительно сократить время разрешения доменного имени.
Об этапах реализации и недостатках схемы (1) и схемы (2)
Этапы реализации
На самом деле, это для контейнера/etc/resolv.conf
Варианты добавления файлов, на данный момент есть два подходящих решения:
- Установить, изменив хук postStart пода
lifecycle:
postStart:
exec:
command:
- /bin/sh
- -c
- "/bin/echo 'options single-request-reopen' >> /etc/resolv.conf"
- Установите, изменив 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)
развертывать
- получить текущий
kube-dns service
IP-адрес кластера
# kubectl -n kube-system get svc kube-dns -o jsonpath="{.spec.clusterIP}"
10.96.0.10
- Загрузите официальный шаблон 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
- Окончательный файл 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:
- Используется тип развертывания
DaemonSet
, т.е. запускать DNS-сервис на каждом узле k8s -
hostNetwork
собственностьtrue
, то есть напрямую использовать сетевую карту физической машины узла для привязки портов, чтобы pod в этом узле узла мог получить прямой доступ к службе dns, без пробрасывания через службу, не будет никакого DNAT -
dnsPolicy
собственностьDefault
, не использовать кластерный DNS, напрямую использовать локальные настройки DNS при разрешении внешних доменных имен - привязать к узлу узел
169.254.20.10
а также10.96.0.10
IP, так что модулям под узлом нужно только установить DNS на169.254.20.10
Вы можете напрямую получить доступ к службе DNS на хосте.
тестовое задание
-
Исправлять
/etc/resolv.conf
сервер имён в файле:nameserver 169.254.20.10
-
Для стресс-теста:
# 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 смогут напрямую интегрировать в нее эту функцию как можно скорее.