云计算百科
云计算领域专业知识百科平台

Go 语言入门笔记(十七):微服务常见实践——从单体到分布式

前面把 Go 的语言特性和工程实践都过了一遍,基础算是扎实了。但真实的工作场景里,很多项目都是微服务架构,尤其是后端岗位,面试时几乎躲不开“你们项目怎么拆服务的”“服务之间怎么调用”“怎么保证稳定性”这类问题。这篇就结合我自己的理解,把 Go 在微服务中的常见实践梳理一下,包括 RPC、服务注册发现、链路追踪、限流熔断等。

为什么 Go 适合写微服务

Go 在微服务领域特别受欢迎,主要有几个原因:

  • 部署简单:编译出来就是一个静态二进制,不需要装运行时,扔到容器里就能跑。
  • 启动快:冷启动毫秒级,适合容器化和弹性伸缩。
  • 并发模型好:goroutine 写高并发服务很自然,资源占用也低。
  • 标准库强:net/http、encoding/json 这些开箱即用,不依赖大量第三方库。

我最早写 Java 的时候,一个 Spring Boot 服务启动要好几秒,内存占用也大。换成 Go 之后,一个服务二进制几 MB,启动几百毫秒,资源占用少很多,部署体验完全不一样。

RPC:服务之间怎么调用

微服务之间通信主要有两种方式:HTTP REST 和 RPC。REST 简单通用,但性能和类型安全差一些;RPC 效率高,接口定义清晰,适合内部服务调用。

gRPC 是主流选择

Go 生态里最常用的 RPC 框架是 gRPC,它基于 HTTP/2 和 Protobuf,支持双向流、超时控制、拦截器等。

先定义一个 .proto 文件:

syntax = "proto3";

package user;

option go_package = "github.com/example/user/pb";

service UserService {
rpc GetUser (GetUserRequest) returns (GetUserResponse);
}

message GetUserRequest {
int64 id = 1;
}

message GetUserResponse {
int64 id = 1;
string name = 2;
int32 age = 3;
}

然后用 protoc 生成 Go 代码:

protoc –go_out=. –go-grpc_out=. user.proto

生成的代码包含客户端和服务端接口,实现服务端:

type UserServer struct {
pb.UnimplementedUserServiceServer
}

func (s *UserServer) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.GetUserResponse, error) {
// 查询逻辑
return &pb.GetUserResponse{
Id: req.Id,
Name: "小明",
Age: 25,
}, nil
}

客户端调用:

conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
if err != nil {
log.Fatal(err)
}
defer conn.Close()

client := pb.NewUserServiceClient(conn)
resp, err := client.GetUser(context.Background(), &pb.GetUserRequest{Id: 1})
if err != nil {
log.Fatal(err)
}
fmt.Println(resp.Name)

gRPC 的好处是接口定义强类型,改动 proto 后重新生成代码,编译期就能发现不兼容的问题。而且 Protobuf 序列化效率比 JSON 高不少,网络传输开销小。

gRPC 拦截器

实际项目里,每个 RPC 调用可能都需要打日志、传 trace id、做鉴权、限流等。gRPC 提供了拦截器(Interceptor),可以统一处理这些横切逻辑:

func loggingInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
start := time.Now()
resp, err := handler(ctx, req)
log.Printf("method=%s duration=%v err=%v", info.FullMethod, time.Since(start), err)
return resp, err
}

server := grpc.NewServer(grpc.UnaryInterceptor(loggingInterceptor))

拦截器的作用和 HTTP 中间件类似,可以把日志、监控、认证这些逻辑从业务代码里剥离出来。

服务注册与发现

服务多了之后,服务 A 要调用服务 B,不能硬编码 B 的地址,因为 B 可能多实例部署、动态扩缩容。这时就需要服务注册与发现。

常见的方案有:

  • Consul:功能全,支持健康检查、KV 存储。
  • etcd:Kubernetes 的底层存储,常用于服务发现。
  • Nacos:阿里开源,支持配置中心和服务发现。
  • Kubernetes Service:如果你跑在 K8s 上,直接用它自带的服务发现就行。

以 etcd 为例,服务启动时注册自己:

func register(etcdClient *clientv3.Client, serviceName, addr string) error {
lease, err := etcdClient.Grant(context.Background(), 10)
if err != nil {
return err
}
key := fmt.Sprintf("/services/%s/%s", serviceName, addr)
_, err = etcdClient.Put(context.Background(), key, addr, clientv3.WithLease(lease))
if err != nil {
return err
}
// 定期续约
go func() {
for {
etcdClient.KeepAliveOnce(context.Background(), lease.ID)
time.Sleep(5 * time.Second)
}
}()
return nil
}

调用方通过监听 /services/user/ 前缀,获取所有可用实例的地址,客户端负载均衡选一个调用。

实际上,很多 gRPC 客户端库(比如 grpc-go 的 resolver)已经内置了服务发现的支持,配置好 resolver 后可以自动感知实例变化。

链路追踪

微服务调用链很长,一个请求可能经过 A -> B -> C -> D,出问题时排查很困难。链路追踪就是为了解决这个问题,把一次请求的所有调用串起来。

OpenTelemetry 是目前的主流标准,兼容 Jaeger、Zipkin 等后端。Go 里用它分三步:

初始化 tracer:

import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/jaeger"
"go.opentelemetry.io/otel/sdk/trace"
)

func initTracer() (*trace.TracerProvider, error) {
exporter, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint("http://localhost:14268/api/traces")))
if err != nil {
return nil, err
}
tp := trace.NewTracerProvider(trace.WithBatcher(exporter))
otel.SetTracerProvider(tp)
return tp, nil
}

在服务里创建 span:

tracer := otel.Tracer("user-service")

func (s *UserServer) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.GetUserResponse, error) {
ctx, span := tracer.Start(ctx, "GetUser")
defer span.End()

// 调用下游服务时,把 ctx 传下去,trace 会自动串联
order, err := s.orderClient.GetOrder(ctx, req.Id)
if err != nil {
span.RecordError(err)
return nil, err
}
return &pb.GetUserResponse{}, nil
}

关键点是 ctx 一路传下去,trace id 通过 HTTP header 或 gRPC metadata 传播,下游服务自动接上。这样在 Jaeger 界面里就能看到完整的调用链,每个环节耗时多少,一目了然。

我之前线上排查一个慢请求,就是从链路追踪里发现某个数据库查询耗时 2 秒,很快就定位了问题。

限流与熔断

微服务里,一个服务出问题可能引发雪崩,所以限流和熔断是必备的保护机制。

限流

常用的是令牌桶算法,Go 里可以用 golang.org/x/time/rate:

limiter := rate.NewLimiter(100, 200) // 每秒 100 个,桶容量 200

func handler(w http.ResponseWriter, r *http.Request) {
if !limiter.Allow() {
http.Error(w, "请求过于频繁", http.StatusTooManyRequests)
return
}
// 业务逻辑
}

这个库简单可靠,单机限流够用了。分布式限流需要借助 Redis,用 Lua 脚本实现滑动窗口。

熔断

熔断的思路是:当某个下游服务连续失败达到阈值,就“熔断”,暂时不再调用它,直接返回错误,避免资源被拖垮。过一段时间再放几个请求试探,如果恢复就关闭熔断。

Go 里可以用 sony/gobreaker:

var cb *gobreaker.CircuitBreaker

func init() {
cb = gobreaker.NewCircuitBreaker(gobreaker.Settings{
Name: "user-service",
MaxRequests: 3,
Interval: 10 * time.Second,
Timeout: 30 * time.Second,
ReadyToTrip: func(counts gobreaker.Counts) bool {
return counts.ConsecutiveFailures > 5
},
})
}

func callUserService(ctx context.Context, id int64) (*pb.GetUserResponse, error) {
result, err := cb.Execute(func() (interface{}, error) {
return client.GetUser(ctx, &pb.GetUserRequest{Id: id})
})
if err != nil {
return nil, err
}
return result.(*pb.GetUserResponse), nil
}

熔断器有三个状态:关闭(正常调用)、打开(直接拒绝)、半开(试探性放行)。实际使用时,结合重试和超时,服务稳定性会好很多。

配置中心

微服务实例多,配置文件不可能一个个改,所以需要配置中心统一管理。常见方案有 Nacos、Apollo、Consul KV、etcd。

我一般用 Nacos 或者 Apollo,配置变更后推送到服务实例,支持热更新,不用重启。比如用 Nacos 的 Go SDK:

client, _ := vo.NewClient(vo.NacosClientParam{
ServerConfigs: []constant.ServerConfig{
{IpAddr: "127.0.0.1", Port: 8848},
},
})

config, _ := clients.NewConfigClient(vo.NacosClientParam{})
content, _ := config.GetConfig(vo.ConfigParam{
DataId: "user-service.yaml",
Group: "DEFAULT_GROUP",
})

配置中心和服务发现经常一起用,Nacos 两者都支持。

一些实践经验

  • 服务拆分不要太细:微服务不是越细越好,拆太细调用链长、运维成本高。我见过一个团队把用户服务拆成用户基本信息、用户账户、用户地址三个服务,结果一个查询要调三个服务,反而更慢。

  • 接口向后兼容:微服务之间通过 proto 或 JSON 通信,改动接口时要注意兼容性,用 proto 的话不要删除字段、不要改字段编号,用 JSON 的话新增字段用 omitempty。

  • 重试要谨慎:重试可以提升成功率,但要注意幂等性,写操作盲目重试可能重复扣款。一般只对读操作重试,写操作配合幂等键。

  • 超时必不可少:每个 RPC 调用都要设置超时,用 context 控制。没有超时的调用是灾难的开始。

  • 日志要带 trace id:排查问题时,通过 trace id 能把一个请求的所有日志串起来,这是基本功。

  • 健康检查要真实:不要只返回 200,要检查依赖的数据库、缓存是否可用。

  • 小结

    这篇把 Go 微服务的常见实践梳理了一遍:

    • gRPC 是 Go 微服务的主流 RPC 框架,接口定义清晰,性能好。
    • 服务注册发现解决动态地址问题,常用 etcd、Consul、Nacos。
    • 链路追踪用 OpenTelemetry,关键是把 ctx 一路传下去。
    • 限流用 rate,熔断用 gobreaker,保护服务稳定性。
    • 配置中心统一管理配置,支持热更新。
    • 拆分粒度、兼容性、重试、超时、日志是实践中的重点。

    微服务是个很大的话题,这篇只是把 Go 生态里常见的东西过了一遍。真正落地时还需要结合团队、业务和基础设施来选型。下一篇准备写 Go 与云原生,聊聊 Docker 打包、Kubernetes 部署、健康检查、优雅退出这些运维相关的内容,把从开发到上线的链路补齐。

    如果这篇文章对你有帮助,欢迎点赞收藏,评论区一起交流。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » Go 语言入门笔记(十七):微服务常见实践——从单体到分布式
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!