本文是 FacebookRocksDB 8年开发历程的回顾,重点讨论了为支持大规模分布式系统所做的开发优先级取舍与演进,以及在生产环境中运行大规模应用的经验。

介绍

RocksDBFacebook 在2012年创建的高性能 KV 持久存储引擎,代码衍生自 GoogleLevelDB。它针对 SSD 的某些特性进行了优化,目标是服务于大型(分布式)应用,在使用上则以类库的方式和上层应用集成。每个 RocksDB 实例是个单机版程序,本身不提供跨主机间的操作,例如副本管理和负载均衡,同时也不提供高阶 API,例如不支持 checkpoint,这些都留给上层应用自行实现。

RocksDB 及其高度可定制的组件设计使其能够从容应对不同的业务需求和工作负载。除了作为数据库系统的存储引擎外,RocksDB 还被用于以下几种不同类型的服务:

  • 流式处理:典型代表如 Flink 借助 RocksDB 保存 checkpoint 的状态数据
  • 日志/队列服务:依托于 RocksDB 可定制化的合并策略,这些服务能够以不亚于追加写单个文件的效率实现高吞吐的写入,同时有着较低的写放大,以及享受内置索引带来的便利
  • 索引服务:RocksDBbulk loading 特性能够为索引服务提供大规模加载离线数据的能力,同时也有着高效的查询性能
  • 基于 SSD 的二级缓存:因为 RocksDB 针对 SSD 进行了优化,所以某些内存式的缓存服务会借助 RocksDB 在内存不够时将部分数据置换到 SSD。这些服务往往要求存储引擎有着足够高的写入速度和优秀的点查询性能

背景

RocksDB 的设计极大的受到了 SSD 特性的影响,SSD 不对称的读写性能和有限的耐用性给 RocksDB 的数据结构设计和系统架构带来了机遇和挑战。

基于 SSD 的嵌入式存储

相比于机械硬盘,SSD 读写的 IOPS 可以达到十万至百万,读写速度可以达到几百至几千 MB/s。一方面,这给如何设计软件从而能充分利用 SSD 的性能带来了挑战;另一方面,受限于 SSD 有限的擦除次数,同时也需要考虑如何设计合理的数据结构,避免提前耗尽 SSD 的寿命。

正因为 SSD 有着出色的性能,在大多数情况下,应用的性能瓶颈也从设备 I/O 转向了网络;应用架构设计时也更倾向于将数据存储在本地 SSD 而不是远程存储服务,因此,能够内嵌在应用中的本地 KV 存储引擎的需求就日渐上涨。

在这个背景下,Facebook 实现了 RocksDB,其中 LSM 树扮演了重大的角色。

RocksDB 的架构和 LSM 树的使用

RocksDB 使用 LSM 树作为主要的数据结构来保存数据并支持以下核心的操作。

写入时会先将数据写入到名为 MemTable 的内存写缓冲中,同时也会在磁盘上记录 Write Aghead Log (WAL)MemTable 由跳表(skiplist)实现,插入和查询的时间复杂度都是 O(logn)WAL 可按需开启,用于 RocksDB 从崩溃后恢复数据。当 MemTable 的大小达到所配置的阈值时:

  1. 当前接受写入的 MemTableWAL 变为只读
  2. 后续新的写入转到新创建的 MemTableWAL
  3. 系统会将变为只读的 MemTableWAL 的内容落盘到 Sorted String Table (SSTable)
  4. 已落盘的 MemTableWAL 则可以丢弃

SSTable 中的数据按序存储,并以等大小的块(block)组织。SSTable 生成后同样只能只读,同时,其内部会维护一个索引块,索引块中会给每个数据块维护一条索引,从而能借助二分查找快速搜索。

合并

alt

如上图所示,一个 LSM 树分为多层。最新的 SSTableMemTable 刷盘生成,并放置在 Level-0。其他层的 SSTable 则统一由合并程序维护。当第 L 层的 SSTable 大小触及了配置值,合并程序会选择该层的部分 SSTable,并将其和第 L + 1 层内键的范围存在重合的 SSTable 进行合并,从而在第 L + 1 层生成一个新的 SSTable。通过这个操作,RocksDB 就可以将已删除和过时的数据清除,同时新生成的 SSTable 也进行了瘦身,节省了磁盘空间,最终写入的数据会逐渐从 Level-0 迁移到最后一层。整个合并过程的 I/O 效率也比较高,一方面不同层的合并可以并行执行,另一方面 I/O 操作只涉及整个 SSTable 文件的批量读和写。

MemTableLevel-0 层的 SSTable 键的范围可能会存在重合,而 Level-1 及其之后的每一层内,RocksDB 会确保每个 SSTable 之间键的范围不会重合(但是不同层之间的 SSTable 键的范围是有可能重合的)。

RocksDB 支持不同类型的合并策略:

  • Leveled Compaction:借鉴自 LevelDB 并加以改进。每一层可容纳的文件大小呈指数级放大。系统会积极的触发合并以确保每层的文件大小不会超过指定阈值
  • Tiered Compaction:在 RocksDB 中也被称为 Universal Compactioin,与 Apache CassandraHBase 采取的合并策略类似。当 Level-0 层文件的个数或者非 Level-0 层的个数超过指定的阈值时,又或者整个数据库的大小和最深层文件大小之比超过指定的阈值时,就会触发合并多个 SSTable。有别于 Leveled CompactionTiered Compaction 是惰性合并,实际的合并会推迟到读性能或者空间效率发生衰减时进行,从而能够一次性合并更多的数据
  • FIFO Compaction:当数据库大小触及到指定阈值时,丢弃最老的 SSTable,且只进行轻量级的合并。适合于基于内存的缓存应用

RocksDB 的读写性能在不同的合并策略下有着不同的表现,应用开发者需要结合自身服务的工作负载来选择合适的合并策略。

读取时,RocksDB 首先在所有的 MemTable 中查找,如果没有找到则继续在位于 Level-0 层的所有 SSTable 中查找,如果还没有找到,则继续向下一层中键的范围包含要查找的键的 SSTable 中查找,所有的查找都借助了二分搜索。另外还有两项辅助查找的优化:

  1. 频繁被访问的 SSTable 块会在内存中缓存从而减少文件 I/O,以及解压缩的开销
  2. 布隆过滤器用于快速排除一定不包含要查找的键的 SSTable

Column Family

RocksDB 在2014年引入了 column family 功能,不同的 column family 下可以包含相同的键,每个 column family 有独立的 MemTableSStable,但是共享 WAL。其优势在于:

  1. 每个 column family 可独立配置,如合并,压缩,merge operators 以及 compaction filters
  2. 共享的 WAL 能够原子性的记录多个 column family 的更新
  3. column family 可动态高效的删除和创建

资源优化目标的演进

写放大

RocksDB 最初的资源优化目标在于减少 SSD 的擦除周期以及写放大,写放大包含两方面:

  1. SSD 本身的写放大:SSD 不能直接覆盖已有的数据,需要先将其擦除,再写入,写入的粒度为 page,但是擦除的粒度是 block,一个 block 包含多个 page;同时 SSD 的垃圾回收也会造成数据移动和擦除;最后 SSDWear Leveling 特性会保证各个 memory cell 均衡的写入,也引入了数据移动
  2. 数据库软件带来的写放大

在这两个因素下有时候写放大能达到100倍。

Leveled Compaction 的写放大倍数基本在10到30,在大多数情况能够数倍优于 B 树的实现。更进一步,Tiered Compaction 能将写放大倍数降至4到10,不过缺点是读性能会有一定的下降。一般来说,当应用的写负载较高时,可以配合写放大较低的合并策略,而当写负载不高时,则可以采用更激进的合并策略,从而有更好的空间效率和读性能。

空间放大

经过了多年的开发后,RocksDB 团队认为对于大多数应用来说,空间使用率远比写放大重要,因为这些场景下还没有触及 SSD 本身的限制,不恰当的比喻来说就是:

以大多数应用程序的稳定性来说,还远没有到比拼不同的操作系统稳定性的地步。

而实际上,应用本身也没有充分利用 SSD 提供的读写吞吐,因此这一阶段的优化重心就转移到了磁盘空间上。

由于 LSM 树无碎片的数据组织方式,天然的避免了由于数据碎片带来的磁盘空间浪费。另一方面,RocksDB 也引入了新的合并策略 Dynamic Leveled Compaction,其中 LSM 树每一层的大小上限会动态的根据最深层文件的大小调整,而不是固定值。这么做的原因是为了减少 LSM 树中无效的数据(已删除和已被覆盖),而和最深层文件大小的比值则可作为有多少无效数据的度量指标。最终的结果也表明相比于 Leveled CompactionDynamic Leveled Compaction 有着更稳定的空间效率。

CPU 利用率

随着 SSD 的发展,一种潜在的担忧是应用程序已不能完全充分利用 SSD 的潜能。因此,系统的瓶颈也从设备 I/O 转移到了 CPU。不过,RocksDB 的开发人员不这么看,因为:

  1. 只有少部分的应用受限于 SSDIOPS,大部分应用受限于磁盘空间
  2. 一个高端 CPU 足够服务于一个高端 SSD。在 Facebook 的生产环境中还没有遇到 RocksDB 不能充分利用 SSD 能力的情况。当然,如果一个 CPU 配备多个 SSD 还是有可能会有 CPU 瓶颈的,不过这属于系统配置层面的资源不均衡问题。另一方面,写密集型的应用也有可能存在 CPU 瓶颈的问题,不过这可以通过使用更轻量级的合并策略解决。而在这之外的场景,其工作负载则可能不适合使用 SSD,因为有可能提前让 SSD 的寿命完结

