Kubernetes在微服务中的最佳实践


你可以在网上找到许多关于如何正确构建微服务体系架构的最佳实践。其中之一是我以前写的一篇文章《Spring Boot在微服务中的最佳实践》。我把重点放在在生产上基于Spring Boot构建的微服务应用程序应该考虑哪些方面。我没有使用任何用于编排或管理应用程序的平台,而只是一组独立的应用程序。在本文中,我将基于已经介绍的最佳实践,在Kubernetes平台上部署微服务,你需要注意一些新规则和事项。

第一个问题是,如果将微服务部署在Kubernetes上,而不是直接独立的运行它们,会有什么不同吗?答案可能是“相同”或者“不同”。不同在于你有了一个管理所有应用程序的平台,负责运行和监控你的应用程序,还会有一些附件的规则你需要遵守。相同在于你的整个应用程序体系仍然是微服务体系架构,由一组低耦合、独立的应用程序构成,你不应该忘记微服务体系架构的本质!在Kubernetes中,前面介绍的许多最佳实践都是依然有效的,只是有一些微调而已,还有一些新的最佳实践被引入。

有一点必须说明。这个最佳实践列表是我总结出的实际经验,并非从其他文章或书籍中抄袭而来。在我的组织中,我们已经将微服务从Spring Cloud(Eureka、Zuul、Spring Cloud Config)迁移到了OpenShift中,基于我们的维护情况,不断地演进此架构。

代码

以下所有代码由Kotlin编写,你可以在如下链接中找到所有完整代码:https://github.com/piomin/samp ... netes

允许平台收集指标数据

我在上一篇文章中加入了类似的章节。当时我们使用InfluxDB作为指标数据的存储介质。但在Kubernetes中,收集指标数据的方法发生了一些变化,所以我重新定义了这一章节,“允许平台收集指标数据”。他们的主要区别在于收集数据的方式。在Kubernetes中,我们推荐使用Prometheus,因为平台可以帮助我们管理。InfluxDB需要应用程序将指标数据推送给它。而Prometheus会定期地主动拉取指标数据。因此,我们的主要职责是在应用程序端为Prometheus提供端点暴露数据。

幸运的是,通过Spring Boot为Prometheus提供端点是非常容易的。你只需要添加如下依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>

我们还需要公开Spring Boot Actuator的HTTP端点。你可以只公开专用于Prometheus的端点,或者如下所示公开所有HTTP端点。
management.endpoints.web.exposure.include: '*'

启动应用程序后,你可以看到如下的可用端点/actuator/prometheus
best-practices-microservices-kubernetes-actuator.jpg

假设你在Kubernetes上运行你的应用程序,那么您需要部署和配置Prometheus,以便从你的Pod中提取日志。配置信息可以通过Kubernetes ConfigMap来实现。prometheus.yml文件应该包含metrics_pathkubernetes_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上运行。
best-practices-microservices-kubernetes-prometheus.png

准备正确格式的日志

收集日志的方法与收集指标数据非常类似。我们的应用程序不应该自己处理发送日志的过程。它只需要适当地格式化发送到输出流的日志。因为Docker为Fluentd提供了内置的日志驱动程序,所以在Kubernetes上运行的应用程序可以很方便地使用它作为日志收集器。这意味着在容器上不需要额外的代理来将日志推送到Fluentd。日志直接从STDOUT发送到Fluentd服务,不需要额外的日志文件或持久存储。Fluentd试图读取结构化的JSON数据,以便进行统一处理。

为了将我们的日志格式化为Fluentd可读的JSON格式,我们可以将Logstash Logback Encoder引入到我们的依赖中。
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>6.3</version>
</dependency>

然后,我们只需要在logback-spring.xml文件中为我们的Spring Boot应用程序设置一个默认的控制台日志追加器。
<configuration>
<appender name="consoleAppender" class="ch.qos.logback.core.ConsoleAppender">
    <encoder class="net.logstash.logback.encoder.LogstashEncoder"/>
</appender>
<logger name="jsonLogger" additivity="false" level="DEBUG">
    <appender-ref ref="consoleAppender"/>
</logger>
<root level="INFO">
    <appender-ref ref="consoleAppender"/>
</root>
</configuration>

日志以如下所示的格式打印到STDOUT中。
best-practices-microservices-kubernetes-log-format.png

在Minikube上安装Fluentd,Elasticsearch和Kibana非常简单。这种方法的缺点是版本不够新。
$ minikube addons enable efk
* efk was successfully enabled
$ minikube addons enable logviewer
* logviewer was successfully enabled

启用efk和logviewer插件后,Kubernetes启动所有必需的Pod,如下所示。
best-practices-microservices-kubernetes-pods-logging.png

感谢logstash-logback-encoder,我们可以自动创建与Fluentd兼容的日志,包括MDC字段。这是Kibana的页面截图,显示了我们的应用程序的日志。
best-practices-microservices-kubernetes-kibana.png

你还可以添加我的库来记录Spring Boot应用程序的请求/响应。
<dependency>
<groupId>com.github.piomin</groupId>
<artifactId>logstash-logging-spring-boot-starter</artifactId>
<version>1.2.2.RELEASE</version>
</dependency>

实现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中。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>

现在,为了获得更多信息,请将以下属性添加到你的application.yml。通过/health端点查看更多详细信息。
management:
endpoint:
health:
  show-details: always

最后,让我们调用/actuator/health查看详细信息。正如你在下图中看到的,结果中返回了有关Postgres和RabbitMQ连接的信息。
best-practices-microservices-kubernetes-readiness.png

在Web应用程序中使用Liveness探针和Readiness探针还有另一个注意点。这与线程池有关。在像Tomcat这样的标准Web容器中,每个请求都由HTTP线程池处理。如果你在主线程中处理每个请求,并且你的应用程序中有一些长时间运行的任务,那么你可能会阻塞所有可用的HTTP线程。如果你的Liveness探针连续几次失败,Pod将会被重新启动。因此,你应该考虑使用另一个线程池来实现长时间运行的任务。下面是使用DeferredResult和Kotlin协程实现HTTP端点的示例。
@PostMapping("/long-running")
fun addLongRunning(@RequestBody person: Person): DeferredResult<Person> {
var result: DeferredResult<Person>  = DeferredResult()
GlobalScope.launch {
    logger.info("Person long-running: {}", person)
    delay(10000L)
    result.setResult(repository.save(person))
}
return result


考虑其他应用程序集成

如果没有任何外部系统,如数据库、消息中间件或其他应用程序,我们的应用程序几乎不可能存在。与第三方应用程序的集成有两个方面需要仔细考虑:连接设置和资源的自动创建。

让我们从连接设置开始。你可能还记得,在上一节中,我们使用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中包含以下依赖项。
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
</dependency>

然后,你只需要创建更改日志,并将其放在默认位置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

使用服务网格

如果你不使用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

允许添加外部解决方案

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

为回滚做好准备

Kubernetes提供了一种方便的方法,可以基于ReplicaSetDeployment对象将应用程序回滚到旧版本。在默认情况下,Kubernetes保留了10个之前的ReplicaSet,并允许你回滚到其中任何一个ReplicaSet。然而,有一件事需要指出。回滚不包括存储在ConfigMap和Secret中的配置。有时不仅需要回滚应用程序的二进制文件,还需要回滚配置。

幸运的是,Spring Boot为我们管理外部配置提供了可能性。我们可以将配置文件保存在应用程序内部,也可以从外部位置加载它们。在Kubernetes上,我们可以使用ConfigMap和Secret来定义Spring配置文件。用ConfigMap定义application-rollbacktest.ymlapplication-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


原文链接:Best Practices For Microservices on Kubernetes(翻译:钟涛)

0 个评论

要回复文章请先登录注册