五.k8s四层负载均衡-Service

5.1-什么是Service

5.1.1-Service作用

在 kubernetes 中,Pod 是有生命周期的,如果 Pod 重启它的 IP 很有可能会发生变化。如果我们的服 务都是将 Pod 的 IP 地址写死,Pod 挂掉或者重启,和刚才重启的 pod 相关联的其他服务将会找不到它所 关联的 Pod,为了解决这个问题,在 kubernetes 中定义了 service 资源对象,Service 定义了一个服务 访问的入口,客户端通过这个入口即可访问服务背后的应用集群实例,service 是一组 Pod 的逻辑集合, 这一组 Pod 能够被 Service 访问到,通常是通过 Label Selector 实现的。

k8s keepalived 负载均衡 其他工作节点换了 kubectl使用不了 k8s service 负载均衡策略probability_负载均衡

5.1.2-Service概念

service 是一个固定接入层,客户端可以通过访问 service 的 ip 和端口访问到 service 关联的后端pod,这个 service 工作依赖于在 kubernetes 集群之上部署的一个附件,就是kubernetes 的 dns 服务(不同 kubernetes 版本的 dns 默认使用的也是不一样的,1.11 之前的版本使用的是 kubeDNs,较新的版本使用的是 coredns),service 的名称解析是依赖于 dns 附件的,因此在部署完 k8s 之后需要再部署 dns附件,kubernetes 要想给客户端提供网络功能,需要依赖第三方的网络插件(flannel,calico 等)。每个 K8s 节点上都有一个组件叫做 kube-proxy,kube-proxy 这个组件将始终监视着 apiserver 中有关service 资源的变动信息,需要跟 master 之上的 apiserver 交互,随时连接到 apiserver 上获取任何一个与 service 资源相关的资源变动状态,这种是通过 kubernetes 中固有的一种请求方法 watch(监视)来实现的,一旦有 service 资源的内容发生变动(如创建,删除),kube-proxy 都会将它转化成当前节点之上的能够实现 service 资源调度,把我们请求调度到后端特定的 pod 资源之上的规则,这个规则可能是 iptables,也可能是 ipvs,取决于 service 的实现方式。

5.1.3-Service工作原理

k8s 在创建 Service 时,会根据标签选择器 selector(lable selector)来查找 Pod,据此创建与 Service 同名的 endpoint 对象,当 Pod 地址发生变化时,endpoint 也会随之发生变化,service 接收前 端 client 请求的时候,就会通过 endpoint,找到转发到哪个 Pod 进行访问的地址。(至于转发到哪个节 点的 Pod,由负载均衡 kube-proxy 决定)

k8s中有三类网络地址

1.Node network :宿主机网络

2.Pod network :pod网络,创建的pod具有的IP

#node和pod网络是物理存在的,节点网络地址配置在节点接口至上,pod网络地址配置在pod资源至上。/硬件,软件模拟

3.Cluster Network:集群地址,虚拟的地址,没有接口,只是在service规则中

service 只要创建完成,我们就可以直接解析它的服务名,每一个服务创建完成后都会在集群 dns 中 动态添加一个资源记录,添加完成后我们就可以解析了,资源记录格式是:

SVC_NAME.NS_NAME.DOMAIN.LTD.

服务名.命名空间.域名后缀

集群默认的域名后缀是 svc.cluster.local.

如:在web 命名空间创建一个nginx-web服务的解析为

nginx-web.web.svc.cluster.local

5.2-Service 代理:kube-proxy 组件详解

5.2.1-kube-proxy 组件介绍

service 只是把应用对外提供服务的方式做了抽象,真正的应用跑在 Pod 中的container 里,我们的请求转到 nodes 对应的 nodePort 上,就是通过 kube-proxy 实现的

kube-proxy 部署在 k8s 的每一个 Node 节点上,是 Kubernetes 的核心组件,我们创建一个 service 的时候,kube-proxy 会在 iptables 中追加一些规则,为我们实现路由与负载均衡的功能。