不过,优化 CPU 利用率也不等于说是无用功,因为空间放大的优化余地已经不多了。优化了 CPU 也等同于省钱,毕竟 CPU 和内存的价格也在节节攀升。一些针对 RocksDBCPU 优化的尝试包括前缀布隆过滤器,在查找索引前先用布隆过滤器判断,以及其他的一些布隆过滤器优化。

适配新技术

一些 SSD 的新技术例如 open-channel SSDsmulti-stream SSDsZNS 能让 SSD 有着更低的查询延迟以及更少的擦除周期损耗。不过,如前面所述,RocksDB 的开发团队认为大部分应用的瓶颈在于磁盘空间,适配这些新技术反而会给 RocksDB 的一致性体验带来挑战,所以这项的优先级不高。

In-storage computing 可能会给应用带来巨大的提升,不过 RocksDB 的开发团队目前还不确定 RocksDB 能从这项技术中受益多少,而且对 API 的改动可能也比较大。

Disaggregated (remote) storage 则更具吸引力,并且也是当前的一个优化重点。前文的优化背景都是应用直接访问本地 SSD,不过,如今更快的网络带宽使得远程访问 SSD 成为了可能,因此,如何优化 RocksDB 使其更好的适配远程 SSD 也变得有意义。在远程存储模式下,CPUSSD 资源可以同时做到充分利用以及独立扩展,相反本地 SSD 的模式则较难实现。目前 RocksDB 的开发团队正在优化远程模式下的 I/O 延迟。

最后,non-volatile memory (NVM) (它相比于 SSD 有着更高的 IO 读写吞吐)这项技术也在考量中:

  1. NVM 作为 DRAM 的扩展
    1. 如何实现核心数据结构(block cache 还是 MemTable)从而结合 NVMDRAM 一起使用
    2. 会引入哪些额外的开销
  2. NVM 作为数据库的主要存储:不过实践表明 RocksDB 的瓶颈主要在于磁盘空间或者 CPU,而不是 I/O
  3. NVM 保存 WAL:其成本是否值得有待考虑,毕竟 WAL 中的数据量不大,并且会刷盘到 SSD

再次审视 RocksDB 使用 LSM 树的合理性

LSM 树依然是最适合的,因为 SSD 还没有到白菜价的地步,对于大多数应用来说,其有限的寿命依然是无法忽略的因素。而另一方面,RocksDB 的开发团队也发现某些写密集型的应用会写大量的大对象,如果能分别存储键值对则能减少 SSD 的写入,其功能实现为 BlobDB

运行大规模系统的经验总结

资源管理

大规模分布式数据服务往往会将数据以 shard 的粒度分区到多个节点上,一个节点可能会持有几十上百个 shard。不过 shard 的大小有限,因为 shard 是负载均衡和副本的最小单位,需要在各节点之间进行拷贝。在 Facebook 的环境内,一个 shard 由一个 RocksDB 实例提供服务,因此一个节点会运行很多 RocksDB 实例,它们可能会共享一个地址空间,也有可能会独享。

在上述背景下,就需要考虑如何进行资源管理,包括:

  1. 分配给 write bufferMemTableblock cache 的内存
  2. 合并程序占用的 I/O 带宽
  3. 合并程序线程数
  4. 磁盘使用量
  5. 文件删除速率

资源管理包括两个维度,全局(分配给每个节点的资源)和局部(分配给每个 RocksDB 实例的资源)。对后者来说,RocksDB 允许应用程序创建 resource controller (以 C++ 对象实现并传递给多个 RocksDB 实例)来对上述提到的资源进行分配。例如,一个实现了对合并程序占用的 I/O 带宽限流的 C++ 对象可以传递给多个 RocksDB 实例,从而保证任一时刻所有 RocksDB 实例的合并程序占用的 I/O 带宽之和不会超过指定值。另外,资源管理需要能够支持按优先级分配,使得最迫切需要资源的实例能够优先获取资源。

另一个在一个进程内运行多个 RocksDB 实例的经验总结是将各实例中执行相似任务的线程统一以一个线程池进行管理,而不是每个实例各自维护线程池。这些线程执行的往往是后台任务,统一了线程池也变相的限制了后台任务执行时占用的 I/O,使得资源使用更具预测性。独立维护线程池的情况下有可能会有瞬时的 CPU 或者 I/O 毛刺,造成服务不稳定。不过,有得则有失,共享线程池的缺点就在于某些实例有可能无法及时的获取线程,从而阻塞后台任务,例如无法及时执行 SSTable 的合并,甚至造成写停顿(write stall)。

相比而言,当不同的 RocksDB 实例运行在多个进程时,全局的资源管理则更具有挑战性,毕竟各进程之间没有信息交互。文中提出了两种策略:

  1. 为每个 RocksDB 实例配置较为保守的资源额度,缺点就是全局资源利用率不一定最优
  2. 各进程间交换资源使用的情况,从而动态调整资源配比

支持副本和备份

RocksDB 本身不提供开箱即用的副本和备份的支持,需要应用自行实现,不过 RocksDB 为实现这两个功能提供了必要的支持。

副本

从一个节点复制出一个全新的副本节点有两种方式:

  1. 逻辑复制(logical copying):遍历源节点的所有键值对,然后写入到目标节点。在源节点端,借助 RocksDB 的快照功能保证了数据的读一致性。同时,RocksDB 支持 scan 操作从而在数据复制时减少对在线查询的影响。在目标节点端,RocksDB 提供了 bulk loading 的功能来批量加载数据
  2. 物理复制(physical copying):直接复制 SSTable 和其他辅助文件到目标节点。RocksDB 在复制时会确保没有文件被修改或删除

备份

备份对于数据库来说至关重要,和副本复制一样,备份的实现同样有逻辑备份和物理备份两种。副本和备份的其中一个区别在于上层应用经常会需要同时管理多个备份。RocksDB 也内置了一个备份引擎针对简易的备份场景。

更新副本面临的挑战

在多副本场景下,如何将主节点的更新以一致的顺序同步到各个副本是一个挑战。直白的做法是依次按序向各个副本写入,当然缺点就是性能很差,无法利用多线程。另外,当某个副本停止同步很久之后,需要有相应的机制能让其快速同步至最新的状态。

而无序写的问题在于读取时有可能数据不一致,一种解决方法是引入快照读,客户端读取时指定序列号,RocksDB 会返回执行快照时对应时间点的数据,而不会受当前正在进行中的写入的影响。

WAL 处理

传统的数据库一般要求每次写入前先写 write-ahead-log (WAL) 来保证数据的持久性。相反,大型分布式存储系统一般使用多副本来提升性能和可用性,例如,如果某个副本的数据损坏或者无法访问,那么系统可以基于其他完好的副本重新构建损坏的副本。对于这些系统来说,WAL 就不是那么重要。另外,分布式系统一般也有自己的一致性协议日志(如 Paxos 协议),这种情况下 WAL 就可以不需要了。

因此,RocksDB 需要能够针对不同的场景灵活配置 WALRocksDB 提供了三种选项:

  1. 同步刷盘写 WAL
  2. 先将 WAL 写入到缓冲区,然后定期由后台低优先级线程刷盘
  3. WAL

数据格式兼容性

大型分布式应用往往运行在诸多节点上,并且最好不发生服务中断。因此,软件更新往往是逐台(或者小批量同时)发布,出现问题时再回滚。因此,RocksDB 需要能够保证存储在磁盘上的数据能够后向和前向兼容。另外,出于副本构建或者负载均衡的需要,系统会在各节点之间复制数据,因此整个集群可能运行着多个版本格式的数据。

对于后向兼容来说,RocksDB 需要能够识别之前的所有数据格式,这无疑增加了实现了维护的复杂度。对于前向兼容来说,RocksDB 需要能识别新的数据格式,并且至少要支持一年的前向兼容,这方面的技术手段借助于 Protocol Buffer 或者 Thrift。对于配置项的兼容性来说,RocksDB 需要能够识别未知的配置,并尽最大可能尝试猜测配置的含义或者忽视。

错误处理的经验总结

RocksDB 的开发团队通过产线的实践总结了三条关于错误处理的经验:

  1. 数据损坏越早监测到越好,从而最低程度的避免数据不可用或丢失,同时也能精确定位数据损坏的源头。RocksDB 通过在系统各层级计算数据的校验和并在数据传输时验证校验和来识别数据是否损坏
  2. 完整性保护必须覆盖整个系统,从而避免由于静默的硬件数据损坏传递给 RocksDB 客户端或者其他副本。仅仅在数据未使用或者传输时检测是不够的,因为数据损坏有可能由异常的软件,异常的 CPU 或者其他异常的硬件引入。不过,即使基础设施一直扫描系统中是否有异常的硬件,某些硬件异常也不一定能够被发现
  3. 错误需要能够区别对待。RocksDB 的开发团队最开始将所有非 EINTR (系统调用中断)类型的文件系统错误统一处理。如果错误发生在读取操作,那么 RocksDB 直接将错误传递给客户端。如果错误发生在写操作,那么 RocksDB 认为这是一个不可恢复的错误,然后永久中断所有的写入;RocksDB 需要重启才能恢复写入,并且可能还需要额外的运维操作。为了减少这种粗暴的重启,RocksDB 的开发团队开始对错误按照严重性分门别类,并且只有在遇到确实是不可恢复的错误时才中断操作

静默损坏的频率

在真实的 RocksDB 使用场景中,多久会发生一次静默的数据损坏?这很难直接给出答案。出于成本的考虑,应用所使用的存储设备一般不提供端到端的数据保护,相反,应用依赖 RocksDB 提供的块校验和来检测数据损坏。另一方面,基于 RocksDB 的应用本身也会运行数据校验程序来对比副本间的数据,不过这个过程识别出的数据损坏既有可能是 RocksDB 引入的,也有可能是应用本身引入的。

通过比较 MyRocks 中主键和二级索引的使用情况,RocksDB 的开发团队推断出每 100 PB 数据在每三个月内会发生一次由 RocksDB 本身引起的数据损坏。其中40%的情况下,这些数据损坏已经扩散到了其他副本上。

另一方面,数据损坏也有可能发生在数据传输中,这经常是由于软件 bug 导致。例如,底层存储系统在处理网络异常时的一个 bug 会导致一段时间后,每传输 1 PB 数据大约有17个校验和不匹配。

多级保护

数据损坏需要尽早识别,以免扩大影响范围,并尽可能的减少服务中断时间和数据丢失。大多数的 RocksDB 应用会持有一份数据的多个副本,并定期检测副本的校验和来识别损坏的副本,一旦发现损坏的副本,应用就可以丢弃该副本并替换为正确的备份。不过,这种做法的前提是系统中始终持有有效数据的副本。

如下图所示,RocksDB 启用了多级校验和保护,从而能尽早的发现数据损坏。

alt

块完整性

块校验和继承自 LevelDB,是为了避免文件系统层的数据损坏传递到客户端。这里的块不仅仅指 SSTable 块,也包括了 WAL 段(fragment),在块生成时会同时生成校验和。每当一个块被读取时,RocksDB 都会检验它的校验和。

SSTable 完整性

每个 SSTable 文件也保存了一个校验和,该功能在2020年引入,是为了避免 SSTable 在传输时造成损坏,校验和会在生成 SSTable 时同时生成,并保存在 SSTable 的元数据中,RocksDB 会在传输 SSTable 时检验校验和。不过,这篇文章发表时,还没有 WAL 文件级别的校验和。

Handoff 完整性

在往文件系统写入数据前,会同时生成一个 handoff 校验和,然后将数据和校验和一起传递给下一层,由下一层进行数据校验。RocksDB 的开发团队期望用这种方式对 WAL 进行校验,因为 WAL 都是增量的追加写,不过可惜的是,很少有本地文件系统支持这种校验方式。不过,当 RocksDB 结合远程存储使用时,可以修改 write 接口使其接收额外的校验和,然后将其添加到存储服务内部的 ECCError Correction Code,用于校验数据完整性)中,最后远程存储服务在收到写请求时就可以进行校验。

端到端的键值对完整性保护

上述的完整性校验依然存在不足,其中一个不足在于文件系统之外的数据没有完整性保护,例如 MemTableblock cache 中的数据。因此,在这一层的数据损坏就无法被监测并有可能最终扩散到上层应用。而如果此时发生了 MemTable 的刷盘或者合并操作,则会将损坏的数据永久的持久化到磁盘上。

因此,RocksDB 的开发团队的解决方案是实现每个键值对级别的校验和,从而在文件系统层之外发现数据损坏。当某个键值对被复制时,其校验和也会随之复制,不过在写入到文件时这部分校验和会忽略,因为在文件系统级别已经有其他校验和机制来保证完整性了,从而减少数据冗余。

基于严重性的错误处理

大部分情况下,RocksDB 遇到的故障都是底层存储系统返回的错误。这些错误可能来自于各种各样的问题,从比较严重的问题例如文件系统变成了只读,到短暂的问题例如磁盘空间满了或者访问远程存储时网络异常。在早些时候,如果是读操作时发生的错误,RocksDB 就简单的将错误信息返回给客户端,而如果是写操作时发生的错误,RocksDB 则会永久性的暂停所有写操作。

而优化后 RocksDB 仅在遇到无法本地恢复的错误时才中断操作,例如暂时的网络错误不应该要求重启 RocksDB 实例。对于暂时性的错误,RocksDB 会周期性的重试。

配置管理和可定制化的经验总结

配置管理

一开始,RocksDB 的配置管理继承自 LevelDB,所有的配置都写死在代码中。这带来两个问题:

  1. 某些配置和保存的数据强相关,因此,由某项配置生成的数据文件可能无法由其他配置的 RocksDB 实例打开
  2. 没有在代码中声明的配置会采用默认值,一旦 RocksDB 版本更新并修改了某些配置的默认值,上层应用可能会遇到不可预知的问题

为了解决配置的问题,RocksDB 可以在打开某个数据库的同时额外接受某些参数配置,之后 RocksDB 又支持将配置持久化到文件中。RocksDB 也提供了额外的两个辅助工具:

  1. 验证配置参数是否和要打开的数据库兼容
  2. 将数据库按照期望的参数配置进行迁移(不过存在使用限制)

配置管理的另一个严峻的问题就是配置项太多了,用户很难知道每个配置参数的影响,进而不知道如何根据自身应用找到最优的配置。但是又很难找到一套放之四海皆准的默认配置,因为每个应用的使用场景,工作负载都不同。另一方面,对于集成了 RocksDB 的应用,例如 MySQL,数据库管理员可能对 RocksDB 了解不多也不知道如何优化。

在这个背景下,RocksDB 的开发团队花费了大量时间去优化默认配置下 RocksDB 的性能以及简化配置。同时,当前的重点在于提供配置的自适应性(automatic adaptivity),另一方面也持续提供 RocksDB 可自定义配置的能力,从而能适配不同类型的应用。同时做到这两方面会显著的增加代码维护的负担,不过一个统一的存储引擎的重要性大于代码的复杂度。

回调函数的威力

RocksDB 需要周期性的合并底层的 LSM 树来清理已删除和过期的数据,如果能在合并时为应用提供额外的接口则能方便的为应用做功能扩展,而不需要额外的标准读写操作。因此,RocksDB 提供了两个在合并时的回调方法 compaction filtermerge operator

Compaction Filter

在合并时,RocksDB 提供了执行合并时针对每个被处理的键值对的回调函数,应用可以自行决定:

  1. 丢弃这个键值对
  2. 修改值
  3. 不做任何修改

一个典型的应用场景是实现 time-to-live (TTL),每个键值对在写入时保存了过期时间,然后在合并期间判断是否过期从而删除数据。另一个应用场景是实现 multi-version concurrency control (MVCC) 中的垃圾回收。另外,compaction filter 也可以用于修改数据,例如,从旧数据格式迁移到新的数据格式,或者根据时间来修改数据。最后,compaction filter 有时候也可以用来收集统计信息。

compaction filter 也非常适合需要扫描全部数据的管理任务,虽然也可以遍历整个数据集然后通过 delete()put() 操作,但是使用 compaction filter 更高效且使用更少的 I/O 操作。借助 compaction filter,用户无需额外维护定时任务,也不用担心由自定义实现可能造成的写入毛刺。

不过,compaction filter 在使用上也有些限制。例如,错误的使用 compaction filter 可能会破坏基本的数据一致性保证,多次快照读也可能返回不一致的结果(如果数据在两次读之间发生了修改)。因此,compaction filter 在不要求一致性的场景下更容易使用。另一个限制是 compaction filter 无法原子的丢弃或者修改一批键值对,例如,无法原子的删除一个键值对并丢弃相应的二级索引中的数据。

Merge Operator

RocksDB 原生支持三类操作:put()delete(),和 merge()。每一个操作都会写入到相应的 MemTable,然后刷盘到 SSTablemerge() 方法使得应用不需要先读取键就能更新键的值,也不需要写入完整的键的内容。在随后的读操作或者合并操作时,如果 RocksDB 遇到了一个 merge record 以及之前调用 put() 写入的记录,或者是多个 merge recordRocksDB 会调用 merge operator 回调函数,应用可以将这些记录合并成一个,既可以是一个 put record,也可以是一个 merge record

merge operator 的一个显著的应用是实现 read-modify-write 操作,例如实现一个计数器或者更新某个复杂对象中的单个字段。相比于用 get()put() 整个键值对来实现,用 merge operator 来实现则更为轻量。不过,这会影响读的性能,因为找到一个 merge record 不代表查询结束,最坏的情况可能需要遍历 LSM 树的所有层,或者直到找到一个 put record 为止(更频繁的合并能缓解这个影响)。

优化删除

删除往往是 LSM 树中被忽略的一个操作。RocksDB 中无法直接删除键值对,删除操作本质上是插入一条标记删除的记录,这使得删除操作很快,但后续对该键的查询有可能变慢。在执行合并操作时,如果遇到标记删除的键值对,并不能直接将其物理删除,因为无法保证该键值对是否还存在于更深层的 SSTable 中。因此,RocksDB 针对删除场景也做了一些优化。

假设应用对同一个键依次执行了三次操作:put()delete()put(),前两个操作属于 MemTable1,后一个操作属于 MemTable2,之后刷盘成 SSTable1SSTable2。因为合并操作只是选取一部分 SSTable,所以有可能 SSTable2 先合并到了更深层。

支持对大范围标记删除的数据范围扫描

应用经常会大批量删除连续或者临近的键,在这种场景下,调用 scan() 遍历每个键时就会遇到一堆已被标记删除的数据需要被跳过,从而浪费 CPUI/O 资源。例如,某个应用可能用 RocksDB 保存文件系统中每个文件的绝对路径,而如果删除了文件夹则会导致一大批键被删除。再例如,使用 RocksDB 模拟队列时,每个出队的元素都会被删除,那么队首的元素则天然的挨着一批被删除的元素。遍历这些被删除的键一方面加重了资源负担,另一方面对查询结果也没有影响。在极端情况下,RocksDB 的开发团队在实践中遇到扫描了几百万个标记删除的键,最终只为了返回几个键值对。

一种解决思路是当出现大量连续标记删除的键时,触发合并操作。RocksDB 提供了几个功能:

  1. 当标记删除的键占所有键之比超过50%时,合并会更积极的发生,并且随着标记删除的键占比增加而更频繁。不过,不能很好的处理标记删除的键占比不超过50%的情况
  2. 允许应用自己标记哪些 SSTable 需要执行合并。在执行合并生成新的 SSTable 时,RocksDB 提供了插件机制能够访问每个被处理的键值对,当新的 SSTable 创建后,RocksDB 会调用该插件从而判断是否需要将该 SSTable 放入下次的合并操作中。RocksDB 的统计信息中也包含了一次查询涉及了多少个标记删除的键,从而辅助应用更好的判断是否需要发起合并
  3. 执行 scan() 操作时,如果遇到了指定数量的标记删除的键,则提前中止遍历。当然,这样做的结果就是返回的数据不全,不过应用就能知道遇到了大量被标记删除的键,需要应用自行决定是否需要继续扫描还是放弃

上述措施一定程度上能缓解前述的问题,不过依然有局限性:

  • 合并需要时间,在这期间 scan() 的性能依然受限
  • 更频繁的合并意味着更大的写放大,这对于某些应用来说是不可接受的

目前,这方面的优化工作仍然在进行中。

回收磁盘空间

一般来说,如果数据被删除了,那么其所占用的磁盘空间也应当被释放。不过在 RocksDB 中数据不是立即删除,需要等待一段时间,而应用可能会要求在指定的时间内就需要释放磁盘空间。因此,RocksDB 提供了一个功能保证在指定时间内所有被标记删除的键都会移动到 LSM 树的最后一层,那么这些数据在随后的合并中就可以被清理。RocksDB 通过在 SSTable 的元数据中维护每个键首次添加到系统中的时间来实现该功能。

文件删除限流

RocksDB 一般构建于能够感知 SSDflash-SSD-aware)的文件系统之上,当某个文件被删除时,它会发送一个 TRIM 命令给 SSDTRIM 的性能较好且对于 SSD 的寿命影响较小。不过,它可能会造成其他的性能问题:除了更新地址映射(大多数位于 SSD 的内部内存中)之外,SSD 固件还需要将这些变更作为 FTL 日志写到闪存上,这又会触发 SSD 内部的垃圾回收,从而造成大量的数据迁移,并最终影响上层应用的 I/O 延迟。所以,RocksDB 增加了文件删除的限流来控制同一时刻删除的文件个数。

内存管理

RocksDB 对于内存的使用主要在于 SSTableblock cache 以及保存 MemTable。相比于其他数据库自己维护缓冲池,RocksDB 则依托于 jemalloc 进行内存分配。

尽管块的大小是可配置的,RocksDB 的实际实现则是采用变长的块,不过其大小会尽可能的接近所配置的值。例如,如果某个键值对的大小超过了指定的块大小,那么 RocksDB 会为其创建较大的块。类似的,如果某个块中已经存在一部分键值对,而此时再放入一个键值对就会超过块的大小时,则该键值对不会被放入该块中,并且 RocksDB 会选择一个较小的块来存放原来的那批键值对。另外,SSTable 的索引块和布隆过滤器的块大小也没有采用固定大小。出于这么做的原因是因为 RocksDB 采用的数据结构不支持就地更新,采用固定大小的块收益不大。

不过,在实践中,借助 jemalloc 来管理内存在分配和回收时都存在不可忽视的开销,其外部的内存碎片和元数据带来的额外内存开销也值得应用注意。这种情况下应用开发者可以选择换一个内存分配器,或者对 jemalloc 进行调优。

另外,RocksDB 的用户也经常对如何高效的调优内存参数感到迷茫。RocksDB 能够精确限制 block cacheMemTable 的内存参数,但是对 jemalloc 的外部内存碎片和元数据无法掌控,所以用户需要自行判断应该给这部分预留多少内存。因此,实验是检验真理的唯一标准。

尽管如此,RocksDB 的开发团队认为使用 jemalloc 仍然是一个合理的决定,因为可以将精力放到其他更重要的方面上。不过未来可能也会将这个内存管理问题提上日程。

Key-Value 接口设计的经验教训

RocksDB 的核心接口就四个:

  • put()
  • delete()
  • get()
  • iterators (scans)

很少有应用无法基于这四个接口实现需要的功能,KV 接口的键和值都是变长的字节数组,因此应用程序可以很自由的存储想要的数据,只需要做好序列化和反序列化。另外一个好处是可移植性,应用可以轻易的从一个 KV 系统迁移到另一个。

不过,天下没有完美的事物,部分应用的性能反而会受限于这精简的接口。例如,在 RocksDB 之外处理并发控制就很难做的高效,尤其是两阶段提交场景下需要在事务提交前先持久化一部分数据的场景。因此,RocksDB 增加了事务的功能,并持续添加新的功能,例如对某个范围内的数据加锁,以及支持大事务。

在其他场景下,应用则受限于过于精简的接口,为此 RocksDB 增加了两项扩展:

  • 由应用定义的时间戳
  • 列支持

版本和时间戳

为了支持诸如 multi-version concurrency control (MVCC) 和从历史某个时间点读取(point-in-time reads)的功能,RocksDB 需要能够支持数据的版本管理,并能高效的访问各个版本。

目前,RocksDB 内部使用一个56位长度的序列号来标识键值对的每个版本。客户端的每一次写请求都会对版本号加1,不过客户端无法直接修改这个版本号。RocksDB 允许应用对其执行快照,RocksDB 保证只要这个快照没有被应用释放,那么在这个快照执行的时间点时的数据就都能始终被访问。

不过,对很多应用来说这依然不够,为了读取历史上的数据,前提是应用必须先曾经做过快照,RocksDB 不支持在当前时间对历史的某个时间点执行快照,因为根本没有这样的接口。另外,RocksDB 的版本号是每个实例各自维护,快照也是各实例粒度。因此,对于多 shard 的应用来说,很难对所有节点同时做一致的快照。

虽然应用可以将时间戳写入到键或者值中,不过这会影响应用的性能。如果将时间戳写入到键中,则点查询的性能会很差,因为实际保存的键和用户查询的键已经不同,需要做前缀扫描遍历;如果将时间戳写入到值中,则会影响对同一个键乱序写入的性能,因为乱序写入时如果不考虑相互间的时间戳顺序则有可能发生数据覆盖,并且读取旧版本的数据也变得复杂。因此,RocksDB 需要提供在键值之外由应用自行指定时间戳的能力。

经过实验,由应用指定时间戳的情况下,RocksDB 相比于将时间戳写入到键的方案有1.2倍的吞吐提升。提升的原因在于:

  • 时间戳是键值对元数据的一部分,因此点查询依然高效
  • 布隆过滤器可以继续发挥作用
  • 每个 SSTable 在元数据中同时也维护了所有键所覆盖的时间戳范围,因此在搜索时有可能直接忽略整个 SSTable

当然缺点就是磁盘使用空间会变大以及移植性变差。

列支持

一些基于 SQL 的数据库实现会以列的形式组织 RocksDB 的数据,虽然应用可以将数据库中一整行的数据以单条 KV 的形式保存在 RocksDB,但是如果能直接在 RocksDB 层面支持列则对应用的性能提升有很大帮助。

假设有些大对象的某些列更新非常频繁,那么在整行数据保存的方案下更新就非常不高效。如果支持列,则只需要更新部分列。另外,如果数据库的某个查询也只涉及部分列,那么也不必读取完整的一行数据。

某些应用已经尝试对上述的问题进行优化。例如 Rocksandra 借助 merge operator 来进行部分列的更新,不过代价就是读性能较差,因为需要读完所有的 merge record 或者遇到一个 put record 才能知道最终的结果。另一种方案是将一行数据的每一列保存为一个键值对,缺点在于:

  1. 读取一行数据需要进行范围扫描(比如所有的列数据的键都以主键为前缀)
  2. 删除和更新一整行数据变得困难

因此,如果能在 RocksDB 层面直接支持列,则能大大提高应用的性能:

  1. 更新和读取单列的数据变得高效
  2. 当应用发起针对某些列的过滤查询时,某些过滤条件可以下推到 SSTable
  3. 某些列可以用不同配置的 column family 保存
  4. 可以像列数据库一样高效压缩保存列数据

另外,支持列也能够让 RocksDB 在主键索引和二级索引间校验数据完整性。

来自失败的提案的经验总结

一路走来,RocksDB 实现了很多的功能,其中也有些失败的案例。

支持基于 DRAM 的存储设备

在2014年,RocksDB 的开发团队决定将 RocksDB 适配到 RamfsRAM File System)上,从而有比 SSD 更低的访问延迟。为此,RocksDBSSTableMemTable 的格式改为插件式,从而针对 Ramfs 进行了特定的优化。

这个结果本身是成功的并且也应用到了某些服务上。不过,在战略上来说这个功能提的太早了。对于大型纯内存式的持久化存储系统来说,RocksDB 的这套方案并未像预期的那样获得关注。而对于内存式的应用来说,一般也不会考虑集成 RocksDB,因为完全直接自己操作内存来的更快和便捷。

支持混合式存储设备

SSDHDD 更快,不过也更贵而且寿命也有限。因此,如果能结合 SSDHDD 一起使用,那么对于大多数的应用来说可以在性能和成本之间做到更好的平衡。同样是在2014年,RocksDB 支持能将 LSM 树的不同层保存到不同的存储设备上。

不过,在该功能推出的时候用户并没有买账。另一方面,在实践中将 SSDHDD 同时配置到一个节点上的情况也比较罕见,RocksDB 的开发团队认为混合存储方案的潮流应该对远程存储服务更有吸引力,而这要到2018年才开始着手。不过,最近RocksDB 的开发团队又重拾了混合式存储项目,因为要支持混合式的本地/远程存储服务,不过依然还有额外的工作要做。

