字节笔记本字节笔记本

Go 微服务基础

2021-10-15

《Go 微服务基础》介绍了单体式架构和微服务的优缺点,以及RPC协议在微服务中的应用,包括其基本原理、使用步骤和编码实现,同时提到了使用jsonrpc和protobuf进行数据序列化的方法。

单体式和微服务

单体式架构服务

—— 过往大家熟悉的服务器。

特性:

  1. 复杂性随着开发越来越高, 遇到问题解决困难。
  2. 技术债务逐渐上升。
  3. 耦合度高,维护成本大!
    1. 出现bug, 不容易排查
    2. 解决旧bug, 会出新bug
  4. 持续交付时间较长。
  5. 技术选型成本高,风险大。
  6. 扩展性较差
    1. 垂直扩展:通过增加单个系统程的负荷来实现扩展。
    2. 水平扩展:通过增加更多的系统成员来实现扩展。

微服务

  • 优点:
    1. 职责单一
    2. 轻量级通信
    3. 独立性
    4. 迭代开发。
  • 缺点:
    1. 运维成本高
    2. 分部式复杂度
    3. 接口成本高
    4. 重复性劳动
    5. 业务分离困难。

单体式服务和微服务对比

新功能开发 需要时间 容易开发和实现
传统单体架构 分布式微服务化架构
部署 不经常而且容易部署 经常发布,部署复杂
隔离性 故障影响范围大 故障影响范围小
架构设计 初期技术选型难度大 设计逻辑难度大
系统性能 相对时间快,吞吐量小 相对时间慢,吞吐量大
系统运维 运维难度简单 运维难度复杂
新人上手 学习曲线大(应用逻辑) 学习曲线大(架构逻辑)
技术 技术单一而且封闭 技术多样而且容易开发
测试和差错 简单 复杂(每个服务都要进行单独测试,还需要集群测试)
系统扩展性 扩展性差 扩展性好
系统管理 重点在于开发成本 重点在于服务治理和调度

RPC 协议

什么是RPC

Remote Procedure Call Protocol —— 远程过程调用协议!

IPC: 进程间通信

RPC:远程进通信 —— 应用层协议(http协议同层)。底层使用 TCP 实现。

回顾:

OSI 7 层模型架构:物、数、网、传、会、表、应

TCP/IP 4 层架构:链路层、网络层、传输层、应用层

  • 理解RPC:

    • ==像调用本地函数一样,去调用远程函数。==
      • 通过rpc协议,传递:函数名、函数参数。达到在本地,调用远端函数,得返回值到本地的目标。
  • 为什么微服务使用 RPC:

    1. 每个服务都被封装成 进程。彼此”独立“。

    2. 进程和进程之间,可以使用不同的语言实现。

RPC 入门使用

远程 —— 网络!!

回顾:Go语言 一般性 网络socket通信

server端:

​ net.Listen() —— listener 创建监听器

​ listener.Accpet() —— conn 启动监听,建立连接

​ conn.read()

​ conn.write()

​ defer conn.Close() / listener.Close()

client端:

​ net.Dial() —— conn

​ conn.Write()

​ conn.Read()

​ defer conn.Close()

RPC 使用的步骤

---- 服务端:

  1. 注册 rpc 服务对象。给对象绑定方法( 1. 定义类, 2. 绑定类方法 )

    rpc.RegisterName("服务名"回调对象)
    
  2. 创建监听器

    listener, err := net.Listen()
    
  3. 建立连接

    conn, err := listener.Accept()
    
  4. 将连接 绑定 rpc 服务。

    rpc.ServeConn(conn)
    

---- 客户端:

  1. 用 rpc 连接服务器。

    conn, err := rpc.Dial()
    
  2. 调用远程函数。

    conn.Call("服务名.方法名", 传入参数, 传出参数)
    

RPC 相关函数

  1. 注册 rpc 服务

    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 服务

    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 变量&变量名  完成传参
    

编码实现

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 880 充当服务器。

看到结果:

​ {"method":"hello.HelloWorld","params":["李白"],"id":0}

修改服务器端

修改服务器端,使用 jsonrpc:

jsonrpc.ServeConn(conn)

使用 nc 127.0.0.1 880 充当客户端。

看到结果:

​ echo -e '{"method":"hello.HelloWorld","params":["李白"],"id":0}' | nc 127.0.0.1 8800

如果,绑定方法返回值的 error 不为空? 无论传出参数是否有值,服务端都不会返回数据。

rpc 封装

服务端封装

  1. // 定义接口
    type xxx interface {
        方法名(传入参数传出参数) error
    }
    
    type MyInterface interface {
        HelloWorld(string, *string) error
    }
    
  2. // 封装注册服务方法
    func RegisterService (i MyInterface) {
        rpc.RegisterName("hello", i)
    }
    

客户端封装

  1. // 定义类
    type MyClient struct {
        c *rpc.Client
    }
    
  2. // 绑定类方法
    func this *MyClientHelloWorld(a string, b *string) error {
       return  this.c.Call("hello.HelloWorld", a, b)
    }
    
  3. // 初始客户端
    func InitClient(addr string) error {
        conn, _ := jsonrpc.Dial("tcp", adddr)
        return MyClient{c:conn}
    }
    

protobuf

--- Google

编写的注意事项

  1. message 成员编号, 可以不从1开始, 但是不能重复. -- 不能使用 19000 - 19999
  2. 可以使用 message 嵌套。
  3. 定义数组、切片 使用 repeated 关键字
  4. 可以使用枚举 enum
  5. 可以使用联合体。 oneof 关键字。成员编号,不能重复。
// 默认是 proto2
syntax = "proto3";

// 指定所在包包名
package pb;

// 定义枚举类型
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 函数名(参数:消息体) returns (返回值:消息)
    }
    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() 函数