最近利用业余时间写了一个简单的分布式对象存储 https://github.com/chengdedeng/yfs , 分布式一致性构建在 https://github.com/atomix/atomix 之上. atomix 实现了 https://raft.github.io/ , 并且提供了更高层次的抽象, 例如 Map,Set,DistributedLock 等. 为了详细的阐述遇到问题的来龙去脉, 有必要简单阐述下 yfs 的架构, 架构图如下:
从上图可知 yfs 由 Gateway 和分组的 Store 两部分组成, Gateway 主要负责路由, 鉴权, 流控, 安全等非存储功能, Store 主要负责存储. 每个 Group 至少由三个 Store 节点组成, 这三个 Store 所存储的数据一模一样, 也就是说每个文件至少有三个备份. Gateway 也至少有三个节点, Store 节点会自动上报 metadata 给 Gateway,Gateway 根据 Store 节点上报的信息来调整自己的路由策略. Gateway 是在我的例外一个开源项目 https://github.com/chengdedeng/waf 的基础上改造的而来的, 每个 Store 节点都运行着一个 Springboot 服务, 用来提供上传和下载服务.
问题一
当上传的文件大于 Store 允许上传的文件 size 时, 发现前端一直在等待响应结果, HTTP 请求传递过程是
- Browser->Gateway->Store
- .
分析
首先尝试在 Store 上 debug 上传接口, 发现请求压根就没有进入到 controller, 一搜发现 https://stackoverflow.com/questions/21089106/converting-multipartfile-to-java-io-file-without-copying-to-local-machine 里面已经说的很清楚, 原来只有等文件上传完成之后, 才会进入到 controller 中.
那为什么没有上传完成呢, wireshark 一抓包发现, 当数据上传部分之后, Tomcat TCP 端口突然发送了一个 RST, 为什么会出现这种情况呢? 一查原来当 Tomcat 发现上传的文件大于允许上传的文件时, Tomcat 就直接 RST TCP 链接. 其实原因很简单, 既然上传的文件不符合规范, 后面的数据包即使发送过来也会被扔掉, 那与其这样, 不如不要上传了, 还节省上传宝贵的网络带宽. 思路是没有错, 但是这样 HTTP 层面就拿不到任何数据, 浏览器最多报一个网络异常, 例如 Chrome 会出现 net::ERR_CONNECTION_RESET. 假如我们允许上传的最大文件是 10M, 如果上传的文件大于 10M 小于 20M, 我们希望返回一个 http response 告诉前端上传的文件不符合规范, 20M 以上我们就认为是异常流量或者攻击直接 RST TCP. 这样的方案既方便测试, 又对使用者友好, 那该如何实施呢?
Tomcat 的配置参数 MaxSwallowSize 从意思上来说已经非常清晰, 由于 Store 节点运行的是 Springboot 服务, 所以需要重新配置 TomcatEmbeddedServletContainerFactory. 设置之后经测试发现 20M 以下的, 前端会迅速收到响应结果, 但是 20M 以上的还是存在问题, 因为 20M 以上的 Tomcat 还是会 RST TCP.
通过 netstat 发现 Gateway->Store 之间的链路 RST 之后, Browser->Gateway 之间的链路并没有断开, 看来问题的关键就在这里了. 发现问题的关键点之后, 改起来就简单, 我在 Gateway 上给 Gateway->Store 之间的 channel 加了一个 CloseFuture Listener 从而去关闭 Browser->Gateway 之间的 channel, 经测试发现问题解决.
问题二
正当我准备逛下论坛庆祝一下的时候, 运维部署到了测试环境, 结果任何请求 Nginx 都报
upstream prematurely closed connection while reading upstream
, 顿时我就懵逼了.
分析
测试环境 HTTP 请求传递过程是
Browser->Nginx->Gateway->Store
, 跟我开发环境不太一样, 多了一层 Nginx, 通过错误信息可以知道 Nginx 的 upstream 也就是 Gateway 出现了问题.
connection close, 那么必然会出现 TCP 关闭, 在 Nginx 服务器上经过抓包一看, 发现 Gateway 主动关闭了 Nginx->Gateway 之间的 TCP 通道. 到 Gateway 服务器上抓包, 发现 Gateway 先关闭了 Gateway->Store 之间的通道, 然后由于 CloseFuture Listener 接着关闭 Nginx->Gateway 之间的通道. Gateway 每次都主动发起关闭, 为什么会这样呢? 我本地从来没有出现过这样的现象, Gateway 和 Store 之间应该是长连接才对啊.
仔细分析整个链路, 先是 Browser->Nginx, 发现走的 HTTP1.1, 没有任何问题, 但是 Nginx->Gateway 却走的 HTTP1.0, 看来问题的关键就出在这个地方了. 原来当 Store 返回响应结果之后, 由于 HTTP1.0 是短链接, 所以 Gateway 主动发起关闭. 由于我前面给 Gateway->Store 的 channel 加了关闭的监听, 所以 Nginx->Gateway 之间的 channel 也会立即关闭, 所以导致 Nginx 会报上面的错误.
接着检查了 Nginx 的配置, 果然发现 Nginx 到 Gateway 之前 HTTP1.1 的配置不正确, 加入如下配置之后问题解决.
- proxy_http_version 1.1;
- proxy_set_header Connection "";
所以
CloseFuture Listener
方案只能工作在 HTTP1.1 之上, 也就是说 Gateway->Store 之间 channel 的关闭, 只可能是由于 idle 超时或者异常关闭, 否者就不应选择上面的方案.
总结
网络问题的定位, 通常都不是非常容易, 但是如果通过 wireshark 等抓包工具来配合研究协议和报文, 一般会事半功倍. TCP 和 HTTP 是值得投入时间来学习和研究的协议, 因为它们就是迷雾中的灯塔, 虽然你不知道岸在何处, 但是你却知道没有偏离航向, 到达彼岸只是时间问题.
来源: https://juejin.im/entry/5adff0ae51882567336a63dc