从一个不寻常的 I/O 卡顿入手,发现苹果 APFS 的一个严重 bug。
近期有用户反馈频繁遇到了一个奇怪的严重卡顿问题,微信刷朋友圈和查看聊天都非常卡,主线程卡在最普通的 access, rename 等常见 I/O 系统调用,并且经常卡上百 ms,而这种场景的底层接口一般都没干什么大量的 I/O 操作。比如 access 接口也就是获取文件是否存在的轻量操作,正常耗时都只有几十 us 而已,远达不到此时的上百 ms 耗时。
堆栈上看,只是很常规的视频号卡片列表滑动时,触发了下载图片和查图片本地缓存的逻辑,通过 access 接口同步查本地图片是否存在,有则直接展示,否则从网络下载图片,下载完成时再尝试删除可能已有的旧文件。这完全是一个非常简单的图片缓存和加载逻辑。
通过搜寻卡顿报告,发现子线程都疑似存在大量的并发 I/O 操作,那是否卡顿的主因是和并发 I/O 有关呢?
尝试触发子线程并发 I/O 这个目录的图片,并打日志输出 access 接口的平均耗时,一切正常。走查功能也一切正常,毫无卡顿。
有同学说可能是目录下文件过多才会有 I/O 问题,在对应目录下构造了足够多的文件,再次走查业务功能,还是一切正常。
最终在多次试验和猜测后,构造出了一个高概率复现的场景,在对应目录下写入10万个小图片伪造图片数据,并触发并发 I/O,此时问题终于复现了。这个时候如果触发视频号卡片滑动,朋友圈卡片滑动,就大概率必现严重的滑动掉帧和卡顿了。
大概知道了必现路径后,我们构造出了一个必现代码,打开 Instruments 的 System Trace 分析,结果如下:
发现 access 等常规 I/O 接口的平均耗时依旧很低只有几十 us,但等待耗时波动很大,可以达到140 ms,也就导致了主线程每次查询图片存在状态时,单次调用耗时超过了140 ms,而滑动过程中大概存在十几次这样的行为,那最终就是每次滑动都要因为这些 I/O wait time 导致滑动耗时数秒之久,甚至个别情况下还会因此滑动卡死触发 watchdog。
继续分析 Instruments 报告,发现等待的主因如下:will wait for event/lock xxx.
经过前面的研究,我们已经能够构造一个必现 demo 了。大概如下:
- 特定目录下写入大约10万个文件
- 主线程触发频繁的 access 接口调用,统计平均耗时
- 子线程触发对该目录下的文件遍历并频繁的 rename 操作调用,统计平均耗时
如果2和3是同一个目录且当前目录文件数较多时,那么会高概率稳定复现平均 access 和 rename 等 I/O 接口调用 调用耗时过高的问题。而其它情况组合下,都不会复现这个问题。
测试工程跑在 MacBook Pro(2019) macOS 12.3 上,会定时 benchmark 测 access 接口耗时。
int retry = 5;
long long duration = benchmark(retry,^{
access(path.UTF8String, F_OK);
});
if(duration > 1000 * retry) {
//avg >1 ms.
LOG_P("lag: avg access %.3f ms",duration*1.f/1000/retry);
}
在 APFS 分区的该目录下会频繁因大目录并发 I/O 遍历导致 access 超时问题,log输出如下:
lag: avg access 2.134 ms
lag: avg access 11.859 ms
lag: avg access 5.483 ms
lag: avg access 5.259 ms
lag: avg access 4.634 ms
这时在 x86 的 ssd 设备上都能稳定复现出 access 调用平均耗时 1ms 以上,个别情况下可以达到几十ms,确实令人费解。
既然 Instruments 告诉我们耗时的主因是 wait for lock,那接下来我们尝试分析下到底在 wait 什么 lock 最多。这就需要借助于 dtrace 来进一步分析了。
DTrace 即动态追踪技术(Dynamic Tracing),是内核提供的高级动态调试能力,可以帮助开发者快速调试定位一些奇怪的疑难杂症。操作系统会在内核的一些关键调用操作里提供 trace 代码执行的入口,我们可以通过注入 trace 命令或代码来实现自定义 trace 分析。Xcode Instruments 本质就是基于内核提供的 dtrace 能力来封装并实现的。
由于 iOS 平台不支持自定义 dtrace (虽然Instruments 就是基于 dtrace 的,但 iOS 即便越狱了也没办法触发自定义 dtrace 行为), 我们只有基于 macOS 打开 dtrace 分析下这个时候到底发生了什么。运行 demo ,多次跑如下 dtrace 命令分析 demo 运行状态。
sudo dtrace -n 'lockstat:::adaptive-block { @[stack()] = sum(arg1); }' -p 95637
sudo dtrace -n 'profile-999 /arg0/ { @[stack()] = count(); }' -p 95806
sudo lockstat sleep 10
得到的 dtrace 相关数据如下:
kernel`0xffffff8005aa6990+0x72
apfs`apfs_vnop_rename+0x94
kernel`vn_rename+0x4ae
kernel`0xffffff8005d42020+0xb12
kernel`unix_syscall64+0x1fb
kernel`hndl_unix_scall64+0x16
1367
kernel`lck_mtx_lock_spinwait_x86+0x2be
kernel`vn_rename+0x4ae
kernel`0xffffff8005d42020+0xb12
kernel`unix_syscall64+0x1fb
kernel`hndl_unix_scall64+0x16
2242
kernel`0xffffff8005aa67f0+0x7e
kernel`namei+0x9f7
kernel`0xffffff8005d375a0+0x79
kernel`0xffffff8005d42020+0x35b
kernel`unix_syscall64+0x1fb
kernel`hndl_unix_scall64+0x16
2262
R/W writer blocked by readers: 239 events in 10.048 seconds (24 events/sec)
Count indv cuml rcnt abs Lock Caller
-------------------------------------------------------------------------------
227 95% 95% 0.00 13385718 0xffffff8b638223e0 apfs_vnop_lookup+0x2f6
11 5% 100% 0.00 8140855 0xffffff8b638223e0 apfs_vnop_getattr+0xc4
1 0% 100% 0.00 2610265 0xffffff90339a97b0 omap_get+0x7c
-------------------------------------------------------------------------------
R/W reader blocked by writer: 192 events in 10.021 seconds (19 events/sec)
Count indv cuml rcnt abs Lock Caller
-------------------------------------------------------------------------------
129 67% 67% 0.00 6408120 0xffffff8b638223e0 IORWLockWrite+0x90
22 11% 79% 0.00 23902 0xffffff99cd1fbc00 IORWLockWrite+0x90
12 6% 85% 0.00 28194 0xffffff804e970608 IORWLockWrite+0x90
4 2% 87% 0.00 18808 0xffffff8048892e08 IORWLockWrite+0x90
3 2% 89% 0.00 41491 0xffffff803825e608 lck_rw_lock_exclusive_check_contended+0x93
2 1% 90% 0.00 22725 0xffffff99cd1fbc80 IORWLockWrite+0x90
2 1% 91% 0.00 78215 0xffffff8b666c2610 IORWLockWrite+0x90
2 1% 92% 0.00 34049 0xffffff8b666c22c8 IORWLockWrite+0x90
2 1% 93% 0.00 23949 0xffffff8b666bc4c0 IORWLockWrite+0x90
2 1% 94% 0.00 28546 0xffffff8b666b94c0 IORWLockWrite+0x90
2 1% 95% 0.00 38088 0xffffff804e97ab08 lck_rw_lock_exclusive_check_contended+0x93
2 1% 96% 0.00 17158 0xffffff803dc78d08 0xffffff8005aa77d0
2 1% 97% 0.00 18658 0xffffff803dc78d08 IORWLockWrite+0x90
2 1% 98% 0.00 29096 tcbinfo+0x38 IORWLockWrite+0x90
1 1% 98% 0.00 52994 0xffffff8b666be7a0 IORWLockWrite+0x90
1 1% 99% 0.00 20135 0xffffff8048892e08 0xffffff8005aa77d0
1 1% 99% 0.00 18584 0xffffff80345de608 IORWLockWrite+0x90
1 1% 100% 0.00 13805 0xffffff803825e608 IORWLockWrite+0x90
-------------------------------------------------------------------------------
通过 dtrace 的 lockstat 数据,大概率怀疑是 kernel 层的 APFS 相关的 lock 出了问题,那为什么 APFS lock 会导致如此严重的问题呢?
rename 和 access 都是系统调用,他们都是 XNU 里 VFS 注册的系统服务。APFS 的系统支持是通过系统的 apfs.kext 内核扩展载入的,我们通过 Hopper 打开 apfs.kext,分析下 APFS 对应的 rename 或 access 里到底干了什么
_apfs_vnop_renamex
{
r12 = arg0;
r14 = *(int32_t *)(arg0 + 0x40);
r14 = r14 & 0xfffffff7;
if (r14 < 0x3) goto loc_5ad56;
loc_5ad47:
rax = 0x2d;
if (r14 != 0x4) goto .l17;
loc_5ad56:
var_C8 = 0x0;
var_170 = 0x0;
var_190 = 0x0;
r15 = _vfs_fsprivate(_vnode_mount(*(r12 + 0x8))); //获取目录
r13 = _vnode_fsnode(*(r12 + 0x8));
var_58 = _vnode_fsnode(*(r12 + 0x20));
//... ....
rax = _current_thread();
var_120 = rax;
if (rbx != rax) {
_lck_rw_lock_shared(var_108); // 获取锁
}
apf.kext 的代码里 vfs_fsprivate 返回了一个结构,这个结构存了每个 vnode 相关的附加字段,比如这里会疑似返回一个目录相关的锁,每次执行 rename 接口时,会取出目录锁,尝试加锁处理,而在 apfs.kext 代码里还有很多处额外的高频加锁逻辑。
通过进一步 System Trace 验证,当并发 I/O 遍历的文件目录是同一个时,Instruments 报告里的 will wait for lock xxx 会显示为同一个,大概率证明了 APFS 内部存在某种目录锁的结构,当对同一个目录的文件进行遍历 I/O 操作时,都会先请求加解锁。而在超大目录遍历时这个加锁导致的等待问题会急剧扩大,导致锁等待超时,最终可能导致了并发 I/O 速度骤降的问题。
我们在同一台电脑上构造了两个不同的磁盘分区:APFS 和 HFS+,分别在各自分区下的同一路径下写入了相同数据的10万个文件,接着开始跑同样的测试程序,又发现了更出人意料的结论:
HFS+ 测试如下:
APFS 测试如下:
通过多次对比测试,发现在这种超大目录文件遍历的并发 I/O 情况下,HFS+ 的平均读写速度要比 APFS 快 8~20 倍,想不到 APFS 竟然反而比 HFS+ 要慢那么多。这个问题在 macOS 12.3 和 iOS 15.4 上都可以稳定构造出必现测试用例。
回到开始的卡顿问题,通过构造 demo 我们稳定复现了该问题,同时继续分析用户的相关数据,确认了用户遇到问题时的情况的确和我们最终 demo 构造的基本一致,至此又一个奇怪的卡顿问题终于找到原因了:超大目录导致了并发 I/O 时性能急剧下降。
既然我们已经明白了问题的根因超大目录结构导致的锁等待超时问题,那解决问题的办法就简单多了,将目录拆分为二级目录即可。
通过部分统计分析发现的确有很大一批老用户也存在这个超大目录结构的问题,目前微信已经将大部分超大目录逐步拆分为二级目录了。通过每级1000 个文件/夹,二级目录可以存储100万个文件,绰绰有余了。
经过测试,将该目录拆分为二级目录并缓解单目录文件过多的情况后,再也没有遇到类似的并发 I/O 卡顿的情况了。另外,该问题已经反馈给苹果,苹果内部也开了 radar ,希望可以进一步提升 APFS 性能。
结合已有数据分析,发现苹果的文件操作里还存在一些坑,如下:
苹果在 [File System Programming Guide] 里建议 app 用 tmp/ 目录来存储临时文件,并且说系统会在 app 不运行时自动删除 tmp/ 目录的数据,但是实测,大概率不会,因为文档写的是 may purge。极端情况下该目录可能会存在 >90G 的占用。
WebKit 的网络缓存默认在 /Library/Caches/WebKit/NetworkCache/ ,按照 WebKit 源码 NetworkCacheStorage 里的实现,WebKit 会采取一定随机+权重的方式删除网络缓存,但是极端情况下,个别用户会因此而占用 10G 或更多的存储空间。也就是这个随机删除存在很大的随机性。
NSURLCache 自定义磁盘缓存路径时,如果 diskCapacity 设置过大,会导致占用超大存储空间,同时也会导致网络请求因为 I/O 读写慢而变慢。
实测部分情况下删文件的 I/O 量基本等于写文件的 I/O 量,而且密集删文件时会容易导致 I/O 性能下降过快。因此业务应尽量避免短时间大量密集 I/O。
System Trace 数据表明:当并发 I/O 遍历的文件目录是同一个时,Instruments 报告里的 will wait for lock xxx 会显示为同一个,也就进一步证明了 APFS 内部存在某种目录锁的结构,当对同一个目录的文件进行遍历 I/O 操作时,都会先请求加解锁。而在超大目录遍历时这个加锁导致的等待问题会急剧扩大,导致锁等待超时,最终可能导致了并发 I/O 速度骤降的问题。
为了避免这种极端情况导致的 I/O 性能骤降问题, 移动端 app 也需要合理的设计存储结构。例如需要分层分级管理文件,尽量不要将单个文件夹或单个文件搞的过大,同时也需要定时清理临时缓存目录,来进一步优化存储空间占用和优化 I/O 效率。
苹果从 iOS10.3 开始引入了 APFS,而在此之前 HFS+ 一直是作为 iOS 和 macOS 的文件系统。
应用程序是如何从 ssd 等存储介质上读写文件的呢?如下图:
VFS 统一并抽象了不同文件系统的接口,使得用户可以通过统一的系统调用接口去访问不同文件系统不同存储介质上的文件。VFS 主要可以被抽象为3层,vfstbllist 用于管理不同的文件系统,mount 管理文件系统的挂载,vnode 则抽象代表了文件和文件夹等对象。
XNU 中主要使用 vfstbllist 来注册管理多个文件系统,典型的 vfstlblist 如下:
/*
* Set up the filesystem operations for vnodes.
*/
static struct vfstable vfstbllist[] = {
/* HFS/HFS+ Filesystem */
#if HFS
{ &hfs_vfsops, "hfs", 17, 0, (MNT_LOCAL | MNT_DOVOLFS), hfs_mountroot, NULL, 0, 0, VFC_VFSLOCALARGS | VFC_VFSREADDIR_EXTENDED | VFS_THREAD_SAFE_FLAG | VFC_VFS64BITREADY | VFC_VFSVNOP_PAGEOUTV2 | VFC_VFSVNOP_PAGEINV2, NULL, 0},
#endif
/* Sun-compatible Network Filesystem */
#if NFSCLIENT
{ &nfs_vfsops, "nfs", 2, 0, 0, NULL, NULL, 0, 0, VFC_VFSGENERICARGS | VFC_VFSPREFLIGHT | VFS_THREAD_SAFE_FLAG | VFC_VFS64BITREADY | VFC_VFSREADDIR_EXTENDED, NULL, 0},
#endif
#ifndef __LP64__
#endif /* __LP64__ */
... ...
{NULL, "<unassigned>", 0, 0, 0, NULL, NULL, 0, 0, 0, NULL, 0},
{NULL, "<unassigned>", 0, 0, 0, NULL, NULL, 0, 0, 0, NULL, 0}
};
内核可以动态的通过 vfs_fsadd 等接口来加载不同的内核扩展,以启用并支持新的文件系统。
文件系统只有被 mount 挂载后才可以被访问。对于内核支持的文件系统,macOS 会自动 从 /System/Library/FileSystems 里找到对应的内核扩展并挂载,而对于内核不支持的文件系统,则需要触发一次 kext 加载操作以支持对应的文件系统。macOS 上常见的 mount 操作如下图:
vnode 是 VFS 中最主要的组成。一个 vnode 可以代表一个文件或特定的一个文件系统对象。一个 vnode 一般对应实际的文件系统的对应 inode。
HFS+(Hierarchical File System Plus) 是 Mac OS 8.1(1998年) 开始引入的文件系统,同时也是 iOS 10.3 以前默认的文件系统。HFS+ 能更好的利用磁盘空间,使用 unicode 存储文件编码,提供了更多当时来说更现代的文件系统支持。HFS+ 使用了 Catalog File 来存储目录结构。Catalog File 的引入极大的提升了文件系统的查找速度,但也导致了所有 I/O 都会因为频繁访问 Catalog File 而被迫串行等待,导致超大并发性能会有较大下降。
APFS(Apple File System) 是苹果推出的最新文件系统,它是 HFS+ 的接任者,解决了 HFS+ 在更现代的文件系统上所缺失的能力。APFS 为 ssd 而设计和优化,新增了 cloning, snapshots, space sharing, fast directory sizing, atomic safe-save, sparse files等特性,并补齐了苹果在现代文件系统能力上的缺失。
苹果的 APFS 和安卓设备的 F2FS 类似,都是专门为移动设备而优化的文件系统。二者设计上有很多异曲同工之处。
以 rename 调用为例,开发者通过触发 rename 系统调用向 VFS 请求文件操作,VFS 触发 vn_rename 调用,如果当前目录使用的分区是 APFS,则最终会触发 apfs_vnop_renamex,而如果是 HFS+ 分区,则会触发 hfs_vnop_rename 调用,最终完成 rename 操作。
[Mac OS X and iOS Internals: To the Apple's Core]
[MacOS and iOS Internals, Volume II: Kernel Mode]
[DTrace]
[File System Programming Guide]
[HFS Plus Volume Format]
[Apple File System Reference]
[Apple File System Guide]
[XNU]
本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/_bdY3hvd8VBEAfgxmMVf_A
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。