微服务架构
现在微服务大行其道,想要对它有更好的把控能力,就需要深入理解其 中的核心概念,微服务中两个比较重要的概念是”微服务“和”微服务架构“,这两者其实有很大的区别,首先,微服务的着重点在于”服务“,任何一个体积足够小、功能足够单一的服务都可以叫做微服务,而微服务架构则是一 种架构模式,它通过一些公共的服务或组件将很多微服务组织起来形成一个整体,对外提供服务,spring cloud正是一个微服务架构的一站式解决方案,其中的组件解决了微服务架构下面临的核心问题。
Eureka
架构
spring cloud eureka是在netflix eurka的基础上进行封装的,作为注册中心提供服务注册与服务发现的功能,eureka集群保证AP,不区分master和slave,每个节点都是独立的服务,彼此之间通过网络复制数据,尽量保证数据的一致性,从理论情况来说,每个节点中的数据在任意时刻都有可能存在差异。
服务注册与服务发现
注册中心维护一个服务列表,结构类似于map,key是服务名称,value是服务信息。服务启动时,向注册中心注册自己的信息,启动完成后,以心跳的方式向注册中心发送服务续约请求,默认如果90秒没有发送心跳续约请求,这个实例就会被eureka server判定为超时,并从服务列表中剔除。
服务消费者在启动时,会从注册中心拉取服务列表并缓存到本地(第一次全量拉取,后续为增量拉取),然后定时对本地缓存进行刷新,当通过服务名称进行调用时,根据服务名称来确定具体的服务提供者的信息并通过http进行远程调用。
自我保护机制
eureka server在运行的过程中会统计注册服务的心跳续约情况,默认如果在15分钟内心跳续约成功的比例不足85%时,就会进入自我保护机制,此时eureka会把注册信息设置成只读状态,续约失败有可能是由于网络抖动引起的,但也可能服务挂掉了,如果是后者,那服务消费者仍然能够调用到宕机的服务,这就会造成业务失败,具体是哪种情况没办法确定,所以在开发消费端服务时,就要做好服务降级,保证业务的正确性。
安全认证
<!-- 让eureka不裸奔 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
@SpringBootConfiguration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http.csrf().disable();
}
}
#eureka server
spring:
application:
name: samsungeshop-eureka-server
#安全认证配置
security:
user:
name: zhangdong
password: zd1991..
server:
port: 7000
eureka:
client:
register-with-eureka: false
fetch-registry: false
registry-fetch-interval-seconds: 30 #eureka客户端缓存的服务列表的刷新频率 30秒
#service-url: http://${eureka.server.username}:${eureka.server.password}@localhost:${server.port}/eureka
instance:
lease-renewal-interval-in-seconds: 30 # 心跳频率
lease-expiration-duration-in-seconds: 90 #最后一次心跳后90秒如果未收到心跳表中该服务以及挂掉
server:
renewal-percent-threshold: 0.85 # 触发自我保护机制的心跳了率
enable-self-preservation: true #开启自我保护机制
#eureka client
eureka:
client:
service-url:
defaultZone: http://localhost:7000/eureka/
源码解析
Eureka Server
一切开始于@EnableEurekaServer
@Import(EurekaServerMarkerConfiguration.class)
public @interface EnableEurekaServer { }
@Configuration
public class EurekaServerMarkerConfiguration {
@Bean
public Marker eurekaServerMarkerBean() {
return new Marker();
}
class Marker {}
}
按照spring boot自动装配原理,会自动加载spring.factories下面的自动装配类里面配置了一个自动装配类,这里就定义了Eureka Server的启动逻辑。
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.netflix.eureka.server.EurekaServerAutoConfiguration
从eureka 架构中了解到,eureka server的主要功能有三个,提供http服务操作注册表、从其他节点复制数据、定时执行超时检查 剔除无效的服务,可以从这三点出发看看源码的实现逻辑。
@Configuration
/**
EurekaServerInitializerConfiguration这个类定义了EurekaServer的启动流程
*/
@Import(EurekaServerInitializerConfiguration.class)
/**
这是一个条件化的Bean,对应了@EnableEurekaServer出似乎还的Marker
其实Marker就是一个开关
*/
@ConditionalOnBean(EurekaServerMarkerConfiguration.Marker.class)
@EnableConfigurationProperties({ EurekaDashboardProperties.class,
InstanceRegistryProperties.class })
@PropertySource("classpath:/eureka/server.properties")
public class EurekaServerAutoConfiguration extends WebMvcConfigurerAdapter { }
再来看EurekaServerAutoConfiguration这个类想向IoC容器中注册了哪些内容,首先是EurekaServer的启动类,它相当于是Eureka服务的一个实例,通过它就可以启动Eureka服务,
@Bean
public EurekaServerBootstrap eurekaServerBootstrap(PeerAwareInstanceRegistry registry,
EurekaServerContext serverContext) {
return new EurekaServerBootstrap(this.applicationInfoManager,
this.eurekaClientConfig, this.eurekaServerConfig, registry,
serverContext);
}
// 这个类定义了与Eureka有关的配置信息
@Configuration
protected static class EurekaServerConfigBeanConfiguration {
@Bean
@ConditionalOnMissingBean
public EurekaServerConfig eurekaServerConfig(EurekaClientConfig clientConfig) {
EurekaServerConfigBean server = new EurekaServerConfigBean();
if (clientConfig.shouldRegisterWithEureka()) {
// Set a sensible default if we are supposed to replicate
server.setRegistrySyncRetries(5);
}
return server;
}
}
总结一下@EnableEurekaServer的主要逻辑:
- 注册用于初始EurekaServer的EurekaServerInitializerConfiguration;
- 注册EurekaServer的web服务实例对象EurekaServerBootstrap;
- 注册于EurekaServer配置相关的EurekaServerConfig;
完成的相关Bean的注册,下面就是对EurekaServer的初始化和启动了
@Configuration
public class EurekaServerInitializerConfiguration
implements ServletContextAware, SmartLifecycle, Ordered {}
这个类实现了SmartLifecycle接口,这是一个Spring 容器接口,IoC容器初始化Bean的时候会调用start()方法,EurekaServer的整个初始化和启动就是在这个start()方法中完成的。
@Override
public void start() {
new Thread(new Runnable() {
@Override
public void run() {
try {
// TODO: is this class even needed now?
//这里是重点
eurekaServerBootstrap.contextInitialized(
EurekaServerInitializerConfiguration.this.servletContext);
log.info("Started Eureka Server");
publish(new EurekaRegistryAvailableEvent(getEurekaServerConfig()));
EurekaServerInitializerConfiguration.this.running = true;
publish(new EurekaServerStartedEvent(getEurekaServerConfig()));
}
catch (Exception ex) {
// Help!
log.error("Could not initialize Eureka servlet context", ex);
}
}
}).start();
}
public void contextInitialized(ServletContext context) {
initEurekaEnvironment();
initEurekaServerContext();
context.setAttribute(EurekaServerContext.class.getName(), this.serverContext)’
}
/**
这个方法很重要,主要做了两件事
1. 从其他节点同步数据
2. 启动无效服务剔除任务
*/
protected void initEurekaServerContext() throws Exception {
// Copy registry from neighboring eureka node
int registryCount = this.registry.syncUp();
this.registry.openForTraffic(this.applicationInfoManager, registryCount);
}
@Override
public void openForTraffic(ApplicationInfoManager applicationInfoManager, int count) {
// Renewals happen every 30 seconds and for a minute it should be a factor of 2.
super.postInit();
}
protected void postInit() {
renewsLastMin.start();
if (evictionTaskRef.get() != null) {
evictionTaskRef.get().cancel();
}
//启动剔除任务
evictionTaskRef.set(new EvictionTask());
evictionTimer.schedule(evictionTaskRef.get(),
serverConfig.getEvictionIntervalTimerInMs(),
serverConfig.getEvictionIntervalTimerInMs());
}
总结一下:
- 从其他节点复制注册信息
- 启动用于剔除无用服务的定时任务
Eureka Client
相对于服务端来说,客户端主要包括服务注册、检查更新和心跳续约这几个功能,Eureka客户端初始化逻辑开始于@EnableEurekaClient
public @interface EnableEurekaClient {}
但是这个注解什么都没做,按照spring boot的逻辑,可以先看看auto config包
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.netflix.eureka.config.EurekaClientConfigServerAutoConfiguration,\
org.springframework.cloud.netflix.eureka.config.EurekaDiscoveryClientConfigServiceAutoConfiguration,\
org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration,\
org.springframework.cloud.netflix.ribbon.eureka.RibbonEurekaAutoConfiguration,\
org.springframework.cloud.netflix.eureka.EurekaDiscoveryClientConfiguration
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
org.springframework.cloud.netflix.eureka.config.EurekaDiscoveryClientConfigServiceBootstrapConfiguration
@Configuration
@EnableConfigurationProperties
@ConditionalOnClass(EurekaClientConfig.class)
@Import(DiscoveryClientOptionalArgsConfiguration.class)
@ConditionalOnBean(EurekaDiscoveryClientConfiguration.Marker.class)
@ConditionalOnProperty(value = "eureka.client.enabled", matchIfMissing = true)
@ConditionalOnDiscoveryEnabled
@AutoConfigureBefore({ NoopDiscoveryClientAutoConfiguration.class,
CommonsClientAutoConfiguration.class, ServiceRegistryAutoConfiguration.class })
@AutoConfigureAfter(name = {
"org.springframework.cloud.autoconfigure.RefreshAutoConfiguration",
"org.springframework.cloud.netflix.eureka.EurekaDiscoveryClientConfiguration",
"org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationAutoConfiguration" })
public class EurekaClientAutoConfiguration {}
这个类中定义了三个前置类和一个Marker,EurekaDiscoveryClientConfiguration这个类向IoC容器中注册了Marker
@Configuration
@EnableConfigurationProperties
@ConditionalOnClass(EurekaClientConfig.class)
@ConditionalOnProperty(value = "eureka.client.enabled", matchIfMissing = true)
@Conditio nabled
public class EurekaDiscoveryClientConfiguration {
@Bean
public Marker eurekaDiscoverClientMarker() {
return new Marker();
}
class Marker {}
}
这个Marker也是一个开关,但没有放到@EnableEurekaClient里面,可能作者觉得只要引入了eureka client的jar包就一定是要开启。回到AutoConfiguration类中
@Bean
@ConditionalOnMissingBean(value = EurekaClientConfig.class, search = SearchStrategy.CURRENT)
public EurekaClientConfigBean eurekaClientConfigBean(ConfigurableEnvironment env) {
EurekaClientConfigBean client = new EurekaClientConfigBean();
if ("bootstrap".equals(this.env.getProperty("spring.config.name"))) {
// We don't register during bootstrap by default, but there will be another
// chance later.
client.setRegisterWithEureka(false);
}
return client;
}
@Bean
public DiscoveryClient discoveryClient(EurekaClient client,
EurekaClientConfig clientConfig) {
return new EurekaDiscoveryClient(client, clientConfig);
}
这两个Bean,一个是配置类,一个是eureka 客户端实例,深入看这个DiscoveryClient发现它在初始化的时候依赖了EurekaClient,而在AutoConfiguration类中就初始化了一个EurekaClient实例对象。
@Bean(destroyMethod = "shutdown")
@ConditionalOnMissingBean(value = EurekaClient.class, search = SearchStrategy.CURRENT)
public EurekaClient eurekaClient(ApplicationInfoManager manager,
EurekaClientConfig config) {
return new CloudEurekaClient(manager, config, this.optionalArgs,
this.context);
}
再深入到这个CloudEurekaClient中
// default size of 2 - 1 each for heartbeat and cacheRefresh
scheduler = Executors.newScheduledThreadPool(2,
new ThreadFactoryBuilder()
.setNameFormat("DiscoveryClient-%d")
.setDaemon(true)
.build());
heartbeatExecutor = new ThreadPoolExecutor(
1, clientConfig.getHeartbeatExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
new ThreadFactoryBuilder()
.setNameFormat("DiscoveryClient-HeartbeatExecutor-%d")
.setDaemon(true)
.build()
); // use direct handoff
cacheRefreshExecutor = new ThreadPoolExecutor(
1, clientConfig.getCacheRefreshExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
new ThreadFactoryBuilder()
.setNameFormat("DiscoveryClient-CacheRefreshExecutor-%d")
.setDaemon(true)
.build()
); // use direct handoff
scheduler.schedule(
new TimedSupervisorTask(
"cacheRefresh",
scheduler,
cacheRefreshExecutor,
registryFetchIntervalSeconds,
TimeUnit.SECONDS,
expBackOffBound,
new CacheRefreshThread()
),
registryFetchIntervalSeconds, TimeUnit.SECONDS);
scheduler.schedule(
new TimedSupervisorTask(
"heartbeat",
scheduler,
heartbeatExecutor,
renewalIntervalInSecs,
TimeUnit.SECONDS,
expBackOffBound,
new HeartbeatThread()
),
renewalIntervalInSecs, TimeUnit.SECONDS);
可以看到,在初始化EurekaClient的时候,内部首先初始化了一个scheduler调度器和两个线程池,分别是处理心跳续约和本地缓存刷新任务,然后用scheduler把这两个任务在这两个线程池中调度起来,总结一下:
- 与EurekaServer建立连接;
- 向注册中心注册自己;
- 初始化心跳续约任务;
- 初始化本地缓存刷新任务;
OpenFein&Ribbon
OpenFeign和Ribbon都是客户端技术,Ribbon的作用有两个,一个是将http接中的ip和端口转换成服务名称,二是提供负载均衡功能,OpenFeign的作用是屏蔽RestTemplate,使得对http接口的调用转换成对Java接口的调用。
配置方式
//添加依赖,Feign内部默认集成了Ribbon
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
#相关配置
feign:
client:
config:
default:
connectTimeout: 5000 #连接超时时间
readTimeout: 1000 #读取数据的超时时间
compression:
request:
enabled: true #开启数据压缩
mime-types: ["text/xml", "application/xml", "application/json"]
min-request-size: 2048 #数据达到该阈值时再进行压缩
response:
enabled: true #对响应数据也开启压缩
//定义Feign接口,接口中描述服务名称和接口名称及参数
@FeignClient(value = "samsungeshop-user")
public interface UserFeign {
@PostMapping("/add")
HttpResult register(@RequestBody UserRegisterVO userRegisterVO);
@PostMapping("/check")
HttpResult login(@RequestParam("username") String userName,
@RequestParam("password") String password);
}
//客户端开启对Feign接口的扫描
@EnableFeignClients(basePackages = {"com.samsung.eshop.api.user"})
public class SsoApplication {}
//向IoC容器中注册RestTemplate
@Bean
@LoadBalanced #开启负载均衡
public RestTemplate restTemplate() {
return new RestTemplate();
}
//在需要的地方进行依赖注入就可以了
@Autowired
private UserFeign userFeign;
负载均衡策略
RoundRobinRule轮询(默认),RandomRule随机,RetryRule重试(先按照轮询策略获取provider,再在超时的时候重试),BestAvaliable最可用策略(选择并发量最小的provider)
service-name:
ribbon:
NFLoadbalancerRuleClassName: com.netflix.loadbalancer.RandomRule
可以通过向IoC容器中注册对应的Rule对象来更换负载均衡策略
@Bean
public IRule loadBalance() {
return new RandomRule();
}
源码分析
在基于spring cloud的微服务架构下的服务之间的调用是通过http完成的,提到http最先想到的可能就是ip和端口了,但是如果使用ip和端口来调用指定的服务,就与服务治理背道而驰,服务治理以注册中心为中心,显然基于ip和端口的调用方式已经脱离了注册中心,很难进行管理,这Ribbon和Feigin的诞生了,Ribbon简单理解它是一个负载均衡器,它把基于ip和端口的服务调用转换成了基于服务名称的调用,这样就与注册中心关联起来了,然后还提供了不同的负载均衡策略,以应对不同的使用场景,但此时在进行服务调用时,依然是基于RestTemplate和url的方式,在使用上依然存在很大的不便性,这就是Feign要解决的问题,它可以简单理解成对RestTemplate的封装,把基于url的调用转换成基于接口的调用,这就更加符合面向对象的设计原则。
Ribbon
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.netflix.ribbon.RibbonAutoConfiguration
@Configuration
@Conditional({RibbonAutoConfiguration.RibbonClassesConditions.class})
@RibbonClients
@AutoConfigureAfter(
//对EurekaClient进行了依赖
name = {"org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration"}
)
//加载了与负载均衡相关的配置
@AutoConfigureBefore({LoadBalancerAutoConfiguration.class, AsyncLoadBalancerAutoConfiguration.class})
@EnableConfigurationProperties({RibbonEagerLoadProperties.class, ServerIntrospectorProperties.class})
public class RibbonAutoConfiguration {}
这里依赖的Eureka主要是用来将服务名称转换成ip和端口的,而LoadBalancerAutoConfiguration这个配置类针对的是负载均衡,这两个东西正好针对了Ribbon的两个核心功能:面向服务的调用和负载均衡。
@Bean
@ConditionalOnMissingBean({LoadBalancerClient.class})
public LoadBalancerClient loadBalancerClient() {
return new RibbonLoadBalancerClient(this.springClientFactory());
}
@Bean
@ConditionalOnClass(name = {"org.springframework.retry.support.RetryTemplate"})
@ConditionalOnMissingBean
public LoadBalancedRetryFactory loadBalancedRetryPolicyFactory(final SpringClientFactory clientFactory) {
return new RibbonLoadBalancedRetryFactory(clientFactory);
}
这里从名字上来看这里初始化的是LoadBalancer的客户端。
下面在看LoadBalancerAutoConfiguration,在这个类中有下面的初始化逻辑。
@Configuration
@ConditionalOnMissingClass({"org.springframework.retry.support.RetryTemplate"})
static class LoadBalancerInterceptorConfig {
LoadBalancerInterceptorConfig() { }
@Bean
public LoadBalancerInterceptor ribbonInterceptor(LoadBalancerClient loadBalancerClient, LoadBalancerRequestFactory requestFactory) {
return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
}
}
这个LoadBalancerInterceptor的主要作用是对RestTemplate进行拦截
public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {
//这里就是在RibbonAutoConfiguration中初始化的LoadBalancerClient实例
private LoadBalancerClient loadBalancer;
private LoadBalancerRequestFactory requestFactory;
public LoadBalancerInterceptor(LoadBalancerClient loadBalancer, LoadBalancerRequestFactory requestFactory) {
this.loadBalancer = loadBalancer;
this.requestFactory = requestFactory;
}
public LoadBalancerInterceptor(LoadBalancerClient loadBalancer) {
this(loadBalancer, new LoadBalancerRequestFactory(loadBalancer));
}
//对请求信息拦截
public ClientHttpResponse intercept(final HttpRequest request, final byte[] body, final ClientHttpRequestExecution execution) throws IOException {
URI originalUri = request.getURI();
String serviceName = originalUri.getHost();
Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
return (ClientHttpResponse)this.loadBalancer.execute(serviceName, this.requestFactory.createRequest(request, body, execution));
}
}
这里是负载均衡的核心,从url中获取服务名称,然后把请求转交给负载均衡器LoadBalancer
public <T> T execute(String serviceId, LoadBalancerRequest<T> request, Object hint) throws IOException {
ILoadBalancer loadBalancer = this.getLoadBalancer(serviceId);
Server server = this.getServer(loadBalancer, hint);
if (server == null) {
throw new IllegalStateException("No instances available for " + serviceId);
} else {
RibbonLoadBalancerClient.RibbonServer ribbonServer = new RibbonLoadBalancerClient.RibbonServer(serviceId, server, this.isSecure(server, serviceId), this.serverIntrospector(serviceId).getMetadata(server));
return this.execute(serviceId, (ServiceInstance)ribbonServer, (LoadBalancerRequest)request);
}
}
这里是根据服务ID获取负载均衡器实例,然后选择一个服务提供者进行调用
public Server chooseServer(Object key) {
if (this.counter == null) {
this.counter = this.createCounter();
}
this.counter.increment();
if (this.rule == null) {
return null;
} else {
try {
//这里根据配置的负载均衡策略选择一个服务提供者,
return this.rule.choose(key);
} catch (Exception var3) {
logger.warn("LoadBalancer [{}]: Error choosing server for key {}", new Object[]{this.name, key, var3});
return null;
}
}
}
简单总结一下:按照spring boot自动装配逻辑加载自动装配类,在自动装配类中初始化LoaderBalancerClient实例,然后加载用于初始化LoaderBalancer的配置类,这里主要初始化了LoadBalancerInterceptor,它主要是对RestTemplate的调用进行拦截,解析出服务名称,然后用配置的负载均衡策略选择一个服务提供者进行调用。
Feign
Reigin的主要作用是把对RestTemplate的调用转换成对接口的调用,那么猜测是通过动态代理实现的,从@EnableFeignClient开始。
@EnableFeignClients(basePackages = {"com.samsung.eshop.api.user"})
@Import({FeignClientsRegistrar.class})
public @interface EnableFeignClients {
String[] value() default {};
String[] basePackages() default {};
Class<?>[] basePackageClasses() default {};
Class<?>[] defaultConfiguration() default {};
Class<?>[] clients() default {};
}
class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware { }
这个FeignClientsRegistrar实现了ImportBeanDefinitionRegistrar接口,能够向IoC容器中注册BeanDefinition。
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
this.registerDefaultConfiguration(metadata, registry);
this.registerFeignClients(metadata, registry);
}
public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
this.registerFeignClient(registry, annotationMetadata, attributes);
}
private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class);
this.validate(attributes);
definition.addPropertyValue("url", this.getUrl(attributes));
definition.addPropertyValue("path", this.getPath(attributes));
String name = this.getName(attributes);
definition.addPropertyValue("name", name);
String contextId = this.getContextId(attributes);
definition.addPropertyValue("contextId", contextId);
definition.addPropertyValue("type", className);
definition.addPropertyValue("decode404", attributes.get("decode404"));
definition.addPropertyValue("fallback", attributes.get("fallback"));
definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory"));
}
一致跟下来会看到这个方法,实际上就是解析了FeignClient标注的接口,然后把它放入一个BeanDefinition中,为BeanDefinition设置了FactoryBean,当对这个Feign接口进行依赖注入的时候就会调用这个FactoryBean的getObject()方法获取实例对象。
public Object getObject() throws Exception {
return this.getTarget();
}
<T> T getTarget() {
FeignContext context = (FeignContext)this.applicationContext.getBean(FeignContext.class);
Builder builder = this.feign(context);
if (!StringUtils.hasText(this.url)) {
if (!this.name.startsWith("http")) {
this.url = "http://" + this.name;
} else {
this.url = this.name;
}
this.url = this.url + this.cleanPath();
return this.loadBalance(builder, context, new HardCodedTarget(this.type, this.name, this.url));
} else {
if (StringUtils.hasText(this.url) && !this.url.startsWith("http")) {
this.url = "http://" + this.url;
}
String url = this.url + this.cleanPath();
Client client = (Client)this.getOptional(context, Client.class);
if (client != null) {
if (client instanceof LoadBalancerFeignClient) {
client = ((LoadBalancerFeignClient)client).getDelegate();
}
builder.client(client);
}
Targeter targeter = (Targeter)this.get(context, Targeter.class);
return targeter.target(this, builder, context, new HardCodedTarget(this.type, this.name, url));
}
}
public <T> T target(FeignClientFactoryBean factory, Builder feign, FeignContext context, HardCodedTarget<T> target) {
return feign.target(target);
}
public <T> T target(Target<T> target) {
return this.build().newInstance(target);
}
public Feign build() {
Factory synchronousMethodHandlerFactory = new Factory(this.client, this.retryer, this.requestInterceptors, this.logger, this.logLevel, this.decode404, this.closeAfterDecode, this.propagationPolicy);
ParseHandlersByName handlersByName = new ParseHandlersByName(this.contract, this.options, this.encoder, this.decoder, this.queryMapEncoder, this.errorDecoder, synchronousMethodHandlerFactory);
return new ReflectiveFeign(handlersByName, this.invocationHandlerFactory, this.queryMapEncoder);
}
这里涉及到一个invocationHandlerFactory,返回了FeignInvocationHandler,从名字上看,是应用到了jdk动态代理
public <T> T newInstance(Target<T> target) {
Map<String, MethodHandler> nameToHandler = this.targetToHandlersByName.apply(target);
Map<Method, MethodHandler> methodToHandler = new LinkedHashMap();
List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList();
Method[] var5 = target.type().getMethods();
int var6 = var5.length;
for(int var7 = 0; var7 < var6; ++var7) {
Method method = var5[var7];
if (method.getDeclaringClass() != Object.class) {
if (Util.isDefault(method)) {
DefaultMethodHandler handler = new DefaultMethodHandler(method);
defaultMethodHandlers.add(handler);
methodToHandler.put(method, handler);
} else {
methodToHandler.put(method, (MethodHandler)nameToHandler.get(Feign.configKey(target.type(), method)));
}
}
}
InvocationHandler handler = this.factory.create(target, methodToHandler);
T proxy = Proxy.newProxyInstance(target.type().getClassLoader(), new Class[]{target.type()}, handler);
Iterator var12 = defaultMethodHandlers.iterator();
while(var12.hasNext()) {
DefaultMethodHandler defaultMethodHandler = (DefaultMethodHandler)var12.next();
defaultMethodHandler.bindTo(proxy);
}
return proxy;
}
最终的InvocationHandler类型是FeignInvocationHandler,总结起来,Feign的原理就是通过动态代理把队接口的调用转换成了对RestTemplate的调用。
Hystrix
服务熔断与服务降级、预熔断、即时熔断。
hystrix的最核心功能是服务熔断,熔断能够保证服务的最佳负载或最大吞吐量,另外Hystrix还提供了服务降级功能,在熔断后返回缓存结果或缓存结果。
处理过程
在处理请求的时候,首先判断熔断器是否开启,如果开启了,就直接走服务降级的逻辑,通过fallbackMethod或fallbackFactory返回降级结果,如果没有,则检查信号量或线程池是否超出限制,如果是线程池隔离策略,则用新的线程来调用服务提供者,如果是信号量隔离策略,则使用当前用户线程调用服务提供者,如果调用失败或者超时则直接走降级落,否则直接将结果返回给用户。
降级策略
服务降级的埋点:
fallback method
fallbackMethod是一种针对某个接口方法的降级策略,作用范围比较小,通过@HystrixCommand来指定
fallback factory
fallbackFactory是针对某个接口的全局降级,作用范围比fallbackMethod要大,这种方式一般与Feign一起使用,在FeignClient中指定fallbackFactory类。
隔离策略
线程隔离
线程隔离策略可以简单理解为用户请求线程和服务调用线程是分开的,也就是Hystrix会在线程池内部启动一个新的线程来进行服务调用,然后用Future来持有这个线程。
信号量隔离
信号量隔离策略并没有把用户请求线程和服务调用线程分开,只是简单的对信号量(并发度)进行了判断,并且这种隔离策略不支持超时时间的设置,也不支持异步调用,很明显线程隔离策略在性能上更有优势,多数情况下也是使用线程隔离策略。
滑动窗口
hystrix是以滑动窗口为单位来统计的,默认一个滑动窗口的时长为10s,滑动窗口被分为多个bucktet,hystrix针对每个bucket进行数据采样统计,然后再计算整个滑动窗口的指标数据,最后在根据配置的阈值来判断是否需要开启熔断器。
#配置隔离策略
hystrix.command.default.execution.isolation.strategy=thread/semaphore
#信号量隔离级别下的信号量个数h
ystrix.command.default.execution.isolation.semaphore.maxConcurrentRequests=10
#线程隔离级别下开启调用超时功能h
ystrix.command.default.execution.timeout.enabled=true/false
#指定服务调用线程执行的超时时间
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=
#是否开启中断,也就是当服务调用线程执行超时后,是否还让它继续执行,true=中断其执行
hystrix.command.default.execution.isolation.thread.interruptOnTimeout=true/false
#当服务调用线程被主动取消时,是否中断其执行
hystrix.command.default.execution.isolation.thread.interruptOnCancel=true/false
#是否开启熔断器
hystrix.command.default.circuitBreaker.enabled=true/false
#当滑动窗口内的请求失败率达到指定的阈值时开启熔断器
hystrix.command.default.circuitBreaker.errorThresholdPercentage=
#滑动窗口触发熔断的最小请求数
hystrix.command.default.circuitBreaker.requestVolumeThreshold=n
#当熔断器开启后,在指定的时间后会尝试检查相关的指标数据,如果符合配置,则关闭熔断器
hystrix.command.default.circuitBreaker.sleepWindowInMilliseconds=
Zuul
zuul是spring cloud体系中的网关服务,主要提供服务路由和过滤两个功能。
配置
//开启zuul服务
@EnableZuulProxy
spring:
application:
name: samsungeshop-gateway
server:
port: 9000
servlet:
context-path: /api
zuul:
#排除用户登录和注册接口,只通过sso服务进行处理
ignored-patterns: /user/login,/user/register/
retryable: true
routes:
samsungeshop-sso:
path: /auth/**
samsungeshop-search:
path: /search/**
samsungeshop-user:
path: /user/**
samsungeshop-item:
path: /item/**
samsungeshop-cart:
path: /cart/**
sensitive-headers:
eureka:
client:
service-url:
defaultZone: http://localhost:7000/eureka/
请求过滤
@Slf4j
@Component
public class AuthFilter extends ZuulFilter {
private static final String SSO_URI_PREFIX = "/api/auth";
@Autowired
private AuthFeign authFeign;
//过滤类型
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
HttpServletRequest request = RequestContext.getCurrentContext().getRequest();
String requestUrl = request.getRequestURI();
//log.info("Auth Filter : {}", requestUrl);
//PreDecorationFilter
//RibbonRoutingFilter
//ZuulServlet
if (requestUrl.startsWith(SSO_URI_PREFIX)) {
log.info("Auth Filter, sso request : {}", requestUrl);
return false;
}
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext requestContext = RequestContext.getCurrentContext();
HttpServletRequest request = requestContext.getRequest();
log.info("start to filter {}", request.getRequestURI());
/**
* 对token和jwt进行校验
* */
String token = getTokenFromCookie(request);
String authorization = getJwtFromHeader(request);
if (StringUtils.isBlank(token) || StringUtils.isBlank(authorization)) {
accessDeined();
}
log.info("auth filter : token = {}, jwt = {}", token, authorization);
authorization = AESUtils.decrypt(authorization);
HttpResult checkResult = authFeign.checkTokenAndJwt(token.trim(), authorization.trim());
log.info("auth filter : token and jwt check result {}", checkResult.isSuccess());
if (!checkResult.isSuccess()) {
accessDeined();
}
HttpResult<String> getUserNameResult = authFeign.getUsernameFromToken(authorization);
if (getUserNameResult.isSuccess()) {
requestContext.addZuulRequestHeader("User", getUserNameResult.getData());
}
return null;
}
private String getTokenFromCookie(HttpServletRequest request) {
Cookie[] cookies = request.getCookies();
String tokenValue = null;
for (Cookie cookie : cookies) {
if ("token".equals(cookie.getName())) {
tokenValue = cookie.getValue();
break;
}
}
return tokenValue;
}
private String getJwtFromHeader(HttpServletRequest request) {
String authorizationValue = request.getHeader("Auth");
return authorizationValue;
}
public void accessDeined() {
RequestContext requestContext = RequestContext.getCurrentContext();
HttpServletResponse response = requestContext.getResponse();
requestContext.setSendZuulResponse(false);
requestContext.setResponseStatusCode(200);
String jsonString = JSON.toJSONString( HttpResult.error("此操作需要登陆系统"));
requestContext.setResponseBody(jsonString);
response.setContentType("application/json;charset=utf-8");
}
}
限流
通过guava ratelimiter + zuul filter实现令牌桶简易限流
通过spring cloud zuul rate limiter实现多维限流
源码分析
按照spring cloud的风格,从@EnableZuulProxy开始
@Import({ZuulProxyMarkerConfiguration.class})
public @interface EnableZuulProxy {}
@Configuration
public class ZuulProxyMarkerConfiguration {
@Bean
public ZuulProxyMarkerConfiguration.Marker zuulProxyMarkerBean() {
return new ZuulProxyMarkerConfiguration.Marker();
}
//定义了一个Marker开关,并注册到IoC容器中
class Marker {
Marker() {
}
}
}
再来看zuul 的spring.factories,里面有一个ZuulProxyAutoProxyConfiguration
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.netflix.zuul.ZuulServerAutoConfiguration,\
org.springframework.cloud.netflix.zuul.ZuulProxyAutoConfiguration
@Configuration
@Import({RestClientRibbonConfiguration.class, OkHttpRibbonConfiguration.class, HttpClientRibbonConfiguration.class, HttpClientConfiguration.class})
@ConditionalOnBean({Marker.class})
public class ZuulProxyAutoConfiguration extends ZuulServerAutoConfiguration {}
这里对前面注册的Marker进行了判断。
public ServletRegistrationBean zuulServlet() {
ServletRegistrationBean<ZuulServlet> servlet = new ServletRegistrationBean(new ZuulServlet(), new String[]{this.zuulProperties.getServletPattern()});
servlet.addInitParameter("buffer-requests", "false");
return servlet;
}
在初始化的过程中,创建了一个ZuulServlet,也就是说,Zuul服务实际上就是一个HttpServlet,在service方法中定义了ZuulFilter完整的生命周期。
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
try {
this.init((HttpServletRequest)servletRequest, (HttpServletResponse)servletResponse);
RequestContext context = RequestContext.getCurrentContext();
context.setZuulEngineRan();
try {
this.preRoute();
} catch (ZuulException var12) {
this.error(var12);
this.postRoute();
return;
}
try {
this.route();
} catch (ZuulException var13) {
this.error(var13);
this.postRoute();
return;
}
try {
this.postRoute();
} catch (ZuulException var11) {
this.error(var11);
}
} catch (Throwable var14) {
this.error(new ZuulException(var14, 500, "UNHANDLED_EXCEPTION_" + var14.getClass().getName()));
} finally {
RequestContext.getCurrentContext().unset();
}
}
Zuul的另一个功能是服务路由,实际上这个路由功能是通过一个内置的Filter类型PreDecorationFilter实现的
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
String requestURI = this.urlPathHelper.getPathWithinApplication(ctx.getRequest());
Route route = this.routeLocator.getMatchingRoute(requestURI);
String location;
if (route != null) {
location = route.getLocation();
if (location != null) {
ctx.put("requestURI", route.getPath());
ctx.put("proxy", route.getId());
if (!route.isCustomSensitiveHeaders()) {
this.proxyRequestHelper.addIgnoredHeaders((String[])this.properties.getSensitiveHeaders().toArray(new String[0]));
} else {
this.proxyRequestHelper.addIgnoredHeaders((String[])route.getSensitiveHeaders().toArray(new String[0]));
}
if (route.getRetryable() != null) {
ctx.put("retryable", route.getRetryable());
}
if (!location.startsWith("http:") && !location.startsWith("https:")) {
if (location.startsWith("forward:")) {
ctx.set("forward.to", StringUtils.cleanPath(location.substring("forward:".length()) + route.getPath()));
ctx.setRouteHost((URL)null);
return null;
}
ctx.set("serviceId", location);
ctx.setRouteHost((URL)null);
ctx.addOriginResponseHeader("X-Zuul-ServiceId", location);
} else {
ctx.setRouteHost(this.getUrl(location));
ctx.addOriginResponseHeader("X-Zuul-Service", location);
}
if (this.properties.isAddProxyHeaders()) {
this.addProxyHeaders(ctx, route);
String xforwardedfor = ctx.getRequest().getHeader("X-Forwarded-For");
String remoteAddr = ctx.getRequest().getRemoteAddr();
if (xforwardedfor == null) {
xforwardedfor = remoteAddr;
} else if (!xforwardedfor.contains(remoteAddr)) {
xforwardedfor = xforwardedfor + ", " + remoteAddr;
}
ctx.addZuulRequestHeader("X-Forwarded-For", xforwardedfor);
}
if (this.properties.isAddHostHeader()) {
ctx.addZuulRequestHeader("Host", this.toHostHeader(ctx.getRequest()));
}
}
} else {
log.warn("No route found for uri: " + requestURI);
location = this.getForwardUri(requestURI);
ctx.set("forward.to", location);
}
return null;
}
在他的run方法中,匹配url,然后将请求url中的域名替换成了配置文件中的服务名称,最终通过Ribbon对后端服务进行调用,而把请求转成Ribbon的过程是在另外一个Filter中完成的RibbonRoutingFilter
public Object run() {
RequestContext context = RequestContext.getCurrentContext();
this.helper.addIgnoredHeaders(new String[0]);
//这里出现了Ribbon
RibbonCommandContext commandContext = this.buildCommandContext(context);
ClientHttpResponse response = this.forward(commandContext);
this.setResponse(response);
return response;
}
总结一下:@EnableZuulProxy会创建一个ZuulServlet主导到Servlet容器中,这个ZuulServlet在service方法中定义ZuulFilter完整的生命周期,然后Zuul内部提供了两个Filter,一个Filter的作用是把url中的域名替换成服务名称,另外一个Filter的作用是把请求交给Ribbon处理,实现面向服务调用。