Skip to content

服务调用与负载均衡 ​

ServiceClient 按服务名调用其他服务:每次调用从注册中心取实例列表,由负载均衡选出一个实例,用 HTTP 发送请求;连不上时自动换一个实例。调用方只关心服务名和路径,实例的增减由 Nacos 实时推送。

调用示例 ​

demo-consumer 调用服务注册与发现中的 demo-provider:

properties
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=version
java
package 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);
  }
}
java
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 中:

java
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();
  }
}
java
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;
  }
}
java
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;
  }
}
java
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 运行两个实例时,请求轮流分到两个实例:

shell
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 ​

java
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。

超时设置:

properties
# 秒
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。

也可以添加自己的拦截器,对每次调用做统一处理:

java
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());
  }
}

负载均衡 ​

properties
# round-robin(默认)或 random(按实例权重随机)
cloud.loadbalancer.type=round-robin

按元数据灰度 ​

给新版本实例加上元数据,请求带上对应的请求头时只发给这些实例;请求头没有匹配的实例时使用全部实例:

properties
# 新版本实例
nacos.discovery.metadata.version=v2
# 调用方与网关
cloud.loadbalancer.metadata-header=gray_version
cloud.loadbalancer.metadata-key=version
shell
for 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 哈希,让同一个用户总是落到同一个实例:

java
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);
  }
}
java
CloudContext.setLoadBalancer(new UserHashLoadBalancer());

LoadBalancerRequest 中的请求头名称为小写。

MIT Licensed