更丰富,高层次的接口

RocksDB 最开始支持的是传统的 KV 接口,不过,在过去的几年中也尝试过扩展支持更为丰富的接口,从而更方便某些应用使用。例如,在2013年 RocksDB 添加了类似于 Redislists 的接口;在2014年添加了2个与空间相关的接口;在2015年支持了文档类型的数据。所有这些接口都基于核心的 KV 接口实现。

但是,这些接口同样没有被广泛采纳,并最终废弃和移除。因此,对于 RocksDB 的开发团队来说,将精力放在核心功能上更有价值。大部分 RocksDB 的用户都能借助简单的 KV 接口构建更为丰富的上层接口,而不需要由 RocksDB 来提供。用户的首要痛点在于效率和易管理性。总结来说,扩展核心接口的前提在于能够显著提升性能。

附录

经验总结

  1. 对于存储引擎来说,能够调优适配不同的工作场景至关重要
  2. 大多数使用 SSD 的应用的瓶颈在于空间效率
  3. 降低 CPU 开销对系统的高效运行越来越重要
  4. 如果一台机器上运行了多个 RocksDB 的实例,那么全局的资源管理就是必须的
  5. WAL 的可配置性(同步刷盘写 WAL;先将 WAL 写入到缓冲区,然后定期刷盘;无 WAL)能够给上层应用带来性能提升
  6. 需要正确的支持数据副本和备份
  7. RocksDB 需要对数据和配置文件提供后向和前向兼容
  8. 越早识别数据损坏越好,而不是在最后检测
  9. 完整性保护必须能够覆盖整个系统来避免数据损坏(例如,由 CPU 或内存引起的 bitflip)扩散给客户端或者其他副本;只在数据空闲时或者传输时进行损坏检测是不够的
  10. 错误处理需要能够根据类别和严重性分别处理
  11. 即使概率很低,CPU 和内存造成数据损坏也有可能发生,因此数据副本并不一定总是能解决这种情况
  12. 自适应的配置对简化配置管理大有益处
  13. 可以通过用户自定的回调函数来提升性能,不过当前的技术方案仍有进步的空间
  14. LSM 树中删除连续的键会带来性能问题
  15. SSDTRIM 对性能大有帮助,不过需要对文件删除作限流来避免偶发的性能问题
  16. 借助第三方内存分配器来管理内存使得开发团队能将精力放在其他重要的功能上,不过缺点是带来了可管理性问题
  17. 目前的 KV 接口已足够有用,不过对于某些应用场景来说可能会有性能问题;在键值之外添加时间戳能够在性能和简洁性上达到较好的平衡
  18. 应用可以在 RocksDB 提供的 KV 接口之上实现列数据并且有着较好的性能,不过如果存储引擎本身支持列则会有更好的性能

设计抉择回顾

  1. 可定制化对用户始终有用:结果就是用户迷失在大量的配置里,而且很难找到最优的配置
  2. RocksDB 无法感知 CPUbitflip:完整性保护需要端到端覆盖
  3. 遇到任何 I/O 错误时可以直接中断操作:过于粗暴,所以需要根据错误的类别和严重性做不同的处理

参考

介绍

本文提出了一种高效阅读文献的方式,相比于一上来就从头读到尾,作者将其拆解为三趟式阅读:

  1. 第一遍了解论文讲什么,解决了什么问题,提出了什么方法
  2. 第二遍理解论文的内容,但忽略细节
  3. 第三遍深入理解论文

第一遍阅读

第一遍阅读建议控制在5到10分钟内,阅读内容包括:

  1. 仔细阅读标题,摘要和简介部分
  2. 阅读每一节和每小节的标题,但忽略内容
  3. 阅读结论部分
  4. 扫一遍论文引用,并标记哪些已经读过

第一遍读完后,你应该能够回答5个问题:

  1. Category:这是一篇什么类型的论文?
  2. Context:有哪些其他相关的论文?
  3. Correctness:论文中的假设对吗?
  4. Contributions:这篇论文的主要贡献是什么?
  5. Clarity:这篇论文写的清晰易懂吗?

当回答了这5个问题后,你就可以决定是否要继续读下去,不继续读的原因可能是因为这篇论文对你价值不大,也可能是因为你还没有足够的知识储备来理解,甚至是论文中描述的假设都是错的。

第二遍阅读

第二遍开始仔细阅读论文,但忽略细节比如证明环节:

  1. 仔细阅读论文中的图表,尤其是图片。一些图表中的常见错误可以提前让你甄别出不严谨甚至是粗制滥造的论文
  2. 标记还没有读过的相关论文引用以便之后阅读,这有助于更好理解该篇论文的背景

第二遍阅读应该控制在1小时以内,通过第二遍阅读,你应该能够理解论文的内容,并能够自我总结论文的要旨给第三者。这个层次的掌握程度对于阅读感兴趣的论文已经足够,但用于科研工作还不够。

如果第二遍读完还不能理解怎么办?这有可能是因为论文的主题对你来说是一个新事物,也可能是因为论文的作者采用的证明让你摸不着头脑,或者这篇论文就是写的晦涩难懂,甚至是因为夜深了。你可以选择:

  • 把论文放一边,即使不理解这篇论文也不影响你事业的成功
  • 之后再看,先补充点背景材料
  • 硬着头皮开始第三遍阅读

第三遍阅读

第三遍阅读能让你真正的理解这篇论文。这次阅读的关键是假设自己是论文的作者,并且基于原作者的假设,重新构建论文。通过你所构建的论文和原论文对比,你就能轻易的发现原论文的创新点,以及潜在隐藏的缺点和假设。

第三遍阅读需要高度关注细节,并时刻本着以怀疑的态度看待论文中的每一个假设。通过这次阅读,也能够为你之后的科研工作提供一些想法。

对于新手来说第三遍阅读大概要花4到5个小时,而对于有经验的读者来说只需要1小时。

参考

介绍

Bitcask 是一个单机 KV 存储引擎,项目起因于 Riak 分布式 KV 数据库需要一个能满足以下条件的单机 KV 存储引擎:

  • 低延迟的单条读写
  • 高吞吐,尤其是面对流式随机 KV 写入
  • 能支持远比内存大的数据量
  • 能从崩溃中快速恢复以及不丢失数据
  • 能轻松的备份和还原数据
  • 相对简单,易理解的代码结构和数据格式
  • 在高负载和大数据场景下系统的行为是可预期的
  • 软件的许可证要能轻易的适配 Riak 使用

作者看了一圈发现市面上还没有一款 KV 存储能全部满足这些条件,因此 Bitcask 就应运而生。

API

Bitcask 的接口非常精简:

接口 描述
bitcask:open(DirectoryName, Opts) -> BitCaskHandle | {error, any()} 在指定目录下以指定选项打开或新建一个 Bitcask 实例。支持的选项包括 read_write 或者 sync_on_put
  • read_write:可读可写
  • sync_on_put:每次写操作后刷盘
连接的进程需要有 DirectoryName 对应目录的读写权限,同时一个时刻只能有一个进程以 read_write 的方式打开 Bitcask 实例。
bitcask:open(DirectoryName) -> BitCaskHandle | {error, any()} 在指定目录下以只读模式打开或新建一个 Bitcask 实例。连接的进程需要有 DirectoryName 对应目录及其内部所有文件的读权限。
bitcask:get(BitCaskHandle, Key) -> not_found | {ok, Value} 获取指定键对应的值。
bitcask:put(BitCaskHandle, Key, Value) -> ok | {error, any()} 插入一个键值对。
bitcask:delete(BitCaskHandle, Key) -> ok | {error, any()} 删除指定键。
bitcask:list_keys(BitCaskHandle) -> [Key] | {error, any()} 返回所有的键。
bitcask:fold(BitCaskHandle, Fun, Acc0) -> Acc 对每一个键值对应用 Fun 函数,Fun 的函数签名为 F(K, V, Acc0) -> Acc。类似于 JavaScriptreduce
bitcask:merge(DirectoryName) -> ok | {error, any()} 合并目录下的数据文件以减少重复的键值对。同时生成 hintfile 辅助加速程序启动时间。
bitcask:sync(BitCaskHandle) -> ok 强制刷盘。
bitcask:close(BitCaskHandle) -> ok 关闭 Bitcask 实例的连接并刷盘。

存储

文件组织

Bitcask 的文件组织非常简单,任意时刻目录下最多只有一个 active data file 接收写操作,其余都是不可修改的历史数据文件。任意时刻 Bitcask 只允许一个进程以写模式建立连接,写入进程只会向 active data file 写入,当其大小超过指定阈值后就会关闭当前文件,然后新建一个 active data file

alt

数据格式

写入进程以追加写的方式写入 active data file,从而避免了随机写的磁盘寻址,其写入数据格式如下:

alt

  • crc:循环冗余校验码,验证数据完整性
  • tstamp:32位整型本地时间戳,仅内部使用,不对外暴露
  • ksz:键的长度
  • value_sz:值的长度
  • key:键的内容
  • value:值的内容

对于每一条记录,前面四个部分都是定长,以此为基址 base,则 basebase + ksz 就是键的内容,base + kszbase + ksz + value_sz 就是值的内容。

如果要删除指定的键,Bitcask 会再次追加写入一个键值对,只不过写入的值是一个特殊值,程序后续读到这条记录时比较值的内容就可以判断该条记录是否已被删除。所以,Bitcask 每个文件内容就是一行行的记录:

alt

读写

Bitcask 写入的同时会在内存中维护写入的键到数据文件的映射(keydir):
alt

