k8s业务迁移与服务部署实践

K8s运行业务的优势

部署上线业务流程

情景模拟:
image.png
业务部署上线是每个运维都需要面对的问题,接下来分别从传统运维和k8s运维角度,梳理操作流程:
传统运维:

  1. 安装操作系统
  2. 初始化系统配置(安全策略、时间同步、yum源……)
  3. 安装配置java环境
  4. 打jar包并部署服务
  5. Systemctl添加自定义服务或supervisor进程守护

k8s运维:

  1. 安装操作系统
  2. 初始化系统配置(安全策略、时间同步、yum源……)
  3. 部署k8s集群(多台机器)
  4. 封装docker镜像
  5. 创建资源清单,完成项目部署

分析:
两者都需要安装操作系统,初始化系统。 不同之处在于传统运维只需要单机配置环境部署服务即可。而k8s运维则需要部署搭建一个k8s集群,然后封装docker镜像,创建k8s的资源清单,才能完成项目的部署。相比较而言,k8s方式部署业务,前期从时间成本,资源使用考虑都是高于传统方式部署业务的。

故障处理转移流程

image.png
业务上线初期,通常访问量很小,大多数情况都是单节点运行业务,那么如果出现硬件故障,例如磁盘坏盘,导致系统死机,用户无法访问业务服务。那么我们应该如何处理呢?
传统运维:

  1. 安装操作系统
  2. 初始化系统配置
  3. 安装配置java环境
  4. 打jar包并部署服务
  5. Systemctl添加自定义服务或supervisor进程守护
  6. 修改DNS解析,指向新的服务器地址

k8s运维:

  1. K8s检测到节点故障后,自动进行故障转移,将故障机器上的服务自动迁移至其他正常机器上运行

分析:
如果是传统运维的话,服务器硬件故障可能一时半会不能修复,为了尽可能缩短业务中断时间,降低经济损失,只能快速再起一台服务器,再重复执行一遍业务上线的流程,最后修改DNS解析,或者修改公网ip,把流量引入新的机器中,恢复故障。
而如果是k8s运维的话,因为前期在部署上线时,已经搭建了k8s集群,当集群内有一个节点故障后,k8s发现节点宕机后,会将异常节点上的pod都变为unknow状态,并自动在其他机器上开启指定数量的副本pod。整个故障迁移过程中,运维人员不需要参与,用户也不会感知到服务异常,更不会对业务造成中断。

业务扩容流程

image.png
随着业务的不断发展,为了提高服务性能、避免单点故障,需要进行由单体服务向集群转变的操作。
传统运维:

  1. 安装操作系统
  2. 初始化系统配置
  3. 安装配置java环境
  4. 打jar包并部署服务
  5. Systemctl添加自定义服务或supervisor进程守护
  6. 部署nginx、lvs或者haproxy等服务,实现负载均衡

k8s运维:

  1. 修改资源清单文件,将副本数从1改为N,由service自动进行负载均衡。

分析:
如果是传统运维,那就再执行几遍业务上线流程,如果机器过多,可以使用ansible等批量工具编写playbook执行,最后部署一个lvs 或者nginx 或者haproxy等反向代理软件,实现负载均衡,但过程中集群机器尽可能使用一样的操作系统和环境。
而如果是一个k8s运维,那么他需要做的操作,只是将副本数从1改为N即可,不需要关注如何实现负载均衡,因为k8s通过service已经实现了负载均衡。而且,面对一些瞬时暴增的流量,k8s可以通过HPA自动扩缩容,非常适合于一些流量波动大,机器资源吃紧,服务数量多的业务场景。

版本更新发布流程

image.png
传统运维:

  1. 拉取最新代码并打jar包
  2. 修改负载均衡配置,剔除将要更新的机器
  3. 停止服务,替换jar包文件
  4. 启动服务,访问测试
  5. 修改负载均衡配置,恢复更新完成的机器
  6. 重复上述步骤,直至全部机器完成

k8s运维:

  1. 拉取最新代码,封装docker镜像,推送至镜像仓库
  2. 修改资源清单文件的镜像配置,执行apply操作,完成滚动更新。

分析:
以java项目为例,当打完jar包后,传统运维的话,因为整个业务是集群方式运行,所以首先就从负载均衡服务中,剔除那个将要更新的机器,防止流量进入。然后逐个更新,期间不停修改负载均衡服务后端地址列表。
而如果是k8s运维的话,只需要将最新jar包打包成一个新版本的业务docker镜像,然后修改资源清单文件的镜像版本即可,使用deployment控制器会自动完成滚动更新,实现零停机发布。

运维成本分析对比

传统运维K8s运维
部署上线配置环境,打包部署服务配置服务管理脚本搭建集群、封装镜像、创建资源
健康检查编写shell脚本或使用守护进程工具k8s提供就绪、存活探针,支持exec执行命令、HTTP状态码、TCP端口探测
故障转移新机器部署服务,修改DNS解析指向新机器K8s内部自动实现故障转移
集群扩容新机器部署服务,部署负载均衡服务,配置后端地址修改k8spod资源副本数,service自动实现负载均衡
版本更新修改负载均衡配置,逐个滚动停止服务,替换jar包,启动服务更新docker镜像并上传,修改k8spod资源镜像信息,k8s自动实现滚动更新

以上五个运维场景相信也是大家在日常工作中会经常用到的,两种运维方式相比较而言,k8s只是在项目初期需要投入更多的时间和精力,但是当业务稳定运行后,k8s可以大大的简化运维的工作内容,是一种一劳永逸,更加优雅的运维方式。

应用迁移基本流程

将应用封装进容器