在 k8s1.8 之前,kube-proxy 默认使用的是 iptables 模式,通过各个 node 节点上的iptables 规则来实现 service 的负载均衡,但是随着 service 数量的增大,iptables 模由于线性查找匹配、全量更新等特点,其性能会显著下降。

从 k8s 的 1.8 版本开始,kube-proxy 引入了 IPVS 模式,IPVS 模式与 iptables 同样基于Netfilter,但是采用的 hash表,因此当 service 数量达到一定规模时,hash 查表的速度优势就会显现出来,从而提高 service 的服务性能。

service 是一组 pod 的服务抽象,相当于一组 pod 的 LB,负责将请求分发给对应的 pod。

service 会 为这个 LB 提供一个 IP,一般称为 cluster IP。kube-proxy 的作用主要是负责 service 的实现,具体来 说,就是实现了内部从 pod 到 service 和外部的从 node port 向 service 的访问。

1、kube-proxy 其实就是管理 service 的访问入口,包括集群内 Pod 到 Service 的访问和集群外访问
service。
2、kube-proxy 管理 sevice 的 Endpoints,该 service 对外暴露一个 Virtual IP,也可以称为是Cluster IP, 集群内通过访问这个 Cluster IP:Port 就能访问到集群内对应的 serivce 下的 Pod。

5.2.2-Kube-proxy三种工作模式

1.Userspace方式:

k8s keepalived 负载均衡 其他工作节点换了 kubectl使用不了 k8s service 负载均衡策略probability_IP_02

Client Pod 要访问 Server Pod 时,它先将请求发给内核空间中的 service iptables 规则,由它再 将请求转给监听在指定套接字上的 kube-proxy 的端口,kube-proxy 处理完请求,并分发请求到指定Server Pod 后,再将请求转发给内核空间中的 service ip,由 service iptables 将请求转给各个节点中 的 Server Pod。

该模式的问题:客户端请求先进入内核空间的,又进去用户空间访问 kube-proxy,由 kube-proxy 封装完成后再进去内核空间的 iptables,再根据 iptables 的规则分发给各节点的用户空间 的 pod。由于其需要来回在用户空间和内核空间交互通信,因此效率很差。在 Kubernetes 1.1 版本之 前,userspace 是默认的代理模型。

2.iptables方式:

k8s keepalived 负载均衡 其他工作节点换了 kubectl使用不了 k8s service 负载均衡策略probability_IP_03

客户端 IP 请求时,直接请求本地内核 service ip,根据 iptables 的规则直接将请求转发到到各 pod 上,因为使用 iptable NAT 来完成转发,也存在不可忽视的性能损耗。另外,如果集群中存上万的 Service/Endpoint,那么 Node 上的 iptables rules 将会非常庞大,性能还会再打折。1.2为默认模式

3.ipvs方式:

k8s keepalived 负载均衡 其他工作节点换了 kubectl使用不了 k8s service 负载均衡策略probability_kubernetes_04

客户端请求时到达内核空间时,根据 ipvs 的规则直接分发到各 pod 上。kube-proxy 会监视 Kubernetes Service 对象和 Endpoints,调用 netlink 接口以相应地创建 ipvs 规则并定期与 Kubernetes Service 对 象和 Endpoints 对象同步 ipvs 规则,以确保 ipvs 状态与期望一致。访问服务时,流量将被重定向到其 中一个后端 Pod。与 iptables 类似,ipvs 基于 netfilter 的 hook 功能,但使用哈希表作为底层数据结 构并在内核空间中工作。这意味着 ipvs 可以更快地重定向流量,并且在同步代理规则时具有更好的性能.1.11默认

ipvs 为负载均衡算法提供了更多选项:rr:轮询,lc:最小链接数,dh:目标哈希,sh:源哈希,sed:最短期望延迟,nq:不排队调度.

如果某个服务后端 pod 发生变化,标签选择器适应的 pod 又多一个,适应的信息会立即反映到apiserver 上,而 kube-proxy 一定可以 watch 到 etc 中的信息变化,而将它立即转为 ipvs 或者 iptables中的规则,这一切都是动态和实时的,删除一个 pod 也是同样的原理。如图:

