本章节将通过一个简单实例介绍怎样使用 Netflix Ribbon 实现负载均衡调用目标服务。
假如我们创建一个用户服务,该服务将分别运行在本机的 7001、7002 和 7003 三个端口。然后使用 Netflix Ribbon 实现简单的负载均衡调用。注意:这里没有将服务注册到 Eureka 服务端,也不用 Spring Cloud 相关的技术,仅仅是一个简单的 Java 项目。如下图:
上图中,我们将 user 服务分别运行在 7001、7002 和 7003 端口,然后客户端使用 Netflix Ribbon 进行负载均衡调用 user 服务。下面将介绍怎样实现上面实例:
Eureka 服务启动类(点击查看完整的 pom.xml 依赖信息):
package com.hxstrive.springcloud.ribbon_eureka_server; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; @SpringBootApplication @EnableEurekaServer public class RibbonEurekaServerApplication { public static void main(String[] args) { SpringApplication.run(RibbonEurekaServerApplication.class, args); } }
配置 application.yml 内容如下:
# 服务端口 server: port: 8077 # 服务名称 spring: application: name: eureka-server # 服务地址 eureka: instance: hostname: localhost client: # 不向注册中心注册自己 register-with-eureka: false # 取消检索服务 fetch-registry: false # 启动独立模式的Eureka server # 关闭客户端行为,这样它就不会不断尝试并无法到达它的对等端 # defaultZone 默认使用 http://localhost:8761/eureka/ 地址 # 使 serviceUrl 指向与本地实例相同的主机 service-url: defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/ server: # 开启注册中心的保护机制,默认是开启 enable-self-preservation: true # 设置保护机制的阈值,默认是0.85 renewal-percent-threshold: 0.5
更多 Eureka 信息请参考“Netflix Eureka 教程”。
使用 Spring Boot 创建一个简单的 WEB 项目,并且提供一个 REST 接口,获取当前日期信息(点击查看完整的 pom.xml 依赖信息)。代码如下:
package com.hxstrive.springcloud.ribbon_service_users; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.EnableEurekaClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import java.util.Date; @RestController @SpringBootApplication // 开启 Eureka 客户端 @EnableEurekaClient public class RibbonServiceUsersApplication { @Value("${appName:server}") private String appName; public static void main(String[] args) { SpringApplication.run(RibbonServiceUsersApplication.class, args); } @GetMapping("/info") public String getInfo() { return "From " + appName + " - " + new Date(); } }
上面代码中,getInfo() 方法将返回当前日期,前面的 appName 是从我们执行“java -jar”命令时提供的,用于区分启动多个User服务时,说明返回数据来自哪个服务。项目编译成功后,通过 maven package 进行打包,然后执行如下命令:
# 服务1 java -jar ribbon_service_user-0.0.1-SNAPSHOT.jar --appName=user1 --server.port=7001 --eureka.instance.instance-id=user1 # 服务2 java -jar ribbon_service_user-0.0.1-SNAPSHOT.jar --appName=user2 --server.port=7002 --eureka.instance.instance-id=user2 # 服务3 java -jar ribbon_service_user-0.0.1-SNAPSHOT.jar --appName=user3 --server.port=7003 --eureka.instance.instance-id=user3
上面命令将启动三个服务。我们可以使用 http://localhost:7001/info 、http://localhost:7002/info 和 http://localhost:7003/info 三个地址分别调用接口。
注意,除了使用上述的脚本方式启动多个服务外,也可以使用 IDEA 启动多个服务,如下图:
(1)点击 IDEA 运行示例,显示下拉菜单,选择“Edit Configurations”,打开“Run/Debug Configurations”。如下图:
(2)在“Run/Debug Configurations”页面中复制多个示例,如下图中的“RibbonServiceUserApplication-7001”、“RibbonServiceUserApplication-7002”和“RibbonServiceUserApplication-7003”,并且修改对应的程序参数,如下图:
(3)启动运行示例,效果如下图:
使用 IDEA 创建一个简单的 Maven Java 项目。
(1)向项目中添加如下 Maven 依赖(点击查看完整的 pom.xml 依赖信息):
<dependency> <groupId>com.netflix.ribbon</groupId> <artifactId>ribbon</artifactId> <version>2.2.5</version> </dependency> <dependency> <groupId>com.netflix.ribbon</groupId> <artifactId>ribbon-core</artifactId> <version>2.2.5</version> </dependency> <dependency> <groupId>com.netflix.ribbon</groupId> <artifactId>ribbon-httpclient</artifactId> <version>2.2.5</version> </dependency> <dependency> <groupId>com.netflix.ribbon</groupId> <artifactId>ribbon-loadbalancer</artifactId> <version>2.2.5</version> </dependency> <dependency> <groupId>com.netflix.archaius</groupId> <artifactId>archaius-core</artifactId> <version>0.7.4</version> </dependency> <dependency> <groupId>commons-configuration</groupId> <artifactId>commons-configuration</artifactId> <version>1.8</version> </dependency> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>16.0.1</version> </dependency>
(2)添加 application.properties 配置文件,内容如下:
# Max number of retries user.ribbon.MaxAutoRetries=1 # Max number of next servers to retry (excluding the first server) user.ribbon.MaxAutoRetriesNextServer=1 # Whether all operations can be retried for this client user.ribbon.OkToRetryOnAllOperations=true # Interval to refresh the server list from the source user.ribbon.ServerListRefreshInterval=2000 # Connect timeout used by Apache HttpClient user.ribbon.ConnectTimeout=3000 # Read timeout used by Apache HttpClient user.ribbon.ReadTimeout=3000 # Initial list of servers, can be changed via Archaius dynamic property at runtime user.ribbon.listOfServers=localhost:7001,localhost:7002,localhost:7003 user.ribbon.EnablePrimeConnections=true
注意:上面配置中 user.ribbon.listOfServers 配置我们刚刚上面启动的服务地址,分别为 7001、7002 和 7003 端口。而在每个配置项前面拥有前缀“user”,该前缀为客户端名称,后续在 ClientFactory.getNamedClient("user") 代码中使用,用来获取 RestClient 对象。
(3)新增 Demo1.java 文件,代码如下:
package com.hxstrive.springcloud.ribbon_demo1; import com.netflix.client.ClientFactory; import com.netflix.client.http.HttpRequest; import com.netflix.client.http.HttpResponse; import com.netflix.config.ConfigurationManager; import com.netflix.niws.client.http.RestClient; public class Demo1 { public static void main(String[] args) throws Exception{ // 1.加载配置信息 ConfigurationManager.loadPropertiesFromResources("application.properties"); System.out.println(ConfigurationManager.getConfigInstance().getProperty("user.ribbon.listOfServers")); // 2.返回名称为 user 的 RestClient 客户端 // 注意:这里的名称是在 application.properties 文件中配置 ribbon 时的前缀 // user.ribbon.listOfServers=localhost:7001,localhost:7002,localhost:7003 // 上面配置中的 user 就是客户端名称 RestClient client = (RestClient) ClientFactory.getNamedClient("user"); // 3.构建指定 URL 的 HTTP 请求 HttpRequest request = HttpRequest.newBuilder().uri("/info").build(); for (int i = 0; i < 10; i++) { // 4.使用负载均衡算法发起 HTTP 请求 HttpResponse response = client.executeWithLoadBalancer(request); // 打印调用状态和结果 System.out.println("Status code for " + response.getRequestedURI() + " status:" + response.getStatus() + " entity: " + response.getEntity(String.class)); } } }
运行上面代码,输出结果如下:
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder". SLF4J: Defaulting to no-operation (NOP) logger implementation SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details. [localhost:7001, localhost:7002, localhost:7003] Status code for http://localhost:7002/info status:200 entity: From user2 - Fri Mar 26 18:00:09 CST 2021 Status code for http://localhost:7003/info status:200 entity: From user3 - Fri Mar 26 18:00:09 CST 2021 Status code for http://localhost:7001/info status:200 entity: From user1 - Fri Mar 26 18:00:09 CST 2021 Status code for http://localhost:7002/info status:200 entity: From user2 - Fri Mar 26 18:00:09 CST 2021 Status code for http://localhost:7003/info status:200 entity: From user3 - Fri Mar 26 18:00:09 CST 2021 Status code for http://localhost:7001/info status:200 entity: From user1 - Fri Mar 26 18:00:09 CST 2021 Status code for http://localhost:7002/info status:200 entity: From user2 - Fri Mar 26 18:00:09 CST 2021 Status code for http://localhost:7003/info status:200 entity: From user3 - Fri Mar 26 18:00:09 CST 2021 Status code for http://localhost:7001/info status:200 entity: From user1 - Fri Mar 26 18:00:09 CST 2021 Status code for http://localhost:7002/info status:200 entity: From user2 - Fri Mar 26 18:00:09 CST 2021
仔细观察可知,客户端使用了轮询的方式调用服务列表。即调用 7002、7003 和 7001,然后再次调用 7002、7003……