云原生微服务框架——Helidon

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

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

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

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

微服务并非万能的,但在许多场景下仍是很是有用的。咱们已经介绍了“为何”须要微服务,如今来介绍下”如何“实现微服务。react

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

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开始,来了解一下这个框架。docker

Helidon SE入门

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

确认下软件的版本:api

$ 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
你也可使用像Kitematic或[Portainer这样的GUI工具。我我的比较喜欢Portainer,如今用它来看下运行状态,结果如图一所示。
云原生微服务框架——Helidon
固然你也能够访问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的结果如图二所示

云原生微服务框架——Helidon
下面要讲的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以及健康度。强烈推荐阅读下项目文档(https://helidon.io/docs/latest/#/about/01_introduction)来了解更多特性

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将是你的不二选择。