在本文中,我将列出构建Spring Boot应用程序的“金科玉律”,这些应用程序是微服务系统一部分。这些“金科玉律”都来自我过往的经验,我曾经将运行在JEE服务器上的单体SOAP应用程序迁往基于REST的小型Spring Boot应用程序。这些最佳实践假设你的产品上已经拥有许多微服务,且每天要应对海量的请求。让我们开始吧。

收集度量指标

度量指标可视化可以改变组织中系统监控的方法,这非常令人惊讶。在Grafana中设置监控之后,我们能够识别系统中90%以上的大问题,避免这些问题在客户环境中发生并由客户提交给我们的支持团队。多亏了这两个带有大量图表和警报的监视器,我们的反应可能比以前快得多。如果你产品的体系架构是微服务而不是单体应用,那么度量指标会变得更加重要。
对我们来说,好消息是Spring Boot提供了收集最重要度量指标的内置机制。我们只需要设置一些配置文件来开启Spring Boot Actuator提供的预定义指标集。要使用Spring Boot Actuator,我们需要添加Spring Boot的启动依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>


要启用Spring Boot Actuator的功能,我们必须将management.endpoint.metrics设置为true。现在,你可以通过调用GET /actuator/metrics来检查度量指标的完整列表。对我们来说,最重要的指标之一是http.server.requests,它展示了请求数量和响应时间的统计信息,并会自动记录下请求类型(POST、GET等)、HTTP状态码和uri。
度量指标必须被存储在某个地方。最流行的工具是InfluxDBPrometheus。它们代表了两种不同的数据收集模型。Prometheus定期从应用程序的公开端点拉取数据,而InfluxDB提供了REST API,由应用程序来调用。Micrometer库提供了与InfluxDB、Prometheus和其他一些工具的集成方案。要启用对InfluxDB的支持,我们需要添加以下依赖项。

<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-influx</artifactId>
</dependency>


我们还需要在Spring Boot的application.yml中配置InfluxDB的URL和数据库名。

management:
metrics:
    export:
      influx:
            db: springboot
            uri: http://192.168.99.100:8086


要启用对Prometheus的支持,我们首先需要添加对应的Micrometer库,然后设置属性management.endpoint.metrics为true。

<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-prometheus</artifactId>
</dependency>


默认情况下,Prometheus每分钟尝试从定义的目标端点收集一次数据。可以在Prometheus的配置文件中设置这些参数,scrape_config部分负责指定一组目标和参数,以及如何与它们连接。

scrape_configs:
- job_name: 'springboot'
    metrics_path: '/actuator/prometheus'
    static_configs:
    - targets: ['person-service:2222']


有时候,为指标数据打上标签是非常有用的,特别是当我们在单个微服务系统中存在多个应用程序,并将指标数据发送给单个Influx数据库时。下面是代码示例。

@Configuration
class ConfigurationMetrics {

    @Value("\${spring.application.name}")
    lateinit var appName: String
    @Value("\${NAMESPACE:default}")
    lateinit var namespace: String
    @Value("\${HOSTNAME:default}")
    lateinit var hostname: String

    @Bean
    fun tags(): MeterRegistryCustomizer<InfluxMeterRegistry> {
            return MeterRegistryCustomizer { registry ->
                    registry.config().commonTags("appName", appName).commonTags("namespace", namespace).commonTags("pod", hostname)
            }
    }

} 


这是一张从Grafana中截取的监控视图,表示单个应用的http.server.requests指标。

 

不要忘记日志

日志记录在开发过程中不是很重要,但在维护过程中却非常重要。值得记住的是,在组织中,通过日志的质量可以间接判断应用程序的质量。通常,应用程序由支持团队维护,因此日志应该是非常重要的。不要试图把所有的事情都放在日志里,只有最重要的事件才应该被记录下来。
为微服务中的所有应用程序使用相同的日志格式也很重要。例如,如果要以JSON格式记录日志,那么对每个应用程序都应该用JSON格式。如果你使用标记appName来指示应用程序的名称或用instanceId来区分同一应用程序的不同实例,那么在任何地方都要这样做。为什么?你通常希望将微服务中收集的所有日志存储在一个单独的中央数据库中。最流行的工具(或者说是收集日志的工具)是Elastic Stack(ELK)。将中央数据库的优势发挥到最大,你应该确保查询条件和响应结构对于所有应用程序都是相同的,尤其是将不同微服务之间的日志关联起来。怎么做呢?当然是通过使用第三方库。我推荐你使用Spring Boot logging,要使用它,你需要添加如下依赖。

