服务调用与负载均衡
ServiceClient 按服务名调用其他服务:每次调用从注册中心取实例列表,由负载均衡选出一个实例,用 HTTP 发送请求;连不上时自动换一个实例。调用方只关心服务名和路径,实例的增减由 Nacos 实时推送。
调用示例
demo-consumer 调用服务注册与发现中的 demo-provider:
app.name=demo-consumer
server.port=18082
server.shutdown.hook=true
nacos.server-addr=127.0.0.1:8848
nacos.namespace=hula-dev
nacos.username=nacos
nacos.password=nacos
nacos.discovery.ip=127.0.0.1
# 把当前请求的这些请求头传给被调用的服务
cloud.client.propagate-headers=token,x-trace-id,gray_version
# 请求头 gray_version 与实例元数据 version 相同的实例优先
cloud.loadbalancer.metadata-header=gray_version
cloud.loadbalancer.metadata-key=versionpackage nexus.io.tio.boot.cloud.demo.consumer;
import nexus.io.tio.boot.TioApplication;
import nexus.io.tio.boot.cloud.demo.consumer.config.ConsumerConfig;
public class ConsumerApp {
public static void main(String[] args) {
TioApplication.run(ConsumerApp.class, new ConsumerConfig(), args);
}
}package nexus.io.tio.boot.cloud.demo.consumer.config;
import nexus.io.context.BootConfiguration;
import nexus.io.tio.boot.cloud.demo.consumer.handler.CallProviderHandler;
import nexus.io.tio.boot.cloud.nacos.TioBootNacos;
import nexus.io.tio.boot.server.TioBootServer;
import nexus.io.tio.http.server.router.HttpRequestRouter;
public class ConsumerConfig implements BootConfiguration {
@Override
public void config() throws Exception {
TioBootNacos.start();
HttpRequestRouter router = TioBootServer.me().getRequestRouter();
router.get("/consumer/hello", new CallProviderHandler()::handle);
}
}调用封装在 Service 中:
package nexus.io.tio.boot.cloud.demo.consumer.service;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import nexus.io.tio.boot.cloud.client.ServiceClient;
import nexus.io.tio.boot.cloud.client.ServiceRequest;
import nexus.io.tio.boot.cloud.client.ServiceResponse;
import nexus.io.tio.boot.cloud.demo.consumer.model.ProviderGreeting;
import nexus.io.tio.boot.cloud.demo.consumer.model.ProviderReply;
/** 按服务名调用 demo-provider,实例由 Nacos 提供 */
public class ProviderService {
private static final String PROVIDER = "demo-provider";
public ProviderGreeting hello(String name) {
String path = "/demo/hello?name=" + URLEncoder.encode(name, StandardCharsets.UTF_8);
ServiceResponse response = ServiceClient.me().call(PROVIDER, ServiceRequest.get(path));
ProviderReply reply = response.json(ProviderReply.class);
return reply == null ? null : reply.getData();
}
}package nexus.io.tio.boot.cloud.demo.consumer.model;
public class ProviderReply {
private int code;
private String msg;
private ProviderGreeting data;
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public ProviderGreeting getData() {
return data;
}
public void setData(ProviderGreeting data) {
this.data = data;
}
}package nexus.io.tio.boot.cloud.demo.consumer.model;
public class ProviderGreeting {
private String message;
private String instance;
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public String getInstance() {
return instance;
}
public void setInstance(String instance) {
this.instance = instance;
}
}package nexus.io.tio.boot.cloud.demo.consumer.handler;
import nexus.io.jfinal.aop.Aop;
import nexus.io.model.body.RespBodyVo;
import nexus.io.tio.boot.cloud.demo.consumer.model.ProviderGreeting;
import nexus.io.tio.boot.cloud.demo.consumer.service.ProviderService;
import nexus.io.tio.boot.http.TioRequestContext;
import nexus.io.tio.http.common.HttpRequest;
import nexus.io.tio.http.common.HttpResponse;
import nexus.io.tio.utils.hutool.StrUtil;
/** GET /consumer/hello?name=tio */
public class CallProviderHandler {
public HttpResponse handle(HttpRequest request) {
String name = request.getParam("name");
if (StrUtil.isBlank(name)) {
name = "tio-boot";
}
ProviderService providerService = Aop.get(ProviderService.class);
ProviderGreeting greeting = providerService.hello(name);
return TioRequestContext.getResponse().respond(RespBodyVo.ok(greeting));
}
}demo-provider 运行两个实例时,请求轮流分到两个实例:
for i in 1 2 3 4; do curl -s "localhost:18082/consumer/hello?name=svc"; echo; done
{"data":{"instance":"127.0.0.1:18083","message":"Hi from Nacos, svc"},"code":1,"ok":true}
{"data":{"instance":"127.0.0.1:18081","message":"Hi from Nacos, svc"},"code":1,"ok":true}
{"data":{"instance":"127.0.0.1:18083","message":"Hi from Nacos, svc"},"code":1,"ok":true}
{"data":{"instance":"127.0.0.1:18081","message":"Hi from Nacos, svc"},"code":1,"ok":true}ServiceRequest 与 ServiceResponse
ServiceClient client = ServiceClient.me();
// GET
ServiceResponse r1 = client.get("user-service", "/api/user/1");
// POST JSON,对象自动序列化
ServiceResponse r2 = client.postJson("user-service", "/api/user", user);
// 完整写法:方法、路径、请求头、请求体
ServiceResponse r3 = client.call("user-service",
ServiceRequest.put("/api/user").header("tenantId", "1").json(user));ServiceRequest.get/post/put/delete(path)创建请求,header(name, value)设置请求头,json(object)设置 JSON 请求体,body(bytes, contentType)设置任意请求体。ServiceResponse提供getStatus()、isSuccessful()、getHeader(name)、getBody()、getBodyString()、json(Class)和getInstance()(处理这次请求的实例)。
没有可用实例或所有尝试的实例都连接失败时抛出 ServiceCallException,可以在全局异常处理器中统一转换为错误响应。
失败换实例
- 连接失败(实例已经停止,但还在实例列表中)时,换一个没试过的实例重新发送,所有请求方法都适用,因为请求还没有到达对方。
- GET、HEAD、OPTIONS 在读取响应超时等其他网络错误时也会换实例重试。
- 最多尝试的实例数由
cloud.client.max-attempts控制,默认 2。
超时设置:
# 秒
cloud.client.connect-timeout=3
cloud.client.read-timeout=60
cloud.client.max-attempts=2请求头透传
ServiceClient.me() 默认带有 HeaderPropagationInterceptor:在处理请求的线程中调用其他服务时,把当前请求的指定请求头带过去,被调用的服务拿到同一个 token、租户和链路 ID。请求头列表由 cloud.client.propagate-headers 配置,默认 token,authorization,x-trace-id。
也可以添加自己的拦截器,对每次调用做统一处理:
package nexus.io.tio.boot.cloud.demo.consumer.config;
import nexus.io.tio.boot.cloud.client.ServiceClient;
import nexus.io.tio.boot.cloud.client.ServiceRequest;
import nexus.io.tio.boot.cloud.client.ServiceRequestInterceptor;
import nexus.io.tio.utils.environment.EnvUtils;
public class InternalSecretInterceptor implements ServiceRequestInterceptor {
@Override
public void apply(String serviceId, ServiceRequest request) {
request.headerIfAbsent("x-internal-secret", EnvUtils.get("app.internal.secret", "change-me"));
}
public static void register() {
ServiceClient.me().addInterceptor(new InternalSecretInterceptor());
}
}负载均衡
# round-robin(默认)或 random(按实例权重随机)
cloud.loadbalancer.type=round-robin按元数据灰度
给新版本实例加上元数据,请求带上对应的请求头时只发给这些实例;请求头没有匹配的实例时使用全部实例:
# 新版本实例
nacos.discovery.metadata.version=v2
# 调用方与网关
cloud.loadbalancer.metadata-header=gray_version
cloud.loadbalancer.metadata-key=versionfor i in 1 2 3; do curl -s -H 'gray_version: v2' "localhost:18080/api/consumer/consumer/hello?name=gray"; echo; done
{"data":{"instance":"127.0.0.1:18083","message":"Hi from Nacos, gray"},"code":1,"ok":true}
{"data":{"instance":"127.0.0.1:18083","message":"Hi from Nacos, gray"},"code":1,"ok":true}
{"data":{"instance":"127.0.0.1:18083","message":"Hi from Nacos, gray"},"code":1,"ok":true}这个请求先经过网关、再由 demo-consumer 调用 demo-provider,gray_version 在每一跳都被传递(网关原样转发请求头,consumer 通过 cloud.client.propagate-headers 透传),最终都落在 v2 实例上。
自定义负载均衡
实现 LoadBalancer 并设置到 CloudContext,例如按用户 ID 哈希,让同一个用户总是落到同一个实例:
package nexus.io.tio.boot.cloud.demo.consumer.config;
import java.util.List;
import nexus.io.tio.boot.cloud.discovery.ServiceInstance;
import nexus.io.tio.boot.cloud.loadbalancer.LoadBalancer;
import nexus.io.tio.boot.cloud.loadbalancer.LoadBalancerRequest;
import nexus.io.tio.boot.cloud.loadbalancer.RoundRobinLoadBalancer;
public class UserHashLoadBalancer implements LoadBalancer {
private final LoadBalancer fallback = new RoundRobinLoadBalancer();
@Override
public ServiceInstance choose(String serviceId, List<ServiceInstance> instances, LoadBalancerRequest request) {
String userId = request.getHeader("userid");
if (userId == null) {
return fallback.choose(serviceId, instances, request);
}
int index = (userId.hashCode() & Integer.MAX_VALUE) % instances.size();
return instances.get(index);
}
}CloudContext.setLoadBalancer(new UserHashLoadBalancer());LoadBalancerRequest 中的请求头名称为小写。
