随着工程的日渐迭代单体工程往往十分臃肿,为了实现更好的解耦与性能通常将一个工程拆分为多个子模块,每个子模块之间相互独立互不影响。
下面通过一张图更生动的展示,多个模块 Server
由服务中心 Center
实现统一管理,由服务网关 Gateway
处理对外的请求访问,分布式配置服务 Config
用于多个模块服务的配置管理。
Spring Cloud
拥有多个版本,不同版本与之对应的 Spring Boot
版本也稍有出入,如果二者版本没有相对应可能出现一系列兼容问题,二者的版本对应参考下表:
名称 | 对应 Spring 版本 |
---|---|
Hoxton | 2.2.x, 2.3.x |
Greenwich | 2.1.x |
Finchley | 2.0.x |
Edgware | 1.5.x |
Dalston | 1.5.x |
按照上述的架构图,本文将详细介绍每个功能模块的具体构建步骤。
一、注册中心
新建 register-server
工程用于充当服务注册中心,这里选择了 Dalston
版本,因此 Spring Boot
需要选择 1.5.x
版本。
1. POM依赖
在新建的工程中导入如下中的 eureka
与 Dalston
依赖。
注意本文后续中所涉及的工程示例中 Spring
均采用 1.5.4
版本,并都导入了 Dalston
依赖,后续将不再重复说明。
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka-server</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Dalston.SR1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
2. 项目配置
在工程的 YML
配置文件中添加如下信息,除了基本的端口和服务配置之外同时在 defaultZone
中指定了服务中心的地址,后续客户端将通过该地址进行注册。
server:
port: 9090
spring:
application:
name: eureka-server
eureka:
instance:
hostname: localhost
client:
register-with-eureka: false
# 禁用自身的默认客户端注册行为
fetch-registry: false
serviceUrl:
# 自定义服务注册中心地址
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
3. 注册激活
在工程的启动类上添加 @EnableEurekaServer
注解表示启动服务注册中心。
@EnableEurekaServer
@SpringBootApplication
public class EurekaServiceApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServiceApplication.class, args);
}
}
二. 服务注册
新建工程 register-client
模拟客户端服务,通过配置将注册到上述的创建的注册中心。
1. POM依赖
在新建的工程 POM
文件中导入下列依赖,略去 Spring parent
与 Dalston
(参考注册中心模块)。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
</dependencies>
2. 项目配置
在工程 YML
文件中配置注册中心地址,即上一点中服务注册中心模块 YML
中配置的地址。
server:
port: 9091
spring:
application:
name: client-1
eureka:
client:
serviceUrl:
# 注册到服务中心
defaultZone: http://localhost:9090/eureka/
3. 测试接口
在工程中添加一个测试接口,通过 DiscoveryClient
的 getServices()
方法可以获取注册中心中已注册的服务,返回一个集合。
import org.springframework.cloud.client.discovery.DiscoveryClient;
@RestController
@RequestMapping("/api/client")
public class ClientController {
@Autowired
DiscoveryClient discoveryClient;
@GetMapping("/getServices")
public String getServices() {
return "Services: " + discoveryClient.getServices();
}
}
4. 注册服务
在工程启动类添加 @EnableDiscoveryClient
注解激活 Eureka
中的 DiscoveryClient
实现。
@EnableDiscoveryClient
@SpringBootApplication
public class EurekaClientApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaClientApplication.class, args);
}
}
完成上述准备工作之后启动 register-server
与 register-client
两个工程,浏览器访问服务中心地址(http://localhost:9090
)即可查看 Eureka
管理面板。
通过管理面板可以看到 client-1
已完成注册操作。
通过 Postman
等工具请求上述的 getServices
接口,即可获取当前服务中心已注册的服务节点名称,这里服务名取的是项目 YML
配置中的 application name
。
三、服务消费
在上面的示例中介绍了如何实现服务注册,但是当存在多个服务时,不同服务之间又该如何实现通讯呢?
下面将在之前的示例中进一步介绍如何实现服务通讯。
1. POM依赖
新建 basic-consumer
工程,并在 POM
文件中引入下列依赖,这里略去 Spring parent
与 Dalston
。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<!-- Spring Boot健康监控 -->
<!-- 控制台会输出一系列 Mapped 信息 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
2. 项目配置
与之前的 register-client
模块类似,在工程的 YML
添加配置将自身注册到服务中心。
server:
port: 9092
spring:
application:
name: basic-consumer
eureka:
client:
serviceUrl:
# 注册到服务中心
defaultZone: http://localhost:9090/eureka/
3. 服务注册
同样需要在工程启动类添加 @EnableDiscoveryClient
注解开启服务发现。
同时在工程中新建 TemplateConfig
配置类并注入 RestTemplate
对象,其可以理解为 Spring
中的网络客户端,用于后续发送网络请求。
@EnableDiscoveryClient
@SpringBootApplication
public class BasicConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(BasicConsumerApplication.class, args);
}
}
@Configuration
public class TemplateConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
4. 通讯示例
新建通讯测试接口,通过负载均衡客户端 loadBalancerClient
选择服务中心中相应的服务,如本示例选择了之前已经注册的 client-1
服务。
客户端选择返回的实例中包含了对应服务的基本信息,如服务地址与端口等等,通过获取的信息使用 restTemplate
发送请求。
@RestController
@RequestMapping("/api/basic/consumer")
public class TestController {
@Autowired
private RestTemplate restTemplate;
@Autowired
private LoadBalancerClient loadBalancerClient;
@GetMapping("/get")
public String get() {
// 通过负载均衡选择客户端实例
ServiceInstance serviceInstance = loadBalancerClient.choose("client-1");
// 根据选择的实例拼接请求地址
StringBuilder builder = new StringBuilder();
builder.append("http://")
.append(serviceInstance.getHost())
.append(":")
.append(serviceInstance.getPort())
.append("/api/client/getServices");
// 通过 restTemplate 调用服务接口
String url = builder.toString();
return restTemplate.getForObject(url, String.class);
}
}
完成上述操作之后依次启动 register-server
、 register-client
与 basic-cosumer
三个工程,通过 Postman
工程请求 basic-cosumer
工程的 get
接口。
可以看到通过 basic-cosumer
服务(9092
)即实现了 register-client
服务中的 getServices
接口服务调用,因此 consumer
通常也作于各服务之间的通讯媒介。
四、Feign调用
在 basic-consumer
模块中介绍了服务之间的相互通讯,但在服务调用时相对较为繁杂,因此 Spring Cloud
中提供了更便捷的 feign
服务调用方式。
Feign
调用通常用于注册服务中心中各服务之间的请求通讯,下面将介绍如何通过 feign
实现服务之间的通讯。
1. POM依赖
新建 feign-consumer
工程并添加下述依赖,同样略去 Spring parent
与 Dalston
。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- feign 消费 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
</dependency>
</dependencies>
2. 项目配置
同样的在 YML
文件中配置服务中心地址实现注册。
server:
port: 9094
spring:
application:
name: feign-consumer
eureka:
client:
serviceUrl:
# 注册到服务中心
defaultZone: http://localhost:9090/eureka/
3. 服务注册
在工程启动类上同时添加 @EnableDiscoveryClient
与 @EnableFeignClients
注解开启服务注册与 Feign
服务。
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class FeignConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(FeignConsumerApplication.class, args);
}
}
4. 消费示例
新建 FeignService
类并在类上添加 @FeignClient
注解用于指定服务节点,@GetMapping
用于指定对应服务的接口地址。
@FeignClient("client-1")
public interface FeignService {
@GetMapping("/api/client/getServices")
String getServices();
}
新建测试接口 get
调用上述的 feignService
实现,启动项目后通过该接口即可实现服务通讯。
与之前的 basic-consumer
示例一致,通过 Postman
等工具请求 feign-consumer
的 get
测试接口即可实现 Client-1
服务的 /api/client/getServices
接口访问。
@RestController
@RequestMapping("/api/feign/consumer")
public class TestController {
@Autowired
FeignService feignService;
@GetMapping("/get")
public String get() {
return feignService.getServices();
}
}
五、配置中心
当工程中的服务达到一定数量后,为了实现配置的统一管理,通常会将 YML
等配置实现云上配置。
1. 仓库准备
在 GitHub
中创建一个仓库,假如存在 config-client
服务则在仓库中创建 config-client.yml
文件。
info:
profile: default
ibudai:
name: hello
2. 服务配置
新建 config-server
工程并在 Pom
文件中导入如下配置,同样略去 Spring parent
与 Dalston
。
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
</dependencies>
在工程的 YML
文件中配置上述的创建的 GitHub
仓库地址,同时将自身注册到服务中心。
server:
port: 9096
spring:
application:
name: config-server
cloud:
config:
server:
git:
uri: https://github.com/great-jin/eureka-config
skip-ssl-validation: true
eureka:
client:
serviceUrl:
# 注册到服务中心
defaultZone: http://localhost:9090/eureka/
在项目的启动类添加 @EnableConfigServer
与 @EnableDiscoveryClient
注解激活配置。
@EnableConfigServer
@EnableDiscoveryClient
@SpringBootApplication
public class ConfigServerApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigServerApplication.class, args);
}
}
完成后启动项目访问 http://localhost:9096/config-client/default/main
地址即可看到下述信息,其中 label
为仓库的分支名称。
3. 配置读取
新建 config-client
工程并在 Pom
文件添加下述依赖。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
</dependencies>
注意读取仓库配置的工程需要将配置文件替换为 boostrap.yml
,其与 application.yml
的主要区别在于前者的执行优先级更高。
在 boostrap.yml
中下述服务配置信息,其中 uri
为上述的 config-server
服务运行地址,profile
为多配置的区分标识, label
为仓库的分支名称。
server:
port: 9097
spring:
application:
name: config-client
cloud:
config:
uri: http://localhost:9096/
profile: default
label: main
eureka:
client:
serviceUrl:
# 注册到服务中心
defaultZone: http://localhost:9090/eureka/
添加测试接口用于读取仓库配置文件值,启动项目后请求接口接口获取仓库中配置的 hello
。
@RestController
@RequestMapping("api/config/client")
public class TestController {
@Value("${ibudai.name:default}")
private String name;
@GetMapping("/getInfo")
public String getInfo() {
return name;
}
}
六、服务网关
当一个工程存在多个服务节点并需要对外开放服务时,显然最合适的方式就是提供一个统一的出入口,而并非将每个服务单独开放出去,服务网关的则正是为此而生。
通过网关,无论内部服务数量有多么庞大,对外的服务路径永远只有一个,从而实现更统一的管理。
1. POM依赖
新建 api-gateway
工程,在 Pom
文件中引入下述依赖。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<!-- Gateway server -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>
<!-- slf4j -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</dependency>
</dependencies>
2. 项目配置
同样在项目的 YML
配置文件中将自身注册到服务中心。
server:
port: 9095
spring:
application:
name: api-gateway
eureka:
client:
serviceUrl:
# 注册自身服务到服务端
defaultZone: http://localhost:9090/eureka/
3. 网关配置
在工程的启动类添加 @EnableZuulProxy
注解激活服务网关。
@EnableZuulProxy
@SpringBootApplication
public class APIGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(APIGatewayApplication.class, args);
}
}
启动 api-gateway
项目之后即可通过网关代理实现服务请求,请求格式如下:
[gateway url]/[server name]/[api adress]
上述格式中各参数表述信息参考下表。
变量 | 描述 |
---|---|
gateway url | 网关项目的运行地址,如上述的 http://localhost:9095 |
server name | 需要访问的服务名称,如之间的 client-1。 |
api adress | 需要访问的服务接口地址。 |
完成上述项目配置之后,依次启动 register-server(9090)
、register-client(9091)
和 api-gateway(9095)
三个工程,在之前的示例中我们在register-client
开放了接口 /api/client/getServices
接口查询当前已注册的服务,其完整的接口访问地址为:http://localhost:9091/api/client/getServices
。
按照上表的中的网关访问规范,现在我们即可通过网关服务实现 client-1
的接口访问,即上述的接口地址可转化为 http://localhost:9095/client-1/api/client/getServices
。
4. 过滤器
既然服务网关实现了统一请求入口,因此通常在网关会搭配过滤器等实现,用于提前拦截非法请求。
网关拦截器实现和以往的拦截器类似,新建 WebFilter
并继承 ZuulFilter
类,重写类中四类方法,各方法的作用参考下表。
方法 | 作用 |
---|---|
filterType() | 拦截器的类型,如事前拦截 pre。 |
filterOrder() | 当前存在多个拦截器时通过其设置优先级。 |
shouldFilter() | 用于指定是否激活拦截器。 |
run() | 拦截器的具体实现内容 |
如下述的示例中定义一个拦截器在请求之前实现拦截,当接口访问地址非登录地址时触发拦截,当触发拦截时通过 RequestContext.getCurrentContext()
获取当前请求上下文从而获取请求头,判断是否包含请求认证头信息,若否则拦截请求并返回 401
状态。
public class WebFilter extends ZuulFilter {
private Logger logger = LoggerFactory.getLogger(WebFilter.class);
/**
* Filter type, set "pre" will active before trigger.
*/
@Override
public String filterType() {
return "pre";
}
/**
* When have multiple filter, use order to set priority.
*/
@Override
public int filterOrder() {
return 0;
}
/**
* To set is enable filter, normally we will specify the effect region
*/
@Override
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String targetPath = request.getRequestURL().toString();
boolean enableFilter;
try {
URL url = new URL(targetPath);
String path = url.getPath();
int index = ordinalIndexOf(path, "/", 2);
enableFilter = path.substring(index).startsWith("/api/login");
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
return !enableFilter;
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
logger.info("Send {} request to {}", request.getMethod(), request.getRequestURL().toString());
Object accessToken = request.getHeader("token");
if (accessToken == null) {
logger.warn("Token is empty");
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(401);
return null;
}
logger.info("Token verify success");
return null;
}
private int ordinalIndexOf(String str, String substr, int n) {
int pos = str.indexOf(substr);
while (--n > 0 && pos != -1)
pos = str.indexOf(substr, pos + 1);
return pos;
}
}
5. 过滤注册
完成上述过滤器定义之后还需要将其注入 Spring
容器之中才可生效。
新建配置类 FilterConfig
,并在其中注入 WebFilter
对象。
@Configuration
public class FilterConfig {
@Bean
public WebFilter webFilter() {
return new WebFilter();
}
}
七、服务熔断
1. 概念介绍
服务熔断是一种微服务架构中常用的容错机制,用于防止雪崩效应和提高系统的可用性。当服务出现异常或者延迟时,服务熔断会暂时地停止对该服务的请求,而不是持续地尝试调用,从而避免因服务故障而导致的整个系统崩溃。
服务熔断通常由以下几个核心组件组成:
(1) 熔断器
熔断器 (Circuit Breaker)
是服务熔断的核心组件,它监控对服务的调用情况,并根据一定的条件 (例如错误率、超时率等) 来决定是否开启熔断。一旦开启熔断,熔断器将拒绝对服务的调用,并在一段时间内直接返回预先定义的降级响应。
(2) 熔断状态
熔断器有两种状态,分别是开启状态 (Open)
和关闭状态 (Closed)
。当熔断器处于开启状态时,所有对服务的请求都会被直接拒绝,而不会发送到服务端。当熔断器处于关闭状态时,对服务的请求将正常发送到服务端。
(3) 熔断器状态转换
熔断器会根据一定的条件进行状态转换 (Tripping/Fixed)
。当服务调用失败或超时达到一定阈值时,熔断器会从关闭状态转换为开启状态 (Tripping)
。
一段时间后,熔断器会自动进入半开启状态 (Half-Open)
,允许部分请求通过以测试服务是否已经恢复。如果服务恢复正常,则熔断器将转换为关闭状态 (Fixed)
,否则继续保持开启状态。
(4) 降级机制
当熔断器开启时,它会直接返回预先定义的降级响应 (Fallback)
,而不是发送请求到服务端。降级响应通常是一些预先定义的固定值或者简单的错误信息,用于告知调用方服务不可用或者出现异常。
2. 依赖引入
Hystrix
是 Netflix
开发的一款服务熔断框架,与之前依赖类似额外引入下述依赖。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-hystrix</artifactId>
</dependency>
3. 熔断配置
Hystrix
提供了 @HystrixCommand
注解用于配置熔断策略,注解参数描述如下:
方法 | 作用 |
---|---|
commandProperties | 熔断策略配置参数。 |
threadPoolProperties | 熔断服务线程池配置。 |
fallbackMethod | 配置降级回退方法。 |
下述即一个简易的服务熔断配置,当模拟睡眠接口超时后将触发熔断机制,执行降级方法 fallback()
,需要注意的是降级方法的入参必须与熔断方法的入参一致。
@Service
public class HystrixService {
@HystrixCommand(
commandProperties = {
// Timeout strategy, default value: true, 1000
@HystrixProperty(name = "execution.timeout.enabled", value = "true"),
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000")
},
threadPoolProperties = {
@HystrixProperty(name = "coreSize", value = "10"),
@HystrixProperty(name = "maxQueueSize", value = "20"),
@HystrixProperty(name = "keepAliveTimeMinutes", value = "2"),
@HystrixProperty(name = "queueSizeRejectionThreshold", value = "15"),
@HystrixProperty(name = "metrics.rollingStats.timeInMilliseconds", value = "600")
},
// The "fallbackMethod" must have same parameter
fallbackMethod = "fallback"
)
public String getServices(long sleep) {
try {
Thread.sleep(sleep);
} catch (Execption e) {
throw new RuntimeException(e);
}
return "work done!";
}
public String fallback(long sleep) {
return "failed!";
}
}
4. 启动配置
在项目启动类上通过 @EnableHystrix
注解开启服务熔断。
@EnableHystrix
@EnableDiscoveryClient
@SpringBootApplication
public class HystrixDemotionApplication {
public static void main(String[] args) {
SpringApplication.run(HystrixDemotionApplication.class, args);
}
}
Talk is cheap, show me you code。
文中示例工程已上传 GitHub
,仓库直达。
参考链接