SpringCloud微服务治理一(介绍,环境搭建,Eureka)

1.远程调用方式

了解一下微服务的调用方式

1.1.RPC

RPC,即 Remote Procedure Call(远程过程调用),是一个计算机通信协议。 该协议允许运行于一台计算机的程序调用另一台计算机的子程序,而程序员无需额外地为这个交互作用编程。

1.2.Http

Http协议:超文本传输协议,是一种应用层协议。

2.Http客户端工具

2.1 Spring的RestTemplate

Spring提供了一个RestTemplate模板工具类,对基于Http的客户端进行了封装,并且实现了对象与json的序列化和反序列化,非常方便。RestTemplate并没有限定Http的客户端类型,而是进行了抽象,目前常用的3种都有支持:

  • HttpClient
  • OkHttp
  • JDK原生的URLConnection(默认的)

首先在项目中注册一个RestTemplate对象,可以在启动类位置注册:

@SpringBootApplication
public class HttpDemoApplication {

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

    @Bean
    public RestTemplate restTemplate() {
        // 默认的RestTemplate,底层是走JDK的URLConnection方式。
        return new RestTemplate();
    }
}

在测试类中直接@Autowired注入:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = HttpDemoApplication.class)
public class HttpDemoApplicationTests {

    @Autowired
    private RestTemplate restTemplate;

    @Test
    public void httpGet() {
        User user = this.restTemplate.getForObject("http://localhost/hello", User.class);
        System.out.println(user);
    }
}

接下来正式介绍微服务。

3.SpringCloud

3.1.简介

SpringCloud是Spring旗下的项目之一,官网地址:http://projects.spring.io/spring-cloud/

SpringCloud将现在非常流行的一些技术整合到一起,实现了诸如:配置管理,服务发现,智能路由,负载均衡,熔断器,控制总线,集群状态等等功能。其主要涉及的组件包括:

netflix

  • Eureka:注册中心
  • Zuul:服务网关
  • Ribbon:负载均衡
  • Feign:服务调用
  • Hystix:熔断器

接下来,我们就一一学习SpringCloud中的重要组件。

4.微服务场景模拟

首先,我们需要模拟一个服务调用的场景。方便后面学习微服务架构

4.1.服务提供者

我们新建一个项目,对外提供查询用户的服务。

4.1.1.Spring脚手架创建工程

依赖也已经全部自动引入:



    4.0.0

    com.leyou.demo
    user-service-demo
    0.0.1-SNAPSHOT
    jar

    user-service-demo
    Demo project for Spring Boot

    
        org.springframework.boot
        spring-boot-starter-parent
        2.0.1.RELEASE
         
    

    
        UTF-8
        UTF-8
        1.8
    

    
        
            org.springframework.boot
            spring-boot-starter-jdbc
        
        
            org.springframework.boot
            spring-boot-starter-web
        
        
            org.mybatis.spring.boot
            mybatis-spring-boot-starter
            1.3.2
        
        
            mysql
            mysql-connector-java
            runtime
        
        
            org.springframework.boot
            spring-boot-starter-test
            test
        
    

    
        
            
                org.springframework.boot
                spring-boot-maven-plugin
            
        
    

当然,因为要使用通用mapper,所以我们需要手动加一条依赖:


    tk.mybatis
    mapper-spring-boot-starter
    2.0.2

非常快捷啊!

4.1.2.编写代码

添加一个对外查询的接口:

@RestController
@RequestMapping("user")
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping("/{id}")
    public User queryById(@PathVariable("id") Long id) {
        return this.userService.queryById(id);
    }
}

Service:

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    public User queryById(Long id) {
        return this.userMapper.selectByPrimaryKey(id);
    }
}

mapper:

@Mapper
public interface UserMapper extends tk.mybatis.mapper.common.Mapper{
}

实体类:

@Table(name = "tb_user")
public class User implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    // 用户名
    private String userName;

    // 密码
    private String password;

    // 姓名
    private String name;

    // 年龄
    private Integer age;

    // 性别,1男性,2女性
    private Integer sex;

    // 出生日期
    private Date birthday;

    // 创建时间
    private Date created;

    // 更新时间
    private Date updated;

    // 备注
    private String note;

   // 。。。省略getters和setters
}

