您现在的位置是:首页 > 正文

游戏思考14:对cache_server缓冲服务器的问题思考(读云风博客有感)

2024-02-01 02:36:30阅读 1

一、游戏服务器的作用类别

引用链接:MMORPG服务器类别介绍

二、原本cache_server的设计

  • 结构
    cache server 的协议设计非常简陋。就是顺序的提交请求,然后每个请求会有序的得到一个回应。这些请求要么是获取 GET 文件,要么是上传 PUT 文件。其中 PUT 文件在协议上不必回应。

  • 我的理解
    想一般的MMORPG游戏会根据客户端提供的id(雪花生成的),判断这个ID是不是人物的ID、工会的ID、NPC的ID等等(其实就是根据宏定义,用联合体强转,用前6位或7位做对比),然后根据客户端的请求信息返回给他对应的消息。(不清楚为啥云风的服务器需要GET\PUT大量文件?)

三、问题展现

  • 前提
    这些请求要么是获取 GET 文件,要么是上传 PUT 文件。其中 PUT 文件在协议上不必回应。
  • 问题
    1)问题一:PUT的问题
    由于 PUT 文件没有回应,所以客户端无法直接确定文件是否全部上传完毕;如果必须确认,只能在 PUT 文件结束后,再提交一个 GET 请求。如果收到了后续 GET 的回应,可以理解为前一个 PUT 已经结束。实际上,Unity 客户端没想去确认 PUT 是否结束,从 log 分析,它只是简单的在最后一个 PUT 结束后等待了一段时间再断开连接。
    2)问题二:这种依赖严格次序的协议,在面对两边数据量不对等、网络速度不对等的近况时,很难有一个健壮的实现。

四、假设是阻塞网络

  • 伪代码体现
while true do
  local req = get_request(fd)
  local resp = handle_request(req)
  put_response(fd, resp)
end
  • 注释
    即用一个死循环,依次获取网络请求,针对请求生成回应数据,然后将回应数据经网络发回。

  • 可能导致死锁的原因
    1)假设 get_request 是阻塞读网络,put_response 是阻塞写网络,那么就要求客户端也是严格的配合:客户端也必须提起一个请求后,等待回应,然后再提下一个请求。否则,若客户端连续提两个请求,服务器在处理第一个请求后,推送的回应客户端不去接收(因为客户端还在提第二个请求),就可能会死锁。
    2)死锁发生时,客户端在推送第二个请求(写操作),而服务器在推送第一个回应(写操作);两边都没在收取对方的数据,两侧的 api 都等待在写网络上(因为对端不读)。

五、读写分离带来的OOM(内存溢出)的问题

  • 现在服务器的基本做法
    一般会将网络读写分离到独立线程中,死锁不会发生。服务器收到新请求就能处理,产生出回应数据。而回应数据将缓存在网络线程中,等待客户端接收,而不会阻塞住上面的业务循环。那里的 put_response 是非阻塞的。

  • 基本做法的缺点
    因为请求和回应是不对等的,客户端可以轻易的发起大量的 GET 请求,一条几十字节的 GET 请求,很可能需要几十上白兆的回应包。巨量的回应包积压在网络线程的发送队列中,很快就会吃光所有的内存。

  • 做法优化
    所以,put_response 这个函数必须在内存耗光前阻塞住,前面的问题就会回来。所以,合理的服务器设计必须分离 get_request 和 put_response 到两个执行序列里。

六、早期unity的缓冲服务器的设计和现在unity的设计

只有一个简单的 js 文件,跑在 nodejs 服务中。nodejs 是基于回调机制的,请求处理放在了 socket 的 data 事件回调中,每个请求都会生成一个新的对象,这个对象会进入一个队列,由 socket 的可写事件触发出队列操作,将文件 pipe 到 socket 上。因为回应操作是由文件的 pipe 到 socket 依次完成的,这个过程可能很慢(取决于对端的接收进度),那么新请求非常可能积压在队列中。假设客户端一直推送请求,而疏于处理回应的话,这个队列将一直增长,直到 OOM 发生。

  • 现在unity的做法
    现在的 cacheserver 版本已经变得非常复杂,不太容易看清楚。我简单浏览了一下,觉得依旧存在这个隐患:在 server/command_processor.js 文件中,_onGet 函数会把要回应的 item 压入队列(this[kSendFileQueue].push(item)😉 这个队列可能无限增长。

七、最终优化的缓冲服务器方法

云风现在的实现也是类似的机制,伪代码如下:

-- request thread
while true do
  local req = get_request(fd)
  push_queue(q, req)
end

-- response thread
while true do
  local req = pop_queue(q)
  local resp = handle_request(req)
  put_response(fd, resp)
end
  • 云风做法
    这里的 push_queue 在达到队列预设的容量后,是会阻塞等待另一个线程的 pop_queue 取走再继续工作的。我们在做此修改后,把 queue 的容量设置为 8192 ,实际运行时,客户反馈以前正常的打包过程(其实会让服务器濒临 OOM 崩溃),现在有时会卡在和 cache server 的通讯上。经过线上观察(使用 skynet 预留的 debug console 的 debug 功能进入服务查看内部状态),发现这个 queue 很容易就满了,等待 pop_queue ;而能执行 pop_queue 的线程却阻塞在 put_response 上,也就是 unity 客户端拒绝接收前面那 8000 个请求产生的回应。

  • 选择点
    针对这种情况的合理推测是, unity 在某些极端情况下,一口气发了上万(甚至十万个)请求,它在这些请求全部从网络发出之前,没有跑网络接收的业务,导致数据全部堵在网络层;而服务器为了避免自己内存耗尽,只能暂停接收新的请求,结果就卡了。换句话说,针对客户端不合理的使用:不断地发送请求,拒绝处理回应,那么服务器若想一直服务下去,只能在内存耗尽卡住间二选一。当然还有拒绝服务的第三条路,即在异常情况(卡住)后,踢掉客户端。客户端发现断线,就会重连服务器再来一次。

  • 最终对策
    我们最终的对策是,优化队列,让队列中保存的数据足够的少(这里可以只讲客户端请求 id 保留在队列中,每个请求所需内存在 100 字节以下)然后增加队列的容量上限到百万级;当队列满时踢掉客户端。

  • 原博文传送们
    传送门

  • 做法建议
    1)非阻塞 API + 流式读写 + 线程池(这里流式读写啥意思,不懂?)
    2)可以记录一下客户端发送但没有接收的请求数,超出一个限额之后就不再把请求放进队列,而是往队列放进一个需踢掉客户端的标记(但不立即踢除)。这样该客户端能保证顺序接收到限额内的文件再被踢掉——使用者如果发现被踢掉,多跑几遍就是了,这种实现每次总能多接受到一点数据的。

网站文章

  • zhang-Suen图像骨架提取(原理和代码)

    zhang-Suen图像骨架提取(原理和代码)

    转自东方fan的博客,感谢! 该算法有四个条件,若满足,则该点置为0。 或: 其中(a)(b)的意思为: 中心像素P1周围的目标像素(二值中的1)的个数之和在2和6之间。 8邻域像素中,按顺时针方向,...

    2024-02-01 02:36:22
  • 【leetcode-python】541. 反转字符串 II

    给定一个字符串 s 和一个整数 k,你需要对从字符串开头算起的每隔 2k 个字符的前 k 个字符进行反转。 如果剩余字符少于 k 个,则将剩余字符全部反转。 如果剩余字符小于 2k 但大于或等于 k ...

    2024-02-01 02:36:15
  • 3-Flutter常用组件--标题栏和状态栏

    3-Flutter常用组件--标题栏和状态栏

    文章目录1.BottomAppBar属性2.BottomNavigationBar属性BottomNavigationBarItem3.SliverAppBar属性1.FlexibleSpaceBar...

    2024-02-01 02:35:45
  • JS 常用数组

    JS 常用数组

    从 0 开始计算的索引,表示要开始改变数组的位置一个整数,表示数组中要从start开始删除的元素数量。(0代表不删除)item1itemN从start开始要加入到数组中的元素(如果不指定,那不增加元素)

    2024-02-01 02:35:38
  • 利用图神经网络进行药物再利用的计算方法(下)

    利用图神经网络进行药物再利用的计算方法(下)

    本研究提出了一种图神经网络药物再利用模型,我们称之为GDRnet,以有效地筛选大型批准药物数据库,并预测新疾病的可能治疗方法。我们将药物再利用作为一个多层异构网络中的链接预测问题,该网络约有140万条...

    2024-02-01 02:35:33
  • Java反射调用ashx

    Java反射调用ashx

    Java发送调用jar包的方法,转换为基类接口调用

    2024-02-01 02:35:05
  • mysql事务机制

    mysql事务机制

    一、事务的基本要素(ACID) 1、原子性(Atomicity) 2、一致性(Consistency) 3、隔离性(Isolation) 4、持久性(Durability) 二、事务的并发问题   1...

    2024-02-01 02:34:58
  • 字符串加空格

    题目描述给定一个字符串,在字符串的每个字符之间都加一个空格。输出修改后的新字符串。输入共一行,包含一个字符串。注意字符串中可能包含空格。数据范围1≤字符串长度≤100输出输出增加空格后的字符串。样例输...

    2024-02-01 02:34:52
  • 将训练好的 mmdetection 模型转为 tensorrt 模型

    mmdetection 是商汤科技(2018 COCO 目标检测挑战赛冠军)和香港中文大学开源的基于Pytorch实现的深度学习目标检测工具箱,性能强大,运算效率高,配置化编程,比较容易训练、测试。但...

    2024-02-01 02:34:23
  • 多样性数据源报表如何做?几行代码就能解决。

    多样性数据源报表如何做?几行代码就能解决。

    ​现代应用已经进入多数据源阶段了,不再是一个单一的数据库包打天下,一个应用中会涉及除关系数据库外各种数据源,如文本文件类数据、NOSQL、多维数据库、HTML Webservice等等,即使是关系数据...

    2024-02-01 02:34:17