大摇大摆作为微服务交互的框架





你好昵称!如果您是一名程序员并且使用微服务体系结构,那么可以想象您需要配置服务A与一些新的但仍未知的服务B的交互。您将首先做什么?



如果您向来自不同公司的100位程序员提出此问题,则很可能我们将获得100个不同的答案。有人用粗鲁的方式描述合同,而gRPC中的某人只是在不描述合同的情况下就为其服务提供客户。甚至有人将JSON存储在googleok中:D。大多数公司都基于一些历史因素,能力,技术堆栈等来开发自己的服务间交互方法。我想告诉您,Delivery Club中的服务如何相互通信,以及为什么我们要做出这样的选择。最重要的是,随着时间的推移,我们如何确保文档的相关性。会有很多代码!



你好,我们又见面了!我的名字叫谢尔盖·波波夫(Sergey Popov),我是负责应用程序和Delivery Club网站上餐厅搜索结果的团队负责人,也是Go内部开发协会的活跃成员(我们可能稍后再讨论,但现在不讨论)。



我将立即进行预订,我们将主要讨论用Go编写的服务。尽管我们以不同的方式实现了方法的统一,但我们尚未为PHP服务实现代码生成。



我们最终想要得到的是:



  1. 确保服务合同是最新的。这应该加快新服务的引入,并促进团队之间的沟通。
  2. 提出一种服务之间通过HTTP进行交互的统一方法(目前,我们将不考虑通过队列和事件流进行交互)。
  3. 标准化处理服务合同的方法。
  4. 使用单个合同存储库,以免寻找各种汇合的码头。
  5. 理想情况下,为不同平台生成客户端。


综上所述,Protobuf是描述合同的统一方式。它具有良好的工具,可以为不同的平台生成客户端(我们的第5节)。但这也有明显的缺点:对于许多人来说,gRPC仍然是未知的新事物,这会使它的实现复杂化。另一个重要因素是该公司长期以来一直采用“规范优先”的方法,并且所有服务的文档都已经以草率或RAML描述的形式存在。



摇摆



巧合的是,与此同时,我们开始在公司中改编Go。因此,我们考虑的下一个候选对象是go-swagger-该工具可让您根据swagger规范生成客户端和服务器代码。明显的缺点是它只为Go生成代码。实际上,它使用gosh代码生成,而go-swagger允许灵活使用模板,因此从理论上讲,它可以用于生成PHP代码,但我们尚未尝试过。



忙碌不仅与传输层生成有关。实际上,它生成了应用程序框架,在这里我想简单介绍一下DC中的开发文化。我们拥有内部资源,这意味着来自任何团队的任何开发人员都可以向我们拥有的任何服务创建拉取请求。为了使这种方案起作用,我们尝试使开发中的方法标准化:我们使用通用术语,日志记录,度量,处理依赖关系以及项目结构的单一方法。



因此,通过实施go-swagger,我们引入了在Go中开发我们的服务的标准。这是朝着我们的目标迈出的又一步,这是我们最初没有想到的,但对总体发展而言是重要的。



第一步



因此,举世无双的人原来是一个有趣的候选人,似乎能够满足我们大多数想要的需求。

注意:所有进一步的代码都与0.24.0版本相关,安装说明可以在我们的存储库中通过示例查看,并且官方网站上提供了有关安装当前版本的说明。
让我们看看他能做什么。让我们昂首阔步规范,并生成一个服务:



> goswagger generate server \
    --with-context -f ./swagger-api/swagger.yml \
    --name example1


我们得到了以下内容:







Makefile和go.mod我已经做好了自己的准备。



实际上,我们最终获得了一项服务,该服务处理了大幅度描述的请求。



> go run cmd/example1-server/main.go
2020/02/17 11:04:24 Serving example service at http://127.0.0.1:54586
 
 
 
> curl http://localhost:54586/hello -i
HTTP/1.1 501 Not Implemented
Content-Type: application/json
Date: Sat, 15 Feb 2020 18:14:59 GMT
Content-Length: 58
Connection: close
 
"operation hello HelloWorld has not yet been implemented"


第二步。了解模板



显然,我们生成的代码与我们要在操作中看到的代码相去甚远。