k8s keepalived 负载均衡 其他工作节点换了 kubectl使用不了 k8s service 负载均衡策略probability_负载均衡_05

以上不论哪种,kube-proxy 都通过 watch 的方式监控着 apiserver 写入 etcd 中关于 Pod 的最新状 态信息,它一旦检查到一个 Pod 资源被删除了或新建了,它将立即将这些变化,反应再 iptables 或 ipvs 规则中,以便 iptables 和 ipvs 在调度 Clinet Pod 请求到 Server Pod 时,不会出现 Server Pod 不存在 的情况.k8s1.11 以后,service 默认使用 ipvs 规则,若 ipvs 没有被激活,则降级使用 iptables 规 则.

5.2.3-kube-proxy 生成的 iptables 规则分析

在 k8s 创建的 service,虽然有 ip 地址,但是 service 的 ip 是虚拟的,不存在物理机上的,是在 iptables 或者 ipvs 规则里的。

1.service的type是clusterIP类型

[root@master2 sc]# kubectl get svc
NAME           TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)    AGE
aliyun-mysql   ClusterIP   10.104.231.127   <none>        3306/TCP   31m
kubernetes     ClusterIP   10.96.0.1        <none>        443/TCP    5d20h
[root@master2 sc]# iptables -t nat -L |grep 10.104.231.127
KUBE-SVC-3Z5SCIS6QZ4IIO7R  tcp  --  anywhere             10.104.231.127       /* default/aliyun-mysql cluster IP */ tcp dpt:mysql
KUBE-MARK-MASQ  tcp  -- !192.168.0.0/16       10.104.231.127       /* default/aliyun-mysql cluster IP */ tcp dpt:mysql

创建的 service,会通过 kube-proxy 在 iptables 中生成一个规则,来实现 流量路由,一系列目标为 KUBE-SVC-xxx 链的规则,每条规则都会匹配某个目标 ip 与端口。也就是说访问某个 ip:port 的请求会由 KUBE-SVC-xxx 链来处理。这个目标 IP 其实就是 service ip。

2.sercice的type是nodePort类型

[root@master2 sc]# kubectl get svc -n kubernetes-dashboard
NAME                        TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)         AGE
dashboard-metrics-scraper   ClusterIP   10.98.37.51     <none>        8000/TCP        3d23h
kubernetes-dashboard        NodePort    10.101.159.76   <none>        443:30443/TCP   3d23h
[root@master2 sc]# iptables -t nat -L | grep 10.101.159.76
KUBE-SVC-CEZPIJSAUFW5MYPQ  tcp  --  anywhere             10.101.159.76        /* kubernetes-dashboard/kubernetes-dashboard cluster IP */ tcp dpt:https
KUBE-MARK-MASQ  tcp  -- !192.168.0.0/16       10.101.159.76        /* kubernetes-dashboard/kubernetes-dashboard cluster IP */ tcp dpt:https
[root@master2 sc]# iptables -t nat -S | grep 30443
-A KUBE-NODEPORTS -p tcp -m comment --comment "kubernetes-dashboard/kubernetes-dashboard" -m tcp --dport 30443 -j KUBE-EXT-CEZPIJSAUFW5MYPQ

5.3-Service服务发现:coredns

5.3.1-什么是coredns

CoreDNS 其实就是一个 DNS 服务,而 DNS 作为一种常见的服务发现手段,所以很多开源项目以及 工程师都会使用 CoreDNS 为集群提供服务发现的功能,Kubernetes 就在集群中使用 CoreDNS 解决服务 发现的问题。 作为一个加入 CNCF(Cloud Native Computing Foundation)的服务, CoreDNS 的实现 非常简单。

在线下载配置文件地址是:https://docs.tigera.io/calico/3.25/getting-started/kubernetes/quickstart

kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.25.0/manifests/tigera-operator.yaml

kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.25.0/manifests/custom-resources.yaml

[root@master2 sc]# kubectl get svc |grep kubernetes
kubernetes     ClusterIP   10.96.0.1        <none>        443/TCP          5d20h

