Service、Endpoint

K8s是运维人员的救星,为什么这么说呢,因为它里面的运行机制能确保你需要运行的服务,一直保持所期望的状态,还是以上面nginx服务举例,我们不能确保其pod运行的node节点什么时候会当掉,同时在pod环节也说过,pod的IP每次重启都会发生改变,所以我们不应该期望K8s的pod是健壮的,而是要按最坏的打算来假设服务pod中的容器会因为代码有bug、所以node节点不稳定等等因素发生故障而挂掉,这时候如果我们用的Deployment,那么它的controller会通过动态创建新pod到可用的node上,同时删除旧的pod来保证应用整体的健壮性;并且流量入口这块用一个能固定IP的service来充当抽象的内部负载均衡器,提供pod的访问,所以这里等于就是K8s成为了一个7 x 24小时在线处理服务pod故障的运维机器人。

创建一个service服务来提供固定IP轮询访问上面创建的nginx服务的2个pod(nodeport)

# 给这个nginx的deployment生成一个service(简称svc)
# 同时也可以用生成yaml配置的形式来创建 kubectl expose deployment nginx --port=80 --target-port=80 --dry-run=client -o yaml
# 我们可以先把上面的yaml配置导出为svc.yaml提供后面,这里就直接用命令行创建了
# kubectl expose deployment nginx --port=80 --target-port=80
service/nginx exposed

# kubectl get svc
NAME         TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
kubernetes   ClusterIP   10.68.0.1      <none>        443/TCP   4d23h
nginx        ClusterIP   10.68.18.121   <none>        80/TCP    5s

# 看下自动关联生成的endpoint
# kubectl  get endpoints nginx
NAME    ENDPOINTS                       AGE
nginx   172.20.139.72:80,172.20.217.72:80   27s

# 接下来测试下svc的负载均衡效果吧,这里我们先进到pod里面,把nginx的页面信息改为各自pod的hostname
# kubectl  exec -it nginx-f89759699-bzwd2 -- bash
root@nginx-f89759699-bzwd2:/# echo nginx-f89759699-bzwd2 > /usr/share/nginx/html/index.html
root@nginx-f89759699-bzwd2:/# exit
# kubectl  exec -it nginx-f89759699-qlc8q -- bash
root@nginx-f89759699-qlc8q:/# echo nginx-f89759699-qlc8q  > /usr/share/nginx/html/index.html
root@nginx-f89759699-qlc8q:/# exit

# curl 10.68.18.121
nginx-f89759699-bzwd2
# curl 10.68.18.121
nginx-f89759699-qlc8q

# 修改svc的类型来提供外部访问
# kubectl patch svc nginx -p '{"spec":{"type":"NodePort"}}'
service/nginx patched

# kubectl  get svc
NAME         TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)        AGE
kubernetes   ClusterIP   10.68.0.1     <none>        443/TCP        3d21h
nginx        NodePort    10.68.86.85   <none>        80:33184/TCP   30m

[root@node-2 ~]# curl 10.0.1.201:20651
nginx-f89759699-bzwd2
[root@node-2 ~]# curl 10.0.1.201:20651
nginx-f89759699-qlc8q

我们这里也来分析下这个svc的yaml配置

cat svc.yaml

apiVersion: v1       # <<<<<<  v1 是 Service 的 apiVersion
kind: Service        # <<<<<<  指明当前资源的类型为 Service
metadata:
  creationTimestamp: null
  labels:
    app: nginx
  name: nginx       # <<<<<<  Service 的名字为 nginx
spec:
  ports:
  - port: 80        # <<<<<<  将 Service 的 80 端口映射到 Pod 的 80 端口,使用 TCP 协议
    protocol: TCP
    targetPort: 80
  selector:
    app: nginx     # <<<<<<  selector 指明挑选那些 label 为 run: nginx 的 Pod 作为 Service 的后端
status:
  loadBalancer: {}

我们来看下这个nginx的svc描述

# kubectl describe svc nginx
Name:                     nginx
Namespace:                default
Labels:                   app=nginx
Annotations:              <none>
Selector:                 app=nginx
Type:                     NodePort
IP:                       10.68.18.121
Port:                     <unset>  80/TCP
TargetPort:               80/TCP
NodePort:                 <unset>  20651/TCP
Endpoints:                172.20.139.72:80,172.20.217.72:80
Session Affinity:         None
External Traffic Policy:  Cluster
Events:                   <none>

我们可以看到在Endpoints列出了2个pod的IP和端口,pod的ip是在容器中配置的,那么这里Service cluster IP又是在哪里配置的呢?cluster ip又是自律映射到pod ip上的呢?

# 首先看下kube-proxy的配置
# cat /etc/systemd/system/kube-proxy.service    
[Unit]
Description=Kubernetes Kube-Proxy Server
Documentation=https://github.com/GoogleCloudPlatform/kubernetes
After=network.target

[Service]
WorkingDirectory=/var/lib/kube-proxy
ExecStart=/opt/kube/bin/kube-proxy\
  --bind-address=10.0.1.202\
  --cluster-cidr=172.20.0.0/16\
  --hostname-override=10.0.1.202\
  --kubeconfig=/etc/kubernetes/kube-proxy.kubeconfig\
  --logtostderr=true\
  --proxy-mode=ipvs        #<-------  我们在最开始部署kube-proxy的时候就设定它的转发模式为ipvs,因为默认的iptables在存在大量svc的情况下性能很低
Restart=always
RestartSec=5
LimitNOFILE=65536

# 看下本地网卡,会有一个ipvs的虚拟网卡
# ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: ens32: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether 00:0c:29:20:b8:39 brd fe:fe:fe:fe:fe:ff
    inet 10.0.1.202/24 brd 10.0.1.255 scope global noprefixroute ens32
       valid_lft forever preferred_lft forever
    inet6 fe80::20c:29fe:fe20:b839/64 scope link
       valid_lft forever preferred_lft forever
3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
    link/ether 02:42:91:ac:ce:13 brd fe:fe:fe:fe:fe:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever
4: dummy0: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether 22:50:98:a6:f9:e4 brd fe:fe:fe:fe:fe:ff
5: kube-ipvs0: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN group default
    link/ether 96:6b:f0:25:1a:26 brd fe:fe:fe:fe:fe:ff
    inet 10.68.0.2/32 brd 10.68.0.2 scope global kube-ipvs0
       valid_lft forever preferred_lft forever
    inet 10.68.0.1/32 brd 10.68.0.1 scope global kube-ipvs0
       valid_lft forever preferred_lft forever
    inet 10.68.120.201/32 brd 10.68.120.201 scope global kube-ipvs0
       valid_lft forever preferred_lft forever
    inet 10.68.50.42/32 brd 10.68.50.42 scope global kube-ipvs0
       valid_lft forever preferred_lft forever
    inet 10.68.18.121/32 brd 10.68.18.121 scope global kube-ipvs0    # <-------- SVC的IP配置在这里
       valid_lft forever preferred_lft forever
6: caliaeb0378f7a4@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether ee:ee:ee:ee:ee:ee brd fe:fe:fe:fe:fe:ff link-netnsid 0
    inet6 fe80::ecee:eefe:feee:eeee/64 scope link
       valid_lft forever preferred_lft forever
7: tunl0@NONe: <NOARP,UP,LOWER_UP> mtu 1440 qdisc noqueue state UNKNOWN group default qlen 1000
    link/ipip 0.0.0.0 brd 0.0.0.0
    inet 172.20.247.0/32 brd 172.20.247.0 scope global tunl0
       valid_lft forever preferred_lft forever

# 来看下lvs的虚拟服务器列表
# ipvsadm -ln
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
  -> RemoteAddress:Port           Forward Weight ActiveConn InActConn
