Spring Cloud

Spring Cloud 微服务概述

Spring Cloud技术点

  • Eureka: 服务注册于发现, 用于服务管理
  • Feign: web调用客户端, 能够简化HTTP接口的调用
  • Ribbon: 基于客户端的负载均衡
  • Hystrix: 熔断降级, 防止服务雪崩
  • Zuul: 网关路由, 提供路由转发, 请求过滤, 限流降级等功能
  • Config: 配置中心, 分布式配置管理
  • Sleuth: 服务链路追踪
  • Admin: 健康管理

为什么要用微服务

最开始都是单体的结构, 所有的功能全部写在一个服务里, 随着业务的zengda, 一台机器已经扛不住了.

这时将多台单体的服务并联在一起使用, 使用负载均衡来处理请求的分发.

但是服务依然很大, 维护一个小功能要将所有服务全部停机重启.

因此可以将一个大服务拆分成多个小服务, 每个小服务专注于自己的一套业务系统, 有自己的数据库.

持续集成, 持续部署, 持续交付

随着微服务的发展, 持续集成, 持续部署, 持续交付也随着发展.

集成是指软件个人研发的部分向软件整体部分集成, 以便尽早发现个人开发部分的问题

部署是指代码尽快向可运行的开发/测试交付, 以便尽早测试

交付是指研发尽快向客户交付, 以便尽早发现生产环境中存在的问题

如果说等到所有东西都完成了才向下个环节交付, 导致所有的问题只能在最后才爆发出来, 解决成本巨大甚至无法解决. 而所谓的持续, 就是说每完成一个完整的部分, 就向下个环节交付, 发现问题可以马上调整. 使问题不会放大到其他部分和后面的环节.

这种做法打核心思想在于: 既然事实上那一做到事先完全了解完整的, 正确的需求, 那么久干脆一小块一小块的做, 并且加快交付的速度和频率, 使得交付物尽早在下个环节得到验证. 早发现问题早返工.

持续集成的工具: Jenkins Pipeline

服务进化概述

单体应用

  1. 概念: 所有功能全部打包在一起. 应用大部分是一个war包或一个jar包. 随着业务发展, 功能增多, 这个项目会越来越臃肿.

  2. 好处: 容易开发,测试,部署, 适合项目初期试错.

  3. 坏处:

    随着项目越来越复杂, 团队不断扩大

    • 复杂性高: 代码多, 十万行百万行级别. 加一个小功能, 会带来其他功能的隐患, 因为他们在一起.
    • 技术债务: 人员流动, 不坏不修,因为不敢修.
    • 持续部署困难: 由于是全量应用, 改一个小功能, 全部部署, 会导致无关的功能暂停使用. 编译部署上线耗时长, 不敢随便部署, 导致部署频率低, 进而又导致两次部署之间功能修改多, 越不敢部署, 恶性循环.
    • 可靠性差: 某些小问题, 比如小功能出现OOM, 会导致整个应用崩溃
    • 扩展受限: 只能整体扩展, 无法按照需要进行扩展, 不能根据计算密集型和IO密集型进行合适的区分.
    • 阻碍创新: 单体应用是以一种技术解决所有问题, 不容易引入新技术. 但在告诉的互联网发展过程中, 适应的潮流是: 用合适的语言做核实的事情. 比如在单体应用中, 一个项目用SpringMVC, 想混成SpringBoot, 切换成本很高, 因为有可能十万,百万行代码都要改, 而微服务可以轻松切换, 因为每个服务功能简单, 代码少.

SOA

对单体应用的改进: 引入SOA(Service-Oriented Archiecture) 面向服务架构, 拆分系统, 用服务的流程化来实现业务的灵活性. 服务间需要某些方法进行连接, 面向接口等. 它是一种设计方法, 其中包含多个服务, 服务之间通过相互依赖最终提供一系列的功能. 一个服务通常以独立的形式存在于操作系统进程中. 各个服务之间通过网络调用. 但是还是需要用些方法来进行服务组合, 有可能还是个单体应用.

所以要引入微服务, 是SOA四象的一种具体实践.

微服务架构 = 80%的SOA服务架构思想 + 100%的组件化架构思想.

微服务

微服务概况

  • 无严格定义
  • 微服务是一种架构风格, 将单体应用划分为小型的服务单元.
  • 微服务架构是一种使用一系列粒度较小的服务来开发单个应用的方式; 每个服务运行在自己的进程中; 服务间采用轻量级的方式进行通信 (通常是HTTP ); 这些服务是基于业务逻辑的范围, 通过自动化部署的机制来独立部署的, 并且服务的集中管理应该的最低限度的, 即每个服务可以采用不同的编程语言编写, 使用不同的数据存储技术.

微服务的特性

独立运行在自己进程中.

一系列独立服务共同构建起整个系统

一个服务只关注自己的独立业务

轻量的通信机制RESTful API

使用不同语言开发

全自动部署机制

微服务组件

不局限于具体的微服务实现技术

  • 服务注册于发现

    服务提供方将己方调用地址注册到服务注册中心, 让服务调用方能够方便的找到自己; 服务调用方从服务注册中心找到自己需要调用的服务的地址.

  • 负载均衡

    服务提供方一般以多实例的形式提供服务, 负载均衡功能能够让服务调用方连接到合适的服务节点. 并且, 服务节点选择的过程对服务调用方来说是透明的.

  • 服务网关

    服务网关是服务调用的唯一入口, 可以在这个组件中实现用户鉴权, 动态路由, 灰度发布, A/B测试, 负载限流等功能

    灰度发布(有名金丝雀发布)是指在黑与白之间, 能够平滑过渡的一种发布方式. 在其上可以进行A/B testing, 即让一部分用户继续用产品特性A, 一部分用户开始用产品特性B, 如果用户对B没有什么反对意见, 那么逐步扩大范围, 把所有用户都迁移到B上面来. 灰度发布可以保证整体系统的稳定, 在初始灰度的时候就可以发现, 调整问题, 以保证其影响度.

  • 配置中心

    将本地化的配置信息(Properties, XML, YAML等形式)注册到配置中心, 实现程序包在开发,测试,生产环境中的无差别性, 方便程序包的迁移, 也是无状态特性.

  • 集成框架

    微服务组件都以职责单一的程序要对外提供服务, 集成框架以配置的形式将所有微服务组件(特别是管理端组件)集成到统一的界面框架下, 让用户能够在统一的界面中使用系统. SpringCloud就是呀一个集成框架.

  • 调用链监控

    记录完成一次请求的先后衔接和调用关系, 并将这种串行或并行的调用关系展示出来. 在系统出错时, 可以方便的找出出错点.

  • 支撑平台

    系统微服务化后, 各个业务模块经过拆分变得更加细化, 系统的部署,运维, 监控等都比单体应用架构更加复杂, 这就需要将大部分的工作自动化. 现在, Docker等工具可以给微服务架构的部署带来较多的便利, 例如持续集成,蓝绿发布, 健康检查, 性能监控等等. 如果没有合适的支撑平台或工具, 微服务的架构就无法发挥最大的功效.

    1. 蓝绿部署就是不停老版本, 部署新版本然后进行测试, 确认OK, 将流量切到新版本, 然后老版本同时也升级到新版本.
    2. 灰度是选择部分部署新版本, 将部分流量引入到新版本, 新老版本同时提供服务. 等待灰度的版本OK, 可全量覆盖老版本.

    灰度是不同版本共存, 蓝绿是新旧版本切换, 两种魔兽的出发点不一样.

