first commit

This commit is contained in:
张乾
2024-10-15 21:07:49 +08:00
parent 58cbf6795b
commit 1b0c35dd30
115 changed files with 6918 additions and 21 deletions

View File

@ -0,0 +1,68 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
00 开篇寄语:缓存,你真的用对了吗?
你好,我是你的缓存老师陈波,可能大家对我的网名 fishermen 会更熟悉。
我是资深老码农一枚,经历了新浪微博从起步到当前月活数亿用户的大型互联网系统的技术演进过程,现任新浪微博技术专家。我于 2008 年加入新浪,最初从事新浪 IM 的后端研发。2009 年之后开始微博 Feed 平台系统的的研发及架构工作深度参与最初若干个版本几乎所有业务的开发和架构改进2013 年后开始从事微博平台基础架构相关的研发工作。目前主要从事微博 Feed 平台的基础设施、缓存中间件、分布式存储等的研发及架构优化工作。
那么,我们为什么要学习缓存呢?有必要学习缓存吗?
随着互联网从门户/搜索时代进入移动社交时代,互联网产品也从满足用户单向浏览的需求,发展为满足用户个性信息获取及社交的需求。这就要求产品做到以用户和关系为基础,对海量数据进行实时分析计算。也就意味着,用户的每次请求,服务后端都要查询用户的个人信息、社交关系图谱,以及关系图谱涉及到的大量关联信息。还要将这些信息进行聚合、过滤、筛选和排序,最终响应给用户。如果这些信息全部从 DB 中加载,将会是一个无法忍受的漫长等待过程。
而缓存的使用,是提升系统性能、改善用户体验的唯一解决之道。
以新浪微博为例,作为移动互联网时代的一个开拓者和重量级社交分享平台,自 2009 年上线后,用户数量和微博数量都从 0 开启并高速增长,到 2019 年,日活跃用户已超 2亿每日新发 Feed 12亿每日访问量百亿级历史数据高达千亿级。同时在微博的日常服务中核心接口可用性要达到 99.99%,响应时间在 1060ms 以内,核心单个业务的数据访问量高达百万级 QPS。
所有这些数据都是靠良好的架构和不断改进的缓存体系来支撑的。
其实,作为互联网公司,只要有直接面对用户的业务,要想持续确保系统的访问性能和可用性,都需要使用缓存。因此,缓存也是后端工程师面试中一个非常重要的考察点,面试官通常会通过应聘者对缓存相关知识的理解深入程度,来判断其开发经验和学习能力。可以说,对缓存的掌握程度,在某种意义上决定了后端开发者的职业高度。
想学好缓存,需要掌握哪些知识呢?
可以看一下这张“缓存知识点全景图”。
首先要熟练掌握缓存的基础知识了解缓存常用的分类、读写模式熟悉缓存的七大经典问题及解决应对之策同时要从缓存组件的访问协议、Client 入手,熟练掌握如何访问各种缓存组件,如 Memcached、Redis、Pika 等。
其次,要尽可能深入理解缓存组件的实现方案、设计原理,了解缓存的各种特性、优势和不足,这样在缓存数据与预期不一致时,能够快速定位并解决问题。
再次还要多了解线上大中型系统是如何对缓存进行架构设计的。线上系统业务功能丰富多变跨域部署环境复杂而且热点频发用户习惯迥异。因此缓存系统在设计之初就要尽量进行良好设计规划好如何进行Hash及分布、如何保障数据的一致性、如何进行扩容和缩容。当然缓存体系也需要伴随业务发展持续演进这就需要对缓存体系进行持续的状态监控、异常报警、故障演练以确保在故障发生时能及时进行人肉或自动化运维处理并根据线上状况不断进行优化和改进。
最后,了解缓存在各种场景下的最佳实践,理解这些最佳实践背后的 Tradeoff做到知其然知其所以然以便在实际工作中能举一反三把知识和经验更好的应用到工作实践中来。
如何高效学习缓存呢?你能学到什么?
对于缓存网上学习资料很多但过于零散和重复想要系统地学习还是需要通过阅读缓存相关的书籍、论文和缓存源码或是学习一些来自实战总结的网络课程。但前面几种形式目前都需要花费较多时间。为了学员既系统又快速地获得所需知识拉勾教育推出了“300 分钟学会”系列技术课,其中“缓存“课由我来讲。
在这 300 分钟里,我将结合自己在微博平台的缓存架构经验,用 10 课时来分享:
如何更好地引入和使用缓存,自系统设计之初,就把缓存设计的关键点对号入座。
如何规避并解决缓存设计中的七大经典问题。
从协议、使用技巧、网络模型、核心数据结构、存储架构、数据处理模型、优化及改进方案等多角度全方位深入剖析互联网企业大量使用的Memcached、Redis等开源缓存组件。
教你如何利用它们构建一个分布式缓存服务体系。
最后,我将结合诸如秒杀、海量计数、微博 Feed 聚合等经典业务场景,分析如何构建相应的高可用、高性能、易扩展的缓存架构体系。
通过本课程,你可以:
系统地学习缓存之设计架构的关键知识点;
学会如何更好地使用 Memcached、Redis 等缓存组件;
对这些缓存组件的内部架构、设计原理有一个较为深入的了解,真正做到知其然更知其所以然;
学会如何根据业务需要对缓存组件进行二次开发;
搞懂如何构建一个大型的分布式缓存服务系统;
了解在当前多种热门场景下缓存服务的最佳实践;
现学现用,针对互联网大中型系统,构建出一个更好的缓存架构体系,在大幅提升系统吞吐和响应性能的同时,达到高可用、高扩展,从而可以更从容地应对海量并发请求和极端热点事件。

View File