TCP  172.17.0.1:20651 rr
  -> 172.20.139.72:80             Masq    1      0          0        
  -> 172.20.217.72:80             Masq    1      0          0        
TCP  172.20.247.0:20651 rr
  -> 172.20.139.72:80             Masq    1      0          0        
  -> 172.20.217.72:80             Masq    1      0          0        
TCP  10.0.1.202:20651 rr
  -> 172.20.139.72:80             Masq    1      0          0        
  -> 172.20.217.72:80             Masq    1      0          0        
TCP  10.68.0.1:443 rr
  -> 10.0.1.201:6443              Masq    1      0          0        
  -> 10.0.1.202:6443              Masq    1      3          0        
TCP  10.68.0.2:53 rr
  -> 172.20.247.2:53              Masq    1      0          0        
TCP  10.68.0.2:9153 rr
  -> 172.20.247.2:9153            Masq    1      0          0        
TCP  10.68.18.121:80 rr                                        #<-----------   SVC转发Pod的明细在这里
  -> 172.20.139.72:80             Masq    1      0          0        
  -> 172.20.217.72:80             Masq    1      0          0        
TCP  10.68.50.42:443 rr
  -> 172.20.217.71:4443           Masq    1      0          0        
TCP  10.68.120.201:80 rr
  -> 10.0.1.201:80                Masq    1      0          0        
  -> 10.0.1.202:80                Masq    1      0          0        
TCP  10.68.120.201:443 rr
  -> 10.0.1.201:443               Masq    1      0          0        
  -> 10.0.1.202:443               Masq    1      0          0        
TCP  10.68.120.201:10254 rr
  -> 10.0.1.201:10254             Masq    1      0          0        
  -> 10.0.1.202:10254             Masq    1      0          0        
TCP  127.0.0.1:20651 rr
  -> 172.20.139.72:80             Masq    1      0          0        
  -> 172.20.217.72:80             Masq    1      0          0        
UDP  10.68.0.2:53 rr
  -> 172.20.247.2:53              Masq    1      0          0

除了直接用cluster ip,以及上面说到的NodePort模式来访问Service,我们还可以用K8s的DNS来访问

# 我们前面装好的CoreDNS,来提供K8s集群的内部DNS访问
# kubectl -n kube-system get deployment,pod|grep dns
deployment.apps/coredns                   1/1     1            1           5d2h
pod/coredns-d9b6857b5-tt7j2                    1/1     Running   1          27h

# coredns是一个DNS服务器,每当有新的Service被创建的时候,coredns就会添加该Service的DNS记录,然后我们通过serviceName.namespaceName就可以来访问到对应的pod了,下面来演示下:

# kubectl run -it --rm busybox --image=busybox -- sh    # --rm代表等我退出这个pod后,它会被自动删除,当作一个临时pod在用
If you don't see a command prompt, try pressing enter.
/ # ping nginx.default
PING nginx.default (10.68.18.121): 56 data bytes
64 bytes from 10.68.18.121: seq=0 ttl=64 time=0.096 ms
64 bytes from 10.68.18.121: seq=1 ttl=64 time=0.067 ms
64 bytes from 10.68.18.121: seq=2 ttl=64 time=0.065 ms
^C
--- nginx.default ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.065/0.076/0.096 ms
/ # wget nginx.default
Connecting to nginx.default (10.68.18.121:80)
saving to 'index.html'
index.html           100% |********************************************************************|    22  0:00:00 ETA
'index.html' saved
/ # cat index.html
nginx-f89759699-bzwd2

service生产小技巧 通过svc来访问非K8s上的服务

上面我们提到了创建service后,会自动创建对应的endpoint,这里面的关键在于 selector: app: nginx 基于lables标签选择了一组存在这个标签的pod,然而在我们创建svc时,如果没有定义这个selector,那么系统是不会自动创建endpoint的,我们可不可以手动来创建这个endpoint呢?答案是可以的,在生产中,我们可以通过创建不带selector的Service,然后创建同样名称的endpoint,来关联K8s集群以外的服务,这个具体能带给我们运维人员什么好处呢,就是我们可以直接复用K8s上的ingress(这个后面会讲到,现在我们就当它是一个nginx代理),来访问K8s集群以外的服务,省去了自己搭建前面Nginx代理服务器的麻烦

