十、Kubernetes网络概念及策略控制
1、Kubernetes基本网络模型
因为容器的网络发展复杂性就在于它其实是寄生在Host网络之上的。从这个角度讲,可以把容器网络方案大体分为Underlay/Overlay两大派别:
- Underlay的标准是它与Host网络是同层的,从外在可见的一个特征就是它是不是使用了Host网络同样的网段、输入输出基础设备、容器的IP地址是不是需要与Host网络取得协同(来自同一个中心分配或统一划分)。这就是Underlay
- Overlay不一样的地方就在于它并不需要从Host网络的IPM的管理的组件去申请IP,一般来说,它只需要跟Host网络不冲突,这个IP可以自由分配的
2、Netns探秘
Network Namespace里面能网络实现的内核基础。狭义上来说runC容器技术是不依赖于任何硬件的,它的执行基础就是它的内核里面,进程的内核代表就是task,它如果不需要隔离,那么用的是主机的空间(namespace),并不需要特别设置的空间隔离数据结构(nsproxy-namespace proxy)
相反,如果一个独立的网络proxy,或者mount proxy,里面就要填上真正的私有数据。它可以看到的数据结构如上图所示
从感官上来看一个隔离的网络空间,它会拥有自己的网卡或者说是网络设备。网卡可能是虚拟的,也可能是物理网卡,它会拥有自己的IP地址、IP表和路由表、拥有自己的协议栈状态。这里面特指就是TCP/Ip协议栈,它会有自己的status,会有自己的iptables、ipvs
从整个感官上来讲,这就相当于拥有了一个完全独立的网络,它与主机网络是隔离的。当然协议栈的代码还是公用的,只是数据结构不相同
上图可以清晰表明pod里Netns的关系,每个pod都有着独立的网络空间,pod net container会共享这个网络空间。一般K8s会推荐选用Loopback接口,在pod net container之间进行通信,而所有的container通过pod的IP对外提供服务。另外对于宿主机上的Root Netns,可以把它看做一个特殊的网络空间,只不过它的Pid是1
3、主流网络方案简介
Flannel方案是目前使用最为普遍的。如上图所示,可以看到一个典型的容器网方案。它首先要解决的是container的包如何到达Host,这里采用的是加一个Bridge的方式。它的backend其实是独立的,也就是说这个包如何离开Host,是采用哪种封装方式,还是不需要封装,都是可选择的
现在来介绍三种主要的backend:
- 一种是用户态的udp,这种是最早期的实现
- 然后是内核的Vxlan,这两种都算是overlay的方案。Vxlan的性能会比较好一点,但是它对内核的版本是有要求的,需要内核支持Vxlan的特性功能
- 如果你的集群规模不够大,又处于同一个二层域,也可以选择采用host-gw的方式。这种方式的backend基本上是由一段广播路由规则来启动的,性能比较高
4、Network Policy的用处
刚才提到了Kubernetes网络的基本模型是需要pod之间全互联,这个将带来一些问题:可能在一个K8s集群里,有一些调用链之间是不会直接调用的。比如说两个部门之间,那么我希望A部门不要去探视到B部门的服务,这个时候就可以用到策略的概念
基本上它的想法是这样的:它采用各种选择器(标签或namespace),找到一组pod,或者找到相当于通讯的两端,然后通过流的特征描述来决定它们之间是不是可以联通,可以理解为一个白名单的机制
在使用Network Policy之前,如上图所示要注意apiserver需要打开一下这几个开关。另一个更重要的是我们选用的网络插件需要支持Network Policy的落地。Network Policy只是K8s提供的一种对象,并没有内置组件做落地实施,需要取决于你选择的容器网络方案对这个标准的支持与否及完备程度,如果你选择Flannel之类,它并没有真正去落地这个Policy,那么你试了这个也没有什么用
接下来讲一个配置的实例,或者说在设计一个Network Policy的时候要做哪些事情?
- 第一件事是控制对象,就像这个实例里面spec的部分。spec里面通过podSelector或者namespace的selector,可以选择做特定的一组pod来接受我们的控制
- 第二个就是对流向考虑清楚,需要控制入方向还是出方向?还是两个方向都要控制
- 最重要的就是第三部分,如果要对选择出来的方向加上控制对象来对它流进行描述,具体哪一些stream可以放进来,或者放出去?类比这个流特征的五元组,可以通过一些选择器来决定哪一些可以作为我的远端,这是对象的选择;也可以通过IPBlock这种机制来得到对哪些IP是可以放行的;最后就是哪些协议或哪些端口。其实流特征综合起来就是一个五元组,会把特定的能够接受的流选择出来
十一、Kubernetes Service
1、需求来源
1)、为什么需要服务发现
在K8s集群里面会通过pod去部署应用,与传统的应用部署不同,传统应用部署在给定的机器上面去部署,我们知道怎么去调用别的机器的IP地址。但是在K8s集群里面应用是通过pod去部署的,而pod生命周期是短暂的。在pod的生命周期过程中,比如它创建或销毁,它的IP地址都会发生变化,这样就不能使用传统的部署方式,不能指定IP去访问指定的应用
另外在K8s的应用部署里,之前虽然学习了deployment的应用部署模式,但还是需要创建一个pod组,然后这些pod组需要提供一个统一的访问入口,以及怎么去控制流量负载均衡到这个组里面。比如说测试环境、预发环境和线上环境,其实在部署的过程中需要保持同样的一个部署模板以及访问方式。因为这样就可以用同一套应用的模板在不同的环境中直接发布
最后应用服务需要暴露到外部去访问,需要提供给外部的用户去调用的。pod的网络跟机器不是同一个段的网络,那怎么让pod网络暴露到去给外部访问呢?这时就需要服务发现
2)、Service:Kubernetes 中的服务发现与负载均衡
在K8s里面,服务发现与负载均衡就是K8s Service。上图就是在K8s里Service的架构,K8s Service向上提供了外部网络以及pod网络的访问,即外部网络可以通过service去访问,pod网络也可以通过K8s Service去访问
向下,K8s对接了另外一组pod,即可以通过K8s Service的方式去负载均衡到一组pod上面去,这样既提供了统一的访问入口去做服务发现,然后又可以给外部网络访问,解决不同的pod之间的访问,提供统一的访问地址
2、用例解读
1)、Service语法
首先来看K8s Service的一个语法,上图实际就是K8s的一个声明结构。这个结构里有很多语法,跟之前所介绍的K8s的一些标准对象有很多相似之处。比如说selector去做一些选择、label去声明它的一些label标签等
这里有一个新的知识点,就是定义了用于K8s Service服务发现的一个协议以及端口。继续来看这个模板,声明了一个名叫my-service的一个K8s Service,它有一个app:my-service
的label,它选择了app:MyApp
这样一个label的pod作为它的后端
最后是定义的服务发现的协议以及端口,这个示例中定义的是TCP协议,端口是80,目的端口是9376,效果是访问到这个service 80端口会被路由到后端的targetPort,就是只要访问到这个service 80端口的都会负载均衡到后端app:MyApp
这种label的pod的9376端口
2)、创建和查看Service
创建service:kubectl apply -f service.yaml
或者是kubectl created -f service.yaml
查看service创建之后的结果:kubectl discribe service
service创建好之后,可以看到它的名字是my-service。Namespace、Label、Selector这些都跟我们之前声明的一样,这里声明完之后会生成一个IP地址,这个IP地址就是service的IP地址,这个IP地址在集群里面可以被其它pod所访问,相当于通过这个IP地址提供了统一的一个pod的访问入口,以及服务发现
这里还有一个Endpoints的属性,通过Endpoints可以看到:通过前面所声明的selector去选择了哪些pod?以及这些pod都是什么样一个状态?比如说通过selector,我们看到它选择了这些pod的一个IP,以及这些pod所声明的targetPort的一个端口
实际的架构如上图所示。在service创建之后,它会在集群里面创建一个虚拟的IP地址以及端口,在集群里,所有的pod和node都可以通过这样一个IP地址和端口去访问到这个service。这个service会把它选择的pod及其IP地址都挂载到后端。这样通过service的IP地址访问时,就可以负载均衡到后端这些pod上面去
当pod的生命周期有变化时,比如说其中一个pod销毁,service就会自动从后端摘除这个pod。这样实现了:就算pod的生命周期有变化,它访问的端点是不会发生变化的
3)、集群内访问Service
在集群里面,其他pod要怎么访问到我们所创建的这个service呢?有三种方式:
-
首先可以通过service的虚拟IP去访问,比如说刚创建的my-service这个服务,通过
kubectl get svc
或者kubectl discribe service
都可以看到它的虚拟IP地址是172.29.3.27,端口是80,然后就可以通过这个虚拟IP及端口在pod里面直接访问到这个service的地址 -
第二种方式直接访问服务名,依靠DNS解析,就是同一个namespace里pod可以直接通过service的名字去访问到刚才所声明的这个service。不同的namespace里面,我们可以通过service名字加
.
,然后加service所在的哪个namespace去访问这个service,例如直接用curl去访问,就是my-service:80就可以访问到这个service -
第三种是通过环境变量访问,在同一个namespace里的pod启动时,K8s会把service的一些IP地址、端口,以及一些简单的配置,通过环境变量的方式放到K8s的pod里面。在K8s pod的容器启动之后,通过读取系统的环境变量比读取到namespace里面其他service配置的一个地址,或者是它的端口号等等。比如在集群的某一个pod里面,可以直接通过curl $取到一个环境变量的值,比如取到MY_SERVICE_SERVICE_HOST就是它的一个IP地址,MY_SERVICE就是刚才我们声明的MY_SERVICE,SERVICE_PORT就是它的端口号,这样也可以请求到集群里面的MY_SERVICE这个service
4)、Headless Service
service有一个特别的形态就是Headless Service。service创建的时候可以指定clusterIP:None,告诉K8s说我不需要clusterIP,然后K8s就不会分配给这个service一个虚拟IP地址,它没有虚拟IP地址怎么做到负载均衡以及统一的访问入口呢?
pod可以直接通过service_name用DNS的方式解析到所有后端pod的IP地址,通过DNS的A记录的方式会解析到所有后端的Pod的地址,由客户端选择一个后端的IP地址,这个A记录会随着pod的生命周期变化,返回的A记录列表也发生变化,这样就要求客户端应用要从A记录把所有DNS返回到A记录的列表里面IP地址中,客户端自己去选择一个合适的地址去访问pod
5)、向集群外暴露Service
前面介绍的都是在集群里面node或者pod去访问service,service怎么去向外暴露呢?怎么把应用实际暴露给公网去访问呢?这里service也有两种类型去解决这个问题,一个是NodePort,一个是LoadBalancer
-
NodePort的方式就是在集群的node上面(即集群的节点的宿主机上面)去暴露节点上的一个端口,这样相当于在节点的一个端口上面访问到之后就会再去做一层转发,转发到虚拟的IP地址上面,就是刚刚宿主机上面service虚拟IP地址
-
LoadBalancer类型就是在NodePort上面又做了一层转换,刚才所说的NodePort其实是集群里面每个节点上面一个端口,LoadBalancer是在所有的节点前又挂一个负载均衡。比如在阿里云上挂一个SLB,这个负载均衡会提供一个统一的入口,并把所有它接触到的流量负载均衡到每一个集群节点的node pod上面去。然后node pod再转化成ClusterIP,去访问到实际的pod上面
3、操作演示
1)、集群内访问Service
service.yaml:
apiVersion: v1
kind: Service
metadata:
name: nginx
labels:
run: nginx
spec:
ports:
- port: 80
protocol: TCP
targetPort: 80
selector:
run: nginx
server.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
labels:
run: nginx
spec:
replicas: 2
selector:
matchLabels:
run: nginx
template:
metadata:
labels:
run: nginx
spec:
containers:
- name: nginx
image: nginx:1.7.9
hanxiantaodeMBP:yamls hanxiantao$ kubectl create -f server.yaml
deployment.apps/nginx created
hanxiantaodeMBP:yamls hanxiantao$ kubectl get pod
NAME READY STATUS RESTARTS AGE
nginx-79699b7df9-jn5p4 1/1 Running 0 46s
nginx-79699b7df9-th5hj 1/1 Running 0 46s
hanxiantaodeMBP:yamls hanxiantao$ kubectl get pod -o wide -l run=nginx
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nginx-79699b7df9-jn5p4 1/1 Running 0 77s 10.1.0.139 docker-desktop <none> <none>
nginx-79699b7df9-th5hj 1/1 Running 0 77s 10.1.0.140 docker-desktop <none> <none>
先创建一组pod,就是创建这个K8s deployment。deployment创建好了之后,再看一下pod有没有创建出来。通过kubectl get pod -o wide可以看到IP地址。通过-l,即label去做筛选,run=nginx。这两个pod分别是10.1.0.139和10.1.0.140这样一个IP地址,并且都是带run=nginx这个label的
hanxiantaodeMBP:yamls hanxiantao$ kubectl create -f service.yaml
service/nginx created
hanxiantaodeMBP:yamls hanxiantao$ kubectl describe svc
Name: kubernetes
Namespace: default
Labels: component=apiserver
provider=kubernetes
Annotations: <none>
Selector: <none>
Type: ClusterIP
IP: 10.96.0.1
Port: https 443/TCP
TargetPort: 6443/TCP
Endpoints: 192.168.65.3:6443
Session Affinity: None
Events: <none>
Name: nginx
Namespace: default
Labels: run=nginx
Annotations: <none>
Selector: run=nginx
Type: ClusterIP
IP: 10.108.96.80
Port: <unset> 80/TCP
TargetPort: 80/TCP
Endpoints: 10.1.0.139:80,10.1.0.140:80
Session Affinity: None
Events: <none>
再来创建K8s service。通过kubectl describe svc可以看到这个service实际的一个状态。创建的nginx service,它的选择器是run=nginx,通过run=nginx这个选择器选择到后端的pod地址,就是刚才所看到那两个pod的地址:10.1.0.139和10.1.0.140。这里可以看到K8s为它生成了集群里面一个虚拟IP地址,通过这个虚拟IP地址,它就可以负载均衡到后面的两个pod上面去
pod1.yaml:
apiVersion: v1
kind: Pod
metadata:
name: nginx1
namespace: default
labels:
env: dev
tie: front
spec:
containers:
- name : nginx
image: nginx:1.8
ports:
- containerPort: 80
创建一个客户端的pod来测试如何访问这个service
hanxiantaodeMBP:yamls hanxiantao$ kubectl create -f pod1.yaml
pod/nginx1 created
hanxiantaodeMBP:yamls hanxiantao$ kubectl get pod
NAME READY STATUS RESTARTS AGE
nginx-79699b7df9-jn5p4 1/1 Running 0 10m
nginx-79699b7df9-th5hj 1/1 Running 0 10m
nginx1 1/1 Running 0 39s
使用kubectl exec -it nginx1 sh进入这个pod,安装curl
先添加163源
tee /etc/apt/sources.list << EOF
deb http://mirrors.163.com/debian/ jessie main non-ffree contrib
deb http://mirrirs.163.com/dobian/ jessie-updates main non-free contrib
EOF
hanxiantaodeMBP:yamls hanxiantao$ kubectl exec -it nginx1 sh
# apt-get update && apt-get install -y curl
通过ClusterIP访问
# curl http://10.108.96.80:80
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
body {
width: 35em;
margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif;
}
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>
通过{service name}.{namespace}访问
# curl http://nginx
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
body {
width: 35em;
margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif;
}
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>
通过service name访问
# curl http://nginx.default
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
body {
width: 35em;
margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif;
}
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>
通过环境变量访问
# env
KUBERNETES_PORT=tcp://10.96.0.1:443
KUBERNETES_SERVICE_PORT=443
HOSTNAME=nginx1
HOME=/root
NGINX_PORT_80_TCP=tcp://10.108.96.80:80
TERM=xterm
KUBERNETES_PORT_443_TCP_ADDR=10.96.0.1
NGINX_VERSION=1.8.1-1~jessie
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
NGINX_SERVICE_HOST=10.108.96.80
KUBERNETES_PORT_443_TCP_PORT=443
KUBERNETES_PORT_443_TCP_PROTO=tcp
NGINX_SERVICE_PORT=80
NGINX_PORT=tcp://10.108.96.80:80
KUBERNETES_SERVICE_PORT_HTTPS=443
KUBERNETES_PORT_443_TCP=tcp://10.96.0.1:443
KUBERNETES_SERVICE_HOST=10.96.0.1
PWD=/
NGINX_PORT_80_TCP_ADDR=10.108.96.80
NGINX_PORT_80_TCP_PORT=80
NGINX_PORT_80_TCP_PROTO=tcp
# curl $NGINX_SERVICE_HOST
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
body {
width: 35em;
margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif;
}
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>
2)、向集群外暴露Service
service.yaml添加type: LoadBalancer
apiVersion: v1
kind: Service
metadata:
name: nginx
labels:
run: nginx
spec:
ports:
- port: 80
protocol: TCP
targetPort: 80
selector:
run: nginx
type: LoadBalancer
hanxiantaodeMBP:yamls hanxiantao$ kubectl apply -f service.yaml
Warning: kubectl apply should be used on resource created by either kubectl create --save-config or kubectl apply
service/nginx configured
hanxiantaodeMBP:yamls hanxiantao$ kubectl get svc -o wide
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 29d <none>
nginx LoadBalancer 10.108.96.80 localhost 80:31943/TCP 29m run=nginx
service这里多了一个EXTERNAL-IP,可以通过http://localhost/访问service
3)、service访问地址和pod声明周期无关
hanxiantaodeMBP:yamls hanxiantao$ kubectl describe svc nginx
Name: nginx
Namespace: default
Labels: run=nginx
Annotations: <none>
Selector: run=nginx
Type: LoadBalancer
IP: 10.108.96.80
LoadBalancer Ingress: localhost
Port: <unset> 80/TCP
TargetPort: 80/TCP
NodePort: <unset> 31943/TCP
Endpoints: 10.1.0.139:80,10.1.0.140:80
Session Affinity: None
External Traffic Policy: Cluster
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Type 4m49s service-controller ClusterIP -> LoadBalancer
现在service映射的后端ip地址为10.1.0.139和10.1.0.140
hanxiantaodeMBP:yamls hanxiantao$ kubectl get pod
NAME READY STATUS RESTARTS AGE
nginx-79699b7df9-jn5p4 1/1 Running 0 35m
nginx-79699b7df9-th5hj 1/1 Running 0 35m
nginx1 1/1 Running 0 25m
hanxiantaodeMBP:yamls hanxiantao$ kubectl delete pod nginx-79699b7df9-jn5p4
pod "nginx-79699b7df9-jn5p4" deleted
hanxiantaodeMBP:yamls hanxiantao$ kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nginx-79699b7df9-bb95z 1/1 Running 0 37s 10.1.0.142 docker-desktop <none> <none>
nginx-79699b7df9-th5hj 1/1 Running 0 36m 10.1.0.140 docker-desktop <none> <none>
nginx1 1/1 Running 0 26m 10.1.0.141 docker-desktop <none> <none>
hanxiantaodeMBP:yamls hanxiantao$ kubectl describe svc nginx
Name: nginx
Namespace: default
Labels: run=nginx
Annotations: <none>
Selector: run=nginx
Type: LoadBalancer
IP: 10.108.96.80
LoadBalancer Ingress: localhost
Port: <unset> 80/TCP
TargetPort: 80/TCP
NodePort: <unset> 31943/TCP
Endpoints: 10.1.0.140:80,10.1.0.142:80
Session Affinity: None
External Traffic Policy: Cluster
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Type 6m2s service-controller ClusterIP -> LoadBalancer
删除其中一个pod,现在pod的ip地址变成了10.1.0.140和10.1.0.142,再来看service映射的后端ip地址也变成了10.1.0.140和10.1.0.142
4、架构设计
如上图所示,K8s服务发现以及K8s Service是这样整体的一个架构
K8s 分为master节点和worker节点:
- master里面主要是K8s管控的内容
- worker节点里面是实际运行用户应用的地方
在K8s master节点里面有APIServer,就是统一管理K8s所有对象的地方,所有的组件都会注册到APIServer上面去监听这个对象的变化,比如说我们刚才的组件pod生命周期发生变化,这些事件
这里面最关键的有三个组件:
- 一个是Cloud Controller Manager,负责去配置LoadBalancer的一个负载均衡器给外部去访问
- 另外一个就是Coredns,就是通过Coredns去观测APIServer里面的service后端pod的一个变化,去配置service的DNS解析,实现可以通过service的名字直接访问到service的虚拟IP,或者是Headless类型的Service中的IP列表的解析
- 然后在每个node里面会有kube-proxy这个组件,它通过监听service以及pod变化,然后实际去配置集群里面的node pod或者是虚拟IP地址的一个访问
实际访问链路是什么样的呢?比如说从集群内部的一个Client Pod3去访问Service,就类似于刚才所演示的一个效果。Client Pod3首先通过Coredns这里去解析出ServiceIP,Coredns会返回给它ServiceName所对应的service IP是什么,这个Client Pod3就会拿这个Service IP去做请求,它的请求到宿主机的网络之后,就会被kube-proxy所配置的iptables或者IPVS去做一层拦截处理,之后去负载均衡到每一个实际的后端pod上面去,这样就实现了一个负载均衡以及服务发现
对于外部的流量,比如说刚才通过公网访问的一个请求。它是通过外部的一个负载均衡器Cloud Controller Manager去监听service的变化之后,去配置的一个负载均衡器,然后转发到节点上的一个NodePort上面去,NodePort也会经过kube-proxy的一个配置的一个iptables,把NodePort的流量转换成ClusterIP,紧接着转换成后端的一个pod的IP地址,去做负载均衡以及服务发现。这就是整个K8s服务发现以及K8s Service整体的结构
课程地址:https://edu.aliyun.com/roadmap/cloudnative?spm=5176.11399608.aliyun-edu-index-014.4.dc2c4679O3eIId#suit