Filesystem Cache
Elasticsearch高度依赖操作系统的filesystem cache来提升其数据读取效率,如果我们将大量的系统内存分配给JVM进程(例如将堆内存设置的很大),那么将导致操作系统无法得到足够的内存来作为filesystem cache,这会严重的影响Elasticsearch的性能。
我们知道现代操作系统中的内存都是按页分配的,操作系统在把某些数据从外存读取到内存中的页面上时,会把紧邻在被读取数据后面的几个页面的数据也读到内存中,这叫做文件预读,文件预读会极大的提升硬盘的顺序读取速度。除此之外,数据在被读取到内存中之后,即使用户已经使用完毕,操作系统也不会立即把这些数据所占用的内存给释放掉,这些数据会暂时的保存在内存中,如果此时用户开始读取硬盘上的同一块数据,操作系统会立即从内存中把该数据返回给用户而不需要再去操作硬盘进行数据读取操作,这部分暂时留存在内存中的数据就叫做页缓存(page cache)。
页面置换算法
cache并不会影响进程的正常运行,一旦操作系统发现物理内存不够用,会立即从cache中分配内存页给进程使用。内存页的更新会使用LRU算法,LRU即Least Recent Used,该算法会淘汰最不经常使用的缓存页,其核心原则为“如果一个缓存页在最近一段时间没有被访问到,那么在将来它被访问的可能性也很小”。
首先我们构建一个页面地址到实际页面的映射关系,我们可以根据这个地址对页面进行读取,这个映射关系我们使用哈希表进行存储。
内存地址 |
内存页 |
0x010000 |
page1 |
0x014096 |
page2 |
0x018192 |
page3 |
0x022288 |
page4 |
0x026384 |
page5 |
… |
… |
内存页本身我们使用一个双向链表进行存储,其核心原理如下
- 一旦一个页面被读取,则把这个页面移动到链表的头部
- 如果内存空间不足,则删除链表尾部的页面以换取充足的空间
下面是一个go语言实现的LRU算法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108
| package main
import ( "fmt" "math/rand" "strconv" "time" )
type Page struct { prev *Page next *Page addr int value []byte }
var ( table = make(map[int]*Page) head = Page{addr: -1} tail = Page{addr: -1} capacity = 10 )
func get(addr int) []byte { if addr < 0 { panic("addr can't be negative number") }
if _, ok := table[addr]; !ok { table[addr] = getFromDisk(addr) } page, _ := table[addr]
if page.addr != head.next.addr { if page.prev != nil && page.next != nil { page.prev.next = page.next page.next.prev = page.prev }
page.next = head.next page.next.prev = page
page.prev = &head head.next = page }
if getSize() > capacity { delete(table, tail.prev.addr)
tail.prev.prev.next = &tail tail.prev = tail.prev.prev }
return page.value }
func getFromDisk(addr int) *Page { return &Page{addr: addr} }
func getSize() int { size := 0 page := head.next for page.addr >= 0 { size += 1 page = page.next } return size }
func pretty() { page := head.next value := strconv.Itoa(page.addr) for { page = page.next if page.addr < 0 { break } value += "\t⇄\t" + strconv.Itoa(page.addr) } fmt.Println(value) }
func randomNum(min int, max int) int { return rand.Intn(max-min) + min }
func main() { head.next = &tail tail.prev = &head
capacity = 8
rand.Seed(time.Now().UTC().UnixNano()) for i := 0; i < 15; i++ { get(randomNum(0, 15)) pretty() } }
|
回到Elasticsearch的问题,假设我们有一个16g内存的机器,如果我们分配了14g内存给JVM进程,那么操作系统就无法充分使用内存作为filesystem cache来为磁盘的读取加速,具体表现就是Elasticsearch的查询变得缓慢。
关闭缓存
在Linux中页缓存是默认开启的,在Linux2.6之后你也可以使用 O_DIRECT
选项来关闭文件读取时的页缓存,这在某些特殊的情况下是有用的。
参考
Help! Linux ate my RAM!
聊聊Linux IO
堆内存:大小和交换 | Elasticsearch: 权威指南 | Elastic
ES内存那点事
LRU和LFU缓存置换算法
本文链接:
https://www.nosuchfield.com/2019/03/25/Memory-limit-of-the-ES-data-node-to-the-JVM-process/