从今天开始陆续分享一下springcloud的相关知识,一方面在分享的同时,也是为了知识的储备,今天要介绍的springcloud的服务治理组件-eureka,这个组件很基础但却非常重要,因为如果没有它的话基本上也就啥都玩不了(不是说少了eukrea啥都玩不了,因为服务治理的组件有很多,比如consul,zookeeper等)我强调的是在springcloud的组件中,必不可少的组件是服务治理相关组件,至于具体选择哪一种实现,后面会给出建议和对比,大家也可以根据自己的需求来选择,在没有正式开始介绍springcloud之前,先说一个前提,就是对于springcloud而言,个人觉得从开发的角度来说没有多大的难度,因为它完全是基于springboot开发的,所以开发起来非常的简便可快捷,所以在学springcloud之前,最好先熟悉一下springboot相关知识,ok概括一下本章的内容提纲
& springcloud的整体介绍
& eureka的概括
& eureka单节点演示
& eureka多节点演示(HA机制)
& eureka+server provider+ server consumer整合
& eureka的核心源码分析(核心方法以及和springcloud如何集成)
一 springcloud的介绍
Spring Cloud是一个基于Spring Boot实现的云应用开发工具,它为基于JVM的云应用开发中涉及的配置管理、服务发现、断路器、智能路由、微代理、控制总线、全局锁、决策竞选、分布式会话和集群状态管理等操作提供了一种简单的开发方式。
Spring Cloud包含了多个子项目(针对分布式系统中涉及的多个不同开源产品),比如:Spring Cloud Config、Spring Cloud Netflix、Spring Cloud0 CloudFoundry、Spring Cloud AWS、Spring Cloud Security、Spring Cloud Commons、Spring Cloud Zookeeper、Spring Cloud CLI等项目。
二 eureka的概括
由于Spring Cloud为服务治理做了一层抽象接口,所以在Spring Cloud应用中可以支持多种不同的服务治理框架,比如:Netflix Eureka、Consul、Zookeeper。在Spring Cloud服务治理抽象层的作用下,我们可以无缝地切换服务治理实现,并且不影响任何其他的服务注册、服务发现、服务调用等逻辑。
所以,下面我们通过介绍两种服务治理的实现来体会Spring Cloud这一层抽象所带来的好处。
三 eureka单节点的演示
后面会用一个项目来整体演示springcloud的所有核心功能,首先看一下项目结构
首先看一下父pom.xml的配置文件内容,这里有个细节地方要特别注意
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.suning.cloud</groupId> <artifactId>spring-cloud</artifactId> <version>1.0-SNAPSHOT</version> <modules> <module>user_server_provider</module> <module>eureka_server</module> <module>eureka_server_ha</module> </modules> <packaging>pom</packaging> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <!--spring-boot-starter-parent 与 spring-cloud-dependencies 版本集成和依赖 --> <!--1.2.x vs A Angel--> <!--1.3.x vs B Brixton--> <!--1.4.x vs C Camden--> <!--1.5.x vs D Dalston--> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.2.RELEASE</version> </parent> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Dalston.SR5</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
上面这个配置文件主要指定了springboot的版本和springcloud的版本,有个地方一定要注意,对于springcloud和springboot的版本是依赖和匹配的关系,如果版本匹配的不正确,会出现各种错误,所以把它们之间的依赖版本关系整理如下所示:
<!--spring-boot-starter-parent 与 spring-cloud-dependencies 版本集成和依赖 -->
<!--1.2.x vs A Angel-->
<!--1.3.x vs B Brixton-->
<!--1.4.x vs C Camden-->
<!--1.5.x vs D Dalston-->
前面的版本是指定springboot的版本,后面的版本是指定springcloud的版本,最好按照这个来,避免一开始搭建项目就包各种错误.
接下来建立一个叫eureka_server的module,这个模块主要包含一个类,一个配置文件
首先看一下对应的pom.xml文件内容:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>spring-cloud</artifactId> <groupId>com.suning.cloud</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>eureka_server</artifactId> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <!--集成eurekaserver--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka-server</artifactId> </dependency> <!--集成spring-security权限验证功能--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> </dependencies> </project>
集成eurekaserver和权限安全验证功能(是否要开启该功能,可自由选择,不需要就不要引进),看一下对应的java类内容也很简单
package com.eureka.server; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; import org.springframework.context.annotation.Profile; /** * @Author 18011618 * @Description 启动服务注册与发现组件 * @Date 10:00 2018/7/9 * @Modify By */ @EnableAutoConfiguration //启动自动依赖配置检查 @SpringBootApplication //启动springboot @EnableEurekaServer //启动eurekaserver public class CloudEurekaServerApplication { public static void main(String[] args) { SpringApplication.run(CloudEurekaServerApplication.class,args); } }
这就完成了eureka实例的创建,而主要就是通过一个这个注解来标识开启eurekaserver.....,下面简单的介绍一下其他注解的功能:
:开启依赖配置文件的检查
:通过springboot的方式启动当前应用,到此代码就完成了,还缺一个配置文件(一般都叫application.yml或者application.properties),内容如下
security: #配置spring-security功能 basic: enabled: true #开启用户登录权限验证 user: name: admin #设置登录用户为admin password: admin #设置账号密码为admin server: port: 8080 #服务端口号 eureka: instance: hostname: localhost #访问域名或者IP地址 client: register-with-eureka: false #服务发现与注册组件自身不进行注册 fetch-registry: false service-url: defaultZone: http://${security.user.name}:${security.user.password}@${eureka.instance.hostname}:${server.port}/eureka/ #注册服务地址url #注意yml文件也是支持表达式的,这样配置可以更加的灵活
上面的配置文件内容如下:
>指定开启eureka登录安全的权限验证以及对应的登录用户和密码
>指定服务端的端口号
>指定服务访问的域名或者IP地址
>指定当前服务并不参与注册
>指定服务访问的域名
接下来就可以启动当前的应用,如下图所示
如果此时服务没有报错,成功启动应用,访问http://localhost:8080/ 会出现如下的界面,
因为上面的配置文件包含了所以才会出现登录界面,这时候输入admin和admin,界面如下
代表一个服务注册的eureka就成功启动了,红色标注的是,代表当前还没有任何应用注册到这个服务组件上,所以接下来就创建一个
服务提供方且注册到该组件上,再建立一个叫模块叫user_server_provider,项目结构如下所示:
这里为了方便演示,数据的产生使用了h2+jpa的注解功能,首先看一下pom.xml的内容
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>spring-cloud</artifactId> <groupId>com.suning.cloud</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>user_server_provider</artifactId> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <!--集成JPA--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <!--启动WEB--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--集成H2DTATABASE--> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> <!--集成EUREKA-SERVER--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka</artifactId> </dependency> <!--系统自省和监控的集成功能--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> </dependencies> </project>
下面看看其他的类:
package com.suning.provider.bean; import java.math.BigDecimal; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; /** * @Author 18011618 * @Date 9:49 2018/7/9 * @Function 使用JPA注解 */ @Entity public class User { public User(Long id, String username) { super(); this.id = id; this.username = username; } public User() { super(); } @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; @Column private String username; @Column private String name; @Column private Short age; @Column private BigDecimal balance; public Long getId() { return this.id; } public void setId(Long id) { this.id = id; } public String getUsername() { return this.username; } public void setUsername(String username) { this.username = username; } public String getName() { return this.name; } public void setName(String name) { this.name = name; } public Short getAge() { return this.age; } public void setAge(Short age) { this.age = age; } public BigDecimal getBalance() { return this.balance; } public void setBalance(BigDecimal balance) { this.balance = balance; } }
定义数据访问接口
package com.suning.provider.respoitory; import com.suning.provider.bean.User; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; /** * @Author 18011618 * @Date 9:50 2018/7/9 * @Function 定义数据访问接口 */ @Repository public interface UserRepository extends JpaRepository<User, Long> { }
定义数据访问controller:
package com.suning.provider.controller; import com.suning.provider.bean.User; import com.suning.provider.respoitory.UserRepository; 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.RestController; /** * @Author 18011618 * @Description 用户提供服务的controller * @Date 9:51 2018/7/9 * @Modify By */ @RestController public class UserController { @Autowired private UserRepository userRepository; //数据访问层接口 @GetMapping("/findUser/{id}") public User findUserById(@PathVariable long id){ return this.userRepository.findOne(id); } }
OK,核心代码就写完了,讲几个区别,就是这里不再使用@controller,@requestmapping,而是使用了@restcontroller,@getmapping,后者的注解功能更强大,也是的开发更加的简单,比如@restcontroller=@controller+@reponsebody,对如果之前开发的话,对于想返回json格式的数据,一般会加一个@responsebody,使用了就不需要了,(后面会对常用的注解进行一个汇总介绍),接下来还缺一个main方法的入口:
package com.suning.provider; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.EnableEurekaClient; /** * @Author 18011618 * @Description * @Date 9:55 2018/7/9 * @Modify By */ @SpringBootApplication //服务启动是以springboot方式 @EnableAutoConfiguration //自动检查相关配置的依赖 @EnableEurekaClient //当前服务注册到eurekaserver上 public class UserServerProviderApplication { public static void main(String[] args) { SpringApplication.run(UserServerProviderApplication.class,args); } }
这里出现了一个新注解:,用来标识当前是eureka的客户端会注册到服务端上,代码全部写完了,接下里看对应的配置文件
server: port: 7900 #配置服务端口号 spring: jpa: #配置对JPA的支持 generate-ddl: false show-sql: true hibernate: ddl-auto: none datasource: platform: h2 schema: classpath:schema.sql data: classpath:data.sql application: name: service-provider-user #配置注册在eukea上的名称 logging: level: root: INFO org.hibernate: INFO org.hibernate.type.descriptor.sql.BasicBinder: TRACE org.hibernate.type.descriptor.sql.BasicExtractor: TRACE #设置日志格式 com.itmuch: DEBUG eureka: client: healthcheck: enabled: true #启动服务健康状态检查 serviceUrl: defaultZone: http://admin:admin@localhost:8080/eureka #服务注册组件的url--单个节点 #defaultZone: http://localhost:8761/eureka,http://localhost:8762/eureka,http://localhost:8763/eureka #服务高可用HA,多个地址以,号分开 instance: prefer-ip-address: true instance-id: ${spring.application.name}:${spring.cloud.client.ipAddress}:${spring.application.instance_id:${server.port}} metadata-map: zone: ABC # eureka可以理解的元数据 lilizhou: BBC # 不会影响客户端行为 lease-renewal-interval-in-seconds: 5 #时间间隔
上面的配置文件内容如下所示
>指定当前服务访问的端口号
>指定当前服务注册的应用名称
>指定当前服务开启健康状态检查
>指定当前服务检查间隔时间
>指定当前服务注册的URL
>指定当前应用支持JPA
看一下对应的数据产生文件和数据表创建文件
insert into user(id,username, name, age, balance) values(1,'user1', '王立', 20, 100.00); insert into user(id,username, name, age, balance) values(2,'user2', '姚杰', 20, 100.00); insert into user(id,username, name, age, balance) values(3,'user3', '帅虎', 20, 100.00); insert into user(id,username, name, age, balance) values(4,'user4', '张军', 20, 100.00);
drop table user if exists; create table user( id bigint generated by default as identity, username varchar(40), name varchar(20), age int(3), balance decimal(10,2), primary key(id) );
OK 这时候,就可以启动,然后重新刷新http://localhost:8080,界面如下
应用里面出现了一个名称,这个名称就是服务提供者的名称,是在配置文件里面指定的,UP代表服务是在线状态,如果是DOWN就代表服务是下线状态,此时就代表一个服务提供者已经成功注册到Eureka上,此时访问http://localhost:7900/findUser/1,会出现如下的结果
数据格式也是我们想要的-json格式,证明@restcontroller的确更强大,OK 至此关于启动eureka和注册服务到eureka上就讲解完了.
四 Eureka的高可用
实际生产环境要注册到的服务成千上万,如果只靠单节点肯定难以支持应用,所以接下来就演示一下,如何配置eureka的高可用.
做点主备工作,在这里因为只有一台电脑,所以需要映射几个伪域名,在C:\Windows\System32\drivers\etc增加下面几行内容:
127.0.0.1 eureka01
127.0.0.1 eureka02
127.0.0.1 eureka03
其实完全可以不用重新建立模块,只不过这里为了更好的区分,所以再重新建立一个叫eureka_server_ha的模块,项目结构如下所示
这里的代码都是直接copy上面的那个模块,所以就不多讲,这里主要讲解一下配置文件
spring: application: name: EUREKA-HA #指定应用的名称 --- server: port: 8761 #服务端口号 spring: profiles: peer1 #激活配置文件标识 eureka: instance: hostname: eureka01 #指定配置文件为peer1的是hostname为eureka01 client: serviceUrl: defaultZone: http://eureka02:8762/eureka/,http://eureka03:8763/eureka/ #把它注册到eureka02和03的服务上 --- server: port: 8762 spring: profiles: peer2 eureka: instance: hostname: eureka02 client: serviceUrl: defaultZone: http://eureka01:8761/eureka/,http://eureka03:8763/eureka/ #把它注册到eureka01和eureka03上 --- server: port: 8763 spring: profiles: peer3 eureka: instance: hostname: eureka03 client: serviceUrl: defaultZone: http://eureka01:8761/eureka/,http://eureka02:8762/eureka/ #把它注册到eureka01和eureka02上 #注意这里因为是要在一台电脑上演示Eureka的HA所以需要通过指定spring.profiles.active这种来进行不同实例的启动 #在生产环境中是不需要配置spring.profiles这个参数项
具体内容可以看红色标注的地方,既然是模拟高可用肯定要模拟多个实例,所以这里就创建了三个实例,然后通过spring.profiles来启动每个实例,然后都是把自己注册到其它的两个服务上,我们来验证一下看一下效果,这里通过Edit Configruation来进行spring.profiles的配置..然后依次启动三个实例。按照下面这个截图来操作
这时候会出现这样的对话框
然后按照这个进行重复操作,分别制定为peer2,peer3,(当然在三个节点没有完全成功启动完,会出现异常,不过没有关系,等3个实例都启动好了,就不会出现异常了.)
这个时候访问http://localhost:8761,出现如下界面
说明eureka01和02,03成了副本的关系,同时application下面有3个服务实例,代表他们三个都注册到这个上面,
此时换http://locahost:8762节点,出现如下界面
再访问http://localhost:8763/ 出现如下界面
下面修改一下user_server_provider的配置文件,其它都不需要修改,只需要修改一行:
就是把单一的服务注册节点改成了多个节点(多个节点用逗号进行分割..),这个时候访问http://localhost:7900/findUser/2
结果还是正常的,
可是到这里还是没有办法演示高可用(后面再讲解ribbon的时候会进行详细演示)
& Eurkea+Server Provider+ Server Consumer整合
为了整合,需要再创建一个服务消费者,通过服务消费者来调用服务提供者,建立一个模块叫user_server_consumer,项目结构如下
看一下pom.xml的内容
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>spring-cloud</artifactId> <groupId>com.suning.cloud</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>user_server_consumer</artifactId> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <!--启动WEB--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--集成EUREKA-SERVER--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka</artifactId> </dependency> <!--系统自省和监控的集成功能--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> </dependencies> </project>
以及对应的java类
package com.user.server.consumer.bean; import java.math.BigDecimal; /** * @Author 18011618 * @Date 16:11 2018/7/9 * @Function */ public class User { private Long id; private String username; private String name; private Short age; private BigDecimal balance; public Long getId() { return this.id; } public void setId(Long id) { this.id = id; } public String getUsername() { return this.username; } public void setUsername(String username) { this.username = username; } public String getName() { return this.name; } public void setName(String name) { this.name = name; } public Short getAge() { return this.age; } public void setAge(Short age) { this.age = age; } public BigDecimal getBalance() { return this.balance; } public void setBalance(BigDecimal balance) { this.balance = balance; } }
和请求的controller
package com.user.server.consumer.controller; import com.user.server.consumer.bean.User; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; /** * @Author 18011618 * @Date 16:12 2018/7/9 * @Function */ @RestController public class UserInfoController { //这里注入一个http简化请求的模版 //且要在main方法的入口加上一个@bean 否则在这里直接使用会报找不到这个resttempleate错误 @Autowired private RestTemplate restTemplate; @Value("${user.userServicePath}") private String userServicePath; @GetMapping("/getUser/{id}") public User findById(@PathVariable Long id) { //简化http模版请求 return this.restTemplate.getForObject(this.userServicePath + id, User.class); } }
这里有一个新的知识点讲解一下,可能大多数人在开发http请求的时候,都会写一大堆的代码,其实spring里面已经提供了对应的实现,而且非常简单,只需要直接调用API即可,只需要开发者注入一个 然后调用对应的方法,
注意上面的那个
userServicePath 是来源于配置文件中提供的参数,指定服务者提供的URL
,那么就看一下配置文件的内容
spring: application: name: service-consumer-movie #服务应用名称 server: port: 7901 #服务端口号 user: userServicePath: http://localhost:7900/findUser/ #服务提供者的url eureka: client: healthcheck: enabled: true #启动服务健康状态的检查 serviceUrl: defaultZone:http://localhost:8761/eureka,http://localhost:8762/eureka,http://localhost:8763/eureka #注册服务组件的url instance: prefer-ip-address: true #使用ip前缀
主要有以下内容
1 指定自身服务应用的名称以及端口号
2 指定服务提供者的URL
3 指定启动当前服务健康状态的检查
4 指定当前服务注册到服务组件上-url
ok 最后再编写一个启动类
package com.user.server.consumer; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.EnableEurekaClient; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate; /** * @Author 18011618 * @Description * @Date 16:13 2018/7/9 * @Modify By */ @EnableAutoConfiguration @SpringBootApplication @EnableEurekaClient public class UserServerConsumerApplication { //这里还要使用@Bean注解来注入到启动类中 否则会保错 //required a bean of type 'org.springframework.web.client.RestTemplate' that @Bean public RestTemplate restTemplate() { return new RestTemplate();//封装http的请求的模版 } public static void main(String[] args) { SpringApplication.run(UserServerConsumerApplication.class,args); } }
注意这里的启动类和之前的不一样,因为它需要加入一个@Bean的注解来注入一个resttemplate模板,如果这里不注入的话会报红色标注的错误.接下来开始测试效果: 访问http://localhost:7901/getUser/1,效果如下
证明服务消费者成功调用了服务提供者,再看一下服务注册情况,访问http://localhost:8761,界面如下
到这里服务提供者+服务注册中心+服务消费者就打通了,最后再强调一下细节点
1 main方法入口类的编写位置
>> 需要有包的存在,否则会保存,因为springboot会扫描包路径
>>最好把它放在所有子包的外面,比如下面
不要放在子包里面
六 Eureka源码分析
上面把eureka怎么用的讲的比较详细,但是所谓知其然要只其所以然,如果要真正理解eukeka机制,看源码是必须的,所以下面就分析它的核心源码.其实springcloud的很多组件都不是spring的自身,比如eureka,hystrix等都是基于netflix等,spingcloud只是集成和扩展,springcloud通过和进行server和client的管理
,从client端开始吧,整个代码并不多
& 服务获取:
通过上面的例子应该知道注册一个服务需要使用注解
EnableEurekaClient,那么接下来看一下这个注解的源码,
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @EnableDiscoveryClient public @interface EnableEurekaClient { }
可以发现该注解还引用了一个其它注解,那么就看一下它的源码,
/** * Annotation to enable a DiscoveryClient implementation. * @author Spencer Gibb */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @Import(EnableDiscoveryClientImportSelector.class) public @interface EnableDiscoveryClient { /** * If true, the ServiceRegistry will automatically register the local server. */ boolean autoRegister() default true; }
通过上面注解的注释,可以知道该注解是标致开启一个D的实现,那么就继续搜索一下这个类的源码,这个类不是springcloud,而是com.netflix.discovery包下面的,通过搜索DiscoveryClient
,我们可以发现有一个类和一个接口。通过梳理可以得到如下图的关系:
其中,左边的org.springframework.cloud.client.discovery.DiscoveryClient
是Spring Cloud的接口,它定义了用来发现服务的常用抽象方法,而org.springframework.cloud.netflix.eureka.EurekaDiscoveryClient
是对该接口的实现,从命名来就可以判断,它实现的是对Eureka发现服务的封装。所以EurekaDiscoveryClient
依赖了Eureka的com.netflix.discovery.EurekaClient
接口,EurekaClient
继承了LookupService
接口,他们都是Netflix开源包中的内容,它主要定义了针对Eureka的发现服务的抽象方法,而真正实现发现服务的则是Netflix包中的com.netflix.discovery.DiscoveryClient
类。
那么,我们就看看来详细看看DiscoveryClient
类,类的注释如下:
1 表明该类是和eurekaserver进行协助通信
2 通过该类向eurekaserver进行服务的注册
3 通过该类向eurekaserver进行服务的续约
4 通过该类向eurekaserver进行服务的解约
5 通过该类向eurekaserver进行服务列表的获取,OK 下面就具体分析每一个方法
在具体研究Eureka Client具体负责的任务之前,我们先看看对Eureka Server的URL列表配置在哪里。根据我们配置的属性名:eureka.client.serviceUrl.defaultZone
,通过serviceUrl
我们找到该属性相关的加载属性,但是在SR5版本中它们都被@Deprecated
标注了,并在注视中可以看到@link
到了替代类com.netflix.discovery.endpoint.EndpointUtils
,我们可以在该类中找到下面这个函数:
public static Map<String, List<String>> getServiceUrlsMapFromConfig(EurekaClientConfig clientConfig, String instanceZone, boolean preferSameZone) { Map<String, List<String>> orderedUrls = new LinkedHashMap<>(); String region = getRegion(clientConfig); String[] availZones = clientConfig.getAvailabilityZones(clientConfig.getRegion()); if (availZones == null || availZones.length == 0) { availZones = new String[1]; availZones[0] = DEFAULT_ZONE; } logger.debug("The availability zone for the given region {} are {}", region, Arrays.toString(availZones)); int myZoneOffset = getZoneOffset(instanceZone, preferSameZone, availZones); String zone = availZones[myZoneOffset]; List<String> serviceUrls = clientConfig.getEurekaServerServiceUrls(zone); if (serviceUrls != null) { orderedUrls.put(zone, serviceUrls); } int currentOffset = myZoneOffset == (availZones.length - 1) ? 0 : (myZoneOffset + 1); while (currentOffset != myZoneOffset) { zone = availZones[currentOffset]; serviceUrls = clientConfig.getEurekaServerServiceUrls(zone); if (serviceUrls != null) { orderedUrls.put(zone, serviceUrls); } if (currentOffset == (availZones.length - 1)) { currentOffset = 0; } else { currentOffset++; } } if (orderedUrls.size() < 1) { throw new IllegalArgumentException("DiscoveryClient: invalid serviceUrl specified!"); } return orderedUrls; }
上面这个方法主要包含了region和zone,serverUrls的获取,下面分别来分析,看第一个子
> getRegion方法,
public static String getRegion(EurekaClientConfig clientConfig) { String region = clientConfig.getRegion(); if (region == null) { region = DEFAULT_REGION; } region = region.trim().toLowerCase(); return region; }
通过从配置中获取到一个region返回,如果region为空的话,就返回一个默认的region,所以由此可以知道,对于一个服务只能属于一个唯一的region.
>getAvaliableZones方法:它是基于EurekaClientConfigBean的实现
函数,我们可以知道当我们没有特别为Region配置Zone的时候,将默认采用defaultZone,这也是我们之前配置参数eureka.client.serviceUrl.defaultZone
的由来。若要为应用指定Zone,我们可以通过eureka.client.availability-zones
属性来进行设置。从该函数的return
内容,我们可以Zone是可以有多个的,并且通过逗号分隔来配置。由此,我们可以判断Region与Zone是一对多的关系,源码如下
public String[] getAvailabilityZones(String region) { String value = (String)this.availabilityZones.get(region); if (value == null) { value = "defaultZone"; } return value.split(","); }
>getEurekaServerServiceUrls方法:也是基于EurekaClientConfigBean实现的,看一下方法对应的源码
public List<String> getEurekaServerServiceUrls(String myZone) { String serviceUrls = (String)this.serviceUrl.get(myZone); if (serviceUrls == null || serviceUrls.isEmpty()) { serviceUrls = (String)this.serviceUrl.get("defaultZone"); } if (!StringUtils.isEmpty(serviceUrls)) { String[] serviceUrlsSplit = StringUtils.commaDelimitedListToStringArray(serviceUrls); List<String> eurekaServiceUrls = new ArrayList(serviceUrlsSplit.length); String[] var5 = serviceUrlsSplit; int var6 = serviceUrlsSplit.length; for(int var7 = 0; var7 < var6; ++var7) { String eurekaServiceUrl = var5[var7]; if (!this.endsWithSlash(eurekaServiceUrl)) { eurekaServiceUrl = eurekaServiceUrl + "/"; } eurekaServiceUrls.add(eurekaServiceUrl); } return eurekaServiceUrls; } else { return new ArrayList(); } }
总结:当客户端在服务列表中选择实例进行访问时,对于Zone和Region遵循这样的规则:优先访问同自己一个Zone中的实例,其次才访问其他Zone中的实例。通过Region和Zone的两层级别定义,配合实际部署的物理结构,我们就可以有效的设计出区域性故障的容错集群。
& 服务注册
服务注册是基于DiscoveryClient里面的,该方法是执行所有的任务调度,重点看一下这个方法,
if (clientConfig.shouldFetchRegistry()) { // registry cache refresh timer int registryFetchIntervalSeconds = clientConfig.getRegistryFetchIntervalSeconds(); int expBackOffBound = clientConfig.getCacheRefreshExecutorExponentialBackOffBound(); scheduler.schedule( new TimedSupervisorTask( "cacheRefresh", scheduler, cacheRefreshExecutor, registryFetchIntervalSeconds, TimeUnit.SECONDS, expBackOffBound, new CacheRefreshThread() ), registryFetchIntervalSeconds, TimeUnit.SECONDS); }
初始化注册缓存刷新的定时器,
if (clientConfig.shouldRegisterWithEureka()) { int renewalIntervalInSecs = instanceInfo.getLeaseInfo().getRenewalIntervalInSecs(); int expBackOffBound = clientConfig.getHeartbeatExecutorExponentialBackOffBound(); logger.info("Starting heartbeat executor: " + "renew interval is: " + renewalIntervalInSecs); // Heartbeat timer scheduler.schedule( new TimedSupervisorTask( "heartbeat", scheduler, heartbeatExecutor, renewalIntervalInSecs, TimeUnit.SECONDS, expBackOffBound, new HeartbeatThread() ), renewalIntervalInSecs, TimeUnit.SECONDS); // InstanceInfo replicator instanceInfoReplicator = new InstanceInfoReplicator( this, instanceInfo, clientConfig.getInstanceInfoReplicationIntervalSeconds(), 2); // burstSize statusChangeListener = new ApplicationInfoManager.StatusChangeListener() { @Override public String getId() { return "statusChangeListener"; } @Override public void notify(StatusChangeEvent statusChangeEvent) { if (InstanceStatus.DOWN == statusChangeEvent.getStatus() || InstanceStatus.DOWN == statusChangeEvent.getPreviousStatus()) { // log at warn level if DOWN was involved logger.warn("Saw local status change event {}", statusChangeEvent); } else { logger.info("Saw local status change event {}", statusChangeEvent); } instanceInfoReplicator.onDemandUpdate(); } }; if (clientConfig.shouldOnDemandUpdateStatusChange()) { applicationInfoManager.registerStatusChangeListener(statusChangeListener); } instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds()); } else { logger.info("Not registering with Eureka server per configuration"); }心跳监测定时器,最重要的是这一行代码,
instanceInfoReplicator = new InstanceInfoReplicator( this, instanceInfo, clientConfig.getInstanceInfoReplicationIntervalSeconds(), 2); // burstSize
这个实例里面有一个很重要的方法:start方法,最终会调用run方法(它本身就是一个线程而已),好看一下run方法源码
红色标注的才是我们需要的,在这里会调用register方法,好继续回来看discoveryclient的对应方法:
这里可以看出原来client和server的通信是基于http请求,最终的register的请求是基于
& 服务续约:renew方法
该方法还是在DiscoveryClient中
那么客户端的请求,服务端是如何处理的?服务端的代码也是比较少的:
这里按类来讲解:
:接受客户端的请求,然后调用其它组件进行处理,返回结果给客户端
:这个类很关键,它的功能就是把eureka和spring进行了很好的整合,从这里也可以发现一个本质的问题,eureka是一个纯天然的servlet请求,springboot通过内嵌的embend tomcat服务容器来启动它
:执行服务的注册,服务的续约,服务的解约,但是它设计是基于模板方法的,所以真正的实现是在父类中
:实现了服务的注册,续约,解约-具体可以看源码。
上面这些代码都是有注册中心来调用的:位于com.netflix.eureka.sources.ApplicationResource
通过addInstance方法来实现注册的
@POST @Consumes({"application/json", "application/xml"}) public Response addInstance(InstanceInfo info, @HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication) { logger.debug("Registering instance {} (replication={})", info.getId(), isReplication); // validate that the instanceinfo contains all the necessary required fields if (isBlank(info.getId())) { return Response.status(400).entity("Missing instanceId").build(); } else if (isBlank(info.getHostName())) { return Response.status(400).entity("Missing hostname").build(); } else if (isBlank(info.getAppName())) { return Response.status(400).entity("Missing appName").build(); } else if (!appName.equals(info.getAppName())) { return Response.status(400).entity("Mismatched appName, expecting " + appName + " but was " + info.getAppName()).build(); } else if (info.getDataCenterInfo() == null) { return Response.status(400).entity("Missing dataCenterInfo").build(); } else if (info.getDataCenterInfo().getName() == null) { return Response.status(400).entity("Missing dataCenterInfo Name").build(); } // handle cases where clients may be registering with bad DataCenterInfo with missing data DataCenterInfo dataCenterInfo = info.getDataCenterInfo(); if (dataCenterInfo instanceof UniqueIdentifier) { String dataCenterInfoId = ((UniqueIdentifier) dataCenterInfo).getId(); if (isBlank(dataCenterInfoId)) { boolean experimental = "true".equalsIgnoreCase(serverConfig.getExperimental("registration.validation.dataCenterInfoId")); if (experimental) { String entity = "DataCenterInfo of type " + dataCenterInfo.getClass() + " must contain a valid id"; return Response.status(400).entity(entity).build(); } else if (dataCenterInfo instanceof AmazonInfo) { AmazonInfo amazonInfo = (AmazonInfo) dataCenterInfo; String effectiveId = amazonInfo.get(AmazonInfo.MetaDataKey.instanceId); if (effectiveId == null) { amazonInfo.getMetadata().put(AmazonInfo.MetaDataKey.instanceId.getName(), info.getId()); } } else { logger.warn("Registering DataCenterInfo of type {} without an appropriate id", dataCenterInfo.getClass()); } } } registry.register(info, "true".equals(isReplication)); return Response.status(204).build(); // 204 to be backwards compatible }
红色标注的很关键,因为它开始调用真正的注册处理实例来实现注册,到此为止就把eukeka整体知识都讲完了
总结:目前eureka还只是内嵌的服务,可扩展性或者性能来说还是有待提升,如果它未来能够作为独立的服务可以部署的话,那样
对我们在生产环境中使用的风险性会更低,目前其实推荐使用Consul组件,这个后面也会介绍到的.