其中 file_id 能够定位具体的数据文件,value_pos 是该条记录的值在文件中的起始偏移位置,那么 value_posvalue_pos + value_sz 就是值的内容。因为 Bitcask 每次写数据的长度是可知的,值在每条记录中的偏移量可知,写之前 active data file 文件总长也可知,所以 value_pos 也能够推算出来。

对于重复写入的键值对,磁盘上会存在同一个键的多条记录,但是 keydir 中始终只保留最新的映射。

读取时先根据键查询 keydir 得到数据文件的映射,然后根据 file_id 定位数据文件,最后根据 value_posvalue_sz 返回值的内容,整个读取只涉及一次磁盘寻址。另一方面,文件系统的 read-ahead 缓存会进一步减少磁盘的交互:

alt

数据合并

由于 Bitcask 追加写的特性,有两种类型的数据是冗余的:

  • 被标记删除的数据
  • 同一个键的旧版本的数据

所以,为了避免磁盘空间的浪费,需要额外的数据合并操作对磁盘上的数据瘦身。数据合并只处理只读的数据文件,遍历剔除掉已删除和旧版本的数据。另外,每一个合并后的数据文件同时额外有一个对应的 hint file

alt

hint file 也是行记录的文件,每一行存储了:

  • tstamp:时间戳
  • ksz:键的长度
  • value_sz:值的长度
  • value_pos:值在数据文件中的起始地址偏移量
  • key:键

当程序启动时,如果 hint file 存在,那么就可以直接扫描 hint file 构建 keydir 从而加速程序启动;反之,则要扫描数据文件。

性能数据

原文并没有给出非常正规的测试报告,仅列出了一些早期未优化的测试数据:

  • 读写延迟:毫秒内
  • 写吞吐:5000~6000次/秒
  • 内存占用:几百万的键在 1GB 内(keydir 需要)

总结

Bitcask 的整体设计思路非常简单,其设计目的也不是为了成为最快的 KV 存储,而是最适合 Riak 的存储引擎,在足够快的同时有着高质量、简洁的代码,设计和数据格式。

参考

定义

自由的软件的定义四要素:

  • 以任何意愿,目的运行软件的自由
  • 学习软件运行原理的自由,并且能按自主意愿修改(前提还需要能自由访问源代码)
  • 将软件的副本再次分发给其他人的自由
  • 将修改后的软件的副本分发给其他人的自由

一个软件只有具备了上述四点才是自由的软件。

不过,自由的软件不表示不能商用,相反,GNU 鼓励并且认为商用是自由软件社区成功的重要途径。自由的软件必须能够在商业上使用,开发,以及分发。

自由与非自由的边界

以任何意愿运行软件的自由

任何个人或组织可以在任意计算机系统上,出于任意目的运行软件,而不需要事先和开发者或者其他组织联系。同时,你也可以将软件再分发给其他用户,其他用户也能自由的以他们自己的意愿去运行软件而不受你的约束。

学习软件源代码并修改的自由

自由的获取软件的源代码是能够自由修改软件和再分发的前提,不过,经过代码混淆工具处理过的代码不算是源代码。

同时,如果对修改软件有限制,那么该软件也不算是自由的软件,例如:

  • 某软件引用了修改后的软件 A,但不允许将其替换为你自己修改的版本
  • 要求你成为所修改的代码的版权拥有者
  • 只能做出其他人认为是改进的修改

软件再分发的自由

你可以自由的再分发未修改或修改后的软件副本给任何人,即使是收费。同时,你也可以自由的修改软件然后私用,而不需要让其他任何人知道;如果你将修改后的软件再分发,也不需要以任何形式知会任何人。

自由再分发的软件副本必须包含可执行文件,以及修改和未修改的源代码。不过有些软件可能(暂时)无法生成可执行的文件,但是依然要保留能够再分发可执行文件的自由。

Copyleft

版权的单词是 copyrightcopyleft 与之对应。它要求所修改和扩展的软件都必须依然保留自由软件的四要素(而不仅仅是免费),GNU 自己的项目就使用符合 copyleft 的许可证来保证软件的自由。不过,这里并不是说所有自由的软件都必须用 copyleft 许可证,不然也违背了自由一词。

软件分发的约束

只要不限制分发修改后的软件的自由,不限制私用修改后的软件,对软件分发做一定程度的约束是可接受的。例如,要求给修改后的软件换个名字,删除原有软件的 logo,或者声明修改为你所有。

出口规定

有时候政府的出口管制会限制你在国际上分发软件的自由。虽然身为开发人员无法对抗这些规定,但是你可以做也必须做的是,不添加这些法律条文作为软件的使用条件。

合规考虑

如果软件的用户没有做任何不合规的事,那么软件的开发人员不能随意的撤销软件的许可证,或添加使用限制,否则这就不是自由的软件。

基于合约的许可证

有些基于合约的许可证会引入较多的使用限制,从而可能违背自由软件的四要素,因此这类许可证不被认为是自由的许可证。例如某些免费的软件许可证会规定允许被安装的设备的数量,以及限制分享给他人使用。

软件之外

软件的使用手册也必须是自由的,因为手册也是软件的一部分。再扩展一步,自由不仅仅是用在软件上,任何能够以数字形式呈现的产物都可以是自由的。

参考

豆瓣个人评分标准:

  • 五星:一见钟情,简直停不下来,必然多周目起步
  • 四星:依然优秀,但是相比五星存在无法忽略的缺点,可能会多周目
  • 三星:没什么吸引人的地方,大部分能坚持到一周目结束,基本不会多周目
  • 二星:肉眼可见的烂,中途就弃
  • 一星:还没遇到过

以下是个人向五星里最喜欢的十款单机游戏,重剧情、代入感,轻玩法、动作,按接触时间倒序。

赛博朋克2077

一开始被蠢驴泼天的 bug 劝退了,一直没有上手。不过,看到 DLC 往日之影发售之后大受好评,而且据说2.0版本相比初版已改进不少,遂抱着试试看的心理购入,最终真香,依然吃蠢驴这套。遗憾的是支线的量不够多,意犹未尽。

上古卷轴5

2024年才接触老滚5,而且还没装 Mod,玩了200小时之后依然觉得还有200小时的内容在等待发掘。个人认为老滚5和荒野之息体现了做开放世界的两个赛道,在老滚5里我能停下来和每个人对话,探索世界的动力在于我会遇到什么样的人,会发生什么事。

荒野大镖客2

R 星的另一个代表作道德与法治5由于缺少对三个主角的代入感,加上整体枪车戏份过多,玩了一遍后就没有重拾的动力。反而比较适合大表哥2慢悠悠的世界,最终随着结尾曲响起,代入感达到顶峰,仿佛失去了一位朋友。

女神异闻录5

中二的剧情,新奇的 UI,魔性的音乐,不知不觉就过了100小时,并且回合制战斗也不显得枯燥。

巫师3

蠢驴入坑之作,虽然相比老滚5显得并不开放,但丰富的支线加上优秀的音乐让人流连忘返。

最后生还者

线性叙事的巅峰,没有一丝冗余,剧情和动作完美结合,无时无刻不在关注下一秒的走向。

空之轨迹

一首星之所在伴随至今,相比于现在注水冗余的轨迹系列,剧情优秀,人物感情细腻。

三国志11

独特的水墨画风,恰到好处的音乐,相比于即时制更喜欢回合制的操控,能够一直待在电脑里,历史感十足。

太阁立志传5

自由度满分,做一个躺平散养的人,喝茶交友,四处乱逛,没有 KPI。

最终幻想10

很难想象当初顶着看不懂的日文玩到了最后,惊艳的 CG 和音乐,共情最深的 FF。

对于整数-5除以2,在 Python 中的结果是-3,但是在 C 中是-2。如果扩展到其他几种常见的语言,可以看到和 C 一致的比较多:

语言 结果
C -2
C++ -2
Java -2
C# -2
Rust -2
Go -2
Python -3
Ruby -3

区别在于对于结果-2.5是选择向0取整还是向负无穷取整,PythonRuby 选择了后者。

对于整数 an,记 a 除以 n 的结果是 q,余数是 r,则有:a = n * q + r,其中 |r| < |n|。在数论中,r 始终是正数,但是不同的编程语言各自有不同的实现。

In number theory, the positive remainder is always chosen, but in computing, programming languages choose depending on the language and the signs of a or n.

编程语言实现

Truncated division

很多语言采用这种实现,约定 q=trunc(an)q = trunc(\frac{a}{n}),其中 trunc 表示向0取整,代表语言如 Java

Floored division

Donald Knuth 提倡这种实现,约定 q=anq = \lfloor \frac{a}{n} \rfloor,即向下取整,代表语言如 Python

Euclidean division

Raymond T. Boute 则提倡这种实现,约定:

q=sgn(n)an={anif n > 0anif n < 0q = sgn(n)\lfloor \frac{a}{|n|} \rfloor = \begin{cases} \lfloor \frac{a}{n} \rfloor & \text{if n > 0}\\ \lceil \frac{a}{n} \rceil & \text{if n < 0} \end{cases}

即根据 n 的正负号来判断是向下取整还是向上取整,代表语言如 ABAP

Rounded division

这是 Common LispIEEE 754 采用的实现,约定 q=round(an)q = round(\frac{a}{n}),其中 round 使用 rounding half to even,即在常规的取整之外,对于1.5,2.5,x.5这样的数字取整到最近的偶数,例如6.5取整到6,7.5取整到8。

Ceiling division

这是 Common Lisp 提供的另一种实现,约定 q=anq = \lceil \frac{a}{n} \rceil,即向上取整。

Python 实现

回到 Python,很难说上述哪种实现一定最优,Python 的作者提到采用 floored division 是因为对于某些应用来说,如果取模运算返回负数没有意义。例如,给定一个 POSIX timestamp,如何返回该天的时间部分,即时分秒?因为一天有86400秒,假设时间戳是 t,那么 t % 86400 就表示该天过了多少秒,就可以进一步转化为时分秒。而对于在 1970-01-01T00:00:00Z 之前的日期,t 则是负数,采用 floored division 的情况下 t % 86400 依然返回正数,并且结果也是正确的,而 truncated division 则返回负数,需要应用程序进一步处理。

不过,一种编程语言中不一定只提供一种实现,其他实现可以借助函数库。例如,Python-5 % 2 结果是1,实现方式为 floored division,但是 math.fmod(-5, 2) 结果是-1,实现方式为 truncated division

参考

介绍

SnowflakeData LoadingData Unloading 可以通过 S3 导入和导出数据。用户可以使用 AWS_KEY_IDAWS_SECRET_KEY 来授权 Snowflake 访问 S3,不过出于安全和权限控制的考虑,一般不会这么做。

Snowflake 建议通过 Storage Integration 来管理权限。

获取 VPC ID

在配置 Storage Integration 前,需要设置 S3 策略。首先获取 SnowflakeVPC ID,后续的 S3 策略配置中将只允许该 VPC 访问。

允许特定 VPC 访问的功能要求 Snowflake 实例和对应的 S3 Bucket 运行在相同的 AWS 区域内。

切换到 ACCOUNTADMIN 角色在 Snowflake 中执行:

1
2
USE ROLE ACCOUNTADMIN;
SELECT SYSTEM$GET_SNOWFLAKE_PLATFORM_INFO();

记录下返回的 VPC ID

1
{"snowflake-vpc-id":["vpc-abc"]}

创建 IAM 策略

然后,需要创建一个 S3 策略来定义 Snowflake 访问 S3 Bucket 的权限。

AWS 控制台进入 IAM,在左侧导航栏 Access management 下选择 Account settings

alt

Security Token Service (STS) 下查看所在区域的 STS 状态是否是 Active

alt

接着,在左侧导航栏 Access management 下选择 Policies,之后点击 Create policy

alt

切换到 JSON 后输入 S3 策略:

alt

下面的策略中 vpc-abcSnowflake 实例的 VPCsnowflake-storage-integration-example 是示例 Bucket 的名字,unloadingloading 是该 Bucket 下的两个文件夹,分别用于 Data UnloadingData Loading 使用:

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
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "Statement1",
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObject",
"s3:GetObjectVersion",
"s3:DeleteObject",
"s3:DeleteObjectVersion"
],
"Resource": [
"arn:aws:s3:::snowflake-storage-integration-example/unloading/*",
"arn:aws:s3:::snowflake-storage-integration-example/loading/*"
],
"Condition": {
"StringEquals": {
"aws:SourceVpc": "vpc-abc"
}
}
},
{
"Sid": "Statement2",
"Effect": "Allow",
"Action": [
"s3:ListBucket",
"s3:GetBucketLocation"
],
"Resource": [
"arn:aws:s3:::snowflake-storage-integration-example"
],
"Condition": {
"StringLike": {
"s3:prefix": [
"unloading/*",
"loading/*"
]
},
"StringEquals": {
"aws:SourceVpc": "vpc-abc"
}
}
}
]
}

创建 IAM 角色

接着,创建一个 IAM 角色并绑定前一步创建的 S3 策略。在 IAM 左侧导航栏 Access management 下选择 Roles,之后点击 Create role

alt

Trusted entity type 选择 AWS account,然后在 An AWS account 下选择 Another AWS accountAccount ID 暂时先填当前账号的 ID,之后会修改:

alt

同时,选择 Require external ID (Best practice when a third party will assume this role)External ID 暂时用一个假的例如 0000 替代,之后同样会修改:

alt

最后绑定先前创建的 S3 策略:

alt

创建角色之后,记录下角色的 ARN,接下来会用到:

alt

创建 Storage Integration

这时就可以在 Snowflake 中创建 Storage Integration 了:

1
2
3
4
5
6
CREATE STORAGE INTEGRATION snowflake_storage_integration_example
TYPE = EXTERNAL_STAGE
STORAGE_PROVIDER = 'S3'
ENABLED = TRUE
STORAGE_AWS_ROLE_ARN = 'arn:aws:iam::123:role/snowflake-integration-role'
STORAGE_ALLOWED_LOCATIONS = ('s3://snowflake-storage-integration-example/loading/', 's3://snowflake-storage-integration-example/unloading/')

其中 STORAGE_AWS_ROLE_ARN 是之前所创建的 IAM 角色的 ARNSTORAGE_ALLOWED_LOCATIONS 是示例 Bucket 下的两个文件夹的地址。

只有授权了 CREATE INTEGRATION 权限的角色才能创建 STORAGE INTEGRATION,默认只有 ACCOUNTADMIN 才有这个权限。

获取 Snowflake 的用户 ARN 和 External ID

接着需要获取所创建的 Storage Integration 对应的 Snowflake IAM 用户的 ARNExternal ID

1
desc integration snowflake_storage_integration_example;

alt

记录下 STORAGE_AWS_IAM_USER_ARNSTORAGE_AWS_EXTERNAL_ID

授权 Snowflake 用户

回到之前创建的 IAM 角色,在 Trust relationships 下替换掉之前填写的临时 Account IDExternal ID

alt

alt

完成后,我们就可以执行一条 Data Unloading 命令来验证配置是否成功:

1
2
3
4
copy into 's3://snowflake-storage-integration-example/unloading/'
from (select OBJECT_CONSTRUCT_KEEP_NULL(*) from (select * from MY_DATABASE.MY_SCHEMA.MY_TABLE limit 10))
FILE_FORMAT = (type = json, COMPRESSION = NONE)
STORAGE_INTEGRATION = snowflake_storage_integration_example

如果配置成功,那么 Snowflake 会将表 MY_DATABASE.MY_SCHEMA.MY_TABLE 的数据导出到 s3://snowflake-storage-integration-example/unloading/ 文件夹下。

参考

翻到了一本压箱底的 Newton 科学世界,本期主题为 就诊指南,大概整理了一下,源文件

alt

参考

  • Newton 科学世界(2022.5)

介绍

这是一篇出现在 Hacker News 上的文章,作者阐述了关于不当管理者的17个理由。

17个理由

1. 你热爱你所做的事

  • 你会为能亲自实现某个功能而兴奋不已吗?
  • 你会时而在敲完一天代码后开心的哼着小曲下班吗?
  • 你会为过去所实现的功能、达成的成就感到自豪吗?

如果是的话,那么你是一个幸运的打工人。永远不要低估一颗热爱工作的心,同时也不要想当然的认为无论何时都能重拾这份热爱。

2. 找到一份工程师的工作很简单

在裁员时代,这点已经不再简单。不过在同等条件下,工程师岗位相对于管理者岗位来说:

  • 技能更能够量化,仅从通过面试来说,一部分技能甚至可以在面试过程中不断学习强化
  • 除了特定行业外,工程师的技能一般不和公司深度绑定。在上一家公司培养的技能并不会因为换了家公司就基本没用;而管理者在上一家公司与各团队构建的信任关系则一般无法带到下一家公司,或者换了一家公司后,需要应对未曾遇到的人员关系

3. 管理者岗位僧多粥少

管理者岗位一个萝卜一个坑,其招聘数量远少于工程师岗位。

4. 管理者最先被炒鱿鱼

如果真要裁员,光裁管理者是不够的,工程师反而有天然的【人数优势】。

除非是一锅端,否则各部门按比例的裁员场景下,应该不会有管理者自告奋勇的说自己产生不了直接价值,底下的人离开我也能转,裁我吧。

5. 管理者不易跳槽

除开管理者岗位本身的原因,年龄也有一定的影响,但这不仅仅针对管理者。管理者的年龄一般比下属的工程师大,即使一个工程师在年轻的时候可以一年两跳,到了管理者同样的年纪也可能会变得不容易跳槽。

6. 工程师会看轻管理者

大家都是打工的,没有必要谁看不起谁。当然也会有唯技术论的工程师,无视技术之外的一切;但同样的,也有始终认为自己是主子的管理者,这种,自然是没有必要迎合的。

那么,管理者需不需要懂技术?如果是放权型管理者,能安心将技术决策委托给核心工程师,是可以不用懂技术的,从而专注发挥好自己的管理长处。不过,这属于可遇不可求的情况,现实中没有那么多的刘备和孔明。虽然作者和他的同事们讨论后都认为所遇到的优秀的管理者都不懂技术,但是优秀的管理者本身是比优秀的工程师更为稀缺的存在。所以,对于一般的管理者,至少在技术上要能认识到十个女人一个月真的生不出孩子。

7. 管理者有时候要当坏人

身为管理者,难免会遇到以下的情况:

  • 绩效有人要背 C
  • 裁员指标
  • 传达上头不合理的要求

而并不是所有人都愿意和能合理的处理好这些场景。

8. 管理者的技能树比你想象中的要少

如果从工程师切到了管理者,可能会觉得自己一直在飘着,不再是实际的执行者,这对于某些工程师来说可能会比较难受。而另一方面,立志往管理线发展的人可能会更乐于去做引导一个产品或项目落地的过程,对实际执行并不太关心,并在这期间逐步提高自己的影响力。

我认为这里能体现管理者水平的地方包含但不限于如何处理:

  • 你的目标对你很重要,但对其他人不重要
  • 你有雄心壮志,但其他人只想安分守己

9. 做得好是你的本分,做不好是你的锅