将应用迁移到k8s,首先第一步就是要编写dockerfile,将原本在Linux服务器上运行的服务打包到容器中运行。我们回想传统方式部署业务的整体流程,我们一般是从最底层开始选择合适版本的操作系统,配置一系列的初始化操作,配置安全策略等。然后再部署运行环境,配置环境变量,最后上传项目代码到服务器中,配置启动脚本或者守护进程。在整个过程中,我们所有的操作分为了三个层面,最底层的操作系统,中间层的运行环境,上层的应用服务。
image.png

而编写dockerfile时,base镜像的选择也是从这几个层面考虑,如果环境层的镜像无法满足需求,就从系统层开始编写dockerfile。基础镜像可以从以下几个网站获取到
https://hub.docker.com/
https://quay.io/
http://gcr.io
通过下面几个常见案例说明:
案例1:
image.png
我们也已经明确知道了这个项目运行的系统和环境,也就是前面提到的三层中的系统层和环境层是已经确定的,使用docker镜像仓库中提供的基础镜像就可以满足我们的要求,我们只需要在应用层上将自己的代码加入镜像即可 。dockerfile基本内容参考文档:https://www.cuiliangblog.cn/detail/section/26458557

FROM openjdk:19-jdk-alpine3.16 # 使用alpine3.16操作系统,java19运行环境为基础镜像
ADD demo.jar /opt/app.jar      # 复制项目jar包文件到容器/opt/目录下
EXPOSE 8888                    # 声明容器暴露的端口
WORKDIR /opt                   # 指定容器工作目录
CMD ["java","-jar","app.jar"]  # 指定启动命令为java –jar /app.jar

案例2:
image.png
在这个场景中,我们需要两个环境,一个是编译打包环境需要nodejs,一个是运行环境需要nginx。此时我们就需要用到多阶段构建,将打包阶段中的文件拷贝到后边的运行阶段中,实现编译环境和运行环境分离,dockerfile多阶段构建镜像参考文档:https://www.cuiliangblog.cn/detail/section/31400933。我们的dockerfile就这样写

FROM node:16.15.0 AS build        # 使用node 16.15运行环境 用于打包项目,并将第一阶段命名为build
COPY . /vue                       # 复制项目代码到/vue目录下
WORKDIR /vue                      # 设置工作目录为/vue
RUN npm install && npm run build  # 安装项目依赖并打包项目

FROM nginx:1.20.1            							 # 使用nginx 1.20运行环境为基础镜像
COPY --from=build /vue/dist /opt/vue/dist  # 拷贝build阶段生成的打包文件dist到容器目录下
EXPOSE 80                                  # 声明容器暴露的端口
COPY vue.conf /etc/nginx/nginx.d/vue.conf  # 拷贝nginx配置文件到容器nginx配置文件目录下
CMD ["nginx", "-g","daemon off;"]          # 指定启动命令

案例3:
image.png
前两个案例中,我们都是使用默认的系统和环境,就可以实现我们的需求,我们只需要自定义应用层的东西即可。但如果碰到这样的需求,也就是说直接使用Python这个环境层的镜像无法满足要求了,因为他不支持yum命令安装sshd服务,直接拷贝ssh的二进制文件编译安装也非常麻烦,那我们就只能使用最基础的系统层镜像,然后自定义Python环境,完成镜像封装。

FROM centos:centos8 # 选取centos8为基础镜像
RUN dnf install openssh-server passwd python38 python38-devel -y # 安装相关软件包
RUN /bin/echo "123.com" | passwd --stdin root # 设置ssh密码
RUN /bin/sed -i 's/.session.required.pam_loginuid.so./session optional pam_loginuid.so/g' /etc/pam.d/sshd && /bin/sed -i 's/UsePAM yes/UsePAM no/g' /etc/ssh/sshd_config && /bin/sed -i "s/#UsePrivilegeSeparation.*/UsePrivilegeSeparation no/g" /etc/ssh/sshd_config # 修改sshd配置文件
RUN ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key && ssh-keygen -t rsa -f /etc/ssh/ssh_host_ecdsa_key && ssh-keygen -t rsa -f /etc/ssh/ssh_host_ed25519_key # 创建密钥
RUN echo -e "#! /bin/bash\n/usr/sbin/sshd -D" > /run.sh # 创建sshd服务启动脚本
ADD . /app # 拷贝代码到容器目录
RUN pip3.8 install -r /app/requirements.txt # 安装依赖
EXPOSE 22 # 声明容器暴露的端口
CMD ["/usr/sbin/sshd","-D"] # 指定启动命令

dockerfile构建镜像经验

  1. 减少镜像的层数,尽量把一些功能上面统一的命令合到一起来做;
  2. 注意清理镜像构建的中间产物,比如一些安装包在装完之后就把它删掉;
  3. 注意优化网络请求,使用yum源的时候,用一些网络比较好的源站点,可节约时间,减少失败率;
  4. 尽量去使用缓存构建,尽量把一些不变的东西或者变动比较少的东西放在前面。例如业务更新,打包新版本镜像时,只有代码发生变化,其他内容不变。此时可以把拷贝代码放最后,前面的构建阶段直接使用缓存即可。

将容器放入Pod中

image.png
当我们把应用封装成docker镜像后,接下来就是在kubernetes中启动镜像运行容器。因为Pod是Kubernetes管理的最小单元,Kubernetes不直接管理容器,而是管理Pod,Pod里面包含一个或多个容器。需要考虑是一个Pod中放置多个容器,还是一个Pod中放置一个容器。在一个pod中所有container共享,PID、Network、IPC
image.png
在有些业务场景中,例如一个web服务,对应的资源文件需要从远端实时监听更新,就需要使用边车 (sidercar)模式,一个pod中包含一个web容器一个file容器,通过共享存储方式实现。又比如一个用户的微服务包含:User API、User Control、User Data等三个模块,彼此之间紧耦合,对外只需要通过User API,这样类型的应用就可以放置在一个Pod中。
Pod资源清单建议

  1. 建议开发项目时,提供healthy健康检查接口,便于检测服务是否正常。
  2. 建议运维配置LivenessProbe存活检测探针,防止服务假死。
  3. 建议配置resources,避免程序出现异常,并占用大量的系统资源,从而会影响节点上其他的Pod。

