本指南中涉及到的所有的在线服务均来自阿里云,大致涉及以下服务
- 容器服务 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