Go-Swagger als Rahmen für die Interaktion mit Microservices





Hallo Spitzname! Wenn Sie Programmierer sind und mit einer Microservice-Architektur arbeiten, stellen Sie sich vor, Sie müssen die Interaktion Ihres Dienstes A mit einem neuen und noch unbekannten Dienst B konfigurieren. Was werden Sie zuerst tun?



Wenn Sie diese Frage 100 Programmierern aus verschiedenen Unternehmen stellen, erhalten wir höchstwahrscheinlich 100 verschiedene Antworten. Jemand beschreibt Verträge in Prahlerei, jemand in gRPC macht Kunden einfach zu ihren Diensten, ohne einen Vertrag zu beschreiben. Und jemand speichert JSON sogar in einem Googleok: D. Die meisten Unternehmen entwickeln ihren eigenen Ansatz für die dienststellenübergreifende Interaktion auf der Grundlage einiger historischer Faktoren, Kompetenzen, Technologiepakete usw. Ich möchte Ihnen sagen, wie die Dienste im Delivery Club miteinander kommunizieren und warum wir eine solche Entscheidung getroffen haben. Und vor allem, wie wir die Relevanz der Dokumentation im Laufe der Zeit sicherstellen. Es wird viel Code geben!



Hallo nochmal! Mein Name ist Sergey Popov, ich bin der Teamleiter des Teams, das für die Suchergebnisse von Restaurants in den Apps und auf der Delivery Club-Website verantwortlich ist, und auch ein aktives Mitglied unserer internen Entwicklungsgilde für Go (wir können später darüber sprechen, aber nicht jetzt).



Ich werde sofort eine Reservierung vornehmen, wir werden hauptsächlich über Dienstleistungen sprechen, die in Go geschrieben wurden. Wir haben die Codegenerierung für PHP-Dienste noch nicht implementiert, obwohl wir dort auf andere Weise einheitliche Ansätze erreichen.



Was wir am Ende haben wollten:



  1. Stellen Sie sicher, dass die Serviceverträge auf dem neuesten Stand sind. Dies sollte die Einführung neuer Dienste beschleunigen und die Kommunikation zwischen den Teams erleichtern.
  2. Kommen Sie zu einer einheitlichen Methode der Interaktion über HTTP zwischen Diensten (wir werden Interaktionen über Warteschlangen und Ereignis-Streaming vorerst nicht berücksichtigen).
  3. Standardisierung des Ansatzes für die Arbeit mit Serviceverträgen.
  4. Verwenden Sie ein einziges Repository von Verträgen, um nicht nach Docks für alle Arten von Zusammenflüssen zu suchen.
  5. Generieren Sie im Idealfall Clients für verschiedene Plattformen.


Aus all diesen Gründen wird Protobuf als einheitliche Methode zur Beschreibung von Verträgen in den Sinn kommen. Es verfügt über gute Tools und kann Clients für verschiedene Plattformen generieren (unsere Klausel 5). Es gibt aber auch offensichtliche Nachteile: Für viele bleibt gRPC etwas Neues und Unbekanntes, was die Implementierung erheblich erschweren würde. Ein weiterer wichtiger Faktor war, dass das Unternehmen seit langem den Ansatz „Spezifikation zuerst“ gewählt hatte und die Dokumentation für alle Dienste bereits in Form einer Prahlerei oder einer RAML-Beschreibung vorhanden war.



Go-Swagger



Zufälligerweise haben wir gleichzeitig begonnen, Go in der Firma anzupassen. Daher war unser nächster Kandidat für die Prüfung go-swagger - ein Tool, mit dem Sie Clients und Servercode aus der Swagger-Spezifikation generieren können. Der offensichtliche Nachteil ist, dass nur Code für Go generiert wird. Tatsächlich wird die Generierung von Gosh-Code verwendet, und Go-Swagger ermöglicht das flexible Arbeiten mit Vorlagen. Theoretisch kann damit PHP-Code generiert werden, aber wir haben es noch nicht ausprobiert.



Bei Go-Swagger geht es nicht nur um die Erzeugung von Transportschichten. Tatsächlich generiert es das Anwendungsskelett, und hier möchte ich ein wenig über die Entwicklungskultur in DC erwähnen. Wir haben eine innere Quelle, was bedeutet, dass jeder Entwickler aus jedem Team eine Pull-Anfrage für jeden Service erstellen kann, den wir haben. Damit dieses Schema funktioniert, versuchen wir, Ansätze in der Entwicklung zu standardisieren: Wir verwenden eine gemeinsame Terminologie, einen einzigen Ansatz für die Protokollierung, Metriken, das Arbeiten mit Abhängigkeiten und natürlich die Projektstruktur.



Mit der Implementierung von go-swagger führen wir daher einen Standard für die Entwicklung unserer Dienste in Go ein. Dies ist ein weiterer Schritt in Richtung unserer Ziele, den wir zunächst nicht erwartet hatten, der aber für die Entwicklung im Allgemeinen wichtig ist.



Die ersten Schritte



Go-Swagger erwies sich als interessanter Kandidat, der die meisten unserer gewünschten Anforderungen zu erfüllen scheint .

Hinweis: Der gesamte weitere Code ist für Version 0.24.0 relevant. Die Installationsanweisungen finden Sie in unserem Repository mit Beispielen . Auf der offiziellen Website finden Sie Anweisungen zur Installation der aktuellen Version.
Mal sehen, was er kann. Werfen wir einen Prahlerei spec und erzeugen einen Service:



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


Wir haben folgendes bekommen:







Makefile und go.mod habe ich mir schon gemacht.



Tatsächlich haben wir einen Service erhalten, der die in swagger beschriebenen Anforderungen verarbeitet.



> 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"


Schritt zwei. Vorlagen verstehen



Offensichtlich ist der Code, den wir generiert haben, weit von dem entfernt, was wir im Betrieb sehen wollen.



Was wir von der Struktur unserer Anwendung erwarten:



  • Sie können die Anwendung konfigurieren: Übertragen Sie die Einstellungen für die Verbindung zur Datenbank, geben Sie den Port der HTTP-Verbindungen an usw.
  • Wählen Sie ein Anwendungsobjekt aus, in dem der Anwendungsstatus, die Datenbankverbindung usw. gespeichert werden.
  • Machen Sie die Handler-Funktionen unserer Anwendung, dies sollte die Arbeit mit dem Code vereinfachen.
  • Initialisieren Sie Abhängigkeiten in der Hauptdatei (in unserem Beispiel wird dies nicht passieren, aber wir möchten dies trotzdem.


Um neue Probleme zu lösen, können wir einige Vorlagen überschreiben. Dazu beschreiben wir wie ich die folgenden Dateien ( Github ):







Wir müssen die Vorlagendateien ( `*.gotmpl`) und die Datei für die Konfiguration ( `*.yml`) zur Generierung unseres Dienstes beschreiben.



Als nächstes analysieren wir der Reihe nach die Vorlagen, die ich erstellt habe. Ich werde nicht tief in die Arbeit mit ihnen eintauchen, da die Go-Swagger-Dokumentation sehr detailliert ist, zum Beispiel hier die Beschreibung der Konfigurationsdatei. Ich werde nur bemerken, dass Go-Templating verwendet wird, und wenn Sie bereits Erfahrung damit haben oder HELM-Konfigurationen beschreiben mussten, wird es nicht schwierig sein, dies herauszufinden.



Anwendung konfigurieren



config.gotmpl enthält eine einfache Struktur mit einem Parameter - dem Port, den die Anwendung auf eingehende HTTP-Anforderungen überwacht . Ich habe auch eine Funktion erstellt InitConfig, die die Umgebungsvariablen liest und diese Struktur füllt. Ich werde es von main.go aus aufrufen, also habe ich InitConfiges zu einer öffentlichen Funktion gemacht.



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
}


Damit diese Vorlage beim Generieren von Code verwendet werden kann, muss sie in der YML-Konfiguration angegeben werden :



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


Ich werde Ihnen ein wenig über die Parameter erzählen:



  • name - hat eine rein informative Funktion und hat keinen Einfluss auf die Erzeugung.
  • source- tatsächlich der Pfad zur Vorlagendatei in camelCase, d. h. serverConfig entspricht ./server/config.gotmpl .
  • target- Verzeichnis, in dem der generierte Code gespeichert wird. Hier können Sie mithilfe von Vorlagen einen Pfad dynamisch generieren ( Beispiel ).
  • file_name - Der Name der generierten Datei, hier können Sie auch Vorlagen verwenden.
  • skip_exists- ein Zeichen dafür, dass die Datei nur einmal generiert wird und die vorhandene nicht überschreibt. Dies ist für uns wichtig, da sich die Konfigurationsdatei mit dem Wachstum der Anwendung ändert und nicht vom generierten Code abhängen sollte.


In der Codegenerierungskonfiguration müssen Sie alle Dateien angeben und nicht nur diejenigen, die wir überschreiben möchten. Für Dateien, die wir nicht ändern, im Sinne von sourcezeigen Sie asset:< >zum Beispiel hier : asset:serverConfigureapi. Übrigens, wenn Sie sich die Originalvorlagen ansehen möchten, sind sie hier .



Anwendungsobjekt und Handler



Ich werde das Anwendungsobjekt zum Speichern des Status, der Datenbankverbindungen und anderer Dinge nicht beschreiben, alles ähnelt der gerade erstellten Konfiguration. Aber mit Handlern ist alles etwas interessanter. Unser Hauptziel ist es, eine Stub-Funktion in einer separaten Datei zu erstellen, wenn wir der Spezifikation eine URL hinzufügen, und vor allem, dass unser Server diese Funktion aufruft, um die Anforderung zu verarbeiten.



Beschreiben wir die Funktionsvorlage und die Stubs:



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")
}


Schauen wir uns ein Beispiel an:



  • pascalize- bringt eine Zeile mit CamelCase (Beschreibung anderer Funktionen hier ).
  • .RootPackage - generiertes Webserver-Paket.
  • .Package- den Namen des Pakets im generierten Code, der alle erforderlichen Strukturen für HTTP-Anforderungen und -Antworten beschreibt, d. H. Strukturen. Zum Beispiel eine Struktur für den Anforderungshauptteil oder eine Antwortstruktur.
  • .Name- der Name des Handlers. Es wird aus der operationID in der Spezifikation entnommen , falls angegeben. Ich empfehle, immer anzugeben, operationIDum ein offensichtlicheres Ergebnis zu erzielen.


Die Konfiguration für den Handler lautet wie folgt:



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


Wie Sie sehen können, wird der Handlercode nicht überschrieben ( skip_exists: true) und der Dateiname wird aus dem Handlernamen generiert.



Okay, es gibt eine Stub-Funktion, aber der Webserver weiß noch nicht, dass diese Funktionen zum Verarbeiten von Anforderungen verwendet werden sollen. Ich habe dies in main.go behoben (ich werde nicht den gesamten Code angeben, die Vollversion finden Sie hier ):



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}}
    ...
}


Der Code im Import sieht kompliziert aus, obwohl es sich in Wirklichkeit nur um Go-Templating und Strukturen aus dem Go-Swagger-Repository handelt. Und in einer Funktion mainweisen wir unsere generierten Funktionen nur den Handlern zu.



Es bleibt der Code zu generieren, der unsere Konfiguration angibt:



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


Das Endergebnis kann in unserem Repository eingesehen werden .



Was wir haben:



  • Wir können unsere Strukturen für die Anwendung, Konfigurationen und was auch immer wir wollen verwenden. Am wichtigsten ist, dass es ziemlich einfach ist, in den generierten Code einzubetten.
  • Wir können die Struktur des Projekts bis hin zu den Namen der einzelnen Dateien flexibel verwalten.
  • Go Templating sieht kompliziert aus und ist gewöhnungsbedürftig, aber insgesamt ist es ein sehr mächtiges Werkzeug.


Schritt drei. Clients generieren



Mit Go-swagger können wir auch ein Client-Paket für unseren Service generieren, das andere Go-Services verwenden können. Hier werde ich nicht im Detail auf die Codegenerierung eingehen, der Ansatz ist genau der gleiche wie beim Generieren von serverseitigem Code.



Bei Go-Projekten ist es üblich, öffentliche Pakete einzufügen ./pkg. Wir gehen genauso vor: Setzen Sie den Client für unseren Service in pkg und generieren Sie den Code selbst wie folgt:



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


Ein Beispiel für den generierten Code finden Sie hier .



Jetzt können alle Verbraucher unseres Dienstes diesen Client für sich selbst importieren, beispielsweise per Tag (in meinem Beispiel ist das Tag example3/pkg/example3/v0.0.1).



Client-Vorlagen können so angepasst werden, dass sie beispielsweise open tracing idvom Kontext zum Header fließen .



Schlussfolgerungen



Natürlich unterscheidet sich unsere interne Implementierung von dem hier gezeigten Code hauptsächlich durch die Verwendung interner Pakete und Ansätze für CI (Ausführen verschiedener Tests und Linters). Im sofort generierten Code werden die Erfassung technischer Metriken, die Arbeit mit Konfigurationen und die Protokollierung konfiguriert. Wir haben alle gängigen Werkzeuge standardisiert. Aus diesem Grund haben wir die Entwicklung im Allgemeinen und die Veröffentlichung neuer Services im Besonderen vereinfacht und einen schnelleren Durchlauf der Service-Checkliste vor der Bereitstellung auf dem Produkt sichergestellt.



Lassen Sie uns überprüfen, ob wir unsere ursprünglichen Ziele erreicht haben:



  1. Stellen Sie sicher, dass die für Services beschriebenen Verträge relevant sind. Dies sollte die Einführung neuer Services beschleunigen und die Kommunikation zwischen den Teams vereinfachen - Ja .
  2. HTTP ( event streaming) — .
  3. , .. Inner Source — .
  4. , — ( — Bitbucket).
  5. , — ( , , ).
  6. Go — ( ).


Der aufmerksame Leser hat wahrscheinlich schon die Frage gestellt: Wie kommen Vorlagendateien in unser Projekt? Wir speichern sie jetzt in jedem unserer Projekte. Dies vereinfacht die tägliche Arbeit und ermöglicht es Ihnen, etwas für ein bestimmtes Projekt anzupassen. Aber es gibt noch eine andere Seite der Medaille: Es gibt keinen Mechanismus für die zentrale Aktualisierung von Vorlagen und die Bereitstellung neuer Funktionen, die hauptsächlich mit CI zusammenhängen.



PS Wenn Ihnen dieses Material gefällt, werden wir in Zukunft einen Artikel über die Standardarchitektur unserer Services erstellen und Ihnen mitteilen, welche Prinzipien wir bei der Entwicklung von Services in Go verwenden.



All Articles