invocation

package
v1.3.7 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 18, 2026 License: MIT Imports: 12 Imported by: 2

README

Invocation

invocation 包定义 Firefly 当前唯一推荐的服务调用模型。

它只解决四件事:

  • 业务侧如何声明一个远程业务服务的标准 DNS
  • 如何把 DNS 组装成稳定的 gRPC target
  • 如何复用 grpc.ClientConn
  • 如何统一传递 metadata、调用者身份与 Authz 上下文

不再负责:

  • 实例发现
  • 节点选择
  • Consul / K8s 后端适配
  • endpoint 轮询

核心理念

业务服务之间的调用,本质上就是面向一个稳定的业务服务 DNS。

例如:

auth.default.svc.cluster.local:9090

含义如下:

  • auth:业务服务名
  • default:命名空间
  • svc:Kubernetes Service 类型片段
  • cluster.local:Cluster Domain
  • 9090:业务服务端口

业务代码只需要表达这份 DNS 结构。

后续流量如何命中实例:

  • 裸机环境交给 sidecar-agent
  • 云原生环境交给 K8s + Istio + service mesh

当前模型

ServiceDNS

ServiceDNS 表示业务服务的标准 DNS 配置。

它直接描述:

  • service
  • namespace
  • service_type
  • cluster_domain
  • port
DNSManager

DNSManager 只负责补齐默认值并构造最终 Target

它不会做:

  • service 校验
  • endpoint 拉取
  • 实例选择
ConnectionManager

ConnectionManager 负责:

  • 基于 ServiceDNS 构造 Target
  • 按最终 gRPC target 缓存连接
  • 统一挂载 gRPC client dial options
UnaryInvoker

UnaryInvoker 负责:

  • 取连接
  • 注入调用 metadata
  • 接入 Authz
  • 发起真实 gRPC unary 调用
InvocationContext

InvocationContext 负责:

  • 统一 metadata
  • 调用者身份
  • timeout
  • trace 相关上下文

一个业务服务多个 proto 子服务

这是当前模型里的重要约束:

  • 一个远程业务服务只维护一份 ServiceDNS
  • 同一个业务服务下的多个 proto 子服务,共用同一份 DNS 和连接
  • 具体调用哪个子服务,由 gRPC full method 决定

例如:

  • 远程业务服务:auth
  • proto 子服务:
    • AuthAppService
    • AuthUserService
    • AuthPermissionService

这些调用都应该共用:

auth.default.svc.cluster.local:9090

推荐接入方式

业务服务应在自己的 internal/data/rs_*.go 中,按“远程业务服务”聚合配置。

推荐做法:

  • New*Repo 中声明该 repo 依赖哪个远程业务服务
  • 为该远程业务服务组装一份 ServiceDNS
  • 复用同一份 ConnectionManager / UnaryInvoker
  • 通过不同 full method 区分具体 proto 子服务

不推荐做法:

  • 按每个 proto 子服务单独维护一份远程地址配置
  • 在调用侧做实例发现
  • 在调用侧感知 Consul / K8s 细节

示例

package example

import (
	"context"
	"time"

	"github.com/fireflycore/go-micro/invocation"
)

func Example() error {
	dnsManager := invocation.NewDNSManager(&invocation.DNSConfig{
		DefaultNamespace: "default",
		DefaultPort:      9090,
	})

	manager, err := invocation.NewConnectionManager(invocation.ConnectionManagerOptions{
		DNSManager: dnsManager,
	})
	if err != nil {
		return err
	}
	defer func() { _ = manager.Close() }()

	invoker := &invocation.UnaryInvoker{
		Dialer: manager,
	}

	service := &invocation.ServiceDNS{
		Service: "auth",
	}

	return invoker.Invoke(
		context.Background(),
		service,
		"/acme.auth.app.v1.AuthAppService/GetAppSecret",
		&struct{}{},
		&struct{}{},
		invocation.WithInvocationContext(&invocation.InvocationContext{
			Timeout: 3 * time.Second,
		}),
	)
}

