在本文中,您将学习如何运行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/
如果你觉的本文对你有帮助,麻烦点赞关注支持一下