<dependency>
<groupId>com.github.piomin</groupId>
<artifactId>logstash-logging-spring-boot-starter</artifactId>
<version>1.2.2.RELEASE</version>
</dependency>


这个库将迫使你使用一些良好的日志记录实践,并自动与Logstash集成(ELK中负责收集日志的工具)。它的主要特点是:

  • 能够记录完整的HTTP请求和HTTP响应,并使用适当的标签将这些日志发送到Logstash,这些标签标明了HTTP的请求类型和HTTP响应码
  • 计算和存储每个请求的执行时间
  • 当使用Spring RestTemplate调用的下游服务时,生成和传递correlationId


为了能够向Logstash发送日志,我们至少需要如下配置信息。

logging.logstash:
enabled: true
url: 192.168.99.100:5000


在添加了logstash-logging-spring-boot-starter之后,你就可以使用Logstash中的日志标记功能。下图是来自Kibana中的单条日志记录的截图。

 


我们还可以将库Spring Cloud Sleuth添加到我们的依赖项中。

<dependency> 
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>


Spring Cloud Sleuth传递与Zipkin(一种流行的分布式跟踪工具)兼容的标头。它的主要特点是:

  • 它将跟踪(相关请求)和span IDs添加到Slf4J MDC
  • 它记录时间信息以帮助进行延迟分析
  • 它修改了日志的格式,以添加一些信息,比如附加的MDC字段
  • 它提供与其他Spring组件的集成,如OpenFeign、RestTemplate或Spring Cloud Netflix Zuul

让你的API易用

在大多数情况下,其他应用程序将通过REST API调用你的应用程序。因此我们需要小心的维护一份API文档。文档应该由代码生成。当然有一些工具可以做到这一点。其中最受欢迎的是Swagger。你可以使用SpringFox项目轻松地将Swagger 2集成到你的Spring Boot应用程序中。为了使用Swagger,我们需要添加如下依赖。第一个库负责从Spring MVC控制器代码中生成Swagger descriptor,而第二个库负责解析Swagger descriptor并在你的浏览器中展示页面。

<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>2.9.2</version>
</dependency>
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    <version>2.9.2</version>
</dependency>


我们还必须提供一些Bean来修改Swagger的默认行为。它应该只展示我们自己提供的REST API,而不展示Spring Boot Actuator的API。我们也可以通过定义UiConfiguration bean来定制UI外观。

@Configuration
@EnableSwagger2
public class ConfigurationSwagger {

    @Autowired
    Optional<BuildProperties> build;

    @Bean
    public Docket api() {
            String version = "1.0.0";
            if (build.isPresent())
                    version = build.get().getVersion();
            return new Docket(DocumentationType.SWAGGER_2)
                            .apiInfo(apiInfo(version))
                            .select()
                            .apis(RequestHandlerSelectors.any())
                            .paths(PathSelectors.regex("(/components.*)"))
                            .build()
                            .useDefaultResponseMessages(false)
                            .forCodeGeneration(true);
    }

    @Bean
    public UiConfiguration uiConfig() {
            return UiConfigurationBuilder.builder().docExpansion(DocExpansion.LIST).build();
    }

    private ApiInfo apiInfo(String version) {
            return new ApiInfoBuilder()
                            .title("API - Components Service")
                            .description("Managing Components.")
                            .version(version)
                            .build();
    }
} 


下图是Swagger 2 UI的例子。

 


接下来是为所有微服务定义相同的REST API准则。如果你始终如一地为你的微服务构建API,那么对于外部和内部客户端,集成微服务要简单得多。指南应该包含如何构建API的说明,需要在请求和响应上设置哪些HTTP header,如何生成错误码等。这些准则应该与组织中的所有开发人员和供应商共享。有关为Spring Boot微服务生成Swagger文档(包括为API网关上的所有应用程序公开它)的更详细说明,您可以参考我的文章《Microservices API Documentation with Swagger2》。

