RPC 的变革 —— ARPC 项目自荐

2020-12-12 00:31:55 +08:00
 lesismal

已加入 awesome-go

RPC 的变革 —— ARPC 项目自荐

项目地址: https://github.com/lesismal/arpc

一、ARPC 示例

echo server

package main

import (
	"log"

	"github.com/lesismal/arpc"
)

func onEcho(ctx *arpc.Context) {
	str := ""
	err := ctx.Bind(&str)
	if err != nil {
		log.Printf("/echo error: %v", err)
		return
	}
	ctx.Write(str)
	log.Printf("/echo: %v", str)
}

func main() {
	svr := arpc.NewServer()

	// register handler
	svr.Handler.Handle("/echo", onEcho)

	svr.Run(":8888")
}

echo client

package main

import (
	"context"
	"log"
	"net"
	"time"

	"github.com/lesismal/arpc"
)

func dialer() (net.Conn, error) {
	return net.DialTimeout("tcp", "localhost:8888", time.Second*3)
}

func main() {
	client, err := arpc.NewClient(dialer)
	if err != nil {
		log.Fatalf("NewClient failed: %v", err)
	}
	defer client.Stop()

	request := "hello"
	response := ""
	// err = client.Call("/echo", &request, &response, time.Second*5)
	err = client.CallWith(context.Background(), "/echo", &request, &response)
	if err != nil {
		log.Fatalf("Call /echo failed: %v", err)
	} else {
		log.Printf("Call /echo Response: \"%v\"", response)
	}
}

二、传统主流的 RPC 框架的局限 /不爽

1. 网络交互模式单一,无法支持更丰富的场景

传统主流 RPC 的网络交互主要是 [客户端到服务端,请求-应答] 的模式,比较单一。按照这个模式以及顾名思义“远程过程调用”,其实 HTTP 也算是 RPC 的一种,只是由于其短连接和 HTTP 协议的文本编码格式等原因导致性能和资源的浪费,所以很少有直接把 HTTP 称为 RPC

网络通信的本质是数据收发,客户端和服务端都可以随时主动发送消息给对方,比如推送服务IM游戏等;而且一方发送数据给另一方,有时是不需要回复的,比如VOIP 电话,其他不要求强一致的消息广播、推送等。

这里把需要应答的通信定义为 Call ,不需要应答的通信定义为 Notify,则网络交互按照发起方、是否需要应答,可以分为以下四种基本模式:

1 )客户端发起请求,服务端应答

2 )服务端发起请求,客户端应答

3 )客户端向服务端发出通知 /推送,无需应答

4 )客户端向服务端发出通知 /推送,无需应答

如此看来,传统主流 RPC 就像是个大内男公务员,因为它只支持了第一种基本模式,只覆盖了 25%——甚至说它的网络通讯模式有点不完整,都算是一种褒奖,因为 25%的模式支持那是相当不完整。

而只支持请求-应答的模式也限制了很多业务场景,其他更广泛的业务场景比如推送服务IM游戏,我们还需要自定义各种协议。

2. Server 端函数调用的写法,函数返回即是调用结束,不够灵活

传统主流 RPC 服务端的写法通常是一个函数,函数返回后框架层把返回值打包发送给客户端作为应答,不支持在该函数中进行异步响应,尤其是 golang 的 RPC ,有的框架为了代码简单,没有写协程,发送数据时直接写到 Conn,高并发写时竞争比较明显会增加时延;有的框架默认采用读协程收到数据 one-by-one 的方式处理,存在线头阻塞的问题,有的框架采用每个消息新开一个 go 协程处理,高并发时协程数量可能暴增、比较浪费,不支持按单个 Method/Router 定制同步或者异步处理,也不支持在 Method/Router handler 内由业务层自主选择同步处理、新开 go 协程异步或者协程池等异步等方式的处理。

三、关于 ARPC

1. 高性能

想说 ARPC 比其他流行的 golang RPC 性能都好,但是自吹最强好像没有说服力,感谢 rpcx 有做一些主流 RPC 框架的性能对比,老仓库 已经废弃并且那时 ARPC 还没有出生,有兴趣的同学可以到 新仓库 跑下代码进行对比,测试时请注意排除其他程序的干扰。

目前最为主流的 gRPC 因为官方综合考量使用了 HTTP2 ,详情参考 gRPC 的动机和设计原则,注定了不能很高性能。而很多 RPC 的业务场景,是基于内部服务集群, HTTP2 的加密流程等显得有些性能浪费。而 ARPC 更注重性能和灵活性,通信协议部分交由业务层决定,通常建议使用 TCP 作为基础通信协议,如有需要,业务层也可以使用 TLSWebsocket 或者 KCP 等 。

