11、分布式服务的复制测试、端口复用及客户端负载均衡

分布式服务的复制测试、端口复用及客户端负载均衡

1. 复制测试

在分布式系统中,需要对Raft的复制功能进行测试。通过向领导者服务器追加一些记录,然后检查Raft是否将这些记录复制到了其追随者。由于Raft追随者在短延迟后才会应用追加消息,因此使用 testify Eventually() 方法,给Raft足够的时间完成复制。

require.Eventually(t, func() bool {
    for j := 0; j < nodeCount; j++ {
        got, err := logs[j].Read(off)
        if err != nil {
            return false
        }
        record.Offset = off
        if !reflect.DeepEqual(got.Value, record.Value) {
            return false
        }
    }
    return true
}, 500*time.Millisecond, 50*time.Millisecond)

完成测试的代码如下:

err := logs[0].Leave("1")
require.NoError(t, err)
time.Sleep(50 * time.Millisecond)
off, err := logs[0].Append(&api.Record{
    Value: []byte("third"),
})
require.NoError(t, err)
time.Sleep(50 * time.Millisecond)
record, err := logs[1].Read(off)
require.IsType(t, api.ErrOffsetOutOfRange{}, err)
require.Nil(t, record)
record, err = logs[2].Read(off)
require.NoError(t, err)
require.Equal(t, []byte("third"), record.Value)
require.Equal(t, off, record.Offset)

这段代码检查领导者是否停止向已离开集群的服务器复制记录,同时继续向现有服务器复制。

2. 端口复用

复用允许在同一端口上提供不同的服务,这样可以减少文档、配置和管理的连接数量,即使防火墙限制只能使用一个端口,也能提供多个服务。不过,每个新连接会有轻微的性能损失,因为复用器需要读取前几个字节来识别连接,但对于长连接来说,这种性能损失可以忽略不计。

许多使用Raft的分布式服务会将Raft与其他服务(如RPC服务)进行复用。使用相互TLS运行gRPC时,复用会变得棘手,因为需要在TLS握手后进行连接复用。在握手之前,无法区分连接,只能知道它们都是TLS连接,需要握手并查看解密后的数据包才能了解更多信息。握手后,可以读取连接的数据包来确定连接是gRPC还是Raft连接。

为了区分Raft和gRPC连接,让Raft连接写入一个字节来标识。将数字1作为Raft连接的第一个字节,以将其与gRPC连接分开。如果有其他服务,可以通过向gRPC客户端传递自定义拨号器来发送数字2作为第一个字节来区分它们。

更新代理以复用其Raft和gRPC连接并创建分布式日志的步骤如下:
1. 更新导入

import (
    "bytes"
    "crypto/tls"
    "fmt"
    "io"
    "net"
    "sync"
    "time"
    "go.uber.org/zap"
    "github.com/hashicorp/raft"
    "github.com/soheilhy/cmux"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials"
    "github.com/travisjeffery/proglog/internal/auth"
    "github.com/travisjeffery/proglog/internal/discovery"
    "github.com/travisjeffery/proglog/internal/log"
    "github.com/travisjeffery/proglog/internal/server"
)
  1. 更新 Agent 类型
type Agent struct {
    Config Config
    mux    cmux.CMux
    log    *log.DistributedLog
    server *grpc.Server
    membership *discovery.Membership
    shutdown bool
    shutdowns chan struct{}
    shutdownLock sync.Mutex
}
  1. 添加 Bootstrap 字段到 Config 结构体
Bootstrap bool
  1. 设置复用器
setup := []func() error {
    a.setupLogger,
    a.setupMux,
    a.setupLog,
    a.setupServer,
    a.setupMembership,
}
func (a *Agent) setupMux() error {
    rpcAddr := fmt.Sprintf(
        ":%d",
        a.Config.RPCPort,
    )
    ln, err := net.Listen("tcp", rpcAddr)
    if err != nil {
        return err
    }
    a.mux = cmux.New(ln)
    return nil
}
  1. 更新 setupLog() 方法
