[TOC]
开发一个 Spring Boot 应用,并使用云原生功能
- gitee项目地址:https://gitee.com/lar0129/prometheus-test-demo
- github项目地址:https://github.com/lar0129/prometheus-test-demo
- Jenkins拉取github仓库经常失败,故gitee为主项目,github为备份
- 统一限流项目地址:https://gitee.com/liu-yaoli165/prometheus-test-demo.git
实现一个 REST 接口(简单接口即可,比如 json 串 {"msg":"hello"})
- 目前提供/ping接口与限流功能
- 放到github上方便进行持续集成/持续部署
接口提供限流功能,当请求达到每秒 100 次的时候,返回 429(Too many requests)
-
代码:
-
private final RateLimiter rateLimiter = RateLimiter.create(100.0); @GetMapping("/ping") public ResponseEntity<String> hello() { if (rateLimiter.tryAcquire()) { return ResponseEntity.ok("{\"msg\":\"Hello, this is NJU13\"}"); } else { return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body("Too many requests"); } }
-
-
使用Jmeter进行并发测试,结果显示限流成功
加分项:当后端服务有多个实例的时候(一个 Service 包含若干个 Pod),如何实现统一限流(bonus 5分)
-
引入eureka注册中心,使用consumer统一访问service服务
目录结构:
![](https://files.lsmcloud.top/blogScreen Shot 2023-08-14 at 10.38.34 AM.png)
-
配置consumer
package demo.feign; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import com.netflix.loadbalancer.RoundRobinRule; @FeignClient(name = "hello-service", configuration = RoundRobinRule.class) public interface FeignService { @GetMapping("/ping") String msg(); }
package demo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.openfeign.EnableFeignClients; @SpringBootApplication @EnableFeignClients public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }
package demo.controller; import com.google.common.util.concurrent.RateLimiter; import demo.feign.FeignService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class DemoController { private final RateLimiter rateLimiter = RateLimiter.create(50.0); @Autowired private FeignService feignService; @GetMapping("/request") public ResponseEntity<String> hello() { if (rateLimiter.tryAcquire()) { return ResponseEntity.ok(feignService.msg()); } else { return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body("Too many requests"); } } }
-
启动eureka服务,注册成功
![](https://files.lsmcloud.top/blogScreen Shot 2023-08-14 at 10.40.49 AM.png)
- 访问admin-service/request,确认两个hello-service在轮流提供服务
![](https://files.lsmcloud.top/blogScreen Shot 2023-08-14 at 10.42.17 AM.png)
![](https://files.lsmcloud.top/blogScreen Shot 2023-08-14 at 10.42.51 AM.png)
- 使用Jmeter进行限流测试
![](https://files.lsmcloud.top/blogScreen Shot 2023-08-14 at 11.55.41 AM.png)
![](https://files.lsmcloud.top/blogScreen Shot 2023-08-14 at 11.55.49 AM.png)
![](https://files.lsmcloud.top/blogScreen Shot 2023-08-14 at 11.55.58 AM.png)
![](https://files.lsmcloud.top/blogScreen Shot 2023-08-14 at 11.56.10 AM.png)
![](https://files.lsmcloud.top/blogScreen Shot 2023-08-14 at 11.56.19 AM.png)
- 结论
可以发现在限流允许的范围内,admin-service使用round ribbin来交替请求hello01,与hello02,当请求数超过50次/s时,返回Too many requests,实现限流
为该项目准备 Dockerfile,用于构建镜像
- Dockerfile见项目根目录Dockerfile
# Dockerfile
FROM openjdk:17
RUN ln -sf /usr/share/zoneinfo/Asia/shanghai /etc/localtime
RUN echo 'Asia/shanghai' >/etc/timezone
WORKDIR /app
ADD target/demo-0.0.1-SNAPSHOT.jar .
ENTRYPOINT ["java", "-jar", "demo-0.0.1-SNAPSHOT.jar"]
为该项目准备 Kubernetes 编排文件,用于在 Kubernetes 集群上创建 Deployment 和 Service
- k8s部署内容见项目中 ./jenkins/scripts/demo.yaml 文件
# demo.yaml
apiVersion: apps/v1
kind: Deployment #对象类型
metadata:
labels:
app: lar-demo
name: lar-demo
namespace: nju13
spec:
replicas: 1 #运行容器的副本数
selector:
matchLabels:
app: lar-demo
template:
metadata:
annotations:
prometheus.io/path: /actuator/prometheus
prometheus.io/port: "8080"
prometheus.io/scheme: http
prometheus.io/scrape: "true"
labels:
app: lar-demo
spec:
containers: #docker容器的配置
- image: harbor.edu.cn/nju13/lar-demo:{VERSION} #pull镜像的地址,本地测试时注销
# - image: demo:latest # win下本地测试用
name: lar-demo
# imagePullPolicy: Always # 本地测试用
imagePullSecrets: # 本地测试时注销
- name: docker-harbor-nju13 # 本地测试时注销
---
apiVersion: v1
kind: Service
metadata:
name: lar-demo
namespace: nju13
labels:
app: lar-demo
spec:
type: NodePort
selector:
app: lar-demo
ports:
- name: tcp
nodePort: 30332
protocol: TCP
port: 8080
targetPort: 8080
2.3 编写 Jenkins 持续集成流水线,实现代码构建/单元测试/镜像构建功能(需要写至少一个单元测试)
2.4 编写 Jenkins 持续部署流水线,实现部署到 Kubernetes 集群的功能,该流水线的触发条件为持续集成流水线执行成功
注意:持续集成流水线和持续部署流水线也可以合二为一。
以下为2.3和2.4的需求实现
- Jenkinsfile见项目中 ./jenkins/Jenkinsfile文件
pipeline {
agent none
environment {
REGISTRY = "harbor.edu.cn/nju13"
}
stages {
stage('Clone Code') {//第一步:克隆代码,为了build image
agent {
label 'master'
}
steps {
echo "1.Git Clone Code"
sh 'curl "http://p2.nju.edu.cn/portal_io/login?username=211250127&password=******"'
git branch: 'main', url:
// 'https://github.com/lar0129/prometheus-test-demo.git'
"https://gitee.com/lar0129/prometheus-test-demo.git"
}
}
stage('Maven Build') {//第二步:maven打包
agent {
docker {
image 'maven:latest'
args '-v /root/.m2:/root/.m2'
}
}
steps {
echo "2.Maven Build Stage"
sh 'mvn -B clean package -Dmaven.test.skip=true'
}
}
stage('Image Build') {//第三步:构建镜像
agent {
label 'master'
}
steps {
echo "3.Image Build Stage"
sh 'docker build -f Dockerfile --build-arg jar_name=target/demo-0.0.1-SNAPSHOT.jar -t lar-demo:${BUILD_ID} . '
// library使用宏定义,可以在全局设置中设置
sh 'docker tag lar-demo:${BUILD_ID} ${REGISTRY}/lar-demo:${BUILD_ID}'
}
}
stage('Push') {
agent {
label 'master'
}
steps {
echo "4.Push Docker Image Stage"
sh "docker login --username=nju13 harbor.edu.cn -p nju132023"
sh "docker push ${REGISTRY}/lar-demo:${BUILD_ID}"
}
}
}
}
node('slave') {
container('jnlp-kubectl') {
stage('Clone YAML') {
echo "5. Git Clone YAML To Slave"
sh 'curl "http://p2.nju.edu.cn/portal_io/login?username=211250127&password=*****"'
git branch: 'main', url:
// 'https://github.com/lar0129/prometheus-test-demo.git'
"https://gitee.com/lar0129/prometheus-test-demo.git"
}
stage('YAML') {
echo "6. Change YAML File Stage"
// ./jenkins/scripts/是自己创建的?
sh 'sed -i "s#{VERSION}#${BUILD_ID}#g" ./jenkins/scripts/demo.yaml'
}
stage('Deploy') {
echo "7. Deploy To K8s Stage"
sh 'kubectl apply -f ./jenkins/scripts/demo.yaml -n nju13'
// sh 'kubectl apply -f ./jenkins/scripts/demo-monitor.yaml'
}
stage('Monitor') {
echo "8. Deploy Prometheus Monitor"
sh 'kubectl apply -f ./jenkins/scripts/demo-monitor.yaml'
}
stage('RTF Test'){//集成测试
echo "9. RTF Test Stage"
// sh 'kubectl apply -f ./jenkins/scripts/rtf.yaml -n nju13'
}
}
}
-
具体过程:
1.将jenkins连接到k8s-nju13-namespace中,完成鉴权
2.部署流水线
- Jenkins流程:
Stage step 持续集成 1.Git Clone Code 拉取spring boot代码 2.Maven Build maven构建jar包 3.Image Build 构建镜像 4.Push Image To Harbor push镜像至docker仓库 持续部署 5.Git Clone Yaml 拉取部署所需yaml文件 6.Change YAML File 改变Yaml环境变量 7.Deploy the Web App 部署spring boot应用 8.Deploy the Monitor 部署Prometheus监控 集成测试 9.Test 测试 - 部署成功截图
3.访问端口验证k8s部署是否成功
- 流水线单元测试
- 添加针对demoController的单元测试
package nju13.demo; import jakarta.annotation.Resource; import nju13.demo.controller.DemoController; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; @SpringBootTest @RunWith(SpringRunner.class) public class PingServiceTest { @Resource private DemoController demoController; @Test public void tryPing(){ Assert.assertEquals("{\"msg\":\"Hello, this is NJU13\"}",demoController.hello().getBody()); } }
- 在jenkins file中添加junit测试步骤
stage('Unit Tests') { //第三步:单元测试 steps { sh 'mvn test' junit '**/target/surefire-reports/*.xml' } }
- 可以在blue ocean中得到maven test的输出,确定test通过 ![](https://files.lsmcloud.top/blogScreen Shot 2023-08-14 at 4.56.09 PM.png)
加分项:代码提交到仓库自动触发流水线
-
尝试使用gitee webhook
经过测试,学校的jenkins服务没有公网ip或域名,因此当代码更新时无法通过webhook发送请求到jenkins,因此难以实现触发式更新
-
经过权衡,最后使用轮询式更新,在configuration中输入H/5 * * * *,让代码仓库每五分钟轮询gitee是否有更新
![](https://files.lsmcloud.top/blogScreen Shot 2023-08-14 at 12.04.26 PM.png)
轮询到代码仓库更新后,jenkins开始重新构建项目
为该 Java 项目提供 Prometheus metrics 接口,可以供 Prometheus 采集监控指标
-
在springboot项目中配置Prometheus metrics 接口
-
部署k8s-monitor对象
- yaml代码位于./jenkins/script/demo-monitor.yaml
apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: labels: k8s-app: lar-demo name: lar-demo namespace: monitoring spec: endpoints: - interval: 30s port: tcp path: /actuator/prometheus scheme: 'http' selector: matchLabels: app: lar-demo namespaceSelector: matchNames: - nju13
- 放入流水线中部署
-
在Prometheus的UI界面验证部署是否成功
在 Grafana 中的定制应用的监控大屏(CPU/内存/JVM)
- 网站系统的安全隐患:Grafana中默认的管理员账户admin密码admin没改,随随便便就能进去操作
- 在Grafana 中的定制监控应用lar-demo的各种性能
使用压测工具(例如 Jmeter)对接口进压测,在 Grafana 中观察监控数据
-
使用Jmeter,向172.29.4.18:30332/ping 用100个进程同时发送 100个GET请求
- 部分请求错误,证明限流成功
-
Grafana 监控应用lar-demo结果如下,明显可看出变化
加分项:使用 Kubernetes HPA 模块根据 CPU 负载做服务的 Auto Scale
-
创建HPA并查看
-
创建demo-hpa.yaml如下配置hpa自动扩容的参数
apiVersion: apps/v1 kind: Deployment metadata: labels: app: lar-demo-hpa name: lar-demo-hpa namespace: nju13 spec: replicas: 1 selector: matchLabels: app: lar-demo-hpa template: metadata: labels: app: lar-demo-hpa spec: containers: - name: lar-demo-hpa image: harbor.edu.cn/nju13/lar-demo:70 ports: - containerPort: 8080 resources: #基于内存自动扩容 limits: cpu: 200m requests: cpu: 80m imagePullSecrets: # 本地测试时注销 - name: docker-harbor-nju13 # 本地测试时注销 --- apiVersion: v1 kind: Service metadata: name: lar-demo-hpa namespace: nju13 spec: ports: - port: 8080 protocol: TCP selector: app: lar-demo-hpa type: ClusterIP
-
流水线中执行yaml文件创建deploy
-
设置HPA:最小1个pod,最多10个pods,cpu负载超过50%扩容
-
查看HPA
demo-hpa成功监测。且目前replicas数量为1
-
压测验证HPA
-
压力测试后,观测到HPA自动扩容成功
完成所有需求及加分项
1.功能要求(20分)
1.1 实现接口 (5 分)√
1.2 实现限流功能(10 分)√
1.3 实现接口访问指标(QPS),并暴露给 Prometheus(5分)√
1.3 **统一限流(bonus 5 分)**√
2. DevOps 要求(20分)
2.1 Dockerfile 用于构建镜像(5 分)√
2.2 Kubernetes 编排文件(5 分)√
2.3 持续集成流水线(5 分)√
2.4 持续部署流水线(5 分)√
**2.5 代码提交到仓库自动触发流水线(bonus 5分)**√
3. 扩容场景(15分)
3.1 Prometheus 采集监控指标(5 分)√
3.2 Grafana 定制应用监控大屏(5 分)√
3.3 压测并观察监控数据(5 分) √
**3.5 Auto Scale(bonus 10 分)**√