我们希望从应用程序的结构中得到什么:



  • 能够配置应用程序:传输用于连接数据库的设置,指定HTTP连接的端口,等等。
  • 选择一个将存储应用程序状态,数据库连接等的应用程序对象。
  • 使我们的应用程序的处理程序功能,这应该简化代码的工作。
  • 在主文件中初始化依赖关系(在我们的示例中不会发生,但是我们仍然想要它。


为了解决新问题,我们可以覆盖一些模板。为此,我们将像我一样(Github描述以下文件







我们需要描述模板文件(`*.gotmpl`)和用于`*.yml`生成服务的配置(的文件



接下来,按顺序,我们将分析我制作的模板。我不会深入研究它们,因为go-swagger文档非常详细,例如,这里是配置文件描述。我只会注意到使用Go-templating,如果您已经对此有经验或必须描述HELM配置,那么将不难理解。



配置应用



config.gotmpl包含一个带有一个参数的简单结构-应用程序将侦听传入HTTP请求的端口。我还做了一个InitConfig读取环境变量并填充此结构的函数我将从main.go调用它,因此将其设为InitConfig公共函数。



package config
 
import (
    "github.com/pkg/errors"
    "github.com/vrischmann/envconfig"
)
 
// Config struct
type Config struct {
    HTTPBindPort int `envconfig:"default=8001"`
}
 
// InitConfig func
func InitConfig(prefix string) (*Config, error) {
    config := &Config{}
    if err := envconfig.InitWithPrefix(config, prefix); err != nil {
        return nil, errors.Wrap(err, "init config failed")
    }
 
    return config, nil
}


为了在生成代码时使用此模板,必须在YML config中指定它



layout:
  application:
    - name: cfgPackage
      source: serverConfig
      target: "./internal/config/"
      file_name: "config.go"
      skip_exists: false


我会告诉您一些有关参数的信息:



  • name -具有纯信息功能,不影响生成。
  • source-实际上是camelCase中模板文件的路径,即 serverConfig等效于./server/config.gotmpl
  • target-将保存生成的代码的目录。在这里,您可以使用模板来动态生成路径(示例)。
  • file_name -生成文件的名称,在这里您还可以使用模板。
  • skip_exists-该文件将仅生成一次且不会覆盖现有文件的迹象。这对我们很重要,因为配置文件将随着应用程序的增长而变化,并且不应依赖于所生成的代码。


在代码生成配置中,您需要指定所有文件,而不仅仅是指定我们要覆盖的文件。对于文件,我们不改变,所指的source点出来asset:< >,例如,在这里asset:serverConfigureapi顺便说一句,如果您有兴趣查看原始模板,请访问此处



应用程序对象和处理程序



我不会描述用于存储状态,数据库连接和其他内容的应用程序对象,所有内容都与刚刚完成的配置类似。但是有了处理程序,一切都会变得更加有趣。我们的主要目标是当我们向规范中添加URL时,在一个单独的文件中创建一个存根函数,最重要的是,对于我们的服务器而言,调用此函数来处理请求。



让我们描述函数模板和存根:



package app
 
import (
    api{{ pascalize .Package }} "{{.GenCommon.TargetImportPath}}/{{ .RootPackage }}/operations/{{ .Package }}"
    "github.com/go-openapi/runtime/middleware"
)
 
func (srv *Service){{ pascalize .Name }}Handler(params api{{ pascalize .Package }}.{{ pascalize .Name }}Params{{ if .Authorized }}, principal api{{ .Package }}.{{ if not ( eq .Principal "interface{}" ) }}*{{ end }}{{ .Principal }}{{ end }}) middleware.Responder {
    return middleware.NotImplemented("operation {{ .Package }} {{ pascalize .Name }} has not yet been implemented")
}


让我们来看一个例子:



  • pascalize-与CamelCase对齐(此处介绍其他功能)。
  • .RootPackage -生成的Web服务器软件包。
  • .Package-生成代码中的程序包名称,其中描述了HTTP请求和响应的所有必要结构,即 结构。例如,请求主体的结构或响应结构。
  • .Name-处理程序的名称。如果指定,则从规范中operationID获取我建议始终指定operationID一个更明显的结果。


处理程序的配置如下:



layout:
  operations:
    - name: handlerFns
      source: serverHandler
      target: "./internal/app"
      file_name: "{{ (snakize (pascalize .Name)) }}.go"
      skip_exists: true


如您所见,处理程序代码不会被覆盖(skip_exists: true),并且将从处理程序名称生成文件名。



好的,有一个存根函数,但是Web服务器尚不知道应使用这些函数来处理请求。我在main.go中修复了此问题(我不会给出完整的代码,完整的版本可以在此处找到):



package main
 
{{ $name := .Name }}
{{ $operations := .Operations }}
import (
    "fmt"
    "log"
 
    "github.com/delivery-club/go-swagger-example/{{ dasherize .Name }}/internal/generated/restapi"
    "github.com/delivery-club/go-swagger-example/{{ dasherize .Name }}/internal/generated/restapi/operations"
    {{range $index, $op := .Operations}}
        {{ $found := false }}
        {{ range $i, $sop := $operations }}
            {{ if and (gt $i $index ) (eq $op.Package $sop.Package)}}
                {{ $found = true }}
            {{end}}
        {{end}}
        {{ if not $found }}
        api{{ pascalize $op.Package }} "{{$op.GenCommon.TargetImportPath}}/{{ $op.RootPackage }}/operations/{{ $op.Package }}"
        {{end}}
    {{end}}
 
    "github.com/go-openapi/loads"
    "github.com/vrischmann/envconfig"
 
    "github.com/delivery-club/go-swagger-example/{{ dasherize .Name }}/internal/app"
)
 
func main() {
    ...
    api := operations.New{{ pascalize .Name }}API(swaggerSpec)
 
    {{range .Operations}}
    api.{{ pascalize .Package }}{{ pascalize .Name }}Handler = api{{ pascalize .Package }}.{{ pascalize .Name }}HandlerFunc(srv.{{ pascalize .Name }}Handler)
    {{- end}}
    ...
}


在进口中的代码看起来很复杂,但实际上它只是去,模板和结构从细末招摇库。在函数中,main我们只需将生成的函数分配给处理程序。



仍然需要生成指示我们的配置的代码:



> goswagger generate server \
        -f ./swagger-api/swagger.yml \
        -t ./internal/generated -C ./swagger-templates/default-server.yml \
        --template-dir ./swagger-templates/templates \
        --name example2


最终结果可以在我们的存储库中查看



我们得到了:



  • 我们可以将结构用于应用程序,配置以及所需的任何内容。最重要的是,将其嵌入到生成的代码中非常容易。
  • 我们可以灵活地管理项目的结构,直至单个文件的名称。
  • Go模板看起来很复杂,需要一些时间来习惯,但总体而言,它是一个非常强大的工具。


第三步 产生客户



Go-swagger还允许我们为其他Go服务可以使用的服务生成客户端程序包。在这里,我将不再详细介绍代码生成,该方法与生成服务器端代码时完全相同。



对于Go项目,习惯./pkg上将public包放在中,我们将做同样的事情:将我们的服务的客户端放在pkg中,并生成代码本身,如下所示:



> goswagger generate client -f ./swagger-api/swagger.yml -t ./pkg/example3


这里 是生成代码的示例



现在,我们服务的所有使用者都可以自己导入此客户端,例如,通过标签导入(例如,标签将为example3/pkg/example3/v0.0.1)。



可以自定义客户端模板,例如,open tracing id从上下文流到标头。



结论



自然,我们的内部实现与此处显示的代码有所不同,主要是由于使用了内部软件包和CI方法(运行各种测试和测试)。在开箱即用的生成代码中,配置了技术指标收集,使用配置和日志记录。我们已经标准化了所有常用工具。因此,我们简化了总体开发过程,尤其是新服务的发布,确保了在部署到产品之前更快地通过服务清单。



让我们检查我们是否实现了最初的目标:



  1. 确保所描述的服务合同具有相关性,这应该加快新服务的实施并简化团队之间的沟通-
  2. HTTP ( event streaming) — .
  3. , .. Inner Source — .
  4. , — ( — Bitbucket).
  5. , — ( , , ).
  6. Go — ( ).


细心的读者可能已经问过一个问题:模板文件如何进入我们的项目?现在,我们将它们存储在我们的每个项目中。这简化了日常工作,使您可以为特定项目自定义内容。但是,另一方面还有一个问题:没有用于集中更新模板和交付主要与CI有关的新功能的机制。



PS:如果您喜欢这种材料,那么将来我们将准备一篇有关我们服务的标准体系结构的文章,我们将告诉您在Go中开发服务时使用哪些原则。



All Articles