Es hora de abordar los contenedores.
En primer lugar, utilizamos la última imagen de Linux Alpine. Linux Alpine es una distribución liviana de Linux diseñada y optimizada para ejecutar aplicaciones web en Docker. En otras palabras, Linux Alpine tiene suficientes dependencias y funcionalidades para ejecutar la mayoría de las aplicaciones. ¡Esto significa que el tamaño de la imagen es de aproximadamente 8 MB!
En comparación con, digamos ... una máquina virtual Ubuntu con una capacidad de aproximadamente 1 GB, es por eso que las imágenes de Docker se han vuelto más naturales para los microservicios y la computación en la nube.
Entonces, ahora espero que vea valor en la contenedorización, y podamos comenzar a "Dockerising" nuestro primer servicio. Creemos un Dockerfile
$ touch consignment
-service / Dockerfile .
Primera parteDepósito original de EwanValentineArtículo originalEn el Dockerfile, agregue lo siguiente:
FROM alpine:latest RUN mkdir /app WORKDIR /app ADD consignment-service /app/consignment-service CMD ["./consignment-service"]
Luego creamos un nuevo directorio para alojar nuestra aplicación. Luego agregamos nuestro binario compilado a nuestro contenedor Docker y lo ejecutamos.
Ahora, vamos a actualizar el registro de compilación de nuestro Makefile para crear una imagen Docker.
build: ... GOOS=linux GOARCH=amd64 go build docker build -t consignment .
Agregamos dos pasos más, y me gustaría explicarlos con más detalle. En primer lugar, creamos nuestro binario Go. Sin embargo, notará dos variables de entorno antes de ejecutar $ go build. GOOS y GOARCH le permiten compilar su binario para otro sistema operativo. Como estoy desarrollando un Macbook, no puedo compilar el ejecutable go y luego ejecutarlo en un contenedor Docker que usa Linux. El binario no tendrá ningún significado en su contenedor Docker y arrojará un error.
El segundo paso que agregué es el proceso de construcción de Docker. Docker leerá su Dockerfile y creará una imagen llamada consign-service, el punto indica la ruta del directorio, por lo que aquí solo queremos que el proceso de compilación mire el directorio actual.
Voy a agregar una nueva entrada a nuestro Makefile:
run: docker run -p 50051:50051 shippy-service-consignment
Aquí lanzamos nuestra imagen docker abriendo el puerto 50051. Dado que Docker opera en una capa de red separada, debe redirigir el puerto. Por ejemplo, si desea iniciar este servicio en el puerto 8080, debe cambiar el argumento -p a 8080: 50051. También puede ejecutar el contenedor en segundo plano incluyendo la bandera -d. Por ejemplo,
docker run -d -p 50051: 50051 consignación-servicio .
Ejecute
$ make run , luego en un panel de terminal separado nuevamente
$ go run main.go y verifique que todavía funcione.
Cuando ejecuta $ docker build, incrusta su código y tiempo de ejecución en la imagen. Las imágenes de Docker son imágenes portátiles de su entorno y sus dependencias. Puede compartir imágenes de Docker publicándolas en Docker Hub. Que es similar a npm o al repositorio de yum para imágenes de docker. Cuando define FROM en su Dockerfile, le dice a Docker que extraiga esta imagen del repositorio de Docker para usarla como base. Luego puede expandir y redefinir partes de este archivo base, redefiniéndolas como desee. No publicaremos nuestras imágenes de docker, pero siéntase libre de navegar por el repositorio de docker y tenga en cuenta que casi cualquier software ya ha sido empaquetado en contenedores. Algunas cosas realmente maravillosas fueron atracadas.
Cada anuncio en Dockerfile se almacena en caché la primera vez que se crea. Esto elimina la necesidad de reconstruir todo el tiempo de ejecución cada vez que realiza cambios. La ventana acoplable es lo suficientemente inteligente como para descubrir qué detalles han cambiado y cuáles deben reconstruirse. Esto hace que el proceso de construcción sea increíblemente rápido.
Suficiente sobre los contenedores! Volvamos a nuestro código.
Al crear el servicio gRPC, hay una gran cantidad de código estándar para crear conexiones, y debe codificar la ubicación de la dirección del servicio en el cliente u otro servicio para que pueda conectarse a él. Esto es difícil porque cuando inicia servicios en la nube, es posible que no usen el mismo host o que la dirección o la dirección IP puedan cambiar después de que el servicio se vuelva a implementar.
Aquí es donde entra en juego el servicio de descubrimiento. El servicio de descubrimiento actualiza el directorio de todos sus servicios y sus ubicaciones. Cada servicio se registra en tiempo de ejecución y se anula el registro al cierre. A cada servicio se le asigna un nombre o identificador. Por lo tanto, incluso si puede tener una nueva dirección IP o una dirección de host, siempre que el nombre del servicio permanezca igual, no necesita actualizar las llamadas a este servicio desde otros servicios.
Como regla, hay muchos enfoques para este problema, pero, como la mayoría de las cosas en la programación, si alguien ya ha solucionado este problema, no tiene sentido reinventar la rueda. @Chuhnk (Asim Aslam), el creador de
Go-micro , resuelve estos problemas con una claridad y facilidad de uso fantásticas. Él solo produce un software fantástico. ¡Por favor considera ayudarlo si te gusta lo que ves!
Ir micro
Go-micro es un potente marco de microservicios escrito en Go, para su uso, en su mayor parte, con Go. Sin embargo, puede usar Sidecar para interactuar con otros idiomas.
Go-micro tiene características útiles para crear microservicios en Go. Pero comenzaremos con quizás el problema más común que resuelve, y este es el descubrimiento de un servicio.
Tendremos que hacer varias actualizaciones a nuestro servicio para poder trabajar con go-micro. Go-micro se integra como un complemento de Protocolos, en este caso reemplaza el complemento estándar de gRPC que estamos utilizando actualmente. Entonces, comencemos reemplazando esto en nuestro Makefile.
Asegúrese de instalar las dependencias go-micro:
go get -u github.com/micro/protobuf/{proto,protoc-gen-go}
Actualice nuestro Makefile para usar el complemento go-micro en lugar del complemento gRPC:
build: protoc -I. --go_out=plugins=micro:. \ proto/consignment/consignment.proto GOOS=linux GOARCH=amd64 go build docker build -t consignment . run: docker run -p 50051:50051 shippy-service-consignment
Ahora necesitamos actualizar nuestro shippy-service-consignment / main.go para usar go-micro. Esto resume la mayor parte de nuestro código gRPC anterior. Procesa fácilmente el registro y acelera la escritura de un servicio.
shippy-service-consignment / main.go El cambio principal aquí es la forma en que creamos nuestro servidor gRPC, que ha sido cuidadosamente abstraído de mico.NewService (), que maneja el registro de nuestro servicio. Y finalmente, la función service.Run (), que procesa la conexión en sí. Como antes, registramos nuestra implementación, pero esta vez con un método ligeramente diferente.
El segundo cambio más importante se refiere a los métodos de servicio en sí: los argumentos y los tipos de respuestas se modifican ligeramente para aceptar las estructuras de solicitud y respuesta como argumentos, y ahora solo devuelven un error. En nuestros métodos, establecemos la respuesta que van los procesos micro.
Finalmente, ya no programamos el puerto. Go-micro debe configurarse utilizando variables de entorno o argumentos de línea de comandos. Para establecer la dirección, use MICRO_SERVER_ADDRESS =: 50051. De manera predeterminada, Micro usa mdns (dns de multidifusión) como agente de descubrimiento de servicios para uso local. Por lo general, no utiliza mdns para descubrir servicios en un entorno de producción, pero queremos evitar tener que ejecutar algo como Consul o etcd localmente para realizar pruebas. Más sobre esto más tarde.
Actualicemos nuestro Makefile para reflejar esto.
build: protoc -I. --go_out=plugins=micro:. \ proto/consignment/consignment.proto GOOS=linux GOARCH=amd64 go build docker build -t consignment . run: docker run -p 50051:50051 \ -e MICRO_SERVER_ADDRESS=:50051 \ shippy-service-consignment
-e es el indicador de la variable de entorno, le permite pasar variables de entorno a su contenedor Docker. Debe tener un indicador para cada variable, por ejemplo -e ENV = puesta en escena -e DB_HOST = localhost, etc.
Ahora, si ejecuta $ make run, tendrá un servicio Dockerised con descubrimiento de servicio. Entonces, actualice nuestra herramienta Cli para usar esto.
consignación-cli package main import ( "encoding/json" "io/ioutil" "log" "os" "context" pb "github.com/EwanValentine/shippy-service-consignment/proto/consignment" micro "github.com/micro/go-micro" ) const ( address = "localhost:50051" defaultFilename = "consignment.json" ) func parseFile(file string) (*pb.Consignment, error) { var consignment *pb.Consignment data, err := ioutil.ReadFile(file) if err != nil { return nil, err } json.Unmarshal(data, &consignment) return consignment, err } func main() { service := micro.NewService(micro.Name("shippy.cli.consignment")) service.Init() client := pb.NewShippingServiceClient("shippy.service.consignment", service.Client())
Aquí, importamos las bibliotecas go-micro para crear clientes y reemplazamos el código de conexión existente con el código de cliente go-micro, que utiliza el permiso del servicio en lugar de conectarse directamente a la dirección.
Sin embargo, si ejecuta esto, no funcionará. Esto se debe a que ahora estamos lanzando nuestro servicio en el contenedor Docker, que tiene sus propios mdns, separados del host mdns que estamos utilizando actualmente. La forma más fácil de solucionar esto es asegurarse de que tanto el servicio como el cliente se estén ejecutando en Dockerland, de modo que ambos trabajen en el mismo host y usen la misma capa de red. Entonces, creemos make consignment-cli / Makefile y creemos algunas entradas.
build: GOOS=linux GOARCH=amd64 go build docker build -t shippy-cli-consignment . run: docker run shippy-cli-consignment
Como antes, queremos construir nuestro binario para Linux. Cuando lanzamos nuestra imagen docker, queremos pasar una variable de entorno para dar el comando go-micro para usar mdns.
Ahora creemos un Dockerfile para nuestra herramienta CLI:
FROM alpine:latest RUN mkdir -p /app WORKDIR /app ADD consignment.json /app/consignment.json ADD consignment-cli /app/consignment-cli CMD ["./shippy-cli-consignment"]
Esto es muy similar a nuestro servicio Dockerfile, excepto que también extrae nuestro archivo de datos json.
Ahora, cuando ejecute $ make run en su envío shippy-cli, debería ver Creado: verdadero, como antes.
Ahora, parece hora de echar un vistazo a la nueva función Docker: compilaciones de varias etapas. Esto nos permite usar múltiples imágenes Docker en un Dockerfile.
Esto es especialmente útil en nuestro caso, ya que podemos usar una imagen para crear nuestro archivo binario con todas las dependencias correctas. Y luego usa la segunda imagen para iniciarlo. Probemos esto, dejaré comentarios detallados junto con el código:
consignación-servicio / Dockerfile # consignment-service/Dockerfile # golang, # . `as builder`, # , . FROM golang:alpine as builder RUN apk --no-cache add git # gopath WORKDIR /app/shippy-service-consignment # COPY . . RUN go mod download # , # Alpine. RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o shippy-service-consignment # FROM, # Docker . FROM alpine:latest # , - RUN apk --no-cache add ca-certificates # , . RUN mkdir /app WORKDIR /app # , , # `builder` # , # , , # . ! COPY --from=builder /app/shippy-service-consignment/shippy-service-consignment . # ! # # run time . CMD ["./shippy-service-consignment"]
Ahora pasaré a otros archivos Docker y adoptaré este nuevo enfoque. ¡Ah, y no olvides eliminar $ go build de tus Makefiles!
Servicio de envío
Creemos un segundo servicio. Tenemos un servicio (envío-servicio-envío), que se ocupa de la coordinación del lote de contenedores con el barco, que es el más adecuado para este lote. Para que coincida con nuestro lote, debemos enviar el peso y la cantidad de contenedores a nuestro nuevo servicio de envío, que luego encontrará un buque capaz de manejar este lote.
Cree un nuevo directorio en su directorio raíz
$ mkdir vessel-service , ahora cree un subdirectorio para nuestra nueva definición de servicios protobuf,
$ mkdir -p shippy-service-vessel / proto / vessel . Ahora creemos un nuevo archivo de protobuf,
$ touch shippy-service-vessel / proto / vessel / vessel.proto .
Dado que la definición de protobuf es de hecho el núcleo de nuestro diseño de software, comencemos con ella.
buque / buque.proto // shippy-service-vessel/proto/vessel/vessel.proto syntax = "proto3"; package vessel; service VesselService { rpc FindAvailable(Specification) returns (Response) {} } message Vessel { string id = 1; int32 capacity = 2; int32 max_weight = 3; string name = 4; bool available = 5; string owner_id = 6; } message Specification { int32 capacity = 1; int32 max_weight = 2; } message Response { Vessel vessel = 1; repeated Vessel vessels = 2; }
Como puede ver, esto es muy similar a nuestro primer servicio. Creamos un servicio con un método rpc llamado FindAvailable. Esto toma un tipo de especificación y devuelve un tipo de respuesta. El tipo de respuesta devuelve el tipo de embarcación o múltiples embarcaciones utilizando un campo repetitivo.
Ahora necesitamos crear un Makefile para manejar nuestra lógica de compilación y nuestro script de inicio.
$ touch shippy-service-vessel / Makefile . Abra este archivo y agregue lo siguiente:
// vessel-service/Makefile build: protoc -I. --go_out=plugins=micro:. \ proto/vessel/vessel.proto docker build -t shippy-service-vessel . run: docker run -p 50052:50051 -e MICRO_SERVER_ADDRESS=:50051 shippy-service-vessel
Esto es casi idéntico al primer Makefile que creamos para nuestro servicio de envío, sin embargo, tenga en cuenta que los nombres de los servicios y puertos han cambiado un poco. No podemos ejecutar dos contenedores de acoplamiento en el mismo puerto, por lo que utilizamos el reenvío de puertos Dockers para que este servicio redirija de 50051 a 50052 en la red host.
Ahora necesitamos un Dockerfile usando nuestro nuevo formato de etapas múltiples:
# vessel-service/Dockerfile FROM golang:alpine as builder RUN apk --no-cache add git WORKDIR /app/shippy-service-vessel COPY . . RUN go mod download RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o shippy-service-vessel FROM alpine:latest RUN apk --no-cache add ca-certificates RUN mkdir /app WORKDIR /app COPY --from=builder /app/shippy-service-vessel . CMD ["./shippy-service-vessel"]
Finalmente, podemos escribir nuestra implementación:
Ahora pasemos a la parte interesante. Cuando creamos un envío, necesitamos cambiar nuestro servicio de manejo de carga para contactar al servicio de búsqueda de barcos, encontrar el barco y actualizar el parámetro ship_id en el envío creado:
shippy / consignment-service / main.go package main import ( "context" "fmt" "log" "sync" pb "github.com/EwanValentine/shippy-service-consignment/proto/consignment" vesselProto "github.com/EwanValentine/shippy-service-vessel/proto/vessel" "github.com/micro/go-micro" ) const ( port = ":50051" ) type repository interface { Create(*pb.Consignment) (*pb.Consignment, error) GetAll() []*pb.Consignment }
Aquí creamos una instancia de cliente para nuestro servicio de envío, que nos permite usar el nombre del servicio, es decir, shipy.service.vessel para llamar al servicio del barco como cliente e interactuar con sus métodos.
En este caso, solo un método (FindAvailable). Enviamos el peso del lote junto con el número de contenedores que queremos enviar como una especificación para el servicio del barco. Lo que nos devuelve el recipiente correspondiente a esta especificación.Actualice el archivo consignment-cli / consignment.json, elimine el ship_id codificado, porque queremos confirmar que nuestro servicio de búsqueda de buques está funcionando. Además, agreguemos algunos contenedores más y aumentemos el peso. Por ejemplo:
{ "description": " ", "weight": 55000, "containers": [ { "customer_id": "_001", "user_id": "_001", "origin": "--" }, { "customer_id": "_002", "user_id": "_001", "origin": "" }, { "customer_id": "_003", "user_id": "_001", "origin": "" } ] }
Ahora ejecute $ make build && make run en consignment-cli. Debería ver una respuesta con una lista de los productos creados. En sus fiestas, debería ver que el parámetro vessel_id está configurado.¡Entonces, tenemos dos microservicios interconectados y una interfaz de línea de comando!En la siguiente parte de esta serie, consideraremos guardar algunos de estos datos con MongoDB. También agregaremos un tercer servicio y usaremos docker-compose para administrar localmente nuestro creciente ecosistema de contenedores.Parte IDepósito original de EwanValentine