简介
Spring Cloud Ribbon是基于HTTP和TCP的客户端负载工具,它是基于Netflix Ribbon实现的。通过Spring Cloud的封装,可以轻松地将面向服务的REST 模板请求,自动转换成客户端负载均衡服务调用。提供云端负载均衡,有多种负载均衡策略可供选择,可配合服务发现和断路器使用。
准备工作
开发环境
Greenwich.SR5Spring Boot 2.1.5MySQL 5.7JDK 1.8依赖管理
<!--负载均衡 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,因此可以不引入。
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-ribbonGET 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 byteshttp://localhost:8012/user/16GET 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-3Spring Cloud 会为每个名称的 Ribbon Client 维护一个子应用程序的上下文,默认是懒加载的,配置饥饿加载后,可以在启动时就加载对应子应用程序的上下文,从而提高首次请求的访问速度。
REFERENCESSpring Cloud 中文索引Spring Cloud Alibaba之负载均衡组件 - Ribbon详解(三) ---来自腾讯云社区的---架构探险之道
微信扫一扫打赏
支付宝扫一扫打赏