func (a *Agent) setupLog() error {
    raftLn := a.mux.Match(func(reader io.Reader) bool {
        b := make([]byte, 1)
        if _, err := reader.Read(b); err != nil {
            return false
        }
        return bytes.Compare(b, []byte{byte(log.RaftRPC)}) == 0
    })
    logConfig := log.Config{}
    logConfig.Raft.StreamLayer = log.NewStreamLayer(
        raftLn,
        a.Config.ServerTLSConfig,
        a.Config.PeerTLSConfig,
    )
    logConfig.Raft.LocalID = raft.ServerID(a.Config.NodeName)
    logConfig.Raft.Bootstrap = a.Config.Bootstrap
    var err error
    a.log, err = log.NewDistributedLog(
        a.Config.DataDir,
        logConfig,
    )
    if err != nil {
        return err
    }
    if a.Config.Bootstrap {
        err = a.log.WaitForLeader(3 * time.Second)
    }
    return err
}
  1. 更新 setupServer() 方法
func (a *Agent) setupServer() error {
    authorizer := auth.New(
        a.Config.ACLModelFile,
        a.Config.ACLPolicyFile,
    )
    serverConfig := &server.Config{
        CommitLog:  a.log,
        Authorizer: authorizer,
    }
    var opts []grpc.ServerOption
    if a.Config.ServerTLSConfig != nil {
        creds := credentials.NewTLS(a.Config.ServerTLSConfig)
        opts = append(opts, grpc.Creds(creds))
    }
    var err error
    a.server, err = server.NewGRPCServer(serverConfig, opts...)
    if err != nil {
        return err
    }
    grpcLn := a.mux.Match(cmux.Any())
    go func() {
        if err := a.server.Serve(grpcLn); err != nil {
            _ = a.Shutdown()
        }
    }()
    return err
}
  1. 更新 setupMembership() 方法
func (a *Agent) setupMembership() error {
    rpcAddr, err := a.Config.RPCAddr()
    if err != nil {
        return err
    }
    a.membership, err = discovery.New(a.log, discovery.Config{
        NodeName: a.Config.NodeName,
        BindAddr: a.Config.BindAddr,
        Tags: map[string]string{
            "rpc_addr": rpcAddr,
        },
        StartJoinAddrs: a.Config.StartJoinAddrs,
    })
    return err
}
  1. 删除不必要的代码 :删除 a.replicator.Close 行和 internal/log/replicator.go 文件。
  2. 启动复用器
go a.serve()
func (a *Agent) serve() error {
    if err := a.mux.Serve(); err != nil {
        _ = a.Shutdown()
        return err
    }
    return nil
}
3. 客户端负载均衡
3.1 三种负载均衡策略
  • 服务器代理 :客户端将请求发送到负载均衡器,负载均衡器知道服务器(通过查询服务注册表或本身就是服务注册表)并将请求代理到后端服务。
  • 外部负载均衡 :客户端查询外部负载均衡服务,该服务知道服务器并告诉客户端将RPC发送到哪个服务器。
  • 客户端侧负载均衡 :客户端查询服务注册表以了解服务器,选择要发送RPC的服务器,并直接将RPC发送到该服务器。
负载均衡策略 优点 缺点
服务器代理 可作为信任边界,控制请求摄入,常用于外部流量负载均衡 增加了中间环节,可能会增加延迟
外部负载均衡 能进行复杂和准确的负载均衡 运营负担大
客户端侧负载均衡 减少延迟,提高效率,具有弹性 需要处理网络和安全问题,让客户端直接访问服务器
3.2 gRPC中的客户端负载均衡

gRPC将服务器发现、负载均衡以及客户端请求和响应处理分开。在gRPC中,解析器负责发现服务器,选择器负责负载均衡,即选择哪个服务器处理当前请求。gRPC还具有管理子连接的平衡器,但将负载均衡工作交给选择器。

