[Перевод] Учебное пособие по микросервису Golang (1)

Go gRPC

Оригинальная ссылка:ewanvalentine.io, перевод одобрен авторомEwan Valentine уполномоченный.

В этом разделе содержится краткое введение в использование gRPC. Для получения дополнительной информации см.:4 режима взаимодействия данных между Клиентом и Сервером в gRPC

предисловие

Обзор серии

«Учебное пособие по микросервисам Golang» разделено на 10 статей, описывающих весь процесс разработки, тестирования и развертывания микросервисов.

В этом разделе представлены основные концепции и терминология микросервисов перед созданием компактной версии нашего первого микросервиса, консигнационного сервиса. В следующих разделах 2–10 мы создадим следующие микросервисы один за другим:

  • консигнационная служба
  • инвентаризация
  • пользовательская служба
  • служба аутентификации
  • ролевая служба
  • судно-сервис

Полный стек используемых технологий выглядит следующим образом:

Golang, gRPC, go-micro			// 开发语言及其 RPC 框架
Google Cloud, MongoDB			// 云平台与数据存储
Docker, Kubernetes, Terrafrom  	// 容器化与集群架构
NATS, CircleCI					// 消息系统与持续集成

репозиторий кода

Код автора:EwanValentine/shippy, код китайского комментария переводчика:wuYin/shippy

Каждая глава соответствует ветке репозитория, например, код части 1 этой статьи находится вfeature/part1

среда разработки

Среда разработки автора - macOS. В этой статье для эффективной компиляции используется инструмент make. Пользователям Windows необходимоРучная установка

$ go env		
GOARCH="amd64"	# macOS 环境
GOOS="darwin"	# 在第二节使用 Docker 构建 alpine 镜像时需修改为 linux
GOPATH="/Users/wuyin/Go"
GOROOT="/usr/local/go"

Подготовить

Освойте базовую грамматику Голанга: рекомендуется прочитать книгу Се Да.«Займитесь веб-программированием»

УстановитьgRPC / protobuf

go get -u google.golang.org/grpc					# 安装 gRPC 框架
go get -u github.com/golang/protobuf/protoc-gen-go	# 安装 Go 版本的 protobuf 编译器

Микросервисы

О каком проекте мы будем писать?

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

Что такое микросервисы?

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

  • Разделить по характеристикам: например, шаблон MVC
  • Разделение по функциям: в более крупных проектах код может быть инкапсулирован в пакеты, предназначенные для разных видов бизнеса, и пакеты могут быть снова разделены.

Независимо от того, как она разделена, в конце концов, код обоих будет разрабатываться и управляться в одной библиотеке, см.:Единое управление кодовой базой Google

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

image-20180512033801893

В чем преимущества микросервисов?

уменьшить сложность

Код всего приложения разбит на небольшие и независимые кодовые базы микросервисов по их функциям, что напоминаетФилософия Unix: Делай что-то одно и делай это хорошо, в применении традиционной единой кодовой базы модули тесно связаны, а границы неоднозначны. При непрерывной итерации продукта разработка и обслуживание кода станут более сложными, потенциально баги А лазеек будет все больше и больше.

Улучшить масштабируемость

При разработке проекта часть кода может часто использоваться в нескольких модулях.Такие повторно используемые модули часто извлекаются и используются в качестве общей кодовой базы, например модуля проверки.Когда необходимо расширить его функции (добавление кода проверки SMS логин и т.д.), размер единой кодовой базы будет только увеличиваться, а все приложение нужно переделывать. В микросервисной архитектуре модуль проверки может быть изолирован как отдельная служба, которую можно запускать, тестировать и развертывать независимо.

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

У Nginx есть серия статей, в которых исследуются многие концепции микросервисов, которые могут бытьНажмите здесь, чтобы прочитать

Преимущества использования Golang?

Микросервис — это архитектурная концепция, а не конкретный фреймворк, его могут реализовать многие языки программирования, но некоторые языки имеют неотъемлемые преимущества для разработки микросервисов, и Golang — одно из них.

Сам Golang очень легкий, чрезвычайно эффективный и имеет встроенную поддержку параллельного программирования, поэтому он может лучше использовать многоядерные процессоры. встроенныйnetПоддержка стандартной библиотеки для веб-разработки также очень полная. Вы можете обратиться к короткой статье Се Да:Преимущества языка Go

Кроме того, у сообщества Golang есть отличный фреймворк микросервисов с открытым исходным кодом.go-mirco, который мы будем использовать в следующем разделе.

Protobuf и gRPC

В единой кодовой базе традиционного приложения каждый модуль может напрямую вызывать функции друг у друга. Однако в микросервисной архитектуре, поскольку кодовая база, соответствующая каждой службе, работает независимо и не может быть вызвана напрямую, связь между собой представляет собой большую проблему.Есть два решения:

API в формате JSON или XML

Микросервисы могут использовать для связи протокол JSON или XML на основе HTTP: прежде чем служба A свяжется со службой B, A должен закодировать данные для передачи в формат JSON/XML, а затем передать их B, B в виде строки. полученные данные необходимо декодировать, прежде чем их можно будет использовать в коде:

  • Преимущества: данные легко читаются, просты в использовании, это необходимый протокол для взаимодействия с браузерами.
  • Недостатки: в случае большого объема данных увеличиваются накладные расходы на кодирование и декодирование, а избыточность полевой информации приводит к увеличению затрат на передачу.

API для протокола RPC

Данные JSON ниже используютсяdescription,weightМетаданные и другие метаданные для описания значения самих данных часто используются в архитектуре браузера/сервера для облегчения анализа браузера:

{
  "description": "This is a test consignment",
  "weight": 550,
  "containers": [
    {
      "customer_id": "cust001",
      "user_id": "user001",
      "origin": "Manchester, United Kingdom"
    }
  ],
  "vessel_id": "vessel001"
}

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

Введение в gRPC

gRPCЭто облегченная платформа связи RPC с открытым исходным кодом от Google.Протокол связи основан на потоке двоичных данных, что обеспечивает отличную производительность gRPC.

gRPC поддерживает протокол HTTP 2.0, использует бинарные кадры для передачи данных, а также может устанавливать непрерывный двунаправленный поток данных для обеих сторон. Вы можете обратиться к:Введение в Google HTTP/2

protobuf как протокол связи

Связь между двумя микросервисами основана на фрейме бинарных данных HTTP 2.0, так как же согласовать формат бинарных данных? Ответ заключается в использовании встроенного в gRPC протокола protobuf, которыйDSLСинтаксис Структура данных, четко определяющая взаимодействие между службами. Вы можете обратиться к:gRPC Go: Beyond the basics

разработка микросервиса консигнационного сервиса

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

Структура проекта

Предположим, этот проект называетсяshippy,тебе надо:

  • существует$GOPATHСоздайте новый каталог проекта shippy в каталоге src.
  • Создайте новый файл в каталоге проектаconsignment-service/proto/consignment/consignment.proto

Для удобства обучения я буду помещать код всех микросервисов в этом проекте в каталог shippy. Такая структура проекта называется «моно-репо». Читатели также могут разбить каждый микросервис на «мульти-репо», самостоятельный проект. больше ссылокБитва стилей РЕПО: МОНО ПРОТИВ МУЛЬТИ

Теперь структура вашего проекта должна выглядеть так:

$GOPATH/src
    └── shippy
        └── consignment-service
            └── proto
                └── consignment
                    └── consignment.proto

Процесс разработки

image-20180512044329199

Определите файл протокола связи 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;// 新托运的货物
}

Ссылка на синтаксис:Protobuf doc

image-20180512010554833

Сгенерировать код протокола

Компилятор protoc использует подключаемый модуль grpc для компиляции файлов .proto.

Во избежание повторной компиляции и запуска команд в терминале в этом проекте используется инструмент make для создания нового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

воплощать в жизньmake build,Будет вproto/consignmentсоздается в каталогеconsignment.pb.go

Соответствие между consignment.proto и consignment.pb.go

service: определяет функции, которые микросервис ShippingService должен предоставлять внешнему миру:CreateConsignment, сгенерированный подключаемым модулем grpc компилятора protobufinterface

type ShippingServiceClient interface {
	// 托运一批货物
	CreateConsignment(ctx context.Context, in *Consignment, opts ...grpc.CallOption) (*Response, error)
}

message: определяет формат данных связи, который генерируется компилятором protobuf после обработкиstruct

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"`
    // ...
}

внедрить сервер

Сервер должен реализоватьShippingServiceClientинтерфейс, создатьconsignment-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)
	}
}

Приведенный выше код реализует методы, требуемые микрослужбой службы доставки, и настраивает сервер gRPC, прослушивающий порт 50051. Если вы запуститеgo run main.go, успешно запустит сервер:

image-20180512051413002

внедрить клиента

Мы помещаем информацию о товарах, которые будут отправлены в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"
}

Клиент прочитает этот файл JSON и отправит груз. Создайте новый файл в каталоге проекта: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)
}

бегатьgo run main.goбежать сноваgo run cli.go:

grpc-runing

Мы можем добавить RPC для просмотра всех партий, добавитьGetConsignmentsметод, так что мы можем видеть все существующие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 {
}

беги сейчасmake buildчтобы получить последний скомпилированный интерфейс микросервиса. Если в этот момент вы запуститеgo run main.go, вы получите сообщение об ошибке, подобное этому:

image-20180512020710310

Если вы знакомы с Go, вы должны знать, что забыли реализоватьinterfaceтребуемый метод. давайте обновим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)
	}
}

Если вы сейчас используетеgo run main.go, все должно работать нормально:

image-20180512020218724

Наконец-то обновимconsignment-cli/cli.goполучитьconsignmentИнформация:

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

бежать сноваgo run cli.go, вы должны увидеть все созданныеconsignment, при нескольких запусках будет отправлено несколько отправлений:

Jietu20180512-053129-HD

На данный момент мы создали микросервис и клиент, используя protobuf и grpc.

В следующей статье мы рассмотрим использованиеgo-microframework и создание нашего второго микросервиса. Также в следующей статье мы расскажем, как разрешить Docker контейнеризовать наши микросервисы.