Service-服务

服务

kubernetes服务是为一组功能相同的pod提供单一不变的接入点资源。当一个服务存在时,它的IP和端口不会改变。客户端通过IP地址和端口号建立连接。连接会被路由到提供该服务的任意一个pod。

通过这种方式,客户端不需要知道提供服务pod的ip和端口信息,这样这些pod可以在集群中随时被创建和删除。

假如有前端web服务器和后端数据库服务器,有很多pod提供前端服务,而中有一个pod提供后台数据库服务。需要解决两个问题才能发挥系统作用

  • 外部客户无须关心服务器数量而连接到前端Pod上。
  • 前端的pod需要连接后端的数据库。由于数据库运行在pod上,他可能会在集群中随时重启而移动,导致IP地址随时变动。当后端数据库被移动时,不必为前端pod重新配置。

通过为前端pod创建服务,并且将其配置成可以在集群外部访问。可以暴露一个单一不变的IP提供给外部客户端访问,同样的,可以在后端创建一个服务,分配一个固定的ip地址。尽管pod的IP地址会改变,但是服务的IP地址固定不变,并始终指向提供前端服务的pod。通过创建服务,能够让前端的pod通过环境变量或DNS以及服务名来访问后端服务。

创建一个服务

通过标签选择器决定哪些pod属于服务

服务的后端可以有不止一个pod。服务的连接对所有的后端pod是负载均衡的。那么,要如何准确定义哪些pod是属于这个服务的呢

通过kubectl expose 创建服务

创建服务最简单的方法是通过kubectl expose,像创建Replication时使用的pod选择器那样,利用expose命令和pod选择器来创建服务资源,从而通过单个并固定的ip来访问所有的pod。

通过YAML描述文件来创建服务

1
2
3
4
5
6
7
8
9
10
11
apiVersion: v1
kind: Service
metadata:
name: static-blog
spec:
selector:
webtype: staticblog #具有webtype=staticblog标签的pod都属于该服务
ports:
- protocol: TCP
port: 80 #该服务的可用端口
targetPort: 80 #服务将连接转发到这个容器端口

使用kubectl create -f [filename]发布文件来创建服务。

创建了一个名叫static-blog的服务,它将在端口80接收请求并将连接路由到具有webtype=staticblog标签的pod80端口上。

查看新的服务

1
2
3
4
kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 443/TCP 32d
static-blog ClusterIP 10.105.172.229 80/TCP 30d

从内部集群测试服务

可以通过以下几种方法向服务发送请求:

  • 创建一个Pod,将请求发送到服务的集群IP并记录响应。通过查看Pod日志检查服务的响应。

  • ssh登录到kubernetes集群的一台子节点上,通过curl命令查看。

  • 通过Kubectl exec登录到一个已存在的pod中执行curl命令。

在运行的容器中执行命令

1
kubectl exec nginx-hexo-blog-798d89bfd4-ntqkj -- curl -s http://10.105.172.229

双横杠(–)代表着kubectl命令的结束。在两个横杠之后的内容是指在pod内部要执行的命令,如果需要执行的命令没有以横杠开始的参数,那么双横杠也不用必须添加。

** 如果这里不使用双横杠,curl命令的-s选项会被解析成kubectl exec选项,导致异常**

1
2
kubectl exec nginx-hexo-blog-798d89bfd4-ntqkj  curl -s http://10.105.172.229
Unable to connect to the server: dial tcp 10.105.172.229:443: i/o timeout

服务会拒绝连接。这是因为kubectl并不能连接到位于10.105.172.229的api服务器(-s选项kubectl需要连接一个不同的api服务器而不是默认的)。

在一个pod容器上执行curl命令。curl命令会向一个后端有三个Pod服务的IP发送了HTTP请求,Kubernetes服务获取该连接,将请求转发给三个Pod中任意一个Pod。nginx应用在Pod中获取该请求,并返回该Pod nginx的HTTP响应。curl命令打印返回值,该返回值被Kubectl截取并打印到宿主机。

配置服务上的会话亲和性

