云原生翘楚KubeSphere 和 知名开源项目 Pig 最佳实践

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
传统型负载均衡 CLB,每月750个小时 15LCU
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
简介: 云原生翘楚KubeSphere 和 知名开源项目Pig 爱的火花。

一、前言

近年来,为了满足越来越复杂的业务需求,我们从传统单体架构系统升级为微服务架构,就是把一个大型应用程序分割成可以独立部署的小型服务,每个服务之间都是松耦合的,通过 RPC 或者是 Rest 协议来进行通信,可以按照业务领域来划分成独立的单元。但是微服务系统相对于以往的单体系统更为复杂,当业务增加时,服务也将越来越多,服务的频繁部署、监控将变得复杂起来,尤其在上了 k8s 以后会更加复杂。那么有没有一款全栈的容器云平台来帮我们解决这些问题哩?那当然是有的,下面我们一起来揭秘一下吧。

二、介绍

KubeSphere

KubeSphere (https://kubesphere.io)是在 Kubernetes 之上构建的开源容器平台,提供全栈的 IT 自动化运维的能力,简化企业的 DevOps 工作流。

Pig

Pig (https://gitee.com/log4j/pig) 是一个基于 Spring Boot 2.7、 Spring Cloud 2021 & Alibaba、 SAS OAuth2 的开源微服务开发平台,也是微服务最佳实践。在国内拥有大量拥护者,同时也有商业版本提供技术支持。

三、环境搭建

四、架构设计

4.1 KubeSphere 架构

KubeSphere 将 前端 与 后端 分开,实现了面向云原生的设计,后端的各个功能组件可通过 REST API 对接外部系统。KubeSphere 无底层的基础设施依赖,可以运行在任何 Kubernetes、私有云、公有云、VM 或物理环境(BM)之上。此外,它可以部署在任何 Kubernetes 发行版上。如下所示:

该图来自 KubeSphere 官网架构说明。

4.2 Pig 架构

Pig 平台设计灵活可扩展、可移植、可应对高并发需求。同时兼顾本地化、私有云、公有云部署,支持 SaaS 模式应用。如下所示:

该图来自 Pig 白皮书中的基础架构图。

4.3 整体架构图

其实就是将原架构加上一层 Ingress,在 KubeSphere 中对应的是应用路由(ingress 路由规则)和项目网关(Ingress Controller)。如下所示:

4.4 整体容器化部署流程图

运维人员可通过 KubeSphere 来管理服务,也可以利用 KubeSphere 中的 Jenkins 来发布。如下所示:

五、部署过程

分别创建两条流水线,一条用于构建 Pig 后端 Java 代码,另外一条用于构建基于 Vue 的 Pig-ui 前端代码。

5.1 创建企业空间

为项目创建一个名称为 pig-workspace 的企业空间,企业空间是一个组织您的项目和 DevOps 项目、管理资源访问权限以及在团队内部共享资源等的逻辑单元,可以作为团队工作的独立工作空间。

5.2 创建 DevOps 项目

DevOps 项目是一个独立的命名空间,其中定义了一组流水线。用户可以按照自己的方式对流水线进行分组(例如:项目类型、组织类型)。

5.3 创建项目

项目用于对资源进行分组管理和控制不同用户的资源管理权限。

5.4 部署 MySql

1)进入应用商店,在应用分类中选择数据库和缓存,找到 Mysql。如下所示:

2)在基本信息中,填写应用名称 pig-mysql,并选择位置,进行下一步。如下所示:

3)在应用配置中,编辑 yaml 文件,将镜像改为 mysql/mysql-server:8.0.30,将密码设置为 root。如下所示:

mysql 镜像采用 pig 项目 db 下 Dockerfile 中的版本,也可自己指定。

4)点击安装

5)进入 pig-mysql 服务,编辑外部访问,从而访问 mysql 导入 pig 的数据

6)进入 mysql 容器,调整帐号允许从远程登陆

登录 mysql 进行授权操作

$ mysql -uroot -proot
$ use mysql;
$ update user set host='%' where user='root';
$ flush privileges;
$ ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY 'root';
$ flush privileges;

7)利用 Navicat 客户端连接 pig-mysql 服务,导入数据

5.5 部署 Redis

1)进入应用商店,在应用分类中选择数据库和缓存,找到 Redis。如下所示:

注:Pig 中默认使用无密码模式,因此可以暂时留空。生产环境不推荐将密码设置为空

2)安装成功后,如下所示:

5.6 创建凭证

Pig 所依赖的后端微服务为无状态服务。利用 KubeSphere 服务创建 DevOps 流水线项目来部署这些微服务。

1)创建 kubeconfig 凭证,如下所示:

名称自定义,需要和 Jenkinsfile 中的一致即可,内容默认或者去/root/.kube 下复制。


2)创建 harbor 凭证,如下所示:

名称自定义,需要和 Jenkinsfile 中的一致即可。

3)创建 gitlab 凭证,如下所示:

名称自定义,需要和 Jenkinsfile 中的一致即可。

全部凭证如下:

5.7 设置 harbor 镜像仓库

新建一个 pig-dev 项目,如下所示:

5.8 部署 Pig 后端无状态服务

1)新建 pig 后端流水线,如下所示:

选择代码仓库

编辑设置

2)代码中创建 Jenkinsfile 文件:

内容如下:

