惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

T
The Exploit Database - CXSecurity.com
J
Java Code Geeks
H
Help Net Security
B
Blog RSS Feed
G
Google Developers Blog
博客园 - 司徒正美
MongoDB | Blog
MongoDB | Blog
量子位
博客园 - 三生石上(FineUI控件)
The Cloudflare Blog
P
Proofpoint News Feed
小众软件
小众软件
人人都是产品经理
人人都是产品经理
云风的 BLOG
云风的 BLOG
V
V2EX
月光博客
月光博客
C
Check Point Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
A
Arctic Wolf
Help Net Security
Help Net Security
Schneier on Security
Schneier on Security
D
DataBreaches.Net
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园_首页
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
P
Palo Alto Networks Blog
T
Tenable Blog
L
LangChain Blog
Attack and Defense Labs
Attack and Defense Labs
Google DeepMind News
Google DeepMind News
N
News and Events Feed by Topic
Forbes - Security
Forbes - Security
F
Fortinet All Blogs
Recent Announcements
Recent Announcements
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
大猫的无限游戏
大猫的无限游戏
www.infosecurity-magazine.com
www.infosecurity-magazine.com
Y
Y Combinator Blog
WordPress大学
WordPress大学
Stack Overflow Blog
Stack Overflow Blog
V
Visual Studio Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Engineering at Meta
Engineering at Meta
NISL@THU
NISL@THU
GbyAI
GbyAI
博客园 - Franky
S
Secure Thoughts
有赞技术团队
有赞技术团队
PCI Perspectives
PCI Perspectives
U
Unit 42

祈雨的笔记

安全多方计算MPC spark原理解析 kueue执行源码分析 spark on k8s执行源码分析 spark-operator源码解析 系统压测遇到的缓存击穿问题 我的世界PC与安卓联机 蚂蚁金服流量投放平台的AIG改造 G1大对象致Old区占用率高 日志打印导致接口响应率下跌分析 Groovy加载类导致OOM分析 ERROR日志打印导致CPU满载 记OceanBase死锁超时 应用发版期间服务响应超时 Ark Serverless初探 系统优化复盘一二三 The user specified as a definer does not exist Kong网关初探 API网关选型调研 CPU火焰图常用工具 配置中心选型调研 root操作Nginx导致用户组错误 基于Proxifier使用代理 FastJSON字段智能匹配踩坑 Nacos初探 记一次Nginx服务器CPU满荷载故障 基于券系统分库分表的思考 limit不参与SQL成本计算致索引失效 Linux常用性能监控命令 golang低版本http2偶现400 hostname in certificate didn't match 常见对称加密原理以及应用 tcp_tw_recycle引起的TCP握手失败 记一次mysql执行DDL导致锁表 mysql磁盘占用查看 mysql对text字段update致磁盘增长 elasticsearch报错index read-only TIME_WAIT与Http的Keep-Alive 记一次TIME_WAIT导致连接数报警 记一次生产事故OOM问题排查 redis分布式锁RedissonLock的实现细节 webservice复杂加密签名(2)java调用 webservice复杂加密签名(1)SoapUI mysql延时关联 利用中间人拦截实现APP内H5窜改 MySQL表字符集不同导致关联查询索引失效 通过SSH隧道远程办公 BeanDefinitionRegistryPostProcessor扩展 mysql空间索引 HTTPS攻击 spring循环依赖过程解析 elasticsearch性能优化 mysql IS NULL 使用索引 mysql字符集utf8mb4失效踩坑 常用加密算法 xml与javaBean转换 初探InnoDB MVCC源码实现 mysql索引原理 redis之list源码分析 redis之key过期源码分析 redis之string源码分析 redis之hash源码分析 线程池之ThreadPoolExecutor mysql数据页结构 Using temporary与Using filesort mysql回表致索引失效 springboot(28)HTTP连接池 定时任务之ScheduledThreadPoolExecutor elasticsearch常用script聚合 elasticsearch实现like查询 elasticsearch实现乐观锁 elasticsearch准实时原理 springboot(27)自定义缓存读写机制CachingConfigurerSupport optimizer tracing arthas常用命令 HTTP和HTTPS详解 redis集群选举机制 kafka消息重试 一点压力测试的经验 kafka架构概念 explain分析sql语句字段的解释 JVM问题分析处理手册 logstash过滤器(2)date logstash过滤器(3)dissect logstash编码器(1)json logstash编码器(2)multiline logstash表达式 logstash输入(1)通用选项 logstash输入(3)file logstash过滤器(1)通用选项 logstash输入(2)stdin logstash安装 记一次前端vConsole导致JSON序列化错误排查 解决多个异步操作嵌套问题 fastjson反序列化失败autoType is not support RTMP串流服务 POI自动调整列宽错误 Nginx+Lua实现动态黑名单 使用curl命令模拟POST和GET请求 ResponseEntity下载文件
数据落盘方案
祈雨的笔记 · 2019-12-31 · via 祈雨的笔记

应用对已存在的文件更新时需要写入数据,当数据写入过程中异常中断例如断电,则会出现数据只写入部分的情况。在这种异常场景下,在应用恢复后对丢失数据的恢复以及避免数据写入未完全导致文件损坏均需要应用保证。

数据储存单元

对于硬盘来说,最小的数据储存单元是扇区,一般为512个字节(现在开始流行4096个字节);而对于操作系统来说,以扇区为读写单位太小效率太慢,所以操作系统以块为数据的读写单位。由此可见,扇区是磁盘的物理结构,而块则是操作系统的抽象逻辑结构,因此块必须是扇区的整数倍。

由下命令可以查看当前环境的扇区大小以及块大小。

1
2
3
4
5
6
7
8
9
10
11
[root@VM_0_10_centos ~]# fdisk -l

Disk /dev/vda: 53.7 GB, 53687091200 bytes, 104857600 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk label type: dos
Disk identifier: 0x000d64b4

Device Boot Start End Blocks Id System
/dev/vda1 * 2048 104857566 52427759+ 83 Linux

其中Sector size则表示硬盘扇区大小为512 bytes

1
2
3
4
5
6
7
8
9
[root@VM_0_10_centos ~]# df -h

Filesystem Size Used Avail Use% Mounted on
/dev/vda1 50G 15G 33G 31% /
devtmpfs 910M 0 910M 0% /dev
tmpfs 920M 24K 920M 1% /dev/shm
tmpfs 920M 656K 919M 1% /run
tmpfs 920M 0 920M 0% /sys/fs/cgroup
tmpfs 184M 0 184M 0% /run/user/0
1
2
3
[root@VM_0_10_centos ~]# tune2fs -l /dev/vda1 | grep 'Block size'

Block size: 4096

其中Block size则表示操作系统一个块的大小为4KB,即一个块由连续的8个扇区构成。

数据落盘异常

此处只讨论用户程序应用层面的数据落盘问题,因此假设操作系统可以保证每个块的写入是完整的,即不会出现当数据落盘时突然异常中断,导致一个块写入数据不完成而出现丢失半截数据的情况(比如4KB的块只写进了前1KB的数据,而后续的3KB数据丢失)。

如图,一个文件由N个块组成,程序修改其中的部分数据,向磁盘写入新数据。当程序写入若干个块的数据后,出现中断异常,剩余部分块的数据未成功写入磁盘,导致文件未完整更新而出现文件损坏的情况。

image

对于该问题的解决,不同场景下有着不同的解决方案。

全量写

全量写即每次写数据时直接重新生成一个新文件,然后通过文件名覆盖原有的旧文件接口。

该方案适用于数据量不大且允许新数据丢失的场景(新文件写入时异常导致新文件生成失败,进而甚至所有新数据丢失),优点是不需要担心新数据写入过程中异常导致旧数据损坏的问题。

比较适合于大多数的数据备份场景,例如redis的RDB快照备份就采用的该方案。如下源码,redis先通过Copy On Write机制将全量RDB文件数据写入到名为temp-%d.rdb的临时文件中,当RDB快照文件全量写入成功后,通过rename的原子操作直接将临时文件重命名覆盖实际的RDB文件。

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

int rdbSave(char *filename, rdbSaveInfo *rsi) {
char tmpfile[256];
char cwd[MAXPATHLEN];
FILE *fp;
rio rdb;
int error = 0;

snprintf(tmpfile,256,"temp-%d.rdb", (int) getpid());
fp = fopen(tmpfile,"w");
if (!fp) {
char *cwdp = getcwd(cwd,MAXPATHLEN);
serverLog(LL_WARNING,
"Failed opening the RDB file %s (in server root dir %s) "
"for saving: %s",
filename,
cwdp ? cwdp : "unknown",
strerror(errno));
return C_ERR;
}

rioInitWithFile(&rdb,fp);

if (server.rdb_save_incremental_fsync)
rioSetAutoSync(&rdb,REDIS_AUTOSYNC_BYTES);

if (rdbSaveRio(&rdb,&error,RDB_SAVE_NONE,rsi) == C_ERR) {
errno = error;
goto werr;
}


if (fflush(fp) == EOF) goto werr;
if (fsync(fileno(fp)) == -1) goto werr;
if (fclose(fp) == EOF) goto werr;



if (rename(tmpfile,filename) == -1) {
char *cwdp = getcwd(cwd,MAXPATHLEN);
serverLog(LL_WARNING,
"Error moving temp DB file %s on the final "
"destination %s (in server root dir %s): %s",
tmpfile,
filename,
cwdp ? cwdp : "unknown",
strerror(errno));
unlink(tmpfile);
return C_ERR;
}

serverLog(LL_NOTICE,"DB saved on disk");
server.dirty = 0;
server.lastsave = time(NULL);
server.lastbgsave_status = C_OK;
return C_OK;

werr:
serverLog(LL_WARNING,"Write error saving DB on disk: %s", strerror(errno));
fclose(fp);
unlink(tmpfile);
return C_ERR;
}

Shadow Paging

影子分页(shadow paging)是一种计算机数据库技术,以实现原子性与持久性。页面在这里指物理存储的单元,可能是在硬盘或内存中,典型为64 KiB。

影子分页是一种写时复制技术,以避免原地修改页面。当一个页面将被修改,一个影子页面被分配。由于影子页面没有被别的地方引用,可以自由修改,不必顾虑一致性。当影子页面变得可以持久,所有引用原页面的地方都被修改为引用影子页面。由于影子页面直到修改完毕才被激活,这保证了原子性。

引用维基百科的解释,Shadow Paging实际上是将操作系统最小的数据单元再抽象成页的概念,一个页由多个连续的块组成,每个页之间通过链表或者索引的方式逻辑连接,而不是物理上的连续。

当某一段数据更新时,则将需要更新的数据所处在的整个页复制到新的页上,所有的数据修改均在新的页上。当数据修改落盘到新的页后,则修改链表指向或者索引即可。

image

该方案的优点是逻辑简单,缺点也是显而易见:IO浪费太大导致写入性能糟糕(一个少量的数据更新实际需要整个页的数据写入),并且也需要额外的逻辑保证在新的页落盘修改索引前更新数据不会丢失(例如下面的事务日志)。另外,由于数据都是通过链表逻辑连接而在物理层不连续,多次更新后会导致文件的页在物理层的离散度高,每次读写造成大量的随机IO,进一步降低了读写性能。

Double Write

该方案实际上是Shadow Paging的一种优化,最大的优点是每个页从逻辑连续变成了物理连续,避免了随机IO而极大地提高了读写性能。Double Write方案在每次更新数据时,则将需要更新的数据所处在的整个页复制到double write buffer区,所有的数据修改均在double write buffer区的页上。当数据修改落盘到double write buffer区后,则再将更新数据直接写到原始数据的页上。当然,该方案同样可以用在没有页概念的文件修改上,只需要把页的概念替换成更新数据在文件中的偏移量和数据长度即可。

如果新的页落盘double write buffer区后而未落盘原始数据的页时,应用中断导致写失败,则在应用恢复时从double write buffer区找到该页的副本,再重新将更新数据写到原始数据的页上,避免了文件未完整更新而出现文件损坏的情况。

image

虽然依然会有类似于Shadow Paging方案的IO浪费的问题,但是每次数据页更新后不会改变该数据页在磁盘上的物理位置,避免了随机IO的问题从而极大提高了读写性能,另外该方案同样需要额外的逻辑保证在新的页落盘double write buffer区前更新数据不会丢失(例如下面的事务日志)。

日志

由于顺序IO性能远远高于随机IO,所以通过顺序写数据变更日志来记录下文件数据的变更操作,当应用中断未落盘的数据丢失后,则通过日志来回放数据的变更操作,从而恢复文件数据的最新状态(日志记录需要优先于数据落盘,即Write Ahead Log)。

对于日志记录的数据量,又分为全量日志和增量日志。全量日志即日志中记录了文件数据的全量信息,只需要日志本身就可以完全恢复全部的文件数据;而增量日志,则一般依靠check point,日志中最少记录了check point触发后的文件数据变更日志,即如果需要恢复文件数据,还必须依赖check point触发后的文件数据本身才行。

全量日志

image

redis的aof就是典型的全量日志,由于全量日志记录了文件数据的所有变更,导致日志文件本身会非常庞大,当应用中断后数据丢失,应用恢复阶段读取日志来重放数据操作的过程会非常耗时。事实上,全量日志不需要记录全量的操作日志,只需要记录每一条数据最后一次的完整日志,但这也意味着应用需要额外的逻辑定时对全量日志进行“瘦身”处理。

image

优点是日志中记录了全量的数据变更操作,即使原文件数据损坏或者丢失,也可以通过日志来回放出完整的最新文件数据。缺点也是显而易见,日志体积会非常庞大,应用中断后的日志回放恢复耗时也会很久。

增量日志

image

由于全量日志的文件体积太大,回放耗时长的问题,于是设计出了check point技术,逻辑十分简单:为每一次数据操作添加版本号,数据变更均在内存中执行(数据落盘变成异步操作),并定时将变更后的内存数据批量落盘写到文件,然后已经落盘的旧版本号操作日志从日志文件中删除。因此当应用中断后恢复数据时,需要回放的日志仅仅是高于文件数据版本号的操作日志,不仅大大缩短了回放恢复耗时,也减少了日志的体积。并且,由于数据落盘变成异步操作,可以将一个块的多次IO操作合并成一次IO操作,也极大地提高了应用的性能。

但同样有新的问题出现,该方案必须同时依靠日志和完整的文件数据,如果文件数据本身损坏甚至丢失,则会导致日志回放错误。因此还需要额外的逻辑保证原文件数据更新时避免文件未完整更新而出现文件损坏的情况,例如上述的Shadow PagingDouble Write方案,以及集群环境下,当主节点原文件损坏后拉取从节点未损坏的文件数据也是解决方案之一。