2. 网络交互模式全面

上面在 不爽 的部分提到了传统主流 RPC 的不完整, ARPC 当然要比较全面的支持这四种基本交互模式:

1 )客户端发起请求,服务端应答

2 )服务端发起请求,客户端应答

3 )客户端向服务端发出通知 /推送,无需应答

4 )客户端向服务端发出通知 /推送,无需应答

3. 丰富的业务场景支持

由于网络交互模式相对全面,ARPC 可以用于处理多种常规业务场景而不受类似 HTTP 短链接、单向请求-应答方式的限制。比如:

1 )推送服务

2 )游戏服务

3 )聊天服务

4 )其他需要长连接、双向、广播等灵活交互方式的业务

4. 写法简单

如[示例](#rpc 进化--arpc-项目自荐)所示,Handler 不采用函数返回即调用结束的形式,写法简单、更像 HTTP Handler 。由于也不强制使用编解码器,甚至不必生成结构化消息或者服务如 Protobuf 的 Message 、Service 等,这样也带来一些额外的好处,比如热点的结构化数据,业务层可以在数据更新时序列化一次并缓存起来,有需要时直接发送序列化之后的数据给需求方,避免每次发送给每个连接都需要进行一次序列化的浪费,在高在线量的广播类业务中这点尤为明显。

其他一些 RPC 框架喜欢注册对象的方式,由框架层通过反射去解析符合 Handler 格式的方法进行隐式注册,由于早年被 C++的各种语言标准、机制等背后动作玩弄得辛苦,golang 项目中希望框架层和业务层都尽量不让用户增加没有必要的心智负担(比如通过对象隐式注册的方式:没有带来性能提升,没有架构设计模块设计的解耦或者其他优化), ARPC 的设计遵循简单、透明的原则,所以像 HTTP 一样进行显式注册,如果有的同行喜欢玩弄语法糖技巧或者被语法糖技巧玩弄,可以自行定制。

5. 更灵活的同步异步

支持单个 Method/Router Handler 级别设置同步或者异步处理,也支持 Handler 内由业务层自主控制同步或异步回包、从而针对性定制快 /慢接口的协程数量控制与线头阻塞问题处理。

6. 最少依赖

目前如果只使用 ARPC 默认参数,则只使用了 golang 标准库,不需要依赖其他第三方 package 。

7. 易扩展

1 )网络协议支持:由用户自主决定,服务端实现 net.Listener 、客户端实现 net.Conn 即可做为 ARPC 的网络载体,arpc/examples/protocols 已经提供了 KCPQUICTLSUnixSocketUTPWebsocket 等示例,欢迎参考。

2 )非结构化的消息体编解码支持:可以直接用 string 、 *string 、 []byte 、 *[]byte 、error 、 *error 等作为消息体参数。

3 )结构化的消息体编解码支持:为了最少依赖, ARPC 默认使用了 encoding/json 作为结构化消息体的编解码器,性能不够强,但是业务层可以很方便地设置使用 json-iter 、Protobuf 等作为结构化消息地编解码器。

4 )消息体编解码中间件支持: ARPC 提供了消息体编解码中间件机制, arpc/extension/middleware/coder 子包实现了 GzipTracer 作为默认示例,有需要的用户可以参考实现自行定制,使用示例在 arpc/examples/middleware/coder

5 ) Method/Router 的中间件支持: ARPC 提供了类似流行的 golang HTTP 框架的中间件,方便业务层自行扩展, arpc/extension/middleware/router 子包实现了 LoggerRecoverGraceful 作为默认示例,有需要的用户可以参考并实现自行定制,使用示例在 arpc/examples/middleware/router

6 ) Web JS Client 支持: ARPC 提供了 JS Client 及示例:API 示例聊天示例。有了 JS Client, 不需要类似其他 RPC 框架那样部署 HTTP 转换 RPC 的网关,前端可以直接通过 WebsocketARPC 服务进行交互,而且因为 ARPC 已经包括了消息的编解码、Method/Router Handler,比 melody 等只封装了收发数据的基础 websocket 框架更方便。

其他扩展不一一列举了,欢迎有兴趣的同学查看代码或者 New issue 。

8. 更多示例

arpc/examples提供了较为丰富的示例,如 通知广播优雅退出服务注册与发现连接池kcp/quic/tls/websocket 等协议支持发布订阅JS Web Chat 等,请见 arpc/examples

9. 其他

个人精力有限,并且 golang 是世界上第二好的编程语言,所以暂时不考虑对其他语言的支持,欢迎 pr 、issue 。

6768 次点击
所在节点    Go 编程语言
66 条回复
xeaglex
2020-12-12 13:47:20 +08:00
赞,和我之前用 gRPC 时的感想不谋而合。区别是你真的做出来了
no1xsyzy
2020-12-12 13:51:31 +08:00
@lesismal
行吧…… 看上去想要弄个任何交互底层上的通用抽象层。

“无需应答是否要求保证送达?”是个分支,分别分析了 “要求送达” 和 “不要求送达” 的情况

送达的话,依赖送达回执和避免重复送达就成,我是指(类似 MQTT 的) QoS 。
传输层送达不会反应在应用上,会导致重传浪费时间。

不要求送达是指前提就是不要求送达(游戏通常是不要求送达的),那么解决方案一般不会是 TCP

(其实 ARP 好像并不分 C/S )
IamYourDad
2020-12-12 14:31:05 +08:00
楼主, 我没看懂, 样例里面 server->client 是不是多次一举啊, 你想调用 client.dosomthing, 直接在 response 返回不就行了吗, client 读 response 自己 dosomthing, 有什么应用场景, 能不能举例呀
lesismal
2020-12-12 14:43:28 +08:00
@xeaglex 哈哈哈,欢迎品尝 ^_^
lesismal
2020-12-12 15:06:20 +08:00
@no1xsyzy 嗯嗯,文字交流、一些场景没有既定的“黑话”,大家可能会理解出一些歧义 😅

其实需要确认对方执行了的场景,使用 Call 的方式等应答结果就行了.MQTT 既然是基于 tcp,业务层再去设计重传相关的我始终感觉有点别扭😅
我个人对 tcp 的设计也不太满意,有了 BBR 之后传输效率更合理些了,但是 三次握手四次断开 依旧很浪费,2 次握手就足够了,毕竟后面每次都会带 ack ;断开也是 2 次就够了——两边各断开一次、一次断双工,因为工作十几年了,我自己业务还从没遇到过需要半双工分别断开的场景,或者说没遇到过半双工分别断开优于双工同时断开的场景,并且,由于仍然可能存在掉电、线路故障等情况导致任何数据的无法送达,所以关闭一半反倒是作茧自缚 😅。
所以,超脱到 4 层之上,再看送达相关的问题可能会简单些:非事务性的业务,送不送大无所谓;事务 /弱事务性的,业务层的自行保障措施省略不掉。所以对于网络交互层的关注,放在保障线路、设备稳定性等工程属性上就好了

游戏、VOIP 电话之类的丢帧就丢帧吧,后面的状态同步过来正确就好,这种场景我也会选择使用 udp,这些特殊场景还是得自家定制才对性能最友好,否则就 pb 咱都觉得性能不够
lesismal
2020-12-12 15:16:52 +08:00
@IamYourDad 兄弟,我也没太看懂,能详细描述下不?比如

“样例里面 server->client 是不是多次一举啊“,这里的多此一举是不是指 ctx.Write()?比如,return xxx 然后框架层自己 Write 给 client 就行了、不需要让用户自己去 Write ?如果是这个意思,请看主贴的这个部分: "2. Server 端函数调用的写法,函数返回即是调用结束,不够灵活" 。而且,这里是可以异步回包的,方便业务层灵活定制业务模块的任务池、流控等,比如:

func onEcho(ctx *arpc.Context) {
str := ""
err := ctx.Bind(&str)
if err != nil {
log.Printf("/echo error: %v", err)
return
}

// 这里也可能不直接用 go 、而是使用其他业务模块的协程池异步回包
go func() {
// do something.
ctx.Write(str)
}()
}

另外,兄弟,建议改个 ID,你这个 ID 别人读出来或者看文字心里默读的时候其实是你自己吃亏啊。。。😅😂
iyangyuan
2020-12-12 15:21:14 +08:00
Spring Cloud 使用 http 协议怎么说
lesismal
2020-12-12 15:26:48 +08:00
@iyangyuan 我不用 java 😂,随便搜了下 Spring Cloud HTTP2 相关,https://www.jianshu.com/p/ed3f8f983764,好像 Spring Cloud 还是有很大提升空间。
并且,"能罩得住" 和 "能罩得住得更好" 也不是同一件事 😁
lesismal
2020-12-12 15:28:30 +08:00
@iyangyuan 上一条 url 跟文字连一起了无法跳转,而且我还没有编辑权限,重新贴下 url:

https://www.jianshu.com/p/ed3f8f983764
kevinwan
2020-12-12 15:43:01 +08:00
@lesismal 我只是举个例子,go-zero 可以生成任意语言的服务端和客户端代码,插件即可
lesismal
2020-12-12 15:44:37 +08:00
@kevinwan 嗯嗯了解😆😁
robot9
2020-12-12 16:20:57 +08:00
感觉 rpc 最重要的是能方便准确的定义 interface 和 data structure
so1n
2020-12-12 20:37:37 +08:00
@lesismal 是指有序的 request→response→request→response 变为 request,request 然后等处理完了再 response,response 吗?忘记叫啥了,io 多路复用?
lesismal
2020-12-12 21:03:57 +08:00
@so1n
单个连接上的消息是顺序读取的,你说的这种是 one-by-one 的方式进行处理,处理完一个再处理下一个,http 是这样子的,一个处理完之前下一个要排队等待,如果一个处理慢了会导致其他消息也被阻塞、这个问题通常叫线头阻塞
io 多路复用是指 select 、poll 、epoll 、iocp 、kqueue 等,通过事件机制、异步 io 对多个文件描述符(网络连接也是文件描述符的一种)进行高效的异步 io 操作
兄弟概念有点迷茫,可以多看些好书比如 CSAPP 、APUE 、UNP 之类的,啃下来消化消化应该会有很大帮助😁😁
so1n
2020-12-12 21:07:21 +08:00
@lesismal 我是突然忘记了那种该叫啥,所以你的异步是不是指这个方式?
lesismal
2020-12-12 21:09:19 +08:00
@robot9 这两点对于非 rpc 的领域好像也同样重要,所以,反倒不是 rpc 的重点了,而是通用场景的重点 😅😅
lesismal
2020-12-12 21:18:13 +08:00
@so1n 兄弟,先读点好书,或者搜知识点,阻塞、非阻塞、同步、异步,还有其他的很多基础知识补上再交流,否则问出来的问题容易把我问死,不是不想回答,但基础知识的科普,我没那么多时间啊,而且零碎的知识不如你自己系统性学习的效果好 😂😂
或者可以直接来读源码,标准库的源码很赞、很值得学习,或者比如你想问的 ARPC 的问题,ARPC 源码也相对简单,你按照 server 的启动流程看进去,很快就能找到答案了
so1n
2020-12-12 21:20:56 +08:00
@lesismal 那算了,我时间也不多……我只是觉得你那个说不清,很难理解异步返回是代表什么
lesismal
2020-12-12 22:02:32 +08:00
@so1n

一次完整的调用过程指 client 端 Call 调用的返回,其中大致包括了几个主要流程:
1. client 发送请求数据
2. server 接收到请求数据
3. server 处理请求
4. server 发送响应数据
5. client 收到请求数据,调用结束

因为传统 RPC server 端的流程,以 go 的 "net/rpc" 为例,对于每个连接的流程大概是
go func() {
for {
2. server 接收到请求数据
go func() {
3. server 处理请求 // 这里是回调业务层注册进来的 handler
4. server 发送响应数据 // 业务层处理完之后,框架层把 3 中的 handler 返回值打包成响应数据,发送给 client
}()
}
}()

3 中 handler 是默认 go 一个新的协程进行处理,这种在连接数非常多、并发请求量大的时候协程数量多、各项资源消耗比较浪费
但是业务层处理 3 的流程,又不能自主选择是否每次 go 一个新的协程


我在主帖中说的是:"异步响应"和"异步回包"。ARPC 提供了更灵活的机制,大概如下:
处理请求的 handler = 3+4 ( server 处理请求+server 发送响应数据)
go func() {
for {
2. server 接收到请求数据
if 注册时设置为异步处理 {
// 此处是指 "异步响应"
go handler() // 3+4
} else {
handler() // 3+4
}
}
}()
而 handler 的实现中也可以自主选择同步或者异步回包,比如:
func handler(ctx *arpc.Context) {
// do something
if condition {
go ctx.Write(...) // 此处指"异步回包"
或者
// 此处是指 "异步响应"
go func() {
// do something
go ctx.Write(...) // 此处是指 "异步回包"
}
} else {
// do something
ctx.Write(...)
}
}

晚上状态有点懵逼,不知道有没有写错的地方,先大概看下吧 😁😁
lesismal
2020-12-12 22:04:23 +08:00
v 站玩的少,markdown 和代码段的格式不知道怎么搞,编辑好的缩进发出来都给我整没了 😂😂

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/734682

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX