Spring Cloud源码分析之eureka+feign远程调用
创始人
2024-04-03 13:04:04
0

是什么

Eureka是一个REST (Representational State Transfer)服务,用于定位服务,以实现中间层服务器的负载平衡和故障转移,我们称此服务为Eureka服务器。Eureka还有一个基于java的客户端组件,Eureka客户端,这使得与服务的交互更加容易,同时客户端也有一个内置的负载平衡器,它执行基本的循环负载均衡。

Feign 是一种声明式服务调用组件,它在 RestTemplate 的基础上做了进一步的封装。通过 Feign,我们只需要声明一个接口并通过注解进行简单的配置(类似于 Dao 接口上面的 Mapper 注解一样)即可实现对 HTTP 接口的绑定。

通过 Feign,我们可以像调用本地方法一样来调用远程服务,而完全感觉不到这是在进行远程调用。

为什么

eureka解决了什么问题?为什么要用eureka?

eureka为分布式环境引入了服务注册与发现的能力,如果没有服务注册与发现,我们便需要手动去维护一大堆服务的地址信息,而当服务的状态变化发生变化,比如有新的服务加入或某些现有服务突然不可用时,现存的服务也无法感知到并及时切换到可用的服务上去

另外,feign屏蔽了请求构建、发送、重试、负载均衡等相关细节,让我们能够从重复的工作中解放出来,更专注于业务本身。

架构图

在这里插入图片描述
在这里插入图片描述

案例

eureka server

引入依赖

implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-server'

启动类

@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApplication {public static void main(String[] args) {SpringApplication.run(EurekaServerApplication.class, args);}
}

配置文件

server.port=8761
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false

service consumer

依赖

org.springframework.bootspring-boot-starter-web

org.springframework.bootspring-boot-starter-actuator

org.springframework.cloudspring-cloud-starter-openfeign

org.springframework.cloudspring-cloud-starter-netflix-eureka-client

启动类

@SpringBootApplication
@EnableDiscoveryClient
@RestController
@EnableFeignClients
public class HelloClientApplication {@AutowiredHelloClient client;@RequestMapping("/")public String hello() {return client.hello();}public static void main(String[] args) {SpringApplication.run(HelloClientApplication.class, args);}@FeignClient("HelloServer")interface HelloClient {@RequestMapping(value = "/", method = GET)String hello();}
}

配置文件

spring:application:name: HelloClientserver:port: 7211eureka:password: passwordclient:serviceUrl:defaultZone: http://user:${eureka.password}@localhost:8761/eureka/instance:leaseRenewalIntervalInSeconds: 10metadataMap:instanceId: ${vcap.application.instance_id:${spring.application.name}:${spring.application.instance_id:${server.port}}}endpoints:restart:enabled: true

service provider

依赖

org.springframework.bootspring-boot-starter-web

org.springframework.bootspring-boot-starter-actuator

org.springframework.cloudspring-cloud-starter-openfeign

org.springframework.cloudspring-cloud-starter-netflix-eureka-client

启动类

@SpringBootApplication
@EnableDiscoveryClient
@RestController
public class HelloServerApplication {@AutowiredDiscoveryClient client;@RequestMapping("/")public String hello() {List instances = client.getInstances("HelloServer");ServiceInstance selectedInstance = instances.get(new Random().nextInt(instances.size()));return "Hello World: " + selectedInstance.getServiceId() + ":" + selectedInstance.getHost() + ":" + selectedInstance.getPort();}public static void main(String[] args) {SpringApplication.run(HelloServerApplication.class, args);}
}

配置文件

spring:application:name: HelloServerserver:port: 7111eureka:password: passwordclient:serviceUrl:defaultZone: http://user:${eureka.password}@localhost:8761/eureka/instance:leaseRenewalIntervalInSeconds: 10metadataMap:instanceId: ${vcap.application.instance_id:${spring.application.name}:${spring.application.instance_id:${server.port}}}

运行

依次启动eureka server、service provider、service consumer。访问http://localhost:7211/

如果在service provider的hello方法里打上断点,就可以发现请求由从service consumer的hello方法进到了service provider的hello方法。

而且,这里并没有在service consumer里配置service provider的地址信息,只是在service consumer里调用了HelloClient接口里的hello方法,便发出了一个到service provider的http请求。

提出问题

  1. 服务消费者怎么调用服务提供者
  2. 服务消费者如何拿到服务提供者的注册信息
  3. 有多个服务提供者的情况下,服务消费者如何判断访问哪一个
  4. 什么时候往eureka server注册
  5. eureka server怎么判断服务是否可用
  6. 有新服务上线或者服务地址发生变化怎么办
  7. 网络超时怎么办

初步分析

service consumerHttpClient接口很简单,只有两个注解和一个接口定义,没有任何Http相关的细节。

而访问HttpClient的方式是通过容器里的bean来实现的,自然而然可以想到这里一定用到了代理。而FeignClient注解大概率就是代理的Pointcut,代理内部就是网络访问的细节。

像这种开箱即用的组件,按之前文章的分析经验来看,Spring的套路基本都是通过XXXAutoConfiguration以及EnableXXX注解来实现的。

可以看到在service consumer的启动类上有EnableDiscoveryClientEnableFeignClients两个注解。

然后搜一搜eurekaClientAutoConfiguration

源码分析

Eureka

先看整体流程图

在这里插入图片描述

EurekaClient初始化

public class EurekaClientAutoConfiguration {//默认启用这个配置类,除非主动将eureka.client.refresh.enable设置为falseprotected static class RefreshableEurekaClientConfiguration {@Autowiredprivate ApplicationContext context;@Autowiredprivate AbstractDiscoveryClientOptionalArgs optionalArgs;public EurekaClient eurekaClient(ApplicationInfoManager manager, EurekaClientConfig config,EurekaInstanceConfig instance, @Autowired(required = false) HealthCheckHandler healthCheckHandler) {//创建eurekaclient,注册register,renew,refresh任务到线程池CloudEurekaClient cloudEurekaClient = new CloudEurekaClient(appManager, config, this.optionalArgs,this.context);cloudEurekaClient.registerHealthCheck(healthCheckHandler);return cloudEurekaClient;}}
}

获取注册信息并注册定时任务