观测约定

invocation 默认和 go-micro 的 OTel 链路保持一致:

  • gRPC client 默认挂 otelgrpc
  • metadata 由 InvocationContext 统一构造
  • Authz 上下文由 NewAuthzContext() 统一生成

设计约束

  • 业务侧只表达业务服务 DNS,不表达实例选择逻辑
  • invocation 只保留通用调用语义,不承载后端专属实现
  • go-consul/invocationgo-k8s/invocation 不再作为主路径保留
  • Authz 默认作为调用前外挂能力接入

Documentation

Overview

Package invocation 定义面向 service-to-service 调用模型的核心错误。

Index

Constants

View Source
const (
	// DefaultServiceType 是 Kubernetes Service FQDN 中的固定服务类型片段。
	DefaultServiceType = "svc"
	// DefaultClusterDomain 是 Kubernetes 集群默认的 Cluster Domain。
	DefaultClusterDomain = "cluster.local"
	// DefaultResolverScheme 是 gRPC 默认推荐使用的 DNS resolver scheme。
	DefaultResolverScheme = "dns"
	// DefaultServicePort 是业务服务默认使用的 gRPC 端口。
	DefaultServicePort = 9090
)
View Source
const MetaKeyParseErrorFormat = "%s 解析失败"

Variables

View Source
var (
	// ErrServiceNameEmpty 表示服务名为空,无法构造逻辑服务身份。
	ErrServiceNameEmpty = errors.New("service name is empty")
	// ErrNamespaceEmpty 表示命名空间为空。
	ErrNamespaceEmpty = errors.New("namespace is empty")
	// ErrTargetHostEmpty 表示目标主机为空,无法生成最终拨号地址。
	ErrTargetHostEmpty = errors.New("target host is empty")
	// ErrTargetPortInvalid 表示端口既未显式提供,也无法从默认值中补齐。
	ErrTargetPortInvalid = errors.New("target port is invalid")
	// ErrDNSManagerIsNil 表示 DNS 管理器为空。
	ErrDNSManagerIsNil = errors.New("dns manager is nil")
	// ErrDialFnIsNil 表示底层拨号函数为空。
	ErrDialFnIsNil = errors.New("dial function is nil")
	// ErrConnectionManagerClosed 表示连接管理器已经关闭,不能再创建新连接。
	ErrConnectionManagerClosed = errors.New("connection manager is closed")
	// ErrInvokerDialerIsNil 表示调用器缺少拨号器依赖。
	ErrInvokerDialerIsNil = errors.New("invoker dialer is nil")
	// ErrInvokeMethodEmpty 表示调用方法名为空。
	ErrInvokeMethodEmpty = errors.New("invoke method is empty")
)

Functions

func DefaultDialFunc

func DefaultDialFunc(_ context.Context, target Target, options []grpc.DialOption) (*grpc.ClientConn, error)

DefaultDialFunc 是默认的 grpc.ClientConn 创建逻辑。

当前实现采用 grpc.NewClient,并默认启用: - insecure credentials:便于在内部受控网络中快速起步; - otelgrpc client handler:保证调用链路自动接入 OTel。

后续若需要在具体实现中启用 mTLS、自定义 resolver 或更多 dial option, 可以通过 ConnectionManagerOptions 覆盖该行为。

func ParseMetaKey added in v1.3.4

func ParseMetaKey(md metadata.MD, key string) (string, error)

func WithUserContext added in v1.3.4

func WithUserContext(ctx context.Context, meta *UserContextMeta) context.Context

WithUserContext 将 UserContextMeta 存入 context。

通常在 gRPC server interceptor 中调用,解析一次后存入 context, 后续 handler 可以直接通过 UserContextFromContext 获取,无需重复解析。

示例:

func UserContextInterceptor() grpc.UnaryServerInterceptor {
    return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
        md, ok := metadata.FromIncomingContext(ctx)
        if ok {
            userMeta, err := invocation.ParseUserContextMeta(md)
            if err == nil {
                ctx = invocation.WithUserContext(ctx, userMeta)
            }
        }
        return handler(ctx, req)
    }
}

Types

type Authorizer

type Authorizer interface {
	Authorize(ctx context.Context, input *AuthzContext) error
}

Authorizer 定义外挂 Authz 的最小能力集合。

返回 nil 表示允许调用; 返回非 nil error 表示拒绝调用或 Authz 本身发生错误。

type AuthzContext

type AuthzContext struct {
	// Service 表示目标业务服务的 DNS 身份。
	Service *ServiceDNS `json:"service"`
	// FullMethod 表示完整 gRPC 方法,例如 /acme.user.v1.UserService/GetUser。
	FullMethod string `json:"full_method"`
	// TraceID 表示链路 ID。
	TraceId string `json:"trace_id"`
	// Caller 表示调用方身份。
	Caller Caller `json:"caller"`
	// Metadata 表示构造判定时附带的完整 metadata 副本。
	Metadata metadata.MD `json:"-"`
}

AuthzContext 表示外挂 Authz 所需的标准化输入。

该对象不直接绑定某个 Authz 实现, 只表达“做权限判断时必须稳定得到的字段”。

func NewAuthzContext

func NewAuthzContext(service *ServiceDNS, method string, invocation *InvocationContext) *AuthzContext

NewAuthzContext 根据 ServiceDNS、方法名和 InvocationContext 生成标准 AuthzContext。

type Caller

type Caller struct {
	// UserId 表示当前用户身份,可为空。
	UserId string `json:"user_id"`
	// AppId 表示当前应用身份,可为空。
	AppId string `json:"app_id"`
	// TenantId 表示当前租户身份,可为空。
	TenantId string `json:"tenant_id"`
	// OrgIds 表示当前组织范围,可为空。
	OrgIds []string `json:"org_ids"`
	// RoleIds 表示当前角色范围,可为空。
	RoleIds []string `json:"role_ids"`
}

Caller 表示一次服务调用的发起方身份。

这里聚焦的是调用治理与 Authz 所需的统一字段, 避免让每个服务都手工拼装一套不同的 metadata。

type ConnectionManager

type ConnectionManager struct {
	// contains filtered or unexported fields
}

ConnectionManager 负责缓存基于 ServiceDNS 创建出的 grpc.ClientConn。

它把“业务服务 DNS -> 目标解析 -> 连接缓存”统一收敛在一处, 让业务层无需关心: - target 拼装; - resolver scheme; - 拨号选项; - 多次调用的连接复用。

func NewConnectionManager

func NewConnectionManager(options ConnectionManagerOptions) (*ConnectionManager, error)

NewConnectionManager 创建连接管理器。

func (*ConnectionManager) Close

func (m *ConnectionManager) Close() error

Close 关闭连接管理器及其持有的全部 grpc.ClientConn。

Close 会尽最大努力关闭所有连接; 若中途出现错误,当前实现返回第一条错误并继续关闭剩余连接。

func (*ConnectionManager) Dial

func (m *ConnectionManager) Dial(ctx context.Context, service *ServiceDNS) (*grpc.ClientConn, error)

Dial 根据 ServiceDNS 获取或创建对应的 grpc.ClientConn。

连接缓存键采用最终 gRPC target,而不是 ServiceDNS 原始字段, 这样可以保证: - 逻辑上等价的服务身份只会生成一条连接; - 端口覆盖、cluster domain、resolver scheme 的变化都能体现在缓存键上。

type ConnectionManagerOptions

type ConnectionManagerOptions struct {
	// DNSManager 用于把业务服务 DNS 描述解析为最终 Target。
	DNSManager *DNSManager
	// DialFunc 用于创建新的 grpc.ClientConn。
	// 若为空,则使用默认拨号实现。
	DialFunc DialFunc
	// DialOptions 表示创建 grpc.ClientConn 时使用的附加选项。
	DialOptions []grpc.DialOption
}

ConnectionManagerOptions 定义连接管理器的配置。

type DNSConfig added in v1.3.7

