单体式和微服务
单体式架构服务
—— 过往大家熟悉的服务器。
特性:
- 复杂性随着开发越来越高, 遇到问题解决困难。
- 技术债务逐渐上升。
- 耦合度高,维护成本大!
- 出现bug, 不容易排查
- 解决旧bug, 会出新bug
- 持续交付时间较长。
- 技术选型成本高,风险大。
- 扩展性较差
- 垂直扩展:通过增加单个系统程的负荷来实现扩展。
- 水平扩展:通过增加更多的系统成员来实现扩展。
微服务
- 优点:
- 职责单一
- 轻量级通信
- 独立性
- 迭代开发。
- 缺点:
- 运维成本高
- 分部式复杂度
- 接口成本高
- 重复性劳动
- 业务分离困难。
单体式服务和微服务对比
新功能开发 | 需要时间 | 容易开发和实现 |
---|---|---|
传统单体架构 | 分布式微服务化架构 | |
部署 | 不经常而且容易部署 | 经常发布,部署复杂 |
隔离性 | 故障影响范围大 | 故障影响范围小 |
架构设计 | 初期技术选型难度大 | 设计逻辑难度大 |
系统性能 | 相对时间快,吞吐量小 | 相对时间慢,吞吐量大 |
系统运维 | 运维难度简单 | 运维难度复杂 |
新人上手 | 学习曲线大(应用逻辑) | 学习曲线大(架构逻辑) |
技术 | 技术单一而且封闭 | 技术多样而且容易开发 |
测试和差错 | 简单 | 复杂(每个服务都要进行单独测试,还需要集群测试) |
系统扩展性 | 扩展性差 | 扩展性好 |
系统管理 | 重点在于开发成本 | 重点在于服务治理和调度 |
RPC 协议
什么是RPC
Remote Procedure Call Protocol —— 远程过程调用协议!
IPC: 进程间通信
RPC:远程进通信 —— 应用层协议(http协议同层)。底层使用 TCP 实现。
回顾:
OSI 7 层模型架构:物、数、网、传、会、表、应
TCP/IP 4 层架构:链路层、网络层、传输层、应用层
- 理解RPC:
- 像调用本地函数一样,去调用远程函数。
- 通过rpc协议,传递:函数名、函数参数。达到在本地,调用远端函数,得返回值到本地的目标。
- 像调用本地函数一样,去调用远程函数。
- 为什么微服务使用 RPC:
- 每个服务都被封装成 进程。彼此”独立“。
- 进程和进程之间,可以使用不同的语言实现。
RPC 入门使用
远程 —— 网络!!
回顾:Go语言 一般性 网络socket通信
server端:
net.Listen() —— listener 创建监听器
listener.Accept() —— conn 启动监听,建立连接
conn.read()
conn.write()
defer conn.Close() / listener.Close()
client端:
net.Dial() —— conn
conn.Write()
conn.Read()
defer conn.Close()
RPC 使用的步骤
—— 服务端:
注册 rpc 服务对象。给对象绑定方法( 1. 定义类, 2. 绑定类方法 )
rpc.RegisterName("服务名",回调对象)
创建监听器
listener, err := net.Listen()
建立连接
conn, err := listener.Accept()
将连接 绑定 rpc 服务。
rpc.ServeConn(conn)
—— 客户端:
用 rpc 连接服务器。
conn, err := rpc.Dial()
调用远程函数。
conn.Call("服务名.方法名", 传入参数, 传出参数)
RPC 相关函数
- 注册 rpc 服务
```go func (server *Server) RegisterName(name string, rcvr interface{}) error 参1:服务名。字符串类型。 参2:对应 rpc 对象。 该对象绑定方法要满足如下条件:
举例:1)方法必须是导出的 —— 包外可见。 首字母大写。
2)方法必须有两个参数, 都是导出类型、內建类型。
3)方法的第二个参数必须是 “指针” (传出参数)
4)方法只有一个 error 接口类型的 返回值。
type World stuct {
}
func (this World) HelloWorld (name string, resp string) error {
}
rpc.RegisterName(“服务名”, new(World))
2. 绑定 rpc 服务
```go
func (server *Server) ServeConn(conn io.ReadWriteCloser)
conn: 成功建立好连接的 socket —— conn
- 调用远程函数:
func (client *Client) Call(serviceMethod string, args interface{}, reply interface{}) error
serviceMethod: “服务名.方法名”
args:传入参数。 方法需要的数据。
reply:传出参数。定义 var 变量,&变量名 完成传参。
编码实现
server端
package main
import (
"net/rpc"
"fmt"
"net"
)
// 定义类对象
type World struct {
}
// 绑定类方法
func (this *World) HelloWorld (name string, resp *string) error {
*resp = name + " 你好!"
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:8800")
if err != nil {
fmt.Println("net.Listen err:", err)
return
}
defer listener.Close()
fmt.Println("开始监听 ...")
// 3. 建立链接
conn, err := listener.Accept()
if err != nil {
fmt.Println("Accept() err:", err)
return
}
defer conn.Close()
fmt.Println("链接成功...")
// 4. 绑定服务
rpc.ServeConn(conn)
}
client端
package main
import (
"net/rpc"
"fmt"
)
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 // 接受返回值 --- 传出参数
err = conn.Call("hello.HelloWorld", "李白", &reply)
if err != nil {
fmt.Println("Call:", err)
return
}
fmt.Println(reply)
}
json 版 rpc
- 使用 nc -l 127.0.0.1 880 充当服务器。
- 02-client.go 充当 客户端。 发起通信。 —— 乱码。
- 因为:RPC 使用了go语言特有的数据序列化 gob。 其他编程语言不能解析。
- 使用 通用的 序列化、反序列化。 —— json、protobuf
修改客户端
修改客户端,使用jsonrpc:
conn, err := jsonrpc.Dial("tcp", "127.0.0.1:8800")
使用 nc -l 127.0.0.1 8800 充当服务器。
执行go run 02-client.go, 看到结果:
{"method":"hello.HelloWorld","params":["李白"],"id":0}
修改服务器端
修改服务器端,使用 jsonrpc:
jsonrpc.ServeConn(conn)
使用 nc 127.0.0.1 8800 充当客户端。
看到结果:
echo -e '{"method":"hello.HelloWorld","params":["李白"],"id":0}' | nc 127.0.0.1 8800
如果,绑定方法返回值的 error 不为空? 无论传出参数是否有值,服务端都不会返回数据。
rpc 封装
服务端封装
// 定义接口
type xxx interface {
方法名(传入参数,传出参数) error
}
例:
type MyInterface interface {
HelloWorld(string, *string) error
}
// 封装注册服务方法
func RegisterService (i MyInterface) {
rpc.RegisterName("hello", i)
}
客户端封装
// 定义类
type MyClient struct {
c *rpc.Client
}
// 绑定类方法
func (this *MyClient)HelloWorld(a string, b *string) error {
return this.c.Call("hello.HelloWorld", a, b)
}
// 初始客户端
func InitClient(addr string) MyClient {
conn, _ := jsonrpc.Dial("tcp", adddr)
return MyClient{c:conn}
}
protobuf
编写的注意事项
- message 成员编号, 可以不从1开始, 但是不能重复. — 不能使用 19000 - 19999
- 可以使用 message 嵌套。
- 定义数组、切片 使用 repeated 关键字
- 可以使用枚举 enum
- 可以使用联合体。 oneof 关键字。成员编号,不能重复。
// 默认是 proto2
syntax = "proto3";
// 指定所在包包名
package pb;
// 加上这一句才能够成功编译
option go_package = "./";
// 定义枚举类型
enum Week {
Monday = 0; // 枚举值,必须从 0 开始.
Turesday = 1;
}
// 定义消息体
message Student {
int32 age = 1; // 可以不从1开始, 但是不能重复. -- 不能使用 19000 - 19999
string name = 2;
People p = 3;
repeated int32 score = 4; // 数组
// 枚举
Week w = 5;
// 联合体
oneof data {
string teacher = 6;
string class = 7;
}
}
// 消息体可以嵌套
message People {
int32 weight = 1;
}
编译 protobuf
回顾:C++ 编译 命令:
protoc —cpp_out=./ *.proto —-> xxx.pb.cc 和 xxx.pb.h 文件
- go 语言中 编译命令:
protoc --go_out=./ *proto
—-> xxx.pb.go 文件。
添加 rpc 服务
语法:
service 服务名 {
rpc 函数名(参数:message) returns (返回值:message)
}
message People {
string name = 1;
}
message Student {
int32 age = 2;
}
例:
service hello {
rpc HelloWorld(People) returns (Student);
}
知识点:
- 默认,protobuf,编译期间,不编译服务。 要想使之编译。 需要使用 gRPC。
- 使用的编译指令为:
protoc --go_out=plugins=grpc:./ *.proto
- 生成的 xxx.pb.go 文件 与 我们自己封装的 rpc 对比:
客户端:
type bj38Client struct {} ----- type MyClient struct {} 类
func NewBj38Client() ----- InitCient() 函数
func (c *bj38Client) Say() ---- HelloWorld() 方法
服务端:
type Bj38Server interface {} ---- type MyInterface interface{} 接口。
func RegisterBj38Server() ---- func RegisterService() 函数。
作业:grpc 远程调用。
- 服务端 grpc
- 初始一个 grpc 对象
- 注册服务
- 设置监听, 指定 IP、port
- 启动服务。—— serve()
- 客户端 grpc
- 连接 grpc 服务,
- 需要给函数传参grpc.WithInsecure(),否则编译容易出问题
- 初始化 grpc 客户端
- 调用远程服务。
- grpcClient.SayHello()传参context.TODO(),表示空对象,占位�
- 连接 grpc 服务,