简介
Spring Cloud Ribbon是基于HTTP和TCP的客户端负载工具,它是基于Netflix Ribbon实现的。通过Spring Cloud的封装,可以轻松地将面向服务的REST 模板请求,自动转换成客户端负载均衡服务调用。提供云端负载均衡,有多种负载均衡策略可供选择,可配合服务发现和断路器使用。
准备工作
开发环境
依赖管理
<!--负载均衡 Ribbon-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
需要注意的是,在spring-cloud-starter-netflix-eureka-client
默认集成了spring-cloud-starter-netflix-ribbon
,因此可以不引入。
RestTemlate 配置
@Configuration
public class RpcConfig {
@Bean
//添加此注解后,可以直接通过 服务 ID 进行接口调用,而无需输入IP 和端口信息
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
RestController
@RestController
public class UserController {
@Autowired
private IUserService userService;
@Autowired
private LoadBalancerClient loadBalancerClient;
@GetMapping("/user/{id}")
public User findById(@PathVariable Long id) {
return userService.findById(id);
}
@GetMapping("/instance/{instanceId}")
public String instance(@PathVariable String instanceId) {
ServiceInstance choose = loadBalancerClient.choose(instanceId);
HashMap<String, String> instanceInfo = new HashMap<>();
return JSON.toJSONString(choose,true);
}
}
UserServiceImpl
@Service
public class UserServiceImpl implements IUserService {
@Autowired
private RestTemplate restTemplate;
@Override
public User findById(Long id) {
return this.restTemplate.getForObject("http://ms-provider-user-v2/" + id, User.class);
}
}
接口测试
http://localhost:8012/instance/ms-consumer-user-v2-ribbon
GET http://localhost:8012/instance/ms-consumer-user-v2-ribbon
HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Content-Length: 1792
Date: Tue, 05 May 2020 06:10:12 GMT
{
"host": "192.168.0.100",
"instanceId": "192.168.0.100:8012",
"metadata": {
"management.port": "8012"
},
"port": 8012,
"secure": false,
"server": {
"alive": true,
"host": "192.168.0.100",
"hostPort": "192.168.0.100:8012",
"id": "192.168.0.100:8012",
"instanceInfo": {
"actionType": "ADDED",
"appName": "MS-CONSUMER-USER-V2-RIBBON",
"coordinatingDiscoveryServer": false,
"countryId": 1,
"dataCenterInfo": {
"name": "MyOwn"
},
"dirty": false,
"healthCheckUrl": "http://192.168.0.100:8012/actuator/health",
"healthCheckUrls": [
"http://192.168.0.100:8012/actuator/health"
],
"homePageUrl": "http://192.168.0.100:8012/",
"hostName": "192.168.0.100",
"iPAddr": "192.168.0.100",
"id": "192.168.0.100:ms-consumer-user-v2-ribbon:8012",
"instanceId": "192.168.0.100:ms-consumer-user-v2-ribbon:8012",
"lastDirtyTimestamp": 1588658754217,
"lastUpdatedTimestamp": 1588658754758,
"leaseInfo": {
"durationInSecs": 90,
"evictionTimestamp": 0,
"registrationTimestamp": 1588658754758,
"renewalIntervalInSecs": 30,
"renewalTimestamp": 1588659024755,
"serviceUpTimestamp": 1588658754254
},
"metadata": {
"$ref": "$.metadata"
},
"overriddenStatus": "UNKNOWN",
"port": 8012,
"sID": "na",
"securePort": 443,
"secureVipAddress": "ms-consumer-user-v2-ribbon",
"status": "UP",
"statusPageUrl": "http://192.168.0.100:8012/actuator/info",
"vIPAddress": "ms-consumer-user-v2-ribbon",
"version": "unknown"
},
"metaInfo": {
"appName": "MS-CONSUMER-USER-V2-RIBBON",
"instanceId": "192.168.0.100:ms-consumer-user-v2-ribbon:8012",
"serviceIdForDiscovery": "ms-consumer-user-v2-ribbon"
},
"port": 8012,
"readyToServe": true,
"zone": "defaultZone"
},
"serviceId": "ms-consumer-user-v2-ribbon",
"uri": "http://192.168.0.100:8012"
}
Response code: 200; Time: 100ms; Content length: 1792 bytes
http://localhost:8012/user/16
GET http://localhost:8012/user/16
HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Tue, 05 May 2020 06:54:25 GMT
{
"id": 16,
"account": "account5",
"userName": "x_user_5",
"age": 20
}
Response code: 200; Time: 27ms; Content length: 61 bytes
配置服务提供者多实例
两个实例的启动参数分别为--spring.profiles.active=ribbon1
和
--spring.profiles.active=ribbon2
server:
port: 8011
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/db_yier?characterEncoding=UTF-8&rewriteBatchedStatements=true
username: root
password: Abc123++
driver-class-name: com.mysql.jdbc.Driver
jpa:
hibernate:
ddl-auto: update
show-sql: true
database-platform: org.hibernate.dialect.MySQL5Dialect
application:
name: ms-provider-user-v2
eureka:
client:
service-url:
defaultZone: http://localhost:8010/eureka/
instance:
prefer-ip-address: true
info:
app:
name: @project.artifactId@
encoding: @project.build.sourceEncoding@
java:
source: @java.version@
target: @java.version@
---
spring:
profiles: ribbon1
server:
port: 8013
---
spring:
profiles: ribbon2
server:
port: 8014
接口测试
http://localhost:8012/instance/ms-provider-user-v2
//第一次调用
GET http://localhost:8012/instance/ms-provider-user-v2
HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Content-Length: 1729
Date: Tue, 05 May 2020 07:39:49 GMT
{
"host": "192.168.0.100",
"instanceId": "192.168.0.100:8014",
"metadata": {
"management.port": "8014"
},
"port": 8014,
"secure": false,
"server": {
"alive": true,
"host": "192.168.0.100",
"hostPort": "192.168.0.100:8014",
"id": "192.168.0.100:8014",
"instanceInfo": {
"actionType": "ADDED",
"appName": "MS-PROVIDER-USER-V2",
"coordinatingDiscoveryServer": false,
"countryId": 1,
"dataCenterInfo": {
"name": "MyOwn"
},
"dirty": false,
"healthCheckUrl": "http://192.168.0.100:8014/actuator/health",
"healthCheckUrls": [
"http://192.168.0.100:8014/actuator/health"
],
"homePageUrl": "http://192.168.0.100:8014/",
"hostName": "192.168.0.100",
"iPAddr": "192.168.0.100",
"id": "192.168.0.100:ms-provider-user-v2:8014",
"instanceId": "192.168.0.100:ms-provider-user-v2:8014",
"lastDirtyTimestamp": 1588663883990,
"lastUpdatedTimestamp": 1588663884538,
"leaseInfo": {
"durationInSecs": 90,
"evictionTimestamp": 0,
"registrationTimestamp": 1588663884538,
"renewalIntervalInSecs": 30,
"renewalTimestamp": 1588664154534,
"serviceUpTimestamp": 1588663884034
},
"metadata": {
"$ref": "$.metadata"
},
"overriddenStatus": "UNKNOWN",
"port": 8014,
"sID": "na",
"securePort": 443,
"secureVipAddress": "ms-provider-user-v2",
"status": "UP",
"statusPageUrl": "http://192.168.0.100:8014/actuator/info",
"vIPAddress": "ms-provider-user-v2",
"version": "unknown"
},
"metaInfo": {
"appName": "MS-PROVIDER-USER-V2",
"instanceId": "192.168.0.100:ms-provider-user-v2:8014",
"serviceIdForDiscovery": "ms-provider-user-v2"
},
"port": 8014,
"readyToServe": true,
"zone": "defaultZone"
},
"serviceId": "ms-provider-user-v2",
"uri": "http://192.168.0.100:8014"
}
Response code: 200; Time: 12ms; Content length: 1729 bytes
//第二次调用
GET http://localhost:8012/instance/ms-provider-user-v2
HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Content-Length: 1729
Date: Tue, 05 May 2020 07:40:26 GMT
{
"host": "192.168.0.100",
"instanceId": "192.168.0.100:8013",
"metadata": {
"management.port": "8013"
},
"port": 8013,
"secure": false,
"server": {
"alive": true,
"host": "192.168.0.100",
"hostPort": "192.168.0.100:8013",
"id": "192.168.0.100:8013",
"instanceInfo": {
"actionType": "ADDED",
"appName": "MS-PROVIDER-USER-V2",
"coordinatingDiscoveryServer": false,
"countryId": 1,
"dataCenterInfo": {
"name": "MyOwn"
},
"dirty": false,
"healthCheckUrl": "http://192.168.0.100:8013/actuator/health",
"healthCheckUrls": [
"http://192.168.0.100:8013/actuator/health"
],
"homePageUrl": "http://192.168.0.100:8013/",
"hostName": "192.168.0.100",
"iPAddr": "192.168.0.100",
"id": "192.168.0.100:ms-provider-user-v2:8013",
"instanceId": "192.168.0.100:ms-provider-user-v2:8013",
"lastDirtyTimestamp": 1588663874416,
"lastUpdatedTimestamp": 1588663874966,
"leaseInfo": {
"durationInSecs": 90,
"evictionTimestamp": 0,
"registrationTimestamp": 1588663874966,
"renewalIntervalInSecs": 30,
"renewalTimestamp": 1588664144962,
"serviceUpTimestamp": 1588663874460
},
"metadata": {
"$ref": "$.metadata"
},
"overriddenStatus": "UNKNOWN",
"port": 8013,
"sID": "na",
"securePort": 443,
"secureVipAddress": "ms-provider-user-v2",
"status": "UP",
"statusPageUrl": "http://192.168.0.100:8013/actuator/info",
"vIPAddress": "ms-provider-user-v2",
"version": "unknown"
},
"metaInfo": {
"appName": "MS-PROVIDER-USER-V2",
"instanceId": "192.168.0.100:ms-provider-user-v2:8013",
"serviceIdForDiscovery": "ms-provider-user-v2"
},
"port": 8013,
"readyToServe": true,
"zone": "defaultZone"
},
"serviceId": "ms-provider-user-v2",
"uri": "http://192.168.0.100:8013"
}
Response code: 200; Time: 12ms; Content length: 1729 bytes
关注"instanceId": "192.168.0.100:ms-provider-user-v2:8013",
和 "instanceId": "192.168.0.100:ms-provider-user-v2:8014",
可以发现实现了负载均衡,两次请求被均匀的分配到2个ms-provider-user-v2
服务实例上。
LoadBalancerInterceptor
LoadBalancerInterceptor
是注解@LoadBalanced
的关联实现类。
/**
* @author Spencer Gibb
* @author Dave Syer
* @author Ryan Baxter
* @author William Tran
*/
public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {
private LoadBalancerClient loadBalancer;
private LoadBalancerRequestFactory requestFactory;
public LoadBalancerInterceptor(LoadBalancerClient loadBalancer,
LoadBalancerRequestFactory requestFactory) {
this.loadBalancer = loadBalancer;
this.requestFactory = requestFactory;
}
public LoadBalancerInterceptor(LoadBalancerClient loadBalancer) {
// for backwards compatibility
this(loadBalancer, new LoadBalancerRequestFactory(loadBalancer));
}
@Override
public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
final ClientHttpRequestExecution execution) throws IOException {
final URI originalUri = request.getURI();
String serviceName = originalUri.getHost();
Assert.state(serviceName != null,
"Request URI does not contain a valid hostname: " + originalUri);
return this.loadBalancer.execute(serviceName,
this.requestFactory.createRequest(request, body, execution));
}
}
在LoadBalancerAutoConfiguration
中,会对RestTemplate
进行增强处理:
//传入拦截器
@Bean
@ConditionalOnMissingBean
public RestTemplateCustomizer restTemplateCustomizer(
final LoadBalancerInterceptor loadBalancerInterceptor) {
return restTemplate -> {
List<ClientHttpRequestInterceptor> list = new ArrayList<>(
restTemplate.getInterceptors());
list.add(loadBalancerInterceptor);
restTemplate.setInterceptors(list);
};
}
//加工RestTemplate
@Bean
public SmartInitializingSingleton loadBalancedRestTemplateInitializerDeprecated(
final ObjectProvider<List<RestTemplateCustomizer>> restTemplateCustomizers) {
return () -> restTemplateCustomizers.ifAvailable(customizers -> {
for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {
for (RestTemplateCustomizer customizer : customizers) {
customizer.customize(restTemplate);
}
}
});
}
而在 Spring 容器注入单例 Bean 的时候,会在DefaultListableBeanFactory
中调用如下一段代码:
// Trigger post-initialization callback for all applicable beans...
for (String beanName : beanNames) {
Object singletonInstance = getSingleton(beanName);
if (singletonInstance instanceof SmartInitializingSingleton) {
final SmartInitializingSingleton smartSingleton = (SmartInitializingSingleton) singletonInstance;
if (System.getSecurityManager() != null) {
AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
smartSingleton.afterSingletonsInstantiated();
return null;
}, getAccessControlContext());
}
else {
smartSingleton.afterSingletonsInstantiated();
}
}
}
改变负载均衡策略,配置形式,或者注解形式都可以(IRule)
ms-provider-user-v2:
ribbon:
# 配置随机策略
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
/**
* 配置均衡负载策略
* @return
*/
@Bean
public IRule ribbonRule() {
return new RandomRule();
}
# 测试 LoadBalancerClient 返回的 实例 ms-provider-user-v2 的信息
GET http://localhost:8012/instance/ms-provider-user-v2
Accept: application/json
通过返回的结果测试,可以验证是随机的,而非默认的轮询选择机制。
依赖配置
删除spring-cloud-starter-netflix-eureka-client
,并引入配置:
<!--负载均衡 Ribbon-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
配置
server:
port: 8012
spring:
application:
name: ms-consumer-user-v2-ribbon-single
#ms-provider-user-v2:
# ribbon:
# # 配置随机策略
# NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
# 单独使用,不使用 Eureka
ms-provider-user-v2:
ribbon:
listOfServers: localhost:8013,localhost:8014
测试
GET http://localhost:8012/instance/ms-provider-user-v2
HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Content-Length: 390
Date: Tue, 05 May 2020 08:27:49 GMT
{
"host": "localhost",
"instanceId": "localhost:8013",
"metadata": {},
"port": 8013,
"secure": false,
"server": {
"alive": true,
"host": "localhost",
"hostPort": "localhost:8013",
"id": "localhost:8013",
"metaInfo": {
"instanceId": "localhost:8013"
},
"port": 8013,
"readyToServe": true,
"zone": "UNKNOWN"
},
"serviceId": "ms-provider-user-v2",
"uri": "http://localhost:8013"
}
Response code: 200; Time: 507ms; Content length: 390 bytes
结果轮询返回的端口为 8013 或 8014。
默认情况下Ribbon是懒加载的。当服务起动好之后,第一次请求是非常慢的,第二次之后就快很多。其解决方式:开启饥饿加载。
ribbon:
eager-load:
# 开启饥饿加载
enabled: true
# 为哪些服务的名称开启饥饿加载,多个用逗号分隔
clients: server-1,server-2,server-3
Spring Cloud 会为每个名称的 Ribbon Client 维护一个子应用程序的上下文,默认是懒加载的,配置饥饿加载后,可以在启动时就加载对应子应用程序的上下文,从而提高首次请求的访问速度