Isito 1.8 新功能: DNS Agent

1.8 的版本中,istio 悄悄的上了一个预览版的功能 DNS Proxy,这个功能极为的强大,让我们一起来看看吧。

为何诞生

从官方的文档中,DNS Proxy的功能主要为了 减少KubeDNS的查询,这个很好理解

除此之外,还要解决一个很重大的问题:VM 和 Kubernetes 的集成问题。

当我们将 虚拟机 接入集群的时候,虚拟机 中的应用如何访问 Kubernates 中的应用一直是一个困难的事情。还顺带解决了多集群访问的问题。让我们看看他是怎没做的。

DNS Proxy

启用此功能需要在 pilot-agent 的环境变量增加 ISTIO_META_DNS_CAPTURE=true

DNS 请求拦截

Pod启动的时候,我们会为 Pod 指定域名解析服务。

1
2
3
4
bash-5.0# cat /etc/resolv.conf 
nameserver 10.96.0.10
search test-istio.svc.cluster.local svc.cluster.local cluster.local byted.org
options ndots:5

这里的 nameserver 指向的就是 kube-dns 的地址

1
2
$ kubectl get svc --all-namespaces | grep '10.96.0.10'
kube-system kube-dns ClusterIP 10.96.0.10 <none> 53/UDP,53/TCP,9153/TCP 39d

istio 在会将此请求拦截,转发至 pilot-agent,这里并没有通过 envoy,是通过 iptables

1
2
3
4
5
6
$ nsenter -t1963097 -n iptables-save -L
.....
-A OUTPUT -p udp -m udp --dport 53 -m owner --uid-owner 1337 -j RETURN
-A OUTPUT -p udp -m udp --dport 53 -m owner --gid-owner 1337 -j RETURN
-A OUTPUT -p udp -m udp --dport 53 -j DNAT --to-destination 127.0.0.1:15053
....

isitopilot/pkg/dns/proxy.go:62 中,我们可以很轻松的看到

1
2
3
4
5
6
7
func (p *dnsProxy) start() {
log.Infof("Starting local %s DNS server at 0.0.0.0:15053", p.protocol)
err := p.downstreamServer.ActivateAndServe()
if err != nil {
log.Errorf("Local %s DNS server terminated: %v", p.protocol, err)
}
}

DNS Agent

对于 DNS Agent 的运行逻辑也相对的不算很复杂。主要集中在 pilot/pkg/dns/dns.goServeDNS 函数中。

ServeDNSgithub
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
func (h *LocalDNSServer) ServeDNS(proxy *dnsProxy, w dns.ResponseWriter, req *dns.Msg) {
var response *dns.Msg

lp := h.lookupTable.Load()

// lookupTable 未加载成功,返回解析失败
if lp == nil {
response = new(dns.Msg)
response.SetReply(req)
response.Rcode = dns.RcodeNameError
_ = w.WriteMsg(response)
return
}
lookupTable := lp.(*LookupTable)
var answers []dns.RR

// 去 lookupTable 进行查找
hostname := strings.ToLower(req.Question[0].Name)
answers, hostFound := lookupTable.lookupHost(req.Question[0].Qtype, hostname)

if hostFound { // 找到就是返回
response = new(dns.Msg)
response.SetReply(req)
response.Answer = answers
if len(answers) == 0 { // 多个地址特殊处理失败
response.Rcode = dns.RcodeNameError
}
} else { // 找不到向上游查询
response = h.queryUpstream(proxy.upstreamClient, req)
}

_ = w.WriteMsg(response)
}

处理的逻辑并不算复杂,而比较复杂的显然是 lookupTable 从何而来,这个答案都指向了 pilot-discovery 组件

