Spring Cloud的一般组件:
- Eureka:注册中心,这个必须要放在第一个说,毕竟没有注册中心就没有服务的自动伸缩,就谈不上微服务了;
- Ribbon、Feign:远程调用,没有远程调用也还是谈不上微服务,所以其实注册中心加上远程调用就可以囊括微服务了,其他功能只是增强;
- Hystrix:服务熔断和降级,主要是防止服务雪崩;
- Gateway:网关,处理统一服务的地方;
- Config:配置中心,集中处理配置的地方。
Eureka主要作用
- 服务自动伸缩;
- 服务负载均衡。
服务自动伸缩
- 为了保证可用性和负载均衡,通常我们会将一个服务起多个服务实例;
- 在管理多个服务实例的时候,有时我们会在生产上动态地删除或者新增服务实例,这个就是服务的自动伸缩;
- 为了实现服务的自动伸缩,我们就需要一个地方来统一管理服务实例,这个地方就是注册中心(Eureka)了;
- 注册中心会帮我们管理我们的服务实例,我们只需要在启动服务实例的时候去注册中心注册服务实例即可,当服务实例出问题不可用时,Eureka会自动把这个实例下线。
服务负载均衡
- 因为每个服务都有多个实例,所以我们完全可以做到服务的负载均衡来分摊流量;
- 为了实现负载均衡,我们在服务实际调用中不会使用实例具体的ip和端口,而是使用服务名;
- 服务名到ip+端口的转换也需要注册中心来帮我们管理了;
- 在实例启动去注册中心注册自己的时候,会带上自己的实例名、ip和端口信息,注册中心会保留这些信息供负载均衡的时候使用。
原理
-
Eureka分为Eureka Server(服务端)和Eureka Client(客户端),Eureka Server就是我们启动的Eureka服务,管理其他服务注册信息;Eureka Client就是其他的应用服务,Eureka Client向Eureka Server发送注册请求,注册自己;
-
服务注册:一般我们会先起一个Eureka Server服务,等待Eureka Client来注册,当启动其他服务实例时,会主动给Eureka Server发送一个注册请求,请求会带上自己的一些元数据,比如ip、端口、主页地址等信息;
-
服务续约(心跳):Eureka Client为了让Eureka Server知道自己运行正常,会隔一段时间就向Erueka Server发送一个心跳请求,时间间隔默认是30s;
-
服务下线:
1、主动下线:当Eureka Client需要关闭时,会向Eureka Server发送一个下线请求,Eureka Server收到请求后,会将该服务状态设置为DOWN,并将该事件广播出去;
2、被动下线:Eureka Server中有一个定时任务(默认60s执行一次),会将90s内(3次)都没有发送心跳请求的Eureka Client下线; -
自我保护:有一种情况是Eureka Server本身网络有问题,导致大家的心跳请求都进不来,这个时候把所有的服务都下线显然是不合理的,这种请求Eureka Server会启动自我保护机制,所有的服务都不会下线,启动自我保护机制的条件是15min内85%的服务都续约失败了;
-
服务获取及调用:在服务间想要相互调用的时候,第一件事就是需要先获取对方服务的地址(ip+端口),这个就需要根据对方的服务名去注册中心获取了,其实在Eureka Client启动的时候,Eureka Client就会从Eureka Server上拉取整个注册的服务列表,并在本地缓存,缓存有效时长默认是30s,获取到地址之后就可以调用对方服务了;Eureka有Region和Zone的概念,一个Region包含多个Zone,优先调用同一Zone的服务;
-
服务同步:为了Eureka Server的高可用性,Eureka Server一般会采用集群的部署方式,集群中的每一个Eureka Server都是平等的,都能提供注册和拉取服务的功能,它们之间的服务信息会相互同步,同步分为两种:
1、新加入的Eureka Server会向就近的节点复制全量的注册信息,以此初始化自己本地的注册表信息;
2、Eureka Server对注册表的操作都会同步给所有节点,这个是增量的。
Eureka Server源码
在Eureka Server使用中,首先就会添加@EnableEurekaServer注解,根据之前看Spring Boot自动装配的经验,Eureka Server的启动应该就是从这个注解开始了。
- @EnableEurekaServer注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(EurekaServerMarkerConfiguration.class)
public @interface EnableEurekaServer {
}
啥也没有,看下EurekaServerMarkerConfiguration类。
- EurekaServerMarkerConfiguration
@Configuration
public class EurekaServerMarkerConfiguration {
@Bean
public Marker eurekaServerMarkerBean() {
return new Marker();
}
class Marker {
}
}
也是啥都没有,所以Eureka Server启动的重点根本就不在@EnableEurekaServer注解中,它只是创建了一个Marker类的实例bean,这个类还是空的,这个在后面会用到,因为启动类上还有@SpringBootApplication注解,所以看下/META-INF/spring.factories文件。
- spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.netflix.eureka.server.EurekaServerAutoConfiguration
看名字就知道EurekaServerAutoConfiguration大概就是Eureka Server的配置类了。
- EurekaServerAutoConfiguration
@Configuration
@Import(EurekaServerInitializerConfiguration.class)
@ConditionalOnBean(EurekaServerMarkerConfiguration.Marker.class)
@EnableConfigurationProperties({ EurekaDashboardProperties.class,
InstanceRegistryProperties.class })
@PropertySource("classpath:/eureka/server.properties")
public class EurekaServerAutoConfiguration extends WebMvcConfigurerAdapter {
这个类太长了,先只看下开头的注解内容吧,其中有一个条件注解:@ConditionalOnBean(EurekaServerMarkerConfiguration.Marker.class),这里的条件就是上面提到的没内容的类,也就是说EurekaServerMarkerConfiguration.Marker类本身没什么用,它只是一个开关,有了它才会去装配EurekaServerAutoConfiguration配置类,然后再看下一个比较重要的bean:PeerAwareInstanceRegistry。
- PeerAwareInstanceRegistry
@Bean
public PeerAwareInstanceRegistry peerAwareInstanceRegistry(
ServerCodecs serverCodecs) {
this.eurekaClient.getApplications(); // force initialization
return new InstanceRegistry(this.eurekaServerConfig, this.eurekaClientConfig,
serverCodecs, this.eurekaClient,
this.instanceRegistryProperties.getExpectedNumberOfRenewsPerMin(),
this.instanceRegistryProperties.getDefaultOpenForTrafficCount());
}
我们用的最多的registry(注册)、renew(续约)、evict(剔除服务)、cancel(服务下线)等等功能都在这个接口里面,具体实现是在AbstractInstanceRegistry类中。
- AbstractInstanceRegistry#registry
public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
try {
read.lock();
// 这里的registry就是存放注册信息的map,appName是服务名
Map<String, Lease<InstanceInfo>> gMap = registry.get(registrant.getAppName());
REGISTER.increment(isReplication);
if (gMap == null) {
final ConcurrentHashMap<String, Lease<InstanceInfo>> gNewMap = new ConcurrentHashMap<String, Lease<InstanceInfo>>();
// 多线程处理,为了防止覆盖,使用putIfAbsent方法
gMap = registry.putIfAbsent(registrant.getAppName(), gNewMap);
if (gMap == null) {
gMap = gNewMap;
}
}
// id是服务实例的唯一id
Lease<InstanceInfo> existingLease = gMap.get(registrant.getId());
// Retain the last dirty timestamp without overwriting it, if there is already a lease
// 如果这个实例已经存在
if (existingLease != null && (existingLease.getHolder() != null)) {
Long existingLastDirtyTimestamp = existingLease.getHolder().getLastDirtyTimestamp();
Long registrationLastDirtyTimestamp = registrant.getLastDirtyTimestamp();
logger.debug("Existing lease found (existing={}, provided={}", existingLastDirtyTimestamp, registrationLastDirtyTimestamp);
// this is a > instead of a >= because if the timestamps are equal, we still take the remote transmitted
// InstanceInfo instead of the server local copy.
// 比较已经存在的和要注册实例的更新时间,使用最新的实例注册信息
if (existingLastDirtyTimestamp > registrationLastDirtyTimestamp) {
logger.warn("There is an existing lease and the existing lease's dirty timestamp {} is greater" +
" than the one that is being registered {}", existingLastDirtyTimestamp, registrationLastDirtyTimestamp);
logger.warn("Using the existing instanceInfo instead of the new instanceInfo as the registrant");
registrant = existingLease.getHolder();
}
// 正常流程应该是走这里
} else {
// The lease does not exist and hence it is a new registration
synchronized (lock) {
if (this.expectedNumberOfRenewsPerMin > 0) {
// Since the client wants to cancel it, reduce the threshold
// (1
// for 30 seconds, 2 for a minute)
this.expectedNumberOfRenewsPerMin = this.expectedNumberOfRenewsPerMin + 2;
// 自我保护的参数,在服务剔除方法中会用到
// numberOfRenewsPerMinThreshold > 0 && getNumOfRenewsInLastMin() > numberOfRenewsPerMinThreshold;
// 当这两个条件为false时,就会启动自我保护机制
this.numberOfRenewsPerMinThreshold =
(int) (this.expectedNumberOfRenewsPerMin * serverConfig.getRenewalPercentThreshold());
}
}
logger.debug("No previous lease information found; it is new registration");
}
// 创建一个新的Lease对象,这个就是服务实例注册信息
Lease<InstanceInfo> lease = new Lease<InstanceInfo>(registrant, leaseDuration);
if (existingLease != null) {
lease.setServiceUpTimestamp(existingLease.getServiceUpTimestamp());
}
// 添加到保存注册信息的map中,就相当于注册成功了
gMap.put(registrant.getId(), lease);
synchronized (recentRegisteredQueue) {
// 添加到最新注册的队列中,这个队列会在/lastn接口中用到
recentRegisteredQueue.add(new Pair<Long, String>(
System.currentTimeMillis(),
registrant.getAppName() + "(" + registrant.getId() + ")"));
}
// This is where the initial state transfer of overridden status happens
if (!InstanceStatus.UNKNOWN.equals(registrant.getOverriddenStatus())) {
logger.debug("Found overridden status {} for instance {}. Checking to see if needs to be add to the "
+ "overrides", registrant.getOverriddenStatus(), registrant.getId());
if (!overriddenInstanceStatusMap.containsKey(registrant.getId())) {
logger.info("Not found overridden id {} and hence adding it", registrant.getId());
overriddenInstanceStatusMap.put(registrant.getId(), registrant.getOverriddenStatus());
}
}
InstanceStatus overriddenStatusFromMap = overriddenInstanceStatusMap.get(registrant.getId());
if (overriddenStatusFromMap != null) {
logger.info("Storing overridden status {} from map", overriddenStatusFromMap);
registrant.setOverriddenStatus(overriddenStatusFromMap);
}
// Set the status based on the overridden status rules
InstanceStatus overriddenInstanceStatus = getOverriddenInstanceStatus(registrant, existingLease, isReplication);
registrant.setStatusWithoutDirty(overriddenInstanceStatus);
// If the lease is registered with UP status, set lease service up timestamp
if (InstanceStatus.UP.equals(registrant.getStatus())) {
lease.serviceUp();
}
registrant.setActionType(ActionType.ADDED);
// 最近变化队列,用于Eureka Client增量获取注册信息
recentlyChangedQueue.add(new RecentlyChangedItem(lease));
registrant.setLastUpdatedTimestamp();
// 让缓存失效,用于Eureka Client全量获取注册信息时,读取到最新的数据
invalidateCache(registrant.getAppName(), registrant.getVIPAddress(), registrant.getSecureVipAddress());
logger.info("Registered instance {}/{} with status {} (replication={})",
registrant.getAppName(), registrant.getId(), registrant.getStatus(), isReplication);
} finally {
read.unlock();
}
}
整个注册流程其实并不复杂,主要是要维护Eureka中的各种队列和缓存。Eureka为了提高效率,采用了多级缓存,读写和只读的复杂得一批。
Eureka Server中的缓存
Eureka Server为了有更高的读写效率,对注册信息做了两层缓存。
- AbstractInstanceRegistry#registry
private final ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry
= new ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>();
这个是Eureka Server中保存注册信息的map,所有的注册信息都会在注册的时候保存到这个map中,但是Eureka Client拉取注册信息的时候,并不是直接从这个map中读取的。
- 读写缓存,ResponseCacheImpl#readWriteCacheMap
private final LoadingCache<Key, Value> readWriteCacheMap;
它主要的作用是从registry中获取注册信息,然后为写缓存提供缓存数据,看readWriteCacheMap中的数据是怎么来的,在ResponseCacheImpl的构造方法中,只截取一段吧:
ResponseCacheImpl(EurekaServerConfig serverConfig, ServerCodecs serverCodecs, AbstractInstanceRegistry registry) {
this.serverConfig = serverConfig;
this.serverCodecs = serverCodecs;
this.shouldUseReadOnlyResponseCache = serverConfig.shouldUseReadOnlyResponseCache();
this.registry = registry;
long responseCacheUpdateIntervalMs = serverConfig.getResponseCacheUpdateIntervalMs();
this.readWriteCacheMap =
CacheBuilder.newBuilder().initialCapacity(1000)
// 设置缓存有效期,默认是180s
.expireAfterWrite(serverConfig.getResponseCacheAutoExpirationInSeconds(), TimeUnit.SECONDS)
.removalListener(new RemovalListener<Key, Value>() {
@Override
public void onRemoval(RemovalNotification<Key, Value> notification) {
Key removedKey = notification.getKey();
if (removedKey.hasRegions()) {
Key cloneWithNoRegions = removedKey.cloneWithoutRegions();
regionSpecificKeys.remove(cloneWithNoRegions, removedKey);
}
}
})
.build(new CacheLoader<Key, Value>() {
@Override
public Value load(Key key) throws Exception {
if (key.hasRegions()) {
Key cloneWithNoRegions = key.cloneWithoutRegions();
regionSpecificKeys.put(cloneWithNoRegions, key);
}
// 这个方法里会从registry中读取注册信息
Value value = generatePayload(key);
return value;
}
});
...
}
除了这里获取之外,在注册的方法里也看到了每次注册成功之后,都会让写缓存失效。
- 读缓存,ResponseCacheImpl#readOnlyCacheMap
private final ConcurrentMap<Key, Value> readOnlyCacheMap = new ConcurrentHashMap<Key, Value>();
这个缓存是只读的,搞两个缓存主要也是为了读写分离,提高数据的写入和读取效率;另外,读缓存是可以通过配置关闭的,通过配置shouldUseReadOnlyResponseCache参数就可以了,这个等下在源码中可以看到。先看下它的数据是从哪来的吧,也是在构造方法中:
if (shouldUseReadOnlyResponseCache) {
timer.schedule(getCacheUpdateTask(),
new Date(((System.currentTimeMillis() / responseCacheUpdateIntervalMs) * responseCacheUpdateIntervalMs)
+ responseCacheUpdateIntervalMs),
responseCacheUpdateIntervalMs);
}
如果配置了shouldUseReadOnlyResponseCache为true(默认是true),就会启动一个定时任务,默认是每30s就去执行getCacheUpdateTask()方法,这个方法很简单就是去读写缓存中读取数据同步到读缓存中。再看下使用的地方:
@VisibleForTesting
Value getValue(final Key key, boolean useReadOnlyCache) {
Value payload = null;
try {
if (useReadOnlyCache) {
final Value currentPayload = readOnlyCacheMap.get(key);
if (currentPayload != null) {
payload = currentPayload;
} else {
payload = readWriteCacheMap.get(key);
readOnlyCacheMap.put(key, payload);
}
} else {
payload = readWriteCacheMap.get(key);
}
} catch (Throwable t) {
logger.error("Cannot get value for key : {}", key, t);
}
return payload;
}
这个方法是缓存使用的方法,逻辑其实也很简单:如果开启了读缓存就从读缓存读,读缓存没有就从读写缓存读,然后写入到读缓存中。