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/

k8s(3):部署一个Quarkus应用 - 图1

1.2 配置postgresql数据库

通过配置文件配置服务使用的数据库内容:

  • 使用vertx的响应连接库
  • 通过hibernate启动时运行sql文件
  • 通过环境变量配置POSTGRE_HOST数据库的host地址
  1. quarkus.datasource.db-kind=postgresql
  2. quarkus.datasource.username=ffzs
  3. quarkus.datasource.password=123zxc
  4. quarkus.datasource.reactive.url=vertx-reactive:postgresql://${POSTGRE_HOST}:5432/mydb
  5. # 和spring中的hibernate用法相同,第一下使用drop-and-create, 之后可以替换为update
  6. quarkus.hibernate-orm.database.generation=drop-and-create
  7. quarkus.hibernate-orm.log.sql=true
  8. 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

k8s(3):部署一个Quarkus应用 - 图2

1.7 构建docker image

在生成项目的时候,项目中的docker文件夹中就有三个Dockerfile文件:

k8s(3):部署一个Quarkus应用 - 图3

前两个都是通过jar包在JVM运行,native不实用JVM。这里使用native模式:

docker build -f src/main/docker/Dockerfile.native -t ffzs/quarkus_app .

k8s(3):部署一个Quarkus应用 - 图4

1.8 运行

使用host模式network运行镜像测试:

docker run -itd --network=host --name=app ffzs/quarkus_app

访问http://0.0.0.0:8080/employee/结果如下,说明成功了。

k8s(3):部署一个Quarkus应用 - 图5

2. k8s文件配置

2.1 编写postgresql的配置

  • 三个部分:Service, Volume, Deployment
  • 通过PersistentVolumeClaim进行数据持久化
  • clusterIP: None不需要外部访问不指定ip
  • imagePullPolicy: 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 ./

k8s(3):部署一个Quarkus应用 - 图6

3.2 验证 Secret 是否存在:

kubectl get secrets

k8s(3):部署一个Quarkus应用 - 图7

3.3 验证是否已动态配置 PersistentVolume:

kubectl get pvc

k8s(3):部署一个Quarkus应用 - 图8

3.4 查看pods是否在运行:

kubectl get pods

k8s(3):部署一个Quarkus应用 - 图9

3.5 查看Service运行:

kubectl get services

k8s(3):部署一个Quarkus应用 - 图10

3.6 获取 quarkus 服务的 IP 地址:

minikube service quarkus --url

k8s(3):部署一个Quarkus应用 - 图11

服务可以访问:

k8s(3):部署一个Quarkus应用 - 图12

3.7 查看deployments

kubectl get deployments

k8s(3):部署一个Quarkus应用 - 图13

3.8 调整Deployment中Pod数:

有这样的应用场景,后端的服务根据时间闲忙不同,白天使用人数多,点击高,后端需求大,晚上没啥人访问,后端需求小,可以通过横向扩展后端使用的pods数来调节后端的吞吐能力,有效的节约计算资源。

  • 通过下面代码对deployment启动的pod进行调整
kubectl scale --replicas=2 deployment/quarkus

k8s(3):部署一个Quarkus应用 - 图14

  • 此时pod变为两个

k8s(3):部署一个Quarkus应用 - 图15

4. 清理

如同docker-compose down通过kustomization.yml文件进行清理,删除 Secret,Deployments,Services 和 PersistentVolumeClaims

kubectl delete -k ./