public class DiscoveryClient implements EurekaClient {DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args,Provider backupRegistryProvider, EndpointRandomizer endpointRandomizer) {//获取注册信息并保存boolean primaryFetchRegistryResult = fetchRegistry(false);if (clientConfig.shouldRegisterWithEureka() && clientConfig.shouldEnforceRegistrationAtInit()) {try {//注册if (!register() ) {throw new IllegalStateException("Registration error at startup. Invalid server response.");}} catch (Throwable th) {logger.error("Registration error at startup: {}", th.getMessage());throw new IllegalStateException(th);}}//初始化用于refresh跟renew的线程池//线程数为2,保证了refresh跟renew的单线程运行scheduler = Executors.newScheduledThreadPool(2,new ThreadFactoryBuilder().setNameFormat("DiscoveryClient-%d").setDaemon(true).build());//注册定时任务initScheduledTasks();}private void initScheduledTasks() {//检查eureka.client.fetch-registry配置项的值,默认为trueif (clientConfig.shouldFetchRegistry()) {//任务执行间隔,默认30s。配置项:eureka.client.registryFetchIntervalSecondsint registryFetchIntervalSeconds = clientConfig.getRegistryFetchIntervalSeconds();int expBackOffBound = clientConfig.getCacheRefreshExecutorExponentialBackOffBound();//创建任务,内部会执行CacheRefreshThread#runcacheRefreshTask = new TimedSupervisorTask("cacheRefresh",scheduler,cacheRefreshExecutor,registryFetchIntervalSeconds,TimeUnit.SECONDS,expBackOffBound,new CacheRefreshThread());// 注册refresh任务,负责刷新从eureka server获取的注册信息scheduler.schedule(cacheRefreshTask,registryFetchIntervalSeconds, TimeUnit.SECONDS);}//eureka.client.register-with-eurek 默认为trueif (clientConfig.shouldRegisterWithEureka()) {//间隔,默认10sint renewalIntervalInSecs = instanceInfo.getLeaseInfo().getRenewalIntervalInSecs();int expBackOffBound = clientConfig.getHeartbeatExecutorExponentialBackOffBound();logger.info("Starting heartbeat executor: " + "renew interval is: {}", renewalIntervalInSecs);heartbeatTask = new TimedSupervisorTask("heartbeat",scheduler,heartbeatExecutor,renewalIntervalInSecs,TimeUnit.SECONDS,expBackOffBound,new HeartbeatThread());//注册renew/heartbeatTaskscheduler.schedule(heartbeatTask,renewalIntervalInSecs, TimeUnit.SECONDS);// 周期性检查当前服务的状态信息,比如hostname变化,或者改变InstanceStatus或者renew失败。// 一旦状态发生变化,会重新将自己注册到eureka serverinstanceInfoReplicator = new InstanceInfoReplicator(this,instanceInfo,clientConfig.getInstanceInfoReplicationIntervalSeconds(),2); // burstSizestatusChangeListener = new ApplicationInfoManager.StatusChangeListener() {@Overridepublic String getId() {return "statusChangeListener";}@Overridepublic void notify(StatusChangeEvent statusChangeEvent) {logger.info("Saw local status change event {}", statusChangeEvent);instanceInfoReplicator.onDemandUpdate();}};//注册状态监听器,好处是状态发生变更时可能比周期性更早的感知到变化//参考:ApplicationInfoManager#setInstanceStatusif (clientConfig.shouldOnDemandUpdateStatusChange()) {applicationInfoManager.registerStatusChangeListener(statusChangeListener);}instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());}}
}

refresh任务: CacheRefreshThread

