原文链接(博主原创):基于Spring Cloud的微服务设计
微服务这个词大家应该都不陌生,是最近几年技术发展的热门词汇之一。在当前系统需求越来越复杂,实现和维护成本越来越高的背景下,微服务确实是未来的发展趋势之一。由于工作的需要,最近花了半个月左右的时间研究了基于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: 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