在 kubernetes 集群中有两种负载均衡分发策略,也可以称之为服务的会话亲和性:

  • RoundRobin:轮询模式,这是默认配置。即定义 YAML 文件时不设置spec.sessionAffinity或设置spec.sessionAffinity=None。它表示的是将请求分发到后端各个 Pod 上,没有固定某个 Pod 对请求进行响应。
  • SessionAffinity:会话保持模式,需要在定义 YAML 文件时设置spec.sessionAffinity=ClientIP。当某个客户端第一次请求转发到后端的某个 Pod 上,那么之后这个客户端的请求也一直由相同的 Pod 进行响应。

暴露多个端口

服务可以暴露一个端口,也可以暴露多个端口。提供nginx服务的Pod有http和https端口,可以将服务暴露两个端口转发至pod的http和https端口,这样就不必创建两个服务了。

当在一个服务中创建多个端口时,需要给每个端口指定名字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
apiVersion: v1
kind: Service
metadata:
name: static-blog
spec:
sessionAffinity: ClientIP
selector:
webtype: staticblog
ports:
- name: http
port: 80
targetPort: 80 # pod的80端口
- name: https
port: 8080
targetPort: 443 #pod的443端口

当在多个端口的服务中设置SessionAffinity时,客户端第一次请求的端口转发到某个pod上时,之后这个客户端的请求也一直由相同的pod进行响应,当有多个端口时,每一个独立端口都会有会话保持模式。

使用命名的端口创建服务

在pod的容器定义中,可以使用一个名字来指定相应的容器端口,这样服务中就可以将targetPort端口指定为pod的命名端口,当spec pod中的端口发生改变时,服务的spec就不需要更改了。

1
2
3
4
5
6
7
8
9
10
......
containers:
- name: hexo-blog
image: nginx:1.20.1
ports:
- name: http
containerPort: 80
- name: https
containerPort: 443
......

service中的设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: v1
kind: Service
metadata:
name: static-blog
spec:
selector:
webtype: staticblog
ports:
- name: http
port: 80
targetPort: http #pod的80端口
- name: https
port: 8080
targetPort: https #pod的443端口

服务发现

通过环境变量发现服务

1
2
3
4
5
6
7
8
[root@VM-20-9-centos nginx]# kubectl exec -it nginx-hexo-blog-7f478ccb98-t86vj -- env|grep -i service
KUBERNETES_SERVICE_HOST=10.96.0.1
KUBERNETES_SERVICE_PORT=443
STATIC_BLOG_SERVICE_PORT=80
STATIC_BLOG_SERVICE_PORT_HTTPS=8080
KUBERNETES_SERVICE_PORT_HTTPS=443
STATIC_BLOG_SERVICE_HOST=10.105.227.251
STATIC_BLOG_SERVICE_PORT_HTTP=80

当创建一个Pod的时候,kubelet会在该Pod中放入集群内所有Service的相关环境变量。需要注意的是,要想一个Pod中放入某个Service的环境变量,则Service要先比Pod创建。

通过DNS发现服务

kube-dns运行着k8s内部的dns服务,每个服务从内部DNS获得一个DNS条目,pod可以用全限定域名FQDN来访问服务

1
static-blog.default.svc.cluster.local

static-blog对应着服务名称

default为Service所在的命名空间

svc.cluster.local是在所有集群本地服务名称中使用的可配置集群后缀

在Kubernetes中,ClusterIP/NodePort类型的Service的IP地址是虚拟的,并不分配给任何实体【没有实际的mac地址】,因此无法响应ICMP请求,也就是说,它们不能被ping通。这是因为Kubernetes的Service仅仅是一个IP地址和端口的组合,用于负载均衡和服务发现,而不是一个实际的网络接口。

endpoint

服务并不是与pod直接相连的,还有一个资源介于两者之间,这就是endpoint

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[root@VM-20-9-centos ~]# kubectl describe svc  static-blog
Name: static-blog
Namespace: default
Labels:
Annotations:
Selector: webtype=staticblog #用于创建服务时选择pod
Type: ClusterIP
IP: 10.96.100.84
Port: http 80/TCP
TargetPort: http/TCP
Endpoints: 10.244.1.16:80,10.244.2.23:80
Port: https 8080/TCP
TargetPort: https/TCP
Endpoints: 10.244.1.16:443,10.244.2.23:443 #用于endpoint的pod ip和端口
Session Affinity: None
Events:

endpint同其他kubernetes资源一样,可以用kubectl info来获取信息