开始实践测试

这里我们挑选node-2节点,用python运行一个简易web服务器

[root@node-2 mnt]# python -m SimpleHTTPServer 9999
Serving HTTP on 0.0.0.0 port 9999 ...

然后我们用之前学会的方法,来生成svc和endpoint的yaml配置,并修改成如下内容,并保存为mysvc.yaml

注意Service和Endpoints的名称必须一致

# 注意我这里把两个资源的yaml写在一个文件内,在实际生产中,我们经常会这么做,方便对一个服务的所有资源进行统一管理,不同资源之间用"---"来分隔
apiVersion: v1
kind: Service
metadata:
  name: mysvc
  namespace: default
spec:
  type: ClusterIP
  ports:
  - port: 80
    protocol: TCP

---

apiVersion: v1
kind: Endpoints
metadata:
  name: mysvc
  namespace: default
subsets:
- addresses:
  - ip: 10.0.1.202
    nodeName: 10.0.1.202
  ports:
  - port: 9999
    protocol: TCP

开始创建并测试

# kubectl  apply -f mysvc.yaml
service/mysvc created
endpoints/mysvc created

# kubectl get svc,endpoints |grep mysvc
service/mysvc        ClusterIP   10.68.71.166   <none>        80/TCP         14s
endpoints/mysvc        10.0.1.202:9999                     14s

# curl 10.68.71.166
mysvc

# 我们回到node-2节点上,可以看到有一条刚才的访问日志打印出来了
10.0.1.201 - - [25/Nov/2020 14:42:45] "GET / HTTP/1.1" 200 -

外部网络如何访问到Service呢?

在上面其实已经给大家演示过了将Service的类型改为NodePort,然后就可以用node节点的IP加端口就能访问到Service了,我们这里来详细分析下原理,以便加深印象

# 我们看下先创建的nginx service的yaml配置
# kubectl get svc nginx -o yaml
apiVersion: v1
kind: Service
metadata:
  creationTimestamp: "2020-11-25T03:55:05Z"
  labels:
    app: nginx
  managedFields:       # 在新版的K8s运行的资源配置里面,会输出这么多的配置信息,这里我们可以不用管它,实际我们在创建时,这些都是忽略的
  - apiVersion: v1
    fieldsType: FieldsV1
    fieldsV1:
      e:metadata:
        e:labels:
          .: {}
          e:app: {}
      e:spec:
        e:externalTrafficPolicy: {}
        e:ports:
          .: {}
          k:{"port":80,"protocol":"TCP"}:
            .: {}
            e:port: {}
            e:protocol: {}
            e:targetPort: {}
        e:selector:
          .: {}
          e:app: {}
        e:sessionAffinity: {}
        e:type: {}
    manager: kubectl
    operation: Update
    time: "2020-11-25T04:00:28Z"
  name: nginx
  namespace: default
  resourceVersion: "591029"
  selfLink: /api/v1/namespaces/default/services/nginx
  uid: 84fea557-e19d-486d-b879-13743c603091
spec:
  clusterIP: 10.68.18.121
  externalTrafficPolicy: Cluster
  ports:
  - nodePort: 20651     # 我们看下这里,它定义的一个nodePort配置,并分配了20651端口,因为我们先前创建时并没有指定这个配置,所以它是随机生成的
    port: 80
    protocol: TCP
    targetPort: 80
  selector:
    app: nginx
  sessionAffinity: None
  type: NodePort
status:
  loadBalancer: {}

# 我们看下apiserver的配置
# cat /etc/systemd/system/kube-apiserver.service
[Unit]
Description=Kubernetes API Server
Documentation=https://github.com/GoogleCloudPlatform/kubernetes
After=network.target

