<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>绿色记忆 &#187; Database</title>
	<atom:link href="https://blog.gmem.cc/category/work/database/feed" rel="self" type="application/rss+xml" />
	<link>https://blog.gmem.cc</link>
	<description></description>
	<lastBuildDate>Thu, 16 Apr 2026 07:10:45 +0000</lastBuildDate>
	<language>en-US</language>
		<sy:updatePeriod>hourly</sy:updatePeriod>
		<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.9.14</generator>
	<item>
		<title>KeyDB学习笔记</title>
		<link>https://blog.gmem.cc/keydb-study-note</link>
		<comments>https://blog.gmem.cc/keydb-study-note#comments</comments>
		<pubDate>Tue, 21 Jan 2020 02:58:48 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Database]]></category>
		<category><![CDATA[NoSQL]]></category>
		<category><![CDATA[Redis]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=35505</guid>
		<description><![CDATA[<p>简介 KeyDB是Redis的替代品，宣称是世界上最快的NoSQL数据库，比Redis快5倍。KeyDB完全遵循Redis的协议，可以无缝的从Redis切换。 KeyDB支持多主复制、跨Region的水平扩容，支持垂直扩容。 多主复制 KeyDB支持多个保持同步的主节点，这些主节点都可以接受读、写请求。主节点可以括Region分布。不需要哨兵节点进行监控。 MVCC支持 KeyDB通过MVCC实现无锁（不需等待）的并发操作、后台保存。 ACID兼容 得益于KeyDB的MVCC支持： 事务、查询是非阻塞（不会有锁）的，同时具有原子性保证 未来将支持事务回滚 多线程 KeyDB是完全的多线程的，不像Redis那样仅仅在I/O上支持并发，这意味着它可以充分利用多核心。 这个特性让KeyDB的吞吐量比Redis 5大5倍，Redis 6大3倍。再启用TLS的情况下，吞吐量可以比Redis大7倍。 垂直扩容 多线程特性让KeyDB能最大化利用单机能力，这意味着通过配置能实现水平扩容。 单个KeyDB能够充分利用10（不使用TLS）-16（使用TLS）核心。 其它特性 更好的过期处理 支持将一个set中的所有键一起过期，过期时的删除操作是接近实时完成的，不会有延迟。 <a class="read-more" href="https://blog.gmem.cc/keydb-study-note">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/keydb-study-note">KeyDB学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">简介</span></div>
<p>KeyDB是<a href="/redis-study-note">Redis</a>的替代品，宣称是世界上最快的NoSQL数据库，比Redis快5倍。KeyDB完全遵循Redis的协议，可以无缝的从Redis切换。</p>
<p>KeyDB支持多主复制、跨Region的水平扩容，支持垂直扩容。</p>
<div class="blog_h2"><span class="graybg">多主复制</span></div>
<p>KeyDB支持多个保持同步的主节点，这些主节点<span style="background-color: #c0c0c0;">都可以接受读、写请求</span>。主节点可以括Region分布。<span style="background-color: #c0c0c0;">不需要哨兵</span>节点进行监控。</p>
<div class="blog_h2"><span class="graybg">MVCC支持</span></div>
<p>KeyDB通过MVCC实现无锁（不需等待）的并发操作、后台保存。</p>
<div class="blog_h3"><span class="graybg">ACID兼容</span></div>
<p>得益于KeyDB的MVCC支持：</p>
<ol>
<li><span style="background-color: #c0c0c0;">事务、查询是非阻塞（不会有锁）</span>的，同时具有原子性保证</li>
<li>未来将支持事务回滚</li>
</ol>
<div class="blog_h2"><span class="graybg">多线程</span></div>
<p>KeyDB是<span style="background-color: #c0c0c0;">完全的多线程</span>的，不像Redis那样仅仅在I/O上支持并发，这意味着它可以充分利用多核心。</p>
<p>这个特性让KeyDB的吞吐量比Redis 5大5倍，Redis 6大3倍。再启用TLS的情况下，吞吐量可以比Redis大7倍。</p>
<div class="blog_h2"><span class="graybg">垂直扩容</span></div>
<p>多线程特性让KeyDB能最大化利用单机能力，这意味着通过配置能实现水平扩容。</p>
<p>单个KeyDB能够充分利用10（不使用TLS）-16（使用TLS）核心。</p>
<div class="blog_h2"><span class="graybg">其它特性</span></div>
<div class="blog_h3"><span class="graybg">更好的过期处理</span></div>
<p>支持将一个set中的所有键一起过期，过期时的删除操作是接近实时完成的，不会有延迟。</p>
<div class="blog_h3"><span class="graybg">ARM友好</span></div>
<p>可以很好的在ARM体系结构下运行。</p>
<div class="blog_h1"><span class="graybg">安装</span></div>
<div class="blog_h2"><span class="graybg">Ubuntu</span></div>
<div class="blog_h3"><span class="graybg">通过PPA</span></div>
<pre class="crayon-plain-tag">sudo curl -s --compressed -o /etc/apt/trusted.gpg.d/keydb.gpg https://download.keydb.dev/keydb-ppa/keydb.gpg
sudo curl -s --compressed -o /etc/apt/sources.list.d/keydb.list https://download.keydb.dev/keydb-ppa/keydb.list
sudo apt update
sudo apt install keydb</pre>
<div class="blog_h3"><span class="graybg">仅安装工具</span></div>
<pre class="crayon-plain-tag">wget https://download.keydb.dev/packages/deb/ubuntu16.04_xenial/amd64/keydb-latest/keydb-tools_6.0.16-1~xenial1_amd64.deb
sudo dpkg -i keydb-tools_6.0.16-1~xenial1_amd64.deb </pre>
<div class="blog_h1"><span class="graybg">配置</span></div>
<div class="blog_h2"><span class="graybg">启用复制</span></div>
<div class="blog_h3"><span class="graybg">主主模式</span></div>
<p>要启用Active Replica模式，遵循如下步骤：</p>
<ol>
<li>两个服务器A/B都需要配置<pre class="crayon-plain-tag">active-replica yes</pre></li>
<li>在B上执行命令<pre class="crayon-plain-tag">replicaof [A address] [A port]</pre>，丢弃自己的数据，加载A的数据</li>
<li>在A上执行命令<pre class="crayon-plain-tag">replicaof [B address] [B port]</pre>，丢弃自己的数据，加载B的数据</li>
<li>现在两个服务器会传播自己的写操作到对方</li>
</ol>
<p>或者完全通过配置文件：</p>
<pre class="crayon-plain-tag"># 节点A的配置
port 6379
# 当前节点的密码
requirepass mypassword123
# 连接到Master的密码
masterauth mypassword123
# 增加下面的
active-replica yes
# 从Redis 5.0开始，slaveof改成replicaof
replicaof 10.0.0.3 6379


# 节点B的配置
port 6379
requirepass mypassword123
masterauth mypassword123
# 增加下面的
active-replica yes
replicaof 10.0.0.2 6379</pre>
<div class="blog_h3"><span class="graybg">多主模式</span></div>
<p>在每个节点上增加多个replicaof配置项：</p>
<pre class="crayon-plain-tag"># 一共有3个节点 A/B/C 10.0.0.2/3/4

# 节点A
multi-master yes
active-replica yes
replicaof 10.0.0.3 6379
replicaof 10.0.0.4 6379

# 节点B
multi-master yes
active-replica yes
replicaof 10.0.0.2 6379
replicaof 10.0.0.4 6379

# 节点C
multi-master yes
active-replica yes
replicaof 10.0.0.2 6379
replicaof 10.0.0.3 6379</pre>
<div class="blog_h2"><span class="graybg">创建集群</span></div>
<p>创建配置文件，参考：</p>
<pre class="crayon-plain-tag">port 6379
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
appendonly yes

dir ./
loglevel notice
logfile keydb.log

save 900 1
save 300 10
save 60 10000</pre>
<p>启动实例，参考：</p>
<pre class="crayon-plain-tag">keydb-server keydb.conf</pre>
<p>所有实例启动后，使用如下命令创建集群：</p>
<pre class="crayon-plain-tag">keydb-cli --cluster create 10.0.0.1:6379   10.0.0.2:6379 ... --cluster-replicas 1</pre>
<div class="blog_h3"><span class="graybg">增加节点</span></div>
<p>后续可以使用如下命令添加新的节点：</p>
<pre class="crayon-plain-tag"># 作为新的Master加入
#                            新节点           通过谁连接到既有集群
keydb-cli --cluster add-node 10.0.0.11:6379  10.0.0.1:6379

# 自动作为副本少的Master的Slave
keydb-cli --cluster add-node 10.0.0.11:6379 10.0.0.1:6379 --cluster-slave

# 强制指定Master
keydb-cli --cluster add-node 10.0.0.11:6379 10.0.0.1:6379 --cluster-slave --cluster-master-id 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e </pre>
<div class="blog_h3"><span class="graybg">再分片</span></div>
<p>通过如下命令可以进行再分片： </p>
<pre class="crayon-plain-tag">keydb-cli reshard 10.0.0.11:6379 --cluster-from &lt;node-id&gt; --cluster-to &lt;node-id&gt; \
  --cluster-slots &lt;number of slots&gt; --cluster-yes</pre>
<div class="blog_h2"><span class="graybg">配置项说明</span></div>
<p>大部分配置项和Redis是相通的，参考<a href="/redis-study-note#config">Redis学习笔记</a>。这里仅仅列出Redis新版本增加 / KeyDB特有的配置项。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 35%; text-align: center;">配置项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>supervised no</td>
<td>
<p>如果KeyDB由Systemd/Upstart管理，此配置项用于和supervision tree交互</p>
</td>
</tr>
<tr>
<td>always-show-logo yes</td>
<td>总是为交互式会话显示KeyDB的Logo</td>
</tr>
<tr>
<td>server-threads</td>
<td>
<p>建议4，默认1</p>
<p>处理请求的线程数量，应该取决于你的网络接口的队列数量（而不是CPU核心数量）</p>
<p>由于KeyDB使用自旋锁来减少延迟，将此值设置的过大会降低性能</p>
</td>
</tr>
<tr>
<td>server-thread-affinity</td>
<td>true/false，是否启用亲和性</td>
</tr>
<tr>
<td>db-s3-object</td>
<td>
<p>添加S3的桶路径，需要在本地具有S3客户端，并且已经配置好</p>
<p>如果配置该项，则保存RDB时，首先保存在本地，然后上传到S3。尝试加载时，首先尝试从本地加载，如果失败从S3加载</p>
</td>
</tr>
<tr>
<td>active-replica yes</td>
<td>启用Active Active复制。主从<span style="background-color: #c0c0c0;">都可以接受读写请求</span></td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">复制</span></div>
<div class="blog_h2"><span class="graybg">简介</span></div>
<p>通过复制（Replication），一个KeyDB节点可以保证自己的数据集和和Master(s)实例一致：</p>
<ol>
<li>当Slave和Master之间的连接良好时，Master通过发送写、键过期、键驱除等操作的数据流给Slave来保证Slave的数据和自己一致</li>
<li>当网络断开后，Slave会尝试重新连接到Master，并进行增量同步</li>
<li>如果<span style="background-color: #c0c0c0;">增量同步无法实现（落后太远），Slave会请求进行全量同步</span>。Master会创建它的数据集的快照，发送给Slave，然后继续发送增量的数据流</li>
</ol>
<p>关于KeyDB的复制机制，你需要知道以下事实：</p>
<ol>
<li>KeyDB默认使用异步的同步方式，这种方式的优势时延迟低、性能高。Slave会在接收了一定量的复制数据之后，向Master发送Ack。类似于Redis，使用WAIT命令可以确保特定的数据被多个Slave所Ack。WAIT命令并不能保证CAP中的CP —— 已经Ack的写操作，仍然可能在故障转移时丢失，尽管几率很小</li>
<li>每个Master可以对应多个Slaves</li>
<li><span style="background-color: #c0c0c0;">Slave可以有自己的Slaves</span>，参与复制的KeyDB节点可以形成树状拓扑</li>
<li>在Master侧，复制是非阻塞的。不管是增量/全量同步，都不影响Master继续处理查询</li>
<li>在Slave侧，复制很大程度上也是非阻塞的：
<ol>
<li>通过配置，KeyDB可以在接收初始同步时使用旧数据集提供服务。你也可以<span style="background-color: #c0c0c0;">配置，当复制流宕掉的情况下，让Slave返回错误</span>给客户端</li>
<li>在初始同步完毕后，有个阻塞的时间窗口，在此窗口中Slave替换旧的数据集</li>
<li>从4.0开始，可以配置KeyDB让删除数据集的操作在其它线程执行。但是，加载新数据集的操作仍然在主线程执行，会阻塞Slave</li>
</ol>
</li>
<li>通过复制，可以：
<ol>
<li>提升可扩容性：将缓慢的O(N)操作offload给Slave</li>
<li>提升数据安全性、高可用性</li>
</ol>
</li>
<li>使用复制，可以<span style="background-color: #c0c0c0;">免于Master需要将数据集写入到磁盘的开销</span>。这需要在Slave上配置高频的存盘，或者启用AOF。需要注意，这种用法下<span style="background-color: #c0c0c0;">Master重启时是空数据集</span>，如果不进行适当处理，它会导致Slave的数据集立刻变空</li>
</ol>
<div class="blog_h2"><span class="graybg">主主模式</span></div>
<p>KeyDB支持Active Replicas（Active Active）模式，这大大简化了故障转移的处理 —— 不需要确定副本何时应该晋升。</p>
<p>默认情况下，KeyDB以类似Redis的方式运行，仅仅允许从Master到Replicas的单向数据复制，副本仅仅支持只读处理。启用Active Replica模式后，即使Replica到Master的连接中断，它也能够处理请求。</p>
<div class="blog_h2"><span class="graybg">多主模式</span></div>
<p>KeyDB在复制时，支持多主模式。 使用配置：<pre class="crayon-plain-tag">multi-master yes</pre>即可启用。</p>
<p>当KeyDB连接到多个Master进行复制时，其行为和传统复制不同：</p>
<ol>
<li>多次调用replicaof命令，会为本节点添加额外的Master，而非替换掉Master</li>
<li>同步到Masters之前，<span style="background-color: #c0c0c0;">不会丢弃自己的数据</span></li>
<li>将<span style="background-color: #c0c0c0;">合并来自多个Masters的读写</span>到自己的内部数据库中</li>
<li><span style="background-color: #c0c0c0;">最后一个操作生效，也就是说，有两个Masters同时修改一个键，那么后修改的那个是实际值</span></li>
</ol>
<p>到目前为止，多主特性仍然是试验性的，在偶然情况下该特性可能触发流量封包。</p>
<p>如果仅仅有两个实例，建议使用主主模式。</p>
<div class="blog_h2"><span class="graybg">脑裂处理</span></div>
<p>KeyDB能够处理脑裂（两个节点之间网络断开）的场景。脑裂发生时，每个节点各自接受写入操作，每个写操作被加上时间戳，当连接恢复后，主节点们合并数据，以后写入的数据为准。</p>
<p>这种last-win原则在KeyDB里很常见，其它的例子如下文会提到的configEpoch冲突处理。</p>
<div class="blog_h2"><span class="graybg">只读Slave</span></div>
<p>从2.6版本开始KeyDB支持Slave的只读模式，并且默认开启。对应的选项是<pre class="crayon-plain-tag">slave-read-only</pre>，可以在运行时使用<pre class="crayon-plain-tag">CONFIG SET</pre>动态开关。</p>
<div class="blog_h2"><span class="graybg">最小Slave数量</span></div>
<p>从2.8版本开始KeyDB支持设置：<span style="background-color: #c0c0c0;">仅仅当至少有N个Slave连接到Master，此Master才允许写操作</span>。这可以增强数据安全性，再次强调一下，任何时候都不能保证数据绝不丢失。</p>
<p>该特性的工作方式是：</p>
<ol>
<li>KeyDB Slaves每秒PING一次Master，Ack自己处理的复制流的量</li>
<li>KeyDB Master记住每个Slave的最后一次PING的时间</li>
<li>如果PING正常的Slave数量小于配置的值，Master停止写入</li>
</ol>
<p>相关配置项：</p>
<ol>
<li><pre class="crayon-plain-tag">min-slaves-to-write</pre>：正常PING的Slave的最小数量</li>
<li><pre class="crayon-plain-tag">min-slaves-max-lag</pre>：如果PING延迟大于此参数指定的秒数，认为Slave不正常</li>
</ol>
<div class="blog_h1"><span class="graybg">集群</span></div>
<div class="blog_h2"><span class="graybg">简介</span></div>
<div class="blog_h3"><span class="graybg">设计目标</span></div>
<p>KeyDB集群的设计目标，依据重要性排列如下：</p>
<ol>
<li>高性能，支持最多1000节点规模。不使用代理、异步复制，不需要对键值进行merge操作</li>
<li>可接受的数据安全性：集群尽可能保证连接到大部分Master节点的客户端的写操作。最小化Ack后的写操作丢失的可能性，连接到少数Master节点的客户端的写操作丢失的可能性更大</li>
<li>可用性：当出现网络分区后，如果大部分Master节点可以连接，并且对于无法连接的Master节点们至少各有一个Slave节点可以连接，则可用性得到保证。此外，如果使用副本迁移（replicas migration），没有Slave的节点可以从Slave过多的节点夺取Slave</li>
</ol>
<div class="blog_h3"><span class="graybg">写安全性</span></div>
<p>KeyDB集群使用异步复制，Last Failover  Wins原则意味着最后晋升的主的数据集会覆盖其它副本的数据集。由于异步复制的天然特征，这意味着总是存在丢失数据的窗口。</p>
<p>如果出现网络分区，连接到大部分Master所在分区的客户端，丢失数据的时间窗口比连接到少数Master所在分区的客户端要小。<span style="background-color: #c0c0c0;">发生故障转移的必要条件是集群中大多数Master到某个Master的通信不可达时间超过NODE_TIMEOUT</span>，如果网络分区在超时前恢复，不会丢失数据（被新的Master覆盖）。<span style="background-color: #c0c0c0;">当连接断开超过NODE_TIMEOUT之后，如果网络分区中Master数量少于一半，则所有Master停止接受写入</span>。</p>
<p>下面是连接到大多数Master的客户端丢失已经Ack数据的场景：</p>
<ol>
<li>写操作已经到达Master，应答客户端时，异步的复制尚未传播到Slave</li>
<li>Master宕机</li>
<li>过了较长时间后，Slave晋升，它的数据集成为权威</li>
</ol>
<p>另外一个场景：</p>
<ol>
<li>Master由于网络分区不可达</li>
<li>发生故障转移，Slave晋升</li>
<li>旧Master网络恢复</li>
<li>某个客户端使用过期的路由表，将数据写到Master</li>
</ol>
<p>第二种场景的发生概率很小，需要多种巧合同时发生。</p>
<p><span style="background-color: #c0c0c0;">对于连接到少数Master的客户端，数据丢失的时间窗口可能较大，直到NODE_TIMEOUT之前的写入都可能丢失。</span></p>
<div class="blog_h3"><span class="graybg">可用性</span></div>
<p>出现网络分区后，少数Master所在的分区失去可用性。 </p>
<p>在多数Master所在的分区，如果对于不可达的Master，至少有一个Slave也在该分区，则可用性在NODE_TIMEOUT+若干秒（Slave晋升为Master，通常需要1-2秒）后恢复。</p>
<p><span style="background-color: #c0c0c0;">副本迁移（replicas migration）可以提升可用性</span>，在大多数Master分区中，如果某个Master有多余的Replica，可以迁移给（在当前分区）没有Slave的（不在当前分区的）Master。</p>
<div class="blog_h3"><span class="graybg">性能</span></div>
<p>KeyDB集群不会代理请求，只会向客户端发起重定向，让它访问匹配的键空间分片的节点。</p>
<p>最终客户端会得到up-to-date的集群、键空间分片-节点映射关系的信息，并且直接访问正确的节点，避免重定向。</p>
<p><span style="background-color: #ffffff;">由于</span><span style="background-color: #c0c0c0;">通常的操作仅仅发生在单个节点上</span>，因此可以认为N主节点的KeyDB集群的性能和单个KeyDB实例是相似的。</p>
<div class="blog_h2"><span class="graybg">集群总线</span></div>
<div class="blog_h3"><span class="graybg">总线功能</span></div>
<p>KeyDB集群节点提供总线进行通信，总线使用客户端端口+10000（默认16379），协议是KeyDB Cluster protocol。</p>
<p>在KeyDB Cluster protocol中，节点负责存储数据、集群状态（包括键到节点的映射）、发现其它节点、检测无效节点、晋升Slave节点为Master。</p>
<p>节点之间使用一种gossip协议来传播关于集群的信息，从而发现新节点、发送PING包来检测节点是否正常工作、同步关于集群特定状态的信号。</p>
<p>KeyDB Cluster Bus还用于在集群范围内传播Pub/Sub消息，协调用户发起的手工故障转移。</p>
<div class="blog_h3"><span class="graybg">全互联</span></div>
<p>为了实现总线的功能，所有节点通过二进制协议KeyDB Cluster Bus连接在一起，支持1000节点需要维持百万级的TCP连接。</p>
<p>为了避免在节点之间交换过多的信息，KeyDB使用gossip协议 + 一种配置更新机制。这保证了消息量不会随着集群规模而指数级增长。</p>
<div class="blog_h3"><span class="graybg">不转发客户端请求</span></div>
<p>KeyDB集群不会代理（转发）客户端请求，它可能给出响应并触发重定向。理论上客户端可以发送请求给任何节点，并在必要时获得重定向，因此它不需要持有集群的状态信息。但是，客户端可以缓存键-节点映射，来改善性能。</p>
<div class="blog_h3"><span class="graybg">节点握手</span></div>
<p>节点总是接受总线端口上的连接，并且应答PONG给PING请求，即使源节点不受信任。但是对于<span style="background-color: #c0c0c0;">其它消息，如果源节点不是集群成员，将全部丢弃</span>。</p>
<p>节点在以下情况下，接受某个源节点作为集群成员：</p>
<ol>
<li>新节点发送<pre class="crayon-plain-tag">MEET</pre>消息，此消息类似于PING，但是会强迫消息接受者，接纳源节点作为集群成员。仅当系统管理员执行：<br />
<pre class="crayon-plain-tag">CLUSTER MEET ip port</pre></p>
<p>节点才会发送MEET消息</p>
</li>
<li>通过gossip协议传递的，关于节点被加入集群的事实。如果A、B已经是集群成员，现在C新加入并且MEET了A，那么A会gossip给B，这样A、B都认可C作为成员了</li>
</ol>
<div class="blog_h3"><span class="graybg">不支持NAT</span></div>
<p>总线不支持在NAT环境下，或者任何TCP被映射的环境下工作。对于Docker来说，你可能需要设置<pre class="crayon-plain-tag">--net=host</pre>来避免NAT。</p>
<div class="blog_h2"><span class="graybg">键空间分片</span></div>
<p>整个键空间被划分为16384个Hash Slots，Slot是分片的最小单位，意味着理论上集群最多有16384个主节点（实际上建议的上限是1000左右）。</p>
<p>集群中每个Master处理这些Slots的子集。如果没有进行中的集群再配置（reconfiguration）—— 没有正在移动的Hash Slots ——则我们称集群为stable的。稳定集群中每个Slot被单个Master节点所处理</p>
<div class="blog_h3"><span class="graybg">映射算法</span></div>
<p>键到Slot的映射算法是：<pre class="crayon-plain-tag">HASH_Slot = CRC16(key) mod 16384</pre></p>
<div class="blog_h3"><span class="graybg">Hash Tag</span></div>
<p>使用Hash tag可以确保多个键被分配到同一个Slot，以便KeyDB集群支持multi-key操作。</p>
<p>如果键中包含 <pre class="crayon-plain-tag">{...}</pre>则仅仅花括号中的部分用来计算Hash Slot，这可以保证{user1000}.following、{user1000}.followers这两个键被分到同一个Slot。</p>
<div class="blog_h2"><span class="graybg">节点属性</span></div>
<div class="blog_h3"><span class="graybg">节点ID</span></div>
<p>集群中的每个节点具有唯一的、自动生成的ID，该名称是160bit的随机数。节点会将此名称存放在配置文件中，并且一直使用相同的名称，除非：</p>
<ol>
<li>配置文件被删除</li>
<li>执行硬重置命令<pre class="crayon-plain-tag">CLUSTER RESET</pre></li>
</ol>
<p>节点ID用于在集群范围内唯一的标识节点，节点<span style="background-color: #c0c0c0;">可以改变自己的IP地址，但是它的ID不需要改变</span>。集群可以感知节点的IP地质变更，并使用gossip协议，通过集群总线进行重配置。</p>
<div class="blog_h3"><span class="graybg">其它属性</span></div>
<p>在任何节点执行<pre class="crayon-plain-tag">CLUSTER NODES</pre>可以获得节点列表，并列出其关键属性。除了上面的节点ID之外，其他属性包括：</p>
<ol>
<li>节点IP地址:端口</li>
<li>一系列标记位</li>
<li>如果节点被标记为Slave，则它的Master是谁</li>
</ol>
<pre class="crayon-plain-tag">keydb-cli cluster nodes

d1861060fe 127.0.0.1:6379 myself - 0 1318428930 1 connected 0-1364
# 节点ID   地质:端口       标记      最后PING/PONG时间   配置epoch  连接状态   Slots
3886e65cc9 127.0.0.1:6380 master - 1318428930 1318428931 2        connected 1365-2729
d289c575dc 127.0.0.1:6381 master - 1318428931 1318428931 3        connected 2730-4095</pre>
<div class="blog_h2"><span class="graybg">重新分片</span></div>
<p>KeyDB集群允许在运行时添加、删除节点。这都潜在的意味着Hash Slot的重新平衡。和调整Hash Slot有关的命令有：</p>
<pre class="crayon-plain-tag"># 添加Slot，通常加入新节点时使用
CLUSTER ADDSlotS Slot1 [Slot2] ... [SlotN]
# 很少使用
CLUSTER DELSlotS Slot1 [Slot2] ... [SlotN]

# 将Slot分配给特定节点
CLUSTER SETSlot Slot NODE node</pre>
<p>Hash Slot重新分配之后，将通过gossip协议进行配置传播（ configuration propagation ）。</p>
<p>SETSlot子命令还可以拆分为两条。假设我们希望将Slot 8从A迁移到B：</p>
<ol>
<li>向B发送：<pre class="crayon-plain-tag">CLUSTER SETSlot Slot MIGRATING A</pre>。这样，B会接受所有关于Slot 8的查询，但是如果本地没有Key，则发送<pre class="crayon-plain-tag">ASK</pre>重定向，让客户度询问A</li>
<li>向A发送：<pre class="crayon-plain-tag">CLUSTER SETSlot Slot IMPORTING B</pre>。这样，A会接受所有关于Slot 8的查询，但是请求必须以<pre class="crayon-plain-tag">ASKING</pre>命令作为前导，否则MOVE重定向给Slot的负责人B</li>
</ol>
<div class="blog_h3"><span class="graybg">关于多键操作</span></div>
<p>重新分片牵涉到键的迁移，这个迁移是逐步进行的 —— 不是原子的移动Slot。</p>
<p>这意味着，在迁移过程中，多键操作的Key，可能一部分位于节点A，一部分以及迁移到节点B。由于KeyDB集群限制多键操作仅仅牵涉单个节点（所有键位于一个Hash Slot），因此这种情况下会应答客户端以<pre class="crayon-plain-tag">TRYAGAIN</pre>错误。</p>
<div class="blog_h2"><span class="graybg">重定向</span></div>
<div class="blog_h3"><span class="graybg">MOVED</span></div>
<p>KeyDB客户端可以自由的向任何节点发送查询请求，节点会进行以下处理：</p>
<ol>
<li>分析查询请求是否是<span style="background-color: #c0c0c0;">acceptable的 —— 要么查询仅仅牵涉一个键，要么牵涉到在同一Hash Slot中的multikey</span></li>
<li>查找其内部的Slot-node映射，找到负责目标Hash Slot的节点：
<ol>
<li>如果是当前节点负责，直接处理请求</li>
<li>如果是其它节点负责，应答客户端以MOVED错误：<br />
<pre class="crayon-plain-tag">GET x
#      键所属Slot   负责处理此Slot的节点
-MOVED 3999        127.0.0.1:6381</pre>
</li>
</ol>
</li>
</ol>
<p>重定向中已经给出正确节点的信息，客户端应该（但不必须）缓存并且向正确节点发请求。</p>
<p>一旦重定向发生，可能潜在的Slot-node映射发生很大变化，客户端可以通过<pre class="crayon-plain-tag">CLUSTER NODES</pre>或<pre class="crayon-plain-tag">CLUSTER SlotS</pre>命令获取最新映射关系，并缓存。</p>
<div class="blog_h3"><span class="graybg">ASK</span></div>
<p>MOVED用于提示客户端，很明确Hash Slot永久的被另外一个服务器所处理了。</p>
<p>ASK重定向则是提示客户端，Hash Slot可能处于迁移过程中，我虽然负责此Slot，但是没有找到你需要的Key，你去问问原来的所有者吧。</p>
<div class="blog_h2"><span class="graybg">在Slave上读</span></div>
<p>默认情况下，Slave节点仅仅会重定向请求给处理Hash Slot的Master节点。</p>
<p>客户端可以使用<pre class="crayon-plain-tag">READONLY</pre>命令，提示Slave节点，我可以读取（可能是陈旧的）你的数据，我不进行写查询。</p>
<p>当一个连接处于只读默认下时，仅当查询牵涉不被Slave的Master负责的Key（可以由于重新分片导致）时才会发送重定向。</p>
<p>使用命令<pre class="crayon-plain-tag">READWRITE</pre>可以清除连接的只读状态。</p>
<div class="blog_h2"><span class="graybg">容错</span></div>
<div class="blog_h3"><span class="graybg">关于心跳</span></div>
<p>KeyDB节点持续的交换PING/PONG封包，这两种消息的差别仅仅是消息类型。PING/PONG消息也叫心跳。</p>
<p>通常情况下，PING会触发接受者回复PONG。但是<span style="background-color: #c0c0c0;">PONG也可以用来携带重要的配置信息，并且不需要应答</span>。这种用法可以尽快广播新的配置。</p>
<p>通常情况下，发送心跳时，节点会<span style="background-color: #c0c0c0;">随机选择几个节点作为目标，而不是发给所有节点</span>。这避免了随着集群规模的增大而出现消息风暴。不过，作为一个前提，节点<span style="background-color: #c0c0c0;">确保在NODE_TIMEOUT / 2的时间内，至少发送一个心跳给从未沟通过（不管谁主动发送PING都可以）的</span>节点。</p>
<p><span style="background-color: #c0c0c0;">即使NODE_TIMEOUT到达了，节点也会重新发起一次TCP连接，防止因为连接的问题导致误判</span>。注意NODE_TIMEOUT必须要大于RTT。</p>
<p>需要注意根据集群规模来配置NODE_TIMEOUT，如果集群规模很大，而NODE_TIMEOUT又偏小，会导致大量的心跳包。</p>
<div class="blog_h3"><span class="graybg">心跳包内容</span></div>
<p>PING/PONG包和其他消息具有相同的头，该头的内容：</p>
<ol>
<li>Node ID，发送节点的标识符</li>
<li>发送节点的currentEpoch、configEpoch，这两个字段和KeyDB集群使用的分布式算法有关。Slave的configEpoch是它的Master的最后一个已知的configEpoch</li>
<li>节点标记，提示节点是Slave还是Master，以及其他的单比特节点属性</li>
<li>发送节点所负责的Hash Slot的位图信息，如果发送节点是Slave则发送它的Master的信息</li>
<li>发送节点的客户端TCP端口</li>
<li>从发送节点的角度来看，集群的状态（down/ok）</li>
<li>对于Slave，其Master的Node ID</li>
</ol>
<p>PING/PONG包还包括一个gossip段，其中包含<span style="background-color: #c0c0c0;">发送者所认为的，集群（一部分随机的，否则消息太大，具体数量取决于集群规模）其它节点的状态</span>。每个节点的信息包括：Node ID、IP:PORT、节点标记。这个<span style="background-color: #c0c0c0;">gossip段可以用来进行故障发现、发现新节点</span>。</p>
<div class="blog_h3"><span class="graybg">故障发现</span></div>
<p>故障发现（Failure Detection）用于发现这样的异常：Master/Slave不再被集群中的大部分节点可见，进而引起的Slave晋升为Master的过程。</p>
<p><span style="background-color: #c0c0c0;">如果无法通过晋升来解决故障，则集群进入错误状态，不再接受客户端查询请求</span>。</p>
<p>通过前面的心跳机制我们了解到，每个节点都维护自己所认为的，其它节点的状态。状态表现为一系列标记，其中和故障发现有关的是：</p>
<ol>
<li><pre class="crayon-plain-tag">PFAIL</pre>，P表示Possible，即某个节点可能出故障了，但是尚未确认。如果目标节点在NODE_TIMEOUT时间范围内，从当前节点不可达，则标注为PFAIL。Master/Slave都有权标记某个节点为PFAIL</li>
<li><pre class="crayon-plain-tag">FAIL</pre>，节点故障在<span style="background-color: #c0c0c0;">固定的时间范围内被大多数Master</span>所认可，该状态从PFAIL升级而来</li>
</ol>
<p>前面我们了解到心跳的gossip段包含当前节点所认为的，随机的其它几个节点的状态。经过若干次心跳后，每个节点的的认知会传播到所有其它节点。当满足以下条件后，PFAIL变为FAIL：</p>
<ol>
<li>某个节点A，将节点B标记为PFAIL</li>
<li>节点A通过心跳接收到的gossip分析集群中大部分节点关于B状态的声明</li>
<li>如果大部分节点在<pre class="crayon-plain-tag">NODE_TIMEOUT * FAIL_REPORT_VALIDITY_MULT</pre>的时间区间内，标记节点B为PFAIL或FAIL。<span style="background-color: #c0c0c0;">那么A标记B为FAIL，并发送<pre class="crayon-plain-tag">FAIL</pre>消息</span>给所有可达节点</li>
<li>FAIL消息会导致所有接收节点都将节点B标记为FAIL，不管它是不是认为B处于PFAIL状态</li>
</ol>
<p>当前实现将FAIL_REPORT_VALIDITY_MULT设置为2。这意味着，在两倍NODE_TIMEOUT时间内，大多数Master将某个节点标记为故障，则集群认为该节点宕掉了。</p>
<p>需要了解PFAIL到FAIL的转换，依赖于一种弱（一致）的协议：</p>
<ol>
<li>节点在一段时间范围内，收集其它节点的（关于节点状态）视图。由于时间窗口的存在，我们<span style="background-color: #c0c0c0;">无法确认（也不需要）在某个时间点</span>是否大多数Master达成一致</li>
<li>FAIL消息会被传播，但是无法保证所有节点都能收到并修改故障节点状态，这是因为故障常常伴随网络分区</li>
</ol>
<div class="blog_h3"><span class="graybg">故障恢复</span></div>
<p>节点状态可以从PFAIL变成FAIL，但是FAIL状态仅仅在以下情况下才能清除掉：</p>
<ol>
<li>节点变得可达，并且成为Slave。由于Slave不能参与故障转移，因此FAIL状态清除</li>
<li>节点变得可达，成为Master，但是不负责任何Slot。这种情况下FAIL状态可以被清除，因为该节点并没有正常参与到集群中</li>
<li>节点变得可达，成为Master，过了很长时间（NODE_TIMEOUT的N倍）仍然没有可觉察的Slave晋升。这种情况下，最好是让该节点重新加入集群</li>
</ol>
<div class="blog_h2"><span class="graybg">配置传播 </span></div>
<p>KeyDB使用类似于Raft算法中的Term的概念，叫Epoch。当多个节点提供了冲突的信息时，Epoch可用于确定谁的状态是up-to-date的：</p>
<ol>
<li>currentEpoch，可以认为是集群状态的版本号，<span style="background-color: #c0c0c0;">最终所有节点应该具有相同的currentEpoch</span></li>
<li>configEpoch，每个Master节点具有独特的<span style="background-color: #c0c0c0;">configEpoch，主要包含它负责的Hash Slots列表</span></li>
</ol>
<p>每个新创建的节点，它的 currentEpoch（64bit无符号整数）都是0。每当从其它节点接收到一个消息，<span style="background-color: #c0c0c0;">如果消息中的epoch大于本地的currentEpoch，则更新currentEpoch为接收到的epoch。</span></p>
<p>过了一段时间后，集群中所有节点都会使用最大的configEpoch作为自己的currentEpoch。</p>
<p>currentEpoch在集群节点需要协商，以决定执行某操作的时候用到，<span style="background-color: #c0c0c0;">currentEpoch大的节点具有话语权</span>。目前只支持Slave晋升这一种操作。</p>
<div class="blog_h3"><span class="graybg">configEpoch</span></div>
<p>所有Master在PING/PONG的时候，都会通告自己的configEpoch（连同自己负责的Hash Slots）。</p>
<p>当一个新节点加入集群后，Master将configEpoch设置为0。<span style="background-color: #c0c0c0;">Slave尝试晋升自己的时候，会增加configEpoch的值</span>，并尝试获得大多数Master的授权。一旦Slave获得授权，则它成为<span style="background-color: #c0c0c0;">使用此新configEpoch的Master</span>。</p>
<p>Slave在PING/PONG中也会通告configEpoch，通告的是它（通过最后一次消息得到）的Master的configEpoch。如果它通告的configEpoch小于Master的真实configEpoch，这意味着它需要更新，选举时<span style="background-color: #c0c0c0;">Master不会投票给持有过期configEpoch的Slave</span>。</p>
<p>节点的configEpoch变更，会被所有接收到相关心跳的节点fsync到nodes.config中。如果引起currentEpoch的变更，也会fsync到nodes.config。</p>
<p>configEpoch值的递增有一个简单算法负责，此算法确保它是新的、递增的、唯一的。configEpoch的变化由于故障转移、再分片导致。</p>
<div class="blog_h3"><span class="graybg">configEpoch冲突</span></div>
<p>如果由于Slave<span style="background-color: #c0c0c0;">晋升导致configEpoch增加，集群会确保它是唯一</span>的。</p>
<p>以下两种操作，仅仅简单的更新本地的configEpoch，可能导致configEpoch冲突：</p>
<ol>
<li>使用带<pre class="crayon-plain-tag">TAKEOVER</pre>选项的<pre class="crayon-plain-tag">CLUSTER FAILOVER</pre>命令，可以强制晋升一个Slave节点，不需要大多数Master可用</li>
<li>手工迁移Slot来进行再分片，也仅仅在本地节点生成新的epoch</li>
</ol>
<p>手工再分片时，如果Slot从A迁移到B，分片程序会强制B增加自己的epoch为可发现集群范围内的最大值 + 1，除非节点的epoch已经是最大。由于再分片常常牵涉到大量Slot的迁移，为了每个Slot，进行协商并获得更高的configEpoch是很低效的，因此增加epoch仅在本地进行，并且仅仅在迁移第一个Slot时进行。</p>
<p>尽管可能性较低，还是会出现多个节点声明相同configEpoch的情况 —— 如果手工再分片和自动故障转移发生外加运气差的话。KeyDB使用下面的冲突解决算法：</p>
<ol>
<li>如果Master A发现Master B正在通告和自己相同的configEpoch</li>
<li>并且根据字典序比较，A的Node ID比B大</li>
<li>那么A将currentEpoch+1，并且将其作为自己的configEpoch</li>
</ol>
<p>也就是说，如果集群中有若干节点具有相同configEpoch，那么其中ID最大的取胜，出现Hash Slot冲突时以它为准。</p>
<div class="blog_h3"><span class="graybg">Slots配置传播</span></div>
<p>Hash Slots - 节点映射关系的传播，不管对于新集群，还是由于故障转移/手工重分片导致的Slot负责节点变更，都是关键的。</p>
<p>Hash Slots配置信息可以通过两种方式传播：</p>
<ol>
<li>心跳消息：节点发送PING/PONG时总是携带它（或者它的Master）所负责的Slot的信息</li>
<li>UPDATE消息：由于心跳中还携带了自己的epoch，如果接受者发现此epoch过期，则会发送UPDATE消息，强制过期节点更新配置</li>
</ol>
<p>当一个新的节点加入到集群后，它的Hash Slot映射（16383 个key）简单的置空（表示未分配）。当接收到心跳/UPDATE消息后，根据以下规则来更新映射：</p>
<ol>
<li>如果Slot为空，并且某个已知的节点claim该Slot，分配Slot给节点</li>
<li>如果Slot不为空，并且它映射给Master A，如果现在接收到Master B的消息，声明Slot归它管。这种情况下，<span style="background-color: #c0c0c0;">如果B的configEpoch大于A，则重新绑定Slot给B</span></li>
</ol>
<p>由于规则2的存在，最终集群中所有的节点都认可configEpoch最大的节点通告的映射关系，这就是所谓last failover wins机制。</p>
<div class="blog_h2"><span class="graybg">故障转移</span></div>
<p>故障转移：</p>
<ol>
<li>依赖于上面两节提到的故障发现、配置传播机制</li>
<li>由出问题的Master的Slave主导，其它Master节点参与投票完成</li>
<li>触发时机为：
<ol>
<li>Master进入FAIL状态，并且经过了选举时延</li>
<li>Master至少负责1个Slot</li>
<li>Slave和Master的复制连接断开时间小于，这是为了保证Slave的数据不过于陈旧</li>
</ol>
</li>
</ol>
<p>故障转移过程如下：</p>
<ol>
<li>Slave增加自己的configEpoch，并同步currentEpoch</li>
<li>请求Masters来投票，具体做法是广播<pre class="crayon-plain-tag">FAILOVER_AUTH_REQUEST</pre>消息给所有Master节点</li>
<li>等待Master的应答，最长<pre class="crayon-plain-tag">NODE_TIMEOUT * 2</pre>，最短2秒</li>
<li>如果应答的Master的epoch小于发起晋升时Slave的currentEpoch，则应答被丢弃。这个可以防止接收到关于上一次选举的投票</li>
<li>如果Slave在限定时间内获得大多数Master的投票，则晋升成功。否则，等待<pre class="crayon-plain-tag">NODE_TIMEOUT * 4</pre>再次进行选举</li>
</ol>
<p>Master参与投票时遵循以下规则：</p>
<ol>
<li>对于一个给定的epoch，每个Master仅会投票一次：如果Master投票给Slave，它应答FAILOVER_AUTH_ACK，同时将lastVoteEpoch字段持久化到配置文件。Master在NODE_TIMEOUT * 2的时间内，不再能投票给故障Master的其它Slave，这可以防止同时选出多个新Master。</li>
<li>只有Master任何被故障转移的Master处于FAIL状态，才会进行投票</li>
<li>如果FAILOVER_AUTH_REQUEST中的currentEpoch小于Master的currentEpoch，请求被忽略 —— 这意味着投票应答的epoch总是和请求的相同</li>
</ol>
<div class="blog_h3"><span class="graybg">选举时延</span></div>
<p>Master进入FAIL状态后，Slave还需要等待一个选举延迟：</p>
<p style="padding-left: 30px;">DELAY = 500 毫秒 + 0 -500 毫秒随机延迟 + SLAVE_RANK * 1000 毫秒</p>
<p>才会发起选举投票。固定延迟让FAIL消息有机会传播到整个集群，随机延迟可以防止两个SLAVE_RANK相同的Slave同时尝试晋升。</p>
<p>SLAVE_RANK的大小取决于复制的进度。当Master故障时，Slave会相互通信，交换自己的复制进度，并进行排名。进度最快的Slave的SLAVE_RANK为0，依次递增。</p>
<div class="blog_h3"><span class="graybg">后处理</span></div>
<p>一旦Slave赢得选举，它将获得大于所有其它Master的configEpoch，并且通过心跳通告自己的configEpoch、负责的Slots。</p>
<p>为了让集群尽快完成重新配置，Slave将发送一个PONG包给整个集群。当前不可达的节点可能通过下面两种方式之一获得更新：</p>
<ol>
<li>接收到其它节点的PING/PONG包</li>
<li>其自己发送的心跳，由于信息过期，被其它节点应答以UPDATE</li>
</ol>
<p>重新配置的内容包括：</p>
<ol>
<li>对于故障的Master的原有其它Slave，需要更新复制源</li>
<li>对于所有节点，需要更新Hash Slot - 节点映射关系 </li>
</ol>
<div class="blog_h3"><span class="graybg">旧Master重新加入</span></div>
<p>如果一个Master节点因为网络分区，脱离集群，并且时间足够长，发生了故障转移。那么网络恢复后，旧的Master如何处理？</p>
<p>在KeyDB中，旧Master<span style="background-color: #c0c0c0;">将配置为偷取了它的Slot的那个节点的Slave</span>。</p>
<div class="blog_h2"><span class="graybg">节点重置</span></div>
<p>重置节点，可以让它以另外一个角色加入集群，或者加入其它集群。</p>
<p>执行<pre class="crayon-plain-tag">CLUSTER RESET</pre>可以软重置一个节点，如果不指定选项，默认为SOFT。制定HARD则进行硬重置。在重置时会进行如下处理：</p>
<ol>
<li>如果节点是Slave，则角色切换为Master，丢弃数据集；如果节点是Master并且持有Key，则中止重置</li>
<li>释放所有Hash Slot，重置手工故障转移状态</li>
<li>移除节点表中所有其它节点，这样节点对原先集群一无所知</li>
<li>对于硬重置：currentEpoch、configEpoch、lastVoteEpoch均置零</li>
<li>对于硬重置：Node ID修改为新的随机值</li>
</ol>
<div class="blog_h2"><span class="graybg">移除节点</span></div>
<p>通过重新分片，将Master的Slot全部迁移走，就可以关闭它了。</p>
<p>由于其它节点仍然记着移除节点的Node ID和地址，还会尝试连接它，因此，你应当调用<pre class="crayon-plain-tag">CLUSTER FORGET &lt;node-id&gt;</pre>，该命令的作用：</p>
<ol>
<li>从所有节点的nodes table中移除指定节点</li>
<li>60s内禁止被移除节点（根据ID）再次加入集群。防止因为gossip导致重新加入</li>
</ol>
<div class="blog_h2"><span class="graybg">订阅/发布</span></div>
<p>集群的客户端可以在任何节点上订阅，在任何节点上发布。KeyDB会保证消息被正确的转发 —— 目前的实现仅仅是简单的广播。</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/keydb-study-note">KeyDB学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/keydb-study-note/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Galera学习笔记</title>
		<link>https://blog.gmem.cc/galera-study-note</link>
		<comments>https://blog.gmem.cc/galera-study-note#comments</comments>
		<pubDate>Tue, 14 Jan 2020 11:24:25 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[MySQL]]></category>
		<category><![CDATA[Galera]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=35345</guid>
		<description><![CDATA[<p>MariaDB对比MySQL MariaDB提供了兼容MySQL的数据库解决方案，它本身是MySQL的一个Fork。 复制 MySQL中复制（Replication）是异步的、单向的。其中一个服务器作为Master，其它的作为Slave。所谓主主模式，是两个服务器分别配置为对方的Slave。MariaDB提供主主复制、主从复制。 MySQL 的默认二进制日志格式是基于行的，而在 MariaDB 中，默认的二进制日志格式是混合式的。 MariaDB支持二进制日志压缩（log_bin_compress）。 集群 MySQL提供shared-nothing的集群支持，通过自动分区的方式将数据分发到不同节点。在内部MySQL使用同步的、两阶段的提交，确保数据被写入到多个分片。 MariaDB使用Galera Cluster实现多主，从10.1开始MariaDB内置Galera，只需要配置参数即可启用集群模式。 存储引擎 MariaDB支持更多的存储引擎：XtraDB（10.2-的默认引擎，InnoDB增强版）、InnoDB（10.2+默认引擎）、MariaDB ColumnStore、Aria、Archive、Blackhole、Cassandra Storage Engine、Connect、CSV、FederatedX、Memory、Merge、Mroonga、MyISAM、MyRocks、QQGraph、Sequence Storage Engine、SphinxSE、Spider、TokuDB 其它特性 不可见列 <a class="read-more" href="https://blog.gmem.cc/galera-study-note">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/galera-study-note">Galera学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">MariaDB对比MySQL</span></div>
<p>MariaDB提供了兼容MySQL的数据库解决方案，它本身是MySQL的一个Fork。</p>
<div class="blog_h2"><span class="graybg">复制</span></div>
<p>MySQL中<span style="background-color: #c0c0c0;">复制（Replication）是异步的、单向的</span>。其中一个服务器作为Master，其它的作为Slave。所谓主主模式，是两个服务器分别配置为对方的Slave。MariaDB提供主主复制、主从复制。</p>
<p>MySQL 的默认二进制日志格式是基于行的，而在 MariaDB 中，默认的二进制日志格式是混合式的。</p>
<p>MariaDB支持二进制日志压缩（log_bin_compress）。</p>
<div class="blog_h2"><span class="graybg">集群</span></div>
<p>MySQL提供shared-nothing的集群支持，通过自动分区的方式将数据分发到不同节点。在内部MySQL使用<span style="background-color: #c0c0c0;">同步的、两阶段的提交，确保数据被写入到多个分片</span>。</p>
<p>MariaDB使用Galera Cluster实现多主，从10.1开始MariaDB内置Galera，只需要配置参数即可启用集群模式。</p>
<div class="blog_h2"><span class="graybg">存储引擎</span></div>
<p>MariaDB支持更多的存储引擎：XtraDB（10.2-的默认引擎，InnoDB增强版）、InnoDB（10.2+默认引擎）、MariaDB ColumnStore、Aria、Archive、Blackhole、Cassandra Storage Engine、Connect、CSV、FederatedX、Memory、Merge、Mroonga、MyISAM、MyRocks、QQGraph、Sequence Storage Engine、SphinxSE、Spider、TokuDB</p>
<div class="blog_h2"><span class="graybg">其它特性</span></div>
<div class="blog_h3"><span class="graybg">不可见列</span></div>
<p>MariaDB 上可用，MySQL 不支持该功能。这个功能允许创建不再 SELECT * 语句中出现的列，而在进行插入时，如果它们的名字没有出现在 INSERT 语句中，就不需要为这些列提供值。</p>
<div class="blog_h3"><span class="graybg">JSON数据类型</span></div>
<p>从 5.7 版本开始，MySQL 支持由 RFC 7159 定义的原生 JSON 数据类型，可以高效地访问 JSON 文档中的数据。</p>
<p>MariaDB 没有提供这一增强功能，认为 JSON 数据类型不是 SQL 标准的一部分。但为了支持从 MySQL 复制数据，MariaDB 为 JSON 定义了一个别名，实际上就是一个 LONGTEXT 列。</p>
<div class="blog_h3"><span class="graybg">线程池</span></div>
<p><span style="background-color: #c0c0c0;">MariaDB 支持连接线程池，这对于短查询和 CPU 密集型的工作负载（OLTP）来说非常有用</span>。<span style="background-color: #c0c0c0;">在 MySQL 的社区版本中，线程数是固定</span>的，因而限制了这种灵活性。MySQL 计划在企业版中增加线程池功能。</p>
<div class="blog_h1"><span class="graybg">Galera基础</span></div>
<p>MariaDB Galera Cluster提供MariaDB多主集群，仅仅对XtraDB/InnoDB存储引擎提供支持。</p>
<p>从MariaDB 10.1开始，<a href="https://github.com/codership/mysql-wsrep">MySQL-wsrep</a>补丁被合并到MariDB，这意味着使用标准的MariaDB+<a href="https://github.com/codership/galera/">Galera wsrep提供者库</a>即可构建Galera集群。该补丁由<a href="https://groups.google.com/forum/?fromgroups#!forum/codership-team">Codership</a>开发，提供<a href="https://galeracluster.com/library/documentation/architecture.html">wsrep API</a>支持。</p>
<div class="blog_h2"><span class="graybg">简介</span></div>
<div class="blog_h3"><span class="graybg">特性</span></div>
<p>Galera的主要特性包括：</p>
<ol>
<li>几乎同步的（virtually synchronous ）的复制</li>
<li>Active-Active多主拓扑支持，不需要failover</li>
<li>针对集群的任何节点进行读写操作</li>
<li>自动的集群成员控制，失败的节点能够自动的移出集群</li>
<li>自动节点加入</li>
<li>真正的（从底层）并行复制</li>
<li>不需要读写分离</li>
<li>Slave支持多线程以提升性能</li>
</ol>
<div class="blog_h3"><span class="graybg">优势</span></div>
<p>基于Galera的集群的主要优势包括：</p>
<ol>
<li>没有Slave的延迟</li>
<li>不会丢失事务</li>
<li>读操作的可扩容性</li>
<li>更小的客户端延迟</li>
</ol>
<div class="blog_h3"><span class="graybg">场景</span></div>
<p>Galera的使用场景包括：</p>
<ol>
<li>从Master读写：Galera支持这种传统场景，但是和MariaDB传统主从复制相比，任何一个<span style="background-color: #c0c0c0;">节点都可以随时成为Master</span>，而其它节点仅仅是客户端将其作为Slave看待而以。由于Slave可以并行的应用writeset，集群的吞吐量会快的多，此外复制的延迟也被消除</li>
<li>广域网集群：可以支持这种网络下的几乎同步复制，可能会存在延迟，取决于RTT。这些延迟仅仅影响commit操作</li>
<li>灾难恢复：广域网集群的子用例。这种情况下，位于某个数据中心的节点仅被动接受复制，不处理任何客户端事务。由于Galera的几乎同步的复制，它不会丢失数据，并可以在主节点宕机后立即升级为新的主节点</li>
<li>延迟消除：让客户端访问靠近自己的节点，可以消除读操作的延迟。延迟仅仅在写操作时才发生</li>
</ol>
<div class="blog_h2"><span class="graybg">版本</span></div>
<p>MariaDB、Galera库、wsrep API版本对应关系如下：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">MariaDB</td>
<td style="text-align: center;">Galera/wsrep提供者库</td>
<td style="text-align: center;">wsrep API</td>
</tr>
</thead>
<tbody>
<tr>
<td>MariaDB 10.4+</td>
<td>4</td>
<td>26</td>
</tr>
<tr>
<td>MariaDB 10.3-</td>
<td>3</td>
<td>25</td>
</tr>
</tbody>
</table>
<p>使用包名galera-4安装Galera 4 wsrep provider。版本26.4.6对应MariaDB的版本是10.5.7 / 10.4.16</p>
<p>使用包名galera安装Galera 3 wsrep provider。版本25.3.31对应MariaDB的版本是10.3.26 / 10.2.35 / 10.1.48</p>
<div class="blog_h2"><span class="graybg">通用复制库</span></div>
<p>Galera复制功能实现为一个共享库，可以连接到任何实现了wsrep API的事务处理系统。它是由一系列组件组成的协议栈，提供<span style="background-color: #c0c0c0;">准备、复制、应用事务writeset</span>的功能。</p>
<p>在Galera 4很多组件被重新设计。</p>
<div class="blog_h3"><span class="graybg">wsrep API</span></div>
<p>即写入集复制API（writeset replication API），它定义了Galera Replication和MariaDB之间的接口，以及各自的职责。 </p>
<div class="blog_h3"><span class="graybg">wsrep hooks</span></div>
<p>在RDBMS引擎中的wsrep集成点。</p>
<div class="blog_h3"><span class="graybg">Galera Provider</span></div>
<p>为Galera库实现wsrep API。</p>
<p>尽管galera provider负责在提交时，在所有节点认证writeset，但是<span style="background-color: #c0c0c0;">writeset不需要被立即apply到节点</span>。<span style="background-color: #c0c0c0;">实际上writeset被放置在节点的接收队列上，由节点的某个galera slave thread最终apply到数据库中</span>。</p>
<p>Galera slave thread的数量可以通过系统变量<pre class="crayon-plain-tag">wsrep_slave_threads</pre>配置。</p>
<p>Galera slave thread们负责自行判断哪些writeset可以被安全的并行apply，但是，如果<span style="background-color: #c0c0c0;">节点经常出现数据一致性问题，可以将线程数量设置为1</span>。</p>
<div class="blog_h3"><span class="graybg">certification layer</span></div>
<p>负责准备writeset，执行认证测试。</p>
<div class="blog_h3"><span class="graybg">replication</span></div>
<p>管理复制协议，提供整体上的排序能力（ordering capabilities）</p>
<div class="blog_h3"><span class="graybg">GCS framework</span></div>
<p>为Group Communication系统（GCS）提供插件化架构。很多GCS实现可以被适配。内置实现是vsbes、gemini </p>
<div class="blog_h2"><span class="graybg">状态转移</span></div>
<p>所谓状态转移是指从现有节点复制数据，以使得新节点/宕机后恢复的节点到达同步状态的操作。</p>
<div class="blog_h3"><span class="graybg">状态快照转移</span></div>
<p>在一次State Snapshot Transfers / SST中，集群通过从一个节点拷贝完整的数据集，来创建新节点。</p>
<p>当新节点加入集群后，它会触发一次SST。</p>
<div class="blog_h3"><span class="graybg">增量状态转移</span></div>
<p>在一次Incremental State Transfers /IST中，集群从正常节点，拷贝某个节点缺失的writeset。</p>
<p>如果节点离开集群时间比较短，则IST通常比SST更块。</p>
<div class="blog_h2"><span class="graybg">原理</span></div>
<p>在Galera集群中，服务器在提交（Commit）期间进行事务复制。其做法是，<span style="background-color: #c0c0c0;">将事务关联的写入集（writeset）广播到所有节点</span>。 </p>
<div class="blog_h3"><span class="graybg">同步/异步复制</span></div>
<p>同步复制能够保证：如果一个节点上的数据发生变更，那么这些变更会同时发生在其它节点上。</p>
<p>异步复制不提供上述保证，这意味着，在第一个节点上发生变更后，复制完成之前该节点宕机，<span style="background-color: #c0c0c0;">可能会导致数据丢失</span>。</p>
<p>同步复制的优势：</p>
<ol>
<li>使用<span style="background-color: #c0c0c0;">同步复制的集群，总是能保证HA和数据一致性</span>。节点崩溃不会导致数据丢失</li>
<li>同步复制允许<span style="background-color: #c0c0c0;">事务在所有节点上并行的执行</span>。而不是进行Replay</li>
<li>因果律保证：如果先在A节点发生事务，然后在B节点执行查询，那么该查询一定能看到A节点那个事务的结果</li>
</ol>
<p>典型的同步复制利用<span style="background-color: #c0c0c0;">两阶段提交或者分布式锁</span>来实现，非常缓慢。再加上实现的复杂度，导致MySQL、PostgreSQL等传统RDBMS都仅仅支持异步复制，或者所谓半同步复制（主库在执行完客户端提交的事务后不是立刻返回给客户端，而是等待至少一个从库接收到并写到Relay log中才返回给客户端）。</p>
<div class="blog_h3"><span class="graybg">基于认证的复制</span></div>
<p>Galera使用基于认证的复制（Certification-based replication），实现几乎完全同步（virtually synchronous）的数据复制。</p>
<p>Galera的基本思想是乐观执行（ optimistic execution）：<span style="background-color: #c0c0c0;">假设不存在冲突，正常执行事务，直到达到提交点（commit point）</span>，在提交之前，通过认证来检查冲突</p>
<p>Galera的核心机制包括：</p>
<ol>
<li>分组通信（group communication）：定义了数据库节点之间的通信pattern，保证复制数据的一致性</li>
<li>写入集（writeset）：将若干数据库写操作合并为一个消息，避免了每次数据库操作都引发节点之间的协作。<span style="background-color: #c0c0c0;">writeset中包含了事务的所有必要信息</span></li>
<li>数据库状态机（database state machine）：只读事务仅在节点本地执行。非只读事务首先在单个节点本地执行，然后广播writeset到其它节点，认证并apply到数据库</li>
<li>全局事务排序（global transaction reordering）：在复制期间，<span style="background-color: #c0c0c0;">Galera为每个事务分配一个全局的序号即seqno，每个节点将以相同的顺序接收（和应用）事务</span>。当事务到达提交点时，节点会检查上一次成功的事务的seqno。这两次事务seqno的差值，提示了尚未apply的事务数量。节点会对这些没处理的事务进行主键冲突检查，发现冲突则触发认证失败</li>
</ol>
<div class="blog_h3"><span class="graybg">复制流程</span></div>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2021/01/certificationbasedreplication.png"><img class="size-full wp-image-35431 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2021/01/certificationbasedreplication.png" alt="certificationbasedreplication" width="800" height="480" /></a></p>
<p>Galera执行数据复制的流程如下：</p>
<ol>
<li>
<p>当客户端发出一个 commit 指令，在<span style="background-color: #c0c0c0;">事务在服务器端被正式（数据已经写入，只差）提交之前，所有对数据库的更改都会被writeset收集起来</span>，并且<span style="background-color: #c0c0c0;">将 writeset 纪录广播</span>给其他节点</p>
</li>
<li>
<p><span style="background-color: #c0c0c0;">writeset 将在每个节点（包括发起writeset的原始节点）上、基于主键进行决定性认证测试（deterministic certification test,）</span>，测试结果决定节点能否 apply writeset</p>
</li>
<li>
<p>如果认证测试失败：节点丢弃writeset，集群<span style="background-color: #c0c0c0;">回滚原始事务</span></p>
</li>
<li>如果认证测试成功：<span style="background-color: #c0c0c0;">事务被正式提交，其余节点则apply writeset</span></li>
</ol>
<p>只有所有节点都认证成功，达成共识，那么发起事务的原始节点就可以正式提交并给客户端响应。在这个时间点，用户的写入集已经同步到了所有节点，不会因为原始节点故障而导致数据丢失</p>
<div class="blog_h2"><span class="graybg">注意点</span></div>
<p>关于从普通MariaDB转到Galera的TIPS，参考：<a href="https://mariadb.com/kb/en/tips-on-converting-to-galera/">https://mariadb.com/kb/en/tips-on-converting-to-galera/</a></p>
<div class="blog_h3"><span class="graybg">仅支持InnoDB</span></div>
<p>对任何其它引擎的表的写入，包括系统表（mysql.*，使用MyISAM引擎）都不会复制到其它节点。<span style="background-color: #c0c0c0;">例外情况是DDL语句，这些语句虽然修改mysql.*表，但是却会被复制</span>。</p>
<p>对MyISAM复制目前提供试验性支持，设置系统变量：wsrep_replicate_myisam。</p>
<p>FLUSH PRIVILEGES 不会被复制。</p>
<div class="blog_h3"><span class="graybg">不支持明确锁定</span></div>
<p>LOCK TABLES、FLUSH TABLES (table list) WITH READ LOCK等语句不被支持，可以通过适当的事务控制来达到相同效果。</p>
<p>全局性的锁操作，例如FLUSH TABLES WITH READ LOCK则可以被支持。</p>
<div class="blog_h3"><span class="graybg">必须有主键</span></div>
<p>所有表必须具有主键（支持复合主键）。针对无主键表的DELETE操作是不支持的。没有主键的表，可能在不同节点上的顺序不同。</p>
<div class="blog_h3"><span class="graybg">查询日志必须写到文件</span></div>
<p>普通查询日志、缓慢查询日志，不支持写入到表中。如果启用这些日志，你必须设置log_out=FILE</p>
<div class="blog_h3"><span class="graybg">不支持XA事务</span></div>
<div class="blog_h3"><span class="graybg">事务尺寸的限制</span></div>
<p>尽管Galera没有明确限制，但是writeset被存储在内存中，这隐含了对事务尺寸的限制。</p>
<p>为了避免非常大的事务严重影响节点性能。系统变量的默认值设置为：</p>
<ol>
<li>wsrep_max_ws_rows  事务最大影响行数，128K</li>
<li>wsrep_max_ws_size 事务最大尺寸（写入集大小）2GB</li>
</ol>
<p>此外根据社区的反馈，大尺寸事务可能有<span style="background-color: #c0c0c0;">额外的不可忽略的开销</span>。你写入100K的行，可能需要额外200-300MB甚至数G内存。</p>
<div class="blog_h3"><span class="graybg">牵涉DDL时锁语义改变</span></div>
<p>你的DML语句操作一个锁，而另外一个DDL已经发起。正常情况下MySQL会等待元数据锁，但是Galera则会立即执行DML。甚至仅有单个节点的情况下，也会发生这种情况。</p>
<div class="blog_h3"><span class="graybg">自增列空隙</span></div>
<p>不要假设自增长的列是一个个顺序增长的。Galera为了保证没有冲突，为每个节点的自增长键都设置了空虚。</p>
<div class="blog_h3"><span class="graybg">网络分区</span></div>
<p>当出现网络分区，并且当前节点不再大多数节点所在分区。命令会导致ER_UNKNOWN_COM_ERROR：WSREP has not yet prepared node for application use / Unknown command。这个报错是为了放置数据不一致。网络分区状态可以通过检查<pre class="crayon-plain-tag">wsrep_ready</pre>变量发现。</p>
<p>少部分节点所在的分区，节点可能会<span style="background-color: #c0c0c0;">丢弃所有客户端连接</span>，这个行为可能比较意外 —— 因为客户端可能是空闲的，甚至不知道发生什么连接就忽然断了。</p>
<p>这种被分区的节点重新连接到集群后，可能有大量数据需要同步。在同步完成之前，它仍然会报告unknown command。</p>
<div class="blog_h3"><span class="graybg">不要修改binlog格式</span></div>
<p>在运行时修改binlog格式，不但会导致复制失败，还会导致其它节点崩溃。</p>
<div class="blog_h3"><span class="graybg">性能问题</span></div>
<p>根据Galera的设计，<span style="background-color: #c0c0c0;">集群的整体性能取决于最慢的节点</span>。</p>
<p>此外，即使<span style="background-color: #c0c0c0;">只有一个节点。性能也可能比非集群模式的MariaDB低很多</span>，特别是对于大事务。</p>
<div class="blog_h3"><span class="graybg">查询缓存问题</span></div>
<p>在MariaDB Galera Cluster 5.5.40, MariaDB Galera Cluster 10.0.14以及MariaDB 10.1.2之前，需要设置query_cache_size=0以禁用查询缓存。</p>
<div class="blog_h3"><span class="graybg">异步复制模式</span></div>
<p>启用异步复制模式的情况下，不支持在Slave节点上进行并行复制（slave-parallel-threads 大于1）。</p>
<div class="blog_h3"><span class="graybg">表结构不一致</span></div>
<p>节点的表结构可能不一致，特别是在执行rolling schema upgrade期间。</p>
<div class="blog_h3"><span class="graybg">读写分离</span></div>
<p>读写分离不是必须的，但是仍然建议做读写分离，未来底层架构可能变化。</p>
<div class="blog_h1"><span class="graybg">变量清单</span></div>
<div class="blog_h2"><span class="graybg">状态变量</span></div>
<p>所有状态变量具有一致的前缀：</p>
<pre class="crayon-plain-tag">SHOW STATUS LIKE 'wsrep%';</pre>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 30%; text-align: center;">变量</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td class=" blog_h3">wsrep_applier_thread_count</td>
<td>当前applier线程的数量，这种线程负责将writeset写入到数据库</td>
</tr>
<tr>
<td class=" blog_h3"><strong>wsrep_apply_oooe</strong></td>
<td>writeset被乱序的apply的频繁程度，可以提示并行处理的效率</td>
</tr>
<tr>
<td class=" blog_h3"><strong>wsrep_apply_oool</strong></td>
<td>具有高序列号的writeset，在具有低序列号的writeset之前被apply —— 这种情况发生的频繁程度，可以提示处理缓慢的writeset</td>
</tr>
<tr>
<td class=" blog_h3"><strong>wsrep_apply_window</strong></td>
<td>并发的被apply的writeset的最高、最低序列号的平均差值</td>
</tr>
<tr>
<td class=" blog_h3">wsrep_cert_deps_distance</td>
<td>可能被并行apply的writeset序列号的平均差值，提示潜在的并行度</td>
</tr>
<tr>
<td class=" blog_h3">wsrep_cert_index_size</td>
<td>认证索引中的条目数量</td>
</tr>
<tr>
<td class=" blog_h3">wsrep_cert_interval</td>
<td>在一个事务被复制期间，平均接收到的新事务的数量</td>
</tr>
<tr>
<td class=" blog_h3"><strong>wsrep_cluster_conf_id</strong></td>
<td>集群成员关系发生变化的总次数</td>
</tr>
<tr>
<td class=" blog_h3"><strong>wsrep_cluster_size</strong></td>
<td>集群成员当前数量</td>
</tr>
<tr>
<td class=" blog_h3"><strong>wsrep_cluster_state_uuid</strong></td>
<td>集群当前状态的唯一标识。如果和wsrep_local_state_uuid相同，意味着本节点和集群同步（in sync）</td>
</tr>
<tr>
<td class=" blog_h3"><strong>wsrep_cluster_status</strong></td>
<td>
<p>集群组件状态，可能值：</p>
<ol>
<li>PRIMARY：primary group configuration, quorum present</li>
<li>NON_PRIMARY：non-primary group configuration, quorum lost</li>
<li>DISCONNECTED：not connected to group, retrying</li>
</ol>
</td>
</tr>
<tr>
<td class=" blog_h3">wsrep_cluster_weight</td>
<td>当前集群成员们的总weight，即当前Primary Component中的节点的pc.weight的求和</td>
</tr>
<tr>
<td class=" blog_h3"><strong>wsrep_commit_oooe</strong></td>
<td>事务被乱序commit的频繁程度</td>
</tr>
<tr>
<td class=" blog_h3"><strong>wsrep_commit_window</strong></td>
<td>并发的被commit的最大、最小seqno的平均差值</td>
</tr>
<tr>
<td class=" blog_h3"><strong>wsrep_connected</strong></td>
<td>MariaDB是否连接到wsrep provider，值ON/OFF</td>
</tr>
<tr>
<td class=" blog_h3">wsrep_desync_count</td>
<td>需要节点临时失去同步状态的、进行中的操作数量</td>
</tr>
<tr>
<td class=" blog_h3">wsrep_evs_delayed</td>
<td>当前节点在哪些节点上注册到了delayed list</td>
</tr>
<tr>
<td class=" blog_h3">wsrep_evs_evict_list</td>
<td>从集群驱除的节点的UUIDs。在重启mysqld进程之前，被驱逐的节点无法重新加入集群</td>
</tr>
<tr>
<td class=" blog_h3">wsrep_evs_repl_latency</td>
<td>
<p>提示组通信（group communication）的复制（replication）延迟。即从消息发出到消息接收的时延（秒）</p>
<p>由于复制是组操作（group operation），因此该参数提示了最慢的ACK和最长的RTT时间</p>
</td>
</tr>
<tr>
<td class=" blog_h3">wsrep_evs_state</td>
<td>显示EVS协议的内部状态</td>
</tr>
<tr>
<td class=" blog_h3">wsrep_flow_control_paused</td>
<td>由于流控而导致复制暂停的时长，从上一个FLUSH STATUS命令开始记时</td>
</tr>
<tr>
<td class=" blog_h3">wsrep_flow_control_paused_ns</td>
<td>总计处于暂体状态的纳秒数</td>
</tr>
<tr>
<td class=" blog_h3">wsrep_flow_control_recv</td>
<td>从最近一次状态查询以来，FC_PAUSE事件的接收总数</td>
</tr>
<tr>
<td class=" blog_h3">wsrep_flow_control_sent</td>
<td>从最近一次状态查询以来，FC_PAUSE事件的发送总数</td>
</tr>
<tr>
<td class=" blog_h3">wsrep_gcomm_uuid</td>
<td>节点的UUID（用于组通信）</td>
</tr>
<tr>
<td class=" blog_h3">wsrep_incoming_addresses</td>
<td>在cluster component中的incoming服务器地址列表</td>
</tr>
<tr>
<td class=" blog_h3">wsrep_last_committed</td>
<td>最近一次事务的seqno</td>
</tr>
<tr>
<td class=" blog_h3"><strong>wsrep_local_bf_aborts</strong></td>
<td>被slave transcation中断的本地事务的总数，中断由于认证失败（存在冲突）导致</td>
</tr>
<tr>
<td class=" blog_h3">wsrep_local_cached_downto</td>
<td>writeset缓存（GCache）中最小的seqno值</td>
</tr>
<tr>
<td class=" blog_h3"><strong>wsrep_local_cert_failures</strong></td>
<td>在认证测试中失败（存在冲突）的本地事务总数</td>
</tr>
<tr>
<td class=" blog_h3">wsrep_local_commits</td>
<td>在本节点上提交的本地事务总数</td>
</tr>
<tr>
<td class=" blog_h3">wsrep_local_index</td>
<td>集群中本节点的索引，索引zero-based</td>
</tr>
<tr>
<td class=" blog_h3"><strong>wsrep_local_recv_queue</strong></td>
<td>当前接收队列长度，即等待被apply的writeset数量</td>
</tr>
<tr>
<td class=" blog_h3">wsrep_local_recv_queue_avg</td>
<td>自最近一次状态查询以来，接收队列的平均长度</td>
</tr>
<tr>
<td class=" blog_h3">wsrep_local_recv_queue_max</td>
<td>自最近一次FLUSH STATUS以来，接收队列的最大长度</td>
</tr>
<tr>
<td class=" blog_h3">wsrep_local_recv_queue_min</td>
<td>自最近一次FLUSH STATUS以来，接收队列的最小长度</td>
</tr>
<tr>
<td class=" blog_h3">wsrep_local_replays</td>
<td>由于asymmetric lock granularity导致replay的事务的总量</td>
</tr>
<tr>
<td class=" blog_h3"><strong>wsrep_local_send_queue</strong></td>
<td>当前发送队列的长度，即等待被发送的writeset数量</td>
</tr>
<tr>
<td class=" blog_h3">wsrep_local_send_queue_avg</td>
<td>自最近一次状态查询以来，发送队列的平均长度</td>
</tr>
<tr>
<td class=" blog_h3">wsrep_local_send_queue_max</td>
<td>自最近一次FLUSH STATUS以来，发送队列的最大长度</td>
</tr>
<tr>
<td class=" blog_h3">wsrep_local_send_queue_min</td>
<td>自最近一次FLUSH STATUS以来，发送队列的最小长度</td>
</tr>
<tr>
<td class=" blog_h3"><strong>wsrep_local_state</strong></td>
<td>内部Galera Cluster FSM state</td>
</tr>
<tr>
<td class=" blog_h3"><strong>wsrep_local_state_comment</strong></td>
<td>
<p>上个字段的可读文本</p>
<p>如果节点是Primary Component的成员，则值可能是：</p>
<p style="padding-left: 30px;">Joining, Waiting on SST, Joined, Synced, Donor</p>
<p>如果节点不是PC成员，则值是：Initialized</p>
<p>节点状态转换示意图：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2021/01/galerafsm.png"><img class="size-full wp-image-35459 alignleft" src="https://blog.gmem.cc/wp-content/uploads/2021/01/galerafsm.png" alt="galerafsm" width="315" height="338" /></a></p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<ol>
<li>节点启动，并连接到Primary Component</li>
<li>节点发起状态转移请求，开始缓存writeset</li>
<li>节点接收SST，获得了所有集群数据，开始apply writeset</li>
<li>节点赶上集群进度。现在它的slave queue是空的，开始启用FC，保持slave queue为空。节点设置wsrep_ready=1，它可以处理事务了</li>
<li>节点接收其它节点发来的SST请求，配合进行状态转移。缓存所有它无法apply的writeset（某些SST会阻塞donor）</li>
</ol>
</td>
</tr>
<tr>
<td class=" blog_h3"><strong>wsrep_local_state_uuid</strong></td>
<td>节点的UUID状态，如果和wsrep_cluster_state_uuid相同则本节点保持了同步</td>
</tr>
<tr>
<td class=" blog_h3">wsrep_open_connections</td>
<td>在wsrep provider中打开的连接数量</td>
</tr>
<tr>
<td class=" blog_h3"><strong>wsrep_open_transactions</strong></td>
<td>
<p>在wsrep provider中已经注册的、本地运行中的事务的数量</p>
<p>这个变量提示了生成writeset的事务的数量，只读事务不在统计中</p>
</td>
</tr>
<tr>
<td class=" blog_h3">wsrep_protocol_version</td>
<td>wsrep协议版本</td>
</tr>
<tr>
<td class=" blog_h3">wsrep_provider_name</td>
<td>wsrep提供者名称</td>
</tr>
<tr>
<td class=" blog_h3">wsrep_provider_vendor</td>
<td>wsrep提供者vendor</td>
</tr>
<tr>
<td class=" blog_h3">wsrep_provider_version</td>
<td>wsrep提供者版本</td>
</tr>
<tr>
<td class=" blog_h3"><strong>wsrep_ready</strong></td>
<td>wsrep提供者是否就绪</td>
</tr>
<tr>
<td class=" blog_h3"><strong>wsrep_received</strong></td>
<td>从其它节点接收到的writeset数量</td>
</tr>
<tr>
<td class=" blog_h3">wsrep_received_bytes</td>
<td>从其它节点接收到的writeset字节数</td>
</tr>
<tr>
<td class=" blog_h3">wsrep_repl_data_bytes</td>
<td>复制的数据字节数</td>
</tr>
<tr>
<td class=" blog_h3">wsrep_repl_keys</td>
<td>复制的键数量</td>
</tr>
<tr>
<td class=" blog_h3">wsrep_repl_keys_bytes</td>
<td>复制的键字节数</td>
</tr>
<tr>
<td class=" blog_h3">wsrep_repl_other_bytes</td>
<td>复制的其它bits数量</td>
</tr>
<tr>
<td class=" blog_h3"><strong>wsrep_replicated</strong></td>
<td>复制到其它节点的writeset数量</td>
</tr>
<tr>
<td class=" blog_h3">wsrep_replicated_bytes</td>
<td>复制到其它节点的writeset字节数</td>
</tr>
<tr>
<td class=" blog_h3">wsrep_rollbacker_thread_count</td>
<td>rollbacker线程数量</td>
</tr>
<tr>
<td class=" blog_h3">wsrep_thread_count</td>
<td>wsrep线程（applier/rollbacker）总数</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">系统变量</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 30%; text-align: center;">变量</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td class="blog_h3">wsrep_auto_increment_control</td>
<td>
<p>如果设置为1（默认），自动根据集群规模调整auto_increment_increment/auto_increment_offset </p>
<p>集群规模变化时也会调整</p>
<p>避免由于auto_increment导致的复制冲突</p>
</td>
</tr>
<tr>
<td class="blog_h3"><strong>wsrep_causal_reads</strong></td>
<td>
<p>默认OFF。如果设置为ON，确保集群范围内的隔离级别：读取已提交</p>
<p>如果master比slave apply的速度快很多，意味着失去同步。该选项设置为<span style="background-color: #c0c0c0;">ON，则让slave在事件处理完毕之前，不去处理查询</span></p>
<p>设置为ON，会导致更大的读延迟</p>
<p>已经废弃：用 wsrep_sync_wait=1 代替</p>
</td>
</tr>
<tr>
<td class="blog_h3">wsrep_certification_rules</td>
<td>
<p>集群中使用的认证规则（certification rules ），可选值：</p>
<ol>
<li>strict，可能导致更多的认证失败</li>
<li>optimized，允许更多的并发，减少认证失败</li>
</ol>
</td>
</tr>
<tr>
<td class="blog_h3">wsrep_certify_nonPK</td>
<td>默认ON。如果设置为ON，允许复制没有PK的表，但是可能导致未定义的行为</td>
</tr>
<tr>
<td class="blog_h3"><strong>wsrep_cluster_address</strong></td>
<td>
<p>节点启动时连接的地址，格式：</p>
<p style="padding-left: 30px;">&lt;schema&gt;://&lt;cluster_address&gt;[?option1=value1[&amp;option2=value2]]</p>
<p>示例：</p>
<p style="padding-left: 30px;">gcomm://192.168.0.1:1234?gmcast.listen_addr=0.0.0.0:2345</p>
<p>建议指定所有可能的节点地址：</p>
<p style="padding-left: 30px;">gcomm://&lt;node1 or ip:port&gt;,&lt;node2 or ip2:port&gt;,&lt;node3 or ip3:port&gt;</p>
<p><span style="background-color: #c0c0c0;">如果指定空白（gcomm://）则节点会发起一个新的集群</span></p>
<p>option指的是wsrep provider选项。这些选项的默认值来自 wsrep_provider_options ，<span style="background-color: #c0c0c0;">这里可以覆盖之</span></p>
<p>某些配置下，该变量可以在运行时修改，这会导致节点断开现有连接，并连接到其它集群</p>
</td>
</tr>
<tr>
<td class="blog_h3"><strong>wsrep_cluster_name</strong></td>
<td>集群的名称，节点<span style="background-color: #c0c0c0;">不能连接到和不同名称的集群</span></td>
</tr>
<tr>
<td class="blog_h3">wsrep_convert_LOCK_to_trx</td>
<td>
<p>将LOCK/UNLOCK TABLES语句转换为BEGIN/COMMIT，用于适配老的应用程序（免修改）</p>
<p>谨慎使用，可能导致巨大的writeset</p>
</td>
</tr>
<tr>
<td class="blog_h3"><strong>wsrep_data_home_dir </strong></td>
<td>wsrep provider存储内部文件的目录 </td>
</tr>
<tr>
<td class="blog_h3">wsrep_dbug_option </td>
<td>向wsrep provider传递调试选项</td>
</tr>
<tr>
<td class="blog_h3"><strong>wsrep_debug</strong> </td>
<td>
<p>wsrep调试日志级别。NONE（默认）, SERVER, TRANSACTION, STREAMING, CLIENT</p>
<p>开启日志，可以记录冲突的事务</p>
</td>
</tr>
<tr>
<td class="blog_h3">wsrep_desync</td>
<td>
<p>默认OFF。当节点接收太多writeset，无法即使消化（apply），则事务被存放到接收队列中。如果接收队列积蓄的writeset太多（由于wsrep选项gcs.fc_limit指定），则节点enage流控</p>
<p>如果将此字段设置为ON，则针对desync的节点禁用流控（flow control）。desync的节点会继续慢慢悠悠的处理队列中的writeset，这可能导致积累越来越多</p>
</td>
</tr>
<tr>
<td class="blog_h3"><strong>wsrep_dirty_reads</strong></td>
<td>可session，默认OFF。默认情况下，如果节点没有和群组（集群）同步（状态wsrep_ready=OFF），则它会拒绝除了SET/SHOW之外的任何查询请求。如果将此选项设置为ON，那么不会导致数据变化的查询请求可以被节点所接受</td>
</tr>
<tr>
<td class="blog_h3">wsrep_drupal_282555_workaround</td>
<td>默认OFF。如果启用，这启用针对缺陷<a href="https://www.drupal.org/node/282555">Drupal/MySQL/InnoDB bug #282555</a>的workaround。该缺陷可能导致向AUTO_INCREMENT列插入DEFAULT值时产生重复KEY </td>
</tr>
<tr>
<td class="blog_h3">wsrep_forced_binlog_format</td>
<td>
<p>覆盖任何session级别binlog格式设置</p>
<p>取值：STATEMENT, ROW, MIXED, NONE</p>
</td>
</tr>
<tr>
<td class="blog_h3">wsrep_gtid_domain_id</td>
<td>用于wsrep GTID（Global Transaction ID）模式的GTID domain ID</td>
</tr>
<tr>
<td class="blog_h3">wsrep_gtid_mode</td>
<td>
<p>wsrep GTID mode尝试在所有节点上，保持writeset的GTID的一致性。在加入新节点时，通过SST将GTID状态复制到节点</p>
<p>如果打算使用MariaDB Replication，启用wsrep GTID mode可能有用</p>
</td>
</tr>
<tr>
<td class="blog_h3">wsrep_gtid_seq_no</td>
<td>仅session，设置WSREP GTID seqno</td>
</tr>
<tr>
<td class="blog_h3">wsrep_ignore_apply_errors</td>
<td>
<p>apply错误应该被忽略，还是报告给provider：</p>
<p style="padding-left: 30px;">0 不跳过任何错误<br />1 忽略某些DDL错误（DROP DATABASE, DROP TABLE, DROP INDEX, ALTER TABLE）<br />2 忽略DML错误（仅仅忽略DELETE错误）<br />4 忽略所有DDL错误</p>
</td>
</tr>
<tr>
<td class="blog_h3">wsrep_log_conflicts</td>
<td>默认OFF。如果设置为ON，冲突的MDL以及InnoDB所的详细信息被记录 </td>
</tr>
<tr>
<td class="blog_h3"><strong>wsrep_max_ws_rows</strong></td>
<td>
<p>一个writeset允许的最大行数</p>
<p>为了保证向后兼容，默认值0，表示允许任意大小 </p>
</td>
</tr>
<tr>
<td class="blog_h3"><strong>wsrep_max_ws_size</strong></td>
<td>一个writeset允许的最大bytes</td>
</tr>
<tr>
<td class="blog_h3">wsrep_mysql_replication_bundle</td>
<td>
<p>可以被分组到一起处理的复制事件的数量。一个试验性的实现允许辅助一个slave节点处理commit时延</p>
<p>默认0，不支持分组</p>
</td>
</tr>
<tr>
<td class="blog_h3"><strong>wsrep_node_address</strong></td>
<td>节点的网络地址。默认0.0.0.0:4567</td>
</tr>
<tr>
<td class="blog_h3">wsrep_node_incoming_address</td>
<td>监听客户端连接的地址</td>
</tr>
<tr>
<td class="blog_h3">wsrep_node_name</td>
<td>节点的名称，可以用在wsrep_sst_donor中</td>
</tr>
<tr>
<td class="blog_h3">wsrep_notify_cmd</td>
<td>每当节点状态或集群成员关系发生变化时调用的命令 </td>
</tr>
<tr>
<td class="blog_h3"><strong>wsrep_on</strong></td>
<td>
<p>可session，默认OFF。是否启用wsrep复制，如果设置为OFF则无法加载wsrep provider，不能加入集群</p>
<p>如果在session级别设置为OFF，则该会话的操作不会被复制，但是其它会话、applier线程仍然正常工作</p>
</td>
</tr>
<tr>
<td class="blog_h3">wsrep_OSU_method</td>
<td>
<p>指定在线Schema更新方法（schema upgrade method） ：</p>
<ol>
<li>TOI（默认）：Total Order Isolation。在每个节点上，DDL以相同的顺序执行，保证数据一致性。数据库被影响的部分在全集群范围锁定</li>
<li>RSU：Rolling Schema Upgrade。DDL仅仅在本地节点处理，用户需要手工在其它节点处理</li>
</ol>
</td>
</tr>
<tr>
<td class="blog_h3">wsrep_patch_version</td>
<td>wsrep补丁版本</td>
</tr>
<tr>
<td class="blog_h3"><strong>wsrep_provider</strong></td>
<td>
<p>wsrep provider库的位置，可能位置：</p>
<ol>
<li>Ubuntu：/usr/lib/libgalera_smm.so</li>
<li>CentOS：/usr/lib64/libgalera_smm.so</li>
</ol>
</td>
</tr>
<tr>
<td class="blog_h3">wsrep_provider_options</td>
<td>传递给wsrep provider的、分号（;）分隔的选项列表 </td>
</tr>
<tr>
<td class="blog_h3"><strong>wsrep_recover</strong></td>
<td>如果在服务器启动时设置为ON，则节点会尝试恢复最近一次apply的writeset的seqno，并让服务器退出</td>
</tr>
<tr>
<td class="blog_h3">wsrep_reject_queries</td>
<td>
<p>用于进入维护状态，节点会继续apply writeset，但是客户端查询会导致Error 1047: Unknown command错误。取值：</p>
<ol>
<li>NONE（默认）：查询正常处理</li>
<li>ALL：所有客户端查询请求被拒绝，已经存在的客户端连接继续维护</li>
<li>ALL_KILL：所有查询被拒绝，同时关闭所有连接</li>
</ol>
</td>
</tr>
<tr>
<td class="blog_h3"><strong>wsrep_replicate_myisam</strong></td>
<td>是否对MyISAM表的DML可以被复制。试验性功能</td>
</tr>
<tr>
<td class="blog_h3">wsrep_restart_slave</td>
<td>默认OFF。如果设置为ON，则节点重新加入集群后replication slave自动重启</td>
</tr>
<tr>
<td class="blog_h3"><strong>wsrep_retry_autocommit</strong></td>
<td>
<p>在报告客户端以错误之前，因为集群范围冲突而导致无法提交的auto-commit请求重试的次数</p>
<p>默认1。设置为0则不会重试</p>
</td>
</tr>
<tr>
<td class="blog_h3">wsrep_slave_FK_checks</td>
<td>默认ON。如果设置为ON，则applier slave thread会执行外键约束检查</td>
</tr>
<tr>
<td class="blog_h3">wsrep_slave_UK_checks</td>
<td>默认OFF。如果设置为ON，则applier slave thread会执行辅助索引的唯一性检查</td>
</tr>
<tr>
<td class="blog_h3"><strong>wsrep_slave_threads</strong></td>
<td>默认1。用于并行apply writeset的slave thread的总数。这些线程能够自行决定writeset 能否被安全的并行apply，但是，如果你发现集群中频繁出现不一致性问题，考虑设置为1</td>
</tr>
<tr>
<td class="blog_h3">wsrep_sr_store </td>
<td>streaming replication fragments的存储方式</td>
</tr>
<tr>
<td class="blog_h3"><strong>wsrep_sst_auth</strong></td>
<td>SST时使用的身份验证信息</td>
</tr>
<tr>
<td class="blog_h3"><strong>wsrep_sst_donor</strong></td>
<td>SST的偏好的源节点 </td>
</tr>
<tr>
<td class="blog_h3">wsrep_sst_donor_rejects_queries</td>
<td>默认OFF。如果设置为ON则供给（donor）节点在SST期间拒绝查询请求并返回UNKNOWN COMMAND</td>
</tr>
<tr>
<td class="blog_h3"><strong>wsrep_sst_method</strong></td>
<td>默认rsync。SST方法 </td>
</tr>
<tr>
<td class="blog_h3">wsrep_sst_receive_address</td>
<td>供给节点（donor）连接到此地址，来发送状态转移更新</td>
</tr>
<tr>
<td class="blog_h3">wsrep_strict_ddl</td>
<td>默认OFF。如果设置为ON，则禁止对不支持Galera Replication的表（即非InnoDB）的DDL</td>
</tr>
<tr>
<td class="blog_h3"><strong>wsrep_sync_wait</strong></td>
<td>
<p>在该参数指定的操作类型执行之前，执行causality checks 以确保操作在完全同步的前提下执行</p>
<p>在causality check期间，所有查询请求被阻塞，以保证节点应用了所有到发起check那个时间点的所有更新。一旦check结束，操作开始在节点上执行</p>
<p>操作类型：</p>
<p style="padding-left: 30px;">0 - 禁用等待同步（默认）<br />1 - READ， 即SELECT、BEGIN/START TRANSACTION / SHOW。等价于wsrep_causal_read=1<br />2 - UPDATE and DELETE<br />3 - READ, UPDATE and DELETE;<br />4 - INSERT and REPLACE;<br />5 - READ, INSERT and REPLACE;<br />6 - UPDATE, DELETE, INSERT and REPLACE;<br />7 - READ, UPDATE, DELETE, INSERT and REPLACE; <br />8 - SHOW <br />9 - READ and SHOW<br />10 - UPDATE, DELETE and SHOW<br />11 - READ, UPDATE, DELETE and SHOW<br />12 - INSERT, REPLACE and SHOW<br />13 - READ, INSERT, REPLACE and SHOW<br />14 - UPDATE, DELETE, INSERT, REPLACE and SHOW <br />15 - READ, UPDATE, DELETE, INSERT, REPLACE and SHOW</p>
</td>
</tr>
<tr>
<td class="blog_h3">wsrep_trx_fragment_size </td>
<td>
<p>对于流式复制（streaming replication），事务分片（ transaction fragments）的大小</p>
<p>单位由wsrep_trx_fragment_unit指定</p>
</td>
</tr>
<tr>
<td class="blog_h3">wsrep_trx_fragment_unit)</td>
<td>
<p>事务分片的单位：</p>
<p style="padding-left: 30px;">bytes  事务的binlog事件缓冲的字节数<br />rows  事务影响的行数<br />statements  多语句事务中执行的SQL数量</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">安装配置</span></div>
<div class="blog_h2"><span class="graybg">普通安装</span></div>
<p>为了安装MariaDB Galera Cluster，需要以下包：</p>
<ol>
<li>支持Galera的MariDB</li>
<li>Galera wsrep提供者库</li>
</ol>
<p>为了支持基于SST的备份，可能需要额外的包。</p>
<div class="blog_h3"><span class="graybg">安装MariaDB</span></div>
<p>添加源配置：</p>
<pre class="crayon-plain-tag"># MariaDB 10.5 CentOS repository list - created 2021-01-15 07:44 UTC
# http://downloads.mariadb.org/mariadb/repositories/
[mariadb]
name = MariaDB
baseurl = http://yum.mariadb.org/10.5/centos8-amd64
module_hotfixes=1
gpgkey=https://yum.mariadb.org/RPM-GPG-KEY-MariaDB
gpgcheck=1</pre>
<p>执行安装：</p>
<pre class="crayon-plain-tag">dnf install MariaDB-server
systemctl start mariadb</pre>
<div class="blog_h3"><span class="graybg">安装Galera wsrep提供者</span></div>
<pre class="crayon-plain-tag">dnf install galera4 </pre>
<div class="blog_h3"><span class="graybg">启动新集群 </span></div>
<p>集群的第一个节点需要以下面的命令行选项来自举（bootstrap）：</p>
<pre class="crayon-plain-tag">mysqld --wsrep-new-cluster</pre>
<p>这个选项告诉MariaDB，不需要连接到既有集群。对于一个现有的节点，以该选项重启，会导致创建新的识别集群身份的UUID，并且它不会再连接到老集群。</p>
<p>对于使用Systemd的系统，应该使用下面的命令创建新集群：</p>
<pre class="crayon-plain-tag">galera_new_cluster</pre>
<div class="blog_h3"><span class="graybg">添加新节点 </span></div>
<p>要添加新节点，需要通过wsrep_cluster_address选项指定集群地址，如果第一个节点的地址是192.168.0.1，则可以用下面的配置加入新节点：</p>
<pre class="crayon-plain-tag">[mariadb]
; 支持IP地址或DNS名称
wsrep_cluster_address=gcomm://192.168.0.1</pre>
<p>使用上述配置，新节点将会连接到一个既有集群节点，并随后发现所有的节点。<span style="background-color: #c0c0c0;">最好在此配置中，指定所有集群成员的地址</span>，免得因为单点的故障 导致无法加入集群。</p>
<p>当所有成员加入到集群，状态不一致的集群可能通过IST/SST来达到一致性。</p>
<div class="blog_h3"><span class="graybg">重启集群</span></div>
<p>当集群所有节点关闭后，需要依次重启节点。</p>
<p>如果第一个节点以常规方式启动， 则它会尝试连接到wsrep_cluster_address中的其它节点，这必然失败。因此，集群完全关闭之后，需要对第一个节点进行bootstrap操作。</p>
<p><span style="background-color: #c0c0c0;">第一个节点，必须是数据最新的节点</span>。如果节点检测到它可能<span style="background-color: #c0c0c0;">不是数据最新（如果它不是最后一个关闭的，或者它是崩掉的）</span>的节点，它会拒绝bootstrap。</p>
<p>查看所有节点的grastate.dat文件，其中seqno字段的值最大的，就是数据最新的节点。如果节点是崩掉的且seqno=-1，则可以使用下面的命令恢复seqno：</p>
<pre class="crayon-plain-tag">mysqld --wsrep_recover</pre>
<p>确定数据最新节点后，可以修改它的数据目录下的grastate.dat文件，设置：</p>
<pre class="crayon-plain-tag">safe_to_bootstrap=1</pre>
<p>使用Systemd的情况下，节点的seqno可以通过命令：<pre class="crayon-plain-tag">galera_recovery</pre>恢复。 galera_recovery脚本调用mysqld时，错误日志会写入到/tmp/wsrep_recovery.XXXXXX。错误日志会有类似下面的内容：</p>
<pre class="crayon-plain-tag">#                                 集群group id                         seqno
[Note] WSREP: Recovered position: 7bff636d-50c7-11eb-81c8-6f8dd75e7fb4:1202</pre>
<p>启用Galera时，MariaDB的Systemd服务会在启动数据库之前自动运行galera_recovery。</p>
<div class="blog_h3"><span class="graybg">状态监控</span></div>
<p>和Galera相关的状态变量，都放在wsrep_前缀下：</p>
<pre class="crayon-plain-tag">SHOW GLOBAL STATUS LIKE 'wsrep_%';</pre>
<div class="blog_h2"><span class="graybg">容器化 </span></div>
<p>参考<a href="#k8s">在K8S中运行</a>一节。</p>
<div class="blog_h2"><span class="graybg">主要配置项说明</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">配置</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td colspan="2"><strong><em>基础配置</em></strong></td>
</tr>
<tr>
<td class=" blog_h3">wsrep_provider</td>
<td>wsrep提供者库的位置</td>
</tr>
<tr>
<td class="blog_h3">wsrep_cluster_name</td>
<td>Galera集群名称</td>
</tr>
<tr>
<td class=" blog_h3">wsrep_cluster_address</td>
<td>
<p>集群地址配置</p>
<p>格式：&lt;schema&gt;://&lt;cluster_address&gt;[?option1=value1[&amp;option2=value2]]</p>
</td>
</tr>
<tr>
<td class=" blog_h3">binlog_format</td>
<td>二进制日志格式，默认ROW</td>
</tr>
<tr>
<td class=" blog_h3">default_storage_engine</td>
<td>要设置为InnoDB</td>
</tr>
<tr>
<td class=" blog_h3">innodb_autoinc_lock_mode</td>
<td>生成AUTO_INCREMENT值时的锁模式，要设置为2（interleaved lock mode）</td>
</tr>
<tr>
<td class=" blog_h3">innodb_doublewrite</td>
<td>
<p>保持默认值1，也就是说InnoDB在写入数据文件之前，首先将数据存储到一个双重写入缓冲（InnoDB Doublewrite Buffer），以提升容错能力</p>
<p>双重写入缓冲用于从不完全写入的page中恢复，如果InnoDB在将页写入磁盘时断电，会出现不完全写入的问题</p>
</td>
</tr>
<tr>
<td class=" blog_h3">query_cache_size</td>
<td>对于5.5.40-  10.0.14- 以及10.1.2，需要设置为0</td>
</tr>
<tr>
<td class=" blog_h3">wsrep_on</td>
<td>设置为ON，启用wsrep复制，10.1.2+</td>
</tr>
<tr>
<td colspan="2"><em><strong>性能配置</strong></em></td>
</tr>
<tr>
<td class="blog_h3">innodb_flush_log_at_trx_commit</td>
<td>可以设置为0，尽管在标准的MariaDB中设置为0会有丢数据的风险，但是在Galera中要安全，因为<span style="background-color: #c0c0c0;">数据的不一致性总是可以从另外一个节点恢复</span></td>
</tr>
<tr>
<td colspan="2"><strong><em>复制行为控制</em></strong></td>
</tr>
<tr>
<td class="blog_h3">log_slave_updates</td>
<td>
<p>设置为ON，则节点将接收到的writeset写入到binlog，默认不写入</p>
<p>如果你希望节点作为传统MariaDB Replication的Master，开启此选项</p>
</td>
</tr>
<tr>
<td class="blog_h3">binlog_do_db</td>
<td>影响到此选项列出的数据库的DML操作，被写入binlog</td>
</tr>
<tr>
<td class="blog_h3">binlog_ignore_db</td>
<td>影响到此选项列出的数据库的DML操作，不写入binlog</td>
</tr>
<tr>
<td class="blog_h3">replicate_wild_do_table</td>
<td>只有该选项匹配的表的DML才被应用到此slave</td>
</tr>
<tr>
<td class="blog_h3">replicate_wild_ignore_table</td>
<td>该选项匹配的表的DML被当前slave丢弃</td>
</tr>
<tr>
<td class="blog_h3">wsrep_sst_auth</td>
<td>SST的 username:password配置，mysqldump SST需要</td>
</tr>
<tr>
<td colspan="2"><strong><em>端口配置</em></strong></td>
</tr>
<tr>
<td class="blog_h3">wsrep_node_address</td>
<td>wsrep复制使用的地址端口，默认0.0.0.04567</td>
</tr>
<tr>
<td class="blog_h3">ist.recv_addr</td>
<td>IST端口，默认4568</td>
</tr>
<tr>
<td class="blog_h3">wsrep_sst_receive_address</td>
<td>SST端口，默认4444。对于任何除mysqldump之外的状态快照转移方式，都使用此端口</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">状态转移</span></div>
<p>当<span style="background-color: #c0c0c0;">新加入一个节点</span>到Galera集群时，此节点需要从集群节点获取数据，这个复制过程被称作状态转移（State Transfer），Galera支持两种状态传输方式：</p>
<ol>
<li>State Snapshot Transfers (SST) 传输一个节点的完整状态（即所有数据），全量复制</li>
<li>Incremental State Transfers (IST) 只传输缺失的数据，增量复制</li>
</ol>
<div class="blog_h1"><span class="graybg">IST</span></div>
<p>使用 IST 的方式时，集群会判断加入集群的节点所缺失的数据，而不必传输完整的数据集。使用 IST 必须满足特定的条件：</p>
<ol>
<li>加入节点的 state UUID 必须和此集群的相同</li>
<li>加入节点所缺失的数据都在 donor 节点的 writeset cache 中存在</li>
</ol>
<p>如果上述条件满足，<span style="background-color: #c0c0c0;">供给节点会发送加入节点上缺失的事务，按照顺序replay它们，直到加入节点赶上集群进度</span>。</p>
<p><span style="background-color: #c0c0c0;">IST不会阻塞供给节点</span></p>
<p>使用 IST 最重要的参数是 donor 节点的 gcache.size 大小，这个参数表示分配多少空间用于缓存 writeset。缓存的空间越多，能够使用 IST 的几率就越大</p>
<div class="blog_h1"><span class="graybg">SST</span></div>
<p>所谓状态快照转移（SST），就是指集群通过将某个up-to-date节点的数据集，完整的拷贝到另外一个节点的过程/操作。</p>
<div class="blog_h2"><span class="graybg">SST类型</span></div>
<p>从概念上来看，存在两种状态转移方式。</p>
<div class="blog_h3"><span class="graybg">逻辑</span></div>
<p>即mysqldump SST，该SST方法，实际上是调用mysqldump，从源节点获取数据库的逻辑dump文件。</p>
<p>要使用mysqldump SST，新加入的节点必须完全初始化，并且准备好接受连接。</p>
<p>该SST方法会阻塞源节点，在状态转移期间，无法对源节点进行修改。该SST方法是最慢的一种，在高负载集群中可能导致问题。</p>
<div class="blog_h3"><span class="graybg">物理</span></div>
<p>这类SST方法将数据文件从源节点拷贝到新加入的节点。节点需要在状态转移之后再完成初始化。mariabackup SST等属于这一类别。</p>
<p>这类方法比mysqldump SST快很多，但是具有一些限制条件：</p>
<ol>
<li>只能在服务器启动时使用</li>
<li>新节点必须和源节点配置很相似，例如 innodb_file_per_table取值必须相同</li>
</ol>
<p>这类方法中的某些，例如mariabackup SST，是非阻塞的，也就是说，在状态转移期间源节点可以继续处理请求。</p>
<div class="blog_h2"><span class="graybg">选择SST方法</span></div>
<p>通过设置全局系统变量，可以修改使用的SST方法：</p>
<pre class="crayon-plain-tag">SET GLOBAL wsrep_sst_method='mariabackup';</pre>
<p>SST方法也可以在配置文件中修改：</p>
<pre class="crayon-plain-tag">[mariadb]
wsrep_sst_method = mariabackup</pre>
<p>注意：<span style="background-color: #c0c0c0;">源节点和新节点必须使用相同的SST方法</span>。 建议所有节点使用一致的SST方法。</p>
<div class="blog_h2"><span class="graybg"><a id="sst-methods"></a>内置SST方法</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">SST方法</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td class="blog_h3">mariabackup</td>
<td>
<p>利用工具mariabackup执行SST，这是两种非阻塞的SST方法之一</p>
<p>如果希望在SST期间，能够在源节点上执行查询，推荐使用该方法</p>
<p>该方法需要在节点上安装socat</p>
</td>
</tr>
<tr>
<td class="blog_h3">rsync</td>
<td>
<p>默认SST方法，使用rsync来创建源节点的数据快照。在执行SST期间，源节点使用使用<span style="background-color: #c0c0c0;">读锁锁定</span></p>
<p>这是<span style="background-color: #c0c0c0;">速度最快的SST方法，特别是对于大数据集</span>（因为仅仅拷贝文件）</p>
<p>如果不希望SST期间，源节点上可以执行查询，推荐使用该方法</p>
</td>
</tr>
<tr>
<td class="blog_h3">mysqldump</td>
<td>在源节点上执行mysqldump，然后通过管道发送给连接新节点的mysql客户端</td>
</tr>
<tr>
<td class="blog_h3">xtrabackup-v2</td>
<td>
<p>使用Percona XtraBackup 执行SST，这是两种非阻塞的SST方法之一</p>
<p>需要安装额外的软件</p>
</td>
</tr>
<tr>
<td class="blog_h3">xtrabackup</td>
<td>已经被xtrabackup-v2代替</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">身份验证</span></div>
<p>除了rsync SST之外，都需要配置基于用户名:密码的身份验证：</p>
<pre class="crayon-plain-tag">SET GLOBAL wsrep_sst_auth = 'mariabackup:password';</pre>
<p>或者修改配置文件：</p>
<pre class="crayon-plain-tag">[mariadb]
wsrep_sst_auth = mariabackup:password</pre>
<p>某些身份认证插件，例如unix_socket或gssapi，不需要密码，这种情况下只需要指定用户名： </p>
<pre class="crayon-plain-tag">[mariadb]
wsrep_sst_auth = mariabackup:</pre>
<div class="blog_h2"><span class="graybg">最小集群规模</span></div>
<p>为了防止脑裂的出现，推荐最小集群规模为3。</p>
<p>另外一个需要3节点的原因是，当SST阻塞了源节点时，仍然可以有一个节点能响应查询。</p>
<div class="blog_h2"><span class="graybg">SST失败</span></div>
<p>如果状态转移失败，新节点一般是不可用的。</p>
<p>如果使用mysqldump SST，可能还需要手工恢复某些MariaDB管理表。</p>
<div class="blog_h2"><span class="graybg">mariabackup SST</span></div>
<p>本节介绍如何使用这种SST方法。</p>
<div class="blog_h3"><span class="graybg">设置SST方法</span></div>
<p>修改配置文件：</p>
<pre class="crayon-plain-tag">[mariadb]
wsrep_sst_method = mariabackup </pre>
<p>或者通过命令行设置：</p>
<pre class="crayon-plain-tag">SET GLOBAL wsrep_sst_method='mariabackup';</pre>
<div class="blog_h3"><span class="graybg">配置身份验证</span></div>
<p>该SST方法需要在所有供给（donor）节点上进行<span style="background-color: #c0c0c0;">本地身份验证</span>。</p>
<p>首先需要创建专用的数据库账号： </p>
<pre class="crayon-plain-tag">CREATE USER 'mariabackup'@'localhost' IDENTIFIED BY 'mypassword';
GRANT RELOAD, PROCESS, LOCK TABLES, REPLICATION CLIENT ON *.* TO 'mariabackup'@'localhost';</pre>
<p>然后修改配置文件或执行命令行设置：</p>
<pre class="crayon-plain-tag">[mariadb]
wsrep_sst_auth = mariabackup:mypassword</pre><br />
<pre class="crayon-plain-tag">SET GLOBAL wsrep_sst_auth = 'mariabackup:mypassword';</pre>
<div class="blog_h3"><span class="graybg">免密码认证</span></div>
<p>可以使用unix_socket身份认证插件，这样就不需要指定密码： </p>
<pre class="crayon-plain-tag">CREATE USER 'mysql'@'localhost' IDENTIFIED VIA unix_socket;
GRANT RELOAD, PROCESS, LOCK TABLES, REPLICATION CLIENT ON *.* TO 'mysql'@'localhost';</pre>
<div class="blog_h3"><span class="graybg">选择供给节点 </span></div>
<p>当mariadbbackup在供给（donor）节点创建备份时，它需要在备份最后创建一个全局的锁：</p>
<ol>
<li>在10.3-，锁通过  <pre class="crayon-plain-tag">FLUSH TABLES WITH READ LOCK</pre>实现</li>
<li>在10.4+，锁通过 <pre class="crayon-plain-tag">BACKUP STAGE BLOCK_COMMIT</pre>实现</li>
</ol>
<p>如果集群中某个节点正作为“主”节点来使用 —— 也就是说应用程序连接到它来写入数据，那么该节点不应该用作SST源，因为全局锁会干扰应用程序的写操作。</p>
<p>可以通过下面的系统变量，来设置偏好使用哪些节点作为源：</p>
<pre class="crayon-plain-tag">[mariadb]
; 如果集群由 node1, node2, node3, node4, node5构成。node1作为主使用
; 那么node2可以这样配置：
wsrep_sst_donor=node3,node4,node5,
; 结尾的逗号表示，如果偏好的节点不可用，允许选择任何节点作为源</pre>
<div class="blog_h3"><span class="graybg">配置日志</span></div>
<p>mariabackup SST具有独立的日志，不和数据库日志在一起：</p>
<pre class="crayon-plain-tag">[sst]
sst-log-archive=1
sst-log-archive-dir=/var/log/mysql/sst/
; 记录到syslog中
sst-syslog=1</pre>
<div class="blog_h1"><span class="graybg">负载均衡</span></div>
<p><a href="https://fromdual.com/galera-load-balancer-documentation">Galera Load Balancer</a>（GLB）是一个简单的、特地为Galera设计负载均衡器。该LB支持多种负载均衡策略。</p>
<p>节点可以被<span style="background-color: #c0c0c0;">设置不同的权重，优先使用高权重的节点</span>，根据选择的策略，<span style="background-color: #c0c0c0;">低权重节点可能被忽略</span>（除非高权重节点挂掉）。</p>
<p>一个轻量级的守护进程glbd负责接收客户端连接，并执行LB算法，重定向客户端请求到适当的节点。</p>
<div class="blog_h2"><span class="graybg">特性</span></div>
<ol>
<li>可以在运行时配置后端节点列表</li>
<li>支持drain节点，也就是不分配新的连接给它，但是不会kill服务器 —— 让它优雅关闭</li>
<li>使用epool提升性能</li>
<li>支持多线程，可以利用多核心CPU</li>
<li>支持可选的watchdog来监控后端并调整路由表</li>
</ol>
<div class="blog_h2"><span class="graybg">策略</span></div>
<p>GLB支持以下负载均衡策略：</p>
<ol>
<li>least connected： 最少连接，新连接并分发给连接最少的节点，需要考虑节点权重</li>
<li>round-robin：依次循环调度给每个节点</li>
<li>single：所有连接调度给最高权重的节点，除非此节点挂了，或者引入一个新的、更高权重的节点</li>
<li>random：随机调度</li>
<li>source tracking：来自同一源地址的连接，总是被调度给同一节点</li>
</ol>
<div class="blog_h1"><span class="graybg">仲裁节点</span></div>
<p>推荐的部署模式是3节点以上，奇数节点。</p>
<p>如果部署第3个节点（例如只有两个数据中心）的成本过高，可以考虑引入仲裁节点（Galera Arbitrator）以防止脑裂。仲裁节点：</p>
<ol>
<li>参与投票，来决定Primary Component。当两节点集群出现网络分区时，能够看到仲裁节点的那个构成Primary Component</li>
<li>不参与复制。<span style="background-color: #c0c0c0;">尽管仲裁节点不参与复制，但是它会接收所有数据（但是不存储），这意味着你不能将其放置在网络连接差的地方，否则可能导致很差的集群性能</span></li>
</ol>
<p><span style="background-color: #c0c0c0;">仲裁节点还有另外一个用途——用于备份</span>，它可以请求数据库状态的一致性快照。</p>
<div class="blog_h2"><span class="graybg">启动仲裁节点</span></div>
<p>由独立的守护进程garbd来运行仲裁节点：</p>
<pre class="crayon-plain-tag">garbd --group=example_cluster \
     --address="gcomm://192.168.1.1,192.168.1.2,192.168.1.3" \
     --option="socket.ssl_key=/etc/ssl/galera/server-key.pem;socket.ssl_cert=/etc/ssl/galera/server-cert.pem;socket.ssl_ca=/etc/ssl/galera/ca-cert.pem;socket.ssl_cipher=AES128-SHA256""</pre>
<p>配置信息也可以放在独立文件中：</p>
<pre class="crayon-plain-tag">group = example_cluster
address = gcomm://192.168.1.1,192.168.1.2,192.168.1.3</pre><br />
<pre class="crayon-plain-tag">garbd --cfg /path/to/arbitrator.config</pre>
<div class="blog_h1"><span class="graybg">备份和恢复</span></div>
<p>由于Galera集群的复制特性，执行备份时我们只需要在一个节点上进行。而在恢复时，也仅仅需要在一个节点上执行，剩下节点的同步，可以依靠状态转移自动完成。</p>
<div class="blog_h2"><span class="graybg">备份节点</span></div>
<p>任何时候，都可以针对某个Galera节点，使用常规的MariaDB备份工具进行备份。</p>
<p>上文中的<a href="sst-methods">内置SST方法</a>，对应了各种备份工具，混合使用多种备份方法，能够提高额外的数据安全性。这里挑选几个工具说明用法：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 120px; text-align: center;">备份工具</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>mysqldump</td>
<td>
<p>逻辑备份工具，也就是说它是面向SQL的，适合在不同版本的数据库之间进行数据迁移</p>
<p>备份时可以使用<pre class="crayon-plain-tag">--single-transaction</pre>选项，这样可以得到InnoDB表的一致性备份</p>
<p>mysqldump实际上使用SELECT * FROM table的方式来加载数据，这样会有个问题，数据量大的时候，会导致InnoDB缓冲池反复刷新，导致节点性能严重下降，进而导致整个Galera集群性能下降 —— Galera集群<span style="background-color: #c0c0c0;">性能取决于最慢的那个节点</span>。如果设置<pre class="crayon-plain-tag">wsrep_desync=OFF</pre>，让备份中的节点临时的不再同步，则集群其他节点的性能不会受到影响</p>
<pre class="crayon-plain-tag"># 备份所有数据库
mysqldump -u root -p pswd –all-databases &gt; data.sql
# 仅仅备份gmem数据库的表结构
mysqldump -u root -p pswd –no-data gmem &gt; schema.sql


# 恢复数据库，可能需要先清空
mysql&gt; drop database gmem;

mysql -u root -p pswd &lt;  schema.sql</pre>
</td>
</tr>
<tr>
<td>xtrabackup</td>
<td>
<p>物理备份工具，针对数据块，支持全量备份、增量备份。该工具来自Percona，包含一个C语言编写的xtrabackup、Perl语言编写的innobackupex，支持备份InnoDB、XtraDB、MyIASM等引擎的表
<p>在备份期间，<span style="background-color: #c0c0c0;">xtrabackup不会锁定（热备份）数据库。对于100G+的大数据库，它的恢复性能要比mysqldump好的多</span></p>
<p>基于xtrabackup的备份/恢复流程包括三个步骤：备份、准备、恢复</p>
<p>使用--backup子命令，可以进行备份。</p>
<pre class="crayon-plain-tag">#                   数据目录                  备份存放目录
xtrabackup --backup --datadir=/var/lib/mysql/ --target-dir=/data/backups/mysql/</pre>
<p>命令将会启动一个文件拷贝线程，拷贝ibdata文件，一个日志扫描线程，复制事务日志：</p>
<pre class="crayon-plain-tag">xtrabackup: Transaction log of lsn (&lt;SLN&gt;) to (&lt;LSN&gt;) was copied.</pre>
<p>备份完毕后，备份存放目录下，会出现类似下面的文件：</p>
<pre class="crayon-plain-tag"># 假设使用innodb_file_per_table选项
/data/backups/mysql/ibdata1
/data/backups/mysql/test
/data/backups/mysql/test/tbl1.ibd
/data/backups/mysql/xtrabackup_checkpoints
/data/backups/mysql/xtrabackup_logfile</pre>
<p>--backup子命令得到的备份，需要进行--prepare操作后，才能实现针对某个特定时间点的、数据文件的一致性。这是由于，不同数据文件是在不同时间点拷贝的，在这个拷贝过程中，先前的数据文件可能又存在新的修改。使用这些数据文件去启动数据库，InnoDB可能会检测到数据损坏并拒绝启动</p>
<p>--prepare子命令调用方式如下：</p>
<pre class="crayon-plain-tag">xtrabackup --prepare --target-dir=/data/backups/mysql/

# 结束时会打印
101107 16:40:15  InnoDB: Shutdown completed; log sequence number &lt;LSN&gt;</pre>
<p>此时的备份是满足数据一致性，可以用于恢复。直接拷贝数据文件即可：</p>
<pre class="crayon-plain-tag">rsync -avrP /data/backup/ /var/lib/mysql/
chown -R mysql:mysql /var/lib/mysql</pre>
<p>注意：xtrabackup仅仅备份InnoDB数据文件，你需要额外备份MySQL系统数据库、MyISAM数据、表定义文件（.frm）等。或者使用innobackupex让这一切都自动化：</p>
<pre class="crayon-plain-tag"># 备份      数据库帐号密码                      备份根目录            不创建基于时间戳的子目录
innobackupex --user=DBUSER --password=DBUSERPASS /path/to/BACKUP-DIR/ --no-timestamp
#            指定MySQL配置文件
innobackupex --defaults-file=/tmp/other-my.cnf --user=DBUSER --password=DBUSERPASS /path/to/BACKUP-DIR/

# 准备                   允许使用多少内存
innobackupex --apply-log --use-memory=100M  /path/to/BACKUP-DIR

# 恢复
innobackupex --copy-back /path/to/BACKUP-DIR</pre>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">备份集群</span></div>
<p><span style="background-color: #ffffff;">由于</span><span style="background-color: #c0c0c0;">所有Galera节点具有相同的数据</span>，因此你可以使用常规的MariaDB备份工具进行备份，但是这种方式存在缺点 —— 缺少全局事务ID信息（GTID）。使用这种备份来恢复，不能让节点进入良好定义的状态。此外，某些备份工具会阻塞节点，这也需要注意。
<p>因此，理想的备份方式是发起一次SST：</p>
<ol>
<li>节点会在一个良好定义的点发起备份</li>
<li>节点会将GTID关联到备份</li>
<li>备份期间，节点从集群desync，避免影响性能 —— 备份过程可能阻塞节点</li>
<li>集群知道节点正在进行备份，因此不会将其选为donor</li>
</ol>
<p>我们需要利用仲裁节点来发起备份：</p>
<pre class="crayon-plain-tag"># garbd可以以函数方式执行，调用一个命令
#                                   如果在集群节点上发起，则4567已被占用，另外选择一个端口
garbd --address gcomm://192.168.1.2?gmcast.listen_addr=tcp://0.0.0.0:4444 \
#   机器名称               供给节点名称           在供给节点上执行的脚本的后缀
  --group example_cluster --donor galera-3 --sst backup_mysqldump</pre>
<p>你也可以通过配置文件指定各种选项：</p>
<pre class="crayon-plain-tag">; 填写集群的wsrep_cluster_name
group='example_cluster'
; 填写集群的wsrep_cluster_address
address="gcomm://172.31.30.39:4567,172.31.18.53:4567,172.31.26.106:4567"
; 填写集群的wsrep_provider_options。listen_addr添加garbd进程的监听端口
options="gmcast.listen_addr=tcp://0.0.0.0:4444"
; 指定哪个节点执行备份
donor="galera-3"
; 日志文件路径
log='/var/log/garbd.log'
; 备份节点上的备份脚本的后缀，对于下面的后缀，脚本名是wsrep_sst_backup_mysqldump
sst='backup_mysqldump'</pre>
<p>下面是一个脚本的示例，它使用了bash + mysqldump，但是任何脚本语言、备份工具都是可以的： </p>
<pre class="crayon-plain-tag">#!/bin/bash

# SET VARIABLES
db_user='admin_backup'
db_passwd='Rover123!'

backup_dir='/backup'
backup_sub_dir='temp'

today=`date +"%Y%m%d"`
# Dump文件路径
backup_today="galera-mysqldump-$today.sql"
# GTID文件路径
gtid_file="gtid-$today.dat"


# 加载此脚本，它提供GTID变量
. /usr/bin/wsrep_sst_common


# 备份配置文件
cp /etc/my.cnf $backup_dir/$backup_sub_dir/
cp /etc/garb.cnf $backup_dir/$backup_sub_dir/
# 备份GTID
echo "GTID: ${WSREP_SST_OPT_GTID}" &gt; $backup_dir/$backup_sub_dir/$gtid_file


# 进行数据库Dump
mysqldump --user="$db_user" --password="$db_passwd" \
          --flush-logs --all-databases \
          &gt; $backup_dir/$backup_sub_dir/$backup_today

# 打包
cd $backup_sub_dir
tar -czf $backup_dir/$backup_today.tgz * --transform "s,^,$backup_today/," </pre>
<div class="blog_h2"><span class="graybg">恢复集群</span></div>
<p>将备份恢复到一个新集群，或者既有集群，可以使用相同的步骤：</p>
<ol>
<li>选择一个节点，作为恢复节点</li>
<li>停止其它节点</li>
<li>关闭恢复节点的Galera Replication，进入Standalone模式：<br />
<pre class="crayon-plain-tag">SET GLOBAL wsrep_provider = 'none';

SHOW STATUS LIKE 'wsrep_connected';

+-----------------+-------+
 
| Variable_name   | Value |
 
+-----------------+-------+
 
| wsrep_connected | OFF   |
 
+-----------------+-------+</pre>
</li>
<li>调用备份工具，执行恢复</li>
<li>在恢复节点上重新开启Galera Replication</li>
<li>启动被停止的节点重新加入集群，并通过SST重新获得同步</li>
</ol>
<div class="blog_h1"><span class="graybg">wsrep提供者</span></div>
<div class="blog_h2"><span class="graybg">GCache</span></div>
<p>用于存放writeset的特殊缓存，叫作writeset cache或GCache。GCache支持三种存储：</p>
<ol>
<li>Permanent In-Memory Store：直接使用操作系统的内存，适合有很多空余内存的系统，默认情况是禁用的</li>
<li>Permanent Ring-Buffer File：在缓存初始化时就预先分配的一块磁盘空间，默认情况下，它的大小是 128Mb</li>
<li>On-Demand Page Store：根据需要在运行时动态分配内存映射页文件，默认情况下是 128Mb，但如果需要更大的 write-set，它可以动态的增长。当 page file 不再使用时，Galera Cluster 会删除这些文件，可以对 page file 的总大小进行限制</li>
</ol>
<p>Galera Cluster 的缓存分配算法会按上面的顺序来分配缓存空间，gcache.dir 参数可以指定缓存保存在磁盘的位置。</p>
<div class="blog_h2"><span class="graybg">复制流控</span></div>
<p>Galera允许一个节点根据需要来暂停/恢复复制，这叫作Flow Control。FC可以节点在apply事务方面，过于落后其它节点。</p>
<p>我们知道，所有节点收到writeset并进行认证之后，原始的接收请求的节点就可以在本地完成提交并应答客户端了。此时，其它节点不一定也apply/commit了事务（取决于配置）。尚未apply/commit的事务（writeset）会存放在接收队列中。</p>
<p>如果某个节点速度缓慢，那么它的接收队列会不断积压，到达一定程度之后，就会触发FC：</p>
<ol>
<li>暂停复制</li>
<li>处理接收队列的积压</li>
<li>当队列到达可管理的大小之后，再恢复复制</li>
</ol>
<div class="blog_h2"><span class="graybg">隔离级别</span></div>
<p>在Galera中，需要去分单节点内的、集群范围的事务隔离级别。单节点内的事务隔离级别不必多提，参考MariaDB提供的配置项即可。</p>
<p>集群范围的事务隔离，受到复制协议的影响，不同节点上发起的事务，可能不会完全等同的隔离。</p>
<div class="blog_h2"><span class="graybg">Component</span></div>
<p>集群中能互相保持心跳通信的节点的集合叫作Component，两个Component之间存在网络分区，无法相互通信。理想情况下，集群只有一个Component。</p>
<div class="blog_h3"><span class="graybg">Primary Component</span></div>
<p>大部分节点组成的Component叫作Primary Component —— <span style="background-color: #c0c0c0;">PC必须满足大多数原则，在三节点集群中，至少两个节点才能组成PC</span>。</p>
<p><span style="background-color: #c0c0c0;">只有位于PC中的节点，才能继续修改数据库状态</span>。</p>
<p>Galera Cluster 会周期性的检查每个节点的连接是否正常， evs.inactive_check_peroid 参数可以设置检查的周期。如果一个节点的失效时间超过了 evs.suspect_timeout 的值，那么节点将被标记为 suspect。如果 Primary Component 中的所有节点都将某个节点标记为 suspect ，那么此节点被移出集群。</p>
<p>如果一个节点的失效时间超过了 evs.inactive_timeout 的值，此节点将直接被移出集群而不需要协商。故障的节点将不可读不可写。</p>
<div class="blog_h3"><span class="graybg">节点权重</span></div>
<p>在计算是否构成Primary Component / Quorum时，节点是可以被加权的。节点的权重通过pc.weight配置。例如下面的配置：</p>
<pre class="crayon-plain-tag">node1: pc.weight = 2
node2: pc.weight = 1
node3: pc.weight = 0</pre>
<p>即使同时杀掉node2和node3，node1仍然构成PC </p>
<div class="blog_h2"><span class="graybg">提供者选项</span></div>
<p>提供者选项在系统变量 wsrep_provider_options 中配置，选项之间用;隔开。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 30%; text-align: center;">选项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td class=" blog_h3">base_dir</td>
<td>数据目录</td>
</tr>
<tr>
<td class=" blog_h3">base_host</td>
<td>默认127.0.0.1。内部使用，不要设置</td>
</tr>
<tr>
<td class=" blog_h3">base_port</td>
<td>默认4567。内部使用，不要设置</td>
</tr>
<tr>
<td class=" blog_h3">cert.log_conflicts</td>
<td>默认no。是否记录认证失败的详细信息</td>
</tr>
<tr>
<td class=" blog_h3">cert.optimistic_pa</td>
<td>
<p>默认yes。控制在slave上并行apply的行为</p>
<p>如果设置，允许认证算法（ certification algorithm）所确定（determined）的全范围并行化（full range of parallelization ）</p>
<p>如果不设置，并行apply窗口（parallel applying window）不会超过在master上看到的窗口。并且，一旦它所看到的master上操作被commit之后，立即开始apply</p>
</td>
</tr>
<tr>
<td class=" blog_h3">debug</td>
<td>默认no。是否启用调试</td>
</tr>
<tr>
<td class=" blog_h3">evs.auto_evict</td>
<td>
<p><span style="background-color: #c0c0c0;">触发自动驱除</span>协议（Auto Eviction protocol）之前，允许一个<span style="background-color: #c0c0c0;">落后（delayed）节点的entry的数量</span></p>
<p>每当接收到某个节点的一个delayed response，就在delayed list中添加一个entry</p>
<p>默认0，表示禁用</p>
</td>
</tr>
<tr>
<td class=" blog_h3">evs.debug_log_mask</td>
<td>
<p>开启EVS调试日志，要求wsrep_debug=on</p>
</td>
</tr>
<tr>
<td class=" blog_h3">evs.delay_margin</td>
<td>
<p>在此节点<span style="background-color: #c0c0c0;">加入一个entry到delayed list之前</span>，<span style="background-color: #c0c0c0;">响应时间的延迟</span>可达到多少。必须设置的比节点之间的RTT大</p>
<p>其实就是，evs.delay_margin决定了多慢的情况下进行一次计数，evs.auto_evict决定了计数多少次后发起驱除</p>
</td>
</tr>
<tr>
<td class=" blog_h3">evs.delayed_keep_period</td>
<td>如果一个节点被当前节点加入自己的delayed list，那么该节点要持续多长时间不再缓慢，才能从延迟列表移除</td>
</tr>
<tr>
<td class=" blog_h3">evs.evict</td>
<td>
<p>如果设置为当前节点的gcomm UUID，则当前节点从集群移除</p>
<p>如果设置为空，则当前节点的eviction list被清空</p>
</td>
</tr>
<tr>
<td class=" blog_h3">evs.inactive_check_period</td>
<td>检测不活动成员（peer）（即响应延迟的节点）的周期。超过此周期之后，节点才可能被加入到delayed list，并在之后进行驱除</td>
</tr>
<tr>
<td class=" blog_h3">evs.inactive_timeout</td>
<td>节点被断定为挂掉之前，inactive的最大持续时间</td>
</tr>
<tr>
<td class=" blog_h3">evs.info_log_mask</td>
<td>记录额外EVS信息的日志</td>
</tr>
<tr>
<td class=" blog_h3">evs.install_timeout</td>
<td>等待install message确认的超时时间</td>
</tr>
<tr>
<td class=" blog_h3">evs.join_retrans_period</td>
<td>组成集群成员关系时，EVS join消息重传的频率</td>
</tr>
<tr>
<td class=" blog_h3">evs.keepalive_period</td>
<td>没有其它流量时，多就发送一次心跳</td>
</tr>
<tr>
<td class=" blog_h3">evs.max_install_timeouts</td>
<td>默认3。install message允许的超时总次数</td>
</tr>
<tr>
<td class=" blog_h3">evs.send_window</td>
<td>
<p>单次允许复制的packet的数量。必须比evs.user_send_window大</p>
<p>仅仅影响数据包，在WAN环境下可以设置的比默认值（4）大的多，例如512</p>
</td>
</tr>
<tr>
<td class=" blog_h3">evs.stats_report_period</td>
<td>报告EVS统计信息的周期</td>
</tr>
<tr>
<td class=" blog_h3">evs.suspect_timeout</td>
<td>在此超时之后，节点被怀疑已经挂了。如果所有节点怀疑它挂了，那么在evs.install_timeout之前节点会被移除集群</td>
</tr>
<tr>
<td class=" blog_h3">evs.use_aggregate</td>
<td>默认true。如果为true则在可能的情况下小的packet会被聚合为大的</td>
</tr>
<tr>
<td class=" blog_h3">evs.user_send_window</td>
<td>
<p>单次允许复制的packet的数量。必须比evs.send_window小，推荐 1/2</p>
</td>
</tr>
<tr>
<td class=" blog_h3">evs.version</td>
<td>EVS协议版本</td>
</tr>
<tr>
<td class=" blog_h3">evs.view_forget_timeout</td>
<td>从view history中移除之前需要经过的时间</td>
</tr>
<tr>
<td class=" blog_h3">gcache.dir</td>
<td>
<p>GCache存放的目录</p>
</td>
</tr>
<tr>
<td class=" blog_h3">gcache.keep_pages_size</td>
<td>缓存页的总数量</td>
</tr>
<tr>
<td class=" blog_h3">gcache.mem_size</td>
<td>GCache缓存使用内存大小</td>
</tr>
<tr>
<td class=" blog_h3">gcache.name</td>
<td>GCache缓存ring buffer文件的名字</td>
</tr>
<tr>
<td class=" blog_h3"><strong>gcache.page_size</strong></td>
<td>GCache缓存页面映射文件的大小</td>
</tr>
<tr>
<td class=" blog_h3">gcache.recover</td>
<td>在节点启动时，是否进行GCache恢复。如果能够恢复GCache，则节点加入集群后可以提供IST —— 这在集群完全重启的时候有用</td>
</tr>
<tr>
<td class=" blog_h3"><strong>gcache.size</strong></td>
<td>缓存ring buffer大小，即用于缓存writeset的空间大小，启动时分配</td>
</tr>
<tr>
<td class=" blog_h3">gcomm.thread_prio</td>
<td>Gcomm线程策略和优先级</td>
</tr>
<tr>
<td class=" blog_h3">gcs.fc_debug</td>
<td>默认0，大于0时会输出SST流控信息</td>
</tr>
<tr>
<td class=" blog_h3">gcs.fc_factor</td>
<td>默认1.0，接收队列中writeset数量跌到gcs.fc_limit的多少比例后，恢复复制</td>
</tr>
<tr>
<td class=" blog_h3">gcs.fc_limit</td>
<td>默认16。接收队列中writeset超过此数量后，复制暂停。在master-slave用法下，可以大大增加</td>
</tr>
<tr>
<td class=" blog_h3">gcs.fc_master_slave</td>
<td>默认no。是否假设集群仅仅有一个master —— 就是所有客户端都连接到一个节点进行写查询</td>
</tr>
<tr>
<td class=" blog_h3">gcs.max_packet_size</td>
<td>最大packet大小，超过此大小则writeset被分片</td>
</tr>
<tr>
<td class=" blog_h3">gcs.max_throttle</td>
<td>
<p>在状态转移时，为了避免内存消耗过大，可以进行复制限流</p>
<p>默认0.25。设置为0则为了完成状态转移，可以暂停复制</p>
</td>
</tr>
<tr>
<td class=" blog_h3">gcs.recv_q_hard_limit</td>
<td>
<p>接收队列大小的硬限制。超过此大小则服务器终止运作</p>
<p>推荐设置为内存的 1/2 + swap大小</p>
</td>
</tr>
<tr>
<td class=" blog_h3">gcs.recv_q_soft_limit</td>
<td>
<p>接收队列软限制。相对于gcs.recv_q_hard_limit的因子，默认0.25</p>
<p>到达软限制后，即开始限流。限流的速度随着队列积压，线性增加</p>
</td>
</tr>
<tr>
<td class=" blog_h3">gcs.sync_donor</td>
<td>
<p>默认no。是否集群其它成员需要和donor保持同步</p>
<p>如果设置为yes，则当donor因为状态转移而阻塞时整个集群被阻塞</p>
</td>
</tr>
<tr>
<td class=" blog_h3">gmcast.listen_addr</td>
<td>默认tcp:0.0.0.0:4567。Galera监听来自其它节点的连接的地址</td>
</tr>
<tr>
<td class=" blog_h3">gmcast.mcast_addr</td>
<td>
<p>默认空。如果设置，则使用UDP组播进行复制，所有节点必须设置一致</p>
<p>示例：gmcast.mcast_addr=239.192.0.11</p>
</td>
</tr>
<tr>
<td class=" blog_h3">gmcast.mcast_ttl</td>
<td>组播的TTL值</td>
</tr>
<tr>
<td class=" blog_h3">gmcast.peer_timeout</td>
<td>initiating message relaying的连接超时</td>
</tr>
<tr>
<td class=" blog_h3">gmcast.segment</td>
<td>
<p>定义节点属于的段（segment）。默认情况下，所有节点都在段0。通常将同一数据中心的节点放在同一个段</p>
<p>Galera协议数据，仅仅会重定向到同一段中的单个节点，然后由该节点再repay给段中其它节点</p>
<p>可以降低跨数据中心的流量</p>
<p>取值0-255之间</p>
</td>
</tr>
<tr>
<td class=" blog_h3">ist.recv_addr</td>
<td>监听IST的地址</td>
</tr>
<tr>
<td class=" blog_h3">pc.ignore_quorum</td>
<td>
<p>默认false。是否忽略quorum计算</p>
<p>如果设置为true，如果master和其它slaves产生了网络分区，它仍然继续工作。但是会导致master-slave模式下slave不能自动重新连接到master</p>
</td>
</tr>
<tr>
<td class=" blog_h3">pc.ignore_sb</td>
<td>默认false。是否允许脑裂情况下继续处理数据修改，在multi-master模式下会导致数据不一致</td>
</tr>
<tr>
<td class=" blog_h3">pc.recovery</td>
<td>默认true。如果设置为true，则Primary Component状态被存储在磁盘中。当集群完全崩溃后，可以进行自动恢复，后续的集群优雅完全重启会要求从一个新的Primary Component自举</td>
</tr>
<tr>
<td class=" blog_h3">pc.wait_prim</td>
<td>默认true。如果设置为true，节点会在pc.wait_prim_timeout之内等待Primary Component。用于启动non-primary component并使通过pc.bootstrap使之称为primary</td>
</tr>
<tr>
<td class=" blog_h3">pc.wait_prim_timeout</td>
<td>等待Primary Component的超时</td>
</tr>
<tr>
<td class=" blog_h3">pc.weight</td>
<td>节点的权重，用于quorum计算</td>
</tr>
<tr>
<td class=" blog_h3">protonet.backend</td>
<td>传输后端，目前仅仅支持ASIO</td>
</tr>
<tr>
<td class=" blog_h3">repl.causal_read_timeout</td>
<td>causal reads（读取已提交）超时</td>
</tr>
<tr>
<td class=" blog_h3">repl.commit_order</td>
<td>
<p>是否、何时允许乱序commit：</p>
<p style="padding-left: 30px;">0 BYPASS  不监控commit顺序<br />1 OOOC 对于所有事务允许乱序提交<br />2 LOCAL_OOOC 仅仅允许本地事务的乱序提交<br />3 NO_OOOC 不允许乱序提交</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg"><a id="k8s"></a>在K8S中运行</span></div>
<div class="blog_h2"><span class="graybg">Orange方案</span></div>
<p>Orange开发了一个Galera Operator，简化K8S中的Galera的部署和运维。支持的特性包括：</p>
<ol>
<li>创建、销毁集群</li>
<li>集群规模括缩容</li>
<li>垂直括缩：修改容器CPU/内存、持久卷大小</li>
<li>节点崩溃后自动故障转移</li>
<li>备份Galera集群到外部S3存储</li>
<li>从外部备份恢复Galera集群</li>
</ol>
<p>&nbsp;</p>
<div class="blog_h2"><span class="graybg">severalnines方案</span></div>
<p>来自 <a href="https://github.com/severalnines/galera-docker-mariadb">severalnines / galera-docker-mariadb</a>。此方案在K8S中运行Maria DB 10.1，支持Galera，需要同时运行一个Etcd集群。</p>
<div class="blog_h3"><span class="graybg">entrypoint.sh</span></div>
<p>逻辑比较乱，主要是通过查询Etcd来获得集群节点列表、各节点的seqno信息：</p>
<pre class="crayon-plain-tag">#!/bin/bash
set -e

# if command starts with an option, prepend mysqld
if [ "${1:0:1}" = '-' ]; then
  CMDARG="$@"
fi

[ -z "$TTL" ] &amp;&amp; TTL=10

if [ -z "$CLUSTER_NAME" ]; then
  echo &gt;&amp;2 'Error:  You need to specify CLUSTER_NAME'
  exit 1
fi

DATADIR="$("mysqld" --verbose --help 2&gt;/dev/null | awk '$1 == "datadir" { print $2; exit }')"
echo &gt;&amp;2 "Content of $DATADIR:"
ls -al $DATADIR

if [ ! -s "$DATADIR/grastate.dat" ]; then
  # 没有grastate.dat文件，意味着集群尚未初始化
  INITIALIZED=1
  if [ -z "$MYSQL_ROOT_PASSWORD" -a -z "$MYSQL_ALLOW_EMPTY_PASSWORD" -a -z "$MYSQL_RANDOM_ROOT_PASSWORD" ]; then
    echo &gt;&amp;2 'error: database is uninitialized and password option is not specified '
    echo &gt;&amp;2 '  You need to specify one of MYSQL_ROOT_PASSWORD, MYSQL_ALLOW_EMPTY_PASSWORD and MYSQL_RANDOM_ROOT_PASSWORD'
    exit 1
  fi
  mkdir -p "$DATADIR"
  chown -R mysql:mysql "$DATADIR"

  # 初始化MariaDB
  echo 'Running mysql_install_db'
  mysql_install_db --user=mysql --datadir="$DATADIR" --rpm
  echo 'Finished mysql_install_db'

  # 在后台启动MariaDB
  mysqld --user=mysql --datadir="$DATADIR" --skip-networking &amp;
  pid="$!"

  mysql=(mysql --protocol=socket -uroot)

  # 等待MariaDB初始化完毕
  for i in {30..0}; do
    if echo 'SELECT 1' | "${mysql[@]}" &amp;&gt;/dev/null; then
      break
    fi
    echo 'MySQL init process in progress...'
    sleep 1
  done
  if [ "$i" = 0 ]; then
    echo &gt;&amp;2 'MySQL init process failed.'
    exit 1
  fi

  # sed is for https://bugs.mysql.com/bug.php?id=20545
  mysql_tzinfo_to_sql /usr/share/zoneinfo | sed 's/Local time zone must be set--see zic manual page/FCTY/' | "${mysql[@]}" mysql
  if [ ! -z "$MYSQL_RANDOM_ROOT_PASSWORD" ]; then
    MYSQL_ROOT_PASSWORD="$(pwmake 128)"
    echo "GENERATED ROOT PASSWORD: $MYSQL_ROOT_PASSWORD"
  fi

  # 在本地（禁止复制）创建root xtrabackup等用户
  "${mysql[@]}" &lt;&lt;-EOSQL
	  -- What's done in this file shouldn't be replicated
	  --  or products like mysql-fabric won't work
	  SET @@SESSION.SQL_LOG_BIN=0;
	  DELETE FROM mysql.user ;
	  CREATE USER 'root'@'%' IDENTIFIED BY '${MYSQL_ROOT_PASSWORD}' ;
	  GRANT ALL ON *.* TO 'root'@'%' WITH GRANT OPTION ;
	  CREATE USER 'xtrabackup'@'localhost' IDENTIFIED BY '$XTRABACKUP_PASSWORD';
	  GRANT RELOAD,LOCK TABLES,REPLICATION CLIENT ON *.* TO 'xtrabackup'@'localhost';
	  GRANT REPLICATION CLIENT ON *.* TO monitor@'%' IDENTIFIED BY 'monitor';
	  DROP DATABASE IF EXISTS test ;
	  FLUSH PRIVILEGES ;
EOSQL
  if [ ! -z "$MYSQL_ROOT_PASSWORD" ]; then
    mysql+=(-p"${MYSQL_ROOT_PASSWORD}")
  fi

  # 创建额外的数据库、用户
  if [ "$MYSQL_DATABASE" ]; then
    echo "CREATE DATABASE IF NOT EXISTS \`$MYSQL_DATABASE\` ;" | "${mysql[@]}"
    mysql+=("$MYSQL_DATABASE")
  fi

  if [ "$MYSQL_USER" -a "$MYSQL_PASSWORD" ]; then
    echo "CREATE USER '"$MYSQL_USER"'@'%' IDENTIFIED BY '"$MYSQL_PASSWORD"' ;" | "${mysql[@]}"

    if [ "$MYSQL_DATABASE" ]; then
      echo "GRANT ALL ON \`"$MYSQL_DATABASE"\`.* TO '"$MYSQL_USER"'@'%' ;" | "${mysql[@]}"
    fi

    echo 'FLUSH PRIVILEGES ;' | "${mysql[@]}"
  fi

  if [ ! -z "$MYSQL_ONETIME_PASSWORD" ]; then
    "${mysql[@]}" &lt;&lt;-EOSQL
		ALTER USER 'root'@'%' PASSWORD EXPIRE;
EOSQL
  fi
  if ! kill -s TERM "$pid" || ! wait "$pid"; then
    echo &gt;&amp;2 'MySQL init process failed.'
    exit 1
  fi

  echo
  echo 'MySQL init process done. Ready for start up.'
  echo
fi
chown -R mysql:mysql "$DATADIR"

function join {
  local IFS="$1"
  shift
  echo "$*"
}


# 将当前MariaDB实例加入到Galera集群，通过访问Etcd获取必要信息
if [ -z "$DISCOVERY_SERVICE" ]; then
  cluster_join=$CLUSTER_JOIN
else
  echo
  echo '&gt;&gt; Registering in the discovery service'

  etcd_hosts=$(echo $DISCOVERY_SERVICE | tr ',' ' ')
  flag=1

  echo
  # 寻找健康Etcd节点
  for i in $etcd_hosts; do
    echo "&gt;&gt; Connecting to https://${i}/health"
    curl -s -k --cacert /etc/etcd/ca.crt --cert /etc/etcd/client.crt --key /etc/etcd/client.key https://${i}/health || continue
    if curl -s -k --cacert /etc/etcd/ca.crt --cert /etc/etcd/client.crt --key /etc/etcd/client.key https://$i/health | jq -e 'contains({ "health": "true"})'; then
      healthy_etcd=$i
      flag=0
      break
    else
      echo &gt;&amp;2 "&gt;&gt; Node $i is unhealty. Proceed to the next node."
    fi
  done

  # 如果没有Etcd节点则放弃
  if [ $flag -ne 0 ]; then
    echo "&gt;&gt; Couldn't reach healthy etcd nodes."
    exit 1
  fi

  echo
  echo "&gt;&gt; Selected healthy etcd: $healthy_etcd"

  if [ ! -z "$healthy_etcd" ]; then
    URL="https://$healthy_etcd/v2/keys/galera/$CLUSTER_NAME"

    set +e
    echo &gt;&amp;2 "&gt;&gt; Waiting for $TTL seconds to read non-expired keys.."
    # 防止刚刚死去的Galera节点的注册信息仍然存在
    sleep $TTL

    # 获取活动Galera节点列表
    echo &gt;&amp;2 "&gt;&gt; Retrieving list of keys for $CLUSTER_NAME"
    addr=$(curl -s -k --cacert /etc/etcd/ca.crt --cert /etc/etcd/client.crt --key /etc/etcd/client.key $URL | jq -r '.node.nodes[]?.key' | awk -F'/' '{print $(NF)}')
    cluster_join=$(join , $addr)

    ipaddr=$(hostname -i | awk {'print $1'})
    [ -z $ipaddr ] &amp;&amp; ipaddr=$(hostname -I | awk {'print $1'})

    echo
    if [ -z $cluster_join ]; then
      # 没有活动Galera节点。集群尚未初始化，或者集群完全宕机后重启（需要bootstrap）
      echo &gt;&amp;2 "&gt;&gt; KV store is empty. This is a the first node to come up."
      echo
      echo &gt;&amp;2 "&gt;&gt; Registering $ipaddr in https://$healthy_etcd"
      # 写入当前节点信息到Etcd
      curl -s -k --cacert /etc/etcd/ca.crt --cert /etc/etcd/client.crt --key /etc/etcd/client.key $URL/$ipaddr/ipaddress -X PUT -d "value=$ipaddr"
    else
      # 存在活动的Galera节点，看看有没有Synced的
      curl -s -k --cacert /etc/etcd/ca.crt --cert /etc/etcd/client.crt --key /etc/etcd/client.key ${URL}?recursive=true\&amp;sorted=true &gt;/tmp/out
      running_nodes=$(cat /tmp/out | jq -r '.node.nodes[].nodes[]? | select(.key | contains ("wsrep_local_state_comment")) | select(.value == "Synced") | .key' | awk -F'/' '{print $(NF-1)}' | tr "\n" ' ' | sed -e 's/[[:space:]]*$//')
      echo
      echo "&gt;&gt; Running nodes: [${running_nodes}]"

      if [ -z "$running_nodes" ]; then
        # 没有Synced节点，必须恢复本节点的seqno，如果本节点最大，则可以自举
        TMP=/var/lib/mysql/$(hostname).err
        echo &gt;&amp;2 "&gt;&gt; There is no node in synced state."
        echo &gt;&amp;2 "&gt;&gt; It's unsafe to bootstrap unless the sequence number is the latest."
        echo &gt;&amp;2 "&gt;&gt; Determining the Galera last committed seqno using --wsrep-recover.."
        echo

        # 恢复seqno
        mysqld_safe --wsrep-cluster-address=gcomm:// --wsrep-recover
        cat $TMP
        seqno=$(cat $TMP | tr ' ' "\n" | grep -e '[a-z0-9]*-[a-z0-9]*:[0-9]' | head -1 | cut -d ":" -f 2)
        # 此容器是新启动的，才会设置INITIALIZED。对于新节点，seqno设置为0
        if [ $INITIALIZED -eq 1 ]; then
          echo &gt;&amp;2 "&gt;&gt; This is a new container, thus setting seqno to 0."
          seqno=0
        fi

        echo
        if [ ! -z $seqno ]; then
          # 将当前节点的seqno写入到Etcd
          echo &gt;&amp;2 "&gt;&gt; Reporting seqno:$seqno to ${healthy_etcd}."
          WAIT=$(($TTL * 2))
          curl -s -k --cacert /etc/etcd/ca.crt --cert /etc/etcd/client.crt --key /etc/etcd/client.key $URL/$ipaddr/seqno -X PUT -d "value=$seqno&amp;ttl=$WAIT"
        else
          seqno=$(cat $TMP | tr ' ' "\n" | grep -e '[a-z0-9]*-[a-z0-9]*:[0-9]' | head -1)
          echo &gt;&amp;2 "&gt;&gt; Unable to determine Galera sequence number."
          exit 1
        fi
        rm $TMP

        echo
        echo &gt;&amp;2 "&gt;&gt; Sleeping for $TTL seconds to wait for other nodes to report."
        sleep $TTL

        echo
        echo &gt;&amp;2 "&gt;&gt; Retrieving list of seqno for $CLUSTER_NAME"
        bootstrap_flag=1

        # Retrieve seqno from etcd
        curl -s -k --cacert /etc/etcd/ca.crt --cert /etc/etcd/client.crt --key /etc/etcd/client.key ${URL}?recursive=true\&amp;sorted=true &gt;/tmp/out
        cluster_seqno=$(cat /tmp/out | jq -r '.node.nodes[].nodes[]? | select(.key | contains ("seqno")) | .value' | tr "\n" ' ' | sed -e 's/[[:space:]]*$//')

        for i in $cluster_seqno; do
          if [ $i -gt $seqno ]; then
            # 如果存在其它节点的seqno更大，本节点不处理bootstrap
            bootstrap_flag=0
            echo &gt;&amp;2 "&gt;&gt; Found another node holding a greater seqno ($i/$seqno)"
          fi
        done

        echo
        if [ $bootstrap_flag -eq 1 ]; then
          # 本节点seqno最大，处理bootstrap
          # Find the earliest node to report if there is no higher seqno
          # node_to_bootstrap=$(cat /tmp/out | jq -c '.node.nodes[].nodes[]?' | grep seqno | tr ',:\"' ' ' | sort -k 11 | head -1 | awk -F'/' '{print $(NF-1)}')
          ## The earliest node to report if there is no higher seqno is computed wrongly: issue #6
          node_to_bootstrap=$(cat /tmp/out | jq -c '.node.nodes[].nodes[]?' | grep seqno | tr ',:"' ' ' | sort -k5,5r -k11 | head -1 | awk -F'/' '{print $(NF-1)}')
          if [ "$node_to_bootstrap" == "$ipaddr" ]; then
            echo &gt;&amp;2 "&gt;&gt; This node is safe to bootstrap."
            cluster_join=
          else
            echo &gt;&amp;2 "&gt;&gt; Based on timestamp, $node_to_bootstrap is the chosen node to bootstrap."
            echo &gt;&amp;2 "&gt;&gt; Wait again for $TTL seconds to look for a bootstrapped node."
            sleep $TTL
            curl -s -k --cacert /etc/etcd/ca.crt --cert /etc/etcd/client.crt --key /etc/etcd/client.key ${URL}?recursive=true\&amp;sorted=true &gt;/tmp/out

            # Look for a synced node again
            running_nodes2=$(cat /tmp/out | jq -r '.node.nodes[].nodes[]? | select(.key | contains ("wsrep_local_state_comment")) | select(.value == "Synced") | .key' | awk -F'/' '{print $(NF-1)}' | tr "\n" ' ' | sed -e 's/[[:space:]]*$//')

            echo
            echo &gt;&amp;2 "&gt;&gt; Running nodes: [${running_nodes2}]"

            if [ ! -z "$running_nodes2" ]; then
              cluster_join=$(join , $running_nodes2)
            else
              echo
              echo &gt;&amp;2 "&gt;&gt; Unable to find a bootstrapped node to join."
              echo &gt;&amp;2 "&gt;&gt; Exiting."
              exit 1
            fi
          fi
        else
          # seqno不是最大的，不处理bootstrap
          echo &gt;&amp;2 "&gt;&gt; Refusing to start for now because there is a node holding higher seqno."
          echo &gt;&amp;2 "&gt;&gt; Wait again for $TTL seconds to look for a bootstrapped node."
          sleep $TTL

          # Look for a synced node again
          curl -s -k --cacert /etc/etcd/ca.crt --cert /etc/etcd/client.crt --key /etc/etcd/client.key ${URL}?recursive=true\&amp;sorted=true &gt;/tmp/out
          running_nodes3=$(cat /tmp/out | jq -r '.node.nodes[].nodes[]? | select(.key | contains ("wsrep_local_state_comment")) | select(.value == "Synced") | .key' | awk -F'/' '{print $(NF-1)}' | tr "\n" ' ' | sed -e 's/[[:space:]]*$//')

          echo
          echo &gt;&amp;2 "&gt;&gt; Running nodes: [${running_nodes3}]"

          if [ ! -z "$running_nodes2" ]; then
            cluster_join=$(join , $running_nodes3)
          else
            echo
            echo &gt;&amp;2 "&gt;&gt; Unable to find a bootstrapped node to join."
            echo &gt;&amp;2 "&gt;&gt; Exiting."
            exit 1
          fi
        fi
      else
        # if there is a Synced node, join the address
        cluster_join=$(join , $running_nodes)
      fi
    fi
    set -e

    echo
    echo &gt;&amp;2 "&gt;&gt; Cluster address is gcomm://$cluster_join"
  else
    echo
    echo &gt;&amp;2 '&gt;&gt; No healthy etcd host detected. Refused to start.'
    exit 1
  fi
fi

echo
echo &gt;&amp;2 "&gt;&gt; Starting reporting script in the background"
nohup /report_status.sh root $MYSQL_ROOT_PASSWORD $CLUSTER_NAME $TTL $DISCOVERY_SERVICE &amp;

# set IP address based on the primary interface
sed -i "s|WSREP_NODE_ADDRESS|$ipaddr|g" /etc/my.cnf

echo
echo &gt;&amp;2 "&gt;&gt; Starting mysqld process"
if [ -z $cluster_join ]; then
  export _WSREP_NEW_CLUSTER='--wsrep-new-cluster'
  # set safe_to_bootstrap = 1
  GRASTATE=$DATADIR/grastate.dat
  [ -f $GRASTATE ] &amp;&amp; sed -i "s|safe_to_bootstrap.*|safe_to_bootstrap: 1|g" $GRASTATE
else
  export _WSREP_NEW_CLUSTER=''
fi

exec mysqld --wsrep_cluster_name=$CLUSTER_NAME --wsrep-cluster-address="gcomm://$cluster_join" --wsrep_sst_auth="xtrabackup:$XTRABACKUP_PASSWORD" $_WSREP_NEW_CLUSTER $CMDARG</pre>
<div class="blog_h3"><span class="graybg">report_status.sh</span></div>
<p>该脚本会每TTL报告一次当前Galera容器的状态。</p>
<div class="blog_h3"><span class="graybg">配置</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 30%; text-align: center;">环境变量</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>MYSQL_ROOT_PASSWORD</td>
<td>
<p>MySQL数据库的密码</p>
<p>&nbsp;</p>
</td>
</tr>
<tr>
<td>XTRABACKUP_PASSWORD</td>
<td>
<p>此方案使用的SST方法是XtraBackup SST，它使用用户xtrabackup@localhost，如果需要定制密码，设置该变量</p>
</td>
</tr>
<tr>
<td>CLUSTER_NAME</td>
<td>Galera集群的名称</td>
</tr>
<tr>
<td>DISCOVERY_SERVICE</td>
<td>
<p>如果需要启用发现服务（目前仅仅支持Etcd），需要配置该变量，形式IP:PORT，多个地址需要逗号分隔</p>
<p>容器会访问发现服务，使用CLUSTER_NAME来查询，如果发现已经存在对应的Galera集群，则加入；否则创建新的Galera集群</p>
</td>
</tr>
<tr>
<td>CLUSTER_JOIN</td>
<td>
<p>如果不启用发现服务，则需要设置此变量：</p>
<ol>
<li>如果变量为空，则创建新集群</li>
<li>如果不为空，则连接到此地址所指向的、既有集群的节点</li>
</ol>
</td>
</tr>
<tr>
<td>TTL</td>
<td>
<p>如果节点是alive的（wsrep_cluster_state_comment=Synced），每TTL - 2秒，report_status.sh会报告自身的状态</p>
<p>如果节点宕掉了，那么Etcd中的key无法刷新，因为过期而删除，这样构建Galera通信地址URI时就会跳过宕掉的节点</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">服务发现</span></div>
<p>如果Galera集群名字为my_wsrep_cluster，则此方案会在Etcd中存储以下数据：</p>
<pre class="crayon-plain-tag">// url -s "http://192.168.55.111:2379/v2/keys/galera/my_wsrep_cluster?recursive=true"
{
    "action": "get",
    "node": {
        "createdIndex": 10049,
        "dir": true,
        "key": "/galera/my_wsrep_cluster",
        "modifiedIndex": 10049,
        "nodes": [
            {
                "createdIndex": 10067,
                "dir": true,
                // 每个节点在 /galera/$CLUSTER_NAME/下有个键
                "key": "/galera/my_wsrep_cluster/10.255.0.6",
                "modifiedIndex": 10067,
                "nodes": [
                    {
                        "createdIndex": 10075,
                        "expiration": "2016-11-29T10:55:35.37622336Z",
                        // 存储节点seqno
                        "key": "/galera/my_wsrep_cluster/10.255.0.6/wsrep_last_committed",
                        "modifiedIndex": 10075,
                        "ttl": 10,
                        "value": "0"
                    },
                    {
                        "createdIndex": 10073,
                        "expiration": "2016-11-29T10:55:34.788170259Z",
                        // 存储节点状态
                        "key": "/galera/my_wsrep_cluster/10.255.0.6/wsrep_local_state_comment",
                        "modifiedIndex": 10073,
                        "ttl": 10,
                        "value": "Synced"
                    }
                ]
            }...
        ]
    }
}</pre>
<p>&nbsp;</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/galera-study-note">Galera学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/galera-study-note/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Prometheus学习笔记</title>
		<link>https://blog.gmem.cc/prometheus-study-note</link>
		<comments>https://blog.gmem.cc/prometheus-study-note#comments</comments>
		<pubDate>Fri, 27 Apr 2018 03:45:21 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Cloud]]></category>
		<category><![CDATA[Database]]></category>
		<category><![CDATA[K8S]]></category>
		<category><![CDATA[TSDB]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=20779</guid>
		<description><![CDATA[<p>简介 Prometheus是一个开源监控系统，它既适用于面向服务器等硬件指标的监控，也适用于高动态的面向服务架构的监控。对于现在流行的微服务，Prometheus的多维度数据收集和数据筛选查询语言也是非常的强大。 Prometheus的主要特性包括： 多维度数据模型 灵活的查询语言 不依赖分布式存储，单个服务器节点是自治的 通过服务（sd，准确的说是监控目标）发现或者静态配置，来发现目标服务对象 支持多种多样的图表和界面展示，可以和Grafana集成 数据采集方式： Pull：通过HTTP协议定期去采集指标，只要被监控系统提供HTTP接口即可接入 Push：被监控系统主动推送指标到网关，Prometheus定期从网关Pull Prometheus包括以下组件： Prometheus Server：负责抓取和存储时间序列数据 Push Gateway：推送网关，第三方可以推送数据到此网关，Prometheus Server再从此网关拉取数据 多种导出工具，支持导出Graphite、StatsD等所需的格式 命令行查询工具 Alert Manager：告警管理器 PromQL查询语言 <a class="read-more" href="https://blog.gmem.cc/prometheus-study-note">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/prometheus-study-note">Prometheus学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">简介</span></div>
<p>Prometheus是一个开源监控系统，它既适用于面向服务器等<span style="background-color: #c0c0c0;">硬件指标的监控</span>，也适用于高动态的<span style="background-color: #c0c0c0;">面向服务架构的监控</span>。对于现在流行的微服务，Prometheus的<span style="background-color: #c0c0c0;">多维度数据收集和数据筛选查询语言</span>也是非常的强大。</p>
<p>Prometheus的主要特性包括：</p>
<ol>
<li>多维度数据模型</li>
<li>灵活的查询语言</li>
<li>不依赖分布式存储，单个服务器节点是自治的</li>
<li>通过<span style="background-color: #c0c0c0;">服务（sd，准确的说是监控目标）发现</span>或者静态配置，来发现目标服务对象</li>
<li>支持多种多样的图表和界面展示，可以和Grafana集成</li>
<li>数据采集方式：
<ol>
<li>Pull：通过HTTP协议定期去采集指标，只要被监控系统提供HTTP接口即可接入</li>
<li>Push：被监控系统主动推送指标到网关，Prometheus定期从网关Pull</li>
</ol>
</li>
</ol>
<p>Prometheus包括以下组件：</p>
<ol>
<li>Prometheus Server：负责抓取和存储时间序列数据</li>
<li>Push Gateway：推送网关，第三方可以推送数据到此网关，Prometheus Server再从此网关拉取数据</li>
<li>多种导出工具，支持导出Graphite、StatsD等所需的格式</li>
<li>命令行查询工具</li>
<li>Alert Manager：告警管理器</li>
<li>PromQL查询语言</li>
</ol>
<div class="blog_h2"><span class="graybg">架构图</span></div>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2018/04/prometheus-architecture.png"><img class="aligncenter  wp-image-20855" src="https://blog.gmem.cc/wp-content/uploads/2018/04/prometheus-architecture.png" alt="prometheus-architecture" width="922" height="635" /></a></p>
<div class="blog_h2"><span class="graybg">数据模型</span></div>
<p>Prometheus中所有数据都存放为时间序列——具有时间戳的数据流，这些数据属于同一指标、以及由一系列标签定义的维度集。每个<span style="background-color: #c0c0c0;">时间序列由指标名+标签集唯一的识别</span>，标签由键、值组成。时间序列通常使用如下记号表示：</p>
<pre class="crayon-plain-tag">&lt;metric name&gt;{&lt;label name&gt;=&lt;label value&gt;, ...}
# 示例：
api_http_requests_total{method="POST", handler="/messages"}</pre>
<p>指标名是需要监控系统特性的一般性名称，例如http_requests_total可以表示HTTP请求计数。</p>
<p>标签为一个具体的指标“实例”提供维度信息。使用PromQL你可以基于标签进行过滤、分组。增加、修改、删除某个标签，则新的时间序列会被创建。标签名只能ASCII字符，但是标签值可以是任何Unicode字符。</p>
<p>样本（Sample）构成了实际的时间序列的数据点。样本由<span style="background-color: #c0c0c0;">float64类型的数值+毫秒精度的时间戳</span>组成。</p>
<div class="blog_h2"><span class="graybg">指标类型</span></div>
<p>Prometheus的客户端库区分了4种指标类型，目前服务器端不理解这些类型的不同，但是未来可能改变。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 120px; text-align: center;">指标类型</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>counter</td>
<td>单调递增的计数器</td>
</tr>
<tr>
<td>gauge</td>
<td>可以任意变化的数值</td>
</tr>
<tr>
<td>histogram</td>
<td>
<p>长尾问题：某个API的绝大部分请求延迟100ms，但是个别请求延迟高达5s。这种情况下使用指标平均值无法分析出问题所在</p>
<p>为了区分延迟是平均的、普遍的，还是由于长尾问题造成的。最简单的方式是统计延迟在0-50ms的请求有多少，50-100ms的请求有多少…… Prometheus的指标类型histogram、summary都可以用于这种<span style="background-color: #c0c0c0;">样本分布的分析</span></p>
<p>histogram对监控得到的数值（<span style="background-color: #c0c0c0;">例如请求用时、响应大小</span>）进行采样，并在可配置的Bucket（<span style="background-color: #c0c0c0;">例如请求用时区间、响应大小区间</span>）中对采样进行计数，同时提供对所有监控数值的求和</p>
<p>具有名称&lt;basename&gt;的histogram，暴露以下几个时间序列：</p>
<ol>
<li>&lt;basename&gt;_bucket{le="&lt;upper inclusive bound&gt;"}，基于观察桶（observation buckets）的累加计数器</li>
<li>&lt;basename&gt;_sum，所有观察值求和</li>
<li>&lt;basename&gt;_count，所有观察值计数</li>
</ol>
<p>下面是一个例子：</p>
<pre class="crayon-plain-tag"># prometheus_tsdb_compaction_chunk_range_count{instance="localhost:9090"}
# 样本总数
tsdb_compaction_chunk_range_count{}                   64388371

# prometheus_tsdb_compaction_chunk_range_sum{instance="localhost:9090"}
# 所有样本的值的总和
tsdb_compaction_chunk_range_sum{}                     456743796630059

# sort(prometheus_tsdb_compaction_chunk_range_bucket{instance="localhost:9090"})
# 值小于25600的样本数量
tsdb_compaction_chunk_range_bucket{le="25600"         1301
tsdb_compaction_chunk_range_bucket{le="400"}          1301
tsdb_compaction_chunk_range_bucket{le="100"}          1301
tsdb_compaction_chunk_range_bucket{le="1600"}         1301
tsdb_compaction_chunk_range_bucket{le="6400"}         1301
tsdb_compaction_chunk_range_bucket{le="102400"}       20000
tsdb_compaction_chunk_range_bucket{le="409600"}       108533
tsdb_compaction_chunk_range_bucket{le="1.6384e+06"}   254369
tsdb_compaction_chunk_range_bucket{le="6.5536e+06"}   655322
tsdb_compaction_chunk_range_bucket{le="2.62144e+07"}  64272199
# 所有样本数量
tsdb_compaction_chunk_range_bucket{le="+Inf"}         64272199</pre>
<p>从上面的例子中，很容易发现绝大部分的样本都落在1.6e+6到2.6e+7这个区间</p>
<p>对于Histogram的指标，我们还可以通过histogram_quantile()函数<span style="background-color: #c0c0c0;">在服务器端拟合</span>出其值的分位数：</p>
<pre class="crayon-plain-tag">// 估算99分位数
histogram_quantile(0.99, 
  // 以le分组求和
  sum(
    // 以interval为区间获得平均值
    irate(
      istio_request_duration_seconds_bucket{
        reporter="destination",
        destination_workload=~"$workload",
        destination_workload_namespace=~"$namespace"
      }[$interval]
    )
  ) by (le)
)</pre>
</td>
</tr>
<tr>
<td>summary</td>
<td>
<p>类似于histogram，基于滑动窗口来计算可配置的分位数（quantile）
<p>下面是一个例子：</p>
<pre class="crayon-plain-tag"># 样本总数
tsdb_wal_fsync_duration_seconds_count{job="prometheus"}                  369717
# 样本值总和
tsdb_wal_fsync_duration_seconds_sum{job="prometheus"}                   24423.798953890488
# 中位数耗时0.05秒，也就是说，有一半的磁盘同步操作在0.05秒内完成
tsdb_wal_fsync_duration_seconds{job="prometheus",quantile="0.5"}	0.053841949
# 九分位数耗时0.12秒，也就是说，90%的磁盘同步操作都在0.12秒内完成
tsdb_wal_fsync_duration_seconds{job="prometheus",quantile="0.9"}	0.116763012
tsdb_wal_fsync_duration_seconds{job="prometheus",quantile="0.99"}	0.263791273</pre>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">Job和实例</span></div>
<p>一个你可以从中<span style="background-color: #c0c0c0;">抓取监控数据的endpoint称为实例（Instance）</span>，实例通常对应一个进程，例如NodeExporter、RedisExporter……
<p><span style="background-color: #c0c0c0;">一系列相同目的的实例，称为Job</span>。多实例的原因可能是为了可靠性、扩容。</p>
<p>当Prometheus服务器抓取数据时，它会自动为时间序列添加标签：</p>
<ol>
<li>job，抓取目标所属的Job的名称</li>
<li>instance，目标从什么host:port抓取得到</li>
</ol>
<p>如果上述两个标签已经存在于抓取的数据上，则配置项honor_labels影响服务器的行为。</p>
<p>除了添加标签之外，还会为以下时间序列添加样本：</p>
<ol>
<li>up{job="&lt;job-name&gt;", instance="&lt;instance-id&gt;"}:1/0。如果实例可达则取值1，否则0</li>
<li>scrape_duration_seconds{job="&lt;job-name&gt;", instance="&lt;instance-id&gt;"}:抓取目标消耗的时间</li>
<li>scrape_samples_post_metric_relabeling{job="&lt;job-name&gt;", instance="&lt;instance-id&gt;"}:指标重打标签后，剩余的样本数量</li>
<li>scrape_samples_scraped{job="&lt;job-name&gt;", instance="&lt;instance-id&gt;"}: 目标暴露的样本数量</li>
</ol>
<div class="blog_h2"><span class="graybg">对比其它TSDB</span></div>
<div class="blog_h3"><span class="graybg">Graphite</span></div>
<p>Graphite专注于作为一个被动的时间序列数据库，同时提供查询语言、图形化特性。Prometheus则是一个全功能的监控和趋势分析系统，内置主动拉取、存储、图形化、报警功能。</p>
<p>Graphite存储数字采样，但是其元数据模型不如Prometheus丰富。Graphite的指标名称使用点号分隔的单词，暗含维度信息。Prometheus则将维度信息作为明确的标签存储。Prometheus更容易支持过滤、分组操作。</p>
<p>Graphite在本地磁盘上，以Whisper格式存储时间序列数据。每个时间序列存储一个文件，一段时间后，新采样会覆盖旧采样，此外采样频率是固定的。Prometheus也是每个时间序列对应一个文件，但是采样频率是动态的，新数据简单的Append到文件尾部。</p>
<div class="blog_h3"><span class="graybg">InfluxDB</span></div>
<p>InfluxDB的持续查询类似于Prometheus的Recording规则。Kapacitor类似于Recording规则、Alerting规则和Alertmanager的通知功能的组合。Alertmanager具有额外的分组、去重、静默功能。</p>
<p>InfluxDB的Tag和Prometheus的Label一样，都是键值对形式的维度信息。InfluxDB还提供第二级的“标签”——字段（Field）</p>
<div class="blog_h2"><span class="graybg">术语</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">名词</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>Alert</td>
<td>Prometheus中报警规则的输出，从Prometheus服务器发送给报警管理器</td>
</tr>
<tr>
<td>Alertmanager</td>
<td>接收Alert，聚合成组、去重、应用 silence、throttles，然后发送电子邮件或者发送到Slack、Pagerduty</td>
</tr>
<tr>
<td>Bridge</td>
<td>从客户端库提取采样，暴露给非Prometheus监控系统的组件</td>
</tr>
<tr>
<td>Collector</td>
<td>Exporter的一部分，可以收集单个或者多个指标</td>
</tr>
<tr>
<td>Exporter</td>
<td>收集指标的应用程序，将各种指标转化为Prometheus支持的数据处理格式</td>
</tr>
<tr>
<td>Notification</td>
<td>Altermanger发出的各种通知</td>
</tr>
<tr>
<td>PromQL</td>
<td>Prometheus查询语言。支持聚合、分片、切割、断言和连接操作</td>
</tr>
<tr>
<td>Silence</td>
<td>根据标签（Label）匹配来禁用警告</td>
</tr>
<tr>
<td>Target</td>
<td>需要抓取（Scrape）的对象的定义，包括以下信息：需要增加的标签、身份验证信息、如何抓取</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">安装</span></div>
<div class="blog_h2"><span class="graybg">K8S</span></div>
<pre class="crayon-plain-tag">helm install gmem/prometheus --name prometheus --namespace=kube-system</pre>
<p>此Chart的定义位于：<a href="https://github.com/gmemcc/charts/tree/master/prometheus">https://github.com/gmemcc/charts/tree/master/prometheus</a></p>
<p>安装完毕，到<a href="https://prometheus.k8s.gmem.cc/targets">https://prometheus.k8s.gmem.cc/targets</a>可以查看各监控目标的状态。</p>
<div class="blog_h2"><span class="graybg">Standalone</span></div>
<pre class="crayon-plain-tag">wget https://github.com/prometheus/prometheus/releases/download/v2.2.1/prometheus-2.2.1.linux-amd64.tar.gz
tar xzf prometheus-2.2.1.linux-amd64.tar.gz
rm prometheus-2.2.1.linux-amd64.tar.gz
mv prometheus-2.2.1.linux-amd64 prometheus
cd prometheus
./prometheus --config.file="prometheus.yml"       # 指定配置文件
             --web.listen-address="0.0.0.0:9090"  # UI/API/Telemetry监听地址
             --storage.tsdb.path="data/"          # 时间序列数据库存储路径
             --storage.tsdb.retention=15d         # 时间序列数据存储时长
             # 等待处理的的告警管理器通知队列长度
             --alertmanager.notification-queue-capacity=10000
             --alertmanager.timeout=10s           # 发送告警给告警管理器的超时</pre>
<div class="blog_h1"><span class="graybg">配置</span></div>
<p>Prometheus通过命令行、配置文件进行配置。命令行参数可以配置一些不变的系统参数，例如存储位置、存留在内存和磁盘中的数据量。配置文件则用于指定Job、Instance、Rule的配置。</p>
<div class="blog_h2"><span class="graybg">配置文件</span></div>
<p>配置文件的格式是YAML，使用--config.file指定配置文件的位置。本节列出重要的配置项。</p>
<div class="blog_h3"><span class="graybg">全局配置</span></div>
<pre class="crayon-plain-tag">global:
  # 默认抓取周期，可用单位ms、smhdwy
  [ scrape_interval: &lt;duration&gt; | default = 1m ]
  # 默认抓取超时
  [ scrape_timeout: &lt;duration&gt; | default = 10s ]
  # 估算规则的默认周期
  [ evaluation_interval: &lt;duration&gt; | default = 1m ]
  # 和外部系统（例如AlertManager）通信时为时间序列或者警情（Alert）强制添加的标签列表
  external_labels:
    [ &lt;labelname&gt;: &lt;labelvalue&gt; ... ]

# 规则文件列表
rule_files:
  [ - &lt;filepath_glob&gt; ... ]

# 抓取配置列表
scrape_configs:
  [ - &lt;scrape_config&gt; ... ]

# 和Alertmanager相关的配置
alerting:
  alert_relabel_configs:
    [ - &lt;relabel_config&gt; ... ]
  alertmanagers:
    [ - &lt;alertmanager_config&gt; ... ]

# 和远程读写特性相关的配置
remote_write:
  [ - &lt;remote_write&gt; ... ]
remote_read:
  [ - &lt;remote_read&gt; ... ]</pre>
<div class="blog_h3"><span class="graybg">scrape_config</span></div>
<p>配置一系列的目标，以及如何抓取它们的参数。一般情况下，每个scrape_config对应单个Job。</p>
<p>目标可以在scrape_config中静态的配置，也<span style="background-color: #c0c0c0;">可以使用某种服务发现机制动态发现</span>。</p>
<pre class="crayon-plain-tag"># 任务名称，自动作为抓取到的指标的一个标签
job_name: &lt;job_name&gt;

# 抓取周期
[ scrape_interval: &lt;duration&gt; | default = &lt;global_config.scrape_interval&gt; ]
# 每次抓取的超时
[ scrape_timeout: &lt;duration&gt; | default = &lt;global_config.scrape_timeout&gt; ]
# 从目标抓取指标的URL路径
[ metrics_path: &lt;path&gt; | default = /metrics ]
# 当添加标签发现指标已经有同名标签时，是否保留原有标签不覆盖
[ honor_labels: &lt;boolean&gt; | default = false ]
# 抓取协议
[ scheme: &lt;scheme&gt; | default = http ]
# 可选的请求参数
params:
  [ &lt;string&gt;: [&lt;string&gt;, ...] ]

# 身份验证信息
basic_auth:
  [ username: &lt;string&gt; ]
  [ password: &lt;secret&gt; ]
  [ password_file: &lt;string&gt; ]
# Authorization请求头取值
[ bearer_token: &lt;secret&gt; ]
# 从文件读取Authorization请求头
[ bearer_token_file: /path/to/bearer/token/file ]

# TLS配置
tls_config:
  [ &lt;tls_config&gt; ]

# 代理配置
[ proxy_url: &lt;string&gt; ]

# DNS服务发现配置
dns_sd_configs:
  [ - &lt;dns_sd_config&gt; ... ]
# 文件服务发现配置
file_sd_configs:
  [ - &lt;file_sd_config&gt; ... ]
# K8S服务发现配置
kubernetes_sd_configs:
  [ - &lt;kubernetes_sd_config&gt; ... ]

# 此Job的静态配置的目标列表
static_configs:
  [ - &lt;static_config&gt; ... ]

# 目标重打标签配置
relabel_configs:
  [ - &lt;relabel_config&gt; ... ]
# 指标重打标签配置
metric_relabel_configs:
  [ - &lt;relabel_config&gt; ... ]

# 每次抓取允许的最大样本数量，如果在指标重打标签后，样本数量仍然超过限制，则整个抓取认为失败
# 0表示不限制
[ sample_limit: &lt;int&gt; | default = 0 ]</pre>
<div class="blog_h3"><span class="graybg">kubernetes_sd_config</span></div>
<p>使用该配置，可以从K8S API Server暴露的REST API中<span style="background-color: #c0c0c0;">发现抓取目标</span>，并且和K8S集群保持同步。你可以配置以下role，以发现目标：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 100px; text-align: center;">role</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>node</td>
<td>
<p>为每个集群节点发现一个目标，目标的端口是Kubelet的HTTP端口、目标的地址是K8S节点对象的NodeInternalIP、NodeExternalIP、NodeLegacyHostIP或NodeHostName</p>
<p>可用的元标签：</p>
<p style="padding-left: 30px;">__meta_kubernetes_node_name 节点的名称<br />__meta_kubernetes_node_label_&lt;labelname&gt; 节点的每个标签<br />__meta_kubernetes_node_annotation_&lt;annotationname&gt; 节点的每个注解<br />__meta_kubernetes_node_address_&lt;address_type&gt; 节点的每种地址的第一个</p>
<p>节点的instance标签被设置为从API Server获取的节点名</p>
</td>
</tr>
<tr>
<td>service</td>
<td>
<p>为每个服务的端口发现一个目标，一般用于服务的黑盒监控。目标地址为服务的DNS名称</p>
<p>可用的元标签：</p>
<p style="padding-left: 30px;">__meta_kubernetes_namespace 服务所在命名空间<br />__meta_kubernetes_service_name 服务的名字<br />__meta_kubernetes_service_label_&lt;labelname&gt; 服务的每个标签<br />__meta_kubernetes_service_annotation_&lt;annotationname&gt; 服务的每个注解<br />__meta_kubernetes_service_port_name 服务端口的名称<br />__meta_kubernetes_service_port_numbe 服务的端口号<br />__meta_kubernetes_service_port_protocol 服务的协议</p>
</td>
</tr>
<tr>
<td>pod</td>
<td>
<p>发现所有Pod并将其容器暴露为目标。对于每个容器+声明端口的组合，生成独立的目标。<span style="background-color: #c0c0c0;">如果容器没有指定端口则仅仅为容器生成一个目标，在重打标签阶段可以为这种目标添加端口</span></p>
<p>可用的元标签：</p>
<p style="padding-left: 30px;">__meta_kubernetes_namespace Pod所在命名空间<br />__meta_kubernetes_pod_name Pod名称<br />__meta_kubernetes_pod_ip Pod地址<br />__meta_kubernetes_pod_label_&lt;labelname&gt; Pod的每个标签<br />__meta_kubernetes_pod_annotation_&lt;annotationname&gt; Pod的每个注解<br />__meta_kubernetes_pod_container_name 容器名<br />__meta_kubernetes_pod_container_port_name 容器端口名<br />__meta_kubernetes_pod_container_port_number 容器端口号<br />__meta_kubernetes_pod_container_port_protocol 容器端口协议<br />__meta_kubernetes_pod_ready 如果Pod就绪设置为true<br />__meta_kubernetes_pod_node_name 节点名<br />__meta_kubernetes_pod_host_ip Pod的宿主机IP<br />__meta_kubernetes_pod_uid Pod的UID<br />__meta_kubernetes_pod_controller_kind Pod控制器的对象类型<br />__meta_kubernetes_pod_controller_name Pod控制器的名称</p>
</td>
</tr>
<tr>
<td>endpoints</td>
<td>
<p>为服务的每个端点发现目标，每个Endpoint+Port的组合生成一个目标。如果Endpoint是基于Pod的，则Pod的任何端口都生成目标</p>
<p>可用的元标签：</p>
<p style="padding-left: 30px;">如果endpoint是基于Pod的，则role: pod发现的所有元标签可用<br />如果endpoint是基于Service的，则role: service发现的所有元标签可用</p>
<p style="padding-left: 30px;">__meta_kubernetes_namespace 端点的命名空间<br />__meta_kubernetes_endpoints_name 端点的名称</p>
<p style="padding-left: 30px;">对于Endpoint中定义的Pod端口，以下元标签可用：<br />__meta_kubernetes_endpoint_ready 端点是否就绪<br />__meta_kubernetes_endpoint_port_name 端点端口的名称<br />__meta_kubernetes_endpoint_port_protocol 端点端口的协议<br />__meta_kubernetes_endpoint_address_target_kind 端点地址目标类型<br />__meta_kubernetes_endpoint_address_target_name 端点地址目标名称</p>
</td>
</tr>
<tr>
<td>ingress</td>
<td>
<p>为所有Ingress的每个路径发现目标，一般用于Ingress的黑盒监控。目标地址设置为Ingress的host字段</p>
<p>可用的元标签：</p>
<p style="padding-left: 30px;">__meta_kubernetes_namespace 所属命名空间<br />__meta_kubernetes_ingress_name Ingress的名字<br />__meta_kubernetes_ingress_label_&lt;labelname&gt; 每个标签<br />__meta_kubernetes_ingress_annotation_&lt;annotationname&gt; 每个注解<br />__meta_kubernetes_ingress_scheme 协议，http/https<br />__meta_kubernetes_ingress_path Ingress的路径，默认/</p>
</td>
</tr>
</tbody>
</table>
<p>通常，你会给相关K8S资源添加以下注解：</p>
<pre class="crayon-plain-tag">annotations:                                                                                                                                                                           
  prometheus.io/path: /metrics                                                                                                                                                         
  prometheus.io/port: "8080"                                                                                                                                                           
  prometheus.io/scrape: "true"</pre>
<p>并配合以下Relabel配置，提示这些资源需要作为Prometheus的抓取目标：</p>
<pre class="crayon-plain-tag">relabel_configs:
  - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
    action: keep
    regex: true
  - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
    action: replace
    target_label: __metrics_path__
    regex: (.+)
  - source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port]
    action: replace
    regex: ([^:]+)(?::\d+)?;(\d+)
    replacement: $1:$2
    target_label: __address__ </pre>
<div class="blog_h3"><span class="graybg">relabel_config</span></div>
<p>重打标签是动态修改目标的标签集的强大工具。每个抓取配置可以定义<span style="background-color: #c0c0c0;">多个重打标签步骤，这些步骤按照声明顺序依次执行、在实际抓取指标数据之前执行</span>。</p>
<p>在一开始，除了为每个目标配置的标签之外，目标的：</p>
<ol>
<li>job标签被设置为抓取配置的job_name字段</li>
<li>__address__标签被设置为目标的&lt;host&gt;:&lt;port&gt;</li>
</ol>
<p>在重打标签之后，目标的：</p>
<ol>
<li>instance标签默认被设置为__address__，如果没有此标签的话</li>
<li>__scheme__标签被设置为http或https</li>
<li>__metrics_path__标签被设置为目标的指标路径，即URL路径</li>
<li>__param_&lt;name&gt;标签为请求时使用的每个参数</li>
</ol>
<p>在重打标签期间，额外的<span style="background-color: #c0c0c0;"> __meta_ 开头的元标签可用，这些标签由服务发现机制自动添加</span>。</p>
<p>在重打标签结束后，<span style="background-color: #c0c0c0;">以__开头的标签会被移除</span>。</p>
<p>如果某个步骤需要临时的设置一些标签，仅仅作为后续步骤的输入，<span style="background-color: #c0c0c0;">应当以__tmp作为前缀</span>。</p>
<p>每个重打标签步骤（relabel_configs的元素）具有以下子配置项：</p>
<pre class="crayon-plain-tag"># 从已有的标签中选取值
[ source_labels: '[' &lt;labelname&gt; [, ...] ']' ]
# 并且使用下面的分隔符连接那些值
[ separator: &lt;string&gt; | default = ; ]
# 然后基于下面的正则式进行匹配，或者保留，或者替换，或者删除

# 对于替换操作来说，替换为的目标标签的名字，可以使用正则式捕获组
[ target_label: &lt;labelname&gt; ]

# 用于匹配源标签值的正则式
[ regex: &lt;regex&gt; | default = (.*) ]

# 用于获取源标签值的哈希的模数
[ modulus: &lt;uint64&gt; ]

# 如果正则式匹配，使用什么替换值，可以使用正则式捕获组
[ replacement: &lt;string&gt; | default = $1 ]

# 如果正则式匹配，执行何种操作
# replace 如果正则式匹配source_labels的值，则设置target_label为指定的内容
# keep 如果正则式匹配，维持目标不变
# drop 如果正则式匹配，丢弃目标
# hashmod 设置target_label标签名为source_labels的值的哈希的取模
# labelmap 针对所有标签名来匹配regex，然后将匹配的标签的值拷贝到replacement所指定的新标签中
# labeldrop 针对所有标签来匹配regex，不匹配的标签都丢弃
# labelkeep 针对所有标签来匹配regex，匹配的标签都丢弃
[ action: &lt;relabel_action&gt; | default = replace ]</pre>
<p>重打标签配置示例：</p>
<pre class="crayon-plain-tag"># 将元标签__meta_kubernetes_pod_node_name替换为nodename
- source_labels: [__meta_kubernetes_pod_node_name]
  action: replace
  target_label: nodename </pre>
<div class="blog_h3"><span class="graybg">metric_relabel_configs</span></div>
<p>在存储（ingestion）样本数据之前，作为最后一个步骤，配置同上。可以用于屏蔽存储成本过高的时间序列。</p>
<div class="blog_h2"><span class="graybg">配置文件示例</span></div>
<div class="blog_h3"><span class="graybg">默认配置文件</span></div>
<p>Prometheus使用YAML格式的配置文件，默认的配置文件内容如下：</p>
<pre class="crayon-plain-tag"># Prometheus服务器的全局配置
global:
  # 拉取Target的间隔，默认1分钟
  scrape_interval:     15s 
  # 执行Rule的间隔，默认1分钟
  evaluation_interval: 15s 
  # 拉取Target的超时时间
  scrape_timeout: 10s      

# 报警管理器配置
alerting:
  alertmanagers:
  - static_configs:
    - targets:
      - alertmanager:9093

# 加载规则文件，并每evaluation_interval执行规则一次
rule_files:
  - "first_rules.yml"
  - "second_rules.yml"

# 拉取配置，说明Prometheus需要监控什么
scrape_configs:
  # 这个默认抓取任务，监控Prometheus服务器自己（Prometheus通过HTTP暴露了自己的Metrics）
  - job_name: 'prometheus'
    metrics_path: /metrics
    scheme: http
    static_configs:
      # 此Job仅具有一个目标
      - targets: ['localhost:9090']</pre>
<div class="blog_h1"><span class="graybg">PromQL</span></div>
<p>Prometheus提供了一种查询语言，用来实时的查询、聚合时间序列数据。查询结果可以在Prometheus的WebUI中展示，或者通过HTTP API暴露给第三方系统。</p>
<div class="blog_h2"><span class="graybg">语法示例</span></div>
<pre class="crayon-plain-tag"># 返回一个指标的所有时间序列
http_requests_total
# 返回一个指标具有特定Label的时间序列
http_requests_total{job="apiserver", handler="/api/comments"}
# 在满足上一条的前提下，返回5分钟内所有时间序列，并形成范围矢量（range vector）
http_requests_total{job="apiserver", handler="/api/comments"}[5m]
# 支持使用正则式来匹配Label
http_requests_total{job=~".*server"}
# 反向匹配
http_requests_total{status!~"4.."}

# 以最近5分钟的范围矢量为基础，统计每秒的数据增量
rate(http_requests_total[5m])

# 按Label job进行分组统计
sum(rate(http_requests_total[5m])) by (job)
# 如果两个指标具有相同的Label（维度信息），则可以对指标值进行运算
sum(
  instance_memory_limit_bytes - instance_memory_usage_bytes
) by (app, proc) / 1024 / 1024

# 取前三
topk(3, sum(rate(instance_cpu_time_ns[5m])) by (app, proc))</pre>
<div class="blog_h2"><span class="graybg">数据类型</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">数据类型</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>Instant vector</td>
<td>一系列时间序列，每个时间序列包含单个采样</td>
</tr>
<tr>
<td>Range vector</td>
<td>一系列时间序列，每个时间序列包含多个采样，采样分布在特定的时间区间</td>
</tr>
<tr>
<td>Scalar</td>
<td>单个浮点数</td>
</tr>
<tr>
<td>String</td>
<td>单个字符串</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">语法</span></div>
<div class="blog_h3"><span class="graybg">瞬时矢量选择器</span></div>
<p>匹配单个时间点的一个或多个时间序列的采样：</p>
<pre class="crayon-plain-tag"># 选择指标的所有时间序列
http_requests_total
# 根据标签匹配
http_requests_total{job="prometheus",group="canary"}</pre>
<p>匹配标签时可以使用四种操作符：=、!=、=~、!~，前两者用于精确匹配，后两者用于正则式匹配：</p>
<pre class="crayon-plain-tag">http_requests_total{environment=~"staging|testing|development",method!="GET"}</pre>
<div class="blog_h3"><span class="graybg">范围矢量选择器 </span></div>
<p>只需要在瞬时矢量选择器后面添加<pre class="crayon-plain-tag">[timePeriod]</pre> 即可，时间的单位可以是s、m、h、d、w、y。示例：</p>
<pre class="crayon-plain-tag">http_requests_total{job="prometheus"}[5m]</pre>
<div class="blog_h3"><span class="graybg">偏移量修饰符</span></div>
<pre class="crayon-plain-tag"># 获取相对当前查询时间，之前5分钟的数据
http_requests_total offset 5m

# 一周前的每秒请求数
rate(http_requests_total[5m] offset 1w)</pre>
<div class="blog_h3"><span class="graybg">操作符 </span></div>
<p>算术：加减乘除、取模、乘方（^）。可以在两个标量、标量vs瞬时矢量、 两个瞬时矢量之间进行。</p>
<p>比较：== != &gt; &gt;= &lt; &lt;=。可以在两个标量、标量vs瞬时矢量、 两个瞬时矢量之间进行。</p>
<p>逻辑：and or unless。仅仅用于两个瞬时矢量之间：</p>
<ol>
<li>and，取v1 v2中具有完全一致Label的那些时间序列，构成v3返回</li>
<li>or，取v1所有时间序列，外加v2中那些Label在v1中不存在的时间序列，构成v3返回</li>
<li>unless，取v1中那些没有在v2中具有相同Label的时间序列</li>
</ol>
<div class="blog_h3"><span class="graybg">矢量匹配</span></div>
<p>对两个瞬时矢量应用操作符时，牵涉到如何找到左侧矢量元素在右侧矢量中的匹配元素的问题。匹配都是基于Label的。</p>
<p>矢量匹配的行为有两种：1对1匹配，1对多匹配：</p>
<table class="full-width fixed-word-wrap">
<tbody>
<tr>
<td style="width: 100px; text-align: center;"><strong>一对一</strong></td>
<td>
<p>语法格式：<pre class="crayon-plain-tag">vector1 &lt;operator&gt; vector2</pre></p>
<p>默认情况下，如果两个元素的标签集完全一致则匹配，可以使用ignoring或者on关键字来限制哪些标签需要匹配：</p>
<pre class="crayon-plain-tag"># 匹配时忽略一些标签
&lt;vector expr&gt; &lt;bin-op&gt; ignoring(&lt;label list&gt;) &lt;vector expr&gt;
# 仅仅针对某些标签进行匹配
&lt;vector expr&gt; &lt;bin-op&gt; on(&lt;label list&gt;) &lt;vector expr&gt;</pre>
<p>示例：</p>
<pre class="crayon-plain-tag"># 500错误占比分析
method_code:http_errors:rate5m{code="500"} / ignoring(code) method:http_requests:rate5m</pre>
</td>
</tr>
<tr>
<td style="text-align: center;"><strong>一对多</strong></td>
<td> </td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">聚合操作符</span></div>
<p>操作符列表：
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">操作符</td>
<td style="text-align: center;">说明</td>
<td style="text-align: center;">操作符</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>min</td>
<td>最小值</td>
<td>max</td>
<td>最大值</td>
</tr>
<tr>
<td>sum</td>
<td>求和</td>
<td>avg</td>
<td>求平均</td>
</tr>
<tr>
<td>count</td>
<td>统计数量</td>
<td>count_values</td>
<td>统计同值元素量</td>
</tr>
<tr>
<td>bottomk</td>
<td>最小N元素</td>
<td>topk</td>
<td>最大N元素</td>
</tr>
<tr>
<td>quantile</td>
<td>分位数（例如求中位数）</td>
<td> </td>
<td> </td>
</tr>
</tbody>
</table>
<p>语法：</p>
<pre class="crayon-plain-tag">&lt;aggr-op&gt;([parameter,] &lt;vector expression&gt;) [without|by (&lt;label list&gt;)]
# parameter：仅count_values, quantile, topk, bottomk需要
# without：从结果矢量中移除指定的标签
# by：仅仅保留指定的标签

# 示例：
sum(http_requests_total) without (instance)
sum(http_requests_total) by (application, group)
count_values("version", build_version)
topk(5, http_requests_total)</pre>
<div class="blog_h2"><span class="graybg">函数</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">函数</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>abs(iv)</td>
<td>将输入瞬时矢量的元素的采样值取绝对值</td>
</tr>
<tr>
<td>absent(iv)</td>
<td>如果输入矢量没有元素，返回空矢量，否则返回包含单个元素，采样值为1的矢量</td>
</tr>
<tr>
<td>ceil(iv)<br />round(v)</td>
<td>取整</td>
</tr>
<tr>
<td>changes(rv)</td>
<td>对于范围矢量中每个时间序列，获取其值的变更次数，返回瞬时矢量</td>
</tr>
<tr>
<td>day_of_month()<br />day_of_week()<br />days_in_month()<br />hour()<br />minute()<br />month()<br />year()</td>
<td>时间转换</td>
</tr>
<tr>
<td>delta(rv)</td>
<td>对于范围矢量中每个时间序列，获取首尾两个采样的差值</td>
</tr>
<tr>
<td>idelta(rv)</td>
<td>对于范围矢量中每个时间序列，获取最后两个采样的差值</td>
</tr>
<tr>
<td>rate(rv)</td>
<td>对于范围矢量中每个时间序列，依据首尾两个采样的差值，计算每秒平均增量</td>
</tr>
<tr>
<td>irate(rv)</td>
<td>对于范围矢量中每个时间序列，依据最后两个采样的差值，计算每秒平均增量</td>
</tr>
<tr>
<td>resets(rv)</td>
<td>返回counter重置次数，只要值变小就认为被重置</td>
</tr>
<tr>
<td>sort(iv)<br />sort_desc(iv)</td>
<td>对矢量进行排序</td>
</tr>
<tr>
<td>AGG_over_time()</td>
<td>对范围矢量中每个时间序列进行基于时间的聚合操作，AGG可以是avg、min、max、sum、count等</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">注意点</span></div>
<div class="blog_h3"><span class="graybg">先rate再sum</span></div>
<p>不能对已经聚合过的数据再进行rate，只能对原始counter进行rate。正确的示例：</p>
<pre class="crayon-plain-tag"># 对平均值，按service进行分组
    # 对每个指标求每分钟平均值
sum(rate(dubbo_consumer_elapsed_ms{kubernetes_namespace="$namespace"}[2m]) * 60) by (service)    /
# 分组之后的数据仍然可以进行算术运算，按service值分别进行运算
sum(rate(dubbo_consumer_success_count{kubernetes_namespace="$namespace"}[2m]) * 60) by (service)</pre>
<div class="blog_h1"><span class="graybg">集成K8S</span></div>
<div class="blog_h2"><span class="graybg">安装</span></div>
<p>这里使用Helm Chart方式安装，Chart的源码位于：</p>
<p style="padding-left: 30px;"><a href="https://git.gmem.cc/alex/helm-charts/src/branch/master/prometheus">https://git.gmem.cc/alex/helm-charts/src/branch/master/prometheus</a>。</p>
<p>安装脚本如下：</p>
<pre class="crayon-plain-tag">rm -rf prometheus
helm fetch gmem/prometheus --untar
helm install prometheus --name=prometheus --namespace=kube-system  -f prometheus/overrides/gmem.yaml</pre>
<div class="blog_h2"><span class="graybg">说明</span></div>
<p>上面的Chart默认配置了以下Job：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 35%; text-align: center;">Job</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>kubernetes-apiservers</td>
<td>通过API Server采集指标，例如API的用量</td>
</tr>
<tr>
<td>kubernetes-nodes</td>
<td>采集节点的监控指标</td>
</tr>
<tr>
<td>kubernetes-nodes-cadvisor</td>
<td>采集容器的监控指标</td>
</tr>
<tr>
<td>kubernetes-service-endpoints</td>
<td>监控K8S的服务端点</td>
</tr>
<tr>
<td>kubernetes-services</td>
<td>监控K8S的服务</td>
</tr>
<tr>
<td>kubernetes-pods</td>
<td>监控K8S的Pod</td>
</tr>
<tr>
<td>prometheus-pushgateway</td>
<td>从推送网关拉取指标</td>
</tr>
<tr>
<td>prometheus</td>
<td>自我监控</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">指标采集</span></div>
<div class="blog_h2"><span class="graybg">自我监控</span></div>
<p>启动Prometheus后，访问http://localhost:9090/metrics可以查看Prometheus本身的指标信息：</p>
<pre class="crayon-plain-tag"># curl http://localhost:9090/metrics
# 输出示意片断
# HELP http_requests_total Total number of HTTP requests made.
# TYPE http_requests_total counter
# 指标名             {标签=值,标签=值...}                       指标值
http_requests_total{code="200",handler="graph",method="get"} 3
http_requests_total{code="200",handler="label_values",method="get"} 5
http_requests_total{code="200",handler="prometheus",method="get"} 4105
http_requests_total{code="200",handler="query",method="get"} 13
http_requests_total{code="200",handler="query_range",method="get"} 3
http_requests_total{code="200",handler="static",method="get"} 26
http_requests_total{code="304",handler="static",method="get"} 20
# 在http://localhost:9090/graph的控制台中输入http_requests_total进行查询，也可以得到上面的输出</pre>
<p>http_requests_total是Prometheus暴露（Export）的关于自身的指标 —— 接受的HTTP请求总数，包括了多个时间序列数据。这些时间序列数据的指标名都一样，但是具有不同的标签，这些标签用于区分不同类型的HTTP请求。</p>
<p>我们可以在控制台输入<pre class="crayon-plain-tag">http_requests_total{code="200"}</pre>，表示仅仅查询具有标签code=200的HTTP请求数。</p>
<p>类似的，还可以使用表达式<pre class="crayon-plain-tag">count(http_requests_total)</pre>，来统计指标http_requests_total具有的时间序列数据的数量。</p>
<p>在http://localhost:9090/graph页面，点击Graph选项卡，可以生成图表。例如表达式<pre class="crayon-plain-tag">rate(http_requests_total[1m])</pre> 表示生成最近一分钟的HTTP请求总数的图表，每个时间序列产生一条曲线。</p>
<div class="blog_h2"><span class="graybg">Node Exporter</span></div>
<p>Node Exporter是Prometheus提供的，用于监控Linux系统的组件。对于Windows，则有功能类似的<a href="https://github.com/martinlindhe/wmi_exporter">WMIExporter</a>。</p>
<p>下载NodeExporter后运行，它默认会在9100端口上暴露本机的各项指标。你可以访问http://localhost:9100/metrics来查看。修改Prometheus默认配置文件尾部的9090端口为9100即可采集这些指标。</p>
<div class="blog_h2"><span class="graybg">cAdvisor</span></div>
<p><a href="https://github.com/google/cadvisor">这个项目</a>是Google开源的，专门采集容器资源用量、性能指标。cAdvisor嵌入在kubelet中运行。</p>
<div class="blog_h2"><span class="graybg">JMX Exporter</span></div>
<p><a href="https://github.com/prometheus/jmx_exporter">此项目</a>可以暴露JMX管理Beans给Prometheus采集。它作为Java Agent运行，开启一个HTTP端口，对外提供本地JVM的各项指标。</p>
<p>为JVM添加参数：<pre class="crayon-plain-tag">-javaagent:jmx_prometheus_javaagent-0.3.1.jar=9100:config.yaml</pre>即可运行此Exporter。其中9100为暴露的端口号，config.yaml为配置文件路径。要采集指标，访问http://host:9100/metrics即可。</p>
<div class="blog_h3"><span class="graybg">配置文件</span></div>
<p>格式为YAML：</p>
<pre class="crayon-plain-tag"># 延迟于JVM启动HTTP端口的时间
startDelaySeconds: 0
# 如果连接到远程JVM，采集JMX信息，则要么指定hostPort，要么指定jmxUrl
hostPort: 127.0.0.1:1234
jmxUrl: service:jmx:rmi:///jndi/rmi://127.0.0.1:1234/jmxrmi
# 远程JMX身份验证信息
username: 
password: 
# 远程JMX是否通过SSL连接
ssl: false
# 是否小写化指标名、标签名
lowercaseOutputName: false
lowercaseOutputLabelNames: false
# 是否采集的ObjectNames的白、黑名单。默认采集所有mBeans
whitelistObjectNames: ["org.apache.cassandra.metrics:*"]
blacklistObjectNames: ["org.apache.cassandra.metrics:type=ColumnFamily,*"]
# 规则列表，从上往下执行，遇到匹配的规则则终止
# 不匹配的Attributes不被采集，默认情况下，以默认格式采集所有
rules:
    # 匹配mBean Attributes的正则式
  - pattern: 'org.apache.cassandra.metrics&lt;type=(\w+), name=(\w+)&gt;&lt;&gt;Value: (\d+)'
    # 指标名
    name: cassandra_$1_$2
    # 指标值
    value: $3
    # 指标值需要乘以的系数
    valueFactor: 0.001
    # 标签，可以使用捕获组
    labels: {}
    # 指标帮助信息
    help: "Cassandra metric $1 $2"
    # 指标类型
    type: GAUGE
    # 转换为下划线 + 小写风格
    attrNameSnakeCase: false</pre>
<p>如果没有任何配置内容，则以默认格式采集本地JVM的所有指标。</p>
<p>官方提供了<a href="https://github.com/prometheus/jmx_exporter/tree/master/example_configs">若干中间件的示例配置文件</a>。 </p>
<div class="blog_h3"><span class="graybg">输入pattern</span></div>
<p>传递给配置文件rules.pattern的格式为：</p>
<pre class="crayon-plain-tag"># domain mBean的名称，JMX ObjectNames的冒号前的部分
# beanProperyName/Value mBean属性列表，JMX ObjectNames的冒号后的部分
# keyN 如果是组合或表格数据，则包含键列表
# attrName 属性的名称，如果是表格数据，则为列名称。如果设置了attrNameSnakeCase则转换为下划线小写
# value 属性值
domain&lt;beanpropertyName1=beanPropertyValue1, beanpropertyName2=beanPropertyValue2, ...&gt;&lt;key1, key2, ...&gt;attrName: value </pre>
<p>不经配置，输出的默认指标格式为：</p>
<pre class="crayon-plain-tag">domain_beanPropertyValue1_key1_key2_...keyN_attrName{beanpropertyName2="beanPropertyValue2", ...}: value </pre>
<div class="blog_h1"><span class="graybg">客户端编程</span></div>
<p>Prometheus提供了主流语言的客户端库。要使用Prometheus的Go客户端库，导入：</p>
<pre class="crayon-plain-tag">import "github.com/prometheus/client_golang/prometheus"</pre>
<div class="blog_h2"><span class="graybg">核心类型</span></div>
<div class="blog_h3"><span class="graybg">Desc</span></div>
<p>该结构是任何Prometheus指标都需要使用的描述符，它本质上是<span style="background-color: #c0c0c0;">指标（Metrics）的不可变元数据</span>：</p>
<pre class="crayon-plain-tag">struct {
	// 全限定名称，由命名空间 - 子系统 - 名称组成
	fqName string
	// 指标的帮助信息
	help string
	// 常量标签键值
	constLabelPairs []*dto.LabelPair
	// 可变标签的名字
	variableLabels []string
	// 基于ConstLabels和fqName生成的哈希，所有注册的Desc都必须具有独特的值
        // 以作为Desc的唯一标识
	id uint64
	// 维度哈希，所有常量/可变标签的哈希，所有具有相同fqName的Desc必须具有相同的dimHash
	// 这意味着每个fqName对应的标签集是固定的
	dimHash uint64
	// 构造时出现的错误，注册时报告
	err error
}</pre>
<div class="blog_h3"><span class="graybg">Metric</span></div>
<p>所有指标的通用接口，表示需要导出到Prometheus的单个采样值+关联的元数据集 。此接口的实现包括Gauge、Counter、Histogram、Summary、Untyped。</p>
<pre class="crayon-plain-tag">type Metric interface {
	// 幂等的返回该指标的、不可变的描述符
	// 不能描述子集的指标，必须返回一个无效的描述符。无效描述符通过NewInvalidDesc创建
	Desc() *Desc
	// 将指标对象编码为ProtoBuffer数据传输对象
	// 指标实现必须考虑并发安全性，因为对指标的读可能随时发生，任何阻塞操作都会影响
	// 所有已经注册的指标的整体渲染性能
	// 理想的实现应该支持并发读
	//
	// 除了产生dto.Metric，实现还负责确保Metric的ProtoBuf合法性验证
	// 建议使用字典序排序标签，LabelPairSorter可能对指标实现者有帮助
	Write(*dto.Metric) error
}</pre>
<div class="blog_h3"><span class="graybg">MetricVec</span></div>
<p>此结构表示用于bundle全限定名称、标签值有所不同的指标。通常不会直接使用此结构，而是将它作为具体指标向量GaugeVec, CounterVec, SummaryVec,  UntypedVec的一部分：</p>
<pre class="crayon-plain-tag">type MetricVec struct {
	mtx      sync.RWMutex // 保护元素的锁
	children map[uint64][]metricWithLabelValues // 所有指标实例（值+标签集）
	desc     *Desc // 描述符

	newMetric   func(labelValues ...string) Metric  // 以指定的标签值创建新指标
	hashAdd     func(h uint64, s string) uint64
	hashAddByte func(h uint64, b byte) uint64
} </pre>
<div class="blog_h3"><span class="graybg">Opts</span></div>
<p>创建大部分的指标类型时，可以通过该接口提供选项：</p>
<pre class="crayon-plain-tag">type Opts struct {
	// Namespace, Subsystem, Name是指标的全限定名称的组成部分，这些部分使用下划线连接
	// 仅仅Name是必须的
	Namespace string
	Subsystem string
	Name      string

	// 帮助信息，单个全限定名称，其帮助信息必须一样
	Help string

	// 常量标签用于为指标提供固定的标签，单个全限定名称，其常量标签集的所包含的标签名必须一致
	// 注意在大部分情况下，标签的值会变化，这些标签通常由指标矢量收集器（metric vector collector）来
	// 处理，例如CounterVec、GaugeVec、UntypedVec，而ConstLabels则仅用于特殊情况，例如：
	// vector collector (like CounterVec, GaugeVec, UntypedVec). ConstLabels
	// 1、在整个处理过程中，标签的值绝不会改变。这种标签例如运行中的二进制程序的修订版号
	// 2、在具有多个收集器（collector）来收集相同全限定名称的指标的情况下，那么每个收集器收集的
	//    指标的常量标签的值必须有所不同
	// 如果任何情况下，标签的值都不会改变，它可能更适合编码到全限定名称中
	ConstLabels Labels
}</pre>
<div class="blog_h3"><span class="graybg">Collector</span></div>
<p>任何Prometheus用来收集指标的对象，都需要实现此接口：</p>
<pre class="crayon-plain-tag">type Collector interface {
	// 将此收集器收集的指标的所有可能的描述符发送到参数提供的通道。并且在最后一个描述符
	// 发送成功后返回。发送的描述符必须满足Desc文档声明的一致性、唯一性要求
	// 
	// 同一收集器发送重复的描述符是允许的，重复自动忽视
	// 但是两个收集器不得发送重复的描述符
	//
	// 如果不发送任何描述符，则收集器标记为unchecked状态，也就是说在注册时，不会进行任何检查
	// 收集器以后可能产生任何匹配它的Collect方法签名的指标
	//
	// 该方法在收集器的生命周期里，幂等的发送相同的描述符
	// 
	// 该方法可能被并发的调用，实现时需要注意线程安全问题
	//
	// 如果在执行该方法的过程中收集器遇到错误，务必发送一个无效的描述符（NewInvalidDesc）来提示注册表
	Describe(chan&lt;- *Desc)
	// 在收集指标时，该方法被Prometheus注册表（Registry）调用。方法的实现必须将所有它收集到的指标
	// 经由参数提供的通道发送，并且在最后一个指标发送后返回
	// 
	// 每个发送的指标的描述符，必须是Describe方法提供的之一（除非收集器是Unchecked）
	// 发送的共享相同描述符的指标，其标签集必须有所不同
	// 
	//
	// 该方法可能被并发的调用，实现时需要注意线程安全问题
	// 
	// 阻塞会导致影响所有已注册的指标的渲染性能，理想情况下，实现应该支持并发读
	Collect(chan&lt;- Metric)
} </pre>
<p>收集器必须被注册（Registerer.Register）才能收集指标值。</p>
<p>内置的指标类型实现了此接口，包括GaugeVec、CounterVec、HistogramVec、SummaryVec。</p>
<div class="blog_h3"><span class="graybg">Registry</span></div>
<p>注册表，此结构实现了Registerer、Gatherer接口。</p>
<p>Registerer接口为注册表提供注册/反注册功能：</p>
<pre class="crayon-plain-tag">type Registerer interface {
	// 注册一个需要包含在指标集中的收集器。如果收集器提供的描述符非法、
	// 或者不满足metric.Desc的一致性/唯一性需求，则返回错误
	//
	// 如果相等的收集器已经注册过，返回AlreadyRegisteredError，其中包含先前注册的收集器的实例
	//
	// 其Describe方法不产生任何Desc的收集器，视为Unchecked，对这种收集器的注册总是成功
	// 重现注册它时也不会有检查。因此，调用者必须负责确保不会重复注册
	Register(Collector) error
	// 注册多个收集器，并且在遇到第一个失败时就Panic
	MustRegister(...Collector)
	// 反注册
	Unregister(Collector) bool
}</pre>
<p>Gatherer为注册表提供汇集（gathering）功能 —— 将已经收集的指标汇集到若干指标族（MetricFamily）中：</p>
<pre class="crayon-plain-tag">type Gatherer interface {
	// 该方法调用所有已经注册的收集器的Collect方法，然后将获得的指标存放到一个字典序排列
	// 的MetricFamily的切片中。该方法保证返回的切片是有效的、自我一致的，可以用于对外
	// 暴露（给Prometheus服务器）该方法容忍相同指标族中具有不同标签集的指标
	//
	// 即时发生错误，该方法也会尝试尽可能收集更多的指标。因此，当该方法返回非空error时
	// 同时返回的dto.MetricFamily切片可能是nil（意味着致命错误）或者包含一定数量的
	// MetricFamily —— 切片可能是不完整的
	Gather() ([]*dto.MetricFamily, error)
} </pre>
<div class="blog_h3"><span class="graybg">Gauge</span></div>
<p>表示gauge类型的指标： </p>
<pre class="crayon-plain-tag">type Gauge interface {
	// 实现的接口
	Metric
	Collector

	// 设值
	Set(float64)
	// 增1
	Inc()
	// 减1
	Dec()
	// 加上一个值
	Add(float64)
	// 减去一个值
	Sub(float64)
}</pre>
<p>要创建一个Gauge，可以调用：</p>
<pre class="crayon-plain-tag">func NewGauge(opts GaugeOpts) Gauge {
	// func newValue(desc *Desc, valueType ValueType, val float64, labelValues ...string) *value
	return newValue(NewDesc(
		BuildFQName(opts.Namespace, opts.Subsystem, opts.Name),
		opts.Help,
		nil,
		opts.ConstLabels,
	), GaugeValue, 0)
}</pre>
<p>GaugeVec表示Gauge的向量：</p>
<pre class="crayon-plain-tag">type GaugeVec struct {
	*MetricVec
}</pre>
<div class="blog_h3"><span class="graybg">Histogram</span></div>
<p>表示histogram类型的指标。</p>
<p>Histogram对可配置的Bucket（观察值的区间）中的事件或样本流，基于Bucket进行独立计数观察。它支持对观察值（observations）进行计数，或者求和。</p>
<p>在Prometheus中，分位数可以基于Histogram，通过函数histogram_quantile计算得到。Histogram依赖于用户定义的、适当的buckets，一般来说精确度相对较低，但是和Summary比起来，Histogram的性能成本较低。</p>
<pre class="crayon-plain-tag">type Histogram interface {
	Metric
	Collector

	// 添加一个观察值
	Observe(float64)
}</pre>
<p>默认的Buckets如下，主要用于测量网络响应延迟的场景：</p>
<pre class="crayon-plain-tag">DefBuckets = []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10}</pre>
<p>对于其它场景，你应当自己定义Buckets。 </p>
<div class="blog_h2"><span class="graybg">创建指标</span></div>
<p>我们不会直接操控指标，而是使用它们的向量 —— GaugeVec、CounterVec、HistogramVec、SummaryVec。这些向量都是Collector接口的实现。</p>
<p>要创建向量，需要调用prometheus.New***Vec方法，例如：</p>
<pre class="crayon-plain-tag">func NewGaugeVec(opts GaugeOpts, labelNames []string) *GaugeVec {
// 描述符，关键信息是全限定名 + 标签集
	desc := NewDesc(
		BuildFQName(opts.Namespace, opts.Subsystem, opts.Name),
		opts.Help,
		labelNames,
		opts.ConstLabels,
	)
	return &amp;GaugeVec{
		metricVec: newMetricVec(desc, func(lvs ...string) Metric {
// 第二个参数是回调，标签集创建一个Metric —— 指标，准确的说是时间序列
			if len(lvs) != len(desc.variableLabels) {
				panic(makeInconsistentCardinalityError(desc.fqName, desc.variableLabels, lvs))
			}
			result := &amp;gauge{desc: desc, labelPairs: makeLabelPairs(desc, lvs)}
			result.init(result) // Init self-collection.
			return result
		}),
	}
} </pre>
<div class="blog_h3"><span class="graybg">Gauge</span></div>
<p>下面的例子创建一个Gauge向量：</p>
<pre class="crayon-plain-tag">weight := prometheus.NewGaugeVec(prometheus.GaugeOpts{
	Subsystem: "flagger",
	Name:      "canary_weight",
	Help:      "The virtual service destination weight current value",
}, []string{"workload", "namespace"})  // 注意标签顺序</pre>
<p>获取具有指定标签值的Gauge（时间序列）： </p>
<pre class="crayon-plain-tag">// 注意标签值的顺序，和上面对应
gauge := cr.weight.WithLabelValues(cd.Spec.TargetRef.Name, cd.Namespace)</pre>
<p>产生一个指标值：</p>
<pre class="crayon-plain-tag">gauge.Set(float64(canary))</pre>
<div class="blog_h3"><span class="graybg">Histogram</span></div>
<p>下面的例子使用默认Bucket创建Histogram向量：</p>
<pre class="crayon-plain-tag">duration := prometheus.NewHistogramVec(prometheus.HistogramOpts{
	Subsystem: controllerAgentName,
	Name:      "canary_duration_seconds",
	Help:      "Seconds spent performing canary analysis.",
	Buckets:   prometheus.DefBuckets,
}, []string{"name", "namespace"})</pre>
<p>产生一个指标值：</p>
<pre class="crayon-plain-tag">cr.duration.WithLabelValues(cd.Spec.TargetRef.Name, cd.Namespace).Observe(duration.Seconds())</pre>
<div class="blog_h2"><span class="graybg">注册指标</span></div>
<div class="blog_h3"><span class="graybg">默认注册表</span></div>
<p>Prometheus客户端提供了开箱即用的默认注册表：<pre class="crayon-plain-tag">prometheus.DefaultRegisterer</pre></p>
<div class="blog_h3"><span class="graybg">创建注册表</span></div>
<p>你可以调用以下函数来创建注册表：</p>
<pre class="crayon-plain-tag">// 不预先注册任何收集器的注册表
func NewRegistry() *Registry {
	return &amp;Registry{
		collectorsByID:  map[uint64]Collector{},
		descIDs:         map[uint64]struct{}{},
		dimHashesByName: map[string]uint64{},
	}
}

// 创建一个严格的注册表。该注册表在收集期间，检查
// 1、每个指标是否和它的Desc一致
// 2、指标的Desc是否已经注册到注册表
// Unchecked的收集器不被检查
func NewPedanticRegistry() *Registry {
	r := NewRegistry()
	r.pedanticChecksEnabled = true
	return r
}</pre>
<p>示例：</p>
<pre class="crayon-plain-tag">registry = prometheus.NewPedanticRegistry()</pre>
<div class="blog_h3"><span class="graybg">注册收集器 </span></div>
<pre class="crayon-plain-tag">// 进行注册
if err := registry.Register(gauge); err != nil {
// 如果已经注册
	if are, ok := err.(prometheus.AlreadyRegisteredError); ok {
// 返回先前注册的收集器
		return are.ExistingCollector, nil
	}
	return nil, err
} </pre>
<div class="blog_h2"><span class="graybg">暴露指标</span></div>
<p>Prometheus客户端不能直接将指标发送给Prometheus服务器。只能通过网络端口暴露一个Exporter。</p>
<p>如果要基于HTTP协议暴露，将promhttp包提供的Handler传递给你的HTTP服务器，<span style="background-color: #c0c0c0;">每个Handler读取单个注册表，从中收集指标信息，生成HTTP响应</span>：</p>
<pre class="crayon-plain-tag">// 使用默认注册表创建Handler
func Handler() http.Handler {
	return InstrumentMetricHandler(
		prometheus.DefaultRegisterer, HandlerFor(prometheus.DefaultGatherer, HandlerOpts{}),
	)
}</pre>
<p>如果你需要使用非默认注册表，直接调用：</p>
<pre class="crayon-plain-tag">func HandlerFor(reg prometheus.Gatherer, opts HandlerOpts) http.Handler {}</pre>
<p> 然后将Handler传递给你的ServeMux即可：</p>
<pre class="crayon-plain-tag">mux := http.NewServeMux()
import "github.com/prometheus/client_golang/prometheus/promhttp"
mux.Handle("/metrics", promhttp.Handler())</pre>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/prometheus-study-note">Prometheus学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/prometheus-study-note/feed</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>InfluxDB学习笔记</title>
		<link>https://blog.gmem.cc/influxdb-study-note</link>
		<comments>https://blog.gmem.cc/influxdb-study-note#comments</comments>
		<pubDate>Fri, 23 Mar 2018 02:05:14 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Database]]></category>
		<category><![CDATA[TSDB]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=19921</guid>
		<description><![CDATA[<p>简介 InfluxDB是一个用于存储和分析时间序列数据的开源数据库。它的特性包括： 支持HTTP接口 支持类SQL的查询 —— InfluxQL 高效的数据读写 InfluxDB默认使用两个端口：8086用于HTTP接口，8088用于备份/恢复数据的RPC服务。 安装 Ubuntu [crayon-69e0b6309b70f111234650/] K8S [crayon-69e0b6309b715603013757/] 执行下面命令进入CLI： [crayon-69e0b6309b719680097698/] 入门 CLI 创建数据库 命令格式类似于MySQL： [crayon-69e0b6309b71c043170804/] 读写数据 InfluxDB中存放的是时间序列数据，包括0-N个数据点。数据点由以下部分组成： time，时间戳 <a class="read-more" href="https://blog.gmem.cc/influxdb-study-note">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/influxdb-study-note">InfluxDB学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">简介</span></div>
<p>InfluxDB是一个用于存储和分析时间序列数据的开源数据库。它的特性包括：</p>
<ol>
<li>支持HTTP接口</li>
<li>支持类SQL的查询 —— InfluxQL</li>
<li>高效的数据读写</li>
</ol>
<p>InfluxDB默认使用两个端口：8086用于HTTP接口，8088用于备份/恢复数据的RPC服务。</p>
<div class="blog_h1"><span class="graybg">安装</span></div>
<div class="blog_h2"><span class="graybg">Ubuntu</span></div>
<pre class="crayon-plain-tag">curl -sL https://repos.influxdata.com/influxdb.key | sudo apt-key add -
source /etc/lsb-release
echo "deb https://repos.influxdata.com/${DISTRIB_ID,,} ${DISTRIB_CODENAME} stable" | sudo tee /etc/apt/sources.list.d/influxdb.list

sudo apt-get update
sudo apt-get install influxdb
sudo service influxdb start</pre>
<div class="blog_h2"><span class="graybg">K8S</span></div>
<pre class="crayon-plain-tag">apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: influxdb-pvc
  namespace: dev
spec:
  storageClassName: rook-block
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi

---

apiVersion: v1
kind: Pod
metadata:
  namespace: dev
  name: influxdb
  labels:
    app: influxdb
  annotations:
    "cni.projectcalico.org/ipAddrs": "[\"172.27.0.20\"]"
spec:
  terminationGracePeriodSeconds: 10
  containers:
  - name: influxdb
    image: docker.gmem.cc/influxdb
    ports:
    - containerPort: 8086
      name: http
    - containerPort: 8082
      name: ifql
    volumeMounts:
      - name: influxdb-pv
        mountPath: /var/lib/influxdb
  volumes:
    - name: influxdb-pv
      persistentVolumeClaim:
        claimName: influxdb-pvc

--- 

apiVersion: v1
kind: Service
metadata:
  name: influxdb
  namespace: dev
spec:
  ports:
  - name: http
    port: 8086
    protocol: TCP
    targetPort: 8086
  selector:
    app: influxdb
  sessionAffinity: None
  type: ClusterIP</pre>
<p>执行下面命令进入CLI：</p>
<pre class="crayon-plain-tag">kubectl -n dev exec -it influxdb  influx -- -precision rfc3339</pre>
<div class="blog_h1"><span class="graybg">入门</span></div>
<div class="blog_h2"><span class="graybg">CLI</span></div>
<div class="blog_h3"><span class="graybg">创建数据库</span></div>
<p>命令格式类似于MySQL：</p>
<pre class="crayon-plain-tag">CREATE DATABASE metrics
SHOW DATABASES
# name: databases
# name
# ----
# _internal
# metrics
USE metrics</pre>
<div class="blog_h3"><span class="graybg">读写数据</span></div>
<p>InfluxDB中存放的是时间序列数据，包括0-N个数据点。数据点由以下部分组成：</p>
<ol>
<li>time，时间戳</li>
<li>measurement，此指标的名字。例如CPU_LOAD</li>
<li>field，至少一个，KV对，V为数值例如value=6.12</li>
<li>tag，0-N个，指标的元数据。 host=zircon</li>
</ol>
<p>指标的模式（Schema）是自由的，你可以任意添加字段、Tag。但是<span style="background-color: #c0c0c0;">字段类型不可以改变</span>，如果你第一次写入host的值类型是字符串，就不能在以后写入数字。</p>
<p>写入数据点时，你需要遵守如下格式的行协议：</p>
<pre class="crayon-plain-tag">&lt;measurement&gt;[,&lt;tag-key&gt;=&lt;tag-value&gt;...] &lt;f1-key&gt;=&lt;f1-val&gt;[,&lt;f2-key&gt;=&lt;f2-val&gt;...] [unix-nano-timestamp]
# 示例：
load,host=xenial-100,group=k8s value=3.1</pre>
<p>通过CLI差入数据的InfluxQL语法为：</p>
<pre class="crayon-plain-tag">INSERT load,host=xenial-100,group=k8s value=3.1</pre>
<p>类似的，可以用SELECT查询指标数据：</p>
<pre class="crayon-plain-tag">SELECT * FROM load
# name: load
# time                group host       value
# ----                ----- ----       -----
# 1522746924735704773 k8s   xenial-100 3.1

# 每个Tag、字段都可以作为列名
SELECT "group", host,value FROM "load"</pre>
<p>FROM子句中，支持使用正则式来匹配指标名：</p>
<pre class="crayon-plain-tag">SELECT * FROM /.*/ LIMIT 1</pre>
<p>WHERE子句也是支持的。</p>
<div class="blog_h2"><span class="graybg">HTTP API</span></div>
<div class="blog_h3"><span class="graybg">数据库管理</span></div>
<pre class="crayon-plain-tag">export URL=http://influxdb.dev.svc.k8s.gmem.cc:8086

# 执行InfluxQL             格式化响应JSON                   查询语句
curl -i -XPOST $URL/query?pretty=true --data-urlencode "q=SHOW DATABASES"
curl -i -XPOST $URL/query?pretty=true --data-urlencode "q=CREATE DATABASE metrics" </pre>
<div class="blog_h3"><span class="graybg">写入操作</span></div>
<pre class="crayon-plain-tag"># 写入操作，响应码204，数据正常写入
# 时间戳（纳秒）可以不传，这样自动使用InfluxDB本地时间
curl -i -XPOST $URL/write?db=metrics --data-binary 'host=xenial-100,group=k8s value=3.1 1434055562000000000'

# 可以同时写入多个数据点，使用换行符分隔

# 导入文件中的数据点、
curl -i -XPOST $URL/write?db=metrics --data-binary @load.txt </pre>
<div class="blog_h3"><span class="graybg">查询操作</span></div>
<pre class="crayon-plain-tag">curl -G $URL/query?pretty=true --data-urlencode "db=metrics" --data-urlencode "q=SELECT * FROM load"</pre>
<p>结果同样以JSON形式返回。要同时<span style="background-color: #c0c0c0;">执行多个查询，只需要用分号分开</span>InfluxQL语句。</p>
<p>使用请求参数chunked/chunk_size可以发起分块（Chunked）请求，数据流式返回：</p>
<pre class="crayon-plain-tag">--data-urlencode "chunked=true" --data-urlencode "chunk_size=20000 </pre>
<div class="blog_h2"><span class="graybg">持续查询</span></div>
<p>Influx提供了持续查询（Continuous Queries，CQ）来<span style="background-color: #c0c0c0;">处理数据采样</span>。CQ是按照时间进行聚合的InfluxQL语句，周期性的在Influx数据库内部运行。示例：</p>
<pre class="crayon-plain-tag">-- 查询数据库metrics的指标network的inbound、outbuound字段，对两者取平均值
-- 每30分钟分组聚合一次
-- 聚合结果存放到RP为one_year的指标sampled_load中，该指标具有字段mean_inbound、mean_outbound
CREATE CONTINUOUS QUERY "cq_30m" ON "metrics" BEGIN
    SELECT mean("inbound") AS "mean_inbound",mean("outbound") AS "mean_outbound"
    INTO "one_year"."sampled_load"
    FROM "network"
    GROUP BY time(30m)
END</pre>
<p>配合使用CQ和RP，可以实现近期数据高精度保留，远期数据低精度保留甚至删除。 </p>
<div class="blog_h2"><span class="graybg">驻留策略</span></div>
<p>Retention Policy（RP）定义了InfluxDB保留历史数据的时长。示例：</p>
<pre class="crayon-plain-tag"># 为指标load定义一个驻留策略，时长2小时
# REPLICATION为数据复制的份数，对于单节点InfluxDB，必须设置为1
CREATE RETENTION POLICY "two_hours" ON "load" DURATION 2h REPLICATION 1 DEFAULT </pre>
<div class="blog_h1"><span class="graybg">配置</span></div>
<p> 配置文件默认位于/etc/influxdb/influxdb.conf。</p>
<pre class="crayon-plain-tag"># 禁止向usage.influxdata.com发送使用情况信息
reporting-disabled = false

# 数据备份/恢复服务绑定地址
bind-address = "127.0.0.1:8088"


[meta]
# metadata/raft数据库的存放位置
dir = "/var/lib/influxdb/meta"
# 创建数据库时，自动创建默认的驻留策略
retention-autocreate = true
# 为meta service打印日志
logging-enabled = true


[data]
# TSM存储引擎在何处存储TSM文件
dir = "/var/lib/influxdb/data"
# TSM存储引擎在何处存储TWAL文件
wal-dir = "/var/lib/influxdb/wal"
# 写操作fsync到磁盘的延迟，大于0的值可以用于慢速磁盘，以及WML写出现竞态的情况
# 对于非SSD磁盘，推荐取值范围0-100ms
wal-fsync-delay = "0s"
# 新分片使用的分片索引（Shard index）类型，默认是内存索引，重启后会重新创建
# 取值tsi1使用基于磁盘的索引，支持大cardinality数据集
index-version = "inmem"
# 是否启用tsm引擎的冗长日志 
trace-logging-enabled = false
# 是否启用查询日志
query-log-enabled = true
### TSM引擎配置 ###
# 在拒绝写入操作之前，分片缓存占用的内存最大量
cache-max-memory-size = "1g"
# 当缓存达到多大时，引擎产生其快照，并写入到TSM文件，以释放内存空间
cache-snapshot-memory-size = "25m"
# 如果分片持续多久没有接收到新的写入/删除操作，则引擎产生缓存的快照，并写入到新的TSM文件
cache-snapshot-write-cold-duration = "10m"
# 如果引擎持续多久没有接收到分片的新的写入/删除操作，它将压缩分片的所有TSM文件
compact-full-write-cold-duration = "4h"
# 压缩并行度
max-concurrent-compactions = 0
# 在丢弃写操作之前，每个数据库允许的最大序列（Series）数量
# 可以在数据库级别解决高Cardinality问题，设置为0则禁用
max-series-per-database = 1000000
# 在丢弃写操作之前，每个Tag的最大值数量
# 可以解决Tag值的高Cardinality问题，设置为0则禁用
max-values-per-tag = 100000


# 集群服务配置
[coordinator]
# 写操作超时时间
write-timeout = "10s"
# 同时执行的查询的最大数量
max-concurrent-queries = 0
# 查询超时：在查询被系统杀掉之前，经过的最大时间
query-timeout = "0s"
# 缓慢查询阈值，超过此阈值的被记录到缓慢日志
log-queries-after = "0s"
# 一个SELECT最多能处理的数据点数量
max-select-point = 0
# 一个SELECT可以处理的Series的最大数量
max-select-series = 0
# 一个SELECT最多创建的Group by 时间桶（Time Bucket）数量
max-select-buckets = 0


# 驻留策略配置，驻留策略决定了如何清除老旧数据
[retention]
# 是否启用驻留策略，也就是说是否清除老旧数据
enabled = true
# 每隔多久进行运行驻留策略
check-interval = "30m"


# 控制分配的预创建，这样在数据到达前分片即可用
[shard-precreation]
# 是否启用分片预创建服务
enabled = true
# 预创建服务运行间隔
check-interval = "10m"

advance-period = "30m"


# 系统自我监控、统计和诊断
[monitor]
# 是否在内部进行统计信息
store-enabled = true
# 存储统计信息的目标数据库
store-database = "_internal"
# 每隔多久记录一次统计信息
store-interval = "10s"


[http]
# 是否启用HTTP端点
enabled = true
# HTTP服务的绑定地址
bind-address = ":8086"
# 是否启用用户身份验证
auth-enabled = false
# 基于HTTP基本认证时，发送给客户端的Realm信息
realm = "InfluxDB"
# 是否记录HTTP请求日志
log-enabled = true
# 访问日志路径
access-log-path = ""
# 是否启用详细的写日志
write-tracing = false
# 是否启用pprof端点，用于调试和诊断
pprof-enabled = true
# 是否启用HTTPS
https-enabled = false
# SSL整数路径
https-certificate = "/etc/ssl/influxdb.pem"
# 可以指定单独的私钥路径
https-private-key = ""
# SON web token共享密钥
shared-secret = ""
# 结果集大小限制，防止查询耗尽InfluxDB内存
# 仅仅限制非分块的HTTP API请求，分块请求不受限制
max-row-limit = 0
# 最大连接数
max-connection-limit = 0
# 是否使用UNIX域套接字
unix-socket-enabled = false
# UNIX域套接字路径
bind-socket = "/var/run/influxdb.sock"
# 客户端请求体大小
max-body-size = 25000000


[ifql]
# 是否启用RPC服务
enabled = true
# 是否进行额外的日志记录
log-enabled = true
# 绑定地址
bind-address = ":8082"


[logging]
# 日志格式
format = "auto"
# 日志级别
level = "info"
# 是否禁止显示LOGO
suppress-logo = false


# 控制订阅，订阅可以用于获取整个数据集的拷贝
[subscriber]
# 是否启用订阅者服务
enabled = true
# 写到订阅者的HTTP请求的超时
http-timeout = "30s"
# 是否允许到订阅者的不安全连接
insecure-skip-verify = false
# 数字证书位置
ca-certs = ""
# Goroutine数量
write-concurrency = 40
# 写缓冲区大小
write-buffer-size = 1000


[continuous_queries]
# 是否启用持续查询服务
enabled = true
# 是否记录CQ日志
log-enabled = true
# 是否记录统计信息
query-stats-enabled = false
# 多久检查CQ服务是否需要运行
run-interval = "1s"</pre>
<p>&nbsp;</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/influxdb-study-note">InfluxDB学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/influxdb-study-note/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Apache Drill学习笔记</title>
		<link>https://blog.gmem.cc/apache-drill-study-note</link>
		<comments>https://blog.gmem.cc/apache-drill-study-note#comments</comments>
		<pubDate>Tue, 08 Aug 2017 03:34:24 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[BigData]]></category>
		<category><![CDATA[Database]]></category>
		<category><![CDATA[MongoDB]]></category>
		<category><![CDATA[学习笔记]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=15283</guid>
		<description><![CDATA[<p>简介 Apache Drill是一个模式自由（Schema-free ）的、低延迟的、分布式的、可扩容的SQL查询引擎，可以让你使用熟悉的SQL语法对各种非关系型数据库进行操作。Drill支持针对PB级别数据的即席查询。Drill支持大量NoSQL数据和文件系统，包括MongoDB、HBase、HDFS。支持对不同数据源中的数据进行join操作。Drill支持Windows/Linux/Mac系统，可以很容易的在服务器集群中扩容。 Drill的优势包括： 支持模式自由的JSON模型，Drill是第一个、目前也是唯一的不对Schema做任何要求的分布式SQL引擎。这种模式自由类似于MongoDB。Drill在查询执行过程中可以自动发现Schema 即席的查询复杂的、半结构化的数据。你不需要对数据进行任何转换，Drill对SQL进行了直观的扩展，方面处理内嵌数据，就好像内嵌数据是普通的SQL列一样 真实的SQL语言，Drill支持标准的SQL 2003语法。支持DATE, INTERVAL, TIMESTAMP, VARCHAR等数据类型，以及关联子查询、JOIN子句 方便的和既有BI工具集成 针对Hive表的交互式查询 同时访问多个数据源 用户自定义函数支持，直接支持Hive用户定义函数 高性能、可扩容 基本概念 Drillbit Drill的核心是Drillbit服务，它负责接受客户请求、处理查询、返回查询结果。Drillbit可以被安装到并运行在数据库集群的所有节点上，这样在执行查询时可以减少网络流量。Drill通过ZooKeeper来维护集群成员状态、检查健康状况。 当你以SQL的形式发起一个查询时，查询被发送给Drill集群中的一个Drillbit，这个Drillbit成为领头（Foreman），它负责协作其它Drillbit以完成查询执行： 解析SQL语句，将SQL操作符转换为Drill理解的逻辑操作符。这些逻辑操作符共同组成了逻辑执行计划，描述了生成查询结果所需的操作、哪些数据源需要参与其中 Foreman把逻辑计划发送给基于成本的优化器，优化操作符的顺序，最终转换为物理执行计划 <a class="read-more" href="https://blog.gmem.cc/apache-drill-study-note">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/apache-drill-study-note">Apache Drill学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">简介</span></div>
<p>Apache Drill是一个模式自由（Schema-free ）的、低延迟的、分布式的、可扩容的SQL查询引擎，可以让你使用熟悉的SQL语法对各种非关系型数据库进行操作。Drill支持针对PB级别数据的即席查询。Drill支持大量NoSQL数据和文件系统，包括MongoDB、HBase、HDFS。支持对不同数据源中的数据进行join操作。Drill支持Windows/Linux/Mac系统，可以很容易的在服务器集群中扩容。</p>
<p>Drill的优势包括：</p>
<ol>
<li>支持模式自由的JSON模型，Drill是第一个、目前也是唯一的不对Schema做任何要求的分布式SQL引擎。这种模式自由类似于MongoDB。Drill在查询执行过程中可以自动发现Schema</li>
<li>即席的查询复杂的、半结构化的数据。你不需要对数据进行任何转换，Drill对SQL进行了直观的扩展，方面处理内嵌数据，就好像内嵌数据是普通的SQL列一样</li>
<li>真实的SQL语言，Drill支持标准的SQL 2003语法。支持DATE, INTERVAL, TIMESTAMP, VARCHAR等数据类型，以及关联子查询、JOIN子句</li>
<li>方便的和既有BI工具集成</li>
<li>针对Hive表的交互式查询</li>
<li>同时访问多个数据源</li>
<li>用户自定义函数支持，直接支持Hive用户定义函数</li>
<li>高性能、可扩容</li>
</ol>
<div class="blog_h2"><span class="graybg">基本概念</span></div>
<div class="blog_h3"><span class="graybg">Drillbit</span></div>
<p>Drill的核心是Drillbit服务，它负责接受客户请求、处理查询、返回查询结果。Drillbit可以被安装到并运行在数据库集群的所有节点上，这样在执行查询时可以减少网络流量。Drill通过ZooKeeper来维护集群成员状态、检查健康状况。</p>
<p>当你以SQL的形式发起一个查询时，查询被发送给Drill集群中的一个Drillbit，这个Drillbit成为领头（Foreman），它负责协作其它Drillbit以完成查询执行：</p>
<ol>
<li>解析SQL语句，将SQL操作符转换为Drill理解的逻辑操作符。这些逻辑操作符共同组成了逻辑执行计划，描述了生成查询结果所需的操作、哪些数据源需要参与其中</li>
<li>Foreman把逻辑计划发送给基于成本的优化器，优化操作符的顺序，最终转换为物理执行计划</li>
<li>Foreman中的并行器（parallelizer）把物理计划分为多个阶段 —— major/minor fragments。这些片断会并行的在所配置的数据源中执行</li>
</ol>
<div class="blog_h3"><span class="graybg">Major Fragments</span></div>
<p>构成执行计划的一个阶段，每个阶段可以由1-N个主片断构成，这些片断代表完成此阶段Drill必须执行的操作。Drill为每个主片段分配一个ID。</p>
<p>例如，为了针对两个文件进行哈希聚合，Drill可能创建具有两个阶段的查询计划，每个计划包含一个主片断。第一个阶段专注于扫描文件，第二个阶段则专注于数据的聚合。</p>
<p>Drill使用exchange operator来分隔多个主片段，所谓exchange可以是：</p>
<ol>
<li>数据位置的变化，或/和</li>
<li>物理计划的并行化</li>
</ol>
<p>一个exchange由sender/receiver组成，允许数据在节点之间流动。</p>
<p>主片断本身不负责任何查询任务的实际执行。每个主片段包含若干个从片断，从片断负责执行并完成查询。</p>
<p>你可以获得物理计划的JSON表示，修改之，然后通过Drill的SUBMIT PLAN命令提交执行。</p>
<div class="blog_h3"><span class="graybg">Minor Fragments</span></div>
<p>每个主片断被并行化为多个从片断。从片断是运行在一个线程内的逻辑工作单元（也叫slice）。每个从片断被分配一个ID。</p>
<p>Foreman中的并行器在执行期间把一个主片段拆分为1-N个从片断。Drill会根据数据局部性（data locality）把从片断调度到特定的节点上，并尽快的执行从片断（根据上流数据需求）。</p>
<p>从片断包含1-N个关系操作符，关系操作符执行关系型操作，例如scan, filter, join,group by。</p>
<p>从片断们可以形成树形结构，并分为root、intermediate、leaf三种角色。这种执行树仅仅包含一个运行在Foreman上的root从片断，需要执行的操作逐级下发，直到leaf从节点。leaf从节点与存储层交互或者访问磁盘数据，得到部分的结果，由上级节点进行聚合操作。 </p>
<div class="blog_h2"><span class="graybg">核心模块</span></div>
<p>每个Drillbit都由以下模块组成：</p>
<p><img class="aligncenter size-full wp-image-15291" src="https://blog.gmem.cc/wp-content/uploads/2017/08/DrillbitModules.png" alt="drillbitmodules" width="545" height="232" /></p>
<div class="blog_h3"><span class="graybg">RPC endpoint</span></div>
<p>Drill暴露的低资源消耗的RPC协议，用于客户端连接。客户端可以直接连接到Drillbit，或者通过ZooKeeper连接。推荐使用后一种方式，以隔离Drill集群变化造成的影响。</p>
<div class="blog_h3"><span class="graybg">SQL parser</span></div>
<p>基于开源SQL解析器Calcite实现，用于解析客户端请求。解析结果是语言无关、计算机友好的逻辑计划。</p>
<div class="blog_h3"><span class="graybg">Storage plugin interface</span></div>
<p>屏蔽特定数据存储的差异性。存储插件的功能包括：</p>
<ol>
<li>从数据源获取元数据</li>
<li>读写数据</li>
<li>数据位置感知、一系列优化规则</li>
</ol>
<div class="blog_h2"><span class="graybg">客户端</span></div>
<p>访问Drill的途径包括：</p>
<ol>
<li>Drill Shell</li>
<li>Drill Web Console</li>
<li>ODBC/JDBC</li>
<li>C++ API</li>
</ol>
<div class="blog_h1"><span class="graybg">安装</span></div>
<div class="blog_h2"><span class="graybg">嵌入式安装</span></div>
<p>如果仅仅在单个节点上使用Drill，可以使用嵌入式安装。这种模式下，不需要安装ZooKeeper，也不需要进行配置。当你启动Drill shell时，本地的Drillbit服务自动启动。</p>
<p>安装步骤：</p>
<pre class="crayon-plain-tag">wget http://apache.mirrors.hoobly.com/drill/drill-1.11.0/apache-drill-1.11.0.tar.gz
tar xzf apache-drill.tar.gz</pre>
<p>要运行Drill，执行下面的命令以打开Drill Shell：</p>
<pre class="crayon-plain-tag">drill-embedded
0: jdbc:drill:zk=local&gt; 
# 命令提示符说明：
# 0 表示连接到drill的连接数
# jdbc为连接类型
# zk=local 作为ZooKeeper的代替</pre>
<p>或者执行<pre class="crayon-plain-tag">sqlline -u "jdbc:drill:zk=local"</pre></p>
<p>要退出Drill Shell，在Shell中输入<pre class="crayon-plain-tag">!quit</pre></p>
<p>要访问Web Console，在浏览器地址栏输入<pre class="crayon-plain-tag">http://127.0.0.1:8047/</pre></p>
<div class="blog_h2"><span class="graybg">分布式安装</span></div>
<p>要在Hadoop集群环境下使用Drill，可以使用分布式安装。ZooKeeper的分布式集群是必须的前提，你也需要对Drill进行配置，才能连接到各种数据源。</p>
<p>下载、解压后，修改配置文件：</p>
<pre class="crayon-plain-tag">drill.exec: {
  # Drill集群标识符
  cluster-id: "drillbits",
  # ZooKeeper连接字符串
  zk.connect: "172.21.0.1:2181,172.21.0.2:2181,172.21.0.3:2181"
}</pre>
<p>要以集群模式启动Drill，首先需要在集群的每个节点上启动守护程序Drillbit：</p>
<pre class="crayon-plain-tag"># 命令格式：drillbit.sh [--config &lt;conf-dir&gt;] (start|stop|status|restart|autorestart)
/home/alex/JavaEE/middleware/drill/bin/drillbit.sh --config /home/alex/JavaEE/middleware/drill/conf start</pre>
<p>要连接到分布式部署的Drill Shell，可以：</p>
<ol>
<li>执行drill-conf，此脚本使用conf/drill-override.conf配置</li>
<li>执行drill-localhost连接到运行在本机的ZooKeeper </li>
</ol>
<p>连接上以后，可以执行<pre class="crayon-plain-tag">SELECT * FROM sys.drillbits;</pre>查询Drill集群成员信息。</p>
<div class="blog_h2"><span class="graybg">在Docker中运行</span></div>
<p>参考如下Dockerfile：</p>
<pre class="crayon-plain-tag">FROM openjdk:8-jre

ENV CLUSTER_ID drillbits
ENV ZK_CONNECT 172.21.0.1:2181


RUN apt-get install -y wget tar

ADD docker-entrypoint.sh .
ADD apache-drill.tar.gz  .

RUN chmod +x docker-entrypoint.sh &amp;&amp; mv apache-drill-1.11.0 /opt/drill

ENTRYPOINT ["/docker-entrypoint.sh"]</pre>
<p>入口脚本：</p>
<pre class="crayon-plain-tag">#!/usr/bin/env bash

cat &lt;&lt; EOF  &gt; /opt/drill/conf/drill-override.conf
drill.exec: {
  cluster-id: "$CLUSTER_ID",
  zk.connect: "$ZK_CONNECT"
}
EOF

/opt/drill/bin/drillbit.sh --config /opt/drill/conf run</pre>
<p>创建并运行容器： </p>
<pre class="crayon-plain-tag">docker run -e ZK_CONNECT=172.21.0.1:2181,172.21.0.2:2181,172.21.0.3:2181 --name drill-14 \
           --network local --ip 172.21.1.14 -d docker.gmem.cc/drill </pre>
<div class="blog_h1"><span class="graybg">配置</span></div>
<div class="blog_h2"><span class="graybg">内存配置</span></div>
<p>你可以配置分配给Drillbit的用于处理查询的直接内存的量。默认配置是8G，在高负载下可能需要16G或者更多。</p>
<p>Drill使用Java的直接内存来存储执行中的操作，除非必须，它不会使用磁盘。这和MapReduce不同，后者将任务每个阶段的输出都存放在磁盘上。JVM的堆内存不限制Drillbit能够使用的直接内存。Drillbit的堆内存通常设置到4-8G就足够了，因为Drill避免在堆中写数据。</p>
<p>从1.5版本开始，Drill使用新的直接内存分配器，可以更好的使用、跟踪直接内存。由于这一变化，sort操作符可能因为内存不足而失败。</p>
<p>系统选项 planner.memory.max_query_memory_per_node 设置单个Drillbit中每个查询的sort操作符能够使用的内存量。如果一个查询计划中包含多个sort操作符，它们共享这一内存。如果sort查询出现内存问题，考虑增加此选项的值。如果问题仍然存在，考虑减小系统选项planner.width.max_per_node的值，该值控制单个节点的并行度。</p>
<div class="blog_h3"><span class="graybg">修改内存限制</span></div>
<p>在drill-env.sh中设置环境变量：</p>
<pre class="crayon-plain-tag"># 如果堆内存没有设置，将其设置为4G
export DRILL_HEAP=${DRILL_HEAP:-"4G”}  
# 如果直接内存没有设置，将其设置为8G
export DRILL_MAX_DIRECT_MEMORY=${DRILL_MAX_DIRECT_MEMORY:-"8G"}</pre>
<div class="blog_h2"><span class="graybg">安全配置</span></div>
<div class="blog_h3"><span class="graybg">角色</span></div>
<p>Drill提供两种角色：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">角色</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>USER</td>
<td>可以对具有访问权限的数据执行查询。每个存储插件负责读写权限的管理</td>
</tr>
<tr>
<td>ADMIN</td>
<td>
<p>当启用身份验证时，仅仅具有Drill集群管理员角色的用户能够执行以下任务：</p>
<ol>
<li>使用ALTER SYSTEM来改变系统级选项</li>
<li>通过Web Console或者REST API来更新存储插件配置</li>
<li>提供和普通用户不同的导航栏</li>
<li>查看集群中正在运行的所有查询的profiles</li>
<li>取消运行中的查询</li>
</ol>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">身份模拟</span></div>
<p>用户身份模拟（Impersonation）允许一个服务代表客户端执行某个操作。默认的，身份模拟被关闭。</p>
<div class="blog_h3"><span class="graybg">PAM认证</span></div>
<p>Drill支持基于Linux PAM的身份验证。PAM允许和系统密码文件（/etc/passwd）或者LDAP等PAM实体进行交互以完成身份验证。</p>
<p>使用PAM验证时，运行Drill查询的用户必须存在于每一个Drill节点上。</p>
<div class="blog_h3"><span class="graybg">Kerberos认证</span></div>
<p>Drill支持Kerberos v5网络认证、客户端 - Drill的通信加密。需要配合JDBC驱动来使用该认证方式。</p>
<p>在启动时，一个Drillbit必须被验证。在运行时Drill使用和KDC共享的keytab文件，Drill使用该文件来验证票据的合法性。</p>
<p>配置<pre class="crayon-plain-tag">security.user.encryption.sasl.enabled</pre>参数为true，可以启用Kerberos加密 —— 保证客户端到Drillbit的数据安全。</p>
<p>你需要为Drill创建principal，可以：</p>
<pre class="crayon-plain-tag">kadmin
# 一个集群使用单个实体
# addprinc  &lt;username&gt;/&lt;clustername&gt;@&lt;REALM&gt;.COM 
addprinc  drill/drillbits@GMEM.CC</pre>
<p>你需要为上面的principal创建一个keytab文件：</p>
<pre class="crayon-plain-tag">ktadd -k /home/alex/JavaEE/middleware/drill/conf/drill.keytab drill/drillbits@GMEM.CC</pre>
<p>然后，为Drill配置文件添加：</p>
<pre class="crayon-plain-tag">drill.exec: {
  security: {
  	user.auth.enabled: true,
  	user.encryption.sasl.enabled: true,
  	auth.mechanisms: ["KERBEROS"],
  	auth.principal: "drill/drillbits@GMEM.CC",
  	auth.keytab: "/home/alex/JavaEE/middleware/drill/conf/drill.keytab"
  }
}</pre>
<p>并重启。</p>
<div class="blog_h2"><span class="graybg">配置选项</span></div>
<div class="blog_h3"><span class="graybg">关键启动选项</span></div>
<p>你可以在conf/drill-override.conf中配置启动选项，其中最常用的如下表：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 30%; text-align: center;">选项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>drill.exec.http.ssl_enabled</td>
<td>布尔（TRUE|FALSE），默认FALSE。是否启用HTTPS支持</td>
</tr>
<tr>
<td>drill.exec.sys.store.provider.class</td>
<td>设置持久化存储提供者（PStore），PStore保存配置数据、Profile</td>
</tr>
<tr>
<td>drill.exec.buffer.size</td>
<td>缓冲区大小，增加此配置可以加快查询速度</td>
</tr>
<tr>
<td>drill.exec.sort.external.spill.directories</td>
<td>进行Spool操作时使用的目录</td>
</tr>
<tr>
<td>drill.exec.zk.connect</td>
<td>提供ZooKeeper连接字符串</td>
</tr>
<tr>
<td>drill.exec.profiles.store.inmemory</td>
<td>布尔，默认FALSE。是否在内存中存放查询Profiles</td>
</tr>
<tr>
<td>drill.exec.profiles.store.capacity</td>
<td>上个选项取值TRUE时，内存中最多存放的查询Profiles数量</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">存储插件</span></div>
<p>Drill通过存储插件（Storage）连接到底层数据源。存储插件通常负责：</p>
<ol>
<li>连接到数据源，例如数据库、文件</li>
<li>优化Drill查询的执行</li>
<li>提供数据的位置信息</li>
<li>配置工作区、文件格式以读取数据</li>
</ol>
<p>常用的几个存储插件跟随Drill一起安装</p>
<div class="blog_h2"><span class="graybg">注册插件配置</span></div>
<p>所谓插件配置，就是连接到目标数据源的配置信息。Drill默认注册了这几个默认的插件配置：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 15%; text-align: center;">插件配置</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>cp</td>
<td>指向Drill类路径中的JAR文件，你可以对其中的文件进行查询</td>
</tr>
<tr>
<td>dfs</td>
<td>指向本地文件系统。你可以使用对应的存储引擎配置指向任意分布式系统，例如Hadoop </td>
</tr>
<tr>
<td>hbase</td>
<td>提供到HBase的连接 </td>
</tr>
<tr>
<td>hive</td>
<td>将Drill和Hive的元数据抽象（文件、HBase）机制集成</td>
</tr>
<tr>
<td>mongo</td>
<td>提供到MongoDB的连接 </td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">注册MongoDB配置</span></div>
<p>通过Web Console连接（地址示例：http://172.21.1.14:8047/storage），可以注册插件配置。</p>
<p>点击Disable按钮可以禁用当前的配置，禁用后，show databases中对应的条目消失。点击Enable可以启用某个可用配置。输入存储插件名称，点击Create，可以建立新的插件配置。</p>
<p>插件配置都是JSON格式，MongoDB配置的示例：</p>
<pre class="crayon-plain-tag">{
  "type": "mongo",
  "connection": "mongodb://root:root@mongo-s1.gmem.cc:27017/",
  "enabled": true
}</pre>
<div class="blog_h3"><span class="graybg">测试配置正确性</span></div>
<p>打开drill-conf，输入命令验证连接是否正常：</p>
<pre class="crayon-plain-tag">0: jdbc:drill:&gt; show databases;
+---------------------+
|     SCHEMA_NAME     |
+---------------------+
| INFORMATION_SCHEMA  |
| mongo.admin         |
| mongo.bais          |
| mongo.config        |
| sys                 |
+---------------------+

# 上面的结果意味着已经连接到此配置，注意数据库名称的前缀，就是配置的名称

use mongo.bais;
select regNo,stocks[0].stockName as stock0Name from corps;
+----------------+-------------+
|     regNo      | stock0Name  |
+----------------+-------------+
| 3208261000000  | 汪震          |
+----------------+-------------+

# 上面的结果意味着查询测试成功 </pre>
<div class="blog_h1"><span class="graybg">JDBC/ODBC</span></div>
<p>除了Shell、Web Console以外，Drill还提供C++ API以及JDBC、ODBC驱动。</p>
<div class="blog_h2"><span class="graybg">JDBC</span></div>
<p>添加依赖以使用此驱动：</p>
<pre class="crayon-plain-tag">&lt;dependency&gt;
    &lt;groupId&gt;org.apache.drill.exec&lt;/groupId&gt;
    &lt;artifactId&gt;drill-jdbc&lt;/artifactId&gt;
    &lt;version&gt;1.11.0&lt;/version&gt;
&lt;/dependency&gt;</pre>
<div class="blog_h3"><span class="graybg">URL格式</span></div>
<pre class="crayon-plain-tag"># jdbc:drill:zk={ZooKeeper连接字符串}/drill/{Drill集群标识符};schema={存储插件配置.数据库名称}
jdbc:drill:zk=zookeeper-1.gmem.cc:2181,zookeeper-2.gmem.cc:2181,zookeeper-3.gmem.cc:2181/drill/drillbits;schema=mongo.bais</pre>
<div class="blog_h3"><span class="graybg">JDBC代码示例</span></div>
<pre class="crayon-plain-tag">Class.forName( "org.apache.drill.jdbc.Driver" );
String url = "jdbc:drill:zk=zookeeper-1.gmem.cc:2181/drill/drillbits;schema=mongo.bais";
Connection connection = DriverManager.getConnection( url );
Statement st = connection.createStatement();
ResultSet rs = st.executeQuery( "select regNo,stocks[0].stockName as stock0Name from corps" );
while ( rs.next() ) {
    System.out.println( rs.getString( 2 ) );
}</pre>
<div class="blog_h1"><span class="graybg">查询数据</span></div>
<div class="blog_h2"><span class="graybg">复杂数据结构</span></div>
<p>所谓复杂数据结构，是指与关系型数据库那种简单的表格形式（行、字段）不同的，具有复杂数据类型字段（内嵌结构）的数据结构。</p>
<p>Drill可以在执行查询请求的时候，发现数据的结构。类似于JSON、Parquet之类的嵌套数据结构不仅仅可以被简单的访问，Drill还提供特殊的操作符、函数对其进行钻取操作。这些操作符、函数能够：</p>
<ol>
<li>引用内嵌数据结构的值</li>
<li>访问数组元素、嵌套数组</li>
</ol>
<div class="blog_h2"><span class="graybg">JOIN操作</span></div>
<p>你可以使用SQL标准的join子句来连接两个表或/和文件。示例：</p>
<pre class="crayon-plain-tag">select c.regNo, c.corpName, o.name from corps as c join orgs as o on c.belongOrg = o._id where c.regCapi &gt; 10000;</pre>
<div class="blog_h2"><span class="graybg">访问嵌套数据</span></div>
<pre class="crayon-plain-tag">-- 访问内嵌文档
select c.address.detail as addr from corps as c;
-- 访问内嵌数组
select c.stocks[0].stockName from corps as c;
select c.stocks[0].stockName, c.stocks[0].subsCapi from corps as c;</pre>
<div class="blog_h1"><span class="graybg">日志与调试</span></div>
<p>Drill使用Logback作为默认的日志系统，日志配置位于conf/logback.xml。</p>
<p>默认的，日志被输出到文件系统，位于$DRILL_HOME/logs目录下，你可以在drill-env.sh中设置$DRILL_HOME环境变量。在每个Drill节点上，文件drillbit_queries.json记录每个查询的ID、profile信息。</p>
<div class="blog_h1"><span class="graybg">性能优化</span></div>
<div class="blog_h2"><span class="graybg">查询计划</span></div>
<p>要获得查询的执行计划，执行<pre class="crayon-plain-tag">explain plan for</pre>语句，示例：</p>
<pre class="crayon-plain-tag">explain plan for select regNo,corpName from bais.corps;</pre>
<p>从输出结果中，可以看到Drill如何访问底层数据源：</p>
<pre class="crayon-plain-tag"># explain plan for select regNo,corpName from bais.corps where regNo like '3208%';
00-00    Screen
00-01      Project(regNo=[$0], corpName=[$1])
00-02        UnionExchange
01-01          Scan(groupscan=[MongoGroupScan [MongoScanSpec=MongoScanSpec
                   [dbName=bais, collectionName=corps, filters=null], columns=[`regNo`, `corpName`]]])
                                                       # 没有过滤器，意味着需要全表扫描
# explain plan for select regNo,corpName from bais.corps where regNo &gt; '320800100' and regNo &lt; '320800200' limit 10;
00-00    Screen
00-01      Project(regNo=[$0], corpName=[$1])
00-02        SelectionVectorRemover
00-03          Limit(fetch=[10])
00-04            UnionExchange
01-01              SelectionVectorRemover
01-02                Limit(fetch=[10])
01-03                  Scan(groupscan=[MongoGroupScan [MongoScanSpec=MongoScanSpec 
                     [dbName=bais, collectionName=corps, 
                     filters=Document{{$and=[Document{{regNo=Document{{$gt=320800100}}}}, 
                     Document{{regNo=Document{{$lt=320800200}}}}]}}], columns=[`regNo`, `corpName`]]])
                                                       # 这里可以看到使用了MongoDB的查询过滤，可能利用到索引</pre>
<div class="blog_h1"><span class="graybg">SQL参考</span></div>
<p>Drill支持ANSI标准SQL，你可以使用统一的语法查询各种数据源。为了支持嵌套数据结构，Drill提供特殊的操作符和函数。</p>
<div class="blog_h2"><span class="graybg">数据类型</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">数据类型</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>BIGINT</td>
<td>8字节有符号整数</td>
</tr>
<tr>
<td>BINARY</td>
<td>变长二进制字符串，示例：B@e6d9eb7</td>
</tr>
<tr>
<td>BOOLEAN</td>
<td>布尔值，示例：true</td>
</tr>
<tr>
<td>DATE</td>
<td>YYYY-MM-DD格式的日期</td>
</tr>
<tr>
<td>DECIMAL(p,s)   DECIMAL(p,s)   NUMERIC(p,s)</td>
<td>38位精度数字</td>
</tr>
<tr>
<td>FLOAT</td>
<td>4字节浮点数</td>
</tr>
<tr>
<td>DOUBLE</td>
<td>8字节浮点数</td>
</tr>
<tr>
<td>INTEGER   INT</td>
<td>4字节有符号整数</td>
</tr>
<tr>
<td>INTERVAL</td>
<td>日/月时间间隔</td>
</tr>
<tr>
<td>SMALLINT</td>
<td>2字节有符号整数</td>
</tr>
<tr>
<td>TIME</td>
<td>HH:mm:ss格式的日期</td>
</tr>
<tr>
<td>TIMESTAMP</td>
<td>yyyy-MM-dd HH:mm:ss.SSS格式的时间戳</td>
</tr>
<tr>
<td>CHARACTER VARYING    CHARACTER    CHAR   VARCHAR</td>
<td>UTF-8字符串</td>
</tr>
<tr>
<td>Map</td>
<td>键值对形式的容器，KVGEN、FLATTEN函数用于处理此类型</td>
</tr>
<tr>
<td>Array</td>
<td>数组形式的容器，FLATTEN函数用于处理此类型</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">类型转换</span></div>
<p>使用CAST、CONVERT TO/FROM、TO_CHAR、TO_DATE、TO_NUMBER、TO_TIMESTAMP，可以进行显式的类型转换。某些类型之间可以进行隐式转换，NULL可以转换到任何类型。示例代码：</p>
<pre class="crayon-plain-tag">-- CAST (&lt;expression&gt; AS &lt;data type&gt;)
CAST( regNo as INT )

-- 把目标列转换为字节
CONVERT_TO (column, type)
-- 把regNo作为大端整数，转换为字节
CONVERT_TO(regNo , 'INT_BE')

-- 把字节转换为type
CONVERT_FROM(column, type)
-- 把字符串转换为JSON map
CONVERT_FROM('{x:100, y:215.6}' ,'JSON')

-- TO_CHAR (expression, 'format') 转换数字、日期、时间、时间戳为字符串形式
SELECT TO_CHAR(1256.789383, '#,###.###') FROM (VALUES(1));   -- 1,256.789
TO_CHAR((CAST('2008-2-23' AS DATE)), 'yyyy-MMM-dd')          -- 2008-Feb-23 
TO_CHAR(CAST('12:20:30' AS TIME), 'HH mm ss'                 --  12 20 30
TO_CHAR(CAST('2015-2-23 12:00:00' AS TIMESTAMP), 'yyyy MMM dd HH:mm:ss')
                                                             -- 2015 Feb 23 12:00:00 

-- TO_DATE (expression [, 'format']) 转换字符串或者UNIX时间戳为日期
TO_DATE('2015-FEB-23', 'yyyy-MM-dd')
-- TO_TIME (expression [, 'format']) 转换为时间
TO_TIME('12:20:30', 'HH:mm:ss')
TO_TIME(82855000)
-- TO_TIMESTAMP (expression [, 'format'])
TO_TIMESTAMP('2008-2-23 12:00:00', 'yyyy-MM-dd HH:mm:ss')</pre>
<div class="blog_h2"><span class="graybg">SQL函数</span></div>
<p>主要分为数学、日期、字符串、聚合等函数，参考<a href="https://drill.apache.org/docs/math-and-trig/">官方文档</a>。</p>
<div class="blog_h2"><span class="graybg">窗口函数</span></div>
<p>窗口函数针对一系列行进行计算操作，并为每一行返回单个值。这些值虽然归属到某个行，但是它可能<span style="background-color: #c0c0c0;">取决于其它多个行（这些行就是所谓窗口）</span>。</p>
<p>你可以使用<pre class="crayon-plain-tag">OVER()</pre>来定义一个窗口，此子句将窗口函数与其它的聚合类函数区分开来，一个查询可以使用多个窗口函数（对应一个或者多个窗口定义）。OVER()子句能够：</p>
<ol>
<li>定义对行进行分组（partition）的标准，聚合函数在这些分组上进行。这通过PARTITION BY子句实现</li>
<li>在一个分组内部，对行进行排序。这通过ORDER BY子句实现</li>
</ol>
<p>对于窗口函数，你需要注意：</p>
<ol>
<li>仅仅支持在查询的SELECT、ORDER BY字句中使用窗口函数</li>
<li>Drill在WHERE, GROUP BY, HAVING之后处理窗口函数</li>
<li>在聚合函数之后跟随OVER()导致其作为窗口函数使用</li>
<li>使用窗口函数，你可以针对窗口帧中任意数量的行进行聚合</li>
<li>如果要针对FLATTEN子句的生成的结果集执行窗口函数，应该在子查询中使用FLATTEN</li>
</ol>
<div class="blog_h3"><span class="graybg">语法</span></div>
<p>窗口函数完整的调用语法：</p>
<pre class="crayon-plain-tag">-- window_function指定一种窗口函数，这些函数可能和普通的聚合函数同名，识别它是否为窗口函数的唯一方法就是看看
-- 后面有没有OVER关键字。窗口函数在窗口内部进行聚合
-- expression 为列表达式
-- PARTITION BY关键字定义了窗口：
-- expr_lists 可以是  expression | column_name [, 其它expr_list ]
-- ORDER BY 定义窗口内排序规则，如果没有PARTITION BY则针对整个表格排序
--  order_lists 可以是 expression | column_name [ASC | DESC] [ NULLS { FIRST | LAST } ] [, 其它 order_list ]  
-- frame_clause 可以是：
-- { RANGE | ROWS } frame_start
-- { RANGE | ROWS } BETWEEN frame_start AND frame_end
-- frame_start 格式：UNBOUNDED PRECEDING 或者 CURRENT ROW 
-- frame_end 格式：CURRENT ROW 或者 UNBOUNDED FOLLOWING 
window_function (expression) OVER (
    [ PARTITION BY expr_list ]
    [ ORDER BY order_list ][ frame_clause ] )</pre>
<div class="blog_h3"><span class="graybg">分类</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 18%; text-align: center;">窗口函数分类</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>聚合</td>
<td>AVG() 计算平均值、COUNT()计算总数、MAX()计算最大值、MIN()计算最小值、SUM()求和</td>
</tr>
<tr>
<td>排名</td>
<td>返回当前行在分组中的排名：
<ol>
<li><span style="color: #333333; font-family: Ubuntu, 'Times New Roman', 'Bitstream Charter', Times, serif; font-size: 13px; line-height: 22px;">CUME_DIST() 返回相对排名：(高名次行数 + 同名次行数) / 总行数</span></li>
<li>DENSE_RANK() 根据窗口的ORDER BY表达式进行排序，排序号不存在gap，也就是说同名次（peer）不会导致后续名次跳号</li>
<li>NTILE() 尽可能的把窗口分组中的所有行划分到指定数量的排名组中</li>
<li>PERCENT_RANK()，百分比排名：(当前行数 - 1) / (分组总行数 - 1)</li>
<li>RANK()，类似于第2个，但是允许gap存在，也就是说两行并列的第1名之后的名次是3</li>
<li>ROW_NUMBER()，返回行号，取决于ORDER BY表达式</li>
</ol>
</td>
</tr>
<tr>
<td>值</td>
<td>
<ol>
<li><span style="color: #333333; font-family: Ubuntu, 'Times New Roman', 'Bitstream Charter', Times, serif; font-size: small;"><span style="line-height: 22px;">LAG()，返回分组中上一行的某个列（或者表达式）的值，如果没有上一行，返回NULL</span></span></li>
<li>LEAD()，返回分组中下一行的某个列（或者表达式）的值，如果没有下一行，返回NULL</li>
<li>FIRST_VALUE()，返回窗口中第一行的值</li>
<li>LAST_VALUE()，返回窗口中最后一行的值</li>
</ol>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">示例</span></div>
<pre class="crayon-plain-tag">-- 查询企业信息，为结果集的每一行增加列：当前企业类型的平均注册资金
select 
    cast(c.corpName as char) corpName, c.corpType, 
    avg( c.regCapi ) over( partition by c.corpType) as avgRegCapi  
from bais.corps c where c.regNo &gt;= '320100100' and c.regNo &lt; '320100200';</pre>
<div class="blog_h2"><span class="graybg">嵌套数据函数</span></div>
<p>嵌套数据函数用于访问内嵌式的数据结构，包括数组、映射、重复标量类型。不要在GROUP BY、ORDER BY子句或者在比较操作符中使用前述内嵌数据。Drill不支持 VARCHAR:REPEATED之间的比较。</p>
<div class="blog_h3"><span class="graybg">FLATTEN</span></div>
<p>把嵌套数据结构分解为单独的记录（行），示例：</p>
<pre class="crayon-plain-tag">-- 每个企业包含多个股东，股东为数组
SELECT FLATTEN(stocks) FROM bais.corps  WHERE stocks IS NOT NULL;</pre>
<div class="blog_h3"><span class="graybg">KVGEN</span></div>
<p>从一个映射中抽取键值对</p>
<div class="blog_h3"><span class="graybg">REPEATED_COUNT</span></div>
<p>返回数组的长度：<pre class="crayon-plain-tag">REPEATED_COUNT (array)</pre></p>
<div class="blog_h3"><span class="graybg">REPEATED_CONTAINS</span></div>
<p>在数组中搜索指定的关键字：<pre class="crayon-plain-tag">REPEATED_CONTAINS(array_name, keyword)</pre>，返回布尔值</p>
<div class="blog_h1"><span class="graybg">常见问题</span></div>
<div class="blog_h2"><span class="graybg">零散问题</span></div>
<div class="blog_h3"><span class="graybg">无法启动：Drillbit is disallowed to bind to loopback address in distributed mode.</span></div>
<p>原因：DNS将当前主机名解析到本地环回地址，可能需要更改/etc/hosts文件</p>
<div class="blog_h3"><span class="graybg">无法启动：Could not get canonical hostname.</span></div>
<p>原因：DNS没有正确配置</p>
<p>解决办法：如果网络中没有启用DNS服务，可以静态的修改/etc/hosts文件</p>
<div class="blog_h3"><span class="graybg">Failed to encode '***' in character set 'ISO-8859-1'</span></div>
<p>可以用这种方式来指定中文查询条件：</p>
<pre class="crayon-plain-tag">select * from bais.trades where name like _UTF16'%农产品%';</pre>
<div class="blog_h2"><span class="graybg">MongoDB问题</span></div>
<div class="blog_h3"><span class="graybg">身份验证失败：com.mongodb.MongoSecurityException: Exception authenticating MongoCredential</span></div>
<p>原因：如果分片集群启用了身份验证，不但需要建立集群上的用户，还要为每个复制集创建本地用户。如果报错信息中有servers=[{address=****而且地址是分片的（而不是mongos的）地址，说明就是分片的身份验证出错。</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/apache-drill-study-note">Apache Drill学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/apache-drill-study-note/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>OpenTSDB学习笔记</title>
		<link>https://blog.gmem.cc/opentsdb-study-note</link>
		<comments>https://blog.gmem.cc/opentsdb-study-note#comments</comments>
		<pubDate>Thu, 20 Oct 2016 02:24:00 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[BigData]]></category>
		<category><![CDATA[Database]]></category>
		<category><![CDATA[TSDB]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=16756</guid>
		<description><![CDATA[<p>简介 OpenTSDB是一个开源的、被广泛使用的时间序列数据库。它提供了一整套和监控有关的功能，可以用来构建分布式、可扩容的监控系统。使用OpenTSDB可以不损失统计精度的永久保存监控数据，统计精度可以达到毫秒级。OpenTSDB的底层是Hadoop/HBase，可以扩容到每秒百万级的监控数据写操作。OpenTSDB自带了前端组件，并且支持通过HTTP Pull API拉取数据，这样你可以选择自己喜爱的前端组件。</p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/opentsdb-study-note">OpenTSDB学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">简介</span></div>
<p>OpenTSDB是一个开源的、被广泛使用的时间序列数据库。它提供了一整套和监控有关的功能，可以用来构建分布式、可扩容的监控系统。使用OpenTSDB可以不损失统计精度的永久保存监控数据，统计精度可以达到毫秒级。OpenTSDB的底层是Hadoop/HBase，可以扩容到每秒百万级的监控数据写操作。OpenTSDB自带了前端组件，并且支持通过HTTP Pull API拉取数据，这样你可以选择自己喜爱的前端组件。</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/opentsdb-study-note">OpenTSDB学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/opentsdb-study-note/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Graphite学习笔记</title>
		<link>https://blog.gmem.cc/graphite-study-note</link>
		<comments>https://blog.gmem.cc/graphite-study-note#comments</comments>
		<pubDate>Mon, 11 Jul 2016 08:33:51 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Database]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[Django]]></category>
		<category><![CDATA[Monitoring]]></category>
		<category><![CDATA[TSDB]]></category>
		<category><![CDATA[学习笔记]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=12245</guid>
		<description><![CDATA[<p>简介 Graphite是一个开源项目，可以作为时间序列数据库（TSDB）使用，当你需要存储随着时间变化的数值时，应当考虑使用时间序列数据库。 除了数据的存储、查询外，Graphite还提供数据可视化（UI层）功能，它可以很好的在廉价的硬件上运行。你可以使用Graphite来监控网站、应用程序、网络服务器等的性能数据（Metrics），轻松实现基于时间维的分析。 Graphite本身不负责性能数据的采集，但是它提供了简单易用的接口，公共这些接口你可以把基于数字的性能数据存储到Graphite中。 术语 术语 说明 datapoint 数据点，存放在timestamp bucket中的数值。timestamp bucket中的默认值是None function 时间序列（ time-series）函数，用来转换、合并、计算多个series resolution 分辨率，也称precision。序列中，一个数据点所跨越（代表）的秒数。分辨率确定了存储数据点频率如果一个series每N秒存储一个数据点，则其分辨率为N retention 驻留，series中包含的数据点的个数 series 一已命名的数据点的集合，每个series由其名称唯一确定，名称由点号分隔的字符串组成也称为Metrics、Metric series series <a class="read-more" href="https://blog.gmem.cc/graphite-study-note">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/graphite-study-note">Graphite学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">简介</span></div>
<p><a href="http://graphiteapp.org/">Graphite</a>是一个开源项目，可以作为时间序列数据库（TSDB）使用，当你需要存储<span style="background-color: #c0c0c0;">随着时间变化的数值</span>时，应当考虑使用时间序列数据库。</p>
<p>除了<span style="background-color: #c0c0c0;">数据的存储、查询</span>外，Graphite还提供<span style="background-color: #c0c0c0;">数据可视化</span>（UI层）功能，它可以很好的在廉价的硬件上运行。你可以使用Graphite来监控网站、应用程序、网络服务器等的性能数据（Metrics），轻松实现基于时间维的分析。</p>
<p>Graphite本身不负责性能数据的采集，但是它提供了简单易用的接口，公共这些接口你可以把基于数字的性能数据存储到Graphite中。</p>
<div class="blog_h2"><span class="graybg">术语</span></div>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">术语</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>datapoint</td>
<td>数据点，存放在timestamp bucket中的数值。timestamp bucket中的默认值是None</td>
</tr>
<tr>
<td>function</td>
<td>时间序列（ time-series）函数，用来转换、合并、计算多个series</td>
</tr>
<tr>
<td>resolution</td>
<td>分辨率，也称precision。序列中，一个数据点所跨越（代表）的秒数。分辨率确定了存储数据点频率<br />如果一个series每N秒存储一个数据点，则其分辨率为N</td>
</tr>
<tr>
<td>retention</td>
<td>驻留，series中包含的数据点的个数</td>
</tr>
<tr>
<td>series</td>
<td>一已命名的数据点的集合，每个series由其名称唯一确定，名称由点号分隔的字符串组成<br />也称为Metrics、Metric series</td>
</tr>
<tr>
<td>series list</td>
<td>包含通配符的series名称，匹配多个series</td>
</tr>
<tr>
<td>target</td>
<td>图形展示时的数据源，可以是metrics名称、metrics通配符、或者基于前两者的函数调用表达式</td>
</tr>
<tr>
<td>timestamp</td>
<td>数据点所关联的时间，1970-01-01到产生数据点那一刻的秒数</td>
</tr>
<tr>
<td>timestamp bucket</td>
<td>经过舍入后，能够整除分辨率的timestamp</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">架构</span></div>
<p>Graphite由三个组件构成：</p>
<ol>
<li>Carbon：一个基于<a href="https://twistedmatrix.com/">Twisted</a>（Python事件驱动网络框架）的守护程序，负责监听外部的时间序列数据</li>
<li>Whisper：一个简单的存储时间序列数据的数据库，设计上和<a href="http://oss.oetiker.ch/rrdtool/">RRDtool</a>类似 </li>
<li>Graphite-Web：一个基于<a href="https://www.djangoproject.com/">Django</a>（Python Web框架）的Web应用，使用<a href="https://www.cairographics.org/">Cairo</a>（一个2D图形库）来渲染性能数据的图表，使用<a href="https://www.sencha.com/products/extjs/">ExtJS</a>作为基础UI框架</li>
</ol>
<p>下面是Graphite的架构图：<img class="aligncenter size-full wp-image-12262" src="https://blog.gmem.cc/wp-content/uploads/2016/07/graphite-arch.png" alt="graphite-arch" width="100%" /></p>
<p>一旦你把数据送给Carbon，它就立刻可以在Webapp的图表中显示，因为数据在被写入文件系统之前，会驻留在缓存中。</p>
<p>除了使用Graphite Webapp，你也可以通过URL API，将图表嵌入到自己的应用程序中去。如何使用Graphite</p>
<div class="blog_h2"><span class="graybg">如何使用Graphite</span></div>
<p>使用Graphite来监控你的性能数据，你需要完成以下工作：</p>
<ol>
<li>理解Graphite组件的职责和相互关系</li>
<li>安装Graphite及其依赖</li>
<li>基础的配置，让Graphite能运行起来</li>
<li>设计Metrics路径</li>
<li>配置Metrics的驻留规则、聚合规则等</li>
<li>向Graphite发送Metrics</li>
<li>从Graphite获取Metrics并展示</li>
</ol>
<div class="blog_h3"><span class="graybg">Metrics路径设计</span></div>
<p>Metrics路径由点号分隔的字符串构成，类似于Python的包名称。路径是Metrics的标识。</p>
<p>你应当仔细的设计此路径的命名空间，以反映出所有Metrics之间的层次关系。例如servers.zircon.cpu，这个三级路径设计中，第一级表示设备类别，第二级表示设备名称，第三级表示监测点类型。</p>
<div class="blog_h1"><span class="graybg">理解Graphite组件</span></div>
<div class="blog_h2"><span class="graybg">Carbon</span></div>
<p>Carbon由一系列的守护进程组成，这些守护进程共同组成Graphite的存储后端。在最小化的安装下，只有一个守护进程carbon-cache.py。根据需要你可以启用carbon-relay.py、carbon-aggregator.py以便实现Metrics分发、定制聚合规则。各Carbon守护进程简介如下：</p>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">守护进程</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>缓存进程<br />carbon-cache</td>
<td>
<p>缓存进程的程序文件是carbon-cache.py。它通过多种协议接收Metrics，然后尽可能高效的将其写入磁盘。从实现角度来说，缓存进程先把Metrics存放在RAM中，然后定期的通过whisper库进行入库</p>
<p>缓存进程提供一个查询服务供Graphite Webapp使用，用来快速获取位于内存中的Metrics数据点</p>
<p>要定制此进程的行为，可以修改carbon.conf的[cache]段、storage-schemas.conf、storage-aggregation.conf</p>
</td>
</tr>
<tr>
<td>中继进程<br />carbon-relay</td>
<td>
<p>该进程可以担任这两个职责之一：复制（<span style="color: #404040;">replication</span>）和分片（<span style="color: #404040;">sharding</span>）</p>
<p>要定制此进程的行为，可以修改carbon.conf的[relay]段、relay-rules.conf</p>
</td>
</tr>
<tr>
<td>前置聚合进程<br />carbon-aggregator</td>
<td>
<p>前置于缓存进程运行，能够在存入whisper之前，缓冲、聚合Metrics。当不需要细粒度的数据时启用该进程，可以减少I/O和.wsp文件的大小</p>
<p>要定制此进程的行为，可以修改carbon.conf的[aggregator]段、aggregation-rules.conf</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">Whisper</span></div>
<p>Whisper是一个固定大小（fixed-size）的数据库，其设计和用途与RRD（round-robin-database）类似。它能提供快速、可靠的数值数据的存储。</p>
<p>Whisper能够对最近的数据进行高分辨率的存储，而对久远的历史数据，自动降低其存储精度（减少样本数量）。</p>
<p>在大多数场景下，Whisper有足够好的性能。它比RRDtool慢，主要原因是Whisper基于Python编写，这个性能差异很小，通常不需要考虑。</p>
<p>Whisper数据库以文件方式存放在磁盘上，扩展名.wsp。Carbon会为每个Metrics创建一个.wsp文件，路径的最后一节作为文件的basename，路径的其它部分形成目录层次。</p>
<div class="blog_h3"><span class="graybg">数据点</span></div>
<p>在Whisper中存储的每一个数值，称为数据点（Data Points）。</p>
<p>在磁盘上，数据点以大端、双精度浮点数存储。每个数据还附带一个时间戳信息，时间戳为1970-01-01到数据采集时间的秒数。</p>
<div class="blog_h3"><span class="graybg">归档</span></div>
<p>一个Whisper数据库文件可以包含一个或者多个“归档”，归档是数据文件中的逻辑段。</p>
<p>每个归档具有不同的数据分辨率（一定时间段内，数据点数量越多，则分辨率越高）。归档以最高分辨率（最小驻留时间） —— 最低分辨率（最长驻留时间）的顺序，在数据库文件中顺序排列。</p>
<p>为了精确的从高分辨率向低分辨率的归档聚合，高分辨率归档和它之后的低分辨率归档，其分辨率应当具有整除关系。例如，第一个归档分辨率为60秒/数据点，那么第二个归档可以是300秒/数据点，第三个归档可以是3600秒/数据点。</p>
<p>一个数据库的总计驻留时间（存放的数据点跨越的时间），由最长驻留时间的（最后一个）归档确定。因为之前的那些归档，时间区间都是它的子区间。</p>
<div class="blog_h3"><span class="graybg">聚合</span></div>
<p>把数据点转移到低分辨率归档时，面临着如何把多个数据点转变为单个数据点的问题。Whisper支持average、sum、last、max、min等聚合函数。</p>
<p>注意，这里的聚合和Carbon提供的前置聚合不是一回事。</p>
<div class="blog_h3"><span class="graybg">多归档的读写策略</span></div>
<p>当Whisper向一个多归档数据写入Metrics时，数据点将被同时写入到所有的归档中。这意味着聚合动作随时可能发生。</p>
<p>当从Whisper中获取数据时，第一个能完整覆盖所需区间的归档被使用。</p>
<div class="blog_h3"><span class="graybg">磁盘空间利用率问题</span></div>
<p>Whisper的磁盘利用效率不高，因为：</p>
<ol>
<li>每个数据点要附加一个时间戳信息</li>
<li>由于归档的时间区间有重叠，因此数据存在冗余</li>
<li>归档中的时间槽位（time-slots）总是占据着磁盘空间，不管有没有值存储在其中</li>
</ol>
<p>这些特征是故意的，主要出于性能方面的考虑。</p>
<div class="blog_h3"><span class="graybg">Whisper与RRD的区别</span></div>
<ol>
<li>RRD不能去填充以前的时间槽位。这意味着每一条数据都必须是更新的，不会“补录”</li>
<li>RRD不能很好的支持不规则的数据更新。如果RRD接收到一条数据，但是后续数据没有到来，则前一条数据可能丢失</li>
</ol>
<div class="blog_h1"><span class="graybg">安装与配置</span></div>
<div class="blog_h2"><span class="graybg">检查并安装依赖</span></div>
<div class="blog_h3"><span class="graybg">检查依赖</span></div>
<pre class="crayon-plain-tag">git clone https://github.com/graphite-project/graphite-web.git
graphite-web/check-dependencies.py
# 根据输出的提示来判断缺少哪些依赖，然后安装</pre>
<div class="blog_h3"><span class="graybg">安装依赖</span></div>
<pre class="crayon-plain-tag">yum -y install python-devel     # Carbon 依赖于 Python Development Headers

yum install pycairo             # Cairo库的Python绑定

pip install django              # Web框架
pip install django-tagging

pip install pytz

yum install fontconfig

yum install -y memcached        # 可选，缓存支持
pip install python-memcached    
                                # 可选，RDDTool
yum install cairo-devel libxml2-devel pango-devel pango libpng-devel freetype freetype-devel libart_lgpl-devel rrdtool-devel

pip install python-rrdtool      # 可选，RRD支持

pip install whitenoise          # 用于Web静态文件处理

yum install pyOpenSSL           # OpenSSL的Python绑定
pip install service_identity    # SSL相关</pre>
<div class="blog_h2"><span class="graybg">通过pip安装Craphite组件</span></div>
<pre class="crayon-plain-tag">pip install https://github.com/graphite-project/ceres/tarball/master
pip install whisper
pip install carbon
pip install graphite-web</pre>
<div class="blog_h3"><span class="graybg">设置目录权限</span></div>
<pre class="crayon-plain-tag">sudo groupadd graphite
sudo usermod -a -G graphite root
sudo usermod -a -G graphite apache
sudo chgrp -R graphite /opt/graphite/storage
sudo chmod -R 770 /opt/graphite/storage</pre>
<div class="blog_h2"><span class="graybg">默认安装布局</span></div>
<p>Whisper被安装到Python全局site-packages目录，另外两个Graphite组件安装到<pre class="crayon-plain-tag">/opt/graphite</pre>，该目录（记为<pre class="crayon-plain-tag">$GRAPHITE_ROOT</pre>  ）的布局如下：</p>
<table class=" fixed-word-wrap full-width">
<thead>
<tr>
<td style="width: 20%; text-align: center;">子目录</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>bin</td>
<td>一些脚本</td>
</tr>
<tr>
<td>conf</td>
<td>配置文件</td>
</tr>
<tr>
<td>lib</td>
<td>一些Python依赖库，Carbon的PYTHONPATH</td>
</tr>
<tr>
<td>storage</td>
<td>存储SQLite数据文件。该目录记为<pre class="crayon-plain-tag">$STORAGE_DIR</pre> </td>
</tr>
<tr>
<td>storage/log</td>
<td>Carbon和Graphite-web的日志</td>
</tr>
<tr>
<td>storage/rrd</td>
<td>待读取的RRD文件</td>
</tr>
<tr>
<td>storages/whisper</td>
<td>Whisper数据文件</td>
</tr>
<tr>
<td>webapp</td>
<td>
<p>Graphite-web的Web资源、PYTHONPATH</p>
</td>
</tr>
<tr>
<td>webapp/graphite</td>
<td>
<p>标准的Django工程结构<br />local_settings.py所在位置</p>
</td>
</tr>
<tr>
<td>webapp/content</td>
<td>静态Web资源</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">配置Graphite-web数据库</span></div>
<p>你需要让Graphite-web的底层框架Django执行数据库的初始化。此数据库被用来存放用户设置、仪表盘，以及支持事件功能。</p>
<p>默认情况下，Graphite-web使用位于$STORAGE_DIR/graphite.db的SQLite数据库。如果要运行多个Graphite-web实例，则必须使用MySQL等数据库以便多个实例可以共享数据。 </p>
<div class="blog_h3"><span class="graybg">配置SQLite</span></div>
<p>执行下面的命令初始化SQLite数据库：</p>
<pre class="crayon-plain-tag">PYTHONPATH=/opt/graphite/webapp django-admin.py migrate --settings=graphite.settings --run-syncdb
# 完毕后 $STORAGE_DIR/graphite.db自动创建</pre>
<p>Django应用需要在Web服务器中运行，Web服务器需要对SQLite数据文件有读写权限。假设你使用Apache2，运行Apache2的用户为apache，则需要执行：</p>
<pre class="crayon-plain-tag">sudo chgrp graphite /opt/graphite/storage/graphite.db</pre>
<div class="blog_h2"><span class="graybg">配置WebApp</span></div>
<div class="blog_h3"><span class="graybg">httplocal_settings.py</span></div>
<p>Graphite不建议修改settings.py，所有定制化的配置，都应该在此文件中进行。常用的设置项如下表：</p>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 30%; text-align: center;">设置项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>TIME_ZONE</td>
<td>时区，规范化的<a href="https://en.wikipedia.org/wiki/Tz_database#Names_of_time_zones">时区名称</a>。默认America/Chicago</td>
</tr>
<tr>
<td>DEBUG</td>
<td>是否启用Django错误页面。默认False</td>
</tr>
<tr>
<td>FLUSHRRDCACHED</td>
<td>如果设置，在从RRD文件获取数据前，执行<pre class="crayon-plain-tag">rrdtool flushcached</pre> <br />设置为rrdcached的地址或者Socket（例如unix:/var/run/rrdcached.sock）</td>
</tr>
<tr>
<td>MEMCACHE_HOSTS</td>
<td>如果设置，启用对计算出的目标、渲染过的图片的缓存。如果运行Graphite Web应用集群，则每个实例应当设置为一样的值<br />设置为memcached主机的数组，例如：['10.10.10.10:11211', '10.10.10.12:11211']</td>
</tr>
<tr>
<td>DEFAULT_CACHE_DURATION</td>
<td>数据、图片默认缓存时间。默认值60</td>
</tr>
<tr>
<td>DEFAULT_CACHE_POLICY</td>
<td>默认缓存策略。为元组的数组，每个元组指定最小查询时间、数据缓存时间<br />通过该设置，你可以让大的查询缓存更长时间。例如：<br />
<pre class="crayon-plain-tag">DEFAULT_CACHE_POLICY = [
    (0, 60), # 默认缓存60秒
    (7200, 120) # &gt;= 2小时以上时间范围的查询，缓存2分钟
] </pre>
</td>
</tr>
<tr>
<td>GRAPHITE_ROOT</td>
<td>Graphite的安装目录。默认/opt/graphite </td>
</tr>
<tr>
<td>CONF_DIR</td>
<td>额外Graphite-web配置文件目录。默认/opt/graphite/conf</td>
</tr>
<tr>
<td>STORAGE_DIR</td>
<td>存储目录。WHISPER_DIR、RRD_DIR、LOG_DIR、INDEX_FILE参照的基准目录</td>
</tr>
<tr>
<td>STATIC_ROOT</td>
<td>Graphite-web的静态文件目录。默认/opt/graphite/static<br />该目录一开始会不存在，你需要在设置STATIC_ROOT、STATIC_URL后执行：<br />
<pre class="crayon-plain-tag"># 如果报Unknown command: 'collectstatic' 说明INSTALLED_APPS中缺少
# django.contrib.staticfiles	
PYTHONPATH=/opt/graphite/webapp django-admin.py collectstatic 
    --noinput --settings=graphite.settings</pre></p>
<p>你还需要在Web服务器中把/static这个前缀映射到此目录，里Apache2为例：</p>
<pre class="crayon-plain-tag">Alias /static/ "/opt/graphite/static"</pre>
<p>如果你安装了whitenoise包，静态文件可以直接由Graphite webapp来处理，不通过Web服务器</p>
</td>
</tr>
<tr>
<td>DASHBOARD_CONF</td>
<td>仪表盘的配置文件。默认$CONF_DIR/dashboard.conf</td>
</tr>
<tr>
<td>GRAPHTEMPLATES_CONF</td>
<td>图形模板的配置文件。默认$CONF_DIR/graphTemplates.conf </td>
</tr>
<tr>
<td>WHISPER_DIR</td>
<td> Whisper数据文件目录。默认/opt/graphite/storage/whisper</td>
</tr>
<tr>
<td>RRD_DIR</td>
<td> RRD数据文件目录。默认/opt/graphite/storage/rrd</td>
</tr>
<tr>
<td>LOG_DIR</td>
<td> Graphite webapp的日志目录。默认$TORAGE_DIR/log/webapp</td>
</tr>
<tr>
<td>INDEX_FILE</td>
<td>搜索索引位置。默认/opt/graphite/storage/index<br />由build-index.sh脚本生成，运行Web应用的用户必须有写权限</td>
</tr>
<tr>
<td>URL_PREFIX</td>
<td>URL前缀</td>
</tr>
</tbody>
</table>
<p>我们的配置如下：</p>
<pre class="crayon-plain-tag"># 如果不设置，会导致报错：AttributeError: 'Settings' object has no attribute 'URL_PREFIX
URL_PREFIX = '/'</pre>
<div class="blog_h3"><span class="graybg">安装Apache和mod_wsgi</span></div>
<p>多种Web服务器可以用于运行基于Django的Web应用，这里我们选择Apache2。在CentOS下安装Apache2：</p>
<pre class="crayon-plain-tag">yum install httpd</pre>
<p>要让Apache2能够运行Python Web应用，需要安装模块mod_wsgi。参考<a href="https://blog.gmem.cc/django-study-note#mod-wsgi">Django学习笔记</a>完成mod_wsgi的构建与安装。</p>
<div class="blog_h3"><span class="graybg">配置Graphite虚拟主机</span></div>
<p>在/opt/graphite/examples/目录下，example-graphite-vhost.conf可以作为Apache虚拟主机的模板，复制该文件到/etc/httpd/conf.d/目录下，然后修改：</p>
<pre class="crayon-plain-tag">&lt;IfModule !wsgi_module.c&gt;
    LoadModule wsgi_module modules/mod_wsgi.so
&lt;/IfModule&gt;
WSGISocketPrefix run/wsgi
&lt;VirtualHost *:7767&gt;
        ServerName xcentos7.local
        DocumentRoot "/opt/graphite/webapp"
        ErrorLog /opt/graphite/storage/log/webapp/error.log
        CustomLog /opt/graphite/storage/log/webapp/access.log common
        WSGIDaemonProcess graphite processes=5 threads=5 display-name='%{GROUP}' inactivity-timeout=120
        WSGIProcessGroup graphite
        WSGIApplicationGroup %{GLOBAL}
        WSGIImportScript /opt/graphite/conf/graphite.wsgi process-group=graphite application-group=%{GLOBAL}
        WSGIScriptAlias / /opt/graphite/conf/graphite.wsgi
        Alias /content/ /opt/graphite/webapp/content/
        &lt;Directory /opt/graphite/webapp/content/&gt;
            Require all granted
        &lt;/Directory&gt;
        &lt;Directory /opt/graphite/conf/&gt;
            Require all granted
        &lt;/Directory&gt;
&lt;/VirtualHost&gt;</pre>
<div class="blog_h3"><span class="graybg">验证配置</span></div>
<p>如果配置没有问题，启动Web服务器后访问：<pre class="crayon-plain-tag">http://GRAPHITE_HOST:GRAPHITE_PORT/render</pre> ，会显示一张330x250大小的图片，上面写着No Data。</p>
<div class="blog_h2"><span class="graybg">配置Carbon</span></div>
<p>所有Carbon守护进程可以基于多种通信协议，监听时间序列数据，并且对数据进行不同的处理。</p>
<p>Carbon的配置文件位于/opt/graphite/conf/目录，默认情况下没有预置的配置文件，但是Graphite提供了若干配置文件样例。你可以复制这些配置文件并定制：</p>
<pre class="crayon-plain-tag">pushd /opt/graphite/conf
cp carbon.conf.example carbon.conf
cp storage-schemas.conf.example storage-schemas.conf</pre>
<div class="blog_h3"><span class="graybg">carbon.conf</span></div>
<p>这是主配置文件，为每个Carbon守护进程定义配置项。该配置文件按段区分不同守护进程的配置：</p>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 14%; text-align: center;">配置项段</td>
<td style="width: 30%; text-align: center;">配置项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td rowspan="17">[cache]   </td>
<td>
<p><span style="font-size: 8pt;">ENABLE_LOGROTATION</span></p>
</td>
<td>
<p> 是否启用每日的日志轮转，启用后每天创建一个日志</p>
</td>
</tr>
<tr>
<td><span style="font-size: 8pt;">USER </span></td>
<td>运行该进程的用户 </td>
</tr>
<tr>
<td><span style="font-size: 8pt;">MAX_CACHE_SIZE</span></td>
<td>内存中Metrics缓存的最大尺寸</td>
</tr>
<tr>
<td><span style="font-size: 8pt;">MAX_UPDATES_PER_SECOND</span></td>
<td>每秒执行whisper的update_many()调用的最大次数，对应磁盘IO的次数，该配置项避免过度的磁盘使用</td>
</tr>
<tr>
<td><span style="font-size: 8pt;">MAX_CREATES_PER_MINUTE</span></td>
<td>对每分钟最多创建的.wsp文件的个数进行软限制。超过限制的新Metrics对应的.wsp文件不会被创建，Metrics也被丢弃</td>
</tr>
<tr>
<td><span style="font-size: 8pt;">LINE_RECEIVER_INTERFACE</span></td>
<td rowspan="2">接收文本格式数据的监听端口，默认0.0.0.0:2003</td>
</tr>
<tr>
<td><span style="font-size: 8pt;">LINE_RECEIVER_PORT</span></td>
</tr>
<tr>
<td><span style="font-size: 8pt;">PICKLE_RECEIVER_INTERFACE<br /></span></td>
<td rowspan="2">接收Pickle格式数据的监听端口，默认0.0.0.0:2004</td>
</tr>
<tr>
<td><span style="font-size: 8pt;">PICKLE_RECEIVER_PORT</span></td>
</tr>
<tr>
<td><span style="font-size: 8pt;">ENABLE_UDP_LISTENER</span></td>
<td>是否启用UDP监听</td>
</tr>
<tr>
<td><span style="font-size: 8pt;">UDP_RECEIVER_INTERFACE</span></td>
<td rowspan="2">通过UDP接收数据的监听端口，默认0.0.0.0:2003</td>
</tr>
<tr>
<td><span style="font-size: 8pt;">UDP_RECEIVER_PORT</span></td>
</tr>
<tr>
<td><span style="font-size: 8pt;">LOG_LISTENER_CONNECTIONS</span></td>
<td>对成功的连接请求记录日志</td>
</tr>
<tr>
<td><span style="font-size: 8pt;">CACHE_QUERY_INTERFACE</span></td>
<td rowspan="2">缓存查询服务的监听端口，默认0.0.0.0：7002</td>
</tr>
<tr>
<td><span style="font-size: 8pt;">CACHE_QUERY_PORT</span></td>
</tr>
<tr>
<td><span style="font-size: 8pt;">USE_FLOW_CONTROL</span></td>
<td>是否进行流量控制。如果设置为True，那么达到MAX_CACHE_SIZE后，会暂停接收数据，直到缓存占用小于MAX_CACHE_SIZE的95%</td>
</tr>
<tr>
<td><span style="font-size: 8pt;">CACHE_WRITE_STRATEGY</span></td>
<td>按何种顺序将缓存从内存中移除，并写入到磁盘的策略：sorted、max、naive</td>
</tr>
<tr>
<td rowspan="13">[relay]</td>
<td><span style="font-size: 8pt;">LINE_RECEIVER_INTERFACE</span></td>
<td rowspan="2">
<p>接收文本格式数据的监听端口，默认0.0.0.0:2003</p>
</td>
</tr>
<tr>
<td><span style="font-size: 8pt;">LINE_RECEIVER_PORT</span></td>
</tr>
<tr>
<td><span style="font-size: 8pt;">PICKLE_RECEIVER_INTERFACE</span></td>
<td rowspan="2">接收Pickle格式数据的监听端口，默认0.0.0.0:2004</td>
</tr>
<tr>
<td><span style="font-size: 8pt;">PICKLE_RECEIVER_PORT</span></td>
</tr>
<tr>
<td><span style="font-size: 8pt;">LOG_LISTENER_CONNECTIONS</span></td>
<td>对成功的连接请求记录日志</td>
</tr>
<tr>
<td><span style="font-size: 8pt;">USER</span></td>
<td>运行该进程的用户</td>
</tr>
<tr>
<td><span style="font-size: 8pt;">RELAY_METHOD</span></td>
<td>
<p>设置为rules：则该进程可以代替carbon-cache.py，然后中继所有Metrics给多个作为后端的carbon-cache.py</p>
<p>设置为consistent-hashing：则依据DESTINATIONS定义的分片策略，分发Metrics给多个作为后端的carbon-cache.py</p>
</td>
</tr>
<tr>
<td><span style="font-size: 8pt;">REPLICATION_FACTOR</span></td>
<td>RELAY_METHOD=consistent-hashing时，可以指定N，从而把每个数据点分发到N台机器</td>
</tr>
<tr>
<td><span style="font-size: 8pt;">DESTINATIONS</span></td>
<td>转发的目标，每个目标的格式是IP:PORT<br />RELAY_METHOD=rules时relay-rules.conf每个servers都要在此字段定义</td>
</tr>
<tr>
<td><span style="font-size: 8pt;">MAX_DATAPOINTS_PER_MESSAGE</span></td>
<td>单个转发报文中包含的数据点的最大个数</td>
</tr>
<tr>
<td><span style="font-size: 8pt;">MAX_QUEUE_SIZE</span></td>
<td>待转发的队列最大包含多少数据点</td>
</tr>
<tr>
<td><span style="font-size: 8pt;">QUEUE_LOW_WATERMARK_PCT</span></td>
<td>队列低水位的百分比，0-1之间</td>
</tr>
<tr>
<td><span style="font-size: 8pt;">USE_FLOW_CONTROL</span></td>
<td>是否进行流量控制。如果设置为True，那么达到MAX_QUEUE_SIZE后，会暂停接收数据，直到转发队列低于QUEUE_LOW_WATERMARK_PCT</td>
</tr>
<tr>
<td rowspan="13">[aggregator]</td>
<td><span style="font-size: 8pt;"> LINE_RECEIVER_INTERFACE</span></td>
<td rowspan="2">接收文本格式数据的监听端口，默认0.0.0.0:2023</td>
</tr>
<tr>
<td><span style="font-size: 8pt;">LINE_RECEIVER_PORT</span></td>
</tr>
<tr>
<td><span style="font-size: 8pt;">PICKLE_RECEIVER_INTERFACE</span></td>
<td rowspan="2">接收Pickle格式数据的监听端口，默认0.0.0.0:2004</td>
</tr>
<tr>
<td><span style="font-size: 8pt;">PICKLE_RECEIVER_PORT</span></td>
</tr>
<tr>
<td><span style="font-size: 8pt;">LOG_LISTENER_CONNECTIONS</span></td>
<td>对成功的连接请求记录日志</td>
</tr>
<tr>
<td><span style="font-size: 8pt;">USER</span></td>
<td>运行该进程的用户</td>
</tr>
<tr>
<td><span style="font-size: 8pt;">FORWARD_ALL</span></td>
<td>如果设置为True，除了根据aggregation-rules.conf进行聚合外，还把原始数据转发给DESTINATIONS</td>
</tr>
<tr>
<td><span style="font-size: 8pt;">DESTINATIONS</span></td>
<td>聚合后的数据发送到的地方</td>
</tr>
<tr>
<td><span style="font-size: 8pt;">REPLICATION_FACTOR</span></td>
<td>如果设置为N，则把数据转发给N个DESTINATION</td>
</tr>
<tr>
<td><span style="font-size: 8pt;">MAX_QUEUE_SIZE</span></td>
<td>待转发的队列最大包含多少数据点</td>
</tr>
<tr>
<td><span style="font-size: 8pt;">USE_FLOW_CONTROL</span></td>
<td>是否进行流量控制。如果设置为True，那么达到MAX_QUEUE_SIZE后，会暂停接收数据，直到转发队列低于80%</td>
</tr>
<tr>
<td><span style="font-size: 8pt;">MAX_DATAPOINTS_PER_MESSAGE</span></td>
<td>单个转发报文中包含的数据点的最大个数</td>
</tr>
<tr>
<td><span style="font-size: 8pt;">MAX_AGGREGATION_INTERVALS</span></td>
<td>控制最多记住多少数据点，只有这些数据点才参与聚合<br />仅最近MAX_AGGREGATION_INTERVALS  * intervalSize秒内的数据点会被记住</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">storage-schemas.conf</span></div>
<p>这个配置文件用于定义Metrics的驻留率（ retention rates）——即Metrics的数据点（datapoints）以什么频率保存，保存多长时间。关于该配置文件，需要注意：</p>
<ol>
<li>该配置文件可以由多个段（Section）组成。每个段定义一个存储规则</li>
<li>这些规则按照从上到下的顺序对Metrics进行匹配，第一个匹配的规则对Metrics生效</li>
<li>匹配Metrics时，采用的是正则表达式</li>
<li>对于一个Metrics，其存储规则在接收到第一个数据时固化。因而修改此配置文件不会影响到既有的.wsp文件。要应用到既有的.wsp可以调用whisper-resize.py</li>
</ol>
<p>每个规则由三个部分组成：</p>
<pre class="crayon-plain-tag"># 段名称（规则名称），主要是文档用途。在匹配段的Metrics被创建时，日志creates.log中会显示此名称
[rulename]
# 匹配Metrics路径（Metrics的全限定名称，点号分隔）的正则式
# 举例：^servers\.www.*\.workers\.busyWorkers$
pattern=regex
# 驻留率表达式，数据点间隔:存留天数，时间后缀s,m,h,d,y分别表示秒、分、小时、天、年
# 举例：10s:14d 表示每个数据点表示10秒（相当于每10秒采集数据一次），并且存储14天的数据
retentions=retention rate</pre>
<p>注意，你可以指定多重驻留率表达式（retention rate），逗号分隔每个表达式。一般从最高精度:最短存留时间开始指定，直到最低精度:最长存留时间。例如：<pre class="crayon-plain-tag">15s:7d,1m:21d,15m:5y</pre> 表示7天内每15秒存留一个采样，而大于21天小于5年的则每15分钟一个采样。</p>
<p>设置多重驻留率表达式，可以在保存足够长时间的历史数据的前提下，尽量减少磁盘I/O和消耗的存储空间。当跨越驻留表达式的时间区间（上例中7天，21天）后，whisper会自动降低采样率（downsamples），<span style="background-color: #c0c0c0;">默认算法是取平均值</span>，可以通过storage-aggregation.conf定制聚合方式</p>
<div class="blog_h3"><span class="graybg">storage-aggregation.conf</span></div>
<p>这个配置文件定义如何在降低采样率（转换为低精度存储时） 如何对数据进行聚合。该文件的格式与storage-schemas.conf类似：</p>
<pre class="crayon-plain-tag">[rulename]
pattern = rexexp
# 0-1之间的浮点数，默认0.5。聚合区间内的值，至少多少比例为非空，聚合后的值才是非空
xFilesFactor = 0.5
# 数据聚合方式，默认average
aggregationMethod = average | sum | min | max | last</pre>
<p>同样的，修改此文件不会影响已经生成的.wsp文件， 要应用到既有的.wsp文件，可以调用whisper-set-aggregation-method.py。</p>
<div class="blog_h3"><span class="graybg">relay-rules.conf</span></div>
<p>这个配置文件指定中继规则——即需要把何种Metrics转发给何种后端。中继由Carbon的中继进程负责执行。该文件格式如下：</p>
<pre class="crayon-plain-tag">[rulename]
pattern = regex
servers = ip:port,ip:port,...</pre>
<div class="blog_h3"><span class="graybg">aggregation-rules.conf</span></div>
<p>该配置文件定义聚合出来的Metrics——由几个Metrics聚合而成的新的Metrics。聚合由Carbon的聚合进程负责执行。</p>
<p>注意：这里的聚合storage-aggregation.conf提及的聚合不同。后者用于单一Metrics的降低采样，而前者用于生成新的Metrics。</p>
<p>与其它配置文件不同，该文件一旦更改，立即生效。</p>
<p>该文件的格式如下：</p>
<pre class="crayon-plain-tag"># 捕获任何匹配input_pattern的Metrics，使用method聚合成新的Metrics：output_template
# frequency：每隔多久执行一次聚合
# method：可选sum/avg
output_template (frequency) = method input_pattern</pre>
<p>举例，假设你的Metrics的命名规则是：</p>
<pre class="crayon-plain-tag">&lt;env&gt;.applications.&lt;app&gt;.&lt;server&gt;.&lt;metric&gt;</pre>
<p>这是你可以配置如下聚合规则，来计算所有应用程序的请求的总数：</p>
<pre class="crayon-plain-tag">&lt;env&gt;.applications.&lt;app&gt;.all.requests (60) = sum &lt;env&gt;.applications.&lt;app&gt;.*.requests</pre>
<p>在此规则下，下面的Metrics：</p>
<pre class="crayon-plain-tag">test.applications.pems.221.request
test.applications.pems.6.request
test.applications.pems.5.request
test.applications.pems.201.request</pre>
<p>会每个60秒求和并生成 <pre class="crayon-plain-tag">test.applications.pems.all.request</pre> 的一个数据点。</p>
<div class="blog_h3"><span class="graybg">rewrite-rules.conf </span></div>
<p>该配置文件定义Metrics名称的改写规则。 与其它配置文件不同，该文件一旦更改，立即生效。该文件的格式如下：</p>
<pre class="crayon-plain-tag">[pre]
# pre段的规则，在接收到数据后立即执行改写
[post]
# post段的规则，在聚合完毕后执行改写
regex-pattern = replacement-text</pre>
<p> 举例：</p>
<pre class="crayon-plain-tag"># \1表示第一个捕获，即[a-z0-9]+
^collectd\.([a-z0-9]+)\. = \1.system.
# collectd.prod.cpu-0.idle-time 会被改写为 prod.system.cpu-0.idle-item</pre>
<div class="blog_h3"><span class="graybg">whitelist.conf和blacklist.conf </span></div>
<p>Carbon提供黑白名单功能。白名单：仅仅接受其中列出的Metrics；黑名单：拒绝其中列出Metrics。</p>
<p>要启用黑白名单功能，需要修改carbon.conf，设置<pre class="crayon-plain-tag">USE_WHITELIST = True</pre> </p>
<div class="blog_h2"><span class="graybg">启动Graphite</span></div>
<p>如何启动Graphite Webapp依赖于其运行的Web服务软件，以Apache为例：</p>
<pre class="crayon-plain-tag"># CentOS 7
systemctl restart httpd</pre>
<p>Carbon组件需要执行下面的命令来启动：</p>
<pre class="crayon-plain-tag">/opt/graphite/bin/carbon-cache.py start</pre>
<p>SQLite是一个“文件数据库”，不需要启动。如果Graphite Webapp使用其它数据库，例如MySQL，则需要启动之。 </p>
<div class="blog_h1"><span class="graybg">向Graphite发送数据点</span></div>
<div class="blog_h2"><span class="graybg">数据格式</span></div>
<p>你可以通过多种方式向Graphite（的Carbon组件）发送Metrics数据点。主要的三种数据格式是：Plaintext、Pickle、AMQP </p>
<div class="blog_h3"><span class="graybg">Plaintext协议</span></div>
<p>这种方式非常简单，你可以用如下格式来发送一条Metrics数据点：</p>
<pre class="crayon-plain-tag">&lt;metric path&gt; &lt;metric value&gt; &lt;metric timestamp&gt;
# 举例：
servers.zircon.cpu 85 1470303923</pre>
<p>出于测试目的，你可以直接通过命令向Carbon发送Metrics数据：</p>
<pre class="crayon-plain-tag">PORT=2003
SERVER=127.0.0.1
echo "servers.zircon.cpu 85 `date +%s`" | nc ${SERVER} ${PORT}</pre>
<div class="blog_h3"><span class="graybg">Pickle协议</span></div>
<p>Pickle是Python下的对象串行化框架，Pickle协议即它的串行化格式协议。使用该协议，你可以一次发送多个Metrics，并且串行化后的数据比较紧凑，因此Pickle协议性能更好。</p>
<p>下面是构建Pickle报文的示例代码：</p>
<pre class="crayon-plain-tag">listOfMetricTuples = [
    (path, (timestamp, value)),
    ...
]
payload = pickle.dumps(listOfMetricTuples, protocol=2)
header = struct.pack("!L", len(payload))
message = header + payload
# 然后通过Socket把message发送出去即可</pre>
<div class="blog_h2"><span class="graybg">客户端示例</span></div>
<div class="blog_h3"><span class="graybg">简单Pickle客户端</span></div>
<pre class="crayon-plain-tag">import pickle
import socket
import struct
import sys
import time
from time import sleep

import psutil


def get_cpu_load():
    load = psutil.cpu_percent()
    print load
    return load


if __name__ == '__main__':
    CARBON_HOST = '172.16.87.132'
    CARBON_PORT = 2004
    s = socket.socket()
    try:
        s.connect((CARBON_HOST, CARBON_PORT))
        while True:
            data = [
                ('servers.zircon.cpu', (time.time(), get_cpu_load()))
            ]
            pkg = pickle.dumps(data, 1)
            s.sendall(struct.pack('!L', len(pkg)))
            s.sendall(pkg)
            sleep(5)
    except socket.error:
        raise SystemExit("Failed to connect to %(host)s:%(port)" % {'host': CARBON_HOST, 'port': CARBON_PORT})
    except KeyboardInterrupt:
        sys.stderr.write("\nExiting on CTRL-C\n")
        sys.exit(0) </pre>
<div class="blog_h1"><span class="graybg">从Graphite获取数据</span></div>
<p>要从Graphite获取数据用于展示，可以使用Graphite Webapp暴露的Render URL API（RUA）。你可以通过</p>
<pre class="crayon-plain-tag">http://GRAPHITE_HOST:GRAPHITE_PORT/render</pre>
<p>访问此API。要向RUA传递参数，可以使用URL请求参数方式：<pre class="crayon-plain-tag">&amp;name=value</pre> 。注意大部分的参数名、函数名是大小写敏感的。</p>
<p>下面列出几个RUA的URL示例：</p>
<pre class="crayon-plain-tag"># Zircon的CPU负载图，获取800x600的图片
http://graphite/render?target=servers.zircon.cpu&amp;height=800&amp;width=600
# 最近12小时，所有服务器的CPU负载平均值
http://graphite/render?target=averageSeries(servers.*.load)&amp;from=-12hours
# 获取原始数据而不是图片，JSON格式
http://graphite/render?target=servers.zircon.cpu&amp;format=json</pre>
<div class="blog_h2"><span class="graybg">target参数</span></div>
<p>该参数用来指定从何处获取数据，你可以指定：</p>
<ol>
<li>单个Metrics路径</li>
<li>带有通配符的的Metrics路径，匹配多个Metrics</li>
<li>函数调用，针对作为入参的Metrics进行各种转换、合并操作</li>
</ol>
<div class="blog_h3"><span class="graybg">通配符</span></div>
<p>你可以在路径中使用三种风格的通配符：</p>
<ol>
<li><pre class="crayon-plain-tag">*</pre> 可以匹配0-N个字符，例如<pre class="crayon-plain-tag">servers.dev-*.cpu</pre> ，可以匹配所有开发服务器的CPU负载Metrics。</li>
<li><pre class="crayon-plain-tag">[...]</pre> 可以匹配列表中枚举的单个字符，例如<pre class="crayon-plain-tag">servers.dev-[a-z0-9].cpu</pre> ，可以匹配dev-0、dev-1等服务器的CPU负载Metrics。</li>
<li><pre class="crayon-plain-tag">{...}</pre> 可以匹配列表中枚举的单个字符串，例如<pre class="crayon-plain-tag">servers.{dev-0,dev-1}.cpu</pre> ，匹配dev-0、dev-2的CPU负载Metrics。</li>
</ol>
<p>注意：所有通配符都<span style="background-color: #c0c0c0;">不能跨越点号</span>。 </p>
<div class="blog_h3"><span class="graybg">template函数</span></div>
<p>你可以指定target为template函数调用，从而在Metrics路径中使用变量，例如：</p>
<pre class="crayon-plain-tag"># $varname 用来声明变量占位符
# template[varname]参数用来传递变量值
&amp;target=template(servers.$servername.cpu)&amp;template[servername]=zircon

# 可以使用数字代替变量名
&amp;target=template(servers.$1.cpu)&amp;template[1]=zircon

# template可以内嵌其它函数
&amp;target=template(constantLine($number))&amp;template[number]=123</pre>
<div class="blog_h3"><span class="graybg">所有函数</span></div>
<p>可用的函数较多，这里不一一列举说明，参见<a href="http://graphite.readthedocs.io/en/latest/functions.html">官方文档</a>。</p>
<div class="blog_h2"><span class="graybg">from/until参数</span></div>
<p>这两个可选参数用来指定相对或者绝对的时间区间（time period）。from表示区间起点，如果忽略，默认值是24小时之前；until表示区间终点，如果或略，默认值是当前时间点。</p>
<div class="blog_h3"><span class="graybg">相对时间</span></div>
<p>如果要使用相对时间，需要加上<pre class="crayon-plain-tag">-</pre> 前缀（负号），后面跟着数值和时间单位。时间单位包括：</p>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">时间单位</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>s</td>
<td>秒</td>
</tr>
<tr>
<td>min</td>
<td>分钟</td>
</tr>
<tr>
<td>h</td>
<td>小时</td>
</tr>
<tr>
<td>d</td>
<td>天</td>
</tr>
<tr>
<td>w</td>
<td>周</td>
</tr>
<tr>
<td>mon</td>
<td>月（30天）</td>
</tr>
<tr>
<td>y</td>
<td>年（365天）</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">绝对时间</span></div>
<p>你可以指定<pre class="crayon-plain-tag">HH:MM_YYYYMMDD</pre> 、<pre class="crayon-plain-tag">YYYYMMDD</pre> 、<pre class="crayon-plain-tag"> MM/DD/YY</pre> 等格式的时间绝对值。</p>
<div class="blog_h2"><span class="graybg">format参数</span></div>
<p>该参数用来指定要获取的数据的格式。 支持以下取值：</p>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 15%; text-align: center;">格式</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>png</td>
<td>根据指定的width、height，直接把数据渲染为PNG图片</td>
</tr>
<tr>
<td>raw</td>
<td>原始数据，分为多行，每行格式为：<br />
<pre class="crayon-plain-tag">&lt;target name&gt;,&lt;start timestamp&gt;,&lt;end timestamp&gt;,&lt;series step&gt;|[data]*</pre></p>
<p> 示例：</p>
<pre class="crayon-plain-tag">entries,1311836008,1311836013,1|1.0,2.0,3.0,5.0,6.0</pre>
</td>
</tr>
<tr>
<td>csv</td>
<td>基于逗号分隔符的格式，每行表示一个数据点。示例：<br />
<pre class="crayon-plain-tag">entries,2011-07-28 01:53:28,1.0
entries,2011-07-28 01:53:29,2.0
entries,2011-07-28 01:53:30,3.0
entries,2011-07-28 01:53:31,5.0
entries,2011-07-28 01:53:32,6.0</pre>
</td>
</tr>
<tr>
<td>json</td>
<td>
<p>JSON数组格式，示例：
<pre class="crayon-plain-tag">[{
  "target": "entries",
  "datapoints": [
    [1.0, 1311836008],
    [2.0, 1311836009],
    [3.0, 1311836010],
    [5.0, 1311836011],
    [6.0, 1311836012]
  ]
}]</pre>
<p>可以和jsonp参数联用，以便把数据包装成函数调用，进行跨域请求</p>
<p>可以和maxDataPoints参数联用，限定最大的数据点个数。超过数量的数据点将被压缩掉</p>
<p>可以和noNullPoints参数联用，移除所有Null值的数据点</p>
</td>
</tr>
<tr>
<td>svg</td>
<td>渲染为SVG图片格式</td>
</tr>
<tr>
<td>pdf</td>
<td>渲染为PDF文档</td>
</tr>
<tr>
<td>dygraph</td>
<td>dygraphs是一个快速、灵活的JavaScript图表（Chart）库。该格式返回dygraphs支持的数据格式。示例：<br />
<pre class="crayon-plain-tag">{
  "labels" : [
    "Time",
    "entries"
  ],
  "data" : [
    [1468791890000, 0.0],
    [1468791900000, 0.0]
  ]
}</pre>
</td>
</tr>
<tr>
<td>rickshaw</td>
<td>rickshaw是一个简单的JavaScript图表库。该格式返回rickshaw支持的数据格式。示例：<br />
<pre class="crayon-plain-tag">[{
  "target": "entries",
  "datapoints": [{
    "y": 0.0,
    "x": 1468791890
  }, {
    "y": 0.0,
    "x": 1468791900
  }]
}]</pre>
</td>
</tr>
<tr>
<td>pickle</td>
<td>返回Pickle串行化格式，设置MIME类型为application/pickle。反串行化后的对象示例：<br />
<pre class="crayon-plain-tag">[
  {
    'name' : 'summarize(test.data, "30min", "sum")',
    'start': 1335398400,
    'end'  : 1335425400,
    'step' : 1800,
    'values' : [None, None, 1.0, None],
  }
]</pre>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">图形参数</span></div>
<p>你可以指定多个参数，来控制生成的图形的样式：</p>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 22%; text-align: center;">参数</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>areaAlpha</td>
<td>启用areaMode时，填充区域的透明度。0-1之间的浮点数</td>
</tr>
<tr>
<td>areaMode</td>
<td>填充曲线与X轴之间的区域，形成Area图，可以取值：<br />none 不进行填充<br />first 填充第一个目标<br />all  填充所有目标<br />stacked 堆叠模式，填充所有目标，一个目标的取值为前面所有其它目标的取值+该目标的取值</td>
</tr>
<tr>
<td>bgcolor</td>
<td>背景颜色。示例：<br />
<pre class="crayon-plain-tag"># 颜色名称
&amp;bgcolor=blue
# HEX代码
&amp;bgcolor=2222FF
# HEX代码，包含透明度
&amp;bgcolor=5522FF60</pre>
</td>
</tr>
<tr>
<td>cacheTimeout</td>
<td>被渲染出的图形，其有效缓存时间</td>
</tr>
<tr>
<td>colorList</td>
<td>多个Target时，每个Target的颜色，逗号分隔颜色代码</td>
</tr>
<tr>
<td>drawNullAsZero</td>
<td>是否把空值渲染为0</td>
</tr>
<tr>
<td>fontBold</td>
<td>是否使用粗体</td>
</tr>
<tr>
<td>fontItalic</td>
<td>是否使用斜体</td>
</tr>
<tr>
<td>fontName</td>
<td>字体名称，该字体必须安装在Graphite服务器上</td>
</tr>
<tr>
<td>fontSize</td>
<td>字体大小，大于1的浮点数</td>
</tr>
<tr>
<td>graphOnly</td>
<td>是否不显示网格线、X/Y轴和图例</td>
</tr>
<tr>
<td>graphType</td>
<td>图表类型，line或者pie</td>
</tr>
<tr>
<td>hideLegend</td>
<td>是否隐藏图例</td>
</tr>
<tr>
<td>hideAxes</td>
<td rowspan="3">是否隐藏X/Y轴</td>
</tr>
<tr>
<td>hideXAxis</td>
</tr>
<tr>
<td>hideYAxis</td>
</tr>
<tr>
<td>hideGrid</td>
<td>是否隐藏网格线</td>
</tr>
<tr>
<td>height</td>
<td rowspan="2">图形的高度、宽度，单位像素</td>
</tr>
<tr>
<td>width</td>
</tr>
<tr>
<td>leftColor</td>
<td>在双Y轴模式下，设置与左轴关联的Metrics的颜色</td>
</tr>
<tr>
<td>rightColor</td>
<td>在双Y轴模式下，设置与右轴关联的Metrics的颜色</td>
</tr>
<tr>
<td>leftDashed</td>
<td>在双Y轴模式下，是否以虚线绘制与左轴关联的Metrics</td>
</tr>
<tr>
<td>rightDashed</td>
<td>在双Y轴模式下，是否以虚线绘制与右轴关联的Metrics</td>
</tr>
<tr>
<td>leftWidth</td>
<td>在双Y轴模式下，设置与左轴关联的Metrics的线条宽度</td>
</tr>
<tr>
<td>rightWidth</td>
<td>在双Y轴模式下，设置与右轴关联的Metrics的线条宽度</td>
</tr>
<tr>
<td>lineMode</td>
<td>设置线条绘制的行为：<br />slope：从一个数据点向下一个数据点绘制斜线，Null值的区间不被绘制<br />connected：与slope类似，但是数据点总是被连接起来，无论它们之间是否存在Null值<br />staircase：绘制直方图</td>
</tr>
<tr>
<td>lineWidth</td>
<td>线条的宽度</td>
</tr>
<tr>
<td>majorGridLineColor</td>
<td>网格线主色</td>
</tr>
<tr>
<td>minorGridLineColor</td>
<td>网格线从色</td>
</tr>
<tr>
<td>minorY</td>
<td>每两个网格主线之间，有几个从线，Y方向</td>
</tr>
<tr>
<td>margin</td>
<td>图形四周的边距</td>
</tr>
<tr>
<td>maxDataPoints</td>
<td>使用</td>
</tr>
<tr>
<td>minXStep</td>
<td>
<p>两个连续的数据点之间，间隔的最小像素</p>
<p>如果数据点过多，则压缩，以满足此配置</p>
</td>
</tr>
<tr>
<td>noCache</td>
<td>禁止图片缓存</td>
</tr>
<tr>
<td>pieLabels</td>
<td>饼图标签如何显示，horizontal或者rotated</td>
</tr>
<tr>
<td>pieMode</td>
<td>饼图聚合方式：<br />average，取series中非空数据点的平均值<br />maximum，取series中非空数据点的最大值<br />minimum，取series中非空数据点的最小值</td>
</tr>
<tr>
<td>valueLabels</td>
<td>如何显示饼图分块的标签：<br />none，不显示<br />numbers，显示原始值<br />percent，显示百分比</td>
</tr>
<tr>
<td>valueLabelsColor</td>
<td>如何显示饼图分块的标签的颜色</td>
</tr>
<tr>
<td>valueLabelsMin</td>
<td>饼图中，分块占比小于此数值的分块，不显示其标签</td>
</tr>
<tr>
<td>title</td>
<td>在图形顶端显示的标题</td>
</tr>
<tr>
<td>vtitle</td>
<td>Y轴标题，垂直显示</td>
</tr>
<tr>
<td>vtitleRight</td>
<td>双Y轴模式下，右Y轴的标题</td>
</tr>
<tr>
<td>tz</td>
<td>用于显示时间值的时区</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">展示Graphite数据</span></div>
<p>上一章内容我们讨论了如何获取Graphite数据。通过Render URL API，我们不但可以获得文本数据，还可以直接获得渲染好的图片。这意味着通过Render URL API本身就可以实现Metrics的渲染，你只需要把生成的图片嵌入到自己的应用程序中即可。</p>
<p>Graphite Webapp本身提供了基于ExtJS的一个管理界面，你可以通过<pre class="crayon-plain-tag">http://GRAPHITE_HOST:GRAPHITE_PORT/admin</pre> 浏览Metrics。</p>
<p>Graphite生成的Metrics曲线的图片，不是非常美观，而静态图片也缺乏交互性。因此，实际项目中常常结合使用第三方基于JavaScript的Charts库来做展示，例如：</p>
<ol>
<li><a href="http://grafana.org/">Grafana</a>：UI比较绚丽，支持设计仪表盘、时间区间联动。参见：<a href="/time-series-data-renderering-with-grafana">使用Grafana展示时间序列数据</a></li>
<li><a href="http://jondot.github.io/graphene/">Graphene</a>：一个较为简单的，基于D3.js和Backbone.js的Graphite仪表盘工具</li>
</ol>
<div class="blog_h1"><span class="graybg">Graphite事件</span></div>
<p>除了简单的，基于Key/Value的Metrics数据，Graphite还可以存储、展示随机出现的数据——事件。</p>
<p>事件不适合存储在Whisper中，因此它被存储在Graphite的Webapp的数据库中（默认使用SQLite）。</p>
<div class="blog_h2"><span class="graybg">发布事件</span></div>
<p>通过向<pre class="crayon-plain-tag">http://GRAPHITE_HOST:GRAPHITE_PORT/events/</pre> 发送POST请求，即可发布Graphite事件。事件使用JSON格式编码在请求体中：</p>
<pre class="crayon-plain-tag">{ 
    "what": "事件类型", 
    "tags": "标签",
    "data": "事件相关的数据" 
}</pre>
<div class="blog_h2"><span class="graybg">查询事件</span></div>
<p>指定target为<pre class="crayon-plain-tag">event(*tags)</pre> 函数调用，即可通过<pre class="crayon-plain-tag">/render</pre> 查询事件，例如</p>
<pre class="crayon-plain-tag">[
   {
      "target" : "events(mytag)",
      "datapoints" : [
         [
            1,
            1388966651
         ],
         [
            3,
            1388966652
         ]
      ]
   }
]</pre>
<p>你也可以通过<pre class="crayon-plain-tag">/render/events/get_data</pre> 获得原始的事件数据，例如：</p>
<pre class="crayon-plain-tag">[
   {
      "when" : 1392046352,
      "tags" : "mytag",
      "data" : "...",
      "id" : 2,
      "what" : "Event - deploy"
   }
] </pre>
<div class="blog_h1"><span class="graybg">常见问题</span></div>
<div class="blog_h3"><span class="graybg">AttributeError: 'WSGIRequest' object has no attribute 'REQUEST'</span></div>
<p>Django的request对象曾经有一个属性REQUEST，用来获得通过GET或者POST请求传递的请求参数，在1.9版本中此属性已经移除。</p>
<p>Graphite代码没有即时更新，存在不兼容的问题，修改一下即可：</p>
<pre class="crayon-plain-tag">def parseOptions(request):
   queryParams = request.GET # request.REQUEST已经被移除</pre>
<p>还有很多其它views.py存在同样的问题，最好搜索一下一并修改。如果觉得麻烦可以安装兼容版本的Django：</p>
<pre class="crayon-plain-tag">pip install django==1.8.14</pre>
<p>&nbsp;</p>
<p>&nbsp;</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/graphite-study-note">Graphite学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/graphite-study-note/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>MongoDB学习笔记</title>
		<link>https://blog.gmem.cc/mongodb-study-note</link>
		<comments>https://blog.gmem.cc/mongodb-study-note#comments</comments>
		<pubDate>Sat, 23 May 2015 03:25:04 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[BigData]]></category>
		<category><![CDATA[Database]]></category>
		<category><![CDATA[NoSQL]]></category>
		<category><![CDATA[学习笔记]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=14820</guid>
		<description><![CDATA[<p>简介 MongoDB是一个开源的文档数据库（Document Database），具有高性能、高可用性、自动化的可扩容性。 高性能的持久化能力，主要体现在： 对内嵌数据模型的支持，减少了数据库系统的I/O活动 支持索引，加快了查询速度。可以包含来自内嵌文档、数组的索引键 高可用性由MongoDB的数据复制机制 —— 复制集（Replica set，一系列持有相同数据集的MongoDB实例）提供，主要体现在： 自动的故障转移 数据冗余 水平可扩容性主要体现在： 分片（Sharding）机制，在集群中分布数据 从3.4开始，支持基于分片键（Shard key）来划分数据区域（Zone）。在平衡化的集群中，MongoDB把读写操作定向到包含目标数据的区域中 所谓文档数据库，是指数据库中的每一条记录是一个“文档”。MongoDB的记录（文档）格式类似于JSON，由一系列字段组成，每个字段的值可以是文档、文档的数组、简单的值。文档格式的优势在于： 文档很自然的对应了主流编程语言中的原生数据结构（对象） 内嵌的文档、数组避免了关系型数据库昂贵的Join操作 由于支持动态Schema（动态字段），因此对动态的支持很轻松 MongoDB提供了富查询语言（Rich Query <a class="read-more" href="https://blog.gmem.cc/mongodb-study-note">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/mongodb-study-note">MongoDB学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">简介</span></div>
<p>MongoDB是一个开源的文档数据库（Document Database），具有高性能、高可用性、自动化的可扩容性。</p>
<p>高性能的持久化能力，主要体现在：</p>
<ol>
<li>对内嵌数据模型的支持，减少了数据库系统的I/O活动</li>
<li>支持索引，加快了查询速度。可以包含来自内嵌文档、数组的索引键</li>
</ol>
<p>高可用性由MongoDB的数据复制机制 —— 复制集（Replica set，一系列持有相同数据集的MongoDB实例）提供，主要体现在：</p>
<ol>
<li>自动的故障转移</li>
<li>数据冗余</li>
</ol>
<p>水平可扩容性主要体现在：</p>
<ol>
<li>分片（Sharding）机制，在集群中分布数据</li>
<li>从3.4开始，支持基于分片键（Shard key）来划分数据区域（Zone）。在平衡化的集群中，MongoDB把读写操作定向到包含目标数据的区域中</li>
</ol>
<p>所谓文档数据库，是指数据库中的每一条记录是一个“文档”。MongoDB的记录（文档）格式类似于JSON，由一系列字段组成，每个字段的值可以是文档、文档的数组、简单的值。文档格式的优势在于：</p>
<ol>
<li>文档很自然的对应了主流编程语言中的原生数据结构（对象）</li>
<li>内嵌的文档、数组避免了关系型数据库昂贵的Join操作</li>
<li>由于支持动态Schema（动态字段），因此对动态的支持很轻松</li>
</ol>
<p>MongoDB提供了富查询语言（Rich Query Language），除了支持读写操作以外，还支持数据聚合、文本搜索、地理空间查询。</p>
<p>MongoDB支持可拔插的存储引擎，你可以自己依据API开发存储引擎，或者使用开箱即用的WiredTiger、MMAPv1</p>
<div class="blog_h1"><span class="graybg">基础知识</span></div>
<div class="blog_h2"><span class="graybg">数据库-集合-文档</span></div>
<p>MongoDB存储记录 —— <span style="background-color: #c0c0c0;"><a href="http://bsonspec.org">BSON</a>文档（JSON的二进制形式）</span>到集合（Collection）中，数据库中包含多个集合。集合的概念类似于RDBMS的表。</p>
<p>要指定使用的数据库，可以通过use语句：</p>
<pre class="crayon-plain-tag">use newdb
# 下面的命令列出现有的数据库 
show dbs</pre>
<p>如果newdb不存在，MongoDB会在你<span style="background-color: #c0c0c0;">第一次向其中存储数据时自动创建</span>。类似的，使用不存在的集合时，也会自动的创建：</p>
<pre class="crayon-plain-tag">// 执行下面的语句时，会自动创建数据库newdb和集合newcoll
db.newcoll.insertOne({x:1});

// 你也可以显式的创建集合，并提供参数，例如最大尺寸、文档验证规则
db.createCollection()</pre>
<div class="blog_h3"><span class="graybg">文档验证</span></div>
<p>在3.2版本之前，集合中存放的文档的Schema是任意的。3.2开始，你可以提供文档验证规则（Document Validation Rules）来限制某个集合中文档的结构。插入/更新文档时，必须满足规则。</p>
<div class="blog_h3"><span class="graybg">文档的其它用途</span></div>
<p>除了定义数据记录之外，MongoDB在很多地方使用文档结构，包括但是不限于：</p>
<ol>
<li>查询过滤器</li>
<li>更新规格文档（update specifications documents）</li>
<li>索引规格文档（update specifications documents）</li>
</ol>
<div class="blog_h2"><span class="graybg">BSON</span></div>
<p>BSON类似于JSON，但是它的字段具有更丰富的类型，例如：</p>
<pre class="crayon-plain-tag">var mydoc = {
    // 类似于UUID，但是支持快速生成，且有序
    _id: ObjectId( "5099803df3f4948bd2f98391" ),
    // 内嵌文档
    name: { first: "Alan", last: "Turing" },
    // 日期
    birth: new Date( 'Jun 23, 1912' ),
    death: new Date( 'Jun 07, 1954' ),
    // 数组
    contribs: [ "Turing machine", "Turing test", "Turingery" ],
    views: NumberLong( 1250000 )
}</pre>
<p>BSON字段的命名限制：</p>
<ol>
<li>名称<span style="background-color: #c0c0c0;">_id保留用作主键</span>，即指必须在集合中唯一、不可变。其类型可以是除了数组之外的任何类型</li>
<li><span style="background-color: #c0c0c0;">字段名不得以 $开头</span></li>
<li>字段名不得包含 . 号</li>
<li>字段名不得包含空字符（\0）</li>
</ol>
<p><span style="background-color: #c0c0c0;">BSON支持重名字段</span>，但是大部分MongoDB客户端使用哈希呈现文档，哈希不支持重名key。MongoDB进程产生的某些文档会使用重名字段，但是绝不会向用户定义的文档中添加重名字段。</p>
<p>对于被索引集合（Indexed collections），被索引的字段的值的长度，受到最大索引键长度的约束。</p>
<p>要访问内嵌文档、数组的元素，需要使用点号标记：</p>
<pre class="crayon-plain-tag">mydoc.name.first
mydoc.contribs.2</pre>
<p><span style="background-color: #c0c0c0;">BSON文档的最大尺寸是16MB</span>。要存储更大的文档，可以使用GridFS API。</p>
<p>文档中字段的顺序，<span style="background-color: #c0c0c0;">和插入文档时指定的顺序一致</span>，除了两点例外：</p>
<ol>
<li>_id总是作为第一个字段</li>
<li>重命名字段名的操作可能导致文档中的字段重排序。从2.6开始，MongoDB积极的尝试保持字段顺序</li>
</ol>
<div class="blog_h3"><span class="graybg">BSON数据类型</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 15%; text-align: center;">类型</td>
<td style="width: 10%; text-align: center;">序号</td>
<td style="width: 15%; text-align: center;">别名</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>Double</td>
<td>1</td>
<td>double</td>
<td> </td>
</tr>
<tr>
<td>String</td>
<td>2</td>
<td>string</td>
<td>
<p>BSON字符串以UTF-8格式编码。各语言的驱动将语言内部字符串编码为UTF-8再进行存储</p>
<p>MongoDB的$regex查询支持在正则式中使用UTF-8字符</p>
<p>由于sort()操作使用C的strcmp函数，某些字符的排序处理可能不正确</p>
</td>
</tr>
<tr>
<td>Object</td>
<td>3</td>
<td>object</td>
<td> </td>
</tr>
<tr>
<td>Array</td>
<td>4 </td>
<td>array</td>
<td> </td>
</tr>
<tr>
<td>Binary data</td>
<td>5 </td>
<td>binData</td>
<td> </td>
</tr>
<tr>
<td>Undefined</td>
<td>6 </td>
<td>undefined</td>
<td>废弃</td>
</tr>
<tr>
<td>ObjectId</td>
<td>7 </td>
<td>objectId</td>
<td>
<p>较小、很可能唯一、快速生成、有序的标识符类型。由12字节组成：</p>
<ol>
<li>4个字节为UNIX时间戳（秒）。可以使用ObjectId.getTimestamp()获得</li>
<li>3个字节的机器识别号</li>
<li>2字节的进程标识符</li>
<li>3字节的计数器，从随机数开始</li>
</ol>
<p>对此类字段进行排序，粗略的等同于按创建时间排序</p>
</td>
</tr>
<tr>
<td>Boolean</td>
<td>8 </td>
<td>bool</td>
<td> </td>
</tr>
<tr>
<td>Date</td>
<td>9 </td>
<td>date</td>
<td>
<p>BSON日期类型是一个64位整数，表示UNIX时间戳（毫秒），支持表示最近29000万年时间范围</p>
</td>
</tr>
<tr>
<td>Null</td>
<td>10 </td>
<td>null</td>
<td> </td>
</tr>
<tr>
<td>Regular Expr</td>
<td>11 </td>
<td>regex</td>
<td> </td>
</tr>
<tr>
<td>DBPointer</td>
<td>12 </td>
<td>dbPointer</td>
<td>废弃 </td>
</tr>
<tr>
<td>JavaScript</td>
<td>13 </td>
<td>javascript</td>
<td> </td>
</tr>
<tr>
<td>Symbol</td>
<td>14 </td>
<td>symbol</td>
<td>废弃</td>
</tr>
<tr>
<td>JavaScript (with scope)</td>
<td>15 </td>
<td>javascriptWithScope</td>
<td> </td>
</tr>
<tr>
<td>32-bit integer</td>
<td>16 </td>
<td>int</td>
<td> </td>
</tr>
<tr>
<td>Timestamp</td>
<td>17 </td>
<td>timestamp</td>
<td>
<p>特殊的类型，供MongoDB内部使用，与Date类型不同，它包含64个位：</p>
<ol>
<li>前32位是UNIX时间戳（秒）</li>
<li>后32位是秒内递增的计数器</li>
</ol>
<p>在单个mongod实例内，每个时间戳都是唯一的</p>
<p>在主从复制时，oplog包含一个ts字段，其类型是timestamp </p>
<p>当你插入空的Timestamp字段到顶级文档中时，MongoDB服务器自动将其替换为当前时间戳：</p>
<pre class="crayon-plain-tag">db.test.insertOne( { ts: new Timestamp() } );
db.test.find()
# { "_id" : ObjectId("542c2b97bac0595474108b48"), 
#   "ts" : Timestamp(1412180887, 1) }</pre>
</td>
</tr>
<tr>
<td>64-bit integer</td>
<td>18 </td>
<td>long</td>
<td> </td>
</tr>
<tr>
<td>Decimal128</td>
<td>19 </td>
<td>decimal</td>
<td>3.4引入</td>
</tr>
<tr>
<td>Min key</td>
<td>-1 </td>
<td>minKey</td>
<td> </td>
</tr>
<tr>
<td>Max key</td>
<td>127 </td>
<td>maxKey </td>
<td> </td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">_id</span></div>
<p>集合中的每一个文档，都必须具有作为主键的_id字段。
<p>如果用户没有指定该字段，MongoDB驱动会自动生成一个ObjectId类型的_id，如果驱动没有生成_id则mongod会生成ObjectId类型的_id。此规则适用于插入操作，以及指定了upsert: true的更新操作。</p>
<p>在创建集合的过程中，MongoDB在_id上创建唯一性索引。</p>
<p>你可以考虑使用以下数据类型作为_id：</p>
<ol>
<li>ObjectId类型</li>
<li>可以考虑使用自然键，可以节省空间并减少索引</li>
<li>使用自增长数字</li>
<li>在应用程序中生成UUID，可以保存为BinData以节省空间</li>
</ol>
<div class="blog_h3"><span class="graybg">比较和排序</span></div>
<p>当比较不同BSON类型的值时，MongoDB使用如下（从低到高）比较顺序：MinKey、Null、Numbers (ints, longs, doubles, decimals)、Symbol, String、Object、Array、BinData、ObjectId、Boolean、Date、Timestamp、Regular Expression、MaxKey (internal type)</p>
<p>在比较时，MongoDB把某些BSON类型当做一种类型看待，例如各种数字类型。</p>
<p>比较字符串时，默认使用简单的二进制比较的方式。从3.4开始，支持所谓排序规则（Collation） —— 用于指定语言相关（是否大小写敏感、是否有声调）的排序算法。排序规则的规格如下：</p>
<pre class="crayon-plain-tag">{
   locale: &lt;string&gt;,  // 该字段必须
   caseLevel: &lt;boolean&gt;,
   caseFirst: &lt;string&gt;,
   strength: &lt;int&gt;,
   numericOrdering: &lt;boolean&gt;,
   alternate: &lt;string&gt;,
   maxVariable: &lt;string&gt;,
   backwards: &lt;boolean&gt;
}</pre>
<p>比较数组时，小于比较/升序排序基于数组的最小元素进行，大于比较/降序排序基于数组的最大元素进行。</p>
<p>比较BinData时，按照以下顺序进行：</p>
<ol>
<li>比较数据的长度</li>
<li>根据BSON单字节子类型比较</li>
<li>执行逐字节比较 </li>
</ol>
<div class="blog_h2"><span class="graybg">视图</span></div>
<p>从3.4版本开始，MongoDB支持在现有的集合、其它视图之上，创建只读的视图（View）。</p>
<p>要创建视图，你可以使用create命令，指定viewOn、pipeline选项，可选的，指定一个collation选项：</p>
<pre class="crayon-plain-tag">db.runCommand( { 
    create: &lt;view&gt;, viewOn: &lt;source&gt;, pipeline: &lt;pipeline&gt;, collation: &lt;collation&gt; 
} )

// 也可以使用新引入的Shell助手：
db.createView(&lt;view&gt;, &lt;source&gt;, &lt;pipeline&gt;, &lt;collation&gt; )</pre>
<p>那些用于列出集合列表的操作，例如db.getCollectionInfos()、db.getCollectionNames()，它们的输出包含视图。</p>
<p>要删除视图，可以使用命令：<pre class="crayon-plain-tag">db.collection.drop()</pre> </p>
<div class="blog_h3"><span class="graybg">基本特性</span></div>
<ol>
<li>只读，对视图进行写操作导致错误</li>
<li>索引与排序：视图使用其底层的集合上的索引，你不能在视图上使用$natural排序</li>
<li>投影操作限制：在视图上进行find()操作，不支持以下投影操作：$、$elemMatch、$slice、$meta</li>
<li>视图的名字不可改变 </li>
</ol>
<p>视图在读操作发生时，按需的进行计算。在视图上执行的读操作，是底层聚合管道（ aggregation pipeline）的一部分，因此，视图不支持：</p>
<ol>
<li>db.collection.mapReduce()</li>
<li>$text操作符，因为聚合中的$text仅仅对于第一阶段（first stage）有效</li>
<li>geoNear命令以及$geoNear管道阶段（pipeline stage） </li>
</ol>
<div class="blog_h3"><span class="graybg">关于分片</span></div>
<p>如果底层集合是分片的，则视图也是分片的。因此不能为$lookup、$graphlookup操作的from字段指定一个分片的视图。</p>
<div class="blog_h3"><span class="graybg">排序规则</span></div>
<ol>
<li>在创建视图时，可以指定一个默认的排序规则（collation，判断两个比较项谁大谁小的准则）。如果不指定，视图的默认排序规则是简单的二进制比较排序规则。视图不会从底层集合继承排序规则</li>
<li> 在视图上进行字符串比较，使用视图默认排序规则。尝试修改视图默认排序规则的操作会失败</li>
<li>如果基于其它视图创建新视图，你不能指定不同于源视图的排序规则</li>
<li>当执行牵涉到多个视图的聚合操作时，例如$lookup、$graphlookup，所有牵涉到的视图必须具有一致的排序规则</li>
</ol>
<div class="blog_h2"><span class="graybg">定长集合</span></div>
<p>定长集合（Capped Collections）是用于支持高吞吐量操作（插入、按插入顺序读取）的、具有固定尺寸的集合。定长集合的工作方式类似于环形缓冲，一旦空间占满，最老的文档会被覆盖。</p>
<p>创建定长集合的方法：</p>
<pre class="crayon-plain-tag">// size 集合的最大字节数，小于4096则MongoDB自动设置为4096。否则，自动舍入到最接近的256的倍数
db.createCollection( "log", { capped: true, size: 100000 } )
// 可以指定max，集合包含的文档的最大数量
db.createCollection("log", { capped : true, size : 5242880, max : 5000 } )</pre>
<p>当你使用find()来操作定长集合，并且没有指定顺序时，MongoDB保证结果的顺序和文档插入的顺序一致。要逆插入序获得结果，可以：</p>
<pre class="crayon-plain-tag">db.cappedCollection.find().sort( { $natural: -1 } )</pre>
<p>可以使用集合的isCapped方法检测定长集合：</p>
<pre class="crayon-plain-tag">db.collection.isCapped()</pre>
<p>普通集合可以被转换为定长集合：</p>
<pre class="crayon-plain-tag">db.runCommand({"convertToCapped": "mycoll", size: 100000});</pre>
<div class="blog_h3"><span class="graybg">基本特性</span></div>
<ol>
<li>定长集合记住文档的插入顺序， 查询时不需要基于索引以得到文档。没有了维护索引的负担，定长集合支持更高的插入吞吐量</li>
<li>定长集合具有 _id 字段，并在其上默认建立了索引</li>
<li>如果你需要对定长集合中的文档进行update操作，应当建立索引，避免全集合扫描</li>
<li>定长集合不支持删除文档，如果更新/替换操作改变了文档的尺寸，会失败</li>
<li>定长集合不支持分片</li>
<li>聚合管道操作符$out不能把结果输出到定长集合</li>
</ol>
<div class="blog_h1"><span class="graybg">安装与配置</span></div>
<div class="blog_h2"><span class="graybg">安装</span></div>
<div class="blog_h3"><span class="graybg">Docker</span></div>
<pre class="crayon-plain-tag"># 在容器中运行MongoDB
docker run --name mongodb -p 27017:27017 -v ~/Docker/volumes/mongodb/data/db:/data/db -d mongo:3.4
# 登录到MongoDB管理Shell
docker exec -it mongodb mongo admin</pre>
<div class="blog_h1"><span class="graybg">Mongo Shell</span></div>
<p>Mongo Shell是交互式的、基于JavaScript语言的MongoDB接口。使用此Shell你可以查询、插入数据，或者执行管理任务。</p>
<p>要启动Mongo Shell，cd到安装目录，执行mongo命令。不加任何参数时，Shell尝试连接到localhost:27017。Shell启动时默认会读取~/.mongorc.js文件，并执行其中的JavaScript脚本。</p>
<div class="blog_h2"><span class="graybg">基本用法</span></div>
<div class="blog_h3"><span class="graybg">连接到服务器</span></div>
<pre class="crayon-plain-tag"># 连接到本地 27017
mongo
# 连接到复制集
mongo --host rs1/mongo-11.gmem.cc,mongo-12.gmem.cc,mongo13.gmem.cc
# 使用指定的用户登陆到服务器，连接到bais数据库，基于admin数据库进行身份验证
mongo -u root -p root --host  mongo-11.gmem.cc --authenticationDatabase admin bais</pre>
<div class="blog_h3"><span class="graybg">常用代码</span></div>
<pre class="crayon-plain-tag">// 显示当前正在使用的数据库
db
// 切换数据库
use mydb
// Shell不支持使用名字中包含空格或者以数字开始的集合，需要改变语法：
mydb.3test.find()    // 错误
mydb["3test"].find()
mydb.getCollection("3test").find()

// 美化输出
db.myCollection.find().pretty()

// 打印输出
print()
// 转换为JSON并打印，等价于printjson() 
print(tojson(obj))

// 退出Shell
quit()</pre>
<div class="blog_h3"><span class="graybg">多行文本</span></div>
<p>当输入行以花、圆、方括号结尾时，下面的行自动以...开头，直到所有开括号的匹配关闭括号都输入了，再回车，输入才会被估算。</p>
<div class="blog_h2"><span class="graybg">配置Shell</span></div>
<div class="blog_h3"><span class="graybg">设置命令提示符</span></div>
<p>默认的命令提示符是 <pre class="crayon-plain-tag">&gt;</pre>，你可以使用如下代码定制：</p>
<pre class="crayon-plain-tag">// 显示计数器
cmdCount = 1;
prompt = function() {
    return (cmdCount++) + "&gt; ";
}

// 显示数据库和主机信息
host = db.serverStatus().host;
prompt = function() {
   return db+"@"+host+"$ ";
}</pre>
<div class="blog_h3"><span class="graybg">使用外部编辑器 </span></div>
<p>你可以在MongoDB Shell中使用自己喜欢的编辑器，只需要在启动Shell之前设置环境变量： <pre class="crayon-plain-tag">export EDITOR=vim</pre>即可。</p>
<p>这样，你就可以编辑变量或者函数了：<pre class="crayon-plain-tag">edit myFunc</pre></p>
<div class="blog_h3"><span class="graybg">设置批量尺寸</span></div>
<p>db.collection.find()会返回一个游标，但是，在Shell中没有把游标赋值给一个变量的情况下，Shell会自动迭代游标20次，并打印结果到屏幕上。这个迭代的次数是可以定制的：</p>
<pre class="crayon-plain-tag">DBQuery.shellBatchSize = 10;</pre>
<div class="blog_h2"><span class="graybg">为Shell编写脚本</span></div>
<p> 你可以使用JavaScript语言编写一段脚本，交由Shell执行，以完整数据操控、管理工作：</p>
<pre class="crayon-plain-tag"># 命令行中提供脚本
mongo test --eval "printjson(db.getCollectionNames())"
# 文件方式提供脚本
mongo localhost:27017/test myjsfile.js
# 在Shell中加载JS
load("myjstest.js")</pre>
<div class="blog_h3"><span class="graybg">打开连接</span></div>
<pre class="crayon-plain-tag">conn = new Mongo();
db = conn.getDB("myDatabase");

// 也可以使用connect函数
db = connect("localhost:27020/myDatabase");</pre>
<div class="blog_h3"><span class="graybg">Shell助手等价代码</span></div>
<p>不能在JavaScript中使用任何Shell助手，因为不是合法的JavaScript表达式。等价写法如下表：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">Shell助手</td>
<td style="text-align: center;">等价JS代码</td>
</tr>
</thead>
<tbody>
<tr>
<td>show dbs, show databases</td>
<td>db.adminCommand('listDatabases')</td>
</tr>
<tr>
<td>use &lt;db&gt;</td>
<td>db = db.getSiblingDB('&lt;db&gt;')</td>
</tr>
<tr>
<td>show collections</td>
<td>db.getCollectionNames()</td>
</tr>
<tr>
<td>show users</td>
<td>db.getUsers()</td>
</tr>
<tr>
<td>show roles</td>
<td>db.getRoles({showBuiltinRoles: true})</td>
</tr>
<tr>
<td>show log &lt;logname&gt;</td>
<td>db.adminCommand({ 'getLog' : '&lt;logname&gt;' })</td>
</tr>
<tr>
<td>show logs</td>
<td>db.adminCommand({ 'getLog' : '*' })</td>
</tr>
<tr>
<td>it</td>
<td>cursor = db.collection.find()<br />if ( cursor.hasNext() ){<br /> cursor.next();<br />}</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">Shell中的数据类型</span></div>
<p>BSON提供了很多数据类型，不同驱动都根据其宿主编程语言，提供了Native的类型映射。Shell类似，提供了助手类以支持这些数据类型。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">BSON类型</td>
<td style="text-align: center;">Shell助手类说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>Date</td>
<td>
<p>Shell提供多种方法来返回日期对象：</p>
<ol>
<li>Date()方法，返回当前日期的<span style="background-color: #c0c0c0;">字符串</span>形式</li>
<li>new Date()构造函数，使用ISODate()包装器返回一个日期对象</li>
<li>ISODate()构造函数，使用ISODate()包装器返回一个日期对象</li>
</ol>
<p>示例：<pre class="crayon-plain-tag">ISODate('2008-08-08T08:08:08.888Z')</pre></p>
</td>
</tr>
<tr>
<td>ObjectId</td>
<td>Shell提供ObjectId包装器类，要生成一个新的ObjectId，你可以<pre class="crayon-plain-tag">new ObjectId</pre>或者<pre class="crayon-plain-tag">ObjectId()</pre></td>
</tr>
<tr>
<td>NumberLong</td>
<td>Shell提供NumberLong包装器类，你可以：<pre class="crayon-plain-tag">NumberLong("1111")</pre></td>
</tr>
<tr>
<td>NumberInt</td>
<td>Shell提供NumberInt包装器类</td>
</tr>
<tr>
<td>NumberDecimal</td>
<td>
<p>3.4版本引入。默认情况下Shell把所有数字作为64bit双精度浮点数看待，使用NumberDecimal()可以明确的构造128bit浮点数，在牵涉到货币的领域使</p>
<p>&nbsp;</p>
</td>
</tr>
</tbody>
</table>
<p> 要检测对象的数据类型，可以使用：</p>
<pre class="crayon-plain-tag">mydoc._id instanceof ObjectId
// 或者
typeof mydoc._id</pre>
<div class="blog_h1"><span class="graybg">读写特性</span></div>
<div class="blog_h2"><span class="graybg">原子性/事务</span></div>
<p>在MongoDB中，写操作在<span style="background-color: #c0c0c0;">单个文档上</span>是原子的，即使在修改多个内嵌的文档的情况下。</p>
<p>相反的，当一个写操作操控了多个文档的情况下，整个操作默认不是原子的。多个写操作可能会发生交叉（interleave）、而且不会在中途出错后回滚</p>
<div class="blog_h3"><span class="graybg">$isolated</span></div>
<p>如果要为影响了多个文档的单个写操作建立隔离性语义，可以使用<pre class="crayon-plain-tag">$isolated</pre>操作符，该操作符可以避免其它写操作交叉进来。<span style="background-color: #c0c0c0;">直到写操作完成后，其它进程不能看到结果</span>。</p>
<p>注意：</p>
<ol>
<li>该操作符<span style="background-color: #c0c0c0;">不能与分片集群（Sharded Clusters）</span>协同工作</li>
<li>该操作符<span style="background-color: #c0c0c0;">不能提供all-or-nothing的原子性语义</span>。如果中途发生错误，已经发生的修改不会回滚</li>
<li>该操作符符导致当前操作获得<span style="background-color: #c0c0c0;">目标集合上的独占锁</span>，即使在使用支持文档级锁的引擎（WiredTiger）的情况下</li>
</ol>
<p>$isolated产生的隔离效果，在修改了第一个文档后显现。</p>
<div class="blog_h3"><span class="graybg">类事务语义</span></div>
<p>由于MongoDB支持内嵌文档，因此在很多应用场景下，单文档级别的原子性足够应付。</p>
<p>如果多个写操作需要在单个事务中执行，可以考虑在代码中实现两阶段提交（two-phase commit ）模式 。注意这只能提供类似于事务的语义，用于保证数据一致性，却不能避免中间结果被其它操作看到</p>
<div class="blog_h3"><span class="graybg">一致性控制</span></div>
<p>一致性控制允许多个程序并发的执行，而不导致数据不一致或者冲突。</p>
<p>实现一致性控制的可选方式：</p>
<ol>
<li>在应当具有唯一性值的（一个或者多个）字段上创建一个唯一性索引，以阻止重复的数据插入</li>
<li>使用查询断言（Query predicate）指定某个字段的当前期望值</li>
</ol>
<div class="blog_h2"><span class="graybg">并发性</span></div>
<p>为保证数据一致性，MongoDB使用锁机制来管理多客户端的并发读写。</p>
<div class="blog_h3"><span class="graybg">锁类型</span></div>
<p>MongoDB使用多粒度的锁，操作可能在全局、数据库、集合级别进行锁定。同时允许存储引擎实现更加细粒度的锁定，例如<span style="background-color: #c0c0c0;">WiredTiger支持文档级别的锁定</span>。</p>
<p>MongoDB使用读写锁（共享锁S，独占锁X），允许多个读操作共享同一资源。对于MMAPv1，写操作权限仅仅能赋予单个写操作。</p>
<p>除了S、X锁以外，MongoDB还支持读意向锁（IS）、写意向锁（IX），表示操作将要对（比被意向锁定的资源）更加细粒度的资源进行读写操作，IX是可以共享的，<span style="background-color: #c0c0c0;">意向锁可以减少不必要的锁检查</span>。当在某一级别进行锁定时，更高级别（粗粒度）资源被意向锁定。举例来说，当X锁一个集合进行写操作时，对应的数据库、全局必须上IX锁。</p>
<p>单个数据库可以同时被IS、IX锁。X锁不能与其它锁共存。S锁仅仅能和IS锁共存。</p>
<p>MongoDB的锁是公平的，操作依据排队的顺序被授予锁。但是为了吞吐量的考虑，兼容的锁可能被一并授予，而不考虑排队情况。</p>
<div class="blog_h3"><span class="graybg">锁粒度</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 15%; text-align: center;">存储引擎</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>WiredTiger</td>
<td>
<p>从3.0开始，MongoDB引入该引擎</p>
<p>对于大部分的读写操作，该引擎使用乐观并发控制。WiredTiger仅仅在全局、数据库、集合级别使用意向锁。当检测到两个操作之间存在冲突时，其中一个操作会透明的重试</p>
<p>某些全局性的（通常是短暂的牵涉到多数据库的）操作，仍然要求全局（实例级别）的锁。drop之类的操作仍然要求数据库级别的独占锁</p>
</td>
</tr>
<tr>
<td>MMAPv1</td>
<td>从3.0开始，MMAPv1引擎使用集合级别的锁</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">让出锁</span></div>
<p>某些情况下，读写操作可能让出它们占有的锁。长时间运行的读写操作，可能在多种情况下让出锁。对于影响到多个文档的操作，锁让出可能在修改每个文档前后发生。</p>
<p>MMAPv1引擎会在认为需要读取的数据不在物理内存的时候，让出锁，等MongoDB把目标数据加载到内存之后，再重新获得锁。</p>
<div class="blog_h3"><span class="graybg">操作和锁</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">操作</td>
<td style="text-align: center;">锁定</td>
</tr>
</thead>
<tbody>
<tr>
<td>发起查询</td>
<td>S</td>
</tr>
<tr>
<td>从游标抓取数据</td>
<td>S</td>
</tr>
<tr>
<td>插入数据</td>
<td>X</td>
</tr>
<tr>
<td>删除数据</td>
<td>X</td>
</tr>
<tr>
<td>更新数据</td>
<td>X</td>
</tr>
<tr>
<td>MapReduce</td>
<td>S、X锁，除非操作被指定为非原子性的。Map Reduce任务的某些部分可以并发执行</td>
</tr>
<tr>
<td>创建索引</td>
<td>在前端（默认）创建索引，导致数据库级别的锁</td>
</tr>
<tr>
<td>aggregate()</td>
<td>S</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">读隔离/一致性/Recency</span></div>
<div class="blog_h3"><span class="graybg">隔离性保证</span></div>
<table class=" full-width fixed-word-wrap">
<tbody>
<tr>
<td style="width: 20%;">读取未提交</td>
<td>
<p>在MongoDB中，客户端可以在写操作持久化（durable）之前，即看到其结果：</p>
<ol>
<li>不论指定什么样的写关注选项。其它使用读关注选项local的客户端都可以在发起写操作获得确认（acknowledged）之前，就看到写的结果</li>
<li>使用local读关注的客户端，可能读取到之后被回滚的数据</li>
</ol>
<p>注意：local是默认的读关注</p>
<p>读取未提交是单实例mongod、复制集，以及分片集群的默认隔离级别</p>
</td>
</tr>
<tr>
<td>读取未提交&amp;单文档原子性</td>
<td>
<p>写操作对于单个文档是原子性的，因而任何客户端都不会读取到只更新了一部分字段的文档</p>
<p>对于单实例mongod，针对某个特定文档的一系列读写操作，是可串行化的（serializable）</p>
<p>对于复制集，仅仅在不存在回滚的情况下，针对某个特定文档的一系列读写操作，是可串行化的（serializable）</p>
</td>
</tr>
<tr>
<td>读取未提交&amp;多文档写操作</td>
<td>
<p>对于操作了多个文档的单个写操作，作为整体来说它不是原子性的，其它写操作可能与之产生交错。你可以使用$isolated操作符改变此行为，使用此操作符时MongoDB的行为如下：</p>
<ol>
<li>没有时间点快照：假设一个读操作在t1时刻开始读取一系列文档，一个写操作在随后的t2提交了针对其中一个文档的写入，则该写入可能被读操作读取到（不可重复读）</li>
<li>不可串行化操作：假设一个读操作在t1读取文档d1，一个写操作在后续的t3更新了d1。这一场景引入了读-写依赖 —— 如果操作能够被串行化，读操作必须发生在写操作之前。再假设写操作在t2更新了文档d2，读操作又在t4读取了d2，这会引入一个写-读依赖 —— 要求读操作发生在写操作之后。读-写、写-读依赖导致一个环形，因而是不可串行化的</li>
<li>读操作可能获取到不匹配查询条件的文档，因为在读处理过程中，文档可能已经被更新过</li>
</ol>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">游标快照</span></div>
<p>MongoDB游标可能出现反常行为 —— 迭代同一个文档超过一次。发生这种现象的原因是，在游标迭代期间，某个写操作交叉进来并修改了某个文档。如果：</p>
<ol>
<li>被修改文档存储位置发生移动（例如文档大小增长导致）</li>
<li>或者查询所使用的索引对应的字段值被修改</li>
</ol>
<p>则可能出现重复迭代。</p>
<p>要防止此情形的出现，你可以使用<pre class="crayon-plain-tag">cursor.snapshot()</pre>调用来隔离游标。注意：</p>
<ol>
<li>该调用不能保证查询返回的数据是某个时间点的快照，不能和其它写操作隔离</li>
<li>不支持分片集群</li>
<li>不能和游标方法sort()、hint()联用</li>
</ol>
<p>另一个防止此情形的方法是，在不会修改的字段上建立唯一索引，然后使用hint()强制使用该索引，可以产生类似于snapshot()的效果。</p>
<div class="blog_h3"><span class="graybg">单调写</span></div>
<p>所为单调写（Monotonic Writes），是指<span style="background-color: #c0c0c0;">写操作扩散的顺序和它们逻辑上的顺序一致</span>。MongoDB针对单实例mongod、复制集、分片集群提供单调写保证。</p>
<div class="blog_h3"><span class="graybg">实时顺序</span></div>
<p>3.4引入的新特性。</p>
<p>针对在主节点上执行的读、写操作。使用linearizable读关注发起的读操作，和使用m:majority发起的写操作，如果这些读写操作针对单个文档进行操作，其效果就好像单个线程在执行这些读写一样，也就是说它们的实时顺序获得保证。</p>
<div class="blog_h2"><span class="graybg">分布式读</span></div>
<div class="blog_h3"><span class="graybg">针对分片集群的读</span></div>
<p>使用分片集群，你可以在多个mongod组成的服务器群中进行数据的分区，该分区对于应用程序来说几乎是透明的。</p>
<p>在分片集群中，客户端向关联到集群的某个mongos（路由器）实例发起操作请求，后者将请求转发给适当的分片：</p>
<p><img class=" wp-image-14982 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2015/05/sharded-cluster.bakedsvg.png" alt="sharded-cluster-bakedsvg" width="466" height="373" /></p>
<p>当被转发到单个特定的分片上时，针对分片集群的读操作效率最高。</p>
<p>针对分片集合的查询，应当包含集合的分片键（Shard key），这样mongos可以基于集群元数据（来自配置服务器）进行准确的路由。如果不包含分片键，则必须向所有mongod实例转发请求并聚合响应，这种分散 - 聚集（scatter gather ）模式通常效率不高。</p>
<p>对于复制集分片，发送给从节点的读操作可能和主节点的最新状态不一致（不确定的延迟）。这种行为可能导致牵涉到多个从节点的非单调读 —— 后入库的数据比先入库的数据先被读到。 </p>
<div class="blog_h3"><span class="graybg">针对复制集的读</span></div>
<p>默认情况下，客户端<span style="background-color: #c0c0c0;">针对复制集的主节点进行读</span>操作。但是客户端可以使用<a href="https://docs.mongodb.com/manual/core/read-preference/">Read perference</a>把读请求定向到复制集的其它成员。例如，客户端可以配置以便从最近的节点进行读取以达成以下目标：</p>
<ol>
<li>在跨数据中心部署的情况下，减少延迟</li>
<li>可以分散大量读请求以增大吞吐量</li>
<li>执行备份操作</li>
<li>在新的主节点被选举出来前，仍然可以执行读操作</li>
</ol>
<p>针对从节点进行读操作，得到的可能不是主节点的最新状态，重定向到不同从节点的读操作可能导致非单调读。</p>
<div class="blog_h2"><span class="graybg">分布式写</span></div>
<div class="blog_h3"><span class="graybg">针对分片集群的写</span></div>
<p>对于分片集群中的分片集合，mongos负责把写操作分发到负责对应数据集的那些分片，路由时mongos从配置数据库（config database，位于config server）中查询元数据信息，并判断如何分发写操作。</p>
<p>MongoDB依据<span style="background-color: #c0c0c0;">分片键的值范围</span><span style="color: #c0c0c0;"><em>（支持Hash方式么）</em></span>来进行数据分区，之后把这些分区（chunks）发布到某些分片上（Shard，即mongod实例）。</p>
<p>对于update类操作来说，如果：</p>
<ol>
<li>针对单个文档，更新操作必须包含分片键或者_id字段</li>
<li>针对多个文档，如果更新操作包含分片键性能在某些情况下会更好，但是有时仍然会广播操作到所有分片</li>
</ol>
<p>如果分片键的值在每次插入时总是递增/递减，那么连续的很多插入操作会落到同一个分片中，产生性能瓶颈。 </p>
<div class="blog_h3"><span class="graybg">针对复制集的写</span></div>
<p>在复制集中，<span style="background-color: #c0c0c0;">所有写操作都针对主节点</span>。 主节点应用写操作，然后把操作记录在自己的操作日志（oplog）中。oplog是可重做的写操作流水，所有从节点都会复制该日志并应用，从节点的操作均是异步的，因此不能假设何时主从节点状态完全一致。</p>
<div class="blog_h2"><span class="graybg">两阶段提交</span></div>
<p>这是一种编程模式，当你进行多文档“事务”操作时，你可以实现类似关系型数据库的回滚功能。</p>
<div class="blog_h3"><span class="graybg">背景</span></div>
<p>在MongoDB中单文档的写操作总是原子的，但是写操作牵涉到多个文档时（通常称为多文档事务）则没有原子性保证。由于MongoDB支持任意复杂度的内嵌文档，因此很多应用场景下不需要多文档操作。</p>
<p>尽管如此，某些情况下你不得不进行多文档写操作。当执行由一系列写操作序列构成的事务时，可能面临以下需求：</p>
<ol>
<li>原子性：如果操作中途失败，前面已经完成的操作需要回滚</li>
<li>一致性：如果重大错误（网络、硬件）中断了事务，数据库必须有能力恢复到一个一致性的状态</li>
</ol>
<div class="blog_h3"><span class="graybg">模式</span></div>
<p>考虑转账的应用场景：你需要从账户A转账到账户B。使用关系型数据库时，你需要在单个事务中，减去A账户的余额并加到B账户中去。使用MongoDB时，你可以手工实现两阶段提交，模拟一个类似的结果。</p>
<p>在该场景中，我们使用集合accounts来存储账户信息，transactions存储交易信息。</p>
<p>正常交易流程的代码如下：</p>
<pre class="crayon-plain-tag">// 初始化账户信息，下面的调用返回一个BulkWriteResult对象
db.accounts.insert(
    [
        { _id: "A", balance: 1000, pendingTransactions: [] },
        { _id: "B", balance: 1000, pendingTransactions: [] }
    ]
)
// 初始化交易（转账）信息，state反应交易的状态，可以取值initial, pending, applied, done, canceling,canceled
db.transactions.insert(
    { _id: 1, source: "A", destination: "B", value: 100, state: "initial", lastModified: new Date() }
)

// 使用两阶段提交来转账
// 1、找到一个初始状态的交易记录
var t = db.transactions.findOne( { state: "initial" } )
// 2、更新交易状态，如果返回值的nMatched、nModified为零，说明被上一步找到的记录，被其它客户端处理过，
//    返回第1步，获取下一条记录
db.transactions.update(
    { _id: t._id, state: "initial" },
    {
        $set: { state: "pending" },
        $currentDate: { lastModified: true }
    }
)
// 3、把交易关联到两个账户，过滤条件pendingTransactions可以防止重复转账
db.accounts.update(
    { _id: t.source, pendingTransactions: { $ne: t._id } },
    { $inc: { balance: -t.value }, $push: { pendingTransactions: t._id } }
)
db.accounts.update(
    { _id: t.destination, pendingTransactions: { $ne: t._id } },
    { $inc: { balance: t.value }, $push: { pendingTransactions: t._id } }
)
// 4、更新交易状态为applied
db.transactions.update(
    { _id: t._id, state: "pending" },
    {
        $set: { state: "applied" },
        $currentDate: { lastModified: true }
    }
)
// 5、更新账户第pending交易列表
db.accounts.update(
    { _id: t.source, pendingTransactions: t._id },
    { $pull: { pendingTransactions: t._id } }
)
db.accounts.update(
    { _id: t.destination, pendingTransactions: t._id },
    { $pull: { pendingTransactions: t._id } }
)
// 6、设置交易状态为done
db.transactions.update(
    { _id: t._id, state: "applied" },
    {
        $set: { state: "done" },
        $currentDate: { lastModified: true }
    }
)</pre>
<div class="blog_h3"><span class="graybg">从错误中恢复</span></div>
<p>交易的关键之处在于，能够从各种错误场景中恢复，而不是简单完成上述6个步骤。</p>
<p>两阶段提交模式允许应用程序应用程序执行一个操作序列，以便恢复事务到一致性的状态 —— 这种能力由代码中刻意安排的filter保证。你可以在应用程序启动时，或者周期性的执行恢复操作，以便捕获并处理那些未完成的事务。</p>
<p>事务经历多就没有完成，则需要恢复，取决于应用程序的需要，我们刻意使用lastModified判断事务的最后操作时间。</p>
<p>找到需要恢复的事务后，根据state确定尚未完毕的后续步骤，并执行。</p>
<div class="blog_h3"><span class="graybg">回滚操作</span></div>
<p>某些时候（例如交易被撤销，或者目标账户在交易过程中被关闭），你需要回滚（撤销，传统意义的回滚，在MongoDB中rollback这个术语通常情况下不是这个含义）一个事务操作。 </p>
<p>两阶段提交模式中，你需要手工实现“补偿行为”来进行回滚。</p>
<div class="blog_h1"><span class="graybg">CRUD操作</span></div>
<div class="blog_h2"><span class="graybg">连接到MongoDB</span></div>
<p>各类驱动连接MongoDB时，均需要提供一个连接字符串，其格式为：</p>
<pre class="crayon-plain-tag">mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[database][?options]]
# 示例
mongodb://172.21.3.1:27017,172.21.3.2:27017/?replicaSet=rs3&amp;connectTimeoutMS=300000</pre>
<p>其中：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>mongodb://</td>
<td>固定的前缀</td>
</tr>
<tr>
<td>username:password@</td>
<td>使用指定的密码来登录</td>
</tr>
<tr>
<td>host*</td>
<td>
<p>服务器的主机名、IP地址或者UNIX Domain Socket</p>
<p>对于复制集，指定复制集成员的信息</p>
<p>对于分片集群，指定mongos的信息</p>
</td>
</tr>
<tr>
<td>port*</td>
<td>服务器的监听端口，默认27017</td>
</tr>
<tr>
<td>/database</td>
<td>可选，当提供用户名密码时，针对哪个数据库进行验证，默认admin</td>
</tr>
<tr>
<td>options</td>
<td>
<p>连接选项：</p>
<p>replicaSet，指定连接到的复制集，连接到复制集时应该至少指定两个host:port并且指定复制集名称，如果不指定，客户端创建的是针对Standalone的mongod的连接</p>
<p>ssl，如果为true，以SSL协议发起连接，不是所有驱动都支持</p>
<p>connectTimeoutMS，连接超时的毫秒数</p>
<p>maxPoolSize，连接池中最大的连接数，默认100<br />minPoolSize，连接池中最小的连接数，默认0<br />maxIdleTimeMS，多余连接被移除之前，最大空闲时间<br />waitQueueMultiple，等待获取连接的调用者排队的最大数量<br />waitQueueTimeoutMS，等待超时时间</p>
<p>w，指定默认写关注的w选项值，可以指定数字、majority、标签集名称<br />wtimeoutMS，指定默认写关注的wtimeoutMS选项值<br />journal，指定默认写关注的j选项值</p>
<p>readConcernLevel，指定默认读隔离级别，可选local、majority<br />readPreference，指定如何对复制集进行读操作，可选值：primary、primaryPreferred、secondary、<br />                                   secondaryPreferred、nearest<br />maxStalenessSeconds，从从节点读取数据时，最大容忍数据有多旧（复制延迟于主节点的秒数）<br />readPreferenceTags，可以指定多次，从具有那些标签的节点读，示例：</p>
<pre class="crayon-plain-tag"># 指定两类tags和一个空tag
readPreferenceTags=dc:ny,rack:1&amp;readPreferenceTags=dc:ny&amp;readPreferenceTags= </pre>
<p>authSource，指定存放用户认证信息的数据库的名称，默认来自连接串的database项<br />authMechanism，认证方式，SCRAM-SHA-1、MONGODB-CR、MONGODB-X509、GSSAPI、PLAIN</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">插入文档</span></div>
<p>这类操作添加新的文档到集合中，如果目标集合不存在，会自动创建。MongoDB提供以下插入文档的方法：</p>
<pre class="crayon-plain-tag"># 插入一个文档
db.collection.insertOne()
# 插入多个文档
db.collection.insertMany()</pre>
<p> 从单个文档级别上来看，所有MongoDB的写操作都是原子的。</p>
<div class="blog_h3"><span class="graybg">Shell示例</span></div>
<pre class="crayon-plain-tag">use local
db.users.insertOne({ name : 'Alex', age : 29, gender : 'M'})
db.users.insertMany([
    { name: 'Meng', age: 26, gender : 'F'},
    { name: 'Cai', age: 2, gender : 'F'},
    { name: 'Dang', age: 0, gender : 'M'},
])</pre>
<div class="blog_h3"><span class="graybg">Python示例</span></div>
<pre class="crayon-plain-tag">from pymongo import MongoClient
if __name__ == '__main__':
    client = MongoClient('mongodb://localhost:27017/')
    db = client.local  # 或者 client['local']
    db.users.insert_one({
        'name': 'FengYu',
        'age': 59,
        'gender': 'F'
    })</pre>
<div class="blog_h3"><span class="graybg">Java示例</span></div>
<p>引入依赖：</p>
<pre class="crayon-plain-tag">&lt;!-- 同步驱动 --&gt;
&lt;dependency&gt;
    &lt;groupId&gt;org.mongodb&lt;/groupId&gt;
    &lt;artifactId&gt;mongodb-driver&lt;/artifactId&gt;
    &lt;version&gt;3.4.2&lt;/version&gt;
&lt;/dependency&gt;

&lt;!-- 异步驱动，支持更快的非阻塞的IO --&gt;
&lt;dependency&gt;
    &lt;groupId&gt;org.mongodb&lt;/groupId&gt;
    &lt;artifactId&gt;mongodb-driver-async&lt;/artifactId&gt;
    &lt;version&gt;3.4.2&lt;/version&gt;
&lt;/dependency&gt;</pre>
<p>MongoDB的Java驱动提供了同步、异步两套接口。同步代码示例：</p>
<pre class="crayon-plain-tag">package cc.gmem.study;

import com.mongodb.MongoClient;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
import org.bson.Document;
import org.junit.Test;

public class CRUDTest {

    @Test
    public void insertDoc() {
        MongoClient client = new MongoClient( "localhost", 27017 );
        MongoDatabase db = client.getDatabase( "local" );
        MongoCollection&lt;Document&gt; coll = db.getCollection( "users" );
        Document doc = new Document( "name", "CongHua" ).append( "age", 55 ).append( "gender", "M" );
        coll.insertOne( doc );
    }
}</pre>
<p>异步代码示例： </p>
<pre class="crayon-plain-tag">package cc.gmem.study;

import com.mongodb.async.SingleResultCallback;
import com.mongodb.async.client.MongoClient;
import com.mongodb.async.client.MongoClients;
import com.mongodb.async.client.MongoCollection;
import com.mongodb.async.client.MongoDatabase;
import org.bson.Document;
import org.junit.Test;

import java.util.concurrent.CountDownLatch;

public class CRUDTest {

    @Test
    public void insertDoc() throws InterruptedException {
        // 这个客户端相当于连接池，即使你有很多并发操作，也不需要第二个实例
        MongoClient client = MongoClients.create( "mongodb://localhost" );
        MongoDatabase db = client.getDatabase( "local" );
        MongoCollection&lt;Document&gt; coll = db.getCollection( "users" );
        Document doc = new Document( "name", "GuangFang" ).append( "age", 55 ).append( "gender", "F" );
        // 这里采用同步机制等待异步操作完成
        final CountDownLatch latch = new CountDownLatch( 1 );
        coll.insertOne( doc, ( result, t ) -&gt; {
            System.out.println( "OK" );
            latch.countDown();
        } );
        latch.await();
    }
}</pre>
<div class="blog_h3"><span class="graybg">Node.js示例</span></div>
<p>安装MongoDB的Node.js驱动：</p>
<pre class="crayon-plain-tag">npm install mongodb</pre><br />
<pre class="crayon-plain-tag">const mongo = require( 'mongodb' );
let client = mongo.MongoClient;
client.connect( 'mongodb://localhost:27017/local' ).then( db =&gt; {
    return db.collection( 'users' ).insertOne( {
        name: 'GuangLiang',
        age: 55,
        gender: 'M'
    } );
} ).then( result =&gt; console.log( result.insertedId ) );</pre>
<div class="blog_h2"><span class="graybg">查询文档</span></div>
<p>这类操作从集合中检索并返回若干文档。MongoDB提供以下方法：</p>
<pre class="crayon-plain-tag">db.collection.find(&lt;query&gt;, &lt;projection&gt;)
# 可以指定一个参数作为查询文档（Query Document），查询文档提供查询条件
db.collection.find( { 'name' : 'Alex' } )</pre>
<div class="blog_h3"><span class="graybg">Python示例</span></div>
<pre class="crayon-plain-tag">client = MongoClient('mongodb://localhost:27017/')
db = client['local']
# 传入空文档作为查询过滤器，得到所有文档，返回值是一个游标
cursor = db.users.find({})
# { &lt;field1&gt;: &lt;value1&gt; } 表示相等过滤，例如
cursor = db.users.find({'gender': 'F'})
# { &lt;field1&gt;: { &lt;operator1&gt;: &lt;value1&gt; } } 使用查询操作符，例如
cursor = db.users.find({'age': {'$gt': 30}})
# 逻辑与
cursor = db.users.find({'gender': 'F', 'age': {'$gt': 30}})
# 逻辑或
cursor = db.users.find({
    '$or': [{'gender': 'F'}, {'age': {'$gt': 30}}]
})</pre>
<div class="blog_h3"><span class="graybg">游标</span></div>
<p>查询操作的返回值是一个<a href="https://docs.mongodb.com/manual/reference/method/js-cursor/">游标</a>。你可以对其进行遍历操作：</p>
<pre class="crayon-plain-tag">cursor = db.trades.find({'state': 'A'})
for trade in cursor:
    print(trade)

# 对于PyPy、Jython以及其它不使用引用计数垃圾回收的Python实现，需要调用
cursor.close()</pre>
<div class="blog_h3"><span class="graybg">查询操作符</span></div>
<p>上面代码中，$or、$gt等以$开头的键，属于查询操作符（Query Operator），是操作符的一种。常用的查询操作符如下表：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 17%; text-align: center;">操作符</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>$eq</td>
<td>匹配等于指定值的字段值</td>
</tr>
<tr>
<td>$gt</td>
<td>匹配大于指定值的字段值</td>
</tr>
<tr>
<td>$gte</td>
<td>匹配大于等于指定值的字段值</td>
</tr>
<tr>
<td>$lt</td>
<td>匹配小于指定值的字段值</td>
</tr>
<tr>
<td>$lte</td>
<td>匹配小于等于指定值的字段值</td>
</tr>
<tr>
<td>$ne</td>
<td>匹配不等于指定值的字段值</td>
</tr>
<tr>
<td>$in</td>
<td>匹配等于数组中元素之一的字段值</td>
</tr>
<tr>
<td>$nin</td>
<td>匹配不等于数组中任何元素的字段值</td>
</tr>
<tr>
<td>$or</td>
<td>逻辑或</td>
</tr>
<tr>
<td>$and</td>
<td>逻辑与</td>
</tr>
<tr>
<td>$not</td>
<td>逻辑非</td>
</tr>
<tr>
<td>$nor</td>
<td>逻辑非或，逻辑或取反</td>
</tr>
<tr>
<td>$exists</td>
<td>
<p>匹配具有指定字段的文档，示例：</p>
<pre class="crayon-plain-tag"># 存在qty字段，且该字段的值不是5或者15
{ qty: { $exists: true, $nin: [ 5, 15 ] } }</pre>
</td>
</tr>
<tr>
<td>$type</td>
<td>匹配字段类型是指定类型的文档，类型使用数字或者别名表示</td>
</tr>
<tr>
<td>$mod</td>
<td>
<p>执行取模操作，示例：
<pre class="crayon-plain-tag"># 匹配qty为12的文档，因为12%4 = 0
{ qty: { $mod: [ 4, 0 ] } }</pre>
</td>
</tr>
<tr>
<td>$regex</td>
<td>
<p>匹配其值匹配指定正则式的字段，格式：
<pre class="crayon-plain-tag">{ &lt;field&gt;: { $regex: /pattern/, $options: '&lt;options&gt;' } }
{ &lt;field&gt;: { $regex: 'pattern', $options: '&lt;options&gt;' } }
{ &lt;field&gt;: { $regex: /pattern/&lt;options&gt; } }</pre>
</td>
</tr>
<tr>
<td>$text</td>
<td>
<p>对建立了文本索引（text index）的字段进行文本搜索与匹配，格式：
<pre class="crayon-plain-tag">{
  $text:
    {
      # 搜索内容，一个字符串，包含多个搜索关键词时进行逻辑或操作，除非指定为短语
      $search: &lt;string&gt;,
      # 可选，用于确定搜索的停用词（stop word）列表，以及词干分析器（Stemmer）
      # 和分词器（tokenizer）使用的规则，默认使用索引的语言
      $language: &lt;string&gt;,
      # 是否大小写敏感
      $caseSensitive: &lt;boolean&gt;,
      # 是否声调敏感
      $diacriticSensitive: &lt;boolean&gt;
    }
}</pre>
</td>
</tr>
<tr>
<td>$where</td>
<td>
<p>传递一个包含JavaScript表达式或者完整JavaScript函数的字符串给查询系统。尽管提供了很强的灵活性，这种操作符需要遍历所有文档，需要注意
<p>要引用当前正在处理的文档，可以使用变量<pre class="crayon-plain-tag">this</pre>或者<pre class="crayon-plain-tag">obj</pre></p>
</td>
</tr>
<tr>
<td>$geoWithin</td>
<td>匹配某个地理位置字段的值，在指定的多边形范围之内，2dsphere /2d 索引支持该查询操作符</td>
</tr>
<tr>
<td>$geoIntersects</td>
<td>选择其地理空间数据与指定的GeoJSON对象交叉的文档，2dsphere /2d 索引支持该查询操作符</td>
</tr>
<tr>
<td>$near</td>
<td>选择其地理空间数据接近指定的点的文档，需要地理空间索引（geospatial index），2dsphere /2d 索引支持该查询操作符</td>
</tr>
<tr>
<td>$nearSphere</td>
<td>选择其地理空间数据接近指定的点（在球面上）的文档，需要地理空间索引（geospatial index），2dsphere /2d 索引支持该查询操作符</td>
</tr>
<tr>
<td>$all</td>
<td>匹配包含此操作符指定的数组中所有元素的数组字段</td>
</tr>
<tr>
<td>$elemMatch</td>
<td>
<p>匹配这样的数组字段：至少有一个元素满足该操作符指定的查询条件，例如：</p>
<pre class="crayon-plain-tag"># results字段中，至少有一个元素的值在80-85之间
{ results: { $elemMatch: { $gte: 80, $lt: 85 } } }</pre>
</td>
</tr>
<tr>
<td>$size</td>
<td>匹配数组字段的尺寸</td>
</tr>
<tr>
<td>$bitsAllSet</td>
<td>匹配这样的数字/二进制字段：该操作符指定的那些位的值均为1</td>
</tr>
<tr>
<td>$bitsAnySet</td>
<td>匹配这样的数字/二进制字段：该操作符指定的那些位的值至少一个为1</td>
</tr>
<tr>
<td>$bitsAllClear</td>
<td>匹配这样的数字/二进制字段：该操作符指定的那些位的值均为0</td>
</tr>
<tr>
<td>$bitsAnyClear</td>
<td>匹配这样的数字/二进制字段：该操作符指定的那些位的值至少一个为0</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">查询内嵌文档</span></div>
<p>要根据内嵌文档进行查询过滤，只需要依次指出内嵌文档各字段的值：
<pre class="crayon-plain-tag"># bson.son.SON类似于Python的字典，但是保持字段的顺序
# 匹配宽高为21/14cm的货物：
{"size": SON([("h", 14), ("w", 21), ("uom", "cm")])}
# 这种匹配，要求内嵌文档与查询条件完全相同，包括字段的顺序</pre>
<p>要根据内嵌文档的某个字段进行查询过滤，可以使用点号导航：</p>
<pre class="crayon-plain-tag">db.inventory.find({"size.uom": "cm"})
# 和简单字段一样，可以使用查询操作符
db.inventory.find({"size.h": {"$lt": 15}})</pre>
<div class="blog_h3"><span class="graybg">查询数组</span></div>
<pre class="crayon-plain-tag"># 匹配标签字段等于["red", "blank"]的存货
db.inventory.find({"tags": ["red", "blank"]})
# 匹配标签字段包含"red", "blank"这两个元素的存货
db.inventory.find({"tags": {"$all": ["red", "blank"]}})
# 匹配标签字段包含"red"元素的存货
db.inventory.find({"tags": "red"})
# 匹配size数组中至少有一个元素大于25的存货
db.inventory.find({"size": {"$gt": 25}})
# 匹配dim_cm数组中至少有一个元素大于15的存货、一个元素小于20的存货。可以单个元素同时满足连个条件
db.inventory.find({"dim_cm": {"$gt": 15, "$lt": 20}})
# 匹配dim_cm数组中至少有一个元素同时满足多个条件的存货
db.inventory.find({"dim_cm": {"$elemMatch": {"$gt": 22, "$lt": 30}}})
# 限定dim_cm的第2个元素的最小值
db.inventory.find({"dim_cm.1": {"$gt": 25}})
# 限定数组的大小
db.inventory.find({"tags": {"$size": 3}})</pre>
<div class="blog_h3"><span class="graybg">投影操作</span></div>
<p>默认情况下，查询操作<span style="background-color: #c0c0c0;">返回匹配文档的全部字段</span>。为了减少传递给应用程序的数据量，你可以指定一个投影文档（Projection document）以限制返回的字段：</p>
<pre class="crayon-plain-tag"># 仅仅返回name字段
# 投影文档（第二个参数）的字段值，1表示结果包含此字段，0表示不包含，默认的_id被包含
db.users.find( { gender: 'F' }, { name: 1, _id: 0 } )</pre>
<p>注意，除了_id字段之外，所有投影文档字段的值必须相等，当值：</p>
<ol>
<li>都为0的时候，表示结果排除这些字段</li>
<li>都为1的时候 ，表示结果仅包含这些字段</li>
</ol>
<p>要包含/排除内嵌文档中字段到查询结果，可以使用点号导航：</p>
<pre class="crayon-plain-tag"># 包含地址的邮编字段
db.users.find( {}, { name: 1, address.zip: 1 } )
# 包含最后一个孩子
db.users.find( {}, { name: 1, children: { "$slice": -1 } } )</pre>
<div class="blog_h3"><span class="graybg">投影操作符</span></div>
<p>上例中的$slice也是操作符，它属于投影操作符的一种：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 15%; text-align: center;">操作符</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>$</td>
<td>
<p>投影并得到数组的<span style="background-color: #c0c0c0;">第一个元素</span>，该操作符根据查询文档（find第一个参数）中某些查询条件进行投影</p>
<p>语法格式：</p>
<pre class="crayon-plain-tag">db.collection.find( { array: value ... }, { "array.$": 1 } )
db.collection.find( { array.field: value ...}, { "array.$": 1 } )</pre>
<p>注意，被限制的数组字段，必须存在于查询文档之中，value可以是查询操作符表达式</p>
<p>针对某个数组字段进行投影时，有如下限制：</p>
<ol>
<li>投影文档中仅能包含一个投影操作符$</li>
<li>查询文档中仅能包含被投影操作符$限定的数组字段， 包含其它数组字段可能导致未定义行为，下面的查询：<br />
<pre class="crayon-plain-tag">db.collection.find( { array: value, someOtherArray: value2 }, { "array.$": 1 } )</pre></p>
<p>是不正确的</p>
</li>
<li>查询文档针对被投影数组字段的查询条件只能有一个</li>
</ol>
<p>示例代码：</p>
<pre class="crayon-plain-tag">client = MongoClient('mongodb://localhost:27017/')
db = client['local']
db.users.drop()
db.users.insert_many([
    {
        'name': 'Alex',
        'age': 30,
        'children': [{'name': 'Dang', 'age': 0}, {'name': 'Cai', 'age': 2}]
    },
    {
        'name': 'Meng',
        'age': 26,
        'children': [{'name': 'Dang', 'age': 0}, {'name': 'Cai', 'age': 2}]
    },
    {
        'name': 'FengYu',
        'age': 59,
        'children': [
            {'name': 'Alex', 'age': 30}, {'name': 'Meng', 'age': 26}, {'name': 'WenJun', 'age': 26}
        ]
    }
])
db.users.find({'children': {'$elemMatch': {'age': {'$lt': 1}}}}, {'name': 1, '_id': 0, 'children': 1})
# { "name" : "Alex", "children" : [ { "name" : "Dang", "age" : 0 }, { "name" : "Cai", "age" : 2 } ] }
# { "name" : "Meng", "children" : [ { "name" : "Dang", "age" : 0 }, { "name" : "Cai", "age" : 2 } ] }
db.users.find({'children': {'$elemMatch': {'age': {'$lt': 1}}}}, {'name': 1, '_id': 0, 'children.$': 1})
# { "name" : "Alex", "children" : [ { "name" : "Dang", "age" : 0 } ] }
# { "name" : "Meng", "children" : [ { "name" : "Dang", "age" : 0 } ] }</pre>
<p>可以看到，增加了$操作符后，满足查询文档的查询结果中，对应数组字段，仅返回了第一个元素 </p>
<p>注意，数组元素是简单值的情况下，此操作符同样适用</p>
</td>
</tr>
<tr>
<td>$elemMatch</td>
<td>
<p>投影并得到数组的<span style="background-color: #c0c0c0;">第一个满足额外条件的元素</span>，该操作符明确指定投影条件，你可以基于<span style="background-color: #c0c0c0;">不存在于查询文档中的条件</span>进行投影、或者基于数组元素（内嵌文档）的字段进行投影。该操作符对处理结果进行二次过滤</p>
<p>示例代码：</p>
<pre class="crayon-plain-tag">db.users.find(
    {'children': {'$elemMatch': {'age': {'$lt': 1}}}}, 
    {'name': 1, '_id': 0, 'children' : {'$elemMatch': {'age': {'$gt': 1}}}})
# { "name" : "Alex", "children" : [ { "name" : "Cai", "age" : 2 } ] }
# { "name" : "Meng", "children" : [ { "name" : "Cai", "age" : 2 } ] }</pre>
</td>
</tr>
<tr>
<td>$meta</td>
<td>投影文档在$text操作期间被分配的分数</td>
</tr>
<tr>
<td>$slice</td>
<td>
<p>限制匹配数组所返回的元素的数量，示例：
<pre class="crayon-plain-tag"># 返回最前面5个元素
db.posts.find( {}, { comments: { $slice: 5 } } )
# 返回最后面5个元素
db.posts.find( {}, { comments: { $slice: -5 } } )
# 先跳过前20个元素，然后返回接着的10个元素
db.posts.find( {}, { comments: { $slice: [ 20, 10 ] } } )
#从倒数第20个元素开始，返回10个元素
db.posts.find( {}, { comments: { $slice: [ -20, 10 ] } } )</pre>
<p>&nbsp;</p>
</td>
</tr>
</tbody>
</table>
<p>注意，针对视图进行的find()操作不支持上表中的投影操作符。</p>
<div class="blog_h3"><span class="graybg">查询空/缺失字段</span></div>
<p>不同查询操作符处理null值的方式是不一样的，需要注意：</p>
<ol>
<li>等于操作符：<pre class="crayon-plain-tag">{ item : null }</pre>匹配item<span style="background-color: #c0c0c0;">为null或者不包含</span>item字段的文档</li>
<li>类型检查操作符：<pre class="crayon-plain-tag">{ item : { $type: 10 } }</pre>仅仅匹配item值为null的文档</li>
<li>存在性检查：<pre class="crayon-plain-tag">{ item : { $exists: false } }</pre>仅仅匹配不包含item字段的文档</li>
</ol>
<div class="blog_h2"><span class="graybg">更新文档</span></div>
<p>这类操作修改既有的文档，MongoDB提供以下方法：</p>
<pre class="crayon-plain-tag"># 更新文档
db.collection.updateOne(&lt;filter&gt;, &lt;update&gt;, &lt;options&gt;)
db.collection.updateMany(&lt;filter&gt;, &lt;update&gt;, &lt;options&gt;)
# 替换掉文档
db.collection.replaceOne(&lt;filter&gt;, &lt;replacement&gt;, &lt;options&gt;)</pre>
<p>参数filter类似于查询文档，update表示更新文档 —— 使用更新操作符来指定哪些字段需要怎么样被更新，replacement则是替换文档，直接替换掉原有的文档。</p>
<p>更新文档的格式：</p>
<pre class="crayon-plain-tag">{
  &lt;update operator&gt;: { &lt;field1&gt;: &lt;value1&gt;, ... },
  &lt;update operator&gt;: { &lt;field2&gt;: &lt;value2&gt;, ... },
  ...
}</pre>
<p>更新代码示例：</p>
<pre class="crayon-plain-tag">db.inventory.update(
    # 过滤器，指定查询条件：item为paper
    {'item': 'paper'},
    # 更新文档：
    {
        # 可以同时设置多个字段
        '$set': {'size.uom': 'cm', 'status': 'P'},
        '$currentDate': {'lastModified': True}
    }
)

# 更新多个满足条件的文档
db.inventory.update_many(
    {'qty': {'$lt': 50}},
    {
        '$set': {'size.uom': 'in', 'status': 'P'},
        '$currentDate': {'lastModified': True}
    }
)</pre>
<div class="blog_h3"><span class="graybg">更新操作特性</span></div>
<ol>
<li>原子性：对于单个文档来说，更新操作总是原子性的</li>
<li>_id字段：该字段不能被更新</li>
<li>文档大小：当更新后，文档的尺寸大于先前分配的空间，则另外在磁盘上分配空间给它</li>
<li>字段顺序：_id总是保持在最前面。包含重命名操作的更新操作可能会改变字段的顺序</li>
</ol>
<div class="blog_h3"><span class="graybg">选项</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 15%; text-align: center;">选项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>upsert</td>
<td>如果设置为true，则当满足filter的文档不存在时，新的文档会依据更新文档来创建，并插入</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">更新操作符</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 15%; text-align: center;">操作符</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td colspan="2"><em>更新字段的操作符</em></td>
</tr>
<tr>
<td>$inc</td>
<td>增加目标字段的值到给定的增量</td>
</tr>
<tr>
<td>$mul</td>
<td>将目标字段的值乘以一定的倍数</td>
</tr>
<tr>
<td>$rename</td>
<td>重命名字段</td>
</tr>
<tr>
<td>$setOnInsert</td>
<td>
<p>当一个更新操作导致了文档的插入时，设置以一个字段的值。当更新操作是对既有文档进行修改时，该操作符不起任何作用</p>
</td>
</tr>
<tr>
<td>$set</td>
<td>设置一个字段的值</td>
</tr>
<tr>
<td>$unset</td>
<td>移除某个字段</td>
</tr>
<tr>
<td>$min</td>
<td>仅当指定的值比目标字段当前值大的时候，进行更新</td>
</tr>
<tr>
<td>$max</td>
<td>仅当指定的值比目标字段当前值小的时候，进行更新</td>
</tr>
<tr>
<td>$currentDate</td>
<td>
<p>设置目标字段的值为当前日期：</p>
<pre class="crayon-plain-tag">{ $currentDate: { &lt;field1&gt;: &lt;typeSpecification1&gt;, ... } }
# 其中 typeSpecification格式：
# true，表示设置为当前日期
# { $type: "timestamp" }或者 { $type: "date" }明确的设置为当前时间戳或者日期</pre>
</td>
</tr>
<tr>
<td>$bit</td>
<td>
<p>对目标字段进行按位操作：
<pre class="crayon-plain-tag">{ $bit: { &lt;field&gt;: { &lt;and|or|xor&gt;: &lt;int&gt; } } } </pre>
</td>
</tr>
<tr>
<td>$isolated</td>
<td>
<p>阻止影响到多个文档的写操作，在彻底完成之前，中间结果被其它客户端看到。示例：
<pre class="crayon-plain-tag">db.foo.update(
    { status : "A" , $isolated : 1 },
    { $inc : { count : 1 } },
    { multi: true }
) </pre>
</td>
</tr>
<tr>
<td colspan="2"><em>更新数组的操作符</em></td>
</tr>
<tr>
<td>$</td>
<td>
<p>更新数组的第一个元素的值：
<pre class="crayon-plain-tag">{ "&lt;arrayField&gt;.$" : value }</pre>
</td>
</tr>
<tr>
<td>$addToSet</td>
<td>如果指定的元素不存在数组中，则将其加入</td>
</tr>
<tr>
<td>$pop</td>
<td>移除数组的第一个或者最后一个元素</td>
</tr>
<tr>
<td>$pullAll</td>
<td>移除数组中所有匹配的元素</td>
</tr>
<tr>
<td>$pull</td>
<td>移除所有匹配查询的数组元素</td>
</tr>
<tr>
<td>$push</td>
<td>添加一个元素</td>
</tr>
<tr>
<td>$each </td>
<td>
<p>修改$push、$addToSet 的行为，使之能够同时添加多个元素：
<pre class="crayon-plain-tag">{ $addToSet: { &lt;field&gt;: { $each: [ &lt;value1&gt;, &lt;value2&gt; ... ] } } }
{ $push: { &lt;field&gt;: { $each: [ &lt;value1&gt;, &lt;value2&gt; ... ] } } } </pre>
</td>
</tr>
<tr>
<td>$slice</td>
<td>
<p>修改$push的行为，限制被更新数组的长度：
<pre class="crayon-plain-tag">{
  $push: {
     &lt;field&gt;: {
       $each: [ &lt;value1&gt;, &lt;value2&gt;, ... ],
       $slice: &lt;num&gt;
     }
  }
}</pre>
<p>num的含义：</p>
<ol>
<li>取值0，表示把目标数组（field字段的值）设置为空数组</li>
<li>取值负数，表示仅保留数组的最后num个元素</li>
<li>取值正数，表示仅保留数组的最前num个元素</li>
</ol>
</td>
</tr>
<tr>
<td> $sort</td>
<td>修改$push的行为，对数组元素进行排序 </td>
</tr>
<tr>
<td>$position </td>
<td>修改$push的行为，指定新元素的插入位置 </td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">替换文档</span></div>
<p>所谓替换，就是指替换掉文档的所有字段 —— _id除外。</p>
<div class="blog_h2"><span class="graybg">删除文档</span></div>
<p>这类操作删除既有的文档。MongoDB提供以下方法：</p>
<pre class="crayon-plain-tag">db.collection.deleteOne(&lt;filter&gt;)
db.collection.deleteMany(&lt;filter&gt;)  # 如果过滤文档为空文档，则集合中所有文档被删除

db.collection.remove()              # 删除匹配过滤文档的文档，有多少删除多少</pre>
<p>过滤文档的格式参考查询文档。</p>
<div class="blog_h3"><span class="graybg">删除操作特性</span></div>
<ol>
<li>原子性：对于单个文档来说，删除操作是原子的</li>
<li>对索引的影响：删除操作不会drop掉索引，即使集合中所有文档都被删除</li>
</ol>
<div class="blog_h2"><span class="graybg">批量写操作</span></div>
<p>MongoDB为客户端提供了批量写操作能力。批量写操作<span style="background-color: #c0c0c0;">影响单个集合</span>。 应用程序可以为批量写操作指定一个可接受的确认级别（acknowledgement level）。</p>
<p>下面的方法用于执行批量插入、更新或者删除操作：</p>
<pre class="crayon-plain-tag">db.collection.bulkWrite(&lt;bulkOperationArray&gt;, &lt;options&gt;)</pre>
<p>bulkWrite支持insertOne、updateOne、updateMany、replaceOne、deleteOne、deleteMany这些写操作：</p>
<pre class="crayon-plain-tag">client.connect( 'mongodb://localhost:27017/local' ).then( db =&gt; {
    // 批量写仍然是针对单个集合的操作
    return db.collection( 'users' ).bulkWrite( [
        { insertOne: { document: { _id: 1, name: 'Alex', age: 30 } } },
        { insertOne: { document: { _id: 2, name: 'Meng', age: 27, gender: 'F' } } },
        {
            updateOne: {
                filter: { name: 'Alex' },
                update: {
                    $set: { gender: 'M' }
                }
            }
        }
    ] );
} ).then( result =&gt; console.log( result ) );</pre>
<div class="blog_h3"><span class="graybg">有序和无序</span></div>
<p>批量写操作支持有序、无序两种方式：</p>
<ol>
<li>有序：串行化的执行，如果其中某个操作失败，则MongoDB会返回，不处理后续的操作</li>
<li>无序：并发的执行，如果其中某个操作失败，别的操作不受其影响</li>
</ol>
<p>对于分片集合，无序批量写操作通常比有序的快。</p>
<p>默认的，MongoDB进行有序批量写，除非你指定选项：<pre class="crayon-plain-tag">ordered : false</pre></p>
<div class="blog_h3"><span class="graybg">分片集合的批量插入</span></div>
<p>大批量的数据插入操作可能影响分片集群（Sharded cluster）的性能，对于批量插入，考虑以下策略：</p>
<ol>
<li>集合预切分（Pre-split）：如果分片集合当前是空的，则集合仅仅包含一个位于单个分片中的初始块（initial chunk）。MongoDB必须花费时间来接收数据、切分数据，然后把切分好的数据块发送到可用的分片中。要避免此性能损失，可以考虑集合的与切分</li>
<li>使用无序写：使用无序写选项可以提高性能，MongoDB会尝试把数据同时发送给多个分片</li>
<li>避免单调递增瓶颈（Monotonic Throttling）：如果你的分片键（ shard key ）在插入过程中单调的递增，则所有插入的数据都会进入集合的最后一个块 —— 总是在单个分片上</li>
</ol>
<div class="blog_h2"><span class="graybg">读隔离</span></div>
<p>查询选项<pre class="crayon-plain-tag">readConcern</pre>用于复制集、复制集分片，决定为查询返回什么数据：</p>
<pre class="crayon-plain-tag">readConcern: { level: &lt;"majority"|"local"|"linearizable"&gt; }</pre>
<p>支持该选项的操作包括：find、aggregate 、distinct、count 、parallelCollectionScan 、geoNear 、geoSearch。</p>
<p>注意：节点上的最新数据，不代表是复制集系统中的最新数据</p>
<div class="blog_h3"><span class="graybg">读关注级别（Concern Levels）</span></div>
<p>该选项可以取以下值：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 15%; text-align: center;">级别</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>local</td>
<td>默认值。查询返回实例最新的数据，不保证这些数据已经写入到复制集中大部分节点或者已经持久化到磁盘。类似于读取未提交</td>
</tr>
<tr>
<td>majority</td>
<td>
<p>MMAPv1引擎不支持</p>
<p>查询返回实例最新的、已经确认被复制集中大部分节点写入的数据。要使用该级别，你需要：</p>
<ol>
<li>使用--enableMajorityReadConcern选项启动mongod实例，或者在配置文件中设置 replication.enableMajorityReadConcern 为true</li>
<li>复制集必须使用 WiredTiger 引擎，且使用推举协议版本1</li>
</ol>
</td>
</tr>
<tr>
<td>linearizable</td>
<td>
<p>3.4版本新加入，用于确保读取到最新鲜（任何发生在之前的写操作都可以读到）、持久化的数据（不会被回滚）</p>
<p>查询返回尽可能新的数据，数据由这样的写操作产生：</p>
<ol>
<li>基于w:majority级别的、成功的写操作</li>
<li>这些写操作在当前读操作开始之前，已经被确认（acknowledged，即发起操作的节点认为写操作已经完成）</li>
</ol>
<p>对于 writeConcernMajorityJournalDefault=true的复制集，该级别返回绝不会被回滚的数据</p>
<p>对于 writeConcernMajorityJournalDefault=false的复制集，MongoDB不等待majority级别写操作持久化到磁盘，即确认相应的写操作。因此，在复制集成员丢失的情况下，majority级别写操作可能回滚</p>
<p>你<span style="background-color: #c0c0c0;">只能在复制集的主节点</span>上指定该读级别。并且，上文所述保证，仅仅在查询的过滤器<span style="background-color: #c0c0c0;">精确的匹配单个文档</span>时有效</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">写确认</span></div>
<p>写关注（Write concern）描述写操作请求的确认级别。这些写操作可以应用在单独的mongod、复制集或者分片集群。 对于分片集群，mongos实例会把写关注级别传递给分片。</p>
<p>从2.6开始，写操作的新协议集成了写关注，你不再需要在写操作之后紧跟着一个getLastError调用来指定写关注级别。</p>
<div class="blog_h3"><span class="graybg">写关注选项</span></div>
<p>写关注相关的选项包括： </p>
<pre class="crayon-plain-tag">{ w: &lt;value&gt;, j: &lt;boolean&gt;, wtimeout: &lt;number&gt; }</pre>
<p>其中：</p>
<ol>
<li>w选项：要求当前写操作已经传播到指定数量的mongod实例，或者具有指定tag的mongod实例</li>
<li>j选项：要求当前写操作已经写入到磁盘日志</li>
<li> wtimeout：等到上述两个条件达成的超时，防止无限阻塞</li>
</ol>
<div class="blog_h3"><span class="graybg">w选项</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 15%; text-align: center;">取值</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>&lt;number&gt;</td>
<td>
<p>要求写操作请求已经传播到指定数量的mongod实例：</p>
<ol>
<li><pre class="crayon-plain-tag">w: 1</pre>要求确认写操作已经传播到单独的mongod实例，或者复制集中的主节点。这是默认取值</li>
<li><pre class="crayon-plain-tag">w: 0</pre>不要求写操作的确认，尽管如此，使用该选项时客户端可以收到套接字异常、网络错误等信息。与<pre class="crayon-plain-tag">j: true</pre>联用时，后者优先，因此需要确认写操作已经传播到单独mongod实例或者复制集中的主节点</li>
</ol>
<p>大于1的取值，仅仅针对复制集有意义。即要求确认写操作传播到包括主节点在内的N个复制集成员</p>
</td>
</tr>
<tr>
<td>majority</td>
<td>
<p>要求确认写操作已经传播到大部分的投票节点，包括主节点</p>
<p>当基于此取值的写操作调用返回后，使用 readConcern:majority的客户端可以读取到其写入的数据</p>
</td>
</tr>
<tr>
<td>&lt;tag set&gt;</td>
<td>要求确认写操作已经传播到复制集中具有指定标签（tag）的节点</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">j选项</span> </div>
<p><pre class="crayon-plain-tag">j: true</pre>从MongoDB获得确认：写操作已经被写到日志（journal）中。该选项本身不保证写操作不被回滚，回滚的原因可能是复制集的主节点发生故障转移。</p>
<p>从3.2开始，该选项导致写操作仅仅在：指定数量（w选项）的复制集节点都写了日志后才返回，之前仅仅要求主节点写了日志就返回（因而就不存在主节点故障转移导致的回滚问题？）</p>
<div class="blog_h3"><span class="graybg">wtimeout</span></div>
<p>指定一个毫秒的限制，但是仅仅用于w取值大于1的情况。如果不指定此选项或者指定为0，可能导致永久的阻塞。</p>
<p>当超时到达后，调用立即以一个错误返回，然而后续所要求的写关注可能成功。 当返回后，MongoDB不会撤销已经执行的数据修改。</p>
<div class="blog_h3"><span class="graybg">确认行为</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">w取值</td>
<td style="text-align: center;">不指定j</td>
<td style="text-align: center;">j:true</td>
<td style="text-align: center;">j:false</td>
</tr>
</thead>
<tbody>
<tr>
<td colspan="4"><em>单独实例（Standalone）</em></td>
</tr>
<tr>
<td><strong>w: 1</strong></td>
<td>确认写入到内存</td>
<td>确认写入到磁盘日志</td>
<td>确认写入到内存 </td>
</tr>
<tr>
<td><strong>w: "majority"</strong></td>
<td>如果启用了日志，确认写入到日志 </td>
<td>确认写入到磁盘日志</td>
<td>确认写入到内存</td>
</tr>
<tr>
<td colspan="4"><em>复制集（Replica Sets）    </em></td>
</tr>
<tr>
<td><strong>w: "majority"</strong></td>
<td>
<p>行为取决于writeConcernMajorityJournalDefault：</p>
<ol>
<li>true 确认写入磁盘</li>
<li>false 确认写入到内存</li>
</ol>
</td>
<td> 确认写入到磁盘日志</td>
<td>确认写入到内存  </td>
</tr>
<tr>
<td><strong>w: &lt;number&gt; </strong></td>
<td> 确认写入到内存</td>
<td>确认写入到磁盘日志 </td>
<td>确认写入到内存</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">可追加游标</span></div>
<p>默认情况下，当客户端消费了游标中所有结果集后，MongoDB会自动关闭游标。</p>
<p>但是，对于定长（Capped）集合来说，你可以使用可追加游标（Tailable Cursor）。该游标在客户端耗尽所有结果集后仍然保持打开状态。从概念上来说，这种游标类似于UNIX命令tail -f。当客户端插入新数据到集合中后，可追加游标会继续取回文档。</p>
<p>在高写入量、不适用索引的定长集合上，可以使用这种游标。例如，MongoDB本身的复制机制，就是在主节点的oplog这个定长集合上使用可追加游标。</p>
<p>注意可追加游标的以下特性：</p>
<ol>
<li>该游标不使用索引，以自然顺序—— 在磁盘上的存储顺序 ——返回文档</li>
<li>由于不使用索引，可追加游标的最初扫描代价很高，但是一旦最初扫描的结果被耗尽后，再取回新的文档，成本很低</li>
<li>可追加游标可能变得不可用，可能的情况包括：
<ol>
<li>查询返回结果为空</li>
<li>游标返回位于集合尾部的文档，而应用程序随后删除了此文档</li>
</ol>
</li>
</ol>
<div class="blog_h1"><span class="graybg">性能优化</span></div>
<div class="blog_h2"><span class="graybg">查询计划</span></div>
<p>MongoDB的查询优化器会分析查询，然后选择最高效的执行计划。后续执行相同查询时，会使用同样的执行计划。</p>
<p>查询优化器会缓存执行计划，但是仅仅缓存那些可以有多个执行路径的查询形状（Query shape，指查询断言、排序、投影的组合）。</p>
<p>查询规划器（query planner ）针对每个查询，搜索执行计划缓存，寻找匹配查询形状的计划。如果匹配的计划不存在，查询规划器会生成候选的计划，在“试用期间”评估它们，然后选取其中最高效的，并缓存该执行计划。</p>
<p>如果匹配的计划存在，查询规划器则会通过replanning机制重新评估其性能，如果评估不通过对应的缓存条目被清除。在发生清除的情况下，查询规划器会按照正常流程重新选择执行计划并缓存。</p>
<p>上述逻辑的流程图如下：</p>
<p><img class="aligncenter  wp-image-14990" src="https://blog.gmem.cc/wp-content/uploads/2015/05/query-planner-diagram.baked_-544x1024.png" alt="query-planner-diagram-baked" width="417" height="785" /></p>
<div class="blog_h3"><span class="graybg">相关API</span></div>
<p>你可以使用<pre class="crayon-plain-tag">db.collection.explain()</pre>或者<pre class="crayon-plain-tag">cursor.explain()</pre>调用获得目标查询的执行计划的统计信息。这些信息有助于帮助你分析如何建立索引。</p>
<div class="blog_h3"><span class="graybg">缓存的清除</span></div>
<p>当mongod重新启动或者关闭后，所有执行计划的缓存都被清除。</p>
<p>从2.6开始，提供了一些方法来控制缓存：<pre class="crayon-plain-tag">PlanCache.clear()</pre>清除所有缓存，<pre class="crayon-plain-tag">PlanCache.clearPlansByQuery()</pre>清除特定缓存。</p>
<div class="blog_h3"><span class="graybg">索引过滤器</span></div>
<p>索引过滤器决定查询优化器使用哪些索引来评估查询形状。当为查询形状指定了索引过滤器时，仅过滤器中包含的索引会被用来优化查询。</p>
<p>索引过滤器存在时，MongoDB会忽略 <pre class="crayon-plain-tag">hint()</pre>调用。鉴于此，谨慎的使用之。</p>
<p>要检查索引过滤器是否存在，获取db.collection.explain()或者cursor.explain()返回值的<pre class="crayon-plain-tag">indexFilterSet</pre>字段。</p>
<p>在服务器关闭后，索引过滤器不会持久化。MongoDB也提供了手工移除索引过滤器的命令。</p>
<div class="blog_h2"><span class="graybg">查询优化</span></div>
<p>本节内容简单的介绍一些优化查询的方向。</p>
<div class="blog_h3"><span class="graybg">使用索引</span></div>
<p>通过减少查询操作需要处理的数据的量，索引可以提升读操作、更新操作以及聚合管线部分阶段的性能。</p>
<p>如果你的业务通常基于某个、某些字段对集合进行查询，你可以考虑在字段上创建索引、复合索引。这可以避免查询操作进行全集合扫描。创建索引的示例代码：</p>
<pre class="crayon-plain-tag">db.inventory.createIndex( { type: 1 } )</pre>
<p>除了优化读操作外，索引还可以用来支持排序操作、优化存储空间利用。</p>
<p>由于MongoDB支持升序、降序读取索引，因此对于<span style="background-color: #c0c0c0;">单键索引来说，其方向不重要</span>。</p>
<p>在大部分情况下，查询优化器都会选择合适的索引。如果你需要强制指定一个索引，可以调用<pre class="crayon-plain-tag">hint()</pre></p>
<div class="blog_h3"><span class="graybg">使用$inc</span></div>
<p>该操作符用于增加或者减少字段的值。它在服务器端工作，不需要把原先的值取到客户端。</p>
<p>该操作符也避免了多个客户端同时get-and-set时的竞态条件。</p>
<div class="blog_h3"><span class="graybg">减少网络流量</span></div>
<p>如果知道需要返回的数据的量，可以使用limit()：</p>
<pre class="crayon-plain-tag">db.posts.find().sort( { timestamp : -1 } ).limit(10)</pre>
<p>另外，可以使用投影，仅仅返回需要的字段。 </p>
<div class="blog_h3"><span class="graybg">查询选择性</span></div>
<p>Query Selectivity用来度量查询断言（条件）过滤掉集合中文档的强度，针对主键的唯一性查询的选择性最高 —— 因为它只会匹配单个文档。查询选择性决定了是否能高效的使用索引，甚至是能否使用索引。</p>
<p>低选择性的常见例子是$nin、$ne查询操作符，它们通常都会匹配索引值域的很大一部分。这导致使用索引有时还不如直接全文档扫描快，因此索引不被使用。</p>
<p>如果使用正则式来指定字段值（条件），查询的选择性取决于正则式本身</p>
<div class="blog_h3"><span class="graybg">覆盖查询</span></div>
<p>所谓Covered Query是指索引即可满足查询所需的全部字段，不需要执行文档扫描的情况，需要配合投影使用：</p>
<pre class="crayon-plain-tag"># 复合索引
db.inventory.createIndex( { type: 1, item: 1 } )
# 覆盖查询
db.inventory.find(
    { type: "food", item:/^c/ },
    { item: 1, _id: 0 }  # 被覆盖，注意指定_id:0排除了主键字段，确保了覆盖
)</pre>
<p>覆盖查询通常具有很高的性能（相对那些需要检索文档的查询），原因包括：</p>
<ol>
<li>索引键值通常要比对应的文档小</li>
<li>索引常常驻留内存，或者在磁盘上顺序的分布 </li>
</ol>
<p>以下情况下，索引无法覆盖查询：</p>
<ol>
<li>任意索引字段，在任意一个文档中存储了数组时。当索引字段存储了数组后，索引称为多键索引（Multi-key Index），这种索引不支持覆盖查询</li>
<li>任何断言字段（条件）、投影字段（返回）是位于嵌入文档中的字段时</li>
</ol>
<p>针对分片集群的限制：当索引不包含分片键的时候，它不能覆盖针对分片集合的查询。一个例外是，查询断言仅针对_id且投影仅仅返回_id，即使_id不是分片键，也可以做到覆盖查询。</p>
<p>使用<pre class="crayon-plain-tag">db.collection.explain()</pre>调用可以查看目标查询是否是覆盖查询。</p>
<div class="blog_h2"><span class="graybg">写操作优化</span></div>
<div class="blog_h3"><span class="graybg">索引</span></div>
<p>集合上的每个索引，都增加了写入操作的成本。</p>
<p>对于插入/删除操作，MongoDB需要插入/删除集合上所有索引中的文档键。更新操作可能导致索引的一个子集的变更。</p>
<p>对于使用MMAPv1引擎的mongod，更新操作可能导致文档增长，超过为其分配的空间。这时MMAPv1需要把文档移动到一个新的地方，并更新索引索引，指向文档的新位置。这些成本较高，但是发生频率较低。</p>
<p>通常来说，索引带来的读性能提升，值得损耗写性能。尽管如此，不要盲目的创建索引，要评估既有索引是否真的有用。</p>
<div class="blog_h3"><span class="graybg">MMAPv1引擎相关</span></div>
<p>更新操作可能会改变文档的尺寸，例如添加新的字段。</p>
<p>对于MMAPv1引擎来说，如果更新操作导致文档尺寸超过当前分配尺寸，MongoDB会在磁盘上重新分配文档，确保有足够的连续空间可以存放文档。需要重新分配空间的更新操作，其效率更加低，特别是结合使用索引的情况下，因为需要修改文档的位置信息。</p>
<p>默认的，从3.0开始MongoDB总是分配2的N次方大小的空间。这可以尽量减少重新分配、高效的重用删除操作回收的空间，但是不能消除重新分配。</p>
<div class="blog_h3"><span class="graybg">存储性能</span></div>
<p>存储系统的硬件因子——随机存取能力、磁盘预读取、RAID等——对MongoDB写操作的性能影响很大。对于随机性的工作负载，SSD能够提供比HDD高100倍的性能。</p>
<p>为了防止意外宕机导致数据丢失，MongoDB使用预写式日志（write ahead logging）—— 变更首先发生在内存中，然后首先写入到日志。不直接写入存储引擎是因为日志文件是顺序写，速度快。如果MongoDB需要终止服务进程或者遭遇错误，可以使用日志文件恢复，把尚未完成的操作应用到存储引擎的数据文件中。</p>
<p>日志和数据文件的写入，存在对存储能力的争用，特别是二者保存在同一物理设备上时。</p>
<p>如果应用程序指定包含j选项的写关注，则mogod会减少日志写操作之间的间隔，从而增大总体的写负载。</p>
<p>日志写间隔可以通过运行时配置<pre class="crayon-plain-tag">commitIntervalMs</pre>来设置，减少此参数会增加写操作的数量，从而降低MongoDB的写容量。反之减少量写操作数量，却有更大概率在宕机时丢失数据。</p>
<div class="blog_h2"><span class="graybg">读懂执行计划</span></div>
<p>使用db.collection.explain()、cursor.explain()方法以及explain命令都可以获得执行计划相关的信息（包括执行统计信息）。</p>
<p>执行计划（上述方法或命令的结果）以阶段（stage）树的形式呈现。每个stage把自身的结果（例如文档或者索引键）传递给父节点。叶子节点访问文档或者索引，中间节点操控文档或者索引键，根节点是最终的stage，MongoDB从中获得结果集。</p>
<p>Stage是操作的描述，例如：</p>
<ol>
<li>COLLSCAN 全集合扫描</li>
<li>IXSCAN 索引键扫描</li>
<li>FETCH  读取文档</li>
<li>SHARD_MERGE 合并来自分片的结果</li>
<li>AND_SORTED 可在索引交叉时出现</li>
<li>AND_HASH 可在索引交叉时出现</li>
</ol>
<div class="blog_h3"><span class="graybg">输出内容</span></div>
<p>执行计划以JSON格式输出，重要的字段包括：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">字段</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td colspan="2">
<p><em><strong>queryPlanner 被查询优化器选取的执行计划的详细信息</strong></em></p>
<p>示例输出：</p>
<pre class="crayon-plain-tag">{
   "queryPlanner" : {
      "plannerVersion" : &lt;int&gt;,
      "namespace" : &lt;string&gt;,
      "indexFilterSet" : &lt;boolean&gt;,
      "parsedQuery" : {
         ...
      },
      "winningPlan" : {
         "stage" : &lt;STAGE1&gt;,
         ...
         "inputStage" : {
            "stage" : &lt;STAGE2&gt;,
            ...
            "inputStage" : {
               ...
            }
         }
      },
      "rejectedPlans" : [
         &lt;candidate plan 1&gt;,
         ...
      ]
  }
}</pre>
</td>
</tr>
<tr>
<td>namespace</td>
<td>查询在什么名字空间上运行，例如&lt;database&gt;.&lt;collection&gt;</td>
</tr>
<tr>
<td>indexFilterSet</td>
<td>应用到此查询形状的索引过滤器</td>
</tr>
<tr>
<td>winningPlan</td>
<td>
<p>被查询优化器选中的执行计划的细节信息，以Stage的树的形式呈现
</td>
</tr>
<tr>
<td>w***P.stage</td>
<td>Stage的名称</td>
</tr>
<tr>
<td>w***P.inputStage</td>
<td>Stage的输入（单个子节点）</td>
</tr>
<tr>
<td>w***P.inputStages</td>
<td>Stage的输入（多个子节点）</td>
</tr>
<tr>
<td>w***P.shards</td>
<td>针对每个分片的信息</td>
</tr>
<tr>
<td>rejectedPlans</td>
<td>被拒绝的候选计划的列表</td>
</tr>
<tr>
<td colspan="2">
<p><strong><em>executionStats 被选中计划的执行情况</em></strong></p>
<p>示例输出：</p>
<pre class="crayon-plain-tag">"executionStats" : {
   "executionSuccess" : &lt;boolean&gt;,
   "nReturned" : &lt;int&gt;,
   "executionTimeMillis" : &lt;int&gt;,
   "totalKeysExamined" : &lt;int&gt;,
   "totalDocsExamined" : &lt;int&gt;,
   "executionStages" : {
      "stage" : &lt;STAGE1&gt;
      "nReturned" : &lt;int&gt;,
      "executionTimeMillisEstimate" : &lt;int&gt;,
      "works" : &lt;int&gt;,
      "advanced" : &lt;int&gt;,
      "needTime" : &lt;int&gt;,
      "needYield" : &lt;int&gt;,
      "isEOF" : &lt;boolean&gt;,
      ...
      "inputStage" : {
         "stage" : &lt;STAGE2&gt;,
         ...
         "nReturned" : &lt;int&gt;,
         "executionTimeMillisEstimate" : &lt;int&gt;,
         "keysExamined" : &lt;int&gt;,
         "docsExamined" : &lt;int&gt;,
         ...
         "inputStage" : {
            ...
         }
      }
   },
   "allPlansExecution" : [
      { &lt;partial executionStats1&gt; },
      { &lt;partial executionStats2&gt; },
      ...
   ]
}</pre>
</td>
</tr>
<tr>
<td> nReturned</td>
<td>匹配查询的文档数量</td>
</tr>
<tr>
<td>executionTimeMillis</td>
<td>包含查询计划选择、计划执行在内的总计消耗时间</td>
</tr>
<tr>
<td>totalKeysExamined</td>
<td>总计扫描的索引条目数量</td>
</tr>
<tr>
<td>totalDocsExamined</td>
<td>总计扫描的文档的数量</td>
</tr>
<tr>
<td>executionStages</td>
<td>被选中计划各阶段的执行细节统计信息zbook g4</td>
</tr>
<tr>
<td>e***S.works</td>
<td>
<p>该阶段执行的工作单元数量，查询执行过程把整个工作划分为细小的单元。一个单元可能包括：
<ol>
<li>检查单个索引键</li>
<li>取回单个文档</li>
<li>对单个文档进行投影</li>
</ol>
</td>
</tr>
<tr>
<td>e***S.advanced</td>
<td>返回（或者提升，advance）到父Stage的中间结果数量</td>
</tr>
<tr>
<td>e***S.needTime</td>
<td>不是用来返回中间结果到父Stage的工作周期数。例如一个索引扫描Stage可能花费一个工作周期来定位索引的下一个位置，不是把所有工作周期都用来向父节点返回索引键</td>
</tr>
<tr>
<td>e***S.needYield</td>
<td>存储层请求查询系统让出它的锁的次数</td>
</tr>
<tr>
<td>e***S.isEOF</td>
<td>指出Stage是否到达的流的尾部</td>
</tr>
<tr>
<td>e***S.shards</td>
<td>针对每个分片的信息</td>
</tr>
<tr>
<td>e***S.inputStage.keysExamined</td>
<td>
<p>对于扫描索引的Stage（例如IXSCAN），该字段表示被检查的键的总数：</p>
<ol>
<li>对于单个连续范围的索引扫描，总数仅仅包含界内（in-bounds）键</li>
<li>对于多个非连续范围的索引扫描，总是还包括界外键，这些键虽然对结果无用，但是可能还是要读取（以便找到下一范围的起点）</li>
</ol>
</td>
</tr>
<tr>
<td>e***S.inputStage.docsExamined</td>
<td>
<p>该字段出现在文档扫描（COLLSCAN）阶段，或者FETCH之类取回文档的阶段</p>
<p>总计扫描的文档数量</p>
</td>
</tr>
<tr>
<td colspan="2"><strong><em>serverInfo 返回MongoDB实例的信息</em></strong></td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">应用示例</span></div>
<p>准备数据：</p>
<pre class="crayon-plain-tag">db.collection( 'inventory' ).insertMany(
    [
        { "_id": 1, "item": "f1", type: "food", quantity: 500 },
        { "_id": 2, "item": "f2", type: "food", quantity: 100 },
        { "_id": 3, "item": "p1", type: "paper", quantity: 200 },
        { "_id": 4, "item": "p2", type: "paper", quantity: 150 },
        { "_id": 5, "item": "f3", type: "food", quantity: 300 },
        { "_id": 6, "item": "t1", type: "toys", quantity: 500 },
        { "_id": 7, "item": "a1", type: "apparel", quantity: 250 },
        { "_id": 8, "item": "a2", type: "apparel", quantity: 400 },
        { "_id": 9, "item": "t2", type: "toys", quantity: 50 },
        { "_id": 10, "item": "f4", type: "food", quantity: 75 }
    ]
)</pre>
<p>获取不使用索引时的执行计划： </p>
<pre class="crayon-plain-tag">require( 'promise.prototype.finally' ).shim();
client.connect( 'mongodb://localhost:27017/local' ).then( db =&gt; {
    db.collection( 'inventory' ).find(
        { quantity: { $gte: 100, $lte: 200 } }
    ).explain( "executionStats" ).then( r =&gt; {
        console.log( r.queryPlanner.winningPlan.stage );    // COLLSCAN 表示全集合扫描
        console.log( r.executionStats.nReturned );          // 匹配并返回3条数据
        console.log( r.executionStats.totalDocsExamined );  // 总计检查10条（全部）数据
    } ).catch( e =&gt; console.log( e ) ).finally( () =&gt; process.exit() );
} );</pre>
<p>创建一个索引：</p>
<pre class="crayon-plain-tag">db.collection( 'inventory' ).createIndex( { quantity: 1 } )</pre>
<p>执行计划现在为：</p>
<pre class="crayon-plain-tag">console.log( r.queryPlanner.winningPlan.inputStage.stage );  // IXSCAN 表示索引扫描（在子Stage完成）
console.log( r.queryPlanner.winningPlan.stage );             // FETCH 直接抓取数据
console.log( r.executionStats.nReturned );                   // 匹配并返回3条数据
console.log( r.executionStats.totalDocsExamined );           // 总计检查3条数据

// 打印整个计划：
const util = require( 'util' );&lt;br&gt;console.log( util.inspect( r, { depth: null, colors: true } ) );
// 输出：
{ queryPlanner: 
   { plannerVersion: 1,
     namespace: 'local.inventory',
     indexFilterSet: false,
     parsedQuery: { '$and': [ { quantity: { '$lte': 200 } }, { quantity: { '$gte': 100 } } ] },
     winningPlan: 
      { stage: 'FETCH',
        inputStage: 
         { stage: 'IXSCAN',
           keyPattern: { quantity: 1 },
           indexName: 'quantity_1',
           isMultiKey: false,
           multiKeyPaths: { quantity: [] },
           isUnique: false,
           isSparse: false,
           isPartial: false,
           indexVersion: 2,
           direction: 'forward',
           indexBounds: { quantity: [ '[100, 200]' ] } } },
     rejectedPlans: [] },
  executionStats: 
   { executionSuccess: true,
     nReturned: 3,
     executionTimeMillis: 0,
     totalKeysExamined: 3,
     totalDocsExamined: 3,
     executionStages: 
      { stage: 'FETCH',
        nReturned: 3,
        executionTimeMillisEstimate: 0,
        works: 4,
        advanced: 3,
        needTime: 0,
        needYield: 0,
        saveState: 0,
        restoreState: 0,
        isEOF: 1,
        invalidates: 0,
        docsExamined: 3,
        alreadyHasObj: 0,
        inputStage: 
         { stage: 'IXSCAN',
           nReturned: 3,
           executionTimeMillisEstimate: 0,
           works: 4,
           advanced: 3,
           needTime: 0,
           needYield: 0,
           saveState: 0,
           restoreState: 0,
           isEOF: 1,
           invalidates: 0,
           keyPattern: { quantity: 1 },
           indexName: 'quantity_1',
           isMultiKey: false,
           multiKeyPaths: { quantity: [] },
           isUnique: false,
           isSparse: false,
           isPartial: false,
           indexVersion: 2,
           direction: 'forward',
           indexBounds: { quantity: [ '[100, 200]' ] },
           keysExamined: 3,
           seeks: 1,
           dupsTested: 0,
           dupsDropped: 0,
           seenInvalidated: 0 } },
     allPlansExecution: [] },
  serverInfo: 
   { host: '226ce0b60d62',
     port: 27017,
     version: '3.4.5',
     gitVersion: '520b8f3092c48d934f0cd78ab5f40fe594f96863' },
  ok: 1 
}</pre>
<div class="blog_h2"><span class="graybg">评估操作性能</span></div>
<p>本节介绍几种评估MongoDB操作性能的技术。</p>
<div class="blog_h3"><span class="graybg">数据库剖析器</span></div>
<p>MongoDB提供了一个数据库剖析器（database profiler ），它能够显示数据库中每个查询的性能特征。使用该剖析器可以定位运行缓慢的读写操作。</p>
<div class="blog_h3"><span class="graybg">db.currentOp()</span></div>
<p>该调用可以显示当前mongod实例上正在执行的操作的各项参数。调用方式：</p>
<pre class="crayon-plain-tag">db.currentOp({filterDocument})
// 示例
db.currentOp( { query: { $exists: true } , ns: 'bais.corps' } ).inprog</pre>
<p>可以传递一个过滤文档作为参数，该文档可以包含以下字段：</p>
<ol>
<li>$ownOps，如果设置为true，仅仅显示当前用户的操作</li>
<li>$all，如果设置为true，返回所有操作的信息，包括那些空闲连接上的操作、系统操作</li>
<li>任何输出字段都可以作为过滤条件使用 </li>
</ol>
<p>在单个mongod实例、复制集上，该调用的输出格式为：</p>
<pre class="crayon-plain-tag">{
    "inprog" : [/* 正在执行的操作列表 */], 
    "ok" : 1.0
}</pre>
<p>在分片集群的mongos上，该调用的输出格式为：</p>
<pre class="crayon-plain-tag">{ 
    // 每个分片的情况
    "raw" : {
        // 分片名称为键
        "rs1/mongo-11.gmem.cc:27017,mongo-12.gmem.cc:27017,mongo-13.gmem.cc:27017" : {
            "inprog" : [/* 正在执行的操作列表 */], 
            "ok" : 1.0, 
            "$gleStats" : {
                "lastOpTime" : Timestamp(0, 0), 
                "electionId" : ObjectId("7fffffff000000000000000e")
            }
        }
    }, 
    // 当前mongos上的情况
    "inprog" : [/* 正在执行的操作列表 */], 
    "ok" : 1.0
}</pre>
<p>上述输出的核心是inprog属性，它包括以下字段：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">字段</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>desc</td>
<td>客户端的描述，其中包含了connectionId</td>
</tr>
<tr>
<td>threadId</td>
<td>用于处理此数据库连接的线程标识 </td>
</tr>
<tr>
<td>connectionId</td>
<td>发起操作的连接的标识符 </td>
</tr>
<tr>
<td>client</td>
<td>客户端的地址和端口，例如<pre class="crayon-plain-tag">"client" : "172.21.1.1:44938" </pre></td>
</tr>
<tr>
<td>appName</td>
<td>客户端提供的应用程序名称</td>
</tr>
<tr>
<td>opid</td>
<td>操作的标识符，可以传递给<pre class="crayon-plain-tag">db.killOp()</pre></td>
</tr>
<tr>
<td>active</td>
<td>
<p>提示该操作是否已经启动的布尔值：true表示操作已经启动；false表示操作空闲（idle）。当一个操作让出锁（yielded）给其它操作的情况下，此字段仍然为true </p>
</td>
</tr>
<tr>
<td>secs_running</td>
<td>操作已经持续的时间 。仅仅active=true时存在 </td>
</tr>
<tr>
<td>microsecs_running</td>
<td>操作已经持续的时间，以微秒计算。仅仅active=true时存在 </td>
</tr>
<tr>
<td>op</td>
<td>
<p>该操作的类型：none、update、insert、query、command、getmore、remove、killcursors</p>
<p>其中：</p>
<ol>
<li>query包含了读操作，不包含其它类型的CUD操作</li>
<li>command包含了大部分<a href="https://docs.mongodb.com/manual/reference/command/">数据库命令</a></li>
<li>insert, update,  delete分别对应插入、更新、删除</li>
<li>getmore 游标抓取操作</li>
</ol>
</td>
</tr>
<tr>
<td>ns</td>
<td>操作针对的名字空间，<pre class="crayon-plain-tag">&lt;database&gt;.&lt;collection&gt;</pre>形式</td>
</tr>
<tr>
<td>insert</td>
<td>如果op为insert则存在，包含正在被插入的文档</td>
</tr>
<tr>
<td>query</td>
<td>如果op不为insert则存在，包含查询、删除、更新的过滤文档。对于getmore操作，包含了对应的find的过滤文档或者aggregate的Stages文档</td>
</tr>
<tr>
<td>planSummary</td>
<td>执行计划的概要信息，便于调试缓慢查询</td>
</tr>
<tr>
<td>locks</td>
<td>
<p>当前操作持有的锁的类型：</p>
<ol>
<li>Global 全局锁</li>
<li>MMAPV1Journal MMAPv1的日志锁，用于同步日志写</li>
<li>Database 数据库级别锁</li>
<li>Collection 文档级别锁</li>
<li>Metadata 元数据锁</li>
<li>oplog oplog锁 </li>
</ol>
<p>以及锁定模式：</p>
<ol>
<li>R 共享锁</li>
<li>W 独占锁</li>
<li>r 共享意向锁</li>
<li>w 独占意向锁</li>
</ol>
</td>
</tr>
<tr>
<td>waitingForLock</td>
<td>当前操作是否在等待锁</td>
</tr>
<tr>
<td>msg</td>
<td>描述操作状态、进度的字符串 </td>
</tr>
<tr>
<td>progress</td>
<td>描述mapReduce或者索引构建的进度</td>
</tr>
<tr>
<td>killPending</td>
<td>当前操作是否被标记为要杀死。当操作进入下一个安全点后会终结</td>
</tr>
<tr>
<td>numYields</td>
<td>当前操作让出锁以便其它操作进行的次数 </td>
</tr>
<tr>
<td>fsyncLock</td>
<td>当前数据库是否被db.fsyncLock()锁定 </td>
</tr>
<tr>
<td>info</td>
<td>仅仅fsyncLock为true时存在，描述如何解锁数据库</td>
</tr>
<tr>
<td>lockStats</td>
<td>
<p>对于每类锁（类型+模式组合），报告以下统计信息：</p>
<ol>
<li>acquireCount 获取到锁的次数</li>
<li>acquireWaitCount 尝试获取锁时，进行的等待次数</li>
<li>timeAcquiringMicros  累计等到锁的时间</li>
<li>deadlockCount 等待锁的时候一共遭遇的死锁次数</li>
</ol>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">mongotop</span></div>
<p>这是一个命令行工具，查看读写最繁忙的集合。该命令不能在mongos上执行。示例：</p>
<pre class="crayon-plain-tag">mongotop -u root -p root --authenticationDatabase admin

#                   ns    total    read    write    2017-08-25T15:33:53+08:00
#       local.oplog.rs      1ms     1ms      0ms                             
#   admin.system.roles      0ms     0ms      0ms                             
#   admin.system.users      0ms     0ms      0ms                             
# admin.system.version      0ms     0ms      0ms                             
#           bais.corps      0ms     0ms      0ms                             
#       bais.corptypes      0ms     0ms      0ms                             
#            bais.orgs      0ms     0ms      0ms                             
#  bais.system.indexes      0ms     0ms      0ms                             
#          bais.trades      0ms     0ms      0ms                             
#             local.me      0ms     0ms      0ms</pre>
<div class="blog_h3"><span class="graybg">mongostat</span></div>
<p>这是一个命令行工具，可以快速的查看当前mongos/mongod的概览信息，类似于vmstat：</p>
<pre class="crayon-plain-tag">mongostat -u root -p root --authenticationDatabase admin
# insert query update delete getmore command flushes mapped vsize   res faults qrw arw net_in net_out conn                time
#    *0    *0     *0     *0       0     2|0       0     0B  300M 19.0M      0 0|0 0|0   288b   16.8k    8 Aug 25 15:15:14.827</pre>
<p>该命令的输出，反映每秒的平均统计指标。字段列表：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 15%; text-align: center;">字段</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>inserts</td>
<td>插入文档数量</td>
</tr>
<tr>
<td>query</td>
<td>查询操作的数量</td>
</tr>
<tr>
<td>update</td>
<td>更新操作的数量</td>
</tr>
<tr>
<td>update</td>
<td>删除操作的数量</td>
</tr>
<tr>
<td>getmore</td>
<td>游标抓取操作的数量</td>
</tr>
<tr>
<td>command</td>
<td>执行命令的数量，在复制集的从节点上，输出为local|replicated格式</td>
</tr>
<tr>
<td>flushes</td>
<td>对于 WiredTiger，为触发的检查点数量；对于 MMAPv1，为fsync操作数量</td>
</tr>
<tr>
<td>dirty</td>
<td>仅WiredTiger，WiredTiger缓存中脏字节占比</td>
</tr>
<tr>
<td>used</td>
<td>仅WiredTiger，WiredTiger缓存当前正被使用的占比</td>
</tr>
<tr>
<td>mapped</td>
<td>仅 MMAPv1，从上次mongostat调用以后，累计映射到内存的数据量（MB）</td>
</tr>
<tr>
<td>vsize</td>
<td>从上次mongostat调用以后，虚拟内存用量（MB）</td>
</tr>
<tr>
<td>non-mapped</td>
<td>仅 MMAPv1，从上次mongostat调用以后，累计没有映射到内存的数据量（MB）。仅仅使用--all 选项时出现</td>
</tr>
<tr>
<td>res</td>
<td>从上次mongostat调用以后，MongoDB进程使用的驻留内存累计数量</td>
</tr>
<tr>
<td>faults</td>
<td>仅 MMAPv1，页面错误次数</td>
</tr>
<tr>
<td>lr</td>
<td>仅 MMAPv1，多少百分比的读操作必须等待锁</td>
</tr>
<tr>
<td>lw</td>
<td>仅 MMAPv1，多少百分比的写操作必须等待锁</td>
</tr>
<tr>
<td>lrt</td>
<td>仅 MMAPv1，等待读锁消耗的平均时间</td>
</tr>
<tr>
<td>lwt</td>
<td>仅 MMAPv1，等待写锁消耗的平均时间</td>
</tr>
<tr>
<td>idx miss</td>
<td>仅 MMAPv1，导致页面错误的索引访问操作的占比</td>
</tr>
<tr>
<td>qr</td>
<td>等待读操作的队列深度</td>
</tr>
<tr>
<td>qw</td>
<td>等待写操作的队列深度</td>
</tr>
<tr>
<td>ar</td>
<td>活动的、正在执行读操作的客户端数量</td>
</tr>
<tr>
<td>aw</td>
<td>活动的、正在执行写操作的客户端数量</td>
</tr>
<tr>
<td>netIn</td>
<td>入站网络流量，单位字节</td>
</tr>
<tr>
<td>netOut</td>
<td>出站网络流量，单位字节</td>
</tr>
<tr>
<td>conn</td>
<td>打开的连接数</td>
</tr>
<tr>
<td>repl</td>
<td>成员的复制状态：<br />M 主节点<br />SEC 从节点<br />REC 正在恢复<br />UNK 未知<br />SLV 主从复制模式的从节点<br />RTR mongos节点<br />ARB 仲裁节点</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">解释计划</span></div>
<p>方法<pre class="crayon-plain-tag">cursor.explain()</pre>和<pre class="crayon-plain-tag">db.collection.explain()</pre>可以返回关于查询的执行计划，例如选取什么索引、执行的统计信息。</p>
<p>你可以在queryPlanner、executionStats 、allPlansExecution三种模式下运行这些方法，获取多或少的信息。</p>
<div class="blog_h1"><span class="graybg">聚合</span></div>
<p>聚合操作处理数据记录，并返回计算过后的结果。这类操作将多个文档中的数值进行分组，通过多种数学计算将其合并为单个数值。</p>
<p>MongoDB提供三种聚合操作途径：聚合管线（aggregation pipeline）、Map-Reduce函数（map-reduce function）、单意图聚合方法（single purpose aggregation methods） 。</p>
<div class="blog_h2"><span class="graybg">聚合管线</span></div>
<p>MongoDB的聚合框架，以数据处理管线的概念进行建模 —— 多个文档进入由多个阶段（stage）构成的管道，并被管道转换为单个聚合后的结果。每个阶段都会对文档进行某种转换。每个阶段的输入、输出文档没有一一对应关系，一个文档可以产生多个新文档，多个文档也可能被合并为单个文档。</p>
<p>最基本管线stage，提供<span style="background-color: #c0c0c0;">过滤器功能</span>，工作方式类似于查询和文档转换，修改输出文档的形式。</p>
<p>其它管线stage，提供依据指定字段来<span style="background-color: #c0c0c0;">分组、排序文档的工具</span>，以及<span style="background-color: #c0c0c0;">聚合数组（包括文档的数据）内容的工具</span>。此外，管线stage可以<span style="background-color: #c0c0c0;">使用操作符</span>来计算平均值、连接字符串…等操作。</p>
<p>通过MongoDB内置的native操作，管线可以提供高效的数据聚合能力。管线是最优选的聚合途径。</p>
<p>聚合管线可以支持分片集合。</p>
<p>在某些stage，聚合管线会利用索引来改善性能。聚合管线声明周期中，具有内部的优化阶段。</p>
<p>要使用聚合管线，可以调用<pre class="crayon-plain-tag">db.collection.aggregate( arrayOfStages )</pre>或者<pre class="crayon-plain-tag">aggregate</pre>命令。</p>
<div class="blog_h3"><span class="graybg">管线表达式</span></div>
<p>某些stage需要一个管线表达式来作为操作数，管线表达式说明如何来转换输入文档。表达式的格式类似于文档，并且可以包含其它表达式。</p>
<p>管线表达式仅可以操作管线中的当前文档，无法引用其它文档中的数据。通常来说，表达式都是无状态的，例外是那些累加器表达式。累加器用在<pre class="crayon-plain-tag">$group</pre>阶段，需要维护自身状态（例如总数、最大值）。</p>
<p>从3.2开始，某些累加器可以用在<pre class="crayon-plain-tag">$project</pre>阶段。 但是用在此阶段时不能跨文档的维护自身的状态。</p>
<div class="blog_h3"><span class="graybg">管线的行为</span></div>
<p>聚合管线操控单个集合，在逻辑上，是把整个集合推到管线上进行处理。为了优化性能，仅可能的使用以下策略避免全集合扫描：</p>
<ol>
<li>管线操作符和索引：当出现在管线的开头时，$match、$sort等管线操作可以使用索引。$geoNear 可以使用地理空间索引。从3.2开始，索引可以覆盖聚合管线，而避免扫描集合</li>
<li>尽早的过滤：如果聚合操作仅仅需要集合的一个子集，可以使用$match、$limit、$skip等stage来限制进入管线开头的文档数量</li>
<li>内部优化阶段（optimization phase）</li>
</ol>
<div class="blog_h3"><span class="graybg">管线优化</span></div>
<p>聚合管线内部有一个优化阶段，会尝试对管线进行塑形，以改善性能。要了解此优化阶段是如何工作的，以explain选项来调用aggregate()方法。随着MongoDB的版本发布，管线优化的实现可能会变化。</p>
<div class="blog_h3"><span class="graybg">管线的限制</span></div>
<table class="full-width fixed-word-wrap">
<tbody>
<tr>
<td>
<p><strong><em>结果集大小的限制</em></strong></p>
<p>从2.6开始，aggregate命令可以返回一个游标，或者把结果存储在一个集合中。每个文档的大小限制当前为16MB（BSON文档尺寸限制），如果某个文档超过此限制，命令会报错。注意这个限制仅仅针对作为结果的文档，在管线中间流转的文档不受限制。</p>
<p>从2.6开始aggregate()方法默认返回游标。对于aggregate命令来说，如果不指定cursor选项，也不在集合中存储结果，结果集会存放在一个大的文档中，该文档可能超过16MB限制而报错</p>
</td>
</tr>
<tr>
<td>
<p><strong><em>内存限制</em></strong></p>
<p>管线的stage使用内存的限制是100MB，如果某个stage超过此限制，MongoDB会报错。为了处理大型数据集，应该使用allowDiskUse选项，以便stage的临时结果可以存储在磁盘上</p>
<p>从3.4开始，$graphLookup 阶段必须受限于100MB内存，allowDiskUse: true对该stage无效</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">关于分片集合</span></div>
<p>聚合管线支持针对分片集合进行操作，但是具有一些特殊的行为。</p>
<p>如果管线以精确匹配针对某个分片键的$match开头，则整个管线在匹配的分片上运行。在3.2之前的行为是，管线分拆在所有管线上运行，并且由主分片负责合并最后结果。</p>
<p>对于必须运行在多个分片上的聚合操作，如果不是必须在主分片上运行，这些操作会把结果路由到随机的分片上，由该分片负责合并结果，避免增加主分片的负担。$out、$lookup操作必须在主分片上运行。</p>
<div class="blog_h3"><span class="graybg">Stages</span></div>
<p>db.collection.aggregate()方法的参数是一个数组，每个数组元素表示一个阶段（Stage）。阶段是单键对象，键的名称以$开头，列于下面的表格中。</p>
<p>除了<pre class="crayon-plain-tag">$out</pre>、<pre class="crayon-plain-tag">$geoNear</pre>之外的所有Stage都<span style="background-color: #c0c0c0;">可以出现多次</span>。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 16%; text-align: center;">Stage</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>$collStats</td>
<td>
<p>依据集合或者视图来返回统计信息，3.4新增：</p>
<pre class="crayon-plain-tag">{
  $collStats:
    {
      // 在输出文档中添加延迟统计信息，histogram:true表示添加延迟直方图信息
      latencyStats: { histograms: &lt;boolean&gt; },
      // 在输出文档中添加存储统计信息
      storageStats: {}
    }
}</pre>
<p>输出文档包含以下字段：<br />ns  请求视图/集合的名字空间<br />localTime  服务器当前时间<br />latencyStats  和目标视图/集合有关的请求延迟信息集合<br />storageStats  和目标视图/集合有关的存储引擎统计信息</p>
</td>
</tr>
<tr>
<td>$project</td>
<td>
<p>对输入文档进行重新构形：<pre class="crayon-plain-tag">{ $project: { &lt;specification(s)&gt; } }</pre> </p>
<p>规格可以包含以下形式的字段：<br /><pre class="crayon-plain-tag">&lt;field&gt;: &lt;1 or true&gt;</pre>  包含输入文档的某些字段<br /><pre class="crayon-plain-tag">_id: &lt;0 or false&gt;</pre>  禁用输入文档的_id字段<br /><pre class="crayon-plain-tag">&lt;field&gt;: &lt;expression&gt;</pre>  以表达式的结果，添加/覆盖一个字段<br /><pre class="crayon-plain-tag">&lt;field&gt;:&lt;0 or false&gt;</pre>  排除一个除了_id之外的字段，一旦使用该形式，前面所有形式不得使用</p>
</td>
</tr>
<tr>
<td>$match</td>
<td>
<p>对输入文档集进行过滤，仅仅允许满足条件的文档进入下一Stage：</p>
<pre class="crayon-plain-tag">{ $match: { &lt;query&gt; } }</pre>
<p>查询条件的规格，与读操作的过滤条件语法一致 </p>
</td>
</tr>
<tr>
<td>$redact</td>
<td>
<p>根据文档本身存储的内容，来限制文档的内容：<pre class="crayon-plain-tag">{ $redact: &lt;expression&gt; }</pre> </p>
</td>
</tr>
<tr>
<td>$limit</td>
<td>限制传递到下一Stage的文档数量：<pre class="crayon-plain-tag">{ $limit: &lt;positive integer&gt; }</pre></td>
</tr>
<tr>
<td>$skip</td>
<td>跳过指定数量的文档，然后把剩下的传递到下一Stage：<pre class="crayon-plain-tag">{ $skip: &lt;positive integer&gt; }</pre></td>
</tr>
<tr>
<td>$unwind</td>
<td>展开某个数组字段，每个数组元素替换该字段形成一个输出文档，对于N元素的数组字段，形成N个输出文档</td>
</tr>
<tr>
<td>$group</td>
<td>
<p>根据指定的_id表达式来分组文档，可选的，应用一个或者多个累加器表达式</p>
<p>对于每个特定的_id表达式组合，输出一个文档 </p>
</td>
</tr>
<tr>
<td>$sample</td>
<td>从输入中随机选择指定数量的文档 </td>
</tr>
<tr>
<td>$sort</td>
<td>
<p>根据指定的key重新排序文档流，改变的仅仅是顺序，每个文档不会改变：</p>
<pre class="crayon-plain-tag"># 1表示升序，-1表示降序。前面的字段优先排序
{ $sort: { &lt;field1&gt;: &lt;sort order&gt;, &lt;field2&gt;: &lt;sort order&gt; ... } }</pre>
</td>
</tr>
<tr>
<td>$geoNear</td>
<td>
<p>根据距离指定地理空间点的远近对文档流进行排序，输出文档包括额外的distance字段，还可以包含地理位置标识符字段：
<pre class="crayon-plain-tag">{
    $geoNear: {
        spherical: 'boolean = false，使用2dsphere索引时应设为true，决定如何计算距离',
        limit: 'number = 100，返回的最大文档数',
        num: '功能和limit相同，优先级高',
        maxDistance: 'number，可返回文档距离中心点的最远多少',
        query: 'document，对输入文档进行过滤',
        distanceMultiplier: 'number，对查询结果的距离字段进行倍乘',
        near: '中心点，使用2dsphere索引时类型为GeoJSON点或者坐标对，使用2d索引时类型为坐标对',
        distanceField: 'string，输出文档字段，该字段包含计算出的距离，可以使用点号来指定为嵌入文档字段',
        minDistance: 'number，可返回文档距离中心点的最近多少',
        includeLocs:'string，可选，存放用来计算距离的那个点的坐标的输出文档字段'
    }

}</pre>
<p>注意：</p>
<ol>
<li>只能作为第一个Stage</li>
<li>必须指定distanceField</li>
<li>该Stage要求目标集合上建立地理空间索引（geospatial index）</li>
<li>该Stage要求目标集合最多具有一个2d或者2dsphere索引</li>
<li>你不需要指定集合中什么字段是存放了GeoJSON点或者坐标对（coordinate pair ），因为MongoDB可以从唯一地理空间索引推导出该字段</li>
<li>在query中不能指定$near操作符</li>
<li>不支持针对视图进行操作</li>
</ol>
<p>示例：</p>
<pre class="crayon-plain-tag">db.places.aggregate([
   {
     $geoNear: {
        near: { type: "Point", coordinates: [ -73.99279 , 40.719296 ] },
        // 距离字段
        distanceField: "dist.calculated",
        maxDistance: 2,
        query: { type: "public" },
        // 存放用来计算到中心点距离的输入值的字段
        includeLocs: "dist.location", 
        num: 5,
        spherical: true
     }
   }
])

// 输出文档示例：
{
   "_id" : 8,
   "name" : "Sara D. Roosevelt Park",
   "type" : "public",
   "location" : {
      "type" : "Point",
      "coordinates" : [ -73.9928, 40.7193 ]
   },
   "dist" : {
      "calculated" : 0.9539931676365992,
      "location" : {
         "type" : "Point",
         "coordinates" : [ -73.9928, 40.7193 ]
      }
   }
}</pre>
</td>
</tr>
<tr>
<td>$lookup</td>
<td>
<p>对同一数据库中的另外一个非分片集合执行左外连接操作：
<pre class="crayon-plain-tag">{
   $lookup:
     {
       from: '被左外连接的集合',
       localField: '输入文档中用于匹配（等于）的字段',
       foreignField: '被连接集合中用于匹配的字段',
       as: '被连接文档在输出中对应的字段名'
     }
}</pre>
<p>示例：</p>
<p><pre class="crayon-plain-tag">// 订单左外连接库存
db.orders.aggregate([
    {
      $lookup:
        {
          from: "inventory",
          localField: "item",
          foreignField: "sku",
          as: "inventory_docs"
        }
   }
])
// 输出文档示例
{
  "_id" : 1,
   "item" : "abc",
  "price" : 12,
  "quantity" : 2,
  // 匹配的被连接文档，数组形式
  "inventory_docs" : [
    { "_id" : 1, "sku" : "abc", description: "product 1", "instock" : 120 }
  ]
}</pre>
</td>
</tr>
<tr>
<td>$out</td>
<td>必须作为最后一个Stage，把输入文档写到目标集合中：<pre class="crayon-plain-tag">{ $out: "&lt;output-collection&gt;" }</pre> </td>
</tr>
<tr>
<td>$indexStats</td>
<td>返回集合中每个索引使用情况的统计信息</td>
</tr>
<tr>
<td>$facet</td>
<td> 在单个Stage中处理多个聚合管线，这些管线针对同一组输入文档进行</td>
</tr>
<tr>
<td>$bucket</td>
<td>
<p>基于指定的表达式和桶边界，将输入文档划分到称为桶（bucket）的组中：</p>
<pre class="crayon-plain-tag">{
  $bucket: {
      // 分组的依据，针对每个输入文档进行评估
      groupBy: &lt;expression&gt;,
      // 桶的划分边界，[)区间，3个值确定2个区间，起始值作为桶的_id
      boundaries: [ &lt;lowerbound1&gt;, &lt;lowerbound2&gt;, ... ],
      // 如果groupBy的值没有落到boundaries声明的任何区间，则归入此_id为此值的桶
      default: &lt;literal&gt;,
      // 每个桶的输出字段列表，_id不需要指定
      output: {
         &lt;output1&gt;: { &lt;$accumulator expression&gt; },
         ...
         &lt;outputN&gt;: { &lt;$accumulator expression&gt; }
      }
   }
}

// 油画拍卖的例子
{ "_id" : 1, "title" : "社会的支柱", "artist" : "格罗斯", "year" : 1926,"price" : 199 }
// 聚合管线
db.artwork.aggregate( [
  {
    $bucket: {
      // 根据价格区间分组
      groupBy: "$price",
      boundaries: [ 0, 200, 400 ],
      default: "Other",
      output: {
        // 输出油画总数
        "count": { $sum: 1 },
        // 油画名称的数组
        "titles" : { $push: "$title" }
      }
    }
  }
] )
// 输出
{
  "_id" : 0,
  "count" : 1,
  "titles" : [
    "The Pillars of Society"
  ]
} </pre>
</td>
</tr>
<tr>
<td>$bucketAuto</td>
<td>
<p>与$bucket，只是boundaries不需要指定，根据需要的桶的数量自动划分：
<pre class="crayon-plain-tag">{
  $bucketAuto: {
      groupBy: &lt;expression&gt;,
      // 期望的分组的数量
      buckets: &lt;number&gt;,
      output: {
         &lt;output1&gt;: { &lt;$accumulator expression&gt; },
         ...
      }
      granularity: &lt;string&gt;
  }
} </pre>
</td>
</tr>
<tr>
<td>$sortByCount</td>
<td>
<p>根据指定表达式的值对输入文档进行分组，并且计算每个分组中文档的数量：
<pre class="crayon-plain-tag">{ $sortByCount:  &lt;expression&gt; }
// 等价于以下两个Stage的组合：
{ $group: { _id: &lt;expression&gt;, count: { $sum: 1 } } },
{ $sort: { count: -1 } }</pre>
</td>
</tr>
<tr>
<td>$addFields</td>
<td>
<p>为每个输入文档添加额外的字段：
<p><pre class="crayon-plain-tag">{ $addFields: { &lt;newField&gt;: &lt;expression&gt;, ... } } </pre>
</td>
</tr>
<tr>
<td>$replaceRoot</td>
<td>提升输入文档中的一个内嵌文档，将其作为根文档，代替原有文档 </td>
</tr>
<tr>
<td>$count</td>
<td>计算输入文档的总数：<pre class="crayon-plain-tag">{ $count: &lt;field_name&gt; }</pre>  </td>
</tr>
<tr>
<td>$graphLookup</td>
<td>
<p>在集合上执行递归的检索：</p>
<pre class="crayon-plain-tag">{
   $graphLookup: {
      // 被搜索的集合的名称
      from: &lt;collection&gt;,
      // 指定connectFromField的初始值
      startWith: &lt;expression&gt;,
      // 指定起始文档中用于匹配的字段名
      connectFromField: &lt;string&gt;,
      // 指定与起始文档进行匹配的，目标文档的字段名
      connectToField: &lt;string&gt;,
      // 保存被匹配文档链条的字段名
      as: &lt;string&gt;,
      // 递归匹配的最大深度
      maxDepth: &lt;number&gt;,
      // 添加到匹配文档链条中每个元素的“深度”字段的名字
      depthField: &lt;string&gt;,
      // 查询文档，为匹配指定额外的条件
      restrictSearchWithMatch: &lt;document&gt;
   }
}

// 示例：
{ "_id" : 1, "name" : "Dev" }
{ "_id" : 2, "name" : "Eliot", "reportsTo" : "Dev" }
{ "_id" : 3, "name" : "Ron", "reportsTo" : "Eliot" }
// 聚合管线：
db.employees.aggregate( [
   {
      $graphLookup: {
         from: "employees",
         startWith: "$reportsTo",
         connectFromField: "reportsTo",
         connectToField: "name",
         as: "reportingHierarchy"
      }
   }
] )
// 输出
{
   "_id" : 1,
   "name" : "Dev",
   "reportingHierarchy" : [ ]
}
{
   "_id" : 2,
   "name" : "Eliot",
   "reportsTo" : "Dev",
   "reportingHierarchy" : [
      { "_id" : 1, "name" : "Dev" }
   ]
}
{
   "_id" : 3,
   "name" : "Ron",
   "reportsTo" : "Eliot",
   "reportingHierarchy" : [
      { "_id" : 1, "name" : "Dev" },
      { "_id" : 2, "name" : "Eliot", "reportsTo" : "Dev" }
   ]
}</pre>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">表达式</span></div>
<p>使用聚合管线时，很多的值可以使用表达式。表达式包括：
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 15%; text-align: center;">表达式类型</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>字段路径</td>
<td>用于读取输入文档中的字段，包括内嵌文档字段，以前缀<pre class="crayon-plain-tag">$</pre>开始，例如$user、$user.name</td>
</tr>
<tr>
<td>系统变量</td>
<td>一些预置的特殊对象，以前缀<pre class="crayon-plain-tag">$$</pre>开始。例如<pre class="crayon-plain-tag">$$CURRENT</pre>通常表示当前正在被处理的输入文档，因此$$CURRENT.&lt;field&gt;通常等价于$&lt;field&gt;</td>
</tr>
<tr>
<td>字面值</td>
<td>
<p>可以使用<pre class="crayon-plain-tag">{ $literal: &lt;value&gt; }</pre>形式指定，例如：</p>
<pre class="crayon-plain-tag">{ $literal: { $add: [ 2, 3 ] } }	  // { "$add" : [ 2, 3 ] }
{ $literal:  { $literal: 1 } }	 	  // { "$literal" : 1 }</pre>
</td>
</tr>
<tr>
<td>操作符表达式</td>
<td>
<p>类似于接受参数的函数：
<pre class="crayon-plain-tag">// 接受多参数的操作符
{ &lt;operator&gt;: [ &lt;argument1&gt;, &lt;argument2&gt; ... ] }
// 接受单参数的操作符
{ &lt;operator&gt;: &lt;argument&gt; } </pre>
<p>MongoDB提供了很多聚合管线专用操作符，对应了各类操作符表达式，主要包括：</p>
<ol>
<li>布尔表达式：$and、$or、$not</li>
<li><a href="https://docs.mongodb.com/manual/reference/operator/aggregation-set/">集合表达式</a>：对数组进行集合操作，把数组当作集合处理——忽略重复元素</li>
<li><a href="https://docs.mongodb.com/manual/reference/operator/aggregation-comparison/">比较表达式</a>：进行比较操作，除了$cmp之外均返回boolean</li>
<li><a href="https://docs.mongodb.com/manual/reference/operator/aggregation-arithmetic/">算术表达式</a>：进行算术操作，某些操作符支持日期</li>
<li><a href="https://docs.mongodb.com/manual/reference/operator/aggregation-string/">字符串表达式</a>：进行字符串查找、子串提取、分割等操作</li>
<li>文本搜索表达式</li>
<li><a href="https://docs.mongodb.com/manual/reference/operator/aggregation-array/">数组表达式</a>：处理数组</li>
<li>变量表达式：<pre class="crayon-plain-tag">$let</pre>，定义在子表达式范围内可见的变量，并返回子表达式的值</li>
<li><a href="https://docs.mongodb.com/manual/reference/operator/aggregation-date/">日期表达式</a>：处理日期和时间</li>
</ol>
</td>
</tr>
<tr>
<td><a href="https://docs.mongodb.com/manual/reference/operator/aggregation-group/">累加器表达式</a></td>
<td>
<p>聚合管线专用操作符中有一类特殊的累加器操作符，只能用于$group阶段，从3.2开始，部分可用在$project阶段（stage）。主要包括：$sum、$avg、$first、$last、$max、$min、$push、$addToSet等</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">例子：邮编数据</span></div>
<pre class="crayon-plain-tag">// 每条数据的结构
{
    "_id": "10280",        // 邮编
    "city": "NEW YORK",    // 市
    "state": "NY",         // 州
    "pop": 5574,           // 人口
    "loc": [               // 经纬
        -74.016323,
        40.710537
    ]
}
// 人口大于1000万的州
db.zipcodes.aggregate( [
    // 输出文档的_id作为分组的依据
    // 该stage决定的输出文档的结构，使用$xxx可以引用输入文档的字段
    // totalPop字段通过$sum操作符（聚合表达式）计算出
    { $group: { _id: "$state", totalPop: { $sum: "$pop" } } },
    // 该stage针对上一stage的结果进行匹配
    { $match: { totalPop: { $gte: 10 * 1000 * 1000 } } }
] );

// 返回每州的平均城市人口
db.zipcodes.aggregate( [
    // 支持依多字段分组。先获得 州 - 市 - 人口总和
    { $group: { _id: { state: "$state", city: "$city" }, pop: { $sum: "$pop" } } },
    // 再次分组，州总人口除以城市数量。注意点号导航
    { $group: { _id: "$_id.state", avgCityPop: { $avg: "$pop" } } }
] );

// 返回每个州里面最大和最小的城市
db.zipcodes.aggregate( [
    {
        // 按照州、市进行分组，统计人口总数
        $group: {
            _id: { state: "$state", city: "$city" },
            pop: { $sum: "$pop" }
        }
    },
    // 根据人口升序排列
    { $sort: { pop: 1 } },
    // 第二次分组，$last、$first分别取最后、最前的组内文档中的值
    {
        $group: {
            _id: "$_id.state",
            biggestCity: { $last: "$_id.city" },
            biggestPop: { $last: "$pop" },
            smallestCity: { $first: "$_id.city" },
            smallestPop: { $first: "$pop" }
        }
    },
    // 投影stage，重命名_id字段为state字段
    // 把城市名称、人口合并为文档
    // 注意投影是针对输入文档每个条目执行的
    {
        $project: {
            _id: 0,  // 设置为0表示明确的在输出文档中禁用该字段
            state: "$_id",
            biggestCity: { name: "$biggestCity", pop: "$biggestPop" },
            smallestCity: { name: "$smallestCity", pop: "$smallestPop" }
        }
    }
] );</pre>
<div class="blog_h3"><span class="graybg">例子：用户参数</span></div>
<pre class="crayon-plain-tag">20// 每条数据的结构
{
    _id : "jane",
    joined : ISODate("2011-03-02"),
    likes : ["golf", "racquetball"]
}
{
    _id : "joe",
    joined : ISODate("2012-07-02"),
    likes : ["tennis", "golf", "swimming"]
}

// 规范化数据结构，_id转换为大写，命名为name，然后根据name升序排列
db.users.aggregate(
    [
        { $project: { name: { $toUpper: "$_id" }, _id: 0 } },
        { $sort: { name: 1 } }
    ]
);


// 根据加入的月份升序排列
db.users.aggregate(
    [
        {
            $project: {
                // 取得月份
                month_joined: { $month: "$joined" },
                name: "$_id",
                _id: 0 // 明确禁用_id字段，该字段是默认包含的
            }
        },
        { $sort: { month_joined: 1 } }
    ]
);
// 获得每月加入的总数，根据月份升序排列
db.users.aggregate(
    [
        { $project: { month_joined: { $month: "$joined" } } },
        { $group: { _id: { month_joined: "$month_joined" }, number: { $sum: 1 } } },
        { $sort: { "_id.month_joined": 1 } }
    ]
);
// 获得五个最被喜爱的体育运动
db.users.aggregate(
    [
        // 展开likes数组，意味着对于likes.lengt = 5的输入文档，将展开为5个输出文档
        { $unwind: "$likes" },
        // 根据喜爱的单项体育运动分组，统计总数（每个人只能喜爱一次，因此sum:1）
        { $group: { _id: "$likes", number: { $sum: 1 } } },
        // 逆序排列
        { $sort: { number: -1 } },
        // 取前五条
        { $limit: 5 }
    ]
);</pre>
<div class="blog_h2"><span class="graybg">MapReduce</span></div>
<p>Map-reduce是一种数据处理范式，用于浓缩海量的数据为有意义的聚合数据。</p>
<p>MongoDB支持map-reduce风格的聚合操作。总体上说，map-reduce操作由以下阶段组成：</p>
<ol>
<li>map阶段，处理多个文档，并针对每个输入文档产生一个或者多个键值对。此阶段由map函数中的emit语句完成</li>
<li>reduce阶段，处理map阶段的输出，把这些输出合并起来。对于具有相同键的所有值，应用reduce函数，聚合（累积）为一个值</li>
<li>可选的finalize阶段，对结果进行最终的修改</li>
</ol>
<p>类似于其它聚合操作，map-reduce也允许指定查询条件、排序方式、限制输入文档数量。</p>
<p>你需要自定义JavaScript函数来提供map、reduce、finalize逻辑。和<span style="background-color: #c0c0c0;">聚合管线比起来，JavaScript函数可以提供巨大的灵活性</span>，但是更加低效和复杂。</p>
<p>在Shell中，你可以调用<pre class="crayon-plain-tag">mapReduce</pre>命令，编程时则调用<pre class="crayon-plain-tag">db.collection.mapReduce()</pre></p>
<div class="blog_h3"><span class="graybg">mapReduce函数</span></div>
<pre class="crayon-plain-tag">db.collection.mapReduce(
    // map函数关联一个value到key，在map阶段之后产生若干key:[values]形式的键值对，作为reduce阶段的输入
    // map函数要求：
    // 1、使用this引用当前文档
    // 2、不得出于任何目的访问数据库
    // 3、必须是纯函数，不得有任何副作用
    // 4、单个emit调用可产生半数于BSON最大文档尺寸的键值对
    // 5、可以调用多次emit(key,value)以产生键值对
    &lt;map&gt;,
    // reduce整个[values]为单个对象，可以作为finalize阶段的输入
    // reduce函数要求：
    // 1、不得出于任何目的访问数据库
    // 2、不得影响外部系统
    // 3、对于只包含一个值的key，MongoDB不会调用reduce函数
    // 4、可以访问定义在scope中的全局变量
    // 5、由于针对同一key，reduce函数可能被调用多次，因此reduce函数的返回值的类型必须和map函数emit的value类型一致
    // 6、以下表达式必须成立：
    //    associative    reduce(key, [ C, reduce(key, [ A, B ]) ] ) == reduce( key, [ C, A, B ] )
    //    idempotent     reduce(key, [ reduce(key, valuesArray) ] ) == reduce( key, valuesArray )
    //    commutative    reduce( key, [ A, B ] ) == reduce( key, [ B, A ] )
    &lt;reduce&gt;,
    // 选项：
    {
        // 指定mapReduce结果的输出目的地，你可以指定
        // 1、一个集合的名字
        // 2、action:collection_name，输出到集合前应用指定的action
        // 3、inline，针对主节点进行mapReduce时可以输出到集合，针对从节点则只能inline
        out: {
            // action取值：
            // replace 如果collectionName存在，则替换其内容
            // merge  合并当前结果到collectionName，如果存在相同的key则覆盖之
            // reduce reduce当前结果到collectionName，对于相同的key（_id），使用此mapReduce的reduce函数进行reduce
            // collectoinName中文档的原型：{ "_id" : key, "value" : reducedValue }
            &lt;action&gt;: &lt;collectionName&gt;,
            // 在内存中完成mapReduce操作，并返回结果，指定此字段，则不能指定任何其它out字段
            inline : 1                    //
            [, db: &lt;dbName&gt;]              // 可以选择其它数据库
            [, sharded: &lt;boolean&gt; ]       // 设置为true则对输出集合启用分片，使用_id（即map的key）字段对输出集合进行分片
            [, nonAtomic: &lt;boolean&gt; ]     // 如果设置为true，则mapReduce的后处理阶段不会全局锁
        },
        // 用于过滤输入文档
        query: &lt;document&gt;,
        // 用于排序输入文档，可以优化性能。如果sort和emit的键一致，则可以减少reduce操作的次数。必须是集合的某个索引字段
        sort: &lt;document&gt;,
        // 限制进入map阶段的最大文档数量
        limit: &lt;number&gt;,
        // 早reduce完成之后，对最终结果进行修改，例如求平均
        finalize: &lt;function&gt;,
        // 定义map、reduce、finalize函数中可以访问的全局变量
        scope: &lt;document&gt;,
        jsMode: &lt;boolean&gt;,
        // 如果为true，则在result信息中包含耗时统计信息
        verbose: &lt;boolean&gt;
    }
)</pre>
<div class="blog_h3"><span class="graybg">分片集合</span></div>
<p>map-reduce支持将分片集合作为输入，并且其结果可以输出到分片集合上。</p>
<p>当作为输入时，MongoDB会自动把map-reduce任务派发到各分片上来并行执行，并等到所有分片上的任务全部完成。</p>
<p>当作为输出时，如果mapReduce的out字段具有sharded值，则MongoDB自动以_id为分片键，输出到分片集合中。</p>
<div class="blog_h3"><span class="graybg">并发性</span></div>
<p>Map-reduce操作由一系列的任务组成，这些任务的职责包括：从输入集合读取数据、执行map函数、执行reduce函数、在处理过程中输出到临时集合、写入到输出集合等。</p>
<p>在处理过程中，Map-reduce持有以下锁：</p>
<ol>
<li>在读阶段，持有读锁，每次锁100个文档</li>
<li>写入临时集合时，持有写锁，每次写操作持有一次</li>
<li>如果输出集合不存在，创建输出集合时持有写锁</li>
<li>如果输出集合存在，则输出操作（merge、replace、reduce等）持有写锁。<span style="background-color: #c0c0c0;">此写锁是全局的，会阻塞mongod实例上所有其它操作</span></li>
</ol>
<div class="blog_h3"><span class="graybg">示例</span></div>
<p>考虑一个订单集合，其文档原型如下：</p>
<pre class="crayon-plain-tag">{
    cust_id: "1000",   // 客户号
    ord_date: new Date( "2015-01-05" ),  // 订单日期
    status: 'A',
    price: 25,  // 订单金额
    // 订单详情
    items: [ { sku: "mmm", qty: 5, price: 3 }, { sku: "nnn", qty: 5, price: 2 } ] 
}</pre>
<p>下面的map-reduce操作，可以获得每个客户的订单总额：</p>
<pre class="crayon-plain-tag">db.collection( 'order' ).mapReduce(
    // 注意map函数必须是纯函数
    function () {
        // this为当前正在处理的文档
        // 调用emit可以生成输出文档
        emit( this.cust_id, this.price );
    },
    // 按照客户ID分组，所有价格构成数组。这样一个键值对送给reduce函数处理
    function ( keyCustId, valuesPrices ) {
        return Array.sum( valuesPrices );
    },
    // 选项，指定结果的输出集合
    { out: "order_prices" }
)</pre>
<div class="blog_h3"><span class="graybg">增量mapReduce</span></div>
<p>如果需要被处理的数据集持续增长，你可能需要进行增量mapReduce，而不是每次都针对完整数据集进行mapReduce。</p>
<p>要进行增量mapReduce，你需要：</p>
<ol>
<li>针对当前集合执行mapReduce，把结果存放到另外一个集合中</li>
<li>当更多的数据入库时，执行后续的mapReduce操作。使用query参数来过滤，仅仅匹配新的文档</li>
<li>使用finalize处理reduce之后的数据，例如求平均值</li>
</ol>
<p>统计用户会话时长信息的例子：</p>
<p>文档原型：</p>
<pre class="crayon-plain-tag">// ts为登录时间戳，是增量mapReduce的关键
{ userid: "a", ts: ISODate('2011-11-03 14:17:00'), length: 95 }</pre>
<p>增量mapReduce：</p>
<pre class="crayon-plain-tag">db.collection( 'sessions' ).mapReduce(
    // map，注意平均时间仅仅是占位符
    function () {
        var key = this.userid;
        var value = {
            userid: this.userid,
            total_time: this.length,
            count: 1,
            avg_time: 0
        };
        emit( key, value );
    },
    // reduce，总计在线时间、登录次数累加，平均登录时间再此无法计算
    function ( key, values ) {

        var reducedObject = {
            userid: key,
            total_time: 0,
            count: 0,
            avg_time: 0
        };

        values.forEach( function ( value ) {
                reducedObject.total_time += value.total_time;
                reducedObject.count += value.count;
            }
        );
        return reducedObject;
    },
    {
        // 增量：仅仅处理新增数据
        query: { ts: { $gt: ISODate( '2011-01-01 00:00:00' ) } },
        // reduce到统计集合，此集合必须最初的全量mapReduce初始化
        out: { reduce: "session_stat" },
        // 本次增量数据reduce到统计集合之后，才能计算平均时间
        finalize: function ( key, reducedValue ) {
            if ( reducedValue.count &gt; 0 )
                reducedValue.avg_time = reducedValue.total_time / reducedValue.count;
            return reducedValue;
        }
    }
) </pre>
<div class="blog_h2"><span class="graybg">单意图聚合操作</span></div>
<p><pre class="crayon-plain-tag">db.collection.count()</pre>、<pre class="crayon-plain-tag">db.collection.distinct()</pre>这些函数用于特殊目的的聚合操作，比较简单。</p>
<div class="blog_h1"><span class="graybg">全文检索</span></div>
<p>MongoDB支持对字符串内容进行检索查询。为了使用这种文本搜索功能，你需要使用文本索引（text index）以及<pre class="crayon-plain-tag">$text</pre>操作符。注意视图不支持文本搜索。</p>
<p>全文检索不是MongoDB的核心功能，如果有复杂的搜索需求，建议配合使用全文搜索引擎，例如<a href="http://lucene.apache.org/solr/">Solr</a>。</p>
<div class="blog_h2"><span class="graybg">行为</span></div>
<div class="blog_h3"><span class="graybg">停用词</span></div>
<p>MongoDB的全文搜索会忽略一些语言相关的单词，例如英语中的there、and、the等单词，这些词作为搜索关键字，没有意义。</p>
<div class="blog_h3"><span class="graybg">词根</span></div>
<p>如果某个文档包含单词blueberry，则你搜索blue不会有结果，但是搜索blueberries则匹配。即全文搜索支持复数形式的识别。</p>
<div class="blog_h2"><span class="graybg">文本索引</span></div>
<p>这类索引用于支持对字符串内容的检索。文本索引可以包含任何类型为字符串、字符串数组的字段。要支持文本搜索，你必须首先为集合创建文本索引：</p>
<pre class="crayon-plain-tag">db.stores.insert(
    [
        { _id: 1, name: "Java Hut", description: "Coffee and cakes" }
    ]
)
# 创建文本索引，该索引将针对name、description两个字段
db.stores.createIndex( { name: "text", description: "text" } )

# 删除文本索引，需要先查询到索引的名字
db.collection.getIndexes()
# 传入名字以删除
db.trades.dropIndex("name_text")</pre>
<p>注意：每个<span style="background-color: #c0c0c0;">集合只能拥有一个文本索引</span>，但是该索引可以覆盖多个字段。</p>
<div class="blog_h2"><span class="graybg">$text操作符</span></div>
<p>使用该操作符，可以对启用了文本索引的集合进行文本搜索：</p>
<pre class="crayon-plain-tag">// 返回包含 java、coffee shop两个词语的文档
db.stores.find( { $text: { $search: "java \"coffee shop\"" } } )
// 返回包含java、shop，但是不包含coffee的文档
db.stores.find( { $text: { $search: "java shop -coffee" } } )</pre>
<p>$search用于指定关键字，多个关键字使用空格分隔，如果某个关键字内部包含空格，必须以双引号包围整个关键字。MongoDB对所有关键字进行逻辑或操作。 </p>
<div class="blog_h2"><span class="graybg">相关性</span></div>
<p>文本搜索的结果默认是无序的。文本搜索会为每个匹配的文档计算一个相关性分数（relevance score），你可以访问该分数用来排序：</p>
<pre class="crayon-plain-tag">db.stores.find(
   { $text: { $search: "java coffee shop" } },
   // 把相关性分数投影为score字段
   { score: { $meta: "textScore" } }
).sort( { score: { $meta: "textScore" } } )</pre>
<div class="blog_h2"><span class="graybg">聚合管线</span></div>
<p>在聚合框架中使用文本搜索时，你需要在$match阶段使用$text操作符。类似的，要进行排序，你需要在$sort阶段使用$meta投影操作符。</p>
<p>在聚合管线中使用文本搜索，受到以下限制：</p>
<ol>
<li>包含$text操作符的$match必须是管线的第一个Stage</li>
<li>$text操作符仅能够在管线中出现一次</li>
<li>$text操作符不可出现在$and或者$or内部</li>
<li>默认搜索结构没有按照相关性排序，请使用$sort 阶段实现</li>
</ol>
<div class="blog_h2"><span class="graybg">中文支持</span></div>
<p>文本搜索支持多种语言，从3.2开始，<span style="background-color: #c0c0c0;">MongoDB Enterprise可以支持中文全文检索</span>。</p>
<p>为了支持中文和阿拉伯文等语言，MongoDB Enterprise集成了RLP（ Basis Technology Rosette Linguistics Platform ），来完成正规化、分词、分句、词干提取（stemming）、符号化等全文检索领域的职责。</p>
<div class="blog_h1"><span class="graybg">地理空间查询</span></div>
<div class="blog_h2"><span class="graybg">地理空间数据</span></div>
<p>在MongoDB中，你可以<span style="background-color: #c0c0c0;">GeoJSON对象</span>的形式或者<span style="background-color: #c0c0c0;">遗留的坐标对</span>形式，来存储地理空间数据。</p>
<div class="blog_h3"><span class="graybg">GeoJSON</span></div>
<p>要沿着类似于地球的球面，进行几何计算，你应当使用GeoJSON来存储位置数据。即使用这样的内嵌文档：</p>
<ol>
<li>包含一个名为type的字段，指定此GeoJSON对象的类型，例如Point、LineString、Polygon等</li>
<li>包含一个名为coordinates的字段，指定构成此GeoJSON对象的所有坐标值。如果指定经纬度坐标，则经度在前、纬度在后</li>
</ol>
<p>GeoJSON对象示例：</p>
<pre class="crayon-plain-tag">// 格式：
&lt;field&gt;: { type: &lt;GeoJSON type&gt; , coordinates: &lt;coordinates&gt; }
// 点：
location: {
    type: "Point",
    coordinates: [-73.856077, 40.848447]
}
// 线
scope: {
    type: "LineString", 
    coordinates: [ [ 40, 5 ], [ 41, 6 ] ]
}</pre>
<p>针对GeoJSON类型字段上的查询，计算行为是在球面上进行的，使用WGS84坐标参考系统。 </p>
<div class="blog_h3"><span class="graybg">坐标对</span></div>
<p>要在欧几里得平面上计算距离，在坐标对中存储位置信息同时使用2d索引。</p>
<p>通过 2dsphere索引并且把坐标对转换为GeoJSON对象，MongoDB支持遗留坐标对的球面计算。</p>
<p>坐标对的示例：</p>
<pre class="crayon-plain-tag">// 数组形式，推荐
&lt;field&gt;: [ &lt;x&gt;, &lt;y&gt; ]
&lt;field&gt;: [&lt;longitude&gt;, &lt;latitude&gt; ]

// 内嵌文档形式
&lt;field&gt;: { &lt;field1&gt;: &lt;x&gt;, &lt;field2&gt;: &lt;y&gt; }</pre>
<div class="blog_h2"><span class="graybg">地理空间索引</span></div>
<div class="blog_h3"><span class="graybg">2dsphere</span></div>
<p>这种索引用于支持在类似于地球的球面上执行几何计算。创建这类索引的方法为：</p>
<pre class="crayon-plain-tag">// location field必须为GeoJSON或者坐标对字段
db.collection.createIndex( { &lt;location field&gt; : "2dsphere" } )</pre>
<div class="blog_h3"><span class="graybg">2d</span></div>
<p>这种索引用于支持在二维平面上进行的几何运算。尽管这种索引可以支持 $nearSphere以实现球面几何运算，最好还是使用2dsphere索引。创建这类索引的方法为：</p>
<pre class="crayon-plain-tag">db.collection.createIndex( { &lt;location field&gt; : "2d" } )</pre>
<div class="blog_h3"><span class="graybg">关于分片集合</span></div>
<p>地理空间索引不能作为分片键使用，但是分片集合的普通字段上可以存在地理空间索引。</p>
<p>对于分片集合，$near、$nearSphere等查询操作符不被支持，你可以考虑使用$geoNear聚合管线。</p>
<div class="blog_h3"><span class="graybg">关于覆盖查询</span></div>
<p>地理空间索引不能够覆盖任何查询。</p>
<div class="blog_h2"><span class="graybg">地理空间查询</span></div>
<p>相关的查询操作符包括：$geoWithin、$geoIntersects、$near、$nearSphere。</p>
<p>相关的查询命令为：geoNear。</p>
<p>相关的聚合管线Stage为：$geoNear。 </p>
<div class="blog_h1"><span class="graybg">数据建模</span></div>
<p>关系型数据库具有严格的Schema —— 例如表结构定义，但是MongoDB则允许你使用非常灵活的Schema，集合中文档的结构不被限制。这种灵活性可以让文档很方便的对应到一个复杂的实体。</p>
<p>实践中，一个集合中的文档的结构都是相似的。</p>
<p>对实体进行建模的关键挑战是在应用程序需求、数据库引擎的性能特征、数据检索图式之间寻求平衡。 </p>
<div class="blog_h2"><span class="graybg">文档结构</span></div>
<p>为MongoDB设计数据模型的关键决策是，如何表示数据之间的关系。关系的表达有两种风格：引用、内嵌文档。</p>
<div class="blog_h3"><span class="graybg">引用</span></div>
<p>规范化数据模型：使用一个字段来引用目标对象的_id，来建立两者的关系。</p>
<p>以下情况下，考虑使用引用：</p>
<ol>
<li>当内嵌文档会导致数据冗余，但是却不能提供足够高效的读性能时（相对其数据冗余的代价）</li>
<li>表示复杂的M2M关系时</li>
<li>对大型的层次化结构进行建模时</li>
</ol>
<p>引用的缺点是，客户端可能需要发起更多的查询请求。</p>
<div class="blog_h3"><span class="graybg">内嵌文档</span></div>
<p>非规范化数据模型：把目标对象的全部/部分数据以内嵌文档的形式，直接存放在当前对象中。这样，访问关联对象的时候不需要发出额外的查询。</p>
<p>以下情况下，考虑使用内嵌文档：</p>
<ol>
<li>两个实体之间是合成关系</li>
<li>O2M关系中，M总是通过O来访问时</li>
</ol>
<p>需要注意，内嵌文档/数组在MMAPv1引擎下，可能导致文档增长而产生磁盘数据块移动、碎片化，影响性能。</p>
<p>从3.0开始MongoDB使用2^N尺寸的空间分配，以最小化数据碎片化的可能性。</p>
<div class="blog_h2"><span class="graybg">影响因素</span></div>
<p>多个方面的因素影响你如何建模。</p>
<div class="blog_h3"><span class="graybg">文档增长</span></div>
<p>某些update操作会导致即有文档尺寸增长，例如添加数组元素、增加字段。</p>
<p>使用MMAPv1引擎时，文档增长是影响建模的考虑因素。因为文档尺寸超过预分配的大小时，引擎会重新在磁盘上分配空间，则意味着数据移动和可能的碎片化。</p>
<p>如果需要进行<span style="background-color: #c0c0c0;">频繁的改变文档大小的update操作，考虑使用引用而非内嵌文档</span>。你也可以使用预分配（pre-allocation）策略来避免文档增长。</p>
<p>从3.0开始的2^N尺寸分配可以减少重新分配空间的几率，并有效的重用因为移动文档而释放的空间。</p>
<div class="blog_h3"><span class="graybg">原子性</span></div>
<p>在MongoDB中，针对单个文档的写操作是原子的。因此内嵌文档风格可以用来保证原子性需求。</p>
<div class="blog_h3"><span class="graybg">索引</span></div>
<p>如果以读操作为主，考虑增加索引。索引可以提升查询性能，MongoDB自动为_id创建唯一索引。注意索引的以下行为：</p>
<ol>
<li>每个索引至少要求8KB的空间</li>
<li>索引对写操作有一定的负面影响。对于写读比很高的集合，索引的代价相对较高</li>
<li>每一个活动的索引都消耗磁盘、内存空间。</li>
</ol>
<div class="blog_h3"><span class="graybg">大量集合</span></div>
<p>通常情况下，使用大量的集合对性能没有太大影响，但是可能改善性能。创建大量集合时，要注意：</p>
<ol>
<li>每个集合需要至少占用若干KB的空间</li>
<li>每个集合至少有一个索引，因此需要占用至少8KB空间</li>
<li>对于每个数据库，一个名字空间文件&lt;database&gt;.ns存储了数据库的所有元数据。这些元数据包含每个集合及其索引的信息</li>
<li>MMAPv1限制了名字空间的数量，可以<pre class="crayon-plain-tag">db.system.namespaces.count()</pre>查询</li>
</ol>
<div class="blog_h3"><span class="graybg">大量小文档</span></div>
<p>如果某个集合中存放了大量的小文档（例如GPS轨迹记录），你可能需要考虑使用内嵌文档或数组，以改善性能。小文档本身表示了独立实体时例外。</p>
<p>如果小文档仅仅包含几个字节，需要考虑存储空间的优化：</p>
<ol>
<li>明确指定_id字段。如果不指定，MongoDB默认生成12字节的ObjectId，可能太长了</li>
<li>使用简短的字段名，MongoDB会把<span style="background-color: #c0c0c0;">字段名附带在每一个文档</span>中</li>
</ol>
<div class="blog_h3"><span class="graybg">文档生命周期</span></div>
<p>集合可以具有TTL特性，让文档在一定时间后自动过期而被删除。</p>
<p>如果应用程序仅仅需要使用最近插入的文档，考虑定长集合。</p>
<div class="blog_h1"><span class="graybg">索引</span></div>
<p>索引用于提升查询性能，没有索引，MongoDB查询必须进行全集合扫描，性能低下。</p>
<p>索引是一种特殊的数据结构，它有序的保存了集合的一小部分信息。索引存储的是特定字段、字段集的值，以值的大小顺序存储 —— 因而能够支持高效的相等性查找、范围查找。 从算法上讲，MongoDB和RDBMS的索引类似。</p>
<p>索引可以在后台构建，这样可以避免影响运行中的应用程序。</p>
<p>要创建索引，可以调用：</p>
<pre class="crayon-plain-tag">db.collection.createIndex( &lt;key and index type specification&gt;, &lt;options&gt; )</pre>
<div class="blog_h2"><span class="graybg">默认_id索引</span></div>
<p>在集合最初被创建时，MongoDB在_id字段上创建唯一性索引。</p>
<p>使用分片集合时，如果_id不作为分片键，则<span style="background-color: #c0c0c0;">应用程序负责确保在集群范围内_id的唯一性</span>。</p>
<div class="blog_h2"><span class="graybg">单字段索引</span></div>
<p>顾名思义，就是针对单个字段创建的索引。</p>
<p>针对直接字段创建索引的示例：</p>
<pre class="crayon-plain-tag">// 升序索引
db.records.createIndex( { score: 1 } )
// 降序索引
db.records.createIndex( { score: -1 } )</pre>
<p>对于单字段索引，升序/降序并不重要，因为MongoDB可以两端访问索引。</p>
<p>类似的，还可以在内嵌文档字段上创建索引：</p>
<pre class="crayon-plain-tag">// 使用点号导航
db.records.createIndex( { "location.state": 1 } )</pre>
<div class="blog_h2"><span class="graybg">复合索引</span></div>
<p>MongoDB支持复合索引，即持有多个字段引用的单个索引。复合索引最多引用31个字段。要创建复合索引，调用：</p>
<pre class="crayon-plain-tag">// type取值1或者-1
db.collection.createIndex( { &lt;field1&gt;: &lt;type&gt;, &lt;field2&gt;: &lt;type2&gt;, ... } )</pre>
<p>注意：</p>
<ol>
<li>已经创建了哈希索引的字段不能包含在复合索引中</li>
<li>字段的顺序非常重要，第一个字段最优先排序，该字段值相同的记录，按照第二字段排序。注意索引按此排序存储在磁盘上</li>
<li>复合索引支持<span style="background-color: #c0c0c0;">针对索引前缀（Prefix）的查询</span>，例如字段一、字段二或者字段一。如果查询条件不包含第一个字段，则索引肯定无法使用</li>
</ol>
<div class="blog_h3"><span class="graybg">升序/降序</span></div>
<p>复合索引中，字段的索引类型（升降）决定了索引能否支持排序操作。例如：</p>
<pre class="crayon-plain-tag">db.events.createIndex( { "username" : 1, "date" : -1 } )
// 上述索引可以支持下面两种排序操作
db.events.find().sort( { username: 1, date: -1 } )  // 正序访问索引，读出的就是排序好的
db.events.find().sort( { username: -1, date: 1 } )  // 逆序访问索引，读出的就是排序好的
// 而不能支持下面的排序操作
db.events.find().sort( { username: 1, date: 1 } )   // 无论以什么方向读取索引，都需要额外的处理才能获得正确的顺序</pre>
<div class="blog_h2"><span class="graybg">多键索引</span></div>
<p>为了能够索引数组字段，MongoDB需要为每个数组元素创建索引键。这种多键（每元素）索引可以很好的支持针对数组字段的查询。</p>
<p>创建多键索引不需要特殊的API，MongoDB会自动发现目标字段是数组，进而自动创建多键索引。</p>
<div class="blog_h2"><span class="graybg">全文索引</span></div>
<p>从3.2开始，MongoDB引入了第3版的全文索引，关键特性包括：</p>
<ol>
<li>改善大小写不敏感性</li>
<li>支持音调不敏感</li>
<li>额外的分界符</li>
</ol>
<p>要创建全文索引，以text作为索引类型：</p>
<pre class="crayon-plain-tag">// 在comments字段上创建全文索引
db.reviews.createIndex( { comments: "text" } )
// 针对多个字段的全文索引
db.reviews.createIndex( { subject: "text", comments: "text" } )
// 针对所有包含字符串数据的字段进行索引
db.collection.createIndex( { "$**": "text" } )</pre>
<div class="blog_h2"><span class="graybg">哈希索引</span></div>
<p>基于Hash的分片需要在分片键上使用这种索引。使用哈希分片可以更加随机的分布数据。</p>
<p>要创建哈希索引，以hashed为索引类型：</p>
<pre class="crayon-plain-tag">db.collection.createIndex( { _id: "hashed" } )</pre>
<p>创建了哈希索引的字段，不能包含在复合索引中。 </p>
<div class="blog_h2"><span class="graybg">索引选项</span></div>
<p>创建索引时，第二个参数文档可以指定以下选项：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 17%; text-align: center;">选项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>background</td>
<td>
<p>默认情况下，创建索引会导致<span style="background-color: #c0c0c0;">当前数据库（而不仅仅是当前集合）的所有读写操作被阻</span>塞</p>
<p>设置该选项为true，则可以避免阻塞其它操作（但是发起索引创建的客户端总是被阻塞）。从2.4开始，支持在后台同时构建多个索引</p>
<p>没有完全构建好的索引，不会影响查询。在索引构建完毕之前，你不能执行影响索引所在集合的任何管理性操作，例如repairDatabase、drop目标集合</p>
<p>后台索引构建使用一种增量的、较为缓慢的方式（比起前台索引构建）</p>
<p>如果索引构建被中断（例如宕机），下次mongod启动时会以前台线程构建索引，这种情况下索引构建失败mongod会继续宕机</p>
</td>
</tr>
<tr>
<td>unique</td>
<td>默认false。是否创建唯一性索引，可以防止重复数据的插入 </td>
</tr>
<tr>
<td>name</td>
<td>为索引指定一个名称 </td>
</tr>
<tr>
<td>partialFilterExpression</td>
<td>
<p>用于创建部分索引 —— 仅仅对集合中部分文档进行索引。可以降低存储空间占用和性能影响</p>
<p>如果指定，则索引仅仅引用匹配过滤表达式的文档。过滤表达式包括$eq、$exist:true、$gt, $gte, $lt, $lte、$type以及位于顶级的$and 。示例：</p>
<pre class="crayon-plain-tag">// 针对烹调风格、餐馆名称创建索引，指对排名大于5的餐馆进行索引
db.restaurants.createIndex(
   { cuisine: 1, name: 1 },
   { partialFilterExpression: { rating: { $gt: 5 } } }
)</pre>
<p>所有索引类型均支持该选项</p>
<p>如果会<span style="background-color: #c0c0c0;">导致不完整的结果集，则MongoDB不会使用部分索引</span>。这意味着，要使用部分索引，你的查询条件必须包含过滤表达式（或者其子集）。例如：</p>
<pre class="crayon-plain-tag">// 该查询可以用到上面的部分索引
db.restaurants.find( { cuisine: "Italian", rating: { $gte: 8 } } )
// 下面的两个查询都不能使用上面的部分索引
db.restaurants.find( { cuisine: "Italian", rating: { $lt: 8 } } )
db.restaurants.find( { cuisine: "Italian" } )</pre>
<p>比起稀疏索引，部分索引更加灵活，可以满足特殊需求</p>
</td>
</tr>
<tr>
<td>sparse</td>
<td>
<p>是否创建稀疏索引，如果是则占用较少的空间，但是行为会发生变化 </p>
<p>稀疏索引仅仅针对包含了索引字段的那些文档进行索引，即使字段的值为null文档也会被索引</p>
<p>如果会导致不完整的结果集，则MongoDB不会使用稀疏索引，除非你使用hint明确指定使用稀疏索引</p>
<p>2dsphere v2、geoHaystack、text这几种索引总是稀疏的</p>
</td>
</tr>
<tr>
<td>expireAfterSeconds</td>
<td>
<p>TTL索引是一种特殊的单字段索引，控制MongoDB保存文档的时间。适用于事件记录、日志类数据，例如：</p>
<pre class="crayon-plain-tag">db.eventlog.createIndex( { "lastModifiedDate": 1 }, { expireAfterSeconds: 3600 } )</pre>
<p>后台线程负责读取索引中的值，并删除过期的文档。对于复制集成员，后台线程仅仅在当前是主节点的情况下才执行删除操作。此操作会通过Replication机制传播到从节点 </p>
<p>注意：</p>
<ol>
<li>不能针对复合索引指定该选项</li>
<li>不能针对定长集合使用该选项</li>
</ol>
</td>
</tr>
<tr>
<td>storageEngine</td>
<td>指定使用的存储引擎：<pre class="crayon-plain-tag">{ &lt;storage-engine-name&gt;: &lt;options&gt; }</pre></td>
</tr>
<tr>
<td colspan="2"><strong><em>排序规则相关选项</em></strong></td>
</tr>
<tr>
<td>collation</td>
<td>
<p>设置索引的排序规则，示例：</p>
<pre class="crayon-plain-tag">collation: {
   locale: &lt;string&gt;,
   caseLevel: &lt;boolean&gt;,
   caseFirst: &lt;string&gt;,
   // 用于指定索引是否大小写敏感
   strength: &lt;int&gt;,     
   numericOrdering: &lt;boolean&gt;,
   alternate: &lt;string&gt;,
   maxVariable: &lt;string&gt;,
   backwards: &lt;boolean&gt;
} </pre>
</td>
</tr>
<tr>
<td colspan="2"><strong><em>全文索引相关选项</em></strong></td>
</tr>
<tr>
<td>weights</td>
<td>以{ &lt;field&gt;: &lt;weight&gt; }的形式设置字段的权重，权重值范围1-99999</td>
</tr>
<tr>
<td>default_language</td>
<td>使用的默认语言</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">为复制集构建索引</span></div>
<p>从2.6开始，复制集的从节点支持在后台构建索引，而之前的版本从节点必须在前台构建。
<p>当主节点在后台构建好索引后，从节点自动在后台开始构建索引。对于分片集群，mongos会发送createIndex命令到每个分片上的复制集的主节点。</p>
<p>推荐使用本节的步骤来为复制集构建索引，避免影响数据库性能。</p>
<p>注意：</p>
<ol>
<li>确保oplog足够大，这样当索引创建操作完毕后，从节点不会太过落后而无法赶上主节点的节奏</li>
<li>该推荐步骤每次把一个成员移出复制集，因此不会影响所有从节点</li>
</ol>
<div class="blog_h3"><span class="graybg">操作步骤</span></div>
<p>要为从节点构建索引，针对所有从节点执行：</p>
<ol>
<li>停止一个从节点，并以独立模式启动（不带 --replSet选项）：<br />
<pre class="crayon-plain-tag">// 不使用默认的27017端口，防止构建索引期间，复制集的成员、客户端连接到该节点
mongod --port 47017</pre>
</li>
<li>使用createIndex命令创建索引</li>
<li>重新启动mongod，并添加到复制集中：<br />
<pre class="crayon-plain-tag">mongod --port 27017 --replSet rs0</pre></p>
<p>该节点将和主节点同步 </p>
</li>
</ol>
<p>要为主节点构建索引，你可以：</p>
<ol>
<li>在主节点上触发后台索引构建</li>
<li>或者，调用<pre class="crayon-plain-tag">rs.stepDown()</pre>让主节点优雅的变成从节点，集群会重新选择主节点。然后，对其执行从节点构建索引的推荐步骤</li>
</ol>
<div class="blog_h2"><span class="graybg">索引交叉</span></div>
<p>从2.6开始，MongoDB能够使用多个索引的交叉来满足查询。典型的索引交叉牵涉到两个索引，但是MongoDB支持多个索引、内嵌索引的交叉。</p>
<p>假设集合orders在qty和item上有索引，则查询：</p>
<pre class="crayon-plain-tag">db.orders.find( { item: "abc123", qty: { $gt: 15 } } )</pre>
<p>会用到索引交叉 —— 即使用item、qty两个索引的扫描结果进行逻辑与操作。查看执行计划，你可以看到AND_SORTED或者AND_HASH这样的Stage。</p>
<div class="blog_h3"><span class="graybg">关于索引前缀</span></div>
<p>复合索引的前缀可以参与到索引交叉中。</p>
<div class="blog_h3"><span class="graybg">关于复合索引</span></div>
<p>由于字段顺序、升序/降序都对复合索引有影响，复合索引可能无法满足某些查询：</p>
<ol>
<li>没有指定合理前缀作为查询条件</li>
<li>不匹配的升/降序排序规则</li>
</ol>
<p>这些情况下，索引交叉能够代替复合索引提升查询性能。</p>
<div class="blog_h2"><span class="graybg">索引策略</span></div>
<p>创建哪些索引最合适，取决于很多因素，例如：</p>
<ol>
<li>期望的查询的形状</li>
<li>读写比</li>
<li>系统的可用内存</li>
</ol>
<div class="blog_h3"><span class="graybg">载入内存</span></div>
<p>如果能够让整个索引位于内存之中，则可以避免磁盘扫描，获得很快的处理速度。执行：<pre class="crayon-plain-tag">db.collection.totalIndexSize()</pre> 可以获得某个集合的全部索引的尺寸。你的内存必须足够大，才能保证索引完整载入内存。</p>
<p>注意：索引并非总是需要完全载入内存。假设索引字段的值随着insert不断增长，而且查询总是针对最新添加的文档，则MongoDB仅仅需要在内存中载入部分索引内容。</p>
<div class="blog_h2"><span class="graybg">管理索引</span></div>
<p>查看集合上现存的索引：</p>
<pre class="crayon-plain-tag">db.collection.getIndexes()</pre>
<p>移除单个索引：</p>
<pre class="crayon-plain-tag">db.collection.dropIndex()</pre>
<p>移除除了_id之外的全部索引：</p>
<pre class="crayon-plain-tag">db.collection.dropIndexes()</pre>
<p>重新构建集合上的全部索引：</p>
<pre class="crayon-plain-tag">// 对于复制集，该操作不会传播到从节点
// 如果索引在创建时，指定了background:true，则索引会在后台重新创建
// 但是，_id的索引总是在前台创建，导致数据库写锁定
db.collection.reIndex()</pre>
<div class="blog_h2"><span class="graybg">度量索引</span></div>
<div class="blog_h3"><span class="graybg">$indexStats</span></div>
<p>你可以使用这一聚合管线Stage来获得索引的使用情况统计信息。</p>
<div class="blog_h3"><span class="graybg">explain()</span></div>
<p>在 executionStats模式下获得执行计划，可以获得查询的统计信息，包括使用的索引、扫描的文档数量、消耗的时间。</p>
<div class="blog_h2"><span class="graybg">hint()</span></div>
<p>要强制查询使用某个索引，可以调用此方法：</p>
<pre class="crayon-plain-tag">db.people.find(
   { name: "John Doe", zipcode: { $gt: "63000" } }
)
// 强制使用zipcode索引
.hint( { zipcode: 1 } )
// 阻止使用任何索引
.hint( { $natural: 1 } )</pre>
<div class="blog_h1"><span class="graybg">存储</span></div>
<div class="blog_h2"><span class="graybg">存储引擎</span></div>
<p>存储引擎是MongoDB的一个核心组件，它决定了数据如何被存储（内存和磁盘）。MongoDB支持多存储引擎，以便你根据不同的工作负载进行选择。</p>
<div class="blog_h3"><span class="graybg">WiredTiger</span></div>
<p>从3.2开始，这是默认的存储引擎。适用于大部分工作负载，推荐新项目使用该引擎。 </p>
<p>WiredTiger提供文档级的并发模型、检查点、压缩以及其它特性。</p>
<div class="blog_h3"><span class="graybg">MMAPv1</span></div>
<p>最初的存储引擎，3.2之前的默认存储引擎。适用于大量读写的工作负载，以及in-place更新。</p>
<div class="blog_h3"><span class="graybg">内存引擎</span></div>
<p>仅在MongoDB Enterprise中可用，在内存中保存数据，速度快。 </p>
<div class="blog_h2"><span class="graybg">WiredTiger</span></div>
<div class="blog_h3"><span class="graybg">文档级并发控制</span></div>
<p>WiredTiger使用文档级并发来控制写操作。因此，多个客户端可以在<span style="background-color: #c0c0c0;">同时修改某个集合中的不同文档</span>。 </p>
<p>对于大部分的读写操作，WiredTiger使用乐观并发控制。它仅仅在全局、数据库、集合级别使用意向锁。当引擎检测到两个操作之间存在冲突时，会透明的重试其中会引起写冲突的操作。 </p>
<p>某些全局操作，通常是牵涉多个数据库的短暂操作，仍然使用全局（实例级别）的锁。某些其它操作，例如drop集合，仍然需要数据库级别的独占锁。</p>
<div class="blog_h3"><span class="graybg">快照和检查点</span></div>
<p>WiredTiger支持多版本并发控制（ MultiVersion Concurrency Control ，MVCC）。在操作开始时，引擎提供数据集在那个时间点的精确快照 —— 位于内存中的一致性视图。</p>
<p>当写入到磁盘时，引擎以一种一致性的方式，把快照中的数据写入到所有相关的数据文件中。快照中的数据集，在数据文件中可以作为检查点（checkpoint），数据库可以恢复到检查点之前的状态。</p>
<p>默认的，WiredTiger每60秒、或者每产生2GB日志文件后，创建检查点。</p>
<p>在写入新检查点的过程中，以前的检查点仍然可用。因此，如果正在写入检查点时MongoDB崩溃，重启后它能够恢复到上一个检查点。</p>
<p>当WiredTiger原子的把对检查点的引用写入到元数据表（metadata table）之后，新的检查点变得可访问、永久化。一旦新检查点变得可访问，旧检查点的页被释放。</p>
<p>使用WiredTiger时，即使没有启用日志（journaling）。MongoDB仍然能够从上一个检查点恢复。但是要想恢复上一检查点后面的变动，需要日志的配合。</p>
<div class="blog_h3"><span class="graybg">日志</span></div>
<p>WiredTiger使用预写式（Write-ahead）日志，联合检查点，来确保数据的安全性。日志（Journal）对上一个检查点以来的所有数据修改都进行了持久化。如果MongoDB宕机，可以重放（Replay）从上一检查点依赖的所有数据修改。日志文件存放在<pre class="crayon-plain-tag">$dbPath/journal/WiredTigerLog.&lt;sequence&gt;</pre>，其中sequence时从0000000001开始的序号。</p>
<p>WiredTiger为客户端发起的每一个操作，创建一条日志记录（journal record），该记录操作触发Mongod对数据库进行修改（包括集合、索引）的全部信息。</p>
<p>WiredTiger使用内存来作为日志记录的缓冲，多个线程协作以分配、拷贝自己的那部分缓冲。在以下情况下，日志记录缓冲被刷入磁盘：</p>
<ol>
<li>从3.2开始，每50ms</li>
<li>一旦检查点被创建</li>
<li>使用写关注j:true执行了写操作后</li>
<li>每当新的日志文件被创建后。MongoDB限制日志文件大小为100MB，因此每超过此大小，就同步日志缓冲到磁盘</li>
</ol>
<p>WiredTiger日志利用snappy库进行压缩，你可以通过<pre class="crayon-plain-tag">storage.wiredTiger.engineConfig.journalCompressor</pre>来指定其它压缩算法或者禁用压缩。日志条目最小128字节，如果条目内容小于128字节，则不会进行压缩。</p>
<p>设置<pre class="crayon-plain-tag">storage.journal.enabled</pre>为false可以禁用日志，以减少日志维护的成本。在独立MongoDB服务器上，禁用日志可能导致上一个检查点以来的数据丢失。在复制集上，数据丢失的可能性较小。</p>
<div class="blog_h3"><span class="graybg">压缩</span></div>
<p>使用WiredTiger引擎时，MongoDB支持所有集合、索引的压缩。压缩通过CPU时间来换取存储空间的节省。</p>
<p>默认的，WiredTiger利用snappy库对集合、索引前缀进行压缩。对于集合来说，还可选zlib进行压缩。你可以设置：</p>
<ol>
<li><pre class="crayon-plain-tag">storage.wiredTiger.collectionConfig.blockCompressor</pre>来改变集合的压缩算法</li>
<li><pre class="crayon-plain-tag">storage.wiredTiger.indexConfig.prefixCompression</pre>来禁用索引前缀压缩</li>
</ol>
<p>压缩可以在集合、索引级别设置，你可以在创建集合、索引的时候指定对应的选项。</p>
<p>在大部分的工作负载下，默认压缩算法在时间和空间之间保持了一个平衡。</p>
<div class="blog_h3"><span class="graybg">内存使用</span></div>
<p>使用WiredTiger时，MongoDB同时利用WiredTiger内部缓存、文件系统缓存。</p>
<p>从3.4开始，WiredTiger 内部缓存的内存用量，默认取内存总量/2 -1GB 和256MB两个值中较大的那个。参数<pre class="crayon-plain-tag">storage.wiredTiger.engineConfig.cacheSizeGB</pre>用于自定义内存用量。</p>
<p>通过文件系统缓存，MongoDB可以利用所有空闲内存，文件系统缓存中的数据是被压缩的。</p>
<div class="blog_h2"><span class="graybg">MMAPv1</span></div>
<p>这是MongoDB最初的，基于内存映射文件的引擎。当面临<span style="background-color: #c0c0c0;">大量插入、读取、原地（in-place，不需要移动文档）更新</span>时，性能比WiredTiger更好。从3.2开始，该引擎不再是MongoDB的默认存储引擎。</p>
<p>当写操作发生时，MMAPv1更新内存视图。如果启用了日志内存中的改变首先被写到日志中而不是直接写到数据文件中。</p>
<div class="blog_h3"><span class="graybg">日志</span></div>
<p>为了确保所有数据集能够正确的持久化。MMAPv1使用磁盘日志，写磁盘日志比数据文件更加频繁，因为其效率较高。</p>
<p>使用默认配置的情况下，MMAPv1每60秒写一次磁盘（可以通过<pre class="crayon-plain-tag">storage.syncPeriodSecs</pre>修改），每100ms左右写一次日志文件（可以通过<pre class="crayon-plain-tag">storage.journal.commitIntervalMs</pre> 修改）。</p>
<div class="blog_h3"><span class="graybg">文档存储特性</span></div>
<p>MMAPv1的所有文档都是连续的存储在磁盘上的。如果文档update后变得过大，则必须重新分配磁盘空间。这意味着<span style="background-color: #c0c0c0;">文档本身的移动、全部索引的更新</span>，会影响性能并引起<span style="background-color: #c0c0c0;">存储碎片化</span>。</p>
<p>从3.0开始，MongoDB使用2^N尺寸的空间分配策略，多余的空间作为补白（padding），可以降低文档移动的几率。原来的精确适合（exact fit）空间分配策略，使用不包含update/delete操作的场景。</p>
<div class="blog_h3"><span class="graybg">内存用量</span></div>
<p>MMAPv1自动使用机器的全部空闲内存作为它的缓存，但是，一旦其它进程需要使用内存，MongoDB占据的内存会立即释放。</p>
<p>典型情况下，操作系统的虚拟内存系统管理MongoDB的内存使用，如果内存不够，MongoDB缓存会被交换到磁盘中。配备足够的内存，可以很大程度的提高性能。</p>
<div class="blog_h2"><span class="graybg">GridFS</span></div>
<p>GridFS是一套规范，用于存放那些大于BSON文档尺寸限制（16MB）的文件。</p>
<p>GridFS不是把文件存放在文档中，而是将其切分成多个部分（chunks），然后把这些部分分别存放在文档中。默认情况下，GridFS使用255KB的chunk，因此除了最后一个之外的chunk具有一致的大小。</p>
<p>GridFS使用两个集合来存放文件：</p>
<ol>
<li><pre class="crayon-plain-tag">fs.chunks</pre>，用于存放文件的chunks，文档原型如下：<br />
<pre class="crayon-plain-tag">{
    "_id" : &lt;ObjectId&gt;,         // 块的唯一标识
    "files_id" : &lt;ObjectId&gt;,    // 所属的文件的唯一标识  
    "n" : &lt;num&gt;,                // 从0开始的序号
    "data" : &lt;binary&gt;           // BinData类型的数据块
} </pre>
</li>
<li><pre class="crayon-plain-tag">fs.files</pre>，用于存放文件的元数据，文档原型如下：<br />
<pre class="crayon-plain-tag">{
    "_id" : &lt;ObjectId&gt;,            // 文件的唯一标识
    "length" : &lt;num&gt;,              // 文件的长度
    "chunkSize" : &lt;num&gt;,           // 块的数量
    "uploadDate" : &lt;timestamp&gt;,    // 存放到GridFS中的时间
    "md5" : &lt;hash&gt;,                // 散列值
    "filename" : &lt;string&gt;,         // 人类可读的可选的文件名
    "contentType" : &lt;string&gt;,      // 可选的MIME类型
    "aliases" : &lt;string array&gt;,    // 别名数组
    "metadata" : &lt;dataObject&gt;,     // 任意额外的自定义元数据
} </pre>
</li>
</ol>
<p>当你查询GridFS以取回文件时，驱动程序负责把chunks装配成文件。GridFS支持针对文件进行范围查询，或者skip一定的长度。</p>
<div class="blog_h3"><span class="graybg">适用场景</span></div>
<p>如果你需要存放大于16MB的文件时，应当使用GridFS。否则，考虑使用BinData类型存储在集合中。</p>
<p>某些情况下，在MongoDB中存储大文件比在文件系统中直接存放更加高效：</p>
<ol>
<li>如果文件系统限制目录中最大文件数，MongoDB没有此限制</li>
<li>如果你想访问大文件的一小部分，而不希望把整个文件加载到内存</li>
<li>如果你想获得复制集带来的文件安全性</li>
</ol>
<div class="blog_h3"><span class="graybg">GridFS索引</span></div>
<p>chunks、files两个集合自带一部分索引（由驱动创建），以提升性能。你也可以创建自己的索引，以满足需要。</p>
<p>chunks上具有唯一性的复合索引：files_id + n，这让chunk的检索非常高效，示例查询：</p>
<pre class="crayon-plain-tag">db.fs.chunks.find( { files_id: myFileID } ).sort( { n: 1 } )</pre>
<p>如果驱动没有创建此索引（不满足规范），可以手工创建：</p>
<pre class="crayon-plain-tag">db.fs.chunks.createIndex( { files_id: 1, n: 1 }, { unique: true } );</pre>
<p>files上具有filename、uploadDate两个索引，你可以很方便的根据名称、日志进行文件检索。</p>
<p>如果驱动没有创建此索引（不满足规范），可以手工创建：</p>
<pre class="crayon-plain-tag">db.fs.files.createIndex( { filename: 1, uploadDate: 1 } );</pre>
<div class="blog_h3"><span class="graybg">GridFS分片</span></div>
<p>如果需要分片chunks，考虑以<pre class="crayon-plain-tag">{ files_id : 1, n : 1 }</pre>或者<pre class="crayon-plain-tag">{ files_id : 1 }</pre>作为分片键。 files_id是单调递增字段。对chunks进行分片时，不能使用哈希分片。</p>
<p>files集合仅仅包含元数据，通常不需要分片。 </p>
<div class="blog_h1"><span class="graybg">复制Replication</span></div>
<p>复制集（replica set ）是指一组mongod进程，它们维护相同的数据集。复制集提供了数据冗余、高可用性，对于生产环境来说复制集基本是标配。</p>
<p>在某些情况下，复制集可以提供额外的读容量，因为客户端可以把请求分发给复制集中的任意成员。对于跨地域部署的应用程序来说，分别在不同数据中心的复制集节点可以大大降低网络延迟。</p>
<div class="blog_h2"><span class="graybg">复制集成员</span></div>
<p>每个复制集可以具有N个数据存储节点，和一个可选的仲裁（arbiter）节点。存储节点中有且仅有一个是主（primary）节点，其它均为从（secondary）节点。</p>
<div class="blog_h3"><span class="graybg">主节点</span></div>
<p><span style="background-color: #c0c0c0;">主节点接收所有写操作</span>请求，此节点负责确认{ w: "majority" }写关注。主节点把自己对<span style="background-color: #c0c0c0;">数据集的全部变更存放在日志 —— oplog中</span>。这些oplog随后被<span style="background-color: #c0c0c0;">异步的传播</span>到所有从节点，确保所有节点的数据集一致：</p>
<p><img class="aligncenter  wp-image-15115" src="https://blog.gmem.cc/wp-content/uploads/2015/05/replica-set-read-write-operations-primary.png" alt="replica-set-read-write-operations-primary" width="438" height="358" /></p>
<p>当主节点不可用时（和其它成员超过10s不进行通信），一个从节点会被选举为新的主节点。你可以额外配置一个mongod实例作为仲裁节点，这类节点不维护数据集，它的职责仅仅是响应心跳、应答其它节点的选举（投票）请求。当存储节点数量为偶数时，添加一个仲裁节点，可以满足投票的大多数（majority）原则。仲裁节点不需要专用的硬件，可以和某个存储节点部署在一起。</p>
<p>尽管所有成员都可以接受读请求，但是<span style="background-color: #c0c0c0;">默认的，应用程序会把读请求重定向给主节点</span>。</p>
<div class="blog_h3"><span class="graybg">priority0从节点</span></div>
<p>这类从节点<span style="background-color: #c0c0c0;">没有称为主节点的资格</span>，没有资格触发选举（从节点总是自荐为主节点以触发选举），但是可以进行投票。在多数据中心部署的情况下，要避免某个数据中心产生主节点，则将其中的节点均配置为priority0从节点。</p>
<p>默认的，没有任何节点是priority0从节点。你需要手工设置节点的priority为0。</p>
<p>注意当前主节点不支持设置priority为0，因此，如果你想把主节点变为priority，先需要将其变为从节点。<pre class="crayon-plain-tag">rs.reconfig()</pre>可以导致当前主节点立即优雅的关闭（ step down）从而导致重新选举主节点。在step down期间mongod会关闭所有客户端，通常会花费10-20秒，最好在例行维护期间进行这种操作。</p>
<p>调用<pre class="crayon-plain-tag">cfg = rs.conf()</pre>获得复制集的配置文档，该文档有一个<pre class="crayon-plain-tag">members</pre>字段，它是一个数组，包含复制集每个成员的配置信息。要设置第N个节点为priority0，执行<pre class="crayon-plain-tag">members[n].priority = 0</pre>。直到你重新配置复制集位置，priority设置不会生效。执行<pre class="crayon-plain-tag">rs.reconfig(cfg)</pre>可以重新配置复制集。</p>
<div class="blog_h3"><span class="graybg">隐藏从节点</span></div>
<p>之类从节点维持主节点数据集的拷贝，但是对客户端不可见。隐藏从节点必须是priority0节点。</p>
<p>由于对客户端不可见，隐藏从节点的流量仅仅包括来自主节点的数据复制流。你可以使用<span style="background-color: #c0c0c0;">隐藏从节点执行专门任务，你如报表分析、备份</span>。在分片集群中，mongos不会和隐藏从节点交互。</p>
<p>当使用隐藏从节点执行备份时，需要注意：</p>
<ol>
<li>如果使用MMAPv1引擎，使用<pre class="crayon-plain-tag">db.fsyncLock()</pre>和<pre class="crayon-plain-tag">db.fsyncUnlock()</pre>操作可以在备份期间flush所有写操作并且锁定mongod，这样可以不必为了备份而停止隐藏节点</li>
<li>从3.2开始db.fsyncLock()可以保证数据文件不改变，不管使用MMAPv1还是WiredTiger 引擎，因此可以保证备份期间的数据一致性。在之前的版本，不保证WiredTiger下的数据一致性</li>
</ol>
<div class="blog_h3"><span class="graybg">延迟从节点</span></div>
<p>延迟从节点也维持主节点数据集的拷贝，但是其状态对应了主节点一个早先的状态，例如一小时前。</p>
<p>由于此特性，延迟从节点可以用做滚动备份（rolling backup），当出现人为操作错误时，可以恢复到先前的数据状态。</p>
<p>延迟节点必须是priority0节点，而且应当是隐藏节点。配置文档示例： </p>
<pre class="crayon-plain-tag">{
   "_id" : &lt;num&gt;,
   "host" : &lt;hostname:port&gt;,
   "priority" : 0,
   "slaveDelay" : &lt;seconds&gt;,
   "hidden" : true
}</pre>
<div class="blog_h3"><span class="graybg">仲裁节点</span></div>
<p>仲裁者节点不维护数据拷贝，不能变为主节点，它仅仅在选举主节点时起作用。 仲裁者总是具有1张选票，因此它可用于确保总是有奇数张选票，以满足大多数原则。</p>
<p>注意：不要在部署了复制集主节点、从节点的机器上部署仲裁节点。</p>
<p>一个节点被作为仲裁者加入到复制集之前，行为与普通mongod无异，它会创建一系列的数据文件、全尺寸的日志文件。为了最小化默认文件的创建，可以配置仲裁节点的配置文件：</p>
<ol>
<li>设置<pre class="crayon-plain-tag">storage.journal.enabled</pre>为false</li>
<li>对于MMAPv1 引擎，设置<pre class="crayon-plain-tag">storage.mmapv1.smallFiles</pre>为true</li>
</ol>
<p>为复制集添加仲裁节点的参考步骤如下：</p>
<ol>
<li>指定复制集名称、数据库路径，启动mongod：<br />
<pre class="crayon-plain-tag">mongod --port 30000 --dbpath /data/arb --replSet rs</pre>
</li>
<li> 在复制集主节点中执行先的命令，把上一步的mongod添加为仲裁者：<br />
<pre class="crayon-plain-tag">rs.addArb("HOST:30000") </pre>
</li>
</ol>
<div class="blog_h2"><span class="graybg">Oplog</span></div>
<p>操作日志（Oplog）是一个特殊的定长集合，保存了所有修改了数据集的操作的滚动记录。MongoDB会在主节点上应用写操作，并将这些操作记录到主节点的Oplog。 复制集中的从节点会在之后异步的读取此定长集合，并将其中的操作应用到自己本地的数据库。</p>
<p>所有复制集成员都在<pre class="crayon-plain-tag">local.oplog.rs</pre>包含一个Oplog副本，便于本地数据库的当前状态。为了进行复制，复制集成员之间相互发送心跳，任意成员可以从其它任意成员那里读取到Oplog条目。</p>
<p>Oplog中的条目是幂等的，也就是说，这些条目应用一次还是多次，其产生的效果是一致的。</p>
<div class="blog_h3"><span class="graybg">Oplog的尺寸</span></div>
<p>当以第一次复制集成员身份启动mongod时，会自动创建默认尺寸的Oplog。默认尺寸对于In-Memory引擎来说是5%内存大小，对于其它引擎默认5%磁盘空闲空间大小。</p>
<p>在第一次启动前，你可以指定<pre class="crayon-plain-tag">oplogSizeMB</pre>参数，来设置Oplog的大小。之后如果需要改变此大小，需要特殊的<a href="https://docs.mongodb.com/manual/tutorial/change-oplog-size/">操作步骤</a>。</p>
<p>某些可能需要大尺寸Oplog的场景：</p>
<ol>
<li>同时更新很多文档：为了确保幂等性，Oplog必须把批量更新操作拆分成很多单条操作</li>
<li>删除的文档和更新的文档数量相近：这种情况下，数据库文件可能增长不大，但是Oplog会较大</li>
<li>还量的In-place更新</li>
</ol>
<div class="blog_h3"><span class="graybg">Oplog状态</span></div>
<p>调用<pre class="crayon-plain-tag">rs.printReplicationInfo()</pre>可以查看Oplog的状态，包括尺寸和操作的时间范围。</p>
<p>在某些异常情况下，从节点的Oplog更新可能过于延迟，达不到性能要求。在从节点调用<pre class="crayon-plain-tag">db.getReplicationInfo()</pre>可以查看复制状态、延迟。</p>
<div class="blog_h2"><span class="graybg">数据的同步</span></div>
<p>为了维护最新的共享数据集，从节点需要从其它节点同步（sync）或者拷贝数据。MongoDB使用两种形式的数据同步：</p>
<ol>
<li><span style="background-color: #c0c0c0;">初始同步</span>（initial sync）：为新的从节点提供完整的数据集</li>
<li><span style="background-color: #c0c0c0;">复制</span>（replication）：在初始数据集之上同步增量数据</li>
</ol>
<div class="blog_h3"><span class="graybg">初始同步</span></div>
<p>MongoDB执行初始同步的流程如下：</p>
<ol>
<li>克隆除了local之外的所有数据库，注意：
<ol>
<li>从3.4开始，所有的索引也被构建，之前的版本仅仅克隆_id索引</li>
<li>从3.4开始，在克隆过程中新产生的Oplog也被拉取过来。你需要确保新的从节点的local数据库有足够的磁盘空间</li>
</ol>
</li>
<li>新从节点应用所有克隆来的数据，并应用Oplog</li>
</ol>
<p>初始同步完毕后，新从节点的复制集成员状态从STARTUP2变为SECONDARY</p>
<p>为了防止偶发的网络错误，<span style="background-color: #c0c0c0;">初始同步内置了重试逻辑</span>作为容错措施。</p>
<div class="blog_h3"><span class="graybg">复制</span></div>
<p>在初始同步之后，从节点会不断的复制增量数据 —— 异步的拷贝Oplog并应用到数据库中。</p>
<p style="text-align: left;">从节点会根据PING延迟、其它节点的复制状态，<span style="background-color: #c0c0c0;">自动切换拷贝Oplog的源节点</span>。 但是：</p>
<ol>
<li>从3.2开始，投票权1的节点不得把投票权0的节点作为源</li>
<li>会避免将延迟从节点、因从从节点作为源</li>
<li>如果当前节点的复制集配置<pre class="crayon-plain-tag">members[n].buildIndexes</pre>为true，则仅此配置也为true的其它节点，才能作为源。此配置默认true</li>
</ol>
<p>为了增强性能，MongoDB会使用多线程来应用Oplog，日志中的条目被按照名字空间（MMAPv1）、文档标识（WiredTiger）分组，每个组一个线程，同时入库。日志的入库顺序总是和主节点上相同，在分组<span style="background-color: #c0c0c0;">批量入库期间，从节点阻塞所有读请求</span>，因此不会读到主节点上不存在的数据</p>
<div class="blog_h3"><span class="graybg">索引预抓取</span></div>
<p>对于MMAPv1引擎来说，MongoDB会抓取包含了受到影响的数据、索引的内存页，用于辅助增强应用Oplog条目的性能。</p>
<p>此索引预抓取可以最小化应用Oplog时持有写锁的时间，默认的从节点预抓取所有索引。配置项<pre class="crayon-plain-tag">secondaryIndexPrefetch</pre>与该特性有关。</p>
<div class="blog_h2"><span class="graybg">部署架构</span></div>
<p>复制集的架构影响其容量和能力。</p>
<p>通常情况下，生产环境中使用<span style="background-color: #c0c0c0;">3成员的复制集</span>，这保证了数据冗余和容错，同时也避免过于复杂的部署。请结合应用程序需要，并注意：</p>
<ol>
<li>复制集最多具有50个成员，但是能投票的最多7个成员。因此，如果复制集已经有7个成员，再添加节点必须是非投票成员</li>
<li>投票成员数量应为奇数，必要时添加一个仲裁节点，确保此规则</li>
<li>为了满足特殊的需求，考虑添加延迟、隐藏从节点</li>
<li>负载均衡与大量读负载：如果应用面临很高的读负载，可以将负载分发到从节点以提升性能。如果应用跨地域分布，可以考虑在不同数据中心部署从节点</li>
<li>异地灾备：为了防止数据中心出现重大灾难而导致数据丢失，至少应当部署一个异地的从节点。为了<span style="background-color: #c0c0c0;">确保主数据中心的的节点优先被选举</span>为主节点，配置异地节点的<pre class="crayon-plain-tag">members[n].priority</pre>为更小的值</li>
<li>为从节点应用标签集（Tag set）：你可以为节点应用一系列标签，从而把读操作引向特定的节点，或者定制写关注——从其它节点获得请求确认</li>
<li>使用日志防止断电导致的数据丢失：MongoDB默认启用了写前日志（journaling ），可以有效的防止断电、宕机导致的数据丢失</li>
<li>网络、计算资源受限的节点，应该避免称为主节点，使用priority0</li>
</ol>
<p>如果你的应用程序连接到多个复制集，则每个复制集需要具有唯一的名称，某些驱动根据此名称对复制集进行分组。</p>
<div class="blog_h3"><span class="graybg">三成员模式</span></div>
<p>这是典型的、最小化的复制集架构。三个成员中，可以有一个作为仲裁节点。</p>
<p>使用三个数据节点时，数据具有三份拷贝，提供了容错和HA。如果主节点宕机，一个从节点会被选举为新主节点。宕机主节点恢复后，自动重新加入复制集。</p>
<div class="blog_h3"><span class="graybg">多数据中心模式</span></div>
<p>在异地数据中心部署节点，可以抵御断电、网络中断甚至自然灾害。</p>
<p>要实现异地灾备，至少在其它数据中心部署一个从节点。可能的话，使用奇数个数的数据中心。要尽可能保证，即使一个数据中心完全不可用，复制集仍然能保证majority原则。</p>
<p>部署示例：</p>
<ol>
<li>三成员复制集：
<ol>
<li>两数据中心：DC1两成员，DC2一成员，如果有仲裁者，部署在DC1。当DC1不可用时复制集变为只读；当DC2不可用时DC1继续运行（大多数原则）</li>
<li>三数据中心：每个数据中心一个节点。不管哪个数据中心不可用，复制集仍然可读写</li>
</ol>
</li>
<li>五成员复制集：
<ol>
<li>两数据中心：DC1三成员，DC2两成员。当DC1不可用时复制集变为只读；当DC2不可用时DC1继续运行（大多数原则）</li>
<li>三数据中心：DC1两成员，DC2两成员，DC3一成员。不管哪个数据中心不可用，复制集仍然可读写（大多数原则）</li>
</ol>
</li>
</ol>
<div class="blog_h2"><span class="graybg">高可用</span></div>
<p>利用<span style="background-color: #c0c0c0;">自动的故障转移机制</span>，复制集提供高可用性。故障转移即当主节点不可用时，某个从节点被选举为新主节点的过程。</p>
<p>从3.2开始，MongoDB引入版本1（(protocolVersion: 1）的复制协议，<span style="background-color: #c0c0c0;">降低了故障转移消耗的时间，加速重复主节点检测速度</span>。新创建的复制集会自动使用版本1的协议。</p>
<div class="blog_h3"><span class="graybg">主节点选举</span></div>
<p>复制集使用选举机制来确定谁作为主节点，在以下情况下，发生选举：</p>
<ol>
<li>初始化一个复制集时</li>
<li>当主节点变得不可用时</li>
</ol>
<p>主节点的选举需要时间，在选举完成之前，复制集不能接受写请求，所有节点呈只读状态。</p>
<p>如果复制集的大部分节点对于主节点不可访问，则主节点会setp down并变成从节点。这样复制集就不能接受写请求了，但是如果配置了在从节点上执行查询，则剩余节点仍然支持读操作。</p>
<p>影响主节点选举的因素包括：</p>
<ol>
<li>复制协议：从3.2引入的版本1提高了性能</li>
<li>心跳：复制集成员每2秒向各成员发送心跳，如果心跳应答<span style="background-color: #c0c0c0;">10秒没有返回</span>，标记为不可达（inaccessible）</li>
<li>成员优先级：选举算法会仅可能的让高优先级的从节点触发选举。优先级影响选举的耗时和结果，因为高优先级的节点会更快的触发选举并很可能赢得选举。但是，低优先级的成员也可能临时的被选举为主节点，此时选举会继续，直到最高优先级的成员当选</li>
<li>数据中心不可用：多数据中心部署的情况下，某个数据中心整体不可用可能导致无法选举</li>
<li>否决：版本1协议取消了否决机制。但是版本0中任何成员均具有否决权</li>
</ol>
<div class="blog_h3"><span class="graybg">投票成员</span></div>
<p>复制集成员的配置<pre class="crayon-plain-tag">members[n].votes</pre>以及其状态（state）决定了它是否具有投票资格：</p>
<ol>
<li><pre class="crayon-plain-tag">members[n].votes=1</pre>的节点具有投票资格，要禁止某个节点投票，可以设置为0。从3.2开始，非投票节点的priority必须为0</li>
<li>仅仅以下状态的节点可以参与投票：PRIMARY、SECONDARY、RECOVERING、ARBITER、ROLLBACK</li>
</ol>
<div class="blog_h3"><span class="graybg">故障转移导致的回滚</span></div>
<p>回滚导致前主节点上的写操作被撤销，当此“前主节点”重新加入到复制时。回滚仅仅在主节点已经应用了写操作，但是此<span style="background-color: #c0c0c0;">写操作没有来得及在step down之前被成功复制</span>的情况下。回滚保证了数据一致性。</p>
<p>如果写操作被任意从节点复制，并且此从节点可以被大多数节点访问，则回滚不会发生。</p>
<p>对于复制集，默认写关注<pre class="crayon-plain-tag">{w: 1}</pre>仅仅要求主节点的确认，这样存在回滚的可能，但是客户端认为数据已经持久化。要避免这种回滚，使用 <pre class="crayon-plain-tag">w: "majority"</pre>。另外：</p>
<ol>
<li>如果writeConcernMajorityJournalDefault设置为false，即使使用前面的写关注，仍然不能保证不会回滚，因为写操作可能没有持久化到磁盘</li>
<li>不管写关注设置为什么，local读关注都可能看到甚至没有被写者确认的数据。local可能读到之后被回滚的数据</li>
</ol>
<p>一个mongod<span style="background-color: #c0c0c0;">不支持回滚超过300MB的数据</span>。你可以在mongod日志中看到：</p>
<pre class="crayon-plain-tag">[replica set sync] replSet syncThread: 13410 replSet too much data to roll back </pre>
<p>如果系统需要回滚超过300MB的数据，你必须手工介入，强制初始同步 —— 删除前主节点的dbPath目录。</p>
<div class="blog_h2"><span class="graybg">复制集的读写语义</span></div>
<p>从客户端角度来说，mongod以独立方式运行，还是作为复制集成员，是透明的。但是，MongoDB提供了额外的读写选项，提供数据一致性保证。</p>
<div class="blog_h3"><span class="graybg">写关注</span></div>
<p>对于复制集，默认写关注仅要求来自主节点的确认。你可以改变此选项，要求来自指定数量/大多数节点的确认。例如，使用w:2时写操作的处理流程如下：<img class="aligncenter  wp-image-15133" src="https://blog.gmem.cc/wp-content/uploads/2015/05/crud-write-concern-w2.png" alt="crud-write-concern-w2" width="435" height="452" /></p>
<p>要修改默认写关注选项，可以为复制集配置<pre class="crayon-plain-tag">settings.getLastErrorDefaults</pre>选项，示例代码：</p>
<pre class="crayon-plain-tag">cfg = rs.conf()
cfg.settings.getLastErrorDefaults = { w: "majority", wtimeout: 5000 }
rs.reconfig(cfg)</pre>
<div class="blog_h3"><span class="graybg">读设置</span></div>
<p>读设置描述MongoDB如何把读操作请求路由到某个复制集成员。<span style="background-color: #c0c0c0;">默认</span>情况下，应用程序把<span style="background-color: #c0c0c0;">读请求</span>发送给复制集中的<span style="background-color: #c0c0c0;">主节点</span>。读设置与直接连接到单个mongod的那些客户端没有关系。修改读设置时需要小心，从节点可能返回陈旧的数据，原因是复制的异步性。</p>
<p>使用Shell时，可以调用游标的<pre class="crayon-plain-tag">cursor.readPref(mode,tagset)</pre>方法进行读设置，执行查询时，可以这样进行读设置：</p>
<pre class="crayon-plain-tag">// 从最近的节点读取数据，仅仅从东部数据中心读取（根据节点的标签集）
db.collection.find().readPref('nearest', [ { 'dc': 'east' } ])</pre>
<p>读设置的mode可以取值：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">取值</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>primary</td>
<td>默认模式，所有读请求发送给主节点</td>
</tr>
<tr>
<td>primaryPreferred</td>
<td>使用主节点，主节点不可用时才使用从节点</td>
</tr>
<tr>
<td>secondary</td>
<td>所有读请求分发给从节点</td>
</tr>
<tr>
<td>secondaryPreferred</td>
<td>使用从节点，从节点不可用时才使用主节点</td>
</tr>
<tr>
<td>nearest</td>
<td>从网络延迟最低的节点读取，不管其类型</td>
</tr>
</tbody>
</table>
<p>标签集用于指定仅具有特定标签的复制集成员才可以接收读操作请求。除了primary模式外，都可以指定该选项。</p>
<p>3.4引入的<pre class="crayon-plain-tag">maxStalenessSeconds</pre>，用于指定一个最大的复制延迟值，如果某个节点的延迟超过此值，则不用它进行读操作。除了primary模式外，都可以指定该选项。</p>
<p>读设置适用于通过mongos连接到分片集群的客户端。mongos连接到某个以复制集形式出现的分片时，遵守此读设置。</p>
<p>使用非主读设置的应用场景：</p>
<ol>
<li>运行不影响前端应用程序的系统操作</li>
<li>对跨地域分布的应用程序提供本地读功能，考虑使用读设置<pre class="crayon-plain-tag">nearest</pre>，以获得低延迟</li>
<li>在故障转移期间维持可用性，考虑使用读设置<pre class="crayon-plain-tag">primaryPreferred</pre>，这样正常情况下读取主节点，如果主节点不可用则允许读取（只读）可能陈旧的数据</li>
</ol>
<p>在一般场景下，不要使用<pre class="crayon-plain-tag">secondary</pre>或者<pre class="crayon-plain-tag">secondaryPreferred</pre>来提供额外的读容量，因为：</p>
<ol>
<li>复制集中的大部分成员的写流量是差不多的，因而，从节点不会比主节点具有更大的读能力</li>
<li>复制是异步进行的，你可能读取到过时的数据，从不同的从节点读取数据，可能导致非单调读</li>
<li>对于针对分片集合的查询，对于启用负载均衡器的集群，由于不完整的chunk迁移，从节点可能返回重复的、丢失的数据</li>
</ol>
<p>要扩容，分片通常是更加优先的选择，因为它利用了多台机器的计算资源。</p>
<div class="blog_h2"><span class="graybg">local数据库</span></div>
<p>每个mongod都拥有一个名为local的数据库，其中存放有关工作集复制处理过程用到的数据、以及其它实例相关的数据，该数据库<span style="background-color: #c0c0c0;">对Replication是不可见</span>的。</p>
<p>该数据库包含以下集合：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">集合</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td colspan="2"><strong><em>所有mongod实例都有的集合</em></strong></td>
</tr>
<tr>
<td>startup_log</td>
<td>
<p>在启动时，monod向此集合插入一条诊断信息：</p>
<p>_id  由主机名和时间戳组成<br />hostname  主机名<br />startTime  服务启动时间<br />startTimeLocal 服务启动时间（本地）<br />cmdLine  启动使用的mogod命令行选项<br />pid  mongod进程的ID<br />buildinfo  mongod的构建信息</p>
</td>
</tr>
<tr>
<td colspan="2"><strong><em>复制集成员拥有的集合</em></strong></td>
</tr>
<tr>
<td>system.replset</td>
<td>保存复制集配置对象，单个文档，调用<pre class="crayon-plain-tag">rs.conf()</pre>可以查看该对象</td>
</tr>
<tr>
<td>oplog.rs</td>
<td>保存oplog的定长集合，复制集配置项oplogSizeMB决定其大小</td>
</tr>
<tr>
<td>replset.minvalid</td>
<td>内部使用，追踪复制状态</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg"><a id="rs-member-state"></a>复制集成员状态</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 30px; text-align: center;">代码</td>
<td style="width: 15%; text-align: center;">状态</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td>STARTUP</td>
<td>尚不是某个复制集的活动成员，每个成员在启动时最初处于这一状态。在此状态时成员会读取复制集配置文档</td>
</tr>
<tr>
<td>1</td>
<td>PRIMARY</td>
<td>作为主节点存在于复制集</td>
</tr>
<tr>
<td>2</td>
<td>SECONDARY</td>
<td>作为从节点存在于复制集</td>
</tr>
<tr>
<td>3</td>
<td>RECOVERING</td>
<td>正在进行启动时自检，或者正在进行回滚、resync。具有投票权</td>
</tr>
<tr>
<td>5</td>
<td>STARTUP2</td>
<td>加入到了工作集，并在进行初始同步</td>
</tr>
<tr>
<td>6</td>
<td>UNKNOWN</td>
<td>不知道该节点的状态</td>
</tr>
<tr>
<td>7</td>
<td>ARBITER</td>
<td>作为仲裁者存在于复制集</td>
</tr>
<tr>
<td>8</td>
<td>DOWN</td>
<td>该节点已经关闭</td>
</tr>
<tr>
<td>9</td>
<td>ROLLBACK</td>
<td>正在进行回滚，不支持读操作</td>
</tr>
<tr>
<td>10</td>
<td>REMOVED</td>
<td>曾经是某个复制集的成员，但是现在被移除了</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">复制集Shell方法</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">方法</td>
<td style="text-align: center;">说明 </td>
</tr>
</thead>
<tbody>
<tr>
<td>rs.add()</td>
<td>为复制集添加一个新成员</td>
</tr>
<tr>
<td>rs.addArb()</td>
<td>为复制集添加一个仲裁者成员</td>
</tr>
<tr>
<td>rs.conf()</td>
<td>
<p>获得复制集的配置文档</p>
<p>对应数据库命令replSetGetConfig</p>
</td>
</tr>
<tr>
<td>rs.freeze()</td>
<td>
<p>在一段时间内，阻止当前成员提起选举自己为主节点</p>
<p>对应数据库命令replSetFreeze</p>
</td>
</tr>
<tr>
<td>rs.help()</td>
<td>打印复制集相关方法的简短帮助</td>
</tr>
<tr>
<td>rs.initiate()</td>
<td>
<p>初始化一个复制集</p>
<p>对应数据库命令replSetInitiate</p>
</td>
</tr>
<tr>
<td>rs.printReplicationInfo()</td>
<td>从主节点的角度，打印复制集的状态</td>
</tr>
<tr>
<td>rs.printSlaveReplicationInfo()</td>
<td>从从节点的角度，打印复制集的状态</td>
</tr>
<tr>
<td>rs.reconfig()</td>
<td>
<p>根据传入的配置文档，对复制集进行重新配置</p>
<p>对应数据库命令replSetReconfig</p>
</td>
</tr>
<tr>
<td>rs.remove()</td>
<td>移除一个复制集成员</td>
</tr>
<tr>
<td>rs.slaveOk()</td>
<td>设置当前连接的slaveOk属性，一般不再使用</td>
</tr>
<tr>
<td>rs.status()</td>
<td>
<p>获得复制集状态</p>
<p>对应数据库命令replSetGetStatus</p>
</td>
</tr>
<tr>
<td>rs.stepDown()</td>
<td>
<p>优雅的关闭当前主节点，让其成为从节点并触发选举</p>
<p>对应数据库命令replSetStepDown</p>
</td>
</tr>
<tr>
<td>rs.syncFrom()</td>
<td>
<p>指定当前从节点的复制源节点</p>
<p>对应数据库命令replSetSyncFrom</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">复制集数据库命令</span></div>
<p>下表仅仅列出没有对应Shell方法的数据库命令：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">命令</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>replSetMaintenance</td>
<td>启用/禁用维护模式，可以将一个从节点置于RECOVERING状态</td>
</tr>
<tr>
<td>resync</td>
<td>强制当前节点从主节点重新同步，仅仅用于主从复制</td>
</tr>
<tr>
<td>isMaster</td>
<td>显示当前成员在复制集中的角色</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">分片</span></div>
<p>解决扩容性问题的两种手段，主要是：</p>
<ol>
<li>垂直扩容（Vertical Scaling）：增加单机的能力，例如更换更强的CPU、增加内存条、增加硬盘等。这种扩容方式有极限，而且成本是指数级增加的。使用云服务时，则不能进行垂直扩容</li>
<li>水平扩容（Horizontal Scaling）：将数据集切分到多个廉价服务器上，通过增加服务器即可提升系统容量</li>
</ol>
<p>分片（Sharding）是跨越多态机器分发、存储数据的手段，属于水平扩容。MongoDB使用分片来支持海量数据集和高吞吐量需求。MongoDB<span style="background-color: #c0c0c0;">在集合的级别进行分片</span>。</p>
<p>分片的优势包括：</p>
<ol>
<li>读/写：进行分片后，MongoDB的读写操作均可以被很好的扩容。当查询包含分片键（前缀）条件时，mongos仅仅发送查询请求给相关的Shard</li>
<li>存储容量：可以使用廉价硬件无限扩展</li>
<li>高可用性：在部分Shard不可用的情况下，仍然能够进行部分的读写操作。配置服务器可以作为复制集部署，保证高可用性</li>
</ol>
<p>执行分片前，应当考虑：</p>
<ol>
<li>分片增加了调试、运维的复杂度</li>
<li>小心的选择分片键，不合理的选择会影响性能</li>
<li>某些需要scatter/gather，因而消耗较多时间</li>
</ol>
<div class="blog_h2"><span class="graybg">分片集群</span></div>
<p>一个MongoDB分片集群（sharded cluster ）由以下成员组成：</p>
<ol>
<li>分片（Shard）：由单个mongod或者一个复制集组成，维护一个数据分片。在生产环境下，每个分片可以定义为3成员复制集</li>
<li>路由器（mongos）：作为查询路由器存在，作为客户端应用和分片集群之间的接口。你可以在每个应用服务器上都部署mongos，或者定义一组mongos并提供前置负载均衡器/代理给应用程序使用</li>
<li>配置服务器：保存分片集群的元数据、配置信息。从3.4开始，复制服务器必须作为复制集部署（CSRS）。在生产环境下，可以定义为3成员复制集</li>
</ol>
<p>下图示意了这些成员之间的关系：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2015/05/sharded-cluster-production-architecture.png"><img class=" wp-image-15181 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2015/05/sharded-cluster-production-architecture.png" alt="sharded-cluster-production-architecture" width="620" height="440" /></a></p>
<p>&nbsp;</p>
<p>应用程序（MongoDB客户端）<span style="background-color: #c0c0c0;">要想和集群进行交互，必须连接到mongos</span> —— 不管你要使用的是分片还是非分片集合，客户端绝不该直接连接到某个Shard，除了执行分片本地管理、维护工作的时候。</p>
<div class="blog_h3"><span class="graybg">主分片</span></div>
<p>分片集群中的每个数据库都有一个主分片（Primary Shard），该分片存放数据库上所有未分片的集合。</p>
<p>当创建新数据库时，mongos会选取集群中保存数据最少的分片，作为数据库的主分片。要改变主分片，可以调用<pre class="crayon-plain-tag">movePrimary</pre>命令，迁移主分片的操作可能消耗很长时间，完毕前不应访问其中的集合。</p>
<div class="blog_h3"><span class="graybg">分片状态</span></div>
<p>执行<pre class="crayon-plain-tag">sh.status()</pre>可以看到分片集群的整体状态信息，包括：</p>
<ol>
<li>数据库的主分片信息</li>
<li>Chunk在Shard中的分布情况</li>
</ol>
<div class="blog_h3"><span class="graybg">配置服务器</span></div>
<p>这类服务器存放分片集群的元数据，元数据记录了：</p>
<ol>
<li>分片集群中所有数据、组件的状态和组织</li>
<li>Chunk对应的值范围</li>
<li>每个分片包含的Chunks</li>
</ol>
<p>此外，配置服务器还：</p>
<ol>
<li>存储身份验证配置信息，例如基于角色的访问控制、内部验证设置</li>
<li>管理分布式锁</li>
</ol>
<p>mongos会缓存以上信息，以便对读写操作进行路由。当元数据变更后mogos的缓存会自动更新。</p>
<p>从3.4开始，配置服务器必须使用满足以下要求的复制集：</p>
<ol>
<li>具有0个仲裁者</li>
<li>不存在延迟节点</li>
<li>任何节点的buildIndexes设置不得为false</li>
</ol>
<div class="blog_h3"><span class="graybg">配置服务器上的读写</span></div>
<p>配置服务器具有config、admin数据库。</p>
<p>admin数据库包含与认证/授权相关的集合，以及 system.*集合（内部使用）。</p>
<p>config数据库包含了分片集群的元数据，当元数据变更（例如chunk迁移、chunk分裂）时MongoDB会写此数据库，而mongos会读取该数据库。客户端不应该直接写此数据库。读写此数据库时，均使用关注级别majority。</p>
<div class="blog_h3"><span class="graybg">配置服务器可用性</span></div>
<p>如果配置服务器复制集的主节点宕机，并且不能选举出新的主节点，则集群的元数据变为只读状态。你仍然可以对分片进行读写操作，但是不能进行Chunk迁移、分裂。</p>
<p>如果配置服务器复制集完全不可用，则分片集群不支持任何操作。但是mongos会缓存元数据，因而在重新启动mongos之前，仍然能进行分片的读写。</p>
<p>由于配置服务器非常重要，并且数据量很小，应该重视对其进行备份。</p>
<div class="blog_h3"><span class="graybg">路由器</span></div>
<p>路由器（mongos）这样路由查询：</p>
<ol>
<li>确定需要接收查询请求的Shard列表</li>
<li>在所有目标分片上建立一个游标</li>
<li>合并各Shard返回的查询结果，返回结果文档给客户端</li>
</ol>
<p>要确保客户端连接到的是MongoDB实例是路由器，你可以执行isMaster命令，其返回值：</p>
<pre class="crayon-plain-tag">{
   "ismaster" : true,
   "msg" : "isdbgrid",
   "maxBsonObjectSize" : 16777216,
   "ok" : 1
}</pre>
<p>中的msg字段如果为isdbgrid，则说明他是mongos。</p>
<div class="blog_h2"><span class="graybg">分片键</span></div>
<p>进行分片时，需要决定每一条数据应该存放到哪个Shard上，这依赖于分片键。分片键由集合中<span style="background-color: #c0c0c0;">每个文档都具有的、不变的（immutable）字段</span>构成。一旦选择好分片键，以后就不能再修改，每个分片集合有且只有一个分片键。分片键的选择影响性能、可扩容性。</p>
<p>要对某个集合进行分片，执行：</p>
<pre class="crayon-plain-tag"># namespace：&lt;database&gt;.&lt;collection&gt;
# key 索引规格文档，用于指定分片键
# unique 对分片键进行唯一性约束，哈希分片键不支持。非空集合的唯一性索引必须提前手工创建好
# options.numInitialChunks 使用哈希分片键来创建空的分片集群时，最初创建的Chunk的数量
sh.shardCollection( namespace, key, unique, options )</pre>
<div class="blog_h3"><span class="graybg">索引</span></div>
<p>分片集合必须有一个支持分片键的索引：此索引要么在分片键上创建，要么以分片键开头。如果：</p>
<ol>
<li>集合是空的，并且分片键上没有索引，则sh.shardCollection()自动在分片键上创建索引</li>
<li>集合是非空，你必须在调用sh.shardCollection()之前，<span style="background-color: #c0c0c0;">手工创建索引</span></li>
</ol>
<div class="blog_h3"><span class="graybg">唯一性索引</span></div>
<p>对于分片集合，仅仅<span style="background-color: #c0c0c0;">_id、位于分片键字段的索引、以分片键字段开头的索引</span>可以是唯一性索引。不满足此条件的集合无法分片，分片后你无法在不满足此条件的字段上新建唯一性索引。</p>
<p>如果对分片键使用唯一性索引（sh.shardCollection指定unique为true），则MongoDB可以保证分片键取值的唯一性。如果分片键包含多个字段，则保证其整体上的唯一性而非某个字段。</p>
<div class="blog_h3"><span class="graybg">分片键的限制</span></div>
<p>分片键长度不能超过512字节。要么对索引键进行索引，要么以其为索引前缀。可以对分片键建立哈希索引。多键索引、文本索引、地理空间索引均不支持。</p>
<p>分片键是不可变的，如果你必须改变分片键的值，按以下步骤：</p>
<ol>
<li>把索引数据Dump出来</li>
<li>Drop原先的分片集合</li>
<li>使用新的分片键进行分片</li>
<li>Pre-split分片键范围，确保均匀的数据分布</li>
<li>把Dump出的数据导入到数据库</li>
</ol>
<p>分片键的值是不可变的：你不能修改分片键字段的值。</p>
<div class="blog_h3"><span class="graybg">选择分片键</span></div>
<p>理想的分片键，应该让MongoDB仅可能均匀的把文档分发到各分片上。</p>
<p>分片键的<span style="background-color: #c0c0c0;">基数</span>（cardinality，候选取值数量）决定了负载均衡器能够创建的<span style="background-color: #c0c0c0;">Chunks的最大数量</span>，进而影响水平扩容的有效性。一个特定的分片键值，任何时刻仅仅能存在于一个Chunk上，因此如果分片键的基数为4，则最多创建4个Chunk，因而最多使用4个分片（服务器），添加额外的服务器得不到任何好处。</p>
<p>如果数据模型要求在低基数字段上分片，可以考虑联合某个高基数字段建立联合索引。</p>
<p>尽管具有<span style="background-color: #c0c0c0;">高基数</span>的分片键可以更好的支持水平扩容，但是它<span style="background-color: #c0c0c0;">不能保证数据均匀分布</span>在各分片，因为各键值对应的记录数量（Frequency）差异可能很大。分片键频率（Shard Key Frequency）用来描述一个特定的分片键值出现的频率。如果某些分片键取值具有非常高的Frequency，那么存储高Frequency的Chunks将出现性能瓶颈，并且这些Chunk可能无法再次分裂（单个键值无法再分裂）。</p>
<p>如果数据模型要求在高Frequency字段上分片，可以考虑联合某个低Frequency/Unique字段建立联合索引。</p>
<p>单调变化的分片键可能限制集群的插入吞吐量。因为一段时间内索引的插入可能都被分发到同一个分片（开区间的第一个、最后一个Chunk）上：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2015/05/monotonically-changing-shard-key.png"><img class="aligncenter size-full wp-image-15186" src="https://blog.gmem.cc/wp-content/uploads/2015/05/monotonically-changing-shard-key.png" alt="monotonically-changing-shard-key" width="687" height="348" /></a></p>
<p>&nbsp;</p>
<p>在上图中，如果分片键单调递增，则后续所有insert都分发到Chunk C；如果单调递减，则都分发到Chunk A。</p>
<p>如果数据模型要求在单调递增字段上分片，可以考虑使用哈希分片。</p>
<div class="blog_h2"><span class="graybg">分片策略</span></div>
<p>MongoDB支持两种分片策略。</p>
<div class="blog_h3"><span class="graybg">哈希分片</span></div>
<p>所谓哈希分片，是指使用<span style="background-color: #c0c0c0;">单字段的哈希索引来作为分片键</span>的分片策略。哈希分片导致<span style="background-color: #c0c0c0;">基于分片键的范围查询难以匹配单个/少量分片</span>（查询隔离，Query Isolation），往往需要广播到所有分片。但是，哈希分片让数据<span style="background-color: #c0c0c0;">更加均匀的分布在各Shard</span>中，特别是在分片键单调递增/减的情况下（例如自增长）。</p>
<p>这种分片策略需要对每一个分片键字段的值计算哈希，每个Chunk对应一个哈希值区间。</p>
<p>如果对空集合进行哈希分片，MongoDB默认为每个Shard创建两个空的Chunk，所有这些Chunks覆盖哈希值域。在执行分片时，可以指定numInitialChunks来定制Chunk的数量。</p>
<p>用作哈希分片的字段，应当具有高基数（大量不同的取值），哈希分片适用于<span style="background-color: #c0c0c0;">时间戳、ObjectId之类的单调变化</span>的值类型，你可以基于自动生成的_id进行哈希分片。</p>
<p>要执行哈希分片，调用命令：</p>
<pre class="crayon-plain-tag">sh.shardCollection( "database.collection", { &lt;field&gt; : "hashed" } )</pre>
<p> 注意：MongoDB的哈希索引会在计算哈希值之前，<span style="background-color: #c0c0c0;">把浮点数截断为64-bit的整数</span>，并且不支持大于2^53的浮点数。为防止哈希冲突，你可能需要将分片键乘以10^N后存储。</p>
<div class="blog_h3"><span class="graybg">范围分片</span></div>
<p>这是MongoDB默认的分片策略，它直接根据分片键字段的值来划分Chunk，分片键值相近的文档，可能分布在同一个Shard/Chunk中。这样，进行范围查询时往往能够实现查询隔离。</p>
<p>选择范围分片键时，要注意前文所述的：高基数、低Frequency、非单调变化原则。</p>
<p>要执行范围分片，调用命令：</p>
<pre class="crayon-plain-tag">sh.shardCollection( "database.collection", { &lt;shard key&gt; } )</pre>
<div class="blog_h2"><span class="graybg">块（Chunks）</span></div>
<p>MongoDB把数据划分为Chunk。 MongoDB可以使用分片集群负载均衡器进行跨Shard的Chunk迁移，以保证每个Shard被合理利用。</p>
<p>每个Shard上存储0-N个Chunk，每个Chunk对应了一个分片键取值区间，区间总是[ 闭，开 ) 形式的。所有Chunk正好覆盖分片键的键空间。</p>
<p>将数据集和Chunk而不是Shard进行关联，其好处是：</p>
<ol>
<li>可以按需添加新的机器（Shard），然后轻易的再平衡数据集（迁移Chunk）</li>
<li>在必要时（假设某个Chunk过大），可以进行Chunk分裂，然后进行再平衡</li>
</ol>
<div class="blog_h3"><span class="graybg">块分裂</span></div>
<p>如果块增长超过配置的尺寸，MongoDB会执行块分裂（Split），插入、更新操作都可能引发块分裂。只要块包含<span style="background-color: #c0c0c0;">超过1个的分片键值，就可以被分裂</span>。</p>
<p>MongoDB默认的Chunk尺寸是64MB。修改此默认值时，要注意：</p>
<ol>
<li>小尺寸的Chunk可以让数据分布更加均匀，代价是更加频繁的数据迁移。性能影响产生在mongos层</li>
<li>大尺寸的Chunk导致的迁移较少，因而从网络、mongos角度是高效的。但是可能导致数据不均匀分布</li>
<li>Chunk尺寸影响Chunk能够包含的最大文档数量（一旦超过此数量就会发生分裂）</li>
<li>分片既有集合时，Chunk尺寸影响集合的最大尺寸</li>
<li>实际的分裂行为只能被插入、更新操作触发</li>
<li>分裂不能被撤销，如果你增大Chunk尺寸，那些分裂产生的Chunk不会合并回去</li>
</ol>
<p>在实际应用中，不要为了追求一点点更加平均的分布，而盲目设置过小的Chunk尺寸。</p>
<p><span style="background-color: #c0c0c0;">块分裂本身是高效</span>的元数据级别的操作，不牵涉到数据迁移（跨Shard）。但是，当数据分布不均匀时，集群的负载均衡器可能会自动重新分布Chunks。</p>
<div class="blog_h3"><span class="graybg">块迁移</span></div>
<p>为了在Shard之间更加均匀的分布数据，MongoDB会执行块的迁移 —— 将一个Chunk移动到另外一个Shard上并更新元数据。</p>
<p>块迁移可以：</p>
<ol>
<li>手工执行，你仅仅在一些特殊情况下才需要进行手工迁移</li>
<li>自动执行，当超过迁移阈值时，负载均衡器自动执行</li>
</ol>
<p>负载均衡器（balancer）是专门负责Chunk迁移的后台进程。如果拥有最大、最小数量Chunk的Shards的<span style="background-color: #c0c0c0;">Chunk数量差超过迁移阈值</span>，负载均衡器启动迁移。Zone的配置影响负载均衡器的行为。</p>
<p>如果一个Chunk仅仅包含一个分片键值，则它可能超过Chunk尺寸而继续增大，成为巨块（jumbo chunk）。因为近包含一个分片键值，这种块无法再分裂，可能形成性能瓶颈。</p>
<div class="blog_h3"><span class="graybg">archiveMovedChunks</span></div>
<p>2.6/3.0版本的 <pre class="crayon-plain-tag">sharding.archiveMovedChunks</pre>参数默认开启，迁移源Shard会自动把被迁移的Chunks归档到storage.dbPath/moveChunk目录下。</p>
<p>如果迁移过程中出现错误，这些归档文件可以用于数据恢复。一旦迁移完成，这些文件不再有用，你可以将其删除。要判断迁移是否已经完成，连接到mongos并执行命令<pre class="crayon-plain-tag">sh.isBalancerRunning()</pre></p>
<div class="blog_h3"><span class="graybg">预创建块</span></div>
<p>某些场景下，MongoDB自动创建的Chunk不足以满足吞吐量需求。这些场景例如：</p>
<ol>
<li>当你希望对位于单个Shard上的大集合进行分片时</li>
<li>当你希望导入大量数据到一个负载不均衡的集群，或者导入可能导致负载不均衡时 —— 例如使用范围分片的情况下进行单调递增数据导入</li>
</ol>
<p>上面的场景对资源敏感，原因是：</p>
<ol>
<li>块迁移要求将Chunk中所有数据进行跨Shard的移动</li>
<li>任何Shard在同一时刻仅仅能参与一次迁移活动，负载均衡器会串行化的迁移Chunk。从3.4开始限制解除，变为：具有n个分片的集群，负载均衡器最多同时进行n/2个迁移活动</li>
<li>块分裂仅仅会在数据插入/更新后发生</li>
</ol>
<p>要进行预分裂（Pre-split，创建满足需要的Chunks数量），你可以执行<pre class="crayon-plain-tag">split</pre>命令。</p>
<p>警告：你只能在空集合上执行预分裂操作。如果集合已经包含数据，MongoDB在你执行集合分片时自动创建Chunks。后续再进行手工创建Chunks可能导致不可预测的Chunk范围/尺寸，以及低效的负载均衡行为。</p>
<div class="blog_h3"><span class="graybg">手工分裂</span></div>
<p>当Chunk尺寸到达限制后，MongoDB会自动分裂Chunk。但是某些场景下你可能期望手工执行分裂，例如：</p>
<ol>
<li>你有少量Chunks，但是要部署大量的数据到集群中</li>
<li>你需要部署大量可能落到单个Chunk/Shard中的数据，例如分片键值200-500归属于一个Chunk，现在你要插入海量分片键值300-400之间的数据</li>
</ol>
<p>根据需要，负载均衡器可能立即迁移刚刚分裂出来的Chunk，不考虑它是自动还是手工创建的。</p>
<p>调用<pre class="crayon-plain-tag">sh.status()</pre>可以了解当前Chunk对应的值区间。要手工分裂，调用split命令，或者Shell助手：<pre class="crayon-plain-tag">sh.splitFind()</pre>、<pre class="crayon-plain-tag">sh.splitAt()</pre>。</p>
<div class="blog_h3"><span class="graybg">手工合并</span></div>
<p>调用<pre class="crayon-plain-tag">mergeChunks</pre>命令，可以把空Chunk合并到同一Shard上的相邻Chunk上。所谓空Chunk，是值其键值范围内没有文档。</p>
<p>以下场景下，你可能需要手工合并：</p>
<ol>
<li>预创建（Pre-split）了过多的块</li>
<li>你删除了很多文档，导致某些Chunk变空</li>
</ol>
<div class="blog_h3"><span class="graybg">无法迁移</span></div>
<p>如果Chunk包含的文档数量大于：</p>
<ol>
<li>250000，或者</li>
<li>1.3 * Chunk尺寸 / 平均文档尺寸。平均文档尺寸通过db.collection.stats().avgObjSize得到</li>
</ol>
<p>则此Chunk无法被移动。</p>
<div class="blog_h2"><span class="graybg">区域（Zones）</span></div>
<p>在分片集群中，你可以依据分片键来创建分片数据的区域（zone）。<span style="background-color: #c0c0c0;">一个Zone可以关联1-N个Shard，每个Shard可以关联到任意个不冲突的Zone（多对多）</span>。在启用负载均衡的集群里，<span style="background-color: #c0c0c0;">Chunk仅仅在Zone内部迁移</span>。</p>
<p>需要使用Zone的场景包括：</p>
<ol>
<li>在一个Shard集上，隔离某个数据子集</li>
<li>确保相关的数据分布于地理位置相近的Shard上</li>
<li>根据Shard的硬件性能来路由</li>
</ol>
<p>考虑下面这个Zone配置：</p>
<p><img class="aligncenter size-full wp-image-15190" src="https://blog.gmem.cc/wp-content/uploads/2015/05/sharding-zone.png" alt="sharding-zone" width="692" height="330" /></p>
<p>分片键值位于1-10之间的，归属于Zone A；10-20之间的归属于Zone B。Shard A属于Zone A，Shard B属于Zone A和B。这样，键值位于1-10之间的数据子集仅仅能在Shard A、B之间迁移。</p>
<div class="blog_h3"><span class="graybg">键值范围</span></div>
<p>每个Zone可以<span style="background-color: #c0c0c0;">覆盖1-N个键值区间</span>。这些区间总是[ 闭，开 ) 形式的。所有<span style="background-color: #c0c0c0;">Zone的键值区间不得重叠</span>，一个Zone的每个键值区间也不能重叠。</p>
<div class="blog_h3"><span class="graybg">负载均衡</span></div>
<p>负载均衡器会尝试把分片集合的Chunks均匀分发在所有Shard上。</p>
<p>对于标记为待迁移的Chunk，负载均衡器根据Zone配置来确定其可选的目标Shard。如果Chunk的值范围没有落到任何Zone里，则可能被迁移到任意Shard上。</p>
<p>如果负载均衡器发现任何Chunk违反了Zone定义（例如进行了Zone配置），它会将其进行迁移。</p>
<p>进行了Zone配置后，集群可能需要一定时间来执行Chunk迁移，迁移由负载均衡器在下一次 balancing rounds执行。</p>
<div class="blog_h3"><span class="graybg">分片键</span></div>
<p>定义Zone覆盖的值范围时，必须使用分片键或者分片键前缀字段。</p>
<div class="blog_h2"><span class="graybg">负载均衡器</span></div>
<p>MongoDB的集群负载均衡器是一个后台进程，它监控每个Shard上Chunk的数量。当最多 - 最少Chunk数量（按Shard）打到迁移阈值后，负载均衡器尝试进行Chunk迁移，使所有Shard持有仅可能平均数量的Chunks。</p>
<p>负载均衡器的行为对于用户、应用程序是完全透明的，但是迁移过程可能对性能产生影响。</p>
<p>从3.4开始，<span style="background-color: #c0c0c0;">负载均衡器在配置服务器的主节点上运行</span>。一旦其进程激活，就会修改配置服务器的lock集合上的一个文档，获得一个“锁”，这个锁一直不会释放。</p>
<p>从2.6开始，负载均衡器可能影响磁盘使用，因为迁移会导致源Shard对被迁移chunk进行归档。</p>
<p>负载均衡器会带来带宽、工作负载方面的成本，从而影响数据库的整体性能。它通过下面的措施使影响最小化：</p>
<ol>
<li>任何时刻仅能执行一个Chunk的迁移。从3.4开始，允许N个Shard的集群同时进行N/2个块迁移</li>
<li>仅仅当到达迁移阈值（migration threshold）时才启动负载均衡周期（balancing round）</li>
</ol>
<p>你可以临时的禁用负载均衡器，以执行维护任务。你也可以限制负载均衡器运行的时间窗口，防止对生产环境产生不利影响。</p>
<div class="blog_h3"><span class="graybg">增减Shard</span></div>
<p>添加Shard到集群时，会导致负载不均衡的出现，因为新的Shard不持有任何Chunk，达到再平衡需要一定的时间。</p>
<p>从集群中移除Shard时，其中的Chunk必须被重新分发，达到再平衡需要一定的时间，切勿再迁移完成前关闭被移除Shard对应的服务器。</p>
<div class="blog_h3"><span class="graybg">迁移工作流</span></div>
<p>所有块迁移均遵循以下流程：</p>
<ol>
<li>负载均衡器向源Shard发送moveChunk命令</li>
<li>源Shard使用内部moveChunk命令启动。在<span style="background-color: #c0c0c0;">迁移完成之前，路由到被移动Chunk的读写请求仍然由源Shard负责</span></li>
<li>目标Shard构建接纳Chunk所需要的、尚不存在的索引</li>
<li>目标Shard开始请求Chunk中的文档，接收数据拷贝并入库</li>
<li>接收完最后一个文档后，目标Shard启动一个同步进程，确保在迁移过程中，对被迁移Chunk的写操作被应用</li>
<li>当完全同步后，源Shard连接到配置数据库，并更新集群元数据，写入Chunk的新位置</li>
<li>元数据写入完毕后，一旦没有针对打开的被迁移Chunk的游标，源Shard删除本地的Chunk数据拷贝。如果负载均衡器需要针对源Shard进行下一个Chunk迁移，它不会等待删除操作的完成。从2.6开始，被迁移Chunk会在源Shard归档</li>
</ol>
<div class="blog_h3"><span class="graybg">迁移阈值</span></div>
<p>此阈值考虑Shard上的Chunk数量的最大差。默认取值：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">Chunk数量</td>
<td style="text-align: center;">阈值</td>
</tr>
</thead>
<tbody>
<tr>
<td>&lt; 20</td>
<td>2</td>
</tr>
<tr>
<td>20-79</td>
<td>4</td>
</tr>
<tr>
<td>&gt;=80</td>
<td>8</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">迁移与复制</span></div>
<p>配置项_secondaryThrottle的值，决定了负载均衡器何时处理Chunk中下一个被迁移的文档（在目标Shard？）：</p>
<ol>
<li>如果取值为true，则当前文档必须至少被复制到一个从节点，才处理下一个文档，相当于写关注{ w :2 }</li>
<li>如果取值为false，则不等待复制到从节点，直接处理下一个文档</li>
</ol>
<p>从3.4开始，如果使用 WiredTiger引擎，该配置的默认值false， MMAPv1引擎的默认值仍然为true。</p>
<p>此外，不管_secondaryThrottle如何取值，迁移工作流的某些阶段，遵行如下复制策略：</p>
<ol>
<li>在更新集群元数据（第6步）之前，MongoDB会短暂的暂停针对源Shard上被迁移集合的读写操作。在更新元数据的前后，要求移动Chunk的写操作被复制集大多数节点确认</li>
<li>在执行源Shard清理（第7步）或者新的Chunk迁移之前，写操作必须被目标Shard复制集的大多数节点确认</li>
</ol>
<div class="blog_h2"><span class="graybg">分片集群Shell方法</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 30%; text-align: center;">方法</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>sh._adminCommand()</td>
<td>针对admin数据库执行一个数据库命令</td>
</tr>
<tr>
<td>sh.getBalancerLockDetails()</td>
<td>报告负载均衡器锁的详细信息</td>
</tr>
<tr>
<td>sh._checkMongos()</td>
<td>检查当前Shell是否连接到mongos</td>
</tr>
<tr>
<td>sh._lastMigration()</td>
<td>报告最后一次发生的块迁移</td>
</tr>
<tr>
<td>sh.addShard()</td>
<td>
<p>添加一个分片到集群中</p>
<p>对应数据库命令addShard</p>
</td>
</tr>
<tr>
<td>sh.addShardTag()</td>
<td>sh.addShardToZone()的别名</td>
</tr>
<tr>
<td>sh.addShardToZone()</td>
<td>
<p>关联一个Shard到一个Zone</p>
<p>对应数据库命令addShardToZone</p>
</td>
</tr>
<tr>
<td>sh.addTagRange()</td>
<td>sh.updateZoneKeyRange()的别名</td>
</tr>
<tr>
<td>sh.updateZoneKeyRange()</td>
<td>关联一个分片键值范围到一个Zone。每个Zone可以关联多个值范围区间</td>
</tr>
<tr>
<td>sh.removeTagRange()</td>
<td>sh.removeRangeFromZone()的别名</td>
</tr>
<tr>
<td>sh.removeRangeFromZone()</td>
<td>解除一个分片键值范围与Zone的关联</td>
</tr>
<tr>
<td>sh.disableBalancing()</td>
<td>针对单个分片集合禁用负载均衡功能</td>
</tr>
<tr>
<td>sh.enableBalancing()</td>
<td>针对单个分片集合启用负载均衡功能</td>
</tr>
<tr>
<td>sh.enableSharding()</td>
<td>
<p>针对某个特定的数据库启用分片功能</p>
<p>对应数据库命令enableSharding</p>
</td>
</tr>
<tr>
<td>sh.getBalancerState()</td>
<td>返回一个布尔值，说明负载均衡器是否启用（全局的）</td>
</tr>
<tr>
<td>sh.help()</td>
<td>显示分片集群的Shell方法的简短帮助</td>
</tr>
<tr>
<td>sh.isBalancerRunning()</td>
<td>负载均衡器是否正在迁移Chunk</td>
</tr>
<tr>
<td>sh.moveChunk()</td>
<td>在分片集群中移动一个Chunk</td>
</tr>
<tr>
<td>sh.removeShardTag()</td>
<td>sh.removeShardFromZone()的别名</td>
</tr>
<tr>
<td>sh.removeShardFromZone()</td>
<td>
<p>从Zone中移除一个Shard</p>
<p>对应数据库命令removeShardFromZone</p>
</td>
</tr>
<tr>
<td>sh.setBalancerState()</td>
<td>启用或者禁用负载均衡器（全局的）</td>
</tr>
<tr>
<td>sh.shardCollection()</td>
<td>
<p>对一个集合进行分片</p>
<p>对应数据库命令shardCollection</p>
</td>
</tr>
<tr>
<td>sh.splitAt()</td>
<td>以分片键的某个值为分割点，把一个Chunk分割为两个</td>
</tr>
<tr>
<td>sh.splitFind()</td>
<td>将包含满足查询条件的Chunk分割为两个近似相等的新Chunks</td>
</tr>
<tr>
<td>sh.startBalancer()</td>
<td>
<p>启用负载均衡器并等待负载均衡触发</p>
<p>对应数据库命令balancerStart</p>
</td>
</tr>
<tr>
<td>sh.status()</td>
<td>报告分片集群的状态</td>
</tr>
<tr>
<td>sh.stopBalancer()</td>
<td>
<p>禁用负载均衡器并等待可能正在进行的负载均衡操作完毕</p>
<p>对应数据库命令balancerStop</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">分片集群数据库命令</span></div>
<p>下表仅仅列出没有对应Shell方法的数据库命令：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">命令</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>flushRouterConfig</td>
<td>强制清空mongos的分片集群配置元数据缓存</td>
</tr>
<tr>
<td>balancerStatus</td>
<td>报告负载均衡器的状态信息</td>
</tr>
<tr>
<td>cleanupOrphaned</td>
<td>清理孤儿数据，所谓孤儿数据，是指其分片键值超越了某个Shard拥有的Chunk范围的数据</td>
</tr>
<tr>
<td>listShards</td>
<td>列出集群中的分片</td>
</tr>
<tr>
<td>removeShard</td>
<td>从集群中移除一个分片。集群将把该分片的Chunks迁移到其它分片上，这需要时间 </td>
</tr>
<tr>
<td>mergeChunks</td>
<td>合并单个分片上的多个Chunk</td>
</tr>
<tr>
<td>shardingState</td>
<td>报告当前mongod是否某个分片集群的成员 </td>
</tr>
<tr>
<td> split</td>
<td>创建一个新的Chunk</td>
</tr>
<tr>
<td> movePrimary</td>
<td>当从集群移除分片时，用于重新分片某个数据库的主分片</td>
</tr>
<tr>
<td> isdbgrid</td>
<td> 检查进程是否是mongos</td>
</tr>
<tr>
<td>updateZoneKeyRange</td>
<td> 添加/删除一个Zone关联的分片键值区间</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">管理MongoDB</span></div>
<div class="blog_h2"><span class="graybg">配置文件</span></div>
<p>启动mongos、mongod实例时，可以指定一个配置文件，提供必要的选项。MongoDB的配置文件是YAML-based格式的，不支持tab，要使用space代替。</p>
<p>要使用配置文件，指定命令行选项：</p>
<pre class="crayon-plain-tag">#  -f 是 --config 的别名
mongod --config /etc/mongod.conf
mongos --config /etc/mongos.conf</pre>
<div class="blog_h3"><span class="graybg">选项详解</span></div>
<pre class="crayon-plain-tag"># 日志选项
systemLog:
   # 默认日志级别，0-5之间，默认0，越大日志越详细
   # 要指定某个MongoDB组件的日志级别，使用 systemLog.component.&lt;name&gt;.verbosity
   verbosity: &lt;int&gt;
   # 安静模式，布尔，可以减少日志输出
   quiet: &lt;boolean&gt;
   # 截断异常信息，布尔
   traceAllExceptions: &lt;boolean&gt;
   # 记录到syslog使用的facility级别，默认user
   syslogFacility: &lt;string&gt;
   # 输出到指定的文件，而不是syslog中
   path: &lt;string&gt;
   # 默认false，是否追加内容到日志文件中
   logAppend: &lt;boolean&gt;
   # 日志轮转方式，rename / reopen
   logRotate: &lt;string&gt;
   # 日志记录目标，syslog / file ，选择file则必须设置systemLog.path
   destination: &lt;string&gt;
   # 日志时间戳格式，ctime / iso8601-utc / iso8601-local
   # 默认iso8601-local，示例 1969-12-31T19:00:00.000-0500
   timeStampFormat: &lt;string&gt;
   # 配置特定MongoDB组件的日志选项
   # 组件：accessControl / command / control / ftdc / geo / index / network / query /
   #       replication / sharding / storage / storage.journal / write 
   component:
      accessControl:
         verbosity: &lt;int&gt;
      command:
         verbosity: &lt;int&gt;

# 进程管理选项
processManagement:
   # 默认false，是否以daemon方式来启动mongos/mongod，默认不是daemon方式
   # MongoDB的Linux包期望此选项为默认值
   fork: &lt;boolean&gt;
   # PID文件的路径
   pidFilePath: &lt;string&gt;

# 网络选项
net:
   # 监听连接的端口，默认27017
   port: &lt;int&gt;
   # 监听的网络接口，默认0.0.0.0
   bindIp: &lt;string&gt;
   # mongos/mongod允许的最大连接数，默认65536
   maxIncomingConnections: &lt;int&gt;
   # 默认true，对每个客户端请求进行校验，防止插入无效、恶意的BSON到数据库
   wireObjectCheck: &lt;boolean&gt;
   # 默认false，是否启用IPv6支持
   ipv6: &lt;boolean&gt;
   # 在UNIX Domain套接字上监听
   unixDomainSocket:
      # 是否启用
      enabled: &lt;boolean&gt;
      # 套接字路径前缀，默认/tmp
      pathPrefix: &lt;string&gt;
      # 套接字文件权限，默认 0700
      filePermissions: &lt;int&gt;
   # 提供RESTful API的HTTP接口，不要在生产环境使用
   http:
      # 是否启用
      enabled: &lt;boolean&gt;
      # 是否支持JSONP
      JSONPEnabled: &lt;boolean&gt;
      # 是否提供RESTful接口
      RESTInterfaceEnabled: &lt;boolean&gt;
   ssl:
      # SSL的运作模式：disabled禁用；allowSSL服务器互联不使用，允许客户端连接使用；
      #              preferSSL服务器互联默认使用，允许客户端不适用；requireSSL 强制所有连接使用SSL
      mode: &lt;string&gt;
      # 同时包含证书和密钥的.pem文件
      # 如果连接时没有指定--sslCAFile则使用系统CA来验证服务器证书
      PEMKeyFile: &lt;string&gt;
      # 如果.pem加密，这里提供密码
      PEMKeyPassword: &lt;string&gt;
      # 用于集群成员、复制集成员相互认证的pem文件
      clusterFile: &lt;string&gt;
      # 如果.pem加密，这里提供密码
      clusterPassword: &lt;string&gt;
      # 包含了根证书链的.pem文件
      CAFile: &lt;string&gt;
      # 包含了证书吊销列表的.pem文件
      CRLFile: &lt;string&gt;
      # 是否允许客户端不提供证书（单向认证）
      allowConnectionsWithoutCertificates: &lt;boolean&gt;
      # 是否允许使用无效证书（集群中其它服务器）
      allowInvalidCertificates: &lt;boolean&gt;
      # 是否允许 TLS/SSL 证书和主机名不匹配
      allowInvalidHostnames: &lt;boolean&gt;
      # 禁用的入站协议类型TLS1_0、TLS1_1、TLS1_2
      disabledProtocols: &lt;string&gt;
      # 是否启用OpenSSL的FIPS模式
      FIPSMode: &lt;boolean&gt;
   # 是否启用服务器之间，服务器与Shell之间的数据压缩
   compression:
      compressors: &lt;string&gt;  # 压缩器，例如snappy

# 安全选项
security:
   # 对分片集群、复制集中其它成员进行认知的共享密钥所在的文件
   keyFile: &lt;string&gt;
   # 集群的认证方式：keyFile共享密钥文件；sendKeyFile发送共享密钥但是也允许其它节点以X509方式请求本节点
   #              sendX509发送X509但是也允许其它节点以共享密钥方式请求本节点；x509 推荐，仅X509
   clusterAuthMode: &lt;string&gt;
   # 默认disabled，可选disabled。是否启用基于角色的访问控制（RBAC），对应命令行选项 --auth
   authorization: &lt;string&gt;
   # 默认false，3.4新增。允许mongos/mongd接受/创建到其它mongos/mongd的认证/非认证连接
   transitionToAuth: &lt;boolean&gt;
   # 是否启用服务器端的JS执行，默认true。如果禁用，则无法使用$where、db.collection.mapReduce()、db.collection.group()等
   javascriptEnabled:  &lt;boolean&gt;

# 设置MongoDB服务器参数
setParameter:
   &lt;parameter1&gt;: &lt;value1&gt;
   &lt;parameter2&gt;: &lt;value2&gt;

# 存储选项
storage:
   # mongod实例存储数据的位置，默认/data/db 
   dbPath: &lt;string&gt;
   # 默认true，是否在下一次启动时构建未完成的索引
   indexBuildRetry: &lt;boolean&gt;
   # 当mongod以--repair进行修复启动时，使用的工作目录
   repairPath: &lt;string&gt;
   # 是否启用日志，64bit默认true，32bit默认false
   journal:
      # 是否启用
      enabled: &lt;boolean&gt;
      # 允许写入日志的最小间隔，默认100ms
      commitIntervalMs: &lt;num&gt;
   # 是否为每个数据库新建子目录，默认false
   directoryPerDB: &lt;boolean&gt;
   # 每隔多久把数据刷入数据文件中，默认60s，通常不需要修改。如果设置为0从不把内存映射文件刷入磁盘
   syncPeriodSecs: &lt;int&gt;
   # 使用的存储引擎，mmapv1 / wiredTiger / inMemory
   engine: &lt;string&gt;
   # 引擎特定选项
   mmapv1:
      # 是否预分配数据文件的磁盘空间
      preallocDataFiles: &lt;boolean&gt;
      # 名字空间文件的大小，每个集合、索引都被认为是名字空间
      # 默认16MB，允许大概24000个名字空间
      nsSize: &lt;int&gt;
      quota:
         # 是否限制每个数据库能够拥有的数据文件的数量
         enforced: &lt;boolean&gt;
         # 每个数据库的最大数据文件数量，默认8
         maxFilesPerDB: &lt;int&gt;
      # 使用小数据文件，设置为true则减小数据文件的初始大小，且不允许超过512MB
      # 设置为true，同时导致日志文件的最大尺寸从1G变为128MB
      # 可能导致创建大量的数据文件，从而影响性能
      smallFiles: &lt;boolean&gt;
      journal:
         debugFlags: &lt;int&gt;
         commitIntervalMs: &lt;num&gt;
   # 引擎特定选项
   wiredTiger:
      engineConfig:
         # 为所有数据准备的内部缓存的大小，最小256MB，默认RAM /2 - 1GB
         # 避免修改此配置，因为该引擎可以利用OS所有空闲内存作为缓存
         # 使用容器时，配合容器可用内存设置该选项
         cacheSizeGB: &lt;number&gt;
         # 日志的压缩算法 snappy / zlib
         journalCompressor: &lt;string&gt;
         # 是否在不同子目录存放索引、数据
         directoryForIndexes: &lt;boolean&gt;
      collectionConfig:
         # 集合的默认压缩算法 none / snappy / zlib
         # 你可以在创建集合的时候覆盖
         blockCompressor: &lt;string&gt;
      indexConfig:
         # 是否启用索引前缀压缩
         prefixCompression: &lt;boolean&gt;
   # 引擎特定选项
   inMemory:
      engineConfig:
         # 使用的内存量
         inMemorySizeGB: &lt;number&gt;

# 性能剖析选项
operationProfiling: 
   # 超过多少ms，操作被剖析器认为是缓慢的
   slowOpThresholdMs: &lt;int&gt;
   # 剖析模式： off / slowOp / all
   mode: &lt;string&gt;

# 复制集选项
replication:
   # Oplog的大小，对于64bit系统，默认5%可用磁盘空间
   oplogSizeMB: &lt;int&gt;
   # 该实例所属的复制集的名称
   replSetName: &lt;string&gt;
   # 仅mmapv1引擎，是否预抓取索引
   secondaryIndexPrefetch: &lt;string&gt;
   # 设置读关注为majority
   enableMajorityReadConcern: &lt;boolean&gt;
   # 以下子项仅仅用于mongos
   localPingThresholdMs: &lt;int&gt;

# 分片选项
sharding:
   # 当前分片在集群中的角色，configsvr / shardsvr
   clusterRole: &lt;string&gt;
   # 是否对被迁移的Chunk进行归档保存
   archiveMovedChunks: &lt;boolean&gt;
   # 以下子项仅仅用于mongos
   configDB: &lt;string&gt;

# 审计选项
auditLog:
   # 审计日志的目的地 syslog / console / file
   destination: &lt;string&gt;
   # 审计日志格式 JSON / BSON
   format: &lt;string&gt;
   # 审计日志存放路径
   path: &lt;string&gt;
   # 过滤器文档，不匹配的不记录
   filter: &lt;string&gt;

# SNMP监控选项
snmp:
   # 是否作为subagent运行
   subagent: &lt;boolean&gt;
   # 是否作为master运行
   master: &lt;boolean&gt;

# 全文搜索选项
basisTech
   # Basis Technology Rosette Linguistics Platform 安装目录
   basisTech.rootDirectory</pre>
<div class="blog_h2"><span class="graybg">管理mongod进程</span></div>
<pre class="crayon-plain-tag"># 启动mongod进程，默认数据目录/data/db，端口27017
mongod
# 以守护进程的方式启动，并把日志记录到指定位置
mongod --fork --logpath /var/log/mongodb.log

# 停止服务
use admin
db.shutdownServer()

# 停止服务，方式二
mongod --shutdown</pre>
<div class="blog_h3"><span class="graybg">停止复制集</span></div>
<p>主节点的mongod的停止流程如下：</p>
<ol>
<li>检查从节点的复制进度</li>
<li>如果没有任何从节点延迟小于10s，mongod会返回一个信息，提示不能停止。要强行停止，可以为shutdown命令提供force参数：<br />
<pre class="crayon-plain-tag">db.adminCommand({shutdown : 1, force : true})</pre></p>
<p>如果要持续检查指定的时间，在此时间内有从节点跟上复制进度则关闭，跟不上则不关闭，可以执行：</p>
<pre class="crayon-plain-tag">db.adminCommand({shutdown : 1, timeoutSecs : 5})
# 或者
db.shutdownServer({timeoutSecs : 5})</pre>
</li>
<li>如果由节点延迟小于10s，主节点会Stepdown，并等待从节点跟上复制进度</li>
<li>从节点跟上复制进度，或者60s之后，主节点关闭 </li>
</ol>
<div class="blog_h3"><span class="graybg">停止操作</span></div>
<p>可以指定查询、命令的最长执行耗时：
<pre class="crayon-plain-tag">db.collection.find(...).maxTimeMS(30)
db.runCommand( { maxTimeMS: 45 } )

# 如果查询、命令因为超时而被终止，可以通过db.getLastError() 或者db.getLastErrorObj()得到相关信息</pre>
<p>你可以调用<pre class="crayon-plain-tag">db.killOp(&lt;opId&gt;)</pre>来强制终止某个操作，注意，不要对任何数据库内部操作执行此调用。 执行<pre class="crayon-plain-tag">db.currentOp()</pre>得到正在运行的操作的列表。</p>
<div class="blog_h2"><span class="graybg">生产环境注意事项</span></div>
<div class="blog_h3"><span class="graybg">并发能力</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 15%; text-align: center;">引擎</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>MMAPv1</td>
<td>
<p>2.2以前的版本：实例级别的全局锁，同一时刻只能有一个客户端执行写操作</p>
<p>2.2 - 2.6版本：每个数据库有一个读写锁，允许针对数据库的并发读，但是不允许针对数据库的并发写。也就是说，同一时刻只能有一个客户端在写某个数据库</p>
<p>3.0以后的版本：每个集合有一个读写锁，允许多个客户端同时对多个不同的集合进行写操作</p>
</td>
</tr>
<tr>
<td>WiredTiger</td>
<td>
<p>支持文档级别的并发读写，一个线程可以修改集合C的文档A，同时另外一个线程可以修改集合C的文档B</p>
<p>在写操作进行的过程中，读操作不受限制</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">数据一致性</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 15%; text-align: center;">数据库特性</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>日志</td>
<td>
<p>MongoDB使用预写式磁盘日志，这种日志写入很快，只要写入了该日志，宕机后可以恢复数据，即使没有写到数据文件中</p>
<p>该日志默认开启，要保证宕机后不影响数据一致性，就不要关闭它</p>
</td>
</tr>
<tr>
<td>读关注</td>
<td>
<p>当使用majority、linearizable两种读关注时，必须配合写关注{ w: "majority" }，这样可以确保线程可以读到它自己先前的写操作</p>
<p>当使用读关注majority时：</p>
<ol>
<li>必须配置replication.enableMajorityReadConcern = true</li>
<li>复制集必须使用WiredTiger引擎，且使用协议版本1</li>
</ol>
</td>
</tr>
<tr>
<td>写关注</td>
<td>写关注影响写操作的返回数据，强写关注导致返回更慢，但是可以更好的确保数据一致性</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">网络</span></div>
<p>你应当总是在受信任的网络中运行MongoDB，使用合理的防火墙策略来阻止非受信任的机器、系统、网络。通常，仅仅应用服务器、监控服务、其它MongoDB组件需要MongoDB的访问权限。</p>
<p>默认情况下，MongoDB的授权系统是关闭的，任何人都可以访问，需要注意。</p>
<p>MongoDB提供了一个用于检查服务器状态、执行查询的HTTP接口，此接口默认是关闭的，不要在生产环境中开启此接口。</p>
<p>避免盲目调整mongos/mongod的连接池大小，以满足你的需要。<pre class="crayon-plain-tag">connPoolStats</pre>命令显示当前数据库中打开的连接数。</p>
<div class="blog_h3"><span class="graybg">CPU和RAM</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 15%; text-align: center;">存储引擎</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>MMAPv1</td>
<td>
<p>由于其并发性，你不需要为MMAPv1分配过多的CPU资源，但是应该保证mongos/mongod能够访问两个真实CPU</p>
<p>增加MongoDB可访问的内存量可以减少页错误发生的频率</p>
<p>注意MMAPv1不会使用Swap</p>
</td>
</tr>
<tr>
<td>WiredTiger</td>
<td>
<p>该引擎是多线程的，能够利用另外的CPU。特别是，活动线程数（并发操作数）与可用CPU的比值，可以影响性能：</p>
<ol>
<li>当比值小于1时，随着并发数增加，吞吐量增加</li>
<li>当比值大于1时，随着并发数增加，吞吐量减小</li>
</ol>
<p>mongostat输出的ar/aw列可以显示活动读/写数量</p>
<p>使用该引擎时，MongoDB同时利用WiredTiger内部缓存、OS缓存。</p>
<p>从3.4开始，WiredTiger缓存默认使用256MB、总RAM/2 - 1GB之中较大的内存量。可以通过<pre class="crayon-plain-tag">storage.wiredTiger.engineConfig.cacheSizeGB</pre>配置。在容器（例如Docker）中运行MongoDB时，应该设置此配置，使之小于容器可使用内存的量</p>
<p>要了解缓存的统计信息、清除率，运行<pre class="crayon-plain-tag">serverStatus</pre>命令并查看wiredTiger.cache字段</p>
<p>MongoDB会使用所有空闲内存作为OS缓存（文件系统缓存），OS缓存中的数据是被压缩的，存放MongoDB数据文件的映射</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">存储</span></div>
<p>使用SSD，MongoDB可以获得更好的性能、更高的性价比。 SSD的随机I/O能力可以很好的适应MMAPv1的更新模型。</p>
<p>注意为系统分配Swap，避免内存争用，或者因为OOM导致mongod被杀死。当使用MMAPv1时，其映射数据文件到内存的行为，导致永远不会使用Swap空间。</p>
<div class="blog_h2"><span class="graybg">数据库性能</span></div>
<p>当面向MongoDB性能低下时，其原因往往和数据库访问策略、硬件、并发操作数有关、错误/不当索引、低效的Schema设计相关。排除了前述可能性之后，数据库可能已经满载运行了，或许需要进行水平扩容。</p>
<p>你应该保证应用的工作集（Working set，最常使用的数据）能够全部装入内存，这样对性能提升有很大帮助，特别是使用MMAPv1引擎时。</p>
<p>某些情况下性能问题是临时的，通常由于突发的高并发导致。</p>
<div class="blog_h3"><span class="graybg">锁性能</span></div>
<p>为了确保数据一致性，MongoDB采用了一套锁系统。长时间运行、队列形式的操作可能导致性能降低，因为它们持有锁导致后续请求被迫等待。</p>
<p>要查看锁是否影响了系统性能，执行命令：<pre class="crayon-plain-tag">db.runCommand( { serverStatus: 1 } )</pre>并查看输出的locks、globalLock段。</p>
<p>locks.timeAcquiringMicros /  locks.acquireWaitCount的结果近似的反映了等待某种特定锁模式所消耗的平均时间。</p>
<p>locks.deadlockCount表示锁请求导致死锁的次数。</p>
<p>如果globalLock.currentQueue.total计数总是很高，意味着可能大量的操作在等待一个锁，提示存在影响性能的并发问题。</p>
<p>如果globalLock.totalTime / uptime的比值较高，意味着数据库很长时间处于一个锁状态。</p>
<p>耗时漫长的操作可能由于：</p>
<ol>
<li>低效的索引</li>
<li>低效的Schema设计</li>
<li>低效的查询语句</li>
<li>内存不足，导致页错误而产生磁盘读操作</li>
</ol>
<div class="blog_h3"><span class="graybg">MMAPv1和内存</span></div>
<p>MMAPv1引擎使用内存映射文件来存储数据。对于一个尺寸足够大的数据集，MMAPv1会分配尽可能多的系统内存供其使用。</p>
<p>判断内存是否对于数据集来说是足够的并不容易，查看 serverStatus的mem段可以查看MongoDB的内存使用情况。mem.resident记录MongoDB驻留工作集使用的内存，如果该值超过系统可用内存，并且还有很多数据没有映射到内存中，说明达到了系统的最大容量。</p>
<p>mem.mapped字段记录了MongoDB使用的内存映射文件大小，如果此值操作系统内存总大小，意味着某些读操作会触发页面错误而导致磁盘读。</p>
<p>serverStatus.extra_info.page_faults记录了MongoDB发生页面错误的数量。如果此计数快速增加，则可能是：</p>
<ol>
<li>系统内存过小</li>
<li>正在访问大量数据、扫描整个集合</li>
</ol>
<p>导致。偶尔发生的页面错误不要紧，但是大量累积的页面错误意味着存在I/O性能问题。</p>
<p>当遇到页面错误的时候，MongoDB可能放弃读锁，这样在换页期间其它数据库进场可以进行读写操作，这种行为提升了吞吐量、并发能力。</p>
<p>处理内存不足的方法是，水平或者垂直扩容。</p>
<div class="blog_h3"><span class="graybg">连接数</span></div>
<p>某些情况下，客户端和数据库之间的连接数会超过服务器的处理能力。serverStatus输出的以下字段可以提供相关信息：</p>
<ol>
<li>globalLock.activeClients，当前正在执行操作、或者排队等待操作的客户端数量</li>
<li>connections.current，连接到服务器的客户端数量</li>
<li>connections.available，空闲的可以供新客户端使用的连接数</li>
</ol>
<p>如果并发连接数持续的高，可能系统需要扩容。对于读过载的应用考虑复制集，对于写过载的应用考虑分片集群。</p>
<p>MongoDB本身没有对并发连接数的限制，操作系统本身的限制，例如UNIX，考虑设置ulimit。</p>
<div class="blog_h3"><span class="graybg">数据库剖析</span></div>
<p>MongoDB自带了一个剖析器，用于识别和分析低效的查询、操作。要为某个数据库启用剖析器，执行：</p>
<pre class="crayon-plain-tag"># 0 不启用剖析
# 1 仅仅剖析缓慢操作，设置项slowOpThresholdMs决定了缓慢查询的判断标准
# 2 剖析所有操作
db.setProfilingLevel(1)
# 输出示例：
{"was" : 0, "slowms" : 100, "ok" : 1 }

# 先的命令检查剖析设置
db.getProfilingStatus()</pre>
<p>要为整个mongod启用剖析器，使用选项：<pre class="crayon-plain-tag">mongod --profile 1 --slowms 15</pre>。</p>
<p>注意，剖析对数据库性能具有负面影响，因而默认是关闭的。你可以针对单个mongod实例、单个数据库进行设置，这些设置不会传播到整个复制集或者分片集群。不支持对整个分片集群设置剖析，你必须针对单个mongod进行设置。</p>
<p>剖析器收集MongoDB写操作、游标、数据库命令的细粒度信息。收集的信息被存放到定长集合 <pre class="crayon-plain-tag">system.profile</pre>中。执行<pre class="crayon-plain-tag">show profile</pre>或者：<pre class="crayon-plain-tag">db.system.profile.find( { millis : { $gt : 100 } } )</pre> 可以查看剖析器的输出。输出示例：</p>
<pre class="crayon-plain-tag">{
   // 操作类型
   "op" : "query",
   // 操作针对的对象
   "ns" : "test.c",
   // 使用的查询文档，对于insert操作来说，则是插入的文档
   "query" : {
      "find" : "c",
      "filter" : {
         "a" : 1
      }
   },
   // 执行update操作时，使用的更新文档
   "updateobj" :..,
   // query、getmore操作使用的游标ID
   "cursorid" :...,
   // 执行的命令
   "command" : ..,
   // 别名system.profile.nscanned，为了完成操作MongoDB扫描的索引值数量
   "keysExamined" : 2,
   // 别名system.profile.nscannedObjects，为了完成操作MongoDB扫描的文档数量
   "docsExamined" : 2,
   // 仅MMAPv1引擎，为了完成操作，在磁盘上移动文档的数量
   "nmoved" :...,
   // 操作删除的文档数量
   "ndeleted" :...,
   // 操作插入的文档数量
   "ninserted" :...,
   // update操作修改文档的数量
   "nModified" :...,
   // 是否为upsert操作
   "upsert" :...,
   // 如果通过索引不能得到需要的排序，则出现此Stage
   "hasSortStage" : ...,
   "cursorExhausted" : true,
   // 写操作插入的索引键数量
   "keysInserted" : 0,
   // 删除的索引键数量
   "keysDeleted" : 0,
   // 写操作冲突文档数量，所谓冲突即多个update操作尝试修改同一文档的情况
   "writeConflicts" : 0,
   // 让出锁以便其它操作能够进行的次数，这意味着当前操作需要读取不在内存中的数据
   "numYield" : 0,
   // 原来该字段叫lockStats
   "locks" : {
      // 锁类型：{剖析信息}
      // Global 全局锁
      // MMAPV1Journal 该引擎特定的，用于同步日志写操作的锁
      // Database 数据库锁
      // Collection 集合锁
      // Metadata 元数据锁
      // oplog Oplog锁
      "Global" : {
         // 请求锁的次数
         "acquireCount" : {
            // 锁模式：
            // R 共享锁，W 独占锁，r 意向共享锁，w 意向独占锁
            "r" : NumberLong(2)
         }
         "acquireWaitCount" :...,      // 等待获得锁的次数
         "timeAcquiringMicros" :...,   // 累积等待锁消耗的微秒数
         "deadlockCount" :...,         // 等待锁时遭遇死锁的次数
      }
   },
   // 操作返回文档的数量
   "nreturned" : 2,
   // 返回文档的字节长度
   "responseLength" : 108,
   // 从mongod角度来看，操作完整消耗的时间
   "millis" : 0,
   // 执行统计信息，参考执行计划
   "execStats" : {},
   // 操作的时间戳
   "ts" : ISODate("2015-09-03T15:26:14.948Z"),
   // 发起操作的客户端地址
   "client" : "127.0.0.1",
   // 发起操作的应用名称，驱动支持设置应用名称
   "appName" : "MongoDB Shell",
   // 当前会话认证的所有用户的数组
   "allUsers" : [ ],
   // 执行此操作的用户
   "user" : ""
}</pre>
<p>需要修改剖析日志的容量时，参考以下步骤：</p>
<pre class="crayon-plain-tag">db.setProfilingLevel(0)
db.system.profile.drop()
db.createCollection( "system.profile", { capped: true, size:4000000 } )
db.setProfilingLevel(1)

# 如果要修改从节点的剖析日志容量，首先需要以standalone模式启动之，然后执行上述修改</pre>
<div class="blog_h3"><span class="graybg">禁用透明巨页</span></div>
<p>透明巨页（Transparent Huge Pages）是一种Linux的内存管理系统，用于减少转译后备缓冲区（Translation Lookaside Buffer ，TLB）的查询成本。由于THP倾向于导致稀疏而非连续的内存访问模式，因而不适合数据库工作负载。</p>
<p>要禁用透明巨页特性，参考：</p>
<pre class="crayon-plain-tag"># Ubuntu
sudo update-rc.d disable-transparent-hugepages defaults
# CentOS
sudo chkconfig --add disable-transparent-hugepages</pre>
<div class="blog_h3"><span class="graybg">ulimit设置</span></div>
<p> 大部分类UNIX系统提供了按用户/进程来控制文件、线程、网络连接等系统资源使用数量的手段 —— ulimits。</p>
<p>ulimit的默认值对于MongoDB可能太低了，需要调整。</p>
<div class="blog_h2"><span class="graybg">备份</span></div>
<div class="blog_h3"><span class="graybg">基于文件系统快照</span></div>
<p>可以基于Linux下的LVM进行文件系统级的备份/恢复。创建快照的命令示意：</p>
<pre class="crayon-plain-tag"># 针对卷组vg0中的mongodb卷创建一个名为mdb-snap01的快照
# 快照容量100M，存放此快照与文件系统最终状态的diff
lvcreate --size 100M --snapshot --name mdb-snap01 /dev/vg0/mongodb

# 归档压缩
umount /dev/vg0/mdb-snap01
dd if=/dev/vg0/mdb-snap01 | gzip &gt; mdb-snap01.gz</pre>
<p>要基于快照恢复，可以执行：</p>
<pre class="crayon-plain-tag"># 在卷组vg0中创建一个名为mdb-new的逻辑卷，大小1G（根据实际MongoDB占用磁盘空间确定）
lvcreate --size 1G --name mdb-new vg0
# 把之前的快照导入到逻辑卷
gzip -d -c mdb-snap01.gz | dd of=/dev/vg0/mdb-new
# 挂载逻辑卷，挂载点直线
mount /dev/vg0/mdb-new /srv/mongodb</pre>
<div class="blog_h3"><span class="graybg">基于MongoDB工具</span></div>
<p>命令mongodump/mongorestore实现基于BSON格式的备份与恢复，适合小型数据库。要实现弹性、无缝的备份，建议使用文件系统、块设备级别的快照功能进行备份。</p>
<p>mongodump/mongorestore需要和运行中的mongod进行交互，因而会影响数据库性能。除了网络流量方面的开销外，这两个工具还需要通过内存来读取数据。MongoDB会因此载入很少使用的数据并把常用数据清除出内存。</p>
<p>备份命令示例：</p>
<pre class="crayon-plain-tag">mongodump --host 172.21.2.1 --port 27017   --out /data/backup/
          # 可以限制导出的数据库，甚至集合
          --db cluster --collection corps
          # 如果服务器启用了身份验证，可以指定密码
          --username user --password "passwd" 
          # 在dump期间收集oplog，形成指向特定时间点的备份
          --oplog</pre>
<p>恢复命令示例：</p>
<pre class="crayon-plain-tag">mongorestore --host 172.21.2.1 --port 27017 
             # 重做oplog
             --oplogReplay
             # 备份所在位置
             /data/backup/</pre>
<div class="blog_h3"><span class="graybg">宕机后修复</span></div>
<p>如果单机mongod禁用了日志，则意外宕机可能导致数据处于不一致状态，启动时报错： Detected unclean shutdown - mongod.lock is not empty。在数据文件目录下会存在非空的 mongod.lock 文件。这种情况下，你需要修复数据库：</p>
<ol>
<li>备份数据文件目录</li>
<li>以修复模式启动数据库：<pre class="crayon-plain-tag">mongod --dbpath /data/db --repair</pre> </li>
</ol>
<div class="blog_h2"><span class="graybg">管理复制集</span></div>
<div class="blog_h3"><span class="graybg">配置管理</span></div>
<p>要读取复制集的配置对象，调用<pre class="crayon-plain-tag">rs.conf()</pre>方法或者在admin数据库上调用：</p>
<pre class="crayon-plain-tag">db.runCommand( { replSetGetConfig: 1 } )</pre>
<p>要修改复制集配置，调用<pre class="crayon-plain-tag">rs.reconfig()</pre>并传入一个配置文档：</p>
<pre class="crayon-plain-tag">{
  // 复制集的名称，一旦设置，不可更改。必须和配置replication.replSetName或者命令行参数--replSet一致
  _id: &lt;string&gt;,
  // 递增的配置版本号，用于区分复制集配置的修订版
  version: &lt;int&gt;,
  // 选举协议的版本，从3.2开始默认1
  protocolVersion: &lt;number&gt;,
  // 3.4新增，protocolVersion为1默认true，否则默认false。指示{ w: "majority" }是否隐含{ j: true }
  writeConcernMajorityJournalDefault: &lt;boolean&gt;,
  // 指示当前复制集是否用作分片集群的配置服务器
  configsvr: &lt;boolean&gt;,
  // 成员配置文档的数组
  members: [
    {
      // 成员标识符，0-255，一旦设置不得修改
      _id: &lt;int&gt;,
      // 成员主机名，或者host:port，主机名必须可以从任何复制集成员进行DNS解析
      host: &lt;string&gt;,
      // 布尔，用于指示该成员是否为仲裁者
      arbiterOnly: &lt;boolean&gt;,
      // 默认true，mongod是否在此成员上构建索引。仅在添加复制集成员时可以设置，不得改变
      // 在下列条件全部满足时，设置为false可能有用：
      // 1、仅仅使用该成员执行mongodump
      // 2、该成员从不接受查询
      // 3、维护索引的成本让硬件受不了
      // 即使设置为false，也会构建_id索引
      // 设置为false时必须同时设置priority为0
      // 其它成员不能从buildIndexes = false的成员进行复制
      buildIndexes: &lt;boolean&gt;,
      // 如果设置为true，db.isMaster()的输出不包含此成员，阻止读操作转发给该成员
      hidden: &lt;boolean&gt;,
      // 默认1.0，0-100之间，优先级权重，值越高，越有资格被选举为主节点
      priority: &lt;number&gt;,
      // 标签集文档，可以包含任意键值对映射。用于定制读写关注，使其感知数据中心
      tags: &lt;document&gt;,
      // 单位秒，默认0，用于配置延迟从节点
      slaveDelay: &lt;int&gt;,
      // 投票权，默认1，可选0。仲裁者总是1
      votes: &lt;number&gt;
    },
    ...
  ],
  settings: {
    // 默认true，如果true，则允许从节点从其它从节点复制，而不仅仅时主节点
    chainingAllowed : &lt;boolean&gt;,
    // 内部使用，复制集成员需要不断的相互发送心跳，确认可达性
    heartbeatIntervalMillis : &lt;int&gt;,
    // 心跳超时时间，默认10s
    heartbeatTimeoutSecs: &lt;int&gt;,
    // 默认10000ms，用于检测主节点不可用的延迟时间。高取值延长故障转移时间但是降低对不稳定网络的敏感性
    electionTimeoutMillis : &lt;int&gt;,
    // 新选举的主节点，其数据可能不是最新的，此时需要从最新的从节点同步
    // 高取值避免从节点回滚数据的可能，但是延长了故障转移时间
    // 同步完成之前，主节点不接受写请求
    catchUpTimeoutMillis : &lt;int&gt;,
    // 用于定义扩展的写关注值，可以提供数据中心感知，例如：
    // { getLastErrorModes: { eastCoast: { "east": 1 } } } 允许使用写关注：
    // { w: "eastCoast" }，表示要求写操作传播到具有east:1标签的节点上
    getLastErrorModes : &lt;document&gt;,
    // 用于指定默认的写关注
    getLastErrorDefaults : &lt;document&gt;,
    // 此复制集的内部唯一标识，自动创建不可更改
    replicaSetId: &lt;ObjectId&gt;
  }
}</pre>
<div class="blog_h3"><span class="graybg">查看复制集状态</span></div>
<p>调用<pre class="crayon-plain-tag">rs.status()</pre>或者在admin数据库执行replSetGetStatus命令，查看当前复制集的状态：</p>
<pre class="crayon-plain-tag">{
    // 复制集名称
    "set" : "replset",
    // 当前时间
    "date" : ISODate("2016-11-02T20:02:16.543Z"),
    // 当前节点的复制状态
    "myState" : 1,
    // 选举发生的次数（本节点知晓的），使用协议版本0时总是返回-1
    "term" : NumberLong(1),
    // 心跳频率
    "heartbeatIntervalMillis" : NumberLong(2000),
    // 用于了解复制信息细节的时间信息
    "optimes" : {
          // 从当前节点角度看，最近一次已经传播到大部分复制集成员的写操作的发生时间
          "lastCommittedOpTime" : {
             "ts" : Timestamp(1478116934, 1),  // 操作发生的时间
             "t" : NumberLong(1)               // 操作发生在那次term
          },
          // 从当前节点角度看，最近一次能满足majority读关注的操作的发生时间
          "readConcernMajorityOpTime" : {
             "ts" : Timestamp(1478116934, 1),
             "t" : NumberLong(1)
          },
          // 从当前节点角度看，最近一次应用到当前节点的操作的发生时间
          "appliedOpTime" : {
             "ts" : Timestamp(1478116934, 1),
             "t" : NumberLong(1)
          },
          // 从当前节点角度看，最近一次应用到当前节点、且已经写入日志的操作的发生时间
          "durableOpTime" : {
             "ts" : Timestamp(1478116934, 1),
             "t" : NumberLong(1)
          }
       },
    // 复制集成员状态
    "members" : [
        {
            "_id" : 0,
            "name" : "m1.example.net:27017",
            // 仅当前节点出现：用于指示当前节点
            "self" : true,
            // 仅非当前节点出现：是否宕机
            "health" : 1,
            // 复制集成员状态
            "state" : 1,
            "stateStr" : "PRIMARY",
            // 此成员已经在线的时间
            "uptime" : 269,
            // 该成员最后一次从oplog应用写操作的时间
            "optime" : {
                        "ts" : Timestamp(1478116934, 1),
                        "t" : NumberLong(1)
            },
            "optimeDate" : ISODate("2016-11-02T20:02:14Z"),
            "infoMessage" : "could not find member to sync from",
            // 选举发生的时间
            "electionTime" : Timestamp(1478116933, 1),
            "electionDate" : ISODate("2016-11-02T20:02:13Z"),
            // 该成员最后一次从oplog应用写操作、且已经写入日志的时间
            "optimeDurable" : {
               "ts" : Timestamp(1478116934, 1),
               "t" : NumberLong(1)
            },
            "optimeDurableDate" : ISODate("2016-11-02T20:02:14Z"),
            // 最后一次该成员发送心跳的时间
            "lastHeartbeat" : ISODate("2016-11-02T20:02:15.619Z"),
            // 最后一次从该成员收到心跳的时间，与上一字段的差值体现了两节点之间的网络延迟
            "lastHeartbeatRecv" : ISODate("2016-11-02T20:02:14.787Z"),
            // PING该节点消耗的时间
            "pingMs" : NumberLong(0),
            // 该节点的复制源
            "syncingTo" : "m1.example.net:27018",
            // 该节点使用的复制集配置版本号
            "configVersion" : 1
        }
    ],
    "ok" : 1
}</pre>
<p>成员状态的含义参考<a href="#rs-member-state">复制集成员状态</a>。</p>
<div class="blog_h3"><span class="graybg">新建复制集</span></div>
<p>在生产环境下部署复制集，应当保证以下前置条件：</p>
<ol>
<li>仅可能把mongod部署在不同的物理机器上。如果使用虚拟机，应当使虚拟机对应不同的物理机器。应当确保物理机器之间有冗余电路、冗余网络路径</li>
<li>保证所有节点可以相互通信</li>
</ol>
<p>配置文件添加复制集配置：</p>
<pre class="crayon-plain-tag">replication:
  replSetName: rs0</pre>
<p>创建三成员复制集：</p>
<pre class="crayon-plain-tag"># 创建三个mongod实例
docker run --name mongo-00 --network local --ip 172.21.0.100 -d  docker.gmem.cc/mongo --config /etc/mongod.conf
docker run --name mongo-01 --network local --ip 172.21.0.101 -d  docker.gmem.cc/mongo --config /etc/mongod.conf
docker run --name mongo-02 --network local --ip 172.21.0.102 -d  docker.gmem.cc/mongo --config /etc/mongod.conf</pre>
<p>启动所有实例，然后在其中一个实例上，打开MongoDB Shell，执行：</p>
<pre class="crayon-plain-tag">rs.initiate( {
   _id : "rs0",
   members: [ { _id : 0, host : "172.21.0.100:27017" } ]
})</pre>
<p>现在可以执行 <pre class="crayon-plain-tag">rs.conf()</pre> 查看复制集的配置信息，可以看到当前复制集有一个成员。接着把另外两个成员加入到复制集中：</p>
<pre class="crayon-plain-tag">rs.add("172.21.0.101:27017")
rs.add("172.21.0.102:27017")</pre>
<p>现在调用<pre class="crayon-plain-tag">rs.status()</pre>可以看到复制集及其三个成员的状态。 </p>
<div class="blog_h3"><span class="graybg">添加成员</span></div>
<p>注意以下几点：</p>
<ol>
<li>每个复制集最多7个投票成员，如果已经有7个成员，再添加成员必须设置votes=0，或者移除既有成员的投票权</li>
<li>可以添加先前被移除的成员，如果该成员的数据没有被删除且足够新，它有可能跟得上oplog的节奏而不需要完全同步</li>
<li>如果你拥有既有成员的足够新的数据备份，拷贝备份到新成员对应dbPath目录下，可以快速添加新成员</li>
</ol>
<div class="blog_h3"><span class="graybg">添加仲裁者</span></div>
<p>以类似创建上面三成员的方式，再创建一个成员，启动后，在既有成员节点上执行：</p>
<pre class="crayon-plain-tag">rs.addArb("172.21.0.103:27017")</pre>
<div class="blog_h3"><span class="graybg">移除成员</span></div>
<p>可以执行命令：</p>
<pre class="crayon-plain-tag">rs.remove("172.21.0.102:27017")</pre>
<p>或者：</p>
<pre class="crayon-plain-tag">cfg = rs.conf()
cfg.members.splice(2,1)
rs.reconfig(cfg)</pre>
<div class="blog_h3"><span class="graybg">替换成员</span> </div>
<pre class="crayon-plain-tag">cfg = rs.conf()
cfg.members[0].host = "172.21.0.105"
rs.reconfig(cfg) </pre>
<div class="blog_h3"><span class="graybg">成员管理</span></div>
<pre class="crayon-plain-tag">cfg = rs.conf()

# 设置优先级，范围0-1000，非投票节点的优先级必须是0
cfg.members[0].priority = 0.5
# 设置优先级为0可以阻止它成为主节点
cfg.members[1].priority = 0

# 隐藏成员对客户端不可见， isMaster的输出中不包含
cfg.members[0].hidden = true

# 配置复制延迟
cfg.members[0].slaveDelay = 3600

# 禁用投票权
cfg.members[4].votes = 0

# 设置标签集
cfg.members[1].tags = { "dc": "east", "use": "reporting" }

# 必须重新配置才能生效
# 注意，下面的方法会导致主节点step down然后触发选举，step down期间MongoDB会关闭所有客户端连接
# 这可能耗时10-20秒
rs.reconfig(cfg)</pre>
<div class="blog_h3"><span class="graybg">开发测试环境</span></div>
<p>这类环境下，你可能没有足够多的机器可用，只需要在命令行指定合适的端口、数据库路径、参与的复制集名称，就可以在一台机器上部署属于多个复制集的mongod：</p>
<pre class="crayon-plain-tag">mongod --port 27010 --dbpath /data/rs0-0 --replSet rs0 --smallfiles --oplogSize 128
mongod --port 27011 --dbpath /data/rs0-1 --replSet rs0 --smallfiles --oplogSize 128
mongod --port 27012 --dbpath /data/rs0-2 --replSet rs0 --smallfiles --oplogSize 128</pre>
<p>通过MongoDB Shell连接时，指定端口即可连接到不同实例：</p>
<pre class="crayon-plain-tag">mongo --port 27010</pre>
<div class="blog_h3"><span class="graybg">异地部署环境</span></div>
<p>注意以下几点：</p>
<ol>
<li>网络安全性：保证成员之间、客户端与复制集之间流量的安全性。手段包括VPN、防火墙配置、启用MongoDB认证和授权机制</li>
<li>使用标签集实现数据中心感知 </li>
<li>最好能保证某个数据中心完全不可用时，复制集仍然能够工作</li>
<li>不要超过7个投票成员</li>
</ol>
<div class="blog_h3"><span class="graybg">成员维护流程</span></div>
<p>要对复制集成员进行维护，一般性流程如下：</p>
<ol>
<li>首先对各从节点执行维护，最后对主节点进行维护</li>
<li>对于每一个节点，执行：
<ol>
<li>以独立模式重启mongod，对于主节点要stepDown</li>
<li>在独立模式运行的mongod下执行维护任务</li>
<li>以复制集成员方式重新启动mongod </li>
</ol>
</li>
</ol>
<div class="blog_h3"><span class="graybg">修改Oplog尺寸</span></div>
<p>oplog本质上是一个定长集合，默认的尺寸通常够用。可能需要增大oplog的场景例如：执行影响大量数据的update操作。</p>
<p>如果要修改复制集的oplog，你必须轮流对每个成员进行操作：</p>
<ol>
<li>将被操作成员切换到独立运行模式：
<ol>
<li>如果该成员是主节点，一定要通过<pre class="crayon-plain-tag">rs.stepDown()</pre>将其优雅的变为从节点</li>
<li>调用<pre class="crayon-plain-tag">db.shutdownServer()</pre>关闭从节点</li>
<li>不带--replSet参数、以不同端口重新启动该成员</li>
</ol>
</li>
<li> 可选的，创建oplog的备份：<pre class="crayon-plain-tag">mongodump --db local --collection 'oplog.rs' --port 37017</pre> </li>
<li>以新尺寸创建oplog集合：<br />
<pre class="crayon-plain-tag"># 或者 db = db.getSiblingDB('local')
use local
# 创建一个临时集合，存放oplog集合的内容
db.temp.drop()
# 备份oplog，以自然反序排列（最新条目排在最前）
db.temp.save( db.oplog.rs.find( { }, { ts: 1, h: 1 } ).sort( {$natural : -1} ).limit(1).next() )

# 删除oplog
db.oplog.rs.drop()

# 重建oplog
db.runCommand( { create: "oplog.rs", capped: true, size: (2 * 1024 * 1024 * 1024) } )

# 导入临时集合中最后（最新）的oplog条目
db.oplog.rs.save( db.temp.findOne() ) </pre>
</li>
<li>以复制集成员的方式重新启动该节点</li>
</ol>
<div class="blog_h3"><span class="graybg">强制主节点</span></div>
<p>要强制某个从节点成为主节点，你可以将其优先级调为最高；类似的，优先级设置为0则可以让从节点永远不能成为主节点。</p>
<p>通过设置高优先级来强制主节点： </p>
<pre class="crayon-plain-tag"># 假设复制集成员 0 1 2 目前0是主节点，现在希望强制2为主节点
# 执行：
cfg = rs.conf()
cfg.members[0].priority = 0.5
cfg.members[1].priority = 0.5
cfg.members[2].priority = 1
rs.reconfig(cfg)
# 上面的语句调用后，会发生以下时间序列：
# 1、成员1/2和0进行同步（通常10秒内完成）
# 2、主节点发现自己的优先级不是最高，通常会stepDown
#    如果2的同步进度远远落后，则不会stepDown，等待2的optime差距在10s以后再stepDown
# 3、选举发生，2当选</pre>
<div class="blog_h3"><span class="graybg">重新同步</span></div>
<p>如果某个从节点复制进度远远落后，则oplog中尚未被该从节点应用到本地的条目可能已经被覆盖（定长集合循环覆盖），则该从节点变得Stale，必须进行完整的重新同步 —— 移除数据，重新执行初始同步。</p>
<p>执行重新同步时，应该选择一个带宽比较空闲的时机。</p>
<p>重新同步有两种方式：</p>
<ol>
<li>将Stale节点的数据目录清空，启动后会自动执行initial sync</li>
<li>从其它复制集成员拷贝数据目录，启动后可以增量同步。注意要一并拷贝local数据库的内容 </li>
</ol>
<div class="blog_h3"><span class="graybg">链式复制</span></div>
<p>启用链式复制的情况下，从节点可以从其它从节点复制，不一定非要主节点。</p>
<p>启用或者禁用链式复制：</p>
<pre class="crayon-plain-tag">cfg = rs.config()
cfg.settings.chainingAllowed = true | false
rs.reconfig(cfg)</pre>
<p>要修改某个从节点的复制源，可以：</p>
<pre class="crayon-plain-tag">rs.syncFrom("hostname&lt;:port&gt;");</pre>
<div class="blog_h3"><span class="graybg">复制集排错</span></div>
<table class="full-width fixed-word-wrap">
<tbody>
<tr>
<td>
<p><strong><em>查看状态</em></strong></p>
<p>调用<pre class="crayon-plain-tag">rs.status()</pre>可以查看复制集、复制集成员的当前状态</p>
</td>
</tr>
<tr>
<td>
<p><strong><em>检查复制日志</em></strong></p>
<p>复制延迟是指从节点应用来自主节点的oplog的落后时间，复制延迟是复制集部署中较为严重的问题：</p>
<ol>
<li>过慢的节点难以快速的切换为主节点，为了保证数据不丢失，新主节点必须复制全部oplog（除非老主节点宕机 + 不适当的写关注，导致oplog没有传播到任何从节点）</li>
<li>数据不一致性的可能性增加 </li>
</ol>
<p>要查看复制延迟，执行<pre class="crayon-plain-tag">rs.printSlaveReplicationInfo()</pre>，输出如下：</p>
<pre class="crayon-plain-tag"># 从节点地址端口
source: 172.21.0.100:27017
	syncedTo: Thu Aug 03 2015 07:51:58 GMT+0000 (UTC)
        # 延迟时间
	0 secs (0 hrs) behind the primary 
source: 172.21.0.101:27017
	syncedTo: Thu Aug 03 2015 07:51:58 GMT+0000 (UTC)
	0 secs (0 hrs) behind the primary 

# 注意：如果主节点长期不活动，延迟从节点的延迟时间也可能小至0</pre>
<p>复制延迟的原因可能是：</p>
<ol>
<li>网络延迟，使用ping/traceroute等工具检查网络</li>
<li>磁盘吞吐量，从节点的磁盘性能可能太差，使用iostat/vmstat等工具检查</li>
<li>高并发，某些情况下，主节点上运行的长时操作可能阻塞从节点的复制，应当要求写操作得到从节点的确认，避免主节点太快而从节点跟不上</li>
<li>不适当的写关注，例如第三点的情况</li>
</ol>
</td>
</tr>
<tr>
<td>
<p><strong><em>无法选举</em></strong></p>
<p>同时重启多个从节点时，要确保可投票节点的大部分在线，否则主节点会stepDown并变为从节点，客户端会被断开连接</p>
</td>
</tr>
<tr>
<td>
<p><strong><em>查看oplog信息</em></strong></p>
<p>调用<pre class="crayon-plain-tag">rs.printReplicationInfo()</pre>可以查看oplog的尺寸和条目信息，输出如下：</p>
<pre class="crayon-plain-tag"># 配置的oplog尺寸，必须足够大，能够保证宕机时间最长的从节点启动后仍然能够跟得上
configured oplog size:   2988.1271476745605MB
log length start to end: 81850secs (22.74hrs)
oplog first event time:  Wed Aug 02 2015 09:15:58 GMT+0000 (UTC)
oplog last event time:   Thu Aug 03 2015 08:00:08 GMT+0000 (UTC)
now:                     Thu Aug 03 2015 08:00:13 GMT+0000 (UTC)</pre>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">管理分片集群</span></div>
<div class="blog_h3"><span class="graybg">部署分片集群</span></div>
<p>首先，<em><strong>①部署配置服务复制集</strong></em>，生产环境下应该部署最少3个成员。
<p>配置文件添加分片集群角色配置：</p>
<pre class="crayon-plain-tag">replication:
  replSetName: rsc

sharding:
  clusterRole: configsvr</pre>
<p>创建复制集的三成员：</p>
<pre class="crayon-plain-tag">docker run --name mongo-c1 --network local --ip 172.21.1.1 -d  docker.gmem.cc/mongo --config /etc/mongod.conf
docker run --name mongo-c2 --network local --ip 172.21.1.2 -d  docker.gmem.cc/mongo --config /etc/mongod.conf
docker run --name mongo-c3 --network local --ip 172.21.1.3 -d  docker.gmem.cc/mongo --config /etc/mongod.conf</pre>
<p>连接到其中一个成员，进行复制集的初始化：</p>
<pre class="crayon-plain-tag">rs.initiate(
  {
    _id: "rsc",
    configsvr: true,
    members: [
      { _id : 0, host : "172.21.1.1:27017" },
      { _id : 1, host : "172.21.1.2:27017" },
      { _id : 2, host : "172.21.1.3:27017" }
    ]
  }
)</pre>
<p>一旦配置服务复制集（CSRS）创建完毕，即可<strong><em>②创建分片复制集</em></strong>，生产环境下，每个分片复制集因该部署最少3个成员。</p>
<p>第一个分片复制集的配置：</p>
<pre class="crayon-plain-tag">replication:
  replSetName: rs0
  
sharding:
  clusterRole: shardsvr</pre>
<p>创建复制集的三成员：</p>
<pre class="crayon-plain-tag">docker run --name mongo-51 --network local --ip 172.21.5.1 -d  docker.gmem.cc/mongo --config /etc/mongod.conf
docker run --name mongo-52 --network local --ip 172.21.5.2 -d  docker.gmem.cc/mongo --config /etc/mongod.conf
docker run --name mongo-53 --network local --ip 172.21.5.3 -d  docker.gmem.cc/mongo --config /etc/mongod.conf</pre>
<p>连接到其中一个成员，进行复制集的初始化：</p>
<pre class="crayon-plain-tag">rs.initiate(
  {
    _id : "rs5",
    members: [
      { _id : 0, host : "172.21.5.1:27017" },
      { _id : 1, host : "172.21.5.2:27017" },
      { _id : 2, host : "172.21.5.3:27017" }
    ]
  }
)</pre>
<p>参考以上方法，创建更多的分片复制集。 </p>
<p><em><strong>③创建一个路由器（mongos）</strong></em>并连接到配置复制集，使用如下配置文件：</p>
<pre class="crayon-plain-tag">net:
  port: 27017
  bindIp: 0.0.0.0

sharding:
  configDB: rsc/172.21.1.1:27017,172.21.1.2:27017,172.21.1.3:27017
  # 至少需要指定一个配置复制集成员的地址</pre>
<p>创建mongos：</p>
<pre class="crayon-plain-tag">docker run --name mongo-s1 --network local --ip 172.21.99.1 -d  docker.gmem.cc/mongo mongos --config /etc/mongos.conf</pre>
<p><em><strong>④连接到mongos</strong></em>：<pre class="crayon-plain-tag">mongo --host 172.21.99.1 </pre> </p>
<p><em><strong>⑤将分片复制集添加到集群</strong></em>中： </p>
<pre class="crayon-plain-tag"># 添加一个复制集
sh.addShard( "rs2/172.21.2.1:27017")
# 你也可以添加独立MongoDB实例
sh.addShard( "172.21.0.1:27017")</pre>
<p><em><strong>⑥为数据库启用分片支持</strong></em>，只有这样，该数据库中才可以有分片集合： </p>
<pre class="crayon-plain-tag">sh.enableSharding("cluster")</pre>
<p>现在，你可以查看分片集群的状态，在mongos上执行<pre class="crayon-plain-tag">sh.status()</pre>，输出如下：</p>
<pre class="crayon-plain-tag">--- Sharding Status --- 
  # 这一段显示配置数据库的基本信息
  sharding version: {
        # 配置元数据的唯一标识
	"_id" : 1,
        # 配置服务器最小的兼容版本号
	"minCompatibleVersion" : 5,
        # 当前配置元数据的版本
	"currentVersion" : 6,
        # 分片集群的唯一标识
	"clusterId" : ObjectId("5982e147eb7a3c5622dd9bc3")
}
  # 此集群中包含的分片
  shards:
        # _id 分片的唯一标识
        # host 分片的主机，如果复制集，显示所有成员的地址
        # state 分片的状态
        # tags，分片的标签集
	{  "_id" : "rs2",  "host" : "rs2/172.21.2.1:27017,172.21.2.2:27017,172.21.2.3:27017",  "state" : 1 }
	{  "_id" : "rs3",  "host" : "rs3/172.21.3.1:27017,172.21.3.2:27017,172.21.3.3:27017",  "state" : 1 }
	{  "_id" : "rs4",  "host" : "rs4/172.21.4.1:27017,172.21.4.2:27017,172.21.4.3:27017",  "state" : 1 }
	{  "_id" : "rs5",  "host" : "rs5/172.21.5.1:27017,172.21.5.2:27017,172.21.5.3:27017",  "state" : 1 }
  # 活动的mongos的版本和数量
  active mongoses:
        # 版本:数量
        "3.4.5" : 1
  # 是否启用了Chunk自动分裂
  autosplit:
        Currently enabled: yes
  # 负载均衡器的状态
  balancer:
        # 目前集群是否启用了负载均衡器
        Currently enabled: yes
        # 负载均衡器是否正在工作（迁移Chunk）
        Currently running: no
              Balancer lock taken at Thu Aug 03 2017 08:39:36 GMT+0000 (UTC) by ConfigServer:Balancer
        # 最近五次负载均衡尝试，失败的次数，如果Chunk迁移失败，则负载均衡失败
        Failed balancer rounds in last 5 attempts: 0
        # 最近24小时迁移的数量
        Migration Results for the last 24 hours: 
              No recent migrations
  # 此集群中包含的数据库
  databases:
        # _id 数据库的名称
        # primary 主分片所在
        # partitioned 该数据库是否支持分片
	{  "_id" : "cluster",  "primary" : "rs2",  "partitioned" : true }</pre>
<p>创建范围分片的集合示例：</p>
<pre class="crayon-plain-tag">sh.shardCollection("cluster.corps", { regNo : 1 }, true )</pre>
<p>再次查看分片集群状态，可以看到databases段有新增内容：</p>
<pre class="crayon-plain-tag"># 集合的名称
cluster.corps
    # 分片键: { &lt;shard key&gt; : &lt;1 or hashed&gt; }
    shard key: { "regNo" : 1 }
    # 是否对分片键应用了唯一性约束
    unique: true
    # 是否对该集合启用了负载均衡
    balancing: true
    # 块的详细信息
    chunks:
        # 下面是一个列表，以Shard名称:拥有块的数量显示
        rs2  1
    # 分片键范围对应的Shard，以及最后修改的时间
    # { &lt;shard key&gt;: &lt;min range1&gt; } --&gt;&gt; { &lt;shard key&gt; : &lt;max range1&gt; } on : &lt;shard name&gt; &lt;last modified timestamp&gt;
    { "regNo" : { "$minKey" : 1 } } --&gt;&gt; { "regNo" : { "$maxKey" : 1 } } on : rs2 Timestamp(1, 0)</pre>
<div class="blog_h3"><span class="graybg">管理配置服务器</span></div>
<p>如果配置管理服务器复制集变为只读状态（无主节点）则分片集群不支持对元数据的写操作，因而Chunk分裂、迁移无法执行。这种情况下，你应该尽快修复或者替换损坏的配置管理复制集成员。替换成员的步骤：</p>
<ol>
<li>以选项--configsvr --replSet启动新成员</li>
<li>在成员节点上，将新成员添加到复制集rs.add()</li>
<li>关闭被替换的成员</li>
<li>在成员节点上，移除被替换成员rs.remove()</li>
<li>可选的，更新mongos的--configdb选项</li>
</ol>
<div class="blog_h3"><span class="graybg">查看集群配置</span></div>
<table class="full-width fixed-word-wrap">
<tbody>
<tr>
<td>
<p><strong><em>列出支持分片的数据库</em></strong></p>
<pre class="crayon-plain-tag">use config
db.databases.find( { "partitioned": true } )

# 输出如下：
{ "_id" : "cluster", "primary" : "rs2", "partitioned" : true }</pre>
</td>
</tr>
<tr>
<td>
<p><strong><em>列出所有分片</em></strong>
<pre class="crayon-plain-tag">use admin
db.runCommand( { listShards : 1 } )
# 输出如下
{
	"shards" : [
		{
			"_id" : "rs2",
			"host" : "rs2/172.21.2.1:27017,172.21.2.2:27017,172.21.2.3:27017",
			"state" : 1
		}
                ...
	],
	"ok" : 1
} </pre>
</td>
</tr>
<tr>
<td>
<p><strong><em>查看集群详细信息</em></strong>
<p>调用db.printShardingStatus()或者sh.status() </p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">硬件迁移</span></div>
<p>如果要把整个分片集群迁移到新的硬件上，可以参考以下步骤：</p>
<ol>
<li>禁用负载均衡器<pre class="crayon-plain-tag">sh.stopBalancer()</pre>，这样可以防止Chunk迁移、元数据写。如果当前正在Chunk迁移，负载均衡器会等待其终止</li>
<li>单独的迁移各个配置服务器。从3.4开始，配置服务器应以复制集的方式部署，使用 WiredTiger引擎。另外注意，此复制集必须：没有仲裁者、没有延迟成员、启用buildIndexes。具体迁移步骤：
<ol>
<li>在新硬件上开启一个配置服务实例（使用适当的选项启动mongod，例如--configsvr --replSet rsc）</li>
<li>添加到配置服务器复制集中</li>
<li>移除被替换的旧硬件成员，主节点要先StepDown</li>
<li>从复制集中移除被替换成员</li>
<li>循环上面4步，直到所有成员都迁移</li>
</ol>
</li>
<li>重新启动mongos，指向新的配置服务器</li>
<li>进行分片的迁移，一个个分片的迁移，先迁移从节点，最后主节点。具体迁移步骤：
<ol>
<li>调用shutdown命令，关闭一个成员，主节点要先StepDown</li>
<li>移动数据目录（dbPath）到新硬件</li>
<li>在新硬件上启动mongod，并连接到当前主节点</li>
<li>如果主机名/IP变化了，要执行rs.reconfig()来重新配置复制集</li>
<li>等待此节点恢复正常，调用rs.status()查看节点状态</li>
<li>循环上面5步，直到所有成员都迁移</li>
</ol>
</li>
<li>重新启用负载均衡器</li>
</ol>
<div class="blog_h3"><span class="graybg">增减分片</span></div>
<p>从分片集群中添加、删除分片，可能会导致负载再平衡（Chunk迁移）。估算一下总计的迁移数据量，抽生产环境空闲的时段执行增减。</p>
<p>增加分片的方式：</p>
<ol>
<li>配置好分片复制集</li>
<li>调用 sh.addShard()增加分片</li>
</ol>
<p>删除分片的方式：</p>
<ol>
<li>确保负载均衡器被启用，因为移除分片必然面临Chunk迁移</li>
<li>执行<pre class="crayon-plain-tag">db.adminCommand( { listShards: 1 } )</pre>来决定要移除的分片</li>
<li>在admin数据库上执行移除命令：<br />
<pre class="crayon-plain-tag">use admin
db.runCommand( { removeShard: "mongodb0" } ) </pre>
</li>
<li>验证Chunk迁移完成，再次调用上述命令，系统会报告迁移进度：<br />
<pre class="crayon-plain-tag">{
    // 正在迁移被移除节点上的Chunk
    "msg" : "draining ongoing",
    "state" : "ongoing",
    "remaining" : {
        // 剩余的Chunk数量
        "chunks" : 42,
        // 剩余的主分片位于被移除Shard的数据库数量
        "dbs" : 1
    },
    "ok" : 1
}</pre></p>
<p> 反复检查此命令，直到remaining为0</p>
</li>
<li>移动未分片集合。如果被删除分片是某个数据库的主分片，则需要转移主分片：
<ol>
<li>检查主分片分布情况<pre class="crayon-plain-tag">sh.status()</pre>：<br />
<pre class="crayon-plain-tag"># 数据库products的主分片为mongodb0（被删除分片）
{  "_id" : "products",  "partitioned" : true,  "primary" : "mongodb0" } </pre>
</li>
<li>
<p>移动主分片，执行命令：</p>
<pre class="crayon-plain-tag"># 移动数据库products的主分片到mongodb1
db.runCommand( { movePrimary: "products", to: "mongodb1" })</pre>
<p>该命令会阻塞，直到移动完成 </p>
</li>
</ol>
</li>
<li>再次执行命令以清除所有元数据：<br />
<pre class="crayon-plain-tag">use admin
db.runCommand( { removeShard: "mongodb0" } )</pre></p>
<p>理想的输出应该是：</p>
<pre class="crayon-plain-tag">{
    "msg" : "removeshard completed successfully",
    "state" : "completed",   // 删除分片成功
    "shard" : "mongodb0",
    "ok" : 1
} </pre>
</li>
</ol>
<div class="blog_h3"><span class="graybg">清除jumbo标记</span></div>
<p>如果某个Chunk超过指定的大小，或者包含的文档数量超过限制，则MongoDB将其标记为 jumbo。如果元数据变更后，jumbo不再超过限制，则MongoDB自动取消其标记。
<p>如果要手工清除标记，执行：</p>
<ol>
<li>对于可分裂（Divisible）Chunk，最好的方式是将其split，分裂成功后，MongoDB会清除jumbo标记：
<ol>
<li>通过客户端连接到mongos</li>
<li>执行<pre class="crayon-plain-tag">sh.status(true)</pre>找到jumbo，例如：<br />
<pre class="crayon-plain-tag">test.foo
     shard key: { "x" : 1 }
...
{ "x" : 2 } --&gt;&gt; { "x" : 4 } on : shard-a Timestamp(2, 2) jumbo</pre>
</li>
<li>对于上面这个分片键值范围为[2,4)，因此可以从分片键值3一分为二。分裂成功后jumbo标记自动清除：<br />
<pre class="crayon-plain-tag">sh.splitAt( "test.foo", { x: 3 }) </pre>
</li>
</ol>
</li>
<li>对于不可见（Indivisible）Chunks，某些情况下jumbo不能再次分裂，比如它仅仅包含一个分片键值，此时的清除步骤如下：
<ol>
<li>临时的停止负载均衡器</li>
<li>备份config数据库：<pre class="crayon-plain-tag">mongodump --db config --port &lt;config server port&gt; --out &lt;output file&gt;</pre> </li>
<li>连接到mongos</li>
<li>执行sh.status(true)找到不可再分的Chunk，例如：<br />
<pre class="crayon-plain-tag">{ "x" : 2 } --&gt;&gt; { "x" : 3 } on : shard-a Timestamp(2, 2) jumbo</pre></p>
<p> 可以看到，该jumbo仅仅一个分片键值2，因此无法再分裂</p>
</li>
<li>手工修改配置数据库：<br />
<pre class="crayon-plain-tag">db.getSiblingDB("config").chunks.update(
   { ns: "test.foo", min: { x: 2 }, jumbo: true },
   # 清除标记
   { $unset: { jumbo: "" } }
)</pre>
</li>
<li>重新启动负载均衡器</li>
<li>刷空元数据缓存：<pre class="crayon-plain-tag">db.adminCommand({ flushRouterConfig: 1 } )</pre> </li>
</ol>
</li>
</ol>
<div class="blog_h3"><span class="graybg">备份元数据</span></div>
<p>集群的配置数据库包含了集群的所有元数据，特别是Chunk如何映射到Shard，应该定期备份防止丢失。</p>
<p>备份步骤：</p>
<ol>
<li>停止负载均衡器</li>
<li>关闭一个配置服务器复制集的成员</li>
<li>拷贝其dbPath下所有数据文件，备份起来</li>
<li>启动关闭的配置服务器复制集成员</li>
<li>重新启用负载均衡器</li>
</ol>
<div class="blog_h2"><span class="graybg">管理脚本样例</span></div>
<div class="blog_h3"><span class="graybg">一键式集群创建脚本</span></div>
<pre class="crayon-plain-tag">#!/bin/bash

run_docker(){
type=$1
ipfx=$type
shrole=shardsvr
if [[ $type = "c" ]]; then :  
	ipfx=
	shrole=configsvr
fi 
num=$2
docker run --name mongo-$type$num --network local --ip 172.21.1.$ipfx$num -d  docker.gmem.cc/mongo --auth \
  --replSet=rs$type --$shrole --config /etc/mongod.conf
(( num++ ))
docker run --name mongo-$type$num --network local --ip 172.21.1.$ipfx$num -d  docker.gmem.cc/mongo --auth \
  --replSet=rs$type --$shrole --config /etc/mongod.conf
(( num++ ))
docker run --name mongo-$type$num --network local --ip 172.21.1.$ipfx$num -d  docker.gmem.cc/mongo --auth \
  --replSet=rs$type --$shrole --config /etc/mongod.conf
}

init_rs(){
type=$1
ipfx=$type
cfgsvr=
if [[ $type = "c" ]]; then :  
	ipfx=
	cfgsvr="configsvr: true,"
fi 
num=$2
num1=$num
(( num++ ))
num2=$num
(( num++ ))
num3=$num
read -d '' scr &lt;&lt;EOF
rs.initiate(
  {
    _id: "rs$type",
    $cfgsvr
    members: [
      { _id : 0, host : "172.21.1.$ipfx$num1:27017" },
      { _id : 1, host : "172.21.1.$ipfx$num2:27017" },
      { _id : 2, host : "172.21.1.$ipfx$num3:27017" }
    ]
  }
)
EOF
echo Prepare to execute Replica Set init script on mongo-$type$2: 
echo  "$scr"
docker exec mongo-$type$2 mongo --eval "$scr"
}

docker stop mongo-11  mongo-13  mongo-22  mongo-31  mongo-33  mongo-42  mongo-51  mongo-53  mongo-c7  \
  mongo-s1  mongo-12  mongo-21  mongo-23  mongo-32  mongo-41  mongo-43  mongo-52  mongo-c6  mongo-c8 

docker rm mongo-11  mongo-13  mongo-22  mongo-31  mongo-33  mongo-42  mongo-51  mongo-53  mongo-c7  mongo-s1  \
  mongo-12  mongo-21  mongo-23  mongo-32  mongo-41  mongo-43  mongo-52  mongo-c6  mongo-c8 

run_docker c 6
run_docker 1 1
run_docker 2 1
run_docker 3 1
run_docker 4 1
run_docker 5 1

sleep 3

init_rs c 6
init_rs 1 1
init_rs 2 1 
init_rs 3 1 
init_rs 4 1
init_rs 5 1


docker run --name mongo-s1 --network local --ip 172.21.1.1 -d  docker.gmem.cc/mongo mongos \
  --configdb=rsc/172.21.1.6:27017,172.21.1.7:27017,172.21.1.8:27017 --config /etc/mongos.conf


read -d '' scr &lt;&lt;EOF
sh.addShard( "rs1/172.21.1.11:27017")
sh.addShard( "rs2/172.21.1.21:27017")
sh.addShard( "rs3/172.21.1.31:27017")
sh.addShard( "rs4/172.21.1.41:27017")
sh.addShard( "rs5/172.21.1.51:27017")

db.getSiblingDB('admin').createUser(
 {
 user: "root",
 pwd: "root",
 roles: [ { role: "root", db: "admin" } ]
 }
)
EOF

docker exec mongo-s1 mongo --eval "$scr" </pre>
<div class="blog_h1"><span class="graybg">安全性</span></div>
<div class="blog_h2"><span class="graybg">身份验证</span></div>
<div class="blog_h3"><span class="graybg">认证机制</span></div>
<p>要改变使用的认证机制，设置mongod/mongos的参数<pre class="crayon-plain-tag">authenticationMechanisms</pre>。</p>
<p>支持的认证机制列表：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">机制</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>SCRAM-SHA-1</td>
<td rowspan="2">3.0之前的版本，MongoDB使用MONGODB-CR作为默认的，质询/响应式的身份验证机制。之后的版本默认使用SCRAM-SHA-1 </td>
</tr>
<tr>
<td>MONGODB-CR</td>
</tr>
<tr>
<td>x.509</td>
<td>基于数字证书的身份验证，该机制支持外部验证（客户端）、内部验证（复制集/分片集群）</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">X.509</span></div>
<p>这种认证机制要求使用TLS/SSL连接，X509可以实现客户端验证和内部验证。在生产环境中使用时，你需要具有单个CA签发的有效证书。可以选择自己维护CA。</p>
<p>客户端可以提供X.509证书来代替用户名/密码，进行登录验证。复制集/分片集群成员可以用X.509来代替keyfile进行相互验证。</p>
<div class="blog_h3"><span class="graybg">基于密码的外部验证</span></div>
<p>所谓外部验证，是指针对MongoDB客户端的身份验证和访问控制。</p>
<p>注意，当没有创建任何用户时，通过localhost匿名登录不受访问限制（Localhost Exception）。但是，一旦创建了任意用户，就不能再用匿名登录。你创建的第一个用户，必须具有创建新用户的权限 —— 针对admin数据库具有 userAdmin 或者userAdminAnyDatabase 权限的用户，后续使用该用户管理普通的MongoDB用户，例如创建、授权、角色管理。</p>
<p>启用步骤：</p>
<ol>
<li>不启用身份验证的情况下，启动mongod</li>
<li>通过Shell连接到mongod</li>
<li>创建管理员用户（注意，用户一旦创建，以后你就不能通过localhost匿名登录了）：<br />
<pre class="crayon-plain-tag">use admin
db.createUser(
  {
    user: "root",
    pwd: "root",
    roles: [ { role: "userAdminAnyDatabase", db: "admin" } ]
  }
)</pre>
</li>
<li>启用身份验证，重启mongod：<pre class="crayon-plain-tag">mongod --auth</pre></li>
<li>通过Shell连接，你可以：
<ol>
<li>提供身份信息连接：<pre class="crayon-plain-tag">mongo --port 27017 -u "root" -p "root" admin</pre> </li>
<li>不提供身份信息，连接到Shell后，调用 <pre class="crayon-plain-tag">db.auth("root","root")</pre>进行验证</li>
</ol>
</li>
<li>添加普通用户：<br />
<pre class="crayon-plain-tag">use cluster
db.createUser(
  {
    user: "cluster",
    pwd: "cluster",
    roles: [ { role: "readWrite", db: "cluster" },
             { role: "read", db: "reporting" } ]
  }
)</pre>
</li>
</ol>
<div class="blog_h3"><span class="graybg">基于Keyfiles的内部验证</span></div>
<p>keyfiles基于SCRAM-SHA-1认证机制。keyfile的内容作为集群成员的共享密钥使用。密钥的长度在6-1024字节之间，仅仅支持base64字符，空白符被自动去除。在UNIX系统中，keyfile不得授予组、全局访问权限。</p>
<p>对于相互连接的所有mongos/mongod实例，它们必须具有相同的keyfile，否则无权加入到复制集或者连接到分片集群。keyfile通过配置项<pre class="crayon-plain-tag">security.keyFile</pre>指定。</p>
<p>为复制集启用keyfiles认证的步骤：</p>
<ol>
<li>创建keyfile，可以使用openssl生成随机密钥：<br />
<pre class="crayon-plain-tag">openssl rand -base64 756 &gt; &lt;path-to-keyfile&gt;
chmod 400 &lt;path-to-keyfile&gt;</pre>
</li>
<li>拷贝keyfile到所有复制集成员</li>
<li>关闭复制集，连接到每一个mongod，并执行：<br />
<pre class="crayon-plain-tag">use admin
db.shutdownServer()</pre>
</li>
<li>在启用访问控制的情况下启动复制集：<br />
<pre class="crayon-plain-tag">security:
  keyFile: &lt;path-to-keyfile&gt;</pre>
</li>
<li>使用Shell连接到主节点</li>
<li>创建管理员用户：<br />
<pre class="crayon-plain-tag">db.getSiblingDB('admin').createUser(
  {
    user: "root",
    pwd: "root",
    roles: [ { role: "userAdminAnyDatabase", db: "admin" } ]
  }
) </pre>
</li>
<li>以管理员身份登录：<pre class="crayon-plain-tag">db.getSiblingDB("admin").auth("root", "root" )</pre></li>
<li>可选的，创建具有clusterAdmin角色的集群（能管理整个复制集、分片集群）管理用户：<br />
<pre class="crayon-plain-tag">db.getSiblingDB("admin").createUser(
  {
    "user" : "ca",
    "pwd" : "ca",
    roles: [ { "role" : "clusterAdmin", "db" : "admin" } ]
  }
)</pre>
</li>
</ol>
<p>为分片集群启用keyfiles认知的步骤：</p>
<ol>
<li>创建keyfile</li>
<li>拷贝keyfile到所有分片集群成员</li>
<li>禁用负载均衡器：<br />
<pre class="crayon-plain-tag">sh.stopBalancer()
# 查看负载均衡器状态，确保停止后进行下一步
sh.getBalancerState()</pre>
</li>
<li>关闭所有mongos实例，通过Shell连接到mongos，执行<br />
<pre class="crayon-plain-tag">db.getSiblingDB("admin").shutdownServer()</pre>
</li>
<li>关闭所有配置服务器实例，类似步骤4</li>
<li>关闭所有分片复制集的所有成员，类似步骤4</li>
<li>修改所有mongos、mongod的配置文件，启用security.keyFile选项</li>
<li>启动分片，可选的，创建分片本地管理员用户。通过Shell连接到分片复制集的主节点进行创建</li>
<li>启动mongos，通过localhost匿名登录，创建管理员，至少需要userAdminAnyDatabase角色</li>
<li>以管理员身份登录mongos，创建具有clusterAdmin角色的集群管理用户</li>
<li>使用集群管理用户登录</li>
<li>启动负载均衡器 </li>
</ol>
<div class="blog_h2"><span class="graybg">访问控制</span></div>
<p>MongoDB使用基于角色的访问控制（RBAC）。一个用户被授予1-N个角色，这些角色决定了用户能够对数据库进行哪些操作。</p>
<div class="blog_h3"><span class="graybg">启用访问控制</span></div>
<p>在配置文件中启用<pre class="crayon-plain-tag">security.authorization</pre>设置，或者使用<pre class="crayon-plain-tag">--auth</pre>命令行选项，即可启用访问控制。</p>
<div class="blog_h3"><span class="graybg">角色</span></div>
<p>所谓角色，即特权（privileges）的组合，每个特权可以针对某个资源进行某种操作。角色可以存在继承层次，子角色继承父角色的所有特权。</p>
<p>要查看角色具有哪些特权，执行：</p>
<pre class="crayon-plain-tag">db.runCommand({
  rolesInfo: { role: &lt;name&gt;, db: &lt;db&gt; },
  # 设置为true则显示角色的特权
  showPrivileges: &lt;Boolean&gt;,
  # 如果设置为true，则db.runCommand({rolesInfo:1})的输出包含内部角色
  showBuiltinRoles: &lt;Boolean&gt;
})</pre>
<p>内置角色列表：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">角色</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td colspan="2">
<p><strong><em>数据库用户角色</em></strong></p>
<p>每个数据库都具有以下两个角色</p>
<p>&nbsp;</p>
</td>
</tr>
<tr>
<td>read</td>
<td>
<p>读取所有非系统集合、system.indexes、system.js、system.namespaces上的数据</p>
</td>
</tr>
<tr>
<td>readWrite</td>
<td>读写所有非系统集合、system.js上的数据</td>
</tr>
<tr>
<td colspan="2">
<p><strong><em>数据库管理角色<br /></em></strong></p>
<p>每个数据库都具有以下角色</p>
</td>
</tr>
<tr>
<td>dbAdmin</td>
<td>执行Schema相关的管理，管理索引，收集统计信息</td>
</tr>
<tr>
<td>dbOwner</td>
<td>readWrite, dbAdmin,userAdmin的组合</td>
</tr>
<tr>
<td>userAdmin</td>
<td>管理数据库上的角色和用户</td>
</tr>
<tr>
<td colspan="2">
<p><strong><em>集群管理角色</em></strong></p>
<p>admin数据库具有以下角色</p>
</td>
</tr>
<tr>
<td>clusterAdmin</td>
<td>对集群有很宽泛的管理权，合并clusterManager, clusterMonitor,hostManager，且支持dropDatabase操作</td>
</tr>
<tr>
<td>clusterManager</td>
<td>对集群进行管理监控，可以访问config、local数据库</td>
</tr>
<tr>
<td>clusterMonitor</td>
<td>只读权限</td>
</tr>
<tr>
<td>hostManager</td>
<td>监控和管理服务器</td>
</tr>
<tr>
<td colspan="2">
<p><strong><em>备份还原角色</em></strong></p>
<p>admin数据库具有以下角色</p>
</td>
</tr>
<tr>
<td>backup</td>
<td>支持数据库备份操作</td>
</tr>
<tr>
<td>restore</td>
<td>支持数据库还原操作</td>
</tr>
<tr>
<td colspan="2">
<p><strong><em>全数据库角色</em></strong></p>
<p>admin数据库具有以下角色，自动应用到除了local、config之外的所有数据库</p>
</td>
</tr>
<tr>
<td>readAnyDatabase</td>
<td>对除了local、config之外的所有数据库（包括集群中）进行读操作</td>
</tr>
<tr>
<td>readWriteAnyDatabase</td>
<td>对除了local、config之外的所有数据库（包括集群中）进行读写操作</td>
</tr>
<tr>
<td>userAdminAnyDatabase</td>
<td>对除了local、config之外的所有数据库（包括集群中）进行用户管理操作</td>
</tr>
<tr>
<td>dbAdminAnyDatabase</td>
<td>对除了local、config之外的所有数据库（包括集群中）具有类似于dbAdmin的特权</td>
</tr>
<tr>
<td colspan="2"><strong><em>超级用户角色</em></strong></td>
</tr>
<tr>
<td>root</td>
<td>readWriteAnyDatabase, dbAdminAnyDatabase, userAdminAnyDatabase,clusterAdmin, restore,backup的组合</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">特权</span></div>
<p>特权即： 资源和针对该资源可以进行的操作。资源可以是：数据库、集合、一系列集合、集群。</p>
<div class="blog_h1"><span class="graybg">常见问题</span></div>
<div class="blog_h2"><span class="graybg">复制集</span></div>
<div class="blog_h3"><span class="graybg">not master and slaveOk=false</span></div>
<p>默认情况下，复制集仅仅允许在主节点上执行读操作，要允许在从节点上进行读，在主节点上执行：<pre class="crayon-plain-tag">rs.slaveOk()</pre></p>
<div class="blog_h3"><span class="graybg">增加Shell中it返回的数据量</span></div>
<p>在MongoDB Shell中执行命令：<pre class="crayon-plain-tag">DBQuery.shellBatchSize = 100</pre>   </p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/mongodb-study-note">MongoDB学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/mongodb-study-note/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Redis学习笔记</title>
		<link>https://blog.gmem.cc/redis-study-note</link>
		<comments>https://blog.gmem.cc/redis-study-note#comments</comments>
		<pubDate>Fri, 27 Mar 2015 12:43:13 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Database]]></category>
		<category><![CDATA[NoSQL]]></category>
		<category><![CDATA[Redis]]></category>
		<category><![CDATA[学习笔记]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=14710</guid>
		<description><![CDATA[<p>简介 Redis（REmote DIctionary Server）是一个开源的、基于内存的数据结构存储。可以作为数据库、缓存、消息代理使用。它支持字符串、散列、列表、集合、有序集合等多种数据结构，并提供范围查询。它支持位图、hyperloglogs（一种用于解决count-distinct问题的算法，可以估算Bag中的distinct元素的近似个数）、地理空间索引，并支持径向查询。Redis内置了复制（replication）机制，支持Lua脚本、LRU清除算法。Redis支持事务，以及不同级别的磁盘持久化。基于Redis Sentinel和Redis Cluster的自动分区（automatic partitioning）机制，它能够提供HA保障。 Redis可以认为是NoSQL数据库的一种，它是目前最流行的键值对存储引擎。这类存储允许你基于键来保存数据，在之后，你必须知道键才能取回数据。 几乎所有的主流语言都有Redis的客户端。 对比Memcached Redis使用场景 由于Redis更加新，特性更加丰富，相比Memcached它通常总是正确的选择。 Redis的最根本优势在于数据结构的支持。它支持长达250MB的键、值大小，支持字符串、哈希、列表、有序集合、集合等数据类型；Memcached仅仅支持250字节的键，值仅仅支持字符串。 Redis作为缓存使用时，数据清除算法更加丰富，相比之下Memcached仅支持LRU、随机清除。Redis提供主动清除（生存期）、被动（延迟）清除，Memcached仅支持被动清除。 Memcached使用场景 Memcached更加适合缓存相对小的、静态数据，例如HTML代码片段。这是因为Memcached的内部内存管理不像Redis那样精巧，在元数据方面消耗较少的内存。但是，如果数据尺寸是动态的，Memcached的上述优势很快消失，因为它存在内存碎片问题。 Scaling是选择Memcached的另外一个场景，因为它是多线程的。你可以很容易的Scaling up来使用更多的计算资源。Redis则基本是单线程的，你需要通过集群来水平Scale，比Memcached复杂。 安装 Ubuntu 参考以下步骤进行安装： [crayon-69e0b630a5c47768126599/] 容器化 <a class="read-more" href="https://blog.gmem.cc/redis-study-note">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/redis-study-note">Redis学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">简介</span></div>
<p>Redis（REmote DIctionary Server）是一个开源的、基于内存的数据结构存储。可以作为数据库、缓存、消息代理使用。它支持字符串、散列、列表、集合、有序集合等多种数据结构，并提供范围查询。它支持位图、hyperloglogs（一种用于解决count-distinct问题的算法，可以估算Bag中的distinct元素的近似个数）、地理空间索引，并支持径向查询。Redis内置了复制（replication）机制，支持Lua脚本、LRU清除算法。Redis支持事务，以及不同级别的磁盘持久化。基于Redis Sentinel和Redis Cluster的自动分区（automatic partitioning）机制，它能够提供HA保障。</p>
<p>Redis可以认为是NoSQL数据库的一种，它是目前最流行的键值对存储引擎。这类存储允许你基于键来保存数据，在之后，你必须知道键才能取回数据。</p>
<p>几乎所有的主流语言都有Redis的客户端。</p>
<div class="blog_h2"><span class="graybg">对比Memcached</span></div>
<div class="blog_h3"><span class="graybg">Redis使用场景</span></div>
<p>由于Redis更加新，特性更加丰富，相比Memcached它通常总是正确的选择。</p>
<p>Redis的<span style="background-color: #c0c0c0;">最根本优势在于数据结构的支持</span>。它支持长达250MB的键、值大小，支持字符串、哈希、列表、有序集合、集合等数据类型；Memcached仅仅支持250字节的键，值仅仅支持字符串。</p>
<p>Redis作为缓存使用时，数据清除算法更加丰富，相比之下Memcached仅支持LRU、随机清除。Redis提供主动清除（生存期）、被动（延迟）清除，Memcached仅支持被动清除。</p>
<div class="blog_h3"><span class="graybg">Memcached使用场景</span></div>
<p>Memcached更加<span style="background-color: #c0c0c0;">适合缓存相对小的、静态数据</span>，例如HTML代码片段。这是因为Memcached的内部内存管理不像Redis那样精巧，在元数据方面消耗较少的内存。但是，如果数据尺寸是动态的，Memcached的上述优势很快消失，因为它存在内存碎片问题。</p>
<p>Scaling是选择Memcached的另外一个场景，因为它是多线程的。你可以很容易的Scaling up来使用更多的计算资源。Redis则基本是单线程的，你需要通过集群来水平Scale，比Memcached复杂。</p>
<div class="blog_h1"><span class="graybg">安装</span></div>
<div class="blog_h2"><span class="graybg">Ubuntu</span></div>
<p>参考以下步骤进行安装：</p>
<pre class="crayon-plain-tag"># 安装Redis服务器
sudo apt-get install redis-server
# 查看版本
redis-server --version
# 禁用服务自动启动
sudo update-rc.d redis-server disable</pre>
<div class="blog_h2"><span class="graybg">容器化</span></div>
<p>使用下面的命令可以运行基于Docker的Redis：</p>
<pre class="crayon-plain-tag"># 拉取镜像
docker pull redis:3

# 在后台运行Redis容器
docker run --name redis -d redis

# 启用持久化存储，存储目录默认/data
docker run 
    -v ~/Docker/volumes/redis/data:/data -v ~/Docker/volumes/redis/conf:/conf 
    -p 6379:6379 --name redis -d redis redis-server /conf/redis.conf</pre>
<p>要定制Redis配置文件，可以扩展镜像： </p>
<pre class="crayon-plain-tag">FROM redis:3

COPY redis.conf /usr/local/etc/redis/redis.conf
CMD [ "redis-server", "/usr/local/etc/redis/redis.conf" ]</pre>
<div class="blog_h1"><span class="graybg">基础知识</span></div>
<div class="blog_h2"><span class="graybg">通信协议</span></div>
<p>Redis是一个TCP服务器，使用请求/应答通信模型。这意味着一个请求通常按如下步骤完成：</p>
<ol>
<li>客户端发送一个请求给服务器，通常以阻塞的方式等待读套接字，以获取服务器响应</li>
<li>服务器处理命令，并把结果发送给客户端</li>
</ol>
<p>客户端/服务器基于网络连接，这个网络可能很快——例如环回网卡，也可能很慢。从客户端发送请求，到接收到服务器响应的这段时间，称为RTT（Round Trip Time）。</p>
<div class="blog_h3"><span class="graybg">管道</span></div>
<p>Redis支持在客户端尚未读取旧的响应之前就处理新的请求，客户端可以连续发送多个命令给服务器，而<span style="background-color: #c0c0c0;">在最后一起处理所有应答</span>。Redis API提供了管道相关的接口</p>
<div class="blog_h2"><span class="graybg">发布/订阅</span></div>
<p>Redis支持这种交互模型，并提供了相关的命令。使用这种模型，消息发送者不需要显式的发消息发送给特定的接受者，而仅仅需要把消息发布到频道（Channel）上，从而实现解耦。</p>
<p>订阅了频道的客户端，不应该发送不相关的命令，仅可以发送：SUBSCRIBE, PSUBSCRIBE, UNSUBSCRIBE, PUNSUBSCRIBE, PING, QUIT这几个命令。</p>
<div class="blog_h3"><span class="graybg">推送消息格式</span></div>
<p>每个推送给订阅客户端的消息，是三元素的数组。第一个元素是消息的类型，值可以是：</p>
<ol>
<li>subscribe，表示成功订阅了通道。目标通道名称作为第二个元素，第三个元素是当前订阅的通道数</li>
<li>unsubscribe，表示成功取消了订阅。目标通道名称作为第二个元素，第三个元素是当前订阅的通道数</li>
<li>message，表示接收到其它客户端PUBLISH的消息。第二个元素是消息来自的通道，第三个元素是消息载荷</li>
</ol>
<div class="blog_h3"><span class="graybg">关于键空间</span></div>
<p>发布/订阅与Redis数据库、键空间没有任何关系，你在数据库0上发布，客户端可以在数据库10上订阅。</p>
<div class="blog_h3"><span class="graybg">基于模式匹配的订阅</span></div>
<p>Redis支持基于通配符的订阅，示例：</p>
<pre class="crayon-plain-tag">PSUBSCRIBE news.*
PSUBSCRIBE f*</pre>
<div class="blog_h2"><span class="graybg">内存优化</span></div>
<div class="blog_h3"><span class="graybg">容器类型的特殊编码</span></div>
<p>从2.2开始，Redis优化了很多数据类型，以占用更少的内存空间。仅仅由整数构成的哈希、集合、列表，以及有序集合，在编码后占用内存大小可能小十倍。从用户和API的角度来说，这一编码是完全透明的。 </p>
<p>特殊编码是一种CPU消耗 - 内存占用的权衡。Redis提供一些参数，来调整编码行为：</p>
<pre class="crayon-plain-tag"># 进行压缩的容器，其元素个数的限制
hash-max-zipmap-entries 512  
# 进行压缩的集合，其元素长度的限制
hash-max-zipmap-value 64 
list-max-ziplist-entries 512
list-max-ziplist-value 64
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
set-max-intset-entries 512</pre>
<p>当目标容器元素个数或者元素长度超过限制，则自动使用正常编码。</p>
<div class="blog_h3"><span class="graybg">使用32位实例</span></div>
<p>由于指针长度短，32位的Redis使用更少的内存，但是缺点是，它最多使用4GB的内存。对于32/64位Redis，RDB、AOF文件的格式是兼容的。</p>
<div class="blog_h3"><span class="graybg">位与字节操作</span></div>
<p>从Redis 2.2引入的GETRANGE, SETRANGE, GETBIT,SETBIT命令，允许你将字符串作为随机访问数组看待。前面两个命令可以按字节操作，后面两个命令更是按位操作。合理使用这些命令可以减少内存消耗。</p>
<div class="blog_h3"><span class="graybg">尽可能使用哈希</span></div>
<p>小的哈希被Redis很好的编码，很节约空间，因此你可以尽可能考虑以哈希的方式存储数据，而不是单独存储每个字段。</p>
<div class="blog_h3"><span class="graybg">内存分配</span></div>
<p>为了存储用户的键值，Redis可以分配<span style="background-color: #c0c0c0;">不超过maxmemory设置</span>的内存。</p>
<p>关于Redis管理内存的方式，你需要知道：</p>
<ol>
<li>当键被移除后，Redis<span style="background-color: #c0c0c0;">不总会把内存归还给OS</span>。你需要<span style="background-color: #c0c0c0;">根据内存需要量的峰值</span>来规划配置</li>
<li>尽管Redis可能不归还内存，但是<span style="background-color: #c0c0c0;">新插入的键会智能的使用这些未归还、但是已经空闲的内存</span></li>
<li>碎片率（ fragmentation ratio）在当前内存用量远小于峰值用量时，可能不准确。<span style="background-color: #c0c0c0;">碎片率 = 当前内存用量 / RSS</span>。RSS即驻留工作集尺寸——占据物理内存的尺寸</li>
</ol>
<p>如果你不设置maxmemory参数，Redis会尽可能多的占用内存，从而影响OS性能。</p>
<div class="blog_h2"><span class="graybg">作为LRU缓存</span></div>
<p>当把Redis作为缓存使用时，让其自动的清除陈旧数据，通常符合缓存需求。</p>
<p>LRU是最常见的一种数据清除算法，Redis也支持其它算法。</p>
<div class="blog_h3"><span class="graybg">限制最大内存</span></div>
<p>使用配置指令可以限制Redis使用内存的峰值：</p>
<pre class="crayon-plain-tag">maxmemory 100mb</pre>
<p>如果设置为0，表示无限制。这是64位系统的默认值，32位系统隐含限制3GB。</p>
<div class="blog_h3"><span class="graybg">清除策略</span></div>
<p>当Redis到达最大内存限制后，可以依据设置的策略（policies）来决定如何处理 —— 例如返回错误，或者清除旧数据。支持的策略如下表：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">策略</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>noeviction</td>
<td>在内存不足的时候，返回错误</td>
</tr>
<tr>
<td>allkeys-lru</td>
<td>针对所有键执行LRU算法，最久未被使用的键被移除</td>
</tr>
<tr>
<td>volatile-lru</td>
<td>针对设置了超时的键执行LRU算法</td>
</tr>
<tr>
<td>allkeys-random</td>
<td>针对所有键进行随机清除</td>
</tr>
<tr>
<td>volatile-random</td>
<td>针对设置了超时的键进行随机清除</td>
</tr>
<tr>
<td>volatile-ttl</td>
<td>尝试移除TTL最短（过期时间最近）的键</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">LFU算法</span></div>
<p>从4.0版本开始，Redis支持新的LFU（ Least Frequently Used）算法，在某些情况下该算法工作的更好。该算法会尝试跟踪每个键被使用的频率，使用频率最少的键会被清除。LRU算法可能会保留最近访问了一次，但是实际上基本不会被访问的键，LFU不存在此问题。</p>
<div class="blog_h2"><span class="graybg">Redis事务</span></div>
<p>Redis支持把多个命令分为一组，然后作为单个事务来执行。</p>
<p>MULTI, EXEC, DISCARD,WATCH这几个命令是Redis事务控制的基础。通过组合使用这些命令，你可以单步执行多个命令，并确保：</p>
<ol>
<li>事务中<span style="background-color: #c0c0c0;">所有命令被顺序的执行</span>。从效果上说，<span style="background-color: #c0c0c0;">不会发生其他客户端发起的命令，插入上述事务中间执行的情况</span>。所有命令就像是单个操作似的</li>
<li><span style="background-color: #c0c0c0;">要么所有命令执行，要么都不执行</span>。EXEC命令触发事务中所有命令的执行。当使用AOF文件时，Redis确保使用单个write(2)系统调用把事务操作写入磁盘，除非Redis崩溃或者被强制杀死 —— 可能仅有部分命令的结果被写入磁盘。如果发生这种意外，在Redis下一次启动时，它会提示错误并退出。你需要使用<pre class="crayon-plain-tag">redis-check-aof</pre>来修复AOF文件，把不完整执行的事务移除，然后再启动Redis</li>
</ol>
<p><span style="background-color: #c0c0c0;">从2.2开始，Redis为事务提供额外保证 —— 使用乐观锁</span>。</p>
<div class="blog_h3"><span class="graybg">用法</span></div>
<p>Redis事务从MULTI命令开始，该命令的应答总是OK。发起该命令后，用户可以继续发起多个命令。后续的命令不会立即执行，而是<span style="background-color: #c0c0c0;">排队，直到你调用EXEC</span>命令。</p>
<p>如果调用DISCARD而不是EXEC，会清除命令队列并且关闭事务。</p>
<div class="blog_h3"><span class="graybg">事务中的错误</span></div>
<p>在事务中，你可能遇到两类命令错误：</p>
<ol>
<li>命令可能无法进入队列，原因例如命令格式错误，或者出现极端情况，例如内存不够</li>
<li>命令可以在EXEC调用之后失败</li>
</ol>
<p>对于第一类错误，用户可以检查入队命令的返回值，如果显示QUEUED意味着入队成功。否则意味着入队失败，这种情况下通常需要DISCARD事务。从2.6.5开始，只要存在入队失败，EXEC一定会返回一个错误并自动DISCARD事务。</p>
<p>对于第二类错误，没有特殊的处理。如果<span style="background-color: #c0c0c0;">某个命令执行失败，其它任务仍然会继续执行</span>。</p>
<div class="blog_h3"><span class="graybg">不支持回滚</span></div>
<p>前面我们提到过，事务过程中某条命令执行失败，并不会中断后续命令的执行，或者回滚之前已经执行的命令，这和关系型数据库很不一样。Redis的这种行为的原因是：</p>
<ol>
<li>Redis<span style="background-color: #c0c0c0;">命令仅仅会在语法错误、键持有不匹配的数据类型的情况下，才会失败</span>。这意味着<span style="background-color: #c0c0c0;">失败通常由于编程错误</span>，应该在开发阶段就发现</li>
<li>由于不去支持回滚，这让Redis更简单、更快</li>
</ol>
<div class="blog_h3"><span class="graybg">乐观锁</span></div>
<p><span style="background-color: #c0c0c0;">WATCH命令</span>提供了一种检查并设置（CAS，check-and-set），为Redis事务提供<span style="background-color: #c0c0c0;">乐观锁</span>。</p>
<p>被监控（WATCHed）的键的修改会被发现，如果存在一个或者更多的键，在EXEC之前修改了，则整个事务被中止，EXEC命令返回一个Null Reply。</p>
<p>WATCH命令可以调用多次，其监控的时间范围是，从调用的那一刻起，到EXEC被调用时为止。</p>
<p>调用UNWATCH，可以撤销之前所有WATCH命令。</p>
<div class="blog_h2"><span class="graybg">持久化</span></div>
<p>Redis提供了两种持久化模式：<span style="background-color: #c0c0c0;">RDB、AOF。这两种模式可以同时启用</span>，你也可以完全禁用持久化。当同时启用时，在Redis启动时会读取AOF文件，因为它更能保证数据的完整性。</p>
<p>Redis官方建议同时使用两种持久化模式，并且在远期未来将其合并为一种。</p>
<p>RDB相关的配置项：save；AOF相关的配置项：appendonly。</p>
<div class="blog_h3"><span class="graybg">RDB模式</span></div>
<p>该模式在磁盘上保存<span style="background-color: #c0c0c0;">某个瞬间的数据集快照</span>，可以定期的保存。Redis异步的把数据写到磁盘上，形成.rdb文件。该模式对于很多应用程序足够好用。</p>
<p>RDB的优势：</p>
<ol>
<li>作为Redis的瞬时快照，RDB文件<span style="background-color: #c0c0c0;">格式紧凑，很适合作备份用途</span>，例如远程灾备</li>
<li>RDB<span style="background-color: #c0c0c0;">性能较好</span>，因为Redis主线程仅仅需要Fork出子线程负责磁盘I/O，<span style="background-color: #c0c0c0;">主线程基本不需要执行磁盘I/O</span></li>
<li>如果数据集很大，RDB的启动速度高于AOF</li>
</ol>
<p>RDB的劣势：</p>
<ol>
<li>如果Redis进程崩溃或者电力中断，你可能<span style="background-color: #c0c0c0;">丢失数分钟的数据，具体时间范围取决于你如何配置保存点（Save point）</span></li>
</ol>
<div class="blog_h3"><span class="graybg">AOF模式</span></div>
<p>另一种持久化模式是仅附加文件（Append Only File），它实际上是服务器<span style="background-color: #c0c0c0;">接收到的每个写命令的日志</span>，且命令日志的格式与Redis协议相同。当服务器重新启动时，该日志可以被<span style="background-color: #c0c0c0;">回放，以还原先前的数据集</span>。</p>
<p>该模式提供了好得多的durability：</p>
<ol>
<li>使用<span style="background-color: #c0c0c0;">默认的数据fsync策略</span>你最多在断电这种<span style="background-color: #c0c0c0;">极端情况下丢失1秒的数据</span></li>
<li><span style="background-color: #c0c0c0;">Redis进程本身出现问题（但OS正常）时，最多丢失最后一次写操作</span></li>
</ol>
<p>AOF的额外优势：</p>
<ol>
<li>AOF日志仅仅需要进行追加操作，因而不需要Seek。AOF不存在文件损坏的问题，即使日志结尾是写入一半的命令，也可以用 redis-check-aof进行修复</li>
<li>AOF文件变大后，Redis可以在后台进行重写。这个重写操作是完全安全的，因为重写时Redis还会在旧文件上追加，如果重写失败，旧文件仍然可用</li>
<li>AOF日志格式容易解析</li>
</ol>
<p>AOF的劣势：</p>
<ol>
<li><span style="background-color: #c0c0c0;">相同数据集下AOF比RDB大</span></li>
<li><span style="background-color: #c0c0c0;">基于使用的fsync策略，AOF可能比RDB慢</span>。禁止fsync在高并发情况下，性能与RDB一样快，如果<span style="background-color: #c0c0c0;">每秒fsync一次，也还算很快</span></li>
</ol>
<div class="blog_h2"><span class="graybg">主从复制</span></div>
<p>Redis中进行主从复制的配置很简单，关于主从复制，你需要知道：</p>
<ol>
<li>Redis使用<span style="background-color: #c0c0c0;">异步的主从复制</span>，从2.8开始，Slave定期的确认（acknowledge）从复制流中处理的数据量</li>
<li>一个Master可以拥有<span style="background-color: #c0c0c0;">多个Slave</span></li>
<li>Slave允许来自其它Slave的连接。并形成<span style="background-color: #c0c0c0;">Master - Slave - Slave的树形结构</span></li>
<li>在Master节点，<span style="background-color: #c0c0c0;">复制是非阻塞的</span>。当一个或者多个Slave执行初始同步时，Master仍然接受查询请求</li>
<li>在Slave节点，复制也是非阻塞的。当Slave执行初始同步时，它仍然能够（如果你进行适当的配置）使用原来的数据集对外提供查询服务</li>
<li>复制可以提供扩容性，可以<span style="background-color: #c0c0c0;">把只读的缓慢查询分配给Slave执行</span></li>
<li>可以<span style="background-color: #c0c0c0;">完全避免Master的磁盘I/O开销</span>，你可以配置一个Slave，并启用AOF。这种技巧需要注意处理Master宕机，因为重启后它的数据集为空，不应该作为Master</li>
</ol>
<div class="blog_h3"><span class="graybg">不持久化Master的安全性</span></div>
<p>上面我们提到过，可以处于性能的考虑禁用Master的持久化。Redis不建议这样，<span style="background-color: #c0c0c0;">如果禁用Master的持久化，一定要同时禁止Redis开机启动</span>。</p>
<div class="blog_h3"><span class="graybg">主从复制工作方式</span></div>
<p>当你配置好一个Slave后，在连接建立后它发送一个<span style="background-color: #c0c0c0;">PSYNC命令</span>。如果这次连接属于<span style="background-color: #c0c0c0;">“重新连接”并且Master的backlog足够大，则Master把增量数据集</span>发送给Slave。否则，触发一次<span style="background-color: #c0c0c0;">完全同步（full resynchronization ）</span>。</p>
<p><span style="background-color: #c0c0c0;">完全同步触发后，Master在后台启动一个保存线程，产生RDB文件</span>。与此同时，<span style="background-color: #c0c0c0;">Master对新的写命令进行缓存</span>。RDB文件准备好之后，发送给Slave加载到内存，然后写命令缓存也被发送给Slave进行回放。</p>
<p>当与Master的<span style="background-color: #c0c0c0;">连接断掉后，Slave会自动进行重连</span>。</p>
<p>如果<span style="background-color: #c0c0c0;">多个Slave需要同步，Master只会产生一个RDB保存进程</span>。</p>
<div class="blog_h2"><span class="graybg">批量插入</span></div>
<p>某些场景下你需要在短时间内完成大量数据的插入，例如添加百万个新的Redis键。本节内容介绍如何尽快的完成数据的插入。</p>
<p>使用普通的Redis客户端执行海量数据插入通常不是好主意，因为：</p>
<ol>
<li>最简单的方式：一个一个的发送命令，大量时间浪费在了RRT上</li>
<li>使用管道（Pipelining）可以缓解上一条，但是，它限制了在最后一起处理响应。在大量数据插入的场景下，最好能一边插入新条目，一边处理旧条目的响应</li>
</ol>
<p>仅仅少量的客户端支持非阻塞I/O，而且，并非所有客户度能够高效的解析响应以最大化吞吐量。因此，在Redis中完成海量数据的最好方式是，生成包含原始数据、Redis协议的文本文件，然后通过Redis客户端的Pipe模式发送给服务器处理。</p>
<div class="blog_h2"><span class="graybg">分区</span></div>
<p>所谓<span style="background-color: #c0c0c0;">分区（Partitioning）</span>是指把你的数据分散到多个Redis实例的处理过程，<span style="background-color: #c0c0c0;">每个实例仅持有键空间的子集</span>。分区的意义在于：</p>
<ol>
<li>通过利用多台计算机，突破单机内存限制，支持更大的数据集</li>
<li>把计算能力Scale到多台计算机的多个CPU；把网络带宽Scale到多台计算机的多个网络接口</li>
</ol>
<div class="blog_h3"><span class="graybg">分区方式</span></div>
<p>最简单的是范围分区（range partitioning），它要求键的格式为<pre class="crayon-plain-tag">key-name:id</pre>。它还需要一张表来记录id范围和实例的映射关系。在实际中很少使用这种方式</p>
<p>另外一种是哈希分区（hash partitioning），它通过计算键的散列值来决定其应当由哪个实例持有，对键格式没有要求，也不需要额外的表。</p>
<div class="blog_h3"><span class="graybg">实现方式</span></div>
<p>分区的职责可以划分给软件栈中的不同组件：</p>
<ol>
<li>客户端分区：客户端直接决定该把键发送给哪个节点处理。很多Redis客户端实现了此功能</li>
<li>代理辅助分区：客户端把请求发送给一个理解Redis协议的代理，此代理负责转发请求给适当的实例。Redis实现了这种方式，Memcached的Twemproxy类似</li>
<li>查询路由：客户端随机的把请求发送给一个实例。由该实例将其转发给正确的实例。<span style="background-color: #c0c0c0;">Redis集群利用客户端的辅助，实现了混合形式的查询路由 —— 请求不在节点间转发</span>，<span style="background-color: #c0c0c0;">而是客户端被重定向到正确的节点</span>，并由客户端直接发送请求给正确的节点</li>
</ol>
<div class="blog_h3"><span class="graybg">分区的缺点</span></div>
<p>Redis分区的某些特性做的不是很好：</p>
<ol>
<li><span style="background-color: #c0c0c0;">牵涉到多个键的操作常常不被支持</span>。例如，你不能（直接）对两个分布在不同实例上的两个集合进行交叉操作</li>
<li>牵涉到<span style="background-color: #c0c0c0;">多个键的事务无法支持</span></li>
<li>分区的粒度是键，<span style="background-color: #c0c0c0;">无法对一个键下的巨大数据集进行分区</span></li>
<li>使用分区时，数据的处理更加复杂。例如需要处理多个RDB/AOF文件。在备份时，你需要从多个机器上收集持久化文件</li>
<li>增减容量可能比较复杂。<span style="background-color: #c0c0c0;">Redis集群支持几乎透明的在运行时添加/移除节点</span>，实现数据的Rebalance。<span style="background-color: #c0c0c0;">其它分区实现，例如客户端分区、代理辅助分区则不支持这种Rebalance</span>，需要使用Pre-sharding技术</li>
</ol>
<div class="blog_h3"><span class="graybg">数据存储还是缓存</span></div>
<p>尽管从概念上说，Redis分区用在数据存储还是缓存场景下没有什么区别。但是，用作数据存储时，有一个重要的限制——<span style="background-color: #c0c0c0;">一个键必须总是映射到相同的Redis实例</span>。</p>
<p>一致性哈希（Consistent hashing）实现通常能够在某个键的最优节点不可用时，自动切换到其它节点。类似的，当添加新节点后，一部分新的键可以自动分配到新节点上。</p>
<p>当<span style="background-color: #c0c0c0;">作为缓存使用时，Redis可以基于一致性哈希很容易的Scale up/down</span>。</p>
<p>当<span style="background-color: #c0c0c0;">用作数据存储时，需要基于固定的键-节点映射表，节点的数量必须是固定的，不能改变</span> —— 否则必须在增减节点时实现Rebalance，当前只有Redis Cluster支持这种Rebalance。</p>
<div class="blog_h3"><span class="graybg">Presharding</span></div>
<p>前面我们提到过，作为数据存储的分区，要添加/删除节点并不容易。但是，随着时间的推移，数据存储肯定是要不断变化的，今天需要10个节点就满足需要，明天可能需要增加到15个。</p>
<p>要解决这个问题，第一个方案是，<span style="background-color: #c0c0c0;">从开始就准备足够多（32或者64个满足绝大部分场景）的实例</span>。可以这样做的原因是Redis实例本身占用很少的资源，它仅仅需要1MB的内存。计算在数据很少的情况下，你也可以在单台机器上使用这种分区方式。当数据增多，单台机器计算资源不够时，可以增加一台服务器，并把一半的实例迁移到新的机器上。利用Redis的复制机制（Replication），可以在免宕机/最小化宕机时间的前提下，实现这种迁移：</p>
<ol>
<li>在新服务器上启动新实例</li>
<li>把这些实例<span style="background-color: #c0c0c0;">作为需要迁移的旧实例的Slave</span></li>
<li>停止客户端</li>
<li>更新迁移实例的IP地址配置</li>
<li>在新服务器上，对实例发送<pre class="crayon-plain-tag">SLAVEOF NO ONE</pre>命令</li>
<li>基于新的配置启动客户端</li>
<li>关闭旧服务器上不再需要的那些实例</li>
</ol>
<div class="blog_h3"><span class="graybg">Redis集群</span></div>
<p>从2015年4月开始，Redis Cluster已经可以胜任生产环境了，<span style="background-color: #c0c0c0;">Redis Cluster的实现方式类似于查询路由、客户端分区的混合</span>。</p>
<p>Redis Cluster是最优的分区方案，因为它可以自动分区并提供高可用性。一旦基于你熟悉编程语言的Redis Cluster客户端可用，Redis集群将作为分区实现的标准。</p>
<div class="blog_h2"><span class="graybg">分布式锁</span></div>
<p>当多个进程需要互斥的操作同一共享资源时，分布式锁是一种有用的原语。</p>
<p>很多第三方库实现了可以配合Redis的分布式锁管理器（Distributed Lock Manager，DLM），它们的实现方式各有不同。Redis推荐基于红锁（Redlock）算法的实现，主流语言有<a href="https://redis.io/topics/distlock">实现</a>。</p>
<div class="blog_h3"><span class="graybg">安全性和活动性保证</span></div>
<p>Redlock提供以下最小化保证：</p>
<ol>
<li>安全性保证：互斥性，在任意时刻，仅一个客户端能够持有锁</li>
<li>活动性A：不会发生死锁，请求者最终一定会获得锁，甚至是在持有锁的客户端崩溃、分区发生的情况下</li>
<li>活动性B：容错，只要大部分Redis节点可用，客户端总是能获得、释放锁</li>
</ol>
<div class="blog_h2"><span class="graybg">键空间通知</span></div>
<p>这是2.8.0开始引入的功能，它允许客户端订阅特定的频道，并在影响了Redis数据集的事件发生时，获得通知。这些事件例如：</p>
<ol>
<li>针对某个特定键的命令被执行</li>
<li>针对所有键的LPUSH命令被执行</li>
<li>数据库0中的任意键过期</li>
</ol>
<p>事件通过普通的Redis Pub/Sub机制完成推送。但是，由于Pub/Sub没有提供持久化机制（因为它是fire and forget的），如果你的应用程序需要可靠的事件通知（reliable notification），键空间通知目前是不满足需求的。</p>
<div class="blog_h2"><span class="graybg">辅助索引</span></div>
<p>由于值可以是结构化的，Redis并不是严格意义上的键-值存储。由于值可以是结构化的，因此支持不同类别的辅助索引（secondary indexes），包括组合（多列）索引就很有意义了。</p>
<p>Redis支持创建以下类型的索引：</p>
<ol>
<li>对于有序集合，可以根据ID或者其它数字字段创建索引</li>
<li>基于词法范围（lexicographical ranges）的有序集合，可以创建更加复杂的辅助索引，组合索引，或者图遍历（graph traversal ）索引</li>
<li>对于集合，可以创建随机索引</li>
<li>对于列表，可以创建简单的迭代索引以及最后N条目的索引</li>
</ol>
<p>索引的实现和维护，是Redis服务器的高级主题。对于大部分使用复杂查询的用户来说，应该考虑是否利用关系型数据库更加合适。</p>
<div class="blog_h3"><span class="graybg">有序集合的简单数字索引</span></div>
<p>在Redis中<span style="background-color: #c0c0c0;">使用辅助索引的最简单方式是，使用有序集合</span> —— 基于浮点数Score来排序元素的数据结构。使用有序集合索引的两个基础命令是ZADD、ZRANGEBYSCORE，前者添加元素，后者根据Score进行范围扫描。</p>
<p>举例来说，你可以根据年龄来索引一系列的用户名：</p>
<pre class="crayon-plain-tag"># 添加元素
ZADD myindex 18 Anna
ZADD myindex 35 Jon
ZADD myindex 67 Helen

# 索引范围查找
ZRANGEBYSCORE myindex 20 40</pre>
<p>通常情况下，用户（User） 实体包含多个字段，而不仅仅是名字，这种情况下，可以在有序集合中存储用户的ID：</p>
<pre class="crayon-plain-tag"># 有序集合中存储实体标识符
ZADD user.age.index 38 1
ZADD user.age.index 42 2

# 实体以哈希方式另外存放
HMSET user:1 id 1 username antirez ctime 1444809424 age 38
HMSET user:2 id 2 username maria ctime 1444808132 age 42</pre>
<p>这样，可以针对多个字段，分别建立索引。</p>
<p>ZADD还可以用来更新索引值，例如一年后，用户的年龄需要增加1（当然使用出身日期更简单），这时候，可以：</p>
<pre class="crayon-plain-tag"># 设置实体哈希的年龄字段
HSET user:1 age 39
# 更新索引
ZADD user.age.index 39 1

# 注意，上面的两个操作，可以使用MULTI/EXEC事务确保原子性</pre>
<div class="blog_h3"><span class="graybg">字典序索引</span></div>
<p>有序集合具有一个重要特性，当两个元素的Score相同时，它们将根据元素值进行字典排序（底层调用memcmp函数）。Redis的这种索引的内部<span style="background-color: #c0c0c0;">实现方式和性能类似于关系型数据库的B树索引</span>。使用字典序索引时，常常把Scoure一律设置为0。</p>
<p>使用 <a href="#zrangbylex">ZRANGEBYLEX</a>、 ZLEXCOUNT 命令，可以使用字典序索引。</p>
<p><strong>自动完成的例子</strong></p>
<p> 字典序索引的一个常见应用是，表单的快速自动完成提示。当用户键入bit时，可以在后端执行以下Redis命令：</p>
<pre class="crayon-plain-tag"># \xff表示最后一个字节的最大值可以是255
ZRANGEBYLEX myindex "[bit" "[bit\xff"</pre>
<p>如果需要对自动提示根据使用频率进行排序，我们可以扩展元素的值：</p>
<pre class="crayon-plain-tag">ZREM myindex 0 banana:1
# banana第二次被搜索的时候，设置频率为2
ZADD myindex 0 banana:2</pre>
<p>这里用冒号来分隔自动完成关键字和搜索频率，由于字典序范围搜索在Redis中是二进制安全的，所以你可以使用任何分隔符，例如\0\0。</p>
<p><strong>添加辅助信息</strong></p>
<p>使用上述的分隔机制，我们可以在字典元素值中附加任意的内容，以满足应用需要。<span style="background-color: #c0c0c0;">总之记住，字典序比较是以前缀为基准的</span>。</p>
<div class="blog_h3"><span class="graybg">复合索引</span></div>
<p>前面我们介绍了索引单个字段的方式，那么，能不能像关系型数据库那样，索引多个字段呢。</p>
<p>考虑一个场景，我们需要对一个巨大仓库中的产品进行查询，依据是房间号、价格。这依赖于这两个字段的复合索引，实现起来其实很简单，关键之处还是字典序的前缀机制。我们需要<span style="background-color: #c0c0c0;">对数字类型的索引字段进行前缀补齐</span>，<span style="background-color: #c0c0c0;">确保它们的长度总是一致</span>，这样才能正确的进行字典序排序：</p>
<pre class="crayon-plain-tag"># room:price:product_id
# 索引字段必须补齐
# 辅助信息产品ID不需要补齐 

ZADD myindex 0 0056:0028.44:90
ZADD myindex 0 0034:0011.00:832

# 查询56号房间中，价格在10-30之间的产品
ZRANGEBYLEX myindex [0056:0010.00 [0056:0030.00</pre>
<div class="blog_h1"><span class="graybg">集群</span></div>
<p>Redis集群（Cluster）提供了自动的把数据分区（Shard）到多个Redis节点（实例）的机制。</p>
<p>此外，集群也提供了某种程度的高可用性，当<span style="background-color: #c0c0c0;">部分节点不可达的情况下，集群仍然可以运行</span>。但是某些极端情况下，例如<span style="background-color: #c0c0c0;">大多数Master不可达，则不行</span>。</p>
<p>Redis集群中的节点需要两个端口，一个用于<span style="background-color: #c0c0c0;">服务客户端，默认6379</span>，另外一个端口比此端口<span style="background-color: #c0c0c0;">大10000，用于集群总线</span>。集群总线使用二进制协议，进行节点-节点之间的通信，用于<span style="background-color: #c0c0c0;">故障检测（failure detection）、配置更新、故障转移授权等操作</span>。</p>
<p>注意Redis集群与Docker的端口映射不兼容（也不兼容一般性的NAT环境、IP或者端口被重映射的环境），你可能需要使用host networking mode模式。</p>
<div class="blog_h2"><span class="graybg">集群基础</span></div>
<div class="blog_h3"><span class="graybg">数据分片</span></div>
<p>Redis集群不使用一致性哈希（consistent hashing），而是基于所谓Hash Slot进行分片，从概念上说，每个键都是Hash Slot的一部分。</p>
<p>集群中一共有16384个Hash Slot，<span style="background-color: #c0c0c0;">计算键所属的Slot时，仅需要获得键的CRC16，然后针对16384进行取模操作</span>。</p>
<p>集群中的每个节点负责所有Hash Slot的一个子集，当<span style="background-color: #c0c0c0;">增减节点时，可以很方便的重新分配Hash Slot</span>。把Hash Slot从一个节点转移到另外一个，不会引入downtime。</p>
<p><span style="background-color: #c0c0c0;">如果所有键都属于同一个Hash  Slot，Redis集群支持针对这些键的multiple key操作</span>。你可以<span style="background-color: #c0c0c0;">使用Hash Tag强制一批键归属于同一个Slot</span>。</p>
<div class="blog_h3"><span class="graybg">集群的主从模型</span></div>
<p>为了避免因为Master子集故障，或者无法连接到大部分的Master节点导致的集群不可用，Redis使用主从节点模型，<span style="background-color: #c0c0c0;">每个Hash Slot具有N份复制，其中一份位于Master节点，N-1份位于N-1个Slave节点上</span>。当Master节点失败后，集群会自动推举它的一个Slave成为新的Master。</p>
<div class="blog_h3"><span class="graybg">一致性保证</span></div>
<p>Redis集群<span style="background-color: #c0c0c0;">不保证强一致性</span>，这意味着某些情况下，服务器已经向客户端确认的写操作，其数据可能丢失。</p>
<p>导致写丢失的一个原因是Redis使用异步复制，考虑以下场景：</p>
<ol>
<li>客户端向Master B发送写操作</li>
<li>Master B向客户端应答OK</li>
<li>Master B向它的Slave  B1 B2 B3传播此写操作</li>
</ol>
<p>可以看到，在<span style="background-color: #c0c0c0;">同步到Slave之前，Master已经应答了客户端OK</span>，Redis这样做是出于性能考虑。如果在第2、3步之间Master B宕机，Bx晋升为Master，则写操作就永远的丢失了。</p>
<p>上述场景和那些配置为每秒执行Flush操作的数据库类似，也就是说你以前可能也面临这种数据丢失的风险。此外，你可以在<span style="background-color: #c0c0c0;">应答客户端之前强制Redis Flush数据，这可能会影响性能，但是可以改善一致性</span>。</p>
<p>Redis集群可以提供同步写操作，如果你的确需要。<span style="background-color: #c0c0c0;">使用WAIT命令，丢失写的可能性大大减小</span>。注意，即使使用WAIT也不能保证强一致性，特殊的情况下，一个不能接收来自Master的写入操作的Slave可能晋升为Master。</p>
<div class="blog_h2"><span class="graybg">创建并使用集群</span></div>
<div class="blog_h3"><span class="graybg">手工建立</span></div>
<p>首先，你需要若干个<span style="background-color: #c0c0c0;">运行在集群模式</span>的Redis实例。节点的最小化配置文件示例如下：</p>
<pre class="crayon-plain-tag">port 7000
cluster-enabled yes
# 节点的集群配置文件，集群内部使用，不应该手工编辑
cluster-require-full-coverage no
cluster-node-timeout 15000
cluster-config-file nodes.conf
cluster-migration-barrier 1
appendonly yes</pre>
<p>节点的数量<span style="background-color: #c0c0c0;">至少需要3个Maste</span>r。本节我们使用6个节点，其余三个为Slave。依次分配端口7000 - 7005。 你可以依据端口分别创建目录，在目录中存放上面的配置文件和Redis服务器二进制文件。然后，启动Redis服务器，你就拥有了6个Redis实例。</p>
<p>在节点第一次启动时，nodes.conf尚不存在，redis会自动初始化此文件，并得到自己的唯一标识（Node  ID）：</p>
<pre class="crayon-plain-tag">[82462] 26 Nov 11:56:55.329 * No cluster configuration found, I'm 97a3a64667477371c4479320d683e4c8db5858b1</pre>
<p>要建立集群，可以使用Redis提供的命令行工具redis-trib，该工具包含在Redis 的源码中，依赖于：</p>
<pre class="crayon-plain-tag">gem install redis</pre>
<p>执行下面的命令即可建立集群：</p>
<pre class="crayon-plain-tag"># create表示建立集群
# --replicas 指定每个Master的Slave数量
# 后面的参数为每个节点的地址和端口
./redis-trib.rb create --replicas 1 127.0.0.1:7000 127.0.0.1:7001 \
127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005</pre>
<p>上述命令执行后，如果一起正常，屏幕上最终会打印： </p>
<pre class="crayon-plain-tag">[OK] All 16384 slots covered</pre>
<div class="blog_h3"><span class="graybg">检查状态</span></div>
<p>你可以随时执行下面的命令，判断集群状态是否正常：</p>
<pre class="crayon-plain-tag">redis-trib.rb check  redis-node-ip:6379
# 理想输出
# [OK] All nodes agree about slots configuration.
# &gt;&gt;&gt; Check for open slots...
# &gt;&gt;&gt; Check slots coverage...
# [OK] All 16384 slots covered.</pre>
<div class="blog_h3"><span class="graybg">使用create-cluster脚本</span></div>
<p>如果你不想如上面那样，手工的配置、执行每个Redis实例， 可以使用Redis的utils目录下的create-cluster，这是一个Bash脚本，在底层它还是调用redis-trib完成集群创建的。示例：</p>
<pre class="crayon-plain-tag">create-cluster create
create-cluster start
create-cluster stop</pre>
<div class="blog_h2"><span class="graybg">重分区（Resharding）</span></div>
<p><span style="background-color: #c0c0c0;">重分区就是把一些Hash Slots从一批节点转移到另外一批节点的过程</span>，我们同样需要使用redis-trib完成此工作：</p>
<pre class="crayon-plain-tag"># 只需要提供一个集群节点，其它节点会自动找到
./redis-trib.rb reshard 127.0.0.1:7000
# 回答问题，需要转移多少个Hash Slots
How many slots do you want to move (from 1 to 16384)?
# 根据提示，提供接收这些Slots的节点的ID
# 最后，可以检查集群的健康状态
./redis-trib.rb check 127.0.0.1:7000

# 非交互式的：
./redis-trib.rb reshard 
    --from &lt;node-id&gt; --to &lt;node-id&gt; --slots &lt;number of slots&gt; 
    --yes &lt;host&gt;:&lt;port&gt;</pre>
<div class="blog_h2"><span class="graybg">手动故障转移</span></div>
<p>某些时候，在Master没有宕机的时候，你就需要进行“故障转移”，例如Master机器需要进行硬件维护。<span style="background-color: #c0c0c0;">手工故障转移时，数据安全性比被迫故障转移要高</span>，数据不会在处理过程中丢失。</p>
<p>要实现手工的故障转移，需要使用命令<pre class="crayon-plain-tag">CLUSTER FAILOVER</pre>，注意必须<span style="background-color: #c0c0c0;">在被转移的Master的某个Slave上执行该命令</span>。 </p>
<div class="blog_h2"><span class="graybg">增减节点</span></div>
<div class="blog_h3"><span class="graybg">添加节点</span></div>
<p>向集群中添加节点，基本上就是建立一个Redis实例，然后：</p>
<ol>
<li><span style="background-color: #c0c0c0;">分配一些Slot给它，这种情况下新节点作为Master</span></li>
<li>作为一个<span style="background-color: #c0c0c0;">已知节点的Replica，这种情况下新节点作为Slave</span></li>
</ol>
<p>添加节点命令：</p>
<pre class="crayon-plain-tag"># 添加一个新节点 7006到集群
./redis-trib.rb add-node 127.0.0.1:7006 127.0.0.1:7000</pre>
<p>新添加的节点，与其它Master的不同之处：</p>
<ol>
<li>由于没有分配Slots，<span style="background-color: #c0c0c0;">因此它不持有数据</span></li>
<li>由于没有分配Slots，<span style="background-color: #c0c0c0;">它不参与Slave晋升的选举处理 </span></li>
</ol>
<p>通过重分区，可以为新节点分配Slots。</p>
<p>如果要作为Slave添加，可以参考如下脚本：</p>
<pre class="crayon-plain-tag">./redis-trib.rb add-node --slave --master-id 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e 127.0.0.1:7006 127.0.0.1:7000</pre>
<p>另外，你也可以使用CLUSTER REPLICATE命令完成上面的操作。 </p>
<div class="blog_h3"><span class="graybg">移除节点</span></div>
<p>从集群中移除节点，可以执行命令：<pre class="crayon-plain-tag">./redis-trib del-node 127.0.0.1:7000 `&lt;node-id&gt;`</pre>  </p>
<p>如果要<span style="background-color: #c0c0c0;">移除的是Master节点，它必须是空的</span>，如果不是，你需要Reshard它的数据到其它节点。</p>
<div class="blog_h2"><span class="graybg">Slave迁移</span></div>
<p>Redis集群支持重新配置Slave节点，让它<span style="background-color: #c0c0c0;">成为其它Master的Slave</span>。你只需要在Slave上执行：</p>
<pre class="crayon-plain-tag">CLUSTER REPLICATE &lt;master-node-id&gt;</pre>
<p>另外，Redis集群支持<span style="background-color: #c0c0c0;">自动化的重新配置Slave节点以增强集群的可用性</span>，不需要管理员手工介入。这种自动化的重新配置称为replicas migration。</p>
<div class="blog_h1"><span class="graybg">Sentinel</span></div>
<p>Redis Sentinel为Redis提供高可用性（HA），利用Sentinel，你可以创建无人值守的高可用系统，自动处理某些故障。此外，Sentinel还负责一些附属任务，包括监控、通知，并作为客户端的配置提供者（configuration provider）：</p>
<ol>
<li>监控：不断的检查你的Master、Slave是否正常工作</li>
<li>通知：可以通过API通知系统管理员或者其他应用程序，某个Redis实例出现问题</li>
<li>自动故障转移：如果一个Master不能正常工作，Sentinel启动一个故障转移，把Master的某个Slave提升为新Master，并让其兄弟Slave重新配置为新Master的Slave。此外，使用Redis的客户端可以得到Master变更的通知</li>
<li>配置提供者：可以作为客户端服务发现的源，客户端连接到Sentinel，后者提供某个服务的Master地址，故障转移发生后，Sentinel会报告新的地址</li>
</ol>
<p>Sentinel是一个分布式的系统，其本身设计为<span style="background-color: #c0c0c0;">多个Sentinel进程相互协作的运作模式</span>。多个Sentinel进程协作模式的优势：</p>
<ol>
<li>当多个Sentinel进程一致认为某个Master不可用，则启动故障检测。这可以降低误报（ false positives）的几率</li>
<li>如果部分Sentinel进程不可用，Sentinel作为一个整体仍然能够正常工作，这增强了系统的健壮性</li>
</ol>
<p>当前版本为Sentinel 2，比起上个版本使用了更强大、简单的预测算法（predict algorithms），作为Redis 2.8+的一部分发布。</p>
<div class="blog_h2"><span class="graybg">快速起步</span></div>
<div class="blog_h3"><span class="graybg">运行Sentinel</span></div>
<p>启动启用了Sentinel的Redis服务器，有两种方式：</p>
<pre class="crayon-plain-tag">redis-sentinel /path/to/sentinel.conf
# 或者
redis-server /path/to/sentinel.conf --sentinel</pre>
<p>提供Sentinel配置文件是必须的，因为配置文件会被用来保存系统的状态，如果不提供配置文件则无法启动。<span style="background-color: #c0c0c0;">Sentinel默认监听26379端口</span>。</p>
<div class="blog_h3"><span class="graybg">重要的事情</span></div>
<p>在部署Sentinel之前，你需要知道一些重要的事情：</p>
<ol>
<li>你需要至少3个Sentinel实例，以确保健壮性</li>
<li>这三个Sentinel相互独立，不会同时发生故障。例如将它们部署在不同的物理服务器上，或者独立的几个虚拟机上</li>
<li>Sentinel + Redis的分布式系统，不能保证已经确认的写操作在故障发生后不丢失，这是由于Redis异步复制机制导致的。但是，可以使用更安全的方式部署Sentinel，是数据丢失的窗口更小</li>
<li>客户端必须支持Sentinel，流行的客户端大多支持</li>
<li>注意测试，甚至是在生产环境下</li>
<li>Sentinel、Docker或者其它形式的NAT/端口映射机制需要小心的共存。Docker的<span style="background-color: #c0c0c0;">端口重映射破坏了Sentinel自动发现其它Sentinel、发现Master的Slave列表的能力</span></li>
</ol>
<div class="blog_h3"><span class="graybg">配置</span></div>
<p>最小化的配置示例：</p>
<pre class="crayon-plain-tag"># 指定每个需要监控的Master，为每个Master提供一个唯一性的名称
# 不需要指定Slave，因为可以自动发现。自动发现后，该配置文件会被Sentinel自动更小以反映Slave信息
# 每当故障转移发生的时候，该配置文件也会被自动更新

# 第一组Redis实例
# 定义Master组的选项：
# sentinel monitor &lt;master-group-name&gt; &lt;ip&gt; &lt;port&gt; &lt;quorum&gt;
# quorum的含义是，如果认定当前Master不可达，需要几个Sentinel进程同意
# quorum仅仅用于检测故障，要执行故障转移，某个Sentinel进程需要被推举为故障转移的Leader然后被授权执行故障转移
# 推举是由Sentinel进程内部进行的，如果大部分Sentinel同意推举则OK，这显然要求大部分Sentinel进程的相互可达
sentinel monitor mymaster 127.0.0.1 6379 2
# 其它选项的格式都是：
# sentinel &lt;option_name&gt; &lt;master_name&gt; &lt;option_value&gt;
# 至少多少mm不可达，Sentinel才开始考虑目标Master是否宕机
sentinel down-after-milliseconds mymaster 60000
sentinel failover-timeout mymaster 180000
# 在新的Master产生后，同时与之重新同步的兄弟Slave的最大数量。数字越低，故障转移消耗的时间越多
# 但是，如果Slave被配置为，继续使用旧数据对外服务，你可能不希望所有兄弟Slave同时重新同步
sentinel parallel-syncs mymaster 1

# 第二组Redis实例
sentinel monitor resque 192.168.1.3 6380 4
sentinel down-after-milliseconds resque 10000
sentinel failover-timeout resque 180000
sentinel parallel-syncs resque 5</pre>
<p>所有配置项在运行时，都可以基于<pre class="crayon-plain-tag">SENTINEL SET</pre>命令来配置。 </p>
<div class="blog_h1"><span class="graybg"><a id="config"></a>配置</span></div>
<p>如果处于测试和开发的目的，可以零配置的启动Redis服务器。但是在生产环境下，你通常需要自定义配置。</p>
<p>提供自定义配置，可以编写配置文件redis.conf。该文件由一系列的指令组成，每个指令的格式如下：</p>
<pre class="crayon-plain-tag">keyword argument1 argument2 ... argumentN
# 示例：
slaveof 127.0.0.1 6380
# 如果某个参数包含空格，可以使用引号：
requirepass "hello world"</pre>
<p>除了提供配置文件以外，还可以直接从命令行传递配置项：</p>
<pre class="crayon-plain-tag"># 格式与配置文件一样，只是每个配置项都具有 -- 前导标记
./redis-server --port 6380 --slaveof 127.0.0.1 6379</pre>
<p>要指定Redis使用的配置文件，可以这样启动服务：</p>
<pre class="crayon-plain-tag">./redis-server /path/to/redis.conf</pre>
<div class="blog_h2"><span class="graybg">运行时修改配置</span></div>
<p>Redis支持运行时按需修改配置，而不需要重启服务器。Redis提供在运行时读取、写入配置的命令：<pre class="crayon-plain-tag">CONFIG GET</pre>、  <pre class="crayon-plain-tag">CONFIG SET</pre>，大部分指令支持基于这两个命令进行读写。</p>
<p>需要注意，CONFIG SET命令对服务器配置的修改不会持久化，因此服务重启后配置信息即丢失。从2.8开始，你可以调用 <pre class="crayon-plain-tag">CONFIG REWRITE</pre>自动更新redis.conf，使其反映当前运行时的配置信息。</p>
<div class="blog_h2"><span class="graybg">配置为缓存</span></div>
<p>将Redis作为缓存服务使用时，你可以配置：</p>
<pre class="crayon-plain-tag">maxmemory 2mb
maxmemory-policy allkeys-lru</pre>
<p>这样，你不需要为键设置过期信息，所有键均会基于LRU算法清除。 </p>
<div class="blog_h2"><span class="graybg">常用配置项</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 35%; text-align: center;">配置项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td class="blog_h3" colspan="2"><strong><em>基本配置</em></strong></td>
</tr>
<tr>
<td>include path-to-file</td>
<td>包含其它的配置文件 </td>
</tr>
<tr>
<td>protected-mode</td>
<td>保护模式，如果on则仅仅本地连接可以访问</td>
</tr>
<tr>
<td>daemonize  bool</td>
<td>
<p>默认情况下Redis不是以守护程序的形式运行的</p>
<p>配置为daemonize yes则作为守护程序运行，Redis会在/var/run/redis.pid写PID文件</p>
</td>
</tr>
<tr>
<td>pidfile path-to-file</td>
<td>定制PID文件的位置</td>
</tr>
<tr>
<td>port  number</td>
<td>监听端口，默认6379。如果设置为0则不监听TCP端口</td>
</tr>
<tr>
<td>tcp-backlog number</td>
<td>TCP连接建立请求最大排队数</td>
</tr>
<tr>
<td>bind ipaddr [ipaddr...]</td>
<td>监听的IP地址，默认0.0.0.0</td>
</tr>
<tr>
<td>timeout number</td>
<td>客户端空闲多久后，断开连接，默认0表示不断开</td>
</tr>
<tr>
<td>tcp-keepalive number</td>
<td>如果不为0，启用TCP保活。默认0，如果启用，建议的值可以是60，表示每60秒使用SO_KEEPALIVE发送TCP ACKs</td>
</tr>
<tr>
<td>loglevel  level</td>
<td>设置服务器日志级别，默认notice。依据日志冗长程度，可选debug、verbose、notice、warning</td>
</tr>
<tr>
<td>logfile path-to-file</td>
<td>日志文件的名称</td>
</tr>
<tr>
<td>syslog-enabled bool</td>
<td>是否记录到操作系统日志</td>
</tr>
<tr>
<td>syslog-ident identity</td>
<td>系统日志标识符，默认redis</td>
</tr>
<tr>
<td>syslog-facility</td>
<td>可选值USER，或者LOCAL0-LOCAL7。默认local0</td>
</tr>
<tr>
<td>databases number</td>
<td>指定可以使用的数据库数量， 默认16。每个连接都可以选择不同的数据库</td>
</tr>
<tr>
<td class="blog_h3" colspan="2"><strong><em>快照相关配置</em></strong></td>
</tr>
<tr>
<td>save delay keynum</td>
<td>
<p>启用RDB快照。在delay秒内，如果有keynum或者更多的键被修改，执行持久化</p>
<p>该指令可以出现多次，可以使用<pre class="crayon-plain-tag">save ""</pre>取消前面所有save指令</p>
</td>
</tr>
<tr>
<td>stop-writes-on-bgsave-error bool</td>
<td>
<p>默认取值yes，意味着，如果启用了RDB快照（至少一个savepoint），并且上一次发起的后台保存操作失败，则不再接受写入命令。加入后台保存线程恢复工作，则Redis会自动允许保存</p>
<p>设置为no，则在保存失败的情况下继续支持写操作</p>
</td>
</tr>
<tr>
<td>rdbcompression bool</td>
<td>
<p>是否在dmup出.rdb文件时，启用压缩。默认yes</p>
</td>
</tr>
<tr>
<td>rdbchecksum bool</td>
<td>从版本5的RDB格式开始，文件尾部放置校验和。这可以检测文件破坏，但是会在保存、加载RDB时，损失10%左右的性能。默认yes</td>
</tr>
<tr>
<td>dbfilename filename</td>
<td>Dump出RDB时，使用的文件名，默认dump.rdb</td>
</tr>
<tr>
<td>dir path-to-dir</td>
<td>
<p>Dump出RDB时的工作目录，默认./</p>
<p>AOF也存放在此目录</p>
</td>
</tr>
<tr>
<td class="blog_h3" colspan="2"><em><strong>主从复制相关配置</strong></em></td>
</tr>
<tr>
<td>slaveof  masterip masterport </td>
<td>设置当前实例为某个实例的Slave</td>
</tr>
<tr>
<td>masterauth master-password</td>
<td>如果Master启用了密码保护（基于requirepass配置项），则这里需要提供此密码</td>
</tr>
<tr>
<td>slave-serve-stale-data bool</td>
<td>
<p>当Slave失去到Master的网络连接，或者复制操作正在进行中，该选项可以设置为：</p>
<ol>
<li>yes（默认值）：Slave仍然响应客户端请求，但是客户端获得的数据可能是陈旧的</li>
<li>no：lave会拒绝执行INFO、SLAVEOF之外的所有命令，并回复错误SYNC with master in progress</li>
</ol>
</td>
</tr>
<tr>
<td>slave-read-only bool</td>
<td>
<p>设置Slave是否是只读的，从2.6开始默认yes</p>
<p>允许Slave可写，可以用它存放一些生存期短暂的数据。因为每次和Slave重新同步时，这些数据都被清除 </p>
</td>
</tr>
<tr>
<td>repl-diskless-sync bool</td>
<td>
<p>是否启用无盘复制，默认no</p>
<p>对于新的或者重新连接到Master的Slave，不能进行增量同步，必须首先进行完整同步——把RDB文件传输给Slave。传输有两种方式：</p>
<ol>
<li>磁盘后备（ Disk-backed）的：Master创建一个新的进程，把RDB文件写到磁盘上。稍后该文件被父进程增量的传递给Slave。使用该方式时，多个Slave可以排队，只要RDB写入完毕，就可以一起接受RDB传输</li>
<li>无盘的：<span style="background-color: #c0c0c0;">Master创建一个新进程，直接把RDB写到Slave的套接字</span>。这种情况下，一旦RDB传输开始，后续到达的Slave需要排队，等RDB传输结束后，在一起接受传输。为了增大并行量，可以配置一个等待时间，超过此时间后，下一次RDB传输才会触发</li>
</ol>
<p>&nbsp;</p>
</td>
</tr>
<tr>
<td>repl-diskless-sync-delay sec</td>
<td>
<p>上面提到的无盘同步方式，产生传输RDB的子进程的延迟，默认5秒</p>
</td>
</tr>
<tr>
<td>repl-ping-slave-period sec</td>
<td>Slave会定期发送PING给Master，默认10秒</td>
</tr>
<tr>
<td>repl-timeout sec</td>
<td>
<p>主从复制的超时时间，超时用于：</p>
<ol>
<li>从Slave的角度，同步时的块I/O传输</li>
<li>从Slave的角度，Master的超时（数据、ping）</li>
<li>从Master的角度，Slave的超时（REPLCONF ACK pings）</li>
</ol>
<p>该配置的值必须比repl-ping-slave-period大，否则总是会超时</p>
</td>
</tr>
<tr>
<td>repl-disable-tcp-nodelay bool</td>
<td>
<p>同步完成后，是否在Slave套接字上禁用TCP_NODELAY，默认否</p>
<p>在禁用TCP_NODELAY的情况下，Redis会使用更小的TCP数据包、更小的带宽峰值来发送数据到Slave，但是会导致数据延迟。在Linux内核默认配置下，Slave可能在40ms后才看到Master上的数据</p>
</td>
</tr>
<tr>
<td>repl-backlog-size number</td>
<td>
<p>主从复制排队（Backlog）大小，默认值1mb。Backlog是一个数据缓冲区，当Slave断开并重连后，如果使用此缓冲区就足以进行增量同步，则不进行完整同步</p>
<p>仅当至少一个Slave连接到本Master的情况下，才分配此缓冲区</p>
<p><span style="background-color: #c0c0c0;">在很多环境下这个默认值显得太小，可能需要调整到100MB</span></p>
</td>
</tr>
<tr>
<td>repl-backlog-ttl  sec</td>
<td>默认600，表示当最后一个Slave断开到Master的连接之后，再过多少秒，清空Backlog</td>
</tr>
<tr>
<td>slave-priority number</td>
<td>Slave优先级，默认100。数值越低，有有资格晋升为Master。数值0表示永远不能晋升</td>
</tr>
<tr>
<td>min-slaves-to-write number</td>
<td>如果连接的Slave的数量少于指定的数字，Master拒绝写入操作，默认0</td>
</tr>
<tr>
<td>min-slaves-max-lag sec</td>
<td>如果最慢的Slave，其延迟超过指定的秒数，Master拒绝写入操作，默认10</td>
</tr>
<tr>
<td class="blog_h3" colspan="2"><em><strong>安全性配置</strong></em></td>
</tr>
<tr>
<td>requirepass passwd</td>
<td>要求客户端在发起其它命令之前，先发起AUTH进行身份认证</td>
</tr>
<tr>
<td>
<p>rename-command cmd newname</p>
</td>
<td>
<p>可以执行命令的重命名，从而避免客户端调用某些重要命令</p>
<p>newname为""则禁用</p>
</td>
</tr>
<tr>
<td class="blog_h3" colspan="2"><em><strong>资源限制配置</strong></em></td>
</tr>
<tr>
<td>maxclients num</td>
<td>同时连接的最大客户端数，默认10000</td>
</tr>
<tr>
<td>maxmemory num</td>
<td>
<p>允许Redis使用的最大内存，如果超过此限制，Redis会根据清除策略来移除键</p>
<p>如果清除策略为noeviction，则那些可能导致内存占用增加的命令，都会收到错误</p>
</td>
</tr>
<tr>
<td>maxmemory-policy policy</td>
<td>
<p>当最大内存限制到达时，Redis选择什么键执行移除：<br />volatile-lru、allkeys-lru、volatile-random、allkeys-random、volatile-ttl、noeviction</p>
<p>默认noeviction</p>
</td>
</tr>
<tr>
<td>maxmemory-samples</td>
<td>为了节省资源，Redis使用的LRU和最小化TTL算法是近似算法。该配置项用于微调，数字越大，CPU消耗越大，但是算法越精确。默认值5，较为适合生产环境，如果取值10则足够精确，但是较慢，如果取值3则足够快，但是不怎么精确</td>
</tr>
<tr>
<td class="blog_h3" colspan="2"><em><strong>APPEND ONLY 配置</strong></em></td>
</tr>
<tr>
<td>appendonly bool</td>
<td>是否启用AOF模式的持久化，默认no</td>
</tr>
<tr>
<td>appendfilename  filename </td>
<td>AOF文件的名字，默认appendonly.aof</td>
</tr>
<tr>
<td>appendfsync when</td>
<td>
<p>设置Redis调用fsync的频率。fsync调用导致操作系统立即同步写操作到磁盘，而不是在OS缓存中等待更多的输出。可选值：</p>
<ol>
<li>no，不主动调用fsync()，由操作系统决定何时flush数据，速度最快</li>
<li>everysec，每秒钟调用一次</li>
<li>always，每次写入Append log后都调用，速度最慢，但是安全性最高</li>
</ol>
</td>
</tr>
<tr>
<td>no-appendfsync-on-rewrite bool</td>
<td>
<p>当AOF策略设置为everysec或者always时，如果一个后台保存线程（一个后台保存或者AOF日志后台重写线程）正在执行很多磁盘IO操作，此时Redis主线程调用fsync可能（在某些Linux配置下）阻塞过长时间，此现象目前无法避免</p>
<p>为了缓和此问题，可以设置此选项的值为yes（默认值no），当BGSAVE、BGREWRITEAOF正在进行时，防止主线程调用fsync</p>
<p>&nbsp;</p>
</td>
</tr>
<tr>
<td>auto-aof-rewrite-percentage  100</td>
<td rowspan="2">
<p>配置自动触发的AOF重写，通过重写，AOF文件的尺寸可以得到优化。AOF重写会隐含的触发BGREWRITEAOF命令</p>
<p>当AOF日志文件增长了一定比例（以上一次重写后AOF文件，如果启动后从来没有进行过重写，则以启动时的AOF文件为基准）后，可以触发重写。第一个配置项设置此比例，设置为0则禁用自动的AOF重写</p>
<p>当AOF文件小于某个尺寸时，可以总不触发重写，第二个配置项设置此尺寸</p>
</td>
</tr>
<tr>
<td>auto-aof-rewrite-min-size  64mb</td>
</tr>
<tr>
<td>aof-load-truncated yes</td>
<td>
<p>当操作系统崩溃后，特别是挂在Ext4文件系统时没有指定data=ordered选项的情况下，你可能发现在Redis启动时，AOF文件被截断了</p>
<p>AOF截断的情况发生后，Redis要么：</p>
<ol>
<li>退出 ，提示错误。用户需要使用redis-check-aof来修复AOF文件</li>
<li>尽可能多的加载数据，这是默认行为</li>
</ol>
<p>该配置项就是控制这个行为的</p>
<p>&nbsp;</p>
</td>
</tr>
<tr>
<td class="blog_h3" colspan="2"><em><strong>集群配置</strong></em></td>
</tr>
<tr>
<td>cluster-enabled yes</td>
<td>是否启用集群，如果设置为yes则以集群节点的方式启动当前实例</td>
</tr>
<tr>
<td>cluster-config-file name</td>
<td>
<p>集群的每个节点具有一个集群配置文件。该文件通常不应该手工编辑，它由节点自己创建和更新</p>
<p>该配置用于手工指定集群配置文件的名称，注意，集群中每个节点必须拥有唯一性名称 </p>
</td>
</tr>
<tr>
<td>cluster-node-timeout 15000 </td>
<td>在认为一个节点进入失败状态（Failure state）之前，它必须处于不可达（unreachable）状态的毫秒数</td>
</tr>
<tr>
<td>cluster-slave-validity-factor 10 </td>
<td>
<p>如果一个Slave的数据貌似过于陈旧，它不会在Master宕机后作为故障转移的目标。陈旧的判断依据：</p>
<ol>
<li>如果有多个候选进行故障转移的Slave，它们会相互通信，<span style="background-color: #c0c0c0;">看谁具有最佳的复制偏移量</span>（replication offset），这样，偏移量最大的Slave数据最新</li>
<li>每个Slave计算与<span style="background-color: #c0c0c0;">Master的最后交互时间</span>——例如最后一个PING或者命令；如果Master已经断开，则计算断开至今的时间。如果最后交互时间过于久远，则此Slave绝不进行故障转移</li>
</ol>
<p>上述第二条的“久远”的阈值，计算公式为：</p>
<p style="padding-left: 30px;">(node-timeout * slave-validity-factor) + repl-ping-slave-period</p>
<p>公式中的slave-validity-factor即为当前所述的配置项</p>
<p>&nbsp;</p>
</td>
</tr>
<tr>
<td>cluster-migration-barrier 1</td>
<td>
<p>Slave可以迁移到孤立Master —— 即那些没有工作中的Slave的Master。该特性增强了Redis集群的健壮性，避免孤立Master无法故障转移</p>
<p>为了避免迁移后，Slave原先的Master又变成孤立Master，可以设置一个阈值，即<span style="background-color: #c0c0c0;">迁移后，原先Master至少拥有的工作中的Slave数量</span></p>
</td>
</tr>
<tr>
<td>cluster-require-full-coverage yes</td>
<td>
<p>默认的，如果有<span style="background-color: #c0c0c0;">任何一个Hash Slot没有被覆盖（分配给某个Master），则集群拒绝接受查询请求</span>。这样，如果集群的一部分节点关闭，则整个集群不再可用</p>
<p>这个默认行为可以改变，设置为no即可</p>
</td>
</tr>
<tr>
<td class="blog_h3" colspan="2"><em><strong>缓慢日志</strong></em></td>
</tr>
<tr>
<td>slowlog-log-slower-than 10000</td>
<td>判定查询为缓慢的阈值，单位微秒。设置为0则记录所有命令</td>
</tr>
<tr>
<td>slowlog-max-len 128</td>
<td>缓慢日志的最大长度</td>
</tr>
<tr>
<td class="blog_h3" colspan="2"><em><strong>延迟监控</strong></em></td>
</tr>
<tr>
<td>latency-monitor-threshold 00</td>
<td>
<p>Redis的延迟监控子系统在运行时分析不同操作的样本，以分析Redis实例高延迟的可能原因</p>
<p>该系统仅仅记录消耗时间大于latency-monitor-threshold的操作。如果latency-monitor-threshold设置为0，则延迟监控系统被关闭</p>
</td>
</tr>
<tr>
<td class="blog_h3" colspan="2"><em><strong>事件通知</strong></em></td>
</tr>
<tr>
<td>notify-keyspace-events ""</td>
<td>Redis可以通知Pub/Sub客户端键空间中发生的事件，该配置项指定启用哪些通知</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">redis-cli</span></div>
<p>该工具是Redis提供的命令行接口，它具有两种工作模式：交互式（REPL - 读取、估算、打印循环）、脚本式（命令作为redis-cli的参数提供）。</p>
<p>在交互式环境下，键入clear可以清屏，键入help可以获取帮助，键入exit可以退出交互模式。</p>
<div class="blog_h2"><span class="graybg">用法示例</span></div>
<pre class="crayon-plain-tag"># 执行一个命令，把键mycounter的值增加1
redis-cli incr mycounter
# 输出如下，括号内是值的类型：
# (integer) 7

# 强制输出原始数据
redis-cli --raw incr mycounter
# 强制输出易读数据
redis-cli --no-raw mycounter

# 默认情况下，redis-cli链接到本机的6379端口，你可以定制主机、端口
redis-cli -h hostname -p 6390 ping
# PONG

# 默认情况下，redis-cli操控数据库序号0，你可以指定操控的数据库序号：
redis-cli flushall
redis-cli -n 1 incr a  # 操控数据库1

# 从文本中读取foo的值，并写入到Redis
redis-cli -x set foo &lt; fooval.txt
# 从文本中读取一系列redis-cli命令，并交付执行
# cat /tmp/commands.txt | redis-cli
# commands.txt的内容如下：
# set foo 100
# incr foo
# append foo xxx
# get foo
# ...

# 连续执行、延迟执行：每隔100ms执行命令，连续5次
redis-cli -r 5 -i 100 incr foo


# 在交互式环境下，可以使用如下命令连接到其它Redis实例
connect host 6379
# 在连接断开后，CLI会自动尝试重新连接，如果连接失败，会给出提示信息
# 执行下面的命令手工重新连接
ping


# 连续打印Redis的统计信息，例如内存用量
redis-cli --stat


# 扫描整个键空间，来获得最大的键
redis-cli --bigkeys
# 扫描键空间
redis-cli --scan
# 扫描键空间，打印匹配指定正则式的键
redis-cli --scan --pattern '*-11*'


# 持续监控在Redis中执行的命令
redis-cli monitor

# 监控Redis实例的延迟，不停的发送PING命令
redis-cli --latency


# RDB远程备份：在Redis复制模式第一次同步的时候，Master/Slave以RDB文件的形式交换整个数据集
# 你可以通过CLI利用这一特性，实现远程备份
redis-cli --rdb /tmp/dump.rdb    #该命令获取Master的Dump，保存到本地</pre>
<div class="blog_h2"><span class="graybg">屏蔽危险命令</span></div>
<p>某些命令是管理性的，甚至能立即清空数据库，这类命令可以重命名，甚至禁用，以防客户端误用：</p>
<pre class="crayon-plain-tag">rename-command FLUSHALL "" </pre>
<div class="blog_h2"><span class="graybg">所有命令</span></div>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">命令</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td class=" blog_h3">APPEND</td>
<td>附加值到指定的键，类似于字符串连接操作，返回此命令结束后，值的长度</td>
</tr>
<tr>
<td class=" blog_h3">AUTH</td>
<td>请求身份验证</td>
</tr>
<tr>
<td class=" blog_h3">BGREWRITEAOF</td>
<td>
<p>指示服务器启动一个AOF（仅附加文件，Append only file）重写进程，此进程<span style="background-color: #c0c0c0;">将当前AOF重写为一个小的、优化的版本</span></p>
<p>如果此命令执行失败，数据不会丢失，因为当前AOF文件没有改变</p>
<p>仅不存在执行持久化操作的后台线程时，重写操作才被Redis触发：</p>
<ol>
<li>如果一个Redis子进程正在磁盘上建立快照，AOF重写仅被调度，直到创建RDB文件的子进程终结之后才启动</li>
<li>如果当前AOF重写操作正在进行，则此命令返回错误，且不调度AOF重写</li>
</ol>
<p>从2.4开始，AOF重写由Redis自动触发，但是你仍然可以使用该命令，随时触发重写</p>
</td>
</tr>
<tr>
<td class=" blog_h3">BGSAVE</td>
<td>
<p>在后台保存DB，立即返回OK。Redis进程将Fork出子进程来执行DB保存任务并在完毕后退出</p>
<p>执行LASTSAVE 命令可以查看上一次BGSAVE的执行状态</p>
</td>
</tr>
<tr>
<td class=" blog_h3">BITCOUNT</td>
<td>
<p>统计值中设置为1的位的数量：<pre class="crayon-plain-tag">BITCOUNT key [start end]</pre> 。start为起始字节索引，end为终止字节索引，闭区间</p>
<p>示例：</p>
<p><pre class="crayon-plain-tag">SETBIT BS 7 1
SETBIT BS 6 1
SETBIT BS 15 1
GET BS             # "\x03\x01"
BITCOUNT BS 0 0    # 2</pre>
</td>
</tr>
<tr>
<td class=" blog_h3">BITFIELD</td>
<td>将Redis字符串看作位的数组，可以处理任意位宽的整数位、任意非对齐的偏移。该命令可以一次性操控多个比特位</td>
</tr>
<tr>
<td class=" blog_h3">BITOP</td>
<td>
<p>对多个键的值进行按位的操作：<pre class="crayon-plain-tag">BITOP operation destkey key [key ...]</pre> </p>
<p>支持的operation包括AND, OR, XOR,NOT</p>
<p>如果某个参数字符串的长度不足，自动补零</p>
<p>返回值：destkey的值的长度</p>
</td>
</tr>
<tr>
<td class=" blog_h3">BITPOS</td>
<td>
<p>返回字符串中第一个被设置为1或者0的位的偏移量：<pre class="crayon-plain-tag">BITPOS key bit [start] [end]</pre> </p>
</td>
</tr>
<tr>
<td class=" blog_h3">BLPOP</td>
<td>
<p>阻塞性的列表弹出操作，LPOP的阻塞版本。如果列表中没有可用元素，则当前连接被阻塞，直到超时或元素可用</p>
<p>格式：<pre class="crayon-plain-tag">BLPOP listkey [listkey ...] timeout</pre> </p>
<p>第一个非空列表的第一个元素将被弹出，返回一个数组，如果成功弹出，则第一个元素是被操作列表的key，第二个元素是弹出的值；如果弹出失败，则元素为nil</p>
</td>
</tr>
<tr>
<td class=" blog_h3">BRPOP</td>
<td>与上面类似，但是弹出的是尾部（R）元素而不是头部（L）</td>
</tr>
<tr>
<td class=" blog_h3">BRPOPLPUSH</td>
<td>
<p>格式：<pre class="crayon-plain-tag">BRPOPLPUSH source destination timeout</pre> </p>
<p>RPOPLPUSH的阻塞版本，如果source包含元素或者在MULTI/EXEC块中使用，此命令的行为与RPOPLPUSH一致</p>
<p>如果source为空，则在超时前或者其它客户端向source压入数据前，连接会一直阻塞。timeout设置为0则一直阻塞</p>
</td>
</tr>
<tr>
<td class=" blog_h3">CLIENT KILL</td>
<td>
<p>格式：</p>
<pre class="crayon-plain-tag">CLIENT KILL [ip:port] [ID client-id] [TYPE normal|master|slave|pubsub] 
            [ADDR ip:port] [SKIPME yes/no]</pre>
<p> 关闭指定的客户端连接，ip:port参数必须和CLIENT LIST命令输出的addr列匹配</p>
</td>
</tr>
<tr>
<td class=" blog_h3">CLIENT LIST</td>
<td>
<p>以可读格式输出客户端连接的基本、统计信息。输出列：<br />id，客户端的标识符<br />addr，地址:端口<br />fd，与套接字对应的文件描述符<br />age，连接总计持续的秒数<br />idle，连接已经空闲的秒数<br />flags，客户端标记<br />db，当前数据库ID<br />sub，频道订阅数量<br />psub，基于模式匹配的订阅数量<br />multi，在MULTI/EXEC 上下文中的命令数量<br />qbuf，查询缓冲长度，0表示没有悬挂的查询<br />qbuf-free，查询缓冲中空闲的长度，0表示查询缓冲已满<br />obl，输出缓冲的长度<br />oll，输出列表的长度（当缓冲满了的时候，答复在此列表排队）<br />omem，输出缓冲的内存用量<br />events，文件描述符事件<br />cmd，最后一次执行的命令</p>
<p>客户端标记可以是以下值的组合：<br />O，客户端是MONITOR模式下的slave<br />S，客户端是一个正常模式的slave服务器<br />M，客户端是一个master<br />x，客户端处于MULTI/EXEC上下文中<br />b，客户端正在等待阻塞的操作完成<br />d，一个watched的键已经被修改，EXEC将失败<br />c，在写入完整的答复后，连接将被关闭<br />u，客户端是非阻塞的<br />U，客户端通过UNIX域套接字连接<br />r，客户端基于集群节点处于只读模式<br />A，客户端将被尽快关闭<br />N，没有特别的标记被设置</p>
<p>文件描述符事件：<br />r，客户端套接字可读<br />w，客户端套接字可写</p>
</td>
</tr>
<tr>
<td class=" blog_h3">CLIENT GETNAME</td>
<td>获得当前连接的名称，该名称由CLIENT SETNAME设定</td>
</tr>
<tr>
<td class=" blog_h3">CLIENT PAUSE</td>
<td>
<p>格式：<pre class="crayon-plain-tag">CLIENT PAUSE timeout</pre> ，在指定的毫秒数内，暂停所有客户端。具体行为：</p>
<ol>
<li>暂停普通客户端、Pub/Sub客户端的所有悬挂命令。但是与Slave的交互仍然会正常进行</li>
<li>该命令会尽快的返回OK，因而此命令不会导致调用者客户端被自己暂停</li>
<li>当指定的时间过去后，所有客户端不再被阻塞，所有客户端的查询缓冲中累积的命令会被处理</li>
</ol>
<p>该命令可以让客户端受控的切换其所连接到的服务器，例如管理员可以：</p>
<ol>
<li>暂停所有客户端</li>
<li>等待数秒，确保Slave处理了来自Master的最后的复制流（replication stream）</li>
<li>把一个Slave切换为Master</li>
<li>重新配置clients，让其连接到新的Master</li>
</ol>
</td>
</tr>
<tr>
<td class=" blog_h3">CLIENT REPLY</td>
<td>
<p>格式：<pre class="crayon-plain-tag">CLIENT REPLY ON|OFF|SKIP</pre> </p>
<p>可以用来禁止来自Redis服务器的任何答复信息，在客户端发送fire and forget命令时、载入大量数据时，该命令可以提升效能。取值ON表示启用客户端回复，OFF表示关闭，SKIP表示跳过当前命令之后所有命令的回复</p>
</td>
</tr>
<tr>
<td class=" blog_h3">CLIENT SETNAME</td>
<td>设置连接的名称</td>
</tr>
<tr>
<td class=" blog_h3">CLUSTER ADDSLOTS</td>
<td>
<p>格式：<pre class="crayon-plain-tag">CLUSTER ADDSLOTS slot [slot ...]</pre> </p>
<p>此命令用于修改节点（Node）的集群配置视图。它<span style="background-color: #c0c0c0;">为接受命令的Redis节点添加若干Hash slot</span>。如果操作成功，节点会把Hash slots映射给自己，并开始<span style="background-color: #c0c0c0;">广播新的配置</span>。需要注意：</p>
<ol>
<li>仅在从接受命令的节点的角度来看，所有Slot目前尚未分配时，命令才会成功。如果Slot当前已经分配给其它节点了，则节点拒绝获得Slot的所有权</li>
<li>如果同一个Slot在命令参数中多次指定，命令失败</li>
<li>如果某个Slot被设置为importing，则节点分配了该Slot后，importing状态被清除</li>
</ol>
<p>示例：</p>
<pre class="crayon-plain-tag">CLUSTER ADDSLOTS 1 2 3
# OK
CLUSTER ADDSLOTS 1 2 3
# ERR Slot 1 is already busy</pre>
<p>该命令仅在集群模式下（cluster mode ）可用，用来实现：</p>
<ol>
<li>创建一个新集群时，使用该命令来初始化Master nodes，在这些节点之间分配Hash Slots</li>
<li>修复一个被破坏的集群，此集群中某些Slots没有被分配</li>
</ol>
<p>注意，当节点给自己分配一系列Slot时，它会在心跳包的头中传播这一信息。然而，其它节点仅仅在：</p>
<ol>
<li>没有认为Slot已经分配给其它节点</li>
<li>或者，传播信息的节点的配置信息更加新的时候</li>
</ol>
<p>才接受这些信息</p>
</td>
</tr>
<tr>
<td class=" blog_h3">CLUSTER COUNT-FAILURE-REPORTS</td>
<td>
<p>返回指定节点未过期的错误报告（failure reports ）的数量。<span style="background-color: #c0c0c0;">Redis集群基于错误报告来把节点的状态从PFAIL（目标节点不可达）升级到FAIL（集群中的大部分Master同意在一个时间窗内，目标节点不可达）</span></p>
<p>关于PFAIL/FAIL的一些细节：</p>
<ol>
<li>当超过配置项<span style="background-color: #c0c0c0;">node timeout</span>（这是Redis集群的基础配置项）之后，节点A到节点B不可达，<span style="background-color: #c0c0c0;">则A标记B为PFAIL</span></li>
<li><span style="background-color: #c0c0c0;">处于PFAIL状态的节点，会写入心跳报文的gossip段</span></li>
<li>每当一个<span style="background-color: #c0c0c0;">节点C接收来自节点A的gossip后，它创建一个错误报告（并在需要时更新TTL），记住节点A认为节点B为PFAIL状态</span></li>
<li>每个错误报告的<span style="background-color: #c0c0c0;">TTL为node timeout的两倍大小</span></li>
<li>在一个特定的时间点，当前节点认为节点B为PFAIL，并且从收集的错误报告中看，<span style="background-color: #c0c0c0;">大多数Master节点（如果当前节点是Master，包含在内）也认为节点B为PFAIL，则当前节点标记B为FAIL，并且广播一个消息，迫使所有可达节点都标记B为FAIL</span></li>
</ol>
</td>
</tr>
<tr>
<td class=" blog_h3">CLUSTER COUNTKEYSINSLOT</td>
<td>返回指定的Redis集群Hash Slot中存储的键的数量。该命<span style="background-color: #c0c0c0;">令仅仅查询本地数据集</span>，因此在没有分配目标Slot的节点上运行此命令总是返回0</td>
</tr>
<tr>
<td class=" blog_h3">CLUSTER DELSLOTS</td>
<td>
<p>格式：<pre class="crayon-plain-tag">CLUSTER DELSLOTS slot [slot ...]</pre> </p>
<p>在Redis集群中，每个节点都跟踪哪个Master负责Serving某个特定的Slot。该命令让目标节点<span style="background-color: #c0c0c0;">忘记谁负责Serving参数指定的Slots，这些Slots变为Unbound</span>。另外，<span style="background-color: #c0c0c0;">尚未被分配给任何Master节点的Slot天然处于Unbound状态</span>。如果Slot已经被分配，在那些尚未接收到通知（心跳或者更新报文）的节点来看，Slot也处于Unbound状态。</p>
<p>认为某个Slot为Unbound的节点，一旦<span style="background-color: #c0c0c0;">收到其它节点声明自己是该Slot所有者</span>的心跳报文，就会立即<span style="background-color: #c0c0c0;">建立节点与Slot之间的关联</span></p>
<p>如果某个节点收到包含配置信息的心跳/更新报文，且此报文的时间比<span style="background-color: #c0c0c0;">该节点自身的配置时间更加新，则节点-Slot关联会被重新建立</span></p>
</td>
</tr>
<tr>
<td class=" blog_h3">CLUSTER FAILOVER</td>
<td>
<p>格式：<pre class="crayon-plain-tag">CLUSTER FAILOVER [FORCE|TAKEOVER]</pre> </p>
<p>该命令<span style="background-color: #c0c0c0;">仅可以在Redis集群的Slave节点上执行</span>。可以强制一个Slave启动手工的故障转移（针对它的Master节点）</p>
<p>手工故障转移是一种特殊的故障转移，通常在未发生实际错误但期望安全的（不招致丢失数据的窗口期）把Master与它的某个Slave（接收命令的节点）进行Swap时执行。工作方式如下：</p>
<ol>
<li>Slave通知Master，停止处理来自客户端的查询请求</li>
<li>Master回复Slave，告知当前的复制偏移量（replication offset）</li>
<li>Slave等待自己的复制偏移量和Master匹配</li>
<li>Slave触发一个故障转移，从大多数Master获得一个新的配置纪元（configuration epoch），并广播新的配置信息</li>
<li>旧的Master接收配置更新，Unblock客户端连接，回复重定向信息，导致客户端与新的Master进行交互</li>
</ol>
<p>这样，客户端可以原子的从旧的Master切换到新的Master</p>
<p>FORCE选项：可以在Master宕机的情况下手工故障转移（failover）。使用此选项时，Slave不会与Master握手，而是直接从上面的第4步开始执行。尽管如此，Slave还是需要联系上大部分Master节点，以确保故障转移被授权，并未Slave生成新的配置时间点</p>
<p>TAKEOVER选项：不经过集群共识（cluster consensus）的手工故障转移。某些情况下，我们希望不经过其它Master同意的情况下就执行故障转移。</p>
</td>
</tr>
<tr>
<td class=" blog_h3">CLUSTER FORGET</td>
<td>
<p>格式：<pre class="crayon-plain-tag">CLUSTER FORGET node-id</pre> </p>
<p><span style="background-color: #c0c0c0;">从集群中移除某个节点</span>。接收此命令的节点的Node table中的某个节点将被移除</p>
<p>集群中的其它节点都需要知晓节点被移除的事实，因而<span style="background-color: #c0c0c0;">该命令需要被发送给所有其它节点</span>。仅仅从Node table中移除节点是不够的，<span style="background-color: #c0c0c0;">Redis还维护一个禁止清单（banlist）</span>，<span style="background-color: #c0c0c0;">防止心跳信息中的Gossip段把被移除的节点重新加入</span></p>
</td>
</tr>
<tr>
<td class=" blog_h3">CLUSTER COUNTKEYSINSLOT</td>
<td>
<p>格式：<pre class="crayon-plain-tag">CLUSTER COUNTKEYSINSLOT slo</pre> </p>
<p>返回指定Slot中本地键的数量，注意该命令仅仅查询本地数据集，因此针对非关联到目标Slot的Node执行此命令，总是返回0</p>
</td>
</tr>
<tr>
<td class=" blog_h3">CLUSTER INFO</td>
<td>获取Redis集群的关键参数：<br />cluster_state，如果节点可以接收查询，为ok；如果至少一个Hash Slot没有关联到Node（Unbound）或者处于错误状态（关联的Node被标记为FAIL），为fail；如果当前节点不能抵达大部分的Master，为fail<br />cluster_slots_assigned，已经关联到Node的Slot数量<br />cluster_slots_ok，已经关联到Node，且Node不处于PFAIL、FAIL状态到Slot数量<br />cluster_slots_pfail，关联到PFAIL节点的Slot数量<br />cluster_slots_fail，关联到FAIL节点的Slot数量<br />cluster_known_nodes，集群中已知节点的数量，包括正处于HANDSHAKE状态的节点<br />cluster_size，至少关联一个Hash Slot的Master节点的数量<br />cluster_current_epoch，本地的Current Epoch变量的值，此变量用于在故障转移时创建唯一的递增的版本号<br />cluster_my_epoch，本地的Config Epoch变量的值，当前节点的配置的版本<br />cluster_stats_messages_sent，通过集群节点-节点总线发送的消息数量<br />cluster_stats_messages_received，通过集群节点-节点总线接收的消息总量</td>
</tr>
<tr>
<td class=" blog_h3">CLUSTER KEYSLOT</td>
<td>
<p>格式：<pre class="crayon-plain-tag">CLUSTER KEYSLOT key</pre> </p>
<p>返回指定的键被散列到的Slot的整数编号</p>
</td>
</tr>
<tr>
<td class=" blog_h3">CLUSTER GETKEYSINSLOT</td>
<td>
<p>格式：<pre class="crayon-plain-tag">CLUSTER GETKEYSINSLOT slot count</pre> </p>
<p>从指定的Slot中获得一定数量的键</p>
</td>
</tr>
<tr>
<td class=" blog_h3">CLUSTER MEET</td>
<td>
<p>格式：<pre class="crayon-plain-tag">CLUSTER MEET ip port</pre> </p>
<p>用于<span style="background-color: #c0c0c0;">连接某个启用了集群支持的节点，将其加入到当前集群</span>中</p>
<p>基本的思想是，<span style="background-color: #c0c0c0;">节点之间默认是相互不信任的，被看作是未知（Unknown）节点</span>。分属于不同集群的节点通常不会因为管理员的误操作、网络地址改变而混在一起</p>
<p>因此，为了让一个节点接受另外一个节点作为集群的成员，有两种方法：</p>
<ol>
<li>系统管理员发起CLUSTER MEET命令，强迫一个节点接受另一个</li>
<li>一个已知节点通过gossip段发送节点列表，如果<span style="background-color: #c0c0c0;">接收报文的节点信任发送节点</span>，则会处理gossip中的节点，然后与其中未知的节点进行握手</li>
</ol>
<p>尽管Redis集群需要构成节点的完整网络（Mesh），但是<span style="background-color: #c0c0c0;">并非你需要向每对节点发送MEET命令</span>，只需要保证任意两个节点可以通过<span style="background-color: #c0c0c0;">已知节点链（Known Nodes Chain）可达</span>。此外，<span style="background-color: #c0c0c0;">MEET命令只需要单向的发送</span></p>
<p>&nbsp;</p>
</td>
</tr>
<tr>
<td class=" blog_h3">
<p>CLUSTER NODES</p>
</td>
<td>
<p>集群中每个节点，<span style="background-color: #c0c0c0;">对于集群的配置有自己的视图</span>。配置信息包括：<span style="background-color: #c0c0c0;">已知节点的集合、当前节点与已知节点的连接状态、已知节点的标记／属性／分配的Slot，等等</span></p>
<p>该命令的输出包括上述配置信息</p>
</td>
</tr>
<tr>
<td class=" blog_h3">CLUSTER REPLICATE</td>
<td>
<p>格式：<pre class="crayon-plain-tag">CLUSTER REPLICATE node-id</pre> </p>
<p>配置<span style="background-color: #c0c0c0;">当前节点，将其作为Master节点（node-id指定）的Slave</span>。如果当前节点是空的Master，其角色从Master变为Slave</p>
<p>当一个节点变成某个Master的Slave后，不需要通知集群中的其它节点，因为节点之间交换的心跳报文会自动传播这一配置变更</p>
<p>如果当前节点是Slave，当满足以下条件时，命令总是被接受：</p>
<ol>
<li>node-id存在于节点表中</li>
<li>node-id不是当前节点的标识符</li>
<li>node-id对应了一个Master的标识符</li>
</ol>
<p>如果当前节点是Master，则仅当满足以下条件时，节点才被转化为Slave并成功返回：</p>
<ol>
<li>node-id没有关联到任何Hash Slot</li>
<li>node-id是空白的，其键空间中没有存放任何键</li>
</ol>
</td>
</tr>
<tr>
<td class=" blog_h3">CLUSTER RESET</td>
<td>
<p>格式：<pre class="crayon-plain-tag">CLUSTER RESET [HARD|SOFT]</pre> </p>
<p>重置一个集群节点，HARD|SOFT决定重置动作的激烈程度，默认SOFT</p>
<p>该命令不能针对存放了键的Master节点使用，如果要重置Master节点，需要先清空其中的键，可以先使用FLUSH ALL命令清空</p>
<p>该命令对节点的作用：</p>
<ol>
<li><span style="background-color: #c0c0c0;">忘记集群中所有其它节点</span></li>
<li>所有<span style="background-color: #c0c0c0;">Slot分配信息被清空</span></li>
<li>如果节点是<span style="background-color: #c0c0c0;">Slave，它变成一个空白的Master</span></li>
<li>如果指定HARD，则节点的标识符被重置</li>
<li>如果指定HARD，则currentEpoch、configEpoch变量重置为0</li>
<li>新的配置信息持久化到该节点的集群配置文件中</li>
</ol>
</td>
</tr>
<tr>
<td class=" blog_h3">CLUSTER SAVECONFIG</td>
<td>
<p>强制一个节点在磁盘上存储nodes.conf文件。命令返回前fsync(2)系统调用被执行以确保持久化成功</p>
<p>如果nodes.conf文件因为某种原因丢失，可以使用该命令重新生成 </p>
</td>
</tr>
<tr>
<td class=" blog_h3">CLUSTER SET-CONFIG-EPOCH</td>
<td>
<p>格式：<pre class="crayon-plain-tag">CLUSTER SET-CONFIG-EPOCH config-epoch</pre> </p>
<p>设置一个新节点的config epoch，仅当满足以下条件时生效：</p>
<ol>
<li>目标节点的节点表（node table）为空</li>
<li>目标节点当前的config epoch为零</li>
</ol>
<p>之所以要求这两个条件，是由于手工修改config epoch是不安全的。此变量的值更大的节点，在声明Slot所有权时，具有更高的优先级</p>
</td>
</tr>
<tr>
<td class=" blog_h3">CLUSTER SETSLOT</td>
<td>
<p>格式：</p>
<pre class="crayon-plain-tag">CLUSTER SETSLOT slot IMPORTING|MIGRATING|STABLE|NODE [node-id]
CLUSTER SETSLOT &lt;slot&gt; MIGRATING &lt;destination-node-id&gt;
CLUSTER SETSLOT &lt;slot&gt; IMPORTING &lt;source-node-id&gt;
CLUSTER SETSLOT &lt;slot&gt; STABLE
CLUSTER SETSLOT &lt;slot&gt; NODE &lt;node-id&gt;  </pre>
<p><span style="background-color: #c0c0c0;">改变</span>接收命令节点<span style="background-color: #c0c0c0;">所看到的Slot的状态</span>，包括以下子命令：</p>
<ol>
<li>MIGRATING，设置Slot状态为migrating</li>
<li>IMPORTING，设置Slot状态为importing</li>
<li>STABLE，清空Slot的migrating或者importing状态</li>
<li>NODE，绑定Slot到其它节点</li>
</ol>
<p>该命令在集群的在线重分区（live resharding）操作中有用，重分区可以把某个Slot完整的迁移到另外一个节点上。通常你会在源节点执行MIGRATING子命令，然后在目标节点执行IMPORTING子命令</p>
</td>
</tr>
<tr>
<td class=" blog_h3">CLUSTER SLAVES</td>
<td>
<p>格式：<pre class="crayon-plain-tag">CLUSTER SLAVES node-id</pre> </p>
<p>列出指定Master节点的Slave。如果接收命令节点的节点表中不存在node-id或者node-id不是Master，命令失败</p>
</td>
</tr>
<tr>
<td class=" blog_h3">CLUSTER SLOTS</td>
<td>列出Hash Slot与Redis实例映射的详细信息</td>
</tr>
<tr>
<td class=" blog_h3">COMMAND</td>
<td>列出所有Redis命令的详细信息</td>
</tr>
<tr>
<td class=" blog_h3">COMMAND COUNT</td>
<td>统计命令的数量 </td>
</tr>
<tr>
<td class=" blog_h3">COMMAND INFO</td>
<td>显示一个或者多个命令的详细信息</td>
</tr>
<tr>
<td class=" blog_h3">CONFIG GET</td>
<td>
<p>格式：<pre class="crayon-plain-tag">CONFIG GET parameter</pre> </p>
<p>该命令用于读取运行中Redis服务器的配置参数，可以使用通配符* </p>
</td>
</tr>
<tr>
<td class=" blog_h3">CONFIG REWRITE </td>
<td>重写当前Redis服务使用的redis.conf配置文件，进行最小化的修订以反映当前运行中的服务正在使用的配置信息</td>
</tr>
<tr>
<td class=" blog_h3">CONFIG SET</td>
<td>
<p>格式：<pre class="crayon-plain-tag">CONFIG SET parameter value</pre>  </p>
<p>在运行时重新配置Redis服务</p>
</td>
</tr>
<tr>
<td class=" blog_h3">CONFIG RESETSTAT</td>
<td>重置INFO命令所报告的统计信息</td>
</tr>
<tr>
<td class=" blog_h3">DBSIZE</td>
<td>返回当前选中的数据库中键的数量</td>
</tr>
<tr>
<td class=" blog_h3">DECR</td>
<td>递减指定键的值，如果指定的键不存在，在操作前将其值设置为0 </td>
</tr>
<tr>
<td class=" blog_h3">DECRBY</td>
<td>
<p>格式：<pre class="crayon-plain-tag">DECRBY key decrement</pre> </p>
<p>减少指定键的值，如果指定的键不存在，在操作前将其值设置为0 </p>
</td>
</tr>
<tr>
<td class=" blog_h3">DEL</td>
<td>删除一个或者多个键</td>
</tr>
<tr>
<td class=" blog_h3">DISCARD</td>
<td>刷出（Flush）事务中排队的所有命令，并且恢复连接状态为Normal</td>
</tr>
<tr>
<td class=" blog_h3">DUMP</td>
<td>串行化指定键的值，以Redis特有的格式返回给用户</td>
</tr>
<tr>
<td class=" blog_h3">ECHO</td>
<td>让服务器回响</td>
</tr>
<tr>
<td class=" blog_h3">EVAL</td>
<td>
<p>格式：<pre class="crayon-plain-tag">EVAL script numkeys key [key ...] arg [arg ...]</pre></p>
<p>基于内置的脚本解释器来估算脚本的值</p>
</td>
</tr>
<tr>
<td class=" blog_h3">EVALSHA</td>
<td>基于SHA1摘要检索缓存在服务器上的脚本，并估算其值</td>
</tr>
<tr>
<td class=" blog_h3">EXEC</td>
<td>
<p>在事务中执行先前排队的命令，然后恢复连接为正常状态</p>
<p>使用WATCH命令时，仅在被监控键未被修改时EXEC才会执行命令</p>
</td>
</tr>
<tr>
<td class=" blog_h3">EXISTS</td>
<td>
<p>格式：<pre class="crayon-plain-tag">EXISTS key [key ...]</pre></p>
<p>判断指定的键是否存在，返回存在的键的数量</p>
</td>
</tr>
<tr>
<td class=" blog_h3">EXPIRE</td>
<td>
<p>格式：<pre class="crayon-plain-tag">EXPIRE key seconds</pre></p>
<p>设置指定键的超时，时间到达后，目标键被自动删除。在Redis的术语体系中，带有超时的键被成为易变键（volatile）</p>
<p>超时仅可以被删除/覆盖键内容的命令清除，包括DEL、SET、GETSET等。<span style="background-color: #c0c0c0;">修改键的值，而不是替换它</span>，不会改变超时设置，因此INCR、LPUSH等操作不会改变超时设置</p>
<p>PERSIST命令用于清除超时设置</p>
<p>RENAME命令修改一个键值时，旧有的超时设置被重命名后的键继承</p>
</td>
</tr>
<tr>
<td class=" blog_h3">EXPIREAT</td>
<td>
<p>格式：<pre class="crayon-plain-tag">EXPIREAT key timestamp</pre></p>
<p>在指定的时间让键超时，时间到达后，目标键被自动删除。timestamp是秒为单位的UNIX时间戳</p>
</td>
</tr>
<tr>
<td class=" blog_h3">FLUSHALL</td>
<td>
<p>格式：<pre class="crayon-plain-tag">FLUSHALL [ASYNC]</pre></p>
<p>从现存的数据库中删除所有的键，包括当前选择的数据库</p>
<p>ASYNC表示异步执行，不会阻塞服务器</p>
<p>在集群模式下仅仅会处理当前分片的数据</p>
</td>
</tr>
<tr>
<td class=" blog_h3">FLUSHDB</td>
<td>
<p>格式：<pre class="crayon-plain-tag">FLUSHDB [ASYNC]</pre></p>
<p>从当前选中的数据库中删除所有的键</p>
</td>
</tr>
<tr>
<td class=" blog_h3">GEOADD</td>
<td>
<p>格式：<pre class="crayon-plain-tag">GEOADD key longitude latitude member [longitude latitude member ...]</pre></p>
<p>添加指定的地理信息条目（经度、纬度、名称）到指定的键，值的存储类型为有序集合（sorted set），便于之后基于径向查询命令取回</p>
</td>
</tr>
<tr>
<td class=" blog_h3">GEOHASH</td>
<td>
<p>格式：<pre class="crayon-plain-tag">GEOHASH key member [member ...]</pre></p>
<p>返回代表位置的HASH，key为先前基于GEOADD添加的地理信息条目，member则为某个条目的名称</p>
</td>
</tr>
<tr>
<td class=" blog_h3">GEOPOS</td>
<td>
<p>格式：<pre class="crayon-plain-tag">GEOPOS key member [member ...]</pre></p>
<p>返回位置信息，经度、纬度组成的数组</p>
</td>
</tr>
<tr>
<td class=" blog_h3">GEODIST</td>
<td>
<p>格式：<pre class="crayon-plain-tag">GEODIST key member1 member2 [unit]</pre></p>
<p>返回两个地理信息条目之间的距离</p>
</td>
</tr>
<tr>
<td class=" blog_h3">GEORADIUS</td>
<td>
<p>格式：<pre class="crayon-plain-tag">GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]</pre></p>
<p>径向查询，返回以指定经纬度为原点，radius半径范围内的地理信息条目</p>
</td>
</tr>
<tr>
<td class=" blog_h3">GEORADIUSBYMEMBER</td>
<td>格式：<pre class="crayon-plain-tag">GEORADIUSBYMEMBER key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]</pre></td>
</tr>
<tr>
<td class=" blog_h3">GET</td>
<td>
<p>格式：<pre class="crayon-plain-tag">GET key</pre></p>
<p>返回指定键对应的值，如果键不存在，返回nil这个特殊值</p>
</td>
</tr>
<tr>
<td class=" blog_h3">GETBIT</td>
<td>
<p>格式：<pre class="crayon-plain-tag">GETBIT key offset</pre></p>
<p>返回值在指定偏移量处的二进制位，如果offset超过长度，则一律返回0</p>
</td>
</tr>
<tr>
<td class=" blog_h3">GETRANGE</td>
<td>
<p>格式：<pre class="crayon-plain-tag">GETRANGE key start end</pre></p>
<p>在2.0之前的版本中命令名为SUBSTR，用于获得一个字符串的子串</p>
</td>
</tr>
<tr>
<td class=" blog_h3">GETSET</td>
<td>
<p>格式：<pre class="crayon-plain-tag">GETSET key value</pre></p>
<p>设置key的值为value，并返回旧的值</p>
</td>
</tr>
<tr>
<td class=" blog_h3">HDEL</td>
<td>
<p>格式：<pre class="crayon-plain-tag">HDEL key field [field ...]</pre></p>
<p>从指定的HASH中删除字段，此HASH以key为键存储在Redis中</p>
<p>在2.4版本之前，一次只能删除一个字段</p>
</td>
</tr>
<tr>
<td class=" blog_h3">HEXISTS</td>
<td>
<p>格式：<pre class="crayon-plain-tag">HEXISTS key field</pre></p>
<p>查询指定的HASH是否存在字段，返回0/1</p>
</td>
</tr>
<tr>
<td class=" blog_h3">HGET</td>
<td>
<p>格式：<pre class="crayon-plain-tag">HGET key field</pre></p>
<p>从指定的HASH中获取一个字段的值</p>
</td>
</tr>
<tr>
<td class=" blog_h3">HGETALL</td>
<td>
<p>格式：<pre class="crayon-plain-tag">HGETALL key</pre></p>
<p>以数组的形式返回一个HASH的所有字段值，如果key不存在则返回一个空数组</p>
</td>
</tr>
<tr>
<td class=" blog_h3">HINCRBY</td>
<td>
<p>格式：<pre class="crayon-plain-tag">HINCRBY key field increment</pre></p>
<p>增加一个HASH字段的值</p>
</td>
</tr>
<tr>
<td class=" blog_h3">HINCRBYFLOAT</td>
<td>
<p>格式：<pre class="crayon-plain-tag">HINCRBYFLOAT key field increment</pre></p>
<p>增加一个HASH字段的值，支持浮点数</p>
</td>
</tr>
<tr>
<td class=" blog_h3">HKEYS </td>
<td>
<p>格式：<pre class="crayon-plain-tag">HKEYS key</pre></p>
<p>以数组的形式返回HASH的所有键，如果key不存在则返回空数组</p>
</td>
</tr>
<tr>
<td class=" blog_h3">HLEN</td>
<td>
<p>格式：<pre class="crayon-plain-tag">HLEN key</pre></p>
<p>返回HASH字段的数量</p>
</td>
</tr>
<tr>
<td class=" blog_h3">HMGET</td>
<td>
<p>格式：<pre class="crayon-plain-tag">HMGET key field [field ...] </pre></p>
<p>同时得到多个HASH字段的值，返回数组</p>
</td>
</tr>
<tr>
<td class=" blog_h3">HMSET</td>
<td>
<p>格式：<pre class="crayon-plain-tag">HMSET key field value [field value ...]</pre></p>
<p>同时设置多个HASH字段的值，如果HASH不存在自动创建，现有的字段值会被覆盖</p>
</td>
</tr>
<tr>
<td class=" blog_h3">HSET</td>
<td>
<p>格式：<pre class="crayon-plain-tag">HSET key field value</pre></p>
<p>设置一个HASH字段的值，如果目标字段已经存在则返回0，否则返回1</p>
</td>
</tr>
<tr>
<td class=" blog_h3">HSETNX</td>
<td>
<p>格式：<pre class="crayon-plain-tag">HSETNX key field value</pre></p>
<p>设置HASH字段的值，仅仅在field 不存在的时候执行</p>
</td>
</tr>
<tr>
<td class=" blog_h3">HSTRLEN</td>
<td>
<p>格式：<pre class="crayon-plain-tag">HSTRLEN key field</pre></p>
<p>返回HASH字段的字符串长度</p>
</td>
</tr>
<tr>
<td class=" blog_h3">HVALS</td>
<td>
<p>格式：<pre class="crayon-plain-tag">HVALS key</pre></p>
<p>以数组的形式返回一个HASH中所有字段的值</p>
</td>
</tr>
<tr>
<td class=" blog_h3">INCR</td>
<td>
<p>格式：<pre class="crayon-plain-tag">INCR key</pre></p>
<p>增加一个键的值，如果键不存在，则在操作前将其值设置为0</p>
<p>注意：这是一个字符串操作，因为Redis不提供专门的整数类型，存在key下的字符串被看做是64位无符号整数</p>
</td>
</tr>
<tr>
<td class=" blog_h3">INCRBY</td>
<td>
<p>格式：<pre class="crayon-plain-tag">INCRBY key increment</pre></p>
<p>增加一个键的值</p>
</td>
</tr>
<tr>
<td class=" blog_h3">INCRBYFLOAT</td>
<td>
<p>格式：<pre class="crayon-plain-tag">INCRBYFLOAT key increment</pre></p>
<p>增加一个键的值，支持浮点数</p>
</td>
</tr>
<tr>
<td class=" blog_h3">INFO</td>
<td>
<p>格式：<pre class="crayon-plain-tag">INFO [section]</pre></p>
<p>返回服务器的基本信息、统计信息，以可读并易于机器解析的格式输出</p>
<p>section用于指定需要获取的信息的段：<br />server  Redis服务器的基本信息<br />clients  客户端连接信息<br />memory  内存消耗相关的信息<br />persistence  RDB和AOF相关的信息<br />stats  一般性的统计信息<br />replication  主从复制相关信息<br />cpu  CPU消耗统计信息<br />commandstats  Redis命令的统计信息<br />cluster  Redis集群相关信息<br />keyspace  数据库相关信息</p>
</td>
</tr>
<tr>
<td class=" blog_h3">KEYS</td>
<td>
<p>格式：<pre class="crayon-plain-tag">KEYS pattern</pre></p>
<p>返回所有匹配指定pattern的键的数组，此操作的效率较高，但是在生产环境下仍然要小心使用以防止对性能造成影响。如果希望在键空间的子集中查找键，最好考虑使用SCAN、SETS</p>
<p>pattern支持有限的几种统配符：</p>
<ol>
<li>?l，匹配单个字符</li>
<li>*l，匹配0-N个字符</li>
<li>[ae]，匹配a或e</li>
<li>[^e]，匹配非e字符</li>
<li>[a-b]，匹配a到b之间的字符</li>
</ol>
<p>此命令和集群模式不兼容，仅仅能返回当前节点上的数据</p>
</td>
</tr>
<tr>
<td class=" blog_h3">LASTSAVE</td>
<td>返回上次数据保存命令执行的UNIX时间。客户端可以发起BGSAVE命令进行数据库保存</td>
</tr>
<tr>
<td class=" blog_h3">LINDEX</td>
<td>
<p>格式：<pre class="crayon-plain-tag">LINDEX key index</pre></p>
<p>返回指定索引上的元素值，索引以0开始。如果index超出索引范围则返回nil</p>
</td>
</tr>
<tr>
<td class=" blog_h3">LINSERT</td>
<td>
<p>格式：<pre class="crayon-plain-tag">LINSERT key BEFORE|AFTER pivot value</pre></p>
<p>在参考值pivot之前/后插入指定的值</p>
</td>
</tr>
<tr>
<td class=" blog_h3">LLEN</td>
<td>
<p>格式：<pre class="crayon-plain-tag">LLEN key</pre></p>
<p>返回指定列表的长度，如果key不存在则当做空列表，如果key存储的不是列表则返回错误</p>
</td>
</tr>
<tr>
<td class=" blog_h3">LPOP</td>
<td>
<p>格式：<pre class="crayon-plain-tag">LPOP key</pre></p>
<p>移除列表的第一个元素并返回之。如果key不存在则返回nil</p>
</td>
</tr>
<tr>
<td class=" blog_h3">LPUSH</td>
<td>
<p>格式：<pre class="crayon-plain-tag">LPUSH key value [value ...]</pre></p>
<p>在列表的头部插入指定的元素，如果key不存在则自动创建列表。指定多个value则最后一个在操作后成为第一个元素。返回操作后列表的长度</p>
</td>
</tr>
<tr>
<td class=" blog_h3">LPUSHX</td>
<td>
<p>格式：<pre class="crayon-plain-tag">LPUSHX key value</pre></p>
<p>与上面的命令类似，但是仅仅在key存在且其值是一个列表时才只需插入操作</p>
</td>
</tr>
<tr>
<td class=" blog_h3">LRANGE</td>
<td>
<p>格式：<pre class="crayon-plain-tag">LRANGE key start stop</pre></p>
<p>从列表中返回一个子列表，从start开始，到stop结束（包含）。如果索引值为负数，则表示倒数，-1表示倒数第一个元素</p>
</td>
</tr>
<tr>
<td class=" blog_h3">LREM</td>
<td>
<p>格式：<pre class="crayon-plain-tag">LREM key count value</pre></p>
<p>从key对应的列表中移除一定数量的值等于value的元素，如果：<br />count为正数，从列表头开始查找<br />count为负数，从列表尾开始查找<br />count为0，移除所有值为value的元素</p>
</td>
</tr>
<tr>
<td class=" blog_h3">LSET</td>
<td>
<p>格式：<pre class="crayon-plain-tag">LSET key index value</pre></p>
<p>设置列表指定索引上的元素值</p>
</td>
</tr>
<tr>
<td class=" blog_h3">LTRIM</td>
<td>
<p>格式：<pre class="crayon-plain-tag">LTRIM key start stop</pre></p>
<p>修剪列表，仅仅保留从start到stop的元素，索引可以为负数</p>
</td>
</tr>
<tr>
<td class=" blog_h3">MGET</td>
<td>
<p>格式：<pre class="crayon-plain-tag">MGET key [key ...]</pre></p>
<p>同时返回多个键的值构成的数组，key不存在的，对应数组元素为nil</p>
</td>
</tr>
<tr>
<td class=" blog_h3">MIGRATE</td>
<td>
<p>格式：<pre class="crayon-plain-tag">MIGRATE host port key|"" destination-db timeout [COPY] [REPLACE] [KEYS key [key ...]]</pre></p>
<p>从源Redis实例传输一个或者多个键到目标Redis实例，如果操作成功则键从源实例删除并存在于目标实例上</p>
<p>该命令是原子的，在操作完成之前，源、目的实例都被阻塞除非超时。在任意时刻，目标键仅仅存在于源或者目的实例上</p>
<p>从3.2开始，可以设置key 为""并通过KEYS子句来指定多个需要迁移的键</p>
<p>该命令在内部，先使用DUMP来串行化目标键，然后在目的实例上执行RESTORE命令，如果RESTORE命令返回OK则源实例执行DEL命令删除键</p>
<p>&nbsp;</p>
</td>
</tr>
<tr>
<td class=" blog_h3">MONITOR</td>
<td>调试命令，流式的显示在Redis服务器上执行的所有命令</td>
</tr>
<tr>
<td class=" blog_h3">MOVE</td>
<td>
<p>格式：<pre class="crayon-plain-tag">MOVE key db</pre></p>
<p>把键从当前选中的数据库转移到指定数据库db。如果key在目目的数据库上存在、或者在源数据库上不存在，什么都不做</p>
</td>
</tr>
<tr>
<td class=" blog_h3">MSET</td>
<td>
<p>格式：<pre class="crayon-plain-tag">MSET key value [key value ...]</pre></p>
<p>同时设置多个键的值</p>
</td>
</tr>
<tr>
<td class=" blog_h3">MSETNX</td>
<td>
<p>格式：<pre class="crayon-plain-tag">MSETNX key value [key value ...]</pre></p>
<p>同时设置多个键的值，仅仅在所有键都不存在的情况下才会执行设置</p>
</td>
</tr>
<tr>
<td class=" blog_h3">MULTI</td>
<td>标记事务块的起点，后续命令会自动排队，直到遇到EXEC命令</td>
</tr>
<tr>
<td class=" blog_h3">OBJECT</td>
<td>
<p>格式：<pre class="crayon-plain-tag">OBJECT subcommand [arguments [arguments ...]]</pre></p>
<p>查看关联到某个键的Redis对象的内部信息</p>
</td>
</tr>
<tr>
<td class=" blog_h3">PERSIST</td>
<td>
<p>格式：<pre class="crayon-plain-tag">PERSIST key</pre></p>
<p>移除键上的超时设置</p>
</td>
</tr>
<tr>
<td class=" blog_h3">PEXPIRE</td>
<td>
<p>格式：<pre class="crayon-plain-tag">PEXPIRE key milliseconds</pre></p>
<p>与EXPIRE命令类似，但是时间以毫秒为单位</p>
</td>
</tr>
<tr>
<td class=" blog_h3">PEXPIREAT</td>
<td>
<p>格式：<pre class="crayon-plain-tag">PEXPIREAT key milliseconds-timestamp</pre></p>
<p>与EXPIREAT类似，但是时间以毫秒为单位</p>
</td>
</tr>
<tr>
<td class=" blog_h3">PING</td>
<td>用于测试连接状态、测量延迟</td>
</tr>
<tr>
<td class=" blog_h3">PSETEX</td>
<td>
<p>格式：<pre class="crayon-plain-tag">PSETEX key milliseconds value</pre></p>
<p>类似SETEX，但是时间以毫秒为单位</p>
</td>
</tr>
<tr>
<td class=" blog_h3">PSUBSCRIBE</td>
<td>
<p>格式：<pre class="crayon-plain-tag">PSUBSCRIBE pattern [pattern ...]</pre></p>
<p>让客户端订阅指定的模式，pattern示例：<br />? 匹配单个字符<br />* 匹配任意个数字符<br />[abc] 匹配abc其中之一</p>
</td>
</tr>
<tr>
<td>PSYNC</td>
<td>向Master申请一条复制流，由Slave调用</td>
</tr>
<tr>
<td class=" blog_h3">PUBSUB</td>
<td>
<p>格式：<pre class="crayon-plain-tag">PUBSUB subcommand [argument [argument ...]]</pre></p>
<p>一个自省命令，用于测试订阅/发布系统的状态。包括子命令：</p>
<p><pre class="crayon-plain-tag">PUBSUB CHANNELS [pattern]</pre></p>
<p>列出当前活动的订阅/发布频道的列表。所谓活动，是指至少具有一个订阅者。如果不指定pattern则所有频道被列出</p>
<p><pre class="crayon-plain-tag">PUBSUB NUMSUB [channel-1 ... channel-N]</pre></p>
<p>返回指定频道的订阅者数量</p>
</td>
</tr>
<tr>
<td class=" blog_h3">PTTL</td>
<td>
<p>格式：<pre class="crayon-plain-tag">PTTL key</pre></p>
<p>以毫秒返回目标键的TTL</p>
</td>
</tr>
<tr>
<td class=" blog_h3">PUBLISH</td>
<td>
<p>格式：<pre class="crayon-plain-tag">PUBLISH channel message</pre></p>
<p>向指定频道发布一个消息</p>
</td>
</tr>
<tr>
<td class=" blog_h3">PUNSUBSCRIBE</td>
<td>
<p>格式：<pre class="crayon-plain-tag">PUNSUBSCRIBE [pattern [pattern ...]]</pre></p>
<p>格局pattern来取消订阅，如果不指定pattern则取消订阅该客户端先前订阅的所有频道</p>
</td>
</tr>
<tr>
<td class=" blog_h3">QUIT</td>
<td>请求服务器关闭连接，当服务器输出所有未决的命令答复后，连接被关闭</td>
</tr>
<tr>
<td class=" blog_h3">RANDOMKEY</td>
<td>从当前选中的数据库中随机返回一个键</td>
</tr>
<tr>
<td class=" blog_h3">READONLY</td>
<td>
<p>对于连接到集群Slave节点的连接，该命令启动只读查询</p>
<p>通常情况下，Slave节点会依据命令牵涉到的Hash Slot来重定向客户端到对应的Master节点。使用该命令后，可能读取到陈旧的数据</p>
<p>如果连接处于readonly模式，仅在命令牵涉到Slave的Master节点所不管理的Hash Slot时，服务器才会向客户端发送重定向信息</p>
</td>
</tr>
<tr>
<td class=" blog_h3">READWRITE</td>
<td>恢复读写模式</td>
</tr>
<tr>
<td class=" blog_h3">RENAME </td>
<td>
<p>格式：<pre class="crayon-plain-tag">RENAME key newkey</pre></p>
<p>重命名指定的键，如果源键不存在返回错误。该命令隐含一个DEL，因此值很大的时候可能消耗较长时间</p>
</td>
</tr>
<tr>
<td class=" blog_h3">RENAMENX </td>
<td>
<p>格式：<pre class="crayon-plain-tag">RENAMENX key newkey</pre></p>
<p>如果newkey不存在，执行RENAME操作</p>
</td>
</tr>
<tr>
<td class=" blog_h3">RESTORE</td>
<td>
<p>格式：<pre class="crayon-plain-tag">RESTORE key ttl serialized-value [REPLACE]</pre></p>
<p>根据串行化的值来恢复一个key，如果key已经存在且没有指定REPLACE则返回错误</p>
</td>
</tr>
<tr>
<td class=" blog_h3">ROLE</td>
<td>获得当前Redis实例在复制（Replication）上下文中的角色</td>
</tr>
<tr>
<td class=" blog_h3">RPOP</td>
<td>
<p>格式：<pre class="crayon-plain-tag">RPOP key</pre></p>
<p>弹出列表的最后一个元素并返回之</p>
</td>
</tr>
<tr>
<td class=" blog_h3">RPOPLPUSH </td>
<td>
<p>格式：<pre class="crayon-plain-tag">RPOPLPUSH source destination</pre></p>
<p>从source列表的尾部弹出一个元素，并把该元素插入到destination列表的头部。如果源列表不存在则返回nil，并不执行LPUSH操作，否则返回弹出的元素</p>
</td>
</tr>
<tr>
<td class=" blog_h3">RPUSH</td>
<td>
<p>格式：<pre class="crayon-plain-tag">RPUSH key value [value ...]</pre></p>
<p>插入多个元素到列表的尾部</p>
</td>
</tr>
<tr>
<td class=" blog_h3">RPUSHX</td>
<td>
<p>格式：<pre class="crayon-plain-tag">RPUSHX key value</pre></p>
<p>如果key存在且存储了列表，则执行RPUSH命令</p>
</td>
</tr>
<tr>
<td class=" blog_h3">SADD </td>
<td>
<p>格式：<pre class="crayon-plain-tag">SADD key member [member ...]</pre></p>
<p>添加多个元素到key所存储的集合（Set）中，返回实际插入的元素数量</p>
</td>
</tr>
<tr>
<td class=" blog_h3">SAVE</td>
<td>
<p>对所有数据集进行同步的保存，产生RDB文件</p>
<p>在生产环境下通常不会使用该命令，因为它会阻塞所有其他客户端，可以使用BGSAVE代替</p>
</td>
</tr>
<tr>
<td class=" blog_h3">SCARD</td>
<td>
<p>格式：<pre class="crayon-plain-tag">SCARD key</pre></p>
<p>返回集合的基数（cardinality，元素的个数）</p>
</td>
</tr>
<tr>
<td class=" blog_h3"> SDIFF</td>
<td>
<p>格式：<pre class="crayon-plain-tag">SDIFF key [key ...] </pre></p>
<p>返回第一个集合与后续所有集合的差集，以数组形式</p>
</td>
</tr>
<tr>
<td class=" blog_h3">SDIFFSTORE</td>
<td>
<p>格式：<pre class="crayon-plain-tag">SDIFFSTORE destination key [key ...]</pre></p>
<p>类似于SDIFF，但是差集不是返回客户端，而是存储到destination</p>
</td>
</tr>
<tr>
<td class=" blog_h3">SELECT </td>
<td>
<p>格式：<pre class="crayon-plain-tag">SELECT index</pre></p>
<p>根据索引选择使用的数据库，新连接默认选择0</p>
</td>
</tr>
<tr>
<td class=" blog_h3">SET</td>
<td>
<p>格式：<pre class="crayon-plain-tag">SET key value [EX seconds] [PX milliseconds] [NX|XX]</pre></p>
<p>让键存储一个字符串值，如果key已经存在，不管它当前存储的值类型，都被覆盖<br />EX  以秒为单位指定生存期<br />PX  以毫秒为单位指定生存期<br />NX  仅在键不存在的情况下才设置值<br />XX  仅在键已存在的情况下才设置值</p>
</td>
</tr>
<tr>
<td class=" blog_h3">SETBIT </td>
<td>
<p>格式：<pre class="crayon-plain-tag">SETBIT key offset value</pre></p>
<p>设置或者清除offset位，如果key不存在，则自动设置为字符串值。字符串长度会自动增长以满足offset，但是最大支持offset为2^32 - 1，也就是说最大支持512MB的位图</p>
<p>注意性能位图：在2010年产的Macbook上设置第2^32-1位需要耗时接近300毫秒，设置第2^30-1位需要80毫秒</p>
</td>
</tr>
<tr>
<td class=" blog_h3">SETEX </td>
<td>
<p>格式：<pre class="crayon-plain-tag">SETEX key seconds value</pre></p>
<p>设置值，并以秒为单位设置键的生存期</p>
</td>
</tr>
<tr>
<td class=" blog_h3">SETNX</td>
<td>
<p>格式：<pre class="crayon-plain-tag">SETNX key value</pre></p>
<p>如果键不存在，则设置其值</p>
</td>
</tr>
<tr>
<td class=" blog_h3">SETRANGE</td>
<td>
<p>格式：<pre class="crayon-plain-tag">SETRANGE key offset value</pre></p>
<p>从指定的偏移处开始用value覆盖key的值</p>
</td>
</tr>
<tr>
<td class=" blog_h3">SHUTDOWN </td>
<td>
<p>格式：<pre class="crayon-plain-tag">SHUTDOWN [NOSAVE|SAVE]</pre></p>
<p>该命令的行为：</p>
<ol>
<li>停止所有客户端</li>
<li>如果至少一个保存点被配置，执行阻塞的SAVE命令</li>
<li>如果AOF被启用，刷出Append Only File</li>
<li>退出服务器</li>
</ol>
<p>如果启用了持久化，该命令确保不丢失数据。相对的，SAVE + QUIT的组合无法保证不丢失数据，因为其它客户端可能在两个命令之间修改了数据</p>
<p>对于配置了不持久化到磁盘的Redis实例（没有配置AOF，也没有使用save指令），该命令不会在磁盘上DUMP出RDB文件</p>
<p>SAVE，强制执行数据库保存操作，即使在没有配置保存点（save points）的情况下<br />NOSAVE，即使在配置了一个或多个保存点的情况下，也阻止数据库保存操作</p>
<p><strong>SHUTDOWN命令失败的情况</strong></p>
<p>如果启用了Append Only File，命令可能失败，因为系统可能处于一个不能立即安全的持久化到磁盘上的状态</p>
<p>通常的，如果一个AOF子进程正在执行AOF重写（Rewrite）操作，Redis会简单的杀掉这些子进程并退出。但是下面两个条件下这样杀死是不安全的，SHUTDOWN命令会被拒绝，并返回错误：</p>
<ol>
<li>用户刚刚启用AOF，并且服务器触发第一次AOF重写以创建初始的AOF文件。这种情况下，杀死AOF子进程会导致数据集完全丢失</li>
<li>一个Slave启用了AOF，重连到它的Master执行一次完整的同步操作，然后重新初始化AOF文件。这种情况下，不让AOF子进程完成工作是危险的，因为从Master接收到的最后的数据集会丢失</li>
</ol>
<p>某些情况下，我们只是想立即关闭服务器，而不管它存储了什么东西。此时可以联用： CONFIG appendonly no、SHUTDOWN NOSAVE。前一个命令会关闭AOF并且杀死AOF子进程（如果存在）</p>
</td>
</tr>
<tr>
<td class=" blog_h3">SINTER</td>
<td>
<p>格式：<pre class="crayon-plain-tag">SINTER key [key ...]</pre></p>
<p>以数组形式返回多个集合的交集</p>
</td>
</tr>
<tr>
<td class=" blog_h3">SINTERSTORE</td>
<td>
<p>格式：<pre class="crayon-plain-tag">SINTERSTORE destination key [key ...]</pre></p>
<p>存储多个集合的交集到destination，返回交集包含的元素个数</p>
</td>
</tr>
<tr>
<td class=" blog_h3">SISMEMBER</td>
<td>
<p>格式：<pre class="crayon-plain-tag">SISMEMBER key member</pre></p>
<p>判断member是不是key存储的集合的成员</p>
</td>
</tr>
<tr>
<td class=" blog_h3">SLAVEOF</td>
<td>
<p>格式：<pre class="crayon-plain-tag">SLAVEOF host port</pre></p>
<p>即时的设置一个Slave的复制设置（replication settings）</p>
<p>如果一个Redis已经是Slave，那么执行<pre class="crayon-plain-tag">SLAVEOF NO ONE</pre>会关闭复制功能，并把此实例变为Master。当指定host port时，会让此实例成为目标实例的Slave，如果当前实例已经是其它某个实例的Slave，则解除其关系</p>
</td>
</tr>
<tr>
<td class=" blog_h3">SLOWLOG</td>
<td>
<p>格式：<pre class="crayon-plain-tag">SLOWLOG subcommand [argument]</pre></p>
<p>读取或者重置Redis缓慢查询日志</p>
<p><strong>缓慢日志概述</strong></p>
<p>这是Redis记录缓慢查询（执行时间超过一定的阈值）的机制。注意执行时间不包括握手、发送答复等网络I/O消耗的时间</p>
<p>与缓慢查询相关的配置参数：</p>
<ol>
<li>slowlog-log-slower-than：慢于多少ms的查询被认为是缓慢的。设置为负数禁用缓慢查询日志，设置为0则记录所有查询</li>
<li>slowlog-max-len：缓慢日志的最大长度，超过此长度后，最老的日志被丢弃</li>
</ol>
<p>你可以编辑redis.conf或者在运行时执行CONFIG SET/GET命令还读写这些参数</p>
<p><strong>读取缓慢日志</strong></p>
<p>缓慢日志驻留内存，因此它才能支持对所有命令进行记录而不影响性能。调用SLOWLOG GET可以读取所有缓慢日志，SLOWLOG GET 10则读取最近的10条日志。输出字段包括：流水号、目标命令执行时的UNIX时间戳、执行消耗的微秒数、命令及其参数</p>
<p><strong>重置缓慢日志</strong></p>
<p>执行SLOWLOG RESET可以清空缓慢日志</p>
<p><strong>示例</strong></p>
<p><pre class="crayon-plain-tag">1) 1) (integer) 32                   # 慢查询条目的标识符
   2) (integer) 1611661425           # 命令开始执行时的时间戳                  
   3) (integer) 24959                # 耗时，单位微秒
   4) 1) "PSYNC"                     # 命令及其参数   
      2) "?"                                           
      3) "-1"                                          
   5) "172.29.0.81:38265"            # 客户端的IP和端口                  
   6) ""                             # 通过CLIENT SETNAME设置的客户端名称</pre>
</td>
</tr>
<tr>
<td class=" blog_h3">SMEMBERS</td>
<td>
<p>格式：<pre class="crayon-plain-tag">SMEMBERS key</pre></p>
<p>以数组的形式返回集合的全部元素</p>
</td>
</tr>
<tr>
<td class=" blog_h3">SMOVE</td>
<td>
<p>格式：<pre class="crayon-plain-tag">SMOVE source destination member</pre></p>
<p>以原子操作的方式，把元素member从集合source移动到集合destination中</p>
<p>如果source不存在或者元素不属于soruce，返回0；否则，从source中移除元素，添加到destination中，如果destination已经包含该元素，则不添加，完成这些操作后返回1；如果source或者destination持有的值不是集合，则返回错误</p>
</td>
</tr>
<tr>
<td class=" blog_h3">SORT </td>
<td>
<p>格式：<pre class="crayon-plain-tag">SORT key [BY pattern] [LIMIT offset count] [GET pattern [GET pattern ...]] [ASC|DESC] [ALPHA] [STORE destination]</pre></p>
<p>对列表、集合或者有序集合进行排序，排序结果返回或者存储到destination</p>
<p>默认排序方式是升序，可以指定DESC来降序</p>
<p>如果元素是字符串，可以指定ALPHA，进行字典序排列。注意Redis支持UTF-8，但是必须正确设置LC_COLLATE环境变量</p>
<p>要限制返回元素的偏移量和数量，可以指定<pre class="crayon-plain-tag">LIMIT 0 10</pre>，表示从0开始，10结束（不包含）</p>
</td>
</tr>
<tr>
<td class=" blog_h3">SPOP </td>
<td>
<p>格式：<pre class="crayon-plain-tag">SPOP key [count]</pre></p>
<p>从集合中随机弹出指定数量的元素</p>
</td>
</tr>
<tr>
<td class=" blog_h3">SRANDMEMBER</td>
<td>
<p>格式：<pre class="crayon-plain-tag">SRANDMEMBER key [count]</pre></p>
<p>从集合中随机的返回1个元素，如果指定count为正数，则返回指定个数随机distinct元素的数组 </p>
</td>
</tr>
<tr>
<td class=" blog_h3">SREM</td>
<td>
<p>格式：<pre class="crayon-plain-tag">SREM key member [member ...]</pre></p>
<p>从集合中移除指定的元素，返回实际移除元素的个数</p>
</td>
</tr>
<tr>
<td class=" blog_h3">STRLEN</td>
<td>
<p>格式：<pre class="crayon-plain-tag">STRLEN key</pre></p>
<p>返回字符串的长度，如果key存储的不是字符串则返回错误</p>
</td>
</tr>
<tr>
<td class=" blog_h3">SUBSCRIBE</td>
<td>
<p>格式：<pre class="crayon-plain-tag">SUBSCRIBE channel [channel ...]</pre></p>
<p>让当前客户端订阅某些频道。一旦客户端进入已订阅状态（ subscribed state），它不应再发起任何除 SUBSCRIBE, PSUBSCRIBE, UNSUBSCRIBE, PUNSUBSCRIBE之外的任何命令</p>
</td>
</tr>
<tr>
<td class=" blog_h3">SUNION</td>
<td>
<p>格式：<pre class="crayon-plain-tag">SUNION key [key ...]</pre></p>
<p>以数组的形式返回并集</p>
</td>
</tr>
<tr>
<td class=" blog_h3">SUNIONSTORE</td>
<td>
<p>格式：<pre class="crayon-plain-tag">SUNIONSTORE destination key [key ...]</pre></p>
<p>求并集并存储到destination，如果destination已经存在，它将被覆盖</p>
</td>
</tr>
<tr>
<td class=" blog_h3">SWAPDB</td>
<td>
<p>格式：<pre class="crayon-plain-tag">SWAPDB index index</pre></p>
<p>立即交换两个数据库，所有客户端将立即看到交换后的数据库</p>
</td>
</tr>
<tr>
<td class=" blog_h3">SYNC</td>
<td>用于同步Master/Slave的内部命令</td>
</tr>
<tr>
<td class=" blog_h3">TIME</td>
<td>返回当前的服务器时间，返回值是两个元素的列表，第一个元素为UNIX时间戳，第二个是在当前秒内已经流逝的微秒数</td>
</tr>
<tr>
<td class=" blog_h3">TOUCH</td>
<td>
<p>格式：<pre class="crayon-plain-tag">TOUCH key [key ...]</pre></p>
<p>修改键的最后访问时间，如果键不存在则不做任何操作</p>
</td>
</tr>
<tr>
<td class=" blog_h3">TTL</td>
<td>
<p>格式：<pre class="crayon-plain-tag">TTL key</pre></p>
<p>返回一个键剩余的生存期，如果key不存在返回-2，如果key没有设置过期时间返回-1</p>
</td>
</tr>
<tr>
<td class=" blog_h3">TYPE</td>
<td>
<p>格式：<pre class="crayon-plain-tag">TYPE key</pre></p>
<p>返回键所存储值的类型</p>
</td>
</tr>
<tr>
<td class=" blog_h3">UNSUBSCRIBE</td>
<td>
<p>格式：<pre class="crayon-plain-tag">UNSUBSCRIBE [channel [channel ...]]</pre></p>
<p>取消客户端对频道的订阅，如果没有指定频道，则所有频道都被取消订阅</p>
</td>
</tr>
<tr>
<td class=" blog_h3">UNLINK</td>
<td>
<p>格式：<pre class="crayon-plain-tag">UNLINK key [key ...]</pre></p>
<p>行为类似于DEL：移除指定的key，如果key不存在则忽略</p>
<p>不同之处是，内存回收工作由其他线程完成，因而该命令是非阻塞的</p>
</td>
</tr>
<tr>
<td class=" blog_h3">UNWATCH</td>
<td>为事务刷出所有先前监控的键（watched keys），如果你调用了EXEC、DISCARD则不需要手工调用该命令</td>
</tr>
<tr>
<td class=" blog_h3">WAIT</td>
<td>
<p>格式：<pre class="crayon-plain-tag">WAIT numslaves timeout</pre></p>
<p>阻塞客户端，直到先前所有的写命令被成功传输、并被至少numslaves个Slave所确认。如果timeout到达，则命令立即返回。该命令返回实际确认了先前写命令的Slave的个数</p>
<p>一些备注：</p>
<ol>
<li>当WAIT命令返回时，在当前连接上下文下发起的所有写命令都确保，被WAIT返回值数量的Slave接收</li>
<li>如果该命令作为MULTI事务的一部分发起，不会引起阻塞，而仅仅会尽快的返回确认了先前写命令的Slave数量</li>
<li>指定timeout为0 则永远阻塞</li>
<li>调用者应该检查返回值，确认确认的Slave数量是否满足复制级别（replication level ，份数）的需求</li>
</ol>
<p><strong>WAIT命令和数据一致性</strong></p>
<p>WAIT命令并不能把Redis变成强一致性的数据库。尽管同步化的复制是复制状态机（replicated state machine）的一部分，但它不是唯一需要的事情</p>
<p>WAIT在集群故障转移的上下文中，的确增强了数据安全性。当一个写命令被传递给一个或者多个Slave时，假设Master宕机，Redis会尽可能推举最好（数据最完整）的Slave成为新的Master</p>
</td>
</tr>
<tr>
<td class=" blog_h3">WATCH</td>
<td>
<p>格式：<pre class="crayon-plain-tag">WATCH key [key ...]</pre></p>
<p>标记指定的键被监控，如果其值没有变化则执行MULTI/EXEC块</p>
</td>
</tr>
<tr>
<td class=" blog_h3">ZADD</td>
<td>
<p>格式：<pre class="crayon-plain-tag">ZADD key [NX|XX] [CH] [INCR] score member [score member ...]</pre></p>
<p>将指定的member附带score（影响其排序）存放到有序集合中。如果member已经是该集合的成员，则仅仅更新它的score</p>
<p>score必须是字符串形式的双精度浮点数。+inf、-inf 是有效的值</p>
<p>XX，仅仅更新既有元素的score，绝不插入新元素<br />NX，不会更新既有元素，仅仅会插入新元素<br />CH，默认情况下，该命令返回添加的元素个数，使用该选项后，返回的是改变（添加、更新scoure）的元素个数<br />INCR，让该命令的行为类似于 ZINCRBY，仅接受一个score/member对</p>
</td>
</tr>
<tr>
<td class=" blog_h3">ZCARD</td>
<td>
<p>格式：<pre class="crayon-plain-tag">ZCARD key</pre></p>
<p>返回有序集合的基数（元素个数），如果key不存在则返回0</p>
</td>
</tr>
<tr>
<td class=" blog_h3">ZCOUNT</td>
<td>
<p>格式：<pre class="crayon-plain-tag">ZCOUNT key min max</pre></p>
<p>在min、max指定区间的score的元素的个数</p>
</td>
</tr>
<tr>
<td class=" blog_h3">ZINCRBY</td>
<td>
<p>格式：<pre class="crayon-plain-tag">ZINCRBY key increment member</pre></p>
<p>增加key对应的有序集合中member元素的score，增加的数值为increment</p>
<p>如果member不是key的成员，则在增加score前，将其添加到集合中并设置初始score为0.0</p>
</td>
</tr>
<tr>
<td class=" blog_h3">ZINTERSTORE</td>
<td>
<p>格式：<pre class="crayon-plain-tag">ZINTERSTORE destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX]</pre></p>
<p>取numkeys个有序集合的交集，这些集合由key指定。交集被保存到destination，如果destination已经存在则覆盖之</p>
<p>默认情况下，destination集合中每个元素的score，为其来源集合中score的求和（作为交集成员的元素必须在每个输入集合中存在）</p>
</td>
</tr>
<tr>
<td class=" blog_h3">ZLEXCOUNT</td>
<td>
<p>格式：<pre class="crayon-plain-tag">ZLEXCOUNT key min max</pre></p>
<p>返回值在min、max之间的元素个数</p>
</td>
</tr>
<tr>
<td class=" blog_h3">ZRANGE</td>
<td>
<p>格式：<pre class="crayon-plain-tag">ZRANGE key start stop [WITHSCORES]</pre></p>
<p>有序集合中的元素依据score升序排列，score相同的元素以字典序排列。该命令返回指定索引范围内的元素，以数组形式返回</p>
<p>start、stop是基于0的索引，构成闭区间。索引值可以指定负数，表示倒数第几个元素（-1表示倒数第一个）</p>
<p>WITHSCORES，附带获得Score</p>
</td>
</tr>
<tr>
<td class=" blog_h3"><a id="zrangbylex"></a>ZRANGEBYLEX</td>
<td>
<p>格式：<pre class="crayon-plain-tag">ZRANGEBYLEX key min max [LIMIT offset count]</pre></p>
<p>当有序集合中所有元素的score一致时，可以使用该命令强制进行字典序排序。该命令返回有序集合中值位于min max之间的元素构成的数组</p>
<p>示例：</p>
<p><pre class="crayon-plain-tag">ZADD myindex 0 baaa
ZADD myindex 0 abbb
ZADD myindex 0 aaaa
ZADD myindex 0 bbbb

# 注意，前缀[或者(是必须的，表示包含或者不包含后面的值（开闭区间）
# 取得以a开头的条目
ZRANGEBYLEX myindex [a (b
1) "aaaa"
2) "abbb"

# 特殊符号 + 表示正无穷大， - 则表示负无穷大
ZRANGEBYLEX myindex [b +</pre>
</td>
</tr>
<tr>
<td class=" blog_h3">ZREVRANGEBYLEX</td>
<td>
<p>格式：<pre class="crayon-plain-tag">ZREVRANGEBYLEX key max min [LIMIT offset count]</pre></p>
<p>与上面的命令类似，但是倒序</p>
</td>
</tr>
<tr>
<td class=" blog_h3">ZRANGEBYSCORE</td>
<td>
<p>格式：<pre class="crayon-plain-tag">ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]</pre></p>
<p>返回有序集合中所有score位于min max之间（包含）的元素构成的数组</p>
</td>
</tr>
<tr>
<td class=" blog_h3">ZRANK</td>
<td>
<p>格式：<pre class="crayon-plain-tag">ZRANK key member</pre></p>
<p>获得有序集合中元素的排名，排名根据score从低到高产生，最低score排名为0</p>
</td>
</tr>
<tr>
<td class=" blog_h3">ZREM</td>
<td>
<p>格式：<pre class="crayon-plain-tag">ZREM key member [member ...]</pre></p>
<p>从有序集合中移除指定的元素，不存在的元素被忽略。返回实际移除的元素个数，如果key存储的不是有序集合则返回错误</p>
</td>
</tr>
<tr>
<td class=" blog_h3">ZREMRANGEBYLEX</td>
<td>
<p>格式：<pre class="crayon-plain-tag">ZREMRANGEBYLEX key min max</pre></p>
<p>从有序集合中移除值在min max之间的所有元素</p>
</td>
</tr>
<tr>
<td class=" blog_h3">ZREMRANGEBYRANK</td>
<td>
<p>格式：<pre class="crayon-plain-tag">ZREMRANGEBYRANK key start stop</pre></p>
<p>从有序集合中移除排名在start stop之间的所有元素</p>
</td>
</tr>
<tr>
<td class=" blog_h3">ZREMRANGEBYSCORE</td>
<td>
<p>格式：<pre class="crayon-plain-tag">ZREMRANGEBYSCORE key min max</pre></p>
<p>从有序集合中移除score在start stop之间的所有元素</p>
</td>
</tr>
<tr>
<td class=" blog_h3">ZREVRANGE</td>
<td>
<p>格式：<pre class="crayon-plain-tag">ZREVRANGE key start stop [WITHSCORES]</pre></p>
<p>以倒序返回指定索引范围的元素构成的数组，元素先按score降序排列，score相同的则按字典序降序排列</p>
</td>
</tr>
<tr>
<td class=" blog_h3">ZREVRANGEBYSCORE</td>
<td>
<p>格式：<pre class="crayon-plain-tag">ZREVRANGEBYSCORE key max min [WITHSCORES] [LIMIT offset count]</pre></p>
<p>以倒序返回指定score范围的元素构成的数组</p>
</td>
</tr>
<tr>
<td class=" blog_h3">ZREVRANK</td>
<td>
<p>格式：<pre class="crayon-plain-tag">ZREVRANK key member</pre></p>
<p>返回一个元素的倒序排名，score最高的元素的排名为0</p>
</td>
</tr>
<tr>
<td class=" blog_h3">ZSCORE</td>
<td>
<p>格式：<pre class="crayon-plain-tag">ZSCORE key member</pre></p>
<p>返回一个元素的score。如果key不存在或者member不是其成员返回nil</p>
</td>
</tr>
<tr>
<td class=" blog_h3">ZUNIONSTORE</td>
<td>
<p>格式：<pre class="crayon-plain-tag">ZUNIONSTORE destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX]</pre></p>
<p>取由key...指定的numkeys个有序集合的并集，存储到destination。destination中元素的score为其来源集合的score的求和</p>
<p>WEIGHTS指定各输入集合的权重，此权重会乘以score以产生destination集合中元素的score的因素</p>
<p>AGGREGATE指定结果score的聚合方式，默认是SUM</p>
</td>
</tr>
<tr>
<td class=" blog_h3">SCAN<br />SSCAN<br />HSCAN<br />ZSCAN</td>
<td>
<p>格式：<pre class="crayon-plain-tag">SCAN cursor [MATCH pattern] [COUNT count]</pre></p>
<p>该命令以及SSCAN、HSCAN、ZSCAN用于遍历对应类型的容器：</p>
<ol>
<li>SCAN：遍历当前选中数据库的所有键，不支持集群</li>
<li>SSCAN：遍历某个集合的元素</li>
<li>HSCAN：遍历哈希的字段，及其关联的值</li>
<li>ZSCAN：遍历有序集合，及其关联的score</li>
</ol>
<p>由于这些命令支持增量迭代（incremental iteration），在每次调用中返回少量的元素，你可以在生产环境下使用它们而不会遭致KEYS、SMEMBERS等命令带来的服务器阻塞</p>
<p>尽管如此，SMEMBERS这样的阻塞命令能够得到容器在一个瞬间所包含的所有元素。SCAN类则没有一致性保证，因为在迭代期间，容器内容可能发生变化</p>
<p><strong>基本用法</strong></p>
<p>SCAN是基于游标的迭代器，这意味着，每次调用该命令时，服务器都会返回一个更新后的游标。此游标在下一次SCAN调用中需要。一次迭代从游标为0时开始，当服务器返回游标0时结束。示例：</p>
<pre class="crayon-plain-tag">redis 127.0.0.1:6379&gt; scan 0
1) "6"
2)  1) "key:12"
    2) "key:8"
    3) "key:4"

redis 127.0.0.1:6379&gt; scan 6
1) "0"
2) 1) "key:5"
   2) "key:18"

# 在这个例子中，第一次迭代调用以0为游标，服务器返回6。下一次调用客户端需要传递6
# 在第二次迭代调用后，服务器返回0，表示没有更多的元素可以迭代

# 从0开始迭代，一直调用SCAN直到服务器返回0的迭代过程，称为完全迭代（full iteration）</pre>
<p><strong>SCAN特性</strong></p>
<p>SCAN以及其它*SCAN命令，在用户进行完全迭代时，能够保证：</p>
<ol>
<li>返回所有在迭代期间，位于容器中的元素。这意味着当迭代开始时元素E位于容器，迭代结束时E仍然在容器中，则E必然被迭代</li>
<li>类似的，如果一个元素在迭代开始前被移除，或者迭代结束后才加入，则它绝不会被迭代 </li>
</ol>
<p>尽管如此，由于游标的特性，*SCAN命令具有以下缺点：</p>
<ol>
<li style="text-align: left;">一个元素可能被返回多次，应用程序需要处理这种重复</li>
<li style="text-align: left;">在迭代期间，不是一直存在于容器中的元素，有可能被迭代，也可能不，这是无法确定的</li>
</ol>
<p><strong>返回元素的数量</strong></p>
<p>*SCAN命令不保证每次迭代返回元素的个数，返回0个元素也是允许的——这并不代表已经完成迭代</p>
<p>尽管如此，Redis会返回“合理”个数的元素：</p>
<ol>
<li>对于很大的容器，可能返回数十个元素</li>
<li>对于较小的容器，可能一次性返回所有元素</li>
</ol>
<p>你可以指定COUNT选项，来提示返回元素的“数量级”。COUNT的默认值是10，它只是一个提示性参数，Redis不作出精确保证</p>
<p><strong>MATCH选项</strong></p>
<p>使用该选项，你可以提供通配符 *，示例：</p>
<pre class="crayon-plain-tag">redis 127.0.0.1:6379&gt; scan 0 MATCH *11*
1) "288"
2) 1) "key:911"
redis 127.0.0.1:6379&gt; scan 228 MATCH *11*
1) "176"
2) (empty list or set)
redis 127.0.0.1:6379&gt; scan 176 MATCH *11* COUNT 1000  # 增大了count提示
1) "0"
2)  1) "key:611"
    2) "key:711"</pre>
<p><strong>并行迭代支持</strong></p>
<p>由于迭代的所有状态都在游标之中，而游标是每次调用产生的，不保存在服务器上，因此很多客户端对同一容器同时进行迭代是可以的</p>
<p><strong>中止迭代</strong></p>
<p>同样由于服务器端不保存迭代状态，你可以随时中途停止迭代，而不需要通知服务器</p>
<p><strong>损坏游标</strong></p>
<p>以损坏的游标调用*SCAN命令，例如负游标，其行为是未定义的，但是不会导致崩溃</p>
<p><strong>返回值</strong></p>
<p>*SCAN命令返回两元素的数组，第一个元素是下一次调用需要的游标，第二个元素：</p>
<ol>
<li>对于SCAN，是键的列表</li>
<li>对于SSCAN，是集合元素的列表</li>
<li>对于HSCAN，是[field,value]构成的数组</li>
<li>对于ZSCAN，是[element,score]构成的数组</li>
</ol>
</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">客户端</span></div>
<div class="blog_h2"><span class="graybg">Java客户端</span></div>
<div class="blog_h3"><span class="graybg">Jedis</span></div>
<p>这是一个非常轻量易用的Java客户端，完全兼容Redis 2.8.x和3.x.x。</p>
<p>Maven依赖：</p>
<pre class="crayon-plain-tag">&lt;dependency&gt;
    &lt;groupId&gt;redis.clients&lt;/groupId&gt;
    &lt;artifactId&gt;jedis&lt;/artifactId&gt;
    &lt;version&gt;2.9.0&lt;/version&gt;
    &lt;type&gt;jar&lt;/type&gt;
    &lt;scope&gt;compile&lt;/scope&gt;
&lt;/dependency&gt; </pre>
<p>简单的客户端代码：</p>
<pre class="crayon-plain-tag">Jedis jedis = new Jedis( "localhost", 6379 );
final String HELLO_KEY = "Hello";
final String NUMS_KEY = "Numbers";

jedis.set( HELLO_KEY, "World" );
jedis.get( HELLO_KEY );
jedis.lpush( NUMS_KEY, "1", "2", "3" );
jedis.lpop( NUMS_KEY );
// 使用Redis事务
Transaction multi = jedis.multi();
jedis.set( HELLO_KEY, "Alex" );
jedis.expire( HELLO_KEY, 10 );
multi.exec();

jedis.close();</pre>
<p>使用连接池：</p>
<pre class="crayon-plain-tag">JedisPoolConfig cfg = new JedisPoolConfig();
cfg.setMaxTotal( 100 );
JedisPool pool = new JedisPool( cfg, "localhost", 6379 );
Jedis jedis = pool.getResource();
jedis.auth( "foobared" );
jedis.set( "foo", "0" );
jedis.close();

jedis = pool.getResource();
jedis.auth( "foobared" );
jedis.incr( "foo" );

jedis.close();
pool.destroy();</pre>
<div class="blog_h3"><span class="graybg">spring-data</span></div>
<p>定义RestTemplate：</p>
<pre class="crayon-plain-tag">@Bean
JedisConnectionFactory jedisConnectionFactory() {
    JedisConnectionFactory jcf = new JedisConnectionFactory();
    jcf.setHostName( "10.4.37.22" );
    jcf.setPort( 6079 );
    jcf.setDatabase( 0 );
    jcf.setUsePool( true );
    JedisPoolConfig cfg = new JedisPoolConfig();
    cfg.setMaxTotal( 256 );
    cfg.setMaxIdle( 16 );
    jcf.setPoolConfig( cfg );
    return jcf;
}

@Bean
public RedisTemplate&lt;String, Object&gt; redisTemplateNoCompress() {
    final RedisTemplate&lt;String, Object&gt; template = new RedisTemplate&lt;String, Object&gt;();
    template.setConnectionFactory( jedisConnectionFactory() );
    template.setValueSerializer( new GenericToStringSerializer&lt;Object&gt;( Object.class ) );
    return template;
}</pre>
<p>使用RestTemplate： </p>
<pre class="crayon-plain-tag">// SET/GET操作
ValueOperations&lt;String, Object&gt; ops = template.opsForValue();
ops.set( key, value );
ops.get( key );</pre>
<p>定制串行化器，示例：</p>
<pre class="crayon-plain-tag">package com.dangdang.digital.spring.data.redis;

import net.jpountz.lz4.LZ4Compressor;
import net.jpountz.lz4.LZ4Factory;
import net.jpountz.lz4.LZ4FastDecompressor;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;

import java.nio.ByteBuffer;
import java.util.Arrays;

/**
 * 基于LZ4算法的RedisSerializer装饰器
 *
 * @param &lt;T&gt; 串行化处理的目标类型
 */
public class LZ4RedisSerializeDecorator&lt;T&gt; implements RedisSerializer&lt;T&gt; {

    private final int compressThreshold;

    private final RedisSerializer&lt;T&gt; serializer;

    private LZ4Factory factory;

    private LZ4Compressor compressor;

    private LZ4FastDecompressor decompressor;

    private static final int COMPRESS_HEAD_SIZE = Integer.SIZE / Byte.SIZE;

    private static final int NOCOMPRESS_HEAD_SIZE = 1;

    /**
     * 构造函数
     *
     * @param serializer        被装饰的串行化器
     * @param fastMode          是否使用快速模式，快速模式压缩比低但是压缩的速度快
     *                          快速模式不会让解压缩的速度更快
     * @param compressThreshold 启用压缩的最小字节数
     */
    public LZ4RedisSerializeDecorator( RedisSerializer&lt;T&gt; serializer, boolean fastMode, int compressThreshold ) {
        this.serializer = serializer;
        this.compressThreshold = compressThreshold;
        factory = LZ4Factory.fastestInstance();
        if ( fastMode ) {
            compressor = factory.fastCompressor();
        } else {
            compressor = factory.highCompressor();
        }
        decompressor = factory.fastDecompressor();
    }

    public byte[] serialize( T src ) throws SerializationException {
        byte[] srcBytes = serializer.serialize( src );
        int srcLen = srcBytes.length;
        ByteBuffer destBuf;
        if ( srcLen &lt; compressThreshold ) {
            destBuf = ByteBuffer.allocate( NOCOMPRESS_HEAD_SIZE + srcLen );
            destBuf.put( (byte) -1 );
            destBuf.put( srcBytes );
            onSerialize( false, srcLen, srcLen );
            return destBuf.array();
        } else {
            int maxDestLen = compressor.maxCompressedLength( srcLen );
            destBuf = ByteBuffer.allocate( COMPRESS_HEAD_SIZE + maxDestLen );
            destBuf.putInt( srcLen );
            byte[] destArray = destBuf.array();
            int compressedLength = compressor.compress( srcBytes, 0, srcLen, destArray, destBuf.position(), maxDestLen );
            onSerialize( true, srcLen, compressedLength );
            return Arrays.copyOfRange( destArray, 0, COMPRESS_HEAD_SIZE + compressedLength );
        }
    }

    public T deserialize( byte[] src ) throws SerializationException {
        if ( src == null ) return serializer.deserialize( src );
        ByteBuffer srcBuf = ByteBuffer.wrap( src );
        boolean noCompressed = srcBuf.array()[0] &lt; 0;
        if ( noCompressed ) {
            byte[] destBytes = Arrays.copyOfRange( src, 1, src.length );
            return serializer.deserialize( destBytes );
        } else {
            int destLen = srcBuf.getInt();
            byte[] destBytes = new byte[destLen];
            decompressor.decompress( src, srcBuf.position(), destBytes, 0, destLen );
            return serializer.deserialize( destBytes );
        }
    }

    protected void onSerialize( boolean compressed, int srcLen, int compressedLen ) {
    }
}</pre>
<div class="blog_h3"><span class="graybg">Lettuce</span></div>
<p>Lettuce是一个可扩容的、线程安全的Redis客户端，提供同步、异步、响应式（reactive）的API。</p>
<p>在不使用阻塞性、事务性操作 —— 例如BLPOP、MULTI/EXEC ——的情况下，多个Java线程可以共享一个连接。到Redis服务器的连接由netty框架管理。Letttuce支持Sentinel、Cluster等高级特性。</p>
<p>Maven依赖：</p>
<pre class="crayon-plain-tag">&lt;dependency&gt;
    &lt;groupId&gt;biz.paluch.redis&lt;/groupId&gt;
    &lt;artifactId&gt;lettuce&lt;/artifactId&gt;
    &lt;version&gt;4.3.1.Final&lt;/version&gt;
&lt;/dependency&gt;</pre>
<p>开启连接： </p>
<pre class="crayon-plain-tag">// 使用URI连接到本机的Redis实例，使用数据库0
// redis://[password@]host[:port][/databaseNumber]
// 如果是使用SSL的连接，协议部分改为rediss
RedisClient redisClient = RedisClient.create( "redis://passwd@localhost:6379/0" );
// 显式创建连接
StatefulRedisConnection&lt;String, String&gt; connection = redisClient.connect();</pre>
<p>资源清理：</p>
<pre class="crayon-plain-tag">// 资源清理
connection.close();
redisClient.shutdown();</pre>
<p>通过Sentinel连接：</p>
<pre class="crayon-plain-tag">// redis-sentinel://[password@]host[:port][,host2[:port2]][/databaseNumber]#sentinelMasterId
RedisClient redisClient = RedisClient.create("redis-sentinel://localhost:26379,localhost:26380/0#mymaster");</pre>
<p>连接到集群：</p>
<pre class="crayon-plain-tag">// Syntax: redis://[password@]host[:port]
RedisClusterClient redisClient = RedisClusterClient.create("redis://password@localhost:7379");
StatefulRedisClusterConnection&lt;String, String&gt; connection = redisClient.connect();</pre>
<p>基于Spring：</p>
<pre class="crayon-plain-tag">&lt;bean id="RedisClient" class="com.lambdaworks.redis.support.RedisClientFactoryBean"&gt;
    &lt;property name="uri" value="redis://localhost:6379"/&gt;
&lt;/bean&gt;</pre>
<p>同步API：</p>
<pre class="crayon-plain-tag">RedisCommands&lt;String, String&gt; syncCommands = connection.sync();
syncCommands.set( "Hello", "World" );</pre>
<p>异步API：</p>
<pre class="crayon-plain-tag">RedisAsyncCommands&lt;String, String&gt; asyncCommands = connection.async();
RedisFuture&lt;String&gt; future = asyncCommands.get( "Hello" );
String value = future.get( 1, TimeUnit.MINUTES );</pre>
<p>响应式API：</p>
<pre class="crayon-plain-tag">RedisStringReactiveCommands&lt;String, String&gt; reactiveCommands = connection.reactive();
reactiveCommands.get( "Hello" ).subscribe( new Action1&lt;String&gt;() {
    public void call( String value ) {
        System.out.println( value );
    }
} );</pre>
<div class="blog_h2"><span class="graybg">Python客户端</span></div>
<div class="blog_h3"><span class="graybg">redis-py</span></div>
<p>首先需要安装包：<pre class="crayon-plain-tag">sudo pip install redis</pre> </p>
<p>代码示例：</p>
<pre class="crayon-plain-tag">import redis

if __name__ == '__main__':
    client = redis.StrictRedis(host='localhost', port=6379, db=0, password='passwd', encoding='utf-8')
    client.set('Hello', '世界')
    print(client.get('Hello').decode())

    # 管道，在一个请求中发送多个命令给服务区
    pipeline = client.pipeline()
    pipeline.set('Name', 'Alex')
    pipeline.get('Name')
    # 批量执行命令，返回结果的列表
    # transaction默认为true，能确保命令被原子的执行
    resp = pipeline.execute(transaction=True)
    print(resp)  # [True, b'Alex']</pre>
<div class="blog_h1"><span class="graybg">基于K8S</span></div>
<div class="blog_h2"><span class="graybg">集群搭建</span></div>
<p>下面是搭配Calico CNI的Redis集群样例。</p>
<div class="blog_h3"><span class="graybg">ConfigMap</span></div>
<p>配置文件内容：</p>
<pre class="crayon-plain-tag">port 6379
cluster-enabled yes
cluster-require-full-coverage no
cluster-node-timeout 5000
cluster-config-file nodes.conf
cluster-migration-barrier 1
appendonly yes
dir /data</pre>
<p>创建ConfigMap：</p>
<pre class="crayon-plain-tag">kubectl -n dev create configmap redis-cluster-conf --from-file=redis.conf</pre>
<div class="blog_h3"><span class="graybg">Pods</span></div>
<p>模板如下：</p>
<pre class="crayon-plain-tag">apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: redis-cluster-8-pvc
  namespace: dev
spec:
  storageClassName: rook-block
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 256Mi

---

apiVersion: v1
kind: Pod
metadata:
  namespace: dev
  name: redis-cluster-8
  labels:
    app: redis-cluster
  annotations:
    "cni.projectcalico.org/ipAddrs": "[\"172.27.0.18\"]"
spec:
  terminationGracePeriodSeconds: 10
  containers:
  - name: redis
    image: docker.gmem.cc/redis-cluster
    ports:
    - containerPort: 6379
      name: client
    - containerPort: 16379
      name: gossip
    command:
    - /usr/local/bin/redis-server
    args:
    - /usr/local/etc/redis/redis.conf
    readinessProbe:
      exec:
        command:
        - sh
        - -c
        - "/usr/local/bin/redis-cli -h $(hostname) ping"
      initialDelaySeconds: 15
      timeoutSeconds: 5
    livenessProbe:
      exec:
        command:
        - sh
        - -c
        - "redis-cli -h $(hostname) ping"
      initialDelaySeconds: 20
      periodSeconds: 3
    resources:
      limits:
        cpu: 100m
        memory: 256Mi
    volumeMounts:
      - name: redis-cluster-conf-volume
        mountPath: /usr/local/etc/redis
      - name: redis-cluster-8-pv
        mountPath: /data
  volumes:
    - name: redis-cluster-conf-volume
      configMap:
        name: redis-cluster-conf
    - name: redis-cluster-8-pv
      persistentVolumeClaim:
        claimName: redis-cluster-8-pvc</pre>
<div class="blog_h3"><span class="graybg">创建集群</span></div>
<p>执行下面的命令创建集群：</p>
<pre class="crayon-plain-tag">./redis-trib.rb create --replicas 1 172.27.0.10:6379 172.27.0.11:6379 172.27.0.12:6379 \
    172.27.0.13:6379 172.27.0.14:6379 172.27.0.15:6379 172.27.0.16:6379 172.27.0.17:6379 \
    172.27.0.18:6379 172.27.0.19:6379

# &gt;&gt;&gt; Creating cluster                                                                                                                                                                
# &gt;&gt;&gt; Performing hash slots allocation on 10 nodes...                                                                                                                                 
# Using 5 masters:                                                                                                                                                                    
# 172.27.0.10:6379                                                                                                                                                                    
# 172.27.0.11:6379                                                                                                                                                                    
# 172.27.0.12:6379                                                                                                                                                                    
# 172.27.0.13:6379                                                                                                                                                                    
# 172.27.0.14:6379                                                                                                                                                                    
# Adding replica 172.27.0.15:6379 to 172.27.0.10:6379                                                                                                                                 
# Adding replica 172.27.0.16:6379 to 172.27.0.11:6379                                                                                                                                 
# Adding replica 172.27.0.17:6379 to 172.27.0.12:6379                                                                                                                                 
# Adding replica 172.27.0.18:6379 to 172.27.0.13:6379                                                                                                                                 
# Adding replica 172.27.0.19:6379 to 172.27.0.14:6379</pre>
<p>根据提示操作，默认前面5个节点作为Master，后面5个作为Slave。</p>
<div class="blog_h3"><span class="graybg">集群命令</span></div>
<p>执行如下命令连接到集群节点（Pod）：</p>
<pre class="crayon-plain-tag">kubectl -n dev exec redis-cluster-0 -it bash</pre>
<p>运行Redis命令行工具redis-cli，查看集群信息：</p>
<pre class="crayon-plain-tag"># CLUSTER INFO
cluster_state:ok
# 全部Slot已经分配
cluster_slots_assigned:16384
cluster_slots_ok:16384
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:10
# 集群的大小取决于Master
cluster_size:5
cluster_current_epoch:10
cluster_my_epoch:1
cluster_stats_messages_sent:2301
cluster_stats_messages_received:2301</pre>
<div class="blog_h1"><span class="graybg">新特性</span></div>
<div class="blog_h2"><span class="graybg">3.0</span></div>
<ol style="list-style-type: undefined;">
<li>实现了Redis Cluster</li>
<li>多种性能提升</li>
</ol>
<div class="blog_h2"><span class="graybg">4.0</span></div>
<ol style="list-style-type: undefined;">
<li>PSYNC 2.0，增强复制性能，避免不必要的全量复制</li>
<li>UNLINK命令，异步删除，提升性能</li>
<li>SWAPDB，交换两个数据库</li>
<li>混合持久化，一种新的RDB-AOF混合持久化模式。开启后，AOF重写产生的文件将同时包含RDB格式的内容以及AOF格式的内容。其中RDB格式的部分记录已有数据，AOF则记录最新的增量数据。利用选项aof-use-rdb-preamble开启</li>
<li>集群模式兼容NAT和Docker</li>
<li>支持自动碎片整理activedefrag，相关配置：<br />
<pre class="crayon-plain-tag"># 开启自动内存碎片整理(总开关)
activedefrag yes
# 当碎片达到 100mb 时，开启内存碎片整理
active-defrag-ignore-bytes 100mb
# 当碎片超过 10% 时，开启内存碎片整理
active-defrag-threshold-lower 10
# 内存碎片超过 100%，则尽最大努力整理
active-defrag-threshold-upper 100
# 内存自动整理占用资源最小百分比
active-defrag-cycle-min 25
# 内存自动整理占用资源最大百分比
active-defrag-cycle-max 75
 </pre>
</li>
</ol>
<div class="blog_h2"><span class="graybg">5.0</span></div>
<ol style="list-style-type: undefined;">
<li> 增加Stream类型，相比起List、Zset等获取元数据更加高效。作为订阅发布模型，比Pub/Sub起来支持：消息持久化、消息分组、ACK等</li>
<li>不再使用redis-trib脚本，直接用redis-cli --cluster来进行集群管理</li>
<li>自动碎片整理V2，配合Jemalloc，更快更智能</li>
<li>HyperLogLog算法改进，计数时内存效率更高</li>
</ol>
<div class="blog_h2"><span class="graybg">6.0</span></div>
<ol style="list-style-type: undefined;">
<li>引入多线程IO，需要设置<pre class="crayon-plain-tag">io-threads-do-reads yes</pre>以启用。注意： Redis的<span style="background-color: #c0c0c0;">多线程部分只是用来处理网络数据的读写和协议解析，执行命令仍然是单线程顺序执行</span>，这意味着<span style="background-color: #c0c0c0;">不需要去考虑</span>控制 key、lua、事务，LPUSH/LPOP 等等的<span style="background-color: #c0c0c0;">并发及线程安全问题</span>。参数<pre class="crayon-plain-tag">io-threads</pre>控制IO线程数量，4核的机器建议设置为2或3个线程，8核的建议设置为6个线程，线程数一定要小于机器核数。线程数不是越多越好，一般认为超过8没有意义</li>
<li>支持SSL协议连接</li>
<li>支持对连接进行ACL控制</li>
<li>集群代理。因为 Redis Cluster 内部使用的是P2P中的Gossip协议，每个节点既可以从其他节点得到服务，也可以向其他节点提供服务，没有中心的概念，通过一个节点可以获取到整个集群的所有信息。所以如果<span style="background-color: #c0c0c0;">应用连接Redis Cluster可以配置一个节点地址，也可以配置多个节点地址</span>。但需要注意<span style="background-color: #c0c0c0;">如果集群进行了增减节点的的操作，其应用也需要进行修改，这样会导致需要重启应用，非常的不友好。</span>通过使用 <pre class="crayon-plain-tag">redis-cluster-proxy</pre>可以与组成Redis集群的一组实例进行通讯，就像是单个实例一样</li>
</ol>
<div class="blog_h2"><span class="graybg">7.0</span></div>
<ol style="list-style-type: undefined;">
<li>新增Function自定义函数库，函数库支持持久化与可复制</li>
<li>Lua脚本（脚本本身代码）不再支持持久化和复制，仅对命令执行结果进行持久化和复制。</li>
<li>ACL支持对Pub/Sub channel的权限控制</li>
<li>支持Multi-Part AOF</li>
<li>支持Client-Eviction</li>
<li>支持Sharded-Pub/Sub</li>
<li>支持命令执行耗时直方图</li>
<li>支持子命令级别的性能统计</li>
<li>Ziplist编码替换为Listpack编码</li>
<li>支持Global Replication Buffer</li>
</ol>
<div class="blog_h1"><span class="graybg">常见问题</span></div>
<div class="blog_h2"><span class="graybg">不同之处</span></div>
<p>和其它键值存储方案相比，Redis具有以下特点：</p>
<table class=" full-width fixed-word-wrap">
<tbody>
<tr>
<td>
<p><strong>复杂数据类型支持</strong></p>
<p>Redis支持多种数据类型，并在其上定义了一些原子操作。这些数据类型和常用的基础数据结构很类似并且直接暴露给程序员，没有添加额外的抽象层</p>
</td>
</tr>
<tr>
<td>
<p><strong>驻留内存而又持久化到磁盘</strong></p>
<p>为了支持可能大于内存总量的数据集，同时保证高速的读写，Redis在内存、磁盘存储之间进行了权衡。作为磁盘存储格式的RDB、AOF不需要考虑随机访问的支持，因而它们格式很紧凑，并且总是以append-only的方式生成</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">内存占用</span></div>
<p>在64bit机器上，Redis内存用量大概如下：</p>
<ol>
<li>实例本身占用 1MB左右内存</li>
<li>100万简短的键 - 字符串值，大概占用100MB内存</li>
<li>100万键 - 哈希值，每个哈希有5个字段，大概占用200MB内存 </li>
</ol>
<p>你可以使用redis-benchmark来自己测试内存占用。</p>
<p>64bit机器会占用比32位机器更多的内存，特别是键值都比较小的时候，这是因为前者指针长度为8字节。</p>
<div class="blog_h2"><span class="graybg">零散问题</span></div>
<div class="blog_h3"><span class="graybg">redis-cli报错(error) MOVED </span></div>
<p>命令行客户端默认不处理重定向指令，加上 -c 参数即可。</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/redis-study-note">Redis学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/redis-study-note/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>High Performance MySQL学习笔记</title>
		<link>https://blog.gmem.cc/high-performance-mysql-study-note</link>
		<comments>https://blog.gmem.cc/high-performance-mysql-study-note#comments</comments>
		<pubDate>Wed, 15 Feb 2012 01:27:10 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[MySQL]]></category>
		<category><![CDATA[学习笔记]]></category>
		<category><![CDATA[性能调优]]></category>

		<guid isPermaLink="false">http://blog.gmem.cc/?p=2967</guid>
		<description><![CDATA[<p>MySQL的架构和历史 MySQL与其它数据库软件很不相同，其架构特性让其具有广泛的使用范围。 MySQL的逻辑架构 MySQL的逻辑架构可以简单的描述为下图： 最上面的一层不是MySQL专有的组件，负责网络连接的处理、身份验证、安全性等逻辑 第二层是MySQL的核心所在，包括parsing, analysis, optimization, caching和内置函数在内的功能均在此实现。提供所有跨引擎的功能，例如procedures, triggers, views 第三层是存储引擎，负责存取数据。每种存储引擎各有特长。MySQL使用Storage engine API与之通信 连接管理与安全性  每个客户端连接在服务端都有自己对应的线程，连接进行的查询在单线程中运行，对应了一个CPU或者核，MySQL会缓存线程供不同连接重用。 客户端连接时需要身份验证，可以基于用户名密码的方式，使用SSL时，则可以基于X.509数字证书验证。 客户端连接成功后，其操作会被授权判断。 优化与执行 MySQL首先会针对SELECT语句来检查查询缓存——其中包含SELECT语句和它关联的结果集——如果语句完全一样，则简单的返回缓存的结果集。 然后MySQL会把SQL查询解析为内部的Parse tree结构，并进行一系列的优化，包括： <a class="read-more" href="https://blog.gmem.cc/high-performance-mysql-study-note">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/high-performance-mysql-study-note">High Performance MySQL学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h2"><span class="graybg">MySQL的架构和历史</span></div>
<p>MySQL与其它数据库软件很不相同，其架构特性让其具有广泛的使用范围。</p>
<div class="blog_h3"><span class="graybg">MySQL的逻辑架构</span></div>
<p>MySQL的逻辑架构可以简单的描述为下图：</p>
<p><img class="aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2012/02/mysql-1.jpg" alt="" width="250" /></p>
<ol>
<li>最上面的一层不是MySQL专有的组件，负责<span style="background-color: #c0c0c0;">网络连接的处理、身份验证、安全性等逻辑</span></li>
<li>第二层是<span style="background-color: #c0c0c0;">MySQL的核心所在</span>，包括parsing, analysis, optimization, caching和内置函数在内的功能均在此实现。提供所有跨引擎的功能，例如procedures, triggers, views</li>
<li>第三层是<span style="background-color: #c0c0c0;">存储引擎，负责存取数据</span>。每种存储引擎各有特长。MySQL使用Storage engine API与之通信</li>
</ol>
<p><span style="text-decoration: underline;"><strong>连接管理与安全性</strong></span></p>
<p> 每个客户端<span style="background-color: #c0c0c0;">连接在服务端都有自己对应的线程</span>，连接进行的查询在单线程中运行，对应了一个CPU或者核，MySQL会缓存线程供不同连接重用。</p>
<p>客户端连接时需要身份验证，可以基于用户名密码的方式，使用SSL时，则可以基于X.509数字证书验证。</p>
<p>客户端连接成功后，其操作会被授权判断。</p>
<p><span style="text-decoration: underline;"><strong>优化与执行</strong></span></p>
<p>MySQL首先会针对SELECT语句来检查<span style="background-color: #c0c0c0;">查询缓存</span>——其中包含SELECT语句和它关联的结果集——如果语句完全一样，则<span style="background-color: #c0c0c0;">简单的返回缓存的结果集</span>。</p>
<p>然后MySQL会把SQL查询解析为内部的Parse tree结构，并进行一系列的优化，包括：</p>
<ol>
<li>重写（rewriting）查询</li>
<li>确定读取表的顺序</li>
<li>选择使用的索引</li>
</ol>
<p>通过<span style="background-color: #c0c0c0;">在SQL语句中附加提示（Hint）</span>，可以影响上面的优化行为。</p>
<p>MySQL优化器不关心特定表使用了何种存储引擎，但是存储引擎会影响查询的优化，优化器根据<span style="background-color: #c0c0c0;">引擎的特性、特定操作的成本、表的统计信息</span>来决定如何优化。</p>
<div class="blog_h3"><span class="graybg">并发控制</span></div>
<p>任何超过一个SQL需要改变数据时，都存在并发问题。处理并发最简单的手段是锁机制，但是可能带来性能问题。</p>
<p><span style="text-decoration: underline;"><strong>读写锁（Read/Write Locks）</strong></span></p>
<p>共享锁（shared locks），又称读锁，<span style="background-color: #c0c0c0;">只会阻塞其它的写锁</span></p>
<p>独占锁（exclusive locks），又称写锁，会<span style="background-color: #c0c0c0;">阻塞其它的读锁、写锁</span></p>
<p><span style="text-decoration: underline;"><strong>锁粒度（Lock Granularity）</strong></span></p>
<p>进行选择性的锁定而不是锁住整个资源，可以增大并发。锁定策略（locking strategy）是一种数据安全性与锁定成本（lock overhead）的折衷。相比起其它数据库，MySQL给予用户给多的锁粒度选择的可能（不限制引擎的实现方式）：</p>
<ol>
<li>表锁（Table locks）：写数据（insert, delete, update）时获取整张表的锁定，其它客户端不能读取或者写入此表。通常写锁在等待队列中具有比读锁更高的优先级</li>
<li>行锁（Row locks）：允许最高的并发（和最高的锁定成本），InnoDB 、XtraDB支持行锁</li>
</ol>
<div class="blog_h3"><span class="graybg">事务</span></div>
<p>考虑下面的场景：将Jane的200美元从她的活期账户转移到储蓄账户：</p>
<ol>
<li>确保获取账户的余额大于200</li>
<li>把活期账户余额减去200</li>
<li>把储蓄账户的余额增加200</li>
</ol>
<p>对应的SQL语句：</p>
<pre class="crayon-plain-tag">START TRANSACTION;
SELECT balance FROM checking WHERE customer_id = 10233276;
UPDATE checking SET balance = balance - 200.00 WHERE customer_id = 10233276;
UPDATE savings SET balance = balance + 200.00 WHERE customer_id = 10233276;
COMMIT;</pre>
<p> 如果执行到上面第四行，数据库服务器崩溃了，会发生什么？白白扣除Jane 200美元？要实现事务性，必须通过ACID测试：</p>
<ol>
<li> 原子性（Atomicity）：整个事务作为不可分操作完成，要么提交，要么回滚</li>
<li>一致性（Consistency）：数据库只能在两个一致性状态之间变换，例如上面的例子，即使在第四行崩溃，也不会出现数据不一致性，因为事务绝不会提交</li>
<li>隔离性（Isolation）：一般的，事务操作的结果在提交前，对其它事务不可见。这依赖于事务隔离级别的配置</li>
<li>持久性（Durability）：一旦提交，事务对数据的改变即被持久化，不会因为系统崩溃而丢失</li>
</ol>
<p><span style="text-decoration: underline;"><strong>隔离级别</strong></span></p>
<p><span style="background-color: #c0c0c0;">SQL标准定义了4种隔离级别，低的隔离级别带来更多的并发、更低的成本（overhead）：</span></p>
<ol>
<li>读取未提交（READ UNCOMMITTED）：不同事务互相看到对方未提交的修改。允许脏读（dirty read）</li>
<li>读取已提交（READ COMMITTED）：大部分数据库的默认隔离级别（MySQL不是），存在不可重复读问题，在同一个事务中两次运行同一个查询，结果可能不一样。允许不可重复读（nonrepeatable read）</li>
<li>可重复读（REPEATABLE READ）：MySQL默认级别。保证在一个事务中，多次读取同一行，其数据保持一致。允许幻影读（phantom reads），幻影读在读取一个范围的数据时会发生，出现数据变多或变少的情况</li>
<li>串行化（SERIALIZABLE）：最高的隔离级别，强制事务排队执行。</li>
</ol>
<p><strong><span style="text-decoration: underline;">死锁</span></strong></p>
<p>死锁在多个事务同时持有、而又请求对方的资源时发生。数据库依赖于死锁检测、超时等机制来解决死锁问题。例如<span style="background-color: #c0c0c0;">InnoDB会立即检测到死锁并回滚持有最少独占行锁的事务</span>。</p>
<p>锁的行为和次序是存储引擎相关的，所有同样的业务场景在某些引擎下死锁，另外一些则不会。</p>
<p><span style="text-decoration: underline;"><b>事务日志（Transaction Logging）</b></span></p>
<p>事务日志可以让事务处理的效率更高。存储引擎可以在发生数据变更时，<span style="background-color: #c0c0c0;">不去写表，而是写在内存中，随后写入事务日志中</span>。事务日志虽然和写表都是磁盘操作，但是前者是小范围的顺序写入，而后者是大范围的随机写入，故前者效率很高。</p>
<p><span style="text-decoration: underline;"><b>MySQL中的事务</b></span></p>
<p><strong>自动提交（AUTOCOMMIT）</strong></p>
<p>默认情况下，MySQL运作在自动提交模式——除非手工开启事务，否则每个语句在一个事务中运行。</p>
<p>运行：SET AUTOCOMMIT = 0;可以禁止自动提交，注意这个设置对非事务性表，例如MyISAM 、Memory表没有意义。</p>
<p>某些语句，例如DDL、LOCK TABLES可能强制性的进行提交。</p>
<p><strong>事务中混合多种引擎（Mixing storage engines in transactions）</strong></p>
<p>在一个<span style="background-color: #c0c0c0;">事务中包含多种存储引擎的操作是不可靠的</span>。例如：对于事务性表A、非事务性表B，在一个事务中进行，如果成功，则没有问题，如果失败，<span style="background-color: #c0c0c0;">非事务性表是无法回滚</span>的。</p>
<p><strong>隐含和明确锁定（Implicit and explicit locking）</strong></p>
<p> InnoDB<span style="background-color: #c0c0c0;">使用两阶段锁定协议</span>（two-phase locking protocol）。可以在事务的<span style="background-color: #c0c0c0;">任何阶段获得锁，但是只有在提交或回滚时才释放锁</span>——在同时释放所有锁。根据隔离级别的设置，InnoDB 隐含的处理所有锁。尽管如此， InnoDB支持明确锁定：</p>
<ol>
<li>SELECT ... LOCK IN SHARE MODE</li>
<li>SELECT ... FOR UPDATE</li>
</ol>
<p>此外，MySQL在上层<span style="background-color: #c0c0c0;">支持表锁定和解锁</span>（使用命令：LOCK TABLES、UNLOCK TABLES）</p>
<div class="blog_h3"><span class="graybg">多版本并发控制</span></div>
<p>大部分MySQL引擎不是简单的使用行锁定，而是使用MultiVersion Concurrency Control (MVCC)，这是很多数据库例如Oracle、PostgreSQL等都在使用的技术。MVCC在<span style="background-color: #c0c0c0;">很多情况下避免锁定</span>，因而性能较好，MVCC通常会实现<span style="background-color: #c0c0c0;">无锁读（nonlocking reads）</span>，只有在写入时要求锁定。</p>
<p>MVCC通过<span style="background-color: #c0c0c0;">保持某些时间点的数据快照</span>来实现无锁读。这意味着，单个事务可以看到一致性的数据（事务开始的时间点）；而不同事务在同一时间看同一张表，数据却可能是不同的。</p>
<p>每种存储引擎使用不同的方式实现MVCC，有些变种包括乐观、悲观并发控制的功能。InnoDB的实现方式：为每行添加额外的2个隐藏字段来记录行<span style="background-color: #c0c0c0;">被创建的时间、过期（或删除）的时间，</span>注意，InnoDB并不使用真实时间，而是数字的版本号（每个事务开始时版本号增加）来记录上述两个记录，在可重复度隔离级别下，InnoDB的MVCC的行为如下：</p>
<ol>
<li>SELECT：InnoDB必须检查每行确保满足以下2条规则：<br />a)：必须找到<span style="background-color: #c0c0c0;">行的至少小于等于事务的版本</span>——即数据在事务前即存在，或者事务创建了此数据<br />b)：行的删除版本必须未定义或者大于事务的版本——即数据不是在事务之前删除的</li>
<li>INSERT：InnoDB把当前系统版本号设置为新行的版本号</li>
<li>DELETE：InnoDB把当前系统的版本号设置为行的Deletion版本号</li>
<li>UPDATE：InnoDB写入行的拷贝，把系统版本号赋予这一新行，同时把系统版本号赋予旧行的Delete版本号</li>
</ol>
<p>以上行为保证了大部分的读不需要锁定，缺点是需要额外存储、管理许多数据。</p>
<p>MVCC仅与REPEATABLE READ 、READ COMMITTED一起工作。</p>
<div class="blog_h3"><span class="graybg">MySQL的存储引擎</span></div>
<p>MySQL存储每一个数据库（又称schema）在数据目录（data directory）下提供一个文件夹。创建表时，表的定义存放在table_name.frm文件中。</p>
<p><span style="text-decoration: underline;"><strong>InnoDB引擎</strong></span></p>
<p>最常用的引擎，也是默认的事务引擎。InnoDB把数据存放在单个或者一系列称为表空间（tablespace）的文件中，<span style="background-color: #c0c0c0;">MySQL4.1以后InnoDB存储数据和索引到不同文件</span>，支持在原始磁盘分区上（raw disk partitions）构建表空间。</p>
<p>InnoDB的默认隔离级别为<span style="background-color: #c0c0c0;">REPEATABLE READ，在此级别下，使用next-key锁策略（next-key locking strategy）来防止幻影读</span>——不仅仅锁定SQL涉及的行，还<span style="background-color: #c0c0c0;">锁定索引结构中的间隙（gaps）</span>，阻止幻影数据被插入。</p>
<p>InnoDB表建立在聚簇索引（clustered index）上。InnoDB<span style="background-color: #c0c0c0;">按主键查找的速度非常快</span>，但是普通索引（secondary indexes）包含主键列信息，因此，如果<span style="background-color: #c0c0c0;">主键大，则索引也会很大</span>，因此对于具有很多索引的大表，选择较小的主键可以提高性能。</p>
<p>InnoDB<span style="background-color: #c0c0c0;">索引的存储结构是平台中立的</span>，从Windows上拷贝到Linux上没有任何问题。</p>
<p>InnoDB支持真正的<span style="background-color: #c0c0c0;">热备份</span>。</p>
<p><strong><span style="text-decoration: underline;">MyISAM引擎</span></strong></p>
<p>MySQL 5.1或者更老版本的默认引擎。该引擎提供一系列特性，例如：<span style="background-color: #c0c0c0;">全文索引、压缩、空间（spatial）数据库</span>。但是<span style="background-color: #c0c0c0;">不支持事务和行锁</span>，此外，它还<span style="background-color: #c0c0c0;">不是宕机安全的</span>（non-crash-safe）。对于<span style="background-color: #c0c0c0;">只读数据、表不是特别大</span>（修复起来不是很痛苦），可以选择此引擎。</p>
<p>MyISAM引擎把数据文件和索引文件单独存放，扩展名分别为：.MYD 、.MYI。MyISAM支持<span style="background-color: #c0c0c0;">定长（fixed-length）的行</span>，会根据DDL自动选择是否启用。</p>
<p>MyISAM具有以下特性：</p>
<ol>
<li>锁定与并发：<span style="background-color: #c0c0c0;">MyISAM锁定整张表</span>，而不是行。读操作共享锁定其涉及的所有表，写操作独占锁定目标表。但是，<span style="background-color: #c0c0c0;">SELECT查询运行时，可以进行插入操作</span>。</li>
<li>修复：支持手工或者自动修复表。使用REPAIR TABLE语句或者离线时使用myisamchk可以修复。修复的速度是非常慢的</li>
<li>索引特性：支持对BLOB 、TEXT前500字符进行索引，支持<span style="background-color: #c0c0c0;">全文索引</span></li>
<li>延迟索引写入（Delayed key writes）：创建时标记为DELAY_KEY_WRITE的表，不会立即写入索引数据到磁盘，而是使用内存缓冲。这可以提高性能，但是宕机后索引必定坏掉，需要修复</li>
</ol>
<p>MyISAM重要的<span style="background-color: #c0c0c0;">性能问题是表锁定</span>，如果很多查询处于Locked状态，说明此问题严重。</p>
<p><span style="text-decoration: underline;"><b>选择正确的引擎</b></span></p>
<p>以下场景可以考虑MyISAM：</p>
<ol>
<li>只读或者几乎只读的表</li>
<li>需要使用全文索引</li>
</ol>
<div class="blog_h2"><span class="graybg">Benchmarking MySQL</span></div>
<p>可以使用sysbench来进行MySQL的性能测试。主要度量包括：<span style="background-color: #c0c0c0;">吞吐量</span>（Throughput，单位时间内的事务数）、<span style="background-color: #c0c0c0;">响应时间</span>（Response time or latency，任务消耗的时间，这是性能最根本的指标）、<span style="background-color: #c0c0c0;">并发</span>（Concurrency，高并发情况下的测试）、<span style="background-color: #c0c0c0;">稳定性</span>（Scalability）</p>
<div class="blog_h2"><span class="graybg">剖析服务器性能</span></div>
<div class="blog_h3"><span class="graybg">剖析MySQL查询</span></div>
<p><span style="text-decoration: underline;"><strong>分析服务器负载</strong></span></p>
<p>缓慢查询日志（slow query log）可以整体上分析服务器的性能。服务器端变量<span style="background-color: #c0c0c0;">long_query_time</span>用于设定阈值，设为零可以捕获所有查询。</p>
<p>SHOW FULL PROCESSLIST也可以看到缓慢的语句。</p>
<p id="profiling-a-single-query"><span style="text-decoration: underline;"><strong>分析单个查询</strong></span></p>
<p><strong>使用SHOW PROFILE<a id="show-profile-for-single-query"></a></strong></p>
<pre class="crayon-plain-tag">-- 启用当前会话的Session
SET profiling = 1;
-- 执行目标SQL
-- 显示剖析结果
SHOW PROFILES;
-- 显示单个查询的耗时
SHOW PROFILE FOR QUERY 1;</pre><br />
<pre class="crayon-plain-tag">-- 使用INFORMATION_SCHEMA.PROFILING表剖析
SET @query_id = 1;

SELECT
	STATE,
	SUM(DURATION) AS "总计耗时",
	ROUND(
		100 * SUM(DURATION) / (
			SELECT SUM(DURATION)
			FROM INFORMATION_SCHEMA.PROFILING
			WHERE QUERY_ID = @query_id
		),2 ) AS "耗时占比",
	COUNT(*) AS "调用次数",
	SUM(DURATION) / COUNT(*) AS "平均耗时"
FROM
	INFORMATION_SCHEMA.PROFILING
WHERE
	QUERY_ID = @query_id
GROUP BY
	STATE
ORDER BY
	Total_R DESC;</pre>
<p><strong>使用SHOW STATUS</strong></p>
<p>此命令显示一些计数器，缺点是没有时间的度量。可以使用FLUSH STATUS清空记录，并执行需要分析的SQL，再次查询检查状态的变化。</p>
<p><strong>使用Performance Schema</strong></p>
<div class="blog_h2"><span class="graybg">优化Schema和数据类型</span></div>
<div class="blog_h3"><span class="graybg">选择最佳数据类型</span></div>
<p>选择正确的数据类型 ，对于MySQL的性能具有重要作用。以下是一些指导性规则：</p>
<ol>
<li>数据尽可能的小，不论用什么类型</li>
<li>简单即可，例如整数类型比字符类型的性能好（后者具有字符集、字符比较问题）。应当使用MySQL内置类型表示时间日期、使用数字存储IP地址</li>
<li>仅可能的避免空值。可空列不利于MySQL进行查询优化</li>
<li>使用整数来代替实数（DECIMAL）</li>
<li>对于特别短的字符串（例如1字符），CHAR优于VARCHAR；对于定长、基本定长字符串，CHAR优于VARCHAR</li>
<li>对于变长（特别是最大长度比平均值大很多）字符串、UTF8字符串，适合VARCHAR</li>
<li>使用枚举代替字符串</li>
</ol>
<p><span style="text-decoration: underline;"><strong>整数类型</strong></span></p>
<p>支持<span style="background-color: #c0c0c0;">TINYINT,SMALLINT, MEDIUMINT, INT, BIGINT</span>，分别占用8, 16, <span style="background-color: #c0c0c0;">24</span>, 32, 64位空间。</p>
<p>支持<span style="background-color: #c0c0c0;">UNSIGNED</span>标记，这样不支持负数，可以增加一倍的最大值。有<span style="background-color: #c0c0c0;">无符号对性能没有影响</span>。</p>
<p><span style="background-color: #c0c0c0;">指定宽度</span>，例如INT(11)，<span style="background-color: #c0c0c0;">对于引擎来说没有任何意义</span>，只是为了交互式工具的需要。</p>
<p><span style="text-decoration: underline;"><b>实数类型</b></span></p>
<p>可以使用DECIMAL来代替BIGINT来存储非常大的整数</p>
<p>FLOAT 、DOUBLE的计算结果与平台上同类型相似，其计算不是精确的；<span style="background-color: #c0c0c0;">DECIMAL支持精确的计算</span>。这两者均支持设置精度（precision），对于DECIMAL可以<span style="background-color: #c0c0c0;">指定小数点前后允许的尾数</span>。</p>
<p>对于DECIMAL，<span style="background-color: #c0c0c0;">MySQL存储9位数字需要4字节</span>，例如DECIMAL(18, 9)，支持9位整数9位小数，需要4+4+1=9字节，1为小数点自己需要的存储。MySQL 5.0以后的版本，DECIMAL最多支持65位数字，但是在计算时，只能支持到和DOUBLE一样的数值范围。</p>
<p><span style="text-decoration: underline;"><strong>字符串类型</strong></span></p>
<p><strong>VARCHAR 和CHAR</strong></p>
<p>字符串类型的存储方式是引擎决定的</p>
<p>VARCHAR是最常用的字符串类型，相比定长的CHAR，它更加节省空间（用多少存多少），一个例外是MyISAM定长行的表。VARCHAR使用1-2字节记录其长度，对于latin1字符集，VARCHAR(10)需要11字节，而VARCHAR(1000)需要1002字节。尽管VARCHAR通过节省磁盘提供性能，但是对于会发生Upadte的行，如果VARCHAR列变了，将会发生引擎依赖的行为，对于InnoDB，将会split the page 来适应行大小的改变。</p>
<p>CHAR则是定长的类型，MySQL会清除尾部的空格。</p>
<p><strong>BLOB 和TEXT</strong></p>
<p>这两者用来存储大的基于二进制、字符的字符串。唯一的区别是一个基于二进制，一个具有字符集</p>
<p>对于TEXT，有TINYTEXT, SMALL TEXT, TEXT, MEDIUMTEXT, LONGTEXT等具体类型，TEXT是SMALLTEXT的同义词</p>
<p>对于BLOG，有TINYBLOB, SMALLBLOB, BLOB, MEDIUMBLOB, and LONGBLOB等具体类型，BLOG是SMALLBLOG的同义词</p>
<p>和其他数据类型不同，MySQL将这两种字段作为具<span style="background-color: #c0c0c0;">有自身标识符的对象</span>来处理，对于InnoDB，这些数据类型并存放在外部（external）的空间。另外这<span style="background-color: #c0c0c0;">两种字段的排序处理也特殊，只会排序max_sort_length前面</span>的字符。</p>
<p>注意内存表不支持这两字段类型，不要使用。</p>
<p><strong>枚举列</strong></p>
<p>枚举列是具有预定义值列表的字符串列。值列表的修改必要DDL。在字符串上下文中，会转换为字符串比较</p>
<p>避免枚举与VARCHAR的连接查询。</p>
<p><span style="text-decoration: underline;"><strong>日期和时间类型</strong></span></p>
<p>MySQL支持多种日期时间类型，<span style="background-color: #c0c0c0;">最细粒度为秒的存储，但是，支持毫秒级别的计算</span>。</p>
<p><strong>DATETIME</strong></p>
<p>支持<span style="background-color: #c0c0c0;">大范围的值</span>，从1001 - 9999年，精确到秒。使用<span style="background-color: #c0c0c0;">整数</span>形式<span style="background-color: #c0c0c0;">YYYYMMDDHHMMSS</span>来存储其值，值<span style="background-color: #c0c0c0;">与时区无关</span>。默认的，MySQL使用可排序无歧义方式显示该字段类型，例如2008-01-16 22:37:08。需要<span style="background-color: #c0c0c0;">8字节</span>存储</p>
<p><strong>TIMESTAMP</strong></p>
<p>存储1970-01-01以来流逝的秒数，只需要<span style="background-color: #c0c0c0;">4字节</span>存储，支持1970-2038年。使用FROM_UNIXTIME() 、UNIX_TIMESTAMP()函数可以转换Unix时间戳、日期。MySQL 4.1+版本该字段的显示与DATETIME一致，<span style="background-color: #c0c0c0;">与时区相关</span>。</p>
<p>时间戳字段默认是NOT NULL的，插入时不指定值，会插入当前时间</p>
<p><span style="text-decoration: underline;"><strong>位包装类型</strong></span></p>
<p>MySQL支持一些用单独位来存储的类型，这些类型从技术上讲属于字符串类型</p>
<p><strong>BIT</strong></p>
<p>MySQL5.0之前只是TINYINT的同义词。可以使用BIT字段存放一个或者多个true/false字段，例如：<span style="background-color: #c0c0c0;">BIT(8)可以存放8个布尔值</span>。对于MyISAM，此数据类型比较节省空间，InnoDB则是使用足够存储其的最小INT类型。在字符串上下文中，会转换为字符串比较</p>
<p><span style="text-decoration: underline;"><b>选择标识符</b></span></p>
<p>整数是最适合最为标识符列的，因为速度快、可以自增长（AUTO_INCREMENT）</p>
<p>如果可能，<span style="background-color: #c0c0c0;">避免使用字符串类型</span>作为标识符。特别是使用MyISAM时，由于其默认对字符串使用packed indexes，则可能导致<span style="background-color: #c0c0c0;">6倍的性能下降</span>。特别<span style="background-color: #c0c0c0;">小心</span>使用<span style="background-color: #c0c0c0;">随机性质的标识符</span>，例如MD5(), SHA1(), UUID()，新插入的数据可能随机的进入大表空间的任何位置，<span style="background-color: #c0c0c0;">导致插入INSERT、某些SELECT性能下降</span>：</p>
<ol>
<li>INSERT慢的原因是，数据可能插入随机的索引位置，导致<span style="background-color: #c0c0c0;">page splits、随机磁盘访问、聚簇索引碎片化</span>（fragmentation）</li>
<li>SELECT慢的原因是，<span style="background-color: #c0c0c0;">逻辑上相邻的列，在物理、内存分布上相距很远</span></li>
<li>随机值导致<span style="background-color: #c0c0c0;">所有查询的缓存效果低</span>下，这和引用的位置（locality of reference）有关——如果整个数据集的热点程度一样，将导致内存缓存命中率低</li>
</ol>
<p>如果必须使用UUID，可以去掉其中的横线，最好是使用UNHEX()转换为16字节的数字，并存储到BINARY(16)列。相比起MD5，UUID还是具有一定的非平均分布特征、序列性的，虽然这不能和INTEGER相比。</p>
<div class="blog_h3"><span class="graybg">设计Schema时的注意点</span></div>
<p>在设计MySQL表结构时，应注意不要：</p>
<ol>
<li>过多的列：存储引擎和上层服务之间的数据格式需要转换，这种转换是以行为单位的，其成本与列数量成正比</li>
<li>过多的JOIN：表连接数量最好十个以下</li>
<li>过大的枚举值列表</li>
</ol>
<div class="blog_h3"><span class="graybg">规范化和反规范化</span></div>
<p>规范化是指对范式的遵从程度。</p>
<p><span style="text-decoration: underline;"><strong>规范化的优缺点</strong></span></p>
<p>优点：</p>
<ol>
<li>UPDATE通常比反规范化设计快</li>
<li>由于没有冗余，需要更新的数据少</li>
<li>规范化设计的表通常比较小</li>
</ol>
<p>缺点：要求过多的JOIN，这不但资源消耗大，并且会导致一些索引策略无效</p>
<p><span style="text-decoration: underline;"><strong>反规范化的优缺点</strong></span></p>
<p>优点：</p>
<ol>
<li>避免JOIN，最糟糕的查询也就是全表扫描。如果数据不再内存中，这会比JOIN快很多，因为避免了随机访问</li>
<li>允许更高效的索引策略，考虑下面的场景：<br />
<pre class="crayon-plain-tag">-- 这是一个规范化的表设计
-- 需要查询高级用户的前十条（根据发布时间）消息
SELECT message_text, user_name
FROM message
    INNER JOIN user ON message.user_id=user.id
WHERE user.account_type='premium'
ORDER BY message.published DESC LIMIT 10;
-- 在上述查询中，MySQL会扫描message表的published索引，对于每一行，需要查找user表
-- 来看他是不是高级用户，如果只有很少的用户是高级的，这个索引策略将是低效的

-- 问题就出在JOIN上，它导致无法在单个索引上同时完成过滤和排序，如果使用非规范化设计，并且在account_type, published上设计联合索引，则会很高效</pre>
</li>
</ol>
<div class="blog_h3"><span class="graybg">缓存和摘要（Summary）表</span></div>
<p>有时候，相比起冗余字段设计，<span style="background-color: #c0c0c0;">缓存表和摘要表是更好的选择，特别是在允许数据不准确</span>（stale）的情况下。</p>
<p>这两种表并不是精确的概念，所谓缓存表，是指其存放<span style="background-color: #c0c0c0;">获取需要很大成本的数据</span>；所谓摘要表，是指其存放经过聚合（aggregated）的数据</p>
<p><span style="text-decoration: underline;"><strong>物化视图（Materialized Views）</strong></span></p>
<p>诸如Oracle、Microsoft SQL Server之类的DBMS提供了物化视图的概念，即<span style="background-color: #c0c0c0;">预先计算并存放在磁盘、可以依据特定策略进行更新</span>的视图。MySQL没有原生的实现，但是可以使用开源的Flexviews工具达到类似的效果，它有如下特性：</p>
<ol>
<li>基于MySQL二进制日志的CDC（Change Data Capture）</li>
<li>一系列用于管理视图定义的存储过程</li>
<li>更新物化数据的工具</li>
</ol>
<p><span style="text-decoration: underline;"><strong>计数表（Counter Tables）</strong></span></p>
<p>对于只有一行数据的计数表，将会导致所有事务并串行执行，极大的降低并发性，<span style="background-color: #c0c0c0;">可以设置一个100行的计数表</span>，然后使用where slot = RAND() * 100的方式进行随机插入，获取总数时，SUM即可。</p>
<div class="blog_h2"><span class="graybg">高性能的索引</span></div>
<div class="blog_h3"><span class="graybg">索引基本知识</span></div>
<p>索引（MySQL中又称keys）<span style="background-color: #c0c0c0;">是用于快速寻找到行的数据结构</span>。不适合的索引会引起性能问题，多索引的优化是最有效的提升查询速度的手段。索引的优化可能会要求查询语句的重写。</p>
<p>索引可以包含多个列，这种情况下<span style="background-color: #c0c0c0;">列顺序很重要</span>，因为MySQL只能对<span style="background-color: #c0c0c0;">最左边的索引前缀</span>做有效的检索。</p>
<p>过多的索引可能降低INSERT、UPDATE、DELETE的性能，特别是过多索引导致超过内存限制的时候。</p>
<p><span style="text-decoration: underline;"><strong>索引的类型</strong></span></p>
<p>索引工作在引擎级别，每个引擎的实现略有不同，某些引擎不能支持部分类型的索引</p>
<p><strong>B树（B-Tree）索引</strong></p>
<p>通常情况下所说的索引，就是这种类型，大部分引擎支持，Archive在5.1之前不支持。不同引擎的实现细节不同：例如MyISAM的索引通过物理位置来引用行；InnoDB则是通过主键值来引用行。</p>
<p>BTree索引之所以能提高性能，是因为避免了全表扫描。从一个ROOT节点开始（不在下图中），其slots存放指向子节点指针，引擎则是沿着这些指针，根据node pages中的值（定义了子节点的值范围）来寻找到合适的指针，最终引擎要么确定其寻找的值不存在，或者到达叶子节点：<img class="aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2012/02/mysql-2.jpg" alt="" width="80%" /></p>
<p><span style="background-color: #c0c0c0;">叶子节点</span>的特殊之处在于，其<span style="background-color: #c0c0c0;">存放了被索引数据的指针</span>，而不是指向其它页的指针。</p>
<p><span style="background-color: #c0c0c0;">BTree适合多种查询：全值匹配、值范围匹配、值前缀匹配、多列索引的第一列匹配、多列索引的某列精确匹配+某列范围匹配、仅索引（Index-only，不去查行数据）查询</span>。</p>
<p>由于BTree节点是排序的，因此该类索引<span style="background-color: #c0c0c0;">不仅适合数据查找，还适合数据排序</span>。WHERE子句依据索引列过滤的同时，使用该索引列<span style="background-color: #c0c0c0;">ORDER BY、GROUP BY</span>不会有额外的开销。</p>
<p>考虑BTree索引key(last_name, first_name, dob)，下面是它的限制：</p>
<ol>
<li> 如果查找不是从<span style="background-color: #c0c0c0;">最左边索引列</span>开始、或者不是用<span style="background-color: #c0c0c0;">单列索引列的前缀</span>查询，则索引对查询无意义</li>
<li>不能<span style="background-color: #c0c0c0;">跳过多列索引中的某列</span>，如果在一个3列索引中，你不指定第2列的值，那么MySQL只会使用第一列进行索引查询</li>
<li>对于多列索引，<span style="background-color: #c0c0c0;">第一个范围查询</span>（非精确查询，例如 last_name = 'Wang' and first_name like 'Al%' and dob='1986-09-12'中的第二个查询条件）<span style="background-color: #c0c0c0;">后的任何索引都用不到</span></li>
</ol>
<p><strong>Hash索引</strong></p>
<p>Hash索引构建在Hash表中，仅仅在根据所有索引列进行查找时有效，引擎根据索引列计算各行的Hash Code。仅内存表支持此类索引</p>
<p><strong>空间索引Spatial (R-Tree) indexes</strong></p>
<p>针对各维度分别索引，用于GIS系统，但是MySQL这方面不是很好，最好选择PostGIS</p>
<p><strong>全文（Full-text）索引</strong></p>
<div class="blog_h3"><span class="graybg">索引的好处</span></div>
<ol>
<li>减少服务器需要检查的数据的量</li>
<li>避免服务器进行排序和临时表</li>
<li>将随机I/O变为顺序I/O</li>
</ol>
<div class="blog_h3"><span class="graybg">高性能索引策略</span></div>
<p>注意：对于 LIKE '%search%'形式的查询，无法使用索引。MySQL只支持在WHERE子句中使用等于、不等于、大于、小于等几种操作符来访问索引，对于 LIKE 'search%'这样形式的查询，MySQL会自动将其转换为大于、小于之类的操作符，从而使用索引。</p>
<p><span style="text-decoration: underline;"><strong>隔离列</strong></span></p>
<p>不要把列作为表达式或者函数调用的一部分，例如：WHERE actor_id + 1 = 5、TO_DAYS(CURRENT_DATE)</p>
<p><span style="text-decoration: underline;"><strong>前缀索引和索引选择度</strong></span></p>
<p>对于很长的列，可以选择前面若干字符进行索引，避免过大的空间占用。前缀索引导致低选择度（唯一索引具有最高的选择度：1）</p>
<p><span style="text-decoration: underline;"><strong>多列索引</strong></span></p>
<p>常见的错误包括，把<span style="background-color: #c0c0c0;">大部分或者所有列单独索引</span>，或者索引列的顺序不正确。</p>
<p>在很多列上分别建立索引，对于大部分查询来说，不能提高性能，MySQL 5.0以上版本有一种索引合并（index merge，通过解释计划可以看到类似Extra: Using union(PRIMARY,idx_fk_film_id)这样的字样）的策略可能对这种零散索引的表有点作用，老版本的MySQL则最多使用一个索引。索引合并有时候能有效工作，更多的时候则是提示表的索引质量较差：</p>
<ol>
<li>如果服务器交叉索引（AND条件）往往以为着应该对相关列建立多列索引</li>
<li>如果服务器联合索引（OR条件），有时缓冲、排序、合并操作会消耗过多的CPU和内存，特别是相关索引的选择度均不高时</li>
</ol>
<p><span style="text-decoration: underline;"><b>选择好的列顺序</b></span></p>
<p>BTree索引中列的顺序，依赖于查询如何使用索引。</p>
<p><span style="background-color: #c0c0c0;">多列索引首先根据第一列排序，然后第二列，依次类推</span>。因此，<span style="background-color: #c0c0c0;">列顺序应该和ORDER BY, GROUP BY, DISTINCT语句中声明的列顺序匹配</span>。</p>
<p>一个老生常谈的规则是“把<span style="background-color: #c0c0c0;">最具选择度的列放左边”，这个规则有时（没有分组、排序要求）</span>有用，但是注意，<span style="background-color: #c0c0c0;">避免随机I/O和排序更加重要</span>。此外，效率不单单取决于选择度，也和用来做过滤的值有关。</p>
<p><strong><span style="text-decoration: underline;">聚簇索引（Clustered Indexes）</span></strong></p>
<p> 聚簇索引不是一种索引类型，而是一种数据存储的方式，所谓<span style="background-color: #c0c0c0;">聚簇，是指具有相邻键值（key）的行被存放在一起</span>。对<span style="background-color: #c0c0c0;">于InnoDB，聚簇索引把BTree索引和对应的行数据存放在一起</span>。当表具有聚簇索引时，其行数据是存放在索引的叶子页（index’s leaf pages）上的，每张表只能具有一个聚簇索引。下图示意具有聚簇索引的表的布局，注意叶子节点包含整个行，其他节点只有索引列：</p>
<p><img class="aligncenter" style="width: 80%;" src="https://blog.gmem.cc/wp-content/uploads/2012/02/mysql-3.jpg" alt="" /></p>
<p><span style="background-color: #c0c0c0;">MySQL仅支持主键作为聚簇索引列</span>，如果不定义主键，则MySQL会<span style="background-color: #c0c0c0;">尝试使用一个非空、Unique索引</span>代替。InnoDB只会以页为单位聚簇记录，相邻的页可能距离很远。</p>
<p>聚簇索引具有以下优点：</p>
<ol>
<li>让相关的数据存放在一起</li>
<li>数据访问速度快，因为聚簇索引同时把索引、数据存放在一个BTree上</li>
<li>使用覆盖索引的查询，可以用到叶子节点上的主键</li>
</ol>
<p>聚簇索引具有以下缺点：</p>
<ol>
<li>插入顺序对插入速度影响大，最好是依据聚簇索引列的顺序来插入，乱序插入后，可以考虑OPTIMIZE TABLE</li>
<li>修改聚簇索引列的代价大，因为强制InnoDB移动其物理位置</li>
<li>构建了聚簇索引的表在插入数据时，受页拆分（Page Splits）的影响，如果被插入数据的key决定它将被插入到一个已满的页内，则split发生，页拆分会导致更多的磁盘占用</li>
<li>全表扫描的速度可能较慢，特别是数据因页拆分而非顺序的存放时</li>
<li>非聚簇索引(nonclustered)占用空间可能很大，因为其叶子节点需要存放主键列</li>
<li>非聚簇索引(nonclustered)的访问需要<span style="background-color: #c0c0c0;">两次索引查找</span>。InnoDB的Adaptive Hash Index可以减少此消耗</li>
</ol>
<p><span style="text-decoration: underline;"><strong>MyISAM和InnoDB数据布局的比较</strong></span></p>
<p>考虑如下的具有两列的表结构：</p>
<pre class="crayon-plain-tag">CREATE TABLE layout_test (
    col1 int NOT NULL,
    col2 int NOT NULL,
    PRIMARY KEY(col1),
    KEY(col2)
);</pre>
<p> 假设按随机顺序插入主键1-100000的数据，并使用OPTIMIZE TABLE来优化表（确保数据按最优方式排列在磁盘上），col1列使用1-100的随机值填充（很多重复值）。</p>
<p><strong>MyISAM的数据布局</strong></p>
<p>下图左侧标记了Row Number，由于这是一个Fixed-Size的表，所以根据Row Number可以迅速的定位到目标行。</p>
<p>主键索引的构建相当简单，就是针对主键顺序进行顺序布局，关联对应的Row Number。</p>
<p>普通索引和主键索引<span style="background-color: #c0c0c0;">没有结构上的差异</span>，只是不是Unique的而已。</p>
<p><img class="aligncenter" style="width: 100%;" src="https://blog.gmem.cc/wp-content/uploads/2012/02/MySQK-10.png" alt="MySQK-10" /></p>
<p><strong>InnoDB的数据布局</strong></p>
<p>由于聚簇索引的关系，InnoDB针对上表的布局完全不同。</p>
<p>首先，如下图，聚簇索引不单单是索引，表本身也包含在其中了：每一个BTree叶子节点包含主键值、事务ID、回滚指针（后面两者和事务、MMVC有关）、以及所有其他的列值。</p>
<p>其次，如下下图，与MyISAM的普通索引不同，InnoDB不是存储行号，而是存储被索引列+主键值。此策略可以<span style="background-color: #c0c0c0;">减少行移动、页拆分时的资源消耗</span>，缺点是索引体积大（特别是主键是很大字段时）</p>
<p><img style="width: 100%;" src="https://blog.gmem.cc/wp-content/uploads/2012/02/MySQL-11.png" alt="MySQL-11" /></p>
<p><span style="text-decoration: underline;"><strong>在InnoDB中按照主键顺序插入数据</strong></span></p>
<p>如果没有任何特殊要求，最好使用自增长（AUTO_INCREMENT）主键，这保证数据按顺序插入，并提供更好的JOIN性能。</p>
<p>最好避免随机性质的主键，例如UUID。</p>
<p>自增长主键和UUID主键的插入性能差异，在到达某个数量级后（例如300万），可能有数倍的差距；索引大小也可能有成倍的差距。</p>
<p>在随机主键场景下，Page Splits以及其造成的碎片（fragmentation）无疑影响了性能，顺序主键和随机主键的数据页变化情况如下图：</p>
<p><img src="https://blog.gmem.cc/wp-content/uploads/2012/02/MySQL-12.png" alt="MySQL-12" width="100%" /></p>
<p>对于顺序主键，InnoDB直接在前一个插入的记录的后面插入新的记录，当前页满了（InnoDB默认页满因子为15/16，留下的空间用于防止修改）后，再下一个记录被插入到新的页中。如果数据加载时按照此顺序进行，那将是非常高效的。</p>
<p>而对于随机主键，由于新插入数据与前一个数据没有递增关系，所有InnoDB通常不能把新数据插入到索引的尾部，而是需要在已有页开辟新空间，则导致以下问题：</p>
<ol>
<li>目标页可能已经被刷入磁盘，并移出缓存，或者目标页从来就没有进入过缓存，则导致了随机磁盘I/O</li>
<li>InnoDB可能不断的分页，来开辟新空间供新插入的行，这导致需要移动很多数据、修改至少3个页</li>
<li>由于不断的分页，页变得稀疏、不规则，最终导致数据碎片化。一段时间后，可能需要运行OPTIMIZE TABLE来整理碎片</li>
</ol>
<p><strong>顺序主键导致更糟糕问题的场景</strong></p>
<p>对于一个<span style="background-color: #c0c0c0;">高并发的插入场景</span>，顺序主键的最高值可能导致竞争热点：</p>
<ol>
<li>大量的并发可能<span style="background-color: #c0c0c0;">争用next-key locks</span></li>
<li>AUTO_INCREMENT的本身的锁机制，可能需要修改innodb_autoinc_lock_mode</li>
</ol>
<p><span style="text-decoration: underline;"><strong>覆盖索引（covering indexes）</strong></span></p>
<p>索引不仅仅需要为WHERE子句建立，还要考虑整个查询语句——MySQL不仅仅用索引快速的找到匹配行，还可以通过索引抓取<span style="background-color: #c0c0c0;">列数据</span>，但是，对于普通索引来说，不能抓取整个行的所有数据。<span style="background-color: #c0c0c0;">覆盖索引</span>可以模仿多聚簇索引（multiple clustered indexes），即，<span style="background-color: #c0c0c0;">抓取查询需要的所有数据</span>，不仅仅是被索引列。覆盖索引可以很好的提高性能，因为它只需要访问索引，而不需要访问数据，这种访问方式有以下好处：</p>
<ol>
<li>索引条目往往比正行数据小的多，MySQL只需要读取很少的数据，特别是对于响应时间主要消耗与拷贝数据的缓存场景（cached workloads）</li>
<li>由于索引是按其索引值来存储的（至少在单个页内），因此，对比从随机磁盘数据获取行，覆盖索引需要较少的I/O。特别是对于MyISAM之类的引擎，通过优化表，可以保证简单的索引查询完全使用顺序索引访问</li>
<li>对于MyISAM之类的引擎，只在MySQL的内存中缓存索引，而由OS缓存数据，访问缓存数据通常意味着系统调用（System Call），这意味着高代价</li>
<li>覆盖索引对于InnoDB特别有意义，由于InnoDB的普通索引需要二次查找，如果使用覆盖索引，则可避免</li>
</ol>
<p>只能使用BTree索引来创建覆盖索引，此外，内存引擎是不支持覆盖索引的。当发起一个<span style="background-color: #c0c0c0;">被索引覆盖</span>的查询，通过解释计划可以看到Extra列，其内容显示为<span style="background-color: #c0c0c0;">Extra: Using index</span>。而对于<span style="background-color: #c0c0c0;">没有被索引覆盖</span>的查询，则会显示<span style="background-color: #c0c0c0;">Extra: Using where</span>。</p>
<p>对于InnoDB，<span style="background-color: #c0c0c0;">索引必定覆盖主键列</span>。</p>
<p><span style="text-decoration: underline;"><strong>基于索引扫描的排序</strong></span></p>
<p> MySQL支持两种产生排序结果集的方法：</p>
<ol>
<li>使用排序操作</li>
<li>按顺序扫描索引</li>
</ol>
<p>MySQL可以使用索引同时完成数据过滤和排序。只有<span style="background-color: #c0c0c0;">ORDER BY子句指定的方向（ASC、DESC）与索引顺序一致，并且对于多表连接，ORDER BY只引用第一个表的列时</span>，才能进行基于索引的排序。此外，与WHERE子句类似，ORDER BY子句只能使用多列索引最左边的前缀进行排序，除非在WHERE子句中把左侧列作为常量：</p>
<pre class="crayon-plain-tag">CREATE TABLE rental (
    PRIMARY KEY (rental_id),
    -- 包含三个列的多列索引
    UNIQUE KEY rental_date (rental_date,inventory_id,customer_id),
    KEY idx_fk_inventory_id (inventory_id),
    KEY idx_fk_customer_id (customer_id),
    KEY idx_fk_staff_id (staff_id),
    ...
);
-- 在WHERE子句指定了三列索引最左侧的值为常量
-- 这样仍然支持基于索引的排序，解释计划不会出现Extra: Using filesort
EXPLAIN SELECT rental_id, staff_id FROM sakila.rental
    WHERE rental_date = '2005-05-25' -- 第一列常量
    ORDER BY inventory_id, customer_id; --排序没有使用左侧列

-- 更多的例子
... WHERE rental_date = '2005-05-25' ORDER BY inventory_id DESC;
... WHERE rental_date &gt; '2005-05-25' ORDER BY rental_date, inventory_id;

-- 下面的例子不能基于索引排序
-- 使用了两个不同的排序方向，而索引列均是升序排列
... WHERE rental_date = '2005-05-25' ORDER BY inventory_id DESC, customer_id ASC;
-- staff_id不再多列索引中
... WHERE rental_date = '2005-05-25' ORDER BY inventory_id, staff_id;
-- customer_id左边的列inventory_id必须存在于排序子句
... WHERE rental_date = '2005-05-25' ORDER BY customer_id;
-- 过滤条件不是常量
... WHERE rental_date &gt; '2005-05-25' ORDER BY inventory_id, customer_id;</pre>
<p><span style="text-decoration: underline;"><strong>打包（前缀压缩的）的索引Packed (Prefix-Compressed) Indexes </strong></span></p>
<p> MyISAM可以对索引前缀压缩，从而减小空间占用，易于在内存中完成匹配。默认MyISAM自动对字符串索引进行压缩操作，可以指定其对整数索引亦进行此操作（创建表时使用PACK_KEYS选项）。压缩索引有时可能降低性能</p>
<p><span style="text-decoration: underline;"><strong>冗余、重复和无用索引</strong></span></p>
<p>MySQL允许在同一列上创建多个索引，并独立的维护它们，这些索引可能影响查询的优化。</p>
<p>重复索引：同样列上、同样类型、同样顺序的索引。没有价值。</p>
<p>冗余索引：BTree索引(A,B)、(A)对于列A是冗余的，因为前者亦可单独用于A列。有时扩充已有索引可能降低性能，这是需要冗余索引，例如在已有一个整数列索引，需要扩充一个长的VARCHAR列的时候——如果建立索引(int_col,varchar_col)，并保留(int_col)可能是最好的选择。</p>
<p>冗余索引可能导致插入性能降低，对于上面(int_col,varchar_col)的例子，百万数据的插入性能可能成倍下降（InnoDB）甚至数倍下降（MyISAM）。</p>
<p><span style="background-color: #c0c0c0;">对于从来不会使用的无用索引，应该删除</span>。</p>
<p><span style="text-decoration: underline;"><strong>索引与锁定</strong></span></p>
<p>如果SQL语句（例如for update，注意普通查询不做任何锁定）<span style="background-color: #c0c0c0;">不去touch</span>其不需要的行，则需要锁定的行也很少，这提高了性能，因为：</p>
<ol>
<li>尽管InnoDB具有很高效的行锁，且需要很少的内存，但是行锁定还是有一些成本</li>
<li>锁定更多的行，引起锁争用，降低了并发性</li>
</ol>
<p>InnoDB仅在访问行时锁定它们，并且索引索引可以减少访问和锁定的行数，但是，<span style="background-color: #c0c0c0;">只有InnoDB在引擎级别能够过滤掉不需要的行才能减少锁定</span>，否则，InnoDB把结果集返回到MySQL Server层，结果集中的所有行都被锁定了（MySQL 5.1和以后的版本，InnoDB能够<span style="background-color: #c0c0c0;">在Server层完成过滤后</span>解锁相关行）</p>
<p>即使在使用索引的时候，也可能锁定不必要的行，如果没有索引，在引擎级别可能锁定所有的行（全表扫描）</p>
<div class="blog_h3"><span class="graybg">一个索引案例</span></div>
<p>本节使用一个在线交友网站的例子来简述索引的设计与使用，假设有一张用户信息表，需要支持country, state/region, city, sex, age, eye color等多种条件组合过滤、支持基于最后一次在线时间、排名进行排序。</p>
<p><span style="text-decoration: underline;"><strong>支持多种方式过滤</strong></span></p>
<p>需要考虑哪些列最常出现在WHERE子句中，哪些列的distinct值较多，这会成为建立索引的优选</p>
<p>country、sex列虽然distinct值较少，但是几乎会包含在所有查询中，所以，我们创建一系列以(sex,country)为前缀的索引，尽管这个决定与传统的最佳实现背道而驰，我们有足够的理由：</p>
<ol>
<li>这两列几乎所有查询中都用到，甚至，我们可以设计为每次用户必须选择这两列作为查询条件</li>
<li>通过一定的技巧，可以使这样的索引没有什么负面作用：此技巧就是：如果用户没有指定sex，我们<span style="background-color: #c0c0c0;">可以人工添加 sex in</span> ('m','f')，这个技巧可以保证索引被使用，但是，如果distinct太多，会导致IN 列表过大</li>
</ol>
<p>确认前缀后，需要考虑哪些条件组合会出现在WHERE子句中，并且在没有索引的情况下可能会很慢，明显(sex, country, age) 是一个候选，(sex, country, region, age) 、(sex, country, region, city, age)上也可能需要索引。</p>
<p>如果为它们分别建立索引，则索引可能太多了，需要考虑索引的重用，如果用IN技巧处理region，则(sex, country, age) 可以和(sex, country, region, age)合并为一个索引，但是要注意IN列表过大的问题</p>
<p>对于使用不多的查询列，例如has_pictures, eye_color, hair_color, education，有两个选择：</p>
<ol>
<li>不进行索引，让MySQL进行少量的额外扫描</li>
<li>加入索引并使用IN技巧</li>
</ol>
<p>注意我们把<span style="background-color: #c0c0c0;">age放在索引的最后面</span>，这是因为，<span style="background-color: #c0c0c0;">age通常是一个范围查询</span>，而其他的列要么是相等查询，要么是IN查询——这两个操作符可以常量化索引的左前缀，从而保证索引被优化器尽可能有效的使用。</p>
<p><span style="background-color: #c0c0c0;">IN列表过大</span>可能导致查询速度严重下降，例如下面的语句需要4*3*2=12种组合，WHERE子句需要逐个组合的检查：</p>
<pre class="crayon-plain-tag">WHERE eye_color IN('brown','blue','hazel')
AND hair_color IN('black','red','blonde','brown')
AND sex IN('M','F')</pre>
<p> 12种组合通常不是问题，<span style="background-color: #c0c0c0;">但是如果组合数上千</span>，就要注意了：对于老版本的MySQL，优化器可能需要很长时间的执行、消耗大量的内存；对于新版本的MySQL，则会在超过一定的组合数量后停止优化估算，这会影响索引使用效率。</p>
<p><span style="text-decoration: underline;"><b>避免多个范围查询</b></span></p>
<p>尽管从执行计划上分不出IN (20,21,22)和 &gt;=20 and &lt;=22的区别（都显示为type：range），但是这两种语句对应索引的处理是完全不同的，<span style="background-color: #c0c0c0;">后者会导致MySQL忽视后续的索引</span>。</p>
<p>考虑下面的查询：</p>
<pre class="crayon-plain-tag">WHERE eye_color IN('brown','blue','hazel')
AND hair_color IN('black','red','blonde','brown')
AND sex IN('M','F')
AND last_online &gt; DATE_SUB(NOW(), INTERVAL 7 DAY)  --最近一周登陆过
AND age BETWEEN 18 AND 25</pre>
<p> MySQL只会使用age或者last_online两者中的一个。如果age无法常量化（值列表过大），则无法把last_online放到索引的尾部，则必须使用某种变通的方法，例如：使用JOB来处理一个active字段，如果最近一周没有登陆，则设置为0，在用户登陆时设置为1，然后把索引改成类似(active, sex, country, age)的结构，即可满足需求。</p>
<p>未来版本的MySQL可能支持在单一索引上使用多个范围查询，这样的话，IN技巧就没有价值了。</p>
<p><span style="text-decoration: underline;"><b>优化排序</b></span></p>
<p>对于小结果集的排序，filesort就足够了。</p>
<p>对于大结果集，例如上百万数据，则可能需要为排序建立特殊索引：</p>
<pre class="crayon-plain-tag">-- 索引(sex, rating)可以供下面的查询使用：
SELECT  FROM profiles WHERE sex='M' ORDER BY rating LIMIT 10;</pre>
<p><span style="text-decoration: underline;"><strong>大分页问题</strong></span></p>
<p>如果用户请求离开始处很远的分页信息，例如：</p>
<pre class="crayon-plain-tag">SELECT  FROM profiles WHERE sex='M' ORDER BY rating LIMIT 100000, 10;
-- MySQL必须扫描大量的，而这些最终都是需要扔掉的</pre>
<p> 这将难以避免的导致性能问题，因为高offset导致太多的时间消耗在扫描没有意义的数据上，反正常化、预计算、缓存可能是有效的应对策略，最好的方式是限制用户能够访问的页数——谁会真正关心10000页后面的数据呢？</p>
<p>另外一种解决大分页的问题的策略是<span style="background-color: #c0c0c0;">延迟连接（Deferred Join）</span>:</p>
<pre class="crayon-plain-tag">SELECT  FROM profiles INNER JOIN (
    -- 主键扫描，避免了MySQL收集其最终要扔掉的数据
    SELECT  FROM profiles
    WHERE x.sex='M' ORDER BY rating LIMIT 100000, 10
) AS x USING();</pre>
<div class="blog_h3"><span class="graybg">索引和表维护</span></div>
<p><span style="text-decoration: underline;"><strong>寻找和修复表破坏（Corruption）</strong></span></p>
<p>对于一张表来说，最严重的事情是破坏，对于MyISAM通常发生于Crash之后。索引破坏可能由于硬件故障、MySQL或者OS的BUG。</p>
<p>被破坏的索引可能返回不正确的结果，在<span style="background-color: #c0c0c0;">没有重复值的时候报duplicate-key错误</span>，甚至锁死和崩溃。</p>
<p>可以运行CHECK TABLE来检查表是否被破坏，该命令可以捕获大部分表和索引错误。使用<span style="background-color: #c0c0c0;">REPAIR TABLE</span>命令可以修复表的错误，某些引擎不支持该命令，这时可以使用NoOp的ALTER命令来修复，例如：ALTER TABLE  tab  ENGINE=INNODB。</p>
<p>InnoDB通常不会出现表破坏，除非出现硬件问题，例如内存或磁盘、或者数据文件被外部改动。使用innodb_force_recovery参数可以进入强制恢复模式，或者使用Percona InnoDB Data Recovery Toolkit从被破坏的数据文件中抽取数据。</p>
<p><span style="text-decoration: underline;"><strong>更新索引统计信息</strong></span></p>
<p>当存储引擎给出一个<span style="background-color: #c0c0c0;">非精确的查询检查行数，或者查询计划过于复杂无法估算行数</span>，优化器<span style="background-color: #c0c0c0;">会使用索引统计信息来估算行数</span>。MySQL优化器是基于<span style="background-color: #c0c0c0;">成本</span>的，主要度量依据是<span style="background-color: #c0c0c0;">查询需要访问的数据量</span>。如果索引统计信息不存在，或者过期，可能导致优化器做出错误决定。<span style="background-color: #c0c0c0;">ANALYZE TABLE</span>可以触发生成新的索引统计信息。</p>
<p>MyISAM把索引统计信息存放于磁盘，ANALYZE TABLE会导致表锁定及全表扫描</p>
<p>InnoDB从MySQL5.5开始存放在内存中，使用随机的索引采样来获取统计信息。采样的数据页数通过innodb_stats_sample_pages来设定，默认值为8，增大此值可能提高统计精确度，InnoDB在以下情况下<span style="background-color: #c0c0c0;">会自动执行索引统计</span>：</p>
<ol>
<li>表第一次被打开时</li>
<li>运行ANALYZE TABLE时</li>
<li>表的尺寸发生重大变化时，例如变化了1/16，或者插入了20亿行数据</li>
<li>查询INFORMATION_SCHEMA中的某些表、执行 SHOW TABLE STATUS 、SHOW INDEX时，这可能导致性能下降，可以通过设置innodb_stats_on_metadata禁用</li>
</ol>
<p>MySQL 5.6中，选项innodb_analyze_is_persistent可以使索引统计持久化到系统表中，这有利于系统预热、查询计划的稳定性。</p>
<p><span style="text-decoration: underline;"><b>减少索引和数据碎片</b></span></p>
<p> BTree索引可能因为Page Split变得碎片化（non-filled、nonsequential），从而影响性能（range scan、full index scan可能慢数倍，特别是使用覆盖索引的场景）</p>
<p>表数据同样可能碎片化，包括如下类型：</p>
<ol>
<li>行碎片化：单行被分为多片存储于多个物理位置。即使结果集只需要一行，也会影响性能</li>
<li>Intra-row碎片化：当逻辑上连续的页或者行，在磁盘上不是连续排列时发生。这会影响全表扫描、聚簇索引范围扫描的性能</li>
<li>自由空间碎片化：当数据页中包含大量空白空间时。这会导致服务器读取很多不需要的数据</li>
</ol>
<p>MyISAM会发生各种碎片化，但是InnoDB则不会发生短行（short rows）的碎片化，它会移动行并写在一起。</p>
<p>通过OPTIMIZE TABLE或者dump/reload数据可以整理碎片。对于<span style="background-color: #c0c0c0;">InnoDB的索引碎片的整理，可以通过删除/重建索引完成</span>，对于不支持OPTIMIZE TABLE的存储引擎，可以做NoOp的ALTER TABLE操作。</p>
<div class="blog_h2"><span class="graybg">查询性能优化</span></div>
<div class="blog_h3"><span class="graybg">查询缓慢的原因</span></div>
<p>查询是一个任务，并且被MySQL拆分为<strong><span style="color: #008080;"><a href="/high-performance-mysql-study-note#show-profile-for-single-query"><span style="color: #008080;">子任务</span></a></span></strong>，要优化查询，必须<span style="background-color: #c0c0c0;">消除某些子任务、减少子任务的发生次数、或者加快子任务的执行速度</span>。</p>
<p>基本上，一个查询需要的处理经过解析（Parsed）、计划（Planned）、执行（Executed）等步骤，其中执行是最重要的一步，包含很多<span style="background-color: #c0c0c0;">存储引擎调用</span>（为了获取rows）以及<span style="background-color: #c0c0c0;">Post-retrieval处理</span>（例如分组、排序）。MySQL需要在网络、CPU、特别是磁盘I/O（如果数据不在内存）中花费时间，对于某些存储引擎，可能需要很多上下文切换、系统调用。</p>
<div class="blog_h3"><span class="graybg">优化数据访问</span></div>
<p>最基本的查询缓慢的原因是，处理了太多的数据（绝大部分是不需要的，只是筛选），分析缓慢查询通常按以下的步骤进行：</p>
<ol>
<li>检查应用程序是否<span style="background-color: #c0c0c0;">获取了不需要的数据</span>，例如：<br />访问太多行：使用limit语句限制返回的行数<br />在联表查询中返回不必要的表的列<br />返回所有列，这可能无法使用覆盖索引，并导致更多的CPU、内存和I/O<br />重复返回同样的数据</li>
<li>检查MySQL是否<span style="background-color: #c0c0c0;">分析了不必要的行</span>，在MySQL中，<span style="background-color: #c0c0c0;">最简洁、粗略的查询成本估算度量是：响应时间、返回的行数、检查的行数</span>。这些信息均会记录在<span style="background-color: #c0c0c0;">slow query log</span>中。<br /><strong>响应时间（Response time）</strong>由两部分组成：service time——MySQL真正用来处理查询的时间，queue time——等待某些资源（I/O的完成、行锁的获取）的时间，这两部分时间并不好区分<br /><strong>检查行数/返回行<strong>数</strong></strong>：最理想的是只检查需要返回的行，现实中则很难实现，例如对于JOIN查询，需要访问两个以上表的多行，并生成结果集，这种情况下检查行通常要比返回行多很多。检查行过多也不一定意味着低效查询，因为较短的行访问起来交快，检查多点也没事<br /><strong>检查行的方式</strong>：有时，思考只返回一行数据的查询，有利于分析查询成本。执行计划结果的type字段反映了<span style="background-color: #c0c0c0;">检查行的方式，包括：全表扫描（full table scan）、索引扫描（index scan）、范围扫描（range scan）、唯一索引扫描（unique index lookup）、常量（constants），</span>后面的访问方式比前面的速度快。如果访问方式不佳，最好是添加适当的索引。MySQL可能以三种方式应用WHERE子句，效果从好多差为：<br />a）在存储引擎层，把过滤条件应用到索引查找操作，并消除不匹配的行<br />b）使用覆盖索引（Extra：Using index）在从索引中获取每一个行后，过滤掉不匹配的行，这发生Server层，但不需要表行的访问<br />c）从表中取得行，然后过滤掉不匹配的行（Extra：Using Where），这发生在Server层，并且需要表行的访问</li>
</ol>
<div class="blog_h3"><span class="graybg">重构查询的方法</span></div>
<p><span style="text-decoration: underline;"><strong>复杂查询vs多个查询</strong></span></p>
<p>在以前网络带宽比较缺乏的时代，倾向于在单个复杂查询中完成工作，但是现在没有这个必要了，MySQL设计为允许快速连接/断开、快速响应简单查询（在普通硬件上MySQL可以每秒响应超过十万个简单查询）。</p>
<p>网络上传输数据相比起在MySQL内存中完成数据处理，速度还是非常慢的，因此，尽可能的使用少的查询还是一个好主意。</p>
<p><span style="text-decoration: underline;"><strong>查询分块（Chopping Up a Query）</strong></span></p>
<p>把查询分为完全一样的“小块”，每次影响少量的数据，这在某些场景下也很有效，例如清除旧的数据。定期删除旧数据的JOB如果在一个<span style="background-color: #c0c0c0;">巨大的DELETE语句中完成，将会导致很多行被长期锁定、事务日志被充满、阻塞其他小的语句<span style="background-color: #ffffff;">。适当的使用LIMIT即可很好的改良。此外，在</span></span>单个批次的DELETE让线程睡眠一会<span style="background-color: #c0c0c0;"><span style="background-color: #ffffff;">也是个好主意，避免负载过于集中，影响系统其他业务的运行。</span></span></p>
<p><span style="text-decoration: underline;"><strong>连接分解（Join Decomposition）</strong></span></p>
<p>很多高性能应用使用连接分解技术，即使用多个单表查询来代替一个连接查询，例如：</p>
<pre class="crayon-plain-tag">SELECT * FROM tag
    JOIN tag_post ON tag_post.tag_id=tag.id
    JOIN post ON tag_post.post_id=post.id
WHERE tag.tag='mysql';
-- 上面的查询可以分解为：
SELECT * FROM tag WHERE tag='mysql';
SELECT * FROM tag_post WHERE tag_id=1234;
SELECT * FROM post WHERE post.id in (123,456,567,9098,8904);</pre>
<p> 咋一看似乎是多此一举，其实这种查询重构具有明显的性能优势：</p>
<ol>
<li>缓存更加有效。某些应用具有缓存单表（映射为Map）数据的能力，如果缓存有效，3个查询中可能有些不需要执行。在使用<span style="background-color: #c0c0c0;">Hibernate的场景</span>中，查询缓存也会更加有效，因为这3张表中，<span style="background-color: #c0c0c0;">如果只有一张表容易变化，那么JOIN查询的缓存将很快失效，而查询重构后，只有1个查询缓存容易失效</span>。</li>
<li>单独执行查询，又是可以减少锁竞争</li>
<li>在应用中进行JOIN，更容易进行Scale，因为可以把表放在不同的服务器上</li>
<li>查询本身可以更加高效，上面的例子中，IN比起JOIN更好</li>
<li>可以减少冗余的行访问，在应用中进行JOIN，意味着每个行只需要获取一次，而JOIN属于反规范化，通常会重复访问很多数据。同样的，这样的重构也减少网络流量和内存消耗</li>
<li>某种程度上，可以认为这种技术是一个手工实现的Hash Join，作为MySQL嵌套循环算法（nested loops algorithm）的替代，Hash Join更加高效</li>
</ol>
<div class="blog_h3"><span class="graybg">查询执行基础知识</span></div>
<p>当客户端发送一个查询给MySQL服务器时，会发生以下事件序列： </p>
<ol>
<li>客户端把SQL语句送到服务器</li>
<li>服务器检查查询缓存，如果命中，则从缓存中获取结果集；否则进入下一步</li>
<li>服务器解析、预处理、优化SQL，并生成执行计划</li>
<li>查询执行引擎通过进行存储引擎API调用，完成计划的执行</li>
<li>服务器返回数据给客户端</li>
</ol>
<p><span style="text-decoration: underline;"><strong> MySQL Client/Server协议</strong></span></p>
<p><span style="background-color: #c0c0c0;">协议是半双工的</span>，任何时刻，MySQL服务器要么在接收数据，要么在发送数据，而不能同时进行。这样的设计使通信简单而快速，但是也有弱点：在接收完消息之前，无法做任何事情。</p>
<p>客户端把整个查询在单个数据报中发送，因此，如果使用很大的查询语句，max_allowed_packet参数就很重要。</p>
<p>相比之下，服务端通常在多个数据报中把响应发过来，客户端在接收完毕之前无法取消，除非强行断开连接，因此必要的LIMIT很重要。</p>
<p>大多数客户端库允许你要么抓取所有结果集并存放在内存，要么逐条抓取（游标），前者是默认行为，在抓取完毕之前，<span style="background-color: #c0c0c0;">查询会处于“Sending data”状态</span>，并不会释放锁和其它资源。</p>
<p><strong>查询状态</strong></p>
<p>每一个MySQL连接（或者说线程）具有一个说明当前其正在做什么的状态字段，使用SHOW FULL PROCESSLIST 命令即可看到当前状态，常见的状态如下表：</p>
<table style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 200px; text-align: center;">状态</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>Sleep</td>
<td>线程正在等待来自客户端的新查询</td>
</tr>
<tr>
<td>Query</td>
<td>线程正在执行查询或者把结果集发送给客户端 </td>
</tr>
<tr>
<td>Locked</td>
<td>线程正在等待Server层授予表锁。注意：基于存储引擎实现的索引，例如InnoDB的行锁，不会导致线程进入Locked状态</td>
</tr>
<tr>
<td>Analyzing and  statistics</td>
<td>线程正在检查存储引擎统计信息，并优化查询</td>
</tr>
<tr>
<td>Copying to tmp table</td>
<td>
<p>线程正在处理查询，把结果集拷贝到<span style="background-color: #c0c0c0;">临时表</span>，可以是因为需要<span style="background-color: #c0c0c0;">GROUP BY，或者filesort、或者UNION</span><br />Copying to tmp table on disk则表示MySQL将<span style="background-color: #c0c0c0;">内存临时表</span>转为<span style="background-color: #c0c0c0;">磁盘临时表</span></p>
</td>
</tr>
<tr>
<td>Sorting result</td>
<td>线程正在进行排序操作</td>
</tr>
<tr>
<td>Sending data</td>
<td>
<p>可能意味着几种状况：</p>
<ol>
<li>线程正在查询的不同Stage之间传递数据</li>
<li>线程正在生成结果集</li>
<li>线程正在把结果集发送给客户端</li>
</ol>
</td>
</tr>
</tbody>
</table>
<p><span style="text-decoration: underline;"><strong>查询缓存</strong></span></p>
<p>在解析查询之前，MySQL就会<span style="background-color: #c0c0c0;">检查查询缓存</span>（如果查询缓存启用的话），这个查找操作是<span style="background-color: #c0c0c0;">大小写敏感的HASH操作</span>。只有语句完全一致，才可能命中缓存。</p>
<p><strong><span style="text-decoration: underline;">查询优化处理</span></strong></p>
<p>该步骤完成执行计划的生成，包含几个子步骤：parsing、preprocessing、optimization</p>
<p><strong>解析器和预处理器</strong></p>
<p>解析器负责把语句转换为parse tree形式，检查语法的合法性。</p>
<p>预处理器对parse tree进行额外的语义检查，例如表和列的存在性、检查访问权限</p>
<p><strong>查询优化器</strong></p>
<p>MySQL使用基于成本的优化器。由于以下原因，有时优化器不能够得到最优执行计划：</p>
<ol>
<li>统计信息错误。Server层依赖于存储引擎提供的统计信息，这些信息可能精确，或者仅仅是大概的数字。例如，由于MVCC，InnoDB不能维护精确的表行数统计信息</li>
<li>成本度量并不是和实际执行成本等价。有时侯读取更多页的计划反而会高效，如果这些页顺序的分布在磁盘上，或者已经被缓存在内存中——优化器并不知道这些信息</li>
<li>MySQL的最优化和我们的理解有所不同，我们通常认为最优化意味着最短的执行时间，而MySQL则认为意味着最低的Cost</li>
<li>MySQL不会考虑当前正在并发执行的SQL语句，这些语句可能影响当前语句的性能</li>
<li>MySQL并不总是采用基于成本的优化，有时采用基于规则的方式，例如：如果语句中存在一个全文MATCH()子句，则会自动尝试使用全文索引</li>
<li>优化器不会考虑不再控制范围内的成本，例如执行存储函数、用户定义函数</li>
<li>优化器不能估算每一个可能的执行计划，这可能导致丢失最优计划</li>
</ol>
<p>MySQL查询优化器是相当复杂的组件，优化可以分为：<span style="background-color: #c0c0c0;">静态优化、动态优化</span>两种：</p>
<ol>
<li>静态优化仅仅通过分析parse tree即可完成，它与WHERE子句中传入的值无关，即时相同的语句使用不同的值执行，优化依旧有效，可以称为“编译时优化”</li>
<li>动态优化则需要根据多种上下文信息来完成，例如WHERE子句中传入的值、索引中具有多少distinct值。每次查询都需要重新优化，可以称为“运行时优化”</li>
</ol>
<p>对于预编译语句、存储过程，MySQL可以只进行一次静态优化，而在每次执行时进行动态优化</p>
<p>以下是常见的MySQL优化：</p>
<ol>
<li>重排连接（Reordering joins）：不一定需要按照SQL中指定的顺序来JOIN，这是一个重要的优化内容</li>
<li>将OUTER JOIN转换为INNER JOIN：MySQL能够识别不必要的OUTER JOIN并自动转换</li>
<li>应用代数等价转换：例如(5=5 AND a&gt;5)会自动转换为a&gt;5</li>
<li>COUNT(), MIN(), MAX()的优化：例如，如果需要寻找BTree最<span style="background-color: #c0c0c0;">左侧列</span>的<span style="background-color: #c0c0c0;">MIN值</span>，只需要请求索引中的<span style="background-color: #c0c0c0;">第一行</span>即可；寻找MAX值则请求最后一行。如果进行了这样的优化，在执行计划里可以看到“<span style="background-color: #c0c0c0;">Select tables optimized away</span>”。此外没有WHERE子句的COUNT(*)会被MyISAM引擎直接优化掉（因为所有表的总数均存放在数据字典）</li>
<li>常量化：如果MySQL发现某些表达式可以简化为常量，会进行优化。例如用户定义@变量在没有发生变化的时候，会被转换为常量表达式，算术表达式也会被转换为常量。此外，一些你可能认为不会常量化的场景下，MySQL也会进行常量化优化：<br />
<pre class="crayon-plain-tag">EXPLAIN SELECT film.film_id, film_actor.actor_id
FROM sakila.film
    INNER JOIN sakila.film_actor USING(film_id) -- 常量化
WHERE film.film_id = 1; -- 常量，只有一行匹配

--- 结果如下，被优化为两个简单查询：
+----+-------------+------------+-------+----------------+-------+------+
| id | select_type | table     | type  | key           | ref   | rows |
+----+-------------+------------+-------+----------------+-------+------+
| 1  | SIMPLE      | film       | const | PRIMARY       | const | 1    |
| 1  | SIMPLE      | film_actor | ref   | idx_fk_film_id | const | 10   |
+----+-------------+------------+-------+----------------+-------+------+</pre>
</li>
<li>覆盖索引：当SELECT子句中所有列被索引覆盖，则不会去寻找行数据</li>
<li>子查询优化：MySQL可以把某些子查询转换为效果等同的形式，将单独查询转换为索引查找</li>
<li>提前结束（Early termination）：MySQL会在<span style="background-color: #c0c0c0;">满足查询要求后尽快结束处理</span>，例如：<br />a）LIMIT语句<br />b）WHERE id = -1发生在只有正数的主键上<br />c）Distinct/not-existsy优化，针对某些DISTINCT、 NOT EXISTS()、LEFT JOIN语句，示例如下：<br />
<pre class="crayon-plain-tag">SELECT film.film_id
FROM sakila.film
    LEFT OUTER JOIN sakila.film_actor USING(film_id)
WHERE film_actor.film_id IS NULL;
 -- 一旦发现右表字段不为空，则立即结束对此电影的处理（通常电影都有很多演员）</pre>
</li>
<li>等同性传播（Equality propagation）：MySQL<span style="background-color: #c0c0c0;">可以识别查询中两列的等同性，例如JOIN的两列</span>，并且把<span style="background-color: #c0c0c0;">WHERE子句在等同列直接进行传播</span>，示例如下：<br />
<pre class="crayon-plain-tag">SELECT film.film_id
FROM sakila.film
    INNER JOIN sakila.film_actor USING(film_id) -- USING强制file_actor.file_id与file表的PK相等
    -- WHERE 子句自动传播给file_actor表的file_id，减少了扫描范围
WHERE film.film_id &gt; 500;  -- WHERE子句限制条件</pre>
</li>
<li>IN()列表比较：MySQL会<span style="background-color: #c0c0c0;">自动排序IN()列表的值，并执行优化的二分查找</span>（binary search）</li>
</ol>
<p><strong>表和索引统计</strong></p>
<p>统计信息是由存储引擎来维护的，像Archive这样的引擎甚至不保存统计信息。Server层（优化器所在）询问存储引擎以下统计信息：</p>
<ol>
<li>表或者索引的总页数</li>
<li>表或者索引的基数（cardinality）</li>
<li>行或者键的长度</li>
<li>键分布信息</li>
</ol>
<p>优化器利用这些信息来协助制定何种执行计划</p>
<p><strong>连接（JOIN）执行策略</strong></p>
<p>MySQL比传统理解更多的使用术语join，它把所有查询看作join——不仅从两张表中匹配行的查询，子查询、单表查询都被看作join：</p>
<ol>
<li>对于FROM中的子查询，首先单独执行它，结果放入临时表，然后将其视为普通表</li>
<li>UNION则被看作多个单端的查询，结果存入临时表，再读取</li>
<li>RIGHT OUTER JOIN被转换为等价的LEFT OUTER JOIN执行</li>
</ol>
<p>MySQL的连接执行策略在目前非常简单：<span style="background-color: #c0c0c0;">每一个JOIN被看作nested-loop join</span>，下面的SQL与对应的伪代码形象的说明这种策略：</p>
<pre class="crayon-plain-tag">SELECT tbl1.col1, tbl2.col2
FROM tbl1 INNER JOIN tbl2 USING(col3)
WHERE tbl1.col1 IN(5,6);
--- 伪代码
outer_iter = iterator over tbl1 where col1 IN(5,6)
outer_row = outer_iter.next
while outer_row  -- 对于左表的每一行，右表的匹配行在嵌套循环里与之结合
    inner_iter = iterator over tbl2 where col3 = outer_row.col3
    inner_row = inner_iter.next
    while inner_row
        output [ outer_row.col1, inner_row.col2 ]
        inner_row = inner_iter.next
    end
    outer_row = outer_iter.next
end

----- 下面是外连接的例子：
SELECT tbl1.col1, tbl2.col2
FROM tbl1 LEFT OUTER JOIN tbl2 USING(col3)
WHERE tbl1.col1 IN(5,6);
--- 伪代码
outer_iter = iterator over tbl1 where col1 IN(5,6)
outer_row = outer_iter.next
while outer_row
    inner_iter = iterator over tbl2 where col3 = outer_row.col3
    inner_row = inner_iter.next
    if inner_row
        while inner_row
            output [ outer_row.col1, inner_row.col2 ]
            inner_row = inner_iter.next
        end
    else  -- 如果右表没有匹配的，则设置一个空行与之匹配
        output [ outer_row.col1, NULL ]
    end
    outer_row = outer_iter.next
end</pre>
<p><strong>执行计划</strong></p>
<p>和很多数据库一样，MySQL不生成字节码来执行查询。查询计划实际上是一个树状的指令，查询执行引擎可以依次执行并最终获得结果：</p>
<p><img class="aligncenter" style="width: 90%;" src="https://blog.gmem.cc/wp-content/uploads/2012/02/MySQL-30.png" alt="" /></p>
<p><strong>连接优化器（The join optimizer）</strong></p>
<p>MySQL优化器最重要的部分是是连接优化器，其决定连接多表的先后顺序。在Oracle的概念里，先执行查询的表称为后面表的<span style="background-color: #c0c0c0;">驱动表</span>。考虑下面的查询：</p>
<pre class="crayon-plain-tag">SELECT film.film_id, film.title, film.release_year, actor.actor_id,
    actor.first_name, actor.last_name
    FROM sakila.film
    INNER JOIN sakila.film_actor USING(film_id)
    INNER JOIN sakila.actor USING(actor_id);

-- MySQL的执行计划如下
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: actor   -- 和声明的顺序相反，从最后一个表开始驱动
         type: ALL
possible_keys: PRIMARY
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 200     -- 如果使用SELECT STRAIGHT_JOIN 语句强制按照声明顺序来连接，这行是951，需要检查的行更多
        Extra:
*************************** 2. row ***************************
           id: 1
  select_type: SIMPLE
        table: film_actor -- 后续表通过PK引用扫描，需要扫描的数量决定于第一个表
         type: ref
possible_keys: PRIMARY,idx_fk_film_id
          key: PRIMARY
      key_len: 2
          ref: sakila.actor.actor_id
         rows: 1
        Extra: Using index
*************************** 3. row ***************************
           id: 1
  select_type: SIMPLE
        table: film
         type: eq_ref
possible_keys: PRIMARY
          key: PRIMARY
      key_len: 2
          ref: sakila.film_actor.film_id
         rows: 1
        Extra:</pre>
<p> 优化器会自动选择合适的顺序，除非你指定STRAIGHT_JOIN关键字（很少有必要）。<span style="background-color: #c0c0c0;">N个表进行连接查询</span>时，会导致需要检查的连接顺序达到<span style="background-color: #c0c0c0;">N阶乘个</span>（这称为可能执行计划的search space）,例如，<span style="background-color: #c0c0c0;">10个表的连接需要3,628,800个不同的检查</span>，这将导致优化极为缓慢，因此，MySQL会在达到一定条件后停止检查，这由参数optimizer_search_depth控制。</p>
<p><strong>排序优化</strong></p>
<p>排序结果集可能是很耗时的操作，应该尽量在较少的行上进行排序。当MySQL无法使用索引进行排序时，Server层必须自行完成排序（<span style="background-color: #c0c0c0;">通过磁盘或者内存，但都称为filesort</span>），如果<span style="background-color: #c0c0c0;">排序缓冲</span>能装得下结果集，则MySQL会在内存中完成排序。</p>
<p>基本上，MySQL具有两种排序算法：</p>
<ol>
<li>两阶段算法：这是老的算法，读取行指针、ORDER BY列，排序，然后读取排完序的列表，重新读取行，生成结果集。由于此算法需要两次读取行，这导致了很多的随机I/O，特别是对于MyISAM。</li>
<li>新算法：读取查询所需的所有列，根据 ORDER BY列排序，并根据排序结果生成结果集。在MySQL 4.1以上支持此算法，该算法把很多随机I/O变为顺序I/O，但是<span style="background-color: #c0c0c0;">需要更多的空间</span>。这导致排序缓冲容易被填满</li>
</ol>
<p>如果<span style="background-color: #c0c0c0;">查询所需所有列SIZE * ORDER BY列数 &lt;= max_length_for_sort_data</span>，则MySQL自动使用新算法。</p>
<p>MySQL排序所需的临时存储空间：<span style="background-color: #c0c0c0;">为每个元组提供fixed-size的空间，该尺寸足够存放最大可能的元组</span>（对于字符串还需要考虑字符集，例如对于UTF-8字符集，100长度的字符串需要300字节的空间）</p>
<p>联表查询时，如果<span style="background-color: #c0c0c0;">ORDER BY仅引用join order中的第一个表</span>，则MySQL可以根据此单表排序，然后进行JOIN处理，在执行计划里会显示：<span style="background-color: #c0c0c0;">Extra:Using filesort</span>。否则，必须先JOIN，然后再临时表里进行排序，执行计划会显示<span style="background-color: #c0c0c0;">Extra:Using temporary; Using filesort</span>。</p>
<p>LIMIT通常发生在排序完成之后，但是在MySQL5.6版本，可能会进行一些优化，在排序前丢弃一些不需要的行</p>
<p><strong>返回结果集给客户端</strong></p>
<p>即使不需要返回结果集，MySQL也会对客户端进行响应，例如通知影响的行数<span style="background-color: #c0c0c0;">。MySQL使用增量的方式向客户端发送数据</span>，当其生成第一条结果数据时，即可以并应该向客户端发送数据。这可以避免MySQL在内存中存放过多的数据。</p>
<div class="blog_h3"><span class="graybg">MySQL查询优化器的限制</span></div>
<p><span style="text-decoration: underline;"><strong>相关性子查询（Correlated Subqueries）</strong></span></p>
<p>MySQL有时把子查询<span style="background-color: #c0c0c0;">优化</span>的特别<span style="background-color: #c0c0c0;">差</span>，特别是<span style="background-color: #c0c0c0;"> WHERE  col IN (SELECT ...)</span>这样的子查询：</p>
<pre class="crayon-plain-tag">-- 很自然的子查询：查询演员1参演的所有电影
SELECT * FROM sakila.film
WHERE film_id IN(
    SELECT film_id FROM sakila.film_actor WHERE actor_id = 1);

-- 你可能期望MySQL这样优化：
SELECT GROUP_CONCAT(film_id) FROM sakila.film_actor WHERE actor_id = 1;
SELECT * FROM sakila.film
WHERE film_id IN(1,23,25,106,140,166,277);

-- 恰恰相反，MySQL尝试从outer表film推入一个关联性：
SELECT * FROM sakila.film
WHERE EXISTS (
    SELECT * FROM sakila.film_actor WHERE actor_id = 1
    -- 自作主张推入的关联性
    -- 执行计划可以看到：DEPENDENT SUBQUERY
    -- 弱国outer表很大，这将导致非常严重的性能问题
    AND film_actor.film_id = film.film_id);

-- 建议如此重写此查询：
SELECT film.* FROM sakila.film
INNER JOIN sakila.film_actor USING(film_id)
    WHERE actor_id = 1;
-- 或者使用GROUP_CONCAT()人工生成IN列表</pre>
<p> 但不是说不能用子查询，有时候子查询可能很快，一切以基准测试为依据。</p>
<p><span style="text-decoration: underline;"><strong>等同性传播</strong></span></p>
<p>当IN列表很大时，等同性传播可能拖慢优化速度</p>
<p><span style="text-decoration: underline;"><strong>并行执行</strong></span></p>
<p>MySQL不支持多处理器并行执行单个查询</p>
<p><span style="text-decoration: underline;"><strong>Hash Join</strong></span></p>
<p>MySQL没有内置的Hash Join支持</p>
<div class="blog_h3"><span class="graybg">查询优化器提示</span></div>
<table style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 200px; text-align: center;">提示 </td>
<td style="text-align: center;"> 说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>HIGH_PRIORITY<br />LOW_PRIORITY</td>
<td>
<p>提示MySQL，相对于在同一个表上执行的查询，当前语句优先级如何。HIGH_PRIORITY让其进入待执行列表的最前面，LOW_PRIORITY则是放到最后面</p>
<p>通常可以用在MyISAM上（表锁），绝不要用在InnoDB上</p>
</td>
</tr>
<tr>
<td>DELAYED</td>
<td>与INSERT、REPLACE一起使用，可以使语句立即返回，待插入行则被放入缓冲，在表空闲时被批量插入。某些引擎没有实现此特性</td>
</tr>
<tr>
<td>STRAIGHT_JOIN</td>
<td>可以仅跟着SELECT，让MySQL根据语句指定的顺序进行JOIN</td>
</tr>
<tr>
<td>SQL_SMALL_RESULT<br />SQL_BIG_RESULT</td>
<td>用于SELECT语句，提示优化器如何、何时在GROUP BY、DISTINCT查询中使用临时表及排序。SQL_BIG_RESULT提示优化器结果集可能很大，最好使用磁盘临时表</td>
</tr>
<tr>
<td>SQL_BUFFER_RESULT</td>
<td>提示MySQL把结果集放入临时表并尽快释放锁</td>
</tr>
<tr>
<td>SQL_CACHE<br />SQL_NO_CACHE</td>
<td>提示结果集是否可以缓存</td>
</tr>
<tr>
<td>FOR UPDATE<br />LOCK IN SHARE MODE</td>
<td>对于支持行锁的引擎，可以对匹配行进行锁定</td>
</tr>
<tr>
<td>USE INDEX<br />IGNORE INDEX<br />FORCE INDEX</td>
<td>提示优化器使用或者忽略某个索引，在5.0以后，可以使用FOR ORDER BY 、FOR GROUP BY来影响排序与分组</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">特殊类型查询的优化建议</span></div>
<p><span style="text-decoration: underline;"><strong>COUNT(*)优化</strong></span></p>
<p>要获取行数时，总是使用COUNT(*)</p>
<p>MyISAM只有在无WHERE子句的COUNT(*)时才有高性能</p>
<p><strong>简单优化</strong></p>
<p>对于MyISAM，有时可以利用其COUNT(*) 特性：</p>
<pre class="crayon-plain-tag">SELECT COUNT(*) FROM world.City WHERE ID &gt; 5;  -- 需要检查1000行
SELECT COUNT(*) FROM world.City WHERE ID &lt;=5;  -- 只需检查5行

-- 优化，使用总数减去&lt;=5，即可得到&gt;5的数目
SELECT (SELECT COUNT(*) FROM world.City) - COUNT(*)
FROM world.City WHERE ID &lt;= 5;</pre>
<p><strong>近似计数</strong></p>
<p>使用EXPLAIN语句，可以得到大概需要检查的行数</p>
<p><strong>复杂优化</strong></p>
<p>可利用概要表（summary tables）、外部缓存（memcached等）来进行总数统计</p>
<p><strong><span style="text-decoration: underline;">优化表连接</span></strong></p>
<ol>
<li>确保连接列（ON或者USING子句）具有必要的索引</li>
<li>尽量保证GROUP BY、ORDER BY仅使用来自单个表的列</li>
</ol>
<p><span style="text-decoration: underline;"><strong>优化子查询</strong></span></p>
<p>通常，尽量使用JOIN代替子查询。但是对于MySQL5.6或者MariaDB等MySQL变体，此规则不适用</p>
<p><span style="text-decoration: underline;"><strong>优化GROUP BY和DISTINCT</strong></span></p>
<p>MySQL可以使用临时表或者filesort来处理GROUP BY，通常，<span style="background-color: #c0c0c0;">使用主键分组具有更高的效率</span>，例如：</p>
<pre class="crayon-plain-tag">SELECT actor.first_name, actor.last_name, COUNT(*)
FROM sakila.film_actor
     INNER JOIN sakila.actor USING(actor_id)
GROUP BY film_actor.actor_id; -- first_name and last_name are dependent on the actor_id</pre>
<p> MySQL自动根据GROUP BY指定的列进行排序，如果不想排序引起filesort，可以指定ORDER BY NULL</p>
<p><span style="text-decoration: underline;"><strong>优化LIMIT和OFFSET</strong></span></p>
<p>分页查询时最常见的问题是<span style="background-color: #c0c0c0;">高OFFSET值导致</span>的，LIMIT 10000,20导致生成10020行，然后扔掉前面的10000行，这是非常昂贵的，这个问题有几个解决思路：</p>
<ol>
<li>禁止访问过大的页</li>
<li>使用提前计算的摘要表</li>
<li>和只包含主键、ORDER BY列的冗余表进行JOIN</li>
<li>使用Sphinx</li>
<li>使用某种能够记录行位置信息的“书签”：<br />
<pre class="crayon-plain-tag">-- 获取前20行
SELECT * FROM sakila.rental
ORDER BY rental_id DESC LIMIT 20;
-- 获取第16049到16030行
SELECT * FROM sakila.rental
WHERE rental_id &lt; 16030  -- 如果此主键总是递增的，无论OFFSET多高，均不会影响性能
ORDER BY rental_id DESC LIMIT 20;</pre>
</li>
<li>使用覆盖索引来进行OFFSET，然后再JOIN需要的其他列：<br />
<pre class="crayon-plain-tag">SELECT film_id, description FROM sakila.film ORDER BY title LIMIT 50, 5;
-- 如果film表非常大，可以优化为：
SELECT film.film_id, film.description
FROM sakila.film
    --这是一个Deferred join，让MySQL在索引中检索尽量少的数据，而不去访问行
    INNER JOIN (
        SELECT film_id FROM sakila.film
        ORDER BY title LIMIT 50, 5  
    ) AS lim USING(film_id);</pre>
</li>
</ol>
<p><span style="text-decoration: underline;"> <strong>优化UNION</strong></span></p>
<p>UNION总会使用到临时表。<span style="background-color: #c0c0c0;">尽量使用UNION ALL而不是UNION</span>，后者会自动增加DISTINCT，导致查询效率变低。</p>
<div class="blog_h2"><span class="graybg">MySQL高级特性</span></div>
<div class="blog_h3"><span class="graybg">分区表（Partitioned Tables）</span></div>
<p>所谓分区表是指有<span style="background-color: #c0c0c0;">多个物理子表（这些子表使用一样的存储引擎）组成的单个逻辑表</span>。可以把<span style="background-color: #c0c0c0;">分区表看作索引的一个粗略形式</span>——Index以很低的成本获取相邻数据，相邻数据要么可以顺序的读取，要么在内存中匹配到；分区表则可以快速的判断出需要的数据在哪个分区里</p>
<p>MySQL的索引是按分区定义的，这与Oracle不同。<span style="background-color: #c0c0c0;">PARTITION BY子句定义了如何分区</span>的方式。</p>
<p>分区表可以减少表的数据访问、集中存储相关行，在以下场景下，分区表特别具有益处：</p>
<ol>
<li>当表<span style="background-color: #c0c0c0;">非常大，不能纳入内存</span>，或者对于<span style="background-color: #c0c0c0;">“热点行”集中在尾部</span>的表（例如日志类的表）</li>
<li>分区表更加容易维护，例如，可以通过<span style="background-color: #c0c0c0;">drop整个分区的方式来删除历史数据</span>，这样做速度很快。可以按分区来优化、检查、修复</li>
<li>分区数据可以物理分布在多个磁盘上，这样可以更有效的使用多磁盘</li>
<li>在某些工作负载下，可以避免性能瓶颈，例如InnoDB的per-index互斥、ext3文件系统的per-inode锁定</li>
<li>可以按分区来备份和恢复</li>
</ol>
<p>MySQL分区表具有一些限制，例如：</p>
<ol>
<li>每个表最多有1024个分区</li>
<li>MySQL5.1的分区表达式必须是整数，MySQL5.5在某些情况下可以根据列值进行分区</li>
<li>主键or唯一索引必须包含分区表达式中出现的所有列</li>
<li>不能使用外键约束</li>
</ol>
<p><span style="text-decoration: underline;"><strong>分区工作原理</strong></span></p>
<p>对于存储引擎来说，表分区就是普通的表；对于用户来说，表分区由Handler Objects表示，无法直接访问。分区表按以下方式实现逻辑操作：</p>
<table style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 80px; text-align: center;">操作 </td>
<td style="text-align: center;"> 实现方式</td>
</tr>
</thead>
<tbody>
<tr>
<td>SELECT</td>
<td>partitioning layer会打开并锁定所有分区，查询优化器会判断是否某些分区可以被忽略掉，然后partitioning layer通过Handler API调用管理分区的存储引擎完成查询</td>
</tr>
<tr>
<td>INSERT</td>
<td>partitioning layer会打开并锁定所有分区，然后决定哪个分区接受此数据，并插入到分区</td>
</tr>
<tr>
<td>DELETE</td>
<td>partitioning layer会打开并锁定所有分区，然后判断哪个分区包含此数据，并从分区删除</td>
</tr>
<tr>
<td>UPDATE</td>
<td>partitioning layer会打开并锁定所有分区，然后判断哪个分区包含此数据，读取，修改，判断哪个分区接受新数据，然后插入目标分区，删除源分区的数据</td>
</tr>
</tbody>
</table>
<p>注意：partitioning layer的锁定行为与存储引擎有关，与对普通表运行这些语句类似。</p>
<p><span style="text-decoration: underline;"><b>分区的类型</b></span></p>
<p>MySQL支持数种分区方式，其中最常用的是range分区——针对列(s)定义一个range值或者函数，例如：</p>
<pre class="crayon-plain-tag">CREATE TABLE sales (
    order_date DATETIME NOT NULL,
    -- Other columns omitted
) ENGINE=InnoDB PARTITION BY RANGE(YEAR(order_date)) (
    -- 按年分区，其他任何返回确定性的整数的函数均可使用
    PARTITION p_2010 VALUES LESS THAN (2010),
    PARTITION p_2011 VALUES LESS THAN (2011),
    PARTITION p_2012 VALUES LESS THAN (2012),
    PARTITION p_catchall VALUES LESS THAN MAXVALUE 
);</pre>
<p> 其它分区方式包括：<span style="background-color: #c0c0c0;">key、hash、list</span>。在MySQL5.5+可以使用<span style="background-color: #c0c0c0;">RANGE COLUMNS分区方式</span>，可以直接使用date-based列进行分区。下面列出一些使用分区的场景：</p>
<ol>
<li>基于hash的子分区（Subpartitioning）可以减少热点行的per-index互斥竞争</li>
<li>基于key分区来减少InnoDB互斥竞争</li>
<li>基于取模函数的range分区，仅保留需要的一部分数据</li>
<li>在一个使用自增长主键的场景下，如果想通过表分区来使最近热点数据clustered在一起，如果想根据date-based列作为分区，其必须作为主键的一部分，这与自增长主键相悖。可以根据表达式<span style="background-color: #c0c0c0;">HASH(id DIV 1000000)</span>来进行分区，这样每100万行会自动形成一个分区，而且最近的数据因为HASH值一样，自动cluster在一起</li>
</ol>
<p><span style="text-decoration: underline;"><strong>如何使用分区</strong></span></p>
<p>考虑如下场景：</p>
<ol>
<li>需要查询一张包含若干年数据的巨大的表，数据量有<span style="background-color: #c0c0c0;">10TB</span>，使用传统<span style="background-color: #c0c0c0;">机械磁盘</span></li>
<li>需要对<span style="background-color: #c0c0c0;">最近几月</span>的数据进行统计分析，数据量<span style="background-color: #c0c0c0;">达1亿行</span></li>
</ol>
<p>该场景下面临的问题：</p>
<ol>
<li><span style="background-color: #c0c0c0;">表太大</span>了，不能扫描整个表</li>
<li>几乎不能使用索引，因为维护成本、空间消耗太大，类似Infobright的系统完全抛弃的BTree索引</li>
<li>根据索引的情况，可能出现<span style="background-color: #c0c0c0;">大量的碎片、聚簇很差的数据</span>，大量<span style="background-color: #c0c0c0;">随机I/O可能导致致命性能问题</span></li>
</ol>
<p>只有两个选项是可行的：</p>
<ol>
<li>查询必须是针对表的portion进行<span style="background-color: #c0c0c0;">顺序扫描</span></li>
<li>期望的表、索引portion<span style="background-color: #c0c0c0;">完整的在内存中匹配</span></li>
</ol>
<p>有两个针对大数据量的策略：</p>
<ol>
<li><span style="background-color: #c0c0c0;">扫描数据但不索引之</span>：仅使用表分区作为导航至期望行的手段，只要WHERE子句仅仅跨越较少的分区，性能可以不错</li>
<li><span style="background-color: #c0c0c0;">索引数据，隔离热点数据</span>：如果除了一小部分热点数据以外，很少使用。可以把热点数据分到足够小的区中，以便可以把数据连同其索引适合内存</li>
</ol>
<p><span style="text-decoration: underline;"><strong>可能的陷阱</strong></span></p>
<ol>
<li><span style="background-color: #c0c0c0;">NULL值可能与表pruning相悖</span>：如果分区函数可能返回NULL，那么<span style="background-color: #c0c0c0;">对应的数据将被存放到定义的第一个分区</span>中。如果第一分区很大，特别是使用扫描但不索引的策略时，性能可能低下。变通办法是<span style="background-color: #c0c0c0;">创建一个dummy第一分区</span>，只要不存放非法数据，这个分区将是空的，检查的代价也就很小了，注意，在MySQL5.5+使用PARTITION BY RANGE COLUMNS不需要此变通</li>
<li><span style="background-color: #c0c0c0;">PARTITION BY 和索引不匹配</span>：假设根据C1分区，而建立C2索引，那么根据C2索引查询需要检查每个分区的索引树，除非索引的所有非叶子节点在内存中，否则比不进行索引扫描更慢，因此，应当<span style="background-color: #c0c0c0;">避免在非分区列上进行索引</span></li>
<li><span style="background-color: #c0c0c0;">分区的选择可能成本很高</span>：不同分区方式具有不同的实现，因此性能表现也不会一样。特别是对于range分区，MySQL需要在分区列表里逐个寻找，如果分区表非常多，可能导致性能低下，这个问题在一行行插入数据时特别明显。解决此问题应该限制分区表的数量，通常<span style="background-color: #c0c0c0;">100个分区</span>在大多数情况下工作良好</li>
<li>打开和锁定分区可能成本很高：打开和锁定表发生在pruning之前，此成本是不可去除的，对于<span style="background-color: #c0c0c0;">简单操作，例如基于主键的单行查询，影响较大</span></li>
<li>维护可能成本很高：诸如创建和DROP表分区的操作很快，但是REORGANIZE PARTITION之类的操作则可能相当耗时，因为其是基于逐行拷贝的方式进行的</li>
</ol>
<p>MySQL5.5+以上版本的分区表技术比较成熟</p>
<p><span style="text-decoration: underline;"><strong>查询优化</strong></span></p>
<p>分区表优化的关键是利用分区函数减少需要访问的分区数量，因此，仅当<span style="background-color: #c0c0c0;">尽可能在WHERE子句中指定partitioned key</span>，使用<span style="background-color: #c0c0c0;">EXPLAIN PARTITIONS</span>可以检查优化器是否在修剪分区：</p>
<pre class="crayon-plain-tag">EXPLAIN PARTITIONS SELECT * FROM sales_by_day WHERE day &gt; '2011-01-01'
*************************** 1. row ***************************
         id: 1
select_type: SIMPLE
      table: sales_by_day
 partitions: p_2011,p_2012

//注意：修剪只能发生在分区函数对应的列（即使基于表达式分区）上，而不能处理表达式的结果：
EXPLAIN PARTITIONS SELECT * FROM sales_by_day WHERE YEAR(day) = 2010
//可以把上面的语句转换为等价形式，以利用修剪
EXPLAIN PARTITIONS SELECT * FROM sales_by_day WHERE day BETWEEN '2010-01-01' AND '2010-12-31'</pre>
<div class="blog_h3"><span class="graybg">视图</span></div>
<p>MySQL支持两种视图实现方式：</p>
<ol>
<li>TEMPTABLE：即根据视图的定义生成临时表，然后在临时表上进行查询。如果视图定义包含<span style="background-color: #c0c0c0;">GROUP BY、 DISTINCT、UNION、聚合函数、子查询等构造，会使用此方式</span></li>
<li>MERGE：即在查询时把视图定义合并到查询语句中，MySQL会尽可能的使用此方式</li>
</ol>
<p>MySQL视图的限制：</p>
<ol>
<li>不支持视图上的触发器</li>
<li>不支持物化视图，可以使用Flexviews实现类似功能</li>
<li>不支持索引的视图，可以使用Flexviews实现类似功能</li>
</ol>
<p><span style="text-decoration: underline;"><strong>可更新视图</strong></span></p>
<p>可更新视图允许使用UPDATE, DELETE, INSERT 语句来修改潜在的表数据，如果<span style="background-color: #c0c0c0;">视图包含GROUP BY, UNION, 或者聚合函数，则不支持更新</span>。修改数据的SQL可以<span style="background-color: #c0c0c0;">包含JOIN，但是被修改的列必须在一个表内</span>。</p>
<pre class="crayon-plain-tag">CREATE VIEW Oceania AS
SELECT * FROM Country WHERE Continent = 'Oceania'
WITH CHECK OPTION; -- 强制任何基于此视图更新的数据匹配视图定义的WHERE约束
-- 因此，下面这样的更新会导致错误
UPDATE Oceania SET Continent = 'Atlantis';</pre>
<div class="blog_h3"><span class="graybg">外键约束</span></div>
<p>InnoDB是唯一支持外键的引擎。</p>
<p>外键可能导致关联表被锁定：例如插入一条数据到子表时，其<span style="background-color: #c0c0c0;">外键引用的父表的对应行也被锁定</span>。这种现象可能导致意外的锁定甚至死锁。</p>
<p>外键可能引起重大的性能开销，某些场景下可以考虑以下代替方案：</p>
<ol>
<li> 在应用程序中控制数据约束</li>
<li>使用枚举值来代替外键来进行列表值约束</li>
<li>使用触发器来实现级联操作</li>
</ol>
<div class="blog_h3"><span class="graybg">在数据库中存储代码</span></div>
<p>MySQL支持触发器、存储过程、存储函数、以及周期性任务中的events。</p>
<div class="blog_h3"><span class="graybg">游标</span></div>
<p>MySQL提供<span style="background-color: #c0c0c0;">只读、单向、服务器端的游标功能</span>，供存储过程或者低级别Client API使用。</p>
<div class="blog_h3"><span class="graybg">预编译语句</span></div>
<p>预编译语句对于需要重复执行的语句可以提高性能，因为：</p>
<ol>
<li>服务器只需要解析语句一次</li>
<li>服务器只需要执行某些优化步骤一次，并缓存这一部分</li>
<li>基于二进制协议传送参数比ASCII方式更加高效（减少网络带宽、客户端内存消耗），特别是BLOB、TEXT字段</li>
<li>MySQL把参数直接存储在服务器缓冲中,减少在服务器内存拷贝值的开销</li>
</ol>
<p>此外，预编译语句也有利于安全，其避免了SQL注入的可能</p>
<p><span style="text-decoration: underline;"><strong>预编译语句的优化过程</strong></span></p>
<p>准备阶段：解析SQL，消除否定表达式、重写子查询</p>
<p>首次执行：如果可能，简化嵌套连接为OUTER JOINS或者INNER JOINS</p>
<p>每次执行：</p>
<ol>
<li>修剪分区</li>
<li>如果可能，消除COUNT(), MIN(), MAX()</li>
<li>移除常量子表达式</li>
<li>检测constant tables</li>
<li>传播等同性</li>
<li>分析和优化ref, range, index_merge</li>
<li>优化JOIN的顺序</li>
</ol>
<div class="blog_h3"><span class="graybg">用户定义函数</span></div>
<p>所谓UDF于存储函数不同，UDF可以基于任何语言编写，通过C进行调用，其性能较高。</p>
<div class="blog_h3"><span class="graybg">插件</span></div>
<p>MySQL支持插件机制，下面是一个简短的插件列表：</p>
<ol>
<li>Procedure 插件：可以对结果集进行后处理</li>
<li>Daemon 插件：作为一个进程与MySQL一起运行，可以进行诸如监听网络连接、执行定期任务等工作。Percona Server的Handler <br />Socket plugin就是一个例子，它监听端口并允许使用NoSQL方式来访问MySQL</li>
<li>INFORMATION_SCHEMA 插件：支持提供INFORMATION_SCHEMA表</li>
<li>Full-text 解析插件：用于支持全文检索</li>
<li>Audit插件：在SQL语句的预定义点接收事件，可以进行记录日志</li>
<li>验证插件：支持例如PAM、LDAP的身份验证扩展</li>
</ol>
<div class="blog_h3"><span class="graybg">字符集</span></div>
<p>所谓字符集（Character Sets）是指<span style="background-color: #c0c0c0;">二进制编码到符号集的映射</span>。排序规则（Collation）是指对指定字符集下不同字符的比较规则。</p>
<p>MySQL字符集设置默认继承规则：</p>
<ol>
<li>创建数据库时，从服务器character_set_server设置继承</li>
<li>创建表时，从数据库设置继承</li>
<li>创建列时，从表设置继承</li>
</ol>
<p>客户端和服务器通信时，可能使用不同的字符集，服务器根据需要进行转换：</p>
<ol>
<li>服务器假设客户端使用character_set_client指定的字符集来发送语句（statement）</li>
<li>服务器接收到语句后，使用character_set_connection来转换字符集,也用character_set_connection来决定如何把数字转换为字符串</li>
<li>当服务器返回结果集或者错误码时，使用character_set_result来转换</li>
</ol>
<p><span style="text-decoration: underline;"><strong>选择字符集合排序规则</strong></span></p>
<p>MySQL 4.1+支持多种字符集合排序规则，包括使用UTF-8编码的多字节Unicode字符。<span style="background-color: #c0c0c0;">排序规则的后缀：_cs, _ci, _bin</span>用来标注使用<span style="background-color: #c0c0c0;">大小写敏感、大小写不敏感、或者根据二进制值</span>排序。</p>
<div class="blog_h3"><span class="graybg">全文检索</span></div>
<p>MySQL 5.6.4+以后的InnoDB支持全文检索，目前对中文的支持不是很好</p>
<div class="blog_h3"><span class="graybg">分布式（XA）事务</span></div>
<p>MySQL5.0+以上版本，部分支持两阶段提交的XA事务，其可作为XA事务参与者（participants），但是不能作为协调者（coordinator）。</p>
<div class="blog_h3"><span class="graybg">查询缓存</span></div>
<p>许多数据库系统能够<span style="background-color: #c0c0c0;">缓存查询的执行计划</span>，MySQL除此之外还能<span style="background-color: #c0c0c0;">直接缓存SELECT的结果集</span>，此即所谓Query Cache。</p>
<p>相关表一旦发生修改，查询缓存即失效。</p>
<p>随着服务器性能的提高，查询缓存往往成为整个服务器上的<span style="background-color: #c0c0c0;">单点竞争热点</span>，因此，可以<span style="background-color: #c0c0c0;">考虑默认禁止查询缓存</span>。如果的确有必要，可以<span style="background-color: #c0c0c0;">设置不超过数十MB的缓存</span>。</p>
<p><span style="text-decoration: underline;"><strong>MySQL如何检查缓存命中</strong></span></p>
<p>检查策略很简单，缓存类似一个HashMap，其key就是查询语句（不做任何处理）的Hash Code。</p>
<p>MySQL<span style="background-color: #c0c0c0;">不会缓存结果集不是确定性的查询</span>，例如带有<span style="background-color: #c0c0c0;">NOW() 、CURRENT_DATE()</span>函数的查询语句，包含<span style="background-color: #c0c0c0;">用户定义函数、存储函数、用户变量、临时表、mysql数据库中的表的查询语句也不会缓存</span>。考虑下面的例子：</p>
<pre class="crayon-plain-tag">... DATE_SUB(CURRENT_DATE, INTERVAL 1 DAY) -- 不能缓存
... DATE_SUB('2014-07-14’, INTERVAL 1 DAY) -- 能缓存</pre>
<p>MySQL<span style="background-color: #c0c0c0;">5.1以前不支持预编译语句的查询缓存</span>。</p>
<p>查询缓存在某些时候能够提高性能，但是<span style="background-color: #c0c0c0;">对读与写均增加额外的消耗</span>：</p>
<ol>
<li>读操作之前必须检查缓存</li>
<li>如果查询是可缓存的，且尚未缓存，那么生成缓存需要一些额外的资源</li>
<li>对于写操作，其必须使相应的缓存条目失效，如果<span style="background-color: #c0c0c0;">缓存碎片严重、缓存特别大</span>，这将严重影响性能</li>
</ol>
<p>对于InnoDB，一旦<span style="background-color: #c0c0c0;">写操作开始，就会失效相应缓存</span>，即使事务尚未提交，并且，<span style="background-color: #c0c0c0;">事务提交前，相应表示不可缓存的</span>。</p>
<p><span style="text-decoration: underline;"><b>查询缓存如何使用内存</b></span></p>
<p>MySQL完全把<span style="background-color: #c0c0c0;">查询缓存置于内存中</span>。查询缓存支持使用可变长度的内存块，每个块知道其类型（存放查询结果/查询语句使用的表/查询文本...）、大小、包含多少数据，并且持有指向前/后物理块/逻辑块的指针。</p>
<p>MySQL启动时，即为查询缓存分配对应的内存。每一次进行缓存时，申请缓存内存中的一个块，其最小大小为query_cache_min_res_unit字节。块的分配属于相对较慢的操作，因为MySQL需要检查空闲块列表，并找到一个足够大的。</p>
<p><span style="text-decoration: underline;"><strong>何时使用查询缓存</strong></span></p>
<p>最适合缓存的查询是<span style="background-color: #c0c0c0;">生成耗时大、结果集小的查询</span>，例如针对大表的COUNT(*)之类的聚合查询。</p>
<p>对于写负载很大的系统应当禁用查询缓存。</p>
<div class="blog_h2"><span class="graybg">优化服务器设置</span></div>
<p>MySQL配置没有特定的公式，只能根据<span style="background-color: #c0c0c0;">实际情况去优化</span>————包括<span style="background-color: #c0c0c0;">负载、数据、应用的需求、以及硬件</span>，MySQL有大量的设置可以改变，但是不应当<span style="background-color: #c0c0c0;">随意的修改、设置很多参数</span>，这样可能导致内存耗尽、导致MySQL使用swap文件。应当<span style="background-color: #c0c0c0;">调整好基本参数（例如InnoDB缓冲池大小、日志文件大小）</span>，并<span style="background-color: #c0c0c0;">把精力放在Schema优化、索引、查询的设计上</span>。如果某些参数需要优化，其必定会在查询响应时间上有所体现。</p>
<div class="blog_h3"><span class="graybg">MySQL配置如何工作</span></div>
<p>从哪里获取配置文件：通过命令行参数指定文件的位置，在Linux下，通常位于/etc/my.cnf、/etc/mysql/my.cnf ：</p>
<pre class="crayon-plain-tag">which mysqld
/usr/sbin/mysqld --verbose --help | grep -A 1 'Default options'
#查找配置文件的位置</pre>
<p><span style="text-decoration: underline;"><strong>语法、作用范围、动态性</strong></span></p>
<p>配置参数均为小写，单词使用下划线或者短横线连接，下面的两个设置（或命令行参数）是等价的：</p>
<pre class="crayon-plain-tag">/usr/sbin/mysqld --auto-increment-offset=5
/usr/sbin/mysqld --auto_increment_offset=5</pre>
<p> 配置参数可能有不同的作用域范围，有些事服务器<span style="background-color: #c0c0c0;">全局范围</span>的，有些是<span style="background-color: #c0c0c0;">连接范围</span>的，有些则是<span style="background-color: #c0c0c0;">针对一个对象的</span>。某些连接范围的参数具有全局等价参数，后者可以看作是其默认值：</p>
<ol>
<li>query_cache_size是全局参数</li>
<li>sort_buffer_size具有全局默认值，每个SESSION可以设置自己的值</li>
<li>join_buffer_size具有全局默认值，可以在SESSION上设置，并且，单个查询可以为每个JOIN设置一个join buffer</li>
</ol>
<p>有些参数允许不停机的情况下<span style="background-color: #c0c0c0;">动态修改（对已经创建的SESSION无效）</span>，这些修改在重启后会消失，例如下面的命令设置SESSION或者GLOBAL的sort_buffer_size：</p>
<pre class="crayon-plain-tag">SET sort_buffer_size = ;
SET GLOBAL sort_buffer_size = ;
SET @@sort_buffer_size := ;
SET @@session.sort_buffer_size := DEFAULT;   -- 设置为GLOBAL默认值
SET @@global.sort_buffer_size := ;</pre>
<p><span style="text-decoration: underline;"><strong>动态参数设置的副作用</strong></span></p>
<p>动态的设置参数可能具有边际效应，例如导致刷空缓冲中的脏块，常见的具有边际效应的动态参数如下表：</p>
<table style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="text-align: center;">参数 </td>
<td style="text-align: center;">说明 </td>
</tr>
</thead>
<tbody>
<tr>
<td style="width: 200px;">key_buffer_size</td>
<td>
<p>设置此值将导致分配指定数量的内存供key buffer（key cache，索引缓存）,但是在使用之前，OS不会提交这些内存给MySQL。</p>
<p>MySQL支持<span style="background-color: #c0c0c0;">创建多个key buffer</span></p>
</td>
</tr>
<tr>
<td>table_cache_size</td>
<td>设置此值后，某个线程<span style="background-color: #c0c0c0;">下次打开表时</span>发生效果，如果设置的值大于缓存中表的数目，则插入缓存；反之，则把不用的表从缓存中删除</td>
</tr>
<tr>
<td>thread_cache_size</td>
<td>下一个Connection关闭后产生效果，如果超过线程缓存限制，则会销毁Connction对应线程而不是缓存之</td>
</tr>
<tr>
<td>query_cache_size</td>
<td>修改此值后，MySQL立即清除所有的查询缓存，调整缓存的大小，并重新初始化缓存。由于MySQL是串行删除缓存的，所以可能导致服务器停顿较长时间</td>
</tr>
<tr>
<td>read_buffer_size</td>
<td>直到查询需要读取缓冲时，MySQL才会分配整块的、指定大小的内存</td>
</tr>
<tr>
<td>read_rnd_buffer_size</td>
<td>直到查询需要读取缓冲时，MySQL才会分配足够其使用的内存</td>
</tr>
<tr>
<td>sort_buffer_size</td>
<td>直到查询需要进行排序时，MySQL才会分配整块的、指定大小的内存</td>
</tr>
</tbody>
</table>
<p><span style="background-color: #c0c0c0;">不要把per-connection的参数全局默认值太高</span>，例如sort_buffer_size，否则将造成巨大的浪费，应当在需要时设置，并在使用完后恢复默认：</p>
<pre class="crayon-plain-tag">SET @@session.sort_buffer_size := ;
-- Execute the query...
SET @@session.sort_buffer_size := DEFAULT;</pre>
<div class="blog_h3"><span class="graybg">创建MySQL配置文件</span></div>
<p>MySQL默认的示例文件中具有很多被注释的配置项，需要注意这些配置项的解释<span style="background-color: #c0c0c0;">可能不是合理的、完整的，甚至是不正确的</span>，这些示例文件对于现代硬件、工作负载来说，已经过期了。</p>
<p>最合适的做法是，<span style="background-color: #c0c0c0;">从头编写一个配置文件</span>，而不是以默认示例配置文件为基础进行修改，下面是一个推荐的配置模板：</p>
<pre class="crayon-plain-tag">[mysqld]
# GENERAL
datadir                    = /var/lib/mysql                  #数据文件位置
socket                     = /var/lib/mysql/mysql.sock       #Socket文件位置，最好不要使用默认值
pid_file                   = /var/lib/mysql/mysql.pid        #PID文件位置，最好不要使用默认值
user                       = mysql                           #运行MySQL的OS用户
port                       = 3306
storage_engine             = InnoDB                          #默认存储引擎，大部分时候InnoDB是最好的选择
# INNODB                                                     #InnoDB正常运行最基本的配置是缓冲池、日志文件大小，默认值均过小了
innodb_buffer_pool_size    =                          #有些建议是设置为75%-80%服务器内存，这不科学
innodb_log_file_size       = 
innodb_file_per_table      = 1                               #每张表一个文件，出于可管理性、灵活性的考虑
innodb_flush_method        = O_DIRECT                        #仅对Unix有意义
# MyISAM
key_buffer_size            = 
# LOGGING
log_error                  = /var/lib/mysql/mysql-error.log
log_slow_queries           = /var/lib/mysql/mysql-slow.log
# OTHER
tmp_table_size             = 32M
max_heap_table_size        = 32M
query_cache_type           = 0
query_cache_size           = 0
max_connections            = 
thread_cache_size          = 
table_cache_size           = 
open_files_limit           = 65535                           #打开文件的限制，现代OS打开文件几乎不消耗什么资源，如果设置的过小，可能导致too many open files 错误
[client]
socket                     = /var/lib/mysql/mysql.sock
port                       = 3306</pre>
<p><span style="text-decoration: underline;"><strong> innodb_buffer_pool_size的正确设置方式</strong></span></p>
<ol>
<li>从服务器内存总量开始计算</li>
<li>减去操作系统、其它程序所需要的内存</li>
<li>减去MySQL其它组件需要的内存，例如每个查询操作需要的缓冲区</li>
<li>减去InnoDB日志文件所需要的内存，以便OS有足够内存来缓存之。最好留有一定的空间供二进制日志缓存使用，特别是使用延迟复制的场景，因为可能需要读取旧的master二进制日志</li>
<li>减去MySQL中其它缓冲、缓存需要空间，例如MyISAM的key cache、查询缓存</li>
<li>将剩余的值除以105%，以去除InnoDB管理buffer pool所需的内存</li>
<li>向下取整，作为目标值</li>
</ol>
<p> 举例：服务器192G内存，作为MySQL专用服务器，只使用InnoDB引擎，不使用查询缓存、没有过多的连接数，InnoDB日志总大小为4G，则InnoDB缓冲池大小估算过程可以如下：</p>
<ol>
<li>考虑2GB或者5%的内存供OS、MySQL其它组件使用</li>
<li>减去4GB供InnoDB日志使用</li>
<li>剩余177GB，向下取整，设置为168GB</li>
</ol>
<p>如果使用MyISAM并且需要缓存其索引，则会有所不同；在Windows下，MySQL存在大内存管理的缺陷，特别是MySQL5.5以前。</p>
<p>最好是设置一个<span style="background-color: #c0c0c0;">安全、较大的值，然后运行服务一段时间</span>，根据工作负载的需要调整，因为MySQL的连接本身占用内存很少，通常在256KB左右，但是如果查询使用了<span style="background-color: #c0c0c0;">临时表、排序、存储过程</span>等，则可能使用很大的内存。</p>
<div class="blog_h3"><span class="graybg">配置内存使用</span></div>
<p>MySQL的内存使用可以分为可控、不可控两类，后者包括：MySQL实例需要的内存、解析查询、管理内部状态需要的内存。内存配置的步骤与上一节类似，可以按以下步骤：</p>
<ol>
<li>确定MySQL可以使用的内存上限</li>
<li>判断per-connection的内存用量，例如：排序缓冲、临时表</li>
<li>判断OS和其它程序需要的内存</li>
<li>其余的内存分配给MySQL缓存，例如InnoDB的buffer pool</li>
</ol>
<p><span style="text-decoration: underline;"><strong>MySQL可以使用多少内存？</strong></span></p>
<p>对于32位的Linux内核，限制进程使用的寻址空间在2.5-2.7GB左右，超过寻址空间限制来使用内存可能导致崩溃。</p>
<p>不同OS对单进程的内存限制不一样，栈大小也需要考虑。</p>
<p>即使是64bit系统，某些限制仍然存在，例如，MyISAM的key buffer在MySQL5.0和以前的版本最多设置到4GB</p>
<p><span style="text-decoration: underline;"><strong>每个连接需要的内存</strong></span></p>
<p>MySQL只需要<span style="background-color: #c0c0c0;">很少的内存来保持connection (thread)的开启状态</span>，也需要一个<span style="background-color: #c0c0c0;">基本数量的内存来执行任何SQL语句</span>。需要确定执行查询时所需要的内存峰值并进行相应的设置，否则查询可能很慢或者失败。</p>
<p>一般不需要考虑最糟糕的峰值占用，例如对于100个连接，设置myisam_sort_buffer_size为256M，那么最糟糕的情况下需要25GB的内存，但是这种情况发生的可能性不大。合理的做法是在真实的Workload下观测服务器中MySQL进程的内存消耗。</p>
<p><span style="text-decoration: underline;"><strong>为OS保留内存</strong></span></p>
<p>OS内存不足的一个指征是频繁的使用Swapping（paging）虚拟内存到磁盘，通常应该至少保留2GB或者5%给OS。</p>
<p><span style="text-decoration: underline;"><strong>为缓存分配内存</strong></span></p>
<p>如果服务器供MySQL专用，那么<span style="background-color: #c0c0c0;">OS保留内存、查询处理所需内存以外的所有内存，均可分配给缓存使用</span>。MySQL缓存是需要内存最大的部分，它使用缓存来避免磁盘访问。对于大部分场景，下面是最重要的缓存类型：</p>
<ol>
<li>InnoDB缓冲池</li>
<li>InnoDB日志文件、MyISAM数据的操作系统缓存</li>
<li>MyISAM的key（索引）缓存</li>
<li>查询缓存</li>
<li>一些不能实际配置的缓存，例如二进制日志、表定义文件的缓存</li>
</ol>
<p>其它类型的缓存使用内存的量很少，不必过分关注。</p>
<p>如果只使用MyISAM，可以完全禁用InnoDB；反之，只需要给MyISAM配置最少的资源（InnoDB内部会使用MyISAM表做一些操作）。</p>
<p><span style="text-decoration: underline;"><strong>InnoDB缓冲池</strong></span></p>
<p>如果主要使用InnoDB表，那么 InnoDB缓冲池将比其它任何组件需要更多的内存。InnoDB<span style="background-color: #c0c0c0;">严重依赖</span>该缓冲池——它负责<span style="background-color: #c0c0c0;">缓存索引、保持行数据、自适应Hash索引、插入缓冲、锁、以及其它内部结构</span>。InnoDB使用<span style="background-color: #c0c0c0;">缓冲池实现延迟写入（delay writes）</span>，可以把多个写操作合并执行。</p>
<p>使用innotop之类的工具可以监视该缓冲池的使用，注意没有必要设置超过需要的大值。过大的缓冲池会<span style="background-color: #c0c0c0;">导致过长的关闭（如果缓冲中有很多脏页，那么关闭时必须写入数据文件，即使强制关闭，在启动时也少不了恢复时间）、预热（warmup）时间</span>。</p>
<p>减少关闭时间：在运行时设置innodb_max_dirty_pages_pct为一个较小值，等待flush线程刷空缓冲池，当脏页数较小（状态变量：Innodb_buffer_pool_pages_dirty）时关闭MySQL。</p>
<p>innodb_max_dirty_pages_pct并不保证在缓冲池中存储更少的脏页，而是控制MySQL停止延迟(lazy)行为的阈值——当脏页占比超过阈值时，MySQL的flush线程会尽快的刷出脏页，保证占比低于阈值。此外当事务日志的空间不足时，会出现“激进刷空”模式。</p>
<p>当大缓冲池搭配慢速磁盘时，服务器可能需要很长的时间来预热，Percona Server提供了一个在重启后重新载入数据页的功能来减少预热时间，MySQL 5.6+亦有类似功能。</p>
<p><span style="text-decoration: underline;"><strong>MyISAM键缓存</strong></span></p>
<p>MyISAM的key caches也称为key buffers，默认为一个，可以创建多个。与InnoDB不同，<span style="background-color: #c0c0c0;">MyISAM只缓存索引，不缓存数据</span>。如果只使用MyISAM，应当分配足够的内存给键缓存。</p>
<p>最重要的配置参数是key_buffer_size，在MySQL5.0-，单个键缓存具有4GB的最大值限制。没必要分配比索引总大小更大的内存：</p>
<pre class="crayon-plain-tag">-- 计算索引总大小
SELECT SUM(INDEX_LENGTH) FROM INFORMATION_SCHEMA.TABLES WHERE ENGINE='MYISAM';</pre>
<p> 默认情况下，只有一个键缓存，下面的语句示意如何创建新的键缓存并把表映射到其上（未明确映射的表，映射到默认缓存）：</p>
<pre class="crayon-plain-tag">#配置文件添加两个新的键缓存
key_buffer_1.key_buffer_size = 1G
key_buffer_2.key_buffer_size = 1G
#现在有3个键缓存了

#把t1、t2表的索引缓存到key_buffer_1
mysql&gt; CACHE INDEX t1, t2 IN key_buffer_1;

#使用init_file选项和下面的命令预先加载索引
LOAD INDEX INTO CACHE t1, t2;</pre>
<p> 使用SHOW STATUS 、SHOW  VARIABLES可以监控键缓存的使用，计算公式为：</p>
<p style="padding-left: 30px;"><span style="background-color: #c0c0c0;">100 - ( (Key_blocks_unused * key_cache_block_size) * 100 / key_buffer_size )</span></p>
<p>如果运行一段时间后，服务器<span style="background-color: #c0c0c0;">没有用满键缓存，可以考虑降低设置</span>的值。</p>
<p>关于键缓存命中率(hit ratio)：数字没有实际意义，不同工作场景下对命中率的要求不同。根据经验，每秒缓存miss更有价值：假设磁盘每秒支持100随机读，那么5次/秒的miss不会造成问题，80次/秒则可能造成问题。<span style="background-color: #c0c0c0;">Key_reads / Uptime</span>可以计算自服务启动以来的miss/s，下面的语句则可以计算最近的miss/s：</p>
<pre class="crayon-plain-tag">-- 每10秒统计
mysqladmin extended-status -r -i 10 | grep Key_reads</pre>
<p> 需要注意的时，MyISAM使用OS缓存来处理数据文件，数据文件通常比索引大，因此，<span style="background-color: #c0c0c0;">通常需要保留比key buffer更大的内存给OS</span>。在完全不使用MyISAM表时，可以设置key_buffer_size=32M左右，因为有时候内部GROUP BY之类操作可能需要MyISAM临时表。</p>
<p><strong>MyISAM key block size</strong></p>
<p>键的块大小很重要，这与MyISAM与OS缓存、文件系统的交互方式有关，错误的设置可能导致read-around wirte——OS在写入数据前必须读出一定的数据，假设系统page size是4KB，而key block size设置为1KB，下面的场景演示了read-around write：</p>
<ol>
<li>MyISAM请求磁盘上的1KB key block</li>
<li>OS从磁盘读取4KB数据并缓存之，然后把其中需要的1KB传递给MyISAM</li>
<li>OS为了丢弃上面的数据，以便缓存其它数据</li>
<li>MyISAM修改1KB数据，要求OS写回磁盘</li>
<li>OS再次读取4KB，合并MyISAM写入的1KB，然后把整个4KB写回磁盘</li>
</ol>
<p>设置<span style="background-color: #c0c0c0;">myisam_block_size</span>与系统page size一致，可以避免read-around write问题。</p>
<p><span style="text-decoration: underline;"><strong>线程缓存</strong></span></p>
<p>Thread cahce缓存没有和Connection关联的线程。Connection到达时，MySQL将其与某个缓存的Thread关联，或者创建新Thread，后者相对较慢。Connection关闭时，把Thread放回缓存，或者销毁。</p>
<p>参数thread_cache_size可以指定线程缓存的大小，观察Threads_created变量，保证其小于10/s即可。Threads_connected变量用于观察当前连接数。</p>
<p>此参数设置的过小并不会节省很多内存，因为缓存/休眠的线程通常仅占用256KB左右的内存。设置过大（例如几千）则可能导致另外的问题，因为有些OS不能很好的处理大量线程，即使大部分都处于休眠状态。</p>
<p><strong><span style="text-decoration: underline;">表缓存</span></strong></p>
<p>与线程缓存类似，但是存放的是代表了表的对象。对象的具体属性与存储引擎相关。</p>
<p>在MySQL5.1+，表缓存分为两个部分：</p>
<ol>
<li>table_open_cache：打开的表的缓存。这个是每个线程单独的缓存</li>
<li>table_definition_cache：已解析的表定义（.frm文件），足够存入所有表定义即可</li>
</ol>
<p><span style="text-decoration: underline;"><strong>InnoDB数据字典</strong></span></p>
<p>InnoDB具有自己的per-table缓存，通常称作表定义缓存或者数据字典，目前不支持手工配置其大小。</p>
<p>如果使用innodb_file_per_table，那么同时打开的*.ibd（数据文件）文件的数量有限制，可以 通过innodb_open_files来控制。</p>
<div class="blog_h3"><span class="graybg">配置I/O行为</span></div>
<p>一些选项控制MySQL如何把数据同步到磁盘、如何进行数据恢复。这些设置体现了<span style="background-color: #c0c0c0;">数据安全性与性能之间的权衡</span>，一般来说，保证数据立即、一致的写入磁盘是需要较大代价的。</p>
<p><span style="text-decoration: underline;"><strong>InnoDB的I/O配置</strong></span></p>
<p>允许对InnoDB的数据恢复（recovers，InnoDB启动后总是会运行数据恢复处理）、打开和刷出（flush）数据的行为进行控制，从而很大程度上的影响恢复和整体性能。InnoDB具有一个复杂的buffer、file链来提高性能和保证ACID属性，链条中的每个环节都是可配置的，这个链条如下图所示：</p>
<p><img src="https://blog.gmem.cc/wp-content/uploads/2012/02/MySQL.1.png" alt="" /></p>
<p><strong>InnoDB的事务日志</strong></p>
<p>事务日志的意义在于<span style="background-color: #c0c0c0;">降低提交的成本</span>——把事务记录到文件（顺序I/O）而不是把buffer pool刷出到磁盘（事务对应索引、数据的修改往往映射到表空间的随机位置，从而导致random I/O）。需要注意的是，对于<span style="background-color: #c0c0c0;">SSD，随机I/O的劣势不像机械磁盘那么严重</span>。</p>
<p>一旦<span style="background-color: #c0c0c0;">事务日志记录到文件，即实现了持久化</span>，即使修改内容没有回写到数据文件中，如果突然宕机，MySQL会在启动时重做日志内容并恢复已提交的事务。</p>
<p>当然，InnoDB最终还是需要把修改内容回写到数据文件，因为<span style="background-color: #c0c0c0;">事务日志具有固定的大小</span>。InnoDB使用循环写入的方式使用日志文件，但是必须确保<span style="background-color: #c0c0c0;">不能覆盖未回写到数据文件的日志</span>。</p>
<p>InnoDB使用<span style="background-color: #c0c0c0;">一个后台线程负责智能回写数据文件</span>。其智能表现在：把写操作进行分组，以尽量实现顺序I/O。后台写入机制使I/O系统与查询负载解耦。</p>
<p>总体的InnoDB事务日志的大小由以下两个参数控制，对写性能具有很大的影响：</p>
<ol>
<li>innodb_log_file_size：默认5MB，单个日志文件的大小</li>
<li>innodb_log_files_in_group：默认2个，日志文件的个数</li>
</ol>
<p>默认值对于高性能工作负载来说太小了，应该<span style="background-color: #c0c0c0;">设置为上百MB，甚至达到GB级别</span>。通常不需要修改日志文件的个数，修改大小的步骤如下：</p>
<ol>
<li>完全的（不能强行关闭，否则日志文件中会存在尚未刷出到数据文件的事务）关闭MySQL</li>
<li>移除旧的日志文件，例如ib_logfile0</li>
<li>重新配置日志文件大小，并重启MySQL</li>
</ol>
<p>权衡日志文件的理想大小：</p>
<ol>
<li>过小的问题：InnoDB需要更多的Checkpoints，导致更多的日志写操作，<span style="background-color: #c0c0c0;">极端情况下，写查询需要等待日志刷出到磁盘，以腾出日志文件空间</span>。</li>
<li>过大的问题：当进行恢复时，InnoDB需要做很多的工作，从而可能需要很多的时间。较新版本的MySQL在这一点上性能有所提高。注意恢复时间还与被修改的数据量、数据分布情况有关。</li>
</ol>
<p><span style="background-color: #c0c0c0;">日志缓冲（log buffer）</span>：当InnoDB修改任何数据时，其会写入事务日志到内存的日志缓冲里，此缓冲<span style="background-color: #c0c0c0;">默认大小为1MB</span>，当以下三种情况之一发生时：<span style="background-color: #c0c0c0;">缓冲满了、事务提交时，或者过了一秒</span>，InnoDB把缓冲刷出到日志文件中。参数innodb_log_buffer_size控制此缓冲的大小。建议的大小范围是1-8MB，除非需要写入大量的BLOB，否则没必要设置的更大。通过SHOW INNODB STATUS查看状态变量<span style="background-color: #c0c0c0;">Innodb_os_log_written</span>，观察10-100秒，检查每秒写入数来判断日志缓冲是否足够大，例如：对于1MB的缓冲，每秒写入的峰值不超过100KB说明缓冲足够了。</p>
<p>Innodb_os_log_written也可以用<span style="background-color: #c0c0c0;">来衡量日志文件是否足够大</span>，例如峰值每秒写入100KB，那么256MB的<span style="background-color: #c0c0c0;">日志文件</span>基本<span style="background-color: #c0c0c0;">够一小时</span>使用了。</p>
<p>日志缓冲只有被刷出，事务才能持久化，设置innodb_flush_log_at_trx_commit可以控制日志缓冲刷出的频率：</p>
<table style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 50px; text-align: center;"> 值</td>
<td style="text-align: center;">说明 </td>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td><span style="background-color: #c0c0c0;">每秒</span>把log buffer写入到log file（在大部分OS，这只是把数据从InnoDB内存移动到系统缓存，仍然在内存中），并刷出log file到持久化存储（Blocked I/O直到写入完成）。但是在事务提交时不做任何操作</td>
</tr>
<tr>
<td>1</td>
<td>
<p><span style="background-color: #c0c0c0;">每次事务提交时</span>把log buffer写入到log file，并刷出log file到持久化存储。这是默认的、最安全的设置，不会丢失任何事务。设置为该值会导致trx/s大大减小，在高速机械磁盘上，只能达到几百次事务/秒</p>
<p>对于高性能事务性应用，应当设置为1，并且把日志文件放在具有电池支持的写缓存RAID磁盘上，这样快且安全。</p>
</td>
</tr>
<tr>
<td>2</td>
<td>每次事务提交时写入log buffer到log file，但是<span style="background-color: #c0c0c0;">不flush（存放在OS的缓存中）</span>。InnoDB每秒进行一次flush调度。与0相比，MySQL崩溃时2不会丢失数据，整个服务器宕机则可能丢失数据</td>
</tr>
</tbody>
</table>
<p><strong>InnoDB如何打开和刷出日志和数据文件</strong></p>
<p>参数innodb_flush_method用于配置InnoDB与文件系统的交互方式（包括读和写，同时影响日志文件、数据文件）：</p>
<table style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="text-align: center;"> 值</td>
<td style="text-align: center;"> 说明</td>
</tr>
</thead>
<tbody>
<tr>
<td style="width: 180px;">fdatasync</td>
<td>
<p>非Windows系统的默认值。InnoDB使用fsync()代替fdatasync()来刷出数据、日志文件。fsync与fdatasync的区别是后者仅写数据，前者同时写文件元数据</p>
<p>使用fsync的缺点是，OS至少会在自己的缓存内缓冲一部分数据，可能（依赖于OS与文件系统）导致不必要的双重缓冲</p>
</td>
</tr>
<tr>
<td>O_DIRECT</td>
<td>
<p>InnoDB使用O_DIRECT标记或者directio()来读写数据文件（对日志文件没影响），某些Unix类系统不支持（GNU/Linux、FreeBSD、Solaris均支持）。</p>
<p>该设置还是使用fsync()，但是指示OS不进行缓存或者预读取（read-ahead），从而避免双重缓冲的问题。该设置不能禁止RAID卡的预读取功能</p>
</td>
</tr>
<tr>
<td>ALL_O_DIRECT</td>
<td>与O_DIRECT类似，但是同时对日志文件起作用。在Percona Server、MariaDB上被支持。</td>
</tr>
<tr>
<td>O_DSYNC</td>
<td>对日志文件起作用，使写操作同步化——完成后才返回</td>
</tr>
<tr>
<td>async_unbuffered</td>
<td>Windows下的默认值，使用Windows原生的异步（重叠）I/O仅需读写</td>
</tr>
<tr>
<td>unbuffered</td>
<td>仅WIndows，与async_unbuffered类似，但是不使用原生异步I/O</td>
</tr>
<tr>
<td>normal</td>
<td>仅WIndows，导致InnoDB不使用异步I/O或者非缓冲I/O</td>
</tr>
<tr>
<td>nosync and littlesync</td>
<td>仅开发时使用</td>
</tr>
</tbody>
</table>
<p><strong>InnoDB表空间</strong></p>
<p>InnoDB把数据存放在表空间中，表空间是一种虚拟文件系统，可以跨越多个磁盘文件。除了数据和索引外，undo log、insert buffer、doublewrite buffer、一些其他的内部数据结构也存放于其中。使用下面的参数配置表空间：</p>
<pre class="crayon-plain-tag">#表空间的目录
innodb_data_home_dir  = /var/lib/mysql/
#表空间文件名和大小，autoextend表示空间用完了自动增长（最好设置最大大小）
innodb_data_file_path = ibdata1:1G;ibdata2:1G;ibdata3:1G:autoextend;ibdata4:1G:autoextend:max:2G</pre>
<p>这样的设置：innodb_data_file_path = /disk1/ibdata1:1G;/disk2/ibdata2:1G并不能分担负载到多个磁盘，因为InnoDB是逐个填满数据文件的，可以通过RAID来分担负载。</p>
<p>使用innodb_file_per_table可以为每个表创建一个名为tablename.ibd的数据文件，有利于把表分散到多个磁盘。这种方式下，对于小表可能存在额外的空间浪费，因为InnoDB页的大小是16KB，如果一个表的数据只有1KB，那么它至少需要占用16KB的空间。此外，innodb_file_per_table可能导致低下的DROP性能，原因如下：</p>
<ol>
<li>DROP表意味着删除文件。在某些文件系统（ext3）可能很慢。可以把ibd文件链接到零字节文件，然后手工删除之，而不是等待MySQL删除</li>
<li>每个表在InnoDB内具有自己的表空间。MySQL移除表空间时，需要锁定并扫描buffer pool来确定哪些页属于该表空间，如果buffer pool很大，该操作会相当耗时。Percona Server提供了innodb_lazy_drop_table参数来解决此问题</li>
</ol>
<p>总之，建议使用innodb_lazy_drop_table配合具有容量上限的表空间，在出现问题时参考上面两条。</p>
<p>不建议使用裸分区（未格式化的）来存储表空间。因为性能提升不大。</p>
<p><strong>其它I/O配置项</strong></p>
<table style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 150px; text-align: center;">参数 </td>
<td style="text-align: center;"> 说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>sync_binlog</td>
<td>
<p>控制MySQL刷出二进制日志到磁盘的方式。0表示由OS决定何时刷出；非0表示刷出前发生多少次二进制日志写操作（自动提交时每个语句对应一个写，否则每个事务对应一个写）。通常设置为0或1。</p>
<p>设置为0时，MySQL崩溃可能导致<span style="background-color: #c0c0c0;">二进制日志与事务数据不同步</span>，从而<span style="background-color: #c0c0c0;">使数据复制、按时间点恢复</span>无法完成。另一方面，设置为1在获取安全性的同时会导致较高的代价——同步二进制日志、事务日志使MySQL必须在两个单独的磁盘位置刷出数据，可能需要磁盘寻道。</p>
<p>在具有不断电写缓存的RAID卷上存放二进制日志能够极大的提高性能。</p>
</td>
</tr>
<tr>
<td>expire_logs_days</td>
<td>自动删除过期的二进制日志。</td>
</tr>
</tbody>
</table>
<p><span style="text-decoration: underline;"><strong>MyISAM的I/O配置</strong></span></p>
<table style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 150px; text-align: center;">参数 </td>
<td style="text-align: center;">说明 </td>
</tr>
</thead>
<tbody>
<tr>
<td>delay_key_write</td>
<td>
<p>控制延迟索引写入</p>
<p>OFF：MyISAM在每次写操作时都刷出key buffer中修改的块到磁盘。除非表被LOCK TABLES锁定<br />ON：启用延迟索引写入，但仅对使用DELAY_KEY_WRITE选项创建的表有效<br />ALL：对所有表启用延迟索引写入<br />延迟索引写入在某些时候有价值，但是不会很大的提高性能。在小数据尺寸、高读索引命中、低写索引命中时最有意义。<br />延迟索引写入具有以下缺点：</p>
<ol>
<li>服务器崩溃时数据块没写出到磁盘时，到导致索引破坏</li>
<li>如果很多写入被延迟，则关闭表需要消耗较多时间（等待写入刷出），在MySQL5.0可能导致较长时间的表缓存锁定</li>
<li>未刷出的脏数据块占用key buffer，可能导致无法从磁盘读入新块，查询因此停顿直到有足够的key buffer空间</li>
</ol>
<p>&nbsp;</p>
</td>
</tr>
<tr>
<td>myisam_recover_options</td>
<td>
<p>控制MyISAM如何从错误中恢复</p>
<p>DEFAULT：指示MySQL修复所有标记为崩溃或者未完全关闭的表<br />BACKUP：保存一个数据文件的备份为.BAK文件便于后续检查<br />FORCE：即使有的行从.MYD文件中丢失，也进行恢复<br />QUICK：除非有删除块，否则跳过恢复</p>
<p>可以使用上述多个选项，使用逗号分隔</p>
</td>
</tr>
<tr>
<td>myisam_use_mmap</td>
<td>
<p>允许通过系统page cache访问.MYD文件</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">配置并发</span></div>
<p><span style="text-decoration: underline;"><strong>InnoDB并发配置</strong></span></p>
<p>InnoDB为高并发场景设计。最近数年InnoDB有很大进步单非完美，某些InnoDB性能指标在高并发时会下降，而有效的处理方法仅仅是降低并发或者升级MySQL版本。在MySQL5.1-高并发是灾难性场景——所有操作在全局互斥对象（例如buffer pool mutex）上列队，导致服务器常常处于挂起状态。</p>
<p>InnoDB具有其自己的线程调度器，来控制<span style="background-color: #c0c0c0;">线程如何进入其“内核”进行数据访问、可以进行哪些操作</span>。</p>
<p><span style="background-color: #c0c0c0;">innodb_thread_concurrency</span>可以用于<span style="background-color: #c0c0c0;">老版本MySQL的线程并发数控制</span>，可以设置为磁盘数*CPU数*2。如果线程数满了，<span style="background-color: #c0c0c0;">后续到达的线程首先休眠innodb_thread_sleep_delay</span>（默认10ms，这个值对于大量小查询场景可能过大了）再次尝试，仍然失败则进入等待队列。</p>
<p>一旦线程进入内核，其持有<span style="background-color: #c0c0c0;">若干票据（tickets）允许其免费再次返回内核（不经过并发检查）</span>。<span style="background-color: #c0c0c0;">innodb_concurrency_tickets</span>控制票据数，这个参数是per-query而非per-transaction的，查询结束后剩余的票据即丢弃，一般不需要修改此参数，除非有很多长时运行（long-running）的查询。</p>
<p><span style="background-color: #c0c0c0;">innodb_commit_concurrency</span>控制同时有多少线程能够进行提交操作。如果innodb_thread_concurrency已经设置的很低，很多线程仍然在等待，可以尝试设置该参数。</p>
<p>另外一个有价值的解决方案是<span style="background-color: #c0c0c0;">使用线程池来限制并发</span>，MariaDB中已经实现，Oracle也为MySQL5.5提供了商业插件。</p>
<p><span style="text-decoration: underline;"><strong>MyISAM并发配置</strong></span></p>
<p>在讨论MyISAM并发设置之前，有必要理解其如何插入和删除数据：</p>
<ol>
<li>删除：不需要重新排序整个表，只是把被删除的行打个标记，在表上形成一个“洞”</li>
<li>插入：MyISAM会倾向于尝试填充删除的洞，以重用空间，如果无法填充或者没有洞，则附加到表的结尾</li>
</ol>
<p><span style="background-color: #c0c0c0;">concurrent_insert</span>参数控制MyISAM的并发插入行为：0表示不允许并发插入，每个INSERT锁定整张表；1表示允许并发插入，只要表中没有洞；2为MySQL5.0+引入的值，强制并发插入附加到表的尾部，即使存在洞（如果没有读线程存在，则会尝试填充洞）</p>
<div class="blog_h3"><span class="graybg">基于工作负载的配置</span></div>
<p><span style="text-decoration: underline;"><strong>优化BLOB和TEXT负载</strong></span></p>
<p>MySQL对BLOB字段的处理和其他类型不同（本节把BLOB和TEXT统称为BLOB，因为其本质上是一样的数据类型），特别是：<span style="background-color: #c0c0c0;">MySQL不能为BLOB值使用内存临时表</span>，因此一个使用临时表的、包含BLOB的查询，不管表有多小，都会导致基于文件的临时表。解决此问题有两个方法：</p>
<ol>
<li>使用SUBSTRING()函数将BLOB转换为VARCHAR</li>
<li>提高临时表的性能，例如将其存放在内存文件系统中（例如tmpfs）。<span style="background-color: #c0c0c0;">参数tmpdir控制临时表的存放位置</span>。</li>
</ol>
<p>对于基于BLOB、TEXT等长列，如果内容大于768字节，InnoDB可能在<span style="background-color: #c0c0c0;">行外部开辟存储空间（external storage space）</span>存放其剩余的部分，将以16KB的页为单位，并且每个长列使用独立外部存储空间。此行为可能导致很大的空间浪费。</p>
<p><span style="text-decoration: underline;"><strong>优化文件排序</strong></span></p>
<p>MySQL5.6针对具有LIMIT子句的查询进行了优化，改变了其使用sort buffer的方式。</p>
<div class="blog_h3"><span class="graybg">InnoDB高级设置</span></div>
<p>新版本的InnoDB引擎具有更多的特性和更好的性能，如果使用MySQL5.1+，可以设置<span style="background-color: #c0c0c0;">ignore_builtin_innodb</span>来忽视内置的InnoDB，然后配置plugin_load来把InnoDB作为插件配置。新版本的InnoDB包含若干可以提高性能、安全性的参数：</p>
<table style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 200px; text-align: center;">参数</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>innodb</td>
<td>设置为FORCE，则InnoDB无法启动的情况下，MySQL启动会失败</td>
</tr>
<tr>
<td>innodb_autoinc_lock_mode</td>
<td>控制InnoDB生成自增长PK的方式，如果很多事务在等待autoincrement lock，则需要考虑此设置</td>
</tr>
<tr>
<td>innodb_buffer_pool_instances</td>
<td>
<p>在MySQL5.5+，可以把buffer pool分成多个segments，这是高并发、多核心CPU的情况下提供MySQL可扩展性（scalability）的最重要的方法。多缓冲池把把工作负载分区，全局性的互斥量不会有太多的争用。可以设置为8个。</p>
<p>Percona Server使用另外一种方式降低争用——细化锁定的粒度。特别是Percona Server5.5，同时支持多个buffer pool和细粒度互斥锁</p>
</td>
</tr>
<tr>
<td>innodb_io_capacity</td>
<td>
<p>InnoDB曾经硬编码的假设：其运行在支持100 I/O的单磁盘上。但是对于高速磁盘，例如SSD，这个假设太低了。</p>
<p>该值设置的越大，每次刷出脏页的数量也就越多。SSD可以配置到2000以上。</p>
</td>
</tr>
<tr>
<td>innodb_read_io_threads<br />innodb_write_io_threads</td>
<td>这两个参数控制InnoDB后台读写I/O线程数，MySQL5.5默认设置为4读4写，对大多数服务器足够了。但是对于具有很多磁盘的高并发场景，可以增加这两个值，或者简单的设置为与物理磁盘的盘面转轴数相等</td>
</tr>
<tr>
<td>innodb_strict_mode</td>
<td>在某些情况下，使InnoDB抛出错误而不是发出警告</td>
</tr>
<tr>
<td>innodb_old_blocks_time</td>
<td>控制一个页从LRU年轻区转移到年老区需要经过的最少毫秒数，默认0，可以设置为1000。</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">操作系统和硬件优化</span></div>
<div class="blog_h3"><span class="graybg">CPU的选择</span></div>
<p><span style="text-decoration: underline;"><strong>多个CPU还是更快的CPU？</strong></span></p>
<p>更快的CPU减低响应时间，更多的CPU则增加吞吐量。</p>
<p>现代服务器通常有多个CPU插槽，每个CPU会有多个核心（独立执行单元），每个核心可能有多个“硬件线程”（现代OS对超线程的支持很好）。本章所说的<span style="background-color: #c0c0c0;">CPU速度指单个执行单元的速度</span>，CPU数则指<span style="background-color: #c0c0c0;">OS看到的CPU数（即：CPU数*核心数*线程数）</span>。</p>
<p>对于<span style="background-color: #c0c0c0;">计算密集型（CPU-bound）工作负载，更快的CPU常常比更多的CPU有优势</span>。由于MySQL不能把单个查询fork到多个CPU上执行，所以对于计算密集型查询，只有提升其速度才能缩短响应时间。<span style="background-color: #c0c0c0;">数据复制（replication）也得益于更快的CPU</span>。</p>
<p>如果需要同时运行<span style="background-color: #c0c0c0;">很多查询</span>，则多个CPU更有优势，目前版本的MySQL可以<span style="background-color: #c0c0c0;">很好的利用16或者24个CPU</span>。</p>
<p>即使不存在很多查询同时执行，MySQL依然可以使用<span style="background-color: #c0c0c0;">空闲CPU来进行后台工作</span>，包括：InnoDB buffer purging、网络操作等。</p>
<p>另外一个判断更快还是更多的方式是检查<span style="background-color: #c0c0c0;">查询实际做了什么</span>。从硬件级别来看，一个查询要么在执行，要么在等待（例如<span style="background-color: #c0c0c0;">在run queue等待</span>，即所有process是runnable而所有CPU均忙；<span style="background-color: #c0c0c0;">等待latch或者lock</span>；<span style="background-color: #c0c0c0;">等待磁盘或者网络</span>）。如果查询在等待latch或者lock，那么最好是有更快的CPU；如果查询在run queue中，则更多更快CPU均可；如果在等待InnoDB log buffer mutex，则说明需要增强I/O能力。</p>
<div class="blog_h3"><span class="graybg">平衡内存与磁盘资源</span></div>
<p>需要大量内存的最重要原因不是需要在内存存放如此多的数据，而是为了避免磁盘I/O。</p>
<p>计算机具有金字塔状的缓存层次，越是上层的，越小越贵：</p>
<p><img class="aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2012/02/MySQL.2.png" alt="" /></p>
<p>每个层次的缓存均应当缓存热点数据以便快速访问。应用程序特定的缓存（例如InnoDB的buffer pool）通常比OS的缓存更加高效，因为它知道自己需要缓存哪些数据。</p>
<p><span style="text-decoration: underline;"><strong>随机I/O对比顺序I/O</strong></span></p>
<p><span style="background-color: #c0c0c0;">随机I/O更多的从缓存中获益</span>——缓存减少了昂贵的磁盘寻道时间，<span style="background-color: #c0c0c0;">顺序I/O通常没必要缓存，除非目标数据完整的fit in 内存</span>。具体分析如下：</p>
<ol>
<li>顺序I/O本省就比随机I/O快，不管<span style="background-color: #c0c0c0;">磁盘还是内存</span>。例如，典型的机械磁盘可能支持每秒100次随机I/O、50MB的顺序I/O，那么对于100字节的row来说，随机读取每秒100行，顺序读取则达到500000行，差距达到5000倍！对于内存，每秒随机I/O可以达到25万次，顺序I/O则可以达到5百万行。可以看到，对于随机I/O，内存比磁盘快2500倍，顺序I/O则仅有10倍。</li>
<li>存储引擎能更快的处理顺序I/O。随机读取通常意味着存储引擎需要进行索引操作——对应了BTree结构导航和值比较，而顺序读通常只需要遍历一个较简单的数据结构。此外，随机读通常用于获取单独的行，但是<span style="background-color: #c0c0c0;">读取的是整个16KB页</span>——可能包括若干行的数据，这里面就存在浪费，因为大部分都是不需要的数据，顺序读则通常需要整个页上的所有数据。</li>
</ol>
<p><span style="background-color: #c0c0c0;">添加内存是解决随机读问题的最好办法</span>。</p>
<p><span style="text-decoration: underline;"><strong>缓存，读取和写入</strong></span></p>
<p>如果有足够的内存，那么可以让读取变成纯粹的内存操作——在服务器预热完成后。而写入则不然，或迟或早，写入必须被持久化到磁盘中。<span style="background-color: #c0c0c0;">延迟的写入</span>可以增加性能，除此之外，写入数据的分组有利于提高性能：</p>
<ol>
<li>Many writes，one flush：单行数据在刷出磁盘之前，可能在内存中被修改多次</li>
<li>I/O merging：内存中发生的多个不同的数据修改可以合并为一个写操作</li>
</ol>
<p>以上两点也是为何很多数据库系统使用写前日志（write-ahead logging）的原因，所谓写前日志就是指零散的数据更改不直接写回到数据文件，而是先写入到一个顺序的日志文件中，后台线程负责把日志中的更改写到数据文件，它可以进行必要的优化。</p>
<p><span style="text-decoration: underline;"><strong>你的工作集（Working Set）是什么？</strong></span></p>
<p>所谓工作集，是指工作真正需要的那一部分数据。在MySQL中，可以认为工作集是最频繁使用的页的集合，包括数据和索引。工作集应该以缓存单元（cache unit）度量，对于InnoDB，一个缓存单元为16KB（默认，页的大小），因此，即使只读取1条数据，也会导致整个页被载入buffer pool，这可能导致很大的浪费。聚簇索引把相关的数据放在一起，尽量的减少这种浪费。</p>
<p><span style="text-decoration: underline;"><strong>寻找一个合适的内存/硬盘比率</strong></span></p>
<p>通过性能基准测试，来确认一个可接受的缓存丢失率（cache miss rate）。缓存丢失率和CPU占用紧密相关，例如。如果一段时间内CPU处于used达到99%，而处于I/O等待为1%，那么缓存丢失率是可接受的。</p>
<p>加大内存和缓存丢失率并不是线性的关系，这和工作负载有关，例如，10GB内存对应了10%缓存丢失率，那么要减小到1%丢失率可能需要500G而不是50G内存。</p>
<p><span style="text-decoration: underline;"><strong>选择硬盘</strong></span></p>
<p>传统机械硬盘读取数据需要三个步骤：</p>
<ol>
<li>把读取磁头移动到磁盘表面的适当位置</li>
<li>等待磁盘转动，直到需要的数据到达磁头下面</li>
<li>等待磁盘转过所有需要的数据</li>
</ol>
<p>其中1、2步消耗的时间叫access time，<span style="background-color: #c0c0c0;">小的随机访问主要的消耗时间是access time</span>。3步消耗的时间主要取决于<span style="background-color: #c0c0c0;">传输速度，大的顺序读取的主要消耗时间</span>主要在第3步。</p>
<p>多种因素影响磁盘的选择，考虑一个流行新闻网站——需要很多小的随机读，需要考虑以下因子：</p>
<table style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 250px; text-align: center;"> 因素</td>
<td style="text-align: center;">说明 </td>
</tr>
</thead>
<tbody>
<tr>
<td>存储容量(Storage capacity)</td>
<td>现代磁盘大部分足够大，如果不够，把小磁盘组成RAID </td>
</tr>
<tr>
<td>传输速度(Transfer speed)</td>
<td>现代磁盘的传输速度都非常快，速度取决于<span style="background-color: #c0c0c0;">转轴速度和磁盘数据密度、以及与主机的接口</span>（很多现代磁盘读取数据的速度大于接口传输速度）。对于在线网站来说，速度通常不是问题，因为主要是小的随机读</td>
</tr>
<tr>
<td>访问时间(Access time)</td>
<td>这是<span style="background-color: #c0c0c0;">影响随机读取速度最大的因素</span></td>
</tr>
<tr>
<td>转轴速度(Spindle rotation speed)</td>
<td>包括7200、10000、15000转，对顺序读取、随机读取均有较大的影响</td>
</tr>
<tr>
<td>物理尺寸</td>
<td>其它参数相同的情况下，尺寸越小，磁头移动耗时也越小</td>
</tr>
</tbody>
</table>
<p> InnoDB很容易扩展到多个磁盘。MyISAM则不是。</p>
<div class="blog_h3"><span class="graybg">固态（Flash）存储技术</span></div>
<p>亦称非易失随机存取存储器（NVRAM），与传统硬盘的结构非常不同。主要可以分为<span style="background-color: #c0c0c0;">两类：SSD、PCIe cards</span>，前者通过实现SATA来模拟标准硬盘，后者则使用特殊驱动，作为块设备。固态存储具有以下特点：</p>
<ol>
<li>相对于机械硬盘，具有<span style="background-color: #c0c0c0;">很好的随机读、写性能</span>。通常读性能相对于写更好一些</li>
<li><span style="background-color: #c0c0c0;">更好的顺序读、写性能</span>。某些入门的固态存储，顺序读写速度可能不如高速传统硬盘</li>
<li><span style="background-color: #c0c0c0;">更好的并发支持</span></li>
</ol>
<p>其中1和3对于数据库来说是最重要的提升。很多反规范化设计的Schema就是为了避免随机I/O。</p>
<p>在未来，RDBMS将因为固态存储技术发生深刻的改变，过去几十年RDBMS已经针对机械磁盘做了大量优化，对于固态存储则没有。</p>
<p><span style="text-decoration: underline;"><strong>固态存储概览</strong></span></p>
<p>固态存储最重要的一个特征是，可以多次快速的读取小的单元，而写的时候则有复杂的问题需要处理——<span style="background-color: #c0c0c0;">除非擦除整个块（例如512KB），不能重新写入</span>一个cell。经过多次擦除，最终数据块将坏掉，为了避免这种损坏，固态硬盘必须能够重新定位数据页并进行垃圾回收——所谓<span style="background-color: #c0c0c0;">磨损均衡（wear leveling）</span>。</p>
<p>由于固态硬盘重新定位、垃圾回收的特性，磁盘使用占比越大， 其效率会越低，对于100GB的文件，其位于160GB、320GB固态硬盘上，写效率是不一样的。</p>
<p><span style="text-decoration: underline;"><strong>闪存技术</strong></span></p>
<p>分为两种类型：</p>
<ol>
<li>single-level cell (SLC)：每个cell存放一个bit，速度快，耐用，但是数据密度低。能够支持10万次循环写入。实际应用中能达到20年寿命</li>
<li>multi-level cell (MLC)：每个cell存放2个甚至3个bit，单位成本低，速度相对慢，不耐用。好的设备能够支持1万次循环写入。有些厂商对技术进行改进，称为企业级MLC（eMLC）</li>
</ol>
<p><span style="text-decoration: underline;"><strong>SSD组建RAID</strong></span></p>
<p>建议使用SATA的SSD组成的RAID以避免单块磁盘的故障导致数据丢失。一些就的RAID控制器不能很好的支持SSD，仍然做诸如buffering、写入重排序之类浪费时间的操作。</p>
<p><span style="text-decoration: underline;"><strong>MySQL针对固态存储的优化</strong></span></p>
<table style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 200px; text-align: center;"> 优化内容</td>
<td style="text-align: center;">说明 </td>
</tr>
</thead>
<tbody>
<tr>
<td>InnoDB的I/O能力配置</td>
<td>
<p>增大innodb_io_capacity，可以<span style="background-color: #c0c0c0;">设置到2000-20000</span>，根据SSD的IOPS（每秒输入输出数）确定</p>
<p>SSD比机械硬盘支持更高的并发访问，所以可以<span style="background-color: #c0c0c0;">修改读写I/O线程数到10-15</span></p>
</td>
</tr>
<tr>
<td>增大InnoDB的日志文件大小</td>
<td>可以设置为4GB或者更大。Percona Server、MySQL 5.6支持<span style="background-color: #c0c0c0;">4GB以上的日志文件</span>，大日志文件可以提高和稳定性能，且对于SSD不会在宕机重启后因大量随机I/O导致的漫长恢复（crash recovery）等待</td>
</tr>
<tr>
<td>把某些文件从Flash移动到RAID</td>
<td>
<p>除了把InnoDB日志改大以外，而可以将其存储位置移动出SSD，存放到具有电池支持的写缓存的RAID中，因为InnoDB日志主要是顺序写入，不会从SSD获益太多</p>
<p>处于类似的原因，可以把二进制日志、ibdata1文件（包含双重写入缓冲、插入缓冲）移动到RAID。</p>
</td>
</tr>
<tr>
<td>禁止预读取（read-ahead）</td>
<td>
<p>有些时候预读取的成本比收益更大，特别是对于SSD。MySQL5.5提供一个参数来设置</p>
</td>
</tr>
<tr>
<td>配置InnoDB的flushing算法</td>
<td>
<p>标准InnoDB没有提供针对SSD的有价值的选项，但是Percona XtraDB（包括PerconaServer、MariaDB）可以：</p>
<p>设置<span style="background-color: #c0c0c0;">innodb_adaptive_checkpoint=keep_average</span>（默认值estimate），设置<span style="background-color: #c0c0c0;">innodb_flush_neighbor_pages=0</span>。</p>
</td>
</tr>
<tr>
<td>潜在的禁用doublewrite buffer</td>
<td>除了考虑把双重缓冲移出SSD，可以考虑禁用它——某些设备声明支持16KB原子写入（一般要求O_DIRECT和XFS文件系统），这导致双重写入缓冲多余。这可能让MySQL的性能提高50%，但是可能不是100%安全。</td>
</tr>
<tr>
<td>限制插入缓冲的大小</td>
<td>
<p>insert buffer（新版本InnoDB称为change buffer）用于减少针对<span style="background-color: #c0c0c0;">不在内存中的非唯一索引页的随机I/O（当对应行被update时）</span>。</p>
<p>在使用普通硬盘的某些情况下，<span style="background-color: #c0c0c0;">工作集比内存大很多时，增大插入缓冲可能减少两个数量级的随机I/O</span>。</p>
<p>但是对于SSD，这没有必要了，因为其随机I/O快了很多，可以将其限制其最大尺寸为较小的值。MySQL 5.6支持此设置。</p>
</td>
</tr>
<tr>
<td>调整InnoDB页大小和checksum</td>
<td>
<p>MySQL5.6允许调整默认的16KB页大小、checksum算法</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">Replica的硬件选择</span></div>
<p>如果出于failover的目的，Replica应当与Master一样高的配置；如果仅仅为了提高整体读取能力，则可以使用廉价的方案。</p>
<div class="blog_h3"><span class="graybg">RAID性能优化</span></div>
<p>RAID可以提供<span style="background-color: #c0c0c0;">冗余、存储容量、缓存和速度的优势</span>。不同的RAID对数据库需求的满足情况如下：</p>
<table style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 50px; text-align: center;">RAID </td>
<td style="width: 200px; text-align: center;">特性</td>
<td style="text-align: center;"> 说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>RAID0</td>
<td>
<p>廉价、高速、危险，不具备冗余</p>
<p>磁盘数：N<br />读写：快读快写</p>
</td>
<td>最廉价、最高性能的RAID方案。不具有冗余特性，适用于不太关心（其数据可以从其它地方很容易的重新获取）的Replica。很简单，易于软件实现。</td>
</tr>
<tr>
<td>RAID1</td>
<td>
<p>高速读、简单、安全</p>
<p>磁盘数：2(通常)<br />读写：快读慢写</p>
</td>
<td>在很多场景下提供更好的读性能，它把数据复制到多块硬盘上，具有很好的冗余性，其读性能比RAID0稍好。适合于日志或者类似的场景，因为顺序I/O不需要底层磁盘性能很高。很简单，易于软件实现。</td>
</tr>
<tr>
<td>RAID5</td>
<td>
<p>安全、速度、成本的折衷 </p>
<p>磁盘数：N+1<br />读写：快读，写依据场景</p>
</td>
<td>
<p>使用分布式奇偶校验块（distributed parity blocks）来把数据分散在多块磁盘上，其中一块磁盘损坏，可以通过奇偶校验重建数据。这是比较经济的RAID，因为只需要浪费阵列中单块磁盘的存储空间。</p>
<p>RAID5的<span style="background-color: #c0c0c0;">随机写成本较高</span>，因为<span style="background-color: #c0c0c0;">每个写操作需要2次读+2次写</span>（包括计算和存储校验位）。如果是<span style="background-color: #c0c0c0;">顺序写，或者阵列中包含很多磁盘，则写性能会有一些提高</span>。RAID5的<span style="background-color: #c0c0c0;">随机读、顺序读性能均较好</span>。</p>
<p>RAID5适合以读为主的工作负载，可以用来存放数据、日志。</p>
<p>RAID5在磁盘出现损坏时，替换新盘会导致严重的性能下降，因为它需要读取所有磁盘来完成数据重建。</p>
</td>
</tr>
<tr>
<td>RAID10</td>
<td>
<p>昂贵、快速、安全</p>
<p>磁盘数：2N<br />读写：快读快写</p>
</td>
<td>由若干镜像磁盘对组成。<span style="background-color: #c0c0c0;">对于读、写性能都有很大的提升</span>。如果一块磁盘坏掉，性能可能下降高达50%。<span style="background-color: #c0c0c0;">成本高</span>。</td>
</tr>
<tr>
<td>RAID50</td>
<td>
<p>适合存放大量数据</p>
<p>磁盘数：2(N+1)<br />读写：快读快写</p>
</td>
<td>相当于RAID5和RAID0的结合。每个RAID5磁盘组需要3+块硬盘。<span style="background-color: #c0c0c0;">读写均较快</span>。适用于海量数据存储（数据仓库、特大OLTP）。</td>
</tr>
</tbody>
</table>
<p><span style="text-decoration: underline;"><strong>RAID故障、恢复和监控</strong></span><br />多块磁盘同时坏掉的可能性往往被低估。实时上，RAID并不能减少对备份的需求。</p>
<p><span style="text-decoration: underline;"><strong>RAID缓存</strong></span></p>
<p>RAID缓存置于RAID控制器之中，作为磁盘和主机之间的数据交换缓冲。RAID控制器可能因为以下原因使用缓存：</p>
<ol>
<li>Caching Reads：控制器从磁盘上读取数据返回给OS后，可以将其缓存起来，这种用法没有太大意义。因为控制器不知道哪些是热点数据，而且缓存容量很小</li>
<li>Caching read-ahead data：如果控制器检测到顺序读，其可能预读取可能马上需要使用的数据。对于InnoDB没有价值，因为InnoDB自己管理预读取</li>
<li>Caching writes：控制器可以把写请求缓存起来，安排在后续步骤中进行写入。这有两个好处：立即像OS返回Success信息；可以重排写入以达到高效目的</li>
<li>Internal operations：某些RAID操作，特别是RAID5写操作非常复杂，控制器需要内部存储来进行计算</li>
</ol>
<p>RAID缓存是稀缺资源，应当合理分配。某些控制器允许你分配缓存使用，通常将更多的缓存分配给写操作能够很好的提高性能。对于RAID1、0、10，可以分配100%给写缓存，对于RAID5，则需要保留一些供内部使用。</p>
<p>某些RAID控制器允许设置写入延迟时间，根据实际工作负载设置。</p>
<p><span style="background-color: #c0c0c0;">缺少battery backup unit (BBU)的写缓存可能造成数据损坏</span>。此外，<span style="background-color: #c0c0c0;">某些磁盘本身具有写缓存功能，这是没有电池保护的</span>，这样的磁盘可能执行虚假的fsync()操作，可能需要禁止这种磁盘缓存。</p>
<div class="blog_h3"><span class="graybg">使用多磁盘分卷</span></div>
<p>MySQL创建多种类型的文件：</p>
<ol>
<li>数据和索引文件</li>
<li>事务日志文件</li>
<li>二进制日志文件</li>
<li>其它日志文件：错误日志、查询日志、缓慢查询日志等</li>
<li>临时文件和表</li>
</ol>
<p>MySQL没有复杂的表空间管理功能，默认情况下，它只是把单个Schema的文件统一存放在一个目录下。</p>
<p>出于性能的考虑来把<span style="background-color: #c0c0c0;">日志和数据分到不同的分卷上是没有必要</span>的，除非你有很多磁盘（20+）或者使用SSD。把数据和二进制日志分到不同卷上的真正价值在于避免因为崩溃（对于没有电池保护的写缓存）导致两种数据均丢失。</p>
<div class="blog_h3"><span class="graybg">网络配置</span></div>
<p>通过skip_name_resolve禁止DNS查找，使用IP地址，防止因为DNS缓慢导致的高延迟。</p>
<p>back_log控制入站TCP连接队列的容量，对于频繁打开、关闭连接的Web应用，默认值50可能太小。将其设置到上千并不会有什么坏处，但是要注意OS的配置，例如somaxconn默认为128、tcp_max_syn_backlog需要增大。</p>
<p>对网络结构、跃点数也需要加以关注，跃点数会增加延迟，网络结构不好可能限制了带宽。对于本地网络，至少应当保证1G带宽，在交换机主干网，应当保证10G带宽。</p>
<div class="blog_h3"><span class="graybg">交换文件（Swapping）</span></div>
<p>物理内存不够时会发生Swapping，这对MySQL性能有严重的影响。</p>
<div class="blog_h2"><span class="graybg">数据复制（Replication）</span></div>
<p>MySQL内置的数据复制机制是构建大型高性能MySQL应用的基础。Replication允许将一台或者多台服务器配置为某个服务器的Replicas，以保持数据同步。这种机制是<span style="background-color: #c0c0c0;">高性能、高可用性、可扩展性（scalability）、灾难恢复、备份、分析、数据仓库</span>等任务的中心。</p>
<div class="blog_h3"><span class="graybg">Replication纵览</span></div>
<p>Replication要解决的基本问题是把数据在多台数据库之间保持同步。多台replica连接到同一台master，replica和master的角色可以相互转换。</p>
<p>MySQL支持两种类型的Replication：</p>
<ol>
<li>基于语句（statement-based）的复制，亦称逻辑复制，从MySQL3.23即开始存在</li>
<li>基于行的复制，MySQL5.1引入</li>
</ol>
<p>这两种复制均是通过<span style="background-color: #c0c0c0;">录制master二进制日志的改变，并在replica中进行replay实现同步</span>，并且都是<span style="background-color: #c0c0c0;">异步的（同步时间没有保证）</span>。</p>
<p><span style="background-color: #c0c0c0;">新版本的MySQL可以作为老版本的Replica</span>，反之则可能存在问题。</p>
<p>Replication对master不会添加额外的压力，只要master开启二进制日志功能即可（开启这个功能对master性能有影响，但是这是数据备份、基于时间点恢复数据的基础）。Replica读取master日志会产生少许的网络I/O压力，特别是读取很老的日志时。</p>
<p><span style="text-decoration: underline;"><strong>Replication解决的问题</strong></span></p>
<p>以下是常见的Replication用法：</p>
<ol>
<li>数据分布（Data distribution）：可以进行异地数据备份，甚至是时断时开的网路状况下</li>
<li>负载均衡（Load balancing）：可以把读请求分布到多台服务器上</li>
<li>备份（Backups）：可以协助数据备份，但是数据复制并不是备份的代替技术</li>
<li>高可用性和故障转移：避免单点故障</li>
</ol>
<p><span style="text-decoration: underline;"><strong>Replication的工作方式</strong></span></p>
<p>大体上说，数据复制包含三个步骤：</p>
<ol>
<li>master录制数据的变化（事件）到二进制日志</li>
<li>replica复制事件到其自己的转播日志（relay log）</li>
<li>replica回放转播日志中的事件，将数据变更应用到自己的数据文件</li>
</ol>
<p>更细致的描述如下图：</p>
<p> <img class="aligncenter  wp-image-3294" src="https://blog.gmem.cc/wp-content/uploads/2012/02/replicas.jpg" alt="replicas" width="604" height="408" /></p>
<p>需要注意<span style="background-color: #c0c0c0;">回放是在replica的单个线程上进行的</span>，而<span style="background-color: #c0c0c0;">变更则可能是在master上并发出现的</span>，这可能导致性能瓶颈。</p>
<div class="blog_h3"><span class="graybg">建立Replication</span></div>
<p>建立步骤根据场景不同有很多变种，对于<span style="background-color: #c0c0c0;">新安装的</span>主从MySQL服务器，步骤大概如下：</p>
<ol>
<li> 在各服务器上创建数据复制账户</li>
<li>配置master、replica</li>
<li>指示replica连接到master并进行数据复制</li>
</ol>
<p><span style="text-decoration: underline;"><strong>创建数据复制账户</strong></span></p>
<p>MySQL具有一些和数据复制相关的特殊权限（privileges），下面是授权示意脚本：</p>
<pre class="crayon-plain-tag">-- 在master、replica上创建一个名为repl的账户，并授权
GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.*
TO repl@'192.168.0.%' IDENTIFIED BY 'p4ssword',; -- 显示用户在192.168.0本地网段</pre>
<p><span style="text-decoration: underline;"><strong>Master和Replica的配置</strong></span></p>
<p>需要开启master的二进制日志，并赋予server_id：</p>
<pre class="crayon-plain-tag">log_bin   = mysql-bin ;日志文件的名称，在MySQL命令行运行SET SQL_LOG_BIN=0可以临时禁止二进制日志
;不能使用默认值1，在某些MySQL版本会导致冲突
server_id = 10
;完成配置后重启，运行命令SHOW MASTER STATUS;检查是否正常：
;File名称可能不同
+------------------+----------+--------------+------------------+
| File             | Position | Binlog_Do_DB | Binlog_Ignore_DB |
+------------------+----------+--------------+------------------+
| mysql-bin.000001 | 98       |              |                  |
+------------------+----------+--------------+------------------+</pre>
<p>在replica上需要类似的配置：</p>
<pre class="crayon-plain-tag">log_bin           = mysql-bin
server_id         = 2
relay_log         = /var/lib/mysql/mysql-relay-bin ;转播日志的名称
log_slave_updates = 1 ;把复制得到的事件存放到replica自己的二进制日志中
read_only         = 1 ;使replica处于只读模式，不允许普通用户创建表、修改数据</pre>
<p><span style="text-decoration: underline;"><strong> 启动复制</strong></span></p>
<pre class="crayon-plain-tag">CHANGE MASTER TO MASTER_HOST='host name or ip',
MASTER_USER='repl',
MASTER_PASSWORD='p4ssword',
MASTER_LOG_FILE='mysql-bin.000001',
MASTER_LOG_POS=0;</pre>
<p>检查确认replica的配置正常：</p>
<pre class="crayon-plain-tag">mysql&gt; SHOW SLAVE STATUS
*************************** 1. row ***************************
       Slave_IO_State:     //表示复制操作没有在进行
          Master_Host: server1
          Master_User: repl
          Master_Port: 3306
        Connect_Retry: 60
      Master_Log_File: mysql-bin.000001
  Read_Master_Log_Pos: 4   //replica知道第一个有意义的事件的位置是4
       Relay_Log_File: mysql-relay-bin.000001
        Relay_Log_Pos: 4
Relay_Master_Log_File: mysql-bin.000001
     Slave_IO_Running: No //表示复制操作没有在进行
    Slave_SQL_Running: No //表示复制操作没有在进行
                     ...omitted...
Seconds_Behind_Master: NULL

mysql&gt; START SLAVE;
mysql&gt; SHOW SLAVE STATUS

*************************** 1. row ***************************
       Slave_IO_State: Waiting for master to send event  //IO线程正在等待master的新事件，这意味着所有事件已经被获取
          Master_Host: server1
          Master_User: repl
          Master_Port: 3306
        Connect_Retry: 60
      Master_Log_File: mysql-bin.000001
  Read_Master_Log_Pos: 164 //日志位置改变，意味着某些日志已经被获取并回放
       Relay_Log_File: mysql-relay-bin.000001
        Relay_Log_Pos: 164
Relay_Master_Log_File: mysql-bin.000001
     Slave_IO_Running: Yes  //注意IO、SQL线程都处于运行状态
    Slave_SQL_Running: Yes
                     ...omitted...
Seconds_Behind_Master: 0

//在master上可以看到有replica创建的线程（连接）
mysql&gt; SHOW PROCESSLIST
*************************** 1. row ***************************
     Id: 55
   User: repl
   Host: replica1.webcluster_1:54813
     db: NULL
Command: Binlog Dump
   Time: 610237
  State: Has sent all binlog to slave; waiting for binlog to be updated
   Info: NULL
//在replica上可以看到一个IO线程、一个SQL线程

mysql&gt; SHOW PROCESSLIST\G
*************************** 1. row ***************************
     Id: 1
   User: system user
   Host:
     db: NULL
Command: Connect
   Time: 611116
  State: Waiting for master to send event
   Info: NULL
*************************** 2. row ***************************
     Id: 2
   User: system user
   Host:
     db: NULL
Command: Connect
   Time: 33   //SQL线程已经空闲33秒，意味着33秒内没有事件回放
  State: Has read all relay log; waiting for the slave I/O thread to update it
   Info: NULL  //会显示其正在执行的语句</pre>
<p><span style="text-decoration: underline;"><strong>从其它服务器上初始化replica</strong></span></p>
<p>更常见的常见是，master已经运行了一段时间后，创建replica进行数据复制，这种情况下replica和master处于不同步状态，需要三个条件才能完成replica的初始化：</p>
<ol>
<li>master某个时间点的快照</li>
<li>master的当前日志文件，以及上述快照时间点在日志文件里的偏移量</li>
<li>master从上述快照时间点到当前的二进制日志文件</li>
</ol>
<p>从其它服务器上克隆一个replica有以下方法：</p>
<ol>
<li>冷拷贝：关闭master，拷贝其文件到replica；启动master，其将会启动一个新二进制日志，使用CHANGE MASTER TO命令从新二进制日志启动replica</li>
<li>热拷贝：如果仅使用MyISAM，可以使用mysqlhotcopy或者rsync在服务启动的情况下拷贝文件</li>
<li>使用mysqldump：如果仅使用InnoDB，可以使用下面的命令把master的所有东西dump出来，全部加在到replica，并把replica的日志坐标移动到master二进制日志对应处：<br />
<pre class="crayon-plain-tag">#-single-transaction导致读取事务开始点的所有数据
mysqldump --single-transaction --all-databases --master-data=1 --host=server1 |mysql --host=server2</pre>
</li>
<li>使用快照或者备份：如果知道对应的二进制日志坐标，可以通过把备份、快照还原到replica，然后使用该坐标运行CHANGE MASTER TO命令。支持的快照例如：LVM snapshots, SAN snapshots, EBS snapshots等。如果使用备份，那么备份时间点之后的二进制日志均需要存在</li>
<li>使用Percona XtraBackup</li>
<li>从其它replica复制数据</li>
</ol>
<p><span style="text-decoration: underline;"><strong>推荐的Replication配置</strong></span></p>
<pre class="crayon-plain-tag">;对于master最重要的配置，每次事务提交时同步二进制日志，防止崩溃导致的数据丢失
sync_binlog=1
;如果使用InnoDB的master，应该
innodb_flush_logs_at_trx_commit=1 ;日志提交刷出日志
innodb_support_xa=1 ;MySQL5.0+
innodb_safe_binlog  ;仅MySQL4.1效果与上一条类似
log_bin=/var/lib/mysql/mysql-bin ;最好明确指定路径和基名(base name)


;对于replica
relay_log=/path/to/logs/relay-bin
skip_slave_start ;阻止replica在崩溃后自动启动
read_only  ;除了具有SUPER权限的线程、replication SQL thread，不能修改库上的非临时表
sync_master_info = 1
sync_relay_log = 1
sync_relay_log_info = 1</pre>
<div class="blog_h3"><span class="graybg">Replication的技术细节</span></div>
<p>MySQL5.0以上支持两种复制模式，<span style="background-color: #c0c0c0;">默认使用基于语句的模式，如果出现无法录制的语句，则临时切换到基于行的模式</span>。</p>
<p><span style="text-decoration: underline;"><strong>基于语句的复制</strong></span></p>
<p>MySQL 5.0-仅支持基于语句的复制，则在RDBMS的世界里很少见。这种方式是通过<span style="background-color: #c0c0c0;">录制并回放master中修改了数据的SQL语句</span>实现的。</p>
<p>优点：占用带宽小，易于理解和配合mysqlbinlog使用</p>
<p>缺点：语句中可能存在上下文相关的函数，例如CURRENT_USER()，这可能导致问题</p>
<p><span style="text-decoration: underline;"><strong>基于行的复制</strong></span></p>
<p>与其它RDBMS的实现类似，录制实实在在的行数据改变。主要的优势是它能<span style="background-color: #c0c0c0;">正确的执行所有语句</span>。缺点则是SQL语句不在日志中，很难弄清到底干了什么。</p>
<p><span style="text-decoration: underline;"><strong>Replication相关的文件</strong></span></p>
<p>除了上述的二进制日志文件，传播日志文件，还有以下文件和数据复制相关</p>
<table style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="text-align: center;">文件名 </td>
<td style="text-align: center;"> 说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>mysql-bin.index</td>
<td>具有和二进制日志一样的前缀，附加.index后缀。跟踪磁盘上的二进制日志文件。</td>
</tr>
<tr>
<td style="width: 150px;">mysql-relay-bin.index</td>
<td>与上一个类似，跟踪磁盘上的转播日志文件</td>
</tr>
<tr>
<td style="width: 150px;">master.info</td>
<td>包含replica连接master需要的信息。包含明码的密码</td>
</tr>
<tr>
<td style="width: 150px;">relay-log.info</td>
<td>包含replica当前二进制日志、转播日志的坐标</td>
</tr>
</tbody>
</table>
<p><span style="text-decoration: underline;"><strong>向其它replica转发Replication Events</strong></span></p>
<p>log_slave_updates选项允许把当前replica作为其它replica的master使用。其工作原理示意图如下：</p>
<p><img class="aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2012/02/replica-2.jpg" alt="" width="90%" /></p>
<p>当log_slave_updates启用时，第一个replica<span style="background-color: #c0c0c0;">接收到master的日志并回放后，会将其记录到自己的二进制日志里</span>，这样，第二个replica就可以收到这个事件，进行类似的回放操作了。需要注意的时，master、第一个replica的日志position并不相同，不要做这种假设。</p>
<p><span style="text-decoration: underline;"><strong>Replication Filters</strong></span></p>
<p>复制过滤器允许仅复制master的部分数据，包含两种类别：</p>
<ol>
<li>在master上过滤二进制日志事件的过滤器：包括binlog_do_db 、binlog_ignore_db，不应当使用</li>
<li>在replica上过滤转播日志事件的过滤器： 若干replicate_*选项过滤SQL线程从转播日志读取的事件。可以忽视/复制一个/多个数据库、重写一个数据库到另外一个、根据LIKE匹配语法忽视/复制表</li>
</ol>
<div class="blog_h3"><span class="graybg">Replication拓扑结构</span></div>
<p>MySQL支持多种复制拓扑结构，基本的规则是：</p>
<ol>
<li>每个replica只能有一个master</li>
<li>每个replica必须具有惟一的server ID</li>
<li>每个master可以具有多个replica</li>
<li>通过设置log_slave_updates，replica可以传播来自master的事件，从而作为其它replica的master</li>
</ol>
<p><span style="text-decoration: underline;"><strong>Master and Multiple Replicas</strong></span></p>
<p>这种拓扑和一主一从的结构没有本质区别，因为replica之间不进行任何通信：</p>
<p><img class="aligncenter  wp-image-3333" src="https://blog.gmem.cc/wp-content/uploads/2012/02/topolog-1.jpg" alt="topolog-1" width="258" height="226" /></p>
<p>在<span style="background-color: #c0c0c0;">有很多读请求、较少写请求</span>时，这种拓扑非常有效。下面列出其常见应用场景：</p>
<ol>
<li>不同replica作为不同的角色使用，例如使用不同的索引、不同的存储引擎</li>
<li>把一个replica作为备用master</li>
<li>把一个replica放到远程数据中心作为灾备</li>
<li>延时（Time-delay）某个replica作为灾备</li>
<li>将某个replica作为备份用，或者培训、开发用</li>
</ol>
<p>该拓扑被广泛使用的一个原因是避免了复杂性。</p>
<p><span style="text-decoration: underline;"><strong>Master-Master in Active-Active Mode</strong></span></p>
<p>又称为dual-master、bidirectional replication。两个服务器各自配置为对方的master和replica：</p>
<p><img class="aligncenter  wp-image-3336" src="https://blog.gmem.cc/wp-content/uploads/2012/02/topolog-2.jpg" alt="topolog-2" width="210" height="106" /></p>
<p>Active-Active模式有其用途，但是仅仅是在特殊的场景下，例如对于地理分布的办公室，每个办公室都需要本地可修改的数据副本的场景。</p>
<p>该模式最大的问题是冲突修改的处理，例如两个数据库同时修改了一行数据，或者在具有AUTO_INCREMENT的表上同时插入数据。MySQL5.0+提供<span style="background-color: #c0c0c0;">auto_increment_increment、auto_increment_offset</span>解决自增长主键冲突问题。</p>
<p><span style="text-decoration: underline;"><strong>Master-Master in Active-Passive Mode</strong></span></p>
<p>该模式是<span style="background-color: #c0c0c0;">非常强大的设计容错（fault-tolerant）和高可用性系统</span>的拓扑。与Active-Active的区别在于，<span style="background-color: #c0c0c0;">其中一个服务器是被动的（passive）、只读的</span>：</p>
<p> <img class="aligncenter  wp-image-3337" src="https://blog.gmem.cc/wp-content/uploads/2012/02/topolog-3.jpg" alt="topolog-3" width="209" height="111" /></p>
<p>该配置允许<span style="background-color: #c0c0c0;">轻松的进行Active、Passive角色的转换</span>，因为两台服务器的配置是相同的，这让故障转移很方便。该模式亦<span style="background-color: #c0c0c0;">支持不停机（downtime）</span>维护、表优化、升级OS、硬件，考虑下面的场景：</p>
<ol>
<li>ALTER TABLE锁定整个表，阻塞读写操作，该操作可能需要很长时间完成，从而打断服务的运行</li>
<li>可以停止Active上的replication threads，这样Active不会从Passive上录制和回放任何事件</li>
<li>然后，在Passive上可以进行ALTER TABLE操作</li>
<li>完成后，把Passive切换为Active，应用程序连接到新的Active</li>
<li>新的Passive读取日志，并回放ALTER TABLE操作</li>
</ol>
<p>使用active-passive master-master拓扑可以解决很多其他问题，已经回避MySQL的限制。</p>
<p>在两台服务器上同时进行以下配置，即可实现该模式：</p>
<ol>
<li>确保两台服务器具有相同的数据</li>
<li>启用二进制日志，设置唯一的Server ID，创建replication账号</li>
<li><span style="background-color: #c0c0c0;">启用replica 更新日志（log_slave_updates）</span>，这对于故障转移、自动恢复（failback）非常重要</li>
<li>可选的，配置Passive为ead-only，防止冲突的修改</li>
<li>分别启动两台服务器的MySQL实例</li>
<li>分别配置为对方的replica，从新创建的二进制日志开始</li>
</ol>
<p>当Active上发生一个数据变化时，会发生以下事件序列：</p>
<ol>
<li>变化作为事件写入Active的二进制日志</li>
<li>Passive读取到该事件，并存入自己的转播日志</li>
<li>Passive执行转播日志，并记录到自己的二进制日志（由于设置了log_slave_updates）</li>
<li>Active读取到该事件，由于发现事件的Server ID与自己相同，它<span style="background-color: #c0c0c0;">忽略这个事件</span></li>
</ol>
<p><span style="text-decoration: underline;"><span style="color: #008080;"><a href="#switching-roles-of-active-passive-topology"><span style="color: #008080; text-decoration: underline;">后续章节</span></a></span></span>包含主从角色切换的详细配置。</p>
<p><span style="text-decoration: underline;"><strong>Master-Master with Replicas</strong></span></p>
<p>可以为每一个master添加1个或者多个replicas：</p>
<p><img class="aligncenter  wp-image-3341" src="https://blog.gmem.cc/wp-content/uploads/2012/02/topolog-4.jpg" alt="topolog-4" width="246" height="197" /></p>
<p>该拓扑的优点是提供额外的冗余，提供更好的读性能。在地理分布的数据复制场景，该拓扑排除了单点故障。</p>
<p><strong><span style="text-decoration: underline;">Ring Replication</span></strong></p>
<p>其实上面几种具有连个master的拓扑，只是环形复制的特例。环形复制依赖于环上的每一个点，因此大大增加了整个系统失败的可能性：</p>
<p><img class="aligncenter  wp-image-3342" src="https://blog.gmem.cc/wp-content/uploads/2012/02/topolog-5.jpg" alt="topolog-5" width="294" height="224" /></p>
<p>通常没有必要使用该拓扑</p>
<p><strong><span style="text-decoration: underline;">Master, Distribution Master, and Replicas</span></strong></p>
<p>每个Replica都要在master上<span style="background-color: #c0c0c0;">创建一个线程来使用binlog dump命令读取二进制日志并发送给replica</span>，Replica很多时可能给master带来不可忽视的负载：</p>
<ol>
<li>每个Replica的线程独立运行互不影响，这会导致很多重复的工作</li>
<li>如果Replica很多，且存在一个很大的二进制日志事件（例如LOAD DATA INFILE），master可能内存溢出并崩溃</li>
<li>如果每个Replica在读取二进制日志的不同部分，可能导致很多的磁盘操作，影响master的性能</li>
</ol>
<p>为了把负载从master上移除，可以使用分布式master（distribution master）——它的唯一作用是读取master的日志并服务于replica：</p>
<p><img class="aligncenter  wp-image-3343" src="https://blog.gmem.cc/wp-content/uploads/2012/02/topolog-6.jpg" alt="topolog-6" width="278" height="323" /></p>
<p>为了溢出实际执行查询的资源消耗，可以把分布式master的存储引擎改为Blackhole。</p>
<p>如何确定需要一个分布式master来减轻master的负载？一个大概的规则是，如果master已经全速运行了，可能不能再为它添加replica；另外如果master只有很少的写操作，则通常不需要分布式master。对于网络带宽敏感的场景，使用slave_compressed_protocol可以减轻master的网络压力。</p>
<p>分布式replica可以作为复制过滤器的集中执行点。</p>
<p>Blackhole引擎的表根本没有数据，因此其速度很快，但是Blackhole引擎具有一些BUG。</p>
<p><span style="text-decoration: underline;"><strong>定制化的数据复制解决方案</strong></span></p>
<p><strong>选择性复制</strong></p>
<p>复制master的一部分到某个replica上，某些情况下这与水平分区（horizontal data partitioning）概念上有相似，不同的是，在这里，master上具有全部的数据，写查询永远在master进行即可，读查询则根据需要可以在replica或者master上进行。</p>
<p>考虑需要把公司不同部门的数据分发在不同的replica上，可以在master上为每个部门创建一个数据库：sales, marketing, procurement…，每个replica则配置replicate_wild_do_table选项，仅录制和回放其感兴趣的库的数据，例如对于销售部门：</p>
<pre class="crayon-plain-tag">replicate_wild_do_table = sales.%</pre>
<p><strong>分离功能</strong></p>
<p>很多应用具有混合OLTP/OLAP的特征，这两类业务是截然不同的，前者多是小的事务性操作，后者则是大的缓慢的读查询且没有数据实时性的要求，他们需要不同的MySQL配置、索引、存储引擎甚至硬件。</p>
<p>一个通用的做法是把OLTP数据库的数据复制到一个OLAP的数据库</p>
<p><strong>数据归档</strong></p>
<p>可以在replica上备份数据，而从master上永久移除之。</p>
<p><strong>创建一个日志服务器（log server）</strong></p>
<p>日志服务器没有数据，它的唯一目的就是让回放、过滤二进制事件变得容易，它有利于在<span style="background-color: #c0c0c0;">崩溃后重启应用、指定时间点（point-in-time）的数据恢复</span>。</p>
<p>假设你有若干日志文件（somelog-bin.000001, somelog-bin.000002……）需要分析，只需要建立一个没有数据的数据库，然后设置：</p>
<pre class="crayon-plain-tag">log_bin       = /var/log/mysql/somelog-bin
log_bin_index = /var/log/mysql/somelog-bin.index ;注意把所有日志文件，每个一行的存入该文件，脚本：/bin/ls −1 /var/log/mysql/somelog-bin.[0-9]* &gt; /var/log/mysql/somelog-bin.index</pre>
<p> 让这个服务器把那些日志当作是他自己的，然后启动日志服务器，使用SHOW MASTER LOGS来验证它已经识别了那些日志。</p>
<p>日志服务器不需要执行日志，它只会让其它服务器来读取它的日志。</p>
<p>对于数据恢复来说，日志服务器比mysqlbinlog更有优势，因为：</p>
<ol>
<li>数据复制其实就是应用二进制日志的过程，这种方式已经经过无数的生产环境的验证，相当稳定。而mysqlbinlog的工作方式有所不同，可能不能完整的重现二进制日志中的变化</li>
<li>日志服务器更快，因为它避免了从日志抽取SQL并到mysql中执行的过程</li>
<li>可以很容易的看到处理进度</li>
<li>容错性好，可以跳过复制失败的语句</li>
<li>很容易的过滤复制事件</li>
<li>如果日志是基于行的格式，mysqlbinlog可能无法读取二进制日志</li>
</ol>
<div class="blog_h3"><span class="graybg">Replication的管理和维护</span></div>
<p><span style="text-decoration: underline;"><strong>监控Replication的状态</strong></span></p>
<pre class="crayon-plain-tag">mysql&gt; SHOW MASTER STATUS; #显示master当前日志的路径和配置
mysql&gt; SHOW MASTER LOGS;   #显示存在于磁盘上的二进制日志
+------------------+-----------+
| Log_name         | File_size |
+------------------+-----------+
| mysql-bin.000220 | 425605    |
| mysql-bin.000221 | 1134128   |
| mysql-bin.000222 | 13653     |
| mysql-bin.000223 | 13634     |
+------------------+-----------+
#显示日志中的事件
mysql&gt; SHOW BINLOG EVENTS IN 'mysql-bin.000223' FROM 13634\G
*************************** 1. row ***************************
Log_name: mysql-bin.000223
Pos: 13634
Event_type: Query
Server_id: 1
End_log_pos: 13723
Info: use `test`; CREATE TABLE test.t(a int)</pre>
<p><span style="text-decoration: underline;"><strong>度量Replication延迟</strong></span></p>
<p>SHOW SLAVE STATUS中的Seconds_behind_master理论上能够显示replica相对于master的延迟，但是往往不可靠。最好的方法是使用<span style="background-color: #c0c0c0;">心跳记录（heartbeat record）</span>，即每秒在master上更新一次的记录。</p>
<p><span style="text-decoration: underline;"><strong>确定replica是否与master处于同步状态</strong></span></p>
<p>验证replica的同步性应当是日常工作的一部分，特别是replica作为备份使用的时候。Percona Toolkit包含一个叫<span style="background-color: #c0c0c0;">pt-table-checksum</span>的工具可以用于根据校验和来检验同步性。</p>
<p><span style="text-decoration: underline;"><strong>replica的重新同步</strong></span></p>
<p>传统做法是，停止replica，重新从master克隆数据，缺点是不方便，特别是数据量大的时候。Percona Toolkit包含一个叫<span style="background-color: #c0c0c0;">pt-table-sync</span>的工具可以提供帮助。</p>
<p><span style="text-decoration: underline;"><strong>修改master</strong></span></p>
<p>由于某些原因，例如服务器升级、master出现故障，可能需要把replica提升为master，并通知给所有replica。</p>
<p><strong>Planned Promotions</strong></p>
<p>整体步骤如下：</p>
<ol>
<li>停止旧的master的写操作</li>
<li>等待replicas完成复制</li>
<li>把一个replica配置为master</li>
<li>把replica、写请求指向新的master，然后启用新master的写操作</li>
</ol>
<p>更深入的讲，以下操作可能需要：</p>
<ol>
<li>停止向发送当前master写请求，用可能需要强制客户端退出</li>
<li>使用FLUSH TABLES WITH READ LOCK停止master的所有写活动。或者通过read_only把master设为只读模式。注意，设置read_only不能阻止当前事务的提交，最好是kill所有活动的事务</li>
<li>选择一个replica作为新master，要确保它已经完全与旧master同步（已经执行完所有从旧master提取的转播日志）</li>
<li>可选的，验证新master与旧master的数据完全一样</li>
<li>在新master上执行STOP SLAVE、CHANGE MASTER TO MASTER_HOST=''、RESET SLAVE，这可以让新master从旧master断开连接，并且丢弃master.info中的信息</li>
<li>按照“推荐的Replication配置”来设置新master</li>
<li>执行SHOW MASTER STATUS来获取新master的二进制日志坐标</li>
<li>确保其它replica也与旧master同步</li>
<li>关闭旧master</li>
<li>在MySQL5.1+，如果需要的话激活新master上的事件</li>
<li>让客户端连接到新master</li>
<li>在各replica上执行CHANGE MASTER TO，指向新master，使用第6步获得的二进制日志坐标</li>
</ol>
<p><strong>Unplanned promotions</strong></p>
<p>如果master崩溃了，你不得不提升一个replica来替换它，这种场景相对较为复杂。可能存在数据丢失，因为master上的某些事件可能没有被任何replica复制，甚至master上执行了一个回滚，而replica尚未执行，如果以后能获取崩溃master的数据，可能可以手工恢复。</p>
<p>下面是提升步骤：</p>
<ol>
<li>确认哪个replica具有<span style="background-color: #c0c0c0;">最新的数据，它将作为新的master</span>。在每个replica上执行SHOW SLAVE STATUS，选择Master_Log_File/Read_Master_Log_Pos最新的一个</li>
<li>让所有replicas完成转播日志的执行</li>
<li>在新master上执行STOP SLAVE、CHANGE MASTER TO MASTER_HOST=''、RESET SLAVE，这可以让新master从旧master断开连接，并且丢弃master.info中的信息</li>
<li>执行SHOW MASTER STATUS来获取新master的二进制日志坐标</li>
<li>比较每个replica的Master_Log_File/Read_Master_Log_Pos与新master的差异，以计算这些replica相对于新master的二进制日志坐标。这里<span style="background-color: #c0c0c0;">假设log_bin 、log_slave_updates在所有replica上开启</span>，以确保能把所有replica恢复到一致的状态</li>
<li>执行上一节的10-12步</li>
</ol>
<p><strong>如何定位日志坐标</strong></p>
<p>如果某个replica的日志坐标与新master不同，则必须计算出该replica<span style="background-color: #c0c0c0;">相对于新master二进制日志的当前坐标</span>。并且将此坐标在命令CHANGE MASTER TO中使用。<span style="background-color: #c0c0c0;">除了使用SHOW SLAVE STATUS命令，亦可通过mysqlbinlog</span>来获取replica最新的最后一条语句，并找到新master二进制日志里同样的语句的位置。</p>
<p>考虑一个具体的例子，如下图所示：</p>
<p><img class="aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2012/02/replica-3.jpg" alt="" width="439" height="453" /></p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/high-performance-mysql-study-note">High Performance MySQL学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/high-performance-mysql-study-note/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