class CacheRefreshThread implements Runnable {void refreshRegistry() {try {boolean isFetchingRemoteRegionRegistries = isFetchingRemoteRegionRegistries();boolean remoteRegionsModified = false;// 判断region是否发生变化// 比如突然某个机房的服务出问题了,需要切到另一个机房。修改fetchRemoteRegionsRegistry配置项String latestRemoteRegions = clientConfig.fetchRegistryForRemoteRegions();if (null != latestRemoteRegions) {String currentRemoteRegions = remoteRegionsToFetch.get();if (!latestRemoteRegions.equals(currentRemoteRegions)) {synchronized (instanceRegionChecker.getAzToRegionMapper()) {if (remoteRegionsToFetch.compareAndSet(currentRemoteRegions, latestRemoteRegions)) {//更新regionString[] remoteRegions = latestRemoteRegions.split(",");remoteRegionsRef.set(remoteRegions);instanceRegionChecker.getAzToRegionMapper().setRegionsToFetch(remoteRegions);//保存region变化状态remoteRegionsModified = true;} else {logger.info("Remote regions to fetch modified concurrently," +" ignoring change from {} to {}", currentRemoteRegions, latestRemoteRegions);}}}}//根据配置的url从eureka server拉取信息//如果region发生变化,则拉取注册信息后进行全量覆盖。否则根据applicationName更新//参考:DiscoveryClient#updateDelta -> applications.getRegisteredApplications(instance.getAppName()).addInstance(instance);boolean success = fetchRegistry(remoteRegionsModified);} catch (Throwable e) {logger.error("Cannot fetch registry from server", e);}}
}

renew

public class DiscoveryClient implements EurekaClient {boolean renew() {EurekaHttpResponse httpResponse;try {//将当前实例的Id等信息发至eurekahttpResponse = eurekaTransport.registrationClient.sendHeartBeat(instanceInfo.getAppName(), instanceInfo.getId(), instanceInfo, null);//如果status时404if (httpResponse.getStatusCode() == Status.NOT_FOUND.getStatusCode()) {//将dirty设置为truelong timestamp = instanceInfo.setIsDirtyWithTime();boolean success = register();if (success) {//如果成功,则将dirty设置为false。//如果失败,InstanceInfoReplicator在扫描到dirty为false后会重新注册instanceInfo.unsetIsDirty(timestamp);}return success;}return httpResponse.getStatusCode() == Status.OK.getStatusCode();} catch (Throwable e) {logger.error(PREFIX + "{} - was unable to send heartbeat!", appPathIdentifier, e);return false;}}
}

Feign

核心组件关系概览

LoadBalancerAutoConfiguration 引入LoadBalancerAutoConfiguration

LoadBalancerAutoConfiguration引入LoadBalancerClientConfiguration

LoadBalancerClientConfiguration引入ServiceInstanceListSupplier

ServiceInstanceListSupplierBeanFactory获取DiscoveryClient

DiscoveryClient持有注册到eureka server的服务的信息

----

LoadBalancerClientConfiguration引入RoundRobinLoadBalancer

RoundRobinLoadBalancerBeanFactory获取ServiceInstanceListSupplier

----

DefaultFeignLoadBalancerConfiguration 引入FeignBlockingLoadBalancerClient

FeignBlockingLoadBalancerClient从容器中获取RoundRobinLoadBalancer

----

EnableFeignClients 引入FeignClientsRegistrar

FeignClientsRegistrar 为所有标注了FeignClient的类生成BeanDefinition

FeignClientsRegistrar 为生成的BeanDefinition注册callback用于生成代理对象

----

FeignClientsRegistrar注册的callback:创建SynchronousMethodHandler ,在其中注入FeignBlockingLoadBalancerClient,然后将SynchronousMethodHandler包装成InvocationHandler并注入代理对象

调用关系概览

访问代理对象->SynchronousMethodHandler->FeignBlockingLoadBalancerClient->RoundRobinLoadBalancer->DiscoveryClient获取服务提供者信息→RoundRobinLoadBalancer轮询选择服务提供者→调用目标服务

流程图

初始化

在这里插入图片描述
在这里插入图片描述

远程调用

在这里插入图片描述

EnableFeignClients

点进EnableFeignClients. 可以看到它import了FeignClientsRegistrar

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {...
}

扫描FeignClient

FeignClientsRegistrar主要做了两件事

