# 基于 Spring Cloud 微服务框架的应用开发治理

    本文将以 Spring Cloud 为例,讲述 Erda 中构建微服务架构应用的最佳实践。

    微服务架构相较于传统的单体应用,最大的变化在于服务拆分,具体来说是根据业务领域进行业务组件拆分,例如采用领域驱动设计(DDD),按照业务领域划分为多个微服务,服务之间相互协同完成业务功能。

    微服务架构解决了众多单体应用的问题,同时也增加了架构的复杂性。下文将针对技术架构的改变,介绍如何在 Erda 上完成微服务应用的研发和治理,主要包括以下内容:

    • 服务的发现和调用
    • 服务的配置集中管理
    • 服务的 API 开放和测试
    • 服务的可观测治理

    提示

    微服务应用天然属于分布式应用,其涉及的分布式架构解决方案,例如分布式缓存、队列、事务等,本文不作讨论。

    # 微服务设计

    首先创建一个微服务项目名为 bestpractice(分配 4 核 CPU 和 8 GB 内存),并创建微服务应用 echo-web 和 echo-service(应用类型为 业务应用,仓库选择 使用内部仓库)。

    echo-web 模拟业务聚合层,对内调用 echo-service 完成服务功能,对外通过 Erda 的 API 网关提供 Open API,其功能包括:

    • 提供 /api/content API 调用 echo-service 实现对 content 资源的增删改查。
    • 提供 /api/error API 调用 echo-service 制造异常。

    echo-service 模拟单一业务领域服务层,处理领域内业务逻辑,并完成持久化,其功能包括:

    • 提供 /echo/content API 实现对 content 资源的增删改查。
    • 提供 /echo/error API 制造异常。

    echo-web 和 echo-service 通过 Erda 的微服务组件注册中心实现服务接口的注册与发现,通过微服务组件配置中心实现配置统一管理和热加载。

    # API 设计

    进入 echo-web 应用 > API > 新建文档,选择分支 feature/api,名称为 echo-web。

    提示

    echo-web 为 Service 名称,与 dice.yml 中的服务名称保持一致。

    # 数据类型

    content: 参数名称 “content”, 类型 “String”
    ContentRequest: 参数名称 “ContentRequest”, 类型 “Object”, 其参数引用类型 “content”
    ContentResponse: 参数名称 “ContentResponse”, 类型 “Object”, 其参数引用类型 “content”
    

    # APIs

    • echo web 应用 API

      1. /api/content
      	1. Method: GET
      		Response
      			MediaType: application/json
      			类型: ContentResponse
      	2. Method: PUT
      		Body
      			MediaType: application/json
      			类型: ContentRequest
      	3. Method: POST
      		Body
      			MediaType: application/json
      			类型: ContentRequest
      	4. Method: DELETE
      		Response
      			MediaType: application/json
      			类型:Object
      				参数名称: deleted, 类型: Boolean
      2. /api/error
      	1. Method:POST
      		Body
      			MediaType: application/json
      			类型:Object
      				参数名称: type, 类型: String
      

      点击 发布,填写 API 名称为 Echo 应用 API,API ID 为 echo-web,发布版本为 1.0.0。

    • echo service 应用 API

      1. /echo/content
      	1. Method: GET
      		Response
      			MediaType: application/json
      			类型: ContentResponse
      	2. Method: PUT
      		Body
      			MediaType: application/json
      			类型: ContentRequest
      	3. Method: POST
      		Body
      			MediaType: application/json
      			类型: ContentRequest
      	4. Method: DELETE
      		Response
      			MediaType: application/json
      			类型:Object
      				参数名称: deleted, 类型: Boolean
      1. /echo/error
      	1. Method: POST
      		Body
      			MediaType: application/json
      			类型:Object
      				参数名称: type, 类型: String
      

      进入 DevOps 平台 > API 管理 > API 集市 查看、管理 API。

      提示

      • 发布后的 API 文档默认为 私有,仅关联项目应用下的成员可查看。
      • 若为企业级 Open API,可设置为 共有,便于组织下所有用户查看。

    # 应用开发

    # echo service 应用

    # 基于 Spring Boot 开发框架创建应用

    使用 IDEA 创建 Maven 项目(Java 1.8)并配置 Spring Boot 框架,目录结构如下:

    ├── pom.xml
    └── src
        ├── main
        │   ├── java/io/terminus/erda/bestpractice/echo
        │   │                                      ├── Application.java
        │   │                                      └── controller
        │   │                                          └── DefaultController.java
        │   └── resources
        │       └── application.yml
        └── test
            └── java
    

    编辑 pom.xml 文件:

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
    
        <groupId>io.terminus.erda.bestpractice.echo</groupId>
        <artifactId>echo-service</artifactId>
        <version>1.0-SNAPSHOT</version>
    
        <properties>
            <maven.compiler.source>8</maven.compiler.source>
            <maven.compiler.target>8</maven.compiler.target>
        </properties>
    
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.1.4.RELEASE</version>
            <relativePath/>
        </parent>
    
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
        </dependencies>
    
        <dependencyManagement>
            <dependencies>
                <dependency>
                    <groupId>org.springframework.cloud</groupId>
                    <artifactId>spring-cloud-dependencies</artifactId>
                    <version>Greenwich.SR1</version>
                    <type>pom</type>
                    <scope>import</scope>
                </dependency>
            </dependencies>
        </dependencyManagement>
    
        <build>
            <finalName>echo-service</finalName>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                    <configuration>
                        <executable>true</executable>
                        <mainClass>io.terminus.erda.bestpractice.echo.Application</mainClass>
                    </configuration>
                    <executions>
                        <execution>
                            <goals>
                                <goal>repackage</goal>
                            </goals>
                        </execution>
                    </executions>
                </plugin>
            </plugins>
        </build>
    </project>
    

    提示

    pom.xml 中 build 部分使用 spring-boot-maven-plugin 构建 Fat JAR,并会在后续制品中作为可执行的 JAR 文件使用。

    编辑 Application.java 文件:

    package io.terminus.erda.bestpractice.echo;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    
    @SpringBootApplication
    public class Application {
        public static void main(String[] args) {
            SpringApplication.run(Application.class, args);
        }
    }
    

    编辑 DefaultController.java 文件增加健康检查 API:

    package io.terminus.erda.bestpractice.echo.controller;
    
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    @RequestMapping(value = "/api")
    public class DefaultController {
        @RequestMapping(value = "/healthy", method = RequestMethod.GET)
        public boolean healthy() {
            return true;
        }
    }
    

    提示

    健康检查 API 用于 dice.yml 中,提供给 Kubernetes 进行 liveness 和 readiness 检查,需保证其返回 200 时服务健康可用。

    # 关联 Erda Git 远程仓库并推送代码

    git remote add erda https://erda.cloud/trial/dop/bestpractice/echo-web
    git push -u erda --all
    git push -u erda --tags
    

    # echo web 应用

    参考上文 echo service 应用的开发过程,完成以下内容:

    1. 基于 Spring Boot 开发框架创建应用。
    2. 关联 Erda Git 远程仓库并推送代码。

    # CI/CD 流水线

    下文以 echo-service 应用为例编排流水线,可供 echo-web 应用参考。

    # pipeline.yml

    进入 echo-service 应用新建流水线,选择 java-boot-maven-erda 模板,切换代码模式开始编辑:

    ...
                  dice.yml中的服务名:
                    cmd: java -jar /target/jar包的名称
                    copys:
                      - ${java-build:OUTPUT:buildPath}/target/jar包的名称:/target/jar包的名称
                    image: registry.cn-hangzhou.aliyuncs.com/terminus/terminus-openjdk:v11.0.6
    ...
    

    将模版中上述部分修改为:

                  echo-service:
                    cmd: java ${java-build:OUTPUT:JAVA_OPTS} -jar /target/echo-service
                    copys:
                      - ${java-build:OUTPUT:buildPath}/target/echo-service.jar:/target/echo-service.jar
                      - ${java-build:OUTPUT:buildPath}/spot-agent:/
                    image: registry.cn-hangzhou.aliyuncs.com/terminus/terminus-openjdk:v11.0.6
    

    提示

    pipeline.yml 中用于替换 JAR 包的名称需与 echo-service 应用 pom.xml 的 build.finalName 保持一致,用于替换 dice.yml 中的服务名需与 dice.yml 保持一致。

    # dice.yml

    在代码仓库添加 dice.yml 文件并进行编辑,新增节点后按照图示填写配置:

    提示

    • dice.yml 中的服务名需与 pipeline.yml 保持一致。
    • 健康检查端口需与应用监听的端口保持一致,Spring Boot 框架内置的 Tomcat 服务器默认监听 8080 端口。

    完成应用开发后,可通过执行流水线任务实现应用的构建发布。

    # 服务注册与发现

    下文将基于 Spring Cloud 和 Erda 的注册中心开发服务接口的注册与发现。

    # echo service

    Erda 的注册中心基于 Nacos 实现(具体请参见 使用 Nacos 云服务),需在 pom.xml 文件中添加 Nacos 依赖:

            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
                <version>2.1.0.RELEASE</version>
            </dependency>
    

    同时在 src/main/resources/application.yml 配置注册中心:

    ```
    spring:
      application.name: echo-service
      cloud:
        nacos:
          discovery:
            server-addr: ${NACOS_ADDRESS:127.0.0.1:8848}
            namespace: ${NACOS_TENANT_ID:}
    ```
    

    提示

    application.name 需与代码中保持一致。

    io.terminus.erda.bestpractice.echo.Application 类增加 @EnableDiscoveryClient 注解表明此应用需开启服务注册与发现功能。

    ```
    package io.terminus.erda.bestpractice.echo;
    
    @SpringBootApplication
    @EnableDiscoveryClient
    public class Application {
        public static void main(String[] args) {
            SpringApplication.run(Application.class, args);
        }
    }
    ```
    

    开发 io.terminus.erda.bestpractice.echo.controller.EchoController 类,并实现功能 API:

    package io.terminus.erda.bestpractice.echo.controller;
    
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.bind.annotation.RestController;
    
    class Content {
        public String content;
    
        public void setContent(String content) {
            this.content = content;
        }
    
        public String getContent() {
            return content;
        }
    }
    
    @RestController
    @RequestMapping(value = "/echo/content")
    public class EchoController {
        private String c = "";
    
        @RequestMapping(method = RequestMethod.PUT)
        public void echoPut(@RequestBody Content content) {
            c = content.content;
        }
    
        @RequestMapping(method = RequestMethod.POST)
        public void echoPost(@RequestBody Content content) {
            if (c != content.content) {
                c = content.content;
            }
        }
    
        @RequestMapping(method = RequestMethod.DELETE)
        public void echoDelete() {
            c = "";
        }
    
        @RequestMapping(method = RequestMethod.GET)
        public String echoGet() {
            return c;
        }
    }
    

    # echo web

    pom.xml 和 application.yml 参考 echo service 部分。

    创建 echo service 的接口类 io.terminus.erda.bestpractice.echo.controller.EchoService:

    package io.terminus.erda.bestpractice.echo.controller;
    
    import org.springframework.cloud.openfeign.FeignClient;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    
    @FeignClient(name="echo-service")
    @RequestMapping(value = "/echo")
    public interface EchoService {
        @RequestMapping(value = "/content", method = RequestMethod.PUT)
        void echoPut(@RequestBody Content content);
    
        @RequestMapping(value = "/content", method = RequestMethod.POST)
        void echoPost(@RequestBody Content content);
    
        @RequestMapping(value = "/content", method = RequestMethod.GET)
        String echoGet();
    
        @RequestMapping(value = "/content", method = RequestMethod.DELETE)
        void echoDelete();
    }
    

    在 io.terminus.erda.bestpractice.echo.controller.EchoController 实现对资源 content 的增删改查:

    @RestController
    @RequestMapping(value = "/api")
    public class EchoController {
        @Autowired
        private EchoService echoService;
    
        @RequestMapping(value = "/content", method = RequestMethod.PUT)
        public void echoPut(@RequestBody Content content) {
            echoService.echoPut(content);
        }
    
        @RequestMapping(value = "/content", method = RequestMethod.POST)
        public void echoPost(@RequestBody Content content) {
            echoService.echoPost(content);
        }
    
        @RequestMapping(value = "/content", method = RequestMethod.GET)
        public Content echoGet() {
            Content content = new Content();
            String c = echoService.echoGet();
            content.setContent(c);
            return content;
        }
    
        @RequestMapping(value = "/content", method = RequestMethod.DELETE)
        public void echoDelete () {
            echoService.echoDelete();
        }
    
        @RequestMapping(value = "/healthy", method = RequestMethod.GET)
        public Boolean health() {
            return true;
        }
    }
    
    class Content {
        private String content;
    
        public void setContent(String content) {
            this.content = content;
        }
    
        public String getContent() {
            return content;
        }
    }
    

    # dice.yml

    编辑 echo-web 和 echo-service 两个应用的 dice.yml,增加 Addon 注册中心。

    完成以上代码后,再次执行 echo-web 和 echo-service 的流水线,随后即可在 环境部署 看到 注册中心

    点击 注册中心,或进入 微服务治理平台 > 服务治理 > 注册中心,查看 HTTP 协议

    echo-service 和 echo-web 已分别完成服务的注册和发现。

    # API 访问测试

    # 关联应用

    进入 DevOps 平台 > API 管理 > API 集市,点击 echo 应用 API管理 选项。

    • 关联关系:选择项目名称为 bestpractice,应用名称为 echo-web。
    • 版本管理:选择服务名称为 echo-web,部署分支未 feature/echo-web,关联实例为 echo-web-xxx.svc.cluster.local:8080。

    提示

    • 应用下可包含多个服务,本示例中应用名称与服务名称均为 echo-web,仅是一种同名的情况。
    • 关联实例是一个 VIP 域名地址(Kubernetes Service 域名地址),由于服务可部署多个 Pod 实例,Erda 通过 Kubernetes 的 Service 实现对多个 Pod 的负载分配。

    # 创建管理条目

    进入 DevOps 平台 > API 管理 > 访问管理,创建管理条目。

    提示

    • 绑定域名 需绑定已解析到 Erda 平台的公网入口 IP 方可从公网访问服务。
    • 若尚未创建 API 网关,请根据提示先行创建 API 网关。

    # 申请调用并测试

    进入 DevOps 平台 > API 管理 > API 集市 > Echo 应用 API > 申请调用。若无合适的客户端,请根据提示先行完成创建。保存系统提示的 Client ID 和 Client Secret,用于后续测试。

    完成审批后进入 API 集市 > Echo 应用 API > 认证,输入 ClientID 和 ClientSecret 后可选择任意 API 并点击测试。

    # 配置中心使用

    # echo service 应用配置热加载

    在 pom.xml 文件中增加依赖:

    <dependency>
        <groupId>io.terminus.common</groupId>
        <artifactId>terminus-spring-boot-starter-configcenter</artifactId>
        <version>2.1.0.RELEASE</version>
    </dependency>
    

    提示

    需使用端点二次开发的 Spring Boot Starter 适配 Erda 平台。

    io.terminus.erda.bestpractice.echo.controller.ErrorController 增加 slowTime 变量模拟耗时请求,并通过配置中心实现配置热加载:

    @RefreshScope
    @RestController
    @RequestMapping(value = "/echo/error")
    public class ErrorController {
        @Value("${echo.slowtime:100}")
        private int slowTime;
    
        @RequestMapping(method = RequestMethod.POST)
        void errorGet(@RequestBody EchoError err) throws Exception {
            if (err.getType().equals("slow")) {
                Thread.sleep(slowTime);
            }
            else {
                throw new Exception("make exception");
            }
        }
    }
    

    其中注解 @RefreshScope 和 @Value 实现配置 echo.slowtime 热加载。

    在 dice.yml 的 Addon 部分增加配置中心:

    # echo web 增加 API

    编辑 io.terminus.erda.bestpractice.echo.controller.ErrorController 类,实现 /api/error API:

    @RefreshScope
    @RestController
    @RequestMapping(value = "/echo/error")
    public class ErrorController {
        @Value("${echo.slowtime:300}")
        private int slowTime;
    
        @RequestMapping(method = RequestMethod.POST)
        void errorPost(@RequestBody EchoError err) throws Exception {
            if (err.getType().equals("slow")) {
                Thread.sleep(slowTime);
            }
            else {
                throw new Exception("make exception");
            }
        }
    }
    
    class EchoError {
        private String type;
    
        public void setType(String type) {
            this.type = type;
        }
    
        public String getType() {
            return type;
        }
    }
    

    # 验证

    再次执行两个应用的工作流完成更新部署。

    在 echo-service 应用的 环境部署 点击 配置中心,或进入 微服务治理平台 > 服务治理 > 配置中心,设置 echo.slowtime 的值:

    可逐步从小到大进行设置(例如 500、1000、1500),每次配置将被热加载实时生效,随后可在 API 测试界面上调用 /api/error API 进行访问。

    # 服务治理

    # 前提条件

    为实践服务治理,需先制造一些请求和异常。

    • 调用 /api/contnet 接口实现对资源的增删改查。
    • 设置 echo.slowtime 设置为 1500 后,调用 /api/error 接口且 type=slow 的情况接口模拟超时。
    • 调用 /api/error 接口且 type=exception 的情况接口模拟异常。

    提示

    由于 Feign 调用默认使用 Ribbon进行负载均衡,且 Ribbon 的默认超时时间为 1 秒,因此 echo.slowtime 设置超过 1 秒时接口可以模拟超时。

    # 平台治理

    进入 微服务治理平台 > 服务总览 > 拓扑,查看项目的微服务全景图,其中异常情况已用红色标明。

    进入 监控中心 > 服务监控 > 链路查询,选择 链路状态错误,可查看异常链路的信息。

    由上图可以看出,echo-service 的 /echo/error 接口耗时 500+ 毫秒导致慢请求。

    更多相关信息,请参见 服务分析

    至此,您已通过一个 echo 微服务项目实践了 Erda 上的 Spring Cloud 开发。整个过程涉及到微服务组件(注册中心、配置中心)的使用、CI/CD 工作流、API 设计和测试、服务异常观测等,本文中仅点到为止,详细使用请参见各平台的使用指南。