Skip to content

Commit 1d5670b

Browse files
authored
Merge pull request cch123#37 from wziww/master
io-directio
2 parents 1aa2a47 + 7c82651 commit 1d5670b

File tree

1 file changed

+196
-0
lines changed

1 file changed

+196
-0
lines changed

io.md

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
# directio
2+
## page cache
3+
页面缓存(Page Cache)是 Linux 内核中针对文件I/O的一项优化, 众所周知磁盘 I/O 的成本远比内存访问来得高, 如果每次进行文件读写都需要直接进行磁盘操作, 那成本会是非常高的, 因此, kernel 针对于文件 I/O 设计了 page cache, 简单来说就是将目标读写的文件页缓存在内存中, 而后操作这块缓存进行读写 (而且例如针对机械磁盘来说, 为了降低磁头寻道的耗时, page cache 通常会采用预读的机制), 写入新数据后该页变为脏页, 等待刷盘, 刷脏的操作可由用户主动请求 (fsync) 或者由内核在合适的时机进行操作
4+
5+
### 总结下 page cache 的好处:
6+
- 缓存最近被访问的数据, 提高文件 I/O 的效率
7+
- 预读功能减少磁头寻道损耗
8+
9+
## 那么 page cache 的设计一定是能提高服务效率的么?
10+
11+
来考虑下一个场景:
12+
> 某服务正在正常工作, 存放了许许多多的静态资源等待访问, 大小为小到几十 kb, 大到几百 GB (大到无法全都加载到内存, 只能存放在本地磁盘)的大文件不等。小文件为热点文件, 大文件为冷门资源, 某天, 突然有用户进行了大文件的访问。
13+
14+
这时候会发生什么?
15+
16+
正常工作的情况下假设内存足够缓存所有小文件, 服务无需进行磁盘 I/O, 而这时候突然来了个大文件的缓存, 直接跑满了大部分的 page cache, 造成内核不得不通过淘汰策略将 page cache 置换了出来(先暂不考虑各种内存拷贝的损耗), 那么接下去再去访问那一堆热点小文件就不得不去进行磁盘 I/O, 然后写入 page cache, 两者之间的矛盾无法调和不断重复, 尤其是大量的小文件基本都为随机读写。从而服务的压力增加, 效率降低。
17+
18+
### 如何优化这种场景?
19+
> 各架构机器支持程度不一, 此处仅讨论 linux x86
20+
21+
```shell
22+
dd if=/dev/zero of=test bs=1M count=1000 # 生成 1000MB 文件以供测试
23+
```
24+
先来测试下正常情况下文件读写的情况:
25+
```go
26+
package main
27+
28+
import (
29+
"log"
30+
"os"
31+
"syscall"
32+
"time"
33+
"unsafe"
34+
)
35+
36+
const (
37+
// Size to align the buffer to
38+
AlignSize = 4096
39+
40+
// Minimum block size
41+
BlockSize = 4096
42+
)
43+
44+
func alignment(block []byte, AlignSize int) int {
45+
return int(uintptr(unsafe.Pointer(&block[0])) & uintptr(AlignSize-1))
46+
}
47+
48+
func AlignedBlock(BlockSize int) []byte {
49+
block := make([]byte, BlockSize+AlignSize)
50+
if AlignSize == 0 {
51+
return block
52+
}
53+
a := alignment(block, AlignSize)
54+
offset := 0
55+
if a != 0 {
56+
offset = AlignSize - a
57+
}
58+
block = block[offset : offset+BlockSize]
59+
// Can't check alignment of a zero sized block
60+
if BlockSize != 0 {
61+
a = alignment(block, AlignSize)
62+
if a != 0 {
63+
log.Fatal("Failed to align block")
64+
}
65+
}
66+
return block
67+
}
68+
func main() {
69+
fd, err := os.OpenFile("/disk/data/tmp/test", os.O_RDWR|syscall.O_DIRECT, 0666)
70+
block := AlignedBlock(BlockSize)
71+
if err != nil {
72+
panic(err)
73+
}
74+
defer fd.Close()
75+
for i := range block {
76+
block[i] = 1
77+
}
78+
for {
79+
n, err := fd.Write(block)
80+
_ = n
81+
if err != nil {
82+
panic(err)
83+
}
84+
fd.Seek(0, 0)
85+
time.Sleep(time.Nanosecond * 10)
86+
}
87+
}
88+
```
89+
```shell
90+
iotop
91+
Total DISK READ : 0.00 B/s
92+
Actual DISK READ: 0.00 B/s
93+
```
94+
可以观察到除了初次 I/O 的时候产生磁盘读写, 待测试代码稳定下后是没有产生磁盘读写的, 再看看文件 page cache 的情况
95+
```shell
96+
vmtouch /disk/data/tmp/test
97+
Files: 1
98+
Directories: 0
99+
Resident Pages: 4/256000 16K/1000M 0.00156%
100+
Elapsed: 0.007374 seconds
101+
```
102+
与预期中的一样缓存了前 16K 文件页
103+
```shell
104+
# 强刷 page cache
105+
echo 3 > /proc/sys/vm/drop_caches
106+
```
107+
再来测试下绕过 page cache 进行文件读写的方法
108+
```go
109+
package main
110+
111+
import (
112+
"log"
113+
"os"
114+
"syscall"
115+
"time"
116+
"unsafe"
117+
)
118+
119+
const (
120+
// Size to align the buffer to
121+
AlignSize = 4096
122+
123+
// Minimum block size
124+
BlockSize = 4096
125+
)
126+
127+
func alignment(block []byte, AlignSize int) int {
128+
return int(uintptr(unsafe.Pointer(&block[0])) & uintptr(AlignSize-1))
129+
}
130+
131+
func AlignedBlock(BlockSize int) []byte {
132+
block := make([]byte, BlockSize+AlignSize)
133+
if AlignSize == 0 {
134+
return block
135+
}
136+
a := alignment(block, AlignSize)
137+
offset := 0
138+
if a != 0 {
139+
offset = AlignSize - a
140+
}
141+
block = block[offset : offset+BlockSize]
142+
// Can't check alignment of a zero sized block
143+
if BlockSize != 0 {
144+
a = alignment(block, AlignSize)
145+
if a != 0 {
146+
log.Fatal("Failed to align block")
147+
}
148+
}
149+
return block
150+
}
151+
func main() {
152+
// syscall.O_DIRECT fsfd 直接 I/O 选项
153+
fd, err := os.OpenFile("/disk/data/tmp/test", os.O_RDWR|syscall.O_DIRECT, 0666)
154+
block := AlignedBlock(BlockSize)
155+
if err != nil {
156+
panic(err)
157+
}
158+
defer fd.Close()
159+
for i := range block {
160+
block[i] = 1
161+
}
162+
for {
163+
n, err := fd.Read(block)
164+
_ = n
165+
if err != nil {
166+
panic(err)
167+
}
168+
fd.Seek(0, 0)
169+
time.Sleep(time.Nanosecond * 10)
170+
}
171+
}
172+
```
173+
> 注: 要使用 O_DIRECT 的方式进行文件 I/O 的话, 文件每次操作的大小得进行文件 lock size 以及 memory address 的对齐
174+
175+
```shell
176+
iotop
177+
Total DISK READ : 17.45 M/s
178+
Actual DISK READ: 17.42 M/s
179+
```
180+
再来看看 page cache 的情况:
181+
```shell
182+
vmtouch /disk/data/tmp/test
183+
Files: 1
184+
Directories: 0
185+
Resident Pages: 0/256000 0/1000M 0%
186+
Elapsed: 0.006608 seconds
187+
```
188+
189+
## 回到 golang
190+
当前标准库如 http.ServeFile, os.Open 等默认采用的访问静态资源的方式均为非直接 I/O, 因此如果有特定场景需要用户自己进行这方面的考量及优化
191+
## 参考资料
192+
- http://nginx.org/en/docs/http/ngx_http_core_module.html#directio
193+
- https://tech.meituan.com/2017/05/19/about-desk-io.html
194+
- https://github.com/ncw/directio
195+
# DMA
196+
# Zero-Copy

0 commit comments

Comments
 (0)