pipeline {
    agent {
        label 'maven'
    }
    parameters {
        choice(choices: ['dev', 'test', 'pre', 'pre2','prod'], name: 'Environments', description: '请选择要发布的环境:dev开发环境、test测试环境、pre预发布环境、pre2灰度环境、prod 生产环境')
        choice(choices: ['pig-gateway', 'pig-auth', 'pig-register', 'pig-upms-biz','pig-codegen', 'pig-monitor', 'pig-sentinel-dashboard', 'pig-xxl-job-admin','all'], name: 'ServicesDeploy', description: '请选择要构建的服务,支持单个服务发布或全部服务发布')
        choice(choices: ['no', 'yes'], name: 'sonarQube', description: '是否进行sonarQube代码质量检查,默认为no')
        string(name: 'MultiServicesBuild', defaultValue: 'no', description: '自由组合发布服务,如填写pig-gateway,pig-auth等,默认此项不生效,和ServicesDeploy只能选其一')
    }
    environment {
        HARBOR_CREDENTIAL_ID = 'harbor-id'
        GITLAB_CREDENTIAL_ID = 'gitlab'
        KUBECONFIG_CREDENTIAL_ID = 'pig-kubeconfig'
        REGISTRY = 'ip:端口'//harbor镜像仓库
        HARBOR_NAMESPACE = 'pig-dev'
        K8S_NAMESPACE = 'pig-dev'
    }
    stages {
        stage ('拉取代码') {
            steps {
                checkout(scm)
            }
        }
  stage('初始化变量') {
    agent none
    steps {
   container('maven') {
     script {
    //自由组合发布
    if("${params.MultiServicesBuild}".trim() != "no") {
      ServicesBuild = "${params.MultiServicesBuild}".split(",")
      for (service in ServicesBuild) {
     println "now got ${service}"
      }
    }else if("${params.ServicesDeploy}".trim() == "all"){
     ServicesBuildStr = 'pig-gateway,pig-auth,pig-register,pig-upms-biz,pig-codegen,pig-monitor,pig-sentinel-dashboard,pig-xxl-job-admin'
     ServicesBuild = "${ServicesBuildStr}".split(",")
    }else if("${params.ServicesDeploy}".trim() != "all"){
     ServicesBuild = "${params.ServicesDeploy}".split(",")
    }
     }
   }
    }
     }
        stage('sonarQube代码质量检查') {
            steps {
                script {
                    if("${params.sonarQube}".trim() == "yes") {
                       for (service in ServicesBuild) {
                          def workspace = "pig-"
                          println "当前进行代码质量检查是:${service}"
                          if("${service}".trim() == "pig-gateway" || "${service}".trim() == "pig-auth" || "${service}".trim() == "pig-register"){
                              workspace = "${workspace}" + "${service}".trim().split("-")[1]
                           }
                          if("${service}".trim() == "pig-codegen" || "${service}".trim() == "pig-monitor" || "${service}".trim() == "pig-sentinel-dashboard" || "${service}".trim() == "pig-xxl-job-admin"){
                              workspace = "pig-visual/" + "${service}".trim()
                          }
                           if("${service}".trim() == "pig-upms-biz"){
                               workspace = "pig-upms/" + "${service}".trim()
                           }
                           //定义当前Jenkins的SonarQubeScanner工具
                           scannerHome = tool 'sonar-scanner'
                           //引用当前JenkinsSonarQube环境
                           withSonarQubeEnv('sonarqube9.4') {
                               sh """
                               cd ${workspace}
                               ${scannerHome}/bin/sonar-scanner
                           """
                           }
                       }
                    }else{
                        println "是no,跳过sonarQube代码质量检查"
                    }
                }
            }
        }
  stage('打包') {
   agent none
   steps {
    container('maven') {
      script {
                    sh "mvn -Dmaven.test.skip=true clean package -P${params.Environments}"
      }
    }
   }
  }
  stage('构建镜像') {
   agent none
   steps {
    container('maven') {
      script {
                    for (service in ServicesBuild) {
                      def workspace = "pig-"
                      println "当前构建的镜像是:${service}"
                      stage ("build ${service}") {
                          if("${service}".trim() == "pig-gateway" || "${service}".trim() == "pig-auth" || "${service}".trim() == "pig-register"){
                              workspace = "${workspace}" + "${service}".trim().split("-")[1]
                           }
                          if("${service}".trim() == "pig-codegen" || "${service}".trim() == "pig-monitor" || "${service}".trim() == "pig-sentinel-dashboard" || "${service}".trim() == "pig-xxl-job-admin"){
                              workspace = "pig-visual/" + "${service}".trim()
                          }
                           if("${service}".trim() == "pig-upms-biz"){
                               workspace = "pig-upms/" + "${service}".trim()
                           }
                        sh "cd ${workspace} && docker build -f Dockerfile -t $REGISTRY/$HARBOR_NAMESPACE/${service}:$BUILD_NUMBER ."
                      }
                    }
      }
    }
   }
  }
  stage('镜像推送') {
   agent none
   steps {
    container('maven') {
      script {
     for (service in ServicesBuild) {
       println "当前推送的镜像是:${service}"
       stage ("push ${service}") {
      withCredentials([usernamePassword(passwordVariable : 'HARBOR_PASSWORD' ,usernameVariable : 'HARBOR_USERNAME' ,credentialsId : "$HARBOR_CREDENTIAL_ID" ,)]) {
       sh 'echo "$HARBOR_PASSWORD" | docker login $REGISTRY -u "$HARBOR_USERNAME" --password-stdin'
       sh "docker push  $REGISTRY/$HARBOR_NAMESPACE/${service}:$BUILD_NUMBER"
      }
       }
     }
      }
    }
   }
  }
  stage('推送镜像之latest') {
   agent none
   steps {
    container('maven') {
      script {
     for (service in ServicesBuild) {
       println "当前推送的latest镜像是:${service}"
       stage ("pushLatest ${service}") {
      sh "docker tag  $REGISTRY/$HARBOR_NAMESPACE/${service}:$BUILD_NUMBER $REGISTRY/$HARBOR_NAMESPACE/${service}:latest"
      sh "docker push  $REGISTRY/$HARBOR_NAMESPACE/${service}:latest"
       }
     }
      }
    }
   }
  }
  stage('部署到dev环境') {
    steps {
     container ('maven') {
       script {
      for (service in ServicesBuild) {
                       //自定义的全局变量,也就是整个流水线可以去使用
                       env.APP_NAME = "${service}"
                       if("${service}".trim() == "pig-gateway") {
                          env.NODEPORT = 31201
                          env.PORT = 9999
                        }
                       if("${service}".trim() == "pig-auth") {
                          env.NODEPORT = 31202
                          env.PORT = 3000
                        }
                       if("${service}".trim() == "pig-register") {
                          env.NODEPORT = 31203
                          env.PORT = 8848
                        }
                       if("${service}".trim() == "pig-upms-biz") {
                          env.NODEPORT = 31204
                          env.PORT = 4000
                        }
                       if("${service}".trim() == "pig-codegen") {
                          env.NODEPORT = 31205
                          env.PORT = 5002
                        }
                       if("${service}".trim() == "pig-monitor") {
                          env.NODEPORT = 31206
                          env.PORT = 5001
                        }
                       if("${service}".trim() == "pig-sentinel-dashboard") {
                          env.NODEPORT = 31207
                          env.PORT = 5003
                        }
                       if("${service}".trim() == "pig-xxl-job-admin") {
                          env.NODEPORT = 31208
                          env.PORT = 5004
                        }
        stage ("deploy ${service}") {
      println "即将部署的服务是 $APP_NAME"
         withCredentials([
          kubeconfigFile(
          credentialsId: env.KUBECONFIG_CREDENTIAL_ID,
          variable: 'KUBECONFIG')
          ]) {
                                   if("${service}".trim() == "pig-register") {
                                       sh "envsubst < deploy/${params.Environments}/nacos-devops.yaml | kubectl apply -f -"
                                   }else{
                                       sh "envsubst < deploy/${params.Environments}/devops.yaml | kubectl apply -f -"
                                   }
         }
        }
      }
       }
     }
    }
  }
 }
}