type DNSConfig struct {
	// DefaultNamespace 表示默认命名空间。
	DefaultNamespace string
	// DefaultServiceType 表示默认服务类型片段。
	DefaultServiceType string
	// DefaultClusterDomain 表示默认集群域名。
	DefaultClusterDomain string
	// DefaultPort 表示默认端口。
	DefaultPort uint16
	// ResolverScheme 表示默认 gRPC resolver scheme。
	ResolverScheme string
}

DNSConfig 定义标准 DNS 管理器的默认行为。

type DNSManager added in v1.3.7

type DNSManager struct {
	// contains filtered or unexported fields
}

DNSManager 负责把结构化的 ServiceDNS 转成最终 Target。

它只做一件事:组装标准 DNS。 它不做实例发现、不做节点选择,也不做后端适配。

func NewDNSManager added in v1.3.7

func NewDNSManager(config *DNSConfig) *DNSManager

NewDNSManager 创建一个标准 DNS 管理器。

func (*DNSManager) Build added in v1.3.7

func (m *DNSManager) Build(service *ServiceDNS) (*Target, error)

Build 根据 ServiceDNS 构造最终 Target。

func (*DNSManager) Config added in v1.3.7

func (m *DNSManager) Config() *DNSConfig

Config 返回当前管理器的规范化配置副本。

func (*DNSManager) Normalize added in v1.3.7

func (m *DNSManager) Normalize(service *ServiceDNS) *ServiceDNS

Normalize 用默认配置补齐业务服务 DNS。

type DialFunc

type DialFunc func(ctx context.Context, target Target, options []grpc.DialOption) (*grpc.ClientConn, error)

DialFunc 表示底层拨号函数。

把拨号过程抽象成函数有两个目的: 1. 让 ConnectionManager 在不依赖具体后端的情况下复用连接; 2. 让单元测试可以替换真实拨号逻辑,避免触发真实网络连接。

type Dialer

type Dialer interface {
	Dial(ctx context.Context, service *ServiceDNS) (*grpc.ClientConn, error)
	Close() error
}

Dialer 定义“如何把业务服务 DNS 变成 grpc.ClientConn”。

在新模型中,Dialer 只关心: - 标准 DNS target 组装结果; - 连接复用; - gRPC 连接创建。

type InvocationContext

type InvocationContext struct {
	// TraceId 表示当前调用使用的链路 ID。
	TraceId string `json:"trace_id"`
	// Caller 表示调用者身份。
	Caller Caller `json:"caller"`
	// Metadata 表示调用方额外附带的 metadata。
	// 这里允许业务侧做补充,但核心库仍会统一注入标准字段。
	Metadata metadata.MD `json:"-"`
	// Timeout 表示本次调用的超时时间。
	Timeout time.Duration `json:"timeout"`
}

InvocationContext 表示一次调用附带的统一上下文。

它的职责不是替代 context.Context, 而是把“应当被稳定传递和审计的调用元信息”沉淀为一个结构化对象。

func (InvocationContext) BuildMetadata

func (c InvocationContext) BuildMetadata() metadata.MD

BuildMetadata 将 InvocationContext 转换为标准 gRPC metadata。

约定: - 调用方显式提供的 Metadata 会被保留; - 标准字段由核心库统一写入,避免上层重复设置; - 对于切片字段,会整体覆盖为当前上下文中的值。

func (InvocationContext) Clone

Clone 返回 InvocationContext 的深拷贝。 该方法用于避免调用方复用同一个 metadata 引起数据串扰。

func (InvocationContext) NewOutgoingContext

func (c InvocationContext) NewOutgoingContext(parent context.Context) (context.Context, context.CancelFunc)

NewOutgoingContext 基于父上下文构造新的 gRPC 出站上下文。

行为说明: - 若 parent 为 nil,则使用 Background; - metadata 会统一写入 outgoing context; - 若 Timeout > 0,则自动附加超时控制。

type InvokeOption

type InvokeOption func(*InvokeOptions)

InvokeOption 表示单个调用选项。

