SpringBoot: Eureka入门

1. IP列表

公司发展到一定的规模之后,应用拆分是无可避免的。假设我们有2个服务(服务A、服务B),如果服务A要调用服务B,我们能怎么做呢?最简单的方法是让服务A配置服务B的所有节点的IP,在服务A内部做负载均衡调用服务B的不同节点。

这种方式有3个明显的问题

  1. 服务B的节点变更,需要将服务B的IP列表更新进每个服务A,可以是通过配置文件、配置中心,甚至是数据库
  2. 服务A需要对服务B的节点做健康检测,避免将调用无效的节点
  3. 服务A要实现服务B调用的负载均衡策略

2. 反向代理

熟悉反向代理的人发现,反向代理不正是解决这个问题的办法吗?如果在服务A和服务B之间添加一个nginx,网络拓扑看起来就是这样的

我们需要将服务B的节点配置为upstream,定义nginx的server,服务A通过nginx调用服务B,我们看看下面的核心配置

  1. weight指定每个服务器的权重,值越大调用的次数越多
  2. max_fails指定失败多少次后标记为不可用,fail_timeout指过多长时间后将失败节点再次加入到服务列表中
  3. backup作为备用节点,其他节点不可用的时候,back节点会接受负载均衡
  4. down标记节点下线
  5. ip_hash指定负载均衡策略,通一个IP调度到同一个IP,默认是round-robin
  6. proxy_next_stream指定哪些条件认为是请求失败的(max_fails统计的条件)
bash 复制代码
upstream service_b {  
    # 默认轮询策略round-robin,默认权重1
    server 192.168.100.11 weight=1;  
    server 192.168.100.12 weight=2 max_fails=10 fail_timeout=30s ;
    server 192.168.100.13 backup;                                   # 备用服务器,当其他服务器都不可用时才使用  
    server 192.168.100.14 down;                                     # 标记为不可用,不参与负载均衡  

    ip_hash;                                                       # 负载均衡策略,基于客户端IP选择,默认round-robin
}

server {  
    listen 80;  

    location / {
        proxy_pass http://service_b;  
        proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504; 
    }  
}

通过这种方式基本解决1.1里提到的3个问题了,不过upstream的修改仍然是手动的,而且需要重启nginx。 nginx提供了一个模块ngx_http_dyups_module让我们可以通过HTTP调用动态的修改upstream,如果我们想要将service_b的节点改成下面2个节点,我们可以这么做:

bash 复制代码
curl 127.0.0.1:8000/upstream/service_b  -d 
"server 192.168.100.11:8080 max_fails=3 fail_timeout=5s weight=10;
 server 192.168.100.12:8080 max_fails=3 fail_timeout=5s weight=10;"

不过这个方案没有开始流行就已经没落了,最明显的问题是所有对服务B的调用都要经过中心节点(nginx),而且经过了一次转发,影响了调用性能。

3. 注册中心

大概在2010年开始国内的大中厂都开始走向服务化,但并没有一个成熟的中间件,dubbo、motan、hedwig都是这个时代产物。服务提供者在启动的时候会将自己注册到服务注册中心(zookeeper、consul等实现),服务消费者从注册中心拿到服务提供者的IP,在客户端做负载均衡,直接连接服务提供者的IP,相较于反向代理的方案好处是服务A和服务B是直接调用,避免了一次中间转发。

现在主流的注册中心实现有很多,这里我们选几个常见的对比一下

|-----------|---------|------|--------|-----------|------------------------------|-------|
| 名称 | CAP | 语言 | 算法 | 数据结构 | 场景 | 存储 |
| Zookeeper | CP | Java | Zab协议 | 树ZNode | 服务发现、锁、选主、配置 | 文件 |
| Eureka | AP | Java | Gossip | key-value | 服务发现 | 内存 |
| Nacos | CP + AP | Java | Raft | key-value | 服务发现、锁、选主、配置 配置推送、流量管理(灰度发布) | MySQL |
| Consul | AP | Go | Raft | key-value | 类似于Nacos | 文件 |

  1. Zookeeper最早出现,常用于大数据系统的协调,比如Hadoop/Kafka等,生态成熟,但特性就比较老,出现在云原生之前,没有考虑云原生/微服务的支持
  2. Eureka专门为微服务设计,不支持一致性协议,适用gossip同步数据,选择AP,一致性较弱,功能单一,仅用于服务发现
  3. Nacos基于Raft算法,支持云原生,比如流量管理、灰度发布等功能

