Quarkus是为GraalVM和HotSpot量身定制的Kubernetes Native Java框架,由最佳的Java库和标准精心打造而成。是一个比较新的框架,基于vert.x编写。可以通过GraalVM 编译,运行内存和镜像大小变得超级小。这里使用Quarkus的响应式简单写个crud,尝尝鲜。跟spring的webflux比,个人感觉对hibernate支持好一些,对docker的支持比较好,开箱即用生成docker image。Route写法现在spring也支持了,GraalVM编译据说spring也支持了。可能主要差别还是在vert.x和webflux这里。
1. 创建Quarkus应用
1.1 生成项目
跟spring一样可以到官网直接根据需求获取一个项目文件包:https://code.quarkus.io/
1.2 配置postgresql数据库
通过配置文件配置服务使用的数据库内容:
- 使用vertx的响应连接库
- 通过hibernate启动时运行sql文件
- 通过环境变量配置POSTGRE_HOST数据库的host地址
quarkus.datasource.db-kind=postgresql
quarkus.datasource.username=ffzs
quarkus.datasource.password=123zxc
quarkus.datasource.reactive.url=vertx-reactive:postgresql://${POSTGRE_HOST}:5432/mydb
# 和spring中的hibernate用法相同,第一下使用drop-and-create, 之后可以替换为update
quarkus.hibernate-orm.database.generation=drop-and-create
quarkus.hibernate-orm.log.sql=true
quarkus.hibernate-orm.sql-load-script=import.sql
本地测试期间通过docker-compose启动postgresql数据库:
postgres:
image: postgres:10.5
container_name: postgres
restart: always
networks:
- spring
ports:
- 5432:5432
environment:
POSTGRES_USER: ffzs
POSTGRES_PASSWORD: 123zxc
POSTGRES_DB: mydb
PGDATA: /tmp
volumes:
- ./postgres/data:/var/lib/postgresql/data/pgdata
sql文件内容为,预写入数据库的一些数据, 创建数据库和数据表可以通过hibernate完成:
INSERT INTO employee(id, name, age, email) VALUES (1, 'ffzs', 12, 'ffzs@163.com');
INSERT INTO employee(id, name, age, email) VALUES (2, 'vincent', 14, 'vincent@163.com');
INSERT INTO employee(id, name, age, email) VALUES (3, 'fanfanzhisu', 17, 'fanfanzhisu@163.com');
1.3 创建Entity类
- 创建一个基础的Entity类关联数据库中的表
- 简单的name,age,email字段
@NamedQuery(name = Employee.FIND_ALL, query = "SELECT e FROM Employee e")
可以通过将name和query绑定,在服务中使用
/**
* @Author: ffzs
* @Date: 2020/10/27 下午4:40
*/
@Entity
@Table(name = "employee")
@NamedQuery(name = Employee.FIND_ALL, query = "SELECT e FROM Employee e")
public class Employee {
public static final String FIND_ALL = "Employee.findAll";
@Id
@SequenceGenerator(name = "fruitsSequence", sequenceName = "known_fruits_id_seq", allocationSize = 1, initialValue = 10)
@GeneratedValue(generator = "fruitsSequence")
private Long id;
@Column(length = 40)
private String name;
private Integer age;
@Column(length = 60, unique = true)
private String email;
public Employee() {}
public Employee(Long id, String name, Integer age, String email) {
this.id = id;
this.name = name;
this.age = age;
this.email = email;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
}
1.4 Route类
由于服务逻辑很简单,直接通过Route类实现服务。
- 全部通过
Mutiny.Session
来处理逻辑,通过反射指定对应entity的功能。 createNamedQuery
直接通过配置的过的query进行调用,不需要传参的时候很好用- 通过
@Route(path = "/*", type = FAILURE)
集中处理报错情况
/**
* @Author: ffzs
* @Date: 2020/10/27 下午4:40
*/
@RouteBase(path = "/employee", produces = "application/json")
public class EmployeeRoutes {
private static final Logger LOGGER = Logger.getLogger(EmployeeRoutes.class.getName());
@Inject
Mutiny.Session session;
@Route(methods = HttpMethod.GET, path = "/")
public Uni<List<Employee>> getAll() {
return session.createNamedQuery(Employee.FIND_ALL, Employee.class).getResultList();
}
@Route(methods = HttpMethod.GET, path = "/:id")
public Uni<Employee> findById(@Param String id){
return session.find(Employee.class, Long.valueOf(id));
}
@Route(methods = POST, path = "/")
public Uni<Employee> create(@Body Employee employee, HttpServerResponse response) {
if (employee == null || employee.getId() != null) {
return Uni.createFrom().failure(new IllegalArgumentException("request中无数据或是包含id无法创建请考虑update"));
}
return session.persist(employee)
.chain(session::flush)
.onItem().transform(ignore -> {
response.setStatusCode(201);
return employee;
});
}
@Route(methods = PUT, path = "/:id")
public Uni<Employee> update(@Body Employee fruit, @Param String id) {
if (fruit == null || fruit.getName() == null) {
return Uni.createFrom().failure(new IllegalArgumentException("request信息中没有员工名"));
}
return session.find(Employee.class, Long.valueOf(id))
// 如果id存在信息的话进行更新操作
.onItem().ifNotNull().transformToUni(entity -> {
entity.setName(fruit.getName());
return session.flush()
.onItem().transform(ignore -> entity);
})
// 否则fail
.onItem().ifNull().fail();
}
@Route(methods = DELETE, path = "/:id")
public Uni<Employee> delete(@Param String id, HttpServerResponse response) {
return session.find(Employee.class, Long.valueOf(id))
// id 存在的话删除
.onItem().ifNotNull().transformToUni(entity -> session.remove(entity)
.chain(session::flush)
.map(ignore -> {
response.setStatusCode(204).end();
return entity;
}))
// 否则报错
.onItem().ifNull().fail();
}
@Route(path = "/*", type = FAILURE)
public void error(RoutingContext context) {
Throwable t = context.failure();
if (t != null) {
LOGGER.error("请求处理失败", t);
int status = context.statusCode();
String chunk = "";
if (t instanceof NoSuchElementException) {
status = 404;
} else if (t instanceof IllegalArgumentException) {
status = 422;
chunk = new JsonObject().put("code", status)
.put("exceptionType", t.getClass().getName()).put("error", t.getMessage()).encode();
}
context.response().setStatusCode(status).end(chunk);
} else {
// 非特殊处理情况使用默认处理
context.next();
}
}
}
1.5 测试类
通过编写测试了,测试个个api接口的功能是否可行:
/**
* @Author: ffzs
* @Date: 2020/10/27 下午6:20
*/
@QuarkusTest
public class EmployeeTest {
@Test
public void testApi (){
// 测试getAll
given().when().get("/employee/")
.then()
.statusCode(200)
.body(
containsString("ffzs"),
containsString("fanfanzhisu"),
containsString("vincent")
);
// 测试getOne
given().when().get("/employee/1")
.then()
.statusCode(200)
.body(containsString("ffzs"));
// 测试delete
given()
.when()
.delete("/employee/1")
.then()
.statusCode(204);
given().when().get("/employee/")
.then()
.statusCode(200)
.body(
not(containsString("ffzs"))
);
// 测试create
given()
.when()
.body("{\"name\" : \"cat\", \"age\" : \"10\", \"email\" : \"cat@163.com\"}")
.contentType("application/json")
.post("/employee/")
.then()
.statusCode(201);
given().when().get("/employee/")
.then()
.statusCode(200)
.body(
containsString("cat")
);
}
}
1.6 构建Native Image
quarkus的特色就是可以构建云原生应用,直接native编译,运行速度大小等都比jar包要快,但是编译时间确实很长。
构建完成后,target目录下会存在一个名字为[project name]-runner
的文件,这个文件可以直接运行。
mvn package -Pnative -Dquarkus.native.container-build=true
1.7 构建docker image
在生成项目的时候,项目中的docker文件夹中就有三个Dockerfile文件:
前两个都是通过jar包在JVM运行,native不实用JVM。这里使用native模式:
docker build -f src/main/docker/Dockerfile.native -t ffzs/quarkus_app .
1.8 运行
使用host模式network运行镜像测试:
docker run -itd --network=host --name=app ffzs/quarkus_app
访问http://0.0.0.0:8080/employee/
结果如下,说明成功了。
2. k8s文件配置
2.1 编写postgresql的配置
- 三个部分:Service, Volume, Deployment
- 通过PersistentVolumeClaim进行数据持久化
clusterIP: None
不需要外部访问不指定ipimagePullPolicy: IfNotPresent
优先获取已经存在的image- 通过
secretKeyRef
配置数据库密码(敏感信息)
apiVersion: v1
kind: Service
metadata:
name: quarkus-postgre
labels:
app: quarkus
spec:
ports:
- port: 5432
selector:
app: quarkus
tier: postgre
clusterIP: None
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgre-pv-claim
labels:
app: quarkus
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: quarkus-postgre
labels:
app: quarkus
spec:
selector:
matchLabels:
app: quarkus
tier: postgre
strategy:
type: Recreate
template:
metadata:
labels:
app: quarkus
tier: postgre
spec:
containers:
- image: postgres:10.5
imagePullPolicy: IfNotPresent
name: postgre
env:
- name: POSTGRES_USER
value: ffzs
- name: POSTGRES_DB
value: mydb
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: postgre-pass
key: password
ports:
- containerPort: 5432
name: postgre
volumeMounts:
- name: postgre-persistent-storage
mountPath: /var/lib/postgresql/data
volumes:
- name: postgre-persistent-storage
persistentVolumeClaim:
claimName: postgre-pv-claim
2.2 编写quarkus服务的配置
type: LoadBalancer
指定负载均衡模式,方便横向扩展value: 'quarkus-postgre'
指定数据库service的name来匹配服务host- 由于服务启动时需要向数据库中写入数据,如果在启动服务之前,数据库还没有ready的话就会报错,因此添加
initContainers
确保服务的host已经存在
apiVersion: v1
kind: Service
metadata:
name: quarkus
labels:
app: quarkus
spec:
ports:
- port: 8080
selector:
app: quarkus
tier: frontend
type: LoadBalancer
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: quarkus
labels:
app: quarkus
spec:
selector:
matchLabels:
app: quarkus
tier: frontend
strategy:
type: Recreate
template:
metadata:
labels:
app: quarkus
tier: frontend
spec:
containers:
- image: ffzs/quarkus_app
name: quarkus
imagePullPolicy: IfNotPresent #设置从本地拉取 默认always
env:
- name: POSTGRE_HOST
value: 'quarkus-postgre'
ports:
- containerPort: 8080
name: quarkus
initContainers:
- name: init-mysql
image: busybox
command: ['sh', '-c', 'until nslookup quarkus-postgre; do echo waiting for quarkus-postgre; sleep 2; done;']
2.3 整合
通过kustomization.yml文件整合任务,为默认启动文件,有点像docker-compose的docker-compose.yml文件,就是-f的默认文件名:
同时通过secretGenerator
指定数据库密码。
secretGenerator:
- name: postgre-pass
literals:
- password=123zxc
resources:
- postgre-deployment.yml
- quarkusApp-deployment.yml
3. 应用、验证和调整
3.1 启动配置:
kubectl apply -k ./
3.2 验证 Secret 是否存在:
kubectl get secrets
3.3 验证是否已动态配置 PersistentVolume:
kubectl get pvc
3.4 查看pods是否在运行:
kubectl get pods
3.5 查看Service运行:
kubectl get services
3.6 获取 quarkus 服务的 IP 地址:
minikube service quarkus --url
服务可以访问:
3.7 查看deployments
kubectl get deployments
3.8 调整Deployment中Pod数:
有这样的应用场景,后端的服务根据时间闲忙不同,白天使用人数多,点击高,后端需求大,晚上没啥人访问,后端需求小,可以通过横向扩展后端使用的pods数来调节后端的吞吐能力,有效的节约计算资源。
- 通过下面代码对deployment启动的pod进行调整
kubectl scale --replicas=2 deployment/quarkus
- 此时pod变为两个
4. 清理
如同docker-compose down
通过kustomization.yml
文件进行清理,删除 Secret,Deployments,Services 和 PersistentVolumeClaims
kubectl delete -k ./