✨✨ 欢迎大家来到景天科技苑✨✨
🎈🎈 养成好习惯,先赞后看哦~🎈🎈
🏆 作者简介:景天科技苑
🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。
🏆《博客》:Python全栈,Golang开发,PyQt5和Tkinter桌面开发,小程序开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi,flask等框架,云原生K8S,linux,shell脚本等实操经验,网站搭建,数据库等分享。所属的专栏:Go语言微服务之grpc
景天的主页:景天科技苑
文章目录
go微服务
一、微服务
近几年,微服务这个词非常火。在百度与谷歌中随便一搜就有几千万条的结果。那么什么是微服务呢?微服务的概念是怎么产生的?
我们首先来了解一下Go语言与微服务千丝万缕的关系
1.1 什么是微服务
服务拆分原则:高内聚,低耦合
在介绍微服务时,首先得先理解什么是微服务,顾名思义,微服务得从两个方面去理解,什么是"微"、什么是”服务"?
微(micro)狭义来讲就是体积小,著名的"2 pizza 团队"很好的诠释了这一解释(2 pizza 团队最早是亚马逊 CEO Bezos提出来的,
意思是说单个服务的设计,所有参与人从设计、开发、测试、运维所有人加起来 只需要2个披萨就够了)。
服务(service)一定要区别于系统,服务一个或者一组相对较小且独立的功能单元,是用户可以感知最小功能集。
那么广义上来讲,微服务是一种分布式系统解决方案,推动细粒度服务的使用,这些服务协同工作。
1.2 微服务这个概念的由来
据说,早在19xx,非IT行业提出的概念,在威尼斯附近的软件架构师讨论会上,就有人提出了微服务架构设计的概念,用它来描述与会者所见的一种通用的架构设计风格。
时隔一年之后,在同一个讨论会上,大家决定将这种架构设计风格用微服务架构来表示。
起初,对微服务的概念,没有一个明确的定义,大家只能从各自的角度说出了微服务的理解和看法。
在2014年3月,詹姆斯·刘易斯(ames Lewis)与马丁·福勒(Martin Fowler)所发表的一篇博客总结了微服务架构设计的一些共同特点,这应该是一个对微服务比较全面的描述。
原文链接 https://martinfowler.com/articles/microservices.html
这篇文章中认为:“简而言之,微服务架构风格是将单个应用程序作为一组小型服务开发的方法,每个服务程序都在自己的进程中运行,并与轻量级机制(通常是HTTP资源API)进行通信。
这些服务是围绕业务功能构建的。可以通过全自动部署机器独立部署。这些服务器可以用不同的编程语言编写,使用不同的数据存储技术,并尽量不用集中式方式进行管理
在这里我们可能会混淆一个点,那就是微服务和微服务架构,这是两个不同的概念,而我们平时说到的微服务已经包含了这两个概念了,我们需要把它们说清楚以免学习中纠结。
微服务架构是一种设计方法而微服务这是指使用这种方法而设计的一个应用。所以我们必要对微服务的概念做出一个比较明确的定义。
微服务架构是将复杂的系统使用组件化的方式进行拆分,并使用轻量级通讯方式进行整合的一种设计方法。
微服务是通过这种架构设计方法拆分出来的一个独立的组件化小应用
微服务架构定义的精髓,可以用一句话来描述,那就是 分而治之,合而用之。将复杂的系统进行拆分的方法,就是分而治之。分而治之,可以让复杂的事情变的简单,这很符合我们平时处理问题的方法。
使用功能轻量级通讯方式进行整合的设计,就是合而用之的方法,可以让微小的力量变得强大。
1.3 微服务和单体式架构的区别
和微服务架构相反的就是单体式架构,我们来看看单体式架构设计的缺点,就更能体会微服务的好处了。
单体架构在规模比较小的情况下工作情况良好,但是随着系统规模的扩大,它暴露出来的问题也越来越多,主要有以下几点:
复杂性逐渐变高
中软国际 boss计费系统十几年了移动联通缴费平台几个亿自己封装函数,代码冗余度特别大
比如有几十万行代码的大项目,代码越多复杂性越高,越难解决遇到的问题。
技术债务逐渐上升
离职证明 留下了64个bug未解决就离职了
公司的人员流动是再正常不过的事情,有的员工在离职之前,疏于代码质量的自我管束,导致留下来很多坑,由于单体项目代码量庞大的惊人,留下的坑很难被发觉,这就给新来的员工带来很大的烦恼人员流动越大所留下的坑越多,也就是所谓的技术债务越来越多
耦合度太高,维护成本大
团队越来越大时,沟通成本、管理成本显著增加。当出现 bug 时,可当应用程序的功能越来越多能引起 bug的原因组合越来越多,导致分析、定位和修复的成本增加;并且在对全局功能缺乏深度理解
的情况下,容易在修复bug时引入新的bug。
持续交付周期长
构建和部署时间会随着功能的增多而增加,任何细微的修改都会触发部署流水线。新人培养周期长:新成员了解背景、熟悉业务和配置环境的时间越来越长。
技术选型成本高
单块架构倾向于采用统一的技术平台或方案来解决所有问题,如果后续想引入新的技术或框架,成本和风险都很大。
扩展性差
随着功能的增加,垂直扩展的成本将会越来越大;而对于水平扩展而言,因为所有代码都运行在同一个进程,没办法做到针对应用程序的部分功能做独立的扩展。
了解了单体式结构的缺点之后,我们来看看微服务架构的解决方案:
- 单一职责
微服务架构中的每个服务,都是具有业务逻辑的,符合高内聚、低耦合原则以及单一职责原则的单元,不同的服务通过“管道”的方式灵活组合,从而构建出庞大的系统。 - 轻量级通信
服务之间通过轻量级的通信机制实现互通互联,而所谓的轻量级,通常指语言无关、平台无关的交互方式。
对于轻量级通信的格式而言,我们熟悉的 XML 和 JSON,它们是语言无关、平台无关的;对于通信的协议而言,通常基于 HTTP,能让服务间的通信变得标准化、无状态化。
使用轻量级通信机制,可以让团队选择更适合的语言、工具或者平台来开发服务本身。
- 独立性
每个服务在应用交付过程中,独立地开发、测试和部署。
在单体式架构中所有功能都在同一个代码库,功能的开发不具有独立性;当不同小组完成多个功能后,需要经过集成和回归测试,测试过程也不具有独立性;当测试完成后,应用被构建成一个包,如果某个功能存在 bug,将导致整个部署失败或者回滚。
在微服务架构中,每个服务都是独立的业务单元,与其他服务高度解耦,只需要改变当前服务本身,就可以完成独立的开发、测试和部署。
- 进程隔离
单块架构中,整个系统运行在同一个进程中,当应用进行部署时,必须停掉当前正在运行的应用,部署完成后再重启进程,无法做到独立部署。
在微服务架构中,应用程序由多个服务组成,每个服务都是高度自治的独立业务实体,可以运行在独立的进程中,不同的服务能非常容易地部署到不同的主机上。
微服务的缺点:
微服务这么多好处,就没有缺点吗?当然不是这样的,事务都有两面性,我们来看看微服务的不足之处。
- 运维要求比较高
对于单体架构来讲,我们只需要维护好这一个项目就可以了,但是对于微服务架构来讲,由于项目是由多个微服务构成的,每个模块出现问题都会造成整个项目运行出现异常,
想要知道是哪个模块造成的问题往往是不容易的,因为我们无法一步一步通过debug的方式来跟踪,这就对运维人员提出了很高的要求。 - 分布式的复杂性
对于单体架构来说,分布式是用来优化项目的,可有可无,但是对于微服务来说,分布式几乎是必会用的技术,由于分布式本身的复杂性,导致微服务架构也变得复杂起来。bug不好调试
- 接口成本高
比如我们的前面的电商项目每个模块做成微服务的话,用户微服务是要被订单微服务和购物车微服务所调用的,一旦用户微服务的接口发生大的变动,那么所有依赖它的微服务都要做相应的调整,由于微服务可能非常多,那么调整接口所造成的成本将会明显提高。 - 重复劳动
对于单体架构来讲,如果某段业务被多个模块所共同使用,我们便可以抽象成一个工具类,被所有模块直接调用,但是微服务却无法这样做,因为这个微服务的工具类是不能被其它微服务所直接调用的,从而我们便不得不在每个微服务上都建这么一个工具类,从而导致代码的重复。 - 业务不好分离
要看程序员的业务理解程度
既然微服务也有这么多的缺点,那为什么还要用微服务架构呢?
- 开发简单
微服务架构将复杂系统进行拆分之后,让每个微服务应用都开发变得非常简单,没有太多的累赘。对于每一个开发者来说,这无疑是一种解脱,因为再也不用进行繁重的劳动了,每天都在一种轻松愉快的氛围中工作,其效率也会整备地提高 - 能够快速相应需求变化
一般的需求变化都来自于局部功能的改变,这种变化将落实到每个微服务上,二每个微服务的功能相对来说都非常简单,更改起来非常容易,所以微服务非常适合敏捷开发方法,能够快速的影响业务的需求变化。 - 随时随地更新
- 不停服更新
一方面,微服务的部署和更新并不会影响全局系统的正常运行;另一方面,使用多实例的部署方法,可以做到一个服务的重启和更新在不易察觉的情况下进行。所以每个服务任何时候都可以进行更新部署。 - 系统更加稳定可靠
微服务运行在一个高可用的分布式环境之中,有配套的监控和调度管理机制,并且还可以提供自由伸缩的管理,充分保障了系统的稳定可靠性
二、RPC协议
2.1 RPC的概念
RPC(Remote Procedure Call,远程过程调用)是一种通信协议,允许程序调用另一台计算机上的函数或过程,就像调用本地函数一样,隐藏了底层网络通信的复杂性。
RPC属于四层网络模型的应用层协议,底层用的是TCP协议实现的
理解RPC:
RPC 就像是让你能像调用本地函数一样去调用远程服务器上的函数。
通过rpc协议,传递:函数名、函数参数。达到在本地,调用远端函数,得返回值到本地的目标。
我们先来看一下本地函数调用。当我们写下如下代码的时候:
规则
result := Add(1,2)
我们知道,我们传入了1,2两个参数,调用了本地代码中的一个Add函数,得到result这个返回值。
这时参数,返回值,代码段都在一个进程空间内,这是本地函数调用。
那有没有办法,我们能够调用一个跨进程(所以叫"远程",典型的事例,这个进程部署在另一台服务器上)的函数呢?
这也是RPC主要实现的功能。
2.2 为什么微服务需要RPC
我们使用微服务化的一个好处就是,不限定服务的提供方使用什么技术选型,能够实现公司跨团队的技术解耦,如下图:
这样的话,如果没有统一的服务框架,RPC框架,各个团队的服务提供方就需要各自实现一套序列化、反序列化、网络框架、连接池、收发线程、超时处理、状态机等“业务之外”的重复技术劳动,造成整体的低效。所以,统一RPC框架把上述业务之外的技术劳动统一处理,是服务化首要解决的问题。
为什么微服务使用 RPC:
- 每个服务都被封装成 进程。彼此”独立“。
- 进程和进程之间,可以使用不同的语言实现。
2.3 RPC入门
在互联网时代,RPC已经和IPC(进程间通信)一样成为一个不可或缺的基础构件。因此Go语言的标准库也提供了一个简单的RPC实现,我们将以此为入口学习RPC的常见用法。
2.3.1 RPC的使用步骤
既然是远程调用,肯定会涉及到网络。
Go语言的RPC包的路径为net/rpc,也就是放在了net包目录下面。
接着我们尝试基于rpc实现一个类似的例子。
1)服务端
1.注册 rpc 服务对象。给对象绑定方法( 1. 定义类, 2. 绑定类方法 )
rpc.RegisterName("服务名",回调对象)
2.创建监听器
listener, err := net.Listen()
3.建立连接
conn, err := listener.Accept()
4.将连接 绑定 rpc 服务。
rpc.ServeConn(conn)
2)客户端
1.用 rpc 连接服务器
conn, err := rpc.Dial()
2.调用远程函数
conn.Call("服务名.方法名", 传入参数, 传出参数) //传出参数以地址方式传递
2.3.2 RPC 相关函数
1)注册 rpc 服务
func (server *Server) RegisterName(name string, rcvr interface{}) error
参1:服务名。字符串类型。
参2:对应 rpc 对象。 该对象绑定方法要满足如下条件:
1)方法必须是导出的 —— 包外可见。 首字母大写。
2)方法必须有两个参数, 都是导出类型、內建类型。
3)方法的第二个参数必须是 “指针” (传出参数)
4)方法只有一个 error 接口类型的 返回值。
举例:
type World stuct {
}
func (world *World) HelloWorld (name string, resp *string) error {
}
rpc.RegisterName("服务名", new(World))
2)绑定rpc服务
func (server *Server) ServeConn(conn io.ReadWriteCloser)
conn: 就是成功建立好连接的 socket —— conn
3)调用远程函数
func (client *Client) Call(serviceMethod string, args interface{}, reply interface{}) error
serviceMethod: “服务名.方法名”
args:传入参数。 方法需要的数据。
reply:传出参数。定义 var 变量,&变量名 取地址完成传参。
2.3.3 编码实现
server端:
package main
import (
"fmt"
"net"
"net/rpc"
)
// World 定义类对象
type World struct {
}
// HelloWorld 绑定类方法
// 结构体的成员方法
// 方法必须是导出的 —— 包外可见。 首字母大写。
// 方法必须有两个参数, 都是导出类型、內建类型。
// 方法的第二个参数必须是 “指针” (传出参数)
// 方法只有一个 error 接口类型的 返回值。
func (word World) HelloWorld(name string, resp *string) error {
*resp = name + " 你好!"
return nil
}
func main() {
// 1. 注册RPC服务, 绑定对象方法
//func RegisterName(name string, rcvr any) error
//RegisterName方法有两个参数,第一个参数是注册的服务名
//第二个参数必须是指针,可以通过New创建指定类型的指针。是我们创建的方法的类对象的指针
err := rpc.RegisterName("hello", new(World))
if err != nil {
fmt.Println("注册 rpc 服务失败!", err)
return
}
// 2. 设置监听
//设置监听地址和端口
listener, err := net.Listen("tcp", "127.0.0.1:8800")
if err != nil {
fmt.Println("net.Listen err:", err)
return
}
defer listener.Close()
fmt.Println("开始监听 ...")
// 3. 建立链接
//通过监听地址返回的listener建立连接
conn, err := listener.Accept()
if err != nil {
fmt.Println("Accept() err:", err)
return
}
defer conn.Close()
fmt.Println("链接成功...")
// 4. 绑定服务
// 参数是建立连接返回的conn
// func ServeConn(conn io.ReadWriteCloser)
rpc.ServeConn(conn)
}
客户端:
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 // 接收返回值 --- 传出参数
//func (client *Client) Call(serviceMethod string, args any, reply any) error
//第一个参数传 服务名.方法名,作为一个字符串传参
//第二个参数是传给调用的方法的参数,方法需要的数据
//第三个参数是调用的方法传回来的参数,以指针形式传参
err = conn.Call("hello.HelloWorld", "景天", &reply)
if err != nil {
fmt.Println("Call:", err)
return
}
//打印调用返回结果
fmt.Println(reply)
}
运行服务端
运行客户端
可见拿到了服务端的返回值,实现了远程调用,即rpc。