在本文中,您将学习如何运行Spring Boot微服务,这些服务在Knative上相互通信。我还向您展示了如何使用GraalVM准备Spring Boot应用程序的本机映像。然后我们将使用Skaffold和jibmaven插件在Kubernetes上运行它。

在Knative上,您可以运行任何类型的应用程序,而不仅仅是函数。在本文中,当我编写“微服务”时,实际上,我考虑的是服务对服务的通信。

作为本文中的微服务示例,我使用了两个应用程序callme service和caller service。它们都公开了一个端点,该端点打印应用程序pod的名称。调用方服务应用程序还调用callme服务应用程序公开的端点。

在Kubernetes上,这两个应用程序都将作为Knative服务部署在多个版本中。我们还将使用Knative路由在这些修订版中分配流量。下图说明了我们的示例系统的体系结构。

准备Spring Boot微服务

我们有两个简单的Spring Boot应用程序,它们公开一个REST端点、运行状况检查,并运行内存中的H2数据库。我们使用Hibernate和Lombok。因此,我们需要在Maven pom.xml 中包含以下依赖项列表。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.16</version>
</dependency>

每次调用ping端点时,它都会创建一个事件并将其存储在H2数据库中。REST端点返回Kubernetes中pod和命名空间的名称以及事件的id。该方法在我们对集群的手动测试中很有用。

@RestController
@RequestMapping("/callme")
public class CallmeController {

    @Value("${spring.application.name}")
    private String appName;
    @Value("${POD_NAME}")
    private String podName;
    @Value("${POD_NAMESPACE}")
    private String podNamespace;
    @Autowired
    private CallmeRepository repository;

    @GetMapping("/ping")
    public String ping() {
        Callme c = repository.save(new Callme(new Date(), podName));
        return appName + "(id=" + c.getId() + "): " + podName + " in " + podNamespace;
    }

}

这是我们的模型课—— Callme 。调用者服务应用程序中的模型类非常类似。

@Entity
@Getter
@Setter
@NoArgsConstructor
@RequiredArgsConstructor
public class Callme {

    @Id
    @GeneratedValue
    private Integer id;
    @Temporal(TemporalType.TIMESTAMP)
    @NonNull
    private Date addDate;
    @NonNull
    private String podName;

}

另外,让我们看看 CallerController 中 ping 方法的第一个版本。稍后我们将在讨论通信和跟踪时对其进行修改。现在,理解这个方法还调用callme服务公开的ping方法并返回整个响应是很重要的。

@GetMapping("/ping")
public String ping() {
    Caller c = repository.save(new Caller(new Date(), podName));
    String callme = callme();
    return appName + "(id=" + c.getId() + "): " + podName + " in " + podNamespace
            + " is calling " + callme;
}

使用GraalVM准备Spring Boot本机映像

Spring Native支持使用GraalVM本机编译器将Spring应用程序编译为本机可执行文件。有关此项目的更多详细信息,请参阅其文档:
https://docs.spring.io/spring-native/docs/current/reference/htmlsingle/

这是我们应用程序的主要类。

@SpringBootApplication
public class CallmeApplication {

   public static void main(String[] args) {
      SpringApplication.run(CallmeApplication.class, args);
   }

}

Hibernate在运行时做很多动态的事情。因此,我们需要使用Hibernate在构建时增强应用程序中的实体。我们需要将以下Maven插件添加到我们的构建中。

<plugin>
   <groupId>org.hibernate.orm.tooling</groupId>
   <artifactId>hibernate-enhance-maven-plugin</artifactId>
   <version>${hibernate.version}</version>
   <executions>
      <execution>
         <configuration>
            <failOnError>true</failOnError>
            <enableLazyInitialization>true</enableLazyInitialization>
            <enableDirtyTracking>true</enableDirtyTracking>
            <enableExtendedEnhancement>false</enableExtendedEnhancement>
         </configuration>
         <goals>
            <goal>enhance</goal>
         </goals>
      </execution>
   </executions>
</plugin>

在本文中,我使用的是最新版本的Spring Native–0.9.0。因为springnative是积极开发的,所以后续版本之间有很大的变化。如果您将它与其他基于早期版本的文章进行比较,我们不必禁用 proxyBeansMethods 、排除
SpringDataWebAutoConfiguration 、将spring上下文索引器添加到依赖项或创建 hibernate.properties 。酷!我还可以使用Buildpacks来构建本机映像。

所以,现在我们只需要添加以下依赖项。

<dependency>
   <groupId>org.springframework.experimental</groupId>
   <artifactId>spring-native</artifactId>
   <version>0.9.0</version>
</dependency>

Spring AOT插件执行改进本机映像兼容性和占用空间所需的提前转换。

<plugin>
    <groupId>org.springframework.experimental</groupId>
    <artifactId>spring-aot-maven-plugin</artifactId>
    <version>${spring.native.version}</version>
    <executions>
        <execution>
            <id>test-generate</id>
            <goals>
                <goal>test-generate</goal>
            </goals>
        </execution>
        <execution>
            <id>generate</id>
            <goals>
                <goal>generate</goal>
            </goals>
        </execution>
    </executions>
