go-micro
一,课程介绍
1,主讲老师: 大地
2,合作网站: www.itying.com
3,我的专栏: https://www.itying.com/category_Z9-b0.html
4,必备基础:学习本教程要有golang和go web基础
5,大地老师Golang入门实战系列教程地址:
Golang教程_IT营
二,微服务架构(micro services)
2.1 微服务架构和微服务
微服务架构:微服务架构是一种具体的设计实现或者设计方案,是将复杂的系统使用组件化的方式进行撤分,并使用轻量级通讯方式进行整合的一种设计方法。
微服务:微服务是微服务架构具体的实现方案,是通过微服务架构设计方法撤分出来的一个独立的组件化的小应用。
微服务架构定义的精髓,可以用一句话来描述,那就是"分而治之,合而用之"。将复杂的系统进行撤分的方法,就是"分而治之"。分而治之,可以让复杂的事情变的简单,这很符合我们平时处理问题的方法,使用轻量级通讯等方式进行整合的设计,就是"合而用之"的方法,合而用之可以让微小的力量变动强大。
2.2 什么是微服务架构
微服务架构是将一个单一应用程序开发为一组小型服务的方法,每个服务运行在自己的进程中,服务间通信采用的轻量级通信机制(通常用HTTP资源API),这些服务围绕业务能力构建并且可通过全自动部署机制独立部署。这些服务公用一个最小型的集中式的管理,服务可用不同的语言进行开发,使用不同的数据存储技术。
在了解微服务之前首先看看单体架构。
单体架构在中小企业内部用的是非常多的,当业务部复杂,团对规模不大的时候,单体架构比微服务架构具有更高的生产率。我们给大家讲的《golang仿小米商城项目》以及2017年前的淘宝都是单体架构。
注意 :下面示例中的服务器和数据库也可以使用docker容器实现
2.2.1 单体架构的程序部署在单台服务器 每天五万下访问量
这样的架构能轻松的应对每天五万以下的访问量
这种架构是目前中小企业用的最多的架构。其中web服务(nginx),网站程序,静态资源(图片),数据库(Mysql,Redis)都在一台服务器上面。如果每天网站的访问ip在5w以下这种架构完全可以应付(服务器配置也有关系)
2.2.2 单体架构的程序部署在多台服务器(负载均衡) 每天几十万的访问量
这样的架构能轻松的应对每天几十万的访问量
把我们的程序部署到多台服务器上面,然后通过nginx配置负载均衡,当客户访问我们的项目的时候随机的分配给不同的服务器处理响应,这样可以防止宕机,提升系统运行稳定性。
2.2.3 单体架构的程序部署在多台服务器(负载均衡+主从数据库)
这样的架构能轻松的应对每天几百万,上千万的访问量
当每天有上亿访问量,或者更高并发量的时候,什么的方法就有点力不从心了,这个时候我们就可以使用微服务架构。
2.2.4 , 微服务架构
微服务架构:通俗的讲就是把单体架构项目抽离成多个项目(服务),部署到多台服务器。
如果用"茶壶煮饺子"来打比方发话,原来我们是在一个茶壶煮很多个饺子,现在(微服务化之后)则基本上是在一个茶壶煮一个饺子,而这些饺子就是服务的功能,茶壶则是将这些服务功能打包交付的服务单元。
2.3 微服务这个概念的由来
据说,早在2011年5月,在威尼斯附近的软件架构师讨论会上,就有人提出了微服务架构设计的概念,用它来描述与会者所见的一种通用的架构设计风格。时隔一年之后,在同一个讨论会上,大家决定将这种架构设计风格用微服务架构来表示。
起初,对微服务的概念,没有一个明确的定义,大家只能从各自的角度说出了微服务的理解和看法。
在2014年3月,詹姆斯.刘易斯(James Lewis)与马丁福勒(Martin Fowler)所发表的一篇博客中,总结了微服务架构设计的一些共同特点,这应该是一个对微服务比较全面的描述。
原文链接:https://martinfowler.com/articlers/microservices.html
2.4 微服务架构和单体式架构区别
2.4.1 单体架构服务
单体架构的优点:
1,部署简单: 由于是完整的结构体,可以直接部署在一个服务器上即可
2,技术单一:项目不需要复杂的技术栈,往往一套熟悉的技术栈就可以完成开发
3,用人成本低:单个程序员可以完成接口到数据库的整个流程
4,项目管理相对较易
5,测试相对简单直观
6,应用开发相对简单
7,横向扩展容易
单体架构的缺点:
1,系统启动慢,一个进程包含了所有的业务逻辑,涉及到启动模块过多,导致系统的启动,重启周期变长
2,系统错误隔离性差,可用性差,任何一个模块的错误可能导致整个系统的宕机
3,可伸缩性差,系统的扩容只能对整个应用扩容,不能做到对整个功能点进行扩容
4,线上问题修复时间长,任何一个线上问题修复需要对整个应用系统进行全面升级
5,交付周期长(需求-->设计-->开发-->测试-->现场实施部署,就传统性质的企业而言)
2.4.2 微服务
微服务的优点:
1,易于开发和维护:一个服务只关注一个特定的业务功能,所以他业务清晰,代码量少,开发和维护单个微服务相当简单。而整个应用是若干个微服务构建而成的,所以整个应用在被维持在一个可控的状态;
2,单个服务启动快:单个服务代码量少,所以启动快;
3,局部修改易部署:单个应用只要有修改,就得重新部署整个应用,微服务解决了这个问题,一般来说,对某个微服务进行修改,只需要重新部署这个服务即可;
4,技术栈不受限:在微服务架构中,可以结合业务和团队的特点,合理选用技术栈。例如有些服务可以使用关系型数据库Mysql,有的服务可以使用非关系型数据库redis,甚至可根据需求,部分服务使用JAVA开发,部分微服务使用Node.js开发。
5,按需收缩:可根据需求,实现细粒度的扩展。例如,系统中某个微服务遇到了瓶颈,可以结合微服务的特点,增加内存,升级CPU或增加节点。
微服务的缺点:
1,运维成本高
2,分布式复杂度高
3,
2.5 为什么要学微服务
1,会单体架构学习微服务非常简单
2,微服务是非常热门的话题,企业招聘中越来越多的要求有委屈开发,架构能力的人才
3,提升技术实力,增加职业转型的可能性
4,微服务解决工作中软件研发难题,比如高并发
5,微服务技术栈不受限,可以发布的和其他语言实现通信
6,如果你是架构师或者项目管理人员微服务是必备技能
三,RPC架构
简单了解微服务定义和优缺点之后,在我们正式学习微服务框架之前,需要首先了解一下RPC架构,通过RPC我们可以形象了解微服务的工作流程。
3.1 RPC的概念
RPC(Remote Procedure Call Protocoll),是远程过程调用的缩写,通俗的说就是调用远处的一个函数。与之相对应的是本地函数调用,我们先来看一下本地函数调用。当我们写下如下代码的时候:
规则
result := Add(1,2)
我们知道,我们传入了1,2两个参数,调用了本地代码中的一个Add函数,得到result这个返回值。这个时参数,返回值,代码段都在一个进程空间内,这是本地函数调用。
那有没有办法,我们能够调用一个跨进程(所以叫"远程",典型的实例,这个进程部署在另一台服务器上)的函数呢?
这就是RPC主要实现的功能,也是微服务的主要功能。
3.2 RPC入门
我们使用微服务化的一个好处就是:
1,不限定服务的提供方使用什么技术选型,能够实现公司跨团队的技术解耦,
2,每个服务都被封装成进程。彼此"独立"。
3,使用微服务可以跨进程通信
RPC协议可以让我们实现不同语言的直接相互调用。在互联网时代,RPC已经和IPC(进程间通信)一样成为不可或缺的基础构件。
IPC:进程间通信
RPC:远程进程间通信--应用层协议(http协议同层)。底层使用TCP实现。
在 golang 中实现RPC非常简单,有封装好的官方库和一些第三方库提供支持。Go RPC可以利用 tcp 或http来传递数据,可以对要传递的数据使用多种类型的编解码方式。golang官方的 net/rpc 库使用 encoding/gob进行编解码,支持tcp或http数据传输方式,由于其他语言不支持gob编解码方式,所以使用 net/rpc 库实现的RPC方法没办法进行跨语言调用。
golang官方还提供了net/rpc/jsonrpc库实现RPC方法,JSON RPC采用JSON进行数据编解码,因而支持跨语言调用。但目前的jsonrpc库是基于tcp协议实现的,暂不支持使用http进行数据传输。
除了 golang 官方提供的rpc库,还有许多第三方库为在golang中实现RPC提供支持,大部分第三方rpc库的实现都是使用protobuf进行数据编解码,根据protobuf声明文件自动生成rpc方法定义域服务注册代码,在 golang 中可以很方便的进行RPC服务调用。
3.3 net/rpc库 使用 http 作为 RPC 的载体实现远程调用(了解)
下面的例子演示一下如何使用 golang 官方的 net/rpc 库实现 RPC 方法,使用 http 作为 RPC 的载体,通过 net/http 包监听客户端连接请求。
http基于tcp,多一层封包和几次握手校验,性能自然比直接用tcp实现网络传输要差一些,所以 RPC 微服务中一般使用的都是tcp
3.3.1 创建RPC微服务端
新建 rpc_demo_http/server/main.go
package main
import (
"fmt"
"log"
"net"
"net/http"
"net/rpc"
"os"
)
// 定义类对象
type world struct {}
// 绑定类方法
func (this *world) Helloworld(rep string,res *string) error {
*res = req + "您好!"
return nil
// return errors.New("未知的错误!")
}
3.4.1 创建RPC微服务端
新建 rpc_demo_tcp/server/main.go
package main
import (
"fmt"
"net"
"net/rpc"
)
// 定义类对象
type World struct {}
// 绑定类方法
func (this *world) Helloworld(req string,res *string) error {
*res = req + "您好!"
return nil
}
func main() {
// 1. 注册RPC服务
err := rpc.RegisterName("hello",new(world))
if err != nil {
fmt.Println("注册 rpc 服务失败!",err)
return
}
// 2,设置监听
listener,err := net.Listen("tcp","127.0.0.1:8080")
if err != nil {
fmt.Println("net.Listen err:",err)
return
}
defer listener.Close()
fmt.Println("开始监听 ...")
// 3,建立链接
for {
// 接收连接
conn,err := listener.Accept()
if err != nil {
fmt.Println("Accept() err:",err)
return
}
// 4, 绑定服务
go rpc.ServeConn(conn)
}
}
HelloWorld 方法必须满足Go语言的RPC规则:
1,方法只能有两个可序列化的参数,其中第二个参数是指针类型
2,方法要返回一个error类型,同时必须是公开的方法。
golang 中的类型比如:channel(通道),complex(复数类型),func(函数)均不能进行 序列化
3.4.2 创建RPC客户端
新建 rpc_demo_tcp/client/main.go
package main
import (
"fmt"
"net/rpc"
)
func main() {
// 1,用 rpc 链接服务器 --Dial()
conn, err := rpc.Dial("tcp","127.0.0.1:8800")
if err != nil {
fmt.Println("Dial err:",err)
return
}
defer conn.Close()
// 2,调用远程函数
var reply string // 接受返回值---传出参数
/*
1,第一个参数 hello.SayHello hello表示服务名称 SayHello方法名称
2,第二个参数 给服务端的req传递数据
3,第三个参数 需要传入地址 获取微服务端返回的数据
*/
err = conn.Call("hello.Helloworld","张三",&reply)
if err != nil {
fmt.Println("Call:",err)
return
}
fmt.Println(reply)
}
说明:首选是通过 rpc.Dial拨号RPC服务,然后通过client.Call调用具体的RPC方法。在调用client.Call时,第一个参数是用点号链接的RPC服务名字和方法名字,第二和第三个参数分别我们定义RPC方法的两个参数。
以下是一个实现简单RPC服务实践
1,服务端 D:/go_micro/rpc_demo/server/hello/main.go
package main
import (
"fmt"
"net"
"net/rpc"
)
// 定义一个远程调用的方法 服务器端程序
// 1,定义一个结构体
type Hello struct {
}
/*
定义一个方法 方法只能有两个序列化的参数 其中第二个参数必须是指针类型
第一个参数是客户端传过来的数据,第二个参数是服务器端返回给客户端的数据
该方法必须有一个error类型的返回值,用来表示是否调用成功
request:表示获取客户端传过来的数据
response:表示服务器端返回给客户端的数据
req 和 res 中的类型比如:channel(通道),complex(复数类型),
func(函数)均不能进行 序列化操作,所以不能作为rpc方法的参数类型
*/
func (Hello) SayHello(request string, response *string) error {
fmt.Println("服务端收到客户端的数据:", request)
*response = "Hello,我是服务端," + request
return nil
}
func main() {
// 1, 注册RPC服务
err1 := rpc.RegisterName("Hello", new(Hello))
if err1 != nil {
fmt.Println("注册服务失败")
}
// 2,监听端口,等待客户端的连接
Listener, err2 := net.Listen("tcp", "127.0.0.1:8080")
if err2 != nil {
fmt.Println("监听端口失败")
}
// 3,应用退出前关闭监听端口
defer Listener.Close()
// 4,建立链接 循环等待客户端的连接请求
for { // 死循环等待客户端的连接请求 for循环等待客户端的连接请求
fmt.Println("开始建立连接")
// 4,建立一次链接
conn, err3 := Listener.Accept()
if err3 != nil {
fmt.Println("接受客户端失败")
}
// 5,绑定服务 为当前链接提供服务
rpc.ServeConn(conn)
fmt.Println("服务端退出")
}
}
2,客户端 D:/go_micro/rpc_demo/client/main.go
package main
import (
"fmt"
"net/rpc"
)
func main() {
// 1,用 rpc.Dial和rpc 微服务端建立连接,然后调用 rpc 微服务端的方法。
conn, err1 := rpc.Dial("tcp", "127.0.0.1:8080") // 这里的参数要和服务器端的一致
if err1 != nil {
fmt.Println(err1)
}
// 2, 当连接建立后,需要关闭它。否则会造成资源泄露。所以这里使用了 defer 关键字来延迟执行 Close 方法。
defer conn.Close()
// 3, 调用远程函数
var reply string
/*
1,第一个参数 hello.SayHello hello表示服务名称 SayHello方法名称
2,第二个参数 给服务端的req传递数据
3,第三个参数 需要传入地址 获取微服务端返回的数据
*/
err2 := conn.Call("Hello.SayHello", "我是客户端的曾国清", &reply)
if err2 != nil {
fmt.Println(err2)
}
// 4, 获取微服务返回的数据 查看返回结果
fmt.Println(reply)
}
以下是一个goods实现简单RPC服务实践
1, 服务端 D:\go_micro\rpc_demo\goods_server\main.go
package main
import (
"fmt"
"net"
"net/rpc"
)
// 创建远程调用的函数,函数一般是放在结构体里面的
type Goods struct{}
// AddGoods 参数对应的结构体
// 定义一个结构体,用于接收客户端传递过来的参数
type AddGoodsReq struct {
Id int
Title string
Price float64
Content string
}
// 定义一个结构体,用于返回客户端的结果
type AddGoodsRes struct {
Success bool
Message string
}
// GetGoods 参数对应的结构体
// 定义一个结构体,用于接收客户端传递过来的参数
type GetGoodsReq struct {
Id int
}
// 定义一个结构体,用于返回客户端的结果
type GetGoodsRes struct {
Id int
Title string
Price float64
Content string
}
// 定义一个方法,该方法可以被远程调用
func (Goods) AddGoods(req AddGoodsReq, res *AddGoodsRes) error {
// 1, 执行增加 模拟 商品的操作,这里就直接打印出来看看参数是什么样子的
fmt.Printf("%#v\n", req)
// 2, 返回增加的结果,这里直接返回nil表示成功
*res = AddGoodsRes{
Success: true,
Message: "增加商品成功",
}
return nil
}
// 定义一个获取数据的方法,该方法可以被远程调用
func (Goods) GetGoods(req GetGoodsReq, res *GetGoodsRes) error {
// 1, 执行增加 模拟 商品的操作,这里就直接打印出来看看参数是什么样子的
fmt.Printf("%#v\n", req)
// 2, 返回增加的结果,这里直接返回nil表示成功
*res = GetGoodsRes{
Id: 11,
Title: "小米手机",
Price: 2888.0,
Content: "小米手机真好用号漂亮的",
}
return nil
}
func main() {
// 1, 注册 RPC 服务
err1 := rpc.RegisterName("goods", new(Goods))
if err1 != nil {
fmt.Println("注册服务失败")
}
// 2, 监听端口
Listener, err2 := net.Listen("tcp", "127.0.0.1:8020")
if err2 != nil {
fmt.Println("监听端口失败")
}
// 3, 关闭监听端口
defer Listener.Close()
for {
// 4, 监听客户端连接,等待客户端的连接请求
fmt.Println("准备接受客户端的连接请求...")
conn, err3 := Listener.Accept()
if err3 != nil {
fmt.Println("接受客户端连接失败")
}
// 5, 处理客户端的连接请求
rpc.ServeConn(conn) // 处理客户端的连接请求,阻塞等待下一个连接请求
}
}
2,客户端 D:\go_micro\rpc_demo\goods_client\main.go
package main
import (
"fmt"
"net/rpc"
)
// AddGoods 参数对应的结构体
// 定义一个结构体,用于接收客户端传递过来的参数
type AddGoodsReq struct {
Id int
Title string
Price float64
Content string
}
// 定义一个结构体,用于返回客户端的结果
type AddGoodsRes struct {
Success bool
Message string
}
// GetGoods 参数对应的结构体
// 定义一个结构体,用于接收客户端传递过来的参数
type GetGoodsReq struct {
Id int
}
// 定义一个结构体,用于返回客户端的结果
type GetGoodsRes struct {
Id int
Title string
Price float64
Content string
}
func main() {
// 1,用 rpc.Dial和rpc 微服务端建立连接,然后调用 rpc 微服务端的方法。
conn, err1 := rpc.Dial("tcp", "127.0.0.1:8020") // 这里的参数要和服务器端的一致
if err1 != nil {
fmt.Println(err1)
}
// 2, 当连接建立后,需要关闭它。否则会造成资源泄露。所以这里使用了 defer 关键字来延迟执行 Close 方法。
defer conn.Close()
// 注意:这里的 goods.AddGoods 指的是微服务端的方法名,要和微服务端的注册方法一致。
// 3, 调用远程函数AddGoodsRes
var reply AddGoodsRes
/*
1,第一个参数 hello.SayHello hello表示服务名称 SayHello方法名称
2,第二个参数 给服务端的req传递数据
3,第三个参数 需要传入地址 获取微服务端返回的数据
*/
err2 := conn.Call("goods.AddGoods", AddGoodsReq{
Id: 1,
Title: "小米手机",
Price: 2999.0,
Content: "小米手机真好用",
}, &reply)
if err2 != nil {
fmt.Println(err2)
}
// 4, 获取微服务返回的数据 查看返回结果
fmt.Printf("%#v\n", reply)
// 5, 调用远程函数GetGoodsRes
var goodsData GetGoodsRes
err3 := conn.Call("goods.GetGoods", GetGoodsReq{
Id: 11,
}, &goodsData)
if err3 != nil {
fmt.Println(err3)
}
// 6, 获取微服务返回的数据 查看返回结果
fmt.Printf("%#v\n", goodsData)
}