阅读视图

发现新文章,点击刷新页面。

[译] 硬盘(HDD)内部结构与工作原理的动画展示(2022)

译者序

本文翻译自 2022 年 Branch Education 的一个科普视频 How do Hard Disk Drives Work? (Youtube), 强烈推荐观看原视频(上不了油管的,B 站也有搬运)。本文整理个图文版方便查阅与思考, 另外增加了点相关的 Linux 软件知识,方便与日常工作衔接起来,

其他优质文章:

  1. How does a hard drive work, https://www.explainthatstuff.com/, 2024

    除了硬件拆解和介绍工作原理,还对比了 HDD 和 SDD,并且更重要的,介绍了 IBM 发明硬盘的历史

  2. How a Hard Drive Works, cs.stanford.edu, 2012

    斯坦福的一个老师实物教学,开盖展示读写数据时,硬盘的工作过程(然后这个盘就废了)。

  3. HDD from Inside: Hard Drive Main Parts, https://hddscan.com/

    硬件拆解部分比本文更详细,想了解更多硬件细节的,可作为补充。

水平及维护精力所限,译文不免存在错误或过时之处,如有疑问,请查阅原视频。 传播知识,尊重劳动,年满十八周岁,转载请注明出处

以下是译文。



原视频由 PCBWay 赞助,感谢赞助商。

1 硬盘拆解

1.1 盘片(platter)

盘片是存储数据的地方,

Disk/platter

  • 根据存储容量的不同,硬盘可能会有多个盘片堆叠,如上面右图所示;
  • 磁盘由铝镁合金(aluminum magnesium alloy)和其他合金的多个涂层组成,

    Disk/platter

  • 磁性功能层是 120nm 的钴铬钽合金薄层(cobalt chromium tantalum alloy), 它由磁性微块组成,磁极方向能变,

    Disk/platter

  • 盘片安装在主轴上,主轴使用中心的无刷直流电机(brushless DC motor)以 7200rpm 的等速度旋转。

1.2 机械臂装置

机械臂装置包括好几个组成部分,分别来看下。

1.2.1 机械臂(arm)

每个盘(platter)上下各有一个臂(arm),

1.2.2 滑橇(slider)和读写头(read/write head)

每个臂的末端有一个称为 slider(滑橇、滑块)的模块,它里面又包括了一个读/写头 (注意,读头和写头是分开的两个部件,后面会详细介绍),

磁盘高速旋转产生的气流能使这个滑块(和读写头)浮起来, 稳定运行在离磁盘表面 15nm(约 100 个原子)的地方,如下面的动图所示,

Fig. 高速旋转的盘片产生的气流使滑橇和读写头飘起来

1.2.3 读写头停靠装置

只有当盘片全速旋转时(有数据读写任务),机械臂才会转到磁盘表面上。 平时盘片不旋转时(没有读写任务),机械臂会停在磁盘边上的一个小塑料装置上。

1.2.4 尾部音圈电机(马达)

机械臂的尾部有一个 音圈电机(voice coil motor),或称音圈马达,它由线圈(coil of wire)和上下两个强钕磁铁(strong neodymium magnets)组成,

VCM(Voice Coil Motor)一种特殊形式的直接驱动电机,原理和扬声器类似,固得名。 通电线圈在磁场内就会产生力,力的大小与施加在线圈上的电流成比例,运动轨迹可以是直线也可以是弧线。 具有结构简单、体积小、速度快、响应快等特点。译注。

线圈通电之后会产生一个力,使机械臂在磁盘上移动(可以正向也可以反向),

这种马达的速度和精度:

  • 速度:读/写头能够在不同磁道上来回移动 ~20 次/秒
  • 精度:读/写头位置精度 ~30nm

1.3 机械臂-电路板之间的数据线

如下图所示,一条柔性电线(a flexible ribbon of wires)沿着机型臂的侧面布线,

  • 一边连接到读/写头
  • 一边连接到一个连接器(connector),该 connector 进一步连接到硬盘的主板,或称印刷电路板(PCB)。

1.4 PCB 和上面的芯片

PCB 上面的东西如下图所示,

这里主要介绍三个芯片:

  1. 主处理器芯片
  2. 内存芯片,作为主处理器的 cache;
  3. 控制音圈马达磁盘主轴电机的芯片。

1.5 数据线接口(e.g. SATA)和电源线接口

PCB 边缘还有两个硬件接口,

  • 数据接口:例如 SATA 接口,用于和电脑主板相连传输数据;
  • 电源接口:用于给 HDD 供电。

1.6 防尘装置

再看一下硬盘的两个防尘装置,

  1. 垫圈:将磁盘密封起来;
  2. 灰尘过滤器:用于捕获灰尘颗粒。

密封和过滤都是非常必要的,因为读写头距离盘片仅 15nm, 而灰尘颗粒的大小可达 10,000nm, 如果与 7200rpm 高速旋转磁盘碰撞,可能会造成严重损坏,

Fig. 读写头正常运行时,距离盘片仅 15nm。

2 盘片的微观组成

了解了粗粒度的硬件构成之后,现在让来深入到盘片的内部,看看它的微观组成。

2.1 磁盘(disk) -> 磁道(track)

首先,每个磁盘以同心圆的方式分割为多个磁道(concentric circles of tracks),

Fig. 磁盘分割为大量磁道。

每个磁盘的磁道数量能达到 500,000 个甚至更多。

2.2 磁道(track) -> 扇区(sector)

然后,沿着直径的方向,所有磁道又被分割为多个扇区,

Fig. 磁道进一步分割为扇区。

2.3 扇区内

现在看一下每个扇区内的结构,

Fig. 每个扇区的内部结构。

如上图所示,每个扇区中,依次包含五部分。

2.3.1 前导/同步区(preamble or synchronization zone)

记录这个旋转磁盘的确切速度每个比特位的长度(length of each bit of data)。

2.3.2 地址区

帮助读/写头确定当前位于哪个磁道和扇区

2.3.3 数据区

扇区大小

扇区的大小因盘而异,例如老一些的盘是 512 字节或 2KB,新一些的通常是 4KB。

查看磁盘扇区大小(译注)

有很多工具可以查看,lsblk 指定显示磁盘名字、物理扇区大小和逻辑扇区大小:

$ lsblk -o NAME,PHY-SeC,LOG-SeC
NAME                   PHY-SEC LOG-SEC
sda                       4096     512  # 这块是 SATA SSD
sdb                        512     512  # 这块是 SATA HDD

fdisk -l,这个命令好记:

$ fdisk -l
Disk /dev/sdb: 2.18 TiB, 2399276105728 bytes, 4686086144 sectors
Disk model: XXX                                                    # 硬盘型号
Units: sectors of 1 * 512 = 512 bytes                              # 当前扇区大小
Sector size (logical/physical): 512 bytes / 512 bytes              # 逻辑值 & 物理支持的最大值
I/O size (minimum/optimal): 512 bytes / 512 bytes

iostat 磁盘读写带宽(译注)

可以通过 cat /proc/diskstats 查看磁盘的读写情况,其中就包括了每个磁盘已经读写的 sectors 数量:

$ cat /proc/diskstats
#                            r_sectors               w_sectors
   8       0 sda 31663 10807 2928442     8471 203024 106672     6765800 ...

这个数量乘以 sector 大小,就是已经读写的字节数,iostat 等工具显示的磁盘读写带宽,就是根据这个来计算(估算)的。

2.3.4 纠错码(ECC)区

Fig. 每个扇区的内部结构。

用于校验存储在块中的数据。

2.3.5 扇区之间的间隔区

给了读/写磁头一定的容错能力。

3 写数据

现在让我们进一步看看读/写磁头的内部机制,以及写头(write head)是是如何写数据的。

3.1 磁场微块和磁化

扇区是由一个个磁场微块组成的, 写头通过改变磁盘微块的磁化方向来实现数据写入,

每个磁盘微块大小约为 90nm x 100nm x 125nm

磁化之外,微块内原子的南北极是随机的; 磁化之,微块所有原子的北南极都指向同一方向,

每个微块对应的就是一个 bit 数据,

3.2 写入 1bit 的过程

下面具体看一下如何磁化一个微块(相当于写入 1bit 数据)。

电流施加到 write head 的线圈之后,就会在此处产生一个强磁场,

这个磁场沿着 write head 向下,聚焦到尖端的一个小点,改变它正下方的磁盘微块极性 (中间的缝隙就是前面提到过的读写头 15nm 悬浮高度),

磁化之后的微块变成永磁体,能保持这个状态很多年,也就是数据已经持久化, 以后可以重复用读头感应这个永久磁场,读出存储的数据。

3.3 覆盖写

原理跟上面一样,也是逐 bit 来。 如果新写入的 bit 跟已经存储的一样,磁极就不变,否则就改变一下方向。

4 读数据

再来看看如何从磁盘读数据。

4.1 如何表示 0 和 1

4.1.1 不是用南北极指向表示

前面我们假设了不同南北极的磁块分别表示 0 和 1,

这在概念上非常简单,但实际实现并非如此。

4.1.2 用南北极指向的变化表示

实际的 read head,检测的是相邻两个微块的磁极变化, 这是因为磁极变化的强度单个微块的磁场强度要大得多,所以这种方式的检测准确率非常高

所以,如上图所示,

  • 相邻微块磁场方向变化,表示 1;
  • 相邻微块磁场方向不变,表示 0。

4.2 读头(read head)内部结构

那么,检测这些磁场的读头内部结构是怎样的呢?

如上图所示,

  • 读头里面是多层导电材料,由铁磁材料和非磁性材料的交替组成。
  • 这种多层材料具有一种称为巨磁阻(giant magnetoresistance, GMR)的特性, 简单来说,穿过它的磁场强度发生变化时,它的电阻率就会变化

4.3 读取数据:GMR 和读头电阻率

基于 GMR 特性,根据读头的电阻率就能判断下面存储的 0 还是 1,

  • 电阻率较低时,表示读取头下方磁场变化强,对应存储的是 bit 1
  • 电阻率较高且无磁场时,对应存储的是 bit 0

4.4 连续 0 的问题

以上过程有一个问题:如果较长连续区域的磁极都一样,对应的就是一长串的 0,由于读头的精度,有可能会导致多读或少读几个 0,导致数据错乱。

解决方少:利用每个 sector 的前导区和纠错码区中的信息。

5 致谢

原作者 Branch Education 感谢所有个人赞助者和会员赞助商,让他们制作了如此精良的科普视频。

6 Linux 存储相关的子系统和软件栈(译注)

6.1 从进程 read/write 请求到 HDD 读写数据

来自 Linux Storage Stack Diagram, 涵盖了 3.x ~ 6.x 多个内核版本,这里先贴一个 3.x 的,因为简单, 方便看出从用户进程发出 read/write 请求到 HDD 读写数据的内核模块链路:

虚拟文件系统(VFS)里面分为几类:

  1. 常规文件系统(ext4, xfs, btrfs, …);
  2. 网络文件系统(NFS, CIFS, …);
  3. 伪文件系统(procfs, sysfs, …);
  4. 特殊文件系统(tmpfs, devtmpfs, …)。

再贴一个 kernel v6.9 的,

6.2 内核 block layer 深入解读

  1. A block layer introduction part 1: the bio layer, LWN.net, 2017
  2. A block layer introduction part 2: the request layer, LWN.net, 2017

Written by Human, Not by AI Written by Human, Not by AI

英国的 NHS 附加费


英国的 NHS 附加费(NHS Surcharge) 是针对非英国居民的医疗附加费用,全称为 Immigration Health Surcharge (IHS),也叫移民健康附加费。自 2015 年开始实施,所有申请英国签证、并在英国停留六个月以上的非居民都需缴纳该费用。这笔费用让签证持有者可以在 NHS 体系下享受与英国居民相同的大部分医疗服务,而无需支付额外费用。以下是 IHS 的详细介绍:

费用标准

IHS 的费用根据签证类型和申请人的年限来计算,目前的标准为每年 624 英镑(儿童则是每年 470 英镑)。

费用通常会在签证申请过程中一次性支付,计算方式为申请人在英国停留的年数乘以相应的年费。若停留时间不满半年,则按半年标准收费。

适用人群

所有在英国停留超过 6 个月的非英国居民,包括工作签证、学生签证、家庭签证和其他长期签证持有者,都需支付 NHS 附加费。

部分人群可获得减免或豁免,例如欧盟永久居留持有者、人道主义签证申请人以及一些短期访问签证申请者。

支付方式

NHS 附加费需在申请签证时预先缴纳,通常会和签证申请费一起支付。

如果签证被拒,IHS 费用将被全额退还。当年我第一次给我媳妇办签证的时候因材料没准备好就签证被拒了,第一次拒的时候就给退了这个费用。

享受的 NHS 服务

支付了 NHS 附加费后,签证持有者在英国可以享受 NHS 提供的大部分免费服务,如家庭医生(GP)诊疗、急诊服务、住院治疗、产前和产后护理等。

比如:我在英国生的两孩子都是免费的:儿子在英国出生了..

但某些服务仍需额外付费,如处方药、牙科和视力检查等,这些项目与英国居民的收费标准相同。

费用涨幅

IHS 费用在2018年、2020年都有过涨幅,且可能随着时间推移继续上涨,以应对 NHS 系统增加的成本和资源需求。

退款政策

如果签证被拒,或者提前离开英国(且未持有有效签证),可以申请部分或全部 NHS 附加费的退款。

如签证被缩短到低于 6 个月,也可以申请部分退款。

NHS 附加费帮助英国政府为 NHS 系统筹集资金,以保障更多人享受免费或低费用的医疗服务。对于在英长居或长期工作的签证持有者,这项费用可以有效减少在 NHS 下享受医疗服务的整体开支。

英国NHS免费医疗

本文一共 770 个汉字, 你数一下对不对.
英国的 NHS 附加费. (AMP 移动加速版本)

扫描二维码,分享本文到微信朋友圈
75a5a60b9cac61e5c8c71a96e17f2d9c 英国的 NHS 附加费 医学 生活 资讯
The post 英国的 NHS 附加费 first appeared on 小赖子的英国生活和资讯.

