關於大小型項目如何最大限度提升WebAPi性能

前言

WebAPi做爲接口請求的一種服務,當咱們請求該服務時咱們目標是須要快速獲取該服務的數據響應,這種狀況在大型項目中尤其常見,此時迫切須要提升WebAPi的響應機制,固然也少不了前端須要做出的努力,這裏咱們只講述在大小型項目中如何利用後臺邏輯儘量最大限度提升WebAPi性能,咱們從如下幾個方面來進行闡述。html

性能提高一:JSON序列化器(Jil)

在.NET裏面默認的序列化器是JavaScriptSrializer,都懂的,性能實在是差,後來出現了Json.NET,以致於在目前建立項目時默認用的序列化器是Json.NET,它被.NET開發者所普遍使用,它的強大和性能毋庸置疑,以致於如今Json.NET版本已經更新到9.0版本,可是在大型項目中一旦數據量巨大時,此時用Json.NET來序列化數據會略慢,這時咱們就能夠嘗試用Jil,它裏面的APi也足夠咱們用,咱們講述幾個經常使用的APi並一塊兒對比Json.NET來看看:前端

序列化對比

在Json.NET中是這樣序列化的json

JsonConvert.SerializeObject(obj)

而在Jil中序列化數據是這樣的瀏覽器

JSON.Serialize(obj)

此時對於Jil序列化數據返回的字符串形式有兩種緩存

(1)直接接收服務器

 object obj = new { Foo = 123, Bar = "abc" };
 string s = Jil.JSON.Serialize(obj)

(2)傳遞給StringWriter來接收併發

var obj = new { Foo = 123, Bar = "abc" };
var t = new StringWriter();
JSON.SerializeDynamic(obj, t);

上述說到對於數據量巨大時用Jil其效率高於Json.NET,下來咱們來驗證序列化10000條數據app

序列化類:框架

    public class Person
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }

測試數據:異步

            var list = new List<Person>();
            for (int i = 0; i < 10000; i++)
            {
                list.Add(new Person(){ Id = i });
            }
            var stop = new Stopwatch();
            stop.Start();
            var jil = SerializeList(list);
            Console.WriteLine(stop.ElapsedMilliseconds);
            stop.Stop();
var stop1 = new Stopwatch(); stop1.Start(); var json = JsonConvert.SerializeObject(list); Console.WriteLine(stop1.ElapsedMilliseconds); stop1.Stop();

Jil序列化封裝:

        private static string SerializeList(List<Person> list)
        {
            using (var output = new StringWriter())
            {
                JSON.Serialize(
                    list,
                    output
                );
                return output.ToString();
            }
        }

咱們來看看測試用例:

此時利用Json.NET序列化數據明顯優於Jil,但序列化數據爲10萬條數,Jil所耗時間會接近於Json.NET,當數據高於100萬條數時這個時候就能夠看出明顯的效果,以下:

此時Jil序列化數據不到1秒,而利用Json.NET則須要足足接近3秒。

測試用例更新:

當將代碼進行以下修改時,少許數據也是優於Json.NET,數據量越大性能越明顯,感謝園友【calvinK】提醒:

            var list = new List<int>();
            for (int i = 0; i < 10000; i++)
            {
                list.Add(i);
            }

            var stop = new Stopwatch();
            stop.Start();
            for (var i = 0; i < 1000; i++)
            {
                var jil = SerializeList(list);

            }

            Console.WriteLine(stop.ElapsedMilliseconds);
            stop.Stop();
            var stop1 = new Stopwatch();
            stop1.Start();
            for (var i = 0; i < 1000; i++)
            {
                var json = JsonConvert.SerializeObject(list);

            }
            Console.WriteLine(stop1.ElapsedMilliseconds);
            stop1.Stop();

結果以下:

 關於Jil的序列化還有一種則是利用JSON.SerializeDynamic來序列化那些在編譯時期沒法預測的類型。 至於反序列化也是和其序列化一一對應。

下面咱們繼續來看看Jil的其餘特性。若在視圖上渲染那些咱們須要的數據,而對於實體中沒必要要用到的字段咱們就須要進行過濾,此時咱們用到Jil中的忽略屬性。

 [JilDirective(Ignore = true)]

