tcmalloc小记

原文:https://google.github.io/tcmalloc/design.html 之前尝试过探索一下golang的内存分配机制,但是由于其太过于庞大,个人积累也不足,就先放弃了。后来想起来它跟tcmalloc的分配方式很像,所以就单独抽时间看了下tcmalloc的设计思路。 这篇几乎是原文的翻译篇,可能会加入一些个人理解。 tcmalloc的特性 大多数的分配和释放内存都是无竞态的,对于多线程程序来说更加友好。 被释放的内存可以给不同大小的对象进行复用,或者交还给操作系统,在内存使用上更加灵活。 分配相同大小对象的page,从而节省用于管理小对象的内存开销。 提供了解当前内存使用情况的能力,并且采样的开销较小。 概览 下面这张图是tcmalloc内部的概览图。 意思是可以将tcmalloc大致分为三个层次: front-end:提供线程或者cpu级别的cache,这是最快的分配对象方式。 middle-end:负责填充front-end的cache。 back-end:负责向操作系统申请更多的可用内存。 就这么简单一看的话,tcmalloc似乎就是利用了缓存以及线程隔离的思想去加快分配内存。 front-end要么是运行在per-cpu的模式或者传统的per-thread模式,以及back-end要么支持传统页堆或者大页堆。 front-end front-end管理cache,这个cache的里面是待分配内存块,cache每次只会被一个线程访问,因此不需要加锁,分配和释放内存都非常快。 如果外界向front-end请求的内存大小对应的cache为空,那么front-end就会向middle-end去请求一批可用内存去填充cache。middle-cache由CentralFreeList和TransferCache组成。 如果外界向front-end请求的内存大于front-end最大的cache,或者middle-end已经耗尽,那么就会找back-end分配这个大内存块,或者请求填充middle-end。back-end也称为PageHeap页堆。 之前说到front-end的两种模式,分别是per-thread和per-cpu,因为现代大型程序通常包含很多线程,每个线程都要cache,就会导致front-end整体内存占用很大,若要降低front-end的整体内存占用,那么每个线程的cache又会变得太小,导致cache频繁耗尽或者溢出。回想一下tcmalloc要弄per-thread cache的原因只是为了降低并发锁冲突,但现代计算机上如果多个线程运行在同一个cpu上,这些线程实际上是线性交替执行,因此tcmalloc支持了per-cpu的模式,从而降低内存占用的同时又能保证降低并发冲突。 分配小对象和大对象 小对象会由front-end来负责映射到60到80个大小类别中,比如分配一个12字节的对象属于16字节的大小类别。但是如果分配的内存太大,就会由back-end来直接负责此次分配,这种对象的大小与tcmalloc页大小对齐。 原文还说了一些关于小对象内存分配对齐的事情,这里跳过。 释放内存 关于内存的释放这块感觉还是挺复杂的。 首先,如果释放的内存大小是编译时未知的,tcmalloc就要去查找pagemap。如果释放的是小对象,就会被归还到front-end的cache中,否则就会被归还到back-end的页堆中。 per-cpu模式 在这个模式中,front-end管理着一大块内存(称为slab) 一图胜千言,slab被分为多个cpu块交给不同的cpu管理,我们将cpu块称为cpu本地缓存。本地缓存中的左边存储元数据,右边存储指向可用对象的指针数组。 元数据中有三种指针,分别指向对应大小类数组中的开始位置、当前分配到的位置、当前最大位置。另外,还有一个全局的“静态最大容量”,限制了当前最大位置减去数组开始位置不能大于这个“静态最大容量”。虽然没看过内部实现,但盲猜这个静态最大容量是硬编码的,并且“开始位置指针”和“当前最大位置指针”在整个cpu块上是动态变化的,目的是尽量高效地提高内存利用率。 对象都从本地缓存中获取或者归还,当获取时如果本地缓存不够用就会找middle-end拿,当归还的时候如果本地缓存溢出则溢出到middle-end中。 当某个大小类别的对象已经分配完,tcmalloc会尝试增大该大小类别的容量,直到达到该大小类别的静态最大容量或者整个cache的最大容量。为了增加容量,该大小类别甚至可以从同一个cpu块的其它大小类别中“偷”容量。 front-end限制的是单个cpu本地缓存的大小,那么如果机器的cpu越多,front-end就占用越多内存。为了节省内存,当程序不再使用某个cpu时,tcmalloc支持释放该cpu缓存中的内存。 rseq tcmalloc的per-cpu模式依赖于linux的rseq系统调用。rseq可以理解成原子指令序列,如果这个指令序列执行到一半发生了上下文切换,那么下一次会重新从头开始执行。该序列要么不间断地完成,要么反复重新启动,直到不间断地完成,这个过程是不需要锁的,从而避免了序列的争用。 那么rseq对于per-cpu模式的意义就在于,在无需使用锁的情况下就读写per-cpu数组的元素。 这部分更具体的设计参考https://google.github.io/tcmalloc/rseq.html per-thread模式 在这个模式中,每个线程都有自己的本地缓存。每个本地缓存中的数据结构组织如图所示,每个大小类的空闲对象用单链表进行组织。 ...

三月 16, 2025 · by NOSAE

请不要再称数据库是CP或者AP (Please stop calling databases CP or AP)

Note 文章转载自https://blog.the-pans.com/cap/ 其它参考: quorum-rw 后分布式时代: 多数派读写的’少数派’实现 经Martin Kleppman本人同意,这篇文章是他英文原文的中文翻译。Authorized by Martin Kleppmann, this is a Chinese translation of his original blog post. ...

十一月 17, 2024 · by NOSAE

向量数据库概述 转载

也许你最近可能听过这样的新闻,某向量数据库的初创公司刚写好 PPT,就获得了几千万的投资,某公司的开源的向量数据库因其代码的简陋而登上了 Hackernews 等等。在过去几个月时间中, AI 应用的发展如火如荼,带动了 AI 应用技术栈上下游的火爆,而向量数据库就是其中最热门的之一。 笔者最近因为开发 ChatFiles 和 VectorHub 两款开源项目的需要从而对向量数据库(Vector Database)进行了学习,在对主流的向量数据库和搜索算法有了大概的了解后,笔者决定将这些知识整理成一篇文章,希望能够帮助到大家。 GPT 的缺陷 过去几个月的时间,我们正处于人工智能的革命中,其中最耀眼的莫过于 GPT-3.5/4 的横空出世,而 GPT-3.5/4 带给我们无限震撼的同时,其天然的缺陷和诸多的限制也让开发者头痛不已,例如其输入端上下文(tokens)大小的限制困扰着很多的开发者和消费者,像 gpt-3.5-turbo 模型它的限制是 4K tokens(~3000 字),这意味着使用者最多只能输入 3000 字给 GPT 来理解和推理答案。 有人可能会疑惑,我使用的 ChatGPT 是有对话记忆功能的,既然它可以做到聊天记忆,那么它的输入端 token 有限制也没什么关系,只要我将给 ChatGPT 的文字内容拆分成多次输入,它自然就可以记住我之前的对话,从而做到解除 token 限制。 这个想法是不太正确的,GPT 作为 LLM 模型是没有记忆功能的,所谓的记忆功能只是开发者将对话记录存储在内存或者数据库中,当你发送消息给 gpt 模型时,程序会自动将最近的几次对话记录(基于对话的字数限制在 4096 tokens 内)通过 prompt 组合成最终的问题,并发送给 ChatGPT。简而言之,如果你的对话记忆超过了 4096 tokens,那么它就会忘记之前的对话,这就是目前 GPT 在需求比较复杂的任务中无法克服的缺陷。 目前,不同模型对于 token 的限制也不同,gpt-4 是 32K tokens 的限制,而目前最大的 token 限制是 Claude 模型的 100K,这意味可以输入大约 75000 字的上下文给 GPT,这也意味着 GPT 直接理解一部《哈利波特》的所有内容并回答相关问题。 但这样就能解决我们所有的问题了吗?答案是否定的,首先 Claude 给出的例子是 GPT 处理 72K tokens 上下文的响应速度是 22 秒。如果我们拥有 GB 级别或更大的文档需要进行 GPT 理解和问答,目前的算力很难带来良好体验,更关键的是目前 GPT API 的价格是按照 tokens 来收费的,所以输入的上下文越多,其价格越按昂贵。 ...

十一月 16, 2024 · by NOSAE

分布式事务综述

理论知识 事务的四个特性:ACID Atomic 原子性:一个事务中的所有操作,要么全部完成,要么全部不完成 Consistency 一致性:在事务开始之前和事务结束以后,数据库的完整性没有被破坏。完整性包括外键约束、应用定义等约束不会被破坏 Isolation 隔离性:防止多个事务并发执行时由于交叉执行而导致数据的不一致 Durability 持久性:事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失 分布式系统三个特性:CAP Consistency 一致性:集群执行某个操作后,所有副本节点的状态都相同,那么这样的系统就被认为具有强一致性 Available 可用性:集群一部分节点故障后,还能对外提供服务 Partition tolerance 分区容忍性:狭义上是集群节点之间是否能正常通信,更广义的是对通信的时限要求,系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况 对于互联网的场景来说,一般选择AP:因为现在集群规模越来越大,主机众多、部署分散,所以节点故障、网络故障是常态,而且要保证服务可用性达到N个9,即保证P和A,舍弃C。 分布式的BASE理论:柔性事务,对CAP的权衡 Basically Available 基本可用:系统在出现不可预知故障的时候,允许损失部分可用性,但这绝不等价于系统不可用 Soft state 软状态:允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性 Eventually consistent 最终一致性:经过一段时间的同步后,最终能够达到一个一致的状态 NewSQL的分布式事务: 以Spanner、TiDB为代表的NewSQL,在内部集群多节点间,实现了ACID的事务,即提供给用户的事务接口与普通本地事务无差别,但是在内部,一个事务是支持多个节点多条数据的写入,此时无法采用本地ACID的MVCC技术,而是会采用一套复杂的分布式MVCC来做到ACID。属于满足CP的系统,同时接近于满足A,称之为CP+HA。但是NewSQL和BASE的系统之间,性能上差异可能是巨大的。 跨库跨服务的分布式事务:这类分布式事务部分遵循 ACID 原子性:严格遵循 一致性:事务完成后的一致性严格遵循;事务中的一致性可适当放宽 隔离性:并行事务间不可影响;事务中间结果可见性允许安全放宽 持久性:严格遵循 现有的分布式事务方案都无法做到强一致,但是有强弱之分:XA事务 > TCC > 二阶段消息 > SAGA(一般情况下)。具体为: XA:XA虽然不是强一致,但是XA的一致性是多种分布式事务中,一致性最好的,因为他处于不一致的状态时间很短,只有一部分分支开始commit,但还没有全部commit的这个时间窗口,数据是不一致的。因为数据库的commit操作耗时,通常是10ms内,因此不一致的窗口期很短。 TCC:理论上,TCC可以用XA来实现,例如Try-Prepare,Confirm-Commit,Cancel-Rollback。但绝大多数时候,TCC会在业务层自己实现Try|Confirm|Cancel,因此Confirm操作耗时,通常高于XA中的Commit,不一致的窗口时间比XA长 MSG:二阶段消息型事务在第一个操作完成后,在所有操作完成之前,这个时间窗口是不一致的,持续时长一般比前两者更久。 SAGA:SAGA的不一致窗口时长与消息接近,但是如果发生回滚,而子事务中正向操作修改的数据又会被用户看到,这部分数据就是错误数据,容易给用户带来较差的体验,因此一致性是最差的。 2PC 两阶段提交流程如下: 准备阶段:RM去开启事务并执行操作,但是不提交事务,此时相关资源都被锁定,第三方不能访问这些资源。返回操作结果给TM 提交阶段:若所有RM在准备阶段都操作成功,TM将发出请求让所有RM提交事务,否则发出请求让所有RM回滚 存在问题: 同步阻塞:从准备阶段的锁定资源,直到事务提交/回滚的过程中,资源都处于被RM锁定的状态,第三方访问会被阻塞。 单点故障:2PC的推进重度依赖TM,TM若发生故障,RM会被阻塞。 数据不一致:若提交阶段部分RM无法与TM正常通信,导致一部分子事务提交了,而发生异常的RM没提交,全局事务发生不一致。 3PC 3PC提交将2PC的准备阶段再次拆分,加入检查阶段,如果检查失败的话马上abort,减少了2PC在失败情况下白白浪费资源,并且引入RM的超时机制(2PC只有TM超时机制),如果在提交阶段TM超时,直接提交事务释放资源。3PC流程如下: 检查阶段:TM 询问 RM,是否具备执行事务的条件,RM 进行自身事务必要条件的检查 预提交阶段:TM 通知 RM 进行事务的预提交 提交阶段:TM 根据预提交阶段 RM 的反馈结果通知 RM 是否进行事务提交或是进行事务回滚 RM超时自动提交是因为此时已经进入了提交阶段,说明RM知道检查阶段已经成功(但是RM不知道预提交阶段是否成功),认为第三阶段很大概率也可以成功。但很明显,如果第二步存在RM失败了,即预提交失败,因此在第三步中,TM发出回滚请求,但此时又有另外一个RM超时并自动提交了事务(超时时间内收不到回滚请求),就会出现不一致。 ...

十月 28, 2024 · by NOSAE

clash规则配置

我一直在用的wmsxwd抑或是一元机场,这些厂商自带的代理规则都太少,对于没匹配上规则的网站,只能要么全部走代理或者全部直连,搞得我经常要手动切换。所以我需要一个相对更加全面的规则列表,让该走代理的网站走代理,该直连的直连!!! 无意中翻到clash-rules这个每日更新代理规则的仓库,便拿来用之。 使用方式如下: 去机场厂商那里拿到订阅链接,注意有的厂商不会直接给订阅链接而是托管链接,并且会给你一个网站自己去转换成订阅链接 创建一个.yaml文件(文件名随便),不同的客户端可能配置文件位置不同,比如我是放在~/.config/clash目录下 编辑文件内容,其中一些配置用原来默认的就行,比如我的是 mixed-port: 7890 allow-lan: true bind-address: '*' mode: rule log-level: info external-controller: '127.0.0.1:9090' 配置proxy-providers配置,顾名思义,配置代理 proxy-providers: CNIX: # 代理名称,自定义即可 type: http # 以远程下载的方式获取代理 url: "订阅链接" # 填写你的订阅链接 path: ./cnix.yaml # 从订阅链接下载存到本地的cnix.yaml文件 interval: 86400 # 更新间隔(s) 配置rule-providers,顾名思义,配置代理规则,同样以远程的方式获取,并定时更新,参考仓库的教程即可,比如加入一个名为google的远程规则(不知道为什么jsdelivr我经常访问不了,所以就直接不走这个cdn了): rule-providers: google: type: http behavior: domain url: "https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/google.txt" path: ./ruleset/google.yaml interval: 86400 配置proxy-groups,CNIX就是刚刚配置的proxy-providers,这里定义一个代理组去使用它 proxy-groups: - name: PROXY type: select use: - CNIX 配置rules,将规则和代理组关联起来。除了PROXY以外,DIRECT和REJECT可视为默认的代理组,即直连和拒绝,其中拒绝网站通过查看reject.txt就能看到,包含一些广告网站或者人家的内部网站、垃圾网站等。想访问某个网站一直被拒而关掉clash后又能访问的话,可以看看是不是reject.txt的锅 ...

十月 21, 2024 · by NOSAE