基于Spring Cloud的微服务设计

个人博客地址:基于Spring Cloud的微服务设计
微信公众号:Code技术资讯,每日为你带来各种开发/运维干货。

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

一、背景

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

二、为什么需要微服务?

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

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

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

三、设计与实现

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

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:

consul-server.png

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

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: 80
spring:
  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: data
hystrix:
  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
 */
@RestController
public 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
@EnableZuulProxy
public 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    // 存放dto
com.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、微服务真的很解耦,各管各的,代码冲突的少了,扯皮也少了,自己管理自己的服务也不会有太多的约束,从各自的服务上也能一定程度反映出各个开发人员的技术水平。(项目够大的时候可能要多个人开发一个服务,那就另当别论了~)。

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容