咱們來看看:

    public class Person
    {
        [JilDirective(Ignore = true)]
        public int Id { get; set; }
        public int Name { get; set; }
    }
            var jil = SerializeList(new Person() { Id = 1, Name = 123 } );
            Console.WriteLine(jil);

另外在Jil中最重要的屬性則是Options,該屬性用來配置返回的日期格式以及其餘配置,若未用其屬性默認利用Json.NET返回如【\/Date(143546676)\/】,咱們來看下:

var jil = SerializeList(new Person() { Id = 1, Name = "123", Time = DateTime.Now });

進行以下設置:

               JSON.Serialize(
                    p,
                    output,
                    new Options(dateFormat: DateTimeFormat.ISO8601)
                );

有關序列化繼承類時咱們一樣須要進行以下設置,不然沒法進行序列化

new Options(dateFormat: DateTimeFormat.ISO8601, includeInherited: true)

Jil的性能絕對優於Json.NET,Jil一直在追求序列化的速度因此在更多可用的APi可能少於Json.NET或者說沒有Json.NET靈活,可是足以知足咱們的要求。 

性能提高二:壓縮(Compress) 

壓縮方式(1) 【IIS設置】

啓動IIS動態內容壓縮

壓縮方式(2)【DotNetZip】 

利用現成的輪子,下載程序包【DotNetZip】便可,此時咱們則須要在執行方法完畢後來進行內容的壓縮便可,因此咱們須要重寫【 ActionFilterAttribute 】過濾器,在此基礎上進行咱們的壓縮操做。以下:

    public class DeflateCompressionAttribute : ActionFilterAttribute
    {
        public override void OnActionExecuted(HttpActionExecutedContext actionContext)
        {
            var content = actionContext.Response.Content;
            var bytes = content == null ? null : content.ReadAsByteArrayAsync().Result;
            var compressContent = bytes == null ? new byte[0] : CompressionHelper.DeflateByte(bytes);
            actionContext.Response.Content = new ByteArrayContent(compressContent);
            actionContext.Response.Content.Headers.Remove("Content-Type");
            if (string.Equals(actionContext.Request.Headers.AcceptEncoding.First().Value, "deflate"))
                actionContext.Response.Content.Headers.Add("Content-encoding", "deflate");
            else
                actionContext.Response.Content.Headers.Add("Content-encoding", "gzip");
            actionContext.Response.Content.Headers.Add("Content-Type", "application/json;charset=utf-8");
            base.OnActionExecuted(actionContext);
        }

    }

利用DotNetZip進行快速壓縮:

    public class CompressionHelper
    {
        public static byte[] DeflateByte(byte[] str)
        {
            if (str == null)
            {

                return null;

            }
            using (var output = new MemoryStream())
            {

                using (var compressor = new Ionic.Zlib.GZipStream(

                output, Ionic.Zlib.CompressionMode.Compress,

                Ionic.Zlib.CompressionLevel.BestSpeed))
                {

                    compressor.Write(str, 0, str.Length);

                }

                return output.ToArray();

            }

        }

    }

咱們來對比看一下未進行內容壓縮先後結果響應的時間以及內容長度,給出以下測試類:

        [HttpGet]
        [DeflateCompression]
        public async Task<IHttpActionResult> GetZipData()
        {
            Dictionary<object, object> dict = new Dictionary<object, object>();
            List<Employee> li = new List<Employee>();
            li.Add(new Employee { Id = "2", Name = "xpy0928", Email = "a@gmail.com" });
            li.Add(new Employee { Id = "3", Name = "tom", Email = "b@mail.com" });
            li.Add(new Employee { Id = "4", Name = "jim", Email = "c@mail.com" });
            li.Add(new Employee { Id = "5", Name = "tony",Email = "d@mail.com" });
dict.Add(
"Details", li);return Ok(dict); }

結果運行錯誤:

這裏應該是序列化出現問題,在有些瀏覽器返回的XML數據,我用的是搜狗瀏覽器,以前學習WebAPi時其返回的就是XML數據,咱們試着將其返回爲Json數據看看。

            var formatters = config.Formatters.Where(formatter =>
                 formatter.SupportedMediaTypes.Where(media =>
                 media.MediaType.ToString() == "application/xml" || media.MediaType.ToString() == "text/html").Count() > 0) //找到請求頭信息中的介質類型
                 .ToList();

            foreach (var match in formatters)
            {
                config.Formatters.Remove(match);
            }

咱們未將其壓縮後響應的長度以下所示:

壓縮後結果明顯獲得提高

接下來咱們自定義用.NET內置的壓縮模式來實現看看

壓縮方式(3)【自定義實現】

既然響應的內容是經過HttpContent,咱們則須要在重寫過濾器ActionFilterAttribute的基礎上來實現重寫HttpContent,最終根據獲取到瀏覽器支持的壓縮格式對數據進行壓縮並寫入到響應流中便可。

    public class CompressContent : HttpContent
    {
        private readonly string _encodingType;
        private readonly HttpContent _originalContent;
        public CompressContent(HttpContent content, string encodingType = "gzip")
        {
            _originalContent = content;
            _encodingType = encodingType.ToLowerInvariant();
            Headers.ContentEncoding.Add(encodingType);
        }
        protected override bool TryComputeLength(out long length)
        {
            length = -1;
            return false;
        }
        protected override Task SerializeToStreamAsync(Stream stream, TransportContext context)
        {
            Stream compressStream = null;
            switch (_encodingType)
            {
                case "gzip":
                    compressStream = new GZipStream(stream, CompressionMode.Compress, true);
                    break;
                case "deflate":
                    compressStream = new DeflateStream(stream, CompressionMode.Compress, true);
                    break;
                default:
                    compressStream = stream;
                    break;
            }
            return _originalContent.CopyToAsync(compressStream).ContinueWith(tsk =>
            {
                if (compressStream != null)
                {
                    compressStream.Dispose();
                }
            });
        }
    }  

重寫過濾器特性

    public class CompressContentAttribute : ActionFilterAttribute
    {
        public override void OnActionExecuted(HttpActionExecutedContext context)
        {
            var acceptedEncoding = context.Response.RequestMessage.Headers.AcceptEncoding.First().Value;
            if (!acceptedEncoding.Equals("gzip", StringComparison.InvariantCultureIgnoreCase)
            && !acceptedEncoding.Equals("deflate", StringComparison.InvariantCultureIgnoreCase))
            {
                return;
            }
            context.Response.Content = new CompressContent(context.Response.Content, acceptedEncoding);
        }  

    }

關於其響應結果對比則再也不敘述,和上述利用DotNetZip結果一致。 

當寫壓縮內容時,我發現一個問題,產生了疑問, context.Response.Content.Headers 和 context.Response.Headers 爲什麼響應中有兩個頭Headers呢?,沒有去細究這個問題,大概說說我的想法。

context.Response.Content.Headers和context.Response.Headers有什麼不一樣呢?

咱們看看context.Response.Headers中的定義,其摘要以下:

        // 摘要: 
        //     Gets a value that indicates if the HTTP response was successful.
        //
        // 返回結果: 
        //     Returns System.Boolean.A value that indicates if the HTTP response was successful.
        //     true if System.Net.Http.HttpResponseMessage.StatusCode was in the range 200-299;
        //     otherwise false.

而context.Response.Content.Headers中的定義,其摘要以下:

        // 摘要: 
        //     Gets the HTTP content headers as defined in RFC 2616.
        //
        // 返回結果: 
        //     Returns System.Net.Http.Headers.HttpContentHeaders.The content headers as
        //     defined in RFC 2616.

對於Content.Headers中的Headers的定義是基於RFC 2616即Http規範,想必這麼作的目的是將Http規範隔離開來,咱們可以方便咱們實現自定義代碼或者設置有關響應頭信息最終直接寫入到Http的響應流中。咱們更多的是操做Content.Headers因此將其區別開來,或許是出於此目的吧,有知道的園友能夠給出合理的解釋,這裏只是個人我的揣測。