通过${service}来判断最终选择那个 deploy 来部署。

3)代码中创建 devops.yaml 部署文件:

内容如下:

---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: $APP_NAME
  name: $APP_NAME
  namespace: $K8S_NAMESPACE
spec:
  progressDeadlineSeconds: 600
  replicas: 1
  selector:
    matchLabels:
      app: $APP_NAME
  template:
    metadata:
      labels:
        app: $APP_NAME
    spec:
      containers:
        - image: $REGISTRY/$HARBOR_NAMESPACE/$APP_NAME:$BUILD_NUMBER
          imagePullPolicy: Always
          name: $APP_NAME
          ports:
            - containerPort: $PORT
              protocol: TCP
          terminationMessagePath: /dev/termination-log
          terminationMessagePolicy: File
      dnsPolicy: ClusterFirst
      restartPolicy: Always
      terminationGracePeriodSeconds: 30
---
apiVersion: v1
kind: Service
metadata:
  labels:
    app: $APP_NAME
  name: $APP_NAME
  namespace: $K8S_NAMESPACE
spec:
  ports:
    - name: http
      port: $PORT
      protocol: TCP
      targetPort: $PORT
      nodePort: $NODEPORT
  selector:
    app: $APP_NAME
  sessionAffinity: None
  type: NodePort

4)代码中创建 nacos-devops.yaml 部署文件:

由于 pig-register 服务是 nacos 服务,其 k8s 的 yaml 部署应该和其他服务不同,采用 StatefulSet 来部署且副本数为 3,,并添加相对应的端口。

内容如下:

---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  labels:
    app: $APP_NAME
  name: $APP_NAME
  namespace: $K8S_NAMESPACE
spec:
  serviceName: $APP_NAME
  replicas: 3
  selector:
    matchLabels:
      app: $APP_NAME
  template:
    metadata:
      labels:
        app: $APP_NAME
      annotations:
        pod.alpha.kubernetes.io/initialized: "true"
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            - labelSelector:
                matchExpressions:
                  - key: "app"
                    operator: In
                    values:
                      - nacos
              topologyKey: "kubernetes.io/hostname"
      containers:
        - image: $REGISTRY/$HARBOR_NAMESPACE/$APP_NAME:$BUILD_NUMBER
          imagePullPolicy: Always
          name: $APP_NAME
          ports:
            - containerPort: 8848
              name: client-port
            - containerPort: 9848
              name: client-rpc
            - containerPort: 9849
              name: raft-rpc
            - containerPort: 7848
              name: old-raft-rpc
          terminationMessagePath: /dev/termination-log
          terminationMessagePolicy: File
      dnsPolicy: ClusterFirst
      restartPolicy: Always
---
apiVersion: v1
kind: Service
metadata:
  labels:
    app: $APP_NAME
  name: $APP_NAME
  namespace: $K8S_NAMESPACE
  annotations:
    service.alpha.kubernetes.io/tolerate-unready-endpoints: "true"
spec:
  ports:
    - port: 8848
      protocol: TCP
      name: server
      targetPort: 8848
      nodePort: $NODEPORT
    - port: 9848
      name: client-rpc
      targetPort: 9848
    - port: 9849
      name: raft-rpc
      targetPort: 9849
    ## 兼容1.4.x版本的选举端口
    - port: 7848
      name: old-raft-rpc
      targetPort: 7848
  selector:
    app: $APP_NAME
  sessionAffinity: None
  type: NodePort

后续这些文件都可可采用共享仓库来统一管理。5)发布

由于 pig-gateway,pig-auth,pig-upms-biz 等其它服务都是依赖 nacos(pig-register)服务的,所以我们先发布 pig-register 服务。

进入 DevOps 项目->pig-dev->pig-backend-dev->运行

这里 KubeSphere3.3.0 版本中有个 bug,choice 类型的还是识别为 string,所以暂时只能手动输入。此 bug 在 3.3.1 版本可修复。

查看任务状态

查看日志

在项目->应用负载->服务下查看刚发布的 pig-register 服务

发布其它服务

进入 DevOps 项目->pig-dev->pig-backend-dev->运行->选择自由组合发布

发布完成查看服务

至此已经完成 Pig 后端无状态服务的部署。