</plugin>

使用Buildpacks在Knative上运行本机映像

使用Builpacks创建本机映像是我们的主要选择。尽管它需要一个Docker守护进程,但它在每个操作系统上都能正常工作。但是,我们需要使用最新的稳定版本的springboot。在这种情况下,它是2.4.3。您还可以使用springbootmaven插件在Maven pom.xml 中配置Buildpacks。因为我们需要一步到位地在Kubernetes上构建和部署应用程序,所以我更喜欢Skaffold中的配置。我们使用
PakeToBuildPack/builder:tiny 。还需要使用 BP_BOOT_NATIVE_IMAGE 环境变量启用本机生成选项。

apiVersion: skaffold/v2beta11
kind: Config
metadata:
  name: callme-service
build:
  artifacts:
  - image: piomin/callme-service
    buildpacks:
      builder: paketobuildpacks/builder:tiny
      env:
        - BP_BOOT_NATIVE_IMAGE=true
deploy:
  kubectl:
    manifests:
      - k8s/ksvc.yaml

Skaffold配置引用了我们的Knative服务清单。这是非常不典型的,因为我们需要将pod和名称空间名称注入到容器中。我们还允许每个pod最多10个并发请求。如果超过此值,则可以放大许多正在运行的实例。

apiVersion: serving.knative.dev/v1
kind: Service
metadata:
  name: callme-service
spec:
  template:
    spec:
      containerConcurrency: 10
      containers:
      - name: callme
        image: piomin/callme-service
        ports:
          - containerPort: 8080
        env:
          - name: POD_NAME
            valueFrom:
              fieldRef:
                fieldPath: metadata.name
          - name: POD_NAMESPACE
            valueFrom:
              fieldRef:
                fieldPath: metadata.namespace

默认情况下,Knative不允许使用Kubernetes fieldRef特性。为了启用它,我们需要更新knative服务命名空间中的knative features ConfigMap。所需的属性名为
kubernetes.podspec-fieldref 。

kind: ConfigMap
apiVersion: v1
metadata:
  annotations:
  namespace: knative-serving
  labels:
    serving.knative.dev/release: v0.16.0
data:
  kubernetes.podspec-fieldref: enabled

最后,我们可以使用以下命令在Knative上构建和部署Spring-Boot微服务。

$ skaffold run

使用Jib在Knative上运行本机映像

与我上一篇关于Knative的文章一样,我们将使用Skaffold和Jib在Kubernetes上构建和运行我们的应用程序。幸运的是,jibmaven插件已经引入了对GraalVM“原生图像”的支持。Jib-GraalVM本机映像扩展期望本机映像maven插件完成生成“本机映像”(使用本机映像native-image:native-image). 然后扩展只需将二进制文件复制到容器映像中,并将其设置为可执行文件。

当然,与Java字节码不同,本机映像不是可移植的,而是特定于平台的。本机映像Maven插件不支持交叉编译,因此本机映像应该构建在与运行时体系结构相同的操作系统上。因为我在ubuntu20.10上构建了我的应用程序的GraalVM映像,所以我应该使用相同的基本Docker映像来运行容器化的微服务。在这种情况下,我选择了图像ubuntu:20.10 as 如下所示。

<plugin>
   <groupId>com.google.cloud.tools</groupId>
   <artifactId>jib-maven-plugin</artifactId>
   <version>2.8.0</version>
   <dependencies>
      <dependency>
         <groupId>com.google.cloud.tools</groupId>
         <artifactId>jib-native-image-extension-maven</artifactId>
         <version>0.1.0</version>
      </dependency>
   </dependencies>
   <configuration>
      <from>
         <image>ubuntu:20.10</image>
      </from>
      <pluginExtensions>
         <pluginExtension>
            <implementation>com.google.cloud.tools.jib.maven.extension.nativeimage.JibNativeImageExtension</implementation>
         </pluginExtension>
      </pluginExtensions>
   </configuration>
</plugin>

如果您使用jibmaven插件,首先需要构建一个本机映像。为了构建应用程序的本机映像,我们还需要包含一个本机映像maven插件。当然,您需要使用graalvmjdk构建我们的应用程序。

<plugin>
   <groupId>org.graalvm.nativeimage</groupId>
   <artifactId>native-image-maven-plugin</artifactId>
   <version>21.0.0.2</version>
   <executions>
      <execution>
         <goals>
            <goal>native-image</goal>
         </goals>
         <phase>package</phase>
      </execution>
   </executions>
</plugin>

因此,本节的最后一部分只是运行Maven构建。在我的配置中,需要在本机映像配置文件下激活本机映像maven插件。

$ mvn clean package -Pnative-image

生成之后,callme服务的本机映像在目标目录中可见。

skafold的配置是典型的。我们只需要启用Jib作为构建工具。

