web-dev-qa-db-fra.com

Ralentir douloureusement l'insertion de tables Azure et supprimer des opérations par lots

Je rencontre un énorme goulot d'étranglement en termes de performances lors de l'utilisation du stockage de table Azure. Mon désir est d'utiliser les tables comme une sorte de cache, donc un long processus peut entraîner des centaines à plusieurs milliers de lignes de données. Les données peuvent ensuite être rapidement interrogées par les clés de partition et de ligne.

L'interrogation fonctionne assez rapidement (extrêmement rapide lorsque vous utilisez uniquement les clés de partition et de ligne, un peu plus lent, mais toujours acceptable lorsque vous recherchez également des propriétés pour une correspondance particulière).

Cependant, l'insertion et la suppression de lignes est douloureusement lente.

Clarification

Je tiens à préciser que même l'insertion d'un seul lot de 100 articles prend plusieurs secondes. Ce n'est pas seulement un problème avec le débit total de milliers de lignes. Cela m'affecte quand je n'insère que 100.

Voici un exemple de mon code pour effectuer une insertion par lots dans ma table:

static async Task BatchInsert( CloudTable table, List<ITableEntity> entities )
    {
        int rowOffset = 0;

        while ( rowOffset < entities.Count )
        {
            Stopwatch sw = Stopwatch.StartNew();

            var batch = new TableBatchOperation();

            // next batch
            var rows = entities.Skip( rowOffset ).Take( 100 ).ToList();

            foreach ( var row in rows )
                batch.Insert( row );

            // submit
            await table.ExecuteBatchAsync( batch );

            rowOffset += rows.Count;

            Trace.TraceInformation( "Elapsed time to batch insert " + rows.Count + " rows: " + sw.Elapsed.ToString( "g" ) );
        }
    }

J'utilise des opérations par lots, et voici un exemple de sortie de débogage:

Microsoft.WindowsAzure.Storage Information: 3 : b08a07da-fceb-4bec-af34-3beaa340239b: Starting asynchronous request to http://127.0.0.1:10002/devstoreaccount1.
Microsoft.WindowsAzure.Storage Verbose: 4 : b08a07da-fceb-4bec-af34-3beaa340239b: StringToSign = POST..multipart/mixed; boundary=batch_6d86d34c-5e0e-4c0c-8135-f9788ae41748.Tue, 30 Jul 2013 18:48:38 GMT./devstoreaccount1/devstoreaccount1/$batch.
Microsoft.WindowsAzure.Storage Information: 3 : b08a07da-fceb-4bec-af34-3beaa340239b: Preparing to write request data.
Microsoft.WindowsAzure.Storage Information: 3 : b08a07da-fceb-4bec-af34-3beaa340239b: Writing request data.
Microsoft.WindowsAzure.Storage Information: 3 : b08a07da-fceb-4bec-af34-3beaa340239b: Waiting for response.
Microsoft.WindowsAzure.Storage Information: 3 : b08a07da-fceb-4bec-af34-3beaa340239b: Response received. Status code = 202, Request ID = , Content-MD5 = , ETag = .
Microsoft.WindowsAzure.Storage Information: 3 : b08a07da-fceb-4bec-af34-3beaa340239b: Response headers were processed successfully, proceeding with the rest of the operation.
Microsoft.WindowsAzure.Storage Information: 3 : b08a07da-fceb-4bec-af34-3beaa340239b: Processing response body.
Microsoft.WindowsAzure.Storage Information: 3 : b08a07da-fceb-4bec-af34-3beaa340239b: Operation completed successfully.
iisexpress.exe Information: 0 : Elapsed time to batch insert 100 rows: 0:00:00.9351871

Comme vous pouvez le voir, cet exemple prend presque 1 seconde pour insérer 100 lignes. La moyenne semble être d'environ 0,8 seconde sur ma machine de développement (3,4 GHz quad core).

Cela semble ridicule.

Voici un exemple d'une opération de suppression par lots:

Microsoft.WindowsAzure.Storage Information: 3 : 4c271cb5-7463-44b1-b2e5-848b8fb10a93: Starting asynchronous request to http://127.0.0.1:10002/devstoreaccount1.
Microsoft.WindowsAzure.Storage Verbose: 4 : 4c271cb5-7463-44b1-b2e5-848b8fb10a93: StringToSign = POST..multipart/mixed; boundary=batch_7e3d229f-f8ac-4aa0-8ce9-ed00cb0ba321.Tue, 30 Jul 2013 18:47:41 GMT./devstoreaccount1/devstoreaccount1/$batch.
Microsoft.WindowsAzure.Storage Information: 3 : 4c271cb5-7463-44b1-b2e5-848b8fb10a93: Preparing to write request data.
Microsoft.WindowsAzure.Storage Information: 3 : 4c271cb5-7463-44b1-b2e5-848b8fb10a93: Writing request data.
Microsoft.WindowsAzure.Storage Information: 3 : 4c271cb5-7463-44b1-b2e5-848b8fb10a93: Waiting for response.
Microsoft.WindowsAzure.Storage Information: 3 : 4c271cb5-7463-44b1-b2e5-848b8fb10a93: Response received. Status code = 202, Request ID = , Content-MD5 = , ETag = .
Microsoft.WindowsAzure.Storage Information: 3 : 4c271cb5-7463-44b1-b2e5-848b8fb10a93: Response headers were processed successfully, proceeding with the rest of the operation.
Microsoft.WindowsAzure.Storage Information: 3 : 4c271cb5-7463-44b1-b2e5-848b8fb10a93: Processing response body.
Microsoft.WindowsAzure.Storage Information: 3 : 4c271cb5-7463-44b1-b2e5-848b8fb10a93: Operation completed successfully.
iisexpress.exe Information: 0 : Elapsed time to batch delete 100 rows: 0:00:00.6524402

Toujours plus de 0,5 seconde.

J'ai également exécuté ce déploiement sur Azure (petite instance) et j'ai enregistré des temps de 20 minutes pour insérer 28000 lignes.

J'utilise actuellement la version 2.1 RC de la bibliothèque cliente de stockage: MSDN Blog

Je dois faire quelque chose de très mal. Des pensées?

