12-Kratos 框架特性与微服务进阶

💡 核心提示:Kratos 不仅仅是一个框架,更是一套微服务设计的标准实践。它强调 Protobuf First 的开发模式,并通过清晰的架构分层(Transport/Middleware)来解耦业务与基础设施。

1. Protocol Buffers:微服务的“普通话”

在 Kratos 中,.proto 文件是绝对的核心。它不仅定义了数据结构,还定义了服务接口(API)。

为什么它是必须的?

  • 强类型契约:相比于 JSON 的灵活但松散,Protobuf 定义了严格的输入输出,避免了“字段拼写错误”导致的扯皮。
  • 跨语言支持:后端用 Go,前端可以用 TS 生成客户端代码,甚至 Python 脚本也能直接调用,大家共用一份 .proto 定义。
  • 高性能:二进制序列化,比 XML/JSON 小且快。

在 Kratos 中的地位

Kratos 也是 Protobuf First

  1. 先写 .proto 定义 API。
  2. 使用 protoc 工具生成 Go 代码(Payload 结构体 + gRPC/HTTP 接口存根)。
  3. 开发者只需要实现生成的 server 接口。
// 例子:定义一个打招呼服务
service Greeter {
  // 定义个 API,既能生成 gRPC 也能通过 google.api.http 映射成 RESTful 接口
  rpc SayHello (HelloRequest) returns (HelloReply) {
    option (google.api.http) = {
      get: "/helloworld/{name}"
    };
  }
}

2. Kratos 核心机制导读

Kratos 的设计非常注重模块化,其中 TransportMiddleware 是最为精华的部分。

2.1 Transport 层 (传输层)

Kratos 将 HTTP 和 gRPC 统一抽象为 Transport。这意味着你可以:

  • 一套代码,双协议支持:你写的业务逻辑(Biz/Service)不需要关心请求是来自于 HTTP 还是 gRPC。它们在进入 Service 层之前已经被 Transport 层统一适配了。
  • 优雅启动与停止:Kratos 的 App 管理着所有的 Server(HTTP/gRPC),统一处理信号量、优雅关闭(Graceful Shutdown),保证请求不丢失。

2.2 Middleware (中间件) 机制

这是 Kratos 最灵活的地方。如果你熟悉 Gin 的中间件,Kratos 的也类似,但更强大,因为它同时作用于 HTTP 和 gRPC

Selector 设计模式: 中间件本质上是一个“洋葱模型”。请求进来先经过 Logging、Recovery、Tracing、Validate 等中间件,最后到达你的 Handler。

// 源码简析:Middleware 本质上是一个函数闭包
type Middleware func(Handler) Handler
 
// 只要实现了这个签名,就能拦截请求
// 比如我们之前想做的“API请求耗时统计”,就可以写成一个 Middleware
func ServiceTelemetryMiddleware() middleware.Middleware {
    return func(handler middleware.Handler) middleware.Handler {
        return func(ctx context.Context, req interface{}) (reply interface{}, err error) {
            start := time.Now()
            reply, err = handler(ctx, req) // 执行下一个中间件或业务逻辑
            duration := time.Since(start)
            // 记录指标...
            return
        }
    }
}

建议阅读源码路径

  • transport/http: 看看它是如何适配标准 net/http 的。
  • middleware: 看看官方提供的 logging, recovery, tracing 是怎么写的。

3. 服务治理入门

随着微服务数量增多,服务之间的协作(治理)变得至关重要。

3.1 服务发现 (Service Discovery)

  • 问题:微服务 A 要调用 微服务 B。B 部署了 10 个实例(IP 不同),A 怎么知道这 10 个 IP 是多少?如果 B 的某个实例挂了,A 怎么避开它?
  • 解决:引入一个“注册中心”(Registry,如 Consul, Etcd)。
    • B 启动时,把自己的 IP 注册到 Registry。
    • A 启动时(或调用时),从 Registry 查 B 的 IP 列表。
  • Kratos 的做法:Kratos 定义了 Registry 接口。你只要在配置里换个插件,就能从 Etcd 切换到 Consul,业务代码完全不用改。

3.2 熔断与降级 (Circuit Breaking & Fallback)

  • 问题:B 服务因为数据库慢,响应很慢。A 调用 B 时一直在等待,导致 A 的线程也卡住,最后 A 也挂了(雪崩效应)。
  • 解决(熔断):A 发现 B 错误率高或响应慢,直接“跳闸”,不再调用 B,而是直接报错或返回默认值。等 B 好了再恢复。
  • 解决(降级):B 挂了,A 返回一个兜底数据(比如“暂无推荐内容”),而不是直接报错 crash。

