概述
使任何数据结构或算法尽可能快的方法是让代码完全按照您的要求进行操作,而不会做更多。构建一个可以做任何人想要的一切的数据存储的问题是它不会做任何特别好的事情。
就性能而言,您可以使用自定义数据存储实现什么?
您可以支持:
- 大约 75 纳秒的读/写延迟。
- 每秒 4000 万次操作的吞吐量。
- 使用二进制编码和压缩将数据大小减少 100 倍或更多。这节省了内存并增加了可扩展性。
- 控制复制如何利用您的网络,或与您的数据库同步。
- Chronicle Map 和 Queue 非常高效,但是,如果它们不能完全满足您的需求,或者它们做的比您需要的更多,这种收集可能会更快。
- 为已知的、相当稳定的密钥集优化散列函数,以最大限度地降低冲突率。
我们真的需要可定制的数据存储吗?
大多数开发人员并不太关心他们的数据存储的效率,通用数据存储工作得很好并且隐藏了它们实际工作方式的细节。这可以为开发人员节省大量时间来担心数据存储功能的细节。
有时,数据存储的选择及其运作方式确实很重要。如果数据存储被大量使用,那么数据的排列方式、它提供的功能以及同样重要的是,它不提供的功能都非常重要。您不想为未使用的支持功能支付开销。
为什么反应式系统有更大的需求?
反应式系统对及时性有更高的要求,需要在提交后的毫秒甚至微秒内查看事件/更新。
反应式系统更可能关心数据如何达到最终状态。与轮询系统不同,在轮询系统中,您更有可能只看到多次更改的最终结果,反应式系统可能需要准确查看进行了哪些更改以及更改的顺序。
低延迟、高吞吐量
一个简单的线程安全、分段的键值存储可以有大约 75 纳秒的延迟,并支持每秒 4000 万次访问(获取或放置)。添加对更多功能的支持会影响性能,因此如果性能也很重要,您只想添加您实际需要的功能。
即使是简单的事情,比如添加可能需要 30 纳秒的时间戳,听起来很快,但可能意味着操作需要 50% 的时间。
您希望能够自定义哪些选项?
您需要哪个级别的排序或事件序列化?
当您降低排序约束的级别时,您会发现:
- 全球总订单
- 数据存储排序
- 段排序
- 密钥排序
- 没有订购
排序(即保留所有读者看到事件的顺序)约束与事件的锁定或序列化密切相关。锁定更容易实现并支持更丰富的功能,但是无锁算法不仅速度更快,而且可扩展性更强,延迟更一致。
在数据存储中,通过总排序,您将看到所有更改的顺序一致。虽然这是最安全的选择,但它对所有数据提出了全局序列化要求。这极大地限制了并发更新的选项。然而,这确实简化了锁定,因为您将对所有数据进行全局锁定。
总排序的替代方法是对数据存储进行排序。这意味着您将知道商店所有更改的确切顺序,但不会记录商店之间的更改。 (您可以添加时间戳以获得更改发生时间的理想情况)
要允许商店内并发,您可以使用基于段或页面的排序。当您更新分配给段的条目时,该段将被锁定,但可以更新其他段。您可以获取该段内所有事件的顺序,但不能获取段与段之间的顺序。
最大的并发性可以通过只限制对单个键的更改顺序来实现。这样可以同时更新任意数量的密钥,但至少您知道更新的密钥是什么。
最后,您可能不需要任何这些。如果一个条目永远不会改变,它要么存在要么不存在,这尤其有用。您可能希望防止任何记录被更改。即只能添加记录。如果具有相同详细信息的相同记录被添加两次,这可能是可以接受的并被忽略为重复。
共享内存数据存储
我们发现一个特别有用的特性是能够在同一台机器上的 JVM 之间共享数据。这允许所有 JVM 以内存速度访问数据。
虽然此功能不会减慢解决方案的速度,但它确实对设计施加了一些限制以允许其工作。特别是,Java 不支持在 JVM 之间共享堆,要共享您需要使用堆外内存的内存。
如果数据结构不在 JVM 之间共享,您可以使用堆锁定机制,这种机制速度较慢但提供其他功能,例如公平锁定。
复制模型
有多种复制数据的方法会影响数据存储的效率:
- 最终一致性。我们喜欢这种模型,因为它可以优雅地处理裂脑情况。
- 交易更新。一个事件要么对集群中的所有节点可见,要么对它们都不可见。
- 至少有一个备份。更新保存到至少两个节点。如果失败,数据不会丢失。这比确保每个节点都接受更新更快。
- 多集群复制。虽然数据可以在本地集群内自由复制,但您可能希望控制将哪些数据复制到区域之间以及如何执行。
- 流量整形您可能希望控制更新速率或使用的带宽,以及是否使用压缩。
同步或异步持久化
我们的解决方案非常努力地与大多数异步执行更新的解决方案一样快速同步。这有助于减少开销和复杂性。
通常,对内存映射文件的写入不会立即刷新到磁盘,因此只要您没有超载,磁盘子系统的选择并不重要。就吞吐量而言,重要的是您的带宽利用率。如果您持续使用一小部分带宽,您可能会很快耗尽磁盘空间。如果您正在写入即使是非常适度的 12 MB/s,也就是每天超过 1 TB。
我们测试过的操作系统不会对您完全隐藏磁盘子系统。对于十分之一或一百分之一的写入,延迟将取决于您拥有的磁盘子系统的类型。如果您关心 99% 的磁贴延迟,您选择的磁盘子系统仍然很重要。
如果不是 PCI-SSD,您会假设任何关心性能的人都会使用 SSD,因为它们的延迟比旋转磁盘快大约 100 倍。企业 SSD 的 IOPS(每秒 IO 数)数量也高出约 100 倍。桌面 SSD 可以高出 1000 倍,因此您可以预期这也将成为企业磁盘的标准。
不幸的是,在大型组织中并没有那么简单,如果他们能够获得批准,使用 SSD 驱动器可能需要很长时间,例如 6 到 12 个月。
一种解决方法是将数据异步写入内存,并在另一个线程中将其假脱机到磁盘。
数据应该存储为文本还是二进制?
二进制数据通常比文本更有效,除非数据已经是文本格式。通过将 XML 或 JSON 等高度冗长的格式转换为二进制格式,在检索时将其转换回文本,可以获得一些收益。这是一种特定于格式的压缩,即使与通用压缩相比也能很好地工作(见下文)
转换为二进制格式可以将数据大小减少 3 到 10 倍。如果格式可以是有损的,则可以节省更多空间。 (例如,可以删除空格)如果还使用通用压缩,您可以获得 20 到 200 倍的压缩率。
是否应该压缩数据?
压缩数据是 CPU 和消耗空间之间的权衡。有许多压缩策略使用较少的 CPU 但不压缩,使用更多 CPU 和进一步压缩数据的策略。
这不仅可以节省磁盘空间,还可以节省内存消耗。这使您可以扩展可以有效存储的数据量。
如果您有足够的内存,您可能希望避免压缩以节省 CPU。
如果您的数据条目很大,则压缩每个单独的条目可能效果很好。如果您的数据条目很小,您可以通过压缩条目块来获得显着的收益。
您甚至可能需要一种混合方法,在这种方法中,近期数据不会被压缩,但长期数据会被异步压缩。
如果使用通用压缩,可以获得 5 到 50 倍的压缩率。
在响应式系统中,消费者能否整合它错过的更新?
如果您的系统中有一个缓慢的消费者,您需要一种简单的方法来赶上。你总会有暂时落后的消费者,但在某些系统中,他们可能落后很长时间。例如,在 Chronicle Queue 中,消费者可以不仅仅是生产者后面的主内存,因为它从不丢弃更新。
如果你放弃更新,你可以很快赶上,假设同一个密钥有很多更新,或者有一个简单的整合策略。
有时您需要查看每个事件/消息/更改,无论它们有多老。这对于审计目的很有用。
您可能需要一种混合方法,其中每个事件都被记录下来,但一些消费者可以跳到密钥的最新更新。
批处理数据
在每个事务开销很高的事务数据中,使用批处理确实很有帮助。批处理对于 IO 操作再次减少开销也很有用。
我们的大多数解决方案都试图使每个事务的开销非常低,以最大程度地减少延迟,因此添加批处理可能会引入比节省的开销更多的开销。
更强大的安全模型
您可能需要能够控制对单个集合的访问,但您可能还需要将访问控制列表添加到每个单独的密钥。
您可能需要根据这些条目的内容访问条目。例如,纽约的员工可能能够更新 location=New York 的条目。一个地区、组织或团队中的员工可以管理自己的数据。
时间戳更改
更新/事件是否应该加上时间戳?这可能很有用,但如果不使用,则会产生不小的开销。
审计信息和简化安全
进行更改时,您可能需要记录其他信息,例如;谁进行了更改、何时更改或来自哪个客户。这对于审计目的和简化安全模型很有用。
使用审核可以鼓励用户更多地考虑他们所做的更改。他们仍然可以执行您可能很少想执行的操作,但用户知道您可以准确跟踪谁在什么时间做了什么。
如果你也有能力撤销/纠正所做的更改,这可能是另一种处理错误的方法。
远程访问、对象序列化和监控呢?
Chronicle Journal 基于 Chronicle Engine 构建,并提供与 Engine 相同的所有功能。 Journal 为 Engine 提供了一个定制的数据存储来分发。
Chronicle Journal 是开源的吗?
我们有两个开源数据存储解决方案,Chronicle Queue 和 Chronicle Map,它们非常适合特定用例,您可能希望先尝试一下,看看它们是否满足您的需求。
Chronicle Journal 的设计更加可定制,这反过来需要更多的咨询来实现解决方案。因此,它在 GitHub 上,但只有签署了支持协议的客户才能访问。