不要害怕使用断路器

如果你使用Spring cloud在微服务之间进行通信,你可以利用Spring Cloud Netflix Hystrix或Spring Cloud断路器来实现断路。然而,由于Netflix不再开发Hystrix,Pivotal团队已经将第一个解决方案转移到了维护模式。推荐的解决方案是构建在resilience4j项目之上的新的Spring Cloud断路器。

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>


然后,我们需要为断路器设置所需的配置,方法是定义一个Customizer bean,该bean被传递给Resilience4JCircuitBreakerFactory。以下示例使用默认值。

@Bean
public Customizer<Resilience4JCircuitBreakerFactory> defaultCustomizer() {
    return factory -> factory.configureDefault(id -> new Resilience4JConfigBuilder(id)
                    .timeLimiterConfig(TimeLimiterConfig.custom().timeoutDuration(Duration.ofSeconds(5)).build())
                    .circuitBreakerConfig(CircuitBreakerConfig.ofDefaults())
                    .build());
} 


有关将Hystrix断路器与Spring Boot应用程序集成的详细信息,请参阅我的另一篇文章:《Part 3: Creating Microservices: Circuit Breaker, Fallback and Load Balancing with Spring Cloud》。

使应用程序透明

我们不应该忘记,迁移到微服务的最重要原因之一是持续交付的需求。今天,快速交付变更的能力在市场上具有优势。你甚至应该能够在一天内多次交付更改。因此,重要的是当前是什么版本,它在哪里发布,以及它包括哪些更改。
在使用Spring Boot和Maven时,我们可以很容易地发布诸如最后更改日期、Git的commit id或应用程序的多个版本等信息。要实现这一点,我们只需要在我们的pom.xml中包含以下Maven插件。

<plugins>
    <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <executions>
                    <execution>
                            <goals>
                                    <goal>build-info</goal>
                            </goals>
                    </execution>
            </executions>
    </plugin>
    <plugin>
            <groupId>pl.project13.maven</groupId>
            <artifactId>git-commit-id-plugin</artifactId>
            <configuration>
                    <failOnNoGitDirectory>false</failOnNoGitDirectory>
            </configuration>
    </plugin>
</plugins>


假设你已经启用了Spring Boot Actuator(参见第1节),你必须启用/info端点来显示所有有趣的数据。

management.endpoint.info.enabled: true


当然,我们有许多微服务组成的一个大系统,并且每个微服务都有一些正在运行的实例。最好是在一个应用程序中监视我们所有的实例——就像收集度量指标和收集日志一样。幸运的是,有一个专门用于Spring Boot应用程序的工具,它能够从所有Actuator端点收集数据并在UI中显示它们。它是由Codecentric开发的Spring Boot Admin。使用Spring Boot Admin最方便的方式是创建一个专门的Spring Boot应用程序,该程序需要添加Spring Boot Admin的依赖项和服务发现的依赖项,例如Spring Cloud Netflix Eureka。

<dependency>
    <groupId>de.codecentric</groupId>
    <artifactId>spring-boot-admin-starter-server</artifactId>
    <version>2.1.6</version>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>


然后,我们应该通过使用@EnableAdminServer注解来启用Spring Boot Admin。

@SpringBootApplication
@EnableDiscoveryClient
@EnableAdminServer
@EnableAutoConfiguration
public class Application {

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

} 


通过Spring Boot Admin,我们可以轻松地浏览在服务发现中注册的应用程序列表,并检查每个应用程序的版本或提交信息。

 


我们可以点击“Details”按钮查看更多详细信息,这些信息都来自于/info端点和从其他Actuator端点收集到的数据。

 

编写合同测试