4. ServiceTelemetry 项目演进结合

我们目前的 ServiceTelemetry 项目还是单体结构(虽然目录分层了),下一步向 Kratos 风格演进时:

  1. API 定义:我们会开始写 .proto 文件,定义 Agent 上报数据的接口。
  2. 插件化:目前的 internal/probe 可以尝试改写,虽然它不是标准的 HTTP 服务,但我们可以借鉴 Kratos 的 Option 模式 来配置探针。
  3. 中间件应用:那个 gin.Handler 里的逻辑,可以思考即便不换框架,能不能抽离成类似 Middleware 的独立逻辑块,而不是写死在 Controller 里。

推荐学习路径

  1. Protobuf 语法:先看懂 message, service, rpc 关键字。
  2. Kratos 快速开始:跑通官方的 helloworld demo,观察生成的 pb.go 代码。
  3. 阅读 Middleware 源码:挑一个简单的(如 recovery)看看它是怎么 recover panic 的。

5. 深入解析 Protobuf 核心语法 (message, service, rpc)

既然你对这三个关键字感兴趣,我们来详细拆解一下。在微服务开发中,这三个词分别对应了数据结构服务定义方法定义

5.1 message:定义数据结构

message 就像 Go 里的 struct。它定义了在这个服务间传输的数据长什么样。

// 定义一个“用户”数据结构
message User {
  // 字段格式:类型 字段名 = 唯一编号;
  
  string name = 1; // 字符串类型
  int32 age = 2;   // 整型
  bool is_active = 3; // 布尔型
  
  // 甚至可以嵌套
  Address address = 4;
}
 
message Address {
  string city = 1;
  string street = 2;
}

为什么要有唯一编号 (Tag)? Protobuf 序列化成二进制时,不存字段名(如 “name”),只存这个编号(1)。

  • 优点:极大地压缩了体积。
  • 注意:一旦定义好发布了,千万不要修改已有的编号(比如把 name 改成 2),否则旧版本的服务就读不懂数据了。

5.2 service:定义服务接口

service 就像 Go 里的 interface。它定义了一组相关功能的集合。在微服务里,通常一个微服务对应一个 service 定义。

// 定义一个“账户服务”
service AccountService {
  // 里面包含各种方法...
}

5.3 rpc:定义具体方法

rpc (Remote Procedure Call) 就是 service 里的具体函数。

它的标准格式非常固定:rpc 方法名 (请求消息) returns (响应消息);

service AccountService {
  // 1. 最普通的调用:一问一答
  rpc GetUser (GetUserRequest) returns (User);
  
  // 2. 只有入参,返回为空(用 google.protobuf.Empty)
  // rpc DeleteUser (DeleteUserRequest) returns (google.protobuf.Empty);
}
 
// 必须为每个 RPC 定义 Request 和 Response message
// 哪怕只有一个字段,也要包装在 message 里,这是 Protobuf 的最佳实践
message GetUserRequest {
  string user_id = 1;
 
  // 即使这里现在只有一个字段,未来如果通过 ID 查不到想加个“是否强制查询”,直接加字段就行
  // 这种设计保证了接口的兼容性
}

总结对照表

Protobuf 关键字Go 语言对应概念作用
messagestruct定义传输的数据包格式(DTO)
serviceinterface定义服务的抽象接口
rpcfunc (Function)定义接口里的具体方法签名

实际生成的 Go 代码样子

Protoc 工具会把上面的定义翻译成类似这样的 Go 代码:

type User struct { // message -> struct
    Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"`
    Age  int32  `protobuf:"varint,2,opt,name=age" json:"age,omitempty"`
    // ...
}
 
type AccountServiceServer interface { // service -> interface
    GetUser(context.Context, *GetUserRequest) (*User, error) // rpc -> func
}

5.4 进阶语法:repeated, enum, oneof, map

掌握了基础的 messageservicerpc 后,以下是你会经常用到的进阶关键字。

repeated:数组/切片

message User {
  string name = 1;
  repeated string tags = 2;         // 对应 Go 的 []string
  repeated Address addresses = 3;   // 对应 Go 的 []*Address
}

生成的 Go 代码:

type User struct {
    Name      string     `json:"name,omitempty"`
    Tags      []string   `json:"tags,omitempty"`       // 切片
    Addresses []*Address `json:"addresses,omitempty"`  // 指针切片
}

enum:枚举类型

enum UserStatus {
  USER_STATUS_UNSPECIFIED = 0;  // 必须有 0 值(默认值)
  USER_STATUS_ACTIVE = 1;
  USER_STATUS_BANNED = 2;
  USER_STATUS_DELETED = 3;
}
 
message User {
  string name = 1;
  UserStatus status = 2;  // 使用枚举
}

生成的 Go 代码:

type UserStatus int32
 
const (
    UserStatus_USER_STATUS_UNSPECIFIED UserStatus = 0
    UserStatus_USER_STATUS_ACTIVE      UserStatus = 1
    UserStatus_USER_STATUS_BANNED      UserStatus = 2
    UserStatus_USER_STATUS_DELETED     UserStatus = 3
)

命名规范:枚举值推荐加前缀(如 USER_STATUS_),避免与其他枚举冲突。

oneof:互斥字段(只能选一个)

message SearchRequest {
  // 搜索条件:只能按 ID 或按名字搜索,不能同时传
  oneof search_by {
    string user_id = 1;
    string username = 2;
    string email = 3;
  }
}

生成的 Go 代码:

type SearchRequest struct {
    // 用接口实现互斥
    SearchBy isSearchRequest_SearchBy  // 只能是下面三种之一
}
 
type SearchRequest_UserId struct { UserId string }
type SearchRequest_Username struct { Username string }
type SearchRequest_Email struct { Email string }

使用时需要类型断言:

switch v := req.SearchBy.(type) {
case *SearchRequest_UserId:
    // 按 ID 搜索
case *SearchRequest_Username:
    // 按用户名搜索
}

map:键值对

message User {
  string name = 1;
  map<string, string> metadata = 2;  // 对应 Go 的 map[string]string
  map<int32, Address> addresses = 3; // 对应 Go 的 map[int32]*Address
}

生成的 Go 代码:

type User struct {
    Name      string              `json:"name,omitempty"`
    Metadata  map[string]string   `json:"metadata,omitempty"`
    Addresses map[int32]*Address  `json:"addresses,omitempty"`
}

限制:map 的 key 只能是整数或字符串类型,不能是浮点数或 message。

optional(Proto3 语法)

在 Proto3 中,所有字段默认都是可选的。但如果你想区分”没传”和”传了零值”,需要显式声明 optional

message UpdateUserRequest {
  string user_id = 1;
  optional string name = 2;       // 可以区分 null 和空字符串
  optional int32 age = 3;         // 可以区分 null 和 0
}

生成的 Go 代码:

type UpdateUserRequest struct {
    UserId string  `json:"user_id,omitempty"`
    Name   *string `json:"name,omitempty"`  // 指针!nil 表示没传
    Age    *int32  `json:"age,omitempty"`   // 指针!nil 表示没传
}

进阶语法对照表

ProtobufGo 类型用途
repeated T[]T[]*T数组/列表
enumint32 + 常量有限状态集
oneof接口 + 类型断言互斥字段
map<K, V>map[K]V键值对
optional T*T(指针)区分空值和零值

6. K8s (Kubernetes) 在其中的角色

你提到的比喻 “K8s 是 Docker 的管家” 非常精准!

在微服务架构中,Kratos 和 K8s 都能实现类似的功能(如服务发现),但它们工作在不同的层面,跨语言通用性也不同

6.1 K8s 的核心职责(管家做了什么?)

如果有 100 个微服务实例(Docker 容器):

  • 调度 (Scheduling):决定把哪个容器放到哪台服务器上跑(比如这台 CPU 闲,就放这)。
  • 自愈 (Self-healing):如果某个容器崩溃了,K8s 自动重启它;如果整台服务器挂了,K8s 把上面的容器挪到别的机器。
  • 配置管理 (ConfigMap/Secret):把配置文件注入到容器里。

6.2 关键冲突点:服务发现 (Service Discovery)

这里是初学者最容易晕的地方:Kratos 能做服务发现(用 Consul/Etcd),K8s 也能做(用 DNS/Service IP),用哪个?

方案Kratos 方式 (客户端发现)K8s 方式 (服务端发现)
原理Pod 启动时自己去 Etcd 注册 IP。客户端调用前先查 Etcd 拿到 IP 列表,自己决定连哪个。K8s 给一组 Pod 分配一个虚拟 IP (ClusterIP)。客户端只管调这个虚拟 IP,K8s 底层负责转发。
优点更灵活。客户端可以做复杂的负载均衡策略(比如:P2C 算法,或者灰度发布只调 v2 版本)。这对应用透明。代码里完全不需要集成 Etcd 客户端,不管后端怎么变,调用地址不变。
缺点代码要集成注册中心 SDK,依赖外部组件 (Etcd)。负载均衡策略比较死板(通常是轮询),难以做精细化控制。
跨语言⚠️ Go 生态专属。如果有 Python/Java 服务,它们也要各自集成 Etcd SDK,增加复杂度。语言无关。任何语言写的服务,只要部署在 K8s 里,都能用 DNS 互调。

