Bonjour NickName! Si vous ĂȘtes programmeur et travaillez avec une architecture de microservices, imaginez que vous devez configurer l'interaction de votre service A avec un nouveau service B encore inconnu. Que ferez-vous en premier?
Si vous posez cette question Ă 100 programmeurs d'entreprises diffĂ©rentes, nous obtiendrons probablement 100 rĂ©ponses diffĂ©rentes. Quelqu'un dĂ©crit les contrats avec fanfaron, quelqu'un dans gRPC fait simplement des clients Ă leurs services sans dĂ©crire un contrat. Et quelqu'un stocke mĂȘme JSON dans un googleok: D. La plupart des entreprises dĂ©veloppent leur propre approche de l'interaction interservices en fonction de certains facteurs historiques, compĂ©tences, pile technologique, etc. Je veux vous dire comment les services de Delivery Club communiquent entre eux et pourquoi nous avons fait un tel choix. Et surtout, comment nous assurons la pertinence de la documentation dans le temps. Il y aura beaucoup de code!
Re-bonjour! Je m'appelle Sergey Popov, je suis le chef d'Ă©quipe de l'Ă©quipe responsable des rĂ©sultats de recherche des restaurants dans les applications et sur le site du Delivery Club, et Ă©galement un membre actif de notre guilde de dĂ©veloppement interne Ă Go (nous en parlerons peut-ĂȘtre plus tard, mais pas maintenant).
Je vais faire une réservation tout de suite, nous parlerons principalement des services écrits en Go. Nous n'avons pas encore implémenté la génération de code pour les services PHP, bien que nous y parvenions d'une maniÚre différente dans les approches.
Ce que nous voulions aboutir:
- Assurez-vous que les contrats de service sont à jour. Cela devrait accélérer l'introduction de nouveaux services et faciliter la communication entre les équipes.
- Arrivez à une méthode unifiée d'interaction via HTTP entre les services (nous ne considérerons pas les interactions via les files d'attente et le streaming d'événements pour le moment).
- Standardiser l'approche du travail avec les contrats de service.
- Utilisez un référentiel unique de contrats afin de ne pas chercher des quais pour toutes sortes de confluences.
- Idéalement, générez des clients pour différentes plates-formes.
De tout ce qui prĂ©cĂšde, Protobuf vient Ă l'esprit comme une maniĂšre unifiĂ©e de dĂ©crire les contrats. Il dispose de bons outils et peut gĂ©nĂ©rer des clients pour diffĂ©rentes plateformes (notre clause 5). Mais il y a aussi des inconvĂ©nients Ă©vidents: pour beaucoup, gRPC reste quelque chose de nouveau et d'inconnu, ce qui compliquerait grandement sa mise en Ćuvre. Un autre facteur important Ă©tait que la sociĂ©tĂ© avait depuis longtemps adoptĂ© l'approche «la spĂ©cification d'abord» et que la documentation existait dĂ©jĂ pour tous les services sous la forme d'un swagger ou d'une description RAML.
Go-swagger
Par coĂŻncidence, en mĂȘme temps, nous avons commencĂ© Ă adapter Go dans l'entreprise. Par consĂ©quent, notre prochain candidat Ă considĂ©rer Ă©tait go-swagger - un outil qui vous permet de gĂ©nĂ©rer des clients et du code serveur Ă partir de la spĂ©cification swagger. L'inconvĂ©nient Ă©vident est qu'il gĂ©nĂšre uniquement du code pour Go. En fait, il utilise la gĂ©nĂ©ration de code gosh, et go-swagger permet un travail flexible avec des modĂšles, donc en thĂ©orie, il peut ĂȘtre utilisĂ© pour gĂ©nĂ©rer du code PHP, mais nous ne l'avons pas encore essayĂ©.
Go-swagger ne concerne pas uniquement la génération de couches de transport. En fait, il génÚre le squelette de l'application, et ici je voudrais parler un peu de la culture du développement dans DC. Nous avons Inner Source, ce qui signifie que tout développeur de n'importe quelle équipe peut créer une pull request vers n'importe quel service que nous avons. Pour qu'un tel schéma fonctionne, nous essayons de standardiser les approches de développement: nous utilisons une terminologie commune, une approche unique de la journalisation, des métriques, du travail avec les dépendances et, bien sûr, de la structure du projet.
Ainsi, en implémentant go-swagger, nous introduisons un standard pour le développement de nos services en Go. C'est un autre pas vers nos objectifs, auquel nous ne nous attendions pas au départ, mais qui est important pour le développement en général.
Les premiers pas
Donc, go-swagger s'est avĂ©rĂ© ĂȘtre un candidat intĂ©ressant qui semble ĂȘtre en mesure de couvrir la plupart de nos besoins
Remarque: tous les autres codes sont pertinents pour la version 0.24.0, les instructions d'installation peuvent ĂȘtre consultĂ©es dans notre rĂ©fĂ©rentiel avec des exemples , et le site officiel contient des instructions pour installer la version actuelle.Voyons ce qu'il peut faire. Prenons une spĂ©cification swagger et gĂ©nĂ©rons un service:
> goswagger generate server \
--with-context -f ./swagger-api/swagger.yml \
--name example1
Nous avons obtenu ce qui suit:
Makefile et go.mod que j'ai dĂ©jĂ crĂ©Ă©s moi-mĂȘme.
En fait, nous nous sommes retrouvĂ©s avec un service qui traite les requĂȘtes dĂ©crites dans swagger.
> 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"
DeuxiÚme étape. Comprendre la création de modÚles
Evidemment, le code que nous avons généré est loin de ce que nous voulons voir en fonctionnement.
Ce que nous attendons de la structure de notre application:
- Ătre capable de configurer l'application: transfĂ©rez les paramĂštres de connexion Ă la base de donnĂ©es, spĂ©cifiez le port des connexions HTTP, etc.
- Sélectionnez un objet d'application qui stockera l'état de l'application, la connexion à la base de données, etc.
- Rendre les fonctions des gestionnaires de notre application, cela devrait simplifier le travail avec le code.
- Initialisez les dépendances dans le fichier principal (dans notre exemple, cela ne se produira pas, mais nous voulons toujours cela.
Pour résoudre de nouveaux problÚmes, nous pouvons remplacer certains modÚles. Pour ce faire, nous allons décrire les fichiers suivants, comme je l'ai fait ( Github ):
Nous devons décrire les fichiers de modÚle (
`*.gotmpl`
) et le fichier de configuration ( `*.yml`
) de génération de notre service.
Ensuite, dans l'ordre, nous analyserons les modÚles que j'ai créés. Je ne vais pas approfondir leur travail avec eux, car la documentation de go-swagger est assez détaillée, par exemple, voici la description du fichier de configuration. Je noterai seulement que Go-templating est utilisé, et si vous avez déjà de l'expérience avec cela ou avez dû décrire des configurations HELM, il ne sera pas difficile de le comprendre.
Configuration de l'application
config.gotmpl contient une structure simple avec un paramĂštre - le port que l'application Ă©coutera pour les requĂȘtes HTTP entrantes. J'ai Ă©galement crĂ©Ă© une fonction
InitConfig
qui lira les variables d'environnement et remplira cette structure. Je l'appellerai depuis main.go, donc j'en ai InitConfig
fait une fonction publique.
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
}
Pour que ce modÚle soit utilisé lors de la génération de code, vous devez le spécifier dans la configuration YML :
layout:
application:
- name: cfgPackage
source: serverConfig
target: "./internal/config/"
file_name: "config.go"
skip_exists: false
Je vais vous parler un peu des paramĂštres:
name
- a une fonction purement informative et n'affecte pas la génération.source
- en fait le chemin vers le fichier modĂšle dans camelCase, c'est-Ă -dire serverConfig Ă©quivaut Ă ./server/config.gotmpl .target
- répertoire dans lequel le code généré sera sauvegardé. Ici, vous pouvez utiliser la création de modÚles pour générer dynamiquement un chemin ( exemple ).file_name
- le nom du fichier généré, ici vous pouvez également utiliser la création de modÚles.skip_exists
- un signe que le fichier ne sera généré qu'une seule fois et n'écrasera pas l'existant. Ceci est important pour nous, car le fichier de configuration changera à mesure que l'application se développera et ne devrait pas dépendre du code généré.
Dans la configuration de génération de code, vous devez spécifier tous les fichiers, et pas seulement ceux que nous voulons remplacer. Pour les fichiers que nous ne changeons pas, au sens du
source
point de sortie asset:< >
, par exemple, ici : asset:serverConfigureapi
. Ă propos, si vous souhaitez consulter les modĂšles originaux, ils sont ici .
Objet d'application et gestionnaires
Je ne décrirai pas l'objet d'application pour stocker l'état, les connexions à la base de données et autres choses, tout est similaire à la configuration que vous venez de créer. Mais avec les gestionnaires, tout est un peu plus intéressant. Notre objectif principal est de créer une fonction de stub dans un fichier séparé lorsque nous ajoutons une URL à la spécification, et surtout, que notre serveur appelle cette fonction pour traiter la demande.
DĂ©crivons le modĂšle de fonction et les 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")
}
Regardons un peu un exemple:
pascalize
- apporte une ligne avec CamelCase (description des autres fonctions ici )..RootPackage
- package de serveur Web généré..Package
- le nom du package dans le code gĂ©nĂ©rĂ©, qui dĂ©crit toutes les structures nĂ©cessaires pour les requĂȘtes et rĂ©ponses HTTP, i.e. structures. Par exemple, une structure pour le corps de la requĂȘte ou une structure de rĂ©ponse..Name
- le nom du gestionnaire. Il est extrait de l' ID d'opération dans la spécification, s'il est spécifié. Je recommande de toujours spécifieroperationID
pour un résultat plus évident.
La configuration du gestionnaire est la suivante:
layout:
operations:
- name: handlerFns
source: serverHandler
target: "./internal/app"
file_name: "{{ (snakize (pascalize .Name)) }}.go"
skip_exists: true
Comme vous pouvez le voir, le code du gestionnaire ne sera pas écrasé (
skip_exists: true
) et le nom du fichier sera généré à partir du nom du gestionnaire.
D'accord, il y a une fonction stub, mais le serveur Web ne sait pas encore que ces fonctions devraient ĂȘtre utilisĂ©es pour traiter les demandes. J'ai corrigĂ© cela dans main.go (je ne donnerai pas le code complet, la version complĂšte peut ĂȘtre trouvĂ©e ici ):
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}}
...
}
Le code de l'importation semble compliquĂ©, mĂȘme s'il ne s'agit en rĂ©alitĂ© que de modĂšles Go et de structures du rĂ©fĂ©rentiel go-swagger. Et dans une fonction,
main
nous affectons simplement nos fonctions générées aux gestionnaires.
Il reste à générer le code indiquant notre configuration:
> goswagger generate server \
-f ./swagger-api/swagger.yml \
-t ./internal/generated -C ./swagger-templates/default-server.yml \
--template-dir ./swagger-templates/templates \
--name example2
Le rĂ©sultat final peut ĂȘtre consultĂ© dans notre rĂ©fĂ©rentiel .
Ce que nous avons:
- Nous pouvons utiliser nos structures pour l'application, les configurations et tout ce que nous voulons. Plus important encore, il est assez facile de l'intégrer dans le code généré.
- Nous pouvons gérer de maniÚre flexible la structure du projet, jusqu'aux noms des fichiers individuels.
- La création de modÚles semble complexe et nécessite un certain temps pour s'y habituer, mais dans l'ensemble, c'est un outil trÚs puissant.
TroisiÚme étape. Générer des clients
Go-swagger nous permet Ă©galement de gĂ©nĂ©rer un package client pour notre service que d'autres services Go peuvent utiliser. Ici, je ne m'attarderai pas sur la gĂ©nĂ©ration de code en dĂ©tail, l'approche est exactement la mĂȘme que lors de la gĂ©nĂ©ration de code cĂŽtĂ© serveur.
Pour les projets Go, il est d'usage de mettre des packages publics
./pkg
, nous ferons la mĂȘme chose: mettre le client de notre service dans pkg, et gĂ©nĂ©rer le code lui-mĂȘme comme suit:
> goswagger generate client -f ./swagger-api/swagger.yml -t ./pkg/example3
Un exemple du code généré est ici .
DĂ©sormais, tous les consommateurs de notre service peuvent importer ce client pour eux-mĂȘmes, par exemple, par tag (pour mon exemple, le tag sera
example3/pkg/example3/v0.0.1
).
Les modĂšles clients peuvent ĂȘtre personnalisĂ©s pour, par exemple, passer
open tracing id
du contexte Ă l'en-tĂȘte.
conclusions
Naturellement, notre implĂ©mentation interne diffĂšre du code prĂ©sentĂ© ici principalement en raison de l'utilisation de packages internes et d'approches de CI (exĂ©cution de divers tests et linters). Dans le code gĂ©nĂ©rĂ© prĂȘt Ă l'emploi, la collecte de mĂ©triques techniques, le travail avec les configurations et la journalisation sont configurĂ©s. Nous avons standardisĂ© tous les outils courants. De ce fait, nous avons simplifiĂ© le dĂ©veloppement en gĂ©nĂ©ral et la sortie de nouveaux services en particulier, assurĂ© un passage plus rapide de la liste de contrĂŽle des services avant le dĂ©ploiement sur le produit.
Vérifions si les objectifs d'origine ont été atteints:
- Assurer la pertinence des contrats décrits pour les services, cela devrait accélérer l'introduction de nouveaux services et simplifier la communication entre les équipes - Oui .
- HTTP ( event streaming) â .
- , .. Inner Source â .
- , â ( â Bitbucket).
- , â ( , , ).
- Go â ( ).
Le lecteur attentif a probablement déjà posé la question: comment les fichiers modÚles entrent-ils dans notre projet? Nous les stockons désormais dans chacun de nos projets. Cela simplifie le travail quotidien, vous permet de personnaliser quelque chose pour un projet spécifique. Mais il y a un autre aspect de la médaille: il n'y a pas de mécanisme de mise à jour centralisée des modÚles et de livraison de nouvelles fonctionnalités, principalement liées à l'IC.
PS Si vous aimez ce matériel, nous préparerons à l'avenir un article sur l'architecture standard de nos services, nous vous indiquerons les principes que nous utilisons lors du développement de services dans Go.