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 。

6707 次点击
所在节点    Go 编程语言
66 条回复
wellsc
2020-12-12 01:39:30 +08:00
安利一下 rSocket
no1xsyzy
2020-12-12 01:47:18 +08:00
主要是 RPC 的含义就是 Remote Procedure Call
无需应答是否要求保证送达?要保证送达需要回执,这个从 TCP 层面做还是从你应用层做多大额外开销的,把应答丢弃就行了
不要求送达,那 TCP 相比 UDP 的开销比你应用层更明显,游戏恐怕不太可能用你这个方法。

另外,ARPC 让人感觉这个是 ARP 的 Client 。
so1n
2020-12-12 02:00:31 +08:00
如果不等函数调用结束就返回 那不就是消息队列吗...
lesismal
2020-12-12 06:55:02 +08:00
@wellsc 看了下 rsocket-go,风格比较 java,有点臃肿了,使用起来不够方便简洁

```golang
package main

import (
"context"
"log"

"github.com/rsocket/rsocket-go"
"github.com/rsocket/rsocket-go/payload"
)

func main() {
// Connect to server
cli, err := rsocket.Connect().
SetupPayload(payload.NewString("Hello", "World")).
Transport(rsocket.TCPClient().SetHostAndPort("127.0.0.1", 7878).Build()).
Start(context.Background())
if err != nil {
panic(err)
}
defer cli.Close()
// Send request
result, err := cli.RequestResponse(payload.NewString("你好", "世界")).Block(context.Background())
if err != nil {
panic(err)
}
log.Println("response:", result)
}
```
lesismal
2020-12-12 06:58:39 +08:00
@so1n “不等函数调用结束就返回” —— 这个可能是兄弟误解了,ARPC 说的不是不等结束就返回,而是可以收到请求后自由选择什么时候回复,可以在处理函数中回包也可以在处理函数之外异步回包之类的,不像其他 RPC 那样函数调用结束了就返回了。
lesismal
2020-12-12 07:25:47 +08:00
@no1xsyzy "主要是 RPC 的含义就是 Remote Procedure Call"
—— 这个定义没错,但正式因为这个定义,限制了很多场景的网络交互方式,比如 web 技术栈,在不同的年代以不同的方式去支持连接复用(比如 http 1.x keepalive, 2.0 以后的长连接)、非 RPC 方式的协议(比如 socketio 、websocket ),ARPC 就是为了简化这种交互模式来反推 RPC 场景,希望能够简化依赖、一个库能支持多数业务、而不是我用了 http 满足不了需求还得使用 grpc 然后还需要再使用个自定义长连接协议或者 websocket 等,这就是为什么我说传统主流 RPC 不爽的原因

"无需应答是否要求保证送达?要保证送达需要回执,这个从 TCP 层面做还是从你应用层做多大额外开销的,把应答丢弃就行了"
"不要求送达,那 TCP 相比 UDP 的开销比你应用层更明显,游戏恐怕不太可能用你这个方法。"
—— 先说"送达"吧,这个是误解,ARPC 没说不要求送达,transport 层使用长连接或者可靠连接作为载体的应用层业务,transport 层本身来负责正常情况时连接能够保持情况下的送达,但是任何 transport 层也都无法保证自己这一层的 100%送达,比如链路中断、设备掉电、切换网络。应用层更没法保证送达,基于正常情况下的业务逻辑交互作业、加上完善的状态管理、错误处理之类的就可以了
—— 再说 "不要求送达",这个是兄弟的误解,"不需要应答" != "不要求送达",所以基于"不要求送达"推断"游戏能不太可能用 ARPC"也是不必要的。而且,ARPC 本身就是基于游戏和其他一些有状态服务的框架、为了反推那些被 RPC 限制了的场景技术栈和资源优化而实现的

另外,ARPC 让人感觉这个是 ARP 的 Client 。
—— 名字这个没办法,众口难调,每个人都会有不同的看法。我定义成 ARPC 大概有两层意思:一是希望像 "老 A"一样,努力做成同类基础设施中最好的;二是 "Async RPC" 的缩写,因为可以自由选择同步还是异步回包、回包方式不受传统 RPC 那种处理函数结束就意味着调用结束的限制
lesismal
2020-12-12 07:33:08 +08:00
@wellsc 另外,还没有深入研究 rsocket,不知道是否方便支持服务端广播之类的
mepwang
2020-12-12 07:53:40 +08:00
传统的分布式调用有很多坑要填的,比如 90 年代的 CORBA 技术就试图搭出一套完全的分布式对象体系。比如你的框架里面,假如服务器端单个请求处理比较耗时的话,多个客户端的并发请求是否会被阻塞?当个客户端的同一个远程方法的多个并发调用,是否会被阻塞? RPC 框架一定和应用场景紧密相连的,能满足几个实际应用场景需求的框架,就是好框架
lesismal
2020-12-12 09:03:44 +08:00
@mepwang

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

—— 主贴介绍的这部分有讲,每个 handler 都支持灵活的同步异步策略,用户可以自己设定请求来了是 one by one 的方式处理还是每个请求开一个新协程,甚至单个请求的处理函数里业务层自己根据实际情况选择同步还是新开协程或者协程池处理。至少 ARPC 框架层不存在这些坑点。除了 erlang golang 的其他语言可选的方式不多,要么像 node 这种也是语言层面提供异步以及各种语法糖,要么像 c++这种自己定制线程 /任务池、连接池各种基础轮子

另外 “多个客户端的并发请求是否会被阻塞” 不只是服务端框架的问题。请求处理对应的业务逻辑比如数据层、其他调用链等耗时操作也可能导致响应慢,还有比如业务层没有做限流导致的请求量爆炸,甚至部署的网络链路不稳定导致 transport 层拥塞,这些非框架原因导致的超载是框架自己不可控的、需要留给业务层处理。当然,有的微服务框架做得大而全,框架本身包含了限流熔断负载均衡等全家桶组件,ARPC 的定位则是网络和协议交互这一层的小而美,毕竟大而全太耗精力而且并不是所有项目都需要大而全,绝大多数团队的业务场景直接拿别人大而全的框架反倒成本太高、不太合适
lesismal
2020-12-12 09:30:59 +08:00
@mepwang 不同领域的分布式系统差别太大了,比如 web 、游戏、分布式数据库或者计算引擎。每种领域又有很多具体的子场景,业务耦合的方式不同,架构设计的方式也都不同,甚至天差地别。通常在 web 领域流行的分布式理论,拿到 MMORPG 里就完全不能用

近十年的互联网发展速度太快了,最基本的一个需求,比如推送,很多应用可能都需要,但是传统的 RPC,server 端是不支持广播的,所以要使用 http static,http api,websocket,服务集群内部还要 RPC,而除了 http static,其他的场景 ARPC 都可以支撑,能很大程度简化技术栈,并且性能更高
Catstyle
2020-12-12 09:43:17 +08:00
啊,13 年开始就这么用了
XiLingHost
2020-12-12 10:20:53 +08:00
@lesismal 名字可以改成 aRPC,就类似于 gRPC
kevinwan
2020-12-12 10:34:54 +08:00
我们有 zRPC,基于 gRPC 的,我觉得 gRPC 还是比较通用解决方案,当然也鼓励创新
https://github.com/tal-tech/go-zero
fox0001
2020-12-12 10:41:15 +08:00
感觉是造了双筷子,然后改个名叫“叉子”……纯直觉,没别的意思
lesismal
2020-12-12 10:44:56 +08:00
@XiLingHost 这个都行,代码 package 都是 arpc,个人开发者没那么大的社区影响力,ARPC/aRPC/arpc 都随意 :joy:
lesismal
2020-12-12 10:50:02 +08:00
@kevinwan 嗯嗯 star 支持了。前阵子还提了个服务注册发现确保强一致的 [issues/227]( https://github.com/tal-tech/go-zero/issues/227) ,其实加上强一致保障不复杂,etcd 的 "go.etcd.io/etcd/client/v3/concurrency" 子包自带了分布式锁的实现:

```golang
client, err := clientv3.New(clientv3.Config{
Endpoints: endpoints,
DialTimeout: 5 * time.Second,
})
if err != nil {
log.Error("NewRegister [%v, %v] clientv3.New failed: %v", key, value, err)
return nil, err
}

session, err := concurrency.NewSession(client)
if err != nil {
log.Error("NewRegister [%v, %v] concurrency.NewSession failed: %v", key, value, err)
return nil, err
}

mux := concurrency.NewMutex(session, RegisterMutexPrefix)
err = mux.Lock(context.TODO())
if err != nil {
log.Error("NewRegister [%v, %v] Lock failed: %v", key, value, err)
return nil, err
}
defer mux.Unlock(context.TODO())
```

然后先 Get 判断再 Put 就行了
lesismal
2020-12-12 10:57:57 +08:00
@fox0001 这么说也对,因为其实我最早不是做 web 服务为主的业务的,最开始做就都是有状态的长连接服务,后面再做 web 类的,觉得 http 相关的 api 、rpc 技术栈虽然工程实践上积累了很多,但是基础设施还是太低效了。这几年又是挖矿,又是全球升温环境恶化,而且以后随着大数据、AI 、5G+这些的更加普及,计算量会越来越大,对应的能源消耗也会越来越大。以前的 http 因其文本协议的便利极大促进了互联网的发展,但也正是由于它短链接、文本格式等低效浪费问题,会造成日趋爆发的数据和计算量的巨大浪费,所以才会有 http 2.0 3.0 quic mqtt 各种升级方案。
lesismal
2020-12-12 10:59:29 +08:00
@lesismal @kevinwan @fox0001

网络交互是通用的基础设施,4 种基础网络交互模式也早已有之,所以我这个也不算创新 😂😂,只是希望把通用的基础设施能推向 web 等其他曾经受限的领域,让做业务更容易、并且性能更高、更节能。
lesismal
2020-12-12 11:02:23 +08:00
@kevinwan 还是举推送的例子,gRPC 这些好像不太便利。很多接口类业务,其实如果换成有状态服务,性能和软硬件消耗都能节省不少,但是难度当然也略高些,如果是用户之间还存在复杂交互耦合的(比如游戏),则业务复杂度和编码难度更高
lesismal
2020-12-12 11:03:41 +08:00
欢迎各位体验、尝试 😊

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

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

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

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

© 2021 V2EX