openresty cosocket 连接池踩坑

cosocket 是 OpenResty 世界中技术、实用价值最高部分。是各种 lua-resty-* 非阻塞库的基础,让我们可以用非常低廉的成本,优雅的姿势,比传统 socket 编程效率高好几倍的方式进行网络编程。无论资源占用、执行效率、并发能力都非常出色。

最近有业务线同事反馈云端vad参数概率性不生效了,恰好最近在做集群迁移,同时有两个集群对外提供服务,拿到recordId看了一下出问题的请求都在新集群里。

由于我的服务直接访问识别服务的内部api,和经过云端vad服务后再访问识别服务做识别请求的path是不一样的。比对识别服务的日志可以确认原本从我的服务期望发送到云端vad服务的请求直接被转到了识别服务上面,同时也请负责云端vad服务的同事确认了,vad服务确实没有过滤出对应的recordId。

新老集群里服务拓扑不是完全一样的,从我的服务跨机房访问识别、vad这些服务需要经过一层网关,这个网关从老集群的kong切换到了新集群的apisix上,于是开始怀疑这一块是否存在问题,找了好几条请求请旺哥挨个确认了一遍,apisix上都没有查到相关的access日志。

于是想到服务开启了连接池,搜不到日志说明很可能是复用了连接,根据recordId查找识别服务的日志,确实印证了这个猜测。同时查看了识别服务上该条连接的日志,确实这条连接上处理了多次的asr请求

那么问题很可能就是出在了连接池上面,马上排查相关代码, connect建连接的代码如下

wc:connect(url, {pool = solution_res})

这里创建连接的时候,指定了按solution_res划分连接池,在请求云端vad服务和识别服务时,只要使用的solution_res一样,即使url不一样,连接池应该也是相同的。

看到这里,心里有一个疑问,如果是代码问题,那为什么老集群里的请求是正常的呢?原因是老集群里没有开启连接复用,既然代码里开启里连接池,那么只有用完连接后显式的调用了close来关闭连接, 相关代码在如下:

if asr.keepalive[solution_res] ~= false then
    asr:set_keepalive(...)
else
    asr:close()
end

老集群里etcd中的热配置连接的keepalive属性是false, 而这个配置新集群里是true,了解了问题的原因,立马先修改热配置关闭了keepalive,问题暂时修复了。

当然更彻底的修复需要将访问这两个不同的服务的连接池区分开,这就需要修改代码重新测试上线了。但是修改之前,决定看一下源码,确认一下上面的推断是否正确。

nginx-lua-module源码中cosocket的connect的C函数ngx_http_lua_socket_tcp_connect可以看到如果指定了pool参数,就会根据key_index对应的字符串查找连接池,如果没查到并且连接池配置的size大于0,则会创建一个连接池, 如果查到了存在的连接池则直接挂到ngx_http_lua_socket_tcp_upstream_t上,接下来就会交给ngx_http_lua_socket_tcp_connect_helper中处理,

    lua_pushlightuserdata(L, ngx_http_lua_lightudata_mask(socket_pool_key));
    lua_rawget(L, LUA_REGISTRYINDEX); /* table */
    lua_pushvalue(L, key_index); /* key */

    lua_rawget(L, -2);
    spool = lua_touserdata(L, -1);
    lua_pop(L, 1);

    if (spool != NULL) {
        u->socket_pool = spool;

    } else if (pool_size > 0) {
        lua_pushvalue(L, key_index);
        key.data = (u_char *) lua_tolstring(L, -1, &key.len);

        ngx_http_lua_socket_tcp_create_socket_pool(L, r, key, pool_size,
                                                   backlog, &spool);
       u->socket_pool = spool;
    }
    return ngx_http_lua_socket_tcp_connect_helper(L, u, r, ctx, p,
                                                  len, port, 0);

而在ngx_http_lua_socket_tcp_connect_helper上了就是先处理连接池存在的情况,首先就会尝试去连接池中取连接,如果可以取到就直接返回了。

    spool = u->socket_pool;
    if (spool != NULL) {
        rc = ngx_http_lua_get_keepalive_peer(r, u);

        if (rc == NGX_OK) {
            lua_pushinteger(L, 1);
            return 1;
        }

源码肯定了我的推测,如果connect中指定了相同的pool参数,即使url不同也可能复用到同一条连接从而导致连接错乱的现象,可以放心的修改代码了。

到这里问题还没完,我们有相关的e2e测试用例, 为什么新集群部署好导流量进来前测试的时候没有测试出来这个问题呢。带着疑问看了一下我们的测试用例,原来里面使用的solution_res和其他case里的solution_res不一样,对应的solution_res很少使用,此外线上有多个副本,每个副本还有多个nginx worker,这导致了仅仅跑测试的时候很难触发复用这个偏门的solution_res连接池的情况。

最后,其实集群迁移的时候其实比对过这些配置,当时keepalive全部打开也没多想,单纯觉得可以充分利用连接池,结果还是栽了,改配置的时候还是要多考虑带来的影响。


wechat
微信扫一扫,订阅我的博客动态^_^