公司发展到一定的规模之后,应用拆分是无可避免的。假设我们有2个服务(服务A、服务B),如果服务A要调用服务B,我们能怎么做呢?最简单的方法是让服务A配置服务B的所有节点的IP,在服务A内部做负载均衡调用服务B的不同节点。
这种方式有3个明显的问题
熟悉反向代理的人发现,反向代理不正是解决这个问题的办法吗?如果在服务A和服务B之间添加一个nginx,网络拓扑看起来就是这样的
我们需要将服务B的节点配置为upstream,定义nginx的server,服务A通过nginx调用服务B,我们看看下面的核心配置
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个节点,我们可以这么做:
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),而且经过了一次转发,影响了调用性能。
大概在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 | 文件 |
一般Java语言开发的新系统的注册中心是在Eureka和Nacos之间选择,Eureka天然和Spring Cloud集成,适用简单,当然功能也相当较弱。我们先来看看Eureka的使用。
先创建Spring Boot应用,参见1. 手动创建应用,引入Spring Cloud的依赖管理
... org.springframework.boot spring-boot-starter-parent 3.2.7 ... UTF-8 org.springframework.cloud spring-cloud-starter-parent 2023.0.0 pom import org.springframework.boot spring-boot-starter-web org.springframework.cloud spring-cloud-starter-netflix-eureka-server
添加启动类,除了正常的Spring Boot应用的注解,额外增加了@EnableEurekaServer注解
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); } }
创建配置文件 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 |
注意点:
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
默认Eureka的接口返回的XML,可以通过提交请求时指定Accept HTTP头设置响应内容的格式为JSON,这一点对所有接口有效,后续不再赘述
curl -s -H 'Accept: application/json' http://192.168.31.52:8080/eureka/apps/${app}/${instanceId}
通过curl http://192.168.31.52:8080/eureka/apps能查看所有可用的节点列表,包括所有的应用(application),应用下所有的节点(instance),节点的元数据(metadata)、租约(leaseInfo)等等
通过如下命令读取数据,这里的KEYNIU-EUREKA-SERVER是app,需要替换成对应的值。
curl http://192.168.31.52:8080/eureka/apps/${app} curl http://192.168.31.52:8080/eureka/apps/KEYNIU-EUREKA-SERVER
通过如下命令读取数据,其中KEYNIU-EUREKA-SERVER是app,Randy:keyniu-eureka-server:8080是instanceId
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
通过修改instance的status字段,我们能控制服务的上下线,比如将节点状态改为OUT_OF_SERVICE
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
如果想让节点恢复为上线状态,通过如下命令修改
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
比如我们要在元数据里添加一个admin字段,值是zhangsan,我们可以这么做
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
通过POST请求,请求体可以是JSON,格式按我们读取到的实例格式,假设我们要新增一个节点: Randy1:keyniu-eureka-server:8080, 命令看起来是这样的
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" } }'
通过指定app、instanceId删除对应实例
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
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
现在我们反过来,从Eureka UI来看,显示的每个字段是从何而来,怎么配置
下图是Eureka UI首页显示的内容,我们主要关系其中的6个显示字段,对应图上的数字,下面列表中是它的说明
通过Eureka Server的REST接口,我们能读到注册表信息,下面这个连接能查看所有的APP信息,不过我们这里只有一个节点
http://127.0.0.1:8080/eureka/apps
下面是其中一个节点的内容,下面有序列表的数字对应图片里的数字