属性文件,这里我们采用了yaml语法,而不是properties:

server:
  port: 8081
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/mydb01
    username: root
    password: 123
    hikari:
      maximum-pool-size: 20
      minimum-idle: 10
mybatis:
  type-aliases-package: com.leyou.userservice.pojo

4.2.服务调用者

4.2.1.创建工程

与上面类似,需要注意的是,我们调用user-service的功能,因此不需要mybatis相关依赖了。

pom:



    4.0.0

    com.leyou.demo
    user-consumer-demo
    0.0.1-SNAPSHOT
    jar

    user-consumer-demo
    Demo project for Spring Boot

    
        org.springframework.boot
        spring-boot-starter-parent
        2.0.1.RELEASE
         
    

    
        UTF-8
        UTF-8
        1.8
    

    
        
            org.springframework.boot
            spring-boot-starter-web
        
        
        
            com.squareup.okhttp3
            okhttp
            3.9.0
        
    

    
        
            
                org.springframework.boot
                spring-boot-maven-plugin
            
        
    


4.2.2.编写代码

首先在启动类中注册RestTemplate

@SpringBootApplication
public class UserConsumerDemoApplication {

    @Bean
    public RestTemplate restTemplate() {
        // 这次我们使用了OkHttp客户端,只需要注入工厂即可
        return new RestTemplate(new OkHttp3ClientHttpRequestFactory());
    }

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

通过RestTemplate远程查询user-service-demo中的接口:

@Component
public class UserDao {

    @Autowired
    private RestTemplate restTemplate;

    public User queryUserById(Long id){
        String url = "http://localhost:8081/user/" + id;
        return this.restTemplate.getForObject(url, User.class);
    }
}

然后编写user-service,循环查询UserDAO信息:

@Service
public class UserService {

    @Autowired
    private UserDao userDao;

    public List querUserByIds(List ids){
        List users = new ArrayList<>();
        for (Long id : ids) {
            User user = this.userDao.queryUserById(id);
            users.add(user);
        }
        return users;
    }
}

编写controller:

@RestController
@RequestMapping("consume")
public class ConsumerController {

    @Autowired
    private UserService userService;

    @GetMapping
    public List consume(@RequestParam("ids") List ids) {
        return this.userService.queryUserByIds(ids);
    }
}

4.3.问题?

  • 在consumer中,我们把url地址硬编码到了代码中,不方便后期维护
  • consumer需要记忆user-service的地址,如果出现变更,可能得不到通知,地址将失效
  • consumer不清楚user-service的状态,服务宕机也不知道
  • user-service只有1台服务,不具备高可用性
  • 即便user-service形成集群,consumer还需自己实现负载均衡

其实上面说的问题,概括一下就是分布式服务必然要面临的问题:

  • 服务管理
    • 如何自动注册和发现
    • 如何实现状态监管
    • 如何实现动态路由
  • 服务如何实现负载均衡
  • 服务如何解决容灾问题
  • 服务如何实现统一配置

以上的问题,我们都将在SpringCloud中得到答案。

5.Eureka注册中心

5.1.原理图

基本架构:

SpringCloud微服务治理一(介绍,环境搭建,Eureka)_第1张图片
image.png
  • Eureka:就是服务注册中心(可以是一个集群),对外暴露自己的地址
  • 提供者:启动后向Eureka注册自己信息(地址,提供什么服务)
  • 消费者:向Eureka订阅服务,Eureka会将对应服务的所有提供者地址列表发送给消费者,并且定期更新
  • 心跳(续约):提供者定期通过http方式向Eureka刷新自己的状态

5.2.入门案例

5.2.1.编写EurekaServer

接下来我们创建一个项目,启动一个EurekaServer:

依然使用spring提供的快速搭建工具,选择依赖:

SpringCloud微服务治理一(介绍,环境搭建,Eureka)_第2张图片

完整的Pom文件:



    4.0.0

    com.leyou.demo
    eureka-demo
    0.0.1-SNAPSHOT
    jar

    eureka-demo
    Demo project for Spring Boot

    
        org.springframework.boot
        spring-boot-starter-parent
        2.0.1.RELEASE
         
    

    
        UTF-8
        UTF-8
        1.8
        
        Finchley.RC1
    

    
        
        
            org.springframework.cloud
            spring-cloud-starter-netflix-eureka-server
        
    

    
        
            
            
                org.springframework.cloud
                spring-cloud-dependencies
                ${spring-cloud.version}
                pom
                import
            
        
    

    
        
            
                org.springframework.boot
                spring-boot-maven-plugin
            
        
    

    
        
            spring-milestones
            Spring Milestones
            https://repo.spring.io/milestone
            
                false
            
        
    

编写启动类:

@SpringBootApplication
@EnableEurekaServer // 声明这个应用是一个EurekaServer
public class EurekaDemoApplication {

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

编写配置:

server:
  port: 10086 # 端口
spring:
  application:
    name: eureka-server # 应用名称,会在Eureka中显示
eureka:
  client:
    register-with-eureka: false # 是否注册自己的信息到EurekaServer,默认是true
    fetch-registry: false # 是否拉取其它服务的信息,默认是true
    service-url: # EurekaServer的地址,现在是自己的地址,如果是集群,需要加上其它Server的地址。
      defaultZone: http://127.0.0.1:${server.port}/eureka

启动服务,并访问:http://127.0.0.1:10086/eureka

SpringCloud微服务治理一(介绍,环境搭建,Eureka)_第3张图片
SpringCloud微服务治理一(介绍,环境搭建,Eureka)_第4张图片

5.2.2.将user-service注册到Eureka

注册服务,就是在服务上添加Eureka的客户端依赖,客户端代码会自动把服务注册到EurekaServer中。

我们在user-service-demo中添加Eureka客户端依赖:

先添加SpringCloud依赖:



    
        
            org.springframework.cloud
            spring-cloud-dependencies
            Finchley.RC1
            pom
            import
        
    



    
        spring-milestones
        Spring Milestones
        https://repo.spring.io/milestone
        
            false
        
    

然后是Eureka客户端:



    org.springframework.cloud
    spring-cloud-starter-netflix-eureka-client

在启动类上开启Eureka客户端功能

通过添加@EnableDiscoveryClient来开启Eureka客户端功能

@SpringBootApplication
@EnableDiscoveryClient // 开启EurekaClient功能
public class UserServiceDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(UserServiceDemoApplication.class, args);
    }
}

编写配置

server:
  port: 8081
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/mydb01
    username: root
    password: 123
    hikari:
      maximum-pool-size: 20
      minimum-idle: 10
  application:
    name: user-service # 应用名称
mybatis:
  type-aliases-package: com.leyou.userservice.pojo
eureka:
  client:
    service-url: # EurekaServer地址
      defaultZone: http://127.0.0.1:10086/eureka
  instance:
    prefer-ip-address: true # 当调用getHostname获取实例的hostname时,返回ip而不是host名称
    ip-address: 127.0.0.1 # 指定自己的ip信息,不指定的话会自己寻找

注意:

  • 这里我们添加了spring.application.name属性来指定应用名称,将来会作为应用的id使用。
  • 不用指定register-with-eureka和fetch-registry,因为默认是true

重启项目,访问Eureka监控页面http://127.0.0.1:10086/eureka查看

SpringCloud微服务治理一(介绍,环境搭建,Eureka)_第5张图片

我们发现user-service服务已经注册成功了

5.2.3.消费者从Eureka获取服务

接下来我们修改consumer-demo,尝试从EurekaServer获取服务。

方法与消费者类似,只需要在项目中添加EurekaClient依赖,就可以通过服务名称来获取信息了!

1)添加依赖:

先添加SpringCloud依赖:



    
        
            org.springframework.cloud
            spring-cloud-dependencies
            Finchley.RC1
            pom
            import
        
    



    
        spring-milestones
        Spring Milestones
        https://repo.spring.io/milestone
        
            false
        
    

然后是Eureka客户端:



    org.springframework.cloud
    spring-cloud-starter-netflix-eureka-client

2)在启动类开启Eureka客户端

