Оригинальная ссылка:ewanvalentine.io, перевод одобрен авторомEwan Valentine уполномоченный.
Полный код этой статьи:GitHub
В предыдущем разделе мы повторно реализовали и докеризовали микросервисы с помощью go-micro, но для каждого микросервиса было бы слишком громоздко поддерживать свой собственный Makefile отдельно. В этом разделе мы изучим docker-compose для унифицированного управления и развертывания микросервисов, представим третий пользовательский сервис микросервиса и сохраним данные.
MongoDB и Постгрес
Хранилище данных для микросервисов
До сих пор данные консигнации consignment-cli хранились непосредственно в памяти, управляемой службой консигнации, и эти данные будут потеряны при перезапуске службы. Для облегчения управления и поиска информации о грузе ее необходимо хранить в базе данных.
Предусмотреть отдельную базу данных для каждого независимо работающего микросервиса можно, но это мало кто делает из-за громоздкости управления. Как выбрать подходящую базу данных для различных микросервисов, см.:How to choose a database for your microservices
Выбирайте между реляционными базами данных и NoSQL
Если надежность и непротиворечивость хранимых данных не столь высоки, то NoSQL будет хорошим выбором, поскольку формат данных, который он может хранить, очень гибкий.Например, данные часто сохраняются в виде JSON для обработки.В этом разделе, выбрана производительность.И экологически совершенная MongoDB
Если данные, которые должны быть сохранены, относительно полны и отношения между данными также сильно коррелированы, можно использовать реляционную базу данных. Растяните структуру данных, которые нужно сохранить заранее, и посмотрите, следует ли больше читать или писать больше в зависимости от бизнеса? Сложны ли высокочастотные запросы? …Ввиду небольшого объема данных и операций в этой статье автор выбрал Postgres, а читатели могут заменить его на MySQL и т.д.
Больше ссылок:Как выбрать базу данных NoSQL,Разберите варианты использования реляционных баз данных и NoSQL
docker-compose
Причина введения
После того, как микрослужба была докеризована в предыдущем разделе, она запускается в упрощенном контейнере, который содержит только необходимые зависимости службы. Пока что для запуска контейнера микросервиса он находится в его Makefiledocker run
В то же время установите его переменные среды, это очень хлопотно управлять после большего количества служб.
основное использование
docker-composeинструмент может напрямую использоватьdocker-compose.yaml
Чтобы организовать несколько контейнеров и управлять ими, а также одновременно установить метаданные и среду выполнения (переменные среды) для каждого контейнера, файлservice
Элементы конфигурации приходят как раньшеdocker run
Команда для запуска контейнера. Например:
команда docker для управления контейнерами
$ docker run -p 50052:50051 \
-e MICRO_SERVER_ADDRESS=:50051 \
-e MICRO_REGISTRY=mdns \
vessel-service
Эквивалент docker-compose для управления
version: '3.1'
vessel-service:
build: ./vessel-service
ports:
- 50052:50051
environment:
MICRO_ADRESS: ":50051"
MICRO_REGISTRY: "mdns"
Если вы хотите добавлять, убирать и настраивать микросервисы, очень удобно напрямую модифицировать docker-compose.yaml.
Больше ссылок:Оркестрация контейнеров с помощью docker-compose
Оркестрация контейнера для текущего проекта
Для текущего проекта используйте docker-compose для управления тремя контейнерами и создайте новый файл в корневом каталоге проекта:
# docker-compose.yaml
# 同样遵循严格的缩进
version: '3.1'
# services 定义容器列表
services:
consignment-cli:
build: ./consignment-cli
environment:
MICRO_REGISTRY: "mdns"
consignment-service:
build: ./consignment-service
ports:
- 50051:50051
environment:
MICRO_ADRESS: ":50051"
MICRO_REGISTRY: "mdns"
DB_HOST: "datastore:27017"
vessel-service:
build: ./vessel-service
ports:
- 50052:50051
environment:
MICRO_ADRESS: ":50051"
MICRO_REGISTRY: "mdns"
Сначала мы указываем, что используемая версия docker-compose — 3.1, затем используемservices
чтобы перечислить три контейнера, которыми нужно управлять.
Каждый микросервис определяет имя своего контейнера,build
Dockerfile в указанном каталоге будет использоваться для компиляции образа или может быть использован напрямуюimage
Параметры напрямую указывают на скомпилированный образ (который будет использоваться позже), другие параметры указывают правила сопоставления портов, переменные среды и т. д. контейнера.
быть пригодным для использованияdocker-compose build
скомпилировать и сгенерировать три соответствующих изображения; использоватьdocker-compose run
для запуска указанного контейнера,docker-compose up -d
Может работать в фоновом режиме; используйтеdocker stop $(docker ps -aq )
чтобы остановить все запущенные контейнеры.
текущий результат
Эффект от использования docker-compose следующий:
Protobuf и операции с базой данных
Повторное использование и его ограничения
На данный момент два наших файла протокола protobuf определяют структуру данных запросов и ответов клиента и сервера микросервиса. Из-за нормативного характера protobuf сгенерированная структура также может использоваться в качестве модели таблицы базы данных для манипулирования данными. Такое повторное использование имеет свои ограничения: например, тип данных в protobuf должен строго соответствовать полям таблицы базы данных, а они тесно связаны. Многим людям не нравятся структуры данных protobuf как табличные структуры в базах данных:Do you use Protobufs in place of structs ?
Преобразование логики среднего уровня
Вообще говоря, после изменения структуры таблицы она несовместима с protobuf, и между ними необходимо выполнить слой логического преобразования, чтобы иметь дело с полями различий:
func (service *Service)(ctx context.Context, req *proto.User, res *proto.Response) error {
entity := &models.User{
Name: req.Name.
Email: req.Email,
Password: req.Password,
}
err := service.repo.Create(entity)
// 无中间转换层
// err := service.repo.Create(req)
...
}
Кажется удобным изолировать модели сущностей базы данных и структуры proto.* таким образом. Однако, когда в .proto определены различные вложения сообщений, модели тоже должны быть вложенными, а это проблематично.
Читателю решать, изолировать или нет, насколько я понимаю, нет необходимости использовать модели для преобразования в середине, protobuf достаточно стандартизирован и может использоваться напрямую.
рефакторинг консигнационного сервиса
Оглядываясь назад на первую микросервисную консигнационную службу, вы обнаружите, что реализация сервера, реализация интерфейса и т. д. все подключены к main.go, а функции работают.Теперь нам нужно разделить код, чтобы сделать структуру проекта более понятной. и проще в обслуживании.
Структура кода MVC
Для студентов, знакомых с моделью разработки MVC, код может быть разделен на разные каталоги по функциям, например:
main.go
models/
user.go
handlers/
auth.go
user.go
services/
auth.go
Структура кода микросервиса
Однако такой метод организации не в стиле Golang, потому что микросервисы вырезаны и независимы, и он должен быть кратким и четким. Для большого проекта Golang это должно быть организовано следующим образом:
main.go
users/
services/
auth.go
handlers/
auth.go
user.go
users/
user.go
containers/
services/
manage.go
models/
container.go
Эта организация называется управляемой категорией (доменом), а не функцией MVC.
Рефакторинг консигнационного сервиса
Из-за простоты микросервисов мы поместим весь код, относящийся к сервису, в одну папку и дадим каждому файлу соответствующее имя.
Создайте три файла в разделе consignmet-service/: handler.go, datastore.go и репозиторий.go.
consignmet-service/
├── Dockerfile
├── Makefile
├── datastore.go # 创建与 MongoDB 的主会话
├── handler.go # 实现微服务的服务端,处理业务逻辑
├── main.go # 注册并启动服务
├── proto
└── repository.go # 实现数据库的基本 CURD 操作
datastore.go отвечает за подключение к MongoDB
package main
import "gopkg.in/mgo.v2"
// 创建与 MongoDB 交互的主回话
func CreateSession(host string) (*mgo.Session, error) {
s, err := mgo.Dial(host)
if err != nil {
return nil, err
}
s.SetMode(mgo.Monotonic, true)
return s, nil
}
Код для подключения к MongoDB достаточно компактный, в качестве параметра передается адрес базы данных, а возвращается сессия базы данных и возможные ошибки, при запуске микросервис подключится к базе данных.
репозиторий.go отвечает за взаимодействие с MongoDB
Теперь разберем код, взаимодействующий с БД в main.go, в чем можно разобраться, обратившись к комментариям:
package main
import (...)
const (
DB_NAME = "shippy"
CON_COLLECTION = "consignments"
)
type Repository interface {
Create(*pb.Consignment) error
GetAll() ([]*pb.Consignment, error)
Close()
}
type ConsignmentRepository struct {
session *mgo.Session
}
// 接口实现
func (repo *ConsignmentRepository) Create(c *pb.Consignment) error {
return repo.collection().Insert(c)
}
// 获取全部数据
func (repo *ConsignmentRepository) GetAll() ([]*pb.Consignment, error) {
var cons []*pb.Consignment
// Find() 一般用来执行查询,如果想执行 select * 则直接传入 nil 即可
// 通过 .All() 将查询结果绑定到 cons 变量上
// 对应的 .One() 则只取第一行记录
err := repo.collection().Find(nil).All(&cons)
return cons, err
}
// 关闭连接
func (repo *ConsignmentRepository) Close() {
// Close() 会在每次查询结束的时候关闭会话
// Mgo 会在启动的时候生成一个 "主" 会话
// 你可以使用 Copy() 直接从主会话复制出新会话来执行,即每个查询都会有自己的数据库会话
// 同时每个会话都有自己连接到数据库的 socket 及错误处理,这么做既安全又高效
// 如果只使用一个连接到数据库的主 socket 来执行查询,那很多请求处理都会阻塞
// Mgo 因此能在不使用锁的情况下完美处理并发请求
// 不过弊端就是,每次查询结束之后,必须确保数据库会话要手动 Close
// 否则将建立过多无用的连接,白白浪费数据库资源
repo.session.Close()
}
// 返回所有货物信息
func (repo *ConsignmentRepository) collection() *mgo.Collection {
return repo.session.DB(DB_NAME).C(CON_COLLECTION)
}
разделить main.go
package main
import (...)
const (
DEFAULT_HOST = "localhost:27017"
)
func main() {
// 获取容器设置的数据库地址环境变量的值
dbHost := os.Getenv("DB_HOST")
if dbHost == ""{
dbHost = DEFAULT_HOST
}
session, err := CreateSession(dbHost)
// 创建于 MongoDB 的主会话,需在退出 main() 时候手动释放连接
defer session.Close()
if err != nil {
log.Fatalf("create session error: %v\n", err)
}
server := micro.NewService(
// 必须和 consignment.proto 中的 package 一致
micro.Name("go.micro.srv.consignment"),
micro.Version("latest"),
)
// 解析命令行参数
server.Init()
// 作为 vessel-service 的客户端
vClient := vesselPb.NewVesselServiceClient("go.micro.srv.vessel", server.Client())
// 将 server 作为微服务的服务端
pb.RegisterShippingServiceHandler(server.Server(), &handler{session, vClient})
if err := server.Run(); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
Реализовать handler.go на стороне сервера
Выделите код, который реализует интерфейс сервера микросервиса в main.go, в handler.go для реализации обработки бизнес-логики.
package main
import (...)
// 微服务服务端 struct handler 必须实现 protobuf 中定义的 rpc 方法
// 实现方法的传参等可参考生成的 consignment.pb.go
type handler struct {
session *mgo.Session
vesselClient vesselPb.VesselServiceClient
}
// 从主会话中 Clone() 出新会话处理查询
func (h *handler)GetRepo()Repository {
return &ConsignmentRepository{h.session.Clone()}
}
func (h *handler)CreateConsignment(ctx context.Context, req *pb.Consignment, resp *pb.Response) error {
defer h.GetRepo().Close()
// 检查是否有适合的货轮
vReq := &vesselPb.Specification{
Capacity: int32(len(req.Containers)),
MaxWeight: req.Weight,
}
vResp, err := h.vesselClient.FindAvailable(context.Background(), vReq)
if err != nil {
return err
}
// 货物被承运
log.Printf("found vessel: %s\n", vResp.Vessel.Name)
req.VesselId = vResp.Vessel.Id
//consignment, err := h.repo.Create(req)
err = h.GetRepo().Create(req)
if err != nil {
return err
}
resp.Created = true
resp.Consignment = req
return nil
}
func (h *handler)GetConsignments(ctx context.Context, req *pb.GetRequest, resp *pb.Response) error {
defer h.GetRepo().Close()
consignments, err := h.GetRepo().GetAll()
if err != nil {
return err
}
resp.Consignments = consignments
return nil
}
До сих пор main.go был разделен, и файлы кода четко разделены, что очень освежает.
Copy() и Clone() библиотеки mgo
В GetRepo() в handler.go мы используем Clone() для создания нового подключения к базе данных.
Вы можете видеть, что после создания основного сеанса в main.go мы больше никогда его не используем, а вместо этого используемsession.Clonse()
Чтобы создать новый сеанс для обработки запросов, см. репозиторий.goClose()
Обратите внимание: если для каждого запроса используется основной сеанс, то все запросы выполняются на одном и том же базовом сокете, а последующие запросы будут заблокированы, что не позволяет использовать преимущества естественной поддержки параллелизма в Go.
Чтобы избежать блокировки запросов, библиотека mgo предоставляетCopy()
а такжеClone()
для создания нового сеанса, они похожи по функциональности, но есть важные различия в тонкостях. Новый сеанс от Clone повторно использует сокет основного сеанса, что позволяет избежать накладных расходов на время и ресурсы трехэтапного рукопожатия при создании сокета, что особенно подходит для быстрых запросов на запись. Если выполняются сложные запросы и операции с большими объемами данных, сокет все равно будет заблокирован, что приведет к блокировке последующих запросов. Copy создает новый сокет для сеанса, что дорого.
Их следует выбирать в соответствии с различными сценариями применения.Запрос в этой статье не является ни сложным, ни большим, а сокет основного сеанса можно использовать повторно напрямую. Но вы должны закрыть() после использования, помните.
рефакторинг судового сервиса
После дизассемблирования кода consignment-service/main.go рефакторим судно-сервис таким же образом
Добавить грузовое судно
Добавляем сюда метод: добавляем новый фрахтовщик, меняем файл protobuf следующим образом:
syntax = "proto3";
package go.micro.srv.vessel;
service VesselService {
// 检查是否有能运送货物的轮船
rpc FindAvailable (Specification) returns (Response) {}
// 创建货轮
rpc Create(Vessel) returns (Response){}
}
// ...
// 货轮装得下的话
// 返回的多条货轮信息
message Response {
Vessel vessel = 1;
repeated Vessel vessels = 2;
bool created = 3;
}
мы создалиCreate()
метод для создания нового грузового судна. Параметр — Vessel и возвращает Response. Обратите внимание, что созданное поле добавляется в Response, чтобы указать, успешно ли создано. использоватьmake build
Создайте новый файл serve.pb.go.
Раздельная работа с базой данных и обработка бизнес-логики
Затем реализуйте его в соответствующем репозитории.go и handler.go.Create()
// vesell-service/repository.go
// 完成与数据库交互的创建动作
func (repo *VesselRepository) Create(v *pb.Vessel) error {
return repo.collection().Insert(v)
}
// vesell-service/handler.go
func (h *handler) GetRepo() Repository {
return &VesselRepository{h.session.Clone()}
}
// 实现微服务的服务端
func (h *handler) Create(ctx context.Context, req *pb.Vessel, resp *pb.Response) error {
defer h.GetRepo().Close()
if err := h.GetRepo().Create(req); err != nil {
return err
}
resp.Vessel = req
resp.Created = true
return nil
}
Представляем MongoDB
После рефакторинга обоих микросервисов пришло время представить MongoDB в контейнере. Добавьте опцию хранилища данных в docker-compose.yaml:
services:
...
datastore:
image: mongo
ports:
- 27017:27017
Обновите переменные среды двух микросервисов одновременно, увеличьтеDB_HOST: "datastore:27017"
, здесь мы используем datastore в качестве имени хоста вместо localhost, потому что в docker встроен надежный механизм DNS. Ссылаться на:Встроенный в Docker рабочий механизм dnsserver
Модифицированный docker-compose.yaml:
# docker-compose.yaml
version: '3.1'
services:
consigment-cli:
build: ./consignment-cli
environment:
MICRO_REGISTRY: "mdns"
consignment-service:
build: ./consignment-service
ports:
- 50051:50051
environment:
MICRO_ADRESS: ":50051"
MICRO_REGISTRY: "mdns"
DB_HOST: "datastore:27017"
vessel-service:
build: ./vessel-service
ports:
- 50052:50051
environment:
MICRO_ADRESS: ":50051"
MICRO_REGISTRY: "mdns"
DB_HOST: "datastore:27017"
datastore:
image: mongo
ports:
- 27017:27017
После изменения кода вам необходимо повторноmake build
, вам нужно построить образdocker-compose build --no-cache
перекомпилировать все.
user-service
Знакомство с Постгресом
Теперь давайте создадим третий микросервис, вdocker-compose.yaml
Представьте Постгрес:
...
user-service:
build: ./user-service
ports:
- 50053:50051
environment:
MICRO_ADDRESS: ":50051"
MICRO_REGISTRY: "mdns"
...
database:
image: postgres
ports:
- 5432:5432
Создайте каталог пользовательской службы в корневом каталоге проекта и создайте следующие файлы в такой последовательности, как первые две службы:
handler.go, main.go, repository.go, database.go, Dockerfile, Makefile
Определить файл protobuf
Создайте proto/user/user.proto со следующим содержимым:
// user-service/user/user.proto
syntax = "proto3";
package go.micro.srv.user;
service UserService {
rpc Create (User) returns (Response) {}
rpc Get (User) returns (Response) {}
rpc GetAll (Request) returns (Response) {}
rpc Auth (User) returns (Token) {}
rpc ValidateToken (Token) returns (Token) {}
}
// 用户信息
message User {
string id = 1;
string name = 2;
string company = 3;
string email = 4;
string password = 5;
}
message Request {
}
message Response {
User user = 1;
repeated User users = 2;
repeated Error errors = 3;
}
message Token {
string token = 1;
bool valid = 2;
Error errors = 3;
}
message Error {
int32 code = 1;
string description = 2;
}
Убедитесь, что ваш пользовательский сервис имеет Makefile, аналогичный первым двум микросервисам, используйтеmake build
для создания кода gRPC.
handler.go, реализующий обработку бизнес-логики
В серверном коде, реализованном handler.go, модуль аутентификации будет использовать JWT для аутентификации в следующем разделе.
// user-service/handler.go
package main
import (
"context"
pb "shippy/user-service/proto/user"
)
type handler struct {
repo Repository
}
func (h *handler) Create(ctx context.Context, req *pb.User, resp *pb.Response) error {
if err := h.repo.Create(req); err != nil {
return nil
}
resp.User = req
return nil
}
func (h *handler) Get(ctx context.Context, req *pb.User, resp *pb.Response) error {
u, err := h.repo.Get(req.Id);
if err != nil {
return err
}
resp.User = u
return nil
}
func (h *handler) GetAll(ctx context.Context, req *pb.Request, resp *pb.Response) error {
users, err := h.repo.GetAll()
if err != nil {
return err
}
resp.Users = users
return nil
}
func (h *handler) Auth(ctx context.Context, req *pb.User, resp *pb.Token) error {
_, err := h.repo.GetByEmailAndPassword(req)
if err != nil {
return err
}
resp.Token = "`x_2nam"
return nil
}
func (h *handler) ValidateToken(ctx context.Context, req *pb.Token, resp *pb.Token) error {
return nil
}
репозиторий.go для взаимодействия с базой данных
package main
import (
"github.com/jinzhu/gorm"
pb "shippy/user-service/proto/user"
)
type Repository interface {
Get(id string) (*pb.User, error)
GetAll() ([]*pb.User, error)
Create(*pb.User) error
GetByEmailAndPassword(*pb.User) (*pb.User, error)
}
type UserRepository struct {
db *gorm.DB
}
func (repo *UserRepository) Get(id string) (*pb.User, error) {
var u *pb.User
u.Id = id
if err := repo.db.First(&u).Error; err != nil {
return nil, err
}
return u, nil
}
func (repo *UserRepository) GetAll() ([]*pb.User, error) {
var users []*pb.User
if err := repo.db.Find(&users).Error; err != nil {
return nil, err
}
return users, nil
}
func (repo *UserRepository) Create(u *pb.User) error {
if err := repo.db.Create(&u).Error; err != nil {
return err
}
return nil
}
func (repo *UserRepository) GetByEmailAndPassword(u *pb.User) (*pb.User, error) {
if err := repo.db.Find(&u).Error; err != nil {
return nil, err
}
return u, nil
}
Использование UUID
Мы изменяем строку UUID, созданную ORM, на целое число, которое безопаснее использовать в качестве первичного ключа или идентификатора таблицы. MongoDB использует аналогичную технику, но Postgres требует от нас генерировать ее вручную с помощью сторонней библиотеки. существуетuser-service/proto/user
Создайте файл extension.go в каталоге:
package go_micro_srv_user
import (
"github.com/jinzhu/gorm"
uuid "github.com/satori/go.uuid"
"github.com/labstack/gommon/log"
)
func (user *User) BeforeCreate(scope *gorm.Scope) error {
uuid, err := uuid.NewV4()
if err != nil {
log.Fatalf("created uuid error: %v\n", err)
}
return scope.SetColumn("Id", uuid.String())
}
функцияBeforeCreate()
Указывает, что библиотека GORM использует uuid в качестве значения столбца ID. Ссылаться на:doc.gorm.io/callbacks
GORM
GormЭто простая в использовании и легкая структура ORM, которая поддерживает Postgres, MySQL, Sqlite и другие базы данных.
Пока три микросервиса включают небольшой объем данных и несколько операций, и их можно поддерживать с помощью собственного SQL, поэтому вам решать, хотите ли вы ORM или нет.
user-cli
Как и в случае с консигнационным сервисом, теперь создайте приложение командной строки user-cli для тестирования пользовательского сервиса.
Создайте каталог user-cli в корневом каталоге проекта и создайте файл cli.go:
package main
import (
"log"
"os"
pb "shippy/user-service/proto/user"
microclient "github.com/micro/go-micro/client"
"github.com/micro/go-micro/cmd"
"golang.org/x/net/context"
"github.com/micro/cli"
"github.com/micro/go-micro"
)
func main() {
cmd.Init()
// 创建 user-service 微服务的客户端
client := pb.NewUserServiceClient("go.micro.srv.user", microclient.DefaultClient)
// 设置命令行参数
service := micro.NewService(
micro.Flags(
cli.StringFlag{
Name: "name",
Usage: "You full name",
},
cli.StringFlag{
Name: "email",
Usage: "Your email",
},
cli.StringFlag{
Name: "password",
Usage: "Your password",
},
cli.StringFlag{
Name: "company",
Usage: "Your company",
},
),
)
service.Init(
micro.Action(func(c *cli.Context) {
name := c.String("name")
email := c.String("email")
password := c.String("password")
company := c.String("company")
r, err := client.Create(context.TODO(), &pb.User{
Name: name,
Email: email,
Password: password,
Company: company,
})
if err != nil {
log.Fatalf("Could not create: %v", err)
}
log.Printf("Created: %v", r.User.Id)
getAll, err := client.GetAll(context.Background(), &pb.Request{})
if err != nil {
log.Fatalf("Could not list users: %v", err)
}
for _, v := range getAll.Users {
log.Println(v)
}
os.Exit(0)
}),
)
// 启动客户端
if err := service.Run(); err != nil {
log.Println(err)
}
}
контрольная работа
запустить успешно
Перед этим нужно вручную вытащить образ Postgres и запустить:
$ docker pull postgres
$ docker run --name postgres -e POSTGRES_PASSWORD=postgres -d -p 5432:5432 postgres
Пользовательские данные успешно созданы и сохранены:
Суммировать
На данный момент мы создали три микросервиса: консигнационный сервис, судовой сервис и пользовательский сервис, все они реализованы и докеризованы с помощью go-micro и единообразно управляются с помощью docker-compose. Дополнительно мы используем библиотеку GORM для взаимодействия с базой данных Postgres и хранения в ней данных командной строки.
Пользовательский cli выше предназначен только для тестирования, и сохранять пароли в виде простого текста небезопасно. Основываясь на основных функциях, выполненных в этом разделе, в следующем разделе будет представлен JWT для проверки.