最近从事了一个 spring cloud 的微服务项目,整个微服务平台的构建都是自己调研的。。。现在重构了一个基本的微服务脚手架,一方面是想巩固一下知识,另一方面也是为了在后面的项目中直接使用。
这里面不仅有基本的理念,还加入了很多实践经验(避免很多人都会踩的坑),比理论上的更好理解,非常适合新手学习。
项目 Github 地址:https://github.com/fishdemon/micro-service-kit
在项目的 oauth 分支有关于 Oauth2+SSO 实现微服务统一鉴权中心的例子,是经过项目实践验证的,之后我会专门写一篇文章来介绍,如果有兴趣可以先行阅读源码
欢迎下载下来一起学习交流,如果觉得还可以,还请加 Star 哦!!!
先上一张微服务的架构图,之后在补充详细内容
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RsBraP4b-1590933150543)(assets/1585374948461.png)]
msk-auth
msk-boot-admin
msk-config
msk-consumer
msk-eureka
msk-gateway
msk-provider
msk-zipkin
之后所有子module 的依赖也在这个根 build.gradle 进行配置,而 module 中自带的 build.gradle 暂时不用。这种集中配置有一个很大的好处,就是修改起来特别方便,对于目前的项目来说很好用。。。当然也有缺点,如果子模块由不同的开发者同时维护,那就不适合这种模式。
build.gradle
plugins {
id 'java'
id 'idea'
id 'org.springframework.boot' version '2.2.5.RELEASE'
id 'io.spring.dependency-management' version '1.0.9.RELEASE'
}
// set dependencies of all projects
allprojects {
apply plugin: 'idea'
apply plugin: 'java'
group 'com.fishdemon.msk'
version '1.0'
sourceCompatibility = 1.8
repositories {
maven{ url 'https://maven.aliyun.com/repository/jcenter'}
maven{ url 'https://maven.aliyun.com/repository/gradle-plugin'}
maven{ url 'https://maven.aliyun.com/repository/public'}
mavenCentral()
}
}
// set dependencies of sub projects
subprojects {
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
ext {
set('springBootAdminVersion', "2.2.1")
set('springCloudVersion', "Hoxton.SR3")
}
dependencies {
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
runtimeOnly 'org.springframework.boot:spring-boot-devtools'
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
}
dependencyManagement {
imports {
mavenBom "de.codecentric:spring-boot-admin-dependencies:${springBootAdminVersion}"
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}
test {
useJUnitPlatform()
}
}
根 build.gradle
project(':msk-eureka') {
dependencies {
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-server'
}
}
进入到 msk-eureka
模块中,Eureka 的启动类很简单,只需要加一个 @EnableEurekaServer 注解可以了
@SpringBootApplication
@EnableEurekaServer
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
application.yml
配置文件为了简单起见,这里先设置一个单节点的 Eureka 服务,若后面想实现高可用,可参考文章
server:
port: 8761
spring:
application:
name: eureka-server
eureka:
client:
# 单机模式下由于自己就是服务器,不需要注册到自己
register-with-eureka: false
# 单机模式下由于自己就是服务器,不需要从服务器获取注册信息
fetch-registry: false
serviceUrl:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka
instance:
# 强烈建议不要用 localhost,否则在分布式部署的时候一大堆坑
hostname: msk-eureka
# 租期更新时间间隔(默认30秒)
lease-renewal-interval-in-seconds: 10
# 租期到期时间(默认90秒)
lease-expiration-duration-in-seconds: 30
server:
# 关闭自我保护(AP),默认是 true
enable-self-preservation: false
# 清理间隔,默认是 60 * 1000 ms
eviction-interval-timer-in-ms: 4000
Eureka 节点注册有两种方案:域名 或 IP地址,如果没有指定域名,默认用 localhost
。
在这里我们用自定义域名来演示,这样系统的移植性比较好,只需要在 hosts 或者 域名服务器 中配置域名映射就可以了。修改系统的 hosts 文件:
127.0.0.1 msk-eureka
根 build.gradle
project(':msk-provider') {
dependencies {
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
implementation 'org.springframework.boot:spring-boot-starter-web'
}
}
进入到子模块msk-provider
中,只需要在启动类上添加注解 @EnableEurekaClient
就可以,表明该服务是Eureka 的一个服务提供方。
package com.fishdemon.msk.provider;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@SpringBootApplication
@EnableEurekaClient
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
application.yml
配置文件如果不使用 docker 部署的话,服务一般使用 ip 地址注册进行比较好,否则分布式部署就要配置很多域名了。
在很多博客文章中,为了简单的演示都是用localhost
来注册服务,并没有说明限制,但对于新手来说,这是很坑的。。。博主当时也是在分布式部署时踩了很多 localhsot
的坑,别提多难受了。
server:
port: 8781
servlet:
context-path: /provider
eureka:
instance:
# 强制使用IP地址进行注册
prefer-ip-address: true
# 设置客户端实例id, 若使用IP地址注册,这个配置必须有,否则会显示成 localhost
instance-id: ${spring.cloud.client.ip-address}:${server.port}
client:
service-url:
# 注册服务到eureka上
defaultZone: http://msk-eureka:8761/eureka
spring:
application:
name: msk-provider
package com.fishdemon.msk.provider.controller;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("hello")
public class ProviderController {
@GetMapping
public String hello() {
return "hello, i'm provider";
}
// 测试 feign 中 path variable 的用法
@GetMapping("/{id}")
public String getById(@PathVariable("id") String id) {
return "hello, you get the provider " + id;
}
// 测试 feign 中 request param 的用法
@GetMapping("/user")
public String getByName(@RequestParam("name") String name) {
return "hello, you get the user " + name;
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YGmNbqFW-1590933150550)(assets/1585378370025.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nlqj9dxI-1590933150558)(assets/1585378394270.png)]
根 build.gradle
project(':msk-consumer') {
dependencies {
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
implementation 'org.springframework.boot:spring-boot-starter-web'
}
}
进入到子模块 msk-consumer
中 , 加入 @EnableEurekaClient
注解表明这是一个 eureka 客户端,同时加入 @EnableFeignClients
注解表明使用 Feign 客户端来调用其他服务的API。
package com.fishdemon.msk.consumer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
application.yml
配置文件server:
port: 8782
servlet:
context-path: /consumer
eureka:
instance:
prefer-ip-address: true
instance-id: ${spring.cloud.client.ip-address}:${server.port}
client:
service-url:
defaultZone: http://msk-eureka:8761/eureka
spring:
application:
name: msk-consumer
Feign 是一种声明式的服务调用,使用方式非常简单。只需创建一个接口,并通过注解注明 服务名称 , 调用的URL,调用参数 即可,使用的都是 WebMVC 已有的注解。它底层仍是使用 RestTemplate
和 Ribbon
来实现的。
package com.fishdemon.msk.consumer.service;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
// 注明远程服务的名称
@FeignClient("msk-provider")
public interface ConsumerService {
// 注明远程调用的 url 及 参数
@GetMapping("/provider/hello")
String hello();
@GetMapping("/provider/hello/{id}")
String getById(@PathVariable("id") String id);
@GetMapping("/provider/hello/user")
String getByName(@RequestParam("name") String name);
}
package com.fishdemon.msk.consumer.controller;
import com.fishdemon.msk.consumer.service.ConsumerService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("hello")
public class ConsumerController {
@Autowired
private ConsumerService consumerService;
@GetMapping
public String hello() {
return consumerService.hello();
}
@GetMapping("/{id}")
public String getById(@PathVariable("id") String id) {
return consumerService.getById(id);
}
@GetMapping("/user")
public String getByName(@RequestParam("name") String name) {
return consumerService.getByName(name);
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VS1XJBrY-1590933150563)(assets/1585380577918.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IKEZv6OY-1590933150568)(assets/1585380543797.png)]
由于新冠疫情影响,最近一段时间股市熔断了3次,扯得有点远,哈哈。。。那这个熔断到底是什么意思呢?股市的熔断和微服务中的熔断一样么?我们来做个实验。
假设生产者服务宕机了,会造成影响什么呢?我们主动将 msk-provider
服务停掉。然后在访问消费者API: http://localhost:8782/consumer/hello ,页面等待了一段时间,最后返回下面的错误:
Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.
Sat Mar 28 15:32:54 CST 2020
There was an unexpected error (type=Internal Server Error, status=500).
connect timed out executing GET http://msk-provider/provider/hello feign.RetryableException:
connect timed out executing GET http://msk-provider/provider/hello
大意就是说调用超时,服务器错误,页面等待的时候就是超时的时间。
可以这样扩展一下,假如超时时间为 2 秒,消费者服务平均1 秒钟有 10000 请求,生产者宕机后,消费者服务中会有每一秒就会有 20000 个服务线程停留在内存中。如果请求更多呢,就可能让消费者服务内存崩溃进而宕掉;如果这时还有其他服务调用这个消费者服务呢,想象一下,其他服务也会宕掉。。。这就是雪崩现象。。
为了不出现上面的情况,微服务中引入了熔断机制,简单来说,就是当生产者服务宕掉后,消费者服务采用默认的 reponse 返回,而不去调用生产者了,保证服务的高可用。
Hystrix 主要配置在服务间通信的地方,而我们使用的Feign正是用来实现服务间通信的。因为Feign已经内置了Hystrix,我们只需要在配置启用 hystrix 即可。
msk-consumer
中的配置application.yml
feign:
hystrix:
# 开启Feign的Hystrix熔断器支持
enabled: true
ConsumerServiceFallback
熔断处理类直接实现 FeignClient 的接口即可,正常时使用 FeignClient 接口来处理,熔断后使用该接口的实现类来处理。
package com.fishdemon.msk.consumer.service.fallback;
import com.fishdemon.msk.consumer.service.ConsumerService;
import org.springframework.stereotype.Service;
@Component
public class ConsumerServiceFallback implements ConsumerService {
@Override
public String hello() {
return "error request: provider service is down";
}
@Override
public String getById(String id) {
return "error request: provider service is down";
}
@Override
public String getByName(String name) {
return "error request: provider service is down";
}
}
这个坑我踩了 1 个小时,注意这里一定要在原 FeignClient 注解中增加这个配置,否则不生效。
@FeignClient(value="msk-provider", fallback = ConsumerServiceFallback.class)
public interface ConsumerService {
// 省略......
}
error request: provider service is down
hello, i'm provider
根 build.gradle
project(':msk-consumer') {
dependencies {
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
implementation 'org.springframework.boot:spring-boot-starter-web'
// 新增依赖
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-hystrix-dashboard'
}
}
增加 @EnableHystrixDashboard 开启页面服务
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
@EnableHystrixDashboard
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
package com.fishdemon.msk.consumer.config;
import com.netflix.hystrix.contrib.metrics.eventstream.HystrixMetricsStreamServlet;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* SpringBoot2.x版本中需要手动指定dashboard的servlet请求地址
*/
@Configuration
public class HystrixDashboardConfig {
@Bean
public ServletRegistrationBean getServlet() {
HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet);
registrationBean.setLoadOnStartup(1);
registrationBean.addUrlMappings("/hystrix");
registrationBean.setName("HystrixMetricsStreamServlet");
return registrationBean;
}
}
根 build.gradle
project(':msk-gateway') {
dependencies {
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-zuul'
}
}
进入到模块 msk-gateway
中,@EnableEurekaClient
开启 Eureka 客户端;@EnableZuulProxy
开启Zuul网关的支持。
package com.fishdemon.msk.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableEurekaClient
@EnableZuulProxy
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
application.yml
配置文件server:
port: 8765
eureka:
instance:
prefer-ip-address: true
instance-id: ${spring.cloud.client.ip-address}:${server.port}
client:
service-url:
defaultZone: http://msk-eureka:8761/eureka
spring:
application:
name: msk-gateway
zuul:
routes:
# 路由名称,可以随意写,不过一般用服务名来区别好一些
msk-consumer:
# zuul默认会将heaher移除再转发,这里设置成传递所有 header
sensitiveHeaders: "*"
# 路由地址
path: /consumer/**
# 路由地址对应的服务名称
service-id: msk-consumer
# 是否去掉路由前缀再转发
stripPrefix: false
msk-provider:
sensitiveHeaders: "*"
path: /provider/**
service-id: msk-provider
stripPrefix: false
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vlHelMdb-1590933150571)(assets/1585386620277.png)]
访问成功,说明通过一个网关就可以访问其他所有的服务。
网关是整个微服务集群的出口,熔断是非常必要的,所以 Zuul 网关中内置了熔断。我们可以试试访问一下不存在的 API,或者将消费者服务停掉在调用一次API,会出现下面的页面。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2o21Jbu6-1590933150575)(assets/1585402117093.png)]
或者是下面的页面
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nNnSJBD7-1590933150576)(assets/1585402319657.png)]
这就是因为服务不可用,导致熔断而返回的信息。在实际中,肯定不能直接返回 spring 抛出的信息,非常不友好。
我们可以配置一个 Fallback 类来定制服务不可用后返回的信息:ConsumerFallbackProvider
package com.fishdemon.msk.gateway.fallback;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Component;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
@Component
public class ConsumerFallbackProvider implements FallbackProvider {
@Override
public String getRoute() {
// 配置生效的服务id, 如果返回 * 或 null , 代表全支持
return "msk-consumer";
}
@Override
public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
return new ClientHttpResponse() {
@Override
public HttpStatus getStatusCode() throws IOException {
return HttpStatus.OK;
}
@Override
public int getRawStatusCode() throws IOException {
return HttpStatus.OK.value();
}
@Override
public String getStatusText() throws IOException {
return HttpStatus.OK.getReasonPhrase();
}
@Override
public void close() {
}
@Override
public InputStream getBody() throws IOException {
ObjectMapper mapper = new ObjectMapper();
Map<String, Object> map = new HashMap<>();
map.put("code", -1);
map.put("message", ",该服务不可用,服务器异常");
return new ByteArrayInputStream(mapper.writeValueAsString(map).getBytes("UTF-8"));
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentType(MediaType.APPLICATION_JSON);
return httpHeaders;
}
};
}
}
停止消费者服务,访问 http://localhost:8765/consumer/hello ,结果是
{"code":-1,"message":",该服务不可用,服务器异常"}
使用 Zuul 做网关,最方便的地方就在于可以通过定义过滤器拦截请求来实现各种各样的功能,比如 权限验证,IP地址过滤,访问日志记录,限流等,下面来示范一个简单权限验证的例子:
package com.fishdemon.msk.gateway.filter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Slf4j
@Component
public class AccessFilter extends ZuulFilter {
@Override
public String filterType() {
// 设置过滤器类型 pre/routing/post/error
return "pre";
}
@Override
public int filterOrder() {
// 设置过滤器优先级, 值越小, 优先级越高
return 0;
}
@Override
public boolean shouldFilter() {
// 过滤器是否生效, true 为生效
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext context = RequestContext.getCurrentContext();
HttpServletRequest request = context.getRequest();
HttpServletResponse response = context.getResponse();
// 查看 header 中是否有token
String token = request.getHeader("x-auth-token");
if (token == null) {
// 不在向后台服务转发
context.setSendZuulResponse(false);
// 设置 response status 为 401
context.setResponseStatusCode(401);
try {
response.getWriter().write("no authentication");
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
}
重启网关,访问 http://localhost:8765/consumer/hello ,结果为
no authentication
根 build.gradle
project(':msk-config') {
dependencies {
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
implementation 'org.springframework.cloud:spring-cloud-config-server'
}
}
package com.fishdemon.msk.config;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@SpringBootApplication
@EnableEurekaClient
@EnableConfigServer
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
application.yml
配置文件server:
port: 8888
eureka:
instance:
prefer-ip-address: true
instance-id: ${spring.cloud.client.ip-address}:${server.port}
client:
service-url:
defaultZone: http://msk-eureka:8761/eureka
spring:
application:
name: msk-config
# config 使用本地存储配置时必须使用 native 环境
profiles:
active: native
cloud:
config:
server:
native:
# 配置文件的存储库配置
search-locations: classpath:config/
这里使用本地作为配置的存储库,当然更好的方案是使用 github/gitlab 来存储,配置如下所示
spring:
# 不要用 native 环境
# profiles:
# active: native
cloud:
config:
server:
# 配置仓库的分支
label: master
git:
uri: http://******.git # git 克隆地址
searchPaths: config # 存储的目录
username: ******* # 用户名
password: ******* # 密码
在 src/main/resources
中创建文件夹 config
, 并将上一节中 zuul 网关服务的配置复制过来放入其下,重命名为application-gateway-pro.yml
,同时只保留一些主要的端口和路由配置
application-gateway-pro.yml
server:
port: 8765
zuul:
routes:
msk-consumer:
sensitiveHeaders: "*"
path: /consumer/**
service-id: msk-consumer
stripPrefix: false
msk-provider:
sensitiveHeaders: "*"
path: /provider/**
service-id: msk-provider
stripPrefix: false
{
"name":"gateway",
"profiles":["pro"],
"label":null,
"version":null,
"state":null,
"propertySources":[]
}
说明 Config 服务配置成功,客户端可以获取到相关的配置。
由于 Config Server 中放置的是 gateway 配置,那我们就用之前的 Zuul Gateway 服务来演示。
根 build.gradle
project(':msk-gateway') {
dependencies {
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-zuul'
// 新增的 config client 依赖
implementation 'org.springframework.cloud:spring-cloud-starter-config'
}
}
为了验证远程配置起作用,将原有的 application.yml
删掉,这样路由信息只存在于远程配置中。同时新建一个 bootstrap.yml
配置:
bootstrap.yml
server:
port: 8765
eureka:
instance:
prefer-ip-address: true
instance-id: ${spring.cloud.client.ip-address}:${server.port}
client:
service-url:
defaultZone: http://msk-eureka:8761/eureka
spring:
application:
name: msk-gateway
profiles:
active: native
cloud:
config:
# 配置文件的前缀名
name: application-gateway
# 配置文件的环境标识, dev/test/prod
profile: prod
# label: # 配置仓库的分支, 这里是本地读取,可不配置
discovery:
# 启动配置发现
enabled: true
# config server 的服务id
serviceId: msk-config
一般 bootstrap.yml
中只放置 端口,eureka 地址,及 config client 的配置即可,作用只是为了与 Config Server 进行通信,因为 bootstrap.yml
的加载先于 application.yml
的加载。其他的配置仍可以放在 application.yml
中,他们可以并存。
网上有些文章说如果 Config server 使用默认的 8888 端口, 则 Config Client 可以使用 application.yml 。我验证了一下,这种说法是谬论。尽管他们的 Client 端正确的获得了配置,但这是一种巧合的环境导致的,Config 的 server端 与 Client端在同一台机器上,且 Config server 使用 8888 端口,这样 Client 在启动的时候默认从 http://localhost:8888
获取配置是成功的。。。但是如果 Config 的 server端 与 Client端不在同一台机器上呢?那就获取不到了把,因此必须用 bootstrap.yml
。
hello, i'm provider
访问成功,证明远程配置中的路由起作用了。
根 build.gradle
project(':msk-boot-admin') {
dependencies {
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
implementation 'de.codecentric:spring-boot-admin-starter-server'
implementation 'de.codecentric:spring-boot-admin-server-ui'
}
}
进入子模块 msk-boot-admin
,@EnableEurekaClient 开启 Eureka 客户端, @EnableAdminServer 开启 boot admin 服务支持。
package com.fishdemon.msk.bootadmin;
import de.codecentric.boot.admin.server.config.EnableAdminServer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@SpringBootApplication
@EnableEurekaClient
@EnableAdminServer
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
application.yml
配置文件server:
port: 8766
eureka:
client:
instance:
prefer-ip-address: true
instance-id: ${spring.cloud.client.ip-address}:${server.port}
service-url:
defaultZone: http://msk-eureka:8761/eureka
spring:
application:
name: msk-boot-admin
boot:
admin:
# boot admin 页面的路径
context-path: /admin
客户端有两种配置方式,区别很大哦,详情请参考文章
这里采用第二种方式,客户端无需做其他修改,只需要加入监控相关的依赖即可
implementation 'org.springframework.boot:spring-boot-starter-actuator'
由于 spring-boot-admin
底层是依赖 actuator 神器实现的,所以需要在之前创建的每一个服务中都加入这个依赖(msk-boot-admin
不需要加,因为它是 Server 端),然后重启所有服务。
http://localhost:8766/admin/wallboard
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tPn5tcCo-1590933150579)(assets/1585393309743.png)]
这个监控页面帅不帅!是不是很有感觉!其实它的功能还很强大,点击任何图中的一个服务进去,还可以看到服务的具体信息(内存/日志/CPU使用率/线程等),它是基于 actuator ,通过配置可以开放更多的信息。
management:
endpoint:
health:
enabled: true
show-details: always
endpoints:
web:
base-path: /actuator
cors:
allowed-origins: true
exposure:
include:
- '*'
根 build.gradle
application.yml
配置文件
如果需要 ZipKin 监控所有服务的信息,需要让每个服务都成为 ZipKin 的客户端。
除了msk-zipkin
服务本身,其他的服务全都要成为 ZipKin 的客户端,在每个服务中都添加下面的配置。
根 build.gradle
// zipkin 客户端
implementation 'org.springframework.cloud:spring-cloud-starter-zipkin'
spring:
zipkin:
base-url: http://localhost:9411
重启之前所有的服务:msk-eureka, msk-config, msk-gateway, msk-provider, msk-consumer
在页面调用一下之前写的消费者接口 http://localhost:8765/consumer/hello
然后打开链路追踪的页面 http://localhost:9411/zipkin ,然后点击查找
,就会出现下面的信息
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1NFmpxke-1590933150580)(assets/1585400645401.png)]
点击下面的第一条链路信息,可以看到这条API经过了哪些服务,以及经过时间等信息
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SFkX9uM8-1590933150582)(assets/1585400993922.png)]
搭建过程非常简单,但是非常强大。
参数说明:
span
: 基本工作单元
Trace
: 一系列Spans组成的树状结构
Annotation
: 用来即时记录一个时间的存在,比如请求的开始于结束
cs
: Client Server,客户端发起一个请求,这个Annotation描述了这个Span的开始
sr
: Server Received,服务端获取请求并开始处理它。sr - cs
得到网络延迟时间ss
: Server Sent 请求处理完成,请求返回客户端。ss - sr
得到服务端处理请求的时间cr
: Client Received 表明Span的结束,客户端成功接收到服务端的回复。cr - cs
得到客户端从服务端获取回复花费的总时间。