第5关 k8s架构师课程攻克作战攻略之四-Service
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":""}}'