线上答题系统,微服务架构的小小实践,项目代码
将版本一的单应用版本分解为基于微服务架构的分模块版本,分为web应用模块,用户模块user、事件模块event、答题模块answer、题目模块problem、联合模块union6个模块。
由原来的view、controller、model三层架构,变成view、controller、service、model四层。
在单体应用中,各个模块可以通过函数互相调用。而改成微服务后,各个service可以单独的部署在不同机器上,因此通信方式有所改变。每个service对外提供能被调用的API接口,web应用或其他服务再进行调用。服务间通信和调用涉及的的思维导图如上,实现这样的API有两个方面需要考虑,通信机制和媒体风格。服务间发送请求是要同步还是异步,发送请求的内容是基于文本传输还是二进制,这些都是根据具体场景选择相应的工具。
Go 语言中常用的 API 风格是 RPC 和 REST API,常用的媒体类型是 JSON、XML 和 Protobuf。在 Go API 开发中常用的组合是 gRPC + Protobuf 和 REST + JSON。本系统使用的是gRPC + Protobuf的方式。
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
媒体类型采用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)的负载均衡,实现动态扩展节点。相比于单体式应用,更加灵活简便。
当我们发送一个请求时,需要知道服务实例的网络位置(IP+端口),在微服务架构中,服务实例具有动态分配的网络位置,且服务可以动态的增加删除,因此需要一套精准的服务发现机制,帮助每个请求找到对应处理的服务实例。服务发现机制的要点如下:
├── 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
cd /usr/local/mysql/support-files
sudo /usr/local/mysql/support-files/mysql.server start
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
安装protobuf
安装教程
只运行系统的话不需要安装,但之后有proto改动则需要该工具重新生成相应文件。
下载代码安装好相应依赖包后,启动各个模块服务
按如下命令启动用户、事件、答题、题目、积分模块的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实现一键部署