1
2
3
[root@VM-20-9-centos ~]# kubectl get endpoints static-blog
NAME ENDPOINTS AGE
static-blog 10.244.1.16:80,10.244.2.23:80,10.244.1.16:443 + 1 more... 11d
1
2
3
4
5
6
7
8
9
10
11
12
13
[root@VM-20-9-centos nginx]# kubectl describe endpoints static-blog
Name: static-blog
Namespace: default
Labels:
Annotations:
Subsets:
Addresses: 10.244.1.16,10.244.2.23
NotReadyAddresses:
Ports:
Name Port Protocol
---- ---- --------
http 80 TCP
https 443 TCP

Events: <none>

手动配置endpoint

创建一个服务,并没有定义selector标签

1
2
3
4
5
6
7
8
[root@VM-20-9-centos nginx]# cat enternal-server.yaml
apiVersion: v1
kind: Service
metadata:
name: enternal-service
spec:
ports:
- port: 80

为service手动创建一个endpoint。

[root@VM-20-9-centos nginx]# cat enternal-server-endpoints.yaml 
apiVersion: v1
kind: Endpoints
metadata:
  name: enternal-service   #endpoint的名称必须和服务的名称相匹配
subsets:
  - addresses:
     - ip 10.10.10.10    #服务将连接重定向到endpoint的IP地址
     - ip 10.10.10.11
    ports:
      - port: 80   # endpoint的目标端口

如上文所说,在此服务之前创建的环境变量中不会包含该服务的环境变量。

1
2
3
4
5
6
7
8
[root@VM-20-9-centos nginx]# kubectl exec -it nginx-hexo-blog-7f478ccb98-t86vj -- env|grep -i service
KUBERNETES_SERVICE_HOST=10.96.0.1
KUBERNETES_SERVICE_PORT=443
STATIC_BLOG_SERVICE_PORT=80
STATIC_BLOG_SERVICE_PORT_HTTPS=8080
KUBERNETES_SERVICE_PORT_HTTPS=443
STATIC_BLOG_SERVICE_HOST=10.105.227.251
STATIC_BLOG_SERVICE_PORT_HTTP=80

当把此pod删除让其自动创建后,在环境变量中就可以看到此服务的信息。

1
2
3
4
5
[root@VM-20-9-centos nginx]# kubectl exec -it nginx-hexo-blog-7f478ccb98-6dck2 -- env|grep -i service
EXTERNAL_SERVICE_SERVICE_HOST=10.111.125.202
EXTERNAL_SERVICE_PORT_80_TCP=tcp://10.111.125.202:80
EXTERNAL_SERVICE_PORT_80_TCP_ADDR=10.111.125.202
...

Endpoint对象需要与服务相同的名称,并要在endpoint对象中定义service的目标IP和端口

创建用于访问外部服务的service类型

如果在k8s集群外部有需要访问的服务,可以将service的type设置为ExternalName。

假如外部有一个nginx需要提供给内部的pod访问

1
2
3
4
5
6
7
8
9
10
cat  external-service.yaml
apiVersion: v1
kind: Service
metadata:
name: external-service
spec:
type: ExternalName
externalName: www.mountainmist.work
ports:
- port: 443

ExternalName服务会创建一个CNAME DNS记录,pod可以使用external-service.default.svc.cluster.local连接到此外部服务,

服务暴露至外部

使用nodeport类型的服务

1
2
3
4
5
6
7
8
9
10
11
12
13
cat nginx-nodeport.yml
apiVersion: v1
kind: Service
metadata:
name: test-nodeport
spec:
type: NodePort # 设置服务类型为NodePort
ports:
- port: 80
targetPort: 80 # pod的目标端口
nodePort: 30012 #通过k8s集群节点的该端口可以访问
selector:
webtype: staticblog

指定端口并不是必须的,如果没有指定,kubernetes将选择一个随机端口。

使用jsonpath可以获取节点所有ip

1
kubectl get nodes -o jsonpath='{.items[*].status.addresses[?(@.type=="InternalIP")].address}'

使用Ingress

在Kubernetes中,为了使外部的应用访问集群内的service,可以使用NodePort和LoadBalancer两种类型的service。

NodePort方式会占用很多集群机器的端口,而LoadBanlancer类型要求Kubernetes运行在支持的云上。当同时存在多个LoadBanlancer类型的Service时,就会占用大量公网ip地址,而Ingress只需要一个公网IP就能为许多服务提供访问,当客户端发起请求时,Ingress会根据请求的主机名和路径将请求转发到哪个服务。

Ingress在应用层操作,可以提供服务不能实现的功能,如基于cookie的会话亲和性。

关于Ingress控制器

Ingress控制器类似于nginx,Ingress类似于nginx的配置文件。如果只安装nginx,而没有nginx的配置文件,则此nginx将毫无意义;反过来,若只有nginx的配置文件,但没安装nginx,则配置文件无运行载体,所以,只有ingress 控制器在kubernetes集群中正常运行,ingress资源才能正常工作,不同的k8s环境可以选择不同的ingress控制器来实现

创建一个ingress 控制器

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
[root@VM-20-9-centos ingress]# cat deploy_nginx.yaml

...

apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app.kubernetes.io/component: controller
app.kubernetes.io/instance: ingress-nginx
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
app.kubernetes.io/version: 1.10.1
name: ingress-nginx-controller
namespace: ingress-nginx
spec:
minReadySeconds: 0
revisionHistoryLimit: 10
selector:
matchLabels:
app.kubernetes.io/component: controller
app.kubernetes.io/instance: ingress-nginx
app.kubernetes.io/name: ingress-nginx
strategy:
rollingUpdate:
maxUnavailable: 1
type: RollingUpdate
template:
metadata:
labels:
app.kubernetes.io/component: controller
app.kubernetes.io/instance: ingress-nginx
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
app.kubernetes.io/version: 1.10.1
spec:
hostNetwork: true #暴露nginx-ingress-controller pod的服务端口(80/443/8443)至宿主机
containers:
- args:
- /nginx-ingress-controller
- --publish-service=$(POD_NAMESPACE)/ingress-nginx-controller
- --election-id=ingress-nginx-leader
- --controller-class=k8s.io/ingress-nginx
- --ingress-class=nginx
- --configmap=$(POD_NAMESPACE)/ingress-nginx-controller
- --validating-webhook=:8443
- --validating-webhook-certificate=/usr/local/certificates/cert
- --validating-webhook-key=/usr/local/certificates/key
- --enable-metrics=false
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: LD_PRELOAD
value: /usr/local/lib/libmimalloc.so
image: registry.cn-hangzhou.aliyuncs.com/google_containers/nginx-ingress-controller:v1.10.1
imagePullPolicy: IfNotPresent
lifecycle:
preStop:
exec:
command:
- /wait-shutdown
livenessProbe:
failureThreshold: 5
httpGet:
path: /healthz
port: 10254
scheme: HTTP
initialDelaySeconds: 10
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 1
name: controller
ports:
- containerPort: 80
name: http
protocol: TCP
- containerPort: 443
name: https
protocol: TCP
- containerPort: 8443
name: webhook
protocol: TCP
...
nodeSelector:
ingress: "true" #这里将pod调度到带有ingress=true的k8s集群工作节点上,避免端口存在冲突导致Pod无法启动
...

创建一个ingress资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[root@VM-20-9-centos ingress]# cat ingress.yaml 
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-blog
spec:
ingressClassName: nginx
rules:
- host: test.example.com #ingress将域名映射到服务
http:
paths:
- path: / #prefix / 表示匹配所有路径
pathType: Prefix
backend:
service:
name: test-nodeport #ingress将请求发送到此服务
port:
number: 80 #ingress将请求发送到此服务端口

通过ingress访问服务

要确保通过ingress访问服务,需要确保将域名解析成为ingress的ip

获取ingress的ip

1
2
3
kubectl get ingress test-ingress
NAME CLASS HOSTS ADDRESS PORTS AGE
test-ingress external-lb * 203.0.113.123 80 59s

address不显示时,因为设置控制器Pod使用宿主机的端口,所以ingres控制器的ip就是宿主机的ip,知道ip以后,通过配置DNS服务器将test.example.com解析为此ip,或者在/etc/hosts文件中(windows系统为C:\Windows\System32\drivers\etc\hosts)添加以下内容:

1
203.0.113.123  test.example.com

通过ingress访问pod

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[root@VM-20-9-centos ingress]# curl test.example.com 
<!DOCTYPE html>
<html lang="zh-CN">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width">
<meta name="theme-color" content="#222"><meta name="generator" content="Hexo 7.2.0">

