基于go的actor框架-protoactor使用小结

alex_023 · · 5585 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

>一年时间转瞬即逝,16年11月份因为业务需要,开始构建actor工具库,因为cluster下grain实现太困难,17年4月切换到具有该特征的protoactor,到现在,使用这个框架快一年。一路踩坑无数,趁这几天规划下阶段业务,就抽空聊聊。 ## ProtoActor框架的基本要点 *Actor*框架的提出基本上与CSP差不多处于同一个年代,相对于后者,过去10年Actor还是要稍微火一些,毕竟目前都强调快速开发,绝大部分的开发人员更关注业务实现,这符合*Actor*侧重于接收消息的对象(*CSP*更强调传输通道)一致。ProtoActor的设计与实现,绝大部分和*AKKA*很相似,只是在序列化、服务发现与注册、生命周期几个地方略有差异。框架使用上整体与*AKKA*相同,但受限于go语言,在使用细节上差别有点大。 **消息传递** *ProtoActor*通过*MPSC*(multi produce single consumer)结构,来实现系统级消息的传递;通过RingBuffer来实现业务消息传递,默认可以缓冲10条业务消息。 跨服传递方面,每一个服务节点即使rpc的客户端,也是rpc的服务端。节点以rpc请求打包发送消息,以rpc服务接收可能的消息回复。 **基于gPRC** 相对于go标准库的rpc包,gRPC具有更多的优点,包括但不限于: 1. 不进支持传统的应答模式,更是支持三种Stream的调用方式,简化大数据流的API设计。 因此,当节点之间传递大数据量的时候,不需要在业务上有其他担心,即使几十上兆的数据,gRPC会通过流控,按照默认64K的数据包进行传递(如果觉得小,可以人为设置)。 2.基于HTTP/2,直接支持TLS 在云服务愈加流行的今天,传输加密是一个非常重要的需求,这大幅简化安全方面的开发投入。 **序列化** *ProtoActor*跨服数据默认采用的Protobuf3序列化协议定义了跨服消息的封装,不支持msgpack、thrift等其他协议。 **服务注册** ProtoActor每个节点允许通过consul进行服务注册,但cluster下的name必须相同。目前小问题还比较多,暂时不建议用于生产环境【后边会说明原因】。 ## ProtoActor使用经验 *ProtoActor*可以算是目前能够早到的最成熟的异步队列封装的框架,基本上可以实现绝大部分业务,屏蔽了底层rpc、序列化的具体实现。 **屏蔽了协程的坑** 尽管go的一大亮点就是goroutinue,但其中很多坑,就一个很简单的代码在客户端正常,在网上`play.golang.org`绝对跑不动: ```go func main() { var ok = true //截止到go1.9.2,如果服务器只有1cpu,则没法调度。 go func() { time.Sleep(time.Second) ok = false }() for ok { } fmt.Println("程序运行结束!") } ``` 框架中,通过`atomic`等偏底层的接口,基本没使用goroutinue和chan,就实现了异步队列的消息传递和函数调用,并且通过`runtime.Goschedule`避免了业务繁忙情况下,其他任务等待的问题。因此,很多时候,将其作为异步消息队列提高服务任性,是很好的选择,不用自己每次去写CSP。 **在actor.Started消息响应完成初始化** 尽管我们会写`Actor`接口的实现,但这个实现代码(暂且叫做MyActor)不要在外部完成业务数据的初始化。涉及到数据库读取,最多就提供一个UID,然后再接收到`*actor.Starting`消息后进行处理。比如这样定义: ```go type MyActor struct{ UID string myContext MyActorContext //业务上下文结构定义 //... } func (my *MyActor)Receive(ctx actor.Context){ switch msg:=ctx.Message().(type){ case *actor.Started: //执行数据加载,初始化myContext //响应其他事件 } } ``` 这样做,有三个好处: 1. actor创建通常是`system`或`parent actor`执行,类似于数据库加载这类IO操作相对耗时,会导致其阻塞。 2. 加载业务很多时候有启动、崩溃回复这两种情况下调用,都属于actor生命周期一部分,封装更一致。 3. 加载操作针对局部变量是进行改写,而执行`Props(actor)`操作,更强调模板不可变,而MyActor的UID是一直不变的。 **禁止使用`FromInstance`** 官方例子文件,使用了不少`FromInstance`来构建Actor,由于go语言不是面向对象语言,该方法实际上是反复使用同一个实例,在业务异常崩溃恢复后,实例并没有重新设置。我们会发现之前的某些变量,并没有因此重置(`FromInstance`以后或许会被废除) **利用鸭子接口进行消息归类** 最初,我们的业务就一个节点。后来需要将该节点的业务分拆为【前端接入】->【若干个后端不同场景服务】,由于消息定义并不支持继承等高级特性,就采用增加同名属性定义,并提供默认值的方式: ```protobuf message Request{ required int64 Router = 1[default 10]; optional int64 UserId = 2; } ``` >客户端只能用protobuf2,正好就用了default特性。 由于protobuf生成的`Request`代码肯定会生成`GetRouter`,我们就可以定义这样的接口,来对具有同样属性的消息进行归类: ```go type Router interface{ func GetRouter()int64 } ``` 采用这种手段,我们在不用列举所有消息、不用`reflect`包的情况下,就可以方便的将前端消息转发给对应的后端服务器。 >同样,scala的case对象在编译后有Product接口实现,只要属性顺序相同,可以在对应序号的属性强制转化,进行分类。kotlin暂时没看到类似特性。 ##ProtoActor的坑 在使用过程中,享受了框架带来的便利,但也有不少出乎预料的问题。相对于AKKA,确实不够成熟,提交反馈之后通常一周会有答复,大概有一半得到修复,其他的就#¥#@%#。比较严重的,大概还有几点: **状态切换没有实际意义** actor非常强调状态,在不同状态下处理业务逻辑非常便于对业务的梳理。*protoactor*对`Actor`接口只定义了一个方法: ```go // Actor is the interface that defines the Receive method. // // Receive is sent messages to be processed from the mailbox associated with the instance of the actor type Actor interface { Receive(c Context) //<-只有这一个方法实现 } ``` 可以用`Setbehavior`来变更消息接收函数,从而达到状态切换的目的。但系统消息并没有像*AKKA*那样,直接交给`postRestart`、`postStop`、`preStart`这三个方法。因此,不论业务Actor在何种状态,都必须在消息接收函数中,处理系统传递的消息,这导致消息接受函数非常臃肿: ```go func (actor *MyActor)Receive1(ctx actor.Context){ handleSystemMsg(ctx.Message) //... } func (actor *MyActor)Receive2(ctx actor.Context){ actor.handleSystemMsg(ctx.Message) //<-通常每个状态都要响应系统消息,避免业务崩溃导致数据没有保存等情况的发生 //... } //响应系统消息 func (actor *MyActor)handleSystemMsg(msg interface{}){ switch msg.(type){ case *actor.Started: case *actor.Stopping: case *actor.Restart: case *actor.Restarting: } } ``` 导致状态切换到别的消息入口时,不得不又要写一遍对系统消息的接收,这导致提供的`Setbehavior`(*AKKA*叫做`become`)失去了实际意义。 **生命周期略有不同** 框架的生命周期与Akka的略有不同,正常情况下:Started->Stopping->Stopped。其中,Stopping是停止actor之前执行,而Stopped是注销actor之后。这是要非常小心,如果Stopping出现崩溃,actor对象将不会释放。 如果在业务运行过程中崩溃,框架会发送消息恢复:Restarting->Started。这个流程与akka的恢复过程非常不同(Stopped->Restarted). **不要跨服Watch** 熟悉AKKA的开发人员比较清楚,child actor是允许跨进程、跨服务节点创建的。但框架的remote包在设计上存在不足,一旦节点失效,即使恢复,两个节点之间也无法建立连接,导致无法接受消息。而框架本来强调的`grain`,必须在cluster模式下,基于`watch`、`unwatch`,所以没法正常使用。 最初在remote模式下发现这问题,原因是: **无法监控** 异步框架中,如果没有监控,相当于开飞机盲降,问题诊断会非常困难。 protoactor在不同actor之间的消息传递,是通过定义`MessageEnvelop`来实现的,三个属性都有着非常重要的作用: ```go //actor之间传递消息时的信封 type MessageEnvelope struct { Header messageHeader //业务之外扩展信息的头,通常用于统计、监控,更多在拦截器中使用 Message interface{} //具体de业务消息 Sender *PID //消息发送者,但发送者调用Tell方法,则Sender为空 } ``` 不过,无解的是Header在拦截器中使用之后,默认用的全局Header信息。Request代码如下: ```go func (ctx *localContext) Request(pid *PID, message interface{}) { env := &MessageEnvelope{ Header: nil, //<--如果Actor接受了之前消息,需要传递Header是没办法的,这里重置为nil Message: message, Sender: ctx.Self(), } ctx.sendUserMessage(pid, env) } ``` 很明显可以看出,当actor向另一个actor发送消息的时候,Header被重置为`nil`,类似的`Tell`方法同样如此。在github上在沟通后,说是考虑到性能。要是增加一个能够传递Header的函数`Forward`多好: ```go func (ctx *localContext) Forward(pid *PID, message interface{}) { env := &MessageEnvelope{ Header: ctx.Header, //<--假定有这个方法能够获取当前ctx的Header信息,如果为nil,则获取全局Header Message: message, Sender: ctx.Self(), } ctx.sendUserMessage(pid, env) } ``` 此外,在不同节点(或不同进程)传输时,最初Header没有定义,但17年年底增加了这个属性,意味着在第一个actor接收消息的时候,能够拦截到Header信息。 **没有原生schedule** actor的定时状态通常都是利用定时消息提供,尽管go原生的timer很好用,但开销不小,并且actor退出时,没法即使撤销对其引用。不过,通常业务的actor规模并不大,并且自己实现也比较简单。 **没有原生eventbus** 框架的消息并不提供分类、主题的投递,只有一个`eventstream`提供所有actor的广播。在尝试抄AKKA的`eventbus`确实复杂,发现go语言实现确实非常复杂,最终放弃。 ##总结 尽管很多年前因为折腾*ProtoActor*,就理解了*异步队列*、*Actor*的实现方式,但都没有在代码规模中如此大规模的应用。在深入应用了Actor之后,不得不说与CSP有巨大的差异。相对而言两者应用场景很大不同: - *CSP* 关注队列,偏底层,更轻,简单异步处理 - *Actor* 关注实例状态,重,默认在实例外加了个壳子(pid+context,AKKA的是actorRef+Context),封装两个队列(MSPC、RingBuffer)三个集合(children、watchers、watchees),更能处理复杂业务逻辑。 客观的说,ProtoActor的开源创造者考虑还是非常全,在序列化方式、消息格式确定之后,先后构造了go、c#、kotlin(这个是个demo的demo),不同节点可以用这几种语言分别实现。源代码也很值得学习(建议使用能够追踪接口定义与实现代码的IDE)。从更新上看,c#更快(没明白为什么不用微软官方的orlean),go语言这边更多是跟随c#的版本迭代,似乎没什么话语权。 整体而言,*ProtoActor*还有很多待改善支出,但对于整天还因为业务中`Mutex`带来的死锁,急需寻找出路,不妨换种思维试试。自己过去一年,尽管使用`Mutex`的能力急剧下降,但视窗观察、战斗同步的业务实现却更加快捷^_^。 [原文链接:protoactor使用小结](http://blog.csdn.net/qq_26981997/article/details/79138111)

有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

5585 次点击  
加入收藏 微博
2 回复  |  直到 2019-04-21 00:56:14
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传