【go语言微服务实践】#2-进化,变身成微服务

1,338 阅读11分钟

线上答题系统,微服务架构的小小实践,项目代码

一、系统架构

  将版本一的单应用版本分解为基于微服务架构的分模块版本,分为web应用模块,用户模块user、事件模块event、答题模块answer、题目模块problem、联合模块union6个模块。

  由原来的view、controller、model三层架构,变成view、controller、service、model四层。

  • view维持原不变,与controller层通过REST API交互,这样可以方便的接受各种不同的前端访问。
  • controller层基本也没有大的变动,只是由原来的直接调用model层,变为通过RPC调用service层提供的服务。
  • service层则是将系统能提供的各个功能划分为事件服务event service、用户服务user service、问题服务problem service、答题服务answer service、联合服务union service等5个服务,对controller提供接口。每个服务service都是一个单独的应用,可以独立部署和运行,这样当某部分功能访问量增多时,可以灵活地增加该服务。保持对外提供的接口内容不变,单个服务也可以在不影响其他服务的情况下进行修改或增加新功能。service层则是直接调用mocel层进行数据库操作。
  • model层也没有什么变化,就是封装相应表的操作给service层调用。这里还进行了一下划分,除了union service外,每个service是能操作特定的model。而一些操作,如联合查询participant和event_problem、problem给用户生成题目,则放在union中。

  这其实也是工作的一点经验,当系统十分的庞大复杂时,服务变多,各个服务能对外提供的能力就会非常多,因此常常还会将各个服务按模块划分,每个模块提供一类功能。模块与模块之间为了安全或便于管理,还会有网络隔离,或者禁止模块间调用。即将系统划分成了不同的模块,每个模块下的服务提供相似的一类功能,同一模块的服务只能操作特定的数据表,模块内的服务可以互相调用,而不可以跨模块调用其他模块的服务。所有需要跨模块提供的功能,都统一放在union service中。因此用户模块就是user service,事件模块event service,题目模块problem service,答题模块由participant service、credit service共同组成,union service提供跨模块服务的调用。
技术选型
前端:planeui+jquery
controller层(页面逻辑处理):beego
service层:go-micro
model层:beego的orm
服务注册与发现:consul
数据库:mysql
其中前端与控制层基于ajax采用REST API进行通信,控制层和服务层采用gRPC+protobuf编码进行通信。

二、技术介绍

2.1 服务间通信

  在单体应用中,各个模块可以通过函数互相调用。而改成微服务后,各个service可以单独的部署在不同机器上,因此通信方式有所改变。每个service对外提供能被调用的API接口,web应用或其他服务再进行调用。服务间通信和调用涉及的的思维导图如上,实现这样的API有两个方面需要考虑,通信机制和媒体风格。服务间发送请求是要同步还是异步,发送请求的内容是基于文本传输还是二进制,这些都是根据具体场景选择相应的工具。
  Go 语言中常用的 API 风格是 RPC 和 REST API,常用的媒体类型是 JSON、XML 和 Protobuf。在 Go API 开发中常用的组合是 gRPC + Protobuf 和 REST + JSON。本系统使用的是gRPC + Protobuf的方式。

2.1.1 go-micro与gRPC

  RPC即远程过程调用,直观说法就是服务器A上的应用,想去调用服务器B上应用提供的方法/函数接口,由于不在一个内存空间无法直接调用,因此A应用通过网络来调用B应用的方法。A告诉RPC框架要调用的B的ip地址、端口、方法等,经过寻址后A与B建立连接,A将调用参数序列化成二进制的形式发送给B,B收到请求后进行反序列化,找到对应的方法进行调用,得到返回值后同样序列化发给A。A收到返回值后进行反序列化,给到当初调用的A服务器上的应用。当数据量大时,RPC的方式比REST API更高效。gRPC则是go语言中实现RPC的一个库。
  go-micro则是go语言的微服务开发框架,提供分布式系统开发的核心库,包含RPC与事件驱动的通信机制。支持服务发现(默认使用consul),服务请求负载均衡,对发送消息进行编码(如使用Protobuf编码格式),以及异步消息、可插拔接口等内容。具体特性可以在官网查看。
go-micro参考:官方文档
  go-micro中已经集成了gRPC、Protobuf和consul,所以使用这个框架可以很方便地将我们的单应用系统改成多个服务模块。我们的web应用中的controller作为RPC的客户端,各个service作为服务器端。以登陆为例,首先需要定义proto

syntax = "proto3";

service UserManage {
    rpc Login(LoginReq) returns (LoginRsp) {}
}

message LoginReq {
    string  username = 1;
    string  pwd = 2;
}

message LoginRsp {
    bool loginFlag = 1;
    int64 userId = 2;
    int32 permission = 3;
    string  token = 4;
}

