聊聊如何设计千万级吞吐量的.Net Core网络通信!
- 作者:大石头
- 时间:2018-10-26 晚上 20:00
- 地点:QQ群-1600800
- 内容:网络通信,
- 网络库使用方式
- 网络库设计理念,高性能要点
介绍
-
首先看下面这张很具有代表性的图,2018年5月份做的测试。当时单服务器得到
2256tps
(Transactions Per Second,每秒事务数) 的吞吐率。这次测试只是说明一个问题,.Net可以做超高吞吐率的应用。
-
当时测试相关记录和代码地址
1.1 开始网络编程
简单的网络程序示例
- 相关使用介绍:https://www.cnblogs.com/nnhy/p/newlife_net_echo.html
- 克隆上面的代码,运行
EchoTest
项目,打开编译的exe,打开两次,一个选1作为服务器,一个选2作为客户端
- 在客户端连接服务器和给服务端发送数据的时候,分别触发
Start
和OnReceive
方法,连接之后服务端发送了Welcome 的消息,客户端发送5次“你好”。服务端回传收到的数据,打了一个日志,把收到的信息转成字符串输出到控制台。 - NetServer是应用级网络服务器,支持tcp/udp/ipv4/ipv6。上面可以看到,同时监听了四个端口。
- 码神工具也可以连接上来
解释
- 对于网络会话来说,最关键的就是客户端连上来,以及收到数据包,这两部分,对应上面
Start
和OnReceive
两个方法
服务端
- 上面是最小的网络库例程,简单演示了服务端和客户端,连接和收发信息。网络应用分为NetServer/NetSession,服务端、会话,N个客户端连接服务器,就会有N个会话。来一个客户端连接,服务端就new一个新的NetSession,并执行Start,收到一个数据包,就执行OnReceive,连接断开,就执行OnDispose,这便是服务端的全部。
- 客户端连接刚上来的时候,没有数据包等其它信息,所以这个时候没有参数。客户端发数据包过来,
OnReceive
函数在处理。 - 服务端的创建,可以是很简单,看以下截图。这里为了测试方便,开了很多Log,实际使用的时候,根据需要注释。
- 长连接、心跳第二节设计理念再讲。
客户端
- 跟很多网络库不同,NewLife.Net除了服务端,还封装了客户端。客户端的核心,也就是Send函数和Received事件,同步发送,异步接收。
- 因为是长连接,所以服务端随时可以向客户端发送数据包,客户端也可以收到。tcp在不做设置的时候,默认长连接2小时。
- NetServer默认20分钟,在没有心跳的时候,20分钟没有数据包往来,服务端会干掉这个会话。
- 虽然上面讲的NetServer和Client,都是tcp,但是换成其它协议也是可以的。这里的NetServer和NetUri.CreateRemote,同时支持Tcp/Udp/IPv4/IPv6等,CreateRemote内部,就是根据地址的不同,去new不同的客户端。所以我们写的代码,根本不在意用的是tcp还是udp,或者IPv6。有兴趣的可以看看源码
1.2 构建可靠网络服务
- 相关博客
- 要真正形成一个网络服务,那得稳定可靠。上面例程EchoTest只是简单演示,接下来看下一个例程EchoAgent。
安装运行
- 这是一个标准的Windows服务,有了这个东西,我们就可以妥妥的注册到Windows里面去。这也是目前我们大量数据分析程序的必备。
- 首先运行EchoAgent,按2,安装注册服务,用管理员身份运行。安装成功然后可以在服务里面找到刚刚安装的服务。
- 安装完成可以在服务上找到,再次按2就是卸载,这个是XAgent提供的功能
- 这时候按3,启动服务
代码解释
- 接下来看代码,服务启动的时候,执行StartWork。在这个时候实例化并启动NetServer,得到的效果就跟例程
EchoTest
一样,区别是一个是控制台一个是服务。停止服务时执行StopWork,我们可以在这里关闭NetServer。详细请看源码
- 必须有这个东西,你的网络服务程序,才有可能达到产品级。linux上直接控制台,上nohup,当然还有很多其它办法。以后希望这个XAgent能够支持linux吧,这样就一劳永逸了
1.3 压测
- 相关博客
- 只需要记住一个两个数字,.net应用打出来2266万tps,流量峰值4.5Gbps
- 两千万吞吐量的数字,当然,只能看不能用。因为服务端只是刚才的Echo而已,并没有带什么业务。实际工作中,带着业务和数据库,能跑到10万已经非常非常牛逼了。
- 我们工作中的服务可以跑到100万,但是我不敢,怕它不小心就崩了。所以我们都是按照10万的上限来设计,不够就堆服务器好了,达到5万以上后,稳定性更重要
网络编程的坑
- 主要有粘包
- 程序员中会网络编程的少,会解决粘包的更少!
1.4 网络编程的坎——粘包
- 普遍情况,上万的程序员,会写网络程序的不到20%,会解决粘包问题的不到1%,如果大家会写网络程序,并且能解决粘包,那么至少已经达到了网络编程的中级水平。
什么是粘包
- 举个栗子:客户端连续发了5个包,服务端就收到了一个大包。代码就不演示了,把第一个例程的这个睡眠去掉。
- 客户端连续发了5个包,服务端就收到了一个大包。
原因
- 很多人可能都听说Tcp是流式协议,但是很少人去问,什么叫流式吧?流式,就是它把数据像管道一样传输过去。
- 刚才我们发了5个 “你好”,它负责把这10个字发到对方,至于发多少次,每次发几个字,不用我们操心,tcp底层自己处理。tcp负责把数据一个不丢的按顺序的发过去。所以,为了性能,它一般会把相近的数据包凑到一起发过去。对方收到一个大包,5个小包都粘在了一起,这就是最简单的粘包。
- 这个特性由NoDelay设置决定。NoDelay默认是false,需要自己设置。如果设置了,就不会等待。但是不要想得那么美好,因为对方可能合包。
- 局域网MTU(Maximum Transmission Unit,最大传输单元)是1500,处于ip tcp 头部等,大概1472多点的样子。
更复杂的粘包及解决方法
- A 1000 字节 B 也是 1000字节,对方可能收到两个包,1400 + 600。对方可能收到两个包,1400 + 600。
- 凡是以特殊符号开头或结尾来处理粘包的办法,都会有这样那样的缺陷,最终是给自己挖坑。所以,tcp粘包,绝大部分解决方案,偏向于指定数据包长度。这其中大部分使用4字节长度,长度+数据。对方收到的时候,根据长度判断后面数据足够了没有。
- 这是粘包的处理代码:http://git.newlifex.com/NewLife/X/Blob/master/NewLife.Core/Net/Handlers/MessageCodec.cs
- 每次判断长度,接收一个或多个包,如果接收不完,留下,存起来。等下一个包到来的时候,拼凑完整。
- 虽然tcp确保数据不丢,但是难免我们自己失手,弄丢了一点点数据。为了避免祸害后面所有包,就需要进行特殊处理了。
- 每个数据帧,自己把头部长度和数据体凑一起发送啊,tcp确保顺序。这里我们把超时时间设置为3~5秒,每次凑包,如果发现上次有残留,并且超时了,那么就扔了它,省得祸害后面。
- 根据以上,粘包的关键解决办法,就是设定数据格式,可以看看我们的SRMP协议,1字节标识,1字节序号,2字节长度
- 如果客户端发送太频繁,服务端tcp缓冲区阻塞,发送窗口会逐步缩小到0,不再接受客户端数据。
1.5 .NetCore版RPC框架
- NetCore版RPC框架NewLife.ApiServer。
- 先看看这个效果
代码分析
- 我们看这部分代码,4次调用远程函数,成功获取结果,包括二进制高速调用、返回复杂对象、捕获远程异常,没错,这就是一个RPC。
服务端
- 有没有发现,这个ApiServer跟前面的NetServer有点像?其实ApiServer内部就有一个NetServer
- 这么些行代码,就几个地方有价值,一个是注册了两个控制器。你可以直接理解为Mvc的控制器,只不过我们没有路由管理系统,直接手工注册。
- 第二个是指定编码器为Json,用Json传输参数和返回值。其实内部默认就是Json,可以不用指定
- 看看我们的控制器,特别像Mvc,只不过这里的Controller没有基类,各个Action返回值不是ActionResult,是的,ApiServer就是一个按照Mvc风格设置的RPC框架
- 返回复杂对象
- 做请求预处理,甚至拦截异常
- 像下面这样写RPC服务,然后把它注册到ApiServer上,客户端就可以在1234端口上请求这些接口服务啦
客户端
- 客户端是ApiClient,这里的MyClient继承自ApiClient
- 这些就是我们刚才客户端远程调用的stub代码啦,当然,我们没有自动生成stub,也没有要求客户端跟服务端共用接口之类。实际上,我们认为完全没有必要做接口约束,大部分项目的服务接口很少,并且要求灵活多变
- stub就是类似于,刚才那个MyController实现IAbc接口,然后客户端根据服务端元数据自动在内存里面生成一个stub类并编译,这个类实现了IAbc接口。
客户端直接操作接口,还以为在调用服务端 的函数呢 - 其实stub代码内部,就是封装了 这里的InvokeAsync这些代码,等同于自动生成这些代码,包括gRPC、Thrift等都是这么干的
框架解析
- 这个RPC框架,封包协议就是刚才的SRMP,负载数据也就是协议是json
- 当需要高速传输的时候,参数用Byte[],它就会直接传输,不经json序列化,这是多年经验得到的灵活性与性能的最佳结合点
2.1 人人都有一个自己的高性能网络库
- 网络库核心代码:http://git.newlifex.com/NewLife/X/Tree/master/NewLife.Core/Net
- 我们一开始就是让Tcp/Udp可以混合使用,网络库设计于2005年,应该要比现如今绝大部分网络框架要老。服务端清一色采用 Server+Session 的方式。
- 网络库的几个精髓文件
- 其中比较重要的一个,里面实现了 Open/Close/Send/Receive 系列封装,Tcp/Udp略有不同,重载就好了。打开关闭比较简单,就不讲了
- 所有对象,不管客户端服务端,都实现ISocket。然后客户端Client,服务端Server+Session。tcp+udp同时支持并不难,因为它们都基于Socket。
- 目前无状态无会话的通信架构,做不到高性能。我们就是依靠长连接以及合并小包,实现超高吞吐量
- 一般灵活性和高性能都是互相矛盾的
2.2 高性能设计要点
- 第一要点:同步发送,因为要做发送队列、拆分、合并,等等,异步发送大大增加了复杂度。大家如果将来遇到诡异的40ms延迟,非常可能就是tcp的nodelay作怪,可以设为true解决
- 第二要点:IOCP,高吞吐率的服务端,一定是异步接收,而不是多线程同步。当然,可以指定若干个线程去select,也就是Linux里面常见的poll,那个不在这里讨论,Windows极少人这么干,大量资料表明,iocp更厉害。
- SAEA是.net/.netcore当下最流行的网络架构,我们可以通俗理解为,把这个缓冲区送给操作系统内核,待会有数据到来的时候,直接放在里面,这样子就减少了一次内核态到用户态的拷贝过程。
- 我们测试4.5Gbps,除以8,大概是 540M字节,这个拷贝成本极高
- 第三要点:零拷贝ZeroCopy,这也是netty的核心优势。iocp是为了减少内核态到用户态的拷贝,zerocopy进一步把这个数据交给用户层,不用拷贝了。
- 数据处理,我们采用了链式管道,
- 这些都是管道的编码器
- 数据处理,我们采用了链式管道,
- 第四要点:合并小包,NoDelay=false,允许tcp合并小包,MTU=1500,除了头部,一般是1472
- 第五要点:二进制序列化,消息报文尽可能短小,每个包1k,对于100Mbps,也就12M,理论上最多12000包,所以大量Json协议或者字符串协议,吞吐量都在1万上下
- SRMP头部4字节,ApiServer的消息报文,一般二三十个字节,甚至十几个字节
- 第五要点:批量操作,
User FindByID(int id); User[] FindByIDs(int[] ids);
最后
- 整理不全,大家凑合着看。中途录屏,语音啥的还掉了,准备得不是很好,下周再来一次吧,选Redis