Golang 微服务导出 FileDescriptorSet / Istio gRPC-JSON transcoder envoyFilter 持续集成

Golang 微服务导出 FileDescriptorSet / Istio gRPC-JSON transcoder envoyFilter 持续集成

大家好,我是 Avtion,目前在一家互联网公司任职 Golang 开发工程师。

一般情况下,我们开发的 Golang gRPC 服务无法直接提供 HTTP 请求能力给前端使用 JSON 调用。

我们可以通过 Google 提供 annotations 注解的方式 使用 Protobuf 直接定义 HTTP 接口,但依旧无法直接由 gRPC 服务提供 HTTP 能力。

少量微服务需要提供 HTTP 调用能力,可以考虑将 grpc-ecosystem/grpc-gateway 开源库集成在基础开发框架中。

在使用 Istio 开源网关下,我们可以通过 sidecar 的能力让 gRPC 服务通过 JSON 调用,详细可见 gRPC-JSON transcoder

在 gRPC-JSON transcoder 文档的示例中,主要使用protoc 编译 Protobuf 的 FileDescriptorSet 进行 Base64 编码后写在 EnvoyFilter 中,步骤过于繁琐,故考虑自动化持续集成方案。

本文主要说明如何在 gRPC 服务直接输出 FileDescriptorSet 以便后续的持续集成,完整代码参考。

package example

import (
  "os"

  "google.golang.org/genproto/googleapis/api/annotations"
  "google.golang.org/protobuf/proto"
  "google.golang.org/protobuf/reflect/protodesc"
  "google.golang.org/protobuf/reflect/protoreflect"
  "google.golang.org/protobuf/reflect/protoregistry"
  "google.golang.org/protobuf/types/descriptorpb"
)

func exportFileDescriptorSet(output string) {
    
    
  var protoFileNum = protoregistry.GlobalFiles.NumFiles()
  fileDescProtoSet := make(map[string]*descriptorpb.FileDescriptorProto, protoFileNum)
  protoNames := make([]string, 0, protoFileNum)
  protoregistry.GlobalFiles.RangeFiles(func(desc protoreflect.FileDescriptor) bool {
    
    
    // transform to FileDescriptorProto
    descProto := protodesc.ToFileDescriptorProto(desc)

    // filter all non http service
    if !hasAnnotateHTTPRule(descProto) {
    
    
      return true
    }

    protoNames = append(protoNames, descProto.GetName())
    fileDescProtoSet[descProto.GetName()] = descProto

    return true
  })

  for _, descProto := range fileDescProtoSet {
    
    
    addImport(fileDescProtoSet, descProto)
  }

  // build desc proto set
  protoset := &descriptorpb.FileDescriptorSet{
    
    File: topoSort(protoNames, fileDescProtoSet) /*very important*/}
  data, err := proto.Marshal(protoset)
  if err != nil {
    
    
    return
  }

  // 0755 make sure istio-proxy can access it
  if err := os.WriteFile(output, data, 0755); err != nil {
    
    
    return
  }
}

// filter non http service
func hasAnnotateHTTPRule(descProto *descriptorpb.FileDescriptorProto) bool {
    
    
  for _, service := range descProto.GetService() {
    
    
    for _, method := range service.GetMethod() {
    
    
      if proto.HasExtension(method.GetOptions(), annotations.E_Http) {
    
    
        return true
      }
    }
  }
  return false
}

// after filter useless proto, it should reimport dependency proto
func addImport(set map[string]*descriptorpb.FileDescriptorProto, desc *descriptorpb.FileDescriptorProto) {
    
    
  for _, depProtoName := range desc.GetDependency() {
    
    
    if _, isExist := set[depProtoName]; isExist {
    
    
      continue
    }
    depDescriptor, err := protoregistry.GlobalFiles.FindFileByPath(depProtoName)
    if err != nil {
    
    
      continue
    }
    descProto := protodesc.ToFileDescriptorProto(depDescriptor.ParentFile())
    // make sure all import proto has been added
    addImport(set, descProto)
    set[depProtoName] = descProto
  }
}

// topoSort sorts FileDesciptorProtos such that imported files (dependencies) come first.
// topoSort moves FileDescriptors from input map `files` to topographically sorted slice in return value.
func topoSort(names []string, files map[string]*descriptorpb.FileDescriptorProto) []*descriptorpb.FileDescriptorProto {
    
    
  var result []*descriptorpb.FileDescriptorProto
  for _, name := range names {
    
    
    if file := files[name]; file != nil {
    
    
      result = append(result, topoSort(file.Dependency, files)...)
      result = append(result, file)
      delete(files, name)
    }
  }
  return result
}

关键流程

  1. 从 protoregistry.GlobalFiles 中读取所有 import 到服务的 protoFile。
  2. 调用 protodesc.ToFileDescriptorProto 方法将反射的 FileDescriptor 转换成描述符 FileDescriptorProto。
  3. 过滤掉没有 google.api.HttpRule 选项的 service。
  4. 根据过滤后剩余的 service 导入依赖项。
  5. 重新排序所有 FileDescriptorProto 描述符,按照依赖项在前的顺序整理为 FileDescriptorSet 描述符集合。
  6. 使用 proto.Marshal 方法序列化 FileDescriptorSet 描述符集合后写入目的文件即可。

参考资料

猜你喜欢

转载自blog.csdn.net/yes169yes123/article/details/125002771