微服务优点

  1. 独立部署. 不依赖其他服务, 耦合性低, 不用管其他服务的部署对自己的影响.
  2. 易于开发和维护: 关注特定业务, 所以业务清晰, 代码量少, 模块变得易开发,易理解, 易维护.
  3. 启动快: 功能少,代码少, 所以启动快, 有需要停机维护的服务, 不会长时间暂停服务.
  4. 局部修改容易: 只需要部署相应的服务即可, 适合敏捷开发.
  5. 技术栈不受限: java, node.js等
  6. 按需伸缩: 某个服务受限,可以按需增加内存, CPU等.
  7. 职责转译: 专门团队负责专门业务, 有利于团队分工.
  8. 代码复用: 不需要重复编写, 底层实现通过接口方式提供.
  9. 便于团队协作: 每个团队只需要提供API就行, 定义好API后, 可以并行开发.

微服务缺点

  1. 分布式固有的复杂性: 容错 (某个服务宕机), 网络延时, 调用关系, 分布式事务等, 都会带来复杂.

  2. 分布式事务的挑战: 每个服务有自己id数据库, 有点在于不同服务可以选择适合自身业务的数据库. 订单用MySQL, 评论用MongoDB等. 目前最理想解决方案是: 柔性事务的最终一致性.

    刚性事务: 遵循ACID原则, 强一致性

    柔性事务: 遵循BASe原则, 最终一致性; 与刚性事务不同, 柔性事务允许一定时间内, 不同节点的数据不一致, 但要求最终一致.

    BASE是Basically Acailable(基本可用), Soft State(软状态)和Eventually constent(最终一致性)三个短语的缩写. BASE理论是对CAP中AP的一个扩展, 通过牺牲强一致性来获得可用性, 当出现故障,允许部分不可用,但要保证核心功能可用, 允许数据在一段时间内是不一致的, 但组中达到一致状态. 满足BASe理论的事务, 我们称之为柔性事务.

  3. 接口调整成本高: 改一个接口, 调用方都要改.

  4. 测试难度提升: 一个接口改变, 所有调用方都得测. 自动化测试就变的重要了. API文档的管理也尤为重要.

  5. 运维要求高: 需要维护几十, 上百个服. 监控变的复杂. 并且还要关注多个集群, 不像原来单体, 一个应用正常运行即可.

  6. 重复工作: 比如java的工具类可以在共享common.jar中, 但在多于艳霞行不通, C++无法直接用java的jar包.

设计原则

单一职责原则: 关注整个系统功能中单独, 有界限的一部分.

服务自治原则: 可以独立开发, 测试, 构建, 部署, 运行, 与其他服务解耦.

轻量级通信原则: 轻, 跨平台, 跨语言. REST, AMQP等.

粒度把控: 与自己实际相结合. 不要追求完美, 随业务进化而调整.

技术选型

  1. SpringCloud和Dubbo组件比较.

    dubbo: zookeeper + dubbo + springmvc/spiringboot

    通信方式: rpc

    注册中心: zookeeper, nacos

    配置中心: diamond (淘宝开发)

spring cloud: spring + Netflix

通信方式: http restful

注册中心: eureka, consul, nacos

配置中心: config

断路器: hystrix

网关: zuul, gateway

分布式追踪系统: sleuth + zipkin

  1. 差别
dubbo spring cloud
背景 国内影响大 国外影响大
社区活跃度 低(曾停止维护过几年,后来重进开始维护)
架构完整度 不完善(dubbo有些不提供,需要用第三方, 它只关注服务治理) 比较完善, 微服务组件应有尽有
学习成本 dubbo需要配套学习 无缝spring
性能 高(基于netty) 低. (基于http) 此性能的损耗对大部分应用是可以接受的.
而HTTP风格的API是很方便的.用小的性能损耗换来了方便

SpringCloud

概念

SpringCloud是实现微服务架构的一系列框架的有机集合.

是在SpringBoot基础上构建的, 用于简化分布式系统构建的工具集. 是拥有众多子项目的项目集合. 利用SpringBoot的开发便利性, 巧妙地简化了分布式系统基础设施的开发

整体架构

组成:

  1. 服务注册于发现组件: Eureka, Zookeeper, Consul等. Eureka是基于REST风格的.
  2. 服务调用组件: Hystrix(熔断降级, 在出现依赖服务失效的情况下, 通过隔离系统依赖服务的方式, 防止服务级联失败, 同时提供失败回滚机制, 使系统能够更快地从异常中恢复.) , Ribbon (客户端负载均衡, 用于提供客户端的软件负载均衡算法, 提供了一系列完善的配置项: 连接超时, 重试等), OpenFeigh(优雅的封装Ribbon, 是一个声明式RESTful网络请求客户端, 它使编写web服务客户端变得更加方便和快捷)
  3. 网关: 路由和过滤. Zuul,Gateway
  4. 配置中心: 提供了配置几种管理, 动态刷新配置的功能; 配置通过git或者其他方式来存储.
  5. 消息组件: Spring Cloud Stream(对分布式消息进行抽象, 包括发布订阅, 分组消费等功能, 实现了微服务之间的异步通信) 和Spring Cloud Bus(主要提供服务间的事件通信, 如刷新配置)
  6. 安全控制组件: Spring Cloud Security基于OAuth2.0开放网络的安全标准, 提供了单点登录, 资源授权和令牌管理等功能.
  7. 链路追踪组件: Spring Cloud Sleuth(收集调用链路上的数据), Zipkin(对Sleuth收集的信息, 进行存储,统计,展示)

Spring Cloud基石

  1. Spring Cloud Context为Spring Cloud 应用上下文提供了实用工具和特性服务
  2. Spring Cloud common针对不同的Spring Cloud实现(比如注册中心: eureka, consul)提供上层抽象和公共类

