记录一次使用 Skynet 框架引发的 Bug

作者:教授

一、事件过程

我们关注到某个游戏服在凌晨的时候发生了2次偶然的全服掉线,游戏进程 CPU 使用率忽高忽低。日志里报请求 SDK 登录域名有大量超时。

从现象入手,检查了网络、机器各方面性能,未发现异常。初步怀疑是 SDK 域名请求质量问题,联系 SDK 运维同事一起排查,先对域名请求进行优化,待观察。

此后不久,问题再次出现了,现象仍然是全服掉线,而且研发提供了关键信息,程序日志不只是报 SDK 域名超时,很多 HTTP 的请求都超时了,其他的线程逻辑同样也出现了处理超时,整个进程像被 Hang 住。

这下子,终于明白了什么了… 我们早在 2015 年时,有个项目使用 Skynet 框架出现过类似的问题,一开始也是莫名其妙的掉线,进程异常。最终发现原来是 skynet 底层设计的一个 Bug。

在 Skynet 的底层,当使用域名而不是 IP 时,由于调用了系统 API getaddrinfo ,有可能阻塞住整个 Socket 线程(不仅仅是阻塞当前服务,而是阻塞整个 Skynet 节点的网络消息处理,而使用了类似 httpc 这样的模块以域名形式向外请求时,一定要关注这个问题。玩家在登陆时 服务端需要请求第三方 HTTP 接口,极大可能触发拥塞,导致玩家无法登陆或者被卡掉线的问题发生。

有了关键信息,处理起来就顺畅多了(多亏了多年来的知识传承),由于主要是进程用了系统 getaddrinfo 引发的问题,那么得到解决的办法就是让进程直接请求 IP 而不是 HTTP,这样就可以绕过域名请求,规避调用 getaddrinfo

二、制定临时处理方案

部署 Nginx,做反向代理,通过访问本地端口,反向代理到对应域名。程序请求本地 IP 和端口,由 Nginx 转发到对应的域名上。

例如:
127.0.0.1:8100 对应 sdk.shouyou.com
127.0.0.1:8101 对应 api.shouyou.com
127.0.0.1:8102 对应 openapi.xg.qq.com

程序原本请求 http://sdk.shouyou.com,则以请求 http://127.0.0.1:8100 来代替。程序则可以跳过域名解析过程。

配置模板:

#==============================================================
# Revision: 1.1
# Description:
# 游戏服所有http请求都要走反向代理,由【IP+端口】请求形式转发到域名请求出去
# 说明:
#       {
#           "127.0.0.1:8100" => "http://sdk.shouyou.com"
#           "127.0.0.1:8101" => "http://api.shouyou.com"
#           "127.0.0.1:8102" => "http://openapi.xg.qq.com"
#       }
#==============================================================

#域名配置 sdk.shouyou.com 
server {
    listen 127.0.0.1:8100 ;

    location /{
        resolver 127.0.0.1 ipv6=off;
        set $host1 shouyou.com;
        proxy_set_header Host $host1;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_pass $scheme://$host1;
    }
    access_log /data/logs/sdk.shouyou.com.proxy.log;
}

...

到这里可能会有疑问:“为什么不直接加 host?”

因为程序访问域名,只能单线程递归访问,即在大量频繁的请求下,无论解析多快,都可能出现堵塞。优化域名解析时间,只是优化堵塞的临界点,不能从根本上解决问题。因此,在官方没有给出最优解决方案之前,使用 Nginx 转发是最有效的解决方式。

不过我们发现 Skynet 官方已经发现了这个问题,并给出了修复方案。

三、官方解决方案

Skynet 官方暂时不打算在底层实现非阻塞的域名查询。但提供了一个上层模块来辅助解决 DNS 查询时造成的线程阻塞问题。

local dns = require “skynet.dns”

在使用前,必须设置 dns 服务器。

dns.server(ip, port) :port 的默认值为 53。如果不填写 ip 的话,将从 /etc/resolv.conf 中找到合适的 ip 。

dns.resolve(name, ipv6) : 查询 name 对应的 ip ,如果 ipv6 为 true 则查询 ipv6 地址,默认为 false 。如果查询失败将抛出异常,成功则返回 ip ,以及一张包含有所有 ip 的 table 。

dns.flush() : 默认情况下,模块会根据 TTL 值 cache 查询结果。在查询超时的情况下,也可能返回之前的结果。dns.flush() 可以用来清空 cache 。注意:cache 保存在调用者的服务中,并非针对整个 skynet 进程。所以,推荐写一个独立的 dns 查询服务统一处理 dns 查询。

3.1 DNS 简单使用

示例代码:test_dns.lua

local skynet = require "skynet"
local dns = require "skynet.dns"

skynet.start(function()
    skynet.error("nameserver:", dns.server())   --设置 DNS 服务器地址
    -- you can specify the server like dns.server("8.8.4.4", 53)
    
    local ip, ips = dns.resolve "github.com"  --调用成功,则把结果缓存到这个服务的内存中,便于下次使用
    skynet.error("dns.resolve return:", ip)
    
    for k,v in ipairs(ips) do
        skynet.error("github.com",v)
    end
    dns.flush()
end)

运行结果:

$ ./skynet examples/config
test_dns
[:01000010] LAUNCH snlua test_dns
[:01000010] nameserver: 127.0.1.1
[:01000010] dns.resolve return: 192.30.255.112  #返回查询到的 ip 地址
[:01000010] github.com 192.30.255.112
[:01000010] github.com 192.30.255.113

3.2 封装一个 DNS 服务

由于每个服务去调用 DNS 接口查询IP时都会在这个服务上缓存一份,下次查询的时候速度就会快很多,但是如果每个服务都保存一份,显示是浪费了资源空间,下面我们来封装用”lua”消息进行查询的 DNS 服务。

示例代码:dnsservice.lua

local skynet = require "skynet"
require "skynet.manager"
local dns = require "skynet.dns"
    
local command = {}

function command.FLUSH()
    return dns.flush()
end


function command.GETIP(domain)
    return dns.resolve(domain)
end

skynet.start(function()
    dns.server()
    skynet.dispatch("lua", function(session, address, cmd, ...)
        cmd = cmd:upper()  
        local f = command[cmd] 
        if f then
            skynet.retpack(f(...))
        else
            skynet.error(string.format("Unknown command %s", tostring(cmd)))
        end
    end)
    skynet.register ".dnsservice" 
end)

测试代码:testdnsservice.lua

local skynet = require "skynet" 
local cmd,domain = ...
function task()
    local r, ips = skynet.call(".dnsservice", "lua", cmd, domain)
    skynet.error("dnsservice Test:", domain, r)
    skynet.exit()
end
skynet.start(function()
    skynet.fork(task)
end)

运行结果:

$ ./skynet examples/config
dnsservice
[:01000010] LAUNCH snlua dnsservice
testdnsservice getip www.baidu.com
[:01000012] LAUNCH snlua testdnsservice getip www.baidu.com
[:01000012] dnsservice Test 14.215.177.39
[:01000012] KILL self

原文链接:https://blog.csdn.net/qq769651718/article/details/79435025

以上方案是官方给出的修复方案。

四、总结

通过线上验证,两种方案均可以规避了游戏掉线问题,至此,问题得到了解决。其实问题是时隔了多年再次出现,再次修复。虽然是有前例参考,但是实际上是可以从业务源头规避的。在项目接入时,对于 Skynet 项目就应该对研发做好宣讲,避坑手册等等,而不是到了业务上线出了问题才给补锅。

另,如果时间能来得及,优先建议使用官方给出的修复方案。如遇到项目临近上线来不及进行修复,可以选择 Nginx 方案进行规避,但最好还是两套方案都同时做好预案,这样在业务首发等重要场景下能最大化规避停机风险,毕竟停服还是带来了不必要的成本。

赞(23)
未经允许不得转载:运维军团 » 记录一次使用 Skynet 框架引发的 Bug

评论 抢沙发

*

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址