apiVersion: skaffold/v2beta11
kind: Config
metadata:
  name: callme-service
build:
  artifacts:
  - image: piomin/callme-service
    jib: {}
deploy:
  kubectl:
    manifests:
      - k8s/ksvc.yaml

最后,我们可以使用以下命令在Knative上构建和部署Spring-Boot微服务。

$ skaffold run

网络上微服务间的通信

我在Knative上部署了每个应用程序的两个修订版。作为比较,部署的应用程序的第一个版本是用OpenJDK编译的。只有最新版本是基于GraalVM本地映像的。因此,我们可以比较两个版本的启动时间。

让我们看看部署了两个版本的应用程序之后的修订列表。流量被分成60%到最新版本,40%到每个应用程序的前一版本。

在幕后,Knative创建了Kubernetes服务和多个部署。每次修订都有一个部署。此外,还有多个服务,但每次修订都有一个服务。该服务是ExternalName服务类型。假设您仍然希望在多个修订版之间分割流量,那么您应该在通信中完全使用该服务。服务的名称是callme service。但是,我们应该使用带有命名空间名称和 svc.cluster.local 后缀的FQDN名称。

我们可以使用Spring RestTemplate来调用callme服务公开的端点。为了保证对整个请求路径的跟踪,我们需要在后续调用之间传播Zipkin头。对于通信,我们将使用具有完全限定的内部域名的服务( callme
service.serverless.svc.cluster.local ),如前所述。

@RestController
@RequestMapping("/caller")
public class CallerController {

   private RestTemplate restTemplate;

   CallerController(RestTemplate restTemplate) {
      this.restTemplate = restTemplate;
   }

   @Value("${spring.application.name}")
   private String appName;
   @Value("${POD_NAME}")
   private String podName;
   @Value("${POD_NAMESPACE}")
   private String podNamespace;
   @Autowired
   private CallerRepository repository;

   @GetMapping("/ping")
   public String ping(@RequestHeader HttpHeaders headers) {
      Caller c = repository.save(new Caller(new Date(), podName));
      String callme = callme(headers);
      return appName + "(id=" + c.getId() + "): " + podName + " in " + podNamespace
                     + " is calling " + callme;
   }

   private String callme(HttpHeaders headers) {
      MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
      Set<String> headerNames = headers.keySet();
      headerNames.forEach(it -> map.put(it, headers.get(it)));
      HttpEntity httpEntity = new HttpEntity(map);
      ResponseEntity<String> entity = restTemplate
         .exchange("http://callme-service.serverless.svc.cluster.local/callme/ping",
                  HttpMethod.GET, httpEntity, String.class);
      return entity.getBody();
   }

}

为了测试微服务之间的通信,我们只需要通过Knative路由调用调用方服务。

让我们对调用方服务 GET/caller/ping 端点执行一些测试调用。我们应该使用网址
http://caller-service-serverless.apps.cluster-d556.d556.sandbox262.opentlc.com/caller/ping

在第一个to requests caller服务中,调用最新版本的callme服务(用GraalVM编译)。在第三个请求中,它与旧版本的callme服务(用OpenJDK编译)通信。让我们比较一下同一应用程序的这两个版本的启动时间。

对于GraalVM,我们有0.3秒而不是5.9秒。我们还应该记住,我们的应用程序启动了一个内存中的嵌入式H2数据库。

用Jaeger配置跟踪

为了启用对Knative的跟踪,我们需要更新Knative服务命名空间中的Knative跟踪ConfigMap。当然,我们首先需要在集群中安装Jaeger。

apiVersion: operator.knative.dev/v1alpha1
kind: KnativeServing
metadata:
  name: knative-tracing
  namespace: knative-serving
spec:
  sample-rate: "1" 
  backend: zipkin 
  zipkin-endpoint: http://jaeger-collector.knative-serving.svc.cluster.local:9411/api/v2/spans 
  debug: "false"

你也可以使用Helm来安装Jaeger。使用此选项,您需要执行以下Helm命令。

$ helm repo add jaegertracing https://jaegertracing.github.io/helm-charts
$ helm install jaeger jaegertracing/jaeger

Knative会自动创建Zipkin span头。我们唯一的目标是在调用方服务和callme服务应用程序之间传播HTTP报头。在我的配置中,Knative向Jaeger发送100%的跟踪。让我们看看knative微服务网格中 GET/caller/ping 端点的一些跟踪。

我们还可以查看每个请求的详细视图。

结论

在Knative上运行微服务时,需要考虑以下几点。我把重点放在与沟通和追踪有关的方面。我还展示了SpringBoot不需要在几秒钟内启动。使用GraalVM,它可以在毫秒内启动,因此您可以肯定地将其视为一个无服务器serverless框架。

原文链接:
https://piotrminkowski.com/2021/03/05/microservices-on-knative-with-spring-boot-and-graalvm/

如果你觉的本文对你有帮助,麻烦点赞关注支持一下