CosmosDB查询性能

我写了我的最新更新,然后从堆栈溢出中得到以下错误:“正文限于30000个字符;您输入了38676。”

公平地说,我在记录我的冒险经历时非常冗长,所以我已经改写了我在这里更加简洁的内容。

我已将我的(长)原始文章和更新存储在pastebin中。 我不认为很多人会阅读他们,但我会付出很多努力,所以不会让他们迷失。


我有一个包含100,000个文档的集合,用于学习如何使用CosmosDB以及诸如性能测试等内容。

每个文档都有一个Location属性,它是GeoJSON Point

根据文档,一个GeoJSON点应该被自动索引。

Azure Cosmos DB支持自动索引点,多边形和LineStrings

我检查了我的收藏的索引策略,并且它具有自动点索引的条目:

{
   "automatic":true,
   "indexingMode":"Consistent",
   "includedPaths":[
      {
         "path":"/*",
         "indexes":[
            ...
            {
               "kind":"Spatial",
               "dataType":"Point"
            },
            ...                
         ]
      }
   ],
   "excludedPaths":[ ]
}

我一直在寻找一种方法来列出或以其他方式询问已创建的索引,但是我还没有找到这样的事情,所以我一直无法确认这个属性是否已经被索引。

我创建了一个GeoJSON Polygon ,然后用它来查询我的文档。

这是我的查询:

var query = client
    .CreateDocumentQuery<TestDocument>(documentCollectionUri)
    .Where(document => document.Type == this.documentType && document.Location.Intersects(target.Area));

然后我将该查询对象传递给以下方法,以便在跟踪所使用的请求单元的同时获得结果:

protected async Task<IEnumerable<T>> QueryTrackingUsedRUsAsync(IQueryable<T> query)
{
    var documentQuery = query.AsDocumentQuery();
    var documents = new List<T>();

    while (documentQuery.HasMoreResults)
    {
        var response = await documentQuery.ExecuteNextAsync<T>();

        this.AddUsedRUs(response.RequestCharge);

        documents.AddRange(response);
    }

    return documents;
}

点位置是从数百万英国地址中随机选取的,因此它们应该具有相当实际的分布。

多边形由16个点组成(第一个和最后一个点是相同的),所以它不是很复杂。 它覆盖了英国最南部的大部分地区,从伦敦下来。

此查询的示例运行使用3917.92 RU在170717.151毫秒内返回了8728个文档,该文档仅171秒,即不到3分钟。

3918 RU / 171 s = 22.91 RU / s

我目前的吞吐量(RU / s)设置为最低值,为400 RU / s。

这是我的理解,这是你保证得到的保留级别。 有时你可以“突破”这个水平,但是频繁地这样做,你会被限制回到你的保留水平。

显然,23 RU / s的“查询速度”远低于400 RU / s的吞吐量设置。

我在本地运行客户端,即在我的办公室,而不是在Azure数据中心。

每个文档大小约为500字节(0.5 kb)。

那么发生了什么?

难道我做错了什么?

我误解了我的查询如何被限制RU / s?

这是GeoSpatial指数运行的速度,所以我会得到最好的性能?

地理空间信息索引是否被使用?

有没有办法可以查看创建的索引?

有没有一种方法可以检查索引是否被使用?

有没有一种方法可以分析查询并获取有关何时花费的指标? 例如,s被用于按照他们的类型查找文档,s被用于对地理空间进行过滤,并且s被用于传输数据。

更新1

这是我在查询中使用的多边形:

Area = new Polygon(new List<LinearRing>()
{
    new LinearRing(new List<Position>()
    {
        new Position(1.8567  ,51.3814),

        new Position(0.5329  ,51.4618),
        new Position(0.2477  ,51.2588),
        new Position(-0.5329 ,51.2579),
        new Position(-1.17   ,51.2173),
        new Position(-1.9062 ,51.1958),
        new Position(-2.5434 ,51.1614),
        new Position(-3.8672 ,51.139 ),
        new Position(-4.1578 ,50.9137),
        new Position(-4.5373 ,50.694 ),
        new Position(-5.1496 ,50.3282),
        new Position(-5.2212 ,49.9586),
        new Position(-3.7049 ,50.142 ),
        new Position(-2.1698 ,50.314 ),
        new Position(0.4669  ,50.6976),

        new Position(1.8567  ,51.3814)
    })
})

我也尝试倒转它(因为环形方向很重要),但倒转多边形的查询花费了更长的时间(我没有时间去处理)并返回了91272个物品。

此外,坐标被指定为经度/纬度,因为这是GeoJSON期望的坐标(即X / Y),而不是传统的纬度/经度顺序。

GeoJSON规范指定经度的第一个纬度和第二个纬度。

更新2

这是我的一个文档的JSON:

{
    "GeoTrigger": null,
    "SeverityTrigger": -1,
    "TypeTrigger": -1,
    "Name": "13, LONSDALE SQUARE, LONDON, N1  1EN",
    "IsEnabled": true,
    "Type": 2,
    "Location": {
        "$type": "Microsoft.Azure.Documents.Spatial.Point, Microsoft.Azure.Documents.Client",
        "type": "Point",
        "coordinates": [
            -0.1076407397346815,
            51.53970315059827
        ]
    },
    "id": "0dc2c03e-082b-4aea-93a8-79d89546c12b",
    "_rid": "EQttAMGhSQDWPwAAAAAAAA==",
    "_self": "dbs/EQttAA==/colls/EQttAMGhSQA=/docs/EQttAMGhSQDWPwAAAAAAAA==/",
    "_etag": ""42001028-0000-0000-0000-594943fe0000"",
    "_attachments": "attachments/",
    "_ts": 1497973747
}

更新3

我创建了一个最小的问题再现,我发现问题不再发生。

这表明问题确实在我自己的代码中。

我着手检查原始代码和复制代码之间的所有差异,并最终发现对我来说看起来相当无辜的事实上已经产生了巨大的影响。 幸运的是,该代码根本不需要,所以简单的修复就是不使用那些代码。

有一次我使用了一个自定义的ContractResolver并且一旦不再需要它,我就没有删除它。

这是违规的复制代码:

using System;
using System.Collections.Generic;
using System.Configuration;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Azure.Documents;
using Microsoft.Azure.Documents.Client;
using Microsoft.Azure.Documents.Spatial;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;

namespace Repro.Cli
{
    public class Program
    {
        static void Main(string[] args)
        {
            JsonConvert.DefaultSettings = () =>
            {
                return new JsonSerializerSettings
                {
                    ContractResolver = new PropertyNameMapContractResolver(new Dictionary<string, string>()
                    {
                        { "ID", "id" }
                    })
                };
            };

            //AJ: Init logging
            Trace.AutoFlush = true;
            Trace.Listeners.Add(new ConsoleTraceListener());
            Trace.Listeners.Add(new TextWriterTraceListener("trace.log"));

            //AJ: Increase availible threads
            //AJ: https://docs.microsoft.com/en-us/azure/storage/storage-performance-checklist#subheading10
            //AJ: https://github.com/Azure/azure-documentdb-dotnet/blob/master/samples/documentdb-benchmark/Program.cs
            var minThreadPoolSize = 100;
            ThreadPool.SetMinThreads(minThreadPoolSize, minThreadPoolSize);

            //AJ: https://docs.microsoft.com/en-us/azure/cosmos-db/performance-tips
            //AJ: gcServer enabled in app.config
            //AJ: Prefer 32-bit disabled in project properties

            //AJ: DO IT
            var program = new Program();

            Trace.TraceInformation($"Starting @ {DateTime.UtcNow}");
            program.RunAsync().Wait();
            Trace.TraceInformation($"Finished @ {DateTime.UtcNow}");

            //AJ: Wait for user to exit
            Console.WriteLine();
            Console.WriteLine("Hit enter to exit...");
            Console.ReadLine();
        }

        public async Task RunAsync()
        {
            using (new CodeTimer())
            {
                var client = await this.GetDocumentClientAsync();
                var documentCollectionUri = UriFactory.CreateDocumentCollectionUri(ConfigurationManager.AppSettings["databaseID"], ConfigurationManager.AppSettings["collectionID"]);

                //AJ: Prepare Test Documents
                var documentCount = 10000; //AJ: 10,000
                var documentsForUpsert = this.GetDocuments(documentCount);
                await this.UpsertDocumentsAsync(client, documentCollectionUri, documentsForUpsert);

                var allDocuments = this.GetAllDocuments(client, documentCollectionUri);

                var area = this.GetArea();
                var documentsInArea = this.GetDocumentsInArea(client, documentCollectionUri, area);
            }
        }

        private async Task<DocumentClient> GetDocumentClientAsync()
        {
            using (new CodeTimer())
            {
                var serviceEndpointUri = new Uri(ConfigurationManager.AppSettings["serviceEndpoint"]);
                var authKey = ConfigurationManager.AppSettings["authKey"];

                var connectionPolicy = new ConnectionPolicy
                {
                    ConnectionMode = ConnectionMode.Direct,
                    ConnectionProtocol = Protocol.Tcp,
                    RequestTimeout = new TimeSpan(1, 0, 0),
                    RetryOptions = new RetryOptions
                    {
                        MaxRetryAttemptsOnThrottledRequests = 10,
                        MaxRetryWaitTimeInSeconds = 60
                    }
                };

                var client = new DocumentClient(serviceEndpointUri, authKey, connectionPolicy);

                await client.OpenAsync();

                return client;
            }
        }

        private List<TestDocument> GetDocuments(int count)
        {
            using (new CodeTimer())
            {
                return External.CreateDocuments(count);
            }
        }

        private async Task UpsertDocumentsAsync(DocumentClient client, Uri documentCollectionUri, List<TestDocument> documents)
        {
            using (new CodeTimer())
            {
                //TODO: AJ: Parallelise
                foreach (var document in documents)
                {
                    await client.UpsertDocumentAsync(documentCollectionUri, document);
                }
            }
        }

