在一些项目和技术交流中,发现很多朋友对OCP pod通信机制和OCP中的DNS犯懵。我专门写一篇进行分析。
本文中大量内容和K8S是相通的,文中也直接大量使用了K8S的概念,因此本文题目起的是K8S。OCP=OpenShift Container Platform,OCP内部的SDN用的是OVS。
我们以反问的方式进行“灵魂拷问”
拷问1. OCP中pod之间通讯是否一定需要service?
不需要。
没有service,pod能通信,而且好好的,如下图所示。
在一个namespace里(因为有networkpolicy,因此先做这个假设),只要指定的端口是pod开放的端口,在一个pod中,curl另外一个pod,就绝对能通。pod之间能不能通和俩pod跨不跨service,有没有service,没半毛钱关系。
拷问2. 从一个pod curl另外一个pod+端口号能通这件事,背后的链路机制是什么?
(1)俩pod在一个节点上,链路如下,流量没有绕出本宿主机的ovs:
Pod 1的eth0 → vethxx → br0 → vethyy → Pod 2的eth0
(2).俩pod在不同node上,链路如下,pod之间的通讯经过了vxlan:
Pod 1的eth0 → vethxx → br0 → vxlan0 → host1 eth0(192.168.1.101)→ network → host2 eth0(192.168.1.102)→ vxlan0 → br0 → vethmm → Pod 3的eth0
也就是说,截止到目前,我们把俩pod之间的通讯机制,简单介绍了。这里面没service啥事,这点记住。还需要记住的是,这是OCP集群中,pod之间通讯的实际数据链路(和后面service寻址对应)。
拷问3:我们都知道K8S service的基本概念,说说为啥K8S需要它?
我们首先要明确:Service是对一组提供相同功能的Pods的抽象,并为它们提供一个统一的内部访问入口。它主要解决:
1.负载均衡(这个大家会最先想到)。
2.服务注册与发现:解决不同服务之间的通信问题。在OpenShift中的创建应用后,需要提供访问应用的地址供其他服务调用,这个地址就是由Service提供。
每创建一个Service会分配一个ServiceIP地址,称为ClusterIP,这个IP地址是一个虚拟的地址,无法执行ping操作。同时自动在内部DNS注册一条对应的A记录(A记录很重要,后面展开说),这就完成了服务注册,注册信息全部保存在Etcd中。
拷问4:详细说说服务注册到底向etcd里注册了啥?
注册到etcd的,实际上是service yaml的先关内容。
我们查看etcd中router service 的内容。我们看到了什么?
service name、namespace、service ip、port number的,以及对应的endpoints等很多信息。
也就是说,etcd里有namespace,service name,service ip等,
通过这三个信息就可组成DNS A记录,也就是,service的FQDN和service ip之间的对应关系。但需要说明的是,etcd不是DNS!DNS A记录是通过查询生成的!OCP的DNS是由SkyDNS/CoreDNS实现的,这个后面说。
我们再说说服务发现。
服务发现这个词经常被妖魔化。我们把字换个顺序,就好理解:发现服务。也就说,service A要和serviceB通讯,我得知道serviceB是谁、在哪、service IP、pod ip都是啥?这就叫服务发现。
拷问5:有了servcice以后,pod之间的通讯和没有service有啥区别?
在数据通讯层,没区别!因为service只是逻辑层面的东西。
但是,没有service,K8S是无法控制pod之间的通讯的,也无法为pod之间进行寻址。也就是说,没有service,pod之间的通讯,就不会和K8S发生任何关系,和自己在笔记本linux中装起两容器ping着玩,没太大区别(在网络层)。
有了service以后,pod之间怎么寻址?
回答这个问题,我们要站在开发者角度。如果一个程序员,要写微服务,微服务之间要相互调用,怎么写?写 pod IP和service ip都不靠谱吧,你都不知道IP地址会是啥。
如果程序员决定用k8s做服务发现的前提下写服务之间的调用(如果使用spring cloud,那就用它的服务注册中心做解析,也就不必用K8S的service了),那么就得写K8S的service名称!
因为service名称我们是可以固定的。
K8S中service有短名和长名。
以下图为例,jws-app就是service的短名,service的长名是:<sevrvice_name>.<namesapce>.svc.cluser.local,也就是jws-app.web.svc.cluser.local。service短名可以自动补充成长名,这是OCP中的DNS做的,这个后面说。
那么,这时候大魏也有个疑问了。如果在两个不同的namespace中,有两个相同的service短名,微服务调用不是会出现混乱?程序员的代码里是不是要写service全名?
不能说想法不对,但一般不这样干。
首先,站在OCP集群cluster-admin的角度,我们看所有的项目,有几十个或者而更多,会觉得在不同namespaces中起相同的service短名是可能(比如namespace A中有个acat的service,namespace B中也有个acat的service)。但站在程序员角度,他只是OCP的使用者,他有自己的namespace,他能访问的namespace很有限,可能就1个。绝大多数情况下,同一个业务项目的微服务一般会运行在同一个namespace中,默认如果使用短名称(只写service name),则会自动补全成当前namespace的FQDN,只有在跨namespace调用的时候才必须写全名FQDN。
所以,程序员写的程序,用到了K8S service name,那么,真正跑应用的pod之间的通讯,也必然会以service name去找。通过service名称找到service ip,然后最终找到pod ip(一个service可能多个pod,从service ip到pod ip的负载均衡实现后面讲)。找到pod的ip以后,接下来实际的数据交换,就和拷问2讲述机制就接上头了。
拷问6. 我们知道Service的作用了,那从service ip到pod ip这段的负载均衡怎么实现的?
K8S中通过service name做服务发现,首选短名会自动自动扩展成FQDN,然后解析成service ip,然后再走kube-proxy负载均衡倒pod ip。
Service的负载均衡可以由很多的实现方式,目前Kubernetes官方提供了三种代理模式:userspace、iptables、ipvs。目前版本OpenShift默认的代理模式是iptables
从图中可以看出,当客户端访问Servcie的ClusterIP时,由Iptables实现负载均衡,选择一个后端处理请求,默认的负载均衡策略是轮询。在这种模式下,每创建一个Service,会自动匹配后端实例Pod记录在Endpoints对象中,并在所有Node节点上添加相应的iptables规则,将访问该Service的ClusterIP与Port的连接重定向到Endpoints中的某一个后端Pod,由于篇幅有限,关于负载均衡实现的细节不再赘述。
这种模式有两个缺点需要关注:第一,不支持复杂的负载均衡算法;第二,当选择的某个后端Pod没有响应时,无法自动重新连接到另一个Pod,用户必须利用Pod的健康监测来保证Endpoints列表中Pod都是存活的(也就是说,生产上你最好把liveness和rediness配上。)。
对于OCP而言,每个节点上都有iptables。而iptables之前的通讯和同步,是通过每个节点的kube-proxy实现的(这个进程在sdn pod中运行。想详细研究的可以看我前两天写的文章: 深度解析:kube-proxy在OpenShift上的实现)
拷问7. 我记得iptables不是做防火墙的么?
OCP中的iptables,主要目的做的是service ip和pod ip链路的事情。一般不涉及常规意义上的防火墙规则的INPUT/OUTPUT ACCEPT/DENY。也就是说,不要尝试自己ssh到OCP4节点上通过iptables做安全规则,不是这样玩的。
拷问8. 前面7个问题清楚了,说说OCP的DNS机制吧。
首先,OCP3和OCP4的DNS机制是不同的。
OpenShift 3内置的是SkyDNS,SkyDNS会监听Kubernetes API,当新创建一个Service,SkyDNS中就会提供<service-name>.<project-name>.svc.cluster.local域名的解析。除了解析Service,还可以通过<service-name>.<project-name>.endpoints.cluster.local解析endpoints。
例如,如果 myproject 服务中存在 myapi 服务,则整个OpenShift集群中的所有Pod都可以解析 myapi.myproject.svc.cluster.local 主机名以获取Service ClusterIP地址。除此之外,OpenShift DNS还提供以下两种短域名:
来自同一项目的Pod可以直接使用 Service名称作为短域名,不带任何域后缀,如myapi。
来自不同项目的Pod可以使用Service名称和项目名称作为短域名,不带任何域后缀,如myapi.myproject。
在简单了解了SkyDNS的机制之后,我们来看看OpenShift3是如何使用和配置DNS的。为了便于理解,我们用下图来进行说明。
上图表示了OpenShift 3中DNS解析流程:
第一步:在每个OpenShift节点上使用Dnsmasq作为所有DNS Server的反向代理,无论宿主机节点还是运行在节点的Pod发起的DNS查询,都会直接到宿主机IP的53端口。
第二步:如果需要解析的域名domain是cluster.local或in-addr.arpa,也就是OpenShift内部Service的域名解析,则将请求转发给节点的SkyDNS代理,若代理cache中有记录,则直接返回解析,如果没有,则向Master的SkyDNS主进程请求解析;如果是非cluster.local和in-addr-arpa,则转发到上游的DNS Server,上游DNS通常是通过网卡配置的DNS Server,代表企业的DNS或运营商的DNS。
第三步:计算节点的SkyDNS代理将无法解析的Service域名转发到Master节点的SkyDNS进程后,SkyDNS会经过API Server取查询Etcd中的Service记录,将IP地址返回给最终的查询客户端。
以上就是OpenShift 3中DNS的解析流程,核心是通过每个节点上运行的Dnsmasq进程做了SkyDNS和上游DNS的代理。
在OpenShift 3的DNS里面需要注意以下几点:
OpenShift节点SkyDNS代理发向Master SkyDNS的查询请求是不经过多个Master的负载均衡的,直接访问的是三个Master的8053端口,这在开通节点间防火墙时至关重要。
所有的上游DNS服务器通过网卡配置添加,并保证NetworkManager服务处于启动状态。绝不要尝试手动修改/etc/resolv.conf文件设定上游DNS服务器。
SkyDNS是查询Etcd获取Service域名的解析,但是在Etcd中并不会真实保存域名解析的A记录,而是直接查询在Etcd中保存的Service对象获取的ClusterIP地址。
SkyDNS进程被封装在OpenShift的Master和Node进程中,不需要额外部署。
OpenShift 4的DNS
OpenShift 4使用CoreDNS替换了OpenShift 3使用的SkyDNS,起到的作用是一样的,同样是提供OpenShift内部的域名解析服务。
在OpenShift 4中CoreDNS使用Operator实现部署,最终会创建出DaemonSet部署CoreDNS,也就是在每个节点会启动一个CoreDNS容器。在Kubelet将--cluster-dns设定为CoreDNS的ServiceClusterIP,这样Pod中就可以使用CoreDNS进行域名解析。
在安装OpenShift 4时,通过名为dns的Clusteroperator创建整个DNS堆栈,最终会在项目openshift-dns-operator下实例化一个dns pod完成具体的部署配置操作。
Cluster Domain定义了集群中Pod和Service域名的基本DNS域,默认为cluster.local,域名服务的地址是CoreDNS的ClusterIP,是配置的Service IP CIDR网段中的第10个地址,默认网段为172.30.0.0/16,第十个地址为172.30.0.10。DNS解析流程如下图所示:
上图表示了OpenShift 4的DNS解析流程
Pod中的应用直接通过Pod中配置的DNS Server 173.30.0.10解析所有域名(这个ip是coredns pod的service),该域名会将解析查询分配到具体的CoreDNS pod实例中(每个OCP节点都有一个coredns pod)。
在CoreDNS pod中,如果有cache缓存则直接返回(不管是内部域名还是外部域名,有缓存都直接返回,所以OCP会存在第一次解析慢,第二次就快的情况),如果缓存中没有则判断,(1)若解析域名属于cluster.local、in-addr.arpa或ip6.arpa,则通过CoreDNS的Kubernetes插件去查询,本质上是通过Kubernetes API和ETCD实现域名解析IP地址的返回;(2)如果是集群外部域名,则转到宿主机/etc/resolv.conf中配置的上游DNS服务器。
说简单点,在OCP中,随便创建一个pod,这个pod中的name server都会指向到172.30.0.10,这是coredns pod的service ip。
我们查看coredns的pod和service ip:
我们访问prometheus-k8s-0 这个pod进行查看。
查看这个规则:
sh-4.2$ cat /etc/resolv.conf
search openshift-monitoring.svc.cluster.local svc.cluster.local cluster.local
nameserver 172.30.0.10
options ndots:5
我们查看宿主机的dns。
OCP宿主机的nameserver可以是数据中心内部的,也可以自行构建。
举例说,如果我要在pod中nslookup baidu.com:
1.如果coredns pod中有缓存,直接返回
2. coredns pod中没有缓存,coredns一看这是外部域名,不归它管,他就会转到宿主机指向的192.168.91.8去解析,如果这192.168.91.8也解析不了,那就看还有没有上级的DNS了。总之,得有dns把baidu.com能解析出来。如果在OCP在纯离线的环境,baidu.com八成就解析失败了。(数据中心内部应该没人给baidu.com自己配解析)
好了,灵魂拷问结束,这种拷问方式是不是感觉比直接叙述看着爽一些?