文章目录
- [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. 小结)
本系列文章:
- Java 转 go 学习 - 项目管理
- Java 转 go 学习 - 基本语法
- Java 转 go 学习 - 类型转换
- Java 转 go 学习 - 流程控制结构
- Java 转 go 学习 - 数组和切片
- Java 转 go 学习 - map
- Java 转 go 学习 - 函数(1)
- Java 转 go 学习 - 函数(2)
- Java 转 go 学习 - 结构体
- Java 转 go 学习 - 接口
- Java 转 go 学习 - 并发编程(1)
- Java 转 go 学习 - 并发编程(2)
- Java 转 go 学习 - 并发编程(3)
- Java 转 go 学习 - web 编程
- Java 转 go 学习 - Hertz 学习(1)
- Java 转 go 学习 - Hertz 学习(2)
- Java 转 go 学习 - Redis(1)
- Java 转 go 学习 - Redis(2)
- Java 转 go 学习 - MYSQL
- Java 转 go 学习 - GRPC(1)
- Java 转 go 学习 - GRPC(2)
- Java 转 go 学习 - GRPC(3)
- Java 转 go 学习 - GRPC(4)
- Java 转 go 学习 - kitex(1)
- Java 转 go 学习 - kitex(2)
- Java 转 go 学习 - kitex(3)
- Java 转 go 学习 - kitex(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 ®istryFile{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(®istry.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. 超时
最后来看下超时相关的内容,下面学习三类超时:
- rpc timeout:这类超时属于服务端处理太慢了,已经超过客户端配置的 RPCTimeout 超时时间。
- connect timeout:看名字就知道,这类超时属于连接不上。
- 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 相关的内容。
如有错误,欢迎指出!!!