目录
Ribbon功能介绍与使用
功能介绍
Ribbon是一个部署在调用端并在生产的云服务项目中经过考验的进程间通信库。Ribbon主要提供
- 客户端负载均衡
- 容错处理
- 支持多协议的异步通信。支持HTTP、TCP、UDP协议。
- 支持缓存和批量处理
Ribbon模块介绍:
- ribbon: Ribbon功能应用入口。使用Ribbon的功能可以通过初始化应用入口,调用接口实现。该模块依赖其他模版实现所需功能,比如容错处理ribbon依赖Histrix。
- ribbon-loadbalancer:负载均衡功能入口。如果仅需要负载均衡功能,可以使用单独使用该模块。
- ribbon-eureka:基于Eureka客户端实现可用服务列表管理
- ribbon-transport: 具备客户端负载均衡能力的,基于RxNetty框架能够支持HTTP、TCP、UDP协议的通信客户端。
- ribbon-httpclient: 具备客户端负载均衡能力的,基于Apache HttpClient,支持REST风格的HTTP请求客户端。
- ribbon-core: 客户端配置APIs和其他共享APIs。
- ribbon-example:使用例子。
Ribbon的使用
Ribbon的负载功能可以单独使用,功能是提供一组服务集合,从中选取一个服务。例子如下:
public class LoadBalancer {
// 负载均衡工具
private final ILoadBalancer loadBalancer;
// 请求重试策略
private final RetryHandler retryHandler = new DefaultLoadBalancerRetryHandler(0, 2, true);
// LoadBalancer 构造函数,主要用于初始化负载均衡工具
public LoadBalancer(List<Server> serverList) {
loadBalancer = LoadBalancerBuilder.newBuilder().buildFixedServerListLoadBalancer(serverList);
}
// 访问服务
public String call(final String path) throws Exception {
// 通过LoadBalancerCommand访问服务
return LoadBalancerCommand.<String>builder()
// 配置负载均衡工具
.withLoadBalancer(loadBalancer)
// 配置无法访问时的重试策略
.withRetryHandler(retryHandler)
.build()
// 异步执行请求调用并返回一个Observable对象
.submit(server -> {
URL url;
try {
url = new URL("http://" + server.getHost() + ":" + server.getPort() + path);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
return Observable.just(conn.getResponseMessage()+"-"+conn.getResponseCode());
} catch (Exception e) {
return Observable.error(e);
}
})
// 转成同步请求
.toBlocking()
// 返回BlockingObservable接收到的第一个响应
.first();
}
// 负载均衡统计信息
public LoadBalancerStats getLoadBalancerStats() {
return ((BaseLoadBalancer) loadBalancer).getLoadBalancerStats();
}
public static void main(String[] args) throws Exception {
LoadBalancer urlLoadBalancer = new LoadBalancer(Lists.newArrayList(
new Server("www.csdn.net", 80),
new Server("www.taobao.com", 80),
new Server("www.baidu.com", 80)
));
// 请求六次
for (int i = 0; i < 6; i++) {
System.out.println(urlLoadBalancer.call("/"));
}
// 打印负载均衡统计信息
System.out.println("=== Load balancer stats ===");
System.out.println(urlLoadBalancer.getLoadBalancerStats());
}
}
// 统计信息如下:
Zone stats: {},Server stats:
[[Server:www.csdn.net:80; Zone:UNKNOWN; Total Requests:2; Successive connection failure:0; Total blackout seconds:0; Last connection made:Tue Sep 01 16:37:41 CST 2020; First connection made: Tue Sep 01 16:37:41 CST 2020; Active Connections:0; total failure count in last (1000) msecs:0; average resp time:87.0; 90 percentile resp time:0.0; 95 percentile resp time:0.0; min resp time:79.0; max resp time:95.0; stddev resp time:8.0]
, [Server:www.baidu.com:80; Zone:UNKNOWN; Total Requests:2; Successive connection failure:0; Total blackout seconds:0; Last connection made:Tue Sep 01 16:37:41 CST 2020; First connection made: Tue Sep 01 16:37:41 CST 2020; Active Connections:0; total failure count in last (1000) msecs:0; average resp time:27.5; 90 percentile resp time:0.0; 95 percentile resp time:0.0; min resp time:27.0; max resp time:28.0; stddev resp time:0.5]
, [Server:www.taobao.com:80; Zone:UNKNOWN; Total Requests:2; Successive connection failure:0; Total blackout seconds:0; Last connection made:Tue Sep 01 16:37:41 CST 2020; First connection made: Tue Sep 01 16:37:41 CST 2020; Active Connections:0; total failure count in last (1000) msecs:0; average resp time:26.5; 90 percentile resp time:0.0; 95 percentile resp time:0.0; min resp time:19.0; max resp time:34.0; stddev resp time:7.5]
]
ribbon-transport提供了更加简洁的HTTP调用,使用例子如下:
public class LoadBalancingExample {
public static void main(String[] args) throws Exception {
// 可用服务列表
List<Server> servers = Lists.newArrayList(new Server("www.qq.com:80"), new Server("www.taobao.com:80"), new Server("www.baidu.com:80"));
// 初始化负载均衡工具
BaseLoadBalancer lb = LoadBalancerBuilder.newBuilder()
.buildFixedServerListLoadBalancer(servers);
// 构建HTTP请求客户端
LoadBalancingHttpClient<ByteBuf, ByteBuf> client = RibbonTransport.newHttpClient(lb);
// 为了获取到异步请求结果,设置同步计数器
final CountDownLatch latch = new CountDownLatch(30);
Observer<HttpClientResponse<ByteBuf>> observer = new Observer<HttpClientResponse<ByteBuf>>() {
@Override
public void onCompleted() {
// 请求响应正常接收到后计数器减一
latch.countDown();
System.out.println("on completed");
}
@Override
public void onError(Throwable e) {
// 请求响应出错后计算器减一
latch.countDown();
System.out.println("on error");
}
@Override
public void onNext(HttpClientResponse<ByteBuf> args) {
System.out.println("on next");
}
};
for (int i = 0; i < 30; i++) {
// 发送三十次请求
HttpClientRequest<ByteBuf> request = HttpClientRequest.createGet("/");
client.submit(request).subscribe(observer);
}
// 等待三十次请求结束
latch.await();
// 打印负载均衡统计信息
System.out.println(lb.getLoadBalancerStats());
}
}
//打印日志:
Zone stats: {},Server stats:
[[Server:www.baidu.com:80; Zone:UNKNOWN; Total Requests:10; Successive connection failure:0; Total blackout seconds:0; Last connection made:Tue Sep 01 16:48:09 CST 2020; First connection made: Tue Sep 01 16:48:09 CST 2020; Active Connections:0; total failure count in last (1000) msecs:0; average resp time:422.5; 90 percentile resp time:0.0; 95 percentile resp time:0.0; min resp time:382.0; max resp time:459.0; stddev resp time:24.618082784814668]
, [Server:www.qq.com:80; Zone:UNKNOWN; Total Requests:10; Successive connection failure:0; Total blackout seconds:0; Last connection made:Tue Sep 01 16:48:09 CST 2020; First connection made: Tue Sep 01 16:48:09 CST 2020; Active Connections:0; total failure count in last (1000) msecs:0; average resp time:113.9; 90 percentile resp time:0.0; 95 percentile resp time:0.0; min resp time:108.0; max resp time:128.0; stddev resp time:6.472248450113717]
, [Server:www.taobao.com:80; Zone:UNKNOWN; Total Requests:10; Successive connection failure:0; Total blackout seconds:0; Last connection made:Tue Sep 01 16:48:09 CST 2020; First connection made: Tue Sep 01 16:48:09 CST 2020; Active Connections:0; total failure count in last (1000) msecs:0; average resp time:127.0; 90 percentile resp time:0.0; 95 percentile resp time:0.0; min resp time:109.0; max resp time:253.0; stddev resp time:42.185305498478954]
]
ribbon-transport 提供的HTTP请求客户端获取请求内容的例子如下:
public class SimpleGet {
public static void main(String[] args) throws Exception {
// 支持负载均衡的HTTP请求客户端
LoadBalancingHttpClient<ByteBuf, ByteBuf> client = RibbonTransport.newHttpClient();
// http请求
HttpClientRequest<ByteBuf> request = HttpClientRequest.createGet("http://www.baidu.com/");
// 同步计数器
final CountDownLatch latch = new CountDownLatch(1);
// 发送HTTP异步请求
client.submit(request)
.toBlocking()
// 在Observable业务结束前阻塞便利每个响应。
.forEach(t1 -> {
System.out.println("Status code: " + t1.getStatus());
// 对请求影响结果做业务处理
t1.getContent().subscribe(new Action1<ByteBuf>() {
@Override
public void call(ByteBuf content) {
// 打印请求结果
System.out.println("Response content: " + content.toString(Charset.defaultCharset()));
// 同步计数器减一
latch.countDown();
}
});
});
// 阻塞2秒等待响应结果
latch.await(2, TimeUnit.SECONDS);
}
}
如果请求在2秒内结束则输出请求结果,如果是请求网页则输出整个网页的HTML内容。如果未能在2秒内结束则,则直接结束。
在ribbon库中对HTTP请求做了更多的封装,提供更加便捷的接口调用,例子如下:
public class ResponseValidatorTest implements HttpResponseValidator {
@Override
public void validate(HttpClientResponse<ByteBuf> response) throws UnsuccessfulResponseException, ServerError {
if (response.getStatus().code() / 100 != 2) {
throw new UnsuccessfulResponseException("Unexpected HTTP status code " + response.getStatus());
}
}
}
private Observer<String> observer = new Observer<String>() {
@Override
public void onSubscribe(@NonNull Disposable d) {
System.out.println("on subscribe");
}
@Override
public void onNext(@NonNull String s) {
System.out.println("on next. message:"+s);
}
@Override
public void onError(@NonNull Throwable e) {
System.out.println("on error");
}
@Override
public void onComplete() {
System.out.println("on complete");
}
};
HttpResourceGroup httpResourceGroup = Ribbon.createHttpResourceGroup("movieServiceClient",
ClientOptions.create()
.withMaxAutoRetriesNextServer(3)
.withConfigurationBasedServerList("localhost:" + port));
HttpRequestTemplate<ByteBuf> requestTemplate = httpResourceGroup.newTemplateBuilder("requestTest", ByteBuf.class)
.withMethod("POST")
.withUriTemplate("/test")
.withHeader("X-Platform-Version", "xyz")
.withHeader("X-Auth-Token", "abc")
.withResponseValidator(new ResponseValidatorTest()).build();
Observable<ByteBuf> observable = requestTemplate.requestBuilder()
.withRawContentSource(Observable.just(""), new ContentTransformer())
.build()
.toObservable();
// 省略更多的RxJava的代码
Ribbon组成
Ribbon多协议异步通信是基于RxJava实现。Ribbon的使用中举了很多基于RxJava的异步通信的例子,如果看不懂RxJava的命令可以直接跳过。在Spring Cloud 技术体系中Ribbon主要还是承担客服端负载均衡。
Ribbon组件构成如下图所示:
- ILoadBalancer:负载均衡器。
- LoadBalancerBuilder: 负载均衡器构造器
- ServerList: 服务列表
- ServerListUpdater: 定时更新的服务列表
- IRule: 路由策略
- IPing: 服务嗅探工具
ServerList
ServerList为ribbon的其他组件提供服务列表。 ServerList定义了两个提供服务列表的方法。
public interface ServerList<T extends Server> {
/**
* 获取初始化时的服务列表数据
*
*/
public List<T> getInitialListOfServers();
/**
* 获取最新的服务列表数据。
*
*/
public List<T> getUpdatedListOfServers();
}
负载均衡工具在初始化时会调用getUpdatedListOfServers拿到最新的服务列表,过滤后更新本地的服务列表。之后通过ServerListUpdater定时定时(默认30秒)调用getUpdatedListOfServers获取到最新服务列表,过滤后更新到本地服务列表中。
ribbon-loadbalancer中提供ServerList 的实现类 ConfigurationBasedServerList。这是基于配置文件获取服务列表。
在Spring Cloud Netflix技术体系中ribbon-eureka提供了基于Eureka客户端服务发现的ServerList实现:DiscoveryEnabledNIWSServerList。
ServerListUpdater
定时更新的服务列表的组件,用于开启一个更新服务列表的任务,终止更新任务等。
public interface ServerListUpdater {
/**
* 更新服务列表的任务
*/
public interface UpdateAction {
void doUpdate();
}
/**
* 开始一个更新服务列表的任务
*
* @param updateAction 更新服务列表任务
*/
void start(UpdateAction updateAction);
/**
* 终止更新服务列表的任务。该方法需要保证幂等性。
*/
void stop();
/**
*
* 最后一次更新服务列表的时间
* @return 最后一次更新的时间。以java.util.Date的字符串格式返回。
*/
String getLastUpdate();
/**
* 返回距离上一次执行更新服务列表的时间,单位是毫秒。
* @return 距离上一次执行服务列表更新的毫秒时间。
*/
long getDurationSinceLastUpdateMs();
/**
*
* @return the number of update cycles missed, if valid
*/
int getNumberMissedCycles();
/**
* @return the number of threads used, if vaid
*/
int getCoreThreads();
}
ServerListUpdater 在 DynamicServerListLoadBalancer初始化过程中初始化ServerListUpdater对象,并在对象初始化好后开启定时任务,按照时间间隔执行更新服务列表的任务。以下是更新服务列表的任务的状态变迁示例:
IPing
IPing是ribbon用于检测本地服务列表是否可用的工具。在LoadBalancerBuilder构造BaseLoadBalancer时指定IPing对象。
DummyPing和NoOpPing不会对服务的可用性做检查,直接返回服务可用(isAlive 永远返回true);PingConstant根据配置信息返回服务是否可用;PingUrl根据请求配置URL地址的返回值判断服务是否可用。
ribbon-eureka提供了NIWSDiscoveryPing,用来校验基于服务发现的服务是否可用。NIWSDiscoveryPing判断服务可用的逻辑是:如果服务是DiscoveryEnabledServer,则根据DiscoveryEnabledServer的instanceInfo.status来判断是否是否可用;如果服务不是DiscoveryEnabledServer则直接返回服务可用。
在BaseLoadBalancer初始化过程中会立即开启任务校验服务列表的可用性,并启动一个Timer任务,定时嗅探服务的可用性。
IRule
路由策略是如何在可用服务列表中选择一个服务的算法。常见路由算法比如Round(循环调用服务),Response Time base(以响应时间作为权重,按照权重调用服务)。IRule定义如下:
public interface IRule{
/*
* 根据关键词从lb.allServers或者lb.upServers查找一个可用的服务
*
* @return 可用的服务。如果没有可用服务则返回null
*/
public Server choose(Object key);
public void setLoadBalancer(ILoadBalancer lb);
public ILoadBalancer getLoadBalancer();
}
IRule的实现类主要围绕实现choose的策略选择。
RandomRule 的策略是通过ThreadLocalRandom在服务数量范围内随机生成一个int值,然后去服务列表中位于该随机值位置的服务。如果获取到服务为空对象或者不是可用服务,则重新获取一个新的服务。
RoundRobinRule 的策略是维护一个AtomicInteger类型的变量nextServerCyclicCounter,每次调用对nextServerCyclicCounter做加一和服务数量进行模运算,得到的余数就是下一个节点的位置。之后在对nextServerCyclicCounter做比较更新(CAS)。如果更新成功则返回余数(下一个服务的位置),否则重复上述操作。
WeightedResponseTimeRule的策略是定时将服务的平均响应时间以累加的方式记录到accumulatedWeights集合中。比如有2个服务,一个服务的平均是2秒,一个服务是3秒,那么accumulatedWeights的集合里面第一个元素是3(总响应时间-当前服务的平均响应时间+累计数,即5-2+0=3。本次计算结束后,累计数为0+3=3。),第二个元素是5(总响应时间-当前服务的平均响应时间+累计数,即5-3+3=5,本次计算结束后累计数是 3+5=8)。在选择服务时,在0到accumulatedWeights集合最后一个值的范围内随机得到一个值,然后看这个值在accumulatedWeights集合的那个位置。这个位置就是选中服务在服务列表中的位置。如果选中的服务是空对象或者不是可用服务,那么重新走选择服务的流程。
ILoadBalancer是负载均衡器,是ribbon提供负载均衡功能的入口。ILoadBalancer本身则会依赖ServerList、ServerListUpdater、IRule、IPing等组件实现动态负载均衡功能。
BaseLoadBalancer是ILoadBalancer的重要实现类,除了基于IRule实现从服务列表中选择一个服务外,还基于IPing实现服务可用性的定时嗅探功能,读取配置信息初始化各种配置,根据配置信息初始化IRule和IPing,服务列表变更监听器以及服务状态变更监听器。
DynamicServerListLoadBalancer是BaseLoadBalancer的重要实现类,额外提供基于ServerList组件获取服务列表,基于ServerListUpdater定时更新服务列表和列表状态。
Spring Cloud中的Ribbon
在Spring Cloud Netflix技术体系中,Ribbon主要为服务间调用提供客户端负载均衡。直接基于Ribbon的服务调用过于繁琐,一般都是基于RestTemplate和Feign做服务调用。
RestTemplate可以直接通过@LoadBalanced注解实现客户端负载均衡,代码如下:
@Component
public class RestTemplateConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
@LoadBalanced 会代理远程调用,利用RibbonLoadBalancerClient执行真正的远程调用。而RibbonLoadBalancerClient在执行远程调用时获取ILoadBalancer,利用ILoadBalancer选择一个服务,并根据服务的信息做服务调用。
使用Feign做远程调用时,会初始化一个feign.Client的实现类LoadBalancerFeignClient。LoadBalancerFeignClient基于ribbon负载均衡和http通信实现远程调用。具体代码如下:
//构建LoadBalancerCommand
protected LoadBalancerCommand<T> buildLoadBalancerCommand(final S request, final IClientConfig config) {
RequestSpecificRetryHandler handler = getRequestSpecificRetryHandler(request, config);
LoadBalancerCommand.Builder<T> builder = LoadBalancerCommand.<T>builder()
.withLoadBalancerContext(this)
.withRetryHandler(handler)
.withLoadBalancerURI(request.getUri());
customizeLoadBalancerCommandBuilder(request, config, builder);
return builder.build();
}
/**
* This method should be used when the caller wants to dispatch the request to a server chosen by
* the load balancer, instead of specifying the server in the request's URI.
* It calculates the final URI by calling {@link #reconstructURIWithServer(com.netflix.loadbalancer.Server, java.net.URI)}
* and then calls {@link #executeWithLoadBalancer(ClientRequest, com.netflix.client.config.IClientConfig)}.
*
* @param request request to be dispatched to a server chosen by the load balancer. The URI can be a partial
* URI which does not contain the host name or the protocol.
*/
public T executeWithLoadBalancer(final S request, final IClientConfig requestConfig) throws ClientException {
// 创建ribbon LoadBalancerCommand。LoadBalancerCommand后续基于RxJava的请求调用的服务信息都是ribbon根据负载均衡策略选择服务的服务信息而进行的服务调用
LoadBalancerCommand<T> command = buildLoadBalancerCommand(request, requestConfig);
try {
return command.submit(
new ServerOperation<T>() {
@Override
public Observable<T> call(Server server) {
URI finalUri = reconstructURIWithServer(server, request.getUri());
S requestForServer = (S) request.replaceUri(finalUri);
try {
return Observable.just(AbstractLoadBalancerAwareClient.this.execute(requestForServer, requestConfig));
}
catch (Exception e) {
return Observable.error(e);
}
}
})
.toBlocking()
.single();
} catch (Exception e) {
Throwable t = e.getCause();
if (t instanceof ClientException) {
throw (ClientException) t;
} else {
throw new ClientException(e);
}
}
}
executeWithLoadBalancer方法和ribbon使用小结列出的例子十分类似。
如果使用Eureka作为注册中心,那么客户端需要引入spring-cloud-starter-netflix-eureka-client依赖,而spring-cloud-starter-netflix-eureka-clien依赖ribbon-eureka。
ribbon-eureka提供了:
- DiscoveryEnabledNIWSServerList(ServerList):基于eureka服务发现实现服务列表的获取。
- DiscoveryEnabledServer(Server):拥有eureka服务状态的服务和服务信息的Server子类。
- EurekaNotificationServerListUpdater(ServerListUpdater):基于eureka服务发现的服务列表更新组件。
- NIWSDiscoveryPing:校验DiscoveryEnabledServer是否可用。基于Eureka服务的服务状态来判断。