Kubernetes在微服务中的最佳实践
你可以在网上找到许多关于如何正确构建微服务体系架构的最佳实践。其中之一是我以前写的一篇文章 Spring Boot在微服务中的最佳实践 。我把重点放在在生产上基于Spring Boot构建的微服务应用程序应该考虑哪些方面。我没有使用任何用于编排或管理应用程序的平台,而只是一组独立的应用程序。在本文中,我将基于已经介绍的最佳实践,在Kubernetes平台上部署微服务,你需要注意的一些新规则和事项。
第一个问题是,如果将微服务部署在Kubernetes上,而不是直接独立的运行它们,会有什么不同吗?答案可能是“相同”或者“不同”。不同在于你有了一个管理所有应用程序的平台,负责运行和监控你的应用程序,还会有一些附件的规则你需要遵守。相同在于你的整个应用程序体系仍然是微服务体系架构,由一组低耦合、独立的应用程序构成,你不应该忘记微服务体系架构的本质!在Kubernetes中,前面介绍的许多最佳实践都是依然有效的,只是有一些微调而已,还有一些新的最佳实践被引入。
有一点必须说明。这个最佳实践列表是我总结出的实际经验,并非从其他文章或书籍中抄袭而来。在我的组织中,我们已经将微服务从Spring Cloud (Eureka、Zuul、Spring Cloud Config)迁移到了OpenShift中,基于我们的维护情况,不断地演进此架构。
代码
以下所有代码由Kotlin编写,你可以在如下链接中找到所有完整代码。 sample-spring-kotlin-microservice
1. 允许平台收集指标数据
我在我上一篇文章中加入了类似的章节。当时我们使用InfluxDB作为指标数据的存储介质。但在Kubernetes中,收集指标数据的方法发生了一些变化,所以我重新定义了这一章节,“允许平台收集指标数据”。他们的主要区别在于收集数据的方式。在Kubernetes中,我们推荐使用Prometheus,因为平台可以帮助我们管理。InfluxDB需要应用程序将指标数据推送给它。而Prometheus会定期地主动拉取指标数据。因此,我们的主要职责是在应用程序端为Prometheus提供端点暴露数据。
幸运的是,通过Spring Boot为Prometheus提供端点是非常容易的。你只需要添加如下依赖。
org.springframework.boot spring-boot-starter-actuator io.micrometer micrometer-registry-prometheus
我们还需要公开Spring Boot Actuator的HTTP端点。您可以只公开专用于Prometheus的端点,或者如下所示公开所有HTTP端点。
management.endpoints.web.exposure.include: '*'
启动应用程序后,你可以看到如下的可用端点, /actuator/prometheus
。
假设您在Kubernetes上运行您的应用程序,那么您需要部署和配置Prometheus,以便从您的Pod中提取日志。配置信息可以通过Kubernetes ConfigMap来实现。 prometheus.yml
文件应该包含 metrics_path
和 kubernetes_sd_configs
。Prometheus试图通过Kubernetes Endpoints来发现应用程序的Pod。应用程序应该使用 app=sample-spring-kotlin-microservice
进行标记,并公开端口。
apiVersion: v1 kind: ConfigMap metadata: name: prometheus labels: name: prometheus data: prometheus.yml: |- scrape_configs: - job_name: 'springboot' metrics_path: /actuator/prometheus scrape_interval: 5s kubernetes_sd_configs: - role: endpoints namespaces: names: - default relabel_configs: - source_labels: [__meta_kubernetes_service_label_app] separator: ; regex: sample-spring-kotlin-microservice replacement: $1 action: keep - source_labels: [__meta_kubernetes_endpoint_port_name] separator: ; regex: http replacement: $1 action: keep - source_labels: [__meta_kubernetes_namespace] separator: ; regex: (.*) target_label: namespace replacement: $1 action: replace - source_labels: [__meta_kubernetes_pod_name] separator: ; regex: (.*) target_label: pod replacement: $1 action: replace - source_labels: [__meta_kubernetes_service_name] separator: ; regex: (.*) target_label: service replacement: $1 action: replace - source_labels: [__meta_kubernetes_service_name] separator: ; regex: (.*) target_label: job replacement: ${1} action: replace - separator: ; regex: (.*) target_label: endpoint replacement: http action: replace
最后一步是把Prometheus部署在Kubernetes中。将ConfigMap作为配置文件添加到Prometheus 的Deployment中。然后你就可以通过路径来使用这个配置文件了,例如, --config.file=/prometheus2/prometheus.yml
。
apiVersion: apps/v1 kind: Deployment metadata: name: prometheus labels: app: prometheus spec: replicas: 1 selector: matchLabels: app: prometheus template: metadata: labels: app: prometheus spec: containers: - name: prometheus image: prom/prometheus:latest args: - "--config.file=/prometheus2/prometheus.yml" - "--storage.tsdb.path=/prometheus/" ports: - containerPort: 9090 name: http volumeMounts: - name: prometheus-storage-volume mountPath: /prometheus/ - name: prometheus-config-map mountPath: /prometheus2/ volumes: - name: prometheus-storage-volume emptyDir: {} - name: prometheus-config-map configMap: name: prometheus
现在,您可以通过访问 /targets
来验证Prometheus是否已经发现您的应用程序在Kubernetes上运行。
2. 准备正确格式的日志
收集日志的方法与收集指标数据非常类似。我们的应用程序不应该自己处理发送日志的过程。它只需要适当地格式化发送到输出流的日志。因为Docker为Fluentd提供了内置的日志驱动程序,所以在Kubernetes上运行的应用程序可以很方便地使用它作为日志收集器。这意味着在容器上不需要额外的代理来将日志推送到Fluentd。日志直接从STDOUT发送到Fluentd服务,不需要额外的日志文件或持久存储。Fluentd试图读取结构化的JSON数据,以便进行统一处理。
为了将我们的日志格式化为Fluentd可读的JSON格式,我们可以将Logstash Logback Encoder引入到我们的依赖中。
net.logstash.logback logstash-logback-encoder 6.3
然后,我们只需要在logback-spring.xml文件中为我们的Spring Boot应用程序设置一个默认的控制台日志追加器。
日志以如下所示的格式打印到STDOUT中。
在Minikube上安装Fluentd, Elasticsearch和Kibana非常简单。这种方法的缺点是版本不够新。
$ minikube addons enable efk * efk was successfully enabled $ minikube addons enable logviewer * logviewer was successfully enabled
启用efk和logviewer插件后,Kubernetes启动所有必需的Pod,如下所示。
感谢logstash-logback-encoder,我们可以自动创建与Fluentd兼容的日志,包括MDC字段。这是Kibana的页面截图,显示了我们的应用程序的日志。
您还可以添加我的库来记录Spring Boot应用程序的请求/响应。
com.github.piomin logstash-logging-spring-boot-starter 1.2.2.RELEASE
3.实现LIVENESS和READINESS健康检查
理解Kubernetes中liveness探针和readiness探针的区别是很重要的。不正确使用这些探针,可能会降低服务的整体操作性,例如导致不必要的重新启动容器。liveness探针用于判断是否需要重启容器。如果应用程序因为任何原因不可用,重新启动容器有时是有意义的。而readiness探针用于确认容器是否能够处理请求。如果readiness探针失败,则将应用程序从负载平衡中移除。readiness探针的失败不会导致Pod重新启动。对于web应用程序来说,最典型的liveness探针和readiness探针是通过HTTP端点实现的。
不在Kubernetes平台之上运行的典型web应用程序,您不会区分liveness探针和readiness探针的健康检查。这就是为什么大多数web框架只提供一个内置的健康检查。对于Spring Boot应用程序,您可以通过Spring Boot Actuator轻松地启用健康检查。您需要注意Spring Boot Actuator的健康检查,它的行为可能会因应用程序和第三方系统之间的集成而有所不同。例如,如果通过Spring datasource定义了数据库连接,或与其他消息中间件的连接。Spring Boot Actuator的健康检查可能会通过自动配置自动包含此类连接的验证。因此,如果将默认的Spring Boot Actuator的健康检查设置为readiness探针,则在应用程序无法连接数据库或其他消息中间件时可能导致不必要的重新启动。由于不希望出现这种行为,我建议您应该实现非常简单的liveness探针端点,它只验证应用程序的可用性,而不检查与其他外部系统的连接。
自定义实现Spring Boot的健康检查并不是很难。有一些不同的方法可以做到这一点。比如,我们正在使用 Spring Boot Actuator。值得注意的是,我们不会覆盖默认的健康检查,但我们将添加另一个自定义的健康检查。下面的实现只是检查应用程序是否能够处理传入的请求。
@Component @Endpoint(id = "liveness") class LivenessHealthEndpoint { @ReadOperation fun health() : Health = Health.up().build() @ReadOperation fun name(@Selector name: String) : String = "liveness" @WriteOperation fun write(@Selector name: String) { } @DeleteOperation fun delete(@Selector name: String) { } }
反过来,默认的Spring Boot Actuator的健康检查可以作为readiness探针使用。假设您的应用程序将连接到数据库Postgres和RabbitMQ消息代理,您应该将以下依赖项添加到Maven pom.xml中。
org.springframework.boot spring-boot-starter-amqp org.springframework.boot spring-boot-starter-data-jpa org.postgresql postgresql runtime
现在,为了获得更多信息,请将以下属性添加到您的application.yml。通过 /health
端点查看更多详细信息。
management: endpoint: health: show-details: always
最后,让我们调用 /actuator/health
查看详细信息。正如您在下图中看到的,结果中返回了有关Postgres和RabbitMQ连接的信息。
在web应用程序中使用liveness探针和readiness探针还有另一个注意点。这与线程池有关。在像Tomcat这样的标准web容器中,每个请求都由HTTP线程池处理。如果您在主线程中处理每个请求,并且您的应用程序中有一些长时间运行的任务,那么您可能会阻塞所有可用的HTTP线程。如果你的liveness探针连续几次失败,Pod将会被重新启动。因此,您应该考虑使用另一个线程池来实现长时间运行的任务。下面是使用DeferredResult和Kotlin协程实现HTTP端点的示例。
@PostMapping("/long-running") fun addLongRunning(@RequestBody person: Person): DeferredResult { var result: DeferredResult = DeferredResult() GlobalScope.launch { logger.info("Person long-running: {}", person) delay(10000L) result.setResult(repository.save(person)) } return result }
4. 考虑其他应用程序集成
如果没有任何外部系统,如数据库、消息中间件或其他应用程序,我们的应用程序几乎不可能存在。与第三方应用程序的集成有两个方面需要仔细考虑:连接设置和资源的自动创建。
让我们从连接设置开始。您可能还记得,在上一节中,我们使用Spring Boot Actuator的 /health
端点作为readiness探针。但是,如果您为Postgres和Rabbit保留默认连接设置,那么每次readiness探针调用都需要很长时间(如果它们不可用的话)。这就是为什么我建议将这些超时时间减少到更低值,如下所示。
spring: application: name: sample-spring-kotlin-microservice datasource: url: jdbc:postgresql://postgres:5432/postgres username: postgres password: postgres123 hikari: connection-timeout: 2000 initialization-fail-timeout: 0 jpa: database-platform: org.hibernate.dialect.PostgreSQLDialect rabbitmq: host: rabbitmq port: 5672 connection-timeout: 2000
除了正确配置的连接超时之外,还应该保证自动创建应用程序所需的资源。例如,如果在两个应用程序之间使用RabbitMQ队列进行异步消息传递,则应确保在启动时创建队列(如果不存在),通常在应用程序的监听器端实现。
@Configuration class RabbitMQConfig { @Bean fun myQueue(): Queue { return Queue("myQueue", false) } }
以下是一个监听器端的例子。
@Component class PersonListener { val logger: Logger = LoggerFactory.getLogger(PersonListener::class.java) @RabbitListener(queues = ["myQueue"]) fun listen(msg: String) { logger.info("Received: {}", msg) } }
跟数据库集成也是类似的情况。首先,即使连接数据库失败,也应该确保应用程序启动。这就是为什么我使用PostgreSQLDialect。如果应用程序无法连接到数据库,则需要使用此方法。此外,实体模型中的每个更改都应该在应用程序启动之前应用于表。
幸运的是,Spring Boot为管理数据库表结构更改提供了一些工具,比如Liquibase和Flyway。要启用Liquibase,我们只需要在Maven pom.xml中包含以下依赖项。
org.liquibase liquibase-core
然后,您只需要创建更改日志,并将其放在默认位置db/changelog/db.changelog-master.yaml中。下面是用于创建表person的示例。
databaseChangeLog: - changeSet: id: 1 author: piomin changes: - createTable: tableName: person columns: - column: name: id type: int autoIncrement: true constraints: primaryKey: true nullable: false - column: name: name type: varchar(50) constraints: nullable: false - column: name: age type: int constraints: nullable: false - column: name: gender type: smallint constraints: nullable: false
5. 使用服务网格
如果您不使用Kubernetes构建微服务架构,那么您需要在应用程序端实现负载平衡、断路、回退或重试等机制。流行的云原生框架,如Spring Cloud简化了应用程序中这些模式的实现,并将其简化为向项目添加专用库。但是,如果将微服务迁移到Kubernetes,就不应该继续使用这些库来进行流量管理。它正在成为某种反模式。微服务之间通信的流量管理应该委托给平台。这种方法在Kubernetes上称为服务网格。
由于Kubernetes最初并没有专门用于微服务,所以它没有为许多应用程序之间的流量管理提供任何内置机制。不过,还有一些专门用于流量管理的附加解决方案,可以很容易地安装在Kubernetes上。其中最受欢迎的一个是Istio。除了流量管理,它还解决了与安全、监视、跟踪和指标数据收集相关的问题。
Istio可以很容易地安装在您的集群或Minikube上。下载Istio之后,只需运行以下命令。
istioctl manifest apply
Istio组件需要注入到部署清单中。在此之后,我们可以使用YAML清单定义通信规则。Istio提供了许多有趣的配置选项。下面的示例显示如何将故障注入到现有路由。它可以是延迟,也可以是中止。我们可以使用percent字段为这两种类型的错误定义一个百分比级别。在Istio资源中,我为每个发送到Service account-service
的请求定义了2秒的延迟。
apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: account-service spec: hosts: - account-service http: - fault: delay: fixedDelay: 2s percent: 100 route: - destination: host: account-service subset: v1
除了VirtualService,我们还需要为 account-service
定义DestinationRule。这非常简单,我们只需定义目标服务的版本标签。
apiVersion: networking.istio.io/v1alpha3 kind: DestinationRule metadata: name: account-service spec: host: account-service subsets: - name: v1 labels: version: v1
6. 允许添加外部解决方案
Kubernetes有很多有趣的工具和解决方案,它们可以帮助您运行和管理应用程序。但是,您也不应该忘记您所使用的框架所提供的一些有趣的工具和解决方案。让我给你举几个例子。其中一个是Spring Boot Admin。它是一个有用的工具,用于发现Spring Boot应用程序。假设你在Kubernetes上运行微服务,你也可以在Kubernetes中安装Spring Boot Admin。
在Spring Cloud中还有另一个有趣的项目——Spring Cloud Kubernetes。它提供了一些有用的特性,简化了Spring Boot应用程序和Kubernetes之间的集成。其中之一是跨所有命名空间的服务发现。如果您将该功能与Spring Boot Admin一起使用,那么您可以轻松创建一个强大的工具,它能够监视运行在Kubernetes集群上的所有Spring Boot微服务。有关实现的更多细节,可以参考我的另一篇文章 Spring Boot Admin on Kubernetes 。
有时,您可以将第三方工具与Spring Boot集成来轻松地在Kubernetes上部署,而无需构建单独的部署。您甚至可以构建一个由多个实例组成的集群。此方法适用于可嵌入到Spring Boot应用程序中的产品。它可以是,例如RabbitMQ或Hazelcast(流行的内存数据网格)。如果您对使用这种方法在Kubernetes上运行Hazelcast集群的更多细节感兴趣,请参阅我的文章 Hazelcast with Spring Boot on Kubernetes 。
7. 为回滚做好准备
Kubernetes提供了一种方便的方法,可以基于 ReplicaSet
和 Deployment
对象将应用程序回滚到旧版本。在默认情况下,Kubernetes保留了10个之前的 ReplicaSet
,并允许您回滚到其中任何一个 ReplicaSet
。然而,有一件事需要指出。回滚不包括存储在ConfigMap和Secret中的配置。有时不仅需要回滚应用程序的二进制文件,还需要回滚配置。
幸运的是,Spring Boot为我们管理外部配置提供了可能性。我们可以将配置文件保存在应用程序内部,也可以从外部位置加载它们。在Kubernetes上,我们可以使用ConfigMap和Secret来定义Spring配置文件。用ConfigMap定义 application-rollbacktest.yml
, application-rollbacktest.yml
只包含一个属性。只有当Spring配置文件rollbacktest被激活时,应用程序才加载该配置。
apiVersion: v1 kind: ConfigMap metadata: name: sample-spring-kotlin-microservice data: application-rollbacktest.yml: |- property1: 123456
ConfigMap通过挂载卷的方式添加到应用程序中。
spec: containers: - name: sample-spring-kotlin-microservice image: piomin/sample-spring-kotlin-microservice ports: - containerPort: 8080 name: http volumeMounts: - name: config-map-volume mountPath: /config/ volumes: - name: config-map-volume configMap: name: sample-spring-kotlin-microservice
在应用程序的类路径上也存在 application.yml
,包含一个属性。
property1: 123
第二步,我们将激活rollbacktest配置文件。因为,特定配置文件 application-rollbacktest.yml
具有比 application.yml
更高的优先级。属性 property1
的值将被 application-rollbacktest.yml
中的值覆盖。
property1: 123 spring.profiles.active: rollbacktest
让我们简单测试一下。
@RestController @RequestMapping("/properties") class TestPropertyController(@Value("\${property1}") val property1: String) { @GetMapping fun printProperty1(): String = property1 }
让我们看看如何回滚Deployment版本。首先,让我们看看有多少个版本。
$ kubectl rollout history deployment/sample-spring-kotlin-microservice deployment.apps/sample-spring-kotlin-microservice REVISION CHANGE-CAUSE 1 2 3
现在,我们调用端点 /properties
,它返回属性 property1
的值。因为配置文件 application-rollbacktest.yml
处于激活状态,返回 application-rollbacktest.yml
中的属性值。
$ curl http://localhost:8080/properties 123456
让我们回滚到以前的版本。
$ kubectl rollout undo deployment/sample-spring-kotlin-microservice --to-revision=2 deployment.apps/sample-spring-kotlin-microservice rolled back
正如下图所示,已经看不到 revision=2
,Deployment现在被部署为最新的 revision=4
。
$ kubectl rollout history deployment/sample-spring-kotlin-microservice deployment.apps/sample-spring-kotlin-microservice REVISION CHANGE-CAUSE 1 3 4
在此版本的应用程序配置文件中, application-rollbacktest.yml
未处于激活状态,因此属性 property1
的值取自 application.yml
。
$ curl http://localhost:8080/properties 123