使用Controller管理Pod

单一Pod如果出现故障,就会影响业务连续性,所以需要多副本,就像我们给一个Web应用做集群是一样的。Kubernetes提供了不同的Controller,需要根据应用的实际情况选择使用Deployment、DaemonSet、StatefulSet、Job、CronJob等,只需要在Pod的YAML模板上封装上对应的配置即可。
image.png
kubernetes提供的控制器如下:

  1. Deployment:封装了Pod的副本管理、部署更新、回滚、扩容、缩容等。
  2. DaemonSet:保证所有的Node上有且只有一个Pod在运行。
  3. StatefulSet:有状态的应用,为Pod提供唯一的标识,它可以保证部署和scale的顺序。
  4. Job:使用Kubernetes运行单一任务。
  5. CronJob:使用Kubernetes运行定时任务。

使用Service访问Pod

由于每次pod重启都会随机生成新的ip,且使用控制器运行多副本pod时,该访问哪个具体的IP呢?
image.png
在传统的方式运行服务时,通常会提供一个VIP和service后端地址池。其中VIP负责对外暴露服务,监听请求。后端地址池复制监听后端服务的变化,随时更新地址池,并通过控制器实现轮循 哈希等负载均衡方式。
在kubernetes中,service便是这样的存在,service只是一个抽象的概念,他的工作主要由endpoint controller和kube-proxy搭配完成。
endpoints controller 是负责生成和维护所有 endpoints 对象的控制器,监听 service 和对应 pod 的变化,更新对应 service 的 endpoints 对象。当用户创建 service 后 endpoints controller 会监听 pod 的状态,当 pod 处于 running 且准备就绪时,endpoints controller 会将 pod ip记录到 endpoints 对象中,因此,service 的容器发现是通过 endpoints 来实现的。
而 kube-proxy 会监听 service 和 endpoints 的更新并调用其代理模块在主机上刷新路由转发规则。实际的路由转发都是由 kube-proxy 组件来实现的。
目前Service的负载均衡支持多种实现方式:User Space、iptable和ipvs。如果使用ipvs,当你创建Service的时候,kube-proxy会获取Service对应的Endpoint,调用LVS帮我们实现负载均衡的功能。
image.png
k8s同样也为我们提供了多种service使用:
ClusterIP(集群IP):集群内的服务间通信。 例如,应用程序的前端(front-end)和后端(back-end)组件之间的通信。
NodePort(节点端口):k8s集群内部服务暴露给外部时访问,可以通过k8s节点ip+端口方式访问服务。
LoadBalancer(负载均衡器):使用云厂商来托管您的 Kubernetes 集群时。由它接入外部客户端的请求并调度至集群节点相应的NodePort之上
ExternalName(外部名称):在 Kubernetes 内创建服务来表示映射外部服务名称,例如在 Kubernetes 中使用公有云数据库时,可以创建一个ExternalName资源,后续更换云数据库地址时,更新ExternalName配置即可

使用Ingress提供外部访问

虽然用户可以通过service提供的nodeport方式或者loadbalancer方式访问k8s集群内部的服务,但是随着服务的增多,让普通用户通过ip+端口方式访问服务是不现实的。此时就需要通过不同的域名对应访问不同的服务,而这种全局的、为了代理不同后端 Service 而设置的负载均衡服务,就是 Kubernetes 里的 Ingress 服务。
image.png
Ingress同样也只是一个概念,它的具体的实现依赖控制器,Ingress控制器并不直接运行为kube-controller-manager的一部分,它是Kubernetes集群的一个重要附件,类似于CoreDNS,需要在集群上单独部署。Ingress控制器可以由任何具有反向代理(HTTP/HTTPS)功能的服务程序实现,如Nginx、Envoy、HAProxy、Vulcand和Traefik等。

使用ConfigMap管理配置文件

在DevOps的部署流水线中,其中有一个核心的理念就是代码和配置的分离,这样更容易实现流水线的编排。我们只需要使用一套代码,配合不同的配置文件,就可以实现灵活发布到测试环境、预发布环境、生产环境等。
image.png
kubernetes同样也为我们提供了配置与文件管理方案:

  • ConfigMap配置文件:可以从文件、文件夹等途径创建ConfigMap。然后再Pod中挂载使用配置文件,例如nginx配置。
  • secret私密文件:使用base64加密的文件,例如存储第三方镜像仓库凭证 配置TLS 类型secret 用于记录证书秘钥等信息。

使用共享存储持久化存储数据

容器中的存储都是临时的,因此Pod重启的时候,内部的数据会发生丢失。实际应用中,我们有些应用是无状态,有些应用则需要保持状态数据,确保Pod重启之后能够读取到之前的状态数据,有些应用则作为集群提供服务。这三种服务归纳为无状态服务、有状态服务以及有状态的集群服务,其中后面两个存在数据保存与共享的需求,因此就要采用容器外的存储方案。
image.pngkubernetes拥有众多类型的用于适配专用存储系统的网络存储卷。这类存储卷包括传统的NAS或SAN设备(如NFS、iSCSI、fc)、分布式存储(如GlusterFS、RBD)、云端存储(如gcePersistentDisk、azureDisk、cinder和awsElasticBlockStore)以及建构在各类存储系统之上的抽象管理层(如flocker、portworxVolume和vsphereVolume)等。

