继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

基于Spring Cloud的微服务设计

慕标5832272
关注TA
已关注
手记 1229
粉丝 229
获赞 1001

原文链接(博主原创):基于Spring Cloud的微服务设计

微服务这个词大家应该都不陌生,是最近几年技术发展的热门词汇之一。在当前系统需求越来越复杂,实现和维护成本越来越高的背景下,微服务确实是未来的发展趋势之一。由于工作的需要,最近花了半个月左右的时间研究了基于Spring Cloud的微服务设计与实现,颇有收获,本文就来对这半个月的成果进行一个总结,并聊一聊在我对微服务的一些思考。

一、背景

博主所在的公司是一家工业领域的巨头,正在往软件公司转型,目前是以互联工业(就是物联网)为发展方向。因此最近几年在软件上下了很大的成本,公司原有的软件也在不断的进行迭代更新,技术选型也是尽量的“前卫”。我所在的团队是负责消防物联网云平台的开发,但是本博文不会介绍太多物联网的相关知识,只是单纯的从技术角度讨论微服务设计与实现的细节。

二、为什么需要微服务?

我们选择做微服务的原因我总结起来有两个:

1、微服务的特点:解耦。实现业务模块的可拔插操作,灵活可配置;
2、跟上时代的前沿,为公司未来的软件布局做好铺垫工作。

公司的软件很多,就单消防系统而言可能就会有七八个子系统等等,他们之间或大或小,但要求都是要可以灵活配置并相互集成,而微服务正好适合这样的场景,服务小而轻便,分而治之,方便集成和维护。

三、设计与实现

我们使用Spring Cloud的相关组件来实现微服务,组件包含:consul、hystrix、zuul、ribbon、openfeign。
先来看下整体的架构图:

webp

SpringCloud微服务架构.png

将服务按业务进行拆分,注册到consul中,gateway是一个较为特殊的服务,此服务统一的接收外部请求,然后使用zuul配置路由,将请求转发到对应的服务上(这要求每个服务的请求必须有一个唯一的前缀,如user服务中有一个接口叫/api/user,那么实际前端在请求时的请求路径应为/user-service/api/user)。每个service都会提供一个client,client中定义了dto并使用feign写好了提供给其他服务调用的接口,其他服务只需要引用这个client的jar就可以方便的调用该服务接口。

实现细节

首先需要在服务器上安装consul,docker pull 拉下consul镜像,然后定义docker-compose.yml:

version: '3.6'services:  consul:    image: consul    container_name: consul    restart: always    ports:      - 8500:8500

执行 docker-compose -f ./docker-compose.yml up -d,启动consul:

webp

consul-server.png

然后启动一个web项目,这里以gateway服务为例介绍具体的配置方法。在这里说明下我的项目中使用了maven的多模块管理,项目结构如下图:

webp

maven多模块结构.png

  • configuration、dependency、service三个module都继承了顶层的项目。

  • dependency模块只有一个pom文件,用来存放所有需要用到的依赖,并且这些依赖的不定义版本的,版本定义放在顶层pom中统一管理,此模块最终打包成pom。

  • configuration用来存放配置相关的类,最终打包成jar供service使用。

  • service是具体的业务模块,此模块中引入了对configuration和dependency的依赖。

在dependency模块的pom.xml中添加spring cloud相关依赖:

<?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">
    <parent>
        <groupId>com.codemonkey</groupId>
        <artifactId>cm-gateway</artifactId>
        <version>1.0.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cm-gateway-dependency</artifactId>
    <packaging>pom</packaging>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- consul -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-consul-all</artifactId>
        </dependency>

        <!-- spring-cloud-starter-feign -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

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

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

        <!-- zuul网关 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-zuul</artifactId>
        </dependency>

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

        <!-- 配置log4j2 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-log4j2</artifactId>
        </dependency>

        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>servlet-api</artifactId>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
        </dependency>
 

        <!-- 数据库相关 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <!--<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-mongodb</artifactId>
        </dependency>-->
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies></project>

顶层项目的pom中定义版本和公共配置:

<?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>com.codemonkey</groupId>
    <artifactId>cm-gateway</artifactId>
    <version>1.0.0</version>
    <packaging>pom</packaging>

    <description>CM-Gateway of CodeMonkey</description>

    <modules>
        <module>cm-gateway-service</module>
        <module>cm-gateway-configuration</module>
        <module>cm-gateway-dependency</module>
    </modules>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <docker.image.prefix>cm-gateway</docker.image.prefix>

        <!-- 版本控制 -->
        <java.version>1.8</java.version>
        <spring.cloud.version>Finchley.RELEASE</spring.cloud.version>
        <spring.boot.version>2.0.2.RELEASE</spring.boot.version>
        <servlet.api.version>2.5</servlet.api.version>
        <commons.dbcp2.version>2.2.0</commons.dbcp2.version>
        <commons.codec.version>1.10</commons.codec.version>
        <fastjson.version>1.2.46</fastjson.version>
        <swagger.version>2.8.0</swagger.version>
        <jaxb.api.version>2.3.0</jaxb.api.version>
        <netty.version>4.1.16.Final</netty.version>
        <postgresql.version>42.2.2</postgresql.version>
        <druid.version>1.1.10</druid.version>
        <hystrix.version>1.4.5.RELEASE</hystrix.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring.cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring.boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-hystrix</artifactId>
                <version>${hystrix.version}</version>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-zuul</artifactId>
                <version>1.4.5.RELEASE</version>
            </dependency>
            
            <!-- 这里只写了一部分,剩下的可自己补充 -->
            ....        </dependencies>
    </dependencyManagement>
    ....</project>

service配置文件有两个,一个是bootstrap.yml,主要存放spring cloud相关配置,一各是application.yml用来存放数据库等其他配置信息。
bootstrap.yml:

server:  port: 80spring:  application:    name: cm-gateway  cloud:    consul:      host: ${consul.server}      port: ${consul.port}      discovery:        enabled: true        register: true        instance-id: ${spring.application.name}        register-health-check: true#        health-check-url: http://${health.ip}:${server.port}/${service.prefix}/health        health-check-interval: 10s        prefer-ip-address: true        health-check-path: /api/health      config:        enabled: true        format: yaml        prefix: config        default-context: application        data-key: datahystrix:  metrics:    enabled: true    polling-interval-ms: 5000
    zuul:
  # zuul默认为所有服务开启默认的路由,为了服务安全,此处关闭  ignored-services: '*'
  # 自定义服务路由,假设此处有user和workorder两个服务  routes:    cm-user: /user-service/**    cm-workorder: /workorder-service/**

注意,这里开启了consul的健康检查机制,consul会定时的向每个服务发送POST请求,判断服务的健康状态,如果健康检查不通过,服务的接口是无法成功访问的。下面看下健康检查的代码:

/**
 * 健康检查
 *
 * @Author: zhenzhong.wang
 * @Date: 8/2/2018 3:44 PM
 */@RestControllerpublic class HealthController {    @GetMapping("/api/health")    public String healthCheck() {        return "OK";
    }
}

application.yml:

spring:  datasource:    druid:      name: druid-database-fireiot      url: ${database.url}      username: ${database.username}      password: ${database.password}      driver-class-name: ${database.driver}      filters: stat
      # 初始连接数      initialSize: 1
      # 最大连接数      maxActive: 20
      # 获取的最长等待时间      maxWait: 60000
      # 最小空闲连接数      minIdle: 1
      # 每一分钟执行空闲连接回收器      timeBetweenEvictionRunsMillis: 60000
      # 每十分钟回收空闲连接      minEvictableIdleTimeMillis: 600000      validationQuery: select 'x'      testWhileIdle: true      testOnBorrow: false      testOnReturn: false      poolPreparedStatements: true      maxOpenPreparedStatements: 20  jpa:    hibernate:      ddl-auto: update      use-new-id-generator-mappings: true    show-sql: true

Application启动类代码:

/**
 * 服务启动类
 *
 * @Author: zhenzhong.wang
 * @Date: 7/31/2018 10:21 AM
 */@SpringBootApplication@EnableDiscoveryClient@EnableZuulProxypublic class GatewayApplication {    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class, args);
    }
}

gateway服务的基本配置已经介绍完了,除了上述配置外,gateway上还可以配置spring security的权限管理,负载均衡等功能,篇幅原因,本文不再详细介绍这些配置。

服务Client的搭建

每个服务都会提供一个client,供其他服务调用,其实就相当于是服务的SDK,方便其他服务调用。对于Client的构建,我专门新建了一个maven项目,用来存放所有服务的client,每个服务的client作为该项目的一个子module存在。假设我们有用户、设备、工单三个服务,那么client项目的结构如下:

cm-back-client
 - cm-client-user
 - cm-client-device
 - cm-client-workorder

看到这里有些同学可能会奇怪了,每个服务都有自己的client,那么为什么不把client放到各自的服务中呢?

注意:这个问题其实是为了解决引用client时的版本号更新问题,想象一下,如果user服务的client放到了user项目内部,那么其他服务想要调用这个user的client的话,必须由负责user服务的开发人员将user-client打包,并deploy到nexus私有仓库,然后其他服务才能够调用。这样就带来一个问题,项目deploy到nexus必须要更新user-client的版本号,否则是无法deploy的!那么就意味着user-client的每一次变化想被其他项目感知,都需要调用方更新所调用的client的版本号!如果有几十个甚至更多服务相互调用的话这个client版本号是很难管理的,特别是在开发前期,一个服务的client可能会经常变化的。

而采用单独的项目存放所有的client,如果client有变动,开发人员只需要push到git仓库,其他服务的开发人员只需要把client更新pull下来,然后本地执行mvn install,就可以将本地的client依赖覆盖更新,不需要更改版本号。

下面来具体介绍下client的内部实现,pom中添加相关依赖:

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-dependencies</artifactId>
        <version>${spring.cloud.version}</version>
        <type>pom</type>
        <scope>import</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-dependencies</artifactId>
        <version>${spring.boot.version}</version>
        <type>pom</type>
        <scope>import</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-hystrix</artifactId>
        <version>${hystrix.version}</version>
    </dependency></dependencies>

client中主要有两个包组成,以user-client为例:

com.codemonkey.client.user.model    // 存放dtocom.codemonkey.client.user.service  // 用feign编写的声明式接口,供其他服务调用

feign接口示例:

@FeignClient("user-service")public interface HelloWorldService {    @GetMapping("/api/user")    public UserDto getUserInfo();
}

至此,一套微服务的骨架已经成型了,但是距离完整的微服务还差一些必要工作没有完成,如分布式事务管理(LCN),微服务session共享(redis),分布式锁(redis)等等,这些工作还在进行中,等以后有时间我在整理到博客中。

四、思考与总结

这段时间对微服务的研究与实践,说实话,由于项目处于初期阶段的原因,暂时还没有体会到微服务的便利性,反而是在缺点上总结了不少 - -,下面来聊一聊微服务带给我的一些感受。

1、首先,相对于传统的SOA而言,微服务将各个模块进行拆分,开发人员独立负责不同服务的开发,这样在对一些依赖版本和通用代码的统一上不是很友好,甚至同样的代码需要在不同服务上分别实现一遍。(其实这严格来说不算是缺点,只是针对微服务需要更多的前期准备工作,比如写一些公用的starter,这需要更多的时间成本)。

2、微服务在权限管理上比传统方式更复杂,由于所有的外部请求都会通过服务网关,因此我们一般会在网关上做统一的权限校验,但同时其他服务也不能完全的将接口暴露在外部或内网,应该同样具备登录校验或权限校验的能力(避免如postman直接访问其他服务的情况),这就需要网关服务提供额外的接口,供其他服务校验用户登录和权限,这同时也会对gateway造成额外的访问负担(如果某同学有更好的解决方案,欢迎在评论区讨论)。

3、微服务的运维成本比传统项目更高,每个服务都是单独的个体,需要分别部署,在服务越来越多的情况下必然需要使用一些更灵活的手段来部署,比如使用docker,k8s这样的容器化技术,再加上现在SaaS模式越来越普遍,针对应用的多环境配置也使得运维变得更加复杂。

4、微服务的数据库往往也会根据服务进行拆分,这意味着以前能够关联的表,现在可能不再允许关联。比如访问比较频繁的user表,很多业务表都会依赖这个user表,在微服务的情况下往往会有一个专门的user服务,那么其他的服务想要获取user信息只能够拿user的关键字到user服务去查,这种访问是极为频繁的,对整体的性能有很大的影响。有朋友提出用cache去解决类似的问题,将类似的访问频繁又不太会变得数据放到redis中,这是一种方法,但是无论怎么做,都需要站在更高的角度做整体的考量,这需要更丰富的业务与实践经验(博主还需要成长~)。

5、微服务真的很解耦,各管各的,代码冲突的少了,扯皮也少了,自己管理自己的服务也不会有太多的约束,从各自的服务上也能一定程度反映出各个开发人员的技术水平。(项目够大的时候可能要多个人开发一个服务,那就另当别论了~)。

总的来说,微服务短期来看增加了我们开发的一些负担,但从长远来看还是值得一试的,有幸在公司有机会参与到这样的过程,后续如果对微服务有了更深的见解,会持续更新在博客中。如果有同学对本篇博文有疑问的,欢迎评论区留言讨论。



作者:半仙LaLaLa
链接:https://www.jianshu.com/p/c37613d0f501


打开App,阅读手记
0人推荐
发表评论
随时随地看视频慕课网APP