南京大学 操作系统 (JYY) 学习笔记:从文件系统到数据库,持久化的终极抽象 (SQL/NoSQL) 写在前面这是本系列的第二十五篇。操作系统为我们的应用程序提供了多种持久化控制的机制包括fsync等保证崩溃一致性的 API。但如果所有的业务逻辑都要靠手工去调这些底层的 API那开发者的心智负担就太大了。在此基础上我们能否在应用程序之上实现一套更为可靠、更为高可用的数据存储抽象本讲内容作为《操作系统》课程的彩蛋延伸我们将跳出内核的泥潭探讨数据库系统SQL 与 NoSQL的设计原理与实现技术。文件系统 API 到此结束作为操作系统留给上层应用的基础原语文件系统 API 的核心可以概括为四个方面文件 (数据 元数据) 管理:open,read,write,close,stat,chmod,chown,fsetxattr/fgetxattr,ftruncate…目录树管理:mount,mkdir,readdir,link,symlink,unlink,rename,chdir…科普rename_ 系统调用是极其强大的它在很多文件系统底层被保证为绝对的原子操作。_一致性管理:syncsyncfsfsyncfdatasync。其他相关 API:flock,mmap…关系代数、SQL 和数据库 (Spicy ️)什么是“应用程序”软件是物理世界过程在信息世界中的投影。软件天生有着 Persist Data (持久化数据) 的需求个人信息学籍、订单、交易流水、维护日志……试想一下如果我们不借助现成的数据库要如何“从零开始”手搓一个南京大学的教务系统尽管我们平时总是吐槽学校的教务系统多么难用但在潜意识里我们依然敢把成绩和学分数据放心地交给它这就是持久化带来的信任。纯手工保存应用数据的痛点方案 1在文件虚拟磁盘上直接构建数据结构就像定义 ELF、BMP 文件格式一样自己定义数据的字节排布structsuperblock{structstudent*s;structcourse*c;}structstudent{charstuid[16];...};structcourse{charcid[16];...}任何需求如expel(s)退学enroll(s, c)选课都需要应用系统自己去翻译成繁琐的read,write,lseek或mmap。(注某些极端的应用确实是这么干的比如单机游戏存档或者直接记录 UI 事件的 Replay 快照)。方案 2利用目录树用文件夹代表实体用文件代表属性。ehall.nju.edu.cn/teacherJxrwApp/22020230/4 ├── enrollment │ ├──231220001.md │ ├──231220002.md │ └──...这样 UNIX 世界里的find,grep等工具都能直接用。但问题接踵而至如何关联学生和成绩用Symlink软链接吗如何在万人同时抢课时保证并发访问的安全和Crash Consistency我们需要的到底是什么Concurrency (并发):全校一起抢课系统不能崩。且不能无脑用一把全局大锁否则几万人的请求排队会直接让系统超时。Persistence (持久化):系统故障甚至是机房断电落盘的数据绝对不能丢。Atomicity (原子性):withAllOrNothing():// 要么全成功要么全失败回滚forid in enroll_list:Path(fenrollment/{id}.md).write_text(y)降维打击Relational Database (关系型数据库)有没有一个比目录/文件更好用的 API 呢有这就是 1981 年图灵奖得主 Edgar F. Codd 提出的关系模型。“Future users of large data banks must be protected from having to know how the data is organized in the machine.”必须保护未来大型数据库的用户使他们不必去了解数据在机器中是如何组织和存储的。数据抽象的极致Everything is a table (万物皆表)。每行代表一个对象对象可以用ID(指针) 关联其他对象。这就消解了 C 语言里繁琐的指针链表。SQL 查询指针的优雅“配对”我们不需要写循环去遍历链表了只需写一条声明式的语句SELECTCourses.TitleFROMStudentJOINTakes_CourseONStudent.IDTakes_Course.IDJOINCoursesONTakes_Course.ClassIDCourses.ClassIDWHEREStudent.NameLIKE张%数据库系统开启了软件的新时代ACID 数据库的承诺A (Atomicity, 原子性)C (Consistency, 一致性)I (Isolation, 隔离性):Strong serializability (强可串行化)无论怎么并发最终结果都像是一个接一个按顺序完成的。D (Durability, 持久性):Strong crash consistency系统断电 Crash 也绝不会损坏或丢失数据。学过了《操作系统》底层机制的你应该很好理解这背后的技术含金量。数据库不仅帮你做了“一把大锁保平安”的效果还在底层疯狂优化实现了大规模并行的恐怖性能并且附赠了基于Redo Log的完全自动崩溃恢复。把应用数据交给数据库对于程序员来说就是一劳永逸。只要不是大到“国民级”的逆天并发应用关系数据库分库、分表、B树索引、读写分离全都能帮你搞定。关系数据库底层的深渊实现 (Spicy ️)Database v.s. Compilers (数据库本质是编译器)实现 SQL 查询本质上就是在做编译优化语义等价的 Rewriting。数据库引擎要把你的 SQL 语句翻译成底层的物理执行计划。数据库是一个超级复杂的并发程序SQL 查询底层对应的无非就是对内存和磁盘的Write(x)和Read(y)。它必须自己维护一套“磁盘上的数据结构”。它大量使用类似文件系统的 Write-ahead Logging (WAL) 技术来保证 Crash Consistency。但它的并发控制比操作系统更困难。因为长事务Transactions是不可能用简单的lock/unlock来实现的这会瞬间锁死整个库。伟大的例子SQLite“SQLite is the most used database engine in the world. SQLite is built into all mobile phones and most computers…”Android, iOS, macOS, Chrome 等几乎所有平台中都深深内嵌了 SQLite。Zero Configuration无需配置守护进程发行版自带libsqlite3.so。它是年轻程序员开发第一个应用的首选。不用关系的数据库NoSQL (Spicy ️)如果要实现“国民级”的应用当你的数据达到了亿级、甚至十亿级别传统的 SQL 就显得吃力了。SELECT*FROMchat_recordsWHERE(sender_iduser_id1ANDreceiver_iduser_id2)OR(sender_iduser_id2ANDreceiver_iduser_id1)ORDERBYtimestampDESCLIMIT10对于国民级社交软件这样一条“合法且正确”的 SQL如果碰上没有建立完美索引的亿级表会瞬间让数据库引擎 CPU 拉满甚至瘫痪整个系统。因为系统提供商永远无法预知应用程序员会写出怎样逆天的 Query 语句。只要提供了功能就必然会被滥用 $ \rightarrow $ 导致 Performance Bug。这是任何底层 Systems 都无法避免的技术债。解决方法做减法(NoSQL 诞生)牺牲掉极其复杂的“多表联查 (JOIN)”和“通用性”换来极简的能力、极其残暴的读写性能和极其容易的横向扩展性。Key/Value (键值对):最简单、最高效。适合缓存、计数器。Document (文档型 JSON):NoSQL 实现“国民级”应用的主力。Column (列族):适合海量数据的分析。Graph (图):适合社交网络关系。AI 时代的 Vector (向量):大语言模型检索的基石。为什么 Key-Value 这么好扩展 (Scale)只要给 Key 做个 Hash就可以把数据完美地散列扔到上千台不同的机器节点上。写操作只需 Append-only 追加写入日志。分布式的高可靠瞬间完成。例如国民级 APP 的点赞功能应用程序全靠 Key 找数据user:{uid}历史记录user:{uid}:like_history(一个简单的 List)List 直接支持极速的append/pop/range速度极快。内存数据库的王者RedisEveything is In-memory.基于简单的 GET / SET。极度丰富的数据结构抽象String, JSON, List, Set, Hash, Sorted Set, Stream。甚至支持复杂的搜索FT.SEARCH products price:[200 300]。支持简单的事务MULTI/WATCH/EXEC也支持异步持久化落盘。持久化 NoSQL 的妥协很多文档型和列族数据库为了降低学习门槛会提供一个 SQL 的“子集”。通过极简的语句接口强行限制程序员的滥用从而在底层保证分布式横向扩展的能力。MongoDB (Document):Key $ \rightarrow $ JSON。比如直接创建一个 JSON 并 append 到user:{uid}.messages。Cassandra (Column):语言叫CQL看起来很像 SQL但功能极其受限。INSERTINTOmessages(user_id,message_id,content,timestamp)VALUES(1234567,now(),message,toTimestamp(now()));总结Take-away messages:在操作系统底层为我们提供的文件、目录、网络等“粗糙”的 API 之上开发者们发挥了惊人的创造力。我们看到了为了降低心智负担而兴起的关系数据库看到了为了应对亿级高并发而做减法繁荣起来的 NoSQL以及今天浪潮正盛的 AI 向量数据库。在这几波狂暴的技术浪潮之间虽然操作系统的内部实现发生过天翻地覆的重构但操作系统的 API 却相当惊人地保持了稳定POSIX。正是这种数十年的“稳定性”支撑起了上层应用生态无所顾忌的繁荣。这也是操作系统作为一切软件“基石平台”最伟大的使命。