打通IO栈:一次编译服务器性能优化实战

背景随着企业SDK在多条产品线的广泛使用 , 随着SDK开发人员的增长 , 每日往SDK提交的补丁量与日俱增 , 自动化提交代码检查的压力已经明显超过了通用服务器的负载 。 于是向公司申请了一台专用服务器 , 用于SDK构建检查 。
$ cat /proc/cpuinfo | grep ^proccessor | wc -l48$ free -htotalusedfreesharedbufferscachedMem:47G45G1.6G20M7.7G25G-/+ buffers/cache:12G35GSwap:0B0B0B$ df文件系统容量已用可用 已用% 挂载点....../dev/sda198G14G81G15% //dev/vda12.9T1.8T986G65% /home这是KVM虚拟的服务器 , 提供了CPU 48线程 , 实际可用47G内存 , 磁盘空间约达到3TB 。
由于独享服务器所有资源 , 设置了十来个worker并行编译 , 从提交补丁到发送编译结果的速度杠杠的 。 但是在补丁提交非常多的时候 , 速度瞬间就慢了下去 , 一次提交触发的编译甚至要1个多小时 。 通过top看到CPU负载并不高 , 难道是IO瓶颈?找IT要到了root权限 , 干起来!
由于认知的局限性 , 如有考虑不周的地方 , 希望一起交流学习
整体认识IO栈如果有完整的IO栈的认识 , 无疑有助于更细腻的优化IO 。 循着IO栈从上往下的顺序 , 我们逐层分析可优化的地方 。
在网上有Linux完整的IO栈结构图 , 但太过完整反而不容易理解 。 按我的认识 , 简化过后的IO栈应该是下图的模样 。
打通IO栈:一次编译服务器性能优化实战文章插图

  1. 用户空间:除了用户自己的APP之外 , 也隐含了所有的库 , 例如常见的C库 。 我们常用的IO函数 , 例如open()/read()/write()是系统调用 , 由内核直接提供功能实现 , 而fopen()/fread()/fwrite()则是C库实现的函数 , 通过封装系统调用实现更高级的功能 。
  2. 虚拟文件系统:屏蔽具体文件系统的差异 , 向用户空间提供统一的入口 。 具体的文件系统通过register_filesystem()向虚拟文件系统注册挂载钩子 , 在用户挂载具体的文件系统时 , 通过回调挂载钩子实现文件系统的初始化 。 虚拟文件系统提供了inode来记录文件的元数据 , dentry记录了目录项 。 对用户空间 , 虚拟文件系统注册了系统调用 , 例如SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode)注册了open()的系统调用 。
  3. 具体的文件系统:文件系统要实现存储空间的管理 , 换句话说 , 其规划了哪些空间存储了哪些文件的数据 , 就像一个个收纳盒 , A文件保存在这个块 , B文件则放在哪个块 。 不同的管理策略以及其提供的不同功能 , 造就了各式各样的文件系统 。 除了类似于vfat、ext4、btrfs等常见的块设备文件系统之外 , 还有sysfs、procfs、pstorefs、tempfs等构建在内存上的文件系统 , 也有yaffs , ubifs等构建在Flash上的文件系统 。
  4. 页缓存:可以简单理解为一片存储着磁盘数据的内存 , 不过其内部是以页为管理单元 , 常见的页大小是4K 。 这片内存的大小不是固定的 , 每有一笔新的数据 , 则申请一个新的内存页 。 由于内存的性能远大于磁盘 , 为了提高IO性能 , 我们就可以把IO数据缓存在内存 , 这样就可以在内存中获取要的数据 , 不需要经过磁盘读写的漫长的等待 。 申请内存来缓存数据简单 , 如何管理所有的页缓存以及如何及时回收缓存页才是精髓 。
  5. 通用块层:通用块层也可以细分为bio层和request层 。 页缓存以页为管理单位 , 而bio则记录了磁盘块与页之间的关系 , 一个磁盘块可以关联到多个不同的内存页中 , 通过submit_bio()提交bio到request层 。 一个request可以理解为多个bio的集合 , 把多个地址连续的bio合并成一个request 。 多个request经过IO调度算法的合并和排序 , 有序地往下层提交IO请求 。
  6. 设备驱动与块设备:不同块设备有不同的使用协议 , 而特定的设备驱动则是实现了特定设备需要的协议以正常驱使设备 。 对块设备而言 , 块设备驱动需要把request解析成一个个设备操作指令 , 在协议的规范下与块设备通信来交换数据 。
形象点来说 , 发起一次IO读请求的过程是怎么样的呢?
用户空间通过虚拟文件系统提供的统一的IO系统调用 , 从用户态切到内核态 。 虚拟文件系统通过调用具体文件系统注册的回调 , 把需求传递到具体的文件系统中 。 紧接着具体的文件系统根据自己的管理逻辑 , 换算到具体的磁盘块地址 , 从页缓存寻找块设备的缓存数据 。 读操作一般是同步的 , 如果在页缓存没有缓存数据 , 则向通用块层发起一次磁盘读 。 通用块层合并和排序所有进程产生的的IO请求 , 经过设备驱动从块设备读取真正的数据 。 最后是逐层返回 。 读取的数据既拷贝到用户空间的buffer中 , 也会在页缓存中保留一份副本 , 以便下次快速访问 。