5.9 部署 Pig 前端无状态服务

1)新建 pig 前端流水线,如下所示:

选择代码仓库

编辑设置

2)代码中创建 Jenkinsfile 文件:

内容如下:

pipeline {
  agent {
    node {
      label 'nodejs'
    }
  }
    parameters {
        choice(choices: ['dev', 'test', 'pre', 'pre2','prod'], name: 'Environments', description: '请选择要发布的环境:dev开发环境、test测试环境、pre预发布环境、pre2灰度环境、prod 生产环境')
        choice(choices: ['no', 'yes'], name: 'sonarQube', description: '是否进行sonarQube代码质量检查,默认为no')
    }
    environment {
        HARBOR_CREDENTIAL_ID = 'harbor-id'
        GITLAB_CREDENTIAL_ID = 'gitlab'
        KUBECONFIG_CREDENTIAL_ID = 'pig-kubeconfig'
        REGISTRY = 'ip:端口'//harbor镜像仓里
        HARBOR_NAMESPACE = 'pig-dev'
        APP_NAME = 'pig-front'
        K8S_NAMESPACE = 'pig-dev'
    }
    stages {
        stage ('拉取代码') {
            steps {
                container('nodejs') {
                   checkout(scm)
                }
            }
        }
        stage('sonarQube代码质量检查') {
            steps {
                script {
                    if("${params.sonarQube}".trim() == "yes") {
                       println "当前进行代码质量检查是:${APP_NAME}"
                       //定义当前Jenkins的SonarQubeScanner工具
                       scannerHome = tool 'sonar-scanner'
                       //引用当前JenkinsSonarQube环境
                       withSonarQubeEnv('sonarqube9.4') {
                           sh """
                           cd .
                           ${scannerHome}/bin/sonar-scanner
                       """
                       }
                    }else{
                        println "是no,跳过sonarQube代码质量检查"
                    }
                }
            }
        }
        stage('项目编译') {
            agent none
            steps {
                container('nodejs') {
                    sh 'node -v'
                    sh 'npm -v'
                    sh 'npm install'
                    sh 'npm run build:docker'
                    sh 'ls'
                }
            }
        }
        stage('构建镜像') {
            agent none
            steps {
                container('nodejs') {
                    sh 'ls'
                    sh 'cd ./docker && docker  build  -t $REGISTRY/$HARBOR_NAMESPACE/$APP_NAME:$APP_NAME-$BUILD_NUMBER .'
                }
            }
        }
        stage('镜像推送') {
            agent none
            steps {
                container('nodejs') {
                    withCredentials([usernamePassword(passwordVariable : 'HARBOR_PASSWORD' ,usernameVariable : 'HARBOR_USERNAME' ,credentialsId : "$HARBOR_CREDENTIAL_ID" ,)]) {
                        sh 'echo "$HARBOR_PASSWORD" | docker login $REGISTRY -u "$HARBOR_USERNAME" --password-stdin'
                        sh 'docker push  $REGISTRY/$HARBOR_NAMESPACE/$APP_NAME:$APP_NAME-$BUILD_NUMBER'
                    }
                }
            }
        }
        stage('推送镜像之latest') {
            agent none
            steps {
                container('nodejs') {
                  sh 'docker tag  $REGISTRY/$HARBOR_NAMESPACE/$APP_NAME:$APP_NAME-$BUILD_NUMBER $REGISTRY/$HARBOR_NAMESPACE/$APP_NAME:latest '
                  sh 'docker push  $REGISTRY/$HARBOR_NAMESPACE/$APP_NAME:latest '
                }
            }
        }
         stage('部署到dev环境') {
              steps {
                  container ('nodejs') {
                       withCredentials([
                           kubeconfigFile(
                           credentialsId: env.KUBECONFIG_CREDENTIAL_ID,
                           variable: 'KUBECONFIG')
                           ]) {
                           sh "envsubst < deploy/${params.Environments}/devops.yaml | kubectl apply -f -"
                       }
                  }
              }
         }
    }
}

3)代码中创建 devops.yaml 部署文件:

内容如下:

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: $APP_NAME
  name: $APP_NAME
  namespace: $K8S_NAMESPACE
spec:
  progressDeadlineSeconds: 600
  replicas: 1
  selector:
    matchLabels:
      app: pig-front
  strategy:
    rollingUpdate:
      maxSurge: 50%
      maxUnavailable: 50%
    type: RollingUpdate
  template:
    metadata:
      labels:
        app: pig-front
    spec:
      containers:
        - image: $REGISTRY/$HARBOR_NAMESPACE/$APP_NAME:$APP_NAME-$BUILD_NUMBER
          imagePullPolicy: Always
          name: pig-front-end
          ports:
            - containerPort: 80
              protocol: TCP
          terminationMessagePath: /dev/termination-log
          terminationMessagePolicy: File
      dnsPolicy: ClusterFirst
      restartPolicy: Always
      terminationGracePeriodSeconds: 30
---
apiVersion: v1
kind: Service
metadata:
  labels:
    app: $APP_NAME
  name: $APP_NAME
  namespace: $K8S_NAMESPACE
spec:
  ports:
    - name: http
      port: 80
      protocol: TCP
      targetPort: 80
      nodePort: 31200
  selector:
    app: $APP_NAME
  sessionAffinity: None
  type: NodePort

后续这些文件都可可采用共享仓库来统一管理。

4)发布

这里 KubeSphere3.3.0 版本中有个 bug,choice 类型的还是识别为 string,此 bug 在 3.3.1 版本可修复。

查看任务状态

查看日志

在项目->应用负载->服务下查看刚发布的 pig-front 服务

至此所有的服务均已发布完成

利用KubeSphere中Jenkins发布

访问ip:30180     账号:admin 密码:P@88w0rd

六、通过 NodePort 方式暴露集群内部容器服务

NodePort 设计之初就不建议用于生产环境暴露服务,所以默认端口都是一些大端口,如下:

输入 node ip + 31200 访问

七、优化和改进

7.1 通过探针优雅的解决部署过程中服务平滑过渡问题

若是只有一个副本的情况下,新的 pod 启动成功时,开始停掉旧的 pod,但是我们看到的 running 状态,并不以为着我们的服务是正常的。若是这个时候杀死旧的 pod,那么将有新的 pod 接受请求,这个时候会出现服务短暂不可用状态,所以需要增加探来确保我们的服务已经正常了,可以接收并处理用户请求。我们常用的探针如下:

livenessProbe:存活性探测

许多应用程序经过长时间运行,最终过渡到无法运行的状态,除了重启,无法恢复。通常情况下,K8S 会发现应用程序已经终止,然后重启应用程序 pod。有时应用程序可能因为某些原因(后端服务故障等)导致暂时无法对外提供服务,但应用软件没有终止,导致 K8S 无法隔离有故障的 pod,调用者可能会访问到有故障的 pod,导致业务不稳定。K8S 提供 livenessProbe 来检测容器是否正常运行,并且对相应状况进行相应的补救措施。

readinessProbe:就绪性探测

在没有配置 readinessProbe 的资源对象中,pod 中的容器启动完成后,就认为 pod 中的应用程序可以对外提供服务,该 pod 就会加入相对应的 service,对外提供服务。但有时一些应用程序启动后,需要较长时间的加载才能对外服务,如果这时对外提供服务,执行结果必然无法达到预期效果,影响用户体验。比如使用 tomcat 的应用程序来说,并不是简单地说 tomcat 启动成功就可以对外提供服务的,还需要等待 spring 容器初始化,数据库连接上等等。

1)SpringBoot 的 actuator

其实 actuator 是用来帮助用户监控和操作 SprinBoot 应用的,这些监控和操作都可以通过 http 请求实现,如下图,http://localhost:7777/actuator/health地址返回的是应用的健康状态。

需引以下 maven:

<!-- 引入Actuator监控依赖 -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

在 SpringBoot-2.3 版本中,actuator 新增了两个地址:/actuator/health/liveness 和/actuator/health/readiness,前者用作 kubernetes 的存活探针,后者用作 kubernetes 的就绪探针,需要先在配置文件中开启,如下:

management:
  endpoint:
    health:
      probes:
        enabled: true
  health:
    livenessstate:
      enabled: true
    readinessstate:
      enabled: true

/actuator/health/和/actuator/health/是默认开启的。

利用 SpringBoot 的接口来作为容器探针的健康检测,按照如下就可以:

readinessProbe:
initialDelaySeconds: 20
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 6
httpGet:
  scheme: HTTP
  port: 9999
  path: /actuator/health/readiness
livenessProbe:
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 6
httpGet:
  scheme: HTTP
  port: 9999
  path: /actuator/health/liveness

2)pig 后端项目增加探针

pig 项目全局所有的模块都会引入 Actuator 监控依赖,如下:

<!--监控-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

调整 pig 项目后端 devops.yaml,增加以下内容:

...
          readinessProbe:
            initialDelaySeconds: 30
            periodSeconds: 10
            timeoutSeconds: 5
            httpGet:
              scheme: HTTP
              port: $PORT
              path: /actuator/health
          livenessProbe:
            initialDelaySeconds: 40
            periodSeconds: 10
            timeoutSeconds: 5
            httpGet:
              scheme: HTTP
              port: $PORT
              path: /actuator/health
...

完整 devops.yaml 内容如下:

---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: $APP_NAME
  name: $APP_NAME
  namespace: $K8S_NAMESPACE
spec:
  progressDeadlineSeconds: 600
  replicas: 1
  selector:
    matchLabels:
      app: $APP_NAME
  template:
    metadata:
      labels:
        app: $APP_NAME
    spec:
      containers:
        - image: $REGISTRY/$HARBOR_NAMESPACE/$APP_NAME:$BUILD_NUMBER
          imagePullPolicy: Always
          name: $APP_NAME
          ports:
            - containerPort: $PORT
              protocol: TCP
          terminationMessagePath: /dev/termination-log
          terminationMessagePolicy: File
          readinessProbe:
            initialDelaySeconds: 90
            periodSeconds: 10
            timeoutSeconds: 5
            failureThreshold: 3
            httpGet:
              scheme: HTTP
              port: $PORT
              path: /actuator/health
          livenessProbe:
            initialDelaySeconds: 100
            periodSeconds: 10
            timeoutSeconds: 5
            failureThreshold: 3
            httpGet:
              scheme: HTTP
              port: $PORT
              path: /actuator/health
      dnsPolicy: ClusterFirst
      restartPolicy: Always
      terminationGracePeriodSeconds: 30
---
apiVersion: v1
kind: Service
metadata:
  labels:
    app: $APP_NAME
  name: $APP_NAME
  namespace: $K8S_NAMESPACE
spec:
  ports:
    - name: http
      port: $PORT
      protocol: TCP
      targetPort: $PORT
      nodePort: $NODEPORT
  selector:
    app: $APP_NAME
  sessionAffinity: None
  type: NodePort

注:/actuator/health/readiness 和/actuator/health/liveness 也可以用,需在配置文件中开启。若是内存、CPU 限制过低,需要调整 initialDelaySeconds 时间,否则会出现还未启动成功,就开始探测,会进入循环,直到探测失败(就是 failureThreshold 定义的次数),要掌握好这个时间的度。

重新发布 pig-geteway 测试

正在进行探测

探测成功,新的 pod 可用,旧的 pod 删除

3)pig 前端项目增加探针

前端项目我们直接探测 nginx 的端口即可。调整 devops.yaml,增加如下内容:

readinessProbe:
            initialDelaySeconds: 20
            periodSeconds: 10
            timeoutSeconds: 5
            httpGet:
              scheme: HTTP
              port: 80
              path: /
          livenessProbe:
            initialDelaySeconds: 30
            periodSeconds: 10
            timeoutSeconds: 5
            httpGet:
              scheme: HTTP
              port: 80
              path: /

完整 devops.yaml 如下:

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: $APP_NAME
  name: $APP_NAME
  namespace: $K8S_NAMESPACE
spec:
  progressDeadlineSeconds: 600
  replicas: 1
  selector:
    matchLabels:
      app: pig-front
  strategy:
    rollingUpdate:
      maxSurge: 50%
      maxUnavailable: 50%
    type: RollingUpdate
  template:
    metadata:
      labels:
        app: pig-front
    spec:
      containers:
        - image: $REGISTRY/$HARBOR_NAMESPACE/$APP_NAME:$APP_NAME-$BUILD_NUMBER
          imagePullPolicy: Always
          name: pig-front-end
          ports:
            - containerPort: 80
              protocol: TCP
          terminationMessagePath: /dev/termination-log
          terminationMessagePolicy: File
          readinessProbe:
            initialDelaySeconds: 20
            periodSeconds: 10
            timeoutSeconds: 5
            httpGet:
              scheme: HTTP
              port: 80
              path: /
          livenessProbe:
            initialDelaySeconds: 30
            periodSeconds: 10
            timeoutSeconds: 5
            httpGet:
              scheme: HTTP
              port: 80
              path: /
      dnsPolicy: ClusterFirst
      restartPolicy: Always
      terminationGracePeriodSeconds: 30
---
apiVersion: v1
kind: Service
metadata:
  labels:
    app: $APP_NAME
  name: $APP_NAME
  namespace: $K8S_NAMESPACE
spec:
  ports:
    - name: http
      port: 80
      protocol: TCP
      targetPort: 80
      nodePort: 31200
  selector:
    app: $APP_NAME
  sessionAffinity: None
  type: NodePort



7.2 通过探针优雅的解决服务部署过程中注册中心服务平滑过渡问题

我们后端采用 Spring Cloud(Spring Cloud Alibaba)微服务结构技术路线进行开发,采用 nacos 作为注册中心。而服务注册到 nacos 是需要时间的。而一般的容器探测只是探测服务是否达到了可用的状态,没有考虑到注册到注册中心的服务是否可用,其实在多副本的情况下只要控制好滚动更新策略,应该不会出现这种情况的。在 nacos 中也是有负载均衡的,nacos 实现负载均衡是通过内置的 Ribbon 实现的。像 gateway 网关服务尤其重要,因为它还要负责服务转发。所以保证这种服务的可用性也变得尤为重要。例如单副本 gateway 网关服务,若是在开始探测时,网关服务并没有及时注册到注册中心里,这个时候开始测探,服务本身是可用的,那么探测成功后,新的 pod 变成了 running 状态,便停掉了第一个 pod,而注册中心中的服务其实并不可用,会出现短暂服务不可用。其实健康探测我们还可以往前走一步,把服务成功注册到注册中心中作为服务可用的状态。

自己编写容器探针接口

/**
 * @author 小盒子
 * @version 1.0
 * @description: 容器探针接口,用来进行探测服务是否注册进入注册中心
 * @date 2022/10/22 10:58
 */
@Slf4j
@RestController
@RequestMapping("nacos")
public class HealthController {
    @Autowired
    private DiscoveryClient discoveryClient;
    @GetMapping("/health/{services}")
    public ResponseEntity<Object> getService(@PathVariable("services")String services) throws NacosException {
        //从nacos中根据serverId获取实例 方法一(采用此方法,DiscoveryClient 代表的就是:服务发现操作对象)
        if (StringUtils.isBlank(services)){
            return new ResponseEntity<Object>(HttpStatus.BAD_REQUEST);
        }
        List<ServiceInstance> instances = discoveryClient.getInstances(services);
        Map<String, Integer> ipMap = new HashMap<>();
        if (instances.size() > 0){
            instances.forEach(key ->{
                ipMap.put(key.getHost(), key.getPort());
            });
        }
        //从nacos中根据serverId获取实例 方法二 采用nacos的java的SDK
//        Properties properties = new Properties();
//        properties.put("serverAddr", "192.168.4.25:8849");
//        properties.put("namespace", "caseretrieval-dev");
//        NamingService naming = NamingFactory.createNamingService(properties);
//        List<Instance> instancesList = naming.selectInstances("case-gateway", true);
        //从nacos中根据serverId获取实例 方法三,采用nacos的OPEN-api
        //http://192.168.4.25:8849/nacos/v1/ns/instance/list?serviceName=case-gateway&namespaceId=caseretrieval-dev
        //获取本机IP地址
        String hostAddress = "127.0.0.1";
        try {
            InetAddress localHost = InetAddress.getLocalHost();
            hostAddress = localHost.getHostAddress();
            log.info("当前服务本机ip是:"+hostAddress);
        } catch (UnknownHostException e) {
            log.error(e.getMessage(), e);
        }
        //查看本机服务是否已经注册到nacos中
        if (ipMap.containsKey(hostAddress)){
            return new ResponseEntity<Object>(HttpStatus.OK);
        }else {
            return new ResponseEntity<Object>(HttpStatus.SERVICE_UNAVAILABLE);
        }
    }
}

其实正常情况下,合理的滚动更新策略,加上就绪性探测、存活性探测或者启动探测就能达到目标了,也不必这么麻烦。

7.3 通过 Ingress 方式暴露集群内部容器服务

如果需要从集群外部访问服务,即将服务暴露给用户使用,KubernetesService 本身提供了两种方式,一种是 NodePort ,另外一种是 LoadBalancer 。另外 Ingress 也是一种常用的暴露服务的方式。可以参考以下这篇文章。

青云 LB(负载均衡)与 k8s 实战(二)

KubeSphere 的项目网关就是 IngressController ,是一个七层负载均衡调度器,客户端的请求先到达这个七层负载均衡调度器,KubeSphere 中的应用路由就是 Ingress ,是定义规则的。数据包走向如下图流程所示:

1)开启项目网关

对项目中的外网访问网关以及服务治理等配置进行设置和管理。

KubeSphere 中的项目网关,开启后会自动安装 IngressController。

对应在 k8s 集群中 svc 如下图所示:

KubeSphere 中应用路由是需要手动创建的,如下图所示:

2)设置路由规则:

指定自定义域名并通过本地 hosts 文件或 DNS 服务器将域名解析为网关 IP 地址。

3)访问 http://pig.com:32576/

7.4 将 SonarQube 集成到流水线

SonarQube 是一种主流的代码质量持续检测工具。您可以将其用于代码库的静态和动态分析。SonarQube 集成到 KubeSphere(Jenkins) 流水线后,如果在运行的流水线中检测到问题,您可以直接在仪表板上查看常见代码问题,例如 Bug 和漏洞。在日常开发中SonarQube往往是基于现有的Gitlab、Jenkins 集成配合使用的,以便在项目拉取代码后进行代码审查。若是存在多套环境的情况下,例如有开发环境、测试环境、生产环境等,我是不建议在生产环境中去进行代码审查的,一来进行重复的代码审查没有意义,二来比较浪费时间,所以建议只在开发环境开启代码审查即可。

在Jenkins中集成SonarQube

微服务项目中需要在每个项目下都建立sonar-project.properties文件,在Jenkins的脚本中需要循环去checking每个微服务。

  1. 在sonarQube中生成token

  1. 在Jenkins中安装SonarQube Scanner插件

  1. 在Jenkins中添加SonarQube凭证

选择Secret text 作为凭证类型。

完成后如下所示:

  1. 在Jenkins系统配置中配置SonarQube

  1. 在Jenkins全工具中安装 SonarQube Scanner

sonar-scanner 名称自定义,在jenkinsfile中要保持一直。

  1. 关闭审查结果上传 SCM 功能

后端代码审查

  1. 调整Jenkins脚本

微服务项目中需要在每个项目下都建立sonar-project.properties文件,在Jenkins的脚本中需要循环去checking每个微服务。

如以下示例:

stage('sonarQube代码质量检查') {
            steps {
                script {
                    if("${params.sonarQube}".trim() == "yes") {
                       for (service in ServicesBuild) {
                          def workspace = "pig-"
                          println "当前进行代码质量检查是:${service}"
                          if("${service}".trim() == "pig-gateway" || "${service}".trim() == "pig-auth" || "${service}".trim() == "pig-register"){
                              workspace = "${workspace}" + "${service}".trim().split("-")[1]
                           }
                          if("${service}".trim() == "pig-codegen" || "${service}".trim() == "pig-monitor" || "${service}".trim() == "pig-sentinel-dashboard" || "${service}".trim() == "pig-xxl-job-admin"){
                              workspace = "pig-visual/" + "${service}".trim()
                          }
                           if("${service}".trim() == "pig-upms-biz"){
                               workspace = "pig-upms/" + "${service}".trim()
                           }
                           //定义当前Jenkins的SonarQubeScanner工具
                           scannerHome = tool 'sonar-scanner'
                           //引用当前JenkinsSonarQube环境
                           withSonarQubeEnv('sonarqube9.4') {
                               sh """
                               cd ${workspace}
                               ${scannerHome}/bin/sonar-scanner
                           """
                           }
                       }
                    }else{
                        println "是no,跳过sonarQube代码质量检查"
                    }
                }
            }
        }

注:sonar-scanner和sonarqube9.4名称需和Jenkins中配置的一致。

  1. 新建sonar-project.properties文件

以pig-gateway为例,在pig项目中新增sonar-project.properties文件。

内容如下:

#项目的key(自定义)
sonar.projectKey=pig-gateway
#项目名称
sonar.projectName=pig-gateway
#项目版本号
sonar.projectVersion=1.0
#需要分析的源码的目录,多个目录用英文逗号隔开
sonar.sources=.
#需要忽略的目录
sonar.exclusions=**/test/**,**/target/**
# 字节码文件所在位置
sonar.java.binaries=.
sonar.java.source=1.8
sonar.java.target=1.8
#sonar.java.libraries=**/target/classes/**
# Encoding of the source code. Default is default system encoding
sonar.sourceEncoding=UTF-8

发布测试

在jenkins中查看

在SonarQube中查看代码检测情况

pig-gateway的代码质量还是很错的,不过有一个漏洞哦。

前端代码审查

  1. 调整Jenkins脚本
stage('sonarQube代码质量检查') {
            steps {
                script {
                    if("${params.sonarQube}".trim() == "yes") {
                       println "当前进行代码质量检查是:${APP_NAME}"
                       //定义当前Jenkins的SonarQubeScanner工具
                       scannerHome = tool 'sonar-scanner'
                       //引用当前JenkinsSonarQube环境
                       withSonarQubeEnv('sonarqube9.4') {
                           sh """
                           cd .
                           ${scannerHome}/bin/sonar-scanner
                       """
                       }
                    }else{
                        println "是no,跳过sonarQube代码质量检查"
                    }
                }
            }
        }
  1. 新建sonar-project.properties文件

内容如下:

sonar.projectKey=pig_ui
sonar.projectName=pig_ui
sonar.projectVersion=1.0
sonar.sources=.

发布测试

在jenkins中查看

在SonarQube中查看代码检测情况

若想解锁SonarQube各种姿势,请参考SonarQube 实战:各种姿势的代码审查(二)



相关实践学习
通过Ingress进行灰度发布
本场景您将运行一个简单的应用,部署一个新的应用用于新的发布,并通过Ingress能力实现灰度发布。
容器应用与集群管理
欢迎来到《容器应用与集群管理》课程,本课程是“云原生容器Clouder认证“系列中的第二阶段。课程将向您介绍与容器集群相关的概念和技术,这些概念和技术可以帮助您了解阿里云容器服务ACK/ACK Serverless的使用。同时,本课程也会向您介绍可以采取的工具、方法和可操作步骤,以帮助您了解如何基于容器服务ACK Serverless构建和管理企业级应用。 学习完本课程后,您将能够: 掌握容器集群、容器编排的基本概念 掌握Kubernetes的基础概念及核心思想 掌握阿里云容器服务ACK/ACK Serverless概念及使用方法 基于容器服务ACK Serverless搭建和管理企业级网站应用
相关文章
|
12天前
|
监控 Cloud Native 持续交付
云原生架构下微服务的最佳实践与挑战####
【10月更文挑战第20天】 本文深入探讨了云原生架构在现代软件开发中的应用,特别是针对微服务设计模式的最优实践与面临的主要挑战。通过分析容器化、持续集成/持续部署(CI/CD)、服务网格等关键技术,阐述了如何高效构建、部署及运维微服务系统。同时,文章也指出了在云原生转型过程中常见的难题,如服务间的复杂通信、安全性问题以及监控与可观测性的实现,为开发者和企业提供了宝贵的策略指导和解决方案建议。 ####
38 5
|
12天前
|
Kubernetes Cloud Native 持续交付
云原生架构下的微服务设计原则与最佳实践##
在数字化转型的浪潮中,云原生技术以其高效、灵活和可扩展的特性成为企业IT架构转型的首选。本文深入探讨了云原生架构的核心理念,聚焦于微服务设计的关键原则与实施策略,旨在为开发者提供一套系统性的方法论,以应对复杂多变的业务需求和技术挑战。通过分析真实案例,揭示了如何有效利用容器化、持续集成/持续部署(CI/CD)、服务网格等关键技术,构建高性能、易维护的云原生应用。文章还强调了文化与组织变革在云原生转型过程中的重要性,为企业顺利过渡到云原生时代提供了宝贵的见解。 ##
|
25天前
|
人工智能 Cloud Native 安全
从云原生到 AI 原生,网关的发展趋势和最佳实践
本文整理自阿里云智能集团资深技术专家,云原生产品线中间件负责人谢吉宝(唐三)在云栖大会的精彩分享。讲师深入浅出的分享了软件架构演进过程中,网关所扮演的各类角色,AI 应用的流量新特征对软件架构和网关所提出的新诉求,以及基于阿里自身实践所带来的开源贡献和商业能力。
117 13
|
22天前
|
存储 运维 监控
云原生应用的可观察性:理解、实现与最佳实践
【10月更文挑战第10天】随着云原生技术的发展,可观察性成为确保应用性能和稳定性的重要因素。本文探讨了云原生应用可观察性的概念、实现方法及最佳实践,包括监控、日志记录和分布式追踪的核心组件,以及如何通过选择合适的工具和策略来提升应用的可观察性。
|
2月前
|
Cloud Native 关系型数据库 Serverless
基于阿里云函数计算(FC)x 云原生 API 网关构建生产级别 LLM Chat 应用方案最佳实践
本文带大家了解一下如何使用阿里云Serverless计算产品函数计算构建生产级别的LLM Chat应用。该最佳实践会指导大家基于开源WebChat组件LobeChat和阿里云函数计算(FC)构建企业生产级别LLM Chat应用。实现同一个WebChat中既可以支持自定义的Agent,也支持基于Ollama部署的开源模型场景。
343 12
|
6月前
|
运维 Kubernetes Cloud Native
构建高效云原生运维体系:Kubernetes最佳实践
【5月更文挑战第9天】 在动态和快速演变的云计算环境中,高效的运维是确保应用稳定性与性能的关键。本文将深入探讨在Kubernetes环境下,如何通过一系列最佳实践来构建一个高效且响应灵敏的云原生运维体系。文章不仅涵盖了容器化技术的选择与优化、自动化部署、持续集成/持续交付(CI/CD)流程的整合,还讨论了监控、日志管理以及灾难恢复策略的重要性。这些实践旨在帮助运维团队有效应对微服务架构下的复杂性,确保系统可靠性及业务的连续性。
|
6月前
|
Java Serverless Apache
9 个开源项目、25 个课题可选丨欢迎报名阿里云云原生开源之夏
2024 开源之夏,阿里云云原生应用平台团队开放了包括 Apache Dubbo/Apache RocketMQ/Apache Seata/Higress/iLogtail /Nacos/Sentinel/Spring Could Alibaba / Serverless Devs 在内,涉及微服务、消息、可观测、Serverless 4 大技术领域的 9 个开源项目。
1333 7
|
6月前
|
负载均衡 Kubernetes Cloud Native
云原生最佳实践系列2:基于 MSE 云原生网关同城多活
通过使用阿里云的云原生微服务引擎 MSE,可以实现注册中心的同城容灾多活微服务应用。MSE 提供了云原生网关和注册中心,支持机房级故障的秒级自动转移、非对等部署下的全局流量负载均衡以及流量精细化管控。
765 42
|
6月前
|
Kubernetes Cloud Native Devops
【阿里云云原生专栏】DevOps与云原生的融合:阿里云CI/CD流水线最佳实践
【5月更文挑战第23天】阿里云融合DevOps与云原生技术,提供高效CI/CD解决方案,助力企业提升研发效能。通过云效平台,集成代码管理、构建服务、容器服务、持续部署及监控日志组件,实现自动化研发流程。案例中,应用从GitHub构建到Kubernetes部署,全程无缝衔接。借助阿里云,企业能快速构建适应云原生的DevOps体系,以应对复杂需求和提升市场竞争力。
177 1
|
6月前
|
弹性计算 监控 Cloud Native
云原生最佳实践系列 4:基于 MSE 和 SAE 的微服务部署与压测
通过MSE(微服务引擎)、SAE(Serverless应用引擎)、ARMS(应用监控服务)、PTS(性能测试服务)等产品,实现微服务的无服务化部署、监控和弹性伸缩。
635 13