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 需要外部支持,这里我们就不详细介绍了。