func WithCallOptions

func WithCallOptions(callOptions ...grpc.CallOption) InvokeOption

WithCallOptions 追加底层 gRPC CallOption。

func WithInvocationContext

func WithInvocationContext(invocation *InvocationContext) InvokeOption

WithInvocationContext 设置本次调用使用的 InvocationContext。

type InvokeOptions

type InvokeOptions struct {
	// InvocationContext 表示本次调用要附带的统一上下文。
	InvocationContext *InvocationContext
	// CallOptions 表示附加的 gRPC CallOption。
	CallOptions []grpc.CallOption
}

InvokeOptions 表示一次调用的附加配置。

type Invoker

type Invoker interface {
	Invoke(ctx context.Context, service *ServiceDNS, method string, req any, resp any, options ...InvokeOption) error
}

Invoker 定义统一调用入口。

与业务代码直接操作 grpc.ClientConn 不同, Invoker 允许框架在调用前统一完成: - 业务服务 DNS 目标解析; - metadata 注入; - Authz 预检查; - 底层连接复用。

type ServiceDNS added in v1.3.7

type ServiceDNS struct {
	// Service 表示业务服务名,例如 auth。
	Service string `json:"service"`
	// Namespace 表示命名空间,例如 default。
	Namespace string `json:"namespace"`
	// ServiceType 表示服务类型片段,默认值为 svc。
	ServiceType string `json:"service_type"`
	// ClusterDomain 表示集群域,默认值为 cluster.local。
	ClusterDomain string `json:"cluster_domain"`
	// Port 表示业务服务监听端口,默认值为 9090。
	Port uint16 `json:"port"`
}

ServiceDNS 表示一个业务服务的标准 DNS 配置。

这里表达的是“这个业务服务在网络上的稳定入口”, 而不是“这个服务当前有哪些实例”。

例如: - service: auth - namespace: default - service_type: svc - cluster_domain: cluster.local - port: 9090

最终会得到: - host: auth.default.svc.cluster.local - address: auth.default.svc.cluster.local:9090

func (ServiceDNS) ClusterDomainName added in v1.3.7

func (s ServiceDNS) ClusterDomainName() string

ClusterDomainName 返回清理空格后的集群域名。

func (ServiceDNS) EffectivePort added in v1.3.7

func (s ServiceDNS) EffectivePort(defaultPort uint16) (uint16, error)

EffectivePort 返回当前 ServiceDNS 的最终端口。

func (ServiceDNS) NamespaceName added in v1.3.7

func (s ServiceDNS) NamespaceName() string

NamespaceName 返回清理空格后的命名空间。

func (ServiceDNS) ServiceName added in v1.3.7

func (s ServiceDNS) ServiceName() string

ServiceName 返回清理空格后的服务名。

func (ServiceDNS) ServiceTypeName added in v1.3.7

func (s ServiceDNS) ServiceTypeName() string

ServiceTypeName 返回清理空格后的服务类型。

func (ServiceDNS) Validate added in v1.3.7

func (s ServiceDNS) Validate() error

Validate 检查 ServiceDNS 是否具备生成稳定 DNS 的最小字段。

func (ServiceDNS) WithPort added in v1.3.7

func (s ServiceDNS) WithPort(port uint16) ServiceDNS

WithPort 返回带显式端口的新 ServiceDNS。

type Target

type Target struct {
	// ResolverScheme 表示 gRPC resolver scheme,例如 dns。
	ResolverScheme string `json:"resolver_scheme"`
	// Host 表示服务的标准主机名。
	Host string `json:"host"`
	// Port 表示服务端口。
	Port uint16 `json:"port"`
}

Target 表示最终可拨号的 gRPC 服务目标。

它已经不表达实例列表,只表达一个稳定的服务入口。

func BuildTarget

func BuildTarget(service *ServiceDNS, config *DNSConfig) (*Target, error)

BuildTarget 是兼容保留的辅助函数。

新代码建议直接使用 DNSManager.Build。

func (Target) Address