[Service]
ExecStart=/opt/kube/bin/kube-apiserver\
  --advertise-address=10.0.1.201\
  --allow-privileged=true\
  --anonymous-auth=false\
  --authorization-mode=Node,RBAC\
  --token-auth-file=/etc/kubernetes/ssl/basic-auth.csv\
  --bind-address=10.0.1.201\
  --client-ca-file=/etc/kubernetes/ssl/ca.pem\
  --endpoint-reconciler-type=lease\
  --etcd-cafile=/etc/kubernetes/ssl/ca.pem\
  --etcd-certfile=/etc/kubernetes/ssl/kubernetes.pem\
  --etcd-keyfile=/etc/kubernetes/ssl/kubernetes-key.pem\
  --etcd-servers=https://10.0.1.201:2379,https://10.0.1.202:2379,https://10.0.1.203:2379\
  --kubelet-certificate-authority=/etc/kubernetes/ssl/ca.pem\
  --kubelet-client-certificate=/etc/kubernetes/ssl/myk8s.pem\
  --kubelet-client-key=/etc/kubernetes/ssl/myk8s-key.pem\
  --kubelet-https=true\
  --service-account-key-file=/etc/kubernetes/ssl/ca.pem\
  --service-cluster-ip-range=10.68.0.0/16\
  --service-node-port-range=20000-40000 \      # 这就是NodePor随机生成端口的范围,这个在我们部署时就指定了
  --tls-cert-file=/etc/kubernetes/ssl/kubernetes.pem\
  --tls-private-key-file=/etc/kubernetes/ssl/kubernetes-key.pem\
  --requestheader-client-ca-file=/etc/kubernetes/ssl/ca.pem\
  --requestheader-allowed-names=\
  --requestheader-extra-headers-prefix=X-Remote-Extra-\
  --requestheader-group-headers=X-Remote-Group\
  --requestheader-username-headers=X-Remote-User\
  --proxy-client-cert-file=/etc/kubernetes/ssl/aggregator-proxy.pem\
  --proxy-client-key-file=/etc/kubernetes/ssl/aggregator-proxy-key.pem\
  --enable-aggregator-routing=true\
  --v=2
Restart=always
RestartSec=5
Type=notify
LimitNOFILE=65536

[Install]
WantedBy=multi-user.target

# NodePort端口会在所在K8s的node节点上都生成一个同样的端口,这就使我们无论所以哪个node的ip接端口都能方便的访问到Service了,但在实际生产中,这个NodePort不建议经常使用,因为它会造成node上端口管理混乱,等用到了ingress后,你就不会想使用NodePort模式了,这个接下来会讲到

[root@node-1 ~]# ipvsadm -ln|grep -C6 20651
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
  -> RemoteAddress:Port           Forward Weight ActiveConn InActConn
......      
TCP  10.0.1.201:20651 rr      # 这里
  -> 172.20.139.72:80             Masq    1      0          0        
  -> 172.20.217.72:80             Masq    1      0          0    

[root@node-2 mnt]# ipvsadm -ln|grep -C6 20651
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
  -> RemoteAddress:Port           Forward Weight ActiveConn InActConn
......      
TCP  10.0.1.202:20651 rr      # 这里
  -> 172.20.139.72:80             Masq    1      0          0        
  -> 172.20.217.72:80             Masq    1      0          0  

生产中Service的调优

# 先把nginx的pod数量调整为1,方便呆会观察
# kubectl scale deployment nginx --replicas=1
deployment.apps/nginx scaled

# 看下这个nginx的pod运行情况,-o wide显示更详细的信息,这里可以看到这个pod运行在node 203上面
# kubectl  get pod -o wide                    
NAME                    READY   STATUS    RESTARTS   AGE     IP              NODE         NOMINATED NODE   READINESS GATES
nginx-f89759699-qlc8q   1/1     Running   0          3h27m   172.20.139.72   10.0.1.203   <none>           <none>