<link rel="apple-touch-icon" sizes="180x180" href="/images/apple-touch-icon-next.png">
<link rel="icon" type="image/png" sizes="32x32" href="/images/favicon-32x32-next.png">
<link rel="icon" type="image/png" sizes="16x16" href="/images/favicon-16x16-next.png">
<link rel="mask-icon" href="/images/logo.svg" color="#222">

<link rel="stylesheet" href="/css/main.css">
...

查看ingress控制器的日志可以看到,请求转发到了test-nodeport的80端口,并把它转发到pod 10.244.9.110

1
2
3
[root@VM-20-9-centos ingress]# kubectl logs -f --tail 20 ingress-nginx-controller-9c6994c4d-72vgh -n ingress-nginx
82.156.79.118 - - [10/Oct/2024:03:56:26 +0000] "GET / HTTP/1.1" 200 33547 "-" "curl/7.29.0" 80 0.001 [default-test-nodeport-80] [] 10.244.9.110:80 33547 0.000 200 00210085880e233324f53a9c58a12e23
82.156.79.118 - - [10/Oct/2024:03:56:38 +0000] "GET / HTTP/1.1" 200 33547 "-" "curl/7.29.0" 80 0.001 [default-test-nodeport-80] [] 10.244.9.110:80 33547 0.000 200 7b31d9f8362676a68c6c06962645a667

了解ingress的工作原理

客户端首先会对test.example.com执行DNS查找,DNS或本地系统返回了ingress控制器的ip。然后客户端向ingress控制器发送HTTP请求,并在Host头中指定test.example.com。控制器从头部确定客户端尝试访问哪个服务,通过与该服务关联的Endpoint对象查看pod ip,并将客户端的请求转发给其中一个Pod

ingress控制器不会将请求转发给服务,只用它来选择一个pod

通过ingress暴露多个服务

将不同的服务映射到相同域名的不同路径

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@VM-20-9-centos ingress]# cat ingress_1.yaml 
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-blog-test
spec:
ingressClassName: nginx
rules:
- host: test.example.com
http:
paths:
- path: /bar
pathType: Exact
backend:
service:
name: test-nodeport
port:
number: 80
- path: /foo
pathType: Exact
backend:
service:
name: static-blog
port:
number: 80

在这种情况下,根据请求URL的路径,请求将发送到两个不同的服务。客户端可以通过一个Ingress IP访问两个不同的服务

1
2
3
4
[root@VM-20-9-centos ingress]# kubectl logs -f --tail 20 ingress-nginx-controller-9c6994c4d-72vgh -n ingress-nginx
...
82.156.79.118 - - [11/Oct/2024:07:43:23 +0000] "GET /foo HTTP/1.1" 200 13 "-" "curl/7.29.0" 83 0.015 [default-static-blog-80] [] 10.244.13.41:80 13 0.015 200 66c489c83d00400e7c7aef7cbcbc9fd9
82.156.79.118 - - [11/Oct/2024:07:43:25 +0000] "GET /bar HTTP/1.1" 200 13 "-" "curl/7.29.0" 83 0.001 [default-test-nodeport-80] [] 10.244.9.110:80 13 0.001 200 dbaa49564be02fdcee05b168f9ebd4c2

可以在ingress控制器的日志中看到,请求发送到了两个不同的服务

将不同的服务映射到不同的域名

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
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-blog
spec:
ingressClassName: nginx
rules:
- host: test.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: test-nodeport
port:
number: 80 #对test.example.com的请求将转发到test-nodeport这个服务上
- host: test1.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: static-blog
port:
number: 80 #对test1.example.com的请求将转发到static-blog这个服务上

根据请求中的host头,控制器收到的请求将转发到这两个不同的服务,DNS需要将这两个域名都指向ingress控制器的ip。

1
2
[11/Oct/2024:07:24:33 +0000] "GET / HTTP/1.1" 200 33547 "-" "curl/7.29.0" 81 0.000 [default-static-blog-80] [] 10.244.9.110:80 33547 0.001 200 699c9be1dc65a057f0ab775413b1cd3d
[11/Oct/2024:07:24:36 +0000] "GET / HTTP/1.1" 200 33547 "-" "curl/7.29.0" 80 0.001 [default-test-nodeport-80] [] 10.244.9.110:80 33547 0.001 200 57e3e285368edb0fe449a34163d7a04f