1、出现问题
客户反应文件上传速度慢,需要对文件服务器的上传速度进行优化。拿到这个任务后首先是使用现有代码对文件服务器进行了上传下载速度的测试。测试的文件服务器是seaweedfs和fastdfs,抛开网络的问题测试结果如下:
1)上传2M文件
seaweedfs:14s
fastdfs:90ms
2)下载2M文件
seaweedfs:90M/s
fastdfs:30M/s
为什么???按照道理上传不应该会这么慢,按照道理seaweedfs的性能应该不会低于其他服务器那么多,肯定是哪里有问题。
2、问题排查
我们seaweedfs的组网是一个master 两个 volume ,上传方式是通过master的9333端口。seaweedfs文件服务器的原理是通过master选择节点上传到对应节点上。但是为什么这么慢,我们首先进行了TCP抓包,分别抓取了三个节点的端口数据流,结果如下(这里一个是早上抓的,一个是下午抓的,中午吃了饭睡了觉):
1)master
分析:上传时间和结束时间很明显,master确实处理了这么长时间。但大部分时间是在消耗在assign之前。
2)volume1
分析:文件真正上传存储完成的时间很短,问题不出在volume,但问题在哪儿呢,摸着石头过河只能下载源码来分析了。
3)源码分析,经过一番折腾我们找到了如下源码:
func submitForClientHandler(w http.ResponseWriter, r *http.Request, masterUrl string, grpcDialOption grpc.DialOption) {
m := make(map[string]interface{})
if r.Method != "POST" {
writeJsonError(w, r, http.StatusMethodNotAllowed, errors.New("Only submit via POST!"))
return
}
debug("parsing upload file...")
fname, data, mimeType, pairMap, isGzipped, originalDataSize, lastModified, _, _, pe := needle.ParseUpload(r)
if pe != nil {
writeJsonError(w, r, http.StatusBadRequest, pe)
return
}
debug("assigning file id for", fname)
r.ParseForm()
count := uint64(1)
if r.FormValue("count") != "" {
count, pe = strconv.ParseUint(r.FormValue("count"), 10, 32)
if pe != nil {
writeJsonError(w, r, http.StatusBadRequest, pe)
return
}
}
ar := &operation.VolumeAssignRequest{
Count: count,
Replication: r.FormValue("replication"),
Collection: r.FormValue("collection"),
Ttl: r.FormValue("ttl"),
}
assignResult, ae := operation.Assign(masterUrl, grpcDialOption, ar)
if ae != nil {
writeJsonError(w, r, http.StatusInternalServerError, ae)
return
}
url := "http://" + assignResult.Url + "/" + assignResult.Fid
if lastModified != 0 {
url = url + "?ts=" + strconv.FormatUint(lastModified, 10)
}
注意到了 url := "http://" + assignResult.Url + "/" + assignResult.Fid 这行代码,就是我们抓包时看到的耗时后调用的接口。这个方法是submit的业务逻辑入口,那问题就在这个方法开始到上边提到的这一行。我们看看其中有什么可能导致速度慢的,翻阅了前边的代码,没有什么耗时的网络请求,可疑点就落在了参数解析上,文件越大耗时越长,是不是参数解析工具将文件当作参数来解析了呢?
4)我们又进行了指令上传测试
通过curl走9333端口对文件进行上传。结果令人诧异,2M文件的上传速度尽然变成了100多ms。问题基本能够确定在java提交数据到文件服务的时候,文件服务将文件当作参数解析了一遍。(时间问题具体的原因就没有深究,不过让人怀疑r.ParseForm()这个方法)
5)看java代码
使用的是各大搜索引擎搜索出来的一个java调用seaweedfs上传文件的一个示例:
httpClient1 = HttpClients.createDefault();
HttpPost httpPost = new HttpPost("http://IP:9333/submit");
httpPost.setHeader(new BasicHeader("Accept-Language", "zh-cn"));
File file = new File("/xxxx.txt");
FileBody bin = new FileBody(file);
HttpEntity reqEntity = MultipartEntityBuilder.create().setCharset(Charset.forName("UTF-8")).setMode(HttpMultipartMode.BROWSER_COMPATIBLE).addBinaryBody(URLEncoder.encode("xxxx.txt"), file).build();
httpPost.setEntity(reqEntity);
System.out.println(httpPost.toString());
response1 = httpClient1.execute(httpPost);
这段代码有问题么?没有问题,但是结合了go语言的http参数解析放到一起就出问题了。
3、问题解决
更换问文件上传方式,先调用master的assign接口,获取文件id及volume节点,然后直接上传文件到volume节点,代替submit接口做这两件事。
修改代码后,测试ok,2M文件上传时间测试180ms左右。
(也可以直接使用seaweedfs-java-client来做这件事情,可以减少很多编码工作)
4、总结
很多时候从软件自身来看确实不存在任何问题,但可能两个或多个软件组合成系统以后就会出现很多奇奇怪怪的问题,需要去调试、检测,最终才能成为一个稳定的系统。