k8s服务是什么?
在了解本块内容之前,你需要对k8s由一个前期的了解包括pod/标签/副本集
不了解的同学可以参考前文:
k8s 实战 1 ---- 初识 (https://www.cnblogs.com/jilodream/p/18245222)
k8s 实战 2 ---- pod 基础 (https://www.cnblogs.com/jilodream/p/18284282)
k8s 实战 3 ---- 标签(https://www.cnblogs.com/jilodream/p/18293278 )
k8s 实战 4----副本集 ReplicaSet(RS)https://www.cnblogs.com/jilodream/p/18597501
我们先不要管它的定义,先说个比较实际的问题,假设我们有一堆的微服务,都是用pod或者副本集rs来启动起来的。这些微服务一般来说是呈网状的调用关系。此时如果微服务A想要调用微服务B,可以怎么办呢?
最简单的办法就是通过pod 分配的ip,直接请求就OK了,但是这就会有一个问题:b服务有好几个实例,怎么调用呢?
很容易就想到如果用spring cloud的话,直接用服务注册和发现,在配合feign之类的客户端,直接请求。这是一个很好的解决办法,但是它存在两个问题:
(1)这必须要求所有的服务都有能力进行服务注册发现,并负载均衡的调用下游。一方面是语言的问题,还有一方面是生态的问题,1这些所有的微服务是否都支持这个服务中心,它们是否都有feign之类的客户端,没有的话,是不是要自己复刻能力。这要是纯手写,先不论工作量怎么样,单就代码质量是否可以在短期内完成商用级别都成问题。
(2)服务无法在逻辑层面统一,比如A服务有实例 a1,a2,a3.此时新开发了一套B服务 B1,B2,B3,假设它们都有发送短信的能力,你现在要随机发送到 A服务的接口中,也可以发送到B服务的接口中,你怎么做呢?原有的方案就很难支持这种动态的随意划分了。
k8s 想到了一个办法:
每个pod 都有标签,我根据标签进行划分,每划分出来一类集合,我给它们创建一个资源作为请求入口,直接通过访问这个资源就可以轻松到访问到对应微服务的接口。
而这些微服务完全没必要是相同的镜像实例,它们只要是符合标签的匹配规则即可。(防盗连接:本文首发自http://www.cnblogs.com/jilodream/ )这就是微服务,换句话说它是一个抽象的pod集合。一个pod可以同时属于多个微服务。
举个例子
A 服务实例:a1,a2,a3 标签是app=asvc, message=true, v=va ,company=yidong
B 服务实例:b1,b2,b3 标签是app=bsvc, message=true,voice=true, v=vb ,company=liantong
C 服务实例:c1,c2,c3 标签是app=csvc, send=voice,voice=true, v=vc ,company=dianxin
我创建两个k8s服务资源
msg-svc, 标签过滤规则是 message=true
voice-svc 标签过滤规则是 voice=true
这样当其它服务需要发送短信时,就向msg-svc 这个服务直接发送请求,
需要发送语言时,就向voice-svc 这个服务直接发送请求。
这样你可以通过标签规则,随意的划拨出一个你需要的服务资源的范围。然后外部统一请求这个入口即可。当然前提是你这个范围内的pod在此处功能点上都有统一的api规范。另外你还也完全屏蔽了由于pod ip地址不断变化,导致的无法动态的请求pod的问题,当然原有的spring cloud服务发现机制本来也解决了这个问题。
以上就是k8s中service资源的介绍,我对它的理解就是通过标签筛选出符合条件的一个容器集合。然后有一个统一的入口,当请求进入入口后,可以通过kube-proxy,将流量转发给具体的pod容器。
下边我们来具体说说k8s service的使用:
k8s是通过使用场景的不同,来划分出不同类型的service的,
(1)clusterIp
它是k8s默认的服务类型,也是最常用的服务类型。基本可以解决绝大多数的使用场景。
定义yaml文件如下:
1 apiVersion: v1
2 kind: Service
3 metadata:
4 name: msg-svc
5 namespace: ${ACTIVE_ENV} //看具体情况,可以不填
6 spec:
7 type: ClusterIP //注意此处可以不填,默认就是clusterip类型的服务
8 ports:
9 - port: 9090
10 protocol: TCP
11 targetPort: 9080
12 selector:
13 message: "true"
保存好文件之后,比如命名为msgsvc.yaml
然后通过 kubectl apply -f msgsvc.yaml 命令 ,(防盗连接:本文首发自http://www.cnblogs.com/jilodream/ )就可以创建一个对应的服务了。
1 # cat nginx-clusterIp-svc.yaml
2 apiVersion: v1
3 kind: Service
4 metadata:
5 name: ng-clusterip-svc
6 spec:
7 type: ClusterIP
8 ports:
9 - port: 9090
10 protocol: TCP
11 targetPort: 9080
12 selector:
13 message: true
14 # kubectl apply -f nginx-clusterIp-svc.yaml
15 service/ng-clusterip-svc created
我们可以用如下命令查询到这个服务:
kubectl get service
1 # kubectl get svc
2 NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
3 kubernetes ClusterIP 10.232.0.1 <none> 443/TCP 234d
4 ng-clusterip-svc ClusterIP 10.232.22.12 <none> 9090/TCP 60s
注意service 作为一层实实在在的资源,是有映射能力的,因此yaml文件中
ports.port 表示的就是service的端口
ports.targetPort 表示的就是service映射到pod后,所对应的pod端口
我们现在有两种请求clusterip service 的办法,
1 通过服务名称
如:
curl ng-clusterip-svc.命名空间:9090 //同命名空间下可以忽略命名空间
2 通过分配的CLUSTER-IP (虚拟ip)
curl 10.232.22.12:9090
这两种方式无论是pod内还是pod 外,只要是在集群中,就可以直接请求,满足了大部分的业务调用场景
(2)NodePort
ClusterIP解决了集群内部请求服务的问题,但是有时候我们需要在集群外部访问这个集群中的某个服务,就需要使用NodePort的方式了。
创建办法如下:
1 # cat nginx-nodeport-svc.yaml
2 apiVersion: v1
3 kind: Service
4 metadata:
5 name: ng-nodeport-svc
6 spec:
7 type: NodePort
8 ports:
9 - port: 9090
10 protocol: TCP
11 targetPort: 9080
12 nodePort: 31010
13 selector:
14 message: "true"
15 kubectl apply -f nginx-nodeport-svc.yaml
16 service/ng-nodeport-svc created
查询service
1 # kubectl get service
2 NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
3 kubernetes ClusterIP 10.232.0.1 <none> 443/TCP 234d
4 ng-clusterip-svc ClusterIP 10.232.33.193 <none> 9090/TCP 4s
5 ng-nodeport-svc NodePort 10.232.1.76 <none> 9090:31010/TCP 14s
我们发现ng-nodeport-svc的yaml其实和clusterip 的创建方式非常像,只有两点区别,类型为NodePort ,端口多了一个ports.nodePort ,这个nodeport 的取值范围是30000 ~ 32767。如果不写的话,会随机分配一个没被占用的端口。
NodePort 的访问方式有两种
1 使用clusterIp的方式
NodePort底层包含clusterIp,所以可以直接用请求clusterIp的方式来请求nodeport,注意端口填port,并且也仅限在集群内部请求。
2 通过NodePort端口
客户端可以直接通过"k8s集群节点:NodePort"
集群节点是集群内的任意节点可以是master也可以是worker Node节点,端口就填yaml中配置的NodePort即可。
比如集群master节点是 36.1.5.1,那么无论集(防盗连接:本文首发自http://www.cnblogs.com/jilodream/ )群内部还是外部都可以通过 curl 36.1.5.1:NodePort 的方式请求成功。
因此nodePort对于不同服务来说是会互相冲突的,因此创建nodeport服务的数量也是有限的,这是nodeport的一个缺点。
一般来说其实nodeport的使用情况很少,一方面是调配端口比较麻烦,很容易冲突,另外一方面是不安全不好管理,生产情况下不愿意暴露这么多的端口出去,引起外部的攻击。一般是在测试环境测试接口用。
(3)Load Balancer
nodeport service 通过节点路由,解决了集群外部请求的问题。这样基本够用了。但是实际生产中,我们往往不使用nodeport ,而是使用Load Balancer。它是基于nodeport 的更高一级的封装,通过使用云服务厂商自带的负载均衡器,以及公网地址,允许用户通过调用固定公网地址,直接请求到内部的nodeport上。
这样做有几个好处:1,统一入口,公网地址不随意调整,不会随着集群的节点扩缩容而不断的调整客户端的请求配置。2,通过负载均衡器,调整不同的节点压力,同时还无需暴露所有节点的ip 端口,性能和安全性全部拉满。3,ssl卸载,所谓ssl卸载是指https协议的请求,在请求到服务端之后需要解密,而这个过程对于高并发下,是个很耗费性能的点,此时可以通过将ssl卸载的逻辑移植到厂商的负载均衡器上,让进入服务的请求不再解密从而降低服务端的压力。4,会话保持,通过负载均衡器的请求,可以进行七层负载均衡,其中就包括cookie header,这样尽可能的将相同用户的请求打到同一个后端pod或某几个pod上,让用户信息尽可能的隔离(防盗连接:本文首发自http://www.cnblogs.com/jilodream/ ),比如vip用户优先渠道,多租户隔离等。
但这些能力有些需要ingress资源(k8s的又一种资源)配合LoadBalancer 的负载均衡器共同完成。如果是在私有云或者内建集群上,也想要体现LB所带来的能力,也可以自建一些开源的负载均衡器来操作入MetalLB,OpenELB,这就说的太远了,不说了。有兴趣的同学可以自己研究下。
下边我们来看下实操,创建lb service:
1 # cat nginx-lb-svc.yaml
2 apiVersion: v1
3 kind: Service
4 metadata:
5 name: ng-lb-svc
6 spec:
7 type: LoadBalancer
8 ports:
9 - port: 9090
10 protocol: TCP
11 targetPort: 9080
12 nodePort: 31011
13 selector:
14 message: "true"
由于上一个例子使用了nodePort端口 31010,因此我们这里使用31011,否则会报端口占用的错误。
1 # kubectl apply -f nginx-lb-svc.yaml
2 service/ng-lb-svc created
3 # kubectl get service
4 NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
5 kubernetes ClusterIP 10.232.0.1 <none> 443/TCP 238d
6 ng-clusterip-svc ClusterIP 10.232.33.193 <none> 9090/TCP 3d21h
7 ng-lb-svc LoadBalancer 10.232.16.65 <pending> 9090:31011/TCP 7s
这样就创建好LB service了,但是注意看EXTERNAL-IP列 ,外部ip 显示是<pending>,这是由于我是在自建集群中操作的。k8s拿不到外部的ip,
假设EXTERNAL-IP 的值是10.12.123.124。
那么就可通过使用 EXTERNAL-IP:PORT 的方式请求服务,如curl 10.12.123.124:9090
(4) headless
headless 又被称之为无头服务。
一般来说服务之间,外部与集群之间,不同集群之间,通过以上3种服务基本就可以满足服务的正常使用了,但是有一种情况却不行,那就是有状态服务。啥是有状态服务呢?简单来说就是服务的多个实例之间(也可以是单个)是有身份区别的,比如mysql 的主从实例 或者服务的内部是有状态差异的,不能每次请求都可以随意请求,比如我在A实例创建订单,将订单状态放到实例内存中,后边更新订单状态时,只能请求到这个订单上。当然后者在微服务场景是不推荐的一种设计。
来看下如何使用无头服务:
先创建无头服务:
1 # cat myapp-headless-svc.yaml
2 apiVersion: v1
3 kind: Service
4 metadata:
5 name: myapp-headless-svc # 服务的名字
6 spec:
7 clusterIP: None # 关键:不分配虚拟 IP,直接暴露 Pod
8 selector:
9 app1: myapp1 # 关联到下面 StatefulSet 的 Pod
10 ports:
11 - port: 4321
12 name: myapp
13
14 # kubectl apply -f myapp-headless-svc.yaml
15 service/myapp-headless-svc created
然后需要创建状态集,它和副本集很像,区别是它所对应的pod是按照一定顺序来命名和创建的(后边介绍statful时,会详细说)
1 # cat myapp-statefulSet.yaml
2 apiVersion: apps/v1
3 kind: StatefulSet
4 metadata:
5 name: myapp-sts
6 spec:
7 serviceName: "myapp-headless-svc" # 关键:必须与 Headless Service 的名字一致!
8 replicas: 3 # 创建 3 个 Pod
9 selector:
10 matchLabels:
11 app1: myapp1
12 template:
13 metadata:
14 labels:
15 app1: myapp1
16 spec:
17 containers:
18 - name: mysql
19 image: registry.cmjt.com:8443/ai_znm/llm-response-api-service:20260805144832
20 ports:
21 - containerPort: 4321
22
23 # kubectl apply -f myapp-statefulSet.yaml
24 statefulset.apps/myapp-sts created
25
26 # kubectl get statefulset
27 NAME READY AGE
28 myapp-sts 3/3 14m
29
30 # kubectl get pod
31 NAME READY STATUS RESTARTS AGE
32 myapp-sts-0 1/1 Running 7 (3m52s ago) 15m
33 myapp-sts-1 1/1 Running 7 (3m52s ago) 15m
34 myapp-sts-2 1/1 Running 7 (3m46s ago) 15m
注意看3个pod的名字都是按照statefulset名称加需要标识好的,也就是说pod名称是固定的,
这样我们就可以将myapp-sts-0 定为主节点,myapp-sts-1 ,myapp-sts-2 定为从节点。
然后根据pod 名称访问不同的节点,如:
当需要访问主节点的时候就
curl myapp-sts-0:4321
访问从节点时
curl myapp-sts-1:4321
curl myapp-sts-2:4321
这样我们在集群中,就可以通过主从节点的名称,定义好它们的数据同步策略等类似的场景。
(5)别名服务
别名服务其实很简单,就是一层对外部域名的映射。
如公司有一套 user.com 的员工系统。
我们直接请求访问就好了,但是后边员工系统升级了,(防盗连接:本文首发自http://www.cnblogs.com/jilodream/ )为了体现区别,就叫做newuser.com,后边再来一次升级,就叫做2026user.com吧。
每次升级我们都要改代码,不改代码也可以,还可以使用配置中心,但是配置中心改起来也很麻烦,那么就还可以直接定义一个服务层(别名服务),然后直接请求到对应真实服务就好,我们每次修改只要修改别名服务的转发配置即可。
就像这样 curl 别名服务名:端口
举个例子
假设有这样一个服务,在其它的命名空间:
1 # kubectl get service -n test
2 NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
3 llm-api-service-svc NodePort 10.233.44.18 <none> 9043:32100/TCP 65d
4
5 # cat myapi-external-svc.yaml
6 apiVersion: v1
7 kind: Service
8 metadata:
9 name: myapi-svc
10 spec:
11 type: ExternalName
12 externalName: llm-api-service-svc.test.svc.cluster.local # 外部真实的域名
13 ports:
14 - port: 9043
15
16 # kubectl apply -f myapi-external-svc.yaml
17 service/myapi-svc configured
18
19 # kubectl get service
20 NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
21 myapi-svc ExternalName <none> llm-api-service-svc.test.svc.cluster.local 9043/TCP
然后我们进一个同命名空间下的pod ,发起curl 请求:
1 kubectl exec -it nacos-0 -- /bin/sh
2 /home/nacos # curl myapi-svc:9043
3 <!doctype html><html lang="en"><head><title>HTTP Status 404 -- Not Found</title>...
这样就可以请求通了。
在此,要注意的是:
按照标准的 服务名称请求的话,标准的url是:
服务名称.命名空间.搜索域:端口号
命名空间 每个资源所在的空间,默认是default 空间
搜索域是 svc.cluster.local
一般来说 搜索域可以直接忽略,直接请求:
服务名称.命名空间:端口号 即可
如果请求容器和服务是相同命名空间的话,则命名空间也可以省略直接请求:
服务名称:端口号 即可。
刚才的别名服务中的目标服务地址(红色加粗部分),需要写完整的服务地址,(防盗连接:本文首发自http://www.cnblogs.com/jilodream/ )这是由于别名服务的定义中,如果是目标服务就是本命名空间,则直接写服务名称即可,而如果是跨命名空间,则需要在指定命名空间的同时,还要指定搜索域。否则容器在使用时,会无法解析到目标地址。