资源清单部署服务案例实践

环境准备

ip主机名角色
192.168.10.100tiaobanharbor私有仓库+nfs服务器+es服务+kibana
192.168.10.10k8s-masterk8s master节点
192.168.10.11k8s-work1k8s work节点
192.168.10.12k8s-work2k8s work节点

nfs部署文档:https://www.cuiliangblog.cn/detail/section/116191364
traefik部署文档:https://www.cuiliangblog.cn/detail/section/94793162
harbor部署文档:https://www.cuiliangblog.cn/detail/section/15189547
es+kibana部署文档:https://www.cuiliangblog.cn/detail/section/117075458

案例1(springboot)

image.png
思路分析

  • 开发提供jar包:基础镜像直接使用open-jdk即可
  • 部署到k8s:多副本管理,滚动更新:需要使用Deployment控制器;Pod多副本负载均衡:需要使用ClusterIP服务资源
  • 用户通过域名访问:使用Ingress创建域名路由,流量转发到Service

构建docker镜像
我们将打包好的jar文件和dockerfile放在同一级目录下,执行docker build指令,即可完成镜像构建

[root@tiaoban springboot]# ls
demo  demo-v1.jar  demo-v2.jar  Dockerfile
[root@tiaoban springboot]# cat Dockerfile 
FROM openjdk:19-jdk-alpine3.16
ADD demo-v1.jar /opt/app.jar
EXPOSE 8888
WORKDIR /opt
CMD ["java","-jar","app.jar"]
# 打包镜像
[root@tiaoban springboot]# docker build -t springboot:v1 .
# 启动容器,访问测试
[root@k8s-master springboot]# docker run -d -p 8888:8888 springboot:v1
74a8bc2446d7f30195099c8f6f905481b2fdc6b585436e4b810849f1df50185d
[root@k8s-master springboot]# curl 127.0.0.1:8888
Hello SpringBoot Version:v1[root@k8s-master springboot]# 
[root@k8s-master springboot]# curl 127.0.0.1:8888/healthy
ok[root@k8s-master springboot]#

推送至harbor私有镜像仓库
构建好镜像测试无误后,接下来就可以推送到harbor私有镜像仓库中

[root@k8s-master springboot]# docker tag springboot:v1 192.168.10.100/demo/springboot:v1
[root@k8s-master springboot]# docker push 192.168.10.100/demo/springboot:v1

image.png
创建pod资源调试
推送完成后,接下来我们开始编写pod的资源清单文件,镜像地址填写harbor仓库的镜像地址

[root@k8s-master springboot]# cat pod.yaml 
apiVersion: v1
kind: Pod
metadata:
  name: springboot
  labels:
    name: springboot
spec:
  containers:
  - name: springboot
    image: 192.168.10.100/demo/springboot:v1
    resources:
      limits:  # 资源使用最大值
        memory: "1Gi"
        cpu: "1"
      requests:
        memory: "128Mi"
        cpu: "100m" 
    ports:
      - containerPort: 8888
        name: web
    livenessProbe:
      httpGet: # http存活检测,返回状态码为200表示正常
        port: web
        path: /healthy
      initialDelaySeconds: 20 # 容器启动后20秒开始检测
    readinessProbe:
      tcpSocket:  # tcp就绪检测,只有当8888端口开始监听,才会有流量引入
        port: web
[root@k8s-master springboot]# kubectl apply -f pod.yaml 
pod/springboot created
[root@k8s-master springboot]# kubectl get pod -o wide
NAME                                               READY   STATUS    RESTARTS      AGE   IP           NODE        NOMINATED NODE   READINESS GATES
nfs-subdir-external-provisioner-7c8776699f-mkpxg   1/1     Running   4 (56m ago)   97m   10.244.2.5   k8s-work2   <none>           <none>
springboot                                         1/1     Running   0             46s   10.244.2.7   k8s-work2   <none>           <none>
# pod启动后,可以通过pod的IP+端口方式访问服务,验证测试是否正常启动
[root@k8s-master springboot]# curl 10.244.2.7:8888
Hello SpringBoot Version:v1

创建deployment资源
pod资源创建完成测试无误,接下来就是使用deployment控制器管理pod,让它以多副本方式运行

[root@k8s-master springboot]# kubectl delete -f pod.yaml 
pod "springboot" deleted
[root@k8s-master springboot]# cat deployment.yaml 
apiVersion: apps/v1
kind: Deployment
metadata:
  name: springboot
spec:
  replicas: 2 # 副本数
  selector:
    matchLabels:
      app: springboot
  template:
    metadata:
      labels:
        app: springboot
    spec:
      containers:
      - name: springboot
        image: 192.168.10.100/demo/springboot:v1
        resources:
          limits:
            memory: "1Gi"
            cpu: "1"
          requests:
            memory: "128Mi"
            cpu: "100m" 
        ports:
          - containerPort: 8888
            name: web
        livenessProbe:
          httpGet:
            port: web
            path: /healthy
          timeoutSeconds: 2       # 表示容器必须在2s内做出相应反馈给probe,否则视为探测失败
          periodSeconds: 30       # 探测周期,每30s探测一次
        readinessProbe:
          tcpSocket:
            port: web
          initialDelaySeconds: 10 # 容器启动后10s开始探测
