使用Spring Boot和GraalVM在Knative上構建微服務 - piotr

banq發表於2022-02-06

在本文中,您將學習如何在 Knative 上執行相互通訊的 Spring Boot 微服務。我還將向您展示如何使用 GraalVM 準備 Spring Boot 應用程式的本機映象/映像。然後我們將使用 Skaffold 和 Jib Maven 外掛在 Kubernetes 上執行它。

在 Knative 上,您可以執行任何型別的應用程式——不僅僅是一個函式。在這篇文章中,當我寫“微服務”時,其實我在思考的是服務到服務的通訊。

原始碼

如果您想自己嘗試一下,可以隨時檢視我的原始碼。為此,您需要克隆我的 GitHub 儲存庫

作為本文中的微服務示例,我使用了兩個應用程式callme-service和caller-service. 它們都公開了一個端點,該端點列印了應用程式 pod 的名稱。caller-service應用程式還呼叫應用程式公開的端點callme-service。

在 Kubernetes 上,這兩個應用程式都將部署為多個修訂版的 Knative 服務。我們還將使用 Knative 路由在這些修訂中分配流量。下面可見的圖片說明了我們示例系統的架構

使用Spring Boot和GraalVM在Knative上構建微服務 - piotr

 

1.準備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。應用程式中的模型類caller-service非常相似。

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

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

}

另外,讓我們看一下ping. CallerController稍後我們將在討論通訊和跟蹤時對其進行修改。現在,重要的是要了解此方法還呼叫 ping 暴露的方法callme-service並返回整個響應。

@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;
}

  

2. 使用 GraalVM 準備 Spring Boot 原生映象

Spring Native 支援使用 GraalVM 本機編譯器將 Spring 應用程式編譯為本機可執行檔案。有關此專案的更多詳細資訊,您可以參考其文件。這是我們應用程式的主要類。

@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。由於 Spring Native 正在積極開發中,後續版本之間會有較大的變化。如果您將其與其他基於早期版本的文章進行比較,我們不必禁用proxyBeansMethods、排除SpringDataWebAutoConfiguration、新增spring-context-indexer到依賴項或建立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>

 

3. 使用 Buildpacks 在 Knative 上執行原生映象

使用 Builpacks 建立原生映象是我們的主要選擇。雖然它需要一個 Docker 守護程式,但它在每個作業系統上都能正常工作。但是,我們需要使用最新的穩定版 Spring Boot。在這種情況下,它是2.4.3。您也可以在 Maven pom.xml 中使用spring-boot-maven-plugin. 由於我們需要在 Kubernetes 上一步構建和部署應用程式,因此我更喜歡在 Skaffold 中進行配置。我們paketobuildpacks/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 配置是指我們的 KnativeService清單。這是非常不典型的,因為我們需要將 pod 和名稱空間名稱注入到容器中。我們還允許每個 pod 最多有 10 個併發請求。如果超過,Knative 會擴大一些正在執行的例項。

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 不允許使用 KubernetesfieldRef功能。為了啟用它,我們需要更新名稱空間knative-features ConfigMap中的knative-serving。所需的屬性名稱是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

 

4. 使用 Jib 在 Knative 上執行原生映象

與我之前關於 Knative 的文章一樣,我們將使用 Skaffold 和 Jib 在 Kubernetes 上構建和執行我們的應用程式。幸運的是,Jib Maven Plugin 已經引入了對 GraalVM “native images”的支援。Jib GraalVM Native Image Extension 希望 能夠完成生成“原生影像”( 目標)native-image-maven-plugin 的繁重工作 。native-image:native-image然後擴充套件只是簡單地將二進位制檔案複製到容器映像中並將其設定為可執行檔案。

當然,與 Java 位元組碼不同,本機映像不可移植,而是特定於平臺的。Native Image Maven Plugin 不支援交叉編譯,因此 native-image 應該構建在與執行時架構相同的作業系統上。由於我在 Ubuntu 20.10 上構建了我的應用程式的 GraalVM 映像,因此我應該使用相同的基礎 Docker 映像來執行容器化微服務。在這種情況下,我選擇了映象ubuntu:20.10,如下所示。

<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>

如果你使用 Jib Maven 外掛,你首先需要構建一個原生映象。為了構建應用程式的本機映像,我們還需要包含一個native-image-maven-plugin. 你需要使用 GraalVM JDK 構建我們的應用程式。

<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 構建。在我的配置中,native-image-maven-plugin需要在native-image配置檔案下啟用一個。

$ mvn clean package -Pnative-image

Skaffold 的配置是典型的。我們只需要啟用 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

 

5. Knative 上微服務之間的通訊

我在 Knative 上部署了每個應用程式的兩個修訂版。只是為了比較,部署應用程式的第一個版本是使用 OpenJDK 編譯的。只有最新版本基於 GraalVM 原生映象。因此,我們可以比較兩個版本的啟動時間。

讓我們看一下部署我們應用程式的兩個版本後的修訂列表。流量分為 60% 到最新版本,40% 到每個應用程式的先前版本。

 

使用Spring Boot和GraalVM在Knative上構建微服務 - piotr

在底層,Knative 建立了 KubernetesServices和多個Deployments. Deployment每個 Knative總是有一個Revision。此外,有多種服務,但始終其中一項服務是針對所有修訂版的。那Service是一種ExternalName服務型別。假設您仍想在多個修訂版之間拆分流量,您應該在通訊中準確使用該服務。服務的名稱是callme-service。但是,我們應該使用帶有名稱空間名稱和svc.cluster.local字尾的 FQDN 名稱。

使用Spring Boot和GraalVM在Knative上構建微服務 - piotr

我們可以使用 SpringRestTemplate來呼叫callme-service. 為了保證對整個請求路徑的跟蹤,我們需要在後續呼叫之間傳播 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();
   }

}

為了測試我們的微服務之間的通訊,我們只需要caller-service通過 Knative呼叫Route。

使用Spring Boot和GraalVM在Knative上構建微服務 - piotr

GET /caller/ping讓我們對呼叫者服務端點執行一些測試呼叫。我們應該使用 URL http://caller-service-serverless.apps.cluster-d556.d556.sandbox262.opentlc.com/caller/ping。

使用Spring Boot和GraalVM在Knative上構建微服務 - piotr

在第一次請求 caller-service 時呼叫最新版本的 callme-service(用 GraalVM 編譯)。在第三個請求中,它與舊版本的 callme-service(使用 OpenJDK 編譯)進行通訊。讓我們比較同一應用程式的這兩個版本的啟動時間。

使用Spring Boot和GraalVM在Knative上構建微服務 - piotr

使用 GraalVM,我們有0.3s而不是5.9s。我們還應該記住,我們的應用程式會啟動一個記憶體中的嵌入式 H2 資料庫。

使用Spring Boot和GraalVM在Knative上構建微服務 - piotr

 

6. 使用 Jaeger 配置跟蹤

為了啟用 Knative 的跟蹤,我們需要更新名稱空間knative-tracing ConfigMap中的knative-serving。當然,我們首先需要在我們的叢集中安裝 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 chart 來安裝 Jaeger。使用此選項,您需要執行以下 Helm 命令。

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

Knative 會自動建立 Zipkin span headers。我們唯一的目標是在caller-service和callme-service應用程式之間傳播 HTTP 標頭。在我的配置中,Knative 向 Jaeger 傳送 100% 的跟蹤資訊。讓我們看一下GET /caller/pingKnative 微服務網格中端點的一些跟蹤。

使用Spring Boot和GraalVM在Knative上構建微服務 - piotr

我們還可以檢視每個請求的詳細檢視。

使用Spring Boot和GraalVM在Knative上構建微服務 - piotr

 

結論

在 Knative 上執行微服務時,需要考慮幾件重要的事情。我專注於與通訊和跟蹤相關的方面。我還展示了 Spring Boot 不必在幾秒鐘內啟動。使用 GraalVM,它可以在幾毫秒內啟動,因此您絕對可以將其視為無伺服器框架。

相關文章