Fork me on GitHub
Fork me on GitHub

Kubernetes Service资源

Service介绍

为了给客户端提供一个固定访问端点,因此在客户端和服务端(Pod)之间添加了一个固定的中间层,这个中间层称为service。这个service的名称解析强依赖于在k8s集群之上部署的一个附件叫k8s的DNS服务。较新版本中,使用的是CoreDNS。1.11之前的版本用的是kube-dns。
Kubernetes要想能够向客户端提供网络功能,需要依赖于第三方的方案。在k8s新版本中可通过cni(容器网络插件标准接口)来进行接入任何遵循这种插件标准的第三方方案。
在Kubernetes集群中有三类网络:

  • node network
  • pod network
  • service network(cluster network):虚拟的地址,Virtual IP。

在每个节点上,有个组件叫kube-proxy。这个组件将始终监视着master上apiserver中有关service资源的变动信息。随时要连到apiserver上获取任何一个与service资源相关的资源变动状态。这种是通过Kubernetes中固有的一种请求方法叫watch来实现的。一旦有service资源的内容发生变动,包括创建、修改、删除,kube-proxy都要把它转换为当前节点之上的能够实现service资源调度到后端特定Pod资源上的规则。这种规则可能是iptables,也有可能是ipvs,取决于service的实现方式。

Service工作模型

service的实现方式在Kubernetes之上有三种模型。

userspace模型

所谓用户空间可以理解为用户的请求。看下图。一是来自于用户内部的请求,Client Pod发请求,请求某个服务时,一定先到达当前节点内核空间的iptables规则,这个iptables规则其实就是service规则。这个service规则,它的工作方式是请求到达Service IP以后(iptables),由service先把它转为本地监听在某个套接字上的用户空间的kube-proxy,由它来负责处理,处理完后在转给Service IP,最终代理至这个service关联的相关Pod。kube-proxy是工作在用户空间的进程。所以被称为userspace。这种方式效率很低,原因在于先要到内核空间,然后然后回到当前主机上的用户空间,由kube-proxy封装请求报文代理完以后在回到内核空间,然后有iptables规则进行分发。效率很对。因此后来就到了第2种方式。

iptables

客户端IP请求service时直接请求service IP,这个请求被本地内核空间的service规则所截取,进而直接调度给相关的后端Pod。

ipvs

client请求到达内核空间后,直接由ipvs规则来调度。直接调度给后端的Pod。

在安装配置Kubernetes集群时,设定service工作在什么模式下,它应该就会生成对应的模式的规则。1.1及之前的版本用的是userspace,1.1-1.10之间用的是iptables,而1.11默认使用ipvs。当然如果安装时没有激活ipvs,它会自动降级为iptables。
如果某个service背后的Pod资源发生改变了,比如Pod多了一个,这个Pod的信息会立即反应到apiserver上,apiserver会把信息更新到etcd上,kube-proxy会检测到apiserver上这个变化,并将该变化立即转为iptables规则。转换是动态的,而且是实时的。
kubernetes集群安装完成后,默认有个service的名称叫 kubernetes。集群内的各种Pod需要和Kubernetes集群的apiserver联系时都要通过这个地址联系的。

Service字段解释

Service简称svc。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
[root@spark32 ~]# kubectl explain svc
KIND: Service
VERSION: v1
DESCRIPTION:
Service is a named abstraction of software service (for example, mysql)
consisting of local port (for example 3306) that the proxy listens on, and
the selector that determines which pods will answer requests sent through
the proxy.
FIELDS:
apiVersion <string>
APIVersion defines the versioned schema of this representation of an
object. Servers should convert recognized schemas to the latest internal
value, and may reject unrecognized values. More info:
https://git.k8s.io/community/contributors/devel/api-conventions.md#resources
kind <string>
Kind is a string value representing the REST resource this object
represents. Servers may infer this from the endpoint the client submits
requests to. Cannot be updated. In CamelCase. More info:
https://git.k8s.io/community/contributors/devel/api-conventions.md#types-kinds
metadata <Object>
Standard object's metadata. More info:
https://git.k8s.io/community/contributors/devel/api-conventions.md#metadata
spec <Object>
Spec defines the behavior of a service.
https://git.k8s.io/community/contributors/devel/api-conventions.md#spec-and-status
status <Object>
Most recently observed status of the service. Populated by the system.
Read-only. More info:
https://git.k8s.io/community/contributors/devel/api-conventions.md#spec-and-status

