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

阅读数:766 2018 年 4 月 25 日

话题:Java语言 & 开发架构Kubernetes

关键要点

  • 服务网格将所需的技术关注点透明地添加到微服务中。
  • 路由、弹性或认证等问题成为服务网格的职责。
  • 应用程序代码变得更加精简,并更多地关注实际的业务逻辑。
  • 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 相结合,是未来企业应用程序的最佳选择。

更多资源

关于作者

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