V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
Game Engines
Unreal Engine
MyCryENGINE
gantleman
V2EX  ›  游戏开发

为什么 luacluster 可以实现万人同屏?

  •  
  •   gantleman · 2022-04-18 09:58:48 +08:00 · 3829 次点击
    这是一个创建于 711 天前的主题,其中的信息可能已经有所发展或是发生改变。
    这篇文章我们来谈谈 luacluster 的整体架构和性能。以及为什么 luacluster 可以实现万人同屏?

    luacluster 整体架构非常的简洁。node 对应进程, docker 对应线程,每个 docker 有一个 luavm 和一个消息队列,在 luavm 里创建 entity 对象。net 网络和 log 日志分别单独线程运行。没有任何多余累赘的东西非常简洁明了。

    luacluster 的设计目标是让任何 entity 之间,可以通过 rpc 的方式无脑异步调用。所谓 rpc 的方式就是在任何 entity 中使用 entity 的 id ,函数名,参数就可以异步调用任何 entity 。

    luacluster 是一个分布式和并行的系统。分布式代表着多进程,并行代表着多多线程。也就是说 luacluster 要想实现一个穿透进程和线程阻隔。能够让任意的 entity 之间像调用普通系统接口一样互相调用。哪么 luacluster 必然是一个异步的系统。也就是说任何在 luacluster 的 entity 之间的功能调用都是异步的。

    例如在 bigworld 对象中通知 entity 进入 sudoku 空间的部分

    local entityProxy = udpproxy.New(id)
    entityProxy:OnEntryWorld(self.spaceType, self.beginx, self.beginz, self.endx, self.endz)
    就是通过 entity id 创建一个远程对象代理。然后通过远程对象代理调用 account 的 OnEntryWorld 函数。如果 entity 是在当前进程内,就会查找 docker id 并投递到指定消息队列。如果在其他 node 就会使用 udp 协议发送过去。通过 id 就能找到对应 entity 的诀窍,是因为把 ip 地址,docker id ,entity id 都塞进了这个 unint64 里。你可以在 entityid.h 中找到 entity id 的定义。

    typedef struct _EID {
    unsigned short id;
    unsigned int addr;//ipv4
    unsigned char dock;
    unsigned char port;//UDP 端口号的偏移
    }*PEID, EID;

    typedef union idl64
    {
    volatile EID eid;
    volatile unsigned long long u;
    volatile double d;
    } idl64;
    这样我们就实现了一个非常惊人和高效的异步通信系统。任意进程和线程中的对象通信只要最多 2 步就可以完成。找到 upd 端口发过去,找到线程队列发过去。在任意环境下只要拿到 entity id 就可以快速知道封包的目的地。实现在分布式网络内的任何对象之间像普通函数调用一样的调用。什么网络编程,什么多线编程可以统统见鬼去了。

    第二个是关于消息风暴的问题。
    万人同屏顾名思义要做服务器上处理 1 万个玩家的位置同步问题。1 万个玩家的位置同步每次要产生 1 亿个消息。1 万乘 1 万产生 1 亿个消息。请记住 1 亿这个数字后面我们要反复提及到。

    首先我们先分析 1 亿个消息的产生流程。服务器会收到 1 万个客户端发起移动的请求。1 万个请求是没有问题的,现在服务器处理 10 万个链接问题都不大。所以这 1 万个请求一般的服务器压力都不大。问题是这 1 万个请求,每个请求要产生 1 万个新的请求发送给其他的玩家。这样服务器就扛不住了。一下产生了 1 个亿的 io 需求,哪种消息队列都扛不住,直接 mutex 就锁死了。

    所以我使用了 CreateMsgList 接口创建了一个消息 list 。哪么 io 请求就转变为插入 1 万个 list 的操作。然后将这个 list 和消息队列合并在一起。这样就 1 亿个 io 请求变为 1 万个 io 请求,io 请求一下就压缩了 1 万倍。同样的道理我们也可以把发送给给客户端的封包进行压缩。处理 1 亿个封包请求很难但处理 1 万个封包难度就低很多了。下面是封包压缩的部分代码。

    void DockerSendToClient(void* pVoid, unsigned long long did, unsigned long long pid, const char* pc, size_t s) {

    if (pVoid == 0)return;
    PDockerHandle pDockerHandle = pVoid;
    idl64 eid;
    eid.u = pid;
    unsigned int addr = eid.eid.addr;
    unsigned char port = ~eid.eid.port;
    unsigned char docker = eid.eid.dock;

    unsigned int len = sizeof(ProtoRoute) + s;
    PProtoHead pProtoHead = malloc(len);
    if (pProtoHead == 0)
    return;
    ...

    sds pbuf = dictGetVal(entryCurrent);
    size_t l = sdslen(pbuf) + len;
    if (l > pDocksHandle->packetSize) {
    DockerSendPacket(pid, pbuf);
    sdsclear(pbuf);
    }
    if (sdsavail(pbuf) < len) {
    pbuf = sdsMakeRoomFor(pbuf, len);

    if (pbuf == NULL) {
    n_error("DockerSendToClient2 sdsMakeRoomFor is null");
    return;
    }

    dictSetVal(pDockerHandle->entitiesCache, entryCurrent, pbuf);
    }
    memcpy(pbuf + sdslen(pbuf), pProtoHead, len);
    sdsIncrLen(pbuf, (int)len);

    pDockerHandle->stat_packet_count++;
    当然封包压缩并不能解决我们的所有问题。处理 1 亿个请求并压缩的挑战也极为艰巨。在 128 核的服务器上,每个核心每秒钟只能处理 20 万次。而 1 亿个请求需要 78 万次的处理能力。1 亿个请求在 128 核上需要大概 4 秒钟以上才能处理完成。或者...每秒钟只处理 20%的玩家。也就是说我们只能保证每秒钟处理 20%的玩家请求。这样就要祭出我们另一个大杀器“状态同步”。我们要把玩家的移动描述成一段时间的状态。有位置,方向,速度,开始时间,停止时间的完整状态。这样在每个客户端就可以根据这些信息,推断出玩家移动的正确状态。

    #在 MoveTo(x, y, z)功能中同步玩家状态的部分代码
    local list = docker.CreateMsgList()
    • for k, v1 in pairs(self.entities) do
    • local v = entitymng.EntityDataGet(k)
    • if v ~= nil then
    • local view = udpproxylist.New(v[1], list)
    • view:OnMove(self.id, self.transform.position.x, self.transform.position.y, self.transform.position.z
    • ,self.transform.rotation.x, self.transform.rotation.y, self.transform.rotation.z, self.transform.velocity
    • , self.transform.stamp, self.transform.stampStop)
    • end
    • end
    • docker.PushAllMsgList(list)
    • docker.DestoryMsgList(list)
    这样我们就能保证在任意状态下只有小于 20%的玩家请求需要处理。但不要乐观虽然可以解决移动的问题。但当新玩家进入场景时,还必然要同步所有玩家的数据并把新玩家的数据同步给其他玩家。如果场景内有 5 千个玩家,再放入 5 千个玩家。两边玩家需要同步的数据是“5 千 * 5 千+5 千 * 5 千”一共 5 千万。虽然比 1 亿要少一半但前面分析过 128 核服务器只能处理 2560 万。“我们可以上 256 核心服务器”,“哦对对对”。

    当然不用上 256 核心服务器了。我们可以换一个方式,就是每次只放进去 500 人。这样需要同步的数据最多就变成 500*1 万 + 500 万,1000 万。这样就能满足我们的硬件需求了。好了今天就到这里吧,祝大家周末愉快。
    原文及图片地址: https://zhuanlan.zhihu.com/p/499296245
    10 条回复    2022-04-18 17:52:13 +08:00
    lesismal
        1
    lesismal  
       2022-04-18 10:38:36 +08:00   ❤️ 9
    做技术的不踏踏实实,半吊子老来故弄玄虚地吹是真烦
    efaun
        2
    efaun  
       2022-04-18 10:48:43 +08:00   ❤️ 3
    你不如直接快进到卖课
    skies457
        3
    skies457  
       2022-04-18 10:51:55 +08:00   ❤️ 7
    > 在 128 核的服务器上,每个核心每秒钟只能处理 20 万次

    可能是盗版 CPU 的受害者
    leisure
        4
    leisure  
       2022-04-18 10:56:34 +08:00   ❤️ 1
    万人同屏更多考虑的难道不是渲染的性能吗?
    learningman
        5
    learningman  
       2022-04-18 12:24:17 +08:00
    一般来说,我们认为单核 CPU 每秒可以执行 10^9 次运算。。。
    mattx
        6
    mattx  
       2022-04-18 13:43:50 +08:00 via iPhone   ❤️ 1
    游戏从业者感受,技术都没摸清,又来故弄玄虚。
    wsgfz000
        7
    wsgfz000  
       2022-04-18 14:24:36 +08:00
    为什么这文体看起来这么像....爽文?
    dcoder
        8
    dcoder  
       2022-04-18 15:23:54 +08:00
    我也写游戏后端, 进来看到大家都在批楼主, 就放心了
    liyang5945
        9
    liyang5945  
       2022-04-18 15:27:03 +08:00 via Android
    写的什么 jb
    lysS
        10
    lysS  
       2022-04-18 17:52:13 +08:00
    看历史记录,这兄弟多少有点问题
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   3641 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 29ms · UTC 10:27 · PVG 18:27 · LAX 03:27 · JFK 06:27
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.