3. k8s Service

0. 简介

前面我们说过,Pod 是临时对象,所以一般而言我们使用 Deployment来动态地创建和销毁 Pod,以匹配集群的预期状态,从而实现 Pod 管理。每个 Pod 都有属于自己的IP地址,对于集群中的 Deployment 来说,此时刻的 Pod 集合和彼时刻的 Pod 即可可能并不相同,对于想要稳定访问 IP 地址的需求,该如何满足呢?

为解决上述问题,k8s 提供了 Service,一个固定的接入层,客户端可以通过其 IP 和 PORT 访问到其关联的后端 Pod,其是将运行在一个或一组 Pod 上的网络应用程序公开为网络服务的方法。

1. Service 类型

k8s Service 允许指定所需要的类型。

ClusterIP :通过集群的内部 IP 公开 Service,选择该值时 Service 只能够在集群内部访问 。 这也是你没有为服务显式指定 type 时使用的默认值。 你可以使用 Ingress 或者 Gateway API 向公共互联网公开服务。

NodePort :通过每个节点上的 IP 和静态端口(NodePort)公开 Service。 为了让 Service 可通过节点端口访问,Kubernetes 会为 Service 配置集群 IP 地址, 相当于你请求了 type: ClusterIP 的服务。

LoadBalancer:使用云平台的负载均衡器向外部公开 Service。Kubernetes 不直接提供负载均衡组件; 你必须提供一个,或者将你的 Kubernetes 集群与某个云平台集成。

ExternalName :将服务映射到 externalName 字段的内容(例如,映射到主机名 api.foo.bar.example)。 该映射将集群的 DNS 服务器配置为返回具有该外部主机名值的 CNAME 记录。 集群不会为之创建任何类型代理。

服务 API 中的 type 字段被设计为层层递进的形式 - 每层都建立在前一层的基础上。 但是,这种层层递进的形式有一个例外。 你可以在定义 LoadBalancer 服务时禁止负载均衡器分配 NodePort

2. Service 实践

2.1 kube-proxy 在 k8s Service 中的工作原理

参考浅谈Kubernetes Service负载均衡实现机制,我们知道:

1. 运行在每个Node节点的kube-proxy会实时的watch Services和 Endpoints对象。

当用户在kubernetes集群中创建了含有label的Service之后,同时会在集群中创建出一个同名的Endpoints对象,用于存储该Service下的Pod IP.

2. 每个运行在Node节点的kube-proxy感知到Services和Endpoints的变化之后,会在各自的Node节点设置相关的iptables或IPVS规则,用于之后用户通过Service的ClusterIP去访问该Service下的服务。

比如在我们之前设置的环境中,configmaps中的 kube-proxy 配置的就是 iptables

bash 复制代码
 ~ kubectl get cm kube-proxy -n kube-system -o yaml | grep mode
    mode: iptables

3. 当kube-proxy把需要的规则设置完成之后,用户便可以在集群内的Node或客户端Pod上通过ClusterIP经过iptables或IPVS设置的规则进行路由和转发,最终将客户端请求发送到真实的后端Pod。

至于详细原理,大家可以参考浅谈Kubernetes Service负载均衡实现机制

2.2 ClusterIP 中的负载均衡

接下来,我们验证一下 ClusterIP Service下的负载均衡。

2.2.1 实现在 k8s 上运行一个自己的服务

函数实现
go 复制代码
package main

import (
    "fmt"
    "log"
    "net/http"
    "os"
)

// w表示response对象,返回给客户端的内容都在对象里处理go
// r表示客户端请求对象,包含了请求头,请求参数等等
func index(w http.ResponseWriter, r *http.Request) {
    // 往w里写入内容,就会在浏览器里输出
    hostName, _ := os.Hostname()
    _, _ = fmt.Fprintf(w, fmt.Sprintf("hello, I am %s!\n", hostName))
}

