Featured image of post Optimize Azure CosmosDB costs

Optimize Azure CosmosDB costs

Azure CosmosDB is one of the best NoSQL databases I ever worked with. But in many use cases, this database makes your budget bleeding. This post describes this service pricing & how to optimize costs (mostly for MongoDB-styled developers).

Azure CosmosDB pricing explained

Pricing of this service is not so complicated as it looks on the first try – you pay for two parameters together – for saved document size ($0.25 per GB) + for reserved database performance (throughput) – in Request Units (RU) / second unit.

Minimum performance you have to reserve is 400 RU/s ($0.008 per 1 hour), scaleable by 100 RU/s.

Pricing #1 per document collection

In this pricing mode, you pay for reserved performance per every document collection.

So at the minimum, you will pay for every document collection in your database $23.61 per month (400 RU/s, 1GB storage, Single Region Write).

You can scale performance for every collection separately. But if you don’t fill up reserved performance, you are wasting your money.

Pricing #2 per database

Azure also provides alternative pricing – you can set up reserved performance for whole database. This reserved performance is shared across all collections in database. Minimum is again 400 RU/s

Every collection needs to have at least 100 RU/s – so first 4 collections are included, extra collection will cost you +100 RU/s per collection.

And small note, this pricing mode has one extra requirement – you need to have shard key in all your collections.

Pricing-problematic use case

Imagine you want to migrate f.ex. application, which has 10x MongoDB entity types (collections).

By pricing mode #1 you get amount $233.6 per month (400 RU/s per collection = 4000 RU/s in total) By pricing mode #2 you get amount $58.4 per month (1000 RU/s in total) – both without storage cost

That could be ok for some kind of applications. But what if I have application, which has some primary collections with frequent access and some supportive collections with infrequent access?

So you can join the group of users, who pay $23.61 per month per every infrequent collection. Or you able to not use CosmosDB for infrequent collections, but I would have to manage another connection to another database service, care about SLAs etc.

First reasonable solution is to use the pricing mode #2, but you have to pay at least 100 RU/s per every collection, so if you have a lot of infrequent collections, you still going to pay too much.

Solution based on MongoDB discriminators

When I read the pricing description in detail, I got an idea:

What about put all entities in one collection and mark into BSON serializated entity their type and use it during deserialization?

One of my friend reminded me, that MongoDB C# SDK (MongoDB SDKs for almost every language) already have built-in feature called discriminator. This feature allows you to use polymorphism on entites saved to MongoDB database (more info).

1
2
3
4
[BsonDiscriminator("User")]
public User {
    // fields and properties
}

The idea is simple – I am going to create only one collection in pricing mode #1 (with 400RU/s), where are located entities across all infrequent collections.

Example implementation in MongoDB .NET SDK

ACommonStoredEntityModel.cs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/// 
/// Abstract class of entity saved in MongoDB
/// 
[BsonIgnoreExtraElements]
[BsonDiscriminator(Required = true)]
public abstract class ACommonStoredEntityModel
{
    const string COMMON_COLLECTION_NAME = "common_storage";

    [BsonId]
    public ObjectId id;
 
    public ACommonStoredEntityModel()
    {
    }


    /// 
    /// Get collection name in database
    /// 
    /// 
    public static string GetCommonCollectionName()
    {
        return COMMON_COLLECTION_NAME;
    }
}

Category.cs (entity #1)

1
2
3
4
public class Category : ACommonStoredEntityModel
{
    public string title;
}

User.cs (entity #2)

1
2
3
4
5
6

public class User : ACommonStoredEntityModel
{
    public string username;
    public string email;
}

AMongoCommonRepository.cs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
/// 
/// Repository interface for common collection saved entities
/// Do not forget to register your new Repository in IoC in Startup.cs (ConfigureServices())
/// 
public abstract class AMongoCommonRepository where T : ACommonStoredEntityModel
{
    /// 
    /// collection
    /// 
    protected IMongoCollection _collection;

    /// 
    /// mongodb connection
    /// 
    protected IMongoDatabase _mongoDatabase = null;

    public AMongoCommonRepository(IMongoDatabase mongoDatabase)
    {
        _mongoDatabase = mongoDatabase;


        if (!IsCommonCollectionExists())
        {
            _mongoDatabase.CreateCollection(ACommonStoredEntityModel.GetCommonCollectionName());
        }
        _collection = _mongoDatabase.GetCollection(ACommonStoredEntityModel.GetCommonCollectionName()).OfType();

        if (!BsonClassMap.IsClassMapRegistered(typeof(T)))
        {
            BsonClassMap.RegisterClassMap(cm =>
            {
                cm.SetDiscriminator(typeof(T).Name);
            });
        }

    }

    /// 
    /// checks if given collectionName exists in database
    /// 
    /// 
    public bool IsCommonCollectionExists()
    {
        var filter = new BsonDocument("name", ACommonStoredEntityModel.GetCommonCollectionName());
        //filter by collection name
        var collections = _mongoDatabase.ListCollections(new ListCollectionsOptions { Filter = filter });
        //check for existence
        return collections.Any();
    }


    /// 
    /// Get all entities
    /// 
    /// all entities
    public async Task> GetAllAsync()
    {
        var result = await _collection.FindAsync(u => 1 == 1);
        return result.ToList();
    }
}

CategoryRepository.cs

1
2
3
4
5
6
7
/// 
/// Repository interface for Category entity
/// Do not forget to register Repository in IoC in Startup.cs (ConfigureServices())
/// 
public class CategoryRepository : AMongoCommonRepository
{
}

UserRepository.cs

1
2
3
4
5
6
7
///
/// Repository interface for User entity
/// Do not forget to register Repository in IoC in Startup.cs (ConfigureServices())
/// 
public class UserRepository : AMongoCommonRepository
{
}

By this repository approach you can access these entities like before the change:

Usage

1
2
var users = await _userRepository.GetAllAsync();
var category = await _categoryRepository.GetAllAsync();

In this approach there is no sharding key, but it’s easy to add it.

Results

As you see, SDK automatically adds discriminator attribute _t. Thanks to this attribute MongoDB SDK recognizes entity type, how to deserialize document.

Example of entity with discriminator saved in MongoDB database

Cost savings – case study

We used this approach in one project – Web API application, which has some collections with frequent access and few with infrequent access. We moved these infrequent collections to this common collection.

Cost savings graph in case study

As you see, we got about -33% cost savings with fully utilized performance, without any performance issues in application (because existing prepaid reserved database performance was not utilized).

Also we still have possibility to extend performance for common collection (by granularity +-100 RU/s).

This solution will not save you every time, but for similar use cases can be really helpful.

comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy