转载

基于云原生技术和服务网格的Java EE

关键要点

  • 服务网格将所需的技术关注点透明地添加到微服务中。
  • 路由、弹性或认证等问题成为服务网格的职责。
  • 应用程序代码变得更加精简,并更多地关注实际的业务逻辑。
  • Istio通过边车代理容器增强工作负载,例如Kubernetes Pod。
  • Java EE通过支持开发人员实施精益业务逻辑与云原生技术完美地集成在一起。

Java EE、云原生和服务网格——听起来似乎不应该把它们放在一起,又或者它们确实应该在一起呢?我们是否有可能在无需自己实现所有东西的情况下开发现代云原生Java企业应用程序来满足可扩展性、监控、跟踪或路由等问题?如果可以,那么该怎样实现?

采用微服务架构的企业面临着一个挑战,就是如何将服务发现、安全、监控、跟踪、路由或故障处理等技术问题以一致的方式添加到微服务当中。软件团队可以使用不同的技术来实现各自的服务,但他们需要遵守组织标准。通过添加共享资源(如API网关)将微服务耦合在一起,这在某种程度上破坏了微服务架构的原本意义。但不管怎样,我们应该避免冗余。

服务网格增强了每个微服务,而这些微服务是网格的一部分。这些增强功能以​​与技术无关的方式添加到系统中,不会影响到应用程序。应用程序专注于实现业务逻辑,由环境负责来处理技术依赖。

仪器和3D打印机

我们即将要给出的示例应用程序是仪器商店和3D打印机机器人。假设有这样的一个仪器商店SaaS应用程序,客户可以通过这个应用程序订购制作好的仪器。商店本身不直接提供仪器,而是将请求转发给通过3D打印技术生产仪器的机器人。

我们使用Java EE 8来实现这两种云原生微服务,部署到Kubernetes集群中,并由Istio来管理。

云原生技术简介

为了使用Kubernetes和Istio来管理Java EE应用程序,我们需要将它们打包成容器。 Docker镜像是通过定义Dockerfile文件来创建的。这些文件指定了整个应用程序的运行时,包括配置、Java运行时(即JRE和应用程序容器)以及所需的操作系统二进制文件。

下面的Dockerfile用于打包仪器商店应用程序,它使用了一个自定义基础镜像,其中包含了一个OpenLiberty应用服务器:

FROM docker.example.com/open-liberty:1

COPY target/instrument-craft-shop.war $DEPLOYMENT_DIR

OpenLiberty基础镜像里已经包含了运行应用程序服务器所必需的东西。Dockerfile将添加可能需要用到的配置。通过使用这种简单的部署方法,我们不仅很好地利用了Docker的Copy-On-Write文件系统,而且带来了快速构建和缩短交付时间的可能性。

构建好的镜像将在编排环境中运行,在我们的例子里,就是要在Kubernetes群集中运行。

因此,与Kubernetes环境相关的文件也成为应用程序代码库的一部分。 YAML描述符包含了集群将如何运行、分布和组织我们的应用程序及Docker容器。

以下显示了仪器商店服务的定义,Kubernetes服务是对应用程序的逻辑抽象。

kind: Service
apiVersion: v1
metadata:
  name: instrument-craft-shop
  labels:
    app: instrument-craft-shop
spec:
  selector:
    app: instrument-craft-shop
  ports:
    - port: 9080
      name: http

该服务将请求分发给正在运行的实例,容器则由Kubernetes来管理。Kubernetes部署文件定义了如何执行Kubernetes Pod(也就是实际运行的工作负载)以及需要多少副本:

kind: Deployment
apiVersion: apps/v1beta1
metadata:
  name: instrument-craft-shop
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: instrument-craft-shop
        version: v1
    spec:
      containers:
      - name: instrument-craft-shop
        image: docker.example.com/instrument-craft-shop:1
        imagePullPolicy: IfNotPresent
      restartPolicy: Always

该服务将采用与定义的选择器相匹配的Pod。这里的应用程序标签实际上是标准名称,与我们的应用程序相匹配。我们最好可以再定义一个版本标签,以便在多个应用程序版本同时存在的时候能够进一步自定义服务路由。

集群外部的客户端将会调用仪器商店应用程序。Kubernetes的摄入资源将入口流量路由到相应的服务:

