1)、单体应用架构
业务简单,数据量小,所有的模块部署在一个应用,一个服务容器。
优点:成本低,开发节奏快,架构简单,易于调试部署。
缺点:功能不断迭代,耦合严重;代码杂乱;沟通成功高 。
2)、垂直应用架构
为了解决单体架构的问题,开始做模块的垂直划分,划分依据业务特性,互相不影响。
优点:系统拆分流量分担,方便水平扩展,系统互不影响,可以独立优化。
缺点:系统之间调用硬编码,方式不统一,监控不到位;数据库资源浪费;集群负载均衡复杂。
3)、分布式SOA
Service-Oriented Architecture面向服务架构,业务逻辑下沉到服务层,在垂直划分基础上拆分出松耦合,独立部署的模块,模块之间相互独⽴,通过暴露接口供其他场景调用(通过Webservice/Dubbo等技术进⾏通信)。
优点:分布式、松耦合、扩展灵活、可重⽤。
缺点:服务抽取粒度复杂、接口数量不好控制,服务调用链路长,调用方和提供方耦合度较⾼,服务质量不稳定。
4)、微服务架构
是SOA的一种拓展,拆分粒度更小,服务更独立。不同的服务可以使⽤不同的开发语⾔,业务彻底的服务化和组件化 。
优点:业务功能聚焦,团队合作⼀定程度解耦,便于实施敏捷开发,便于重⽤和模块之间的组装。
缺点:分布式管理复杂,分布式链路跟踪难。
由于单体结构的应用随着系统复杂度的增高,会暴露出各种各样的问题。微服务架构逐渐取代了单体架构。Spring Cloud是目前最常用的微服务开发框架,已经在企业级开发中大量的应用。
Spring Cloud规范及实现意图要解决的问题其实就是微服务架构实施过程中存在的⼀些问题,比如微服务架构中的服务注册发现问题、网络问题(比如熔断场景)、统⼀认证安全授权问题、负载均衡问题、链路追踪等问题。
Spring Cloud是一系列框架的有序集合。它利用Spring Boot的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务发现注册、配置中心、智能路由、消息总线、负载均衡、断路器、数据监控等,都可以用Spring Boot的开发风格做到一键启动和部署。
Spring Cloud提供了一站式的微服务的解决方案。
第⼀代 Spring Cloud(Netflix, SCN) | 第⼆代 Spring Cloud(主要就是Spring Cloud Alibaba, SCA) | |
注册中⼼ | Netflix Eureka | 阿⾥巴巴 Nacos |
客户端负载均衡 | Netflix Ribbon | 阿⾥巴巴 Dubbo LB、 Spring Cloud Loadbalancer |
熔断器 | Netflix Hystrix | 阿⾥巴巴 Sentinel |
网关 | Netflix Zuul:性能⼀般,未来将退出Spring Cloud ⽣态圈 | 官⽅ Spring Cloud Gateway |
配置中心 | 官⽅ Spring Cloud Config | 阿⾥巴巴 Nacos、携程 Apollo |
服务调⽤ | Netflix Feign | 阿⾥巴巴 Dubbo RPC |
消息驱动 | 官⽅ Spring Cloud Stream | |
链路追踪 | 官⽅ Spring Cloud Sleuth/Zipkin |
服务提供者将所提供服务的信息(服务器IP和端口、服务访问协议等)注册/登记到注册中心,服务消费者能够从注册中心获取到较为实时的服务列表,然后根据⼀定的策略选择⼀个服务访问。
为了支持弹性扩缩容特性,微服务的提供者数量和分布往往是动态变化的,静态LB机制不再适用,所以需要额外引入注册中心组件。
服务注册中心本质上是为了解耦服务提供者和服务消费者。
1)服务提供者启动
2)服务提供者将相关服务信息主动注册到注册中心
3)服务消费者获取服务注册信息:pull模式和push模式
4) 服务消费者直接调⽤服务提供者
Eureka是Netflix出品服务注册发现工具,Spring Cloud封装了Netflix公司开发的Eureka模块来实现服务注册和发现。Eureka采用C-S的设计架构,包含Eureka Server 和Eureka Client两个组件。基于 RestfulAPI风格开发。
1)原理
2)细节详解
a、元数据
DiscoveryClient通过@EnableDiscoveryClient的方式进行启用,专门负责服务发现。
ServiceInstance服务实例对象存储实例的元数据信息。
元数据有两种:标准元数据和⾃定义元数据。
标准元数据: 主机名、 IP地址、端⼝号等信息,这些信息都会被发布在服务注册表中,⽤于服务之间的调⽤。
⾃定义元数据: 可以使⽤eureka.instance.metadata-map配置,符合key/value的存储格式。
instance:
metadata-map:
# ⾃定义元数据(kv⾃定义)
cluster: cl1
region: rn1
b、客户端
服务注册:
1)当我们导⼊了eureka-client依赖坐标,配置Eureka服务注册中⼼地址
2)服务在启动时会向注册中⼼发起注册请求,携带服务元数据信息
3) Eureka注册中⼼会把服务的信息保存在Map中。
服务续约:
服务每隔30秒会向注册中⼼续约(心跳)⼀次(也称为报活),如果没有续约,租约在90秒后到期,然后服务会被失效。每隔30秒的续约操作我们称之为心跳检查。
获取服务列表:
1)服务消费者启动时,从 EurekaServer服务列表获取只读备份,缓存到本地 2)每隔30秒,会重新获取并更新数据 3)每隔30秒的时间可以通过配置eureka.client.registry-fetch-interval-seconds修改
c、服务端
服务下线
1)当服务正常关闭操作时,会发送服务下线的REST请求给EurekaServer。
2)服务中⼼接受到请求后,将该服务置为下线状态。
失效剔除
Eureka Server会定时(间隔值是eureka.server.eviction-interval-timer-in-ms,默认60s)进⾏检查,如果发现实例在在⼀定时间(此值由客户端设置的eureka.instance.lease-expiration-duration-in-seconds定义,默认值为90s)内没有收到⼼跳,则会注销此实例。
⾃我保护
默认情况下,如果Eureka Server在⼀定时间内(默认90秒)没有接收到某个微服务实例的心跳, Eureka Server将会移除该实例。但是当⽹络分区故障发⽣时,微服务与Eureka Server之间⽆法正常通信,而微服务本身是正常运⾏的,此时不应该移除这个微服务,所以引⼊了⾃我保护机制。
当处于⾃我保护模式时 1)不会剔除任何服务实例(可能是服务提供者和EurekaServer之间⽹络问题),保证了⼤多数服务依然可⽤ 2) Eureka Server仍然能够接受新服务的注册和查询请求,但是不会被同步到其它节点上,保证当前节点依然可⽤,当⽹络稳定时,当前Eureka Server新的注册信息会被同步到其它节点中。 3)在Eureka Server⼯程中通过eureka.server.enable-self-preservation配置可⽤关停⾃我保护,默认值是打开。
1)、单例服务
a、基于Maven构建SpringBoot⼯程,创建EurekaServer模块。
父工程中需要引入Spring Cloud 依赖。
--dependencyManagement:对所依赖jar包进行版本管理的管理器
org.springframework.cloud
spring-cloud-dependencies
Greenwich.RELEASE
pom
import
b、EurekaServer服务pom.xml中引⼊依赖
org.springframework.cloud
spring-cloud-starter-netflix-eureka-server
c、配置文件application.yml
#eureka server服务端口
server:
port: 8761
spring:
application:
name: cloud-eureka-server # 应用名称,应用名称会在Eureka中作为服务名称
# eureka 客户端配置(和Server交互),Eureka Server 其实也是一个Client
eureka:
instance:
hostname: localhost # 当前eureka实例的主机名
client:
service-url:
# 配置客户端所交互的Eureka Server的地址(Eureka Server集群中每一个Server其实相对于其它Server来说都是Client)
# 集群模式下,defaultZone应该指向其它Eureka Server,如果有更多其它Server实例,逗号拼接即可
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
register-with-eureka: true # 是否注册到注册中心,集群模式下true
fetch-registry: true #是否从注册中心获取信息 集群模式下true
d、 SpringBoot启动类使⽤@EnableEurekaServer声明当前项目为EurekaServer服务
@SpringBootApplication
@EnableEurekaServer
public class EurekaApp8761
{
public static void main( String[] args )
{
SpringApplication.run(EurekaApp8761.class);
}
}
2)、高可用集群
a、开启两台 EurekaServer 以搭建集群,两个实例互为对方的集群镜像,修改defaultZone属性。
eureka:
instance:
hostname: EurekaServerA # 当前eureka实例的主机名
client:
service-url:
defaultZone: http://EurekaServerB:8762/eureka
register-with-eureka: true
fetch-registry: true
Eureka Server集群之中的节点通过点对点(P2P)通信的方式共享服务注册表。
b、创建微服务客户端模块,注册到Eureka Server集群中
pom文件中引入客户依赖:
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
配置文件中指定注册中心地址:
eureka:
client:
serviceUrl:
defaultZone: http://EurekaServerA:8761/eureka/,http://EurekaServerB:8762/eureka/
#eureka 集群中的所有 url 都填写了进来,也可以只写一台,因为各个 eureka server可以同步注册表
instance:
#使用ip注册,否则会使用主机名注册了(此处考虑到对老版本的兼容,新版本经过实验都是ip)
prefer-ip-address: true
#自定义实例显示格式,加上版本号,便于多版本管理,注意是ip-address,早期版本是ipAddress
instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@
启动类中添加@EnableDiscoveryClient注解
@SpringBootApplication
@EnableDiscoveryClient
public class CodeAppliction
{
public static void main( String[] args )
{
SpringApplication.run(CodeAppliction.class);
}
}
c、服务消费者使用调用服务提供者,使用DiscoveryClient获取服务实例的注册信息
@RestController
@RequestMapping("/code")
public class CodeController {
@Autowired
private RestTemplate restTemplate;
@Autowired
private DiscoveryClient discoveryClient;
@GetMapping("/create/{email}")
public Boolean createCode(@PathVariable String email) {
// 1、从 Eureka Server中获取关注服务的实例信息(使用客户端对象做这件事)
List instances = discoveryClient.getInstances("cloud-service-email");
// 2、如果有多个实例,选择一个使用(负载均衡的过程)
ServiceInstance serviceInstance = instances.get(0);
// 3、从元数据信息获取host port
String host = serviceInstance.getHost();
int port = serviceInstance.getPort();
String url = "http://" + host + ":" + port + "/email/" + email;
System.out.println("===============>>>从EurekaServer集群获取服务实例拼接的url:" + url);
// 调用远程服务—> 简历微服务接口 RestTemplate -> JdbcTempate
// httpclient封装好多内容进行远程调用
Boolean forObject = restTemplate.getForObject(url, Boolean.class);
return forObject;
}
}
1)、Eureka Server启动过程
a、入口:
@EnableEurekaServer注解中引入了EurekaServerMarkerConfiguration
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({EurekaServerMarkerConfiguration.class})
public @interface EnableEurekaServer {
}
b、SpringCloud充分利⽤了SpringBoot的自动装配的特点,启动时自动装配EurekaServerAutoConfiguration。
eureka-server的jar包,META-INF下的配置文件spring.factories中指定了SpringBoot启动时会自动加载的EurekaServerAutoConfiguration。
观察EurekaServerAutoConfiguration类中指定了加载条件,存在EurekaServerMarkerConfiguration.Marker实例;也就是说添加了@EnableEurekaServer注解决定了EurekaServer配置的前提。
@Configuration
@Import({EurekaServerInitializerConfiguration.class})
@ConditionalOnBean({EurekaServerMarkerConfiguration.Marker.class})
@EnableConfigurationProperties({EurekaDashboardProperties.class, InstanceRegistryProperties.class})
@PropertySource({"classpath:/eureka/server.properties"})
public class EurekaServerAutoConfiguration extends WebMvcConfigurerAdapter {
...
}
c、EurekaServerAutoConfiguration中注入EurekaController、PeerAwareInstanceRegistry、PeerEurekaNodes 等基础对象
/* 注入对外接口,仪表盘,配置文件中指定eureka.dashboard.enabled=true开启 */
@Bean
@ConditionalOnProperty(prefix = "eureka.dashboard", name = {"enabled"}, matchIfMissing = true)
public EurekaController eurekaController() {
return new EurekaController(this.applicationInfoManager);
}
/* 对等节点感知实例注册器,集群模式下注册服务使用的注册器 */
@Bean
public PeerAwareInstanceRegistry peerAwareInstanceRegistry(ServerCodecs serverCodecs) {
this.eurekaClient.getApplications();
return new InstanceRegistry(this.eurekaServerConfig, this.eurekaClientConfig, serverCodecs, this.eurekaClient,
this.instanceRegistryProperties.getExpectedNumberOfClientsSendingRenews(),
this.instanceRegistryProperties.getDefaultOpenForTrafficCount());
}
/* 辅助封装对等节点相关信息和操作,比如更新集群中的对等 */
@Bean
@ConditionalOnMissingBean
public PeerEurekaNodes peerEurekaNodes(PeerAwareInstanceRegistry registry, ServerCodecs serverCodecs) {
return new EurekaServerAutoConfiguration.RefreshablePeerEurekaNodes(registry, this.eurekaServerConfig, this.eurekaClientConfig, serverCodecs, this.applicationInfoManager);
}
在com.netflix.eureka.cluster.PeerEurekaNodes的start方法中完成节点更新,触发时机为DefaultEurekaServerContext的initialize()方法中;
public void start() {
// 构建Eureka-PeerNodesUpdater线程池
this.taskExecutor = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() {
public Thread newThread(Runnable r) {
Thread thread = new Thread(r, "Eureka-PeerNodesUpdater");
thread.setDaemon(true);
return thread;
}
});
try {
// 更新对等节点信息 任务触发时机?
this.updatePeerEurekaNodes(this.resolvePeerUrls());
Runnable peersUpdateTask = new Runnable() {
public void run() {
try {
PeerEurekaNodes.this.updatePeerEurekaNodes(PeerEurekaNodes.this.resolvePeerUrls());
} catch (Throwable var2) {
PeerEurekaNodes.logger.error("Cannot update the replica Nodes", var2);
}
}
};
this.taskExecutor.scheduleWithFixedDelay(peersUpdateTask, (long)this.serverConfig.getPeerEurekaNodesUpdateIntervalMs(), (long)this.serverConfig.getPeerEurekaNodesUpdateIntervalMs(), TimeUnit.MILLISECONDS);
} catch (Exception var3) {
throw new IllegalStateException(var3);
}
Iterator var4 = this.peerEurekaNodes.iterator();
while(var4.hasNext()) {
PeerEurekaNode node = (PeerEurekaNode)var4.next();
logger.info("Replica node URL: {}", node.getServiceUrl());
}
}
d、EurekaServerAutoConfiguration还注入了EurekaServerContext、EurekaServerBootstrap、FilterRegistationBean
/* 注入EurekaServer上下文DefaultEurekaServerContext */
@Bean
public EurekaServerContext eurekaServerContext(ServerCodecs serverCodecs, PeerAwareInstanceRegistry registry, PeerEurekaNodes peerEurekaNodes) {
return new DefaultEurekaServerContext(this.eurekaServerConfig, serverCodecs, registry, peerEurekaNodes, this.applicationInfoManager);
}
// 注入EurekaServer启动类
@Bean
public EurekaServerBootstrap eurekaServerBootstrap(PeerAwareInstanceRegistry registry, EurekaServerContext serverContext) {
return new EurekaServerBootstrap(this.applicationInfoManager, this.eurekaClientConfig, this.eurekaServerConfig, registry, serverContext);
}
而DefaultEurekaServerContext类中的指定了PeerEurekaNodes的启动
c、关注到EurekaServerAutoConfiguration中引入了EurekaServerInitializerConfiguration注册中心初始化配置类:
EurekaServerInitializerConfiguration类实现了 ServletContextAware
(拿到了tomcat的ServletContext对象)、SmartLifecycle
(Spring容器初始化该bean时会调用相应生命周期方法);
其中start方法调用EurekaServerBootstrap的contextInitialized方法初始化环境和上下文,完成Eureka服务启动。
public class EurekaServerInitializerConfiguration implements ServletContextAware, SmartLifecycle, Ordered {
...
public void start() {
(new Thread(new Runnable() {
public void run() {
try {
// 初始化EurekaServletContext
EurekaServerInitializerConfiguration.this.eurekaServerBootstrap.contextInitialized(EurekaServerInitializerConfiguration.this.servletContext);
EurekaServerInitializerConfiguration.log.info("Started Eureka Server");
// 发布事件
EurekaServerInitializerConfiguration.this.publish(new EurekaRegistryAvailableEvent(EurekaServerInitializerConfiguration.this.getEurekaServerConfig()));
// 状态属性设置
EurekaServerInitializerConfiguration.this.running = true;
EurekaServerInitializerConfiguration.this.publish(new EurekaServerStartedEvent(EurekaServerInitializerConfiguration.this.getEurekaServerConfig()));
} catch (Exception var2) {
EurekaServerInitializerConfiguration.log.error("Could not initialize Eureka servlet context", var2);
}
}
})).start();
}
}
查看EurekaServerBootstrap#initEurekaServerContext方法初始化细节:
protected ApplicationInfoManager applicationInfoManager;
protected PeerAwareInstanceRegistry registry;
public void contextInitialized(ServletContext context) {
try {
// 初始化环境
this.initEurekaEnvironment();
// 初始化Context细节
this.initEurekaServerContext();
context.setAttribute(EurekaServerContext.class.getName(), this.serverContext);
} catch (Throwable var3) {
log.error("Cannot bootstrap eureka server :", var3);
throw new RuntimeException("Cannot bootstrap eureka server :", var3);
}
}
protected void initEurekaServerContext() throws Exception {
// 注册数据类型转换器
JsonXStream.getInstance().registerConverter(new V1AwareInstanceInfoConverter(), XStream.PRIORITY_VERY_HIGH);
XmlXStream.getInstance().registerConverter(new V1AwareInstanceInfoConverter(), XStream.PRIORITY_VERY_HIGH);
if (this.isAws(this.applicationInfoManager.getInfo())) {
this.awsBinder = new AwsBinderDelegate(this.eurekaServerConfig, this.eurekaClientConfig, this.registry, this.applicationInfoManager);
this.awsBinder.start();
}
// 给非ioc容器提供获取serverContext的接口
EurekaServerContextHolder.initialize(this.serverContext);
log.info("Initialized server context");
// 从邻近节点拷贝注册信息
int registryCount = this.registry.syncUp();
// 更改实例状态为up 对外提供服务
this.registry.openForTraffic(this.applicationInfoManager, registryCount);
// 注册统计器
EurekaMonitors.registerAllStats();
}
其中PeerAwareInstanceRegistryImpl#syncUp方法,又调用了register方法完成注册列表的复制。
而PeerAwareInstanceRegistryImpl类继承抽象类com.netflix.eureka.registry.AbstractInstanceRegistry(*)。
查看父类com.netflix.eureka.registry.AbstractInstanceRegistry#register方法:
-- 客户端将注册服务到注册中心;属性registry属性保存注册服务数据。
该类实现了org.springframework.cloud.netflix.eureka.server.InstanceRegistry接口;
PeerAwareInstanceRegistryImpl#openForTraffic方法,ApplicationInfoManager:Eureka实例信息的管理器。
public void openForTraffic(ApplicationInfoManager applicationInfoManager, int count) {
this.expectedNumberOfClientsSendingRenews = count;
this.updateRenewsPerMinThreshold();
logger.info("Got {} instances from neighboring DS node", count);
logger.info("Renew threshold is: {}", this.numberOfRenewsPerMinThreshold);
this.startupTime = System.currentTimeMillis();
if (count > 0) {
this.peerInstancesTransferEmptyOnStartup = false;
}
Name selfName = applicationInfoManager.getInfo().getDataCenterInfo().getName();
boolean isAws = Name.Amazon == selfName;
if (isAws && this.serverConfig.shouldPrimeAwsReplicaConnections()) {
logger.info("Priming AWS connections for all replicas..");
this.primeAwsReplicas(applicationInfoManager);
}
logger.info("Changing status to UP");
// 实例状态改为UP
applicationInfoManager.setInstanceStatus(InstanceStatus.UP);
// 定时任务
super.postInit();
}
protected void postInit() {
this.renewsLastMin.start();
if (this.evictionTaskRef.get() != null) {
((AbstractInstanceRegistry.EvictionTask)this.evictionTaskRef.get()).cancel();
}
// 失效剔除定时任务 默认每隔60s进行一次服务失效剔除
this.evictionTaskRef.set(new AbstractInstanceRegistry.EvictionTask());
this.evictionTimer.schedule((TimerTask)this.evictionTaskRef.get(), this.serverConfig.getEvictionIntervalTimerInMs(), this.serverConfig.getEvictionIntervalTimerInMs());
}
二、Ribbon负载均衡
分为服务端负载均衡(Nginx,F5)和客户端负载均衡(Ribbon)。
@Bean
// Ribbon负载均衡
@LoadBalanced
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
Ribbon内置了多种负载均衡策略,内部负责复杂均衡的顶级接⼝为
com.netflix.loadbalancer.IRule
工作原理
Ribbon给restTemplate添加了⼀个拦截器。当根据服务名访问接口时, ribbon根据服务名获取到该服务的实例列表并按照⼀定的负载均衡策略从实例列表中获取⼀个实例Server,并最终通过RestTemplate进⾏请求访问
三、Feign远程调用
Feign是Netflix开发的⼀个轻量级RESTful的HTTP服务客户端(⽤它来发起请求,
远程调⽤的) ,是以Java接⼝注解的⽅式调⽤Http请求,⽽不⽤像Java中通过封装
HTTP请求报⽂的⽅式直接调⽤, Feign被⼴泛应⽤在Spring Cloud 的解决⽅案中。