背景

在做传统业务开发的时候,当我们的服务提供方有多个实例时,往往我们需要将对方的服务列表保存在本地,然后采用一定的算法进行调用;当服务提供方的列表变化时还得及时通知调用方。

student:     url:        - 192.168.1.1:8081        - 192.168.1.2:8081

这样自然是对双方都带来不少的负担,所以后续推出的服务调用框架都会想办法解决这个问题。

以spring cloud为例:


(相关资料图)

图片

服务提供方会向一个服务注册中心注册自己的服务(名称、IP等信息),客户端每次调用的时候会向服务注册中心获取一个节点信息,然后发起调用。

但当我们切换到k8s后,这些基础设施都交给了k8s处理了,所以k8s自然得有一个组件来解决服务注册和调用的问题。

也就是我们今天重点介绍的service。

service

在介绍service之前我先调整了源码:

func main() {     http.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {        name, _ := os.Hostname()        log.Printf("%s ping", name)        fmt.Fprint(w, "pong")     })     http.HandleFunc("/service", func(w http.ResponseWriter, r *http.Request) {        resp, err := http.Get("http://k8s-combat-service:8081/ping")        if err != nil {           log.Println(err)           fmt.Fprint(w, err)           return        }        fmt.Fprint(w, resp.Status)     })       http.ListenAndServe(":8081", nil)  }

新增了一个/service的接口,这个接口会通过 service 的方式调用服务提供者的服务,然后重新打包。

make docker

同时也新增了一个deployment-service.yaml:

apiVersion: apps/v1  kind: Deployment  metadata:    labels:      app: k8s-combat-service # 通过标签选择关联    name: k8s-combat-service  spec:    replicas: 1    selector:      matchLabels:        app: k8s-combat-service    template:      metadata:        labels:          app: k8s-combat-service      spec:        containers:          - name: k8s-combat-service            image: crossoverjie/k8s-combat:v1            imagePullPolicy: Always            resources:              limits:                cpu: "1"                memory: 100Mi              requests:                cpu: "0.1"                memory: 10Mi  ---  apiVersion: v1  kind: Service  metadata:    name: k8s-combat-service  spec:    selector:      app: k8s-combat-service # 通过标签选择关联    type: ClusterIP    ports:      - port: 8081        # 本 Service 的端口        targetPort: 8081  # 容器端口        name: app

使用相同的镜像部署一个新的 deployment,名称为k8s-combat-service,重点是新增了一个kind: Service的对象。

这个就是用于声明service的组件,在这个组件中也是使用selector标签和deployment进行了关联。

也就是说这个service用于服务于名称等于k8s-combat-service的deployment。

下面的两个端口也很好理解,一个是代理的端口, 另一个是 service 自身提供出去的端口。

至于type: ClusterIP是用于声明不同类型的service,除此之外的类型还有:

NodePortLoadBalancerExternalName等类型,默认是ClusterIP,现在不用纠结这几种类型的作用,后续我们在讲到Ingress的时候会具体介绍。负载测试

我们先分别将这两个deployment部署好:

k apply -f deployment/deployment.yamlk apply -f deployment/deployment-service.yaml❯ k get podNAME                                  READY   STATUS    RESTARTS   AGEk8s-combat-7867bfb596-67p5m           1/1     Running   0          3h22mk8s-combat-service-5b77f59bf7-zpqwt   1/1     Running   0          3h22m

由于我新增了一个/service的接口,用于在k8s-combat中通过service调用k8s-combat-service的接口。

resp, err := http.Get("http://k8s-combat-service:8081/ping")

其中k8s-combat-service服务的域名就是他的服务名称。

如果是跨 namespace 调用时,需要指定一个完整名称,在后续的章节会演示。

我们整个的调用流程如下:

图片

相信大家也看得出来相对于spring cloud这类微服务框架提供的客户端负载方式,service是一种服务端负载,有点类似于Nginx的反向代理。

为了更直观的验证这个流程,此时我将k8s-combat-service的副本数增加到 2:

spec:    replicas: 2

只需要再次执行:

❯ k apply -f deployment/deployment-service.yamldeployment.apps/k8s-combat-service configuredservice/k8s-combat-service unchanged

图片

image.png

不管我们对deployment的做了什么变更,都只需要apply这个yaml文件即可, k8s 会自动将当前的deployment调整为我们预期的状态(比如这里的副本数量增加为 2);这也就是k8s中常说的声明式 API。

可以看到此时k8s-combat-service的副本数已经变为两个了。如果我们此时查看这个service的描述时:

❯ k describe svc k8s-combat-service |grep EndpointsEndpoints:         192.168.130.133:8081,192.168.130.29:8081

会发现它已经代理了这两个Pod的 IP。

图片

此时我进入了k8s-combat-7867bfb596-67p5m的容器:

k exec -it k8s-combat-7867bfb596-67p5m bashcurl http://127.0.0.1:8081/service

并执行两次/service接口,发现请求会轮训进入k8s-combat-service的代理的 IP 中。

由于k8s service是基于TCP/UDP的四层负载,所以在http1.1中是可以做到请求级的负载均衡,但如果是类似于gRPC这类长链接就无法做到请求级的负载均衡。

换句话说service只支持连接级别的负载。

如果要支持gRPC,就得使用 Istio 这类服务网格,相关内容会在后续章节详解。

总结

总的来说k8s service提供了简易的服务注册发现和负载均衡功能,当我们只提供 http 服务时是完全够用的。

推荐内容