个人博客地址:基于Spring Cloud的微服务设计
微信公众号:Code技术资讯,每日为你带来各种开发/运维干货。
微服务这个词大家应该都不陌生,是最近几年技术发展的热门词汇之一。在当前系统需求越来越复杂,实现和维护成本越来越高的背景下,微服务确实是未来的发展趋势之一。由于工作的需要,最近花了半个月左右的时间研究了基于Spring Cloud的微服务设计与实现,颇有收获,本文就来对这半个月的成果进行一个总结,并聊一聊在我对微服务的一些思考。
一、背景
博主所在的公司是一家工业领域的巨头,正在往软件公司转型,目前是以互联工业(就是物联网)为发展方向。因此最近几年在软件上下了很大的成本,公司原有的软件也在不断的进行迭代更新,技术选型也是尽量的“前卫”。我所在的团队是负责消防物联网云平台的开发,但是本博文不会介绍太多物联网的相关知识,只是单纯的从技术角度讨论微服务设计与实现的细节。
二、为什么需要微服务?
我们选择做微服务的原因我总结起来有两个:
1、微服务的特点:解耦。实现业务模块的可拔插操作,灵活可配置;
2、跟上时代的前沿,为公司未来的软件布局做好铺垫工作。
公司的软件很多,就单消防系统而言可能就会有七八个子系统等等,他们之间或大或小,但要求都是要可以灵活配置并相互集成,而微服务正好适合这样的场景,服务小而轻便,分而治之,方便集成和维护。
三、设计与实现
我们使用Spring Cloud的相关组件来实现微服务,组件包含:consul、hystrix、zuul、ribbon、openfeign。
先来看下整体的架构图:
将服务按业务进行拆分,注册到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:
然后启动一个web项目,这里以gateway服务为例介绍具体的配置方法。在这里说明下我的项目中使用了maven的多模块管理,项目结构如下图:
- 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、微服务真的很解耦,各管各的,代码冲突的少了,扯皮也少了,自己管理自己的服务也不会有太多的约束,从各自的服务上也能一定程度反映出各个开发人员的技术水平。(项目够大的时候可能要多个人开发一个服务,那就另当别论了~)。
总的来说,微服务短期来看增加了我们开发的一些负担,但从长远来看还是值得一试的,有幸在公司有机会参与到这样的过程,后续如果对微服务有了更深的见解,会持续更新在博客中。如果有同学对本篇博文有疑问的,欢迎评论区留言讨论。