官网链接
本教程会描述如何创建前端(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官方的镜像站在国内是拉不下来的,有几种方法解决:
- 在拉取镜像的虚拟机/服务器上科学上网
- 配置k8s的镜像源,目前国内只有阿里云支持改版后的k8s镜像源(registry.k8s.io)。
- 需要拉取镜像的时候,指定拉取策略为本地拉取(
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.io和registry.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: hello和tier: 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)。
- ClusterIP(默认):在集群内部分配一个 ClusterIP(虚拟 IP),只在集群内部可达。
后端服务只给集群内部其他服务调用;前端Service在用Ingress之前,会首先尝试使用ClusterIP。
- NodePort:在每个Node上开一个静态端口(NodePort),: 会被转发到Service的ClusterIP。
本质上是ClusterIP + 每个节点上都开一个端口。用于从集群外访问服务,尤其是本地环境、测试环境,例如使用nodePort: 30001暴露前端。
- LoadBalancer:依赖云厂商的 外部负载均衡器(如 AWS ELB、GCE LB、阿里云 SLB 等),对应云厂商会给你分配一个 外部 IP,然后转发到 NodePort 或直接转发到 Pod。适用于公有云上需要公网 IP对外暴露服务。
- ExternalName:不会分配 ClusterIP,也不会创建 Endpoints/EndpointSlices;
只是在集群DNS里做一个 CNAME 映射:.ns.svc.cluster.local映射到externalName指定的DNS名字。
适用于在集群内部解耦外部服务,例如在集群内部要访问外部服务(比如外部数据库、第三方 API),但希望用内部 Service 名访问;
方便以后把外部服务迁到集群里时,只改 Service 定义,不用改应用代码。
- 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
|