当前位置 博文首页 > m0_53222768的博客:63.SpringCloud
@一贤爱吃土豆
微服务架构样式
https://martinfowler.com/articles/microservices.html
Eureka 是 Netflix 出品的用于实现服务注册和发现的工具。 Spring Cloud 集成了 Eureka,并提供了开箱即用的支持。其中, Eureka 又可细分为 Eureka Server 和 Eureka Client。
上图是基于集群配置的eureka;
处于不同节点的eureka通过Replicate进行数据同步
Application Service为服务提供者
Application Client为服务消费者
Make Remote Call完成一次服务调用
Eureka原理
服务启动后向Eureka注册,Eureka Server会将注册信息向其他Eureka Server进行同步,当服务消费者要调用服务提供者,则向服务注册中心获取服务提供者地址,然后会将服务提供者地址缓存在本地,下次再调用时,则直接从本地缓存中取,完成一次调用。
当服务注册中心Eureka Server检测到服务提供者因为宕机、网络原因不可用时,则在服务注册中心将服务置为DOWN状态,并把当前服务提供者状态向订阅者发布,订阅过的服务消费者更新本地缓存。
服务提供者在启动后,周期性(默认30秒)向Eureka Server发送心跳,证明当前服务是可用状态。Eureka Server在一定的时间(默认90秒)未收到客户端的心跳,则认为服务宕机,注销该实例。
<packaging>pom</packaging>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Greenwich.SR5</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
@SpringBootApplication
@EnableEurekaServer
public class EurekaApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaApplication.class,args);
}
}
server:
port: 8761 # 端口号
eureka:
instance:
hostname: localhost # localhost
client:
# 当前的eureka服务是单机版的
registerWithEureka: false
fetchRegistry: false
serviceUrl:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
@SpringBootApplication
@EnableEurekaClient
public class CustomerApplication {
public static void main(String[] args) {
SpringApplication.run(CustomerApplication.class,args);
}
}
# 指定Eureka服务地址
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka
#指定服务的名称
spring:
application:
name: CUSTOMER
@Autowired
private EurekaClient eurekaClient;
@GetMapping("/customer")
public String customer(){
//1. 通过eurekaClient获取到SEARCH服务的信息
InstanceInfo info = eurekaClient.getNextServerFromEureka("SEARCH", false);
//2. 获取到访问的地址
String url = info.getHomePageUrl();
System.out.println(url);//http://localhost:8082/search
//3. 通过restTemplate访问
String result = restTemplate.getForObject(url + "/search", String.class);
//4. 返回
return result;
}
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// 忽略掉/eureka/**
http.csrf().ignoringAntMatchers("/eureka/**");
super.configure(http);
}
}
# 指定用户名和密码
spring:
security:
user:
name: root
password: root
eureka:
client:
service-url:
defaultZone: http://用户名:密码@localhost:8761/eureka
server:
port: 8001
spring:
application:
name: eureka-server # 服务器域名
eureka:
client:
fetch-registry: false
register-with-eureka: false
service-url:
#集群的情况下,服务端之间要互相注册,指向对方,多个地址用逗号隔开
defaultZone: http://eureka8002.com:8002/eureka,http://eureka8003.com:8003/eureka
instance:
instance-id: eureka8001.com
server:
port: 8002
spring:
application:
name: eureka-server2
eureka:
client:
fetch-registry: false
register-with-eureka: false
service-url:
defaultZone: http://eureka8001.com:8001/eureka,http://eureka8003.com:8003/eureka
instance:
instance-id: eureka8002.com
server:
port: 8003
spring:
application:
name: eureka-server3
eureka:
client:
fetch-registry: false
register-with-eureka: false
service-url:
defaultZone: http://eureka8001.com:8001/eureka,http://eureka8002.com:8002/eureka
instance:
instance-id: eureka8003.com
集群后三台Eureka服务的IP都不一样,所以为了方便在本地测试,可以在hosts文件中做域名映射,把三台服务的ip都映射成127.0.0.1。hosts文件路劲如下:
C:\Windows\System32\drivers\etc
127.0.0.1 eureka8001.com
127.0.0.1 eureka8002.com
127.0.0.1 eureka8003.com
发布的每一个服务都应该注册到所有的Eureka中,所以每一个服务中都要写三个Eureka服务地址。
server:
port: 8082
spring:
application:
name: provider-server
eureka:
client:
service-url:
# 这里写三台Eureka地址
defaultZone: http://eureka8001.com:8001/eureka,http://eureka8002.com:8002/eureka,http://eureka8003.com:8003/eureka
搭建Eureka高可用准备多台Eureka采用了复制的方式,删除iml和target文件,并且修改pom.xml中的项目名称,再给父工程添加一个module,让服务注册到多台Eureka
eureka:
client:
service-url:
defaultZone: http://root:root@localhost:8761/eureka,http://root:root@localhost:8762/eureka
让多台Eureka之间相互通讯
eureka:
client:
registerWithEureka: true # 注册到Eureka上
fetchRegistry: true # 从Eureka拉取信息
serviceUrl:
defaultZone: http://root:root@localhost:8762/eureka/
eureka:
instance:
lease-renewal-interval-in-seconds: 30 #心跳的间隔
lease-expiration-duration-in-seconds: 90 # 多久没发送,就认为你宕机了
eureka:
client:
registry-fetch-interval-seconds: 30 # 每隔多久去拉取服务注册信息
eureka:
server:
enable-self-preservation: true # 开启自我保护机制
instance:
lease-expiration-duration-in-seconds: 2 #每间隔1s,向服务端发送一次心跳,证明自己依然”存活“
lease-renewal-interval-in-seconds: 1 #告诉服务端,如果我2s之内没有给你发心跳,就代表我“死”了,将我踢出掉
如果选择CP,保证了一致性,可能会造成你系统在一定时间内是不可用的,如果你同步数据的时间比较长,造成的损失大。
Eureka就是一个AP的效果,高可用的集群,Eureka集群是无中心,Eureka即便宕机几个也不会影响系统的使用,不需要重新的去推举一个master,也会导致一定时间内数据是不一致。
总结:
以上可以知道分区容错性(P)主要代表网络波动产生的错误,这是不可避免的,且这个三个模式不可兼得,所以目前就只有两种模式:CP和AP模式。
其中CP表示遵循一致性原则,但不能保证高可用性,其中zookeeper作为注册中心就是采用CP模式,因为zookeeper有过半节点不可以的话整个zookeeper将不可用。
AP表示遵循于可用性原则,例如Eureka作为注册中心用的是AP模式,因为其为去中心化,采用你中有我我中有你的相互注册方式,只要集群中有一个节点可以使用,整个eureka服务就是可用的,但可能会出现短暂的数据不一致问题。
Eureka Server 集群相互之间通过 Replicate 来同步数据,相互之间不区分主节点和从节点,所有的节点都是平等的。在这种架构中,节点通过彼此互相注册来提高可用性,每个节点需要添加一个或多个有效的 serviceUrl 指向其他节点。
如果某台 Eureka Server 宕机,Eureka Client 的请求会自动切换到新的 Eureka Server 节点。当宕机的服务器重新恢复后,Eureka 会再次将其纳入到服务器集群管理之中。当节点开始接受客户端请求时,所有的操作都会进行节点间复制,将请求复制到其它 Eureka Server 当前所知的所有节点中。
另外 Eureka Server 的同步遵循着一个非常简单的原则:只要有一条边将节点连接,就可以进行信息传播与同步。所以,如果存在多个节点,只需要将节点之间两两连接起来形成通路,那么其它注册中心都可以共享信息。每个 Eureka Server 同时也是 Eureka Client,多个 Eureka Server 之间通过 P2P 的方式完成服务注册表的同步。
Eureka Server 集群之间的状态是采用异步方式同步的,所以不保证节点间的状态一定是一致的,不过基本能保证最终状态是一致的。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
@GetMapping("/customer")
public String customer(){
String result = restTemplate.getForObject("http://SEARCH/search", String.class);
//4. 返回
return result;
}
@Bean
public IRule robbinRule(){
return new RandomRule();
}
# 指定具体服务的负载均衡策略
SEARCH: # 编写服务名称
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.WeightedResponseTimeRule # 具体负载均衡使用的类
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
@EnableFeignClients
@FeignClient("SEARCH") // 指定服务名称
public interface SearchClient {
// value -> 目标服务的请求路径,method -> 映射请求方式
@RequestMapping(value = "/search",method = RequestMethod.GET)
String search();
}
@Autowired
private SearchClient searchClient;
@GetMapping("/customer")
public String customer(){
String result = searchClient.search();
return result;
}
SEARCH:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
@RequestMapping(value = "/getUserById/{id}")
public String getUserById(@PathVariable Integer id){
System.out.println("id = [" + id + "]");
return "getUserById:"+id;
}
@RequestMapping(value = "/login")
public String login(String username,String password){
System.out.println("username = [" + username + "], password = [" + password + "]");
return "login:username"+username+":password:"+password;
}
@RequestMapping(value = "/addUserFrom")
public String addUserFrom(@RequestBody User user){ // 表单转json的方式
System.out.println(user);
return "addUserFrom:"+user;
}
@RequestMapping(value = "/addUserJSON")
public String addUserJSON(@RequestBody User user){
System.out.println(user);
return "addUserJSON:"+user;
}
@Autowired
private IProviderService providerService;
@RequestMapping(value = "/getUserById/{id}")
public String getUserById(@PathVariable Integer id){
System.out.println("id = [" + id + "]");
return "consuemr:"+providerService.getUserById(id);
}
@RequestMapping(value = "/login")
public String login(String username,String password){
return "consuemr:"+providerService.login(username,password);
}
@RequestMapping(value = "/addUserFrom")
public String addUserFrom(User user){
System.out.println("user = [" + user + "]");
return "consuemr:"+providerService.addUserFrom(user);
}
@RequestMapping(value = "/addUserJSON")
public String addUserJSON(@RequestBody User user){
System.out.println(user);
return "consuemr:"+providerService.addUserJSON(user);
}
@FeignClient(value = "PROVIDER")
@RequestMapping(value = "/provider")
public interface IProviderService {
@RequestMapping(value = "/hello")
public String hello();
@RequestMapping(value = "/getUserById/{id}") // 这里必须要写属性名称,它不想springmvc那样会自动的把形参作为参数值
public String getUserById(@PathVariable("id") Integer id);
@RequestMapping(value = "/login") // 这里必须要写属性名称,它不想springmvc那样会自动的把形参作为参数值
public String login(@RequestParam("username") String username, @RequestParam("password") String password);
// feign不支持直接传递对象,可以通过这样方式转成json然后传递给服务提供者,服务提供者收到的json字符串
// feign[对象]--》对象转成json--》provider[对象]
@RequestMapping(value = "/addUserFrom",consumes = "application/json")
public String addUserFrom(@RequestBody User user);
@RequestMapping(value = "/addUserJSON")
public String addUserJSON(@RequestBody User user);
}
@Component
public class ProviderServiceImpl implements IProviderService {
@Override
public String hello() {
return "服务出现故障,这里是默认值";
}
@Override
public String getUserById(Integer id) {
return "id";
}
@Override
public String login(String username, String password) {
return "login";
}
@Override
public String addUserFrom(User user) {
return "addUser";
}
@Override
public String addUserJSON(User user) {
return "addUserJSON";
}
}
@FeignClient(value = "PROVIDER",fallback = ProviderServiceImpl.class)
//@RequestMapping(value = "/provider") //开启了hystrix后这个路径要写到下面的方法上面
public interface IProviderService {
@RequestMapping(value = "/provider/hello")
public String hello();
}
# feign和hystrix组件整合
feign:
hystrix:
enabled: true
<Client>
@Component
public class ProviderFactory implements FallbackFactory<IProviderService> {
@Autowired
private ProviderServiceImpl providerService;
@Override
public IProviderService create(Throwable throwable) {
throwable.printStackTrace();; // 打印异常信息
return providerService; // 返回自定义的实现方法
}
}
@FeignClient(value = "PROVIDER",
// fallback = ProviderServiceImpl.class
fallbackFactory = ProviderFactory.class
)
//@RequestMapping(value = "/provider")
public interface IProviderService {
}
详细知识扩展情(以下部分)
在一个基于微服务的应用程序中,您通常需要调用多个微服务完成一个特定任务。这些调用默认是使用相同的线程来执行调用的,这些线程Java容器为处理所有请求预留的。在高服务器请求的情况下,一个性能较低的服务会“霸占”java容器中绝大多数线程,而其它性能正常的服务的请求则需要等待线程资源的释放。最后,整个java容器会崩溃。
- Hystrix的线程池(默认),当用户请求服务A和服务I的时候,tomcat的线程(图中蓝色箭头标注)会将请求的任务交给服务A和服务I的内部线程池里面的线程(图中橘色箭头标注)来执行,tomcat的线程就可以去干别的事情去了,当服务A和服务I自己线程池里面的线程执行完任务之后,就会将调用的结果返回给tomcat的线程,从而实现资源的隔离,当有大量并发的时候,服务内部的线程池的数量就决定了整个服务的并发度,例如服务A的线程池大小为10个,当同时有12请求时,只会允许10个任务在执行,其他的任务被放在线程池队列中,或者是直接走降级服务,此时,如果服务A挂了,就不会造成大量的tomcat线程被服务A拖死,服务I依然能够提供服务。整个系统不会受太大的影响。
- 信号量,信号量的资源隔离只是起到一个开关的作用,例如,服务X的信号量大小为10,那么同时只允许10个tomcat的线程(此处是tomcat的线程,而不是服务X的独立线程池里面的线程)来访问服务X,其他的请求就会被拒绝,从而达到限流保护的作用。
Hystrix的线程池的配置(具体配置的name属性要查看HystrixCommandProperties类)