gRPC默认使用轮询负载均衡算法,该算法按顺序将请求依次发送到各个服务器,适用于每个请求需要服务器进行相同工作量的情况。但轮询负载均衡不考虑请求、客户端和服务器的具体情况,例如:
- 对于具有单个写入器和多个读取器的复制分布式服务,希望从副本读取数据,让写入器专注于写入,这需要知道请求是读还是写,以及服务器是主服务器还是副本。
- 对于全球分布式服务,希望客户端优先与本地服务器进行网络通信,这需要知道客户端和服务器的位置。
- 对于对延迟敏感的系统,可以跟踪服务器的飞行中或排队请求数量等延迟指标,并让客户端请求具有最小数量的服务器。

为了解决这些问题,可以编写自己的解析器和选择器。解析器负责发现服务器并确定哪个服务器是领导者,选择器负责将生产调用定向到领导者,并在追随者之间平衡消费调用。

4. 让服务器可发现

解析器需要一种方法来发现集群中的服务器,包括每个服务器的地址以及是否为领导者。可以通过gRPC服务的端点将这些信息暴露给解析器。

  1. 更新 api/v1/log.proto 文件
service Log {
    rpc Produce(ProduceRequest) returns (ProduceResponse) {}
    rpc Consume(ConsumeRequest) returns (ConsumeResponse) {}
    rpc ConsumeStream(ConsumeRequest) returns (stream ConsumeResponse) {}
    rpc ProduceStream(stream ProduceRequest) returns (stream ProduceResponse) {}
    rpc GetServers(GetServersRequest) returns (GetServersResponse) {}
}

message GetServersRequest {}
message GetServersResponse {
    repeated Server servers = 1;
}
message Server {
    string id = 1;
    string rpc_addr = 2;
    bool is_leader = 3;
}
  1. internal/log/distributed.go 文件中添加 GetServers() 方法
func (l *DistributedLog) GetServers() ([]*api.Server, error) {
    future := l.raft.GetConfiguration()
    if err := future.Error(); err != nil {
        return nil, err
    }
    var servers []*api.Server
    for _, server := range future.Configuration().Servers {
        servers = append(servers, &api.Server{
            Id:       string(server.ID),
            RpcAddr:  string(server.Address),
            IsLeader: l.raft.Leader() == server.Address,
        })
    }
    return servers, nil
}
  1. 更新 DistributedLog 测试
servers, err := logs[0].GetServers()
require.NoError(t, err)
require.Equal(t, 3, len(servers))
require.True(t, servers[0].IsLeader)
require.False(t, servers[1].IsLeader)
require.False(t, servers[2].IsLeader)

err = logs[0].Leave("1")
require.NoError(t, err)
time.Sleep(50 * time.Millisecond)

servers, err = logs[0].GetServers()
require.NoError(t, err)
require.Equal(t, 2, len(servers))
require.True(t, servers[0].IsLeader)
require.False(t, servers[1].IsLeader)
  1. 更新 internal/server/server.go 文件
type Config struct {
    CommitLog    CommitLog
    Authorizer   Authorizer
    GetServerer  GetServerer
}

func (s *grpcServer) GetServers(
    ctx context.Context, req *api.GetServersRequest,
) (
    *api.GetServersResponse, error) {
    servers, err := s.GetServerer.GetServers()
    if err != nil {
        return nil, err
    }
    return &api.GetServersResponse{Servers: servers}, nil
}

type GetServerer interface {
    GetServers() ([]*api.Server, error)
}

通过以上步骤,可以实现分布式服务的复制测试、端口复用以及客户端负载均衡,提高服务的可用性、可扩展性和用户体验。

分布式服务的复制测试、端口复用及客户端负载均衡

5. 实现解析器和选择器

为了让客户端能够自动发现服务器、将追加调用定向到领导者并在追随者之间平衡消费调用,我们需要实现自定义的解析器和选择器。

5.1 解析器的实现思路

解析器的主要任务是发现集群中的服务器,并确定哪个是领导者。可以利用前面提到的 GetServers 端点来获取服务器信息。以下是一个简单的解析器实现示例:

package resolver

import (
    "context"
    "google.golang.org/grpc/resolver"
    "yourproject/api/v1"
    "yourproject/internal/client"
)

type CustomResolver struct {
    target resolver.Target
    cc     resolver.ClientConn
    client api.LogClient
}

func NewCustomResolver(target resolver.Target, cc resolver.ClientConn) resolver.Resolver {
    conn, err := client.Dial(target.Endpoint())
    if err != nil {
        // 处理错误
    }
    r := &CustomResolver{
        target: target,
        cc:     cc,
        client: api.NewLogClient(conn),
    }
    r.ResolveNow(resolver.ResolveNowOptions{})
    return r
}

func (r *CustomResolver) ResolveNow(options resolver.ResolveNowOptions) {
    ctx := context.Background()
    resp, err := r.client.GetServers(ctx, &api.GetServersRequest{})
    if err != nil {
        // 处理错误
    }
    var addresses []resolver.Address
    for _, server := range resp.Servers {
        addresses = append(addresses, resolver.Address{
            Addr:       server.RpcAddr,
            ServerName: server.Id,
            Attributes: map[string]interface{}{
                "is_leader": server.IsLeader,
            },
        })
    }
    r.cc.UpdateState(resolver.State{
        Addresses: addresses,
    })
}

func (r *CustomResolver) Close() {
    // 关闭连接等操作
}

func init() {
    resolver.Register(&resolver.Builder{
        Scheme: "custom",
        Build: func(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) {
            return NewCustomResolver(target, cc), nil
        },
    })
}

这个解析器会定期调用 GetServers 端点,获取服务器信息,并更新gRPC的连接状态。

5.2 选择器的实现思路

选择器负责根据解析器提供的服务器信息,将请求定向到合适的服务器。对于追加调用,应该定向到领导者;对于消费调用,应该在追随者之间进行负载均衡。以下是一个简单的选择器实现示例:

package picker

import (
    "google.golang.org/grpc/balancer"
    "google.golang.org/grpc/balancer/base"
    "google.golang.org/grpc/resolver"
)

type CustomPicker struct {
    leaderAddr resolver.Address
    followerAddrs []resolver.Address
    index int
}

func NewCustomPicker(buildInfo base.PickerBuildInfo) balancer.Picker {
    var leaderAddr resolver.Address
    var followerAddrs []resolver.Address
    for _, subConnInfo := range buildInfo.ReadySCs {
        isLeader, ok := subConnInfo.Address.Attributes.Value("is_leader").(bool)
        if ok && isLeader {
            leaderAddr = subConnInfo.Address
        } else {
            followerAddrs = append(followerAddrs, subConnInfo.Address)
        }
    }
    return &CustomPicker{
        leaderAddr: leaderAddr,
        followerAddrs: followerAddrs,
        index: 0,
    }
}

func (p *CustomPicker) Pick(info balancer.PickInfo) (balancer.PickResult, error) {
    if info.FullMethodName == "/api.Log/Produce" {
        if p.leaderAddr.Addr != "" {
            return balancer.PickResult{
                SubConn: info.SubConnMap[p.leaderAddr],
            }, nil
        }
        return balancer.PickResult{}, balancer.ErrNoSubConnAvailable
    }
    if len(p.followerAddrs) == 0 {
        return balancer.PickResult{}, balancer.ErrNoSubConnAvailable
    }
    addr := p.followerAddrs[p.index]
    p.index = (p.index + 1) % len(p.followerAddrs)
    return balancer.PickResult{
        SubConn: info.SubConnMap[addr],
    }, nil
}

func init() {
    balancer.Register(base.NewBalancerBuilderV2("custom", NewCustomPicker, base.Config{}))
}

这个选择器会根据请求的方法名( Produce Consume )来选择合适的服务器。

6. 测试与验证

在实现了解析器和选择器之后,需要进行测试和验证,确保客户端能够正确地发现服务器、定向请求和进行负载均衡。

6.1 测试环境搭建

