53

云原生微服务框架——Helidon - Java译站

 4 years ago
source link: http://it.deepinmind.com/kubernetes/2019/08/02/cloud-native-microframework-helidon.html?
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

云原生微服务框架——Helidon

Published: 02 Aug 2019 Category: Kubernetes

在互联网早期的相当长一段时间内,WEB应用都是”单体应用(monolithic)“。也就是说所有的API和前端展示层代码都被封装在一个独立的、自给自足的应用当中。业务逻辑,校验,数据获取及计算,持久化,安全,UI都封装成一个大的包,部署在应用服务器或者web服务器上,比如说Tomcat, Apache或者Microsoft IIS。这个方法过去有效,未来也仍将有效,只不过当你的应用到达一定规模之后,就会面临诸多挑战:

  • 部署:对于单体应用来说,checkout源代码,编译,测试,打包和部署这些都需要花费相当长的时间。
  • 依赖关系,框架及开发语言:整个应用都和具体的选型和版本强绑定,一旦这些基础框架发布了新的版本,升级会非常困难。
  • 单点故障:单体应用非常脆弱,如果web服务器挂了,整个应用也就挂了。
  • 扩展性:哪怕只是应用程序其中的某一部分引发的负载升高,也必须对整个应用来进行扩容。

当然还会碰到其它问题,但这些就已经够让开发人员、项目经理、运维人员头疼的了。长久以来,大家不得不去处理这些事情。

  • 部署:每个服务都可以单独地测试、编译及部署。
  • 依赖关系、框架及开发语言:每个服务都可以使用自己所需要的开发语言、框架、依赖及不同的版本。
  • 单点故障:每个服务都部署在容器里并使用编排工具来管理,单点宕机会被隔离掉,不会影响到整个应用。
  • 扩展性:服务可以独立进行扩展,高负载的服务扩容,低负载的服务缩容。

微服务并不是万能的,但在许多场景下还是非常有用的。我们已经介绍了“为什么”需要微服务,现在来介绍下”如何“实现微服务。

目前市面上已经有不少微服务框架了,再造一个新的似乎没这个必要,不过Oracle还真就这么做了,这个项目便是Helidon。光看项目名字你可能就知道Oracle为什么要创建这个项目了:Helidon在希腊语中是燕子的意思——一种小巧、灵活的鸟类,它们天然就适合在云端翱翔。因此,这个项目的发起人应该是想开发出一款无需应用服务器且能被用于Java SE应用的轻量级框架。

Helidon有两种版本:SE和MP。Helidon SE算是一个微框架(microframework),比较简单、轻量,采用了函数式编程、响应式编程的思想,运行在自带的Netty web服务器上。它比较类似于Javalin、Micronaut或者Spark Java这样的框架。而Helidon MP实现了MicroProfile的规范,采用了Java EE/Jakarta EE开发人员所熟知的注解和组件的技术,比如说JAX-RS/Jersey, JSON-P以及CDI。它和Open Liberty, Payara还有Thorntail (正式名称是 WildFly Swarm)的定位差不多。我们先从Helidon SE开始,来了解一下这个框架。

Helidon SE入门

新工具的学习就是在摸着石头过河,不过Helidon不存在这个问题。只需要安装一些必要的依赖软件(JDK 8+,Maven 3.5+)就可以开始使用了。使用Docker或者Kubernetes的话,能让容器的创建和部署更加容易。那还需要安装Docker 18.02或更新的版本,以及Kubernetes 1.7.4+。(可以使用Minikube或Docket Desktop在桌面操作系统上运行你的Kubernetes集群)。

确认下软件的版本:

$ java --version
$ mvn --version
$ docker --version
$ kubectl version --short

一旦安装完成,便能够通过Helidon提供的Maven项目模板(原型,Archetype)来快速生成一个工程。可能你对Maven Archetype还不太了解,它其实就是一些项目模板,可以用来搭建某个框架的启动工程以便快速使用。Oracle提供了两套项目模板:Helidon SE和Helidon MP各一个。

mvn archetype:generate -DinteractiveMode=false \
    -DarchetypeGroupId=io.helidon.archetypes \
    -DarchetypeArtifactId=helidon-quickstart-se \
    -DarchetypeVersion=0.10.2 \
    -DgroupId=[io.helidon.examples] \
    -DartifactId=[quickstart-se] \
    -Dpackage=[io.helidon.examples.quickstart.se]

项目模板在Maven的中央仓库中,在这里你可以找到最新发布的版本。前面方括号内的值是和具体项目相关的,可以根据你的需要来进行编辑。本文中的示例将使用下面的命令来创建完成:

$ mvn archetype:generate -DinteractiveMode=false \
    -DarchetypeGroupId=io.helidon.archetypes \
    -DarchetypeArtifactId=helidon-quickstart-se \
    -DarchetypeVersion=0.10.2 \
    -DgroupId=codes.recursive \
    -DartifactId=helidon-se-demo \
    -Dpackage=codes.recursive.helidon.se.demo

完成之后,一个完整的示例工程就会在新生成的目录当中了,目录名便是artifactId参数里所指定的。这是一个完整的可运行的工程,可以编译打包一下:

$ mvn package

这个命令会把所有生成的测试用例全执行一遍,并在target/libs目录下生成应用的jar包。这个框架还自带了一个内嵌的web服务器,现在你可以通过下述的命令来运行一下:

$ java -jar target/helidon-se-demo.jar

可以看到应用程序启动起来了,工作在8080端口上:

[DEBUG] (main) Using Console logging
2018.10.18 14:34:10 INFO io.netty.util.internal.PlatformDependent Thread[main,5,main]:
Your platform does not provide complete low-level API for accessing direct buffers
reliably. Unless explicitly requested, heap buffer will always be preferred to avoid
potential system instability.
2018.10.18 14:34:10 INFO io.helidon.webserver.netty.NettyWebServer
Thread[nioEventLoopGroup-2-1,10,main]: Channel '@default' started:
[id: 0x3002c88a, L:/0:0:0:0:0:0:0:0:8080]
WEB server is up! http://localhost:8080

但是访问根路径会报错,因为这个模板并没有在根路径下配置路由。你可以访问http://localhost:8080/greet,它会返回一个JSON格式的”Hello Wrold“的信息。

到目前为止,除了执行了几个maven命令并启动应用之外,我们没写过一行代码,却已经有了一个搭建好的完整的可运行的应用程序。当然了,未来还是要和代码打交道的,不过在这之前我们先来看下Helidon为Docker提供了什么样的支持。

我们先通过ctrl+c把程序停掉。在target目录中,我们可以看到运行mvn package命令时额外生成了一些文件。Helidon生成了一个可以用来构建Docker容器的Dockerfile,以及一个用于部署到Kubernetes的application.yaml。这两文件虽然简单,但有了它们可以很快把应用部署起来。

下面是这个demo工程的Dockerfile(为了简洁起见,授权信息就去掉了):

FROM openjdk:8-jre-alpine

RUN mkdir /app
COPY libs /app/libs
COPY helidon-se-demo.jar /app

CMD ["java", "-jar", "/app/helidon-se-demo.jar"]

也许你是第一次接触Dockerfile,在首行它声明了一个基础的镜像。这里用的是8-jre-alpine的openjdk镜像,这是基于Alpine Linux的包含了Java 8 JRE的一个非常轻量级的镜像。两行之后,Dockerfile创建了一个app目录来存储应用程序。接下来这行将libs目录中的文件拷贝到app/libs下,然后将jar也包复制到app下。最后一行告诉Docker执行java jar命令来启动应用。

我们在工程的根目录下运行下面的命令来测试一下这个Dockerfile:

(注:如果你用的是kubemini,在运行后面的docker build命令前,一定要先执行下:

eval $(minikube docker-env)

否则后面kubernetes会找不到镜像。)

$ docker build -t helidon-se-demo target

它会告诉Docker使用target目录下的Dockerfile去创建一个tag为helidon-se-demo的镜像。执行完docker builld的输出结果大概是这样的:

Sending build context to Docker daemon  5.231MB
Step 1/5 : FROM openjdk:8-jre-alpine
 ---> 0fe3f0d1ee48
Step 2/5 : RUN mkdir /app
 ---> Using cache
---> ab57483b1f76
Step 3/5 : COPY libs /app/libs
 ---> 6ac2b96f4b9b
Step 4/5 : COPY helidon-se-demo.jar /app
 ---> 7d2135433bcc
Step 5/5 : CMD ["java", "-jar", "/app/helidon-se-demo.jar"]
 ---> Running in 5ab71094a72f
Removing intermediate container 5ab71094a72f
 ---> 7e81289d5267
Successfully built 7e81289d5267
Successfully tagged helidon-se-demo:latest

运行下这个命令确认下结果是否ok:

docker images helidon-se-demo

你可以在目录下找到一个叫helidon-se-demo的容器文件。我这里生成的文件大小是88.2MB。通过下面的命令来启动这个容器:

$ docker run -d -p 8080:8080 helidon-se-demo

docker run命令加上-d开关后会在后台运行容器实例,-p开关用来指定端口。最后是要运行的镜像名,这里是helidon-se-demo。

如果想看下你的系统中有哪些容器在运行,可以使用这个命令:

$ docker ps -a

你也可以使用像KitematicPortainer这样的GUI工具。我个人比较喜欢Portainer,现在用它来看下运行状态,结果如图一所示。

图一

