上一章講述了服務(wù)注冊和發(fā)現(xiàn)組件Eureka,同時(shí)追蹤源碼深入講解了Eureka的機(jī)制,并通過案例講解了如何構(gòu)建高可用的EurekaServer。本章講解如何使用 RestTemplate和Ribbon相結(jié)合作為服務(wù)消費(fèi)者去消費(fèi)服務(wù),同時(shí)從源碼的角度來深入講解 Ribbon。
一、RestTemplate簡介
??RestTemplate是Spring Resources中一個(gè)訪問第三方RESTful API接口的網(wǎng)絡(luò)請求框架。RestTemplate的設(shè)計(jì)原則和其他SpringTemplate (例如 JdbcTemplate、 JmsTemplate)類似,都是為執(zhí)行復(fù)雜任務(wù)提供了一個(gè)具有默認(rèn)行為的簡單方法。
??RestTemplate是用來消費(fèi)REST服務(wù)的,所以RestTemplate 的主要方法都與REST的Http協(xié)議的一些方法緊密相連,例如HEAD、GET、POST、PUT、DELETE和OPTIONS等方法, 這些方法在RestTemplate類對應(yīng)的方法為headForHeaders()、getForObject()、postForObject()、put()和delete()等。
二、Ribbon簡介
??負(fù)載均衡是指將負(fù)載分?jǐn)偟蕉鄠€(gè)執(zhí)行單元上,常見的負(fù)載均衡有兩種方式。一種是獨(dú)立進(jìn)程單元,通過負(fù)載均衡策略,將請求轉(zhuǎn)發(fā)到不同的執(zhí)行單元上,例如Ngnix。另一種是將負(fù)載均衡邏輯以代碼的形式封裝到服務(wù)消費(fèi)者的客戶端上,服務(wù)消費(fèi)者客戶端維護(hù)了一份服務(wù)提供者的信息列表,有了信息列表,通過負(fù)載均衡策略將請求分?jǐn)偨o多個(gè)服務(wù)提供者,從而達(dá)到負(fù)載均衡的目的。
??Ribbon是Netflix公司開源的一個(gè)負(fù)載均衡的組件,它屬于上述的第二種方式,是將負(fù)載均衡邏輯封裝在客戶端中,并且運(yùn)行在客戶端的進(jìn)程里。Ribbon是一個(gè)經(jīng)過了云端測試的IPC庫,可以很好地控制HTTP和TCP 客戶端的負(fù)載均衡行為。
??在SpringCloud構(gòu)建的微服務(wù)系統(tǒng)中, Ribbon作為服務(wù)消費(fèi)者的負(fù)載均衡器,有兩種使用方式,一種是和RestTemplate相結(jié)合,另一種是和Feign相結(jié)合。Feign已經(jīng)默認(rèn)集成了Ribbon, 關(guān)于Feign的內(nèi)容將會在下一章進(jìn)行詳細(xì)講解。
??Ribbon有很多子模塊,但很多模塊沒有用于生產(chǎn)環(huán)境 ,目前Netflix公司用于生產(chǎn)環(huán)境的Ribbon子模塊如下:
- ribbon-loadbalancer: 可以獨(dú)立使用或與其他模塊一起使用的負(fù)載均衡器API。
- ribbon-eureka: Ribbon結(jié)合Eureka客戶端的API,為負(fù)載均衡器提供動態(tài)服務(wù)注冊列表信息。
- ribbon-core: Ribbon的核心API
三、使用RestTemplate和Ribbo和Ribbon來消費(fèi)服務(wù)
??本案例是上一節(jié)案例的基礎(chǔ)上進(jìn)行改造的,先回顧一下上一節(jié)中的代碼結(jié)構(gòu),它包括一個(gè)服務(wù)注冊中心eureka-server、一個(gè)服務(wù)提供者eureka-client。eureka-client向eureka-server注冊服務(wù),并且eureka-client提供了一個(gè)“ /hi"API接口,用于提供服務(wù)。
??啟動eureka-server, 端口為8671。啟動兩個(gè)eureka-client實(shí)例,端口分別為 8762和8763。 啟動完成后,在瀏覽器上訪問 http://localhost:8671/,瀏覽器顯示 eureka-client 的兩個(gè)實(shí)例已經(jīng)成功向服務(wù)注冊中心注冊,它們的端口分別為8672和8673,如下所示。
??創(chuàng)建完成 eureka-ribbon-client 的 Module 工程之后 , 在其pom文件中引入相關(guān)的依賴,包括繼承了主Maven工程的pom文件,引入了EurekaClient的起步依賴spring-cloud-starter- eureka、Ribbon的起步依賴spring-cloud-starter-ribbon,以及Web的起步依賴spring-boot-starter-web, 代碼如下:
<parent>
<groupId>com.hand</groupId>
<artifactId>macro-service</artifactId>
<version>1.0-SNAPSHOT</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-ribbon</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
macro-service為主模塊的配置,內(nèi)容如下:
<groupId>com.hand</groupId>
<artifactId>macro-service</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.10.RELEASE</version>
<relativePath/>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring.cloud.vension>Dalston.SR1</spring.cloud.vension>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring.cloud.vension}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
??在工程的配置文件appIication.yml做程序的相關(guān)配置,包括指定程序名為 eureka-ribbon--client,程序的端口號為8674,服務(wù)的注冊地址http://localhost:8761/eureka/,代碼如下:
spring:
application:
name: eureka-ribbon-client
server:
port: 8674
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8671/eureka/
??另外,作為EurekaClient需要在程序的入口類加上注解@EnableEurekaClient開啟EurekaClient功能,代碼如下:
@SpringBootApplication
@EnableEurekaClient
public class EurekaRibbonClientApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaRibbonClientApplication.class, args);
}
}
??寫一個(gè)RESTful API接口,在該API接口內(nèi)部需要調(diào)用eureka-client的API接口"/hi”, 即服務(wù)消費(fèi)。由于eureka-client為兩個(gè)實(shí)例,它們的端口為8672和8673。在調(diào)用eureka-client的API接口“/hi”時(shí)希望做到輪流訪問這兩個(gè)實(shí)例,這時(shí)就需要將RestTemplate和Ribbon相結(jié)合,進(jìn)行負(fù)載均衡。
??首先需要在程序的IoC容器中注入一個(gè) restTemplate的Bean,并在這個(gè)Bean上加上@LoadBalanced注解,此時(shí)RestTemplate就結(jié)合了Ribbon開啟了負(fù)載均衡功能 ,代碼如下:
@Configuration
public class RibbonConfig {
@Bean
@LoadBalanced
RestTemplate restTemplate(){
return new RestTemplate();
}
}
??寫一個(gè)RibbonService類,在該類的 hi()方法用 restTemplate 調(diào)用eureka-client的API接口,此時(shí)Uri上不需要使用硬編碼(例如IP地址),只需要寫服務(wù)名eureka-client 即可,代碼如下:
@Service
public class RibbonService {
@Autowired
private RestTemplate restTemplate;
public String hi(String name) {
return restTemplate.getForObject("http://eureka-client/hi?name=" + name, String.class);
}
}
寫一個(gè)RibbonController類,為該類加上@RestController注解,開啟RestController的功能, 寫一個(gè)“/hi” Get方法的接口,調(diào)用RibbonService類的hi()方法,代碼如下:
@RestController
public class RibbonController {
@Autowired
private RibbonService ribbonService;
@GetMapping(value = "/hi")
public String hi(@RequestParam String name){
return ribbonService.hi(name);
}
}
啟動eureka-ribbon-client工程,在瀏覽器上訪問http://localhost:8671,顯示的EurekaServer 的主界面如下圖所示。在主界面上發(fā)現(xiàn)有兩個(gè)服務(wù)被注冊,分別為eureka-client和eureka-ribbon-client,其中eureka-client有兩個(gè)實(shí)例,端口為8672和8673,而eureka-ribbon-client的端口為8674
在瀏覽器上多次訪問 http://localhost:8674/hi?name=ben,瀏覽器會輪流顯示如下內(nèi)容:
hi ben, i am from port:8672
hi ben, i am from port:8673
四、LoadBalancerClient簡介
??負(fù)載均衡器的核心類為LoadBalancerClient, LoadBalancerCiient可以獲取負(fù)載均衡的服務(wù)提供者的實(shí)例信息。為了演示,在RibbonController重新寫一個(gè)接口“/testRibbon”,通過LoadBalancerCIient去選擇一個(gè)eureka-client的服務(wù)實(shí)例的信息,并將該信息返回,繼續(xù)在eureka-ribbon-client工程上修改,代碼如下:
@RestController
public class RibbonController {
...//省略代碼
@Autowired
private LoadBalancerClient loadBalancer;
@GetMapping ("/testRibbon")
public String testRibbon() {
ServiceInstance instance = loadBalancer.choose("eureka-client");
return instance.getHost() + ":" + instance.getPort();
}
}
??重新啟動工程,在瀏覽器上多次訪問http://localhost:8764/testRibbon,瀏覽器會輪流顯示如下內(nèi)容 :
localhost:8672
localhost:8673
??可見,LoadBalancerClient 的 choose(”eureka-client'’)方法可以輪流得到 eureka-client 的兩個(gè) 服務(wù)實(shí)例的信息。
??負(fù)載均衡器LoadBalancerClient是從EurekaClient獲取服務(wù)注冊列表信息的,并將服務(wù)注冊列表信息緩存了一份。在LoadBalancerCJient 調(diào)用choose()方法時(shí),根據(jù)負(fù)載均衡策略選擇一個(gè)服務(wù)實(shí)例的信息,從而進(jìn)行了負(fù)載均衡。 LoadBalancerClient也可以不從EurekaClient獲取注冊列表信息, 這時(shí)需要自己維護(hù)一份服務(wù)注冊列表信息。需要修改application.xml的配置信息,通過stores.ribbon.listOfServers來配置這些服務(wù)實(shí)例的Uri。
#禁止Ribbon從Eureka獲取注冊列表信息
ribbon:
eureka:
enabled: false
#手動配置服務(wù)列表
stores:
ribbon:
listOfServers: example1.com,example2.com
@RestController
public class RibbonController {
@Autowired
private LoadBalancerClient loadBalancer;
@GetMapping ("/testSelfConfigRibbon")
public String testSelfConfigRibbon() {
ServiceInstance instance = loadBalancer.choose("stores");
return instance.getHost() + ":" + instance.getPort();
}
啟動工程 ,在瀏覽器上多次訪問內(nèi)容:http://localhost:8769/testRibbon,瀏覽器會交替出現(xiàn)以下內(nèi)容:
example1.com:80
example2.com:80
??由此,我們知道在Ribbon中的負(fù)載均衡客戶端為LoadBalancerClient。在SpringCloud項(xiàng)目中,負(fù)載均衡器Ribbon會默認(rèn)從EurekaClient的服務(wù)注冊列表中獲取服務(wù)的信息,并緩存一份。根據(jù)緩存的服務(wù)注冊列表信息,可以通過LoadBalancerClient來選擇不同的服務(wù)實(shí)例, 從而實(shí)現(xiàn)負(fù)載均衡。如果禁止Ribbon從Eureka獲取注冊列表信息,則需要自己去維護(hù)一份服務(wù)注冊列表信息。根據(jù)自己維護(hù)服務(wù)注冊列表的信息,Ribbon也可以實(shí)現(xiàn)負(fù)載均衡。
五、源碼解析Ribbon
??為了深入理解Ribbon,通過查看源碼來分析Ribbon如何和RestTemplate相結(jié)合來做負(fù)載均衡。開啟負(fù)載均衡的關(guān)鍵在@LoadBalanced這個(gè)注解上,首先從這個(gè)注解入手。
@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Qualifier
public @interface LoadBalanced {
}
??上面是@LoadBalanced的定義,這就是一個(gè)普通的標(biāo)記注解,作用就是修飾RestTemplate讓其擁有負(fù)載均衡的能力,全局搜索發(fā)現(xiàn)在LoadBalancerAutoConfiguration.java這個(gè)類里用到了。
@Configuration
@ConditionalOnClass(RestTemplate.class)
@ConditionalOnBean(LoadBalancerClient.class)
@EnableConfigurationProperties(LoadBalancerRetryProperties.class)
public class LoadBalancerAutoConfiguration {
@LoadBalanced
@Autowired(required = false)
private List<RestTemplate> restTemplates = Collections.emptyList();
@Bean
public SmartInitializingSingleton loadBalancedRestTemplateInitializer(
final List<RestTemplateCustomizer> customizers) {
return new SmartInitializingSingleton() {
@Override
public void afterSingletonsInstantiated() {
for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {
for (RestTemplateCustomizer customizer : customizers) {
customizer.customize(restTemplate);
}
}
}
};
}
//這里的restTemplates是所有的被@LoadBalanced注解的集合,這就是標(biāo)記注解的作用(Autowired是可以集合注入的)
@Autowired(required = false)
private List<LoadBalancerRequestTransformer> transformers = Collections.emptyList();
@Bean
@ConditionalOnMissingBean
public LoadBalancerRequestFactory loadBalancerRequestFactory(
LoadBalancerClient loadBalancerClient) {
return new LoadBalancerRequestFactory(loadBalancerClient, transformers);
}
//生成一個(gè)LoadBalancerInterceptor的Bean
@Configuration
@ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
static class LoadBalancerInterceptorConfig {
@Bean
public LoadBalancerInterceptor ribbonInterceptor(
LoadBalancerClient loadBalancerClient,
LoadBalancerRequestFactory requestFactory) {
return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
}
//給注解了@LoadBalanced的RestTemplate加上攔截器
@Bean
@ConditionalOnMissingBean
public RestTemplateCustomizer restTemplateCustomizer(
final LoadBalancerInterceptor loadBalancerInterceptor) {
return new RestTemplateCustomizer() {
@Override
public void customize(RestTemplate restTemplate) {
List<ClientHttpRequestInterceptor> list = new ArrayList<>(
restTemplate.getInterceptors());
list.add(loadBalancerInterceptor);
restTemplate.setInterceptors(list);
}
};
}
}
...//省略后面代碼
}
??看到這里,我們應(yīng)該大致知道@loadBalanced的作用了,就是起到一個(gè)標(biāo)記RestTemplate的作用,當(dāng)服務(wù)啟動時(shí),標(biāo)記了的RestTemplate對象里面就會被自動加入LoadBalancerInterceptor攔截器,這樣當(dāng)RestTemplate像外面發(fā)起http請求時(shí),會被LoadBalancerInterceptor的intercept函數(shù)攔截,而intercept里面又調(diào)用了LoadBalancerClient接口實(shí)現(xiàn)類execute方法,我們接著往下看;
public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {
...//省略部分代碼
@Override
public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
final ClientHttpRequestExecution execution) throws IOException {
//這是以服務(wù)名為地址的原始請求:例:http://HI-SERVICE/hi
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, requestFactory.createRequest(request, body, execution));
}
}
這里的LoadBalancerClient的實(shí)現(xiàn)是RibbonLoadBalancerClient,調(diào)用的是RibbonLoadBalancerClient.execute()方法。在execute內(nèi)首先執(zhí)行g(shù)etLoadBalancer(serviceId)獲取ILoadBalancer的實(shí)現(xiàn)者,然后調(diào)用getServer(loadBalancer)方法通過負(fù)載均衡策略獲取服務(wù)。
@Override
public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
//通過serviceId找到ILoadBalancer的實(shí)現(xiàn)者
ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
Server server = getServer(loadBalancer);
if (server == null) {
throw new IllegalStateException("No instances available for " + serviceId);
}
RibbonServer ribbonServer = new RibbonServer(serviceId, server, isSecure(server,
serviceId), serverIntrospector(serviceId).getMetadata(server));
return execute(serviceId, ribbonServer, request);
}
繼續(xù)看getServer(loadBalancer)方法,發(fā)現(xiàn)是調(diào)用ILoadBalancer實(shí)現(xiàn)類對象的chooseServer()方法。
protected Server getServer(ILoadBalancer loadBalancer) {
if (loadBalancer == null) {
return null;
}
return loadBalancer.chooseServer("default"); // TODO: better handling of key
}
這里ILoadBalancer接口有三個(gè)實(shí)現(xiàn)類,通過查看源碼發(fā)現(xiàn),BaseLoadBalancer和ZoneAwareLoadBalancer類里都有具體的實(shí)現(xiàn)方法,到底調(diào)用的是哪個(gè)類的方法呢?
查看RibbonClientConfiguration.java類發(fā)現(xiàn)如下代碼:
@Bean
@ConditionalOnMissingBean
public ILoadBalancer ribbonLoadBalancer(IClientConfig config,
ServerList<Server> serverList, ServerListFilter<Server> serverListFilter,
IRule rule, IPing ping) {
if (this.propertiesFactory.isSet(ILoadBalancer.class, name)) {
return this.propertiesFactory.get(ILoadBalancer.class, config, name);
}
ZoneAwareLoadBalancer<Server> balancer = LoadBalancerBuilder.newBuilder()
.withClientConfig(config).withRule(rule).withPing(ping)
.withServerListFilter(serverListFilter).withDynamicServerList(serverList)
.buildDynamicServerListLoadBalancer();
return balancer;
}
由此可知,攔截器里默認(rèn)調(diào)用的是ZoneAwareLoadBalancer.chooseServer()方法。
public Server chooseServer(Object key) {
//ENABLED默認(rèn)值為true
if (!ENABLED.get() || getLoadBalancerStats().getAvailableZones().size() <= 1) {
logger.debug("Zone aware logic disabled or there is only one zone");
return super.chooseServer(key);
}
Server server = null;
try {
LoadBalancerStats lbStats = getLoadBalancerStats();
Map<String, ZoneSnapshot> zoneSnapshot = ZoneAvoidanceRule.createSnapshot(lbStats);
logger.debug("Zone snapshots: {}", zoneSnapshot);
if (triggeringLoad == null) {
triggeringLoad = DynamicPropertyFactory.getInstance().getDoubleProperty(
"ZoneAwareNIWSDiscoveryLoadBalancer." + this.getName() + ".triggeringLoadPerServerThreshold", 0.2d);
}
if (triggeringBlackoutPercentage == null) {
triggeringBlackoutPercentage = DynamicPropertyFactory.getInstance().getDoubleProperty(
"ZoneAwareNIWSDiscoveryLoadBalancer." + this.getName() + ".avoidZoneWithBlackoutPercetage", 0.99999d);
}
Set<String> availableZones = ZoneAvoidanceRule.getAvailableZones(zoneSnapshot, triggeringLoad.get(), triggeringBlackoutPercentage.get());
logger.debug("Available zones: {}", availableZones);
if (availableZones != null && availableZones.size() < zoneSnapshot.keySet().size()) {
String zone = ZoneAvoidanceRule.randomChooseZone(zoneSnapshot, availableZones);
logger.debug("Zone chosen: {}", zone);
if (zone != null) {
BaseLoadBalancer zoneLoadBalancer = getLoadBalancer(zone);
server = zoneLoadBalancer.chooseServer(key);
}
}
} catch (Exception e) {
logger.error("Error choosing server using zone aware logic for load balancer={}", name, e);
}
if (server != null) {
return server;
} else {
logger.debug("Zone avoidance logic is not invoked.");
return super.chooseServer(key);
}
}
但是由于我是模擬單服務(wù)器測試的,所以是單區(qū)域,通過調(diào)試可以看到空間數(shù)為1,如下圖。所以這里會去調(diào)用ZoneAwareLoadBalancer父類的chooseServer()方法,也就是BaseLoadBalancer的chooseServer()方法。
//BaseLoadBalancer.chooseServer()
public Server chooseServer(Object key) {
if (counter == null) {
counter = createCounter();
}
counter.increment();
if (rule == null) {
return null;
} else {
try {
return rule.choose(key);
} catch (Exception e) {
logger.warn("LoadBalancer [{}]: Error choosing server for key {}", name, key, e);
return null;
}
}
}
接下來就是調(diào)用rule.choose(key);這里是選擇負(fù)載均衡策略。IRule的實(shí)現(xiàn)類如下:
IRule的默認(rèn)實(shí)現(xiàn)類有以下7種。在大多數(shù)情況下,這些默認(rèn)的實(shí)現(xiàn)類是可以滿足需求的,如果有特殊的諦求,可以自己實(shí)現(xiàn)。
- BestAvailableRule: 選擇最小請求數(shù)。
- ClientConfigEnabledRoundRobinRule:輪詢。
- RandornRule: 隨機(jī)選擇一個(gè)server。
- RoundRobinRule: 輪詢選擇server。
- RetryRule: 根據(jù)輪詢的方式重試。
- ZoneAvoidanceRule:根據(jù)server的zone區(qū)域和可用性來輪詢選擇。
- WeightedResponseTirneRule: 根據(jù)響應(yīng)時(shí)間去分配一個(gè)weight,weight越低,被選擇的可能性就越低。
??綜上所述,Ribbon的負(fù)載均衡,主要通過LoadBalancerClient來實(shí)現(xiàn)的,而LoadBalancerClient具體交給了ILoadBalancer來處理,ILoadBalancer通過配置IRule等信息,并向EurekaClient獲取注冊列表的信息,得到注冊列表后,ILoadBalancer根據(jù)IRule的策略進(jìn)行負(fù)載均衡。
??RestTemplate 被@LoadBalance注解后,能使用負(fù)載均衡,主要是維護(hù)了一個(gè)被@LoadBalance注解的RestTemplate列表,并給列表中的RestTemplate添加攔截器,進(jìn)而交給負(fù)載均衡器去處理。