CosmosDB 查询性能

CosmosDB Query Performance

我写了最新的更新,然后从 Stack Overflow 收到以下错误:"Body is limited to 30000 characters; you entered 38676."

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

我已经存储了我的(长)原始 post 和 pastebin 上的更新。我不认为很多人会读它们,但我为它们付出了很多努力,所以最好不要让它们丢失。


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

这些文档中的每一个都有一个 Location 属性,这是一个 GeoJSON Point.

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

Azure Cosmos DB supports automatic indexing of Points, Polygons, and LineStrings

我已经检查了我的 collection 的索引政策,它有自动点索引的条目:

{
   "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));

然后我将该查询 object 传递给以下方法,这样我就可以在跟踪使用的请求单位的同时获得结果:

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个点组成(首末点相同),所以不是很复杂。它覆盖了英国最南部的大部分地区,从伦敦向下。

此查询的一个示例 运行 在 170717.151 毫秒(不到 171 秒或不到 3 分钟)内使用 3917.92 RU 返回了 8728 个文档。

3918 卢/171 秒 = 22.91 RU/s

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

据我了解,这是您可以保证获得的保留级别。有时您可以 "burst" 高于该级别,但如果这样做过于频繁,您将被限制回保留的级别。

23 RU/s 的 "query speed" 显然远低于 400 RU/s 的吞吐量设置。

我运行正在连接客户端"locally",即在我的办公室,而不是在 Azure 数据中心。

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

所以发生了什么?

我是不是做错了什么?

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

这是 GeoSpatial 索引运行的速度,所以我将获得最佳性能吗?

没有使用 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 个项目。

此外,坐标被指定为 Longitude/Latitude、this is how GeoJSON expects them(即 X/Y),而不是传统的 Latitude/Longitude 顺序。

The GeoJSON specification specifies longitude first and latitude second.

更新 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 倍!