当然你也可以访问http:localhost:8080/greet来确认下应用程序是否还在本地运行着(只不过这次它是运行在Docker里了)。

在Kubernetes中运行

了解完Helidon对Docker的支持度后,我们再来看看它对Kubernetes支持得怎么样。先kill掉Docker容器(命令行或GUI工具都可以)。然后看一下生成的target/app.yaml文件。它的内容如下:

kind: Service
apiVersion: v1
metadata:
  name: helidon-se-demo
  labels:
    app: helidon-se-demo
spec:
  type: NodePort
  selector:
    app: helidon-se-demo
  ports:
  - port: 8080
    targetPort: 8080
    name: http
---
kind: Deployment
apiVersion: extensions/v1beta1
metadata:
  name: helidon-se-demo
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: helidon-se-demo
        version: v1
    spec:
      containers:
      - name: helidon-se-demo
        image: helidon-se-demo
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 8080
---

这里我不再详细介绍配置细节,你可以用它来快速地将应用部署到Kubernetes中,Kubernetes则提供了容器管理和编排的能力。通过下述命令将它部署到Kubernetes集群里(同样的,也是工程根目录下执行,否则的话需要更新下app.yaml的路径):

$ kubectl create -f target/app.yaml

如果一切正常,应该能看到这样的结果:

service/helidon-se-demo created
deployment.extensions/helidon-se-demo created

可以通过kubectl get deployments来确认下部署情况,kubectl get services可以用来检查服务状态:

NAME              TYPE     CLUSTER-IP     EXTERNAL-IP PORT(S)
helidon-se-demo   NodePort 10.105.215.173 <none>      8080:32700/TCP

可以看到现在服务运行在32700端口上,你可以在浏览器中访问该地址确认一下。

目前为止我们已经搭建好一个应用、生成Docker容器,并且部署到了Kubernetes中——仍然没有写过一行代码。

那现在我们就换一换,来看一下代码。打开src/main/java/Main.java,看一看startServer()方法中Helidon SE是如何初始化内嵌的Netty服务器的:

protected static WebServer startServer() throws IOException {

    // load logging configuration
    LogManager.getLogManager().readConfiguration(
        Main.class.getResourceAsStream("/logging.properties"));

    // By default this will pick up application.yaml from

    // the classpath
    Config config = Config.create();

    // Get web server config from the "server" section of
    // application.yaml
    ServerConfiguration serverConfig =
        ServerConfiguration.fromConfig(config.get("server"));

    WebServer server =
        WebServer.create(serverConfig, createRouting());

    // Start the server and print some info.
    server.start().thenAccept(ws -> {
        System.out.println(
            "WEB server is up! http://localhost:" + ws.port());
    });

    // Server threads are not demon. NO need to block. Just react.
    server.whenShutdown().thenRun(()
        -> System.out.println("WEB server is DOWN. Goodbye!"));

    return server;
}

代码中生成的注释已经解释的很清楚了,总结一下:

  1. 日志初始化:从生成的application.yaml中获取配置信息(额外的配置变量可以放到这里)
  2. 通过配置文件中的host/port信息来创建一个ServerConfiguration实例。
  3. 创建并启动WebServer实例,将createRouting()返回的路由信息传给它。

createRouting()方法是这样注册服务的:

private static Routing createRouting() {
    return Routing.builder()
             .register(JsonSupport.get())
             .register("/greet", new GreetService())
             .build();
}

这里我们注册了"/greet"服务,指向了GreetService。会看到有几个类变量通过Config从前面提到的application.yaml文件中获取配置值。

private static final Config CONFIG =
    Config.create().get("app");
private static String greeting =
    CONFIG.get("greeting").asString("Ciao");

GreetService类实现了Service接口并重写了update()方法,里面定义了子路径/greet的实现。

@Override
public final void update(final Routing.Rules rules) {
    rules
        .get("/", this::getDefaultMessage)
        .get("/{name}", this::getMessage)
        .put("/greeting/{greeting}", this::updateGreeting);
}

update()方法接收Routing.Rules的实例对象,Routing.Rules的方法分别对应着不同的HTTP请求——get(),post(),put(),head(),options()和trace()——还有一些比较有用的方法比如any(),它可以用来兜底,实现一些日志或安全类的功能。

这里我注册了三个endpoint:/greet/, /greet/{name}和/greet/greeting。每个endpoint都有一个指向服务方法的引用。注册成endpoint的方法接收两个参数:request和response。这样设计的话,你可以从request中获取参数,比如请求头及参数,也可以往response中设置响应头及响应体。getDefaultMessage()方法的内容如下:

private void getDefaultMessage(final ServerRequest request,
                               final ServerResponse response) {
    String msg = String.format("%s %s!", greeting, "World");
    JsonObject returnObject = Json.createObjectBuilder()
            .add("message", msg)
            .build();
    response.send(returnObject);
}