一般Java语言开发的新系统的注册中心是在Eureka和Nacos之间选择,Eureka天然和Spring Cloud集成,适用简单,当然功能也相当较弱。我们先来看看Eureka的使用。

4. Eureka入门

1. 创建应用

先创建Spring Boot应用,参见1. 手动创建应用,引入Spring Cloud的依赖管理

XML 复制代码
<project>
    ...
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>   <!-- 创建为SpringBoot应用 -->
        <version>3.2.7</version>
    </parent>
    ...
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-parent</artifactId>  <!-- 使用Spring Cloud依赖 -->
                <version>2023.0.0</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

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

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> <!-- 添加EurekaServer依赖 -->
        </dependency>

    </dependencies>
</project>
2. 启动类

添加启动类,除了正常的Spring Boot应用的注解,额外增加了@EnableEurekaServer注解

java 复制代码
package org.keyniu;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@SpringBootApplication
@EnableEurekaServer
public class StartEurekaServer {
    public static void main(String[] args) {
        SpringApplication.run(StartEurekaServer.class, args);
    }
}
3. 核心配置

创建配置文件 application.yml,包含的内容如下,我们我看看每个配置字段的含义

|------------------------------------------------------|---------------------------------------------------------------------------------------------------------------|------------------------------------------------------|
| 配置 | 含义 | Eureka字段 |
| spring.application.name | 应用名,注册Eureka是的应用名 | application.instance.app |
| eureka.instance.hostname | 运行实例的主机名或IP,默认取当前机器的主机名 ; 为了方便识别一般会在/etc/hosts绑定IP和主机名,设置对应节点机器名 ; Docker环境会选择prefer-ip-address=true,直接采用IP地址 | application.instance.hostName |
| eureka.instance.lease-renewal-interval-in-seconds | 客户端向EurekaServer续租的心跳,默认30s | application.instance.leaseInfo.renewalIntervalInSecs |
| eureka.instance.lease-expiration-duration-in-seconds | 最大的心跳时间间隔,超过时间没心跳的客户端被认为宕机,默认90s | application.instance.leaseInfo.durationInSecs |
| eureka.server.eviction-interval-timer-in-ms | Eureka定时任务,清理lease-expiration-duration没心跳的节点,默认60s | |
| eureka.client.register-with-eureka | 是否向EurekaServer注册自己 | |
| eureka.client.fetch-registry | 是否从EurekaServer获取注册表信息 | |
| eureka.client.registry-fetch-interval-seconds | 从EurekaServer获取注册表信息的时间间隔 | |
| eureka.client.serviceUrl.defaultZone | 客户端向这个地址注册和拉取注册信息,服务端节点用它来感知其他peer节点 | |
| eureka.server.wait-time-in-ms-when-sync-empty | 长轮询的概念,同步数据时如果没有数据变更,请求会阻塞等待的时间 | |
| eureka.server.renewal-percent-threshold | 心跳到底比例,如果少于少于这个比例,不会清理无心跳的节点,默认0.85 | |

注意点:

  1. Eureka服务端,一般register-with-eureka、fetch-registry都设置为false,不注册自己也从eureka拉取注册信息,信息的同步通过内部的gossip协议进行
  2. Eureka服务端,通过eureka.server.serviceUrl的配置感知其他peer节点,运行期间新增/删除节点通过修改配置文件实现,或配置中心配置
  3. Client周期性(lease-renewal-interval-in-seconds)向服务端续租,如果超过最大时间(lease-expiration-duration-in-seconds)没收到续租请求,这个节点被认为不可用
  4. Server周期性(eviction-interval-timer-in-ms)检查服务器节点,清理不可用的节点
  5. Client向Server注册后,Server之间通过gossip同步,同步后每个Server节点存储自己的注册表,evict线程(eviction-interval-timer-in-ms)统计本地注册表,查看阈值(renewal-percent-threshold)看是否进入保护模式,如果不是保护模式,清理过去的节点,节点的leaseInfo中保存了每个节点最后一次renewal的时间
bash 复制代码
server:
  port: 8080
spring:
  application:
    name: keyniu-eureka-server
eureka:
  instance:
    hostname: localhost
    lease-renewal-interval-in-seconds: 30
    lease-expiration-duration-in-seconds: 90
  client:
    register-with-eureka: false
    fetch-registry: false
    service-url:
      defaultZone: http://127.0.0.1:${server.port}/eureka/
  server:
    wait-time-in-ms-when-sync-empty: 5
    enable-self-preservation: true
    eviction-interval-timer-in-ms: 10000
    renewal-percent-threshold: 0.85