func main() {
    // 设置路由,如果访问/,则调用index方法
    http.HandleFunc("/", index)

    // 启动web服务,监听9090端口
    err := http.ListenAndServe(":9090", nil)
    if err != nil {
       log.Fatal("ListenAndServe: ", err)
    }
}

我们实现以上的一个简单程序,在:9090端口起 http 服务,返回自身的节点名称。

Dockerfile
dockerfile 复制代码
FROM golang:1.18 as builder
WORKDIR /app
COPY . .
RUN GOOS=linux GOARCH=amd64 go build -o main .

FROM alpine:latest
WORKDIR /app
COPY --from=builder /app /app
ENTRYPOINT ["/app/main"]

然后将代码打包成镜像:

bash 复制代码
docker build --rm -t k8s_service .
k8s Deployment & Service
yaml 复制代码
kind: Deployment
apiVersion: apps/v1
metadata:
  name: service-clusterip
spec:
  selector:
    matchLabels:
      app: service-clusterip
  replicas: 2
  template:
    metadata:
      labels:
        app: service-clusterip
    spec:
      containers:
      - name: service-clusterip
        image: k8s_service
        imagePullPolicy: Never

---
apiVersion: v1
kind: Service
metadata:
  name: service-clusterip
spec:
  ports:
  - name: http
    port: 9090
    targetPort: 9090
  selector:
    app: service-clusterip

因为是希望使用本地打包的镜像,所以第 18 行 imagePullPolicy: Never 表示不会从远端拉取镜像,同时需要在本地往 kind 集群上推送镜像:

bash 复制代码
~ kind load docker-image k8s_service k8s_service --name multi

然后kubectl apply -f service_clusterip.yaml运行此命令即可。

2.2.2 验证负载均衡

bash 复制代码
~ kubectl get svc
NAME                TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
kubernetes          ClusterIP   10.96.0.1       <none>        443/TCP    7h44m
service-clusterip   ClusterIP   10.96.216.179   <none>        9090/TCP   5m32s

可以发现,我们实现了 TYPE = ClusterIP 的 Service,由于 ClusterIP 只能在集群内部访问,所以我们可以到容器环境中去验证。

bash 复制代码
~ kubectl get po
NAME                                READY   STATUS    RESTARTS   AGE
service-clusterip-788cb8d88-2tkfq   1/1     Running   0          87s
service-clusterip-788cb8d88-fkd8x   1/1     Running   0          87s

~ kubectl exec -it service-clusterip-788cb8d88-2tkfq /bin/sh
kubectl exec [POD] [COMMAND] is DEPRECATED and will be removed in a future version. Use kubectl exec [POD] -- [COMMAND] instead.

进入到容器后,可以执行以下命令:

bash 复制代码
/app ~ for i in $(seq 1 10)  ; do wget -q http://service-clusterip:9090 -O a.html && cat a.html; done
hello, I am service-clusterip-788cb8d88-fkd8x!
hello, I am service-clusterip-788cb8d88-fkd8x!
hello, I am service-clusterip-788cb8d88-fkd8x!
hello, I am service-clusterip-788cb8d88-2tkfq!
hello, I am service-clusterip-788cb8d88-2tkfq!
hello, I am service-clusterip-788cb8d88-2tkfq!
hello, I am service-clusterip-788cb8d88-fkd8x!
hello, I am service-clusterip-788cb8d88-fkd8x!
hello, I am service-clusterip-788cb8d88-2tkfq!
hello, I am service-clusterip-788cb8d88-2tkfq!

可以发现,Service 实现了对后端访问的负载均衡。其实,service的负载均衡只是针对于其selector而定的,并不一定限定于同一个Deployment,假设我们增加如下的配置:

yaml 复制代码
kind: Deployment
apiVersion: apps/v1
metadata:
  name: http-echo