Spring Cloud Context

  1. 在Spring Boot中, 应用上下文通过application.yml配置

  2. Bootstrap上下文(Spring Cloud提供, 也叫引导程序上下文)

    Spring Cloud启动的时候会创建一个bootstrap的上下文, 它是应用的父级上下文(注意, 这里说的bootstrap指的是启动最开始时加载的配置项, 与bootstrap.yml或者说bootstrap.properties是两码事). 它负责从一些外部环境中加载配置项, 如配置中心. 这部分配置项的优先级是最高的, 因此它不会被其他的配置文件中加载的配置项给覆盖.

    它是主程序的父级上下文, 负责从外部资源中(git仓库)加载配置属性和解密本地外部配置文件中的属性. 是所有Spring程序的外部属性来源. 通过Bootstrap加载进来的属性的优先级较高, 不能被本地配置覆盖.

    bootstrap.yml
    
    spring:
        application:
            name: my-application
        cloud:
            config:
                uri: ${CONFIG_SERVER:http://localhost:8080}
    
    ---------------------------------------------------------------------
    如果想要禁止Bootstrap引导过程, 可以在bootstrap.yml中设置:
    spring:
        cloud:
            bootstrap:
                enabled: false
    1. 加载顺序

      Spring Cloud应用加载的配置项可以来自于以下几个位置:

      1. 启动命令中指定的配置项
      2. 配置中心中的配置文件
      3. 本地的application.yml
      4. 本地bootstrap.yml

      这几个位置的配置项从上往下优先级递减, 即从上面位置加载的配置项会覆盖下面位置加载的配置项.

Spring Cloud Commons

将服务发现, 负载均衡, 断路器等封装到Commons中, 供Cloud客户端使用, 不依赖于具体的实现, 类似于jdbc提供了一套规范, 数据库厂商来实现它.

服务注册与发现

Eureka单节点搭建

  1. pom.xml

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    </dependency>
  2. application.yml

    eureka:
        client:
            # 是否将自己注册到Eureka Server, 默认为ture.
            # 由于当前就是server, 故设置成false, 表明该服务不会向eureka注册自己的信息
            register-with-eureka: false
            # 是否从eureka server获取注册信息, 由于单节点, 不需要同步其他节点数据, 用false
            fetch-registry: false
            # 设置服务注册中心的url, 用于client和server端交流
            service-url:
                defaultZone: http://root:root@localhost:7901/eureka
  3. 代码

    启动类上添加此注解标识该服务为配置中心
    @EnableEurekaServer
  4. PS: Eureka会暴露一些端点. 端点用于Eureka Client注册自身, 获取注册表, 发送心跳.x

整体介绍

  1. 背景: 在传统应用中, 组件之间的调用, 通过有规范的约束的接口来实现, 从而实现不同模块间良好的协作.但是被拆分成微服务后, 每个微服务实例的网络地址都可能动态变化, 数量也会变化, 使得原来硬编码的地址失去了作用. 需要一个中心化的组件来进行服务的登记和管理.

  2. 概念: 实现服务治理, 即管理所有的服务信息和状态.

    注册中心相当于买票乘车, 只看有没有票(有没有服务), 有就去买票(获取注册列表), 然后乘车(调用). 不比关心有多少火车正在运行.

  3. 注册中心好处: 不用关心有多少提供方

  4. 注册中心有哪些: Eureka, Nacos, Consul, Zookeeper等.

  5. 服务注册于发现包括两部分, 一个是服务器端, 另一个是客户端

    server是一个公共服务, 为client提供服务注册和发现的功能, 维护注册到自身的client的相关信息, 同时提供接口给client获取注册表中其他服务的信息, 使得动态变化的client能够进行服务间的相互调用.

    client将自己的服务信息通过一定的方式登记到server上, 并在正常范围内维护自己信息一致性, 方便其他服务发现自己, 同时可以通过server获取到自己依赖的其他服务信息, 完成服务调用, 还内置了负载均衡器, 用来进行基本的负载均衡.

  6. Eureka: 是一个RESTful风格的服务, 是一个用于服务发现和注册的基础组件, 是搭建SpringCloud微服务的前提之一, 它屏蔽了Server和client的交互细节, 使得开发者将精力放到业务上.

  7. serverA从serverB同步信息, 则serverB是serverA的peer.

注册中心和服务之间的关系

服务提供者和服务消费者都会将自己注册到注册中心, 服务消费者会将服务注册表拉取到消费者本地.

注册表中就是服务名和ip的对应关系, 可以直接通过注册表中的信息发起调用, 哪怕注册中心挂了, 因为消费者本地已经拉取过有缓存, 也是可以正常使用的.

client功能

  1. 注册: 每个微服务启动是, 将自己的网络地址等信息注册到注册中心, 注册中心会存储(内存中)这些内容
  2. 获取服务注册表: 服务消费者从注册中心, 查询服务提供者的网络地址, 并使用该地址调用服务提供者, 避免每次都查注册表信息, 所以client会定时去server拉取注册表信息缓存到client本地.
  3. 心跳: 各个微服务与注册中心通过某种机制(心跳)通信, 若注册中心长时间与服务器没有通信, 就
  4. 调用: 实际的服务调用, 通过注册表, 解析服务名和具体地址的对应关系, 找到具体服务的地址, 进行调用.

server注册中心功能

  1. 服务注册表: 记录各个微服务信息, 例如服务名称,ip,端口等.

    注册表提供查询API(查询可用的微服务实例)和管理API(用于服务的注册和注销)

  2. 服务注册于发现:

    注册: 将微服务信息注册到注册中心

    发现: 查询可勇微服务列表及其网络地址.

  3. 服务检查: 定时检测已注册的服务, 如发现某实例长时间无法访问, 就从注册表中移除.

服务注册

  1. pom文件中加上依赖

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    </dependency>
  2. application.yml中添加配置

    eureka:
        client:
            service-url:
                defaultZone: http://root:root@localhost:7900/eureka/

ps:

Eureka Server与Eureka Client之间的联系主要通过心跳的方式实现. 心跳(Heartbeat)即Eureka Client定时向Eureka Server汇报本服务实例当前状态, 维护本服务实例在注册表中租约的有效性.

Eureka Client将定时从Eureka Server中拉取注册表中的信息, 并将这些信息缓存到本地, 用于服务发现.

Eureka高可用

高可用: 可以通过运行多个Eureka Server实例并相互注册的方式实现 . Server节点之间会彼此增量地同步信息,从而确保节点中数据一致.

  1. 注册中心改造.

    application.yml
    参考: # 高可用两个节点的yml
    
    # 高可用两个节点
    # 应用名称及验证账号
    spring:
      application:
        name: eureka
      security:
        user:
          name: root
          password: root
    
    logging:
      level: debug
    
    ---
    spring:
      prifiles: 7901
    server:
      port: 7901
    eureka:
      instance:
        hostname: eureka-7901
      client:
        # 设置服务注册中心的url
        service-url:
          defaultZone: http://root:root@localhost:7902/eureka/
    
    ---
    spring:
      prifiles: 7902
    server:
      port: 7902
    eureka:
      instance:
        hostname: eureka-7902
      client:
        # 设置服务注册中心的url
        service-url:
          defaultZone: http://root:root@localhost:7901/eureka/
  2. 服务注册改造

    eureka:
        client:
            service-url:
                defaultZone: http://root:root@localhost:7901/eureka/,http://root:root@localhost:7902/eureka/
    

写一个地址也行(但是server得互相注册), Eureka Server会自动同步, 但为了避免极端情况, 还是写多个.

集群PS:

集群中各个server会从其他server同步注册表信息.

Eureka端点

Eureka注册服务不局限于语言和框架, 只要符合注册服务的规范即可.

使用http post请求向 /eureka/apps/{applicationName}发送请求, 请求体中需要一些格式.

Eureka原理

  1. 本质: 存储了每个客户端的注册信息. EurekaClient从EurekaServer同步获取服务注册列表. 通过一定的规则选择一个服务进行调用.
  2. 详解:
    • 服务提供者: 是一个eureka client, 向eureka server 注册和更新自己的信息, 同时能从eureka server注册表中获取到其他服务的信息.
    • 服务注册中心: 提供服务注册和发现的功能. 每个eureka client向eureka server注册自己的信息, 也可以通过eureka server 获取到其他服务的信息达到发现和调用其他服务的目的.
    • 服务消费者: 是一个eureka client, 通过eureka server 获取注册到其上其他服务的信息, 从而根据信息找到所需的服务发起远程调用.
    • 同步复制: eureka server之间注册表信息的同步复制, 使eureka server集群中不同注册表中服务实例信息保持一致.
    • 远程调用: 服务之间的远程调用
    • 注册: client端向server端注册自身的元数据以供服务发现.
    • 续约: 通过发送心跳到server以维护和更新注册表中服务实例元数据的有效性. 当在一定市场内, server没有收到client的信条信息, 将默认服务下线, 会把服务实例的信息从注册表中删除.
    • 下线: client在关闭时主动向server注销服务实例元数据, 这时client的服务示例数据将从server的注册表中删除.
    • 获取注册表: client向server请求注册表信息, 用于服务发现, 从而发起服务间远程调用.

如果我们自己实现一个注册中心, 应该怎么做

客户端:

实现功能:

  1. 向服务端注册
  2. 拉取注册表
  3. 根据注册表信息选择一个服务发起调用.

服务端:

写一个web server

实现功能:

  1. 定义注册表: Map<name, Map<id, InstanceInfo>>
  2. 客户端可以注册自己的信息
  3. 客户端可以拉取注册表的信息
  4. 可以和其他server共享注册表

eureka源码

端点:

Operation HTTP action Description
Register new application instance POST /eureka/v2/apps/appID Input: JSON/XMLpayload HTTPCode: 204 on success
De-register application instance DELETE /eureka/v2/apps/appID/instanceID HTTP Code: 200 on success
Send application instance heartbeat PUT /eureka/v2/apps/appID/instanceID HTTP Code: * 200 on success * 404 if instanceIDdoesn’t exist
Query for all instances GET /eureka/v2/apps HTTP Code: 200 on success Output: JSON/XML
Query for all appID instances GET /eureka/v2/apps/appID HTTP Code: 200 on success Output: JSON/XML
Query for a specific appID/instanceID GET /eureka/v2/apps/appID/instanceID HTTP Code: 200 on success Output: JSON/XML
Query for a specific instanceID GET /eureka/v2/instances/instanceID HTTP Code: 200 on success Output: JSON/XML
Take instance out of service PUT /eureka/v2/apps/appID/instanceID/status?value=OUT_OF_SERVICE HTTP Code: * 200 on success * 500 on failure
Move instance back into service (remove override) DELETE /eureka/v2/apps/appID/instanceID/status?value=UP (The value=UP is optional, it is used as a suggestion for the fallback status due to removal of the override) HTTP Code: * 200 on success * 500 on failure
Update metadata PUT /eureka/v2/apps/appID/instanceID/metadata?key=value HTTP Code: * 200 on success * 500 on failure
Query for all instances under a particular vip address GET /eureka/v2/vips/vipAddress * HTTP Code: 200 on success Output: JSON/XML * 404 if the vipAddressdoes not exist.
Query for all instances under a particular secure vip address GET /eureka/v2/svips/svipAddress * HTTP Code: 200 on success Output: JSON/XML * 404 if the svipAddressdoes not exist.

Eureka Client

启动流程:

  • 找到eureka client配置相关类

    根据springboot自动装配原理, 在org.springframework.cloud:spring-cloud-netflix-eureka-client:2.2.1.RELEASE包下的spring-factories中包含了spring-cloud-starter-netflix-eureka-client的配置类.

    org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
    org.springframework.cloud.netflix.eureka.config.EurekaClientConfigServerAutoConfiguration,\
    org.springframework.cloud.netflix.eureka.config.EurekaDiscoveryClientConfigServiceAutoConfiguration,\
    org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration,\
    org.springframework.cloud.netflix.ribbon.eureka.RibbonEurekaAutoConfiguration,\
    org.springframework.cloud.netflix.eureka.EurekaDiscoveryClientConfiguration,\
    org.springframework.cloud.netflix.eureka.reactive.EurekaReactiveDiscoveryClientConfiguration,\
    org.springframework.cloud.netflix.eureka.loadbalancer.LoadBalancerEurekaAutoConfiguration
    
    org.springframework.cloud.bootstrap.BootstrapConfiguration=\
    org.springframework.cloud.netflix.eureka.config.EurekaDiscoveryClientConfigServiceBootstrapConfiguration
    

    EurekaClientAutoConfiguration (Eureka Client自动配置类, 负责Eureka client中关键beans的配置和初始化)

    RibbonEurekaAutoConfiguration(Ribbon负载均衡相关配置)

    EurekaDiscoveryClientConfiguration(配置自动注册和应用的健康检查器)

  • EurekaDiscoveryClientConfiguration介绍

    @ConditionalOnClass({EurekaClientConfig.class})

    当EurekaClientConfig类被注册进来了, EurekaDiscoveryClientConfiguration就会被注册.

    EurekaClientConfig根据上面的图, 主要是Eureka client和server交互的配置信息.

    EurekaClientConfig是一个接口, 有一个注解

    @ImplementedBy(DefaultEurekaClientConfig.class)

    在EurekaDiscoveryClientConfiguration中还配置了一个bean

    @Bean
    @ConditionalOnMissingBean
    public EurekaDiscoveryClient discoveryClient(EurekaClient client,
                                                 EurekaClientConfig clientConfig) {
        return new EurekaDiscoveryClient(client, clientConfig);
    }

    看一下EurekaDiscoveryClient.

    public class EurekaDiscoveryClient implements DiscoveryClient

    可以看到, EurekaDiscoveryClient实现了DiscoveryClient, 这是上面提到的Spring Cloud Commons中提供的内容.

    EurekaDiscoveryClient中有一个属性是

    private final EurekaClient eurekaClient;

    EurekaClient是一个接口, 查看他的实现类DiscoveryClient.

    DiscoveryClient中有ApplicationInfoManager, 应用信息管理器. 进入后发现有两个属性

    private InstanceInfo instanceInfo;
    private EurekaInstanceConfig config;

    服务实例的信息类InstanceInfo和服务实例配置信息类EurekaInstanceConfig.

  • InstanceInfo介绍

    打开InstanceInfo, 里面有instanceId等服务实例信息.

    InstanceInfo封装了将被发送到Eureka Server进行注册的服务实例元数据. 它在Eureka Server列表中代表一个服务实例, 其他服务可以通过instanceInfo了解到该服务的实例相关信息, 包括地址等, 从而发起请求.

  • EurekaInstanceConfig介绍.

    EurekaInstanceConfig是一个接口, 找到他的实现类EurekaInstanceConfigBean.

    此类封装了EurekaClient自身服务实例的配置信息, 主要用于构建InstanceInfo. 看到此类有一段代码

    @ConfigurationProperties("eureka.instance")

    在配置文件中用eureka.instance属性配置, EurekaInstanceConfigBean提供了默认值.

  • 通过EurekaInstanceConfig构建InstacneInfo

    在ApplicationInfoManager中有一个方法

    public void initComponent(EurekaInstanceConfig config) {
        try {
            this.config = config;
            // 通过EurekaInstanceConfig构造InstanceInfo
            this.instanceInfo = new EurekaConfigBasedInstanceInfoProvider(config).get();
        } catch (Throwable e) {
            throw new RuntimeException("Failed to initialize ApplicationInfoManager", e);
        }
    }
  • 顶级接口DiscoveryClient介绍.

  • Eureka的实现

    接下来我们找Eureka的实现. EurekaDiscoveryClient中组合了EurekaClient来实现.

    @Override
    public List<ServiceInstance> getInstances(String serviceId) {
        List<InstanceInfo> infos = this.eurekaClient.getInstancesByVipAddress(serviceId,
                                                                              false);
        List<ServiceInstance> instances = new ArrayList<>();
        for (InstanceInfo info : infos) {
            instances.add(new EurekaServiceInstance(info));
        }
        return instances;
    }
  • EurekaClient的实现

    EurekaClient有一个注解@ImplementedBy(DiscoveryClient.class), 此类的默认实现类DiscoveryClient提供了:

    服务注册到server方法 register()

    续约 renew()

    下线 shutdown()

    查询服务列表功能

    结合前面的图, 提供了与server交互的关键逻辑.

  • DiscoveryClient

    实现了EurekaClient(继承自LookupService)

  • LookupService

    LookupService的作用: 发现活跃的服务实例

    // 根据服务实例注册的appName来获取封装有相同appName的服务实例信息容器:
    Application getApplication(String appName);
    // 获取所有的服务实例信息
    Applications getApplications();
    // 根据实例id, 获取服务实例信息
    List<InstanceInfo> getInstancesById(String id);
    

    上面的Applications,持有服务实例信息列表, 是同一个服务的集群信息.

    而InstanceInfo代表一个服务实例的信息. 为了保证原子性, 对某个InstanceInfo的操做使用了大量的同步代码.

  • 健康检测器和事件监听器

    EurekaClient在LookupService上做了扩充. 提供了更丰富的获取服务实例的方法. 另外还有另外两个方法

     public void registerHealthCheck(HealthCheckHandler healthCheckHandler);

    向client注册健康检查处理器, client存在一个定时任务通过HealthCheckHandler检查当前client状态, 当client状态发生变化时,将会触发新的注册时间, 去更新eureka server的注册表中的服务实例信息.

    通过HealthCheckHandler实现应用状态检测.

    public void registerEventListener(EurekaEventListener eventListener);

    此外, 还通过此方法注册时间监听器, 当实例信息有更变时, 触发对应的处理事件.

  • DiscoveryClient构造函数 - 不注册不拉取

    DiscoveryClient的构造函数:

    DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args,
                    Provider<BackupRegistry> backupRegistryProvider, EndpointRandomizer endpointRandomizer){}

    此方法中依次执行了从server中拉取注册表, 服务注册, 初始化发送心跳, 缓存刷新(定时拉取注册表信息), 按需注册定时任务等, 贯穿了client启动阶段的各项工作.

    if (config.shouldFetchRegistry()) {
        this.registryStalenessMonitor = new ThresholdLevelsMetric(this, METRIC_REGISTRY_PREFIX + "lastUpdateSec_", new long[]{15L, 30L, 60L, 120L, 240L, 480L});
    } else {
        this.registryStalenessMonitor = ThresholdLevelsMetric.NO_OP_METRIC;
    }

    对应eureka.client.fetch-register true表示client从server拉取注册表信息. 默认是true.

    if (config.shouldRegisterWithEureka()) {
        this.heartbeatStalenessMonitor = new ThresholdLevelsMetric(this, METRIC_REGISTRATION_PREFIX + "lastHeartbeatSec_", new long[]{15L, 30L, 60L, 120L, 240L, 480L});
    } else {
        this.heartbeatStalenessMonitor = ThresholdLevelsMetric.NO_OP_METRIC;
    }

    对应eureka.client.register-with-eureka true表示client将注册到server.

    if (!config.shouldRegisterWithEureka() && !config.shouldFetchRegistry()) {}

    此处判断表示既不注册服务也不拉取注册表, 什么都不做 , 直接返回.

  • DiscoveryClient构造函数 - 两个定时任务

    // default size of 2 - 1 each for heartbeat and cacheRefresh
    scheduler = Executors.newScheduledThreadPool(2,
                                                 new ThreadFactoryBuilder()
                                                 .setNameFormat("DiscoveryClient-%d")
                                                 .setDaemon(true)
                                                 .build());
    
    heartbeatExecutor = new ThreadPoolExecutor(
        1, clientConfig.getHeartbeatExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,
        new SynchronousQueue<Runnable>(),
        new ThreadFactoryBuilder()
        .setNameFormat("DiscoveryClient-HeartbeatExecutor-%d")
        .setDaemon(true)
        .build()
    );  // use direct handoff
    
    cacheRefreshExecutor = new ThreadPoolExecutor(
        1, clientConfig.getCacheRefreshExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,
        new SynchronousQueue<Runnable>(),
        new ThreadFactoryBuilder()
        .setNameFormat("DiscoveryClient-CacheRefreshExecutor-%d")
        .setDaemon(true)
        .build()
    );  // use direct handoff

    再往后, 开启了两个线程池, 一个用于发送心跳, 一个用于刷新缓存

  • DiscoveryClient构造函数 - client与server交互的Jersey客户端

    接着构建eurekaTransport = new EurekaTransport(); 它是client和server进行http交互jersey客户端. 点开EurekaTransport, 可以看到许多httpclient相关的属性.

  • DiscoveryClient构造函数 - 拉取注册信息

    if (clientConfig.shouldFetchRegistry() && !fetchRegistry(false)) {
        fetchRegistryFromBackup();
    }

    如果判断前半部分为true, 执行后半部分fetchRegistry. 此时会从server拉取注册表中的信息, 将注册表缓存到本地, 可以就近获取其他服务信息, 减少与server的交互.

  • DiscoveryClient构造函数 - 服务注册

    
    if (clientConfig.shouldRegisterWithEureka() && clientConfig.shouldEnforceRegistrationAtInit()) {
        try {
            if (!register() ) {
                throw new IllegalStateException("Registration error at startup. Invalid server response.");
            }
        } catch (Throwable th) {
            logger.error("Registration error at startup: {}", th.getMessage());
            throw new IllegalStateException(th);
        }
    }

    如果注册失败抛异常.

  • 启动定时任务

    // finally, init the schedule tasks (e.g. cluster resolvers, heartbeat, instanceInfo replicator, fetch
    initScheduledTasks();
    
    