kind: Ingress
apiVersion: extensions/v1beta1
metadata:
  name: instrument-craft-shop
  annotations:
    kubernetes.io/ingress.class: istio
spec:
  rules:
  - http:
      paths:
      - path: /instrument-craft-shop/.*
        backend:
          serviceName: instrument-craft-shop
          servicePort: 9080

ingress.class注释指定了将istio作为入口实现,因此Kubernetes将为我们部署正确的Istio入口。

仪器商店应用程序将通过HTTP与后端的机器人应用程序进行通信。机器人应用程序定义了类似的Kubernetes服务和部署资源,名为maker-bot。

因为这两个应用程序都是Kubernetes群集的一部分,所以它们可以使用服务定义作为主机名进行通信。 Kubernetes通过DNS来解析服务名称。

下面是机器人客户端的代码:

@ApplicationScoped
public class MakerBot {

    private Client client;
    private WebTarget target;

    @PostConstruct
    private void initClient() {
        client = ClientBuilder.newBuilder()
                .connectTimeout(1, TimeUnit.SECONDS)
                .readTimeout(3, TimeUnit.SECONDS)
                .build();
        target = client.target("http://maker-bot:9080/maker-bot/resources/jobs");
    }

    public void printInstrument(InstrumentType type) {
        JsonObject requestBody = createRequestBody(type);
        Response response = sendRequest(requestBody);
        validateResponse(response);
    }

    private JsonObject createRequestBody(InstrumentType type) {
        return Json.createObjectBuilder()
                .add("instrument", type.name().toLowerCase())
                .build();
    }

    private Response sendRequest(JsonObject requestBody) {
        try {
            return target
                .request()
                .post(Entity.json(requestBody));
        } catch (Exception e) {
            throw new IllegalStateException("Could not print instrument, reason: "
                    + e.getMessage(), e);
        }
    }

    private void validateResponse(Response response) {
        if (response.getStatusInfo().getFamily() != Response.Status.Family.SUCCESSFUL)
            throw new IllegalStateException("Could not print instrument, status: "
                    + response.getStatus());
    }

    @PreDestroy
    private void closeClient() {
        client.close();
    }
}

从Java EE 8开始,JAX-RS客户端构建器API支持connectTimeout和readTimeout方法。我们强烈建议设置这些超时参数,以防止长时间阻塞线程。

正如你所看到的,机器人应用程序通过主机名称maker-bot和端口9080(与Kubernetes的服务定义相匹配)进行配置。我们因此能够摆脱服务发现配置,例如为不同的环境定义不同的目标端点、IP地址或主机名称。不管在哪个Kubernetes集群环境中,URL都是稳定不变的,并可以被恰当地解析。

Istio登场

接下来我们将演示Istio,它是Java/JVM领域使用最为广泛的服务网格示例之一。

Istio将技术切面关注点透明地添加到应用程序中。它通过代理边车容器增强了应用程序Pod,捕获主容器的流入和流出流量。主应用程序容器连接到必要的服务上,但不知道代理服务器的存在。我们可以将Istio视为一个切面,就像在面向方面的编程模型里一样,它们被透明地添加到应用程序中。Istio可以使用多种编排框架实现,包括Kubernetes。

我们的示例应用程序被部署到Kubernetes集群中,这个集群使用了Istio和自动边车注入。边车注入自动将Istio代理容器添加到每个Pod。

Istio Pilot负责配置边车代理的路由规则和弹性参数。我们在YAML文件中配置Istio切面,类似Kubernetes的资源。

根据最佳实践,我们为相应的应用程序服务添加默认路由:

apiVersion: config.istio.io/v1alpha2
kind: RouteRule
metadata:
  name: instrument-craft-shop-default
spec:
  destination:
    name: instrument-craft-shop
  precedence: 1
  route:
  - weight: 100
    labels:
      version: v1

该路由规则指定了所有流向仪器商店应用程序的流量都将被路由到版本为v1的实例上。Istio资源以与Kubernetes资源相同的方式添加到群集中,例如通过kubectl命令行。我们现在可以进一步增强这些路由规则。

以下路由规则给后端机器人增加了一个2秒种的超时时间:

apiVersion: config.istio.io/v1alpha2
kind: RouteRule
metadata:
  name: maker-bot-default
spec:
  destination:
    name: maker-bot
  precedence: 1
  route:
  - weight: 100
    labels:
      version: v1
  httpReqTimeout:
    simpleTimeout:
      timeout: 2s

这个超时时间与其他应用程序级别的超时时间是相互独立的,一旦被触发,代理就会返回503错误。这样可以防止系统出现无限制的阻塞,即使没有为MakerBot类的JAX-RS客户端配置超时时间。客户端将收到超时通知,以先触发的那个超时时间为准。

Istio的另一个特点是可以通过添加回路断路器来以防止应用程序出现过载和整体失效。我们为后端机器人目标策略添加了一个回路断路器:

apiVersion: config.istio.io/v1alpha2
kind: DestinationPolicy
metadata:
  name: maker-bot-circuit-breaker
spec:
  destination:
    name: maker-bot
  circuitBreaker:
    simpleCb:
      httpConsecutiveErrors: 1
      sleepWindow: 10s
      httpDetectionInterval: 10s
      httpMaxEjectionPercent: 100
      maxConnections: 1
      httpMaxPendingRequests: 1
      httpMaxRequestsPerConnection: 1

这个目标策略一次只允许一个连接,并会拒绝其他连接。我们还可以配置回路断路器如何再次打开和关闭,具体需要根据特定的系统设置进行调整。

被透明地添加到现有应用程序中的还有监视、日志和跟踪,以及身份认证。边车容器中包含了Envoy代理,我们就是通过它们来添加这些横切面关注点,并把它们暴露给外部环境。

DevOps工程师可以通过检查Grafana和Prometheus扩展或跟踪解决方案来访问他们需要的信息。我们通过加密边车代理之间的连接来添加认证。用户可以添加自己的证书,并配置通信策略。

结论

Java EE与服务网格背后的想法相得益彰,而技术横切面问题(如路由、弹性或认证)成为服务网格环境的职责。

实际上,Java EE是基于这个想法而构建起来的。应用程序本身应该更多地关注业务逻辑,解决实际的领域问题,为应用程序的用户提供价值。而处理生命周期管理、依赖注入、事务或线程等问题则是应用程序容器的职责。

编排框架和服务网格进一步采用这种方法,进行服务发现、增强弹性、认证、监控或追踪。因此这些问题不再是应用程序的关注点,应用程序应该专注于实现业务逻辑。

在未来,我们将使用存粹的Java EE 8或Jakarta EE来构建和打包应用程序,然后从应用程序外部添加技术横切面。

如果域名需要额外的关注点,例如与业务相关的指标,则可以通过集成第三方扩展来添加,例如MicroProfile Metrics。使用支持MicroProfile的容器,或者将第三方库安装到应用程序容器中,作为底层的Docker镜像层,我们仍然能够利用精简部署的优势。这种想法符合关注点分离原则。

Docker、Kubernetes和Istio等云原生技术与Java EE或将来的Jakarta EE相结合,是未来企业应用程序的最佳选择。

更多资源

  • GitHub项目:  instrument craft shop  &  maker bot
  • Kubernetes 文档
  • Istio 文档
  • Prometheus with Java EE & MicroProfile Metrics
  • Distributed tracing with Java EE, Istio & MicroProfile

关于作者

基于云原生技术和服务网格的Java EE Sebastian Daschner 是一名独立Java顾问、作者和培训师,对编程和Java(EE)充满热情。他是“架构现代Java EE应用程序”一书的作者。Sebastian积极参与JCP,帮助制定未来的Java EE标准,服务于JAX-RS、JSON-P和Config专家组,并在各种开源项目上进行合作。他因为在Java社区和生态系统中的贡献而被授予了Java Champion、Oracle Developer Champion和2016年JavaOne Rockstar的荣誉。除Java外,Sebastian还是Linux和云原生技术的重要用户。他通过@DaschnerS在 博客 、新闻组和推特上传播计算机科学实践。平常他还喜欢搭飞机或骑摩托环游世界。

查看英文原文: Get Ready for Cloud Native, Service-Meshed Java Enterprise

原文  http://www.infoq.com/cn/articles/cloud-native-service-mesh-java-ee
正文到此结束
Loading...