func (t Target) Address() string

Address 返回标准的 host:port 地址。

func (Target) GRPCTarget

func (t Target) GRPCTarget() string

GRPCTarget 返回适合 grpc.NewClient 使用的 target 字符串。

func (Target) Validate

func (t Target) Validate() error

Validate 检查 Target 是否可以用于真实拨号。

type UnaryInvokeFunc

type UnaryInvokeFunc func(ctx context.Context, conn *grpc.ClientConn, method string, req any, resp any, options ...grpc.CallOption) error

UnaryInvokeFunc 表示底层 unary 调用函数。

通过把真实调用抽象成函数, 可以让 UnaryInvoker 在测试中替换掉真实的 grpc.ClientConn.Invoke。

type UnaryInvoker

type UnaryInvoker struct {
	// Dialer 负责连接获取与复用。
	Dialer Dialer
	// Authorizer 是可选依赖;若为空,则跳过 Authz 预检查。
	Authorizer Authorizer
	// InvokeFunc 是可选依赖;若为空,则默认调用 grpc.ClientConn.Invoke。
	InvokeFunc UnaryInvokeFunc
}

UnaryInvoker 是默认的 Invoker 实现。

它的执行流程非常明确: 1. 基于 ServiceDNS 获取连接; 2. 基于 InvocationContext 构造统一 metadata; 3. 在真正发起 gRPC 调用前执行 Authz 判定; 4. 使用 grpc.ClientConn.Invoke 发起 unary 调用。

func (*UnaryInvoker) Invoke

func (u *UnaryInvoker) Invoke(ctx context.Context, service *ServiceDNS, method string, req any, resp any, options ...InvokeOption) error

Invoke 执行一次标准 unary 调用。

type UserContextMeta added in v1.3.4

type UserContextMeta struct {
	Session  string `json:"session"`
	ClientIp string `json:"client_ip"`

	UserId   string `json:"user_id"`
	AppId    string `json:"app_id"`
	TenantId string `json:"tenant_id"`

	RoleIds []string `json:"role_ids"`
	OrgIds  []string `json:"org_ids"`
}

func MustUserContextFromContext added in v1.3.4

func MustUserContextFromContext(ctx context.Context) *UserContextMeta

MustUserContextFromContext 从 context 获取 UserContextMeta。

如果 context 中不存在 UserContextMeta,则 panic。 适用于明确知道 context 中一定有 UserContextMeta 的场景。

示例:

func Handler(ctx context.Context) error {
    userMeta := invocation.MustUserContextFromContext(ctx)
    log.Info("user request", "user_id", userMeta.UserId)
    return nil
}

func ParseUserContextMeta added in v1.3.4

func ParseUserContextMeta(md metadata.MD) (raw *UserContextMeta, err error)

ParseUserContextMeta 从 metadata 解析用户上下文元信息。

推荐使用方式:

  1. 在 gRPC server interceptor 中调用 ParseUserContextMeta 解析一次
  2. 使用 WithUserContext 存入 context
  3. 后续使用 UserContextFromContext 直接获取

不推荐在每个 handler 中重复调用此函数。

示例:

// 在 interceptor 中解析一次
md, _ := metadata.FromIncomingContext(ctx)
userMeta, err := invocation.ParseUserContextMeta(md)
if err == nil {
    ctx = invocation.WithUserContext(ctx, userMeta)
}

// 在 handler 中直接获取
userMeta, ok := invocation.UserContextFromContext(ctx)

func UserContextFromContext added in v1.3.4

func UserContextFromContext(ctx context.Context) (*UserContextMeta, bool)

UserContextFromContext 从 context 获取 UserContextMeta。

返回值:

  • meta: UserContextMeta 指针,如果不存在则返回 nil
  • ok: 是否成功获取

示例:

func Handler(ctx context.Context) error {
    userMeta, ok := invocation.UserContextFromContext(ctx)
    if !ok {
        return errors.New("user context not found")
    }
    log.Info("user request", "user_id", userMeta.UserId)
    return nil
}

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL