关于大小型项目如何最大限度提升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。

相关文章
相关标签/搜索