        private List<TestDocument> GetAllDocuments(DocumentClient client, Uri documentCollectionUri)
        {
            using (new CodeTimer())
            {
                var query = client
                    .CreateDocumentQuery<TestDocument>(documentCollectionUri, new FeedOptions()
                    {
                        MaxItemCount = 1000
                    });

                var documents = query.ToList();

                return documents;
            }
        }

        private Polygon GetArea()
        {
            //AJ: Longitude,Latitude i.e. X/Y
            //AJ: Ring orientation matters 
            return new Polygon(new List<LinearRing>()
            {
                new LinearRing(new List<Position>()
                {
                    new Position(1.8567  ,51.3814),

                    new Position(0.5329  ,51.4618),
                    new Position(0.2477  ,51.2588),
                    new Position(-0.5329 ,51.2579),
                    new Position(-1.17   ,51.2173),
                    new Position(-1.9062 ,51.1958),
                    new Position(-2.5434 ,51.1614),
                    new Position(-3.8672 ,51.139 ),
                    new Position(-4.1578 ,50.9137),
                    new Position(-4.5373 ,50.694 ),
                    new Position(-5.1496 ,50.3282),
                    new Position(-5.2212 ,49.9586),
                    new Position(-3.7049 ,50.142 ),
                    new Position(-2.1698 ,50.314 ),
                    new Position(0.4669  ,50.6976),

                    //AJ: Last point must be the same as first point
                    new Position(1.8567  ,51.3814)
                })
            });
        }

        private List<TestDocument> GetDocumentsInArea(DocumentClient client, Uri documentCollectionUri, Polygon area)
        {
            using (new CodeTimer())
            {
                var query = client
                    .CreateDocumentQuery<TestDocument>(documentCollectionUri, new FeedOptions()
                    {
                        MaxItemCount = 1000
                    })
                    .Where(document => document.Location.Intersects(area));

                var documents = query.ToList();

                return documents;
            }
        }
    }

    public class TestDocument : Resource
    {
        public string Name { get; set; }
        public Point Location { get; set; } //AJ: Longitude,Latitude i.e. X/Y

        public TestDocument()
        {
            this.Id = Guid.NewGuid().ToString("N");
        }
    }

    //AJ: This should be "good enough". The times being recorded are seconds or minutes.
    public class CodeTimer : IDisposable
    {
        private Action<TimeSpan> reportFunction;
        private Stopwatch stopwatch = new Stopwatch();

        public CodeTimer([CallerMemberName]string name = "")
            : this((ellapsed) =>
            {
                Trace.TraceInformation($"{name} took {ellapsed}, or {ellapsed.TotalMilliseconds} ms.");
            })
        { }

        public CodeTimer(Action<TimeSpan> report)
        {
            this.reportFunction = report;
            this.stopwatch.Start();
        }

        public void Dispose()
        {
            this.stopwatch.Stop();
            this.reportFunction(this.stopwatch.Elapsed);
        }
    }

    public class PropertyNameMapContractResolver : DefaultContractResolver
    {
        private Dictionary<string, string> propertyNameMap;

        public PropertyNameMapContractResolver(Dictionary<string, string> propertyNameMap)
        {
            this.propertyNameMap = propertyNameMap;
        }

        protected override string ResolvePropertyName(string propertyName)
        {
            if (this.propertyNameMap.TryGetValue(propertyName, out string resolvedName))
                return resolvedName;

            return base.ResolvePropertyName(propertyName);
        }
    }
}

我使用了一个自定义的ContractResolver ,这显然对.Net SDK中的DocumentDB类的性能有很大的影响。

这就是我设置ContractResolver

JsonConvert.DefaultSettings = () =>
{
    return new JsonSerializerSettings
    {
        ContractResolver = new PropertyNameMapContractResolver(new Dictionary<string, string>()
        {
            { "ID", "id" }
        })
    };
};

这是如何实施的:

public class PropertyNameMapContractResolver : DefaultContractResolver
{
    private Dictionary<string, string> propertyNameMap;

    public PropertyNameMapContractResolver(Dictionary<string, string> propertyNameMap)
    {
        this.propertyNameMap = propertyNameMap;
    }

    protected override string ResolvePropertyName(string propertyName)
    {
        if (this.propertyNameMap.TryGetValue(propertyName, out string resolvedName))
            return resolvedName;

        return base.ResolvePropertyName(propertyName);
    }
}

解决方案很简单,不要设置JsonConvert.DefaultSettings以便不使用ContractResolver

结果:

我能够在21799.0221毫秒内执行我的空间查询,这是22秒。

之前花了170717.151毫秒,这是2分50秒。

这大约快8倍!

链接地址: http://www.djcxy.com/p/39827.html

上一篇: CosmosDB Query Performance

下一篇: Game Center Leaderboard not appearing on production version of game?