依然是这5个一级字段。

1
[root@spark32 ~]# kubectl explain svc.spec

svc.spec下有几个重要字段:

  • ports <[]Object> Service的哪个port和后端Pod的端口建立关联关系。
  • selector 关联到哪些Pod资源上
  • clusterIP 集群IP,这个IP是动态分配的。可以用这个字段定义固定地址。但是给定后,就无法修改了
  • type service类型。如果有必要的话,要指定type。ExternalName, ClusterIP, NodePort, and LoadBalancer。Defaults to ClusterIP.
    • ClusterIP:分配一个集群IP地址,仅用于集群内通信
    • NodePort:接入进群外部的流量
    • LoadBalancer:这表示把k8s部署在虚拟机上,而虚拟机是工作在云环境中,而云环境支持LBAAS,叫负载均衡级服务的一键调用,创建软负载均衡器使用的
    • ExternalName:把集群外部的服务引入到进群内部来

svc.spec.ports

  • name port的名称
  • nodePort 指定节点上的端口,这个不用定义,因为只有service的类型是NodePort时,节点端口才有用
  • port -required- 这个service对外提供服务的端口
  • protocol 不指定就是TCP
  • targetPort Pod的端口

Service示例

示例1:ClusterIP类型的Service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[root@spark32 manifests]# vim redis-svc.yaml
apiVersion: v1
kind: Service
metadata:
name: redis
namespace: default
spec:
selector:
app: redis
role: logstor
clusterIP: 10.97.97.97
type: ClusterIP
ports:
- port: 6379
targetPort: 6379


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[root@spark32 manifests]# kubectl apply -f redis-svc.yaml
service/redis created
[root@spark32 manifests]# kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 128d
redis ClusterIP 10.97.97.97 <none> 6379/TCP 4s
[root@spark32 manifests]# kubectl describe svc redis
Name: redis
Namespace: default
Labels: <none>
Annotations: kubectl.kubernetes.io/last-applied-configuration:
{"apiVersion":"v1","kind":"Service","metadata":{"annotations":{},"name":"redis","namespace":"default"},"spec":{"clusterIP":"10.97.97.97","...
Selector: app=redis,role=logstor
Type: ClusterIP
IP: 10.97.97.97
Port: <unset> 6379/TCP
TargetPort: 6379/TCP
Endpoints: 10.244.2.53:6379
Session Affinity: None
Events: <none>


其实service到pod是有一个中间层的,service不会直接到pod。service是先到endpoints。endpoints也是一个标准的Kubernetes资源对象,endpoints就是地址+端口。然后再由endpoints关联至后端的Pod。只不过作为我们来讲,可以理解为是从service直接到pod。事实上我们可以手动为service创建endpoints资源。

1
2
3
4
[root@spark32 manifests]# kubectl get endpoints
NAME ENDPOINTS AGE
kubernetes 172.16.206.32:6443 128d
redis 10.244.2.53:6379 5m3s

service创建完,只要k8s集群内的dns附件存在,那么我们就可以直接解析服务名。每个service创建,都会在集群dns中动态添加相应的资源记录。
资源记录: SVC_NAME.NS_NAME.DOMAIN.LTD.
集群的默认域名后缀:svc.cluster.local. 如果我们没有改这个特定域名后缀,每一个服务创建后对应的都是这种格式。比如:
redis.default.svc.cluster.local.

示例2:NodePort类型的Service

字段nodePort可以不指定,但是如果指定请确保指定的每一个节点的nodePort都不能被占用。这个端口默认是动态分配的,从30000-32767之间。当然如果你确保两个节点上的80没被占用,你在清单文件中指定80也是可以的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[root@spark32 manifests]# cp redis-svc.yaml myapp-svc.yaml
apiVersion: v1
kind: Service
metadata:
name: myapp
namespace: default
spec:
selector:
app: myapp
release: canary
clusterIP: 10.99.99.99
type: NodePort
ports:
- port: 80
targetPort: 80
nodePort: 30080


1
2
3
4
5
6
7
[root@spark32 manifests]# kubectl apply -f myapp-svc.yaml
service/myapp created
[root@spark32 manifests]# kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 128d
myapp NodePort 10.99.99.99 <none> 80:30080/TCP 4s
redis ClusterIP 10.97.97.97 <none> 6379/TCP 12m

Service的80端口映射到节点的30080端口。
集群外通过节点IP和PORT访问服务:

1
2
3
4
5
6
7
[root@spark32 manifests]# while true; do curl http://172.16.206.32:30080/hostname.html; sleep 1; done
myapp-deploy-67b6dfcd8-xzgdl
myapp-deploy-67b6dfcd8-ff9xf
myapp-deploy-67b6dfcd8-wls6h
myapp-deploy-67b6dfcd8-msv5x
myapp-deploy-67b6dfcd8-ff9xf
myapp-deploy-67b6dfcd8-ff9xf

示例3:LoadBalancer类型的service

假如你在阿里云上买了4个虚拟机,同时也买了LB服务LBAAS。在这4个虚拟主机上部署k8s集群,这个k8s集群可以与底层的公有云IaaS的api做交互,k8s具有这样的能力。调的时候能够去请求创建1个外置的负载均衡器。1个master,3个node。3个node上都使用同一个nodePort对外输出服务,它会自动请求底层IaaS的api,用纯软件的方式做一个负载均衡器,并且为这个负载均衡器提供的配置信息是这3个节点的节点端口上提供的服务。这样外部客户在访问时直接访问LBAAS,由它来调度到3个nodePort上。nodePort先代理给service,由service在集群内部负载均衡至Pod。OpenStack也支持LBAAS。

示例4:ExternalName类型的service

集群中建立的Service的endpoints不是集群内的Pod,而是集群外的服务。

Service会话绑定

service在实现负载均衡时还支持 sessionAffinity。默认是None,不做会话绑定。

1
2
3
4
5
sessionAffinity <string>
Supports "ClientIP" and "None". Used to maintain session affinity. Enable
client IP based session affinity. Must be ClientIP or None. Defaults to
None. More info:
https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies


1
2
[root@spark32 manifests]# kubectl patch svc myapp -p '{"spec":{"sessionAffinity":"ClientIP"}}'
service/myapp patched


headless Service

Pod也是有名称的,Pod的主机名就是Pod的名称。所谓无头,就是去掉service对应的clusterIP,解析service时解析到后端Pod的IP。这种service就叫headless service。

1
2
3
4
5
6
7
8
9
[root@spark32 manifests]# kubectl exec myapp-deploy-67b6dfcd8-ff9xf -it -- cat /etc/hosts
# Kubernetes-managed hosts file.
127.0.0.1 localhost
::1 localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
fe00::0 ip6-mcastprefix
fe00::1 ip6-allnodes
fe00::2 ip6-allrouters
10.244.1.19 myapp-deploy-67b6dfcd8-ff9xf

1
2
3
4
5
6
7
8
9
10
11
FIELDS:
clusterIP <string>
clusterIP is the IP address of the service and is usually assigned randomly
by the master. If an address is specified manually and is not in use by
others, it will be allocated to the service; otherwise, creation of the
service will fail. This field can not be changed through updates. Valid
values are "None", empty string (""), or a valid IP address. "None" can be
specified for headless services when proxying is not required. Only applies
to types ClusterIP, NodePort, and LoadBalancer. Ignored if type is
ExternalName. More info:
https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies

Valid values are “None”, empty string (“”), or a valid IP address. “None” can be specified for headless services when proxying is not required.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[root@spark32 manifests]# cp myapp-svc.yaml myapp-svc-headless.yaml
[root@spark32 manifests]# vim myapp-svc-headless.yaml
apiVersion: v1
kind: Service
metadata:
name: myapp-svc-headless
namespace: default
spec:
selector:
app: myapp
release: canary
clusterIP: None
ports:
- port: 80
targetPort: 80

1
2
3
4
5
6
7
8
[root@spark32 manifests]# kubectl apply -f myapp-svc-headless.yaml
service/myapp-svc-headless created
[root@spark32 manifests]# kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 128d
myapp NodePort 10.99.99.99 <none> 80:30080/TCP 37m
myapp-svc-headless ClusterIP None <none> 80/TCP 28s
redis ClusterIP 10.97.97.97 <none> 6379/TCP 50m

看看解析情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
[root@spark32 manifests]# dig -t A myapp-svc-headless.default.svc.cluster.local. @10.96.0.10
; <<>> DiG 9.9.4-RedHat-9.9.4-29.el7 <<>> -t A myapp-svc-headless.default.svc.cluster.local. @10.96.0.10
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 63409
;; flags: qr aa rd; QUERY: 1, ANSWER: 5, AUTHORITY: 0, ADDITIONAL: 1
;; WARNING: recursion requested but not available
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;myapp-svc-headless.default.svc.cluster.local. IN A
;; ANSWER SECTION:
myapp-svc-headless.default.svc.cluster.local. 5 IN A 10.244.3.7
myapp-svc-headless.default.svc.cluster.local. 5 IN A 10.244.2.45
myapp-svc-headless.default.svc.cluster.local. 5 IN A 10.244.1.19
myapp-svc-headless.default.svc.cluster.local. 5 IN A 10.244.1.18
myapp-svc-headless.default.svc.cluster.local. 5 IN A 10.244.2.46
;; Query time: 0 msec
;; SERVER: 10.96.0.10#53(10.96.0.10)
;; WHEN: Mon Aug 19 11:14:20 CST 2019
;; MSG SIZE rcvd: 373

直接解析到了Pod的地址。看看此前创建的Service myapp的解析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[root@spark32 manifests]# dig -t A myapp.default.svc.cluster.local. @10.96.0.10
; <<>> DiG 9.9.4-RedHat-9.9.4-29.el7 <<>> -t A myapp.default.svc.cluster.local. @10.96.0.10
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 9086
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; WARNING: recursion requested but not available
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;myapp.default.svc.cluster.local. IN A
;; ANSWER SECTION:
myapp.default.svc.cluster.local. 5 IN A 10.99.99.99
;; Query time: 0 msec
;; SERVER: 10.96.0.10#53(10.96.0.10)
;; WHEN: Mon Aug 19 11:15:06 CST 2019
;; MSG SIZE rcvd: 107

它就是解析到了Service的IP。
@10.96.0.10表示不使用本地的dns解析,10.96.0.10是集群CoreDNS的地址。

1
2
3
[root@spark32 manifests]# kubectl get svc -n kube-system
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kube-dns ClusterIP 10.96.0.10 <none> 53/UDP,53/TCP,9153/TCP 128d

StatefulSet用到的就是headless Service。

Service的问题

service有个问题,通过nodePort访问时需要做两次转换,另外无论是iptables还是ipvs,都是四层调度。因此如果我们要建一个https服务的话,每一个myapp都得配置为https的主机,因为四层调度是没有办法卸载https会话的。Kubernetes还有一种引入集群外部流量的方式。叫Ingress。
Ingress资源是一种七层调度器,因为它利用一种七层Pod来实现将外部流量引入到内部来。事实上它也脱离不了service的工作。可用的解决方法有Nginx、Haproxy等。还有Traefik。
k8s之上调度用的更多的还是Nginx。