5. REST API

默认Eureka的接口返回的XML,可以通过提交请求时指定Accept HTTP头设置响应内容的格式为JSON,这一点对所有接口有效,后续不再赘述

bash 复制代码
curl -s -H 'Accept: application/json' http://192.168.31.52:8080/eureka/apps/${app}/${instanceId}
1. 获取所有实例

通过curl http://192.168.31.52:8080/eureka/apps能查看所有可用的节点列表,包括所有的应用(application),应用下所有的节点(instance),节点的元数据(metadata)、租约(leaseInfo)等等

2. 指定app的实例

通过如下命令读取数据,这里的KEYNIU-EUREKA-SERVER是app,需要替换成对应的值。

bash 复制代码
curl http://192.168.31.52:8080/eureka/apps/${app}
curl http://192.168.31.52:8080/eureka/apps/KEYNIU-EUREKA-SERVER
3. 指定app/instanceId的实例

通过如下命令读取数据,其中KEYNIU-EUREKA-SERVER是app,Randy:keyniu-eureka-server:8080是instanceId

bash 复制代码
curl http://192.168.31.52:8080/eureka/apps/${app}/${instanceId}
curl http://192.168.31.52:8080/eureka/apps/KEYNIU-EUREKA-SERVER/Randy:keyniu-eureka-server:8080
4. 服务上下线

通过修改instance的status字段,我们能控制服务的上下线,比如将节点状态改为OUT_OF_SERVICE

bash 复制代码
curl -v -XPUT http://192.168.31.52:8080/eureka/apps/${app}/${instanceId}/status?value=OUT_OF_SERVICE
curl -v -XPUT http://192.168.31.52:8080/eureka/apps/KEYNIU-EUREKA-SERVER/Randy:keyniu-eureka-server:8080/status?value=OUT_OF_SERVICE

如果想让节点恢复为上线状态,通过如下命令修改

bash 复制代码
curl -v -XDELETE http://192.168.31.52:8080/eureka/apps/${app}/${instanceId}/status?value=UP
curl -v -XDELETE http://192.168.31.52:8080/eureka/apps/KEYNIU-EUREKA-SERVER/Randy:keyniu-eureka-server:8080/status?value=UP
5. 更新元数据

比如我们要在元数据里添加一个admin字段,值是zhangsan,我们可以这么做

bash 复制代码
curl -v -XPUT http://192.168.31.52:8080/eureka/apps/${app}/${instanceId}/metadata?${key}=${value}
curl -v -XPUT http://192.168.31.52:8080/eureka/apps/KEYNIU-EUREKA-SERVER/Randy:keyniu-eureka-server:8080/metadata?admin=zhangsan
6. 新增instance

通过POST请求,请求体可以是JSON,格式按我们读取到的实例格式,假设我们要新增一个节点: Randy1:keyniu-eureka-server:8080, 命令看起来是这样的

bash 复制代码
curl -v -H 'Content-Type: application/json' -XPOST http://192.168.31.52:8080/eureka/apps/KEYNIU-EUREKA-SERVER -d '{
  "instance": {
    "instanceId": "Randy1:keyniu-eureka-server:8080",
    "hostName": "192.168.31.53",
    "app": "KEYNIU-EUREKA-SERVER",
    "ipAddr": "192.168.31.53",
    "status": "UP",
    "overriddenStatus": "UNKNOWN",
    "port": {
      "$": 8080,
      "@enabled": "true"
    },
    "securePort": {
      "$": 443,
      "@enabled": "false"
    },
    "countryId": 1,
    "dataCenterInfo": {
      "@class": "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo",
      "name": "MyOwn"
    },
    "leaseInfo": {
      "renewalIntervalInSecs": 30,
      "durationInSecs": 90,
      "registrationTimestamp": 1719660811233,
      "lastRenewalTimestamp": 1719661187133,
      "evictionTimestamp": 0,
      "serviceUpTimestamp": 1719655154360
    },
    "metadata": {
      "admin": "zhangsan",
      "management.port": "8080",
      "group": "secondKill"
    },
    "homePageUrl": "http://192.168.31.53:8080/",
    "statusPageUrl": "http://192.168.31.53:8080/actuator/info",
    "healthCheckUrl": "http://192.168.31.53:8080/actuator/health",
    "vipAddress": "keyniu-eureka-server",
    "secureVipAddress": "keyniu-eureka-server",
    "isCoordinatingDiscoveryServer": "true",
    "lastUpdatedTimestamp": "1719660811233",
    "lastDirtyTimestamp": "1719655154170",
    "actionType": "ADDED"
  }
}'
7. 删除instance