消费者驱动契约(Consumer Driven Contract,CDC)测试是一种方法,它允许你验证系统内应用程序之间的集成。系统内部的集成数量可能非常大,尤其是在维护基于微服务的系统时。由于Spring Cloud Contract项目的存在,在Spring Boot中进行契约测试会相对容易一些。还有一些专门为CDC设计的框架,如Pact,但Spring Cloud Contract可能是首选,因为我们使用的是Spring Boot。
要在生产者端使用Spring Cloud Contract,我们需要添加Spring Cloud Contract Verifier。

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-contract-verifier</artifactId>
    <scope>test</scope>
</dependency>


在消费者端,我们需要添加Spring Cloud Contract Stub Runner。

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
    <scope>test</scope>
</dependency>


第一步是定义契约。编写它的一种方法是使用Groovy语言。合同应在生产者和消费者双方进行核实。例如:

import org.springframework.cloud.contract.spec.Contract
Contract.make {
    request {
            method 'GET'
            urlPath('/persons/1')
    }
    response {
            status OK()
            body([
                    id: 1,
                    firstName: 'John',
                    lastName: 'Smith',
                    address: ([
                            city: $(regex(alphaNumeric())),
                            country: $(regex(alphaNumeric())),
                            postalCode: $(regex('[0-9]{2}-[0-9]{3}')),
                            houseNo: $(regex(positiveInt())),
                            street: $(regex(nonEmpty()))
                    ])
            ])
            headers {
                    contentType(applicationJson())
            }
    }
} 


契约与存根一起打包在JAR中。它可以发布到像Artifactory或Nexus这样的存储库管理器,然后消费者可以在JUnit测试期间从存储库管理器下载契约。生成的JAR文件以存根作为后缀。

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.NONE)
@AutoConfigureStubRunner(ids = {"pl.piomin.services:person-service: :stubs:8090"}, consumerName = "letter-consumer",  stubsPerConsumer = true, stubsMode = StubsMode.REMOTE, repositoryRoot = "http://192.168.99.100:8081/artifactory/libs-snapshot-local")
@DirtiesContext
public class PersonConsumerContractTest {

    @Autowired
    private PersonClient personClient;

    @Test
    public void verifyPerson() {
            Person p = personClient.findPersonById(1);
            Assert.assertNotNull(p);
            Assert.assertEquals(1, p.getId().intValue());
            Assert.assertNotNull(p.getFirstName());
            Assert.assertNotNull(p.getLastName());
            Assert.assertNotNull(p.getAddress());
            Assert.assertNotNull(p.getAddress().getCity());
            Assert.assertNotNull(p.getAddress().getCountry());
            Assert.assertNotNull(p.getAddress().getPostalCode());
            Assert.assertNotNull(p.getAddress().getStreet());
            Assert.assertNotEquals(0, p.getAddress().getHouseNo());
    }

} 


契约测试不会验证微服务系统中的复杂用例。然而,这是测试微服务之间集成的第一阶段。一旦确保了应用程序之间的API契约是有效的,就可以进行更高级的集成或端到端测试。关于持续集成与Spring Cloud Contract的更详细解释,可以参考我的另一篇文章《Continuous Integration with Jenkins, Artifactory and Spring Cloud Contract》。

更新到最新版本

Spring Boot和Spring Cloud相对频繁地发布新版本的框架。假设您的微服务的代码数量不是很多,那么很容易的就可以升级到最新版本。Spring Cloud使用release train模式发布项目的新版本,以简化依赖关系管理并避免库的不兼容版本之间的冲突问题。
此外,Spring Boot系统地改善了应用程序的启动时间和内存占用,因此值得对其进行更新。这是Spring Boot和Spring Cloud当前的稳定版本。

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.2.1.RELEASE</version>
</parent>
<dependencyManagement>
    <dependencies>
            <dependency>
                    <groupId>org.springframework.cloud</groupId>
                    <artifactId>spring-cloud-dependencies</artifactId>
                    <version>Hoxton.RELEASE</version>
                    <type>pom</type>
                    <scope>import</scope>
            </dependency>
    </dependencies>
</dependencyManagement>

结论

我向你展示了如何使用Spring Boot的特性和Spring Cloud的一些附加库,作为最佳实践这并不困难。这些最佳实践将使您更容易地迁移到基于微服务的体系结构中,并在容器中运行应用程序。