5.4-Service的应用

5.4.1-Service的资源清单
apiVersion: v1
kind: Service
metada:
  name:
  labels:
    key: value
spec:
  clusterIP: #动态分配地址,创建指定无法修改
  ports: #定义 service 端口,用来和后端 pod 建立联系
  selector <map[string]string> #通过标签选择器选择关联的 pod 有哪些
  sessionAffinity <string> 
  sessionAffinityConfig <Object> 
#service 在实现负载均衡的时候还支持 sessionAffinity,sessionAffinity 什么意思?会话联系,默认是 none,随机调度的(基于 iptables 规则调度的);如果我们定义sessionAffinity 的 client ip,那就表示把来自同一客户端的 IP 请求调度到同一个 pod上
  type <string> #定义 service 的类型
#四种类型:
#1.ExternalName: 类似于仅主机模式,容器访问外部,无selctor无端口无endpoint
#示例
kind: Service 
apiVersion: v1 
metadata: 
  name: my-service 
  namespace: prod 
spec: 
  type: ExternalName 
  externalName: my.database.example.com
#将 prod 名称空间中的 my-service 服务映射到 my.database.example.com,当查询主机 my-service.prod.svc.cluster.local 时,群集 DNS 将返回值为my.database.example.com 的 CNAME 记录。
#2.ClusterIP:通过集群内部IP暴露服务,只能内部访问,默认的
#3.Nodeport: 通过node的IP和容器的端口映射,实现访问
#Client----->NodeIP:NodePort----->Service Ip:ServicePort----->PodIP:ContainerPort。
#4.LoadBalancer: 使用云提供商的负载均衡器,可以向外部暴露服务。外部的负载均衡器可以路由到 NodePort 服务和ClusterIP 服务。
5.4.2-Service的端口
#service.spec.ports
spec:
  ports:
  - name:
    nodePort: #宿主机上的映射端口
    port: #service的端口,集群内部
    protocol: #协议
    targetPort: #pod上的端口从 port 和 nodePort 上来的流量,经过 kube-proxy 流入到后端 pod的 targetPort 上,最后进入容器。
5.4.3-ClusterIP 类型
apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo-web
spec:
  replicas: 2
  selector:
    matchLabels:
      demo: web
      version: v1
  template:
    metadata:
      labels:
        demo: web
        version: v1
    spec:
      containers:
      - name: demo-web-deploy
        image: docker.io/library/nginx:1.23.3
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: demo-web-sc
  labels:
    demo: web
spec:
  type: ClusterIP
  ports:
  - port: 80 #集群内部中暴露的端口
    protocol: TCP
    targetProt: 80 #容器端口
  selector:
    demo: web
[root@master2 sc]# kubectl get svc -l demo=web
NAME          TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
demo-web-sc   ClusterIP   10.105.73.16   <none>        80/TCP    69s
#在 k8s 控制节点访问 service 的 ip:端口就可以把请求代理到后端 pod
[root@master2 sc]# kubectl describe svc demo-web-sc|grep End
Endpoints:         192.168.137.84:80,192.168.180.41:80
#访问192.168.137.84和service IP是一样的;

#service 可以对外提供统一固定的 ip 地址,并将请求重定向至集群中的 pod。其中“将请求重定向至集群中的 pod”就是通过 endpoint 与 selector 协同工作实现。selector 是用于选择 pod,由selector 选择出来的 pod 的 ip 地址和端口号,将会被记录在 endpoint 中。endpoint 便记录了所有 pod的 ip 地址和端口号。当一个请求访问到 service 的 ip 地址时,就会从 endpoint 中选择出一个 ip 地址和端口号,然后将请求重定向至 pod 中。具体把请求代理到哪个 pod,需要的就是 kube-proxy 的轮询实现的。service 不会直接到 pod,service 是直接到 endpoint 资源,就是地址加端口,再由 endpoint 再关联到 pod。
#client→service→selector选择pod的ip和端口,记录到endpoint→kube-proxy轮询→service→client
5.4.4-NodePort类型
apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo-web
spec:
  replicas: 2
  selector:
    matchLabels:
      demo: web
      version: v1
  template:
    metadata:
      labels:
        demo: web
        version: v1
    spec:
      containers:
      - name: demo-web-deploy
        image: docker.io/library/nginx:1.23.3
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: demo-web-sc
  labels:
    demo: web