在LoginController中,创建客户端

func (this *LoginController) Check() {
   username := this.GetString("username") 
   password := this.GetString("password")

   var result map[string]interface{}
   userManage,ctx := common.InitUserManage(this.CruSession)//创建新的服务
   req := userProto.LoginReq{Username: username, Pwd: password}
   LoginRsp, err := userManage.Login(ctx, &req)//调用user service提供的login方法
   
   //错误处理、设置session等,略
      ……
   //返回给前端view
   this.Data["json"] = result
   this.ServeJSON()
   return
}

func (this *Config)initServiceRegistry(serviceName string) micro.Service{
	//create service
	service := micro.NewService(micro.Name(serviceName),
		micro.RegisterTTL(time.Second*30),
		micro.RegisterInterval(time.Second*20),
		micro.Registry(consul.NewRegistry(func(options *registry.Options) {
		options.Addrs = []string{
			viper.GetString("consul.host")+":"+viper.GetString("consul.port"),
		}
	})))
	//init
	service.Init()
	return service
}

在user service中,创建服务器端

func (this *UserManage) Login(ctx context.Context, req *proto.LoginReq, rsp *proto.LoginRsp) error {
   var userName = req.Username
   var pwd = req.Pwd
   user, flag := model.Login(userName, pwd)//调用model层提供的方法
   }else{
      //类型转换
      rsp.UserId = -1
      rsp.LoginFlag = flag
      rsp.Permission = 0
   }
   return nil
}

func main() {
    // 创建新的服务,具体见下方方法
   service,err := common.initServiceRegistry("UserManage")
   if err != nil {
      panic(err)
   }

   // 注册处理器
   proto.RegisterUserManageHandler(service.Server(), new(UserManage))

   //运行这个user service服务
   if err := service.Run(); err != nil {
      logs.Error("failed-to-do-somthing", err)
   }
}

func (this *Config)initServiceRegistry(serviceName string) micro.Service{
   //create service
   service := micro.NewService(micro.Name(serviceName),
      micro.RegisterTTL(time.Second*30),
      micro.RegisterInterval(time.Second*20),
      micro.Registry(consul.NewRegistry(func(options *registry.Options) {
      options.Addrs = []string{
         viper.GetString("consul.host")+":"+viper.GetString("consul.port"),
      }
   })))

   //init
   service.Init()
   return service
}

  各个模块都按照这个思路进行改造即可。答题模块answer service最后会有两个服务,分别对应ParticipantManage.go和CreditManage.go

2.1.2 Protobuf

  媒体类型采用Protocol Buffers 序列化方法,使用的是GO语言中的protobuf库。Protocol Buffers 类似于xml,可以将发送内容序列化,即将结构数据或对象转换成能够被存储和传输(例如网络传输)的格式,同时保证这个序列化结果在之后(可能在另一个计算环境中)能够被重建回原来的结构数据或对象。相比XML,它更小(3 ~ 10倍)、更快(20 ~ 100倍)、更为简单。
入门例子可见:入门实例

生成命令:进入protoc --proto_path=. --micro_out=. --go_out=. answer_system.proto(proto文件路径)

  每个服务采用protobuf格式,定义好他能对外提供的服务接口、出参、入参。调用方在调用时,也使用protobuf格式编辑好传入该服务的参数。一个“获取系统用户列表”的例子如下:
原写法:view层—(ajax)—>controller层—(方法调用)—>model层

新写法:view层—(ajax)—>controller层—(gRPC+Protocol)—>service层—(方法调用)—>model层
  这样来看,前端使用ajax与后端controller通信不变,实际上是多增加了一个service层,controller或其他service通过gRPC+protobuf来调用service提供的接口,service中再调用model获取数据库数据。service则对外提供服务,注册在服务注册中心中(如consul)。controller层没有处理逻辑,只是获取前端传入参数,调用相应service层提供的接口。当获取用户列表的请求增多时,可增加该user service,相当于增加了处理请求的节点,通过服务发现功能(如consul)的负载均衡,实现动态扩展节点。相比于单体式应用,更加灵活简便。

2.2 服务发现与consul

  当我们发送一个请求时,需要知道服务实例的网络位置(IP+端口),在微服务架构中,服务实例具有动态分配的网络位置,且服务可以动态的增加删除,因此需要一套精准的服务发现机制,帮助每个请求找到对应处理的服务实例。服务发现机制的要点如下:

  • 客户端发现模式:客户端负责确定可用服务实例的网络位置和请求负载均衡。客户端查询服务注册中心(service registry),利用负载均衡算法选择一个可用的服务实例并发出请求。
  • 服务端发现模式:客户端通过负载均衡器向服务发出请求。负载均衡器查询服务注册中心并将每个请求路由到可用的服务实例。与客户端发现一样,服务实例由服务注册中心注册与销毁。
  • 服务注册中心:一个包含了服务实例网络位置的数据库。
  • 自注册模式:服务实例负责在服务注册中心注册和注销自己。
  • 第三方注册模式:服务注册器通过轮询部署环境或订阅事件来跟踪运行实例集的变更情况。当它检测到一个新的可用服务实例时,它会将该实例注册到服务注册中心。此外,服务注册器可以注销终止的服务实例。

