Spring Cloud 快速上手之 Ribbon 負載均衡

Spring Cloud 快速上手之 Ribbon 負載均衡

簡介java

Spring Cloud Ribbon是基於HTTP和TCP的客戶端負載工具,它是基於Netflix Ribbon實現的。經過Spring Cloud的封裝,能夠輕鬆地將面向服務的REST 模板請求,自動轉換成客戶端負載均衡服務調用。提供雲端負載均衡,有多種負載均衡策略可供選擇,可配合服務發現和斷路器使用。mysql


準備工做Ribbon基本配置負載均衡源碼分析負載均衡策略單獨使用飢餓加載REFERENCES獲取更多web

手機用戶請橫屏獲取最佳閱讀體驗,REFERENCES中是本文參考的連接,如須要連接和更多資源,能夠掃碼加入『知識星球』(文末)獲取長期知識分享服務。spring

準備工做

開發環境sql

  • Greenwich.SR5json

  • Spring Boot 2.1.5微信

  • MySQL 5.7架構

  • JDK 1.8app

依賴管理負載均衡

<!--負載均衡 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-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 維護一個子應用程序的上下文,默認是懶加載的,配置飢餓加載後,能夠在啓動時就加載對應子應用程序的上下文,從而提升首次請求的訪問速度。

REFERENCES

  • Spring Cloud 中文索引

  • Spring Cloud Alibaba之負載均衡組件 - Ribbon詳解(三)


獲取更多

掃碼關注架構探險之道,回覆"源碼",獲取本文相關源碼

.

掃碼加入知識星球,獲取更多珍貴筆記、視頻、電子書的等資源。

.


本文分享自微信公衆號 - 架構探險之道(zacsnz1314)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索