大和田老师在半泽直树1里说过:

下属的功劳是上司的功绩,上司的过错是下属的责任

做不好又能把锅甩出去也是管理的一种能力。

10. 你需要以 IC 的身份和管理者分庭抗礼

IC 全称 Individual Contributor,常见翻译为独立贡献者或个人贡献者。IC 最明显的特点是没有管理职责,注意不等同于没有管理工作,他们利用自己的专业水平协同或者独立完成任务,最终可能成为某一方面的专家。

IC 也分等级,例如 Dropbox 的软件工程师职位就划分为了 IC1IC7,而管理者岗位则是 M 线。高级的 IC 也会有管理工作,例如项目管理(高级 IC 负责的项目很可能已经不是自己能独立完成的了)或者人员管理(什么地方用什么样的人)。

这里作者认为需要有能够发声的高级 IC,因为他们毕竟还是 IC 线,他们所代表的利益有时也符合普通工程师的利益。如果高级 IC 最终都转到了管理岗,那么本来就人微言轻的普通工程师的利益也更难传达到上层。不过,这也要求公司有能够让高级 IC 开花结果的土壤。

11. 管理只是一系列技能,你同样能以 IC 的身份去尝试所有有趣的管理工作

随着在 IC 路线上的成长,你会逐渐涉及一些技术之外的管理工作。有人可能就会乐于去尝试这些管理工作,例如担任导师,面试,参与决策,制定职业规划等。作者认为一个健康的公司应当鼓励并允许高级 IC 去参与这些工作。这样就避免了参与管理者职责中的一些不讨喜的活,例如绩效考核,裁人等。

12. 更难从工作中感到愉悦

修复一个问题或者学习新知识所带来的愉悦可能就此一去不复返,同时,工作中的正反馈周期也可能变长。

不过,这也因人而异,那些享受改完一个高深 Bug 的工程师可能根本不会想着做管理,而有些做管理的人也可能根本不认为改完一个高深的 Bug 是种享受,他们的愉悦点可能在于来自底下的服从。

13. 情绪影响会衍生甚至占据你的个人生活

身为管理者后,会与更多的人打交道,而人不是一个确定的个体,每个人有各自的行为处世,你可能会觉得更心累。

14. 你的时间不再属于你

普通工程师的时间都不能够一定保证,管理者可能更甚。

15. 会议

更恐怖的是无尽的低效会议。

16. 如果你的心之所向是技术引领

成为管理者后,你的做事方式就转变为了影响团队,提高团队。你的技术水平也会因此停滞不前,然后逐渐衰退。如果你认为这是一种折磨,那么你就不适合成为管理者。

17. 管理者岗位始终会等着你

即使是技术路线越往上走也越会要涉及管理工作,如果你不在乎一个头衔,又何必急于一时。

最后

理想的情况下自然是合适的人在合适的位置上,不过现实中也会有赶鸭子上架而做了管理者的人,或者为了延长自己的职业寿命而无奈转了管理者。但无论如何,管理并不是一个想当然的工作,并不是因为工程师干不下去了所以到时候就转管理,这既不尊重管理岗位本身,也不尊重团队中的其他人,只会多一个不靠谱的管理者,而不靠谱的管理者比不靠谱的工程师更糟糕。

参考

介绍

Grafana Cloud 为免费账户提供了一万条指标的存储额度,对于业余项目来说可以考虑将指标上传到由 Grafana Cloud 托管的 Prometheus 中。

安装 Grafana Agent

Prometheus 指标数据的上传需要通过 Grafana Agent 来完成,以下安装步骤以 Ubuntu 为例:

1
2
3
4
5
mkdir -p /etc/apt/keyrings/
wget -q -O - https://apt.grafana.com/gpg.key | gpg --dearmor | sudo tee /etc/apt/keyrings/grafana.gpg
echo "deb [signed-by=/etc/apt/keyrings/grafana.gpg] https://apt.grafana.com stable main" | sudo tee /etc/apt/sources.list.d/grafana.list
sudo apt-get update
sudo apt-get install grafana-agent

安装完成之后通过 sudo systemctl start grafana-agent 将其启动,并可通过 sudo systemctl status grafana-agent 显示 grafana-agent 的当前状态:

1
2
3
4
5
6
7
8
9
10
11
● grafana-agent.service - Monitoring system and forwarder
Loaded: loaded (/lib/systemd/system/grafana-agent.service; disabled; vendor preset: enabled)
Active: active (running) since Sun 2023-03-12 05:08:43 UTC; 13s ago
Docs: https://grafana.com/docs/agent/latest/
Main PID: 1049084 (grafana-agent)
Tasks: 7 (limit: 1041)
Memory: 125.6M
CGroup: /system.slice/grafana-agent.service
└─1049084 /usr/bin/grafana-agent --config.file /etc/grafana-agent.yaml -server.http.address=127.0.0.1:9090 -server.grpc.address=127.0.0.1:9091

Mar 12 05:08:43 example-name systemd[1]: Started Monitoring system and forwarder.

同时,如果希望系统重启后自动启动 grafana-agent 服务,可以执行如下的命令:

1
sudo systemctl enable grafana-agent.service

另外,可以通过 sudo journalctl -u grafana-agent 查看 grafana-agent 的运行日志:

1
2
3
4
5
6
7
8
-- Logs begin at Sun 2021-12-26 04:48:21 UTC, end at Sun 2023-03-12 06:34:53 UTC. --
Mar 12 05:08:43 example-name systemd[1]: Started Monitoring system and forwarder.
Mar 12 05:38:45 example-name grafana-agent[1049084]: ts=2023-03-12T05:38:45.6366501Z caller=cleaner.go:203 level=warn agent=prometheus component=cleaner msg="unable to fi>
Mar 12 06:08:45 example-name grafana-agent[1049084]: ts=2023-03-12T06:08:45.63549564Z caller=cleaner.go:203 level=warn agent=prometheus component=cleaner msg="unable to f>
Mar 12 06:20:16 example-name systemd[1]: Stopping Monitoring system and forwarder...
Mar 12 06:20:16 example-name systemd[1]: grafana-agent.service: Succeeded.
Mar 12 06:20:16 example-name systemd[1]: Stopped Monitoring system and forwarder.
Mar 12 06:20:16 example-name systemd[1]: Started Monitoring system and forwarder.

上报监控数据

grafana-agent 上报的监控数据分两种,一种是 grafana-agent 自身及其所在主机的监控数据,另一种是自定义服务的监控数据,我们需要修改 grafana-agent 的配置文件来指定如何收集监控数据。

grafana-agent 的默认配置文件为 /etc/grafana-agent.yaml

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
# Sample config for Grafana Agent
# For a full configuration reference, see: https://grafana.com/docs/agent/latest/configuration/.
server:
log_level: warn

metrics:
global:
scrape_interval: 1m
wal_directory: '/var/lib/grafana-agent'
configs:
# Example Prometheus scrape configuration to scrape the agent itself for metrics.
# This is not needed if the agent integration is enabled.
# - name: agent
# host_filter: false
# scrape_configs:
# - job_name: agent
# static_configs:
# - targets: ['127.0.0.1:9090']

integrations:
agent:
enabled: true
node_exporter:
enabled: true
include_exporter_metrics: true
disable_collectors:
- "mdadm"

自定义服务的监控数据收集需要定义在 metrics.configs 下,grafana-agent 自身及其所在主机的监控数据收集默认已经是开启的。

假设需要收集由 Spring Bootactuator 模块所暴露的 Prometheus 监控数据,则需要在 metrics.configs 下新增如下类似配置:

1
2
3
4
5
6
7
8
9
- name: 'My Spring Boot App'
scrape_configs:
- job_name: 'My Spring Boot App'
metrics_path: '/actuator/prometheus'
scrape_interval: 1m
static_configs:
- targets: ['127.0.0.1:8080']
labels:
application: 'My Spring Boot App'

最后,再通过 remote_write 设置将监控数据推送到 Grafana Cloud 下的 Prometheus

1
2
3
4
5
remote_write:
- url: https://prometheus-xxx.grafana.net/api/prom/push
basic_auth:
username: username
password: password

其中 urlusernamepassword 这三个信息都可以在所创建的 Grafana Cloud Stack 下的 Prometheus 的详情页中找到。password 对应 Grafana Cloud API Key,如果之前没有创建过的话需要新生成一个,角色选择 MetricsPublisher 即可:

alt

完整的配置文件示例如下:

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
server:
log_level: warn

metrics:
global:
scrape_interval: 1m
wal_directory: '/var/lib/grafana-agent'
configs:
- name: 'My Spring Boot App'
scrape_configs:
- job_name: 'My Spring Boot App'
metrics_path: '/actuator/prometheus'
scrape_interval: 1m
static_configs:
- targets: ['127.0.0.1:8080']
labels:
application: 'My Spring Boot App'
remote_write:
- url: https://prometheus-xxx.grafana.net/api/prom/push
basic_auth:
username: username
password: password

integrations:
agent:
enabled: true
node_exporter:
enabled: true
include_exporter_metrics: true
disable_collectors:
- "mdadm"
prometheus_remote_write:
- url: https://prometheus-xxx.grafana.net/api/prom/push
basic_auth:
username: username
password: password

配置文件修改完成之后,通过 sudo systemctl restart grafana-agent 来重启 grafana-agent 服务。

Grafana 展示

对于自定义服务的监控展示使用自己熟悉的方式即可,例如 Java 应用可以配合使用 JVM (Micrometer)

对于 grafana-agent 自身的监控展示可以结合 agent-remote-write.json

alt

最后,grafana-agent 对于所在主机的监控展示可以借助 Node Exporter Full

alt

参考

0%