性能提高三:緩存(Cache:粒度比較大)

緩存大概是談的最多的話題,固然也有大量的緩存組件供咱們使用,這裏只是就比較大的粒度來談論這個問題,對於一些小的項目仍是有一點做用,大的則另當別論。

當咱們進行請求能夠查看響應頭中會有這樣一個字段【Cache-Control】,若是咱們未作任何處理固然則是其值爲【no-cache】。在任什麼時候期都不會進行緩存,都會從新進行請求數據。這個屬性裏面對應的值還有private/public、must-revalidate,當咱們未指定max-age的值時且設置值爲private、no-cache、must-revalidate此時的請求都會去服務器獲取數據。這裏咱們首先了解下關於Http協議的基本知識。

【1】若設置爲private,則其不能共享緩存意思則是不會在本地緩存頁面即對於代理服務器而言不會複製一份,而若是對於用戶而言其緩存更加是私有的,只是對於我的而言,用戶之間的緩存相互獨立,互不共享。若爲public則說明每一個用戶均可以共享這一塊緩存。對於這兩者打個比方對於博客園的推送的新聞是公開的,則能夠設置爲public共享緩存,充分利用緩存。

【2】max-age則是緩存的過時時間,在某一段時間內不會去從新請求從服務器獲取數據,直接在本地瀏覽器緩存中獲取。

【3】must-revalidate從字面意思來看則是必須從新驗證,也就是對於過時的數據進行從新獲取新的數據,那麼它到底何時用呢?歸根結底一句話:must-revalidate主要與max-age有關,當設置了max-age時,同時也設置了must-revalidate,等緩存過時後,此時must-revalidate則會告訴服務器來獲取最新的數據。也就是說當設置max-age = 0,must-revalidate = true時能夠說是與no-cache = true等同。

下面咱們來進行緩存控制:

    public class CacheFilterAttribute : ActionFilterAttribute
    {
        public int CacheTimeDuration { get; set; }
        public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
        {
            actionExecutedContext.Response.Headers.CacheControl = new CacheControlHeaderValue
            {
                MaxAge = TimeSpan.FromSeconds(CacheTimeDuration),
                MustRevalidate = true,
                Public = true
            };
        }
    }

添加緩存過濾特性:

        [HttpGet]
        [CompressContent]
        [CacheFilter(CacheTimeDuration = 100)]
        public async Task<IHttpActionResult> GetZipData()
        {
            var sw = new Stopwatch();
            sw.Start();
            Dictionary<object, object> dict = new Dictionary<object, object>();
            List<Employee> li = new List<Employee>();
            li.Add(new Employee { Id = "2", Name = "xpy0928", Email = "a@gmail.com" });
            li.Add(new Employee { Id = "3", Name = "tom", Email = "b@mail.com" });
            li.Add(new Employee { Id = "4", Name = "jim", Email = "c@mail.com" });
            li.Add(new Employee { Id = "5", Name = "tony", Email = "d@mail.com" });

            sw.Stop();

            dict.Add("Details", li);
            dict.Add("Time", sw.Elapsed.Milliseconds);

            return Ok(dict);

        }

結果以下:

性能提高四:async/await(異步方法)

當在大型項目中會出現並發現象,常見的狀況例如註冊,此時有若干個用戶同時在註冊時,則會致使當前請求阻塞而且頁面一直無響應最終致使服務器崩潰,爲了解決這樣的問題咱們須要用到異步方法,讓多個請求過來時,線程池分配足夠的線程來處理多個請求,提升線程池的利用率 !以下:

         public async Task<IHttpActionResult> Register(Employee model)
         {
            var result = await UserManager.CreateAsync(model);
            return Ok(result);
         }

總結 

本節咱們從以上幾方面講述了在大小項目中如何儘量最大限度來提升WebAPi的性能,使數據響應更加迅速,或許還有其餘更好的解決方案,至少以上所述也能夠做爲一種參考,WebAPi一個很輕量的框架,你值得擁有,see u。

相關文章
相關標籤/搜索