微服务之路(六)spring cloud netflix eureka

前言

本章将主要讲解服务的注册与发现,我们通过Eureka Server简单Demo讲解相关内容。

主要议题

  • 前微服务时代

  • 高可用架构

  • Eureka服务端

  • Eureka客户端

  • 问题总结

主体内容

一、前微服务时代

1.概念

  • 分布式系统基本组成
    • 服务提供方(Porvider)
    • 服务消费放(Consumer)
    • 服务注册中心(Registry)
    • 服务路由(Router)
    • 服务代理(Broker)
    • 通讯协议(Protocol)

2.传统的服务治理

通讯协议介质表现:

XML-RPC->XML方法的描述、方法的参数->WSDL(WebService定义语言)

WebService->SOAP(HTTP\SMTP)->文本协议(头部分、体部分)

REST->JSON/XML(Schema:类型、结构)->文本协议(HTTP Header、Body)

W3C Schema:xsd:string原子类型,自定义,自由组合原子类型

那么Dubbo一般采用:Hession、Java Serialization(二进制),跨语言不变,一般通过Client(Java C++)

二进制的性能是非常好(字节流、免去字符流(字符编码),免去了字符解释,对机器友好,对人不友好)

序列化:把编程语言数据结构转换成字节流、反序列化:字节流并语言的数据结构(原生类型的组合)

二、高可用架构

  • 基本原则
    • 消除单点失败
    • 可靠性交迭
    • 故障探测
  • 可用性比率计算

URI:统一资源定位符,用于网络资源定位,网络是通讯方式,资源是需要消费媒介,定位是路由。

Proxy:一般性代理,路由(Nginx反向代理)

Broker:包括路由,并且算法,老的称谓(MOM)

Message Broker:消息路由、消息管理(消息是否可达)

可用性比率计算

可用性比率:通过时间来计算(一年或者一月)

比如:一年99.99%

可用时间:365243600*99.99%

不可用时间:365243600*0.01%=3153.6秒<1小时

不可用时间:1小时推算一年 1/24/365=0.01%

单台机器可用性比率:99%

两台机器可用性比率就是:99%*99%

N台机器可用性比率就是:99%^N

结论:增加机器可以提高可用性,增加服务调用会降低可靠性,同时降低了可用性。

三、Eureka服务端

1.构建服务端项目

首先,我们去http://start.spring.io构建项目。构建完毕,IDEA导入该项目。

2.服务端基本配置

首先,配置application.properties文件,具体解释已经在注释中。

#Eureka Server 应用名称
spring.application.name = spring-cloud-eureka-server
#Eureka Server服务端口
server.port=9090
#取消服务器自我注册
eureka.client.register-with-eureka=false
#注册中心的服务器,没必要再去检索服务
eureka.client.fetch-registry=false
#Eureka Server服务URL,用于客户端注册和服务发现
eureka.client.serviceUrl.defaultZone=http://localhost:${eureka.server.port}/eureka

然后在启动类上加上@EnableEurekaServer,激活Eureka Server。启动项目访问http://localhost:9090,以下就是Eureka注册中心图示:

我们稍微解读一下图中的组件意义:

  • System Status(系统状态)
    • Data Source:图中为default,代表默认的注册中心,代表Eureka还有其他形式的注册中心。
    • Current time:顾名思义,当前时间。
    • Uptime:已活动时间。
    • Lease expiration enabled :是否启用租约过期 , 自我保护机制关闭时,该值默认是true, 自我保护机制开启之后为false。
    • Renews threshold : 每分钟最少续约数
    • Renews (last min) : 最后一分钟的续约数量(不含当前,1分钟更新一次)
  • DS Replicas (这里表示这个地址是这个Eureka Server相邻节点,互为一个集群)
  • Instances currently registered with Eureka(表示各个微服务注册到这个服务上的实例信息)
  • General Info(总体信息)
    • total-avail-memory : 总共可用的内存
    • environment : 环境名称,默认test
    • num-of-cpus : CPU的个数
    • current-memory-usage : 当前已经使用内存的百分比
    • server-uptime : 服务启动时间
    • registered-replicas : 相邻集群复制节点
    • unavailable-replicas :不可用的集群复制节点,如何确定不可用? 主要是server1 向 server2和server3 发送接口查询自身的注册信息,
      如果查询不到,则默认为不可用 , 也就是说如果Eureka Server自身不作为客户端注册到上面去,则相邻节点都会显示为不可用。
    • available-replicas :可用的相邻集群复制节点
  • Last 1000 since startup
    • Last 1000 cancelled leases(最后1000个取消的租约)
    • Last 1000 newly registered leases(最后1000个新注册的租约)

