Jekyll2024-01-05T23:08:31+08:00https://ironartisan.github.io/feed.xmlironartisanironartisan的博客ironartisan书生·浦语大模型全链路开源开放体系2024-01-04T00:00:00+08:002024-01-04T00:00:00+08:00https://ironartisan.github.io/2024/01/04/%E5%A4%A7%E6%A8%A1%E5%9E%8B%E5%85%A8%E9%93%BE%E8%B7%AF%E5%BC%80%E6%BA%90%E5%BC%80%E6%94%BE%E4%BD%93%E7%B3%BB<blockquote>
<p>上海人工智能实验室重磅推出书生·浦语大模型实战营,为广大开发者搭建大模型学习和实践开发的平台,两周时间带你玩转大模型微调、部署与评测全链路。</p>
</blockquote>
<h2 id="大模型成为发展通用人工智能的重要途径">大模型成为发展通用人工智能的重要途径</h2>
<ul>
<li>专用模型:针对特定任务,一个模型解决一个问题</li>
<li>通用大模型:一个模型应对多种任务、多种模态</li>
</ul>
<h2 id="从模型到应用">从模型到应用</h2>
<p><img src="https://cdn.jsdelivr.net/gh/ironartisan/picRepo/20240104151128.png" alt="20240104151128" /></p>
<ul>
<li>模型选型
<ul>
<li>根据模型特点和应用场景选择合适的模型</li>
</ul>
</li>
<li>评估业务场景
<ul>
<li>简单:能直接满足业务需求,判断是否需要环境交互
<ul>
<li>不需要环境交互,则直接进行模型评测</li>
<li>需要环境交互,构建智能体解决</li>
</ul>
</li>
<li>复杂:需要微调</li>
</ul>
</li>
<li>微调
<ul>
<li>算法足够:进行续训和全参数微调</li>
<li>算力不足:利用LoRA、QLoRA等算法进行部分参数微调</li>
</ul>
</li>
<li>构建智能体
<ul>
<li>需要环境交互,调用外部API、操作数据库等操作</li>
</ul>
</li>
<li>模型评测
<ul>
<li>评测模型能力是否能满足业务需求,若满足,则上线,进行模型部署</li>
</ul>
</li>
<li>模型部署
<ul>
<li>考虑硬件环境,对模型进行量化、加速等操作</li>
</ul>
</li>
</ul>
<h2 id="全链条开源开放体系">全链条开源开放体系</h2>
<p><img src="https://cdn.jsdelivr.net/gh/ironartisan/picRepo/20240104173023.png" alt="20240104173023" /></p>
<h3 id="全链条开源开放体系数据">全链条开源开放体系|数据</h3>
<p>书生·万卷1.0为书生·万卷多模态语料库的首个开源版本,包含文本数据集、图文数据集、视频数据集三部分,数据总量超过2TB。基于大模型数据联盟构建的语料库,上海AI实验室对其中部分数据进行细粒度清洗、去重以及价值对齐,形成了书生·万卷1.0,具备多元融合、精细处理、价值对齐、易用高效等四大特征。</p>
<p><strong>- 在多元融合方面</strong>,书生·万卷1.0包含文本、图文、视频等多模态数据,范围覆盖科技、文学、媒体、教育、法律等多个领域,在训练提升模型知识含量、逻辑推理和泛化能力方面具有显著效果。<br />
<strong>- 在精细处理方面</strong>,书生·万卷1.0经历了语言甄别、正文抽取、格式标准化、基于规则及模型的数据过滤与清洗、多尺度去重、数据质量评估等精细化数据处理环节,因而能更好地适配后续的模型训练需求。<br />
<strong>- 在价值对齐方面</strong>,研究人员在书生·万卷1.0的构建过程中,着眼于内容与中文主流价值观的对齐,通过算法与人工评估结合的方式,提升了语料的纯净度。<br />
<strong>- 在易用高效方面</strong>,研究人员在书生·万卷1.0采用统一格式,并提供详细的字段说明和工具指导,使其兼顾了易用性和效率,可快速应用于语言、多模态等大模型训练。</p>
<p>目前,书生·万卷1.0已被应用于书生·多模态、书生·浦语的训练。通过对高质量语料的“消化”,书生系列模型在语义理解、知识问答、视觉理解、视觉问答等各类生成式任务表现出的优异性能。</p>
<p>下载地址:<a href="https://opendatalab.org.cn/WanJuan1.0">https://opendatalab.org.cn/WanJuan1.0</a></p>
<p><img src="https://cdn.jsdelivr.net/gh/ironartisan/picRepo/20240104170644.png" alt="20240104170644" /></p>
<h3 id="全链条开源开放体系预训练">全链条开源开放体系|预训练</h3>
<p>InternLM 是一个开源的轻量级训练框架,旨在支持大模型训练而无需大量的依赖。通过单一的代码库,它支持在拥有数千个 GPU 的大型集群上进行预训练,并在单个 GPU 上进行微调,同时实现了卓越的性能优化。在1024个 GPU 上训练时,InternLM 可以实现近90%的加速效率。</p>
<p>基于InternLM训练框架,已经发布了两个开源的预训练模型:InternLM-7B 和 InternLM-20B。</p>
<p>InternLM 深度整合了 Flash-Attention, Apex 等高性能模型算子,提高了训练效率。通过构建 Hybrid Zero 技术,实现计算和通信的高效重叠,大幅降低了训练过程中的跨节点通信流量。InternLM 支持 7B 模型从 8 卡扩展到 1024 卡,千卡规模下加速效率可高达 90%,训练吞吐超过 180TFLOPS,平均单卡每秒处理的 token 数量超过3600。</p>
<p>项目地址:<a href="https://github.com/InternLM/InternLM">https://github.com/InternLM/InternLM</a></p>
<p><img src="https://cdn.jsdelivr.net/gh/ironartisan/picRepo/20240104171301.png" alt="20240104171301" /></p>
<h3 id="全链条开源开放体系微调">全链条开源开放体系|微调</h3>
<p>XTuner 是一个轻量级微调大语言模型的工具库,由 <a href="https://github.com/open-mmlab/mmrazor">MMRazor</a> 和 <a href="https://github.com/open-mmlab/mmdeploy">MMDeploy</a> 团队联合开发。</p>
<ul>
<li><strong>轻量级</strong>: 支持在消费级显卡上微调大语言模型。对于 7B 参数量,微调所需的最小显存仅为 <strong>8GB</strong>,这使得用户可以使用几乎任何显卡(甚至免费资源,例如Colab)来微调获得自定义大语言模型助手。</li>
<li><strong>多样性</strong>: 支持多种<strong>大语言模型</strong>(<a href="https://huggingface.co/internlm">InternLM</a>、<a href="https://huggingface.co/meta-llama">Llama2</a>、<a href="https://huggingface.co/THUDM">ChatGLM</a>、<a href="https://huggingface.co/Qwen">Qwen</a>、<a href="https://huggingface.co/baichuan-inc">Baichuan2</a>, …),<strong>数据集</strong>(<a href="https://huggingface.co/datasets/fnlp/moss-003-sft-data">MOSS_003_SFT</a>, <a href="https://huggingface.co/datasets/tatsu-lab/alpaca">Alpaca</a>, <a href="https://huggingface.co/datasets/WizardLM/WizardLM_evol_instruct_V2_196k">WizardLM</a>, <a href="https://huggingface.co/datasets/timdettmers/openassistant-guanaco">oasst1</a>, <a href="https://huggingface.co/datasets/garage-bAInd/Open-Platypus">Open-Platypus</a>, <a href="https://huggingface.co/datasets/HuggingFaceH4/CodeAlpaca_20K">Code Alpaca</a>, <a href="https://huggingface.co/datasets/burkelibbey/colors">Colorist</a>, …)和<strong>微调算法</strong>(<a href="http://arxiv.org/abs/2305.14314">QLoRA</a>、<a href="http://arxiv.org/abs/2106.09685">LoRA</a>),支撑用户根据自身具体需求选择合适的解决方案。</li>
<li><strong>兼容性</strong>: 兼容 <a href="https://github.com/microsoft/DeepSpeed">DeepSpeed</a> 🚀 和 <a href="https://huggingface.co">HuggingFace</a> 🤗 的训练流程,支撑用户无感式集成与使用。</li>
<li><strong>轻量级</strong>:支持在消费级显卡上的参数大语言模型。对于7B参数量,所需所需的最小显存电流<strong>8GB</strong>,这使得用户可以使用几乎任何显卡(甚至免费)资源,例如Colab)来获得自定义大语言模型助手。 - <strong>多样性</strong>:支持多种<strong>大语言模型</strong>(<a href="https://huggingface.co/internlm">InternLM</a>、<a href="https://huggingface.co/meta-llama"> Llama2</a>、<a href="https://huggingface.co/THUDM">ChatGLM</a>、<a href="https://huggingface.co/Qwen">Qwen</a>、<a href="https://huggingface.co/baichuan-inc">Baichuan2</a>, …),<strong>数据集</strong>(<a href="https://huggingface.co/datasets/fnlp/moss-003-sft-data">MOSS_003_SFT</a>, <a href="https://huggingface.co/datasets/tatsu-lab/alpaca">羊驼</a>、<a href="https://huggingface.co/datasets/WizardLM/WizardLM_evol_instruct_V2_196k">WizardLM</a>、<a href="https://huggingface. co/datasets/timdettmers/openassistant-guanaco),[Open-Platypus](https://huggingface.co/datasets/garage-bAInd/Open-Platypus),[代码羊驼](https://huggingface.co/datasets) /HuggingFaceH4/CodeAlpaca_20K">oasst1</a>, <a href="https://huggingface.co/datasets/burkelibbey/colors">Colorist</a>, …)和<strong>变形算法</strong>(<a href="http://arxiv.org/abs">QLoRA</a> /2305.14314)、<a href="http://arxiv.org/abs/2106.09685">LoRA</a>),支持用户根据自身具体需求选择合适的解决方案。 - <strong>兼容性</strong>: 兼容 <a href="https:// /github.com/microsoft/DeepSpeed">DeepSpeed</a> 🚀 和 <a href="https://huggingface.co">HuggingFace</a> 🤗 的训练流程,支持用户无感集成方式与使用。</li>
</ul>
<p>项目地址:<a href="https://github.com/InternLM/xtuner">https://github.com/InternLM/xtuner</a></p>
<h3 id="全链条开源开放体系评测">全链条开源开放体系|评测</h3>
<p>OpenCompass 是面向大模型评测的一站式平台。其主要特点如下:</p>
<ul>
<li>
<p>开源可复现:提供公平、公开、可复现的大模型评测方案</p>
</li>
<li>
<p>全面的能力维度:五大维度设计,提供 70+ 个数据集约 40 万题的的模型评测方案,全面评估模型能力</p>
</li>
<li>
<p>丰富的模型支持:已支持 20+ HuggingFace 及 API 模型</p>
</li>
<li>
<p>分布式高效评测:一行命令实现任务分割和分布式评测,数小时即可完成千亿模型全量评测</p>
</li>
<li>
<p>多样化评测范式:支持零样本、小样本及思维链评测,结合标准型或对话型提示词模板,轻松激发各种模型最大性能</p>
</li>
<li>
<p>灵活化拓展:想增加新模型或数据集?想要自定义更高级的任务分割策略,甚至接入新的集群管理系统?OpenCompass 的一切均可轻松扩展!</p>
</li>
</ul>
<p>项目地址:<a href="https://github.com/open-compass/opencompass">https://github.com/open-compass/opencompass</a></p>
<h3 id="全链条开源开放体系部署">全链条开源开放体系|部署</h3>
<p>LMDeploy 由 MMDeploy 和 MMRazor 团队联合开发,是涵盖了 LLM 任务的全套轻量化、部署和服务解决方案。 这个强大的工具箱提供以下核心功能:</p>
<ul>
<li>
<p>高效推理引擎 TurboMind:开发了 Persistent Batch(即 Continuous Batch),Blocked K/V Cache,动态拆分和融合,张量并行,高效的计算 kernel等重要特性,保障了 LLMs 推理时的高吞吐和低延时。</p>
</li>
<li>
<p>有状态推理:通过缓存多轮对话过程中 attention 的 k/v,记住对话历史,从而避免重复处理历史会话。显著提升长文本多轮对话场景中的效率。</p>
</li>
<li>
<p>量化:LMDeploy 支持多种量化方式和高效的量化模型推理。在不同规模的模型上,验证了量化的可靠性。</p>
</li>
</ul>
<p>项目地址:<a href="https://github.com/InternLM/lmdeploy">https://github.com/InternLM/lmdeploy</a></p>
<h3 id="全链条开源开放体系智能体">全链条开源开放体系|智能体</h3>
<p>开源了两个Agent相关的框架,分别是Lagent和Agent Lego</p>
<p>Lagent 是一个轻量级、开源的基于大语言模型的智能体(agent)框架,支持用户快速地将一个大语言模型转变为多种类型的智能体,并提供了一些典型工具为大语言模型赋能。</p>
<ul>
<li>支持高性能推理 lmdeploy turbomind</li>
<li>实现了ReAct,AutoGPT 和 ReWoo 等多种类型的智能体</li>
<li>框架简单易拓展,支持 Python 解释器、API 调用和搜索三类常用典型工具</li>
<li>灵活支持多个大语言模型. 包括 InternLM、Llama-2 等开源模型和 GPT-4/3.5 等基于 API 的闭源模型</li>
</ul>
<p>项目地址:<a href="https://github.com/InternLM/lagent">https://github.com/InternLM/lagent</a> </p>
<p>Agent Lego 是一个开源的多功能工具 API 库,用于扩展和增强基于大型语言模型(LLM)的智能体(Agent),具有以下突出特点:</p>
<ul>
<li>丰富的多模态扩展工具集,包括视觉感知、图像生成和编辑、语音处理和视觉语言推理等</li>
<li>灵活的工具接口,允许用户轻松扩展具有任意类型参数和输出的自定义工具</li>
<li>与基于LLM的代理程序框架轻松集成,如 LangChain、Transformers Agent、Lagent</li>
<li>支持部署工具服务和远程访问</li>
</ul>
<p>项目地址:<a href="https://github.com/InternLM/agentlego">https://github.com/InternLM/agentlego</a></p>
<h2 id="总结">总结</h2>
<p>书生·浦语全链条开源开放体系提供大模型一站式解决方案,包含了数据集、预训练、微调、部署、评测和应用等开源框架:</p>
<ul>
<li>数据(书生·万卷):2TB数据,涵盖多种模态与任务</li>
<li>预训练(InternLM-Train):并行训练,极致优化,速度达到 3600 tokens/sec/gpu</li>
<li>微调(XTuner):支持 全参数微调,支持LoRA等低成本微调</li>
<li>部署(LMDeploy):全链路部署,性能领先,每秒生成 2000+ tokens</li>
<li>评测(OpenCompass):全方位评测,性能可复现,80 套评测集,40 万道题目</li>
<li>应用(Lagent、AgentLego):支持多种智能体,支持代码解释器等多种工具</li>
</ul>
<h2 id="参考链接">参考链接</h2>
<ul>
<li><a href="https://www.bilibili.com/video/BV1Rc411b7ns/">陈恺老师 书生·浦语大模型全链路开源体系</a></li>
<li><a href="https://github.com/InternLM/tutorial">InternLM Tutorial</a></li>
<li><a href="https://opendatalab.org.cn/WanJuan1.0">书生·万卷</a></li>
<li><a href="https://github.com/InternLM/InternLM">InternLM</a></li>
<li><a href="https://github.com/InternLM/xtuner">XTuner</a></li>
<li><a href="https://github.com/InternLM/lmdeploy">LMDeploy</a></li>
<li><a href="https://github.com/open-compass/opencompass">OpenCompass</a></li>
<li><a href="https://github.com/InternLM/lagent">Lagent</a></li>
<li><a href="https://github.com/InternLM/agentlego">Agent Lego</a></li>
</ul>ironartisan上海人工智能实验室重磅推出书生·浦语大模型实战营,为广大开发者搭建大模型学习和实践开发的平台,两周时间带你玩转大模型微调、部署与评测全链路。docker食用openpose1.6.0教程2023-04-20T00:00:00+08:002023-04-20T00:00:00+08:00https://ironartisan.github.io/2023/04/20/docker%E5%91%BD%E4%BB%A4<meta name="referrer" content="no-referrer" />
<h2 id="docker创建openpose环境">docker创建openpose环境</h2>
<h3 id="创建image">创建IMAGE</h3>
<p>使用Dockerfile创建images</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker build <span class="nt">-t</span> openpose16u18 <span class="nb">.</span>
</code></pre></div></div>
<h3 id="创建container">创建CONTAINER</h3>
<p>根据创建好的image创建自己的容器</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nvidia-docker run <span class="nt">-tdi</span> <span class="nt">-v</span> ~/data:/openpose/data <span class="nt">--net</span><span class="o">=</span>host <span class="nt">-e</span> DISPLAY <span class="nt">--runtime</span><span class="o">=</span>nvidia <span class="nt">--name</span><span class="o">=</span>openpose16u18 openpose16u18:latest
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">-v</code>:表示将宿主机的路径<code class="language-plaintext highlighter-rouge">~/data</code>挂载到docker容器的<code class="language-plaintext highlighter-rouge">/openpose/data</code>,即宿主机的<code class="language-plaintext highlighter-rouge">~/data</code>文件与docker中的<code class="language-plaintext highlighter-rouge">/openpose/data</code>文件共享。</p>
<h2 id="openpose使用">openpose使用</h2>
<h3 id="进入到docker容器中">进入到docker容器中</h3>
<p>默认会进入到/openpose文件夹中</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker <span class="nb">exec</span> <span class="nt">-it</span> 4c368d916d185ff /bin/bash
</code></pre></div></div>
<p>运行测试脚本</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd</span> /openpose/build/examples/tutorial_api_python
python3 01_body_from_image.py
</code></pre></div></div>
<p>输出结果:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Starting OpenPose Python Wrapper...
Auto-detecting all available GPUs... Detected 4 GPU<span class="o">(</span>s<span class="o">)</span>, using 4 of them starting at GPU 0.
Body keypoints:
<span class="o">[[[</span>3.2809854e+02 2.1974371e+02 7.5993967e-01]
<span class="o">[</span>3.2289130e+02 2.1718504e+02 8.3385360e-01]
<span class="o">[</span>2.9802701e+02 2.2368245e+02 8.8920242e-01]
<span class="o">[</span>2.7715598e+02 2.5109933e+02 8.6595273e-01]
<span class="o">[</span>2.9809534e+02 2.7328116e+02 8.2083827e-01]
<span class="o">[</span>3.4900360e+02 2.1327135e+02 9.2912471e-01]
<span class="o">[</span>3.6726160e+02 2.3550529e+02 8.7097746e-01]
<span class="o">[</span>3.6850781e+02 2.6154324e+02 8.3065987e-01]
<span class="o">[</span>3.2806213e+02 2.8759146e+02 8.6716896e-01]
<span class="o">[</span>3.1245990e+02 2.8764346e+02 7.8310686e-01]
<span class="o">[</span>3.2936475e+02 3.3851541e+02 8.6039579e-01]
<span class="o">[</span>3.3854834e+02 3.9202173e+02 7.7205557e-01]
<span class="o">[</span>3.3985031e+02 2.8631238e+02 8.6554557e-01]
<span class="o">[</span>3.4376807e+02 3.3462039e+02 7.7605212e-01]
<span class="o">[</span>3.4638205e+02 3.7248010e+02 6.3853985e-01]
<span class="o">[</span>3.2416013e+02 2.1451590e+02 6.8749976e-01]
<span class="o">[</span>3.3074908e+02 2.1451418e+02 6.3172626e-01]
<span class="o">[</span>3.1632730e+02 2.0415282e+02 7.0270222e-01]
<span class="o">[</span>3.3860080e+02 2.0545940e+02 2.5160685e-01]
<span class="o">[</span>3.5936539e+02 3.8163797e+02 3.0262479e-01]
<span class="o">[</span>3.5938715e+02 3.8030585e+02 3.8734350e-01]
<span class="o">[</span>3.4764966e+02 3.7510175e+02 3.5746363e-01]
<span class="o">[</span>3.4114755e+02 4.1029895e+02 6.9135833e-01]
<span class="o">[</span>3.3590115e+02 4.0902325e+02 6.3767231e-01]
<span class="o">[</span>3.3981992e+02 3.9857721e+02 5.6923801e-01]]
<span class="o">[[</span>0.0000000e+00 0.0000000e+00 0.0000000e+00]
<span class="o">[</span>4.2074823e+02 3.0593616e+02 8.6281455e-01]
<span class="o">[</span>4.5077130e+02 3.0857712e+02 8.0427569e-01]
<span class="o">[</span>4.7298636e+02 3.5023221e+02 9.0337217e-01]
<span class="o">[</span>4.4819788e+02 3.6990717e+02 6.9541442e-01]
<span class="o">[</span>3.8944159e+02 2.9939224e+02 8.4941977e-01]
<span class="o">[</span>3.5947684e+02 3.2944754e+02 9.4136912e-01]
<span class="o">[</span>3.8165787e+02 3.5419604e+02 8.3384556e-01]
<span class="o">[</span>4.1292001e+02 3.8290024e+02 7.8305215e-01]
<span class="o">[</span>4.3123010e+02 3.8421210e+02 7.1762729e-01]
<span class="o">[</span>4.2077008e+02 4.4560986e+02 7.9618287e-01]
<span class="o">[</span>0.0000000e+00 0.0000000e+00 0.0000000e+00]
<span class="o">[</span>3.9596503e+02 3.8159378e+02 7.0373863e-01]
<span class="o">[</span>3.8551382e+02 4.3511887e+02 7.6988512e-01]
<span class="o">[</span>3.7378726e+02 4.7693509e+02 2.2625722e-01]
<span class="o">[</span>0.0000000e+00 0.0000000e+00 0.0000000e+00]
<span class="o">[</span>0.0000000e+00 0.0000000e+00 0.0000000e+00]
<span class="o">[</span>4.3515445e+02 2.8369504e+02 6.7381614e-01]
<span class="o">[</span>4.1160095e+02 2.7851880e+02 9.0691555e-01]
<span class="o">[</span>0.0000000e+00 0.0000000e+00 0.0000000e+00]
<span class="o">[</span>0.0000000e+00 0.0000000e+00 0.0000000e+00]
<span class="o">[</span>3.7247885e+02 4.7694193e+02 1.0687940e-01]
<span class="o">[</span>0.0000000e+00 0.0000000e+00 0.0000000e+00]
<span class="o">[</span>0.0000000e+00 0.0000000e+00 0.0000000e+00]
<span class="o">[</span>0.0000000e+00 0.0000000e+00 0.0000000e+00]]
<span class="o">[[</span>0.0000000e+00 0.0000000e+00 0.0000000e+00]
<span class="o">[</span>4.7818686e+02 2.7064902e+02 8.0004317e-01]
<span class="o">[</span>5.0167981e+02 2.6683105e+02 7.8615856e-01]
<span class="o">[</span>5.0557230e+02 2.8631616e+02 4.1359463e-01]
<span class="o">[</span>0.0000000e+00 0.0000000e+00 0.0000000e+00]
<span class="o">[</span>4.5605368e+02 2.7328049e+02 8.1177378e-01]
<span class="o">[</span>4.4161093e+02 3.0716895e+02 5.7765806e-01]
<span class="o">[</span>0.0000000e+00 0.0000000e+00 0.0000000e+00]
<span class="o">[</span>4.7428549e+02 3.3856525e+02 8.4464747e-01]
<span class="o">[</span>4.8600989e+02 3.3856204e+02 8.0585420e-01]
<span class="o">[</span>4.8735986e+02 3.8683725e+02 8.0624354e-01]
<span class="o">[</span>4.9649228e+02 4.4296634e+02 8.5596460e-01]
<span class="o">[</span>4.6247906e+02 3.3857523e+02 7.3071110e-01]
<span class="o">[</span>4.6380847e+02 3.9203394e+02 8.5256344e-01]
<span class="o">[</span>4.6517609e+02 4.4815677e+02 8.0368519e-01]
<span class="o">[</span>0.0000000e+00 0.0000000e+00 0.0000000e+00]
<span class="o">[</span>0.0000000e+00 0.0000000e+00 0.0000000e+00]
<span class="o">[</span>4.8732034e+02 2.4456866e+02 2.3870933e-01]
<span class="o">[</span>4.6514789e+02 2.4456361e+02 8.3356500e-01]
<span class="o">[</span>4.4687830e+02 4.4818286e+02 6.0361665e-01]
<span class="o">[</span>4.4946494e+02 4.5209204e+02 6.8886167e-01]
<span class="o">[</span>4.7293256e+02 4.5470789e+02 8.3570737e-01]
<span class="o">[</span>4.9907031e+02 4.4429254e+02 4.9687880e-01]
<span class="o">[</span>5.0562808e+02 4.4431171e+02 5.9700477e-01]
<span class="o">[</span>4.9649667e+02 4.5205167e+02 8.1779170e-01]]
<span class="o">[[</span>0.0000000e+00 0.0000000e+00 0.0000000e+00]
<span class="o">[</span>3.7371924e+00 3.2548853e+02 3.6309335e-01]
<span class="o">[</span>1.6248102e+01 3.3074368e+02 8.2002681e-01]
<span class="o">[</span>2.5341917e+01 3.7768115e+02 8.5994357e-01]
<span class="o">[</span>2.6669228e+01 4.0245346e+02 8.6616194e-01]
<span class="o">[</span>0.0000000e+00 0.0000000e+00 0.0000000e+00]
<span class="o">[</span>0.0000000e+00 0.0000000e+00 0.0000000e+00]
<span class="o">[</span>0.0000000e+00 0.0000000e+00 0.0000000e+00]
<span class="o">[</span>0.0000000e+00 0.0000000e+00 0.0000000e+00]
<span class="o">[</span>0.0000000e+00 0.0000000e+00 0.0000000e+00]
<span class="o">[</span>0.0000000e+00 0.0000000e+00 0.0000000e+00]
<span class="o">[</span>0.0000000e+00 0.0000000e+00 0.0000000e+00]
<span class="o">[</span>0.0000000e+00 0.0000000e+00 0.0000000e+00]
<span class="o">[</span>0.0000000e+00 0.0000000e+00 0.0000000e+00]
<span class="o">[</span>0.0000000e+00 0.0000000e+00 0.0000000e+00]
<span class="o">[</span>2.0087900e+01 2.9541397e+02 5.8924217e-02]
<span class="o">[</span>0.0000000e+00 0.0000000e+00 0.0000000e+00]
<span class="o">[</span>1.4901900e+01 2.9680981e+02 8.9045525e-01]
<span class="o">[</span>0.0000000e+00 0.0000000e+00 0.0000000e+00]
<span class="o">[</span>0.0000000e+00 0.0000000e+00 0.0000000e+00]
<span class="o">[</span>0.0000000e+00 0.0000000e+00 0.0000000e+00]
<span class="o">[</span>0.0000000e+00 0.0000000e+00 0.0000000e+00]
<span class="o">[</span>0.0000000e+00 0.0000000e+00 0.0000000e+00]
<span class="o">[</span>0.0000000e+00 0.0000000e+00 0.0000000e+00]
<span class="o">[</span>0.0000000e+00 0.0000000e+00 0.0000000e+00]]
<span class="o">[[</span>0.0000000e+00 0.0000000e+00 0.0000000e+00]
<span class="o">[</span>0.0000000e+00 0.0000000e+00 0.0000000e+00]
<span class="o">[</span>0.0000000e+00 0.0000000e+00 0.0000000e+00]
<span class="o">[</span>0.0000000e+00 0.0000000e+00 0.0000000e+00]
<span class="o">[</span>0.0000000e+00 0.0000000e+00 0.0000000e+00]
<span class="o">[</span>0.0000000e+00 0.0000000e+00 0.0000000e+00]
<span class="o">[</span>0.0000000e+00 0.0000000e+00 0.0000000e+00]
<span class="o">[</span>0.0000000e+00 0.0000000e+00 0.0000000e+00]
<span class="o">[</span>3.7469666e+00 4.0509769e+02 7.3330581e-02]
<span class="o">[</span>3.7331178e+00 4.0640887e+02 4.0140808e-01]
<span class="o">[</span>3.7368219e+00 4.5732520e+02 4.1400149e-01]
<span class="o">[</span>0.0000000e+00 0.0000000e+00 0.0000000e+00]
<span class="o">[</span>0.0000000e+00 0.0000000e+00 0.0000000e+00]
<span class="o">[</span>0.0000000e+00 0.0000000e+00 0.0000000e+00]
<span class="o">[</span>0.0000000e+00 0.0000000e+00 0.0000000e+00]
<span class="o">[</span>0.0000000e+00 0.0000000e+00 0.0000000e+00]
<span class="o">[</span>0.0000000e+00 0.0000000e+00 0.0000000e+00]
<span class="o">[</span>0.0000000e+00 0.0000000e+00 0.0000000e+00]
<span class="o">[</span>0.0000000e+00 0.0000000e+00 0.0000000e+00]
<span class="o">[</span>0.0000000e+00 0.0000000e+00 0.0000000e+00]
<span class="o">[</span>0.0000000e+00 0.0000000e+00 0.0000000e+00]
<span class="o">[</span>0.0000000e+00 0.0000000e+00 0.0000000e+00]
<span class="o">[</span>0.0000000e+00 0.0000000e+00 0.0000000e+00]
<span class="o">[</span>0.0000000e+00 0.0000000e+00 0.0000000e+00]
<span class="o">[</span>0.0000000e+00 0.0000000e+00 0.0000000e+00]]]
: cannot connect to X server localhost:11.0
</code></pre></div></div>ironartisan3.Bigtable解读2022-06-21T00:00:00+08:002022-06-21T00:00:00+08:00https://ironartisan.github.io/2022/06/21/%E5%A4%A7%E6%95%B0%E6%8D%AE-3.Bigtable%E8%A7%A3%E8%AF%BB<h2 id="bigtable-要解决什么问题">Bigtable 要解决什么问题?</h2>
<blockquote>
<p>Bigtable 想要解决什么问题?我们不能用 MySQL 这样的关系型数据库,搭建一个集群
来解决吗?
Bigtable 的架构是怎么样的?它是怎么来解决可用性、一致性以及容易运维这三个目标
的?
Bigtable 的底层数据结构是怎么样的?它是通过什么样的方式在机械硬盘上做到高并发
地随机读写呢?</p>
</blockquote>
<p>当你理解了这三个问题,相信你对分布式数据库的设计可以算是正式入门了,而且你对于
计算机底层数据结构、硬件原理和大型系统设计之间的联系也建立起来了。这样,无论你
后续是想专门从事分布式数据库的开发,还是成为一个熟知各类系统原理的架构师,都会
有很大帮助。</p>
<p>不知道你有没有听说过 Friendster这个网站?如果听说过的话,你多半暴露年龄了。
Friendster 是一个成立于 2002 年的社交网络,比 Facebook 要早,更远远早于微信、微
博乃至校内网。然而,Friendster 虽然起了个大早,却最终因为种种原因销声匿迹了。其
中很重要的一个问题,就是在技术上,Friendster 没有解决好“伸缩性”(Scalability)
问题。</p>
<p>从 2003 年开始,Friendster 就一直遇到严重的性能瓶颈,并且因为性能问题限制了很多
功能的实现。甚至 MIT 在 2003 年的一门《Web 应用的软件工程》的课程里,还专门
把 Friendster 的可用性分析,作为期中考试里的一题,可见当时 Friendster 的体验有多糟
糕了。</p>
<p>Friendster 在成立一年之后的 2003 年,就已经有 7500 万用户了,所以服务器压力的确
很大。那么根据上面 MIT 学生的描述,我们可以想象一个简单的社交网络的功能,以及对
应需要的读写请求数量:用户去看自己的时间线的时候,需要看到自己 150 个好友发的帖
子。这里有两种解决办法。</p>
<p>一种是用户发帖子的时候,系统往所有好友的时间线里写一条数据,那么写入就会放大
150 倍。假设每天有 20% 的用户发 3 条帖子,那么写入的数据量就是:</p>
<p>7500 万 x 20% x 150 x 3 = 67.5 亿</p>
<p>67.5 亿条随机数据写入,如果均匀分配到 10 个小时,每秒的随机写入量大概是:</p>
<p>67.5 亿 / (3600 秒 * 10) = 18.75 万次 / 秒</p>
<p>还有一种是每个用户看自己时间线的时候,系统会查询 150 个好友各自发表的内容,然后
做合并。那么对应的就是 22.5 亿次的随机数据读取,也就是每秒 6.25 万次的随机读取。</p>
<p>一块 7200 转的机械硬盘,只能支撑 100 的 IOPS,也就是 100 次随机读写。那按照上面的数
据来看,我们至少需要 600 块硬盘,才能支撑简单地读取自己的时间线信息。事实上,
600 块硬盘远远不够的,无论是读写什么数据,都不太可能只写入 1 条数据,更不可能只
有 1 次随机读写,而我们的硬盘,也不可能刚好跑满 IOPS</p>
<p>所以一方面,我们可能需要数千块硬盘,对应的,也就需要上千台服务器。另一方面,这
个集群需要能够支持海量的随机读写,至少需要支持到每秒百万次级别的随机读写,而
Bigtable 就是这样一个系统。</p>
<h2 id="小数据的-mysql-集群">“小数据”的 MySQL 集群</h2>
<p>Bigtable 的论文发表于 2006 年,而基于论文实现的开源系统 HBase,要到 2008 年才第
一次正式发布(0.18.0 版本)。所以,Friendster 并没有 Bigtable 可以用,在 2003 年,
一家互联网公司面对“伸缩性”这个问题,最好的选择是使用一个 MySQL 集群。</p>
<p>维护一个几十乃至上百台服务器的 MySQL 集群是可行的,但是,如果要像 GFS 一样到一
千乃至数千台服务器,还有可行性吗?下面我们就一起来看一下。</p>
<h3 id="分库分表的扩容方式">分库分表的扩容方式</h3>
<p>一致性的随机读写,在单个服务器上似乎并不是什么问题。如果你是做后端应用开发的,
肯定用过 MySQL 这样的数据库,你可以很容易地通过简单的 SQL,完成增删改查这样的随机数据读写。如果要把单机的 MySQL 扩展成分布式,好像也不是什么难题,只要做个分库分表就好了,这些套路你应该会非常熟悉。</p>
<p>一般来说,我们会先做垂直分库,在电商的系统里,我们把用户、商品、订单的表拆分到
不同服务器的数据库上。如果发现这样还不行,我们就再进行水平分库,把订单号 Hash
一下,然后取“模”(mod)个 4,拆分到 4 台不同的服务器的数据库里。</p>
<p>这样,每台机器只需要承接 1/4 的负担,看起来这种方式也能解决问题。当然,在分库分
表的过程中,我们已经放弃了 MySQL 这样的关系型数据库的很多特性,比如外键关联这
样的约束,以及单个数据库里面的跨行跨表的事务。</p>
<p><img src="https://cdn.jsdelivr.net/gh/ironartisan/picRepo/20220621165015-2022-06-21-16-50-15.png" alt="20220621165015-2022-06-21-16-50-15" /></p>
<p>那么,为什么谷歌还需要发明一个 Bigtable 呢?这是因为分库分表,并不是一个很好的实
现“可伸缩性”和“可运维性”的方案。基于分库分表的方案,运维起来会很费劲,主要
体现在以下三点</p>
<h3 id="不得不进行的翻倍扩容">不得不进行的“翻倍扩容”</h3>
<p>首先,是资源使用很浪费。当服务器性能出现瓶颈需要扩容的时候,我们常常只能采
取“翻倍”分库增加服务器的方案。就以前面举的订单表为例,我们通过把订单
号“模”上个 4,拆分到 4 个不同的服务器的数据库里。</p>
<p>而随着我们承接的订单越来越多,每天 SQL 查询的请求越来越多,服务器的峰值 CPU 可
能超过了 60%。为了安全起见,我们希望对服务器进行扩容,让峰值 CPU 控制在 40% 以
下。但是这个时候,我们没办法只是增加 4 * 0.6 / 0.4 - 4 = 2 台服务器,而是不得不“翻
倍”增加 4 台服务器。</p>
<p><img src="https://cdn.jsdelivr.net/gh/ironartisan/picRepo/20220621165235-2022-06-21-16-52-35.png" alt="20220621165235-2022-06-21-16-52-35" /></p>
<p>为什么呢?因为如果我们只增加 2 台服务器,把各个服务器的分片,从模上 4 变成模上
6,我们就需要在增加服务器之后,搬运大量的数据。并且这个数据搬运,不只是搬到新增
加的服务器上,而是有些数据还要在原有的 4 台服务器上互相搬运。</p>
<p>这个搬运过程需要占用大量的网络带宽和硬盘读写,所以很有可能要让数据库暂停服务。
而如果不暂停服务的话,我们就要面对在数据搬运的过程中,到底应该从哪个服务器读和
写数据的问题,问题一下子就变得极其复杂了。</p>
<p>而翻倍扩容服务器,我们可以只需要简单复制 50% 的数据,并且在数据完成复制之后自动
切换分片就可以了。但是翻倍扩容的方案,自然就带来了很多浪费,明明我们只需要加两
台服务器,但是现在要加上四台。更浪费的是,我们增加的服务器,也许只是为了应对双
十一促销这样的一小段时间,等到促销完成,我们又不再需要这些服务器了。</p>
<p>可这个时候,如果我们需要缩减服务器,也会非常麻烦,我们需要再把两台服务器的数据
复制到一台服务器上,才能完成缩容。可以看到,这个集群虽然可以“伸缩”,但是伸缩
起来非常不容易。</p>
<p>而我们希望的伸缩性是什么样的呢?自然是需要的时候,加 1 台服务器也加得,加 10 台
服务器也加得。而用不上的时候,减少个 8 台 10 台服务器也没有问题,并且这些动作都
不需要停机。这个,也是 Bigtable 的设计目标。</p>
<p><img src="https://cdn.jsdelivr.net/gh/ironartisan/picRepo/20220621165430-2022-06-21-16-54-30.png" alt="20220621165430-2022-06-21-16-54-30" /></p>
<h3 id="我怎么早没想到的数据分区">“我怎么早没想到”的数据分区</h3>
<p>其次,是底层的数据分区策略对于应用不透明。如何分库和分表都需要开发人员来设计,
撰写代码的时候,也常常要考虑底层分库分表的设计。</p>
<p>我们还是以 MySQL 分表作为例子,这一次我们来分一下用户表。我们还是分到 4 台机器
上,用了用户出生的月份“模”上个 4。这个时候,很幸运,一年是有 12 个月,正好可以
均匀分布到 4 台不同的机器上。</p>
<p>但是当我们进行扩容,变成 8 台机器之后,问题就出现了。我们会发现,服务器 A 分到了
1 月和 9 月生日的用户,而服务器 B 只分到了 6 月生日的用户。在扩容之后,服务器 A 无
论是数据量,还是日常读写的负载,都比服务器 B 要高上一倍。而我们只能按照服务器 A
的负载要求来采购硬件,这也就意味着,服务器 B 的硬件性能很多都被浪费了。</p>
<p><img src="https://cdn.jsdelivr.net/gh/ironartisan/picRepo/20220623153327-2022-06-23-15-33-28.png" alt="20220623153327-2022-06-23-15-33-28" /></p>
<p>而且,不但用月份不行,用年份和日也不行。比如公司是 2018 年成立,2019 年和 2020
年快速成长,每年订单数涨 10 倍,如果你用年份来进行订单的分片,那么服务器之间的负
载就要差上十倍。而用日的话,双十一这样的大促也会让你猝不及防。</p>
<p>你会发现,使用 MySQL 集群,需要你在一开始就对如何切分数据做好精心设计。一旦稍
有不慎,设计上出现了数据倾斜,就很容易造成服务器忙得忙死,闲得闲死的现象。并且
即使你已经考虑得非常仔细了,随着业务本身的变化,比如要搞个双十一,也会把你一朝
打回原形。</p>
<p>那么,我们希望的分布式数据库是什么样的呢?自然是数据的分片是自适应的。比如 2019
年只有 100 万订单,那就分片到一个服务器节点上;2020 年有了 1000 万订单,自动给
你分了 10 个节点;当 2021 年有 1 亿订单的时候,就给你分配上 100 个节点。而这一
点,也同样是 Bigtable 的设计目标。</p>
<h3 id="天天跑机房的人肉运维">天天跑机房的人肉运维</h3>
<p>最后,是故障恢复需要人工介入。在 MySQL 集群里,我们可以对每个服务器都准备一个
高可用的备份,避免一出现故障整个集群就没法用了。但是此时,我们的运维人员仍然需
要立刻介入,因为这个时候系统是多了一个“单点”的,我们需要手工添加一台新的服务
器进入集群,同步到最新的数据。</p>
<p>我们可以一起来算一算,如果有一个 1000 台服务器的 MySQL 集群,每台服务器上都给
插上 12 块硬盘,一共有 1 万 2 千块硬盘。这么多硬盘,我们到底要面临多少故障呢?</p>
<p>2003 年,谷歌的论文用的还是传统的机械硬盘,那个时候机械硬盘的可靠性数据我已经找
不到了。不过我们可以看一下 2021 年的数据:Backblaze 这个公司从 2012 年开始就会
发布硬盘的可靠性数据,从 2021 年 Q2 季度来看,他们数据中心里将近 18 万块的硬盘,
在 90 天里一共坏了 439 块,差不多每天要坏上将近 5 块硬盘。</p>
<p>我们的 1 万 2 千块硬盘,是他们的 7% 不到,基本上 3 天也要坏上一块硬盘。要知道,这
个还是只考虑了硬盘的硬件损坏,还没有算上 CPU、内存、交换机、网络等等各种各样的
问题。</p>
<p>而我们希望的可运维性是怎么样的呢?最好是 1000 台节点的服务器,坏个 10 台 8 台没
事儿,系统能够自动把这 10 台 8 台服务器下线,用剩下的 990 台继续完成服务。我们的
运维人员只要 1 个月跑一趟机房批量换些机器就好,而不用 996 甚至 007 地担心硬件故
障带来的不可用问题。</p>
<h2 id="bigtable-的设计目标">Bigtable 的设计目标</h2>
<p>看到这里,相信你对 Bigtable 的设计目标应该更清楚了。最基础的目标自然是应对业务需
求的,能够支撑百万级别随机读写 IOPS,并且伸缩到上千台服务器的一个数据库。但是光
能撑起 IOPS 还不够。在这个数据量下,整个系统的“可伸缩性”和“可运维性”就变得
非常重要。</p>
<p>这里的伸缩性,包括两点:</p>
<p>第一个,是可以随时加减服务器,并且对添加减少服务器数量的限制要小,能够做到忙
的时候加几台服务器,过几个小时峰值过去了,就可以把服务器降下来。</p>
<p>第二个,是数据的分片会自动根据负载调整。某一个分片写入的数据多了,能够自动拆
成多个分片来平衡负载。而如果负载大了,添加了服务器之后,也能很快平衡数据,让
各个节点均匀承担压力。</p>
<p>而可运维性,则除了上面的两点之外,小部分节点的故障,不应该影响整个集群的运行,
我们的运维人员也不用急匆匆地立刻去恢复。集群自身也要有很强的容错能力,能够把对
应的请求和服务,调度到其他节点去。</p>
<p>那么,当我们回头看这个设计目标之后,会发现 Bigtable 的设计思路和 GFS 以及
MapReduce 一脉相承。</p>
<p>这三个系统的核心设计思路,就是把一个集群当成一台计算机。对于使用者来说,完全不
用在意后面的分布式的存在。这样的设计思路,使得所有的工程师,并不需要学习什么新
知识,只要熟悉这些分布式系统给到的接口,就能上手写大型系统。而这一点就让谷歌在
很长一段时间都拥有极强的工程优势。</p>
<p>在 GFS+MapReduce+Bigtable 发布的前后几年里,谷歌发布了很多优秀的产品,比如
Gmail、Google Maps、Google Analytics 等,而这些产品的底层,就是优秀的分布式架
构系统给谷歌带来的竞争优势。</p>
<p>当然,除了这些目标之外,Bigtable 也放弃了很多目标,其中有两个非常重要:</p>
<p>第一个是放弃了关系模型,也不支持 SQL 语言;</p>
<p>第二个,则是放弃了跨行事务,Bigtable 只支持单行的事务模型。</p>
<p>而这两个问题,一直要到 10 年后的 Spanner 里,才被真正解决好。在后续的课程里,你
也会看到 Spanner 是怎么一步步从 Bigtable 进化而来的。到时候,你也可以对照着
Spanner 的论文来回头看看 Bigtable,看看这些逐步迭代的设计是否和你自己的思考和猜
想一致。</p>
<p>无论是加减服务器、数据自动分片,还是硬件故障下的自动恢复,都不是一个“没有也能
坚持,有了更好”的可选的需求。在“大数据时代”,在需要上千台服务器的集群之下,
这些都变成了比优化一下性能、支持一下新的某个接口更重要的需求点了。</p>
<p>而 Bigtable 针对这些问题的答案,其实就是三点:</p>
<p>第一点,是将整个系统的存储层,搭建在 GFS 上。然后通过单 Master 调度多 Tablets
的方式,使得整个集群非常容易伸缩和维护。</p>
<p>第二点,是通过 MemTable+SSTable 这样一个底层文件格式,解决高速随机读写数据
的问题。</p>
<p>最后一点,则是通过 Chubby 这个高可用的分布式锁服务解决一致性的挑战。</p>
<p>Bigtable 在一开始,也不准备先考虑事务、Join 等高级的功能,而是把核心放在
了“可伸缩性”上。因此,Bigtable 自己的数据模型也特别简单,是一个很宽的稀疏表。</p>
<p>每一张 Bigtable 的表都特别简单,每一行就是一条数据:</p>
<p>第一个,Bigtable 是如何进行数据分区,使得整个集群灵活可扩展的;</p>
<p>第二个,Bigtable 是如何设计,使得 Master 不会成为单点故障,乃至单点性能的瓶
颈;</p>
<p>最后,自然是整个 Bigtable 的整体架构和组件由哪些东西组成。</p>
<p>一条数据里面,有一个行键(Row Key),也就是这条数据的主键,Bigtable 提供了通
过这个行键随机读写这条记录的接口。因为总是通过行键来读写数据,所以很多人也把
这样的数据库叫做 KV 数据库。</p>
<p>每一行里的数据呢,你需要指定一些列族(Column Family),每个列族下,你不需要
指定列(Column)。每一条数据都可以有属于自己的列,每一行数据的列也可以完全
不一样,因为列不是固定的。这个所谓不是固定的,其实就是列下面没有值。因为
Bigtable 在底层存储数据的时候,每一条记录都要把列和值存下来,没有值,意味着对
应的这一行就没有这个列。这也是为什么说 Bigtable 是一个“稀疏”的表。</p>
<p>列下面如果有值的话,可以存储多个版本,不同版本都会存上对应版本的时间戳
(Timestamp),你可以指定保留最近的 N 个版本(比如 N=3,就是保留时间戳最近
的三个版本),也可以指定保留某一个时间点之后的版本。</p>
<p><img src="https://cdn.jsdelivr.net/gh/ironartisan/picRepo/20220623173437-2022-06-23-17-34-37.png" alt="20220623173437-2022-06-23-17-34-37" /></p>
<p>其实,这里的有些命名容易让人误解,比如列族,这个名字很容易让人误解 Bigtable 是一
个基于列存储的数据库。但事实完全不是这样,我觉得对于列族,更合理的解读是,它是
一张“物理表”,同一个列族下的数据会在物理上存储在一起。而整个表,是一张“逻辑
表”。</p>
<p>在现实当中,Bigtable 的开源实现 HBase,就是把每一个列族的数据存储在同一个 HFile
文件里。而在 Bigtable 的论文中,Google 定义了一个叫做本地组(Locality Group)的
概念,我们可以把多个列族放在同一个本地组中,而同一个本地组的所有列的数据,都会
存储在同一个 SSTable 文件里。</p>
<p>这个设计,就使得我们不需要针对字段多的数据表,像 MySQL 那样,进行纵向拆表了。</p>
<p>Bigtable 的这个数据模型,使得我们能很容易地去增加列,而且增加列并不算是修改
Bigtable 里一张表的 Schema,而是在某些这个列需要有值的行里面,直接写入数据就好
了。这里的列和值,其实是直接以 key-value 键值对的形式直接存储下来的。</p>
<p>这个灵活、稀疏而又宽的表,特别适合早期的互联网业务。虽然数据量很大,但是数据本
身的 Schema 我们可能没有想清楚,加减字段都不需要停机或者锁表。要知道,MySQL
直到 5.5 版本,用 ALTER 命令修改表结构仍然需要将整张表锁住。并且在锁住这张表的时
候,我们是不能往表里写数据的。对于一张数据量很大的表来说,这会让整张表有很长一
段时间不能写入数据。</p>
<p>而 Bigtable 这个稀疏列的设计,就为我们带来了很大的灵活性,如同《架构整洁之道》的
作者 Uncle Bob说的那样:“架构师的工作不是作出决策,而是尽可能久地推迟决策,
在现在不作出重大决策的情况下构建程序,以便以后有足够信息时再作出决策。</p>
<h2 id="数据分区可伸缩的第一步">数据分区,可伸缩的第一步</h2>
<p>Bigtable 是怎么解决上一讲MySQL 集群解决不好的<strong>水平分库</strong>问题的。</p>
<p>把一个数据表,根据主键的不同,拆分到多个不同的服务器上,在分布式数据库里被称之
为数据分区( Paritioning)。分区之后的每一片数据,在不同的分布式系统里有不同的名
字,在 MySQL 里呢,我们一般叫做 Shard,Bigtable 里则叫做 Tablet。</p>
<p>MySQL 集群的分区之所以遇到种种困难,是因为我们通过取模函数来进行分
区,也就是所谓的哈希分区。我们会拿一个字段哈希取模,然后划分到预先定好 N 个分片
里面。这里最大的问题,在于分区需要在一开始就设计好,而不是自动随我们的数据变化
动态调整的。</p>
<p>但是往往计划不如变化快,当我们的业务变化和计划稍有不同,就会遇到需要搬运数据或
者各个分片负载不均衡的情况。你可以看一下我从上一讲里搬过来的这张图,当我们将 4
台服务器扩展到 6 台服务器的时候,哈希分区的方式使得我们要在网络上搬运整个数据库
2/3 的数据。</p>
<p><img src="https://cdn.jsdelivr.net/gh/ironartisan/picRepo/20220623174421-2022-06-23-17-44-22.png" alt="20220623174421-2022-06-23-17-44-22" /></p>
<p>所以,在 Bigtable 里,我们就采用了另外一种分区方式,也就是动态区间分区。我们不再
是一开始就定义好需要多少个机器,应该怎么分区,而是采用了一种自动去“分
裂”(split)的方式来动态地进行分区。</p>
<p>我们的整个数据表,会按照行键排好序,然后按照连续的行键一段段地分区。如果某一段
行键的区间里,写的数据越来越多,占用的存储空间越来越大,那么整个系统会自动地将
这个分区一分为二,变成两个分区。而如果某一个区间段的数据被删掉了很多,占用的空
间越来越小了,那么我们就会自动把这个分区和它旁边的分区合并到一起。</p>
<p>这个分区的过程,就好像你按照 A~Z 的字母顺序去管理你的书的过程。一开始,你只有一
个空箱子放在地上,然后你把你的书按照书名的拼音,从上到下放在箱子里。当有一本新
书需要放进来的时候,你就按照字母顺序插在某两本书中间。而当箱子放不下的时候,你
就再拿一个空箱子,放在放不下的箱子下面,然后把之前的箱子里的图书从中间一分,把
下面的一半放到新箱子里。</p>
<p>而我们删除数据的时候,就要把书从箱子里面拿走。当两个相邻的箱子里都很空的时候,
我们就可以把两个箱子里面的书放到一个箱子里面,然后把腾出来的空箱子挪走。这里的
一个个“箱子”就是我们的分片,这里面的一本本书,就是我们的一行数据,而书名的拼
音,就是我们的行键。可能以 A、B、C 开头的书多一些,那么它们占用的分区就会多一
些,以 U、V、W 开头的书少一些,可能这些书就都在一个分区里面。</p>
<p><img src="https://cdn.jsdelivr.net/gh/ironartisan/picRepo/20220623175220-2022-06-23-17-52-21.png" alt="20220623175220-2022-06-23-17-52-21" /></p>
<p>采用这样的方式,你会发现,你可以动态地调整数据是如何分区的,并且每个分区在数据
量上,都会相对比较均匀。而且,在分区发生变化的时候,你需要调整的只有一个分区,
再没有需要大量搬运数据的压力了。</p>
<h2 id="通过-master--chubby-进行分区管理">通过 Master + Chubby 进行分区管理</h2>
<p>MySQL 集群也用这样的分区方式,问题是不是就解决了?</p>
<p>答案当然是办不到了。因为我们还需要有一套存储、管理分区信息的机制,这在哈希分片
的 MySQL 集群里是没有的。在 Bigtable 里,我们是通过 Master 和 Chubby 这两个组
件来完成这个任务的。这两个组件,加上每个分片提供服务的 Tablet Server,以及实际
存储数据的 GFS,共同组成了整个 Bigtable 集群。</p>
<h3 id="masterchubby-和-tablet-server-的用途">Master、Chubby 和 Tablet Server 的用途</h3>
<p>Tablet Server 的角色最明确,就是用来实际提供数据读写服务的。一个 Tablet Server 上
会分配到 10 到 1000 个 Tablets,Tablet Server 就去负责这些 Tablets 的读写请求,并且在单个 Tablet 太大的时候,对它们进行分裂。</p>
<p>而哪些 Tablets 分配给哪个 Tablet Server,自然是由 Master 负责的,而且 Master 可以
根据每个 Tablet Server 的负载进行动态的调度,也就是 Master 还能起到负载均衡
(load balance)的作用。而这一点,也是 MySQL 集群很难做到的。</p>
<p>这是因为,Bigtable 的 Tablet Server 只负责在线服务,不负责数据存储。实际的存储,
是通过一种叫做 SSTable 的数据格式写入到 GFS 上的。也就是 Bigtable 里,数据存储和
在线服务的职责是完全分离的。我们调度 Tablet 的时候,只是调度在线服务的负载,并不
需要把数据也一并搬运走。</p>
<p>而MySQL 集群服务职责和数据存储是在同一个节点上的。我们要想把负
载大的节点调度到其他地方去,就意味着数据也要一并迁移走,而复制和迁移数据又会进
一步加大节点的负载,很有可能造成雪崩效应。</p>
<p><img src="https://cdn.jsdelivr.net/gh/ironartisan/picRepo/20220623175701-2022-06-23-17-57-02.png" alt="20220623175701-2022-06-23-17-57-02" /></p>
<p>事实上,Master 一共会负责 5 项工作:</p>
<ul>
<li>分配 Tablets 给 Tablet Server;</li>
<li>检测 Tablet Server 的新增和过期;</li>
<li>平衡 Tablet Server 的负载;</li>
<li>对于 GFS 上的数据进行垃圾回收(GC);</li>
<li>管理表(Table)和列族的 Schema 变更,比如表和列族的创建与删除。</li>
</ul>
<p>好像 Master 加上 Tablet Server 就足以组成 Bigtable 了,为
什么还有一个 Chubby 这个组件呢?</p>
<p>Bigtable 需要 Chubby 来搞定这么几件事儿:</p>
<ul>
<li>确保我们只有一个 Master;</li>
<li>存储 Bigtable 数据的引导位置(Bootstrap Location);</li>
<li>发现 Tablet Servers 以及在它们终止之后完成清理工作;</li>
<li>存储 Bigtable 的 Schema 信息;</li>
<li>存储 ACL,也就是 Bigtable 的访问权限。</li>
</ul>
<p>这里面的最后两项只是简单的数据存储功能,我们重点来看看前三项。</p>
<p>如果没有 Chubby 的话,我能想到最直接的集群管理方案,就是让所有的 Tablet Server
直接和 Master 通信,把分区信息以及 Tablets 分配到哪些 Tablet Server,也直接放在
Master 的内存里面。这个办法,就和我们之前在 GFS 里的办法一样。但是这个方案,也
就使得 Master 变成了一个单点故障点(SPOF-Single Point of Failure)。当然,我们可
以通过 Backup Master 以及 Shadow Master 等方式,来尽可能提升可用性。</p>
<p>可是这样第一个问题就来了,我们在 GFS 的论文里面说过,我们可以通过一个外部服务去
监控 Master 的存活,等它挂了之后,自动切换到 Backup Master。但是,我们怎么知道
Master 是真的挂了,还是只是“外部服务”和 Master 之间的网络出现故障了呢?</p>
<p>如果是后者的话,我们很有可能会遇到一个很糟糕的情况,就是系统里面出现了两个
Master。这个时候,可能两个客户端分别认为这两个 Master 是真的,当它们分头在两边
写入数据的时候,我们就会遇到数据不一致的问题。</p>
<p>那么 Chubby,就是这里的这个外部服务,不过 Chubby 不是 1 台服务器,而是 5 台服务
器组成的一个集群,它会通过 Paxos 这样的共识算法,来确保不会出现误判。而且因为它
有 5 台服务器,所以也一并解决了高可用的问题,就算挂个 1~2 台,也并不会丢数据。</p>
<h2 id="为什么数据读写不需要-master">为什么数据读写不需要 Master?</h2>
<p>Chubby 帮我们保障了只有一个 Master,那么我们再来看看分区和 Tablets 的分配信息,
这些信息也没有放在 Master。Bigtable 在这里用了一个很巧妙的方法,就是直接把这个
信息,存成了 Bigtable 的一张 METADATA 表,而这张表在哪里呢,它是直接存放在
Bigtable 集群里面的,其实 METADATA 表自己就是一张 Bigtable 的数据表。</p>
<p>这其实有点像 MySQL 里面的 information_schema 表,也就是数据库定义了一张特殊的
表,用来存放自己的元数据。不过,Bigtable 是一个分布式数据库,所以我们还要知道,
这个元数据究竟存放在哪个 Tablet Server 里,这个就需要通过 Chubby 来告诉我们了。</p>
<p>Bigtable 在 Chubby 里的一个指定的文件里,存放了一个叫做 Root Tablet 的分区所
在的位置。</p>
<p>然后,这个 Root Tablet 的分区,是 METADATA 表的第一个分区,这个分区永远不会
分裂。它里面存的,是 METADATA 里其他 Tablets 所在的位置。</p>
<p>而 METADATA 剩下的这些 Tablets,每一个 Tablet 中,都存放了用户创建的那些数据
表,所包含的 Tablets 所在的位置,也就是所谓的 User Tablets 的位置。</p>
<p><img src="https://cdn.jsdelivr.net/gh/ironartisan/picRepo/20220623181049-2022-06-23-18-10-49.png" alt="20220623181049-2022-06-23-18-10-49" /></p>
<p>这里我们来看一个具体的 Bigtable 数据读写的例子,来帮助你理解这样一个三层结构。比
如,客户端想要根据订单号,查找我们的订单信息,订单都存在 Bigtable 的
ECOMMERCE_ORDERS 表里,这张要查的订单号,就是 A20210101RST。</p>
<p>那么,我们的客户端具体是怎么查询的呢?</p>
<ul>
<li>客户端先去发起请求,查询 Chubby,看我们的 Root Tablet 在哪里。(第一次查询)</li>
<li>Chubby 会告诉客户端,Root Tablet 在 5 号 Tablet Server,这里我们简写成 TS5。</li>
<li>客户端呢,会再向 TS5 发起请求,说我要查 Root Tablet,告诉我哪一个 METADATATablet 里,存放了 ECOMMERCE_ORDERS 业务表,行键为 A20210101RST 的记录的位置。(第二次查询)</li>
<li>TS5 会从 Root Tablet 里面查询,然后告诉客户端,说这个记录的位置啊,你可以从TS8 上面的 METADATA 的 tablet 107,找到这个信息。</li>
<li>然后,客户端再发起请求到 TS8,说我要在 tablet 107 里面,找ECOMMERCE_ORDERS 表,行键为 A20210101RST 具体在哪里。(第三次查询)</li>
<li>TS8 告诉客户端,这个数据在 TS20 的 tablet 253 里面。</li>
<li>客户端发起最后一次请求,去问 TS20 的 tablet 253,问 ECOMMERCE_ORDERS 表,行键为 A20210101RST 的具体数据。(第四次查询)</li>
<li>TS20 最终会把数据返回给客户端</li>
</ul>
<p><img src="https://cdn.jsdelivr.net/gh/ironartisan/picRepo/20220628175905-2022-06-28-17-59-06.png" alt="20220628175905-2022-06-28-17-59-06" /></p>
<p>可以看到,在这个过程里,我们用了三次网络查询,找到了想要查询的数据的具体位置,
然后再发起一次请求拿到最终的实际数据。一般我们会把前三次查询位置结果缓存起来,
以减少往返的网络查询次数。而对于整个 METADATA 表来说,我们都会把它们保留在内
存里,这样每个 Bigtable 请求都要读写的数据,就不需要通过访问 GFS 来读取到了。</p>
<p>这个 Tablet 分区信息,其实是一个三层 Tablet 信息存储的架构,而三层结构让 Bigtable
可以“伸缩”到足够大。METADATA 的一条记录,大约是 1KB,而 METADATA 的
Tablet 如果限制在 128MB,三层记录可以存下大约 (128*1000)2=234 个 Tablet 的位
置,也就是大约 160 亿个 Tablet,肯定是够用了。</p>
<p>这个设计带来了一个很大的好处,就是查询 Tablets 在哪里这件事情,尽可能地被分摊到
了 Bigtable 的整个集群,而不是集中在某一个 Master 节点上。而唯一所有人都需要查
询的 Root Tablet 的位置和里面的数据,考虑到 Root Tablet 不会分裂,并且客户端可以
有缓存,Chubby 和 Root Tablet 所在的 Tablet 服务器也不会有太大压力。</p>
<p>另外你还会发现,在整个数据读写的过程中,客户端是不需要经过 Master 的。即使
Master 节点已经挂掉了,也不会影响数据的正常读写。客户端不需要认识 Master 这
个“主人”,也不依赖 Master 这个“主人”为我们提供服务。这个设计,让 Bigtable 更
加“高可用”了。</p>
<p>而如果我们回顾前面整个查询过程,其实就很容易理解,为什么 Chubby 里面存的叫做
Bigtable 的引导位置,因为这个过程和操作系统启动的过程很类似,都是要从一个固定的
位置读取信息,来获得后面的动态的信息。在操作系统里,这个是读取硬盘的第一个扇
区,而在 Bigtable 里,则是 Chubby 里存放 Root Tablet 位置的固定文件。</p>
<h2 id="master-的调度者角色">Master 的调度者角色</h2>
<p>的确,在单纯的数据读写的过程中不需要 Master。Master 只负责 Tablets 的调度而已,
而且这个调度功能,也对 Chubby 有所依赖。我们来看一看这个过程是怎么样的:</p>
<ul>
<li>所有的 Tablet Server,一旦上线,就会在 Chubby 下的一个指定目录,获得一个和自己名字相同的独占锁(exclusive lock)。你可以看作是,Tablet Server 把自己注册到集群上了。</li>
<li>Master 会一直监听这个目录,当发现一个 Tablet Server 注册了,它就知道有一个新的Tablet Server 可以用了,也就是可以分配 Tablets。</li>
<li>分配 Tablets 的情况很多,可能是因为其他的 Tablet Server 挂了,导致部分 Tablets没有分配出去,或者因为别的 Tablet Server 的负载太大,这些情况都可以让 Master去重新分配 Tablet。具体的分配策略论文里并没有说,你可以根据自己的需要实现对应的分配策略。</li>
<li>Tablet Server 本身,是根据是否还独占着 Chubby 上对应的锁,以及锁文件是否还在,来确定自己是否还为自己分配到的 Tablets 服务。比如 Tablet Server 到 Chubby的网络中断了,那么 Tablet Server 就会失去这个独占锁,也就不再为原先分配到的Tablets 提供服务了。</li>
<li>而如果我们把 Tablet Server 从集群中挪走,那么 Tablet Server 会主动释放锁,当然它也不再服务那些 Tablets 了,这些 Tablets 都需要重新分配。</li>
<li>无论是前面的第 4、5 点这样异常或者正常的情况,都是由 Master 来检测 TabletServer 是不是正常工作的。检测的方法也不复杂,其实就是通过心跳。Master 会定期问 Tablets,你是不是还占着独占锁呀?无论是 Tablet Server 说它不再占有锁了,还是Master 连不上 Tablet Server 了,Master 都会做一个小小的测试,就是自己去获取这个锁。如果 Master 能够拿到这个锁,说明 Chubby 还活得好好的,那么一定是 Tablet Server 那边出了问题,Master 就会删除这个锁,确保 Tablet Server 不会再为 Tablets提供服务。而原先 Tablet Server 上的 Tablets 就会变回一个未分配的状态,需要回到上面的第 3 点重新分配。</li>
<li>而 Master 自己,一旦和 Chubby 之间的网络连接出现问题,也就是它和 Chubby 之间的会话过期了,它就会选择“自杀”,这个是为了避免出现两个 Master 而不自知的情况。反正,Master 的存活与否,不影响已经存在的 Tablets 分配关系,也不会影响到整个 Bigtable 数据读写的过程。</li>
</ul>
<p>整个 Bigtable 是由 4 个组件组成的,分别是:</p>
<ul>
<li>负责存储数据的 GFS;</li>
<li>负责作为分布式锁和目录服务的 Chubby;</li>
<li>负责实际提供在线服务的 Tablet Server;</li>
<li>负责调度 Tablet 和调整负载的 Master。</li>
</ul>
<p><img src="https://cdn.jsdelivr.net/gh/ironartisan/picRepo/20220628180629-2022-06-28-18-06-29.png" alt="20220628180629-2022-06-28-18-06-29" /></p>
<p>而通过动态区域分区的方式,Bigtable 的分区策略需要的数据搬运工作量会很小。在
Bigtable 里,Master 并不负责保存分区信息,也不负责为分区信息提供查询服务。</p>
<p>Bigtable 是通过把分区信息直接做成了三层树状结构的 Bigtable 表,来让查询分区位置
的请求分散到了整个 Bigtable 集群里,并且通过把查询的引导位置放在 Chubby 中,解
决了和操作系统类似的“如何启动”问题。而整个系统的分区分配工作,由 Master 完
成。通过对于 Chubby 锁的使用,就解决了 Master、Tablet Server 进出整个集群的问
题。</p>
<h2 id="参考链接">参考链接</h2>
<p><a href="https://time.geekbang.org/column/article/421579">https://time.geekbang.org/column/article/421579</a></p>ironartisanBigtable 要解决什么问题?2.MapReduce解读2022-06-20T00:00:00+08:002022-06-20T00:00:00+08:00https://ironartisan.github.io/2022/06/20/%E5%A4%A7%E6%95%B0%E6%8D%AE-2.MapReduce%E8%A7%A3%E8%AF%BB<h2 id="mapreduce一个分布式的-bash">MapReduce:一个分布式的 Bash</h2>
<p>要针对存放在上千个节点的 GFS 上的数据,进行数据处理,你会怎么做?你会有哪些计算方式的需求呢?</p>
<p>最直接的方式就是在很多台机器上,同时来做运算,也就是进行并行计算,
这样可以利用我们有上千个节点的优势。而需要的计算方式,抽象来说,无非是三种情况。</p>
<ul>
<li>第一种,是对所有的数据,我们都只需要单条数据就能完成处理。比如,我们有很多网页的内容,我们要从里面提取出来每一个网页的标题。这样的计算可以完全并行化。</li>
<li>第二种,是需要汇总多条数据才能完成计算。比如,要统计日志里面某个 URL 被访问了多少次,只需要简单累加就可以了。或者我们需要更复杂一些的操作,比如统计某个 URL 下面的唯一用户数。而对于这里的第二种情况,我们就需要将所有相同 URL 的数据,搬运到同一个计算节点上进行处理。不过,在搬运之后,不同的 URL 还是可以放到不同的节点进行处理的。</li>
<li>自然是一、二两种情况的组合了。比如,我们先从网页数据里面,提取出网页的URL 和标题,然后根据标题里面的关键字,统计特定关键字出现在多少个不同的 URL 里面,这就需要同时采用一二这两种情况的操作。</li>
</ul>
<p>当然,我们可以有更复杂的数据操作,但是这些动作也都可以抽象成前面的两个动作的组
合。因为无非,我们要处理的数据要么是完全独立的,要么需要多条数据之间的依赖。实
际上,前面的第一种动作,就是 MapReduce 里面的 Map;第二种动作,就是MapReduce 里面的 Reduce;而搬运的过程,就是 Shuffle。</p>
<h2 id="mapreduce-编程模型">MapReduce 编程模型</h2>
<p>MapReduce 的编程模型非常简单,对于想要利用 MapReduce 框架进行数据处理的开发
者来说,只需要实现一个 Map 函数,一个 Reduce 函数。</p>
<p>Map 函数,顾名思义就是一个映射函数,它会接受一个 key-value 对,然后把这个 key-
value 对转换成 0 到多个新的 key-value 对并输出出去。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>map (k1, v1) -> list (k2, v2)
</code></pre></div></div>
<p>Reduce 函数,则是一个化简函数,它接受一个 Key,以及这个 Key 下的一组 Value,然
后化简成一组新的值 Value 输出出去。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>reduce (k2, list(v2)) -> list(v3)
</code></pre></div></div>
<p>而在 Map 函数和 Reduce 函数之外,开发者还需要指定一下输入输出文件的路径。输入
路径上的文件内容,会变成一个个键值对给到 Map 函数。而 Map 函数会运行开发者写好
的映射逻辑,把数据作为新的一组键值对输出出去。</p>
<p>Map 函数的输出结果,会被整个 MapReduce 程序接手,进行一个叫做混洗的操作。混洗
会把 Map 函数输出的所有相同的 Key 的 Value 整合到一个列表中,给到 Reduce 函数。
并且给到 Reduce 函数的 Key,在每个 Reduce 里,都是按照 Key 排好序的。</p>
<p><img src="https://cdn.jsdelivr.net/gh/ironartisan/picRepo/20220620225720-2022-06-20-22-57-20.png" alt="20220620225720-2022-06-20-22-57-20" /></p>
<h2 id="mapreduce-的应用场景">MapReduce 的应用场景</h2>
<p>别看在 MapReduce 框架下,你只能定义简简单单的一个 Map 和一个 Reduce 函数,实
际上它能够实现的应用场景,论文里可列了不少,包括以下六个:</p>
<ul>
<li>分布式 grep;</li>
<li>统计 URL 的访问频次;</li>
<li>反转网页 - 链接图;</li>
<li>分域名的词向量;</li>
<li>生成倒排索引;</li>
<li>分布式排序。</li>
</ul>
<p>主要来关注一下前两个场景的用途,看看最简单的两个场景是如何通过
MapReduce 来实现的。然后,我们可以把其中的第二个场景和 Unix 下的 Bash 脚本对
应起来,来理解为什么我说 MapReduce 的设计思想,就是来自于 Unix 下的 Bash 和管
道。</p>
<h3 id="分布式-grep">分布式 grep</h3>
<p>在日常使用 Linux 的过程中,相信你没少用过 grep 这个命令。早年间,在出现各种线上
故障的时候,我常常会通过 grep 来检索各种应用和 Web 服务器的错误日志,去排查线上
问题,如下所示:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>grep "error" access.log > /tmp/error.log.1
</code></pre></div></div>
<p>在单台 Linux 服务器上,我们当然可以用一个 grep 命令。那么如果有很多台服务器,我
们怎么才能知道在哪台机器上会有我们需要的错误日志呢?</p>
<p>最简单的办法,当然就是在每台服务器上,都执行一遍相同的 grep 命令就好了。这个动作
就是所谓的“分布式 grep”,在整个 MapReduce 框架下,它其实就是一个只有 Map,
没有 Reduce 的任务。</p>
<p>在真实的应用场景下,“分布式 grep”当然不只是用来检索日志。对于谷歌这个全球最大
搜索引擎来说,这是完美地用来做网页预处理的方案。通过网络爬虫抓取到的网页内容,
你都可以直接存到 GFS 上,这样你就可以撰写一个 Map 函数,从 HTML 的网页中,提取
网页里的标题、正文,以及链接。然后你可以再去撰写一个 Map 函数,对标题和正文进行
关键词提取。</p>
<p>这些一步步的处理结果,还会作为后续的反转网页 - 链接图、生成倒排索引等
MapReduce 任务的输入数据。</p>
<p>实际上,“分布式 grep”就是一个分布式抽取数据的抽象,无论是像 grep 一样通过一个
正则表达式来提取内容,还是用复杂的算法和策略从输入中提取内容,都可以看成是一
种“分布式 grep”。而在 MapReduce 这个框架下,你只需要撰写一个 Map 函数,并不
需要关心数据存储在具体哪台机器上,也不需要关心哪台机器的硬件或者网络出了问题。</p>
<h3 id="统计-url-的访问频次">统计 URL 的访问频次</h3>
<p>下面放了一个表格,我们把它叫做 url_visit_logs 。在这个表格里面有三个字段,分别是:</p>
<ul>
<li>URL,记录用户具体是看专栏里的哪一篇文章;</li>
<li>USER_ID,记录具体是哪一个用户访问;</li>
<li>VISIT_TIME,记录用户访问的具体时间。</li>
</ul>
<p><img src="https://cdn.jsdelivr.net/gh/ironartisan/picRepo/20220620230245-2022-06-20-23-02-45.png" alt="20220620230245-2022-06-20-23-02-45" /></p>
<p>那么,作为像谷歌这样的搜索引擎,它通常都会有统计网页访问频次的需求。访问频次高
的网页,通常可以被认为是内容质量高,会在搜索结果的排名里面,排在更前面的位置。</p>
<p>如果只是极客时间的网页,我们可以把这张表里面的数据放在数据库里面,通过一句 SQL
就可以完成了:</p>
<p><code class="language-plaintext highlighter-rouge">SELECT URL, COUNT(*) FROM url_visit_logs GROUP BY URL ORDER BY URL</code></p>
<p>但是,如果考虑全网的所有数据网页访问日志,数据库就肯定放不下了。我们可以把这些
日志以文件的形式放在 GFS 上,然后通过 MapReduce 来做数据统计。
Map 函数很简单,它拿到的输入数据是这样的:</p>
<ul>
<li>Key 就是单条日志记录在文件中的行号;</li>
<li>Value 就是对应单条记录的字符串,不同字段之间以 Tab 分割。</li>
</ul>
<p><img src="https://cdn.jsdelivr.net/gh/ironartisan/picRepo/20220620230952-2022-06-20-23-09-53.png" alt="20220620230952-2022-06-20-23-09-53" /></p>
<p>Map 函数只需要通过一个 split 或者类似的函数,对 Value 进行分割,拿到 URL,然后输
出一个 List 的 key-value 对。在当前的场景下,这个 List 只有一个 key-value 对</p>
<ul>
<li>输出的 Key 就是 URL;</li>
<li>输出的 Value 为空字符串。</li>
</ul>
<p><img src="https://cdn.jsdelivr.net/gh/ironartisan/picRepo/20220620231026-2022-06-20-23-10-27.png" alt="20220620231026-2022-06-20-23-10-27" /></p>
<p>这个 URL 肯定不只被访问了一次,因为 MapReduce 框架会把所有相同 URL 的 Map 的
输出记录,都混洗给到同一个 Reduce 函数里。所以在这里,Reduce 函数拿到的输入数
据是这样的:</p>
<ul>
<li>Key 就是 URL;</li>
<li>一个 List 的 Value,里面的每一项都是空字符串。</li>
</ul>
<p><img src="https://cdn.jsdelivr.net/gh/ironartisan/picRepo/20220620231131-2022-06-20-23-11-31.png" alt="20220620231131-2022-06-20-23-11-31" /></p>
<p>Reduce 函数的逻辑也非常简单,就是把 list 里面的所有 Value 计个数,然后和前面的
Key 拼装到一起,并输出出去。Reduce 函数输出的 list 里,也只有这一个元素。</p>
<p><img src="https://cdn.jsdelivr.net/gh/ironartisan/picRepo/20220620231146-2022-06-20-23-11-46.png" alt="20220620231146-2022-06-20-23-11-46" /></p>
<p>这样一个 MapReduce 的过程,就和我们之前通过 SELECT + GROUP BY 关键字执行的
SQL 起到了同样的作用。</p>
<p>事实上,SQL 是一种申明式的语言,MapReduce 是我们的实现过程,后面我们在讲解
Hive 论文时候,你就会发现,Hive 的 HQL 就是通过一个个 MapReduce 程序来实现
的。而前面的整个 MapReduce 的过程,其实用一段 Bash 代码也可以实现。
其中的 Map 过程,类似于 SELECT 关键字,从输入数据中提取出需要使用的 Key 和
Value 字段。</p>
<p>MapReduce 框架完成的混洗过程,类似于 SQL 中的 GROUP BY 关键字,根据相同的
Key,把 Map 中选择的数据混洗到一起。</p>
<p>最后的 Reduce 过程,就是先对混洗后数据中的 Value,执行了 COUNT 这个函数,然
后再将 Key 和 COUNT 执行的结果拼装到一起,输出为最后的结果。</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cat</span> <span class="nv">$input</span> |
<span class="nb">awk</span> <span class="s1">'{print $1}'</span> |
<span class="nb">sort</span> |
<span class="nb">uniq</span> <span class="nt">-c</span> <span class="o">></span> <span class="nv">$output</span>
</code></pre></div></div>
<p>在这段 Bash 代码中:
如果和 MapReduce 框架对照起来,你会发现:</p>
<p>读写 HDFS 文件的内容,对应着 cat 命令和标准输出;
对于数据进行混洗,对应着 sort 命令;
整个框架,不同阶段之间的数据传输,用的就是标准的输入输出管道。</p>
<p>那么,对于开发者来说,只要自己实现 Map 和 Reduce 函数就好了,其他都不需要关
心。而对于实现 MapReduce 的底层框架代码,也可以映射到读取、外部排序、输出,以
及通过网络进行跨机器的数据传输就好了。在这个设计框架下,每一个组件都只需要完成
自己的工作,整个框架就能很容易地串联起来了。</p>
<p>作为一个框架,MapReduce 设计的一个重要思想,就是让使用者意识不到“分布式”这
件事情本身的存在。从设计模式的角度,MapReduce 框架用了一个经典的设计模式,就
是模版方法模式。而从设计思想的角度,MapReduce 的整个流程,类似于 Unix 下一个个
命令通过管道把数据处理流程串接起来。</p>
<p>MapReduce 的数据处理设计很直观,并不难理解。Map 帮助我们解决了并行在很多台机
器上处理互相之间没有依赖关系的数据;而 Reduce 则用来处理互相之间有依赖关系的数
据,我们可以通过 MapReduce 框架自带的 Shuffle 功能,通过排序来根据设定好的 Key
进行分组,把相同 Key 的数据放到同一个节点上,供 Reduce 处理。</p>
<p>cat 相当于我们 MapReduce 框架从 HDFS 读取数据;
awk 的脚本,是我们实现的 Map 函数;
sort 相当于 MapReduce 的混洗,只是这个混洗是在本机上执行的;
而最后的 uniq -c 则是实现了 Reduce 函数,在排好序的数据下,完成了同一 URL 的去
重计数的工作。</p>
<h2 id="mapreduce-框架的三个挑战">MapReduce 框架的三个挑战</h2>
<p>要想让写 Map 和 Reduce 函数的人不需要关心“分布式”的存在,那么 MapReduce 框
架本身就需要解决好三个很重要的问题:</p>
<p>第一个,自然是如何做好各个服务器节点之间的“协同”,以及解决出现各种软硬件问
题后的“容错”这两部分的设计。</p>
<p>第二个,是性能问题。
MapReduce 框架一样非常容易遇到网络性能瓶颈。尽量充分利用 MapReduce 集群的
计算能力,并让整个集群的性能可以随硬件的增加接近于线性增长,可以说是非常大的
一个挑战。</p>
<p>最后一个,还是要回到易用性。Map 函数和 Reduce 函数最终还是运行在多个不同的机
器上的,并且在 Map 和 Reduce 函数中还会遇到各种千奇百怪的数据。当我们的程序
在遭遇到奇怪的数据出错的时候,我们需要有办法来进行 debug。</p>
<p>而谷歌在论文里面,也通过第三部分的“MapReduce 的实现”,以及第四部分
的“MapReduce 的完善”,很好地回答了怎么解决这三个问题。下面,我们就来具体看
看,论文里是怎么讲的。</p>
<h3 id="mapreduce-的协同">MapReduce 的协同</h3>
<p>一个 MapReduce 的集群,通常就是之前的分布式存储系统 GFS 的集群。在这个集群里,
本身会有一个调度系统(Scheduler)。当我们要运行一个 MapReduce 任务的时候,其
实就是把整个 MapReduce 的任务提交给这个调度系统,让这个调度系统来分配和安排
Map 函数和 Reduce 函数,以及后面会提到的 master 在不同的硬件上运行。</p>
<p>在 MapReduce 任务提交了之后,整个 MapReduce 任务就会按照这样的顺序来执行。</p>
<p>第一步,你写好的 MapReduce 程序,已经指定了输入路径。所以 MapReduce 会先找到
GFS 上的对应路径,然后把对应路径下的所有数据进行分片(Split)。每个分片的大小通
常是 64MB,这个尺寸也是 GFS 里面一个块(Block)的大小。接着,MapReduce 会在
整个集群上,启动很多个 MapReduce 程序的复刻(fork)进程。</p>
<p>第二步,在这些进程中,有一个和其他不同的特殊进程,就是一个 master 进程,剩下的
都是 worker 进程。然后,我们会有 M 个 map 的任务(Task)以及 R 个 reduce 的任
务,分配给这些 worker 进程去进行处理。这里的 master 进程,是负责找到空闲的(idle)worker 进程,然后再把 map 任务或者 reduce 任务,分配给 worker 进程去处理。</p>
<p>这里你需要注意一点,并不是每一个 map 和 reduce 任务,都会单独建立一个新的
worker 进程来执行。而是 master 进程会把 map 和 reduce 任务分配给有限的 worker,
因为一个 worker 通常可以顺序地执行多个 map 和 reduce 的任务。</p>
<p>第三步,被分配到 map 任务的 worker 会读取某一个分片,分片里的数据就像上一讲所说
的,变成一个个 key-value 对喂给了 map 任务,然后等 Map 函数计算完后,会生成的新
的 key-value 对缓冲在内存里。</p>
<p>第四步,这些缓冲了的 key-value 对,会定期地写到 map 任务所在机器的本地硬盘上。
并且按照一个分区函数(partitioning function),把输出的数据分成 R 个不同的区域。
而这些本地文件的位置,会被 worker 传回给到 master 节点,再由 master 节点将这些地
址转发给 reduce 任务所在的 worker 那里。</p>
<p>第五步,运行 reduce 任务的 worker,在收到 master 的通知之后,会通过 RPC(远程过
程调用)来从 map 任务所在机器的本地磁盘上,抓取数据。当 reduce 任务的 worker 获
取到所有的中间文件之后,它就会将中间文件根据 Key 进行排序。这样,所有相同 Key 的
Value 的数据会被放到一起,也就是完成了我们上一讲所说的混洗(Shuffle)的过程。</p>
<p>第六步,reduce 会对排序后的数据执行实际的 Reduce 函数,并把 reduce 的结果输出到
当前这个 reduce 分片的最终输出文件里。</p>
<p>第七步,当所有的 map 任务和 reduce 任务执行完成之后,master 会唤醒启动
MapReduce 任务的用户程序,然后回到用户程序里,往下执行 MapReduce 任务提交之
后的代码逻辑。</p>
<p>其实,以上整个 MapReduce 的执行过程,还是一个典型的 Master-Slave 的分布式系
统。map 和 reduce 所在的 worker 之间并不会直接通信,它们都只和 master 通信。另
外,像是 map 的输出数据在哪里这样的信息,也是告诉 master,让 master 转达给
reduce 所在的 worker。reduce 从 map 里获取数据,也是直接拿到数据所在的地址去抓
取,而不是让 reduce 通过 RPC,调用 map 所在的 worker 去获取数据。</p>
<p><img src="https://cdn.jsdelivr.net/gh/ironartisan/picRepo/20220621090647-2022-06-21-09-06-48.png" alt="20220621090647-2022-06-21-09-06-48" /></p>
<p>在 Hadoop 1.0 里,MapReduce 论文里面的 worker 就是 TaskTracker,用来执行
map 和 reduce 的任务。而分配任务,以及和 TaskTracker 沟通任务的执行情况,都由单
一的 JobTracker 来负责。</p>
<p>这个设计,也导致了只要服务器数量一多,JobTracker 的负载就会很重。所以早年间,单
个 Hadoop 集群能够承载的服务器上限,被卡在了 4000 台。而且 JobTracker 也成为了
整个 Hadoop 系统很脆弱的“单点”。</p>
<p><img src="https://cdn.jsdelivr.net/gh/ironartisan/picRepo/20220621090822-2022-06-21-09-08-22.png" alt="20220621090822-2022-06-21-09-08-22" /></p>
<p>所以之后在 Hadoop 2.0,Hadoop 社区把 JobTracker 的角色,拆分成了进行任务调度的
Resource Mananger,以及监控单个 MapReduce 任务执行的 Application Master,
回到了和 MapReduce 论文相同的架构。</p>
<p>而在 2015 年,谷歌发布了 Borg 这个集群管理系统的论文的时候,大家发现谷歌早在
2003~2004 年,就已经有了独立的集群管理系统 Borg,也就是 MapReduce 里面所提到
的调度系统。</p>
<h2 id="mapreduce-的容错fault-tolerance">MapReduce 的容错(Fault Tolerance)</h2>
<p>MapReduce 的容错机制非常简单,可以简单地用两个关键词来描述,就是重新运行和写
Checkpoints。</p>
<h3 id="worker-节点的失效master-failure">worker 节点的失效(Master Failure)</h3>
<p>对于 Worker 节点的失效,MapReduce 框架解决问题的方式非常简单。就是换一台服务
器重新运行这个 Worker 节点被分配到的所有任务。master 节点会定时地去 ping 每一个
worker 节点,一旦 worker 节点没有响应,我们就会认为这个节点失效了。</p>
<p>于是,我们会重新在另一台服务器上,启动一个 worker 进程,并且在新的 worker 进程所
在的节点上,重新运行所有失效节点上被分配到的任务。而无论失效节点上,之前的 map
和 reduce 任务是否执行成功,这些任务都会重新运行。因为在节点 ping 不通的情况下,
我们很难保障它的本地硬盘还能正常访问。</p>
<h3 id="master-节点的失效worker-failure">master 节点的失效(Worker Failure)</h3>
<p>对于 master 节点的失效,事实上谷歌已经告诉了我们,他们就任由 master 节点失败了,
也就是整个 MapReduce 任务失败了。那么,对于开发者来说,解决这个问题的办法也很
简单,就是再次提交一下任务去重试。</p>
<p>因为 master 进程在整个任务中只有一个,它会失效的可能性很小。而 MapReduce 的任
务也是一个用户离线数据处理的任务,并不是一个实时在线的服务,失败重来通常也没有
什么影响,只是晚一点拿到数据结果罢了。</p>
<p>虽然在论文发表的时候,谷歌并没有实现对于 master 的失效自动恢复机制,但他们也给
出了一个很简单的解决方案,那就是让 master 定时把它里面存放的信息,作为一个个的
Checkpoint 写入到硬盘中去。</p>
<p>那么我们动一下脑筋,我们可以把这个 Checkpoint 直接写到 GFS 里,然后让调度系统监
控 master。这样一旦 master 失效,我们就可以启动一个新的 master,来读取
Checkpoints 数据,然后就可以恢复任务的继续执行了,而不需要重新运行整个任务。</p>
<h3 id="对错误数据视而不见">对错误数据视而不见</h3>
<p>worker 和 master 的节点失效,以及对应的恢复机制,通常都是来自于硬件问题。但是在
海量数据处理的情况下,比如在 TB 乃至 PB 级别的数据下,我们还会经常遇到“脏数
据”的问题。</p>
<p>这些数据,可能是日志采集的时候就出错了,也可能是一个非常罕见的边界情况(edge-
case),我们的 Map 和 Reduce 函数正好处理不了。甚至有可能,只是简单的硬盘硬件
的问题带来的错误数据。</p>
<p>那么,对于这些异常数据,我们固然可以不断 debug,一一修正。但是这么做,大多数时
候都是划不来的,你很可能为了一条数据记录,由于 Map 函数处理不了,你就要重新扫描
几 TB 的数据。</p>
<p>所以,MapReduce 不仅为节点故障提供了容错机制,对于这些极少数的数据异常带来的
问题,也提供了一个容错机制。MapReduce 会记录 Map 或者 Reduce 函数,运行出错
的具体数据的行号,如果同样行号的数据执行重试还是出错,它就会跳过这一行的数据。
如果这样的数据行数在总体数据中的比例很小,那么整个 MapReduce 程序会忽视这些错
误,仍然执行完成。毕竟,一个 URL 被访问了 1 万次还是 9999 次,对于搜素引擎的排序
结果不会有什么影响。</p>
<h2 id="mapreduce-的性能优化">MapReduce 的性能优化</h2>
<p>我们在前面说过,其实 MapReduce 的集群就是 GFS 的集群。所以 MapReduce 集群里的硬件
配置,和 GFS 的硬件配置差不多,最容易遇到的性能瓶颈,也是 100MB 或者 1GB 的网
络带宽。</p>
<h3 id="把程序搬到数据那儿去">把程序搬到数据那儿去</h3>
<p>既然网络带宽是瓶颈,那么优化的办法自然就是尽可能减少需要通过网络传输的数据。</p>
<p>在 MapReduce 这个框架下,就是在分配 map 任务的时候,根据需要读取的数据在哪里
进行分配。通过前面 GFS 论文的学习,我们可以知道,GFS 是知道每一个 Block 的数据是
在哪台服务器上的。而 MapReduce,会找到同样服务器上的 worker,来分配对应的
map 任务。如果那台服务器上没有,那么它就会找离这台服务器最近的、有 worker 的服
务器,来分配对应的任务。你可以参考下面给出的示意图:</p>
<p><img src="https://cdn.jsdelivr.net/gh/ironartisan/picRepo/20220621114542-2022-06-21-11-45-43.png" alt="20220621114542-2022-06-21-11-45-43" /></p>
<p>除此之外,由于 MapReduce 程序的代码往往很小,可能只有几百 KB 或者几 MB,但是
每个 map 需要读取的一个分片的数据是 64MB 大小。这样,我们通过把要执行的
MapReduce 程序,复制到数据所在的服务器上,就不用多花那 10 倍乃至 100 倍的网络
传输量了。</p>
<p>这就好像你想要研究金字塔,最好的办法不是把金字塔搬到你家来,而是你买张机票飞过
去。这里的金字塔就是要处理的数据,而你,就是那个分配过去的 MapReduce 程序。</p>
<h3 id="通过-combiner-减少网络数据传输">通过 Combiner 减少网络数据传输</h3>
<p>除了 Map 函数需要读取输入的分片数据之外,Reduce 所在的 worker 去抓取中间数据,
一样也需要通过网络。那么要在这里减少网络传输,最简单的办法,就是尽可能让中间数
据的数据量小一些。</p>
<p>自然,在 MapReduce 的框架里,也不会放过这一点。MapReduce 允许开发者自己定义
一个 Combiner 函数。这个 Combiner 函数,会对在同一个服务器上所有 map 输出的结
果运行一次,然后进行数据合并。</p>
<p>既然只是对访问次数计数,我们自然就可以通过一个 Combiner,把 1 万条相同域名的访
问记录做个化简。把它们变成 Key 还是域名,Value 就是有多少次访问的数值这样的记录
就好了。而这样一化简,reduce 所在的 worker 需要抓取的数据,就从 1 万条变成了 1
条。</p>
<p>实际上,不仅是同一个 Map 函数的输出可以合并,同一台服务器上多个 Map 的输出,我
们都可以合并。反正它们都在一台机器上,合并只需要本地的硬盘读写和 CPU,并不需要
我们最紧缺的网络资源。</p>
<p>我就以域名的访问次数为例,它的数据分布一定有很强的头部效应,少量 20% 的域名可能
占了 80% 的访问记录。这样一合并,我们要传输的数据至少可以减少 60%。如果考虑一
台 16 核的服务器,有 16 个 map 的 worker 运行,应该还能再减少 80% 以上。这样,通
过一个中间的 Combiner,我们要传输的数据一下子就下降了两个数量级,大大缓解了网
络传输的压力。</p>
<p>你可以参考下面给出的示意图:</p>
<p><img src="https://cdn.jsdelivr.net/gh/ironartisan/picRepo/20220621114708-2022-06-21-11-47-09.png" alt="20220621114708-2022-06-21-11-47-09" /></p>
<h2 id="mapreduce-的-debug-信息">MapReduce 的 debug 信息</h2>
<p>虽然我们一直说,我们希望 MapReduce 让开发者意识不到分布式的存在。但是归根到
底,map 和 reduce 的任务都是在分布式集群上运行的,这个就给我们对程序 debug 带
来了很大的挑战。无论是通过 debugger 做单步调试,还是打印出日志来看程序执行的情
况,都不太可行。所以,MapReduce 也为开发者贴心地提供了三个办法来解决这一点。</p>
<p>第一个,是提供一个单机运行的 MapReduce 的库,这个库在接收到 MapReduce 任务
之后,会在本地执行完成 map 和 reduce 的任务。这样,你就可以通过拿一点小数据,在
本地调试你的 MapReduce 任务了,无论是 debugger 还是打日志,都行得通。</p>
<p>第二个,是在 master 里面内嵌了一个 HTTP 服务器,然后把 master 的各种状态展示出
来给开发者看到。这样一来,你就可以看到有多少个任务执行完了,有多少任务还在执行
过程中,它处理了多少输入数据,有多少中间数据,有多少输出的结果数据,以及任务完
成的百分比等等。同样的,里面还有每一个任务的日志信息。</p>
<p>另外通过这个 HTTP 服务器,你还可以看到具体是哪一个 worker 里的任务失败了,对应
的错误日志是什么。这样,你就可以快速在线上定位你的程序出了什么错,是在哪台服务
器上。</p>
<p>最后一个,是 MapReduce 框架里提供了一个计数器(counter)的机制。作为开发者,
你可以自己定义几个计数器,然后在 Map 和 Reduce 的函数里去调用这个计数器进行自
增。所有 map 和 reduce 的计数器都会汇总到 master 节点上,通过上面的 HTTP 服务器
里展现出来。</p>
<p>比如,你就可以利用这个计数器,去统计有多少输入日志的格式和预期的不一样。如果比
例太高,那么多半你的程序就有 Bug,没有兼容所有合法的日志。下图展示的就是在
Hadoop 里,通过 JobTracker 查看 Task 的执行情况,以及对应每个 Task 的日志:</p>
<p><img src="https://cdn.jsdelivr.net/gh/ironartisan/picRepo/20220621114948-2022-06-21-11-49-48.png" alt="20220621114948-2022-06-21-11-49-48" /></p>
<h2 id="遗憾与缺陷">遗憾与缺陷</h2>
<p>尽管 MapReduce 框架已经作出了很多努力,但是今天来看,整个计算框架的缺陷还是不
少的。在我看来,主要的缺陷有两个:</p>
<p>第一个是还没有 100% 做到让用户意识不到“分布式”的存在,无论是 Combiner 还
是 Partitioner,都是让开发者意识到,它面对的还是分布式的数据和分布式的程序。</p>
<p>第二个是性能仍然不太理想,这体现在两个方面,一个是每个任务都有比较大的
overhead,都需要预先把程序复制到各个 worker 节点,然后启动进程;另一个是所有
的中间数据都要读写多次硬盘。map 的输出结果要写到硬盘上,reduce 抓取数据排序
合并之后,也要先写到本地硬盘上再进行读取,所以快不起来。</p>
<p>不过,随着时间的变迁,会有更多新一代的系统,像是 Dremel 和 Spark 逐步取代
MapReduce,让我们能更容易地写出分布式数据处理程序,处理起数据也比原始的
MapReduce 快上不少。</p>
<h2 id="参考链接">参考链接</h2>
<p><a href="https://time.geekbang.org/column/article/421579">https://time.geekbang.org/column/article/421579</a></p>ironartisanMapReduce:一个分布式的 Bash0.建立大数据知识网络2022-06-13T00:00:00+08:002022-06-13T00:00:00+08:00https://ironartisan.github.io/2022/06/13/%E5%A4%A7%E6%95%B0%E6%8D%AE-0.%E5%BB%BA%E7%AB%8B%E5%A4%A7%E6%95%B0%E6%8D%AE%E7%9F%A5%E8%AF%86%E7%BD%91%E7%BB%9C<h2 id="google的三驾马车">Google的三驾马车</h2>
<p><img src="https://cdn.jsdelivr.net/gh/ironartisan/picRepo/20220613155411-2022-06-13-15-54-12.png" alt="20220613155411-2022-06-13-15-54-12" /></p>
<p><img src="https://cdn.jsdelivr.net/gh/ironartisan/picRepo/20220613155452-2022-06-13-15-54-52.png" alt="20220613155452-2022-06-13-15-54-52" /></p>
<h2 id="实时数据处理的抽象进化">实时数据处理的抽象进化</h2>
<p><img src="https://cdn.jsdelivr.net/gh/ironartisan/picRepo/20220613155636-2022-06-13-15-56-37.png" alt="20220613155636-2022-06-13-15-56-37" /></p>
<h2 id="olap-和-oltp-数据库">OLAP 和 OLTP 数据库</h2>
<p><img src="https://cdn.jsdelivr.net/gh/ironartisan/picRepo/20220613155522-2022-06-13-15-55-22.png" alt="20220613155522-2022-06-13-15-55-22" /></p>
<h2 id="大数据知识点">大数据知识点</h2>
<p><img src="https://cdn.jsdelivr.net/gh/ironartisan/picRepo/20220613153140-2022-06-13-15-31-41.png" alt="20220613153140-2022-06-13-15-31-41" /></p>
<h2 id="分布式系统">分布式系统</h2>
<p>作为一个分布式的数据系统,它就需要满足三个特性,也就是可靠性、可扩展性和可维护性</p>
<p>分布式系统的核心问题就是 CAP 这个不可能三角,我们需要在一致性、可用性和分区容
错性之间做权衡和选择。因此,我们选择的主从架构、复制策略、分片策略,以及容错和
恢复方案,都是根据我们实际的应用场景下对于 CAP 进行的权衡和选择。</p>
<p><img src="https://cdn.jsdelivr.net/gh/ironartisan/picRepo/20220613153335-2022-06-13-15-33-36.png" alt="20220613153335-2022-06-13-15-33-36" /></p>
<h2 id="存储引擎">存储引擎</h2>
<p><img src="https://cdn.jsdelivr.net/gh/ironartisan/picRepo/20220613153840-2022-06-13-15-38-40.png" alt="20220613153840-2022-06-13-15-38-40" /></p>
<h2 id="计算引擎">计算引擎</h2>
<p><img src="https://cdn.jsdelivr.net/gh/ironartisan/picRepo/20220613154322-2022-06-13-15-43-22.png" alt="20220613154322-2022-06-13-15-43-22" /></p>ironartisanGoogle的三驾马车1.GFS解读2022-06-13T00:00:00+08:002022-06-13T00:00:00+08:00https://ironartisan.github.io/2022/06/13/%E5%A4%A7%E6%95%B0%E6%8D%AE-1.GFS%E8%A7%A3%E8%AF%BB<h2 id="gfs-的设计决策">GFS 的设计决策</h2>
<ul>
<li>第一个是以工程上“简单”作为设计原则。
<ul>
<li>直接使用了 Linux 服务上的普通文件作为基础存储层,并且选择了最简单的单 Master 设计</li>
</ul>
</li>
<li>第二个是根据硬件特性来进行设计取舍。
<ul>
<li>重视的是顺序读写的性能,对随机写入的一致性甚至没有任何保障</li>
<li>专门设计了一个 Snapshot 的文件复制操作,避免了数据在网络上传输</li>
</ul>
</li>
<li>第三个是根据实际应用的特性,放宽了数据一致性(consistency)的选择。
<ul>
<li>GFS 本身对于随机写入的一致性没有任何保障,而是把这个任务交给了客户端。对于追加写入(Append),GFS 也只是作出了“至少一次(At Least Once)”这样宽松的保障。</li>
</ul>
</li>
</ul>
<p><img src="https://cdn.jsdelivr.net/gh/ironartisan/picRepo/20220613162148-2022-06-13-16-21-48.png" alt="20220613162148-2022-06-13-16-21-48" /></p>
<h2 id="master-的第一个身份一个目录服务">Master 的第一个身份:一个目录服务</h2>
<p>在整个 GFS 中,有两种服务器,一种是 master,也就是整个 GFS 中有且
仅有一个的主控节点;第二种是 chunkserver,也就是实际存储数据的节点。</p>
<p>在 GFS 里面,会把每一个文件按照 64MB 一块的大小,切分成一个个 chunk。每
个 chunk 都会有一个在 GFS 上的唯一的 handle,这个 handle 其实就是一个编号,能够
唯一标识出具体的 chunk。然后每一个 chunk,都会以一个文件的形式,放在
chunkserver 上。</p>
<p>而 chunkserver,其实就是一台普通的 Linux 服务器,上面跑了一个用户态的 GFS 的
chunkserver 程序。这个程序,会负责和 master 以及 GFS 的客户端进行 RPC 通信,完
成实际的数据读写操作。</p>
<p>每个 chunk 都会存上整整三份副本(replica)。其中一份是主数据(primary),两份是副数据
(secondary),当三份数据出现不一致的时候,就以主数据为准。有了三个副本,不仅
可以防止因为各种原因丢数据,还可以在有很多并发读取的时候,分摊系统读取的压力</p>
<p><img src="https://cdn.jsdelivr.net/gh/ironartisan/picRepo/20220613162659-2022-06-13-16-26-59.png" alt="20220613162659-2022-06-13-16-26-59" /></p>
<p>GFS 的客户端,怎么知道该去哪个 chunkserver 找自己要的文件呢?</p>
<p>答案当然是问 master 啦。
首先,master 里面会存放三种主要的元数据(metadata):</p>
<ul>
<li>文件和 chunk 的命名空间信息,也就是类似前面 /data/geektime/bigdata/gfs01 这样的路径和文件名;</li>
<li>这些文件被拆分成了哪几个 chunk,也就是这个全路径文件名到多个 chunk handle 的映射关系;</li>
<li>这些 chunk 实际被存储在了哪些 chunkserver 上,也就是 chunk handle 到 chunkserver 的映射关系。</li>
</ul>
<p><img src="https://cdn.jsdelivr.net/gh/ironartisan/picRepo/20220613162821-2022-06-13-16-28-22.png" alt="20220613162821-2022-06-13-16-28-22" /></p>
<p>然后,当我们要通过一个客户端去读取 GFS 里面的数据的时候,需要怎么做呢?GFS 会有
以下三个步骤:</p>
<p>客户端先去问 master,我们想要读取的数据在哪里。这里,客户端会发出两部分信息,
一个是文件名,另一个则是要读取哪一段数据,也就是读取文件的 offset 及 length。
因为所有文件都被切成 64MB 大小的一个 chunk 了,所以根据 offset 和 length,我们
可以很容易地算出客户端要读取的数据在哪几个 chunk 里面。于是,客户端就会告诉
master,我要哪个文件的第几个 chunk。</p>
<p>master 拿到了这个请求之后,就会把这个 chunk 对应的所有副本所在的
chunkserver,告诉客户端。</p>
<p>等客户端拿到 chunk 所在的 chunkserver 信息后,客户端就可以直接去找其中任意的
一个 chunkserver 读取自己所要的数据。</p>
<p><img src="https://cdn.jsdelivr.net/gh/ironartisan/picRepo/20220613163020-2022-06-13-16-30-20.png" alt="20220613163020-2022-06-13-16-30-20" /></p>
<h2 id="master-的快速恢复性和可用性保障">master 的快速恢复性和可用性保障</h2>
<p>master 节点的所有数据,都是保存在内存里的。这样,master 的性能才能跟得上
几百个客户端的并发访问。</p>
<p>但是数据放在内存里带来的问题,就是一旦 master 挂掉,数据就会都丢了。所以,
master 会通过记录操作日志和定期生成对应的 Checkpoints 进行持久化,也就是写到硬
盘上。</p>
<p>这是为了确保在 master 里的这些数据,不会因为一次机器故障就丢失掉。当 master 节点
重启的时候,就会先读取最新的 Checkpoints,然后重放(replay)Checkpoints 之后的
操作日志,把 master 节点的状态恢复到之前最新的状态。这是最常见的存储系统会用到
的可恢复机制。</p>
<p>GFS 还为 master 准备好了几个“备胎”,也就是另外几台 Backup Master。所有针对 master 的数据操作,都需
要同样写到另外准备的这几台服务器上。只有当数据在 master 上操作成功,对应的操作
记录刷新到硬盘上,并且这几个 Backup Master 的数据也写入成功,并把操作记录刷新到
硬盘上,整个操作才会被视为操作成功。</p>
<p>这种方式,叫做数据的“同步复制”,是分布式数据系统里的一种典型模式。假如你需要
一个高可用的 MySQL 集群,一样也可以采用同步复制的方式,在主从服务器之间同步数
据。</p>
<p><img src="https://cdn.jsdelivr.net/gh/ironartisan/picRepo/20220613163713-2022-06-13-16-37-13.png" alt="20220613163713-2022-06-13-16-37-13" /></p>
<p>在恢复时间段里,我们可能仍然有几百个客户端程序“嗷嗷待
哺”,希望能够在 GFS 上读写数据。虽然作为单个 master 的设计,这个时候的确是没有
办法去写入数据的。</p>
<p>但是 Google 的工程师还是想了一个办法,让我们这个时候还能够从 GFS 上读取数据。
这个办法就是加入一系列只读的“影子 Master”,这些影子 Master 和前面的备胎不同,
master 写入数据并不需要等到影子 Master 也写入完成才返回成功。而是影子 Master 不
断同步 master 输入的写入,尽可能保持追上 master 的最新状态。</p>
<p>这种方式,叫做数据的“异步复制”,是分布式系统里另一种典型模式。异步复制下,影
子 Master 并不是和 master 的数据完全同步的,而是可能会有一些小小的延时。</p>
<p>影子 Master 会不断同步 master 里的数据,不过当 master 出现问题的时候,客户端们就
可以从这些影子 Master 里找到自己想要的信息。当然,因为小小的延时,客户端有很小
的概率,会读到一些过时的 master 里面的信息,比如命名空间、文件名等这些元数据。
但你也要知道,这种情况其实只会发生在以下三个条件都满足的情况下:</p>
<p>第一个,是 master 挂掉了;</p>
<p>第二个,是挂掉的 master 或者 Backup Master 上的 Checkpoints 和操作日志,还没
有被影子 Master 同步完;</p>
<p>第三个,则是我们要读取的内容,恰恰是在没有同步完的那部分操作上;</p>
<p><img src="https://cdn.jsdelivr.net/gh/ironartisan/picRepo/20220613170459-2022-06-13-17-04-59.png" alt="20220613170459-2022-06-13-17-04-59" /></p>
<h2 id="gfs-的数据写入">GFS 的数据写入</h2>
<p>GFS 写入数据的具体步骤</p>
<p>第一步,客户端会去问 master 要写入的数据,应该在哪些 chunkserver 上。</p>
<p>第二步,和读数据一样,master 会告诉客户端所有的次副本(secondary replica)所
在的 chunkserver。这还不够,master 还会告诉客户端哪个 replica 是“老大”,也就
是主副本(primary replica),数据此时以它为准。</p>
<p>第三步,拿到数据应该写到哪些 chunkserver 里之后,客户端会把要写的数据发给所有
的 replica。不过此时,chunkserver 拿到发过来的数据后还不会真的写下来,只会把数
据放在一个 LRU 的缓冲区里。</p>
<p>第四步,等到所有次副本都接收完数据后,客户端就会发送一个写请求给到主副本。我
在上节课一开始就说过,GFS 面对的是几百个并发的客户端,所以主副本可能会收到很
多个客户端的写入请求。主副本自己会给这些请求排一个顺序,确保所有的数据写入是
有一个固定顺序的。接下来,主副本就开始按照这个顺序,把刚才 LRU 的缓冲区里的数
据写到实际的 chunk 里去。</p>
<p>第五步,主副本会把对应的写请求转发给所有的次副本,所有次副本会和主副本以同样
的数据写入顺序,把数据写入到硬盘上。</p>
<p>第六步,次副本的数据写入完成之后,会回复主副本,我也把数据和你一样写完了。</p>
<p>第七步,主副本再去告诉客户端,这个数据写入成功了。而如果在任何一个副本写入数
据的过程中出错了,这个出错都会告诉客户端,也就意味着这次写入其实失败了。</p>
<p><img src="https://cdn.jsdelivr.net/gh/ironartisan/picRepo/20220613210148-2022-06-13-21-01-48.png" alt="20220613210148-2022-06-13-21-01-48" /></p>
<h2 id="分离控制流和数据流">分离控制流和数据流</h2>
<p>和之前从 GFS 上读数据一样,GFS 客户端只从 master 拿到了 chunk data 在哪个
chunkserver 的元数据,实际的数据读写都不再需要通过 master。另外,不仅具体的数据
传输不经过 master,后续的数据在多个 chunkserver 上同时写入的协调工作,也不需要
经过 master。</p>
<h2 id="流水线式的网络数据传输">流水线式的网络数据传输</h2>
<p>采用了流水线(pipeline)式的网络传输。数据不一定是先给到主副本,而是看
网络上离哪个 chunkserver 近,就给哪个 chunkserver,数据会先在 chunkserver 的缓冲
区里存起来,就是前面提到的第 3 步。但是写入操作的指令,也就是上面的第 4~7 步,则
都是由客户端发送给主副本,再由主副本统一协调写入顺序、拿到操作结果,再给到客户
端的。</p>
<p>之所以要这么做,还是因为 GFS 最大的瓶颈就在网络。如果用一个最直观的想法来进行数
据传输,我们可以把所有数据直接都从客户端发给三个 chunkserver。</p>
<p>但是这种方法的问题在于,客户端的出口网络会立刻成为瓶颈</p>
<p>而在流水线式的传输方式下,客户端可以先把所有数据,传输给到网络里离自己最近的次
副本 A,然后次副本 A 一边接收数据,一边把对应的数据传输给到离自己最近的另一个副
本,也就是主副本。</p>
<p>同样的,主副本可以如法炮制,把数据也同时传输给次副本 B。在这样的流水线式的数据
传输方式下,只要网络上没有拥堵的情况,只需要 10 秒多一点点,就可以把所有的数据从
客户端,传输到三个副本所在的 chunkserver 上。</p>
<p><img src="https://cdn.jsdelivr.net/gh/ironartisan/picRepo/20220613210607-2022-06-13-21-06-08.png" alt="20220613210607-2022-06-13-21-06-08" /></p>
<p>为什么客户端传输数据,是先给离自己最近的次副本 A,而不是先给主副本呢?</p>
<p><img src="https://cdn.jsdelivr.net/gh/ironartisan/picRepo/20220613210651-2022-06-13-21-06-52.png" alt="20220613210651-2022-06-13-21-06-52" /></p>
<p>我们几百台服务器所在的数据中心,一般都是通过三层交换机连通起来的:</p>
<p>同一个机架(Rack)上的服务器,都会接入到一台接入层交换机(Access Switch)
上;</p>
<p>各个机架上的接入层交换机,都会连接到某一台汇聚层交换机(Aggregation Switch)
上;</p>
<p>而汇聚层交换机,再会连接到多台核心交换机(Core Switch)上。</p>
<p>那么根据这个网络拓扑图,你会发现,两台服务器如果在同一个机架上,它们之间的网络
传输只需要通过接入层的交换机即可。在这种情况下,除了两台服务器本身的网络带宽之
外,它们只会占用所在的接入层交换机的带宽。</p>
<p>但是,如果两台服务器不在一个机架,乃至不在一个 VLAN 的情况下,数据传输就要通过
汇聚层交换机,甚至是核心交换机了。而如果大量的数据传输,都是在多个不同的 VLAN
之间进行的,那么汇聚层交换机乃至核心交换机的带宽,就会成为瓶颈。</p>
<p>所以我们再回到之前的链式传输的场景,GFS 最大利用网络带宽,同时又减少网络瓶颈的
选择就是这样的:</p>
<p>首先,客户端把数据传输给离自己“最近”的,也就是在同一个机架上的次副本 A 服务
器;</p>
<p>然后,次副本 A 服务器再把数据传输给离自己“最近”的,在不同机架,但是处于同一
个汇聚层交换机下的主副本服务器上;</p>
<p>最后,主副本服务器,再把数据传输给在另一个汇聚层交换机下的次副本 B 服务器。</p>
<p>这样的传输顺序,就最大化地利用了每台服务器的带宽,并且减少了交换机的带宽瓶颈。
而如果我们非要先把数据从客户端传输给主副本,再从主副本传输到次副本 A,那么同样
的数据就需要多通过汇聚层交换机一次,从而就占用了更多的汇聚层交换机的资源。</p>
<h2 id="独特的-snapshot-操作">独特的 Snapshot 操作</h2>
<p>GFS 就专门为文件复制设计了一个 Snapshot 指令,当客户端通过这个指令进行文
件复制的时候,这个指令会通过控制流,下达到主副本服务器,主副本服务器再把这个指
令下达到次副本服务器。不过接下来,客户端并不需要去读取或者写入数据,而是各个
chunkserver 会直接在本地把对应的 chunk 复制一份。</p>
<p>这样,数据流就完全不需要通过网络传输了。</p>
<h2 id="放宽数据一致性的要求">放宽数据一致性的要求</h2>
<p>在 GFS 里面,主要定义了对一致性的两个层级的概念:</p>
<p>第一个,就叫做“一致的(Consistent)”。这个就是指,多个客户端无论是从主副本
读取数据,还是从次副本读取数据,读到的数据都是一样的。</p>
<p>第二个,叫做“确定的(Defined)”。这个要求会高一些,指的是对于客户端写入到
GFS 的数据,能够完整地被读到。可能看到这个定义,你还是不清楚,没关系,我下面
会给你详细讲解“确定的”到底是个什么问题。</p>
<p><img src="https://cdn.jsdelivr.net/gh/ironartisan/picRepo/20220613211821-2022-06-13-21-18-22.png" alt="20220613211821-2022-06-13-21-18-22" /></p>
<p>首先,如果数据写入失败,GFS 里的数据就是不一致的。</p>
<p>其次,如果客户端的数据写入是顺序的,并且写入成功了,那么文件里面的内容就是确定的。</p>
<p>但是,如果由多个客户端并发写入数据,即使写入成功了,GFS 里的数据也可能会进入一个一致但是非确定的状态</p>
<p><img src="https://cdn.jsdelivr.net/gh/ironartisan/picRepo/20220613211933-2022-06-13-21-19-33.png" alt="20220613211933-2022-06-13-21-19-33" /></p>
<p>为什么 GFS 的数据写入会出现一致但是非确定的状态呢?这个来自于两个因素。</p>
<p>第一种因素是在 GFS 的数据读写中,为了减轻 Master 的负载,数据的写入顺序并不需要
通过 Master 来进行协调,而是直接由存储了主副本的 chunkserver,来管理同一个
chunk 下数据写入的操作顺序。</p>
<p>第二种因素是随机的数据写入极有可能要跨越多个 chunk。</p>
<p>这个一致但是非确定的状态,是因为随机的数据写入,没有原子性(Atomic)或者
事务性(Transactional)。如果想要随机修改 GFS 上的数据,一般会建议使用方在客户端
的应用层面,保障数据写入是顺序的,从而可以避免并发写入的出现。</p>
<h2 id="追加写入的至少一次的保障">追加写入的“至少一次”的保障</h2>
<p>实际上,这是因为随机写入并不是 GFS 设计的主要的数据写入模式,GFS 设计了一个专门
的操作,叫做记录追加(Record Appends)。这是 GFS 希望我们主要使用的数据写入的
方式,而且它是原子性(Atomic)的,能够做到在并发写入时候是基本确定的。</p>
<p>GFS 的记录追加的写入过程,和上一讲的数据写入几乎一样。它们之间的差别主要在于,
GFS 并不会指定在 chunk 的哪个位置上写入数据,而是告诉最后一个 chunk 所在的主副
本服务器,“我”要进行记录追加。</p>
<p>这个时候,主副本所在的 chunkserver 会做这样几件事情:</p>
<p>检查当前的 chunk 是不是可以写得下现在要追加的记录。如果写得下,那么就把当前的
追加记录写进去,同时,这个数据写入也会发送给其他次副本,在次副本上也写一遍。</p>
<p>如果当前 chunk 已经放不下了,那么它先会把当前 chunk 填满空数据,并且让次副本
也一样填满空数据。然后,主副本会告诉客户端,让它在下一个 chunk 上重新试验。这
时候,客户端就会去一个新的 chunk 所在的 chunkserver 进行记录追加。</p>
<p>因为主副本所在的 chunkserver 控制了数据写入的操作顺序,并且数据只会往后追加,
所以即使在有并发写入的情况下,请求也都会到主副本所在的同一个 chunkserver 上排
队,也就不会有数据写入到同一块区域,覆盖掉已经被追加写入的数据的情况了。</p>
<p>而为了保障 chunk 里能存的下需要追加的数据,GFS 限制了一次记录追加的数据量是
16MB,而 chunkserver 里的一个 chunk 的大小是 64MB。所以,在记录追加需要在
chunk 里填空数据的时候,最多也就是填入 16MB,也就是 chunkserver 的存储空间最
多会浪费 1/4</p>
<p><img src="https://cdn.jsdelivr.net/gh/ironartisan/picRepo/20220613212450-2022-06-13-21-24-51.png" alt="20220613212450-2022-06-13-21-24-51" /></p>
<p>如果在主副本上写入成功了,但是在次副本上写入失败了怎么办呢?这样不是还会出现数据不一致的情况吗?</p>
<p>其实在这个时候,主副本会告诉客户端数据写入失败,然后让客户端重试。不过客户端发
起的重试,并不是在原来的位置去写入数据,而是发起一个新的记录追加操作。这个时
候,可能已经有其他的并发追加写入请求成功了,那么这次重试会写入到更后面。</p>
<p>我们可以一起来看这样一个例子:有三个客户端 X、Y、Z 并发向同一个文件进行记录追
加,写入数据 A、B、C,对应的三个副本的 chunkserver 分别是 Q、P、R。</p>
<p>主副本先收到数据 A 的记录追加,在主副本和次副本上进行数据写入。在 A 写入的同时,
B,C 的记录追加请求也来了,这个时候写入会并行进行,追加在 A 的后面。</p>
<p>这个时候,A 的写入在某个次副本 R 上失败了,于是主副本告诉客户端去重试;同时,客
户端再次发起记录追加的重试,这次的数据写入,不在 A 原来的位置,而会是在 C 后面。
如此一来,在 B 和 C 的写入,以及 A 的重试完成之后,我们可以看到:</p>
<p>所以在这个记录追加的场景下,GFS 承诺的一致性,叫做“至少一次(At Least
Once)”。也就是写入一份数据 A,在重试的情况下,至少会完整地在三个副本的同一个
位置写入一次。但是也可能会因为失败,在某些副本里面写入多次。那么,在不断追加数
在 Q 和 P 上,chunkserver 里面的数据顺序是 A-B-C-A;</p>
<p>但是在 R 上,chunkserver 里面的数据顺序是 N/A-B-C-A;</p>
<p>也就是 Q 和 P 上,A 的数据被写入了两次,而在 R 上,数据里面有一段是有不可用的
脏数据。</p>
<p><img src="https://cdn.jsdelivr.net/gh/ironartisan/picRepo/20220613212824-2022-06-13-21-28-25.png" alt="20220613212824-2022-06-13-21-28-25" /></p>
<p>所以在这个记录追加的场景下,GFS 承诺的一致性,叫做“至少一次(At Least
Once)”。也就是写入一份数据 A,在重试的情况下,至少会完整地在三个副本的同一个
位置写入一次。但是也可能会因为失败,在某些副本里面写入多次。那么,在不断追加数据的情况下,你会看到大部分数据都是一致的,并且是确定的,但是整个文件中,会夹杂
着少数不一致也不确定的数据。</p>
<h2 id="解决一致性的机制">解决一致性的机制</h2>
<p>事实上,GFS 的客户端里面自带了对写入的数据去添加校验和(checksum),并在读取
的时候计算来验证数据完整性的功能。而对于数据可能重复写入多次的问题,你也可以对
每一条要写入的数据生成一个唯一的 ID,并且在里面带上当时的时间戳。这样,即使这些
日志顺序不对、有重复,你也可以很容易地在你后续的数据处理程序中,通过这个 ID 进行
排序和去重。</p>
<p>而这个“至少一次”的写入模型也带来了两个巨大的好处。</p>
<p>第一是高并发和高性能,这个设计使得我们可以有很多个客户端并发向同一个 GFS 上的文
件进行追加写入,而高性能本身也是我们需要分布式系统的起点。</p>
<p>第二是简单,GFS 采用了一个非常简单的单 master server,多个 chunkserver 架构,所
有的协调动作都由 master 来做,而不需要复杂的一致性模型。毕竟,2003 年我们只有极
其难以读懂的 Paxos 论文,Raft 这样的分布式共识算法要在 10 年之后的 2013 年才会诞
生。而简单的架构设计,使得系统不容易出 Bug,出了各种 Bug 也更容易维护。</p>
<h2 id="参考链接">参考链接</h2>
<p><a href="https://time.geekbang.org/column/article/421579">https://time.geekbang.org/column/article/421579</a></p>ironartisanGFS 的设计决策在家如何访问内网机器?内网穿透教程2022-02-13T00:00:00+08:002022-02-13T00:00:00+08:00https://ironartisan.github.io/2022/02/13/%E5%85%B6%E4%BB%96-%E5%86%85%E7%BD%91%E7%A9%BF%E9%80%8F%E6%95%99%E7%A8%8B<h2 id="前言">前言</h2>
<p>每次到放假回家,笔者都会遇到实验室服务器访问不到的困扰。因为实验室服务器没有公网 IP,在外无法直接访问,
经过一番的研究之后,终于找到了一个相对好用的内网穿透方案。之前曾尝试过TeamViewer、花生壳之类的软件,但效果都差强人意。</p>
<ol>
<li>远程桌面使用TeamViewer。可用,但需双方都要安装TeamViewer软件,且版本要一致。</li>
<li>使用花生壳软件进行DDNS解析,可用,但免费版本有带宽限制,使用效果不理想。</li>
<li>搭建frp服务器进行内网穿透,推荐使用,可以达到不错的速度,且可以开放任何想要的端口,可以让处于内网或防火墙后的设备对外界提供服务,它支持HTTP、TCP、UDP等众多协议。</li>
</ol>
<h2 id="准备">准备</h2>
<p>需要准备的东西:</p>
<ol>
<li>一台公网的服务器VPS,笔者使用的是阿里云服务器配置要求不用太高,网速会影响连接的质量</li>
<li>frp软件包</li>
</ol>
<h2 id="教程">教程</h2>
<h3 id="下载frp软件">下载frp软件</h3>
<p>下载frp软件并进行解压</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>wget https://github.com/fatedier/frp/releases/download/v0.39.1/frp_0.39.1_linux_amd64.tar.gz
</code></pre></div></div>
<p>解压后可看到所有文件,但我们只需要关注如下几个文件</p>
<ul>
<li>frps: 服务端启动程序</li>
<li>frps.ini:服务端配置文件</li>
<li>frpc:客户端启动程序</li>
<li>frpc.ini:客户端配置文件</li>
</ul>
<h3 id="配置内网服务器">配置内网服务器</h3>
<p>比如我想映射出内网的 8080 端口,那么需要怎么配置呢?</p>
<p>frpc.ini配置如下:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[common]
# server ip
server_addr = xxx.xxx.xxx.xxx
# server端配置的端口
server_port = 2221
[web]
type = tcp
local_ip = 127.0.0.1
# 本地要映射的端口
local_port = 8080
# server端访问的端口
remote_port = 8080
</code></pre></div></div>
<ul>
<li>server_addr: 公网服务器VPS的IP。</li>
<li>server_port: 服务端设置的端口。</li>
<li>type: 代理的类型。</li>
<li>local_ip: 本地IP。</li>
<li>local_port: 内网客户端设置的端口。</li>
<li>remote_port: 内网提供给外网访问的服务端口。</li>
</ul>
<h3 id="配置公网服务器vps">配置公网服务器VPS</h3>
<p>公网服务器上只需要修改 frps.ini 文件
frps.ini配置如下:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[common]
bind_port = 2221
</code></pre></div></div>
<p>修改后运行 frps,开启服务端程序,然后再内网服务器上执行 frpc 程序,若配置正确,则可连接成功。
服务端会出现以下类似信息。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>2022/02/13 18:27:31 [I] [proxy.go:192] [e37bd8c0f0a34a7c] [ssh25] tcp proxy listen port [8010]
2022/02/13 18:27:31 [I] [control.go:320] [e37bd8c0f0a34a7c] new proxy [ssh25] success
</code></pre></div></div>
<p>因为默认阿里云服务器仅仅开放22等其他常用端口,对于自定义映射的端口可能未开放,所以需要自己开放公网服务器的映射端口,使得在公网上可以访问。</p>
<p><img src="https://cdn.jsdelivr.net/gh/ironartisan/picRepo/20220213175827-2022-02-13-17-58-29.png" alt="20220213175827-2022-02-13-17-58-29" /></p>
<p>配置成功后即可访问http://公网服务器IP:8080,大功告成。</p>ironartisan前言numpy常用命令总结2022-01-27T00:00:00+08:002022-01-27T00:00:00+08:00https://ironartisan.github.io/2022/01/27/numpy-%E5%B8%B8%E7%94%A8%E5%91%BD%E4%BB%A4%E6%80%BB%E7%BB%93<h2 id="concatenate">concatenate</h2>
<p>用来对数列或矩阵进行合并</p>
<ul>
<li>两个一维数组</li>
</ul>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">numpy</span> <span class="k">as</span> <span class="n">np</span>
<span class="n">a</span><span class="o">=</span><span class="p">[</span><span class="mi">1</span><span class="p">,</span><span class="mi">2</span><span class="p">,</span><span class="mi">3</span><span class="p">]</span>
<span class="n">b</span><span class="o">=</span><span class="p">[</span><span class="mi">4</span><span class="p">,</span><span class="mi">5</span><span class="p">,</span><span class="mi">6</span><span class="p">]</span>
<span class="n">np</span><span class="p">.</span><span class="n">concatenate</span><span class="p">((</span><span class="n">a</span><span class="p">,</span><span class="n">b</span><span class="p">),</span><span class="n">axis</span><span class="o">=</span><span class="mi">0</span><span class="p">)</span>
</code></pre></div></div>
<p>输出为</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">array</span><span class="p">([</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">5</span><span class="p">,</span> <span class="mi">6</span><span class="p">])</span>
</code></pre></div></div>
<p>因为上述a和b都是一维的,所以当指定axis=1时,程序就会报错。</p>
<ul>
<li>两个二维数组</li>
</ul>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">numpy</span> <span class="k">as</span> <span class="n">np</span>
<span class="n">a</span><span class="o">=</span><span class="n">np</span><span class="p">.</span><span class="n">array</span><span class="p">([[</span><span class="mi">1</span><span class="p">,</span><span class="mi">2</span><span class="p">,</span><span class="mi">3</span><span class="p">],[</span><span class="mi">111</span><span class="p">,</span><span class="mi">222</span><span class="p">,</span><span class="mi">333</span><span class="p">]])</span>
<span class="n">b</span><span class="o">=</span><span class="n">np</span><span class="p">.</span><span class="n">array</span><span class="p">([[</span><span class="mi">4</span><span class="p">,</span><span class="mi">5</span><span class="p">,</span><span class="mi">6</span><span class="p">],[</span><span class="mi">44</span><span class="p">,</span><span class="mi">55</span><span class="p">,</span><span class="mi">67</span><span class="p">]])</span>
<span class="k">print</span><span class="p">(</span><span class="n">a</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="s">'</span><span class="se">\n</span><span class="s">矩阵a的维度为:'</span><span class="p">,</span><span class="n">a</span><span class="p">.</span><span class="n">shape</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="n">b</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="s">"</span><span class="se">\n</span><span class="s">axis=0的结果为:</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span><span class="n">np</span><span class="p">.</span><span class="n">concatenate</span><span class="p">((</span><span class="n">a</span><span class="p">,</span><span class="n">b</span><span class="p">),</span><span class="n">axis</span><span class="o">=</span><span class="mi">0</span><span class="p">))</span>
<span class="k">print</span><span class="p">(</span><span class="s">"</span><span class="se">\n</span><span class="s">axis=0的维度为:</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span><span class="n">np</span><span class="p">.</span><span class="n">concatenate</span><span class="p">((</span><span class="n">a</span><span class="p">,</span><span class="n">b</span><span class="p">),</span><span class="n">axis</span><span class="o">=</span><span class="mi">0</span><span class="p">).</span><span class="n">shape</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="s">"</span><span class="se">\n</span><span class="s">axis=1的结果为:</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span><span class="n">np</span><span class="p">.</span><span class="n">concatenate</span><span class="p">((</span><span class="n">a</span><span class="p">,</span><span class="n">b</span><span class="p">),</span><span class="n">axis</span><span class="o">=</span><span class="mi">1</span><span class="p">))</span>
<span class="k">print</span><span class="p">(</span><span class="s">"</span><span class="se">\n</span><span class="s">axis=1的维度为:</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span><span class="n">np</span><span class="p">.</span><span class="n">concatenate</span><span class="p">((</span><span class="n">a</span><span class="p">,</span><span class="n">b</span><span class="p">),</span><span class="n">axis</span><span class="o">=</span><span class="mi">1</span><span class="p">).</span><span class="n">shape</span><span class="p">)</span>
</code></pre></div></div>
<p>输出结果为</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[[ 1 2 3]
[111 222 333]]
矩阵a的维度为: (2, 3)
[[ 4 5 6]
[44 55 67]]
axis=0的结果为:
[[ 1 2 3]
[111 222 333]
[ 4 5 6]
[ 44 55 67]]
axis=0的维度为:
(4, 3)
axis=1的结果为:
[[ 1 2 3 4 5 6]
[111 222 333 44 55 67]]
axis=1的维度为:
(2, 6)
</code></pre></div></div>
<p>axis=0 可以看做直接把 <code class="language-plaintext highlighter-rouge">b</code> 矩阵接在 <code class="language-plaintext highlighter-rouge">a</code> 的下面,即按行进行合并,此时的维度为(4,3),axis=1 可以看做将矩阵连接在 <code class="language-plaintext highlighter-rouge">a</code> 的右边,即按列进行合并,此时的维度为(2,6)。</p>
<ul>
<li>两个三维数组</li>
</ul>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">numpy</span> <span class="k">as</span> <span class="n">np</span>
<span class="n">a</span><span class="o">=</span><span class="n">np</span><span class="p">.</span><span class="n">array</span><span class="p">([[[</span><span class="mi">1</span><span class="p">,</span><span class="mi">2</span><span class="p">,</span><span class="mi">3</span><span class="p">],[</span><span class="mi">111</span><span class="p">,</span><span class="mi">222</span><span class="p">,</span><span class="mi">333</span><span class="p">]],[[</span><span class="mi">134</span><span class="p">,</span><span class="mi">131</span><span class="p">,</span><span class="mi">423</span><span class="p">],[</span><span class="mi">134</span><span class="p">,</span><span class="mi">6356</span><span class="p">,</span><span class="mi">754</span><span class="p">]],[[</span><span class="mi">984</span><span class="p">,</span><span class="mi">1940</span><span class="p">,</span><span class="mi">2940</span><span class="p">],[</span><span class="mi">245</span><span class="p">,</span><span class="mi">245</span><span class="p">,</span><span class="mi">546</span><span class="p">]]])</span>
<span class="n">b</span><span class="o">=</span><span class="n">np</span><span class="p">.</span><span class="n">array</span><span class="p">([[[</span><span class="mi">13</span><span class="p">,</span><span class="mi">13</span><span class="p">,</span><span class="mi">35</span><span class="p">],[</span><span class="mi">697</span><span class="p">,</span><span class="mi">2572</span><span class="p">,</span><span class="mi">33633</span><span class="p">]],[[</span><span class="mi">13354</span><span class="p">,</span><span class="mi">132461</span><span class="p">,</span><span class="mi">4723</span><span class="p">],[</span><span class="mi">1374</span><span class="p">,</span><span class="mi">63856</span><span class="p">,</span><span class="mi">754</span><span class="p">]],[[</span><span class="mi">9844</span><span class="p">,</span><span class="mi">19640</span><span class="p">,</span><span class="mi">2940</span><span class="p">],[</span><span class="mi">23645</span><span class="p">,</span><span class="mi">23645</span><span class="p">,</span><span class="mi">56346</span><span class="p">]]])</span>
<span class="k">print</span><span class="p">(</span><span class="n">a</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="s">'</span><span class="se">\n</span><span class="s">矩阵a的维度为:'</span><span class="p">,</span><span class="n">a</span><span class="p">.</span><span class="n">shape</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="n">b</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="s">"</span><span class="se">\n</span><span class="s">axis=0的结果为:</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span><span class="n">np</span><span class="p">.</span><span class="n">concatenate</span><span class="p">((</span><span class="n">a</span><span class="p">,</span><span class="n">b</span><span class="p">),</span><span class="n">axis</span><span class="o">=</span><span class="mi">0</span><span class="p">))</span> <span class="c1">#axis=0表示在行上加,axis=1表示在列上加
</span><span class="k">print</span><span class="p">(</span><span class="s">"</span><span class="se">\n</span><span class="s">axis=0的维度为:</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span><span class="n">np</span><span class="p">.</span><span class="n">concatenate</span><span class="p">((</span><span class="n">a</span><span class="p">,</span><span class="n">b</span><span class="p">),</span><span class="n">axis</span><span class="o">=</span><span class="mi">0</span><span class="p">).</span><span class="n">shape</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="s">"</span><span class="se">\n</span><span class="s">axis=1的结果为:</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span><span class="n">np</span><span class="p">.</span><span class="n">concatenate</span><span class="p">((</span><span class="n">a</span><span class="p">,</span><span class="n">b</span><span class="p">),</span><span class="n">axis</span><span class="o">=</span><span class="mi">1</span><span class="p">))</span> <span class="c1">#axis=0表示在行上加,axis=1表示在列上加
</span><span class="k">print</span><span class="p">(</span><span class="s">"</span><span class="se">\n</span><span class="s">axis=1的维度为:</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span><span class="n">np</span><span class="p">.</span><span class="n">concatenate</span><span class="p">((</span><span class="n">a</span><span class="p">,</span><span class="n">b</span><span class="p">),</span><span class="n">axis</span><span class="o">=</span><span class="mi">1</span><span class="p">).</span><span class="n">shape</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="s">"</span><span class="se">\n</span><span class="s">axis=2的结果为:</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span><span class="n">np</span><span class="p">.</span><span class="n">concatenate</span><span class="p">((</span><span class="n">a</span><span class="p">,</span><span class="n">b</span><span class="p">),</span><span class="n">axis</span><span class="o">=</span><span class="mi">2</span><span class="p">))</span> <span class="c1">#axis=0表示在行上加,axis=1表示在列上加
</span><span class="k">print</span><span class="p">(</span><span class="s">"</span><span class="se">\n</span><span class="s">axis=2的维度为:</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span><span class="n">np</span><span class="p">.</span><span class="n">concatenate</span><span class="p">((</span><span class="n">a</span><span class="p">,</span><span class="n">b</span><span class="p">),</span><span class="n">axis</span><span class="o">=</span><span class="mi">2</span><span class="p">).</span><span class="n">shape</span><span class="p">)</span>
</code></pre></div></div>
<p>a和b是两个三维数组,其维度为(3,2,3),输出的结果如下</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[[[</span> <span class="mi">1</span> <span class="mi">2</span> <span class="mi">3</span><span class="p">]</span>
<span class="p">[</span> <span class="mi">111</span> <span class="mi">222</span> <span class="mi">333</span><span class="p">]]</span>
<span class="p">[[</span> <span class="mi">134</span> <span class="mi">131</span> <span class="mi">423</span><span class="p">]</span>
<span class="p">[</span> <span class="mi">134</span> <span class="mi">6356</span> <span class="mi">754</span><span class="p">]]</span>
<span class="p">[[</span> <span class="mi">984</span> <span class="mi">1940</span> <span class="mi">2940</span><span class="p">]</span>
<span class="p">[</span> <span class="mi">245</span> <span class="mi">245</span> <span class="mi">546</span><span class="p">]]]</span>
<span class="n">矩阵a的维度为</span><span class="p">:</span> <span class="p">(</span><span class="mi">3</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">)</span>
<span class="p">[[[</span> <span class="mi">13</span> <span class="mi">13</span> <span class="mi">35</span><span class="p">]</span>
<span class="p">[</span> <span class="mi">697</span> <span class="mi">2572</span> <span class="mi">33633</span><span class="p">]]</span>
<span class="p">[[</span> <span class="mi">13354</span> <span class="mi">132461</span> <span class="mi">4723</span><span class="p">]</span>
<span class="p">[</span> <span class="mi">1374</span> <span class="mi">63856</span> <span class="mi">754</span><span class="p">]]</span>
<span class="p">[[</span> <span class="mi">9844</span> <span class="mi">19640</span> <span class="mi">2940</span><span class="p">]</span>
<span class="p">[</span> <span class="mi">23645</span> <span class="mi">23645</span> <span class="mi">56346</span><span class="p">]]]</span>
<span class="n">axis</span><span class="o">=</span><span class="mi">0</span><span class="n">的结果为</span><span class="p">:</span>
<span class="p">[[[</span> <span class="mi">1</span> <span class="mi">2</span> <span class="mi">3</span><span class="p">]</span>
<span class="p">[</span> <span class="mi">111</span> <span class="mi">222</span> <span class="mi">333</span><span class="p">]]</span>
<span class="p">[[</span> <span class="mi">134</span> <span class="mi">131</span> <span class="mi">423</span><span class="p">]</span>
<span class="p">[</span> <span class="mi">134</span> <span class="mi">6356</span> <span class="mi">754</span><span class="p">]]</span>
<span class="p">[[</span> <span class="mi">984</span> <span class="mi">1940</span> <span class="mi">2940</span><span class="p">]</span>
<span class="p">[</span> <span class="mi">245</span> <span class="mi">245</span> <span class="mi">546</span><span class="p">]]</span>
<span class="p">[[</span> <span class="mi">13</span> <span class="mi">13</span> <span class="mi">35</span><span class="p">]</span>
<span class="p">[</span> <span class="mi">697</span> <span class="mi">2572</span> <span class="mi">33633</span><span class="p">]]</span>
<span class="p">[[</span> <span class="mi">13354</span> <span class="mi">132461</span> <span class="mi">4723</span><span class="p">]</span>
<span class="p">[</span> <span class="mi">1374</span> <span class="mi">63856</span> <span class="mi">754</span><span class="p">]]</span>
<span class="p">[[</span> <span class="mi">9844</span> <span class="mi">19640</span> <span class="mi">2940</span><span class="p">]</span>
<span class="p">[</span> <span class="mi">23645</span> <span class="mi">23645</span> <span class="mi">56346</span><span class="p">]]]</span>
<span class="n">axis</span><span class="o">=</span><span class="mi">0</span><span class="n">的维度为</span><span class="p">:</span>
<span class="p">(</span><span class="mi">6</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">)</span>
<span class="n">axis</span><span class="o">=</span><span class="mi">1</span><span class="n">的结果为</span><span class="p">:</span>
<span class="p">[[[</span> <span class="mi">1</span> <span class="mi">2</span> <span class="mi">3</span><span class="p">]</span>
<span class="p">[</span> <span class="mi">111</span> <span class="mi">222</span> <span class="mi">333</span><span class="p">]</span>
<span class="p">[</span> <span class="mi">13</span> <span class="mi">13</span> <span class="mi">35</span><span class="p">]</span>
<span class="p">[</span> <span class="mi">697</span> <span class="mi">2572</span> <span class="mi">33633</span><span class="p">]]</span>
<span class="p">[[</span> <span class="mi">134</span> <span class="mi">131</span> <span class="mi">423</span><span class="p">]</span>
<span class="p">[</span> <span class="mi">134</span> <span class="mi">6356</span> <span class="mi">754</span><span class="p">]</span>
<span class="p">[</span> <span class="mi">13354</span> <span class="mi">132461</span> <span class="mi">4723</span><span class="p">]</span>
<span class="p">[</span> <span class="mi">1374</span> <span class="mi">63856</span> <span class="mi">754</span><span class="p">]]</span>
<span class="p">[[</span> <span class="mi">984</span> <span class="mi">1940</span> <span class="mi">2940</span><span class="p">]</span>
<span class="p">[</span> <span class="mi">245</span> <span class="mi">245</span> <span class="mi">546</span><span class="p">]</span>
<span class="p">[</span> <span class="mi">9844</span> <span class="mi">19640</span> <span class="mi">2940</span><span class="p">]</span>
<span class="p">[</span> <span class="mi">23645</span> <span class="mi">23645</span> <span class="mi">56346</span><span class="p">]]]</span>
<span class="n">axis</span><span class="o">=</span><span class="mi">1</span><span class="n">的维度为</span><span class="p">:</span>
<span class="p">(</span><span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">3</span><span class="p">)</span>
<span class="n">axis</span><span class="o">=</span><span class="mi">2</span><span class="n">的结果为</span><span class="p">:</span>
<span class="p">[[[</span> <span class="mi">1</span> <span class="mi">2</span> <span class="mi">3</span> <span class="mi">13</span> <span class="mi">13</span> <span class="mi">35</span><span class="p">]</span>
<span class="p">[</span> <span class="mi">111</span> <span class="mi">222</span> <span class="mi">333</span> <span class="mi">697</span> <span class="mi">2572</span> <span class="mi">33633</span><span class="p">]]</span>
<span class="p">[[</span> <span class="mi">134</span> <span class="mi">131</span> <span class="mi">423</span> <span class="mi">13354</span> <span class="mi">132461</span> <span class="mi">4723</span><span class="p">]</span>
<span class="p">[</span> <span class="mi">134</span> <span class="mi">6356</span> <span class="mi">754</span> <span class="mi">1374</span> <span class="mi">63856</span> <span class="mi">754</span><span class="p">]]</span>
<span class="p">[[</span> <span class="mi">984</span> <span class="mi">1940</span> <span class="mi">2940</span> <span class="mi">9844</span> <span class="mi">19640</span> <span class="mi">2940</span><span class="p">]</span>
<span class="p">[</span> <span class="mi">245</span> <span class="mi">245</span> <span class="mi">546</span> <span class="mi">23645</span> <span class="mi">23645</span> <span class="mi">56346</span><span class="p">]]]</span>
<span class="n">axis</span><span class="o">=</span><span class="mi">2</span><span class="n">的维度为</span><span class="p">:</span>
<span class="p">(</span><span class="mi">3</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">6</span><span class="p">)</span>
</code></pre></div></div>
<p>axis=0,则表示合并后第一个维度数据要变(axis是从0开始计算的,即第一维表示0),axis=1,则表示合并后第二个维度的数据要变,axis=2,则表示合并后第三个维度数据要变。数据变换一般是两个数组相同维度数值相加。</p>
<h2 id="与-torchcat-对比">与 torch.cat 对比</h2>
<ol>
<li>
<p>字面理解:torch.cat是将两个张量(tensor)拼接在一起,cat是concatenate的意思,即拼接,联系在一起。</p>
</li>
<li>
<p>例子理解</p>
</li>
</ol>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">>>></span> <span class="kn">import</span> <span class="nn">torch</span>
<span class="o">>>></span> <span class="n">A</span><span class="o">=</span><span class="n">torch</span><span class="p">.</span><span class="n">ones</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span><span class="mi">3</span><span class="p">)</span> <span class="c1">#2x3的张量(矩阵)
</span><span class="o">>>></span> <span class="n">A</span>
<span class="n">tensor</span><span class="p">([[</span> <span class="mf">1.</span><span class="p">,</span> <span class="mf">1.</span><span class="p">,</span> <span class="mf">1.</span><span class="p">],</span>
<span class="p">[</span> <span class="mf">1.</span><span class="p">,</span> <span class="mf">1.</span><span class="p">,</span> <span class="mf">1.</span><span class="p">]])</span>
<span class="o">>>></span> <span class="n">B</span><span class="o">=</span><span class="mi">2</span><span class="o">*</span><span class="n">torch</span><span class="p">.</span><span class="n">ones</span><span class="p">(</span><span class="mi">4</span><span class="p">,</span><span class="mi">3</span><span class="p">)</span><span class="c1">#4x3的张量(矩阵)
</span><span class="o">>>></span> <span class="n">B</span>
<span class="n">tensor</span><span class="p">([[</span> <span class="mf">2.</span><span class="p">,</span> <span class="mf">2.</span><span class="p">,</span> <span class="mf">2.</span><span class="p">],</span>
<span class="p">[</span> <span class="mf">2.</span><span class="p">,</span> <span class="mf">2.</span><span class="p">,</span> <span class="mf">2.</span><span class="p">],</span>
<span class="p">[</span> <span class="mf">2.</span><span class="p">,</span> <span class="mf">2.</span><span class="p">,</span> <span class="mf">2.</span><span class="p">],</span>
<span class="p">[</span> <span class="mf">2.</span><span class="p">,</span> <span class="mf">2.</span><span class="p">,</span> <span class="mf">2.</span><span class="p">]])</span>
<span class="o">>>></span> <span class="n">C</span><span class="o">=</span><span class="n">torch</span><span class="p">.</span><span class="n">cat</span><span class="p">((</span><span class="n">A</span><span class="p">,</span><span class="n">B</span><span class="p">),</span><span class="mi">0</span><span class="p">)</span><span class="c1">#按维数0(行)拼接
</span><span class="o">>>></span> <span class="n">C</span>
<span class="n">tensor</span><span class="p">([[</span> <span class="mf">1.</span><span class="p">,</span> <span class="mf">1.</span><span class="p">,</span> <span class="mf">1.</span><span class="p">],</span>
<span class="p">[</span> <span class="mf">1.</span><span class="p">,</span> <span class="mf">1.</span><span class="p">,</span> <span class="mf">1.</span><span class="p">],</span>
<span class="p">[</span> <span class="mf">2.</span><span class="p">,</span> <span class="mf">2.</span><span class="p">,</span> <span class="mf">2.</span><span class="p">],</span>
<span class="p">[</span> <span class="mf">2.</span><span class="p">,</span> <span class="mf">2.</span><span class="p">,</span> <span class="mf">2.</span><span class="p">],</span>
<span class="p">[</span> <span class="mf">2.</span><span class="p">,</span> <span class="mf">2.</span><span class="p">,</span> <span class="mf">2.</span><span class="p">],</span>
<span class="p">[</span> <span class="mf">2.</span><span class="p">,</span> <span class="mf">2.</span><span class="p">,</span> <span class="mf">2.</span><span class="p">]])</span>
<span class="o">>>></span> <span class="n">C</span><span class="p">.</span><span class="n">size</span><span class="p">()</span>
<span class="n">torch</span><span class="p">.</span><span class="n">Size</span><span class="p">([</span><span class="mi">6</span><span class="p">,</span> <span class="mi">3</span><span class="p">])</span>
<span class="o">>>></span> <span class="n">D</span><span class="o">=</span><span class="mi">2</span><span class="o">*</span><span class="n">torch</span><span class="p">.</span><span class="n">ones</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span><span class="mi">4</span><span class="p">)</span> <span class="c1">#2x4的张量(矩阵)
</span><span class="o">>>></span> <span class="n">C</span><span class="o">=</span><span class="n">torch</span><span class="p">.</span><span class="n">cat</span><span class="p">((</span><span class="n">A</span><span class="p">,</span><span class="n">D</span><span class="p">),</span><span class="mi">1</span><span class="p">)</span><span class="c1">#按维数1(列)拼接
</span><span class="o">>>></span> <span class="n">C</span>
<span class="n">tensor</span><span class="p">([[</span> <span class="mf">1.</span><span class="p">,</span> <span class="mf">1.</span><span class="p">,</span> <span class="mf">1.</span><span class="p">,</span> <span class="mf">2.</span><span class="p">,</span> <span class="mf">2.</span><span class="p">,</span> <span class="mf">2.</span><span class="p">,</span> <span class="mf">2.</span><span class="p">],</span>
<span class="p">[</span> <span class="mf">1.</span><span class="p">,</span> <span class="mf">1.</span><span class="p">,</span> <span class="mf">1.</span><span class="p">,</span> <span class="mf">2.</span><span class="p">,</span> <span class="mf">2.</span><span class="p">,</span> <span class="mf">2.</span><span class="p">,</span> <span class="mf">2.</span><span class="p">]])</span>
<span class="o">>>></span> <span class="n">C</span><span class="p">.</span><span class="n">size</span><span class="p">()</span>
<span class="n">torch</span><span class="p">.</span><span class="n">Size</span><span class="p">([</span><span class="mi">2</span><span class="p">,</span> <span class="mi">7</span><span class="p">])</span>
</code></pre></div></div>
<p>上面给出了两个张量A和B,分别是2行3列,4行3列。即他们都是2维张量。因为只有两维,这样在用torch.cat拼接的时候就有两种拼接方式:按行拼接和按列拼接。即所谓的维数0和维数1.</p>
<p>C=torch.cat((A,B),0)就表示按维数0(行)拼接A和B,也就是竖着拼接,A上B下。此时需要注意:列数必须一致,即维数1数值要相同,这里都是3列,方能列对齐。拼接后的C的第0维是两个维数0数值和,即2+4=6.</p>
<p>C=torch.cat((A,B),1)就表示按维数1(列)拼接A和B,也就是横着拼接,A左B右。此时需要注意:行数必须一致,即维数0数值要相同,这里都是2行,方能行对齐。拼接后的C的第1维是两个维数1数值和,即3+4=7.</p>
<p>从2维例子可以看出,使用torch.cat((A,B),dim)时,除拼接维数dim数值可不同外其余维数数值需相同,方能对齐。</p>
<h2 id="参考链接">参考链接</h2>
<ul>
<li><a href="https://blog.csdn.net/qq_39709535/article/details/80803003">https://blog.csdn.net/qq_39709535/article/details/80803003</a></li>
</ul>ironartisanconcatenate0.注意力机制2022-01-27T00:00:00+08:002022-01-27T00:00:00+08:00https://ironartisan.github.io/2022/01/27/%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0-0.%E6%B3%A8%E6%84%8F%E5%8A%9B%E6%9C%BA%E5%88%B6<h2 id="什么是注意力机制">什么是注意力机制?</h2>
<p>Transformer、BERT等模型在NLP领域取得了突破,其模型主要依赖了注意力机制(Attention Mechanism)。注意力Attention机制被应用到越来越多的地方,那么注意力Attention机制的原理和本质到底是什么?</p>
<h2 id="发展历史">发展历史</h2>
<p>Attention的发展主要经历了两个阶段:</p>
<p>一、2017年前,Attention开始被广泛应用在各类NLP任务上。各种各样的花式Attention被提出,比如用在机器翻译上的Bahdanau Attention等。这一阶段的Attention常常和RNN、CNN结合。</p>
<p>二、2017年之后是Transformer的时代。2017年,Transformer模型被提出,Transformer完全抛弃了RNN结构,突破了RNN无法并行化的缺点。之后BERT使用了Transformer中的Encoder部分。</p>
<h2 id="基本原理">基本原理</h2>
<p>Attention(注意力)机制如果浅层的理解,跟他的名字非常匹配。他的核心逻辑就是「从关注全部到关注重点」。从“Attention”这个名字可以读出,Attention机制主要是对注意力的捕捉。Attention的原理与大脑处理信息有一些相似。比如看到下面这张图,短时间内大脑可能只对图片中的“锦江饭店”有印象,即注意力集中在了“锦江饭店”处。短时间内,大脑可能并没有注意到锦江饭店上面有一串电话号码,下面有几个行人,后面还有“喜运来大酒家”等信息。</p>
<p><img src="https://cdn.jsdelivr.net/gh/ironartisan/picRepo/20220508084936-2022-05-08-08-49-36.png" alt="20220508084936-2022-05-08-08-49-36" /></p>
<p>所以,大脑在短时间内处理信息时,主要将图片中最吸引人注意力的部分读出来了,类似下面。</p>
<p><img src="https://cdn.jsdelivr.net/gh/ironartisan/picRepo/20220508084951-2022-05-08-08-49-51.png" alt="20220508084951-2022-05-08-08-49-51" /></p>
<h2 id="attention机制">Attention机制</h2>
<p>Attention的输入由三部分构成:Query、Key和Value。其中,(Key, Value)是具有相互关联的KV对,Query是输入的“问题”,Attention可以将Query转化为与Query最相关的向量表示。</p>
<p>Attention的计算主要分3步,如下图所示。</p>
<p><img src="https://cdn.jsdelivr.net/gh/ironartisan/picRepo/20220508085039-2022-05-08-08-50-39.png" alt="20220508085039-2022-05-08-08-50-39" /></p>
<p>第一步:Query和Key进行相似度计算,得到Attention Score;</p>
<p>第二步:对Attention Score进行Softmax归一化,得到权值矩阵;</p>
<p>第三步:权重矩阵与Value进行加权求和计算。</p>
<p>Query、Key和Value的含义是什么呢?我们以刚才大脑读图为例。Value可以理解为人眼视网膜对整张图片信息的原始捕捉,不受“注意力”所影响。我们可以将Value理解为像素级别的信息,那么假设只要一张图片呈现在人眼面前,图片中的像素都会被视网膜捕捉到。Key与Value相关联,Key是图片原始信息所对应的关键性提示信息,比如“锦江饭店”部分是将图片中的原始像素信息抽象为中文文字和牌匾的提示信息。一个中文读者看到这张图片时,读者大脑有意识地向图片获取信息,即发起了一次Query,Query中包含了读者的意图等信息。在一次读图过程中,Query与Key之间计算出Attention Score,得到最具有吸引力的部分,并只对具有吸引力的Value信息进行提取,反馈到大脑中。就像上面的例子中,经过大脑的注意力机制的筛选,一次Query后,大脑只关注“锦江饭店”的牌匾部分。</p>
<p>再以一个搜索引擎的检索为例。使用某个Query去搜索引擎里搜索,搜索引擎里面有好多文章,每个文章的全文可以被理解成Value;文章的关键性信息是标题,可以将标题认为是Key。搜索引擎用Query和那些文章们的标题(Key)进行匹配,看看相似度(计算Attention Score)。我们想得到跟Query相关的知识,于是用这些相似度将检索的文章Value做一个加权和,那么就得到了一个新的信息,新的信息融合了相关性强的文章们,而相关性弱的文章可能被过滤掉。</p>
<h2 id="attention的优点">Attention的优点</h2>
<p>之所以要引入 Attention 机制,主要是3个原因:</p>
<ol>
<li>参数少
模型复杂度跟 CNN、RNN 相比,复杂度更小,参数也更少。所以对算力的要求也就更小。</li>
<li>速度快
Attention 解决了 RNN 不能并行计算的问题。Attention机制每一步计算不依赖于上一步的计算结果,因此可以和CNN一样并行处理。</li>
<li>效果好
在 Attention 机制引入之前,有一个问题大家一直很苦恼:长距离的信息会被弱化,就好像记忆能力弱的人,记不住过去的事情是一样的。
Attention 是挑重点,就算文本比较长,也能从中间抓住重点,不丢失重要的信息。下图红色的预期就是被挑出来的重点。</li>
</ol>
<h2 id="attention分类">Attention分类</h2>
<h2 id="参考链接">参考链接</h2>
<ul>
<li><a href="https://easyaitech.medium.com/%E4%B8%80%E6%96%87%E7%9C%8B%E6%87%82-attention-%E6%9C%AC%E8%B4%A8%E5%8E%9F%E7%90%86-3%E5%A4%A7%E4%BC%98%E7%82%B9-5%E5%A4%A7%E7%B1%BB%E5%9E%8B-e4fbe4b6d030">https://easyaitech.medium.com/%E4%B8%80%E6%96%87%E7%9C%8B%E6%87%82-attention-%E6%9C%AC%E8%B4%A8%E5%8E%9F%E7%90%86-3%E5%A4%A7%E4%BC%98%E7%82%B9-5%E5%A4%A7%E7%B1%BB%E5%9E%8B-e4fbe4b6d030</a></li>
<li><a href="https://lulaoshi.info/machine-learning/attention">https://lulaoshi.info/machine-learning/attention</a></li>
</ul>ironartisan什么是注意力机制? Transformer、BERT等模型在NLP领域取得了突破,其模型主要依赖了注意力机制(Attention Mechanism)。注意力Attention机制被应用到越来越多的地方,那么注意力Attention机制的原理和本质到底是什么?5.队列基础2022-01-25T00:00:00+08:002022-01-25T00:00:00+08:00https://ironartisan.github.io/2022/01/25/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95%E4%B9%8B%E7%BE%8E-5.%E9%98%9F%E5%88%97<h2 id="前言">前言</h2>
<p>我们知道,CPU 资源是有限的,任务的处理速度与线程个数并不是线性正相关。相反,过多的线程反而会导致 CPU 频繁切换,处理性能下降。所以,线程池的大小一般都是综合考虑要处理任务的特点和硬件环境,来事先设置的。当我们向固定大小的线程池中请求一个线程时,如果线程池中没有空闲资源了,这个时候线程池如何处理这个请求?是拒绝请求还是排队请求?各种处理策略又是怎么实现的呢?实际上,这些问题并不复杂,其底层的数据结构就是我们今天要学的内容,队列
(queue)。</p>
<h2 id="如何理解队列">如何理解“队列”?</h2>
<p>队列这个概念非常好理解。你可以把它想象成排队买票,先来的先买,后来的人只能站末尾,不允许插队。先进者先出,这就是典型的“队列”。</p>
<p>我们知道,栈只支持两个基本操作:入栈 push()和出栈 pop()。队列跟栈非常相似,支持的操作也很有限,最基本的操作也是两个:入队 enqueue(),放一个数据到队列尾部;出队 dequeue(),从队列头部取一个元素。</p>
<p><img src="https://cdn.jsdelivr.net/gh/ironartisan/picRepo/20220124222045-2022-01-24-22-20-46.png" alt="20220124222045-2022-01-24-22-20-46" /></p>
<p>所以,队列跟栈一样,也是一种操作受限的线性表数据结构。</p>
<p>队列的概念很好理解,基本操作也很容易掌握。作为一种非常基础的数据结构,队列的应用也非常广泛,特别是一些具有某些额外特性的队列,比如循环队列、阻塞队列、并发队列。它们在很多偏底层系统、框架、中间件的开发中,起着关键性的作用。比如高性能队列 Disruptor、Linux 环形缓存,都用到了循环并发队列;Java concurrent 并发包利用ArrayBlockingQueue 来实现公平锁等。</p>
<h2 id="顺序队列和链式队列">顺序队列和链式队列</h2>
<p>队列跟栈一样,也是一种抽象的数据结构。它具有先进先出的特性,支持在队尾插入元素,在队头删除元素,那究竟该如何实现一个队列呢?</p>
<p>跟栈一样,队列可以用数组来实现,也可以用链表来实现。用数组实现的栈叫作顺序栈,用链表实现的栈叫作链式栈。同样,用数组实现的队列叫作顺序队列,用链表实现的队列叫作链式队列。</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 用数组实现的队列</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">ArrayQueue</span> <span class="o">{</span>
<span class="c1">// 数组:items,数组大小:n</span>
<span class="kd">private</span> <span class="nc">String</span><span class="o">[]</span> <span class="n">items</span><span class="o">;</span>
<span class="kd">private</span> <span class="kt">int</span> <span class="n">n</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span>
<span class="c1">// head表示队头下标,tail表示队尾下标</span>
<span class="kd">private</span> <span class="kt">int</span> <span class="n">head</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span>
<span class="kd">private</span> <span class="kt">int</span> <span class="n">tail</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span>
<span class="c1">// 申请一个大小为capacity的数组</span>
<span class="kd">public</span> <span class="nf">ArrayQueue</span><span class="o">(</span><span class="kt">int</span> <span class="n">capacity</span><span class="o">)</span> <span class="o">{</span>
<span class="n">items</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">String</span><span class="o">[</span><span class="n">capacity</span><span class="o">];</span>
<span class="n">n</span> <span class="o">=</span> <span class="n">capacity</span><span class="o">;</span>
<span class="o">}</span>
<span class="c1">// 入队</span>
<span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">enqueue</span><span class="o">(</span><span class="nc">String</span> <span class="n">item</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">// 如果tail == n 表示队列已经满了</span>
<span class="k">if</span> <span class="o">(</span><span class="n">tail</span> <span class="o">==</span> <span class="n">n</span><span class="o">)</span> <span class="k">return</span> <span class="kc">false</span><span class="o">;</span>
<span class="n">items</span><span class="o">[</span><span class="n">tail</span><span class="o">]</span> <span class="o">=</span> <span class="n">item</span><span class="o">;</span>
<span class="o">++</span><span class="n">tail</span><span class="o">;</span>
<span class="k">return</span> <span class="kc">true</span><span class="o">;</span>
<span class="o">}</span>
<span class="c1">// 出队</span>
<span class="kd">public</span> <span class="nc">String</span> <span class="nf">dequeue</span><span class="o">()</span> <span class="o">{</span>
<span class="c1">// 如果head == tail 表示队列为空</span>
<span class="k">if</span> <span class="o">(</span><span class="n">head</span> <span class="o">==</span> <span class="n">tail</span><span class="o">)</span> <span class="k">return</span> <span class="kc">null</span><span class="o">;</span>
<span class="c1">// 为了让其他语言的同学看的更加明确,把--操作放到单独一行来写了</span>
<span class="nc">String</span> <span class="n">ret</span> <span class="o">=</span> <span class="n">items</span><span class="o">[</span><span class="n">head</span><span class="o">];</span>
<span class="o">++</span><span class="n">head</span><span class="o">;</span>
<span class="k">return</span> <span class="n">ret</span><span class="o">;</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>对于栈来说,我们只需要一个栈顶指针就可以了。但是队列需要两个指针:一个是 head 指针,指向队头;一个是 tail 指针,指向队尾。</p>
<p>结合下面这张图来理解。当 a、b、c、d 依次入队之后,队列中的 head 指针指向下标为 0 的位置,tail 指针指向下标为 4 的位置。</p>
<p><img src="https://cdn.jsdelivr.net/gh/ironartisan/picRepo/20220124222822-2022-01-24-22-28-25.png" alt="20220124222822-2022-01-24-22-28-25" /></p>
<p>当我们调用两次出队操作之后,队列中 head 指针指向下标为 2 的位置,tail 指针仍然指向下标为 4 的位置</p>
<p><img src="https://cdn.jsdelivr.net/gh/ironartisan/picRepo/20220124223200-2022-01-24-22-32-02.png" alt="20220124223200-2022-01-24-22-32-02" /></p>
<p>随着不停地进行入队、出队操作,head 和 tail 都会持续往后移动。当 tail 移动到最右边,即使数组中还有空闲空间,也无法继续往队列中添加数据了。这个问题该如何解决呢?</p>
<p>数组的删除操作会导致数组中的数据不连续。你还记得我们当时是怎么解决的吗?对,用数据搬移!但是,每次进行出队操作都相当于删除数组下标为 0 的数据,要搬移整个队列中的数据,这样出队操作的时间复杂度就会从原来的 O(1) 变为 O(n)。能不能优化一下呢?</p>
<p>实际上,我们在出队时可以不用搬移数据。如果没有空闲空间了,我们只需要在入队时,再集中触发一次数据的搬移操作。借助这个思想,出队函数 dequeue() 保持不变,我们稍加改造一下入队函数 enqueue() 的实现,就可以轻松解决刚才的问题了。下面是具体的代码:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 入队操作,将item放入队尾</span>
<span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">enqueue</span><span class="o">(</span><span class="nc">String</span> <span class="n">item</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">// tail == n表示队列末尾没有空间了</span>
<span class="k">if</span> <span class="o">(</span><span class="n">tail</span> <span class="o">==</span> <span class="n">n</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">// tail ==n && head==0,表示整个队列都占满了</span>
<span class="k">if</span> <span class="o">(</span><span class="n">head</span> <span class="o">==</span> <span class="mi">0</span><span class="o">)</span> <span class="k">return</span> <span class="kc">false</span><span class="o">;</span>
<span class="c1">// 数据搬移</span>
<span class="k">for</span> <span class="o">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="n">head</span><span class="o">;</span> <span class="n">i</span> <span class="o"><</span> <span class="n">tail</span><span class="o">;</span> <span class="o">++</span><span class="n">i</span><span class="o">)</span> <span class="o">{</span>
<span class="n">items</span><span class="o">[</span><span class="n">i</span><span class="o">-</span><span class="n">head</span><span class="o">]</span> <span class="o">=</span> <span class="n">items</span><span class="o">[</span><span class="n">i</span><span class="o">];</span>
<span class="o">}</span>
<span class="c1">// 搬移完之后重新更新head和tail</span>
<span class="n">tail</span> <span class="o">-=</span> <span class="n">head</span><span class="o">;</span>
<span class="n">head</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span>
<span class="o">}</span>
<span class="n">items</span><span class="o">[</span><span class="n">tail</span><span class="o">]</span> <span class="o">=</span> <span class="n">item</span><span class="o">;</span>
<span class="o">++</span><span class="n">tail</span><span class="o">;</span>
<span class="k">return</span> <span class="kc">true</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>
<p>当队列的 tail 指针移动到数组的最右边后,如果有新的数据入队,我们可以将 head 到 tail 之间的数据,整体搬移到数组中 0 到 tail-head 的位置。</p>
<p><img src="https://cdn.jsdelivr.net/gh/ironartisan/picRepo/20220124223947-2022-01-24-22-39-51.png" alt="20220124223947-2022-01-24-22-39-51" /></p>
<p>出队操作的时间复杂度仍然是 O(1),但入队操作的时间复杂度还是 O(1)吗?</p>
<p>在正常情况下,队列的入队和出队操作时间复杂度都是O(1),在进行“数据搬移”改造的情况下,入队的时间复杂度:</p>
<p>如果队尾没有满,可以直接入队,时间复杂度为O(1)。</p>
<p>如果队尾已满的情况下,就必须进行数据搬移了,tail=n,搬移的时间复杂度为O(n).</p>
<p>总体情况来看,tail的可能是0~n的任意值,在0~n-1的时候队列入队的时间复杂度都是O(1),不需要搬移直接入队即可;只有当tail=n的时候时间复杂度才迅速飙升为O(n),即需要进行n次搬移,此时n次的搬移如果均摊到0~n-1这n次上,其实总体的均摊复杂度还是O(1)。</p>
<p>接下来,我们再来看下基于链表的队列实现方法。</p>
<p>基于链表的实现,我们同样需要两个指针:head 指针和 tail 指针。它们分别指向链表的第一个结点和最后一个结点。如图所示,入队时,tail->next= new_node, tail = tail->next;
出队时,head = head->next。</p>
<p><img src="https://cdn.jsdelivr.net/gh/ironartisan/picRepo/20220124224036-2022-01-24-22-40-36.png" alt="20220124224036-2022-01-24-22-40-36" /></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>public class LinkedQueue {
//定义一个节点类
private class Node{
String value;
Node next;
}
//记录队列元素个数
private int size = 0;
//head指向队头结点,tail指向队尾节点
private Node head;
private Node tail;
//申请一个队列
public LinkedQueue(){}
//入队
public boolean enqueue(String item){
Node newNode = new Node();
newNode.value = item;
if (size == 0) head = newNode;
else tail.next = newNode;
tail = newNode;
size++;
return true;
}
//出队
public String dequeue(){
String res = null;
if(size == 0) return res;
if(size == 1) tail = null;
res = head.value;
head = head.next;
size--;
return res;
}
}
</code></pre></div></div>
<h2 id="循环队列">循环队列</h2>
<p>我们刚才用数组来实现队列的时候,在 tail==n 时,会有数据搬移操作,这样入队操作性能就会受到影响。那有没有办法能够避免数据搬移呢?我们来看看循环队列的解决思路。</p>
<p>循环队列,顾名思义,它长得像一个环。原本数组是有头有尾的,是一条直线。现在我们把首尾相连,扳成了一个环。我画了一张图,你可以直观地感受一下。</p>
<p><img src="https://cdn.jsdelivr.net/gh/ironartisan/picRepo/20220124224449-2022-01-24-22-44-50.png" alt="20220124224449-2022-01-24-22-44-50" /></p>
<p>图中这个队列的大小为 8,当前 head=4,tail=7。当有一个新的元素 a 入队时,我们放入下标为 7 的位置。但这个时候,我们并不把 tail 更新为 8,而是将其在环中后移一位,到下标为 0 的位置。当再有一个元素 b 入队时,我们将 b 放入下标为 0 的位置,然后 tail 加 1 更新为 1。所以,在 a,b 依次入队之后,循环队列中的元素就变成了下面的样子:</p>
<p><img src="https://cdn.jsdelivr.net/gh/ironartisan/picRepo/20220124224731-2022-01-24-22-47-31.png" alt="20220124224731-2022-01-24-22-47-31" /></p>
<p>通过这样的方法,我们成功避免了数据搬移操作。看起来不难理解,但是循环队列的代码实现难度要比前面讲的非循环队列难多了。要想写出没有 bug 的循环队列的实现代码,我个人觉得,最关键的是,确定好队空和队满的判定条件。</p>
<p>在用数组实现的非循环队列中,队满的判断条件是 tail == n,队空的判断条件是 head == tail。那针对循环队列,如何判断队空和队满呢?队列为空的判断条件仍然是 head == tail。但队列满的判断条件就稍微有点复杂了。我画了
一张队列满的图,你可以看一下,试着总结一下规律。</p>
<p><img src="https://cdn.jsdelivr.net/gh/ironartisan/picRepo/20220124225005-2022-01-24-22-50-06.png" alt="20220124225005-2022-01-24-22-50-06" /></p>
<p>就像我图中画的队满的情况,tail=3,head=4,n=8,所以总结一下规律就是:(3+1)%8=4。多画几张队满的图,你就会发现,当队满时,(tail+1)%n=head。</p>
<p>你有没有发现,当队列满时,图中的 tail 指向的位置实际上是没有存储数据的。所以,循环队列会浪费一个数组的存储空间。</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">CircularQueue</span> <span class="o">{</span>
<span class="c1">// 数组:items,数组大小:n</span>
<span class="kd">private</span> <span class="nc">String</span><span class="o">[]</span> <span class="n">items</span><span class="o">;</span>
<span class="kd">private</span> <span class="kt">int</span> <span class="n">n</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span>
<span class="c1">// head表示队头下标,tail表示队尾下标</span>
<span class="kd">private</span> <span class="kt">int</span> <span class="n">head</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span>
<span class="kd">private</span> <span class="kt">int</span> <span class="n">tail</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span>
<span class="c1">// 申请一个大小为capacity的数组</span>
<span class="kd">public</span> <span class="nf">CircularQueue</span><span class="o">(</span><span class="kt">int</span> <span class="n">capacity</span><span class="o">)</span> <span class="o">{</span>
<span class="n">items</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">String</span><span class="o">[</span><span class="n">capacity</span><span class="o">];</span>
<span class="n">n</span> <span class="o">=</span> <span class="n">capacity</span><span class="o">;</span>
<span class="o">}</span>
<span class="c1">// 入队</span>
<span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">enqueue</span><span class="o">(</span><span class="nc">String</span> <span class="n">item</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">// 队列满了</span>
<span class="k">if</span> <span class="o">((</span><span class="n">tail</span> <span class="o">+</span> <span class="mi">1</span><span class="o">)</span> <span class="o">%</span> <span class="n">n</span> <span class="o">==</span> <span class="n">head</span><span class="o">)</span> <span class="k">return</span> <span class="kc">false</span><span class="o">;</span>
<span class="n">items</span><span class="o">[</span><span class="n">tail</span><span class="o">]</span> <span class="o">=</span> <span class="n">item</span><span class="o">;</span>
<span class="n">tail</span> <span class="o">=</span> <span class="o">(</span><span class="n">tail</span> <span class="o">+</span> <span class="mi">1</span><span class="o">)</span> <span class="o">%</span> <span class="n">n</span><span class="o">;</span>
<span class="k">return</span> <span class="kc">true</span><span class="o">;</span>
<span class="o">}</span>
<span class="c1">// 出队</span>
<span class="kd">public</span> <span class="nc">String</span> <span class="nf">dequeue</span><span class="o">()</span> <span class="o">{</span>
<span class="c1">// 如果head == tail 表示队列为空</span>
<span class="k">if</span> <span class="o">(</span><span class="n">head</span> <span class="o">==</span> <span class="n">tail</span><span class="o">)</span> <span class="k">return</span> <span class="kc">null</span><span class="o">;</span>
<span class="nc">String</span> <span class="n">ret</span> <span class="o">=</span> <span class="n">items</span><span class="o">[</span><span class="n">head</span><span class="o">];</span>
<span class="n">head</span> <span class="o">=</span> <span class="o">(</span><span class="n">head</span> <span class="o">+</span> <span class="mi">1</span><span class="o">)</span> <span class="o">%</span> <span class="n">n</span><span class="o">;</span>
<span class="k">return</span> <span class="n">ret</span><span class="o">;</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<h2 id="阻塞队列和并发队列">阻塞队列和并发队列</h2>
<p>队列这种数据结构很基础,平时的业务开发不大可能从零实现一个队列,甚至都不会直接用到。而一些具有特殊特性的队列应用却比较广泛,比如阻塞队列和并发队列。</p>
<p>阻塞队列其实就是在队列基础上增加了阻塞操作。简单来说,就是在队列为空的时候,从队头取数据会被阻塞。因为此时还没有数据可取,直到队列中有了数据才能返回;如果队列已经满了,那么插入数据的操作就会被阻塞,直到队列中有空闲位置后再插入数据,然后再返回。</p>
<p><img src="https://cdn.jsdelivr.net/gh/ironartisan/picRepo/20220124230055-2022-01-24-23-00-56.png" alt="20220124230055-2022-01-24-23-00-56" /></p>
<p>其实上述的定义就是一个“生产者 - 消费者模型”!是的,我们可以使用阻塞队列,轻松实现一个“生产者 - 消费者模型”!</p>
<p>这种基于阻塞队列实现的“生产者 - 消费者模型”,可以有效地协调生产和消费的速度。当“生产者”生产数据的速度过快,“消费者”来不及消费时,存储数据的队列很快就会满了。
这个时候,生产者就阻塞等待,直到“消费者”消费了数据,“生产者”才会被唤醒继续“生产”。</p>
<p>而且不仅如此,基于阻塞队列,我们还可以通过协调“生产者”和“消费者”的个数,来提高数据的处理效率。比如前面的例子,我们可以多配置几个“消费者”,来应对一个“生产者”。</p>
<p><img src="https://cdn.jsdelivr.net/gh/ironartisan/picRepo/20220124230355-2022-01-24-23-03-56.png" alt="20220124230355-2022-01-24-23-03-56" /></p>
<p>前面我们讲了阻塞队列,在多线程情况下,会有多个线程同时操作队列,这个时候就会存在线程安全问题,那如何实现一个线程安全的队列呢?</p>
<p>线程安全的队列我们叫作并发队列。最简单直接的实现方式是直接在 enqueue()、dequeue() 方法上加锁,但是锁粒度大并发度会比较低,同一时刻仅允许一个存或者取操作。实际上,基于数组的循环队列,利用 CAS 原子操作,可以实现非常高效的并发队列。这也是循环队列比链式队列应用更加广泛的原因。</p>
<h2 id="解答开篇">解答开篇</h2>
<p>回过来看下开篇的问题。线程池没有空闲线程时,新的任务请求线程资源时,线程池该如何处理?各种处理策略又是如何实现的呢?</p>
<p>我们一般有两种处理策略。第一种是非阻塞的处理方式,直接拒绝任务请求;另一种是阻塞的处理方式,将请求排队,等到有空闲线程时,取出排队的请求继续处理。那如何存储排队的请求呢?</p>
<p>我们希望公平地处理每个排队的请求,先进者先服务,所以队列这种数据结构很适合来存储排队请求。我们前面说过,队列有基于链表和基于数组这两种实现方式。这两种实现方式对于排队请求又有什么区别呢?</p>
<p>基于链表的实现方式,可以实现一个支持无限排队的无界队列(unbounded queue),但是可能会导致过多的请求排队等待,请求处理的响应时间过长。所以,针对响应时间比较敏感的系统,基于链表实现的无限排队的线程池是不合适的。</p>
<p>而基于数组实现的有界队列(bounded queue),队列的大小有限,所以线程池中排队的请求超过队列大小时,接下来的请求就会被拒绝,这种方式对响应时间敏感的系统来说,就相对更加合理。不过,设置一个合理的队列大小,也是非常有讲究的。队列太大导致等待的请求太多,队列太小会导致无法充分利用系统资源、发挥最大性能。</p>
<p>除了前面讲到队列应用在线程池请求排队的场景之外,队列可以应用在任何有限资源池中,用于排队请求,比如数据库连接池等。实际上,对于大部分资源有限的场景,当没有空闲资源时,基本上都可以通过“队列”这种数据结构来实现请求排队。</p>
<h2 id="总结">总结</h2>
<p>队列最大的特点就是先进先出,主要的两个操作是入队和出队。跟栈一样,它既可以用数组来实现,也可以用链表来实现。用数组实现的叫顺序队列,用链表实现的叫链式队列。特别是长得像一个环的循环队列。在数组实现队列的时候,会有数据搬移操作,要想解决数据搬移的问题,我们就需要像环一样的循环队列。</p>
<p>循环队列是我们这节的重点。要想写出没有 bug 的循环队列实现代码,关键要确定好队空和队满的判定条件,具体的代码你要能写出来。</p>
<p>除此之外,我们还讲了几种高级的队列结构,阻塞队列、并发队列,底层都还是队列这种数据结构,只不过在之上附加了很多其他功能。阻塞队列就是入队、出队操作可以阻塞,并发队列就是队列的操作多线程安全。</p>
<p>除了线程池这种池结构会用到队列排队请求,你还知道有哪些类似的池结构或者场景中会用到队列的排队请求呢?</p>
<p>今天讲到并发队列,关于如何实现无锁并发队列,网上有非常多的讨论。对这个问题,你怎么看呢?</p>
<h2 id="参考链接">参考链接</h2>
<p><a href="https://time.geekbang.org/column/intro/100017301">https://time.geekbang.org/column/intro/100017301</a></p>ironartisan前言 我们知道,CPU 资源是有限的,任务的处理速度与线程个数并不是线性正相关。相反,过多的线程反而会导致 CPU 频繁切换,处理性能下降。所以,线程池的大小一般都是综合考虑要处理任务的特点和硬件环境,来事先设置的。当我们向固定大小的线程池中请求一个线程时,如果线程池中没有空闲资源了,这个时候线程池如何处理这个请求?是拒绝请求还是排队请求?各种处理策略又是怎么实现的呢?实际上,这些问题并不复杂,其底层的数据结构就是我们今天要学的内容,队列 (queue)。