k8s dns解析
集群内域名解析原理
Kubernetes 集群节点上 kubelet 有--cluster-dns=${dns-service-ip} 和
--cluster-domain=${default-local-domain} 两个 dns 相关参数,
分别被用来设置集群DNS服务器的IP地址和主域名后缀。
查看集群 default 命名空间下 dnsPolicy:ClusterFirst模式的 Pod 内的 DNS 域名解析配置文件 /etc/resolv.conf 内容:
nameserver 172.24.0.10
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5
各参数描述如下:
nameserver: 定义 DNS 服务器的 IP 地址。
search: 设置域名的查找后缀规则,查找配置越多,说明域名解析查找匹配次数越多。
集群匹配有 default.svc.cluster.local、svc.cluster.local、cluster.local 3个后缀,
option: 定义域名解析配置文件选项,支持多个KV值。
例如该参数设置成ndots:5,说明如果访问的域名字符串内的点字符数量超过ndots值,则认为是完整域名,
并被直接解析;如果不足ndots值,则追加search段后缀再进行查询。
根据上述文件配置,在 Pod 内尝试解析:
1、同命名空间下服务,如 kubernetes:
添加一次 search 域,发送kubernetes.default.svc.cluster.local. 一次 ipv4 域名解析请求
到172.24.0.10 进行解析即可。
2、跨命名空间下的服务,如 kube-dns.kue-system:
添加两次 search 域,发送 kube-dns.kue-system.default.svc.cluster.local. 和
kube-dns.kue-system.svc.cluster.local. 两次 ipv4 域名解析请求到
172.24.0.10 才能解析出正确结果。
3、集群外服务,如 aliyun.com:
添加三次 search 域,发送 aliyun.com.default.svc.cluster.local.、
aliyun.com.svc.cluster.local.、aliyun.com.cluster.local. 和 aliyun.com 四次
ipv4 域名解析请求到 172.24.0.10 才能解析出正确结果。
Pod dnsPolicy
Kubernetes 集群中支持通过 dnsPolicy 字段为每个 Pod 配置不同的 DNS 策略。
目前支持四种策略:
1、None
表示空的DNS设置,这种方式一般用于想要自定义 DNS 配置的场景,而且,
往往需要和 dnsConfig 配合一起使用达到自定义 DNS 的目的。
2、Default
Default 的方式不一定是使用宿主机的方式,这种说法并不准确。
这种方式其实是让 kubelet 来决定使用何种 DNS 策略。
而 kubelet 默认的方式,就是使用宿主机的 /etc/resolv.conf
kubelet 是可以灵活来配置使用什么文件来进行DNS策略的,我们完全可以使用 kubelet 的参数:
–resolv-conf=/etc/resolv.conf 来决定你的DNS解析文件地址。
3、ClusterFirst
这种方式,表示 POD 内的 DNS 使用集群中配置的 DNS 服务,简单来说,就是使用 Kubernetes 中
kubedns 或 coredns 服务进行域名解析。如果解析不成功,才会使用宿主机的 DNS 配置进行解析。
4、ClusterFirstWithHostNet
在某些场景下,我们的 POD 是用 HOST 模式启动的(HOST模式,是共享宿主机网络的),
一旦用 HOST 模式,表示这个 POD 中的所有容器,
都要使用宿主机的 /etc/resolv.conf 配置进行DNS查询,但如果你想使用了 HOST 模式,
还继续使用 Kubernetes 的DNS服务,那就将 dnsPolicy 设置为 ClusterFirstWithHostNet。
CoreDNS
CoreDNS 目前是 Kubernetes 标准的服务发现组件,dnsPolicy: ClusterFirst 模式的 Pod 会使用 CoreDNS 来解析集群内外部域名。
在 Kubernetes 中,服务发现有几种方式:
①:基于环境变量的方式
②:基于内部域名的方式
DNS 如何解析,依赖容器内 resolv 文件的配置
cat /etc/resolv.conf
nameserver 10.233.0.3
search default.svc.cluster.local svc.cluster.local cluster.local
这个文件中,配置的 DNS Server,一般就是 K8S 中,kubedns 的 Service 的 ClusterIP
[root@node4 user1]# kubectl get svc -n kube-system
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kube-dns ClusterIP 10.233.0.3 <none> 53/UDP,53/TCP 270d
kubernetes-dashboard ClusterIP 10.233.22.223 <none> 443/TCP 124d
所以,所有域名的解析,其实都要经过 kubedns 的虚拟IP 10.233.0.3 进行解析,不论是 Kubernetes 内部域名还是外部的域名。
Kubernetes 中,域名的全称,必须是 service-name.namespace.svc.cluster.local 这种模式,服务名,就是Kubernetes中 Service 的名称
所以,当我们执行下面的命令时:
curl b
必须得有一个 Service 名称为 b,这是前提。
在容器内,会根据 /etc/resolve.conf 进行解析流程。选择 nameserver 10.233.0.3 进行解析,
然后,用字符串 “b”,依次带入 /etc/resolve.conf 中的 search 域,进行DNS查找,分别是:
// search 内容类似如下(不同的pod,第一个域会有所不同)
search default.svc.cluster.local svc.cluster.local cluster.local
b.default.svc.cluster.local -> b.svc.cluster.local -> b.cluster.local ,直到找到为止。
curl b,要比 curl b.default 效率高?
答案是肯定的,因为 curl b.default,多经过了一次 DNS 查询。
当执行 curl b.default,也就使用了带有命名空间的内部域名时,容器的第一个 DNS 请求是
// b.default + default.svc.cluster.local
b.default.default.svc.cluster.local
当请求不到 DNS 结果时,使用
// b.default + svc.cluster.local
b.default.svc.cluster.local
进行请求,此时才可以得到正确的DNS解析。
访问外部域名走 search 域吗
在 Kubernetes 中,其实 /etc/resolv.conf 这个文件,并不止包含 nameserver 和 search 域,
还包含了非常重要的一项:ndots。
[root@xxxx-67f54c6dff-h4zxq /]# cat /etc/resolv.conf
nameserver 10.233.0.3
search cicd.svc.cluster.local svc.cluster.local cluster.local
options ndots:5
ndots:5,表示:如果查询的域名包含的点“.”,不到5个,那么进行DNS查找,将使用非完全限定名称
(或者叫绝对域名),如果你查询的域名包含点数大于等于5,那么DNS查询,默认会使用绝对域名进行查询。
域名中点数少于5个的情况:
// 对域名 a.b.c.d.ccccc 进行DNS解析请求
[root@xxxxx-67f54c6dff-h4zxq /]# nslookup a.b.c.d.ccccc 172.22.121.65
Server: 172.22.121.65
Address: 172.22.121.65#53
** server can't find a.b.c.d.ccccc: NXDOMAIN
// 抓包数据如下:
18:08:11.013497 IP 172.20.92.100.33387 > node011094.domain: 28844+ A? a.b.c.d.ccccc.cicd.svc.cluster.local. (54)
18:08:11.014337 IP 172.20.92.100.33952 > node011094.domain: 57782+ A? a.b.c.d.ccccc.svc.cluster.local. (49)
18:08:11.015079 IP 172.20.92.100.45984 > node011094.domain: 55144+ A? a.b.c.d.ccccc.cluster.local. (45)
18:08:11.015747 IP 172.20.92.100.54589 > node011094.domain: 22860+ A? a.b.c.d.ccccc. (31)
18:08:11.015970 IP node011094.36383 > 192.168.x.x.domain: 22860+ A? a.b.c.d.ccccc. (31)
// 结论:
// 点数少于5个,先走search域,最后将其视为绝对域名进行查询
域名中点数>=5个的情况:
// 对域名 a.b.c.d.e.ccccc 进行DNS解析请求
[root@xxxxx-67f54c6dff-h4zxq /]# nslookup a.b.c.d.e.ccccc 172.22.121.65
Server: 172.22.121.65
Address: 172.22.121.65#53
** server can't find a.b.c.d.e.ccccc: NXDOMAIN
// 抓包数据如下:
18:10:14.514595 IP 172.20.92.100.34423 > node011094.domain: 61170+ A? a.b.c.d.e.ccccc. (33)
18:10:14.514856 IP node011094.58522 > 192.168.x.x.domain: 61170+ A? a.b.c.d.e.ccccc. (33)
18:10:14.515880 IP 172.20.92.100.49328 > node011094.domain: 267+ A? a.b.c.d.e.ccccc.cicd.svc.cluster.local. (56)
18:10:14.516678 IP 172.20.92.100.35651 > node011094.domain: 54181+ A? a.b.c.d.e.ccccc.svc.cluster.local. (51)
18:10:14.517356 IP 172.20.92.100.33259 > node011094.domain: 53022+ A? a.b.c.d.e.ccccc.cluster.local. (47)
// 结论:
// 点数>=5个,直接视为绝对域名进行查找,只有当查询不到的时候,才继续走 search 域。
如何优化 DNS 请求浪费的情况
优化方式1:使用全限定域名
其实最直接,最有效的优化方式,就是使用 “fully qualified name”,
简单来说,使用“完全限定域名”(也叫绝对域名)
即:你访问的域名,必须要以 “.” 为后缀,这样就会避免走 search 域进行匹配,我们抓包再试一次:
// 注意:youku.com 后边有一个点 .
nslookup youku.com. 172.22.121.65
在DNS服务容器上抓到的包如下:
16:57:07.628112 IP 172.20.92.100.36772 > nodexxxx.domain: 46851+ [1au] A? youku.com. (38)
16:57:07.628339 IP nodexxxx.47350 > 192.168.x.x.domain: 46851+ [1au] A? youku.com. (38)
并没有多余的DNS请求。
优化方式2:具体应用配置特定的 ndots
在 Kubernetes 中,默认设置了 ndots 值为5,是因为,Kubernetes 认为,内部域名,最长为5,
要保证内部域名的请求,优先走集群内部的DNS,而不是将内部域名的DNS解析请求,有打到外网的机会,
Kubernetes 设置 ndots 为5是一个比较合理的行为。
如果你需要定制这个长度,最好是为自己的业务,单独配置 ndots 即可:
apiVersion: v1
kind: Pod
metadata:
namespace: default
name: dns-example
spec:
containers:
- name: test
image: nginx
dnsConfig:
options:
- name: ndots
value: "1"
k8s服务发现方式以及原理:环境变量
在 Node 上新创建一个 Pod 时,kubelet 会为每个 Pod(容器)添加一组环境变量,其中就包括当前系统中已经存在的 Service 的 IP 地址和端口号。
1、kubernetes Service 环境变量
Kubernetes为每个Service资源生成包括以下形式的环境变量在内的一系列环境变量,
在同一名称空间中创建的Pod对象都会自动拥有这些变量。
{SVCNAME}_SERVICE_HOST
{SVCNAME}_SERVICE_PORT
如果SVCNAME中使用了连接线,那么Kubernetes会在定义为环境变量时将其转换为下划线。
2、Docker Link 形式的环境变量
Docker使用--link选项实现容器连接时所设置的环境变量形式,
在创建Pod对象时,Kubernetes也会将与此形式兼容的一系列环境变量注入Pod对象中。
例如,在Service资源myapp-svc创建后创建的Pod对象中查看可用的环境变量,
其中以MYAPP_SVC_SERVICE开头的表示Kubernetes Service环境变量,
名称中不包含“SERVICE”字符串的环境变量为Docker Link形式的环境变量:
/ # env | grep -i myapp
MYAPP_SVC_PORT_80_TCP_ADDR=10.98.57.156
MYAPP_SVC_PORT_80_TCP_PORT=80
HOSTNAME=myapp-deploy-5cbd66595b-2lhds
MYAPP_SVC_PORT_80_TCP_PROTO=tcp
MYAPP_SVC_PORT_80_TCP=tcp://10.98.57.156:80
MYAPP_SVC_SERVICE_HOST=10.98.57.156
MYAPP_SVC_SERVICE_PORT=80
MYAPP_SVC_PORT=tcp://10.98.57.156:80
基于环境变量的服务发现的特点:
基于环境变量的服务发现其功能简单、易用,但存在一定的局限,例如,仅有那些与创建Pod对象在同一名称空间中且事先存在的Service对象的信息才会以环境变量的形式注入,那些处于非同一名称空间,或者是在Pod资源创建之后才创建的Service对象的相关环境变量则不会被添加。幸而,基于DNS的发现机制并不存在此类限制。
k8s服务发现方式以及原理:dns配置注入
kubelet启动Pod的时候,会将DNS配置注入到Pod中,其实就是kubelet 会为每个 Pod 重写此文件
DNS 查询可以使用 Pod 中的 /etc/resolv.conf 展开。
当pod调度到节点上之后,kubelet会来给pod配置具体的resolv.conf内容:
1 kubelet会先创建并运行pod的sandbox,然后获取到sandbox的ResolvConfPath
(/var/lib/docker/containers/xxxxxxx/resolv.conf),
接下来,把dns policy的具体内容写到sandbox的ResolvConfPath(直接覆盖写)。
2 kubelet继续创建同一个pod中的其他container,并且使用相同的ResolvConfPath
(同一个pod的所有容器的ResolvConfPath在宿主机上的真实源是同一个)。
所以,可以看到,pod内的resolv.conf是pod在创建的时候就确定下来的。
源码剖析:
func (ds *dockerService) RunPodSandbox(ctx context.Context, r *runtimeapi.RunPodSandboxRequest) (*runtimeapi.RunPodSandboxResponse, error) {
config := r.GetConfig()
// Step 1: Pull the image for the sandbox.
image := defaultSandboxImage
podSandboxImage := ds.podSandboxImage
if len(podSandboxImage) != 0 {
image = podSandboxImage
}
// NOTE: To use a custom sandbox image in a private repository, users need to configure the nodes with credentials properly.
// see: http://kubernetes.io/docs/user-guide/images/#configuring-nodes-to-authenticate-to-a-private-repository
// Only pull sandbox image when it's not present - v1.PullIfNotPresent.
if err := ensureSandboxImageExists(ds.client, image); err != nil {
return nil, err
}
// Step 2: Create the sandbox container.
if r.GetRuntimeHandler() != "" && r.GetRuntimeHandler() != runtimeName {
return nil, fmt.Errorf("RuntimeHandler %q not supported", r.GetRuntimeHandler())
}
createConfig, err := ds.makeSandboxDockerConfig(config, image)
if err != nil {
return nil, fmt.Errorf("failed to make sandbox docker config for pod %q: %v", config.Metadata.Name, err)
}
createResp, err := ds.client.CreateContainer(*createConfig)
if err != nil {
createResp, err = recoverFromCreationConflictIfNeeded(ds.client, *createConfig, err)
}
if err != nil || createResp == nil {
return nil, fmt.Errorf("failed to create a sandbox for pod %q: %v", config.Metadata.Name, err)
}
resp := &runtimeapi.RunPodSandboxResponse{PodSandboxId: createResp.ID}
ds.setNetworkReady(createResp.ID, false)
defer func(e *error) {
// Set networking ready depending on the error return of
// the parent function
if *e == nil {
ds.setNetworkReady(createResp.ID, true)
}
}(&err)
// Step 3: Create Sandbox Checkpoint.
if err = ds.checkpointManager.CreateCheckpoint(createResp.ID, constructPodSandboxCheckpoint(config)); err != nil {
return nil, err
}
// Step 4: Start the sandbox container.
// Assume kubelet's garbage collector would remove the sandbox later, if
// startContainer failed.
err = ds.client.StartContainer(createResp.ID)
if err != nil {
return nil, fmt.Errorf("failed to start sandbox container for pod %q: %v", config.Metadata.Name, err)
}
// Rewrite resolv.conf file generated by docker.
// NOTE: cluster dns settings aren't passed anymore to docker api in all cases,
// not only for pods with host network: the resolver conf will be overwritten
// after sandbox creation to override docker's behaviour. This resolv.conf
// file is shared by all containers of the same pod, and needs to be modified
// only once per pod.
if dnsConfig := config.GetDnsConfig(); dnsConfig != nil {
containerInfo, err := ds.client.InspectContainer(createResp.ID)
if err != nil {
return nil, fmt.Errorf("failed to inspect sandbox container for pod %q: %v", config.Metadata.Name, err)
}
// 重写容器的dns解析配置
// containerInfo.ResolvConfPath这里为容器目录在宿主机上对应的路径
// 比如:/var/lib/docker/containers/xxxx/resolv.conf"
if err := rewriteResolvFile(containerInfo.ResolvConfPath, dnsConfig.Servers, dnsConfig.Searches, dnsConfig.Options); err != nil {
return nil, fmt.Errorf("rewrite resolv.conf failed for pod %q: %v", config.Metadata.Name, err)
}
}
...
...
}
kubelet读取容器的resolv.conf,其实是读取了容器的这个变量:
docker inspect f580cc012e09 | grep res
"ResolvConfPath": "/var/lib/docker/containers/010287003ba360003b0b7ec48b63240f084669de6771cd158ed4c287f7a1ac75/resolv.conf",