@ -0,0 +1,66 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
01 业务数据访问性能太低怎么办?
你好我是你的缓存老师陈波欢迎进入第1课时“缓存的原理”。这节课主要讲缓存的基本思想、缓存的优点、缓存的代价三个部分。
缓存的定义
先来看下缓存的定义。
缓存最初的含义,是指用于加速 CPU 数据交换的 RAM即随机存取存储器通常这种存储器使用更昂贵但快速的静态 RAMSRAM技术用以对 DRAM进 行加速。这是一个狭义缓存的定义。
而广义缓存的定义则更宽泛,任何可以用于数据高速交换的存储介质都是缓存,可以是硬件也可以是软件。
缓存存在的意义就是通过开辟一个新的数据交换缓冲区,来解决原始数据获取代价太大的问题,让数据得到更快的访问。本课主要聚焦于广义缓存,特别是互联网产品大量使用的各种缓存组件和技术。
缓存原理
缓存的基本思想
缓存构建的基本思想是利用时间局限性原理,通过空间换时间来达到加速数据获取的目的,同时由于缓存空间的成本较高,在实际设计架构中还要考虑访问延迟和成本的权衡问题。这里面有 3 个关键点。
一是时间局限性原理,即被获取过一次的数据在未来会被多次引用,比如一条微博被一个人感兴趣并阅读后,它大概率还会被更多人阅读,当然如果变成热门微博后,会被数以百万/千万计算的更多用户查看。
二是以空间换时间,因为原始数据获取太慢,所以我们开辟一块高速独立空间,提供高效访问,来达到数据获取加速的目的。
三是性能成本 Tradeoff构建系统时希望系统的访问性能越高越好访问延迟越低小越好。但维持相同数据规模的存储及访问性能越高延迟越小成本也会越高所以在系统架构设计时你需要在系统性能和开发运行成本之间做取舍。比如左边这张图相同成本的容量SSD 硬盘容量会比内存大 1030 倍以上,但读写延迟却高 50100 倍。
缓存的优势
缓存的优势主要有以下几点:
提升访问性能
降低网络拥堵
减轻服务负载
增强可扩展性
通过前面的介绍,我们已经知道缓存存储原始数据,可以大幅提升访问性能。不过在实际业务场景中,缓存中存储的往往是需要频繁访问的中间数据甚至最终结果,这些数据相比 DB 中的原始数据小很多,这样就可以减少网络流量,降低网络拥堵,同时由于减少了解析和计算,调用方和存储服务的负载也可以大幅降低。缓存的读写性能很高,预热快,在数据访问存在性能瓶颈或遇到突发流量,系统读写压力大增时,可以快速部署上线,同时在流量稳定后,也可以随时下线,从而使系统的可扩展性大大增强。
缓存的代价
然而不幸的是,任何事情都有两面性,缓存也不例外,我们在享受缓存带来一系列好处的同时,也注定需要付出一定的代价。
首先,服务系统中引入缓存,会增加系统的复杂度。
其次,由于缓存相比原始 DB 存储的成本更高,所以系统部署及运行的费用也会更高。
最后,由于一份数据同时存在缓存和 DB 中,甚至缓存内部也会有多个数据副本,多份数据就会存在一致性问题,同时缓存体系本身也会存在可用性问题和分区的问题。这就需要我们加强对缓存原理、缓存组件以及优秀缓存体系实践的理解,从系统架构之初就对缓存进行良好设计,降低缓存引入的副作用,让缓存体系成为服务系统高效稳定运行的强力基石。
一般来讲,服务系统的全量原始数据存储在 DB 中(如 MySQL、HBase 等),所有数据的读写都可以通过 DB 操作来获取。但 DB 读写性能低、延迟高,如 MySQL 单实例的读写 QPS 通常只有千级别30006000读写平均耗时 10100ms 级别,如果一个用户请求需要查 20 个不同的数据来聚合,仅仅 DB 请求就需要数百毫秒甚至数秒。而 cache 的读写性能正好可以弥补 DB 的不足,比如 Memcached 的读写 QPS 可以达到 10100万 级别,读写平均耗时在 1ms 以下,结合并发访问技术,单个请求即便查上百条数据,也可以轻松应对。
但 cache 容量小,只能存储部分访问频繁的热数据,同时,同一份数据可能同时存在 cache 和 DB如果处理不当就会出现数据不一致的问题。所以服务系统在处理业务请求时需要对 cache 的读写方式进行适当设计,既要保证数据高效返回,又要尽量避免数据不一致等各种问题。
好了,第 1 课时的内容到这里就全部结束了,我们一起来做一个简单的回顾。首先,这一课时,你先了解了缓存的定义以及基本思想。然后,又学习了缓存的优点和代价。

View File

@ -0,0 +1,76 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
02 如何根据业务来选择缓存模式和组件?
你好,我是你的缓存老师陈波,欢迎进入第 2 课时“缓存的读写模式及分类”。这一课时我们主要学习缓存的读写模式以及缓存的分类。
缓存读写模式
如下图,业务系统读写缓存有 3 种模式:
Cache Aside旁路缓存
Read/Write Through读写穿透
Write Behind Caching异步缓存写入
Cache Aside
如上图所示Cache Aside 模式中,业务应用方对于写,是更新 DB 后,直接将 key 从 cache 中删除,然后由 DB 驱动缓存数据的更新;而对于读,是先读 cache如果 cache 没有,则读 DB同时将从 DB 中读取的数据回写到 cache。
这种模式的特点是,业务端处理所有数据访问细节,同时利用 Lazy 计算的思想,更新 DB 后,直接删除 cache 并通过 DB 更新,确保数据以 DB 结果为准,则可以大幅降低 cache 和 DB 中数据不一致的概率。
如果没有专门的存储服务,同时是对数据一致性要求比较高的业务,或者是缓存数据更新比较复杂的业务,这些情况都比较适合使用 Cache Aside 模式。如微博发展初期,不少业务采用这种模式,这些缓存数据需要通过多个原始数据进行计算后设置。在部分数据变更后,直接删除缓存。同时,使用一个 Trigger 组件,实时读取 DB 的变更日志然后重新计算并更新缓存。如果读缓存的时候Trigger 还没写入 cache则由调用方自行到 DB 加载计算并写入 cache。
Read/Write Through
如上图,对于 Cache Aside 模式,业务应用需要同时维护 cache 和 DB 两个数据存储方,过于繁琐,于是就有了 Read/Write Through 模式。在这种模式下,业务应用只关注一个存储服务即可,业务方的读写 cache 和 DB 的操作,都由存储服务代理。存储服务收到业务应用的写请求时,会首先查 cache如果数据在 cache 中不存在,则只更新 DB如果数据在 cache 中存在,则先更新 cache然后更新 DB。而存储服务收到读请求时如果命中 cache 直接返回,否则先从 DB 加载,回种到 cache 后返回响应。
这种模式的特点是,存储服务封装了所有的数据处理细节,业务应用端代码只用关注业务逻辑本身,系统的隔离性更佳。另外,进行写操作时,如果 cache 中没有数据则不更新,有缓存数据才更新,内存效率更高。
微博 Feed 的 Outbox Vector即用户最新微博列表就采用这种模式。一些粉丝较少且不活跃的用户发表微博后Vector 服务会首先查询 Vector Cache如果 cache 中没有该用户的 Outbox 记录,则不写该用户的 cache 数据,直接更新 DB 后就返回,只有 cache 中存在才会通过 CAS 指令进行更新。
Write Behind Caching
Write Behind Caching 模式与 Read/Write Through 模式类似,也由数据存储服务来管理 cache 和 DB 的读写。不同点是数据更新时Read/write Through 是同步更新 cache 和 DB而 Write Behind Caching 则是只更新缓存,不直接更新 DB而是改为异步批量的方式来更新 DB。该模式的特点是数据存储的写性能最高非常适合一些变更特别频繁的业务特别是可以合并写请求的业务比如对一些计数业务一条 Feed 被点赞 1万 次,如果更新 1万 次 DB 代价很大,而合并成一次请求直接加 1万则是一个非常轻量的操作。但这种模型有个显著的缺点即数据的一致性变差甚至在一些极端场景下可能会丢失数据。比如系统 Crash、机器宕机时如果有数据还没保存到 DB则会存在丢失的风险。所以这种读写模式适合变更频率特别高但对一致性要求不太高的业务这样写操作可以异步批量写入 DB减小 DB 压力。
讲到这里,缓存的三种读写模式讲完了,你可以看到三种模式各有优劣,不存在最佳模式。实际上,我们也不可能设计出一个最佳的完美模式出来,如同前面讲到的空间换时间、访问延迟换低成本一样,高性能和强一致性从来都是有冲突的,系统设计从来就是取舍,随处需要 trade-off。这个思想会贯穿整个 cache 课程,这也许是我们学习这个课程的另外一个收获,即如何根据业务场景,更好的做 trade-off从而设计出更好的服务系统。
缓存分类及常用缓存介绍
前面介绍了缓存的基本思想、优势、代价以及读写模式,接下来一起看下互联网企业常用的缓存有哪些分类。
按宿主层次分类
按宿主层次分类的话,缓存一般可以分为本地 Cache、进程间 Cache 和远程 Cache。
本地 Cache 是指业务进程内的缓存,这类缓存由于在业务系统进程内,所以读写性能超高且无任何网络开销,但不足是会随着业务系统重启而丢失。
进程间 Cache 是本机独立运行的缓存,这类缓存读写性能较高,不会随着业务系统重启丢数据,并且可以大幅减少网络开销,但不足是业务系统和缓存都在相同宿主机,运维复杂,且存在资源竞争。
远程 Cache 是指跨机器部署的缓存,这类缓存因为独立设备部署,容量大且易扩展,在互联网企业使用最广泛。不过远程缓存需要跨机访问,在高读写压力下,带宽容易成为瓶颈。
本地 Cache 的缓存组件有 Ehcache、Guava Cache 等,开发者自己也可以用 Map、Set 等轻松构建一个自己专用的本地 Cache。进程间 Cache 和远程 Cache 的缓存组件相同,只是部署位置的差异罢了,这类缓存组件有 Memcached、Redis、Pika 等。
按存储介质分类
还有一种常见的分类方式是按存储介质来分,这样可以分为内存型缓存和持久化型缓存。
内存型缓存将数据存储在内存,读写性能很高,但缓存系统重启或 Crash 后,内存数据会丢失。
持久化型缓存将数据存储到 SSD/Fusion-IO 硬盘中,相同成本下,这种缓存的容量会比内存型缓存大 1 个数量级以上,而且数据会持久化落地,重启不丢失,但读写性能相对低 12 个数量级。Memcached 是典型的内存型缓存,而 Pika 以及其他基于 RocksDB 开发的缓存组件等则属于持久化型缓存。

View File

@ -0,0 +1,84 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
06 Hot Key和Big Key引发的问题怎么应对
你好我是你的缓存老师陈波欢迎进入第6课时“缓存特殊 key 相关的经典问题”。
Hot key
问题描述
第六个经典问题是 Hot key。对于大多数互联网系统数据是分冷热的。比如最近的新闻、新发表的微博被访问的频率最高而比较久远的之前的新闻、微博被访问的频率就会小很多。而在突发事件发生时大量用户同时去访问这个突发热点信息访问这个 Hot key这个突发热点信息所在的缓存节点就很容易出现过载和卡顿现象甚至会被 Crash。
原因分析
Hot key 引发缓存系统异常,主要是因为突发热门事件发生时,超大量的请求访问热点事件对应的 key比如微博中数十万、数百万的用户同时去吃一个新瓜。数十万的访问请求同一个 key流量集中打在一个缓存节点机器这个缓存机器很容易被打到物理网卡、带宽、CPU 的极限,从而导致缓存访问变慢、卡顿。
业务场景
引发 Hot key 的业务场景很多比如明星结婚、离婚、出轨这种特殊突发事件比如奥运、春节这些重大活动或节日还比如秒杀、双12、618 等线上促销活动,都很容易出现 Hot key 的情况。
解决方案
要解决这种极热 key 的问题,首先要找出这些 Hot key 来。对于重要节假日、线上促销活动、集中推送这些提前已知的事情,可以提前评估出可能的热 key 来。而对于突发事件,无法提前评估,可以通过 Spark对应流任务进行实时分析及时发现新发布的热点 key。而对于之前已发出的事情逐步发酵成为热 key 的,则可以通过 Hadoop 对批处理任务离线计算,找出最近历史数据中的高频热 key。
找到热 key 后,就有很多解决办法了。首先可以将这些热 key 进行分散处理,比如一个热 key 名字叫 hotkey可以被分散为 hotkey#1、hotkey#2、hotkey#3……hotkey#n,这 n 个 key 分散存在多个缓存节点,然后 client 端请求时,随机访问其中某个后缀的 hotkey这样就可以把热 key 的请求打散,避免一个缓存节点过载,如下图所示。
其次,也可以 key 的名字不变,对缓存提前进行多副本+多级结合的缓存架构设计。
再次,如果热 key 较多,还可以通过监控体系对缓存的 SLA 实时监控,通过快速扩容来减少热 key 的冲击。
最后,业务端还可以使用本地缓存,将这些热 key 记录在本地缓存,来减少对远程缓存的冲击。
Big key
问题描述
最后一个经典问题是 Big key也就是大 Key 的问题。大 key是指在缓存访问时部分 Key 的 Value 过大,读写、加载易超时的现象。
原因分析
造成这些大 key 慢查询的原因很多。如果这些大 key 占总体数据的比例很小,存 Mc对应的 slab 较少导致很容易被频繁剔除DB 反复加载,从而导致查询较慢。如果业务中这种大 key 很多,而这种 key 被大量访问,缓存组件的网卡、带宽很容易被打满,也会导致较多的大 key 慢查询。另外,如果大 key 缓存的字段较多,每个字段的变更都会引发对这个缓存数据的变更,同时这些 key 也会被频繁地读取,读写相互影响,也会导致慢查现象。最后,大 key 一旦被缓存淘汰DB 加载可能需要花费很多时间,这也会导致大 key 查询慢的问题。
业务场景
大 key 的业务场景也比较常见。比如互联网系统中需要保存用户最新 1万 个粉丝的业务,比如一个用户个人信息缓存,包括基本资料、关系图谱计数、发 feed 统计等。微博的 feed 内容缓存也很容易出现,一般用户微博在 140 字以内,但很多用户也会发表 1千 字甚至更长的微博内容,这些长微博也就成了大 key如下图。
解决方案
对于大 key给出 3 种解决方案。
第一种方案,如果数据存在 Mc 中,可以设计一个缓存阀值,当 value 的长度超过阀值,则对内容启用压缩,让 KV 尽量保持小的 size其次评估大 key 所占的比例,在 Mc 启动之初,就立即预写足够数据的大 key让 Mc 预先分配足够多的 trunk size 较大的 slab。确保后面系统运行时大 key 有足够的空间来进行缓存。
第二种方案,如果数据存在 Redis 中,比如业务数据存 set 格式,大 key 对应的 set 结构有几千几万个元素,这种写入 Redis 时会消耗很长的时间,导致 Redis 卡顿。此时,可以扩展新的数据结构,同时让 client 在这些大 key 写缓存之前,进行序列化构建,然后通过 restore 一次性写入,如下图所示。
第三种方案时,如下图所示,将大 key 分拆为多个 key尽量减少大 key 的存在。同时由于大 key 一旦穿透到 DB加载耗时很大所以可以对这些大 key 进行特殊照顾,比如设置较长的过期时间,比如缓存内部在淘汰 key 时,同等条件下,尽量不淘汰这些大 key。
至此,本课时缓存的 7 大经典问题全部讲完。
我们要认识到,对于互联网系统,由于实际业务场景复杂,数据量、访问量巨大,需要提前规避缓存使用中的各种坑。你可以通过提前熟悉 Cache 的经典问题,提前构建防御措施, 避免大量 key 同时失效,避免不存在 key 访问的穿透,减少大 key、热 key 的缓存失效,对热 key 进行分流。你可以采取一系列措施,让访问尽量命中缓存,同时保持数据的一致性。另外,你还可以结合业务模型,提前规划 cache 系统的 SLA如 QPS、响应分布、平均耗时等实施监控以方便运维及时应对。在遇到部分节点异常或者遇到突发流量、极端事件时也能通过分池分层策略、key 分拆等策略,避免故障发生。
最终,你能在各种复杂场景下,面对高并发、海量访问,面对突发事件和洪峰流量,面对各种网络或机器硬件故障,都能保持服务的高性能和高可用。

View File

@ -0,0 +1,61 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
07 MC为何是应用最广泛的缓存组件
好,我是你的缓存老师陈波,欢迎你进入第 7 课时“Memcached 原理及特性”的学习。
众所周知用户体验可以说是互联网企业最看重的指标而在用户体验中请求响应速度首当其冲。因此互联网系统对性能的追求是永无止境的。性能争霸缓存为王Memcached作为互联网系统使用最广泛、影响最大的标配缓存组件可以说的上是王中之王了。
本课时将讲解 Memcached 的原理及特性,系统架构,还会重点讲解 Memcached 的网络模型、状态机,最后还会涉及到 Memcached 命令处理的整个流程。
Memcached 原理及特性
首先来看 Memcached 的原理及特性。
原理
Memcached 是一个开源的、高性能的分布式 key/value 内存缓存系统。它以 key/value 键值对的方式存储数据,是一个键值类型的 NoSQL 组件。
NoSQL 即 Not SQL泛指非关系型数据存储。NoSQL 是通过聚合模型来进行数据处理的。其聚合模型主要分为key/value 键值对、列族、图形等几种方式。其中 key/value 键值类似我们平常使用的 map只能通过 key 来进行查找和变更操作。我们使用的 Memcached、Redis 等都是 key/value 类型的 NoSQL 存储组件。
Memcached 简称 Mc是一个典型的内存型缓存组件这就意味着Mc 一旦重启就会丢失所有的数据。如下图所示Mc 组件之间相互不通信,完全由 client 对 key 进行 Hash 后分布和协同。Mc 采用多线程处理请求,由一个主线程和任意多个工作线程协作,从而充分利用多核,提升 IO 效率。
slab 机制
接下来介绍 Mc 的 slab 机制。
Mc 并不是将所有数据放在一起来进行管理的,而是将内存划分为一系列相同大小的 slab 空间后,每个 slab 只管理一定范围内的数据存储。也就是说 Mc 内部采用 slab 机制来管理内存分配。Mc 内的内存分配以 slab 为单位,默认情况下一个 slab 是 1MB可以通过 -I 参数在启动时指定其他数值。
slab 空间内部,会被进一步划分为一系列固定大小的 chunk。每个 chunk 内部存储一个 Item利用 Item 结构存储数据。因为 chunk 大小固定,而 key/value 数据的大小随机。所以Item存储完 key/value 数据后一般还会有多余的空间这个多余的空间就被浪费了。为了提升内存的使用效率chunk size 就不能太大,而要尽量选择与 key/value size 接近的 ,从而减少 chunk 内浪费的空间。
Mc 在分配内存时,先将内存按固定大小划分成 slab然后再将不同 slab 分拆出固定 size 的 chunk。虽然 slab 内的 chunk 大小相同,但不同 slab 的 chunk size 并不同Mc 会按照一个固定比例,使划分的 chunk size 逐步增大,从而满足不同大小 key/value 存储的需要。
如下图,一组具有相同 chunk size 的所有 slab就组成一个 slabclass。不同 slabclass 的 chunk size 按递增因子一次增加。Mc 就通过 slabclass 来管理一组 slab 内的存储空间的。每个 slabclass 内部有一个 freelist ,包含这组 slab 里所有空闲的 chunk当需要存储数据时从这个 freelist 里面快速分配一个 chunk 做存储空间。当 Item 数据淘汰剔除时,这个 Item 所在的 chunk 又被回收至这个 freelist。
Mc 在通过 slab 机制管理内存分配时,实际 key/value 是存在 Item 结构中,所以对 key/value 的存储空间分配就转换为对 Item 的分配。而 Item 空间的分配有 2 种方式,如果 Mc 有空闲空间,则从 slabclass 的 freelist 分配;如果没有空闲空间,则从对应 slabclass id 对应的 LRU 中剔除一个 Item来复用这个 Item 的空间。
在查找或变更一个 key 时,首先要定位这个 key 所在的存储位置。Mc 是通过哈希表 Hashtable 来定位 key 的。Hashtable 可以看作是一个内存空间连续的大数组,而这个大数据的每一个槽位对应一个 key 的 Hash 值,这个槽位也称 bucket。由于不同 key 的 Hash 值可能相同,所以 Mc 在 Hashtable 的每个捅内部再用一个单向链表,来解决 Hash 冲突的问题。
Mc 内部是通过 LRU 来管理存储 Item 数据的,当内存不足时,会从 LRU 队尾中剔除一个过期或最不活跃的 key供新的 Item 使用。
特性
讲完 slab 机制我们来学习 Mc 的特性。
Mc 最大的特性是高性能,单节点压测性能能达到百万级的 QPS。
其次因为 Mc 的访问协议很简单,只有 get/set/cas/touch/gat/stats 等有限的几个命令。Mc 的访问协议简单,跟它的存储结构也有关系。
Mc 存储结构很简单,只存储简单的 key/value 键值对,而且对 value 直接以二进制方式存储,不识别内部存储结构,所以有限几个指令就可以满足操作需要。
Mc 完全基于内存操作,在系统运行期间,在有新 key 写进来时,如果没有空闲内存分配,就会对最不活跃的 key 进行 eviction 剔除操作。
最后Mc 服务节点运行也特别简单,不同 Mc 节点之间互不通信,由 client 自行负责管理数据分布。

View File

@ -0,0 +1,61 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
08 MC系统架构是如何布局的
你好,我是你的缓存老师陈波,欢迎你进入第 8 课时“Memcached 系统架构”的学习。
系统架构
我们来看一下 Mc 的系统架构。
如下图所示Mc 的系统架构主要包括网络处理模块、多线程处理模块、哈希表、LRU、slab 内存分配模块 5 部分。Mc 基于 Libevent 实现了网络处理模块,通过多线程并发处理用户请求;基于哈希表对 key 进行快速定位,基于 LRU 来管理冷数据的剔除淘汰,基于 slab 机制进行快速的内存分配及存储。
系统架构
Mc 基于 Libevent 开发实现了多线程网络模型。Mc 的多线程网络模型分为主线程、工作线程。这些线程通过多路复用 IO 来进行网络 IO 接入以及读写处理。在 Linux 下,通常使用 epoll。通过多路复用 IO特别是 epoll 的使用Mc 线程无须遍历整个被侦听的描述符集,只要在被通知后遍历 Ready 队列的描述符集合就 OK 了。这些描述符是在各项准备工作完成之后,才被内核 IO 事件异步通知。也就是说只在连接做好准备后系统才会进行事件通知Mc 才会进行 I/O 操作。这样就不会发生阻塞,使 Mc 在支持高并发的同时,拥有非常高的 IO 吞吐效率。
Mc 除了用于 IO 的主线程和工作线程外,还用于多个辅助线程,如 Item 爬虫线程、LRU 维护线程、哈希表维护线程等通过多线程并发工作Mc 可以充分利用机器的多个核心,实现很好的网络 IO 性能和数据处理能力。
Mc 通过哈希表即 Hashtable 来快速定位 key。数据存储时数据 Item 结构在存入 slab 中的 chunk 后,也会被存放到 Hashtable 中。同时Mc 的哈希表会在每个桶,通过 Item 记录一个单向链表,以此来解决不同 key 在哈希表中的 Hash 冲突问题。 当需要查找给定 key 的 Item 时,首先计算 key 的 Hash 值,然后对哈希表中与 Hash 值对应的 bucket 中进行搜索,通过轮询 bucket 里的单向链表,找到该 key 对应的 Item 指针,这样就找到了 key 对应的存储 Item如下图所示。
正常情况下Mc 对哈希表的插入、查找操作都是在主表中进行的。当表中 Item 数量大于哈希表 bucket 节点数的 1.5 倍时就对哈希表进行扩容。如下图所示扩容时Mc 内部使用两张 Hashtable一个主哈希表 primary_hashtable一个是旧哈希表 old_hashtable。当扩容开始时原来的主哈希表就成为旧哈希表而新分配一个 2 倍容量的哈希表作为新的主表。扩容过程中,维护线程会将旧表的 Item 指针,逐步复制插入到新主哈希表。迁移过程中,根据迁移位置,用户请求会同时查旧表和新的主表,当数据全部迁移完成,所有的操作就重新回到主表中进行。
LRU 机制
Mc 主要通过 LRU 机制,来进行冷数据淘汰的。自 1.4.24 版本之后Mc 不断优化 LRU 算法,当前 Mc 版本已默认启用分段 LRU 了。在启用分段 LRU 之前,每个 slabclass id 只对应一个 COLD LRU在内存不足时会直接从 COLD LRU 剔除数据。而在启用分段 LRU 之后,每个 slabclass id 就有 TEMP、HOT、WARM 和 COLD 四个 LRU。
如下图所示TEMP LRU 中 Item 剩余过期时间通常很短,默认是 61 秒以内。该列队中的 Item 永远不会发生在队列内搬运,也不会迁移到其他队列。在插入新 key/value 时,如果 key 的剩余过期时间小于 61 秒,则直接进入 TEMP LRU。后面在必要时直接进行过期即可。这样避免了锁竞争性能也更高。
对于 HOT LRU内部不搬运当队列满时如果队尾 Item 是 Active 状态,即被访问过,那么会迁移到 WARM 队列,否则迁移到 COLD 队列。
对于 WARM LRU如果队列的 Item 被再次访问,就搬到队首,否则迁移到 COLD 队列。
对于 COLD LRU存放的是最不活跃的 Item一旦内存满了队尾的 Item 会被剔除。如果 COLD LRU 里的 Item 被再次访问,会迁移到 WARM LRU。
slab 分配机制
一般应用系统的内存分配是直接采用 malloc 和 free 来进行分配及回收的。长时间运行后内存碎片越来越多严重增加系统内存管理器的负担。碎片的不断产生不仅导致大量的内存浪费而且碎片整理越来越复杂会导致内存分配越来越慢进而导致系统分配速度和存储效率越来越差。Mc 的 slab 分配机制的出现,碎片问题迎刃而解。下面我们来先简单了解一下 Mc 的 slab 分配机制。
Mc 通过 slab 机制来分配管理内存的如下图所示。可以说slab 分配机制的使用,是 Mc 分配及存储高性能的关键所在。在 Mc 启动时,会创建 64 个 slabclass但索引为 0 的 slabclass 做 slab 重新分配之用,基本不参与其他 slabclass 的日常分配活动。每个 slabclass 会根据需要不断分配默认大小为 1MB 的 slab。
每个 slab 又被分为相同大小的 chunk。chunk 就是 Mc 存储数据的基本存储单位。slabclass 1 的 chunk size 最小,默认最小 chunk 的大小是 102 字节,后续的 slabclass 会按照增长因子逐步增大 chunk size具体数值会进一步对 8 取整。Mc 默认的增长因子是 1.25,启动时可以通过 -f 将增长因子设为其他值。比如采用默认值slabclass 1 的 chunk size 是 102slabclass 2 的 chunk size 是 102×1.25,再对 8 取整后是 128。
Mc slab 中的 chunk 中通过 Item 结构存 key/value 键值对Item 结构体的头部存链表的指针、flag、过期时间等然后存 key 及 value。一般情况下Item 并不会将 chunk 填满,但由于每个 key/value 在存储时,都会根据 kev/value size选择最接近的 slabclass所以 chunk 浪费的字节非常有限,基本可以忽略。
每次新分配一个 slab 后,会将 slab 空间等分成相同 size 的 chunk这些 chunk 会被加入到 slabclass 的 freelist 中,在需要时进行分配。分配出去的 chunk 存储 Item 数据,在过期被剔除后,会再次进入 freelist供后续使用。

View File

@ -0,0 +1,65 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
12 为何MC能长期维持高性能读写
你好,我是你的缓存课老师陈波,欢迎进入第 12 课时“Memcached 内存管理 slab 机制”的学习。
内存管理 slab 机制
讲完淘汰策略,我们接下来学习内存管理 slab 机制。
Mc 内存分配采用 slab 机制slab 机制可以规避内存碎片,是 Mc 能持续高性能进行数据读写的关键。
slabclass
Mc 的 slab 机制是通过 slabclass 来进行运作的如下图所示。Mc 在启动时,会构建长度为 64 的 slabclass 数组,其中 0 号 slabclass 用于 slab 的重新分配1~63 号 slabclass 存储数据 Item。存储数据的每个 slabclass都会记录本 slabclass 的 chunk size同时不同 slabclass 的 chunk size 会按递增因子增加,最后一个 slabclass即 63 号 slabclass的 chunk size 会直接设为最大的 chunk size默认是 0.5MB。每个 slabclass 在没有空闲的 chunk 时Mc 就会为其分配一个默认大小为 1MB 的 slab同时按照本 slabclass 的 chunk size 进行拆分,这些分拆出来的 chunk 会按 Item 结构体进行初始化,然后记录到 slabclass 的 freelist 链表中。当有 key/value 要存储在本 slabclass 时,就从 freelist 分配一个 Item供其使用。同时如果 Item 过期了,或被 flush_all 失效了,或在内存不够时被强项剔除了,也会在适当时刻,重新被回收到 freelist以供后续分配使用。
存储 slab 分配
如下图所示Mc 的存储空间分配是以 slab 为单位的,每个 slab 的默认大小时 1MB。因此在存数据时Mc 的内存最小分配单位是 1MB分配了这个 1MB 的 slab 后,才会进一步按所在 slabclass 的chunk size 进行细分,分拆出的相同 size 的 chunk。这个 chunk 用来存放 Item 数据Item 数据包括 Item 结构体字段,以及 key/value。
一般来讲Item 结构体及 key/value 不会填满 chunk会存在少量字节的浪费但这个浪费的字节很少基本可以忽略。Mc 中slab 一旦分配,就不会再被回收,但会根据运行状况,重新在不同 slabclass 之间进行分配。
当一个 slabclass 没有空闲 chunk而新数据插入时就会对其尝试增加一个新的 slab。slabclass 增加新 slab 时,首先会从 0 号全局 slabclass 中复用一个之前分配的 slab如果 0 号 slabclass 没有 slab则会尝试从内存堆空间直接分配一个 slab。如果 0 号全局 slabclass 没有空闲 slab而且 Mc 内存分配已经达到 Mc 设定的上限值,就说明此时没有可重新分配的 slab分配新 slab 失败,直接返回。
当然,虽然 slabclass 分配 slab 失败,但并不意味着 Item分配会失败前面已经讲到可以通过同步 LRU 淘汰,回收之前分配出去的 Item供新的存储请求使用。
Item
Mc 中slabclass 中的 chunk 会首先用 Item 结构体进行初始化,然后存到 freelist 链表中,待需要分配给数据存储时,再从 freelist 中取出,存入 key/value以及各种辅助属性然后再存到 LRU 链表及 Hashtable 中如下图所示。Item 结构体,首先有两个 prev、next 指针,在分配给待存储数据之前,这两个指针用来串联 freelist 链表,在分配之后,则用来串联所在的 LRU 链表。接下来是一个 h_next 指针用来在分配之后串联哈希表的桶单向链表。Item 结构体还存储了过期时间、所属 slabclass idkey 长度、cas 唯一 id 值等,最后在 Item 结构体尾部,存储了 key、flag、value 长度,以及 value block 数据。在 value 之后的 chunk 空间就被浪费掉了。Item 在空闲期间,即初始分配时以及被回收后,都被 freelist 管理。在存储期间被哈希表、LRU 管理。
存储 Item 分配
Mc 采用 slab 机制管理分配内存,采用 Item 结构存储 key/value因此对存储 key/value 的内存分配,就转换为对 Item 的分配。分配 Item 空间时,会进行 10 次大循环,直到分配到 Item 空间才会提前返回。如果循环了 10 次,还没有分配到 Item 空间,则存储失败,返回一个 SERVER_ERROR 响应。
在分配过程中,首先,如果 slabclass 的 freelist 有空间,则直接分配。否则,尝试分配一个新的 slab新 slab 依次尝试从全局 slab 池(即 0 号 slabclass中复用一个空闲 slab如果全局 slab 池没有 slab则尝试从内存直接分配。分配新 slab 成功后,会按照 slabclass 记录的 chunk size 对 slab 进行分拆,并将分拆出来的 chunk 按 Item 结构初始化后记录到 freelist。如果全局 slab 池为空,且 Mc 内存分配已经达到设定的上限,则走新增 slab 的路径失败,转而进行 5 次小循环,尝试从 COLD LRU 回收过期 key如果没有过期则直接强制剔除队尾的一个正常 key。如果该 slabclass 的 COLD LRU 没有 Item则对其 HOT LRU 进行处理,对 HOT 链表队尾 Item 进行回收或者迁移,以方便在下次循环中找到一个可用的 Item 空间。
数据存储机理
讲完 Mc 的哈希表定位、LRU 淘汰、slab 内存分配,接下来我们来看看 Mc 中 key/value 数据的存储机理,通过对数据存储以及维护过程的分析,来把 Mc 的核心模块进行打通和关联。
首先来看 Mc 如何通过 slab 机制将数据写入预分配的存储空间。
如下图所示,当需要存储 key/value 数据时,首先根据 key/value size以及 Item 结构体的 size计算出存储这个 key/value 需要的字节数,然后根据这个字节数选择一个能存储的 chunk size 最小的 slabclass。再从这个 slabclass 的 freelist 分配一个空闲的 chunk 给这个 key/value 使用。如果 freelist 为空,首先尝试为该 slabclass 新分配一个 slab如果 slab 分配成功,则将 slab 按 size 分拆出一些 chunk通过 Item 结构初始化后填充到 freelist。如果 slab 分配失败,则通过 LRU 淘汰失效的 Item 或强行剔除一个正常的 Item然后这些 Item 也会填充到 freelist。当 freelist 有 Item 时,即可分配给 key/value。这个过程会重试 10 次,直到分配到 Item 位置。一般情况下Item 分配总会成功,极小概率情况下也会分配失败,如果分配失败,则会回复一个 SERVER_ERROR 响应,通知 client 存储失败。分配到一个空闲的 Item 后,就会往这个 Item 空间写入过期时间、flag、slabclass id、key以及 value 等。对于 set 指令,如果这个 key 还有一个旧值,在存入新 value 之前,还会先将这个旧值删除掉。
当对 key/value 分配 Item 成功,并写入数据后,接下来就会将这个 Item 存入哈希表。因为Mc 哈希表存在迁移的情况,所以对于正常场景,直接存入主哈希表。在哈希表迁移期间,需要根据迁移位置,选择存入主哈希表还是旧哈希表。存入哈希表之后,这个 key 就可以快速定位了。然后这个 Item 还会被存入 LRUMc 会根据这个 key 的过期时间进行判断,如果过期时间小于 61s则存入 TEMP LRU否则存入 HOT LRU。
至此,这个 key/value 就被正确地存入 Mc 了,数据内容写入 slabclass 中某个 slab 的 chunk 位置,该 chunk 用 Item 结构填充,这个 Item 会被同时记录到 Hashtable 和 LRU如下图所示。通过 Hashtable 可以快速定位查到这个 key而 LRU 则用于 Item 生命周期的日常维护。
Mc 对 Item 生命周期的日常维护,包括异步维护和同步维护。异步维护是通过 LRU 维护线程来进行的,整个过程不影响 client 的正常请求,在 LRU 维护线程内,对过期、失效 key 进行回收,并对 4 个 LRU 进行链表内搬运和链表间迁移。这是 Item 生命周期管理的主要形式。同步维护,由工作线程在处理请求命令时进行。工作线程在处理 delete 指令时,会直接将 key/value 进行删除。在存储新 key/value 时,如果分配失败,会进行失效的 key 回收,或者强行剔除正常的 Item。这些 Item 被回收后,会进入到 slabclass 的 freelist 进行重复使用。

View File

@ -0,0 +1,107 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
13 如何完整学习MC协议及优化client访问
你好,我是你的缓存课老师陈波,欢迎进入第 13 课时“Memcached 协议分析”的学习。
协议分析
异常错误响应
接下来,我们来完整学习 Mc 协议。在学习 Mc 协议之前,首先来看看 Mc 处理协议指令如果发现异常如何进行异常错误响应的。Mc 在处理所有 client 端指令时,如果遇到错误,就会返回 3 种错误信息中的一种。
第一种错误是协议错误一个”ERROR\r\n”的字符串。表明 client 发送了一个非法命令。
第二种错误是 client 错误格式为”CLIENT_ERROR \r\n”。这个错误信息表明 client 发送的协议命令格式有误,比如少了字段、多了非法字段等。
第三种错误是”SERVER_ERROR \r\n”。这个错误信息表明 Mc server 端,在处理命令时出现的错误。比如在给 key/value 分配 Item 空间失败后会返回”SERVER_ERROR out of memory storing object” 错误信息。
存储协议命令
现在再来看看 Mc 的存储协议。Mc 的存储协议命令不多,只有 6 个。
Mc 存储指令分 2 行。第一行是报文首部,第二行是 value 的 data block 块。这两部分用 \r\n 来进行分割和收尾。
存储类指令的报文首行分 2 种格式,其中一种是在 cmd 存储指令,后面跟 key、flags、expiretime、value 字节数,以及一个可选的 noreply。
其中 flags 是用户自己设计的一个特殊含义数字Mc 对 flag 只存储而不进行任何额外解析处理expiretime 是 key 的过期时间value 字节数是 value block 块的字节长度,而带上 noreply 是指 Mc 处理完后静默处理,不返回任何响应给 client。
这种 cmd 指令包括我们最常用的 set 指令,另外还包括 add、replace、append、reppend ,总共 5 个指令:
Set 命令用于存储一个 key/value
Add 命令是在当 key 不存在时,才存储这个 key/value
Replace 命令,是当 key 存在时,才存储这个 key/value
Append 命令,是当 key 存在时,追加 data 到 value 的尾部;
Prepend 命令,是当 key 存在时,将 data 加到 value 的头部。
另外一种存储协议指令,主要格式和字段与前一种基本相同,只是多了一个 cas unique id这种格式只有 cas 指令使用。cas 指令是指只有当这个 key 存在,且从本 client 获取以来没有其他任何人修改过时才进行修改。cas 的英文含义是 compare and set即比较成功后设置的意思。
存储命令响应
Mc 在响应存储协议时如果遇到错误就返回前面说的3种错误信息中的一种。否则就会返回如下 4 种正常的响应”STORED\r\n”、”EXISTS\r\n”、”NOT_STORED\r\n”、”NOT_FOUND\r\n“。
其中stored 表明存储修改成功。NOT_STORED 表明数据没有存储成功,但并不是遇到错误或异常。这个响应一般表明 add 或 replace 等指令,前置条件不满足时,比如 add这个 key 已经存在 Mc就会 add 新 key 失败。replace 时, key 不存在,也无法 replace 成功。EXISTS 表明待 cas 的key 已经被修改过了,而 NOT_FOUND 是指待 cas 的 key 在 Mc 中不存在。
Mc 对存储命令的请求及响应协议,可以参考下面的思维导图来有一个完整的印象。
获取命令
Mc 的获取协议,只有 get、gets 两种指令,如下图所示。格式为 get/gets 后,跟随若干个 key然后 \r\n 结束请求命令。get 指令只获取 key 的 flag 及 valuegets 会额外多获取一个 cas unique id值。gets 主要是为 cas 指令服务的。
获取命令的响应,就是 value 字串,后面跟上 key、flag、value 字节数,以及 value 的 data block 块。最后跟一个 END\r\n 表明所有存在的 key/value 已经返回,如果没有返回的 key则表明这个 key 在 Mc 中不存在。
其他指令
Mc 的其他协议指令包括 delete、incr、decr、touch、gat、gats、slabs、lru、stats 这 9 种指令。
其中 delete 用于删除一个 key。
incr/decr 用于对一个无符号长整型数字进行加或减。
touch、gat、gats 是 Mc 后来增加的指令,都可以用来修改 key 的过期时间。不同点是 touch 只修改 key 的过期时间,不获取 key对应的value。
而 gat、gats 指令,不仅会修改 key 的过期时间,还会获取 key 对应的 flag 和 value 数据。gats 同 gets还会额外获取 cas 唯一 id 值。
Slabs reassign 用于在 Mc 内存达到设定上限后,将 slab 重新在不同的 slabclass 之间分配。这样可以规避 Mc 启动后自动分配而产生随机性,使特殊 size 的数据也得到较好的命中率。Slabs automove 是一个开关指令,当打开时,就允许 Mc 后台线程自行决定何时将 slab 在slabclass 之间重新分配。
lru 指令用于 Mc LRU 的设置和调优。比如 LRU tune 用于设置 HOT、WARM LRU 的内存占比。LRU mode 用来设置 Mc 只使用 COLD LRU还是使用新版的 4 个 LRU 的新策略。LRU TEMP_TTL 用来设置 Mc 的 TEMP LRU 的TTL值默认是 61s小于这个 TMEP_TTL 的 key会被插入到 TEMP LRU。
Stats 用于获取 Mc 的各种统计数据。Stats 后面可以跟 statistics、slabs、size 等参数,来进一步获取更多不同的详细统计。
Client 使用
Mc 在互联网企业应用广泛,热门语言基本都有 Mc client 的实现。以 Java 语言为例,互联网业界广泛使用的有 Memcached-Java-Client、SpyMemcached、Xmemcached 等。
Memcached-Java-Client 推出时间早10 年前就被广泛使用,这个 client 性能一般,但足够稳定,很多互联网企业至今仍在使用。不过这个 client 几年前就停止了更新。
SpyMemcached 出现的比较晚,性能较好,但高并发访问场景,稳定性欠缺。近几年
变更很少,基本停止了更新。
Xmemcached 性能较好综合表现最佳。而且社区活跃度高近些年也一直在持续更新中。Java 新项目启动,推荐使用 Xmemcached。
在使用 Mc client 时,有一些通用性的调优及改进方案。比如,如果读写的 key/value 较大,需要设置更大的缓冲 buf以提高性能。在一些业务场景中需要启用 TCP_NODELAY避免 40ms 的延迟问题。同时,如果存取的 key/value size 较大可以设置一个压缩阀值超过阀值就对value 进行压缩算法,减少读写及存储的空间。
为了避免缓存雪崩,并更好地应对极热 key 及洪水流量的问题,还可以对 Mc client 进行封装,加入多副本、多层级策略,使 Mc 缓存系统在任何场景下,都可做到高可用、高性能。
讲到这里Mc 的核心知识点就基本讲完了,知识点结构图如下所示。
回顾一下最近几节课的内容。首先,学习了 Mc 的系统架构,学习了 Mc 基于 libevent 的网络模型,学习了 Mc 的多线程处理,包括主线程、工作线程如何进行网络 IO 协调及处理,学习了 Mc 的状态机。然后,继续学习了 Mc 用于定位 key 的哈希表,学习了用于数据生命周期管理的 LRU还学习 slab 分配机制,以及 Mc 数据的存储机理。最后,还完整学习了 Mc的协议了解了以 Java 语言为例的 3 种 Mc client以及 Mc client 在线上使用过程中,如何进行调优及改进。
根据下面 Mc 协议的思维导图,查看自己是否对所有指令都有理解,可以结合 Mc 的协议文档,启动一个 Mc 实例,进行各个命令的实际操练。