Open C4o opened 1 year ago
兄弟客气了,欢迎交流!
如果不考虑语言,c/cpp/rust 应该是性能首选,golang、nodejs单纯测这种简单功能也还算快;
如果限制使用golang,有一些疑问:
如果用golang、同时存在的连接数量不是特别大,通常用golang标准库就可以了,连接数不是非常大的情况下、poller框架可能不如标准库响应性能好
首先感谢老哥回复。 用golang的话是因为方便维护而且团队人少,没有会 c/cpp/rust 的,之前 java 那套代码很长,而且原作者离职了,改起来稍微有点抽象。
然后老哥的疑问我一一回复下:
netpoll.SetNumLoops(runtime.GOMAXPROCS(0))
netpoll.DisableGopool()
listener, err := netpoll.CreateListener("tcp", fmt.Sprintf(":%d", np.Port))
if err != nil {
logger.Logf(logger.FATAL, "create netpoll listener failed")
}
if np.eventLoop, err = netpoll.NewEventLoop( handle, netpoll.WithOnPrepare(prepare), netpoll.WithReadTimeout(time.Second), netpoll.WithIdleTimeout(1*time.Minute), ); err != nil { logger.Logf(logger.FATAL, "failed to new netpoll event loop: %v", err) }
4. 打印了netpoll的连接数,64c的机器一直保持1600,我暂时没找到哪里改,后面本来想着改连接数来测试30ms的超时率
5. 我在openresty接收请求后先过一遍这个 tcp 服务,然后根据结果再决定要不要走到业务,然后目前openresty的平均耗时在20ms以内,除了那些超过30ms的,其他我打印下来都是比较快的,10ms内大概
6. 检测系统其实就类似 web 应用防火墙,每个请求包的检测,所以有多少外部请求,就要处理多少
7. openresty 和 tcp 服务的网络一般是同机房,测试下来问题应该不大,因为上面提到的gnet的超时率,我在本地openresty连接本地服务也会这样
8. 硬件64c,128g内存这样子,单机 1w qps 的话,跑起来cpu大概 3%,内存大概两百多M,因为包含了比较多的规则和缓存信息
本来是想用原生库来写的,但是因为比较急着灰度,所以先用netpoll跑起来的,目前来看暂时符合预期,但是这两天有活动,qps上涨,观察到30ms超时率也在增加,所以有点担心。
是这种拓扑对吗:
openresty -> golang检测服务 -> 其他web服务
openresty -> golang检测服务
和 golang检测服务 -> 其他web服务
都是同机房
如果是这样,那排除公网波动的问题
硬件64c,128g内存`
这个配置的话,我建议用标准库,单节点20w连接、20w协程完全没问题,响应性能应该是要比poller框架好的。
是这种拓扑对吗:
openresty -> golang检测服务 -> 其他web服务
openresty -> golang检测服务
和golang检测服务 -> 其他web服务
都是同机房如果是这样,那排除公网波动的问题
不是的,是openresty到web服务 openresty -> ( ngx_tcp_send -> golang -> ngx_tcp_receive -> openresty ) -> web service
你上面提到的比如 1w qps,以及后续每个节点20w连接后、20w qps,只是在每轮检测的时候才有这个高qps、不需要每秒都来一轮这个qps吧? 如果是这样,也可以试试根据检测间隔,做成时间轮、把连接错开分布,比如检测间隔是1分钟,那么每秒检测 20w/60 个连接,这样错开请求、避免同时拥堵、降低每个连接的响应延迟
协议解析,可以还用你们之前的tcp发送的部分,直接发送预先生成好的固定buffer,读取可以用http.ReadRequest或者你们如果已经封装的解析逻辑更简化和高效
openresty -> ( ngx_tcp_send -> golang -> ngx_tcp_receive -> openresty ) -> web service
我更懵了 :joy: 前面说的用gnet、netpoll慢,是指 ngx_tcp 请求 golang 到响应回来比较慢?
你上面提到的比如 1w qps,以及后续每个节点20w连接后、20w qps,只是在每轮检测的时候才有这个高qps、不需要每秒都来一轮这个qps吧? 如果是这样,也可以试试根据检测间隔,做成时间轮、把连接错开分布,比如检测间隔是1分钟,那么每秒检测 20w/60 个连接,这样错开请求、避免同时拥堵、降低每个连接的响应延迟
协议解析,可以还用你们之前的tcp发送的部分,直接发送预先生成好的固定buffer,读取可以用http.ReadRequest或者你们如果已经封装的解析逻辑更简化和高效
单台1w的qps是持续的,每一秒都是这样的 协议解析都已经做好了的,拆tcp包取http,然后解析http包
openresty -> ( ngx_tcp_send -> golang -> ngx_tcp_receive -> openresty ) -> web service
我更懵了 😂 前面说的用gnet、netpoll慢,是指 ngx_tcp 请求 golang 到响应回来比较慢?
对的 逻辑大概是这样的,通过这种方式打印跟golang服务的耗时,绝大部分都是10ms内
local sock = ngx.socket.tcp()
sock:settimeout(30)
local ok, err = sock:connect(host, port)
if not ok then
sock:close()
return
end
local req = 'flag ' .. #raw_header .. '\r\n' .. raw_header
local bytes, err = sock:send(req)
if not bytes then
sock:close()
return
end
local result, err = sock:receive()
if not result then
sock:close()
return
end
sock:setkeepalive(15000, 2000)
local cost = ngx.now()-start
local cost = cost*1000
if cost > 30 then
ngx.log(ngx.ERR, ngx.var.uri, " : ", cost, "ms")
end
单台1w的qps是持续的,每一秒都是这样的
单台1w,用标准库试试吧,我压测得到的数据,这种通常标准库更快些
协议解析都已经做好了的,拆tcp包取http,然后解析http包
跟用标准库的对比是你们自己实现的这个要快不?因为你前面说用的是gnet、netpoll,异步非阻塞的网络层,解析器速度未必比标准库同步解析快,但是检测的payload小、并且如果你们简化了解析逻辑去掉了不必要的,那是能快
单台1w的qps是持续的,每一秒都是这样的
单台1w,用标准库试试吧,我压测得到的数据,这种通常标准库更快些
好的,我先试试标准库,感谢老哥。现在单台是1w,后续的话单台会有20w qps,但是预期还是想要万分之一以内的30ms超时率
协议解析都已经做好了的,拆tcp包取http,然后解析http包
跟用标准库的对比是你们自己实现的这个要快不?因为你前面说用的是gnet、netpoll,异步非阻塞的网络层,解析器速度未必比标准库同步解析快,但是检测的payload小、并且如果你们简化了解析逻辑去掉了不必要的,那是能快
是的,目前测下来自己实现的http解析比http. ReadRequest要快一些,然后所有除了网络库操作以外的操作耗时,单个请求都在100微妙以内
func handle(ctx context.Context, conn netpoll.Connection) error {
now := time.Now()
defer func() {
// 100μs
Observe(time.Since(now).Seconds())
}()
// decode connection bytes
// read http package
// detector(request)
// conn.Write()
}
如果20w连接数确实大、延迟还是高,也可以试下nbio,可能不比gnet快太多、或者同一水平线,但是还可以更多定制,每个连接固定的读buffer,可以尝试ET单IO协程派发事件+不同的协程池、避免event loop协程里处理所有连接的IO导致的部分连接IO延迟(因为都在同一个for循环里排队)。 当然,要想更快,我建议是把epoll这部分拆出来,把不必要的功能都去掉、比如去掉锁、简化写失败时候的缓存等待可写逻辑。因为你的测试场景相当于echo、没有并发写、一个请求对应一个响应,而且只是测试可达、payload也不大。
gnet的benchmark,给标准库用16k的ReadBuffer: https://github.com/gnet-io/gnet-benchmarks/blob/v2/echo-net-server/main.go#L33
gnet自己用默认参数64k: https://github.com/gnet-io/gnet-benchmarks/blob/v2/echo-gnet-server/main.go https://github.com/panjf2000/gnet/blob/master/gnet.go#L465 https://github.com/panjf2000/gnet/blob/master/gnet.go#L418 虽然有区别:gnet是每个poller上一个read buffer所以buffer总内存占用不会太大,标准库每个连接一个buffer、所以单个连接不适合设置得特别大。但测吞吐,这样毕竟不是特别好。把标准库buffer也调大后其实标准库还是很快的
nbio性能对比其他框架、标准库在这里: https://github.com/lesismal/go-net-benchmark/issues/1 ,但最好还是自己跑下测试代码、跑多轮避免环境波动影响。
刚才用net写了下。。发现跟gnet一样的现象,就是如果包比较小的话(大概4k以内),会出现一小部分的请求超过30ms,但是一旦包大了,可能大部分后者全部请求耗时都会超过30ms,都是在本地测试的,之前用netpoll在本地暂时不会出现这种问题。现在拿nbio测一下。
用net是这么写的,老哥有空帮看下。
func process(conn net.Conn) {
var err error
var request []byte
var req http.Request
now := time.Now()
codec := Codec{}
remote := conn.RemoteAddr().String()
defer func() {
conn.Close()
}()
reader := bufio.NewReader(conn)
for {
request, err = codec.DecodeNet(remote, reader)
if err == ErrEmptyPackage {
break
}
// serialize request
//req, err = string2xttp(string(request))
req, err = http.NewRequest(string(request))
if err != nil {
logger.Logf(logger.VERBOSE, "failed to decode http from `%s`: %v", remote, err)
if _, err = conn.Write([]byte("OK;" + err.Error() + "\n")); err != nil {
}
continue
}
// code, reason, comment := detector.Detect(&req)
code = "NO"
reason = "test"
if _, err = conn.Write([]byte(string(code) + ";" + reason + "\n")); err != nil {
logger.Logf(logger.ERROR, "failed to write to conn `%s`: %v", remote, err)
}
}
}
func (std *Standard) Serve() {
listen, err := net.Listen("tcp", fmt.Sprintf(":%d", std.Port))
if err != nil {
fmt.Println("Listen() failed, err: ", err)
return
}
for {
conn, err := listen.Accept()
if err != nil {
fmt.Println("Accept() failed, err: ", err)
continue
}
go process(conn)
}
}
func (codec *Codec) DecodeNet(remote string, reader *bufio.Reader) (request []byte, err error) {
// 从reader里peek出对应的http包长度,然后返回request并且discard掉request的长度
}
可不可以提供下完整能跑的client server例子,我闲了跑下看看
可不可以提供下完整能跑的client server例子,我闲了跑下看看
好的,我整理下代码,感谢老哥。。
刚测试用了nbio,确实不存在上面说的gnet和net遇到的那种现象了。
但是想问一哈,OnData里的data,咋流式处理呀?之前用其他框架,好像一般都有peek和discard这种。
OnData里的data就是[]byte,raw bytes的方式自己处理
nbio每个poller是带一个读buffer,有可读事件时默认读到这个buffer里然后调用OnData,如果处理streaming half-packet,需要conn自己对应地去缓存,比如 nbio.Conn.SetSession(buffer) 可以设置session相关地信息、struct里带buffer也行。 但这样是需要从poller buffer拷贝到conn自己的buffer的,稍微浪费,也可以用engine.OnRead来设置自己处理读的handler,这样OnData就失效、nbio不自动读取,然后你自己的handler里直接读到自己conn的常驻buffer上就可以了。因为你的场景是echo、payload预计也不大,所以多数时候可能不会触发half-packet的情况,所以未必能提升太多,可以试试看
OnData里的data就是[]byte,raw bytes的方式自己处理
nbio每个poller是带一个读buffer,有可读事件时默认读到这个buffer里然后调用OnData,如果处理streaming half-packet,需要conn自己对应地去缓存,比如 nbio.Conn.SetSession(buffer) 可以设置session相关地信息、struct里带buffer也行。 但这样是需要从poller buffer拷贝到conn自己的buffer的,稍微浪费,也可以用engine.OnRead来设置自己处理读的handler,这样OnData就失效、nbio不自动读取,然后你自己的handler里直接读到自己conn的常驻buffer上就可以了。因为你的场景是echo、payload预计也不大,所以多数时候可能不会触发half-packet的情况,所以未必能提升太多,可以试试看
了解,我试一下。老哥,代码放在这了 https://github.com/C4o/go-net-demo
有client的代码吗?这个没看出来检测相关的
还有延迟统计的逻辑
有client的代码吗?这个没看出来检测相关的
client是用的nginx lua,然后直接请求nginx的方式来调用
lua代码是这样的,要放在openresty的lualib目录。延时统计有个ngx.log(ngx.ERR, ngx.var.uri, " : ", cost, "ms")
用来打印耗时的,然后设置了30ms超时,sock:settimeout(30)
,如果超时了就不会打印耗时了
local ngx_re_split = require("ngx.re").split
local _M = {}
function _M.handler()
local start = ngx.now()
local tcp_host = "127.0.0.1"
local tcp_port = 9002
local sock = ngx.socket.tcp()
sock:settimeout(30)
local ok, err = sock:connect(tcp_host, tcp_port)
if not ok then
ngx.log(ngx.ERR, "failed to connect ("..tcp_host..":"..tcp_port.. "): ", err)
sock:close()
return
end
-- header
local headers = ngx.req.get_headers()
-- 获取 raw_header
local raw_header = ngx.req.raw_header()
local req = 'FLAG ' .. #raw_header .. '\r\n' .. raw_header
local bytes, err = sock:send(req)
if not bytes then
ngx.log(ngx.ERR, "failed to send request ("..tcp_host..":"..tcp_port.."): ", err)
sock:close()
return
end
local result, err = sock:receive()
if not result then
ngx.log(ngx.ERR, "failed to sock:receive ("..tcp_host..":"..tcp_port.."): ", err)
sock:close()
return
end
sock:setkeepalive(15000, 2000)
local cost = ngx.now()-start
local cost = cost*1000
-- if cost > 30 then
-- 打印耗时
ngx.log(ngx.ERR, ngx.var.uri, " : ", cost, "ms")
-- end
local response_data = ngx_re_split(result, ";")
if response_data == nil then
return
end
local code = response_data[1]
if code == "NO" then
ngx.exit(403)
end
end
return _M
server段的配置加载个lua就好了
server {
listen 80;
server_name _;
error_log /var/logs/error.log;
location / {
rewrite_by_lua_block {
local cli = require("client")
cli.handler()
ngx.exit(200)
}
}
}
OnData里的data就是[]byte,raw bytes的方式自己处理
nbio每个poller是带一个读buffer,有可读事件时默认读到这个buffer里然后调用OnData,如果处理streaming half-packet,需要conn自己对应地去缓存,比如 nbio.Conn.SetSession(buffer) 可以设置session相关地信息、struct里带buffer也行。 但这样是需要从poller buffer拷贝到conn自己的buffer的,稍微浪费,也可以用engine.OnRead来设置自己处理读的handler,这样OnData就失效、nbio不自动读取,然后你自己的handler里直接读到自己conn的常驻buffer上就可以了。因为你的场景是echo、payload预计也不大,所以多数时候可能不会触发half-packet的情况,所以未必能提升太多,可以试试看
老哥这个说的,有示例代码吗?我这么写好像不太行。。刚才在测试环境跑了下,还是会粘包的,只用OnData处理不了。。
reader := bufio.NewReader(nbio.Conn)
reader.Peek()
reader.Discard()
不适合用阻塞的方式去读非阻塞的Conn,要不你还是先用OnData吧,OnData传给你的data是已经读取到的,自己处理粘包、半包缓存就可以了,或者参考默认读取的这块逻辑、自己定制OnRead: https://github.com/lesismal/nbio/blob/master/poller_epoll.go#L201
不适合用阻塞的方式去读非阻塞的Conn,要不你还是先用OnData吧,OnData传给你的data是已经读取到的,自己处理粘包、半包缓存就可以了,或者参考默认读取的这块逻辑、自己定制OnRead: https://github.com/lesismal/nbio/blob/master/poller_epoll.go#L201
了解,我先试试。
好的
老哥好,最近在使用到go网络框架的时候遇到些问题,看到老哥在一些仓库的issues中很多详细的分析,所以想请教一下,打扰了。
最近写一个检测系统,使用场景是通过openresty cosocket来同步检测,并且控制超时是30ms,预期万分之一的超时率。然后检测系统的耗时总和测下来一直是100微秒以内。
在选择网络处理框架的时候,一开始使用gnet v2版本,测下来在单机1w qps的时候就会有10%以上超过30ms了;后面改用了netpoll,虽然单机1w qps的30ms超时率在十万分之六左右,但是发现当qps超过1w5的时候,30ms超时率会上升到十万分之十二。后期这套检测系统的qps大概会有400w,机器二十台,单机20w qps的样子,所以担心会扛不住。
然后之前有一套netty写的类似的,单机3w qps大概只有十万分之五的30ms超时率。所以想着老哥愿意的话帮忙看下什么框架能够符合需求。十分感谢。