  1. 扫描basePackage下带FeignClient注解的类
  2. 注册工厂方法,用于为第一步扫描出来的类生成代理对象
public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {LinkedHashSet candidateComponents = new LinkedHashSet<>();//指定注解过滤器,过滤的注解为FeignClientscanner.addIncludeFilter(new AnnotationTypeFilter(FeignClient.class));Set basePackages = getBasePackages(metadata);for (String basePackage : basePackages) {//扫描basePackage下带FeignClient注解的类并包装成beanDefinitioncandidateComponents.addAll(scanner.findCandidateComponents(basePackage));}for (BeanDefinition candidateComponent : candidateComponents) {if (candidateComponent instanceof AnnotatedBeanDefinition) {// verify annotated class is an interfaceAnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();Map attributes = annotationMetadata.getAnnotationAttributes(FeignClient.class.getCanonicalName());String name = getClientName(attributes);registerClientConfiguration(registry, name, attributes.get("configuration"));registerFeignClient(registry, annotationMetadata, attributes);}}}

注册beanDefinition以及callback

factoryBean.getObject(); 生成代理对象。通过将SynchronousMethodHandler包装成InvocationHandler植入目标对象来完成。而SynchronousMethodHandler里负责http调用的细节。

private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata,Map attributes) {String className = annotationMetadata.getClassName();Class clazz = ClassUtils.resolveClassName(className, null);FeignClientFactoryBean factoryBean = new FeignClientFactoryBean();factoryBean.setType(clazz);//注册一个回调来生成类型为clazz的bean//往beanDefinition里放入一个Supplier的实例,Spring会优先通过Supplier来创建目标beanBeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(clazz, () -> {//返回代理对象return factoryBean.getObject();});BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, qualifiers);//注册beanDefinitionBeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);}

拦截器

创建SynchronousMethodHandler.Factory

public abstract class Feign {public  T target(Target target) {return build().newInstance(target);}public  T target(Target target) {return build().newInstance(target);}public Feign build() {//创建SynchronousMethodHandler的factorySynchronousMethodHandler.Factory synchronousMethodHandlerFactory =new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger,logLevel, decode404, closeAfterDecode, propagationPolicy, forceDecoding);ParseHandlersByName handlersByName =new ParseHandlersByName(contract, options, encoder, decoder, queryMapEncoder,errorDecoder, synchronousMethodHandlerFactory);return new ReflectiveFeign(handlersByName, invocationHandlerFactory, queryMapEncoder);}
}

生成代理对象

public class ReflectiveFeign extends Feign {public  T newInstance(Target target) {//通过SynchronousMethodHandler.Factory的create方法创建SynchronousMethodHandlerMap nameToHandler = targetToHandlersByName.apply(target);Map methodToHandler = new LinkedHashMap();methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));//通过SynchronousMethodHandler创建FeignInvocationHandlerInvocationHandler handler = factory.create(target, methodToHandler);//生成代理对象T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(),new Class[] {target.type()}, handler);return proxy;}
}

InvocationHandler

InvocationHandler的生成与执行

static class FeignInvocationHandler implements InvocationHandler {private final Target target;private final Map dispatch;FeignInvocationHandler(Target target, Map dispatch) {//SynchronousMethodHandler是MethodHandler的子类//method是目标方法this.target = checkNotNull(target, "target");this.dispatch = checkNotNull(dispatch, "dispatch for %s", target);}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {//根据目标方法拿到MethodHandler并调用其invoke方法return dispatch.get(method).invoke(args);}
}

重试策略

SynchronousMethodHandler,http调用入口以及重试策略

final class SynchronousMethodHandler implements MethodHandler {@Overridepublic Object invoke(Object[] argv) throws Throwable {RequestTemplate template = buildTemplateFromArgs.create(argv);Retryer retryer = this.retryer.clone();while (true) {try {//执行http requestreturn executeAndDecode(template, options);} catch (RetryableException e) {//IO异常//服务提供者返回服务端错误(status>300&status!=404),并且response header里有Retry-Aftertry {//检查是否重试,重试次数达到retryer.maxAttempts就会抛出RetryableException//重试间隔:如果是服务端错误,则使用Retry-After指定的值,但是不能超过maxPeriod//如果是IO异常,则每次的间隔延长1.5倍//如果符合重试要求,则不抛出异常//服务端异常参考:ErrorDecoder#decode//IO异常参考:SynchronousMethodHandler#executeAndDecoderetryer.continueOrPropagate(e);} catch (RetryableException th) {Throwable cause = th.getCause();if (propagationPolicy == UNWRAP && cause != null) {throw cause;} else {throw th;}}continue;}}}Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {...Request request = targetRequest(template);try {response = client.execute(request, options);} catch (IOException e) {//抛出RetryableExceptionthrow errorExecuting(request, e);}...}}

调用LoadBalancer

获取服务提供者信息并执行http请求

public class FeignBlockingLoadBalancerClient implements Client {@Overridepublic Response execute(Request request, Request.Options options) throws IOException {//从eureka client中根据serviceId获取服务提供方的地址信息//serviceId是FeignClient注解的value//通过eurekaClient从eurekaServer获取服务提供方的地址信息ServiceInstance instance = loadBalancerClient.choose(serviceId, lbRequest);...//生成并执行http请求return executeWithLoadBalancerLifecycleProcessing(delegate, options, newRequest, lbRequest, lbResponse,supportedLifecycleProcessors);}
}

获取服务提供者信息

根据FeignClient里设置的value获取服务提供者信息

public class BlockingLoadBalancerClient implements LoadBalancerClient {public  ServiceInstance choose(String serviceId, Request request) {//从容器中获取ReactorServiceInstanceLoadBalancer的实例//默认是RoundRobinLoadBalancer//参考LoadBalancerClientConfigurationReactiveLoadBalancer loadBalancer = loadBalancerClientFactory.getInstance(serviceId);if (loadBalancer == null) {return null;}//通过loadBanlancer获取地址信息Response loadBalancerResponse = Mono.from(loadBalancer.choose(request)).block();if (loadBalancerResponse == null) {return null;}return loadBalancerResponse.getServer();}
}

负载均衡处理

public class RoundRobinLoadBalancer implements ReactorServiceInstanceLoadBalancer {	@Overridepublic Mono> choose(Request request) {//拿到ServiceInstanceListSupplier,持有eurekaClientServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new);//根据请求从eureka拿到所有的服务端实例后选出一个实例return supplier.get(request).next().map(serviceInstances -> //从serviceInstances选择一个实例processInstanceResponse(supplier, serviceInstances));}private Response processInstanceResponse(ServiceInstanceListSupplier supplier,List serviceInstances) {//轮询选出一个实例Response serviceInstanceResponse = getInstanceResponse(serviceInstances);if (supplier instanceof SelectedInstanceCallback && serviceInstanceResponse.hasServer()) {((SelectedInstanceCallback) supplier).selectedServiceInstance(serviceInstanceResponse.getServer());}return serviceInstanceResponse;}private Response getInstanceResponse(List instances) {//每次获取+1int pos = Math.abs(this.position.incrementAndGet());//取余实现轮询ServiceInstance instance = instances.get(pos % instances.size());return new DefaultResponse(instance);}
}

解决问题

  1. 通过代理生成http进行远程调用
  2. 过eureka clienteureka server根据appName(FeignClient注解的value)获取服务提供者信息,并通过refresh保证信息及时性
  3. 默认通过轮询的方式
  4. 启动时,并通过周期性任务以及状态监听器保证数据变更时能够及时通知到eureka server
  5. 通过定时(默认10s)向eureka server发送心跳消息(renew)
  6. 通过定时(默认30s)从eureka server获取服务信息,然后根据appName更新本地缓存(refresh)
  7. 如果没超过最大重试次数,则定时重试,间隔时间每次增加1.5倍,但不超过最大重试时间(默认5s)。如果服务端返回的header里设置了Retry-After,则根据该header指定的值来设置间隔时间,且不超过配置的最大间隔

参考

https://github.com/Netflix/eureka/wiki/Eureka-at-a-glance

相关内容

热门资讯

【NI Multisim 14...   目录 序言 一、工具栏 🍊1.“标准”工具栏 🍊 2.视图工具...
银河麒麟V10SP1高级服务器... 银河麒麟高级服务器操作系统简介: 银河麒麟高级服务器操作系统V10是针对企业级关键业务...
不能访问光猫的的管理页面 光猫是现代家庭宽带网络的重要组成部分,它可以提供高速稳定的网络连接。但是,有时候我们会遇到不能访问光...
AWSECS:访问外部网络时出... 如果您在AWS ECS中部署了应用程序,并且该应用程序需要访问外部网络,但是无法正常访问,可能是因为...
Android|无法访问或保存... 这个问题可能是由于权限设置不正确导致的。您需要在应用程序清单文件中添加以下代码来请求适当的权限:此外...
北信源内网安全管理卸载 北信源内网安全管理是一款网络安全管理软件,主要用于保护内网安全。在日常使用过程中,卸载该软件是一种常...
AWSElasticBeans... 在Dockerfile中手动配置nginx反向代理。例如,在Dockerfile中添加以下代码:FR...
AsusVivobook无法开... 首先,我们可以尝试重置BIOS(Basic Input/Output System)来解决这个问题。...
ASM贪吃蛇游戏-解决错误的问... 要解决ASM贪吃蛇游戏中的错误问题,你可以按照以下步骤进行:首先,确定错误的具体表现和问题所在。在贪...
月入8000+的steam搬砖... 大家好,我是阿阳 今天要给大家介绍的是 steam 游戏搬砖项目,目前...