系统在三种情况下会出现红色加粗的字体提示,了解一下吧:

(1)在配置上,自我保护机制关闭
RENEWALS ARE LESSER THAN THE THRESHOLD. THE SELF PRESERVATION MODE IS TURNED OFF.THIS MAY NOT PROTECT INSTANCE EXPIRY IN CASE OF NETWORK/OTHER PROBLEMS.

(2)自我保护机制开启了
EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY’RE NOT. RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE INSTANCES ARE NOT BEING EXPIRED JUST TO BE SAFE

(3)在配置上,自我保护机制关闭了,但是一分钟内的续约数没有达到85% , 可能发生了网络分区,会有如下提示
THE SELF PRESERVATION MODE IS TURNED OFF.THIS MAY NOT PROTECT INSTANCE EXPIRY IN CASE OF NETWORK/OTHER PROBLEMS

补充:Eureka服务器一般不需要自我注册,也不需要注册其他服务器,也不需要检索服务。但是这两个设置并不是影响作为服务器的使用,不过建议关闭是为了不必要的异常堆栈,减少错误的干扰(比如:系统异常和业务异常)

四、Eureka客户端

服务发现

Spring Cloud 客户端

  • NetFlix Eureka Client
    • 依赖:spring-cloud-starter-eureka
    • API
      • EurekaClient
      • ServiceInstance
  • Spring Cloud Commons

服务注册

Spring Cloud

  • 客户端
    • Netflix Eureka Client
      • 激活:@EnableEurekaClient
      • 健康指标:HealthIndicator
  • 服务端
    • NetFlix Eureka Server
      • 激活:@EnableEurekaServer

1.构建客户端项目

当我们搞定服务端后,同样通过http://start.spring.io构建客户端项目,记得变更依赖和名称。

记得我们之前讲过springboot多模块创建。这里就要应用到这个功能。

(1)那么首先我们需要把初始的packaing改为pom,即所谓的父项目。如下:

...
<groupId>com.example</groupId>
<artifactId>springcloud-eureka-client</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>pom</packaging><!-这里-->
<name>springcloud-eureka-client</name>
<description>Demo project for Spring Boot</description>
...

(2)接下来,我们可以删除src文件夹了,然后需要右键项目建立Module->user-api。

(3)接着在Module(user-api)中创建包com.gupao.domain,里面创建用户模型User.java

/**
 * @ClassName
 * @Describe 用户的模型 API
 * @Author 66477
 * @Date 2020/5/2522:28
 * @Version 1.0
 */
public class User {
    private Long id;
    private String name;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
    
     @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}

(4)创建service层,创建业务接口UserService.java。内容如下:

import com.gupao.domain.User;
import java.util.Collection;

/**
 * @ClassName
 * @Describe TODO
 * @Author 66477
 * @Date 2020/5/2620:40
 * @Version 1.0
 */
public interface UserService {

    /**
     * 保存用户
     * @param user
     * @return 如果保存成功的话,返回<code>true</code>,否则返回<code>false</code>
     */
    boolean save(User user);

    /**
     * 查询所有的用户对象
     * @return 不会返回<code>null</code>
     */
    Collection<User> findAll();
}

(5)然后右键项目建立新的Module->user-consumer,在模块下建立包com.gupao.user.web.controller,下面是UserRestApiController.java类。

import java.util.Collection;

/**
 * @ClassName
 * @Describe 用户服务 Rest API
 * @Author 66477
 * @Date 2020/5/2621:37
 * @Version 1.0
 */
 @RestController
public class UserRestApiController {

    @Autowired
    private UserService userService;

    /**
     * 保存用户
     * @param name 请求参数名为“name"的数据
     * @return 如果保存成功的话,返回{@link User},否则返回<code>null</code>
     */
    @PostMapping("/user/save")
    public User saveUser(@RequestParam String name){
        User user = new User();
        user.setName(name);
        if(userService.save(user)){
            return user;
        }else{
            return null;
        }
    }

    /**
     * 罗列所有的用户数据
     * @return 所有的用户数据
     */
    @GetMapping
    public Collection<User> list(){
        return userService.findAll();
    }
}

记得引入user-api依赖:

<dependencies>
    <dependency>
        <groupId>com.example</groupId>
        <artifactId>user-api</artifactId>
        <version>${project.version}</version>
    </dependency>
</dependencies>

(6)右键项目创建Module->user-provider,即服务提供者。该模块下创建包:com.gupao.user.repository/service/config/controller,创建完毕后。