6.3 最佳实践建议

对于 Go + Kratos 微服务,通常有两种流派:

  1. 纯 K8s 模式(适合初期)

    • 不用 Etcd,不用 Consul。
    • 直接用 K8s 的 Service 域名调用(如 http://account-service:8000)。
    • 简单,运维成本低。
  2. Kratos + K8s 混合模式(适合大厂/高性能)

    • K8s 负责部署和保活(管家职责)。
    • Kratos 负责服务发现(绕过 K8s 的 Service IP,直接点对点连 Pod IP)。
    • 为什么? 因为 K8s 的转发有一点点性能损耗,且不如客户端直连灵活。

结论:既然我们刚开始学,你可以先把 K8s 当作纯粹的部署运维平台(管家),负责把你的 Go 程序跑起来、别挂掉。服务内部的逻辑(如 API 定义、数据验证)交给 Kratos。


7. 微服务中的路由层架构

在微服务架构中,“路由”不仅仅是 URL 匹配,而是涉及多层的请求分发

7.1 三层路由结构

                     ┌─────────────────────────────────────────────────────────┐
                     │                   外部请求 (客户端/浏览器)                  │
                     └─────────────────────────────────────────────────────────┘
                                                    ↓
┌────────────────────────────────────────────────────────────────────────────────────────────┐
│ 1️⃣ API 网关 (外部路由层)                                                                       │
│    - Nginx / Kong / Traefik / K8s Ingress                                                 │
│    - 职责:SSL 卸载、限流、认证、按路径分发到不同微服务                                               │
│    - 例子:/user/* → user-service, /order/* → order-service                                 │
└────────────────────────────────────────────────────────────────────────────────────────────┘
                                                    ↓
┌────────────────────────────────────────────────────────────────────────────────────────────┐
│ 2️⃣ 微服务内部路由层 (Kratos Transport/HTTP Server)                                             │
│    - 每个微服务内部的 HTTP/gRPC Server                                                          │
│    - 职责:接收请求,分发到具体的 Handler/Service 方法                                             │
│    - Kratos:由 .proto 文件自动生成,开发者不需要手写路由                                            │
└────────────────────────────────────────────────────────────────────────────────────────────┘
                                                    ↓
┌────────────────────────────────────────────────────────────────────────────────────────────┐
│ 3️⃣ 业务逻辑层 (Biz/Service)                                                                   │
│    - 实际处理业务逻辑的地方                                                                       │
│    - 与路由解耦,只关心输入输出                                                                    │
└────────────────────────────────────────────────────────────────────────────────────────────┘

7.2 Kratos 的”无路由”设计

在 Kratos 框架里,你几乎不需要手写路由。路由是通过 .proto 文件生成的。

// api/user/v1/user.proto
service UserService {
  // 这里的 option 就是路由定义!
  rpc GetUser (GetUserRequest) returns (User) {
    option (google.api.http) = {
      get: "/api/v1/user/{id}"   // 对应 GET /api/v1/user/123
    };
  }
 
  rpc CreateUser (CreateUserRequest) returns (User) {
    option (google.api.http) = {
      post: "/api/v1/user"
      body: "*"
    };
  }
}

执行 kratos proto client 后,会自动生成 HTTP 路由代码,你只需要实现业务逻辑

// internal/service/user.go
func (s *UserService) GetUser(ctx context.Context, req *v1.GetUserRequest) (*v1.User, error) {
    // 直接写业务逻辑,不用管路由
    user, err := s.uc.GetUser(ctx, req.Id)
    return user, err
}

7.3 对比传统 Gin 路由

特点传统 Gin 路由Kratos 风格
定义方式手写 r.GET("/user/:id", handler).proto 里用 option (google.api.http)
协议支持仅 HTTPHTTP + gRPC 自动双协议
维护成本路由分散在代码各处集中在 .proto 文件
类型安全手动解析参数自动生成强类型 Request/Response

总结:在 Kratos 微服务里,“路由”这个概念被弱化了。你定义的是API 契约(.proto),而不是路由规则。路由只是契约的副产品。