作者:教授
我们关注到某个游戏服在凌晨的时候发生了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 查询。
示例代码: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
由于每个服务去调用 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 方案进行规避,但最好还是两套方案都同时做好预案,这样在业务首发等重要场景下能最大化规避停机风险,毕竟停服还是带来了不必要的成本。