2018年07月26日

Microsoft Orleans+Mysql+Docker的那些坑

作者 非鱼

Orleans是一套DLL库,是微软研究院推出的基于Actors模型的,面向高并发大流量的微服务架构的系统框架基础库,同时又能做到跨服务器保证单线程执行的能力,具体可以去看这个产品的相关文档。

之前基于这个库写了一个Demo项目,这几天在折腾Docker,突然想是不是把这套东西部署在Docker上,于是,就踩了不少坑。

1、之前的项目是基于.Net Core 2.0建立的,更新到2.1以后,编译不通过了,经过无数次搜索,定位到是Orleans本身的问题,它的2.0.3版本不支持.net core 2.1的编译时代码生成,改用Daily Build版本的2.0.4rc版本,通过。

2、之前演示用的是Localhost Cluster,要正式部署到Docker里肯定是前后端分离的,不能再用Localhost了,先尝试了Redis版本的集群,无效,原因不明,这个Cluster扩展连一行使用说明的代码都没有。最后不得不回到官方的Ado Cluster上,整个项目改成基于Mysql+EntityFramework的,编译通过。

3、部署到Docker之后一启动Silo,异常退出,发现是驱动的大小写不对,MySql.Data.MySqlClient,改正后再运行,还是异常退出,提示Mysql不支持ssl模式,连接字符串加上SslMode=none后再启动,没有异常,但是启动成功后直接就退出了。再经过一系列的搜索,发现之前的Silo的启动代码是以Console的形式来运行的,SiloHost启动成功之后使用Console.ReadLine()来使程序进入执行等待中,但是这个代码在Docker里是不行的,它不会卡住程序的运行状态,而是直接返回null并继续执行后面的退出Silo的指令了。改成事件机制AssemblyLoadContext.Default.Unloading后通过,终于运行成功。(运行前需要执行项目下自动添加的几个Mysql脚本,用来生成Orleans运行Cluster所需要的几个数据表。)

4、项目本身所需要的数据表还没有,使用EntityFramework的migrations命令来根据对象自动生成Sql创建脚本,报错,找不到Microsoft.EntityFrameworkCore.Storage.Internal.RelationalCommandBuilderFactory..ctor构造函数。又是一阵搜索,Oracle提供的官方Mysql.EntityFrameworkCore库有问题,使用到了Internal方法,在EFCore改版后失效了。卸载掉这个库,换成Pomelo.EntityFrameworkCore.MySql库,编译通过,运行dotnet ef migrations add和dotnet ef migrations script,脚本创建成功。运行脚本创建表又报datetime(6)语法错误,对Mysql 5.7之前的版本,需要把(6)去掉。

5、项目成功运行之后,读取数据库失败,Silo端的日志里没有任何错误输出,前端Web端的日志报后端超时。后来发现在数据库访问层使用了Pomelo的异步访问代码,直接return async()方法,而在Service部分收到返回的Task之后使用了.Result来获取数据,并转换成Dto对象。问题就出在这个Result调用上。在这里不能使用Result方法,必须使用await方法获取返回结果。或者,干脆把数据库访问部分改成同步代码。 这是由于.net自己的异步实现机制造成的,异步方法使用同步等待的机制,会在返回后获取当前线程的句柄信息,但当前线程在这种情况下是处于等待后台线程完成的状态的,无法执行返回句柄的动作,就死锁了。网上说的这个问题一般出现在UI界面主进程中这么调用的情况下,但Orleans的Grains使用了单进程机制来保证同一个Id的Grain跨进程同步,因此出现了同样的问题。

最终的效果是这样的:使用多步编译的Dockerfile,先用一个.netcore 2.1 sdk的镜像,编译并使用publish -r linux-arm来生成发布文件,然后分别将Silo和Web客户端的发布目录复制到两个独立的基于.netcore 2.1 runtime-deps的镜像里来运行,启动时link到mysql的容器上,就一切正常了。配置成swarm之后还可以自动扩展多个Silo节点来做测试,多个Silo节点自动通过Mysql来发布自身的连接信息,客户端就可以连接上。

另外因为整个过程是在树莓派上的,所以还可以外接LED灯,镜像开始build的时候点亮红灯,编译正常结束之后点亮绿灯,编译过程中可以把dotnet test自动化单元测试项目加进去,如果单元测试无法全部通过,后面的build过程就会自动中止,LED灯就会停在红灯上。