@SpringBootApplication
@EnableDiscoveryClient // 开启Eureka客户端
public class UserConsumerDemoApplication {
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate(new OkHttp3ClientHttpRequestFactory());
    }
    public static void main(String[] args) {
        SpringApplication.run(UserConsumerDemoApplication.class, args);
    }
}

3)修改配置:

server:
  port: 8080
spring:
  application:
    name: consumer # 应用名称
eureka:
  client:
    service-url: # EurekaServer地址
      defaultZone: http://127.0.0.1:10086/eureka
  instance:
    prefer-ip-address: true # 当其它服务获取地址时提供ip而不是hostname
    ip-address: 127.0.0.1 # 指定自己的ip信息,不指定的话会自己寻找

4)修改代码,用DiscoveryClient类的方法,根据服务名称,获取服务实例:

@Service
public class UserService {

    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private DiscoveryClient discoveryClient;// Eureka客户端,可以获取到服务实例信息

    public List queryUserByIds(List ids) {
        List users = new ArrayList<>();
        // String baseUrl = "http://localhost:8081/user/";
        // 根据服务名称,获取服务实例
        List instances = discoveryClient.getInstances("user-service");
        // 因为只有一个UserService,因此我们直接get(0)获取
        ServiceInstance instance = instances.get(0);
        // 获取ip和端口信息
        String baseUrl = "http://"+instance.getHost() + ":" + instance.getPort()+"/user/";
        ids.forEach(id -> {
            // 我们测试多次查询,
            users.add(this.restTemplate.getForObject(baseUrl + id, User.class));
            // 每次间隔500毫秒
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        return users;
    }
}

5.4.Eureka进阶

5.4.1.高可用的Eureka Server

Eureka Server即服务的注册中心,在刚才的案例中,我们只有一个EurekaServer,事实上EurekaServer也可以是一个集群,形成高可用的Eureka中心。

动手搭建高可用的EurekaServer

我们假设要搭建两条EurekaServer的集群,端口分别为:10086和10087

1)我们修改原来的EurekaServer配置:

server:
  port: 10086 # 端口
spring:
  application:
    name: eureka-server # 应用名称,会在Eureka中显示
eureka:
  client:
    service-url: # 配置其他Eureka服务的地址,而不是自己,比如10087
      defaultZone: http://127.0.0.1:10087/eureka

所谓的高可用注册中心,其实就是把EurekaServer自己也作为一个服务进行注册,这样多个EurekaServer之间就能互相发现对方,从而形成集群。因此我们做了以下修改:

  • 删除了register-with-eureka=false和fetch-registry=false两个配置。因为默认值是true,这样就会吧自己注册到注册中心了。
  • 把service-url的值改成了另外一台EurekaServer的地址,而不是自己

2)另外一台配置恰好相反:

server:
  port: 10087 # 端口
spring:
  application:
    name: eureka-server # 应用名称,会在Eureka中显示
eureka:
  client:
    service-url: # 配置其他Eureka服务的地址,而不是自己,比如10087
      defaultZone: http://127.0.0.1:10086/eureka

注意:idea中一个应用不能启动两次,我们需要重新配置一个启动器:

SpringCloud微服务治理一(介绍,环境搭建,Eureka)_第6张图片
SpringCloud微服务治理一(介绍,环境搭建,Eureka)_第7张图片
SpringCloud微服务治理一(介绍,环境搭建,Eureka)_第8张图片

然后启动即可。

3)启动测试:


SpringCloud微服务治理一(介绍,环境搭建,Eureka)_第9张图片

4)客户端注册服务到集群

因为EurekaServer不止一个,因此注册服务的时候,service-url参数需要变化:

eureka:
  client:
    service-url: # EurekaServer地址,多个地址以','隔开
      defaultZone: http://127.0.0.1:10086/eureka,http://127.0.0.1:10087/eureka

5.4.2.服务提供者

服务提供者要向EurekaServer注册服务,并且完成服务续约等工作。

服务注册

服务提供者在启动时,会检测配置属性中的:eureka.client.register-with-erueka=true参数是否正确,事实上默认就是true。如果值确实为true,则会向EurekaServer发起一个Rest请求,并携带自己的元数据信息,Eureka Server会把这些信息保存到一个双层Map结构中。第一层Map的Key就是服务名称,第二层Map的key是服务的实例id。