/**

  • Initializes all scheduled tasks.

  • /
    private void initScheduledTasks() {
    if (clientConfig.shouldFetchRegistry()) {

      // registry cache refresh timer
      int registryFetchIntervalSeconds = clientConfig.getRegistryFetchIntervalSeconds();
      int expBackOffBound = clientConfig.getCacheRefreshExecutorExponentialBackOffBound();
      // scheduler是ScheduledExecutorService, 在这个线程池中定义了时间.
      // 这里启动了租约任务
      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");

    }
    }

  • 拉取注册表信息

    private boolean fetchRegistry(boolean forceFullRegistryFetch) {
        Stopwatch tracer = FETCH_REGISTRY_TIMER.start();
    
        try {
            // If the delta is disabled or if it is the first time, get all
            // applications
            Applications applications = getApplications();
    
            if (clientConfig.shouldDisableDelta()
                || (!Strings.isNullOrEmpty(clientConfig.getRegistryRefreshSingleVipAddress()))
                || forceFullRegistryFetch
                || (applications == null)
                || (applications.getRegisteredApplications().size() == 0)
                || (applications.getVersion() == -1)) //Client application does not have latest library supporting delta
            {
                logger.info("Disable delta property : {}", clientConfig.shouldDisableDelta());
                logger.info("Single vip registry refresh property : {}", clientConfig.getRegistryRefreshSingleVipAddress());
                logger.info("Force full registry fetch : {}", forceFullRegistryFetch);
                logger.info("Application is null : {}", (applications == null));
                logger.info("Registered Applications size is zero : {}",
                            (applications.getRegisteredApplications().size() == 0));
                logger.info("Application version is -1: {}", (applications.getVersion() == -1));
                // 全量拉取注册表
                getAndStoreFullRegistry();
            } else {
                // 增量拉取
                getAndUpdateDelta(applications);
            }
            applications.setAppsHashCode(applications.getReconcileHashCode());
            logTotalInstances();
        } catch (Throwable e) {
            logger.error(PREFIX + "{} - was unable to refresh its cache! status = {}", appPathIdentifier, e.getMessage(), e);
            return false;
        } finally {
            if (tracer != null) {
                tracer.stop();
            }
        }
    
        // Notify about cache refresh before updating the instance remote status
        onCacheRefreshed();
    
        // Update remote status based on refreshed data held in the cache
        updateInstanceRemoteStatus();
    
        // registry was fetched successfully, so return true
        return true;
    }
  • 全量拉取

    进入getAndStoreFullRegistry()方法

    private void getAndStoreFullRegistry() throws Throwable {
        long currentUpdateGeneration = fetchRegistryGeneration.get();
    
        logger.info("Getting all instance registry info from the eureka server");
    
        Applications apps = null;
        EurekaHttpResponse<Applications> httpResponse = clientConfig.getRegistryRefreshSingleVipAddress() == null
            ? eurekaTransport.queryClient.getApplications(remoteRegionsRef.get())
            : eurekaTransport.queryClient.getVip(clientConfig.getRegistryRefreshSingleVipAddress(), remoteRegionsRef.get());
        if (httpResponse.getStatusCode() == Status.OK.getStatusCode()) {
            apps = httpResponse.getEntity();
        }
        logger.info("The response status is {}", httpResponse.getStatusCode());
    
        if (apps == null) {
            logger.error("The application is null for some reason. Not storing this information");
        } else if (fetchRegistryGeneration.compareAndSet(currentUpdateGeneration, currentUpdateGeneration + 1)) {
            localRegionApps.set(this.filterAndShuffle(apps));
            logger.debug("Got full registry with apps hashcode {}", apps.getAppsHashCode());
        } else {
            logger.warn("Not updating applications as another thread is updating it already");
        }
    }

    在eurekaTransport.queryClient.getApplications(remoteRegionsRef.get())中, 是通过前面提到的请求端点实现的.

    @Override
    public EurekaHttpResponse<Applications> getApplications(String... regions) {
        return getApplicationsInternal("apps/", regions);
    }
  • 增量拉取

    
    private void getAndUpdateDelta(Applications applications) throws Throwable {
        long currentUpdateGeneration = fetchRegistryGeneration.get();
    
        Applications delta = null;
        EurekaHttpResponse<Applications> httpResponse = eurekaTransport.queryClient.getDelta(remoteRegionsRef.get());
        if (httpResponse.getStatusCode() == Status.OK.getStatusCode()) {
            delta = httpResponse.getEntity();
        }
    
        if (delta == null) {
            logger.warn("The server does not allow the delta revision to be applied because it is not safe. "
                        + "Hence got the full registry.");
            getAndStoreFullRegistry();
        } else if (fetchRegistryGeneration.compareAndSet(currentUpdateGeneration, currentUpdateGeneration + 1)) {
            logger.debug("Got delta update with apps hashcode {}", delta.getAppsHashCode());
            String reconcileHashCode = "";
            if (fetchRegistryUpdateLock.tryLock()) {
                try {
                    updateDelta(delta);
                    reconcileHashCode = getReconcileHashCode(applications);
                } finally {
                    fetchRegistryUpdateLock.unlock();
                }
            } else {
                logger.warn("Cannot acquire update lock, aborting getAndUpdateDelta");
            }
            // There is a diff in number of instances for some reason
            if (!reconcileHashCode.equals(delta.getAppsHashCode()) || clientConfig.shouldLogDeltaDiff()) {
                reconcileAndLogDifference(delta, reconcileHashCode);  // this makes a remoteCall
            }
        } else {
            logger.warn("Not updating application delta as another thread is updating it already");
            logger.debug("Ignoring delta update with apps hashcode {}, as another thread is updating it already", delta.getAppsHashCode());
        }
    }

    同样, 也是通过端点实现的eurekaTransport.queryClient.getDelta(remoteRegionsRef.get());

    @Override
    public EurekaHttpResponse<Applications> getDelta(String... regions) {
        return getApplicationsInternal("apps/delta", regions);
    }

    在拉取结束之后, 紧跟着一个判断

    if (delta == null) {
        logger.warn("The server does not allow the delta revision to be applied because it is not safe. "
                    + "Hence got the full registry.");
        getAndStoreFullRegistry();
    }

    如果拉取为空, 则全量拉取.

    通常来讲是3分钟之内的注册表的信息变化(在server端判断), 获取到delta后, 会更新本地注册表.

    增量拉取是为了维护client和server端注册表的一致性, 防止本地数据过久而失效, 采用增量拉取的方式减少了client和server的通信量.

    client有一个注册表缓存刷新定时器, 专门负责维护两者之间的信息同步, 但是当增量出现意外时, 定时器将执行全量拉取以更新本地缓存信息. 更新本地注册表方法 updateDelta有一个细节

    if (ActionType.ADDED.equals(instance.getActionType()))
    
    

public enum ActionType {
ADDED, // Added in the discovery server
MODIFIED, // Changed in the discovery server
DELETED // Deleted from the discovery server
}


  ADDED和MODIFIED状态的将更新到本地注册表

  DELETED将从本地注册表中剔除.



#### Eureka Server

1. Eureka Server 功能

   接受服务注册

   接受服务心跳

   服务剔除

   服务下线

   集群同步

   获取注册表中服务实例信息



   需要注意的是, Server同时也是一个Client, 在不禁止Server的客户端行为时,它也要从配置文件中其他Server进行拉取注册表, 服务注册和发送心跳等操作.

2. Server源码

   - 开启Server

     需要在启动类上加上@EnableEurekaServer, 而这个注解中使用了@Import({EurekaServerMarkerConfiguration.class}), 意思是动态注入此bean到spring容器. 

     我们需要的是EurekaServerAutoConfiguration这个类, 而这个类上有一个注解@ConditionalOnBean({Marker.class}). 

     所以根据上面的Import, 就成功的将Marker装载进来了.

     ```java
     @Configuration(proxyBeanMethods = false)
     public class EurekaServerMarkerConfiguration {

         @Bean
         public Marker eurekaServerMarkerBean() {
             return new Marker();
         }

         class Marker {

         }

     }
     ```

     Marker没有任何东西, 单纯的是做一个`开关`的作用.

   - 开启注册

     在EurekaServerAutoConfiguration上又导入了EurekaServerInitializerConfiguration类,@Import({EurekaServerInitializerConfiguration.class}).

     ```java
     public class EurekaServerInitializerConfiguration
             implements ServletContextAware, SmartLifecycle, Ordered {}
     ```

     作用是初始化之后, 执行start()方法.

     ```java
     @Override
     public void start() {
         //启动一个线程
         new Thread(() -> {
             try {
                 // TODO: is this class even needed now?
                 eurekaServerBootstrap.contextInitialized(
                     EurekaServerInitializerConfiguration.this.servletContext);
                 // eureka启动成功
                 log.info("Started Eureka Server");

                 // 发布事件, 告诉client可以来注册了
                 publish(new EurekaRegistryAvailableEvent(getEurekaServerConfig()));
                 EurekaServerInitializerConfiguration.this.running = true;
                 publish(new EurekaServerStartedEvent(getEurekaServerConfig()));
             }
             catch (Exception ex) {
                 // Help!
                 log.error("Could not initialize Eureka servlet context", ex);
             }
         }).start();
     }
     ```

     所以上面是在eurekaServerBootstrap.contextInitialized(EurekaServerInitializerConfiguration.this.servletContext);启动的server端.

     ```java
     public void contextInitialized(ServletContext context) {
         try {
             initEurekaEnvironment();
             // 初始化eureka上下文
             initEurekaServerContext();

             context.setAttribute(EurekaServerContext.class.getName(), this.serverContext);
         }
         catch (Throwable e) {
             log.error("Cannot bootstrap eureka server :", e);
             throw new RuntimeException("Cannot bootstrap eureka server :", e);
         }
     }
     ```

     进入initEurekaServerContext();

     ```java
     protected void initEurekaServerContext() throws Exception {
         // For backward compatibility
         JsonXStream.getInstance().registerConverter(new V1AwareInstanceInfoConverter(),
                                                     XStream.PRIORITY_VERY_HIGH);
         XmlXStream.getInstance().registerConverter(new V1AwareInstanceInfoConverter(),
                                                    XStream.PRIORITY_VERY_HIGH);

         if (isAws(this.applicationInfoManager.getInfo())) {
             this.awsBinder = new AwsBinderDelegate(this.eurekaServerConfig,
                                                    this.eurekaClientConfig, this.registry, this.applicationInfoManager);
             this.awsBinder.start();
         }

         EurekaServerContextHolder.initialize(this.serverContext);

         log.info("Initialized server context");

         // Copy registry from neighboring eureka node
         // 从相邻的eureka节点复制注册表.
         int registryCount = this.registry.syncUp();
         // traffic 主要是和client交换信息
         this.registry.openForTraffic(this.applicationInfoManager, registryCount);

         // Register all monitoring statistics.
         EurekaMonitors.registerAllStats();
     }
     ```

   - 服务实例注册表

     server是围绕注册表管理的, 有两个InstanceRegistry.

     com.netflix.eureka.registry.InstanceRegistry是server中注册表管理的核心接口, 职责是在内存中管理注册到server中服务实例信息.实现类有PeerAwareInstanceRegistryImpl.

     org.springframework.cloud.netflix.eureka.server.InstanceRegistry继承了PeerAwareInstanceRegistryImpl,并进行了扩展, 使其适配SpringCloud的使用环境, 主要的实现由PeerAwareInstanceRegistryImpl提供.

     com.netflix.eureka.registry.InstanceRegistry实现了两个接口, LeaseManager<InstanceInfo>, LookupService<String>.

     LeaseManager<InstanceInfo>是对注册到server中的服务实例租约进行管理.

     LookupService<String>是提供服务实例的检索查询功能.

   - 接受服务注册

     之前在client部分已经了解过InstanceInfo, client在发起服务注册时会将自身的服务实例元数据封装在InstanceInfo中, 然后将InstanceInfo发送到server. server在接收到client发送到的InstanceInfo后将会尝试将其放到本地注册表中, 以供其他client进行服务发现.

     ApplicationResource是Server对client的rest请求的定义. 

### Eureka元数据

Eureka的元数据有两种:标准元数据和自定义元数据。

标准元数据:主机名、IP地址、端口号、状态页和健康检查等信息,这些信息都会被发布在服务注册表中,用于服务之间的调用。

自定义元数据:可以使用eureka.instance.metadata-map配置,这些元数据可以在远程客户端中访问,但是一般不改变客户端行为,除非客户端知道该元数据的含义。

### 自我保护

1. 红色警告

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.


2. 默认情况下, Server在一定时间内, 没有接收到某个微服务心跳,会将某个微服务注销(90s). 但是当微服务与server之间网络出问题时, 微服务正常提供服务,只是连不上server , 上述行为就非常危险, 因为微服务正常, 不应该注销. 

   Eureka通过自我保护模式来解决整个问题, 当server在短时间内丢失过多客户端时, 那么server会进入自我保护模式, 会保护注册表中的微服务不被注销掉. 当网络故障恢复后, 退出自我保护模式.

3. 思想: 宁可保留健康的和不健康的, 也不盲目注销任何健康的服务.

4. 关闭自我保护

eureka:
server:
enable-self-preservation: false


5. 自我保护触发

   自我保护触发的条件:

   当每分钟心跳次数小于期望心跳次数阈值时, 且自我保护开启, 会触发自我保护机制, 不在自动过期租约.

   例如: 服务实例数:10个, 期望每分钟续约数: 20, 期望阈值: 20*0.85 = 17, 那么在心跳小于17时触发自我保护机制.

### 多网卡选择

eureka:
instance:
prefer-ip-address: true
ip-address: 实际能访问到的Ip


表示将自己的ip注册到EurekaServer上。不配置或false,表示将操作系统的hostname注册到server

### Eureka健康检查

由于server和client通过心跳保持 服务状态,而只有状态为UP的服务才能被访问。看eureka界面中的status。

比如心跳一直正常,服务一直UP,但是此服务数据库连不上了,无法正常提供服务。

此时,我们需要将 微服务的健康状态也同步到server。只需要启动eureka的健康检查就行。这样微服务就会将自己的健康状态同步到eureka。配置如下即可。

在client端配置:将自己真正的健康状态传播到server。

eureka:
client:
healthcheck:
enabled: true


注: 需要client启动健康检查
org.springframework.boot spring-boot-starter-actuator ```

Eureka缺陷

由于集群间的同步复制是通过HTTP的方式进行, 基于网络的不可靠性, 集群中的server间的注册表信息难免存在不同步的时间节点, 不满足CAP中的C(数组一致性)

服务间调用

微服务中, 很多服务系统都在独立的进程中运行, 通过各个服务系统之间的协作来实现一个大项目的所有业务功能. 服务系统间, 使用多种跨进程的方式进行通信协作吗而RESTful风格的网络请求是最为常见的交互方式之一.

springcloud提供的方式:

  1. RestTemplate
  2. Feign

RestTemplate

RestTemplate是Spring提供的同步HTTP网络客户端接口, 它可以简化客户端与HTTP服务器之间的交互, 并且它强制使用RESTful风格, 它会处理HTTP连接和关闭,只需要使用者提供服务器的地址和模板参数.

负载均衡

两种负载均衡

当系统面临大量的用户访问, 负载过高的时候, 通常会增加服务器数量来进行横向扩展(集群), 多个服务器的负载需要均衡, 以免出现服务器负载不均衡. 通过负载均衡, 可以使得集群中服务器的负载保持在稳定高效的状态, 从而提高整个系统的处理能力.

客户端负载均衡:

在客户端负载均衡中, 所有的客户端节点都有一份自己要访问的服务端地址列表, 这些列表统统是从服务注册中心获取的.

服务端负载均衡:

在服务端负载均衡中, 客户端节点只知道单一服务代理的地址, 服务代理则知道所有服务端的地址.

Ribbon

概念

Ribbon是Netflix开发的客户端负载均衡器, 为Ribbon配置服务提供者地址列表后, Ribbon就可以基于某种负载均衡策略算法, 自动地帮助服务消费者去请求提供者. Ribbon默认提供了很多负载均衡算法, 例如轮询, 随机等. 我们也可以实现自定义负载均衡算法.

Ribbon作为SpringCloud的负载均衡机制的实现.

  1. Ribbon可以单独使用, 作为一个独立的负载均衡组件. 只是需要我们手动配置服务地址列表
  2. Ribbon与Eureka配合使用时, Ribbon可以自动从Server获取服务提供者地址列表(DiscoveryClient), 并基于负载均衡算法, 请求其中一个服务提供者实例.
  3. Ribbon与OpenFeign和RestTemplate进行无缝对接, 让二者具有负载均衡的能力. OpenFeign默认集成了Ribbon.

Feign

OpenFeign是Netflix 开发的声明式、模板化的HTTP请求客户端。可以更加便捷、优雅地调用http api。

OpenFeign会根据带有注解的函数信息构建出网络请求的模板,在发送网络请求之前,OpenFeign会将函数的参数值设置到这些请求模板中。

feign主要是构建微服务消费端。只要使用OpenFeign提供的注解修饰定义网络请求的接口类,就可以使用该接口的实例发送RESTful的网络请求。还可以集成Ribbon和Hystrix,提供负载均衡和断路器。

英文表意为“假装,伪装,变形”, 是一个 Http 请求调用的轻量级框架,可以以 Java 接口注解的方式调用 Http 请求,而不用像 Java 中通过封装 HTTP 请求报文的方式直接调用。通过处理注解,将请求模板化,当实际调用的时候,传入参数,根据参数再应用到请求上,进而转化成真正的请求,这种请求相对而言比较直观。Feign 封装 了HTTP 调用流程,面向接口编程,回想第一节课的SOP。

#### Feign和OpenFeign的关系

Feign本身不支持Spring MVC的注解,它有一套自己的注解

OpenFeign是Spring Cloud 在Feign的基础上支持了Spring MVC的注解,如@RequesMapping等等。 OpenFeign的@FeignClient可以解析SpringMVC的@RequestMapping注解下的接口, 并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。

原理

  1. 主程序入口添加@EnableFeignClients注解开启对Feign Client扫描加载处理。根据Feign Client的开发规范,定义接口并加@FeignClient注解。
  2. 当程序启动时,会进行包扫描,扫描所有@FeignClient注解的类,并将这些信息注入Spring IoC容器中。当定义的Feign接口中的方法被调用时,通过JDK的代理方式,来生成具体的RequestTemplate。当生成代理时,Feign会为每个接口方法创建一个RequestTemplate对象,该对象封装了HTTP请求需要的全部信息,如请求参数名、请求方法等信息都在这个过程中确定。
  3. 然后由RequestTemplate生成Request,然后把这个Request交给client处理,这里指的Client可以是JDK原生的URLConnection、Apache的Http Client,也可以是Okhttp。最后Client被封装到LoadBalanceClient类,这个类结合Ribbon负载均衡发起服务之间的调用。

Feign压缩

Feign 是通过 http 调用的,那么就牵扯到一个数据大小的问题。如果不经过压缩就发送请求、获取响应,那么会因为流量过大导致浪费流量,这时就需要使用数据压缩,将大流量压缩成小流量。

开启压缩可以有效节约网络资源, 但是会增加CPU压力, 建议把最小压缩的文档大小适度调大一点, 进行GZIP压缩

一般不需要设置压缩, 如果系统流量浪费比较多, 可以考虑一下.

熔断

概念

在分布式系统下, 微服务之间不可避免地会发生相互调用, 但每个系统都无法百分之百保证自身运行没问题. 在服务调用中, 很可能面临以来服务失效的问题(网络延时,服务异常, 负载过大无法及时响应). 因此需要一个组件, 提供强大的容错能力, 为服务间调用提供保护和控制.

目的: 当我自身依赖的服务不可用时, 服务自身不会被拖垮. 防止微服务级联异常,

本质: 本质就是隔离坏的服务, 不让坏服务拖垮其他服务(调用坏服务的服务)

雪崩效应

每个服务发出一个HTTP请求都会在服务中开启一个新线程. 而下游服务挂了或者网络不可达, 通常线程会阻塞住, 知道timeout. 如果并发量高一点, 这些阻塞的线程就会占用大量的资源, 很有可能把自己本身这个微服务所在的机器资源耗尽,导致自己也挂掉.

如果服务提供者相应非常缓慢, 那么服务消费者调用此提供者就会一直等待, 知道提供者相应或超市. 在高并发场景下, 如果不做任何处理, 就会导致服务消费者的资源耗尽甚至整个系统的崩溃. 一层一层的崩溃, 导致所有系统崩溃.

雪崩三个流程:

服务提供者不可用

重试会导致网络流量加大, 更影响服务提供者

导致服务调用者不可用, 由于调用者一直等待返回, 一直占用系统资源.

*服务不可用原因: *

服务器宕机

网络故障

宕机

程序异常

负载过大,导致服务提供者响应慢

缓存击穿导致服务超负荷运行

容错机制

  1. 为网络请求设置超时

    必须为网络请求设置超时. 一般的调用在几十毫秒内响应. 如果服务不可用, 或者网络有问题, 那么响应时间会变得很长. 长到几十秒.

    每一次调用, 对应一个线程或进程, 如果响应时间长, 那么线程就长时间得不到释放, 而线程对应着系统资源, 包括CPU, 内存 得不到释放的线程越多, 资源被小号的越多, 最终导致系统崩溃.

    因此必须设置超时时间, 让资源尽快释放.

  2. 使用断路器模式

    类比保险丝, 跳闸. 如果家里有短路或者大功率电器使用, 超过电路负载时, 就会跳闸, 如果不跳闸, 电路烧毁, 波及到其他家庭, 导致其他家庭也不可用. 通过跳闸保护电路安全, 当短路问题或者大功率问题被解决后, 再合闸.

断路器

如果对某个微服务请求有大量超时( 说明该服务不可用), 再让新的请求访问该服务就没有意义, 只会无谓的消耗资源. 例如设置了超时时间1s, 如果短时间内有大量的请求无法在1s内响应, 就没有必要请求依赖的服务了.

  1. 断路器是对容易导致错误的操作的代理. 这种代理能统计一段时间内的失败次数, 并依据次数决定是正常请求依赖的服务还是直接返回.
  2. 断路器可以实现款速失败,如果它在一段时间内检测到许多类似的错误(超时), 就会在之后的一段时间内,对该服务的调用快速失败, 即不再请求所调用的服务. 这样对于消费者就无须浪费CPU去等待长时间的超时.
  3. 断路器也可自动诊断依赖的服务是否恢复正常. 如果发现依赖的服务已经恢复正常, 那么就会恢复请求该服务. 通过重置时间来决定断路器的重新闭合.

这样就实现了微服务的”自我修复”: 当依赖的服务不可用时, 打开断路器, 让服务快速失败, 从而防止雪崩. 当依赖的服务恢复正常时, 又恢复请求.

断路器状态转换的逻辑

关闭状态: 正常情况下, 断路器关闭, 可以正常请求依赖的服务.

打开状态: 当一段时间捏, 请求失败率达到一定的阈值, 断路器就会打开. 服务请求不会去请求依赖的服务. 调用方直接返回. 不发生真正的调用. 重置时间过后,进入半开模式.

半开模式: 断路器打开一段时间后, 会自动进入”半开模式”, 此时断路器允许一个服务请求访问依赖的服务. 如果此请求成功(或成功达到一定的比例),则关闭断路器, 恢复正常访问. 否则, 继续保持打开状态.

断路器的打开, 能保证服务调用者在调用异常服务时, 快速返回结果, 避免大量的同步等待, 减少服务调用者的资源消耗.并且断路器能在打开一段时间后继续侦测请求执行结果, 判断断路器是否能关闭, 恢复服务的正常调用.

降级

为了在整体资源不够的时候, 适当放弃部分服务, 将主要的资源投放到核心服务中, 待渡过难关后, 再重启已经关闭的服务, 保证了系统核心服务的稳定. 当服务停掉后, 自动进入fallback替换主方法.

用fallback方法代替主方法执行并返回结果, 对失败的服务进行降级. 当调用服务失败次数在一段时间内超过了断路器的阈值时, 断路器将打开, 不再进行真正的调用, 而是快速失败, 直接执行fallback逻辑. 服务降级保护了服务调用者的逻辑.

熔断和降级:

共同点:

  1. 为了防止系统崩溃, 保证主要功能的可用性和可靠性
  2. 用户体验到某些功能不可用.

不同点:

  1. 熔断由下级故障触发, 主动惹祸.
  2. 降级由调用方从负荷角度触发

Hystrix

Hystrix实现了超时机制和断路器模式

Hystrix是Netflix开源的一个类库, 用于隔离远程系统,服务或者第三方库, 防止级联失败, 从而提升系统的可用性和容错性. 主要有以下几点功能.

  1. 为系统提供保护机制. 在依赖的服务出现高延迟或失败时, 为系统提供保护和控制.
  2. 防止雪崩.
  3. 包裹请求: 使用HystrixCommand包裹对依赖的调用逻辑, 每个命令在独立的线程中运行.
  4. 跳闸机制: 当某服务失败率达到一定阈值时, Hystrix可以自动跳闸, 停止请求该服务一段时间.
  5. 资源隔离: Hystrix为每个请求的依赖都维护了一个小型线程池, 如果该线程池已满, 发往该依赖的请求就会被立即拒绝, 而不是排队等候, 从而加速失败判定. 防止级联失败.
  6. 快速失败: Fail Fast, 同时能快速恢复. 侧重点是: 不去真正的请求服务, 发生异常再返回.
  7. 监控: Hystrix可以实时监控运行指标和配置的变化, 提供近实时的监控, 报警, 运维控制.
  8. 回退机制: fallback, 当请求失败, 超时, 被拒绝,或当断路器被打开时, 执行回退逻辑 . 回退逻辑我们自定义,提供优雅的服务降级.
  9. 自我修复: 断路器打开一段时间后, 会自动进入”半开”状态, 可以进行打开, 关闭,半开状态的转换.

   转载规则


《Spring Cloud》 echi1995 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
多级缓存架构 多级缓存架构
多级缓存架构Lua标题明明是缓存架构, 为什么要提到Lua呢. 因为Lua是一个脚本语言, 可以和各种应用程序结合使用, 方便扩展. 比如生成验证码的功能, 会直接发送到server中, 由java生成并返回, 但是在tomcat接收请求之
下一篇 
Zookeeper Zookeeper
ZookeeperZookeeper做分布式协调服务. 可以看到, Zookeeper和redis的主从复制很像, 也是有一主多从的. 那么Zookeeper一定也有redis一样的问题, 如果主机挂了怎么办. 是不是也要引入哨兵模式来实
  目录