最近在做一个项目,其实就是前面一篇写的那个项目。这个项目呢可能每秒的并发度会上几千,而且最主要的几个高并发都是读操作,所以就在考虑能不能上Azure Cosmos DB。
Azure Cosmos DB呢也是一个奇葩的产品,是一个仅靠它独自一人就能支持KV,文档和图的nosql数据库。并且可以支持SQL,Mongo,Cassandra,Gremlin的不同的API,不过即便你用SQL API,它后面也是对应到一个文档数据库。
那么使用Azure Cosmos DB做尝试呢其实之前也有一些不同的人做过几次尝试,但都是以失败告终,最大的问题是这个东西贼贵,它不是说我买了就按月收费,而是说每一次访问都要收费,而且你如果design的不好的话它每次访问收的费用和一个design的好的相比,可能会差上几千倍。这个Azure的文档里已经有例子了。总之就是一个字贼贵,然后据说也没有达到预期的性能。
那么我其实一直认为的是前面的人没有design好,或者没有用对这个Cosmos DB,所以导致贵和性能不达预期。我一直以为我只要很好的design,是可以让它发挥威力的。
为此我们就做了SQL Server和Cosmos DB的对比测试。注意本文中所指的SQL Server都是指的Azure上的SQL Server。首先呢我们把这个2个数据库的每月价格调到差不多的程度,在价格几乎相等的情况下来测试性能,我们发现,SQL Server能支持的最大并发每秒数量是Cosmos的4倍,但在latency上,2者都满载的情况下,Cosmos DB是有一点优势的,可能在10%左右,如果每月价格再上去,那么Cosmos DB的latency优势会提高很多,但是它在最大并发数上还是落后SQL Server 4倍。
然后呢我们又把这个2个数据调整到最大并发每秒数量都差不多,此时呢,我们发现在相同并发数情况下,Cosmos DB的价格会是SQL Server的4倍。但在latency上Cosmos DB仍然有优势。
不管怎么测,Cosmos DB的latency对于每个call都非常平均,它的P75和P95其实没有太大差别,也就是说cosmos可以给你提供非常稳定的latency表现,而SQL Server随着并发数的增加,它的latency是越来越慢的,所以它的P75和P95有着明显的差距。
因此,总结一下:
- 相同价格下,SQL Server的最大并发数是Cosmos DB的4倍,满载情况下SQL Server的latency不及Cosmos DB。
- 相同最大并发数下,Cosmos的价格是SQL Server的4倍,满载情况下SQL Server的latency仍然不及Cosmos DB。
- 随着并发数增加,越来越接近满载,SQL Server的latency会越来越慢,而Cosmos DB可以提供较为稳定的latency,latency增长程度不如SQL Server。
但是,这还没完,我们又换了个API继续测,此时,我们发现Cosmos DB全线溃败,相同价格下Cosmos的最大并发数竟然只有SQL Server的几十分之一,并且即便把Cosmos的性能加满到100000RU,它也只有最大1000的并发度,而SQL Server以一个很小的价格早就突破了2000并发度。
那么为什么换了个API就造成了完全不一样的结果呢,我想了一下,这2个API其实每次调用都需要7-8次的DB访问,本质上并没有太大的区别,那么唯一的问题是,对于第一个API,Cosmos的DB访问每次都能query到它的partitionKey,而对于第二个API,Cosmos的DB访问每次都不能query到它的partitionKey,因为对于这个API它的数据的partitionKey并不是它query的那个field,所以这就造成了在所有partition上的fan out query,后果很严重。
我本来也没想到partitionKey对query的影响会这么大,因为对于第一个API我们插入了1千万条数据,而对于第二个API我们只有2万条数据而已。本来我以为如果数据量小,那么partitionKey应该关系不大,但事实证明我的想法是错的。
所以一个很重要的总结如下:
- 对于Cosmos DB,必须保证每个query都能hit到它的partitionKey,不hit到partitionKey的query是决不能允许的。
那么这个限制条件其实是一个非常非常强的限制条件。非常非常严重的限制了Cosmos DB的应用。原因如下:
- 如果我2个query要分别根据不同的field来query,那么一个container只能设置一个partitionKey,所以我不得不duplicate这个数据到另一个container来设置另一个partionKey,这样2个query分别query不同的container才能,数据的duplicate导致了数据的维护逻辑会变得很复杂。当然我可以不duplicate所有的东西,只duplicate一部分每个query各自关心的东西,那么此时duplicate就变成了denormalize,其实你看Azure的官方文档里举的例子也是做了类似的数据的denormalize,但是这样做的复杂性就会把数据的设计严重依赖于业务逻辑,就得根据业务逻辑来优化,过程会非常复杂。
- 最好的denormalize其实是没有任何duplicate的denormalize,比如说我有一个json A里面就直接contain了B,而B也只在A里出现,这样我插入或者读取的时候直接把A和B一起写进去或者读出来,这样denormalize是最高效的。但是这也就是意味着A和B必须一起出现,对于我这个项目,目前连UI都没确定,A和B能不能一起出现是不能保证的,所以这种方式我现在做不到,我现在只能把A和B分开成2个json,那么既然A和B是1对多的关系,那么B里面必然会有一个A的Id,然后query A里有多少B的时候,其实是query B里面的这个A的Id是不是等于给定的值,那么B的partitionKey设置为这个A的Id是最好不过的。但是这一点我也做不到,因为我目前还不能确定A和B的存在的先后关系,比如我在UI上可以先save B,再save A,那么在这种情况下势必我一开始的B里面的这个A的Id的值一开始是空的,然后等A save了以后,我才能update这个值到某一个A的Id。但是Cosmos的partitionKey的值,一旦确定就不能再修改了,除非把B删了在重新插入,那么逻辑就又变的复杂了,而且我不确定空的partitionKey值会对query造成什么影响,如果有太多空的partitionKey值的话,那么就违反了Cosmos要求的partitionKey值应该尽可能均匀分布的要求,又会带来其他问题我就不再多说了。
综上,我最后意识到的最重要的一条总结是:
- Cosmos DB,或者说一般性的nosql,只能用在需求非常明确的项目上,因为它的数据设计十分依赖业务逻辑具体是什么样的。对于需求不是特别清晰的项目,不适合使用Cosmos DB。相反如果用SQL Server,因为SQL自带范式,所以可以相对脱离业务逻辑来设计它的数据,对于需求不是特别清晰的项目这是一个灵活性上的大的优势。