[root@k8s-master springboot]# kubectl apply -f deployment.yaml 
deployment.apps/springboot created
[root@k8s-master springboot]# kubectl get pod -o wide
NAME                                               READY   STATUS    RESTARTS      AGE    IP           NODE        NOMINATED NODE   READINESS GATES
nfs-subdir-external-provisioner-7c8776699f-mkpxg   1/1     Running   4 (61m ago)   102m   10.244.2.5   k8s-work2   <none>           <none>
springboot-55df548fdb-lr4v4                        1/1     Running   0             32s    10.244.2.8   k8s-work2   <none>           <none>
springboot-55df548fdb-pdpfh                        1/1     Running   0             32s    10.244.1.6   k8s-work1   <none>           <none>
# deployment控制器资源部署成功后,我们继续使用pod IP+端口方式访问测试
[root@k8s-master springboot]# curl 10.244.2.8:8888
Hello SpringBoot Version:v1
[root@k8s-master springboot]# curl 10.244.1.6:8888/healthy
ok

创建service资源
控制器资源创建无误后,接下来我们部署service资源,然后就可以通过clusterIP+端口方式访问服务

[root@k8s-master springboot]# cat service.yaml 
apiVersion: v1
kind: Service
metadata:
  name: springboot
spec:
  type: ClusterIP 
  selector:
    app: springboot 
  ports:
  - name: http
    port: 8888
    protocol: TCP 
    targetPort: 8888 
[root@k8s-master springboot]# kubectl apply -f service.yaml 
service/springboot created
[root@k8s-master springboot]# kubectl get svc 
NAME         TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)    AGE
kubernetes   ClusterIP   10.96.0.1        <none>        443/TCP    127m
springboot   ClusterIP   10.100.116.123   <none>        8888/TCP   6s
[root@k8s-master springboot]# curl 10.100.116.123:8888
Hello SpringBoot Version:v1

创建ingress资源
至此,资源创建基本完成,接下来我们配置一个ingress域名资源,客户端配置hosts地址后,就可以通过域名方式访问服务

[root@k8s-master springboot]# cat ingress.yaml 
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
  name: springboot
spec:
  entryPoints:
  - web
  routes:
  - match: Host(`springboot.test.com`) # 域名
    kind: Rule
    services:
      - name: springboot  # 与svc的name一致
        port: 8888     # 与svc的port一致[root@k8s-master springboot]# 
[root@k8s-master springboot]# kubectl apply -f ingress.yaml 
ingressroute.traefik.containo.us/springboot created
[root@k8s-master springboot]# kubectl get ingressroute
NAME         AGE
springboot   18s

image.png

案例2(vue)

image.png
思路分析

  • VUE项目,只提供源码:构建镜像时,使用Nodejs和NGINX两个基础镜像多阶段构建
  • 部署开发与生产环境:一套代码,两套环境,通过configmap创建两套不同配置。
  • 使用两个域名访问两个环境:部署两套Controller 、Service、Ingress。
  • 从远端定时拉取资源:需要使用sidecar模式,一个vue容器,一个img容器,共享存储路径,实现静态资源远端拉取更新。

构建docker镜像
在这个案例中,我们只有git仓库的地址。因此我们首先要克隆代码,然后再执行打包并测试。

[root@k8s-master vue]# git clone https://gitee.com/cuiliang0302/vue3_vite_element-plus.git
[root@k8s-master vue]# docker build -f Dockerfile-vue -t vue:v1 .
[root@k8s-master vue]# docker run -d -p 90:80 vue:v1
09b2ad25f1aa1f7d71f9977d965f82b17edfced266dc3f9287c143fef02dfd9c

image.png
vue镜像测试无误,接下来打包测试拉取图片的img镜像

[root@k8s-master vue]# docker build -f Dockerfile-img -t img:v1 .
[root@k8s-master vue]# docker run -d img:v1
c212f514f52dfa84f2b499c73a684ee2037c2ebf9654b4d37ee2d0bebde3b9a6
root@k8s-master vue]# docker exec -it c212f514f5 sh
/media # cd /media/
/media # ls -lh
total 332K   
-rw-r--r--    1 root     root      329.7K Mar  7 12:17 images.jpg

推送至harbor私有镜像仓库

[root@k8s-master vue]# docker tag vue:v1 192.168.10.100/demo/vue:v1
[root@k8s-master vue]# docker push 192.168.10.100/demo/img:v1
[root@k8s-master vue]# docker tag img:v1 192.168.10.100/demo/img:v1
[root@k8s-master vue]# docker push 192.168.10.100/demo/img:v1

创建configmap资源
我们分别创建两套nginx的配置文件,分别对应开发环境和生产环境。

[root@k8s-master vue]# cat configmap.yaml 
apiVersion: v1
kind: ConfigMap
metadata:
  name: vue-dev
data:
  default.conf: |-
    server {
      listen       80;
      server_name  vue-dev.test.com; # 开发环境域名
      location /img {                # 访问img路径下资源时,重定向到百度页面
          return 301 https://www.baidu.com;
      }
      location / {
          root  /opt/vue/dist;
          index  index.html index.htm;
          try_files $uri $uri/ /index.html;
          add_header Access-Control-Allow-Origin *;
      }
    } 
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: vue
data:
  default.conf: |-
    server {
      listen       80;
      server_name   vue.test.com; # 生产环境域名
      location /img {             # 访问img路径下资源时,从远端获取,保存到/media目录下
          alias /media/;
      }
      location / {
          root  /opt/vue/dist;
          index  index.html index.htm;
          try_files $uri $uri/ /index.html;
          add_header Access-Control-Allow-Origin *;
      }
    }
[root@k8s-master vue]# kubectl apply -f configmap.yaml 
configmap/vue-dev created
configmap/vue created

创建deployment资源
生产和开发环境对应两个不同的控制器,他俩的区别在于使用的configmap不同,在生产模式下,使用sidecar定期从远端获取资源,存放在/media共享路径下。

[root@k8s-master vue]# cat deployment.yaml 
apiVersion: apps/v1
kind: Deployment
metadata:
  name: vue-dev
spec:
  replicas: 1
  selector:
    matchLabels:
      app: vue-dev
  template:
    metadata:
      labels:
        app: vue-dev
    spec:
      containers:
      - name: vue
        image: 192.168.10.100/demo/vue:v1
        resources:
          limits:
            memory: "1Gi"
            cpu: "1"
          requests:
            memory: "128Mi"
            cpu: "100m" 
        ports:
          - containerPort: 80
            name: web
        livenessProbe:
          httpGet:
            port: web
            path: /
        readinessProbe:
          tcpSocket:
            port: web
        volumeMounts:
          - mountPath: /etc/nginx/conf.d/default.conf
            subPath: default.conf
            name: nginx-config
      volumes:  # 使用开发环境的nginx配置文件
        - name: nginx-config
          configMap:
            name:  vue-dev
            
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: vue
spec:
  replicas: 1
  selector:
    matchLabels:
      app: vue
  template:
    metadata:
      labels:
        app: vue
    spec:
      containers:
      - name: vue
        image: 192.168.10.100/demo/vue:v1
        resources:
          limits:
            memory: "1Gi"
            cpu: "1"
          requests:
            memory: "128Mi"
            cpu: "100m" 
        ports:
          - containerPort: 80
            name: web
        livenessProbe:
          httpGet:
            port: web
            path: /
        readinessProbe:
          tcpSocket:
            port: web
        volumeMounts:
          - mountPath: /etc/nginx/conf.d/default.conf
            subPath: default.conf
            name: nginx-config
          - mountPath: /media
            name: media
      - name: img
        image: 192.168.10.100/demo/img:v1
        resources:
          limits:
            memory: "128Mi"
            cpu: "100m" 
        volumeMounts:
          - mountPath: /media
            name: media
      volumes:
        - name:  nginx-config  # 使用生产模式nginx配置文件
          configMap:
            name:  vue
        - name: media          # 挂载空目录,用于存放远端资源
          emptyDir: {}
[root@k8s-master vue]# kubectl apply -f deployment.yaml 
deployment.apps/vue-dev created
deployment.apps/vue created

创建service资源
service资源同样也是两套,唯一不同的区别在于匹配不同的资源标签

[root@k8s-master vue]# cat service.yaml 
apiVersion: v1
kind: Service
metadata:
  name: vue-dev
spec:
  type: ClusterIP 
  selector:
    app: vue-dev 
  ports:
  - name: http
    port: 80
    protocol: TCP 
    targetPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: vue
spec:
  type: ClusterIP 
  selector:
    app: vue
  ports:
  - name: http
    port: 80
    protocol: TCP 
    targetPort: 80
[root@k8s-master vue]# kubectl apply -f service.yaml 
service/vue-dev created
service/vue created

创建ingress资源
ingress资源同样也是两套,对应不同的环境域名

[root@k8s-master vue]# cat ingress.yaml 
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
  name: vue-dev
spec:
  entryPoints:
  - web
  routes:
  - match: Host(`vue-dev.test.com`) # 域名
    kind: Rule
    services:
      - name: vue-dev
        port: 80
---
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
  name: vue
spec:
  entryPoints:
  - web
  routes:
  - match: Host(`vue.test.com`) # 域名
    kind: Rule
    services:
      - name: vue
        port: 80
[root@k8s-master vue]# kubectl apply -f ingress.yaml 
ingressroute.traefik.containo.us/vue-dev created
ingressroute.traefik.containo.us/vue created

访问验证
开发环境,当我们访问http://vue-dev.test.com/img/images.jpg时,会301重定向到百度页面。
image.png
生产环境,当我们访问http://vue.test.com/img/images.jpg时,会从远端服务器拉取资源文件并返回给客户端。
image.png

案例3(python)

image.png
思路分析

  • 安装网络软件包:不能使用Python基础镜像,只能使用centos、ubuntu等系统镜像
  • 定时运行项目:使用cronjob控制器
  • 持久化存储数据:使用nfs、ceph共享存储服务
  • 将日志采集并实时上传到ES:方案1:使用sidecar模式启动filebeat服务,同一pod包含两个container,共用日志目录。方案2:使用hostpath挂载宿主机目录,使用daemonset控制器启动filebeat服务,采集宿主机目录日志。但是此案例中,python程序仅需几秒便可执行完成,而filebeat需要持续运行,使用sidecar模式会导致filebeat还未开始采集日志,python程序已经运行完成,最终导致pod状态码异常,因此采用daemonset方案。

打包镜像并上传

[root@k8s-master python]# cat Dockerfile 
FROM rockylinux:8
RUN dnf -y install wget gcc gcc-c++ net-tools telnet iproute procps tcpdump python38 python38-devel
ADD project /project
RUN pip3.8 install -r /project/requirements.txt -i https://pypi.doubanio.com/simple
WORKDIR /project
CMD ["python3.8","main.py"]
[root@k8s-master python]# docker build -t python:v1 .
[root@k8s-master python]# docker run python:v1
2023-03-07 14:49:33.213 | INFO     | __main__:<module>:26 - 爬虫程序开始执行
2023-03-07 14:49:33.214 | INFO     | __main__:get_data:10 - 获取的数据ID:43
2023-03-07 14:49:40.958 | INFO     | __main__:get_data:14 - 获取的name值:oddish
2023-03-07 14:49:40.965 | INFO     | __main__:save_data:22 - 文件写入完成,文件名:oddish1678200580.txt
2023-03-07 14:49:40.966 | INFO     | __main__:<module>:29 - 爬虫程序执行完成
[root@k8s-master python]# docker tag python:v1 192.168.10.100/demo/python:v1
[root@k8s-master python]# docker push 192.168.10.100/demo/python:v1

创建pvc资源,用于持久化存储爬虫数据

[root@k8s-master python]# cat pvc.yaml 
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: python-pvc
spec:
  storageClassName: nfs-client
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 1Gi
[root@k8s-master python]# kubectl apply -f pvc.yaml 
persistentvolumeclaim/python-pvc created

创建cronjob资源,定时执行爬虫程序

[root@k8s-master python]# cat cronjob.yaml 
apiVersion: batch/v1
kind: CronJob
metadata:
  name: python
spec:
  schedule: "* * * * *"
  jobTemplate:
    metadata:
      name: python
    spec:
      template:
        spec:
          containers:
          - name: python
            image: 192.168.10.100/demo/python:v1
            volumeMounts:
            - name: python-data
              mountPath: "/project/data"
            - name: python-log
              mountPath: "/project/log"
            env:
            - name: TZ
              value: Asia/Shanghai
          restartPolicy: Never
          volumes:
          - name: python-data
            persistentVolumeClaim:
              claimName: python-pvc
          - name: python-log
            hostPath:
              path: /data/python-log
              type: DirectoryOrCreate
[root@k8s-master python]# kubectl apply -f cronjob.yaml 
cronjob.batch/python created

导出es证书
因为es从8开始,安装后默认开启了base基础用户验证和TLS证书。filebeat想要连接ES需要指定SSL证书,否则会报X509证书异常错误。

[root@tiaoban ~]# docker cp elasticsearch:/usr/share/elasticsearch/config/certs .
Successfully copied 21.5kB to /root/.
[root@tiaoban ~]# ls
anaconda-ks.cfg  certs  elasticsearch.yml
[root@tiaoban ~]# cd certs/
[root@tiaoban certs]# ls
http_ca.crt  http.p12  transport.p12
[root@tiaoban certs]# scp http_ca.crt k8s-master:/root

创建secret资源
从docker容器中导出es证书后,创建secret资源,后续filebeat直接挂载证书资源即可。

[root@k8s-master ~]# kubectl create secret generic es --from-file=./http_ca.crt 
secret/es created
[root@k8s-master ~]# kubectl describe secrets es 
Name:         es
Namespace:    default
Labels:       <none>
Annotations:  <none>

Type:  Opaque

Data
====
http_ca.crt:  1915 bytes

创建filebeat的configmap资源

[root@k8s-master python]# cat configmap.yaml 
apiVersion: v1
kind: ConfigMap
metadata:
  name: filebeat-config
