En esta sección, el uso de gRPC es un poco fácil. Para obtener más información, consulte: 4 modos de interacción de datos entre el Cliente y el Servidor en gRPC
Prólogo
Resumen de la serie
El "Tutorial de microservicios de Golang" se divide en 10 artículos, que resumen el proceso completo de desarrollo, prueba e implementación de microservicios.
Esta sección presenta primero los conceptos básicos y la terminología de los microservicios, y luego crea nuestra primera versión concisa del servicio de consignación de microservicios. En las siguientes secciones 2 ~ 10, crearemos los siguientes microservicios uno tras otro:
- servicio de consignación (servicio de carga)
- servicio de inventario (servicio de almacén)
- servicio de usuario
- servicio de autenticación
- servicio de roles
- servicio de buque (servicio de buque de carga)
La pila tecnológica completa utilizada es la siguiente:
Golang, gRPC, go-micro // 开发语言及其 RPC 框架
Google Cloud, MongoDB // 云平台与数据存储
Docker, Kubernetes, Terrafrom // 容器化与集群架构
NATS, CircleCI // 消息系统与持续集成
Almacén de código
Código de autor: EwanValentine / shippy , código de anotación chino del traductor: wuYin / shippy
Cada capítulo corresponde a una sucursal del almacén, por ejemplo, el código de la parte1 en este artículo está en la función / parte1
Entorno de desarrollo
El entorno de desarrollo del autor es macOS, este artículo utiliza la herramienta de creación para compilar de manera eficiente, los usuarios de Windows deben instalar manualmente
$ go env
GOARCH="amd64" # macOS 环境
GOOS="darwin" # 在第二节使用 Docker 构建 alpine 镜像时需修改为 linux
GOPATH="/Users/wuyin/Go"
GOROOT="/usr/local/go"
Listo
Domina la gramática básica de Golang: se recomienda leer "Go Web Programming" de Xie Da
Instalar gRPC / protobuf
go get -u google.golang.org/grpc # 安装 gRPC 框架
go get -u github.com/golang/protobuf/protoc-gen-go # 安装 Go 版本的 protobuf 编译器
Microservicios
¿Qué proyecto escribiremos?
Queremos construir una plataforma de gestión de carga para el puerto. Este proyecto se desarrolla con una arquitectura de microservicios, que es simple en general y de concepto universal. Sin más preámbulos, comencemos nuestro viaje de microservicios.
¿Qué son los microservicios?
En el desarrollo de software tradicional, el código de toda la aplicación se organiza en una sola base de código, generalmente en la forma del siguiente código dividido:
- Dividir según las características: como el patrón MVC
- Dividir según la función: en proyectos más grandes, el código puede encapsularse en paquetes que manejan diferentes negocios, y el paquete puede dividirse nuevamente
No importa cómo se divida, al final, el código de los dos se concentrará en una biblioteca para el desarrollo y la administración, puede consultar: Administración de la biblioteca de código único de Google
Los microservicios son una extensión del segundo método de división descrito anteriormente. El código se divide en varios paquetes según la función, todos los cuales son bibliotecas de código único que se pueden ejecutar de forma independiente. Las diferencias son las siguientes:
¿Cuáles son las ventajas de los microservicios?
Reducir la complejidad
El código de toda la aplicación se divide en bases de código de microservicio pequeñas e independientes de acuerdo con la función, que no puede dejar de recordar la filosofía de Unix : Do One Thing and Do It Well, en la aplicación de la base de código único tradicional, entre los módulos es Límites estrechamente acoplados y difusos, a medida que el producto continúa iterando, el desarrollo y mantenimiento del código se volverá más complejo y habrá cada vez más posibles errores y vulnerabilidades.
Mejora la escalabilidad
En el desarrollo de proyectos, parte del código se puede usar con frecuencia en múltiples módulos. Estos módulos altamente reutilizables a menudo se extraen y se usan como bases de código público, como los módulos de verificación. Cuando se desea ampliar las funciones (Agregue el inicio de sesión del código de verificación de SMS, etc.), el tamaño de una sola base de código solo aumenta y toda la aplicación debe volver a implementarse. En la arquitectura de microservicios, el módulo de verificación puede aislarse como un solo servicio y puede ejecutarse, probarse e implementarse de forma independiente.
Seguir el concepto de código de división de microservicios puede reducir en gran medida el acoplamiento entre módulos, y la expansión horizontal será mucho más fácil. Es adecuado para el entorno actual de alto rendimiento, alta disponibilidad y desarrollo distribuido de computación en la nube.
Nginx tiene una serie de artículos para explorar muchos conceptos de microservicios, haga clic aquí para leer
¿Cuáles son los beneficios de usar Golang?
Microservicios es un concepto arquitectónico más que un proyecto de marco específico. Se pueden implementar muchos lenguajes de programación, pero algunos lenguajes tienen ventajas inherentes en el desarrollo de microservicios. Golang es uno de ellos.
Golang en sí mismo es muy ligero y altamente eficiente. Al mismo tiempo, tiene soporte nativo para la programación concurrente, que puede hacer un mejor uso de los procesadores multi-core. La net
biblioteca estándar incorporada también tiene soporte perfecto para el desarrollo de redes. Puede consultar el breve artículo de Xie Da: las ventajas del lenguaje Go
Además, la comunidad de Golang tiene un excelente marco de microservicio de código abierto, go-mirco , que usaremos en la siguiente sección.
Protobuf y gRPC
En una base de código único para aplicaciones tradicionales, las funciones se pueden llamar directamente entre cada módulo. Sin embargo, en la arquitectura de microservicios, dado que la base de código correspondiente a cada servicio se ejecuta de forma independiente y no se puede llamar directamente, la comunicación entre ellos es un gran problema, y hay 2 soluciones:
API para protocolo JSON o XML
Los microservicios pueden usar el protocolo JSON o XML basado en HTTP para comunicarse: antes de que el servicio A se comunique con el servicio B, A debe codificar los datos que se transferirán al formato JSON / XML y luego pasarlos a B, B en forma de cadena Los datos recibidos deben decodificarse antes de que puedan usarse en el código:
- Ventajas: los datos son fáciles de leer y de usar, es un protocolo necesario para interactuar con el navegador.
- Desventajas: en el caso de una gran cantidad de datos, la sobrecarga de codificación y decodificación se hace mayor, y la información de campo adicional conduce a mayores costos de transmisión
API de protocolo RPC
los datos JSON a continuación en el uso description
, weight
y otros metadatos para describir el significado de los datos en sí, en la arquitectura navegador / servidor con mucho, a fin de facilitar los análisis sintácticos del navegador:
{
"description": "This is a test consignment",
"weight": 550,
"containers": [
{
"customer_id": "cust001",
"user_id": "user001",
"origin": "Manchester, United Kingdom"
}
],
"vessel_id": "vessel001"
}
Sin embargo, cuando se comunica entre dos microservicios, si el formato de transmisión de datos se acuerda entre sí, el flujo de datos binarios se puede usar para la comunicación directamente, y los metadatos voluminosos y redundantes ya no son necesarios.
Introducción a gRPC
gRPC es un marco de comunicación RPC liviano abierto de Google. El protocolo de comunicación se basa en el flujo de datos binarios, lo que hace que gRPC tenga un rendimiento excelente.
gRPC admite el protocolo HTTP 2.0, utiliza tramas binarias para la transmisión de datos y también puede establecer un flujo continuo de datos bidireccional para ambas partes en la comunicación. Ver también: Introducción a Google HTTP / 2
protobuf como protocolo de comunicación
Los dos microservicios se comunican a través de un marco de datos binarios basado en HTTP 2.0, entonces, ¿cómo acordar el formato de los datos binarios? La respuesta es usar el protocolo protobuf integrado en gRPC, y su sintaxis DSL puede definir claramente la estructura de datos de la comunicación entre servicios. Ver también: gRPC Go: más allá de lo básico
desarrollo de microservicios de servicio de consignación
Después de la explicación conceptual necesaria anterior, comencemos a desarrollar nuestro primer microservicio : consignación-servicio
Estructura del proyecto
Suponiendo que este proyecto se llame shippy , debe:
- En
$GOPATH
Nueva Shippy directorio del proyecto en el directorio src - Crea un nuevo archivo en el directorio del proyecto
consignment-service/proto/consignment/consignment.proto
Para la conveniencia de la enseñanza, pondré todos los códigos de microservicio de este proyecto en el directorio shippy. Esta estructura de proyecto se llama "mono-repo", y los lectores también pueden dividir cada microservicio en "multi-repo" Proyecto independiente Más referencia a la batalla de estilo REPO: MONO VS MULTI
La estructura de su proyecto ahora debería ser la siguiente:
$GOPATH/src
└── shippy
└── consignment-service
└── proto
└── consignment
└── consignment.proto
Proceso de desarrollo
Definir archivo de protocolo de comunicación protobuf
// shipper/consignment-service/proto/consignment/consignment.proto
syntax = "proto3";
package go.micro.srv.consignment;
// 货轮微服务
service ShippingService {
// 托运一批货物
rpc CreateConsignment (Consignment) returns (Response) {
}
}
// 货轮承运的一批货物
message Consignment {
string id = 1; // 货物编号
string description = 2; // 货物描述
int32 weight = 3; // 货物重量
repeated Container containers = 4; // 这批货有哪些集装箱
string vessel_id = 5; // 承运的货轮
}
// 单个集装箱
message Container {
string id = 1; // 集装箱编号
string customer_id = 2; // 集装箱所属客户的编号
string origin = 3; // 出发地
string user_id = 4; // 集装箱所属用户的编号
}
// 托运结果
message Response {
bool created = 1; // 托运成功
Consignment consignment = 2;// 新托运的货物
}
Referencia de sintaxis: Protobuf doc
Generar código de protocolo
El compilador de protocolos utiliza el complemento grpc para compilar archivos .proto
Para evitar la ejecución repetida de compilar y ejecutar comandos en la terminal, este proyecto utiliza la herramienta de creación para crear consignment-service/Makefile
build:
# 一定要注意 Makefile 中的缩进,否则 make build 可能报错 Nothing to be done for build
# protoc 命令前边是一个 Tab,不是四个或八个空格
protoc -I. --go_out=plugins=grpc:$(GOPATH)/src/shippy/consignment-service proto/consignment/consignment.proto
Ejecución make build
, se proto/consignment
generará en el directorio consignment.pb.go
La correspondencia entre consignment.proto y consignment.pb.go
servicio : define la función CreateConsignment
del servicio de envío del microservicio que se expondrá como la llamada externa : generada por el complemento grpc del compilador protobuf para generar la interfaz
type ShippingServiceClient interface {
// 托运一批货物
CreateConsignment(ctx context.Context, in *Consignment, opts ...grpc.CallOption) (*Response, error)
}
mensaje : define el formato de datos de la comunicación, que procesa el compilador protobuf para generar una estructura
type Consignment struct {
Id string `protobuf:"bytes,1,opt,name=id" json:"id,omitempty"`
Description string `protobuf:"bytes,2,opt,name=description" json:"description,omitempty"`
Weight int32 `protobuf:"varint,3,opt,name=weight" json:"weight,omitempty"`
Containers []*Container `protobuf:"bytes,4,rep,name=containers" json:"containers,omitempty"`
// ...
}
Implemente el servidor
El servidor necesita implementar la ShippingServiceClient
interfaz y crearconsignment-service/main.go
package main
import (
// 导如 protoc 自动生成的包
pb "shippy/consignment-service/proto/consignment"
"context"
"net"
"log"
"google.golang.org/grpc"
)
const (
PORT = ":50051"
)
//
// 仓库接口
//
type IRepository interface {
Create(consignment *pb.Consignment) (*pb.Consignment, error) // 存放新货物
}
//
// 我们存放多批货物的仓库,实现了 IRepository 接口
//
type Repository struct {
consignments []*pb.Consignment
}
func (repo *Repository) Create(consignment *pb.Consignment) (*pb.Consignment, error) {
repo.consignments = append(repo.consignments, consignment)
return consignment, nil
}
func (repo *Repository) GetAll() []*pb.Consignment {
return repo.consignments
}
//
// 定义微服务
//
type service struct {
repo Repository
}
//
// service 实现 consignment.pb.go 中的 ShippingServiceServer 接口
// 使 service 作为 gRPC 的服务端
//
// 托运新的货物
func (s *service) CreateConsignment(ctx context.Context, req *pb.Consignment) (*pb.Response, error) {
// 接收承运的货物
consignment, err := s.repo.Create(req)
if err != nil {
return nil, err
}
resp := &pb.Response{Created: true, Consignment: consignment}
return resp, nil
}
func main() {
listener, err := net.Listen("tcp", PORT)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
log.Printf("listen on: %s\n", PORT)
server := grpc.NewServer()
repo := Repository{}
// 向 rRPC 服务器注册微服务
// 此时会把我们自己实现的微服务 service 与协议中的 ShippingServiceServer 绑定
pb.RegisterShippingServiceServer(server, &service{repo})
if err := server.Serve(listener); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
El código anterior implementa los métodos necesarios para el microservicio de servicio de consignación y establece un servidor gRPC para escuchar en el puerto 50051. Si ejecuta en este momento go run main.go
, el servidor se iniciará correctamente:
Implementar al cliente
Pondremos la información de los productos a registrar en consignment-cli/consignment.json
:
{
"description": "This is a test consignment",
"weight": 550,
"containers": [
{
"customer_id": "cust001",
"user_id": "user001",
"origin": "Manchester, United Kingdom"
}
],
"vessel_id": "vessel001"
}
El cliente leerá este archivo JSON y enviará los productos. Cree un nuevo archivo en el directorio del proyecto:consingment-cli/cli.go
package main
import (
pb "shippy/consignment-service/proto/consignment"
"io/ioutil"
"encoding/json"
"errors"
"google.golang.org/grpc"
"log"
"os"
"context"
)
const (
ADDRESS = "localhost:50051"
DEFAULT_INFO_FILE = "consignment.json"
)
// 读取 consignment.json 中记录的货物信息
func parseFile(fileName string) (*pb.Consignment, error) {
data, err := ioutil.ReadFile(fileName)
if err != nil {
return nil, err
}
var consignment *pb.Consignment
err = json.Unmarshal(data, &consignment)
if err != nil {
return nil, errors.New("consignment.json file content error")
}
return consignment, nil
}
func main() {
// 连接到 gRPC 服务器
conn, err := grpc.Dial(ADDRESS, grpc.WithInsecure())
if err != nil {
log.Fatalf("connect error: %v", err)
}
defer conn.Close()
// 初始化 gRPC 客户端
client := pb.NewShippingServiceClient(conn)
// 在命令行中指定新的货物信息 json 文件
infoFile := DEFAULT_INFO_FILE
if len(os.Args) > 1 {
infoFile = os.Args[1]
}
// 解析货物信息
consignment, err := parseFile(infoFile)
if err != nil {
log.Fatalf("parse info file error: %v", err)
}
// 调用 RPC
// 将货物存储到我们自己的仓库里
resp, err := client.CreateConsignment(context.Background(), consignment)
if err != nil {
log.Fatalf("create consignment error: %v", err)
}
// 新货物是否托运成功
log.Printf("created: %t", resp.Created)
}
Ejecutar go run main.go
nuevamente después de correr go run cli.go
:
Podemos agregar un RPC para ver todos los envíos y agregar un GetConsignments
método para que podamos ver todos los existentes consignment
:
// shipper/consignment-service/proto/consignment/consignment.proto
syntax = "proto3";
package go.micro.srv.consignment;
// 货轮微服务
service ShippingService {
// 托运一批货物
rpc CreateConsignment (Consignment) returns (Response) {
}
// 查看托运货物的信息
rpc GetConsignments (GetRequest) returns (Response) {
}
}
// 货轮承运的一批货物
message Consignment {
string id = 1; // 货物编号
string description = 2; // 货物描述
int32 weight = 3; // 货物重量
repeated Container containers = 4; // 这批货有哪些集装箱
string vessel_id = 5; // 承运的货轮
}
// 单个集装箱
message Container {
string id = 1; // 集装箱编号
string customer_id = 2; // 集装箱所属客户的编号
string origin = 3; // 出发地
string user_id = 4; // 集装箱所属用户的编号
}
// 托运结果
message Response {
bool created = 1; // 托运成功
Consignment consignment = 2; // 新托运的货物
repeated Consignment consignments = 3; // 目前所有托运的货物
}
// 查看货物信息的请求
// 客户端想要从服务端请求数据,必须有请求格式,哪怕为空
message GetRequest {
}
Ahora ejecute make build
para obtener la última interfaz de microservicio compilada. Si ejecuta en este momento go run main.go
, recibirá un mensaje de error similar a este:
Aquellos de ustedes que estén familiarizados con Go deben saber que han olvidado implementar un interface
método requerido. Actualicemos consignment-service/main.go
:
package main
import (
pb "shippy/consignment-service/proto/consignment"
"context"
"net"
"log"
"google.golang.org/grpc"
)
const (
PORT = ":50051"
)
//
// 仓库接口
//
type IRepository interface {
Create(consignment *pb.Consignment) (*pb.Consignment, error) // 存放新货物
GetAll() []*pb.Consignment // 获取仓库中所有的货物
}
//
// 我们存放多批货物的仓库,实现了 IRepository 接口
//
type Repository struct {
consignments []*pb.Consignment
}
func (repo *Repository) Create(consignment *pb.Consignment) (*pb.Consignment, error) {
repo.consignments = append(repo.consignments, consignment)
return consignment, nil
}
func (repo *Repository) GetAll() []*pb.Consignment {
return repo.consignments
}
//
// 定义微服务
//
type service struct {
repo Repository
}
//
// 实现 consignment.pb.go 中的 ShippingServiceServer 接口
// 使 service 作为 gRPC 的服务端
//
// 托运新的货物
func (s *service) CreateConsignment(ctx context.Context, req *pb.Consignment) (*pb.Response, error) {
// 接收承运的货物
consignment, err := s.repo.Create(req)
if err != nil {
return nil, err
}
resp := &pb.Response{Created: true, Consignment: consignment}
return resp, nil
}
// 获取目前所有托运的货物
func (s *service) GetConsignments(ctx context.Context, req *pb.GetRequest) (*pb.Response, error) {
allConsignments := s.repo.GetAll()
resp := &pb.Response{Consignments: allConsignments}
return resp, nil
}
func main() {
listener, err := net.Listen("tcp", PORT)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
log.Printf("listen on: %s\n", PORT)
server := grpc.NewServer()
repo := Repository{}
pb.RegisterShippingServiceServer(server, &service{repo})
if err := server.Serve(listener); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
Si lo usa ahora go run main.go
, todo debería ser normal:
Finalmente déjenos actualizar consignment-cli/cli.go
para obtener la consignment
información:
func main() {
...
// 列出目前所有托运的货物
resp, err = client.GetConsignments(context.Background(), &pb.GetRequest{})
if err != nil {
log.Fatalf("failed to list consignments: %v", err)
}
for _, c := range resp.Consignments {
log.Printf("%+v", c)
}
}
Ejecutar de nuevo en este momento go run cli.go
, debería poder ver todo lo creado consignment
, varias ejecuciones verán que se consignan varios productos:
Transferencia fallida y re-carga cancelar
Hasta ahora, hemos creado un microservicio y un cliente usando protobuf y grpc.
En el próximo artículo, presentaremos el uso de go-micro
frameworks y la creación de nuestro segundo microservicio. En el próximo artículo, presentaremos cómo permitir que Docker contenga nuestros microservicios en contenedores.