Kubernetes系列(4)——网络

半日闲 2020年12月07日 38次浏览

前言

本文只专注于讲解Kubernetes的原生功能,不包含cni方面的讲解,重点在于普及Kubernetes的原生功能,使大家可以快速明白Kubernetes相关的概念以及使用方法,师傅领进门,修行在个人,基础弄明白了,其他的也就比较容易接受了。
建议学习Kubbernetes的小伙伴们,在了解了Kubernetes的概念并且工作中用到了Kubernetes以后, 一定要去官网通读一下官方文档 ,半日闲我也是,最近在写本系列文档的时候,感觉到了自己的严重不足,也是看了很多的官网资料,有些深坑才渐渐的爬出来的!
因此强烈建议大家在有一定基础后,去官网阅读官方文档!!!

Service与Endpoint的关系

Endpoint

Kubernetes中,业务是以pod的形式运行其中,并且在pod上暴露好相应的端口,留给外部服务去访问,并且每个pod都会有自己的ip,这些pod ip:port的形式,便是称之为endpoint。
但是endpoint往往不需要我们自己去创建,我们只需要在deploy等控制器中写好pod的label和port即可,Kubernetes中的endpoint如下,
image.png
可以看到每个svc的后面都有1个与之名称一样的endpoint,通俗来讲endpoint就是后端pod ip:pod port的集合,这样的话,就实现了外部服务访问业务的时候,有了1个固定的入口,而Kubernetes会自动维护endpoint,后端pod的ip发生变化的时候,自动删除或添加该ip,因此外部请求不必直接去与后端的pod通信,大大降低了复杂程度。

Endpointslice

前面我们说过,Kubernetes集群中几乎所有的数据都会存储到etcd中,因此,endpoint的信息也会存储到etcd中,但是etcd对于键值对的大小是有要求的,默认为1.5MB,也就是大概5000个,而每一次的endpoint的变更,都会被发送到整个集群中所有的机器上,因此对于大型集群来说,这是不可以承受的。为此,产生了endpointslice。
endpointslice很好理解,就是将endpoint拆分成了均匀的块,这个块就是endpointslice,然后每个块中再存储pod ip:pod port信息,这样子就解决了大型集群endpoint过长的问题,其结构图如下
image.png
默认情况下,每个endposlice可以存储100个后端端点,可以通过调节kube-controller-manager的参数--max-endpoints-per-slice进行配置。

Service

在Kubernetes中,往往运行着诸多服务,而为了使这些服务更加的高可用,往往同时1个pod会运行着多个副本,因此如何访问这些pod便成了1个问题?而Service就是解决这个问题的极佳方案!
Service结构如下
image.png
Kubernetes中的每1个pod都有单独的ip,因此当外部服务进入Kubernetes中的时候,自然是不可能去访问每个ip,而是访问到了service这里,然后由service根据相应的转发规则,转发至后续相应的后端。
先来看1个service的yaml,如下

[root@a test]$ cat nginx-svc.yaml
apiVersion: v1
kind: Service
metadata:
  name: nginx
  namespace: default
spec:
  clusterIP: 10.68.16.76
  ports:
  - name: http
    port: 80
    protocol: TCP
    targetPort: 80
    ## 该svc依靠app:nginx的label选择后端对应的pod
  selector:
    app: nginx
  sessionAffinity: None
  type: ClusterIP

随后创建2个后续的pod,如下

[root@a test]$ cat nginx-deploy.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
    ## 给该pod打上app:nginx的label供svc选择
      labels:
        app: nginx
    spec:
      containers:
      - image: nginx
        imagePullPolicy: Always
        name: nginx
        ports:
        - containerPort: 80
          name: http-nginx
          protocol: TCP
        terminationMessagePath: /dev/termination-log
        terminationMessagePolicy: File

使用kubectl apply创建pod和svc后,可以访问其FQDN, svc的FQDN形式如service_name.namespace.svc.cluster.local 如下
image.png
image.png

HeadlessService

headlessService是一种特殊的svc,正常的svc被创建以后,Kubernetes会自动分配1个cluster ip给svc,然后会根据selector来选择后端的pod,将其后端符合lable的pod的ip以及port都收集起来,组成endpoint,然后与svc链接起来。但是生产环境中,如果我们需要在pod中来使用FQDN的方式访问外部服务的话,那么svc便无法做到了,而此时,便可以使用headlessservice,使其可以将请求转发到我们想要的后端(包括外部ip)
headlessservice的yaml写法,其实和正常的svc是一致的,只是不需要写selector字段即可( 即不需要依靠label自动选择后端 ),其yaml如下

[root@a test]$ cat nginx-headlessservice.yaml
apiVersion: v1
kind: Service
metadata:
  name: nginx-headlessservice
  namespace: default
spec:
  clusterIP: 10.68.16.96
  ports:
  - name: http
    port: 80
    protocol: TCP
    targetPort: 80

然后我们 创建1个和headlessservice名字相同的endpoint ,其yaml如下

[root@a test]$ cat nginx-headlessservice-endpoint.yaml
apiVersion: v1
kind: Endpoints
metadata:
  name: nginx-headlessservice
  namespace: default
subsets:
- addresses:
  - ip: 192.168.1.8
  ports:
  - name: http
    port: 12345
    protocol: TCP

然后启动1个httpd服务,使其监听在上述endpoint的12345端口,用于测试,创建如下
image.png
使用kubectl apply创建好了svc和endpoint后,可以查看其headlessservice的状态如下
image.png
如上,可以使用kubectl describe svc的时候,已经显示出了上述创建好的endpoint,使用curl在宿主机以及pod内部访问该headlessservice,如下
image.png
可以看到不论是直接访问headlessservice的ip还是其FQDN都是成功的(上述返回状态码403只是因为httpd服务的配置没修改,拒绝了所有的请求)。

Kube-proxy的3种模式

一直以来总是没搞懂kube-proxy和cni插件的关系,现在终于明白了,cni插件只是负责pod到pod之间网络互通,但是其他的svc到pod,宿主机到pod之间的网络是由kube-proxy进行转发的。
主要有以下3种,

  • userspace
  • iptables
  • ipvs

随着现在Kubernetes越来越完善,一般默认的话,就是启用的IPVS的模式。

userspace

该模式下,kube-proxy会在为每个svc都在node节点上创建1个端口,并监听,同时会创建1个cluster ip,将所有到cluster ip的请求转到到该端口,请求到达该端口后,由kube-proxy根据Round Robin(轮询)或Session Affinity(会话亲和性)的规则,直接转发到后端的pod上,请求全程在用户空间,因此性能比较低,目前已弃用。
结构图,如下
image.png

iptable

在这种模式下,Kube-proxy只是1个controller,负责增删改查iptables规则,当请求到达svc后,由对应的iptables 规则先将请求转发到自定义的链上,然后再转发到后端的pod上。这种模式的请求都是走的内核态,因此无论是安全还是性能上,都比userspace要好,但是当集群规模达到一定量之后,随着iptables规则的越来越多,每次遍历规则都需要花费大量的时间,因此性能会越来越差,但是目前应该也不会遇到,因为iptable默认是在Kubernetes的v1.2版本到v1.12之间,v1.12版本之后,默认就是ipvs了。
结构图如下,
image.png

ipvs

ipvs是目前常用版本种的默认模式,    在此处,简单说一下ipvs和lvs以及iptable的关系,因为有时候总是会搞混淆了。
LVS的IP负载均衡技术是通过IPVS模块实现的。IPVS模块是LVS集群的核心软件模块,它安装在LVS集群作为负载均衡的主节点上,虚拟出一个IP地址和端口对外提供服务。用户通过访问这个虚拟服务(VS),然后访问请求由负载均衡器(LB)调度到后端真实服务器(RS)中,由RS实际处理用户的请求给返回响应。而ipvs和iptable一样,也是基于netfilter,但是ipvs提供了更高级的功能和更加强劲的性能。
在ipvs的模式下,kube-proxy也只是1个controller而已,只负责对ipset(可以理解为iptable的扩展功能)的增删改查,处于ipvs模式下的时候,在某些情况下会依赖于iptable的功能,比如数据包过滤,SNAT和NodePort类型的服务。
结构图如下,
image.png
使用ipvsadm和ipset可以查看ipvs创建的规则信息,如下
image.png

Ingress

默认情况下Kubernetes中的网络,在外部是访问不到的,因此出现了Ingress(即外部访问内部),需要提醒的是,Kubernetes正常情况下只提供了Ingress的功能,而当我们需要更强大的功能的时候,比如对pod访问外部服务的情况做出控制的时候,就需要借助其他的工具了,比如istio,istio提供了更加强大的网络管理情况,比如ingress和engress(内部访问外部),服务熔断,限流升级,灰度发布等,后续有情况会专门出一个istio系列或者专门讲网络的系列文章。
Ingress其实是分2部分的,但是往往为了方便,就直接说是ingress,其1是ingress,2是ingress controller,前者与后者的关系相当于nginx的配置文件和nginx本身。
Ingress的实现其实有很多种方法,有官方的nginx controller,也有前几天我博客中写到的traefik conroller,还有一些其他的实现方法( 集群中可以安装多种ingress controller,然后通过annotation来选择使用哪一种ingress controller来进行服务 ),其中nginx和traefik是性能最高的2种,其中nginx的使用是最广的,但是traefki的文章在前段时间的文章种已经深入写过了,这里就不再讲了,只说nginx。
首先安装1个nginx ingress,推荐使用helm安装,此处helm版本为helm3,如下

helm add repo ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update
helm install ingress-nginx ingress-nginx/ingress-nginx
## 注意,应为nginx ingress是官方的实现方案,因此image是存放在k8s.gcr.io中的,此地址国内无法访问

使用helm安装后,如下
image.png
使用kubectl get svc可以看到nginx ingress controller创建的svc,如下
image.png
可以看到ingress controller监听了2个端口,分别用于转发80端口和443端口的请求,此处测试ingress的时候,我们借用一下前面讲解service和endpoint的时候,创建的nginx的pod和svc,免得再重新去创建,麻烦!
创建1个ingress的资源,其yaml如下

[root@a test]$ cat ingress-nginx.yaml
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  annotations:
    kubernetes.io/ingress.class: nginx
  name: ingress-nginx
spec:
  rules:
  ## 请先在主机的/etc/hosts文件中添加nginx.a.b的映射关系
    - host: nginx.a.b
      http:
        paths:
          - backend:
              serviceName: nginx
              servicePort: 80
            path: /

如上,我们便可以将访问nginx.a.b:27881/的请求( 27881是ingress controller监听的端口,用于转发80请求,可仔细查看上一张图片,即可明白 )转发到后端服务nginx上,如下
image.png
如上图,我们可以看到,不论是直接访问svc还是通过ingress资源访问过去,都是成功的
然后我们来看一个失败的例子,我们修改下访问到饿path路径,如下

[root@a test]$ cat ingress-nginx.yaml
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  annotations:
    kubernetes.io/ingress.class: nginx
  name: ingress-nginx
spec:
  rules:
    - host: nginx.a.b
      http:
        paths:
          - backend:
              serviceName: nginx
              servicePort: 80
            path: /nginx-test

使用kubectl apply 重新更新ingress资源后,再使用curl命令去访问,如下
image.png
可以发现使用该path路径去访问的时候,居然是404,即无法访问到首页,这是为什么呢?原因是因为我们后端的nginx服务是启动在/(根路径)下的,因此访问/nginx-test/显示找不到,我们查询后端nginx的pod的日志,也可以引证这一点,如下
image.png
因此在实际中使用ingress资源的时候,经常需要配合ingress 的配置来使用,在kubernetes中,nginx ingress的配置是以annotation的形式存放的,我们还是以刚才返回404的path为例子,讲ingress的资源修改,如下

[root@a test]$ cat ingress-nginx.yaml
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  annotations:
    kubernetes.io/ingress.class: nginx
    nginx.ingress.kubernetes.io/rewrite-target: /$1
  name: ingress-nginx
spec:
  rules:
    - host: nginx.a.b
      http:
        paths:
          - backend:
              serviceName: nginx
              servicePort: 80
            path: /nginx-test(.*)

使用kubectl apply 创建后,再使用curl访问,如下
image.png
可以看到,使用/nginx-test路径去访问nginx服务,已经成功。

通过hostAliaes为pod添加额外的dns记录

当测试过程中,需要为测试的pod添加dns记录的时候,可以采用hostAliaes方式,给pod中的/etc/hosts文件添加映射关系。
创建1个nginx的deploy,其yaml如下

[root@a test]$ cat nginx-deploy.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
    ## 在192.168.1.8的宿主机上,我搭建了1个gitea服务
      hostAliases:
      - ip: "192.168.1.8"
        hostnames:
        - "git.a.b"
      containers:
      - image: nginx
        imagePullPolicy: Always
        name: nginx
        ports:
        - containerPort: 80
          name: http-nginx
          protocol: TCP
        terminationMessagePath: /dev/termination-log
        terminationMessagePolicy: File

使用kubectl apply 创建之后,验证如下
image.png
可以看到生成的pod的/etc/hosts文件有了对应的host记录,注意 pod中的/etc/hosts文件由kubelet维护,因此只有在pod生成的时候,才会写入记录 

备忘

ingress nginx的官方配置文档:https://github.com/kubernetes/ingress-nginx/blob/master/docs/user-guide/nginx-configuration/annotations.md
对于Kubernetes来说,网络的解决方案是众多的,但是必须包含以下方面

  • 每个Pod都拥有一个独立的IP地址,而且假定所有的pod都在一个可以直接连通的、扁平的网络空间中;
  • 用户不需要额外考虑如何建立Pod之间的连接,也不需要考虑将容器端口映射到主机端口等问题;
  • 网络方面:
    • 所有的容器都可以在不用NAT的方式下同别的容器通讯
    • 所有节点都可在不用NAT的方式下同所有容器通讯
    • 容器的地址和其他宿主机和容器看到的地址是同一个地址