在 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 ....
在 isito 的 pilot/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.go 的 ServeDNS 函数中。
ServeDNS github 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() 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 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 组件
HandleUpstream github 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 Generate github 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 }
在代码的生成中,我们可以发现那些东西会被 Push 到 Agent 中
BuildNameTable github 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{}, } for _, svc := range push.Services(node) { if svc.Hostname.IsWildCarded() { continue } svcAddress := svc.GetServiceAddressForProxy(node, push) var addressList []string if svcAddress == constants.UnspecifiedIP { if svc.Attributes.ServiceRegistry == string (serviceregistry.Kubernetes) && svc.Resolution == model.Passthrough && len (svc.Ports) > 0 { 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 获得。
GetServiceAddressForProxy github 1 2 3 4 5 6 7 8 9 10 11 12 func (s *Service) GetServiceAddressForProxy(node *Proxy, push *PushContext) string { if node.Metadata != nil && node.Metadata.ClusterID != "" && push.ServiceIndex.ClusterVIPs[s][node.Metadata.ClusterID] != "" { return push.ServiceIndex.ClusterVIPs[s][node.Metadata.ClusterID] } 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 内资源的时候,会经过
VM 内应用查询 DNS 解析 demo.demo.srv.cluster.local
DNS 解析返回缓存的地址,实际上对应的 srv 地址
访问此地址,被 Envoy 拦截
Envoy 将请求转发至 Ingress Gateway
Ingress Gateway 通过 SNI 转发至 Service
跨集群访问
在以前的跨集群访问中,如果某个服务仅仅在某一个集群中,此时我们在另外一个不包含此服务的集群中访问
1 2 / 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 的问题,解决办法是创建一个 headless 的 service 资源。
而现在不需要了,ServiceEntry 的对象会分配一个 E Class IP,因此我们进行解析的时候会获得一个 IP
1 2 3 4 5 6 7 8 9 / 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