- 什么是微服务?
- rpc架构的主要区别
- rpcx与grpc的区别
- rpcx:
- grpc:
- 为什么grpc要使用http2,为什么不适应http1或者http3?
- 为什么grpc要使用proto而不是json或者其他数据格式?
- 为什么rpcx快,快多少?
- rpcx的具体性能指标与grpc比较:
什么是微服务?
整体功能通过多个程序实现,每个程序只关心特定的业务.
优点:
简化功能: 单个服务之需要关心部分业务,实现起来更容易
更灵活: 不同服务间互不影响,可以使用不同的语言与技术栈,以及交给不同的成员/团队实现,便于团队合作/外包
隔离: 部分服务出问题不影响其他服务的功能
拓展: 更容易针对借口的实际压力情况进行横向拓展.
rpc架构的主要区别
rpc架构的核心功能实际上是实现远程调用服务方法调用,客户端能像调用本地方法一样调用服务端和方法
那么核心问题就是如何实现接口的远程调用,选择什么网络协议,数据格式,这些决定了rpc架构是否跨语言,以及性能如何
可以写一个简单的rpc服务的demo去了解rpc是如何工作的
下面是一个很粗糙的demo;使用tcp进行通信,json进行编码的demo
public.go
package public
//公共的方法与类
import (
"bytes"
"encoding/binary"
)
func Encode(data []byte) []byte {
l := len(data)
lBytes := IntToBytes(l)
return append(lBytes, data...)
}
func IntToBytes(n int) []byte {
data := int64(n)
bytebuf := bytes.NewBuffer([]byte{})
binary.Write(bytebuf, binary.BigEndian, data)
return bytebuf.Bytes()
}
func BytesToInt(bys []byte) int {
bytebuff := bytes.NewBuffer(bys)
var data int64
binary.Read(bytebuff, binary.BigEndian, &data)
return int(data)
}
type ReqData struct {
ServerName string
Tag string //标记哪个线程调用的服务,返回的时候带上可以将数据传输到对应的县城
Data []byte
}
type RspData struct {
Tag string //标记哪个线程调用的服务,返回的时候带上可以将数据传输到对应的县城
Data []byte
}
type AddReq struct {
NumA int
NumB int
}
type AddRsp struct {
Sum int
}
server.go
package main
import (
"bufio"
"encoding/json"
"fmt"
"net"
"rpc_demo/public"
)
type Server struct{}
func (s *Server) Add(a *public.AddReq) *public.AddRsp {
return &public.AddRsp{Sum: a.NumA + a.NumB}
}
// 服务调用
// 服务名+方法名
// 封装对应的服务调用过程:根据方法名解析数据,并调用对应的方法
// 数据打包返回
// 这里做简化板手写处理:1. 没有实现自动化的服务方法注册;2. 我暂定使用uuid进行标识请求,以便于客户端可以将数据读取到对应的请求线程上,但事实上uuid过长,应该使用更为简单的标识方式
func serve(conn net.Conn) {
defer conn.Close()
reader := bufio.NewReader(conn)
for {
//解析长度
lBytes := make([]byte, 8)
_, err := reader.Read(lBytes[:])
if err != nil {
fmt.Printf("数据读取失败%v\n", err)
return
}
l := public.BytesToInt(lBytes)
reqBytes := make([]byte, l)
_, err = reader.Read(reqBytes)
if err != nil {
fmt.Printf("数据读取失败%v\n", err)
return
}
go func(reqData []byte) {
req := new(public.ReqData)
err = json.Unmarshal(reqData, req)
if err != nil {
fmt.Printf("json 解析失败%v\n", err)
return
}
//解析处理(这里只注册了一个服务接口)
switch req.ServerName {
case "Server.Add":
s := &Server{}
data := new(public.AddReq)
err := json.Unmarshal(req.Data, data)
if err != nil {
fmt.Printf("json 解析失败%v\n", err)
return
}
rsp := s.Add(data)
result, err := json.Marshal(rsp)
if err != nil {
fmt.Printf("数据编码失败%v\n", err)
return
}
rspBytes, err := json.Marshal(&public.RspData{Tag: req.Tag, Data: result})
if err != nil {
fmt.Printf("数据编码失败%v\n", err)
return
}
rspData := append(public.IntToBytes(len(rspBytes)), rspBytes...)
conn.Write(rspData)
default:
conn.Write([]byte("该方法没有注册"))
}
}(reqBytes)
}
}
func main() {
listen, err := net.Listen("tcp", "127.0.0.1:9999")
if err != nil {
fmt.Println("Listen() failed, err: ", err)
return
}
for {
conn, err := listen.Accept() // 监听客户端的连接请求
if err != nil {
fmt.Println("Accept() failed, err: ", err)
continue
}
go serve(conn) // 启动一个goroutine来处理客户端的连接请求
}
}
client.go
package main
import (
"bufio"
"encoding/json"
"fmt"
"net"
"rpc_demo/public"
"time"
"github.com/google/uuid"
)
type Client struct{ Conn net.Conn }
func NewClient() *Client {
conn, err := net.Dial("tcp", "127.0.0.1:9999")
if err != nil {
fmt.Println("err : ", err)
return nil
}
return &Client{Conn: conn}
}
// 每次调用都生成单独的uuid,并作为key,请求后select uuid对应的chan,直到有数据,读取数据,关闭通道,清除对应的map记录
var M map[string]chan ([]byte)
// 启动客户端连接服务端并解析数据
func (c *Client) Run() {
defer c.Conn.Close() // 关闭TCP连接
reader := bufio.NewReader(c.Conn)
for {
lBytes := make([]byte, 8)
_, err := reader.Read(lBytes[:])
if err != nil {
fmt.Printf("数据读取失败")
return
}
l := public.BytesToInt(lBytes)
reqBytes := make([]byte, l)
_, err = reader.Read(reqBytes)
if err != nil {
fmt.Printf("数据读取失败")
return
}
//解析数据体并写入对应的chan
go func(data []byte) {
rspData := new(public.RspData)
err := json.Unmarshal(data, rspData)
if err != nil {
fmt.Printf("数据解析失败")
return
}
M[rspData.Tag] <- rspData.Data
}(reqBytes)
}
}
// 我这边就不封装自动call了,直接手动call
func (c *Client) Call(serverAndfunc string, data []byte) []byte {
//生成uuid
tag := uuid.New().String()
reqData := &public.ReqData{ServerName: serverAndfunc, Tag: tag, Data: data}
r, err := json.Marshal(reqData)
if err != nil {
fmt.Println("编码错误")
return nil
}
Ch := make(chan []byte)
defer close(Ch)
defer delete(M, tag)
M[tag] = Ch
c.Conn.Write(append(public.IntToBytes(len(r)), r...))
return <-Ch
}
func main() {
// 初始化map
M = make(map[string]chan []byte)
//建立tcp连接服务端
client := NewClient()
// 启动处理
go client.Run()
//模拟调用call方法
req1 := &public.AddReq{
NumA: 1,
NumB: 2,
}
reqdata1, err := json.Marshal(req1)
if err != nil {
fmt.Println("编码错误")
return
}
req2 := &public.AddReq{
NumA: 2,
NumB: 2,
}
reqdata2, err := json.Marshal(req2)
if err != nil {
fmt.Println("编码错误")
return
}
//模拟多线程调用服务端
go fmt.Printf("线程1调用结果:%s\n", string(client.Call("Server.Add", reqdata1)))
go fmt.Printf("线程2调用结果:%s\n", string(client.Call("Server.Add", reqdata2)))
time.Sleep(10 * time.Second)
}
client运行结果:
线程1调用结果:{"Sum":3}
线程2调用结果:{"Sum":4}
- 当然这只是最简单的demo,模拟了使用tcp进行rpc远程调用,
- 是否跨语言就在于双方时候都支持相同时协议与数据格式,比如使用了tcp的通信协议,那么只要支持tcp的的语言就可以使用打包成相同的数据结构就可以被服务端解析,而那些跨语言的rpc(比如grpc)在这方面做得更好,他们隐式的生成了接口代码,你不需要知道他是如何编码与解码的.可以直接使用,这对使用者是非常友好的.
rpcx与grpc的区别
rpcx:
- 通信协议: 支持tcp,http,quic,kcp
- 数据格式: 支持json,proto等多种解码器
- 服务发现。支持 peer2peer、configured peers、zookeeper、etcd、consul 和 mDNS。
- 其他: 多功能支持 https://github.com/smallnest/rpcx?tab=readme-ov-file
- 性能优越
grpc:
- 通信协议:http2
- 数据格式: proto
- 服务发现: 支持etcd等多种组件.
- 其他:https://www.cnblogs.com/leijiangtao/p/4453914.html
- 自动生成photo文件规范,节省开发时间,方便快捷的部署微服务,跨语言开发等多种优势
为什么grpc要使用http2,为什么不适应http1或者http3?
- http1是一次请求一次响应的形式,要等上一次请求完成才能下一次请求,效率太低;而http2:每个请求都是一个双向流,一个连接可以包含多个流,等于是同时发起多个请求,效率更高
- 当时,http3技术不成熟,并且http3相对来讲比较复杂.并且http2对于grpc来讲已经够用了.
为什么grpc要使用proto而不是json或者其他数据格式?
- proto格式只包含数据,即T-(L)-V(TAG-LENGTH-VALUE)方式编码,没有额外不用的:与{,不像json那样包含字段名+数据的格式,数据结构更紧凑.数据体更小,传输的性能更好
- grpc作为一个跨语言的rpc架构,指定特定的数据类型可以更好的对接不同语言的接口
参考: https://segmentfault.com/a/1190000039158535
为什么rpcx快,快多少?
我翻阅了许多博客,他们都没有讲清楚为什么rpcx快.大多数都是在将rpcx与其他的比如grpc,阿里的Dubbo进行性能测试对比
rpcx的作者想做一个性能强大,服务治理的golang的rpc框架来补充golang rpc框架的空缺(虽然grpc与一些rpc架构开始支持go,但是他们都是走跨语言路线的.)
rpcx作者的发展历程介绍到开始的rpcx就是对标准库的rpc进行的封装,rpc标准库就是一个性能非常优秀的库;客户度通过tcp连接和服务器通讯,协议分为header和payload两部分,header很简单,包括服务名、方法和seq,payload包括序列化的数据。简单的数据格式,高效的网络通信使得他的性能非常的优秀.
rpcx开始的版本就是根据标准库进行封装的,封装了服务发现,各种fail处理以及丰富的路由支持.所以rpcx事实上继承了标准rpc库的性能优势,并且在后期重构了代码并且提供了更加丰富的功能.
参考: rpcx简史
rpcx的具体性能指标与grpc比较:
- 模拟0ms处理时间
客户端并发数 | 500 | 2000 | 5000 | ||
---|---|---|---|---|---|
测试指标 | 吞吐量(call/s) | 平均延迟(ms) | p99延迟(ms) | 吞吐量(call/s) | 平均延迟(ms) |
rpcx | 20万 | 25 | 20万 | ||
grpc | 14万 | 25 | 14万 |
- 模拟10ms处理时间
客户端并发数 | 500 | 2000 | 5000 | ||
---|---|---|---|---|---|
测试指标 | 吞吐量(call/s) | 平均延迟(ms) | p99延迟(ms) | 吞吐量(call/s) | 平均延迟(ms) |
rpcx | 5万 | 10 | 25 | 15万 | 20 |
grpc | 5万 | 10 | 25 | 9万 | 30 |
- 模拟30ms处理时间
客户端并发数 | 500 | 2000 | 5000 | ||
---|---|---|---|---|---|
测试指标 | 吞吐量(call/s) | 平均延迟(ms) | p99延迟(ms) | 吞吐量(call/s) | 平均延迟(ms) |
rpcx | 1.8万 | 10 | 25 | 7万 | 30 |
grpc | 1.8万 | 10 | 25 | 6万 | 20 |
参考:rpcx- GitHub
总结: 在并发数量增加的情况下,rpcx相比grpc的吞吐量与与p99延迟(处理99%请求的平均延迟)要更加优秀.