consul参考:consul官方文档
  本系统使用go-micro框架,默认使用consul作服务注册中心,采用服务端发现方式。即每个服务将自己的网络位置(ip+port)注册到consul中,当客户端(某个服务或web)想调用某个服务时,就在consul获取该服务可用的网络地址进行调用。同一功能的服务可以启动并注册多个,由consul进行负载均衡。
具体go-micro和consul的运行机制见:(施工中)

三、代码结构说明

  1. github版本: 5834b4bbdd93ded7250c80d065fc1fa3809a0ffa 、[feat]microservice version
  2. 代码结构说明:
├── conf#配置文件位置
│   └── config.yaml
├── service#各个service应用的代码,包括CreditManage、ParticipantManage、EventManage、ProblemManage、unionManage、UserManage,对应go文件单独运行
│   ├── answer
│   │   ├── CreditManage.go
│   │   ├── ParticipantManage.go
│   │   └── model
│   │       ├── creditLog.go
│   │       ├── participant.go
│   │       ├── participantHavedAnswer.go
│   │       └── team.go
│   ├── common#通用方法
│   │   ├── config.go
│   │   ├── token.go
│   │   └── wapper.go
│   ├── event
│   │   ├── EventManage.go
│   │   └── model
│   │       ├── event.go
│   │       └── eventProblem.go
│   ├── problem
│   │   ├── ProblemManage.go
│   │   └── model
│   │       └── problem.go
│   ├── protoc#定义的protoc文件,包括creditManage.proto、participantManage.proto、 eventManage.proto、problemManage.proto、unionManage.proto、userManage.proto等
│   ├── union
│   │   ├── model
│   │   │   └── union.go
│   │   └── unionManage.go
│   └── user
│       ├── UserManage.go
│       └── model
│           └── user.go
└── web
    ├── Dockerfile
    ├── common
    │   └── common.go
    ├── conf#配置文件位置
    │   ├── app.conf
    │   └── config.go
    ├── controllers#controller层
    │   ├── AnswerController.go#答题模块
    │   ├── EventManageController.go#事件模块
    │   ├── EventMessageController.go#事件模块
    │   ├── LoginController.go#用户模块
    │   ├── ParticipantManageController.go#答题模块
    │   ├── ProblemManageController.go#题目模块
    │   ├── UserIndexController.go#用户模块
    │   └── UserManageController.go#用户模块
    ├── main.go
    ├── models#数据库model层,已删除,被移到service中
    │   └── db.go
    ├── routers#路由
    │   └── router.go
    ├── sql#建表sql
    │   └── problem.sql
    ├── static#静态文件,如引用的js库,图片等,以及上传文件的存放位置
    ├── views#前端页面存放
    └── web

四、运行项目

  1. 安装并启动mysql数据库,运行项目中的web/problem.sql建表
cd /usr/local/mysql/support-files
sudo /usr/local/mysql/support-files/mysql.server start
  1. 安装并开启consul
    安装教程
cd /Users/gan/Documents/software(consul安装目录)
consul agent -server -node=answer_system -bind=127.0.0.1 -data-dir /tmp/consul -bootstrap-expect 1 -ui
  1. 安装protobuf
    安装教程
    只运行系统的话不需要安装,但之后有proto改动则需要该工具重新生成相应文件。

  2. 下载代码安装好相应依赖包后,启动各个模块服务
    按如下命令启动用户、事件、答题、题目、积分模块的go文件

cd AnswerSystem_go/src/service/user
go run UserManage.go
启动前端页面
cd AnswerSystem_go/src/web
bee run

  最终可以在consul中看到注册的服务,每个服务可以注册多个。当web应用发送请求时,就会从consul获取到一个可用的服务。
http://localhost:8500/ui/dc1/services

前端的访问和使用与版本一相同

五、总结

  现在已经初步将系统拆解成了几个服务,web应用和服务间通过RPC通信,有那么点微服务的样子了。但微服务的架构还有许多要考虑的方面,如各个服务的部署、接口安全认证、监控、链路跟踪、熔断限流、分布式日志分析等等。接下来需要实践的就是微服务的部署,结合docker实现更简单方便的部署方式。
继续了解请戳:【go语言微服务实践】#3-docker实现一键部署