这是个非常简单的例子,但是也能看出服务方法的基本实现结构。getMessage()方法是一个动态路径参数({name}参数是在URL路径中注册进来的)的例子,你可以从URL中获取参数。

private void getMessage(final ServerRequest request,
                        final ServerResponse response) {
    String name = request.path().param("name");
    String msg = String.format("%s %s!", greeting, name);
    JsonObject returnObject = Json.createObjectBuilder()
            .add("message", msg)
            .build();
    response.send(returnObject);
}

http://localhost:8080/greet/todd的结果如图二所示。

图二

下面要讲的updateGreeting()方法和getMessage()有很大的不同,需要注意的是这里只能调用Put方法而不是get,因为在update()里就是这样注册的。

private void updateGreeting(final ServerRequest request, final ServerResponse response)
{
    greeting = request.path().param("greeting");
    JsonObject returnObject = Json.createObjectBuilder()
            .add("greeting", greeting)
            .build();
    response.send(returnObject);
}

Helidon SE还包含很多东西,包括异常处理、静态内容、metrics以及健康度。强烈推荐阅读下项目文档来了解更多特性。

Helidon MP入门

Helidon MP是MicroProfile规范的实现版本。如果你使用过Java EE的话应该不会觉得陌生。前面也提到,你可能会看到像JAX-RS/Jersey, JSON-P以及CDI这些常用的东西。

和Helidon SE一样,我们先通过Helidon MP的项目模板来快速创建一个工程:

$ mvn archetype:generate -DinteractiveMode=false \
    -DarchetypeGroupId=io.helidon.archetypes \
    -DarchetypeArtifactId=helidon-quickstart-mp \
    -DarchetypeVersion=0.10.2 \
    -DgroupId=codes.recursive \
    -DartifactId=helidon-mp-demo \
    -Dpackage=codes.recursive.helidon.mp.demo

看一下Main.java类,你会发现它比Helidon SE还要简单。

protected static Server startServer() throws IOException {
    // load logging configuration
    LogManager.getLogManager().readConfiguration(
        Main.class.getResourceAsStream("/logging.properties"));
    // Server will automatically pick up configuration from
    // microprofile-config.properties
    Server server = Server.create();
    server.start();

    return server;
}

应用的定义在GreetApplication类中,它的getClasses()方法中注册了路由资源。

@ApplicationScoped
@ApplicationPath("/")
public class GreetApplication extends Application {
    @Override
    public Set<Class<?>> getClasses() {
        Set<Class<?>> set = new HashSet<>();
        set.add(GreetResource.class);
        return Collections.unmodifiableSet(set);
    } 

}

Helidon MP中的GreetResource和Helidon SE中的GreetService的角色差不多。不过它不用单独去注册路由信息,你可以使用注解来表示endpoint、HTTP方法和content-type头。

@Path("/greet")
@RequestScoped
public class GreetResource {

    private static String greeting = null;

    @Inject
    public GreetResource(@ConfigProperty(name = "app.greeting")
        final String greetingConfig) {

        if (this.greeting == null) {
            this.greeting = greetingConfig;
        } 
    }

    @Path("/")
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public JsonObject getDefaultMessage() {
        String msg = String.format("%s %s!", greeting, "World");

        JsonObject returnObject = Json.createObjectBuilder()
            .add("message", msg)
            .build();
        return returnObject;
    }

    @Path("/{name}")
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public JsonObject getMessage(@PathParam("name") final String name){
        String msg = String.format("%s %s!", greeting, name);

        JsonObject returnObject = Json.createObjectBuilder()
            .add("message", msg)
            .build();
        return returnObject;
    }

    @Path("/greeting/{greeting}")
    @PUT
    @Produces(MediaType.APPLICATION_JSON)
    public JsonObject updateGreeting(@PathParam("greeting")
                                     final String newGreeting) {
        this.greeting = newGreeting;

        JsonObject returnObject = Json.createObjectBuilder()
                .add("greeting", this.greeting)
                .build();
        return returnObject;
    } 
}

Helidon MP和Helidon SE的区别还不止这些,但它们的目标都是一致的,即降低微服务的使用门槛。Helidon是一个功能非常强大的框架,能够帮助你快速开发微服务应用。如果你不希望使用容器技术,你也可以像部署传统jar一样去部署它。如果你的团队使用容器技术,它内建的支持能够帮忙你快速地部署到任何云上或自有的Kubernetes集群中。由于Helidon是Oracle公司开发的,因此团队后续会计划将它集成到Oracle Cloud上。如果你已经在使用Oracle Cloud部署应用,或者最近有计划要迁移到上面,那么Helidon将是你的不二选择。

英文原文链接

« Java 10的类型推导 Kubernetes简介及入门 »


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK