服务上云指南——java程序部署到k8s中

本指南中涉及到的所有的在线服务均来自阿里云,大致涉及以下服务

  1. 容器服务 ACK https://cs.console.aliyun.com/#/k8s/cluster/list
  2. 云服务器 ECS https://ecs.console.aliyun.com/
  3. 日志服务 SLE https://sls.console.aliyun.com/

涉及技术栈:

  1. spring 全家桶 + Alibaba cloud(主要是 Nacos 注册中心,换成别的也可以)
  2. docker (目前容器化技术首选)
  3. K8s (容器编排)
  4. gitlab cicd 当然别的可以实现 cicd 的方式也行,当然人工 cicd 也行

一、前期准备工作

众所周知,k8s 是一款容器编排系统,可以快速的搭建起一个容器集群,动态扩容缩容,灰度发布等。在 k8s 的世界中,一切的业务运转都离不开容器,而目前最流行的容器技术便是 docker。

建立一个公共的基础镜像

基础镜像的搭建很重要,毕竟谁都不想在每个 dockerfile 中编写大量的脚本操作,因此我们需要将公共的,重复的工作交给基础镜像去处理。

对于 spring 程序来说,一般来说分为 jar 包与 war 包。jar 包依赖与 jre 环境,而 war 包同时依赖与 jretomcat

其次需要思考一些中间件的使用,譬如想要使用基于 java agent 技术的链路追踪。也可以在打包到基础镜像,以下做一个大致演示。

# 基于 openjdk:8u312-jdk
FROM openjdk:8u312-jdk
# 设置一个环境变量
ENV ALIYUN_TRACE_JAR_PATH /usr/local/trace/opentelemetry-javaagent.jar
# 添加 agent jar 包到容器内
# 前者是宿主机 后者是容器
ADD opentelemetry-javaagent.jar /usr/local/trace/opentelemetry-javaagent.jar

将项目打包到项目镜像中

这里推荐使用 mavendockerfile-maven-plugin 插件

<build>
  <finalName>${project.artifactId}</finalName>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-compiler-plugin</artifactId>
      <configuration>
        <source>1.8</source>
        <target>1.8</target>
      </configuration>
    </plugin>
    <plugin>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-maven-plugin</artifactId>
    </plugin>
    <plugin>
      <groupId>com.spotify</groupId>
      <artifactId>dockerfile-maven-plugin</artifactId>
      <version>1.4.13</version>
      <executions>
        <execution>
          <id>default</id>
          <goals>
            <goal>build</goal>
          </goals>
        </execution>
      </executions>
      <configuration>
        <useMavenSettingsForAuth>true</useMavenSettingsForAuth>
        <repository>demo-server-name</repository>
        <tag>latest</tag>
        <buildArgs>
          <!-- 如果是 war 则换成 .war -->
          <JAR_FILE>${project.build.finalName}.jar</JAR_FILE>
        </buildArgs>
      </configuration>
    </plugin>
  </plugins>
</build>

useMavenSettingsForAuth 的使用方式见 dockerfile-maven 。你可以在你的 settings.xml 中配置私有库的信息,这样在 docker 构建的过程中拉取私有库就不会遇到权限问题

<servers>
    <server>
      <!--该id对应于docker.repository.registry的值-->
      <id>xxxxxxxxxxx</id>
      <!--这里是阿里云访问凭证账号-->
      <username>xxxxxxxx</username>
      <!--这里是在阿里云访问凭证密码-->
      <password>xxxxxxxxxxxx</password>
    </server>
  </servers>

buildArgs 可以认为是 docker 执行 build --build-age 可以在 dockerfile 文件中指定一些参数,在 build 的阶段构建进去。这里我们定义一个 JAR_FILE 用来标识我们打包完成的包名。详参 docker-build

接下来我们在项目的根目录准备我们的 dockerfile 文件

# 你自己的基础镜像地址(或者直接基于 openjdk 都可以)
FROM xxxxxxxxxx/xxxxxxxxxxxxxx/xxxxxxxxxx:xxxxx
# 我们定义的参数,maven 构建的时候自动带进来
ARG JAR_FILE
# 额外的环境变量
ENV xxxx xxxx
# 左边为宿主,右边为容器。我们将宿主机当前执行 dockerfile 文件的相对路径下的 target 目录下的 jar 包复制到容器的根目录
ADD target/${JAR_FILE} /demo-server-name.jar
# docker run 后运行的命令
CMD ["java", "-jar", "/demo-server-name.jar"]

执行命令后我们即可在宿主机中看到打包好的镜像,接下来可以手动把此服务镜像推送到私有仓库中或者采用 ci 的方式。打包项目镜像教程结束。

mvn deploy -Dmaven.deploy.skip=true

二、成为一名 YAML 工程师

成为一名 YAML 工程师,迈入云原生的第一步。—— 沃兹基硕德

1. 首先你需要有个 k8s 集群。

这里无论是自己搭建,还是买的云服务商的都可以,我们尽量以命令行的方式控制 k8s 减少差异性。

2. 在开发机器与 CICD 机器上安装 kubectl 控制集群

根据你的环境不同 安装 kubectl

配置 kubectl 这里以阿里云为例,找到 集群信息 - 连接信息 ,根据提示复制到 $HOME/.kube/config 下就可以了。

image-20220623213637299

使用 kubectl cluster-info 命令即可检查是否配置成功,有打印出一下信息即可。

# kubectl cluster-info
Kubernetes control plane is running at https://xxx.xxx.xxx.xxx:xxxx
metrics-server is running at https://xxx.xxx.xxx.xxx:xxxx/api/v1/namespaces/kube-system/services/heapster/proxy
KubeDNS is running at https://xxx.xxx.xxx.xxx:xxxx/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy

3. 编写自己的第一个 YAML

此处概念颇多,深究以后分享

简单介绍 k8s 中的三个概念

Deployment

参考资料

简单来说,就是可以配置一系列容器的声明。

Service

参考资料

将运行在一组 Pods 上的应用程序公开为网络服务的抽象方法。使用 Kubernetes,你无需修改应用程序即可使用不熟悉的服务发现机制。 Kubernetes 为 Pods 提供自己的 IP 地址,并为一组 Pod 提供相同的 DNS 名, 并且可以在它们之间进行负载均衡。

Ingress

参考资料

Ingress 是对集群中服务的外部访问进行管理的 API 对象,典型的访问方式是 HTTP。

Ingress 可以提供负载均衡、SSL 终结和基于名称的虚拟托管。

样例

apiVersion: apps/v1
kind: Deployment
metadata:
  name: k8s-app-server
  labels:
    appName: k8s-app-server
spec:
  replicas: 2
  selector:
    matchLabels:
      appName: k8s-app-server
  template:
    metadata:
      labels:
        appName: k8s-app-server
    spec:
    	# 亲合度, 如果你的 k8s 节点有配置的话, weight 值越高,该 pod 则越会去找那些有该标签的节点
			# 可以做很多事情,譬如对性能要求比较高的就可以设置个标签,使用亲合度去控制
      affinity:
        nodeAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
            - preference:
                matchExpressions:
                  - key: env
                    operator: In
                    values:
                      - prod
              weight: 100
      containers:
        - name: k8s-app-server
        	# 给该容器分配内存 [1]
          resources:
            requests:
              memory: "500M"
            limits:
              memory: "500M"
          image: k8s-app-server:latest
          # 镜像拉取策略
          imagePullPolicy: Always
          ports:
            - containerPort: 9100
          # 可以定义一个 comfigMap , 其中的所有配置,均会变成环境变量注入到当前容器
          envFrom:
            - configMapRef:
                name: common-config
          env:
            - name: JAVA_TOOL_OPTIONS
            	# 给该容器分配内存 [1]
              value: >-
                -XX:+UseContainerSupport
                -XX:MaxRAMPercentage=80.0
                -Dserver.port=9100
          # 启动探测器 [2]
          startupProbe:
            httpGet:
              path: /actuator/health
              port: 9100
            # 启动失败的重试次数
            # (10 * 10) 100s 最大等待 100s 容器启动时间
            failureThreshold: 30
            periodSeconds: 10
          # 存活探针
          livenessProbe:
            httpGet:
              path: /actuator/health
              port: 9100
            # 启动后多少秒开始检测
            initialDelaySeconds: 30
            # 探测时间间隔
            periodSeconds: 5
            # 探测超时
            timeoutSeconds: 10
          # 就绪探针
          readinessProbe:
            failureThreshold: 3
            httpGet:
              path: /actuator/health
              port: 9100
            initialDelaySeconds: 30
            periodSeconds: 5
            timeoutSeconds: 10
---
# 定义一个 service [3]
apiVersion: v1
kind: Service
metadata:
  name: k8s-app-server
  labels:
    appName: k8s-app-server
spec:
  # server 类型
  type: NodePort
  ports:
    - name: k8s-app-server
      port: 9100
      targetPort: 9100
      nodePort: 30001
  selector:
    appName: k8s-app-server
    
---
# 定义一个 ingress [4]
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: k8s-app-server
  annotations:
    kubernetes.io/ingress.class: nginx
    # 重定向
    nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
  rules:
    - http:
        paths:
          - backend:
              service:
                name: k8s-app-server
                port:
                  number: 9100
            # 匹配目标
            path: /svc/app-server(/|$)(.*)
            pathType: Prefix

[1] 给该容器分配内存

详见[如何在 docker、k8s 下对 JVM 调优](#四、如何在 docker、k8s 下对 JVM 调优)

[2] 探测器

参考文档:配置存活、就绪和启动探测器

此处设置探测器是为了保障健壮性。

存活探测器可以帮我们监控容器是否停止运行,如果探测器发现服务已经无法联通,就会自动杀掉服务,启动一个新服务

就绪和启动探测器效果类似,都是用于判断该服务什么时候可以对外提供服务,方便 serviceingress 切换流量到新的容器中。

[3] 定义一个 service

k8s 对外提供服务的最小单位是 service,一个 service 就是一群 pods 。service 会自动帮助 pods 做负载均衡,对外而已这仅是一个服务。

[4] 定义一个 ingress

k8s 对外提供服务的方式很多,上面介绍了 NodePort 类型的 service ,可以讲将服务暴漏出一个端口对外使用,使用集群中任意一个 node 的 ip 加上 port 即可进行访问,此方法有较多弊端,非必须请使用 ingress。但 ingress 一般是 7 层代理,而 nodePort 是天然的 4 层代理。

img

扩展:交换机可以称为二层交换机,路由器可以称为三层交换机。

三、结合 cicd

cicd 的工具有很多,可以选择比较流行的 jenkins 、gitlab cicd 等,接下来的教程以 gitlab cicd 为例,使用其他工具的也可以做一个参考,工具不同思想相同。

1. 安装 gitlab runner

参考之前的文章

2. 编写 .gitlab-ci.yml 文件

请保障 gitlab runner 的宿主环境有 kubectl 运行环境,无论你是以 docker 还是服务的型式安装的。

参考文档: https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/ci/yaml/index.md

before_script:
  - echo "CI Build Start"

after_script:
  - echo "CI Build Finish"

# 定义三个阶段
stages:
  - build
  - deploy
  - rollback
  
# 第一个节点打包
# 先将 latest 设置为 last
# 在打包本地 latest
# 推送本地 latest 到远程
k8s-test-build:
  stage: build
  # 仅在那个分支下生效
  only:
    - k8s_develop
  script:
    - docker pull k8s-app-server:latest
    - docker tag k8s-app-server:latest k8s-app-server:last
    - docker push k8s-app-server:last
    - mvn dependency:purge-local-repository
    - mvn deploy -Dmaven.deploy.skip=true
    - docker push k8s-app-server:latest
  # 目标 runner 的 tag
  tags:
    - gitlab-runner

# 部署
# 执行 rollout 让 k8s 的 deployment 滚动发布,结合上面的镜像更新策略,做到更新为最新的镜像
k8s-test-deploy:
  stage: deploy
  only:
    - k8s_develop
  script:
    - kubectl rollout restart deployment k8s-app-server -n test
  tags:
    - gitlab-runner

# 回滚
# 打包的反向操作
k8s-test-rollback:
  stage: rollback
  only:
    - k8s_develop
  script:
    - docker pull k8s-app-server:last
    - docker tag k8s-app-server:last k8s-app-server:latest
    - docker push k8s-app-server:latest
    - kubectl rollout restart deployment k8s-test-k8s-app-server -n test
  tags:
    - inficloud
  when: manual

四、如何在 docker、k8s 下对 JVM 调优

避坑指南

从 java 10 开始,JVM 在分配堆大小的时候,会考虑容器内存限制,而不是主机配置。

对应 JVM 参数为默认开启

java -XX:+UseContainerSupport

可以用过此方法禁用

java -XX:-UseContainerSupport

此参数已向后移植到 Java 8: 文档地址

如果不修改 JVM 内存参数,则默认最大使用总内存的四分之一。

在容器中可以设置一下参数进行更细粒度的内存控制。

  • -XX:InitialRAMPercentage

  • -XX:MaxRAMPercentage

  • -XX:MinRAMPercentage

值介于 0.0 到 100.0 之间,MaxRAMPercentage 默认值为 25.0。

基于 tomcat 容器下 JVM 配置

可以通过设置容器环境变量 JAVA_OPTS

containers:
  - name: java_app
    env:
      - name: JAVA_OPTS
        value: "xxxxxxx"

基于 jdk 容器的 Spring boot 下 JVM 配置

可以通过设置容器环境变量 JAVA_TOOL_OPTIONS

containers:
  - name: java_app
    env:
      - name: JAVA_TOOL_OPTIONS
        value: "xxxxxxx"

我们可以通过 k8s 的 resources 对我们的容器进行内存,性能限制,这里我们将内存限制为 500m

containers:
  - name: java_app
    # 给该容器分配内存
		resources:
			requests:
				memory: "1000M"
			limits:
    		memory: "1000M"
    env:
      - name: JAVA_TOOL_OPTIONS
        value: "-XX:+UseContainerSupport -XX:MaxRAMPercentage=80.0"

配置是将该容器设置内存最大为 1000M, JVM 内存最大使用容器内存的 80%,也就是 800M。

接下来模拟一个比较苛刻的条件,我们将内存限制为 200M。

image-20220616165835566

可以看到容器未能正常启动

image-20220616165936394

查看时间可见为 pod 使用的内存已经超过 k8s 的限制,因此 OOM killed 了

image-20220616170012740

总结 使用 k8s limitsjvm -XX:+UseContainerSupport -XX:MaxRAMPercentage=80.0 的配合可以做到对java 程序更加细粒度的控制,无需针对每个程序进行 jvm 内存的计算,仅需控制 k8s limits 即可控制对应程序的内存控制。

参考资料

https://stackoverflow.com/questions/54516988/what-does-usecontainersupport-vm-parameter-do

https://www.oracle.com/java/technologies/javase/8u191-relnotes.html

消息盒子

# 暂无消息 #

只显示最新10条未读和已读信息