spec:
  type: NodePort
  ports:
  - port: 80 #集群内部中暴露的端口
    protocol: TCP
    targetPort: 80 #容器端口
    nodePort: 30080
  selector:
    demo: web
#更新启动验证结果
[root@master2 deploy]# kubectl apply -f nginx.yaml
deployment.apps/demo-web unchanged
service/demo-web-sc configured
[root@master2 deploy]# kubectl get svc
NAME          TYPE           CLUSTER-IP     EXTERNAL-IP                        PORT(S)        AGE
demo-nginx    ExternalName   <none>         kubernetes.default.svc.nginx.com   80/TCP         5d17h
demo-web-sc   NodePort       10.105.73.16   <none>                             80:30080/TCP   17h
kubernetes    ClusterIP      10.96.0.1      <none>                             443/TCP        5d19h
[root@master2 deploy]# curl localhost:30080
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>

服务走向:

Client-→node ip:30080->service ip:80-→pod ip:container port

5.4.5-ExternalName类型

ExternalName类型类似于仅主机模式,容器访问外部,应用场景:跨名称空间访问

apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo-busybox
spec:
  replicas: 1
  selector:
    matchLabels:
      demo: busybox
      version: v1
  template:
    metadata:
      labels:
        demo: busybox
        version: v1
    spec:
      containers:
      - name: demo-busybox
        image: docker.io/library/busybox:latest
        command: ["/bin/sh","-c","sleep 36000"]
---
apiVersion: v1
kind: Service
metadata:
  name: demo-busybox-svc
  labels:
    demo: busybox
spec:
  type: ExternalName
  externalName: demo-web-sc.default.svc.cluster.local
  ports:
  - port: 80 #集群内部中暴露的端口
    protocol: TCP
    targetPort: 80 #容器端口
  selector:
    demo: busybox
[root@master2 sc]# kubectl exec -it demo-busybox-bc9fd6585-wvwn2 -- sh
/ # wget -q -o - demo-web-sc.default.svc.cluster.local
/ # wget -q -o - demo-web-sc.default.svc.cluster.local
wget: can't open 'index.html': File exists
5.4.6-K8s最佳实践:映射外部MySQL服务

1.在集群外部署一个mysql服务。以下为docker-compse部署

[root@my cfsql]# cat docker-compose.yaml
version: "3.3"

services:
  cfdb:
    image: mysql:5.7
    volumes:
      - ./db_data:/var/lib/mysql
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: #自定义
    ports:
      - "3306:3306"
[root@my cfsql]# docker-compose ps
    Name                 Command             State                 Ports
--------------------------------------------------------------------------------------
cfsql_cfdb_1   docker-entrypoint.sh mysqld   Up      0.0.0.0:3306->3306/tcp, 33060/tcp

2.k8s集群编写SVC的服务

apiVersion: v1
kind: Service
metadata:
  name: aliyun-mysql
  labels:
    app: online
spec:
  type: ClusterIP
  ports:
  - port: 3306
---
apiVersion: v1 
kind: Endpoints 
metadata: 
  name: aliyun-mysql 
subsets: 
- addresses: 
  - ip: 47.109.97.22 #集群外部服务IP地址
  ports: 
  - port: 3306 #放开的端口
[root@master2 sc]# kubectl get svc |grep ali
aliyun-mysql   ClusterIP   10.104.231.127   <none>        3306/TCP   17s
[root@master2 sc]# mysql -u root -h 10.104.231.127 -p
Enter password:
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MySQL connection id is 39056
Server version: 5.7.40 MySQL Community Server (GPL)

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MySQL [(none)]>

现在即可用集群内部访问clusterIP+端口进行访问部署在阿里云的mysql;在生产环境中,如果用的公有云数据库,可用此方案。