# 我们先直接通过pod运行的node的IP来访问测试
[root@node-1 ~]# curl 10.0.1.203:20651
nginx-f89759699-qlc8q

# 可以看到日志显示这条请求的来源IP是203,而不是node-1的IP 10.0.1.201
# 注: kubectl logs --tail=1 代表查看这个pod的日志,并只显示倒数第一条
[root@node-1 ~]# kubectl logs --tail=1 nginx-f89759699-qlc8q
10.0.1.203 - - [25/Nov/2020:07:22:54 +0000] "GET / HTTP/1.1" 200 22 "-" "curl/7.29.0" "-"

# 再来通过201来访问
[root@node-1 ~]# curl 10.0.1.201:20651          
nginx-f89759699-qlc8q

# 可以看到显示的来源IP非node节点的
[root@node-1 ~]# kubectl logs --tail=1 nginx-f89759699-qlc8q
172.20.84.128 - - [25/Nov/2020:07:23:18 +0000] "GET / HTTP/1.1" 200 22 "-" "curl/7.29.0" "-"

# 这就是一个虚拟网卡转发的
[root@node-1 ~]# ip a|grep -wC2 172.20.84.128
9: tunl0@NONe: <NOARP,UP,LOWER_UP> mtu 1440 qdisc noqueue state UNKNOWN group default qlen 1000
    link/ipip 0.0.0.0 brd 0.0.0.0
    inet 172.20.84.128/32 brd 172.20.84.128 scope global tunl0
       valid_lft forever preferred_lft forever

# 可以看下lvs的虚拟服务器列表,正好是转到我们要访问的pod上的
[root@node-1 ~]# ipvsadm -ln|grep -A1 172.20.84.128
TCP  172.20.84.128:20651 rr
  -> 172.20.139.72:80             Masq    1      0          0      

详细处理流程如下:

* 客户端发送数据包 10.0.1.201:20651
* 10.0.1.201 用自己的IP地址替换数据包中的源IP地址(SNAT)
* 10.0.1.201 使用 pod IP 替换数据包上的目标 IP
* 数据包路由到 10.0.1.203 ,然后路由到 endpoint
* pod的回复被路由回 10.0.1.201
* pod的回复被发送回客户端

                   client
                      \ ^
                       \\
                        v\
10.0.1.203 <--- 10.0.1.201
    | ^        SNAT
    | |           --->
    v |
 endpoint   

为避免这种情况, Kubernetes 具有保留客户端IP 的功能。设置 service.spec.externalTrafficPolicy 为 Local 会将请求代理到本地端点,不将流量转发到其他节点,从而保留原始IP地址。如果没有本地端点,则丢弃发送到节点的数据包,因此您可以在任何数据包处理规则中依赖正确的客户端IP。

# 设置 service.spec.externalTrafficPolicy 字段如下:
# kubectl patch svc nginx -p '{"spec":{"externalTrafficPolicy":"Local"}}'
service/nginx patched

# 现在通过非pod所在node节点的IP来访问是不通了
[root@node-1 ~]# curl 10.0.1.201:20651                      
curl: (7) Failed connect to 10.0.1.201:20651; Connection refused

# 通过所在node的IP发起请求正常
[root@node-1 ~]# curl 10.0.1.203:20651
nginx-f89759699-qlc8q

# 可以看到日志显示的来源IP就是201,这才是我们想要的结果
[root@node-1 ~]# kubectl logs --tail=1 nginx-f89759699-qlc8q          
10.0.1.201 - - [25/Nov/2020:07:33:42 +0000] "GET / HTTP/1.1" 200 22 "-" "curl/7.29.0" "-"

# 去掉这个优化配置也很简单
# kubectl patch svc nginx -p '{"spec":{"externalTrafficPolicy":""}}'

视频

https://www.ixigua.com/6938384340048937480

标签: k8s, service

添加新评论