原创文章,转载请注明: 转载自系统技术非业余研究
本文链接地址: posix_fadvise清除缓存的误解和改进措施
在典型的IO密集型的数据库服务器如MYSQL中,会涉及到大量的文件读写,通常这些文件都是通过buffer io来使用的,以便充分利用到Linux操作系统的page cache。
Buffer IO的特点是读的时候,先检查页缓存里面是否有需要的数据,如果没有就从设备读取,返回给用户的同时,加到缓存一份;写的时候,直接写到缓存去,再由后台的进程定期涮到磁盘去。这样的机制看起来非常的好,在实践中也效果很好。
但是如果你的IO非常密集,就会出现问题。首先由于pagesize是4K,内存的利用效率比较低。其次缓存的淘汰算法很简单,由操作系统自主进行,用户不大好参与。当你的写很多,超过系统内存的某个上限的时候,后台的进程(swapd)要出来回收页面,而且一旦回收的速度小于写入的速度,就会出现不可预期的行为。
这里面最大的问题是:当你使用的内存包括缓存,没超过操作系统规定的上限的时候,操作系统选择不作为,让用户充分使用缓存,从它的角度来看这样效率最高。但是正是由于这种策略在实践中会导致问题。
比如说MYSQL服务器,我们可以把数据直接走direct IO,但是它的日志是走bufferio的。因为走directio需要对写入文件的偏移和大小都要扇区对全,这对日志系统来讲太麻烦了。由于MYSQL是基于事务的,会涉及到大量的日志动作,频繁的写入,然后fsync. 日志一旦写入磁盘,buffer page就没用了,但是一直会在内存呆着,直到达到内存上限,引起操作系统突然大量回收
页面,出现IO柱塞或者内存交换等负面问题。
那么我们知道了困境在哪里,我们可以主动避免这个现象的发生。有二种方法:
1. 日志也走direct io,需要规模的修改MYSQL代码,如percona就这么做了,提供相应的patch。
2. 日志还是走buffer io, 但是定期清除无用page cache.
第一张方法不是我们要讨论的,我们重点讨论第二种如何做:
我们在程序里知道文件的句柄,是不是就可以很轻松的用:
int posix_fadvise(int fd, off_t offset, off_t len, int advice);
POSIX_FADV_DONTNEED
The specified data will not be accessed in the near future.
来解决问题呢?
比如写类似 posix_fadvise(fd, 0, len_of_file, POSIX_FADV_DONTNEED);这样的代码来清掉文件所属的缓存。
前面介绍的vmtouch就有这样的功能,清某个文件的缓存。
vmtouch -ve logfile 就可以试验,但是你会发现内存根本就没下来,原因呢?
我们从代码来看posix_fadvise如何运作的:
参看 mm/fadvise.c:
SYSCALL_DEFINE(fadvise64_64)( int fd, loff_t offset, loff_t len, int advice) |
case POSIX_FADV_DONTNEED: |
if (!bdi_write_congested(mapping->backing_dev_info)) |
start_index = (offset+(PAGE_CACHE_SIZE-1)) >> PAGE_CACHE_SHIFT; |
end_index = (endbyte >> PAGE_CACHE_SHIFT); |
if (end_index >= start_index) |
invalidate_mapping_pages(mapping, start_index, |
我们可以看到如果后备设备不忙的话,会先调用filemap_flush(mapping)把脏页面刷掉,然后再调invalidate_mapping_pages清除页面。先看下如何刷页面的:
mm/filemap.c
int filemap_flush( struct address_space *mapping) |
return __filemap_fdatawrite(mapping, WB_SYNC_NONE); |
int __filemap_fdatawrite_range( struct address_space *mapping, loff_t start, |
loff_t end, int sync_mode) |
struct writeback_control wbc = { |
if (!mapping_cap_writeback_dirty(mapping)) |
ret = do_writepages(mapping, &wbc); |
int filemap_fdatawrite_range( struct address_space *mapping, loff_t start, |
return __filemap_fdatawrite_range(mapping, start, end, WB_SYNC_ALL); |
我们看到它刷页面用的参数是是 WB_SYNC_NONE, 也就是说不是同步等待页面刷新完成。
而fsync和fdatasync是最终会调用filemap_fdatawrite_range, 用WB_SYNC_ALL参数等到完成才返回的。
我们来看下代码mm/page-writeback.c确认下:
int generic_writepages( struct address_space *mapping, |
struct writeback_control *wbc) |
return write_cache_pages(mapping, wbc, __writepage, mapping); |
int do_writepages( struct address_space *mapping, struct writeback_control *wbc) |
if (mapping->a_ops->writepages) |
ret = mapping->a_ops->writepages(mapping, wbc); |
ret = generic_writepages(mapping, wbc); |
int generic_writepages( struct address_space *mapping, |
struct writeback_control *wbc) |
if (!mapping->a_ops->writepage) |
return write_cache_pages(mapping, wbc, __writepage, mapping); |
int write_cache_pages( struct address_space *mapping, |
struct writeback_control *wbc, writepage_t writepage, |
if (--wbc->nr_to_write <= 0 && |
wbc->sync_mode == WB_SYNC_NONE) { |
从代码和注释可以看出,在WB_SYNC_NONE模式下,提交完写脏页,然后就返回了,确实不等到回写完成。
到这里为止如何刷脏页就很清楚了,再接着看第二步清除内存的操作:
看下mm/truncate.c的实现:
unsigned long invalidate_mapping_pages( struct address_space *mapping, |
pgoff_t start, pgoff_t end) |
pagevec_lookup(&pvec, mapping, next, PAGEVEC_SIZE)) { |
mem_cgroup_uncharge_start(); |
for (i = 0; i < pagevec_count(&pvec); i++) { |
struct page *page = pvec.pages[i]; |
ret += invalidate_inode_page(page); |
mem_cgroup_uncharge_end(); |
int invalidate_inode_page( struct page *page) |
struct address_space *mapping = page_mapping(page); |
if (PageDirty(page) || PageWriteback(page)) |
return invalidate_complete_page(mapping, page); |
从上面的注释我们可以看到清除相关的页面要满足二个条件: 1. 不脏。 2. 未被使用。
如果满足了这二个条件就调用invalidate_complete_page继续:
invalidate_complete_page( struct address_space *mapping, struct page *page) |
if (page->mapping != mapping) |
if (page_has_private(page) && !try_to_release_page(page, 0)) |
ret = remove_mapping(mapping, page); |
我们看到invalidate_complete_page在满足更多条件的话会继续调用remove_mapping:
int remove_mapping( struct address_space *mapping, struct page *page) |
if (__remove_mapping(mapping, page)) { |
page_unfreeze_refs(page, 1); |
static int __remove_mapping( struct address_space *mapping, struct page *page); |
BUG_ON(!PageLocked(page)); |
BUG_ON(mapping != page_mapping(page)); |
spin_lock_irq(&mapping->tree_lock); |