服务续约

在注册服务完成以后,服务提供者会维持一个心跳(定时向EurekaServer发起Rest请求),告诉EurekaServer:“我还活着”。这个我们称为服务的续约(renew);

有两个重要参数可以修改服务续约的行为:

eureka:
  instance:
    lease-expiration-duration-in-seconds: 90
    lease-renewal-interval-in-seconds: 30
  • lease-renewal-interval-in-seconds:服务续约(renew)的间隔,默认为30秒
  • lease-expiration-duration-in-seconds:服务失效时间,默认值90秒

也就是说,默认情况下每个30秒服务会向注册中心发送一次心跳,证明自己还活着。如果超过90秒没有发送心跳,EurekaServer就会认为该服务宕机,会从服务列表中移除,这两个值在生产环境不要修改,默认即可。

但是在开发时,这个值有点太长了,经常我们关掉一个服务,会发现Eureka依然认为服务在活着。所以我们在开发阶段可以适当调小。

eureka:
  instance:
    lease-expiration-duration-in-seconds: 10 # 10秒即过期
    lease-renewal-interval-in-seconds: 5 # 5秒一次心跳

实例id

先来看一下服务状态信息:

在Eureka监控页面,查看服务注册信息:

SpringCloud微服务治理一(介绍,环境搭建,Eureka)_第10张图片

在status一列中,显示以下信息:

  • UP(1):代表现在是启动了1个示例,没有集群
  • DESKTOP-2MVEC12:user-service:8081:是示例的名称(instance-id),
    • 默认格式是:${hostname} + ${spring.application.name} + ${server.port}
    • instance-id是区分同一服务的不同实例的唯一标准,因此不能重复。

我们可以通过instance-id属性来修改它的构成:

eureka:
  instance:
    instance-id: ${spring.application.name}:${server.port}

重启服务再试试看:

SpringCloud微服务治理一(介绍,环境搭建,Eureka)_第11张图片

5.4.3.服务消费者

获取服务列表

当服务消费者启动时会检测eureka.client.fetch-registry=true参数的值,如果为true,则会从Eureka Server服务的列表只读备份,然后缓存在本地。并且每隔30秒会重新获取并更新数据。我们可以通过下面的参数来修改:

eureka:
  client:
    registry-fetch-interval-seconds: 5

生产环境中,我们不需要修改这个值。

但是为了开发环境下,能够快速得到服务的最新状态,我们可以将其设置小一点。

5.4.4.失效剔除和自我保护

失效剔除

有些时候,我们的服务提供方并不一定会正常下线,可能因为内存溢出、网络故障等原因导致服务无法正常工作。Eureka Server需要将这样的服务剔除出服务列表。因此它会开启一个定时任务,每隔60秒对所有失效的服务(超过90秒未响应)进行剔除。

可以通过eureka.server.eviction-interval-timer-in-ms参数对其进行修改,单位是毫秒,生成环境不要修改。

这个会对我们开发带来极大的不变,你对服务重启,隔了60秒Eureka才反应过来。开发阶段可以适当调整,比如10S

自我保护

我们关停一个服务,就会在Eureka面板看到一条警告:

这是触发了Eureka的自我保护机制。当一个服务未按时进行心跳续约时,Eureka会统计最近15分钟心跳失败的服务实例的比例是否超过了85%。在生产环境下,因为网络延迟等原因,心跳失败实例的比例很有可能超标,但是此时就把服务剔除列表并不妥当,因为服务可能没有宕机。Eureka就会把当前实例的注册信息保护起来,不予剔除。生产环境下这很有效,保证了大多数服务依然可用。

但是这给我们的开发带来了麻烦, 因此开发阶段我们都会关闭自我保护模式:

eureka:
  server:
    enable-self-preservation: false # 关闭自我保护模式(缺省为打开)
    eviction-interval-timer-in-ms: 1000 # 扫描失效服务的间隔时间(缺省为60*1000ms)

你可能感兴趣的:(SpringCloud微服务治理一(介绍,环境搭建,Eureka))