通过指定app、instanceId删除对应实例

bash 复制代码
curl -XDELETE http://192.168.31.52:8080/eureka/${app}/${instanceId}
curl -XDELETE http://192.168.31.52:8080/eureka/KEYNIU-EUREKA-SERVER/Randy1:keyniu-eureka-server:8080
8. 发送心跳
bash 复制代码
curl -XPUT http://192.168.31.52:8080/eureka/apps/${app}/${instanceId}
curl -XPUT http://192.168.31.52:8080/eureka/apps/KEYNIU-EUREKA-SERVER/Randy1:keyniu-eureka-server:8080

6. 案例解析

现在我们反过来,从Eureka UI来看,显示的每个字段是从何而来,怎么配置

1. Eureka首页

下图是Eureka UI首页显示的内容,我们主要关系其中的6个显示字段,对应图上的数字,下面列表中是它的说明

  1. 环境,通过eureka.environment配置,默认test
  2. 数据中心,通过eureka.datacenter配置,默认MyOwn
  3. 是否删除过去租约,只要不处于自我保护模式,这个值就是true,不启动eureka.server.enable-self-preservation
  4. 进入自我保护模式的阈值,资料上计算公式: 客户端数量 * (60 / lease-renewal-interval-in-seconds ) * renewal-percent-threshold,实测下来有出入,待进一步研究
    1. 仅一个客户端,lease-renewal-interval-in-seconds = 30 , Renews threshod = 1,计算值= 1 * (60/30) * 0.85 = 1.7
    2. 仅一个客户端,lease-renewal-interval-in-seconds = 20 , Renews threshod = 3,计算值= 1 * (60/20) * 0.85 = 2.55
  5. EurekaServer的节点,取值是eureka.client.serviceUrl.defaultZone中配的机器
  6. 注册到EurekaServer的节点,这个值是我们配置的spring.application.name,Status里显示的是我们的nodeName,默认格式是: {hostName}:{app.name}:${server.port}
2. 注册表信息

通过Eureka Server的REST接口,我们能读到注册表信息,下面这个连接能查看所有的APP信息,不过我们这里只有一个节点

http://127.0.0.1:8080/eureka/apps

下面是其中一个节点的内容,下面有序列表的数字对应图片里的数字

  1. dataCenterInfo表示数据中心,一般配置到Eureka Client,定义是个枚举DataCenterInfo.Name类,可选值有Netflix、Amazon、MyOwn,默认MyOwn
    1. 对应配置项eureka.instance.data-center-info
    2. eureka.datacenter也是数据中心的概念,一般配置到Eureka Server
    3. 暂时没看到这两个值如果不同的话有什么影响,但会让人觉得混乱,需要进一步研究
  2. metadata元数据信息,默认只有management.port,可以做自定义配置
    1. 对应配置项eureka.instance.metadata-map
    2. 基于元数据可以做服务分组,用MetadataAwarePredicate实现调用对应分组的节点
  3. instanceId实际是拼接值: {主机名}:{app.name}:${server.port}
  4. overriddenstatus用来覆盖默认状态,节点的状态默认通过心跳来维护,心跳正常状态为UP
  5. lastRenewalTimestamp表示最后一次收到心跳的时间,Eureka用这个时间来判断节点是否可用
  6. lastDirtyTimestamp表示节点信息最后一次更新的时间,用来Eureka Server节点之间的增量
相关推荐
bobz9657 分钟前
ovs patch port 对比 veth pair
后端
Asthenia041217 分钟前
Java受检异常与非受检异常分析
后端
uhakadotcom31 分钟前
快速开始使用 n8n
后端·面试·github
JavaGuide37 分钟前
公司来的新人用字符串存储日期,被组长怒怼了...
后端·mysql
bobz9651 小时前
qemu 网络使用基础
后端
Asthenia04121 小时前
面试攻略:如何应对 Spring 启动流程的层层追问
后端
Asthenia04121 小时前
Spring 启动流程:比喻表达
后端
Asthenia04122 小时前
Spring 启动流程分析-含时序图
后端
ONE_Gua2 小时前
chromium魔改——CDP(Chrome DevTools Protocol)检测01
前端·后端·爬虫
致心2 小时前
记一次debian安装mariadb(带有迁移数据)
后端