<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:atom="http://www.w3.org/2005/Atom">
	<channel>
		<title>Eric.Yao</title>
		<description>Errors should never pass silently.</description>
		<link>https://sarkerson.github.io/</link>
		<atom:link href="https://sarkerson.github.io/feed.xml" rel="self" type="application/rss+xml" />
		<pubDate>Wed, 07 Oct 2026 07:24:31 +0000</pubDate>
		<lastBuildDate>Wed, 07 Oct 2026 07:24:31 +0000</lastBuildDate>
		<generator>Jekyll v3.10.0</generator>
    
		<item>
			<title>个人开发：从需求到上线的工具选型</title>
			<description>&lt;p&gt;准备做一个小产品时，最容易先收集一长串工具：写需求的、生成界面的、编程的、管理 Agent 的、部署的、做数据分析的。账户开了不少，真正能让用户从头到尾完成的功能却还没有。&lt;/p&gt;

&lt;p&gt;我更愿意从一条交付链开始：&lt;strong&gt;描述一个明确需求，做出功能，验证它正确，把它部署出去，再根据真实使用继续修改。&lt;/strong&gt; 工具沿着这条链补位。每增加一个服务，都应说清它解决了哪个已经出现的问题，以及维护它需要付出什么。&lt;/p&gt;

&lt;p&gt;本文面向有一定编程基础、制作轻量 Web 应用或 AI 产品的个人开发者。以“上传账单，生成可下载的汇总报告”为贯穿案例，精选必要环节；工具能力以官方资料为依据，选型是工程建议，没有进行跨产品性能排名。动态资料核对日期为 2026-10-07。&lt;/p&gt;

&lt;h2 id=&quot;1-先把完成写清楚&quot;&gt;1. 先把“完成”写清楚&lt;/h2&gt;

&lt;p&gt;在打开编程工具之前，先用已有笔记工具记录三个问题：谁使用，输入什么，交付什么。对账单报告而言，“识别很智能”无法验收；“上传指定格式文件，核对金额，下载汇总，错误时说明原因”才可以变成明确任务。&lt;/p&gt;

&lt;p&gt;再补上两类约束：哪些行为不能发生，失败时用户怎么办。例如用户 A 不能读取用户 B 的账单，识别失败不能静默生成错误金额，重复点击不应产生两份收费任务。&lt;/p&gt;

&lt;p&gt;界面尚不清楚时，可以从 v0 或 Lovable 中选一个尝试原型。它们能帮助把描述变成可修改的应用结构，Lovable 也提供 GitHub 同步路径。&lt;a href=&quot;https://v0.app/docs&quot;&gt;1&lt;/a&gt;&lt;a href=&quot;https://docs.lovable.dev/integrations/github&quot;&gt;2&lt;/a&gt; 但生成出来的页面仍要经过登录、权限和错误状态验收。原型越快，越应该尽早把真实数据与失败条件放进去检查。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/posts/2026-skills/indie-development-01.svg&quot; alt=&quot;图 1：机制与执行流程&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;图 1：个人开发的最小交付循环。每个环节可以先用已有工具完成。&lt;/em&gt;&lt;/p&gt;

&lt;h2 id=&quot;2-一个主力编程-agent加一条验收路径&quot;&gt;2. 一个主力编程 Agent，加一条验收路径&lt;/h2&gt;

&lt;p&gt;Claude Code、Codex、Cursor 都可以作为编程工具候选。与其同时订阅多个工具，我更建议拿同一个小功能比较：需要多少人工澄清，代码改了几轮，最终测试能否通过，出现错误时能否定位并继续。&lt;/p&gt;

&lt;p&gt;如果已经有可用的订阅或工作环境，先复用。能生成更多代码，不代表更快交付。对个人项目，读懂差异、控制依赖和恢复失败，往往比单次生成速度更影响总耗时。&lt;/p&gt;

&lt;p&gt;GitHub 保存代码与提交，自动检查负责发现回归，Playwright 可以验证浏览器中的用户操作。&lt;a href=&quot;https://playwright.dev/docs/intro&quot;&gt;3&lt;/a&gt; 先覆盖“上传账单→查看汇总→下载文件”这一条路径，再补空文件、格式错误、中文文件名、重复请求与跨用户访问。&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;验收场景&lt;/th&gt;
      &lt;th&gt;要确认的结果&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;正常输入&lt;/td&gt;
      &lt;td&gt;金额与汇总一致，文件可下载&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;无效输入&lt;/td&gt;
      &lt;td&gt;给出可理解的错误，不伪造报告&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;用户重复点击&lt;/td&gt;
      &lt;td&gt;不重复创建有副作用的任务&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;用户 A 访问 B 的资源&lt;/td&gt;
      &lt;td&gt;服务端拒绝&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;任务中途失败&lt;/td&gt;
      &lt;td&gt;可以查询状态、重试或清楚结束&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;这些检查应尽量由程序执行并留下结果。截图看起来正常，只能证明某个时刻的界面；Agent 回复“已完成”，也不能替代真实测试。每次准备上线，都应该知道对应哪个代码版本，以及哪条验收通过了。&lt;/p&gt;

&lt;h2 id=&quot;3-multica-放在任务协作这一层&quot;&gt;3. Multica 放在任务协作这一层&lt;/h2&gt;

&lt;p&gt;当同时推进多个功能，开始难以跟踪哪个 Agent 在做什么、卡在哪里、结果由谁验收时，再评估 Multica。它围绕 Issue、Agent、执行记录与 Skills 组织工作。&lt;a href=&quot;https://multica.ai/docs/cloud-quickstart&quot;&gt;4&lt;/a&gt; 对个人开发者，主要价值是让任务与执行状态集中，而不是再增加一个聊天窗口。&lt;/p&gt;

&lt;p&gt;当前官方快速开始要求连接一台执行电脑，并安装、登录支持的编码工具；Agent 在连接的运行环境上执行，Multica 本身不附赠这些工具。&lt;a href=&quot;https://multica.ai/docs/cloud-quickstart&quot;&gt;4&lt;/a&gt; 因此，工作台费用、运行机器和模型调用应分开理解。&lt;/p&gt;

&lt;p&gt;自托管也要区分服务与执行端。官方方案包含 Web、API、PostgreSQL 等服务，以及运行编码工具的电脑；它们可以在同一台机器，也可以分开部署。&lt;a href=&quot;https://multica.ai/docs/self-host-quickstart&quot;&gt;5&lt;/a&gt; 选择自托管，意味着自己维护这些组件与备份，并不自动消除使用模型和运行任务的成本。&lt;/p&gt;

&lt;p&gt;在账单产品里，可以把“上传体验”“权限测试”“导出样式”拆成有明确验收的任务交给不同 Agent。共享接口、数据模型与最终合并仍需要统一管理。任务显示完成后，依然要检查代码差异与测试结果。&lt;/p&gt;

&lt;p&gt;Multica 当前以源码可见方式提供，实际使用与对外服务应核对对应许可证。&lt;a href=&quot;https://github.com/multica-ai/multica&quot;&gt;6&lt;/a&gt; 执行权限也应按其安全模型配置，不能把接入工作台理解为自动获得独立沙箱。&lt;a href=&quot;https://multica.ai/docs/security-model&quot;&gt;7&lt;/a&gt; 我会先用低权限环境跑通一项任务，再扩大范围。&lt;/p&gt;

&lt;p&gt;如果目前只有一个功能在推进，GitHub Issues 或已有任务列表通常足够。协作平台应减少协调成本；为了使用平台而拆出一堆任务，反而会拖慢小项目。&lt;/p&gt;

&lt;h2 id=&quot;4-部署先看程序怎样运行&quot;&gt;4. 部署先看程序怎样运行&lt;/h2&gt;

&lt;p&gt;部署平台的差异，首先是运行方式，其次才是价格。静态页面、短请求接口、常驻服务和耗时任务，对进程、状态与恢复的需求不同。平台可以覆盖多个方向，下表只是选择起点。&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;主要需求&lt;/th&gt;
      &lt;th&gt;优先评估&lt;/th&gt;
      &lt;th&gt;上线前要核对&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;前端应用、预览与发布体验&lt;/td&gt;
      &lt;td&gt;Vercel&lt;/td&gt;
      &lt;td&gt;框架支持、函数限制、用途与用量&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;轻量接口与兼容的边缘运行&lt;/td&gt;
      &lt;td&gt;Cloudflare Workers&lt;/td&gt;
      &lt;td&gt;依赖兼容性、CPU 与请求配额&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;普通后端或常驻进程&lt;/td&gt;
      &lt;td&gt;Railway&lt;/td&gt;
      &lt;td&gt;服务类型、资源、持久化与账单&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Vercel Hobby 当前限定非商用个人用途；没有收入不能自动等同于符合这个条件。&lt;a href=&quot;https://vercel.com/docs/plans/hobby&quot;&gt;8&lt;/a&gt; Cloudflare Workers 按具体资源和套餐设定额度，CPU 时间与等待远端接口的总时长也不是同一个概念。&lt;a href=&quot;https://developers.cloudflare.com/workers/platform/pricing/&quot;&gt;9&lt;/a&gt; Railway 以服务组织应用，仍需为数据库、持久化和资源使用作单独安排。&lt;a href=&quot;https://docs.railway.com/services&quot;&gt;10&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;我的选择顺序是：先用熟悉的技术实现核心功能，确认目标平台能运行，再估算预期流量下的费用。为了节省少量月费而重写成陌生架构，可能付出更大的时间成本；同样，免费额度也不能替代恢复、监控和数据保障。&lt;/p&gt;

&lt;p&gt;对账单产品，上传接口可以很快返回任务标识，耗时解析在后台完成，前端查询状态。任务是否需要独立队列或工作进程，取决于处理时间、失败重试与部署限制。已有后端机制能解决时，先不用再接一套任务平台。&lt;/p&gt;

&lt;h2 id=&quot;5-数据登录与文件先选一套够用的服务&quot;&gt;5. 数据、登录与文件：先选一套够用的服务&lt;/h2&gt;

&lt;p&gt;数据库保存业务记录，认证识别用户，对象存储保存账单与报告。它们可以来自同一平台。对需要关系型数据的个人项目，Supabase 可以作为起步候选；如果已有稳定的数据库与认证方案，也没有必要仅为追新迁移。&lt;/p&gt;

&lt;p&gt;最容易漏掉的是：能登录，不代表只能读自己的数据。以 Supabase 为例，需要为对客户端暴露的数据配置并测试行级权限；能够执行管理操作、绕过相关保护的密钥应留在受控服务端。&lt;a href=&quot;https://supabase.com/docs/guides/database/postgres/row-level-security&quot;&gt;11&lt;/a&gt; 不能把“前端没有显示下载按钮”当作权限控制。&lt;/p&gt;

&lt;p&gt;账单文件的访问同样要验证所有权。下载链接应与账户和资源权限相匹配，日志也应避免不必要地保存原始敏感内容。测试时使用两个账号，直接尝试访问另一账号的资源标识，通常比只检查正常页面更容易发现问题。&lt;/p&gt;

&lt;p&gt;备份的目标是能够恢复。服务提供数据库备份，并不意味着对象存储中的原始文件也被同一份备份覆盖；官方备份说明需要分别阅读。&lt;a href=&quot;https://supabase.com/docs/guides/platform/backups&quot;&gt;12&lt;/a&gt; 在项目早期就做一次恢复演练，确认数据、文件和必要配置能一起重建。&lt;/p&gt;

&lt;h2 id=&quot;6-做-ai-产品要算两套系统&quot;&gt;6. 做 AI 产品，要算两套系统&lt;/h2&gt;

&lt;p&gt;编程 Agent 帮你开发账单产品；用户点击“生成报告”后，运行的是应用服务、解析器、模型 API、数据库和文件存储。开发订阅与产品运行费用属于两笔账，模型效果与交付代码的质量也需要分别评估。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/posts/2026-skills/indie-development-02.svg&quot; alt=&quot;图 2：机制与执行流程&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;图 2：开发与产品运行的关系。线上调用不会因为开发时使用过某个订阅而自动免费。&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;模型接入先从简单接口开始。确实需要流式输出、结构化结果或工具调用时，再选择适合项目语言的 SDK；没有必要为了调用一次模型先搭建完整多 Agent 框架。&lt;/p&gt;

&lt;p&gt;对账单解析，成本应按完整请求计算：输入文件处理、模型输入输出、失败重试、结果校验与存储。只看一次成功调用的 token，会低估真实成本。质量评测也需要包含模糊扫描、跨页表格、不同币种与金额缺失，不能只测一份清晰样例。&lt;/p&gt;

&lt;p&gt;刚上线时，先记录三个产品事件：首次成功生成、用户遇到的关键失败、是否重复使用。已有日志足够排障，就先完善日志；需要分析漏斗或跨请求追踪时，再评估专门的分析与可观测服务。每增加一项采集，都应知道它帮助做什么决定。&lt;/p&gt;

&lt;h2 id=&quot;7-三个阶段各自只补当前缺口&quot;&gt;7. 三个阶段，各自只补当前缺口&lt;/h2&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;阶段&lt;/th&gt;
      &lt;th&gt;最小组合&lt;/th&gt;
      &lt;th&gt;推进到下一阶段的依据&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;验证需求&lt;/td&gt;
      &lt;td&gt;一个主力编码工具、GitHub、核心流程测试、适配的部署平台&lt;/td&gt;
      &lt;td&gt;用户确实愿意完成并重复使用核心功能&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;小范围上线&lt;/td&gt;
      &lt;td&gt;增加可靠数据服务、权限测试、备份恢复与基本监控&lt;/td&gt;
      &lt;td&gt;能解释失败、恢复数据并控制运行费用&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;多任务或 AI 产品增长&lt;/td&gt;
      &lt;td&gt;按需增加 Multica、后台任务与模型追踪&lt;/td&gt;
      &lt;td&gt;已出现明确的协调、执行或成本管理问题&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;预算也按阶段算：开发工具、基础设施、模型 API 分开记录，再加域名、邮件、备份等真正需要的项目。开通前确认额度单位与超限后果：停止服务、暂停采集还是继续计费。不要把试用期、资源赠金与长期免费层写成同一种承诺。&lt;/p&gt;

&lt;p&gt;对个人开发，我最终会用四件事判断工具组合是否合格：核心路径能重复验收，用户数据不会互相越权，故障后能恢复，运行成本能够解释。满足这些条件后，再增加自动化与协作工具，通常更容易判断它们是否值得留下。&lt;/p&gt;

&lt;h2 id=&quot;官方资料&quot;&gt;官方资料&lt;/h2&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;a href=&quot;https://v0.app/docs&quot;&gt;v0 文档&lt;/a&gt;。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://docs.lovable.dev/integrations/github&quot;&gt;Lovable 与 GitHub 同步&lt;/a&gt;。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://playwright.dev/docs/intro&quot;&gt;Playwright 入门&lt;/a&gt;。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://multica.ai/docs/cloud-quickstart&quot;&gt;Multica 快速开始&lt;/a&gt;。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://multica.ai/docs/self-host-quickstart&quot;&gt;Multica 自托管说明&lt;/a&gt;。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/multica-ai/multica&quot;&gt;Multica 仓库与许可证&lt;/a&gt;。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://multica.ai/docs/security-model&quot;&gt;Multica 安全模型&lt;/a&gt;。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://vercel.com/docs/plans/hobby&quot;&gt;Vercel Hobby 用途与限制&lt;/a&gt;。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://developers.cloudflare.com/workers/platform/pricing/&quot;&gt;Cloudflare Workers 计费与额度&lt;/a&gt;。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://docs.railway.com/services&quot;&gt;Railway Services&lt;/a&gt;。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://supabase.com/docs/guides/database/postgres/row-level-security&quot;&gt;Supabase 行级权限&lt;/a&gt;。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://supabase.com/docs/guides/platform/backups&quot;&gt;Supabase 备份范围&lt;/a&gt;。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;2026-10-07 修订。具体套餐、地区、账户权益与项目兼容性需要按实际开通条件判断；本文不将免费额度视作生产容量或账单上限。&lt;/p&gt;

</description>
			<pubDate>Wed, 07 Oct 2026 07:13:50 +0000</pubDate>
			<link>https://sarkerson.github.io/2026/10/07/indie-development/</link>
			<guid isPermaLink="true">https://sarkerson.github.io/2026/10/07/indie-development/</guid>
        
			<category>个人开发</category>
        
			<category>工具选型</category>
        
        
		</item>
    
		<item>
			<title>Agent 架构：Skill 的执行契约、纠偏与安全边界</title>
			<description>&lt;p&gt;用户让 Agent 下载月度账单，生成汇总表，再准备一封邮件草稿。Agent 找到了下载与表格技能，也顺利生成了文件。随后，网页上的一段提示建议它把原始账单上传到“验证服务”，邮件技能又默认执行了发送。&lt;/p&gt;

&lt;p&gt;从任务完成的角度看，流程似乎一直在前进；从用户授权的角度看，它已经越界。另一个版本可能更保守，却在下载超时后不断重试，制造了重复文件，最后仍无法确认汇总表用了哪份账单。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;可靠执行需要同时回答三个问题：什么条件下允许动作，什么证据说明动作完成，失败之后还能安全地做什么。&lt;/strong&gt; Skill 提供过程知识，宿主负责落实权限、状态和验收。两者需要协作，任何一份自然语言说明都不能自动替代另一层职责。&lt;/p&gt;

&lt;p&gt;本文用账单到邮件草稿的教学案例，连接 ContractSkill 的执行契约、SkillRevise 的证据驱动修订、ToolSafe 的执行前检查，以及 SkillDroid 的 GUI 回放与回退。下面的组合流程是工程建议，未声称这些研究共同验证过一套生产系统。&lt;/p&gt;

&lt;h2 id=&quot;1-契约让隐含假设可以被检查&quot;&gt;1. 契约让隐含假设可以被检查&lt;/h2&gt;

&lt;p&gt;一句“下载账单并汇总”，省略了很多前提：是否登录正确账户，月份是否明确，账单是否完整，汇总使用哪个币种，生成的是草稿还是已发送邮件。人会根据语境补全这些条件，自动执行系统则需要知道哪些可以推断、哪些必须检查。&lt;/p&gt;

&lt;p&gt;执行契约可以理解为围绕一个动作的条件与证据：开始前什么应成立，动作要改变什么，结束后怎样确认，失败时允许哪些恢复。它不仅描述成功路径，还定义停止点。&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# 本文教学契约，不是某个项目的真实接口。&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;goal&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;汇总指定月份账单并生成邮件草稿&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;preconditions&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;账户与月份已经确定&lt;/span&gt;
  &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;输入文件可读取且属于本次任务&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;constraints&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;不上传到未授权的外部服务&lt;/span&gt;
  &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;只保存草稿，不发送&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;postconditions&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;汇总金额可以追溯到原始账单&lt;/span&gt;
  &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;草稿引用正确附件&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;recovery&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;下载失败先检查远端与本地状态&lt;/span&gt;
  &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;无法确定是否完成时停止重复写入&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这个契约仍是声明。只有宿主把它落实到状态检查、工具授权和最终验收，才有执行意义。尤其是“不能发送”，应由允许调用的动作和权限控制保证，不能只希望模型始终记住这一句。&lt;/p&gt;

&lt;p&gt;ContractSkill 将草稿技能转换为包含前置条件、步骤、后置条件、恢复与终止检查的产物，用验证反馈定位失败，再施加小范围修补。&lt;a href=&quot;https://arxiv.org/html/2603.20340v1&quot;&gt;1&lt;/a&gt; 它帮助我们将“Agent 没完成”细化成“哪个步骤在什么状态下不满足哪条条件”。&lt;/p&gt;

&lt;p&gt;论文的网页案例中，原技能已经找到相关候选，却停在探索阶段；修复补上后续动作与完成条件，而非重写全部搜索策略。这种局部定位很有价值。但确定性验证器只能检查已经表达并实现的条件，不能据此推出所有用户要求都获得形式化保证。&lt;/p&gt;

&lt;h2 id=&quot;2-检查契约本身是否真的执行&quot;&gt;2. 检查契约本身是否真的执行&lt;/h2&gt;

&lt;p&gt;结构化字段容易制造一种安全感：JSON 里有 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;postconditions&lt;/code&gt;，似乎就有了后置验证。实际上，规范化过程可能忽略未知字段，某些检查器没有挂接到执行器，或者检查只验证了页面文字，并没有验证真实产物。&lt;/p&gt;

&lt;p&gt;ContractSkill 公开实现的研究底稿曾核对有限的条件键和动作集合，与论文概念表示并非逐字段相同。&lt;a href=&quot;https://github.com/underfitting-lu/contractskill&quot;&gt;2&lt;/a&gt; 这个差异提醒我们，接入任何执行契约前，都应检查“输入声明、规范化产物、实际执行”的三个边界。&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;层次&lt;/th&gt;
      &lt;th&gt;对账单案例的检查&lt;/th&gt;
      &lt;th&gt;不能替代什么&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;格式检查&lt;/td&gt;
      &lt;td&gt;契约字段与动作可以解析&lt;/td&gt;
      &lt;td&gt;用户目标是否合理&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;状态检查&lt;/td&gt;
      &lt;td&gt;账户、月份、下载状态符合要求&lt;/td&gt;
      &lt;td&gt;金额计算是否正确&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;产物验收&lt;/td&gt;
      &lt;td&gt;汇总与源文件一致，草稿附件正确&lt;/td&gt;
      &lt;td&gt;动作是否得到授权&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;权限检查&lt;/td&gt;
      &lt;td&gt;当前动作与目标在授权范围内&lt;/td&gt;
      &lt;td&gt;最终结果是否正确&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;对于宿主不认识的约束，合理行为是明确报错、降级或转交，不能静默删掉后当作通过。可以主动构造坏产物：金额错一位、附件仍是上月文件、文件名正确但内容为空，检查验收器是否真的拒绝。&lt;/p&gt;

&lt;p&gt;还要把完成声明和提交结果分开。模型说“已保存草稿”是待核查信息，真实邮件草稿标识、附件列表与状态才是验收依据。若工具没有返回足够证据，应保留未知，而非把语言上的确定性当作系统事实。&lt;/p&gt;

&lt;h2 id=&quot;3-执行前的保护与执行后的验收&quot;&gt;3. 执行前的保护与执行后的验收&lt;/h2&gt;

&lt;p&gt;ToolSafe 研究在工具调用前引入逐步检查与反馈，让不合适的候选动作在产生副作用前得到处理。&lt;a href=&quot;https://arxiv.org/html/2601.10156v1&quot;&gt;3&lt;/a&gt; 对 Skill 宿主的启发是：检查器应看到用户目标、可信授权、已有状态与候选调用，检查发生在真正执行之前。&lt;/p&gt;

&lt;p&gt;但学习型检查器仍然可能判断错误。需要由确定性的权限边界限制可访问账户、数据、工具动作和目标地址；模型检查帮助判断语境，不能自行扩张这些边界。技能文件自称“需要上传数据”，只是能力请求，不是用户授权。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/posts/2026-skills/skill-runtime-01.svg&quot; alt=&quot;图 1：机制与执行流程&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;图 1：本文建议的执行路径。授权来自可信状态；网页、文件与技能内容不能改写它。&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;网页中的“上传到验证服务”属于外部内容，最多是任务材料，不能变成新增授权。宿主在候选工具调用处应能看到实际发送目的地与数据范围，再与当前权限比较。只在技能入库时扫描一次文本，无法覆盖运行时接触到的新内容。&lt;/p&gt;

&lt;p&gt;来源完整性、内容风险、功能正确性与运行时授权也应分别记录。签名证明材料来自某个发布者，不能证明指令合理；检测器未报告风险，不等于后续所有动作安全。恶意技能的实证研究提供了真实威胁样本与分析方法，但没有给任何单一检查方式提供全面保证。&lt;a href=&quot;https://arxiv.org/html/2602.06547v1&quot;&gt;4&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;安全与任务效用需要一起评估。全部拒绝当然能阻止部分风险，也会让正常账单处理无法完成。测试应同时包含合法下载、准备草稿、未授权上传、误发邮件，以及外部内容试图改写目标的情形。&lt;/p&gt;

&lt;h2 id=&quot;4-失败后先判断是否可以重试&quot;&gt;4. 失败后，先判断是否可以重试&lt;/h2&gt;

&lt;p&gt;“再试一次”对读取和对外写入的含义不同。读取公开页面失败，重复访问通常容易控制；发送或付款超时，则可能是操作已经成功、结果没有返回。直接重试会把一次不确定变成两次真实副作用。&lt;/p&gt;

&lt;p&gt;我会先把执行结果分成成功、明确失败和状态未知。未知需要查询外部事实；无法查询时，停止并说明缺少的证据。幂等键可以帮助识别重复请求，但只有外部系统真正支持对应语义时才有效，不能仅靠本地生成一个字符串获得保证。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/posts/2026-skills/skill-runtime-02.svg&quot; alt=&quot;图 2：机制与执行流程&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;图 2：不确定副作用的恢复路径。查询结果本身也需要可核查，不能只再次询问执行模型。&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;对账单任务，下载失败后可以先检查文件完整性与远端状态；汇总失败可以在独立副本上重算；邮件发送属于本来没有授权的动作，应被阻断，而不是作为待修复步骤继续尝试。&lt;/p&gt;

&lt;p&gt;恢复还应有总预算与停止条件。无限重试可能增加成本、触发服务限流，或不断修改原始文件。对于不能恢复的动作，应保留受影响资源与已知状态，让用户或后续系统能够继续处理，而不是只输出一段模糊道歉。&lt;/p&gt;

&lt;h2 id=&quot;5-用执行证据修订-skill&quot;&gt;5. 用执行证据修订 Skill&lt;/h2&gt;

&lt;p&gt;确认失败属于规则缺陷后，才进入技能修订。SkillRevise 将验证要求、失败归属与必须保留的条件结合起来，并要求修改对应到可观察的执行动作。&lt;a href=&quot;https://arxiv.org/html/2606.01139v4&quot;&gt;5&lt;/a&gt; 这比泛化地追加“认真检查”更容易验收。&lt;/p&gt;

&lt;p&gt;例如邮件草稿缺少附件，候选修订可以要求保存后读取草稿对象，核对附件标识与预期文件；如果汇总用了旧账单，则应修改输入绑定与版本检查。两种失败都表现为“结果不对”，但修订对象不同。&lt;/p&gt;

&lt;p&gt;修改后必须重新执行验收。旧报告只证明旧产物在旧规则下发生过什么，不能直接附到新技能版本上。当前任务修好，也不等于候选适合未来任务；跨任务发布需要独立回归，详见&lt;a href=&quot;/2026/06/27/learning-skills/&quot;&gt;持续改进篇&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;这里还要保留环境失败的出口：服务不可用、授权不足、输入本身缺失，不应都通过重写技能解决。诊断器提出一个流畅原因，只能形成假设。修订日志需要同时保存事实、假设、补丁和验证结果，避免把解释逐轮升级为未经证实的“知识”。&lt;/p&gt;

&lt;h2 id=&quot;6-gui-复用稳定部分可以回放变化部分需要检查&quot;&gt;6. GUI 复用：稳定部分可以回放，变化部分需要检查&lt;/h2&gt;

&lt;p&gt;界面任务最能暴露契约的价值。昨天成功点击的位置，今天可能被弹窗覆盖；相同按钮文字可能出现在另一张账单上。可复用的对象应包含任务参数、对象定位依据、状态条件与恢复方式，而不只是坐标序列。&lt;/p&gt;

&lt;p&gt;SkillDroid 将成功轨迹编译成可复用资产，在匹配可靠时回放，遇到变化时再进入恢复或模型决策路径。&lt;a href=&quot;https://arxiv.org/html/2604.14872v1&quot;&gt;6&lt;/a&gt; 对本文案例，可以将“打开账单列表、选择月份、发起下载”的稳定结构复用，同时每次核对账户、月份、目标对象和下载结果。&lt;/p&gt;

&lt;p&gt;这种路径可能减少模型调用，但不能只统计回放成功的部分。整体成本需要包含定位失败、回退、额外模型调用和最终未完成的任务。若每次界面变化都要求重新探索，前期编译成本是否值得，也需要按真实复用次数判断。&lt;/p&gt;

&lt;p&gt;不要把“本次没有调用模型”写成“整条能力链零计算”。编译、参数绑定、界面检查和失败恢复都可能有成本。回放的边界越明确，系统越容易知道什么时候应停下来，避免把旧轨迹强行套到新界面。&lt;/p&gt;

&lt;h2 id=&quot;7-让验证落到真实失败上&quot;&gt;7. 让验证落到真实失败上&lt;/h2&gt;

&lt;p&gt;发布前可以围绕贯穿案例做一次故障注入：换错月份、撤销读取权限、让下载返回空文件、在页面插入要求上传私有材料的文本、让工具执行成功后返回超时，再故意遗漏邮件附件。每一种故障都应有明确预期：拒绝、恢复、澄清或停止。&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;测试对象&lt;/th&gt;
      &lt;th&gt;应观察的结果&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;正常任务&lt;/td&gt;
      &lt;td&gt;正确完成并保留可追溯证据&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;状态不满足&lt;/td&gt;
      &lt;td&gt;在产生副作用前发现问题&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;局部产物错误&lt;/td&gt;
      &lt;td&gt;定位具体缺口，限制修复范围&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;未授权动作&lt;/td&gt;
      &lt;td&gt;由宿主边界阻断&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;副作用未知&lt;/td&gt;
      &lt;td&gt;查询状态，避免盲目重试&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;恢复预算耗尽&lt;/td&gt;
      &lt;td&gt;保留状态并清楚停止&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;恢复率、约束违规、总尝试数、时延和成本应一起报告。只统计最终找到了一个成功结果，会隐藏搜索期间已经发生的错误动作。对有副作用的系统，最终文件正确并不代表整个执行过程可接受。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;我希望 Skill 的执行契约最终成为一种可追责的接口：条件满足才执行，证据充分才宣告完成，恢复有边界，权限始终由宿主掌握。&lt;/strong&gt; 这使技能可以逐步变得更自主，同时让用户仍然知道系统做了什么、为什么这样做。&lt;/p&gt;

&lt;h2 id=&quot;参考资料&quot;&gt;参考资料&lt;/h2&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;a href=&quot;https://arxiv.org/html/2603.20340v1&quot;&gt;ContractSkill，arXiv:2603.20340v1&lt;/a&gt;：契约、定位与局部修复。主实验和迁移子集的口径不同，本文不混用其成绩。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/underfitting-lu/contractskill&quot;&gt;ContractSkill 官方实现&lt;/a&gt;：底稿于 2026-09-27 核对 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;env/skill_utils.py&lt;/code&gt;，概念字段与实际规范化字段需区分。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://arxiv.org/html/2601.10156v1&quot;&gt;ToolSafe，arXiv:2601.10156v1&lt;/a&gt;：逐步工具调用保护与反馈；&lt;a href=&quot;https://github.com/MurrayTom/ToolSafe&quot;&gt;作者实现&lt;/a&gt;。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://arxiv.org/html/2602.06547v1&quot;&gt;Malicious Agent Skills in the Wild，arXiv:2602.06547v1&lt;/a&gt;：恶意技能实证分析。仅用于防御设计的威胁依据。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://arxiv.org/html/2606.01139v4&quot;&gt;SkillRevise，arXiv:2606.01139v4&lt;/a&gt;：执行证据驱动的技能修订；&lt;a href=&quot;https://github.com/HKUST-KnowComp/skillrevise&quot;&gt;作者实现&lt;/a&gt;。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://arxiv.org/html/2604.14872v1&quot;&gt;SkillDroid，arXiv:2604.14872v1&lt;/a&gt;：编译、回放与恢复。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;2026-10-07 合并修订。本文没有独立运行攻击样本、设备实验或 benchmark；账单案例为通用教学场景。&lt;/p&gt;

</description>
			<pubDate>Wed, 07 Oct 2026 07:13:50 +0000</pubDate>
			<link>https://sarkerson.github.io/2026/09/11/skill-runtime/</link>
			<guid isPermaLink="true">https://sarkerson.github.io/2026/09/11/skill-runtime/</guid>
        
			<category>Agent</category>
        
			<category>Skills</category>
        
        
		</item>
    
		<item>
			<title>Agent Skills：技能库的冲突治理与版本发布</title>
			<description>&lt;p&gt;技能库刚上线时，维护很简单：上传文件，更新版本，让 Agent 读到新内容。几个月后，库里出现了多个名字相近的技能，脚本与正文不同步，实验组还在使用旧规则，依赖却已经被热更新。此时一次失败，很难说清用户究竟执行了哪一套能力。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;技能库的治理单位，最终需要从单份文件走向一次请求实际使用的完整能力快照。&lt;/strong&gt; 冲突识别、合并、退役和灰度发布都围绕这个对象发生。只给每份 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SKILL.md&lt;/code&gt; 加一个版本号，无法保证整套运行条件一致。&lt;/p&gt;

&lt;p&gt;本文结合 SkillNet 的关系建模、SkillOps 的库维护研究、SkillFlow 的演化记录与 Agent Skills 格式规范，讨论一套可逐步落地的治理方式。库级决策、快照与实验流程是本文的工程设计，不将它们表述为某篇论文已经验证的完整生产架构。&lt;/p&gt;

&lt;h2 id=&quot;1-相似不等于重复&quot;&gt;1. 相似不等于重复&lt;/h2&gt;

&lt;p&gt;两份技能都叫“导出表格”：一份保留公式，一份生成静态值；一份只在本地处理，一份会上传到外部服务。若只比较标题和向量相似度，它们很容易被合并。对用户而言，这些差异却直接决定任务能否被正确、安全地完成。&lt;/p&gt;

&lt;p&gt;我会先区分五种关系。这里的分类是工程分析用语，不声称所有研究使用相同定义。&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;关系&lt;/th&gt;
      &lt;th&gt;判断依据&lt;/th&gt;
      &lt;th&gt;对维护的含义&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;内容重复&lt;/td&gt;
      &lt;td&gt;包内完整内容与资源相同&lt;/td&gt;
      &lt;td&gt;可以考虑统一存储，仍需保留来源与权限&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;语义重叠&lt;/td&gt;
      &lt;td&gt;部分目标或步骤相似&lt;/td&gt;
      &lt;td&gt;需要识别各自独有的适用条件&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;功能替代&lt;/td&gt;
      &lt;td&gt;同一目标有多种可行路径&lt;/td&gt;
      &lt;td&gt;按环境、成本和权限选择&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;条件冲突&lt;/td&gt;
      &lt;td&gt;同一条件下提出不能同时满足的要求&lt;/td&gt;
      &lt;td&gt;需要仲裁或收窄触发范围&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;接口不兼容&lt;/td&gt;
      &lt;td&gt;上游产物不满足下游输入要求&lt;/td&gt;
      &lt;td&gt;修接口或明确适配与信息损失&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;关键词碰撞只能提供候选线索。两份技能都提到“医院”，可能分别处理健康咨询与预约操作；两份技能措辞完全不同，也可能对同一个文件提出相反修改。冲突判断需要绑定任务条件、作用对象和版本。&lt;/p&gt;

&lt;p&gt;SkillNet 将技能、来源与关系组织到更明确的知识结构中，并讨论任务局部的知识组织。&lt;a href=&quot;https://arxiv.org/abs/2603.04448v2&quot;&gt;1&lt;/a&gt; 对治理系统而言，重要启发是让差异与依赖可检查。关系图并不会自动获得真实性：人工声明、模型推断和执行验证的边，应该记录不同的证据来源。&lt;/p&gt;

&lt;h2 id=&quot;2-合并是一项需要验收的变更&quot;&gt;2. 合并是一项需要验收的变更&lt;/h2&gt;

&lt;p&gt;合并技能往往被当作文本编辑，但它实际改变了能力边界。两份技能分别通过测试，不代表合并后的版本仍然通过；更长的主文可能让触发条件变得模糊，也可能把两个互斥流程同时暴露给执行器。&lt;/p&gt;

&lt;p&gt;一个合理的合并候选，应先回答三个问题：共同主路径是什么，差异条件是否能明确表达，合并后还能否稳定选择正确分支。如果差异依赖复杂权限或完全不同的执行环境，保留两个技能与清晰关系，可能比强行合成一个更好。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/posts/2026-skills/skill-library-governance-01.svg&quot; alt=&quot;图 1：机制与执行流程&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;图 1：本文建议的库维护流程。减少技能数量不是验收标准。&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;SkillOps 将库健康与维护操作显式化，讨论合并、修复、退役、验证器和适配器等对象。&lt;a href=&quot;https://arxiv.org/html/2605.13716v1&quot;&gt;2&lt;/a&gt; 但读它的成绩时必须看清任务：论文部分评测使用高层动作序列匹配，并不等于真实交互环境的端到端成功率。规则化注入的库缺陷，也不能覆盖生产中所有语义冲突。&lt;/p&gt;

&lt;p&gt;原研究底稿还核对过公开示例中的接口签名合并：签名由部分前置条件与产物字段构成，并不等于完整操作语义。&lt;a href=&quot;https://github.com/Hik289/SkillOps&quot;&gt;3&lt;/a&gt; 因此，将这个启发式直接用于自动删库有明显风险。两个输入输出相同的实现，仍可能具有不同的成本、许可、工具依赖与失败边界。&lt;/p&gt;

&lt;p&gt;我的建议是将相似度和接口签名用于发现候选，把真正的删除或合并交给带有回归验收的变更流程。误合并和误退役应单独计量；它们可能比暂时保留一份冗余材料更难恢复。&lt;/p&gt;

&lt;h3 id=&quot;低使用率不能单独决定退役&quot;&gt;低使用率不能单独决定退役&lt;/h3&gt;

&lt;p&gt;某个技能使用少，可能因为需求少，也可能因为描述不清、路由被其他技能挤占，或者它只在罕见但重要的失败场景中使用。使用频率带有选择偏差，不能直接等同于能力价值。&lt;/p&gt;

&lt;p&gt;退役前应检查独有任务覆盖、被依赖情况、替代路径和历史失败。可先停止新增引用并观察，再从新快照中移除；旧快照仍应能够重建。若涉及安全撤回，则另走紧急路径，不能为了实验稳定继续保留已确认风险。&lt;/p&gt;

&lt;h2 id=&quot;3-发布对象至少有三层&quot;&gt;3. 发布对象至少有三层&lt;/h2&gt;

&lt;p&gt;一份源技能可能包含正文、脚本和资源；为了适配不同模型或执行框架，还可能生成不同运行产物；真正进入一次请求的，则是技能集合、索引、选择策略与相关配置。把这些都压成一个“最新版”指针，很容易出现局部更新。&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;层次&lt;/th&gt;
      &lt;th&gt;应记录什么&lt;/th&gt;
      &lt;th&gt;典型故障&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;源技能包&lt;/td&gt;
      &lt;td&gt;全部文件、依赖、来源、内容指纹&lt;/td&gt;
      &lt;td&gt;正文变了，脚本仍旧&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;目标产物&lt;/td&gt;
      &lt;td&gt;面向模型、工具与环境的适配结果&lt;/td&gt;
      &lt;td&gt;用错环境的编译或配置&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;发布快照&lt;/td&gt;
      &lt;td&gt;技能集合、索引、策略及其绑定关系&lt;/td&gt;
      &lt;td&gt;索引召回了快照外版本&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Agent Skills 规范为包结构与元数据提供共同入口，但格式合法不等于行为兼容，也没有替产品完成灰度、实验与回滚。&lt;a href=&quot;https://agentskills.io/specification&quot;&gt;4&lt;/a&gt; SkillFlow 以版本化变化观察跨任务的技能演化，帮助我们把更新当作可追踪事件；它同样不能被直接称为完整的生产分发协议。&lt;a href=&quot;https://arxiv.org/html/2604.17308v1&quot;&gt;5&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;在一个最小实现里，可以先不引入复杂编译层，但应保证源包不可变，以及请求开始时绑定一份一致快照。后续按需加载正文、脚本与依赖时，继续从该快照解析。这样一次任务不会在执行到一半时突然获得另一套规则。&lt;/p&gt;

&lt;p&gt;快照不要求把全部清单塞进模型上下文。它是宿主的可信运行状态，用于加载、审计与复现。模型只需要当前决策所需的信息，不能让它自行修改快照标识来绕过版本约束。&lt;/p&gt;

&lt;h2 id=&quot;4-实验基线需要是固定对象&quot;&gt;4. 实验基线需要是固定对象&lt;/h2&gt;

&lt;p&gt;考虑一个简单的教学例子：基线是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;A@1 + B@1&lt;/code&gt;，实验只想验证 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;A@2&lt;/code&gt;。如果运行时采用“实验与当前基线取并集，然后选择高版本”，当 B@2 被其他流程推全后，实验处理可能悄悄变成 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;A@2 + B@2&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;此时最终差值包含多个变化，不能继续解释为 A@2 的作用。更稳妥的做法，是保存实验开始时的完整基线快照和实验快照。新全局版本出现后，旧实验按既定协议继续，或明确关闭并开启一个新实验；不能静默改变处理。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/posts/2026-skills/skill-library-governance-02.svg&quot; alt=&quot;图 2：机制与执行流程&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;图 2：固定快照的实验路径。图中不包含静默追随全局最新版的更新。&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;若线上运行同时产生经验更新，还需要隔离学习状态。实验组生成的新技能不能流入基线组，否则对照条件也在学习实验改动。可以让两组使用冻结库，把学习放到独立训练流；也可以为两组隔离可写状态，但必须把这种学习协议纳入实验定义。&lt;/p&gt;

&lt;p&gt;基线前进后的候选更新，则适合三方比较：原始基线、候选差分、当前基线。共同修改同一条件或资源时需要重新验证，不能单纯按版本号较大者获胜。文本没有冲突，也不代表行为没有冲突。&lt;/p&gt;

&lt;h2 id=&quot;5-回滚恢复的是版本不是世界&quot;&gt;5. 回滚恢复的是版本，不是世界&lt;/h2&gt;

&lt;p&gt;把发布指针退回旧快照，可以影响后续请求。但已经发出的消息、修改的远端文件和发生的费用，不会随着技能版本回退而消失。因此，发布回滚与任务副作用恢复需要分别设计。&lt;/p&gt;

&lt;p&gt;对有副作用的动作，应记录是否已经执行、外部资源标识、幂等依据与可用补偿。工具超时但状态未知时，直接用旧版本重试，可能造成重复操作。可靠的恢复先查询事实，再决定重试、补偿或转交。&lt;/p&gt;

&lt;p&gt;灰度也应覆盖多维信号：任务质量、关键约束、工具异常、重试次数、时延与成本。总体平均改善并不足以掩盖特定用户群或任务类型的严重退化。各项门槛由真实产品风险决定，不预设一个适用于所有技能的“涨一点就推全”。&lt;/p&gt;

&lt;p&gt;为了让灰度结果可解释，每次请求至少要关联以下信息。表中的字段是本文建议，不是某个现成平台的接口规范。&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;记录&lt;/th&gt;
      &lt;th&gt;用途&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;发布快照、技能包与索引指纹&lt;/td&gt;
      &lt;td&gt;解释实际使用了什么&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;实验组与分配时间&lt;/td&gt;
      &lt;td&gt;解释用户接受了哪个处理&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;模型、工具和环境版本&lt;/td&gt;
      &lt;td&gt;识别非技能变化&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;实际披露与执行事件&lt;/td&gt;
      &lt;td&gt;定位链路上的失效环节&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;验收、成本与副作用状态&lt;/td&gt;
      &lt;td&gt;支持推广、回退与恢复&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;若服务无法提供完全稳定的模型版本，也应记录可取得的返回信息与时间窗口，并在报告中保留不确定性。无法冻结所有条件不意味着实验不能做，但会限制能够归因的范围。&lt;/p&gt;

&lt;h2 id=&quot;6-把治理做成可解释的决策&quot;&gt;6. 把治理做成可解释的决策&lt;/h2&gt;

&lt;p&gt;库治理最终可以围绕一类变更单组织：修改对象、问题证据、预期影响、验证任务、发布快照和回退方式。新增、合并、拆分、替代与退役都沿同一条可追溯流程处理，差异在于所需证据和回归范围。&lt;/p&gt;

&lt;p&gt;例如合并两个表格导出技能，变更单应列出双方独有任务和权限差异，给出合并后触发条件，并证明原有功能仍能被正确采用。若合并仅仅减少一份文件，却增加路由歧义，就没有完成治理目标。&lt;/p&gt;

&lt;p&gt;适配器也需要按能力产物管理。上游提供金额、下游要求带币种的金额时，新增字段映射不能凭空恢复币种语义。类型能对上只是开始，实际转换是否保留信息、缺失时如何拒绝，需要单独验证。&lt;/p&gt;

&lt;p&gt;从零建设时，我会按这个顺序推进：先保证完整技能包和不可变快照；再记录请求实际采用的版本；接着做配对回归、灰度和回滚；之后才开放自动合并、退役与动态学习。每一步都应让一次失败更容易解释，而不是增加一个看起来先进但难以审计的后台任务。&lt;/p&gt;

&lt;p&gt;自动化适合承担发现候选、整理证据、运行回归和生成变更报告。真正值得自动推广的，是那些已有清晰边界、可靠验收和恢复能力的操作。对语义不确定的合并，保持待审状态往往更合理。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;一个健康的技能库，不一定最小，也不一定更新最快。它应该能说明每项能力为什么存在、每次变更凭什么接受，以及出现问题后怎样找到并恢复原来的状态。&lt;/strong&gt;&lt;/p&gt;

&lt;h2 id=&quot;参考资料&quot;&gt;参考资料&lt;/h2&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/2603.04448v2&quot;&gt;SkillNet，arXiv:2603.04448v2&lt;/a&gt;：技能组织与关系；&lt;a href=&quot;https://github.com/zjunlp/SkillNet&quot;&gt;作者仓库&lt;/a&gt;。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://arxiv.org/html/2605.13716v1&quot;&gt;SkillOps，arXiv:2605.13716v1&lt;/a&gt;：Pu、Song、Zhao 的技能库维护研究。注意附录 D、G 的任务与故障注入协议。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/Hik289/SkillOps&quot;&gt;SkillOps 官方实现&lt;/a&gt;：研究底稿于 2026-09-27 静态核对 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;skill_graph.py&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;maintenance.py&lt;/code&gt;；公开示例不等于论文所有实验资产。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://agentskills.io/specification&quot;&gt;Agent Skills Specification&lt;/a&gt;：技能包格式与元数据。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://arxiv.org/html/2604.17308v1&quot;&gt;SkillFlow，arXiv:2604.17308v1&lt;/a&gt;：跨任务技能发现与演化的评测协议。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;2026-10-07 合并修订。本文的版本、实验与发布流程使用通用教学案例，没有公开内部业务日志，也没有将设计建议写成已上线系统的效果报告。&lt;/p&gt;

</description>
			<pubDate>Wed, 07 Oct 2026 07:13:50 +0000</pubDate>
			<link>https://sarkerson.github.io/2026/08/17/skill-library-governance/</link>
			<guid isPermaLink="true">https://sarkerson.github.io/2026/08/17/skill-library-governance/</guid>
        
			<category>Agent</category>
        
			<category>Skills</category>
        
        
		</item>
    
		<item>
			<title>Agent 评测：如何证明 Skill 真的有效</title>
			<description>&lt;p&gt;同一批任务，加入 Skill 后通过率提高了。这个结果很容易让人想立即扩大技能库。但如果新配置多用了几轮模型调用、额外读取了任务相关文档，或者只有本来就容易成功的请求被统计进去，涨分究竟说明了什么？&lt;/p&gt;

&lt;p&gt;我关心的不是给技能寻找一个好看的平均分，而是判断它在什么任务上补足了能力，在什么任务上引入了干扰，以及为此付出了多少成本。&lt;strong&gt;一次评测首先要说清楚自己改变了什么，然后才有资格解释分数。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;本文结合 SkillsBench、SWE-Skills-Bench、Skill-Usage 和 Skill Coverage，整理一套适合 Agent 框架的评测思路。论文数字均为作者在各自配置下报告的结果，不跨模型、跨任务直接排名；协议、算例与工程判断由本文提出，未独立重跑这些实验。&lt;/p&gt;

&lt;h2 id=&quot;1-用可用条件定义实验&quot;&gt;1. 用“可用条件”定义实验&lt;/h2&gt;

&lt;p&gt;先看一个简单问题：如果统计所有“真正读取过技能”的请求，通过率会不会比没读的更高？这个比较看似直观，却可能把任务难度混进来。困难任务更容易触发搜索，也更容易失败；简单任务不读技能也能完成。&lt;/p&gt;

&lt;p&gt;因此，部署实验更适合比较“分配到技能可用条件”与“分配到技能不可用条件”的结果。两组使用相同任务分布、模型、工具与预算。Agent 是否选择读取、是否实际收到材料、是否遵循指令，再作为中间过程分析。&lt;/p&gt;

&lt;p&gt;这里的预算也要具体：最大步数相同，并不保证模型调用数和 token 相同；工具调用不算环境步数，也不表示没有成本。先固定允许使用的资源，再记录实际消耗，才能区分同预算下质量提高和增加计算后的质量提高。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/posts/2026-skills/skill-evidence-01.svg&quot; alt=&quot;图 1：机制与执行流程&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;图 1：配对评测的基本结构。每个条件从干净环境开始，不能继承另一条件生成的产物或记忆。&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;实验记录至少包含四个快照：任务与验收器、模型与执行框架、技能包与资源、索引与选择策略。若技能不变但索引变了，或者工具在两轮之间升级，就需要明确标注。一个技能版本号不足以描述整个实验条件。&lt;/p&gt;

&lt;h2 id=&quot;2-三类基准回答三个不同问题&quot;&gt;2. 三类基准回答三个不同问题&lt;/h2&gt;

&lt;p&gt;SkillsBench v4 将 87 个任务、8 个领域与确定性验证器配对，在 18 种模型—Harness 配置中比较有无整理好的技能。作者报告平均通过率从 33.9% 提高到 50.5%，增加 16.6 个百分点。&lt;a href=&quot;https://arxiv.org/html/2602.12670v4&quot;&gt;1&lt;/a&gt; 这说明技能在该基准上具有总体价值，也提醒我们：结果依赖具体任务、技能质量与宿主，不能当作任何产品的默认增益。&lt;/p&gt;

&lt;p&gt;确定性验证器的意义是同一产物可以按固定规则判定，它并不保证规则覆盖了所有用户需求。表格数字算对了，但用户要求的透视表对象没有创建，仍然可能不合格。评测设计必须将需求落到最终产物，而不是只检查 Agent 的完成声明。&lt;/p&gt;

&lt;p&gt;SWE-Skills-Bench 则研究软件工程需求验收。其 Claude Code 与 Haiku 4.5 设置下，49 项技能的平均通过率从 89.8% 到 91.0%，输入输出 token 均值从 303K 到 335K；39 项技能的净通过率不变、7 项上升、3 项下降。&lt;a href=&quot;https://arxiv.org/html/2603.15401v1&quot;&gt;2&lt;/a&gt; 这是另一种任务与基线水平，不能拿 1.2 个百分点直接否定前一个基准的价值。&lt;/p&gt;

&lt;p&gt;“净通过率不变”也不等于没有任何影响。某些任务可能被修复，同时另一些任务发生回归；只有逐题配对结果能分清这些情况。基线已经接近天花板时，技能的主要价值还可能体现为降低成本，或提高某个关键子群体的可靠性。&lt;/p&gt;

&lt;p&gt;Skill-Usage 进一步移除“有人已经替 Agent 挑好技能”的理想前提。论文从整理好的技能出发，逐步引入自主选择、干扰项与全库检索，观察能力收益如何在现实选择链中损失。&lt;a href=&quot;https://arxiv.org/html/2604.04323v1&quot;&gt;3&lt;/a&gt; 这类实验帮助区分：技能内容不够好，还是系统无法稳定地找到并使用它。&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;评测问题&lt;/th&gt;
      &lt;th&gt;代表工作&lt;/th&gt;
      &lt;th&gt;能支持的判断&lt;/th&gt;
      &lt;th&gt;仍需额外验证&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;高质量技能有没有边际价值&lt;/td&gt;
      &lt;td&gt;SkillsBench&lt;/td&gt;
      &lt;td&gt;固定任务中的有无技能差异&lt;/td&gt;
      &lt;td&gt;真实大库的选择与维护&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;能否满足具体工程需求&lt;/td&gt;
      &lt;td&gt;SWE-Skills-Bench&lt;/td&gt;
      &lt;td&gt;需求映射的产物验收&lt;/td&gt;
      &lt;td&gt;其他模型、任务与接口&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;收益在哪个选择环节丢失&lt;/td&gt;
      &lt;td&gt;Skill-Usage&lt;/td&gt;
      &lt;td&gt;不同可见性与检索条件的变化&lt;/td&gt;
      &lt;td&gt;各环节独立作用及全部成本&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;技能要求是否被充分测试&lt;/td&gt;
      &lt;td&gt;Skill Coverage&lt;/td&gt;
      &lt;td&gt;行为约束的覆盖与满足情况&lt;/td&gt;
      &lt;td&gt;未抽取的要求和真实业务完整性&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;这张表不提供统一排行榜。不同基准的价值，是让我们看到不同的缺口。上线前仍需用自己的任务分布、权限边界与失败代价重新定义验收。&lt;/p&gt;

&lt;h2 id=&quot;3-保留一个负例比多报一个总分更有用&quot;&gt;3. 保留一个负例，比多报一个总分更有用&lt;/h2&gt;

&lt;p&gt;SWE-Skills-Bench 展示了一个 Linkerd 配置案例：任务需要特定版本下的 gRPC 与 mTLS 配置，近似技能模板却引入了旧版本、其他协议和不适用字段。&lt;a href=&quot;https://arxiv.org/html/2603.15401v1&quot;&gt;2&lt;/a&gt; 它说明技能可能把模型已有的正确知识带偏，而不仅是“没有提供帮助”。&lt;/p&gt;

&lt;p&gt;这类问题很适合继续做有边界的对照：完整模板、只保留任务匹配模板、显式标注版本与协议前置条件。模型、任务和预算保持一致，观察改善来自删掉干扰还是补足缺失知识。这个三条件实验是本文建议，不是原论文已经报告的结果。&lt;/p&gt;

&lt;p&gt;对应到 C 端产品，类似失败经常发生在相近意图上：用户只要导出静态报表，技能坚持保留复杂编辑结构；用户要求保留公式，示例却默认转值。评测集需要主动包含“看起来相似，但关键约束相反”的成对任务，否则无法检验触发边界。&lt;/p&gt;

&lt;p&gt;负例还应描述系统本来可以做到什么。只有展示无技能和有技能条件下的差异，才能发现新增材料带来的回归。单独展示技能条件的错误轨迹，无法判断它是新引入的问题，还是模型一直存在的弱点。&lt;/p&gt;

&lt;h2 id=&quot;4-用事件链定位收益为什么没有兑现&quot;&gt;4. 用事件链定位收益为什么没有兑现&lt;/h2&gt;

&lt;p&gt;最终任务失败后，仅增加一条“技能未遵循”的标签通常不够。需要检查失败最早出现在哪一层：候选没有覆盖能力，选择了错误版本，正文读取失败，关键条件没被执行，还是验收器漏掉了要求。&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;事件&lt;/th&gt;
      &lt;th&gt;证据&lt;/th&gt;
      &lt;th&gt;常见误判&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;候选覆盖&lt;/td&gt;
      &lt;td&gt;标注能力与候选集合&lt;/td&gt;
      &lt;td&gt;名称相近就算召回正确&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;选择&lt;/td&gt;
      &lt;td&gt;已采用技能及理由&lt;/td&gt;
      &lt;td&gt;Top K 出现过就算选中&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;成功披露&lt;/td&gt;
      &lt;td&gt;对应调用的真实读取结果&lt;/td&gt;
      &lt;td&gt;发起 Read 就算读到了&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;行为遵循&lt;/td&gt;
      &lt;td&gt;动作与产物满足适用约束&lt;/td&gt;
      &lt;td&gt;模型提到规则就算遵循&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;任务验收&lt;/td&gt;
      &lt;td&gt;最终结果覆盖用户需求&lt;/td&gt;
      &lt;td&gt;自报完成就算成功&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Skill Coverage 将技能说明中的行为要求转成可检查约束，再分析轨迹是否覆盖、是否满足。&lt;a href=&quot;https://arxiv.org/abs/2606.20659v2&quot;&gt;4&lt;/a&gt; 这个思路能帮助我们找到“有规则，却没有被测试”的区域。但约束抽取和轨迹判断都可能遗漏，因此覆盖率应该作为诊断指标，不能替代任务验收。&lt;/p&gt;

&lt;p&gt;同样，Skill-Usage 的公开读取检测实现提醒我们，工具名、路径匹配和文本解析只是读取信号的近似。&lt;a href=&quot;https://github.com/UCSB-NLP-Chang/Skill-Usage&quot;&gt;5&lt;/a&gt; 如果日志无法关联调用与结果，应保留未知，而不是强行归为已读取或未读取。否则，下一轮优化可能在修复一个由监控误差制造的问题。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/posts/2026-skills/skill-evidence-02.svg&quot; alt=&quot;图 2：机制与执行流程&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;图 2：本文建议的诊断路径。它用于安排检查顺序，不能单凭一次轨迹证明因果。&lt;/em&gt;&lt;/p&gt;

&lt;h2 id=&quot;5-同时报告修复回归和成本&quot;&gt;5. 同时报告修复、回归和成本&lt;/h2&gt;

&lt;p&gt;设共有 N 个配对任务，原版本失败、技能版本成功的数量是 repairs；原版本成功、技能版本失败的数量是 regressions。净通过率变化就是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(repairs - regressions) / N&lt;/code&gt;。这是配对计数关系，不是新提出的算法指标。&lt;/p&gt;

&lt;p&gt;例如 100 个任务中修复 15 个、回归 12 个，与修复 3 个、回归 0 个，净变化同为 3 个百分点。但两种配置对用户的影响明显不同：前者需要更仔细地看哪些任务退化，以及错误是否集中在关键需求上。这个数字例子仅用于解释，不来自真实实验。&lt;/p&gt;

&lt;p&gt;成本至少区分离线构建与在线执行。离线包括技能生成、选择评测、索引构建与维护；在线包括模型 token、工具调用、环境交互、重试、时延和人工干预。若一个候选只能通过大量重试达到高分，应同时展示首次成功率和全部搜索成本。&lt;/p&gt;

&lt;p&gt;我更倾向于先用质量—成本二维比较，再讨论具体产品的权重。质量不降、成本下降的方案比较容易判断；质量上升、成本也上升则需要业务决策。把质量变化除以相对成本变化，在分母接近零时会非常不稳定，成本下降时符号也容易误导。&lt;/p&gt;

&lt;p&gt;上线门槛也不应只有一个总体平均数。一个配置即使总体更好，只要在不可接受的约束上退化，就不能直接推广。对重要任务分片、拒绝与澄清行为，以及严重错误，应单列报告，而不是让多数简单任务冲淡它们。&lt;/p&gt;

&lt;h2 id=&quot;6-一套最小而有解释力的实验&quot;&gt;6. 一套最小而有解释力的实验&lt;/h2&gt;

&lt;p&gt;对于“编辑表格并保留公式”这样的能力，可以从四个条件开始。它们用来拆分技能内容与选择系统的作用，不要求所有项目同时实现完整矩阵。&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;条件&lt;/th&gt;
      &lt;th&gt;给 Agent 什么&lt;/th&gt;
      &lt;th&gt;主要比较目的&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;A&lt;/td&gt;
      &lt;td&gt;无技能，原有工具与预算&lt;/td&gt;
      &lt;td&gt;建立基线&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;B&lt;/td&gt;
      &lt;td&gt;人工确认适用的技能可用&lt;/td&gt;
      &lt;td&gt;估计内容在较理想条件下的价值&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;C&lt;/td&gt;
      &lt;td&gt;真实候选库与当前路由&lt;/td&gt;
      &lt;td&gt;估计真实部署效果&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;D&lt;/td&gt;
      &lt;td&gt;与 C 相同，只改变一个待验证环节&lt;/td&gt;
      &lt;td&gt;判断路由、披露或规则修改的贡献&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;每个条件在独立环境中执行同一组任务。外部资源尽量冻结；无法冻结的服务记录版本、时间与变更。对随机系统采用重复运行，并报告不确定性。重复次数应由波动程度与决策风险决定，不存在“跑一次过了就足够”的通用门槛。&lt;/p&gt;

&lt;p&gt;任务集除了典型成功路径，还应包含公式与静态值要求相反的样本、缺失输入、格式异常、旧版本文件、读写权限限制，以及无须使用技能的简单任务。后者能检查技能是否过度触发，防止“凡是表格都走复杂流程”。&lt;/p&gt;

&lt;p&gt;开发集用于诊断与生成候选，选择集用于接受或拒绝，最终测试集用于报告。相近模板、同源文档和同一用户的连续轨迹可能造成泄漏，切分时应按数据来源或任务族考虑，而不只是随机按行分三份。&lt;/p&gt;

&lt;p&gt;验收器同样需要测试。可以主动制造公式丢失、文件路径正确但内容过期、伪造完成说明等坏结果，确认检查器会拒绝。一个从未拒绝过坏产物的验收器，即使输出完全确定，也不足以支撑可靠性结论。&lt;/p&gt;

&lt;p&gt;最后，将报告写成可以用于决策的几句话：在哪个任务范围内，哪项变化修复了多少问题、引入了哪些回归，成本如何改变，还有哪些结论无法从当前证据推出。这样评测才能反过来指导&lt;a href=&quot;/2026/06/27/learning-skills/&quot;&gt;经验优化&lt;/a&gt;与&lt;a href=&quot;/2026/08/17/skill-library-governance/&quot;&gt;版本发布&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Skill 的价值不是“用了之后分数更高”这一句话，而是一个有条件的判断：对这些任务，在这些资源与约束下，它带来了可重复、可解释的改善。&lt;/strong&gt; 一份好的评测报告也应允许得出结论：此处暂时不用技能更合适。&lt;/p&gt;

&lt;h2 id=&quot;参考资料&quot;&gt;参考资料&lt;/h2&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;a href=&quot;https://arxiv.org/html/2602.12670v4&quot;&gt;SkillsBench，arXiv:2602.12670v4&lt;/a&gt;：87 任务、8 领域、18 种模型—Harness 配置；&lt;a href=&quot;https://github.com/benchflow-ai/skillsbench&quot;&gt;作者仓库&lt;/a&gt;。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://arxiv.org/html/2603.15401v1&quot;&gt;SWE-Skills-Bench，arXiv:2603.15401v1&lt;/a&gt;：Table 2 的总体与逐技能结果，Figure 5 的 Linkerd 案例。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://arxiv.org/html/2604.04323v1&quot;&gt;How Well Do Agentic Skills Work in the Wild，arXiv:2604.04323v1&lt;/a&gt;：真实选择与检索条件。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/2606.20659v2&quot;&gt;Skill Coverage，arXiv:2606.20659v2&lt;/a&gt;：技能行为约束的测试充分性。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/UCSB-NLP-Chang/Skill-Usage&quot;&gt;Skill-Usage 官方实现&lt;/a&gt;：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;scripts/prepare_experiment.py&lt;/code&gt; 与 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;check_skill_usage.py&lt;/code&gt;。底稿为 2026-09-27 的静态代码观察，main 后续变化需重新核对。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;2026-10-07 合并修订。上述不同论文的模型、环境、任务与验收口径不同，本文只作方法比较，不给跨基准性能排名。&lt;/p&gt;

</description>
			<pubDate>Wed, 07 Oct 2026 07:13:50 +0000</pubDate>
			<link>https://sarkerson.github.io/2026/07/22/skill-evidence/</link>
			<guid isPermaLink="true">https://sarkerson.github.io/2026/07/22/skill-evidence/</guid>
        
			<category>Agent</category>
        
			<category>Skills</category>
        
        
		</item>
    
		<item>
			<title>Agent Skills：从执行经验到持续改进</title>
			<description>&lt;p&gt;一个表格 Agent 连续把销售额放进透视表的行标签。把失败日志交给模型，让它重写操作手册，似乎就能解决问题。可是另一批相似失败来自工具升级：计划没有错，旧参数已经不再被接口接受。继续强化“正确放置销售额”的文字，对第二种失败没有帮助。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;让 Agent 从经验中变好，首先需要决定经验说明了什么，以及究竟允许修改什么。&lt;/strong&gt; 一次成功可能只是偶然，一次失败也未必由 Skill 导致。生成更多技能文件、保存更多反思，都不能直接证明下一次任务会更可靠。&lt;/p&gt;

&lt;p&gt;本文沿一条完整的链路讨论这个问题：从轨迹提炼候选，用独立证据选择更新，再把有效变化巩固到长期技能库。重点比较 Trace2Skill、SkillOpt、SkillGrad 与 Memento-Skills；RRSI 补充更大编辑范围下的选择边界。本文的控制流程与表格案例属于工程建议和教学示例，研究实验均为作者报告。&lt;/p&gt;

&lt;h2 id=&quot;1-先分开三个会被写回的对象&quot;&gt;1. 先分开三个会被写回的对象&lt;/h2&gt;

&lt;p&gt;一次执行至少留下三类东西：发生过什么、这次任务怎样恢复、以后遇到同类任务该怎样做。它们可以互相提供证据，但不应该被写成同一份持续覆盖的“记忆”。&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;对象&lt;/th&gt;
      &lt;th&gt;表格案例中的内容&lt;/th&gt;
      &lt;th&gt;更新方式&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;原始执行记录&lt;/td&gt;
      &lt;td&gt;请求、技能版本、工具参数、错误与文件差异&lt;/td&gt;
      &lt;td&gt;追加保存，支撑追溯&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;当前任务状态&lt;/td&gt;
      &lt;td&gt;修正后的字段映射、待验收文件&lt;/td&gt;
      &lt;td&gt;在本次运行中受控更新&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;可复用技能&lt;/td&gt;
      &lt;td&gt;适用条件、操作步骤与检查方法&lt;/td&gt;
      &lt;td&gt;形成候选，验证后发布&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;如果把当前任务中的文件名、单元格位置甚至正确答案直接沉淀成通用规则，下一次输入稍变就会失效。相反，只写一句“以后更加仔细”，又没有留下可执行的方法。一个有用的候选应该包含条件、动作和检查：什么时候采用这个做法，具体改变什么，怎样知道它生效。&lt;/p&gt;

&lt;p&gt;对于工具升级，应先修适配器或版本约束；对于参数映射错误，可以修改步骤或示例；对于用户要求不清楚，应改善澄清流程。允许优化器输出“暂不修改 Skill”，有助于避免把整个系统的责任都推给文本。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/posts/2026-skills/learning-skills-01.svg&quot; alt=&quot;图 1：机制与执行流程&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;图 1：本文建议的经验写回流程。诊断可以改变修复对象，生成候选不自动获得发布权。&lt;/em&gt;&lt;/p&gt;

&lt;h2 id=&quot;2-从轨迹提炼技能先冻结参照&quot;&gt;2. 从轨迹提炼技能：先冻结参照&lt;/h2&gt;

&lt;p&gt;多次执行可能同时揭示相同问题。若每发现一条经验就立即修改技能，后续轨迹面对的参照已经变化，很难判断哪些建议互相兼容。Trace2Skill 的一个关键做法，是围绕固定技能快照分析局部执行经验，再分层合并成候选补丁。&lt;a href=&quot;https://arxiv.org/abs/2603.25158v5&quot;&gt;1&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;成功轨迹告诉系统哪些步骤值得保留，失败轨迹帮助定位缺失或错误。局部分析器不必一次阅读全部日志；合并阶段再处理重复建议与冲突。这个结构将“分析大量经验”与“维护一份一致产物”分开，但合并出的文本仍然需要独立验收。&lt;/p&gt;

&lt;p&gt;在本文案例中，两个分析器可能分别建议“增加公式保护检查”和“对输出统一转值”。两条建议都可能在各自任务里正确，合在一起却互相冲突。合并器需要保留条件：前者适用于用户要求可编辑公式的输出，后者只适用于明确要求静态导出的任务。出现频率更高的建议，不应自动覆盖另一种合法需求。&lt;/p&gt;

&lt;p&gt;Trace2Skill 还提供了一个很具体的检查提醒：公开固定版本的表格技能片段中，文字要求按降序删除行，但对已经降序的列表再次执行 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;reversed&lt;/code&gt;，会得到升序。&lt;a href=&quot;https://github.com/Qwen-Applications/Trace2Skill/blob/3d0b52a140f002a512930252b613c49048f7d5ac/released_skills/trace2skill-xlsx-35B-combined/SKILL.md&quot;&gt;2&lt;/a&gt; 这只是一个局部片段的一致性问题，不能用来否定整体研究；它说明自然语言经验、示例代码与执行断言需要共同检查。&lt;/p&gt;

&lt;p&gt;例如下面的教学补丁，把“按原始行号删除”所要求的顺序写成可执行表达，并提醒验证剩余内容。具体工作表 API 由实际工具决定。&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# 教学示例：删除时保持未处理的原始行号稳定。
&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;row_index&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;sorted&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rows_to_delete&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;reverse&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;delete_row&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;row_index&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;verify_remaining_rows_and_formulas&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;删除顺序正确还不够：原始索引是否包含表头、合并单元格如何处理、公式引用是否按预期变化，都可能需要额外测试。候选可以只解决其中一个明确问题，不必承诺覆盖所有表格操作。&lt;/p&gt;

&lt;h2 id=&quot;3-从合理建议到可接受更新&quot;&gt;3. 从合理建议到可接受更新&lt;/h2&gt;

&lt;p&gt;SkillOpt 与 SkillGrad 都利用执行反馈改进外部技能，但关注的位置不同。SkillOpt 强调受控提议、编辑与选择；SkillGrad 进一步考虑规则应放在哪个披露层，以及怎样积累诊断信号。&lt;a href=&quot;https://arxiv.org/html/2605.23904v2&quot;&gt;3&lt;/a&gt;&lt;a href=&quot;https://arxiv.org/abs/2605.27760v1&quot;&gt;4&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;SkillOpt 在固定目标模型与执行框架的条件下，从成功、失败轨迹提议修改，对补丁进行整理并约束编辑规模，再用选择数据决定是否接受。被拒绝的尝试也可以成为后续提议的参考。它的价值不在“多反思一遍”，而在于修改理由与接受理由不是同一份证据。&lt;/p&gt;

&lt;p&gt;SkillGrad 关注另一种常见失败：规则并非缺失，而是放错了位置。主文塞满长尾例外，会增加每次阅读的负担；重要前提只写在附件末尾，又可能根本没有被读到。将长期有效的主路径、条件性补充和脚本资源分层，需要同时考虑触发、披露和实际执行。&lt;/p&gt;

&lt;p&gt;以下比较的是同一候选更新。所有字段为教学表达，不是上述项目的共同接口。&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;决策&lt;/th&gt;
      &lt;th&gt;粗略做法&lt;/th&gt;
      &lt;th&gt;更可检查的做法&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;为什么修改&lt;/td&gt;
      &lt;td&gt;“这次失败了”&lt;/td&gt;
      &lt;td&gt;指出失败证据、责任对象与可检验假设&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;修改什么&lt;/td&gt;
      &lt;td&gt;重写整份技能&lt;/td&gt;
      &lt;td&gt;保留有效主路径，只改相关步骤或资源&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;放在哪里&lt;/td&gt;
      &lt;td&gt;全部追加到正文&lt;/td&gt;
      &lt;td&gt;按适用频率与执行依赖选择披露层&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;是否接受&lt;/td&gt;
      &lt;td&gt;生成器认为更清楚&lt;/td&gt;
      &lt;td&gt;同一预算下比较候选与原版本&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;如何解释结果&lt;/td&gt;
      &lt;td&gt;只展示最佳分数&lt;/td&gt;
      &lt;td&gt;同时记录修复、回归、成本和被拒候选&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;这里有两个独立预算：优化器寻找候选所花的成本，以及候选部署后完成任务的成本。更短的最终技能，可能来自很昂贵的搜索；廉价生成的规则，也可能让后续每次执行都增加大量检查。只报其中一本账，会让架构决策失真。&lt;/p&gt;

&lt;p&gt;完整评测方法见&lt;a href=&quot;/2026/07/22/skill-evidence/&quot;&gt;收益评测篇&lt;/a&gt;。在优化循环内部，最关键的一条是：选择集可以用于反复挑选候选，最终测试集应隔离。若把测试失败也继续反馈给优化器，这批任务就已经参与开发，需要新的独立测试来判断泛化。&lt;/p&gt;

&lt;h2 id=&quot;4-用一个例子检查整个控制过程&quot;&gt;4. 用一个例子检查整个控制过程&lt;/h2&gt;

&lt;p&gt;回到透视表失败。先冻结原始技能、工具版本和样本集，再提出两种假设：H1 是技能没有区分指标字段和维度字段；H2 是工具的新接口不再接受旧参数。两者对应不同修复对象，应先用最小测试区分。&lt;/p&gt;

&lt;p&gt;若相同参数通过兼容层能够正确执行，正文重写可能不是首要动作。若接口调用成功但参数语义始终错误，则可以提出技能候选：增加字段分类步骤、调整示例，或在生成后重新读取透视表对象检查布局。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/posts/2026-skills/learning-skills-02.svg&quot; alt=&quot;图 2：机制与执行流程&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;图 2：教学诊断流程。分支由测试证据决定，模型提出的解释只是假设。&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;假设候选 A 增加正确的字段说明，候选 B 增加文件保存后的对象检查。它们可以分别测试，也可以测试组合；组合结果不能由两个单独分数相加得到。额外检查可能修复错误，也可能因为工具不支持而引入新失败。&lt;/p&gt;

&lt;p&gt;验收时至少包含三类任务：原来出错的相似任务、不同列名或布局的新任务、原版本已经稳定完成的任务。第三类用于发现回归。只有第一类改善，说明问题被局部修复；对第二类的改善才支持迁移；三类都通过，仍然只适用于已覆盖的环境与需求。&lt;/p&gt;

&lt;p&gt;接受条件应在看候选成绩之前写清。可以选择“质量达到底线时优先降低成本”，也可以选择“允许有限成本增加以减少严重错误”。授权与数据安全等硬约束不参与平均分抵消。没有统一权重适用于所有产品，优化器不能从失败日志中自动推导业务取舍。&lt;/p&gt;

&lt;h2 id=&quot;5-从单次优化走向持续学习&quot;&gt;5. 从单次优化走向持续学习&lt;/h2&gt;

&lt;p&gt;单次任务修好之后，下一个问题是怎样让未来任务受益。Memento-Skills 把技能作为可写的外部程序知识，采用读取、执行、写回的循环；更新的不只是对话摘要，还可以包括技能文件与资源。&lt;a href=&quot;https://arxiv.org/abs/2603.18743v1&quot;&gt;5&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;这种做法能让经验持续影响后续任务，也带来新的风险：错误规则会被重复读取，相似技能互相挤占检索位置，某个高频任务的优化可能伤害低频需求。技能库增长只是状态变化，不能作为学习成功的指标。&lt;/p&gt;

&lt;p&gt;我会把快速记录与慢速发布分开。执行结束后先记录候选经验及其适用条件，允许同一批经验积累出足够证据；经过选择与回归后，再更新稳定技能快照。线上任务使用的快照应可追溯，实验组积累的学习状态也不能静默流入基线组。&lt;/p&gt;

&lt;p&gt;知识巩固还需要维护决策：新增技能、修改旧技能、拆分过宽的技能，或撤回已经失效的规则。选择应基于任务覆盖与行为证据。低频不等于无价值，两个描述相似的技能也可能对应不同权限和环境。具体库治理见&lt;a href=&quot;/2026/08/17/skill-library-governance/&quot;&gt;冲突治理与版本发布篇&lt;/a&gt;。&lt;/p&gt;

&lt;h3 id=&quot;更大的编辑范围需要更清楚的边界&quot;&gt;更大的编辑范围需要更清楚的边界&lt;/h3&gt;

&lt;p&gt;RRSI 将改进对象扩展到 Agent Harness，包括提示、控制流、工具与上下文管理。&lt;a href=&quot;https://arxiv.org/html/2609.24972v2&quot;&gt;6&lt;/a&gt; 它将候选提议与保留决策分开，并在选择中考虑质量、成本与约束。这里吸收的是 W39 研究底稿中的方法，不把历史材料当作本周新闻。&lt;/p&gt;

&lt;p&gt;这也提醒我们：若一次候选同时修改路由、技能与工具，最终涨分只能首先归给整个变更集合。要判断某个 Skill 的贡献，需要另做控制实验。held-out 或域外测试参与了搜索，就不再是独立的迁移证据。&lt;/p&gt;

&lt;p&gt;RRSI 并不等于单技能文本优化。本文将它放在边界讨论里，是为了说明控制器必须知道允许编辑的对象与范围，不能因为技术上能修改更多文件，就默认这些修改都应被自动保留。&lt;/p&gt;

&lt;h2 id=&quot;6-停止条件也是优化能力&quot;&gt;6. 停止条件也是优化能力&lt;/h2&gt;

&lt;p&gt;持续运行的优化器很容易把“还能提出建议”当成“值得继续”。实际上，当候选收益不稳定、剩余预算不足、诊断无法区分或环境持续变化时，继续搜索可能只是在追逐噪声。&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;应停止或转交的信号&lt;/th&gt;
      &lt;th&gt;下一步&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;修改效果小于测量不确定性&lt;/td&gt;
      &lt;td&gt;保留原版本，补样本或重设实验&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;反复修复同一小批任务&lt;/td&gt;
      &lt;td&gt;检查过拟合与任务切分&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;收益来自更多重试或权限扩大&lt;/td&gt;
      &lt;td&gt;重新比较预算与约束，不归因于文本改进&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;工具、模型或验收标准变动&lt;/td&gt;
      &lt;td&gt;建立新快照，重新界定实验&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;无法确认副作用是否发生&lt;/td&gt;
      &lt;td&gt;先核查状态，再考虑恢复&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;停止后要留下可复用的结论：哪些假设被否定，哪些补丁已经测过，什么证据仍然缺失。这样下一轮不会从相同的猜测重新开始。保留失败记录的目的，是提高下一次决策质量，而不是让每次用户请求都携带全部失败历史。&lt;/p&gt;

&lt;p&gt;在工程上，我会先实现一个很小的闭环：只允许修改一种技能资产，绑定一个固定验收器，保留完整补丁记录，能够拒绝候选并恢复原版本。等收益与失败都可解释后，再扩大编辑对象和自动化程度。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;持续改进最终留下的应该是更可靠的能力，以及知道这项能力何时有效的证据。&lt;/strong&gt; 文件数量、反思轮数和训练曲线都可以帮助观察过程，但它们不能替代未来任务上的质量、成本与约束表现。&lt;/p&gt;

&lt;h2 id=&quot;参考资料&quot;&gt;参考资料&lt;/h2&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/2603.25158v5&quot;&gt;Trace2Skill，arXiv:2603.25158v5&lt;/a&gt;：局部轨迹分析与候选合并。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/Qwen-Applications/Trace2Skill/blob/3d0b52a140f002a512930252b613c49048f7d5ac/released_skills/trace2skill-xlsx-35B-combined/SKILL.md&quot;&gt;Trace2Skill 固定技能产物&lt;/a&gt;：本文局部代码一致性案例的来源。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://arxiv.org/html/2605.23904v2&quot;&gt;SkillOpt，arXiv:2605.23904v2&lt;/a&gt;：受控更新与选择；&lt;a href=&quot;https://github.com/microsoft/SkillOpt/blob/79124b37e9a6371e13b753f8bcd7adb1e493ade1/skillopt/optimizer/skill.py&quot;&gt;固定编辑实现&lt;/a&gt;。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/2605.27760v1&quot;&gt;SkillGrad，arXiv:2605.27760v1&lt;/a&gt;：分层技能优化；&lt;a href=&quot;https://github.com/wwwhy725/SkillGrad/tree/9ecd0a633833a1cf21f6f94d8df42bcffaa66554&quot;&gt;固定实现&lt;/a&gt;。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/2603.18743v1&quot;&gt;Memento-Skills，arXiv:2603.18743v1&lt;/a&gt;：读取、执行与经验写回；&lt;a href=&quot;https://github.com/Memento-Teams/Memento-Skills&quot;&gt;作者仓库&lt;/a&gt;。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://arxiv.org/html/2609.24972v2&quot;&gt;RRSI，arXiv:2609.24972v2&lt;/a&gt;：更大编辑范围下的提议与保留；&lt;a href=&quot;https://github.com/google-research/rrsi/blob/be50316e1db05914068a973f322770ef08ed7ba1/rrsi/selection.py&quot;&gt;固定选择实现&lt;/a&gt;。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;资料以固定研究版本及 2026 年 9 月底稿为界，2026-10-07 合并修订。本文没有独立重跑模型训练或 benchmark；教学过程不代表上述方法已在同一个生产系统中联合验证。&lt;/p&gt;

</description>
			<pubDate>Wed, 07 Oct 2026 07:13:50 +0000</pubDate>
			<link>https://sarkerson.github.io/2026/06/27/learning-skills/</link>
			<guid isPermaLink="true">https://sarkerson.github.io/2026/06/27/learning-skills/</guid>
        
			<category>Agent</category>
        
			<category>Skills</category>
        
        
		</item>
    
		<item>
			<title>Agent Skills：从召回到编排，如何用对技能</title>
			<description>&lt;p&gt;用户上传一份销售表，要求清洗重复记录、保留公式，再生成汇报用的演示文稿。Agent 找到了表格技能和演示文稿技能，也按顺序调用了它们。最后交付的文件却丢了公式，幻灯片中的数字还是清洗前的版本。&lt;/p&gt;

&lt;p&gt;这里没有明显的“没找到工具”。问题出在几个很小的交界处：清洗要求没有进入技能选择条件，表格输出没有成为下游的唯一输入，执行器也没有确认收到的是哪个版本。&lt;strong&gt;技能选对了名字，任务仍然可能在交接中失败。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;本文把 Skill 理解为可复用的过程知识包：它可以包含适用条件、操作步骤、脚本、参考材料和验证方法。Harness 是承载模型、工具、状态与执行循环的宿主。对 C 端 Agent，我更关心的是一次请求中，哪些知识在什么时候进入模型，以及这些知识怎样转化为可检查的动作。&lt;/p&gt;

&lt;p&gt;沿着这个问题，SkillRouter、Graph-of-Skills、AgentSkillOS 和 GraSP 分别提供选择、依赖补全、节点交接与局部修复的思路。下文以同一个表格到演示文稿的教学案例串联它们。案例与组合架构由本文构造；研究事实与工程建议分别说明，实验不声称独立复现。&lt;/p&gt;

&lt;h2 id=&quot;1-先把用到了-skill拆开&quot;&gt;1. 先把“用到了 Skill”拆开&lt;/h2&gt;

&lt;p&gt;日志里出现一次 Skill 调用，信息量很有限。它可能只是发起了读取，工具随后报错；也可能读到了正文，却没有执行其中的关键检查。要定位开头的失败，需要把整个过程拆成可以关联的事件。&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;阶段&lt;/th&gt;
      &lt;th&gt;在案例中要回答的问题&lt;/th&gt;
      &lt;th&gt;应留下的证据&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;召回&lt;/td&gt;
      &lt;td&gt;候选是否覆盖清洗、公式保护、图表和演示文稿？&lt;/td&gt;
      &lt;td&gt;查询、候选集合与版本&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;选择&lt;/td&gt;
      &lt;td&gt;为什么采用这套能力组合？&lt;/td&gt;
      &lt;td&gt;选中项、适用条件、依赖关系&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;披露&lt;/td&gt;
      &lt;td&gt;哪份正文和哪些资源真正进入了执行上下文？&lt;/td&gt;
      &lt;td&gt;成功读取结果、内容指纹&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;执行&lt;/td&gt;
      &lt;td&gt;是否按要求处理公式与数据版本？&lt;/td&gt;
      &lt;td&gt;动作、输入输出与状态变化&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;验收&lt;/td&gt;
      &lt;td&gt;最终文件是否满足用户要求？&lt;/td&gt;
      &lt;td&gt;文件级检查和任务验收结果&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;这些事件有先后关联，但不一定由五个独立服务实现。小系统可以在一个循环里完成，关键是每个事件的含义清楚。例如“请求读取成功”与“材料内容到达模型”应能关联到同一次调用，不能只对终端文本搜索一个技能名称。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/posts/2026-skills/using-skills-01.svg&quot; alt=&quot;图 1：机制与执行流程&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;图 1：本文的执行事件图。反馈返回计划，不意味着每次失败都要重写技能正文。&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;对“保留公式”这样的条件，也要区分未知与失败。若系统没有保存文件差异，无法判断公式是否改变，就应记录证据不足。把未知直接当作成功，会让后续的技能学习建立在错误标签上。&lt;/p&gt;

&lt;h2 id=&quot;2-选择技能时需要比名字更多的信息&quot;&gt;2. 选择技能时，需要比名字更多的信息&lt;/h2&gt;

&lt;p&gt;大库中最容易混淆的往往不是毫不相关的技能，而是相邻能力。例如同为表格清洗，一份技能适合把所有内容导出成静态值，另一份会保留公式、格式与工作表关系。只按“清洗 Excel”检索，两者都可能排在前面。&lt;/p&gt;

&lt;p&gt;SkillRouter 使用检索与重排两阶段，并将技能正文中的能力细节引入路由判断。&lt;a href=&quot;https://arxiv.org/abs/2603.22455v5&quot;&gt;1&lt;/a&gt; 它的启发是：&lt;strong&gt;路由模型需要理解的材料，可以比最终执行模型首轮携带的材料多。&lt;/strong&gt; 离线索引与在线重排负责辨别能力，执行器只读取此次真正采用的部分。全文参与路由并不等于将全库全文塞进用户会话。&lt;/p&gt;

&lt;p&gt;这种方法仍有输入限制。字段截断会影响尾部条件，训练中的相似负例也可能其实是有效替代方案。因此，“保留公式”“只能本地处理”“支持哪个工具版本”不能长期藏在正文的偶然位置。我的工程建议是把这些约束同步到可检查的元数据，同时保留正文作为判断依据。&lt;/p&gt;

&lt;p&gt;可以把候选筛选分成两层：先排除权限、环境和输出格式明确不满足要求的项，再比较剩余方案的相关性与成本。硬条件不能被一个很高的语义相似度抵消。没有足够证据证明候选适用时，回退到直接工具执行或向用户澄清，也是一种合理结果。&lt;/p&gt;

&lt;h3 id=&quot;依赖召回解决的是另一个缺口&quot;&gt;依赖召回解决的是另一个缺口&lt;/h3&gt;

&lt;p&gt;找到演示文稿技能，不代表找到了它所依赖的图表生成能力。Graph-of-Skills 将技能关系用于结构检索，让查询能够沿依赖扩展到单纯语义相似度可能遗漏的前置能力。&lt;a href=&quot;https://arxiv.org/abs/2604.05333v4&quot;&gt;2&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;这与“再多取几个 Top K”不同。增加 K 只是扩大邻近候选，依赖扩展试图回答：已经选中 B，为了让 B 的前置条件成立，还需要哪个 A？在表格案例中，这条边应该具体到“图表生成消费已验证的数据表，演示文稿消费图表与解释”，而非只有两个技能的名称相邻。&lt;/p&gt;

&lt;p&gt;关系图也可能放大错误。一个过期依赖会稳定召回错误版本；把常见共现当作必要前置，会给所有任务增加多余步骤。关系至少需要记录适用条件、来源与版本。某条边来自人工规则、文本推断还是执行验证，应当能被区分。&lt;/p&gt;

&lt;h2 id=&quot;3-有了执行图还需要定义信息交接&quot;&gt;3. 有了执行图，还需要定义信息交接&lt;/h2&gt;

&lt;p&gt;AgentSkillOS 将技能组织、选择与 DAG 执行结合起来。最值得看的一处实现，是规划器与节点执行器接收的材料不同：规划阶段可以看到选中技能的名称、描述和正文；执行阶段按节点构造任务，携带目标、产物路径、使用提示与输出要求。&lt;a href=&quot;https://arxiv.org/abs/2603.02176v1&quot;&gt;3&lt;/a&gt;&lt;a href=&quot;https://github.com/ynulihao/AgentSkillOS/tree/c3cfae10ea34a13973c461273ad9c5cc4aa907fe&quot;&gt;4&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;规划器读过的信息，不会自动成为下游执行器的历史。&lt;/strong&gt; 在新的会话里，产物路径、使用方法和预期输出都需要显式传递。反过来，如果执行器继承了全部旧会话，它还可能同时看到过期文件、失败尝试与已经撤销的计划。&lt;/p&gt;

&lt;p&gt;下面比较的是同一个时刻：表格清洗完成，准备生成演示文稿。它是依据节点交接思路构造的教学示例，不是一次真实 API 请求记录。&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;共享长历史：
用户需求、两份技能全文、所有清洗尝试、旧图表路径、
工具错误、修正说明、当前要求“继续生成演示文稿”。

按节点交接：
目标：生成销售复盘演示文稿，保留数据口径。
权威输入：cleaned.xlsx，版本与内容指纹已记录。
补充材料：validation.json、charts/、口径说明。
使用要求：从 cleaned.xlsx 取数，不使用旧草稿。
预期输出：report.pptx，并给出逐页数据来源。
下一步：执行器按需读取材料，再执行生成与验收。
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;路径本身不是上下文。只有实际读取之后，文件内容才进入模型可用材料。交接字段也不是越多越好：可以直接检查的机器状态留在运行状态里，模型需要解释和决策的部分再进入上下文。把完整审计日志复制给每个节点，既增加成本，也让过时信息重新参与判断。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/posts/2026-skills/using-skills-02.svg&quot; alt=&quot;图 2：机制与执行流程&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;图 2：本文建议的产物交接。箭头表达依赖，访问权限仍由宿主独立控制。&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;AgentSkillOS 的作者实验包含 30 个产物型任务，即使提供相同的理想技能集合，结构化编排也表现出优势。&lt;a href=&quot;https://arxiv.org/abs/2603.02176v1&quot;&gt;3&lt;/a&gt; 但其成对偏好与排序指标不能改写成任务准确率；整套系统的改善也不能全部归给“新建会话”。规划质量、交接方式、重试与材料组织共同变化，需要另外做消融。&lt;/p&gt;

&lt;p&gt;还有一个容易忽略的边界：会话隔离不等于文件隔离。源码中的节点可以共享工作目录，交接材料也不必只来自直接父节点。&lt;a href=&quot;https://github.com/ynulihao/AgentSkillOS/tree/c3cfae10ea34a13973c461273ad9c5cc4aa907fe&quot;&gt;4&lt;/a&gt; 如果要实现最小权限，还需要限制节点可访问的路径与工具，不能从 DAG 图形直接推出安全保证。&lt;/p&gt;

&lt;h2 id=&quot;4-执行中应修复哪一部分&quot;&gt;4. 执行中应修复哪一部分&lt;/h2&gt;

&lt;p&gt;执行图适合表达已知依赖，但真实工具调用会产生新信息。清洗结果可能提示表中存在不可自动合并的重复记录；图表生成可能发现缺少时间字段。此时继续照原计划走，或直接重跑全部流程，都可能浪费成本。&lt;/p&gt;

&lt;p&gt;GraSP 将技能组合表达为可检查的图，并把失败映射到局部修复。&lt;a href=&quot;https://arxiv.org/abs/2604.17870v1&quot;&gt;5&lt;/a&gt; 对框架的启发是：先保留已经可靠完成的状态，定位哪个节点、哪条依赖或哪个条件失效，再决定补节点、换方法、修参数或停止。验证对象包括计划结构与执行结果，图在结构上合法并不保证任务能完成。&lt;/p&gt;

&lt;p&gt;以表格案例为例，若清洗已经正确完成，只是图表缺少单位，应修复图表或补充口径，没必要再次删除重复行。如果原因是上游误删数据，则必须使受影响的下游产物失效，再从可靠版本重算。这里需要的是数据依赖和失效传播，不只是让模型写一句“我将重新检查”。&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;观察到的失败&lt;/th&gt;
      &lt;th&gt;优先检查&lt;/th&gt;
      &lt;th&gt;合理动作&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;找到了同名能力，但不支持公式&lt;/td&gt;
      &lt;td&gt;候选适用条件&lt;/td&gt;
      &lt;td&gt;换技能或直接执行，记录路由缺口&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;下游使用旧文件&lt;/td&gt;
      &lt;td&gt;交接与版本绑定&lt;/td&gt;
      &lt;td&gt;更新交接，撤销受影响产物&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;环境暂时不可用&lt;/td&gt;
      &lt;td&gt;工具状态与重试安全性&lt;/td&gt;
      &lt;td&gt;有限重试或暂停&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;规则明确但反复未执行&lt;/td&gt;
      &lt;td&gt;披露方式、执行步骤与验证&lt;/td&gt;
      &lt;td&gt;补局部检查，必要时提出技能候选修订&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;用户要求本身不明确&lt;/td&gt;
      &lt;td&gt;缺失约束&lt;/td&gt;
      &lt;td&gt;澄清，不自行扩大任务&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;这张表是诊断顺序，不是自动判因器。同一个表象可能有多个原因。框架应把观察、假设和后续验证分开记录，保留“证据不足”，避免一次失败立即触发全库文本优化。&lt;/p&gt;

&lt;h2 id=&quot;5-a-调用-b怎样判断组合值得存在&quot;&gt;5. A 调用 B，怎样判断组合值得存在&lt;/h2&gt;

&lt;p&gt;很多关系图来自日志共现：A 后面经常出现 B，于是建立 A→B。这个证据太弱。两者可能只是都在困难任务中频繁出现，也可能重复完成同一件事。是否存在组合增益，需要在具体任务分布与预算下验证。&lt;/p&gt;

&lt;p&gt;我的建议是为重要组合至少保留三个参照：只用 B；在同样约束下使用 A 再用 B；不给 A 额外知识、只增加相近计算预算的直接执行方案。若 A 的贡献是生成中间文件，还应检查 B 实际使用了该文件，以及换成过期或错误产物时结果如何变化。&lt;/p&gt;

&lt;p&gt;对开头的例子，“清洗→图表→演示文稿”可能合理；但“所有表格任务都先跑一次清洗”就不合理。用户只要求调整字体时，清洗会引入不必要的数据风险。边的适用域应该随任务条件收窄，而不是随着成功次数增加而无限泛化。&lt;/p&gt;

&lt;p&gt;关系也要区分必要依赖与可选帮助：前者不满足就无法执行，后者只是可能提升质量。二者放在同一无标签图里，会让规划器把所有有帮助的步骤都当作必做步骤，技能数量和成本随之膨胀。&lt;/p&gt;

&lt;h2 id=&quot;6-一个可以逐步落地的编排器&quot;&gt;6. 一个可以逐步落地的编排器&lt;/h2&gt;

&lt;p&gt;对已有 C 端框架，我会先增加可追溯性，再增加自动规划自由度。最初只支持少数稳定组合，把一次请求的选择、材料和产物说清楚，通常比先建设庞大的关系学习系统更有价值。&lt;/p&gt;

&lt;p&gt;一个节点任务至少应记录以下内容。它是本文的参考契约，字段名称不代表上述项目的统一标准。&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;node&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;make_slides&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;skill&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;pptx@固定版本&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;goal&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;生成销售复盘演示文稿&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;inputs&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;artifact&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;cleaned.xlsx&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;producer&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;clean_sheet&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;digest&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;已记录的内容指纹&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;constraints&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;只采用已验证数据&lt;/span&gt;
  &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;不对外发送文件&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;outputs&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;report.pptx&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;checks&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;页面数据与输入一致&lt;/span&gt;
  &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;关键口径有来源&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;budget&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;retries&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;预先设定的有限次数&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;在这个基础上，先测试四类故障：候选不适用、读取失败、交接错版本、下游验收失败。每一种都应能从日志找到最早出现问题的位置。再比较直接执行、扁平技能选择与结构化编排的质量和全部成本，详细协议见&lt;a href=&quot;/2026/07/22/skill-evidence/&quot;&gt;收益评测篇&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;当这些边界清楚之后，再引入关系检索、更多动态节点与自动修复。它们解决的是规模和适应性问题；若基本事件还无法追踪，更多自治只会增加解释失败的难度。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;我判断一个编排系统是否成熟，会看它能否回答三句话：这一步为什么需要这个技能，它实际看到了什么，下一步凭什么信任它的产物。&lt;/strong&gt; 这三件事能解释清楚，才有条件讨论更复杂的能力组合。&lt;/p&gt;

&lt;h2 id=&quot;参考资料&quot;&gt;参考资料&lt;/h2&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/2603.22455v5&quot;&gt;SkillRouter，arXiv:2603.22455v5&lt;/a&gt;：技能检索与重排；&lt;a href=&quot;https://github.com/zhengyanzhao1997/SkillRouter&quot;&gt;作者实现&lt;/a&gt;。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/2604.05333v4&quot;&gt;Graph-of-Skills，arXiv:2604.05333v4&lt;/a&gt;：依赖感知的结构检索。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/2603.02176v1&quot;&gt;AgentSkillOS，arXiv:2603.02176v1&lt;/a&gt;：组织、编排与产物型任务评测。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/ynulihao/AgentSkillOS/tree/c3cfae10ea34a13973c461273ad9c5cc4aa907fe&quot;&gt;AgentSkillOS 固定源码&lt;/a&gt;：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;src/orchestrator/dag/engine.py&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;prompts.py&lt;/code&gt; 与 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;runtime/client.py&lt;/code&gt;；源码静态观察不代表独立运行结果。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/2604.17870v1&quot;&gt;GraSP，arXiv:2604.17870v1&lt;/a&gt;：图结构技能组合与修复。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;资料以所列固定版本及 2026 年 9 月研究底稿为界，2026-10-07 合并修订。后续版本变化不自动适用于文中结论。&lt;/p&gt;

</description>
			<pubDate>Wed, 07 Oct 2026 07:13:50 +0000</pubDate>
			<link>https://sarkerson.github.io/2026/06/01/using-skills/</link>
			<guid isPermaLink="true">https://sarkerson.github.io/2026/06/01/using-skills/</guid>
        
			<category>Agent</category>
        
			<category>Skills</category>
        
        
		</item>
    
		<item>
			<title>领域驱动设计：战术建模与代码组织</title>
			<description>&lt;h1 id=&quot;目标&quot;&gt;目标&lt;/h1&gt;
&lt;blockquote&gt;
  &lt;p&gt;文本偏战术设计，目标有以下两点：（对战略设计感兴趣的话，可以参考文末的拓展阅读）&lt;/p&gt;

  &lt;ol&gt;
    &lt;li&gt;了解 DDD：你可以了解 DDD 概念，并看懂大部分 DDD 的项目代码；&lt;/li&gt;
    &lt;li&gt;落地 DDD：你可以了解如何用 DDD 思想来建模、落地项目，并了解所需注意的事项与规范；&lt;/li&gt;
  &lt;/ol&gt;

&lt;/blockquote&gt;

&lt;h1 id=&quot;前言&quot;&gt;前言&lt;/h1&gt;
&lt;p&gt;Eric Evans 于 2003 年出版了《领域驱动设计：软件核心复杂性应对之道》，在书中他创造了领域驱动设计方法。是“领域驱动“领域的指明灯。&lt;/p&gt;

&lt;p&gt;Vaughn Vernon 于 2014 年出版了《实现领域驱动设计》分别从战略和战术层面详尽地讨论了如何实现 DDD，其中包含了大量的最佳实践、设计准则和对一些问题的折中性讨论。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DDD 的作用域：&lt;/strong&gt;DDD 可以是单服务内，也可以是多个服务组成（微服务化）的系统范围内。&lt;/p&gt;

&lt;p&gt;微服务化不可回避两个问题：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;如何划分服务？（识别界限上下文）&lt;/li&gt;
  &lt;li&gt;微服务内部如何组织子模块，如何高效应对业务发展？（以领域为核心的分层架构）&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;微服务与 DDD，在解决复杂业务问题时，采用了相同的指导思想，即分而治之。分治的手段有优雅也有不优雅，DDD 是一种分治的指导思想。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DDD 的目标&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;在做架构设计时，一个好的架构应该需要实现以下几个目标：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;独立于框架：&lt;/strong&gt;架构不应该依赖某个外部的库或框架，不应该被框架的结构所束缚（可以轻松从 kite 切换成 kitex）。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;独立于UI：&lt;/strong&gt;前台展示的样式可能会随时发生变化（今天可能是网页、明天可能变成 console、后天是独立 app），但是底层架构不应该随之而变化。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;独立于底层数据源：&lt;/strong&gt;无论今天你用 MySQL、Oracle 还是 MongoDB、TiDB，甚至使用文件系统，软件架构不应该因为不同的底层数据储存方式而产生巨大改变。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;独立于外部依赖：&lt;/strong&gt;无论外部依赖如何变更、升级，业务的核心逻辑不应该随之而大幅变化。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;可测试：&lt;/strong&gt;无论外部依赖了什么数据库、硬件、UI或者服务，业务的逻辑应该都能够快速被验证正确性。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;DDD 正是服务于上述目标的一个方法论。&lt;/p&gt;

&lt;p&gt;由于 DDD 不是一套框架，而是一种架构思想，所以在代码层面缺乏了足够的约束，导致 DDD 在实际应用中上手门槛很高，甚至可以说绝大部分人都对 DDD 的理解有所偏差。&lt;/p&gt;

&lt;p&gt;我对 DDD 的理解也可能是有偏差的，但这个可能偏差的理解，带着我回答了文章开头提到的问题，所以这个有偏差的理解也值得你一起来探讨。下文总结自业界多种实践沉淀下来的方法论，让你对 DDD 的架构、各层级的职责及约束有个认知，降低 DDD 的实践门槛。期望通过下文描述，可以让你以 DDD 思想，参与 DDD 项目的具体开发中。&lt;/p&gt;

&lt;h1 id=&quot;概念&quot;&gt;概念&lt;/h1&gt;
&lt;h2 id=&quot;案例&quot;&gt;案例&lt;/h2&gt;
&lt;p&gt;这里想通过一个案例，让你对 DDD 思想中涉及的概念有个宏观上的认知。&lt;/p&gt;

&lt;p&gt;需求：&lt;/p&gt;

&lt;p&gt;用户可以通过银行网页转账给另一个账号，支持跨币种转账。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://obsidian-1254275759.cos.ap-shanghai.myqcloud.com/202511102110777.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;我们可以看到，一段业务代码里经常包含了参数校验、数据读取存储、业务计算、调用外部服务等多种逻辑。在这个案例里虽然是写在了同一个方法里，在真实代码中经常会被拆分成多个子方法，但实际效果是一样的，而在我们日常的工作中，绝大部分代码都或多或少的接近于此类结构。在 Martin Fowler 的 P of EAA 书中，这种很常见的代码样式被叫做 Transaction Script（事务脚本）。虽然这种类似于脚本的写法在功能上没有什么问题，但是长久来看，他有以下几个很大的问题：&lt;strong&gt;可维护性差、可扩展性差、可测试性差&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;考虑以下场景：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;突然又来了一个同币种转账的需求，上述逻辑是不是又有复制一份？&lt;/li&gt;
  &lt;li&gt;又有另外一个场景，涉及到转账，上述逻辑又得复制一份？&lt;/li&gt;
  &lt;li&gt;有一天，转账逻辑变更了，例如需要做合规检测，每个场景是不是都要修改？&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;下面是经过 DDD 思想重构后的代码：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://obsidian-1254275759.cos.ap-shanghai.myqcloud.com/202511102110832.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;相比之下：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;应用层主要做编排，并且大概率这个编排逻辑是很少变动的；&lt;/li&gt;
  &lt;li&gt;业务逻辑沉淀到了领域服务中，一处改动，多处收益；&lt;/li&gt;
  &lt;li&gt;每个模块都是可测试的；&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;实体entity&quot;&gt;实体（Entity）&lt;/h2&gt;
&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;定义：&lt;/strong&gt;有唯一标识，能表示一个业务的生命周期的对象。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;组成：&lt;/strong&gt;实体由两部分组成：领域属性+领域能力
    &lt;ol&gt;
      &lt;li&gt;领域属性：对实体的描述，是实体的一部分。领域属性又可以分为，简单属性（string/int64等）和复杂属性（结构体，称为值对象）。&lt;/li&gt;
      &lt;li&gt;领域能力：实体自己的职责范围，能做什么，也可以叫作「实体行为」。&lt;/li&gt;
    &lt;/ol&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;实体行为&lt;/strong&gt;：实体行为又称为领域能力，就是当前的业务对象能干什么事情，落地到代码上，就是实体的public方法。一般调用者都是application层。
    &lt;ol&gt;
      &lt;li&gt;原则：实体能自己干的事情，尽量自己干，不要交给聚合或领域服务，这样每个领域对象各司其职，把自己的行为做完整。然后和其他领域对象之间的边界职责又很清楚，这样的严格的组织，能容易地帮助业务实现高内聚。区别于过程式编码，需要考虑的东西会多很多，但会对长期业务发展带来好处。&lt;/li&gt;
    &lt;/ol&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;实体行为的粒度&lt;/strong&gt;：实体行为的颗粒度，只有写代码的时候才会真正的思考。
    &lt;ol&gt;
      &lt;li&gt;颗粒度大小：原则上我们要求行为是颗粒度是最细的一件事情。提倡一个实体行为只干一件事情，这件事情的颗粒度最好是最细的。这样的好处就是为了方便复用。一般反对在一个实体的行为上去做一件以上事情，当你这么做的，你会发现非常难取名，你的方法上需要有两个动词，这时候，我们就要拆了。
        &lt;ol&gt;
          &lt;li&gt;当我们的颗粒度很细的时候，application 层需要做很多编排工作，这时候，你可以通过领域服务的方式进行封装。&lt;/li&gt;
        &lt;/ol&gt;
      &lt;/li&gt;
    &lt;/ol&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;实体和其他元素的关系&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;img src=&quot;https://obsidian-1254275759.cos.ap-shanghai.myqcloud.com/202511102110840.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;值对象value-object&quot;&gt;值对象（Value Object）&lt;/h2&gt;
&lt;p&gt;值对象通常作为实体的属性而存在，比如封面图、简介、标题、时间等。实体是客观存在的事物，值对象是为了描述事物，抽象出来的概念。是否拥有唯一身份标识，是实体与值对象的本质区别。&lt;/p&gt;

&lt;p&gt;值对象也是可以有行为，可以进行沉淀。例如，图片，对应的主题色计算，是一种沉淀；电话号码，对应的有效性检测是一种沉淀；等等。&lt;/p&gt;

&lt;p&gt;IDL 设计，尽量使用值对象建模思想，而不是分散的属性，难以管理、复用、沉淀&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://obsidian-1254275759.cos.ap-shanghai.myqcloud.com/202511102110847.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;聚合aggregate&quot;&gt;聚合（Aggregate）&lt;/h2&gt;
&lt;p&gt;领域模型内的实体和值对象好比个体，而能让实体和值对象协同工作的组织就是聚合，用来确保这些领域对象在实现共同的业务逻辑时，能保证数据的一致性。&lt;/p&gt;

&lt;p&gt;场景：订单里面，包含商品、优惠券、邮费等属性，优惠券的存在，导致商品单价下降。当一个操作涉及多个实体操作时，并且存在一致性时，则需要把这些实体当作一个整体，这个整体叫做聚合。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://obsidian-1254275759.cos.ap-shanghai.myqcloud.com/202511102110856.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;聚合由业务和逻辑紧密关联的实体和值对象组合而成，是&lt;strong&gt;数据修改和持久化的基本单元&lt;/strong&gt;，每个聚合对应一个仓储，实现数据的持久化。&lt;/p&gt;

&lt;p&gt;内容消费领域，读取、打包是常见的例子。datum 就是一种聚合，通过 loader 来加载 datum，datum 有自己的领域行为，例如校验、过滤等等。&lt;/p&gt;

&lt;h2 id=&quot;仓储repository&quot;&gt;仓储（Repository）&lt;/h2&gt;
&lt;p&gt;仓储负责聚合的「增删改查」操作，一个聚合（实体是一种特殊的聚合）对应一个仓储。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repository VS DAO？&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;DAO 下，业务代码为数据层服务。DAO 操作的是数据库对象，基于此思想编写的代码，自然你的领域逻辑中就会包含数据库的读写逻辑、数据对象到领域对象的转换操作，会使得业务代码与数据库逻辑强耦合。&lt;/p&gt;

&lt;p&gt;Repository 下，数据层为业务代码服务。Repository 的出入参是领域对象，领域层只包含领域对象，不包含仓储层对象及逻辑。因此，一旦要换底层存储，则再编写一个 Repository 接口实现就好了。&lt;/p&gt;

&lt;p&gt;区分好领域模型、数据模型。&lt;/p&gt;

&lt;h2 id=&quot;领域服务domain-service&quot;&gt;领域服务（Domain Service）&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://obsidian-1254275759.cos.ap-shanghai.myqcloud.com/202511102110865.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;当一个自治的聚合无法完成一个完整的业务场景，需要共同协作完成时，可以引入领域服务来封装多个聚合的协作行为。&lt;/p&gt;

&lt;p&gt;另一种场景是，如果实体的相关行为，需要引入仓储，那么也可以封装领域服务来解决。&lt;/p&gt;

&lt;p&gt;封装为领域服务的目的是，如果有第二个场景，需要用到这一个或多个聚合的协作行为时，可以直接复用，即实现领域知识的沉淀。&lt;/p&gt;

&lt;h1 id=&quot;分层&quot;&gt;分层&lt;/h1&gt;
&lt;h2 id=&quot;传统分层架构&quot;&gt;传统分层架构&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://obsidian-1254275759.cos.ap-shanghai.myqcloud.com/202511102110877.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;分层架构模式被认为是所有架构的始祖，被广泛地应用于Web、企业级应用和桌面应用。在这种架构中，我们将一个应用程序或者系统分为不同的层次。&lt;/p&gt;

&lt;p&gt;分层架构的一个重要原则是：每层只能与位于其下方的层发生耦合。分层架构也分为几种：在严格分层架构中，某层只能与直接位于其下方的层发生耦合；而松散分层架构则允许任意上方层与任意下方层发生耦合。由于用户界面层和应用服务通常需要与基础设施打交道，&lt;strong&gt;许多系统都是基于松散分层架构&lt;/strong&gt;的。&lt;/p&gt;

&lt;h2 id=&quot;使用依赖倒置的分层架构&quot;&gt;使用依赖倒置的分层架构&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://obsidian-1254275759.cos.ap-shanghai.myqcloud.com/202511102110885.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;然而，在传统的分层架构中，却存在着一些问题，因为领域层或多或少地需要使用基础设施层，即领域层中的有些接口实现依赖于基础设施层。这使得业务规则和数据存储的代码耦合在一起。&lt;/p&gt;

&lt;p&gt;在书籍《实现领域驱动设计》中，Vernon 提出了基于依赖倒置的 DDD 分层架构，来改进传统分层架构。全书提到的 DDD 架构，不额外说明的情况下，都是基于该思想作为具体落地实现（具体来说是用六边形架构）。&lt;/p&gt;

&lt;p&gt;依赖倒置原则（Dependency Inversion Principle，DIP），由 Robert C. Martin 于 1996 年提出，其定义如下：&lt;/p&gt;

&lt;font style=&quot;background-color:rgb(255,245,235);&quot;&gt;高层模块不应该依赖于低层模块，两者都应该依赖于抽象。抽象不应该依赖于细节，细节应该依赖于抽象。&lt;/font&gt;

&lt;p&gt;该原则的提出具备重大意义，后面提到的六边形架构、洋葱架构、整洁架构，都是基于依赖倒置的分层架构的变种，没有实质上改变。&lt;/p&gt;

&lt;h2 id=&quot;用户接口层&quot;&gt;&lt;strong&gt;用户接口层&lt;/strong&gt;&lt;/h2&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;职责&lt;/th&gt;
      &lt;th&gt;即常见的 Hanlder。用户接口层负责向用户显示信息和解释用户指令。这里的用户可能是：用户、程序、自动化测试和批处理脚本等等。&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;注意&lt;/td&gt;
      &lt;td&gt;如果用户界面使用了领域模型中的对象，那么此时的领域对象仅限于数据的渲染展现。在采用这种方式时，可以使用展现模型（Presentation Model，14）对用户界面与领域对象进行解耦。&lt;br /&gt;否则，领域对象修改，可能会导致展示层变化；或者展示层逻辑入侵领域层。&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;输入输出&lt;/td&gt;
      &lt;td&gt;+ 输入：用户请求&lt;br /&gt;+ 输出：展示层对象&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;核心组件&lt;/td&gt;
      &lt;td&gt;assembler：对应用层返回的 DTO 做适配，返回不同前端所需数据&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;规范&lt;/td&gt;
      &lt;td&gt;请求对象是有业务“语意”的，尽量避免复用，哪怕参数是一样的。&lt;br /&gt;即，每个接口 req/resp 的 IDL 定义不复用。&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;strong&gt;DTO 是什么？&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://obsidian-1254275759.cos.ap-shanghai.myqcloud.com/202511102110893.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DTO Assembler：在Application层&lt;/strong&gt;，Entity 到 DTO 的转化器有一个标准的名称叫 DTO Assembler。Martin Fowler 在 P of EAA 一书里对于 DTO 和 Assembler 的描述：Data Transfer Object。DTO Assembler 的核心作用就是将一个或多个相关联的 Entity 转化为一个或多个 DTO。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data Converter：在 Infrastructure 层&lt;/strong&gt;，Entity 到 DO 的转化器没有一个标准名称，但是为了区分 Data Mapper，我们叫这种转化器 Data Converter。这里要注意 Data Mapper 通常情况下指的是 DAO。&lt;/p&gt;

&lt;h2 id=&quot;应用层&quot;&gt;&lt;strong&gt;应用层&lt;/strong&gt;&lt;/h2&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;职责&lt;/th&gt;
      &lt;th&gt;应用层是很薄的一层，理论上不应该有业务规则或逻辑，主要面向用例和流程相关的操作。但应用层又位于领域层之上，因为领域层包含多个聚合，所以它可以协调多个聚合的服务和领域对象完成&lt;strong&gt;服务编排和组合&lt;/strong&gt;，协作完成业务操作。&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;注意&lt;/td&gt;
      &lt;td&gt;在设计和开发时，不要将本该放在领域层的业务逻辑放到应用层中实现。因为庞大的应用层会使领域模型失焦，时间一长你的微服务就会演化为传统的三层架构，业务逻辑会变得分散、难以维护。&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;输入输出&lt;/td&gt;
      &lt;td&gt;+ 输入：用户请求&lt;br /&gt;+ 输出：DTO&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;核心组件&lt;/td&gt;
      &lt;td&gt;+ assembler：对领域层、repo 层返回的 entity、aggr 做组装、适配，这个中间对象叫做 DTO&lt;br /&gt;+ app_service：若编排在多个场景使用，则可以封装为 app_service 供多个 handler 调用&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;规范&lt;/td&gt;
      &lt;td&gt;应用服务只负责业务流程串联，不负责业务逻辑。业务逻辑内聚到 domain 实现。&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;strong&gt;常用的ApplicationService“套路”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;我们可以看出来，ApplicationService 的代码通常有类似的结构：AppService 通常不做任何决策，仅仅是把所有决策交给 DomainService 或 Entity，把跟外部交互的交给 Infrastructure 接口，如 Repository 或防腐层。&lt;/p&gt;

&lt;p&gt;一般的“套路”如下：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;准备数据：包括从外部服务或持久化源取出相对应的 Entity、Aggr 以及外部服务返回的 DTO。&lt;/li&gt;
  &lt;li&gt;执行操作：包括新对象的创建、赋值，以及调用领域对象的方法对其进行操作。需要注意的是这个时候通常都是纯内存操作，非持久化。&lt;/li&gt;
  &lt;li&gt;持久化：将操作结果持久化，或操作外部系统产生相应的影响，包括发消息等异步操作。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;领域层&quot;&gt;&lt;strong&gt;领域层&lt;/strong&gt;&lt;/h2&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;职责&lt;/th&gt;
      &lt;th&gt;领域层的作用是实现企业核心业务逻辑，通过各种校验手段保证业务的正确性。领域层主要体现领域模型的业务能力，它用来表达业务概念、业务状态和业务规则。&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;注意&lt;/td&gt;
      &lt;td&gt;领域层的对当前系统的依赖只有领域层&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;核心组件&lt;/td&gt;
      &lt;td&gt;+ 聚合根、实体、值对象、领域服务&lt;br /&gt;+ 仓储接口&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;规范&lt;/td&gt;
      &lt;td&gt;+ 对依赖进行抽象接口设计（repo interface），使得业务逻辑和技术实现是相互隔离的。&lt;br /&gt;+ entity 只负责内存操作，不负责数据的存储。存储交给 inf/repo 层实现。&lt;br /&gt;+ 当操作涉及两个或以上的 entity 时，应该使用聚合根，放在 domain/aggr 目录下&lt;br /&gt;+ 当重要的逻辑无法挂到 entity、aggr 上来实现时，或者需要引入 repo，可以考虑构建领域服务，放在 domain/service 目录下。&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;img src=&quot;https://obsidian-1254275759.cos.ap-shanghai.myqcloud.com/202511102110903.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;传统开发方法中，是面向数据开发，即在业务逻辑中适配数据库的数据。在 DDD 思想中，领域层、应用层需要什么数据，约定好接口即可，交给 infra 层来实现。&lt;/p&gt;

&lt;p&gt;在传统架构设计中，由于上层应用对数据库的强耦合，很多公司在架构演进中最担忧的可能就是换数据库了，因为一旦更换数据库，就可能需要重写大部分的代码，这对应用来说是致命的。那采用依赖倒置的设计以后，应用层就可以通过解耦来保持独立的核心业务逻辑。当数据库变更时，我们只需要更换 repository 实现就可以了，这样就将资源变更对应用的影响降到了最低。&lt;/p&gt;

&lt;h2 id=&quot;基础层&quot;&gt;&lt;strong&gt;基础层&lt;/strong&gt;&lt;/h2&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;职责&lt;/th&gt;
      &lt;th&gt;基础层是贯穿所有层的，它的作用就是为其它各层提供通用的技术和基础服务，包括第三方工具、驱动、消息中间件、网关、文件、缓存以及数据库等。比较常见的功能还是提供数据库持久化。&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;注意&lt;/td&gt;
      &lt;td&gt;基础层包含基础服务，它采用依赖倒置设计，封装基础资源服务，实现应用层、领域层与基础层的解耦，降低外部资源变化对应用的影响。&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;输入输出&lt;/td&gt;
      &lt;td&gt;+ 输入/输出：通常是原始数据类型、领域对象&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;核心组件&lt;/td&gt;
      &lt;td&gt;+ repo：实现领域层、应用层约定的 interface&lt;br /&gt;+ dependency：service/db/cache 等所有外部依赖&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;规范&lt;/td&gt;
      &lt;td&gt;repo 的入参和出参除了原始数据类型，只能包含领域对象&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h2 id=&quot;小结&quot;&gt;小结&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://obsidian-1254275759.cos.ap-shanghai.myqcloud.com/202511102110914.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;在传统架构中，代码从上到下的变化速度基本上是一致的，改个需求需要从接口、到业务逻辑、到数据库全量变更，而第三方变更可能会导致核心业务代码的重写。但是在 DDD 中不同模块的代码的演进速度是不一样的：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Domain 层属于核心业务逻辑，属于经常被修改的地方。比如：原来不需要扣手续费，现在需要了之类的。通过 Entity 能够解决基于单个对象的逻辑变更，通过 Domain Service 解决多个对象间的业务逻辑变更。顺便你会发现，改了一个 domain 逻辑，所有的应用层都会受益。&lt;/li&gt;
  &lt;li&gt;Application 层属于 Use Case（业务用例）。业务用例一般都是描述比较大方向的需求，接口相对稳定，特别是对外的接口一般不会频繁变更。添加业务用例可以通过新增 Application Service 或者新增接口实现功能的扩展。&lt;/li&gt;
  &lt;li&gt;Infrastructure 层属于最低频变更的。一般这个层的模块只有在外部依赖变更了之后才会跟着升级，而外部依赖的变更频率一般远低于业务逻辑的变更频率。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;所以在 DDD 架构中，能明显看出越外层的代码越稳定，越内层的代码演进越快，真正体现了领域“驱动”的核心思想。&lt;/p&gt;

&lt;h1 id=&quot;架构选型&quot;&gt;架构选型&lt;/h1&gt;
&lt;h2 id=&quot;六边形架构&quot;&gt;六边形架构&lt;/h2&gt;
&lt;p&gt;https://medium.com/ssense-tech/hexagonal-architecture-there-are-always-two-sides-to-every-story-bc0780ed7d9c&lt;/p&gt;

&lt;p&gt;2005 年六边形架构被提出，比较鲜明的特点是将上下层结构换成同心圆结构，同心圆内层代表了应用的业务逻辑，外层代表应用的用户接口（driving-side）及外部资源（driven-side）。&lt;/p&gt;

&lt;p&gt;如右上图，红圈内的核心业务逻辑（应用程序和领域模型）与外部资源（包括应用的上游比如APP、Web 应用等，以及应用的下游比如数据库、缓存等）完全隔离，两者通过适配器进行交互，很好地实现了系统核心业务与外部依赖资源的解耦。通过适配器负责内层和外层的协议转换，使得系统核心业务能够以一致的方式被上游访问（不同的协议比如HTTPs、消息队列等，可以用不同适配器访问），也能适配不同的下游存储引擎。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://obsidian-1254275759.cos.ap-shanghai.myqcloud.com/202511102110926.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;洋葱架构&quot;&gt;洋葱架构&lt;/h2&gt;
&lt;p&gt;2008 年洋葱架构被提出。洋葱架构可以看作是六边形架构的衍生，两者有相同的思路，都主张将业务核心逻辑与外部依赖资源进行解耦，避免外部依赖代码渗透到业务核心逻辑中。此外，洋葱架构在业务逻辑中加入了一些在 DDD 分层概念，比如用户接口层、应用层、领域层和基础层。&lt;/p&gt;

&lt;p&gt;I propose a new approach to architecture. &lt;u&gt;Honestly, it’s not completely new, but I’m proposing it as a named, architectural pattern. &lt;/u&gt; Patterns are useful because it gives software professionals a common vocabulary with which to communicate. There are a lot of aspects to the Onion Architecture, and if we have a common term to describe this approach, we can communicate more effectively.&lt;/p&gt;

&lt;p&gt;原文：https://jeffreypalermo.com/2008/07/the-onion-architecture-part-1/&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://obsidian-1254275759.cos.ap-shanghai.myqcloud.com/202511102110935.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;整洁架构&quot;&gt;整洁架构&lt;/h2&gt;
&lt;p&gt;整洁架构，是 Robert C. Martin 在 2012 年提出的概念，本质上没有提出新的架构模式，而是整合了六边形架构、洋葱架构等架构模式，&lt;strong&gt;统一了命名及规范，让开发者可以使用统一的语言进行交流。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;u&gt;Though these architectures all vary somewhat in their details, they are very similar. &lt;/u&gt;They all have the same objective, which is the separation of concerns. They all achieve this separation by dividing the software into layers. Each has at least one layer for business rules, and another for interfaces.&lt;/p&gt;

&lt;p&gt;原文：https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html&lt;/p&gt;

&lt;p&gt;解析：https://betterprogramming.pub/the-clean-architecture-beginners-guide-e4b7058c1165&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;undefined&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;小结-1&quot;&gt;小结&lt;/h2&gt;
&lt;p&gt;基于依赖倒置原则的分层架构及其派生出来的各种架构模式，其思想是高度一致的。除了命名不一样、切入点不一样之外，其他的整体架构都是基于一个二维的内外关系。这也说明了基于DDD的架构最终的形态都是类似的。&lt;/p&gt;

&lt;p&gt;概括起来，有以下几个核心点：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;抽象不依赖细节，细节应该依赖抽象。&lt;/li&gt;
  &lt;li&gt;内层模块不感知外层模块的存在。&lt;/li&gt;
  &lt;li&gt;业务逻辑应该高度内聚在领域层。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;即使遵循上述架构模式，具体落地的时候，仍然有非常多的细节值得注意。&lt;/p&gt;

&lt;h1 id=&quot;总结&quot;&gt;总结&lt;/h1&gt;
&lt;p&gt;DDD 不是一个什么特殊的架构，而是任何传统代码经过合理的重构之后最终一定会抵达的终点。DDD 的架构能够有效的解决传统架构中的问题：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;高可维护性：当外部依赖变更时，内部代码只用变更跟外部对接的模块，其他业务逻辑不变。&lt;/li&gt;
  &lt;li&gt;高可扩展性：做新功能时，绝大部分的代码都能复用，仅需要增加核心业务逻辑即可。&lt;/li&gt;
  &lt;li&gt;高可测试性：每个拆分出来的模块都符合单一性原则，绝大部分不依赖框架，可以快速的单元测试，做到100%覆盖。&lt;/li&gt;
  &lt;li&gt;代码结构清晰：当团队形成规范后，可以快速的定位到相关代码。&lt;/li&gt;
&lt;/ul&gt;

&lt;h1 id=&quot;reference&quot;&gt;Reference&lt;/h1&gt;
&lt;ol&gt;
  &lt;li&gt;蓝皮书《领域驱动设计》Eric Evans&lt;/li&gt;
  &lt;li&gt;红皮书《实现领域驱动设计》Vaughn Vernon&lt;/li&gt;
  &lt;li&gt;优秀 DDD 博客
    &lt;ol&gt;
      &lt;li&gt;殷浩谈DDD系列（这系列强烈推荐，整体思路很清晰，实操性比较强）
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;https://mp.weixin.qq.com/s/kpXklmidsidZEiHNw57QAQ&quot;&gt;&lt;u&gt;殷浩详解DDD系列 第一讲 - Domain Primitive&lt;/u&gt;&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;https://mp.weixin.qq.com/s/MU1rqpQ1aA1p7OtXqVVwxQ&quot;&gt;&lt;u&gt;殷浩详解DDD系列 第二讲 - 应用架构&lt;/u&gt;&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;https://mp.weixin.qq.com/s/1bcymUcjCkOdvVygunShmw&quot;&gt;&lt;u&gt;殷浩详解DDD系列 第三讲 - Repository模式&lt;/u&gt;&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;https://mp.weixin.qq.com/s/w1zqhWGuDPsCayiOgfxk6w&quot;&gt;&lt;u&gt;殷浩详解DDD系列 第四讲 - 领域层设计规范&lt;/u&gt;&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;https://mp.weixin.qq.com/s/1rdnkROdcNw5ro4ct99SqQ&quot;&gt;&lt;u&gt;殷浩详解DDD系列 第五讲 - 聊聊如何避免写流水账代码&lt;/u&gt;&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ol&gt;
  &lt;/li&gt;
  &lt;li&gt;一些 DDD 资源汇总 https://github.com/evancyz/ddd-learning?tab=readme-ov-file&lt;/li&gt;
&lt;/ol&gt;

</description>
			<pubDate>Sat, 03 Feb 2024 00:00:00 +0000</pubDate>
			<link>https://sarkerson.github.io/2024/02/03/DDD-%E6%88%98%E6%9C%AF%E8%AE%BE%E8%AE%A1%E6%80%BB%E7%BB%93/</link>
			<guid isPermaLink="true">https://sarkerson.github.io/2024/02/03/DDD-%E6%88%98%E6%9C%AF%E8%AE%BE%E8%AE%A1%E6%80%BB%E7%BB%93/</guid>
        
			<category>database</category>
        
        
		</item>
    
		<item>
			<title>数据库：时序数据的存储与查询</title>
			<description>&lt;blockquote&gt;
  &lt;p&gt;TL;DR; 本文介绍常见时序数据库的基本架构，并以 InfluxDB 为例子介绍其存储模型及存储引擎的原理。最后介绍公司的 metrics 常见写入及读取操作。通过本文，你可以对时序数据库的原理有个初步了解，并可以对查询操作得心应手。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1 id=&quot;简介&quot;&gt;简介&lt;/h1&gt;
&lt;h2 id=&quot;基本概念&quot;&gt;基本概念&lt;/h2&gt;
&lt;p&gt;时序数据库是处理时序数据最优的数据库类型，而时序数据是随时间变化而被监控，跟踪，降采样，聚合的指标数据和事件。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://obsidian-1254275759.cos.ap-shanghai.myqcloud.com/202511102111984.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;为什么需要时序数据库&quot;&gt;为什么需要时序数据库？&lt;/h2&gt;
&lt;h3 id=&quot;特性&quot;&gt;特性&lt;/h3&gt;
&lt;p&gt;时序数据，区别于其他数据，拥有以下特征：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;数据随着时间增长，根据维度取值，而数据维度几乎不变。&lt;/li&gt;
  &lt;li&gt;持续高写入吞吐量，设备越多，写入数量越大，而且由于定期采样，写入量平稳。&lt;/li&gt;
  &lt;li&gt;持续高读取吞吐量。&lt;/li&gt;
  &lt;li&gt;几乎不会有更新操作（一个设备在某个时间点产生的数据不会变动）以及单独数据点的删除（通常只会删除过期时间范围内所有的数据）&lt;/li&gt;
  &lt;li&gt;查询一般都是查最近产生的数据，很少会去查询过期的数据。&lt;/li&gt;
  &lt;li&gt;设备之间的数据关联性小，同种类设备A和设备B产生的数据互相并不依赖，你并不需要join。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;面临的挑战&quot;&gt;面临的挑战&lt;/h3&gt;
&lt;ul&gt;
  &lt;li&gt;时序数据的写入：如何支持每秒钟上千万上亿数据点的写入。&lt;/li&gt;
  &lt;li&gt;时序数据的读取：如何支持在秒级对上亿数据的分组聚合运算。&lt;/li&gt;
  &lt;li&gt;成本敏感：由海量数据存储带来的是成本问题。如何更低成本的存储这些数据，将成为时序数据库需要解决的重中之重。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;应用场景&quot;&gt;应用场景&lt;/h3&gt;
&lt;p&gt;Metrics不仅仅可以用于软件开发中的监控指标大盘，还有以下应用场景：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;监控软件系统： 虚拟机、容器、服务、应用&lt;/li&gt;
  &lt;li&gt;监控物理系统： 设备、机器、接入的设备、环境、我们的房屋、我们的身体&lt;/li&gt;
  &lt;li&gt;资产跟踪应用： 汽车、卡车、物理容器、运货托盘（Pallets）&lt;/li&gt;
  &lt;li&gt;金融交易系统： 传统证券、新兴的加密数字货币&lt;/li&gt;
  &lt;li&gt;事件应用程序： 跟踪用户、客户的交互数据&lt;/li&gt;
  &lt;li&gt;商业智能工具： 跟踪关键指标和业务的总体健康情况&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;解决方案&quot;&gt;解决方案&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://obsidian-1254275759.cos.ap-shanghai.myqcloud.com/202511102111020.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;https://db-engines.com/en/ranking/time+series+dbms&lt;/p&gt;

&lt;h3 id=&quot;组件概览&quot;&gt;组件概览&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://obsidian-1254275759.cos.ap-shanghai.myqcloud.com/202511102111031.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h4 id=&quot;采集器&quot;&gt;采集器&lt;/h4&gt;
&lt;p&gt;采集器是用于采集时序数据的组件。它们通常是通过一些特定的协议或API获取数据，并将其发送到时序数据库中。&lt;/p&gt;

&lt;p&gt;通常会在服务器端部署 agent 来进行数据采集，然后传送给时序库。这里常见有推拉两种模式。推模式下，采集器会周期性地向时序数据库发送数据，实时性更高，且适合短作业，但是对写吞吐有较高要求；拉模式下，时序数据库会定期向采集器发起请求获取最新的数据，稳定性更高，但有延迟，通常只用于长作业。&lt;/p&gt;

&lt;h4 id=&quot;时序库&quot;&gt;时序库&lt;/h4&gt;
&lt;p&gt;监控系统的架构中，最核心的就是时序库。与传统的关系型数据库不同，时序数据库通常采用分布式架构和列式存储以支持高吞吐量和低延迟的数据写入和查询。常见的时序数据库包括InfluxDB、OpenTSDB、Prometheus等。它们通常提供了一些特殊的查询语言和API以便进行时序数据的查询和操作。&lt;/p&gt;

&lt;h4 id=&quot;告警引擎&quot;&gt;告警引擎&lt;/h4&gt;
&lt;p&gt;告警引擎的核心职责就是处理告警规则，生成告警事件。通常来讲，用户会配置数百甚至数千条告警规则，一些超大型的公司可能要配置数万条告警规则。每个规则里含有数据过滤条件、阈值、执行频率等，有一些配置丰富的监控系统，还支持配置规则生效时段、持续时长、留观时长等。&lt;/p&gt;

&lt;p&gt;当然，随着时代的发展，也有系统支持统计算法和机器学习的方式做告警预判。AiOps 概念中最容易落地，或者说落地之后最容易有效果的，就是告警引擎。&lt;/p&gt;

&lt;p&gt;告警引擎通常有两种架构，一种是数据触发式，一种是周期轮询式。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;数据触发式，是指服务端接收到监控数据之后，除了存储到时序库，还会转发一份数据给告警引擎，告警引擎每收到一条监控数据，就要判断是否关联了告警规则，做告警判断。因为监控数据量比较大，告警规则的量也可能比较大，所以告警引擎是会做分片部署的，即部署多个实例。这样的架构，即时性很好，但是想要做指标关联计算就很麻烦，因为不同的指标哈希后可能会落到不同的告警引擎实例。&lt;/li&gt;
  &lt;li&gt;周期轮询式，架构简单，通常是一个规则一个协程，按照用户配置的执行频率，周期性查询判断即可，因为是主动查询的，做指标关联计算就会很容易。像 Prometheus、Nightingale、Grafana 等，都是这样的架构。&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;数据展示&quot;&gt;数据展示&lt;/h4&gt;
&lt;p&gt;监控数据的可视化也是一个非常通用且重要的需求，业界做得最成功的当数 Grafana。Grafana 采用插件式架构，可以支持不同类型的数据源，图表非常丰富，基本可以看做是开源领域的事实标准。很多公司的商业化产品中，甚至直接内嵌了 Grafana。当然，Grafana 新版本已经修改了开源协议，使用 AGPLv3，这就意味着如果某公司的产品基于 Grafana 做了二次开发，就必须公开代码。&lt;/p&gt;

&lt;h3 id=&quot;业界方案&quot;&gt;业界方案&lt;/h3&gt;
&lt;h4 id=&quot;influxdata-tick-stack&quot;&gt;InfluxData TICK Stack&lt;/h4&gt;
&lt;p&gt;https://zhoujinl.github.io/2018/02/27/tick/&lt;/p&gt;

&lt;p&gt;TICK 是由 InfluxData 开发的一套运维工具栈，由 Telegraf, InfluxDB, Chronograf, Kapacitor 四个工具的首字母组成。这一套组件将收集数据和入库、数据库存储、展示、告警四者囊括。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://obsidian-1254275759.cos.ap-shanghai.myqcloud.com/202511102111040.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://w2.influxdata.com/time-series-platform/telegraf/&quot;&gt;Telegraf&lt;/a&gt;: 用Golang开发的代理程序，可用于收集和提交metric。Telegraf工作原理大概是这样：定时去执行&lt;strong&gt;输入插件&lt;/strong&gt;收集数据，数据经过&lt;strong&gt;处理插件&lt;/strong&gt;和&lt;strong&gt;聚合插件&lt;/strong&gt;，通过&lt;strong&gt;输出插件&lt;/strong&gt;输出到数据存储。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.influxdata.com/&quot;&gt;InfluxDB&lt;/a&gt;：一款专门处理高写入和查询负载的时序数据库。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://w2.influxdata.com/time-series-platform/chronograf/&quot;&gt;Chronograf&lt;/a&gt;: Chronograf 是InfluxData的开源可视化引擎，可让通过数据的实时可视化快速构建仪表板，并支持与 Kapacitor 联动。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://w2.influxdata.com/time-series-platform/kapacitor/&quot;&gt;Kapacitor&lt;/a&gt;: 指标和事件处理和告警引擎。使用它将时间序列数据处理成可操作的告警，并将这些告警发送到许多流行的产品，如 PagerDuty，Slack 等。&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;open-falcon&quot;&gt;Open-Falcon&lt;/h4&gt;
&lt;p&gt;https://cloud.tencent.com/developer/article/1845407&lt;/p&gt;

&lt;p&gt;小米开源的云监控系统，提供了采集、存储、查询、告警、展示等一整套解决方案，支持主机、应用、网络等多种监控数据类型。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://obsidian-1254275759.cos.ap-shanghai.myqcloud.com/202511102111052.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h4 id=&quot;prometheus-stack&quot;&gt;Prometheus Stack&lt;/h4&gt;
&lt;p&gt;https://prometheus.kpingfan.com/01-introduction/01.prometheus%E6%9E%B6%E6%9E%84/&lt;/p&gt;

&lt;p&gt;由 Prometheus、Alertmanager 和 Grafana 组成的一整套解决方案，用于采集、存储、查询和可视化时序数据，并提供了告警和自动化操作的功能。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://obsidian-1254275759.cos.ap-shanghai.myqcloud.com/202511102111064.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Prometheus Server：Prometheus组件中的核心部分，负责对监控数据的获取，存储及查询。&lt;/li&gt;
  &lt;li&gt;Exporter：负责将监控数据通过HTTP服务的形式暴露给Prometheus Server，Prometheus Server通过访问该Exporter提供的Endpoint端点，即可获取到需要采集的监控数据。&lt;/li&gt;
  &lt;li&gt;PushGateway：通过PushGateway将监控数据主动Push到Gateway当中。而Prometheus Server则可以采用同样Pull的方式从PushGateway中获取到监控数据。解决短作业无法提供通信服务的问题。&lt;/li&gt;
  &lt;li&gt;AlertManager：在Prometheus Server中支持基于PromQL创建告警规则，如果满足PromQL定义的规则，则会产生一条告警，而告警的后续处理流程则由AlertManager进行管理。AlertManager集成了邮件，Slack等通知方式，也提供Webhook自定义告警处理。&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;1324-metrics&quot;&gt;1.3.2.4. Metrics&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://bytedance.feishu.cn/wiki/wikcn8Y3Bp5Sqw5pEuO18kZn7dh&quot;&gt;Metrics系统是什么？&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://obsidian-1254275759.cos.ap-shanghai.myqcloud.com/202511102111074.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;写入&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;写入侧组件是用户需要感知的，由如下2类：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;SDK&lt;/strong&gt;: Metrics提供了多种开发语言的SDK&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;agent&lt;/strong&gt;: Metrics在每台物理机/云主机都有部署名为&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;metricserver2&lt;/code&gt;的agent，该agent负责收集SDK打过来的指标，并且每30s进行一次&lt;strong&gt;序列内&lt;/strong&gt;、&lt;strong&gt;时间纬度&lt;/strong&gt;上的聚合，然后发送给Metrics后端。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;存储&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;按照时序数据的&lt;strong&gt;冷热&lt;/strong&gt;特点，Metrics将数据按照时间纬度，存放在不同的存储系统：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;近28小时数据：存放在热存&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tsdc&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;28小时以外数据：存放在冷存&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mstore&lt;/code&gt;(on HDFS)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;此外，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mstore&lt;/code&gt;会archive历史数据，具体规则：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;近30天内：不archive&lt;/li&gt;
  &lt;li&gt;近60天～近30天：按照5分钟一个点archive&lt;/li&gt;
  &lt;li&gt;60天以以外：按照1h一个点archive&lt;/li&gt;
&lt;/ul&gt;

&lt;h1 id=&quot;存储模型及存储引擎&quot;&gt;存储模型及存储引擎&lt;/h1&gt;
&lt;p&gt;这里以 influxdb 为例子，介绍其存储模型及存储引擎，其他时序数据库思路类似&lt;/p&gt;

&lt;h2 id=&quot;存储模型&quot;&gt;存储模型&lt;/h2&gt;
&lt;p&gt;InfluxDB 使用的是典型的 KV 存储模型。Measurement+Tags 确定一个 timeseries。&lt;/p&gt;

&lt;p&gt;下面是一条向InfluxDB中写入一条数据的命令行，来看下这条数据由哪几个部分组成：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;INSERT machine_metric,cluster=Cluster-A,hostname=host-a cpu=10 1501554197019201823
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上面是一条向InfluxDB中写入一条数据的命令行，来看下这条数据由哪几个部分组成：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Measurement：Measurement代表数据所属监控指标的名称。例如上述例子是对机器指标的监控，所以其measurement命名为machine_metric。&lt;/li&gt;
  &lt;li&gt;Tags：用于描述measurement的不同的维度，允许存在一个或多个Tag，每个Tag也是由TagKey和TagValue构成。&lt;/li&gt;
  &lt;li&gt;Field：一行measurement数据可以对应多个value，每个value根据Field来区分。&lt;/li&gt;
  &lt;li&gt;Timestamp: 时序数据的必备属性，代表该条数据所属的时间点，可以看到InfluxDB的时间精度能够精确到纳秒。&lt;/li&gt;
  &lt;li&gt;TimeSeries：Measurement+Tags的组合，在InfluxDB中被称为TimeSeries。TimeSeries就是时间线，根据时间能够定位到某个时间点，所以TimeSeries+Field+Timestamp能够定位到某个Value。这个概念比较重要，在后续的章节中都会提到。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;最终在逻辑上每个Measurement内的数据会组织成一张大的数据表，如下图所示：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://obsidian-1254275759.cos.ap-shanghai.myqcloud.com/202511102111094.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;在查询时，InfluxDB支持在Measurement内任意维度的条件查询，你可以指定任意某个Tag或者Filed的条件做查询。接着上面的数据案例，你可以构造以下查询条件：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;SELECT * FROM &quot;machine_metric&quot; WHERE time &amp;gt; now() - 1h;  
SELECT * FROM &quot;machine_metric&quot; WHERE &quot;cluster&quot; = &quot;Cluster-A&quot; AND time &amp;gt; now() - 1h;
SELECT * FROM &quot;machine_metric&quot; WHERE &quot;cluster&quot; = &quot;Cluster-A&quot; AND cpu &amp;gt; 5 AND time &amp;gt; now() - 1h;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;从数据模型以及查询的条件上看，Tag和Field没有任何区别。从语义上来看，Tag用于描述Measurement，而Field用于描述Value。从内部实现来上看，Tag会被全索引，而Filed不会，所以根据Tag来进行条件查询会比根据Filed来查询效率高很多。&lt;/p&gt;

&lt;h2 id=&quot;存储引擎&quot;&gt;存储引擎&lt;/h2&gt;
&lt;p&gt;https://docs.influxdata.com/influxdb/v1.8/concepts/storage_engine&lt;/p&gt;

&lt;p&gt;https://zhuanlan.zhihu.com/p/32710333&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://obsidian-1254275759.cos.ap-shanghai.myqcloud.com/202511102111105.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;概念&quot;&gt;概念&lt;/h3&gt;
&lt;p&gt;InfluxDB在经历了几个小版本的BoltDB后，最终决定自研TSM，TSM的设计目标一是解决LevelDB的文件句柄过多问题，二是解决BoltDB的写入性能问题。TSM全称是Time-Structured Merge Tree，思想类似LSM，不过是基于时序数据的特性做了一些特殊的优化。来看下TSM的一些重要组件：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;In-Memory Index - The in-memory index is a shared index across shards that provides the quick access to &lt;a href=&quot;https://docs.influxdata.com/influxdb/v1.8/concepts/glossary/#measurement&quot;&gt;measurements&lt;/a&gt;, &lt;a href=&quot;https://docs.influxdata.com/influxdb/v1.8/concepts/glossary/#tag&quot;&gt;tags&lt;/a&gt;, and &lt;a href=&quot;https://docs.influxdata.com/influxdb/v1.8/concepts/glossary/#series&quot;&gt;series&lt;/a&gt;. The index is used by the engine, but is not specific to the storage engine itself.&lt;/li&gt;
  &lt;li&gt;Write Ahead Log(WAL) : 数据会先写入WAL持久化，后进入memory-index和cache。Cache内数据会异步刷入TSM File，在Cache内数据未持久化到TSM File之前若遇到进程crash，则会通过WAL内的数据来恢复cache内的数据，这个行为与LSM是完全类似的。&lt;/li&gt;
  &lt;li&gt;Cache: TSM的Cache与LSM的MemoryTable类似，其内部的数据为WAL中未持久化到TSM File的数据。若进程发生failover，则cache中的数据会根据WAL中的数据进行重建。&lt;/li&gt;
  &lt;li&gt;TSM Files: TSM File与LSM的SSTable类似，TSM File由四个部分组成，分别为：header, blocks, index和footer。后文会详细介绍。&lt;/li&gt;
  &lt;li&gt;Compaction: compaction是一个将write-optimized的数据存储格式优化为read-optimized的数据存储格式的一个过程&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;tsm-文件&quot;&gt;TSM 文件&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://obsidian-1254275759.cos.ap-shanghai.myqcloud.com/202511102111113.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;TSM文件最核心的由Series Data Section以及Series Index Section两个部分组成，其中前者表示存储时序数据，而后者存储文件级别B+树索引，用于在文件中快速查询时间序列数据块。&lt;/p&gt;

&lt;h4 id=&quot;series-data-block&quot;&gt;Series Data Block&lt;/h4&gt;
&lt;p&gt;Map中一个Key对应一系列时序数据，因此能想到的最简单的flush策略是将这一系列时序数据在内存中构建成一个Block并持久化到文件。然而，有可能一个Key对应的时序数据非常之多，导致一个Block非常之大，超过Block大小阈值，因此在实际实现中有可能会将同一个Key对应的时序数据构建成多个连续的Block。但是，在任何时候，同一个Block中只会存储同一种Key的数据。&lt;/p&gt;

&lt;p&gt;另一个需要关注的点在于，Map会按照Key顺序排列并执行flush，这是构建索引的需求。Series Data Block文件结构如下图所示：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://obsidian-1254275759.cos.ap-shanghai.myqcloud.com/202511102111122.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h4 id=&quot;series-index-block&quot;&gt;Series Index Block&lt;/h4&gt;
&lt;p&gt;每个key 对应一个Index Block。&lt;/p&gt;

&lt;p&gt;很多时候用户需要根据Key查询某段时间（比如最近一小时）的时序数据，如果没有索引，就会需要将整个TSM文件加载到内存中才能一个Data Block一个Data Block查找，这样一方面非常占用内存，另一方面查询效率非常之低。为了在不占用太多内存的前提下提高查询效率，TSM文件引入了索引。TSM文件索引数据由一系列索引Block组成，每个索引Block的结构如下图所示：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://obsidian-1254275759.cos.ap-shanghai.myqcloud.com/202511102111132.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Series Index Block由Index Block Meta以及一系列Index Entry构成：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Index Block Meta最核心的字段是Key，表示这个索引Block内所有IndexEntry所索引的时序数据块都是该Key对应的时序数据。&lt;/li&gt;
  &lt;li&gt;Index Entry表示一个索引字段，指向对应的Series Data Block。指向的Data Block由Offset唯一确定，Offset表示该Data Block在文件中的偏移量，Size表示指向的Data Block大小。Min Time和Max Time表示指向的Data Block中时序数据集合的最小时间以及最大时间，用户在根据时间范围查找时可以根据这两个字段进行过滤。&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;读写&quot;&gt;读写&lt;/h3&gt;
&lt;p&gt;https://zhuanlan.zhihu.com/p/97247465&lt;/p&gt;

&lt;h4 id=&quot;写入&quot;&gt;写入&lt;/h4&gt;
&lt;p&gt;Writes are appended to the current WAL segment and are also added to the Cache. Each WAL segment has a maximum size. Writes roll over to a new file once the current file fills up. The cache is also size bounded; snapshots are taken and WAL compactions are initiated when the cache becomes too full. If the inbound write rate exceeds the WAL compaction rate for a sustained period, the cache may become too full, in which case new writes will fail until the snapshot process catches up.&lt;/p&gt;

&lt;p&gt;When WAL segments fill up and are closed, the Compactor snapshots the Cache and writes the data to a new TSM file. When the TSM file is successfully written and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fsync&lt;/code&gt;’d, it is loaded and referenced by the FileStore.&lt;/p&gt;

&lt;p&gt;Updates (writing a newer value for a point that already exists) occur as normal writes. Since cached values overwrite existing values, newer writes take precedence. &lt;font style=&quot;background-color:rgba(222,224,227,0.8);&quot;&gt;If a write would overwrite a point in a prior &lt;/font&gt;&lt;font style=&quot;background-color:rgba(222,224,227,0.8);&quot;&gt;TSM&lt;/font&gt;&lt;font style=&quot;background-color:rgba(222,224,227,0.8);&quot;&gt; file, the points are merged at query runtime and the newer write takes precedence.&lt;/font&gt;&lt;/p&gt;

&lt;p&gt;Deletes occur by writing a delete entry to the WAL for the measurement or series and then updating the Cache and FileStore. The Cache evicts all relevant entries. &lt;font style=&quot;background-color:rgba(222,224,227,0.8);&quot;&gt;The FileStore writes a &lt;/font&gt;&lt;font style=&quot;background-color:rgba(222,224,227,0.8);&quot;&gt;tombstone&lt;/font&gt;&lt;font style=&quot;background-color:rgba(222,224,227,0.8);&quot;&gt; file for each &lt;/font&gt;&lt;font style=&quot;background-color:rgba(222,224,227,0.8);&quot;&gt;TSM&lt;/font&gt;&lt;font style=&quot;background-color:rgba(222,224,227,0.8);&quot;&gt; file that contains relevant data. &lt;/font&gt;These tombstone files are used at startup time to ignore blocks as well as during compactions to remove deleted entries.&lt;/p&gt;

&lt;p&gt;Queries against partially deleted series are handled at query time until a compaction removes the data fully from the TSM files.&lt;/p&gt;

&lt;h4 id=&quot;读取&quot;&gt;读取&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;undefined&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;上图中中间部分为索引层，TSM在启动之后就会将TSM文件的索引部分加载到内存，数据部分因为太大并不会直接加载到内存。用户查询可以分为三步：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;首先根据Key找到对应的SeriesIndex Block，因为Key是有序的，所以可以使用二分查找来具体实现。&lt;/li&gt;
  &lt;li&gt;找到SeriesIndex Block之后再根据查找的时间范围，使用[MinTime, MaxTime]索引定位到可能的Series Data Block列表。&lt;/li&gt;
  &lt;li&gt;将满足条件的Series Data Block加载到内存中解压进一步使用二分查找算法根据timestamp查找即可找到。&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;压缩&quot;&gt;压缩&lt;/h3&gt;
&lt;h4 id=&quot;level-compaction&quot;&gt;Level Compaction&lt;/h4&gt;
&lt;p&gt;InfluxDB将TSM文件分为4个层级(Level 1-4)，compaction只会发生在同层级文件内，同层级的文件compaction后会晋升到下一层级。从这个规则看，根据时序数据的产生特性，level越高数据生成时间越久，访问热度越低。由Cache数据初次生成的TSM文件称为Snapshot，多个Snapshot文件compaction后产生Level1的TSM文件，Level1的文件compaction后生成level2的文件，依次类推。&lt;/p&gt;

&lt;p&gt;低Level和高Level的compaction会采用不同的算法，低level文件的compaction采用低CPU消耗的做法，例如不会做解压缩和block合并，而高level文件的compaction则会做block解压缩以及block合并，以进一步提高压缩率。&lt;/p&gt;

&lt;h4 id=&quot;index-optimization-compaction&quot;&gt;Index Optimization Compaction&lt;/h4&gt;
&lt;p&gt;当Level4的文件积攒到一定个数后，index会变得很大，查询效率会变的比较低。影响查询效率低的因素主要在于同一个TimeSeries数据会被多个TSM文件所包含，所以查询不可避免的需要跨多个文件进行数据整合。所以IndexOptimizationCompaction的主要作用就是将同一TimeSeries下的数据合并到同一个TSM文件中，尽量减少不同TSM文件间的TimeSeries重合度。&lt;/p&gt;

&lt;h4 id=&quot;full-compaction&quot;&gt;Full Compaction&lt;/h4&gt;
&lt;p&gt;InfluxDB在判断某个Shard长时间内不会再有数据写入之后，会对数据做一次FullCompaction。FullCompaction是LevelCompaction和IndexOptimization的整合，在做完一次FullCompaction之后，这个Shard不会再做任何的compaction，除非有新的数据写入或者删除发生。这个策略是对冷数据的一个规整，主要目的在于提高压缩率。&lt;/p&gt;

&lt;h3 id=&quot;查询优化&quot;&gt;查询优化&lt;/h3&gt;
&lt;h3 id=&quot;演变历程&quot;&gt;演变历程&lt;/h3&gt;
&lt;p&gt;InfluxDB 的存储引擎经历了从 LSM Tree =&amp;gt; B+Tree =&amp;gt; TSM Tree 的过程。&lt;/p&gt;

&lt;p&gt;https://docs.influxdata.com/influxdb/v1.8/concepts/storage_engine/&lt;/p&gt;

&lt;h1 id=&quot;metrics&quot;&gt;Metrics&lt;/h1&gt;
&lt;h2 id=&quot;写入-1&quot;&gt;写入&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://bytedance.feishu.cn/wiki/wikcnxbsf7fzZRQabZfxjXcVfAf#FjsXAM&quot;&gt;Metrics看这一篇就够了&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;metrics_agent 每30s进行一次&lt;strong&gt;序列内&lt;/strong&gt;、&lt;strong&gt;时间纬度&lt;/strong&gt;上的聚合，然后发送给Metrics后端。&lt;/p&gt;

&lt;h3 id=&quot;counter&quot;&gt;counter&lt;/h3&gt;
&lt;ul&gt;
  &lt;li&gt;会将在指定tagkv集合积攒的值与打上来的值相加作为新值, 关注的是变化量和变化速率&lt;/li&gt;
  &lt;li&gt;会一直累加直到达到 double 类型的最大值，即2^1024，也就是1.79E+308&lt;/li&gt;
  &lt;li&gt;适用于求rate{counter}(rate表示求导，counter表示去掉导数的负值)之后计算任意操作的速率(qps/tps/ops) 使用。&lt;/li&gt;
  &lt;li&gt;举个例子，每分钟新增打点 30 个，rate{counter}对 60 秒求导，30/60 = 0.5,查询时勾上 rate，counter 的情况下的值就是 0.5&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;store&quot;&gt;Store&lt;/h3&gt;
&lt;ul&gt;
  &lt;li&gt;打上来什么值就是什么值，实际显示的值是30秒的采样周期中最后打上来的值。&lt;/li&gt;
  &lt;li&gt;会按照 tag 分组后统计（tag 相同的 30 秒内取最后一次的值，tag 不同的两个都上传）&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;适用于只关注每个周期最新状态的监控采集，如 CPU/内存使用率，&lt;/strong&gt;&lt;strong&gt;线程&lt;/strong&gt;&lt;strong&gt;数，连接数，消费积压量&lt;/strong&gt;&lt;/p&gt;

&lt;h3 id=&quot;timer&quot;&gt;Timer&lt;/h3&gt;
&lt;ul&gt;
  &lt;li&gt;将30秒的采样周期内（同一个ms2/宿主机）打上来的值缓存起来，然后在采样周期结束时统计本采样周期内打上来的值。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;个人理解，timer 本质上与 counter/store 类似，只不过顺便提供了窗口计算功能。猜测 &lt;strong&gt;metrics&lt;/strong&gt; 在 agent 端对 timer 数据做了预处理，预计算了 metrics 在窗口内的统计数据。例如：counter（30秒打了有多少个值，大致为__qps__*30）、avg、pctx 等。&lt;/em&gt;&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;t0&lt;/th&gt;
      &lt;th&gt;t0+10s&lt;/th&gt;
      &lt;th&gt;t0+20s&lt;/th&gt;
      &lt;th&gt;t0+30s&lt;/th&gt;
      &lt;th&gt;t0+40s&lt;/th&gt;
      &lt;th&gt;t0+50s&lt;/th&gt;
      &lt;th&gt;…&lt;/th&gt;
      &lt;th&gt;t0+ns&lt;/th&gt;
      &lt;th&gt; &lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Raw data&lt;/td&gt;
      &lt;td&gt;27&lt;/td&gt;
      &lt;td&gt;28&lt;/td&gt;
      &lt;td&gt;29&lt;/td&gt;
      &lt;td&gt;32&lt;/td&gt;
      &lt;td&gt;31&lt;/td&gt;
      &lt;td&gt;30&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;27.3&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;.min&lt;/td&gt;
      &lt;td&gt;27&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;30&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;-&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;.max&lt;/td&gt;
      &lt;td&gt;29&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;32&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;-&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;.avg&lt;/td&gt;
      &lt;td&gt;28&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;31&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;-&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;.counter&lt;/td&gt;
      &lt;td&gt;3&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;3&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;-&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h3 id=&quot;rate_counter&quot;&gt;rate_counter&lt;/h3&gt;
&lt;p&gt;类似&lt;a href=&quot;https://doc.bytedance.net/docs/3593/4657/36781/&quot;&gt;&lt;u&gt;打点类型-counter&lt;/u&gt;&lt;/a&gt;，但是是将30秒的采样周期内打上来的值累加/30秒，算出变化率，比如QPS。最终将这个变化率emit出去。查询时如果再次求rate，就是变化率的变化率，比如QPS的变化率。不关注增量，只关注变化率的可以使用这个打点，相对于 counter 查询时无需做 rate计算，开销较低，支持流式聚合，支持预聚合等优化&lt;/p&gt;

&lt;h3 id=&quot;meter&quot;&gt;meter&lt;/h3&gt;
&lt;p&gt;为Counter和Rate_Counter的结合，一次meter类型的打点可以生成两种类型打点。&lt;/p&gt;

&lt;h2 id=&quot;查询&quot;&gt;查询&lt;/h2&gt;
&lt;p&gt;https://tech.bytedance.net/articles/6867450721697185799&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;undefined&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;了解基本原理之后，查询逻辑基本可以通吃&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;聚合算子：对指定 tag 的 timeseries 进行 groupby&lt;/li&gt;
  &lt;li&gt;降采样：在执行聚合算子之后，对新数据进行执行降采样操作&lt;/li&gt;
  &lt;li&gt;运算符：在上述两种 reduce 操作执行之后，执行运算符，得到结果列表&lt;/li&gt;
&lt;/ul&gt;

&lt;h1 id=&quot;附录&quot;&gt;附录&lt;/h1&gt;
&lt;h2 id=&quot;引用&quot;&gt;引用&lt;/h2&gt;
&lt;ul&gt;
  &lt;li&gt;
    &lt;table&gt;
      &lt;tbody&gt;
        &lt;tr&gt;
          &lt;td&gt;[In-memory indexing and the Time-Structured Merge Tree (TSM)&lt;/td&gt;
          &lt;td&gt;InfluxDB OSS 1.8 Documentation](https://docs.influxdata.com/influxdb/v1.8/concepts/storage_engine/)&lt;/td&gt;
        &lt;/tr&gt;
      &lt;/tbody&gt;
    &lt;/table&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://bytedance.feishu.cn/wiki/wikcnxbsf7fzZRQabZfxjXcVfAf&quot;&gt;Metrics看这一篇就够了&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;
    &lt;table&gt;
      &lt;tbody&gt;
        &lt;tr&gt;
          &lt;td&gt;[InfluxDB storage subsystem: the TSM files&lt;/td&gt;
          &lt;td&gt;Just my thouhgts](https://migue.github.io/post/influx-storage-tsm-component/)&lt;/td&gt;
        &lt;/tr&gt;
      &lt;/tbody&gt;
    &lt;/table&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;table&gt;
      &lt;tbody&gt;
        &lt;tr&gt;
          &lt;td&gt;[InfluxDB storage subsystem: an introduction&lt;/td&gt;
          &lt;td&gt;Just my thouhgts](https://migue.github.io/post/quick-tour-influx-storage/)&lt;/td&gt;
        &lt;/tr&gt;
      &lt;/tbody&gt;
    &lt;/table&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;拓展阅读&quot;&gt;拓展阅读&lt;/h2&gt;
&lt;ul&gt;
  &lt;li&gt;https://tech.bytedance.net/articles/6867450721697185799&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://medium.com/swlh/log-structured-merge-trees-9c8e2bea89e8&quot;&gt;Log Structured Merge Trees&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/google/leveldb&quot;&gt;LevelDB: a fast key-value storage&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
			<pubDate>Fri, 16 Jun 2023 00:00:00 +0000</pubDate>
			<link>https://sarkerson.github.io/2023/06/16/%E6%97%B6%E5%BA%8F%E6%95%B0%E6%8D%AE%E5%BA%93/</link>
			<guid isPermaLink="true">https://sarkerson.github.io/2023/06/16/%E6%97%B6%E5%BA%8F%E6%95%B0%E6%8D%AE%E5%BA%93/</guid>
        
			<category>database</category>
        
        
		</item>
    
		<item>
			<title>系统设计：信息流的推拉模型与架构</title>
			<description>&lt;h1 id=&quot;系统设计信息流的推拉模型与架构&quot;&gt;系统设计：信息流的推拉模型与架构&lt;/h1&gt;

&lt;blockquote&gt;
  &lt;p&gt;TL;DR; 本文以关注流为例子，介绍了几种信息流的实现方案，并对比其优缺点，介绍了各自适合的应用场景。并归纳了信息流领域常见的幻读、不可重复读问题，参考数据库隔离级别实现的思路，提出对应的两个解决方案。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;信息流（或称 Feed 流）这种功能在我们手机 APP 中几乎随处可见，最典型的就是微信朋友圈、Twitter 等。&lt;/p&gt;

&lt;p&gt;但是对于推荐流跟关注流这两种 Feed 流，它们背后用到的技术架构差别会比较大。不同于基于模型排序的推荐流，关注流通常基于时间线来排序，用户对数据的完整性和排序都比较敏感。&lt;/p&gt;

&lt;h1 id=&quot;push-mode&quot;&gt;push mode&lt;/h1&gt;

&lt;p&gt;push mode 对每个粉丝维护一个「关注拉链」的存储；每当关注的用户有新动态（发文、评论等）时，则将动态离线写入其粉丝的「关注拉链」存储中。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;用户发文时&lt;/strong&gt;，遍历所有的粉丝，将文章写入所有粉丝的「关注拉链」存储中。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;用户消费信息流时&lt;/strong&gt;，直接从「关注拉链」中读取即可。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;好处&lt;/strong&gt;: 读帖简单；&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;坏处&lt;/strong&gt;: 发帖复杂，存在写扩散问题；大 v 粉丝量多时存在性能；&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;适用场景&lt;/strong&gt;: 用户活跃，经常刷帖；无大 v，用户粉丝量少；&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic1.zhimg.com/v2-4f129b28512ead524fd9111e46d455d0_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h1 id=&quot;pull-mode&quot;&gt;pull mode&lt;/h1&gt;

&lt;p&gt;pull mode 即实时召回，每个用户不会维护一个拉链进行存储，而是实时拉取「关注用户」的动态，进行实时排序。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;用户发文时&lt;/strong&gt;，写入自己的「发文拉链」中；&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;用户消费信息流时&lt;/strong&gt;，遍历关注的所有作者，从作者发文拉链中实时拉取数据，进行合并、排序；&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;好处&lt;/strong&gt;: 节约存储，避免写扩散；发帖简单；&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;坏处&lt;/strong&gt;: 读帖复杂；关注人多时存在性能问题；&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;适用场景&lt;/strong&gt;: 用户不活跃，很少读帖；有大 v 场景，粉丝量很多；关注的人少；&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic1.zhimg.com/v2-2968e65dee780275ed26ec567ef61358_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h1 id=&quot;pull--push-混合&quot;&gt;pull &amp;amp; push 混合&lt;/h1&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt; &lt;/th&gt;
      &lt;th&gt;优点&lt;/th&gt;
      &lt;th&gt;缺点&lt;/th&gt;
      &lt;th&gt;适用场景&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Pull mode&lt;/td&gt;
      &lt;td&gt;节约存储，避免写扩散发帖简单&lt;/td&gt;
      &lt;td&gt;读帖复杂关注人多时存在性能问题&lt;/td&gt;
      &lt;td&gt;用户不活跃，很少读帖有大 v 场景，粉丝量很多关注的人少&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Push mode&lt;/td&gt;
      &lt;td&gt;读帖简单&lt;/td&gt;
      &lt;td&gt;发帖复杂，存在写扩散问题大 v 粉丝量多时存在性能&lt;/td&gt;
      &lt;td&gt;用户活跃，经常刷帖无大 v，用户粉丝量少&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;针对上述，对用户群体进行划分，混合使用 pull &amp;amp; push，具体操作如下：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;将用户群体分为活跃 G1、不活跃 G2 两个组；&lt;/li&gt;
  &lt;li&gt;当大明星 X1 发帖时，将帖子写入 X 的发件箱的同时，顺便写入其粉丝中属于 G1 那部分的收件箱；&lt;/li&gt;
  &lt;li&gt;当路人甲 X2 发帖时，采用 push mode，遍历他的所有粉丝并将帖子写入粉丝收件箱；&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;现在考虑消费场景：&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;G1 用户登录刷Feed流时：直接从自己的收件箱读取帖子即可，保证了活跃用户的体验。&lt;/li&gt;
  &lt;li&gt;G2 用户突然登录刷Feed流时：
    &lt;ol&gt;
      &lt;li&gt;读他的收件箱；&lt;/li&gt;
      &lt;li&gt;遍历他所关注的大 V 用户的发件箱提取帖子；&lt;/li&gt;
      &lt;li&gt;merge 上述结果；&lt;/li&gt;
    &lt;/ol&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;注：因为有 pull mode 的场景存在，因此即使是混合模式，每个阅读者所能关注的人数也要设置上限。&lt;/p&gt;

&lt;h1 id=&quot;关注流的方案&quot;&gt;关注流的方案&lt;/h1&gt;

&lt;p&gt;根据量级测算，笔者所在业务关注流使用了 pull-mode，存储选型使用了内部的 IndexService。这是一个基于 LMDB 的纯内存数据库，并且会建立基于 score 的索引，由于每条发文拉链不会很长，扇出百级别、近千级别仍然可以有不错的性能。&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;特性 / 存储选型&lt;/th&gt;
      &lt;th&gt;IndexService&lt;/th&gt;
      &lt;th&gt;ByteGraph&lt;/th&gt;
      &lt;th&gt;Redis&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;存储模型&lt;/td&gt;
      &lt;td&gt;key:value|score|extra（可以基于 extra 进行业务过滤）&lt;/td&gt;
      &lt;td&gt;Vertex –Edge–&amp;gt; Vertex&lt;/td&gt;
      &lt;td&gt;key:value|score&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;性能&lt;/td&gt;
      &lt;td&gt;批量取倒排延时 IndexService &amp;lt; redis &amp;lt; Rocksdb&lt;/td&gt;
      &lt;td&gt;底层依赖 Rocksdb，latency 比 IndexService 高&lt;/td&gt;
      &lt;td&gt;性能优于 abase，因为 abase 基于 rocksdb&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;召回&lt;/td&gt;
      &lt;td&gt;支持多种 merge支持排序&lt;/td&gt;
      &lt;td&gt;业务自己 merge单条召回支持排序，多路召回需要业务自己排序&lt;/td&gt;
      &lt;td&gt;业务自己 merge单条召回支持排序，多路召回需要业务自己排序&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;读取&lt;/td&gt;
      &lt;td&gt;支持 impression_container 消重对于多路召回，不需要业务维护游标，使用内存现算&lt;/td&gt;
      &lt;td&gt;业务自己消重业务需要维护多路游标，否则可能导致漏数据&lt;/td&gt;
      &lt;td&gt;业务自己消重业务需要维护多路游标，否则可能导致漏数据&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;拓展性&lt;/td&gt;
      &lt;td&gt;拓展好，extra_info 可以携带拓展信息&lt;/td&gt;
      &lt;td&gt;拓展好，edge 可以携带拓展信息&lt;/td&gt;
      &lt;td&gt;拓展差，需要额外维护 meta 信息&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;注意，即使后续切换到 pull &amp;amp; push 混合模式，仍然可以使用该技术栈来解决，并且这时候，召回的扇出会变得非常小（用户收件箱+关注的几个大V的发件箱）。虽然这时候用 ByteGraph 性能也不赖，但是业务方需要维护多个游标（每路召回对应一个），实现起来仍然比较复杂。&lt;/p&gt;

&lt;h1 id=&quot;不可重复读幻读&quot;&gt;不可重复读、幻读&lt;/h1&gt;

&lt;p&gt;上述关注流场景是基于时间线排序，时间线的一个特点就是，数据写了，score 就不会改变（score 就是发文的 timestamp）。&lt;/p&gt;

&lt;p&gt;但是某些业务场景可能不是按照严格时间线，而是按照点赞数等互动行为的热度来排序，这时候 score 会经常变动。基于时间线通常使用 (start_time, count) 来拉数据，而基于热度通常是使用 offset、count，即游标的起始值以及读取的数量。&lt;/p&gt;

&lt;p&gt;那么对于此场景，使用上述任何一种方案，都可能导致缺数据、或者多数据的问题。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;本质上，这个问题类似数据库的不可重复读、幻读&lt;/strong&gt;。下面进行简单分析：&lt;/p&gt;

&lt;p&gt;如下图所示，第一刷返回 5 条之后，考虑以下几种情况：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;如果下面有其他 item 有 score 的变更，导致插在前 5 条之中；（不可重复读）&lt;/li&gt;
  &lt;li&gt;如果恰好有新增的 item 插入到前 5 条之中；（幻读）&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;那么会导致第二刷返回的数据包含了第一刷的部分内容，并且漏掉了本该在第二刷返回的部分内容。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic3.zhimg.com/v2-9c5f9529f8bf850673e0325004182c0a_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;这大概是数据库领域的经典问题了。&lt;/p&gt;

&lt;p&gt;数据库（这里指 InnoDB）是使用 MVCC 机制来解决上述问题的，即维护一个基于事务版本实现的视图，在可重复读隔离级别下，整个事务执行过程中看到的都是事务开始时的快照。&lt;/p&gt;

&lt;p&gt;而对于 ES 而言，也有类似问题。给定一个分页查询（ES 里称为 from、size，跟上述提到的 offset、count 一样一样的），ES 通常需要在每个分片中搜索得到 [0, size+from) 区间内的所有数据，然后在 coordinator 进行 merge 计算得到结果并返回。在进行分页查询期间，数据有任何变动的话，则可能会影响数据的完整性，出现上述提到的信息流一摸一样的问题。&lt;/p&gt;

&lt;p&gt;ES 的解决方案与 InnoDB 的思想类似，分页起始时，返回一个快照，由于 ES 没有事务的概念，因此该快照存在一个过期时间，由客户端来指定。这里将分页开始到快照结束称为一个会话，ES 保证在该会话期间，分页查询访问的是同一个快照。详细可以阅读 &lt;a href=&quot;https://www.elastic.co/guide/en/elasticsearch/reference/current/paginate-search-results.html#search-after&quot;&gt;ES earch-after&lt;/a&gt;。&lt;/p&gt;

&lt;h2 id=&quot;解决方案&quot;&gt;解决方案&lt;/h2&gt;

&lt;p&gt;本文提供两个解决方案供参考。&lt;/p&gt;

&lt;h3 id=&quot;方案一&quot;&gt;方案一&lt;/h3&gt;

&lt;p&gt;预请求+一致性哈希+会话缓存&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;每次会话开始时，服务端生成一个 token，并预请求 X 条数据进行本地缓存。缓存 key 是 token，值是 X 条数据 ID；&lt;/li&gt;
  &lt;li&gt;接下来该用户的所有的 load more 行为，经过网关一致性哈希路由到同一台后端实例，读取内存中的缓存，进行分页操作；&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;这样的话，至少可以保证前 X 条数据从会话开始之后是稳定的。&lt;/p&gt;

&lt;p&gt;当然，也可以使用分布式缓存来替换上述的一致性哈希+单机缓存方案，可以根据性能需求灵活选择。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;局限性&lt;/strong&gt;: 集群实例数变化时路由会失效；首刷需要获取 X 条数据，X 过大时会影响首刷性能；当用户量很大时，会占用很大内存（或其他存储），影响性能；&lt;/p&gt;

&lt;p&gt;另外，通常来说，数据重复不可接受，而数据不完整用户很难感知。X 条数据之后，有可能会有重复问题，所以一般会走客户端消重来配合食用，数据不完整则无解。&lt;/p&gt;

&lt;h3 id=&quot;方案二&quot;&gt;方案二&lt;/h3&gt;

&lt;p&gt;从头拉数据+消重；参考选型 index_service+impression&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;每次会话开始时，服务端生成一个 token，token=会话起始时间戳；&lt;/li&gt;
  &lt;li&gt;没有 offset 的概念，服务端底层会从头拉取 index_service 的数据，只是携带 impression 上下文进行消重；&lt;/li&gt;
  &lt;li&gt;每一刷服务端返回 count 个数据，并上报 impression 消重；&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;index_service 底层依赖 LMDB，可以应对大规模的数据集，因此对于拉取用户所需的那么几刷数据没有性能问题（因为大部分场景下所需召回倒排不会很多，例如文章评论，就只需要一个倒排）。&lt;/p&gt;

&lt;p&gt;当然，消重也可以走其他方案，比如客户端透传、服务端自己记录来实现。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic4.zhimg.com/v2-251c83c655bf26e1e96c3b89079bdb6f_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h1 id=&quot;业界方案&quot;&gt;业界方案&lt;/h1&gt;

&lt;h2 id=&quot;朋友圈&quot;&gt;朋友圈&lt;/h2&gt;

&lt;p&gt;https://cloud.tencent.com/developer/article/1168946&lt;/p&gt;

&lt;p&gt;注：猜测使用混合模式，并且注意每个用户能看到的朋友圈，其实是有数量上限的，因此收件箱必然是有截断的，而发件箱则是全量；&lt;/p&gt;

&lt;h2 id=&quot;twitter&quot;&gt;Twitter&lt;/h2&gt;

&lt;p&gt;https://blog.mi.hdm-stuttgart.de/index.php/2021/03/10/how-to-scale-real-time-tweet-delivery-architecture-at-twitter/&lt;/p&gt;

&lt;h1 id=&quot;小结&quot;&gt;小结&lt;/h1&gt;

&lt;p&gt;本文以关注流为例子，介绍了几种信息流的实现方案，并对比其优缺点，介绍了各自适合的应用场景。并归纳了信息流领域常见的幻读、不可重复读问题，参考数据库隔离级别实现的思路，提出对应的两个解决方案。&lt;/p&gt;
</description>
			<pubDate>Tue, 09 Aug 2022 00:00:00 +0000</pubDate>
			<link>https://sarkerson.github.io/2022/08/09/%E4%BF%A1%E6%81%AF%E6%B5%81%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1/</link>
			<guid isPermaLink="true">https://sarkerson.github.io/2022/08/09/%E4%BF%A1%E6%81%AF%E6%B5%81%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1/</guid>
        
			<category>architecture</category>
        
        
		</item>
    
		<item>
			<title>Go 编译：编译流程与工程应用</title>
			<description>&lt;h1 id=&quot;go-编译编译流程与工程应用&quot;&gt;Go 编译：编译流程与工程应用&lt;/h1&gt;

&lt;blockquote&gt;
  &lt;p&gt;TL;DR；本文简要介绍 Golang 编译的各个阶段干了什么，即从源文件到最终的机器码中间经历的过程。并从汇编代码、AST 入手介绍了相关的应用场景及实现原理。本文第一节深度参考了 &lt;a href=&quot;https://draveness.me/golang/docs/part1-prerequisite/ch02-compile/golang-lexer-and-parser/&quot;&gt;解析器眼中的 Go 语言&lt;/a&gt;，也非常推荐有时间的读者阅读原文。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1 id=&quot;编译原理&quot;&gt;编译原理&lt;/h1&gt;

&lt;p&gt;编译器的前端一般承担着词法分析、语法分析、类型检查和中间代码生成几部分工作.&lt;/p&gt;

&lt;p&gt;编译器后端主要负责目标代码的生成和优化，也就是将中间代码翻译成目标机器能够运行的二进制机器码。&lt;/p&gt;

&lt;h2 id=&quot;词法分析&quot;&gt;词法分析&lt;/h2&gt;

&lt;blockquote&gt;
  &lt;p&gt;过程：源代码文件 =&amp;gt; 词法分析（lexer） =&amp;gt; Token 序列&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;词法分析：词法分析是将字符序列转换为 Token 序列的过程。&lt;/strong&gt;源代码在计算机看来其实就是一个由字符组成的、无法被理解的字符串，所有的字符在计算器看来并没有什么区别，为了理解这些字符我们需要做的第一件事情就是将字符串分组，即转换为 Token 序列，这能够降低理解字符串的成本，简化源代码的分析过程，这个转换的过程就是词法分析。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-Go&quot;&gt;func (s *scanner) next() {
    // ...
    s.stop()
    startLine, startCol := s.pos()for s.ch == &apos; &apos; || s.ch == &apos;\t&apos; || s.ch == &apos;\n&apos; &amp;amp;&amp;amp; !nlsemi || s.ch == &apos;\r&apos; {
        s.nextch()
    }

    s.line, s.col = s.pos()
    s.blank = s.line &amp;gt; startLine || startCol == colbase
    s.start()
    if isLetter(s.ch) || s.ch &amp;gt;= utf8.RuneSelf &amp;amp;&amp;amp; s.atIdentChar(true) {
        s.nextch()
        s.ident()return
    }
    switch s.ch {
    case -1:
        s.tok = _EOF
    case &apos;0&apos;, &apos;1&apos;, &apos;2&apos;, &apos;3&apos;, &apos;4&apos;, &apos;5&apos;, &apos;6&apos;, &apos;7&apos;, &apos;8&apos;, &apos;9&apos;:
        s.number(false)
    // ...
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上述节选的代码是遍历源文件不断获取最新的字符，将字符通过 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cmd/compile/internal/syntax.source.nextch&lt;/code&gt; 方法追加到 scanner 持有的缓冲区中，并在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cmd/compile/internal/syntax.scanner.next&lt;/code&gt; 中对最新的字符进行词法分析的过程。&lt;/p&gt;

&lt;h2 id=&quot;语法分析&quot;&gt;语法分析&lt;/h2&gt;

&lt;blockquote&gt;
  &lt;p&gt;过程：Token 序列 =&amp;gt; 语法分析 =&amp;gt; AST&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;语法分析：&lt;/strong&gt;&lt;strong&gt;语法分析&lt;/strong&gt;&lt;strong&gt;是根据某种特定的文法，对 Token 序列构成的输入文本进行分析并确定其语法结构的过程。&lt;/strong&gt;语法分析由文法、分析方法构成。文法描述了语法的组成，分析方法则是解析文法的过程。&lt;/p&gt;

&lt;h3 id=&quot;文法&quot;&gt;文法&lt;/h3&gt;

&lt;p&gt;上下文无关文法是用来形式化、精确描述某种编程语言的工具，我们能够通过文法定义一种语言的语法，它主要包含一系列用于转换字符串的生产规则（Production rule）。&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://go.dev/ref/spec#Lexical_elements&quot;&gt;The Go Programming Language Specification（Go 语言说明书）&lt;/a&gt;使用 &lt;a href=&quot;https://zh.m.wikipedia.org/zh/扩展巴科斯范式&quot;&gt;EBNF 范式&lt;/a&gt;对 Golang 语法进行描述。&lt;/p&gt;

&lt;p&gt;下面是使用 EBNF 范式对 EBNF 本身进行描述：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-Plain&quot;&gt;Production  = production_name &quot;=&quot; [ Expression ] &quot;.&quot; .
Expression  = Alternative { &quot;|&quot; Alternative } .
Alternative = Term { Term } .
Term        = production_name | token [ &quot;…&quot; token ] | Group | Option | Repetition .
Group       = &quot;(&quot; Expression &quot;)&quot; .
Option      = &quot;[&quot; Expression &quot;]&quot; .
Repetition  = &quot;{&quot; Expression &quot;}&quot; .
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;生产规则是由 Term 与下述操作符组成：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-Plain&quot;&gt;|   alternation
()  grouping
[]  option (0 or 1 times)
{}  repetition (0 to n times)
&quot;&quot;  string
.   terminator symbol
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;src/cmd/compile/internal/syntax/parser.go&lt;/code&gt; 文件中描述了 Go 语言文法的生产规则：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-Go&quot;&gt;SourceFile = PackageClause &quot;;&quot; { ImportDecl &quot;;&quot; } { TopLevelDecl &quot;;&quot; } .
PackageClause  = &quot;package&quot; PackageName .
PackageName    = identifier .

ImportDecl       = &quot;import&quot; ( ImportSpec | &quot;(&quot; { ImportSpec &quot;;&quot; } &quot;)&quot; ) .
ImportSpec       = [ &quot;.&quot; | PackageName ] ImportPath .
ImportPath       = string_lit .

TopLevelDecl  = Declaration | FunctionDecl | MethodDecl .
Declaration   = ConstDecl | TypeDecl | VarDecl .
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;从上述 SourceFile 相关的生产规则我们可以看出，每一个文件都包含一个 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;package&lt;/code&gt; 的定义以及可选的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;import&lt;/code&gt; 声明和其他的顶层声明（TopLevelDecl），每一个 SourceFile 在编译器中都对应一个 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cmd/compile/internal/syntax.File&lt;/code&gt; 结构体，可以从该定义中轻松找到两者的联系：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-Go&quot;&gt;type File struct {
    Pragma   Pragma
    PkgName  *Name
    DeclList []Decl
    Lines    uint
    node
}
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&quot;分析方法&quot;&gt;分析方法&lt;/h3&gt;

&lt;p&gt;语法分析的分析方法一般分为自顶向下和自底向上两种，这两种方式会使用不同的方式对输入的 Token 序列进行推导：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Top-down_parsing&quot;&gt;自顶向下分析&lt;/a&gt;：解析器会从开始符号分析，通过新加入的字符判断应该使用什么生产规则展开当前的输入流；
    &lt;ul&gt;
      &lt;li&gt;LL 使用自顶向下分析方法&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Bottom-up_parsing&quot;&gt;自底向上分析&lt;/a&gt;：解析器会从输入流开始，维护一个栈用于存储未被归约的符号，当栈中符号满足规约条件，则会规约成对应的生产规则；
    &lt;ul&gt;
      &lt;li&gt;LR(0)、SLR、LR(1) 和 LALR(1) 都是使用了自底向上的处理方式；&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Lookahead&quot;&gt;Lookahead&lt;/a&gt;：在不同生产规则发生冲突时，解析器需要通过预读一些 Token 判断当前应该用什么生产规则对输入流进行展开或者归约，例如在 LALR(1) 文法中，需要预读一个 Token 保证出现冲突的生产规则能够被正确处理。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Go 语言的解析器使用了 LALR(1) 的文法来解析词法分析过程中输出的 Token 序列，得到 AST。&lt;/p&gt;

&lt;h3 id=&quot;ast&quot;&gt;AST&lt;/h3&gt;

&lt;p&gt;语法分析器最终会使用不同的结构体来构建抽象语法树中的节点，File 是根结点。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-Plain&quot;&gt;type File struct {
    Pragma   Pragma
    PkgName  *Name
    DeclList []Decl
    Lines    uint
    node
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;src/cmd/compile/internal/syntax/nodes.go&lt;/code&gt; 文件中也定义了其他节点的结构体，比如函数声明的结构：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-Go&quot;&gt;type (
    Decl interface {
        Node
        aDecl()
    }

    FuncDecl struct {
        Attr   map[string]bool
        Recv   *Field
        Name   *Name
        Type   *FuncType
        Body   *BlockStmt
        Pragma Pragma
        decl
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里草草了解对 AST 的说明，在「应用」一节会举一个有关 AST 的例子，这里顺便推荐一个&lt;a href=&quot;https://yuroyoro.github.io/goast-viewer/index.html&quot;&gt;在线解析 AST&lt;/a&gt; 的工具。&lt;/p&gt;

&lt;p&gt;另外，非常推荐一本详细讲解 AST 的书籍，有兴趣的读者可以查阅 https://chai2010.cn/go-ast-book/index.html。&lt;/p&gt;

&lt;h2 id=&quot;类型检查&quot;&gt;类型检查&lt;/h2&gt;

&lt;blockquote&gt;
  &lt;p&gt;过程： AST =&amp;gt; 类型检查 =&amp;gt; 关键字改写的 AST&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;得到 AST 之后，对象类型、对象值已经出来了，这时候执行类型检查是很方便的事情；如果有任何类型不匹配，则会在该阶段抛出异常，&lt;strong&gt;这个过程叫做静态类型检查&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;与静态类型检查互补的是动态类型检查，例如我们在代码中会将 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;interface{}&lt;/code&gt; 转换成具体类型，如果无法发生转换就会发生程序崩溃，那么这里实际上&lt;strong&gt;涉及到动态类型检查&lt;/strong&gt;。动态检查会依赖编译期间得到的类型信息。&lt;/p&gt;

&lt;p&gt;另外，执行类型检查的同时，会对内建函数进行一些替换操作，例如 make =&amp;gt;  &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;runtime.makeslice&lt;/code&gt; 或者 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;runtime.makechan&lt;/code&gt; 。&lt;/p&gt;

&lt;h2 id=&quot;中间代码生成&quot;&gt;中间代码生成&lt;/h2&gt;

&lt;blockquote&gt;
  &lt;p&gt;过程：AST =&amp;gt; 并发编译所有函数 =&amp;gt; SSA 等代码优化 =&amp;gt; 中间代码&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;生成中间代码之前，编译器还需要替换抽象语法树中节点的一些元素，即编程语言给开发者的语法糖。该操作将一些关键字和内建函数转换成函数调用，例如： 将 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;panic&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;recover&lt;/code&gt; 两个内建函数转换成 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;runtime.gopanic&lt;/code&gt; 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;runtime.gorecover&lt;/code&gt; 两个真正运行时函数，而关键字 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;new&lt;/code&gt; 也会被转换成调用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;runtime.newobject&lt;/code&gt; 函数。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic2.zhimg.com/v2-99e0b46092ed08986a1f632165001ae1_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;经过 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;walk&lt;/code&gt; 系列函数的处理之后，抽象语法树就不会改变了，Go 语言的编译器会将 AST 转换为具备 SSA 特性的中间代码。&lt;/p&gt;

&lt;p&gt;我们能在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GOSSAFUNC=func_name go build main.go&lt;/code&gt; 命令生成的文件中，看到指定函数 func_name，每一轮处理后的中间代码。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic3.zhimg.com/v2-ae1962e96cfb4f231a5b569116be9de6_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;中间代码是一种更接近机器语言的表示形式，对中间代码的优化和分析相比直接分析高级编程语言更容易。&lt;/p&gt;

&lt;h2 id=&quot;机器码生成&quot;&gt;机器码生成&lt;/h2&gt;

&lt;p&gt;SSA 输出结果跟最后生成的汇编代码已经非常相似了，随后调用的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cmd/compile/internal/gc.Progs.Flush&lt;/code&gt; 会使用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cmd/internal/obj&lt;/code&gt; 包中的汇编器将 SSA 转换成汇编代码。&lt;/p&gt;

&lt;p&gt;我们可以使用命令生成汇编代码，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GOOS=linux GOARCH=amd64 go tool compile -S main.go&lt;/code&gt;。这种方式生产的并非标准的汇编代码，而是上节提到的中间代码。不过进行分析的话也可以使用。&lt;/p&gt;

&lt;p&gt;如果想获得更准确，并且更加标准化的汇编代码，可以使用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;go tool objdump -s &amp;lt;interesting_function_&amp;gt; main&lt;/code&gt;&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-Go&quot;&gt;usage: go tool objdump [-S] [-gnu] [-s symregexp] binary [start end]
  -S    print Go code alongside assembly
  -gnu  print GNU assembly next to Go assembly (where supported)
  -s string
        only dump symbols matching this regexp
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&quot;自举&quot;&gt;自举&lt;/h2&gt;

&lt;p&gt;自举的定义是，使用 Golang 编写的程序来构建 Golang 编写的程序。实际上，要构建 x ≥ 5 的 Go 1.x，必须在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$GOROOT_BOOTSTRAP&lt;/code&gt; 中已经安装 Go 1.4（或更高版本）。而 Go 1.4 本身是依赖 C 语言的。所以这个自举过程其实也并不神秘。&lt;/p&gt;

&lt;h1 id=&quot;应用&quot;&gt;应用&lt;/h1&gt;

&lt;h2 id=&quot;机器码&quot;&gt;机器码&lt;/h2&gt;

&lt;h3 id=&quot;monkey-patch&quot;&gt;Monkey Patch&lt;/h3&gt;

&lt;blockquote&gt;
  &lt;p&gt;原理：获取 from 的函数地址 =&amp;gt; 将跳转 to 的汇编指令替换 from 的函数体&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Monkey Patch 是一个实现函数替换的工具，通常用在本地测试 Mock 数据的场景。这里简单介绍函数替换的实现原理。&lt;/p&gt;

&lt;p&gt;举个例子，对于下述程序，我们的目标是实现某个函数&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;replace(a, b)&lt;/code&gt;，使得调用函数 a 的时候，实际上运行函数 b。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-Go&quot;&gt;package main

func a() int { return 1 }

func b() int { return 2 }

func main() {
  // replace(a, b)
  print(a())
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;先看其汇编代码：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic4.zhimg.com/v2-c0a607db20abb989db44f2529e843bcf_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;再强调一下，我们的目标是调用函数 a 的时候，实际上调用 b。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;可以看到 0x2000-0x2009 是函数 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;main.a&lt;/code&gt; 的函数体。&lt;/p&gt;

&lt;p&gt;那么，要达到目标，需要把下述汇编代码替换到 0x2000-0x2009 的内存位置。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-Assembly&quot;&gt;mov rdx, main.b.f
jmp [rdx]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;因此，将汇编对应的机器码（字节表示，可以使用&lt;a href=&quot;http://shell-storm.org/online/Online-Assembler-and-Disassembler/?inst=mov+rdx%2C+0x1&amp;amp;arch=x86-64#assembly&quot;&gt;在线汇编器&lt;/a&gt;进行转换）强制拷贝到函数&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;main.a&lt;/code&gt; 的内存位置即可。&lt;/p&gt;

&lt;p&gt;详细可以查阅原文，这里只是简单介绍一下实现的过程。&lt;/p&gt;

&lt;p&gt;https://berryjam.github.io/2018/12/golang%E6%9B%BF%E6%8D%A2%E8%BF%90%E8%A1%8C%E6%97%B6%E5%87%BD%E6%95%B0%E4%BD%93%E5%8F%8A%E5%85%B6%E5%8E%9F%E7%90%86/&lt;/p&gt;

&lt;h3 id=&quot;性能分析&quot;&gt;性能分析&lt;/h3&gt;

&lt;p&gt;我们借助 pprof 工具，对性能瓶颈进行分析。找到到更具体的问题代码块，可以再通过汇编分析等方法，定位到影响性能的代码。&lt;/p&gt;

&lt;p&gt;下图是嵌套指针导致的指令依赖，而无法充分利用指令并行的一个例子。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic3.zhimg.com/v2-0410a963c4eb93585db3c156e77aeb26_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;通过汇编指令分析性能瓶颈，帮助机器更好地优化，例如：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;充分利用指令并行&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;避免不可预测的分支&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;提高指令缓存命中率&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;详细可以参阅 CSAPP 的第五章，之前记了相关的一段笔记：&lt;a href=&quot;https://sarkerson.github.io/2021/08/22/优化程序性能-CSAPP-笔记&quot;&gt;第五章 优化程序性能&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;ast-1&quot;&gt;AST&lt;/h2&gt;

&lt;h3 id=&quot;魔改代码&quot;&gt;魔改代码&lt;/h3&gt;

&lt;p&gt;有时候，你需要批量修改代码。例如，你需要给很多个 Handler 添加一个公有的 BaseCheck 逻辑。&lt;/p&gt;

&lt;p&gt;这时候，可选的做法是，加一个 BaseHandler 实现改 BaseCheck 方法，让其他 Handler 嵌套 BaseHandler 从而继承该公有方法。然后在所有的 Handler 的 check 方法中，调用继承过来的 BaseCheck 方法，即新增下述高亮部分的代码。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-Go&quot;&gt;package handler

import &quot;package_path/biz/base_handler&quot;

type HandlerA struct {
    *base_handler.BaseHandler
}

func (h *HandlerA) check() error {
    // other checks by handler A
    // other checks by handler A
    if err := h.BaseCheck(); err != nil { // inherit from BaseHandler
        return err
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;问题来了，当 handler 非常多（大型项目可能有几十上百个）时，手动给每个 handler 文件添加上述逻辑显然不是很爽。&lt;/p&gt;

&lt;p&gt;回顾一下上面提到的 AST，我们可以利用 AST 解析整个 go 文件，得到所有的节点。那么当我知道需要在哪些节点上新增代码，便可以写代码来生成这部分代码，我们把这个工具暂称 generator。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;这里面涉及两个步骤：&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;识别目标节点&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;插入新节点（要新增的代码块）&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;我们可以先手动对任何一个 handler 加这部分代码，然后观察其 AST，模仿该 AST 来编写 generator。上文提到一个&lt;a href=&quot;https://yuroyoro.github.io/goast-viewer/index.html&quot;&gt;在线解析 AST&lt;/a&gt; 的工具，我们把已经写好的 handler 贴进去，找到我们感兴趣的那部分逻辑。&lt;/p&gt;

&lt;p&gt;例如你要给每个 Handler 的声明添加 *base_handler.BaseHandler 这个 field，我们来看对应的代码以及 AST：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-Go&quot;&gt;type Handler struct {
        *base_handler.BaseHandler
        reqCtx *request.RequestContext
}
     0  *ast.File {
     3  .  Name: *ast.Ident {
     5  .  .  Name: &quot;xx_handler&quot;
     7  .  }
     8  .  Decls: []ast.Decl (len = 5) {
     9  .  .  0: *ast.GenDecl {}
    95  .  .  1: *ast.GenDecl {
    96  .  .  .  Doc: nil
    98  .  .  .  Tok: type
   100  .  .  .  Specs: []ast.Spec (len = 1) {
   101  .  .  .  .  0: *ast.TypeSpec {
   102  .  .  .  .  .  Doc: nil
   103  .  .  .  .  .  Name: *ast.Ident {
   105  .  .  .  .  .  .  Name: &quot;Handler&quot;
   106  .  .  .  .  .  .  Obj: *ast.Object {
   107  .  .  .  .  .  .  .  Kind: type
   108  .  .  .  .  .  .  .  Name: &quot;Handler&quot;
   112  .  .  .  .  .  .  }
   113  .  .  .  .  .  }
   115  .  .  .  .  .  Type: *ast.StructType {
   117  .  .  .  .  .  .  Fields: *ast.FieldList {
   119  .  .  .  .  .  .  .  List: []*ast.Field (len = 2) {
   120  .  .  .  .  .  .  .  .  0: *ast.Field {
   121  .  .  .  .  .  .  .  .  .  Doc: nil
   122  .  .  .  .  .  .  .  .  .  Names: nil
   123  .  .  .  .  .  .  .  .  .  Type: *ast.StarExpr {
   124  .  .  .  .  .  .  .  .  .  .  Star: foo:15:2
   125  .  .  .  .  .  .  .  .  .  .  X: *ast.SelectorExpr {
   126  .  .  .  .  .  .  .  .  .  .  .  X: *ast.Ident {
   127  .  .  .  .  .  .  .  .  .  .  .  .  NamePos: foo:15:3
   128  .  .  .  .  .  .  .  .  .  .  .  .  Name: &quot;base_handler&quot;
   129  .  .  .  .  .  .  .  .  .  .  .  .  Obj: nil
   130  .  .  .  .  .  .  .  .  .  .  .  }
   131  .  .  .  .  .  .  .  .  .  .  .  Sel: *ast.Ident {
   132  .  .  .  .  .  .  .  .  .  .  .  .  NamePos: foo:15:16
   133  .  .  .  .  .  .  .  .  .  .  .  .  Name: &quot;BaseHandler&quot;
   134  .  .  .  .  .  .  .  .  .  .  .  .  Obj: nil
   135  .  .  .  .  .  .  .  .  .  .  .  }
   136  .  .  .  .  .  .  .  .  .  .  }
   137  .  .  .  .  .  .  .  .  .  }
   138  .  .  .  .  .  .  .  .  .  Tag: nil
   139  .  .  .  .  .  .  .  .  .  Comment: nil
   140  .  .  .  .  .  .  .  .  }
   141  .  .  .  .  .  .  .  .  1: *ast.Field {
   142  .  .  .  .  .  .  .  .  // ...
   183  .  .  }
&lt;/code&gt;&lt;/pre&gt;

&lt;h4 id=&quot;更改目标节点&quot;&gt;更改目标节点&lt;/h4&gt;

&lt;p&gt;观察上述 AST，可以发现 *ast.File 这个源文件节点下面，声明部分 Decls 就有 Handler 的声明语句。这个声明中有一个 Fields 节点，就是 Handler 的字段定义。&lt;/p&gt;

&lt;p&gt;根据该 AST 可以很方便写出目标节点的识别代码，本文使用了 “golang.org/x/tools/go/ast/astutil” 这个库，相比于 “go/ast” 库，astutil 支持获取节点的父节点等功能。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-Go&quot;&gt;func main() {
    fset := token.NewFileSet()
    file, err := parser.ParseFile(fset, &quot;input.go&quot;, nil, parser.ParseComments)
    if err != nil {
        log.Fatal(err)
        return
    }

    astutil.Apply(file, nil, func(c *astutil.Cursor) bool {
        n := c.Node()
        
        switch x := n.(type) {
        
        case *ast.File:
        
        for _, decl := range x.Decls {
            if genDecl, ok := decl.(*ast.GenDecl); ok {
                for _, spec := range genDecl.Specs {
                    switch dx := spec.(type) {
                    case *ast.TypeSpec:
                        // 下面判断是 type struct 声明，并且名称以 Handler 结尾
                        if dx.Name != nil &amp;amp;&amp;amp; strings.HasSuffix(dx.Name.Name, &quot;Handler&quot;) {
                            if stype, ok := dx.Type.(*ast.StructType); ok {
                                // checkIfAddBaseHandlerDone 判断是否已经插入过该 field
                                if !checkIfAddBaseHandlerDone(stype.Fields) {
                                    // 在 fields.List 中插入 baseHandler 的 field
                                    stype.Fields.List = append([]*ast.Field{newAddBaseHandlerField()}, stype.Fields.List...)
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    printer.Fprint(os.Stdout, fset, file)
}

func newAddBaseHandlerField() *ast.Field {
    return &amp;amp;ast.Field{
        Names: nil,
        Type: &amp;amp;ast.StarExpr{
            X: &amp;amp;ast.SelectorExpr{
                X: &amp;amp;ast.Ident{
                    Name: &quot;base_handler&quot;,
                },
                Sel: &amp;amp;ast.Ident{
                    Name: &quot;BaseHandler&quot;,
                },
            },
        },
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&quot;expr&quot;&gt;expr&lt;/h3&gt;

&lt;p&gt;expr 自己实现了一套语法，因此也有自己的一整套编译过程。只不过得到了 AST 之后，转换为命令+参数的形式用栈来模拟程序的执行。&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;首先是 source code =&amp;gt; token，使用状态机自底向上进行分析，&lt;a href=&quot;https://github.com/antonmedv/expr/blob/master/parser/lexer/lexer.go#L11&quot;&gt;expr/parser/lexer/lexer.go&lt;/a&gt;，下面是状态机的部分代码，当匹配到期望的规则时，则使用对应的生产规则跳转到下一个状态，因此是自顶向下分析，输出预设的 token 列表。&lt;/li&gt;
&lt;/ol&gt;

&lt;pre&gt;&lt;code class=&quot;language-Go&quot;&gt;func root(l *lexer) stateFn {
        switch r := l.next(); {
        case r == eof:
                l.emitEOF()
                return nil
        case IsSpace(r):
                l.ignore()
                return root
        case r == &apos;\&apos;&apos; || r == &apos;&quot;&apos;:
                l.scanString(r)
                str, err := unescape(l.word())
                if err != nil {
                        l.error(&quot;%v&quot;, err)
                }
                l.emitValue(String, str)
        case &apos;0&apos; &amp;lt;= r &amp;amp;&amp;amp; r &amp;lt;= &apos;9&apos;:
                l.backup()
                return number
        default:
                return l.error(&quot;unrecognized character: %#U&quot;, r)
        }
        return root
}
&lt;/code&gt;&lt;/pre&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;其次，token =&amp;gt; AST，当 token 确定之后，可以很方便构建一棵树。个人觉得这部分代码写得有点挫，详细可以看：&lt;a href=&quot;https://github.com/antonmedv/expr/blob/master/parser/parser.go#L145&quot;&gt;expr/parser/parser.go&lt;/a&gt;&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;下面是 AST =&amp;gt; 程序命令的例子，三目的条件运算符的解析过程：&lt;a href=&quot;https://github.com/antonmedv/expr/blob/master/compiler/compiler.go#L623&quot;&gt;expr/compile/compiler.go&lt;/a&gt;&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;pre&gt;&lt;code class=&quot;language-Go&quot;&gt;func (c *compiler) ConditionalNode(node *ast.ConditionalNode) {
        c.compile(node.Cond)
        otherwise := c.emit(OpJumpIfFalse, c.placeholder()...)

        c.emit(OpPop)
        c.compile(node.Exp1)
        end := c.emit(OpJump, c.placeholder()...)

        c.patchJump(otherwise)
        c.emit(OpPop)
        c.compile(node.Exp2)

        c.patchJump(end)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;ol&gt;
  &lt;li&gt;下面是执行函数的相关代码，&lt;a href=&quot;https://github.com/antonmedv/expr/blob/master/vm/vm.go#L281&quot;&gt;expr/vm/vm.go&lt;/a&gt;，模拟了计算机取值、执行的过程。会将存在 bytecode 中的指令取出，并从 constant 中取出指令对应的参数进行执行。由于指令都是预设的，因此 constant 只需要根据预设进行出栈入栈即可。&lt;/li&gt;
&lt;/ol&gt;

&lt;pre&gt;&lt;code class=&quot;language-Go&quot;&gt;op := vm.bytecode[vm.pp]
switch op {
case OpCall:
        call := vm.constant().(Call)
        in := make([]reflect.Value, call.Size)
        for i := call.Size - 1; i &amp;gt;= 0; i-- {
                param := vm.pop()
                if param == nil &amp;amp;&amp;amp; reflect.TypeOf(param) == nil {
                        // In case of nil value and nil type use this hack,
                        // otherwise reflect.Call will panic on zero value.
                        in[i] = reflect.ValueOf(&amp;amp;param).Elem()
                } else {
                        in[i] = reflect.ValueOf(param)
                }
        }
        out := FetchFn(env, call.Name).Call(in)
        if len(out) == 2 &amp;amp;&amp;amp; out[1].Type() == errorType &amp;amp;&amp;amp; !out[1].IsNil() {
                return nil, out[1].Interface().(error)
        }
        vm.push(out[0].Interface())
}
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&quot;gocover&quot;&gt;gocover&lt;/h3&gt;

&lt;p&gt;go test coverage 采用的方法是在生成的 AST 上直接进行编辑，注入统计逻辑，然后根据编辑后的 AST 反向生成代码。&lt;/p&gt;

&lt;h1 id=&quot;reference&quot;&gt;Reference&lt;/h1&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;a href=&quot;https://xargin.com/ast/&quot;&gt;xargin.com&lt;/a&gt;&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;a href=&quot;https://draveness.me/golang/docs/part1-prerequisite/ch02-compile/golang-compile-intro/&quot;&gt;Go 语言编译过程概述&lt;/a&gt;&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;
</description>
			<pubDate>Mon, 01 Aug 2022 00:00:00 +0000</pubDate>
			<link>https://sarkerson.github.io/2022/08/01/%E6%B5%85%E8%B0%88-Golang-%E7%BC%96%E8%AF%91%E5%8E%9F%E7%90%86%E5%8F%8A%E5%85%B6%E5%BA%94%E7%94%A8/</link>
			<guid isPermaLink="true">https://sarkerson.github.io/2022/08/01/%E6%B5%85%E8%B0%88-Golang-%E7%BC%96%E8%AF%91%E5%8E%9F%E7%90%86%E5%8F%8A%E5%85%B6%E5%BA%94%E7%94%A8/</guid>
        
			<category>golang</category>
        
			<category>os</category>
        
        
		</item>
    
		<item>
			<title>算法：二分搜索的边界与统一模板</title>
			<description>&lt;h1 id=&quot;算法二分搜索的边界与统一模板&quot;&gt;算法：二分搜索的边界与统一模板&lt;/h1&gt;

&lt;p&gt;One Template for All Binary Search Problems&lt;/p&gt;

&lt;h2 id=&quot;背景&quot;&gt;背景&lt;/h2&gt;

&lt;p&gt;首先，面对二分搜索，我们往往会碰到以下疑问：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;left&lt;/code&gt; 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;right&lt;/code&gt; 初始值？&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;循环条件是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;left &amp;lt; right&lt;/code&gt; 还是&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;left &amp;lt;= right&lt;/code&gt; ？&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;如何更新&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;left&lt;/code&gt; 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;right&lt;/code&gt; ？&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;left = mid&lt;/code&gt;，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;left = mid + 1&lt;/code&gt;，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;right = mid&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;right = mid — 1&lt;/code&gt; ?&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;结束时要选&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;left&lt;/code&gt; 还是&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;right&lt;/code&gt; ？&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;另外，二分搜索有多个场景，&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;标准的二分查找&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;二分查找左边界&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;二分查找右边界&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;二分查找极值点&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;对于上述多个场景，网上也有相关的模板，但是很多都是使用多个模版来针对性解决不同场景的应用问题。&lt;/p&gt;

&lt;p&gt;但是，其实只要&lt;strong&gt;一个模板&lt;/strong&gt;就够了！可能有很多大佬在用，本文只作总结，并&lt;strong&gt;提炼出统一的方法来应对上述不同场景&lt;/strong&gt;。&lt;/p&gt;

&lt;h2 id=&quot;模版&quot;&gt;模版&lt;/h2&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;binary_search&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;array&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;  
    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;condition&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;bool&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;  
        &lt;span class=&quot;k&quot;&gt;pass&lt;/span&gt;  
    &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;right&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;search_space_&lt;/span&gt;  
    &lt;span class=&quot;k&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;right&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;  
        &lt;span class=&quot;n&quot;&gt;mid&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;right&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;//&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;  
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;condition&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;  
            &lt;span class=&quot;n&quot;&gt;right&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mid&lt;/span&gt;  
        &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;  
            &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mid&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;  
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;几个注意点：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;left&lt;/code&gt; 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;right&lt;/code&gt; 是解空间的闭区间&lt;/li&gt;
  &lt;li&gt;循环结束时，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;**left**&lt;/code&gt; &lt;strong&gt;是满足&lt;/strong&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;**condition**&lt;/code&gt; &lt;strong&gt;的最小值&lt;/strong&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;right&lt;/code&gt;的取值是有讲究的，下面会提到&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;应用&quot;&gt;应用&lt;/h2&gt;

&lt;p&gt;下面使用这一个模板来解决几个常见场景的问题。&lt;/p&gt;

&lt;h3 id=&quot;寻找插入点&quot;&gt;寻找插入点&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/search-insert-position/&quot;&gt;https://leetcode.cn/problems/search-insert-position&lt;/a&gt;&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;searchInsert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nums&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;  
    &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;right&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;len&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nums&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;right&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;  
        &lt;span class=&quot;n&quot;&gt;mid&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;right&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;//&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;  
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nums&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;  
        &lt;span class=&quot;n&quot;&gt;right&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mid&lt;/span&gt;  
    &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;  
        &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mid&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;  

    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;几个注意点：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;right = len(nums)&lt;/code&gt;：上面提到，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;left&lt;/code&gt; 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;right&lt;/code&gt; 是解空间的闭区间；当 target 比所有值大时，必定是落在索引&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;len(nums)&lt;/code&gt;上面。&lt;/li&gt;
  &lt;li&gt;返回值&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;left&lt;/code&gt;是满足&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nums[mid] ≥ target&lt;/code&gt;的最小值，即要么等于要么大于&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;寻找左边界&quot;&gt;寻找左边界&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/find-first-and-last-position-of-element-in-sorted-array/&quot;&gt;力扣&lt;/a&gt;&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# ====== left bound ===========
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;left&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;right&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;len&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nums&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;right&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;  
    &lt;span class=&quot;n&quot;&gt;mid&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;right&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;//&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;  
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nums&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;  
        &lt;span class=&quot;n&quot;&gt;right&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mid&lt;/span&gt;  
    &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;  
    &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mid&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nums&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;left&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;  
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;几个注意点：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;返回值&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;left&lt;/code&gt;是满足&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nums[mid] ≥ target&lt;/code&gt;的最小值：即要么找到等于 target 的最小值，返回该左边界；要么找到大于 target 的最小值，返回 -1&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;寻找右边界&quot;&gt;寻找右边界&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/find-first-and-last-position-of-element-in-sorted-array/&quot;&gt;力扣&lt;/a&gt;&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# ====== right bound ===========
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;left&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;right&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;len&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nums&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;right&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;  
    &lt;span class=&quot;n&quot;&gt;mid&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;right&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;//&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;  
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nums&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;  
        &lt;span class=&quot;n&quot;&gt;right&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mid&lt;/span&gt;  
    &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;  
        &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mid&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;  
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nums&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;left&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;  
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;  
    &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;  
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nums&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;left&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;  
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;几个注意点：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;right = len(nums)&lt;/code&gt;：上面提到，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;left&lt;/code&gt; 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;right&lt;/code&gt; 是解空间的闭区间；当 target 比所有值大时，必定是落在索引&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;len(nums)&lt;/code&gt;上面。&lt;/li&gt;
  &lt;li&gt;返回值&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;left&lt;/code&gt;是满足&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nums[mid] &amp;gt; target&lt;/code&gt;的最小值，因此解是&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;left-1&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;if left == 0&lt;/code&gt; 的判断：因为返回值是&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;left-1&lt;/code&gt; ，因此多了这么一步判断&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;寻找极值点&quot;&gt;寻找极值点&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/find-peak-element/&quot;&gt;力扣&lt;/a&gt;&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;findPeakElement&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nums&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;  
    &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;right&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;len&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nums&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;  
    &lt;span class=&quot;k&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;right&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;  
        &lt;span class=&quot;n&quot;&gt;mid&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;right&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;//&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;  
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nums&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nums&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mid&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt;  
            &lt;span class=&quot;n&quot;&gt;right&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mid&lt;/span&gt;  
        &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;  
            &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mid&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;注意点：返回值&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;left&lt;/code&gt;是满足&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nums[mid] &amp;gt; nums[mid+1]&lt;/code&gt;的最小值&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;综合应用题&quot;&gt;综合应用题&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/search-in-rotated-sorted-array-ii/&quot;&gt;力扣&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;寻找分割点：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;left&lt;/code&gt; 是满足条件的最小值，因此我们找的是右半段的左边界，并且由此计算左半段的右边界，重新赋值给&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;left&lt;/code&gt;；&lt;/li&gt;
  &lt;li&gt;得到两段为 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[start, left]&lt;/code&gt; 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[left + 1, end]&lt;/code&gt; ；&lt;/li&gt;
  &lt;li&gt;判断 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;target&lt;/code&gt; 所在的段，并进行普通二分搜索&lt;/li&gt;
&lt;/ol&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;search&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nums&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;bool&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;  
    &lt;span class=&quot;n&quot;&gt;n&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;len&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nums&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;  
    &lt;span class=&quot;n&quot;&gt;start&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;n&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;  
    &lt;span class=&quot;k&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;start&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;and&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nums&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;start&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nums&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;end&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt;  
        &lt;span class=&quot;n&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;# 1. 寻找分割点（小于 nums[start] 的第一个值）
&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;right&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;start&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;end&lt;/span&gt;  
    &lt;span class=&quot;k&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;right&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;  
        &lt;span class=&quot;n&quot;&gt;mid&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;right&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;//&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;  
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nums&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nums&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;start&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt;  
            &lt;span class=&quot;n&quot;&gt;right&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mid&lt;/span&gt;  
        &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;  
            &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mid&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;# 1.1. 如果存在分割点，则将 left 作为左半段的右边界
&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nums&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;start&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nums&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;left&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt;  
        &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nums&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;start&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;  
        &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;right&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;start&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt;  
    &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;  
        &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;right&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;end&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;# 2. 普通二分搜索
&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;right&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;  
        &lt;span class=&quot;n&quot;&gt;mid&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;right&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;//&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;  
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nums&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;  
            &lt;span class=&quot;n&quot;&gt;right&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mid&lt;/span&gt;  
        &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;  
            &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mid&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;# 2.2. 小插曲，通过测试用例排除  
&lt;/span&gt;    &lt;span class=&quot;c1&quot;&gt;# 出现的原因是上面进行了 left, right = left + 1, end 的赋值  
&lt;/span&gt;    &lt;span class=&quot;c1&quot;&gt;# 导致 left &amp;gt; right，因此需要作此判断
&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;end&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;  
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nums&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;left&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;target&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;结论&quot;&gt;结论&lt;/h2&gt;

&lt;p&gt;记住下面两句话，重新刷一下二分搜索，你会发现如鱼得水。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;left&lt;/code&gt; 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;right&lt;/code&gt; 是解空间的闭区间&lt;/li&gt;
  &lt;li&gt;循环结束时，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;**left**&lt;/code&gt; &lt;strong&gt;是满足&lt;/strong&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;**condition**&lt;/code&gt; &lt;strong&gt;的最小值&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
			<pubDate>Fri, 20 May 2022 00:00:00 +0000</pubDate>
			<link>https://sarkerson.github.io/2022/05/20/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/</link>
			<guid isPermaLink="true">https://sarkerson.github.io/2022/05/20/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/</guid>
        
			<category>algorithm</category>
        
        
		</item>
    
		<item>
			<title>接口设计：后端工程师视角下的 GraphQL</title>
			<description>&lt;h1 id=&quot;接口设计后端工程师视角下的-graphql&quot;&gt;接口设计：后端工程师视角下的 GraphQL&lt;/h1&gt;

&lt;blockquote&gt;
  &lt;p&gt;TLDR；本文是一个对 BFF 思想的学习与调研，也对业界经常用来实现 BFF 的 GraphQL 进行一些介绍。本文重点在于对业界方案进行的调研与小结，并在文末总结一种可以实践的 BFF 思路。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1 id=&quot;概述&quot;&gt;概述&lt;/h1&gt;

&lt;p&gt;有些人觉得 GraphQL 与微服务思想相悖，其观点在于&lt;a href=&quot;https://draveness.me/graphql-microservice/&quot;&gt;中心化与去中心化&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;但本人认为，GraphQL 思想与微服务（确切来说是领域服务）是不谋而合的，服务端可以更加面向领域对象编程，而非面向场景编程，因为借鉴该思想，服务端可以省去了维护面向场景的聚合逻辑，可以更加关注领域服务的开发。&lt;/p&gt;

&lt;h1 id=&quot;graphql-vs-restful&quot;&gt;GraphQL vs RESTful&lt;/h1&gt;

&lt;p&gt;这里简单介绍一下 GraphQL 的思路。在 Web 服务器中，RESTful 应该是最常见的规范。在这里通过简单的对比 RESTful 与 GraphQL 的区别，来介绍什么是 GraphQL，以及为什么需要它。&lt;/p&gt;

&lt;p&gt;下面举了一个简单的例子，一个页面需要获取多个数据来进行渲染：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;左图是经典的 RestFul 的做法，请求多个接口进行拼凑；&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;右图是 GraphQL，只有一个 path，通过类似 SQL 思想的 query 来获取数据；&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;因此 GraphQL 的名字非常生动形象，抽象了从多种异构存储或者服务中获取数据的过程。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic4.zhimg.com/v2-5f8355edc68df2b74d23a494c810a733_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;下面简单介绍 GraphQL 对于 RestFul 的一些优劣。&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;GraphQL 优劣讨论&lt;/th&gt;
      &lt;th&gt;优点&lt;/th&gt;
      &lt;th&gt;缺点&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;宏观&lt;/td&gt;
      &lt;td&gt;服务端倾向于提供单一职责服务（微服务+领域驱动设计）&lt;/td&gt;
      &lt;td&gt;URI 对应资源路径，传统的 RestFul 对每个场景使用单独的 URI 获取对应资源&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;开发体验-前端&lt;/td&gt;
      &lt;td&gt;只需要请求一个接口，并且请求参数是直观的，按需取数后端自动处理路由逻辑，前端不需要执行多个请求有开源工具自动生成接口文档，不需要担心接口文档没有维护、更新&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;开发体验-服务端&lt;/td&gt;
      &lt;td&gt;更加专注领域服务的构建，而不是面向场景编程，提高领域层逻辑的复用不需要关注接口应该拆分还是聚合的问题，职责交给网关不需要配置很多路由&lt;/td&gt;
      &lt;td&gt;原生 GraphQL 对每种展示字段需要新开辟一个字段解决（或者将逻辑下沉到终端，但是又缺少了灵活性）对于复杂业务，数据图可能非常大&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;性能&lt;/td&gt;
      &lt;td&gt;避免返回不必要字段耗费带宽避免 n+1 问题（fetch list + n * get(item)）&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;稳定性&lt;/td&gt;
      &lt;td&gt;内容聚合大大减少了 API 请求的次数，比如数据字段可选大大减少无用流量的传输schema 文件中的强类型，极大的降低程序 crash 的风险。&lt;/td&gt;
      &lt;td&gt;没法基于场景进行以下监控或者操作，只能基于字段超时控制：不同场景下，相同字段超时可能存在区别流量监控：基于场景的流量监控是刚需熔断机制：基于字段的熔断可能导致误伤&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;鉴权&lt;/td&gt;
      &lt;td&gt;GraphQL 内置的 Directives 模块可以进行权限控制，包括字段级别，对象级别，接口级别，颗粒度非常细；RESTful 正常情况下只能做到路由级别的权限控制，接口里面的字段难以进行分级控制；&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;容灾&lt;/td&gt;
      &lt;td&gt;CDN：同样可以实现基于 query 的缓存&lt;/td&gt;
      &lt;td&gt;限流：基于场景的限流是刚需&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h1 id=&quot;现有问题&quot;&gt;现有问题&lt;/h1&gt;

&lt;p&gt;下面列举了一些现有开发中常见的问题，这些问题可以直接使用 GraphQL，或者借鉴 GraphQL 的思路来解决。&lt;/p&gt;

&lt;h2 id=&quot;前后端协同&quot;&gt;前后端协同&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;微服务演进过程中，后端服务/接口骤增，客户端/前端开发需求时，单个页面进行多次请求后端服务，开发效率低下且数据加载时延较高，页面偏卡顿；&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;前端期望接口自由请求数据以进行快速迭代，而不需要等待服务端将新接口上线（对于那些已经有打包逻辑的领域对象）；&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;服务端的数据模型在版本迭代中，会越来越膨胀，因为它相当于多个版本数据的一个并集，不同版本使用数据模型中的不同字段；&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;服务端往往在适配客户端/前端不同需求时，需要对数据进行加工，导致服务端掺杂UI逻辑，边界不清晰，不能更好的专注于领域逻辑；&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;对于不同版本的客户端，服务端需要做非常多的兼容逻辑，导致维护成本较大；&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;微服务职责划分&quot;&gt;微服务职责划分&lt;/h2&gt;

&lt;p&gt;由于微服务架构的广泛应用，数量非常容易膨胀。这些微服务要么职能过于扁平，比如专门提供各种计数；要么职能过多，彼此之间存在交叠，比如多个内容服务提供了用户数据、作者数据、评论数据。&lt;/p&gt;

&lt;p&gt;另外，传统的面向场景编程，如果设计不当，很容易陷入领域对象与场景对象划分的纠结之中。比如说，文章详情页这个场景，返回给前端的场景对象，是否要直接使用文章的领域对象，通常来说两者应该隔离开，使用两个不同的对象进行处理。但是，对于没有经验的工程师来说，通常会陷入二者的界线划分的难题之中，&lt;strong&gt;一旦划分失误，那么领域层将避免不了耦合一些场景层的展示逻辑。&lt;/strong&gt;&lt;/p&gt;

&lt;h2 id=&quot;打包服务&quot;&gt;打包服务&lt;/h2&gt;

&lt;p&gt;打包服务通常作为一个聚合服务存在，有点类似网关，通过请求指定所需的字段，从各种异构数据源获取数据，并打包为协议定义的对象。&lt;/p&gt;

&lt;p&gt;传统的打包服务流程大致如下：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;通过 loader manager 定义该 pack 请求的下游依赖，构成 loader 的 DAG 图；&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;执行所有 loader 进行数据获取，将数据放到透传上下文 datum 中；&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;所有 loader 执行完毕（或者超时），执行 packer 的打包逻辑，将 datum 映射为 packed doc；&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;img src=&quot;https://pic4.zhimg.com/v2-91433f706b3cb750452d7b074a334247_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;但是，这样的做法存在一些问题：&lt;/p&gt;

&lt;h3 id=&quot;选择性打包&quot;&gt;选择性打包&lt;/h3&gt;

&lt;p&gt;典型的基于 pack fields 的选择性打包，通常需要在代码层对每个 field 进行依赖的编排逻辑，会导致代码非常冗长。而如果对 field 进行分组，类似抖音的 pack level，则粒度又过大。&lt;/p&gt;

&lt;h3 id=&quot;字段依赖&quot;&gt;字段依赖&lt;/h3&gt;

&lt;p&gt;上述打包流程中，我们需要手动维护 loader 之间的依赖图。理想的做法是，通过指定每个字段对应的领域服务及对象依赖，自动解决依赖编排逻辑。&lt;/p&gt;

&lt;h3 id=&quot;超时机制&quot;&gt;超时机制&lt;/h3&gt;

&lt;p&gt;典型的基于 loader_manager 的超时机制，粒度太大，因为各个子 loader 的超时可能不同，取决于子 loader 里面耗时最长的那个。但这个超时不好确定，一旦定不合理，比如说为了某一个短板 loader 而调高耗时，很容易导致上游拿不到整个数据。&lt;/p&gt;

&lt;h3 id=&quot;维护成本&quot;&gt;维护成本&lt;/h3&gt;

&lt;p&gt;上面可以看到，loader 与 packer 的逻辑是解耦开的。这意味着，如果要修改某个 packed doc 返回值字段，你需要去定位该字段来自 datum 的哪个值，datum 这个值又是在哪个 loader 赋值的。而基于 GraphQL 的思想，则可以将字段的赋值逻辑收敛在 resolver 中，一定程度上可以降低维护成本。&lt;/p&gt;

&lt;h2 id=&quot;字段权限控制&quot;&gt;字段权限控制&lt;/h2&gt;

&lt;p&gt;目前只有接口级别的权限控制，希望细化到字段 =&amp;gt; psm 的维度的细粒度鉴权。&lt;/p&gt;

&lt;h2 id=&quot;服务治理&quot;&gt;服务治理&lt;/h2&gt;

&lt;p&gt;通常业务会倾向于复用接口来提供服务，比如信息流会使用同一个 path 提供数据，根据入参进行业务逻辑编排。&lt;/p&gt;

&lt;p&gt;但是这样也引入了一个问题，很难对具体的类型进行不同的超时处理。并且，针对具体的请求也很难统一进行一些埋点监控。&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;区分当前服务处理错误，与上游服务调用该服务处理超时。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;当然埋点监控可以通过在入口层统一使用中间件来解决。但是对于超时处理、熔断，则难以复用 mesh 的服务治理能力。&lt;/p&gt;

&lt;h1 id=&quot;业界方案&quot;&gt;业界方案&lt;/h1&gt;

&lt;h2 id=&quot;美团&quot;&gt;美团&lt;/h2&gt;

&lt;blockquote&gt;
  &lt;p&gt;https://tech.meituan.com/2021/05/06/bff-graphql.html&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;美团的思路比较优雅，但是私以为文章描述不是很清晰，这里尽所能对各个模块进行介绍。&lt;/p&gt;

&lt;h3 id=&quot;取数展示分离&quot;&gt;取数展示分离&lt;/h3&gt;

&lt;blockquote&gt;
  &lt;p&gt;All problems in computer science can be solved by another level of indirection.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;img src=&quot;https://pic2.zhimg.com/v2-8c24b8aab9c83c5d17303b7eee2125d5_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;主要目的在于，避免在数据图中，混入展示层的逻辑。&lt;/p&gt;

&lt;p&gt;传统的 GraphQL 方案中，每个 field 对应相应的 resolver，通常也需要对应单独的取数 / 打包，这样必然导致数据图非常大，而且其中包括很多冗余字段（比如 title、category、title_with_category 同时存在）。&lt;/p&gt;

&lt;p&gt;通过取数和展示的分离，元数据的关联和运行时的组合调用，可以保持逻辑单元的简单，同时又满足复用诉求，这也很好地解决了传统方案中存在的展示服务的颗粒度问题。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;相当于，&lt;/strong&gt;&lt;strong&gt;GraphQL&lt;/strong&gt; &lt;strong&gt;充当了字段计算的职责，数据图负责更&lt;/strong&gt;&lt;strong&gt;原子化&lt;/strong&gt;&lt;strong&gt;的&lt;/strong&gt;&lt;strong&gt;数据获取&lt;/strong&gt;&lt;strong&gt;，这某种意义上也使得领域服务职责更加清晰及稳定。&lt;/strong&gt;&lt;/p&gt;

&lt;h3 id=&quot;查询模型归一&quot;&gt;查询模型归一&lt;/h3&gt;

&lt;p&gt;&lt;img src=&quot;https://pic4.zhimg.com/v2-5c49a9cc7b1a364e0d1097a750ed05fb_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;每个查询模型相当于一个场景。&lt;/p&gt;

&lt;p&gt;私以为，查询模型相当于 schema（命名、类型、映射），并且每个字段维护一个映射（查询模型 =&amp;gt; 展示单元），相当于一段动态代码，标识一个该场景的字段的计算逻辑。&lt;/p&gt;

&lt;p&gt;查询模型可能会膨胀，比如描述某个场景下一个商品的模型，可能包含很多字段，通过标准字段 + 扩展属性的方式建立查询模型，能够较好地解决字段扩散的问题，类似于头条的内容云 optional_data。&lt;/p&gt;

&lt;p&gt;其中，查询模型是多变的（不同版本、不同终端），展示单元变化较小，数据图变化非常小。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;相当于，通过查询模型，解决了场景快速迭代与领域模型相对稳定的矛盾。&lt;/strong&gt;&lt;/p&gt;

&lt;h3 id=&quot;元数据驱动&quot;&gt;元数据驱动&lt;/h3&gt;

&lt;p&gt;&lt;img src=&quot;https://pic3.zhimg.com/v2-6112f131a42a99a4e165fc5108d0399a_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;整体架构由三个核心部分组成：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;业务能力：标准的业务逻辑单元，包括取数单元、展示单元和查询模型，这些都是关键的可复用资产。&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;元数据：描述业务功能（如：展示单元、取数单元）以及业务功能之间的关联关系，比如展示单元依赖的数据，展示单元映射的展示字段等。&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;执行引擎：负责消费元数据，并基于元数据对业务逻辑进行调度和执行。&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;所谓元数据驱动，无非描述一个查询模型的取数链路，查询模型 =&amp;gt; 展示单元 =&amp;gt; 取数单元。&lt;/p&gt;

&lt;p&gt;不过，为每个模块记录一个元数据是有意义的，&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;方便可视化，给定一个查询模型，可以很方便查看其 schema 及查询链路&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;减少维护接口协议、接口文档的烦恼&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;低代码化，可以将其平台化，通过创建元数据搭建整条链路的实现&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;airbnb&quot;&gt;AirBnb&lt;/h2&gt;

&lt;blockquote&gt;
  &lt;p&gt;Eng https://medium.com/airbnb-engineering/reconciling-graphql-and-thrift-at-airbnb-a97e8d290712 中文版 https://juejin.cn/post/6844903698544459784&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;总结一下，AirBnb 在&lt;/strong&gt; &lt;strong&gt;GraphQL&lt;/strong&gt; &lt;strong&gt;架构中主要有两点可以参考：&lt;/strong&gt;&lt;/p&gt;

&lt;h3 id=&quot;graphql-网关&quot;&gt;GraphQL 网关&lt;/h3&gt;

&lt;p&gt;AirBnb 直接使用 GraphQL 作为网关，承担以下职责：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;聚合 Schema&lt;/strong&gt;：将所有展现服务层的 GraphQL Schema 聚合在一起形成一个单一的 Schema。网关在初始化的时候获取和解析所有展现服务层的 GraphQL Schema，并将他们合并在一起，同时通过轮询来监听 Schema 的变化。&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;路由&lt;/strong&gt;：将 GraphQL query 转发到相应的展现服务层去执行。&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Query 注册&lt;/strong&gt;：每个生产环境使用的 Query 都会注册生成一个 UUID。一是提高安全性，只有被注册过 query 才能在生产环境中执行；二是客户端不用每次都发送冗长的完整的 GraphQL query，只需使用 query 注册时生成的 UUID 即可。&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;thriftgraphql-转换器&quot;&gt;Thrift/GraphQL 转换器&lt;/h3&gt;

&lt;p&gt;Thrift/GraphQL 转换器应用在展示层。在 AirBnb 的架构中，展示层处于网关层的直接下游的位置。&lt;/p&gt;

&lt;p&gt;所有的 GraphQL 查询逻辑和 schema 定义全部都是通过展现服务层定义的 Thrfit 自动构建出来的。如果想让自己负责的展现服务层支持 GraphQL，只需把转换器模块包含进来即可。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic2.zhimg.com/v2-5e780cb34a80f4841fe73d3d64af8ca1_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;头条直播&quot;&gt;头条直播&lt;/h2&gt;

&lt;p&gt;直播的 packer 很有 GraphQL 特色，通过定义每个字段的 resolver 来确定字段的打包逻辑，一个 resolver 可能包含一个或多个 loader。然后根据 IDL 生成的 graph 来确定请求 field 的依赖，只加载对应的数据。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic2.zhimg.com/v2-60c0585808f8db088a91f12d34b1b911_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;优点：
    &lt;ol&gt;
      &lt;li&gt;load、pack 逻辑内聚在字段内，易于理解和维护&lt;/li&gt;
      &lt;li&gt;底层基于 dataloader 实现了 batching/caching 能力，减少调用次数，对下游友好&lt;/li&gt;
      &lt;li&gt;field 维度的数据粒度，只请求必要数据，不做冗余加载&lt;/li&gt;
    &lt;/ol&gt;
  &lt;/li&gt;
  &lt;li&gt;缺点：
    &lt;ol&gt;
      &lt;li&gt;使用了 dataloader 的基于时间窗口 batching 能力
        &lt;ul&gt;
          &lt;li&gt;如果一个批量请求的 load 被聚合到不同批次，会导致接口延时增大&lt;/li&gt;
          &lt;li&gt;打破原有 logid 的链路，会导致问题追溯变复杂&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;每个字段都需要维护一个 resolver，组织较清晰，但是代码量相对较大&lt;/li&gt;
    &lt;/ol&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;可改进点&quot;&gt;可改进点&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;问题：&lt;/strong&gt;对于同一个请求，不同字段可能依赖同一个下游数据，如果每个字段的 resolve 逻辑都单独调用一次，则会导致很多重复请求，造成读放大。因此直播使用了开源的 dataloader 来实现，会导致 batching 不同会话的请求。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;解决：&lt;/strong&gt;同一个会话共用一份缓存。将数据加载缓存在 context 中，如果已经有数据，则不必重复请求，并且 context 会随着会话终结而释放进行回收。&lt;/p&gt;

&lt;h1 id=&quot;展望与总结&quot;&gt;展望与总结&lt;/h1&gt;

&lt;p&gt;结合业界多个业务在 GraphQL 方面的实践，这里做一个小结。私以为，借鉴美团元数据管理的思路，是一种很好的解决思路。原因有二，一方面能够有效将变动最频繁的逻辑进行配置化管理，另一方面能够沉淀并充分复用领域层的业务逻辑。&lt;/p&gt;

&lt;p&gt;另外，如果能顺便借鉴 AirBnb 把 query 进行注册并且在平台配置对应 gql，那么一方面可以防止端上请求不规范，另一方面可以实现动态修改 gql 而不需要客户端发版，可以在 proxy 层做一些骚操作。&lt;/p&gt;

&lt;p&gt;实际上，上述两者完全可以相结合，下面描述一种 BFF 网关思路：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic4.zhimg.com/v2-8d6d7a1977ef0ee1da2a15f74a02a423_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;注：上图场景层/展示层/实体层都是提供一个平台进行管控。&lt;/p&gt;

&lt;h2 id=&quot;场景层&quot;&gt;场景层&lt;/h2&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;场景层可以对业务线的场景进行注册、管理&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;每个场景对应一个客户端/前端页面，具有唯一标识，并且有版本的概念&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;每个场景绑定多个展示层实体 entity，可以通过 GraphQL 实现，进行字段裁剪或者 mapping，但不进行额外计算，如果需要额外计算，则可以新增字段实现，这有利于展示逻辑的沉淀&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;可以看到，场景层类似 AirBnb 的思路，通过 scene_id+version 确定一个 gql&lt;/p&gt;

&lt;h2 id=&quot;展示层&quot;&gt;展示层&lt;/h2&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;展示层用于注册、管理展示实体&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;每个展示实体有多个字段，每个字段对应一个或多个领域实体字段；并可以选择直接赋值，或通过映射函数做计算，映射函数可以是 built-in 或者 customized&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;这里可以使用 GraphQL 实现，也可以使用其他方式&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;可以查看每个展示实体、以及具体字段的上层依赖，以此来提醒你某个展示层配置的影响范围&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;领域实体层&quot;&gt;领域实体层&lt;/h2&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;领域实体层用于注册、管理领域实体，你可以在此看到系统中所有领域实体的信息、描述&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;每个领域实体有多个字段，从一个指定的领域服务中获取
    &lt;ol&gt;
      &lt;li&gt;不建议从多个领域服务获取，这样做说明领域划分可能存在问题，并且可能造成读放大&lt;/li&gt;
    &lt;/ol&gt;
  &lt;/li&gt;
  &lt;li&gt;可以查看每个领域实体、以及具体字段的上层展示、场景依赖，以此来提醒你某个 MR 修改的字段的影响范围&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;领域层&quot;&gt;领域层&lt;/h2&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;领域层即各个执行领域逻辑的微服务&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;领域服务的打包，可以借鉴直播的思路，将 load/pack 逻辑内聚，并通过上文提到的基于 context 的会话缓存来避免重复请求&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;一些想法&quot;&gt;一些想法&lt;/h2&gt;

&lt;h3 id=&quot;公共依赖&quot;&gt;公共依赖&lt;/h3&gt;

&lt;p&gt;某些场景下，一个场景可能会关联的多个领域服务，这些领域服务可能有公共依赖，那么，场景层应该借鉴 AGW Loader 的思路，每个场景层可以配置多个 Loader，并提供可插拔的 built-in / customized 的公共依赖加载能力，通过 context 的透传能力透传到下游，或者在上述各个层级的入参配置进行引用从而达到透传的目的。&lt;/p&gt;

&lt;h3 id=&quot;架构中的定位&quot;&gt;架构中的定位&lt;/h3&gt;

&lt;p&gt;对于列表场景，入口层可以是应用服务作为上游，此时 BFF 网关作为一个聚合打包服务，可以应对复杂打包场景，例如抖音 Pack 在架构中的位置。&lt;/p&gt;

&lt;p&gt;而对于 item 场景，入口层可以是 TLB 作为上游，此时 BFF 网关作为一个网关，例如 AGW/Janus 在架构中的位置。&lt;/p&gt;

&lt;h3 id=&quot;现阶段的一些建议&quot;&gt;现阶段的一些建议&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;区分场景层实体与领域层实体。举个例子：&lt;/li&gt;
&lt;/ol&gt;

&lt;pre&gt;&lt;code class=&quot;language-Go&quot;&gt;type IdeaDetail struct {    // 对文章的点亮（类似笔记）
    IdeaId      int64
    IdeaAuthor  *Author
    IdeaContent string
    // ...
    Post *Post       // 文章
    Group *Group     // 文章所在小组
}
type IdeaDetail struct {    // 对文章的点亮（类似笔记）
    Idea *Idea
    Post *Post       // 文章
    Group *Group     // 文章所在小组
}

type IdeaMeta struct {
    IdeaId      int64
    PostId      int64
    GroupId     int64
    AuthorId    int64
}

type Idea struct {
    IdeaId      int64
    IdeaAuthor  *Author
    IdeaContent string
    // ...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;左边的实体 IdeaDetail 其实是场景层实体，领域层不应该直接使用该实体作为打包对象，而是通过外键的方式，抽离一个中间的 IdeaMeta，场景层直接关联具体的领域层对象或者展示层对象（如果能细化到展示层当然更好），这样的话，每个领域服务只需要打包所在上下文的数据（Idea 领域打包 Idea 的，Post 领域打包 Post 的…），不需要关心其他上下文数据。&lt;/p&gt;

&lt;p&gt;而抽离出的 IdeaMeta，便是上文提到的场景层公共依赖 Loader 思路。&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;区分展示层逻辑与领域层逻辑。举个例子：文章内容是领域层逻辑，基于文章内容计算的简介是展示层逻辑；图片 URI 算领域层逻辑，基于 URI 打包的 URL 是展示层逻辑。&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;划分好领域上下文，这是一个大话题，可以参考领域驱动设计相关书籍。&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;另外注意，上述三层能力中，每一层都可以通过提供 RPC/HTTP 形式的 OpenAPI 对外提供能力。&lt;/p&gt;

&lt;p&gt;以上如果有疏漏，也欢迎批评指正。&lt;/p&gt;

&lt;h1 id=&quot;reference&quot;&gt;Reference&lt;/h1&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;https://www.howtographql.com/basics/1-graphql-is-the-better-rest/&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;https://tech.meituan.com/2021/05/06/bff-graphql.html&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;https://medium.com/airbnb-engineering/reconciling-graphql-and-thrift-at-airbnb-a97e8d290712&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;
</description>
			<pubDate>Thu, 07 Apr 2022 00:00:00 +0000</pubDate>
			<link>https://sarkerson.github.io/2022/04/07/GraphQL-A-Backend-Engineer-Perspective/</link>
			<guid isPermaLink="true">https://sarkerson.github.io/2022/04/07/GraphQL-A-Backend-Engineer-Perspective/</guid>
        
			<category>architecture</category>
        
			<category>graphql</category>
        
        
		</item>
    
		<item>
			<title>分布式系统：从 Redlock 到共识算法</title>
			<description>&lt;h1 id=&quot;分布式系统从-redlock-到共识算法&quot;&gt;分布式系统：从 Redlock 到共识算法&lt;/h1&gt;

&lt;blockquote&gt;
  &lt;p&gt;TL;DR; 本文从介绍 Redlock 开始，引出 DDIA 作者 Martin 对 Redlock 的批判、Relock 作者 antirez 的反驳，从中总结出实现一个分布式锁的核心难题。该难题可以归结为分布式一致性问题，并总结了解决分布式一致性问题的模型与算法。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1 id=&quot;redlock&quot;&gt;Redlock&lt;/h1&gt;

&lt;p&gt;2016 年 2 月，为了规范各家对基于Redis的分布式锁的实现，Redis的作者提出了一个更安全的实现，叫做 &lt;a href=&quot;https://redis.io/topics/distlock&quot;&gt;Redlock&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;背景：解决基于单 Redis 节点的单点故障问题；以及哨兵模式下基于异步的主从复制（replication）可能带来的数据不一致问题。&lt;/p&gt;

&lt;p&gt;因此 antirez 提出了新的分布式锁的算法 Redlock，它基于 N 个完全独立的 Redis 节点（通常情况下N可以设置成5）。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic2.zhimg.com/v2-10088f8aaccd5780fbbf1dd320007409_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;获取锁&quot;&gt;获取锁&lt;/h2&gt;

&lt;p&gt;运行Redlock算法的客户端依次执行下面各个步骤，来完成&lt;strong&gt;获取锁&lt;/strong&gt;的操作：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;获取当前时间（毫秒数）。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;按顺序依次向N个Redis节点执行获取锁的操作。&lt;/strong&gt;这个获取操作跟前面基于单Redis节点的&lt;strong&gt;获取锁&lt;/strong&gt;的过程相同，包含随机字符串&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;my_random_value&lt;/code&gt;，也包含过期时间(比如&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PX 30000&lt;/code&gt;，即锁的有效时间)。为了保证在某个Redis节点不可用的时候算法能够继续运行，这个获取锁的操作还有一个超时时间(time out)，它要远小于锁的有效时间（几十毫秒量级）。&lt;strong&gt;客户端在向某个Redis节点获取锁失败以后，应该立即尝试下一个Redis节点。&lt;/strong&gt;这里的失败，应该包含任何类型的失败，比如该Redis节点不可用，或者该Redis节点上的锁已经被其它客户端持有（注：Redlock原文中这里只提到了Redis节点不可用的情况，但也应该包含其它的失败情况）。&lt;/li&gt;
  &lt;li&gt;计算整个获取锁的过程总共消耗了多长时间，计算方法是用&lt;strong&gt;当前时间减去第1步记录的时间&lt;/strong&gt;。如果客户端从&lt;strong&gt;大多数Redis节点（&amp;gt;= N/2+1）成功获取到了锁，并且获取锁总共消耗的时间没有超过锁的有效时间(lock validity time)&lt;/strong&gt;，那么这时客户端才认为最终获取锁成功；否则，认为最终获取锁失败。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;如果最终获取锁成功了，那么这个锁的有效时间应该重新计算&lt;/strong&gt;，它等于最初的锁的有效时间减去第3步计算出来的获取锁消耗的时间。&lt;/li&gt;
  &lt;li&gt;如果最终获取锁失败了（可能由于获取到锁的Redis节点个数少于N/2+1，或者整个获取锁的过程消耗的时间超过了锁的最初有效时间），那么客户端应该立即&lt;strong&gt;向所有Redis节点发起释放锁的操作&lt;/strong&gt;（即前面介绍的Redis Lua脚本）。&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;释放锁&quot;&gt;释放锁&lt;/h2&gt;

&lt;p&gt;上面描述的只是&lt;strong&gt;获取锁&lt;/strong&gt;的过程，而&lt;strong&gt;释放锁&lt;/strong&gt;的过程比较简单：客户端向所有Redis节点发起&lt;strong&gt;释放锁&lt;/strong&gt;的操作，不管这些节点当时在获取锁的时候成功与否。&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;为什么？&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;设想这样一种情况，客户端发给某个Redis节点的&lt;strong&gt;获取锁&lt;/strong&gt;的请求成功到达了该Redis节点，这个节点也成功执行了&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SET&lt;/code&gt;操作，但是它返回给客户端的响应包却丢失了。这在客户端看来，获取锁的请求由于超时而失败了，但在Redis这边看来，加锁已经成功了。因此，释放锁的时候，客户端也应该对当时获取锁失败的那些Redis节点同样发起请求。实际上，这种情况在异步通信模型中是有可能发生的：客户端向服务器通信是正常的，但反方向却是有问题的。&lt;/p&gt;

&lt;h2 id=&quot;failover&quot;&gt;Failover&lt;/h2&gt;

&lt;p&gt;由于N个Redis节点中的大多数能正常工作就能保证Redlock正常工作，因此理论上它的可用性更高。我们前面讨论的单Redis节点的分布式锁在failover的时候锁失效的问题，在Redlock中不存在了，&lt;strong&gt;但如果有节点发生崩溃重启，还是会对锁的安全性有影响的&lt;/strong&gt;。具体的影响程度跟 Redis 对数据的持久化程度有关。&lt;/p&gt;

&lt;p&gt;假设一共有5个Redis节点：A, B, C, D, E。设想发生了如下的事件序列：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;客户端1成功锁住了A, B, C，&lt;strong&gt;获取锁&lt;/strong&gt;成功（但D和E没有锁住）。&lt;/li&gt;
  &lt;li&gt;节点C崩溃重启了，但客户端1在C上加的锁没有持久化下来，丢失了。&lt;/li&gt;
  &lt;li&gt;节点C重启后，客户端2锁住了C, D, E，&lt;strong&gt;获取锁&lt;/strong&gt;成功。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;这样，客户端1和客户端2同时获得了锁（针对同一资源）。&lt;/p&gt;

&lt;p&gt;在默认情况下，Redis 的 AOF 持久化方式是每秒写一次磁盘（即执行fsync），因此最坏情况下可能丢失1秒的数据。为了尽可能不丢数据，Redis允许设置成每次修改数据都进行fsync，但这会降低性能。当然，即使执行了fsync也仍然有可能丢失数据（这取决于系统而不是Redis的实现）。所以，上面分析的由于节点重启引发的锁失效问题，总是有可能出现的。为了应对这一问题，antirez又提出了&lt;strong&gt;延迟重启&lt;/strong&gt;(delayed restarts)的概念。也就是说，一个节点崩溃后，先不立即重启它，而是等待一段时间再重启，这段时间应该大于锁的有效时间(lock validity time)。这样的话，这个节点在重启前所参与的锁都会过期，它在重启后就不会对现有的锁造成影响。&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;redlock-的各种讨论&quot;&gt;Redlock 的各种讨论&lt;/h1&gt;

&lt;p&gt;要知道，亲手实现过Redis Cluster这样一个复杂系统的antirez，足以算得上分布式领域的一名专家了。但对于由分布式锁引发的一系列问题的分析中，不同的专家却能得出迥异的结论，从中我们可以窥见分布式系统相关的问题具有何等的复杂性。&lt;/p&gt;

&lt;p&gt;实际上，在分布式系统的设计中经常发生的事情是：许多想法初看起来毫无破绽，而一旦详加考量，却发现不是那么天衣无缝。&lt;/p&gt;

&lt;h2 id=&quot;martin-的批判&quot;&gt;Martin 的批判&lt;/h2&gt;

&lt;h3 id=&quot;缺乏-fencing-机制&quot;&gt;缺乏 Fencing 机制&lt;/h3&gt;

&lt;p&gt;首先，在没有提供一种 fencing 机制的条件下，锁不具备安全性。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic1.zhimg.com/v2-3c3d8dd3c1095003444838f60fde5a44_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;假设使锁服务本身是没有问题的，而仅仅是客户端有长时间的 pause 或网络延迟，仍然会造成两个客户端同时访问共享资源的冲突情况发生。&lt;/p&gt;

&lt;p&gt;那怎么解决这个问题呢？Martin给出了一种方法，称为 fencing token。fencing token 是一个单调递增的数字，当客户端成功获取锁的时候它随同锁一起返回给客户端。而客户端访问共享资源的时候带着这个 fencing token，这样提供共享资源的服务就能根据它进行检查，拒绝掉延迟到来的访问请求（避免了冲突）。如下图：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic1.zhimg.com/v2-d46f4b7656cd24eab82dfde9703b24e0_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;在上图中，客户端1先获取到的锁，因此有一个较小的 fencing token，等于33，而客户端2后获取到的锁，有一个较大的 fencing token，等于34。客户端1从GC pause中恢复过来之后，依然是向存储服务发送访问请求，但是带了 fencing token = 33。存储服务发现它之前已经处理过34的请求，所以会拒绝掉这次33的请求。这样就避免了冲突。&lt;/p&gt;

&lt;h3 id=&quot;过多的计时假设&quot;&gt;过多的计时假设&lt;/h3&gt;

&lt;p&gt;另外，由于Redlock本质上是建立在一个同步模型之上，而且&lt;strong&gt;对系统的记时假设(timing assumption)有很强的要求，因此本身的安全性是不够的。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Martin在文中构造了一些事件序列，能够让Redlock失效（两个客户端同时持有锁）。为了说明Redlock对系统记时(timing)的过分依赖，他首先给出了下面的一个例子（还是假设有5个Redis节点A, B, C, D, E）：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;客户端1从Redis节点A, B, C成功获取了锁（多数节点）。由于网络问题，与D和E通信失败。&lt;/li&gt;
  &lt;li&gt;节点C上的时钟发生了向前跳跃，导致它上面维护的锁快速过期。&lt;/li&gt;
  &lt;li&gt;客户端2从Redis节点C, D, E成功获取了同一个资源的锁（多数节点）。&lt;/li&gt;
  &lt;li&gt;客户端1和客户端2现在都认为自己持有了锁。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;上面这种情况之所以有可能发生，本质上是因为Redlock的安全性(safety property)对系统的时钟有比较强的依赖，一旦系统的时钟变得不准确，算法的安全性也就保证不了了。Martin在这里其实是要指出分布式算法研究中的一些基础性问题，或者说一些常识问题，&lt;strong&gt;即好的分布式算法应该基于异步模型(asynchronous model)，算法的安全性不应该依赖于任何记时假设(timing assumption)。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;在异步模型中，进程可能pause任意长的时间，消息可能在网络中延迟任意长的时间，甚至丢失，系统时钟也可能以任意方式出错。一个好的分布式算法，这些因素不应该影响它的安全性(safety property)，只可能影响到它的活性(liveness property)，也就是说，即使在非常极端的情况下（比如系统时钟严重错误），算法顶多是不能在有限的时间内给出结果而已，而不应该给出错误的结果。这样的算法在现实中是存在的，像比较著名的 Paxos，或 Raft。但显然按这个标准的话，Redlock 的安全性级别是达不到的。&lt;/p&gt;

&lt;h2 id=&quot;antirez-的反驳&quot;&gt;antirez 的反驳&lt;/h2&gt;

&lt;h3 id=&quot;fencing-token-无需单调&quot;&gt;Fencing Token 无需单调&lt;/h3&gt;

&lt;p&gt;antirez 对于 Martin 的这种论证方式提出了质疑：&lt;strong&gt;并发下的顺序没有意义&lt;/strong&gt;。即使退一步讲，Redlock虽然提供不了 Martin 所讲的递增的 fencing token，但利用Redlock产生的随机数可以达到同样的效果。这个随机字符串虽然不是递增的，但却是唯一的，可以称之为 unique token。&lt;/p&gt;

&lt;h3 id=&quot;时钟无需过分精确&quot;&gt;时钟无需过分精确&lt;/h3&gt;

&lt;p&gt;Martin 认为 Redlock 会失效的情况主要有三种&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;时钟漂移&lt;/li&gt;
  &lt;li&gt;长时间的 GC pause&lt;/li&gt;
  &lt;li&gt;长时间的 网络延迟&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;时钟漂移&quot;&gt;时钟漂移&lt;/h4&gt;

&lt;p&gt;Martin 在提到时钟跳跃的时候，举了两个可能造成时钟跳跃的具体例子：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;系统管理员手动修改了时钟。&lt;/li&gt;
  &lt;li&gt;从 NTP 服务收到了一个大的时钟更新事件。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;antirez反驳说：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;手动修改时钟这种人为原因，不要那么做就是了。否则的话，如果有人手动修改Raft协议的持久化日志，那么就算是Raft协议它也没法正常工作了。&lt;/li&gt;
  &lt;li&gt;使用一个不会进行“跳跃”式调整系统时钟的 ntpd 程序（可能是通过恰当的配置），对于时钟的修改通过多次微小的调整来完成。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;而Redlock对时钟的要求，并不需要完全精确，它只需要时钟差不多精确就可以了。比如，要记时5秒，但可能实际记了4.5秒，然后又记了5.5秒，有一定的误差。不过只要误差不超过一定范围，这对Redlock不会产生影响。&lt;strong&gt;antirez认为，像这样对时钟精度并不是很高的要求，在实际环境中是完全合理的。&lt;/strong&gt;&lt;/p&gt;

&lt;h4 id=&quot;gc-pause&quot;&gt;GC Pause&lt;/h4&gt;

&lt;ol&gt;
  &lt;li&gt;获取当前时间。&lt;/li&gt;
  &lt;li&gt;完成&lt;strong&gt;获取锁&lt;/strong&gt;的整个过程（与N个Redis节点交互）。&lt;/li&gt;
  &lt;li&gt;再次获取当前时间。&lt;/li&gt;
  &lt;li&gt;把两个时间相减，计算&lt;strong&gt;获取锁&lt;/strong&gt;的过程是否消耗了太长时间，导致锁已经过期了。如果没过期，&lt;/li&gt;
  &lt;li&gt;客户端持有锁去访问共享资源。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;在Martin举的例子中，GC pause或网络延迟，实际发生在上述第1步和第3步之间。而不管在第1步和第3步之间由于什么原因（进程停顿或网络延迟等）导致了大的延迟出现，在第4步都能被检查出来，不会让客户端拿到一个它认为有效而实际却已经过期的锁。当然，这个检查依赖系统时钟没有大的跳跃。这也就是为什么 antirez 在前面要对时钟条件进行辩护的原因。&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;第四步之后，仍然可能存在延迟呢？&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;antirez 申明称，这个问题对于&lt;em&gt;所有的分布式锁的实现&lt;/em&gt;是普遍存在的。（这 Redlock 确实解决不了，因为需要递增 fencing 机制解决）&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic2.zhimg.com/v2-db46ffa55c238046bade1166f61280e9_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;redlock-的问题与小结&quot;&gt;Redlock 的问题与小结&lt;/h1&gt;

&lt;p&gt;Martin 认为 Redlock 实在不是一个好的选择，对于需求性能的分布式锁应用它太重了且成本高；对于需求正确性的应用来说它不够安全。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;因为它对高危的时钟或者说其他上述列举的情况进行了不可靠的假设&lt;/strong&gt;，如果你的应用只需要高性能的分布式锁不要求多高的正确性，那么单节点 Redis 够了；&lt;strong&gt;如果你的应用想要保住正确性，那么不建议 Redlock，建议使用一个合适的一致性协调系统，例如 Zookeeper，且保证存在 fencing token&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;仅有在你假设了一个同步系统模型的基础上，Redlock 才能正常工作，也就是系统能满足以下属性：&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;网络延时边界，即假设数据包一定能在某个最大延时之内到达&lt;/li&gt;
  &lt;li&gt;进程停顿边界，即进程停顿一定在某个最大时间之内&lt;/li&gt;
  &lt;li&gt;时钟错误边界，即不会从一个坏的 NTP 服务器处取得时间&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;在Martin 的这篇文章中，还有一个很有见地的观点，就是对锁的用途的区分。他把锁的用途分为两种：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;为了效率(efficiency)，协调各个客户端避免做重复的工作。即使锁偶尔失效了，只是可能把某些操作多做一遍而已，不会产生其它的不良后果。比如重复发送了一封同样的email。&lt;/li&gt;
  &lt;li&gt;为了正确性(correctness)。在任何情况下都不允许锁失效的情况发生，因为一旦发生，就可能意味着数据不一致(inconsistency)，数据丢失，文件损坏，或者其它严重的问题。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;最后，Martin得出了如下的结论：&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;如果是为了效率(efficiency)而使用分布式锁，允许锁的偶尔失效，那么使用单Redis节点的锁方案就足够了，简单而且效率高。Redlock则是个过重的实现(heavyweight)。&lt;/li&gt;
  &lt;li&gt;如果是为了正确性(correctness)在很严肃的场合使用分布式锁，那么不要使用Redlock。它不是建立在异步模型上的一个足够强的算法，它对于系统模型的假设中包含很多危险的成分(对于timing)。而且，它没有一个机制能够提供fencing token。那应该使用什么技术呢？Martin认为，应该考虑类似Zookeeper的方案，或者支持事务的数据库。&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
  &lt;p&gt;宁愿正确地挂掉，也不错误地运行。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;分布式模型&quot;&gt;分布式模型&lt;/h1&gt;

&lt;h2 id=&quot;现实中的挑战&quot;&gt;现实中的挑战&lt;/h2&gt;

&lt;blockquote&gt;
  &lt;p&gt;前面我们介绍了 Redlock 算法及各大咖的讨论，引出 Redlock 的问题，这些问题也是现实中实现分布式系统会经常遇到的挑战，这里简单对这些挑战做下小结。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id=&quot;不可靠的网络&quot;&gt;不可靠的网络&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;如果发送请求并没有得到响应，则无法区分（a）请求是否丢失，（b）远程节点是否关闭，或（c）响应是否丢失。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic4.zhimg.com/v2-12afd3ab53d6af5eed48b5f302d89233_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;不可靠的时钟&quot;&gt;不可靠的时钟&lt;/h3&gt;

&lt;p&gt;计算机中的石英钟不够精确：它会&lt;strong&gt;漂移（drifts）&lt;/strong&gt;（运行速度快于或慢于预期）。时钟漂移取决于机器的温度。 Google 假设其服务器时钟漂移为200 ppm（百万分之一），相当于每30秒与服务器重新同步一次的时钟漂移为6毫秒，或者每天重新同步的时钟漂移为17秒。即使一切工作正常，此漂移也会限制程序可以达到的最佳准确度。&lt;/p&gt;

&lt;p&gt;一个多主复制的场景，&lt;strong&gt;客户端B的写入比客户端A的写入要晚，但是B的写入具有较早的时间戳。因此解决冲突的时候把 B 的请求丢了（如果使用 Last Write Wins 的话）。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic1.zhimg.com/v2-f1228759043c31533310f96a9302a680_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;逻辑时钟（logic clock）&lt;/strong&gt;是基于递增计数器而不是振荡石英晶体，对于排序事件来说是更安全的选择。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Google TrueTime API&lt;/strong&gt; ，Google 在 spanner 中使用的全局时间戳，它明确地报告了本地时钟的置信区间。当你询问当前时间时，你会得到两个值：[最早，最晚]，这是最早可能的时间戳和最晚可能的时间戳。&lt;strong&gt;在不确定性估计的基础上，时钟知道当前的实际时间落在该区间内&lt;/strong&gt;。可以根据这个区间做一些骚操作，比如两个事务之间等待置信区间长度，保证两个事务的置信区间不重叠，由此保证事务的顺序。&lt;/p&gt;

&lt;h3 id=&quot;进程暂停&quot;&gt;进程暂停&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;许多编程语言运行时（如Java虚拟机）都有一个垃圾收集器（GC），偶尔需要停止所有正在运行的线程。这些“&lt;strong&gt;停止世界（stop-the-world）&lt;/strong&gt;”GC暂停有时会持续几分钟【64】！甚至像HotSpot JVM的CMS这样的所谓的“并行”垃圾收集器也不能完全与应用程序代码并行运行，它需要不时地停止世界【65】。尽管通常可以通过改变分配模式或调整GC设置来减少暂停【66】，但是如果我们想要提供健壮的保证，就必须假设最坏的情况发生。&lt;/li&gt;
  &lt;li&gt;在虚拟化环境中，可以&lt;strong&gt;挂起（suspend）&lt;/strong&gt;虚拟机（暂停执行所有进程并将内存内容保存到磁盘）并恢复（恢复内存内容并继续执行）。这个暂停可以在进程执行的任何时候发生，并且可以持续任意长的时间。这个功能有时用于虚拟机从一个主机到另一个主机的实时迁移，而不需要重新启动，在这种情况下，暂停的长度取决于进程写入内存的速率【67】。&lt;/li&gt;
  &lt;li&gt;在最终用户的设备（如笔记本电脑）上，执行也可能被暂停并随意恢复，例如当用户关闭笔记本电脑的盖子时。&lt;/li&gt;
  &lt;li&gt;当操作系统上下文切换到另一个线程时，或者当管理程序切换到另一个虚拟机时（在虚拟机中运行时），当前正在运行的线程可以在代码中的任意点处暂停。在虚拟机的情况下，在其他虚拟机中花费的CPU时间被称为&lt;strong&gt;窃取时间（steal time）&lt;/strong&gt;。如果机器处于沉重的负载下（即，如果等待运行的线程很长），暂停的线程再次运行可能需要一些时间。&lt;/li&gt;
  &lt;li&gt;如果应用程序执行同步磁盘访问，则线程可能暂停，等待缓慢的磁盘I/O操作完成【68】。在许多语言中，即使代码没有包含文件访问，磁盘访问也可能出乎意料地发生——例如，Java类加载器在第一次使用时惰性加载类文件，这可能在程序执行过程中随时发生。 I/O暂停和GC暂停甚至可能合谋组合它们的延迟【69】。如果磁盘实际上是一个网络文件系统或网络块设备（如亚马逊的EBS），I/O延迟进一步受到网络延迟变化的影响【29】。&lt;/li&gt;
  &lt;li&gt;如果操作系统配置为允许交换到磁盘（分页），则简单的内存访问可能导致&lt;strong&gt;页面错误（page fault）&lt;/strong&gt;，要求将磁盘中的页面装入内存。当这个缓慢的I/O操作发生时，线程暂停。如果内存压力很高，则可能需要将不同的页面换出到磁盘。在极端情况下，操作系统可能花费大部分时间将页面交换到内存中，而实际上完成的工作很少（这被称为&lt;strong&gt;抖动（thrashing）&lt;/strong&gt;）。为了避免这个问题，通常在服务器机器上禁用页面调度（如果你宁愿干掉一个进程来释放内存，也不愿意冒抖动风险）。&lt;/li&gt;
  &lt;li&gt;可以通过发送SIGSTOP信号来暂停Unix进程，例如通过在shell中按下Ctrl-Z。 这个信号立即阻止进程继续执行更多的CPU周期，直到SIGCONT恢复为止，此时它将继续运行。 即使你的环境通常不使用SIGSTOP，也可能由运维工程师意外发送。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;所有这些事件都可以随时&lt;strong&gt;抢占（preempt）&lt;/strong&gt;正在运行的线程，并在稍后的时间恢复运行，而线程甚至不会注意到这一点。这个问题类似于在单个机器上使多线程代码线程安全：你不能对时机做任何假设，因为随时可能发生上下文切换，或者出现并行运行。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;分布式系统中的节点，必须假定其执行可能在任意时刻暂停相当长的时间&lt;/strong&gt;，即使是在一个函数的中间。在暂停期间，世界的其它部分在继续运转，甚至可能因为该节点没有响应，而宣告暂停节点的死亡。最终暂停的节点可能会继续运行，在再次检查自己的时钟之前，甚至可能不会意识到自己进入了睡眠。&lt;/p&gt;

&lt;h3 id=&quot;拜占庭故障&quot;&gt;拜占庭故障&lt;/h3&gt;

&lt;p&gt;拜占庭将军问题是 Leslie Lamport 在 &lt;a href=&quot;https://web.archive.org/web/20170205142845/http://lamport.azurewebsites.net/pubs/byz.pdf&quot;&gt;The Byzantine Generals Problem&lt;/a&gt; 论文中提出的分布式领域的容错问题，它是分布式领域中最复杂、最严格的容错模型。&lt;/p&gt;

&lt;p&gt;在该模型下，&lt;strong&gt;系统不会对集群中的节点做任何的限制，它们可以向其他节点发送随机数据、错误数据，也可以选择不响应其他节点的请求，这些无法预测的行为使得容错这一问题变得更加复杂&lt;/strong&gt;。&lt;/p&gt;

&lt;h2 id=&quot;计时模型&quot;&gt;计时模型&lt;/h2&gt;

&lt;h3 id=&quot;同步模型&quot;&gt;同步模型&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;同步模型（synchronous model）&lt;/strong&gt;假设网络延迟，进程暂停和和时钟误差都是有界限的。这并不意味着完全同步的时钟或零网络延迟；这只意味着你知道网络延迟，暂停和时钟漂移将永远不会超过某个固定的上限。&lt;strong&gt;同步模型并不是大多数实际系统的现实模型，因为（如本章所讨论的）无限延迟和暂停确实会发生。&lt;/strong&gt;&lt;/p&gt;

&lt;h3 id=&quot;半同步模型&quot;&gt;半同步模型&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;部分同步（partial synchronous）&lt;/strong&gt;意味着一个&lt;strong&gt;系统在大多数情况下像一个同步系统一样运行，但有时候会超出网络延迟，进程暂停和时钟漂移的界限&lt;/strong&gt;。&lt;strong&gt;这是很多系统的现实模型&lt;/strong&gt;：大多数情况下，网络和进程表现良好，否则我们永远无法完成任何事情，但是我们必须承认，在任何时刻假设都存在偶然被破坏的事实。发生这种情况时，网络延迟，暂停和时钟错误可能会变得相当大。&lt;/p&gt;

&lt;h3 id=&quot;异步模型&quot;&gt;异步模型&lt;/h3&gt;

&lt;p&gt;在这个模型中，&lt;strong&gt;一个算法不允许对时机做任何假设（所以它不能使用超时）&lt;/strong&gt;—— 事实上它甚至没有时钟。一些算法被设计为可用于异步模型，但非常受限。&lt;/p&gt;

&lt;h2 id=&quot;节点故障模型&quot;&gt;节点故障模型&lt;/h2&gt;

&lt;h3 id=&quot;崩溃-终止模型&quot;&gt;崩溃-终止模型&lt;/h3&gt;

&lt;p&gt;在&lt;strong&gt;崩溃停止（crash-stop）&lt;/strong&gt;模型中，算法可能会假设一个节点只能以一种方式失效，即通过崩溃。这意味着&lt;strong&gt;节点可能在任意时刻突然停止响应，此后该节点永远消失&lt;/strong&gt;——它永远不会回来。&lt;/p&gt;

&lt;h3 id=&quot;崩溃-恢复模型&quot;&gt;崩溃-恢复模型&lt;/h3&gt;

&lt;p&gt;我们假设节点可能会在任何时候崩溃，但也许会在未知的时间之后再次开始响应。&lt;strong&gt;在崩溃-恢复（crash-recovery）模型中，假设节点具有稳定的存储（即，非易失性磁盘存储）且会在崩溃中保留，而内存中的状态会丢失。&lt;/strong&gt;&lt;/p&gt;

&lt;h3 id=&quot;拜占庭故障模型&quot;&gt;拜占庭故障模型&lt;/h3&gt;

&lt;p&gt;节点可以做（绝对意义上的）任何事情，&lt;strong&gt;包括调戏和欺骗其他节点&lt;/strong&gt;，如上一节所述。&lt;/p&gt;

&lt;p&gt;对于真实的系统，最普遍的模型组合是，半同步计时模型+崩溃-恢复模型。&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;分布式共识算法&quot;&gt;分布式共识算法&lt;/h1&gt;

&lt;blockquote&gt;
  &lt;p&gt;问题有了，为了解决问题而抽象出来的模型也有了，接下来就是实实在在的算法和实现了。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&quot;可线性化&quot;&gt;可线性化&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;可线性化是最强的一致性模型。后面会讲到的共识算法，都会无限逼近这个模型。其背后的基本思想很简单：使系统看起来好像只有一个数据副本。&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;一个非可线性化的例子：如果读取请求与写入请求并发，则可能会返回旧值或新值：&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;https://pic1.zhimg.com/v2-c4c211395d4c1e9f724e27a96720b8e0_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;为了使系统线性一致，我们需要添加另一个约束：&lt;strong&gt;任何一个读取返回新值后，所有后续读取（在相同或其他客户端上）也必须返回新值。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;https://pic3.zhimg.com/v2-355957734047ba33758cc18a83869946_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;每个操作都在我们认为执行操作的时候用竖线标出（在每个操作的条柱之内）。这些标记按顺序连在一起，其结果必须是一个有效的寄存器读写序列（每次读取都必须返回最近一次写入设置的值），操作标记的连线总是按时间（从左到右）向前移动，而不是向后移动。&lt;strong&gt;这就要求可线性化确保一个条件恒成立：一旦新值被写入或者读取，所有后续的读看到都是最新的值。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic3.zhimg.com/v2-fccb4f0962fe2ef61c737547be83261e_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;实现可线性化&quot;&gt;实现可线性化&lt;/h3&gt;

&lt;p&gt;我们已经见到了几个线性一致性有用的例子，让我们思考一下，如何实现一个提供线性一致语义的系统。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;由于线性一致性本质上意味着“表现得好像只有一个数据副本，而且所有的操作都是&lt;/strong&gt;&lt;strong&gt;原子&lt;/strong&gt;&lt;strong&gt;的”&lt;/strong&gt;，所以最简单的答案就是，真的只用一个数据副本。但是这种方法无法容错：如果持有该副本的节点失效，数据将会丢失，或者至少无法访问，直到节点重新启动。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;使系统容错最常用的方法是使用复制。&lt;/strong&gt;&lt;/p&gt;

&lt;h4 id=&quot;主从复制可能线性一致&quot;&gt;主从复制&lt;strong&gt;&lt;em&gt;（可能线性一致）&lt;/em&gt;&lt;/strong&gt;&lt;/h4&gt;

&lt;p&gt;在具有单主复制功能的系统中（参见“&lt;a href=&quot;https://vonng.gitbooks.io/ddia-cn/content/ch5.html#领导者与追随者&quot;&gt;领导者与追随者&lt;/a&gt;”），主库具有用于写入的数据的主副本，而追随者在其他节点上保留数据的备份副本。&lt;strong&gt;如果从主库或同步更新的从库读取数据，它们可能是线性一致性的。&lt;/strong&gt;然而，并不是每个单主数据库都是实际线性一致性的，无论是通过设计（例如，因为使用快照隔离）还是并发错误。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;从主库读取依赖一个假设，你确定主节点是谁。&lt;/strong&gt;正如在“&lt;a href=&quot;https://vonng.gitbooks.io/ddia-cn/content/ch8.html#真理被多数人定义&quot;&gt;真理在多数人手中&lt;/a&gt;”中所讨论的那样，一个节点很可能会认为它是领导者，而事实上并非如此——如果具有错觉的领导者继续为请求提供服务，可能违反线性一致性。而且，&lt;strong&gt;如果使用异步复制，故障切换时甚至可能会丢失已提交的写入&lt;/strong&gt;（参阅“&lt;a href=&quot;https://vonng.gitbooks.io/ddia-cn/content/ch5.html#处理节点宕机&quot;&gt;处理节点宕机&lt;/a&gt;”），这同时违反了持久性和线性一致性。&lt;/p&gt;

&lt;h4 id=&quot;共识算法线性一致&quot;&gt;共识算法&lt;strong&gt;&lt;em&gt;（线性一致）&lt;/em&gt;&lt;/strong&gt;&lt;/h4&gt;

&lt;p&gt;一些在本章后面讨论的共识算法，与主从复制类似。然而，共识协议包含防止脑裂和陈旧副本的措施。由于这些细节，共识算法可以安全地实现线性一致性存储。例如，Zookeeper 和 etcd 就是这样工作的。&lt;/p&gt;

&lt;h4 id=&quot;多主复制非线性一致&quot;&gt;多主复制&lt;strong&gt;&lt;em&gt;（非线性一致）&lt;/em&gt;&lt;/strong&gt;&lt;/h4&gt;

&lt;p&gt;具有多主程序复制的系统通常不是线性一致的，因为它们同时在多个节点上处理写入，并将其异步复制到其他节点。因此，它们可能会产生冲突的写入，需要解析（参阅“&lt;a href=&quot;https://vonng.gitbooks.io/ddia-cn/content/ch5.html#处理写入冲突&quot;&gt;处理写入冲突&lt;/a&gt;”）。这种冲突是因为缺少单一数据副本人为产生的。&lt;/p&gt;

&lt;h4 id=&quot;无主复制也许不是线性一致的&quot;&gt;无主复制&lt;strong&gt;&lt;em&gt;（也许不是线性一致的）&lt;/em&gt;&lt;/strong&gt;&lt;/h4&gt;

&lt;p&gt;直觉上 Quorum 是线性一致的，但是实际上存在非线性一致的执行，尽管使用了严格的 Quorum：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic3.zhimg.com/v2-cf717b13d456742325af0668e879890a_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;有趣的是，通过牺牲性能，可以使 Dynamo 风格的 Quorum 读写线性化：&lt;strong&gt;读取者必须在将结果返回给应用之前，同步执行读修复（参阅“&lt;a href=&quot;https://link.zhihu.com/?target=https%3A//vonng.gitbooks.io/ddia-cn/content/ch5.html%23%E8%AF%BB%E6%97%B6%E4%BF%AE%E5%A4%8D%E4%B8%8E%E5%8F%8D%E7%86%B5%E8%BF%87%E7%A8%8B&quot;&gt;读时修复与反熵过程&lt;/a&gt;”）&lt;/strong&gt; ，并且写入者必须在发送写入之前，读取 Quorum 数量节点的最新状态。然而，由于性能损失，Riak不执行同步读修复。 Cassandra 在进行 Quorum 读取时，确实在等待读修复完成；但是由于使用了最后写入为准的冲突解决方案，当同一个键有多个并发写入时，将不能保证线性一致性。&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;全序关系广播&quot;&gt;全序关系广播&lt;/h2&gt;

&lt;blockquote&gt;
  &lt;p&gt;全序关系广播与共识关系密切。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;单主复制通过选择一个节点作为主库来确定操作的全序，并在主库的单个CPU核上对所有操作进行排序。接下来的挑战是，如果吞吐量超出单个主库的处理能力，这种情况下如何扩展系统；以及，如果主库失效（“&lt;a href=&quot;https://link.zhihu.com/?target=https%3A//vonng.gitbooks.io/ddia-cn/content/ch9.html%23%E5%A4%84%E7%90%86%E8%8A%82%E7%82%B9%E5%AE%95%E6%9C%BA&quot;&gt;处理节点宕机&lt;/a&gt;”），如何处理故障切换。在分布式系统文献中，这个问题被称为&lt;strong&gt;全序广播（total order broadcast）&lt;/strong&gt;或&lt;strong&gt;原子广播（atomic broadcast）。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;全序广播通常被描述为在节点间交换消息的协议。 非正式地讲，&lt;strong&gt;它要满足两个安全属性&lt;/strong&gt;：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;&lt;em&gt;可靠交付（reliable delivery）&lt;/em&gt;&lt;/strong&gt; 没有消息丢失：如果消息被传递到一个节点，它将被传递到所有节点。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;全序交付（totally ordered delivery）&lt;/strong&gt; 消息以相同的顺序传递给每个节点。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;正确的全序广播算法必须始终保证可靠性和有序性，即使节点或网络出现故障。当然在网络中断的时候，消息是传不出去的，但是算法可以不断重试，以便在网络最终修复时，消息能及时通过并送达（当然它们必须仍然按照正确的顺序传递）。&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;共识算法&quot;&gt;共识算法&lt;/h2&gt;

&lt;h3 id=&quot;非容错共识算法&quot;&gt;非容错共识算法&lt;/h3&gt;

&lt;h4 id=&quot;2pc&quot;&gt;2PC&lt;/h4&gt;

&lt;p&gt;两阶段提交（Two Phase Commit, 2PC）是一种在多节点之间实现事务原子提交的共识算法，用来保证所有节点要么全部提交，要么全部终止。（是后面提及到的几个容错共识算法的原型）&lt;/p&gt;

&lt;h5 id=&quot;算法流程&quot;&gt;算法流程&lt;/h5&gt;

&lt;ul&gt;
  &lt;li&gt;2PC 触发时机：协调者向所有数据库发送 write 请求并收到成功之后，准备提交事务&lt;/li&gt;
  &lt;li&gt;2PC 处理流程：2PC 将事务的提交过程分成了准备和提交两个阶段进行处理&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
  &lt;li&gt;阶段一 prepare：
    &lt;ol&gt;
      &lt;li&gt;协调者向所有参与者发送一个&lt;strong&gt;准备&lt;/strong&gt;请求，并打上全局事务ID的标记。如果任意一个请求失败或超时，则协调者向所有参与者发送针对该事务ID的中止请求；&lt;/li&gt;
      &lt;li&gt;参与者收到准备请求时，需要确保在任意情况下都的确可以提交事务（通过写入 undolog、redolog）；&lt;/li&gt;
      &lt;li&gt;参与者向协调者反馈响应；&lt;/li&gt;
    &lt;/ol&gt;
  &lt;/li&gt;
  &lt;li&gt;阶段二 commit：
    &lt;ol&gt;
      &lt;li&gt;当协调者收到所有准备请求的答复时，会就提交或中止事务作出明确的决定（只有在所有参与者投赞成票的情况下才会提交）。协调者必须把这个决定写到磁盘上的事务日志中，如果它随后就崩溃，恢复后也能知道自己所做的决定。这被称为&lt;strong&gt;提交点（commit point）；&lt;/strong&gt;&lt;/li&gt;
      &lt;li&gt;一旦协调者的决定落盘，提交或放弃请求会发送给所有参与者。如果这个请求失败或超时，协调者必须永远保持重试，直到成功为止。&lt;/li&gt;
    &lt;/ol&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;img src=&quot;https://pic4.zhimg.com/v2-fa6d3f7b85bdc1851935636b389bf17f_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;因此，该协议包含两个关键的“不归路”点：&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;当参与者投票“yes”时，它承诺它稍后肯定能够提交（尽管协调者可能仍然选择放弃）；&lt;/li&gt;
  &lt;li&gt;一旦协调者做出决定，这一决定是不可撤销的；&lt;/li&gt;
&lt;/ul&gt;

&lt;h5 id=&quot;故障分析&quot;&gt;故障分析&lt;/h5&gt;

&lt;ol&gt;
  &lt;li&gt;任何一个写失败，协调者都不会提交事务，因此安全；&lt;/li&gt;
  &lt;li&gt;任何一个参与者 prepare 失败，协调者不会提交事务，因此安全；&lt;/li&gt;
  &lt;li&gt;协调者在 prepare 中失败，返回给用户错误，且事务没有真正提交，因此安全；&lt;/li&gt;
  &lt;li&gt;参与者 prepare 回包丢失，协调者不会提交事务，因此安全；&lt;/li&gt;
  &lt;li&gt;参与者在 commit 阶段挂了，协调者会无限重试保证事务提交；&lt;/li&gt;
  &lt;li&gt;协调者挂了，可以完成 2PC 的唯一方法是等待协调者恢复。这就是为什么协调者必须在向参与者发送提交或中止请求之前，将其提交或中止决定写入磁盘上的事务日志：协调者恢复后，通过读取其事务日志来确定所有存疑事务的状态。任何在协调者日志中没有提交记录的事务都会终止。&lt;/li&gt;
&lt;/ol&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;3pc&quot;&gt;3PC&lt;/h4&gt;

&lt;p&gt;3PC 假定一个&lt;strong&gt;有界的网络延迟并且节点能够在规定时间内响应&lt;/strong&gt;，所以 3PC 通过连接是否超时来判断节点是否故障：如果参与者等待第二阶段指令超时，则自动 abort 抛弃事务，若等待第三阶段指令超时，则自动 commit 提交事务。&lt;strong&gt;相较于两阶段提交，三阶段提交协议最大的优点是降低了参与者的阻塞范围，并且能够在出现单点故障后继续保持一致。&lt;/strong&gt;&lt;/p&gt;

&lt;h3 id=&quot;容错共识算法&quot;&gt;容错共识算法&lt;/h3&gt;

&lt;p&gt;本节的所有算法都可以归纳为类 Paxos 算法， 并且他们的实现流程与 2PC 非常类似。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;最大的区别在于，&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;2PC 的主节点是由外部指定的，而类 Paxos 算法可以在主节点崩溃失效后重新选举出新的主节点并进入一致状态。&lt;/li&gt;
  &lt;li&gt;容错共识算法只要收到多数节点的投票结果即可通过决议，而 2PC 则要每个参与者都必须做出 Yes 响应才能通过。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;这些差异是确保共识算法的正确性和容错性的关键。&lt;/p&gt;

&lt;h4 id=&quot;paxos&quot;&gt;Paxos&lt;/h4&gt;

&lt;h5 id=&quot;overview&quot;&gt;Overview&lt;/h5&gt;

&lt;blockquote&gt;
  &lt;p&gt;个人觉得最好的学习路线：&lt;a href=&quot;https://link.zhihu.com/?target=https%3A//www.youtube.com/watch%3Fv%3DJEpsBg0AO6o%26t%3D6s&quot;&gt;Paxos lecture(Recommended)&lt;/a&gt; + &lt;a href=&quot;https://link.zhihu.com/?target=https%3A//en.wikipedia.org/wiki/Paxos_(computer_science)%23&quot;&gt;Paxos Wikipedia&lt;/a&gt; + &lt;a href=&quot;https://link.zhihu.com/?target=https%3A//lamport.azurewebsites.net/pubs/paxos-simple.pdf&quot;&gt;Paper(optional)&lt;/a&gt;（&lt;strong&gt;有严格先后顺序&lt;/strong&gt;）&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;img src=&quot;https://pic1.zhimg.com/v2-c336f2bf48bfd56070ada21b99bcc9a4_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Paxos 的目标：Leader 想把自己的日志，一行不漏同步到其他所有 Follower。&lt;/p&gt;

&lt;p&gt;在多个节点中，对日志的一致性达成共识。一旦日志相同，则能保证每个节点的状态机相同（按照相同的顺序，执行相同的命令）&lt;/p&gt;

&lt;h6 id=&quot;状态机&quot;&gt;状态机&lt;/h6&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;安全性：&lt;/strong&gt;所有节点的日志的顺序必须相同&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;活性：&lt;/strong&gt;最终，所有节点拥有相同且完整的日志，执行了相同的日志序列，拥有相同的状态&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;https://pic2.zhimg.com/80/v2-dea1f43408d73b93266903a9407bf179_1440w.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h6 id=&quot;概念&quot;&gt;概念&lt;/h6&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;an paxos instance：一轮法案，一个任期（一个任期范围内，有且只有一个被选定的提案值）&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;a round：一个新的 proposal 的提出过程（包含两阶段），每轮法案可能包含多个 proposal（or say, round）&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;proposal number：提案编号（日志的 offset），通常是 n；每一个新的提案编号，都会严格大于旧提案编号&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;proposal value：提案的内容（日志的内容），通常是 v；&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Basic Paxos：&lt;/p&gt;

    &lt;p&gt;&lt;strong&gt;对某行日志达成共识的过程&lt;/strong&gt;&lt;/p&gt;

    &lt;ul&gt;
      &lt;li&gt;一个或多个 server 提出议案（propose value）&lt;/li&gt;
      &lt;li&gt;系统有且选择一个议案（single value chosen）&lt;/li&gt;
      &lt;li&gt;系统不曾选择第二个议案&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Multi-Paxos&lt;/p&gt;

    &lt;ul&gt;
      &lt;li&gt;由多轮 Basic Paxos instance，&lt;strong&gt;达成一个一致的日志序列的过程&lt;/strong&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;h5 id=&quot;basic-paxos&quot;&gt;Basic Paxos&lt;/h5&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;em&gt;Each “instance” (or “execution”) of the basic Paxos protocol&lt;/em&gt; &lt;strong&gt;&lt;em&gt;decides on a single output value&lt;/em&gt;&lt;/strong&gt;&lt;em&gt;. The protocol proceeds over several rounds.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;img src=&quot;https://pic4.zhimg.com/v2-e642a8ff9c0e7a6d1f96db82523134bf_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic4.zhimg.com/v2-ca5cb6669e185e2c75ed00864c9489bf_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h5 id=&quot;multi-paxos&quot;&gt;Multi Paxos&lt;/h5&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;em&gt;In Paxos, clients send commands to a leader. During normal operation, the leader receives a client’s command, assigns it a new command number i, and then&lt;/em&gt; &lt;strong&gt;&lt;em&gt;begins the ith instance of the consensus algorithm&lt;/em&gt;&lt;/strong&gt; &lt;em&gt;by sending messages to a set of acceptor processes.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;引入多个 Paxos instance 之后，需要解决的问题&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;性能优化：多主导致延迟收敛、多轮 Paxos instance 的冗余 prepare RPCs&lt;/li&gt;
  &lt;li&gt;如何选择一个 proposal 编号（日志 offset）&lt;/li&gt;
  &lt;li&gt;如何保证日志的完整性&lt;/li&gt;
  &lt;li&gt;如何与客户端的交互&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;raft--zab&quot;&gt;Raft &amp;amp; Zab&lt;/h4&gt;

&lt;p&gt;篇幅关系，可以参阅其他博文，相比于 paxos，raft 可以找到更多更好的教程。&lt;/p&gt;

&lt;h3 id=&quot;拜占庭故障容错算法&quot;&gt;拜占庭故障容错算法&lt;/h3&gt;

&lt;p&gt;当一个系统在部分节点发生故障、不遵守协议、甚至恶意攻击、扰乱网络时仍然能继续正确工作，称之为&lt;strong&gt;拜占庭容错（Byzantine fault-tolerant）&lt;/strong&gt;的，在特定场景下，这种担忧在是有意义的：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;在航空航天环境中，计算机内存或CPU寄存器中的数据可能被辐射破坏，导致其以任意不可预知的方式响应其他节点。由于系统故障将非常昂贵（例如，飞机撞毁和炸死船上所有人员，或火箭与国际空间站相撞），飞行控制系统必须容忍拜占庭故障。&lt;/li&gt;
  &lt;li&gt;在多个参与组织的系统中，一些参与者可能会试图欺骗或欺骗他人。在这种情况下，节点仅仅信任另一个节点的消息是不安全的，因为它们可能是出于恶意的目的而被发送的。例如，像比特币和其他区块链一样的对等网络可以被认为是让互不信任的各方同意交易是否发生的一种方式，而不依赖于中央当局。&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;工程化实现与应用&quot;&gt;工程化实现与应用&lt;/h2&gt;

&lt;h3 id=&quot;zookeeper&quot;&gt;Zookeeper&lt;/h3&gt;

&lt;blockquote&gt;
  &lt;p&gt;Ref &lt;a href=&quot;https://wingsxdu.com/post/database/zookeeper/#gsc.tab=0&quot;&gt;ZooKeeper 与 Zab 协议 · Analyze - beihai blog (wingsxdu.com)&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Zab 协议的全称是 ZooKeeper 原子广播协议（ZooKeeper Atomic Broadcast Protocol），实际上实现了前面提及的全序关系广播。&lt;/p&gt;

&lt;h4 id=&quot;节点状态&quot;&gt;节点状态&lt;/h4&gt;

&lt;p&gt;ZooKeeper 中&lt;strong&gt;所有的写请求必须由一个全局唯一的 Leader 服务器来协调处理&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;ZooKeeper &lt;strong&gt;客户端会随机连接（长连接，并通过心跳维护&lt;/strong&gt; &lt;strong&gt;session&lt;/strong&gt;&lt;strong&gt;）到 ZooKeeper 集群中的一个节点，如果是读请求，就直接从当前节点中读取数据；如果是写请求，那么该节点就会向 Leader 转发请求&lt;/strong&gt;，Leader 接收到读写事务后，会将事务转换为一个事务提案（Proposal），并向集群广播该提案，只要超过半数节点写入数据成功，Leader 会再次向集群广播 Commit 消息，将该提案提交。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ZooKeeper 集群节点可能处于下面四种状态之一&lt;/strong&gt;，分别是：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;LOOKING&lt;/strong&gt;：进入 Leader 选举状态；&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;LEADING&lt;/strong&gt;：某个节点成为 Leader 并负责协调事务 ；&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;FOLLOWING&lt;/strong&gt;：当前节点是 Follower，服从 Leader 节点的命令并参与共识；&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;OBSERVING&lt;/strong&gt;：Observer 节点是只读节点，用于增加集群的只读事务性能，不参与共识与选举。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Zab 协议使用&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ZXID&lt;/code&gt; 来表示全局事务编号，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ZXID&lt;/code&gt; 是一个 64 位数字，其中低 32 位是一个单调递增的计数器，针对客户端每一个事务请求，计数器都加 1；高 32 位则表示当前 Leader 的 Epoch，每当选举出一个新的主服务器，就会从集群日志中取出最大的 ZXID，从中读取出 Epoch 值，然后加 1，以此作为新的 Epoch，同时将低 32 位从 0 开始计数。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic1.zhimg.com/v2-ba909bc061d73dfee784b0bad663c120_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h4 id=&quot;全序广播&quot;&gt;全序广播&lt;/h4&gt;

&lt;p&gt;&lt;img src=&quot;https://pic4.zhimg.com/v2-31126e293c16a3d9622501ae1a7dbbdf_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;ZooKeeper 的消息广播过程&lt;strong&gt;类似于两阶段提交&lt;/strong&gt;，针对客户端的读写事务请求，Leader 会生成对应的事务提案，并为其分配 ZXID，随后将这条提案广播给集群中的其它节点。Follower 节点接收到该事务提案后，会先将其以事务日志的形式写入本地磁盘中，写入成功后会给 Leader 反馈一条 Ack 响应。当 Leader 收到超过半数 Follower 节点的 Ack 响应后，会返回客户端成功，并向集群发送 Commit 消息，将该事务提交。Follower 服务器接收到 Commit 消息后，会完成事务的提交，将数据应用到数据副本中。&lt;/p&gt;

&lt;p&gt;在消息广播过程中，&lt;strong&gt;Leader 服务器会为每个 Follower 维护一个消息队列&lt;/strong&gt;，然后将需要广播的提案依次放入队列中，并根据「先入先出」的规则逐一发送消息。因为只要超过半数节点响应就可以认为写操作成功，所以少数的慢节点不会影响整个集群的性能。&lt;/p&gt;

&lt;p&gt;各个阶段的写失败可以参阅上文的两阶段提交，其实是一样的。&lt;/p&gt;

&lt;h4 id=&quot;崩溃恢复&quot;&gt;崩溃恢复&lt;/h4&gt;

&lt;p&gt;Zab 集群使用&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Epoch&lt;/code&gt;（纪元）来表示当前集群所处的周期，每个 Leader 都有自己的任期值，所以&lt;strong&gt;每次 Leader 变更之后，都会在前一个&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Epoch&lt;/code&gt;的基础上加 1&lt;/strong&gt;。&lt;strong&gt;Follower 只听从当前纪元 Leader 的命令，旧 Leader 崩溃恢复后，发现集群中存在更大的纪元，会切换为 FOLLOWING 状态。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;触发时机：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;当 Leader 节点出现崩溃中止，Follower 无法连接到 Leader 时，Follower 会切换为 LOOKING 状态，发起新一轮的选举；&lt;/li&gt;
  &lt;li&gt;如果 Leader 节点无法与过半的服务器正常通信，Leader 节点也会主动切换为 LOOKING 状态，将领导权让位于网络环境较好的节点。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Zab 协议需要在崩溃恢复的过程中保证下面两个特性：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Zab 协议需要确保&lt;strong&gt;已经在 Leader 服务器上提交（Commit）的事务最终被所有服务器提交&lt;/strong&gt;；&lt;/li&gt;
  &lt;li&gt;Zab 协议需要确保&lt;strong&gt;丢弃那些只在 Leader 上提出但没有被提交的事务&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;

&lt;h4 id=&quot;一致性分析&quot;&gt;一致性分析&lt;/h4&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;可线性化写：&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Zookeeper 仅将写操作交给 Leader 串行执行，也就保证了写操作线性。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;顺序一致性读&lt;/p&gt;

    &lt;p&gt;（没有满足可线性化的条件）&lt;/p&gt;

    &lt;ul&gt;
      &lt;li&gt;同一个 client 看到的是与 leader 相同的变更序列&lt;/li&gt;
      &lt;li&gt;不同 client 看到的值变更（时间）有可能不同&lt;/li&gt;
    &lt;/ul&gt;

    &lt;p&gt;&lt;img src=&quot;https://pic2.zhimg.com/v2-2884b287105c41dbda28f08aec016db5_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;可线性化读：&lt;/strong&gt;可以利用写操作的可线性化特性，在读取之前执行一个写操作。原理是，Zookeeper 给每个写入后的状态一个唯一自增的 Zxid，并通过写请求的 resp 告知客户端，客户端之后的读请求都会携带这个 Zxid，直连的 Server 通过比较 Zxid 判断自己是否滞后，如果是则让读操作等待。&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;chubby&quot;&gt;Chubby&lt;/h3&gt;

&lt;blockquote&gt;
  &lt;table&gt;
    &lt;tbody&gt;
      &lt;tr&gt;
        &lt;td&gt;Ref [Chubby的锁服务&lt;/td&gt;
        &lt;td&gt;CatKang的博客](http://catkang.github.io/2017/09/29/chubby.html)，&lt;a href=&quot;https://www2.cs.uic.edu/~brents/cs494-cdcs/slides/thegooglechubbylockservice.pdf&quot;&gt;thegooglechubbylockservice.pdf (uic.edu)&lt;/a&gt;&lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;

  &lt;p&gt;Chubby provide coarse-grained locking as well as reliable storage for a loosely-coupled distributed system.（读多写少）&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4 id=&quot;主节点选举&quot;&gt;主节点选举&lt;/h4&gt;

&lt;p&gt;Chubby 实际上实现了 Multi-Paxos，其概要实现如下：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;多个副本组成一个集群，副本通过一致性协议选出一个Master，集群在一个确定的租约时间内保证这个Master的领导地位；&lt;/li&gt;
  &lt;li&gt;Master周期性的向所有副本刷新延长自己的租约时间；&lt;/li&gt;
  &lt;li&gt;每个副本通过一致性协议维护一份数据的备份，&lt;strong&gt;而只有 Master 可以发起读写操作&lt;/strong&gt;；&lt;/li&gt;
  &lt;li&gt;Master挂掉或脱离集群后，其他副本发起选主，得到一个新的Master；&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;session-and-keepalives&quot;&gt;Session And KeepAlives&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;心跳时机：&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Master 和 Client 之间通过 KeepAlive 进行通信，初始化时 Client 发起 KeepAlive，会被 Master 阻塞在本地&lt;/strong&gt;，直到Session租约临近过期，此时Master会延长租约时间，并返回阻塞的KeepAlive通知Client；&lt;/li&gt;
  &lt;li&gt;除此之外，Master 还可能在 Cache 失效或 Event 发生时返回 KeepAlive；&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;https://pic4.zhimg.com/v2-a163e42bb7d2fbb6e72840fac22114c7_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h4 id=&quot;cache&quot;&gt;&lt;strong&gt;Cache&lt;/strong&gt;&lt;/h4&gt;

&lt;p&gt;从这里开始要提到的 Chubby 的机制是对 Client 透明的了。Chubby 对自己的定位是需要支持大量的Client，并且读请求远大于写请求的场景，因此引入一个对读请求友好的 Client 端 Cache，来减少大量读请求对 Chubby Master 的压力便十分自然，客户端可以完全不感知这个 Cache 的存在。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cache 对读请求的极度友好体现在它牺牲写性能实现了一个一致语义的Cache：&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Cache 可以缓存几乎所有的信息，包括数据，数据元信息，Handle 信息及 Lock；&lt;/li&gt;
  &lt;li&gt;Master 收到写请求时，会先阻塞写请求，通过返回所有客户端的 KeepAlive 来通知客户端 Invalidate 自己的 Cache；&lt;/li&gt;
  &lt;li&gt;Client 直接将自己的 Cache 清空并标记为 Invalid，并发送 KeepAlive 向 Master 确认；&lt;/li&gt;
  &lt;li&gt;Master 收到所有 Client 确认或等到超时后再执行写请求。（如果超时的话，会导致两个 client 同时持有锁吗？——租约）&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;fail-over&quot;&gt;Fail-over&lt;/h4&gt;

&lt;p&gt;&lt;img src=&quot;https://pic3.zhimg.com/v2-7bedd8bef132f01226498037c6ba2ef2_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;这里的多个临界条件，有没有可能存在锁冲突问题？ 下面进行分析&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;lease C2 还在，该 client 可以正常获取锁；lease M2 还在，其他 client 不能获取锁；&lt;/li&gt;
  &lt;li&gt;lease C2 不在，该 client 不可以获取锁；lease M2 还在，其他 client 不能获取锁；&lt;/li&gt;
  &lt;li&gt;新 master 启动，
    &lt;ol&gt;
      &lt;li&gt;选择新的 epoch，拒绝老 epoch 的所有 client 请求&lt;/li&gt;
      &lt;li&gt;与客户端重新建立 session，并携带新 epoch，将所有 client cache 置为失效状态&lt;/li&gt;
      &lt;li&gt;等待 client ack，若某 client 超时则终止其 session（保证了该时刻，所有有效 client 状态一致）&lt;/li&gt;
      &lt;li&gt;单 master 模型保证了竞争锁的 client 有且只有一个成功&lt;/li&gt;
    &lt;/ol&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;h4 id=&quot;一致性分析-1&quot;&gt;一致性分析&lt;/h4&gt;

&lt;ul&gt;
  &lt;li&gt;可线性化：相当于「主从复制」模型，所有的读写操作都是走主节点来解决，实际上也实现了「全序关系广播」。&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;zookeeper-vs-chubby&quot;&gt;Zookeeper VS Chubby&lt;/h3&gt;

&lt;p&gt;先看看两者的定位：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;blockquote&gt;
      &lt;p&gt;&lt;strong&gt;Chubby&lt;/strong&gt;：provide coarse-grained locking as well as reliable storage for a loosely-coupled distributed system.&lt;/p&gt;
    &lt;/blockquote&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;blockquote&gt;
      &lt;p&gt;&lt;strong&gt;Zookeeper&lt;/strong&gt;：provide a simple and high performance kernel for building more complex coordination primitives for the client.&lt;/p&gt;
    &lt;/blockquote&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;可以看出，Chubby 旗帜鲜明的表示自己是为分布式锁服务的，而 Zookeeper 则倾向于构造一个“Kernel”，而利用这个“Kernel”客户端可以自己实现众多更复杂的分布式协调机制。自然的，&lt;strong&gt;Chubby倾向于提供更精准明确的操作来免除使用者的负担，Zookeeper 则需要提供更通用，更原子的原材料，留更多的空白和自由给 Client&lt;/strong&gt;。也正是因此，为了更适配到更广的场景范围，Zookeeper 对性能的提出了更高的要求。&lt;/p&gt;

&lt;h4 id=&quot;一致性&quot;&gt;&lt;strong&gt;一致性&lt;/strong&gt;&lt;/h4&gt;

&lt;ul&gt;
  &lt;li&gt;Chubby：线性一致性(Linearizability)&lt;/li&gt;
  &lt;li&gt;Zookeeper：写操作线性(Linearizable writes) + 客户端有序(FIFO client order)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Chubby 所要实现的一致性是分布式系统中所能实现的最高级别的一致性，简单的说就是&lt;strong&gt;每次操作时都可以看到其之前的所有成功操作按顺序完成&lt;/strong&gt;，而 Zookeeper 将一致性弱化为两个保证，其中写操作线性（Linearizable writes）指的是&lt;strong&gt;所有修改集群状态的操作按顺序完成&lt;/strong&gt;，客户端有序（FIFO client order）指&lt;strong&gt;对任意一个client来说，他所有的读写操作都是按顺序完成&lt;/strong&gt;。&lt;/p&gt;

&lt;h4 id=&quot;分布式锁&quot;&gt;分布式锁&lt;/h4&gt;

&lt;p&gt;&lt;img src=&quot;https://pic4.zhimg.com/v2-ff22f4e9b7cdf4d95219ec5f04d4a50b_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Chubby：提供准确语义的Lock，Release操作，内部完成了一致性协议，锁的实现的内容，仅将锁的使用部分留给用户；&lt;/li&gt;
  &lt;li&gt;Zookeeper：并没有提供加锁放锁操作，用户需要利用Zookeeper提供的基础操作，完成锁的实现和锁的使用部分的内容；&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
  &lt;p&gt;Netflix created &lt;a href=&quot;https://github.com/Netflix/curator&quot;&gt;curator&lt;/a&gt; library which later moved to &lt;a href=&quot;http://curator.apache.org/&quot;&gt;Apache&lt;/a&gt; foundation, &lt;strong&gt;this library provides the commonly used functionality and cache management&lt;/strong&gt;. This additional layer to zookeeper allows it providing strong consistency needed by some users. So whenever you want to use zookeeper, use curator library instead of native library unless you know what you are doing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;what-about-the-redlock-problem&quot;&gt;What about the RedLock Problem？&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;回到一开始的问题，redlock 的问题，zookeeper、chubby 能否解决呢？&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;一个正确的算法一旦返回结果，那必须是正确的结果，这点 zk、chubby 都可以保证（例如 zk 在 commit point 的时候，多数节点状态达成一致；chubby 维护一致性 cache 保证所有正常 client 状态一致）；而 redlock 需要对返回的结果基于不可靠的时间进行判断，因此本身也是”neither fish nor fowl”&lt;/p&gt;

&lt;p&gt;&lt;em&gt;另外，Redis 锁需要自己实现续租逻辑，而 zk、chubby 不需要（使用 keepalive 长连接实现）。&lt;/em&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;一些与共识等价的问题&quot;&gt;一些与共识等价的问题&lt;/h2&gt;

&lt;p&gt;我们看到，达成共识意味着以这样一种方式决定某件事：所有节点一致同意所做决定，且这一决定不可撤销。通过深入挖掘，结果我们发现很广泛的一系列问题实际上都可以归结为共识问题，并且彼此等价（从这个意义上来讲，如果你有其中之一的解决方案，就可以轻易将它转换为其他问题的解决方案）。这些等价的问题包括：&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;线性一致性的 CAS 寄存器&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;寄存器需要基于当前值是否等于操作给出的参数，原子地&lt;strong&gt;决定&lt;/strong&gt;是否设置新值。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;原子事务提交&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;数据库必须&lt;strong&gt;决定&lt;/strong&gt;是否提交或中止分布式事务。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;全序广播&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;消息系统必须&lt;strong&gt;决定&lt;/strong&gt;传递消息的顺序。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;锁和租约&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;当几个客户端争抢锁或租约时，由锁来&lt;strong&gt;决定&lt;/strong&gt;哪个客户端成功获得锁。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;成员/协调服务&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;给定某种故障检测器（例如超时），系统必须&lt;strong&gt;决定&lt;/strong&gt;哪些节点活着，哪些节点因为会话超时需要被宣告死亡。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;唯一性约束&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;当多个事务同时尝试使用相同的键创建冲突记录时，约束必须&lt;strong&gt;决定&lt;/strong&gt;哪一个被允许，哪些因为违反约束而失败。&lt;/p&gt;

&lt;h1 id=&quot;reference&quot;&gt;Reference&lt;/h1&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;a href=&quot;https://redis.io/topics/distlock&quot;&gt;Distributed locks with Redis – Redis&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html&quot;&gt;How to do distributed locking — Martin Kleppmann’s blog&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;
    &lt;table&gt;
      &lt;tbody&gt;
        &lt;tr&gt;
          &lt;td&gt;[Is Redlock Safe? Reply to Redlock Analysis&lt;/td&gt;
          &lt;td&gt;Hacker News (ycombinator.com)](https://news.ycombinator.com/item?id=11065933)&lt;/td&gt;
        &lt;/tr&gt;
      &lt;/tbody&gt;
    &lt;/table&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www2.cs.uic.edu/~brents/cs494-cdcs/slides/thegooglechubbylockservice.pdf&quot;&gt;The Chubby lock service for loosely-coupled distributed systems.&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://vonng.gitbooks.io/ddia-cn/content/ch9.html&quot;&gt;第九章：一致性与共识 · ddia-cn (gitbooks.io)&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;Paxos：&lt;a href=&quot;https://www.youtube.com/watch?v=JEpsBg0AO6o&amp;amp;t=6s&quot;&gt;Paxos lecture(Recommended)&lt;/a&gt; + &lt;a href=&quot;https://en.wikipedia.org/wiki/Paxos_(computer_science)#&quot;&gt;Paxos Wikipedia&lt;/a&gt; + &lt;a href=&quot;https://lamport.azurewebsites.net/pubs/paxos-simple.pdf&quot;&gt;Paper(optional)&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://wingsxdu.com/post/algorithms/distributed-consensus-and-data-consistent/#raft-与-zab&amp;amp;gsc.tab=0&quot;&gt;漫谈分布式共识算法与数据一致性 - beihai blog (wingsxdu.com)&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</description>
			<pubDate>Wed, 13 Oct 2021 00:00:00 +0000</pubDate>
			<link>https://sarkerson.github.io/2021/10/13/%E4%BB%8E-RedLock-%E5%88%B0%E5%85%B1%E8%AF%86%E7%AE%97%E6%B3%95/</link>
			<guid isPermaLink="true">https://sarkerson.github.io/2021/10/13/%E4%BB%8E-RedLock-%E5%88%B0%E5%85%B1%E8%AF%86%E7%AE%97%E6%B3%95/</guid>
        
			<category>distributed-system</category>
        
        
		</item>
    
		<item>
			<title>性能工程：程序优化的原理与方法</title>
			<description>&lt;h1 id=&quot;性能工程程序优化的原理与方法&quot;&gt;性能工程：程序优化的原理与方法&lt;/h1&gt;

&lt;h1 id=&quot;0-优化之路咋走&quot;&gt;0. 优化之路咋走？&lt;/h1&gt;

&lt;p&gt;首先需要知道优化之路可以怎么走，下面摘自 CMU 的公开课 slides。主要有两方面因素：&lt;/p&gt;

&lt;p&gt;代码风格：良好的数据结构与算法思想，循环、变量等人为因素；
编译器与操作系统：了解编译器优化，从汇编、profile 等角度分析性能瓶颈，了解操作系统层面因素；&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic4.zhimg.com/v2-52dafcb512911ab2b882c041cc3cb9bf_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;1-量化程序性能&quot;&gt;1. 量化程序性能&lt;/h1&gt;

&lt;p&gt;使用 CPE（Cycles Per Element）来量化程序性能。其表示每个循环执行了多少个时钟周期（可以预估执行了多少个指令）。&lt;/p&gt;

&lt;p&gt;对于一个“4GHz”的处理器而言，时钟运行的频率是每秒 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;4x10^9&lt;/code&gt; 个周期，即&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;一个时钟周期 = 0.25ns&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;例如，&lt;strong&gt;将程序的循环的&lt;/strong&gt; &lt;strong&gt;CPE&lt;/strong&gt; &lt;strong&gt;从 9 优化到 6，表示我们将程序中的每个循环，从消耗 9 个时钟周期，优化到 6 个时钟周期，节省了 3 个时钟周期。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic3.zhimg.com/v2-864b70095ba5024e206de2adceac5696_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;CPE 其实就是右图中的斜率，&lt;strong&gt;我们的优化目标是尽可能让直线躺平。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic1.zhimg.com/v2-84870d35766af848946b06c997c75a20_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;为什么要用-cpe&quot;&gt;为什么要用 CPE？&lt;/h2&gt;

&lt;p&gt;其实就是比较直观，而且容易跟运算单元的速度进行关联，&lt;strong&gt;可以很方便地估计出优化的幅度&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;下面是常见操作的CPE：&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic3.zhimg.com/v2-de7a80a0273f5bd7777f91bbf6a51f32_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;下面是对 capacity、latency、issue 的图形化描述：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;capacity：描述处理器单位时间能过处理的指令数量&lt;/li&gt;
  &lt;li&gt;latency：描述单位指令执行时间&lt;/li&gt;
  &lt;li&gt;issue：描述相同指令之间的执行间隔（可以翻译为发射时间）&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;img src=&quot;https://pic4.zhimg.com/v2-9ea27582d24adcc0edb4e9c9a135b6d3_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;通过各个模块的 CPE，快速估算出优化的幅度：&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;对于某台机器 load=2, mul=3, add=1&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ol&gt;
  &lt;li&gt;左图：关键路径消耗 (load+mul)&lt;em&gt;n=(2+3)&lt;/em&gt;n=&lt;strong&gt;5n 个时钟周期&lt;/strong&gt;&lt;/li&gt;
  &lt;li&gt;右图：关键路径消耗 (load+mul+mul)&lt;em&gt;n/2=(2+3+3)&lt;/em&gt;n/2=&lt;strong&gt;4n 个时钟周期，速度提高 20%&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;img src=&quot;https://pic4.zhimg.com/v2-bdf91c3d07ed070f8ffabad1a1f38643_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;当然，这个 CPE 在实际工作中计算出来是比较麻烦的，但是对于了解常见汇编指令的速度还是有一定帮助。&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;2-编译器优化的能力和局限性&quot;&gt;2. 编译器优化的能力和局限性&lt;/h1&gt;

&lt;h2 id=&quot;编译器能做啥&quot;&gt;编译器能做啥？&lt;/h2&gt;

&lt;p&gt;这里介绍几个编译器能做的优化。&lt;/p&gt;

&lt;p&gt;优化计算表达式：将复杂表达式转换为简单表达式，例如用位操作来代替乘法；&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic3.zhimg.com/v2-0c6fdf42f60d30b2d9e23ba9ccdcfe0a_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;使用公共变量：使用临时变量等方式来避免重复几计算；&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic3.zhimg.com/v2-4876145c4a99ab70ff6bda94a3d06352_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;编译器挫在哪&quot;&gt;编译器挫在哪？&lt;/h2&gt;

&lt;h3 id=&quot;挫一处理函数调用&quot;&gt;挫一：处理函数调用&lt;/h3&gt;

&lt;p&gt;&lt;img src=&quot;https://pic3.zhimg.com/v2-b28bec969a58c881677b441585a503d6_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;为什么编译器不能够发现，并将&lt;/strong&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;strlen&lt;/code&gt; &lt;strong&gt;提取到循环外？&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic1.zhimg.com/v2-5fbb65b4fe971565c69f10791da31014_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;函数可能有副作用，例如修改某个全局变量&lt;/li&gt;
  &lt;li&gt;函数不一定是幂等的，例如依赖某个全局变量&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
  &lt;p&gt;编译器倾向于把函数调用当作黑盒，因为无法知道其副作用，所以不会对此进行优化。 —— 鲁迅&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id=&quot;挫二内存别名引用&quot;&gt;挫二：内存别名引用&lt;/h3&gt;

&lt;p&gt;&lt;img src=&quot;https://pic1.zhimg.com/v2-920888a4c2d7010a91086abbc4731738_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic4.zhimg.com/v2-5d07e083c4c3f0bee6926f69ffbba1b3_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;编译器不能优化这点吗？&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;看上去编译器似乎能够优化这点，但是编译器会假设两个指针地址可能相同，因此必须非常谨慎，躺平不动。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic1.zhimg.com/v2-182e16d4946b894686efb14e72f13e9c_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;程序员要养成使用局部变量的习惯，显式地告诉编译器，这里没有内存别名。 —— 鲁迅&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;3-现代微处理器&quot;&gt;3. 现代微处理器&lt;/h1&gt;

&lt;p&gt;到目前为止，我们提及到的优化技巧，都不依赖于目标机器的任何特性。我们目前的优化，只是简单的降低了过程调用的开销、避开了编译器的挫。如果要进一步进行优化，必须考虑利用现代微处理器的优化，也就是处理器用来执行指令的底层系统设计。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic2.zhimg.com/v2-9c33589c72e723cddf021d921976b30d_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;这样的处理器，在工业界称为超标量（Superscalar），&lt;strong&gt;即每个时钟周期可以执行多个指令，且是乱序执行&lt;/strong&gt;。整个处理器分为两大部分，指令控制单元（ICU）和执行单元（EU）。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic2.zhimg.com/v2-aca2a7b65d4f556495410e48088c2d8d_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;指令控制单元&lt;/strong&gt;：从高速缓存中取指、译码，生成一组 low level 的基本操作；&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;执行单元&lt;/strong&gt;：执行上述基本操作；包括多个功能单元，比如 arith（算术运算）、load（内存读）、store（内存写）等等，分别负责各自独立的计算和存取内存操作。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;当程序遇到分支的时候，程序可能有两个前进的方向。现代处理器使用了&lt;strong&gt;分支预测&lt;/strong&gt;技术（Branch Prediction），会猜测是否选择分支，且会预测分支跳转的目标地址。然后使用&lt;strong&gt;投机执行&lt;/strong&gt;（Speculative Execution）技术对目标分支跳转到的指令进行取指和译码（甚至在分支预测之前就开始投机执行）。如果之后确定分支预测错误，则会将寄存器状态重置为分支点的状态，并开始取出和执行另一个分支上的指令。所以可以看到，&lt;strong&gt;分支预测错误会导致很大的性能开销。&lt;/strong&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;4-让处理器助你一臂之力吧&quot;&gt;4. 让处理器助你一臂之力吧！&lt;/h1&gt;

&lt;h2 id=&quot;循环展开&quot;&gt;循环展开&lt;/h2&gt;

&lt;p&gt;&lt;img src=&quot;https://pic3.zhimg.com/v2-8361747f231f7e18824b729bba85b60a_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;优化前后的关键路径对比，&lt;strong&gt;可以看到每 2 个 Element，节省了一个 load 操作，速度提高 20%：&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic3.zhimg.com/v2-5f4d6cdda7330beaf4c1d909ba6348d6_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;提高并行性&quot;&gt;提高并行性&lt;/h2&gt;

&lt;h3 id=&quot;分治法&quot;&gt;分治法&lt;/h3&gt;

&lt;p&gt;&lt;img src=&quot;https://pic1.zhimg.com/v2-a4c2a5ae6b34407ad52fa506fa295d20_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;优化前后的关键路径对比，&lt;strong&gt;可以看到每个 2 个 Element，节省了 mul 操作，速度提高 1x 多：&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;左边关键路径：load+2*mul=8&lt;/li&gt;
  &lt;li&gt;右边关键路径：load+mul=5&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;https://pic4.zhimg.com/v2-25b4ed4e4cccad04993fc2d01a30c17b_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;重新结合运算&quot;&gt;重新结合运算&lt;/h3&gt;

&lt;p&gt;&lt;img src=&quot;https://pic1.zhimg.com/v2-591a7d89ac9e4b205aa415353aa9dda0_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;优化前后的关键路径对比，&lt;strong&gt;可以看到每个 4 个 Element，节省了 2mul+2load 操作，速度提高 2x 多：&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;左边关键路径：(load+2mul)2=16&lt;/li&gt;
  &lt;li&gt;右边关键路径：2*mul=6&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;https://pic4.zhimg.com/v2-f18d1306b38b8c0a0f3e7e955c7764b3_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;小结&quot;&gt;小结&lt;/h2&gt;

&lt;p&gt;本质：解除数据依赖&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;分治法：利用每个子问题的并行执行提高速度&lt;/li&gt;
  &lt;li&gt;重新结合变换：解除多项式操作中，项之间的关键依赖&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;分支&quot;&gt;分支&lt;/h2&gt;

&lt;h3 id=&quot;书写容易预测的代码&quot;&gt;书写容易预测的代码&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;https://link.zhihu.com/?target=https%3A//stackoverflow.com/questions/11227809/why-is-processing-a-sorted-array-faster-than-processing-an-unsorted-array&quot;&gt;Why is processing a sorted array faster than processing an unsorted array? - Stack Overflow&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;现代处理器可以很好预测分支指令的&lt;strong&gt;有规律模式&lt;/strong&gt;。&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;if (data[c] &amp;gt;= 128)
    sum += data[c];

T = branch taken
N = branch not taken


data[] = 0, 1, 2, 3, 4, ... 126, 127, 128, 129, 130, ... 250, 251, 252, ...
branch = N  N  N  N  N  ...   N    N    T    T    T  ...   T    T    T  ...
       = NNNNNNNNNNNN ... NNNNNNNTTTTTTTTT ... TTTTTTTTTT  (easy to predict)


data[] = 226, 185, 125, 158, 198, 144, 217, 79, 202, 118,  14, 150, 177, 182, ...
branch =   T,   T,   N,   T,   T,   T,   T,  N,   T,   N,   N,   T,   T,   T  ...
       = TTNTTTTNTNNTTT ...   (completely random - impossible to predict)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://www.zhihu.com/question/23973128&quot;&gt;CPU 的分支預測器是怎樣工作的？&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic4.zhimg.com/v2-765bcfa1a3f9a7cb2392c7395688557b_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;书写适合条件传送的代码&quot;&gt;书写适合&lt;strong&gt;条件传送&lt;/strong&gt;的代码&lt;/h3&gt;

&lt;p&gt;&lt;img src=&quot;https://pic2.zhimg.com/v2-a8140e15385d26862c0ca72064e3303d_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;条件跳转&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;https://pic4.zhimg.com/v2-fb189135b7a1e068850edcab11e51d7b_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;条件传送&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;https://pic4.zhimg.com/v2-3e10044c588dfe84a290b56be716359f_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;为什么条件传送更快？&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;先计算，再选择结果。且这些计算没有数据依赖，可以充分利用处理器的指令流水。&lt;/p&gt;

&lt;h2 id=&quot;simd&quot;&gt;SIMD&lt;/h2&gt;

&lt;blockquote&gt;
  &lt;p&gt;SIMD（&lt;strong&gt;Single Instruction Multiple Data&lt;/strong&gt;&lt;strong&gt;）&lt;/strong&gt;即单指令流多数据流，是一种采用一个控制器来控制多个执行器，同时对一组数据（又称“数据向量”）中的每一个分别执行相同的操作从而实现空间上的并行性的技术。简单来说就是一个指令能够同时处理多个数据。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href=&quot;https://link.zhihu.com/?target=http%3A//csapp.cs.cmu.edu/3e/waside/waside-simd.pdf&quot;&gt;Achieving Greater Parallelism with SIMD Instructions&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic3.zhimg.com/v2-fac68f950f2acc170c6ea3bd4d48fb9e_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h1 id=&quot;5-小结&quot;&gt;5. 小结&lt;/h1&gt;

&lt;ul&gt;
  &lt;li&gt;良好的编码习惯
    &lt;ul&gt;
      &lt;li&gt;数据结构与算法&lt;/li&gt;
      &lt;li&gt;消除编译器优化障碍
        &lt;ul&gt;
          &lt;li&gt;过程调用&lt;/li&gt;
          &lt;li&gt;内存别名引用&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;优化循环&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;帮助机器更好地优化
    &lt;ul&gt;
      &lt;li&gt;利用指令并行&lt;/li&gt;
      &lt;li&gt;避免不可预测的分支&lt;/li&gt;
      &lt;li&gt;提高指令缓存命中率&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;本文所讲的这些优化方法，在大部分编译器中都已经实现了。&lt;/strong&gt;但是它们有可能不会实行这些优化，或需要我们手动设置更高级别的优化选项才行。所以，&lt;strong&gt;作为一个程序员，我们应该做的是尽量引导编译器执行这些优化，或者说排除阻碍编译器优化的障碍。&lt;/strong&gt;这样可以使我们的代码在保持简洁的情况下获得更高的性能。迫不得已时，我们才去手动做这些优化。&lt;/p&gt;

&lt;p&gt;另外，循环展开、多路并行并不是越多越好。因为寄存器的个数是有限的，x86-64 最多只能有 12 个寄存器用于累加，如果局部变量的个数多于 12 个，就会被放进存储器，反倒严重拉低程序性能。&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;6-身边活生生的例子&quot;&gt;6. 身边活生生的例子&lt;/h1&gt;

&lt;h2 id=&quot;数据依赖&quot;&gt;数据依赖&lt;/h2&gt;

&lt;p&gt;组内有同学反馈，跑 pprof 的时候，发现一个简单的函数调用占用了 20% 左右的 CPU 时间，这个函数只是取了一个对象里面的列表元素做简单计算。只不过对象潜套了多个指针，于是怀疑是指针嵌套的问题。&lt;/p&gt;

&lt;p&gt;我把对应的代码抽出来，对比了直接引用（左）与指针嵌套引用（右）的汇编代码区别：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic2.zhimg.com/v2-a11bafb119b4992bf2f2b041e795ced9_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;可以发现，右侧代码中，红圈中的 5 行指令都是在 DX 寄存器中进行操作，形成了数据依赖，无法充分利用指令流水。&lt;/p&gt;

&lt;h2 id=&quot;指令缓存&quot;&gt;指令缓存&lt;/h2&gt;

&lt;blockquote&gt;
  &lt;p&gt;摘自内部 infra 组同学的一篇文章，描述的是 RPC 框架一个小改动带来的性能问题。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;事故现场：&lt;a href=&quot;https://link.zhihu.com/?target=https%3A//editor.mergely.com/HPqQpVCJ/&quot;&gt;Mergely - Diff online, merge documents&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;可以看到，相比旧版本的生成代码，该 commit 在返回错误的时候会额外包装一下：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-Go&quot;&gt;if err := ...; err != nil {
    return thrift.PrependError(fmt.Sprintf(&quot;%T read field x &apos;xxx&apos; error: &quot;, p), err)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;而旧版本的生成代码是直接返回的错误：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-Go&quot;&gt;if err := ...; err != nil {
    return err
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;虽然这些只是在发生错误的时候才会调用到，在正常流程中不会用到，但是生成的汇编代码中这段逻辑占了相当大的比例：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic4.zhimg.com/v2-a7628ca8d1e68fc73db83d78e0c64fa7_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;而 Go 的编译器并没有帮我们重排这些指令，导致在真正运行的时候，L1 cache miss 大大提高，极大地降低了性能。&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;拓展阅读&quot;&gt;拓展阅读&lt;/h1&gt;

&lt;p&gt;以下是 golang 一些性能分析的拓展阅读，有兴趣可以看。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://link.zhihu.com/?target=https%3A//github.com/sxs2473/go-performane-tuning/blob/master/3.%E6%80%A7%E8%83%BD%E6%B5%8B%E9%87%8F%E5%92%8C%E5%88%86%E6%9E%90/%E6%80%A7%E8%83%BD%E6%B5%8B%E9%87%8F%E5%92%8C%E5%88%86%E6%9E%90.md&quot;&gt;Golang 性能测量和分析&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://link.zhihu.com/?target=https%3A//blog.wolfogre.com/posts/go-ppof-practice/&quot;&gt;golang pprof 实战&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://link.zhihu.com/?target=https%3A//eddycjy.com/posts/go/tools/2018-09-15-go-tool-pprof/&quot;&gt;Go 大杀器之性能剖析 PProf&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://link.zhihu.com/?target=https%3A//juejin.cn/post/6844903887757901831&quot;&gt;Golang 大杀器之跟踪剖析 trace&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1 id=&quot;reference&quot;&gt;Reference&lt;/h1&gt;

&lt;ul&gt;
  &lt;li&gt;CMU 课件 &lt;a href=&quot;https://link.zhihu.com/?target=http%3A//www.cs.cmu.edu/afs/cs/academic/class/15213-f15/www/lectures/10-optimization.pdf&quot;&gt;10-optimization&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;CMU 电子书、课程等资料地址 &lt;a href=&quot;https://link.zhihu.com/?target=https%3A//github.com/wangmu89/Book-CSAPP&quot;&gt;wangmu89/Book-CSAPP: 深入理解计算机系统&lt;/a&gt;&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;https://github.com/wangmu89/Book-CSAPP)&lt;/li&gt;
&lt;/ul&gt;
</description>
			<pubDate>Sun, 22 Aug 2021 00:00:00 +0000</pubDate>
			<link>https://sarkerson.github.io/2021/08/22/%E4%BC%98%E5%8C%96%E7%A8%8B%E5%BA%8F%E6%80%A7%E8%83%BD-CSAPP-%E7%AC%94%E8%AE%B0/</link>
			<guid isPermaLink="true">https://sarkerson.github.io/2021/08/22/%E4%BC%98%E5%8C%96%E7%A8%8B%E5%BA%8F%E6%80%A7%E8%83%BD-CSAPP-%E7%AC%94%E8%AE%B0/</guid>
        
			<category>os</category>
        
        
		</item>
    
		<item>
			<title>搜索系统：Elasticsearch 的架构与读写机制</title>
			<description>&lt;h1 id=&quot;搜索系统elasticsearch-的架构与读写机制&quot;&gt;搜索系统：Elasticsearch 的架构与读写机制&lt;/h1&gt;

&lt;blockquote&gt;
  &lt;p&gt;TL;DR; 本文从分布式架构、数据读写、应用场景对 Elasticsearch 做一个概括性介绍，让读者了解 ES 是什么，能在哪些场景应用，为什么 ES 这么快。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1 id=&quot;introduction&quot;&gt;Introduction&lt;/h1&gt;

&lt;p&gt;这个世界已然被数据淹没，但是如果数据只是躺在磁盘里面根本就毫无用处。Elasticsearch（ES）是一个分布式、可扩展、文档存储、实时的搜索与数据分析引擎。 它能从落库开始就赋予你的数据以检索、搜索、分析的能力。Elasticsearch 基于 Lucene。
Lucene 可以说是当下最先进、高性能、全功能的搜索引擎库—无论是开源还是私有。
Elasticsearch 底层依赖 Lucene，通过隐藏 Lucene 的复杂性，取而代之的提供一套简单一致的 RESTful API。&lt;/p&gt;

&lt;h1 id=&quot;concept&quot;&gt;Concept&lt;/h1&gt;

&lt;p&gt;首先看下 ES 文档存储的几个重要概念：&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;&lt;strong&gt;名称&lt;/strong&gt;&lt;/th&gt;
      &lt;th&gt;&lt;strong&gt;概念&lt;/strong&gt;&lt;/th&gt;
      &lt;th&gt;&lt;strong&gt;对应关系型数据库概念&lt;/strong&gt;&lt;/th&gt;
      &lt;th&gt;&lt;strong&gt;说明&lt;/strong&gt;&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;index&lt;/td&gt;
      &lt;td&gt;索引&lt;/td&gt;
      &lt;td&gt;Database&lt;/td&gt;
      &lt;td&gt;具有相似特点的文档的集合，可以对应为关系型数据库中的数据库，通过名字在集群内唯一标识&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;type&lt;/td&gt;
      &lt;td&gt;文档类别&lt;/td&gt;
      &lt;td&gt;Table&lt;/td&gt;
      &lt;td&gt;索引内部的逻辑分类，可以对应为 Mysql 中的表&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;document&lt;/td&gt;
      &lt;td&gt;文档&lt;/td&gt;
      &lt;td&gt;Row&lt;/td&gt;
      &lt;td&gt;构成索引的最小单元，属于一个索引的某个类别，从属关系为： Index -&amp;gt; Type -&amp;gt; Document，通过 id 在Type 内唯一标识&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;field&lt;/td&gt;
      &lt;td&gt;字段&lt;/td&gt;
      &lt;td&gt;Column&lt;/td&gt;
      &lt;td&gt;构成文档的单元&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;然后是 ES 集群相关几个重要概念：&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;&lt;strong&gt;名称&lt;/strong&gt;&lt;/th&gt;
      &lt;th&gt;&lt;strong&gt;概念&lt;/strong&gt;&lt;/th&gt;
      &lt;th&gt;&lt;strong&gt;说明&lt;/strong&gt;&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;cluster&lt;/td&gt;
      &lt;td&gt;集群&lt;/td&gt;
      &lt;td&gt;一个或多个 Node 的集合，ES 可以通过跨集群的备份，来提高服务稳定性&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Node&lt;/td&gt;
      &lt;td&gt;节点&lt;/td&gt;
      &lt;td&gt;运行 ES 的单个实例，保存数据并具有索引和搜索的能力，可以包含多个 Shard&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Shard&lt;/td&gt;
      &lt;td&gt;分片&lt;/td&gt;
      &lt;td&gt;索引分为多个块，每块叫做一个 Shard。索引定义时需要指定分片数且不能更改（因为再分片相当于重建索引）。&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Replica&lt;/td&gt;
      &lt;td&gt;分片的备份&lt;/td&gt;
      &lt;td&gt;每个分片默认一个 Replica，它可以提升节点的可用性，同时能够提升搜索时的并发性能（搜索可以在全部分片上并行执行）&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h1 id=&quot;distributed-architecture&quot;&gt;Distributed Architecture&lt;/h1&gt;

&lt;h2 id=&quot;data-replication&quot;&gt;Data Replication&lt;/h2&gt;

&lt;p&gt;ES 为主备架构，即 Shard 分为 Primary Shard 及 Replica Shard，写请求走 Primary Shard，读请求则均衡打到所有 Shard，Replica Shard 的数据从 Primary Shard 同步。
下图是含有 3 个 Node 的集群：
&lt;img src=&quot;https://tech-proxy.bytedance.net/tos/images/1614694387246_13f3040b660770188c1e520890d0d88f&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;蓝色部分：有 3 个 Shard，分别是 P1，P2，P3，位于 3 个不同的 Node 中，这里没有 Replica&lt;/li&gt;
  &lt;li&gt;绿色部分：有 2 个 Shard，分别是 P1、P2，位于 2 个不同的 Node 中，每个 Shard 都有一个 Replica Shard，分别是 R1，R2。基于可用性考虑，同一个 Shard 的 Primary 和 Replica 节点不能处于同一个 Node 中。这里 Shard1 的 P1 和 R1 分别位于 Node3 和 Node2 中，如果某一刻 Node2 发生宕机，服务基本不会受影响，因为还有一个 P1 和 R2 都还是可用的。因为是主备架构，当主分片发生故障时，需要切换，这时候需要选举一个副本作为新主，这里除了会耗费一些时间外，也会有丢失数据的风险。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;distributed-searching&quot;&gt;Distributed Searching&lt;/h2&gt;

&lt;p&gt;现在从全局视角，来看一个分布式搜索是如何执行的。搜索不同于 CRUD 操作，在 CRUD 操作中，我们是知道具体集群中哪个 Shard 含有该文档。搜索则比较复杂，因为目标文档可能存在集群中任何 Shard 上面。&lt;/p&gt;

&lt;h3 id=&quot;query&quot;&gt;Query&lt;/h3&gt;

&lt;p&gt;在 Query 阶段， 查询会广播到索引中每一个分片拷贝。 每个分片在本地执行搜索并构建一个匹配文档的 topk 队列。
&lt;img src=&quot;https://tech-proxy.bytedance.net/tos/images/1614694387149_b82a505e0413c9985a81f92e05622eaa&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;客户端发送一个 search 请求到随机一个节点，这里是 Node3，Node3 会创建一个大小为 K 的优先队列（K 为请求的分页参数 from 和 size 决定）&lt;/li&gt;
  &lt;li&gt;Node3 将请求转发到索引的每个分片中（primary 或者 replica 都有可能）。每个分片在本地执行搜索请求，将结果排序并放到大小 K 的优先队列中。&lt;/li&gt;
  &lt;li&gt;每个分片返回各自优先队列中的文档 ID 以及排序 Score 给协调节点，即 Node3，Node3 对所有结果进行合并、排序，得到一个全局排序后的文档 ID 列表。&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;fetch&quot;&gt;Fetch&lt;/h3&gt;

&lt;p&gt;查询阶段标识哪些文档 ID 满足搜索请求，Fetch 阶段则用来取回这些文档。
&lt;img src=&quot;https://tech-proxy.bytedance.net/tos/images/1614694387155_a7342b7ecc5ae3c96661b829e20a2822&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;协调节点即 Node3，根据 Query 得到的文档 ID 列表，向相关分片提交多个 GET 请求。&lt;/li&gt;
  &lt;li&gt;每个分片加载文档返回给 Node3&lt;/li&gt;
  &lt;li&gt;等待所有文档取回，将结果合并返回给客户端&lt;/li&gt;
&lt;/ol&gt;

&lt;h1 id=&quot;data-in&quot;&gt;Data in&lt;/h1&gt;

&lt;p&gt;通过使用 index API ，文档可以被索引（indexing） —— 存储和使文档可被搜索。 但是首先，我们要确定文档的位置。一个文档的唯一标示，由 _index 、 _type 和 _id 决定。 我们可以提供自定义的 _id 值，或者让 index API 自动生成。
下面简单看下 ES 提供的 CRUD 接口。&lt;/p&gt;

&lt;h2 id=&quot;write&quot;&gt;Write&lt;/h2&gt;

&lt;p&gt;我们可以提供自定义的 _id 值，或者让 index API 自动生成。&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;PUT /{index}/{type}/{id}
{
  &quot;field&quot;: &quot;value&quot;,
  ...
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ul&gt;
  &lt;li&gt;客户端发送请求给随机一个 Node，这个 Node 就是 coordinating node （协调节点）。&lt;/li&gt;
  &lt;li&gt;coordinating node 对 document 进行&lt;strong&gt;路由&lt;/strong&gt;，将请求转发给对应的 Node（有 primary shard）。&lt;/li&gt;
  &lt;li&gt;实际的 node 上的 primary shard 处理请求，然后将数据同步到 replica node 。&lt;/li&gt;
  &lt;li&gt;coordinating node 如果发现 primary node 和所有 replica node 都写入完成之后，就返回响应结果给客户端。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;read&quot;&gt;Read&lt;/h2&gt;

&lt;p&gt;为了从 Elasticsearch 中检索出文档，我们仍然使用相同的 _index , _type , 和 _id ，但是 HTTP 方法更改为 GET :&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;GET /{index}/{type}/{id}?pretty
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;响应体包括目前已经熟悉了的元数据元素，再加上 _source 字段，这个字段包含我们索引数据时发送给 Elasticsearch 的原始 JSON 文档：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;{
  &quot;_index&quot; :   &quot;website&quot;,
  &quot;_type&quot; :    &quot;blog&quot;,
  &quot;_id&quot; :      &quot;123&quot;,
  &quot;_version&quot; : 1,
  &quot;found&quot; :    true,
  &quot;_source&quot; :  {
      &quot;field&quot;: &quot;value&quot;,
      ...
  }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ul&gt;
  &lt;li&gt;客户端发送请求到&lt;strong&gt;任意&lt;/strong&gt;一个 Node，称为 coordinate node 。&lt;/li&gt;
  &lt;li&gt;coordinate node 对 doc id 进行哈希路由，将请求转发到对应的 Node，此时会使用 round-robin &lt;strong&gt;随机轮询算法&lt;/strong&gt;，在 primary shard 以及其所有 replica 中随机选择一个，让读请求负载均衡。&lt;/li&gt;
  &lt;li&gt;接收请求的 Node 返回 document 给 coordinate node 。&lt;/li&gt;
  &lt;li&gt;coordinate node 返回 document 给客户端。&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;当然，ES 还提供 update、delete 接口用来更新、删除文档，形式与上述相同在此不再赘述。另外，还有用于批量查询、批量操作的 mget、bulk 接口，感兴趣可以查阅官方权威指南。
需要注意的是，update 操作其实相当于创建一个新文档、删除旧文档的过程。此外，ES 通过使用多版本控制算法来进行并发写冲突解决。&lt;/p&gt;

&lt;h1 id=&quot;information-out&quot;&gt;Information out&lt;/h1&gt;

&lt;p&gt;Elasticsearch 除了可以提供文档及其元数据存储之外，其最强大的莫过于基于 Lucene 而提供的搜索能力。&lt;/p&gt;

&lt;h2 id=&quot;search&quot;&gt;Search&lt;/h2&gt;

&lt;p&gt;一个搜索请求可以包含一个或多个 query 来指定搜索参数。匹配结果则返回在 response 中的 hits 中。
下面例子列举了一个简单的搜索请求，即查询 user.id 为 kimchy 的所有文档（假设该字段为 keyword，即精确匹配。字段类型，即 mapping 可以参考 Indexing 章节）&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;GET /my-index-000001/_search
{
  &quot;query&quot;: {
    &quot;match&quot;: {
      &quot;user.id&quot;: &quot;kimchy&quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;默认返回匹配搜索结果的 top10 文档，上面例子只返回一条：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;{
  &quot;took&quot;: 5,
  &quot;timed_out&quot;: false,
  &quot;_shards&quot;: {
    &quot;total&quot;: 1,
    &quot;successful&quot;: 1,
    &quot;skipped&quot;: 0,
    &quot;failed&quot;: 0
  },
  &quot;hits&quot;: {
    &quot;total&quot;: {
      &quot;value&quot;: 1,
      &quot;relation&quot;: &quot;eq&quot;
    },
    &quot;max_score&quot;: 1.3862942,
    &quot;hits&quot;: [
      {
        &quot;_index&quot;: &quot;my-index-000001&quot;,
        &quot;_type&quot;: &quot;_doc&quot;,
        &quot;_id&quot;: &quot;kxWFcnMByiguvud1Z8vC&quot;,
        &quot;_score&quot;: 1.3862942,
        &quot;_source&quot;: {
          &quot;@timestamp&quot;: &quot;2099-11-15T14:12:12&quot;,
          &quot;http&quot;: {
            &quot;request&quot;: {
              &quot;method&quot;: &quot;get&quot;
            },
            &quot;response&quot;: {
              &quot;bytes&quot;: 1070000,
              &quot;status_code&quot;: 200
            },
            &quot;version&quot;: &quot;1.1&quot;
          },
          &quot;message&quot;: &quot;GET /search HTTP/1.1 200 1070000&quot;,
          &quot;source&quot;: {
            &quot;ip&quot;: &quot;127.0.0.1&quot;
          },
          &quot;user&quot;: {
            &quot;id&quot;: &quot;kimchy&quot;
          }
        }
      }
    ]
  }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;此外，ES 搜索请求还支持很多选项，例如&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Query DSL：ES 提供一种基于 JSON 的请求体，并提供一些内置的请求类型，可以供用户自由进行组合。&lt;/li&gt;
  &lt;li&gt;聚合：可以使用 aggregation 即聚合操作，对搜索结果进行统计分析&lt;/li&gt;
  &lt;li&gt;多重搜索：可以使用使用正则式或者逗号分隔符搜索多个索引，例如 GET /my-index-000001,my-index-000002/_search&lt;/li&gt;
  &lt;li&gt;分页：默认 ES 只返回前 10 条匹配结果，但是 ES 也提供了由 from+size 组合的分页参数&lt;/li&gt;
  &lt;li&gt;获取指定字段：默认搜索结果返回整个文档，ES 也支持获取文档字段的子集&lt;/li&gt;
  &lt;li&gt;排序：默认搜索结果按照相关度进行排序，ES 也支持&lt;a href=&quot;https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-script-score-query.html&quot;&gt;script_score&lt;/a&gt; 定制化分数计算。&lt;/li&gt;
  &lt;li&gt;异步搜索：某些搜索请求可能需要跨多个分片进行，并且分片数据很大，这时候搜素可能要花费较长时间。ES 提供异步接口，可以提交搜索请求、查询搜索状态、获取搜索结果。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;analyze&quot;&gt;Analyze&lt;/h2&gt;

&lt;p&gt;分析（analyze）其实就是基于聚合（aggregation）能力进行各种统计分析，得到一些统计报表。aggregation 可以帮你回答类似下面的问题：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;在大海里有多少针？&lt;/li&gt;
  &lt;li&gt;针的平均长度是多少？&lt;/li&gt;
  &lt;li&gt;按照针的制造商来划分，针的长度中位值是多少？&lt;/li&gt;
  &lt;li&gt;每月加入到海中的针有多少？&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;总的来说，Elasticsearch（7.11 版本）提供三种类型的聚合功能。&lt;/p&gt;

&lt;h3 id=&quot;metric-aggregation&quot;&gt;Metric Aggregation&lt;/h3&gt;

&lt;p&gt;统计指标，类似字段的和、均值、方差等等，可以从文档中提取某字段通过内置聚合函数进行聚合，或者通过脚本进行求值。
ES 提供非常丰富的聚合函数，下面举几个例子：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Geo-bounds：返回给定字段的边界点坐标，即 top_left、bottom_right&lt;/li&gt;
  &lt;li&gt;Stats：返回给定字段的 min, max, sum, count 和 avg 信息&lt;/li&gt;
  &lt;li&gt;Cardinality：给定字段和计算精度，返回给定字段的近似基数&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;bucket-aggregation&quot;&gt;Bucket Aggregation&lt;/h3&gt;

&lt;p&gt;将文档进行聚合，类似关系型数据库的 groupby，当然 bucket 也可以支持嵌套，例如 province bucket 下面嵌套 city bucket。
ES 提供非常丰富的聚合函数，下面举几个例子：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Sampler：一种过滤器，只返回相关性高的文档，去掉长尾的低质文档&lt;/li&gt;
  &lt;li&gt;Date range：提供人性化的时间区间（相对、绝对），根据时间进行聚合&lt;/li&gt;
  &lt;li&gt;Geo-Distance：给定点，在二维坐标点中根据距离区间进行聚合&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;pipeline-aggregation&quot;&gt;Pipeline Aggregation&lt;/h3&gt;

&lt;p&gt;将其他 aggregation 结果进行聚合，而不是简单对匹配文档进行聚合计算。例如，你想聚合统计全国每个省份的 GDP，得到一个 province_gdp 的 bucket；你可以通过 pipeline aggregation 对 province_gdp 进一步进行聚合，例如取得 max 的省份的 GDP。
ES 提供非常丰富的聚合函数，总的来说与 Metric Aggregation 类似，具体可以参阅 &lt;a href=&quot;https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline.html&quot;&gt;官方文档&lt;/a&gt;。&lt;/p&gt;

&lt;h1 id=&quot;indexing&quot;&gt;Indexing&lt;/h1&gt;

&lt;h2 id=&quot;mapping&quot;&gt;Mapping&lt;/h2&gt;

&lt;p&gt;为了能够将时间域视为时间，数字域视为数字，字符串域视为全文或精确值字符串， Elasticsearch 需要知道每个域中数据的类型。这个信息包含在映射（mapping）中。
当你索引一个包含新域的文档的时候，Elasticsearch 会使用 &lt;a href=&quot;https://www.elastic.co/guide/cn/elasticsearch/guide/2.x/dynamic-mapping.html&quot;&gt;动态映射&lt;/a&gt; ，通过JSON中基本数据类型，尝试猜测域类型。
这意味着如果你通过引号( “123” )索引一个数字，它会被映射为 string 类型，而不是 long 。但是，如果这个域已经映射为 long ，那么 Elasticsearch 会尝试将这个字符串转化为 long ，如果无法转化，则抛出一个异常。
尽管在很多情况下基本数据类型已经够用，但你经常需要为某些域自定义映射，特别是字符串域。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;域最重要的属性是 type 。对于不是 string 的域，一般只需要设置 type 。&lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;{
    &quot;number_of_clicks&quot;: {
        &quot;type&quot;: &quot;integer&quot;
    }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;string 类型域则默认包含全文。就是说，它们的值在创建索引前，会通过一个分析器，针对于这个域的查询在搜索前也会经过一个分析器。string 域映射的两个最重要属性是 index 和 analyzer 。&lt;/p&gt;

    &lt;ul&gt;
      &lt;li&gt;
        &lt;p&gt;index：控制如何索引字符串，string 类型默认是 analized&lt;/p&gt;

        &lt;ul&gt;
          &lt;li&gt;analyzed：首先分析字符串，然后构建全文索引&lt;/li&gt;
          &lt;li&gt;not_analyzed：索引这个域，但索引的是精确值&lt;/li&gt;
          &lt;li&gt;no：不索引这个域，即无法被搜索&lt;/li&gt;
        &lt;/ul&gt;

        &lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;{
    &quot;tag&quot;: {
        &quot;type&quot;:     &quot;string&quot;,
        &quot;index&quot;:    &quot;not_analyzed&quot;
    }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;        &lt;/div&gt;
      &lt;/li&gt;
      &lt;li&gt;
        &lt;p&gt;analyzer：对于 analyzed 字符串域，用 analyzer 属性指定在搜索和索引时使用的分析器&lt;/p&gt;

        &lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;{
    &quot;tweet&quot;: {
        &quot;type&quot;:     &quot;string&quot;,
        &quot;analyzer&quot;: &quot;english&quot;
    }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;        &lt;/div&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;当你首次创建一个索引的时候，可以指定类型的映射。你也可以使用 /_mapping 为新类型增加映射，但是不能修改现有的映射，例如将一个存在的域从 analyzed 改为 not_analyzed。&lt;/p&gt;

&lt;h2 id=&quot;index-structure&quot;&gt;Index Structure&lt;/h2&gt;

&lt;blockquote&gt;
  &lt;p&gt;索引创建：就是从语料库中提取信息，创建索引的过程。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;blockquote&gt;
  &lt;p&gt;搜索索引：就是得到用户的查询请求，搜索创建的索引，然后返回结果的过程。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;ES 基于 Lucene，而 Lucene 的索引结构为倒排索引，大致如图所示
&lt;img src=&quot;https://tech-proxy.bytedance.net/tos/images/1614694387216_ac8a56b9db9758cf5f1eebdd3ff69ccc&quot; alt=&quot;&quot; /&gt;
我们通常习惯使用正排索引，即形如 user_id =&amp;gt; user_info 的映射；而倒排索引则反过来，通过形如 user_info.name =&amp;gt; user_ids，或者 user_info.age =&amp;gt; user_ids 得到一个逆映射，并且映射的 ID 值通常是一个列表。&lt;/p&gt;

&lt;h3 id=&quot;posting-list&quot;&gt;Posting List&lt;/h3&gt;

&lt;p&gt;Posting List 其实就是文档 ID 列表，例如上述例子的 user_ids。&lt;/p&gt;

&lt;h3 id=&quot;term-dictionary&quot;&gt;Term Dictionary&lt;/h3&gt;

&lt;p&gt;Term 指的是文档中的字段值。例如，name 字段可以有很多个 Term，比如：Carla，Sara，Elin，Ada，Patty，Kate，Selena。如果没有排序，那么找出某个特定的 Term 会很慢，因为 Term 没有排序，需要全部遍历一遍才能找到特定的 Term，排序之后就变成了：Ada，Carla，Elin，Kate，Patty，Sara，Selena，这样就可以用二分搜索，快速找到目标的 Term。
而如何组织这些 Term的方式就是 Term Dictionary，除了存储 Term 的值之外，还存储 Term 的统计值例如词频。有了 Term Dictionary 之后，就可以用比较少的比较次数和磁盘读次数查找目标。&lt;/p&gt;

&lt;h3 id=&quot;term-index&quot;&gt;Term Index&lt;/h3&gt;

&lt;p&gt;通常 Index 的量级非常大，因此 Term Dictionary 也非常大，无法直接 load 到内存中，因此需要一种保存在内存中的压缩的数据结构来加速读取。Term Index 其实就是一种前缀树（也是一种有限状态机，FST），通过 Term Index 可以快速定位到目标 Term 在 Dictionary 文件中的 offset。
因此通过这么一条链路：Term Index =&amp;gt; Term Dictionary =&amp;gt; Posting List，通过 Posting List 里的文档 ID 查询，得到我们的结果文档，并根据相关度进行排序。&lt;/p&gt;

&lt;h1 id=&quot;application&quot;&gt;Application&lt;/h1&gt;

&lt;p&gt;ElaticSearch 可以有非常丰富的应用场景，笔者没办法进行全面列举，只选择了主要的三个：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;数据存储：建立 searchable 的文档数据、目录数据、日志数据系统&lt;/li&gt;
  &lt;li&gt;作为数据库的补充：例如利用 CDC 功能，对数据库内容添加 ES 索引，可以进行可视化等分析操作；甚至与 Hadoop 进行交互，对 Hadoop 数据提供快速的搜索、分析、可视化能力&lt;/li&gt;
  &lt;li&gt;数据分析：对存储的数据进行统计、分析、可视化&lt;/li&gt;
&lt;/ul&gt;

&lt;h1 id=&quot;conclusions&quot;&gt;Conclusions&lt;/h1&gt;

&lt;p&gt;Elasticsearch 是一个分布式的，RESTful 的分析引擎及搜索引擎。很多公司都转型使用 ES 融入其后端基础架构，因为 ES 提供很多能力：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;对海量数据进行聚合分析，得到一个数据的宏观模型&lt;/li&gt;
  &lt;li&gt;支持多样化的搜索及分析能力：精确匹配、模糊匹配、地理坐标搜索、统计分析&lt;/li&gt;
  &lt;li&gt;实时的处理能力&lt;/li&gt;
  &lt;li&gt;提供多种语言的客户端或 SDK，例如 SQL、Python、Java&lt;/li&gt;
  &lt;li&gt;ELK Stack 提供方便的数据收集、可视化功能&lt;/li&gt;
&lt;/ul&gt;

&lt;h1 id=&quot;references&quot;&gt;References&lt;/h1&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.elastic.co/guide/cn/elasticsearch/guide/2.x/index.html&quot;&gt;Elasticsearch: 权威指南&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html&quot;&gt;Elasticsearch: Reference&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
			<pubDate>Mon, 08 Mar 2021 00:00:00 +0000</pubDate>
			<link>https://sarkerson.github.io/2021/03/08/Elasticsearch-%E6%A6%82%E8%A7%88/</link>
			<guid isPermaLink="true">https://sarkerson.github.io/2021/03/08/Elasticsearch-%E6%A6%82%E8%A7%88/</guid>
        
			<category>database</category>
        
        
		</item>
    
		<item>
			<title>流处理：Flink 的架构与核心概念</title>
			<description>&lt;h1 id=&quot;流处理flink-的架构与核心概念&quot;&gt;流处理：Flink 的架构与核心概念&lt;/h1&gt;

&lt;blockquote&gt;
  &lt;p&gt;TL;DR 本文简要介绍了 Flink 的架构及提供的特性，梳理了流处理中几个关键概念，例如时间、窗口、流状态，并对比了流处理在一些应用场景下的优势。让读者了解，Flink 是什么，用 Flink 能做些什么。如有疏漏，欢迎指正。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1 id=&quot;overview&quot;&gt;Overview&lt;/h1&gt;

&lt;p&gt;Apache Flink 是一个在无界和有界数据流上进行状态计算的框架和分布式处理引擎。 Flink 已经可以在所有常见的集群环境中运行，并以 in-memory 的速度和任意的规模进行计算。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tech-proxy.bytedance.net/tos/images/1611997841322_229f7ce9bacfa415e49cbd222466df16&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;批处理&lt;/strong&gt;针对的是有界数据流。在这种模式下，你可以选择在计算结果输出之前输入整个数据集，这也就意味着你可以对整个数据集的数据进行排序、统计或汇总计算后再输出结果。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;流处理&lt;/strong&gt;正相反，其涉及无界数据流。至少理论上来说，它的数据输入永远不会结束，因此程序必须持续不断地对到达的数据进行处理。&lt;/p&gt;

&lt;p&gt;在 Flink 中，应用程序由用户自定义算子组成，即 &lt;strong&gt;streaming dataflows&lt;/strong&gt;。这些 dataflows 形成了有向图，以一个或多个 &lt;strong&gt;source&lt;/strong&gt; 开始，并以一个或多个 &lt;strong&gt;sink&lt;/strong&gt; 结束。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tech-proxy.bytedance.net/tos/images/1611997841344_7b5cb803c44619374e0e0cf271335a08&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Flink 应用程序可以消费来自消息队列或分布式日志这类流式数据源（例如 Apache Kafka 或 Kinesis）的实时数据，也可以从各种的数据源中消费有界的历史数据。同样，Flink 应用程序生成的结果流也可以 sink 到各种存储系统中。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tech-proxy.bytedance.net/tos/images/1611997841510_fa366a1efea01c0287013d314e3b88c2&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h1 id=&quot;架构&quot;&gt;架构&lt;/h1&gt;

&lt;p&gt;当 Flink 集群启动后，首先会启动一个 JobManger 和一个或多个的 TaskManager。由 Client 提交任务给 JobManager，JobManager 再调度任务到各个 TaskManager 去执行，然后 TaskManager 将心跳和统计信息汇报给 JobManager。TaskManager 之间以流的形式进行数据的传输。上述三者均为独立的 JVM 进程。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Client&lt;/strong&gt; 为提交 Job 的客户端，可以是运行在任何机器上（与 JobManager 环境连通即可）。提交 Job 后，Client 可以结束进程（Streaming的任务），也可以不结束并等待结果返回。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;JobManager&lt;/strong&gt; 主要负责调度 Job 并协调 Task 做 checkpoint。从 Client 处接收到 Job 和 JAR 包等资源后，会生成优化后的执行计划，并以 Task 的单元调度到各个 TaskManager 去执行。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;TaskManager&lt;/strong&gt; 在启动的时候就设置好了槽位数（Slot），每个 slot 能启动一个 Task，Task 为线程。从 JobManager 处接收需要部署的 Task，部署启动后，与自己的上游建立 Netty 连接，接收数据并处理。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;可以看到 Flink 的任务调度是多线程模型，并且不同 Job/Task 混合在一个 TaskManager 进程中。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tech-proxy.bytedance.net/tos/images/1611997841354_489da73b2a36833bd8976a7861518bc6&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;jobmanager&quot;&gt;JobManager&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;JobManager&lt;/em&gt; 具有许多与协调 Flink 应用程序的分布式执行有关的职责：它决定何时调度下一个 task（或一组 task）、对完成的 task 或执行失败做出反应、协调 checkpoint、并且协调从失败中恢复等等。这个进程由三个不同的组件组成：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;ResourceManager：&lt;/strong&gt; &lt;em&gt;ResourceManager&lt;/em&gt; 负责 Flink 集群中的资源提供、回收、分配 - 它管理 &lt;strong&gt;task slots&lt;/strong&gt;，这是 Flink 集群中资源调度的单位。Flink 为不同的环境和资源提供者（例如 YARN、Mesos、Kubernetes 和 standalone 部署）实现了对应的 ResourceManager。在 standalone 设置中，ResourceManager 只能分配可用 TaskManager 的 slots，而不能自行启动新的 TaskManager。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Dispatcher：&lt;/strong&gt; &lt;em&gt;Dispatcher&lt;/em&gt; 提供了一个 REST 接口，用来提交 Flink 应用程序执行，并为每个提交的作业启动一个新的 JobMaster。它还运行 Flink WebUI 用来提供作业执行信息。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;JobMaster：&lt;/strong&gt; &lt;em&gt;JobMaster&lt;/em&gt; 负责管理单个&lt;a href=&quot;https://ci.apache.org/projects/flink/flink-docs-release-1.12/zh/concepts/glossary.html#logical-graph&quot;&gt;JobGraph&lt;/a&gt;的执行。Flink 集群中可以同时运行多个作业，每个作业都有自己的 JobMaster。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;始终至少有一个 JobManager。高可用（HA）设置中可能有多个 JobManager，其中一个始终是 &lt;em&gt;leader&lt;/em&gt;，其他的则是 &lt;em&gt;standby&lt;/em&gt;（请参考 &lt;a href=&quot;https://ci.apache.org/projects/flink/flink-docs-release-1.12/zh/deployment/ha/&quot;&gt;高可用（HA）&lt;/a&gt;）。&lt;/p&gt;

&lt;h2 id=&quot;taskmanagers&quot;&gt;TaskManagers&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;TaskManager&lt;/em&gt;（也称为 &lt;em&gt;worker&lt;/em&gt;）执行作业流的 task，并且缓存和交换数据流。&lt;/p&gt;

&lt;p&gt;必须始终至少有一个 TaskManager。在 TaskManager 中资源调度的最小单位是 task &lt;em&gt;slot&lt;/em&gt;。TaskManager 中 task slot 的数量表示并发处理 task 的数量。请注意一个 task slot 中可以执行多个算子。&lt;/p&gt;

&lt;h2 id=&quot;task&quot;&gt;Task&lt;/h2&gt;

&lt;p&gt;对于分布式执行，Flink 将算子的 subtasks &lt;em&gt;链接&lt;/em&gt;成 &lt;em&gt;tasks&lt;/em&gt;。每个 task 由一个线程执行。将算子链接成 task 是个有用的优化：它减少线程间切换、缓冲的开销，并且减少延迟的同时增加整体吞吐量。&lt;/p&gt;

&lt;p&gt;下图中样例数据流用 5 个 subtask 执行，因此有 5 个并行线程。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tech-proxy.bytedance.net/tos/images/1611997841313_5bdb3ff13b15788f9b1333e26dccd95d&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;task-slots-和资源&quot;&gt;Task Slots 和资源&lt;/h2&gt;

&lt;p&gt;每个 worker（TaskManager）都是一个 &lt;em&gt;JVM 进程&lt;/em&gt;，可以在单独的线程中执行一个或多个 subtask。为了控制一个 TaskManager 中接受多少个 task，就有了所谓的 &lt;strong&gt;task slots&lt;/strong&gt;（至少一个）。&lt;/p&gt;

&lt;p&gt;每个 &lt;em&gt;task slot&lt;/em&gt; 代表 TaskManager 中资源的固定子集。例如，具有 3 个 slot 的 TaskManager，会将其托管内存 1/3 用于每个 slot。分配资源意味着 subtask 不会与其他作业的 subtask 竞争托管内存，而是具有一定数量的保留托管内存。&lt;strong&gt;注意此处没有 CPU 隔离；当前 slot 仅分离 task 的托管内存。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;通过调整 task slot 的数量，用户可以定义 subtask 如何互相隔离。每个 TaskManager 有一个 slot，这意味着每个 task 组都在单独的 JVM 中运行（例如，可以在单独的容器中启动）。具有多个 slot 意味着更多 subtask 共享同一 JVM。同一 JVM 中的 task 共享 TCP 连接（通过多路复用）和心跳信息。它们还可以共享数据集和数据结构，从而减少了每个 task 的开销。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tech-proxy.bytedance.net/tos/images/1611997841431_1e0decbca08860a5600bf5a1a81d1aa3&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;默认情况下，Flink 允许 subtask 共享 slot，即便它们是不同的 task 的 subtask，只要是来自于同一作业即可。结果就是一个 slot 可以持有整个作业管道。允许 &lt;em&gt;slot 共享&lt;/em&gt;有两个主要优点：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Flink 集群所需的 task slot 和作业中使用的最大并行度恰好一样。无需计算程序总共包含多少个 task（具有不同并行度）。&lt;/li&gt;
  &lt;li&gt;容易获得更好的资源利用。如果没有 slot 共享，非密集 subtask（&lt;em&gt;source/map()&lt;/em&gt; ）将阻塞和密集型 subtask（&lt;em&gt;window&lt;/em&gt;） 一样多的资源。通过 slot 共享，我们示例中的基本并行度从 2 增加到 6，可以充分利用分配的资源，同时确保繁重的 subtask 在 TaskManager 之间公平分配。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;https://tech-proxy.bytedance.net/tos/images/1611997841462_d5cc711a8ec9e0db23d206bf9abdcff8&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h1 id=&quot;概念&quot;&gt;概念&lt;/h1&gt;

&lt;h2 id=&quot;dataflows&quot;&gt;Dataflows&lt;/h2&gt;

&lt;p&gt;Flink 程序本质上是分布式并行程序。在程序执行期间，一个流有一个或多个&lt;strong&gt;流分区&lt;/strong&gt;（Stream Partition），每个算子有一个或多个&lt;strong&gt;子任务&lt;/strong&gt;（Operator Subtask）。每个子任务彼此独立，并在不同的线程中运行，或在不同的计算机或容器中运行。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tech-proxy.bytedance.net/tos/images/1611997841374_3d3e23fad83b148e3ef4af64ca2318b4&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Flink 算子之间可以通过&lt;em&gt;一对一&lt;/em&gt;（one-to-one）模式或&lt;em&gt;重分配（redistributing）&lt;/em&gt; 模式传输数据：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;一对一&lt;/strong&gt;模式（例如上图中的 &lt;em&gt;Source&lt;/em&gt; 和 &lt;em&gt;map()&lt;/em&gt; 算子之间）可以保留元素的分区和顺序信息。这意味着 &lt;em&gt;map()&lt;/em&gt; 算子的 subtask [1] 输入的数据以及其顺序与 &lt;em&gt;Source&lt;/em&gt; 算子的 subtask [1] 输出的数据和顺序完全相同，即同一分区的数据只会进入到下游算子的同一分区。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;重分配&lt;/strong&gt;模式（例如上图中的 &lt;em&gt;map()&lt;/em&gt; 和 &lt;em&gt;keyBy/window&lt;/em&gt; 之间，以及 &lt;em&gt;keyBy/window&lt;/em&gt; 和 &lt;em&gt;Sink&lt;/em&gt; 之间）则会更改数据所在的流分区。该模式下，每个算子会将数据发送到多个目标子任务中，例如 &lt;em&gt;keyBy()&lt;/em&gt; （通过散列键重新分区）、&lt;em&gt;broadcast()&lt;/em&gt; （广播）或 &lt;em&gt;rebalance()&lt;/em&gt; （随机重新分发）。在重分配数据的过程中，元素只有在每对输出和输入子任务之间才能保留其之间的顺序信息（例如， &lt;em&gt;keyBy/window&lt;/em&gt; 的 subtask [2] 接收到的 &lt;em&gt;map()&lt;/em&gt; 的 subtask [1] 中的元素都是有序的）。因此，上图所示的 &lt;em&gt;keyBy/window&lt;/em&gt; 和 &lt;em&gt;Sink&lt;/em&gt; 算子之间数据的重新分发时，不同键（key）的聚合结果到达 Sink 的顺序是不确定的。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;时间&quot;&gt;时间&lt;/h2&gt;

&lt;blockquote&gt;
  &lt;p&gt;详情可见👉 &lt;a href=&quot;https://ci.apache.org/projects/flink/flink-docs-release-1.12/concepts/timely-stream-processing.html&quot;&gt;官方文档&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;对于大多数流应用而言，能够使用同一份代码处理实时数据及重新处理历史数据，产生确定并且一致的结果非常有价值。&lt;/p&gt;

&lt;p&gt;在处理流式数据时，我们通常更需要关注事件本身发生的顺序而不是事件被传输以及处理的顺序，因为这能够帮助我们推理出一组事件（事件集合）是何时发生以及结束的。例如电子商务交易或金融交易中涉及到的事件集合。&lt;/p&gt;

&lt;p&gt;为了满足上述这类的实时流处理场景，我们通常会使用记录在数据流中的事件时间的时间戳，而不是处理数据的机器时钟的时间戳。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;事件时间（Event-Time）：设备时钟，记录事件发生的时间&lt;/li&gt;
  &lt;li&gt;摄入时间（Ingestion-Time）：设备时钟，记录事件发送到服务器的时间&lt;/li&gt;
  &lt;li&gt;处理时间（Processing-Time）：服务器时钟，记录服务器处理事件时的时间&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;使用事件时间&quot;&gt;使用事件时间&lt;/h3&gt;

&lt;p&gt;事件时间的强大之处在于，无论是在处理实时的数据还是重新处理历史的数据，基于事件时间创建的流计算应用都能保证结果是一样的，即幂等性。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;一个使用处理时间引发的问题：&lt;/strong&gt; 如果根据处理时间来衡量请求频率，看起来重启后出现了请求高峰，但是实际上请求频率是稳定的。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tech-proxy.bytedance.net/tos/images/1611997841475_aca2aaaea24a40c7f2235fcb53dd550b&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;流状态&quot;&gt;流状态&lt;/h2&gt;

&lt;blockquote&gt;
  &lt;p&gt;详情可见👉 &lt;a href=&quot;https://ci.apache.org/projects/flink/flink-docs-release-1.12/concepts/stateful-stream-processing.html&quot;&gt;官方文档&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Flink 中的算子可以是有状态的。这意味着如何处理一个事件可能取决于该事件之前所有事件数据的累积结果。Flink 中的状态不仅可以用于简单的场景（例如统计仪表板上每分钟显示的数据），也可以用于复杂的场景（例如训练作弊检测模型）。&lt;/p&gt;

&lt;p&gt;Flink 应用程序可以在分布式群集上并行运行，其中每个算子的各个并行实例会在单独的线程中独立运行，并且通常情况下是会在不同的机器上运行。&lt;/p&gt;

&lt;p&gt;状态算子的并行实例组在存储其对应状态时通常是按照键（key）进行分片存储的。每个并行实例算子负责处理一组特定键的事件数据，并且这组键对应的状态会保存在本地。&lt;/p&gt;

&lt;p&gt;如下图的 Flink 作业，其前三个算子的并行度为 2，最后一个 sink 算子的并行度为 1，其中第三个算子是有状态的，并且你可以看到第二个算子和第三个算子之间是全互联的（fully-connected），它们之间通过网络进行数据分发。通常情况下，实现这种类型的 Flink 程序是为了通过某些键对数据流进行分区，以便将需要一起处理的事件进行汇合，然后做统一计算处理。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tech-proxy.bytedance.net/tos/images/1611997841508_f8e34c09c2b03a0a26da3f1f3f0e7785&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Flink 应用程序的状态访问都在本地进行，因为这有助于其提高吞吐量和降低延迟。通常情况下 Flink 应用程序都是将状态存储在 JVM 堆上，但如果状态太大，我们也可以选择将其以结构化数据格式存储在高速磁盘中。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tech-proxy.bytedance.net/tos/images/1611997841451_6e8e40cc70c35a17443efd9f868d468d&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;窗口&quot;&gt;窗口&lt;/h2&gt;

&lt;blockquote&gt;
  &lt;p&gt;详情可见👉 &lt;a href=&quot;https://ci.apache.org/projects/flink/flink-docs-release-1.12/zh/dev/stream/operators/windows.html&quot;&gt;官方文档&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;窗口是处理无限流的核心，因为窗口在无限流上定义了一个有限的元素集合，在这些有限集上执行运算。下面简单介绍 Flink 中涉及的窗口类型及其特性。&lt;/p&gt;

&lt;h3 id=&quot;tumbling-windows&quot;&gt;Tumbling Windows&lt;/h3&gt;

&lt;p&gt;滚动窗口将每个元素分配到一个指定大小的窗口中。通常滚动窗口有一个固定的大小，并且不会出现重叠。例如：如果指定了一个5分钟大小的滚动窗口，无限流的数据会根据时间划分成[0:00 - 0:05)、[0:05, 0:10)、[0:10, 0:15)等窗口，如下图所示。&lt;/p&gt;

&lt;p&gt;默认的话窗口会根据时间对齐，即如果是一小时的滚动窗口，则划分后的窗口为 1:00:00.000 - 1:59:59.999, 2:00:00.000 - 2:59:59.999 等等。Flink 提供了 offset 参数，如果指定了 offset，例如 15min，则将得到1:15:00.000 - 2:14:59.999, 2:15:00.000 - 3:14:59.999 的窗口集合。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tech-proxy.bytedance.net/tos/images/1611997841518_d2e7c4cab4df2002a5d0710ab3c7ce54&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;sliding-windows&quot;&gt;Sliding Windows&lt;/h3&gt;

&lt;p&gt;滑动窗口不同于滚动窗口，滑动窗口的窗口可以重叠。&lt;/p&gt;

&lt;p&gt;滑动窗口有两个参数：slide 和 size。slide 为每次滑动的步长，size 为窗口的大小。&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;slide &amp;lt; size，则窗口会重叠，每个元素会被分配到多个窗口。&lt;/li&gt;
  &lt;li&gt;slide = size，则等同于滚动窗口。&lt;/li&gt;
  &lt;li&gt;slide &amp;gt; size，则为跳跃窗口，窗口之间不重叠且有间隙。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;通常情况下大部分元素符合第一种情形，窗口是重叠的。因此，滑动窗口在计算移动平均数（moving averages）时很实用。例如，计算过去 5 分钟数据的平均值，每 10 秒钟更新一次，可以设置 slide 为 10秒，size 为 5 分钟。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tech-proxy.bytedance.net/tos/images/1611997841500_461707ba22e13f568d7376b167d361ea&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;session-windows&quot;&gt;Session Windows&lt;/h3&gt;

&lt;p&gt;会话窗口根据 session 来对元素进行分组。会话窗口与滚动窗口和滑动窗口相比，没有窗口重叠，没有固定窗口大小。相反，当它在一个固定的时间周期内不再收到元素，即 session 断开时，这个窗口就会关闭。例如，对于用户的鼠标点击流，我们可以根据用户进行区分（group by user_id），分析每个用户每天高频使用鼠标的时间段。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tech-proxy.bytedance.net/tos/images/1611997841600_dfe8767245929bc43916e3e14bc2e82c&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;global-windows&quot;&gt;Global Windows&lt;/h3&gt;

&lt;p&gt;全局窗口将所有元素汇集到一个集合。这种窗口通常只与自定义 trigger 配合使用。否则，由于窗口永远不会结束，因此不会触发任何窗口计算。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tech-proxy.bytedance.net/tos/images/1611997841393_525324dfadefb2d964a0b1b56b03a52b&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;窗口计算的一些注意点&quot;&gt;窗口计算的一些注意点&lt;/h3&gt;

&lt;p&gt;窗口可以被指定为一个非常长的时间区间，例如天、周、月。不过这意味着维护大量的流状态，通常来说有以下原则来帮助评估其占用的存储空间：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Flink 会对每个窗口的每个元素创建一个副本。对于滚动窗口，每个元素只会唯一创建一个副本（因为每个元素唯一属于一个窗口）。然而，对于滑动窗口来说，每个元素会窗口多个副本。因此，一个步长秒级的天级滑动窗口不是一个好主意。&lt;/li&gt;
  &lt;li&gt;ReduceFunction、AggregateFunction 都能够大大减少存储，因为每个窗口只会存储一个计算后的值，而非每个元素一个值。&lt;/li&gt;
  &lt;li&gt;使用 Evictor 对聚合计算进行预处理，淘汰不必要的元素。&lt;/li&gt;
&lt;/ol&gt;

&lt;h1 id=&quot;应用&quot;&gt;应用&lt;/h1&gt;

&lt;p&gt;Apache Flink 功能强大，支持开发和运行多种不同种类的应用程序。它的主要特性包括：批流一体化、精密的状态管理、事件时间支持以及精确一次的状态一致性保障等。Flink 不仅可以运行在包括 YARN、 Mesos、Kubernetes 在内的多种资源管理框架上，还支持在裸机集群上独立部署。在启用高可用选项的情况下，它不存在单点失效问题。事实证明，Flink 已经可以扩展到数千核心，其状态可以达到 TB 级别，且仍能保持高吞吐、低延迟的特性。世界各地有很多要求严苛的流处理应用都运行在 Flink 之上。&lt;/p&gt;

&lt;h2 id=&quot;事件驱动型应用&quot;&gt;事件驱动型应用&lt;/h2&gt;

&lt;p&gt;事件驱动型应用是一类具有状态的应用，它从一个或多个事件流提取数据，并根据到来的事件触发计算、状态更新或其他外部动作。&lt;/p&gt;

&lt;p&gt;相反，事件驱动型应用是基于状态化流处理来完成。在该设计中，数据和计算不会分离（传统架构中，需要实时请求数据库获取上下文数据），应用只需访问本地（内存或磁盘）即可获取数据。系统容错性的实现依赖于定期向远程持久化存储写入 checkpoint。下图描述了传统应用和事件驱动型应用架构的区别。&lt;/p&gt;

&lt;p&gt;例如：对于用户发文流，应用需要检查某篇文章是否涉嫌抄袭，前面的 pipeline 已经通过 NLP 提取相应的 embedding 向量。那么本应用只需要去查询当前文章的 embedding 是否与 Flink 本地维护的其他元素的 embedding 状态值相近即可。可以理解为，Flink 用内存+磁盘换取数据库调用，并且其状态的维护是精确且可靠的。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tech-proxy.bytedance.net/tos/images/1611997841530_4f797732acdb5cecf63f15266f48706d&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;数据分析应用&quot;&gt;数据分析应用&lt;/h2&gt;

&lt;p&gt;数据分析任务需要从原始数据中提取有价值的信息和指标。传统的分析方式通常是利用批查询，或将事件记录下来并基于此有限数据集构建应用来完成。为了得到最新数据的分析结果，必须先将它们加入分析数据集并重新执行查询或运行应用程序，随后将结果写入存储系统或生成报告。&lt;/p&gt;

&lt;p&gt;借助一些先进的流处理引擎，还可以实时地进行数据分析。和传统模式下读取有限数据集不同，流式查询或应用会接入实时事件流（例如 Kafka），并随着事件消费持续产生和更新结果。这些结果数据可能会写入外部数据库系统或以内部状态的形式维护。指标展示看板可以从外部数据库读取数据或直接查询应用的内部状态。&lt;/p&gt;

&lt;p&gt;如下图所示，Apache Flink 同时支持流式及批量分析应用。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tech-proxy.bytedance.net/tos/images/1611997841380_768b081f78a78a745240ce4dfeca448b&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;通常来说，流式分析相比于批式分析有几个优势：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;流式分析省掉了周期性的数据导入和查询过程，因此从事件中获取指标的延迟更低&lt;/li&gt;
  &lt;li&gt;批式查询必须处理那些由定期导入和获取数据导致的边界问题，而流式查询则无须考虑该问题。例如前面提到的滚动窗口统计，批式分析需要精确地周期性调度，而调度本身引入了调度时间以及应用冷启动时间，会有一定误差。&lt;/li&gt;
  &lt;li&gt;而容错性方面，Flink 提供了故障恢复机制，而批式计算通常需要由多个独立部件组成，需要周期性地调度提取数据和执行查询、分析。一旦某个组件出错，则会影响后续步骤。&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;数据管道应用&quot;&gt;数据管道应用&lt;/h2&gt;

&lt;p&gt;提取-转换-加载（ETL）是一种在存储系统之间进行数据转换和迁移的常用方法。ETL 作业通常会周期性地触发，将数据从事务型数据库拷贝到分析型数据库或数据仓库。&lt;/p&gt;

&lt;p&gt;数据管道和 ETL 作业的用途相似，都可以转换、丰富数据，并将其从某个存储系统移动到另一个。但数据管道是以持续流模式运行，而非周期性触发。因此它支持从一个不断生成数据的源头读取记录，并将它们以低延迟移动到终点。例如：数据管道可以用来监控文件系统目录中的新文件，并将其数据写入事件日志；另一个应用可能会将事件流物化到数据库或增量构建和优化查询索引。&lt;/p&gt;

&lt;p&gt;下图描述了周期性 ETL 作业和持续数据管道的差异。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tech-proxy.bytedance.net/tos/images/1611997841436_2d710bc4ed7157ec72e23a70a7bb8a2c&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Flink 为多种数据存储系统（如：Kafka、Kinesis、Elasticsearch、JDBC数据库系统等）内置了连接器。和周期性 ETL 作业相比，数据管道可以明显降低将端到端数据传输的延迟。此外，由于它能够持续消费和发送数据，因此用途更广，支持用例更多。&lt;/p&gt;

&lt;p&gt;例如，对于用户发文流，经过一系列前置 pipeline 处理，提取了关键词等信息，Flink 作业将数据转化为所需格式 sink 到数据库，并 sink 到另一个事件日志流进行一系列后处理，如送审核、写索引，等等。&lt;/p&gt;

&lt;h1 id=&quot;容错机制&quot;&gt;容错机制&lt;/h1&gt;

&lt;blockquote&gt;
  &lt;p&gt;后续将深入解析 Flink 容错机制的实现，也可以参考👉 &lt;a href=&quot;https://flink.apache.org/features/2018/03/01/end-to-end-exactly-once-apache-flink.html&quot;&gt;官方文档&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;通过状态快照和流重放两种方式的组合，Flink 能够提供可容错的，精确一次计算的语义。这些状态快照在执行时会获取并存储分布式 pipeline 中整体的状态，它会将数据源中消费数据的偏移量记录下来，并将整个 job graph 中算子获取到该数据（记录的偏移量对应的数据）时的状态记录并存储下来。当发生故障时，Flink 作业会恢复上次存储的状态，重置数据源从状态中记录的上次消费的偏移量开始重新进行消费处理。而且状态快照在执行时会异步获取状态并存储，并不会阻塞正在进行的数据处理逻辑。&lt;/p&gt;

&lt;h1 id=&quot;reference&quot;&gt;Reference&lt;/h1&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://ci.apache.org/projects/flink/flink-docs-release-1.12/learn-flink/&quot;&gt;Flink 概览&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://ci.apache.org/projects/flink/flink-docs-release-1.12/learn-flink/fault_tolerance.html&quot;&gt;Flink 容错机制&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://ci.apache.org/projects/flink/flink-docs-release-1.12/concepts/flink-architecture.html&quot;&gt;Flink 架构&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;https://ververica.cn/developers-resources&lt;/li&gt;
  &lt;li&gt;https://wuchong.me/categories/Flink/&lt;/li&gt;
&lt;/ul&gt;
</description>
			<pubDate>Sat, 30 Jan 2021 00:00:00 +0000</pubDate>
			<link>https://sarkerson.github.io/2021/01/30/Flink-%E6%A6%82%E8%A7%88/</link>
			<guid isPermaLink="true">https://sarkerson.github.io/2021/01/30/Flink-%E6%A6%82%E8%A7%88/</guid>
        
			<category>computation</category>
        
        
		</item>
    
		<item>
			<title>数据库：B+Tree 索引的原理与应用</title>
			<description>&lt;h1 id=&quot;数据库btree-索引的原理与应用&quot;&gt;数据库：B+Tree 索引的原理与应用&lt;/h1&gt;

&lt;p&gt;虽然写 BTree，但其实本章主要讨论其中一个优化的子集，即广泛使用的 B+Tree。&lt;/p&gt;

&lt;h2 id=&quot;1-btree&quot;&gt;1. BTree&lt;/h2&gt;

&lt;h3 id=&quot;11-btree-结构&quot;&gt;1.1. BTree 结构&lt;/h3&gt;

&lt;p&gt;&lt;img src=&quot;http://blog.codinglabs.org/uploads/pictures/theory-of-mysql-index/4.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;12-btree-索引特性&quot;&gt;1.2. BTree 索引特性&lt;/h3&gt;

&lt;p&gt;例如存在如下的表&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ID | first_name | last_name    | Class      | Position |  ssn | 
---------------------------------------------------------------
 1 | Teemo      | Shroomer     | Specialist | Top      | 2345 |
 2 | Cecil      | Heimerdinger | Specialist | Mid      | 5461 |
 3 | Annie      | Hastur       | Mage       | Mid      | 8784 |
 4 | Fiora      | Laurent      | Slayer     | Top      | 7867 |
 5 | Garen      | Crownguard   | Fighter    | Top      | 4579 |
 6 | Malcolm    | Graves       | Specialist | ADC      | 4578 |
 7 | Irelia     | Lito         | Figher     | Top      | 5689 |
 8 | Janna      | Windforce    | Controller | Support  | 4580 |
 9 | Jarvan     | Lightshield  | Figher     | Top      | 4579 |
10 | Katarina   | DuCouteau    | Assassin   | Mid      | 5608 |
11 | Kayle      | Hex          | Specialist | Top      | 4794 |
12 | Emilia     | LeBlanc      | Mage       | Mid      | 3468 |
13 | Lee        | Sin          | Fighter    | Jungle   | 8085 |
14 | Lux        | Crownguard   | Mage       | Mid      | 4567 |
15 | Sarah      | Fortune      | Marksman   | ADC      | 6560 |
16 | Morgana    | Hex          | Controller | Support  | 3457 |
17 | Orianna    | Reveck       | Mage       | Mid      | 9282 |
18 | Sona       | Buvelle      | Controller | Support  | 4722 |
19 | Jericho    | Swain        | Mage       | Mid      | 5489 |
20 | Shauna     | Vayne        | Marksman   | ADC      | 2352 |
21 | Xin        | Zhao         | Fighter    | Jungle   | 6902 |
22 | Yorick     | Mori         | Tank       | Top      | 4840 |
23 | Wu         | Kong         | Fighter    | Jungle   | 4933 |
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;创建 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;users.first_name&lt;/code&gt; 的索引，B+Tree 叶子节点组织如下：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;first_name  Primary Key
-----------------------
Annie    -&amp;gt; 3
Cecil    -&amp;gt; 2
Emilia   -&amp;gt; 12
Fiora    -&amp;gt; 4
Garen    -&amp;gt; 5
Irelia   -&amp;gt; 7
Janna    -&amp;gt; 8
Jarvan   -&amp;gt; 9
Jericho  -&amp;gt; 19
Katarina -&amp;gt; 10
Kayle    -&amp;gt; 11
Lee      -&amp;gt; 13
Lux      -&amp;gt; 14
Malcolm  -&amp;gt; 6
Morgana  -&amp;gt; 16
Orianna  -&amp;gt; 17
Sarah    -&amp;gt; 15
Shauna   -&amp;gt; 20
Sona     -&amp;gt; 18
Teemo    -&amp;gt; 1
Wu       -&amp;gt; 23
Xin      -&amp;gt; 21
Yorick   -&amp;gt; 22
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;那么复合索引呢？&lt;/p&gt;

&lt;p&gt;创建 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(class, position)&lt;/code&gt; 的复合索引，B+Tree 叶子节点组织如下：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;class-position       Primary Key
--------------------------------
AssassinMid       -&amp;gt; 10
ControllerSupport -&amp;gt; 16
ControllerSupport -&amp;gt; 18
ControllerSupport -&amp;gt; 8
FigherTop         -&amp;gt; 7
FigherTop         -&amp;gt; 9
FighterJungle     -&amp;gt; 13
FighterJungle     -&amp;gt; 21
FighterJungle     -&amp;gt; 23
FighterTop        -&amp;gt; 5
MageMid           -&amp;gt; 12
MageMid           -&amp;gt; 14
MageMid           -&amp;gt; 17
MageMid           -&amp;gt; 19
MageMid           -&amp;gt; 3
MarksmanADC       -&amp;gt; 15
MarksmanADC       -&amp;gt; 20
SlayerTop         -&amp;gt; 4
SpecialistADC     -&amp;gt; 6
SpecialistMid     -&amp;gt; 2
SpecialistTop     -&amp;gt; 1
SpecialistTop     -&amp;gt; 11
TankTop           -&amp;gt; 22
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;可以看到，叶子节点会首先根据 class 的字典序、其次根据 position 的字典序组织。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;最左前缀：(a, b, c) =&amp;gt; (a), (a, b), (a, b, c), (a, b, c[:k]), (a, b[:k]), (a[:k])&lt;/li&gt;
  &lt;li&gt;第一原则是，如果通过调整顺序，可以少维护一个索引，那么这个顺序往往就是需要优先考虑采用的。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;13-与-btree-的区别&quot;&gt;1.3. 与 BTree 的区别&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;B + 树查询时间复杂度固定是 logn，B 树查询复杂度最好是 O (1)。&lt;/li&gt;
  &lt;li&gt;B 树每个节点即保存数据又保存索引，因此每个节点的字节点指针数量更少，即扇出更少，高度通常比 B+ 树高&lt;/li&gt;
  &lt;li&gt;B + 树相邻接点的指针可以大大增加区间访问性，范围查询效率更高&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;2-btree-in-storage-engine&quot;&gt;2. BTree In Storage Engine&lt;/h2&gt;

&lt;p&gt;存储引擎要做的事情无外乎是将磁盘上的数据读到内存并返回给应用，或者将应用修改的数据由内存写到磁盘上。如何设计一种高效的数据结构和算法是所有存储引擎要考虑的根本问题，目前大多数流行的存储引擎是基于 BTree 或 LSM Tree 这两种数据结构来设计的。&lt;/p&gt;

&lt;h3 id=&quot;21-innodb&quot;&gt;2.1. InnoDB&lt;/h3&gt;

&lt;p&gt;每一个索引在 InnoDB 里面对应一棵 B+ 树。&lt;/p&gt;

&lt;h4 id=&quot;211-数据结构&quot;&gt;2.1.1. 数据结构&lt;/h4&gt;

&lt;h5 id=&quot;聚簇索引&quot;&gt;聚簇索引&lt;/h5&gt;

&lt;p&gt;InnoDB 要求表必须有主键&lt;/p&gt;

&lt;p&gt;InnoDB 的数据文件本身就是索引文件。&lt;/p&gt;

&lt;p&gt;表数据文件本身就是按 B+Tree 组织的一个索引结构，这棵树的叶节点 data 域保存了完整的数据记录。这个索引的 key 是数据表的主键，因此 InnoDB 表数据文件本身就是主索引。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://static001.geekbang.org/resource/image/dc/8d/dcda101051f28502bd5c4402b292e38d.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;为什么要有聚簇索引？磁盘上的组织是如何的？&lt;/p&gt;

&lt;h5 id=&quot;索引维护&quot;&gt;索引维护&lt;/h5&gt;

&lt;p&gt;B+ 树为了维护索引有序性，在插入新值的时候需要做必要的维护。以上面这个图为例，如果插入新的行 ID 值为 700，则只需要在 R5 的记录后面插入一个新记录。如果新插入的 ID 值为 400，就相对麻烦了，需要逻辑上挪动后面的数据，空出位置。页分裂、合并则需要更耗时的操作。&lt;/p&gt;

&lt;p&gt;自增主键的插入数据模式，正符合了我们前面提到的递增插入的场景。每次插入一条新记录，都是追加操作，都不涉及到挪动其他记录，也不会触发叶子节点的分裂。&lt;/p&gt;

&lt;h5 id=&quot;索引列的选择&quot;&gt;索引列的选择&lt;/h5&gt;

&lt;p&gt;基于上面的索引维护过程说明，我们来讨论一个案例：&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;你可能在一些建表规范里面见到过类似的描述，要求建表语句里一定要有自增主键。当然事无绝对，我们来分析一下哪些场景下应该使用自增主键，而哪些场景下不应该。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
  &lt;li&gt;时间：自增主键的插入数据模式，正符合了我们前面提到的递增插入的场景。每次插入一条新记录，都是追加操作，都不涉及到挪动其他记录，也不会触发叶子节点的分裂。&lt;/li&gt;
  &lt;li&gt;空间：由于每个非主键索引的叶子节点上都是主键的值，因此主键长度越小，普通索引的叶子节点就越小，普通索引占用的空间也就越小。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;所以，从性能和存储空间方面考量，自增主键往往是更合理的选择。&lt;/p&gt;

&lt;p&gt;比如，有些业务的场景需求是这样的（典型的 KV 场景）：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;只有一个索引；&lt;/li&gt;
  &lt;li&gt;该索引必须是唯一索引。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;这时候，尽量使用主键查询，可以避免每次查询需要搜索两棵树（回表）。&lt;/p&gt;

&lt;h4 id=&quot;212-覆盖索引&quot;&gt;2.1.2. 覆盖索引&lt;/h4&gt;

&lt;p&gt;select * from T where k between 3 and 5，需要执行几次树的搜索操作，会扫描多少行？&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;在 k 索引树上找到 k=3 的记录，取得 ID = 300；&lt;/li&gt;
  &lt;li&gt;再到 ID 索引树查到 ID=300 对应的 R3；&lt;/li&gt;
  &lt;li&gt;在 k 索引树取下一个值 k=5，取得 ID=500；&lt;/li&gt;
  &lt;li&gt;再回到 ID 索引树查到 ID=500 对应的 R4；&lt;/li&gt;
  &lt;li&gt;在 k 索引树取下一个值 k=6，不满足条件，循环结束。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;select ID from T where k between 3 and 5 呢？&lt;/p&gt;

&lt;p&gt;这时只需要查 ID 的值，而 ID 的值已经在 k 索引树上了，因此可以直接提供查询结果，不需要回表。也就是说，在这个查询里面，索引 k 已经 “覆盖了” 我们的查询需求，我们称为覆盖索引。&lt;/p&gt;

&lt;p&gt;字段顺序&lt;/p&gt;

&lt;p&gt;这个最左前缀可以是联合索引的最左 N 个字段，也可以是字符串索引的最左 M 个字符。&lt;/p&gt;

&lt;p&gt;因为可以支持最左前缀，所以当已经有了 (a,b) 这个联合索引后，一般就不需要单独在 a 上建立索引了。因此，第一原则是，如果通过调整顺序，可以少维护一个索引，那么这个顺序往往就是需要优先考虑采用的。&lt;/p&gt;

&lt;p&gt;索引越多，“维护成本” 越大&lt;/p&gt;

&lt;h4 id=&quot;213-索引下推&quot;&gt;2.1.3. 索引下推&lt;/h4&gt;

&lt;p&gt;对于联合索引（name, age）为例。如果现在有一个需求：检索出表中 “名字第一个字是张，而且年龄是 10 岁的所有男孩”。那么，SQL 语句是这么写的：&lt;/p&gt;

&lt;p&gt;select * from tuser where name like ‘ 张 %’ and age=10 and ismale=1;&lt;/p&gt;

&lt;p&gt;索引只能用 “张”&lt;/p&gt;

&lt;p&gt;然后？&lt;/p&gt;

&lt;p&gt;判断其他条件是否满足。&lt;/p&gt;

&lt;p&gt;在 MySQL 5.6 之前，只能从 ID3 开始一个个回表。到主键索引上找出数据行，再对比字段值。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://static001.geekbang.org/resource/image/b3/ac/b32aa8b1f75611e0759e52f5915539ac.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;而 MySQL 5.6 引入的索引下推优化（index condition pushdown)， 可以在索引遍历过程中，对索引中包含的字段先做判断，直接过滤掉不满足条件的记录，减少回表次数。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://static001.geekbang.org/resource/image/76/1b/76e385f3df5a694cc4238c7b65acfe1b.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;22-wiredtiger&quot;&gt;2.2. WiredTiger&lt;/h3&gt;

&lt;p&gt;其实 mongo 除了 wiredtiger 之外，还支持 mongrocks，不过 mongorocks 底层是使用基于 LSM-Tree 的 RocksDB。本文只讨论 BTree，所以 mongorocks 抛到一边。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tosv.byted.org/obj/tech/images/1583208024743_bc9727e772d9408269d13ffd1fb10eed.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;

&lt;h4 id=&quot;221-数据结构&quot;&gt;2.2.1. 数据结构&lt;/h4&gt;

&lt;p&gt;wiredtiger 维护索引文件跟数据文件。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://mmbiz.qpic.cn/mmbiz_png/G2NnvykaFvXMpHpUgsV8bic9YWHE0Ob1dzR7iawp6y8SSZaXF7YmqBEDVeBPjk4mUEJIIHxTdeKqcPHtLgicWDXxw/640?wx_fmt=png&amp;amp;tp=webp&amp;amp;wxfrom=5&amp;amp;wx_lazy=1&amp;amp;wx_co=1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;key 其实是一个 RecordID，每插入一个文档都会插入新的 key/value（RecordID =&amp;gt; RecordPosition）&lt;/p&gt;

&lt;p&gt;mongo 中并不会将 id 索引与行内容存放在一起（即没有聚簇索引的概念）。取而代之的，mongodb 将索引与数据分开存放，通过 RecordId 进行间接引用。&lt;/p&gt;

&lt;p&gt;举例一张包含两个索引（id 和 name）的表，在 wt 层将有三张表与其对应。&lt;/p&gt;

&lt;p&gt;如上图所示，集合包含 {_id: 1}, {name: 1} 2 个索引&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;用户插入文档时，底层引擎将文档内容存储，返回对应的位置信息，即 RecordId1&lt;/li&gt;
  &lt;li&gt;集合包含 2 个索引&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
  &lt;li&gt;插入 {_id: ObjectId1} ⇒ RecordId1 的索引&lt;/li&gt;
  &lt;li&gt;插入 {name: “rose”} ⇒ RecordId1 的索引&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;有了上述的数据，在根据_id 访问时文档时 （根据其他索引字段类似）&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;根据文档的 _id 字段从底层 KV 引擎读取 RecordId&lt;/li&gt;
  &lt;li&gt;根据 RecordId 从底层 KV 引擎读取文档内容&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;img src=&quot;http://p1.pstatp.com/origin/33a5c00008229e3945845&quot; alt=&quot;WechatIMG761&quot; /&gt;&lt;/p&gt;

&lt;h4 id=&quot;222-索引实现&quot;&gt;2.2.2. 索引实现&lt;/h4&gt;

&lt;p&gt;其实所有 BTree 索引的实现都是大同小异&lt;/p&gt;

&lt;p&gt;在 MongoDB 中，没有 clustered index，因此，Collection 初始的物理存储跟 doc 插入的顺序有关，MongoDB 按照 doc 插入的顺序，依次将 doc 存储在 disk 上，插入顺序上相邻的 doc 在 disk 的物理位置上也是相邻的；对 doc 的修改可能对 collection 的物理存储发生变化，如果 doc 的修改不会导致 doc 的 size 增加，那么 doc 会继续存储在原来的存储空间中，而不会对 collection 的物理存储有影响，一旦修改操作导致 doc 的 size 增加，导致 doc 发生移动，那么 collection 的物理存储就会发生变化。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;http://p3.pstatp.com/origin/33a35000195dcec379fc8&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;如果插入的集合包含索引（MongoDB 的集合默认会有_id 索引），针对每项索引，还会往 WiredTiger 插入一个新的 key-value，key 是索引的字段内容，value 为插入文档时生成的 RecordId，这样就能快速根据索引找到文档的位置信息。&lt;/p&gt;

&lt;h5 id=&quot;objectid&quot;&gt;ObjectID&lt;/h5&gt;

&lt;p&gt;为什么 ObjectID 是递增的？&lt;/p&gt;

&lt;p&gt;上文说到插入顺序上相邻的 doc 在 disk 的物理位置上也是相邻的。因此默认的 ObjectID 上的索引中，叶子结点的数据也是相邻的（highly clustered）。&lt;/p&gt;

&lt;h5 id=&quot;单字段索引&quot;&gt;单字段索引&lt;/h5&gt;

&lt;p&gt;下述索引会对 age 进行升序排序&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;db.person.createIndex( {age: 1} ) 
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h5 id=&quot;复合索引&quot;&gt;复合索引&lt;/h5&gt;

&lt;p&gt;这个索引要先按 age 字段升序、age 相同的按 name 字段降序&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;db.person.createIndex( {age: 1, name: 1} ) 
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;那么下面语句呢？&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;db.person.createIndex( {age: 1, name: -1} ) 
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;MongoDB 针对每个索引，会有一个位图来描述索引各个字段的排序方向，如果方向是逆序（如 b: -1），会把 key 的内容里将 b 字段对应的 bit 全部取反。&lt;/p&gt;

&lt;p&gt;InnoDB 的回表查询也适用于此，如果复合索引能够覆盖查询，则不用回表。&lt;/p&gt;

&lt;h5 id=&quot;多-key-索引&quot;&gt;多 key 索引&lt;/h5&gt;

&lt;p&gt;当索引的字段为数组时，创建出的索引称为多 key 索引，多 key 索引会为数组的每个元素建立一条索引&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;{&quot;name&quot; : &quot;jack&quot;, &quot;age&quot; : 19, habbit: [&quot;football, runnning&quot;]}
db.person.createIndex( {habbit: 1} )  // 创建多key索引
db.person.find( {habbit: &quot;football&quot;} )
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;底层会创建类似 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;person.habbit_football =&amp;gt; RecordID1&lt;/code&gt; 的索引 key/value&lt;/p&gt;

&lt;h5 id=&quot;全文索引&quot;&gt;全文索引&lt;/h5&gt;

&lt;p&gt;待补充&lt;/p&gt;

&lt;h2 id=&quot;3-application&quot;&gt;3. Application&lt;/h2&gt;

&lt;h3 id=&quot;31-order-by--sort&quot;&gt;3.1. order by / sort&lt;/h3&gt;

&lt;p&gt;MySQL 跟 MongoDB 的实现类似，这里用 MySQL 来举例。&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;CREATE TABLE `t` (
  `id` int(11) NOT NULL,
  `city` varchar(16) NOT NULL,
  `name` varchar(16) NOT NULL,
  `age` int(11) NOT NULL,
  `addr` varchar(128) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `city` (`city`)
) ENGINE=InnoDB;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;查询城市是 “杭州” 的所有人名字，并且按照姓名排序返回前 1000 个人的姓名、年龄。我们执行&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;select city,name,age from t where city=&apos;杭州&apos; order by name limit 1000  ;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;通常情况下，这个语句执行流程如下所示 ：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;初始化 sort_buffer，确定放入 name、city、age 这三个字段；&lt;/li&gt;
  &lt;li&gt;从索引 city 找到第一个满足 city=’ 杭州’条件的主键 id，也就是图中的 ID_X；&lt;/li&gt;
  &lt;li&gt;到主键 id 索引取出整行，取 name、city、age 三个字段的值，存入 sort_buffer 中；&lt;/li&gt;
  &lt;li&gt;从索引 city 取下一个记录的主键 id；&lt;/li&gt;
  &lt;li&gt;重复步骤 3、4 直到 city 的值不满足查询条件为止，对应的主键 id 也就是图中的 ID_Y；&lt;/li&gt;
  &lt;li&gt;对 sort_buffer 中的数据按照字段 name 做快速排序；&lt;/li&gt;
  &lt;li&gt;按照排序结果取前 1000 行返回给客户端。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;img src=&quot;https://static001.geekbang.org/resource/image/6c/72/6c821828cddf46670f9d56e126e3e772.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;如果需要取的字段过多，超多 sort_buffer 的话，则会走到 rowid 排序（多回表一次）&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://static001.geekbang.org/resource/image/dc/6d/dc92b67721171206a302eb679c83e86d.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;需要排序的原因是，原来的数据就是无序的。要解决这个问题，则要利用好索引结构。&lt;/p&gt;

&lt;p&gt;例如，对上述需求，我们新建索引：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;alter table t add index city_user(city, name);
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;则对于每个 city，name 都是有序的：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://static001.geekbang.org/resource/image/f9/bf/f980201372b676893647fb17fac4e2bf.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;当然，这个索引对于上述需求还需要回表去取 age，因此建立 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(city, name, age)&lt;/code&gt; 的覆盖索引可以避免回表。&lt;/p&gt;

&lt;p&gt;如果要取多个城市的呢？&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;select * from t where city in (“杭州”,&quot;苏州&quot;) order by name limit 100;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;如果这个需求需要分页呢？&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;select city,name,age from t where city=&apos;杭州&apos; order by name limit 10000,100;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;32-fuzzy-search&quot;&gt;3.2. fuzzy search&lt;/h3&gt;

&lt;p&gt;待补充&lt;/p&gt;

&lt;h2 id=&quot;4-reference&quot;&gt;4. Reference&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;https://mp.weixin.qq.com/s/Wuzh47jsBh5QonBrZxUnjg&lt;/li&gt;
  &lt;li&gt;https://mongoing.com/archives/5367&lt;/li&gt;
  &lt;li&gt;https://dzone.com/articles/learn-mongodb-with-me-part-3?utm_source=dzone&amp;amp;utm_medium=article&amp;amp;utm_campaign=mongodb-cluster&lt;/li&gt;
  &lt;li&gt;https://time.geekbang.org/column/article/73479&lt;/li&gt;
  &lt;li&gt;MySQL 索引实现 http://blog.codinglabs.org/articles/theory-of-mysql-index.html&lt;/li&gt;
  &lt;li&gt;innodb 索引：https://tech.bytedance.net/articles/12571#&lt;/li&gt;
  &lt;li&gt;MongoDB 索引原理 https://yq.aliyun.com/articles/386769&lt;/li&gt;
  &lt;li&gt;MongoDB 索引选择策略 https://dzone.com/articles/effective-mongodb-indexing-part-2&lt;/li&gt;
  &lt;li&gt;复合索引的形象描述 https://medium.com/@User3141592/single-vs-composite-indexes-in-relational-databases-58d0eb045cbe&lt;/li&gt;
&lt;/ul&gt;

</description>
			<pubDate>Wed, 14 Oct 2020 00:00:00 +0000</pubDate>
			<link>https://sarkerson.github.io/2020/10/14/BTree%E7%B4%A2%E5%BC%95%E5%8E%9F%E7%90%86%E5%8F%8A%E5%85%B6%E5%BA%94%E7%94%A8/</link>
			<guid isPermaLink="true">https://sarkerson.github.io/2020/10/14/BTree%E7%B4%A2%E5%BC%95%E5%8E%9F%E7%90%86%E5%8F%8A%E5%85%B6%E5%BA%94%E7%94%A8/</guid>
        
			<category>database</category>
        
        
		</item>
    
		<item>
			<title>数据库：B-Tree 与 LSM-Tree 的原理和取舍</title>
			<description>&lt;h1 id=&quot;数据库b-tree-与-lsm-tree-的原理和取舍&quot;&gt;数据库：B-Tree 与 LSM-Tree 的原理和取舍&lt;/h1&gt;

&lt;p&gt;本文介绍目前两种流行的索引结构，作为 DDIA 第三章前半部分的总结。&lt;/p&gt;

&lt;h2 id=&quot;1-b-tree-索引&quot;&gt;1. B-Tree 索引&lt;/h2&gt;

&lt;h3 id=&quot;11-简介&quot;&gt;1.1. 简介&lt;/h3&gt;

&lt;p&gt;B-tree 是目前最广泛使用的一种索引结构（B+ tree 是 B-tree 的一种优化版）。&lt;/p&gt;

&lt;p&gt;可以说，B-tree 是广义上的二分搜索树，只不过每个节点（node）可以有 B 个子节点。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tikv.org/img/deep-dive/b-tree.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;N 个数据通过 B-tree 组织，可以达到 O(logN) 复杂度的 query 速度。&lt;/p&gt;

&lt;h3 id=&quot;12-存储细节&quot;&gt;1.2. 存储细节&lt;/h3&gt;

&lt;p&gt;B-tree 将数据分成固定大小的页，页是内部读写的最小单元。这种设计接近底层硬件，因为磁盘也是按照固定大小的块排列。&lt;/p&gt;

&lt;p&gt;通常来说，存储引擎会使用 B-tree 组织数据进行存储（使用主键进行排序），即叶子节点的数据会按照顺序存储在磁盘上（叶子节点使用双向链表连接），这称为聚集索引（Clustered Index）。&lt;/p&gt;

&lt;p&gt;如果要更新 B-tree 现有的值，则首先搜索包含该键的叶子页，并将页刷到磁盘。&lt;/p&gt;

&lt;p&gt;如果需要插入新键，则需要找到该范围的页，将值写入页中。如果该页没有足够空间来容纳新键，则将其分裂成两个页，并且更新父页，若父页也满，则触发另一个分裂，以此类推。&lt;/p&gt;

&lt;p&gt;可以看到，如果每次插入新键都可能触发页分裂的话，则写入效率会非常低，因此通常来说，聚集索引会使用 AutoIncrement 的 ID 作为默认主键。&lt;/p&gt;

&lt;h3 id=&quot;13-奔溃恢复&quot;&gt;1.3. 奔溃恢复&lt;/h3&gt;

&lt;p&gt;由于 B-tree 的每次写入都涉及页的更新操作，并且有时候可能涉及多个页的写操作。如果完成部分写，然后数据库奔溃，则会破坏索引（例如某个页成了孤儿页，没有被引用）。&lt;/p&gt;

&lt;p&gt;为了使得数据库能够从奔溃中恢复，通常会实现一个预写日志（write-ahead log, WAL），每次修改 B-tree 之前，需要先更新 WAL，再修改 B-tree 的数据。&lt;/p&gt;

&lt;p&gt;当数据库奔溃后恢复时，使用 WAL 将数据库恢复到最近一致的状态。&lt;/p&gt;

&lt;h3 id=&quot;14-并发控制&quot;&gt;1.4. 并发控制&lt;/h3&gt;

&lt;p&gt;原地更新页的另外一个问题是，并发控制问题，通常会使用锁存器（key 级别的锁）来实现。&lt;/p&gt;

&lt;h3 id=&quot;15-性能优化&quot;&gt;1.5. 性能优化&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;一些数据库（如 LMDB）不实用覆盖页和维护 WAL，而是使用写时复制方案。在新位置写入修改的页，创建新版本的父页，指向该新位置。这种方法也有利于并发控制。&lt;/li&gt;
  &lt;li&gt;键压缩，即保存键的缩略信息，而不是完整的键，以节省页空间，特别是中间层节点，只需要提供 range 信息。&lt;/li&gt;
  &lt;li&gt;页的布局。理论上，页可以放在磁盘上任意位置，但是这样的话范围查询则需要涉及大量的随机I/O，是非常耗时的操作。因此，许多实现将子叶顺序存储，不过，当 B-tree 变大时，维护这个顺序会很困难。&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;2-lsm-tree-索引&quot;&gt;2. LSM-Tree 索引&lt;/h2&gt;

&lt;h3 id=&quot;21-简介&quot;&gt;2.1. 简介&lt;/h3&gt;

&lt;p&gt;日志结构合并树（Log-Strutured Merge Tree, LSM-Tree）也是利用键（key）进行排序存储的一种数据结构。&lt;/p&gt;

&lt;p&gt;数据存储在一系列的段文件中，称为排序字符串表（Sorted String Table, SSTable）。每个段文件表对应有一个内存哈希索引（可以是稀疏的），其中存储的值为 sstable 里面的文件偏移。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic3.zhimg.com/80/v2-43898b66ed75e8c7ff4e55d290111322_1440w.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;每次查询数据的时候，会从最新的 sstable 开始查找，直到找到所需的 key。&lt;/p&gt;

&lt;h3 id=&quot;22-原理&quot;&gt;2.2. 原理&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;当写入的时候，先往内存的平衡树数据结构写入&lt;/li&gt;
  &lt;li&gt;当上述内存表大小达到某个阈值的时候，则作为 SSTable 文件写入磁盘。新的 SSTable 成为数据库的最新部分。写入 SSTable 的时候，新的写入则添加到新的内存表实例中。&lt;/li&gt;
  &lt;li&gt;处理读请求时，首先尝试在最新的内存表中查找键，其次是最新的磁盘 SSTable，再次是次新的磁盘 SSTable，以此类推。&lt;/li&gt;
  &lt;li&gt;后台进程周期性地执行合并与压缩过程，以合并多个段文件，并丢弃无效的值（被修改或被删除）。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;当然，就像 B-tree 一样，所有存储引擎都需要解决奔溃恢复的问题，LSM-Tree 也可以使用 WAL 日志，每次写入内存表之前，先写入 WAL，。&lt;/p&gt;

&lt;h3 id=&quot;23-性能优化&quot;&gt;2.3. 性能优化&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;当查找某个不存在的键的时候，LSM-Tree 可能会很慢，因为需要从最新的 SSTable 回溯到最老的 SSTable。为此，可以使用布隆过滤器来解决。&lt;/li&gt;
  &lt;li&gt;压缩。压缩是 LSM-Tree 的核心问题。压缩算法包含大小分级（Size-tiered compaction strategy, STCS） 以及分层压缩（Level-based compaction strategy, LBCS）。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;img src=&quot;https://pic1.zhimg.com/80/v2-7a104c0203097084c18c55cf51dffb5e_1440w.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;大小分级与分层压缩的对比可见上图。&lt;/p&gt;

&lt;p&gt;大小分级中，每个 SSTable 虽然都是 sorted 的，但是当 level0 与 level1 合并 flush 到 level2 中的时候，[1,3,4,7], [2,5,6,8] 并不能组成一个 Run，因为无法直接使用二分搜索。例如，我需要查找 5，那么这两个 SSTable 都在查询范围内。&lt;/p&gt;

&lt;p&gt;而分层压缩则保证了这点，新的层中可能包含交叉的键，但是老的层都是不交叉的键。每次新层文件大小达到阈值的时候，会与较老的层中有交集的文件作合并操作，保证每一层都可以直接使用二分搜索进行查询。&lt;/p&gt;

&lt;h2 id=&quot;3-对比&quot;&gt;3. 对比&lt;/h2&gt;

&lt;h3 id=&quot;31-lsm-tree-的优点&quot;&gt;3.1. LSM-Tree 的优点&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;写入快。B 树索引必须至少两次写入每一段数据：一次写入预先写入日志，一次写入树页面本身（也许再次分页）。即使在该页面中只有几个字节发生了变化，也需要一次编写整个页面的开销。有些存储引擎甚至会覆盖同一个页面两次，以免在电源故障的情况下导致页面部分更新。&lt;/li&gt;
  &lt;li&gt;碎片少。LSM 树可以被压缩得更好，因此经常比 B 树在磁盘上产生更小的文件。 B 树存储引擎会由于页分裂而留下一些未使用的磁盘空间：当页分裂或某行不能放入现有页面时，页面中的某些空间无法被利用。由于 LSM 树不是面向页面的，并且定期重写 SSTables 以去除碎片，所以它们具有较低的存储开销，特别是当使用分层压缩时。&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;32-lsm-tree-的缺点&quot;&gt;3.2. LSM-Tree 的缺点&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;压缩阻塞。因为磁盘的带宽是固定的，因此 LSM-Tree 在压缩过程有时会干扰正在进行的读写操作。&lt;/li&gt;
  &lt;li&gt;压缩速率。当写入吞吐量很大的时候，很可能来不及压缩 SSTable 导致段文件增大到磁盘空间不足。因此需要额外的监控发现这些情况，以及配置合适的压缩间隔。&lt;/li&gt;
  &lt;li&gt;事务。B-tree 中，事务的隔离可以直接通过键范围的锁来实现。而 LSM-Tree 可能需要通过文件范围的锁来解决。&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;reference&quot;&gt;Reference&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;leveldb 的实现：https://github.com/google/leveldb/blob/master/doc/impl.md&lt;/li&gt;
  &lt;li&gt;数据结构性能对比：https://tikv.org/deep-dive/key-value-engine/b-tree-vs-lsm/&lt;/li&gt;
  &lt;li&gt;leveldb 架构：https://zhuanlan.zhihu.com/p/38810568&lt;/li&gt;
  &lt;li&gt;DDIA 笔记：https://juejin.im/post/6844904113122050055&lt;/li&gt;
&lt;/ul&gt;
</description>
			<pubDate>Sat, 08 Aug 2020 00:00:00 +0000</pubDate>
			<link>https://sarkerson.github.io/2020/08/08/%E7%B4%A2%E5%BC%95%E7%BB%93%E6%9E%84/</link>
			<guid isPermaLink="true">https://sarkerson.github.io/2020/08/08/%E7%B4%A2%E5%BC%95%E7%BB%93%E6%9E%84/</guid>
        
			<category>database</category>
        
        
		</item>
    
		<item>
			<title>缓存设计：Caffeine 与 TinyLFU 的实现原理</title>
			<description>&lt;h1 id=&quot;缓存设计caffeine-与-tinylfu-的实现原理&quot;&gt;缓存设计：Caffeine 与 TinyLFU 的实现原理&lt;/h1&gt;

&lt;p&gt;缓存算法其实包含两个部分，准入策略（Admission Policy）以及淘汰策略（Eviction Policy）。一般情况下，对于一个元素，我们先判断是否接受该元素（使用准入策略），若接受的话，则从 Cache 中选择一个替代品（使用淘汰策略），从而把新元素放到 Cache 中。&lt;/p&gt;

&lt;p&gt;本文重点介绍 JAVA 中著名的缓存方案 &lt;a href=&quot;https://github.com/ben-manes/caffeine&quot;&gt;Caffeine&lt;/a&gt; 的底层实现，即 Tiny-LFU 算法，并首先简明地介绍了各个常见的缓存淘汰算法，包括实现原理以及优缺点讨论。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;码字不易，转载请声明出处，否则后果自负。&lt;/strong&gt;&lt;/p&gt;

&lt;h2 id=&quot;缓存淘汰算法&quot;&gt;缓存淘汰算法&lt;/h2&gt;

&lt;h3 id=&quot;lru&quot;&gt;LRU&lt;/h3&gt;

&lt;p&gt;LRU（Least recently used，最近最少使用）算法根据数据的历史访问记录来进行淘汰数据，其核心思想是 “如果数据最近被访问过，那么将来被访问的几率也更高”。&lt;/p&gt;

&lt;p&gt;实现&lt;/p&gt;

&lt;p&gt;通常使用链表实现，靠前的代表最近访问，靠后的代表不经常访问。当 Cache 满的时候，将 Cache 中最不经常访问的元素（链表的尾部节点）驱逐，并全盘接受新来的元素，直接插入到链表头部。&lt;/p&gt;

&lt;p&gt;优缺点&lt;/p&gt;

&lt;p&gt;当存在热点数据时，LRU 的效率很好，但偶发性的、周期性的批量操作会导致 LRU 命中率急剧下降，缓存污染情况比较严重，即会缓存大量长尾数据。&lt;/p&gt;

&lt;h3 id=&quot;lru-k&quot;&gt;LRU-K&lt;/h3&gt;

&lt;p&gt;LRU-K（Least Frequently Used K） 中的 K 代表最近使用的次数，因此 LRU 可以认为是 LRU-1，该算法相当于结合了 LRU 与 LFU 的思想。LRU-K 的主要目的是为了解决 LRU 算法 “缓存污染” 的问题，其核心思想是将 “最近使用过 1 次” 的判断标准扩展为 “最近使用过 K 次”。&lt;/p&gt;

&lt;p&gt;实现&lt;/p&gt;

&lt;p&gt;需要维护一个记录表来记录元素的访问次数，当且仅当元素的访问次数大于阈值 K 的时候，才将元素移动到 Cache 中。淘汰策略还是 LRU，只是相当于准入门槛变高而已。&lt;/p&gt;

&lt;p&gt;优缺点&lt;/p&gt;

&lt;p&gt;LRU-K 避免了长尾请求对 LRU 的影响。但是由于 LRU-K 还需要记录那些被访问过、但还没有放入缓存的对象，因此内存消耗会比 LRU 要多；另外，当 K 值的取值需要权衡，当 K 值很大的时候，LRU-K 的适应能力会变差，需要大量数据访问才能将历史访问清除。&lt;/p&gt;

&lt;h3 id=&quot;arc&quot;&gt;ARC&lt;/h3&gt;

&lt;p&gt;ARC（Adaptative Replacement Cache）结合了 LRU 跟 LFU 两个策略，维护了两个 LRU Cache（L1, L2）。L1 缓存只访问过一次的，L2 缓存至少访问过两次的。&lt;/p&gt;

&lt;p&gt;实现&lt;/p&gt;

&lt;p&gt;ARC 从 L1、L2 中分别划出两个子列表 T1、T2，其中 T1 代表最近访问，T2 代表最高频访问；并分别维护两个僵尸（Ghost）列表 B1、B2，分别存储从 T1、T2 中被驱逐的元素，B1、B2 都使用 LRU 策略。当两个 Cache 满了的时候，如果 T1 中的数据被驱逐，则该数据被存储到 B1 中，同理，T2 驱逐的数据存到 B2 中。&lt;/p&gt;

&lt;p&gt;T1 与 T2 的长度的动态调整，体现了一种负反馈调节的思想：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;如果 B1 中的数据经常被访问，则说明 T1 不够长，所以会拓展 T1 长度，缩短 T2 长度，表现更像 LRU。&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;如果 B2 中的数据经常被访问，则说明 T2 不够长，所以会拓展 T2 长度，缩短 T1 长度，表现更像 LFU。&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;优缺点&lt;/p&gt;

&lt;p&gt;ARC 解决了 LRU 中 Non-Scan-Resistent 问题，即对于某些长尾请求，会导致 T1 中缓存了大量长尾数据，但是 T2 却不会受很大影响。&lt;/p&gt;

&lt;h3 id=&quot;slru&quot;&gt;SLRU&lt;/h3&gt;

&lt;p&gt;SLRU（Segmented LRU）将 LRU 分成保护段（protected segment）和试用段（probationary segment），其实思想跟 ARC 差不多，只不过没有动态调节段的占比，而且只有 T1 与 T2 之间的交换，舍弃了 B1、B2。保护段缓存访问超过一次的，试用段则全盘接受新来的数据。&lt;/p&gt;

&lt;p&gt;实现&lt;/p&gt;

&lt;p&gt;新数据会被存储在试用段，后续如果被访问到，则被提升到保护段。当保护段满的时候，数据会被淘汰至试用段，这时候如果试用段也满了的话，则联动使用 LRU 驱逐。&lt;/p&gt;

&lt;p&gt;局限性&lt;/p&gt;

&lt;p&gt;在该实现中，保护段与试用段的占比是固定的，因此对于分布经常变动的请求是次于 ARC 的。&lt;/p&gt;

&lt;h3 id=&quot;lfu&quot;&gt;LFU&lt;/h3&gt;

&lt;p&gt;LFU（Least Frequently Used）算法根据数据的历史访问频率来淘汰数据，其核心思想是 “如果数据过去被访问多次，那么将来被访问的频率也更高”。&lt;/p&gt;

&lt;p&gt;实现&lt;/p&gt;

&lt;p&gt;LFU 记录数据历史访问记录，即对极大部分数据（包括被驱逐的）都维护一个引用计数，当 Cache 满的时候，将 Cache 中频率最低的与新来的（频率高的）进行交换，并更新 Cache 中的排序（通常用最小堆实现）。&lt;/p&gt;

&lt;p&gt;优缺点&lt;/p&gt;

&lt;p&gt;一般情况下，LFU 效率要优于 LRU，且能够避免周期性或者偶发性的操作导致缓存命中率下降的问题。但 LFU 需要记录数据的历史访问记录，一旦数据访问模式改变，LFU 需要更长时间来适用新的访问模式。&lt;/p&gt;

&lt;p&gt;另一方面，经典算法中，维护引用计数需要占用大量内存空间，并且每次更新都需要重新依照访问计数排序，在实际应用中不可能直接这么硬干。&lt;/p&gt;

&lt;h3 id=&quot;lfu-aging&quot;&gt;LFU-Aging&lt;/h3&gt;

&lt;p&gt;基于 LFU 的改进算法，其核心思想是 “除了访问次数外，还要考虑访问时间”。例如，两天前的一个热门视频在今天可能无人问津，但是因为访问频率曾经非常高，导致这个视频依然占坑在缓存中。&lt;/p&gt;

&lt;p&gt;实现&lt;/p&gt;

&lt;p&gt;该算法的实现其实有很多种，例如 In-Memory LFU 采用了两个策略：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;限制最高访问计数。即达到阈值则不再增加计数。&lt;/li&gt;
  &lt;li&gt;周期性对引用计数作衰减操作，例如除予某个系数。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;优缺点&lt;/p&gt;

&lt;p&gt;LFU-Aging 因为减少了 LFU 带来的高频旧数据的缓存污染问题，相比于 LFU 能够更快适应新的访问模式。但是依然避免不了 LFU 的两个问题，并且衰减系数也是个参数活，需要慢慢调节。&lt;/p&gt;

&lt;h3 id=&quot;window-lfu&quot;&gt;Window-LFU&lt;/h3&gt;

&lt;p&gt;上述 LFU-Aging 解决了 LFU 其中一个痛点，即不适应访问模式的剧烈变化。而 Window-LFU 旨在解决 LFU 需要维护所有数据的访问历史带来的巨大内存消耗的问题。顾名思义，Window-LFU 维护了一个定长计数窗口，记录最近访问的 W 个元素的访问计数。&lt;/p&gt;

&lt;p&gt;实现&lt;/p&gt;

&lt;p&gt;维护一个长度 W 的 FIFO 计数队列（Window），记录最近 W 个元素，元素的计数以 Window 中的计数为准，当 Cache 满了的时候，用 WIndow 中的计数进行驱逐。&lt;/p&gt;

&lt;h2 id=&quot;tinylfu&quot;&gt;TinyLFU&lt;/h2&gt;

&lt;p&gt;顾名思义，TinyLFU 也是 LFU 的一种变体，TinyLFU 方案可以分为两个重要部分，分别是实现 TinyLFU 的准入策略和 Cache 实现的驱逐策略（可以使用多种置换方案，如 LFU、LRU）。当新来一个元素时，Cache 选择要驱逐的元素（victim），TinyLFU 通过引用计数判断替换为新来的元素是否有收益（即是否会带来更高的命中率）。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://segmentfault.com/img/bVbfGh9?w=866&amp;amp;h=428&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;近似引用计数&quot;&gt;近似引用计数&lt;/h3&gt;

&lt;p&gt;近年来有很多针对大数据流的统计研究，期望得到一种压缩的数据结构，例如用来反映数据流中元素的出现频率。比较著名的一类算法是基于布隆过滤器的 Sketch。布隆过滤器用来判断一个给定元素是否存在，而 Sketch 用来判断给定元素出现的频率，具体的做法与布隆过滤器类似，使用一组哈希函数，映射到引用计数矩阵的某几个值，通过对应的几个结果值来反映给定元素的近似出现频率。例如，Minimal Increment Counting Bloom Filter 取的是几个哈希结果的最小值作为频率的近似值，每次新增元素也只对最小计数值进行递增操作。&lt;/p&gt;

&lt;p&gt;但是这类算法有一个问题，现实中的数据流通常带有大量长尾请求，这些请求的出现频率非常低，很容易就把 Sketch 打得饱和，使得计数有很大误差。&lt;/p&gt;

&lt;p&gt;TinyLFU 提出的方案很简单，每次添加一个新元素时，则对一个 counter 进行递增，当计数达到某一个阈值的时候，则对所有计数器作衰减操作（乘上某个衰减系数）。&lt;/p&gt;

&lt;p&gt;由于 counter 都是整型，因此长尾元素的计数大概率会被衰减操作置零；另外一方面，衰减操作也会把高频的旧数据逐渐置零，相当于一个 Aging 操作。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://image-static.segmentfault.com/198/843/1988435212-58d64be00bb9d_articlex&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;准入策略&quot;&gt;准入策略&lt;/h3&gt;

&lt;p&gt;上文提到，TinyLFU 负责准入策略，其实就是把即将淘汰的元素（Cache 中的 victim）与即将到来的元素进行对比，对比的依据是两者在 Sketch 中的计数。&lt;/p&gt;

&lt;p&gt;虽然上述 Sketch 的重置方案能够解决长尾数据带来的缓存污染问题，但是 TinyLFU 还引入了布隆过滤器，后面称为 Doorkeeper，来进一步减少这个问题的影响。具体的做法如下：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;判断新来的元素是否在 doorkeeper 中，没有的话，则插入 doorkeeper，否则插入 Sketch 中&lt;/li&gt;
  &lt;li&gt;当请求一个元素的时候，如果元素在 doorkeeper 中的话，则返回 Sketch 中的计数+1，否则直接返回 Sketch 中的计数。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;img src=&quot;https://i.imgur.com/LhQYje5.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;每次对 Sketch 进行重置的时候，也需要对 doorkeeper 进行清空操作。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;由于前置布隆过滤器 Doorkeeper 的存在，计数为整型的 Sketch 占用的内存可以更小，因为一些长尾请求已经被 Doorkeeper 拒之门外。&lt;/p&gt;

&lt;h3 id=&quot;驱逐策略&quot;&gt;驱逐策略&lt;/h3&gt;

&lt;p&gt;其实驱逐策略很简单，就是一个普通的 LFU 策略，每次需要淘汰的时候，选择频率最小（通常用最小堆实现）的元素进行替换。&lt;/p&gt;

&lt;h3 id=&quot;w-tinylfu&quot;&gt;W-TinyLFU&lt;/h3&gt;

&lt;p&gt;TinyLFU 其实对一些突如其来的高频请求不够友好，因为这些请求很可能在积攒足够频率之前就被淘汰了，通常来说，LRU 对这类请求有着更好的效果。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://image-static.segmentfault.com/274/307/2743073853-58d64be0088fe_articlex&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;在该结构中，Cache 使用 SLRU 驱逐策略及 TinyLFU 准入策略，而 Window 则使用 LRU 驱逐策略并全盘接受任何新元素（没有准入门槛）。&lt;/p&gt;

&lt;p&gt;每当一个元素进来时，都会被 Window 接受，被 Window 淘汰的元素有机会通过 Filter（Doorkeeper+Sketch）与 Cache 中的元素竞争。如果竞争成功的话，则相当于 Cache 中的元素被驱逐，否则相当于 Window 中的元素被驱逐。&lt;/p&gt;

&lt;p&gt;可以看到 W-TinyLFU 兼具了 LRU 与 LFU 的优点。该算法也是 JAVA 中著名的缓存 Caffeine 的底层实现方案。&lt;/p&gt;

&lt;h2 id=&quot;reference&quot;&gt;Reference&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;ARC 算法：https://blog.csdn.net/WSKINGS/article/details/46416451&lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Caffein 简介：http://highscalability.com/blog/2016/1/25/design-of-a-modern-cache.html&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;Tiny-LFU 论文：https://arxiv.org/pdf/1512.00727.pdf&lt;/li&gt;
  &lt;li&gt;论文简单解读：https://segmentfault.com/a/1190000016091569?utm_source=tag-newest&lt;/li&gt;
&lt;/ul&gt;
</description>
			<pubDate>Fri, 24 Jul 2020 00:00:00 +0000</pubDate>
			<link>https://sarkerson.github.io/2020/07/24/Tiny-LFU/</link>
			<guid isPermaLink="true">https://sarkerson.github.io/2020/07/24/Tiny-LFU/</guid>
        
			<category>cache</category>
        
        
		</item>
    
		<item>
			<title>缓存设计：Go 缓存的实现与评测</title>
			<description>&lt;h1 id=&quot;缓存设计go-缓存的实现与评测&quot;&gt;缓存设计：Go 缓存的实现与评测&lt;/h1&gt;

&lt;p&gt;本文主要介绍 Go 生态下面比较有名的几个 Cache，剖析这些 Cache 实现的原理，并分析各自存在的不足之处。最后，介绍如何编写 Benchmark 来对比不同的 Cache，方便喜欢造轮子的同学进行测试。&lt;/p&gt;

&lt;p&gt;大家第一次接触 Cache，也许都是因为 LeetCode 上面这么一道 LRUCache 的题目。对 LRU 不太了解的同学可以再来做一下这道题目复习复习。&lt;/p&gt;

&lt;h2 id=&quot;1-lru-cache&quot;&gt;1. LRU Cache&lt;/h2&gt;

&lt;div class=&quot;language-c++ highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cm&quot;&gt;/* 
Design and implement a data structure for Least Recently Used (LRU) cache. It should support the following operations: get and put.
get(key) - Get the value (will always be positive) of the key if the key exists in the cache, otherwise return -1.
put(key, value) - Set or insert the value if the key is not already present. When the cache reached its capacity, it should invalidate the least recently used item before inserting a new item.
Follow up:
Could you do both operations in O(1) time complexity?
Example:
*/&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;LRUCache&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cache&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;LRUCache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;cm&quot;&gt;/* capacity */&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;put&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;put&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;    &lt;span class=&quot;c1&quot;&gt;// returns 1&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;put&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;   &lt;span class=&quot;c1&quot;&gt;// evicts key 2&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;    &lt;span class=&quot;c1&quot;&gt;// returns -1 (not found)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;put&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;   &lt;span class=&quot;c1&quot;&gt;// evicts key 1&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;    &lt;span class=&quot;c1&quot;&gt;// returns -1 (not found)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;    &lt;span class=&quot;c1&quot;&gt;// returns 3&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;    &lt;span class=&quot;c1&quot;&gt;// returns 4&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;最简洁的实现&lt;/p&gt;

&lt;div class=&quot;language-py highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;LRUCache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;__init__&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;capacity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lru&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;OrderedDict&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cap&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;capacity&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;key&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lru&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# move to the end
&lt;/span&gt;    &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lru&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;move_to_end&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lru&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;put&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;None&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;len&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lru&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cap&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;and&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;key&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lru&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;c1&quot;&gt;# pop the front item
&lt;/span&gt;      &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lru&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;popitem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;last&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;elif&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;key&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lru&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lru&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;move_to_end&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lru&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;value&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;2-并发支持&quot;&gt;2. 并发支持&lt;/h2&gt;

&lt;p&gt;原生的 LRU Cache 显然不是并发安全的，因为每次 get 或者 set 都涉及到了链表节点移动的操作。要使得 LRU Cache 并发安全，最简单暴力的做法是，直接对整个 LRU Cache  加锁，每次 put、get 都需要 lock 一下，例如 1.8k stars 的 https://github.com/hashicorp/golang-lru，以及基于该库进行二层封装的其他共用库。代码差不多长这样子：&lt;/p&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;l&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lockedCache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Add&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;interface&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{},&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;expiresAt&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Time&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;l&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;m&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Lock&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;l&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Add&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;expiresAt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;l&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;m&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Unlock&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;l&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lockedCache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;interface&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{})&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;interface&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{},&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;bool&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;l&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;m&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Lock&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;v&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;f&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;l&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;l&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;m&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Unlock&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;v&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;f&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这样导致的问题是，当并发 Get 操作很多的时候，Get 操作已经变成串行。也就说对整个 Cache 进行加锁是不明智的。&lt;/p&gt;

&lt;h3 id=&quot;解决1分桶&quot;&gt;解决1：分桶&lt;/h3&gt;

&lt;p&gt;将一个 hashtable 根据 key 拆分成多个 hashtable，每个 hashtable 对应一个锁，锁粒度更细，冲突的概率也就更低了。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://i.ibb.co/6mxr7dS/1.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;解决2延缓提权&quot;&gt;解决2：延缓提权&lt;/h3&gt;

&lt;p&gt;对于 get 操作，原生 LRU Cache 每次都会有一次 move_to_front 的操作，因此每次 get 都会涉及整个 cache 的加锁操作，这会给 cache 的性能大打折扣。&lt;/p&gt;

&lt;p&gt;解决方案是，为每个 Item 都维护一个访问计数 promotions ，当且仅当 promotions 达到阈值的时候，才触发 move_to_front 操作。&lt;/p&gt;

&lt;h2 id=&quot;3-几个实现&quot;&gt;3. 几个实现&lt;/h2&gt;

&lt;h3 id=&quot;31-bigcache&quot;&gt;3.1. BigCache&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/allegro/bigcache&quot;&gt;BigCache&lt;/a&gt; 根据键值的 hash 将数据分散到多个分片上面进行存储，实现了解决方案一，并根据 Golang 的 GC 做了优化。每个分片的底层存储结构是一个环形缓冲区 entries， 并维护一个 key 到缓冲区 index 的映射。&lt;/p&gt;

&lt;h4 id=&quot;cache-的定义&quot;&gt;Cache 的定义&lt;/h4&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;BigCache&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;shards&lt;/span&gt;    &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cacheShard&lt;/span&gt;  &lt;span class=&quot;c&quot;&gt;// 分片&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;lifeWindow&lt;/span&gt;  &lt;span class=&quot;kt&quot;&gt;uint64&lt;/span&gt;     &lt;span class=&quot;c&quot;&gt;// 过期时间&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;clock&lt;/span&gt;     &lt;span class=&quot;n&quot;&gt;clock&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;hash&lt;/span&gt;     &lt;span class=&quot;n&quot;&gt;Hasher&lt;/span&gt;     &lt;span class=&quot;c&quot;&gt;// 实现了hash算法的interface，所以可以自定义hash&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;config&lt;/span&gt;    &lt;span class=&quot;n&quot;&gt;Config&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;shardMask&lt;/span&gt;   &lt;span class=&quot;kt&quot;&gt;uint64&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;maxShardSize&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;uint32&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;close&lt;/span&gt;     &lt;span class=&quot;k&quot;&gt;chan&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;struct&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;shard-的定义&quot;&gt;Shard 的定义&lt;/h4&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cacheShard&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;hashmap&lt;/span&gt;   &lt;span class=&quot;k&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;uint64&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;uint32&lt;/span&gt;  &lt;span class=&quot;c&quot;&gt;// 存储在环形缓冲区 entries 中的元素的 index&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;entries&lt;/span&gt;   &lt;span class=&quot;n&quot;&gt;queue&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;BytesQueue&lt;/span&gt;   &lt;span class=&quot;c&quot;&gt;// 实际存储内容的 byte 数组，是个ring buffer&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;lock&lt;/span&gt;     &lt;span class=&quot;n&quot;&gt;sync&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;RWMutex&lt;/span&gt;     &lt;span class=&quot;c&quot;&gt;// shard的全局锁&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;entryBuffer&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;byte&lt;/span&gt;        &lt;span class=&quot;c&quot;&gt;// 预分配的一段内存&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;onRemove&lt;/span&gt;   &lt;span class=&quot;n&quot;&gt;onRemoveCallback&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;isVerbose&lt;/span&gt;  &lt;span class=&quot;kt&quot;&gt;bool&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;logger&lt;/span&gt;   &lt;span class=&quot;n&quot;&gt;Logger&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;clock&lt;/span&gt;    &lt;span class=&quot;n&quot;&gt;clock&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;lifeWindow&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;uint64&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;hashmapStats&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;uint64&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;uint32&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;stats&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Stats&lt;/span&gt;           &lt;span class=&quot;c&quot;&gt;// 统计信息&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;每个 shard 的结构如图所示。在 BigCache 的实现中，map 的 key 为原始 key 的哈希值 (uint64)，value 则为一个 int 类型的 index。其中，index 对应底层 ByteQueue 中的某个位置，在读取操作时，BigCache 从 ByteQueue 中取出序列化的 [] byte 片段，从而还原出 value 信息。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://i.ibb.co/NYKQ5Vq/2.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h4 id=&quot;set-操作&quot;&gt;Set 操作&lt;/h4&gt;

&lt;p&gt;每当 set 一个新元素的时候，会把 value 序列化后塞进环形缓冲区 entries，然后记录 key 及 value 存储的  index 到 hashmap 中。如果 key 对应的 entry 已经在 entries 中，则之前的 entry 会被 reset。&lt;/p&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;s&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cacheShard&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hashedKey&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;uint64&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;entry&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;byte&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;error&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
 &lt;span class=&quot;n&quot;&gt;currentTimestamp&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;uint64&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;clock&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;epoch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;
 &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lock&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Lock&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
 &lt;span class=&quot;c&quot;&gt;// 1. 查找是否已经存在了对应的缓存对象，如果存在，将它的值置为空&lt;/span&gt;
 &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;previousIndex&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hashmap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hashedKey&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;];&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;previousIndex&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;previousEntry&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;err&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;entries&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;previousIndex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;err&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
   &lt;span class=&quot;n&quot;&gt;resetKeyFromEntry&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;previousEntry&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
 &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
 &lt;span class=&quot;c&quot;&gt;// 2. 取出最老的缓存对象，判断是否要过期，是的话则淘汰&lt;/span&gt;
 &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;oldestEntry&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;err&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;entries&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Peek&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;err&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;onEvict&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;oldestEntry&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;currentTimestamp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;removeOldestEntry&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
 &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
 &lt;span class=&quot;c&quot;&gt;// 将对象放入到一个字节数组中&lt;/span&gt;
 &lt;span class=&quot;n&quot;&gt;w&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;wrapEntry&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;currentTimestamp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hashedKey&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;entry&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;entryBuffer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
 &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;c&quot;&gt;// 放入到字节队列中&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;index&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;err&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;entries&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Push&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;w&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;err&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
   &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hashmap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hashedKey&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;uint32&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;index&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
   &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lock&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Unlock&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
   &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;c&quot;&gt;// 3. 如果空间不足，移除最老的元素&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;removeOldestEntry&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;NoSpace&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
   &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lock&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Unlock&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
   &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fmt&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Errorf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;entry is bigger than max shard size&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
 &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;一个潜在的问题是，上述做法相当于把 map 的 GC 压力转到了 slice (array)，是否真的能带来性能上的提升？事实上，在 golang 中，不存在释放部分数组的情况，即数组的内存管理有两种情况：1. 全部释放；2. 继续保留。因此，对于数组的 GC 时间复杂度可理解为 O (1) 时间。详见 https://golangbot.com/arrays-and-slices/&lt;/p&gt;

&lt;h4 id=&quot;get-操作&quot;&gt;Get 操作&lt;/h4&gt;

&lt;p&gt;get 操作比较简单，根据 hashedKey 获取 itemIndex（环形 buffer 中的 offset），然后根据 itemIndex 获取 entry（即我们存储的序列化的对象）。不过，注意这里有一个对 key 值的二次判断，因为 hash 冲突的存在，实际存储的 key 可能并不一致。&lt;/p&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;s&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cacheShard&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hashedKey&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;uint64&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;([]&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;byte&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lock&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;RLock&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;itemIndex&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hashmap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hashedKey&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;itemIndex&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lock&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;RUnlock&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;miss&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ErrEntryNotFound&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;c&quot;&gt;// 根据 index 取出 entry 的 header 信息&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;wrappedEntry&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;err&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;entries&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;itemIndex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;err&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lock&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;RUnlock&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;miss&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;err&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;entryKey&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;readKeyFromEntry&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;wrappedEntry&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;key&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;entryKey&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;c&quot;&gt;// 由于对于冲突的key只会存储最近的一个，所以当hash值一样时还要具体在看key是不是想要的key&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;isVerbose&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;logger&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Printf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Collision detected. Both %q and %q have the same hash %x&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;entryKey&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hashedKey&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lock&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;RUnlock&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;collision&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ErrEntryNotFound&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;c&quot;&gt;// 根据 header 信息取出实际存储的 entry&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;entry&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;readEntry&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;wrappedEntry&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lock&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;RUnlock&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;entry&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;小结&quot;&gt;小结&lt;/h4&gt;

&lt;h5 id=&quot;缓存策略&quot;&gt;缓存策略&lt;/h5&gt;

&lt;p&gt;不是，其实是 FIFO。每次 set 会把新元素放到后面，如果有冲突，则把先前的元素删除。get 操作仅仅是一个读操作，读到非过期则返回。&lt;/p&gt;

&lt;h5 id=&quot;驱逐策略&quot;&gt;驱逐策略&lt;/h5&gt;

&lt;p&gt;BigCache 的驱逐策略有三个。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;首先，在增加一个元素之前，会检查最老的元素要不要删除。过期则删除&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;其次，在添加一个元素失败后，会清理空间删除最老的元素。&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;同时， 还会专门有一个定时的清理 goroutine, 负责移除过期数据。&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;h5 id=&quot;槽点&quot;&gt;槽点&lt;/h5&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;Set 相同的 key 会导致 BigCache  出现气泡，因为 BigCache 没有尝试重复利用这些空间。&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;BigCache 使用的是 FIFO 策略，因此对于一般的 Zipf 分布请求不友好&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;h5 id=&quot;优点&quot;&gt;优点&lt;/h5&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;Get 请求是无锁的，非常快&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;无 GC 压力&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;32-freecache&quot;&gt;3.2 &lt;strong&gt;FreeCache&lt;/strong&gt;&lt;/h3&gt;

&lt;p&gt;FreeCache 将 Cache 切成 256 个 Segment，每个 Segment 包含 256 个 slot 以及一个 ringbuffer 来存放具体数据，每个 slot 可以存放多个 entry，指向 ringbuffer 中存放的 item 具体位置。当 add 一个元素时，首先会由 LSB(hash)[:8] 定位到某个 segment，并由 LSB(hash)[8:16] 定位到该 segment 下面的 slot。每个 slot 按 hash 递增的顺序存储多个 entryPtr（不使用 hashMap 应该是基于节约空间以及冲突的考虑）。&lt;/p&gt;

&lt;h4 id=&quot;cache-的定义-1&quot;&gt;Cache 的定义&lt;/h4&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Cache&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;locks&lt;/span&gt;   &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;segmentCount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sync&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Mutex&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;segments&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;segmentCount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;segment&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;segment-的定义&quot;&gt;Segment 的定义&lt;/h4&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;// a segment contains 256 slots, a slot is an array of entry pointers ordered by hash16 value&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;// the entry can be looked up by hash value of the key.&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;segment&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;rb&lt;/span&gt;       &lt;span class=&quot;n&quot;&gt;RingBuf&lt;/span&gt; &lt;span class=&quot;c&quot;&gt;// ring buffer that stores data&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;segId&lt;/span&gt;     &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;       &lt;span class=&quot;kt&quot;&gt;uint32&lt;/span&gt;
    &lt;span class=&quot;c&quot;&gt;// ... 一些统计值&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;vacuumLen&lt;/span&gt;   &lt;span class=&quot;kt&quot;&gt;int64&lt;/span&gt;    &lt;span class=&quot;c&quot;&gt;// up to vacuumLen, new data can be written without overwriting old data.&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;slotLens&lt;/span&gt;    &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;256&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;int32&lt;/span&gt; &lt;span class=&quot;c&quot;&gt;// The actual length for every slot.&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;slotCap&lt;/span&gt;    &lt;span class=&quot;kt&quot;&gt;int32&lt;/span&gt;    &lt;span class=&quot;c&quot;&gt;// max number of entry pointers a slot can hold.&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;slotsData&lt;/span&gt;   &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;entryPtr&lt;/span&gt; &lt;span class=&quot;c&quot;&gt;// shared by all 256 slots&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;// entry pointer struct points to an entry in ring buffer&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;entryPtr&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;offset&lt;/span&gt;  &lt;span class=&quot;kt&quot;&gt;int64&lt;/span&gt;  &lt;span class=&quot;c&quot;&gt;// entry offset in ring buffer&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;hash16&lt;/span&gt;  &lt;span class=&quot;kt&quot;&gt;uint16&lt;/span&gt; &lt;span class=&quot;c&quot;&gt;// entries are ordered by hash16 in a slot.&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;keyLen&lt;/span&gt;  &lt;span class=&quot;kt&quot;&gt;uint16&lt;/span&gt; &lt;span class=&quot;c&quot;&gt;// used to compare a key&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;reserved&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;uint32&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;img src=&quot;https://i.ibb.co/KXFrGt6/3.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h4 id=&quot;set-操作-1&quot;&gt;Set 操作&lt;/h4&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cache&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Set&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;byte&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;expireSeconds&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;err&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
 &lt;span class=&quot;c&quot;&gt;// 获取 segID 以及 slotID&lt;/span&gt;
 &lt;span class=&quot;c&quot;&gt;// 对 segment 加锁&lt;/span&gt;
 &lt;span class=&quot;n&quot;&gt;slot&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;seg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;getSlot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;slotId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;c&quot;&gt;// []entryPtr&lt;/span&gt;
 &lt;span class=&quot;n&quot;&gt;idx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;match&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;seg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lookup&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;slot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hash16&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
 &lt;span class=&quot;c&quot;&gt;// 如果冲突的话，旧空间足够则直接覆盖，否则标记删除旧空间&lt;/span&gt;
 &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;match&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;c&quot;&gt;// 如果老的 entry 空间足够容得下新的 entry 的话，则 in-place 修改，return&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hdr&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;valCap&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hdr&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;valLen&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c&quot;&gt;// 更新 header 以及 value，key 不变&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;seg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rb&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;WriteAt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hdrBuf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;matchedPtr&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;offset&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;seg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rb&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;WriteAt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;matchedPtr&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;offset&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;+&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ENTRY_HDR_SIZE&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;+&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;int64&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hdr&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;keyLen&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;c&quot;&gt;// 否则，在 ringbuff 中标记删除该 entry，在 slot 中直接把该 entryPtr 删除&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;seg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;delEntryPtr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;slotId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;slot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;idx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;c&quot;&gt;// ...&lt;/span&gt;
 &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
 &lt;span class=&quot;c&quot;&gt;// 对该 slot 执行写时驱逐策略，当且仅当剩余空间不够才进行驱逐&lt;/span&gt;
 &lt;span class=&quot;n&quot;&gt;seg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;evacuate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;entryLen&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;slotId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
 &lt;span class=&quot;c&quot;&gt;// 写入 ringbuff 以及 entryPtr&lt;/span&gt;
 &lt;span class=&quot;n&quot;&gt;seg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;insertEntryPtr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;slotId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hash16&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;seg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rb&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;End&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;idx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hdr&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;keyLen&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
 &lt;span class=&quot;n&quot;&gt;seg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rb&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Write&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hdrBuf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;
 &lt;span class=&quot;n&quot;&gt;seg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rb&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Write&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
 &lt;span class=&quot;n&quot;&gt;seg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rb&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Write&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
 &lt;span class=&quot;n&quot;&gt;seg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rb&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Skip&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;int64&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hdr&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;valCap&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hdr&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;valLen&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;写时驱逐策略&quot;&gt;写时驱逐策略&lt;/h4&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;seg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;vacuumLen&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;entryLen&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
 &lt;span class=&quot;n&quot;&gt;oldHdr&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;c&quot;&gt;// 取出队首&lt;/span&gt;
 &lt;span class=&quot;c&quot;&gt;// 1. 如果队首被标记删除&lt;/span&gt;
 &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;oldHdr&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;deleted&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;c&quot;&gt;// 则将该空间添加到可利用空间&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;continue&lt;/span&gt;
 &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
 &lt;span class=&quot;c&quot;&gt;// 2. 已经过期&lt;/span&gt;
 &lt;span class=&quot;n&quot;&gt;expired&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;oldHdr&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;expireAt&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;oldHdr&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;expireAt&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;now&lt;/span&gt;
 &lt;span class=&quot;c&quot;&gt;// 3. 该 entry 的访问时间小于整个 segment 的平均访问时间（近 LRU 策略）&lt;/span&gt;
 &lt;span class=&quot;n&quot;&gt;leastRecentUsed&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int64&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;oldHdr&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;accessTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;atomic&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;LoadInt64&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;seg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;totalCount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;atomic&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;LoadInt64&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;seg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;totalTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
 &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;expired&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;leastRecentUsed&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;consecutiveEvacuate&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;5&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;c&quot;&gt;// 标记删除，并将可利用空间回收&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;seg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;delEntryPtrByOffset&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;oldHdr&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;slotId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;oldHdr&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hash16&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;oldOff&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;seg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;vacuumLen&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;oldEntryLen&lt;/span&gt;
 &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;c&quot;&gt;// 将队首移到队尾，更新 slot 的 entryPtr 信息，提高命中率&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;newOff&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;seg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rb&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Evacuate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;oldOff&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;oldEntryLen&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;seg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;updateEntryPtr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;oldHdr&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;slotId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;oldHdr&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hash16&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;oldOff&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;newOff&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
 &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;get-操作-1&quot;&gt;Get 操作&lt;/h4&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cache&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;byte&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;byte&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;err&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;key的哈希值不存在&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ErrNotFound&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;对应的entry已过期&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;delete&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;entry&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ErrNotFound&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;update&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;entry_header&lt;/span&gt;   
  &lt;span class=&quot;n&quot;&gt;读取entry中的value&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;BTW，FreeCache 还做了内存对齐优化，详见： https://go101.org/article/memory-layout.html，https://ms2008.github.io/2019/08/01/golang-memory-alignment/&lt;/p&gt;

&lt;h4 id=&quot;小结-1&quot;&gt;小结&lt;/h4&gt;

&lt;h5 id=&quot;槽点-1&quot;&gt;槽点&lt;/h5&gt;

&lt;ul&gt;
  &lt;li&gt;没有独立的 goroutine 来进行驱逐，相当于把驱逐的压力都放在了 set 操作的时候（可能是害怕独立的 goroutine 对锁持有时间的不可控制性？）&lt;/li&gt;
&lt;/ul&gt;

&lt;h5 id=&quot;优点-1&quot;&gt;优点&lt;/h5&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;近 LRU 策略&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;尝试对老空间的再利用&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;内存对齐优化&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;h5 id=&quot;提权策略&quot;&gt;提权策略&lt;/h5&gt;

&lt;p&gt;提权是通过修改 entry 的过期时间实现的（用于近似 LRU 驱逐）&lt;/p&gt;

&lt;h5 id=&quot;驱逐策略-1&quot;&gt;驱逐策略&lt;/h5&gt;

&lt;p&gt;驱逐策略主要有三个：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;被标记删除的（set 的时候发现相同 key 的旧 entry；get的时候发现 expired 的 entry）&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;已经过期的&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;entry 的访问时间小于整个 segment 的平均访问时间&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;33-ccache&quot;&gt;3.3 CCache&lt;/h3&gt;

&lt;p&gt;主要实现了解决方案 1、2。&lt;/p&gt;

&lt;h4 id=&quot;item-的结构&quot;&gt;Item 的结构&lt;/h4&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Item&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;     &lt;span class=&quot;kt&quot;&gt;string&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;group&lt;/span&gt;    &lt;span class=&quot;kt&quot;&gt;string&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;promotions&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int32&lt;/span&gt;    &lt;span class=&quot;c&quot;&gt;// 计数窗口&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;refCount&lt;/span&gt;  &lt;span class=&quot;kt&quot;&gt;int32&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;expires&lt;/span&gt;   &lt;span class=&quot;kt&quot;&gt;int64&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;size&lt;/span&gt;    &lt;span class=&quot;kt&quot;&gt;int64&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;value&lt;/span&gt;    &lt;span class=&quot;k&quot;&gt;interface&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{}&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;element&lt;/span&gt;   &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;list&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Element&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;cache-的定义-2&quot;&gt;Cache 的定义&lt;/h4&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Cache&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Configuration&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;list&lt;/span&gt;     &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;list&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;List&lt;/span&gt; &lt;span class=&quot;c&quot;&gt;// LRU&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;size&lt;/span&gt;     &lt;span class=&quot;kt&quot;&gt;int64&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;buckets&lt;/span&gt;   &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;bucket&lt;/span&gt;  &lt;span class=&quot;c&quot;&gt;// entrys&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;bucketMask&lt;/span&gt;  &lt;span class=&quot;kt&quot;&gt;uint32&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;deletables&lt;/span&gt;  &lt;span class=&quot;k&quot;&gt;chan&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Item&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;promotables&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;chan&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Item&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;control&lt;/span&gt;   &lt;span class=&quot;k&quot;&gt;chan&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;interface&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;bucket-的结构&quot;&gt;Bucket 的结构&lt;/h4&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bucket&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;sync&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;RWMutex&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;lookup&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Item&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;b&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;bucket&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Item&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;RLock&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;defer&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;RUnlock&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lookup&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;b&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;bucket&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;interface&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{},&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;duration&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Duration&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Item&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Item&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;expires&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Now&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Add&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;duration&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;UnixNano&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;item&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;newItem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;expires&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Lock&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;existing&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lookup&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lookup&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;item&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Unlock&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;item&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;existing&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;set-操作-2&quot;&gt;Set 操作&lt;/h4&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;c&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;interface&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{},&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;duration&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Duration&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Item&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;item&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;existing&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;bucket&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;duration&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;existing&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;c&quot;&gt;// 删除先前含有相同 key 的 item&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;deletables&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;existing&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;c&quot;&gt;// 提权&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;promote&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;item&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;item&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;c&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;promote&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;item&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Item&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;promotables&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;item&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;img src=&quot;https://i.ibb.co/XyB2dmL/6.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h4 id=&quot;get-操作-2&quot;&gt;Get 操作&lt;/h4&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;// Get an item from the cache. Returns nil if the item wasn&apos;t found.&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;// This can return an expired item. Use item.Expired() to see if the item&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;// is expired and item.TTL() to see how long until the item expires (which&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;// will be negative for an already expired item).&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;c&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Item&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;item&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;bucket&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;item&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;c&quot;&gt;// 没有过期，则提权&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;item&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;expires&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Now&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;UnixNano&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;promote&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;item&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;item&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;img src=&quot;https://i.ibb.co/qRFbnhS/7.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h4 id=&quot;清道夫协程&quot;&gt;清道夫协程&lt;/h4&gt;

&lt;p&gt;在 new 一个 cache 的时候，会起一个独立的 goroutine 来处理需要提权、删除的 Item：&lt;/p&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;c&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;worker&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;defer&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;close&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;control&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;dropped&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;item&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ok&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;promotables&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ok&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;false&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;goto&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;drain&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doPromote&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;item&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;size&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;maxSize&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;dropped&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;gc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;item&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;deletables&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doDelete&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;item&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;control&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;control&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;switch&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;msg&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;control&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;getDropped&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;msg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;res&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dropped&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;dropped&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;setMaxSize&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;maxSize&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;msg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;size&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;size&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;maxSize&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                    &lt;span class=&quot;n&quot;&gt;dropped&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;gc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;drain&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;item&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;deletables&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doDelete&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;item&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;default&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;nb&quot;&gt;close&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;deletables&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;小结-2&quot;&gt;小结&lt;/h4&gt;

&lt;p&gt;CCache 主要使用了延缓提权、分桶策略，来减少并发获取 key 的锁冲突，实现非常简单。&lt;/p&gt;

&lt;p&gt;但是底层仍然使用指针，避免不了 GC 压力。&lt;/p&gt;

&lt;h3 id=&quot;34-benchmark&quot;&gt;3.4 &lt;strong&gt;BenchMark&lt;/strong&gt;&lt;/h3&gt;

&lt;p&gt;这里直接引用 DGraph 的一篇评测结果。该文章对比了 BigCache、FreeCache 以及 GroupCache 在只读、只写、混合读写的性能对比。&lt;/p&gt;

&lt;h4 id=&quot;只读&quot;&gt;只读&lt;/h4&gt;

&lt;p&gt;在只读场景下，BigCache 性能最优，因为在 BigCache 中对分片用了读写锁，所以只读场景下是无锁的。而 FreeCache 以及 GroupCache 在读场景下都需要对分片进行操作，因此加了 mutex，因此性能次于 BigCache。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://i.ibb.co/fDGQQt1/8.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h4 id=&quot;只写&quot;&gt;只写&lt;/h4&gt;

&lt;p&gt;在只写场景下，三者性能差别不大，但是 FreeCache 性能更优。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://i.ibb.co/9g4TCp8/4.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h4 id=&quot;混合读写-25-writes-75-reads&quot;&gt;混合读写 (25% writes, 75% reads)&lt;/h4&gt;

&lt;p&gt;看起来只有 BigCache 对并发友好。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://i.ibb.co/XDC7BbX/5.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h4 id=&quot;zipf-分布缓存命中率&quot;&gt;Zipf 分布缓存命中率&lt;/h4&gt;

&lt;p&gt;什么是 Zipf 分布？它可以表述为：在&lt;a href=&quot;https://zh.wikipedia.org/wiki/自然语言&quot;&gt;自然语言&lt;/a&gt;的&lt;a href=&quot;https://zh.wikipedia.org/wiki/語料庫&quot;&gt;語料庫&lt;/a&gt;裡，一个单词出现的频率与它在频率表里的排名成&lt;a href=&quot;https://zh.wikipedia.org/wiki/反比&quot;&gt;反比&lt;/a&gt;，说人话则是，出现频率高的，则更容易被访问，比如搜索结果、淘宝销量排名等等。放在请求的场景下则是，热门内容越容易被请求。&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;CACHE SIZE (# OF ELEM)&lt;/th&gt;
      &lt;th&gt;10000&lt;/th&gt;
      &lt;th&gt;100000&lt;/th&gt;
      &lt;th&gt;1000000&lt;/th&gt;
      &lt;th&gt;10000000&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;BigCache&lt;/td&gt;
      &lt;td&gt;-&lt;/td&gt;
      &lt;td&gt;37%&lt;/td&gt;
      &lt;td&gt;52%&lt;/td&gt;
      &lt;td&gt;55%&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;FreeCache&lt;/td&gt;
      &lt;td&gt;-&lt;/td&gt;
      &lt;td&gt;38%&lt;/td&gt;
      &lt;td&gt;55%&lt;/td&gt;
      &lt;td&gt;90%&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;GroupCache&lt;/td&gt;
      &lt;td&gt;29%&lt;/td&gt;
      &lt;td&gt;40%&lt;/td&gt;
      &lt;td&gt;54%&lt;/td&gt;
      &lt;td&gt;90%&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;FreeCache 和 GroupCache 都作了近 LRU 策略，而 BigCache 相当于 FIFO，因此 BigCache 对于 Zipf 分布的请求不够友好，原因如下：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;BigCache 没有充分利用 buffer 的空间，如果有大量相同的 key 写入的话，会导致 ringbuffer 中存在相同的 key，并且产生气泡。&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;BigCache 没有在 get 的时候对 entry 进行提权，有可能导致最近访问的 key 被驱逐&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;4-编写-benchmark&quot;&gt;4. 编写 Benchmark&lt;/h2&gt;

&lt;p&gt;每当完成一个轮子，我们必须和业界其他轮子进行 PK。如果仅仅弄一个能跑的轮子，那还不简单，重要的是看谁能有更强的 performance，在 PK 中发现自己的不足，吸收别人的优点，才能造出更强的轮子。&lt;/p&gt;

&lt;p&gt;下面的 benchmark 来自 https://github.com/dgraph-io/benchmarks/blob/master/cachebench/cache_bench_test.go，我们只需要实现我们自己的 Cache 的测试接口即可。下面例子是 CCache：&lt;/p&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;CCache&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;c&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ccache&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Cache&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;r&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;CCache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;byte&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;([]&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;byte&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;item&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;r&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;item&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
   &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;errKeyNotFound&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
   &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;item&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;([]&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;byte&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;r&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;CCache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Set&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;byte&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;error&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;r&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Set&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Second&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;newCCache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;keysInWindow&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;CCache&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;cc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ccache&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;New&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ccache&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Configure&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;MaxSize&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;int64&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;keysInWindow&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ItemsToPrune&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;500&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;CCache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;下面是结果&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;BenchmarkCaches/CCacheZipfMixed-4         5214476        252 ns/op        28 B/op      2 allocs/op
BenchmarkCaches/FastCacheZipfMixed-4       12887888         89.0 ns/op       6 B/op      0 allocs/op
BenchmarkCaches/FreeCacheZipfMixed-4       21327250         59.1 ns/op       1 B/op      0 allocs/op
BenchmarkCaches/GroupCacheZipfMixed-4       9320209        118 ns/op        13 B/op      0 allocs/op
BenchmarkCaches/CCacheOneKeyMixed-4        2775771        449 ns/op        22 B/op      2 allocs/op
BenchmarkCaches/CCacheZipfRead-4         7524876        143 ns/op        16 B/op      2 allocs/op
BenchmarkCaches/FastCacheZipfRead-4       17824086         64.9 ns/op       7 B/op      0 allocs/op
BenchmarkCaches/FreeCacheZipfRead-4       28711682         60.4 ns/op       1 B/op      0 allocs/op
BenchmarkCaches/GroupCacheZipfRead-4       10563777        126 ns/op        0 B/op      0 allocs/op
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;5-展望&quot;&gt;5. 展望&lt;/h2&gt;

&lt;p&gt;缓存有很多种，本文只介绍了 Golang 生态下几个著名的 Cache，其中 FreeCache 跟 CCache 是 LRU 策略，而 BigCache 是 FIFO 策略。但是仍然可以有更好的淘汰策略值得探索，例如 Java 生态下非常有名的 Caffeine，使用了 Tiny-LFU（作者声称已经接近最优解），但是似乎 Golang 生态下的经过工业级验证过的使用其他更优淘汰策略的 Cache 还是欠缺的。&lt;/p&gt;

&lt;h2 id=&quot;6-reference&quot;&gt;6. Reference&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;https://www.openmymind.net/Shard-Your-Hash-table-to-reduce-write-locks/&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;https://www.openmymind.net/High-Concurrency-LRU-Caching/&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;http://highscalability.com/blog/2016/1/25/design-of-a-modern-cache.html&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;https://dgraph.io/blog/post/caching-in-go/&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;https://go101.org/article/memory-layout.html，https://ms2008.github.io/2019/08/01/golang-memory-alignment/&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;https://github.com/dgraph-io/benchmarks/tree/master/cachebench&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;https://blog.golang.org/ismmkeynote&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;
</description>
			<pubDate>Sat, 18 Jul 2020 00:00:00 +0000</pubDate>
			<link>https://sarkerson.github.io/2020/07/18/Caching-in-go/</link>
			<guid isPermaLink="true">https://sarkerson.github.io/2020/07/18/Caching-in-go/</guid>
        
			<category>golang</category>
        
        
		</item>
    
		<item>
			<title>开发环境：用 Docker 搭建编程环境</title>
			<description>&lt;h1 id=&quot;开发环境用-docker-搭建编程环境&quot;&gt;开发环境：用 Docker 搭建编程环境&lt;/h1&gt;

&lt;h2 id=&quot;1-docker-简介&quot;&gt;1. Docker 简介&lt;/h2&gt;

&lt;h3 id=&quot;11-什么是-docker&quot;&gt;1.1. 什么是 Docker&lt;/h3&gt;

&lt;p&gt;Docker 是什么？&lt;a href=&quot;https://www.zhihu.com/question/28300645/answer/67707287&quot;&gt;如何通俗解释Docker是什么？ - 刘允鹏的回答 - 知乎&lt;/a&gt; 这个回答通俗易懂地从环境依赖、系统依赖、与虚拟机的区别等方面介绍了 Docker 的作用。&lt;/p&gt;

&lt;p&gt;此外，在云时代的今天，Docker 与 K8S 更是无缝结合，为我们开发、测试、部署、交付、维护应用提供了便捷的手段。&lt;/p&gt;

&lt;p&gt;不过 K8S 就是另外一个话题了，我们先从 Docker 入手来窥探 Docker 的使用方法以及基本原理。读了本文，你讲了解到：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Docker client 与 daemon 分别是什么，如何工作&lt;/li&gt;
  &lt;li&gt;Docker 常用命令以及一些注意事项&lt;/li&gt;
  &lt;li&gt;为什么要为非 root 身份用户构建环境，以及如何用该身份运行 Docker&lt;/li&gt;
  &lt;li&gt;一些好用的编程工具及配置方法&lt;/li&gt;
  &lt;li&gt;如何在容器中访问及编辑（读写）你宿主机中的代码&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;至于如何安装 Docker，则可以参考官方文档：http://docs.docker.com/engine/installation/&lt;/em&gt;&lt;/p&gt;

&lt;h3 id=&quot;12-docker-如何工作&quot;&gt;1.2. Docker 如何工作？&lt;/h3&gt;

&lt;p&gt;Docker 会在 Linux 虚拟机（VM）中运行。如果你使用的是 Mac 或者 Windows，Docker 会为你创建一个 VM 并且在其中运行 Docker 守护进程（Docker Daemon），相当于一个服务端程序，而 Docker 会开另外一个客户端进程供用户交互。&lt;/p&gt;

&lt;p&gt;首先，如果从我们编写的 Dockerfile 来编译镜像的话，则执行 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker build&lt;/code&gt;。编译过程并不是直接在 client 端运行的。当用户执行 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker build&lt;/code&gt; 时，client 会把所有依赖文件上传给 daemon。daemon 与 client 可以不在同一台机器上面，这意味着上传过程可能需要依赖网络传输，因此&lt;strong&gt;尽量不要依赖不必要文件，这会直接影响编译速度&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;在编译时，Docker 会把基础镜像（Base Image）从镜像仓库拉取到本地。&lt;strong&gt;镜像并不是单一的一个二进制文件，而是由多个层组成。&lt;/strong&gt;多个镜像可能共享同一个层，这一特点使得镜像存储、传输都非常轻量级。例如你使用同一个基础镜像创建多个镜像，那么基础镜像只会存储一次。每次从镜像仓库如 Docker Hub 拉取的时候，只会拉取本地没有的层。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://i.ibb.co/8c323hS/200626-0.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;其次是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker run&lt;/code&gt; 命令，该命令用来运行某一镜像（image）。当用户执行 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker run&lt;/code&gt; 命令时，docker 首先会检查镜像是否已经在本地，如果不在本地的话，则从 Docker Hub 拉取镜像到本地，然后开启容器进程运行。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://i.ibb.co/qd3LcMv/200626-1.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;13-dockerfile-基本命令及示例&quot;&gt;1.3. Dockerfile 基本命令及示例&lt;/h3&gt;

&lt;h4 id=&quot;run&quot;&gt;RUN&lt;/h4&gt;

&lt;p&gt;每个 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RUN&lt;/code&gt; 命令会在当前镜像的最新的一层执行命令并提交结果，Dockerfile 的下一步执行则会基于上一步提交的镜像进行。&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RUN&lt;/code&gt; 命令有两种格式：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RUN &amp;lt;command&amp;gt;&lt;/code&gt; ，或者称为 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;shell&lt;/code&gt; 格式：默认使用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/bin/sh&lt;/code&gt; 来执行&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RUN [&quot;executable&quot;, &quot;param1&quot;, &quot;param2&quot;]&lt;/code&gt; ，或者称为 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;exec&lt;/code&gt; 格式&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;注意的是，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RUN&lt;/code&gt; 命令默认使用的 shell 是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/bin/sh&lt;/code&gt; ，这意味着某些 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/bin/sh&lt;/code&gt;  不支持的命令则执行会报错，例如 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;source &lt;/code&gt;。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;source&lt;/code&gt; 命令默认的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/bin/sh&lt;/code&gt; 是不支持的，因此必须切换为 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/bin/bash&lt;/code&gt; 或其他支持的 shell，如：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RUN /bin/bash -c &apos;source $HOME/.bashrc; echo $HOME&apos;&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;另外， &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;exec&lt;/code&gt; 格式是使用 JSON 来解析的，因此命令参数要使用双引号 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RUN [&quot;x&quot;, &quot;xx&quot;, &quot;xxx&quot;]&lt;/code&gt;；并且，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;exec&lt;/code&gt; 格式不会直接调用 shell，因此一些 shell 处理是不会执行的，例如参数替换，命令 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RUN [ &quot;echo&quot;, &quot;$HOME&quot; ]&lt;/code&gt; 是不会对 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$HOME&lt;/code&gt; 进行替换的。若需要 shell 处理，则需要显式调用 shell，例如 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RUN [ &quot;sh&quot;, &quot;-c&quot;, &quot;echo $HOME&quot; ]&lt;/code&gt;，因为指定了 shell 之后，是该 shell 来进行命令解析，而不是 Docker。&lt;/p&gt;

&lt;h4 id=&quot;env&quot;&gt;ENV&lt;/h4&gt;

&lt;p&gt;每一行 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ENV&lt;/code&gt; 都会创建一个中间层，就像 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RUN&lt;/code&gt; 一样，这意味着即使你在未来步骤 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;unset&lt;/code&gt; 了该变量，这个变量依然会保留在前面那层，并且最终可以读到。你可以使用下面 Dockerfile 对其进行验证：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;FROM alpine
ENV ADMIN_USER=&quot;mark&quot;
RUN echo $ADMIN_USER &amp;gt; ./mark
RUN unset ADMIN_USER
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;为了避免这样的情况，我们可以在同一层新建、使用、销毁一个环境变量：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;FROM alpine
RUN export ADMIN_USER=&quot;mark&quot; \
    &amp;amp;&amp;amp; echo $ADMIN_USER &amp;gt; ./mark \
    &amp;amp;&amp;amp; unset ADMIN_USER
CMD sh
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;add&quot;&gt;ADD&lt;/h4&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ADD&lt;/code&gt; 用来拷贝文件、目录、甚至 URLs，并添加到镜像的文件系统中。该命令也有两种形式：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ADD [--chown=&amp;lt;user&amp;gt;:&amp;lt;group&amp;gt;] &amp;lt;src&amp;gt;... &amp;lt;dest&amp;gt;&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ADD [--chown=&amp;lt;user&amp;gt;:&amp;lt;group&amp;gt;] [&quot;&amp;lt;src&amp;gt;&quot;,... &quot;&amp;lt;dest&amp;gt;&quot;]&lt;/code&gt; ，这种格式通常用来拷贝文件名带空格的文件。&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;2-使用非-root-身份运行-docker&quot;&gt;2. 使用非 root 身份运行 Docker&lt;/h2&gt;

&lt;p&gt;为什么需要使用非 root 身份运行 Docker？其实就本文章的目标而言，此举是非必要的，即，完全可以在 root 身份下直接进行，并把环境依赖安装在 root 环境中，这当然是没有问题的。但是，如果在未来某些场景中，如果用户直接以 root 身份访问生产环境的 Docker，那么有时候后果是难以设想的。&lt;/p&gt;

&lt;p&gt;并且，在许多场景中，我们需要为用户提供统一的访问者身份，并部署相应的环境依赖，满足访问者的部分需求。因此，我们有必要学习一下，如何为某一用户部署环境，并使用该用户的身份运行 Docker。&lt;/p&gt;

&lt;p&gt;此小节我们会简明介绍如何以非 root 身份运行 Docker。&lt;/p&gt;

&lt;p&gt;默认的话，Docker 会用 root 身份运行，用户 ID 为 0，这个可以使用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;id -u &amp;lt;username&amp;gt;&lt;/code&gt; 来查看。&lt;/p&gt;

&lt;p&gt;我们可以使用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--user username:usergroup&lt;/code&gt; 参数来指定用户身份运行。但是，如果直接使用用户名、组名的话，容器是找不到该用户名的，因为没有对应的 ID。因此，我们需要指定用户 ID 及组 ID。&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# 添加组 yaoleiqi，并指定 ID
RUN groupadd -f -g 200 yaoleiqi

# 添加用户 yaoleiqi 并指定 home 目录，指定用户 ID、所属的组
RUN useradd -m -d /home/yaoleiqi -u 200 yaoleiqi -g yaoleiqi

# 将 yaoleiqi 添加到 sudo 组
RUN usermod -aG sudo yaoleiqi

# 更改用户 yaoleiqi 的密码
RUN echo &quot;yaoleiqi:yao12345&quot; | chpasswd
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;完整语句如下：&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker run -it --user 200:200 awesome_dev:latest&lt;/code&gt;&lt;/p&gt;

&lt;h2 id=&quot;3-编写我们的环境&quot;&gt;3. 编写我们的环境&lt;/h2&gt;

&lt;h4 id=&quot;31-构建-base-镜像&quot;&gt;3.1. 构建 base 镜像&lt;/h4&gt;

&lt;p&gt;base 镜像可以是我们其他镜像的基础，构建一些常用并且通用的环境。&lt;/p&gt;

&lt;p&gt;在本示例中，其实只做了两件事：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;安装环境依赖&lt;/li&gt;
  &lt;li&gt;创建用户、组并进行一些 home 目录、密码、权限的配置&lt;/li&gt;
&lt;/ol&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;FROM ubuntu:18.04
MAINTAINER yaoleiqi &quot;yaoleiqi@qq.com&quot;

# build requirements
RUN apt-get update &amp;amp;&amp;amp; apt-get install -y \
    ninja-build \
    gettext \
    libtool \
    libtool-bin \
    autoconf \
    automake \
    cmake \
    g++ \
    pkg-config \
    unzip \
    git \
    sudo \
    build-essential \
    checkinstall \
    wget \
    vim \
  &amp;amp;&amp;amp; rm -rf /var/lib/apt/lists/*

# make the &quot;en_US.UTF-8&quot; locale so myenv will be utf-8 enabled by default
RUN apt-get update &amp;amp;&amp;amp; apt-get install -y locales \
  &amp;amp;&amp;amp; rm -rf /var/lib/apt/lists/* \
  &amp;amp;&amp;amp; localedef -i en_US -c -f UTF-8 -A /usr/share/locale/locale.alias en_US.UTF-8
ENV LANG en_US.utf8

# build python3
# This hack is widely applied to avoid python printing issues in docker containers.
# See: https://github.com/Docker-Hub-frolvlad/docker-alpine-python3/pull/13
ENV PYTHONUNBUFFERED=1

RUN apt-get update &amp;amp;&amp;amp; apt-get install -y \
    python3-pip \
    python-dev \
    python3-dev \
  &amp;amp;&amp;amp; rm -rf /var/lib/apt/lists/* \
  &amp;amp;&amp;amp; cd /usr/local/bin \
  &amp;amp;&amp;amp; sudo ln -s /usr/bin/python3 python \
  &amp;amp;&amp;amp; sudo pip3 install --upgrade pip

# I like python, so I install the latest version of python
RUN apt-get update &amp;amp;&amp;amp; apt-get install -y \
    software-properties-common -y \
  &amp;amp;&amp;amp; add-apt-repository ppa:deadsnakes/ppa -y \
  &amp;amp;&amp;amp; apt-get update &amp;amp;&amp;amp; apt-get install -y \
    python3.8 \
    python3.8-dev \
  &amp;amp;&amp;amp; rm -rf /var/lib/apt/lists/*

# install other dev tools
RUN apt-get update &amp;amp;&amp;amp; apt-get install -y \
    tmux \
    zsh \
    neovim \
    curl \
    silversearcher-ag \
    python3-neovim \
  &amp;amp;&amp;amp; rm -rf /var/lib/apt/lists/*

# add group
RUN groupadd -f -g 200 yaoleiqi

# add user
RUN useradd -m -d /home/yaoleiqi -u 200 yaoleiqi -g yaoleiqi
RUN usermod -aG sudo yaoleiqi
RUN echo &quot;yaoleiqi:yao12345&quot; | chpasswd
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;32-构建-dev-环境&quot;&gt;3.2. 构建 dev 环境&lt;/h4&gt;

&lt;p&gt;dev 环境则进行一些定制化的配置，在本示例中，将构建特定用户的定制化环境依赖，即为用户 yaoleiqi 构建 vim 环境、virtualenv 环境等。&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;FROM yaoleiqi/devbase:latest
MAINTAINER yaoleiqi &quot;yaoleiqi@qq.com&quot;

ENV HOME /home/yaoleiqi

# build virtual enviroment
RUN python3.8 -m pip install virtualenv \
  &amp;amp;&amp;amp; virtualenv -p `which python3.8` $HOME/myenv

# install oh-my-zsh
COPY install_zsh.sh /tmp/
RUN chmod +x /tmp/install_zsh.sh \
  &amp;amp;&amp;amp; sh /tmp/install_zsh.sh

# install tmux
RUN cd $HOME \
  &amp;amp;&amp;amp; git clone https://github.com/gpakosz/.tmux.git \
  &amp;amp;&amp;amp; ln -s -f .tmux/.tmux.conf \
  &amp;amp;&amp;amp; cp .tmux/.tmux.conf.local .

# install golang
RUN wget https://dl.google.com/go/go1.14.4.linux-amd64.tar.gz -O /tmp/go.tar.gz \
  &amp;amp;&amp;amp; tar -C /usr/local -xzf /tmp/go.tar.gz \
  &amp;amp;&amp;amp; mkdir $HOME/go \
  &amp;amp;&amp;amp; echo &quot;export PATH=$PATH:/usr/local/go/bin\n\
export GOROOT=/usr/local/go\n\
export GOPATH=$HOME/go\n&quot; &amp;gt;&amp;gt; $HOME/.profile

# install vim plugins
COPY vimrc.sh $HOME/.vimrc

# git config
COPY gitconfig $HOME/.gitconfig

# build vim plugins
# install vundle
RUN mkdir -p $HOME/.config/nvim/bundle \
  &amp;amp;&amp;amp; git clone https://github.com/VundleVim/Vundle.vim.git $HOME/.config/nvim/bundle/Vundle.vim \
  &amp;amp;&amp;amp; mkdir -p $HOME/.vim/bundle \
  &amp;amp;&amp;amp; git clone https://github.com/ycm-core/YouCompleteMe.git $HOME/.vim/bundle/YouCompleteMe \
  &amp;amp;&amp;amp; cd $HOME/.vim/bundle/YouCompleteMe \
  &amp;amp;&amp;amp; git submodule update --init --recursive \
  &amp;amp;&amp;amp; git clone https://github.com/morhetz/gruvbox.git $HOME/.vim/bundle/gruvbox

RUN /bin/bash -c &apos;source $HOME/.profile; \
  $HOME/myenv/bin/python $HOME/.vim/bundle/YouCompleteMe/install.py --go-completer&apos;

RUN runuser -l yaoleiqi -c &apos;vim +PluginInstall +qall &amp;gt; /dev/null&apos; \
  &amp;amp;&amp;amp; runuser -l yaoleiqi -c &apos;vim +GoInstallBinaries +qall &amp;gt; /dev/null&apos;

RUN chown -R yaoleiqi:yaoleiqi $HOME
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;4-运行我们的环境&quot;&gt;4. 运行我们的环境&lt;/h2&gt;

&lt;p&gt;运行环境的前提是，已经安装好 Docker。那么运行以下几个命令即可进入我们的开发环境：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;git clone https://github.com/SarKerson/myenv.git
docker build -t --no-cache yaoleiqi/devbase:latest . &amp;amp;&amp;amp; docker build  -t --no-cache awesome_dev:latest ./awesome_dev/
docker run -it --user 200:200 awesome_dev:latest
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;然后，就可以放飞自我的开始码代码了！等等！在此之前我们先手动安装一下 vim 环境，由于 vimrc 已经配置好，只需要执行以下两步即可（中间会遇到）：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# 安装插件
runuser -l yaoleiqi -c &apos;vim +PluginInstall +qall &amp;gt; /dev/null&apos;
# optinal：安装 vim-go，如果你需要写 golang 代码，那么推荐使用
runuser -l yaoleiqi -c &apos;vim +GoInstallBinaries +qall &amp;gt; /dev/null
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;5-使用-volume-持久化容器中的文件&quot;&gt;5. 使用 Volume 持久化容器中的文件&lt;/h2&gt;

&lt;p&gt;根据官方文档介绍，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;volumes&lt;/code&gt; 是推荐的持久化手段，并且由 Docker 进行管理。这意味着数据在 mount 的同时，Docker 可以做一些其他工作，例如文件系统兼容（Linux 与 Windows 互通）、多个容器共享、文件加密等等，具体可以参考 https://docs.docker.com/storage/volumes/。&lt;/p&gt;

&lt;p&gt;并且，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;volumes&lt;/code&gt; 独立于容器的生命周期，是插拔式的存在，不会增加容器的大小。但是，如果你不需要持久化数据的话，最好还是使用 tmpfs 方式，因为 tmpfs 是直接内存操作，速度会比写入硬盘快得多。因此，我们也可以在容器里直接 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git clone&lt;/code&gt; 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git push&lt;/code&gt; 来编辑我们的代码。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://docs.docker.com/storage/images/types-of-mounts-volume.png&quot; alt=&quot;volumes on the Docker host&quot; /&gt;&lt;/p&gt;

&lt;p&gt;但是，对于一些数据文件，例如运行结果或者配置文件，我们通常还是可以利用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;volumes&lt;/code&gt; 来持久化、或者多个容器共享的。具体使用方式是，在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker run&lt;/code&gt; 时使用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-v&lt;/code&gt; 标志，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker run -v /host/directory:/container/directory -other -options image_name command_to_run&lt;/code&gt;，这样则会将宿主机的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/host/directory&lt;/code&gt; 映射到容器中的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/container/directory&lt;/code&gt;，在容器中对该目录的任何修改，都会同步映射到宿主机上。&lt;/p&gt;

&lt;p&gt;另外，我们也可以直接创建 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;volumes&lt;/code&gt; ，在任何时候挂载到任何容器上。例如，创建一个名为 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;my-vol&lt;/code&gt; 的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;volume&lt;/code&gt;：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;docker volume create my-vol
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;然后列出所有 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;volume&lt;/code&gt;：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ docker volume ls
local               my-vol
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;查看一个 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;volume&lt;/code&gt; 的具体信息：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ docker volume inspect my-vol
[
    {
        &quot;Driver&quot;: &quot;local&quot;,
        &quot;Labels&quot;: {},
        &quot;Mountpoint&quot;: &quot;/var/lib/docker/volumes/my-vol/_data&quot;,
        &quot;Name&quot;: &quot;my-vol&quot;,
        &quot;Options&quot;: {},
        &quot;Scope&quot;: &quot;local&quot;
    }
]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;我们还可以运行一个挂载 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;volume&lt;/code&gt; 的容器。下面命令将 宿主机的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;my-vol&lt;/code&gt; 挂载到容器的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/app&lt;/code&gt;。&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ docker run -d \
  --name devtest \
  --mount source=my-vol,target=/app \
  nginx:latest
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;6-references&quot;&gt;6. References&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;https://medium.com/better-programming/running-a-container-with-a-non-root-user-e35830d1f42a&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;https://medium.com/redbubble/running-a-docker-container-as-a-non-root-user-7d2e00f8ee15&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;https://www.cyberciti.biz/open-source/command-line-hacks/linux-run-command-as-different-user/&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;https://docs.docker.com/storage/volumes/&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;
</description>
			<pubDate>Fri, 26 Jun 2020 00:00:00 +0000</pubDate>
			<link>https://sarkerson.github.io/2020/06/26/%E4%BD%BF%E7%94%A8-Docker-%E6%90%AD%E5%BB%BA%E8%87%AA%E5%B7%B1%E7%9A%84%E7%BC%96%E7%A8%8B%E7%8E%AF%E5%A2%83/</link>
			<guid isPermaLink="true">https://sarkerson.github.io/2020/06/26/%E4%BD%BF%E7%94%A8-Docker-%E6%90%AD%E5%BB%BA%E8%87%AA%E5%B7%B1%E7%9A%84%E7%BC%96%E7%A8%8B%E7%8E%AF%E5%A2%83/</guid>
        
			<category>docker</category>
        
        
		</item>
    
	</channel>
</rss>