data:
  filebeat.yml: |-
    filebeat.inputs:
    - type: log
      enabled: true
      paths:
      - /project/log/*.log
    setup.ilm.enabled:  false #新版本的Filebeat则默认的配置开启了ILM 导致索引的命名规则被ILM策略控制
    setup.template.name:  "python"
    setup.template.pattern:  "python-*"
    setup.template.overwrite:  false
    setup.template.settings:
      index.number_of_shards: 1 #索引分片数
      index.number_of_replicas: 0 #索引副本数
    output.elasticsearch:  #指定ES的配置
      hosts:  ["https://192.168.10.100:9200"]
      username: "elastic"
      password: "HufU-ybqd5aeEvh8xf6y"
      index: "python-%{+yyyy.MM.dd}"
      ssl.certificate_authorities: ["/secret/http_ca.crt"]
[root@k8s-master python]# kubectl apply -f configmap.yaml 
configmap/filebeat-config created

创建daemonset资源,每个节点启动一个filebeat

[root@k8s-master python]# cat daemonset.yaml 
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: filebeat
  labels:
    app: filebeat
spec:
  selector:
    matchLabels:
      app: filebeat
  template:
    metadata:
      labels:
        app: filebeat
    spec:
      containers:
      - name: filebeat
        image: elastic/filebeat:8.6.2
        resources:
          limits:
            memory: "128Mi"
            cpu: "100m" 
          requests:
            memory: "10Mi"
            cpu: "10m" 
        args: ["-c","/etc/filebeat/filebeat.yml","-e"]
        volumeMounts:
        - name: filebeat-config
          mountPath: /etc/filebeat/filebeat.yml
          subPath: filebeat.yml
        - name: python-log
          mountPath: /project/log
        - name: es-cert
          mountPath: /secret
          readOnly: true
      volumes:
      - name: python-log
        hostPath:
          path: /data/python-log
          type: DirectoryOrCreate
      - name: filebeat-config
        configMap:
          name: filebeat-config
      - name: es-cert
        secret:
          secretName: es
[root@k8s-master python]# kubectl apply -f daemonset.yaml 
daemonset.apps/filebeat created

访问kibana查看日志是否成功上传

  • 在index management页面查看index是否成功创建

image.png

  • 建立kibana data views

image.png

  • discover查看index详细数据

image.png
查看nfs目录数据

[root@tiaoban default-python-pvc-pvc-b52722f6-1c82-41cb-9e79-fd7e991c78dd]# pwd
/data/nfs/default-python-pvc-pvc-b52722f6-1c82-41cb-9e79-fd7e991c78dd
[root@tiaoban default-python-pvc-pvc-b52722f6-1c82-41cb-9e79-fd7e991c78dd]# ls -lt
总用量 201
-rw-r--r-- 1 root root 183112 38 23:03 pidgey1678287784.txt
-rw-r--r-- 1 root root 210376 38 23:02 poliwag1678287723.txt
-rw-r--r-- 1 root root 193014 38 23:01 cloyster1678287664.txt
-rw-r--r-- 1 root root  27881 38 23:00 weedle1678287603.txt
-rw-r--r-- 1 root root 243530 38 22:59 pikachu1678287544.txt
-rw-r--r-- 1 root root 278708 38 22:58 drowzee1678287483.txt
-rw-r--r-- 1 root root 182849 38 22:57 ninetales1678287423.txt
-rw-r--r-- 1 root root 234397 38 22:56 vulpix1678287363.txt

Helm部署服务

为什么需要Helm

通过上面几个实践案例,大家肯定会思考这样的问题。部署一个服务,需要创建ConfigMap、PV、PVC、Deployment 、Service、Ingress这一堆资源。

  • 能否统一管理、配置和更新这些分散的 k8s 资源文件,防止服务器到处都是散落的yaml文件,修改配置时先要查找yaml文件
  • 能否分发和复用一套应用模板,每个springboot资源基本都是相同的资源,只是镜像版本和名称不一致
  • 能否将应用的一系列资源当做一个软件包管理,像使用yum一样一键部署安装服务

kubernetes为了解决上述问题,推出了helm。

Helm是什么

Helm 是 Kubernetes 的包管理器。包管理器类似于我们在 Ubuntu 中使用的apt、Centos中使用的yum 或者Python中的 pip 一样,能快速查找、下载和安装软件包。能够将一组K8S资源打包统一管理, 是查找、共享和使用Kubernetes构建的软件的最佳方式。

Helm部署服务

类似于docker hub一样,helm也为我们提供了hub仓库,地址为https://artifacthub.io/。我们只需要浏览helm仓库,找到合适的helm安装包,添加仓库源,然后安装即可。以使用helm部署elk为例,参考文档https://www.cuiliangblog.cn/detail/section/15189467

自定义chart

除了浏览官方的helm仓库安装应用外,自定义服务部署也可以使用helm方式实现,开发者只需要按照 Helm Chart 的格式,将应用所需的资源文件包装起来,通过模版化 (Templating) 的方式将一些可变字段(比如我们之前提到的暴露哪个端口、使用多少副本)暴露给用户,最后将封装好的应用包,就完成了Helm Chart的制作,最后集中存放在统一的仓库中供其他用户浏览下载。参考文档https://www.cuiliangblog.cn/detail/section/15287438

operator部署服务

Operator 模型基于 Kubernetes 中的两个概念结合而成:自定义资源和自定义控制器。要想理解operator,首先就要知道自定义资源和自定义控制器

什么是CRD

CRD(Custom Resource Definitions),也就是自定义K8S资源类型。
当内置的POD、Deployment、Configmap等资源类型不满足需求时,我们就需要扩展k8s,常用方式有三种:

  • 使用CRD自定义资源类型
  • 开发自定义的APIServer(例如HPA)
  • 定制扩展Kubernetes源码(例如腾讯云TKE)

在 Kubernetes中,资源是 Kubernetes API中的一个端点,用于存储一堆特定类型的API对象。它允许我们通过向集群添加更多种类的对象来扩展Kubernetes。添加新种类的对象之后,我们可以像其他任何内置对象一样,使用 kubectl 来访问我们自定义的 API 对象,CRD无须修改Kubernetes源代码就能扩展它支持使用API资源类型。

什么是Controller

Kubernetes 的所有控制器,都有一个控制循环,负责监控集群中特定资源的更改,并确保特定资源在集群里的当前状态与控制器自身定义的期望状态保持一致。
Controller是需要CRD配套开发的程序,它通过Apiserver监听相应类型的资源对象事件,例如:创建、删除、更新等等,然后做出相应的动作,例如一个 Deployment 控制器管控值集群里的一组 Pod ,当你 Kill 掉一个 Pod 。控制器发现定义中期望的Pod数量与当前的数量不匹配,它就会马上创建一个 Pod 让当前状态与期望状态匹配。

什么是Operator

operator 是一种 kubernetes的扩展形式,利用自定义资源对象CRD来管理应用和组件,允许用户以 Kubernetes 的声明式 API 风格来管理应用及服务。operator 定义了一组在 Kubernetes 集群中打包和部署复杂业务应用的方法,operator主要是为解决特定应用或服务关于如何运行、部署及出现问题时如何处理提供的一种特定的自定义方式。

使用Operator方式部署有状态服务

例如MySQLOperator可以实现:

  • 全生命周期设置和维护
  • 使用 oracle 官方发布的 MGR 高可用架构
  • 自动、按需备份
  • 自动故障检测,故障切换和故障恢复

例如ESOperator(ECK)可以实现:

  • 管理和监测集群
  • 证书安全配置
  • 轻松升级至新的版本
  • 扩大或缩小集群容量
  • 更改集群配置

例如KafkaOperator(Strimzi)可以实现:

  • 快速部署集群和安全配置
  • 自动化证书管理
  • 快速集群扩容
  • 快速灾难恢复
  • 支持滚动更新,支持备份以及还原

总结

横向对比

HelmOperator
资源类型标准对象,自定义对象自定义对象
运维能力手工,较弱自动故障恢复及异常处理
设计理念资源模板化,配置分离复杂应用的自动化管理
实现方式传统镜像k8s控制器
实现难度较低偏高
灵活性高度灵活修改YAML文件依赖控制程序,不太灵活

适用场景

方式场景举例
手写YAML部署业务项目、需要频繁需改配置或架构、需要高度灵活性Prometheus、Traefik
Helm部署无状态集群服务、快速部署实验测试环境Redis、Consul、Grafana
Operator部署有状态集群服务MySQL、Kafka、Elasticsearch

资源文件地址

gitee:https://gitee.com/cuiliang0302/blog_demo
github:https://github.com/cuiliang0302/blog_demo

查看更多

微信公众号

微信公众号同步更新,欢迎关注微信公众号第一时间获取最近文章。在这里插入图片描述

博客网站

崔亮的博客-专注devops自动化运维,传播优秀it运维技术文章。更多原创运维开发相关文章,欢迎访问https://www.cuiliangblog.cn

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值