spec:
  selector:
    matchLabels:
      app: service-clusterip
  replicas: 2
  template:
    metadata:
      labels:
        app: service-clusterip
    spec:
      containers:
      - name: http-echo
        image: hashicorp/http-echo:latest
        args:
        - "-listen=:9090"

然后重新执行以上命令,可以发现其也会随机打到其他 Deployment 的 Pod 服务上。

bash 复制代码
/app ~ for i in $(seq 1 10)  ; do wget -q http://service-clusterip:9090 -O a.h
tml && cat a.html; done
hello-world
hello, I am service-clusterip-788cb8d88-zt476!
hello-world
hello-world
hello, I am service-clusterip-788cb8d88-bwl8h!
hello, I am service-clusterip-788cb8d88-bwl8h!
hello-world
hello, I am service-clusterip-788cb8d88-bwl8h!
hello, I am service-clusterip-788cb8d88-bwl8h!
hello, I am service-clusterip-788cb8d88-zt476!

2.3 NodePort

NodePort 会在每个节点 上的 IP 和 静态端口(NodePort)公开服务,Kubernetes 控制平面将在 --service-node-port-range 标志所指定的范围内分配端口(默认值:30000-32767)。

如下,借助前面的程序,我们配置如下的 Deployment 和 Service:

yaml 复制代码
kind: Deployment
apiVersion: apps/v1
metadata:
  name: service-nodeport
spec:
  selector:
    matchLabels:
      app: service-nodeport
  replicas: 2
  template:
    metadata:
      labels:
        app: service-nodeport
    spec:
      containers:
      - name: service-nodeport
        image: k8s_service
        imagePullPolicy: Never
---
apiVersion: v1
kind: Service
metadata:
  name: service-nodeport
spec:
  type: NodePort
  ports:
  - name: http
    port: 9090
    targetPort: 9090
    nodePort: 30950
  selector:
    app: service-nodeport

这里需要注意的是,因为我们的 kind 环境并不是真正的多集群环境,所以需要通过配置 kind 的节点对外进行端口映射,比如这里使用的是 kind-multi 集群,其配置文件参考1. 使用kind搭建k8s集群中:

yaml 复制代码
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
  # port forward 80 on the host to 80 on this node
  extraPortMappings:
  - containerPort: 30950
    hostPort: 80
    # optional: set the bind address on the host
    # 0.0.0.0 is the current default
    listenAddress: "127.0.0.1"
    # optional: set the protocol to one of TCP, UDP, SCTP.
    # TCP is the default
    protocol: TCP
- role: worker
- role: worker
- role: worker

这里只在 control-plane 节点做了端口映射,感兴趣的可以试试在任何节点都能做端口映射,并且访问到服务。这时候在外部就可以访问到服务了,比如:

bash 复制代码
~ curl localhost
hello, I am service-nodeport-c5f8587dd-j8x4n!

2.3.1 和 hostPort 的区别

在k8s中,若pod使用主机网络 ,也就是hostNetwork=true。则该pod会使用主机的dns以及所有网络配置,这相当于 docker run --net=host。有关此,可以参考【云原生】k8s 中的 hostNetwork 和 NetworkPolicy(网络策略)讲解与实战操作聊聊k8s的hostport和NodePort

值得注意的是:

hostPort 只会在运行机器上开启端口, NodePort 是所有 Node 上都会开启端口。

2.3.2 Service 实现的是基于连接的负载均衡

在 Serice 中实现的负载均衡其实是在 OSI模型 的第四层,说白了就是基于连接的负载均衡,其对于长连接本质上是没有作用的(譬如基于 HTTP2 长连接的 gRPC),有以下例子:

go 复制代码
package main

import (
    "io"
    "net/http"
    "testing"
)

func TestGetService(t *testing.T) {
    c := &http.Client{}
    for i := 0; i < 10; i++ {
       resp, err := c.Get("http://localhost")
       if err != nil {
          panic(err)
       }
       b, _ := io.ReadAll(resp.Body)
       t.Logf(string(b))
    }
}