a.首先关闭spring security,这里同样演示spring boot2.0操作。

引入依赖:

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-security</artifactId>
</dependency>

config包中创建类SecurityConfig继承WebSecurityConfigurerAdapter,复写它的configure方法。

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
     
        @Override
        public void configure(WebSecurity web) throws Exception {
            web.ignoring().antMatchers("/**");
        }

b.创建服务提供方的仓储,repository包下创建UserRepository.java类。

import com.gupao.domain.User;
import org.springframework.stereotype.Repository;
import java.util.Collection;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicLong;

/**
 * @ClassName
 * @Describe {@link User 用户} 仓储
 * @Author 66477
 * @Date 2020/5/2622:25
 * @Version 1.0
 */
@Repository
public class UserRepository {
    //为啥用到ConcurrentMap,spring默认controller,service,repository都是单例的,这个成员变量是共享的,所以为了避免线程安全性问题,故采用ConcurrentMap
    private ConcurrentMap<Long,User> repository = new ConcurrentHashMap<>();
    //为啥用到原子性,原理同上
    private static final AtomicLong idGenerator = new AtomicLong();

    public boolean save(User user) {
        Long id =idGenerator.incrementAndGet();
        user.setId(id);
        //putIfAbsent:如果存在的话,它就不存了,防止重复提交
        return repository.putIfAbsent(id,user)==null;
    }

    public Collection<User> findAll() {
        return repository.values();
    }
}

c.创建服务提供方的service,在service包下创建UserServiceImpl类实现对外暴露api中的UserService接口。

import com.gupao.user.resository.UserRepository;
import com.gupao.domain.User;
import com.gupao.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Collection;

/**
 * @ClassName
 * @Describe {@link UserService 用户服务} 提供者实现
 * @Author 66477
 * @Date 2020/5/2622:23
 * @Version 1.0
 */
@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public boolean save(User user) {
        return userRepository.save(user);
    }

    @Override
    public Collection<User> findAll() {
        return userRepository.findAll();
    }
}

d.最后,创建controller层,controller包下创建UserServicePrivoiderRestApiController.java。这个方法是不是有点熟悉,没错,这是消费者的controller层中的方法。从某种角度来说,这是spring脱裤子放p的写法。

import com.gupao.domain.User;
import com.gupao.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.Collection;

/**
 * {@link UserService 用户服务}提供方 Rest API {@link RestController}
 * @ClassName
 * @Describe
 * @Author 66477
 * @Date 2020/5/2622:20
 * @Version 1.0
 */
@RestController
public class UserServicePrivoiderRestApiController {

    @Autowired
    private UserService userService;

    /**
     * 保存用户
     * @param name 请求参数名为“name"的数据
     * @return 如果保存成功的话,返回{@link User},否则返回<code>null</code>
     */
    @PostMapping("/user/save")
    public User saveUser(@RequestParam String name){
        User user = new User();
        user.setName(name);
        if(userService.save(user)){
            System.out.println("UserService服务方:保存用户成功!"+user);
            return user;
        }else{
            return null;
        }
    }

    /**
     * 罗列所有的用户数据
     * @return 所有的用户数据
     */
    @GetMapping("/user/list")
    public Collection<User> list(){
        return userService.findAll();
    }

}

e.别忘了还有该模块下的application.properties还没有配置。

spring.application.name=user-service-provider
#Eureka 注册中心服务器端口
eureka.server.port=9090
# 服务提供方(provider)端口
server.port=7070
#Eureka Server服务URL,用于客户端注册和服务发现
eureka.client.serviceUrl.defaultZone=http://localhost:${eureka.server.port}/eureka

f.配置启动类。

import com.gupao.service.UserService;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

/**
 * @ClassName
 * @Describe {@link UserService 用户服务}引导类
 * @Author 66477
 * @Date 2020/5/2622:52
 * @Version 1.0
 */
@SpringBootApplication
@EnableDiscoveryClient//其中@SpringCloudApplication包含了@EnableCircuitBreaker短路注解,暂时不采用@SpringCloudApplication包含了
public class UserServiceProviderBootstrap {
    public static void main(String[] args) {
        SpringApplication.run(UserServiceProviderBootstrap.class,args);
    }
}

(7)紧接着,我们再看user-consumer模块。

a.在下面继续创建com.gupao.user.config包,在其中首先关闭spring security,这里同样演示spring boot2.0操作。

引入依赖:

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-security</artifactId>
</dependency>

config包中创建类SecurityConfig继承WebSecurityConfigurerAdapter,复写它的configure方法。

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
     
        @Override
        public void configure(WebSecurity web) throws Exception {
            web.ignoring().antMatchers("/**");
        }

b.创建com.gupao.user.service,包下创建代理业务类UserServiceProxy实现UserService接口,采用RestTemplate转发请求。

import com.gupao.domain.User;
import com.gupao.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.Collection;

/**
 * @ClassName
 * @Describe {@link UserService} Proxy实现 代理实现
 * @Author 66477
 * @Date 2020/5/2623:21
 * @Version 1.0
 */
@Service
public class UserServiceProxy implements UserService {
    //user-service-provider应用前缀
    private  static final String PROVIDER_SERVER_URL_PREFIX="http://user-service-provider/";
    //通过REST API代理到服务提供者
    @Autowired
    private RestTemplate restTemplate;

    @Override
    public boolean save(User user) {
       User returnValue = restTemplate.postForObject(PROVIDER_SERVER_URL_PREFIX+"/user/save",user,User.class);
       return returnValue==null?false:true;
    }

    @Override
    public Collection<User> findAll() {
        return restTemplate.getForObject(PROVIDER_SERVER_URL_PREFIX+"/user/list",Collection.class);
    }
}

c.创建启动类。

import com.gupao.service.UserService;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

/**
 * @ClassName
 * @Describe {@link UserService 用户消费} 引导类
 * @Author 66477
 * @Date 2020/5/2622:52
 * @Version 1.0
 */
@SpringBootApplication
@EnableDiscoveryClient//其中@SpringCloudApplication包含了@EnableCircuitBreaker短路注解,暂时不采用@SpringCloudApplication包含了
public class UserServiceConsumerBootstrap {

    public static void main(String[] args) {
        SpringApplication.run(UserServiceConsumerBootstrap.class,args);
    }

    @LoadBalanced//负载均衡
    @Bean
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }
}

d.最后,别忘了application.properties。

spring.application.name=user-service-consumer
#Eureka 注册中心服务器端口
eureka.server.port=9090
# 服务提供方(provider)端口
server.port=8080
#Eureka Server服务URL,用于客户端注册和服务发现
eureka.client.serviceUrl.defaultZone=http://localhost:${eureka.server.port}/eureka

(8)最后,启动Eureka-server项目,启动user-consumer,user-provider。发现http://localhost:9090 eureka界面注册了两个服务。

(9)接下来,我去访问http://localhost:8080/user/save?name=小明,也就是消费者模块的接口。

为啥不让它直接访问provider的接口呢,后面我们会讲到,先简要提一下,以后我们服务提供者端会在内网,消费者端放在公网,这样提供一个对外服务。

现在的关系就是:用户->用户服务消费者Web服务器->Eureka Client->服务提供者Web服务器。其中用户服务消费者Web服务器与Eureka之间采用RestTemplate实现;用户与用户服务消费者Web服务器之间是HTTP。

五、问题总结

1.consul和Ekreka是一样的吗?

解答:提供功能类似,consul功能更强大,广播式服务发现/注册。

2.重启Eureka服务器,客户端应用需要重启吗?

解答:不用,因为客户端在不停地上报信息,不过再Eureka服务器启动过程中,客户端会大量报错。

3.生产环境中,consumer是分别注册多个服务,还是统一放在一起注册成一个服务?权限应该如何处理?

解答:consumer是否要分成多个服务,要根据情况,大多数情况是需要的,根据应用职责划分。权限根据服务方法需要,比如有些敏感操作的话,可以根据不同用户做鉴权。

4.客户端上报的信息存储在哪里?内存中还是数据库中?

解答:都是在内存里面缓存着。Eureka Client并不是所有的服务,而是存放需要的服务。比如:Eureka Server管理了200个应用,每个应用存放100个实例,总体管理20000个实例。客户端根据自己的需要的应用实例。

5.要是其他模块查询列表里面有用到用户信息怎么办呢?是循环调用户接口,还是直接关联用户表呢?怎么实现才好?

解答:用户Api依赖即可。

6.consumer调用Aprovider-a挂了,会自动切换Aprovider-b吗?保证请求可用吗?

解答:当Aprovider-a挂了,会自动切换,不过不一定及时。不及时,服务端可能存在脏数据,或者轮询更新时间未达。

7.一个业务中调用多个service如何保证事务?

解答:需要分布式事务实现(JTA),可是一般的互联网项目,没有这种昂贵的操作。

猜你喜欢

转载自www.cnblogs.com/jmy520/p/12977184.html