微服务与rpc/grpc
微服务简介
-
微(micro)狭义来讲就是体积小,著名的"2 pizza 团队"很好的诠释了这一解释(2 pizza 团队最早是亚马逊 CEO Bezos提出来的,意思是说单个服务的设计,所有参与人从设计、开发、测试、运维所有人加起来 只需要2个披萨就够了 )。
-
服务(service)区别于系统,服务一个或者一组相对较小且独立的功能单元,是用户可以感知最小功能集
从广义上来讲,微服务是一种分布式系统解决方案,推动细粒度服务的使用,这些服务协同工作
微服务架构
微服务架构风格是将单个应用程序作为一组小型服务开发的方法,每个服务程序都在自己的进程中运行,并与轻量级机制(通常是HTTP资源API)进行通信。这些服务是围绕业务功能构建的。可以通过全自动部署机器独立部署。这些服务器可以用不同的编程语言编写,使用不同的数据存储技术,并尽量不用集中式方式进行管理
-
微服务架构是将复杂的系统使用组件化的方式进行拆分,并使用轻量级通讯方式进行整合的一种设计方法。
-
微服务是通过这种架构设计方法拆分出来的一个独立的组件化的小应用。
微服务架构定义的精髓,就是“分而治之,合而用之”。
-
将复杂的系统进行拆分的方法,就是“分而治之”。分而治之,可以让复杂的事情变的简单
-
使用轻量级通讯等方式进行整合的设计,就是“合而用之”的方法,合而用之可以让微小的力量变动强大
服务拆分原则:高内聚低耦合
微服务和单体架构
单体架构的问题和缺陷
-
复杂性逐渐变高:代码越多复杂性越高,越难解决遇到的问题
-
技术债务逐渐上升:人员流动越大所留下的坑越多,也就是所谓的技术债务越来越多
-
耦合度太高,维护成本大
-
持续交付周期长
-
技术选型成本高:单块架构倾向于采用统一的技术平台或方案来解决所有问题,如果后续想引入新的技术或框架,成本和风险都很大
-
可扩展性差:随着功能的增加,垂直扩展的成本将会越来越大;而对于水平扩展而言,因为所有代码都运行在同一个进程,没办法做到针对应用程序的部分功能做独立的扩展
微服务架构的解决方案
单一职责
微服务架构中的每个服务,都是具有业务逻辑的,符合高内聚、低耦合原则以及单一职责原则的单元,不同的服务通过“管道”的方式灵活组合,从而构建出庞大的系统。
轻量级通信
服务之间通过轻量级的通信机制实现互通互联,而所谓的轻量级,通常指语言无关、平台无关的交互方式。
-
对于轻量级通信的格式而言,例如 XML 和 JSON,它们是语言无关、平台无关的
-
对于通信的协议而言,通常基于 HTTP,能让服务间的通信变得标准化、无状态化
使用轻量级通信机制,可以让团队选择更适合的语言、工具或者平台来开发服务本身。
独立性
每个服务在应用交付过程中,独立地开发、测试和部署
在单体式架构中:所有功能都在同一个代码库,功能的开发不具有独立性;当不同小组完成多个功能后,需要经过集成和回归测试,测试过程也不具有独立性;当测试完成后,应用被构建成一个包,如果某个功能存在 bug,将导致整个部署失败或者回滚
在微服务架构中:每个服务都是独立的业务单元,与其他服务高度解耦,只需要改变当前服务本身,就可以完成独立的开发、测试和部署
进程隔离
单块架构中,整个系统运行在同一个进程中,当应用进行部署时,必须停掉当前正在运行的应用,部署完成后再重启进程,无法做到独立部署。
在微服务架构中,应用程序由多个服务组成,每个服务都是高度自治的独立业务实体,可以运行在独立的进程中,不同的服务能非常容易地部署到不同的主机上。
微服务架构的缺陷
-
运维要求高,难度大
-
分布式的复杂性,导致bug不好调试
-
接口成本高:一旦微服务的接口发生大的变动,那么所有依赖它的微服务都要做相应的调整,由于微服务可能非常多,那么调整接口造成的成本会很高。
-
重复劳动:对于单体架构来讲,如果某段业务被多个模块所共同使用,便可以抽象成一个工具类,被所有模块直接调用,但是微服务却无法这样做,因为这个微服务的工具类是不能被其它微服务所直接调用的,从而我们便不得不在每个微服务上都建这么一个工具类,从而导致代码的重复。
-
业务不好分离
单体架构和微服务架构的对比
新功能开发 | 所需要时间 | 开发和实现的难易度 |
---|---|---|
传统单体架构 | 分布式微服务化架构 | |
部署 | 不经常而且容易部署 | 经常发布,部署复杂 |
隔离性 | 故障影响范围大 | 故障影响范围小 |
架构设计 | 初期技术选型难度大 | 设计逻辑难度大 |
系统性能 | 相对时间快,吞吐量小 | 相对时间慢,吞吐量大 |
系统运维 | 运维难度简单 | 运维难度复杂 |
新人上手 | 学习曲线大(应用逻辑) | 学习曲线大(架构逻辑) |
技术 | 技术单一而且封闭 | 技术多样而且容易开发 |
测试和差错 | 简单 | 复杂(每个服务都要进行单独测试,还需要集群测试) |
系统扩展性 | 扩展性差 | 扩展性好 |
系统管理 | 重点在于开发成本 | 重点在于服务治理和调度 |
RPC协议
RPC概述
RPC(Remote Procedure Call Protocol),是远程过程调用的缩写,通俗的说就是调用远处的一个函数
使用微服务化的一个好处就是,不限定服务的提供方使用什么技术选型,能够实现公司跨团队的技术解耦,如下图:
这样的话,如果没有统一的服务框架(RPC框架),各个团队的服务提供方就需要各自实现一套序列化、反序列化、网络框架、连接池、收发线程、超时处理、状态机等“业务之外”的重复技术劳动,造成整体的低效。所以,统一RPC框架把上述“业务之外”的技术劳动统一处理,是服务化首要解决的问题。
go语言中使用RPC
Go语言的RPC规则:方法只能有两个可序列化的参数,其中第二个参数是指针类型,并且返回一个error类型,同时必须是公开的方法。
golang 中的类型比如:channel(通道)、complex(复数类型)、func(函数)均不能进行 序列化
Server端
func main(){
// rpc注册服务, 注册rpc服务,维护一个hash表,key值是服务名称,value值是服务的地址
rpc.RegisterName("HelloService", new(HelloService))
// 设置服务监听
listener,err := net.Listen("tcp","127.0.0.1:8888")
if err != nil {
panic(err)
}
// 接受传输的数据
conn,err := listener.Accept()
if err != nil {
panic(err)
}
// rpc调用,并返回执行后的数据
// 1.read,获取服务名称和方法名,获取请求数据
// 2.调用对应服务里面的方法,获取传出数据
// 3.write,把数据返回给client
rpc.ServeConn(conn)
}
-
rpc.Register
函数调用会将对象类型中所有满足RPC规则的对象方法注册为RPC函数,所有注册的方法会放在“HelloService”服务空间之下 -
然后建立一个唯一的TCP链接,并且通过
rpc.ServeConn
函数在该TCP链接上为对方提供RPC服务
Client端
func main(){
//用rpc连接
client,err := rpc.Dial("tcp","127.0.0.1:8888")
if err != nil {
panic(err)
}
var reply string
//调用服务中的函数
err = client.Call("HelloService.Hello","world",&reply) // 这里的HelloServer就是Server注册的服务名,然后Hello就是这个服务下的Hello方法
if err != nil {
panic(err)
}
fmt.Println("收到的数据为,",reply)
}
-
首选是通过
rpc.Dial
拨号RPC服务,然后通过client.Call
调用具体的RPC方法 -
在调用
client.Call
时,第一个参数是用点号链接的RPC服务名字和方法名字,第二和第三个参数分别定义RPC方法的两个参数。
跨语言的RPC
标准库的RPC默认采用Go语言特有的gob编码。因此,其它语言调用Go语言实现的RPC服务将比较困难,但是可以通过官方自带的net/rpc/jsonrpc扩展实现一个跨语言RPC服务端和客户端。
无论采用何种语言,只要遵循同样的json结构,以同样的流程就可以和Go语言编写的RPC服务进行通信,这样就可以用json实现了跨语言的RPC。但是除了用json做跨语言的RPC服务之外,还会选用protobuf做跨语言的RPC服务。
基于json编码重新实现RPC服务端
func main(){
// rpc注册服务, 注册rpc服务,维护一个hash表,key值是服务名称,value值是服务的地址
rpc.RegisterName("HelloService", new(HelloService))
// 设置服务监听
listener,err := net.Listen("tcp","127.0.0.1:8888")
if err != nil {
panic(err)
}
for {
// 接受传输的数据
conn,err := listener.Accept()
if err != nil {
panic(err)
}
//给当前连接提供针对json格式的rpc服务
go rpc.ServeCodec(jsonrpc.NewServerCodec(conn))
// 用rpc.ServeCodec函数替代了rpc.ServeConn函数,传入的参数是针对服务端的json编解码器
}
}
json版本的RPC客户端
先手工调用net.Dial函数建立TCP链接,然后基于该链接建立针对客户端的json编解码器
func main(){
//用rpc连接
conn,err := rpc.Dial("tcp","127.0.0.1:8888")
if err != nil {
panic(err)
}
// 建立基于json编解码的rpc服务
client := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn))
var reply string
//调用服务中的函数
err = client.Call("HelloService.Hello","world",&reply)
if err != nil {
panic(err)
}
fmt.Println("收到的数据为,",reply)
}
RPC协议封装
未封装的代码服务名都是写死的,不够灵活,且容易写错,所以这里对RPC的服务端和客户端再次进行一次封装,来屏蔽掉服务名。
服务端封装
package main
import (
"fmt"
"net"
"net/rpc"
"net/rpc/jsonrpc"
)
var ServiceName = "login"
// 定义一个接口,作用是限定我们自己定义的RegisterRPC函数的参数类型,以及它要实现的方法
type RPCDesign interface {
Say(string, *string) error
}
// 定义要注册到RPC的类和方法,这里Persion实现的了Say方法,所以它可以作为RegisterRPC的参数
type Person struct{}
func (p *Person) Say(in string, out *string) error {
*out = in + "hahahahahaha"
return nil
}
// 把RPC的注册方法再封装一层
func RegisterRPC(srv RPCDesign) error {
return rpc.RegisterName(ServiceName, srv)
}
func main() {
// 设置监听
Listener, err := net.Listen("tcp", "127.0.0.1:9999")
if err != nil {
fmt.Println("listen failed, err: ", err)
return
}
defer Listener.Close()
fmt.Println("Start Listening...")
for {
// 开始接受连接
conn, err := Listener.Accept()
if err != nil {
fmt.Println("connection err: ", err)
return
}
defer conn.Close()
fmt.Println(conn.RemoteAddr().String() + "connect success.")
// 注册RPC
err = RegisterRPC(new(Person))
if err != nil {
fmt.Println("Regist RPC failed, err: ", err)
return