相关文章:

  1. 按揭贷款(房贷,车贷) 每月还贷计算器 去年给银行借了17万英镑 买了20万7500英镑的房子, 25年还清. 前2年是定率 Fix Rate 的合同 (年利率2.49%). 每个月大概是还 700多英镑. 有很多种还贷的计算方式, 定率/每月固定 是比较常用的. 简单来说就是 每个月交的钱是...
  2. 智能手机 HTC One M9 使用测评 虽然我对手机要求不高, 远远没有像追求VPS服务器一样, 但是怎么算来两年内换了四个手机, 先是三星 S4 用了一年多, 然后 Nokia Lumia 635 Windows Phone, 后来又是 BLU, 半年多前换了...
  3. 英国国家医疗服务体系(NHS)简介 英国国家医疗服务体系 (National Health Service, 简称 NHS) 是英国政府提供的公共医疗卫生服务体系,于1948年创立,旨在为英国居民提供从出生到老年的综合医疗服务。NHS 的宗旨是确保每位英国居民无论其收入、地位和健康状况如何,都能获得公平且免费(或低价)的医疗服务,属于全球范围内最早建立的全民医疗体系之一。以下是 NHS 的一些关键特点: 英国国家医疗服务体系(NHS)于1948年7月5日正式成立。这一体系是由当时的卫生部长阿奈林·贝文(Aneurin Bevan)主导创立的,其核心理念是确保所有人都能获得公平的医疗服务,无论收入高低。NHS 的诞生标志着现代公共医疗服务的开端,为全球范围内的全民医疗体系提供了参考。 全民覆盖和免费医疗 NHS...
  4. 英国NHS为年龄在40到70岁之间的中老年人提供免费身体检查 前几天过了40岁生日,正式步入了四十不惑的年龄。 “四十不惑” 是中国古代儒家思想中的一个成语,出自《论语·为政》一书,原文是“吾十有五而志于学,三十而立,四十而不惑,五十而知天命,六十而耳顺,七十而从心所欲,不逾矩”。这句话表达了孔子对人生各个阶段的理解和期望。 “四十不惑”的意思是指一个人到了四十岁,已经积累了足够的经验和智慧,不再轻易受到外界的干扰或困惑。这个年龄段的人对自己的人生观、价值观有了比较清晰的认知,能够判断是非,不会被事物表象或外界的干扰所迷惑。 在现代语境下,“四十不惑”常被理解为人到中年,对生活、工作和人际关系有了更成熟的态度,有自己的独立见解和判断,不再轻易摇摆或迷茫。 没过几天,就收到了英国NHS(全民医保)的短信,说我可以去做个免费体检(身体健康检查)。 根据所给的电话,我打过去询问,得知,这是个第三方的公司,和当地的市政厅合作,给居民提供免费的身体检查,不过有三个条件: 过去五年内没有做过体检。 年龄在40到70岁之间。 居住在剑桥地区(CB邮编)或者皮德堡/peterborough/PE邮编 所以,我不符合条件,毕竟我这一次全面体检就是半年前回国的时候,我顺便问了一下身体检查有啥项目,被告知就是很基本的那几项:身高、体重、血压、血常规、尿检。毕竟英国全民医保,并不可能给你像国内那种体检套餐没事做个MRI核磁共震或者是X光。英国的理念是健康的人不需要身体检查,有问题才会对症下药/检查。 第三方体检机构表示,如果有任何问题或感到不适,应该先去找GP全科医生。以上三个条件中,第一条感觉是有商量的空间的。我还特别问了什么样的检查算体检。不过无所谓,反正公司有福利,每年都可以安排一次体检,刚好下周就约到了公司的体检。 本文一共 693 个汉字,...
  5. 车轮胎边上鼓起了包需要换掉么? 今天在30英理/小时 的时候不小心碰到马路崖, 就听见碰咚一声, 下车看, 车身没事, 倒是右边轮胎 (Off Side Front) 起了一个大包, 长这样. 不幸中的万幸, 轮胎是消耗品, 不会直接影响到车的保值, 而车身如果有刮擦,...
  6. 同一台服务器上多个WORDPRESS站点的一些设置可以移出去 我自从把所有网站都挪到一处VPS服务器上 就发现很多事情省事很多 可以同时管理多个网站 包括 WORDPRESS博客. 比如我有四个WORDPRESS博客 然后我就把通用的一些资料给移出去 移到 HTTP或者HTTPS都不能直接访问的文件夹里这样就更安全许多. 文件 wp-conn.php 存储了 相同的数据库资料. 1 2...
  7. 公司请的专业摄影师 公司来了新的CEO管理之后,很多事情都不一样了, 特别是一些公司对外形象的事情就特别的在意, 比如公司网站用上SSL.现在公司还有空闲的位置,请速来(钱多人不傻). 一月份出差回LUTON,刚好公司请来摄影师给高层管理照像放网站上的,于是我也凑了凑热闹(但是却还不够资格被放在公司网站上),不过没关系,放这里也差不多. 人到中年, 沧桑感强了些. 更新更新: 同事用他NB的单反给谢菲尔得办公室的人也拍了一组这样的照片.看起来很不错, 很专业,灯光,道具应有尽有.我已经用在了LINKEDIN页面上,立马高大上. 本文一共 230 个汉字, 你数一下对不对. 公司请的专业摄影师. (AMP...
  8. 在英国给孩子换学校的经历: 孩子离开了村里的小学 由于搬了家, 孩子上学得提前半小时出门了, 因为早上堵, 也得开车半小时才能到. 之前在 Fen Drayton 村庄上小学, 早上8:45学校门开, 9点敲钟孩子排队依次进入教室, 我们由于在村里, 只需要提前5分钟出门和孩子一起走路就可以了. 现在一下子早上变得很匆忙, 得叫孩子起床, 做早饭,...

英国国家医疗服务体系(NHS)简介


英国国家医疗服务体系 (National Health Service, 简称 NHS) 是英国政府提供的公共医疗卫生服务体系,于1948年创立,旨在为英国居民提供从出生到老年的综合医疗服务。NHS 的宗旨是确保每位英国居民无论其收入、地位和健康状况如何,都能获得公平且免费(或低价)的医疗服务,属于全球范围内最早建立的全民医疗体系之一。以下是 NHS 的一些关键特点:

英国国家医疗服务体系(NHS)于1948年7月5日正式成立。这一体系是由当时的卫生部长阿奈林·贝文(Aneurin Bevan)主导创立的,其核心理念是确保所有人都能获得公平的医疗服务,无论收入高低。NHS 的诞生标志着现代公共医疗服务的开端,为全球范围内的全民医疗体系提供了参考。

全民覆盖和免费医疗

NHS 是一种由税收资助的公共医疗系统。其最核心的理念是“按需分配医疗资源,不以盈利为目的”,因此所有注册的英国居民可以免费享受绝大部分基础的医疗服务,包括急诊、住院治疗、产前产后护理等。

部分非紧急医疗服务可能收取少量费用,例如牙科检查和处方药,但对低收入人群、儿童、老年人等有一定的减免政策。

非英国居民(比如办签证的时候)需要交纳一个NHS附加费才可以享受这个NHS医疗。

分层的医疗服务

NHS 系统主要分为初级、二级和三级医疗服务。初级医疗服务包括家庭医生(GP)诊所,提供常见病的诊治、咨询和转诊。家庭医生是每个患者进入 NHS 的首要接触点,只有通过 GP 转诊才能到医院就诊。

二级医疗服务包括专科医生和医院治疗,通常是由家庭医生转诊的疾病。

三级医疗服务则包括更专业的治疗和康复服务,例如癌症治疗、移植手术等。

国家拨款和政府监管

NHS 主要由英国政府通过税收资金提供支持,预算每年由财政部批准,并由卫生与社会保障部进行监管。

英格兰、苏格兰、威尔士和北爱尔兰的 NHS 体系虽各自独立运营,但有类似的结构和服务内容,并共同受中央卫生政策的指导。

今年大选工党上台(Labour)承诺要多给NHS预算。

NHS 的挑战

随着人口老龄化和慢性疾病患者的增多,NHS 近年来面临着日益增长的医疗需求,导致资源紧张。

NHS 的一些服务,例如预约、急诊等待时间等,可能会因资源限制而较长。

疫情等突发公共卫生事件进一步加剧了 NHS 的压力,带来了一些财务和人力方面的挑战。

社会和文化影响

NHS 不仅是英国居民的医疗保障体系,还成为了英国社会的重要组成部分,深受民众的信赖和支持。NHS 在国际上也享有较高的声誉,被许多国家视为公共医疗的成功典范。

总体来说,NHS 代表了英国政府的社会福利政策,通过免费或低费用的医疗保障服务,提高了全民健康水平,使其成为英国社会稳定的重要支柱。

英国NHS免费医疗

本文一共 984 个汉字, 你数一下对不对.
英国国家医疗服务体系(NHS)简介. (AMP 移动加速版本)

扫描二维码,分享本文到微信朋友圈
75a5a60b9cac61e5c8c71a96e17f2d9c 英国国家医疗服务体系(NHS)简介 医学 生活 资讯
The post 英国国家医疗服务体系(NHS)简介 first appeared on 小赖子的英国生活和资讯.

相关文章:

  1. 智能手机 HTC One M9 使用测评 虽然我对手机要求不高, 远远没有像追求VPS服务器一样, 但是怎么算来两年内换了四个手机, 先是三星 S4 用了一年多, 然后 Nokia Lumia 635 Windows Phone, 后来又是 BLU, 半年多前换了...
  2. 按揭贷款(房贷,车贷) 每月还贷计算器 去年给银行借了17万英镑 买了20万7500英镑的房子, 25年还清. 前2年是定率 Fix Rate 的合同 (年利率2.49%). 每个月大概是还 700多英镑. 有很多种还贷的计算方式, 定率/每月固定 是比较常用的. 简单来说就是 每个月交的钱是...
  3. 英国房子的EPC节能报告(Energe/Efficiency Performance Certificate) EPC (Energe/Efficiency Performance Certificate) 是英国房子的节能报告, 法律上规定, 每个房子都必须要有一个EPC报告, 报告的有效期为十年. 房东在把房子出租或者想卖房的时候, 这个EPC就必须有效, 在一些情况下 比如出租房子的时候, 这个EPC报告还必须符合一些最低标准, 比如房子必须满足 F档(类似及格线)...
  4. 英国的 NHS 附加费 英国的 NHS 附加费(NHS Surcharge) 是针对非英国居民的医疗附加费用,全称为 Immigration Health Surcharge (IHS),也叫移民健康附加费。自 2015 年开始实施,所有申请英国签证、并在英国停留六个月以上的非居民都需缴纳该费用。这笔费用让签证持有者可以在 NHS 体系下享受与英国居民相同的大部分医疗服务,而无需支付额外费用。以下是 IHS 的详细介绍:...
  5. 车轮胎边上鼓起了包需要换掉么? 今天在30英理/小时 的时候不小心碰到马路崖, 就听见碰咚一声, 下车看, 车身没事, 倒是右边轮胎 (Off Side Front) 起了一个大包, 长这样. 不幸中的万幸, 轮胎是消耗品, 不会直接影响到车的保值, 而车身如果有刮擦,...
  6. 英国NHS为年龄在40到70岁之间的中老年人提供免费身体检查 前几天过了40岁生日,正式步入了四十不惑的年龄。 “四十不惑” 是中国古代儒家思想中的一个成语,出自《论语·为政》一书,原文是“吾十有五而志于学,三十而立,四十而不惑,五十而知天命,六十而耳顺,七十而从心所欲,不逾矩”。这句话表达了孔子对人生各个阶段的理解和期望。 “四十不惑”的意思是指一个人到了四十岁,已经积累了足够的经验和智慧,不再轻易受到外界的干扰或困惑。这个年龄段的人对自己的人生观、价值观有了比较清晰的认知,能够判断是非,不会被事物表象或外界的干扰所迷惑。 在现代语境下,“四十不惑”常被理解为人到中年,对生活、工作和人际关系有了更成熟的态度,有自己的独立见解和判断,不再轻易摇摆或迷茫。 没过几天,就收到了英国NHS(全民医保)的短信,说我可以去做个免费体检(身体健康检查)。 根据所给的电话,我打过去询问,得知,这是个第三方的公司,和当地的市政厅合作,给居民提供免费的身体检查,不过有三个条件: 过去五年内没有做过体检。 年龄在40到70岁之间。 居住在剑桥地区(CB邮编)或者皮德堡/peterborough/PE邮编 所以,我不符合条件,毕竟我这一次全面体检就是半年前回国的时候,我顺便问了一下身体检查有啥项目,被告知就是很基本的那几项:身高、体重、血压、血常规、尿检。毕竟英国全民医保,并不可能给你像国内那种体检套餐没事做个MRI核磁共震或者是X光。英国的理念是健康的人不需要身体检查,有问题才会对症下药/检查。 第三方体检机构表示,如果有任何问题或感到不适,应该先去找GP全科医生。以上三个条件中,第一条感觉是有商量的空间的。我还特别问了什么样的检查算体检。不过无所谓,反正公司有福利,每年都可以安排一次体检,刚好下周就约到了公司的体检。 本文一共 693 个汉字,...
  7. 同一台服务器上多个WORDPRESS站点的一些设置可以移出去 我自从把所有网站都挪到一处VPS服务器上 就发现很多事情省事很多 可以同时管理多个网站 包括 WORDPRESS博客. 比如我有四个WORDPRESS博客 然后我就把通用的一些资料给移出去 移到 HTTP或者HTTPS都不能直接访问的文件夹里这样就更安全许多. 文件 wp-conn.php 存储了 相同的数据库资料. 1 2...
  8. 汽车电池没电启动不了: 花了50英镑让AA公司来家里修车 上周开始在家办公 (Work From Home), 再加上平时坐公交上班已经不怎么用车了, 导致这天想用车的时候发现车启动不了. 具体现象就是打火打到一半怎么样也启动不了. 我猜很有可能就是电池没电了(但车灯还是可以亮的). 我打电话叫了AA路道救援, 被告知我的保险并没有包括车停在家里, 需要额外交钱. Call-out 费用是一次50英镑, 然后需要升级到 Home...

理解系统设计中的可用性百分比: 计算系统的停机时间(Availability)


aws-four-nines-sla 理解系统设计中的可用性百分比: 计算系统的停机时间(Availability) 学习笔记 程序员 系统设计 计算机 面试

亚马逊云AWS的SLA是4个9也就是99.99%在线时间uptime

系统设计面试中,可用性百分比是软件工程师应该熟悉的基本知识。

在系统可靠性领域(System Availability),99.9% 或 99.99% 之类的可用性百分比是关键的基准。但是这些数字究竟意味着什么?它们又如何转化为实际停机时间(Downtime)?以下介绍了如何计算与不同可用性水平相关的停机时间,并使用示例来说明 99.9%、99.99% 和其他可用性目标所带来的预期。

什么是可用性百分比?

可用性百分比表示系统在给定时间段(通常是一年、一月或一天)内预计正常运行的时间比例。例如,99.9% 的可用性意味着系统在指定期间内可以停机 0.1% 的时间。

可用性百分比和停机时间

以下是根据不同时间段的可用性百分比计算停机时间的方法:

  1. 确定总时间周期:选择参考周期:
    • 年:365 天,或 31,536,000 秒(365 天 × 24 小时 × 60 分钟 × 60 秒)
    • 月:30 天,或 2,592,000 秒
    • 日:24 小时,或 86,400 秒
  2. 计算允许的停机时间:使用公式:

    停机时间 = 总时间周期 × (1 – 可用性百分比)

示例:99.9% 可用性的停机时间

99.9% 可用性的年度停机时间

  • 一年中的总秒数:31,536,000
  • 可用性:99.9% = 0.999
  • 停机时间 = 31,536,000 × (1 – 0.999) = 31,536 秒
  • 转换为小时和分钟:31,536 秒约为 8 小时 45 分钟

99.9% 可用性的月度停机时间

  • 一个月中的总秒数:2,592,000
  • 停机时间 = 2,592,000 × (1 – 0.999) = 2,592 秒
  • 转换为分钟:2,592 秒约为 43.2 分钟

99.9% 可用性的每日停机时间

  • 一天中的总秒数:86,400
  • 停机时间 = 86,400 × (1 – 0.999) = 86.4 秒
  • 转换为分钟:86.4 秒约为 1.44 分钟

更高可用性水平的停机时间

对于具有更高可用性目标的系统,如 99.99% 或 99.999%,允许的停机时间会变得更短。以下是总结不同可用性水平停机时间的表格:

可用性 年度停机时间 月度停机时间 每日停机时间
99.9%(三个 9) 约 8 小时 45 分钟 约 43.2 分钟 约 1.44 分钟
99.99%(四个 9) 约 52.6 分钟 约 4.4 分钟 约 8.6 秒
99.999%(五个 9) 约 5.3 分钟 约 26 秒 约 0.86 秒
99.9999%(六个 9) 约 31.5 秒 约 2.6 秒 约 86 毫秒

高可用性的重要性

具有高可用性目标的系统对于那些停机会直接影响收入、客户满意度或安全的行业至关重要。实现这些目标需要精心设计,包括负载均衡、冗余、故障转移机制,有时还需要资源的地理分布。

总结

可用性百分比提供了表达系统可靠性的一种方便方式,但将其转换为停机时间则可以更清楚地看到面临的风险。使用这些计算来设置现实的可用性目标,并相应地准备您的基础设施。

英文:Understanding Availability Percentages: Calculating Downtime for Your Systems

本文一共 768 个汉字, 你数一下对不对.
理解系统设计中的可用性百分比: 计算系统的停机时间(Availability). (AMP 移动加速版本)

扫描二维码,分享本文到微信朋友圈
75a5a60b9cac61e5c8c71a96e17f2d9c 理解系统设计中的可用性百分比: 计算系统的停机时间(Availability) 学习笔记 程序员 系统设计 计算机 面试
The post 理解系统设计中的可用性百分比: 计算系统的停机时间(Availability) first appeared on 小赖子的英国生活和资讯.

相关文章:

  1. 深度体验: OneKey虚拟货币出金卡(美元黑卡) 出金/变现的几种方法 出金:也叫Cash out/变现,一般把虚拟货币(如比特币BTC或以太坊ETH)变成法币的方式就叫出金。一般有几种方法: P2P:也叫线下,最直白的方式就是私下一手交钱/法币,一手交币。大型交易所都会有一个P2P的交易,比如币安和HTX火币都有。之前localbitcoin也是这种方式,可惜在2023年倒闭了。我曾经在微信上卖了几十个STEEM,当时是几美元一个的时候。一手交人民币,一手交STEEM币。这种P2P私下的方式不受监管,但是要互相信任。可以当面交易这样减少风险:见个面喝个茶,就把交易做成了。 变成法币:之前我用过Coinbase直接卖成英镑,然后通过发到Paypal再提现到英国银行帐号上变成实实在在在的英镑,不过这一趟下来,手续费不低,就当学费了。 直接花掉:我个人比较喜欢这种方式,有几种Crypto Visa/Master银行卡,可以把虚拟货币卖成法币然后购物花掉。大部分是需要有一个卖币成法币的过程,也有少部分是实时转换虚拟货币成法币,当然基本上是稳定币:USDC, USDT泰达币等。 在英国,想把虚拟货币出金,可以用几种选择: Wirex:支持波场U,支持各种Defi产品,比如定期30天存USDT可以达16%年利率,世界好多国家都支持Wirex卡,上次去塞尔维亚就刷了一次,不过发现汇率并不划算(有5%-10%的差别)。Wirex提现费用较高,不过转换成法币汇率较好。Wirex在乌克兰有个开发办公室。 Crypto.com:这家总部好像在香港,也是不错的,去年的时候它家的DEFI利率挺高,但后来越来越少,直接分成三档/Tier,有次无意和Wirex比较,发现它家USDT转英镑的利率比Wirex低多了,于是不怎么用了。Crypto.com也是需要先把币变成法币。 Crypto Ledger:这是家做硬件钱包的,最近一两年搞了这个产品,它家是直接刷稳定币,也就是消费的时候再兑换虚拟币成法币,有一个2%的费用,不过选择它家平台代币BXX就可以拿回这2%的返现/cashback,相当于不花钱。选择USDT或者BTC返现只有1%。它家的卡是支持加入Apple Pay的,所以可以用在线下支持,日常买菜吃饭都可以出金,很是方便。 OneKey:本文接下来要讲的。...
  2. 微软终于弃用VBScript, 一个时代结束了 VBScript是我最喜欢的编程语言之一,因为其简单的语法,性能稳定,而且在Windows上和COM组件结合,可以做很多事情,Windows管理员在Powershell出来之前用VBScript来完成各种管理工作。VBScript也是我早期学会的编程语言之一(还有LOGO海龟作图,FoxBase数据库,Pascal等)。现在我的任务栏还有VBS Editor,因为我很有时候需要验证些数学或者其它事情,我就会用VBScript来写。比较复杂的我就会用Python。 据说比尔盖茨对Basic语言情有独钟,因为他老人家当年就是设计并开发了Basic语言,后来一直在Windows产品中支持Basic,比如Visual Basic,VB for Application,ASP等。 2023年10月份也就是这个月,微软发布声明,说弃用VBScript了。因为现在,Powershell更为强大,可以完全取代VBScript。VBScript的语法简单很多,而且已经十几二十年没有更新了,已经跟不上主流语言的各种语法糖和框架,和COM结合也带来了一些安全问题,比如当年VBScript来写一些恶意脚本还是非常容易的。 可以在微软的这个页面看到: In future releases of Windows, VBScript will...
  3. 按揭贷款(房贷,车贷) 每月还贷计算器 去年给银行借了17万英镑 买了20万7500英镑的房子, 25年还清. 前2年是定率 Fix Rate 的合同 (年利率2.49%). 每个月大概是还 700多英镑. 有很多种还贷的计算方式, 定率/每月固定 是比较常用的. 简单来说就是 每个月交的钱是...
  4. 智能手机 HTC One M9 使用测评 虽然我对手机要求不高, 远远没有像追求VPS服务器一样, 但是怎么算来两年内换了四个手机, 先是三星 S4 用了一年多, 然后 Nokia Lumia 635 Windows Phone, 后来又是 BLU, 半年多前换了...
  5. 和媳妇聊聊 区块链 (Web3.0, 还有共识算法 PoW, PoS, DPoS) #blockchain #blockchaintechnology #区块链 #共识算法 #Web3 #web30technology #web30 #pos #dpos #pow #consensus 闲聊区块链, 很多方面讲得不是很详细, 轻喷. 主要是给媳妇普及一下区块链,...
  6. 简洁的 C# LINQ 写法 – 例子 1 LINQ 的全称是Language-Integrated Query, 在 .NET 2.0 之后就可以使用这种简洁的语法. 使用 LINQ 可以使代码变得简短, 清楚. 比如: 1 2 3...
  7. 币圈交易所安全实践 一周前,被骗1000英镑:在币圈第一次被骗1355 USDT(1000英镑)的惨痛经历(Wirex),近期在推上又被爆出两个比较大的事件,钱放交易所被黑客盗走,一夜清零。 在加密货币的世界中,安全至关重要。虽然区块链的去中心化特性提供了对许多类型欺诈的强大保护,但您用来买卖和持有数字资产的交易所可能容易受到黑客和其他安全漏洞的攻击。交易所一般是中心化的,也就是我们说的CEX(Centralize Exchange)。以下是确保您的加密资产在交易所中保持安全的综合指南。 如果很多币/钱,最好放在自己的本地钱包里(Not Your Keys, Not Your Funds),并且用硬件钱包(如Ledger),这样风险会小一些,不过很多人把币放交易所上就为了挣一些利息(交易所有很多DeFi项目),不过你得到的也许是利息,但可能失去的是本金。 我知道的HTX火币交易所就很不错,每次提款都需要三个验证:手机SMS短信、邮件验证,还有就是Google二维验证码。一些交易所在主帐号改密码24小时内是不能提现的,这也一定程度给予用户时间减少损失。 启用双重身份验证(2FA) 双重身份验证(2FA)是一个重要的安全措施,可以为您的账户增加一层额外的保护。以下是它的工作原理: 为什么需要2FA? 它不仅要求您的密码,还需要第二个因素,通常是发送到您手机的代码或由身份验证应用生成的代码。...
  8. 最好的给CPU降温方法就是通过改BIOS里的CPU风扇速度 家里的HPZ800服务器啥都好, 就是太吵. 之前显卡温度过高(可能是积灰的缘故)拿着电风扇对着机箱吹, 就更吵了. 最近我发现CPU温度也偏高, 因为一旦跑一些程序, CPU利用率上来了, CPU自动调节风扇就特别响, 像飞机起飞的. 而且温度也很容易升到80多度. 我查了一下, 因特志强X5650系列最高工作温度(健康温度)是81点3度. 想着不能就这样放任它不管, 于是想到了一招: 重启F10进BIOS设置,...

直观解读 JuiceFS 的数据和元数据设计(三):看山还是山(2024)

本系列分为三篇文章,试图通过简单的实地环境来直观理解 JuiceFS 的数据(data)和元数据(metadata)设计。

Fig. JuiceFS object key naming and the objects in MinIO.

水平及维护精力所限,文中不免存在错误或过时之处,请酌情参考。 传播知识,尊重劳动,年满十八周岁,转载请注明出处



1 如何从数据和元数据中恢复文件

1.2 理论步骤

对于一个给定的 JuiceFS 文件,我们在上一篇中已经看到两个正向的过程:

  1. 文件本身被切分成 Chunk、Slice、Block,然后写入对象存储;
  2. 文件的元数据以 inode、slice、block 等信息组织,写入元数据引擎。

有了对正向过程的理解,我们反过来就能从对象存储和元数据引擎中恢复文件: 对于一个给定的 JuiceFS 文件,

  1. 首先扫描元数据引擎,通过文件名、inode、slice 等等信息,拼凑出文件的大小、位置、权限等等信息;
  2. 然后根据 slice_id/block_id/block_size 拼凑出对象存储中的 object key;
  3. 依次去对象存储中根据这些 keys 读取数据拼到一起,得到的就是这个文件,然后写到本地、设置文件权限等等。

但这个恢复过程不是本文重点。本文主要看几个相关的问题,以加深对 JuiceFS 数据/元数据 设计的理解。 更多信息见官方文档 [2]。

1.2 juicefs info 查看文件 chunk/slice/block 信息

JuiceFS 已经提供了一个命令行选项,能直接查看文件的 chunk/slice/block 信息,例如:

$ ./juicefs info foo-dev/file2_5MB
foo-dev/file2_5MB :
  inode: 3
  files: 1
   dirs: 0
 length: 5.00 MiB (5242880 Bytes)
   size: 5.00 MiB (5242880 Bytes)
   path: /file2_5MB
 objects:
+------------+--------------------------------+---------+--------+---------+
| chunkIndex |           objectName           |   size  | offset |  length |
+------------+--------------------------------+---------+--------+---------+
|          0 | foo-dev/chunks/0/0/3_0_4194304 | 4194304 |      0 | 4194304 |
|          0 | foo-dev/chunks/0/0/3_1_1048576 | 1048576 |      0 | 1048576 |
+------------+--------------------------------+---------+--------+---------+

和我们在 MinIO 中看到的一致。

2 如何判断 {volume}/chunks/ 中的数据是否是合法

bucket 中的数据是 JuiceFS 写入的,还是其他应用写入的呢? 另外即使是 JuiceFS 写入的,也可能有一些数据是无效的,比如 size 为 0 的 block、超出所属 slice 范围的 block 等等。 我们来看看基于哪些规则,能对这些非法数据进行判断。

2.1 原理

准备工作:

  1. 从 JuiceFS 的元数据引擎中读取所有 slice size,这对应的是元数据信息
  2. 从 object storage 中读取所有 object key,这对应的数据信息

接下来,根据几条标准,判断 bucket 中 {volume}/chunks/ 内的数据是否是合法的 JuiceFS 数据:

  1. 如果 object 不符合命名规范 {volume}/chunks/{slice_id/1000/1000}/{slice_id/1000}/{slice_id}_{block_id}_{block_size}, 那么这个 object 就不是 JuiceFS 写入的;
  2. 如果符合以上命名规范,,那么这个 object 就是 JuiceFS 写入的,接下来,
    1. 如果 object 大小为零,那可以清理掉,因为这种 object 留着没意义;
    2. 如果 object 大小不为零,根据元数据内记录的 slice/block 信息计算这个 block 应该是多大,
      1. 如果大小跟 object 一致,那这个 object 就是一个合法的 JuiceFS 数据(Block);
      2. 否则,说明这个 object 有问题。

这个过程是没问题的,但需要对所有 object 和所有元数据进行遍历和比对,效率比较低。 有没有更快的方法呢?

2.2 改进:pending delete slices

回忆上一篇,在元数据引擎中其实已经记录了待删除的 slice/block 信息, 这里“待删除”的意思是 JuiceFS 中已经把文件删掉了(用户看不到了,volume usage 统计也不显示了), 但还没有从对象存储中删掉,

  • D 开头的记录:deleted inodes
  • 格式:D{8bit-inode}{8bit-length}

这种记录是 JuiceFS 在从 object storage 删除文件之前插入到元数据引擎中的, 所以扫描所有 D 开头的记录,可以找到所有待删除的 slice/block 信息。

2.3 工具:juicefs gc

结合 2.1 & 2.2,就可以快速判断 bucket 中的数据是否是 JuiceFS 合法数据,不是就删掉; 基于 juicefs 已有的代码库,就可以写一个工具 —— 但用不着自己写 —— JuiceFS 已经提供了。

2.3.1 核心代码

完整代码见 pkg/cmd/gc.go

从元数据引擎 list 所有 slice 信息

func (m *kvMeta) ListSlices(ctx Context, slices map[Ino][]Slice, delete bool, showProgress func()) syscall.Errno {
    if delete
        m.doCleanupSlices()

    // 格式:A{8digit-inode}C{4digit-blockID}   file chunks
    klen := 1 + 8 + 1 + 4
    result := m.scanValues(m.fmtKey("A"), -1, func(k, v []byte) bool { return len(k) == klen && k[1+8] == 'C' })

    for key, value := range result {
        inode := m.decodeInode([]byte(key)[1:9])
        ss := readSliceBuf(value) // slice list
        for _, s := range ss
            if s.id > 0
                slices[inode] = append(slices[inode], Slice{Id: s.id, Size: s.size})
    }

    if m.getFormat().TrashDays == 0
        return 0

    return errno(m.scanTrashSlices(ctx, func(ss []Slice, _ int64) (bool, error) {
        slices[1] = append(slices[1], ss...)
        if showProgress != nil
            for range ss
                showProgress()
        return false, nil
    }))
}

从对象存储 list 所有 objects 信息

    // Scan all objects to find leaked ones
    blob = object.WithPrefix(blob, "chunks/")
    objs := osync.ListAll(blob, "", "", "", true) // List {vol_name}/chunks/ 下面所有对象

遍历所有 objects,跟元数据引擎中的 slice 信息比对

    for obj := range objs {
        // key 格式:{slice_id/1000/1000}/{slice_id/1000}/{slice_id}_{index}_{size}
        parts := strings.Split(obj.Key(), "/")     // len(parts) == 3
        parts = strings.Split(parts[2], "_")       // len(parts) == 3

        sliceID, _ := strconv.Atoi(parts[0])       // slice id, JuiceFS globally unique
        blockID, _ := strconv.Atoi(parts[1])       // blockID in this slice
        blockSize, _ := strconv.Atoi(parts[2])     // block size, <= 4MB
        sliceSizeFromMetaEngine := sliceSizesFromMetaEngine[uint64(sliceID)]       // tikv 中记录的 slice size

        var isEmptySize bool
        if sliceSizeFromMetaEngine == 0 {
            sliceSizeFromMetaEngine = sliceSizesFromTrash[uint64(sliceID)]
            isEmptySize = true
        }
        if sliceSizeFromMetaEngine == 0 {
            foundLeaked(obj)
            continue
        }

        if blockSize == chunkConf.BlockSize { // exactly 4MB
            if (blockID+1)*blockSize > sliceSizeFromMetaEngine
                foundLeaked(obj)
        } else {                              // < 4MB
            if blockID*chunkConf.BlockSize+blockSize != sliceSizeFromMetaEngine 
                foundLeaked(obj)
        }
  1. slice size 为 0,说明这个 slice 在元数据引擎中被 compact 过了;
  2. slice size 非零,
    • block size == 4MB,可能是也可能不是最后一个 block;
    • block size != 4MB,说明这个 block 是最后一个 block;

2.3.2 使用方式

$ ./juicefs gc -h
NAME:
   juicefs gc - Garbage collector of objects in data storage

USAGE:
   juicefs gc [command options] META-URL

大致效果:

$ ./juicefs gc tikv://192.168.1.1:2379,192.168.1.2:2379,192.168.1.3:2379/foo-dev
<INFO>: TiKV gc interval is set to 3h0m0s [tkv_tikv.go:138]
<INFO>: Data use minio://localhost:9000/juicefs-bucket/foo-dev/ [gc.go:101]

Pending deleted files: 0                             0.0/s
 Pending deleted data: 0.0 b     (0 Bytes)           0.0 b/s
Cleaned pending files: 0                             0.0/s
 Cleaned pending data: 0.0 b     (0 Bytes)           0.0 b/s
        Listed slices: 6                             327.3/s
         Trash slices: 0                             0.0/s
           Trash data: 0.0 b     (0 Bytes)           0.0 b/s
 Cleaned trash slices: 0                             0.0/s
   Cleaned trash data: 0.0 b     (0 Bytes)           0.0 b/s
      Scanned objects: 37/37 [=================================]  8775.9/s used: 4.268971ms
        Valid objects: 37                            11416.0/s
           Valid data: 134.0 MiB (140509216 Bytes)   41.0 GiB/s
    Compacted objects: 0                             0.0/s
       Compacted data: 0.0 b     (0 Bytes)           0.0 b/s
       Leaked objects: 0                             0.0/s
          Leaked data: 0.0 b     (0 Bytes)           0.0 b/s
      Skipped objects: 0                             0.0/s
         Skipped data: 0.0 b     (0 Bytes)           0.0 b/s

<INFO>: scanned 37 objects, 37 valid, 0 compacted (0 bytes), 0 leaked (0 bytes), 0 delslices (0 bytes), 0 delfiles (0 bytes), 0 skipped (0 bytes) [gc.go:379]

3 问题讨论

3.1 chunk id 和 slice id 的分配

  1. 每个文件都是从 chunk0 开始的;
  2. 实际上没有 chunk id 的概念,只是在查找文件的过程中动态使用,并没有存储到数据和元数据中;

代码里就是直接根据 64MB 计算下一个 chunk id,接下来的读写都是 slice 维度的, slice id 是全局唯一的,会存储到数据(object key)和元数据(tikv keys/values)中。

下一个可用的 sliceID 和 inodeID 记录在 global unique 变量中,初始化:

Register("tikv", newKVMeta)                  // pkg/meta/tkv_tikv.go
                 |-newBaseMeta(addr, conf)   // pkg/meta/tkv.go
                   |-newBaseMeta(addr, conf) // pkg/meta/base.go
                     |-.freeInodes // initialized as default value of type `freeID`
                     |-.freeSlices // initialized as default value of type `freeID`

然后,以写文件为例,调用栈:

Write(off uint64, data)
  |-if f.totalSlices() >= 1000 {
  |     wait a while
  | }
  |-chunkID := uint32(off / meta.ChunkSize) // chunk index, or chunk id
  |-pos := uint32(off % meta.ChunkSize)     // position inside the chunk for writing
  |-for len(data) > 0 {
  |   |-writeChunk
  |       |-c := f.findChunk(chunkID)
  |       |-s := c.findWritableSlice(off, uint32(len(data)))
  |       |-if no wriatable slice {
  |       |     s = &sliceWriter{chunk: c, off: off, }
  |       |     go s.prepareID(meta.Background, false) // pkg/vfs/writer.go
  |       |           |-NewSlice
  |       |               |-*id = m.freeSlices.next    // globally unique ID
  |       |
  |       |     c.slices = append(c.slices, s)
  |       |     if len(c.slices) == 1 {
  |       |         f.refs++
  |       |         go c.commitThread()
  |       |     }
  |       |-}
  |       |-return s.write(ctx, off-s.off, data)
  |         NewSlice // pkg/meta/base.go
  |-}

3.2 JuiceFS pending delete slices 和 background job

3.2.1 设计初衷

引入 pending delete slices 主要是大批量删除场景的性能优化

  1. 每个 JuiceFS 客户端只允许并发 100 的删除操作;
  2. 超过 100 时,自动放入后台队列,由 background job 异步删除;

3.2.2 代码

// pkg/meta/base.go

func (m *baseMeta) fileDeleted(opened, force bool, inode Ino, length uint64) {
    if opened
        m.removedFiles[inode] = true
    else
        m.tryDeleteFileData(inode, length, force)
}

func (m *baseMeta) tryDeleteFileData(inode Ino, length uint64, force bool) {
    if force {
        m.maxDeleting <- struct{}{}
    } else {
        select {
        case m.maxDeleting <- struct{}{}: // maxDeleting 没满,直接删
        default:                          // maxDeleting 满了之后走到这里,直接返回,靠后台任务删
            return // will be cleanup later
        }
    }

    go func() {
        m.en.doDeleteFileData(inode, length)
        <-m.maxDeleting
    }()
}

这个 maxDeleting 初始为一个 100 的 buffered channel,每次删除文件时,会尝试往里面放一个元素,

// pkg/meta/base.go

func newBaseMeta(addr string, conf *Config) *baseMeta {
    return &baseMeta{
        sid:          conf.Sid,
        removedFiles: make(map[Ino]bool),
        compacting:   make(map[uint64]bool),
        maxDeleting:  make(chan struct{}, 100), // 代码里写死了 100
        ...

3.2.3 潜在的问题

后台删除是 JuiceFS client 中的 background job 做的,这个 background job 的开关是可配置的,

$ ./juicefs mount --no-bgjob ... # 关闭 background job

这个开关的控制有点 tricky:

  1. 打开:如果一个 volume 的客户端太多,大家都会去做后台清理,都获取文件锁,对元数据引擎的压力非常大;
  2. 关闭:没有客户端去做后台清理,导致这些文件一直存在于对象存在中,也可以称为文件泄露,使用成本上升。

一种折中的做法:

  • 客户端不太多的 volumes:默认启用 bgjob;
  • 客户端太多的 volumes,默认关闭 bgjob,然后指定特定的 client 开启 bgjob,代表这个 volume 的所有客户端执行清理操作。

3.3 JuiceFS 支持的单个最大文件 128PiB 是怎么来的

从以上定义可以看到,理论上 JuiceFS 支持的单个文件大小是 maxSliceID (int64) * maxChunkSize, 以默认的 maxChunkSize=64MB(2^26 Byte)为例,

  • 理论上限:2^63 * 2^26 = 2^(63+26) Byte
  • 实际上限:2^31 * 2^26 = 2^(31+26) Byte = 128PiB,这个数字来自官方文档

实际上限是 128PiB 的原因也很简单,在代码里写死了

// pkg/vfs/vfs.go

const (
    maxFileSize = meta.ChunkSize << 31
)

3.4 为什么 JuiceFS 写入对象存储的文件,不能通过对象存储直接读取?

这里说的“不能读取”,是指不能直接读出原文件给到用户,而不是说不能读取 objects。

看过本文应该很清楚了,JuiceFS 写入对象存储的文件是按照 Chunk、Slice、Block 进行切分的, 只有数据内容,且保护重复数据,还没有文件信息元信息(文件名等)。

所以,以对象的存储的方式只能读这些 objects,是无法恢复出原文件给到用户的。

3.5 JuiceFS 不会对文件进行合并

Highlight:JuiceFS 不会文件进行合并写入对象存储, 这是为了避免读放大

4 总结

至此,我们对 JuiceFS 数据和元数据设计的探索学习就告一段落了。希望有了这些知识, 用户和工程师在日常的使用和维护 JuiceFS 过程中,看问题和解决问题能更加得心应手。

参考资料

  1. 官方文档:JuiceFS 如何存储文件, juicefs.com
  2. 官方文档:文件数据格式, juicefs.com

Written by Human, Not by AI Written by Human, Not by AI

直观解读 JuiceFS 的数据和元数据设计(二):看山不是山(2024)

本系列分为三篇文章,试图通过简单的实地环境来直观理解 JuiceFS 的数据(data)和元数据(metadata)设计。

Fig. JuiceFS object key naming and the objects in MinIO.

水平及维护精力所限,文中不免存在错误或过时之处,请酌情参考。 传播知识,尊重劳动,年满十八周岁,转载请注明出处



1 引言

上一篇从功能的角度体验了下 JuiceFS,这一篇我们深入到背后,看看 JuiceFS 分别在数据和元数据上做了哪些设计,才给到用户和本地文件系统一样的体验的。

2 对象存储中 JuiceFS 写入的文件

本篇以 MinIO 为例,来看 JuiceFS 写入到对象存储中的文件是怎样组织的。 其他云厂商的对象存储(AWS S3、阿里云 OSS 等)也都是类似的。

2.1 Bucket 内:每个 volume 一个“目录”

可以用上一篇介绍的 juicefs format 命令再创建两个 volume,方便观察它们在 bucket 中的组织关系,

Fig. MinIO bucket browser: volume list.

如上图所示,bucket 内的顶层“目录”就是 JuiceFS 的 volumes

我们这里提到“目录”时加双引号,是因为对象存储是扁平的 key-value 存储,没有目录的概念, 前端展示时模拟出目录结构(key 前缀一样的,把这个前缀作为一个“目录”)是为了查看和理解方便。 简单起见,后文不再加双引号。

2.2 每个 volume 的目录: {chunks/, juicefs_uuid, meta/, ...}

每个 volume 目录内的结构如下:

{volume_name}/
  |-chunks/         # 数据目录,volume 中的所有用户数据都放在这里面
  |-juicefs_uuid    
  |-meta/           # `juicefs mount --backup-meta ...` 产生的元数据备份存放的目录

2.2.1 juicefs_uuid:JuiceFS volume 的唯一标识

可以把这个文件下载下来查看内容,会发现里面存放的就是 juicefs format 输出里看到的那个 uuid, 也就是这个 volume 的唯一标识。

删除 volume 时需要用到这个 uuid。

2.2.2 meta/:JuiceFS 元数据备份

如果在 juicefs mount 时指定了 --backup-meta,JuiceFS 就会定期把元数据(存在在 TiKV 中)备份到这个目录中, 用途:

  1. 元数据引擎故障时,可以从这里恢复;
  2. 在不同元数据引擎之间迁移元数据。

详见 JuiceFS 元数据引擎五探:元数据备份与恢复(2024)

2.2.3 chunks/

Fig. MinIO bucket browser: files in a bucket.

chunks/ 内的目录结构如下,

{volume_name}/
  |-chunks/
  |   |-0/                # <-- id1 = slice_id / 1000 / 1000
  |   |  |-0/             # <-- id2 = slice_id / 1000
  |   |     |-1_0_16      # <-- {slice_id}_{block_id}_{size_of_this_block}
  |   |     |-3_0_4194304 #
  |   |     |-3_1_1048576 #
  |   |     |-...
  |-juicefs_uuid    
  |-meta/

如上,所有的文件在 bucket 中都是用数字命名和存放的,分为三个层级:

  1. 第一层级:纯数字,是 sliceID 除以 100 万得到的;
  2. 第二层级:纯数字,是 sliceID 除以 1000 得到的;
  3. 第三层级:纯数字加下划线,{slice_id}_{block_id}_{size_of_this_block},表示的是这个 chunk 的这个 slice 内的 block_id 和 block 的大小。

不理解 chunk/slice/block 这几个概念没关系,我们马上将要介绍。

2.3 小结

通过以上 bucket 页面,我们非常直观地看到了一个 JuiceFS volume 的所有数据在对象存储中是如何组织的

接下来进入正题,了解一下 JuiceFS 的数据和元数据设计。

3 JuiceFS 数据的设计

3.1 顶层切分:一切文件先切 chunk

对于每个文件,JuiceFS 首先会按固定大小(64MB)切大块, 这些大块称为「Chunk」。

  • 这是为了读或修改文件内容时,方便查找和定位
  • 不管是一个只有几字节的文本文件,还是一个几十 GB 的视频文件, 在 JuiceFS 中都是切分成 chunk,只是 chunk 的数量不同而已。

3.1.1 示意图

Fig. JuiceFS: split each file into their respective chunks (with max chunk size 64MB).

3.1.2 对象存储:不存在 chunk 实体

结合上一节在对象存储中看到的目录结构,

{volume_name}/
  |-chunks/
  |   |-0/                # <-- id1 = slice_id / 1000 / 1000
  |   |  |-0/             # <-- id2 = slice_id / 1000
  |   |     |-1_0_16      # <-- {slice_id}_{block_id}_{size_of_this_block}
  |   |     |-3_0_4194304 #
  |   |     |-3_1_1048576 #
  |   |     |-...
  |-juicefs_uuid    
  |-meta/
  1. Chunk 在对象存储中 没有对应任何实际文件,也就是说在对象存储中没有一个个 64MB 的 chunks
  2. 用 JuiceFS 的话来说,Chunk 是一个逻辑概念。暂时不理解没关系,接着往下看。

3.2 Chunk 内的一次连续写入:Slice

chunk 只是一个“框”,在这个框里面对应文件读写的,是 JuiceFS 称为「Slice」 的东西。

  • chunk 内的一次连续写入,会创建一个 slice,对应这段连续写入的数据;
  • 由于 slice 是 chunk 内的概念,因此它不能跨 Chunk 边界,长度也不会超 max chunk size 64M。
  • slice ID 是全局唯一的;

3.2.1 Slice 的重叠问题

根据写入行为的不同,一个 Chunk 内可能会有多个 Slice,

  • 如果文件是由一次连贯的顺序写生成,那每个 Chunk 只包含一个 Slice
  • 如果文件是多次追加写,每次追加均调用 flush 触发写入上传,就会产生多个 Slice

Fig. JuiceFS: chunks are composed of slices, each slice corresponds to a continues write operation.

拿 chunk1 为例,

  1. 用户先写了一段 ~30MB 数据,产生 slice5
  2. 过了一会,从 ~20MB 的地方重新开始写 45MB(删掉了原文件的最后一小部分,然后开始追加写),
    • chunk1 内的部分产生 slice6
    • 超出 chunk1 的部分,因为 slice 不能跨 chunk 边界,因此产生 chunk2slice7
  3. 过了一会,从 chunk1 ~10MB 的地方开始修改(覆盖写),产生 slice8

由于 Slice 存在重叠,因此引入了几个字段标识它的有效数据范围,

// pkg/meta/slice.go

type slice struct {
    id    uint64
    size  uint32
    off   uint32
    len   uint32
    pos   uint32
    left  *slice // 这个字段不会存储到 TiKV 中
    right *slice // 这个字段不会存储到 TiKV 中
}

3.2.2 读 chunk 数据时的多 slice 处理:碎片化和碎片合并

Fig. JuiceFS: chunks are composed of slices, each slice corresponds to a continues write operation.

对 JuiceFS 用户来说,文件永远只有一个,但在 JuiceFS 内部,这个文件对应的 Chunk 可能会有多个重叠的 Slice,

  • 有重叠的部分,以最后一次写入的为准。
  • 直观上来说,就是上图 chunk 中的 slices 从上往下看,被盖掉的部分都是无效的

因此,读文件时,需要查找「当前读取范围内最新写入的 Slice」,

  • 在大量重叠 Slice 的情况下,这会显著影响读性能,称为文件「碎片化」。
  • 碎片化不仅影响读性能,还会在对象存储、元数据等层面增加空间占用。
  • 每当写入发生时,客户端都会判断文件的碎片化情况,并异步地运行碎片合并,将一个 Chunk 内的所有 Slice 合并。

3.2.3 对象存储:不存在 slice 实体

跟 chunk 类似,在对象存储中 slice 也没有 没有对应实际文件

{volume_name}/
  |-chunks/
  |   |-0/                # <-- id1 = slice_id / 1000 / 1000
  |   |  |-0/             # <-- id2 = slice_id / 1000
  |   |     |-1_0_16      # <-- {slice_id}_{block_id}_{size_of_this_block}
  |   |     |-3_0_4194304 #
  |   |     |-3_1_1048576 #
  |   |     |-...
  |-juicefs_uuid    
  |-meta/

3.3 Slice 切分成固定大小 Block(e.g. 4MB):并发读写对象存储

为了加速写到对象存储,JuiceFS 将 Slice 进一步拆分成一个个「Block」(默认 4MB),多线程并发写入。

Fig. JuiceFS: slices are composed of blocks (4MB by default), each block is an object in object storage.

Block 是 JuiceFS 数据切分设计中最后一个层级,也是 chunk/slice/block 三个层级中唯一能在 bucket 中看到对应文件的

Fig. MinIO bucket browser: objects in a bucket.

  • 连续写:前面 Block 默认都是 4MB,最后一个 Block 剩多少是多少。
  • 追加写:数据不足 4MB 时,最终存入对象存储的也会是一个小于 4M 的 Block。

从上图的名字和大小其实可以看出分别对应我们哪个文件:

  1. 1_0_16:对应我们的 file1_1KB
    • 我们上一篇的的追加写 echo "hello" >> file1_1KB 并不是写入了 1_0_16, 而是创建了一个新对象 7_0_16,这个 object list 最后面,所以在截图中没显示出来;
    • 换句话说,我们的 file1_1KB 虽然只有两行内容,但在 MinIO 中对应的却是两个 object,各包含一行。
    • 通过这个例子,大家可以体会到 JuiceFS 中连续写和追加写的巨大区别
  2. 3_0_4194304 + 3_1_1048576:总共 5MB,对应我们的 file2_5MB
  3. 4_*:对应我们的 file3_129MB

3.4 object key 命名格式(及代码)

格式:{volume}/chunks/{id1}/{id2}/{slice_id}_{block_id}_{size_of_this_block},对应的代码,

// pkg/chunk/cached_store.go

func (s *rSlice) key(blockID int) string {
    if s.store.conf.HashPrefix  // false by default
        return fmt.Sprintf("chunks/%02X/%v/%v_%v_%v", s.id%256, s.id/1000/1000, s.id, blockID, s.blockSize(blockID))

    return fmt.Sprintf("chunks/%v/%v/%v_%v_%v", s.id/1000/1000, s.id/1000, s.id, blockID, s.blockSize(blockID))
}

3.5 将 chunk/slice/block 对应到对象存储

最后,我们将 volume 的数据切分和组织方式对应到 MinIO 中的路径和 objects,

Fig. JuiceFS object key naming and the objects in MinIO.

3.6 小结:光靠对象存储数据和 slice/block 信息无法还原文件

至此,JuiceFS 解决了数据如何切分和存放的问题,这是一个正向的过程:用户创建一个文件,我们能按这个格式切分、命名、上传到对象存储。

对应的反向过程是:给定对象存储中的 objects,我们如何将其还原成用户的文件呢? 显然,光靠 objects 名字中包含的 slice/block ID 信息是不够的,例如,

  1. 最简单情况下,每个 chunk 都没有任何 slice 重叠问题,那我们能够根据 object 名字中的 slice_id/block_id/block_size 信息拼凑出一个文件, 但仍然无法知道这个文件的文件名、路径(父目录)、文件权限(rwx)等等信息;
  2. chunk 一旦存在 slice 重叠,光靠对象存储中的信息就无法还原文件了;
  3. 软链接、硬链接、文件属性等信息,更是无法从对象存储中还原。

解决这个反向过程,我们就需要文件的一些元数据作为辅助 —— 这些信息在文件切分和写入对象存储之前,已经记录到 JuiceFS 的元数据引擎中了。

4 JuiceFS 元数据的设计(TKV 版)

JuiceFS 支持不同类型的元数据引擎,例如 Redis、MySQL、TiKV/etcd 等等,每种类型的元数据引擎都有自己的 key 命名规则。 本文讨论的是 JuiceFS 使用 transactional key-value(TKV)类型的元数据引擎时的 key 命名规则。

更具体地,我们将拿 TiKV 作为元数据引擎来研究。

4.1 TKV 类型 key 列表

这里的 key 是 JuiceFS 定义元数据 key,key/value 写入元数据引擎; 请注意跟前面提到的对象存储 key 区别开,那个 key/value 是写入对象存储的

key 是一个字符串,所有 key 的列表,

// pkg/meta/tkv.go

  setting                           format
  C{name}                           counter
  A{8byte-inode}I                   inode attribute
  A{8byte-inode}D{name}             dentry
  A{8byte-inode}P{8byte-inode}      parents // for hard links
  A{8byte-inode}C{4byte-blockID}    file chunks
  A{8byte-inode}S                   symlink target
  A{8byte-inode}X{name}             extented attribute
  D{8byte-inode}{8byte-length}      deleted inodes
  F{8byte-inode}                    Flocks
  P{8byte-inode}                    POSIX locks
  K{8byte-sliceID}{8byte-blockID}   slice refs
  Ltttttttt{8byte-sliceID}          delayed slices
  SE{8byte-sessionID}               session expire time
  SH{8byte-sessionID}               session heartbeat // for legacy client
  SI{8byte-sessionID}               session info
  SS{8byte-sessionID}{8byte-inode}  sustained inode
  U{8byte-inode}                    usage of data length, space and inodes in directory
  N{8byte-inode}                    detached inde
  QD{8byte-inode}                   directory quota
  R{4byte-aclID}                    POSIX acl

在 TKV 的 Keys 中,所有整数都以编码后的二进制形式存储 [2]:

  • inode 和 counter value 占 8 个字节,使用小端编码
  • SessionID、sliceID 和 timestamp 占 8 个字节,使用大端编码

setting 是一个特殊的 key,对应的 value 就是这个 volume 的设置信息。 前面的 JuiceFS 元数据引擎系列文章中介绍过 [3],这里不再赘述。

其他的,每个 key 的首字母可以快速区分 key 的类型,

  • C:counter,这里面又包含很多种类,例如 name 可以是:
    • nextChunk
    • nextInode
    • nextSession
  • A:inode attribute
  • D:deleted inodes
  • F:Flocks
  • P:POSIX lock
  • S:session related
  • K:slice ref
  • L: delayed (to be deleted?) slices
  • U:usage of data length, space and inodes in directory
  • N:detached inode
  • QD:directory quota
  • R:POSIX acl

需要注意的是,这里是 JuiceFS 定义的 key 格式,在实际将 key/value 写入元数据引擎时, 元数据引擎可能会对 key 再次进行编码,例如 TiKV 就会在 key 中再插入一些自己的字符。 前面的 JuiceFS 元数据引擎系列文章中也介绍过,这里不再赘述。

4.2 元数据引擎中的 key/value

4.2.1 扫描相关的 TiKV key

TiKV 的 scan 操作类似 etcd 的 list prefix,这里扫描所有 foo-dev volume 相关的 key,

$ ./tikv-ctl.sh scan --from 'zfoo-dev' --to 'zfoo-dew'
key: zfoo-dev\375\377A\000\000\000\020\377\377\377\377\177I\000\000\000\000\000\000\371
key: zfoo-dev\375\377A\001\000\000\000\000\000\000\377\000Dfile1_\3771KB\000\000\000\000\000\372
key: zfoo-dev\375\377A\001\000\000\000\000\000\000\377\000Dfile2_\3775MB\000\000\000\000\000\372
...
key: zfoo-dev\375\377SI\000\000\000\000\000\000\377\000\001\000\000\000\000\000\000\371
        default cf value: start_ts: 453485726123950084 value: 7B225665727369...33537387D
key: zfoo-dev\375\377U\001\000\000\000\000\000\000\377\000\000\000\000\000\000\000\000\370
key: zfoo-dev\375\377setting\000\376
        default cf value: start_ts: 453485722598113282 value: 7B0A224E616D65223A202266...0A7D

4.2.2 解码成 JuiceFS metadata key

tikv-ctl --decode <key> 可以解码出来,注意去掉最前面的 z,得到的就是 JuiceFS 的原始 key,看着会更清楚一点,

foo-dev\375A\000\000\000\020\377\377\377\177I
foo-dev\375A\001\000\000\000\000\000\000\000Dfile1_1KB
foo-dev\375A\001\000\000\000\000\000\000\000Dfile2_5MB
foo-dev\375A\001\000\000\000\000\000\000\000Dfile3_129MB
foo-dev\375A\001\000\000\000\000\000\000\000I
foo-dev\375A\002\000\000\000\000\000\000\000C\000\000\000\000
foo-dev\375A\002\000\000\000\000\000\000\000I
foo-dev\375A\003\000\000\000\000\000\000\000C\000\000\000\000
foo-dev\375A\003\000\000\000\000\000\000\000I
foo-dev\375A\004\000\000\000\000\000\000\000C\000\000\000\000
foo-dev\375A\004\000\000\000\000\000\000\000C\000\000\000\001
foo-dev\375A\004\000\000\000\000\000\000\000C\000\000\000\002
foo-dev\375A\004\000\000\000\000\000\000\000I
foo-dev\375ClastCleanupFiles
foo-dev\375ClastCleanupSessions
foo-dev\375ClastCleanupTrash
foo-dev\375CnextChunk
foo-dev\375CnextCleanupSlices
foo-dev\375CnextInode
foo-dev\375CnextSession
foo-dev\375CtotalInodes
foo-dev\375CusedSpace
foo-dev\375SE\000\000\000\000\000\000\000\001
foo-dev\375SI\000\000\000\000\000\000\000\001
foo-dev\375U\001\000\000\000\000\000\000\000
foo-dev\375setting

从上面的 keys,可以看到我们创建的三个文件的元信息了, 这里面是用 slice_id 等信息关联的,所以能和对象存储里的数据 block 关联上

可以基于上一节的 key 编码规则进一步解码,得到更具体的 sliceID/inode 等等信息,这里我们暂时就不展开了。

5 总结

这一篇我们深入到 JuiceFS 内部,从数据和元数据存储中的东西反观 JuiceFS 切分数据和记录元数据的设计。 站在这个层次看,已经跟前一篇的理解程度全然不同。

如果说第一篇是“见自己”(功能如所见),这第二篇就是“见天(元数据引擎)地(对象存储)”, 那必然还得有一篇“见众生”。

参考资料

  1. 官方文档:JuiceFS 如何存储文件, juicefs.com
  2. 官方文档:JuiceFS 开发:内部实现, juicefs.com
  3. JuiceFS 元数据引擎初探:高层架构、引擎选型、读写工作流(2024)

Written by Human, Not by AI Written by Human, Not by AI

直观解读 JuiceFS 的数据和元数据设计(一):看山是山(2024)

本系列分为三篇文章,试图通过简单的实地环境来直观理解 JuiceFS 的数据(data)和元数据(metadata)设计。

Fig. MinIO bucket browser: one object was created ({volume}/juicefs_uuid) on a new juicefs volume creation.

水平及维护精力所限,文中不免存在错误或过时之处,请酌情参考。 传播知识,尊重劳动,年满十八周岁,转载请注明出处



本篇首先快速了解下 JuiceFS 架构和组件,然后将搭建一个极简 JuiceFS 集群, 并以 JuiceFS 用户的身份来体验下它的基本功能。

1 JuiceFS 高层架构与组件

JuiceFS 的高层架构和组件,

Fig. JuiceFS cluster initialization, and how POSIX file operations are handled by JuiceFS.

三大组件:

  1. 元数据引擎:存储文件元数据,例如文件名、权限等。JuiceFS 支持多种元数据引擎,比如 TiKV、sqlite、redis 等。
  2. 对象存储:存储文件本身。JuiceFS 支持多种对象存储,比如 MinIO、AWS S3、阿里云 OSS 等。
  3. JuiceFS 客户端:将 JuiceFS volume 挂载到机器上,提供文件系统视图给用户。

更多架构信息,见 [1]。

2 搭建极简 JuiceFS 集群

接下来搭建一个极简 JuiceFS 环境,方便我们做一些功能测试。 按上一节提到的,只需要搭建以下 3 个组件:

  1. 元数据引擎,这里我们用 TiKV
  2. 对象存储,这里我们用 MinIO
  3. JuiceFS 客户端。

2.1 搭建元数据集群

对于功能测试来说,使用哪种元数据引擎都无所谓,比如最简单的 sqlite 或 redis。

不过,本系列第二篇会介绍 TiKV 相关的一些设计,所以本文用的 TiKV 集群作为元数据引擎, 相关的搭建步骤见社区文档。

本篇假设搭建的是三节点的 TiKV 集群,IP 地址分别是 192.168.1.{1,2,3}

2.2 搭建对象存储(MinIO)

这里我们用 MinIO 搭建一个对象存储服务,主要是空集群方便观察其中的文件变化

2.2.1 启动 MinIO server

MinIO 是一个兼容 S3 接口的开源对象存储产品,部署非常简单,就一个可执行文件,下载执行就行了。

也可以用容器,一条命令启动:

$ sudo docker run -p 9000:9000 -p 8080:8080 \
    quay.io/minio/minio server /data --console-address "0.0.0.0:8080"

访问 http://localhost:8080/ 就能看到 MinIO 的管理界面了。默认账号密码都是 minioadmin

2.2.2 创建 bucket

通过 MinIO 管理界面创建一个 bucket,这里我们命名为 juicefs-bucket

Fig. MinIO bucket list: an empty bucket.

可以看到现在里面一个对象也没有,已使用空间也是 0 字节

2.3 下载 juicefs 客户端

从 https://github.com/juicedata/juicefs/releases 下载一个可执行文件就行了,

$ wget https://github.com/juicedata/juicefs/releases/download/v1.2.1/juicefs-1.2.1-linux-amd64.tar.gz
$ tar -xvf juicefs-1.2.1-linux-amd64.tar.gz
$ chmod +x juicefs

2.4 创建 JuiceFS volume

接下来就可以创建一个 JuiceFS volume 了,这里命名为 foo-dev

2.4.1 创建/格式化 volume:juicefs format

$ juicefs format --storage minio --bucket http://localhost:9000/juicefs-bucket \
        --access-key minioadmin \
        --secret-key minioadmin \
        tikv://192.168.1.1:2379,192.168.1.2:2379,192.168.1.3:2379/foo-dev  \
        foo-dev

<INFO>: Meta address: tikv://192.168.1.1:2379,192.168.1.2:2379,192.168.1.3:2379/foo-dev [interface.go:504]
<INFO>: Data use minio://localhost:9000/juicefs-bucket/foo-dev/ [format.go:528]
<INFO>: Volume is formatted as {
  "Name": "foo-dev",
  "UUID": "3b4e509b-a7c8-456f-b726-cb8395cf8eb6",
  "Storage": "minio",
  "Bucket": "http://localhost:9000/juicefs-bucket",
  "AccessKey": "minioadmin",
  "SecretKey": "removed",
  "BlockSize": 4096,
  "UploadLimit": 0,
  "DownloadLimit": 0,
  ...
}

2.4.2 查看 MinIO bucket:多了一个 juicefs_uuid 文件

再查看 MinIO bucket,会发现多了一个 object,

Fig. MinIO bucket browser: one object was created on a new juicefs volume creation.

点进去,发现是一个叫 juicefs_uuid 的文件,

Fig. MinIO bucket browser: one object was created after juicefs format.

可以把这个文件下载下来,其内容就是上面 juicefs format 命令输出的 uuid 信息,也就是说 juicefs client 会把 volume 的 uuid 上传到对象存储中。

3 将 JuiceFS volume 挂载到本地路径

这么我们将这个 volume 挂载到本地路径 /tmp/foo-dev

$ ./juicefs mount --debug --backup-meta 0 \
     tikv://192.168.1.1:2379,192.168.1.2:2379,192.168.1.3:2379/foo-dev /tmp/foo-dev

[INFO] [client.go:405] ["[pd] create pd client with endpoints"] [component=tikv] [pid=2881678] [pd-address="[192.168.1.1:2379,192.168.1.2:2379,192.168.1.3:2379]"]
[INFO] [base_client.go:378] ["[pd] switch leader"] [component=tikv] [pid=2881678] [new-leader=https://192.168.1.3:2379] [old-leader=]
[INFO] [base_client.go:105] ["[pd] init cluster id"] [component=tikv] [pid=2881678] [cluster-id=7418858894192002550]
[INFO] [client.go:698] ["[pd] tso dispatcher created"] [component=tikv] [pid=2881678] [dc-location=global]
<INFO>: Data use minio://localhost:9000/juicefs-bucket/foo-dev/ [mount.go:650]
...

进入目录:

$ cd /tmp/foo-dev
$ ls -ahl
-r--------  1 root root    0 Oct 26 10:45 .accesslog
-r--------  1 root root 2.9K Oct 26 10:45 .config
-r--r--r--  1 root root    0 Oct 26 10:45 .stats
dr-xr-xr-x  2 root root    0 Oct 26 10:45 .trash

可以看到几个隐藏文件,

  • 这些是 JuiceFS 的元数据文件,在 [1] 系列文章中有过详细介绍。
  • 这些都是 volume 本地文件,不会上传到 MinIO。此时,MinIO juicefs-bucket 里面还是只有一个 uuid 文件。

4 在 JuiceFS volume 挂载的本地路径内读写

接下来进行一些 POSIX 操作测试。

4.1 创建和写入文件

创建三个文件,一个只有几十字节(但命名为 file1_1KB), 一个 5MB,一个 129MB

$ cd /tmp/foo-dev

$ echo "Hello, JuiceFS!" > file1_1KB

$ dd if=/dev/zero of=file2_5MB bs=1M count=5
5+0 records in
5+0 records out
5242880 bytes (5.2 MB, 5.0 MiB) copied, 0.0461253 s, 114 MB/s

$ dd if=/dev/zero of=file3_129MB bs=1M count=129
129+0 records in
129+0 records out
135266304 bytes (135 MB, 129 MiB) copied, 0.648757 s, 209 MB/s

4.2 查看文件属性

$ ls -ahl file*
-rw-r----- 1 root root   16  file1_1KB
-rw-r----- 1 root root 5.0M  file2_5MB
-rw-r----- 1 root root 129M  file3_129MB

$ file file2_5MB
file2_5MB: data

4.3 读取和追加文件

$ cat file1_1KB
Hello, JuiceFS!

$ echo "Hello, JuiceFS!" >> file1_1KB
$ cat file1_1KB
Hello, JuiceFS!
Hello, JuiceFS!

4.4 查找文件

$ find /tmp -name file1_1KB
/tmp/foo-dev/file1_1KB

4.5 删除文件

直接用 rm 删除就行了,不过这几个文件我们还有用,先不删。

4.6 目录操作

目录的创建、移动、修改权限、删除等待也是一样的,大家可以自己试试,这里不再赘述。

4.7 小结

根据以上测试,在 JuiceFS 挂载路径里创建/读写/查找/删除文件,都跟本地目录没什么区别 —— 这也正是「分布式“文件系统”」的意义所在 —— 兼容 POSIX 语义,用户无需关心数据存在哪, 当本地目录使用就行了(性能另当别论)。

5 总结

本篇中,我们作为 JuiceFS 用户对它进行了一些最基本的功能测试,结论是和本地文件系统没什么区别。

对于普通用户来说,了解到这一层就够了; 但对于高阶用户以及 JuiceFS 的开发/运维来说,这只是表象,必有第二重境界等着他们。

参考资料

  1. JuiceFS 元数据引擎初探:高层架构、引擎选型、读写工作流(2024)

Written by Human, Not by AI Written by Human, Not by AI

无奈清空GE/美国通用电气的股票


今天我清空了手中的美国通用电气(GE)股票。五年前加入GE通用电气的时候,每个月都用部分工资购买了公司股票,并且享受了公司15%的配股。离职时,不得不卖出大约25%的股票来交税。好在这几年GE重组,拆分成了三家公司,并按比例发放了相应的股票,不然真是亏惨了。近期由于家庭装修支出超出了预算,只能忍痛将这些股票套现来补贴家用。另一个原因是管理这些股票的公司每年要收50美元的管理费,股息根本不够支付,实在让人无奈。

GE分成了三个独立的公司:GE HealthCare、GE Vernova和GE Aerospace/航空。

GE-3-companies-stock-price-2024-10-16 无奈清空GE/美国通用电气的股票 小确幸 投资 美国通用电气 股市 财商 资讯 跟我一起来谈钱

美国通用电气拆分成三个公司的股价

由于GE拆分成三个公司,送了两次股票,卖股票的时候得卖了三次,每次卖都有手续费 + 转到银行的费用,肉疼了三次。

交易费用是2.24% – 银行转帐费用是10美元。

ge-stock-shares-account-history 无奈清空GE/美国通用电气的股票 小确幸 投资 美国通用电气 股市 财商 资讯 跟我一起来谈钱

五年前开始持有GE股票,今天清空

GE 无奈清空GE/美国通用电气的股票 小确幸 投资 美国通用电气 股市 财商 资讯 跟我一起来谈钱

General Electric 美国通用电气

equate-mobile-general-electric-stock-scaled 无奈清空GE/美国通用电气的股票 小确幸 投资 美国通用电气 股市 财商 资讯 跟我一起来谈钱

清空了美国通用电气的股票

general-electric 无奈清空GE/美国通用电气的股票 小确幸 投资 美国通用电气 股市 财商 资讯 跟我一起来谈钱

美国通用电气 GE (General Electric) Imagination at work

清空了帐号,感觉才真正的和美国通用电气说再见。从今天开始,不再是GE的股东了,竟然有一点伤感。GE 2000年初 还是世界第一的公司,现在成这鸟样(GE各种卖资产,最后面还被拆分),一个时代的没落。

更新:前天卖GE股票,今天就到帐(RBS苏格兰皇家银行),算了一下回报率,5年 平均每年 33% APR,相当不错了。我本金没多少,每年还得交50美元,股息就几块钱,然后50美元是扣股数来交的,感觉很肉疼,所以才没有继续持有。

美国通用电气 General Electric

本文一共 548 个汉字, 你数一下对不对.
无奈清空GE/美国通用电气的股票. (AMP 移动加速版本)

扫描二维码,分享本文到微信朋友圈
75a5a60b9cac61e5c8c71a96e17f2d9c 无奈清空GE/美国通用电气的股票 小确幸 投资 美国通用电气 股市 财商 资讯 跟我一起来谈钱
The post 无奈清空GE/美国通用电气的股票 first appeared on 小赖子的英国生活和资讯.

相关文章:

  1. 智能手机 HTC One M9 使用测评 虽然我对手机要求不高, 远远没有像追求VPS服务器一样, 但是怎么算来两年内换了四个手机, 先是三星 S4 用了一年多, 然后 Nokia Lumia 635 Windows Phone, 后来又是 BLU, 半年多前换了...
  2. 按揭贷款(房贷,车贷) 每月还贷计算器 去年给银行借了17万英镑 买了20万7500英镑的房子, 25年还清. 前2年是定率 Fix Rate 的合同 (年利率2.49%). 每个月大概是还 700多英镑. 有很多种还贷的计算方式, 定率/每月固定 是比较常用的. 简单来说就是 每个月交的钱是...
  3. 同一台服务器上多个WORDPRESS站点的一些设置可以移出去 我自从把所有网站都挪到一处VPS服务器上 就发现很多事情省事很多 可以同时管理多个网站 包括 WORDPRESS博客. 比如我有四个WORDPRESS博客 然后我就把通用的一些资料给移出去 移到 HTTP或者HTTPS都不能直接访问的文件夹里这样就更安全许多. 文件 wp-conn.php 存储了 相同的数据库资料. 1 2...
  4. 在英国开车的简单介绍/英国开车上路需要准备什么? 在英国合法上路需要有: 有效的驾照; MOT 车的年检; 路税 (Road Tax);还有最重要的汽车保险; 四者缺一不可. 千万不要有侥幸心理, 因为警察现在都高科技, 都能扫描车牌就能知道你合不合法. 不合法直接拦下来轻则罚款, 重则扣车上述法庭. 驾照 在英国可以用欧盟的大部分驾照,...
  5. 怎样换发中国旅行证/Travel Document? 卡中国大使馆BUG, 英国中国双国籍 前几年,我给娃换了英国护照,因为他们在英国出生,在英国学习和生活,短期也没有想回国的打算,拿着英国护照会方便一些(比如学校会组织学生去欧洲旅行)。孩子是我在拿英国永居前出生的,所以最开始是有中国护照的,只不过我后来拿了英国绿卡后,他们可以申请入英国国籍(当年费用是一次性交个1000多英镑)。 中国政府不承认双国籍,拿了英国护照后中国护照就作废了。不过由于孩子没有自主做决定的能力,所以可以给娃申请中国旅行证,疫情前去伦敦中国大使馆给娃第一次申请了中国旅行证,当时材料比较复杂,得父母的各种信息:什么时候拿到的英国绿卡,什么时候生的娃,什么时候结婚等等。孩子也得带到大使馆面签,很是麻烦。 由于疫情,虽然当年给娃申请到了中国旅行证/Travel Document,但是一直没有回国,两年有效期就这么过去了。这次再次给娃换发中国旅行证,发现申请过程简单了很多。 怎样换发中国旅行证/Travel Document? 首先,只需要在手机APP上申请,需要下载一个“中国领事”的手机应用,创建一个帐号,然后需要上传身份证/护照证明身份,然后就是点击开始旅行证换发,根据要求填写资料,拍照上传证件。 无论走到哪里,祖国在你身后,服务以你同行! 点击提交,会有一个人工审核/初审的过程,材料不够会打回让补齐,比如需要根据模板写一个未成年人父母授权的说明等。我第一次填写名字的时候少写了弟弟的中文名,结果就被拒了,得重新申请一份。 提交初审通过后,有一个视频验证,在APP里和大使馆约了一个时间,视频验证主要就是看看娃,然后还有就是需要给大使馆看看家里的插座,主要是为了确定人在英国。初审过后就需要把材料寄到大使馆指定地址(驻英国使馆:Chinese Embassy(Consular Section), 49-51 Portland Place,...
  6. 公司给配了台高配DELL笔记本 早上例会结束的时候我顺便说了一句 我的笔记本有点慢, 当时我并不知道我的经理远程用电话也参加会议了(他全程在听), senior staff SE 对着电话说, “peter, you hear that? btw, my disks are...
  7. 优化设计 个人主页 并且 PageSpeed Insights 双项 100分 坛子的个人主页 www.tanzhijun.com 不错 很适合个人主页的模板. 而且是手机友好. 于是我照着把 我的主页改了改. https://steakovercooked.com 并且做了几点修改: 0. 使用 google mod_pagespeed 把 JS,...
  8. 老婆的配偶签证被拒 郁闷死了, 601镑签证费打水漂,一去不回!费钱费力. 去年12月份我请了律师拿到了永居.老婆是T1G签证的陪工签 (DEPENDENT VISA) 2016年4月份到期. 然后我就想说得趁早把她的签证转成配偶签(SPOUSE)这样她就可以尽快走五年永居的路线. 今天收到拒签信,原因是我没有提供 有工资进帐的那份银行帐单,我提供了我和我老婆的联名帐户, 但是工资并不是直接打到这个帐单上的.所以就这一点被拒了.完全不给解释,不给补材料的机会.601镑就这样再见了. 英国的签证寄出之后是先由另一个部门先收费, 收完费才正式审理,而且不管结果如何是不退钱的.后悔没让律师弄,也不至于到现在浪费这么多时间和金钱,签证还没过.由于原签证还没到期,所以还不能上述.估计只能等搬完家后年底请律师搞定这事. 真是郁闷, 600镑, 我可以再买一个IPHONE6,或者给我的新买的车换四个轮胎....

JuiceFS 元数据引擎五探:元数据备份与恢复(2024)

Fig. TiKV backup with different CLI tools (and their problems).

水平及维护精力所限,文中不免存在错误或过时之处,请酌情参考。 传播知识,尊重劳动,年满十八周岁,转载请注明出处



1 JuiceFS 元数据备份方式

再复习下 JuiceFS 架构,如下图所示:

Fig. JuiceFS cluster initialization, and how POSIX file operations are handled by JuiceFS.

JuiceFS 的元数据都存储在元数据引擎(例如,TiKV)里, 因此元数据的备份有两种实现方式:

  1. 从上层备份:JuiceFS client 扫描 volume,将 volume 内所有元数据备份;
  2. 元数据引擎(例如 TiKV)备份。

下面分别看看这两种方式。

2 JuiceFS 自带方式(volume 级别)

2.1 juicefs dump 手动备份 volume metadata

对指定 volume 进行备份,

$ juicefs dump tikv://ip:2379/foo-dev foo-dev-dump.json
<INFO>: Meta address: tikv://<ip>:2379/foo-dev [interface.go:406]
<WARNING>: Secret key is removed for the sake of safety [tkv.go:2571]
           Scan keys count: 357806 / 357806 [===========================]  done
      Dumped entries count: 122527 / 122527 [===========================]  done
<INFO>: Dump metadata into dump succeed [dump.go:76]

生成的是一个 JSON 文件,包含了 volume 的所有元数据信息,

{  "Setting": {
    "Name": "foo-dev",
    "UUID": "ca95c258",
    "Storage": "OSS",
    "Bucket": "http://<url>",
    "AccessKey": "ak",
    "BlockSize": 4096,
    "Capacity": 0,
    "Inodes": 0,
    "MetaVersion": 0,
    "MinClientVersion": "",
    "MaxClientVersion": "",
  },
  "Counters": {
    "usedSpace": 6164512768,
    "usedInodes": 5010,
    "nextInodes": 10402,
    "nextChunk": 25001,
    "nextSession": 118,
  },
  "FSTree": {
    "attr": {"inode":1,"type":"directory","mode":511,"atime":1645791488,"mtime":1652433235,"ctime":1652433235,"mtimensec":553010494,"ctimensec":553010494,"nlink":2,"length":0},
    "xattrs": [{"name":"lastBackup","value":"2024-05-30T13:50:25+08:00"}],
    "entries": {
      "001eb8b": {
        "attr": {...},
        "chunks": [{"index":0,"slices":[{"chunkid":15931,"size":32,"len":32}]}]
        ...
      }
   }
  },
}

其中,volume 中的所有文件和目录信息都描述在 FSTree 字段中。

2.2 juicefs mount --backup-meta <duration> 自动备份

juicefs client 默认会自动备份 volume 的元数据,

  • 备份间隔通过 --backup-meta {duration} 选项控制,默认 1h
  • 备份文件在对象存储的 meta 特殊目录中,该目录在挂载点中不可见,用对象存储的文件浏览器可以查看和管理,
  • 多 client 挂载同一个 volume 也不会发生备份冲突,因为 JuiceFS 维护了一个全局的时间戳,确保同一时刻只有一个客户端执行备份操作,但是,
  • 当文件数太多(默认达到 100w)且备份频率为默认值 1h 时,为避免备份开销太大,JuiceFS 会自动停止元数据备份,并打印相应的告警。

2.3 juicefs load 从元数据备份文件恢复

$ juicefs load tikv://<ip>:2379/foo-dev-new foo-dev-dump.json

2.4 限制及问题

根据官方文档,以上两种方式都有一些限制或问题:

  • 导出过程中如果业务仍在写入,导出的文件可能不可用。如果对一致性有更高要求,需要在导出前停写。
  • 对规模较大的 volume,直接在线上进行导出可能会影响业务稳定性。

另外,以上方式都是 volume 级别的备份,如果要备份整个 JuiceFS 集群,需要逐个 volume 备份,比较麻烦。 下面再看看直接从元数据引擎进行备份的方式。

3 从 TiKV 层面对 JuiceFS 元数据进行备份

这里假设 JuiceFS 的元数据引擎是 TiKV。

3.1 TiKV backup/restore 原理

从上层来说,很简单:

  1. 发请求给 TiKV 集群的管理者 PD,让它对集群的所有数据进行备份;

  2. 接下来,PD 会发请求给集群的所有 TiKV 节点,通知它们各自进行备份

    • TiKV 是按 region 进行多副本存储的,因此只需要一个副本进行备份就行了,
    • 在当前的设计里面就是让每个 region 的 leader 副本进行备份,
  3. TiKV region leaders 把这个 region 内的数据写到指定位置。可以是本地磁盘或分布式存储。

3.2 备份工具 TiDB br 和 TiKV tikv-br

理论上,有两个工具可能实现以上效果,它们分别来自 TiDB 和 TiKV 社区,

Fig. TiKV backup with different CLI tools (and their problems).

  1. br:以前是个独立项目(图中 A.1),后来合到 tidb 仓库里了(图中 B.1),

    这个工具主要是给 TiDB 备份用的(虽然底层备份的是 TiDB 的 TiKV), 所以需要一些 TiDB 知识(上下文),例如 db/table 都是 TiDB 才有的概念。 理论上,它也能备份独立部署的 TiKV 集群(“不依赖 TiDB 的 TiKV”), 所以加了 raw/txn 支持,但不是 TiDB 社区的重点,所以目前还是 experimental 特性,且用下来有 bug。

  2. tikv-br:是个独立项目,应该是当时 TiKV 作为独立项目推进时,想搞一个配套的独立备份工具, 但目前看起来跟 TiKV 社区一样已经不活跃了,它也没法对 txn 进行备份(JuiceFS 用的 txnkv 接口)。

至少对于 5.x TiKV 集群,测试下来以上哪个工具都无法完成备份: 有的工具备份和恢复都提示完成,看起来是成功的,但实际上是失败的,JuiceFS 挂载时才能发现。

最后,我们是基于目前(2024.09)最新的 TiDB br,修改了两个地方,才成功完成 TiKV 的备份与恢复。

3.3 基于 TiDB br 对 JuiceFS TiKV 集群进行备份与恢复的步骤

之所以要强调 “JuiceFS TiKV 集群”,是因为 JuiceFS 用的 txnkv 接口,这个比较特殊; 如果是 rawkv 接口,那 tikv 自带的备份工具 tikv-br 也许就能用了(没测过)。

  1. (可选)关闭 TiKV MVCC GC;
  2. br 执行备份,

    br-dev 是我们基于最新 master(202409)改过的版本。

     $ ./br-dev backup txn \
             --ca /tmp/pki/root.crt --cert /tmp/pki/pd.crt --key /tmp/pki/pd.key \
             --pd https://$pd_addr \
             --s3.endpoint $s3_addr \
             --storage $storage_path \
             --log-file /var/log/tikv/br.log \
             --ratelimit $bw_limit_per_node \
             --log-level debug \
             --check-requirements=false
    

    可以设置限速等参数,避免备份占用的 CPU/Memory/DiskIO/… 过大。根据 db size 等等因素,备份的耗时是可估算的,下面拿一个真实集群的备份为例:

    • 每个 TiKV 的 DB size:监控能看到,一般每个节点的 DB size 都差不太多,这里是 25GB per TiKV node;
    • MVCC 保留了数据的多个版本:假设平均保留两个版本,那就是 DB size * 2
    • 限速带宽:设置为 30MB/s,这个带宽不算大,不会是磁盘和网络瓶颈,因此可以全速运行

    根据以上参数,估算耗时:25GB * 2 / 30MBps = 1700s = 28min

    Fig. TiKV backup resource usage with br --ratelimit=30MB/s.

    可以看到跟预估的差不多。资源销毁方面:

    • CPU 利用率比平时翻倍
    • 其中两台机器的 CPU 数量比较少,所以会比其他节点更明显。
  3. 检查备份

    如果是备份到 S3,可以用 s3cmd 或 web 控制台查看,

     $ s3cmd du s3://{bucket}/<backup>/
     295655971082   18513 objects s3://{bucket}/<backup>/
    

    290GB 左右,比监控看到的 DB size 大一倍,因为保留了 MVCC 多版本。 大多少倍与 MVCC GC 间隔有密切关系, 比如写或更新很频繁的场景,1h 和 3h 的 MVCC 数据量就差很多了。

  4. br 恢复:将备份数据恢复到一个新的 JuiceFS TiKV 集群,

     $ ./br-dev restore txn \
             --ca /tmp/pki/root.crt --cert /tmp/pki/pd.crt --key /tmp/pki/pd.key \
             --pd https://$pd_addr \
             --s3.endpoint $s3_addr \
             --storage $storage_path \
             --log-file /var/log/tikv/br.log \
             --ratelimit $bw_limit_per_node \
             --log-level debug \
             --check-requirements=false
    

    可能的问题:ratelimit 好像不起作用,全速恢复,网络带宽打的很高。

  5. JuiceFS client 挂载,验证恢复成功

    用 juicefs 挂载目录,指定新 TiKV 集群的 PD 地址,

     $ juicefs mount tikv://<new-pd-ip>:2379/<volume name> /tmp/test
    
     $ cd /tmp/test && ls
     # 原来 volume 内的文件都在
    

3.4 TiDB br 备份逻辑

感兴趣的可以看看 br 源码的备份逻辑,

3.4.1 RunBackupTxn()

// tidb br/pkg/task/backup_txn.go

// RunBackupTxn starts a backup task inside the current goroutine.
func RunBackupTxn(c context.Context, g glue.Glue, cmdName string, cfg *TxnKvConfig) error {
    mgr := NewMgr(ctx, g, cfg.PD, cfg.TLS, GetKeepalive(&cfg.Config), cfg.CheckRequirements, false)
    client := backup.NewBackupClient(ctx, mgr)

    backupRanges := make([]rtree.Range, 0, 1)

    // current just build full txn range to support full txn backup
    minStartKey := []byte{}
    maxEndKey := []byte{}
    backupRanges = append(backupRanges, rtree.Range{
        StartKey: minStartKey,
        EndKey:   maxEndKey,
    })

    // Backup
    req := backuppb.BackupRequest{
        ClusterId:        client.GetClusterID(),
        StartVersion:     0,
        EndVersion:       client.GetCurrentTS(ctx), // gets a new timestamp (TSO) from PD
        RateLimit:        cfg.RateLimit,
        Concurrency:      cfg.Concurrency,
        StorageBackend:   client.GetStorageBackend(),
        IsRawKv:          false,
    }

    ranges, schemas, policies := client.BuildBackupRangeAndSchema(mgr.GetStorage(), cfg.TableFilter, backupTS, isFullBackup(cmdName))

    // StartWriteMetasAsync writes four kind of meta into backupmeta.
    // 1. file
    // 2. schema
    // 3. ddl
    // 4. rawRange( raw kv )
    metaWriter := metautil.NewMetaWriter(client.GetStorage(), metautil.MetaFileSize, false, metautil.MetaFile, &cfg.CipherInfo)
    metaWriter.StartWriteMetasAsync(ctx, metautil.AppendDataFile)

    // Start TiKV backup
    client.BackupRanges(ctx, backupRanges, req, 1, nil, metaWriter, progressCallBack)

    // Backup has finished
    metaWriter.Update(func(m *backuppb.BackupMeta) {
        m.StartVersion = req.StartVersion
        m.EndVersion = req.EndVersion
        m.IsRawKv = false
        m.IsTxnKv = true
        m.ClusterId = req.ClusterId
        m.ClusterVersion = mgr.GetClusterVersion(ctx)
        m.BrVersion = brVersion
        m.ApiVersion = client.GetApiVersion()
    })
    metaWriter.FinishWriteMetas(ctx, metautil.AppendDataFile)
    metaWriter.FlushBackupMeta(ctx)
}

几点说明:

  1. KV 的 backup range 是全量(start/end key 都是空);
  2. MVCC 的 start/end version 分别是 0 和当前 PD 最新的 TSO;

3.4.2 调用栈

BackupRanges // make a backup of the given key ranges.
  |-mainBackupLoop := &MainBackupLoop
  |     BackupSender:       &MainBackupSender{},
  |     BackupReq:          request,
  |     Concurrency:        concurrency,
  |     GlobalProgressTree: &globalProgressTree,
  |     ReplicaReadLabel:   replicaReadLabel,
  |     GetBackupClientCallBack: func(ctx , storeID uint64, reset bool) (backuppb.BackupClient, error) {
  |         return bc.mgr.GetBackupClient(ctx, storeID)
  |     },
  | }
  |-bc.RunLoop(ctx, mainBackupLoop) // infinite loop to backup ranges on all tikv stores
      |-for {
          inCompleteRanges = iter.GetIncompleteRanges() // 还未完成备份的 key 范围
          loop.BackupReq.SubRanges = getBackupRanges(inCompleteRanges)
          allStores := bc.getBackupStores(mainCtx, loop.ReplicaReadLabel)
          for _, store := range allStores {
            cli := loop.GetBackupClientCallBack(mainCtx, storeID, reset)
            loop.SendAsync(round, storeID, loop.BackupReq, loop.Concurrency, cli, ch, loop.StateNotifier)
                  |-go startBackup(storeID, request, cli, concurrency, respCh)
                        |-for i, req := range reqs {
                            doSendBackup(ectx, backupCli, bkReq, ...)
                              |-ctx, timerecv := StartTimeoutRecv(pctx, TimeoutOneResponse)
                              |-bCli := client.Backup(ctx, &req) // protobuf grpc method
                              |-for {
                              |-    resp := bCli.Recv()
                              |-    timerecv.Refresh()
                              |-    respFn(resp)
                              |-}
          }
        }

3.4.3 tikv-server 备份代码

// components/backup/src/service.rs

impl<H> Backup for Service<H>
{
    fn backup(
        req: BackupRequest,
        mut sink: ServerStreamingSink<BackupResponse>,
    ) {
        if let Err(status) = match Task::new(req, tx) {
            Ok((task, c)) => {
                self.scheduler.schedule(task)
            }
        }

        let send_task = async move {
            let mut s = rx.map(|resp| Ok((resp, WriteFlags::default())));
            sink.send_all(&mut s).await?;
        }

        ctx.spawn(send_task);
    }
}
/// Backup Task.
pub struct Task {
    request: Request,
    pub(crate) resp: UnboundedSender<BackupResponse>,
}

// components/backup/src/endpoint.rs
impl Task {
    /// Create a backup task based on the given backup request.
    pub fn new(
        req: BackupRequest,
        resp: UnboundedSender<BackupResponse>,
    ) -> Result<(Task, Arc<AtomicBool>)> {
        let speed_limit = req.get_rate_limit();
        let limiter = Limiter::new(if speed_limit > 0  else f64::INFINITY });
        let cf = name_to_cf(req.get_cf())

        let task = Task {
            request: Request {
                start_key: req.get_start_key().to_owned(),
                end_key: req.get_end_key().to_owned(),
                sub_ranges: req.get_sub_ranges().to_owned(),
                start_ts: req.get_start_version().into(),
                end_ts: req.get_end_version().into(),
                backend: req.get_storage_backend().clone(),
                limiter,
                is_raw_kv: req.get_is_raw_kv(),
                dst_api_ver: req.get_dst_api_version(),
                cf,
                replica_read: req.get_replica_read(),
                resource_group_name: .get_resource_group_name().to_owned(),
                }),
            },
            resp,
        };
    }
}

// components/backup/src/endpoint.rs
BackupRanges -> BackupWriterBuilder -> S3Uploader

self.writer.put(&data_key_write, value) -> s3 put

参考资料

  1. 官方文档:元数据备份和恢复, juicefs.com

Written by Human, Not by AI Written by Human, Not by AI

用WinMerge进行文件夹比较时忽略git目录的方法

在选择比较目录目录的对话框上,点击左下方【Folder:filter/文件夹:过滤器】处的【Select…/选择】按钮,然后在【File Filters/文件过滤器】选项卡中选中【Ignore git】,确定,即可。
进入比较后,这一项也有办法解决。点击菜单【Tools/工具】->【Filters/过滤器】后,同样在【File Filters/文件过滤器】选项卡中选中【Ignore git】,确定,然后刷新重新比较,亦可。


  • (1):其实两周后的决赛李明又喂了一次屎,不过那场比赛不如这场有戏剧性。
  • (2):进第二个球的那个赖特后来在假A沈阳队踢过。
  • (3):饭店里并没有天津本地品牌“五星”
  • (4):章鱼
  • (5):海胆
  • (6):不像《侍魂》和《天外魔境》那样还需要把武器拾回来,而是每个人的武器都能飞去来。

JuiceFS 元数据引擎四探:元数据大小评估、限流与限速的设计思考(2024)

Fig. JuiceFS upload/download data bandwidth control.

水平及维护精力所限,文中不免存在错误或过时之处,请酌情参考。 传播知识,尊重劳动,年满十八周岁,转载请注明出处



1 元数据存储在哪儿?文件名到 TiKV regions 的映射

1.1 pd-ctl region 列出所有 region 信息

$ pd-ctl.sh region | jq .
{
  "regions": [
    {
      "id": 11501,
      "start_key": "6161616161616161FF2D61692D6661742DFF6261636B7570FD41FFCF68030000000000FF4900000000000000F8",
      "end_key": "...",
      "epoch": {
        "conf_ver": 23,
        "version": 300
      },
      "peers": [
        {
          "id": 19038,
          "store_id": 19001,
          "role_name": "Voter"
        },
        ...
      ],
      "leader": {
        "id": 20070,
        "store_id": 20001,
        "role_name": "Voter"
      },
      "written_bytes": 0,
      "read_bytes": 0,
      "written_keys": 0,
      "read_keys": 0,
      "approximate_size": 104,
      "approximate_keys": 994812
    },
  ]
}

1.2 tikv-ctl region-properties 查看 region 属性详情

$ ./tikv-ctl.sh region-properties -r 23293
mvcc.min_ts: 438155461254971396
mvcc.max_ts: 452403302095650819
mvcc.num_rows: 1972540
mvcc.num_puts: 3697509
mvcc.num_deletes: 834889
mvcc.num_versions: 4532503
mvcc.max_row_versions: 54738
num_entries: 4549844
num_deletes: 17341
num_files: 6
sst_files: 001857.sst, 001856.sst, 002222.sst, 002201.sst, 002238.sst, 002233.sst
region.start_key: 6e6772...
region.end_key: 6e6772...
region.middle_key_by_approximate_size: 6e6772...

1.3 tikv-ctl --to-escaped:从 region 的 start/end key 解码文件名范围

如上,每个 region 都会有 start_key/end_key 两个属性, 这里面编码的就是这个 region 内存放是元数据的 key 范围。我们挑一个来解码看看:

$ tikv-ctl.sh --to-escaped '6161616161616161FF2D61692D6661742DFF6261636B7570FD41FFCF68030000000000FF4900000000000000F8'
aaaaaaaa\377-ai-fat-\377backup\375A\377\317h\003\000\000\000\000\000\377I\000\000\000\000\000\000\000\370

再 decode 一把会更清楚:

$ tikv-ctl.sh --decode 'aaaaaaaa\377-ai-fat-\377backup\375A\377\317h\003\000\000\000\000\000\377I\000\000\000\000\000\000\000\370'
aaaaaaaa-ai-fat-backup\375A\317h\003\000\000\000\000\000I

对应的是一个名为 aaaaaaa-ai-fat-backup 的 volume 内的一部分元数据。

1.4 filename -> region:相关代码

这里看一下从文件名映射到 TiKV region 的代码。

PD 客户端代码,

    // GetRegion gets a region and its leader Peer from PD by key.
    // The region may expire after split. Caller is responsible for caching and
    // taking care of region change.
    // Also, it may return nil if PD finds no Region for the key temporarily,
    // client should retry later.
    GetRegion(ctx , key []byte, opts ...GetRegionOption) (*Region, error)

// GetRegion implements the RPCClient interface.
func (c *client) GetRegion(ctx , key []byte, opts ...GetRegionOption) (*Region, error) {
    options := &GetRegionOp{}
    for _, opt := range opts {
        opt(options)
    }
    req := &pdpb.GetRegionRequest{
        Header:      c.requestHeader(),
        RegionKey:   key,
        NeedBuckets: options.needBuckets,
    }
    serviceClient, cctx := c.getRegionAPIClientAndContext(ctx, options.allowFollowerHandle && c.option.getEnableFollowerHandle())
    resp := pdpb.NewPDClient(serviceClient.GetClientConn()).GetRegion(cctx, req)
    return handleRegionResponse(resp), nil
}

PD 服务端代码,

func (h *regionHandler) GetRegion(w http.ResponseWriter, r *http.Request) {
    rc := getCluster(r)
    vars := mux.Vars(r)
    key := url.QueryUnescape(vars["key"])
    // decode hex if query has params with hex format
    paramsByte := [][]byte{[]byte(key)}
    paramsByte = apiutil.ParseHexKeys(r.URL.Query().Get("format"), paramsByte)

    regionInfo := rc.GetRegionByKey(paramsByte[0])
    b := response.MarshalRegionInfoJSON(r.Context(), regionInfo)

    h.rd.Data(w, http.StatusOK, b)
}

// GetRegionByKey searches RegionInfo from regionTree
func (r *RegionsInfo) GetRegionByKey(regionKey []byte) *RegionInfo {
    region := r.tree.search(regionKey)
    if region == nil {
        return nil
    }
    return r.getRegionLocked(region.GetID())
}

返回的是 region info,

// RegionInfo records detail region info for api usage.
// NOTE: This type is exported by HTTP API. Please pay more attention when modifying it.
// easyjson:json
type RegionInfo struct {
    ID          uint64              `json:"id"`
    StartKey    string              `json:"start_key"`
    EndKey      string              `json:"end_key"`
    RegionEpoch *metapb.RegionEpoch `json:"epoch,omitempty"`
    Peers       []MetaPeer          `json:"peers,omitempty"` // https://github.com/pingcap/kvproto/blob/master/pkg/metapb/metapb.pb.go#L734

    Leader            MetaPeer      `json:"leader,omitempty"`
    DownPeers         []PDPeerStats `json:"down_peers,omitempty"`
    PendingPeers      []MetaPeer    `json:"pending_peers,omitempty"`
    CPUUsage          uint64        `json:"cpu_usage"`
    WrittenBytes      uint64        `json:"written_bytes"`
    ReadBytes         uint64        `json:"read_bytes"`
    WrittenKeys       uint64        `json:"written_keys"`
    ReadKeys          uint64        `json:"read_keys"`
    ApproximateSize   int64         `json:"approximate_size"`
    ApproximateKeys   int64         `json:"approximate_keys"`
    ApproximateKvSize int64         `json:"approximate_kv_size"`
    Buckets           []string      `json:"buckets,omitempty"`

    ReplicationStatus *ReplicationStatus `json:"replication_status,omitempty"`
}

// GetRegionFromMember implements the RPCClient interface.
func (c *client) GetRegionFromMember(ctx , key []byte, memberURLs []string, _ ...GetRegionOption) (*Region, error) {
    for _, url := range memberURLs {
        conn := c.pdSvcDiscovery.GetOrCreateGRPCConn(url)
        cc := pdpb.NewPDClient(conn)
        resp = cc.GetRegion(ctx, &pdpb.GetRegionRequest{
            Header:    c.requestHeader(),
            RegionKey: key,
        })
        if resp != nil {
            break
        }
    }

    return handleRegionResponse(resp), nil
}

2 JuiceFS 集群规模与元数据大小(engine size)

2.1 二者的关系

一句话总结:并没有一个线性的关系

2.1.1 文件数量 & 平均文件大小

TiKV engine size 的大小,和集群的文件数量每个文件的大小都有关系。 例如,同样是一个文件,

  1. 小文件可能对应一条 TiKV 记录;
  2. 大文件会被拆分,对应多条 TiKV 记录。

2.1.2 MVCC GC 快慢

GC 的勤快与否也会显著影响 DB size 的大小。第三篇中有过详细讨论和验证了,这里不再赘述,

Fig. TiKV DB size soaring in a JuiceFS cluster, caused by TiKV GC lagging.

2.2 两个集群对比

  • 集群 1:~1PB 数据,以小文件为主,~30K regions,~140GB TiKV engine size (3 replicas);
  • 集群 2:~7PB 数据,以大文件为主,~800 regions,~3GB TiKV engine size (3 replicas);

如下面监控所示,虽然集群 2 的数据量是前者的 7 倍,但元数据只有前者的 1/47

Fig. TiKV DB sizes and region counts of 2 JuiceFS clusters: cluster-1 with ~1PB data composed of mainly small files, cluster-2 with ~7PB data composed of mainly large files.

3 限速(上传/下载数据带宽)设计

限速(upload/download bandwidth)本身是属于数据平面(data)的事情,也就是与 S3、Ceph、OSS 等等对象存储关系更密切。

但第二篇中已经看到,这个限速的配置信息是保存在元数据平面(metadata)TiKV 中 —— 具体来说就是 volume 的 setting 信息; 此外,后面讨论元数据请求限流(rate limiting)时还需要参考限速的设计。所以,这里我们稍微展开讲讲。

3.1 带宽限制:--upload-limit/--download-limit

  • --upload-limit,单位 Mbps
  • --download-limit,单位 Mbps

3.2 JuiceFS 限速行为

  1. 如果 juicefs mount 挂载时指定了这两个参数,就会以指定的参数为准;
  2. 如果 juicefs mount 挂载时没指定,就会以 TiKV 里面的配置为准,

    • juicefs client 里面有一个 refresh() 方法一直在监听 TiKV 里面的 Format 配置变化,
    • 当这俩配置发生变化时(可以通过 juicefs config 来修改 TiKV 中的配置信息),client 就会把最新配置 reload 到本地(本进程)
    • 这种情况下,可以看做是中心式配置的客户端限速,工作流如下图所示,

Fig. JuiceFS upload/download data bandwidth control.

3.3 JuiceFS client reload 配置的调用栈

juicefs mount 时注册一个 reload 方法,

mount
 |-metaCli.OnReload
    |-m.reloadCb = append(m.reloadCb, func() {
                                        updateFormat(c)(fmt) //  fmt 是从 TiKV 里面拉下来的最新配置
                                        store.UpdateLimit(fmt.UploadLimit, fmt.DownloadLimit)
                                      })

然后有个后台任务一直在监听 TiKV 里面的配置,一旦发现配置变了就会执行到上面注册的回调方法,

refresh()
  for {
        old := m.getFormat()
        format := m.Load(false) // load from tikv

        if !reflect.DeepEqual(format, old) {
            cbs := m.reloadCb
            for _, cb := range cbs {
                cb(format)
        }
  }

4 限流(metadata 请求)设计

4.1 为什么需要限流?

如下图所示,

Fig. JuiceFS cluster initialization, and how POSIX file operations are handled by JuiceFS.

  • 限速保护的是 5;
  • 限流保护的是 3 & 4

下面我们通过实际例子看看可能会打爆 3 & 4 的几种场景。

4.2 打爆 TiKV API 的几种场景

4.2.1 mlocate (updatedb) 等扫盘工具

一次故障复盘

下面的监控,左边是 TiKV 集群的请求数量,右边是 node CPU 利用率(主要是 PD leader 在用 CPU),

Fig. PD CPU soaring caused by too much requests.

大致时间线,

  • 14:30 开始,kv_get 请求突然飙升,导致 PD leader 节点的 CPU 利用率大幅飙升;
  • 14:40 介入调查,确定暴增的请求来自同一个 volume,但这个 volume 被几十个用户的 pod 挂载, 能联系到的用户均表示 14:30 没有特殊操作;
  • 14:30~16:30 继续联系其他用户咨询使用情况 + 主动排查;期间删掉了几个用户暂时不用的 pod,减少挂载这个 volume 的 juicefs client 数量,请求量有一定下降;
  • 16:30 定位到请求来源
    • 确定暴增的请求不是用户程序读写导致的
    • 客户端大部分都 ubuntu 容器(AI 训练),
    • 使用的是同一个容器镜像,里面自带了一个 daily 的定时 mlocate 任务去扫盘磁盘,

这个扫盘定时任务的时间是每天 14:30,因此把挂载到容器里的 JuiceFS volume 也顺带扫了。 确定这个原因之后,

  • 16:40 开始,逐步强制停掉(pkill -f updatedb.mlocate) 并禁用(mv /etc/cron.daily/mlocate /tmp/)这些扫盘任务, 看到请求就下来了,PD CPU 利用率也跟着降下来了;
  • 第二天早上 6:00 又发生了一次(凌晨 00:00 其实也有一次),后来排查发生是还有几个基础镜像也有这个任务,只是 daily 时间不同。

juicefs mount 时会自动禁用 mlocate,但 CSI 部署方式中部分失效

其实官方已经注意到了 mlocate,所以 juicefs mount 的入口代码就专门有检测,开了之后就自动关闭,

// cmd/mount_unix.go

func mountMain(v *vfs.VFS, c *cli.Context) {
    if os.Getuid() == 0 {
        disableUpdatedb()
          |-path := "/etc/updatedb.conf"
          |-file := os.Open(path)
          |-newdata := ...
          |-os.WriteFile(path, newdata, 0644)
    }
    ...

但是,在 K8s CSI 部署方式中,这个代码是部分失效的

Fig. JuiceFS K8s CSI deployment

JuiceFS per-node daemon 在创建 mount pod 时,会把宿主机的 /etc/updatedb.conf 挂载到 mount pod 里面, 所以它能禁掉宿主机上的 mlocate,

  volumes:
  - hostPath:
      path: /etc/updatedb.conf
      type: FileOrCreate
    name: updatedb

但正如上一小结的例子看到的,业务 pod 里如果开了 updatedb,它就管不到了。 而且业务容器很可能是同一个镜像启动大量 pod,挂载同一个 volume,所以扫描压力直线上升

4.2.2 版本控制工具

类似的工具可能还有版本控制工具(git、svn)、编程 IDE(vscode)等等,威力可能没这么大,但排查时需要留意。

4.3 需求:对元数据引擎的保护能力

以上 case,包括上一篇看到的用户疯狂 update 文件的 case,都暴露出同一个问题: JuiceFS 缺少对元数据引擎的保护能力。

4.3.1 现状:JuiceFS 目前还没有

社区版目前(2024.09)是没有的,企业版不知道有没有。

下面讨论下如果基于社区版,如何加上这种限流能力。

4.4 客户端限流方案设计

Fig. JuiceFS upload/download data bandwidth control.

基于 JuiceFS 已有的设计,再参考其限速实现,其实加上一个限流能力并不难,代码也不多:

  1. 扩展 Format 结构体,增加限流配置;
  2. juicefs format|config 增加配置项,允许配置具体限流值;这会将配置写到元数据引擎里面的 volume setting
  3. juicefs mount 里面解析 setting 里面的限流配置,传给 client 里面的 metadata 模块;
  4. metadata 模块做客户端限流,例如针对 txnkv 里面的不到 10 个方法,在函数最开始的地方增加一个限流检查,allow 再继续,否则就等待。

这是一种(中心式配置的)客户端限流方案。

4.5 服务端限流方案设计

在 TiKV 集群前面挡一层代理,在代理上做限流,属于服务端限流

参考资料

  1. 图解 JuiceFS CSI 工作流:K8s 创建带 PV 的 Pod 时,背后发生了什么(2024)

Written by Human, Not by AI Written by Human, Not by AI

JuiceFS 元数据引擎三探:从实践中学习 TiKV 的 MVCC 和 GC(2024)

Fig. TiKV MVCC GC mechanisms.

水平及维护精力所限,文中不免存在错误或过时之处,请酌情参考。 传播知识,尊重劳动,年满十八周岁,转载请注明出处



1 概念与实测

1.1 MVCC(多版本并发控制)

来自 wikipedia 的定义

Multiversion concurrency control (MCC or MVCC), is a non-locking concurrency control method commonly used by database management systems to provide concurrent access to the database and in programming languages to implement transactional memory.

TiKV 支持 MVCC,当更新数据时,旧的数据不会被立即删掉,而是新老同时保留,以时间戳来区分版本。 官方有几篇很不错的博客 [1,3]。

下面进行一个简单测试来对 MVCC 有一个初步的直观认识。

1.1.2 TiKV MVCC 测试

参考上一篇,新创建一个新 volume,里面什么文件都没有,有 8 条记录

$ tikv-ctl.sh scan --from 'zfoo' --to 'zfop' | grep "key:" | wc -l
8

然后进入这个 volume 的挂载目录,在里面创建一个文件

$ cd <mount dir>
$ echo 1 > foo.txt

再次扫描这个 volume 对应的所有 keys,

$ tikv-ctl.sh scan --from 'zfoo' --to 'zfop' | grep "key:" | wc -l
16

可以看到变成 16 条记录,比之前多了 8 条。内容如下,依稀能看出大部分条目的用途 (行末的注释是本文加的),

key: zfoo-dev\375\377A\001\000\000\000\000\000\000\377\000Dfoo.tx\377t\000\000\000\000\000\000\000\370 # foo.txt
key: zfoo-dev\375\377A\002\000\000\000\000\000\000\377\000C\000\000\000\000\000\000\375
key: zfoo-dev\375\377A\002\000\000\000\000\000\000\377\000I\000\000\000\000\000\000\371
key: zfoo-dev\375\377ClastCle\377anupFile\377s\000\000\000\000\000\000\000\370                         # lastCleanupFile
key: zfoo-dev\375\377ClastCle\377anupSess\377ions\000\000\000\000\373                                  # lastCleanupSessions
key: zfoo-dev\375\377CtotalIn\377odes\000\000\000\000\373                                              # totalInodes
key: zfoo-dev\375\377CusedSpa\377ce\000\000\000\000\000\000\371                                        # UsedSpace
key: zfoo-dev\375\377U\001\000\000\000\000\000\000\377\000\000\000\000\000\000\000\000\370

接下来继续更新这个文件 1000 次(每次都是一个整数,由于文件内容极小,不会导致 TiKV 的 region split 等行为),

$ for n in {1..1000}; do echo $n > bar.txt; done

再次查看元数据条目数量:

$ tikv-ctl.sh scan --from 'zfoo' --to 'zfop' | grep key | wc -l
59

多了 43 条。多的条目大致长这样:

key: zfoo-dev\375\377L\000\000\000\000f\356\221\377\231\000\000\000\000\000\000\000\3777\000\000\000\000\000\000\000\370
key: zfoo-dev\375\377L\000\000\000\000f\356\221\377\233\000\000\000\000\000\000\000\377j\000\000\000\000\000\000\000\370
key: zfoo-dev\375\377L\000\000\000\000f\356\221\377\234\000\000\000\000\000\000\000\377\235\000\000\000\000\000\000\000\370
...
key: zfoo-dev\375\377L\000\000\000\000f\356\221\377\271\000\000\000\000\000\000\003\377\362\000\000\000\000\000\000\000\370

TiKV supports MVCC, which means that there can be multiple versions for the same row stored in RocksDB. All versions of the same row share the same prefix (the row key) but have different timestamps as a suffix.

https://tikv.org/deep-dive/key-value-engine/rocksdb/

下面我们再看看执行以上文件更新操作期间,juicefs 客户端的日志。

1.1.2 JuiceFS client 日志

在执行以上 for 循环期间,JuiceFS client 的日志,

$ juicefs mount ...
...
<DEBUG>: PUT chunks/0/0/170_0_4 (req_id: "xx", err: <nil>, cost: 32.002516ms) [cached_store.go:669]
<DEBUG>: PUT chunks/0/0/171_0_4 (req_id: "xx", err: <nil>, cost: 32.002516ms) [cached_store.go:669]
<DEBUG>: PUT chunks/0/0/172_0_4 (req_id: "xx", err: <nil>, cost: 32.002516ms) [cached_store.go:669]
...

这个似乎对应的就是以上多出来的条目。

1.1.3 小结

本节的例子让我们看到,虽然 volume 里面从头到尾只有一个文件, 但随着我们不断覆盖这个文件内的值,元数据引擎 TiKV 内的条目数量就会持续增加。 多出来的这些东西,对应的就是这份数据的多个版本,也就是 MVCC 里面 multi-version 的表现。

显然,没有冲突的话,只保留最后一个版本就行了,其他版本都可以删掉 —— 这就是垃圾回收(GC)的作用。

1.2 GC(垃圾回收)

垃圾回收 (GC) 的功能是清理 MVCC 留下的旧版本。比如同一份数据保存了 1000 个版本,那原则上前面大部分版本都可以清掉了,只保留最新的一个或几个。

那如何判断哪些版本可以安全地清掉呢?TiKV 引入了一个时间戳概念: safepoint

GC is a process to clean up garbage versions (versions older than the configured lifetime) of each row.

https://tikv.org/deep-dive/key-value-engine/rocksdb/

1.3 Safepoint(可安全删除这个时间戳之前的版本)

In order to ensure the correctness of all read and write transactions, and make sure the GC mechanism works, TiKV/TiDB introduced the concept of safe-point. There is a guarantee that all active transactions and future transactions’ timestamp is greater than or equal to the safe-point. It means old versions whose commit-ts is less than the safe-point can be safely deleted by GC. [3]

2 TiKV MVCC GC

以上看到,TiKV 有 GC 功能,但由于其“历史出身”,也存在一些限制。

2.1 历史:从 TiDB 里面拆分出来,功能不完整

TiKV 是从 TiDB 里面拆出来的一个产品,并不是从一开始就作为独立产品设计和开发的。 这导致的一个问题是:MVCC GC 功能在使用上有点蹩脚:

  1. 默认情况下,靠底层 RocksDB 的 compaction 触发 GC,这周触发周期不确定且一般比较长;
  2. TiKV+PD 也内置了另一种 GC 方式,但并不会自己主动去做,而是将 GC 接口暴露出来,靠 TiDB 等在使用 TiKV 的更上层组件来触发(见下节的图);
  3. tikv-ctl/pd-ctl 等等命令行工具也都没有提供 GC 功能,这导致 TiKV 的运维很不方便,比如有问题想快速手动触发时用不了。

下面具体看看 TiKV 中的 GC 设计。

2.2 TiKV GC 设计和配置项

Fig. TiKV MVCC GC mechanisms.

2.2.1 设计:两种 GC 触发方式

  1. 被动 GC:TiKV 底层的 RocksDB compact 时进行垃圾回收。
    • 通过 tikv-server 的 enable-compaction-filter 配置项控制;
    • 默认启用
    • 触发 RocksDB compaction 时才能进行 GC。
    • tikv-ctl compact/compact-cluster 可以手动触发这种 compact,进而 GC。
  2. 半主动 GC:内置了 GC worker,
    • 定期获取 PD 里面的 gc safepoint,然后进行 GC;会占用一些 CPU/IO 资源;
    • PD 不会主动更新这个 gc safepoint,一般是由在使用 TiKV 的更外围组件来更新的,例如 TiDB、JuiceFS 等等;
    • 所以本文把这种方式称为“半主动”。

2.2.2 tikv-server 启动日志中的 GC 配置信息

tikv-server.log

[INFO] [server.rs:274] ["using config"] [config="{..., "enable-compaction-filter":true, ...}"]
[INFO] [compaction_filter.rs:138] ["initialize GC context for compaction filter"]
[INFO] [gc_worker.rs:786] ["initialize compaction filter to perform GC when necessary"]

2.2.3 tikv-ctl compact/compact-cluster 触发被动 GC 例子

# compact-cluster 必须要指定 --pd 参数,因为针对是整个集群。指定 --host 会失败,但没有提示错在哪,TiKV 的命令行工具经常这样
$ tikv-ctl.sh compact-cluster --from 'zfoo' --to 'zfop' 

$ tikv-ctl.sh compact --from 'zfoo' --to 'zfop'
store:"192.168.1.1:20160" compact db:Kv cf:default range:[[122, 122, 121, 110], [122, 122, 121, 111]) success!

$ tikv-ctl.sh compact --from 'zfoo' --to 'zfop' -c default  # 很快
$ tikv-ctl.sh compact --from 'zfoo' --to 'zfop' -c lock     # 很快
store:"192.168.1.1:20160" compact db:Kv cf:lock range:[[122, 122, 121, 110], [122, 122, 121, 111]) success!
$ tikv-ctl.sh compact --from 'zfoo' --to 'zfop' -c write    # 非常慢
store:"192.168.1.1:20160" compact db:Kv cf:write range:[[122, 122, 121, 110], [122, 122, 121, 111]) success!

# 还可以指定本地 TiKV 数据路径直接 compact
# -d: specify the RocksDB that performs compaction. default: kv. Valid values: {kv, raft}
$ tikv-ctl --data-dir /path/to/tikv compact -d kv

2.2.4 小结

“半主动方式”需要外围组件去更新 PD 中的 gc safepoint 信息,这样下面的 TiKV 才会去执行 GC 操作。作为两个具体例子,我们接下来看看 TiDB 和 JuiceFS 在使用 TiKV 时,分别是怎么去更新这个信息的。

2.3 TiDB 中触发 TiKV GC 的方式

TiDB 有 GC 相关的配置和 worker,会按照配置去触发底层的 TiKV GC,

Fig. TiDB SQL layer overview. GC worker is outside of TiKV. Image Source: pingcap.com

更多信息可以参考 [3,4]。

2.4 JuiceFS 触发 TiKV GC 的方式

TiKV 作为元数据引擎时,JuiceFS 并没有使用 TiDB,而是直接使用的 TiKV(和 PD), 所以就需要 JuiceFS client 来触发这个 GC (因为不考虑 CSI 部署方式的话,JuiceFS 就一个客户端组件,也没有其他 long running 服务来做这个事情了)。

Fig. Typical JuiceFS cluster.

2.4.1 定期更新 gc safepoint 的代码

JuiceFS v1.0.4+ 客户端会周期性地设置 PD 中的 gc safepoint,默认是 now-3h,也就是可以删除 3 小时之前的旧版本数据,

// pkg/meta/tkv_tikv.go

func (c *tikvClient) gc() {
    if c.gcInterval == 0 {
        return
    }

    safePoint := c.client.GC(context.Background(), oracle.GoTimeToTS(time.Now().Add(-c.gcInterval)))
}

2.4.2 配置:META URL \?gc-interval=1h

这个 gc-interval 可在 juicefs 挂载卷时加到 TiKV URL 中,

  • 默认值:3h
  • 最小值:1h,设置的值小于这个值会打印一条 warning,然后强制设置为 1h。

juicefs client 挂载时显式设置 gc-interval

$ juicefs mount tikv://localhost:2379\?gc-interval=1h ~/mnt/jfs
<INFO>: Meta address: tikv://localhost:2379?gc-interval=1h [interface.go:491]
<INFO>: TiKV gc interval is set to 1h0m0s [tkv_tikv.go:84]
...

2.4.3 juicefs gc 手动触发 TiKV GC

还可以通过 juicefs gc 子命令来主动触发 TiKV GC。这个例子中设置的时间太短,可以看到被强制改成了允许的最小值 1h

$ juicefs gc tikv://<ip>:2379/foo-dev\?gc-interval=1m --delete
...
<WARNING>: TiKV gc-interval (1m0s) is too short, and is reset to 1h [tkv_tikv.go:133]
<INFO>: TiKV gc interval is set to 1h0m0s [tkv_tikv.go:138]
Cleaned pending slices: 0                      0.0/s
 Pending deleted files: 0                      0.0/s
  Pending deleted data: 0.0 b   (0 Bytes)      0.0 b/s
 Cleaned pending files: 0                      0.0/s
  Cleaned pending data: 0.0 b   (0 Bytes)      0.0 b/s
         Cleaned trash: 0                      0.0/s
Cleaned detached nodes: 0                      0.0/s
         Listed slices: 2047                   4930.4/s
          Trash slices: 2026                   55423.8/s
            Trash data: 7.7 KiB (7883 Bytes)   211.8 KiB/s
  Cleaned trash slices: 0                      0.0/s
    Cleaned trash data: 0.0 b   (0 Bytes)      0.0 b/s
       Scanned objects: 2047/2047 [===========================================]  18138.6/s used: 113.115519ms
         Valid objects: 21                     187.2/s
            Valid data: 85.0 b  (85 Bytes)     758.0 b/s
     Compacted objects: 2026                   18064.2/s
        Compacted data: 7.7 KiB (7883 Bytes)   68.6 KiB/s
        Leaked objects: 0                      0.0/s
           Leaked data: 0.0 b   (0 Bytes)      0.0 b/s
       Skipped objects: 0                      0.0/s
          Skipped data: 0.0 b   (0 Bytes)      0.0 b/s
<INFO>: scanned 2047 objects, 21 valid, 2026 compacted (7883 bytes), 0 leaked (0 bytes), 0 delslices (0 bytes), 0 delfiles (0 bytes), 0 skipped (0 bytes) [gc.go:379]

2.5 外挂组件 github.com/tikv/migration/gc-worker

代码仓库,是个在 TiKV 之上的组件, 从 PD 获取 service safepoint 信息,然后计算 gc safepoint 并更新到 PD,从而触发 TiKV GC。

3 GC 不及时导致的问题一例

这里挑一个典型的问题讨论下。

3.1 问题现象

3.1.1 监控:TiKV db size 暴增,磁盘空间不断减小

如下面监控所示,

Fig. TiKV DB size soaring in a JuiceFS cluster, caused by TiKV GC lagging.

  • TiKV DB size 暴增;
  • TiKV region 分布出现显著变量,总数量也有一定程度上升;
  • TiKV node 可用磁盘空间不断下降。

3.1.2 tikv-server 错误日志:failed to split region

查看 tikv-server 日志,看到一直在刷下面这样的 warning/error:

[WARN] [split_observer.rs:73] ["invalid key, skip"] [err="\"key 6E677... should be in (6E677..., 6E677...)\""] [index=0] [region_id=39179938]
[ERROR] [split_observer.rs:136] ["failed to handle split req"] [err="\"no valid key found for split.\""] [region_id=39179938]
[WARN] [peer.rs:2971] ["skip proposal"] [error_code=KV:Raftstore:Coprocessor] [err="Coprocessor(Other(\"[components/raftstore/src/coprocessor/split_observer.rs:141]: no valid key found for split.\"))"] [peer_id=39179939] [region_id=39179938]

也就是 region split 失败。

3.2 问题排查

  1. 根据日志报错,网上搜到一些帖子,初步了解问题背景(JuiceFS/TiKV 新人,接触没多久);
  2. 对报错日志进行分析,发现:

    • 报错集中在几十个 regiongrep "failed to handle split req" tikv.log | awk '{print $NF}' | sort | uniq -c | sort -n -k1,1),相对总 region 数量很少;
    • pd-ctl region-properties -r <region> 看,发现 start/end key 都来自同一个 volume(命令行操作见下一篇);
    • 根据 volume 监控看,只有一个客户端 set 请求非常高,每秒 400 次请求,而这个 volume 只有几个 GB,可以说非常小;
  3. tikv-ctl mvcc -k <key> 查看有问题的 key,发现超时了,报错说文件(元数据)太大

结合以上三点,判断是某个或少数几个文件的 MVCC 版本太多,导致 TiKV split region 失败,进而不断累积垃圾数据。

3.3 问题根因

以上,猜测直接原因是这个用户 非正常使用 JuiceFS疯狂更新文件,也就是我们 1.1 中例子的极端版。 这导致部分文件的历史版本极其多,TiKV 在 auto split region 时失败。网上也有一些类似的 case(大部分是 TiDB 用户)。

但本质上,还是因为 TiKV 的 GC 太滞后,

  1. 被动 GC(RocksDB compact 方式)的频率不可控,跟集群所有客户端的总 write/update/delete 行为有关;
  2. JuiceFS 的主动 GC 频率太慢,跟不上某些文件的版本增长速度。

    • JuiceFS 默认 now-3h,最小 now-1h,也就是至少会保留一个小时内的所有版本(实际上我们是有个外部服务在定期更新 PD 的 gc safepoint,但也是设置的 now-1h);
    • 根据监控看,异常的 juicefs client 每秒有 400+ set 请求,一个小时就是 144w 次的更新(这些请求更新的文件很集中)。

3.4 解决方式

  1. 写了个程序,允许以非常小的粒度去更新 PD 的 gc safepoint,例如 now-5m, 也就是最多保留最近 5 分钟内的版本,其他的都删掉;这一步下去就有效果了,先稳住了,DB 不再增长,开始缓慢下降;
  2. 通知用户去处理那个看起来异常的客户端(我们没权限登录用户的机器,客户端不可控,这是另一个问题了)。

1+2,DB 开始稳步下降,最终完全恢复正常。

3.5 问题小结

对于 TiKV 这种 MVCC 的元数据引擎来说,JuiceFS 的一条元数据可能会保留多个版本,老版本什么时候删掉很大程度上依赖外部 GC 触发。 如果 GC 间隔太长 + 文件更新太频繁,单条元数据极端情况下就可以占几个 GB,这时候不仅 DB size 暴大,还会导致 TiKV split region 工作不正常。

4 问题讨论

前面看到,JuiceFS 支持配置 TiKV 的 GC 间隔,但从管理和运维层面,这里面也有几个问题可以探讨。

4.1 允许的最小 GC 间隔太大

目前最小是 now-1h,极端情况会导致第 3 节中的问题,TiKV DB size 暴增,集群被打爆。

4.2 GC 配置放在客户端,增加了用户的认知负担和学习成本

  • 用户必需感知 TiKV gc 这个东西,增加认知成本和使用负担;

    用户只是用 JuiceFS volume 读写文件,原则上没有必要去知道 JuiceFS 集群用什么元数据引擎, 甚至还必现了解这种元数据引擎的 GC 知识,后者都是 JuiceFS 集群管理员需要关心和解决的;

  • 用户如果没有配置,就只完全依赖 RocksDB compaction 来 GC,更容易触发版本太多导致的问题。

4.3 管理员运维困境

用户一旦没有显式配置 gc-interval(使用很大的默认值),TiKV 可能就被打爆, 这种情况下用户不知道,管理员知道但可能没短平快的解决办法(不一定有权限管理用户的机器)。

4.4 小结

对集群管理员来说,更好的方式可能是,

  1. 有个(内部或外部)服务,可以按管理员的需求随时和/或定时去 GC;
  2. 用户侧完全不用感知这个事情;
  3. 有 Meta 操作的限流能力(可以隔离有问题的 volume 或 client),下一篇讨论。

参考资料

  1. MVCC in TiKV, pingcap.com, 2016
  2. JuiceFS 元数据引擎最佳实践:TiKV, juicefs.com
  3. Deep Dive into Distributed Transactions in TiKV and TiDB, medium.com, 2024
  4. MVCC garbage collection, TiDB doc, 2024

Written by Human, Not by AI Written by Human, Not by AI

JuiceFS 元数据引擎再探:开箱解读 TiKV 中的 JuiceFS 元数据(2024)

Fig. JuiceFS upload/download data bandwidth control.

水平及维护精力所限,文中不免存在错误或过时之处,请酌情参考。 传播知识,尊重劳动,年满十八周岁,转载请注明出处



有了第一篇的铺垫,本文直接进入正题。

  • 首先创建一个 volume,然后在其中做一些文件操作,然后通过 tikv-ctl 等工具在 TiKV 中查看对应的元数据。
  • 有了这些基础,我们再讨论 JuiceFS metadata key 和 TiKV 的编码格式。

之前有一篇类似的,开箱解读 etcd 中的 Cilium 元数据: What’s inside Cilium Etcd (kvstore)

1 创建一个 volume

创建一个名为 foo-dev 的 JuiceFS volume。

1.1 JuiceFS client 日志

用 juicefs client 的 juicefs format 命令创建 volume,

$ juicefs format --storage oss --bucket <bucket> --access-key <key> --secret-key <secret key> \
  tikv://192.168.1.1:2379,192.168.1.2:2379,192.168.1.3:2379/foo-dev foo-dev

<INFO>: Meta address: tikv://192.168.1.1:2379,192.168.1.2:2379,192.168.1.3:2379/foo-dev
<INFO>: Data use oss://xxx/foo-dev/
<INFO>: Volume is formatted as {
  "Name": "foo-dev",
  "UUID": "ec843b",
  "Storage": "oss",
  "BlockSize": 4096,
  "MetaVersion": 1,
  "UploadLimit": 0,
  "DownloadLimit": 0,
  ...
}
  • 对象存储用的是阿里云 OSS;
  • TiKV 地址指向的是 PD 集群地址,上一篇已经介绍过,2379 是 PD 接收客户端请求的端口;

1.2 JuiceFS client 中的 TiKV/PD client 初始化/调用栈

下面我们进入 JuiceFS 代码,看看 JuiceFS client 初始化和连接到元数据引擎的调用栈:

mount
 |-metaCli = meta.NewClient
 |-txnkv.NewClient(url)                                          // github.com/juicedata/juicefs: pkg/meta/tkv_tikv.go
 |  |-NewClient                                                  // github.com/tikv/client-go:    txnkv/client.go
 |     |-pd.NewClient                                            // github.com/tikv/client-go:    tikv/kv.go
 |          |-NewClient                                          // github.com/tikv/pd:           client/client.go
 |             |-NewClientWithContext                            // github.com/tikv/pd:           client/client.go
 |                |-createClientWithKeyspace                     // github.com/tikv/pd:           client/client.go
 |                   |-c.pdSvcDiscovery = newPDServiceDiscovery  // github.com/tikv/pd:           client/pd_xx.go
 |                   |-c.setup()                                 // github.com/tikv/pd:           client/pd_xx.go
 |                       |-c.pdSvcDiscovery.Init()
 |                       |-c.pdSvcDiscovery.AddServingURLSwitchedCallback
 |                       |-c.createTokenDispatcher()
 |-metaCli.NewSession
    |-doNewSession
       |-m.setValue(m.sessionKey(m.sid), m.expireTime())  // SE
       |-m.setValue(m.sessionInfoKey(m.sid), sinfo)       // SI

这里面连接到 TiKV/PD 的代码有点绕,

  • 传给 juicefs client 的是 PD 集群地址
  • 但代码使用的是 tikv 的 client-go 包,创建的是一个 tikv transaction client
  • 这个 tikv transaction client 里面会去创建 pd client 连接到 PD 集群,

所以,架构上看 juicefs 是直连 PD,但实现上并没有直接创建 pd client, 也没有直接使用 pd 的库。

Fig. JuiceFS cluster initialization, and how POSIX file operations are handled by JuiceFS.

1.3 tikv-ctl 查看空 volume 的系统元数据

现在再把目光转到 TiKV。看看这个空的 volume 在 TiKV 中对应哪些元数据:

$ ./tikv-ctl.sh scan --from 'zfoo' --to 'zfop'
key: zfoo-dev\375\377A\001\000\000\000\000\000\000\377\000I\000\000\000\000\000\000\371  # attr?
key: zfoo-dev\375\377ClastCle\377anupSess\377ions\000\000\000\000\373                    # lastCleanupSessions
key: zfoo-dev\375\377CnextChu\377nk\000\000\000\000\000\000\371                          # nextChunk
key: zfoo-dev\375\377CnextIno\377de\000\000\000\000\000\000\371                          # nextInode
key: zfoo-dev\375\377CnextSes\377sion\000\000\000\000\373                                # nextSession
key: zfoo-dev\375\377SE\000\000\000\000\000\000\377\000\001\000\000\000\000\000\000\371  # session
key: zfoo-dev\375\377SI\000\000\000\000\000\000\377\000\001\000\000\000\000\000\000\371  # sessionInfo
key: zfoo-dev\375\377setting\000\376                                                     # setting

以上就是我们新建的 volume foo-dev 的所有 entry 了。 也就是说一个 volume 创建出来之后,默认就有这些 JuiceFS 系统元数据

TiKV 中的每个 key 都经过了两层编码(JuiceFS 和 TiKV),我们后面再介绍编码规则。 就目前来说,根据 key 中的字符还是依稀能看出每个 key 是干啥用的, 为方便起见直接注释在上面每行的最后了。比如,下面两个 session 相关的 entry 就是上面调用栈最后两个创建的:

  • session
  • sessionInfo

1.4 例子:tikv-ctl mvcc 解码 volume setting 元数据

TiKV 中的每个 entry 都是 key/value。现在我们尝试解码最后一个 entry,key 是 zfoo-dev\375\377setting\000\376, 我们来看看它的 value —— 也就是它的内容 —— 是什么

$ value_hex=$(./tikv-ctl.sh mvcc -k 'zfoo-dev\375\377setting\000\376' --show-cf=default | awk '/default cf value:/ {print $NF}')
$ value_escaped=$(./tikv-ctl.sh --to-escaped $value_hex)
$ echo -e $value_escaped | sed 's/\\"/"/g' | jq .

输出:

{
  "Name": "foo-dev",
  "UUID": "1ce2973b",
  "Storage": "S3",
  "Bucket": "http://xx/bucket",
  "AccessKey": "xx",
  "SecretKey": "xx",
  "BlockSize": 4096,
  "MetaVersion": 1,
  "UploadLimit": 0,
  "DownloadLimit": 0,
  ...
}

可以看到是个 JSON 结构体。这其实就是这个 volume 的配置信息。如果对 JuiceFS 代码有一定了解, 就会看出来它对应的其实就是 type Format 这个 struct。

1.4.1 对应 JuiceFS Format 结构体

// https://github.com/juicedata/juicefs/blob/v1.2.0/pkg/meta/config.go#L72

type Format struct {
    Name             string
    UUID             string
    Storage          string
    StorageClass     string `json:",omitempty"`
    Bucket           string
    AccessKey        string `json:",omitempty"`
    SecretKey        string `json:",omitempty"`
    SessionToken     string `json:",omitempty"`
    BlockSize        int
    Compression      string `json:",omitempty"`
    Shards           int    `json:",omitempty"`
    HashPrefix       bool   `json:",omitempty"`
    Capacity         uint64 `json:",omitempty"`
    Inodes           uint64 `json:",omitempty"`
    UploadLimit      int64  `json:",omitempty"` // Mbps
    DownloadLimit    int64  `json:",omitempty"` // Mbps
    ...
}

2 将 volume 挂载(mount)到机器

接下来我们找一台机器,把这个 volume 挂载上去,这样就能在这个 volume 里面读写文件了。

2.1 JuiceFS client 挂载日志

$ juicefs mount --verbose --backup-meta 0 tikv://192.168.1.1:2379,192.168.1.2:2379,192.168.1.3:2379/foo-dev /tmp/foo-dev
<INFO>:  Meta address: tikv://192.168.1.1:2379,192.168.1.2:2379,192.168.1.3:2379/foo-dev [interface.go:406]
<DEBUG>: Creating oss storage at endpoint http://<url> [object_storage.go:154]
<INFO>:  Data use oss://xx/foo-dev/ [mount.go:497]
<INFO>:  Disk cache (/var/jfsCache/ec843b85/): capacity (10240 MB), free ratio (10%), max pending pages (15) [disk_cache.go:94]
<DEBUG>: Scan /var/jfsCache/ec843b85/raw to find cached blocks [disk_cache.go:487]
<DEBUG>: Scan /var/jfsCache/ec843b85/rawstaging to find staging blocks [disk_cache.go:530]
<DEBUG>: Found 8 cached blocks (32814 bytes) in /var/jfsCache/ec843b85/ with 269.265µs [disk_cache.go:515]
<INFO>:  Create session 4 OK with version: 1.2.0 [base.go:279]
<INFO>:  Prometheus metrics listening on 127.0.0.1:34849 [mount.go:165]
<INFO>:  Mounting volume foo-dev at /tmp/foo-dev ... [mount_unix.go:203]
<INFO>:  OK, foo-dev is ready at /tmp/foo-dev [mount_unix.go:46]

可以看到成功挂载到了本机路径 /tmp/foo-dev/

2.2 查看挂载信息

$ mount | grep juicefs
JuiceFS:foo-dev on /tmp/foo-dev type fuse.juicefs (rw,relatime,user_id=0,group_id=0,default_permissions,allow_other)

$ cd /tmp/foo-dev
$ ls # 空目录

2.3 查看 JuiceFS 隐藏(系统)文件

新建的 volume 里面其实有几个隐藏文件:

$ cd /tmp/foo-dev
$ ll
-r-------- 1 root root  .accesslog
-r-------- 1 root root  .config
-r--r--r-- 1 root root  .stats
dr-xr-xr-x 2 root root  .trash/

2.3.1 .accesslog

可以通过 cat 这个文件看到一些 JuiceFS client 底层的操作日志,我们一会会用到。

2.3.2 .config

包括 Format 在内的一些 volume 配置信息:

$ cat .config
{
 "Meta": {
  "Strict": true,
  "Retries": 10,
  "CaseInsensi": false,
  "ReadOnly": false,
  "NoBGJob": false,
  "OpenCache": 0,
  "Heartbeat": 12000000000,
  "MountPoint": "/tmp/foo-dev",
  "Subdir": "",
  "CleanObjFileLever": 1
 },
 "Format": {
  "Name": "foo-dev",
  "UUID": "ec843b85",
  "Storage": "oss",
  "Bucket": "http://<url>",
  "UploadLimit": 0,
  "DownloadLimit": 0,
  ...
 },
 "Chunk": {
  "CacheDir": "/var/jfsCache/ec843b85",
  "CacheMode": 384,
  "CacheSize": 10240,
  "FreeSpace": 0.1,
  "AutoCreate": true,
  "Compress": "none",
  "MaxUpload": 20,
  "MaxDeletes": 2,
  "MaxRetries": 10,
  "UploadLimit": 0,
  "DownloadLimit": 0,
  "Writeback": false,
  "UploadDelay": 0,
  "HashPrefix": false,
  "BlockSize": 4194304,
  "GetTimeout": 60000000000,
  "PutTimeout": 60000000000,
  "CacheFullBlock": true,
  "BufferSize": 314572800,
  "Readahead": 0,
  "Prefetch": 1,
  "UseMountUploadLimitConf": false,
  "UseMountDownloadLimitConf": false
 },
 "Version": "1.2.0",
 "AttrTimeout": 1000000000,
 "DirEntryTimeout": 1000000000,
 "EntryTimeout": 1000000000,
 "BackupMeta": 0,
 "HideInternal": false
}

2.3.3 .stats

cat 能输出一些 prometheus metrics:

$ cat .stats
...
juicefs_uptime 374.021754516
juicefs_used_buffer_size_bytes 0
juicefs_used_inodes 7
juicefs_used_space 28672

用 prometheus 采集器把这个数据收上去,就能在 grafana 上展示 volume 的各种内部状态。

2.3.4 .trash

类似于 Windows 的垃圾箱。如果启用了,删掉的文件会在里面保存一段时间再真正从对象存储删掉。

3 创建、更新、删除文件

接下来做一些文件操作,看看 TiKV 中对应元数据的变化。

3.1 创建文件

3.1.1 创建文件

$ cd /tmp/foo-dev
$ echo test3 > file3.txt

3.1.2 JuiceFS .accesslog

$ cat .accesslog
[uid:0,gid:0,pid:169604] getattr (1): OK (1,[drwxrwxrwx:0040777,3,0,0,1725503250,1725585251,1725585251,4096]) <0.001561>
[uid:0,gid:0,pid:169604] lookup (1,file3.txt): no such file or directory <0.000989>
[uid:0,gid:0,pid:169604] create (1,file3.txt,-rw-r-----:0100640): OK (103,[-rw-r-----:0100640,1,0,0,1725585318,1725585318,1725585318,0]) [fh:27] <0.003850>
[uid:0,gid:0,pid:169604] flush (103,27): OK <0.000005>
[uid:0,gid:0,pid:169604] write (103,6,0,27): OK <0.000048>
[uid:0,gid:0,pid:169604] flush (103,27): OK <0.026205>
[uid:0,gid:0,pid:0     ] release (103): OK <0.000006>
[uid:0,gid:0,pid:169749] getattr (1): OK (1,[drwxrwxrwx:0040777,3,0,0,1725503250,1725585318,1725585318,4096]) <0.000995>
[uid:0,gid:0,pid:169750] getattr (1): OK (1,[drwxrwxrwx:0040777,3,0,0,1725503250,1725585318,1725585318,4096]) <0.001219>

3.1.3 TiKV 元数据

$ ./tikv-ctl.sh scan --from 'zfoo' --to 'zfop' --limit 100
...
key: zfoo-dev\375\377A\001\000\000\000\000\000\000\377\000Dfile3.\377txt\000\000\000\000\000\372
...

可以看到 meta 中多了几条元数据,依稀可以分辨出对应的就是我们创建的文件,

  1. 这个 key 经过了 juicefs 和 tikv 两次编码,
  2. 简单来说,它是 volume + 0xFD(8 进制的 \375)+ 文件名 + tikv 编码,最终得到的就是上面看到的这个 key。

对应的 value 一般长这样:

$ ./tikv-ctl.sh mvcc -k 'zfoo-dev\375\377A\001\000\000\000\000\000\000\377\000Dfile3.\377txt\000\000\000\000\000\372' --show-cf default,lock,write
key: zfoo-dev\375\377A\001\000\000\000\000\000\000\377\000Dfile3.\377txt\000\000\000\000\000\372
         write cf value: start_ts: 452330816414416901 commit_ts: 452330816414416903 short_value: 010000000000000002

先粗略感受一下,后面再具体介绍 key/value 的编解码规则。

3.2 删除文件操作

3.2.1 删除文件

rm file4.txt

3.2.2 JuiceFS .accesslog

$ cat .accesslog
[uid:0,gid:0,pid:169604] getattr (1): OK (1,[drwxrwxrwx:0040777,3,0,0,1725503250,1725585532,1725585532,4096]) <0.001294>
[uid:0,gid:0,pid:169902] lookup (1,file4.txt): OK (104,[-rw-r-----:0100640,1,0,0,1725585532,1725585532,1725585532,6]) <0.001631>
[uid:0,gid:0,pid:169902] unlink (1,file4.txt): OK <0.004206>
[uid:0,gid:0,pid:169904] getattr (1): OK (1,[drwxrwxrwx:0040777,3,0,0,1725503250,1725585623,1725585623,4096]) <0.000718>
[uid:0,gid:0,pid:169905] getattr (1): OK (1,[drwxrwxrwx:0040777,3,0,0,1725503250,1725585623,1725585623,4096]) <0.000843>

3.2.3 TiKV 元数据

对应的元数据就从 TiKV 删掉了。

3.3 更新(追加)文件

3.3.1 更新文件

$ echo test3 >> file3.txt

3.3.2 JuiceFS .accesslog

$ cat .accesslog
[uid:0,gid:0,pid:169604] getattr (1): OK (1,[drwxrwxrwx:0040777,3,0,0,1725503250,1725585623,1725585623,4096]) <0.001767>
[uid:0,gid:0,pid:169604] lookup (1,file3.txt): OK (103,[-rw-r-----:0100640,1,0,0,1725585318,1725585318,1725585318,6]) <0.001893>
[uid:0,gid:0,pid:169604] open (103): OK [fh:51] <0.000884>
[uid:0,gid:0,pid:169604] flush (103,51): OK <0.000011>
[uid:0,gid:0,pid:169604] write (103,6,6,51): OK <0.000068>
[uid:0,gid:0,pid:169604] flush (103,51): OK <0.036778>
[uid:0,gid:0,pid:0     ] release (103): OK <0.000024>

3.3.3 TiKV 元数据

  • 如果追加的内容不多,TiKV 中还是那条元数据,但 value 会被更新;
  • 如果追加的内容太多(例如几百兆),文件就会被切分,这时候元数据就会有多条了。

4 元数据操作和 TiKV key/value 编码规则

上一节简单看了下创建、更新、删除 volume 中的文件,TiKV 中对应的元数据都有什么变化。 我们有意跳过了 key/value 是如何编码的,这一节就来看看这块的内容。

4.1 JuiceFS key 编码规则

4.1.1 每个 key 的公共前缀:<vol_name> + 0xFD

TiKV 客户端初始化:每个 key 的 base 部分<vol_name> + 0xFD

// pkg/meta/tkv_tikv.go

func init() {
    Register("tikv", newKVMeta)
    drivers["tikv"] = newTikvClient
}

func newTikvClient(addr string) (tkvClient, error) {
    client := txnkv.NewClient(strings.Split(tUrl.Host, ","))
    prefix := strings.TrimLeft(tUrl.Path, "/")
    return withPrefix(&tikvClient{client.KVStore, interval}, append([]byte(prefix), 0xFD)), nil
}

4.1.2 每个 key 后面的部分

根据对应的是文件、目录、文件属性、系统元数据等等,会有不同的编码规则:

// pkg/meta/tkv.go

/**
  Ino     iiiiiiii
  Length  llllllll
  Indx    nnnn
  name    ...
  sliceId cccccccc
  session ssssssss
  aclId   aaaa

All keys:
  setting            format
  C...               counter
  AiiiiiiiiI         inode attribute
  AiiiiiiiiD...      dentry
  AiiiiiiiiPiiiiiiii parents // for hard links
  AiiiiiiiiCnnnn     file chunks
  AiiiiiiiiS         symlink target
  AiiiiiiiiX...      extented attribute
  Diiiiiiiillllllll  delete inodes
  Fiiiiiiii          Flocks
  Piiiiiiii          POSIX locks
  Kccccccccnnnn      slice refs
  Lttttttttcccccccc  delayed slices
  SEssssssss         session expire time
  SHssssssss         session heartbeat // for legacy client
  SIssssssss         session info
  SSssssssssiiiiiiii sustained inode
  Uiiiiiiii          data length, space and inodes usage in directory
  Niiiiiiii          detached inde
  QDiiiiiiii         directory quota
  Raaaa                 POSIX acl
*/

具体可以再看看这个文件中的代码。

4.1.3 最终格式:字节序列

// pkg/meta/tkv.go

func (m *kvMeta) fmtKey(args ...interface{}) []byte {
    b := utils.NewBuffer(uint32(m.keyLen(args...)))
    for _, a := range args {
        switch a := a.(type) {
        case byte:
            b.Put8(a)
        case uint32:
            b.Put32(a)
        case uint64:
            b.Put64(a)
        case Ino:
            m.encodeInode(a, b.Get(8))
        case string:
            b.Put([]byte(a))
        default:
            panic(fmt.Sprintf("invalid type %T, value %v", a, a))
        }
    }
    return b.Bytes()
}

4.2 TiKV 对 JuiceFS key 的进一步编码

JuiceFS client 按照以上规则拼好一个 key 之后,接下来 TiKV 会再进行一次编码:

  1. 加一些 TiKV 的前缀,例如给文件 key 加个 z 前缀;

  2. 转义,例如 8 个字节插入一个 \377(对应 0xFF),不够 8 字节的补全等等;

最终得到的就是我们用 tikv-ctl scan 看到的那些 key。

4.3 例子:查看特殊元数据:volume 的 setting/format 信息

JuiceFS 的 Format 配置保存在 tikv 中,原始 key 是 setting,经过以上两层编码就变成了下面的样子:

$ ./tikv-ctl.sh scan --from 'zfoo' --to 'zfop' --limit 100
key: zfoo-dev\375\377setting\000\376
        default cf value: start_ts: 452330324173520898 value: 7B0A22...

其中的 value 是可以解码出来的,

# hex -> escaped string
$ ./tikv-ctl.sh --to-escaped '7B0A22...'
{\n\"Name\": \"foo-dev\",\n\"UUID\": \"8cd1ac73\",\n\"Storage\": \"S3\",\n\"Bucket\": \"http://xxx\",\n\"AccessKey\": \"...\",\n\"BlockSize\": 4096,\n\"Compression\": \"none\",\n\"KeyEncrypted\": true,\n\"MetaVersion\": 1,\n\"UploadLimit\": 0,\n\"DownloadLimit\": 0,\n\"\": \"\"\n}

对应的就是 pkg/meta/config.go 中的 Format 结构体。

5 总结

本文结合一些具体 JuiceFS 操作,分析了 TiKV 内的元数据格式与内容。

参考资料

  1. What’s inside Cilium Etcd (kvstore)

Written by Human, Not by AI Written by Human, Not by AI

JuiceFS 元数据引擎初探:高层架构、引擎选型、读写工作流(2024)

Fig. JuiceFS cluster initialization, and how POSIX file operations are handled by JuiceFS.

水平及维护精力所限,文中不免存在错误或过时之处,请酌情参考。 传播知识,尊重劳动,年满十八周岁,转载请注明出处



1 JuiceFS 高层架构与组件

Fig. JuiceFS components and architecutre.

如图,最粗的粒度上可以分为三个组件。

1.1 JuiceFS client

  • juicefs format ... 可以创建一个 volume;
  • juicefs config ... 可以修改一个 volume 的配置;
  • juicefs mount ... 可以把一个 volume 挂载到机器上,然后用户就可以在里面读写文件了;

1.2 Metatdata engine(元数据引擎)

  • 用于存储 JuiceFS 的元数据,例如每个文件的文件名、最后修改时间等等;
  • 可选择 etcd、TiKV 等等;

1.3. Object store

实际的对象存储,例如 S3、Ceph、阿里云 OSS 等等,存放 JuiceFS volume 内的数据。

2 JuiceFS 元数据存储引擎对比:tikv vs. etcd

2.1 设计与优缺点对比

  TiKV as metadata engine etcd as metadata engine
管理节点(e.g. leader election) PD (TiKV cluster manager) etcd server
数据节点(存储 juicefs metadata) TiKV server etcd server
数据节点对等 无要求 完全对等
数据一致性粒度 region-level (TiKV 的概念,region < node) node-level
Raft 粒度 region-level (multi-raft,TiKV 的概念) node-level
缓存多少磁盘数据在内存中 一部分 所有
集群支持的最大数据量 PB 级别 几十 GB 级别
性能(JuiceFS 场景) 高(猜测是因为 raft 粒度更细,并发读写高)
维护和二次开发门槛 高(相比 etcd)
流行度 & 社区活跃度 低(相比 etcd)
适用场景 大和超大 JuiceFS 集群 中小 JuiceFS 集群

2.2 几点解释

etcd 集群,

  • 每个节点完全对等,既负责管理又负责存储数据;
  • 所有数据全部缓存在内存中,每个节点的数据完全一致。 这一点限制了 etcd 集群支持的最大数据量和扩展性, 例如现在官网还是建议不要超过 8GB(实际上较新的版本在技术上已经没有这个限制了, 但仍受限于机器的内存)。

TiKV 方案可以可以理解成把管理和数据存储分开了,

  • PD 可以理解为 TiKV cluster manager,负责 leader 选举、multi-raft、元数据到 region 的映射等等;
  • 节点之间也不要求对等,PD 按照 region(比如 96MB)为单位,将 N(默认 3)个副本放到 N 个 TiKV node 上,而实际上 TiKV 的 node 数量是 M,M >= N
  • 数据放在 TiKV 节点的磁盘,内存中只缓存一部分(默认是用机器 45% 的内存,可控制)。

2.3 例子:TiKV 集群 engine size 和内存使用监控

TiKV 作为存储引擎,总结成一句话就是:根据硬件配置干活,能者多劳 —— 内存大、磁盘大就多干活,反之就少干活。

下面的监控展示是 7 台 TiKV node 组成的一个集群,各 node 内存不完全一致: 3 台 256GB 的,2 台 128GB 的,2 台 64GB 的, 可以看到每个 TiKV server 确实只用了各自所在 node 一半左右的内存:

Fig. TiKV engine size and memory usage of a 7-node (with various RAMs) cluster.

3 JuiceFS + TiKV:集群启动和宏观读写流程

3.1 架构

用 TiKV 作为元数据引擎,架构如下(先忽略其中的细节信息,稍后会介绍):

Fig. JuiceFS cluster initialization, and how POSIX file operations are handled by JuiceFS.

3.2 TiKV 集群启动

3.2.1 TiKV & PD 配置差异

两个组件的几个核心配置项,

$ cat /etc/tikv/pd-config.toml
name = "pd-node1"
data-dir = "/var/data/pd"

client-urls = "https://192.168.1.1:2379" # 客户端(例如 JuiceFS)访问 PD 时,连接这个地址
peer-urls   = "https://192.168.1.1:2380" # 其他 PD 节点访问这个 PD 时,连接这个地址,也就是集群内互相通信的地址

# 创建集群时的首批 PD
initial-cluster-token = "<anything you like>"
initial-cluster = "pd-node1=https://192.168.1.3:2380,pd-node2=https://192.168.1.2:2380,pd-node3=https://192.168.1.1:2380"

可以看到,PD 的配置和 etcd 就比较类似,需要指定其他 PD 节点地址,它们之间互相通信。

TiKV 节点(tikv-server)的配置就不一样了,

$ cat /etc/tikv/tikv-config.toml
[pd]
endpoints = ["https://192.168.1.1:2379", "https://192.168.1.2:2379", "https://192.168.1.3:2379"]

[server]
addr = "192.168.1.1:20160"        # 服务地址,JuiceFS client 会直接访问这个地址读写数据
status-addr = "192.168.1.1:20180" # prometheus 

可以看到,

  1. TiKV 会配置所有 PD 节点的地址,以便自己注册到 PD 作为一个数据节点(存储JuiceFS 元数据);
  2. TiKV 还会配置一个地址的 server 地址,这个读写本节点所管理的 region 内的数据用的; 正常流程是 JuiceFS client 先访问 PD,拿到 region 和 tikv-server 信息, 然后再到 tikv-server 来读写数据(对应 JuiceFS 的元数据);
  3. TiKV 不会配置其他 TiKV 节点的地址,也就是说 TiKV 节点之间不会 peer-to-peer 互连。 属于同一个 raft group 的多个 region 通信,也是先通过 PD 协调的,最后 region leader 才发送数据给 region follower。 详见 [1]。

3.2.2 服务启动

Fig. JuiceFS cluster initialization, and how POSIX file operations are handled by JuiceFS.

对应图中 step 1 & 2:

  • step 1. PD 集群启动,选主;
  • step 2. TiKV 节点启动,向 PD 注册;每个 TiKV 节点称为一个 store,也就是元数据仓库。

3.3 宏观读写流程

对应图中 step 3~5:

  • step 3. JuiceFS 客户端连接到 PD;发出读写文件请求;

    • JuiceFS 客户端中会初始化一个 TiKV 的 transaction kv client,这里面又会初始化一个 PD client,
    • 简单来说,此时 JuiceFS 客户端就有了 PD 集群的信息,例如哪个文件对应到哪个 region,这个 region 分布在哪个 TiKV 节点上,TiKV 服务端连接地址是多少等等;
  • step 4. JuiceFS (内部的 TiKV 客户端)直接向 TiKV 节点(准确说是 region leader)发起读写请求;
  • step 5. 元数据处理完成,JuiceFS 客户端开始往对象存储里读写文件。

4 TiKV 内部数据初探

TiKV 内部存储的都是 JuiceFS 的元数据。具体来说又分为两种:

  1. 用户文件的元数据:例如用户创建了一个 foo.txt,在 TiKV 里面就会对应一条或多条元数据来描述这个文件的信息;
  2. JuiceFS 系统元数据:例如每个 volume 的配置信息,这些对用户是不可见的。

TiKV 是扁平的 KV 存储,所以以上两类文件都放在同一个扁平空间,通过 key 访问。 本文先简单通过命令看看里面的元数据长什么样,下一篇再结合具体 JuiceFS 操作来深入解读这些元数据。

4.1 简单脚本 tikv-ctl.sh/pd-ctl.sh

简单封装一下对应的命令行工具,使用更方便,

$ cat pd-ctl.sh
tikv-ctl \
        --ca-path /etc/tikv/pki/root.crt --cert-path /etc/tikv/pki/tikv.crt --key-path /etc/tikv/pki/tikv.key \
        --host 192.168.1.1:20160 \
        "$@"

$ cat pd-ctl.sh
pd-ctl \
        --cacert /etc/tikv/pki/root.crt --cert /etc/tikv/pki/pd.crt --key /etc/tikv/pki/pd.key \
        --pd https://192.168.1.1:2379  \
        "$@"

4.2 tikv-ctl scan 扫描 key/value

tikv-ctl 不支持只列出所有 keys,所以只能 key 和 value 一起打印(扫描)。

扫描前缀是 foo 开头的所有 key:

$ ./tikv-ctl.sh scan --from 'zfoo' --to 'zfop' --limit 100
...
key: zfoo-dev\375\377A\001\000\000\000\000\000\000\377\000Dfile3.\377txt\000\000\000\000\000\372
key: zfoo-dev\375\377A\001\000\000\000\000\000\000\377\000Dfile4.\377txt\000\000\000\000\000\372
...
key: zfoo-dev\375\377setting\000\376
        default cf value: start_ts: 452330324173520898 value: 7B0A22...

扫描的时候一定要在 key 前面加一个 z 前缀,这是 TiKV 的一个设计

The raw-scan command scans directly from the RocksDB. Note that to scan data keys you need to add a ‘z’ prefix to keys.

代码出处 components/keys/src/lib.rs。 但对用户来说不是太友好,暴露了太多内部细节,没有 etcdctl 方便直接。

4.3 tikv-ctl mvcc 查看给定 key 对应的 value

$ ./tikv-ctl.sh mvcc -k 'zfoo-dev\375\377A\001\000\000\000\000\000\000\377\000Dfile1.\377txt\000\000\000\000\000\372' --show-cf default,lock,write
key: zfoo-dev\375\377A\001\000\000\000\000\000\000\377\000Dfile1.\377txt\000\000\000\000\000\372
         write cf value: start_ts: 452330816414416901 commit_ts: 452330816414416903 short_value: 010000000000000002

CF 是 column family 的缩写,进一步了解,可参考 Google bigtable 中关于 CF 的定义 译 | Bigtable: A Distributed Storage System for Structured Data (OSDI, 2006)

4.4 tikv-ctl --decode <key> 解除字符转义

# tikv escaped format -> raw format
./tikv-ctl.sh --decode 'foo-dev\375\377A\001\000\000\000\000\000\000\377\000Dfile4.\377txt\000\000\000\000\000\372'
foo-dev\375A\001\000\000\000\000\000\000\000Dfile4.txt

4.5 tikv-ctl --to-hex:转义表示 -> 十六进制表示

$ ./tikv-ctl.sh --to-hex '\375'
FD

4.6 tikv-ctl --to-escaped <value>:十六进制 value -> 带转义的字符串

./tikv-ctl.sh scan --from 'zfoo' --to 'zfop' --limit 100
key: zfoo-dev\375\377setting\000\376
        default cf value: start_ts: 452330324173520898 value: 7B0A22...

其中的 value 是可以解码出来的,

# hex -> escaped string
$ ./tikv-ctl.sh --to-escaped '7B0A22...'
{\n\"Name\": \"...\",\n\"UUID\": \"8cd1ac73\",\n\"Storage\": \"S3\",\n\"Bucket\": \"http://xxx\",\n\"AccessKey\": \"...\",\n\"BlockSize\": 4096,\n\"Compression\": \"none\",\n\"KeyEncrypted\": true,\n\"MetaVersion\": 1,\n\"UploadLimit\": 0,\n\"DownloadLimit\": 0,\n\"\": \"\"\n}

5 总结

本文介绍了一些 JuiceFS 元数据引擎相关的内容。

参考资料

  1. A Deep Dive into TiKV, 2016, pincap.com

Written by Human, Not by AI Written by Human, Not by AI

图解 JuiceFS CSI 工作流:K8s 创建带 PV 的 Pod 时,背后发生了什么(2024)

JuiceFS 是一个架设在对象存储(S3、Ceph、OSS 等)之上的分布式文件系统, 简单来说,

  • 对象存储:只能通过 key/value 方式使用;
  • 文件系统:日常看到的文件目录,能执行 ls/cat/find/truncate 等等之类的文件读写操作。

本文从 high-level 梳理了 JuiceFS CSI 方案中,当创建一个带 PV 的 pod 以及随后 pod 读写 PV 时, k8s/juicefs 组件在背后都做了什么,方便快速了解 K8s CSI 机制及 JuiceFS 的基本工作原理。

水平及维护精力所限,文中不免存在错误或过时之处,请酌情参考。 传播知识,尊重劳动,年满十八周岁,转载请注明出处



1 背景知识

简单列几个基础知识,有背景的可直接跳过。

1.1 K8s CSI (Container Storage Interface )

The Container Storage Interface (CSI) is a standard for exposing arbitrary block and file storage systems to containerized workloads on Container Orchestration Systems (COs) like Kubernetes.

https://kubernetes-csi.github.io/docs/

CSI 是 K8s 支持的一种容器存储机制,扩展性非常好, 各存储方案只要根据规范实现一些接口,就能集成到 k8s 中提供存储服务。

一般来说,存储方案需要在每个 node 上部署一个称为 “CSI plugin” 的服务, kubelet 在创建带 PV 容器的过程中会调用这个 plugin。但要注意,

  • K8s 的网络插件 CNI plugin 是一个可执行文件, 放在 /opt/cni/bin/ 下面就行了,kubelet 在创建 pod 网络时直接运行 这个可执行文件;
  • K8s 的存储插件 CSI plugin 是一个服务(某种程度上, 称为 agent 更好理解),kubelet 在初始化 PV 时通过 gRPC 调用这个 plugin;

1.2 FUSE (Filesystem in Userspace)

FUSE 是一种用户态文件系统,使得用户开发自己的文件系统非常方便。

懒得再重新画图, 这里借 lxcfs(跟 juicefs 没关系,但也是一种 FUSE 文件系统)展示一下 FUSE 的基本工作原理

Linux 容器底层工作机制:从 500 行 C 代码到生产级容器运行时(2023)

Fig. lxcfs/fuse workflow: how a read operation is handled [2]

JuiceFS 基于 FUSE 实现了一个用户态文件系统。

来自社区文档的一段内容,简单整理:

传统上,实现一个 FUSE 文件系统,需要基于 Linux libfuse 库,它提供两种 API:

  • high-level API:基于文件名和路径

    libfuse 内部做了 VFS 树的模拟,对外暴露基于路径的 API。

    适合元数据本身是基于路径提供的 API 的系统,比如 HDFS 或者 S3 之类。 如果元数据本身是基于 inode 的目录树,这种 inode → path →inode 的转换就会 影响性能。

  • low-level API:基于 inode。内核的 VFS 跟 FUSE 库交互就使用 low-level API。

JuiceFS 的元数据基于 inode 组织,所以用 low-level API 实现( 依赖 go-fuse 而非 libfuse),简单自然,性能好。

1.3 JuiceFS 三种工作模式

JuiceFS 有几种工作或部署方式:

  1. 进程挂载模式

    JuiceFS client 运行在 CSI Node plugin 容器中,所有需要挂载的 JuiceFS PV 都会在这个容器内以进程模式挂载。

  2. CSI 方式,又可分为两种:

    1. mountpod 方式:在每个 node 上,CSI plugin 动态为每个被 local pod 使用的 PV 创建一个保姆 pod,

      • 这个 mount pod 是 per-PV 而非 per-business-pod 的, 也就是说如果 node 上有多个业务 pod 在使用同一 PV,那只会有一个 mount pod, 下图可以看出来,

        Fig. JuiceFS as K8s CSI solution: workflow when a business pod is created (JuiceFS mountpod mode).

      • mount pod 里面装了 juicefs client,替业务 pod 完成 juicefs 相关的读写操作; 为了从字面上更容易理解,本文接下来把 mount pod 称为 dynamic client pod 或 client pod。
      • 这是 JuiceFS CSI 的默认工作方式
      • FUSE 需要 mount pod 具有 privilege 权限;
      • client pod 重启会导致业务 pod 一段时间读写不可用,但 client pod 好了之后业务 pod 就能继续读写了。
    2. . CSI sidecar 方式:给每个使用 juicefs PV 的业务 pod 创建一个 sidecar 容器。

      • per-pod 级别的 sidecar;
      • 注意 sidecar 就不是 JuiceFS plugin 创建的了,CSI Controller 会注册一个 Webhook 来监听容器变动,在创建 pod 时, webhook 给 pod yaml 自动注入一个 sidecar,跟 Istio 自动给 pod 注入 Envoy 容器类似;
      • Sidecar 重启需要重建业务 Pod 才能恢复。
      • 也依赖 FUSE,所以 sidecar 需要 privilege 权限。这会导致每个 sidecar 都能看到 node 上所有设备,有风险,所以不建议;

1.4 小结

有了以上基础,接下来看 k8s 中创建一个业务 pod 并且它要求挂载一个 PV 时,k8s 和 juicefs 组件都做了什么事情。

2 创建一个使用 PV 的 pod 时,k8s 和 juicefs 组件都做了什么

Fig. JuiceFS as K8s CSI solution: workflow when a business pod is created (JuiceFS mountpod mode).

Step 1:kubelet 启动,监听集群的 pod 资源变化

kubelet 作为 k8s 在每个 node 上的 agent,在启动后会监听整个 k8s 集群中的 pod 资源变化。 具体来说就是,kube-apiserver 中有 pod create/update/delete events 发生时,kubelet 都会立即收到。

Step 2:kubelet 收到业务 pod 创建事件,开始创建 pod

kubelet 收到一条 pod create 事件后,首先判断这个 pod 是否在自己的管辖范围内(spec 中的 nodeName 是否是这台 node), 是的话就开始创建这个 pod

Step 2.1 创建业务 pod:初始化部分

kubelet.INFO 中有比较详细的日志:

10:05:57.410  Receiving a new pod "pod1(<pod1-id>)"
10:05:57.411  SyncLoop (ADD, "api"): "pod1(<pod1-id>)"
10:05:57.411  Needs to allocate 2 "nvidia.com/gpu" for pod "<pod1-id>" container "container1"
10:05:57.411  Needs to allocate 1 "our-corp.com/ip" for pod "<pod1-id>" container "container1"
10:05:57.413  Cgroup has some missing paths: [/sys/fs/cgroup/pids/kubepods/burstable/pod<pod1-id> /sys/fs/cgroup/systemd/kubepods/burstable/pod<pod1-id> /sys/fs/cgroup/cpuset/kubepods/burstable/pod<pod1-id> /sys/fs/cgroup/memory/kubepods/burstable/pod<pod1-id> /sys/fs/cgroup/cpu,cpuacct/kubepods/burstable/pod<pod1-id> /sys/fs/cgroup/cpu,cpuacct/kubepods/burstable/pod<pod1-id> /sys/fs/cgroup/hugetlb/kubepods/burstable/pod<pod1-id>]
10:05:57.413  Cgroup has some missing paths: [/sys/fs/cgroup/memory/kubepods/burstable/pod<pod1-id> /sys/fs/cgroup/systemd/kubepods/burstable/pod<pod1-id> /sys/fs/cgroup/cpu,cpuacct/kubepods/burstable/pod<pod1-id> /sys/fs/cgroup/cpu,cpuacct/kubepods/burstable/pod<pod1-id> /sys/fs/cgroup/hugetlb/kubepods/burstable/pod<pod1-id> /sys/fs/cgroup/pids/kubepods/burstable/pod<pod1-id> /sys/fs/cgroup/cpuset/kubepods/burstable/pod<pod1-id>]
10:05:57.413  Cgroup has some missing paths: [/sys/fs/cgroup/cpu,cpuacct/kubepods/burstable/pod<pod1-id> /sys/fs/cgroup/pids/kubepods/burstable/pod<pod1-id> /sys/fs/cgroup/cpuset/kubepods/burstable/pod<pod1-id> /sys/fs/cgroup/systemd/kubepods/burstable/pod<pod1-id> /sys/fs/cgroup/memory/kubepods/burstable/pod<pod1-id> /sys/fs/cgroup/cpu,cpuacct/kubepods/burstable/pod<pod1-id> /sys/fs/cgroup/hugetlb/kubepods/burstable/pod<pod1-id>]
10:05:57.415  Using factory "raw" for container "/kubepods/burstable/pod<pod1-id>"
10:05:57.415  Added container: "/kubepods/burstable/pod<pod1-id>" (aliases: [], namespace: "")
10:05:57.419  Waiting for volumes to attach and mount for pod "pod1(<pod1-id>)"

10:05:57.432  SyncLoop (RECONCILE, "api"): "pod1(<pod1-id>)"

10:05:57.471  Added volume "meminfo" (volSpec="meminfo") for pod "<pod1-id>" to desired state.
10:05:57.471  Added volume "cpuinfo" (volSpec="cpuinfo") for pod "<pod1-id>" to desired state.
10:05:57.471  Added volume "stat" (volSpec="stat") for pod "<pod1-id>" to desired state.
10:05:57.480  Added volume "share-dir" (volSpec="pvc-6ee43741-29b1-4aa0-98d3-5413764d36b1") for pod "<pod1-id>" to desired state.
10:05:57.484  Added volume "data-dir" (volSpec="juicefs-volume1-pv") for pod "<pod1-id>" to desired state.
...

可以看出里面会依次处理 pod 所需的各种资源:

  1. 设备:例如 GPU
  2. IP 地址;
  3. cgroup 资源隔离配置;
  4. volumes

本文主要关注 volume 资源。

Step 2.2 处理 pod 依赖的 volumes

上面日志可以看到,业务 pod 里面声明了一些需要挂载的 volumes。几种类型

  1. hostpath 类型:直接把 node 路径挂载到容器内;
  2. lxcfs 类型:为了解决资源视图问题 [2];
  3. 动态/静态 PV 类型

本文的 JuiceFS volume 就属于 PV 类型,继续看 kubelet 日志:

# kubelet.INFO
10:05:57.509  operationExecutor.VerifyControllerAttachedVolume started for volume "xxx"
10:05:57.611  Starting operationExecutor.MountVolume for volume "xxx" (UniqueName: "kubernetes.io/host-path/<pod1-id>-xxx") pod "pod1" (UID: "<pod1-id>") 
10:05:57.611  operationExecutor.MountVolume started for volume "juicefs-volume1-pv" (UniqueName: "kubernetes.io/csi/csi.juicefs.com^juicefs-volume1-pv") pod "pod1" (UID: "<pod1-id>") 
10:05:57.611  kubernetes.io/csi: mounter.GetPath generated [/var/lib/k8s/kubelet/pods/<pod1-id>/volumes/kubernetes.io~csi/juicefs-volume1-pv/mount]
10:05:57.611  kubernetes.io/csi: created path successfully [/var/lib/k8s/kubelet/pods/<pod1-id>/volumes/kubernetes.io~csi/juicefs-volume1-pv]
10:05:57.611  kubernetes.io/csi: saving volume data file [/var/lib/k8s/kubelet/pods/<pod1-id>/volumes/kubernetes.io~csi/juicefs-volume1-pv/vol_data.json]
10:05:57.611  kubernetes.io/csi: volume data file saved successfully [/var/lib/k8s/kubelet/pods/<pod1-id>/volumes/kubernetes.io~csi/juicefs-volume1-pv/vol_data.json]
10:05:57.613  MountVolume.MountDevice succeeded for volume "juicefs-volume1-pv" (UniqueName: "kubernetes.io/csi/csi.juicefs.com^juicefs-volume1-pv") pod "pod1" (UID: "<pod1-id>") device mount path "/var/lib/k8s/kubelet/plugins/kubernetes.io/csi/pv/juicefs-volume1-pv/globalmount"
10:05:57.616  kubernetes.io/csi: mounter.GetPath generated [/var/lib/k8s/kubelet/pods/<pod1-id>/volumes/kubernetes.io~csi/juicefs-volume1-pv/mount]
10:05:57.616  kubernetes.io/csi: Mounter.SetUpAt(/var/lib/k8s/kubelet/pods/<pod1-id>/volumes/kubernetes.io~csi/juicefs-volume1-pv/mount)
10:05:57.616  kubernetes.io/csi: created target path successfully [/var/lib/k8s/kubelet/pods/<pod1-id>/volumes/kubernetes.io~csi/juicefs-volume1-pv/mount]
10:05:57.618  kubernetes.io/csi: calling NodePublishVolume rpc [volid=juicefs-volume1-pv,target_path=/var/lib/k8s/kubelet/pods/<pod1-id>/volumes/kubernetes.io~csi/juicefs-volume1-pv/mount]
10:05:57.713  Starting operationExecutor.MountVolume for volume "juicefs-volume1-pv" (UniqueName: "kubernetes.io/csi/csi.juicefs.com^juicefs-volume1-pv") pod "pod1" (UID: "<pod1-id>") 
...
10:05:59.506  kubernetes.io/csi: mounter.SetUp successfully requested NodePublish [/var/lib/k8s/kubelet/pods/<pod1-id>/volumes/kubernetes.io~csi/juicefs-volume1-pv/mount]
10:05:59.506  MountVolume.SetUp succeeded for volume "juicefs-volume1-pv" (UniqueName: "kubernetes.io/csi/csi.juicefs.com^juicefs-volume1-pv") pod "pod1" (UID: "<pod1-id>") 
10:05:59.506  kubernetes.io/csi: mounter.GetPath generated [/var/lib/k8s/kubelet/pods/<pod1-id>/volumes/kubernetes.io~csi/juicefs-volume1-pv/mount]

对于每个 volume,依次执行,

  1. operationExecutor.VerifyControllerAttachedVolume() 方法,做一些检查;
  2. operationExecutor.MountVolume() 方法,将指定的 volume 挂载到容器目录;
  3. 对于 CSI 存储,还会调用到 CSI plugin 的 NodePublishVolume() 方法,初始化对应的 PV,JuiceFS 就是这种模式。

接下来 kubelet 会不断检测所有 volumes 是否都挂载好,没好的话不会进入下一步(创建 sandbox 容器)。

Step 3:kubelet --> CSI plugin(juicefs):setup PV

下面进一步看一下 node CSI plugin 初始化 PV 挂载的逻辑。调用栈

         gRPC NodePublishVolume()
kubelet ---------------------------> juicefs node plugin (also called "driver", etc)

Step 4:JuiceFS CSI plugin 具体工作

看一下 JuiceFS CSI node plugin 的日志,这里直接在机器上看:

(node) $ docker logs --timestamps k8s_juicefs-plugin_juicefs-csi-node-xxx | grep juicefs-volume1
10:05:57.619 NodePublishVolume: volume_id is juicefs-volume1-pv

10:05:57.619 NodePublishVolume: creating dir /var/lib/k8s/kubelet/pods/<pod1-id>/volumes/kubernetes.io~csi/juicefs-volume1-pv/mount

10:05:57.620 ceFormat cmd: [/usr/local/bin/juicefs format --storage=OSS --bucket=xx --access-key=xx --secret-key=${secretkey} --token=${token} ${metaurl} juicefs-volume1]
10:05:57.874 Format output is juicefs <INFO>: Meta address: tikv://node1:2379,node2:2379,node3:2379/juicefs-volume1
10:05:57.874 cefs[1983] <INFO>: Data use oss://<bucket>/juicefs-volume1/

10:05:57.875 Mount: mounting "tikv://node1:2379,node2:2379,node3:2379/juicefs-volume1" at "/jfs/juicefs-volume1-pv" with options [token=xx]

10:05:57.884 createOrAddRef: Need to create pod juicefs-node1-juicefs-volume1-pv.
10:05:57.891 createOrAddRed: GetMountPodPVC juicefs-volume1-pv, err: %!s(<nil>)
10:05:57.891 ceMount: mount tikv://node1:2379,node2:2379,node3:2379/juicefs-volume1 at /jfs/juicefs-volume1-pv
10:05:57.978 createOrUpdateSecret: juicefs-node1-juicefs-volume1-pv-secret, juicefs-system
10:05:59.500 waitUtilPodReady: Pod juicefs-node1-juicefs-volume1-pv is successful

10:05:59.500 NodePublishVolume: binding /jfs/juicefs-volume1-pv at /var/lib/k8s/kubelet/pods/<pod1-id>/volumes/kubernetes.io~csi/juicefs-volume1-pv/mount with options []
10:05:59.505 NodePublishVolume: mounted juicefs-volume1-pv at /var/lib/k8s/kubelet/pods/<pod1-id>/volumes/kubernetes.io~csi/juicefs-volume1-pv/mount with options []

可以看到确实执行了 NodePublishVolume() 方法, 这个方法是每个 CSI plugin 方案各自实现的,所以里面做什么事情就跟存储方案有很大关系。 接下来具体看看 JuiceFS plugin 做的什么。

Step 4.1 给 pod PV 创建挂载路径,初始化 volume

默认配置下,每个 pod 会在 node 上对应一个存储路径,

(node) $ ll /var/lib/k8s/kubelet/pods/<pod-id>
containers/
etc-hosts
plugins/
volumes/

juicefs plugin 会在以上 volumes/ 目录内给 PV 创建一个对应的子目录和挂载点,

/var/lib/k8s/kubelet/pods/{pod1-id}/volumes/kubernetes.io~csi/juicefs-volume1-pv/mount

然后用 juicefs 命令行工具格式化

$ /usr/local/bin/juicefs format --storage=OSS --bucket=xx --access-key=xx --secret-key=${secretkey} --token=${token} ${metaurl} juicefs-volume1

例如,如果 JuiceFS 对接的是阿里云 OSS,上面就对应阿里云的 bucket 地址及访问秘钥。

Step 4.2 volume 挂载信息写入 MetaServer

此外,还会把这个挂载信息同步到 JuiceFS 的 MetaServer,这里用的是 TiKV,暂不展开:

Fig. JuiceFS as K8s CSI solution: workflow when a business pod is created (JuiceFS mountpod mode).

Step 4.3 JuiceFS plugin:如果 client pod 不存在,就创建一个

JuiceFS CSI plugin 判断这个 PV 在 node 上是否已经存在 client pod,如果不存在,就创建一个;存在就不用再创建了。

当 node 上最后一个使用某 PV 的业务 pod 销毁后,对应的 client pod 也会被 juicefs CSI plugin 自动删掉。

我们这个环境用的是 dynamic client pod 方式,因此会看到如下日志:

(node) $ docker logs --timestamps <csi plugin container> | grep 
...
10:05:57.884 createOrAddRef: Need to create pod juicefs-node1-juicefs-volume1-pv.
10:05:57.891 createOrAddRed: GetMountPodPVC juicefs-volume1-pv, err: %!s(<nil>)
10:05:57.891 ceMount: mount tikv://node1:2379,node2:2379,node3:2379/juicefs-volume1 at /jfs/juicefs-volume1-pv
10:05:57.978 createOrUpdateSecret: juicefs-node1-juicefs-volume1-pv-secret, juicefs-system
10:05:59.500 waitUtilPodReady:

JuiceFS node plugin 会去 k8s 里面创建一个名为 juicefs-{node}-{volume}-pv 的 dynamic client pod。

Fig. JuiceFS as K8s CSI solution: workflow when a business pod is created (JuiceFS mountpod mode).

Step 5:kubelet 监听到 client pod 创建事件

这时候 kubelet 的业务 pod 还没创建好,“伺候”它的 juicefs client pod 又来“请求创建”了:

(node) $ grep juicefs-<node>-<volume>-pv /var/log/kubernetes/kubelet.INFO | grep "received "
10:05:58.288 SyncPod received new pod "juicefs-node1-volume1-pv_juicefs-system", will create a sandbox for it

所以接下来进入创建 juicefs dynamic client pod 的流程。

兵马未动,粮草先行。juicefs client pod 没有好,业务 pod 即使起来了也不能读写 juicefs volume

Step 6:kubelet 创建 client pod

创建 client pod 的流程跟业务 pod 是类似的,但这个 pod 比较简单,我们省略细节,认为它直接就拉起来了。

查看这个 client pod 内运行的进程

(node) $ dk top k8s_jfs-mount_juicefs-node1-juicefs-volume1-pv-xx
/bin/mount.juicefs ${metaurl} /jfs/juicefs-volume1-pv -o enable-xattr,no-bgjob,allow_other,token=xxx,metrics=0.0.0.0:9567

/bin/mount.juicefs 其实只是个 alias,指向的就是 juicefs 可执行文件

(pod) $ ls -ahl /bin/mount.juicefs
/bin/mount.juicefs -> /usr/local/bin/juicefs

Step 7:client pod 初始化、FUSE 挂载

查看这个 client pod 干了什么:

root@node:~  # dk top k8s_jfs-mount_juicefs-node1-juicefs-volume1-pv-xx
<INFO>: Meta address: tikv://node1:2379,node2:2379,node3:2379/juicefs-volume1
<INFO>: Data use oss://<oss-bucket>/juicefs-volume1/
<INFO>: Disk cache (/var/jfsCache/<id>/): capacity (10240 MB), free ratio (10%), max pending pages (15)
<INFO>: Create session 667 OK with version: admin-1.2.1+2022-12-22.34c7e973
<INFO>: listen on 0.0.0.0:9567
<INFO>: Mounting volume juicefs-volume1 at /jfs/juicefs-volume1-pv ...
<INFO>: OK, juicefs-volume1 is ready at /jfs/juicefs-volume1-pv
  1. 初始化本地 volume 配置
  2. 与 MetaServer 交互
  3. 暴露 prometheus metrics
  4. 以 juicefs 自己的 mount 实现(前面看到的 /bin/mount.juicefs),将 volume 挂载到 /jfs/juicefs-volume1-pv,默认对应的是 /var/lib/juicefs/volume/juicefs-volume1-pv

此时在 node 上就可以看到如下的挂载信息

(node) $ cat /proc/mounts | grep JuiceFS:juicefs-volume1
JuiceFS:juicefs-volume1 /var/lib/juicefs/volume/juicefs-volume1-pv fuse.juicefs rw,relatime,user_id=0,group_id=0,default_permissions,allow_other 0 0
JuiceFS:juicefs-volume1 /var/lib/k8s/kubelet/pods/<pod-id>/volumes/kubernetes.io~csi/juicefs-volume1-pv/mount fuse.juicefs rw,relatime,user_id=0,group_id=0,default_permissions,allow_other 0 0

可以看到是 fuse.juicefs 方式的挂载。 忘了 FUSE 基本工作原理的,再来借 lxcfs 快速回忆一下:

Fig. lxcfs/fuse workflow: how a read operation is handled [2]

这个 dynamic client pod 创建好之后, 业务 pod(此时还不存在)的读写操作都会进入 FUSE 模块, 然后转发给用户态的 juicefs client 处理。juicefs client 针对不同的 object store 实现了对应的读写方法。

Step 8:kubelet 创建业务 pod:完成后续部分

至此,Pod 所依赖的 volumes 都处理好了,kubelet 就会打印一条日志:

# kubelet.INFO
10:06:06.119  All volumes are attached and mounted for pod "pod1(<pod1-id>)"

接下来就可以继续创建业务 pod 了:

# kubelet.INFO
10:06:06.119  No sandbox for pod "pod1(<pod1-id>)" can be found. Need to start a new one
10:06:06.119  Creating PodSandbox for pod "pod1(<pod1-id>)"
10:06:06.849  Created PodSandbox "885c3a" for pod "pod1(<pod1-id>)"
...

小结

更详细的 pod 创建过程,可以参考 [1]。

3 业务 pod 读写 juicefs volume 流程

juicefs dynamic client pod 先于业务 pod 创建,所以业务 pod 创建好之后,就可以直接读写 juicefs PV (volume) 了,

Fig. JuiceFS as K8s CSI solution: workflow when a business pod reads/writes (JuiceFS mountpod mode).

这个过程可以大致分为四步。

Step 1:pod 读写文件(R/W operations)

例如在 pod 内进入 volume 路径(e.g. cd /data/juicefs-pv-dir/),执行 ls、find 等等之类的操作。

Step 2:R/W 请求被 FUSE 模块 hook,转给 juicefs client 处理

直接贴两张官方的图略作说明 [3],这两张图也透露了随后的 step 3 & 4 的一些信息:

读操作:

Fig. JuiceFS Internals: read operations.

写操作:

Fig. JuiceFS Internals: write operations.

Step 3:juicefs client pod 从 meta server 读取(文件或目录的)元数据

上面的图中已经透露了一些 JuiceFS 的元数据设计,例如 chunk、slice、block 等等。 读写操作时,client 会与 MetaServer 有相关的元信息交互。

Step 4:juicefs client pod 从 object store 读写文件

这一步就是去 S3 之类的 object store 去读写文件了。

4 总结

以上就是使用 JuiceFS 作为 k8s CSI plugin 时,创建一个带 PV 的 pod 以及这个 pod 读写 PV 的流程。 限于篇幅,省略了很多细节,感兴趣的可移步参考资料。

参考资料

  1. 源码解析:K8s 创建 pod 时,背后发生了什么(系列)(2021)
  2. Linux 容器底层工作机制:从 500 行 C 代码到生产级容器运行时(2023)
  3. 官方文档:读写请求处理流程, juicefs.com
  4. kubernetes-csi.github.io/docs/, K8s CSI documentation

Written by Human, Not by AI Written by Human, Not by AI

[网盘] 任天堂Switch模拟器 yuzu 下载存档(Windows, Linux)[InfiniCLOUD][腾讯微云]

声明:

资源均源自网络,因使用本页面提供的资源链接产生的版权问题或计算机数据安全问题造成的任何损失,本站概不负责。您应该确保在您使用网络资源时已拥有适当抵御病毒的措施和其他安全措施。

本页面含有通向其他网站和资源的链接,这些链接仅供您参考。本站无法控制这些网站或资源的内容,对这些内容或因使用这些内容导致的任何损失或损害,本站亦概不负责。

  • InfiniCLOUD 无墙免注册免登录可以直接下载。
  • 腾讯微云 需要使用 QQ/微信 登录后才能下载。

模拟器资源

请根据文件名自行推断其版本号以及使用方式。

链接顺序与版本号无关。

Windows

yuzu-windows-msvc-1361

来源:Github Relese,已和谐,无从考证

2024-02-27-09-02-46-yuzu-mainline-yuzu-windows-msvc-20240226-da3bc8921.zip

来源:已和谐,无从考证

yuzu-1730.zip

来源:https://yuzu.cn.uptodown.com/windows/download/126081659

yuzu-1734.zip

来源:https://emuyuzu.com/

yuzu-windows-msvc-20240303-7ffac53c9

来源:已和谐,无从考证


Linux

yuzu-mainline-20240303-7ffac53c9.AppImage

来源:已和谐,无从考证

yuzu-1708-1-x86_64.pkg.tar.zst

来源:https://archlinux.pkgs.org/rolling/extra-alucryd-x86_64/yuzu-1708-1-x86_64.pkg.tar.zst.html

yuzu-mainline-git-1734.r0.g5372960-1-x86_64.pkg.tar

来源:https://archlinux.pkgs.org/rolling/chaotic-aur-x86_64/yuzu-mainline-git-1734.r0.g5372960-1-x86_64.pkg.tar.zst.html

yuzu-mainline-20240304-537296095.AppImage

来源:https://emuyuzu.com/


Firmware 固件

固件资源可以去来源页面下载,会有更新的版本

Firmware 15.0.1.zip

来源:https://prodkeys.net/yuzu-firmware/


ProdKeys

ProdKeys资源可以去来源页面下载,会有更新的版本

ProdKeys 15.0.1.zip

来源:https://prodkeys.net/prod-keys/


结尾

不包教包会,yuzu的简易安装说明可以参考我去年的文章:《用我的老电脑玩任天堂Switch模拟器》,只不过 GitHub 的资源已经完全被 DMCA Takedown 了,连 Wayback Machine 的内容都不剩。

百度吃屎去吧。

The post [网盘] 任天堂Switch模拟器 yuzu 下载存档(Windows, Linux)[InfiniCLOUD][腾讯微云] first appeared on 石樱灯笼博客.

微软剑桥研究院2024夏日派对 Microsoft Summer Party


除了疫情那几年,每年微软剑桥研究院都会有组织一个夏日聚会 Microsoft Summer Party。我是2021年加入微软剑桥研究院MSRC的(三年了),那一年因为疫情还没完全放开,所以没有举行,2022年的时候是在Duxford剑桥飞机博物馆,2023年的时候微软包了一整个动物园。今年是在剑桥河边的一个酒店里。具体地址是:Graduate Hotel, Granta Place, Mill Lane, Cambridge, CB2 1RT

微软剑桥研究院每年的这个Summer Party都是可以带家属的,很多小孩和小小孩,所以我们组那些没有娃的同事都不太愿意来,说是比较 Family oriented,他们去了没啥意思,不过经理说,好歹可以networking,拓展一下“人脉”,了解一下别的组做的事情。

经理本来说要来的,他说他女儿说想来,但他儿子比较大了,今年不太愿意来了,不过当天并没有看到经理,估计是临时计划有变。去年的派对我们组就我去了。

今年明显感觉公司的预算并没有前两年的多了,比如2022年的飞机博物馆,一家四口的门票也得七八十英镑,去年的动物园的门票也得好几十。今年在酒店是没有门票的。

酒店有一个大的房间,然后还有院子,唯一的亮点是院子在河边,院子里只有三个亲子游戏:大型国际象棋,扔沙包,还有抽积木(wooden tumbling blocks)。室内有专门给小孩子(三岁)以上画脸(Face Painting),还有气球玩具和手工等。基本上是给那些年纪小一点的孩子玩的。

吃的还可以,自助。10点到2点,我们1点多吃完就走了。当天的天气并不好,一会下雨一会儿晴天,但是六月份穿个短袖还是有点冷。

经理说,这个酒店在市中心,费用很贵。

microsoft-summer-party-cambridge-msrc-2024-06-08-10.02.14 微软剑桥研究院2024夏日派对 Microsoft Summer Party 工作 微软 派对 Party 照片 资讯

简陋的指示牌

microsoft-summer-party-cambridge-msrc-chess-big-2024-06-08-10.05.53-scaled 微软剑桥研究院2024夏日派对 Microsoft Summer Party 工作 微软 派对 Party 照片 资讯

酒店院子草地上摆着一个大型的国际象棋,不知道是不是微软特意为了这些派对临时摆上的

microsoft-summer-party-cambridge-msrc-2024-06-09-12.00.20 微软剑桥研究院2024夏日派对 Microsoft Summer Party 工作 微软 派对 Party 照片 资讯

开饭啦,等了有一小会儿,孩子都饿了。

microsoft-summer-party-cambridge-msrc-2024-06-09-11.59.16-scaled 微软剑桥研究院2024夏日派对 Microsoft Summer Party 工作 微软 派对 Party 照片 资讯

两娃在走国际象棋,没下完,大娃还拿手机拍下了残局。

microsoft-summer-party-cambridge-msrc-2024-06-09-11.57.57-scaled 微软剑桥研究院2024夏日派对 Microsoft Summer Party 工作 微软 派对 Party 照片 资讯

媳妇在观棋,不过她看不懂。

microsoft-summer-party-cambridge-msrc-2024-06-08-17.21.57 微软剑桥研究院2024夏日派对 Microsoft Summer Party 工作 微软 派对 Party 照片 资讯

媳妇让娃去帮她要一个气球玩具。她自己不好意思去拿。

microsoft-summer-party-cambridge-msrc-2024-06-08-17.13.01 微软剑桥研究院2024夏日派对 Microsoft Summer Party 工作 微软 派对 Party 照片 资讯

媳妇让娃去帮她要一个气球玩具。她自己不好意思去拿。

microsoft-summer-party-cambridge-msrc-2024-06-08-17.11.21 微软剑桥研究院2024夏日派对 Microsoft Summer Party 工作 微软 派对 Party 照片 资讯

每年都带媳妇来派对

microsoft-summer-party-cambridge-msrc-2024-06-08-17.09.31 微软剑桥研究院2024夏日派对 Microsoft Summer Party 工作 微软 派对 Party 照片 资讯

媳妇开心 因为她让两娃去给她要玩具,拿到了。真好满足。

microsoft-summer-party-cambridge-msrc-2024-06-08-17.04.37-scaled 微软剑桥研究院2024夏日派对 Microsoft Summer Party 工作 微软 派对 Party 照片 资讯

酒店在剑桥的河边上。

microsoft-summer-party-cambridge-msrc-2024-06-08-13.16.02 微软剑桥研究院2024夏日派对 Microsoft Summer Party 工作 微软 派对 Party 照片 资讯

媳妇在酒店里合影。

microsoft-summer-party-cambridge-msrc-2024-06-08-13.06.42 微软剑桥研究院2024夏日派对 Microsoft Summer Party 工作 微软 派对 Party 照片 资讯

这指示牌子有点简陋。

microsoft-summer-party-cambridge-msrc-2024-06-08-12.58.58 微软剑桥研究院2024夏日派对 Microsoft Summer Party 工作 微软 派对 Party 照片 资讯

媳妇在看儿子拿到了气球没。

microsoft-summer-party-cambridge-msrc-2024-06-08-12.33.26 微软剑桥研究院2024夏日派对 Microsoft Summer Party 工作 微软 派对 Party 照片 资讯

饭后甜点

microsoft-summer-party-cambridge-msrc-2024-06-08-11.40.00 微软剑桥研究院2024夏日派对 Microsoft Summer Party 工作 微软 派对 Party 照片 资讯

看着很一般,吃着也一般。

microsoft-summer-party-cambridge-msrc-2024-06-08-11.37.51 微软剑桥研究院2024夏日派对 Microsoft Summer Party 工作 微软 派对 Party 照片 资讯

自助餐

microsoft-summer-party-cambridge-msrc-2024-06-08-11.26.24-scaled 微软剑桥研究院2024夏日派对 Microsoft Summer Party 工作 微软 派对 Party 照片 资讯

酒店的会议室

microsoft-summer-party-cambridge-msrc-2024-06-08-11.13.25 微软剑桥研究院2024夏日派对 Microsoft Summer Party 工作 微软 派对 Party 照片 资讯

沙发里竟然是玉米粒

microsoft-summer-party-cambridge-msrc-2024-06-08-10.58.38 微软剑桥研究院2024夏日派对 Microsoft Summer Party 工作 微软 派对 Party 照片 资讯

微软派对室外亲子活动

microsoft-summer-party-cambridge-msrc-2024-06-08-10.29.06-scaled 微软剑桥研究院2024夏日派对 Microsoft Summer Party 工作 微软 派对 Party 照片 资讯

媳妇坐着 挺无聊的。

微软剑桥研究院/MSRC Summer Party

本文一共 1107 个汉字, 你数一下对不对.
微软剑桥研究院2024夏日派对 Microsoft Summer Party. (AMP 移动加速版本)

扫描二维码,分享本文到微信朋友圈
75a5a60b9cac61e5c8c71a96e17f2d9c 微软剑桥研究院2024夏日派对 Microsoft Summer Party 工作 微软 派对 Party 照片 资讯
The post 微软剑桥研究院2024夏日派对 Microsoft Summer Party first appeared on 小赖子的英国生活和资讯.

相关文章:

  1. 按揭贷款(房贷,车贷) 每月还贷计算器 去年给银行借了17万英镑 买了20万7500英镑的房子, 25年还清. 前2年是定率 Fix Rate 的合同 (年利率2.49%). 每个月大概是还 700多英镑. 有很多种还贷的计算方式, 定率/每月固定 是比较常用的. 简单来说就是 每个月交的钱是...
  2. 智能手机 HTC One M9 使用测评 虽然我对手机要求不高, 远远没有像追求VPS服务器一样, 但是怎么算来两年内换了四个手机, 先是三星 S4 用了一年多, 然后 Nokia Lumia 635 Windows Phone, 后来又是 BLU, 半年多前换了...
  3. 微软剑桥研究院/所 Summer Party (2022夏日派对) 大厂(特别是互联网大厂)每年都会有整个公司员工的团建活动. 微软每年都有一次夏日派对 (Summer Party) 还有一次圣诞派对 (Christmas Party). 当年我入职 Amazon 一周的时候正好参加了 冬日派对 Winter Party. 这种全公司的派对一年搞两次, 可以邀请家人...
  4. 在英国给孩子换学校的经历: 孩子离开了村里的小学 由于搬了家, 孩子上学得提前半小时出门了, 因为早上堵, 也得开车半小时才能到. 之前在 Fen Drayton 村庄上小学, 早上8:45学校门开, 9点敲钟孩子排队依次进入教室, 我们由于在村里, 只需要提前5分钟出门和孩子一起走路就可以了. 现在一下子早上变得很匆忙, 得叫孩子起床, 做早饭,...
  5. 同一台服务器上多个WORDPRESS站点的一些设置可以移出去 我自从把所有网站都挪到一处VPS服务器上 就发现很多事情省事很多 可以同时管理多个网站 包括 WORDPRESS博客. 比如我有四个WORDPRESS博客 然后我就把通用的一些资料给移出去 移到 HTTP或者HTTPS都不能直接访问的文件夹里这样就更安全许多. 文件 wp-conn.php 存储了 相同的数据库资料. 1 2...
  6. 公司请的专业摄影师 公司来了新的CEO管理之后,很多事情都不一样了, 特别是一些公司对外形象的事情就特别的在意, 比如公司网站用上SSL.现在公司还有空闲的位置,请速来(钱多人不傻). 一月份出差回LUTON,刚好公司请来摄影师给高层管理照像放网站上的,于是我也凑了凑热闹(但是却还不够资格被放在公司网站上),不过没关系,放这里也差不多. 人到中年, 沧桑感强了些. 更新更新: 同事用他NB的单反给谢菲尔得办公室的人也拍了一组这样的照片.看起来很不错, 很专业,灯光,道具应有尽有.我已经用在了LINKEDIN页面上,立马高大上. 本文一共 230 个汉字, 你数一下对不对. 公司请的专业摄影师. (AMP...
  7. Leetcode 的在线调试器 最近 leetcode 刷题网站出了一个在线调试器. 个人感觉非常好用. 因为我平时是用 IPAD+蓝牙键盘来刷题, 而在 ipad 上是没有集成的IDE的, 对于调试来说, 只能很原始的让函数退出一个值, 然后尝试不同的输入来发现问题. leetcode在线调试器的好处 理论上来说, 你可以直接在浏览器里解决任何一道...
  8. 优化设计 个人主页 并且 PageSpeed Insights 双项 100分 坛子的个人主页 www.tanzhijun.com 不错 很适合个人主页的模板. 而且是手机友好. 于是我照着把 我的主页改了改. https://steakovercooked.com 并且做了几点修改: 0. 使用 google mod_pagespeed 把 JS,...

Practical Storage Hierarchy and Performance: From HDDs to On-chip Caches(2024)

This post summarizes bandwidths for local storage media, networking infra, as well as remote storage systems. Readers may find this helpful when identifying bottlenecks in IO-intensive applications (e.g. AI training and LLM inference).

Fig. Peak bandwidth of storage media, networking, and distributed storage solutions.

Note: this post may contain inaccurate and/or stale information.



1 Fundamentals

Before delving into the specifics of storage, let’s first go through some fundamentals about data transfer protocols.

1.1 SATA

From wikepedia SATA:

SATA (Serial AT Attachment) is a computer bus interface that connects host bus adapters to mass storage devices such as hard disk drives, optical drives, and solid-state drives.

1.1.2 Real world pictures

Fig. SATA interfaces and cables on a computer motherboard. Image source wikipedia

1.1.1 Revisions and data rates

The SATA standard has evolved through multiple revisions. The current prevalent revision is 3.0, offering a maximum IO bandwidth of 600MB/s:

Table: SATA revisions. Data source: wikipedia

Spec Raw data rate Data rate Max cable length
SATA Express 16 Gbit/s 1.97 GB/s 1m
SATA revision 3.0 6 Gbit/s 600 MB/s 1m
SATA revision 2.0 3 Gbit/s 300 MB/s 1m
SATA revision 1.0 1.5 Gbit/s 150 MB/s 1m

1.2 PCIe

From wikipedia PCIe (PCI Express):

PCI Express is high-speed serial computer expansion bus standard.

PCIe (Peripheral Component Interconnect Express) is another kind of system bus, designed to connect a variety of peripheral devices, including GPUs, NICs, sound cards, and certain storage devices.

1.1.2 Real world pictures

Fig. Various slots on a computer motherboard, from top to bottom:
PCIe x4 (e.g. for NVME SSD)
PCIe x16 (e.g. for GPU card)
PCIe x1
PCIe x16
Conventional PCI (32-bit, 5 V)
Image source wikipedia

As shown in the above picture, PCIe electrical interface is measured by the number of lanes. A lane is a single data send+receive line, functioning similarly to a “one-lane road” with traffic in both directions.

1.2.2 Generations and data rates

Each new PCIe generation doubles the bandwidth of a lane than the previous generation:

Table: PCIe Unidirectional Bandwidth. Data source: trentonsystems.com

Generation Year of Release Data Transfer Rate Bandwidth x1 Bandwidth x16
PCIe 1.0 2003 2.5 GT/s 250 MB/s 4.0 GB/s
PCIe 2.0 2007 5.0 GT/s 500 MB/s 8.0 GB/s
PCIe 3.0 2010 8.0 GT/s 1 GB/s 16 GB/s
PCIe 4.0 2017 16 GT/s 2 GB/s 32 GB/s
PCIe 5.0 2019 32 GT/s 4 GB/s 64 GB/s
PCIe 6.0 2021 64 GT/s 8 GB/s 128 GB/s

Currently, the most widely used generations are Gen4 and Gen5.

Note: Depending on the document you’re referencing, PCIe bandwidth may be presented as either unidirectional or bidirectional, with the latter indicating a bandwidth that is twice that of the former.

1.3 Summary

With the above knowledge, we can now proceed to discuss the performance characteristics of various storage devices.

2 Disk

2.1 HDD: ~200 MB/s

From wikipedia HDD:

A hard disk drive (HDD) is an electro-mechanical data storage device that stores and retrieves digital data using magnetic storage with one or more rigid rapidly rotating platters coated with magnetic material.

2.1.1 Real world pictures

A real-world picture is shown below:

Fig. Internals of a real world HDD. Image source hardwaresecrets.com

2.1.2 Supported interfaces (bus types)

HDDs connect to a motherboard over one of several bus types, such as,

  • SATA
  • SCSI
  • Serial Attached SCSI (SAS)

Below is a SATA HDD:

Fig. A real world SATA HDD. Image source hardwaresecrets.com

and how an HDD connects to a computer motherboard via SATA cables:

Fig. An HDD with SATA cables. Data source datalab247.com

2.1.3 Bandwidth: constrained by machanical factors

HDDs are machanical devices, and their peak IO performance is inherently limited by various mechanical factors, including the speed at which the actuator arm can function. The current upper limit of HDDs is ~200MB/s, which is significantly below the saturation point of a SATA 3.0 interface (600MB/s).

2.1.4 Typical latencies

Table. Latency characteristics typical of HDDs. Data source: wikipedia

Rotational speed (rpm) Average rotational latency (ms)
15,000 2
10,000 3
7,200 4.16
5,400 5.55
4,800 6.25

2.2 SATA SSD: ~600MB/s

What’s a SSD? From wikipedia SSD:

A solid-state drive (SSD) is a solid-state storage device. It provides persistent data storage using no moving parts.

Like HDDs, SSDs support several kind of bus types:

  • SATA
  • PCIe (NVME)

Let’s see the first one: SATA-interfaced SSD, or SATA SSD for short.

2.2.1 Real world pictures

SSDs are usually smaller than HDDs,

Fig. Size of different drives, left to right: HDD, SATA SSD, NVME SSD. Image source avg.com

2.2.2 Bandwidth: constrained by SATA bus

The absence of mechanical components (such as rotational arms) allows SATA SSDs to fully utilize the capabilities of the SATA bus. This results in an upper limit of 600MB/s IO bandwidth, which is 3x faster than that of SATA HDDs.

2.3 NVME SSD: ~7GB/s, ~13GB/s

Let’s now explore another type of SSD: the PCIe-based NVME SSD.

2.3.1 Real world pictures

NVME SSDs are even smaller than SATA SSDs, and they connect directly to the PCIe bus with 4x lanes instead of SATA cables,

Fig. Size of different drives, left to right: HDD, SATA SSD, NVME SSD. Image source avg.com

2.3.2 Bandwidth: contrained by PCIe bus

NVME SSDs has a peak bandwidth of 7.5GB/s over PCIe Gen4, and ~13GB/s over PCIe Gen5.

2.4 Summary

We illustrate the peak bandwidths of afore-mentioned three kinds of local storage media in a graph:

Fig. Peak bandwidths of different storage media.

These (HDDs, SSDs) are commonly called non-volatile or persistent storage media. And as the picture hints, in next chapters we’ll delve into some other kinds of storage devices.

3 DDR SDRAM (CPU Memory): ~400GB/s

DDR SDRAM nowadays serves mainly as the main memory in computers.

3.1 Real world pictures

Fig. Front and back of a DDR RAM module for desktop PCs (DIMM). Image source wikipedia

Fig. Corsair DDR-400 memory with heat spreaders. Image source wikipedia

DDR memory connects to the motherboard via DIMM slots:

Fig. Three SDRAM DIMM slots on a ABIT BP6 computer motherboard. Image source wikipedia

3.2 Bandwidth: contrained by memory clock, bus width, channel, etc

Single channel bandwidth:

  Transfer rate Bandwidth
DDR4 3.2GT/s 25.6 GB/s
DDR5 4–8GT/s 32–64 GB/s

if Multi-channel memory architecture is enabled, the peak (aggreated) bandwidth will be increased by multiple times:

Fig. Dual-channel memory slots, color-coded orange and yellow for this particular motherboard. Image source wikipedia

Such as [4],

  • Intel Xeon Gen5: up to 8 memory-channels running at up to 5600MT/s (358GB/s)
  • Intel Xeon Gen4: up to 8 memory-channels running at up to 4800MT/s (307GB/s)

3.3 Summary

DDR5 bandwidth in the hierarchy:

Fig. Peak bandwidths of different storage media.

4 GDDR SDRAM (GPU Memory): ~1000GB/s

Now let’s see another variant of DDR, commonly used in graphics cards (GPUs).

4.1 GDDR vs. DDR

From wikipedia GDDR SDRAM:

Graphics DDR SDRAM (GDDR SDRAM) is a type of synchronous dynamic random-access memory (SDRAM) specifically designed for applications requiring high bandwidth, e.g. graphics processing units (GPUs).

GDDR SDRAM is distinct from the more widely known types of DDR SDRAM, such as DDR4 and DDR5, although they share some of the same features—including double data rate (DDR) data transfers.

4.2 Real world pictures

Fig. Hynix GDDR SDRAM. Image Source: wikipedia

4.3 Bandwidth: contrained by lanes & clock rates

Unlike DDR, GDDR is directly integrated with GPU devices, bypassing the need for pluggable PCIe slots. This integration liberates GDDR from the bandwidth limitations imposed by the PCIe bus. Such as,

  • GDDR6: 1008GB/s. Peak per-pin data rate 16Gb/s, max memory bus width 384-bits.
  • GDDR6x: 1008GB/s, used by NVIDIA RTX 4090

4.4 Summary

With GDDR included:

Fig. Peak bandwidths of different storage media.

5 HBM: 1~5 TB/s

If you’d like to achieve even more higher bandwidth than GDDR, then there is an option: HBM (High Bandwidth Memory).

A great innovation but a terrible name.

5.1 What’s new

HBM is designed to provide a larger memory bus width than GDDR, resulting in larger data transfer rates.

Fig. Cut through a graphics card that uses HBM. Image Source: wikipedia

HBM sits inside the GPU die and is stacked – for example NVIDIA A800 GPU has 5 stacks of 8 HBM DRAM dies (8-Hi) each with two 512-bit channels per die, resulting in a total width of 5120-bits (5 active stacks * 2 channels * 512 bits) [3].

As another example, HBM3 (used in NVIDIA H100) also has a 5120-bit bus, and 3.35TB/s memory bandwidth,

Fig. Bandwidth of several HBM-powered GPUs from NVIDIA. Image source: nvidia.com

5.2 Real world pictures

The 4 squares in left and right are just HBM chips:

Fig. AMD Fiji, the first GPU to use HBM. Image Source: wikipedia

5.3 Bandwidth: contrained by lanes & clock rates

From wikipedia HBM

  Bandwidth Year GPU
HBM 128GB/s/package    
HBM2 256GB/s/package 2016 V100
HBM2e ~450GB/s 2018 A100, ~2TB/s; Huawei Ascend 910B
HBM3 600GB/s/site 2020 H100, 3.35TB/s
HBM3e ~1TB/s 2023 H200, 4.8TB/s

5.4 HBM-powered CPUs

HBM is not exclusive to GPU memory; it is also integrated into some CPU models, such as the Intel Xeon CPU Max Series.

5.5 Summary

This chapter concludes our exploration of dynamic RAM technologies, which includes

  • DDR DRAM
  • GDDR DRAM
  • HBM DRAM

Fig. Peak bandwidths of different storage media.

In the next, let’s see some on-chip static RAMs.

6 SRAM (on-chip): 20+ TB/s

The term “on-chip” in this post refers to memory storage that's integrated within the same silicon as the processor unit.

6.1 SRAM vs. DRAM

From wikipedia SRAM:

Static random-access memory (static RAM or SRAM) is a type of random-access memory that uses latching circuitry (flip-flop) to store each bit. SRAM is volatile memory; data is lost when power is removed.

The term static differentiates SRAM from DRAM:

  SRAM DRAM
data freshness stable in the presence of power decays in seconds, must be periodically refreshed
speed (relative) fast (10x) slow
cost (relative) high low
mainly used for cache main memory

SRAM requires more transistors per bit to implement, so it is less dense and more expensive than DRAM and also has a higher power consumption during read or write access. The power consumption of SRAM varies widely depending on how frequently it is accessed.

6.2 Cache hierarchy (L1/L2/L3/…)

In the architecture of multi-processor (CPU/GPU/…) systems, a multi-tiered static cache structure is usually used:

  • L1 cache: typically exclusive to each individual processor;
  • L2 cache: commonly accessible by a group of processors.

NVIDIA H100 chip layout (L2 cache in the middle, shared by many SM processors). Image source: nvidia.com

6.3 Groq LPU: eliminating memory bottleneck by using SRAM as main memory

From the official website: Groq is the AI infra company that builds the world’s fastest AI inference technology with both software and hardware. Groq LPU is designed to overcome two LLM bottlenecks: compute density and memory bandwidth.

  • An LPU has greater compute capacity than a GPU and CPU in regards to LLMs. This reduces the amount of time per word calculated, allowing sequences of text to be generated much faster.
  • Eliminating external memory bottlenecks (using on-chip SRAM instead) enables the LPU Inference Engine to deliver orders of magnitude better performance on LLMs compared to GPUs.

Regarding to the chip:

Fig. Die photo of 14nm ASIC implementation of the Groq TSP. Image source: groq paper [2]

The East and West hemisphere of on-chip memory module (MEM)

  • Composed of 44 parallel slices of SRAM and provides the memory concurrency necessary to fully utilize the 32 streams in each direction.
  • Each slice provides 13-bits of physical addressing of 16-byte memory words, each byte maps to a lane, for a total of 220 MiBytes of on-chip SRAM.

6.4 Bandwidth: contrained by clock rates, etc

6.5 Summary

This chapter ends our journey to various physical storage media, from machanical devices like HDDs all the way to on-chip cache. We illustrate their peak bandwidth in a picture, note that the Y-axis is log10 scaled:

Fig. Speeds of different storage media.

These are the maximum IO bandwidths when performing read/write operations on a local node.

Conversely, when considering remote I/O operations, such as those involved in distributed storage systems like Ceph, AWS S3, or NAS, a new bottleneck emerges: networking bandwidth.

7 Networking bandwidth: 400GB/s

7.1 Traditional data center: 2*{25,100,200}Gbps

For traditional data center workloads, the following per-server networking configurations are typically sufficient:

  • 2 NICs * 25Gbps/NIC, providing up to 6.25GB/s unidirectional bandwidth when operating in active-active mode;
  • 2 NICs * 100Gbps/NIC, delivering up to 25GB/s unidirectional bandwidth when operating in active-active mode;
  • 2 NICs * 200Gbps/NIC, achieving up to 50GB/s unidirectional bandwidth when operating in active-active mode.

7.2 AI data center: GPU-interconnect: 8*{100,400}Gbps

This type of networking facilitates inter-GPU communication and is not intended for general data I/O. The data transfer pathway is as follows:

            HBM <---> NIC <---> IB/RoCE <---> NIC <--> HBM
                Node1                            Node2

7.3 Networking bandwidths

Now we add networking bandwidths into our storage performance picture:

Fig. Speeds of different storage media, with networking bandwidth added.

7.4 Summary

If remote storage solutions (such as distributed file systems) is involved, and networking is fast enough, IO bottleneck would shift down to the remote storage solutions, that’s why there are some extremely high performance storage solutions dedicated for today’s AI trainings.

8 Distributed storage: aggregated 2+ TB/s

8.1 AlibabaCloud CPFS

AlibabaCloud’s Cloud Parallel File Storage (CPFS) is an exemplar of such high-performance storage solutions. It claims to offer up to 2TB/s of aggregated bandwidth.

But, note that the mentioned bandwidth is an aggregate across multiple nodes, no single node can achieve this level of IO speed. You can do some calcuatations to understand why, with PCIe bandwidth, networking bandwidth, etc;

8.2 NVME SSD powered Ceph clusters

An open-source counterpart is Ceph, which also delivers impressive results. For instance, with a cluster configuration of 68 nodes * 2 * 100Gbps/node, a user achieved aggregated throughput of 1TB/s, as documented.

8.3 Summary

Now adding distributed storage aggregated bandwidth into our graph:

Fig. Peak bandwidth of storage media, networking, and distributed storage solutions.

9 Conclusion

This post compiles bandwidth data for local storage media, networking infrastructure, and remote storage systems. With this information as reference, readers can evaluate the potential IO bottlenecks of their systems more effectively, such as GPU server IO bottleneck analysis [1]:

Fig. Bandwidths inside a 8xA100 GPU node

References

  1. Notes on High-end GPU Servers (in Chinese), 2023
  2. Think Fast: A Tensor Streaming Processor (TSP) for Accelerating Deep Learning Workloads, ISCA paper, 2020
  3. GDDR6 vs HBM - Defining GPU Memory Types, 2024
  4. 5th Generation Intel® Xeon® Scalable Processors, intel.com

Written by Human, Not by AI Written by Human, Not by AI

新标签页助你重新掌控你的收藏夹

在数字时代,我们的浏览器收藏夹,就如同一个宝库,里面藏着我们曾经因兴趣、工作或学习而保留的无数网站链接。小舒同学,一个浏览器新标签页扩展,帮助您高效地管理这些珍贵的在线资源。通过整洁、直观的界面和强大的功能,通过小舒同学这款强大的浏览器扩展,让您的收藏夹焕发新生。

一键展示管理收藏夹

不需要反复整理和搜索您的主页,小舒同学提供了一种轻松的方法来展示和管理您的收藏夹。通过在新标签页中,将书签以清晰的图形方式呈现,帮助您聚焦真正重要的事情。无论是微软的 Edge、Google 的 Chrome,还是 Mozilla 的 Firefox,都可以实现一步直观整洁地展示管理您的网络财富。

一键展示管理收藏夹

随时随地的同步空间

小舒同学的同步空间可以让您轻松同步收藏夹,通过同步空间功能,不管您是在工作场合的电脑还是在家里的设备,都可以轻松连接,访问您全部的收藏内容。您的内容将被安全地存储与同步,确保您可以在任何时间访问。网络世界的一切都将由您连接统一,不仅能启动哔哩哔哩,更能直接访问您关注的具体内容。

同步空间

网页端

直观且舒适的重组织

小舒同学以流畅和直观的用户体验,帮您重新组织您的在线生活。我们专注于舒适的交互设计,将您的收藏夹以一种对您有意义的方式进行组织。

小舒同学提供各式各样的卡片形式和布局,让您可以完全按照自己的喜好进行自定义。您可以创建个性化的卡片组合,以满足您的独特需求。

重组织

严格隐私保护

小舒同学重视用户的隐私保护,拒绝记录和上传用户数据。在不使用同步空间的情况下,您的收藏夹数据也只在浏览器本地数据中,确保隐私数据的私密性。依托于 Manifest V3,小舒同学严格控制所需权限,保证只加载本地代码,保护用户的安全与私密。

定制个性化新标签页

小舒同学的自由度极高,可以按照用户的喜好进行多种个性化设置。您可以更换壁纸、主题、主题色,甚至可以用自定义 CSS 美化您的标签页。拥有如此高度自定义的新标签页,您的浏览器将成为真正属于您的个性空间。

主题 Fluid

主题 Meteor

主题 Hyper

小舒同学不仅仅是一个简单的收藏夹工具,它改变了我们与网络信息的互动方式,提供了一个更高效、更有组织性的网络生活体验。减少了无谓的折腾,让每一次收藏都变得值得,让每一次浏览都充满发现的喜悦。现在,尝试小舒同学,让您的收藏夹变成您的独特网络资产,发挥它们真正的潜力吧。

❌