25 July 2019

Как создать простой микросервис на Golang и gRPC и выполнить его контейнеризацию с помощью Docker

ProgrammingGoMicroservices
Sandbox
Привет, Хабр! представляю вашему вниманию перевод статьи «Go, gRPC and Docker» автора Mat Evans.

Существует множество статей о совместном использовании Go и Docker. Создавать контейнеры, способные взаимодействовать с клиентами и между собой, очень легко. Далее следует небольшой пример того, как это делается на базовом уровне.

Что мы создаем?


Мы будем создавать очень простые клиент и сервер, взаимодействующие между собой при помощи gRPC. Сервер при этом будет находиться внутри Docker-контейнера, чтобы его можно было легко развернуть.

Предположим, что нам нужен сервис, который принимает от клиента строку и возвращает в ответ строку с обращенным порядком символов. Например, посылаем «кот» и получаем в ответ «ток».

.proto-файл


.proto-файл описысывает, какие операции наш сервис будет осуществлять и какими данными он при этом будет обмениваться. Создаем в проекте папку proto, а в ней — файл reverse.proto

syntax = "proto3";

package reverse;

service Reverse {
    rpc Do(Request) returns (Response) {}
}

message Request {
    string message = 1;
}

message Response {
    string message = 1;
}

Функция, которая вызывается удаленно на сервере и возвращает данные клиенту, помечается как rpc. Структуры данных, служащие для обмена информацией между взаимодействующими узлами, помечаются как message. Каждому полю сообщения необходимо присвоить порядковый номер. В данном случае наша функция принимает от клиента сообщения типа Request и возвращает сообщения типа Response.
Как только мы создали .proto-файл, необходимо получить .go-файл нашего сервиса. Для этого нужно выполнить седующую консольную команду в папке proto:

$ protoc -I . reverse.proto --go_out=plugins=grpc:.

Разумеется, сначала вам нужно выполнить сборку gRPC.
Выполнение вышеприведенной команды создаст новый .go-файл, содержащий методы для создания клиента, сервера и сообщений, которыми они обмениваются. Если мы вызовем godoc, то увидим следующее:

$ godoc .
PACKAGE DOCUMENTATION

package reverse
    import "."

Package reverse is a generated protocol buffer package.

It is generated from these files:

reverse.proto

It has these top-level messages:

Request
 Response
....

Клиент


Было бы неплохо, если бы наш клиент работал вот так:

reverse "this is a test"
tset a si siht

Вот код, который создает gRPC-клиент, используя структуры данных, сгенерированные из .proto-файла:

package main

import (
    "context"
    "fmt"
    "os"    
     pb "github.com/matzhouse/go-grpc/proto"
    "google.golang.org/grpc"
    "google.golang.org/grpc/grpclog"
)

func main() {
    opts := []grpc.DialOption{
        grpc.WithInsecure(),
    }
    args := os.Args
    conn, err := grpc.Dial("127.0.0.1:5300", opts...)

    if err != nil {
        grpclog.Fatalf("fail to dial: %v", err)
    }

    defer conn.Close()

    client := pb.NewReverseClient(conn)
    request := &pb.Request{
        Message: args[1],
    }
    response, err := client.Do(context.Background(), request)

    if err != nil {
        grpclog.Fatalf("fail to dial: %v", err)
    }

   fmt.Println(response.Message)
}


Сервер


Сервер испозует тот же самый сгенерированный .go-файл. Однако он определяет только интерфейс сервера, логику же нам придется реализовать самостоятельно:

package main

import (
    "net"
    pb "github.com/matzhouse/go-grpc/proto"
    "golang.org/x/net/context"
    "google.golang.org/grpc"
    "google.golang.org/grpc/grpclog"
)

func main() {
    listener, err := net.Listen("tcp", ":5300")

    if err != nil {
        grpclog.Fatalf("failed to listen: %v", err)
    }

    opts := []grpc.ServerOption{}
    grpcServer := grpc.NewServer(opts...)

    pb.RegisterReverseServer(grpcServer, &server{})
    grpcServer.Serve(listener)
}

type server struct{}

func (s *server) Do(c context.Context, request *pb.Request)
(response *pb.Response, err error) {
    n := 0
    // Сreate an array of runes to safely reverse a string.
    rune := make([]rune, len(request.Message))

    for _, r := range request.Message {
        rune[n] = r
        n++
    }

    // Reverse using runes.
    rune = rune[0:n]

    for i := 0; i < n/2; i++ {
        rune[i], rune[n-1-i] = rune[n-1-i], rune[i]
    }

    output := string(rune)
    response = &pb.Response{
        Message: output,
    }

    return response, nil    
}


Docker


Я предполагаю, что вы знаете, что такое Docker и для чего он нужен. Вот наш Dockerfile:

FROM golang:1.12

ADD . /go/src/github.com/matzhouse/go-grpc/server

RUN go install github.com/matzhouse/go-grpc/server

ENTRYPOINT ["/go/bin/server"]

EXPOSE 5300

Здесь прописан код сборки Docker-образа. Разберем его построчно.

FROM golang:1.12

Это команда обозначает, что мы хотим создать образ нашего приложения на основе заранее созданного образа, а именно golang. Это Docker-образ с уже настроенной средой для сборки и запуска программ, написанных на Go.

ADD . /go/src/github.com/matzhouse/go-grpc/server

Эта команда копирует исходный код нашего приложения в GOPATH/src контейнера.

RUN go install github.com/matzhouse/go-grpc/server

Эта команда собирает наше приложение из скопированных в контейнер исходников и устанавливает его в папку контейнера GOPATH/bin.

ENTRYPOINT ["/go/bin/server"]

Эта команда конфигурирует контейнер таким образом, чтобы он работал как исполнимая программа. В ней мы указываем путь к исполнимому файлу приложения и, при необходимости, аргументы командной строки.

EXPOSE 5300

Этой командой мы сообщаем контейнеру, какие порты должны быть доступны извне.

Запуск сервера


Нам нужно запустить контейнер с нашим серверным приложением.
Сначала необходимо построить образ на основе инструкций из Dockerfile:

$ sudo docker build -t matzhouse/grpc-server .
Sending build context to Docker daemon 31.76 MB
Step 1/5 : FROM golang
 ---> a0c61f0b0796
Step 2/5 : ADD . /go/src/github.com/matzhouse/go-grpc
 ---> 9508be6501c1
Removing intermediate container 94dc6e3a9a20
Step 3/5 : RUN go install github.com/matzhouse/go-grpc/server
 ---> Running in f3e0b993a420
 ---> f7a0370b7f7d
Removing intermediate container f3e0b993a420
Step 4/5 : ENTRYPOINT /go/bin/server
 ---> Running in 9c9619e45df4
 ---> fb34dfe1c0ea
Removing intermediate container 9c9619e45df4
Step 5/5 : EXPOSE 5300
 ---> Running in 0403390af135
 ---> 008e09b9aebd
Removing intermediate container 0403390af135
Successfully built 008e09b9aebd

Теперь мы можем увидеть данный образ в списке:

$ docker images
REPOSITORY                         TAG              IMAGE ID
...    
matzhouse/grpc-server              latest           008e09b9aebd
... 

Отлично! У нас есть образ нашего серверного приложения, при помощи которого можно запустить его контейнер с помошью следующей команды:

$ docker run -it -p 5300:5300 matzhouse/grpc-server

В данном случае выполняется т.н. проброс портов. Заметьте, что для него нам нужны как инструкция EXPOSE, так и аргумент -p.

Запуск клиента


Контейнеризация клиента не даст больших преимуществ, так что запустим его обычным способом:

$ go build -o reverse
$ ./reverse "this is a test"
tset a si siht

Спасибо за прочтение!
Tags:golangdockergrpcprotobufmicroservicesмикросервисыgo
Hubs: Programming Go Microservices
+23
15.2k 137
Comments 6
Popular right now
Top of the last 24 hours