随意的世界 - 那随意了
type: channel
link: https://www.nasuiyile.cn/author/1/
description: 那随意了
items: CMU-数据库导论-3
items: CMU-数据库导论-2
items: CMU数据库导论-1
items: linux网络IO
items: agent学习笔记
items: 时间API
items: 那些底层使用了Rocksdb的项目
items: LLM的有趣技术
items: Epoch-Based Reclamation算法
items: MQTT学习笔记

CMU-数据库导论-3
type: item
date: 2026-07-23T17:23:00+08:00
description: CMU-数据库导论-3数据库日志ACID的最后一块拼图是持久性。持久性代表回复外界时，数据就要保证不能丢失。恢复算法分为俩个部分，为了应对可能发生的故障，在事务运行期间，我们需要采取额外的措施。准备失败。另一个是当我们从故障恢复后，要弄清楚崩溃发生时的系统状态。处理失败。为了实现这一目标，我们需要俩个关键操作，undo和redo。undo是一出未完成的事务的更改或影响。因为系统自身终止系统崩溃，或者由于俩阶段锁定或者OCC机制限制无法提交。该事务被认为未提交，redo是对于已提交的事务确保在系统恢复后，保留其所有更改。我们需要确保每个已提交的事务的更改都已写入。我们如何进行undo和redo操作，将取决于缓冲池管理器能为我们提供什么支持。我们实际上需要对它进行一些更改，使其能够更智能地处理系统中的各个情况。我们可以进行俩种策略不窃取（No-Steal）：只有事务提交后，其脏页才能落盘。这要求缓冲区为每个活跃事务保留足够的空间，如果一个长事务修改了大量页面，可能会撑爆缓冲区。窃取（Steal）：即使事务未提交，其脏页也可以被刷写回磁盘。这解决了缓冲区空间被未提交事务占满的问题，允许缓冲池更灵活地换入换出页面。预写日志read-ahread logging将使用窃取策略。因为允许从尚未提交的事务中驱逐页面，缓冲池 管理器的替换策略（LRU-K）就能有更多可释放帧的选择。如果不适用窃取策略，那么一个事务修改的所有内容都需要保留在内存中。这意味着如果事务修改的页面超过了你可用的内存，那么就糟了，因为你无法逐出任何内容。另外一种策略：force（强制）：当一个事务已经提交时，在告知外界事务已经提交前，需要将所有的脏写更改写回并刷新到磁盘。no-force（非强制）：事务提交不需要将脏数据刷新到磁盘。即：Steal / No-Steal 解决的是：事务未提交时，它的脏数据能否被挤出内存写到磁盘？Force / No-Force 解决的是：事务提交时，它的脏数据是否必须强制写到磁盘？force会让提交速度便慢。影子分页假设 这里采用no-steal、force策略。如果我们有ABC三个数据在同一个页面上。比如我们T1读取A页面被加载到了缓存中，T2读取并修改了B。然后提交了事务。当事务提交，根据force策略，就必须将脏数据刷回磁盘。这里就产生了冲突问题，因为根据no-steal策略，我们不允许写出来自尚未提交的任何脏页或任何未提交的修改。因此解决这个问题的方法是，在缓冲区中复制这个页面，但是只包含自己已提交事务的更改。然后就可以把数据回写。这样一来当回到T1时，T1的回滚操作就会变得非常简单。因为实际上没有修改任何东西，任何修改的东西都没有被写入磁盘。很明显上述方案非常糟糕。如果更新10亿个页面呢？那么需要复制10亿个页面才能删除不需要的内容。高并发系统中有成千上万个事务同时运行，我需要跟踪所有事务修改过的页面。还需要制作这些页面的副本。因此没有任何系统用这种做法，这种做法实际上也会加速硬件老化，SSD的擦写次数是有限的。这就是shadow paging 影子分页。是一种no-steal，force策略 。它避免了每次都复制整个数据库。这是IBM在System R项目初期发明的。在影子分页中，不允许多个事务同时更新一个页面。因此，将数据刷新到磁盘的时候，就不需要剔除未提交事务的部分了。其中主版本用于存放已经提交的事务更改，影子副本用于存放未提交事务的更改。当事务提交时，我们会将所有脏页从影子表刷新到磁盘。然后用一个指针来指向当前的主页表。这基本上就是写时复制的工作原理。它在处理并发时表现不佳。影子分页还会导致数据不再连续存储。undo：移除影子页。保留主页面和数据库根指针不动。redo：不需要任何操作Sqlite（2010之后）sqlite有一种叫做回滚的机制。sqlite第一个版本的实现类似影子分页。（2010年后处于性能考虑，添加了WAL）sqlite最开始的做法是，页面在内存中，数据库在磁盘上，此外还有一个额外的文件叫做日志文件（journal file）。当事务想要修改一个页面时，你会从磁盘读取它，加载到缓存中，然后在你做任何更改之前，将更改复制到日志文件中。现在就可以在内存中进行修改了。此时更改page2，然后是page3。但是更改page2的时候崩溃了，page的一部分写入了磁盘，在写入page3之前崩溃了，此时你就会知道这个事务没有提交。在这里内存中的缓存区会被清除。在恢复时，首先查找日志文件。然后将这些页面复制回内存，因为你知道这些页面时事务开始之前的原始版本。然后直接将他们复制回磁盘，因为你知道任何事务运行并更改之前，这些页面是数据库的正确版本。sqlite使用但写线程，因此无需担心多个事务并发修改页面的问题。当事务提交…

CMU-数据库导论-2
type: item
date: 2026-07-19T14:10:00+08:00
description: CMU-数据库导论-2Query Execution处理模型当关系型数据库接收到SQL查询时，会将其转化为执行计划。理想情况下，你希望查询计划是一个有向无环图，directed acyclic graph，也就是DAG。大多数系统是将其实现为树。理论上DAG是更优的结构，因为树不能重用一些输出，除非使用特殊的技巧。管道是操作符的合计，数据库在这些操作符之间连续传递，无需阻塞或等待更多数据。管道中断器（pipeline breaker）本质上是管道的边界，除非在查询中执行另一个操作（另一个管道），否则无法继续处理当前管道中的任何元组。管道的核心思想是，定义了一个边界，在这个边界内，我们可以执行一系列操作符，而无需将结果物化到中间存储。处理模型定义了数据库将如何执行查询计划，以及如何将数据从一个操作符移动到另一个操作符。这里有些操作方式适合OLAP有些适合OLTP。控制流（control flow）是只是数据库系统用来下一步要运行哪个操作符的机制。它可以是一个管道或者一系列运算符。数据流则决定了操作符发送的位置和方式。在迭代器模型中控制流和数据流的界限实际上会变的模糊。在迭代器模型中，表示数据已足够，停止运行的方式，和指示其完全停止的方式是相同的。一个操作符的输出会取决于我们想要使用的处理模型。常见的三种方法是iterator model：最常见，pg，mysql，oracle，db2都是用这个。materialization model：少见vectorized/batch model：常见，OLAP系统中用的多。迭代模型这是最常见的实现方式，尤其在行存储系统中。查询计划中的每个算子都要实现系统提供的API，这个API包含三个基本的函数。其中next函数会发出指令，算子会生成下一个元组，这个元组成为正在处理数据的输出结果。父算子会不断在其子算子上重复调用next函数，直到子算子没有更多元组能够返回。它会返回空指针和一个文件结束标记。因此，如果一个算子有一个子算子，其并不关心子算子实际上是如何生成数据的。只需要调用子算子的next函数，不需要关心是来自网络，还是来自索引。我们可以维护一些关于算子状态的元数据。所以我们必须要open和close函数。当调用索引扫描算子的open函数式，就像为B+树创建迭代器一样，实例化迭代器，移动它生成一个元组。先open，调用next直到eof，最后调用close清理迭代器。当顶层的算子调用child.next时，会发生阻塞，然后执行流程会跳转到下层算子。（不设置的情况下，数据库默认使用的是inner join）管道之间存在依赖关系，R没有执行完毕之前，S是不能执行的。父级不在乎子级真正调用next时干了什么，可以是实际去查询一条数据，也可以时将查询好的数据直接返回。这种方式几乎被用了所有的DBMS中。这种方法的另一个优点是容易控制输出，如果有一个limit语句只要10条数据，那么只需要在获取10个元组后停止调用getNext即可。这就是之前说的，控制流和数据流在某种程度上是混合在一起的。物化模型物化模型中仍然有一种next函数，它实际上就像，给我一切。因此不再是每次调用操作符时生成一个元组，而是直接获取该操作符产生的所有元组。这项技术，20世纪90年代名为monadb的项目，Duckdb实际上是mongnadb的嵌入式版本。获取S中的所有元组，添加到输出缓冲区，然后现在传递给这个运算符，它会扫描数据并应用谓词。但是如果有一百万条记录，谓词只会匹配其中俩条，显然是不合理的。因此这个领域里一个非常常见的优化方式是算子融合。基本思想是，将同意流水线的不同算子内联，或者组合起来，从而避免不必要的复杂操作。在上述例子中，扫描R表，应用谓词，如果满足条件就将其放入输出缓冲区。物化模型更适合OLTP，因为查询只访问小部分的元组。更少的执行和协调开销，更少的函数掉用。对于OLAP，调用next会很昂贵，因为必须要扫描10亿个元组的场景存在。而且我们不想一次性物化所有的数据，因为我们希望分解处理流程并将其并行化，而不必担心耗尽所有内存，只需要生成运算符的之间结果即可。一个显而易见的方式是在俩者之间做一些事情向量化模型向量化模型类似迭代器模型，当调用next时，得到的不是一个元组，而是一批元组。现在获得该批元组的运算符将处理这批元组，在整个批上面处理。操作符的一次内部循环处理多一个元组批的大小可以基于精简和查询资源如果知道某个列中的属性和元组返回相同的值，可以使用类似游程编码的方式来进行。Duckdb底层就会进行类似的操作。和之前的例子类似，但是这次不再是传递单个元组，而是传递一个向量。该模型在Clickhouse，oracle，sql server，duckdb中均有使用。这是2006,2007年才提出的概念，最…

CMU数据库导论-1
type: item
date: 2026-07-12T14:14:29+08:00
description: CMU数据库导论-1关系模型关系型数据库（Relational Database，RDB）是一种基于关系模型（Relational Model）来组织和管理数据的数据库。它由Edgar F. Codd于 1970 年提出，其核心思想是将数据组织成二维表（Table），并利用数学中的关系代数来操作数据。但是论文中的表述是完全数学化的。IBM发明了第一个查询语言叫做SQUARE，但是这种语言写起来很怪，没有人真正实现它。然后SEQUEL被发明了，由于版权原因被改名为了SQL。SQL在86年就被标准化了，但是规范仍然会不停的更新。这些变化是为了适应人们编写应用的方式，以及对查询语言需求的变化。XML在90年代末20年代初很火，所以增加了XML的支持，Json在2010年火，所以增加了JSON。2023年增加了对图查询的支持，因此neo4j，可以通过SQL，在表的基础上进行图的遍历。如今要构建一个新系统，只少需要支持SQL92，它支持了常见的功能，比如select，from，group by，聚合函数等。平均每10年都有人说SQL要死了，但是它们从未发生。SQL分为DML，DDL，DCL。数据存储文件数据库可以分为多层查询计划-操作执行-访问方法-缓存管理-磁盘管理这一章讨论磁盘管理我们数据库会假设，在数据库管理系统中，数据主要存储在非易失性存储上。系统本质上是在协调数据在非易失性存储和内存上移动。磁盘数据需要加载到内存中才能提取答案。中间这条线以上的任何存储都是按照字节寻址的。按字节寻址意味着，每一个内存地址对应1Byte的数据。地址 数据0x1000 0x110x1001 0x220x1002 0x330x1003 0x44地址0x1000保存1个字节，地址0x1001保存1个字节，地址0x1002保存1个字节。。。。这里和操作系统的位数不同32位CPUCPU寄存器宽度、指针宽度、地址宽度主要为32位64位CPUCPU寄存器宽度、指针宽度、地址宽度主要为64位32位操作系统运行在32位架构上，使用32位指针和地址空间64位操作系统运行在64位架构上，使用64位指针和地址空间在non-volatile上存储的数据都是按块存储的。我们必须以块为单位访问整个页面，通常4KB。现代的存储设备，普遍采用4KB作为物理扇区，以前的硬盘很多是512byte现在通常是4096bytes。如果物理社区大小为4KB每个数据块需要刚好都落在4KB扇区内。如果一条数据跨越了俩个物理扇区，则需要进行多一次的内存读写。这就是为什么需要4k对齐。这将决定我们使用什么数据结构和算法。但是现代计算机中这些界限正在变模糊。现在SSD和HDD中出现了fast network storage，不存储在本地，但可以进行快速的访问，而且它是持久的。此外傲腾其实介于内存和磁盘之间。然而2022年傲腾被砍掉了。傲腾是一种特殊的硬件，向dram一样插在DIMM插槽中，但是如果拔掉电源可以保存存储的内容。如果intel没有砍掉它，很多课程中的内容将会消失，因为我们不再需要DRAM和磁盘之间来回移动数据了。现在替代傲腾的CXL3，是一种字节寻址的内存，它可能并不在你本地的物理机上。三星会售卖这种插在PCIE插槽上的设备，它就像是扩展了DRAM插槽的数量。它就像扩展了DRAM插槽的数量。CXL本身也不能解决断点数据保存。NVDIMM，运行时和普通内存一样，断电时电池继续供电，然后将数据写入磁盘，但是其也没有得到广泛的使用，在部分细分市场。随机访问通常比顺序访问来的慢。即便是SSD，真正读写的是page，顺序访问可以充分利用并行性，现代SSD内部其实有很多Flash芯片，多个channel同时工作，顺序读取几乎能跑满PCIE贷款。随机访问很难形成连续流水，假设读取4KB，可能在chip0,7,3。控制器需要查FTL，找到物理地址，发送命令，等待Flash响应，每一次都有固定的延迟。这意味着我们在设计数据库系统时，劲量选择顺序算法，因为顺序写入比随机写入块。我们假设系统运行在现成的文件系统上，并且我们只是在读取和操作这些文件。页在数据库系统和计算机中，实际上有三种页面概念在最低级别，有一个硬件页面，这是硬件公开的，实际可以执行的原子写入操作的最大大小，通常是4KB原子写入：要么写入完成，要么写入全部失败，不会出现中间状态。OS层级页会有page的概念，以为这是x86在80年代的做法。默认是4kb，在更新的版本中可以使用巨页（huge page）是2mb甚至1GB。在数据库中，基于操作系统页面，我们可以使用不同大小的页面。页面大小因数据库而异。sqlite、oracle、IMBDB2、rocksdb、wiredtiger（mongodb的默认存储引擎）都是4kb。sqlserver，pgsq…

linux网络IO
type: item
date: 2026-06-17T13:39:00+08:00
description: linux网络IO无论是select poll epoll都属于NIO。他们本身不负责读写数据，它们只是负责同志哪些fd已经准备就绪。BIO的核心是一个线程管理一个socket，NIO的核心是一个线程管理多个socket。虽然是non-blocking IO。但其实底层都是要进行同步阻塞的。BIO的核心是线程等待IO，NIO的核心是线程等待事件。select、poll 和 epoll 本质上都是 Linux/Unix 提供的系统调用（system call）接口，而在 C 语言中表现为函数名称。Go语言中BIO的底层实现通常是Epoll，java的BIO底层实现就不是。G语言更像把netty藏进了语言运行时，对上层暴露的接口仍然是BIO风格的。select最简单的方式获取网络中传输过来的数据是通过select，不停的轮询是否有数据到来，但是这样子就会不停的询问内核，导致用户态和内核态不停切换造成开销。一个简单的优化方式是，将自己需要监听的文件描述符通过bitmap直接告诉内核。sockfd = socket(AF_INET, SOCK_STREAM, 0); memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_port = htons(2000); addr.sin_addr.s_addr = INADDR_ANY; bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)); listen(sockfd, 5); for (i = 0; i < 5; i++) { memset(&client, 0, sizeof(client)); addrlen = sizeof(client); fds[i] = accept(sockfd, (struct sockaddr *)&client, &addrlen); if (fds[i] > max) max = fds[i]; } while (1) { FD_ZERO(&rset); for (i = 0; i < 5; i++) { FD_SET(fds[i], &rset); } puts("round again"); select(max + 1, &rset, NULL, NULL, NULL); for (i = 0; i < 5; i++) { if (FD_ISSET(fds[i], &rset)) { memset(buffer, 0, MAXBUF); read(fds[i], buffer, MAXBUF); puts(buffer); } } }我们调用核提供的select方法，内核会接受一个1024个bit的bitmap（rset），不需要监听的被设置为0。我们让内核来判断是否有数据到来，这个select函数是一个阻塞函数。直到有数据才会继续执行。当有数据来的时候，内核会将有数据的FD置位，select函数会返回。这里的fd+1是最大需要的文件描述符编号。比如我最大需要的FD是9，那么内核会将0-9这10个文件描述符拿出来。当有数据的时候，可能存在同时多个FD都有数据。因此下面还是要遍历每个fd是否有数据。用户程序需要完整遍历一次bitmap才知道哪些FD是有数据的。这里提高效率主要的一点是把判断放到了内核态。但是这里的缺点是bitmap最大是1024，此外rset会被内核修改，每次都要重新开一个新的rset进行设置。这里用户态和内核态还是要频繁的拷贝这个bitmap，还是有一定开销。而且每次要重新O（n）去遍历bitmap。select是19世纪的API，因此有很多的缺陷。现在很多系统还在使用select。POLLstruct pollfd { int fd; short events; //在意的事件 short revents; //对event的回馈 }; for (i = 0; i < 5; i++) { memset(&client, 0, sizeof(client)); addrlen = sizeof(client); pollfds[i].fd = accept(sockfd, (struct sockaddr *)&client, &addrlen); pollfds[i].events = POLLIN; } sleep(1); while (1) { puts("round again"); poll(pollfds, 5, 50000); for (i = 0; i < 5; i++) { if (pollfds[i].revents & POLLIN) { pollfds[i].revents …

agent学习笔记
type: item
date: 2026-06-02T21:46:03+08:00
description: agent学习笔记https://xiaolinnote.com/ai/agent/ageng是什么？agent本质是是一个能自主完成目标的AI系统，和传统AI核心区别在于自主性和能行动。普通语言模型在被训练完毕之后知识就被冻结。本质上就是一个文字输出器。 并且每次都会忘记之前做过什么。感知->规划->行动->再感知。Anthropic 在 2024 年底提出的 MCP（Model Context Protocol，模型上下文协议）然后是谷歌在2025年四月退出的agent2agent，解决了agent和agent之间的写作问题。agent的组成部分LLM+工具系统+记忆系统+规划记忆这里记忆系统分长短期记忆。短期记忆是当前这轮上下文中的对话内容，是在context window里的。长期记忆使用向量数据库来实现，把重要的信息embeddig之后存起来，下次用语义检索的时候可以拿回来。短期记忆的挑战是上下文窗口的限制，每次工具有大量文本，上下文很快就满了。这时候要恶魔摘要压缩（把前面的步骤浓缩成关键信息）要么滑动窗口，只保留最近的几步详细内容。但不管哪种方式都会丢失信息，怎么在记住够多和不撑爆上下文中间做取舍，是记忆刚才里最核心的设计问题。长期记忆的挑战在什么该存，什么不该存。存的太多会产生噪音，存的太少又失去长期上下文的意义。比如三个月前客服处理的问题现在通常是不重要的。常用的指数衰减公式：规划规划决定了agent能否应对复杂任务。比如任务是克隆一个写竞品分析报告，它就需要搜索资料，整理相关数据，对比分析，撰写报告。最基础的是（chain of thought）COT，思维连，核心思想是让模型把思考过程写出来，而不是直接输出最终答案。为什么这么简单一句话就能提升效果？因为 LLM 的 token 生成是逐步进行的，每一步推理的输出会成为下一步推理的输入，把中间步骤写出来，等于给了模型更多的「思考空间」。在此基础上还有Tree of thoughts（ToT）思维数。它不是走一条线性的推理链，而是在每个推理节点上展开多个可能的分支，然后评估每个分支的质量，选出最优的路径继续往下走。一种是先规划后执行，也就是plan and execute模式，先让LLM输出一个完整的步骤列表ReAct模式，每走一步就工具当前结果重新思考下一步该做什么。不提前指定计划。好处是灵活性高。总的来说agent的节奏很简单：规划 决策 执行 结果存入记忆 再决策。workflow agent Tools分别是什么？Toolstools是整个体系里最简单，最底层的概念，本质是是一个按定制格式暴露给LLM的函数：普通函数是给程序员调用的，Tool是给LLM调用的。所以要配一份LLM才能看得懂的schema（名字、描述、参数类型）工具本身没有决策能力，它甚至不知道自己应该在什么时候被使用。工具的设计要职责单一，描述精确，错误信息要清晰。Anthropic 在2024年提出的MCP，就是把工具的注册，描述调用做成一套标准的协议。AgentTools就是拿着工具的角色。Agent的运行方式是Thought、Action、Observation反复循环的模式。WorkFlow假设要做一个客服系统，大致流程就是先判断用户问的是什么问题。再去知识库里检索相关内容 生成一个回答。这些步骤分支完全可以在代码里写死。Workflow 最大的优点是可预测、可控、好调试。你在代码里看到什么，它就做什么，不会有任何「惊喜」。生产环境里出了问题，你可以打断点逐步追，精确定位是哪个节点出了故障。这种确定性在线上系统里非常珍贵。Anthropic 在他们的 Agent 工程实践中总结了几种常见的 Workflow 编排模式，值得了解一下。• 第一种叫「Prompt Chaining」（提示链），就是把一个大任务拆成多个小步骤，前一步的输出作为后一步的输入，像流水线一样串起来。• 第二种叫「Routing」（路由），先用一个 LLM 做分类判断，然后根据分类结果把请求分发到不同的处理分支，前面客服系统的例子就是典型的路由模式。• 第三种叫「Parallelization」（并行化），把可以同时进行的子任务并行执行，最后汇总结果，这在需要多维度分析的场景下特别有用，比如同时从多个数据源检索信息。• 第四种叫「Orchestrator-Workers」（编排者-工人），一个中央编排者负责分配任务，多个 Worker 各自完成子任务，适合任务可以分解但子任务之间相互独立的场景。另一个非常实用的模式是evaluator-optimizer 评估者优化者模型。一个 LLM 负责生成输出，另一个 LLM（或者同一个模型换一个角色）负责评估这个输出的质量，如果评估不通过就把反馈给回生成者，让它改进后重…

时间API
type: item
date: 2026-05-08T10:45:00+08:00
description: 时间APITSCTSC（Time Stamp Counter，时间戳计数器）是现代 CPU 中一个非常底层的硬件计数器，用来记录自 CPU 上电以来经过的时钟周期数。TSC 本质上是一个 64 位寄存器，通常位于 CPU 内部，每当 CPU 时钟周期推进一次，它的值就 +1（在现代 CPU 中已经不完全等价于“每个核心周期”，但概念上仍是“高精度单调计数器”）。在 x86 架构中，可以通过指令读取：RDTSC（Read Time Stamp Counter）TSC 常用于性能分析，因为它的分辨率非常高（纳秒级甚至更细）。操作系统、数据库（比如 MySQL InnoDB）、高性能网络系统都会用 TSC 做：函数耗时统计lock contention 分析cache miss 成本分析TSC ≠ wall clock timevDSO优化vDSI是linux提供的一种机制，把一部分内核提供的只读系统调用映射到用户态的空间里，让用户态直接调用，而不需要陷入内核。vDSO = “内核提前帮你准备好的用户态函数库（但数据由内核更新）”典型函数包括：gettimeofdayclock_gettimetime传统路径：用户态函数调用 → syscall 指令 → CPU 切换到内核态 → 内核处理 → 返回用户态问题在于：syscall 有 上下文切换成本需要保存/恢复寄存器可能污染 CPU cache / pipeline高频时间查询会非常昂贵比如：一个高性能服务每秒可能调用 clock_gettime 上千万次具体方式：内核在进程地址空间映射一段“只读共享代码”用户态直接调用这些函数不发生 syscall数据仍由内核更新（比如时间）Linux CLOCK_MONOTONICCLOCK_MONOTONIC是Linux 提供的一种单调递增时钟，用于测量时间间隔，而不是表示当前真实时间。他的关键特性是：时间只会向前走不会因为系统时间被修改而改变。类型含义是否受系统时间影响CLOCK_REALTIME真实世界时间（Unix time）会（NTP、手动修改会跳变）CLOCK_MONOTONIC单调时间（从某个起点开始计时）不会CLOCK_MONOTONIC 的起点是：系统启动（boot）之后的某个未定义时间点注意：不等于 Unix epoch（1970）不可预测只保证单调递增他不能跨机器比较。也不能转换成unix时间戳。它不是时间而是时间差工具。

那些底层使用了Rocksdb的项目
type: item
date: 2026-03-20T15:25:00+08:00
description: 那些底层使用了Rocksdb的项目元数据使用了RocksDB的框架kafka：的streams默认将持久化状态的中间计算结果Pulsar的底层存储，也就是Bookkeeper：的默认存储实现中，Rocksdb用来维护Ledger元数据索引rocketmq：RocketMQ 在 broker 端通过 RocksDB 来保存 消费偏移量、订阅组管理/配置、认证授权数据等元数据Ceph：使用Rocksdb来管理对象映射和磁盘分配记录。Doris在后端BE存储中使用了rocksdb保存元数据的键值对。主要状态数据使用RocksDBMySQL/MariaDB中存在innodb的另一个存储引擎选择，MyRocks，底层使用了rocksdb作为存储引擎。顺序批量写入性能更高，但是查询延迟稍高。Kvrocks，pikiwidb，Tendis他们都兼容Redis协议，并且底层使用rocksdb作为主要的持久化协议存储。tidb：底层使用的tikv的内部使用了rocksdb作为存储实现。Flink：Checkpoint可以选择，通过使用rocksdb存储到本机。ClickHouse 提供了一个叫 EmbeddedRocksDB 的表引擎，可以让用户把一个表的数据存放在一个 RocksDB 实例中，并通过 ClickHouse SQL 访问它。Nacos等这些底层依赖sofa-jraft的框架底层都采用rocksdb作为存储，不过也允许切换为其他存储实现。MongoDB提供了rocksdb存储引擎插件，优化高写入场景

LLM的有趣技术
type: item
date: 2026-03-18T22:18:00+08:00
description: LLM的有趣技术https://www.bilibili.com/video/BV1gEcmzzE4P/语言模型的进化多模态大语言模型是如何将所有的感官，对齐到语言系统中的？这本质上属于多模态对齐（multimodal alignment）与表征学习（representation learning）的交叉问题。可以从结构层面+训练机制+对齐目标三个维度来精确理解。人类的感官数据在原始形态上完全是异构的，所以核心问题变成，如何把连续的信号映射到一恶搞可以被语言模型理解的符号空间。现代多模态大模型（如 GPT-4o、Gemini、Claude 等）通常采用如下结构：不同模态编码器 → 统一 embedding 空间 → LLM（语言核心）每种感官都有专属编码器，比如图像的Vit，音频的wav2vec，视频的时空tranformer，文本的token embedding。这些编码器的目标都是抓成一个高维语义向量。那么如何对其到共享语义空间？对比学习对比学习：Contrastive learning 代表作为CLIP。训练目标：图像embedding=描述该图像的文本。即最大化：正样本（匹配图文）的相似度负样本（不匹配）的距离结果图像和语言落在同一个语义空间中跨模态生成Generative Alignment图像 → 文本（captioning）音频 → 文本（ASR）视频 → 文本（描述）训练方式输入：图像 embedding目标：生成对应文本 token让 LLM 学会“用语言解释感官”联合训练直接训练一个模型处理：[图像 tokens] + [文本 tokens] → 预测下一个 token把非语言模态“token化”（tokenization of modalities）语义一致性最终目标不是“数据形式一致”，而是：不同模态表达相同“意义”时，其内部表示接近一张猫的图片“a cat sitting on a sofa”在 embedding 空间中应接近为什么语言成为“中心模态”？语言有几个决定性优势可以表达复杂结构：时间因果抽象概念标注成本最低：图像/音频的语义通常通过语言标注获得LLM 的核心能力：chain-of-thoughtplanningabstraction都发生在语言空间。语言不是被对齐的对象，而是“语义坐标系”所有感官数据都被投影到这个坐标系中。换句话说：图像 ≠ 变成文字图像 → 映射到“可以用语言解释的语义空间”思维连chain-of+thought。本质是让模型在给出最终答案之前，显式或隐式的生成中间推理步骤，从而提升复杂任务的正确率。传统语言模型直接输出答案，COT是在输出和最终答案中增加了思考步骤。模型被引导像人一样一步步想。可以将一个复杂的问题，拆成多个简单的子问题。在训练或推理是。没有CoT只有最终loss，有CoT：每一步都有结构化的模式。本质上相当于在token级别上引入推理轨迹约束。CoT 提示过程是一种最近开发的提示方法，它鼓励大语言模型解释其推理过程。思维链的主要思想是通过向大语言模型展示一些少量的 exapmles，在样例中解释推理过程，大语言模型在回答提示时也会显示推理过程。这种推理的解释往往会引导出更准确的结果。零样本思维链（Zero Shot Chain of Thought，Zero-shot-CoT）提示过程是对 CoT prompting 的后续研究，引入了一种非常简单的零样本提示。他们发现，通过在问题的结尾附加“Let's think step by step”这几个词，大语言模型能够生成一个回答问题的思维链。从这个思维链中，他们能够提取更准确的答案。外部工具ReAct 和 Toolformer 是两种典型的“让模型使用工具/外部能力”的方法论，但它们的设计哲学和实现路径差异很大ReActReAct = 推理（Reasoning）+ 行动（Acting）交替进行模型在推理过程中，不只是“想”，还会：显式生成思考链（Thought）决定调用工具（Action）接收工具返回结果（Observation）再继续推理本质：把推理过程外显，并嵌入工具调用显式推理链（Chain-of-Thought）类似 CoT，但更进一步推理过程中可以“打断去查资料”动态工具调用工具使用是 runtime 决策不需要提前训练工具调用能力强交互性适合：多步推理（multi-hop QA）agent系统复杂决策任务ToolformerToolformer = 让模型自己学会什么时候调用工具和 ReAct 最大区别：ReAct：靠 prompt 引导Toolformer：靠训练学出来Toolformer 做了三件关键事：自监督生成工具调用数据给模型少量 seed examples，然后：模型自动：在文…

Epoch-Based Reclamation算法
type: item
date: 2026-03-13T13:51:32+08:00
description: Epoch-Based Reclamation算法https://www.zhihu.com/question/406428296/answer/1345620324概述EBR是在无锁或低锁并发结构中常用的聂村回收机制。核心目标是解决并发环境中内存安全释放的问题。在并发数据结构中，删除节点不等于可以立即释放内存。比如在删除的时候，其他线程可能仍然持有指针，但删除线程无法知道。常见的类似方案RCU：read-copy-update。写操作不直接修改正在被读取的对象，而是创建新的副本。类似COW引用计数，开销稍大。Hazard Pointers：精确但复杂EBR的思想是：延迟释放核心Bw-Tree（B-link Tree without latches）是一种无锁的并发B+树变体。目标是实现高并发环境下实现高吞吐的索引结构。避免传统B+树锁竞争的问题。Bw-Tree主要针对内存数据库和无锁结构。每个操作在开始的时候注册到一个Epoch，可以根据物理时间，每10ms作为一个epoch操作开始时增加Epoch counter，操作结束时减少Epoch counter操作过程中，产生的垃圾都放到epoch内部的garbage list上当这个epoch的所有操作都结束了，即counter为0时，这个epoch的所有资源都可以被释放用上图举例，这是BwTree的内存垃圾回收。众所周知BwTree是一个latch-free的高性能索引，既然要latch-free那么内存分配一定不能拖后腿了。在对索引进行操作时，例如删除索引的一条数据，并不能直接释放内存，否则无锁算法很容易出现ABA问题。所以在操作过程中产生的垃圾，都放到epoch的garbage list上，当所有线程都退出这个epoch就可以安全地释放掉所有资源。（事实上，上面这个图还是Centralized，Open-BwTree里用了扩展性更好的Decentrailzed实现）EBR算法的回收是机会式回收。是许多lock-free数据结构的默认实现方式。相比RC相比RC（reference counting）引用计数法，EBR有更好的缓存局部性。引用计数每次操作都要原子写入。这会导致cpu的缓存行竞争严重。多次出发缓存行失效是严重的性能问题。EBR 也不是完美的。一个线程的sllep会导致全局epoch无法推荐。导致重试列表无限增长。内存回收也可能会不及时导致内存占用更高。特性Reference CountingEpoch Reclamation读开销高（atomic inc）极低写开销atomic dec低cache contention严重很小回收方式即时延迟内存占用低可能较高线程停顿影响无有扩展性差好EBR 的效率来自：消除 per-access 原子操作，把回收同步从“每次访问”降低到“偶尔 epoch 推进”。

MQTT学习笔记
type: item
date: 2026-03-11T17:54:00+08:00
description: MQTT学习笔记简介写给那些已经学习过消息队列的人（）或者间接连接。比如移动网络，卫星网络等。协议非常轻量header只有2字节。MQTT只有一种通信模型Publisher → Broker → Subscriber例如temperature/sensor1 temperature/sensor2设备只关心topic，topic是层级字符串，支持通配符订阅。如temperature/+ temperature/#不同于消息队列mqtt有统一的开源标准化协议。由OASIS 维护。通信模式MQTT支持多种模式如点对点：每个消息只消费一次Producer → Queue → Consumer发布订阅：每个订阅者都收到消息。流处理MQTT的Qos从高到低依次为Qos 0：至多依次（at most once1）发布者发送消息后不等确认，直接发送下一条消息。代理收到消息后直接转发给订阅者，同样不等待订阅者确认。Qos 1：至少一次（at least once）这是中等级别，确保消息至少被送达一次，但可能重复。发布者发送消息后，会等待代理返回一个 PUBACK（发布确认）包。如果超时未收到确认，发布者会重新发送消息，直到收到确认为止。代理收到消息后，会存储消息，并确保将其发送给订阅者（同样需要订阅者的确认）。消息不会丢失知道重试成功。Qos 2：恰好一次（exaclly once）最高的级别。确保消息仅有一次被送达。这个机制通过四次握手实现（four way handshake）PUBLISH：发布者发送消息。PUBREC：代理收到后回复“已接收”。PUBREL：发布者收到确认后，发送“释放”信号。PUBCOMP：代理回复“完成”，此时双方才确认消息处理完毕。Qos机制只是传输层的机制，只负责消息物理上从生产者到消费者，但无法感知消费者应用层代码是否执行成功。PUBREC/PUBCOMP 由 MQTT client library 自动发送。而不是等你业务处理完成后再发。MQTT没有类似消息队列中的DLQ（死信队列）的实现。MQTT语义是传递保证，而不是处理保证。因此broker不会感知业务是否处理成功。QoS2保证的是“协议层 exactly-once”，不是“业务 exactly-once”。这里的ack和kafka不同的是收到消息就ack，而不是完成处理才ack。MQTT 本身只是一个消息传输协议，是否把消息保存到磁盘、保存多久、是否清理，取决于具体的 MQTT Broker 实现和配置。不同 Broker（如 Eclipse Mosquitto、EMQX、HiveMQ）策略会不同。可以从几个机制来理解。如果要保证消费成功可以通过应用层ackProducer → MQTT → Consumer ↓ 处理成功 ↓ publish ack message例如topic: order/create topic: order/create/ack和消息队列的区别mqtt 出现的更早，是1999年的，kafka是2011年。并且一般kafka的生产者都是内网的机器有单独的稳定的IP。而mqtt生产者可能只有NAT的IP，并且无法固定。以EMQX距离，吞吐量能来到50w到100w。kafka在100w-1000w。由于许多应用层消费逻辑都只适配了kafka的。你甚至可以把MQTT消息桥接到kafkaIoT Device / Client │ ▼ MQTT Broker (EMQX / HiveMQ / Mosquitto) │ │ MQTT → Kafka Bridge / Connector ▼ Kafka Cluster │ ├── Stream processing (Kafka Streams / Flink / Spark) ├── Storage (Data lake / DB) └── MicroservicesMQTT为极低带宽+不稳定网络设计。最初为IOT、传感器网络设计。适用于设别算力弱。网络延迟高的场景。MQTT协议甚至不强制要求持久化。直接进行实时转发。MQTT的设计重点通常不是吞吐，而是轻量级通信。如EMQX默认所有数据都存储在内存中。通过规则引擎（Rule Engine）和数据桥接（Data Bridge），可以将消息、客户端上下线事件等数据持久化到外部数据库（如 MySQL、PostgreSQL、Redis、MongoDB 等）。这种方式通常用于业务数据归档或与外部系统集成。总的来说MQTT是无状态的。MQTT的顺序消费还要结合Qos来保证。无论是Qos1还是2都不保证严格顺序。这个顺序更像是TCP的顺序，业务层要自己控制执行顺序（毕竟ack是自动回复的，而不是业务层实现的）。