Java 转 go 学习 - kitex(5)

文章目录

  • [1. 负载均衡 + 自定义服务熔断](#1. 负载均衡 + 自定义服务熔断)
    • [1.1 介绍](#1.1 介绍)
    • [1.2 thrift 文件](#1.2 thrift 文件)
    • [1.3 自定义熔断器](#1.3 自定义熔断器)
    • [1.4 注册中心](#1.4 注册中心)
    • [1.5 Server 服务启动](#1.5 Server 服务启动)
    • [1.6 服务端处理函数](#1.6 服务端处理函数)
    • [1.7 客户端启动](#1.7 客户端启动)
    • [1.8 服务启动输出](#1.8 服务启动输出)
  • [2. 自定义负载均衡算法](#2. 自定义负载均衡算法)
    • [2.1 负载均衡接口](#2.1 负载均衡接口)
    • [2.2 实现接口](#2.2 实现接口)
    • [2.3 启动示例](#2.3 启动示例)
  • [3. 超时](#3. 超时)
  • [4. 小结](#4. 小结)

本系列文章:


1. 负载均衡 + 自定义服务熔断

1.1 介绍

上一篇文章我们学习了服务熔断和负载均衡,这一篇文章学习下自定义服务熔断 + 负载均衡,核心目标是实现一个 针对「单个服务实例 + 单个接口方法」的熔断器

  • 不是熔断整个服务,而是只熔断出问题的那一台服务器 + 那一个接口
  • 其他正常的服务器 / 接口完全不受影响
  • 只要请求报错(无论业务错误 / 网络错误),都统计熔断

首先来简单认识一个组件之前没有用过的:gocb ,这个是 Kitex 内置的 sony/gobreaker 熔断库,负责统计请求数、错误数、计算错误率、切换熔断状态。然后还有一个 circuitbreak.Control,是 Kitex 定义的熔断规则控制器,用来定义熔断的粒度、错误判断逻辑。

下面就继续看下例子,边看例子边学习。

1.2 thrift 文件

首先还是 thrift 文件,kitex 接口提供两个方法,SayHello 和 SayHi。

go 复制代码
namespace go hello

struct HelloRequest {
  1: string name
}

struct HelloResponse {
  1: string message
  2: string method
  3: string instance
  4: i32 weight
  5: i32 count
}

service HelloService {
  HelloResponse SayHello(1: HelloRequest req)
  HelloResponse SayHi(1: HelloRequest req)
}

1.3 自定义熔断器

首先是结构体 MethodInstanceCB。

go 复制代码
type MethodInstanceCB struct {
	// 负责统计请求数、错误数、计算错误率、切换熔断状态(半开/打开/关闭)
	panel   gocb.Panel
	// 定义熔断的粒度、错误判断逻辑, 告诉 kitex 怎么生成 key
	control circuitbreak.Control
}

然后是自定义实例熔断的方法 NewMethodInstanceCB,在这个方法会去创建 MethodInstanceCB,初始化上面两个字段。

go 复制代码
// NewMethodInstanceCB 创建一个自定义实例熔断器, errRate 和 minSample 的含义与普通熔断一致
// - errRate: 错误率达到多少时触发熔断
// - minSample: 至少累计多少次请求后才开始判断
func NewMethodInstanceCB(errRate float64, minSample int64) (*MethodInstanceCB, error) {
	// Panel 是底层真正维护熔断状态的对象
	panel, err := gocb.NewPanel(nil, gocb.Options{
		// 所有的 key 使用同一套熔断策略
		ShouldTripWithKey: func(key string) gocb.TripFunc {
			// 按错误率触发熔断, 收集 minSample 个请求后, 如果错误率 ≥ errRate,立刻触发熔断
			return gocb.RateTripFunc(errRate, minSample)
		},
	})
	if err != nil {
		return nil, err
	} 

	return &MethodInstanceCB{
		panel: panel,
		control: circuitbreak.Control{
			// GetKey 如何获取 key
			GetKey: func(ctx context.Context, request interface{}) (string, bool) {
				ri := rpcinfo.GetRPCInfo(ctx)
				if ri == nil {
					return "", false
				}
				// 默认实例熔断只会拿到实例地址, 这里额外把方法名拼进去, 也就是熔断粒度按照 "实例 + 方法"
				return fmt.Sprintf("%s/%s", ri.To().Address().String(), ri.To().Method()), true
			},
			// 所有非 nil 错误都算作失败, 便于观察熔断效果, 也就是方法只要返回 err 都算报错了
			GetErrorType: circuitbreak.FailIfError,
			DecorateError: func(ctx context.Context, request interface{}, err error) error {
				// 一旦命中熔断, 对外返回的熔断错误使用 kitex 的 ErrInstanceCircuitBreak, 我们可以自定义类型,
				// 客户端可以用 errors.Is(err, kerrors.ErrInstanceCircuitBreak) 判断是否触发熔断
				return kerrors.ErrInstanceCircuitBreak
			},
		},
	}, nil
}

在初始化的时候关注下面几个指标:

  • ShouldTripWithKey:熔断触发策略 ,按错误率触发熔断,收集 minSample 个请求后,如果错误率 ≥ errRate,立刻触发熔断。
  • GetKey:熔断粒度 ,最关键的一个,粒度是 实例地址/方法名,可以自己决定熔断的粒度。
  • GetErrorType:针对什么样的错误会开始判断熔断,FailIfError 表示 只要请求报错,就判定为熔断错误,默认是统计网络错误,这里得改一下,方便看熔断。
  • DecorateError:熔断触发的时候,包装成固定错误 ErrInstanceCircuitBreak 返回。

接下来是 Middleware 方法,这个方法 把熔断器转换成 Kitex 中间件,就是让你的自定义熔断器,能嵌入 RPC 调用流程中。

go 复制代码
func (m *MethodInstanceCB) Middleware() endpoint.Middleware {
	return circuitbreak.NewCircuitBreakerMW(m.control, m.panel)
}

最后是 Close 释放资源,关闭熔断器。

go 复制代码
func (m *MethodInstanceCB) Close() error {
	if m.panel != nil {
		m.panel.Close()
	}
	return nil
}

1.4 注册中心

注册中心部分前一篇文章已经学习过,代码都是一样的。

go 复制代码
package registry

import (
	"context"
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"runtime"
	"sort"
	"sync"

	"github.com/cloudwego/kitex/pkg/discovery"
	kitexregistry "github.com/cloudwego/kitex/pkg/registry"
	"github.com/cloudwego/kitex/pkg/rpcinfo"
)

const defaultRegistryFile = "data/registry.json"

type serviceRecord struct {
	// 地址
	Address string            `json:"address"`
	// tcp
	Network string            `json:"network"`
	// 权重
	Weight  int               `json:"weight"`
	// 存实例名称
	Tags    map[string]string `json:"tags,omitempty"`
}

type registryFile struct {
	Services map[string][]serviceRecord `json:"services"`
}

type FileRegistry struct {
	path string
	mu   sync.Mutex
}

func NewFileRegistry(path string) *FileRegistry {
	if path == "" {
		path = RegistryPath()
	}
	return &FileRegistry{path: path}
}

func (r *FileRegistry) Register(info *kitexregistry.Info) error {
	r.mu.Lock()
	defer r.mu.Unlock()

	data, err := r.load()
	if err != nil {
		return err
	}

	record := serviceRecord{
		Address: info.Addr.String(),
		Network: info.Addr.Network(),
		Weight:  normalizeWeight(info.Weight),
		Tags:    cloneMap(info.Tags),
	}

	records := data.Services[info.ServiceName]
	replaced := false
	for i := range records {
		if sameRecord(records[i], record) {
			records[i] = record
			replaced = true
			break
		}
	}
	if !replaced {
		records = append(records, record)
	}
	data.Services[info.ServiceName] = records
	return r.save(data)
}

func (r *FileRegistry) Deregister(info *kitexregistry.Info) error {
	r.mu.Lock()
	defer r.mu.Unlock()

	data, err := r.load()
	if err != nil {
		return err
	}

	records := data.Services[info.ServiceName]
	filtered := records[:0]
	for _, record := range records {
		if record.Address == info.Addr.String() && record.Network == info.Addr.Network() {
			continue
		}
		filtered = append(filtered, record)
	}
	if len(filtered) == 0 {
		delete(data.Services, info.ServiceName)
	} else {
		data.Services[info.ServiceName] = filtered
	}
	return r.save(data)
}

type FileResolver struct {
	path string
}

func NewFileResolver(path string) *FileResolver {
	if path == "" {
		path = RegistryPath()
	}
	return &FileResolver{path: path}
}

func (r *FileResolver) Target(_ context.Context, target rpcinfo.EndpointInfo) string {
	return target.ServiceName()
}

func (r *FileResolver) Resolve(_ context.Context, desc string) (discovery.Result, error) {
	content, err := os.ReadFile(r.path)
	if err != nil {
		if os.IsNotExist(err) {
			return discovery.Result{}, fmt.Errorf("service %q not found: registry file %s does not exist", desc, r.path)
		}
		return discovery.Result{}, err
	}

	var data registryFile
	if err := json.Unmarshal(content, &data); err != nil {
		return discovery.Result{}, err
	}

	// 根据服务名称读取出注册记录
	records := data.Services[desc]
	instances := make([]discovery.Instance, 0, len(records))
	for _, record := range records {
		// 获取所有实例
		instances = append(instances, discovery.NewInstance(
			record.Network,
			record.Address,
			normalizeWeight(record.Weight),
			record.Tags,
		))
	}
	if len(instances) == 0 {
		return discovery.Result{}, fmt.Errorf("service %q not found in registry", desc)
	}

	// 封装返回
	return discovery.Result{
		Cacheable: true,
		CacheKey:  desc,
		Instances: instances,
	}, nil
}

func (r *FileResolver) Diff(cacheKey string, prev, next discovery.Result) (discovery.Change, bool) {
	return discovery.DefaultDiff(cacheKey, prev, next)
}

func (r *FileResolver) Name() string {
	return "file-resolver"
}

func (r *FileRegistry) load() (*registryFile, error) {
	content, err := os.ReadFile(r.path)
	if err != nil {
		if os.IsNotExist(err) {
			return &registryFile{Services: map[string][]serviceRecord{}}, nil
		}
		return nil, err
	}

	var data registryFile
	if err := json.Unmarshal(content, &data); err != nil {
		return nil, err
	}
	if data.Services == nil {
		data.Services = map[string][]serviceRecord{}
	}
	return &data, nil
}

// 全量更新
func (r *FileRegistry) save(data *registryFile) error {
	if err := os.MkdirAll(filepath.Dir(r.path), 0o755); err != nil {
		return err
	}
	for svc := range data.Services {
		sort.Slice(data.Services[svc], func(i, j int) bool {
			if data.Services[svc][i].Address == data.Services[svc][j].Address {
				return data.Services[svc][i].Network < data.Services[svc][j].Network
			}
			return data.Services[svc][i].Address < data.Services[svc][j].Address
		})
	}

	content, err := json.MarshalIndent(data, "", "  ")
	if err != nil {
		return err
	}
	return os.WriteFile(r.path, content, 0o644)
}

func normalizeWeight(weight int) int {
	if weight <= 0 {
		return discovery.DefaultWeight
	}
	return weight
}

func cloneMap(in map[string]string) map[string]string {
	if len(in) == 0 {
		return nil
	}
	out := make(map[string]string, len(in))
	for k, v := range in {
		out[k] = v
	}
	return out
}

func sameRecord(a, b serviceRecord) bool {
	return a.Address == b.Address && a.Network == b.Network
}

func RegistryPath() string {
	_, file, _, ok := runtime.Caller(0)
	if !ok {
		return defaultRegistryFile
	}
	root := filepath.Dir(filepath.Dir(file))
	return filepath.Join(root, defaultRegistryFile)
}

1.5 Server 服务启动

这个就是服务启动的方法。

go 复制代码
package main

import (
	"log"
	"net"
	"os"
	"strconv"
	"time"

	helloservice "example.com/kitex-12/kitex_gen/hello/helloservice"
	fileregistry "example.com/kitex-12/registry"

	"github.com/cloudwego/kitex/pkg/discovery"
	"github.com/cloudwego/kitex/pkg/registry"
	"github.com/cloudwego/kitex/pkg/rpcinfo"
	"github.com/cloudwego/kitex/server"
)

func main() {
	addrText := getenv("KITEX_ADDR", "127.0.0.1:8899")
	instanceName := getenv("KITEX_INSTANCE", addrText)
	weight := mustAtoi(getenv("KITEX_WEIGHT", "10"))
	helloFailLimit := mustAtoi(getenv("KITEX_HELLO_FAIL_LIMIT", "4"))
	hiFailLimit := mustAtoi(getenv("KITEX_HI_FAIL_LIMIT", "4"))

	addr, err := net.ResolveTCPAddr("tcp", addrText)
	if err != nil {
		log.Printf("[%s] %v", now(), err)
		return
	}

	svr := helloservice.NewServer(
		&HelloServiceImpl{
			instance:       instanceName,
			weight:         weight,
			helloFailLimit: int32(helloFailLimit),
			hiFailLimit:    int32(hiFailLimit),
		},
		// 服务信息
		server.WithServerBasicInfo(&rpcinfo.EndpointBasicInfo{
			ServiceName: "HelloService",
		}),
		// 地址
		server.WithServiceAddr(addr),
		// 注册中心
		server.WithRegistry(fileregistry.NewFileRegistry(fileregistry.RegistryPath())),
		// 注册信息
		server.WithRegistryInfo(&registry.Info{
			// 服务名称
			ServiceName: "HelloService",
			// 权重
			Weight: weight,
			// 实例名称
			Tags: map[string]string{
				"instance": instanceName,
			},
		}),
	)

	log.Printf("[%s] server starting addr=%s instance=%s weight=%d hello_fail_limit=%d hi_fail_limit=%d registry=%s",
		now(), addrText, instanceName, weight, helloFailLimit, hiFailLimit, fileregistry.RegistryPath())
	if err := svr.Run(); err != nil {
		log.Printf("[%s] %v", now(), err)
	}
}

func getenv(key, fallback string) string {
	if value := os.Getenv(key); value != "" {
		return value
	}
	return fallback
}

func mustAtoi(s string) int {
	v, err := strconv.Atoi(s)
	if err != nil {
		return discovery.DefaultWeight
	}
	return v
}

func now() string {
	return time.Now().Format("15:04:05.000")
}

1.6 服务端处理函数

根据环境遍历传入的参数,固定前 limit 个返回 error。

go 复制代码
package main

import (
	"context"
	"fmt"
	"sync/atomic"

	echo "example.com/kitex-12/kitex_gen/hello"
)

type HelloServiceImpl struct {
	instance       string
	weight         int
	helloFailLimit int32
	hiFailLimit    int32
	helloCallCount atomic.Int32
	sayHiCallCount atomic.Int32
}

func (s *HelloServiceImpl) SayHello(ctx context.Context, req *echo.HelloRequest) (resp *echo.HelloResponse, err error) {
	count := s.helloCallCount.Add(1)
	if shouldFail(req.Name, count, s.helloFailLimit) {
		return nil, fmt.Errorf("mock hello failure: instance=%s method=SayHello call=%d name=%s", s.instance, count, req.Name)
	}
	return s.success("SayHello", req.Name, count), nil
}

func (s *HelloServiceImpl) SayHi(ctx context.Context, req *echo.HelloRequest) (resp *echo.HelloResponse, err error) {
	count := s.sayHiCallCount.Add(1)
	if shouldFail(req.Name, count, s.hiFailLimit) {
		return nil, fmt.Errorf("mock hi failure: instance=%s method=SayHi call=%d name=%s", s.instance, count, req.Name)
	}
	return s.success("SayHi", req.Name, count), nil
}

func (s *HelloServiceImpl) success(method, name string, count int32) *echo.HelloResponse {
	return &echo.HelloResponse{
		Message:  fmt.Sprintf("%s from %s to %s", method, s.instance, name),
		Method:   method,
		Instance: s.instance,
		Weight:   int32(s.weight),
		Count:    count,
	}
}

func shouldFail(name string, count, limit int32) bool {
	return count < limit
}

1.7 客户端启动

客户端启动通过 NewClient 方法注入注册中心和自定义的负载均衡配置。

java 复制代码
package main

import (
	"context"
	"errors"
	"fmt"
	"log"
	"time"

	"example.com/kitex-12/cb"
	echo "example.com/kitex-12/kitex_gen/hello"
	helloservice "example.com/kitex-12/kitex_gen/hello/helloservice"
	fileregistry "example.com/kitex-12/registry"

	"github.com/cloudwego/kitex/client"
	"github.com/cloudwego/kitex/pkg/kerrors"
	"github.com/cloudwego/kitex/pkg/loadbalance"
	"github.com/cloudwego/kitex/pkg/rpcinfo"
)

func main() {
	resolverPath := fileregistry.RegistryPath()
	// 创建自定义熔断器
	methodInstanceCB, err := cb.NewMethodInstanceCB(1.0, 3)
	if err != nil {
		log.Fatal(err)
	}
	defer methodInstanceCB.Close()

	cli, err := helloservice.NewClient(
		"HelloService",
		client.WithResolver(fileregistry.NewFileResolver(resolverPath)),
		// 权重负载均衡
		client.WithLoadBalancer(loadbalance.NewWeightedRoundRobinBalancer()),
		client.WithClientBasicInfo(&rpcinfo.EndpointBasicInfo{
			ServiceName: "client",
		}),
		// 初始化自定义熔断器
		client.WithInstanceMW(methodInstanceCB.Middleware()),
	)
	if err != nil {
		log.Fatal(err)
	}

	runMethod("SayHello", func(ctx context.Context, req *echo.HelloRequest) (*echo.HelloResponse, error) {
		return cli.SayHello(ctx, req)
	})
	fmt.Println()
	runMethod("SayHi", func(ctx context.Context, req *echo.HelloRequest) (*echo.HelloResponse, error) {
		return cli.SayHi(ctx, req)
	})
}

func runMethod(method string, call func(context.Context, *echo.HelloRequest) (*echo.HelloResponse, error)) {
	logf("=== start method=%s ===", method)
	for i := 1; i <= 15; i++ {
		name := method
		resp, err := call(context.Background(), &echo.HelloRequest{
			Name: fmt.Sprintf("%s-%02d", name, i),
		})
		switch {
		case err == nil:
			logf("%s %02d -> success instance=%s method=%s weight=%d count=%d message=%s",
				method, i, resp.Instance, resp.Method, resp.Weight, resp.Count, resp.Message)
		case errors.Is(err, kerrors.ErrInstanceCircuitBreak):
			logf("%s %02d -> blocked by instance circuit breaker", method, i)
		default:
			logf("%s %02d -> rpc error: %v", method, i, err)
		}

		time.Sleep(1000 * time.Millisecond)
	}
	logf("=== end method=%s ===", method)
}

func logf(format string, args ...interface{}) {
	fmt.Printf("[%s] %s\n", now(), fmt.Sprintf(format, args...))
}

func now() string {
	return time.Now().Format("15:04:05.000")
}

1.8 服务启动输出

首先启动两个服务端,一个是 8899 端口,一个是 8900 端口,8899 端口的权重是 10,8900 的权重是 5。

两个服务端的输出如下:

go 复制代码
2026/05/02 19:34:35 [19:34:35.814] server starting addr=127.0.0.1:8899 instance=127.0.0.1:8899 weight=10 hello_fail_limit=4 hi_fail_limit=4 registry=D:\go-code\go-learn-1\learn-6\kitex-12\data\registry.json
2026/05/02 19:34:35.833088 server.go:79: [Info] KITEX: server listen at addr=127.0.0.1:8899
go 复制代码
2026/05/02 19:34:37 [19:34:37.239] server starting addr=127.0.0.1:8900 instance=127.0.0.1:8900 weight=5 hello_fail_limit=4 hi_fail_limit=4 registry=D:\go-code\go-learn-1\learn-6\kitex-12\data\registry.json
2026/05/02 19:34:37.256226 server.go:79: [Info] KITEX: server listen at addr=127.0.0.1:8900

接下来启动客户端,关键是看客户端的输出。

go 复制代码
[19:34:38.659] === start method=SayHello ===
[19:34:38.679] SayHello 01 -> rpc error: remote or network error[remote]: biz error: mock hello failure: instance=127.0.0.1:8900 method=SayHello call=1 name=SayHello-01
[19:34:39.681] SayHello 02 -> rpc error: remote or network error[remote]: biz error: mock hello failure: instance=127.0.0.1:8899 method=SayHello call=1 name=SayHello-02
[19:34:40.684] SayHello 03 -> rpc error: remote or network error[remote]: biz error: mock hello failure: instance=127.0.0.1:8899 method=SayHello call=2 name=SayHello-03
[19:34:41.688] SayHello 04 -> rpc error: remote or network error[remote]: biz error: mock hello failure: instance=127.0.0.1:8900 method=SayHello call=2 name=SayHello-04
[19:34:42.691] SayHello 05 -> rpc error: remote or network error[remote]: biz error: mock hello failure: instance=127.0.0.1:8899 method=SayHello call=3 name=SayHello-05
2026/05/02 19:34:43.691770 middlewares.go:137: [Warn] KITEX: auto retry retryable error, to_service=HelloService, retry=1 error=forbidden by circuitbreaker: instance circuitbreak
[19:34:43.694] SayHello 06 -> rpc error: remote or network error[remote]: biz error: mock hello failure: instance=127.0.0.1:8900 method=SayHello call=3 name=SayHello-06
2026/05/02 19:34:44.695908 middlewares.go:137: [Warn] KITEX: auto retry retryable error, to_service=HelloService, retry=1 error=forbidden by circuitbreaker: instance circuitbreak
2026/05/02 19:34:44.695908 middlewares.go:137: [Warn] KITEX: auto retry retryable error, to_service=HelloService, retry=2 error=forbidden by circuitbreaker: instance circuitbreak
2026/05/02 19:34:44.695908 middlewares.go:137: [Warn] KITEX: auto retry retryable error, to_service=HelloService, retry=3 error=forbidden by circuitbreaker: instance circuitbreak
2026/05/02 19:34:44.695908 middlewares.go:137: [Warn] KITEX: auto retry retryable error, to_service=HelloService, retry=4 error=forbidden by circuitbreaker: instance circuitbreak
2026/05/02 19:34:44.695908 middlewares.go:137: [Warn] KITEX: auto retry retryable error, to_service=HelloService, retry=5 error=forbidden by circuitbreaker: instance circuitbreak
2026/05/02 19:34:44.695908 middlewares.go:137: [Warn] KITEX: auto retry retryable error, to_service=HelloService, retry=6 error=forbidden by circuitbreaker: instance circuitbreak
[19:34:44.695] SayHello 07 -> blocked by instance circuit breaker
2026/05/02 19:34:45.696183 middlewares.go:137: [Warn] KITEX: auto retry retryable error, to_service=HelloService, retry=1 error=forbidden by circuitbreaker: instance circuitbreak
2026/05/02 19:34:45.696183 middlewares.go:137: [Warn] KITEX: auto retry retryable error, to_service=HelloService, retry=2 error=forbidden by circuitbreaker: instance circuitbreak
2026/05/02 19:34:45.696183 middlewares.go:137: [Warn] KITEX: auto retry retryable error, to_service=HelloService, retry=3 error=forbidden by circuitbreaker: instance circuitbreak
2026/05/02 19:34:45.696183 middlewares.go:137: [Warn] KITEX: auto retry retryable error, to_service=HelloService, retry=4 error=forbidden by circuitbreaker: instance circuitbreak
2026/05/02 19:34:45.696183 middlewares.go:137: [Warn] KITEX: auto retry retryable error, to_service=HelloService, retry=5 error=forbidden by circuitbreaker: instance circuitbreak
2026/05/02 19:34:45.696686 middlewares.go:137: [Warn] KITEX: auto retry retryable error, to_service=HelloService, retry=6 error=forbidden by circuitbreaker: instance circuitbreak
[19:34:45.696] SayHello 08 -> blocked by instance circuit breaker
2026/05/02 19:34:46.696989 middlewares.go:137: [Warn] KITEX: auto retry retryable error, to_service=HelloService, retry=1 error=forbidden by circuitbreaker: instance circuitbreak
2026/05/02 19:34:46.696989 middlewares.go:137: [Warn] KITEX: auto retry retryable error, to_service=HelloService, retry=2 error=forbidden by circuitbreaker: instance circuitbreak
2026/05/02 19:34:46.697306 middlewares.go:137: [Warn] KITEX: auto retry retryable error, to_service=HelloService, retry=3 error=forbidden by circuitbreaker: instance circuitbreak
2026/05/02 19:34:46.697306 middlewares.go:137: [Warn] KITEX: auto retry retryable error, to_service=HelloService, retry=4 error=forbidden by circuitbreaker: instance circuitbreak
2026/05/02 19:34:46.697306 middlewares.go:137: [Warn] KITEX: auto retry retryable error, to_service=HelloService, retry=5 error=forbidden by circuitbreaker: instance circuitbreak
2026/05/02 19:34:46.697306 middlewares.go:137: [Warn] KITEX: auto retry retryable error, to_service=HelloService, retry=6 error=forbidden by circuitbreaker: instance circuitbreak
[19:34:46.697] SayHello 09 -> blocked by instance circuit breaker
[19:34:47.700] SayHello 10 -> success instance=127.0.0.1:8899 method=SayHello weight=10 count=4 message=SayHello from 127.0.0.1:8899 to SayHello-10
[19:34:48.701] SayHello 11 -> success instance=127.0.0.1:8899 method=SayHello weight=10 count=5 message=SayHello from 127.0.0.1:8899 to SayHello-11
[19:34:49.705] SayHello 12 -> success instance=127.0.0.1:8900 method=SayHello weight=5 count=4 message=SayHello from 127.0.0.1:8900 to SayHello-12
[19:34:50.706] SayHello 13 -> success instance=127.0.0.1:8899 method=SayHello weight=10 count=6 message=SayHello from 127.0.0.1:8899 to SayHello-13
[19:34:51.706] SayHello 14 -> success instance=127.0.0.1:8899 method=SayHello weight=10 count=7 message=SayHello from 127.0.0.1:8899 to SayHello-14
[19:34:52.707] SayHello 15 -> success instance=127.0.0.1:8900 method=SayHello weight=5 count=5 message=SayHello from 127.0.0.1:8900 to SayHello-15
[19:34:53.707] === end method=SayHello ===

[19:34:53.707] === start method=SayHi ===
[19:34:53.709] SayHi 01 -> rpc error: remote or network error[remote]: biz error: mock hi failure: instance=127.0.0.1:8899 method=SayHi call=1 name=SayHi-01
[19:34:54.712] SayHi 02 -> rpc error: remote or network error[remote]: biz error: mock hi failure: instance=127.0.0.1:8899 method=SayHi call=2 name=SayHi-02
[19:34:55.714] SayHi 03 -> rpc error: remote or network error[remote]: biz error: mock hi failure: instance=127.0.0.1:8900 method=SayHi call=1 name=SayHi-03
[19:34:56.717] SayHi 04 -> rpc error: remote or network error[remote]: biz error: mock hi failure: instance=127.0.0.1:8899 method=SayHi call=3 name=SayHi-04
2026/05/02 19:34:57.717831 middlewares.go:137: [Warn] KITEX: auto retry retryable error, to_service=HelloService, retry=1 error=forbidden by circuitbreaker: instance circuitbreak
[19:34:57.720] SayHi 05 -> rpc error: remote or network error[remote]: biz error: mock hi failure: instance=127.0.0.1:8900 method=SayHi call=2 name=SayHi-05
2026/05/02 19:34:58.721153 middlewares.go:137: [Warn] KITEX: auto retry retryable error, to_service=HelloService, retry=1 error=forbidden by circuitbreaker: instance circuitbreak
2026/05/02 19:34:58.721153 middlewares.go:137: [Warn] KITEX: auto retry retryable error, to_service=HelloService, retry=2 error=forbidden by circuitbreaker: instance circuitbreak
[19:34:58.721] SayHi 06 -> rpc error: remote or network error[remote]: biz error: mock hi failure: instance=127.0.0.1:8900 method=SayHi call=3 name=SayHi-06
2026/05/02 19:34:59.722966 middlewares.go:137: [Warn] KITEX: auto retry retryable error, to_service=HelloService, retry=1 error=forbidden by circuitbreaker: instance circuitbreak
2026/05/02 19:34:59.724073 middlewares.go:137: [Warn] KITEX: auto retry retryable error, to_service=HelloService, retry=2 error=forbidden by circuitbreaker: instance circuitbreak
2026/05/02 19:34:59.724073 middlewares.go:137: [Warn] KITEX: auto retry retryable error, to_service=HelloService, retry=3 error=forbidden by circuitbreaker: instance circuitbreak
2026/05/02 19:34:59.724073 middlewares.go:137: [Warn] KITEX: auto retry retryable error, to_service=HelloService, retry=4 error=forbidden by circuitbreaker: instance circuitbreak
2026/05/02 19:34:59.724073 middlewares.go:137: [Warn] KITEX: auto retry retryable error, to_service=HelloService, retry=5 error=forbidden by circuitbreaker: instance circuitbreak
2026/05/02 19:34:59.724073 middlewares.go:137: [Warn] KITEX: auto retry retryable error, to_service=HelloService, retry=6 error=forbidden by circuitbreaker: instance circuitbreak
[19:34:59.724] SayHi 07 -> blocked by instance circuit breaker
2026/05/02 19:35:00.725278 middlewares.go:137: [Warn] KITEX: auto retry retryable error, to_service=HelloService, retry=1 error=forbidden by circuitbreaker: instance circuitbreak
2026/05/02 19:35:00.725278 middlewares.go:137: [Warn] KITEX: auto retry retryable error, to_service=HelloService, retry=2 error=forbidden by circuitbreaker: instance circuitbreak
2026/05/02 19:35:00.725278 middlewares.go:137: [Warn] KITEX: auto retry retryable error, to_service=HelloService, retry=3 error=forbidden by circuitbreaker: instance circuitbreak
2026/05/02 19:35:00.725278 middlewares.go:137: [Warn] KITEX: auto retry retryable error, to_service=HelloService, retry=4 error=forbidden by circuitbreaker: instance circuitbreak
2026/05/02 19:35:00.725278 middlewares.go:137: [Warn] KITEX: auto retry retryable error, to_service=HelloService, retry=5 error=forbidden by circuitbreaker: instance circuitbreak
2026/05/02 19:35:00.725278 middlewares.go:137: [Warn] KITEX: auto retry retryable error, to_service=HelloService, retry=6 error=forbidden by circuitbreaker: instance circuitbreak
[19:35:00.725] SayHi 08 -> blocked by instance circuit breaker
[19:35:01.728] SayHi 09 -> success instance=127.0.0.1:8899 method=SayHi weight=10 count=4 message=SayHi from 127.0.0.1:8899 to SayHi-09
[19:35:02.729] SayHi 10 -> success instance=127.0.0.1:8899 method=SayHi weight=10 count=5 message=SayHi from 127.0.0.1:8899 to SayHi-10
[19:35:03.729] SayHi 11 -> success instance=127.0.0.1:8900 method=SayHi weight=5 count=4 message=SayHi from 127.0.0.1:8900 to SayHi-11
[19:35:04.730] SayHi 12 -> success instance=127.0.0.1:8899 method=SayHi weight=10 count=6 message=SayHi from 127.0.0.1:8899 to SayHi-12
[19:35:05.731] SayHi 13 -> success instance=127.0.0.1:8899 method=SayHi weight=10 count=7 message=SayHi from 127.0.0.1:8899 to SayHi-13
[19:35:06.731] SayHi 14 -> success instance=127.0.0.1:8900 method=SayHi weight=5 count=5 message=SayHi from 127.0.0.1:8900 to SayHi-14
[19:35:07.732] SayHi 15 -> success instance=127.0.0.1:8899 method=SayHi weight=10 count=8 message=SayHi from 127.0.0.1:8899 to SayHi-15
[19:35:08.733] === end method=SayHi ===

来解释下一些关键的输出。

  • SayHello 05 请求到 8899,此时 8899 的总失败数为 3,开始熔断,5s 之内不能处理请求。
  • SayHello 06 请求到 8899,因为处于熔断状态,所以进行 retry,retry 之后请求到 8900,此时 8900 的总失败数为 3,开始熔断,5s 之内不能处理请求。
  • SayHello 07 由于 retry 日志没有输出请求到哪个节点了,但是可以得知此时两个节点都是熔断状态的,所以此时 SayHello 07 请求失败,重试 6 次,最后输出日志 blocked by instance circuit breaker。
  • ...

上面是关键节点的日志解释,主要是解释下为什么 retry 前面会只重试 1 次,后面又是重试 6 次。且观察节点的日志输出能发现请求数基本 8899:8900 是 2:1,也满足负载均衡。

2. 自定义负载均衡算法

2.1 负载均衡接口

客户端使用负载均衡的时候使用的是 client.WithLoadBalancer,这个方法签名是。

go 复制代码
func WithLoadBalancer(lb loadbalance.Loadbalancer, opts ...*lbcache.Options) Option {}

可以看到主要的负载均衡器是 Loadbalancer,也就是说我们可以自己定义一个 Loadbalancer,然后使用我们自定义的负载均衡算法。

go 复制代码
// Picker picks an instance for next RPC call.
type Picker interface {
	Next(ctx context.Context, request interface{}) discovery.Instance
}

// Loadbalancer generates pickers for the given service discovery result.
type Loadbalancer interface {
	// 获取 Picker 接口, 调用 Next 去获取实例, discovery.Result 中存了所有的实例
	GetPicker(discovery.Result) Picker
	// 负载均衡算法名称
	Name() string // unique key
}

Picker 接口是核心,里面的 Next 负责从实例列表里面选出一台服务器,实例就在 discovery.Result 参数中。

go 复制代码
type Result struct {
	Cacheable bool
	CacheKey  string
	// 实例列表
	Instances []Instance
}

好了,下面我们来实现下这个接口。

2.2 实现接口

首先是 Picker,我们用一个结构体存储发现的所有实例,方便选择,当然这里就是简单演示,没有考虑实例新增减少。

go 复制代码
type parityPicker struct {
	instances []discovery.Instance
}

Next 方法选择实例,我们本地模拟启动两个实例,根据请求参数的最后的数字来决定要请求到哪个参数,如果是奇数就请求到下标 1,偶数就请求到下标 0。

  • 请求名最后一位是偶数,走第一个实例
  • 请求名最后一位是奇数,走第二个实例
go 复制代码
func (p *parityPicker) Next(ctx context.Context, request interface{}) discovery.Instance {
	if len(p.instances) == 0 {
		return nil
	}
	if len(p.instances) == 1 {
		return p.instances[0]
	}

	args, ok := request.(*echo.HelloServiceSayHelloArgs)
	if !ok || args == nil || args.Req == nil || args.Req.Name == "" {
		return p.instances[0]
	}

	name := args.Req.Name
	last := name[len(name)-1]
	if last >= '0' && last <= '9' {
		if (last-'0')%2 == 0 {
			return p.instances[0]
		}
		return p.instances[1]
	}

	if strings.Contains(strings.ToLower(name), "odd") {
		return p.instances[1]
	}
	return p.instances[0]
}

然后就是负载均衡器 ParityBalancer,实现 Name 和 GetPicker 方法,GetPicker 的时候将实例存到上面 instances 数组中。

go 复制代码
// - 请求名最后一位是偶数,选第一个实例
// - 请求名最后一位是奇数,选第二个实例
type ParityBalancer struct{}

func NewParityBalancer() loadbalance.Loadbalancer {
	return &ParityBalancer{}
}

func (b *ParityBalancer) Name() string {
	return "parity-balancer"
}

func (b *ParityBalancer) GetPicker(result discovery.Result) loadbalance.Picker {
	return &parityPicker{instances: result.Instances}
}

2.3 启动示例

上面就是负载均衡算法,客户端启动的时候就这么配置就行。

go 复制代码
cli, err := helloservice.NewClient(
	"HelloService",
	client.WithHostPorts("127.0.0.1:8899", "127.0.0.1:8900"),
	client.WithLoadBalancer(lb.NewParityBalancer()),
	client.WithClientBasicInfo(&rpcinfo.EndpointBasicInfo{
		ServiceName: "client",
	}),
)

服务端的启动代码就不贴出来了,跟上面的差不多,且服务端的 handler.go 收到请求的时将当前服务端信息返回给客户端就行了,方便看本次请求到了哪个实例。

go 复制代码
package main

import (
	"context"
	"fmt"

	echo "example.com/kitex-13/kitex_gen/hello"
)

type HelloServiceImpl struct {
	instance string
}

func (s *HelloServiceImpl) SayHello(ctx context.Context, req *echo.HelloRequest) (resp *echo.HelloResponse, err error) {
	return &echo.HelloResponse{
		Message:  fmt.Sprintf("hello, %s", req.Name),
		Instance: s.instance,
	}, nil
}

接下来启动来看下客户端的输出。

go 复制代码
[22:19:36.180] === custom load balancer demo ===
[22:19:36.198] request-01 -> instance=127.0.0.1:8900 message=hello, request-01
[22:19:36.528] request-02 -> instance=127.0.0.1:8899 message=hello, request-02
[22:19:36.828] request-03 -> instance=127.0.0.1:8900 message=hello, request-03
[22:19:37.129] request-04 -> instance=127.0.0.1:8899 message=hello, request-04
[22:19:37.431] request-05 -> instance=127.0.0.1:8900 message=hello, request-05
[22:19:37.732] request-06 -> instance=127.0.0.1:8899 message=hello, request-06
[22:19:38.034] request-07 -> instance=127.0.0.1:8900 message=hello, request-07
[22:19:38.334] request-08 -> instance=127.0.0.1:8899 message=hello, request-08
[22:19:38.636] request-09 -> instance=127.0.0.1:8900 message=hello, request-09
[22:19:38.936] request-10 -> instance=127.0.0.1:8899 message=hello, request-10

3. 超时

最后来看下超时相关的内容,下面学习三类超时:

  1. rpc timeout:这类超时属于服务端处理太慢了,已经超过客户端配置的 RPCTimeout 超时时间。
  2. connect timeout:看名字就知道,这类超时属于连接不上。
  3. business timeout:业务超时,这类就属于服务端处理太慢,但是没有超过 RPCTimeout,只是说客户端自己在业务层又设置了超时时间,这类就是客户端自己控制的超时,而不是框架的超时。

下面来看例子,为了方便演示,thrift 文件中定义的接口要添加一个时间,服务端收到请求之后先 sleep 这么长的时间再处理请求返回。

go 复制代码
namespace go hello

struct HelloRequest {
  1: string name
  2: i32 sleep_ms
}

struct HelloResponse {
  1: string message
  2: i32 actual_sleep_ms
}

service HelloService {
  HelloResponse SayHello(1: HelloRequest req)
}

然后下面是服务端的代码。

go 复制代码
package main

import (
	"context"
	"fmt"
	"time"

	echo "example.com/kitex-14/kitex_gen/hello"
)

type HelloServiceImpl struct{}

func (s *HelloServiceImpl) SayHello(ctx context.Context, req *echo.HelloRequest) (resp *echo.HelloResponse, err error) {
	sleep := time.Duration(req.SleepMs) * time.Millisecond
	select {
	case <-time.After(sleep):
		return &echo.HelloResponse{
			Message:       fmt.Sprintf("hello, %s", req.Name),
			ActualSleepMs: req.SleepMs,
		}, nil
	case <-ctx.Done():
		return nil, ctx.Err()
	}
}

接下来是客户端的代码,主要就是模拟这三种错误。

go 复制代码
package main

import (
	"context"
	"errors"
	"fmt"
	"log"
	"time"

	echo "example.com/kitex-14/kitex_gen/hello"
	helloservice "example.com/kitex-14/kitex_gen/hello/helloservice"

	"github.com/cloudwego/kitex/client"
	"github.com/cloudwego/kitex/pkg/kerrors"
	"github.com/cloudwego/kitex/pkg/rpcinfo"
)

func main() {
	runRPCTimeoutDemo()
	fmt.Println()
	runBusinessTimeoutDemo()
	fmt.Println()
	runConnectTimeoutDemo()
}

// 调用 rpc 超时
func runRPCTimeoutDemo() {
	logf("=== rpc timeout demo ===")
	cli, err := helloservice.NewClient(
		"HelloService",
		client.WithHostPorts("127.0.0.1:8899"),
		client.WithClientBasicInfo(&rpcinfo.EndpointBasicInfo{ServiceName: "client"}),
		// rpc 超时时间 800ms
		client.WithRPCTimeout(800*time.Millisecond),
	)
	if err != nil {
		log.Fatal(err)
	}

	// 服务端处理 1.5s, 超过 rpc 超时时间
	call(cli, "rpc-timeout", 1500)
	call(cli, "rpc-ok", 200)
}

// 业务超时
func runBusinessTimeoutDemo() {
	logf("=== business context timeout demo ===")
	cli, err := helloservice.NewClient(
		"HelloService",
		client.WithHostPorts("127.0.0.1:8899"),
		client.WithClientBasicInfo(&rpcinfo.EndpointBasicInfo{ServiceName: "client"}),
		// rpc 超时时间 3s
		client.WithRPCTimeout(3*time.Second),
	)
	if err != nil {
		log.Fatal(err)
	}

	// 业务超时时间 600ms
	ctx, cancel := context.WithTimeout(context.Background(), 600*time.Millisecond)
	defer cancel()
	// rpc 服务处理时间 1.5s
	callWithContext(ctx, cli, "business-timeout", 1500)

	call(cli, "business-ok", 200)
}

// 连接超时
func runConnectTimeoutDemo() {
	logf("=== connect timeout demo ===")
	cli, err := helloservice.NewClient(
		"HelloService",
		// 随便连一个地址
		client.WithHostPorts("10.255.255.1:8899"),
		client.WithClientBasicInfo(&rpcinfo.EndpointBasicInfo{ServiceName: "client"}),
		// 700ms 超时时间
		client.WithConnectTimeout(700*time.Millisecond),
		client.WithRPCTimeout(3*time.Second),
	)
	if err != nil {
		log.Fatal(err)
	}

	call(cli, "connect-timeout", 100)
}

func call(cli helloservice.Client, name string, sleepMs int32) {
	callWithContext(context.Background(), cli, name, sleepMs)
}

func callWithContext(ctx context.Context, cli helloservice.Client, name string, sleepMs int32) {
	start := time.Now()
	resp, err := cli.SayHello(ctx, &echo.HelloRequest{
		Name:    name,
		SleepMs: sleepMs,
	})
	cost := time.Since(start)

	switch {
	case err == nil:
		logf("%s -> success sleep_ms=%d actual_sleep_ms=%d cost=%s message=%s", name, sleepMs, resp.ActualSleepMs, cost.Round(time.Millisecond), resp.Message)
	case kerrors.IsTimeoutError(err):
		// rpc 超时和 connect 连接超时都是走的这里
		logf("%s -> timeout cost=%s err=%v", name, cost.Round(time.Millisecond), err)
	case errors.Is(err, context.DeadlineExceeded):
		logf("%s -> context deadline exceeded cost=%s err=%v", name, cost.Round(time.Millisecond), err)
	default:
		logf("%s -> error cost=%s err=%v", name, cost.Round(time.Millisecond), err)
	}
}

func logf(format string, args ...interface{}) {
	fmt.Printf("[%s] %s\n", now(), fmt.Sprintf(format, args...))
}

func now() string {
	return time.Now().Format("15:04:05.000")
}

然后来看下输出。

go 复制代码
[23:52:41.045] === rpc timeout demo ===
[23:52:41.861] rpc-timeout -> timeout cost=800ms err=rpc timeout: timeout=800ms, to=HelloService, method=SayHello, location=kitex.rpcTimeoutMW, remote=127.0.0.1:8899
2026/05/02 23:52:41.868041 rpctimeout.go:108: [Error] KITEX: to_service=HelloService method=SayHello addr=127.0.0.1:8899 error=remote or network error: default codec read failed: read tcp 127.0.0.1:24302->127.0.0.1:8899: i/o timeout
[23:52:42.064] rpc-ok -> success sleep_ms=200 actual_sleep_ms=200 cost=202ms message=hello, rpc-ok

[23:52:42.064] === business context timeout demo ===
[23:52:42.664] business-timeout -> timeout cost=601ms err=rpc timeout: timeout=3s, to=HelloService, method=SayHello, location=kitex.rpcTimeoutMW, remote=127.0.0.1:8899, timeout by business, actual=600ms
[23:52:42.867] business-ok -> success sleep_ms=200 actual_sleep_ms=200 cost=202ms message=hello, business-ok

[23:52:42.867] === connect timeout demo ===
2026/05/02 23:52:43.569268 middlewares.go:137: [Warn] KITEX: auto retry retryable error, to_service=HelloService, retry=1 error=remote or network error: get connection error: dial tcp 10.255.255.1:8899: i/o timeout
2026/05/02 23:52:44.269831 middlewares.go:137: [Warn] KITEX: auto retry retryable error, to_service=HelloService, retry=2 error=remote or network error: get connection error: dial tcp 10.255.255.1:8899: i/o timeout
2026/05/02 23:52:44.970518 middlewares.go:137: [Warn] KITEX: auto retry retryable error, to_service=HelloService, retry=3 error=remote or network error: get connection error: dial tcp 10.255.255.1:8899: i/o timeout
2026/05/02 23:52:45.671524 middlewares.go:137: [Warn] KITEX: auto retry retryable error, to_service=HelloService, retry=4 error=remote or network error: get connection error: dial tcp 10.255.255.1:8899: i/o timeout
[23:52:45.867] connect-timeout -> timeout cost=3s err=rpc timeout: timeout=3s, to=HelloService, method=SayHello, location=kitex.rpcTimeoutMW, remote=10.255.255.1:8899

这里第三个报错重试 4 次是因为每一次请求的 connect 超时时间是 700ms,经过 4 次超时,在第五次就超过 rpc 超时时间 3s 了,所以最后不重试,直接截断。

4. 小结

好了,负载均衡的内容就到这里了,下一篇学习 dubbo 相关的内容。

如有错误,欢迎指出!!!

相关推荐
AOI小白新手上路1 小时前
VDMamba·AutoDL 复现笔记 · 学习笔记
笔记·学习
~kiss~1 小时前
LangGraph ~ 为图添加跨线程持久化
学习
谢亮_vipxieliang2 小时前
Go net/http 标准库深入——构建生产级 HTTP 服务
http·golang·iphone
xianrenli382 小时前
水肺潜水学习
学习
小小龙学IT3 小时前
Go 语言 gRPC(grpc-go)深度解析
rpc·架构·golang·go
凌云若寒4 小时前
BarTender提示#807错误:无法在拥有其他许可证的Licensing Service上激活节点锁定的 Professional 版许可证 的解决办法
运维·服务器·前端·学习·软件需求
爱吃苹果的日记本4 小时前
离散数学第七课
学习·离散数学
迪丽热爱4 小时前
多媒体应用31-914(计算机硬件与体系结构)
学习
dadaobusi5 小时前
学习:BPU
学习