本指南中涉及到的所有的在线服务均来自阿里云,大致涉及以下服务
- 容器服务 ACK https://cs.console.aliyun.com/#/k8s/cluster/list
- 云服务器 ECS https://ecs.console.aliyun.com/
- 日志服务 SLE https://sls.console.aliyun.com/
涉及技术栈:
- spring 全家桶 + Alibaba cloud(主要是 Nacos 注册中心,换成别的也可以)
- docker (目前容器化技术首选)
- K8s (容器编排)
- gitlab cicd 当然别的可以实现 cicd 的方式也行,当然人工 cicd 也行
一、前期准备工作
众所周知,k8s 是一款容器编排系统,可以快速的搭建起一个容器集群,动态扩容缩容,灰度发布等。在 k8s 的世界中,一切的业务运转都离不开容器,而目前最流行的容器技术便是 docker。
建立一个公共的基础镜像
基础镜像的搭建很重要,毕竟谁都不想在每个 dockerfile 中编写大量的脚本操作,因此我们需要将公共的,重复的工作交给基础镜像去处理。
对于 spring 程序来说,一般来说分为 jar  包与 war 包。jar 包依赖与 jre 环境,而 war 包同时依赖与 jre 与 tomcat 。
其次需要思考一些中间件的使用,譬如想要使用基于 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
将项目打包到项目镜像中
这里推荐使用 maven 的 dockerfile-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 下就可以了。

使用 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] 探测器
参考文档:配置存活、就绪和启动探测器
此处设置探测器是为了保障健壮性。
存活探测器可以帮我们监控容器是否停止运行,如果探测器发现服务已经无法联通,就会自动杀掉服务,启动一个新服务
就绪和启动探测器效果类似,都是用于判断该服务什么时候可以对外提供服务,方便 service 与 ingress 切换流量到新的容器中。
[3] 定义一个 service
k8s 对外提供服务的最小单位是 service,一个 service 就是一群 pods 。service 会自动帮助 pods 做负载均衡,对外而已这仅是一个服务。
[4] 定义一个 ingress
k8s 对外提供服务的方式很多,上面介绍了 NodePort 类型的 service ,可以讲将服务暴漏出一个端口对外使用,使用集群中任意一个 node 的 ip 加上 port 即可进行访问,此方法有较多弊端,非必须请使用 ingress。但 ingress 一般是 7 层代理,而 nodePort 是天然的 4 层代理。

扩展:交换机可以称为二层交换机,路由器可以称为三层交换机。
三、结合 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。

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

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

总结 使用 k8s limits 与 jvm -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
.jpg)