可以使用测试框架(如 go test )来搭建测试环境。启动多个服务器实例,模拟集群环境,并使用客户端进行请求。

6.2 测试用例编写

以下是一些测试用例的示例:
- 追加记录到领导者

func TestAppendToLeader(t *testing.T) {
    ctx := context.Background()
    conn, err := grpc.Dial("custom:///your-target", grpc.WithResolvers(&resolver.CustomResolverBuilder{}), grpc.WithBalancerName("custom"))
    if err != nil {
        t.Fatalf("Failed to dial: %v", err)
    }
    defer conn.Close()
    client := api.NewLogClient(conn)
    resp, err := client.Produce(ctx, &api.ProduceRequest{
        Record: &api.Record{
            Value: []byte("test record"),
        },
    })
    if err != nil {
        t.Fatalf("Failed to produce: %v", err)
    }
    // 验证响应
}
  • 从追随者消费记录
func TestConsumeFromFollowers(t *testing.T) {
    ctx := context.Background()
    conn, err := grpc.Dial("custom:///your-target", grpc.WithResolvers(&resolver.CustomResolverBuilder{}), grpc.WithBalancerName("custom"))
    if err != nil {
        t.Fatalf("Failed to dial: %v", err)
    }
    defer conn.Close()
    client := api.NewLogClient(conn)
    resp, err := client.Consume(ctx, &api.ConsumeRequest{
        Offset: 0,
    })
    if err != nil {
        t.Fatalf("Failed to consume: %v", err)
    }
    // 验证响应
}
7. 总结

通过实现分布式服务的复制测试、端口复用、客户端负载均衡以及服务器发现功能,我们可以提高服务的可用性、可扩展性和用户体验。具体步骤如下:
1. 进行复制测试,确保Raft能够正确地将记录复制到追随者。
2. 实现端口复用,让Raft和gRPC服务可以在同一端口上运行。
3. 选择合适的负载均衡策略,如客户端侧负载均衡,并在gRPC中实现自定义的解析器和选择器。
4. 让服务器可发现,通过gRPC端点暴露服务器信息。
5. 编写测试用例,验证系统的正确性。

以下是整个过程的流程图:

graph TD
    A[复制测试] --> B[端口复用]
    B --> C[选择负载均衡策略]
    C --> D[实现解析器和选择器]
    D --> E[让服务器可发现]
    E --> F[测试与验证]

通过以上步骤的实施,可以构建一个高效、稳定的分布式服务系统。

提供了基于BP(Back Propagation)神经网络结合PID(比例-积分-微分)控制策略的Simulink仿真模型。该模型旨在实现对杨艺所著论文《基于S函数的BP神经网络PID控制器及Simulink仿真》中的理论进行实践验证。在Matlab 2016b环境下开发,经过测试,确保能够正常运行,适合学习和研究神经网络在控制系统中的应用。 特点 集成BP神经网络:模型中集成了BP神经网络用于提升PID控制器的性能,使之能更好地适应复杂控制环境。 PID控制优化:利用神经网络的自学习能力,对传统的PID控制算法进行了智能调整,提高控制精度和稳定性。 S函数应用:展示了如何在Simulink中通过S函数嵌入MATLAB代码,实现BP神经网络的定制化逻辑。 兼容性说明:虽然开发于Matlab 2016b,但理论上兼容后续版本,可能会需要调整少量配置以适配不同版本的Matlab。 使用指南 环境要求:确保你的电脑上安装有Matlab 2016b或更高版本。 模型加载: 下载本仓库到本地。 在Matlab中打开.slx文件。 运行仿真: 调整模型参数前,请先熟悉各模块功能和输入输出设置。 运行整个模型,观察控制效果。 参数调整: 用户可以自由调节神经网络的层数、节点数以及PID控制器的参数,探索不同的控制性能。 学习和修改: 通过阅读模型中的注释和查阅相关文献,加深对BP神经网络与PID控制结合的理解。 如需修改S函数内的MATLAB代码,建议有一定的MATLAB编程基础。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值