HandleUpstreamgithub
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func (p *XdsProxy) HandleUpstream(ctx context.Context, con *ProxyConnection, xds discovery.AggregatedDiscoveryServiceClient) error {
for {
select {
case resp, ok := <-con.responsesChan:
switch resp.TypeUrl {
case v3.NameTableType:
if p.localDNSServer != nil && len(resp.Resources) > 0 {
var nt nds.NameTable
if err = ptypes.UnmarshalAny(resp.Resources[0], &nt); err != nil {
log.Errorf("failed to unmarshall name table: %v", err)
}
p.localDNSServer.UpdateLookupTable(&nt)
}
}
}

可见 pilot-discovery 将这些数据 push 到了我们的 pilot-agent 上。

Pilot-Discovery

对于 XDS 来说,我们熟系的有 LDS RDS RSDS 等,对于 DNS 的数据,istio 拓展了一种 NDS 协议

DNS Generategithub
1
2
3
4
5
6
7
8
9
10
11
func (n NdsGenerator) Generate(proxy *model.Proxy, push *model.PushContext, w *model.WatchedResource, req *model.PushRequest) model.Resources {
if !ndsNeedsPush(req) {
return nil
}
nt := n.Server.ConfigGenerator.BuildNameTable(proxy, push)
if nt == nil {
return nil
}
resources := model.Resources{util.MessageToAny(nt)}
return resources
}

在代码的生成中,我们可以发现那些东西会被 PushAgent

BuildNameTablegithub
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
func (configgen *ConfigGeneratorImpl) BuildNameTable(node *model.Proxy, push *model.PushContext) *nds.NameTable {
out := &nds.NameTable{
Table: map[string]*nds.NameTable_NameInfo{},
}

// 获得所有的服务 [这里是一个整合的概念,不仅仅包含了 kube Service]
for _, svc := range push.Services(node) {
if svc.Hostname.IsWildCarded() {
continue
}

svcAddress := svc.GetServiceAddressForProxy(node, push)
var addressList []string

// 如果地址是 0.0.0.0
if svcAddress == constants.UnspecifiedIP {
if svc.Attributes.ServiceRegistry == string(serviceregistry.Kubernetes) &&
svc.Resolution == model.Passthrough && len(svc.Ports) > 0 {
// 如果是 Passthrough 的模式并且有 Ports 的配置,就把 ServiceInstance 的 IP 写入
for _, instance := range push.ServiceInstancesByPort(svc, svc.Ports[0].Port, nil) {
addressList = append(addressList, instance.Endpoint.Address)
}
}

if len(addressList) == 0 {
continue
}
} else { // 其他直接放地址即可
addressList = append(addressList, svcAddress)
}

nameInfo := &nds.NameTable_NameInfo{
Ips: addressList,
Registry: svc.Attributes.ServiceRegistry,
}
}
return out
}

抛开 headless 的服务来说,大部分的服务地址都可以通过 GetServiceAddressForProxy 获得。

GetServiceAddressForProxygithub
1
2
3
4
5
6
7
8
9
10
11
12
func (s *Service) GetServiceAddressForProxy(node *Proxy, push *PushContext) string {
// 从节点内获得 ClusterIP
if node.Metadata != nil && node.Metadata.ClusterID != "" && push.ServiceIndex.ClusterVIPs[s][node.Metadata.ClusterID] != "" {
return push.ServiceIndex.ClusterVIPs[s][node.Metadata.ClusterID]
}
// 如果开启 DNSCapture 而且服务又是 Headless 的,这里会使用 AutoAllocatedAddress
if node.Metadata != nil && node.Metadata.DNSCapture != "" &&
s.Address == constants.UnspecifiedIP && s.AutoAllocatedAddress != "" {
return s.AutoAllocatedAddress
}
return s.Address
}

而生产 IP 的逻辑在 autoAllocateIPs 不做过多的展开。

PushContext 中,获得所有的 Service 的时候,会自动的生成这些 IP

1
2
3
4
5
6
7
8
func (s *ServiceEntryStore) Services() ([]*model.Service, error) {
services := make([]*model.Service, 0)
for _, cfg := range s.store.ServiceEntries() {
services = append(services, convertServices(cfg)...)
}

return autoAllocateIPs(services), nil
}

DNS Proxy Works

说了那么多的原理,让我们看看,这个功能究竟会给我们带来什么。

VM 访问 Kube 内资源

当我们让 VM 访问 Kube 内资源的时候,会经过

  1. VM 内应用查询 DNS 解析 demo.demo.srv.cluster.local
  2. DNS 解析返回缓存的地址,实际上对应的 srv 地址
  3. 访问此地址,被 Envoy 拦截
  4. Envoy 将请求转发至 Ingress Gateway
  5. Ingress Gateway 通过 SNI 转发至 Service

跨集群访问

在以前的跨集群访问中,如果某个服务仅仅在某一个集群中,此时我们在另外一个不包含此服务的集群中访问

1
2
/ # curl something.demo.srv.cluster.local
curl: (6) Could not resolve host: something.demo.srv.cluster.local

这里错误是因为 KubeDNS 并不能成功的了解析其他集群中的服务地址。现在基于 DNS PROXY#VM 访问 Kube 内资源 一样解决了这样的问题。

ServiceEntry 一键接入外部服务

以前我们想要将一个 vm 的服务单方向的接入系统,我们需要如下配置

1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
name: details-svc
spec:
hosts:
- notexist.foo.cluster.local
location: MESH_EXTERNAL
ports:
- number: 80
name: http
protocol: HTTP
resolution: STATIC

此时我们访问 notexist.foo.cluster.local 依然会出现 Could not resolve host 的问题,解决办法是创建一个 headlessservice 资源。

而现在不需要了,ServiceEntry 的对象会分配一个 E Class IP,因此我们进行解析的时候会获得一个 IP

1
2
3
4
5
6
7
8
9
/ # nslookup notexist.foo.cluster.local
Server: 10.233.0.3
Address: 10.233.0.3:53

** server can't find notexist.foo.cluster.local: NXDOMAIN

Non-authoritative answer:
Name: notexist.foo.cluster.local
Address: 240.240.12.17