RPC 的变革 —— ARPC 项目自荐

lesismal · 2020-12-08 21:35:25 · 5445 次点击

原帖不能编辑,单独一层楼更新下:

RPC 的变革 —— ARPC 项目自荐

已加入 awesome-go

项目地址: 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 | Call | 需要 | 客户端发起请求,服务端应答 | >> | 2 | Call | 需要 | 服务端发起请求,客户端应答 | >> | 3 | Notify | 不需要 | 客户端向服务端发出通知/推送 | >> | 4 | Notify | 不需要 | 客户端向服务端发出通知/推送 |

> 如此看来,传统主流 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 当然要有比较全面的支持:

>> | 序号 | 交互方式 | 是否需要应答 | 交互流程 | 传统主流RPC | ARPC | >> | - | ------ | ----- | ---------- | ---------- | ---------- | >> | 1 | Call | 需要 | 客户端发起请求,服务端应答 | | | >> | 2 | Call | 需要 | 服务端发起请求,客户端应答 | | | >> | 3 | Notify | 不需要 | 客户端向服务端发出通知/推送 | | | >> | 4 | Notify | 不需要 | 客户端向服务端发出通知/推送 | | |

3. 丰富的业务场景支持

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

>> 1)推送服务

>> 2)游戏服务

>> 3)聊天服务

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

4. 写法简单

> 如示例所示, 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 。

#13
更多评论

看了下楼主的项目, 好像目前没用go mod来管理? 如果可以,希望能用go mod

#1

@soluty 帖子里最少依赖的部分有讲,因为ARPC框架部分package和自己子package本身不依赖标准库之外的其他三方库,所以不需要 go mod ^_^

micro、protocol里的子package实现了服务发现与注册、对quic和websocket的支持是作为样例,需要依赖etcd、gorillia等,但这属于业务层,并不是ARPC框架本身的依赖项

#2