作为一只从事.NET开发长达6年的程序狗,个人对.NET又爱又恨,爱的是强大的基础类库和全面的api文档让我所有的问题都能找到答案,恨的是因为微软的封闭策略丧失了大批的开发者的支持,让大多数人转向了Java的怀抱,让Java的生态圈蓬勃发展。从2015年开始微软发布.NET CORE开始,以全新的开源+重构的方式展现了全新的.NET,内部API经过了一些列的重构,号称在性能上有很大突破,今天闲来无事,就性能问题做一个对比。
实验的方案如下,分别用.NET的WebAPI2.0和.NET Core API以及Go语言写一个服务端,然后再写一个客户端程序分别新建1000个线程同时访问这三个服务,记录每次从服务开始到服务返回所花费的时间以及服务端的内存使用情况。本次对比不针对指定代码,主要是测试3个平台框架提供的基础处理和并发能力,为以后的技术选型提供一定的支持。
.NET的项目结构如下。
在Model中新建两个类,Request和Response用来记录传递的数据
using System;
namespace APICompare.Model
{
public class RequestInfo
{
///
/// 记录的Request是哪个线程发送的
///
public int RequestIndex { get; set; }
///
/// 唯一Guid
///
public string RequestGuid { get; set; }
///
/// 开始时间
///
public DateTime BeginTime { get; set; }
///
/// 结束时间
///
public DateTime EndTime { get; set; }
}
}
using System;
using System.Collections.Generic;
using System.Text;
namespace APICompare.Model
{
public class ResponseInfo
{
///
/// 返回开始时间
///
public DateTime BeginTime { get; set; }
public DateTime EndTime { get; set; }
public RequestInfo Request { get; set; }
///
/// 当前工作集(bytes)
///
public long WorkSet { get; set; }
}
}
新建API2.0的项目,并新建一个CostController,实现Get方法,接收RequestInfo参数,返回ResponseInfo
新建.NET Core API项目,处理过程参考API2.0
新建Go目录和对应的Model
在ResponseInfo.go中新建两个Model如下
package main
import "time"
type ResponseInfo struct {
BeginTime time.Time
EndTime time.Time
Request RequestInfo
WorkSet uint64
}
type RequestInfo struct {
RequestIndex int
RequestGuid string
BeginTime time.Time
EndTime time.Time
}
新建Http服务端主程序Server.go
package main
import (
"encoding/json"
"log"
"net/http"
"net/url"
"time"
"strings"
//"os"
"runtime"
"strconv"
)
func main() {
http.HandleFunc("/", handlerHttpRequest)
err := http.ListenAndServe(":8058", nil)
if err != nil {
log.Fatal("ListenAndServe:", err)
}
log.Fatal("ListenAndServe on 8058")
}
func handlerHttpRequest(w http.ResponseWriter, r *http.Request) {
log.Println("r.URL.RawQuery:", r.URL.RawQuery)
query, err := url.QueryUnescape(r.URL.RawQuery)//解码url
if err != nil {
log.Fatal("url.QueryUnescape:", r.URL.RawQuery)
}
queryStrings :=strings.Split(query,"&")
var request RequestInfo
for _,queryStr := range queryStrings{
if strings.Contains(queryStr,"RequestIndex"){
request.RequestIndex,err=strconv.Atoi(strings.Split(queryStr,"=")[1]);
if err!=nil{
log.Println("strconv.Atoi:",err)
}
}
if strings.Contains(queryStr,"RequestGuid"){
request.RequestGuid = strings.Split(queryStr,"=")[1];
}
if strings.Contains(queryStr,"BeginTime"){
request.BeginTime,err = time.Parse("2006-01-02 15:04:05",strings.Split(queryStr,"=")[1]);
if err!=nil{
log.Fatal("time.Parse:",err)
}
}
if strings.Contains(queryStr,"EndTime"){
request.EndTime,err = time.Parse("2006-01-02 15:04:05",strings.Split(queryStr,"=")[1]);
if err!=nil{
log.Println("time.Parse:",err)
}
}
}
queryStrings = nil
//err = json.Unmarshal([]byte(query), &request)
//if err != nil {
// log.Fatal("json.Unmarshal:", err)
//}
request.EndTime = time.Now()
var response = new(ResponseInfo)
response.Request = request
response.BeginTime = time.Now()
//获取当前程序的内存大小
var m runtime.MemStats;
runtime.ReadMemStats(&m);
response.WorkSet = m.Alloc;
json, err := json.Marshal(response)
if err != nil {
log.Fatal("json.Marshal:", err)
}
w.Write(json)
response = nil
json = nil
}
客户端程序用传统MVC,新建一个Controller,在页面中异步访问CallAPI,最后把返回结果放在Dictionary中,然后在所有线程返回后开始下一个程序的测试,页面中采用HighCharts显示数据。
@using APICompare.Model
@using System.Runtime
@{
Layout = null;
Dictionary lstResponse = ViewBag.lstResponse as Dictionary;
Dictionary lstResponseCore = ViewBag.lstResponseCore as Dictionary;
Dictionary lstResponseGo = ViewBag.lstResponseGo as Dictionary;
}
性能对比
public JsonResult CallAPI()
{
bool hasFinished = false;
const int MaxThreat = 1000;
string classicalUrl = "http://localhost:8056/api/Cost";
string coreUrl = "http://localhost:8057/api/Cost";
string goUrl = "http://localhost:8058";
//新建三个容器存放每次请求的数据
Dictionary dicResponse = new Dictionary();
Dictionary dicResponseCore = new Dictionary();
Dictionary dicResponseGo = new Dictionary();
TaskFactory taskFactory = new TaskFactory();
TaskFactory taskFactory2 = new TaskFactory();
TaskFactory taskFactory3 = new TaskFactory();
Task[] tasks = new Task[MaxThreat];
Task[] tasks2 = new Task[MaxThreat];
Task[] tasks3 = new Task[MaxThreat];
//begin call classical api
for (var i = 0; i < MaxThreat; i++)
{
RequestInfo requestInfo = new RequestInfo() { RequestIndex = i, RequestGuid = Guid.NewGuid().ToString(), BeginTime = DateTime.Now };
dicResponse.Add(requestInfo.RequestGuid, null);
tasks[i] = taskFactory.StartNew(() =>
{
var res = GetResponseInfo(classicalUrl, requestInfo);
dicResponse[res.Request.RequestGuid] = res;
});
}
taskFactory.ContinueWhenAll(tasks, c =>
{
hasFinished = true;
});
while (true)
{
if (hasFinished)
{
hasFinished = false;
break;
}
}
//begin call core api
for (var i = 0; i < MaxThreat; i++)
{
RequestInfo requestInfo = new RequestInfo() { RequestIndex = i, RequestGuid = Guid.NewGuid().ToString(), BeginTime = DateTime.Now };
dicResponseCore.Add(requestInfo.RequestGuid, null);
tasks2[i] = taskFactory2.StartNew(() =>
{
var res = GetResponseInfo(coreUrl, requestInfo);
dicResponseCore[res.Request.RequestGuid] = res;
});
}
taskFactory2.ContinueWhenAll(tasks2, c =>
{
hasFinished = true;
});
while (true)
{
if (hasFinished)
{
hasFinished = false;
break;
}
}
//begin call go api
for (var i = 0; i < MaxThreat; i++)
{
RequestInfo requestInfo = new RequestInfo() { RequestIndex = i, RequestGuid = Guid.NewGuid().ToString(), BeginTime = DateTime.Now };
dicResponseGo.Add(requestInfo.RequestGuid, null);
tasks3[i] = taskFactory3.StartNew(() =>
{
var res = GetResponseInfo(goUrl, requestInfo);
dicResponseGo[res.Request.RequestGuid] = res;
});
}
taskFactory3.ContinueWhenAll(tasks3, c =>
{
hasFinished = true;
});
while (true)
{
if (hasFinished)
{
hasFinished = false;
break;
}
}
List lstResponseClassical = new List();
List lstResponseCore = new List();
List lstResponseGo = new List();
foreach (var item in dicResponse)
{
if (item.Value.Request.RequestIndex % 10 == 1)
{
lstResponseClassical.Add(item.Value);
}
}
foreach (var item in dicResponseCore)
{
if (item.Value.Request.RequestIndex % 10 == 1)
{
lstResponseCore.Add(item.Value);
}
}
foreach (var item in dicResponseGo)
{
if (item.Value.Request.RequestIndex % 10 == 1)
{
lstResponseGo.Add(item.Value);
}
}
//List lstClassData = new List();
//foreach (var item in lstResponse)
//{
// var response = item.Value;
// var timespan = response.EndTime - response.Request.BeginTime;
// lstClassData.Add(timespan.Minutes * 60 * 1000 + timespan.Seconds * 1000 + timespan.Milliseconds)
//}
var ClassicalData = from a in lstResponseClassical select new { a.Request.RequestIndex, timeSpan = GetTotalMilSeconds(a.EndTime - a.Request.BeginTime), WorkSet= a.WorkSet/1000 };
var CoreData = from a in lstResponseCore select new { a.Request.RequestIndex, timeSpan = GetTotalMilSeconds(a.EndTime - a.Request.BeginTime), WorkSet = a.WorkSet / 1000 };
var GoData = from a in lstResponseGo select new { a.Request.RequestIndex, timeSpan = GetTotalMilSeconds(a.EndTime - a.Request.BeginTime), WorkSet = a.WorkSet / 1000 };
var data = new
{
ClassicalData = ClassicalData.ToList(),
CoreData = CoreData.ToList(),
GoData = GoData.ToList()
};
return Json(data, JsonRequestBehavior.AllowGet);
}
private int GetTotalMilSeconds(TimeSpan ts)
{
return ts.Minutes * 60 * 1000 + ts.Seconds * 1000 + ts.Milliseconds;
}
private ResponseInfo GetResponseInfo(string url, RequestInfo request)
{
HttpWebRequest requestClassic = HttpWebRequest.CreateHttp(ParseGetUri(url, request));
requestClassic.Method = "GET";
requestClassic.KeepAlive = false;
requestClassic.ContentType = "application/json";
HttpWebResponse response = requestClassic.GetResponse() as HttpWebResponse;
using (Stream responseStream = response.GetResponseStream())
{
StreamReader reader = new StreamReader(responseStream, Encoding.Default);
string srcString = reader.ReadToEnd();
var r = JsonConvert.DeserializeObject(srcString);
r.EndTime = DateTime.Now;
requestClassic = null;
return r;
}
}
private Uri ParseGetUri(string baseUrl, RequestInfo request)
{
if (!baseUrl.Contains("?"))
{
baseUrl += "?";
}
baseUrl = string.Concat(baseUrl, "RequestIndex=", request.RequestIndex, "&RequestGuid=", request.RequestGuid, "&BeginTime=", HttpUtility.UrlEncode(request.BeginTime.ToString("yyyy-MM-dd hh:mm:ss.fff")));
Uri uri = null;
if (!Uri.TryCreate(baseUrl, UriKind.Absolute, out uri))
{
return null;
}
return uri;
}
关于程序的部署:
WebAPI采用本地IIS作为服务器
.NET CORE API采用FDD-CLI的部署方式,定位到程序dll目录,使用dotnet命令启动API服务
Go直接在IDE中启动运行程序
万事具备,只欠一个F5了,那就跑起来,time cost如下,很直观的看出来,随着并发的数量越来越大,.net core的延迟时间比webapi2.0还要高,到981个请求时,webapi基本上需要2s的时间,而core api需要3.5s的时间,相比goapi依然保持在1s以下。
我们再看内存使用情况,core api和webapi2.0的内存大概保存在90M左右的范围内,GC的回收周期也非常明显,相比于Go内存依然保持很小,GC回收周期也很长。