此时结果为:

bash 复制代码
~ go test -v -run TestGetService
=== RUN   TestGetService
    main_test.go:17: hello, I am service-nodeport-c5f8587dd-5wtxz!
    main_test.go:17: hello, I am service-nodeport-c5f8587dd-5wtxz!
    main_test.go:17: hello, I am service-nodeport-c5f8587dd-5wtxz!
    main_test.go:17: hello, I am service-nodeport-c5f8587dd-5wtxz!
    main_test.go:17: hello, I am service-nodeport-c5f8587dd-5wtxz!
    main_test.go:17: hello, I am service-nodeport-c5f8587dd-5wtxz!
    main_test.go:17: hello, I am service-nodeport-c5f8587dd-5wtxz!
    main_test.go:17: hello, I am service-nodeport-c5f8587dd-5wtxz!
    main_test.go:17: hello, I am service-nodeport-c5f8587dd-5wtxz!
    main_test.go:17: hello, I am service-nodeport-c5f8587dd-5wtxz!
--- PASS: TestGetService (0.01s)
PASS
ok  	k8s_service	0.019s

这是因为 go 客户端默认开启了长连接,这时候可以将客户端连接改为 c := &http.Client{Transport: &http.Transport{DisableKeepAlives: true}} ,即可得到:

bash 复制代码
~ go test -v -run TestGetService
=== RUN   TestGetService
    main_test.go:18: hello, I am service-nodeport-c5f8587dd-5wtxz!
    main_test.go:18: hello, I am service-nodeport-c5f8587dd-5wtxz!
    main_test.go:18: hello, I am service-nodeport-c5f8587dd-j8x4n!
    main_test.go:18: hello, I am service-nodeport-c5f8587dd-5wtxz!
    main_test.go:18: hello, I am service-nodeport-c5f8587dd-j8x4n!
    main_test.go:18: hello, I am service-nodeport-c5f8587dd-5wtxz!
    main_test.go:18: hello, I am service-nodeport-c5f8587dd-j8x4n!
    main_test.go:18: hello, I am service-nodeport-c5f8587dd-j8x4n!
    main_test.go:18: hello, I am service-nodeport-c5f8587dd-j8x4n!
    main_test.go:18: hello, I am service-nodeport-c5f8587dd-j8x4n!
--- PASS: TestGetService (0.03s)
PASS
ok  	k8s_service	0.039s

3. 小结

总体而言,Service 提供了 OSI模型 第四层的负载均衡,并依次提供了层层递进的服务类型。至于 LoadBalancer 和 ExternalName 需要外部支持,这里我们就不详细介绍了。

相关推荐
ggaofeng3 小时前
通过命令学习k8s
云原生·容器·kubernetes
qq_道可道6 小时前
K8S升级到1.24后,切换运行时导致 dind 构建镜像慢根因定位与解决
云原生·容器·kubernetes
SONGW20186 小时前
k8s拓扑域 :topologyKey
kubernetes
weixin_438197388 小时前
K8S实现反向代理,负载均衡
linux·运维·服务器·nginx·kubernetes
华为云开发者联盟13 小时前
解读Karmada多云容器编排技术,加速分布式云原生应用升级
kubernetes·集群·karmada·多云容器
严格要求自己1 天前
nacos-operator在k8s集群上部署nacos-server2.4.3版本踩坑实录
云原生·容器·kubernetes
少吃一口就会少吃一口1 天前
k8s笔记
云原生·容器·kubernetes
葡萄皮Apple1 天前
[CKS] K8S ServiceAccount Set Up
服务器·容器·kubernetes
2301_806131361 天前
Kubernetes 核心组件调度器(Scheduler)
云原生·容器·kubernetes
放手啊2 天前
sealos部署K8s,安装docker时master节点突然NotReady
docker·容器·kubernetes