K8s实践练习3_服务发现与网络

官网链接

本教程会描述如何创建前端(Frontend)微服务和后端(Backend)微服务。后端微服务是一个 hello 欢迎程序。 前端通过 nginx 和一个 Kubernetes 服务(service)暴露后端所提供的服务。

教程目标

使用部署对象(Deployment object)创建并运行一个 hello 后端微服务 使用一个 Service 对象将请求流量发送到后端微服务的多个副本 同样使用一个 Deployment 对象创建并运行一个 nginx 前端微服务 配置前端微服务将请求流量发送到后端微服务 使用 type=NodePort 的 Service 对象将前端微服务暴露到集群外部

注意:在大多数本地/自建 K8s 环境(如 minikube、kubeadm、裸机等)里,集群没有云厂商的负载均衡器控制器来分配外部 IP,LoadBalancer 类型 Service的EXTERNAL-IP会长期 。我的环境是本地建的三台Ubuntu虚拟机里的k8s集群,所以前端service的type改成了NodePort。本地想用loadBalancer需要部署 MetalLB(让它变成“云式”LoadBalancer)或者进阶使用Ingress(集群只需一个公网/NodePort 入口)

前置准备

搭建好一套k8s集群,可以参考我写的这篇教程:搭建k8s集群

k8s官方的镜像站在国内是拉不下来的,有几种方法解决:

  1. 在拉取镜像的虚拟机/服务器上科学上网
  2. 配置k8s的镜像源,目前国内只有阿里云支持改版后的k8s镜像源(registry.k8s.io)。
  3. 需要拉取镜像的时候,指定拉取策略为本地拉取(imagePullPolicy:Never),每次需要拉取镜像前都手动拉取/上传一份镜像到服务器上再导入镜像

这里给出阿里云镜像源的配置教程: 旧版的k8s直接修改/etc/containerd/config.toml里的mirror信息,添加上阿里云的镜像站就行。但是新版的不支持inline或者说暂时兼容,未来不支持。所以这里就只给出新版k8s镜像源配置教程。

修改/etc/containerd/config.yaml,填入下列信息(如果你已经有了config.yaml且这个配置文件是从containerd默认配置里生成的,那直接备份,然后使用下面的内容)。sudo vim /etc/containerd/config.yaml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
version = 2

[plugins."io.containerd.grpc.v1.cri"]
  sandbox_image = "registry.aliyuncs.com/google_containers/pause:3.9"
  [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
    runtime_type = "io.containerd.runc.v2"
    [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
      SystemdCgroup = true
  [plugins."io.containerd.grpc.v1.cri".registry]
    config_path = "/etc/containerd/certs.d"

创建/etc/containerd/certs.d目录,在这个目录填入docker.io和registry.k8s.io的镜像源。

注意:k8s里修改镜像源之后,使用kubectl describe pod <pod_name> 查看时还是显示的docker.ioregistry.k8s.io。配置镜像源只物理修改从哪里修改,不改镜像拉取的逻辑源。所以改好镜像源之后也不太好验证成功,随便拉个镜像sudo crictl pull nginx:1.14.2,能拉下来就是成了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# Docker Hub 加速
sudo mkdir -p /etc/containerd/certs.d/docker.io
sudo tee /etc/containerd/certs.d/docker.io/hosts.toml << 'EOF'
server = "https://registry-1.docker.io"

[host."https://docker.m.daocloud.io"]
  capabilities = ["pull", "resolve"]
EOF

# K8s 镜像加速
sudo mkdir -p /etc/containerd/certs.d/registry.k8s.io
sudo tee /etc/containerd/certs.d/registry.k8s.io/hosts.toml << 'EOF'
server = "https://registry.k8s.io"

[host."https://k8s.nju.edu.cn"]
  capabilities = ["pull", "resolve"]

[host."https://registry.cn-hangzhou.aliyuncs.com/google_containers"]
  capabilities = ["pull", "resolve"]
EOF

到这里,镜像源就配置好了,如果不出意外,文件目录应该是下面这样:

1
2
3
4
5
6
7
rust@k8s1:/etc/containerd$ ll
总计 28
drwxr-xr-x   3 root root  4096  2月  4 10:31 ./
drwxr-xr-x 144 root root 12288  2月  2 17:01 ../
drwxr-xr-x   4 root root  4096  2月  2 16:44 certs.d/
-rw-r--r--   1 root root   423  2月  2 19:02 config.toml
-rw-r--r--   1 root root   886 12月 19 02:48 config.toml.dpkg-dist

修改完配置文件后需要重启containerd:

1
2
sudo systemctl restart containerd
sudo systemctl status containerd

准备镜像

镜像下载

官网演示里用的后端镜像和前端镜像阿里云并没有同步,好在问ai发现docker仓库里有个用户上传了镜像。

这里拉镜像有两种方式,一种就是找个能科学上网的机器,把k8s上的镜像拉下来(不用阿里云镜像源);另一种就是从docker上把镜像拉下来再导到k8s集群里。我用的第二种方式(由于镜像文件太大,放不进蓝奏云里,放百度网盘下载又很慢,而且最近百度云盘才被爆出后台扫盘,所以不建议用百度云盘)。文中会给出具体命令,读者就八仙过海,各显神通吧。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 拉取前后端的镜像 docker/crictl(containerd的)
# sudo crictl pull docker.io/vaikulkaz/hello-go-gke:1.0
# sudo crictl pull docker.io/vaikulkaz/hello-frontend:1.0

docker pull docker.io/vaikulkaz/hello-go-gke:1.0
docker pull docker.io/vaikulkaz/hello-frontend:1.0

# 保存镜像
docker save -o hello-go-gke-1.0.tar vaikulkaz/hello-go-gke:1.0
docker save -o hello-frontend-1.0.tar vaikulkaz/hello-frontend:1.0

上传保存的镜像tar包到k8s集群上的每一个节点。建议上传到一台上,然后用脚本分发。因为后面还要在每个节点上导入镜像。

文件分发和命令传输的脚本

在用户目录创建一个bin目录,创建xcall(命令传输,用于在三台虚拟机上依次执行相同命令)、xsync(文件分发,用于把文件分发到三台虚拟机上)。

1
2
3
4
5
6
7
cd 
mkdir bin
cd bin
vim xsync
sudo chmod 777 xsync
vim xcall
sudo chmod 777 xcall

xsync内容如下,注意我的三台虚拟机叫k8s1、k8s2、k8s3。如果你的虚拟机不叫这个名字,记得修改成自己的主机名

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#!/bin/bash
#校验参数是否合法
if(($#==0))
then
        echo 请输入要分发的文件!
        exit;
fi
#获取分发文件的绝对路径
dirpath=$(cd `dirname $1`; pwd -P)
filename=`basename $1`

echo 要分发的文件的路径是:$dirpath/$filename

#循环执行rsync分发文件到集群的每条机器
for((i=1;i<=3;i++))
do
        echo ---------------------k8s$i---------------------
        rsync -rvlt $dirpath/$filename  k8s$i:$dirpath
done
#此脚本用于虚拟机之间通过scp传送文件

xcall内容如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#!/bin/bash
#在集群的所有机器上批量执行同一个命令
if(($#==0))
then
        echo 请输入要操作的命令!
        exit;
fi

echo 要执行的命令是$*

#循环执行此命令
for((i=1;i<=3;i++))
do
        echo --------------------k8s$i--------------------
        ssh k8s$i $*
done
#此脚本用于对所有声明的虚拟机进行命令的传输

分发导入镜像

分发导入的命令如下:

1
2
3
4
5
6
# 分发镜像文件
xsync hello-frontend-1.0.tar hello-go-gke-1.0.tar

# 脚本批处理,在k8s节点上导入镜像
xcall sudo ctr -n k8s.io images import hello-frontend-1.0.tar
xcall sudo ctr -n k8s.io images import hello-go-gke-1.0.tar

创建前后端

后端部署

创建后端deploymentbackend-deployment.yaml,内容如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
apiVersion: apps/v1
kind: Deployment
metadata:
  name: backend
spec:
  selector:
    matchLabels:
      app: hello
      tier: backend
      track: stable
  replicas: 3
  template:
    metadata:
      labels:
        app: hello
        tier: backend
        track: stable
    spec:
      containers:
        - name: hello
          image: vaikulkaz/hello-go-gke:1.0
          ports:
            - name: http
              containerPort: 80

创建后端servicebackend-service.yaml,内容如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
apiVersion: v1
kind: Service
metadata:
  name: hello
spec:
  selector:
    app: hello
    tier: backend
  ports:
  - protocol: TCP
    port: 80
    targetPort: http

创建后端的pod和service:

1
2
kubectl apply -f backend-deployment.yaml
kubectl apply -f backend-service.yaml

前端部署

创建前端deploymentfrontend-deployment.yaml,内容如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend
spec:
  selector:
    matchLabels:
      app: hello
      tier: frontend
      track: stable
  replicas: 1
  template:
    metadata:
      labels:
        app: hello
        tier: frontend
        track: stable
    spec:
      containers:
        - name: nginx
          image: vaikulkaz/hello-frontend:1.0          
          lifecycle:
            preStop:
              exec:
                command: ["/usr/sbin/nginx","-s","quit"]

创建前端servicefrontend-service.yaml,内容如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
apiVersion: v1
kind: Service
metadata:
  name: frontend
spec:
  selector:
    app: hello
    tier: frontend
  ports:
  - protocol: "TCP"
    port: 80
    targetPort: 80
    nodePort: 30001   # 可选,不指定则系统自动分配
  type: NodePort

创建前端的pod和service:

1
2
kubectl apply -f frontend-deployment.yaml
kubectl apply -f frontend-service.yaml

状态检查

查看前后端的pod状态,应该都是running;

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
rust@k8s1:~$ kubectl get pods
NAME                        READY   STATUS             RESTARTS          AGE
backend-5fd65cb786-dv265    1/1     Running            1 (128m ago)      4d15h
backend-5fd65cb786-hdzvz    1/1     Running            1 (128m ago)      4d15h
backend-5fd65cb786-zdjqb    1/1     Running            1 (128m ago)      4d15h
frontend-5c6cf9bbfc-5mln7   1/1     Running            10 (121m ago)     4d15h
liveness-exec               0/1     CrashLoopBackOff   136 (2m30s ago)   4d21h
liveness-grpc               1/1     Running            1 (128m ago)      4d19h
liveness-http               0/1     CrashLoopBackOff   144 (4m29s ago)   4d21h
liveness-tcp                0/1     CrashLoopBackOff   139 (4m47s ago)   4d21h

查看前后端的service状态,后端service类型是ClusterIP,前端service类型是NodePort

1
2
3
4
5
rust@k8s1:~$ kubectl get services
NAME         TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE
frontend     NodePort    10.101.209.199   <none>        80:30001/TCP   99m
backend        ClusterIP   10.111.212.50    <none>        80/TCP         119m
kubernetes   ClusterIP   10.96.0.1        <none>        443/TCP        7d

原理介绍

将请求从前端发送到后端的关键是后端 Service。Service 创建一个固定 IP 和 DNS 解析名入口, 使得后端微服务总是可达。Service使用选择算符(.selector/hello .selector/tier)来寻找目标 Pod。

后端配置文件中,可以看到名为hello的Service将流量路由到包含app: hellotier: backend标签的 Pod,也就是后端的pod上。后端pod的作用就是返回网页。

前端使用被赋予后端 Service的DNS名称将请求发送到后端工作 Pods。这一 DNS 名称为 hello,也就是backend-service.yaml配置 文件中 name 字段的取值。

前端 Deployment 中的 Pods 运行一个 nginx 镜像,这个已经配置好的镜像会将请求转发给后端的 hello Service。 下面是 nginx 的配置文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# Backend 是 nginx 的内部标识符,用于命名以下特定的 upstream
upstream Backend {
    # hello 是 Kubernetes 中的后端服务所使用的内部 DNS 名称
    server hello;
}
server {
listen 80;
location / {
    # 以下语句将流量通过代理方式转发到名为 Backend 的上游
    proxy_pass http://Backend;
}
}

说明:这个 nginx 配置文件是被打包在容器镜像里的。更好的方法是使用ConfigMap,这样解耦了配置参数和应用镜像,可以更轻易地更改配置。

k8s Service介绍

Service按API type字段分有4种type:ClusterIP(默认)、NodePort、LoadBalancer和ExternalName。 此外,ClusterIP还有一种特殊用法Headless Service(spec.clusterIP: None)。

  1. ClusterIP(默认):在集群内部分配一个 ClusterIP(虚拟 IP),只在集群内部可达。 后端服务只给集群内部其他服务调用;前端Service在用Ingress之前,会首先尝试使用ClusterIP。
  2. NodePort:在每个Node上开一个静态端口(NodePort),: 会被转发到Service的ClusterIP。 本质上是ClusterIP + 每个节点上都开一个端口。用于从集群外访问服务,尤其是本地环境、测试环境,例如使用nodePort: 30001暴露前端。
  3. LoadBalancer:依赖云厂商的 外部负载均衡器(如 AWS ELB、GCE LB、阿里云 SLB 等),对应云厂商会给你分配一个 外部 IP,然后转发到 NodePort 或直接转发到 Pod。适用于公有云上需要公网 IP对外暴露服务。
  4. ExternalName:不会分配 ClusterIP,也不会创建 Endpoints/EndpointSlices; 只是在集群DNS里做一个 CNAME 映射:.ns.svc.cluster.local映射到externalName指定的DNS名字。 适用于在集群内部解耦外部服务,例如在集群内部要访问外部服务(比如外部数据库、第三方 API),但希望用内部 Service 名访问; 方便以后把外部服务迁到集群里时,只改 Service 定义,不用改应用代码。
  5. Headless:type: ClusterIP(或者不写 type,默认就是 ClusterIP)再加上 spec.clusterIP: None。K8s不会分配 ClusterIP; kube-proxy 不会给它做负载均衡和转发;集群 DNS 会返回每个Pod的IP(A/AAAA 记录),而不是一个VIP。适用于客户端自己选择连哪个 Pod(比如某些数据库客户端、服务发现场景);StatefulSet + Headless Service,每个 Pod 有稳定的网络标识(DNS 名)。

hello项目测试

前端和后端已经完成连接了。可以使用 curl 命令通过你的前端 Service 的外部 IP 访问服务端点。

为了确定服务的连通性,我在k8s2上进行测试。由于我配置了host映射,所以这里可以用k8s1:port。没配置的要换成自己的ip(集群上哪个节点的ip都可以)。

1
2
rust@k8s2:~$ curl k8s1:30001
{"message":"Hello"}

测试完成,练习结束。不想保存这个练习的服务和pod可用如下命令清除:

1
2
kubectl delete services frontend backend
kubectl delete deployment frontend backend
网站总访客数:Loading
网站总访问量:Loading
使用 Hugo 构建
主题 StackJimmy 设计