[~ # ~] mise à jour [~ # ~]

J'ai essayé le parallélisme avec l'effet net d'une amélioration de la vitesse globale (et 8 processeurs logiques au maximum), mais à peine 150 insertions de ligne par seconde sur ma machine de développement.

Pas mieux dans l'ensemble que je peux dire, et peut-être encore pire lorsqu'il est déployé sur Azure (petite instance).

J'ai augmenté le pool de threads et augmenté le nombre maximum de connexions HTTP pour mon WebRole en suivant ce conseil .

Je sens toujours que je manque quelque chose de fondamental qui limite mes insertions/suppressions à 150 ROPS.

MISE À JOUR 2

Après avoir analysé certains journaux de diagnostic de ma petite instance déployée sur Azure (à l'aide de la nouvelle journalisation intégrée au client de stockage 2.1 RC), j'ai un peu plus d'informations.

Le premier journal du client de stockage pour une insertion par lots est à 635109046781264034 ticks:

caf06fca-1857-4875-9923-98979d850df3: Starting synchronous request to https://?.table.core.windows.net/.; TraceSource 'Microsoft.WindowsAzure.Storage' event

Puis près de 3 secondes plus tard, je vois ce journal à 635109046810104314 ticks:

caf06fca-1857-4875-9923-98979d850df3: Preparing to write request data.; TraceSource 'Microsoft.WindowsAzure.Storage' event

Ensuite, quelques journaux supplémentaires qui prennent 0,15 secondes combinées se terminant par celui-ci à 635109046811645418 ticks qui termine l'insertion:

caf06fca-1857-4875-9923-98979d850df3: Operation completed successfully.; TraceSource 'Microsoft.WindowsAzure.Storage' event

Je ne sais pas quoi faire de cela, mais c'est assez cohérent dans les journaux d'insertion de lots que j'ai examinés.

MISE À JOUR 3

Voici le code utilisé pour l'insertion par lots en parallèle. Dans ce code, juste pour les tests, je m'assure d'insérer chaque lot de 100 dans une partition unique.

static async Task BatchInsert( CloudTable table, List<ITableEntity> entities )
    {
        int rowOffset = 0;

        var tasks = new List<Task>();

        while ( rowOffset < entities.Count )
        {
            // next batch
            var rows = entities.Skip( rowOffset ).Take( 100 ).ToList();

            rowOffset += rows.Count;

            string partition = "$" + rowOffset.ToString();

            var task = Task.Factory.StartNew( () =>
                {
                    Stopwatch sw = Stopwatch.StartNew();

                    var batch = new TableBatchOperation();

                    foreach ( var row in rows )
                    {
                        row.PartitionKey = row.PartitionKey + partition;
                        batch.InsertOrReplace( row );
                    }

                    // submit
                    table.ExecuteBatch( batch );

                    Trace.TraceInformation( "Elapsed time to batch insert " + rows.Count + " rows: " + sw.Elapsed.ToString( "F2" ) );
                } );

            tasks.Add( task );
        }

        await Task.WhenAll( tasks );
    }

Comme indiqué ci-dessus, cela permet d'améliorer le temps global pour insérer des milliers de lignes, mais chaque lot de 100 prend toujours plusieurs secondes.

MISE À JOUR 4

J'ai donc créé un tout nouveau projet Azure Cloud Service, à l'aide de VS2012.2, avec le rôle Web comme modèle de page unique (le nouveau avec l'exemple TODO dedans).

C'est tout droit sorti de la boîte, pas de nouveaux packages NuGet ou quoi que ce soit. Il utilise la bibliothèque cliente de stockage v2 par défaut, et l'EDM et les bibliothèques associées v5.2.

J'ai simplement modifié le code HomeController pour qu'il soit le suivant (en utilisant des données aléatoires pour simuler les colonnes que je veux stocker dans la vraie application):

public ActionResult Index( string returnUrl )
    {
        ViewBag.ReturnUrl = returnUrl;

        Task.Factory.StartNew( () =>
            {
                TableTest();
            } );

        return View();
    }

    static Random random = new Random();
    static double RandomDouble( double maxValue )
    {
        // the Random class is not thread safe!
        lock ( random ) return random.NextDouble() * maxValue;
    }

    void TableTest()
    {
        // Retrieve storage account from connection-string
        CloudStorageAccount storageAccount = CloudStorageAccount.Parse(
            CloudConfigurationManager.GetSetting( "CloudStorageConnectionString" ) );

        // create the table client
        CloudTableClient tableClient = storageAccount.CreateCloudTableClient();

        // retrieve the table
        CloudTable table = tableClient.GetTableReference( "test" );

        // create it if it doesn't already exist
        if ( table.CreateIfNotExists() )
        {
            // the container is new and was just created
            Trace.TraceInformation( "Created table named " + "test" );
        }


        Stopwatch sw = Stopwatch.StartNew();

        // create a bunch of objects
        int count = 28000;
        List<DynamicTableEntity> entities = new List<DynamicTableEntity>( count );

        for ( int i = 0; i < count; i++ )
        {
            var row = new DynamicTableEntity()
            {
                PartitionKey = "filename.txt",
                RowKey = string.Format( "$item{0:D10}", i ),
            };

            row.Properties.Add( "Name", EntityProperty.GeneratePropertyForString( i.ToString() ) );
            row.Properties.Add( "Data", EntityProperty.GeneratePropertyForString( string.Format( "data{0}", i ) ) );
            row.Properties.Add( "Value1", EntityProperty.GeneratePropertyForDouble( RandomDouble( 10000 ) ) );
            row.Properties.Add( "Value2", EntityProperty.GeneratePropertyForDouble( RandomDouble( 10000 ) ) );
            row.Properties.Add( "Value3", EntityProperty.GeneratePropertyForDouble( RandomDouble( 1000 ) ) );
            row.Properties.Add( "Value4", EntityProperty.GeneratePropertyForDouble( RandomDouble( 90 ) ) );
            row.Properties.Add( "Value5", EntityProperty.GeneratePropertyForDouble( RandomDouble( 180 ) ) );
            row.Properties.Add( "Value6", EntityProperty.GeneratePropertyForDouble( RandomDouble( 1000 ) ) );

            entities.Add( row );
        }

        Trace.TraceInformation( "Elapsed time to create record rows: " + sw.Elapsed.ToString() );

        sw = Stopwatch.StartNew();

        Trace.TraceInformation( "Inserting rows" );

        // batch our inserts (100 max)
        BatchInsert( table, entities ).Wait();

        Trace.TraceInformation( "Successfully inserted " + entities.Count + " rows into table " + table.Name );
        Trace.TraceInformation( "Elapsed time: " + sw.Elapsed.ToString() );

        Trace.TraceInformation( "Done" );
    }


            static async Task BatchInsert( CloudTable table, List<DynamicTableEntity> entities )
    {
        int rowOffset = 0;

        var tasks = new List<Task>();

        while ( rowOffset < entities.Count )
        {
            // next batch
            var rows = entities.Skip( rowOffset ).Take( 100 ).ToList();

            rowOffset += rows.Count;

            string partition = "$" + rowOffset.ToString();

            var task = Task.Factory.StartNew( () =>
            {
                var batch = new TableBatchOperation();

                foreach ( var row in rows )
                {
                    row.PartitionKey = row.PartitionKey + partition;
                    batch.InsertOrReplace( row );
                }

                // submit
                table.ExecuteBatch( batch );

                Trace.TraceInformation( "Inserted batch for partition " + partition );
            } );

            tasks.Add( task );
        }

        await Task.WhenAll( tasks );
    }

Et voici la sortie que j'obtiens:

iisexpress.exe Information: 0 : Elapsed time to create record rows: 00:00:00.0719448
iisexpress.exe Information: 0 : Inserting rows
iisexpress.exe Information: 0 : Inserted batch for partition $100
...
iisexpress.exe Information: 0 : Successfully inserted 28000 rows into table test
iisexpress.exe Information: 0 : Elapsed time: 00:01:07.1398928

C'est un peu plus rapide que dans mon autre application, à plus de 460 ROPS. C'est encore inacceptable. Et encore une fois dans ce test, mon processeur (8 processeurs logiques) est presque au maximum et l'accès au disque est presque inactif.

Je ne sais pas ce qui ne va pas.

MISE À JOUR 5

Des rondes et des manipulations et des ajustements ont apporté des améliorations, mais je ne peux tout simplement pas l'obtenir beaucoup plus rapidement que 500-700 (ish) ROPS effectuant des opérations InsertOrReplace par lots (par lots de 100).

Ce test est effectué dans le cloud Azure, à l'aide d'une petite instance (ou deux). Sur la base des commentaires ci-dessous, je suis résigné au fait que les tests locaux seront au mieux lents.

Voici quelques exemples. Chaque exemple est sa propre PartitionKey:

Successfully inserted 904 rows into table org1; TraceSource 'w3wp.exe' event
Elapsed time: 00:00:01.3401031; TraceSource 'w3wp.exe' event

Successfully inserted 4130 rows into table org1; TraceSource 'w3wp.exe' event
Elapsed time: 00:00:07.3522871; TraceSource 'w3wp.exe' event

Successfully inserted 28020 rows into table org1; TraceSource 'w3wp.exe' event
Elapsed time: 00:00:51.9319217; TraceSource 'w3wp.exe' event

Peut-être que c'est mon compte MSDN Azure qui a des limites de performances? Je ne sais pas.

À ce stade, je pense que j'en ai fini avec cela. Peut-être que c'est assez rapide à utiliser pour mes besoins, ou peut-être que je suivrai un chemin différent.

[~ # ~] conclusion [~ # ~]

Toutes les réponses ci-dessous sont bonnes!

Pour ma question spécifique, j'ai pu voir des vitesses allant jusqu'à 2k ROPS sur une petite instance Azure, plus généralement autour de 1k. Étant donné que je dois réduire les coûts (et donc la taille des instances), cela définit pour quoi je pourrai utiliser les tables.

Merci à tous pour toute l'aide.

37
Keith Murray

Ok, la 3e répond à un charme?

http://blogs.msdn.com/b/windowsazurestorage/archive/2010/11/06/how-to-get-most-out-of-windows-Azure-tables.aspx

Un couple de choses - l'émulateur de stockage - d'un ami qui a sérieusement creusé.

"Tout frappe une seule table dans une seule base de données (plus de partitions n'affectent rien). Chaque opération d'insertion de table représente au moins 3 opérations sql. Chaque lot est à l'intérieur d'une transaction. En fonction du niveau d'isolement de la transaction, ces lots auront limité possibilité d'exécuter en parallèle.

Les lots en série doivent être plus rapides que les insertions individuelles en raison du comportement du serveur SQL. (Les insertions individuelles sont essentiellement de petites transactions qui se vident chacune sur le disque, tandis qu'une transaction réelle se vide sur le disque en tant que groupe). "

IE utilisant plusieurs partitions n'affecte pas les performances de l'émulateur, contrairement au stockage Azure réel.

Activez également la journalisation et vérifiez un peu vos journaux - c:\users\username\appdata\local\developmentstorage

La taille de lot de 100 semble offrir les meilleures performances réelles, désactivez le naggle, désactivez-le, attendez 100, augmentez la limite de connexion.

Assurez-vous également que vous n'insérez pas de doublons accidentellement, cela entraînera une erreur et ralentira tout le chemin.

et tester par rapport au stockage réel. Il y a une bibliothèque assez décente qui gère la plupart de cela pour vous - http://www.nuget.org/packages/WindowsAzure.StorageExtensions/ , assurez-vous que vous appelez réellement ToList sur les ajouts et tel qu'il ne s'exécutera pas avant d'être énuméré. De plus, cette bibliothèque utilise dynamictableentity et donc il y a un petit succès pour la sérialisation, mais elle vous permet d'utiliser des objets POCO purs sans aucun élément TableEntity.

~ JT

10
JTtheGeek

concept de base - utilisez le paraléisme pour accélérer cela.

étape 1 - donnez à votre pool de threads suffisamment de threads pour cela - ThreadPool.SetMinThreads (1024, 256);

étape 2 - utilisez des partitions. J'utilise des GUID comme identifiants, j'utilise les derniers caractères pour les diviser en 256 partitions uniques (en fait, je les regroupe en N sous-ensembles dans mon cas, 48 ​​partitions)

étape 3 - insérer à l'aide de tâches, j'utilise le regroupement d'objets pour les références de table

public List<T> InsertOrUpdate(List<T> items)
        {
            var subLists = SplitIntoPartitionedSublists(items);

            var tasks = new List<Task>();

            foreach (var subList in subLists)
            {
                List<T> list = subList;
                var task = Task.Factory.StartNew(() =>
                    {
                        var batchOp = new TableBatchOperation();
                        var tableRef = GetTableRef();

                        foreach (var item in list)
                        {
                            batchOp.Add(TableOperation.InsertOrReplace(item));
                        }

                        tableRef.ExecuteBatch(batchOp);
                        ReleaseTableRef(tableRef);
                    });
                tasks.Add(task);
            }

            Task.WaitAll(tasks.ToArray());

            return items;
        }

private IEnumerable<List<T>> SplitIntoPartitionedSublists(IEnumerable<T> items)
        {
            var itemsByPartion = new Dictionary<string, List<T>>();

            //split items into partitions
            foreach (var item in items)
            {
                var partition = GetPartition(item);
                if (itemsByPartion.ContainsKey(partition) == false)
                {
                    itemsByPartion[partition] = new List<T>();
                }
                item.PartitionKey = partition;
                item.ETag = "*";
                itemsByPartion[partition].Add(item);
            }

            //split into subsets
            var subLists = new List<List<T>>();
            foreach (var partition in itemsByPartion.Keys)
            {
                var partitionItems = itemsByPartion[partition];
                for (int i = 0; i < partitionItems.Count; i += MaxBatch)
                {
                    subLists.Add(partitionItems.Skip(i).Take(MaxBatch).ToList());
                }
            }

            return subLists;
        }

        private void BuildPartitionIndentifiers(int partitonCount)
        {
            var chars = new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }.ToList();
            var keys = new List<string>();

            for (int i = 0; i < chars.Count; i++)
            {
                var keyA = chars[i];
                for (int j = 0; j < chars.Count; j++)
                {
                    var keyB = chars[j];
                    keys.Add(string.Concat(keyA, keyB));
                }
            }


            var keySetMaxSize = Math.Max(1, (int)Math.Floor((double)keys.Count / ((double)partitonCount)));
            var keySets = new List<List<string>>();

            if (partitonCount > keys.Count)
            {
                partitonCount = keys.Count;
            }

            //Build the key sets
            var index = 0;
            while (index < keys.Count)
            {
                var keysSet = keys.Skip(index).Take(keySetMaxSize).ToList();
                keySets.Add(keysSet);
                index += keySetMaxSize;
            }

            //build the lookups and datatable for each key set
            _partitions = new List<string>();
            for (int i = 0; i < keySets.Count; i++)
            {
                var partitionName = String.Concat("subSet_", i);
                foreach (var key in keySets[i])
                {
                    _partitionByKey[key] = partitionName;
                }
                _partitions.Add(partitionName);
            }

        }

        private string GetPartition(T item)
        {
            var partKey = item.Id.ToString().Substring(34,2);
            return _partitionByKey[partKey];
        }

        private string GetPartition(Guid id)
        {
            var partKey = id.ToString().Substring(34, 2);
            return _partitionByKey[partKey];
        }

        private CloudTable GetTableRef()
        {
            CloudTable tableRef = null;
            //try to pop a table ref out of the stack
            var foundTableRefInStack = _tableRefs.TryPop(out tableRef);
            if (foundTableRefInStack == false)
            {
                //no table ref available must create a new one
                var client = _account.CreateCloudTableClient();
                client.RetryPolicy = new ExponentialRetry(TimeSpan.FromSeconds(1), 4);
                tableRef = client.GetTableReference(_sTableName);
            }

            //ensure table is created
            if (_bTableCreated != true)
            {
                tableRef.CreateIfNotExists();
                _bTableCreated = true;
            }

            return tableRef;
        }

résultat - compte de stockage 19-22kops maximum

frappez-moi si vous êtes intéressé par la source complète

besoin de gémir? utilisez plusieurs comptes de stockage!

cela vient de mois d'essais et d'erreurs, de tests, de coups de tête contre un bureau. J'espère vraiment que ça aide.

13
JTtheGeek

Après avoir traversé beaucoup de difficultés, les expériences ont finalement pu obtenir un débit optimal pour une partition de table unique (plus de 2000 opérations d'écriture par lot par seconde) et un bien meilleur débit dans le compte de stockage (3500+ opérations d'écriture par lot par seconde) avec le stockage Azure Table. J'ai essayé toutes les différentes approches, mais la définition de la limite de connexion .net par programme (j'ai essayé l'exemple de configuration, mais n'a pas fonctionné pour moi) a résolu le problème (sur la base d'un Livre blanc fourni par Microsoft), comme indiqué ci-dessous:

ServicePoint tableServicePoint = ServicePointManager
    .FindServicePoint(_StorageAccount.TableEndpoint);

//This is a notorious issue that has affected many developers. By default, the value 
//for the number of .NET HTTP connections is 2.
//This implies that only 2 concurrent connections can be maintained. This manifests itself
//as "underlying connection was closed..." when the number of concurrent requests is
//greater than 2.

tableServicePoint.ConnectionLimit = 1000;

Quiconque a obtenu une opération d'écriture par lots de 20 000+ par compte de stockage, veuillez partager votre expérience.

6
Ashraf Alam

Pour plus de plaisir, voici une nouvelle réponse - un test indépendant isolé qui tire des chiffres incroyables pour les performances d'écriture en production et fait beaucoup mieux en évitant IO blocage et gestion des connexions. Je suis très intéressé de voir comment cela fonctionne pour vous car nous obtenons des vitesses d'écriture ridicules (> 7kps).

webconfig

 <system.net>
    <connectionManagement>
      <add address="*" maxconnection="48"/>
    </connectionManagement>
  </system.net>

Pour le test, j'utilisais des paramètres basés sur le volume, donc comme 25000 éléments, 24 partitions, la taille de lot de 100 semble toujours être la meilleure et le nombre de références de 20. Ceci utilise le flux de données TPL ( http: // www .nuget.org/packages/Microsoft.Tpl.Dataflow / ) pour BufflerBlock qui fournit une belle extraction de référence de table sécurisée pour les threads.

public class DyanmicBulkInsertTestPooledRefsAndAsynch : WebTest, IDynamicWebTest
{
    private int _itemCount;
    private int _partitionCount;
    private int _batchSize;
    private List<TestTableEntity> _items;
    private GuidIdPartitionSplitter<TestTableEntity> _partitionSplitter;
    private string _tableName;
    private CloudStorageAccount _account;
    private CloudTableClient _tableClient;
    private Dictionary<string, List<TestTableEntity>> _itemsByParition;
    private int _maxRefCount;
    private BufferBlock<CloudTable> _tableRefs;


    public DyanmicBulkInsertTestPooledRefsAndAsynch()
    {
        Properties = new List<ItemProp>();    
        Properties.Add(new ItemProp("ItemCount", typeof(int)));
        Properties.Add(new ItemProp("PartitionCount", typeof(int)));
        Properties.Add(new ItemProp("BatchSize", typeof(int)));
        Properties.Add(new ItemProp("MaxRefs", typeof(int)));


    }

    public List<ItemProp> Properties { get; set; }

    public void SetProps(Dictionary<string, object> propValuesByPropName)
    {
        _itemCount = (int)propValuesByPropName["ItemCount"];
        _partitionCount = (int)propValuesByPropName["PartitionCount"];
        _batchSize = (int)propValuesByPropName["BatchSize"];
        _maxRefCount = (int)propValuesByPropName["MaxRefs"];
    }

    protected override void SetupTest()
    {
        base.SetupTest();

        ThreadPool.SetMinThreads(1024, 256);
        ServicePointManager.DefaultConnectionLimit = 256;
        ServicePointManager.UseNagleAlgorithm = false;
        ServicePointManager.Expect100Continue = false;


        _account = CloudStorageAccount.Parse(CloudConfigurationManager.GetSetting("DataConnectionString"));
        _tableClient = _account.CreateCloudTableClient();
        _tableName = "testtable" + new Random().Next(100000);

        //create the refs
        _tableRefs = new BufferBlock<CloudTable>();
        for (int i = 0; i < _maxRefCount; i++)
        {
            _tableRefs.Post(_tableClient.GetTableReference(_tableName));
        }

        var tableRefTask = GetTableRef();
        tableRefTask.Wait();
        var tableRef = tableRefTask.Result;

        tableRef.CreateIfNotExists();
        ReleaseRef(tableRef);

        _items = TestUtils.GenerateTableItems(_itemCount);
        _partitionSplitter = new GuidIdPartitionSplitter<TestTableEntity>();
        _partitionSplitter.BuildPartitions(_partitionCount);

        _items.ForEach(o =>
            {
                o.ETag = "*";
                o.Timestamp = DateTime.Now;
                o.PartitionKey = _partitionSplitter.GetPartition(o);
            });

        _itemsByParition = _partitionSplitter.SplitIntoPartitionedSublists(_items);
    }

    private async Task<CloudTable> GetTableRef()
    {
        return await _tableRefs.ReceiveAsync();            
    }

    private void ReleaseRef(CloudTable tableRef)
    {
        _tableRefs.Post(tableRef);
    }

    protected override void ExecuteTest()
    {
        Task.WaitAll(_itemsByParition.Keys.Select(parition => Task.Factory.StartNew(() => InsertParitionItems(_itemsByParition[parition]))).ToArray());
    }

    private void InsertParitionItems(List<TestTableEntity> items)
    {

        var tasks = new List<Task>();

        for (int i = 0; i < items.Count; i += _batchSize)
        {
            int i1 = i;

            var task = Task.Factory.StartNew(async () =>
            {
                var batchItems = items.Skip(i1).Take(_batchSize).ToList();

                if (batchItems.Select(o => o.PartitionKey).Distinct().Count() > 1)
                {
                    throw new Exception("Multiple partitions batch");
                }

                var batchOp = new TableBatchOperation();
                batchItems.ForEach(batchOp.InsertOrReplace);   

                var tableRef = GetTableRef.Result();
                tableRef.ExecuteBatch(batchOp);
                ReleaseRef(tableRef);
            });

            tasks.Add(task);

        }

        Task.WaitAll(tasks.ToArray());


    }

    protected override void CleanupTest()
    {
        var tableRefTask = GetTableRef();
        tableRefTask.Wait();
        var tableRef = tableRefTask.Result;
        tableRef.DeleteIfExists();
        ReleaseRef(tableRef);
    }

Nous travaillons actuellement sur une version qui peut gérer plusieurs comptes de stockage pour, espérons-le, obtenir des vitesses folles. De plus, nous les exécutons sur des machines virtuelles à 8 cœurs pour les grands ensembles de données, mais avec le nouveau non bloquant IO il devrait fonctionner parfaitement sur un vm limité. Bonne chance!

 public class SimpleGuidIdPartitionSplitter<T> where T : IUniqueId
{
    private ConcurrentDictionary<string, string> _partitionByKey = new ConcurrentDictionary<string, string>();
    private List<string> _partitions;
    private bool _bPartitionsBuilt;

    public SimpleGuidIdPartitionSplitter()
    {

    }

    public void BuildPartitions(int iPartCount)
    {
        BuildPartitionIndentifiers(iPartCount);
    }

    public string GetPartition(T item)
    {
        if (_bPartitionsBuilt == false)
        {
            throw new Exception("Partitions Not Built");
        }

        var partKey = item.Id.ToString().Substring(34, 2);
        return _partitionByKey[partKey];
    }

    public string GetPartition(Guid id)
    {
        if (_bPartitionsBuilt == false)
        {
            throw new Exception("Partitions Not Built");
        }

        var partKey = id.ToString().Substring(34, 2);
        return _partitionByKey[partKey];
    }

    #region Helpers
    private void BuildPartitionIndentifiers(int partitonCount)
    {
        var chars = new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }.ToList();
        var keys = new List<string>();

        for (int i = 0; i < chars.Count; i++)
        {
            var keyA = chars[i];
            for (int j = 0; j < chars.Count; j++)
            {
                var keyB = chars[j];
                keys.Add(string.Concat(keyA, keyB));
            }
        }


        var keySetMaxSize = Math.Max(1, (int)Math.Floor((double)keys.Count / ((double)partitonCount)));
        var keySets = new List<List<string>>();

        if (partitonCount > keys.Count)
        {
            partitonCount = keys.Count;
        }

        //Build the key sets
        var index = 0;
        while (index < keys.Count)
        {
            var keysSet = keys.Skip(index).Take(keySetMaxSize).ToList();
            keySets.Add(keysSet);
            index += keySetMaxSize;
        }

        //build the lookups and datatable for each key set
        _partitions = new List<string>();
        for (int i = 0; i < keySets.Count; i++)
        {
            var partitionName = String.Concat("subSet_", i);
            foreach (var key in keySets[i])
            {
                _partitionByKey[key] = partitionName;
            }
            _partitions.Add(partitionName);
        }

        _bPartitionsBuilt = true;
    }
    #endregion
}



internal static List<TestTableEntity> GenerateTableItems(int count)
        {
            var items = new List<TestTableEntity>();
            var random = new Random();

            for (int i = 0; i < count; i++)
            {
                var itemId = Guid.NewGuid();

                items.Add(new TestTableEntity()
                {
                    Id = itemId,
                    TestGuid = Guid.NewGuid(),
                    RowKey = itemId.ToString(),
                    TestBool = true,
                    TestDateTime = DateTime.Now,
                    TestDouble = random.Next() * 1000000,
                    TestInt = random.Next(10000),
                    TestString = Guid.NewGuid().ToString(),
                });
            }

            var dupRowKeys = items.GroupBy(o => o.RowKey).Where(o => o.Count() > 1).Select(o => o.Key).ToList();
            if (dupRowKeys.Count > 0)
            {
                throw  new Exception("Dupicate Row Keys");
            }

            return items;
        }

et encore une chose - votre timing et comment le framework a été affecté le montrent http://blogs.msdn.com/b/windowsazurestorage/archive/2013/08/08/net-clients-encountering-port- épuisement-après-installation-kb2750149-ou-kb2805227.aspx

5
JTtheGeek