<?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; BigData</title>
	<atom:link href="https://blog.gmem.cc/category/work/bigdata/feed" rel="self" type="application/rss+xml" />
	<link>https://blog.gmem.cc</link>
	<description></description>
	<lastBuildDate>Mon, 06 Apr 2026 02:15:06 +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>ElasticSearch学习笔记</title>
		<link>https://blog.gmem.cc/elasticsearch-study-note</link>
		<comments>https://blog.gmem.cc/elasticsearch-study-note#comments</comments>
		<pubDate>Tue, 09 Jan 2018 16:14:56 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[BigData]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=17540</guid>
		<description><![CDATA[<p>简介 Elasticsearch是一个基于Apache Lucene的全文检索和分析引擎，可以扩容到上百台服务器，处理PB级结构化/非结构化数据。 ES的应用场景举例： 支持在线搜索、自动完成（搜索建议）功能 作为ELK栈的一部分，收集、聚合、分析日志/事务数据 海量数据的即席分析 Elasticsearch 尽可能地屏蔽了分布式系统的复杂性，它在后台自动执行的操作包括： 分配文档到不同的容器或分片中，文档可以储存在一个或多个节点中 按集群节点来均衡分配这些分片，从而对索引和搜索过程进行负载均衡 复制每个分片以支持数据冗余，从而防止硬件故障导致的数据丢失 将集群中任一节点的请求路由到存有相关数据的节点 集群扩容时无缝整合新节点，重新分配分片以便从离群节点恢复 ES是一个准实时（Near Realtime）的搜索平台，从你开始索引一个文档，到该文档可以被搜索，有个较小的延迟，通常秒级。 安装 Ubuntu 唯一的依赖是JDK，请预先安装好JDK8（建议1.8.0_131+），然后： [crayon-69d36b6053f08927394852/] 所有节点、客户端都应该使用一样的JDK版本。 Docker <a class="read-more" href="https://blog.gmem.cc/elasticsearch-study-note">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/elasticsearch-study-note">ElasticSearch学习笔记</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>Elasticsearch是一个基于Apache Lucene的全文检索和分析引擎，可以扩容到上百台服务器，处理PB级结构化/非结构化数据。</p>
<p>ES的应用场景举例：</p>
<ol>
<li>支持在线搜索、自动完成（搜索建议）功能</li>
<li>作为ELK栈的一部分，收集、聚合、分析日志/事务数据</li>
<li>海量数据的即席分析</li>
</ol>
<p>Elasticsearch 尽可能地屏蔽了分布式系统的复杂性，它在后台自动执行的操作包括：</p>
<ol>
<li>分配文档到不同的容器或分片中，文档可以储存在一个或多个节点中</li>
<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>集群扩容时<span style="background-color: #c0c0c0;">无缝整合新节点</span>，<span style="background-color: #c0c0c0;">重新分配分片</span>以便从离群节点恢复</li>
</ol>
<p>ES是一个<span style="background-color: #c0c0c0;">准实时（Near Realtime）的搜索平台</span>，从你开始索引一个文档，到该文档可以被搜索，有个较小的延迟，通常秒级。</p>
<div class="blog_h1"><span class="graybg">安装</span></div>
<div class="blog_h2"><span class="graybg">Ubuntu</span></div>
<p>唯一的依赖是JDK，请预先安装好JDK8（建议1.8.0_131+），然后：</p>
<pre class="crayon-plain-tag">wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-6.2.3.tar.gz
tar xzf elasticsearch-6.2.3.tar.gz
mv elasticsearch-6.2.3 6.2.3

# 启动ES，-d以守护进程方式运行，默认端口9200
./elasticsearch -d
# 指定集群和节点名称
./elasticsearch -Ecluster.name=es.gmem.cc -Enode.name=es-10.gmem.cc

# 停止ES
kill -SIGTERM $ES_PID</pre>
<p>所有节点、客户端都应该使用一样的JDK版本。</p>
<div class="blog_h2"><span class="graybg">Docker</span></div>
<p>ES的Docker镜像基于CentOS:7。相关镜像的列表参考：<a href="https://www.docker.elastic.co/">https://www.docker.elastic.co/</a>。</p>
<p>镜像分为三种风格：basic包含基本的X-Pack特性，自动激活免费License；platinum包含全部X-Pack特性，默认30天试用；oss不支持X-Pack，仅仅包含ES。</p>
<p>执行下面的命令拉取镜像：</p>
<pre class="crayon-plain-tag">docker pull docker.elastic.co/elasticsearch/elasticsearch:6.2.3
docker pull docker.elastic.co/elasticsearch/elasticsearch-platinum:6.2.3
docker pull docker.elastic.co/elasticsearch/elasticsearch-oss:6.2.3</pre>
<div class="blog_h3"><span class="graybg">开发环境</span></div>
<p>参考如下命令部署容器：</p>
<pre class="crayon-plain-tag">docker run -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" docker.elastic.co/elasticsearch/elasticsearch:6.2.3</pre>
<div class="blog_h3"><span class="graybg">生产环境 </span></div>
<p>内核参数vm.max_map_count至少要增大到262144：<pre class="crayon-plain-tag">sysctl -w vm.max_map_count=262144</pre>  </p>
<div class="blog_h2"><span class="graybg">Kubernetes</span></div>
<p>参考<a href="https://github.com/pires/kubernetes-elasticsearch-cluster">kubernetes-elasticsearch-cluster</a>搭建集群。注意点：</p>
<ol>
<li>ES的Pod需要超级用户权限运行init容器，避免设置某些VM选项。因此你需要以--allow-privileged选项运行kubelet</li>
<li>ES_JAVA_OPTS的默认值为-Xms256m -Xmx256m，非常小。你可以按需调整</li>
<li>数据节点Pod默认在一个emptyDir中存储数据，请根据实际情况修改</li>
<li>PROCESSORS的默认值为1，如果需要调整，请设置resources.limits.cpu、livenessProbe</li>
<li>支持1.9.3+版本的K8S</li>
</ol>
<p>从下面的仓库下载K8S资源定义文件：</p>
<pre class="crayon-plain-tag">git clone https://github.com/pires/kubernetes-elasticsearch-cluster.git es
cd es</pre>
<div class="blog_h3"><span class="graybg">准备镜像</span></div>
<pre class="crayon-plain-tag">docker pull quay.io/pires/docker-elasticsearch-kubernetes:6.2.2_1
docker tag quay.io/pires/docker-elasticsearch-kubernetes:6.2.2_1 docker.gmem.cc/elasticsearch-kubernetes:6.2.2
docker push docker.gmem.cc/elasticsearch-kubernetes:6.2.2

docker pull busybox:1.27.2
docker tag busybox:1.27.2 docker.gmem.cc/busybox:1.27.2
docker push docker.gmem.cc/busybox:1.27.2</pre>
<div class="blog_h3"><span class="graybg">部署</span></div>
<pre class="crayon-plain-tag"># Master节点服务
kubectl create -f es-discovery-svc.yaml
# Data节点服务
kubectl create -f es-svc.yaml
# Master节点的Deployment，默认3个Replica，init容器调用sysctl
kubectl create -f es-master.yaml
# 等待所有Master节点就绪
kubectl -n dev rollout status -f es-master.yaml
# Client节点的Deployment，默认2个Replica，init容器调用sysctl
kubectl create -f es-client.yaml
# 等待所有Client节点就绪
kubectl rollout status -f es-client.yaml

# 数据节点，使用本地目录
kubectl create -f es-data.yaml
kubectl rollout status -f es-data.yaml

# 基于SS的数据节点（推荐）
kubectl create -f stateful/es-data-svc.yaml
kubectl create -f stateful/es-data-stateful.yaml</pre>
<p>资源规格定义如下：</p>
<pre class="crayon-plain-tag">apiVersion: v1
kind: Service
metadata:
  name: elasticsearch-discovery
  namespace: dev
  labels:
    component: elasticsearch
    role: master
spec:
  selector:
    component: elasticsearch
    role: master
  ports:
  - name: transport
    port: 9300
    protocol: TCP</pre><br />
<pre class="crayon-plain-tag">apiVersion: v1
kind: Service
metadata:
  name: elasticsearch
  namespace: dev
  labels:
    component: elasticsearch
    role: client
spec:
  selector:
    component: elasticsearch
    role: client
  ports:
  - name: http
    port: 9200</pre><br />
<pre class="crayon-plain-tag">apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: es-master
  namespace: dev
  labels:
    component: elasticsearch
    role: master
spec:
  replicas: 3
  template:
    metadata:
      labels:
        component: elasticsearch
        role: master
    spec:
      initContainers:
      - name: init-sysctl
        image: docker.gmem.cc/busybox:1.27.2
        command:
        - sysctl
        - -w
        - vm.max_map_count=262144
        securityContext:
          privileged: true
      containers:
      - name: es-master
        image: docker.gmem.cc/elasticsearch-kubernetes:6.2.2
        env:
        - name: NAMESPACE
          valueFrom:
            fieldRef:
              fieldPath: metadata.namespace
        - name: NODE_NAME
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        - name: CLUSTER_NAME
          value: gmemes
        - name: NUMBER_OF_MASTERS
          value: "2"
        - name: NODE_MASTER
          value: "true"
        - name: NODE_INGEST
          value: "false"
        - name: NODE_DATA
          value: "false"
        - name: HTTP_ENABLE
          value: "false"
        - name: ES_JAVA_OPTS
          # ES要求堆最大最小值一样
          value: -Xms256m -Xmx256m
        - name: PROCESSORS
          valueFrom:
            resourceFieldRef:
              resource: limits.cpu
        resources:
          limits:
            cpu: 1
        ports:
        - containerPort: 9300
          name: transport
        livenessProbe:
          tcpSocket:
            port: transport
        volumeMounts:
        - name: storage
          mountPath: /data
      volumes:
          # Pod第一次调度到节点上创建一个空白目录，除非重新调度到其它节点，不会重新创建
          - emptyDir:
              medium: ""
            name: "storage"</pre><br />
<pre class="crayon-plain-tag">apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: es-client
  namespace: dev
  labels:
    component: elasticsearch
    role: client
spec:
  replicas: 2
  template:
    metadata:
      labels:
        component: elasticsearch
        role: client
    spec:
      initContainers:
      - name: init-sysctl
        image: docker.gmem.cc/busybox:1.27.2
        command:
        - sysctl
        - -w
        - vm.max_map_count=262144
        securityContext:
          privileged: true
      containers:
      - name: es-client
        image: docker.gmem.cc/elasticsearch-kubernetes:6.2.2
        env:
        - name: NAMESPACE
          valueFrom:
            fieldRef:
              fieldPath: metadata.namespace
        - name: NODE_NAME
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        - name: CLUSTER_NAME
          value: gmemes
        - name: NODE_MASTER
          value: "false"
        - name: NODE_DATA
          value: "false"
        - name: HTTP_ENABLE
          value: "true"
        - name: ES_JAVA_OPTS
          value: -Xms256m -Xmx256m
        - name: NETWORK_HOST
          value: _site_,_lo_
        - name: PROCESSORS
          valueFrom:
            resourceFieldRef:
              resource: limits.cpu
        resources:
          limits:
            cpu: 1
        ports:
        - containerPort: 9200
          name: http
        - containerPort: 9300
          name: transport
        livenessProbe:
          tcpSocket:
            port: transport
        readinessProbe:
          httpGet:
            path: /_cluster/health
            port: http
          initialDelaySeconds: 20
          timeoutSeconds: 5
        volumeMounts:
        - name: storage
          mountPath: /data
      volumes:
          - emptyDir:
              medium: ""
            name: storage</pre><br />
<pre class="crayon-plain-tag">apiVersion: v1
kind: Service
metadata:
  name: elasticsearch-data
  namespace: dev
  labels:
    component: elasticsearch
    role: data
spec:
  ports:
  - port: 9300
    name: transport
  clusterIP: None
  selector:
    component: elasticsearch
    role: data</pre><br />
<pre class="crayon-plain-tag">apiVersion: apps/v1beta1
kind: StatefulSet
metadata:
  name: es-data
  namespace: dev
  labels:
    component: elasticsearch
    role: data
spec:
  serviceName: elasticsearch-data
  replicas: 5
  template:
    metadata:
      labels:
        component: elasticsearch
        role: data
    spec:
      affinity:
        podAntiAffinity:
          # 不得存在role=data的其它pod
          preferredDuringSchedulingIgnoredDuringExecution:
          - weight: 100
            podAffinityTerm:
              labelSelector:
                matchExpressions:
                - key: role
                  operator: In
                  values:
                  - data
              # 当前节点上
              topologyKey: kubernetes.io/hostname
      initContainers:
      - name: init-sysctl
        image: docker.gmem.cc/busybox:1.27.2
        command:
        - sysctl
        - -w
        - vm.max_map_count=262144
        securityContext:
          privileged: true
      containers:
      - name: es-data
        image: docker.gmem.cc/elasticsearch-kubernetes:6.2.2
        env:
        - name: NAMESPACE
          valueFrom:
            fieldRef:
              fieldPath: metadata.namespace
        - name: NODE_NAME
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        - name: CLUSTER_NAME
          value: gmemes
        - name: NODE_MASTER
          value: "false"
        - name: NODE_INGEST
          value: "false"
        - name: HTTP_ENABLE
          value: "false"
        - name: ES_JAVA_OPTS
          value: -Xms256m -Xmx256m
        - name: PROCESSORS
          valueFrom:
            resourceFieldRef:
              resource: limits.cpu
        resources:
          limits:
            cpu: 1
        ports:
        - containerPort: 9300
          name: transport
        livenessProbe:
          tcpSocket:
            port: transport
          initialDelaySeconds: 20
          periodSeconds: 10
        volumeMounts:
        - name: es-data-pvc
          mountPath: /data
  volumeClaimTemplates:
  - metadata:
      name: es-data-pvc
      namespace: dev
    spec:
      storageClassName: rook-block
      accessModes: [ ReadWriteOnce ]
      resources:
        requests:
          storage: 4Gi </pre>
<div class="blog_h2"><span class="graybg">Helm</span></div>
<pre class="crayon-plain-tag">helm repo add gmem https://chartmuseum.gmem.cc
helm install --name=es --namespace=kube-system gmem/elasticsearch 

# 检查集群健康状态
curl http://es-elasticsearch.kube-system.svc.k8s.gmem.cc:9200/_cat/health?v</pre>
<div class="blog_h1"><span class="graybg">基本概念</span></div>
<div class="blog_h2"><span class="graybg">集群</span></div>
<p>一系列节点的集合，它们在整体上持有完整的数据集，提供联合的索引（Federated indexing），并在整体上对外提供搜索功能。</p>
<p>每个集群以名字来识别，默认名字elasticsearch，每个节点只能属于单个集群。如果部署多套集群，注意确保集群名字不重复。</p>
<div class="blog_h2"><span class="graybg">节点</span></div>
<p>节点是集群中的单个服务器。节点的唯一标识也是名称，默认是节点启动时随机生成的UUID。</p>
<div class="blog_h2"><span class="graybg">主节点</span></div>
<p>当一个节点被<span style="background-color: #c0c0c0;">选举成为主节点</span>时， 它将负责<span style="background-color: #c0c0c0;">管理集群范围内的所有变更</span>，例如增加、删除索引，或者增加、删除节点等。 而主节点并不需要涉及到文档级别的变更和搜索等操作，所以当集群只拥有一个主节点的情况下，即使流量的增加它也不会成为瓶颈。 任何节点都可以成为主节点。</p>
<p>用户可以将请求发送到 集群中的任何节点 ，包括主节点<span style="background-color: #c0c0c0;">。每个节点都知道任意文档所处的位置</span>，并且能够将请求直接转发到存储所需文档的节点。</p>
<div class="blog_h2"><span class="graybg">索引</span></div>
<p>索引是一系列具有相近特性的文档的集合。例如客户数据可以对应一个索引，商品目录可以对应另一个索引。ES中的索引类似于RDBMS中的表。索引的唯一标识是名称，名称必须全小写。</p>
<p>当<span style="background-color: #c0c0c0;">索引作为动词</span>使用时，表示将一个文档存储到索引（名词）中，是其支持全文检索的过程。</p>
<div class="blog_h2"><span class="graybg">映射</span></div>
<p>Mapping用于描述数据的每个字段如何存储。ES自动<span style="background-color: #c0c0c0;">生成一个_all字段，其类型为字符串，属于全文字段</span>。ES会根据文档内容进行猜测，<span style="background-color: #c0c0c0;">动态产生一个映射</span>。</p>
<div class="blog_h3"><span class="graybg">简单类型</span></div>
<p>Elasticsearch 支持 如下简单域类型：</p>
<ol>
<li>字符串：string</li>
<li>整数：byte, short, integer, long</li>
<li>浮点数：float, double</li>
<li>布尔型：boolean</li>
<li>日期：date</li>
</ol>
<p>当你索引一个<span style="background-color: #c0c0c0;">包含新字段</span>的文档时，ES自动进行动态映射。JSON类型到上述类型的转换比较直白，除了要注意2018-04-03这样的字符串会被<span style="background-color: #c0c0c0;">自动解析为date</span>类型。</p>
<div class="blog_h3"><span class="graybg">复杂类型</span></div>
<p>除了上述的简单标量类型外，JSON中的null、数组、对象，都是被ES支持的。</p>
<p>空字段不会被索引，包括：null、null、[ null ]</p>
<p>Lucene不理解嵌套对象，Lucene文档由一组键值对的列表构成。为了支持复杂类型的处理，ES必须对文档进行扁平化。</p>
<p>多值域以数组形式表示，数组的元素类型必须相同。尽管提取文档时，数组元素顺序不会丢失，但是索引是以<span style="background-color: #c0c0c0;">无序</span>的多值域形式进行的：</p>
<pre class="crayon-plain-tag">{
    "followers": [
        { "age": 35, "name": "Mary White"},
        { "age": 26, "name": "Alex Jones"},
        { "age": 19, "name": "Lisa Smith"}
    ]
}
# 被扁平化为
{
    # 扁平化后，age和name之间的关系丢失
    # 如果要查询是否有名为alex的26岁的follower，需要使用嵌套对象
    "followers.age":    [19, 26, 35],
    "followers.name":   [alex, jones, lisa, smith, mary, white]
}</pre>
<p>类似的，为了让ES有效的索引嵌套对象，同样需要扁平化：</p>
<pre class="crayon-plain-tag">{
    "tweet":            [elasticsearch, flexible, very],
    "user.id":          [@johnsmith],
    "user.gender":      [male]
}?</pre>
<div class="blog_h2"><span class="graybg">分析</span></div>
<p>Analysis是处理全文字段，使其可以被搜索的过程。分析包含下面的步骤：</p>
<ol>
<li>首先，将一块文本分成适合于倒排索引的独立的词条</li>
<li>之后，将这些词条统一化为标准格式以提高它们的“可搜索性”</li>
</ol>
<div class="blog_h3"><span class="graybg">分析器</span></div>
<p>分析器负责执行上面的工作。 分析器实际上是将三个功能封装到了一个包里：</p>
<ol>
<li>字符过滤器：首先，字符串按顺序通过每个<span style="background-color: #c0c0c0;">字符过滤器</span> 。他们的任务是在分词前整理字符串。一个字符过滤器可以用来去掉HTML，或者将 &amp; 转化成and</li>
<li>分词器：其次，字符串被<span style="background-color: #c0c0c0;">分词器分为单个的词条</span>。一个简单的分词器遇到空格和标点的时候，可能会将文本拆分成词条</li>
<li>Token过滤器：最后，词条按顺序通过每个<span style="background-color: #c0c0c0;">Token过滤器</span> 。这个过程可能会：
<ol>
<li>改变词条，例如小写化?</li>
<li>删除词条，例如像 a， and， the这样的无用词</li>
<li>增加词条，例如像像 jump 和 leap 这种同义词</li>
</ol>
</li>
</ol>
<p>ES提供了开箱即用的字符过滤器、分词器、Token过滤器，它们可以自由的组合成分析器，满足不同应用场景。</p>
<div class="blog_h3"><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>标准分析器</td>
<td>默认的分析器，根据Unicode联盟定义的单词边界来话分文本，删除绝大部分标点符号，最后小写化</td>
</tr>
<tr>
<td>简单分析器</td>
<td>在任何非字母的地方拆分词条</td>
</tr>
<tr>
<td>空格分析器</td>
<td>以空格为界拆分词条</td>
</tr>
<tr>
<td>语言分析器</td>
<td>
<p>针对特定语言，例如：</p>
<ol>
<li>英语分析器附带了一组英语无用词，并且理解英语语法规则，能够提取词干</li>
</ol>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">分析器何时生效</span></div>
<ol>
<li>当索引一个文档时，它的全文域被分析成词条以用来创建倒排索引</li>
<li>当在全文字段上执行搜索时，查询字符串也需要类似的分析过程</li>
</ol>
<p>当查询一个精确值字段时，分析器不会介入。</p>
<div class="blog_h3"><span class="graybg">指定分析器</span></div>
<p>当ES在你的文档中检测到一个新的字符串域 ，它会<span style="background-color: #c0c0c0;">自动设置其为一个全文字符串域</span>，使用<span style="background-color: #c0c0c0;">标准分析器</span>对它进行分析。某些情况下你可能需要改变此默认行为：</p>
<ol>
<li>你想使用一个不同的分析器，适用于你的数据使用的语言</li>
<li>你想要一个字符串域就是一个精确值，不需要分析</li>
</ol>
<p>可以通过<a href="#api_mapping">自定义映射</a>来满足上述需求。</p>
<div class="blog_h2"><span class="graybg">字段类型</span></div>
<p>总体来说，ES中的字段可以<span style="background-color: #c0c0c0;">分为精确值、全文两个大类</span>。</p>
<p>精确值包括日期、数字，字符串也可以表示精确值。精确值是大小写敏感的。精确值很容易查询，它要么匹配，要么不匹配查询条件。</p>
<p>查询全文字段则要复杂的多，通常不会对全文字段进行精确匹配查询。全文字段匹配查询条件时有个相关度的概念，表现为分数（Scoure）。搜索引擎<span style="background-color: #c0c0c0;">应该能够识别缩写、词根、同义词</span>，并给出适当的相关度。</p>
<p>对全文检索的支持<span style="background-color: #c0c0c0;">依赖于分析，在分析之后需要创建倒排索引</span>。</p>
<div class="blog_h2"><span class="graybg">倒排索引</span></div>
<p>ES使用一种称为倒排索引的结构来支持快速的全文检索。</p>
<p><span style="background-color: #c0c0c0;">倒排索引</span>的结构类似于RDBMS的位图索引，对于索引中出现的<span style="background-color: #c0c0c0;">任何不重复的词的标准模式，生成包含该此的文档列表</span>。标准模式提取了词干、同义词。</p>
<div class="blog_h2"><span class="graybg">相关性</span></div>
<p>默认情况下，查询返回结果是<span style="background-color: #c0c0c0;">按相关性倒序排列</span>的。每个文档都有相关性评分，用一个正浮点数字段 _score 来表示 。 _score 的评分越高，相关性越高。</p>
<p>查询语句会为每个文档生成一个 _score 字段。评分的计算方式取决于查询类型。不同的查询语句用于不同的目的：<span style="background-color: #c0c0c0;">fuzzy 查询会计算与关键词的拼写相似程度</span>。terms 查询会计算<span style="background-color: #c0c0c0;">找到的内容与关键词组成部分匹配的百分比</span>。</p>
<p>通常我们说的 relevance 是我们用来<span style="background-color: #c0c0c0;">计算全文本字段的值相对于全文本检索词相似程度的算法</span>。ES的相似度算法被定义为检索词频率/反向文档频率， 包括以下内容：</p>
<ol>
<li>检索词频率：检索词在该字段出现的频率？出现频率越高，相关性也越高</li>
<li>反向文档频率：每个检索词在索引中出现的频率？频率越高，相关性越低</li>
<li>字段长度准则：字段本身的长度是多少？长度越长，相关性越低</li>
</ol>
<div class="blog_h2"><span class="graybg">文档类型</span></div>
<p>Type用于对索引进行分区/分类，允许你在一个索引里存储不同类型的文档。从6.0开始Type被弃用。</p>
<div class="blog_h2"><span class="graybg">文档</span></div>
<p>可被索引的、最小的信息单元。以JSON形式表示。?</p>
<div class="blog_h2"><span class="graybg">分片/副本</span></div>
<p>一个索引的数据量可以超过硬盘的物理容量限制，ES使用分片来突破此限制。每个分片都是独立的、完整功能的“子索引”</p>
<p>创建索引时，你可以指定分片的数量。但是，<span style="background-color: #c0c0c0;">分片如何分配给节点，分片中的文档如何被聚合以响应查询，完全由ES管理，对用户透明</span>。</p>
<p>每个分片可以创建0-N个副本，这样可以避免单点故障。</p>
<div class="blog_h1"><span class="graybg">API</span></div>
<div class="blog_h2"><span class="graybg">API约定</span></div>
<p>ES的API通过JSON over RESTful HTTP暴露。除非特别强调，所有API都遵守本节描述的约定。</p>
<div class="blog_h3"><span class="graybg">多个索引</span></div>
<p>大部分支持index参数的API，都能够跨越多个索引执行。你可以用以下形式指定多个索引：</p>
<pre class="crayon-plain-tag"># 枚举
test1,test2,test3
# 所有索引
_all
# 使用通配符
test*
# 排除索引
-test3</pre>
<p>所有支持多索引的API，均识别以下URL参数：</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>ignore_unavailable</td>
<td>是否忽视不存在或者关闭的索引，取值true/false</td>
</tr>
<tr>
<td>allow_no_indices</td>
<td>是否允许没有任何匹配的索引</td>
</tr>
<tr>
<td>expand_wildcards</td>
<td>通配符如何展开，open仅仅展开匹配打开的索引，其它取值all，close</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">索引名中的日期</span></div>
<p>指定索引名时，你可以提供一些特殊符号，以匹配时间序列索引集中的一个范围，这样可以避免全集群扫描过滤。</p>
<p>几乎所有支持index参数的API，均可以指定如下格式的索引名：</p>
<pre class="crayon-plain-tag">&lt;static_name{date_math_expr{date_format|time_zone}}&gt;
# static_name 索引名中固定的部分
# date_math_expr 动态计算为时间点的表达式
# date_format 日期展示格式，默认YYYY.MM.dd
# time_zone 时区，默认UTC</pre>
<p>示例：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 45%; text-align: center;">表达式</td>
<td style="text-align: center;">?说明</td>
<td style="text-align: center;">解析为</td>
</tr>
</thead>
<tbody>
<tr>
<td>&lt;logstash-{now/d}&gt;</td>
<td>截断到日起点</td>
<td>logstash-2018.04.12</td>
</tr>
<tr>
<td>&lt;logstash-{now/M}&gt;</td>
<td>截断到月起点?</td>
<td>logstash-2018.04.01</td>
</tr>
<tr>
<td>&lt;logstash-{now/M{YYYY.MM}}&gt;</td>
<td>截断到月，格式化为年月</td>
<td>logstash-2018.04</td>
</tr>
<tr>
<td>&lt;logstash-{now/M-1M{YYYY.MM}}&gt;</td>
<td>截断到月，减一月</td>
<td>logstash-2018.03</td>
</tr>
<tr>
<td>&lt;logstash-{now/d{YYYY.MM.dd|+8:00}}&gt;?</td>
<td>使用东八区格式化</td>
<td>logstash-2018.04.12</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">日期计算</span></div>
<p>运算符：<pre class="crayon-plain-tag">+1d</pre>添加一天、<pre class="crayon-plain-tag">-1d</pre>减少一天、<pre class="crayon-plain-tag">/d</pre>向下截断到最近一天、<pre class="crayon-plain-tag">/h</pre>向下截断到最近一小时。例如<pre class="crayon-plain-tag">now-1h/d</pre>表示当前时间的毫秒数减去1小时，然后向下截断为UTC当日零时。</p>
<p>日期字段：y年、M月、w周、d日、h时、m分、s秒</p>
<div class="blog_h3"><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>pretty=true</td>
<td>格式化打印，默认打印为JSON格式</td>
</tr>
<tr>
<td>format=yaml</td>
<td>打印为YAML格式</td>
</tr>
<tr>
<td>human=false</td>
<td>是否以人类易读方式输出数字</td>
</tr>
<tr>
<td>filter_path</td>
<td>
<p>用于减少服务器返回的响应长度，该参数为逗号分隔的、响应字段过滤器。例如：</p>
<p style="padding-left: 30px;">filter_path=took,hits.hits._id,hits.hits._score?<br />filter_path=metadata.indices.*.stat* ?支持通配符<br />filter_path=routing_table.indices.**.state ?通配符**表示可以跨越多级路径<br />filter_path=-_shards 短横线表示排除</p>
</td>
</tr>
<tr>
<td>flat_settings</td>
<td>影响_settings查询的输出格式</td>
</tr>
<tr>
<td>error_trace</td>
<td>设置为true，则查询出错时返回结果包含调用栈信息，便于诊断</td>
</tr>
<tr>
<td>source</td>
<td>使用不支持非POST请求体的HTTP客户端库时，使用此参数传递请求体内容</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">参数风格</span></div>
<p>REST参数（使用HTTP时对应URL参数）使用小写+下划线的风格。</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>布尔</td>
<td>取值true或者false，不支持其它取值</td>
</tr>
<tr>
<td>数字</td>
<td>
<p>支持原生JSON数字类型</p>
<p>支持单位：k、m、g、t、p</p>
</td>
</tr>
<tr>
<td>时间</td>
<td>支持单位：d、h、m、s、ms、micros、nanos</td>
</tr>
<tr>
<td>字节数</td>
<td>支持单位：b、kb、mb、gb、tb、pb</td>
</tr>
<tr>
<td>距离</td>
<td>支持单位：km、m、cm、mm</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">模糊查询</span></div>
<p>某些查询/API支持非精确的“模糊查询”，你可以指定fuzziness参数。</p>
<p>当针对text或keyword字段进行模糊查询时，“模糊”被解释为编辑距离（Levenshtein Edit Distance）——为了让一个字符串变为另一个，所需改变的字符个数。</p>
<div class="blog_h3"><span class="graybg">关于请求体</span></div>
<p>某些库不支持非POST请求的请求体，此时你可以将请求体作为source查询参数传递。</p>
<p>需要同时传递参数source_content_type来指定内容类型，例如application/json。</p>
<div class="blog_h3"><span class="graybg">内容类型</span></div>
<p>必须通过请求头Content-Type来设置请求体格式，大部分API支持 JSON, YAML, CBOR,SMILE这些格式。批量/多搜索API支持NDJSON,JSON,SMILE。</p>
<div class="blog_h2"><span class="graybg">查看集群状态</span></div>
<div class="blog_h3"><span class="graybg">总体健康状况</span></div>
<pre class="crayon-plain-tag">curl http://localhost:9200/_cat/health?v
# status取值含义：
# green   一切正常，集群功能完整
# yellow  所有数据可用，但是某些副本分片（Replica）没有分配。集群功能完整
# red    某些数据不可用，存在没有运行的主分片</pre>
<div class="blog_h3"><span class="graybg">节点信息</span></div>
<pre class="crayon-plain-tag">// curl http://es-elasticsearch.kube-system.svc.k8s.gmem.cc:9200/_nodes/os?pretty

{
  "_nodes" : {
    "total" : 12,
    "successful" : 12,
    "failed" : 0
  },
  "cluster_name" : "es",
  "nodes" : {
    "CmHIMj5aReqUPXL4yglVjQ" : {
      "name" : "es-client-695458dd5c-4dt26",
      "transport_address" : "172.27.208.114:9300",
      "host" : "172.27.208.114",
      "ip" : "172.27.208.114",
      "version" : "6.2.4",
      "build_hash" : "ccec39f",
      "roles" : [
        "ingest"
      ],
      "os" : {
        "refresh_interval_in_millis" : 1000,
        "name" : "Linux",
        "arch" : "amd64",
        "version" : "4.15.18-041518-generic",
        // 可用的CPU数量
        "available_processors" : 4,
        // ES可用的CPU数量
        "allocated_processors" : 1
      }
    }
  }
}</pre>
<div class="blog_h2"><span class="graybg">修改副本份数</span></div>
<pre class="crayon-plain-tag"># PUT /index_name/_settings
{
   "number_of_replicas" : 2
}</pre>
<p>默认副本份数是1，要为将来创建的索引修改副本份数，执行：</p>
<pre class="crayon-plain-tag"># 未来创建的以fluentd开头的索引，副本份数为0

curl -XPUT "localhost:9200/_template/logstash_template" -H 'Content-Type: application/json' -d'
{
  "index_patterns": ["fluentd*"],
  "settings": {
    "number_of_replicas": 0
  }
}
'</pre>
<div class="blog_h2"><span class="graybg">测试分析器</span></div>
<p>你可以使用 analyze API 来看文本是如何被分析的：</p>
<pre class="crayon-plain-tag"># GET /_analyze
{
  "analyzer": "standard",
  "text": "Text to analyze"
}</pre>
<div class="blog_h2"><span class="graybg">查看映射</span></div>
<p>使用_mapping API可以获取1-N个索引的1-N个字段的映射信息：</p>
<pre class="crayon-plain-tag"># 获取索引gb的tweet（文档）类型的映射信息
# GET /gb/_mapping/tweet

{
   "gb": {
      "mappings": {
         "tweet": {
            # 字段列表
            "properties": {
               "date": {
                  "type": "date",
                  "format": "strict_date_optional_time||epoch_millis"
               },
               "name": {
                  "type": "string"
               },
               "tweet": {
                  "type": "string"
               },
               "user_id": {
                  "type": "long"
               }
            }
         }
      }
   }
}

# 具有内嵌对象的文档的映射形式
{
  "gb": {
    "tweet": { 
      "properties": {
        "tweet":            { "type": "string" },
        # 内嵌文档
        "user": { 
          "type":             "object",
          "properties": {
            "id":           { "type": "string" },
            # 内嵌文档
            "name":   { 
              "type":         "object",
              "properties": {
                "first":    { "type": "string" },
                "last":     { "type": "string" }
              }
            }
          }
        }
      }
    }
  }
}</pre>
<div class="blog_h2"><span class="graybg">验证查询</span></div>
<p>_validate 可以用来验证查询是否合法：</p>
<pre class="crayon-plain-tag"># GET /gb/tweet/_validate/query
# 显示查询不合法的原因
# GET /gb/tweet/_validate/query?explain
{
   "query": {
      "tweet" : {
         "match" : "really powerful"
      }
   }
}</pre>
<div class="blog_h2"><span class="graybg">创建索引</span></div>
<pre class="crayon-plain-tag">curl -X PUT http://localhost:9200/media?pretty

{
  "acknowledged" : true,
  "shards_acknowledged" : true,
  "index" : "media"
}</pre>
<div class="blog_h3"><span class="graybg"><a id="api_mapping"></a>自定义映射</span></div>
<p>尽管在很多情况下基本域数据类型已经够用，但你经常需要为单独域自定义映射 ，特别是字符串域。自定义映射允许你执行下面的操作：</p>
<ol>
<li>全文字符串域和精确值字符串域的区别</li>
<li>使用特定语言分析器</li>
<li>优化域以适应部分匹配</li>
<li>指定自定义数据格式</li>
</ol>
<p>指定字段映射时，最重要的属性是type，对于非string字段，通常仅仅需要设置type。</p>
<p>首次创建一个索引时，你可以指定自定义映射，以后，你可以使用_mapping API创建新字段的映射，或者修改现有字段的映射。注意一个限制：不能把字段从analyzed修改为not_analyzed。</p>
<p>首次创建索引时指定映射的例子：</p>
<pre class="crayon-plain-tag"># PUT /gb 
{
  "mappings": {
    "tweet" : {
      "properties" : {
        "tweet" : {
          "type" :    "string",
          # analyzed 设置为全文字段；not_analyzed 设置为精确值字段；no 不索引此字段，不支持检索
          "index" : "analyzed",
          # 分析器，默认standard
          "analyzer": "english"
        },
        "date" : {
          "type" :   "date"
        },
        "name" : {
          "type" :   "string"
        },
        "user_id" : {
          "type" :   "long"
        }
      }
    }
  }
}</pre>
<p>修改某个文档类型的某个字段的映射的例子：</p>
<pre class="crayon-plain-tag"># PUT /gb/_mapping/tweet
{
  "properties" : {
    "tag" : {
      "type" :    "string",
      "index":    "not_analyzed"
    }
  }
} </pre>
<div class="blog_h2"><span class="graybg">列出索引</span></div>
<pre class="crayon-plain-tag">curl http://localhost:9200/_cat/indices?v
# health status index uuid                   pri rep docs.count docs.deleted store.size pri.store.size
# yellow open   media EGuJhl4oSMy-FAhgK0bPJQ   5   1          0            0      1.1kb          1.1kb
# 由于没有额外的Replica，存在单点风险，因此yellow   包含5个分片，副本份数1</pre>
<div class="blog_h2"><span class="graybg">索引文档</span></div>
<p>插入一个文档：</p>
<pre class="crayon-plain-tag">curl -X PUT 'localhost:9200/customer/_doc/1?pretty' -H 'Content-Type: application/json' -d'{"name":"Alex"}'

{
  "_index" : "customer",   # 文档所在的索引
  "_type" : "_doc",        # 文档的类型，不同规格的文档可以共享类似的Schema，这些文档放在同一个索引中，以type区分
  "_id" : "1",             # 文档标识符，从URL参数获得
  "_version" : 1,          # 文档版本号
  "result" : "created",
  "_shards" : {            # 索引操作的复制处理情况
    "total" : 2,           # 有多少分片（包括主分片、复制分片）执行了索引操作
    # 成功完成索引操作的分片个数，至少为1才意味着索引操作成功。默认情况下仅主分片成功后就会返回
    "successful" : 1,      
    "failed" : 0           
  },
  "_seq_no" : 0,
  "_primary_term" : 1
}</pre>
<p>使用相同URL再次PUT，则对标识符为1的文档进行<span style="background-color: #c0c0c0;">替换（Reindex）</span>操作。</p>
<p>插入文档时，可以不显式指定ID，ES会自动随机生成一个：<pre class="crayon-plain-tag">POST /customer/_doc?pretty ...</pre> </p>
<div class="blog_h3"><span class="graybg">自动创建索引</span></div>
<p>如果索引文档时，目标索引不存在，则：</p>
<ol>
<li>自动创建索引，设置选项action.auto_create_index=false，可以禁用</li>
<li>自动创建一个类型映射，设置选项index.mapper.dynamic=false，可以禁用</li>
</ol>
<div class="blog_h3"><span class="graybg">版本化</span></div>
<p>每个被索引的文档都具有对应的版本号，此版本号作为响应的一部分返回。默认情况下，索引<span style="background-color: #c0c0c0;">从1开始，每次更新（即使没有做任何改变）、删除操作后增1</span>。</p>
<p>请求可以直接指定版本号，这种情况下ES自动进行乐观并发控制：PUT index_name/_doc/1?version=2。乐观并发控制的典型应用场景是<span style="background-color: #c0c0c0;">读后更新</span>。</p>
<p>版本化是完全实时的，不受检索操作的准实时性影响。如果不提供version参数，则ES不对操作进行版本检查。</p>
<p>版本号可以存放在ES外部，要启用此特性，设置version_type=external。这种情况下，请求参数中的版本号会和当前被索引的文档中的版本号进行比较，如果请求中的版本号大则新文档被存储、索引。</p>
<div class="blog_h3"><span class="graybg">操作类型</span></div>
<p>指定<pre class="crayon-plain-tag">op_type=create</pre>则强制进行创建操作，如果同ID的文档已经存在，则报错。</p>
<div class="blog_h3"><span class="graybg">路由</span></div>
<p>默认情况下，文档ID的哈希值决定了它被存放到哪个分片上。你可以使用请求参数<pre class="crayon-plain-tag">routing</pre>，其值作为哈希函数的入参。</p>
<div class="blog_h3"><span class="graybg">复制</span></div>
<p>根据路由的结果，索引操作在相应的主分片（所在的节点）上执行。当主分片的索引操作完成后，更新操作复制到可用的从分片。</p>
<p>为了提升可靠性，ES允许配置为必须<span style="background-color: #c0c0c0;">等待一定数量的分片的写操作完成</span>，在此之前，请求必须等待、重试，或者超时。默认情况下只需要等待主分片，即index.write.wait_for_active_shards=1。你也可以针对请求来设置wait_for_active_shards参数。设置为all则需要等待所有分片操作完成。</p>
<div class="blog_h3"><span class="graybg">超时</span></div>
<p>执行索引操作时，主分片可能处于不可用状态，默认情况下，ES会等待1分钟，此超时时间可以通过请求参数timeout定制。</p>
<div class="blog_h2"><span class="graybg">更新文档</span></div>
<p>除了插入/替换文档之外，我们还可以进行更新操作。注意，实际上ES是不支持In-place更新的，它仅仅是把更新信息merge到原文档中，然后替换掉原文档。示例：</p>
<pre class="crayon-plain-tag">curl -XPOST 'localhost:9200/customer/_doc/1/_update?pretty' -H 'Content-Type: application/json' -d'
{
  # 可以指定一个需要merge from的文档
  "doc": { "name": "Wong", "age": 30 }
  # 也可以指定一段脚本
  "script" : "ctx._source.age += 1"
}
' </pre>
<div class="blog_h2"><span class="graybg">提取文档</span></div>
<p>这类API允许基于ID来取得JSON格式的文档：</p>
<pre class="crayon-plain-tag">curl http://localhost:9200/customer/_doc/1?pretty

{
  "_index" : "customer",
  "_type" : "_doc",
  "_id" : "1",
  "_version" : 4,
  "found" : true,
  "_source" : {           # 文档的完整JSON
    "name" : "Alex"
  }
}</pre>
<div class="blog_h3"><span class="graybg">存在性检查</span></div>
<p>你也可以使用HEAD方法来检查目标文档是否存在：HEAD customer/_doc/id</p>
<div class="blog_h3"><span class="graybg">实时性</span></div>
<p>默认情况下get操作是实时的，不受到索引刷新率的影响。</p>
<p>如果所请求的文档已经更新，但是尚未刷新，则此API会即席的发起一次刷新操作。设置请求参数realtime=false禁用此自动刷新行为。</p>
<div class="blog_h3"><span class="graybg">源文档过滤</span></div>
<p>默认情况下此API获取源文档的所有内容，即_source字段。设置_source=false则不返回源文档的任何内容。</p>
<p>如果需要返回某些字段，使用_source_include请求参数；如果需要排除某些字段，使用_source_exclude字段，示例：</p>
<pre class="crayon-plain-tag"># 可以使用通配符
_source_include=*.id&amp;_source_exclude=entities
# 如果仅仅使用include，可以直接简写为_source
_source=*.id,retweeted</pre>
<div class="blog_h3"><span class="graybg">直接返回_source</span></div>
<p>要仅仅返回响应文档的_source字段，使用请求：customer/_doc/1/_source</p>
<div class="blog_h3"><span class="graybg">路由</span></div>
<p>如果索引文档时使用routing来指定路由，则<span style="background-color: #c0c0c0;">提取文档时必须传入相同的routing参数</span>。</p>
<div class="blog_h3"><span class="graybg">在哪提取</span></div>
<p>使用参数preference，可以指定在什么分片上提取文档。默认值是随机选取分片。取值_primary则仅仅在主分片上提取，取值_local则尽可能在本地分配的分片上提取。</p>
<p>分片副本份数越多，则操作的性能越好。</p>
<div class="blog_h3"><span class="graybg">刷新</span></div>
<p>使用参数refresh=true，可以在提取之前刷新相关的分片。使用此参数时要注意对系统性能的潜在影响。</p>
<div class="blog_h3"><span class="graybg">版本化</span></div>
<p>传递version参数，则仅在当前文档版本号匹配时，才返回。</p>
<div class="blog_h2"><span class="graybg">批量提取</span></div>
<p>使用mget API可以同时提取多个文档：</p>
<pre class="crayon-plain-tag"># GET /_mget
{
   "docs" : [
      {
         "_index" : "website",
         "_type" :  "blog",
         "_id" :    2
      },
      {
         "_index" : "website",
         "_type" :  "pageviews",
         "_id" :    1,
         "_source": "views"
      }
   ]
}
# 其它变体
# GET /index_name/_mget

# 如果索引、类型都相同，则仅指定一个ids数组即可
# GET /index_name/type/_mget
{
    "ids" : [ "2", "1" ]
}</pre>
<p>返回值包含一个docs数组</p>
<div class="blog_h2"><span class="graybg">删除文档</span></div>
<p>根据ID来删除一个已被索引的文档：</p>
<pre class="crayon-plain-tag">curl -X DELETE 'localhost:9200/customer/_doc/1?pretty' </pre>
<p>版本化、路由、分布式、刷新、超时类似于索引文档。</p>
<div class="blog_h2"><span class="graybg">按查询删除</span></div>
<p>可以将匹配查询条件的文档删除：</p>
<pre class="crayon-plain-tag">curl -X POST "/twitter/_delete_by_query" -H 'Content-Type: application/json' -d'
{
  "query": { 
    "match": {
      "message": "some message"
    }
  }
}
'</pre>
<p>此API会获取其被调用时的索引的快照，并基于此快照中匹配文档的内部版本号，执行删除操作。</p>
<p>注意：由于内部版本化不支持版本号0， 因此version=0的文档无法被_delete_by_query删除。 </p>
<div class="blog_h2"><span class="graybg">删除索引</span></div>
<pre class="crayon-plain-tag">curl -X DELETE 'localhost:9200/customer?pretty'

{
  "acknowledged" : true
}</pre>
<div class="blog_h2"><span class="graybg">批量处理</span></div>
<p>ES提供了批量操作的API，可以把多个CRUD操作组合在一起执行：</p>
<pre class="crayon-plain-tag"># 插入两个文档
curl -X POST 'localhost:9200/customer/_doc/_bulk?pretty' -H 'Content-Type: application/json' -d'
{"index":{"_id":"1"}}
{"name": "John Doe" }     # Source Document
{"index":{"_id":"2"}}
{"name": "Jane Doe" }
'

# 更新一个文档，删除一个文档
curl -XPOST 'localhost:9200/customer/_doc/_bulk?pretty' -H 'Content-Type: application/json' -d'
{"update":{"_id":"1"}}
{"doc": { "name": "John Doe becomes Jane Doe" } }
{"delete":{"_id":"2"}}
'

# 注意删除操作不需要指定的Source Document</pre>
<p>如果批处理中的某个操作失败，则它会继续处理。最终，所有操作的执行结果会返回给调用者。</p>
<div class="blog_h2"><span class="graybg">全文检索</span></div>
<p>准备数据：</p>
<pre class="crayon-plain-tag">wget https://raw.githubusercontent.com/elastic/elasticsearch/master/docs/src/test/resources/accounts.json
# 从文件读取批处理的输入
curl -H "Content-Type: application/json" -X POST "localhost:9200/bank/_doc/_bulk?pretty&amp;refresh" --data-binary "@accounts.json"</pre>
<div class="blog_h3"><span class="graybg">路由</span></div>
<p>当执行搜索时，请求会广播给索引的全部分片，并以RR算法选择分片的Replica。使用routing参数可以强制在匹配哈希值的分片上执行搜索，你可以为<span style="background-color: #c0c0c0;">routing指定逗号分隔的多个值</span>。</p>
<div class="blog_h3"><span class="graybg">自适应Replica选择</span></div>
<p>除了默认的RR轮询算法以外，ES还支持自适应Replica选择，自动选取最适当的Replica。选取准则包括：</p>
<ol>
<li>根据协调（coordinating）节点向数据节点转发请求的响应时间</li>
<li>在数据节点上执行请求所消耗的时间</li>
<li>数据节点的搜索线程池大小</li>
</ol>
<p>要启用该特性，设置集群选项：</p>
<pre class="crayon-plain-tag"># PUT /_cluster/settings
{
    "transient": {
        "cluster.routing.use_adaptive_replica_selection": true
    }
}</pre>
<div class="blog_h3"><span class="graybg">全局超时</span></div>
<p>除了在每个请求中设置超时之外，ES还支持全局性的搜索超时search.default_search_timeout，此设置没有默认值，设置为-1可以取消先前设置的值。</p>
<div class="blog_h3"><span class="graybg">取消搜索</span></div>
<p>搜索可以通过标准的<a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/tasks.html#task-cancellation">任务取消机制</a>来取消。默认情况下ES仅仅在段边界（segment boundaries）来检查请求是否已经被取消，因此取消操作可能由于大段而延迟。要降低取消操作的响应时间，可以设置search.low_level_cancellation=true，但是要注意此设置会导致更加频繁的检查。</p>
<div class="blog_h3"><span class="graybg">并发和并行</span></div>
<p>默认情况下ES不限制搜索请求牵涉到的分片数量，你可以设置软限制 action.search.shard_count.limit 来拒绝命中太多分片的请求。</p>
<p>参数max_concurrent_shard_requests可以限制搜索请求最多同时在多少个分片上执行，可以防止单个搜索请求消耗整个集群的资源。 此参数的默认值取决于集群中数据节点的数量，最多256。</p>
<div class="blog_h3"><span class="graybg">检索参数</span></div>
<p>检索API支持两种传递查询参数的方式：通过URL参数、通过请求体。</p>
<p>要检索特定索引上，任何类型的文档，使用：/index_name/_search</p>
<p>要检索特定索引上，特定类型的文档，使用：/index_name/type1,type2.../_search</p>
<p>要检索多个索引上，具有特定标签的，使用：/index1,index2/_search?q=tag:tag1</p>
<p>要检索任何的索引，使用：/_all/_search</p>
<div class="blog_h3"><span class="graybg">通过URL传参</span></div>
<p>示例：</p>
<pre class="crayon-plain-tag"># q=* 匹配所有文档，不指定field默认使用_all字段，此字段是String类型

# q=field:value 仅field字段匹配value的文档

# sort=account_number:asc  根据账号升序排列结果
curl -XGET 'localhost:9200/bank/_search?q=*&amp;sort=account_number:asc&amp;pretty'</pre>
<p>注意，<a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-uri-request.html#_parameters_3">并非所有检索选项支持URL方式传参</a>。</p>
<div class="blog_h3"><span class="graybg">通过请求体传参</span></div>
<p>query参数传递Query DSL，示例：</p>
<pre class="crayon-plain-tag">curl -XGET 'localhost:9200/bank/_search?pretty' -H 'Content-Type: application/json' -d'
{
  "from" : 0, "size" : 10,
  "query": { "match_all": {} },
  "sort": [
    { "account_number": "asc" }
  ]
}
'</pre>
<p>可用参数：</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>timeout</td>
<td>搜索超时，默认无</td>
</tr>
<tr>
<td>from</td>
<td>分页，起始偏移量，默认0</td>
</tr>
<tr>
<td>size</td>
<td>分页，抓取结果数量，默认10</td>
</tr>
<tr>
<td>search_type</td>
<td>
<p>搜索类型，取值dfs_query_then_fetch、query_then_fetch，默认query_then_fetch</p>
<p>只能作为URL参数传递</p>
</td>
</tr>
<tr>
<td>request_cache</td>
<td>
<p>true/false，是否启用搜索结果（仅针对size为0的请求，亦即聚合/建议请求）的缓存</p>
<p>只能作为URL参数传递</p>
</td>
</tr>
<tr>
<td>terminate_after</td>
<td>
<p>每个分片最多收集的文档数量，如果超过限制查询立即终止，响应中的terminated_early设置为true</p>
<p>设置此参数为1，可以实现快速检查是否存在匹配（exists）</p>
</td>
</tr>
<tr>
<td>batched_reduce_size</td>
<td>在协调节点（coordinating node）上，每次Reduce分片结果的数量。可以防止单个请求占用太多的内存</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">搜索结果示例 </span></div>
<pre class="crayon-plain-tag">{
  "took" : 10,               # 执行搜索消耗的时间
  "timed_out" : false,       # 搜索是否超时
  "_shards" : {              # 多少分片参与到搜索
    "total" : 5,
    "successful" : 5,     
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {                 # 搜索结果
    "total" : 1000,          # 匹配的总数
    "max_score" : null,      # 最大分数
    "hits" : [               # 实际匹配文档的数组
      {
        "_index" : "bank",   
        "_type" : "_doc",
        "_id" : "0",
        "_score" : null,     # 分数表示文档匹配查询的程度，值越大，匹配程度越高
        "_source" : {
          "account_number" : 0,
          "balance" : 16623,
          "firstname" : "Bradshaw",
          "lastname" : "Mckenzie",
          "age" : 29,
          "gender" : "F",
          "address" : "244 Columbus Place",
          "employer" : "Euron",
          "email" : "bradshawmckenzie@euron.com",
          "city" : "Hobucken",
          "state" : "CO"
        },
        "sort" : [
          0
        ]
      },
      {
        "_index" : "bank",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : null,
        "_source" : {
          "account_number" : 1,
          "balance" : 39225,
          "firstname" : "Amber",
          "lastname" : "Duke",
          "age" : 32,
          "gender" : "M",
          "address" : "880 Holmes Lane",
          "employer" : "Pyrami",
          "email" : "amberduke@pyrami.com",
          "city" : "Brogan",
          "state" : "IL"
        },
        "sort" : [
          1
        ]
      },
      ...
    ]
  }
}</pre>
<p>一旦搜索结果返回，ES就不会在服务器端存留任何资源，例如游标。这个特性和关系型数据库不同。</p>
<div class="blog_h3"><span class="graybg">分页查询</span></div>
<pre class="crayon-plain-tag"># 返回所有文档
{
  "query": { "match_all": {} }
}
# 返回第一个匹配的文档，size默认为10
{
  "query": { "match_all": {} },
  "size": 1
}
# 分页，返回11-20个文档
{
  "query": { "match_all": {} },
  "from": 10,
  "size": 10
}</pre>
<div class="blog_h2"><span class="graybg">结果排序</span></div>
<p>默认情况下，返回的结果是<span style="background-color: #c0c0c0;">按照相关性（评分）进行排序</span>的——最相关的文档排在最前，默认按照_score字段降序排序。</p>
<p>你也可以定制排序方式：</p>
<pre class="crayon-plain-tag"># 根据balance升序排列
{
  "query": { "match_all": {} },
  "sort": { "balance": { "order": "desc" } }
}

# 多值字段排序
{
   "query" : {
      "term" : { "country" : "china" }
   },
   "sort" : [
      {"population" : {"order" : "asc", "mode" : "avg"}}
   ]
}</pre>
<div class="blog_h3"><span class="graybg">多值字段排序</span></div>
<p>ES支持根据数组字段、多值字段进行排序，此时可以设置sort.mode字段：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">sort.mode</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>min</td>
<td>取数组中的最小值参与排序</td>
</tr>
<tr>
<td>max</td>
<td>取数组中的最大值参与排序</td>
</tr>
<tr>
<td>sum</td>
<td>取数组元素总和参与排序</td>
</tr>
<tr>
<td>avg</td>
<td>取数组元素平均值参与排序</td>
</tr>
<tr>
<td>median</td>
<td>取中位数参与排序</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">嵌套字段排序</span></div>
<p>ES支持依据文档中的嵌套字段进行排序，示例：</p>
<pre class="crayon-plain-tag">{
   "query" : {
      "term" : { "product" : "chocolate" }
   },
   "sort" : [
       {
          # 根据嵌套字段排序
          "offer.price" : {
             # 取嵌套字段平均值
             "mode" :  "avg",
             # 升序排列
             "order" : "asc",
             # 嵌套字段信息
             "nested": {
                # 导航路径
                "path": "offer",
                # 过滤条件
                "filter": {
                   "term" : { "offer.color" : "blue" }
                }
             }
          }
       }
    ]
}</pre>
<div class="blog_h3"><span class="graybg">缺失字段处理</span></div>
<p>使用missing指定可以指定当某个文档没有参与排序的字段时该怎么办，默认值为_last，可以取值_first或者自定义一个用于排序的数值：</p>
<pre class="crayon-plain-tag">{
    "sort" : [
        { "price" : {"missing" : "_last"} }
    ]
}</pre>
<div class="blog_h3"><span class="graybg">地理距离排序</span></div>
<p>ES支持根据二维平面上的距离值来排序，示例：</p>
<pre class="crayon-plain-tag">{
    "sort" : [
        {
            "_geo_distance" : {
                # 支持多种形式的坐标：
                "pin.location" : { "lat" : 40, "lon" : -70 },
                "pin.location" : "40,-70",
                "pin.location" : [[-70, 40], [-71, 42]],
                "pin.location" : [-70, 40],  # 排序的字段: 计算距离时的中心点
                "order" : "asc",             # 升降序
                "unit" : "km",
                "mode" : "min",              # 如果排序字段中包含多个Geo点，如何处理
                "distance_type" : "arc"      # 可以取值plane，速度快但是长距离、近极地时不准确
            }
        }
    ]
}</pre>
<div class="blog_h3"><span class="graybg">基于脚本排序</span></div>
<p>排序算法可以由自定义的脚本提供：</p>
<pre class="crayon-plain-tag">{
    "sort" : {
        "_script" : {
            "type" : "number",
            "script" : {
                "lang": "painless",
                "source": "doc['field_name'].value * params.factor",
                "params" : {
                    "factor" : 1.1
                }
            },
            "order" : "asc"
    }
}</pre>
<div class="blog_h3"><span class="graybg">计算分数</span></div>
<p>当使用排序时，默认不会计算匹配分数，要改变此行为设置track_scores = true</p>
<div class="blog_h3"><span class="graybg">内存消耗</span></div>
<p>当执行排序时，排序相关的字段被载入内存。每个分片都需要具有足够的内存来容纳这些字段：</p>
<ol>
<li>对于参与排序的字符串类型，不应该被analyzed/tokenized</li>
<li>对于参与排序的数字类型，尽可能使用更短的类型</li>
</ol>
<div class="blog_h2"><span class="graybg">指定返回字段</span></div>
<pre class="crayon-plain-tag"># 不返回任何字段
{
    "_source": false
}

# 返回account_number、balance两个字段
{
  "query": { "match_all": {} },
  "_source": ["account_number", "balance"]
}

# 使用通配符指定字段
{
    "_source": [ "obj1.*", "obj2.*" ]
}

# 指定需要包含、排除的字段
{
    "_source": {
        "includes": [ "obj1.*", "obj2.*" ],
        "excludes": [ "*.description" ]
    }
}</pre>
<div class="blog_h3"><span class="graybg">脚本生成字段</span></div>
<p>允许根据现有字段，进行计算，衍生出新的字段：</p>
<pre class="crayon-plain-tag">{
    "query" : {
        "match_all": {}
    },
    "script_fields" : {
        "test2" : {
            # 使用doc更快，内存消耗高（因为目标字段的terms被载入内存）。仅能返回简单的值（不能返回JSON文档）
            "script" : {
                "lang": "painless",
                "source": "doc['my_field_name'].value * params.factor",
                "params" : {
                    "factor"  : 2.0
                }
            },
            # 使用_source非常慢，因为整个文档需要载入并解析
            "script" : "params['_source']['my_field_name']"
        }
    }
}</pre>
<div class="blog_h2"><span class="graybg">查询条件</span></div>
<p>通常的规则是<span style="background-color: #c0c0c0;">，使用 查询（query）语句来进行全文搜索</span>或者其它任何<span style="background-color: #c0c0c0;">需要影响相关性得分的搜索</span>。除此以外的情况都使用过滤（filters)。</p>
<p>本节以Query DSL语法说明如何指定查询条件。</p>
<div class="blog_h3"><span class="graybg">查询语法</span></div>
<pre class="crayon-plain-tag"># 查询语句典型结构
{
    QUERY_NAME: {
        ARGUMENT: VALUE,
        ARGUMENT: VALUE,...
    }
}
# 针对特定字段的查询语句结构
{
    QUERY_NAME: {
        FIELD_NAME: {
            ARGUMENT: VALUE,
            ARGUMENT: VALUE,...
        }
    }
}</pre>
<div class="blog_h3"><span class="graybg">简单查询</span></div>
<pre class="crayon-plain-tag"># 全部匹配
"query": {}
"query": {
    "match_all": {}
}

# 账号为20
{
  "query": { "match": { "account_number": 20 } }
}
# 地址中包含mill字样
{
  "query": { "match": { "address": "mill" } }
}
# 地址中包含mill或者lane字样
{
  "query": { "match": { "address": "mill lane" } }
}
# 地址中包含"mill lane"这个短语
{
  "query": { "match_phrase": { "address": "mill lane" } }
}

# 在多个字段上进行匹配
{
    "multi_match": {
        "query":    "full text search",
        "fields":   [ "title", "body" ]
    }
}

# 查询落在指定区间的时间、数字
{
    "range": {
        "age": {
            "gte":  20,
            "lt":   30
        }
    }
}

# 精确匹配查询
{ "term": { "age":    26           }}
{ "term": { "date":   "2014-09-01" }}
{ "term": { "public": true         }}
{ "term": { "tag":    "full_text"  }}

# 精确匹配查询（多值，匹配任意一个即可）
{ "terms": { "tag": [ "search", "full_text", "nosql" ] }}


# 存在性查询
{
    # 存在title字段
    "exists":   {
        "field":    "title"
    }
    # 不存在title字段
    "missing":   {
        "field":    "title"
    }
}</pre>
<div class="blog_h3"><span class="graybg">逻辑或与非</span></div>
<pre class="crayon-plain-tag">{
  "query": {
    "bool": {
      # 与：地址中同时包含mill和lane
      "must": [
        { "match": { "address": "mill" } },
        { "match": { "address": "lane" } }
      ]
    }
  }
}
{
  "query": {
    "bool": {
      # 或：地址中包含mill或lane
      "should": [
        { "match": { "address": "mill" } },
        { "match": { "address": "lane" } }
      ]
    }
  }
}
{
  "query": {
    "bool": {
      # 非：地址中不得包含mill或lane
      "must_not": [
        { "match": { "address": "mill" } },
        { "match": { "address": "lane" } }
      ]
    }
  }
}

{
  "query": {
    # 返回年龄为40，且不住在爱达荷州的顾客账户
    "bool": {
      "must": [
        { "match": { "age": "40" } }
      ],
      "must_not": [
        { "match": { "state": "ID" } }
      ]
    }
  }
}</pre>
<div class="blog_h2"><span class="graybg">过滤条件</span></div>
<p>过滤查询（Filtering queries）<span style="background-color: #c0c0c0;">只是简单的检查包含或者排除，这就使得计算起来非常快</span>。过滤查询不进行评分，结果可以被缓存。</p>
<p>相反，评分查询（scoring queries）不仅仅要找出 匹配的文档，还要计算每个匹配文档的相关性，计算相关性使得它们<span style="background-color: #c0c0c0;">比不评分查询费力的多</span>。同时，<span style="background-color: #c0c0c0;">查询结果并不缓存</span>。</p>
<p>由于倒排索引（inverted index），一个简单的评分查询在匹配少量文档时可能与一个涵盖百万文档的filter表现的一样好，甚至会更好。但是在<span style="background-color: #c0c0c0;">一般情况下，一个filter 会比一个评分的query性能更优异</span>，并且每次都表现的很稳定。</p>
<p>注意filter可以放在不同位置，仅仅<span style="background-color: #c0c0c0;">其引用的查询条件不影响评分，而不是整个查询不支持评分</span>。过滤查询示例：</p>
<pre class="crayon-plain-tag">{
  "query": {
    "bool": {
      # 返回地址中包含lane（默认大小写不敏感），且余额在2w到3w之间的账户
      "must": { "match": { "address": "lane" } },
      "filter": {
        "range": {
          # 下面的条件不影响评分
          "balance": {
            "gte": 20000,
            "lte": 30000
          }
        }
      }
    }
  }
} </pre>
<div class="blog_h2"><span class="graybg">高亮匹配</span></div>
<p>ES支持修改搜索结果，为匹配搜索的字段添加HTML标签，以便<a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-highlighting.html">高亮显示</a>：</p>
<pre class="crayon-plain-tag">{
    "highlight": {
        "fields" : {
            # 默认情况下，about字段中匹配搜索条件的部分会被&lt;em&gt;标签包围
            "about" : {}
        }
    }
}</pre>
<div class="blog_h2"><span class="graybg">聚合查询</span></div>
<p>示例：</p>
<pre class="crayon-plain-tag">{
  # 不返回聚合前的结果集，我们仅仅关注聚合
  "size": 0,
  "aggs": {
    # 聚合结果的键
    "group_by_state": {
      "terms": {
        # 根据state字段进行分组
        "field": "state.keyword"
      },
      # 聚合函数默认是统计总数
    }
  }
}

# 类似于SQL：SELECT state, COUNT(*) FROM bank GROUP BY state ORDER BY COUNT(*) DESC</pre>
<p>执行结果如下：</p>
<pre class="crayon-plain-tag">{
  "took" : 28,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "skipped" : 0,
    "failed" : 0
  },
  # 聚合前的结果集
  "hits" : {
    "total" : 1000,
    "max_score" : 0.0,
    "hits" : [ ]
  },
  # 聚合
  "aggregations" : {
    # 键
    "group_by_state" : {
      "doc_count_error_upper_bound" : 20,
      "sum_other_doc_count" : 770,
      "buckets" : [
        {
          "key" : "ID",
          "doc_count" : 27
        },
        {
          "key" : "TX",
          "doc_count" : 27
        }
        ...
      ]
    }
  }
}</pre>
<p>你可以指定聚合函数、排序方式：</p>
<pre class="crayon-plain-tag">{
  "size": 0,
  "aggs": {
    "group_by_state": {
      "terms": {
        "field": "state.keyword",
        # 根据平均余额降序排列
        "order": {
          "average_balance": "desc"
        }
      },
      # 聚合函数：对账户余额取平均值
      "aggs": {
        "average_balance": {  # 字段名
          "avg": {
            "field": "balance"
          }
        }
      }
    }
  }
}</pre>
<div class="blog_h1"><span class="graybg">配置</span></div>
<p>ES提供了很适当的默认配置，需要很少的定制化。大部分配置项都可以<a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/cluster-update-settings.html">在运行时更新</a>。 </p>
<p>ES的配置文件主要有三个：</p>
<ol>
<li>elasticsearch.yml 主配置文件</li>
<li>jvm.options 使用的JVM参数</li>
<li>log4j2.properties 日志配置</li>
</ol>
<p>这些配置文件位于conf目录，具体位置和你使用的安装方式有关。可以通过环境变量定制：</p>
<pre class="crayon-plain-tag">ES_PATH_CONF=/path/to/my/config ./bin/elasticsearch</pre>
<div class="blog_h2"><span class="graybg">主配置文件</span></div>
<p>主配置文件基于YML格式，支持通过${VAR_NAME}来引用环境变量，例如：</p>
<pre class="crayon-plain-tag">node.name:    ${HOSTNAME}
network.host: ${ES_NETWORK_HOST}</pre>
<p>主要的配置项如下：</p>
<pre class="crayon-plain-tag">path:
  # 日志存放目录
  logs: /var/log/elasticsearch
  # 数据存放目录
  data: /var/data/elasticsearch
  # 可以指定多个存储位置
  data:
    - /mnt/elasticsearch_1
    - /mnt/elasticsearch_2
    - /mnt/elasticsearch_3

# 当前节点所属集群名称
cluster.name: logging-prod

# 当前节点的名称
node.name: ${HOSTNAME}

# 绑定的监听地址
network.host: 10.0.0.1

 
# 节点的相互发现
# ES实现了所谓Zen Discovery，用于节点发现、Master选举
# 不进行任何配置的情况下，ES会扫描localhost的9300-9305来寻找相同服务器上运行的节点
# 在实际集群中，你需要指定当前节点需要连接的其它节点
discovery.zen.ping.unicast.hosts:
   - 192.168.1.10:9300
   - 192.168.1.11 
   - seeds.mydomain.com 
# 节点列表可以不指定端口，默认使用transport.profiles.default.port
# 如果transport.profiles.default.port没有配置则使用transport.tcp.port

# 为了防止数据丢失，需要配置每个有资格成为Master的节点能够看到的，其它有资格成为Master的节点的最小数量
# 如果不设置此选项，在网络分区的情况下，集群会分裂为两个独立的小集群，即脑裂。脑裂会导致数据丢失
# 为了防止脑裂，需要设置此选项为 (master_eligible_nodes / 2) + 1
discovery.zen.minimum_master_nodes: 2
  zen:
    # 故障发现，默认1秒执行一次，超时30秒，超时3次则剔除节点。可能导致索引重新分配
    ping_interval: 1
    ping_timeout: 30s
    ping_retries: 3</pre>
<div class="blog_h2"><span class="graybg">JVM配置</span></div>
<p>默认情况下，ES使用固定大小的1G堆内存。关于JVM参数配置的建议包括：</p>
<ol>
<li>设置堆的最小值和最大值相同</li>
<li>堆越大，ES越可以缓存更多的东西。但是大堆意味着更长的GC停顿</li>
<li>Xmx不要设置超过50%的物理内存。为内核和系统缓存留下空间 </li>
</ol>
<p>ES使用Java安全管理器。JVM默认无限期的缓存DNS解析记录，如果你依赖于动态解析的DNS，则需要配置安全管理器：</p>
<pre class="crayon-plain-tag">networkaddress.cache.ttl=&lt;timeout&gt;、
networkaddress.cache.negative.ttl=&lt;timeout&gt;</pre>
<div class="blog_h2"><span class="graybg">操作系统配置</span></div>
<div class="blog_h3"><span class="graybg">禁用Swap</span></div>
<p>Swap可能导致JVM堆甚至可执行页被交换到磁盘中，对性能有非常不利的影响。Swap可能导致GC从毫秒级变为分钟级完成、导致节点响应异常缓慢甚至脱离集群。因此，宁愿让OS把节点杀掉也不要启用Swap。</p>
<p>你可以执行下面的命令来禁用所有交换文件：</p>
<pre class="crayon-plain-tag">sudo swapoff -a</pre>
<p>注释掉/etc/fstab中的相关行，可以永久禁用Swap。</p>
<p>设置systctl参数vm.swappiness为1，可以减小Linux内核进行Swap的倾向，让它在通常情况下不会Swap。</p>
<p>或者，你可以利用Linux的mlockall，将ES进程的地址空间锁定在内存中，配置ES：</p>
<pre class="crayon-plain-tag">bootstrap.memory_lock: true</pre>
<div class="blog_h3"><span class="graybg">文件描述符</span></div>
<p>ES需要使用大量的文件描述符/文件句柄。确保运行ES的用户可以打开65536或者更多的文件描述符。</p>
<div class="blog_h3"><span class="graybg">虚拟内存</span></div>
<p>ES默认使用一个mmapfs目录来存储其索引，OS的mmap计数默认值很低，可能导致OOM异常。执行下面命令修改：</p>
<pre class="crayon-plain-tag">sysctl -w vm.max_map_count=262144
# 一个进程可以拥有的VMA(虚拟内存区域)的数量，虚拟内存区域是一个连续的虚拟地址空间区域
# 在进程的生命周期中，每当程序尝试在内存中映射文件，链接到共享内存段，或者分配堆空间的时候，这些区域将被创建</pre>
<div class="blog_h3"><span class="graybg">线程数</span></div>
<p>ES需要创建很多线程来完成不同的操作，你最少要保证ES能够创建4096个线程：ulimit -u 4096或者设置nprocs</p>
<div class="blog_h1"><span class="graybg">x-pack</span></div>
<div class="blog_h2"><span class="graybg">启用TLS</span></div>
<p>对于Gold/Platinum类型的License，如果你启用了安全，则必须同时启用TLS。</p>
<div class="blog_h3"><span class="graybg">生成CA证书</span></div>
<p>你可以使用外部提供的CA证书，或者通过下面的命令生成：</p>
<pre class="crayon-plain-tag">bin/x-pack/certutil ca </pre>
<div class="blog_h3"><span class="graybg">生成节点密钥</span></div>
<p>可以使用下面的命令生成p12格式的私钥、证书：</p>
<pre class="crayon-plain-tag">bin/x-pack/certutil cert --ca elastic-stack-ca.p12</pre>
<p>默认情况下，此证书不包含SAN字段，因此可以所有节点共享。如果需要更加严格的身份验证，可以传入--name, --dns --ip参数。</p>
<p>你可以可以使用外部提供的证书。</p>
<div class="blog_h3"><span class="graybg">拷贝密钥</span></div>
<p>把密钥拷贝到节点的config/certs目录下</p>
<div class="blog_h3"><span class="graybg">修改配置</span></div>
<p>添加以下ES配置项：</p>
<pre class="crayon-plain-tag">xpack.security.transport.ssl.enabled: true
xpack.security.transport.ssl.verification_mode: certificate 
xpack.security.transport.ssl.keystore.path: certs/elastic-certificates.p12 
xpack.security.transport.ssl.truststore.path: certs/elastic-certificates.p12</pre>
<p>验证模式默认certificate，不检查SAN，如果需要更加严格的身份验证，设置为full。</p>
<p>重启服务后，集群节点间的TLS通信OK，你可以使用Gold/Platinum的License了。</p>
<div class="blog_h2"><span class="graybg">安装License</span></div>
<pre class="crayon-plain-tag">curl -u elastic:$PSWD 'http://es-elasticsearch.kube-system.svc.k8s.gmem.cc:9200/_xpack/license' \
     -H "Content-Type: application/json" -d @eslic.json
                                            # license文件路径</pre>
<div class="blog_h2"><span class="graybg">身份验证和授权</span></div>
<div class="blog_h3"><span class="graybg">更改内置用户密码</span></div>
<p>登陆到客户端节点，执行下面的命令，可以随机生成elastic、kibana、logstash_system的新密码：</p>
<pre class="crayon-plain-tag">bin/x-pack/setup-passwords auto</pre>
<p> 或者，你也可以交互式的设置密码：</p>
<pre class="crayon-plain-tag">bin/x-pack/setup-passwords interactive </pre>
<div class="blog_h3"><span class="graybg">启用匿名用户</span></div>
<p>添加ES配置项：</p>
<pre class="crayon-plain-tag">xpack.security.authc:
  anonymous:
    # 匿名用户的名称
    username: anonymous
    # 授予的角色，逗号分隔
    roles: transport_client</pre>
<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>ingest_admin</td>
<td>可以访问所有索引模板、ingest流水线配置</td>
</tr>
<tr>
<td>kibana_dashboard_only_user</td>
<td>可以访问Kibana仪表盘，只读访问.kibana索引</td>
</tr>
<tr>
<td>kibana_system</td>
<td>可以读写Kibana索引，检查集群可用性，管理索引模板。可以读.monitoring-*索引，读写.reporting-*索引</td>
</tr>
<tr>
<td>kibana_user</td>
<td>Kibana用户的最小权限，可以访问Kibana索引，监控集群状态</td>
</tr>
<tr>
<td>logstash_admin</td>
<td>可以访问.logstash*索引，以管理配置</td>
</tr>
<tr>
<td>logstash_system</td>
<td>Logstash系统级用户，可以发送监控数据给ES</td>
</tr>
<tr>
<td>monitoring_user</td>
<td>支持X-pack monitoring</td>
</tr>
<tr>
<td>remote_monitoring_agent</td>
<td>支持写入监控数据到ES</td>
</tr>
<tr>
<td>superuser</td>
<td>超级用户</td>
</tr>
<tr>
<td>transport_client</td>
<td>
<p>支持访问Node Liveness API、Cluster State API，例如/_cluster/state/version</p>
</td>
</tr>
<tr>
<td>watcher_admin</td>
<td>读写.watches索引</td>
</tr>
<tr>
<td>watcher_user</td>
<td>只读.watches索引</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">kibana</span></div>
<p>启用身份验证后，如果没有修改内置用户密码，则kibana登陆界面会提示：Login is currently disabled. Administrators should consult the Kibana logs for more details.</p>
<p>你需要更改kibana的配置项elasticsearch.password，如果在Docker中运行Kibana，也可以修改环境变量ELASTICSEARCH_PASSWORD。然后使用setup-passwords命令更改内置用户密码。</p>
<div class="blog_h1"><span class="graybg">最佳实践</span></div>
<div class="blog_h2"><span class="graybg">节点角色分离</span></div>
<p>建议把节点分为三类：</p>
<ol>
<li>Master节点，仅仅负责集群管理，不存储数据，不提供HTTP API</li>
<li>Client节点，和客户端通信，不存储数据，提供HTTP API</li>
<li>Data节点，仅仅负责存储、索引数据，不提供HTTP API</li>
</ol>
<div class="blog_h2"><span class="graybg">性能优化</span></div>
<div class="blog_h3"><span class="graybg">内存分配</span></div>
<p>对于超过32GB的堆，无法使用压缩普通对象指针（Compressed Ordinary Object Pointer），指针大小变为64bit。<span style="background-color: #c0c0c0;">这导致50GB的堆，能存储的对象数量和30GB的堆差不多</span>。</p>
<p>堆的最大、最小值应该设置为一致。</p>
<div class="blog_h3"><span class="graybg">注意磁盘空间</span></div>
<p>低水位：默认情况下，节点磁盘用量超过85%后，ES不会分发新的分片到该节点。即便如此，已有的分片仍然可能继续增大。</p>
<p>高水位：默认情况下，节点磁盘用量超过90%后，ES会停止写入，并且将该节点上的分片重现分配给磁盘空闲的其它节点。</p>
<p>副本：默认情况下使用1个副本，这意味着两倍的磁盘空间</p>
<p>分片：更大的分片，在存储上越高效。但是节点失败导致数据迁移的成本也越高。</p>
<div class="blog_h3"><span class="graybg">不要频繁落盘</span></div>
<p>索引是存储文档并让其可检索的过程。文档必须落盘，才能被搜索。</p>
<p>默认的落盘间隔是一秒，由参数refresh_interval指定。如果将此参数改为半分钟甚至更大，则能极大的增加ES的吞吐量：</p>
<pre class="crayon-plain-tag">PUT fluentd-2018.12.26/_settings
{
  "index" : {
    "refresh_interval" : "10s"
  }
}</pre>
<p><span style="background-color: rgb(192, 192, 192);">每次落盘，ES都会创建一个新的段（segment ）</span>。</p>
<div class="blog_h3"><span class="graybg">更多的分片</span></div>
<p>设置更多的分片，插入数据的并发度更高：</p>
<pre class="crayon-plain-tag">PUT /_template/logstash_template
{
  "index_patterns": ["fluentd*"],
  "settings": {
    "number_of_shards" : 12, 
    "number_of_replicas": 0,
    "refresh_interval" : "10s"
  }
}</pre>
<div class="blog_h3"><span class="graybg">字段数据缓存</span></div>
<p>字段数据（Field Data ）反转倒排索引。如果你需要知道某个字段包含哪些值，则ES需要反转字段的倒排索引，并产生字段数据。</p>
<p>字段数据存放在堆中，不加任何限制可能充满整个堆。参数indices.fielddata.cache.size用于控制字段数据的内存用量，可以指定百分比或者绝对值。</p>
<div class="blog_h3"><span class="graybg">索引缓冲区大小</span></div>
<p>如果写入量非常大，则需要保证内存中的索引缓冲足够大，对应的参数是indices.memory.index_buffer_size。可以设置高达512MB/分片。</p>
<div class="blog_h3"><span class="graybg">查询缓存</span></div>
<p>ES 6.x的查询缓存使用LRU算法清除，其内存用量通过indices.queries.cache.size配置。 </p>
<div class="blog_h3"><span class="graybg">使用批量请求</span></div>
<p><span style="background-color: rgb(192, 192, 192);">批量请求</span>比针对单个文档的请求性能要好很多。批次的最佳大小需要基准测试才能得出。</p>
<p>可以使用<span style="background-color: rgb(192, 192, 192);">多个客户端并发的进行批量请求</span>，单线程往往不能用尽ES的吞吐能力。</p>
<div class="blog_h3"><span class="graybg">禁用交换分区</span></div>
<p>交换分区会导致严重的性能下降。</p>
<div class="blog_h3"><span class="graybg">文件系统缓存</span></div>
<p>在进行IO操作时，需要使用文件系统缓存。你应该保证有<span style="background-color: rgb(192, 192, 192);">一半的ES节点内存</span>用于文件系统缓存。</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/elasticsearch-study-note">ElasticSearch学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/elasticsearch-study-note/feed</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>基于EFK构建日志分析系统</title>
		<link>https://blog.gmem.cc/efk-as-a-log-analysis-system</link>
		<comments>https://blog.gmem.cc/efk-as-a-log-analysis-system#comments</comments>
		<pubDate>Tue, 09 Jan 2018 16:10:18 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[BigData]]></category>
		<category><![CDATA[LOG]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=17536</guid>
		<description><![CDATA[<p>Elasticsearch 参考：ElasticSearch学习笔记 Fluentd Fluentd是一个C编写的开源的日志收集器，支持100+不同系统的日志收集处理。 source 定义Fluentd的输入，需要指定一个输入插件。例如： [crayon-69d36b60555eb379934826/] 定义了一个HTTP输入。Fluentd会在8888端口上监听，等待外部传入事件。事件的例子： [crayon-69d36b60555ef679459989/] source捕获到的Fluentd事件，交由Fluentd路由引擎处理。 filter 多个filter可以构成事件处理流水线。使用filter你可以将不需要的事件过滤掉，不再继续下一步处理。例如： [crayon-69d36b60555f2808413704/] 根据正则式匹配输入事件的action字段，如果匹配，路由给match处理，否则丢弃。 match 定义Fluentd的输出，并将匹配的事件传递给目标。例如： [crayon-69d36b60555f5728974042/] 会匹配具有Tag：test.cycle的输入事件，并传递给stdout这个输出插件。 label 用于定义一个可以被跳转到的路由片段，打破默认的从上到下的路由搜索顺序。该指令内部可以包含filter、match指令。示例： [crayon-69d36b60555f7905916255/] parse <a class="read-more" href="https://blog.gmem.cc/efk-as-a-log-analysis-system">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/efk-as-a-log-analysis-system">基于EFK构建日志分析系统</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">Elasticsearch</span></div>
<p>参考：<a href="https://blog.gmem.cc/elasticsearch-study-note">ElasticSearch学习笔记</a></p>
<div class="blog_h1"><span class="graybg">Fluentd</span></div>
<p>Fluentd是一个C编写的开源的日志收集器，支持100+不同系统的日志收集处理。</p>
<div class="blog_h2"><span class="graybg">source</span></div>
<p>定义Fluentd的输入，需要指定一个输入插件。例如：</p>
<pre class="crayon-plain-tag">&lt;source&gt;
  # 使用什么插件
  @type http
  # 你可以这样推送日志：http://localhost:8888/tag.name?json={...}
  port 8888
  bind 0.0.0.0
&lt;/source&gt;</pre>
<p>定义了一个HTTP输入。Fluentd会在8888端口上监听，等待外部传入事件。事件的例子：</p>
<pre class="crayon-plain-tag">curl -i -X POST -d 'json={"action":"login","user":2}' http://localhost:8888/test.cycle</pre>
<p>source捕获到的Fluentd事件，交由Fluentd路由引擎处理。</p>
<div class="blog_h2"><span class="graybg">filter</span></div>
<p><span style="background-color: #c0c0c0;">多个filter可以构成事件处理流水线</span>。使用filter你可以将不需要的事件过滤掉，不再继续下一步处理。例如：</p>
<pre class="crayon-plain-tag">&lt;filter test.cycle&gt;
  @type grep
  &lt;exclude&gt;
    key action
    pattern ^logout$
  &lt;/exclude&gt;
&lt;/filter&gt;</pre>
<p>根据正则式匹配输入事件的action字段，如果匹配，路由给match处理，否则丢弃。</p>
<div class="blog_h2"><span class="graybg">match</span></div>
<p>定义Fluentd的输出，并将匹配的事件传递给目标。例如：</p>
<pre class="crayon-plain-tag">&lt;match test.cycle&gt;
  @type stdout
&lt;/match&gt;</pre>
<p>会匹配具有Tag：test.cycle的输入事件，并传递给stdout这个输出插件。</p>
<div class="blog_h2"><span class="graybg">label</span></div>
<p>用于定义一个可以被跳转到的路由片段，打破默认的从上到下的路由搜索顺序。该指令<span style="background-color: #c0c0c0;">内部可以包含filter、match指令</span>。示例：</p>
<pre class="crayon-plain-tag">&lt;source&gt;
  @type http
  bind 0.0.0.0
  port 8888
  # 指定路由标签
  @label @STAGING
&lt;/source&gt;

&lt;filter test.cycle&gt;
&lt;/filter&gt;

# http源直接跳转到这里，不使用上面的filter
&lt;label @STAGING&gt;
  &lt;filter test.cycle&gt;
    @type grep
    &lt;exclude&gt;
      key action
      pattern ^logout$
    &lt;/exclude&gt;
  &lt;/filter&gt;

  &lt;match test.cycle&gt;
    @type stdout
  &lt;/match&gt;
&lt;/label&gt;</pre>
<div class="blog_h2"><span class="graybg">parse</span></div>
<p>可以位于source、match、filter指令的内部。对于那些支持的插件，用于解析原始数据。示例：</p>
<pre class="crayon-plain-tag">&lt;source&gt;
  @type tail
  # 输入插件的参数
  &lt;parse&gt;
    # 解析插件的类型
    @type apache2
    # 解析插件的参数
  &lt;/parse&gt;
&lt;/source&gt;</pre>
<div class="blog_h3"><span class="graybg">通用参数</span></div>
<p>每个Parser都可以覆盖这些参数的值：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 170px; text-align: center;">参数</td>
<td style="width: 60px; text-align: center;">类型</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>types</td>
<td>hash</td>
<td>
<p>指定如何将各字段转换为其它类型：field1:type,field2:type...</p>
<p>支持的类型：string、bool、integer、float、time、array</p>
</td>
</tr>
<tr>
<td>time_key</td>
<td>string</td>
<td>事件的发生事件从什么字段中获取，如果该字段不存在，则取值当前时间</td>
</tr>
<tr>
<td>null_value_pattern</td>
<td>string</td>
<td>空值的Pattern</td>
</tr>
<tr>
<td>null_empty_string</td>
<td>bool</td>
<td>是否将空串替换为nil，默认false</td>
</tr>
<tr>
<td>estimate_current_event</td>
<td>bool</td>
<td>是否以当前时间作为time_key的值，默认false</td>
</tr>
<tr>
<td>keep_time_key</td>
<td>bool</td>
<td>是否保留事件中的时间字段</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: 170px; text-align: center;">参数</td>
<td style="width: 50px; text-align: center;">类型</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>time_type</td>
<td>enum</td>
<td>
<p>可选值：</p>
<p style="padding-left: 30px;">float：UNIX时间.纳秒<br />unixtime： UNIX时间（秒）<br />string：根据后面几个参数决定具体格式</p>
</td>
</tr>
<tr>
<td>time_format</td>
<td>string</td>
<td>
<p>参考Ruby API：<a href="https://docs.ruby-lang.org/en/2.4.0/Time.html#method-i-strftime">时间格式化</a>、<a href="https://docs.ruby-lang.org/en/2.4.0/Time.html#method-c-strptime">时间解析</a></p>
<p>除了遵循Ruby的时间格式化，还可以取值%iso8601</p>
</td>
</tr>
<tr>
<td>localtime</td>
<td>bool</td>
<td>是否使用本地时间而非UTC，默认true</td>
</tr>
<tr>
<td>utc</td>
<td>bool</td>
<td>是否使用UTC而非本地时间，默认false</td>
</tr>
<tr>
<td>timezone</td>
<td>string</td>
<td>指定时区，例如+09:00、+0900、+09、Asia/Tokyo</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">buffer</span></div>
<p>可以位于match指令的内部，指定如何对事件进行缓冲（避免对输出的目的地造成压力）。Fluentd内置了两种缓冲插件：<a href="https://docs.fluentd.org/v1.0/articles/buf_file">memory</a>、<a href="https://docs.fluentd.org/v1.0/articles/buf_memory">file</a>。</p>
<p>使用buffer指令时，你也需要通过@type来指定插件类型。如果省略@type，则使用输出插件（match）指定的默认插件，或者使用memory。</p>
<div class="blog_h3"><span class="graybg">分块键</span></div>
<p>你可以为buffer指定分块键：</p>
<pre class="crayon-plain-tag"># 为空，或者逗号分隔的字符串
&lt;buffer ARGUMENT_CHUNK_KEYS&gt;
&lt;/buffer&gt;</pre>
<p>分块键决定了事件被收集到哪个缓冲块：</p>
<ol>
<li>如果不指定分块键（并且输出插件也没有指定默认分块键），则输出插件将所有的事件都写到单个块中，直到此块充满</li>
<li>如果分块键被设置为“tag”，则不同标签（Tag）的事件被收集到不同的缓冲块</li>
<li>如果分块键被设置为“time”，且指定了timekey参数，则每个Time Key对应一个缓冲块：<br />
<pre class="crayon-plain-tag">&lt;buffer time&gt;
  # 如果不指定单位，默认为秒
  timekey      1h # 每小时一个块
  timekey_wait 5m # 延迟5分钟刷出缓冲
&lt;/buffer&gt;</pre>
</li>
<li>如果分块键被设置为其它值，则认为是<span style="background-color: #c0c0c0;">事件记录的字段名</span></li>
<li>使用事件记录的嵌套字段也支持：<pre class="crayon-plain-tag">&lt;buffer $.nest.field&gt; # 访问记录的nest.field字段</pre></li>
<li>联用多个分块键也支持：<pre class="crayon-plain-tag">&lt;buffer tag,time,$.nest.field&gt;</pre></li>
</ol>
<div class="blog_h3"><span class="graybg">占位符</span></div>
<p>某些输出插件，可以使用分块键作为变量：</p>
<pre class="crayon-plain-tag">&lt;match log.*&gt;
  @type file
  path  /data/${tag}/access.${key1}.${$.nest.field}.log # 输出文件名使用变量，不同块输出到不同文件
  &lt;buffer tag,key1,$.nest.field&gt;
  &lt;/buffer&gt;
&lt;/match&gt;</pre>
<div class="blog_h3"><span class="graybg">缓冲参数</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 170px; text-align: center;">参数</td>
<td style="width: 60px; text-align: center;">类型</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>chunk_limit_size</td>
<td>size</td>
<td>缓冲块的最大尺寸，默认值：内存缓冲8MB，文件缓冲256MB</td>
</tr>
<tr>
<td>chunk_limit_records</td>
<td>integer</td>
<td>限制单个块最多包含的记录数</td>
</tr>
<tr>
<td>total_limit_size</td>
<td>size</td>
<td>此缓冲插件实例的总限制。默认值：内存缓冲512MB，文件缓冲64GB</td>
</tr>
<tr>
<td>chunk_full_threshold</td>
<td>float</td>
<td>刷空缓冲块的阈值，默认0.95，也就是缓冲块占用超过95%刷出</td>
</tr>
<tr>
<td>compress</td>
<td>enum</td>
<td>取值text、gzip，缓冲块的压缩算法。默认text表示不压缩</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: 170px; text-align: center;">参数</td>
<td style="width: 60px; text-align: center;">类型</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>flush_at_shutdown</td>
<td>bool</td>
<td>关闭前是否刷空</td>
</tr>
<tr>
<td>flush_mode</td>
<td>enum</td>
<td>
<p>刷空模式：</p>
<p style="padding-left: 30px;">interval 以flush_interval为周期刷空<br />lazy 每个timekey刷空一次<br />immediate 事件进入缓冲块后立即刷空</p>
</td>
</tr>
<tr>
<td>flush_interval</td>
<td>time</td>
<td>默认60s</td>
</tr>
<tr>
<td>flush_thread_count</td>
<td>integer</td>
<td>输出插件的线程数量，默认1，增大可以并行刷出缓冲块</td>
</tr>
<tr>
<td>flush_thread_interval</td>
<td>float</td>
<td>如果没有缓冲块等待被刷出，则本次刷空后，线程休眠几秒以进行下一次尝试</td>
</tr>
<tr>
<td>flush_thread_burst_interval</td>
<td>float</td>
<td>如果有缓冲块排队等待被刷出时的休眠间隔</td>
</tr>
<tr>
<td>delayed_commit_timeout</td>
<td>time</td>
<td>输出插件认定异步写操作失败的超时，默认60s</td>
</tr>
<tr>
<td>overflow_action</td>
<td>enum</td>
<td>
<p>当缓冲队列满了，输出插件的行为：</p>
<p style="padding-left: 30px;">throw_exception 抛出异常，打印错误<br />block 阻塞输入插件，禁止它释放新事件<br />drop_oldest_chunk 丢弃最旧的缓冲块</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: 170px; text-align: center;">参数</td>
<td style="width: 60px; text-align: center;">类型</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>retry_timeout</td>
<td>time</td>
<td>重试超时，默认72h</td>
</tr>
<tr>
<td>retry_forever</td>
<td>bool</td>
<td>是否永远重试，默认false</td>
</tr>
<tr>
<td>retry_max_times</td>
<td>integer</td>
<td>最大重试刷空的次数</td>
</tr>
<tr>
<td>retry_type</td>
<td>enum</td>
<td>
<p>重试方式：</p>
<p style="padding-left: 30px;">exponential_backoff 频率指数降低<br />periodic 频率恒定</p>
<p>对于指数方式，底数由参数retry_exponential_backoff_base确定，默认2</p>
<p>对于指数方式，最大重试间隔由retry_max_interval确定</p>
</td>
</tr>
<tr>
<td>retry_wait</td>
<td>time</td>
<td>下一次重试的等待间隔，默认1s</td>
</tr>
<tr>
<td>retry_randomize</td>
<td>bool</td>
<td>是否随机化重试间隔，默认true。可以防止高并发</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">format</span></div>
<p>部分插件支持在内部包含format指令，用来指定如何对日志记录进行格式化。match、filter指令内部可以包含format指令：</p>
<pre class="crayon-plain-tag">&lt;match tag.*&gt;
  @type file
  &lt;format&gt;
    @type json
  &lt;/format&gt;
&lt;/match&gt;</pre>
<p>内置的插件包括：out_file、json、ltsv、csv、msgpack、hash、single_value。下面的配置，将事件的log字段存储到文件：</p>
<pre class="crayon-plain-tag">&lt;match **&gt;
  @type file
  path  /var/log/kubernetes
  &lt;format&gt;
    @type single_value
    message_key log
  &lt;/format&gt;
&lt;/match&gt;</pre>
<div class="blog_h2"><span class="graybg">inject</span></div>
<p>可以位于match、filter指令内部，向事件记录注入额外的字段。</p>
<div class="blog_h2"><span class="graybg">extract</span></div>
<p>可以位于source、match、filter指令内部，从事件记录中抽取值。</p>
<div class="blog_h2"><span class="graybg">storage</span></div>
<p>部分插件支持此指令，用于指定如何存储插件的内部状态。可以位于source、match、filter指令内部。</p>
<div class="blog_h2"><span class="graybg">transport</span></div>
<p>使用server插件助手的source、match、filter插件，支持在内部配置该指令。用于说明如何处理网络连接。</p>
<div class="blog_h2"><span class="graybg">@include </span></div>
<p>该指令用于包含其它配置文件</p>
<div class="blog_h2"><span class="graybg">system</span></div>
<p>该指令用于进行系统级的配置，包括配置项：</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>log_level</td>
<td>日志级别，可以取值debug、info、error、fatal</td>
</tr>
<tr>
<td>suppress_repeated_stacktrace</td>
<td> </td>
</tr>
<tr>
<td>emit_error_log_interval</td>
<td> </td>
</tr>
<tr>
<td>suppress_config_dump</td>
<td> </td>
</tr>
<tr>
<td>without_source</td>
<td> </td>
</tr>
<tr>
<td>process_name</td>
<td>配置fluentd的supervisor和worker进程的名称</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">事件结构</span></div>
<p>每个Fluentd事件包含以下部分：</p>
<ol>
<li>Tag：标签，用于说明事件的“来源”，用于事件路由。<span style="background-color: #c0c0c0;">标签是点号（.）分隔的多个字符串</span></li>
<li>Time：事件发生的时间，必须是UNIX time格式</li>
<li>Record：实际的日志内容，JSON对象形式</li>
</ol>
<div class="blog_h2"><span class="graybg">标签匹配</span></div>
<p>标签（Tag）是日志事件的一种属性。filter、match指令可以指定一个匹配Pattern，来声明它负责处理哪些事件：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">Pattern</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>app.tag</td>
<td>精确匹配</td>
</tr>
<tr>
<td>app.*</td>
<td>匹配app.tag1、app.tag2，但是不匹配app.tag1.xx</td>
</tr>
<tr>
<td>app.**</td>
<td>匹配任何以app开头的标签</td>
</tr>
<tr>
<td>app.{x,y}.*</td>
<td>匹配app.x.*以及app.y.*，其中x、y可以是Pattern，例如app.{x,y.**}</td>
</tr>
<tr>
<td>app.tag app.tag</td>
<td>或</td>
</tr>
</tbody>
</table>
<p>Fluentd<span style="background-color: #c0c0c0;">根据配置文件中声明的顺序，自上而下的尝试匹配</span>，一旦找到匹配日志事件的filter、match就不再继续。</p>
<div class="blog_h2"><span class="graybg">配置文件</span></div>
<p>如果通过td-agent包安装，则配置文件位置为/etc/td-agent/td-agent.conf。</p>
<p>如果通过Ruby Gem安装，则配置文件位置为/etc/fluent/fluent.conf。</p>
<p>要修改配置文件的位置，使用环境变量FLUENT_CONF，或者命令行选项 -c</p>
<div class="blog_h2"><span class="graybg">配置参数</span></div>
<p>任何一个Fluentd插件都暴露若干可配置参数。</p>
<div class="blog_h3"><span class="graybg">参数类型</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 80px; text-align: center;">类型</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>string</td>
<td>字符串</td>
</tr>
<tr>
<td>integer</td>
<td>整数</td>
</tr>
<tr>
<td>float</td>
<td>浮点数</td>
</tr>
<tr>
<td>size</td>
<td>字节数量</td>
</tr>
<tr>
<td>time</td>
<td>时间长度（Duration）</td>
</tr>
<tr>
<td>array</td>
<td>JSON数组，可以<pre class="crayon-plain-tag">["key1", "key2"]</pre>形式，或者<pre class="crayon-plain-tag">key1,key2</pre>形式</td>
</tr>
<tr>
<td>hash</td>
<td>JSON对象，可以<pre class="crayon-plain-tag">{"key1":"value1", "key2":"value2"}</pre>或者<pre class="crayon-plain-tag">key1:value1,key2:value2</pre></td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">通用参数</span></div>
<p>Fluentd定义了一系列以@开头的参数：</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>@type</td>
<td>插件类型</td>
</tr>
<tr>
<td>@id</td>
<td>插件ID</td>
</tr>
<tr>
<td>@label</td>
<td>指定路由标签</td>
</tr>
<tr>
<td>@log_level</td>
<td>插件的日志级别</td>
</tr>
</tbody>
</table>
<p>type, id 和 log_level是对应上面几个参数，向后兼容用。</p>
<div class="blog_h3"><span class="graybg">内嵌Ruby代码</span></div>
<p>你可以在字符串中包含<pre class="crayon-plain-tag">#{}</pre>标记，其中可以包含合法的Ruby表达式，示例：</p>
<pre class="crayon-plain-tag">host_param "#{Socket.gethostname}"
env_param "foo-#{ENV["FOO_BAR"]}"</pre>
<div class="blog_h2"><span class="graybg">输入插件</span></div>
<div class="blog_h3"><span class="graybg">tail</span></div>
<p>这是一个内置插件，不需要额外的安装步骤。该插件从目标配置文件的尾部开始读取新产生的日志。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 160px; text-align: center;">参数</td>
<td style="width: 80px; text-align: center;">类型</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>tag</td>
<td>string</td>
<td>支持使用通配符 * ，该符号会展开为日志文件的实际路径</td>
</tr>
<tr>
<td>path</td>
<td>string</td>
<td>
<p>需要读取的日志的路径，可以指定多个路径，逗号分隔</p>
<p>通配符*和strftime格式占位符可以使用，用以动态的添加/移除日志文件：</p>
<pre class="crayon-plain-tag"># 仅仅读取default命名空间的日志
path /var/log/containers/*_default_*.log
# 日期
path /path/to/%Y/%m/%d/*</pre>
</td>
</tr>
<tr>
<td>exclude_path</td>
<td>array</td>
<td>
<p>需要排除掉的日志路径，示例：
<pre class="crayon-plain-tag">exclude_path ["/path/to/*.gz", "/path/to/*.zip"]</pre>
</td>
</tr>
<tr>
<td>read_from_head</td>
<td>bool</td>
<td>从文件的头部开始读取日志，而非尾部</td>
</tr>
<tr>
<td>&lt;parse&gt;</td>
<td>directive</td>
<td>你必须为tail配置parse指令，说明如何解析日志内容</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">systemd</span></div>
<p>读取并解析Systemd日志。示例：
<pre class="crayon-plain-tag">&lt;source&gt;
  @type systemd
  @id in_systemd_kubelet
  # 读取Kubelet.service的0-5级别的日志
  matches [{ "_SYSTEMD_UNIT": "kubelet.service", "PRIORITY": [0,1,2,3,4,5] }]
  &lt;storage&gt;
    @type local
    persistent true
    path /var/log/fluentd-journald-kubelet-cursor.json
  &lt;/storage&gt;
  &lt;entry&gt;
    fields_strip_underscores true
  &lt;/entry&gt;
  read_from_head false
  tag kubelet
&lt;/source&gt;</pre>
<div class="blog_h2"><span class="graybg">过滤插件</span></div>
<div class="blog_h3"><span class="graybg">record_transformer</span></div>
<p>支持以多种方式来修改事件。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 160px; text-align: center;">参数</td>
<td style="width: 80px; text-align: center;">类型</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>&lt;record&gt;</td>
<td>directive</td>
<td>
<p>在该指令中，定义需要新增加的字段。配置示例：</p>
<pre class="crayon-plain-tag">&lt;filter kubernetes.**&gt;
  @type record_transformer
  enable_ruby
  &lt;record&gt;
    # 添加主机名
    hostname "#{Socket.gethostname}"
    # 将事件的标签存储为记录字段
    tag ${tag}
    # 读取事件标签的第2部分
    service_name ${tag_parts[1]}
    # 读取记录的字段并进行运算
    avg ${record["total"] / record["count"]}
    # 一个名为message的字段，使用$进行字符串插值
    message yay, ${record["message"]}
  &lt;/record&gt;
&lt;/filter&gt;</pre>
<p>支持以下方式来访问标签：</p>
<p style="padding-left: 30px;">tag_parts[N] 标签的第N段<br />tag_prefix[N] 标签的0-N段<br />tag_suffix[N] 标签的N+段</p>
</td>
</tr>
<tr>
<td>enable_ruby</td>
<td>bool</td>
<td>
<p>默认false。如果为true，则可以在${}中包含Ruby代码，代码可以使用变量：</p>
<p style="padding-left: 30px;">record 当前事件记录<br />time 当前事件的时间对象</p>
<p>配置示例：</p>
<pre class="crayon-plain-tag">&lt;filter kubernetes.**&gt;
  @type record_transformer
  enable_ruby
  remove_keys [ "log" ]
  &lt;record&gt;
    # 从%-5p %d{yyyy-MM-dd HH:mm:ss.SSS} ::: [%15.15t] %-48.48c{36} ::: %m%n%ex
    # 形式的Logback Pattern中抽取字段
    level ${record["log"][0,5].strip}
    timestamp ${record["log"][6,23]}
    thread ${record["log"][35,15].strip}
    class  ${record["log"][52,49].strip}
    message ${record["log"][105..-1].strip}
  &lt;/record&gt;
&lt;/filter&gt;</pre>
<p>代码示例：</p>
<pre class="crayon-plain-tag"># 将记录转换为JSON
${record.to_json}
# 格式化时间
${time.strftime('%Y-%m-%dT%H:%M:%S%z')}
# 取标签的最后一段
${tag_parts.last}
# 访问嵌套字段
${record["payload"]["key"]}</pre>
</td>
</tr>
<tr>
<td>auto_typecast</td>
<td>bool</td>
<td>默认false。是否自动进行类型转换</td>
</tr>
<tr>
<td>renew_record</td>
<td>bool</td>
<td>默认false。如果true则在空的新哈希上进行操作，而非修改incoming的记录</td>
</tr>
<tr>
<td>renew_time_key</td>
<td>string</td>
<td>使用指定的字段来修改事件的时间，目标字段必须是UNIX time</td>
</tr>
<tr>
<td>keep_keys</td>
<td>array</td>
<td>仅当renew_record=true时有意义。列出记录中需要保留的键</td>
</tr>
<tr>
<td>remove_keys</td>
<td>array</td>
<td>列出需要删除的键</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">grep</span></div>
<p>根据事件的字段进行过滤，不匹配的记录被丢弃。
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 160px; text-align: center;">参数</td>
<td style="width: 80px; text-align: center;">类型</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>&lt;and&gt;</td>
<td>directive</td>
<td>
<p>内部指定几个其它指令，进行与操作：</p>
<pre class="crayon-plain-tag">&lt;and&gt;
  &lt;exclude&gt;
  &lt;/exclude&gt;
  &lt;exclude&gt;
  &lt;/exclude&gt;
&lt;/and&gt;</pre>
</td>
</tr>
<tr>
<td>&lt;or&gt;</td>
<td>directive</td>
<td>内部指定几个其它指令，进行或操作</td>
</tr>
<tr>
<td>&lt;regexp&gt;</td>
<td>directive</td>
<td>
<p>指定基于正则式的匹配规则，不匹配的事件会被排除：
<pre class="crayon-plain-tag">&lt;regexp&gt;
  # 检查的字段
  key price
  # 匹配的正则式
  pattern /[1-9]\d*/
&lt;/regexp&gt;</pre>
</td>
</tr>
<tr>
<td>&lt;exclude&gt;</td>
<td>directive</td>
<td>类似上面，但是匹配的事件会被排除</td>
</tr>
</tbody>
</table>
<p>示例：
<pre class="crayon-plain-tag"># 针对所有以calico-node开头的日志
&lt;filter kubernetes.var.log.containers.calico-node-*.log&gt;
  @type grep
  @id filter_grep_container_calico_node
  &lt;regexp&gt;
    # 针对日志记录的log字段
    key log
    # 仅仅保留警告、错误日志
    pattern ^.{25}(W|E)
  &lt;/regexp&gt;
&lt;/filter&gt;

# 仅仅保留具有标签tier=application的Pod产生的日志
&lt;filter kubernetes.**&gt;
  @type grep
  @id filter_grep_kubernetes
  &lt;regexp&gt;
    key $.kubernetes.labels.tier
    pattern ^application$
  &lt;/regexp&gt;
&lt;/filter&gt;</pre>
<div class="blog_h3"><span class="graybg">parser</span></div>
<p>解析日志的字符串字段，并<span style="background-color: #c0c0c0;">把事件记录替换为解析结果</span>：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 200px; text-align: center;">参数</td>
<td style="width: 80px; text-align: center;">类型</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>&lt;parse&gt;</td>
<td>directive</td>
<td>指定解析器及其参数</td>
</tr>
<tr>
<td>key_name</td>
<td>string</td>
<td>需要被解析的记录字段名</td>
</tr>
<tr>
<td>reserve_time</td>
<td>bool</td>
<td>是否在新记录中保留原始事件的时间字段</td>
</tr>
<tr>
<td>reserve_data</td>
<td>bool</td>
<td>是否在新记录中保留原始时间的所有字段</td>
</tr>
<tr>
<td>remove_key_name_field</td>
<td>bool</td>
<td>如果解析成功，是否删除key_name指定的原始事件字段，1.2.2引入</td>
</tr>
<tr>
<td>inject_key_prefix</td>
<td>string</td>
<td>解析结果字段，统一增加的前缀</td>
</tr>
<tr>
<td>hash_value_field</td>
<td>string</td>
<td>解析结果字段，以哈希（对象）形式保存为该参数指定字段的值</td>
</tr>
<tr>
<td>emit_invalid_record_to_error</td>
<td>bool</td>
<td>是否将无法解析的记录发射给@ERROR标签</td>
</tr>
</tbody>
</table>
<p>示例，解析ElasticSearch的JSON格式的日志，并把Wrapping Docker日志替换掉：</p>
<pre class="crayon-plain-tag">&lt;filter kubernetes.var.log.containers.es-master-*.log&gt;
  @type parser
  @id filter_parser_containers_es_master
  key_name log
  reserve_time true
  reserve_data true
  remove_key_name_field true
  &lt;parse&gt;
    @type json
    time_format %Y-%m-%dT%H:%M:%S.%NZ
  &lt;/parse&gt;
&lt;/filter&gt;</pre>
<div class="blog_h3"><span class="graybg">concat</span></div>
<p>该插件非内置，执行<pre class="crayon-plain-tag">gem install fluent-plugin-concat</pre>安装。</p>
<p>用于将多个日志事件合并为一个。具有工作三种模式：</p>
<ol>
<li>n_lines 将连续的N个事件合并为一个</li>
<li>multiline_start_regexp ... 根据正则式匹配来确定该事件是否作为合并后的第一个、中间事件、最后一个</li>
<li>partial_key 取源事件中的某个字段，如果该字段的值为partial_value指定的值，则认为它应该合并到前面的事件</li>
</ol>
<p>注意： 如果超时后仍然没有接收到被合并序例的的最后一个事件，则整个序列会被丢弃。</p>
<p>下面的示例用于处理被Docker的日志驱动按行收集的Java Logback日志信息，它会将日志中的异常栈合并到一起，然后与它们之前的（紧靠着的）那个事件合并：</p>
<pre class="crayon-plain-tag">&lt;filter kubernetes.**&gt;
  @type concat
  @log_level trace
  key log
  multiline_start_regexp /^(TRACE|DEBUG|INFO|WARN|ERROR|FATAL)/
  timeout_label @ES
&lt;/filter&gt;

&lt;match kubernetes.**&gt;
  @type relabel
  @label @ES
&lt;/match&gt;

&lt;label @ES&gt;
...
&lt;/label&gt;</pre>
<div class="blog_h2"><span class="graybg">解析插件</span></div>
<div class="blog_h3"><span class="graybg">regexp</span></div>
<p>根据正则式来解析日志。指定的正则式至少需要指定一个<a href="/regex#named-capture">命名捕获</a>，命名捕获会作为记录的字段，名字为time的命名捕获，会作为事件的发生时间。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 200px; text-align: center;">参数</td>
<td style="width: 80px; text-align: center;">类型</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>time_key</td>
<td>string</td>
<td>事件的发生时间字段，默认time</td>
</tr>
<tr>
<td>time_format</td>
<td>string</td>
<td>时间的格式</td>
</tr>
<tr>
<td>keep_time_key</td>
<td>string</td>
<td>是否在记录中保留时间字段，默认false</td>
</tr>
<tr>
<td>expression</td>
<td>regexp</td>
<td>
<p>解析日志的正则式，需要指定至少一个命名捕获</p>
<p>下面的例子解析Containerd默认日志：</p>
<pre class="crayon-plain-tag">expression /^(?&lt;time&gt;.{30}) (?&lt;stream&gt;\w+) . (?&lt;log&gt;.*)$/ </pre>
</td>
</tr>
<tr>
<td>types</td>
<td>string</td>
<td>
<p>指定解析出的各字段的类型，如果不指定所有字段为string类型。格式：
<pre class="crayon-plain-tag">types &lt;field_name_1&gt;:&lt;type_name_1&gt;,&lt;field_name_2&gt;:&lt;type_name_2&gt;
# 示例
types user_id:integer,paid:bool,paid_usd_amount:float</pre>
<p>支持的类型：string、bool、integer、float、time、array</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">multiline</span></div>
<p>regexp的多行版本，支持将多行日志合并为一个事件，特别适用于解析异常栈。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 200px; text-align: center;">参数</td>
<td style="width: 70px; text-align: center;">类型</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>time_key</td>
<td>string</td>
<td>事件的发生时间字段，默认time</td>
</tr>
<tr>
<td>time_format</td>
<td>string</td>
<td>时间的格式</td>
</tr>
<tr>
<td>format_firstline</td>
<td>string</td>
<td>匹配多行日志事件的第一行的正则式</td>
</tr>
<tr>
<td>formatN</td>
<td>string</td>
<td>N可以是1-20，指定完整的日志事件格式</td>
</tr>
<tr>
<td>keep_time_key</td>
<td>string</td>
<td>是否在记录中保留时间字段，默认false</td>
</tr>
</tbody>
</table>
<p>Java异常日志的例子：</p>
<pre class="crayon-plain-tag">&lt;parse&gt;
  @type multiline
  # 识别新记录的正则式
  format_firstline /\d{4}-\d{1,2}-\d{1,2}/
  # 解析完整记录的正则式
  format1 /^(?&lt;time&gt;\d{4}-\d{1,2}-\d{1,2} \d{1,2}:\d{1,2}:\d{1,2}) \[(?&lt;thread&gt;.*)\] (?&lt;level&gt;[^\s]+)(?&lt;message&gt;.*)/
&lt;/parse&gt;

# 第一个记录
2013-3-03 14:27:33 [main] INFO  Main - Start
# 第二个记录
2013-3-03 14:27:33 [main] ERROR Main - Exception
javax.management.RuntimeErrorException: null
    at Main.main(Main.java:16) ~[bin/:na]
# 第三个记录
2013-3-03 14:27:33 [main] INFO  Main - End</pre>
<p>Rails日志的例子：</p>
<pre class="crayon-plain-tag">&lt;parse&gt;
  @type multiline
  # 识别新记录的正则式
  format_firstline /^Started/
  # 分为多个参数，每个参数对应一行日志信息
  format1 /Started (&lt;method&gt;[^ ]+) "(&lt;path&gt;[^"]+)" for (&lt;host&gt;[^ ]+) at (&lt;time&gt;[^ ]+ [^ ]+ [^ ]+)\n/
  format2 /Processing by (&lt;controller&gt;[^\u0023]+)\u0023(&lt;controller_method&gt;[^ ]+) as (&lt;format&gt;[^ ]+)\n/
  format3 /(  Parameters: (&lt;parameters&gt;[^ ]+)\n)/
  format4 /  Rendered (&lt;template&gt;[^ ]+) within (&lt;layout&gt;.+) \([\d\.]+ms\)\n/
  format5 /Completed (&lt;code&gt;[^ ]+) [^ ]+ in (&lt;runtime&gt;[\d\.]+)ms \(Views: (&lt;view_runtime&gt;[\d\.]+)ms \| ActiveRecord: (&lt;ar_runtime&gt;[\d\.]+)ms\)/
&lt;/parse&gt;

# 第一个记录
Started GET "/users/123/" for 127.0.0.1 at 2013-06-14 12:00:11 +0900
Processing by UsersController#show as HTML
  Parameters: {"user_id"=&gt;"123"}
  Rendered users/show.html.erb within layouts/application (0.3ms)
Completed 200 OK in 4ms (Views: 3.2ms | ActiveRecord: 0.0ms)</pre>
<div class="blog_h2"><span class="graybg">输出插件</span></div>
<p>在Fluentd 1.0，允许三种输出插件的缓冲/刷出模式：</p>
<ol>
<li>无缓冲模式，直接写出到外部系统</li>
<li>同步缓冲模式，使用缓冲块（事件集），缓冲块排队等候刷出。行为由buffer段控制</li>
<li>异步缓冲模式，类似2，但是输出插件在后台异步的提交请求给外部系统</li>
</ol>
<p>每个插件可以支持全部3种模式，也可以仅仅支持一种模式。如果<span style="background-color: #c0c0c0;">对不支持缓冲的插件配置buffer段，fluentd会出错并终止</span>。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2018/01/fluentd-v0.14-plugin-api-overview.png"><img class="aligncenter  wp-image-23561" src="https://blog.gmem.cc/wp-content/uploads/2018/01/fluentd-v0.14-plugin-api-overview.png" alt="fluentd-v0-14-plugin-api-overview" width="912" height="472" /></a></p>
<div class="blog_h3"><span class="graybg">route</span></div>
<p>该插件非内置，执行<pre class="crayon-plain-tag">gem install fluent-plugin-route</pre>安装。</p>
<p><a href="https://github.com/tagomoris/fluent-plugin-route">fluent-plugin-route</a>支持修改标签，支持定义多个路由规则。可以实现事件<span style="background-color: #c0c0c0;">复制分发</span>：</p>
<pre class="crayon-plain-tag">&lt;match worker.**&gt;
  @type route
  # 修改标签前缀
  remove_tag_prefix worker
  add_tag_prefix metrics.event

  # 复制事件，走路由规则一
  &lt;route **&gt;
    copy # 不使用COPY会导致路由在此结束
  &lt;/route&gt;
  # 复制事件，走路由规则二
  &lt;route **&gt;
    copy
    @label @BACKUP
  &lt;/route&gt;
&lt;/match&gt;

# 路由规则一
&lt;match metrics.event.**&gt;
  @type stdout
&lt;/match&gt;

# 路由规则二
&lt;label @BACKUP&gt;
  &lt;match metrics.event.**&gt;
    @type file
    path /var/log/fluent/bakcup
  &lt;/match&gt;
&lt;/label&gt;</pre>
<div class="blog_h3"><span class="graybg">rewrite_tag_filter</span></div>
<p><a href="https://github.com/fluent/fluent-plugin-rewrite-tag-filter">fluent-plugin-rewrite-tag-filter</a>支持根据日志事件的内容来修改标签。示例：</p>
<pre class="crayon-plain-tag">&lt;match app.**&gt;
  @type rewrite_tag_filter
  # 根据内容的message字段，对齐进行正则式匹配，获取日志级别，捕获为$1，作为Tag的前缀
  rewriterule1 message ^\[(\w+)\] $1.${tag}
&lt;/match&gt;

# 错误消息路由到这里
&lt;match error.app.**&gt;
  @type mail
&lt;/match&gt;
# 其它消息路由到这里
&lt;match *.app.**&gt;
  @type file
&lt;/match&gt;</pre>
<div class="blog_h3"><span class="graybg">elasticsearch</span></div>
<p>将日志记录输出到ES中。插件参数：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 170px; text-align: center;">参数</td>
<td style="width: 70px; text-align: center;">类型</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>include_tag_key</td>
<td>bool</td>
<td>
<p>是否将事件的Fluentd Tag作为ES文档的字段存储</p>
<p>字段名默认为tag，可以用参数tag_key修改</p>
</td>
</tr>
<tr>
<td>logstash_format</td>
<td>bool</td>
<td>
<p>兼容Logstash格式的索引命名，<span style="background-color: #c0c0c0;">设置为true才能使用Kibana</span></p>
<p>如果设置为true自动忽视参数index_name，索引名称自动设置为：</p>
<p><pre class="crayon-plain-tag">#{logstash_prefix}-#{formated_date}</pre></p>
</td>
</tr>
<tr>
<td>time_key</td>
<td>string</td>
<td>默认情况下，@timestamp自动会自动设置为消费日志的时间，如果要修改此行为，通过该参数指定一个记录字段名</td>
</tr>
<tr>
<td>include_timestamp</td>
<td>bool</td>
<td>是否包含一个@timestamp字段到输出文档中</td>
</tr>
<tr>
<td>logstash_prefix</td>
<td>string</td>
<td>索引名前缀，默认logstash</td>
</tr>
<tr>
<td>logstash_dateformat</td>
<td>string</td>
<td>作为索引名后缀的日期格式</td>
</tr>
</tbody>
</table>
<p>示例：</p>
<pre class="crayon-plain-tag">&lt;match *.**&gt;
  @type elasticsearch
  host 10.0.10.2
  port 9200
  user logstash_system
  password logstash_system
  logstash_format true
  logstash_prefix openstack
  enable_ilm true
  index_date_pattern "now/m{yyyy.mm}"
  flush_interval 10s
&lt;/match&gt;</pre>
<div class="blog_h3"><span class="graybg">null</span></div>
<p>简单的丢弃事件，示例：</p>
<pre class="crayon-plain-tag">&lt;match fluent.**&gt;
    @type null
&lt;/match&gt;</pre>
<div class="blog_h3"><span class="graybg">kafka</span></div>
<p>将日志写入到Kafka主题中。</p>
<p>执行下面的命令安装此插件：</p>
<pre class="crayon-plain-tag">fluent-gem install fluent-plugin-kafka</pre>
<p>配置示例：</p>
<pre class="crayon-plain-tag">&lt;match pattern&gt;
  @type kafka_buffered

  # 种子代理列表
  brokers &lt;broker1_host&gt;:&lt;broker1_port&gt;,&lt;broker2_host&gt;:&lt;broker2_port&gt;

  # 缓冲设置
  buffer_type file
  buffer_path /var/log/td-agent/buffer/td
  flush_interval 3s

  # Kafka主题
  default_topic messages

  # 数据类型设置
  output_data_type json
  compression_codec gzip

  # 生产者配置
  max_send_retries 1
  required_acks -1

&lt;/match&gt;</pre>
<div class="blog_h3"><span class="graybg">relabel</span></div>
<p>此插件用于给事件重新打标签，例如：</p>
<pre class="crayon-plain-tag">&lt;match pattern&gt;
  @type relabel
  @label @foo
&lt;/match&gt;

&lt;label @foo&gt;
  &lt;match pattern&gt;
    ...
  &lt;/match&gt;
&lt;/label&gt;</pre>
<p>会给Tag匹配pattern的事件全部打上@foo标签，这些事件会全部交由名为@foo的label区段处理。</p>
<div class="blog_h3"><span class="graybg">file</span></div>
<p>将事件写入到文件。不是记录写入后立即就生成文件，只有<span style="color: #444444;">time_slice_format条件满足时文件才生成，默认情况下该插件每天生成一个文件。</span></p>
<p>插件参数：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 170px; text-align: center;">参数</td>
<td style="width: 70px; text-align: center;">类型</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>path</td>
<td>string</td>
<td>
<p>文件前缀，实际文件路径为<span style="background-color: #c0c0c0;">path + time + .log，</span>其中time取决于time_slice_format</p>
</td>
</tr>
<tr>
<td>append</td>
<td>bool</td>
<td>
<p>刷出的chunk是否覆盖到已经存在的文件。默认情况下每个chunk都输出到不同位置（即取值false）</p>
<p>不同取值对应的文件布局：</p>
<pre class="crayon-plain-tag"># append false
log.20140608_0.log
log.20140608_1.log
log.20140609_0.log
log.20140609_1.log

# append true
log.20140608.log
log.20140609.log </pre>
</td>
</tr>
<tr>
<td>format </td>
<td>string</td>
<td>输出文件格式 </td>
</tr>
<tr>
<td>time_format</td>
<td>string </td>
<td>日期写出格式 </td>
</tr>
<tr>
<td>compress</td>
<td>string</td>
<td>输出压缩算法，默认gzip</td>
</tr>
<tr>
<td>time_slice_format</td>
<td>string</td>
<td>
<p>用于文件名中time部分的、时间的格式化方式：
<p>%Y:  年度<br />%m: 月份 01-12<br />%d: 日期01-31<br />%H: 小时00-23<br />%M: 分钟00-59<br />%S: 秒00-60</p>
<p>默认取值 %Y%m%d%H ，也就是每小时一个文件</p>
</td>
</tr>
<tr>
<td>time_slice_wait</td>
<td>time</td>
<td>Fluentd等待迟到日志到达的最大时间，默认10m。用于处理事件到达fluentd节点有延迟的情况</td>
</tr>
<tr>
<td>flush_interval</td>
<td>time</td>
<td>刷出缓冲的间隔，默认60s</td>
</tr>
</tbody>
</table>
<p>一个实例：</p>
<pre class="crayon-plain-tag">&lt;label @K8S_OUT_FILE&gt;
  &lt;match **&gt;
    @type file
    # 目录 %Y-%m-%d.%H.${$.kubernetes.labels.application} 中会存放缓冲文件，每小时（3600）刷出
    # 文件名称示意 2018-11-14.17.account.log.gz，  log.gz自动添加，不需要在path中声明
    path  /var/log/kubernetes/%Y-%m-%d.%H.${$.kubernetes.labels.application}
    append true
    # 压缩文件可以这样浏览：gzip -dc 2018-11-14.17.account.log.gz  | grep ERROR
    compress gzip
    &lt;buffer time,$.kubernetes.labels.application&gt;
      timekey 3600
      timekey_wait 10
      timekey_zone +0800
    &lt;/buffer&gt;
    &lt;format&gt;
      @type single_value
      message_key log
    &lt;/format&gt;
  &lt;/match&gt;
&lt;/label&gt; </pre>
<div class="blog_h2"><span class="graybg">性能优化</span></div>
<div class="blog_h3"><span class="graybg">安装NTP</span></div>
<p>为了保证节点的时间戳精确，你需要安装NTP守护程序，例如chrony、ntpd。</p>
<div class="blog_h3"><span class="graybg">内核参数</span></div>
<p>文件描述符数量：</p>
<pre class="crayon-plain-tag">root soft nofile 65536
root hard nofile 65536
* soft nofile 65536
* hard nofile 65536</pre>
<p>对于高负载、多个Fluentd的环境，需要修改网络参数：</p>
<pre class="crayon-plain-tag">net.core.somaxconn = 1024
net.core.netdev_max_backlog = 5000
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216relabel
net.ipv4.tcp_wmem = 4096 12582912 16777216
net.ipv4.tcp_rmem = 4096 12582912 16777216
net.ipv4.tcp_max_syn_backlog = 8096
net.ipv4.tcp_slow_start_after_idle = 0
net.ipv4.tcp_tw_reuse = 1</pre>
<p>重启或者<pre class="crayon-plain-tag">sysctl -p</pre>生效。</p>
<div class="blog_h3"><span class="graybg">配置flush_thread_count</span></div>
<p>如果日志的目的地是远程设备、存储，可以使用该选项来并行化输出（默认1）。使用多线程可以缓和网络延迟的影响。</p>
<p>所有输出插件支持该参数。</p>
<div class="blog_h3"><span class="graybg">减少内存占用</span></div>
<p>Ruby提供了若干GC参数，你可以通过环境变量来设置它们。</p>
<p>要减少内存占用，可以设置<span style="color: #444444;">RUBY_GC_HEAP_OLDOBJECT_LIMIT_FACTOR为较小的值，此参数默认值2.0。资源受限环境下可以设置为1.2-</span></p>
<div class="blog_h3"><span class="graybg">多个Worker</span></div>
<p>处理10亿级别的日志输入时，CPU会出现瓶颈，这时考虑增加工作进程数量：</p>
<pre class="crayon-plain-tag">&lt;system&gt;
  workers 8
&lt;/system&gt;</pre>
<div class="blog_h1"><span class="graybg">Kibana</span></div>
<p>Kibana是一个开源的分析和可视化平台，必须配合ES一起使用。可以使用Kibana来检索、分析ES索引中的数据，日志分析是最常用的应用场景。</p>
<div class="blog_h2"><span class="graybg">日志查询</span></div>
<p>用于交互式的日志查询，使用Lucene查询语法：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 250px; text-align: center;">查询串示例</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>level:error</td>
<td>level字段包含单词error</td>
</tr>
<tr>
<td>level:(error OR warn)<br />level:(error warn)</td>
<td>
<p>level字段包含单词error或warn</p>
<p>操作符默认OR</p>
</td>
</tr>
<tr>
<td>message: "Connection Reset"</td>
<td>精确包含短语Connection Reset</td>
</tr>
<tr>
<td>user.\*:(alex)</td>
<td>user的任何字段包含alex</td>
</tr>
<tr>
<td>_exists_:title</td>
<td>title字段不为空 </td>
</tr>
<tr>
<td>level:e?r*</td>
<td>通配符：*匹配任意个数字符，?匹配单个字符 </td>
</tr>
<tr>
<td>name:/joh?n(ath[oa]n)/</td>
<td>支持正则式</td>
</tr>
<tr>
<td>quikc~1</td>
<td>模糊查询操作符~，可以匹配拼写错误的情况。1为距离，默认2，取值1可以捕获80%的拼写错误</td>
</tr>
<tr>
<td>age:&gt;10<br />age:&gt;=10<br />age:&lt;10<br />age:&lt;=10</td>
<td>比较操作符</td>
</tr>
<tr>
<td>(quick OR brown) AND fox</td>
<td>分组操作符</td>
</tr>
<tr>
<td>date:[2012-01-01 TO 2012-12-31]<br />count:[1 TO 5]</td>
<td>范围查询，闭区间</td>
</tr>
<tr>
<td>tag:{alpha TO omega}</td>
<td>范围查询，开区间（不包含首尾）</td>
</tr>
<tr>
<td>count:[10 TO *]</td>
<td>范围查询，无上限</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">可视化和仪表盘</span></div>
<p>Visualize用于设计一个小器件，例如饼图、曲线图。Dashboard则可以将小器件组合为仪表盘。</p>
<div class="blog_h3"><span class="graybg">可视化</span></div>
<p>这里以曲线图为例，点击曲线图的图标，首先要选择数据源，也就是索引。点选fluentd-*索引，看到如下界面：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2018/01/visualize-1.png"><img class="aligncenter size-full wp-image-24133" src="https://blog.gmem.cc/wp-content/uploads/2018/01/visualize-1.png" alt="visualize-1" width="955" height="766" /></a></p>
<p>&nbsp;</p>
<p>点击顶部的Add a filter，可以添加过滤条件。</p>
<p>Metrics区域用于定义统计指标（度量），支持多个度量。</p>
<p>Buckets区域用于指定如何分组展示，示例：</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>X轴显示时间，根据应用程序名称拆分Series</td>
<td>
<ol>
<li><span style="color: #333333; font-family: Ubuntu, sans-serif;"><span style="font-size: 13px; line-height: 22px;">点选X-Axis，聚合方式选择Date Histogram。可以设置X轴的标签</span></span></li>
<li>点击Add sub-bucket，选择Split Series</li>
<li>子聚合方式选择Terms，字段选择kubernetes.labels.app.keyword</li>
<li>点击做面板右上角的“播放”按钮，测试效果</li>
</ol>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">仪表盘</span></div>
<p>可以选取在“可视化“中定义的小器件，并拖拽以布局，效果示例：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2018/01/visualize-2.png"><img class="aligncenter  wp-image-24141" src="https://blog.gmem.cc/wp-content/uploads/2018/01/visualize-2.png" alt="visualize-2" width="1015" height="568" /></a></p>
<div class="blog_h2"><span class="graybg">集群监控</span></div>
<div class="blog_h3"><span class="graybg">总体状态</span></div>
<p>首页是总体状态信息，包括ES、Kibana的健康状态，已用/可用的各类硬件资源信息：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2018/01/es-monitoring-1.png"><img class="aligncenter size-full wp-image-24113" src="https://blog.gmem.cc/wp-content/uploads/2018/01/es-monitoring-1.png" alt="es-monitoring-1" width="930" height="775" /></a></p>
<p>如果Elasticsearch的健康状态为red，则说明集群存在问题。截图中的情况是主分片尚未分配到节点导致。</p>
<div class="blog_h3"><span class="graybg">ES状态概览</span></div>
<p>点击上图Elasticsearch区域的Overview连接，可以看到ES集群更多的信息：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2018/01/es-monitoring-2.png"><img class="aligncenter  wp-image-24115" src="https://blog.gmem.cc/wp-content/uploads/2018/01/es-monitoring-2.png" alt="es-monitoring-2" width="1018" height="601" /></a>本页面显示ES的读（检索）、写（索引）性能指标，包括QPS和延迟。</p>
<p>Shard Activity通常是空的，上图中的情况是正在应用事务日志到ES数据节点。Translog是一种写前日志，记录所有针对ES的写操作。</p>
<div class="blog_h3"><span class="graybg">索引状态概览</span></div>
<p>此页面显示索引的列表，如果有红色说明索引存在问题。下图中第一个索引有一个分片不正常。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2018/01/es-monitoring-3.png"><img class="aligncenter  wp-image-24117" src="https://blog.gmem.cc/wp-content/uploads/2018/01/es-monitoring-3.png" alt="es-monitoring-3" width="1026" height="399" /></a></p>
<p>每个索引包含的文档数量、占用的磁盘空间、读写速率也显示在页面上。</p>
<div class="blog_h3"><span class="graybg">索引状态详情</span></div>
<p>该页面显示单个索引文档数量、占用的磁盘空间、读写速率随时间的变化曲线，以及索引各分片的状态。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2018/01/es-monitoring-4.png"><img class="aligncenter  wp-image-24119" src="https://blog.gmem.cc/wp-content/uploads/2018/01/es-monitoring-4.png" alt="es-monitoring-4" width="1015" height="722" /></a></p>
<p>上图中，es-data-0分配了序号为4的分片，并且此分片正在初始化。其它分配均正常。</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: 25%; text-align: center;">参数</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>dateFormat</td>
<td>日期显示格式，例如MM-DD HH:mm:ss.SSS</td>
</tr>
<tr>
<td>truncate:maxHeight</td>
<td>检索时，每条日志占用的最大UI高度</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">集成K8S</span></div>
<p>捆绑了X-pack的ElasticSearch镜像：<a href="https://git.gmem.cc/alex/docker-elasticsearch">https://git.gmem.cc/alex/docker-elasticsearch</a></p>
<p>ElasticSearch+Kibana的Helm Chart：<a href="https://git.gmem.cc/alex/oss-charts/src/branch/master/elasticsearch">https://git.gmem.cc/alex/oss-charts/src/branch/master/elasticsearch</a></p>
<p>Fluentd的Helm Chart：<a href="https://git.gmem.cc/alex/oss-charts/src/branch/master/fluentd">https://git.gmem.cc/alex/oss-charts/src/branch/master/fluentd</a></p>
<div class="blog_h1"><span class="graybg">常见问题</span></div>
<div class="blog_h2"><span class="graybg">Fluentd</span></div>
<div class="blog_h3"><span class="graybg">极高IO</span></div>
<p>iotop看到fluentd进程有高达200M/s的读操作，但是定位不到针对的是什么文件</p>
<p>使用csysdig跟踪进程系统调用，发现大量内存映射操作，针对/var/log/journal/4f2be1039e944e028f2e86e02fe410e1目录，删除目录后问题消失。</p>
<div class="blog_h2"><span class="graybg">Kibana</span></div>
<div class="blog_h3"><span class="graybg">容器中节点CPU显示N/A</span></div>
<p>设置Kibana的环境变量XPACK_MONITORING_UI_CONTAINER_ELASTICSEARCH_ENABLED为false </p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/efk-as-a-log-analysis-system">基于EFK构建日志分析系统</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/efk-as-a-log-analysis-system/feed</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>Apache Curator学习笔记</title>
		<link>https://blog.gmem.cc/apache-curator-study-note</link>
		<comments>https://blog.gmem.cc/apache-curator-study-note#comments</comments>
		<pubDate>Thu, 10 Aug 2017 09:09:21 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[BigData]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[ZooKeeper]]></category>
		<category><![CDATA[学习笔记]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=15366</guid>
		<description><![CDATA[<p>简介 Apache Curator（音标[kjʊ(ə)'reɪtə]）Framework是ZooKeeper的Keeper（动物园管理员的管理员）。它是一个Java库，提供了比ZooKeeper更加高层的API，更加易用、可靠。Curator的推荐的ZooKeeper版本是 3.5+，但它也和3.4兼容。 Curator实现了自动的连接管理，当会话过期后，你需要重新创建ZooKeeper客户端，并重新设置Watcher。Curator可以透明的重新创建、重试连接。 子项目 Curator以组标识[crayon-69d36b6056212973418485-i/]发布在Maven中心仓库，包含以下构件： 构件 说明 curator-recipes 对于大部分用户来说，只需要依赖此构件。包含所有recipes  curator-async 异步DSL curator-framework Curator框架的高层API  curator-client 客户端，代理ZooKeeper类  curator-x-discovery 基于 Curator框架的服务发现实现 curator-x-discovery-server 用于Curator发现的RESTful服务器  起步 <a class="read-more" href="https://blog.gmem.cc/apache-curator-study-note">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/apache-curator-study-note">Apache Curator学习笔记</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 Curator<span style="color: #c0c0c0;"><em>（音标[kjʊ(ə)'reɪtə]）</em><span style="color: #333333;">Framework</span></span>是ZooKeeper的Keeper（动物园管理员的管理员）。它是一个Java库，提供了比ZooKeeper更加高层的API，更加易用、可靠。Curator的推荐的ZooKeeper版本是 3.5+，但它也和3.4兼容。</p>
<p>Curator实现了自动的连接管理，当会话过期后，你需要重新创建ZooKeeper客户端，并重新设置Watcher。Curator可以透明的重新创建、重试连接。</p>
<div class="blog_h2"><span class="graybg">子项目</span></div>
<p>Curator以组标识<pre class="crayon-plain-tag">org.apache.curator</pre>发布在Maven中心仓库，包含以下构件：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 28%; text-align: center;">构件</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>curator-recipes</td>
<td>对于大部分用户来说，只需要依赖此构件。包含所有recipes </td>
</tr>
<tr>
<td>curator-async</td>
<td>异步DSL</td>
</tr>
<tr>
<td>curator-framework</td>
<td>Curator框架的高层API </td>
</tr>
<tr>
<td>curator-client</td>
<td>客户端，代理ZooKeeper类 </td>
</tr>
<tr>
<td>curator-x-discovery</td>
<td>基于 Curator框架的服务发现实现</td>
</tr>
<tr>
<td>curator-x-discovery-server</td>
<td>用于Curator发现的RESTful服务器 </td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">起步</span></div>
<div class="blog_h2"><span class="graybg">Maven依赖</span></div>
<p>当前版本是4.0.0，通常你需要引用下面这个构件。此构件对curator-framework、curator-client有依赖：</p>
<pre class="crayon-plain-tag">&lt;dependency&gt;
    &lt;groupId&gt;org.apache.curator&lt;/groupId&gt;
    &lt;artifactId&gt;curator-recipes&lt;/artifactId&gt;
    &lt;version&gt;4.0.0&lt;/version&gt;
&lt;/dependency&gt;</pre>
<div class="blog_h2"><span class="graybg">连接和操控</span></div>
<p>Curator内置了重连逻辑，因此你不再需要手工Watch和管理了：</p>
<pre class="crayon-plain-tag">String zookeeperConnectionString = "172.21.0.1:2181,172.21.0.2:2181,172.21.0.3:2181";
// 重试策略，如果连接不上ZooKeeper集群如何重连
RetryPolicy retryPolicy = new ExponentialBackoffRetry( 1000, 3 );
CuratorFramework client = CuratorFrameworkFactory.newClient( zookeeperConnectionString, retryPolicy );
client.start();
// 创建znode
client.create().forPath( "/tmp", EMPTY );</pre>
<div class="blog_h2"><span class="graybg">CuratorFramework</span></div>
<div class="blog_h3"><span class="graybg">API</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>create</td>
<td>启动一个znode创建操作，可以调用额外方法来设置节点类型、添加Watcher，使用forPath完成操作：<br />
<pre class="crayon-plain-tag">client.create().withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath("/user", new byte[0]);</pre>
</td>
</tr>
<tr>
<td>delete</td>
<td>启动一个删除操作，使用forPath完成操作：<br />
<pre class="crayon-plain-tag">client.delete().inBackground().forPath("/user");</pre>
</td>
</tr>
<tr>
<td>checkExists</td>
<td>启动一个检查znode存在性的操作，使用forPath完成操作</td>
</tr>
<tr>
<td>getData</td>
<td>启动一个获取znode关联数据的操作，使用forPath完成操作：<br />
<pre class="crayon-plain-tag">client.getData().watched().inBackground().forPath("/user");</pre>
</td>
</tr>
<tr>
<td>setData</td>
<td>启动一个设置znode关联数据的操作，使用forPath完成操作</td>
</tr>
<tr>
<td>getChildren</td>
<td>启动获取znode子节点集合的操作，使用forPath完成操作</td>
</tr>
<tr>
<td>transactionOp</td>
<td>调用以生成供transaction()使用的操作条目</td>
</tr>
<tr>
<td>transaction</td>
<td>原子的提交一系列的操作条目</td>
</tr>
<tr>
<td>getACL</td>
<td>启动一个获取znode访问控制列表的操作，使用forPath完成操作</td>
</tr>
<tr>
<td>setACL</td>
<td>启动一个设置znode访问控制列表的操作，使用forPath完成操作</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">Recipes</span></div>
<p>Recipes封装了很多高层语义，例如互斥锁、Leader选举，你不再需要手工实现了：</p>
<pre class="crayon-plain-tag">// 分布式互斥锁用法：
InterProcessMutex lock = new InterProcessMutex(client, lockPath);
if ( lock.acquire(maxWait, waitUnit) ) {
    try{
        // 临界区
    }
    finally{
        lock.release();
    }
}</pre><br />
<pre class="crayon-plain-tag">// 在多个进程之间选择Leader
LeaderSelectorListener listener = new LeaderSelectorListenerAdapter() {
    public void takeLeadership(CuratorFramework client) throws Exception {
        // 如果当前进程被选择为Leader则此回调被调用
        // 除非当前进程想放弃Leader地位，不要退出此方法
    }
}

LeaderSelector selector = new LeaderSelector(client, path, listener);
selector.autoRequeue();
selector.start();</pre>
<div class="blog_h2"><span class="graybg">ConnectionStateListener</span></div>
<p>Curator提供了此接口，用于处理连接中断：</p>
<pre class="crayon-plain-tag">public interface ConnectionStateListener {
    public void stateChanged(CuratorFramework client, ConnectionState newState);
} </pre>
<p>使用某些Recipe时，你应该通过Listenable.addListener注册此接口的实现。</p>
<div class="blog_h3"><span class="graybg">ConnectionState</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>CONNECTED</td>
<td>第一次成功连接到ZooKeeper后，进入此状态。对于每个CuratorFramework对象，此状态仅出现一次</td>
</tr>
<tr>
<td>READONLY</td>
<td>连接进入只读模式，调用CuratorFrameworkFactory.Builder.canBeReadOnly(true)后导致此状态</td>
</tr>
<tr>
<td>SUSPENDED</td>
<td>到ZooKeeper的连接丢失</td>
</tr>
<tr>
<td>RECONNECTED</td>
<td>丢失的连接被重新建立</td>
</tr>
<tr>
<td>LOST</td>
<td>
<p>当Curator认为ZooKeeper会话已经过期，则进入此状态。可能的原因包括：</p>
<ol>
<li>ZooKeeper返回Watcher.Event.KeeperState.Expired或者KeeperException.Code.SESSIONEXPIRED</li>
<li>Curator关闭了内部管理的ZooKeeper客户端实例</li>
<li>由于网络中断导致的会话过期</li>
</ol>
</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">Recipes</span></div>
<p>Recipes是Curator提供的高层封装。大部分Recipes都会自动创建所需要的父znode。</p>
<div class="blog_h2"><span class="graybg">选举类</span></div>
<p>在分布式计算领域，多个同类型服务器（节点）通常需要指定其中一员作为Leader，例如主从复制场景下需要指定主节点（Leader）。</p>
<p>Leader通常通过选举产生，在选举前，任何节点均不知道Leader是谁，选举完毕后，则所有节点对Leader是谁达成共识。</p>
<p>Curator提供了两个Recipes来支持选举。其中LeaderSelectorListener已经在起步一章示例过。</p>
<div class="blog_h3"><span class="graybg">LeaderLatch</span></div>
<p>用法示例：</p>
<pre class="crayon-plain-tag">String id = "websvr0";
LeaderLatch ll = new LeaderLatch( client, "/pems/websvr", id );
// 需要先启动
// 一旦启动，LeaderLatch会自动联系其它选举参与者，并随机选择一个作为Leader
ll.start();
// 任何时候，你可以调用下面的接口来判断当前进程是否为Leader
ll.hasLeadership();
// 一直等待，知道变为Leader
ll.await();
// 把当前进程从被选举人列表中移除，如果当前是Leader则放弃此权利，其它进程会被选举为Leader
ll.close();</pre>
<p>LeaderLatch会添加一个ConnectionStateListener来监控连接问题：</p>
<ol>
<li>当连接变为SUSPENDED/LOST状态后，当前Leader的hasLeadership()调用返回false（丢失Leader权限）</li>
<li>当断开的连接变为RECONNECTED，LeaderLatch会删除之前创建的znode，并创建新的</li>
</ol>
<div class="blog_h2"><span class="graybg">分布式锁</span></div>
<div class="blog_h3"><span class="graybg">InterProcessMutex</span></div>
<p>这是一个分布式的可重入（锁的持有者可以反复获得锁）的共享锁，保证任何时刻只能有一个客户端占有锁。</p>
<p>用法示例：</p>
<pre class="crayon-plain-tag">InterProcessMutex mutex = new InterProcessMutex( client, "/mutex/resource" );
// 阻塞，直到获得锁。返回true
mutex.acquire();
// 阻塞一定时间，尝试获得锁，返回true/false
mutex.acquire( 1000, TimeUnit.SECONDS );
// 释放锁，仅仅当调用线程是当初获得锁的线程时，才可以释放
mutex.release();

// 使得锁可被撤销，当其它进程/线程attemptRevoke此锁时，你会得到通知
mutex.makeRevocable( forLock -&gt; {
    // 得到通知后，你可以选择释放锁
    forLock.release();
} );

// 尝试撤销锁
Revoker.attemptRevoke(client,"/mutex/resource" );</pre>
<p>你应当设置一个ConnectionStateListener来监听SUSPENDED/LOST状态变更：</p>
<ol>
<li>当连接变为SUSPENDED状态后，你无法确定当前是否仍然持有锁，除非恢复到RECONNECTED状态</li>
<li>当连接变为LOST状态后，你肯定已经丢失了锁 </li>
</ol>
<div class="blog_h3"><span class="graybg">InterProcessSemaphoreMutex</span></div>
<p>API类似于InterProcessMutex，对应类InterProcessSemaphoreMutex。此锁不可以重入。</p>
<div class="blog_h3"><span class="graybg">InterProcessReadWriteLock</span></div>
<p>这是一个可重入的读写锁，所有JVM中的所有线程共享一个分布式的临界区域。此外，该锁是“公平的” —— 每个请求者按照其请求的顺序（ZooKeeper角度）来获得锁。</p>
<p>读写锁维护一对相关联的锁，一个用于读，一个用于写。其中读锁可以被多个进程共同持有，而写锁则是独占的（也不允许读锁被持有）。</p>
<p>持有写锁者，可以继续请求读锁，反之则不行。通过获取写锁 —— 获取读锁 —— 释放写锁，可以实现锁降级。</p>
<p>代码示例：</p>
<pre class="crayon-plain-tag">InterProcessReadWriteLock lock = new InterProcessReadWriteLock( client, "/resource" );
// 获取两个相关的锁，然后调用InterProcessMutex的API即可
InterProcessMutex readLock = lock.readLock();
InterProcessMutex writeLock = lock.writeLock();</pre>
<p>你应当设置一个ConnectionStateListener来监听SUSPENDED/LOST状态变更。 </p>
<div class="blog_h3"><span class="graybg">InterProcessSemaphoreV2</span></div>
<p>这是一个分布式的信号量。所谓信号量，是一个有限数量的资源集，进程可以租借/归还信号量。此信号量实现是“公平的” —— 每个请求者按照其请求的顺序（ZooKeeper角度）来获得信号量。</p>
<p>确定信号量资源数的方式有两种：</p>
<ol>
<li>由SharedCountReader类提供</li>
<li>静态声明信号量数量，这要求所有参与者声明相同的数值</li>
</ol>
<p>代码示例：</p>
<pre class="crayon-plain-tag">// 声明一个具有10个资源的信号量
InterProcessSemaphoreV2 semaphore = new InterProcessSemaphoreV2( client, "/semaphore", 10 );
// 通过SharedCount指定资源数量
SharedCountReader sc = new SharedCount( client, "/sharedcount", 10 );
semaphore = new InterProcessSemaphoreV2( client, "/semaphore", sc );

// 租借一个资源，可选参数等待时间
Lease lease = semaphore.acquire();
// 租借N个资源，可选参数等待时间
Collection&lt;Lease&gt; leases = semaphore.acquire( 2 );

// 归还资源
lease.close();
// 或者：
semaphore.returnLease( lease );
semaphore.returnAll( leases );</pre>
<p>你应当设置一个ConnectionStateListener来监听SUSPENDED/LOST状态变更。</p>
<div class="blog_h3"><span class="graybg">InterProcessMultiLock</span></div>
<p>持有一系列InterProcessLock的引用，客户端要么获取所有锁，要么失败。当释放时，所有锁一起被释放。</p>
<div class="blog_h2"><span class="graybg">屏障</span></div>
<div class="blog_h3"><span class="graybg">DistributedBarrier</span></div>
<p>所谓屏障，是一个同步点。每一个进程到达此点都要等待，直到所有进程都到达，则继续。</p>
<p>代码示例： </p>
<pre class="crayon-plain-tag">DistributedBarrier barrier = new DistributedBarrier( client, "/barrier" );
// 设置屏障，每个客户端设置一次
barrier.setBarrier();

// 等待所有客户端都到达，如果连接丢失，此方法会抛出异常
barrier.waitOnBarrier();</pre>
<div class="blog_h3"><span class="graybg">DistributedDoubleBarrier</span></div>
<p>双重屏障，在协作开始之前同步，当足够数量的进程加入到屏障后，开始协作，当所有进程完毕后离开屏障。</p>
<p>代码示例：</p>
<pre class="crayon-plain-tag">// 建立一个10个资源的屏障
DistributedDoubleBarrier barrier = new DistributedDoubleBarrier( client, "/barrier", 10 );
// 进入屏障，当有10个客户端进入屏障后，阻塞解除
barrier.enter();
// 这里是协作逻辑 ...
// 离开屏障，当所有客户端都尝试离开时，阻塞解除
barrier.leave();</pre>
<p>如果连接丢失，则enter/leave会抛出异常。 </p>
<div class="blog_h2"><span class="graybg">计数器</span></div>
<div class="blog_h3"><span class="graybg">SharedCount</span></div>
<p>一个共享的整数，所有客户端均看到一致性的、最新的数值。</p>
<p>代码示例：</p>
<pre class="crayon-plain-tag">// 第三个参数为初始值，假设路径不存在，则自动设置为此值
SharedCount counter = new SharedCount( client, "/counter", 0 );
// 此计数器需要启动
counter.start();
// 设置计数值
counter.setCount( 10 );
// 获取计数值
counter.getCount();
counter.addListener( new SharedCountListener() {
    @Override
    public void countHasChanged( SharedCountReader sharedCount, int newCount ) throws Exception {
        // 计数值变化后异步回调
    }

    @Override
    public void stateChanged( CuratorFramework curatorFramework, ConnectionState connectionState ) {
        // 连接数量变化后的异步回调
        // 如果连接变为SUSPENDED状态，你必须假设计数器的值不再精确
        // 如果连接变为LOST状态，则计数器永久性失效
    }
} );</pre>
<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;">Recipe</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td><a href="http://curator.apache.org/curator-recipes/distributed-atomic-long.html">DistributedAtomicLong </a></td>
<td>分布式的原子的整数，支持自增、自减、加、减等操作</td>
</tr>
<tr>
<td><a href="http://curator.apache.org/curator-recipes/node-cache.html">NodeCache</a></td>
<td>拥有监控一个znode。每当数据变更或者此节点被删除，NodeCache都会更新自己的状态，反映当前数据（如果节点被删除数据为null）</td>
</tr>
<tr>
<td><a href="http://curator.apache.org/curator-recipes/path-cache.html">PathChildrenCache</a></td>
<td>用于监控一个znode。每当添加、更新、删除子节点时，PathChildrenCache都会更新自己的状态，反映最新的子节点集合、子节点数据、子节点状态</td>
</tr>
<tr>
<td><a href="http://curator.apache.org/curator-recipes/tree-cache.html">TreeCache</a></td>
<td>监控一个znode的整个子树</td>
</tr>
<tr>
<td><a href="http://curator.apache.org/curator-recipes/persistent-node.html">PersistentNode</a></td>
<td>尝试驻留ZooKeeper的节点，即使在连接/会话中断的情况下</td>
</tr>
<tr>
<td><a href="http://curator.apache.org/curator-recipes/persistent-ttl-node.html">PersistentTtlNode</a></td>
<td>如果你想创建TTL节点，但是又不愿意手工周期性设置其数据，可以使用</td>
</tr>
<tr>
<td><a href="http://curator.apache.org/curator-recipes/group-member.html">GroupMember</a></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>Curator提供了一个基于Java 8 Completion Stage的纯异步客户端实现。要使用此实现，添加依赖：</p>
<pre class="crayon-plain-tag">&lt;dependency&gt;
    &lt;groupId&gt;org.apache.curator&lt;/groupId&gt;
    &lt;artifactId&gt;curator-x-async&lt;/artifactId&gt;
    &lt;version&gt;4.0.0&lt;/version&gt;
&lt;/dependency&gt;</pre>
<div class="blog_h3"><span class="graybg">代码示例</span></div>
<pre class="crayon-plain-tag">String zookeeperConnectionString = "172.21.0.1:2181,172.21.0.2:2181,172.21.0.3:2181";
RetryPolicy retryPolicy = new ExponentialBackoffRetry( 1000, 3 );
CuratorFramework client = CuratorFrameworkFactory.newClient( zookeeperConnectionString, retryPolicy );
client.start();


// 此包装器提供异步API
AsyncCuratorFramework async = AsyncCuratorFramework.wrap( client );
//    检查znode /user是否存在             如果存在，打印状态结构
async.checkExists().forPath( "/user" ).thenAccept( stat -&gt; LOGGER.debug( stat.toString() ) );

// 获取数据
async.getData().forPath( "/user" ).thenAccept( data -&gt; LOGGER.debug( new String( data ) ) );

// 在最前面添加watched()调用，可以增加Watcher
// 使用AsyncStage（CompletionStage子类型）的event()方法，设置Watcher的回调
async.with( WatchMode.successOnly) // 不关心连接丢失类的事件
     .watched().getData().forPath( "/user" ).event().thenAccept( ev -&gt; LOGGER.debug( ev.toString() ) );

// 同步化
async.create().forPath("/user").toCompletableFuture().get();</pre>
<div class="blog_h2"><span class="graybg">强类型模型</span></div>
<p>通过串行化机制，Apache Curator支持在ZooKeeper的znode中存储类型化的数据。这一功能由Modeled Curator组件负责。</p>
<div class="blog_h3"><span class="graybg">ZPath</span></div>
<p>MC不使用原始的字符串形式的路径，而是使用ZPath来抽象ZooKeeper路径。ZPath可以是简单的字符串，也可以包含路径变量，这些变量可以根据需要替换为动态的取值：</p>
<pre class="crayon-plain-tag">// 静态路径
ZPath staticPath = ZPath.parse( "/static/path" );
// 动态路径，使用 {} 包围路径变量，路径变量又叫ID，其中可以包含任何东西，例如{ any thing}但是没有什么意义
// 路径变量（ID）总是从左向右依次解析
ZPath path = ZPath.parseWithIds( "/static/{}/{}" );
// 路径变量可以被替换，如果用于替换的对象是NodeName，则调用其nodeName()方法，否则调用toString()方法
ZPath resolvedPath = path.resolved( "path", new NodeName() {
    public String nodeName() {
        return "name";
    }
}  );
// 输出内容：/static/path/name
System.out.println( resolvedPath );

// 路径可以被部分的解析：
System.out.println( path.resolved( "path" ) );</pre>
<div class="blog_h3"><span class="graybg">ModelSpec</span></div>
<p>此类型包含对ZooKeeper路径进行操作（存取强类型对象）所需的全部元数据：</p>
<ol>
<li>一个ZPath</li>
<li>用于串行化对象的串行化器</li>
<li>关于如何创建znode的选项（顺序、压缩、TTL等）</li>
<li>针对znode的ACL</li>
<li>如何删除znode（是否删除子节点）</li>
</ol>
<div class="blog_h3"><span class="graybg">写入模型</span></div>
<p>要写入一个数据模型到znode的关联数据，参考如下代码： </p>
<pre class="crayon-plain-tag">public static class ServerInfo implements NodeName {
    private String name;
    private String type;

    @Override
    public String nodeName() {
        return getName();
    }
}

ModelSpec&lt;ServerInfo&gt; spec = ModelSpec.builder(
        ZPath.parseWithIds("/servers/{}"),
        JacksonModelSerializer.build(ServerInfo.class)
).build();
// 依赖异步客户端
ModeledFramework&lt;ServerInfo&gt; modeledClient = ModeledFramework.wrap(async, spec);

ServerInfo info = new ServerInfo();
info.setType("webserver");
info.setName("tk.gmem.cc");
// 输出：/servers/hk.gmem.cc，这个路径是把模型传入ZPath而解析得到的，因为模型实现了NodeName，因此调用其nodeName()替换路径变量
modeledClient.set(info).thenAccept(path -&gt; LOGGER.debug(path)).toCompletableFuture().get();</pre>
<p>本例使用JacksonJSON作为串行化机制的实现，需要引入依赖：</p>
<pre class="crayon-plain-tag">&lt;dependency&gt;
    &lt;groupId&gt;com.fasterxml.jackson.core&lt;/groupId&gt;
    &lt;artifactId&gt;jackson-databind&lt;/artifactId&gt;
    &lt;version&gt;LATEST&lt;/version&gt;
&lt;/dependency&gt;</pre>
<div class="blog_h3"><span class="graybg">读取模型</span></div>
<pre class="crayon-plain-tag">ModelSpec&lt;ServerInfo&gt; spec = ModelSpec.builder(
        ZPath.parseWithIds("/servers/hk.gmem.cc"),
        JacksonModelSerializer.build(ServerInfo.class)
).build();
ModeledFramework&lt;ServerInfo&gt; modeledClient = ModeledFramework.wrap(async, spec);
modeledClient.read().whenComplete((info, e) -&gt; {
    if (e != null) {
        
    } else {
        LOGGER.debug(info.getType());
    }
}).toCompletableFuture().get();</pre>
<div class="blog_h2"><span class="graybg">迁移</span></div>
<p>这一功能允许你在一个事务中，执行一系列的ZooKeeper操作。你可以使用迁移来保证某个ZooKeeper子树的数据一致性。示例：</p>
<pre class="crayon-plain-tag">// 一系列操作组成一个迁移
CuratorOp op1 = async.transactionOp().create().forPath("/parent");
CuratorOp op2 = async.transactionOp().create().forPath("/parent/one");
CuratorOp op3 = async.transactionOp().create().forPath("/parent/two");
CuratorOp op4 = async.transactionOp().create().forPath("/parent/three");
// Migration是一个函数式接口，本质上就是一系列操作
Migration migration = () -&gt; Arrays.asList(op1, op2, op3, op4);
// 迁移集可以包含若干个迁移，迁移集必须有个ID
MigrationSet set = MigrationSet.build("main", Collections.singletonList(migration));

// 迁移管理器，负责执行迁移
// 迁移管理器会监控迁移集的执行状态，它只会应用那些没有应用的迁移
MigrationManager manager = new MigrationManager(client,
        lockPath,       // 迁移管理器会锁定此路径
        metaDataPath,   // 存储元数据的路径
        executor,       // 异步执行器
        lockMax         // 锁定最长时间
);
manager.migrate(set).exceptionally(e -&gt; {
    if (e instanceof MigrationException) {
        // migration checksum failed, etc.
    } else {
        // some other kind of error
    }
    return null;
});</pre>
<div class="blog_h1"><span class="graybg">服务发现</span></div>
<p>在SOA/分布式系统中，服务需要能够相互发现。例如，一个Web服务需要发现缓存服务。可以使用服务发现系统（Service Discovery system）来避免服务位置的硬编码。服务发现系统的功能包括：</p>
<ol>
<li>允许服务注册自己，便于其它服务调用之</li>
<li>定位某种服务的但个实例</li>
<li>当服务的实例发生变化，可以发出通知</li>
</ol>
<div class="blog_h2"><span class="graybg">Curator服务发现</span></div>
<p>Curator服务发现功能由单独的子项目实现：</p>
<pre class="crayon-plain-tag">&lt;dependency&gt;
    &lt;groupId&gt;org.apache.curator&lt;/groupId&gt;
    &lt;artifactId&gt;curator-x-discovery&lt;/artifactId&gt;
    &lt;version&gt;4.0.0&lt;/version&gt;
&lt;/dependency&gt;</pre>
<div class="blog_h3"><span class="graybg">ServiceInstance</span></div>
<p>这个类代表一个服务的实例，具有名称（同一类型服务共享）、标识符、地址、端口、可选的附加详细信息。服务实例被串行化在znode中，路径为/basePath/serviceName/id。</p>
<div class="blog_h3"><span class="graybg">ServiceProvider</span></div>
<p>基于提供策略（provider strategy）对某个特定的命名服务的发现服务进行封装，所谓提供策略，决定了如何从多个服务实例中选择一个实例。内置的策略包括循环选择（Round Robin）、随机、粘性（Sticky，总选择一个实例）。</p>
<p>ServiceProviderBuilder作为ServiceProvider的工厂，由ServiceDiscovery提供。ServiceProviderBuilder允许你设置服务的名称以及其它可选的属性。</p>
<p>ServiceProvider必须在<pre class="crayon-plain-tag">start()</pre>之后才能正常工作，当你不再需要它以后，应该调用<pre class="crayon-plain-tag">close()</pre>，要获取服务实例，调用<pre class="crayon-plain-tag">getInstance()</pre>。当无法连接到某个实例时，你应该调用<span style="color: rgba(0, 0, 0, 0.74902);"><pre class="crayon-plain-tag">noteError()</pre>，这样ServiceProvider会根据DownInstancePolicy决定何时将其标注为宕机。</span></p>
<div class="blog_h3"><span class="graybg">ServiceDiscovery</span></div>
<p>由ServiceDiscoveryBuilder创建，此类负责提供ServiceProviderBuilder。</p>
<p>ServiceProvider必须在<pre class="crayon-plain-tag">start()</pre>之后才能正常工作，当你不再需要它以后，应该调用<pre class="crayon-plain-tag">close()</pre></p>
<div class="blog_h3"><span class="graybg">示例代码</span> </div>
<pre class="crayon-plain-tag">// 当前JVM提供的服务实例
ServiceInstance&lt;InstanceDetail&gt; thisService = ServiceInstance.&lt;InstanceDetail&gt;builder()
        .name( "cacheService" )
        .payload( new InstanceDetail() )
        .address( "192.168.0.89" )
        .port( 8088 )
        .uriSpec( new UriSpec( "http://{address}:{port}" ) )
        .build();

// 服务发现
JsonInstanceSerializer&lt;InstanceDetail&gt; serializer = new JsonInstanceSerializer&lt;InstanceDetail&gt;( InstanceDetail.class );
ServiceDiscovery&lt;InstanceDetail&gt; discovery = ServiceDiscoveryBuilder
        .builder( InstanceDetail.class )
        .client( client )
        .basePath( "/sd" )
        .serializer( serializer )
        .thisInstance( thisService )
        .build();
// 需要启动
discovery.start();

// 服务提供者
ServiceProvider&lt;InstanceDetail&gt; sp = discovery
        .serviceProviderBuilder()
        .serviceName( "cacheService" )
        // 随机选取服务实例
        .providerStrategy( new RandomStrategy&lt;&gt;() )
        // 如果在一分钟内，有10此noteError则认为目标实例宕机
        .downInstancePolicy( new DownInstancePolicy( 60, TimeUnit.SECONDS, 10 ) )
        .build();
// 需要启动
sp.start();


LOGGER.debug( sp.getInstance().buildUriSpec() );</pre>
<div class="blog_h2"><span class="graybg">服务发现服务器</span></div>
<p>为了让非JVM应用使用服务发现功能，Curator提供了RESTful的WebService，你可以通过HTTP协议来注册、移除、查询服务。</p>
<p>Curator提供了JAX-RS组件，你可以在任何Web容器中，配合JAX-RS提供者（例如Jersey）使用该组件。</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">KeeperErrorCode = Unimplemented</span></div>
<p>可能原因是ZooKeeper服务器的版本与工程声明的ZooKeeper客户端版本不兼容导致。</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/apache-curator-study-note">Apache Curator学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/apache-curator-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>Apache Storm学习笔记</title>
		<link>https://blog.gmem.cc/apache-storm-study-note</link>
		<comments>https://blog.gmem.cc/apache-storm-study-note#comments</comments>
		<pubDate>Fri, 07 Jul 2017 05:08:58 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[BigData]]></category>
		<category><![CDATA[实时处理]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=16475</guid>
		<description><![CDATA[<p>简介 Apache Storm是一个分布式的实时计算系统，它能够可靠的对无边界的数据流进行处理。与Hadoop的批量处理方式不同，Storm对数据进行的是实时处理。Storm很简单，支持很多编程语言。 Storm的应用场景包括：实时分析、在线机器学习、持续计算、分布式RPC、分布式ETL。 Storm的性能很好，每个节点在一秒内能够处理多达100万个元组。Storm支持无限制扩容、容错、并且保证数据能够被处理。 你可以让既有的数据库、消息队列系统和Storm进行集成。Storm拓扑消费数据流，以任意复杂的方式处理数据流，数据流可以在任何计算阶段（Stage）重新分区。 基本概念 拓扑（Topology） 实时计算的逻辑被封装到Storm拓扑中，拓扑和MapReduce的Job类似。一个关键的区别是，MapReduce Job最终会结束，而拓扑则一直运行下去。 你可以使用TopologyBuilder构建一个拓扑。 Storm把每个具体的工作委托给多种类型的组件处理。这些组件都装配在拓扑中。 拓扑可以跨越Storm集群的多个工作节点运行。 流（Stream） 流是Storm的核心抽象，它是无边界的元组的序列。流以分布式的风格被并行的创建、处理。流的元素是元组，元组可以包含整数、字符串、浮点数、布尔、比特数组等类型的字段。你可以自己实现串行化器，支持任何自定义数据类型。 你可以使用OutputFieldsDeclarer来声明流以及流的Schema。在声明的时候，每个流被赋予一个标识符。仅仅释放单个流的Spout很常见，因此OutputFieldsDeclarer提供了声明单个流的便捷方法，不需要指定标识符，流的标识符自动设置为default。 元组（Tuple） Storm中被处理的数据的基本单元，本质上就是一个预定义的字段的值列表。 Spout可以释放出元组，不同的Bolt可以读取同一Spout产生的元组，并释放不同格式的元组，供其它Bolt消费。 Spout 在拓扑中，Spout是流的来源。一般来说，Spout从外部读取元组，并释放（Emit）出元组的流（Spout本意即喷射口）。Spout可以是可靠的（Reliable ）或者不可靠的。可靠的Spout支持在处理失败后重复（Replay）元组。所有Spout必须实现IRichSpout接口。 <a class="read-more" href="https://blog.gmem.cc/apache-storm-study-note">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/apache-storm-study-note">Apache Storm学习笔记</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 Storm是一个分布式的实时计算系统，它能够可靠的对无边界的数据流进行处理。与Hadoop的批量处理方式不同，Storm对数据进行的是实时处理。Storm很简单，支持很多编程语言。</p>
<p>Storm的应用场景包括：实时分析、在线机器学习、持续计算、分布式RPC、分布式ETL。</p>
<p>Storm的性能很好，每个节点在一秒内能够处理多达100万个元组。Storm支持无限制扩容、容错、并且保证数据能够被处理。</p>
<p>你可以让既有的数据库、消息队列系统和Storm进行集成。Storm拓扑消费数据流，以任意复杂的方式处理数据流，数据流可以在任何计算阶段（Stage）重新分区。</p>
<div class="blog_h2"><span class="graybg">基本概念</span></div>
<div class="blog_h3"><span class="graybg">拓扑（Topology）</span></div>
<p>实时计算的逻辑被封装到Storm拓扑中，拓扑和MapReduce的Job类似。一个关键的区别是，MapReduce Job最终会结束，而拓扑则一直运行下去。</p>
<p>你可以使用<a href="http://storm.apache.org/releases/1.1.1/javadocs/org/apache/storm/topology/TopologyBuilder.html">TopologyBuilder</a>构建一个拓扑。</p>
<p>Storm把每个具体的工作委托给多种类型的组件处理。这些组件都装配在拓扑中。</p>
<p>拓扑可以跨越Storm集群的多个工作节点运行。</p>
<div class="blog_h3"><span class="graybg">流（Stream）</span></div>
<p>流是Storm的核心抽象，它是无边界的元组的序列。流以分布式的风格被并行的创建、处理。流的元素是元组，元组可以包含整数、字符串、浮点数、布尔、比特数组等类型的字段。你可以自己实现串行化器，支持任何自定义数据类型。</p>
<p>你可以使用<a href="http://storm.apache.org/releases/1.1.1/javadocs/org/apache/storm/topology/OutputFieldsDeclarer.html">OutputFieldsDeclarer</a>来声明流以及流的Schema。在声明的时候，每个流被赋予一个标识符。仅仅释放单个流的Spout很常见，因此OutputFieldsDeclarer提供了声明单个流的便捷方法，不需要指定标识符，流的标识符自动设置为default。</p>
<div class="blog_h3"><span class="graybg">元组（Tuple）</span></div>
<p>Storm中被处理的数据的基本单元，本质上就是一个预定义的字段的值列表。</p>
<p>Spout可以释放出元组，不同的Bolt可以读取同一Spout产生的元组，并释放不同格式的元组，供其它Bolt消费。</p>
<div class="blog_h3"><span class="graybg">Spout</span></div>
<p>在拓扑中，Spout是流的来源。一般来说，Spout从外部读取元组，并释放（Emit）出元组的流（Spout本意即喷射口）。Spout可以是可靠的（Reliable ）或者不可靠的。可靠的Spout支持在处理失败后重复（Replay）元组。所有Spout必须实现<a href="http://storm.apache.org/releases/1.1.1/javadocs/org/apache/storm/topology/IRichSpout.html">IRichSpout</a>接口。</p>
<p>Spout可以释放不止一个流，你可以多次调用<pre class="crayon-plain-tag">OutputFieldsDeclarer</pre>的<pre class="crayon-plain-tag">declareStream</pre>。你可以指定<pre class="crayon-plain-tag">SpoutOutputCollector.emit</pre>调用释放哪个流。</p>
<p>Spout的主要方法是nextTuple，调用它可以释放一个新的元组到拓扑中，如果没有可用的元组，该方法应该简单的返回。Storm强制要求所有Spout实现类的nextTuple方法是非阻塞性的，因为Storm在单个线程中调用Spout的所有方法。</p>
<p>其它重要的方法包括ack、fail，这些方法仅仅针对可靠Spout调用，分别在元组顺利通过拓扑后、处理失败后调用，将处理结果通知给Spout。</p>
<div class="blog_h3"><span class="graybg">Bolt</span></div>
<p>Spout将数据传递给Bolt这种组件进行实质上的逻辑处理。拓扑中最主要的内容就是Bolt构成的链条。</p>
<p>拓扑中所有处理逻辑发生在Bolt中，Bolt可以进行过滤、聚合、Join等操作，可以和数据库进行交互。</p>
<p>Bolt可以进行简单的流变换（Stream Transformation），复杂的流变换通常需要多个步骤，也就需要多个Bolt。</p>
<p>Bolt可以释放不止一个流，你可以多次调用<pre class="crayon-plain-tag">OutputFieldsDeclarer</pre>的<pre class="crayon-plain-tag">declareStream</pre>。你可以指定<pre class="crayon-plain-tag">OutputCollector.emit</pre>调用释放哪个流。</p>
<p>在声明Bolt的输入流时，你需要订阅其它组件的某个特定流，如果要订阅某个组件的多个流，你需要逐个订阅。<a href="http://storm.apache.org/releases/1.1.1/javadocs/org/apache/storm/topology/InputDeclarer.html">InputDeclarer</a>提供了订阅具有默认标识符的流的快捷方法，例如<pre class="crayon-plain-tag">declarer.shuffleGrouping("1")</pre>订阅组件1的<pre class="crayon-plain-tag">DEFAULT_STREAM_ID</pre>流。在构建拓扑时，你也可以声明这种流订阅。</p>
<p>Bolt的主要方法是execute，此方法接受一个元组输入。Bolt使用<a href="http://storm.apache.org/releases/1.1.1/javadocs/org/apache/storm/task/OutputCollector.html">OutputCollector</a>对象释放新的元组。Bolt会针对每个输入元组调用OutputCollector.ack方法，以通知Storm目标元组已经处理成功，这些ack调用最终导致Spout.ack被调用。大部分Bolt根据输入元组输出0-N个输出元组。<a href="http://storm.apache.org/releases/1.1.1/javadocs/org/apache/storm/topology/IBasicBolt.html">IBasicBolt</a>提供自动确认（Ack）功能。</p>
<p>在Bolt中启动新线程进行异步处理是很好的做法，OutputCollector是线程安全的，你可以在任何时候调用它。</p>
<p>Bolt的基础接口是<a href="http://storm.apache.org/releases/1.1.1/javadocs/org/apache/storm/topology/IRichBolt.html">IRichBolt</a>。要设计进行过滤、简单function的Bolt可以选择实现IBasicBolt接口。Bolt调用OutputCollector来释放元组到它的输出流中。</p>
<div class="blog_h3"><span class="graybg">流分组</span></div>
<p>定义拓扑的一部分工作是，指定每个Bolt能够接受哪些流作为其输入。流分组（Stream Grouping）指定了如何在Bolt的Task之间进行流的分区（Partition）。</p>
<div class="blog_h3"><span class="graybg">可靠性</span></div>
<p>Storm可以保证每个元组都被拓扑处理。为了确认元组的处理过程，Storm对Spout释放出的元组树进行跟踪，并以此确定何时元组树被成功处理。每个拓扑都具有一个消息超时属性，如果在此超时到达之前没有检测到元组已经处理成功，则认定处理失败，并Replay目标元组。</p>
<p>之所有叫元组树，是因为在任何一个Bolt中，一个源元组可能产生多个目标元组，这样经过多个Bolt后，就形成树状依赖关系。注意，目的和源的依赖关系需要编程式的声明，一个目标元组可以依赖于多个源元组。</p>
<div class="blog_h3"><span class="graybg">任务</span></div>
<p>每个Spout、Bolt在集群中执行一定数量的Task，每个Task对应一个执行线程。流分组决定了元组如何从一组Task分发到另外一组Task。调用TopologyBuilder的setSpout/setBolt方法可以设置并发度。</p>
<div class="blog_h3"><span class="graybg">Worker</span></div>
<p>Topology跨越1-N个工作进程执行，每个工作进程都是一个JVM实例，负责执行拓扑中所有Task的一个子集。Storm会尝试尽可能平均的分配Task给Worker。例如，一个总和并行度为300的拓扑，分配了50个工作进程，则每个进程需要执行6个Task。</p>
<div class="blog_h3"><span class="graybg">节点</span></div>
<p>Storm集群包含两类节点：</p>
<ol>
<li>主节点（Master）：这类节点运行一个名为Nimbus的守护程序。此程序负责在集群中分发代码（处理逻辑）、向工作节点分配Task、监控Task是否执行失败</li>
<li>工作节点（Worker）：这类节点运行一个名为Supervisor的守护程序。此程序负责执行拓扑的某个部分</li>
</ol>
<p>Storm支持在本地磁盘或者ZooKeeper上保存集群的所有状态信息，因此守护程序宕机不会对系统的健康状况产生影响。</p>
<div class="blog_h3"><span class="graybg">和Hadoop对比</span></div>
<p>Storm和Hadoop的核心概念的对应关系如下图：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 15%; text-align: center;"> </td>
<td style="text-align: center;">Storm</td>
<td style="text-align: center;">Hadoop</td>
</tr>
</thead>
<tbody>
<tr>
<td rowspan="3">组件</td>
<td>JobTracker</td>
<td>Nimbus</td>
</tr>
<tr>
<td>TaskTracker</td>
<td>Supervisor</td>
</tr>
<tr>
<td>Child</td>
<td>Worker</td>
</tr>
<tr>
<td>应用</td>
<td>Job</td>
<td>Topology</td>
</tr>
<tr>
<td>接口</td>
<td>Mapper/Reducer</td>
<td>Spout /Bolt</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">调度器</span></div>
<p>Storm提供四类内置的调度器： DefaultScheduler, IsolationScheduler, MultitenantScheduler, ResourceAwareScheduler。</p>
<div class="blog_h2"><span class="graybg">运行模式</span></div>
<p>Storm有两种运行模式（Operation Modes）。</p>
<div class="blog_h3"><span class="graybg">本地模式</span></div>
<p>在此模式下，Storm的拓扑在本地机器中单个JVM中运行。此模式主要用于开发、测试和调试。</p>
<div class="blog_h3"><span class="graybg">远程模式</span></div>
<p>在此模式下，我们需要将定义好的拓扑提交到Storm集群中。集群通常由运行在很多机器中的进程组成。远程模式下不会显示调试信息，通常用于产品环境。</p>
<p>你也可以在单台机器上进行远程模式的部署。</p>
<div class="blog_h2"><span class="graybg">容错能力</span></div>
<p>Storm包含若干不同的守护进程，Nimbus能够调度Worker进程，Supervisors负责启动、杀死Worker进程。Logger View让你能够访问日志。Web服务器让你能够通过GUI查看Storm状态。</p>
<p>当Worker进程死掉后，Supervisor会重新启动它。如果Worker反复死掉并且不能完成到Nimbus的心跳检测，Nimbus会重新调度此Worker。</p>
<p>当节点宕机后，Nimbus会重新分配在节点上执行的Task给其它节点。</p>
<p>Nimbus/Supervisor被设计为无状态的，所有状态信息存放在ZooKeeper或者磁盘上。因此它们宕机后，你可以安全的重启它们。你应该考虑使用daemontools/monit之类的工具监控这类进程的状态，并在它们死掉后立即重启。</p>
<p>如果Nimbus节点（Master）宕机，Worker节点仍然会继续工作。此外Supervisor仍然会自动重启死掉的Worker进程。但是没有Nimbus就不能把Worker节点分配到其它机器上。从1.0.0开始，Storm支持Nimbus的HA。</p>
<div class="blog_h1"><span class="graybg">起步</span></div>
<p>在本章，我们创建一个简单的，分析动力环境监测值的拓扑。</p>
<div class="blog_h2"><span class="graybg">MessageSource</span></div>
<p>此类型模拟监测值采集程序，以随机的方式产生监测值：</p>
<pre class="crayon-plain-tag">package cc.gmem.study.storm.mv;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.lang3.RandomUtils;
import org.apache.commons.lang3.Range;
import org.apache.commons.lang3.mutable.MutableInt;
import org.apache.commons.lang3.tuple.Pair;
import org.slf4j.Logger;

import java.util.*;
import java.util.concurrent.TimeUnit;

import static org.slf4j.LoggerFactory.getLogger;

public class MessageSource extends Thread {

    private Map&lt;String, Pair&lt;Double, Double&gt;&gt; config;

    private ObjectMapper om;

    Queue&lt;String&gt; messages;

    private static final Logger LOGGER = getLogger( MessageSource.class );

    private boolean term;

    public MessageSource( Map&lt;String, Pair&lt;Double, Double&gt;&gt; config ) {
        super();
        this.config = config;
        this.messages = new LinkedList&lt;&gt;();
        this.om = new ObjectMapper();
        start();
    }

    @Override
    public void run() {
        setName( "MessageSource" );
        LOGGER.debug( "Starting monitoring value source thread" );
        while ( true ) {
            if ( term ) {
                LOGGER.debug( "Terminating monitoring value source" );
                break;
            }
            List&lt;Map&lt;String, Object&gt;&gt; mvs = new ArrayList&lt;&gt;();
            MutableInt idPfx = new MutableInt( 10 );
            config.forEach( ( type, pair ) -&gt; {
                int n = RandomUtils.nextInt( 1, 5 );
                for ( int i = 0; i &lt; n; i++ ) {
                    Map&lt;String, Object&gt; mv = new LinkedHashMap&lt;&gt;();
                    double value = RandomUtils.nextDouble( pair.getLeft(), pair.getRight() );
                    mv.put( "id", idPfx.intValue() + RandomUtils.nextInt( 1, 5 ) );
                    mv.put( "type", type );
                    mv.put( "value", value );
                    mvs.add( mv );
                }
                idPfx.add( 10 );
            } );
            LOGGER.debug( "Generated new message with {} monitoring value", mvs.size() );
            try {
                String msg = om.writeValueAsString( mvs );
                messages.offer( msg );
                TimeUnit.SECONDS.sleep( 10 );
            } catch ( Exception e ) {
                LOGGER.error( e.getMessage(), e );
            }
        }
    }

    public boolean available() {
        return !messages.isEmpty();
    }

    public String nextMessage() {
        return messages.poll();
    }

    public void singalTerm() {
        this.term = true;
    }
} </pre>
<div class="blog_h2"><span class="graybg">MessageReader</span></div>
<p>MessageReader是此拓扑中唯一的Spout，从MessageSource中读取监测值报文并直接传递给Bolt处理。</p>
<p>任何Spout需要实现IRichSpout接口，你可以选择继承缺省适配BaseRichSpout。</p>
<pre class="crayon-plain-tag">package cc.gmem.study.storm.mv;

import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.storm.spout.SpoutOutputCollector;
import org.apache.storm.task.TopologyContext;
import org.apache.storm.topology.IRichSpout;
import org.apache.storm.topology.OutputFieldsDeclarer;
import org.apache.storm.tuple.Fields;
import org.apache.storm.tuple.Values;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

public class MessageReader implements IRichSpout {

    private static final Logger LOGGER = LoggerFactory.getLogger( MessageReader.class );

    private MessageSource msgSrc;

    private SpoutOutputCollector collector;


    /**
     * 当前组件的任一个Task在集群的某个工作节点中初始化时，自动调用此方法
     *
     * @param conf      配置信息，在创建拓扑时指定的配置合并到当前机器的集群配置而成
     * @param context   拓扑的上下文信息，可以获得组件ID、Task ID、输入输出信息
     * @param collector 输出收集器，线程安全，应该作为实例变量
     *                  此收集器可以在任何时候调用，以释放元组供下游组件消费
     */
    @Override
    public void open( Map conf, TopologyContext context, SpoutOutputCollector collector ) {
        LOGGER.debug( "Spout {}/{} opening", context.getThisComponentId() ,context.getThisTaskId());
        Map&lt;String, Pair&lt;Double, Double&gt;&gt; config = new LinkedHashMap&lt;&gt;();
        // 读取配置信息
        List&lt;List&lt;Object&gt;&gt; msgSrcCfg = (List&lt;List&lt;Object&gt;&gt;) conf.get( "msgSrcCfg" );
        msgSrcCfg.forEach( typeCfg -&gt; {
            config.put( (String) typeCfg.get( 0 ), new ImmutablePair( (Double) typeCfg.get( 1 ), (Double) typeCfg.get( 2 ) ) );
        } );
        this.msgSrc = new MessageSource( config );
        this.collector = collector;
    }

    /**
     * 当此Spout即将被关闭时调用
     * &lt;p&gt;
     * 注意：此方法通常不保证一定被调用，因为在集群中Supervisor会以kill -9杀死工作进程。
     * 当以本地模式运行Storm时，此方法保证被调用
     */
    @Override
    public void close() {
        LOGGER.debug( "Spout closing" );
        msgSrc.singalTerm();
    }

    /**
     * 当Spout被激活后调用，激活后nextTuple很快被调用
     * 使用Storm客户端可以激活或者禁用Spout
     */
    @Override
    public void activate() {

    }

    /**
     * 当Spout被禁用后调用，处于禁用状态的Spout的nextTuple不会被调用
     */
    @Override
    public void deactivate() {

    }

    /**
     * 此方法会被周期性的调用，释放下一个元组
     */
    @Override
    public void nextTuple() {
        if ( !msgSrc.available() ) {
            try {
                // 休眠至少10ms再返回，避免无限制的CPU占用
                TimeUnit.MILLISECONDS.sleep( 10 );
                return;
            } catch ( InterruptedException e ) {
            }
        }
        // Values是对ArrayList&lt;Object&gt;的简单封装，其值可以是任何可串行化的Java对象
        String msg = msgSrc.nextMessage();
        LOGGER.debug( "Emit new tuple: {}", msg );
        this.collector.emit( new Values( msg ) );
    }

    /**
     * 当此Spout释放出的具有指定ID的元组已经成功通过拓扑之后，Storm调用此方法
     * 此方法的典型实现是，将对应数据从队列中移除并且阻止它被Replay
     *
     * @param msgId 元组的ID
     */
    @Override
    public void ack( Object msgId ) {
        LOGGER.debug( "Tuple with id {} acknowledged", msgId );
    }

    /**
     * 当此Spout释放出的具有指定ID的元组在某个环节处理失败后，Storm调用此方法
     * 此方法的典型实现是，将对应数据放回队列，以便后续Replay
     *
     * @param msgId 元组的ID
     */
    @Override
    public void fail( Object msgId ) {
        LOGGER.debug( "Tuple with id {} failed", msgId );
    }

    /**
     * 此方法来自IComponent接口，用于声明针对此组件的配置。仅topology.*之下的配置可以被覆盖
     * 当通过TopologyBuilder构建拓扑时，这里声明的配置可能被覆盖
     *
     * @return
     */
    @Override
    public Map&lt;String, Object&gt; getComponentConfiguration() {
        return null;
    }

    /**
     * 此方法来自IComponent接口，声明输出元组包含的字段
     */
    @Override
    public void declareOutputFields( OutputFieldsDeclarer declarer ) {
        // 为当前组件的默认输出流声明字段
        // 你还可以调用 declarer.declareStream( streamId,fields ) 声明新的输出流
        declarer.declare( new Fields( "message" ) );
    }
}</pre>
<div class="blog_h2"><span class="graybg">MessageParser</span></div>
<p>这是一个Bolt，读取监测值报文并解析之，释放多个监测值信息元组。 </p>
<p>任何Bolt都需要实现IRichBolt接口。你可以选择实现IBasicBolt（继承BaseBasicBolt），这样Storm会自动进行元组确认（Ack），使用IBasicBolt时你可以抛出FailedException表示当前元组处理失败。</p>
<pre class="crayon-plain-tag">package cc.gmem.study.storm.mv;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.lang3.mutable.MutableInt;
import org.apache.storm.task.OutputCollector;
import org.apache.storm.task.TopologyContext;
import org.apache.storm.topology.IRichBolt;
import org.apache.storm.topology.OutputFieldsDeclarer;
import org.apache.storm.tuple.Fields;
import org.apache.storm.tuple.Tuple;
import org.apache.storm.tuple.Values;
import org.slf4j.Logger;

import java.io.IOException;
import java.util.List;
import java.util.Map;

import static org.slf4j.LoggerFactory.getLogger;

public class MessageParser implements IRichBolt {

    private static final Logger LOGGER = getLogger( MessageParser.class );

    private ObjectMapper om;

    private OutputCollector collector;

    /**
     * 当前组件的任一个Task在集群的某个工作节点中初始化时，自动调用此方法
     * 类似Spout的open方法
     */
    @Override
    public void prepare( Map stormConf, TopologyContext context, OutputCollector collector ) {
        LOGGER.debug( "Bolt {}/{} preparing", context.getThisComponentId(), context.getThisTaskId() );
        om = new ObjectMapper();
        this.collector = collector;
    }

    /**
     * 处理单个输入元组，每当接收到新元组此方法都被调用
     * Tuple对象包含一些元数据信息：此元组来自什么component/stream/task
     * 调用Tuple#getValue可以获得元组的实际数据
     * &lt;p&gt;
     * Bolt不一定要立刻处理元组，例如你可能暂存它并在后续进行聚合或连接操作
     * 每个元组都需要在未来某个时刻被ack/fail，否则Storm无法确认Spout释放的原初元组释放已经被完整除了
     * 子接口IBasicBolt支持在execute()执行完毕后自动ack
     * &lt;p&gt;
     * 如果此Bolt要释放元组，需要调用prepare传入的OutputCollector
     *
     * @param input The input tuple to be processed.
     */
    @Override
    public void execute( Tuple input ) {
        // 一个Bolt获得的元组可能来自不同的流
        LOGGER.debug( "Received input tuple from {}", input.getSourceStreamId() );
        // 元组字段可以根据名称或者索引读取
        String msg = input.getStringByField( "message" );
        try {
            List&lt;Map&lt;String, Object&gt;&gt; mvs = om.readValue( msg, List.class );
            MutableInt count = new MutableInt( 0 );
            mvs.forEach( mv -&gt; {
                collector.emit( new Values( mv.get( "id" ), mv.get( "type" ), mv.get( "value" ) ) );
                count.increment();
            } );
            LOGGER.debug( "{} monitoring value parsed", count.intValue() );
        } catch ( IOException e ) {
            collector.fail( input );
        }
        collector.ack( input );
    }

    /**
     * 此Bolt即将被关闭时调用，此调用不保证一定发生
     * 类似Spout的close方法
     */
    @Override
    public void cleanup() {

    }

    @Override
    public void declareOutputFields( OutputFieldsDeclarer declarer ) {
        declarer.declare( new Fields( "id", "type", "value" ) );
    }

    @Override
    public Map&lt;String, Object&gt; getComponentConfiguration() {
        return null;
    }
}</pre>
<div class="blog_h3"><span class="graybg">MonitorValueCounter</span></div>
<p>这是一个Bolt，它读取MessageParser释放的监测值元组，然后进行实时统计分析。</p>
<pre class="crayon-plain-tag">package cc.gmem.study.storm.mv;

import org.apache.storm.task.TopologyContext;
import org.apache.storm.topology.BasicOutputCollector;
import org.apache.storm.topology.OutputFieldsDeclarer;
import org.apache.storm.topology.base.BaseBasicBolt;
import org.apache.storm.tuple.Tuple;
import org.slf4j.Logger;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Map;

import static java.math.BigDecimal.ONE;
import static java.math.BigDecimal.ZERO;
import static org.slf4j.LoggerFactory.getLogger;

public class MonitorValueCounter extends BaseBasicBolt {

    private static final Logger LOGGER = getLogger( MonitorValueCounter.class );

    BigDecimal[] counter;

    @Override
    public void prepare( Map stormConf, TopologyContext context ) {
        LOGGER.debug( "Bolt {}/{} preparing", context.getThisComponentId(), context.getThisTaskId() );
        counter = new BigDecimal[]{ ZERO /*计数*/, ZERO /*平均*/, BigDecimal.valueOf( Integer.MAX_VALUE ) /*最小*/, ZERO /*最大*/ };
    }

    @Override
    public void execute( Tuple input, BasicOutputCollector collector ) {
        String type = input.getStringByField( "type" );
        BigDecimal value = BigDecimal.valueOf( input.getDoubleByField( "value" ) );
        if ( counter[2].compareTo( value ) &gt; 0 ) counter[2] = value;
        if ( counter[3].compareTo( value ) &lt; 0 ) counter[3] = value;
        if ( counter[0].equals( ZERO ) ) {
            counter[0] = ONE;
            counter[1] = value;
        } else {
            BigDecimal newCount = counter[0].add( ONE );
            counter[1] = counter[1].multiply( counter[0] ).add( value ).divide( newCount, RoundingMode.HALF_EVEN );
            counter[0] = newCount;
        }
        LOGGER.debug( "Type: " + type + ", Count: {}, Avg: {}, Min: {}, Max: {}", counter );
    }

    @Override
    public void declareOutputFields( OutputFieldsDeclarer declarer ) {
    }
}</pre>
<div class="blog_h2"><span class="graybg">MonitorValueStatTopology</span></div>
<p>主程序，创建拓扑并提交到集群中执行：</p>
<pre class="crayon-plain-tag">package cc.gmem.study.storm.mv;


import org.apache.storm.Config;
import org.apache.storm.LocalCluster;
import org.apache.storm.generated.StormTopology;
import org.apache.storm.topology.TopologyBuilder;
import org.apache.storm.tuple.Fields;
import org.apache.storm.tuple.Values;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

public class MonitorValueStatTopology {

    public static void main( String[] args ) throws InterruptedException {
        TopologyBuilder builder = new TopologyBuilder();
        // 每个组件都具有一个ID，供其它组件引用，以消费此组件的输出
        builder.setSpout( "message-reader", new MessageReader() );

        builder.setBolt( "message-parser", new MessageParser() )
               .shuffleGrouping( "message-reader" ); // 消费message-reader的默认流，随机、均匀分发元组给当前Bolt

        builder.setBolt( "monitor-value-counter", new MonitorValueCounter() )
               // 设置并发度
               .setNumTasks( 3 )
               // 消费message-parser的默认流，根据输入元组的type字段分发到不同的Task
               // 如果Task数量不足，则type字段值不同的元组会被分发给同一个Task
               .fieldsGrouping( "message-parser", new Fields( "type" ) );

        Config conf = new Config();
        List&lt;Values&gt; msgSrcCfg = new ArrayList&lt;&gt;();
        msgSrcCfg.add( new Values( "温度", new Double( 0 ), new Double( 40 ) ) );
        msgSrcCfg.add( new Values( "湿度", new Double( 0 ), new Double( 100 ) ) );
        msgSrcCfg.add( new Values( "电压", new Double( 0 ), new Double( 360 ) ) );
        conf.put( "msgSrcCfg", msgSrcCfg );

        conf.setDebug( true );
        // 对于每一个Spout Task，处于未决状态的元组最大数量
        // 所谓未决，即元组尚未被ack/fail
        conf.put( Config.TOPOLOGY_MAX_SPOUT_PENDING, 1 );

        // 创建一个本地模式的Storm集群
        LocalCluster cluster = new LocalCluster();
        // 创建拓扑
        StormTopology topology = builder.createTopology();
        // 提交拓扑到集群
        cluster.submitTopology( "monitor-value-stat-topology", conf, topology );

        TimeUnit.SECONDS.sleep( 60 );

        // 关闭集群
        cluster.shutdown();
    }
}</pre>
<div class="blog_h1"><span class="graybg">拓扑</span></div>
<div class="blog_h2"><span class="graybg">并行性</span></div>
<p>Storm对于下述运行在集群中的拓扑中的实体：</p>
<ol>
<li>工作进程（Worker processes）</li>
<li>执行器（Executors，线程）</li>
<li>任务（Task）</li>
</ol>
<p>进行了明确的区分：</p>
<ol>
<li>Storm集群中的节点（机器）可以运行0-N个工作进程，这些工作进程可以属于1-N个拓扑。每个工作进程就是一个JVM，<span style="background-color: #c0c0c0;">它仅仅为单个拓扑运行</span>Executor。也就是说Worker不能被两个拓扑共享</li>
<li>单个Worker进程中可以运行1-N个Executor，每个Executor都是Worker进程产生的线程。每个Executor运行<span style="background-color: #c0c0c0;">单个拓扑的单个组件的1-N个Task</span>。也就是说Executor不能被两个组件共享</li>
<li>任务进行实际的数据处理，它执行Spout或者Bolt中的代码逻辑。它由Executor来执行</li>
</ol>
<p>可以看到：</p>
<ol>
<li>工作进程执行一个拓扑的子集，它只能属于一个拓扑</li>
<li>工作进程可以运行多个执行器线程，这些线程可以运行不同的组件。但是每个线程只能运行一种组件的任务</li>
<li>任务必须在执行器线程内部运行</li>
</ol>
<p>要对并行度进行配置，参考<a href="#parallelism-config">并发度配置</a>。</p>
<div class="blog_h2"><span class="graybg">流分组</span></div>
<p>流分组指定了如何在Bolt的Task之间进行流的分区（Partition）——流的每个元组应该由哪个Task处理。</p>
<p>在定义拓扑的时候，你可以为Bolt指定流分组，指定<span style="background-color: #c0c0c0;">流分组的同时也就指定了Bolt的数据源</span>。你可以针对一个bolt多次指定流分组，也就是指定多个输入流：</p>
<pre class="crayon-plain-tag">builder.setBolt( "monitor-value-counter", new MonitorValueCounter() )
       .fieldsGrouping( "message-parser", new Fields( "type" ) )
       // 为Bolt指定另一个输入：signals-reader组件的signals流
       .allGrouping("signals-reader","signals"); </pre>
<p>要实现自己的流分组，需要实现<a href="http://storm.apache.org/releases/1.1.1/javadocs/org/apache/storm/grouping/CustomStreamGrouping.html">CustomStreamGrouping</a>接口。通常情况下，你应该优先考虑使用Storm内置的流分组。</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>Shuffle</td>
<td>最常用的一种流分组。元组被随机的分发给Bolt的Task，此实现保证每个Task接受相等数量的元组</td>
</tr>
<tr>
<td>Fields</td>
<td>
<p>根据元组的字段值进行分组，字段值相同的元组被分发给同一个Task</p>
<p>可以基于1-N个字段的值进行分组</p>
</td>
</tr>
<tr>
<td>Partial Key </td>
<td>元组首先按照字段分组的方式被分区，并进一步的被两个下游的Bolt进行负载均衡</td>
</tr>
<tr>
<td>All</td>
<td>
<p>输入流的每个元组被复制到Bolt的所有实例（Task）</p>
<p>需要执行某种广播逻辑时，考虑使用这种流分组</p>
</td>
</tr>
<tr>
<td>Global</td>
<td>整个流被转发给单个具有最小Id的那个Task</td>
</tr>
<tr>
<td>None</td>
<td>表示不关心如何进行分组，当前此分组的行为和Shuffle类似。Storm在处理这种方式分组的流时，上游Spout/Bolt和下游Bolt使用同一执行线程</td>
</tr>
<tr>
<td>Direct</td>
<td>
<p>上游Spout/Bolt（生产者）直接决定元组分发给下游Bolt（消费者）的哪个Task</p>
<p>这种分组方式仅仅能应用到被声明为直接流（Direct Stream）的流上。要释放元组到直接流上，必须调用OutputCollector.emitDirect方法。获得消费者的Task Id途径可以是：</p>
<ol>
<li>通过TopologyContext获得</li>
<li>根据OutputCollector.emit方法的返回值获得，返回值是接受元组的Task 的Id</li>
</ol>
<p>示例代码：</p>
<pre class="crayon-plain-tag">// 获得某个下游组件的所有Task Id
Set&lt;Integer&gt; taskIds = context.getTaskToComponent( "downstream-bolt-id" ).keySet();
// 发送给目标Task
collector.emitDirect( taskId,tuple );</pre>
</td>
</tr>
<tr>
<td>Local or shuffle </td>
<td>如果下游Bolt在相同（与释放流的组件）工作进程（Work Process）中行运行着一个或者更多的Task，则元组被分发给这些进程内的Task。否则，行为与Shuffle grouping相同</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">自定义流分组</span></div>
<p>实现CustomStreamGrouping、Serializable接口：
<pre class="crayon-plain-tag">package cc.gmem.study.storm.mv;


import org.apache.storm.generated.GlobalStreamId;
import org.apache.storm.grouping.CustomStreamGrouping;
import org.apache.storm.task.WorkerTopologyContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.Serializable;
import java.util.List;

import static org.slf4j.LoggerFactory.getLogger;
// 实现Serializable接口，Storm需要跨JVM分发此对象
public class DeviceStreamGrouping implements CustomStreamGrouping, Serializable {

    private static final Logger LOGGER = getLogger( DeviceStreamGrouping.class );

    private List&lt;Integer&gt; targetTasks;

    /**
     * 初始化流分组实例，告知流分组必要的信息
     *
     * @param context     拓扑上下文
     * @param stream      此分组应用到的流
     * @param targetTasks 接收流的bolt的Task Id的列表
     */
    @Override
    public void prepare( WorkerTopologyContext context, GlobalStreamId stream, List&lt;Integer&gt; targetTasks ) {
        LOGGER.debug( "Apply grouping on stream {}.{}", stream.get_componentId(), stream.get_streamId() );
        this.targetTasks = targetTasks;
    }

    /**
     * 确定元组分发到哪个Task
     *
     * @param taskId 源组件产生当前元组的那个Task的Id
     * @param values 当前元组
     * @return 接受此元组的Task Id列表
     */
    @Override
    public List&lt;Integer&gt; chooseTasks( int taskId, List&lt;Object&gt; values ) {
        return targetTasks;
    }
}</pre>
<p>使用上述自定义流分组：</p>
<pre class="crayon-plain-tag">builder.setBolt(...).customGrouping( "message-parser", new DeviceStreamGrouping() );</pre>
<div class="blog_h2"><span class="graybg">提交拓扑</span></div>
<div class="blog_h3"><span class="graybg">LocalCluster</span></div>
<p>此类用于在当前JVM中建立一个本地模式的集群。通常在开发阶段使用，可以方便的对各种拓扑进行调试：</p>
<pre class="crayon-plain-tag">LocalCluster cluster = new LocalCluster();
StormTopology topology = builder.createTopology();
// 提交到本地集群
cluster.submitTopology( "topology-name", conf, topology );</pre>
<div class="blog_h3"><span class="graybg">StormSubmitter</span></div>
<p>此类用于将拓扑提交到一个远程Storm集群中运行。与本地模式不同，你不能对远程集群进行控制。</p>
<pre class="crayon-plain-tag">StormSubmitter.submitTopology("monitor-value-stat-topology", conf, builder.createTopology());</pre>
<p>下文有一个<a href="#submit-topology-to-remote-cluster">实际的例子</a>。</p>
<div class="blog_h3"><span class="graybg">storm命令</span></div>
<p>通过storm客户端命令，也可以将拓扑提交到远程集群。首先你需要打JAR包，然后执行：</p>
<pre class="crayon-plain-tag"># 提交执行，需要指定main方法及其参数，main方法调用了StormSubmitter.submitTopology
storm jar topology.jar cc.gmem.study.storm.mv.MonitorValueStatTopology arg0 arg1...
# 要终止提交的拓扑，需要传入其Id
storm kill monitor-value-stat-topology</pre>
<p>注意，JAR的入口函数必须调用submitTopology。</p>
<div class="blog_h2"><span class="graybg">拓扑模式</span></div>
<p>本节列出常见的拓扑模式（Pattern）。</p>
<div class="blog_h3"><span class="graybg">批处理</span></div>
<p>很多情况下，出于效率的考虑，你可能希望一批批的处理元组，而不是一个个的。例如，你可能希望批量的将元组入库，以利用数据库的吞吐能力。</p>
<p>你可以简单的将元组存储到实例变量中，等到积累的足够多了，批量的处理它们然后统一Ack。</p>
<p>如果执行批处理的Bolt释放元组，你可能需要使用多重锚定，确定输出元组和输入元组之间的因果关系。</p>
<div class="blog_h3"><span class="graybg">自动ACK</span></div>
<p>很多Bolt都是读取一个输入元组、处理后根据此元组释放0-N个元组，最后对输入元组进行Ack。基于IBasicBolt可以获得此模式的封装。</p>
<div class="blog_h3"><span class="graybg">内存缓存+字段分组</span></div>
<p>经过字段分组之后，该字段一样的元组总是由同一个Bolt处理。假设Bolt需要根据该字段进行某种转换，那么在Bolt实例变量中缓存这种转换结果，其缓存性能会较高。</p>
<p>例如，一个Bolt负责接收短URL并展开它，展开需要通过HTTP来调用第三方服务，因此缓存展开URL很有必要。可以在Bolt中创建一个短URL到展开URL的HashMap作为缓存。配合字段分组，让相同短URL总是分发给同一Bolt实例，这样上述缓存的命中率会比不分组高得多。</p>
<div class="blog_h3"><span class="graybg">释放TopN元组</span></div>
<p>某些情况下，你需要处理很多输入元组，并且释放出其中最xx的10个元组。</p>
<p>最简单的方案是，使用全局流分组，也就是让所有元组都输入到单个Bolt实例中。这种方案可能会导致性能瓶颈。</p>
<p>一个改进的方案是，让多个Bolt都进行TopN计算，在一个下游的Bolt中，重新进行一次TopN计算：</p>
<pre class="crayon-plain-tag">builder.setBolt("rank", new RankObjects(), parallelism)
    .fieldsGrouping("objects", new Fields("value"));  // 字段分组，每个实例进行自己的TopN计算
builder.setBolt("merge", new MergeObjects())
    .globalGrouping("rank");   // 全局分组，但是要处理的数据量很少，不会出现瓶颈</pre>
<div class="blog_h1"><span class="graybg">事务性拓扑</span></div>
<p>我们知道，基于ack/fail可以保证消息：</p>
<ol>
<li>要么完全通过拓扑</li>
<li>要么，可选的，进行Replay</li>
</ol>
<p>问题是，如果进行Replay时，如何保证事务性？或者说，如何保证某些逻辑不会重复执行。</p>
<p>从0.7开始，Storm引入了事务性拓扑，可以保证Replay安全的进行，确保它们仅仅被处理一次。</p>
<div class="blog_h2"><span class="graybg">简介</span></div>
<p>在事务性拓扑中，Storm混合使用并行、串行的元组处理流。Spout生成的一批元组，被并行的发送给Bolt处理。某些Bolt被称为Committer，它们按照严格有序的方式提交已经被处理过的元组批次。</p>
<p>例如，Spout释放两个元组批次，每个批次包含5个元组，这两个元组被并发的交付Bolt处理，但是，如果链条中存在Committer Bolt，则在此Bolt批次必须严格有序的串行化 —— 它不会在第一个批次成功提交之前，提交第二个批次。</p>
<p>Storm事务由两个部分阶段组成：</p>
<ol>
<li>处理阶段：完全并行阶段，很多批次可以被同时的执行</li>
<li>提交阶段：完全有序阶段，保证批次按照顺序逐一执行</li>
</ol>
<div class="blog_h2"><span class="graybg">实例</span></div>
<p>我们以一个Twitter分析工具的例子来了解事务性拓扑的工作机制。此工具的逻辑如下：</p>
<ol>
<li>读取Tweets并存储到Redis</li>
<li>通过若干Bolt处理Tweets</li>
<li>将处理结果存放到另一个Redis数据库，处理结果包括：所有主题标签（Hashtag）的及其出现次数、用户及其出现在Tweet中的次数、主题标签和用户出现在同一Tweet中的次数</li>
</ol>
<p>用于构建此工具的拓扑示意如下：</p>
<p><img class="aligncenter size-full wp-image-16604" src="https://blog.gmem.cc/wp-content/uploads/2017/07/twt-ana-tool.png" alt="twt-ana-tool" width="820" height="420" /></p>
<p>说明如下：</p>
<ol>
<li>TweetsTransactionalSpout连接到数据源，读取并释放元组批次到拓扑</li>
<li>随后的两个Bolt会接受所有元组
<ol>
<li> UserSplitterBolt，从Tweet中搜索用户（@之后的单词）</li>
<li>HashatagSplitter，从Tweet中搜索主题标签（#之后的单词）</li>
</ol>
</li>
<li>后面的UserHashtagJoinBolt，对前面两个Bolt的结果进行Join，获得每个用户+标签的组合同时出现的次数。此Bolt是BaseBatchBolt的子类型</li>
<li>最后的RedisCommitterBolt是一个Committer，它接受3个流。进行各种统计，并在处理完一批次后进行入库</li>
</ol>
<div class="blog_h3"><span class="graybg">源码</span></div>
<p>相关说明已经附加在源码注释：</p>
<pre class="crayon-plain-tag">package cc.gmem.study.storm.twt;

import org.apache.storm.coordination.BatchOutputCollector;
import org.apache.storm.task.TopologyContext;
import org.apache.storm.topology.BasicOutputCollector;
import org.apache.storm.topology.OutputFieldsDeclarer;
import org.apache.storm.topology.base.BaseBasicBolt;
import org.apache.storm.topology.base.BaseBatchBolt;
import org.apache.storm.topology.base.BaseTransactionalBolt;
import org.apache.storm.topology.base.BaseTransactionalSpout;
import org.apache.storm.transactional.ICommitter;
import org.apache.storm.transactional.ITransactionalSpout;
import org.apache.storm.transactional.TransactionAttempt;
import org.apache.storm.transactional.TransactionalTopologyBuilder;
import org.apache.storm.tuple.Fields;
import org.apache.storm.tuple.Tuple;
import org.apache.storm.tuple.Values;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;

import java.io.Serializable;
import java.math.BigInteger;
import java.util.*;

public class TwitterAnalyticsTool {

    /* 封装Redis相关操作 */
    public static class RQ {

        // 存放下一个读取游标的键
        public static final String NEXT_READ = "NEXT_READ";

        // 存放下一个写入游标的键
        public static final String NEXT_WRITE = "NEXT_WRITE";

        Jedis jedis;

        public RQ() {
            jedis = new Jedis( "localhost" );
        }

        /* 可读Tweet量 */
        public long getAvailableToRead( long current ) {
            return getNextWrite() - current;
        }

        public long getNextRead() {
            String sNextRead = jedis.get( NEXT_READ );
            if ( sNextRead == null )
                return 1;
            return Long.valueOf( sNextRead );
        }

        public long getNextWrite() {
            return Long.valueOf( jedis.get( NEXT_WRITE ) );
        }

        public void close() {
            jedis.disconnect();
        }

        public void setNextRead( long nextRead ) {
            jedis.set( NEXT_READ, "" + nextRead );
        }

        /* 从指定游标from开始，读取quantity条Tweet */
        public List&lt;String&gt; getMessages( long from, int quantity ) {
            String[] keys = new String[quantity];
            for ( int i = 0; i &lt; quantity; i++ ) keys[i] = "" + ( i + from );
            return jedis.mget( keys );
        }
    }

    /**
     * 事务元数据，释放元组批次时用到此元数据：
     * from 事务的起始游标
     * quantity 事务处理Tweet的数量
     * &lt;p&gt;
     * 元数据必须包含Replay此事务所牵涉到的批次的所需要的全部信息
     */
    public static class TransactionMetadata implements Serializable {

        private static final long serialVersionUID = 1L;

        long from;

        int quantity;

        public TransactionMetadata( long from, int quantity ) {
            this.from = from;
            this.quantity = quantity;
        }
    }

    /**
     * 协调器，整个拓扑中仅仅有单个协调器实例
     */
    public static class TweetsTransactionalSpoutCoordinator implements ITransactionalSpout.Coordinator&lt;TransactionMetadata&gt; {

        private static final long MAX_TRANSACTION_SIZE = 16;

        RQ rq = new RQ();

        long nextRead = 0;

        public TweetsTransactionalSpoutCoordinator() {
            nextRead = rq.getNextRead();
        }

        /**
         * 根据上一次事务的元数据，确定本次事务的元数据
         * &lt;p&gt;
         * 元数据必须包含Replay此事务所牵涉到的批次的所需要的全部信息
         * &lt;p&gt;
         * 元数据被存放在ZooKeeper中，以txid为键。如果事务失败，Storm可以通过Emtter重放此事务
         *
         * @param txid         本次事务标识符，此事务从未被提交过
         * @param prevMetadata 上一个事务的元数据
         * @return 本次事务的元数据
         */
        @Override
        public TransactionMetadata initializeTransaction( BigInteger txid, TransactionMetadata prevMetadata ) {
            long quantity = rq.getAvailableToRead( nextRead );
            quantity = quantity &gt; MAX_TRANSACTION_SIZE ? MAX_TRANSACTION_SIZE : quantity;
            TransactionMetadata ret = new TransactionMetadata( nextRead, (int) quantity );
            nextRead += quantity;
            return ret;
        }

        /**
         * 如果可以发起新事务，返回true。如果不可以，应当睡眠一段时间然后返回false
         * 如果此方法返回true，initializeTransaction()被调用
         *
         * @return
         */
        @Override
        public boolean isReady() {
            return rq.getAvailableToRead( nextRead ) &gt; 0;
        }

        @Override
        public void close() {
            rq.close();
        }
    }

    /**
     * 发射器，根据事务元数据，从数据源读取一批次数据，并释放到流中
     */
    public static class TweetsTransactionalSpoutEmitter implements ITransactionalSpout.Emitter&lt;TransactionMetadata&gt; {

        RQ rq = new RQ();

        /**
         * 释放一个批次，此方法的实现一定要能够重复执行 ———— 根据txId、txMeta必须可以重新释放此批次
         *
         * @param tx        当前事务尝试
         * @param txMeta    事务元数据
         * @param collector 用于释放元组
         */
        @Override
        public void emitBatch( TransactionAttempt tx, TransactionMetadata txMeta, BatchOutputCollector collector ) {
            // 移动读游标
            if ( tx.getAttemptId() == 0 ) { // attempt id存放当前事务重复的次数，由此可以推断是否Replay
                rq.setNextRead( txMeta.from + txMeta.quantity );
            }
            // 读取一批消息
            List&lt;String&gt; messages = rq.getMessages( txMeta.from, txMeta.quantity );
            long tweetId = txMeta.from;
            // 下面释放的消息都在批次中
            for ( String message : messages ) {
                collector.emit( new Values( tx, String.valueOf( tweetId ), message ) );
                tweetId++;
            }
        }

        @Override
        public void cleanupBefore( BigInteger txid ) {
        }

        @Override
        public void close() {
            rq.close();
        }
    }

    /**
     * 和普通的Spout不同，事务性Spout扩展自泛型接口BaseTransactionalSpout
     */
    public static class TweetsTransactionalSpout extends BaseTransactionalSpout&lt;TransactionMetadata&gt; {

        /* 指定用来协调批次生成的协调器 */
        @Override
        public Coordinator&lt;TransactionMetadata&gt; getCoordinator( Map conf, TopologyContext context ) {
            return new TweetsTransactionalSpoutCoordinator();
        }

        /* 指定发射器，负责读取源中的一批元组，并释放到流中 */
        @Override
        public Emitter&lt;TransactionMetadata&gt; getEmitter( Map conf, TopologyContext context ) {
            return new TweetsTransactionalSpoutEmitter();
        }

        @Override
        public void declareOutputFields( OutputFieldsDeclarer declarer ) {
            declarer.declare( new Fields( "txid", "tweet_id", "tweet" ) );
        }
    }

    /* 针对当前Tweet中每个@的用户，释放一个 tweetId,userId 元组 */
    public static class UserSplitterBolt extends BaseBasicBolt {

        private static final long serialVersionUID = 1L;

        @Override
        public void execute( Tuple input, BasicOutputCollector collector ) {
            String tweetId = input.getStringByField( "tweet_id" );
            String tweet = input.getStringByField( "tweet" );
            StringTokenizer strTok = new StringTokenizer( tweet, " " );
            TransactionAttempt tx = (TransactionAttempt) input.getValueByField( "txid" );
            HashSet&lt;String&gt; users = new HashSet&lt;String&gt;();
            while ( strTok.hasMoreTokens() ) {
                String user = strTok.nextToken();
                // 如果一个Tweet中，用户出现两次，仅仅计算一次
                if ( user.startsWith( "@" ) &amp;&amp; !users.contains( user ) ) {
                    // 释放到users流
                    collector.emit( "users", new Values( tx, tweetId, user ) );
                    users.add( user );
                }
            }
        }

        @Override
        public void declareOutputFields( OutputFieldsDeclarer declarer ) {
            // 声明非默认流
            declarer.declareStream( "users", new Fields( "txid", "tweet_id", "user" ) );
        }
    }

    /* 针对当前Tweet中每个主题标签，释放一个 tweetId,hashTag 元组 */
    public static class HashtagSplitterBolt extends BaseBasicBolt {

        @Override
        public void execute( Tuple input, BasicOutputCollector collector ) {
            String tweet = input.getStringByField( "tweet" );
            String tweetId = input.getStringByField( "tweet_id" );
            StringTokenizer strTok = new StringTokenizer( tweet, " " );
            TransactionAttempt tx = (TransactionAttempt) input.getValueByField( "txid" );
            HashSet&lt;String&gt; words = new HashSet&lt;String&gt;();
            while ( strTok.hasMoreTokens() ) {
                String word = strTok.nextToken();
                if ( word.startsWith( "#" ) &amp;&amp; !words.contains( word ) ) {
                    collector.emit( "hashtags", new Values( tx, tweetId, word ) );
                    words.add( word );
                }
            }
        }

        @Override
        public void declareOutputFields( OutputFieldsDeclarer declarer ) {
            // 声明非默认流
            declarer.declareStream( "hashtags", new Fields( "txid", "tweet_id", "hashtag" ) );
        }
    }

    /**
     * UserHashtagJoinBolt是一个BaseBatchBolt，这意味着：
     * 1、execute处理接收到的元组，但是不释放任何新元组
     * 2、在本批次的元组处理完毕后，Storm自动调用finishBatch
     */
    public static class UserHashtagJoinBolt extends BaseBatchBolt {

        /* 每个Tweet都有哪些Tag */
        private Map&lt;String, Set&lt;String&gt;&gt; tweetHashtags;

        /* 每个用户都被哪些Tweet @ */
        private Map&lt;String, Set&lt;String&gt;&gt; userTweets;

        private BatchOutputCollector collector;

        private Object id;

        private void add( Map&lt;String, Set&lt;String&gt;&gt; map, String key, String val ) {
            Set&lt;String&gt; vals = map.get( key );
            if ( vals == null ) {
                vals = new HashSet&lt;&gt;();
                map.put( key, vals );
            }
            vals.add( val );
        }

        /**
         * 对于每一个事务，都会调用此方法
         *
         * @param id 事务标识符
         */
        @Override
        public void prepare( Map conf, TopologyContext context, BatchOutputCollector collector, Object id ) {
            tweetHashtags = new HashMap&lt;&gt;();
            userTweets = new HashMap&lt;&gt;();
            this.collector = collector;
            this.id = id;
        }

        /* 生成两个映射关系 */
        @Override
        public void execute( Tuple tuple ) {
            String source = tuple.getSourceStreamId();
            String tweetId = tuple.getStringByField( "tweet_id" );
            if ( "hashtags".equals( source ) ) {
                String hashtag = tuple.getStringByField( "hashtag" );
                add( tweetHashtags, tweetId, hashtag );
            } else if ( "users".equals( source ) ) {
                String user = tuple.getStringByField( "user" );
                add( userTweets, user, tweetId );
            }
        }

        /* 处理完当前批次后调用。进行JOIN操作，得到每个用户和每个主题标签一起被提及的频率 */
        @Override
        public void finishBatch() {
            for ( String user : userTweets.keySet() ) {
                Set&lt;String&gt; tweets = getUserTweets( user );
                HashMap&lt;String, Integer&gt; hashtagsCounter = new HashMap&lt;String, Integer&gt;();
                for ( String tweet : tweets ) {
                    Set&lt;String&gt; hashtags = getTweetHashtags( tweet );
                    if ( hashtags != null ) {
                        for ( String hashtag : hashtags ) {
                            Integer count = hashtagsCounter.get( hashtag );
                            if ( count == null ) count = 0;
                            count++;
                            hashtagsCounter.put( hashtag, count );
                        }
                    }
                }
                for ( String hashtag : hashtagsCounter.keySet() ) {
                    int count = hashtagsCounter.get( hashtag );
                    collector.emit( new Values( id, user, hashtag, count ) );
                }
            }
        }

        private Set&lt;String&gt; getTweetHashtags( String tweet ) {
            return tweetHashtags.get( tweet );
        }

        private Set&lt;String&gt; getUserTweets( String user ) {
            return userTweets.get( user );
        }

        @Override
        public void declareOutputFields( OutputFieldsDeclarer declarer ) {
            declarer.declare( new Fields( "txid", "user", "hashtag", "count" ) );
        }
    }

    /**
     * RedisCommiterCommiterBolt是一个Committer，这是由标记性接口ICommitter指定的。
     * 你也可以TransactionalTopologyBuilder.setCommiterBolt将一个Bolt设置为Committer
     * &lt;p&gt;
     * 和普通BaseBatchBolt不同之处是，Committer的finishBatch方法仅仅在可以提交的时候才执行，也就是说
     * 只有当前面的所有事务均成功提交之后，Committer才会提交当前事务。
     * &lt;p&gt;
     * 同一拓扑中所有事务，在Committer都是串行执行的
     */
    public static class RedisCommiterCommiterBolt extends BaseTransactionalBolt implements ICommitter {

        public static final String LAST_COMMITED_TRANSACTION_FIELD = "LAST_COMMIT";

        TransactionAttempt id;

        BatchOutputCollector collector;

        Jedis jedis;

        HashMap&lt;String, Long&gt; hashtags = new HashMap&lt;String, Long&gt;();

        HashMap&lt;String, Long&gt; users = new HashMap&lt;String, Long&gt;();

        HashMap&lt;String, Long&gt; usersHashtags = new HashMap&lt;String, Long&gt;();

        private void count( HashMap&lt;String, Long&gt; map, String key, int count ) {
            Long value = map.get( key );
            if ( value == null ) value = (long) 0;
            value += count;
            map.put( key, value );
        }

        @Override
        public void prepare( Map conf, TopologyContext context, BatchOutputCollector collector, TransactionAttempt id ) {
            this.id = id;
            this.collector = collector;
            this.jedis = new Jedis( "localhost" );
        }

        @Override
        public void execute( Tuple tuple ) {
            String origin = tuple.getSourceComponent();
            if ( "users-splitter".equals( origin ) ) {
                String user = tuple.getStringByField( "user" );
                count( users, user, 1 );
            } else if ( "hashtag-splitter".equals( origin ) ) {
                String hashtag = tuple.getStringByField( "hashtag" );
                count( hashtags, hashtag, 1 );
            } else if ( "user-hashtag-merger".equals( origin ) ) {
                String hashtag = tuple.getStringByField( "hashtag" );
                String user = tuple.getStringByField( "user" );
                String key = user + ":" + hashtag;
                Integer count = tuple.getIntegerByField( "count" );
                count( usersHashtags, key, count );
            }
        }

        /**
         * 一定要记住：保存上一次事务的ID，在执行提交时，再次检查此ID是否和当前事务ID重复
         *
         * 这样可以防止Replay时，入库逻辑被重复执行
         */
        @Override
        public void finishBatch() {
            // 获得上一次事务ID
            String lastCommitedTransaction = jedis.get( LAST_COMMITED_TRANSACTION_FIELD );
            String currentTransaction = String.valueOf( id.getTransactionId() );
            // 当前事务已经提交过，直接返回
            if ( currentTransaction.equals( lastCommitedTransaction ) ) return;
            // 开启Redis事务
            Transaction multi = jedis.multi();
            // 更新上一次事务ID
            multi.set( LAST_COMMITED_TRANSACTION_FIELD, currentTransaction );
            Set&lt;String&gt; keys = hashtags.keySet();
            // 更新统计信息
            for ( String hashtag : keys ) {
                Long count = hashtags.get( hashtag );
                multi.hincrBy( "hashtags", hashtag, count );
            }
            keys = users.keySet();
            for ( String user : keys ) {
                Long count = users.get( user );
                multi.hincrBy( "users", user, count );
            }
            keys = usersHashtags.keySet();
            for ( String key : keys ) {
                Long count = usersHashtags.get( key );
                multi.hincrBy( "users_hashtags", key, count );
            }
            multi.exec();
        }

        @Override
        public void declareOutputFields( OutputFieldsDeclarer declarer ) {

        }

    }

    @SuppressWarnings( "deprecation" )
    public static void main( String[] args ) {
        // 可以使用TransactionalTopologyBuilder构建事务性拓扑，目前此类的功能已经被Trident代替
        String topoId = "twitter-analytics-tool";  // 拓扑的标识符，Storm基于此标识符在ZooKeeper中存储事务性Spout的状态
        String spoutId = "spout";                  // Spout的标识符
        TransactionalTopologyBuilder builder = new TransactionalTopologyBuilder( topoId, spoutId, new TweetsTransactionalSpout() );
        builder.setBolt( "users-splitter", new UserSplitterBolt(), 4 ).shuffleGrouping( "spout" );
        builder.setBolt( "hashtag-splitter", new HashtagSplitterBolt(), 4 ).shuffleGrouping( "spout" );
        builder.setBolt( "user-hashtag-merger", new UserHashtagJoinBolt(), 4 )
               .fieldsGrouping( "users-splitter", "users", new Fields( "tweet_id" ) )
               .fieldsGrouping( "hashtag-splitter", "hashtags", new Fields( "tweet_id" ) );
        builder.setBolt( "redis-committer", new RedisCommiterCommiterBolt() )
               .globalGrouping( "users-splitter", "users" )
               .globalGrouping( "hashtag-splitter", "hashtags" )
               .globalGrouping( "user-hashtag-merger" );
    }
}</pre>
<p>事务性拓扑提供了一种可靠的批量处理语义，其关键在于：</p>
<ol>
<li>提供了组装元组批次，也就是事务的机制，并且信息持久化在ZooKeeper中，不会因为宕机而丢失</li>
<li>提供了串行有序执行语义，你只需要记录一个事务标识符 —— 上一个事务的标识符，因为任何时刻只会有一个事务正在准备提交</li>
</ol>
<div class="blog_h1"><span class="graybg">Streams</span></div>
<p>流是连续不断的元组序列。任何Spout、Bolt都可以释放0-N个流。</p>
<div class="blog_h2"><span class="graybg">流连接</span></div>
<p>通过<pre class="crayon-plain-tag">JoinBolt</pre>，Storm支持将多个流合并为一个。JoinBolt是一个窗口化（Windowed）的Bolt，它会等待进行合并的多个流的Window duration匹配，确保流依据窗口边界对齐。</p>
<p>每个参与合并的流，必须是根据Join字段进行分fieldsGrouping，也就是说，Join字段一样的元组必须由同一Task处理。</p>
<div class="blog_h3"><span class="graybg">执行连接</span></div>
<p>考虑如下SQL语句：</p>
<pre class="crayon-plain-tag">select  userId, key4, key2, key3
from        table1
inner join  table2  on table2.userId =  table1.key1
inner join  table3  on table3.key3   =  table2.userId
left join   table4  on table4.key4   =  table3.key3</pre>
<p>对应的流连接如下：</p>
<pre class="crayon-plain-tag">JoinBolt jbolt =  new JoinBolt("spout1", "key1")                   // from        spout1  
                    //         我组件       我键       他组件
                    .join     ("spout2", "userId",  "spout1")      // inner join  spout2  on spout2.userId = spout1.key1
                    .join     ("spout3", "key3",    "spout2")      // inner join  spout3  on spout3.key3   = spout2.userId   
                    .leftJoin ("spout4", "key4",    "spout3")      // left join   spout4  on spout4.key4   = spout3.key3
                    // 选择输出字段
                    .select  ("userId, key4, key2, spout3:key3")   // chose output fields
                    // 滚动窗口，时长10分钟
                    .withTumblingWindow( new Duration(10, TimeUnit.MINUTES) ) ;
                    // 你也可以调用withWindow()配置滑动窗口

topoBuilder.setBolt("joiner", jbolt, 1)
            // 参与连接的流必须以Join字段分组
            .fieldsGrouping("spout1", new Fields("key1") )
            .fieldsGrouping("spout2", new Fields("userId") )
            .fieldsGrouping("spout3", new Fields("key3") )
            .fieldsGrouping("spout4", new Fields("key4") );</pre>
<div class="blog_h3"><span class="graybg">组件名称和连接顺序</span></div>
<p>在Join某个流之前，你必须先引入它： </p>
<pre class="crayon-plain-tag">new JoinBolt( "spout1", "key1") 
      // arg0引入              arg2连接到                
  .join( "spout2", "userId",  "spout3") // 错误：spout3尚未引入
  .join( "spout3", "key3",    "spout1");</pre>
<p>实际Join发生的顺序，就是用户声明的顺序。</p>
<div class="blog_h3"><span class="graybg">使用流而非组件名称</span></div>
<p>上面流连接的例子中，都使用了组件名称，而不是流名称。Storm支持基于流名称连接，但是很多组件仅仅释放一个流，且流名称默认为default，要基于流名称连接，必须先定义好命名流。</p>
<pre class="crayon-plain-tag">// 提示此JoinBolt基于流名称而非组件名称引用参与连接的流
new JoinBolt(JoinBolt.Selector.STREAM,  "stream1", "key1").join("stream2", "key2");
topoBuilder.setBolt("joiner", jbolt, 1)
    //              配置流分组时要指定流的名称
    .fieldsGrouping("bolt1", "stream1", new Fields("key1") )</pre>
<div class="blog_h3"><span class="graybg">注意事项</span></div>
<ol>
<li>目前仅仅支持内连接（INNER）和左连接（LEFT）</li>
<li>SQL支持针对一个表，通过多个字段分别Join。Storm不支持，每个流有且仅有一个字段能用于Join，该字段用于流分组，确保键相同的分组发送给同一Task</li>
<li>要使用多字段连接，你需要将它们合并为单个字段</li>
<li>连接操作可能占用很高的CPU和内存。当前窗口中累积的数据越多，则连接消耗的时间越长。使用滑动窗口时，越短的滑动间隔导致越频繁的连接操作。因此太长的窗口、太短的滑动间隔都可能导致性能问题</li>
<li>使用滑动窗口时，多个窗口之间可能存在重复的Joined记录，这是因为这些记录的源元组可能跨越多个窗口存在</li>
<li>如果使用消息超时，应当正确设置topology.message.timeout.sec，确保它能够匹配窗口大小，同时考虑拓扑中其它组件可能消耗的时间</li>
<li>连接两个流时，假设窗口大小分别为M、N，那么最坏的情况下可能产生M x N个输出元组，下游Bolt将会释放更多的ACK。这种情况下，Storm的消息子系统可能面临很大的压力，不小心使用可能导致拓扑运行缓慢。下面是管理消息子系统的一些建议：
<ol>
<li>增加Worker的堆大小：<pre class="crayon-plain-tag">topology.worker.max.heap.size.mb</pre></li>
<li>如果拓扑不需要ACK，可以禁用它：<pre class="crayon-plain-tag">topology.acker.executors=0</pre></li>
<li>禁用事件记录器：<pre class="crayon-plain-tag">topology.eventlogger.executor=0</pre></li>
<li>禁用拓扑调试功能：<pre class="crayon-plain-tag">topology.debug=false</pre></li>
<li><pre class="crayon-plain-tag">topology.max.spout.pending</pre>设置要匹配完整窗口需要的大小，附加额外的一些空间。这可以避免消息子系统过载时Spout仍然不停释放元组，该项设置为null时更容易发生此情况</li>
<li>在能够满足需求的情况下，窗口应该尽量的小</li>
</ol>
</li>
</ol>
<div class="blog_h1"><span class="graybg">Spouts</span></div>
<p>本章主要讨论设计拓扑入口点 —— Spout的通用策略，以及如何让Spout支持容错。</p>
<div class="blog_h2"><span class="graybg">消息可靠性</span></div>
<p>在设计拓扑时，考虑消息可靠性需求是一个重要事项。当一个消息无法完成处理时，你需要决定针对此消息进行怎样的处理？整个拓扑有应该有怎样的行为。打个比方，在处理银行存款业务时，每个交易消息都不能丢失；单是在处理百万级数据的统计信息时，丢失一部分消息则不会对结果精确性产生太大影响。</p>
<p>在Storm中，拓扑的设计者负责保证消息的可靠性。这常常需要权衡考虑，因为拓扑需要更多的资源以管理消息不丢失。</p>
<p>你可以在释放元组时为其指定标识符：</p>
<pre class="crayon-plain-tag">// 此重载版本为SpoutOutputCollector特有，Bolt使用的OutputCollector没有此方法
collector.emit(new Values(...),tupleId)</pre>
<p>如果不指定标识符，Storm会自动生成一个。在消费者中可以调用<pre class="crayon-plain-tag">input.getMessageId()</pre>获得元组的Id。</p>
<p>当消费者针对元组进行ack/fail操作时，生产者的ack/fail回调会被调用，并传入确认/失败的那个元组。至于如何处理失败的元组，需要根据具体业务场景决定，常见的做法是放回消息队列。</p>
<p>引起元组失败的原因有两种：</p>
<ol>
<li>消费者调用<pre class="crayon-plain-tag">collector.fail(tuple)</pre></li>
<li>元组处理的时间超过配置的时限，<pre class="crayon-plain-tag">Config.TOPOLOGY_MESSAGE_TIMEOUT_SECS</pre></li>
</ol>
<div class="blog_h2"><span class="graybg">接入数据源</span></div>
<div class="blog_h3"><span class="graybg">直接连接</span></div>
<p>最简单的接入方式是Spout直接连接到数据源（Message Emitter）。如果数据源是明确的（Well-known）的设备（在这里是一个抽象概念，理解为一般性的数据源即可）或者设备组，这种方式实现起来很简单。</p>
<p>所谓明确的，是指在拓扑启动时设备就就是已知的（Known）并且在拓扑运行期间它保持不变。所谓未知设备，就是在拓扑运行之后才添加进来的。</p>
<p>所谓明确设备组，就是其中所有设备都是已知的一组设备。</p>
<p>下面是一个基于Twitter流API的拓扑，它使用直接连接的接入方式：</p>
<pre class="crayon-plain-tag">package cc.gmem.study.storm.twt;

import com.esotericsoftware.kryo.util.IdentityMap;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.storm.shade.org.apache.http.HttpResponse;
import org.apache.storm.shade.org.apache.http.StatusLine;
import org.apache.storm.shade.org.apache.http.client.CredentialsProvider;
import org.apache.storm.shade.org.apache.http.client.methods.HttpGet;
import org.apache.storm.shade.org.apache.http.impl.client.DefaultHttpClient;
import org.apache.storm.spout.SpoutOutputCollector;
import org.apache.storm.task.TopologyContext;
import org.apache.storm.topology.OutputFieldsDeclarer;
import org.apache.storm.topology.base.BaseRichSpout;
import org.apache.storm.tuple.Fields;
import org.apache.storm.tuple.Values;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Map;

public class TwitterStreamSpout extends BaseRichSpout {

    private static final String STREAMING_API_URL = "...";

    private CredentialsProvider credentialProvider;

    private String track;

    private ObjectMapper om;

    private SpoutOutputCollector collector;

    @Override
    public void open( Map conf, TopologyContext context, SpoutOutputCollector collector ) {
        // 任何组件都可以访问拓扑上下文，其中包含了拓扑的所有全局性信息
        // 获得当前Spout的实例数量
        int spoutsSize = context.getComponentTasks( context.getThisComponentId() ).size();
        // 获得当前Spoput实例的Id
        int myIdx = context.getThisTaskIndex();
        String[] tracks = ( (String) conf.get( "track" ) ).split( "," );
        StringBuffer tracksBuffer = new StringBuffer();
        for ( int i = 0; i &lt; tracks.length; i++ ) {
            // 让各Spout实例处理不同的track关键字
            if ( i % spoutsSize == myIdx ) {
                tracksBuffer.append( "," );
                tracksBuffer.append( tracks[i] );
            }
        }
        this.track = tracksBuffer.substring( 1 );
        this.collector = collector;
    }

    @Override
    public void nextTuple() {
        // 通过HTTP获取消息
        DefaultHttpClient client = new DefaultHttpClient();
        client.setCredentialsProvider( credentialProvider );
        // 让每个Spout实例跟踪不同的track关键字
        HttpGet get = new HttpGet( STREAMING_API_URL + this.track );
        HttpResponse response;
        try {
            response = client.execute( get );
            StatusLine status = response.getStatusLine();
            if ( status.getStatusCode() == 200 ) {
                InputStream inputStream = response.getEntity().getContent();
                BufferedReader reader = new BufferedReader( new InputStreamReader( inputStream ) );
                String in;
                // 逐行读取
                while ( ( in = reader.readLine() ) != null ) {
                    // 解析并释放元组
                    Object json = om.readValue( in, Map.class );
                    collector.emit( new Values( this.track, json ) );
                }
            }
        } catch ( IOException e ) {
            // 连接到Twitter API失败，可以休眠一段时间后重试
        }
    }


    @Override
    public void declareOutputFields( OutputFieldsDeclarer declarer ) {
        declarer.declare( new Fields( "track", "content" ) );
    }
}</pre>
<p>注意上面的例子中用到Storm的一个重要特性 —— 在任何组件中访问TopologyContext。利用TopologyContext你可以感知到Spout的Task数量，然后将不同数据源映射到不同的Task，增加拓扑的并行度。</p>
<p>上面是连接到明确的设备的例子，通过类似的方式，可以直接连接到未知设备。但是你需要一个协调（Coordinator）系统来维护可用设备的列表。例如，当进行Web服务器日志分析时，Web服务器的列表可能动态变化。当一个新的Web服务器加入后，Coordinator检测到并为其创建一个Spout。</p>
<p>当建立连接时，应当总是由Spout发起，而不是由数据源发起。因为运行Spout的服务器宕机后Storm可能在另外一台机器上重启此Spout，数据源难以精确的定位到Spout。</p>
<div class="blog_h3"><span class="graybg">消息列队</span></div>
<p>除了直接连接，你还可以使用消息队列系统。Spout连接到消息队列读取消息，数据源则把消息发布到消息队列。</p>
<p>消息中间件软件通常都提供可靠性、持久化等保证，这简化了开发任务。使用消息中间件后Spout对数据源毫无感知。</p>
<p>使用消息列队这种接入方式时，如果想增加Spout并行度，可以：</p>
<ol>
<li>让Spout循环轮询（Round-robin Poll）同一队列</li>
<li>基于哈希再分发：根据哈希值将消息分发给不同Spout，或者分发到不同的子队列。Spout读取子队列。Kafka是一个很好的支持子队列（分区）的消息中间件</li>
</ol>
<div class="blog_h1"><span class="graybg">Bolts</span></div>
<p>Bolt是拓扑中的关键组件，大部分逻辑都发生在Bolt链中。</p>
<div class="blog_h2"><span class="graybg">生命周期</span></div>
<p>Bolt这类组件，以元组为输入，可选的，它也能够输出元组。</p>
<p>在创建Bolt时，你需要实现IRichBolt接口。Bolt在客户端创建，然后串行化到拓扑，最后整个拓扑被提交到Storm集群的Master节点。集群会启动工作节点（JVM进程）来运行拓扑组件，Bolt会被反串行化，其prepare方法会被调用，然后开始接受元组。</p>
<div class="blog_h2"><span class="graybg">可靠性</span></div>
<p>Storm确保Spout释放的所有元组都能传递给相应的Bolt，至于Bolt是否保证信息堡丢失，由开发人员决定。</p>
<p>拓扑是组件构成的网络，Spout释放的元组，是网络中传递的信息。信息经过不同组件之后，可能分裂、合并，这和Bolt灵活的Tuple处理机制有关：</p>
<ol>
<li>可以根据一个输入元组产生多个输出元组</li>
<li>可以根据多个输入元组产生一个输出元组</li>
</ol>
<p>从框架角度来说，无法自动推导输入、输出元组的对应关系。因此在Storm中你需要利用锚定（Anchoring）技术手工指定。</p>
<p>以从Spout发出的原初元组为根，其衍生的任何元组被fail，则此原初元组也就fail，发起此元组的Spout的fail回调自动被调用。</p>
<div class="blog_h2"><span class="graybg">多个流</span></div>
<p>每个Spout/Bolt都可以声明多个输出流，标识符为default的默认流自动声明。要声明新的流，可以调用：</p>
<pre class="crayon-plain-tag">declarer.declareStream( streamId, fields );</pre>
<p>要向指定的流释放元组，调用：</p>
<pre class="crayon-plain-tag">collector.emit( streamId, tuple );</pre>
<div class="blog_h2"><span class="graybg">Anchoring</span></div>
<p>前面提到过，要追踪信息流动方向，需要使用锚定技术。也就是指定输入、输出元组之间的关联关系：</p>
<pre class="crayon-plain-tag">// 此重载版本为OutputCollector特有
collector.emit( inputTuple, outputTuple );</pre>
<p>有了这种调用后，Storm才可以对原初元组进行有效的追踪。</p>
<p>如果输出元组基于多个输入元组推导出，可以一起锚定它们：</p>
<pre class="crayon-plain-tag">List&lt;Tuple&gt; anchors = new ArrayList&lt;Tuple&gt;();
anchors.add( inputTuple1 );
anchors.add( inputTuple2 );
collector.emit( anchors, outputTuple );</pre>
<p>如果outputTuple失败，则产生inputTuple1、inputTuple2的Spout都会得到通知。 </p>
<div class="blog_h2"><span class="graybg">IBasicBolt</span></div>
<p>Storm会在此接口的实现的execute()执行后，自动调用ack，如果execute()抛出异常，则自动调用fail。你可以考虑继承BaseBasicBolt。</p>
<div class="blog_h2"><span class="graybg">状态管理</span></div>
<p>Storm提供了一个抽象，允许Bolt存储/取回其操作的状态信息。该抽象有两个实现，一个是基于内存的默认实现，另一个以Redis作为后备存储。</p>
<p>需要状态管理功能的Bolt，需要实现IStatefulBolt接口或者继承BaseStatefulBolt，并实现initState方法。Storm在初始化Bolt实例时，调用initState并传入上次存储的状态，调用发生在prepare之后，处理任何元组之前。</p>
<p>当前仅仅支持的状态实现是org.apache.storm.state.KeyValueState。</p>
<div class="blog_h1"><span class="graybg">DRPC</span></div>
<p>分布式远程过程调用（Distributed Remote Procedure Call）利用Storm的计算能力进行远程调用。Storm提供了一系列DRPC的支持工具。</p>
<div class="blog_h2"><span class="graybg">简介</span></div>
<div class="blog_h3"><span class="graybg">DRPC流程示意<img class="aligncenter  wp-image-16590" src="https://blog.gmem.cc/wp-content/uploads/2017/07/drpc-topology-schema.png" alt="drpc-topology-schema" width="909" height="268" /></span></div>
<p>说明：</p>
<ol>
<li>客户端向DRPC服务器发起同步调用</li>
<li>DRPC将请求转发给DRPC Spout，进而在拓扑异步的处理</li>
<li>处理结果由最后一个Bolt返回给DRPC服务器。并返回给客户端 </li>
</ol>
<div class="blog_h3"><span class="graybg">DRPC服务器</span></div>
<p>DRPC服务器是客户端和Storm拓扑之间的桥梁，它作为拓扑中Spout的源运行。</p>
<p>DRPC服务器接受需要执行的函数及其参数，它可以执行多个函数，函数通过名称唯一识别。</p>
<p>对于每一次函数调用，服务器分配一个请求Id，用于在拓扑中唯一的识别调用请求。当拓扑中最后一个Bolt执行完毕后，此Bolt必须释放出包含请求Id、调用结果的数组，DRPC会把结果分发给正确的客户端。</p>
<div class="blog_h3"><span class="graybg">LinearDRPCTopologyBuilder</span></div>
<p>注意：目前此类已经被废弃，其功能由Trident代替。</p>
<p>用于构建DRPC拓扑的抽象。此Builder生成的拓扑会：</p>
<ol>
<li>自动创建DRPCSpouts，连接DRPC服务器读取请求，并释放元组到拓扑的其它部分</li>
<li>对Bolts进行包装，让调用结果从最后一个Bolt返回</li>
</ol>
<p>所有添加到LinearDRPCTopologyBuilder的Bolt均按序依次执行</p>
<p>要提交拓扑到Storm集群，可以调用LinearDRPCTopologyBuilder的createRemoteTopology（而不是createLocalTopology），此方法会利用Storm配置中的DRPC相关配置。</p>
<div class="blog_h3"><span class="graybg">DRPCClient</span></div>
<p>要连接到DRPC服务器，使用此类，此类是ThriftClient的子类型。</p>
<p>DRPC服务器暴露了基于Thrift的API，可以在很多语言中使用此API。并且，不管DRPC服务器是本地还是远程的，API完全一样。</p>
<div class="blog_h2"><span class="graybg">示例</span></div>
<p>下面是一个简单的加法计算器：</p>
<pre class="crayon-plain-tag">package cc.gmem.study.storm.drpc;

import org.apache.storm.Config;
import org.apache.storm.LocalCluster;
import org.apache.storm.LocalDRPC;
import org.apache.storm.drpc.LinearDRPCTopologyBuilder;
import org.apache.storm.generated.StormTopology;
import org.apache.storm.topology.BasicOutputCollector;
import org.apache.storm.topology.OutputFieldsDeclarer;
import org.apache.storm.topology.base.BaseBasicBolt;
import org.apache.storm.tuple.Fields;
import org.apache.storm.tuple.Tuple;
import org.apache.storm.tuple.Values;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class DrpcAdder {

    private static final Logger LOGGER = LoggerFactory.getLogger( DrpcAdder.class );

    public static class AdderBolt extends BaseBasicBolt {

        @Override
        public void execute( Tuple input, BasicOutputCollector collector ) {
            // 元组第一个参数是RPC请求Id
            Object rpcId = input.getValue( 0 );
            // 元组第二个参数是RPC请求参数
            String args = input.getString( 1 );
            String[] numbers = args.split( "," );
            Integer added = 0;

            for ( String num : numbers ) {
                added += Integer.parseInt( num );
            }
            collector.emit( new Values( rpcId, added ) );
        }

        @Override
        public void declareOutputFields( OutputFieldsDeclarer declarer ) {
            // 作为最后一个Bolt，需要释放RPC请求Id
            declarer.declare( new Fields( "id", "result" ) );
        }
    }

    public static void main( String[] args ) {
        // 和DRPCClient一样是DistributedRPC.Iface的实现
        LocalDRPC drpc = new LocalDRPC();

        // 拓扑对应一个远程调用函数
        LinearDRPCTopologyBuilder builder = new LinearDRPCTopologyBuilder( "add" );
        builder.addBolt( new AdderBolt(), 2 );

        Config conf = new Config();
        conf.setDebug( true );

        LocalCluster cluster = new LocalCluster();
        // 创建本地拓扑，需要传入客户端对象
        StormTopology topology = builder.createLocalTopology( drpc );
        cluster.submitTopology( "drpc-adder-topology", conf, topology );

        // 进行一次调用
        String result = drpc.execute( "add", "1,-1" );
        LOGGER.debug( "DRPC result {}", result );

        cluster.shutdown();
        drpc.shutdown();
    }
}</pre>
<div class="blog_h1"><span class="graybg">调度器</span></div>
<p>Storm使用调度器在集群中进行拓扑的调度，它提供了四种内置的调度器实现：DefaultScheduler, IsolationScheduler, MultitenantScheduler, ResourceAwareScheduler。</p>
<p>要实现自己的调度器，实现IScheduler接口：</p>
<pre class="crayon-plain-tag">package cc.gmem.study.storm;

import org.apache.storm.scheduler.Cluster;
import org.apache.storm.scheduler.IScheduler;
import org.apache.storm.scheduler.Topologies;

import java.util.Map;

public class Scheduler implements IScheduler {

    @Override
    public void prepare( Map conf ) {

    }

    /**
     * 为需要调度的拓扑设置Assignment，调用cluster.getAssignments()可以得到最新设置的Assignment
     *
     * @param topologies 提交到集群中所有的拓扑，某些拓扑需要被调度
     *                   这里的拓扑对象仅仅包含拓扑的静态信息，Assignment、Slot等信息存在于cluster中
     * @param cluster    拓扑所在的Storm集群，此对象包含开发新调度器所需的全部信息，例如：
     *                   Supervisor信息、可用Slot、当前Assignment
     */
    @Override
    public void schedule( Topologies topologies, Cluster cluster ) {
    }
}</pre>
<p>要切换调度器，修改配置文件<pre class="crayon-plain-tag">storm.yaml</pre>的<pre class="crayon-plain-tag">storm.scheduler</pre>配置项。</p>
<div class="blog_h2"><span class="graybg">IsolationScheduler</span></div>
<p>此调度器可以让多个拓扑安全、简单的共享单个集群。此调度器允许你隔离某些拓扑 —— 让它在一组专用的机器上执行，其它拓扑不能使用这些专用机器。被隔离的拓扑优先享用集群资源，被所有隔离拓扑占用内，剩下的机器，所有非隔离拓扑共享之。</p>
<p>要启用此调度器，在Nimbus节点的配置文件中进行如下设置：</p>
<pre class="crayon-plain-tag">storm.scheduler: org.apache.storm.scheduler.IsolationScheduler
# 拓扑名称：专用机器数量的映射
isolation.scheduler.machines: 
    "my-topology": 8
    "tiny-topology": 1
    "some-other-topology": 3</pre>
<div class="blog_h1"><span class="graybg">窗口化</span></div>
<p>Storm支持对一组落到同一“窗口”的元组进行统一的处理。所谓窗口，是被特定条件限定的、连续的元组集合。限定条件有两种：</p>
<ol>
<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>窗口具有一定的长度，并且每隔一段时间，窗口就向前滑动一次。例如下面的滑动窗口示意：</p>
<p style="padding-left: 30px;"><span class="monospace">........| e1 e2 | e3 e4 e5 e6 | e7 e8 e9 |...</span><br /><span class="monospace">-5      0       5             10         15    -&gt; time</span><br /><span class="monospace">|&lt;------- w1 --&gt;|</span><br /><span class="monospace">        |&lt;---------- w2 -----&gt;|</span><br /><span class="monospace">                |&lt;-------------- w3 ----&gt;|</span></p>
<p>窗口的长度是10秒，滑动间隔是5秒。注意元组可能属于多个窗口，每个窗口包含的元组数量可以变化。</p>
<div class="blog_h3"><span class="graybg">滚动窗口</span></div>
<p>窗口具有固定的长度，不同元组仅仅会属于单个窗口。下面是滚动窗口的示意：</p>
<p style="padding-left: 30px;"><span class="monospace">| e1 e2 | e3 e4 e5 e6 | e7 e8 e9 |...</span><br /><span class="monospace">0       5             10         15   -&gt; time</span><br /><span class="monospace">    w1        w2           w3</span></p>
<p>窗口长度是5秒，窗口之间不存在重叠。</p>
<div class="blog_h2"><span class="graybg">IWindowedBolt</span></div>
<p>要获得窗口化支持，Bolt需要实现IWindowedBolt接口：</p>
<pre class="crayon-plain-tag">public interface IWindowedBolt extends IComponent {
    void prepare(Map stormConf, TopologyContext context, OutputCollector collector);
    // 处理落到窗口中的元组，可选的，释放新的元组
    // 每当一个窗口激活（满了）时，该方法就被调用
    void execute(TupleWindow inputWindow);
    void cleanup();
}</pre>
<p>通常情况下，你可以选择继承BaseWindowedBolt类，该类提供了一些指定窗口长度、滑动间隔的方法：</p>
<pre class="crayon-plain-tag">builder.setBolt("slidingwindowbolt", 
                     // 滑动窗口长度 30 元组，滑动间隔 10 元组
                     // 要指定滚动窗口，使用withTumblingWindow方法，要基于时间而非长度，使用Duration
                     new SlidingWindowBolt().withWindow(new Count(30), new Count(10)),
                     1).shuffleGrouping("spout");</pre>
<div class="blog_h2"><span class="graybg">时间戳和乱序</span></div>
<p>默认的，窗口以Bolt处理元组的时间为计算窗口边界的基准。Storm还支持以元组字段给出的时间戳为基准：</p>
<pre class="crayon-plain-tag">// 根据fieldName字段指定的时间（Long型）计算窗口边界
// 如果fieldName字段不存在，导致异常
BaseWindowedBolt.withTimestampField(String fieldName); 

// 另外一种方式，提供一个能够从元组中抽取时间戳的回调函数
BaseWindowedBolt.withTimestampExtractor(TimestampExtractor timestampExtractor);</pre>
<div class="blog_h3"><span class="graybg">乱序延迟</span></div>
<p>除了指定时间戳获取方式之外，Storm还允许指定一个最大的延迟参数：</p>
<pre class="crayon-plain-tag">BaseWindowedBolt.withLag(Duration duration); </pre>
<p>如果Bolt接收到一个时间戳为06:00:05的元组，且最大延迟为5s，那么后续不应该再接收到时间戳小于06:00:00的元组。如果的确接收到这样的过期（Late）元组，Storm默认不会处理，仅仅在Worker日志中以INFO级别记录。</p>
<p>要改变上述默认行为，可以调用：</p>
<pre class="crayon-plain-tag">// 过期元组将被释放到streamId流中，后续可以通过WindowedBoltExecutor.LATE_TUPLE_FIELD访问此流
// 必须和withTimestampField一起调用，否则IllegalArgumentException
BaseWindowedBolt.withLateTupleStream(String streamId);</pre>
<div class="blog_h3"><span class="graybg">水位</span></div>
<p>当基于元组字段时间戳计算窗口边界时，Storm内部基于接收到的元组的时间戳来计算水位。所谓水位，就是所有输入流中，所有元组中<span style="background-color: #c0c0c0;">最晚的那个时间戳减去乱序延迟</span>后得到的数值。</p>
<p>Storm周期性的释放出水位时间戳，此时间戳将作为判断窗口边界是否已经到达的标准。周期默认1s，要修改周期可以调用：</p>
<pre class="crayon-plain-tag">BaseWindowedBolt.withWatermarkInterval(Duration interval)</pre>
<p>在协同多个输入流，进行窗口化处理时，水位时间戳相当于一个标准时钟。一旦窗口右边界小于等于此时间戳，就会立刻被处理，且小于此时间戳的后续元组均过期。如果多个输入流释放元组的速度差距很大，那么慢的那些流的元组甚至可能全部成为过期元组。</p>
<p>举个例子，假设基于元组字段时间戳的窗口参数如下：</p>
<p style="padding-left: 30px;">Window length = 20s, sliding interval = 10s, watermark emit frequency = 1s, max lag = 5s</p>
<p>当前墙上时间为09:00:00，在 09:00:00-09:00:01之间接收到以下元组：</p>
<p style="padding-left: 30px;">e1(6:00:03), e2(6:00:05), e3(6:00:07), e4(6:00:18), e5(6:00:26), e6(6:00:36)</p>
<p>墙上时间到达09:00:01时，水位时间戳06:00:31（最晚元组时间戳-乱序延迟5）被释放，后续任何时间戳小于06:00:31的元组均过期。水位时间戳导致三个窗口被处理：</p>
<p style="padding-left: 30px;">05:59:50 - 06:00:10  e1 e2 e3<br />06:00:00 - 06:00:20  e1 e2 e3 e4<br />06:00:10 - 06:00:30  e4 e5</p>
<p>将<span style="background-color: #c0c0c0;">第一个元组的时间戳针对滑动间隔向上取整</span>（这确保它落在第一个窗口中），得到06:00:10，作为第一个窗口的右边界。这样有多少个窗口，窗口边界是哪里都可以推算出来。</p>
<p>元组e6没有落在适当的窗口中，因此此时暂不会被处理。</p>
<p>在09:00:01-09:00:02之间接收到以下元组：</p>
<p style="padding-left: 30px;">e7(8:00:25), e8(8:00:26), e9(8:00:27), e10(8:00:39)</p>
<p>墙上时间戳到达09:00:02时，水位是按戳08:00:34被释放。水位时间戳导致三个窗口被处理：</p>
<p style="padding-left: 30px;">06:00:20 - 06:00:40  e5 e6<br />06:00:30 - 06:00:50  e6<br />08:00:10 - 08:00:30  e7 e8 e9</p>
<p>第一个窗口的左边界，等于上一批次首个窗口的左边界 + 滑动间隔10。第二个窗口继续右滑10s。</p>
<p>向右滑动<span style="background-color: #c0c0c0;">跳过N多空窗</span>，得到从08:00:10开始的非空窗，包含了e7-e9。e10所属窗口右边界超出水位时间戳，因此暂不处理。</p>
<div class="blog_h2"><span class="graybg">保证</span></div>
<p>Storm的窗口化功能，可以提供最少一次处理的保证。</p>
<p>在方法<pre class="crayon-plain-tag">execute(TupleWindow inputWindow)</pre>中释放的元组，自动锚定到inputWindow中所有元组。下游Bolt应当Ack从WindowedBolt接收的元组，以完成元组树的处理。如果不Ack元组需要回放，窗口计算也会重新进行。过期的元组会自动被确认。</p>
<p>对于基于时间的窗口，配置参数topology.message.timeout.secs应当远大于windowLength + slidingInterval，否则元组可能来得及处理之前就超时，导致Replay。</p>
<p>对于基于计数的窗口，配置参数topology.message.timeout.secs应当调整，保证windowLength + slidingInterval个元组能够在超时前得到处理。</p>
<div class="blog_h1"><span class="graybg">分布式缓存</span></div>
<p>Storm提供了一个分布式缓存API，利用它可以高效的分发巨大的、在拓扑生命周期内可能变化的文件（或者叫Blob），这些文件的大小可能在KB-GB之间。</p>
<p>对于小的，不需要动态更新的数据集，可以考虑将其嵌入在JAR中。但是过大的文件则不行，会导致拓扑启动非常缓慢。这种情况下使用分布式缓存可以加快拓扑的启动速度。</p>
<p>在拓扑启动时，用户可以指定拓扑所需要的文件集。在拓扑运行期间，用户可以查询分布式缓存中的任何文件，并更换为新版本。更新行为遵从最终一致性模型。在分布式缓存中，文件基于LRU算法管理。Worker负责确认什么文件不再需要，并删除以释放磁盘空间。</p>
<p>分布式缓存的接口是BlobStore，目前它有两种实现：LocalFsBlobStore、HdfsBlobStore</p>
<div class="blog_h2"><span class="graybg">LocalFsBlobStore</span></div>
<p>这是基于本地文件系统的缓存实现。</p>
<div class="blog_h3"><span class="graybg">通过命令创建文件</span></div>
<pre class="crayon-plain-tag"># 复制因子4，任何人可以读写管，键key1，文件内容在README.txt中
storm blobstore create --file README.txt --acl o::rwa --replication-factor 4 key1</pre>
<div class="blog_h3"><span class="graybg">文件的创建过程</span></div>
<p>文件的创建由接口ClientBlobStore负责，其实现是NimbusBlobStore。在使用基于本地文件系统的缓存时，NimbusBlobStore调用Nimbus，在其本地文件系统创建文件。</p>
<p>拓扑提交时，JAR和配置文件也是在BlobStore的帮助下上传的，就好像一个普通文件一样。</p>
<div class="blog_h3"><span class="graybg">文件的下载过程</span></div>
<p>拓扑启动后，被分配负责运行它的节点的Supervisor会从Nimbus下载JAR、配置文件，并根据topology.blobstore.map确定此拓扑需要使用哪些缓存文件并下载到本地。</p>
<div class="blog_h3"><span class="graybg">拓扑提交和文件映射</span></div>
<p>在提交拓扑的时候，可以指定该拓扑需要使用哪些文件：</p>
<pre class="crayon-plain-tag">storm jar /path/to/jar class 
    # 文件key1在拓扑中基于名称blob_file访问，不压缩
    -c topology.blobstore.map='{"key1":{"localname":"blob_file", "uncompress":"false"},"key2":{}}'</pre>
<div class="blog_h2"><span class="graybg">HdfsBlobStore </span></div>
<p>和基于本地文件系统的缓存实现类似，但是容错性已经由HDFS提供了。</p>
<p>LocalFsBlobStore需要在ZooKeeper中存储状态信息，以便支持Nimbus HA。</p>
<p>文件创建、下载、更新由HdfsClientBlobStore负责。</p>
<div class="blog_h2"><span class="graybg">BlobStore细节</span></div>
<p>BlobStore基于键、版本来生成一个哈希码，作为Blob的文件名。Blob存放在blobstore.dir指定的目录，默认值对于本地实现来说是storm.local.dir/blobs，对于HDFS实现是一个类似的路径。</p>
<p>一旦Blob被提交，BlobStore即读取配置并为文件创建元数据，元数据用于访问Blob以及对访问者的进行授权。</p>
<p>对于本地实现，Supervisor节点的缓存大小软限制为10240MB，每隔600s会自动根据LRU算法清理超过软限制的那部分blob。</p>
<p>HDFS实现能够减少Nimbus的负载，而且容错性也不受到Nimbus数量的限制。Supervisor进行blob下载时不需要和Nimbus通信，因此减少了对Nimbus的依赖。</p>
<div class="blog_h2"><span class="graybg">Nimbus的HA</span></div>
<p>Storm的Master是运行在单台机器上的，在监控工具保护下的进程。一般情况下，Nimbus宕机后会由监控工具重启，不会造成太大问题。但是当Nimbus所在机器出现硬件问题时，例如磁盘损坏，Nimbus就无法启动了。</p>
<p>如果Nimbus无法启动，现有的拓扑能够继续运行，但是无法提交新拓扑。现有的拓扑也不能被杀死、激活、禁用。此外，如果某个Supervisor宕掉，Worker重新分配也无法进行。</p>
<p>因此，实现Nimbus的HA是有必要的，以便：</p>
<ol>
<li>增加Nimbus总体可用时间</li>
<li>允许Nimbus主机在任何时候加入、离开集群</li>
<li>如果Nimbus宕机，不需要进行任何拓扑重提交</li>
<li>活动的拓扑绝不丢失</li>
</ol>
<div class="blog_h3"><span class="graybg">ILeaderElector</span></div>
<p>实现HA，需要在多个Nimbus节点中选择Leader。Nimbus服务器基于如下接口进行Leader选举：</p>
<pre class="crayon-plain-tag">public interface ILeaderElector {
    /**
     * 排队等待获取Leader锁，该方法立即返回，调用者应该检查自己是否Leader
     */
    void addToLeaderLockQueue();

    /**
     * 将自己从等待Leader锁的队列中移除，如果当前持有锁，释放之
     */
    void removeFromLeaderLockQueue();

    /**
     * 当前节点是否持有了Leader锁
     */
    boolean isLeader();

    /**
     * 返回当前Leader的地址，如果当前没有节点持有锁，则抛出异常
     */
    InetSocketAddress getLeaderAddress();

    /**
     * 返回所有Nimbus节点的地址
     */
    List&lt;InetSocketAddress&gt; getAllNimbusAddresses();
}</pre>
<p>Storm提供了基于ZooKeeper的ILeaderElector实现。</p>
<div class="blog_h3"><span class="graybg">Nimbus状态存储</span></div>
<p>为实现Nimbus故障转移，所有状态、数据必须在所有Nimbus节点之间复制，或者存储在外部的分布式存储系统中。BlobStore就可以用作这样的存储系统。</p>
<p>用户可以通过<pre class="crayon-plain-tag">topology.min.replication.count</pre>声明一个代码复制因子N，在拓扑启动前，其代码、Jar、配置必须复制到至少N个Nimbus节点。使用基于本地文件系统的BlobStore时，当发生故障转移后，如果某个Blob丢失，Nimbus会在需要的时候去下载。也就是说，成为Leader的时候Nimbus不一定在本地存储了所有Blob。</p>
<p>当使用基于本地文件系统的BlobStore时，所有Blob的元数据在ZooKeeper中存储；而使用基于HDFS的BlobStore时，HDFS负责管理。</p>
<div class="blog_h2"><span class="graybg">Java API</span></div>
<p>基于Storm CLI使用分布式缓存的方法，请参考<a href="#cli-blobstore">blobstore命令</a>。本节给出相应的Java API的例子：</p>
<pre class="crayon-plain-tag">import org.apache.storm.utils.Utils;
import org.apache.storm.blobstore.ClientBlobStore;
import org.apache.storm.blobstore.AtomicOutputStream;
import org.apache.storm.blobstore.InputStreamWithMeta;
import org.apache.storm.blobstore.BlobStoreAclHandler;
import org.apache.storm.generated.*;

// 创建BlobStore客户端
Config conf = new Config();
conf.putAll(Utils.readStormConfig());
ClientBlobStore clientBlobStore = Utils.getClientBlobStore(conf);

// 创建ACL
String stringBlobACL = "u:username:rwa";
AccessControl blobACL = BlobStoreAclHandler.parseAccessControl(stringBlobACL);
List&lt;AccessControl&gt; acls = new LinkedList&lt;AccessControl&gt;();
acls.add(blobACL);
SettableBlobMeta settableBlobMeta = new SettableBlobMeta(acls);
settableBlobMeta.set_replication_factor(4);   // ACL的复制因子

// 创建Blob，并授予ACL
AtomicOutputStream blobStream = clientBlobStore.createBlob("some_key", settableBlobMeta);
blobStream.write("Some String or input data".getBytes());
blobStream.close();

// 更新Blob
String blobKey = "some_key";
AtomicOutputStream blobStream = clientBlobStore.updateBlob(blobKey);

// 更改ACL
String blobKey = "some_key";
AccessControl updateAcl = BlobStoreAclHandler.parseAccessControl("u:USER:--a"); // u:USER:-w-
List&lt;AccessControl&gt; updateAcls = new LinkedList&lt;AccessControl&gt;();
updateAcls.add(updateAcl);
SettableBlobMeta modifiedSettableBlobMeta = new SettableBlobMeta(updateAcls);
clientBlobStore.setBlobMeta(blobKey, modifiedSettableBlobMeta); // 设置Blob元数据

// 设置、读取复制因子
String blobKey = "some_key";
BlobReplication replication = clientBlobStore.updateBlobReplication(blobKey, 5);
int replication_factor = replication.get_replication();

// 读取Blob
String blobKey = "some_key";
InputStreamWithMeta blobInputStream = clientBlobStore.getBlob(blobKey);
BufferedReader r = new BufferedReader(new InputStreamReader(blobInputStream));
String blobContents = r.readLine();

// 删除Blob
String blobKey = "some_key";
clientBlobStore.deleteBlob(blobKey);

// 列出所有Blob
Iterator &lt;String&gt; stringIterator = clientBlobStore.listKeys();</pre>
<div class="blog_h1"><span class="graybg">串行化</span></div>
<p>从0.6.0版本开始，Storm使用Kryo作为串行化库，Kryo是一个高性能、灵活的Java串行化框架。它可以产生较紧凑的串行化格式。</p>
<p>默认情况下，Storm能够串行化基本类型、字符串、字节数组、ArrayList、HashMap、HashSet、Clojure集合类型。如果你希望在元组中使用其它类型，需要提供自定义的串行化器。</p>
<div class="blog_h2"><span class="graybg">动态类型</span></div>
<p>在声明输出字段时，Storm不支持指定字段的类型。你仅仅需要把对象放入元组，Storm负责动态的识别其类型并串行化。</p>
<p>这个行为和Hadoop不同，Hadoop对键、值进行强制类型，这需要你提供大量的注解。Hadoop这种强制静态类型往往得不偿失。除了出于简单的目的，下面两条也是Storm选择动态类型的原因：</p>
<ol>
<li>没有办法很好的静态确定元组字段类型。Storm很灵活，一个Bolt可能订阅了多个流，这些流释放的元组字段各不相同</li>
<li>为了便于和JRuby、Clojure这样的动态语言配合</li>
</ol>
<div class="blog_h2"><span class="graybg">自定义串行化</span></div>
<p>要自定义串行化器，你需要实现Kryo的某些接口。要注册这些串行化器，你需要配置拓扑的topology.kryo.register，此配置项的值是一个列表：</p>
<pre class="crayon-plain-tag">topology.kryo.register:
  # 形式一：指定需要串行化的类型，这种情况下使用Kryo的FieldsSerializer来串行化该类型
  - com.mycompany.CustomType1
  # 形式二：指定类型到串行化器（com.esotericsoftware.kryo.Serializer）的映射
  - com.mycompany.CustomType2: com.mycompany.serializer.CustomType2Serializer</pre>
<p>调用<pre class="crayon-plain-tag">Config.registerSerialization</pre>也可以自定义串行化。</p>
<p>配置项<pre class="crayon-plain-tag">Config.TOPOLOGY_SKIP_MISSING_KRYO_REGISTRATIONS</pre>设置为true时，Storm忽略所有找不到的串行化器类，如果设置为false则抛出异常。</p>
<div class="blog_h2"><span class="graybg">Java串行化</span></div>
<p>如果Storm遇到某个元组字段，它没有可用的串行化器，则转用Java原生的串行化机制。如果该字段无法基于Java串行化，Storm抛出异常。</p>
<p>注意：Java串行化的成本很高，CPU占用高、输出格式尺寸大。应当尽量避免使用。</p>
<p>配置项<pre class="crayon-plain-tag">Config.TOPOLOGY_FALL_BACK_ON_JAVA_SERIALIZATION</pre>设置为false，则禁用Java串行化。</p>
<div class="blog_h1"><span class="graybg">钩子</span></div>
<p>Storm提供了钩子机制，允许你在任何Storm事件发生时注入自定义代码。扩展BaseTaskHook类，覆盖对应你关心事件的方法即可实现钩子。相关方法包括：emit、spoutAck、spoutFail、boltAck、boltFail、boltExecute。</p>
<p>注册钩子的方法有两种：</p>
<ol>
<li>在Spout的open、Bolt的prepare方法中，调用TopologyContext的方法注册</li>
<li>通过拓扑配置项topology.auto.task.hooks注册，这样钩子会应用到任何Spout、Bolt。在和外部监控系统集成时，可以使用这种方式</li>
</ol>
<div class="blog_h1"><span class="graybg">度量</span></div>
<p>Storm暴露了一个度量（metrics）接口，用于报告整个拓扑的统计信息。在Storm内部，此接口用于元组计数、处理延迟统计、Work堆用量统计，等等。</p>
<div class="blog_h2"><span class="graybg">度量类型</span></div>
<p>任何度量都需要实现IMetric接口，该接口仅包含一个方法getValueAndReset，此方法用于执行必要的统计并重置度量为初始值。Storm提供的度量子类型有：</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>AssignableMetric</td>
<td>支持设置度量为一个明确的值</td>
</tr>
<tr>
<td>CombinedMetric</td>
<td>可以被关联更新的度量的通用接口</td>
</tr>
<tr>
<td>CountMetric</td>
<td>计数性的度量，其方法<pre class="crayon-plain-tag">incr()</pre>用于增加1个计数，<pre class="crayon-plain-tag">incrBy(n)</pre>用于增加n个计数</td>
</tr>
<tr>
<td>MultiCountMetric</td>
<td>CountMetric的HashMap</td>
</tr>
<tr>
<td>ReducedMetric</td>
<td>
<p>MeanReducer：用于求平均值</p>
<p>MultiReducedMetric：ReducedMetric的HashMap</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">创建度量</span></div>
<div class="blog_h3"><span class="graybg">Task级别</span></div>
<p>你可以在Bolt中编程式的声明度量、并通过TopologyContext注册：</p>
<pre class="crayon-plain-tag">private transient CountMetric countMetric;

public void prepare(Map conf, TopologyContext context, OutputCollector collector) {
    // 注册度量
    countMetric = new CountMetric();
    // 每60秒自动调用countMetric.getValueAndReset()以重置度量
    context.registerMetric("execute_count", countMetric, 60);
}
public void execute(Tuple input) {
    // 更新度量值
    countMetric.incr();
}</pre>
<div class="blog_h3"><span class="graybg">Worker级别</span></div>
<p>要注册Worker级别度量，可以配置：</p>
<ol>
<li>Config.WORKER_METRICS：针对集群中所有Worker</li>
<li>Config.TOPOLOGY_WORKER_METRICS：针对某个拓扑的所有Worker </li>
</ol>
<p>上述两个配置的取值都是度量名到度量类的HashMap。</p>
<p>Worker级别度量具有以下限制：</p>
<ol>
<li>通过SystemBolt注册，不暴露给用户Task</li>
<li>基于默认构造器创建，不会进行任何属性或配置的注入</li>
</ol>
<div class="blog_h2"><span class="graybg">消费度量</span></div>
<p>要消费度量值实现<pre class="crayon-plain-tag">IMetricsConsumer</pre>接口。通过注册消费者，你可以监听、处理拓扑的度量数据。编程式注册：</p>
<pre class="crayon-plain-tag">conf.registerMetricsConsumer(org.apache.storm.metric.LoggingMetricsConsumer.class, 1);</pre>
<p>通过配置文件：</p>
<pre class="crayon-plain-tag">topology.metrics.consumer.register:
  - class: "org.apache.storm.metric.LoggingMetricsConsumer"
    parallelism.hint: 1
  - class: "org.apache.storm.metric.HttpForwardingMetricsConsumer"
    # 消费者对应的Bolt的并行度
    parallelism.hint: 1
    # 消费者的prepare方法接收此参数
    argument: "http://example.com:8080/metrics/my-topology/"</pre>
<div class="blog_h3"><span class="graybg">消费者如何工作</span></div>
<p>注册消费者后，Storm会在内部为每个消费者添加一个MetricsConsumerBolt（到拓扑），每个MetricsConsumerBolt都会订阅来自任何Task的度量信息。</p>
<p>MetricsConsumerBolt的并行度由parallelism.hint指定，其组件ID为<pre class="crayon-plain-tag">__metrics_consumerClassFQName#SEQ</pre>，其中SEQ仅仅在多次注册同一个消费者类时出现。</p>
<p>注意：消费者仅仅是一个普通的Bolt，如果它本身性能低下，则很多度量值会因而出现偏差。</p>
<div class="blog_h3"><span class="graybg">内置消费者</span></div>
<p>Storm提供了一些内置的度量消费者，你可以使用它们来了解拓扑提供了哪些度量：</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>LoggingMetricsConsumer</td>
<td>监听度量值，以TSV格式存储到文件</td>
</tr>
<tr>
<td>HttpForwardingMetricsConsumer</td>
<td>监听度量值，以HTTP Post方式发送到外部服务器</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">配置</span></div>
<p>为了调整Nimbus、Supervisor、运行中拓扑的行为，Storm提供了多种配置信息。 某些配置信息是系统级的，不能为每个拓扑定制。 </p>
<p>所有配置项的默认值记录在<a href="http://github.com/apache/storm/blob/v1.1.1/conf/defaults.yaml">defaults.yaml</a>中。要覆盖默认值，在Nimbus、Supervisor的Classpath下放置一个storm.yaml文件。</p>
<p>通过 StormSubmitter提交拓扑时，你可以连同自定义配置项一起提交，这些配置项必须以topology.开头。</p>
<p>从Storm 0.7开始，某些配置项可以在Spout/Bolt级别上定制，包括：</p>
<ol>
<li>topology.debug</li>
<li>topology.max.spout.pending</li>
<li>topology.max.task.parallelism</li>
<li>topology.kryo.register</li>
</ol>
<p>通过Java API，你可以为组件指定上述配置信息，途径有两种：</p>
<ol>
<li>组件内部配置：覆盖getComponentConfiguration方法</li>
<li>组件外部配置：调用TopologyBuilder的setSpout/setBolt的返回值的addConfiguration/addConfigurations方法。此会覆盖途径1的配置项</li>
</ol>
<p>配置信息的优先级，从低到高：defaults.yaml  ⇨ storm.yaml  ⇨ 拓扑配置 ⇨ 组件内部配置 ⇨ 组件外部配置</p>
<div class="blog_h2"><span class="graybg">Config</span></div>
<p>通过<a href="http://storm.apache.org/releases/1.1.1/javadocs/org/apache/storm/Config.html">此助手类</a>，可以为拓扑指定配置，此类包含了很多对应了配置项Key的常量值。</p>
<div class="blog_h3 blog_h2"><span class="graybg"><a id="parallelism-config"></a>并发度配置</span></div>
<div class="blog_h3"><span class="graybg">工作进程数量</span></div>
<p>要配置一个拓扑在整个集群上，拥有的工作进程的数量，可以使用Config.TOPOLOGY_WORKERS配置项。</p>
<div class="blog_h3"><span class="graybg">执行器线程数量</span></div>
<p>要提示为每个组件创建执行线程的数量，可以在setSpout/setBolt时传递parallelism_hint参数，指定初始的执行器数量。</p>
<div class="blog_h3"><span class="graybg">任务数量</span></div>
<p>要设置某个组件的任务数量，可以使用Config.TOPOLOGY_TASKS配置项。或者调用<pre class="crayon-plain-tag">ComponentConfigurationDeclarer.setNumTasks()</pre>方法。</p>
<div class="blog_h3"><span class="graybg">运行时更改</span></div>
<p>调用命令 storm rebalance 可以在运行期间修改拓扑工作进程数量、拓扑组件的执行器数量，示例：</p>
<pre class="crayon-plain-tag"># 使用5个执行线程
# 组件blue-spout使用3个执行器
# 组件yellow-bolt使用10个执行器
storm rebalance mytopology -n 5 -e blue-spout=3 -e yellow-bolt=10</pre>
<div class="blog_h2"><span class="graybg">defaults.yaml</span></div>
<pre class="crayon-plain-tag"># Native库的位置
java.library.path: "/usr/local/lib:/opt/local/lib:/usr/lib"

### storm.* 一般性配置
# JAR存放位置
storm.local.dir: "storm-local"
# 使用的日志实现
storm.log4j2.conf.dir: "log4j2"
# 使用的ZooKeeper服务器列表
storm.zookeeper.servers:
    - "localhost"
# 使用的ZooKeeper服务器端口
storm.zookeeper.port: 2181
# 使用的ZooKeeper根节点
storm.zookeeper.root: "/storm"
# 常规性ZooKeeper配置项
storm.zookeeper.session.timeout: 20000
storm.zookeeper.connection.timeout: 15000
storm.zookeeper.retry.times: 5
storm.zookeeper.retry.interval: 1000
storm.zookeeper.retry.intervalceiling.millis: 30000
storm.zookeeper.auth.user: null
storm.zookeeper.auth.password: null
storm.exhibitor.port: 8080
storm.exhibitor.poll.uripath: "/exhibitor/v1/cluster/list"
# 集群运行模式：distributed、local
storm.cluster.mode: "distributed"
storm.local.mode.zmq: false
storm.thrift.transport: "org.apache.storm.security.auth.SimpleTransportPlugin"
storm.thrift.socket.timeout.ms: 600000
storm.principal.tolocal: "org.apache.storm.security.auth.DefaultPrincipalToLocal"
storm.group.mapping.service: "org.apache.storm.security.auth.ShellBasedGroupsMapping"
storm.group.mapping.service.params: null
storm.messaging.transport: "org.apache.storm.messaging.netty.Context"
storm.nimbus.retry.times: 5
storm.nimbus.retry.interval.millis: 2000
storm.nimbus.retry.intervalceiling.millis: 60000
storm.auth.simple-white-list.users: []
storm.auth.simple-acl.users: []
storm.auth.simple-acl.users.commands: []
storm.auth.simple-acl.admins: []
storm.cluster.state.store: "org.apache.storm.cluster_state.zookeeper_state_factory"
storm.meta.serialization.delegate: "org.apache.storm.serialization.GzipThriftSerializationDelegate"
storm.codedistributor.class: "org.apache.storm.codedistributor.LocalFileSystemCodeDistributor"
storm.workers.artifacts.dir: "workers-artifacts"
storm.health.check.dir: "healthchecks"
storm.health.check.timeout.ms: 5000
storm.disable.symlinks: false

### nimbus.* configs are for the master
nimbus.seeds : ["localhost"]
nimbus.thrift.port: 6627
nimbus.thrift.threads: 64
nimbus.thrift.max_buffer_size: 1048576
nimbus.childopts: "-Xmx1024m"
nimbus.task.timeout.secs: 30
nimbus.supervisor.timeout.secs: 60
nimbus.monitor.freq.secs: 10
nimbus.cleanup.inbox.freq.secs: 600
nimbus.inbox.jar.expiration.secs: 3600
# Nimbus节点多久尝试同步本地缺少的代码Blob
nimbus.code.sync.freq.secs: 120
nimbus.task.launch.secs: 120
nimbus.file.copy.expiration.secs: 600
nimbus.topology.validator: "org.apache.storm.nimbus.DefaultTopologyValidator"
# 最低复制因子，拓扑的代码、JAR、配置复制到多少Nimbus节点后，Leader才能将拓扑标记为Active并开始分配Worker
topology.min.replication.count: 1
# 等待topology.min.replication.count满足的最大时间
# 超过此时间，则不等待复制完成，立即启动拓扑
# 设置为-1表示一直等待
topology.max.replication.wait.time.sec: 60
nimbus.credential.renewers.freq.secs: 600
nimbus.queue.size: 100000
scheduler.display.resource: false

### ui.* configs are for the master
ui.host: 0.0.0.0
ui.port: 8080
ui.childopts: "-Xmx768m"
ui.actions.enabled: true
ui.filter: null
ui.filter.params: null
ui.users: null
ui.header.buffer.bytes: 4096
ui.http.creds.plugin: org.apache.storm.security.auth.DefaultHttpCredentialsPlugin
ui.http.x-frame-options: DENY

logviewer.port: 8000
logviewer.childopts: "-Xmx128m"
logviewer.cleanup.age.mins: 10080
logviewer.appender.name: "A1"
logviewer.max.sum.worker.logs.size.mb: 4096
logviewer.max.per.worker.logs.size.mb: 2048

logs.users: null

drpc.port: 3772
drpc.worker.threads: 64
drpc.max_buffer_size: 1048576
drpc.queue.size: 128
drpc.invocations.port: 3773
drpc.invocations.threads: 64
drpc.request.timeout.secs: 600
drpc.childopts: "-Xmx768m"
drpc.http.port: 3774
drpc.https.port: -1
drpc.https.keystore.password: ""
drpc.https.keystore.type: "JKS"
drpc.http.creds.plugin: org.apache.storm.security.auth.DefaultHttpCredentialsPlugin
drpc.authorizer.acl.filename: "drpc-auth-acl.yaml"
drpc.authorizer.acl.strict: false

# 用于Trident拓扑中Spout相关的元数据的存储
# 存储的根路径
transactional.zookeeper.root: "/transactional"
# 用于存储的ZooKeeper集群信息
transactional.zookeeper.servers: null
transactional.zookeeper.port: null

## blobstore configs
# Supervisor用于和BlobStore通信的客户端类
# 如果使用HDFS实现，改为org.apache.storm.blobstore.HdfsClientBlobStore
supervisor.blobstore.class: "org.apache.storm.blobstore.NimbusBlobStore"
# Supervisor上用于并行下载Blob的线程数量
supervisor.blobstore.download.thread.count: 5
# 下载失败后，最大重试次数
supervisor.blobstore.download.max_retries: 3
# 本地缓存最大尺寸
supervisor.localizer.cache.target.size.mb: 10240
# 本地缓存清理间隔，超过最大尺寸的部分，以LRU算法清除
supervisor.localizer.cleanup.interval.ms: 600000
# 分布式缓存实现（BlobStore）实现类
nimbus.blobstore.class: "org.apache.storm.blobstore.LocalFsBlobStore"
# 通过Master和Blobstore交互时，会话过期时间，单位秒
nimbus.blobstore.expiration.secs: 600
# 所有Blob存储的位置，对于本地文件系统实现，对应了Nimbus本地目录，对于HDFS实现，对应hdfs文件路径
blobstore.dir: $storm.local.dir/blobs
# 上传Blob时，缓冲区大小
storm.blobstore.inputstream.buffer.size.bytes: 65536
# Storm客户端用于和BlobStore通信的客户端类
client.blobstore.class: "org.apache.storm.blobstore.NimbusBlobStore"
# BlobStore中每个Blob的复制因子
# topology.min.replication.count用于设置拓扑的数据的复制因子
# topology.min.replication.count 应该小于等于 blobstore.replication.factor
storm.blobstore.replication.factor: 3
# For secure mode we would want to change this config to true
storm.blobstore.acl.validation.enabled: false

### supervisor.* configs are for node supervisors
# Define the amount of workers that can be run on this machine. Each worker is assigned a port to use for communication
supervisor.slots.ports:
    - 6700
    - 6701
    - 6702
    - 6703
supervisor.childopts: "-Xmx256m"
supervisor.run.worker.as.user: false
#how long supervisor will wait to ensure that a worker process is started
supervisor.worker.start.timeout.secs: 120
#how long between heartbeats until supervisor considers that worker dead and tries to restart it
supervisor.worker.timeout.secs: 30
#how many seconds to sleep for before shutting down threads on worker
supervisor.worker.shutdown.sleep.secs: 3
#how frequently the supervisor checks on the status of the processes it's monitoring and restarts if necessary
supervisor.monitor.frequency.secs: 3
#how frequently the supervisor heartbeats to the cluster state (for nimbus)
supervisor.heartbeat.frequency.secs: 5
supervisor.enable: true
supervisor.supervisors: []
supervisor.supervisors.commands: []
supervisor.memory.capacity.mb: 3072.0
#By convention 1 cpu core should be about 100, but this can be adjusted if needed
# using 100 makes it simple to set the desired value to the capacity measurement
# for single threaded bolts
supervisor.cpu.capacity: 400.0

### worker.* configs are for task workers
worker.heap.memory.mb: 768
worker.childopts: "-Xmx%HEAP-MEM%m -XX:+PrintGCDetails -Xloggc:artifacts/gc.log -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=1M -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=artifacts/heapdump"
worker.gc.childopts: ""

# Unlocking commercial features requires a special license from Oracle.
# See http://www.oracle.com/technetwork/java/javase/terms/products/index.html
# For this reason, profiler features are disabled by default.
worker.profiler.enabled: false
worker.profiler.childopts: "-XX:+UnlockCommercialFeatures -XX:+FlightRecorder"
worker.profiler.command: "flight.bash"
worker.heartbeat.frequency.secs: 1

# check whether dynamic log levels can be reset from DEBUG to INFO in workers
worker.log.level.reset.poll.secs: 30

# control how many worker receiver threads we need per worker
topology.worker.receiver.thread.count: 1

task.heartbeat.frequency.secs: 3
task.refresh.poll.secs: 10
task.credentials.poll.secs: 30
task.backpressure.poll.secs: 30

# now should be null by default
topology.backpressure.enable: false
backpressure.disruptor.high.watermark: 0.9
backpressure.disruptor.low.watermark: 0.4

zmq.threads: 1
zmq.linger.millis: 5000
zmq.hwm: 0


storm.messaging.netty.server_worker_threads: 1
storm.messaging.netty.client_worker_threads: 1
storm.messaging.netty.buffer_size: 5242880 #5MB buffer
# Since nimbus.task.launch.secs and supervisor.worker.start.timeout.secs are 120, other workers should also wait at least that long before giving up on connecting to the other worker. The reconnection period need also be bigger than storm.zookeeper.session.timeout(default is 20s), so that we can abort the reconnection when the target worker is dead.
storm.messaging.netty.max_retries: 300
storm.messaging.netty.max_wait_ms: 1000
storm.messaging.netty.min_wait_ms: 100

# If the Netty messaging layer is busy(netty internal buffer not writable), the Netty client will try to batch message as more as possible up to the size of storm.messaging.netty.transfer.batch.size bytes, otherwise it will try to flush message as soon as possible to reduce latency.
storm.messaging.netty.transfer.batch.size: 262144
# Sets the backlog value to specify when the channel binds to a local address
storm.messaging.netty.socket.backlog: 500

# By default, the Netty SASL authentication is set to false.  Users can override and set it true for a specific topology.
storm.messaging.netty.authentication: false

# Default plugin to use for automatic network topology discovery
storm.network.topography.plugin: org.apache.storm.networktopography.DefaultRackDNSToSwitchMapping

# default number of seconds group mapping service will cache user group
storm.group.mapping.service.cache.duration.secs: 120

### topology.* configs are for specific executing storms
# 是否启用消息处理超时
topology.enable.message.timeouts: true
# 释放禁用拓扑调试
topology.debug: false
topology.workers: 1
# ACK线程的数量，设置为0则禁用ACK
topology.acker.executors: null
# 事件记录器线程数量，设置为0则禁用事件记录
topology.eventlogger.executors: 0
topology.tasks: null
# maximum amount of time a message has to complete before it's considered failed
topology.message.timeout.secs: 30
topology.multilang.serializer: "org.apache.storm.multilang.JsonSerializer"
topology.shellbolt.max.pending: 100
topology.skip.missing.kryo.registrations: false
topology.max.task.parallelism: null
# 对于事务性拓扑，表示能够同时处理的元组批次的数量
# 对于非事务性拓扑，如果启用了消息确认（ACK），则拓扑中尚未ACK的原初元组的数量，超过此数量不再继续释放原初元组
topology.max.spout.pending: null
topology.state.synchronization.timeout.secs: 60
topology.stats.sample.rate: 0.05
topology.builtin.metrics.bucket.size.secs: 60
topology.fall.back.on.java.serialization: true
topology.worker.childopts: null
topology.worker.logwriter.childopts: "-Xmx64m"
topology.executor.receive.buffer.size: 1024 #batched
topology.executor.send.buffer.size: 1024 #individual messages
topology.transfer.buffer.size: 1024 # batched
topology.tick.tuple.freq.secs: null
topology.worker.shared.thread.pool.size: 4
topology.spout.wait.strategy: "org.apache.storm.spout.SleepSpoutWaitStrategy"
topology.sleep.spout.wait.strategy.time.ms: 1
topology.error.throttle.interval.secs: 10
topology.max.error.report.per.interval: 5
topology.kryo.factory: "org.apache.storm.serialization.DefaultKryoFactory"
topology.tuple.serializer: "org.apache.storm.serialization.types.ListDelegateSerializer"
topology.trident.batch.emit.interval.millis: 500
topology.testing.always.try.serialize: false
topology.classpath: null
topology.environment: null
topology.bolts.outgoing.overflow.buffer.enable: false
topology.disruptor.wait.timeout.millis: 1000
topology.disruptor.batch.size: 100
topology.disruptor.batch.timeout.millis: 1
topology.disable.loadaware.messaging: false
topology.state.checkpoint.interval.ms: 1000

# Configs for Resource Aware Scheduler
# topology priority describing the importance of the topology in decreasing importance starting from 0 (i.e. 0 is the highest priority and the priority importance decreases as the priority number increases).
# Recommended range of 0-29 but no hard limit set.
topology.priority: 29
topology.component.resources.onheap.memory.mb: 128.0
topology.component.resources.offheap.memory.mb: 0.0
topology.component.cpu.pcore.percent: 10.0
# Worker进程的最大堆尺寸
topology.worker.max.heap.size.mb: 768.0
topology.scheduler.strategy: "org.apache.storm.scheduler.resource.strategies.scheduling.DefaultResourceAwareStrategy"
resource.aware.scheduler.eviction.strategy: "org.apache.storm.scheduler.resource.strategies.eviction.DefaultEvictionStrategy"
resource.aware.scheduler.priority.strategy: "org.apache.storm.scheduler.resource.strategies.priority.DefaultSchedulingPriorityStrategy"

dev.zookeeper.path: "/tmp/dev-storm-zookeeper"

pacemaker.host: "localhost"
pacemaker.port: 6699
pacemaker.base.threads: 10
pacemaker.max.threads: 50
pacemaker.thread.timeout: 10
pacemaker.childopts: "-Xmx1024m"
pacemaker.auth.method: "NONE"
pacemaker.kerberos.users: []

#default storm daemon metrics reporter plugins
storm.daemon.metrics.reporter.plugins:
     - "org.apache.storm.daemon.metrics.reporters.JmxPreparableReporter"

# configuration of cluster metrics consumer
storm.cluster.metrics.consumer.publish.interval.secs: 60</pre>
<div class="blog_h1"><span class="graybg"><a id="setup-dev-env"></a>搭建开发环境</span></div>
<div class="blog_h2"><span class="graybg">何为开发环境</span></div>
<p>前面我们提到过，Storm支持两种运行模式：本地模式、远程模式。在本地模式中你可以在单JVM进程中调试整个拓扑。</p>
<p>开发环境就是包含了所有Storm组件的环境，你可以在本地模式下开发、测试Storm拓扑。或者将集群提交到远程集群，启动/停止远程集群上的拓扑。</p>
<p>你的开发机器和远程集群的关系是这样的：远程集群被主节点——Nimbus所管理，你的机器和Nimbus交互，提交JAR格式的拓扑。随后拓扑在集群中被调度、执行。你的机器可以通过命令行客户端storm和Nimbus交互，此客户端仅仅支持远程模式。</p>
<div class="blog_h2"><span class="graybg">安装步骤</span></div>
<p>首先，下载<a href="https://mirrors.tuna.tsinghua.edu.cn/apache/storm/apache-storm-1.1.1/apache-storm-1.1.1.tar.gz">最新版本的Storm</a>，解压后，把bin子目录放到PATH环境变量中。bin目录中包含客户端脚本。</p>
<p>要想管理远程集群，将集群连接信息存放到<pre class="crayon-plain-tag">~/.storm/storm.yaml</pre>文件中。</p>
<div class="blog_h1"><span class="graybg">搭建Storm集群</span></div>
<p>主要步骤包括：</p>
<ol>
<li>创建ZooKeeper集群</li>
<li>在Nimbus、Worker节点上安装依赖</li>
<li>在Nimbus、Worker节点上下载并解压Storm</li>
<li>在storm.yaml中添加必须的配置</li>
<li>使用你选择的监控工具，通过storm命令启动守护程序。</li>
</ol>
<div class="blog_h2"><span class="graybg">创建ZooKeeper集群</span></div>
<p>请参考：<a href="/zookeeper-study-note">ZooKeeper学习笔记</a></p>
<div class="blog_h2"><span class="graybg">安装依赖</span></div>
<p>安装Java 7+以及Python 2.6.6，不赘述。</p>
<p>Python 3应该也可以工作，但是没有经过详尽的测试。</p>
<div class="blog_h2"><span class="graybg">安装Storm</span></div>
<p>请参考：<a href="#setup-dev-env">搭建开发环境</a></p>
<div class="blog_h2"><span class="graybg">配置Storm</span></div>
<div class="blog_h3"><span class="graybg">基础配置</span></div>
<p>打开$STORM_HOME/ conf/storm.yaml文件，添加以下配置：</p>
<pre class="crayon-plain-tag"># 指定ZooKeeper集群的主机列表：
storm.zookeeper.servers:
    - "zookeeper-1.gmem.cc"
    - "zookeeper-1.gmem.cc"
    - "zookeeper-3.gmem.cc"
storm.zookeeper.port: 2181

# 当前节点的名称，默认自动根据主机名获取
storm.local.hostname: "storm-n1.gmem.cc"

# Nimbus/Supervisor需要在本地文件系统中存放少量状态数据，例如 jars, confs
# 此目录需要提前创建，并授予适当的读写权限
storm.local.dir: "/home/alex/JavaEE/middleware/storm/data/local"

# 工作节点需要知道哪些机器是Master节点的候选，以便从这些节点下载拓扑JAR、配置文件
# 你应该列出目标机器的全限定域名（FQDN）。如果使用Nimbus HA，要列出所有运行Nimbus进程的机器
nimbus.seeds: ["storm-n1.gmem.cc"]

# 对于每个工作节点，你需要指定它运行多少个工作进程。每个工作进程需要独特的端口来接收消息
# 这里指定多少端口，就创建多少工作进程。默认创建6700-6703这4个端口
supervisor.slots.ports:
    - 6700
    - 6701
    - 6702
    - 6703</pre>
<div class="blog_h3"><span class="graybg">Supervisor健康状态监控</span></div>
<p>Storm允许你提供一些脚本，让Supervisor定期的执行，以监控节点的健康状态。如果脚本监测到节点不健康，它应当在标准输出打印一行以ERROR开头的文字。Supervisor会周期性的执行脚本，一旦发现ERROR，就会关闭节点上所有Worker进程并退出。</p>
<p>在监控工具下运行守护进程时，你可以考虑调用storm node-health-check命令确认节点健康状态，进一步决定是否继续运行守护进程。</p>
<pre class="crayon-plain-tag"># 指定健康检测脚本所在目录
storm.health.check.dir: "healthchecks"
# 脚本执行超时时间
storm.health.check.timeout.ms: 5000</pre>
<div class="blog_h3"><span class="graybg">配置外部库</span></div>
<p>如果需要支持外部库、自定义插件，可以把相关的JAR存放到extlib、extlib-daemon目录下。需要注意， extlib-daemon中的JAR仅仅能被守护进程（不能被Worker也就是拓扑逻辑）使用。</p>
<p>你也可以使用STORM_EXT_CLASSPATH、STORM_EXT_CLASSPATH_DAEMON这两个环境变量。</p>
<div class="blog_h3"><span class="graybg">其它常用配置</span></div>
<pre class="crayon-plain-tag"># 仅Nimbus节点

# UI服务器配置
ui.host: 0.0.0.0
ui.port: 6600

# 日志查看器配置
logviewer.port: 6601</pre>
<div class="blog_h2"><span class="graybg">启动</span></div>
<p>强烈推荐在监控工具下（supervision tool）启动守护程序，这样意外崩溃后可以立即重启。Storm守护程序被设计为快速失败、无状态的，重启后拓扑不会受到影响：</p>
<ol>
<li>要启动Nimbus，执行<pre class="crayon-plain-tag">storm nimbus</pre>命令</li>
<li>要启动Supervisor，在每个Worker节点执行<pre class="crayon-plain-tag">storm supervisor</pre>命令</li>
<li>要运行Storm UI服务器，在Nimbus节点执行<pre class="crayon-plain-tag">storm ui</pre>，默认Web端口8080</li>
</ol>
<p>所有守护程序的日志默认存放在logs目录下。</p>
<div class="blog_h3"><span class="graybg">monit</span></div>
<p>安装此监控工具参考：<a href="/monit-under-monit">Ubuntu下使用monit</a>。服务条目示例：</p>
<pre class="crayon-plain-tag">check process storm-nimbus matching daemon.name=nimbus
    start program = "/home/alex/JavaEE/middleware/storm/bin/storm nimbus"
        as uid alex and gid alex
    stop program = "/bin/kill -9 $MONIT_PROCESS_PID"
        as uid alex and gid alex

check process storm-ui matching daemon.name=ui
    start program = "/home/alex/JavaEE/middleware/storm/bin/storm ui"
        as uid alex and gid alex
    stop program = "/bin/kill -9 $MONIT_PROCESS_PID"
        as uid alex and gid alex

check process storm-drpc matching daemon.name=drpc
    start program = "/home/alex/JavaEE/middleware/storm/bin/storm drpc"
        as uid alex and gid alex
    stop program = "/bin/kill -9 $MONIT_PROCESS_PID"
        as uid alex and gid alex

check process storm-log matching daemon.name=logviewer
    start program = "/home/alex/JavaEE/middleware/storm/bin/storm logviewer"
        as uid alex and gid alex
    stop program = "/bin/kill -9 $MONIT_PROCESS_PID"
        as uid alex and gid alex</pre>
<div class="blog_h2"><span class="graybg">容器化</span></div>
<div class="blog_h3"><span class="graybg">官方镜像说明</span></div>
<p>请参考：<a href="https://hub.docker.com/_/storm/">官方Docker镜像</a>。 </p>
<p>拉取镜像：<pre class="crayon-plain-tag">docker pull storm:1.1.1</pre></p>
<p>用法示例：</p>
<pre class="crayon-plain-tag"># 以本地模式运行一个拓扑
docker run -it -v topology.jar:/topology.jar storm storm jar /topology.jar pkg.Topology
# 运行nimbus
docker run -d --restart always --name nimbus storm storm nimbus
# 运行supervisor
docker run -d --restart always --name supervisor storm storm supervisor</pre>
<p>此镜像使用的默认配置是<a href="http://github.com/apache/storm/blob/v1.1.1/conf/defaults.yaml">defaults.yaml</a>，改变配置的方式由两种：</p>
<ol>
<li>使用命令行参数，例如<pre class="crayon-plain-tag">-c storm.zookeeper.servers='["zookeeper"]'</pre></li>
<li>在容器文件系统路径<pre class="crayon-plain-tag">/conf/storm.yaml</pre>放置配置文件</li>
</ol>
<p>数据和日志的存储目录位于/data、/logs，这两个目录的所有者为storm。</p>
<div class="blog_h3"><span class="graybg">创建容器</span></div>
<pre class="crayon-plain-tag">docker run --name storm-s1 --hostname storm-s1 --network local --ip 172.21.2.1 --dns 172.21.0.1 -d docker.gmem.cc/storm storm supervisor -c storm.local.hostname='storm-s1.gmem.cc'</pre>
<div class="blog_h1"><span class="graybg">调试</span></div>
<p>Storm拓扑的调试主要依赖于日志，特别是在远程集群中运行的情况下。</p>
<div class="blog_h2"><span class="graybg">动态日志级别</span></div>
<p>你可以通过Storm UI或者命令行动态的改变某个包前缀的日志级别，方式类似于设置Log4J的日志级别。</p>
<div class="blog_h3"><span class="graybg">通过Storm UI设置</span></div>
<p>通过Storm UI首页找到需要调试的拓扑，点击链接进入，可以看到类似下面的页面：</p>
<p><img class="aligncenter size-large wp-image-16718" src="https://blog.gmem.cc/wp-content/uploads/2017/07/storm-ui-topolog-summary-860x1024.jpg" alt="storm-ui-topolog-summary" width="710" height="845" /></p>
<p>此页面提供了和拓扑相关的大量信息：</p>
<ol>
<li>顶部的搜索栏，可以对Worker的日志进行搜索</li>
<li>按钮区域，提供了激活、禁用、Rebalance、杀死、起停调试功能，还可以设置日志级别</li>
<li>拓扑的名称、状态、Worker数量、Executor数量、Task数量等基本信息</li>
<li>释放、传输（释放并发送给1-N个Bolt）元组的数量、延迟时间，Ack、Fail的数量</li>
<li>Spout、Bolt的各类统计信息</li>
<li>拥有的Worker在集群中的分布情况，组件和Worker的对应关系</li>
<li>可以查看图形化的拓扑结构（蓝色表示Spout）</li>
<li>拓扑的配置信息</li>
</ol>
<p>要通过Storm UI修改日志级别，点击Change Log Level链接，填写表单然后Apply即可。Logger字段可以填写你的代码中使用的日志器前缀（包名前缀）。Timeout指明本次日志级别调整激活多久。</p>
<div class="blog_h3"><span class="graybg">通过Storm CLI设置</span></div>
<p>参见<a href="#storm-cli-set-log-lv">命令行客户端</a>。</p>
<div class="blog_h2"><span class="graybg">搜索Worker日志</span></div>
<p>通过Logviewer不能直接查看Worker的日志文件。Worker的所有日志存放在<pre class="crayon-plain-tag">${storm.log.dir}/workers-artifacts/${topologyId}/${port}</pre>目录下。worker.log即为工作日志文件，worker.log.err则包含错误级别的日志。</p>
<p>你可以在上述Storm UI界面的顶部，输入关键字（例如Exception）来搜索Worker日志，右上角的放大镜按钮也可以用来进行搜索（默认针对所有拓扑）。</p>
<div class="blog_h2"><span class="graybg">动态Worker剖析</span></div>
<p>要进行动态JVM剖析，点击拓扑的某个组件，可以看到Profiling and Debugging面板。点选下面的Executors面板的条目，然后点击对应的按钮：</p>
<ol>
<li>JStack：生成Thread Dump</li>
<li>Start：启动剖析</li>
<li>Heap：生成Heap Dump</li>
</ol>
<p>点击My Dump Files可以下载上述按钮生成的剖析文件。</p>
<div class="blog_h3"><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>worker.profiler.command</td>
<td>指定剖析工具</td>
</tr>
<tr>
<td>worker.profiler.enabled</td>
<td>可能被禁用，如果JDK不支持JFR记录、剖析插件也不可用的话</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">查看拓扑事件</span></div>
<p>拓扑事件查看器能够用来确定元组如何穿过拓扑的每一个Stage，实时查看元组处理过程时，不会对拓扑运行产生影响。</p>
<div class="blog_h3"><span class="graybg">启用事件记录</span></div>
<p>默认事件记录功能被禁用，你可以设置<pre class="crayon-plain-tag">topology.eventlogger.executors</pre>为大于0的值，以启用之。取值1表示每个拓扑一个日志记录Task，取值nil表示每个Worker一个日志记录Task。</p>
<div class="blog_h3"><span class="graybg">启动事件记录</span></div>
<p>要启动事件记录，点击Topolog Actions面板上的Debug按钮。</p>
<p>要查看被处理的元组，进入对应Spout/Bolt组件的页面，在Component summary面板中，点击events链接。</p>
<div class="blog_h3"><span class="graybg">扩展事件日志</span></div>
<p>Storm中的日志记录Bolt利用IEventLogger接口实现事件日志，此接口的默认实现是FileBasedEventLogger。你可以扩展此接口实现更加复杂的逻辑，例如记录到数据库。</p>
<div class="blog_h1"><span class="graybg">命令行客户端</span></div>
<p>Storm提供了一个名为storm的客户端程序，用于和远程集群进行交互。</p>
<div class="blog_h2"><span class="graybg">子命令列表</span></div>
<div class="blog_h3"><span class="graybg">jar</span></div>
<p>调用格式：</p>
<pre class="crayon-plain-tag">storm jar topology-jar-path class 
    # 逗号分隔
    --jars  your-local-jar.jar,your-local-jar2.jar
    # 逗号分隔，^用于排除依赖
    --artifacts "redis.clients:jedis:2.9.0,org.apache.kafka:kafka_2.10:0.8.2.2^org.slf4j:slf4j-log4j12"
    # 指定下载构件的仓库，^用作分隔符
    --artifactRepositories "jboss-repository^"</pre>
<p>调用class的main函数，需要的配置文件和依赖的jar包（例如storm的jar），放置于~/.storm目录。main函数通常调用StormSubmitter，并导致topology-jar-path被上传到Nimbus。</p>
<p>如果存在依赖的、没有打包到topology-jar-path的其它jar包，可以使用--jars选项。如果想使用Maven构件的方式指定依赖，可以使用--artifacts选项。</p>
<p>依赖会同时上传作为Worker进程的classpath条目。</p>
<div class="blog_h3"><span class="graybg">sql</span></div>
<p>调用格式：<pre class="crayon-plain-tag">storm sql sql-file topology-name   --jars  --artifacts --artifactRepositories</pre></p>
<p>编译SQL为Trident拓扑，并提交到Storm集群。</p>
<div class="blog_h3"><span class="graybg">kill</span></div>
<p>调用格式：<pre class="crayon-plain-tag">storm kill topology-name [-w wait-time-secs]</pre></p>
<p>杀死具有指定名称的拓扑，最多等待wait-time-secs秒。</p>
<p>Storm会首先禁用Spout，然后等待一定时间（默认是拓扑的消息处理超时），让正在处理的消息完成。之后，Storm会关闭Worker进程并清理状态。</p>
<div class="blog_h3"><span class="graybg">activate</span></div>
<p>调用格式：<pre class="crayon-plain-tag">storm activate topology-name</pre></p>
<p>激活指定拓扑的Spout</p>
<div class="blog_h3"><span class="graybg">deactivate</span></div>
<p>调用格式：<pre class="crayon-plain-tag">storm deactivate topology-name</pre></p>
<p>禁用指定拓扑的Spout</p>
<div class="blog_h3"><span class="graybg">rebalance</span></div>
<p>调用格式：</p>
<pre class="crayon-plain-tag">storm rebalance topology-name [-w wait-time-secs] [-n workers-num] [-e component=parallelism]*</pre>
<p>某些时候你希望把拓扑的Worker进程均匀分布到更多的机器上。</p>
<p>举例来说，假设你有个10台机器的集群，每个运行4个工作进程。现在，又添加了10个节点，你需要使用这些机器的运算能力，让每个节点运行2个工作进程。一种方法是，杀死并重新提交拓扑，但是rebalance提供了更加简单的方法。</p>
<p>rebalance首先禁用指定的拓扑Spout（等待到消息超时或者-w），然后，在集群中均匀的创建Worker进程，最后激活拓扑。</p>
<p>rebalance也可以用来改变拓扑的并发度： -n用来改变Worker进程数量；-e用来指定组件的Executor数量（线程数量，和Task数量不是一回事）</p>
<div class="blog_h3"><span class="graybg">repl</span></div>
<p>调用格式：<pre class="crayon-plain-tag">storm repl</pre></p>
<p>开启Clojure REPL环境。</p>
<div class="blog_h3"><span class="graybg">classpath</span></div>
<p>调用格式：<pre class="crayon-plain-tag">storm classpath</pre>，打印客户端的classpath信息。</p>
<div class="blog_h3"><span class="graybg">localconfvalue</span></div>
<p>调用格式：<pre class="crayon-plain-tag">storm localconfvalue conf-name</pre></p>
<p>打印配置项conf-name的本地值，本地配置是~/.storm/storm.yaml合并了defaults.yaml的结果。</p>
<div class="blog_h3"><span class="graybg">remoteconfvalue</span></div>
<p>调用格式：<pre class="crayon-plain-tag">storm remoteconfvalue conf-name</pre></p>
<p>必须在集群中的节点上执行，打印集群的配置项。集群配置是$STORM-PATH/conf/storm.yaml合并了defaults.yaml的结果。</p>
<div class="blog_h3"><span class="graybg">nimbus</span></div>
<p>调用格式：<pre class="crayon-plain-tag">storm nimbus</pre>。启动Nimbus守护进程，此命令应该在 daemontools、 monit之类的supervision工具中调用。</p>
<div class="blog_h3"><span class="graybg">supervisor</span></div>
<p>调用格式：<pre class="crayon-plain-tag">storm supervisor</pre>。启动Supervisor守护进程，此命令应该在 daemontools、 monit之类的supervision工具中调用。</p>
<div class="blog_h3"><span class="graybg">ui</span></div>
<p>调用格式：<pre class="crayon-plain-tag">storm ui</pre>。启动Web UI服务器守护进程，此命令应该在 daemontools、 monit之类的supervision工具中调用。</p>
<div class="blog_h3"><span class="graybg">drpc</span></div>
<p>调用格式：<pre class="crayon-plain-tag">storm drpc</pre>。启动DRPC服务器守护进程，此命令应该在 daemontools、 monit之类的supervision工具中调用。</p>
<div class="blog_h3"><span class="graybg"><a id="cli-blobstore"></a>blobstore</span></div>
<p>调用格式：<pre class="crayon-plain-tag">storm blobstore cmd</pre>。管理Storm的分布式缓存。子命令列表：</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>list [KEY...]</td>
<td>列出当前存储的blob</td>
</tr>
<tr>
<td>cat [-f FILE] KEY</td>
<td>读取一个blob，写入到文件或者stdout</td>
</tr>
<tr>
<td>create [-f FILE] [-a ACL ...]<br />    [--replication-factor NUMBER] KEY</td>
<td>
<p>创建一个blob，其内容来自文件或者stdin</p>
<p>ACL是一个逗号分隔的列表，条目格式[uo]:[username]:[r-][w-][a-] </p>
</td>
</tr>
<tr>
<td>update [-f FILE] KEY</td>
<td>更新一个blob的内容</td>
</tr>
<tr>
<td>delete KEY</td>
<td>删除一个blob</td>
</tr>
<tr>
<td>set-acl [-s ACL] KEY</td>
<td>设置一个blob的访问控制列表</td>
</tr>
<tr>
<td>replication --read KEY</td>
<td>读取blob的复制因子</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">dev-zookeeper</span></div>
<p>onitor<br />调用格式：<pre class="crayon-plain-tag">storm dev-zookeeper</pre></p>
<p>启动一个全新的zookeeper服务器，使用dev.zookeeper.path作为其本地目录，使用storm.zookeeper.port作为其端口。仅仅用于开发/测试。</p>
<div class="blog_h3"><span class="graybg">get-errors</span></div>
<p>调用格式：<pre class="crayon-plain-tag">storm get-errors topology-name</pre></p>
<p>获得指定拓扑的最新的错误信息。返回结果是component-name: component-error的键值对JSON</p>
<div class="blog_h3"><span class="graybg">heartbeats</span></div>
<p>调用格式：<pre class="crayon-plain-tag">storm heartbeats [cmd]</pre></p>
<div class="blog_h3"><span class="graybg">kill_workers</span></div>
<p>调用格式：<pre class="crayon-plain-tag">storm kill_workers</pre></p>
<p>杀死运行在此Supervisor节点上的Worker进程，必须在Supervisor节点上调用。</p>
<div class="blog_h3"><span class="graybg">list</span></div>
<p>调用格式：<pre class="crayon-plain-tag">storm list</pre>。列出集群中运行的拓扑，以及这些拓扑的状态。</p>
<div class="blog_h3"><span class="graybg">logviewer</span></div>
<p>调用格式：<pre class="crayon-plain-tag">storm logviewer</pre>。启动日志查看器进程，此进程提供Web UI，此命令应该在 daemontools、 monit之类的supervision工具中调用。</p>
<div class="blog_h3"><span class="graybg">monitor</span></div>
<p>调用格式：</p>
<pre class="crayon-plain-tag">storm monitor topology-name 
    [-i interval-secs]               # 默认4秒轮询一次
    [-m component-id]                # 默认所有组件
    [-s stream-id]                   # 默认default
    [-w [emitted | transferred]]     # 默认emitted</pre>
<p>交互式的对指定拓扑的吞吐量进行监控。 </p>
<div class="blog_h3"><span class="graybg">node-health-check</span></div>
<p>调用格式：<pre class="crayon-plain-tag">storm node-health-check</pre>。在本地Supervisor上运行健康状态检测。</p>
<div class="blog_h3"><span class="graybg">pacemaker</span></div>
<p>调用格式：<pre class="crayon-plain-tag">storm pacemaker</pre>。启动Pacemaker进程，此进程提供Web UI，此命令应该在 daemontools、 monit之类的supervision工具中调用。</p>
<div class="blog_h3"><span class="graybg"><a id="storm-cli-set-log-lv"></a>set_log_level</span></div>
<p>调用格式：</p>
<pre class="crayon-plain-tag">storm set_log_level
    # 日志级别ALL, TRACE, DEBUG, INFO, WARN, ERROR, FATAL, OFF
    # timeout 秒，整数
    -l [logger name]=[log level][:optional timeout] 
    -r [logger name] 
    topology-name

# 示例
storm set_log_level -l ROOT=DEBUG:30 my-topology
storm set_log_level -l com.myapp=WARN my-topology</pre>
<p>动态的控制拓扑的日志级别。</p>
<div class="blog_h3"><span class="graybg">upload-credentials</span></div>
<p>调用格式：<pre class="crayon-plain-tag">storm upload_credentials topology-name [credkey credvalue]*</pre></p>
<p>更新credentials集到运行中的拓扑。</p>
<div class="blog_h1"><span class="graybg">Trident</span></div>
<p>Trident（音标/'traɪd(ə)nt/）是Storm提供的另外一种访问接口，它提供一些高层抽象，包括：</p>
<ol>
<li>精确的一次处理语义</li>
<li>事务性数据持久化</li>
<li>一些通用的流分析功能</li>
</ol>
<p>使用Trident你可以无缝的将低延迟分布式查询和高吞吐量（每秒百万级消息）、有状态流处理混合。如果你对Pig、Cascading之类的高层批处理工具熟悉，理解起Trident会很容易。Trident有类似的连接（Join）、聚合（Aggregation）、分组（Grouping）、函数（Function）、过滤器（Filter）概念，此外Trident还提供在任何数据库、存储机制基础上进行有状态、增量处理的原语。</p>
<div class="blog_h2"><span class="graybg">简单的例子</span></div>
<p>考虑两个流处理需求：</p>
<ol>
<li>从输入流中统计每个姓名出现的次数</li>
<li>能够传入一个姓名列表，获得这些姓名出现的总次数</li>
</ol>
<div class="blog_h3"><span class="graybg">统计的实现</span></div>
<p>非线性随机名字生成器：</p>
<pre class="crayon-plain-tag">package cc.gmem.study.bdg;

import org.apache.commons.lang3.RandomUtils;
import org.apache.commons.lang3.StringUtils;

import java.util.function.BiFunction;

public class RandomNameGenerator {

    private static String COMMON_FIRST_NAMES[] = new String[]{
            "伟", "芳", "秀", "英", "娜", "敏", "静", "丽", "强", "磊",
            "军", "洋", "勇", "艳", "杰", "娟", "涛", "明", "超", "兰",
            "霞", "平", "桂", "诚", "先", "敬", "震", "振", "壮", "会",
            "思", "群", "豪", "心", "邦", "承", "乐", "绍", "功", "松",
            "善", "厚", "裕", "河", "哲", "江", "亮", "政", "谦", "亨",
            "奇", "固", "之", "轮", "翰", "朗", "伯", "宏", "言", "海",
            "山", "仁", "波", "宁", "福", "生", "龙", "元", "全", "国",
            "胜", "学", "祥", "才", "发", "武", "新", "利", "毅", "俊",
            "峰", "保", "东", "文", "辉", "力", "永", "健", "世", "广",
            "鸣", "朋", "斌", "行", "时", "泰", "博", "磊", "民", "友",
            "志", "清", "坚", "庆", "若", "德", "彪", "盛", "雄", "琛",
            "钧", "冠", "策", "腾", "楠", "榕", "风", "航", "弘", "义",
            "兴", "良", "飞", "彬", "富", "和", "梁", "栋", "维", "启",
            "克", "伦", "翔", "旭", "鹏", "泽", "晨", "辰", "士", "以",
            "建", "家", "致", "树", "炎", "蕊", "薇", "菁", "梦", "岚",
            "苑", "婕", "馨", "瑗", "琰", "韵", "融", "园", "艺", "咏",
            "卿", "聪", "澜", "纯", "爽", "琬", "茗", "羽", "希", "宁",
            "欣", "飘", "育", "滢", "馥", "筠", "柔", "竹", "霭", "凝",
            "晓", "欢", "霄", "伊", "亚", "宜", "可", "姬", "舒", "影",
            "荔", "枝", "芬", "芳", "燕", "莺", "媛", "珊", "莎", "蓉",
            "好", "君", "琴", "毓", "悦", "昭", "冰", "枫", "芸", "菲",
            "寒", "锦", "玲", "秋", "秀", "娟", "英", "华", "慧", "巧",
            "美", "淑", "惠", "珠", "翠", "雅", "芝", "玉", "萍", "红",
            "月", "彩", "春", "菊", "凤", "洁", "梅", "琳", "怡", "宝"
    };

    private static String COMMON_LAST_NAMES[] = {
            "李", "王", "张", "刘", "陈", "杨", "赵", "黄", "周", "吴",
            "徐", "孙", "胡", "朱", "高", "林", "何", "郭", "马", "罗",
            "梁", "宋", "郑", "谢", "韩", "唐", "冯", "于", "董", "萧",
            "程", "曹", "袁", "邓", "许", "傅", "沈", "曾", "彭", "吕",
            "苏", "卢", "蒋", "蔡", "贾", "丁", "魏", "薛", "叶", "阎",
            "余", "潘", "杜", "戴", "夏", "钟", "汪", "田", "任", "姜",
            "范", "方", "石", "姚", "谭", "廖", "邹", "熊", "金", "陆",
            "郝", "孔", "白", "崔", "康", "毛", "邱", "秦", "江", "史",
            "顾", "侯", "邵", "孟", "龙", "万", "段", "漕", "钱", "汤",
            "尹", "黎", "易", "常", "武", "乔", "贺", "赖", "龚", "文",
            "庞", "樊", "兰", "殷", "施", "陶", "洪", "翟", "安", "颜",
            "倪", "严", "牛", "温", "芦", "季", "俞", "章", "鲁", "葛",
            "伍", "韦", "申", "尤", "毕", "聂", "丛", "焦", "向", "柳",
            "邢", "路", "岳", "齐", "沿", "梅", "莫", "庄", "辛", "管",
            "祝", "左", "涂", "谷", "祁", "时", "舒", "耿", "牟", "卜",
            "路", "詹", "关", "苗", "凌", "费", "纪", "靳", "盛", "童",
            "欧", "甄", "项", "曲", "成", "游", "阳", "裴", "席", "卫",
            "查", "屈", "鲍", "位", "覃", "霍", "翁", "隋", "植", "甘",
            "景", "薄", "单", "包", "司", "柏", "宁", "柯", "阮", "桂",
            "闵", "欧阳", "解", "强", "柴", "华", "车", "冉", "房", "边"
    };


    // 初值即为终值
    public static final BiFunction&lt;Integer, Integer, Integer&gt; REMAPPING_NO = ( value, max ) -&gt; value;

    public static class RemappingFunctionPower implements BiFunction&lt;Integer, Integer, Integer&gt; {

        private Integer powerOf;

        public RemappingFunctionPower( int powerOf ) {
            this.powerOf = powerOf;
        }

        @Override
        public Integer apply( Integer value, Integer max ) {
            double pos = (double) value / (double) max;
            return (int) ( Math.pow( pos, powerOf ) * max );
        }
    }

    // 二次方映射，一系列随机初值的映射结果向靠近0的方向集中
    public static final BiFunction&lt;Integer, Integer, Integer&gt; REMAPPING_POW_2 = new RemappingFunctionPower( 2 );

    // 四次方映射
    public static final BiFunction&lt;Integer, Integer, Integer&gt; REMAPPING_POW_4 = new RemappingFunctionPower( 4 );

    // 八次方映射
    public static final BiFunction&lt;Integer, Integer, Integer&gt; REMAPPING_POW_8 = new RemappingFunctionPower( 8 );


    private BiFunction&lt;Integer, Integer, Integer&gt; remappingFunc;

    private String firstNames[];

    private String lastNames[];

    public RandomNameGenerator() {
        this.firstNames = COMMON_FIRST_NAMES;
        this.lastNames = COMMON_LAST_NAMES;
        this.remappingFunc = REMAPPING_NO;
    }

    public RandomNameGenerator( BiFunction&lt;Integer, Integer, Integer&gt; func ) {
        this();
        this.remappingFunc = func;
    }

    private String randomFirstName() {
        int length = firstNames.length;
        int rand = RandomUtils.nextInt( 0, length );
        return firstNames[remappingFunc.apply( rand, length )];
    }

    private String randomLastName() {
        int length = lastNames.length;
        int rand = RandomUtils.nextInt( 0, length );
        return lastNames[remappingFunc.apply( rand, length )];
    }

    public String randomNameBatch( int size ) {
        String names[] = new String[size];
        for ( int i = 0; i &lt; size; i++ ) {
            String lastName = randomLastName();
            String firstName = randomFirstName();
            if ( RandomUtils.nextInt( 1, 3 ) == 2 ) {
                firstName += randomFirstName();
            }
            names[i] = lastName + firstName;
        }
        return StringUtils.join( names, " " );
    }
}</pre>
<p>Storm代码： </p>
<pre class="crayon-plain-tag">package cc.gmem.study.storm.trident;

import org.apache.commons.lang3.RandomUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.storm.task.TopologyContext;
import org.apache.storm.trident.TridentState;
import org.apache.storm.trident.TridentTopology;
import org.apache.storm.trident.operation.TridentCollector;
import org.apache.storm.trident.operation.builtin.Count;
import org.apache.storm.trident.operation.builtin.FilterNull;
import org.apache.storm.trident.operation.builtin.MapGet;
import org.apache.storm.trident.operation.builtin.Sum;
import org.apache.storm.trident.spout.IBatchSpout;
import org.apache.storm.trident.testing.MemoryMapState;
import org.apache.storm.trident.testing.Split;
import org.apache.storm.tuple.Fields;
import org.apache.storm.tuple.Values;

import java.util.Map;

public class NameCounterTridentTopology {

    private static final Logger LOGGER = getLogger( NameCounterTridentTopology.class );

    public static final String F_NAMES = "names";

    public static final String F_NAME = "name";

    // IBatchSpout是ITridentDataSource的子接口，作为Trident的数据源
    public static class RandomNameBatchSpout implements IBatchSpout {

        @Override
        public void open( Map conf, TopologyContext context ) {
        }

        // 为一个批次生成元组集，元组集的长度由你决定，注意考虑输入吞吐量的大小
        @Override
        public void emitBatch( long batchId, TridentCollector collector ) {
            // 随机生成一批名字
            int batchSize = RandomUtils.nextInt( 10, 100 );
            for ( int i = 0; i &lt; batchSize; i++ ) {
                int size = RandomUtils.nextInt( 10, 1000 );
                // 每次释放的元组具有单个字段，字段内容是空格分隔的人名
                String names = new RandomNameGenerator( REMAPPING_POW_8 ).randomNameBatch( size );
                LOGGER.debug( "Batch {} item {} : {}", new Object[]{ batchId, i + 1, names } );
                collector.emit( new Values( names ) );
            }
        }


        @Override
        public void ack( long batchId ) {
        }

        @Override
        public void close() {
        }

        @Override
        public Map&lt;String, Object&gt; getComponentConfiguration() {
            return null;
        }

        @Override
        public Fields getOutputFields() {
            return new Fields( F_NAMES );
        }
    }

    public static void main( String[] args ) {
        /**
         * TridentTopology暴露构建Trident计算拓扑的接口
         * 拓扑图由三类节点构成：操作（operation），分区（partition，例如分组和洗牌），Spout
         * 每个操作节点具有finishBatch方法，且可以作为Committer（有序串行化）节点
         */
        TridentTopology topology = new TridentTopology();
        String txId = "randomNamesSpout";  // Trident在ZooKeeper中存储数据使用的键
        TridentState nameCounts = topology
            /**
             * 声明一个Trident输入流，返回值trident.Stream类型。实际项目中常用的输入流包括Kafka、Kestrel等队列代理
             * Trident在ZooKeeper中存储一小部分数据，表示输入流的状态（被消费的情况）
             */
            .newStream( txId, new RandomNameBatchSpout() )
            // 对于上述流的每个元组，执行Function操作，使用Split函数以空格分割，分割的结果存放在字段name中
            // 返回值trident.Stream类型
            .each( new Fields( F_NAMES ), new Split(), new Fields( F_NAME ) )
            // 执行Grouping操作，返回值GroupedStream类型
            .groupBy( new Fields( F_NAME ) )
            // 执行Aggregation操作，聚合方式为统计总数，返回值TridentState类型
            .persistentAggregate( new MemoryMapState.Factory(), new Count(), new Fields( "nameCount" ) )
            .parallelismHint( 2 );
    }
}</pre>
<div class="blog_h3"><span class="graybg">统计实现说明</span></div>
<p>Trident以较小的批次的方式来处理输入流，根据输入throughput的不同，一个批次包含的元组数量可以在数千到数百万之间。 </p>
<p>Trident提供了全特性的API，用于处理元组批次。这些API和Pig、Cascading等Hadoop高层抽象很类似。Trident提供了跨越批次进行聚合、持久化存储（可以在内存、Redis等“状态源”中）聚合结果的功能。此外，Trident还具有查询实时状态源的功能，这些状态可以被Trident更新，状态源也可以是独立于Trident维护的。</p>
<p>回到上面的人名统计代码中：</p>
<ol>
<li>Spout释放包含单个字段names的元组，names字段实际上是空格分隔的人名列表</li>
<li>我们使用Split函数对每个names字段进行分割，每个输入元组可能产生多个输出元组，输出元组包含单个字段name。Split函数的实现：<br />
<pre class="crayon-plain-tag">public class Split extends BaseFunction {
   // 处理一个输入元组，可以释放任意个输出元组
   public void execute(TridentTuple tuple, TridentCollector collector) {
       String sentence = tuple.getString(0);
       for(String word: sentence.split(" ")) {
           collector.emit(new Values(word));                
       }
   }
}</pre>
</li>
<li>我们使用groupBy对人名进行分组，相同的名字分为一组</li>
<li>我们使用聚合器Count对分组进行聚合，并调用persistentAggregate对聚合结果进行持久化。此方法知道如何存储、更新位于状态源中的聚合结果。在此例中我们使用内存作为状态源，实际项目中你可以使用Memcached、Cassandra、Redis等持久化存储，例如：<br />
<pre class="crayon-plain-tag">.persistentAggregate(MemcachedState.transactional(serverLocations), new Count(), new Fields("nameCount"));</pre></p>
<p>注意：persistentAggregate所存储的值，是输入流发出的<span style="background-color: #c0c0c0;">所有批次的总的</span>聚合结果 </p>
</li>
<li>persistentAggregate把流转换为TridentState对象。在本例中，TridentState对象包含了所有人名的计数结果。使用此TridentState对象可以实现需求的分布式查询部分</li>
</ol>
<p>Trident的一个很酷的地方是，它提供完全的容错、精确的一次处理语义，这可以很好的简化实时处理逻辑。通过巧妙的存储状态，Trident能够在出错而需要重试时，不会对相同的源数据进行重复的入库（例如聚合到Memcached）。</p>
<div class="blog_h3"><span class="graybg">查询的实现</span></div>
<p>人名统计需求的第一部分已经实现，拓扑运行后，TridentState不断更新，记录了当前各人名出现的总次数。</p>
<p>需求的第二部分是实现对统计结果的低延迟、实时查询。对于一个空格分隔的人名列表，我们要返回这些名字出现的次数的总和。</p>
<div class="blog_h3"><span class="graybg">查询客户端</span></div>
<pre class="crayon-plain-tag">package cc.gmem.study.storm.trident;

import org.apache.storm.Config;
import org.apache.storm.security.auth.SimpleTransportPlugin;
import org.apache.storm.security.auth.plain.PlainSaslTransportPlugin;
import org.apache.storm.thrift.TException;
import org.apache.storm.utils.DRPCClient;
import org.slf4j.Logger;

import java.util.Arrays;

import static org.slf4j.LoggerFactory.getLogger;

public class NameCounterQueryClient {

    private static final Logger LOGGER = getLogger( NameCounterQueryClient.class );

    public static void main( String[] args ) throws TException {
        Config conf = new Config();
        conf.put( Config.STORM_ZOOKEEPER_SERVERS, Arrays.asList( "zookeeper-1.gmem.cc", "zookeeper-2.gmem.cc", "zookeeper-3.gmem.cc" ) );
        conf.put( Config.STORM_ZOOKEEPER_PORT, 2181 );
        conf.put( Config.STORM_ZOOKEEPER_CONNECTION_TIMEOUT, 2000 );
        conf.put( Config.STORM_THRIFT_TRANSPORT_PLUGIN, "org.apache.storm.security.auth.SimpleTransportPlugin" );
        conf.put( Config.STORM_NIMBUS_RETRY_TIMES, 3 );
        conf.put( Config.STORM_NIMBUS_RETRY_INTERVAL, 10 );
        conf.put( Config.STORM_NIMBUS_RETRY_INTERVAL_CEILING, 20 );
        conf.put( Config.DRPC_MAX_BUFFER_SIZE, 1048576 );
        DRPCClient client = new DRPCClient( conf, "storm-n1.gmem.cc", 3772 );
        // JSON编码的结果
        String result = client.execute( "nameCountFunc", "汪静好 汪震" );
        LOGGER.debug( result );
    }
}</pre>
<p>可以看到，查询客户端和普通的RPC客户端没有区别， 只是它的请求可以在Storm集群中并行的处理。类似于这种小的查询请求，其延迟可以低达10ms。</p>
<div class="blog_h3"><span class="graybg">查询服务器</span></div>
<p>我们需要对上面的拓扑进行扩展，让它作为DRPC服务器：</p>
<pre class="crayon-plain-tag">topology.newDRPCStream( "nameCountFunc" )  // 供客户端调用的函数名称
        // 对请求参数进行分隔
        .each( new Fields( "args" ), new Split(), new Fields( "name" ) )
        .groupBy( new Fields( "name" ) )
        // 获取每个名字的当前计数，namsCount即TridentState
        .stateQuery( nameCounts, new Fields( "name" ), new MapGet(), new Fields( "nameCount" ) )
        // 过滤空计数
        .each( new Fields( "nameCount" ), new FilterNull() )
        // 求和聚合
        .aggregate( new Fields( "nameCount" ), new Sum(), new Fields( "allNameCount" ) );</pre>
<p>查询服务器业务逻辑说明：</p>
<ol>
<li>每个DRPC请求被作为一个独立的小批处理任务来看待，整个请求参数被作为单个元组处理。此元组仅仅包含一个args字段，对应客户端传递来的请求参数字符串</li>
<li>首先，Split函数对请求参数进行分割</li>
<li>操作符stateQuery能够对TridentState对象进行查询。TridentState对象由拓扑前一部分的persistentAggregate调用暴露 —— 持久化后的聚合结果允许后续的查询操作。stateQuery接受三个参数：
<ol>
<li>状态源，在本例中即持久化的人名统计结果</li>
<li>用于查询的函数，在本例中是MapGet，输入字段name作为函数入参</li>
<li>查询结果输出字段</li>
</ol>
</li>
<li>上一步的返回值是一个流，我们用FilterNull这个过滤器进行过滤，把空值去掉</li>
<li>最后，调用聚合器Sum进行聚合，结果仍然是一个流。此流会被Trident自动发送给等待结果的客户端</li>
</ol>
<div class="blog_h3"><span class="graybg"><a id="submit-topology-to-remote-cluster"></a>提交拓扑</span></div>
<p>本例中的拓扑需要提交到Storm集群才能运行：</p>
<pre class="crayon-plain-tag">Config conf = new Config();
// 下面4个参数，指定前两个、后两个均可以，不必同时指定
conf.put( Config.NIMBUS_SEEDS, Arrays.asList( "storm-n1.gmem.cc" ) );
conf.put( Config.NIMBUS_THRIFT_PORT, 6627 );
conf.put( Config.STORM_ZOOKEEPER_SERVERS, Arrays.asList( "zookeeper-1.gmem.cc", "zookeeper-2.gmem.cc", "zookeeper-3.gmem.cc" ) );
conf.put( Config.STORM_ZOOKEEPER_PORT, 2181 );
conf.put( Config.DRPC_SERVERS, Arrays.asList( "storm-n1.gmem.cc" ) );
conf.setDebug( true );
conf.setNumWorkers( 8 ); // 此拓扑总计使用的Worker进程数量
// 下面的属性指定打包好的、包含了拓扑依赖的JAR的位置，StormSubmitter会自动读取此属性，并上传JAR到Nimbus
// 此JAR被上传到${storm.local.dir}/nimbus/inbox/目录下
System.setProperty( "storm.jar", "/home/alex/JavaEE/projects/idea/storm-study/target/storm-study-1.0-SNAPSHOT-jar-with-dependencies.jar" );
// 提交拓扑，提交后拓扑立即持续运行，除非被杀死
StormSubmitter.submitTopologyWithProgressBar( "name-counter-trident-topology", conf, topology.build() );</pre>
<div class="blog_h2"><span class="graybg">另一个例子</span></div>
<p>下面是一个纯粹的DRPC拓扑，用于计算按需计算URL的Reach值。Reach值针对单个URI，统计暴露于此URI的用户的数量。具体算法是，针对给定的URI，获取所有推特了此URI的用户、以及这些用户的Follower，并将所有这些用户去重、计数。</p>
<p>计算Reach对于单台机器来说，计算密集度太高了，它可能上千个数据库查询、上千万的元组处理。利用Trident可以很轻松的实现并行处理。</p>
<p>该DRPC拓扑拓扑会读取两个状态源：一个映射URI到推特了此URI的用户的列表；另外一个映射用户到其Follower的列表：</p>
<pre class="crayon-plain-tag">package cc.gmem.study.storm.twt;

import org.apache.storm.trident.TridentState;
import org.apache.storm.trident.TridentTopology;
import org.apache.storm.trident.operation.Aggregator;
import org.apache.storm.trident.operation.CombinerAggregator;
import org.apache.storm.trident.operation.builtin.Count;
import org.apache.storm.trident.operation.builtin.MapGet;
import org.apache.storm.trident.state.StateFactory;
import org.apache.storm.trident.tuple.TridentTuple;
import org.apache.storm.tuple.Fields;

public class ReachCounter {

    public static void main( String[] args ) {
        TridentTopology topology = null;

        // 创建两个代表外部数据库的TridentState对象，注意针对这些状态源的查询会自动批量处理以提升性能
        TridentState urlToTweeters = topology.newStaticState( getUrlToTweetersState() );
        TridentState tweetersToFollowers = topology.newStaticState( getTweeterToFollowersState() );

        // 定义一个reach函数
        topology.newDRPCStream( "reach" )
                // 读取参数URI，查询urlToTweeters获得推特了此URI的用户，结果存入tweeters字段
                .stateQuery( urlToTweeters, new Fields( "args" ), new MapGet(), new Fields( "tweeters" ) )
                // 展开列表
                .each( new Fields( "tweeters" ), new ExpandList(), new Fields( "tweeter" ) )
                // 下一步查询的计算密集度很高，因此洗牌，让所有tweeter平均的分布到拓扑的所有Worker中
                .shuffle()
                // 针对每个用户，查询其Followers
                .stateQuery( tweetersToFollowers, new Fields( "tweeter" ), new MapGet(), new Fields( "followers" ) )
                // 并行度提示
                .parallelismHint( 200 )
                // 展开Follower并分组
                .each( new Fields( "followers" ), new ExpandList(), new Fields( "follower" ) )
                .groupBy( new Fields( "follower" ) )
                // 使用聚合器One进行聚合，实际上就是去重
                .aggregate( (Aggregator) new One(), new Fields( "one" ) )
                .parallelismHint( 20 )
                // 统计总数
                .aggregate( new Count(), new Fields( "reach" ) );
    }

    // CombinerAggregator知道如何进行局部聚合，避免发送大量网络数据
    public static class One implements CombinerAggregator&lt;Integer&gt; {

        // 根据未聚合前的元组，计算其单个元素的聚合值
        public Integer init( TridentTuple tuple ) {
            return 1;
        }

        // 聚合两个值
        public Integer combine( Integer val1, Integer val2 ) {
            return 1;
        }

        public Integer zero() {
            return 1;
        }
    }
}</pre>
<div class="blog_h2"><span class="graybg">性能特性</span></div>
<p>Trident能够智能的决定如何执行拓扑，让性能最大化。在上面的例子中，我们应当注意一些有趣的细节：</p>
<ol>
<li>对于需要对状态源进行读、写的操作（persistentAggregate、stateQuery），会自动的批量式的和源交互。如果当前批次需要对状态源进行20个更新操作，Trident会自动将这20个操作进行批量提交，不管是20个写还是读。也就是说Trident仅仅和状态源进行一次交互</li>
<li>Trident的聚合器是高度优化的，它不会把分组中所有的元组传递给单台机器，然后进行聚合。它会尽可能的多执行局部聚合，避免跨越网络发送不必要的流量。例如聚合器Count会在每个节点上进行部分计数，把部分计数通过网络发送，最终获得总合计数。这种行为特点和MapReduce的combiners类似</li>
</ol>
<div class="blog_h2"><span class="graybg">字段和元组</span></div>
<p>Trident数据模型由TridentTuple表示，和普通Storm元组一样，它也是命名值的列表。</p>
<p>在Trident拓扑中，操作节点通常将若干输入字段作为参数，并释放一系列函数字段（function Fields），这些新字段被添加到元组中。</p>
<p>考虑一个流stream，释放三元素x、y、z构成的元组，下面的过滤器仅仅把y字段作为输入：</p>
<pre class="crayon-plain-tag">// 将每一个元组的y字段暴露给过滤器处理
stream.each(new Fields("y"), ( new BaseFilter() {
   // 此操作节点仅仅能够访问y字段
   public boolean isKeep(TridentTuple tuple) {
       return tuple.getInteger(0) &lt; 10;
   }
})());</pre>
<p>上述过滤器将仅保留y小于10的那些元组。 再看看下面的函数：</p>
<pre class="crayon-plain-tag">stream.each( new Fields( "x", "y" ), ( new BaseFunction() {
    public void execute ( TridentTuple tuple, TridentCollector collector){
        int i1 = tuple.getInteger( 0 );
        int i2 = tuple.getInteger( 1 );
        // 释放两个字段
        collector.emit( new Values( i1 + i2, i1 * i2 ) );
    }
}),new Fields( "added", "multiplied" ) /* 定义函数释放字段的名称 */);</pre>
<p>该函数以x、y字段为输入字段，同时定义了两个输出字段 —— <span style="background-color: #c0c0c0;">这些输出字段是添加到输入元组上的</span>，这导致输入元组现在有5个字段：x、y、z、added、multiplied。</p>
<p>使用<span style="background-color: #c0c0c0;">聚合器的情况下，输出字段则是代替了输入字段</span>，此外输入输出元组也不是一一对应关系：</p>
<pre class="crayon-plain-tag">// 收集所有输入元组的x字段并求和，求和存放到唯一一个输出元组的sum字段
stream.aggregate(new Fields("x"), new Sum(), new Fields("sum"));</pre>
<p>使用<span style="background-color: #c0c0c0;">分组+聚合器的情况下，输出字段同时包含分组所依据的字段</span>：</p>
<pre class="crayon-plain-tag">stream.groupBy(new Fields("x"))
     .aggregate(new Fields("y"), new Sum(), new Fields("sum"));
// 输出字段为x,sum</pre>
<div class="blog_h2"><span class="graybg">Spout</span></div>
<p>类似于原始Storm API，Trident的数据来源也是Spout。</p>
<p>你可以在Trident中使用原始的Spout，这种Spout是非事务性的：</p>
<pre class="crayon-plain-tag">// 所有Trident拓扑中的Spout必须具有唯一性的id。注意，必须是全局唯一，在集群中所有拓扑里都唯一
// Trident使用此id在ZooKeeper中记录Spout被消费的状态，包括txid以及其它相关元数据
topology.newStream("spoutid", new IRichSpout(){/**/});</pre>
<p>要定制ZooKeeper相关参数，修改transactional.zookeeper.*配置项。 </p>
<div class="blog_h3"><span class="graybg">管线</span></div>
<p>Trident拓扑的默认行为是，每次处理一个批次。当前批次成功或者失败后，才可能处理下一个批次。</p>
<p>通过管线化批次的处理流，可以大大提升拓扑的吞吐能力、降低单个批次的处理延迟。要定制同时能够处理的批次的最大数量，修改topology.max.spout.pending参数。</p>
<p>即使同时处理多个批次，Trident的严格有序语义保持不变 —— 状态更新总是串行化、有序的执行。以单词统计为例，当Batch 1进行全局聚合的时候，Batch 2...10不能进行全局聚合，但是它们可以进行分区的部分聚合。</p>
<div class="blog_h3"><span class="graybg">类型</span></div>
<p>Trident提供以下专有类型的Spout：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 35%; text-align: center;">Spout类型</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>ITridentSpout</td>
<td>最通用的接口，可以用于实现事务性、非透明事务性语义</td>
</tr>
<tr>
<td>IBatchSpout</td>
<td>每次释放一个元组批次的非事务性Spout </td>
</tr>
<tr>
<td>IPartitionedTridentSpout </td>
<td>从分区数据源（例如Kafka集群）读取数据的事务性Spout </td>
</tr>
<tr>
<td>IOpaquePartitionedTridentSpout:</td>
<td>从分区数据源（例如Kafka集群）读取数据的非透明事务性Spout  </td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">状态</span></div>
<p>实时计算的一个关键问题是，如何管理状态——包括外部数据源的状态、拓扑的内部状态——以确保当失败、重试发生时，对状态的更新是幂等的。 在生产环境中，失败是常态，当节点宕机或者出现其它问题，只能重试处理元组批次。所谓幂等，可以简单的理解为，每个元组批次导致的状态更新，仅仅发生一次。</p>
<p>Trident确保幂等性的机制是：</p>
<ol>
<li>每个元组批次被赋予唯一性的ID，所谓事务ID。如果发生重试，批次的事务ID保持不变</li>
<li>不同批次之间的状态更新，是严格有序的。也就是说，Batch3对状态源的更新动作不会发生，直到Batch2的状态更新动作完成</li>
</ol>
<p>这种处理机制和Storm中的事务性拓扑类似。但是，你不需要像事务性拓扑那样，自己记录txid、比较txid、存储多个数据项，这些工作已经由Trident的状态抽象封装起来了。</p>
<div class="blog_h3"><span class="graybg">存储策略</span></div>
<p>你可以选择任何期望的策略来存储状态。例如存放在外部数据库中、存放在内存中并以HDFS作为后备。</p>
<p>状态信息也非必须永久保存，你可以考虑使用内存状态存储，并仅仅保留最近N个小时的数据。</p>
<div class="blog_h3"><span class="graybg">Spout和状态</span></div>
<p>可能和容错相关的Spout有三类：非事务性的、事务性的、不透明事务性的，相应的，对应了三类状态对象：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">Spout</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>事务性Spout</td>
<td>
<p>事务性Spout有以下特性：</p>
<ol>
<li>每个批次的txid总是保持不变。Replay时，批次包含的元组完全不变</li>
<li>不同批次的元组没有交集</li>
<li>任何元组都属于一个批次</li>
</ol>
<p>这是最理想、简单的情况。例如storm-contrib提供的支持Kafka的事务性Spout实现：TransactionalTridentKafkaSpout</p>
<p>事务性Spout对应的状态实现，利用了txid和元组集的绝对对应关系。用一个例子说明，现在有一个Trident拓扑，用于统计单词出现数量，并持久化到一个键（单词）值（计数）数据库中。为了支持事务性，仅仅存储键值对是不够的，你必须知道哪些批次已经入库。由于Trident批次的严格有序性，额外引入一个值字段，记录上一次处理完成的txid就可以了。当Replay时，如果发现库中txid和当前批次的txid一样，跳过入库步骤即可</p>
<p>更具体化一些，假设txid 3包含元组：<pre class="crayon-plain-tag">["man"] ["man"] ["dog"]</pre>，当前数据库状态如下：</p>
<pre class="crayon-plain-tag"># 一定要保证，状态和txid原子性的入库
man =&gt; [count=3, txid=1]        # After [count=5, txid=3]
dog =&gt; [count=4, txid=3]        # After [count=4, txid=3]
apple =&gt; [count=10, txid=2]</pre>
<p>可以看到，单词man计数3，上一次成功的txid不等于当前txid3，因此应当把本批次的man计数2入库。再强调一下，由于批次的严格有序性，只要库中的txid不等于当前txid，那么它必然是前面批次的txid而绝不会是后面的。再看看dog记录的txid和当前txid一致，因此本批次dog计数1不应该入库</p>
<p>为什么上一次提交出现dog入库成功，man入库失败的情况呢？这牵涉到底层数据源的工作机制，我们假设它不支持原子的写两个键值</p>
<p>那么需要担心txid 没有处理完成么？不需要，还是严格有序性问题，一旦txid3执行到更新状态源这一步了，说明前面所有txid都成功完成了</p>
</td>
</tr>
<tr>
<td>不透明事务性Spout</td>
<td>
<p>为什么不仅仅使用事务性Spout？原因是，保证完全的容错可能需要更苛刻的外部条件，而这种容错级别可能并非必要</p>
<p>以TransactionalTridentKafkaSpout为例，它的一个批次中的元组，可能来自Kafka主题的所有分区。一旦某个批次被释放，为了满足事务性语义，未来它处理失败需要Replay时，所有分区都必须可读。如果某个Kafka节点宕机（假设没有启用Replication）了呢？那么分区就读不到，相应的就无法满足事务性，整个Trident拓扑就卡住了</p>
<p>不透明（Opaque）事务性拓扑用于解决上述问题。它允许丢失某些数据源节点，但是仍然保证一次性处理语义</p>
<p>不透明事务性拓扑的特性是：</p>
<ol>
<li>不保证每个批次的元组完全一致</li>
<li>保证每个元组仅仅在一个批次中被成功处理。一个元组可能在Batch 1中失败，后来在Batch 3中成功</li>
</ol>
<p>OpaqueTridentKafkaSpout是不透明事务性Spout的例子，它允许Kafka主题的某个分区节点临时宕机。不管什么时候，它总是从上一批次最后一个Kafka记录（对应元组）的偏移量处读取下一个批次。这确保每个记录仅仅被一个批次成功处理</p>
<p>使用不透明事务时，记录txid字段的技巧不再有效，因为批次Replay时它包含的元组可能不同。但是，引入更多的状态字段，仍然可以达成容错目标。仍然看单词计数的例子，假设当前状态源的数据如下：</p>
<pre class="crayon-plain-tag">man =&gt; (
    // 对于每个单词，存储三个字段，这些字段仍然需要原子的更新
    value =&gt; 4,        // 本次事务更新后的值
    prevValue =&gt; 1,    // 本次事务更新前的值
    txid =&gt; 2          // 本次事务ID
)</pre>
<p>现在来了包含元组<pre class="crayon-plain-tag">["man"] ["man"]</pre>的txid2，也就是和数据库记录的txid相同。这意味着什么？txid2的上一次尝试失败了。由于同一事务的两个批次的元组集可能不同，因此上次尝试记录的value是无效的。应当以prevValue为基准加上本批次包含的元组数量2，成功处理后数据变为：</p>
<pre class="crayon-plain-tag">man =&gt; ( value =&gt; 3, prevValue =&gt; 1,txid =&gt; 2 )</pre>
<p>然后，又来了包含元组<pre class="crayon-plain-tag">["man"] ["man"]</pre>的txid3，也就是和数据库记录的txid不同。这意味着上一次批次处理成功了，因此数据库中记录的value是有效的。应当以value为基准加上本批次包含的元组数量2，成功处理后数据变为：</p>
<pre class="crayon-plain-tag">man =&gt; ( value =&gt; 5, prevValue =&gt; 3,txid =&gt; 3 )</pre>
<p>这种机制的关键仍然是严格有序性，一旦txid3准备入库，就可以确信txid2是成功的，因此其value就有效，否则，必须回滚为prevValue再行计算</p>
</td>
</tr>
<tr>
<td>非事务性Spout</td>
<td>
<p>不对批次中包含的内容作任何保证</p>
</td>
</tr>
</tbody>
</table>
<p>三种Spout必须搭配对应类型的状态实现使用：</p>
<p><img class="aligncenter size-full wp-image-17008" src="https://blog.gmem.cc/wp-content/uploads/2017/07/spout-vs-state.png" alt="spout-vs-state" width="433" height="309" /></p>
<p>其中非透明事务性状态具有最强的容错能力，但是要为每个状态数据存储额外的两个字段。事务性状态仅需要存储一个额外的字段，但是它只能和事务性Spout搭配使用。</p>
<div class="blog_h3"><span class="graybg">状态API</span></div>
<p>上节讨论的复杂的存储细节，并不需要开发者自行实现。Trident已经把它们全部封装到状态API中了。</p>
<p>调用partitionPersist、persistentAggregate等方法，即可创建一个状态对象，例如： </p>
<pre class="crayon-plain-tag">TridentState wordCounts =
      topology.newStream("spout", spout)
        .each(new Fields("sentence"), new Split(), new Fields("word"))
        .groupBy(new Fields("word"))
        // 所有管理不透明事务状态的逻辑，都封装在MemcachedState.opaque()返回的状态（工厂）中
        // 更新的批量处理逻辑，也被封装在状态中
        .persistentAggregate(MemcachedState.opaque(serverLocations), new Count(), new Fields("count"))                
        .parallelismHint(6);</pre>
<p>状态（源）是一个接口，它很简单：</p>
<pre class="crayon-plain-tag">public interface State {
    // 更新开始时，获得通知
    void beginCommit(Long txid);
    // 更新结束时，获得通知
    void commit(Long txid);
}</pre>
<p>Trident不对你的状态的工作方式做任何假设，它不规定调用什么方法可以更新状态，调用什么方法可以读取状态。</p>
<p>假设有一个存储用户位置的数据库，你想通过Trident访问它，可以这样实现State：</p>
<pre class="crayon-plain-tag">public class LocationDB implements State {
    public void beginCommit(Long txid) {    
    }

    public void commit(Long txid) {    
    }

    public void setLocation(long userId, String location) {
      // 更新位置
    }

    public String getLocation(long userId) {
      // 读取位置
    }
}

// 除了实现State，还需要提供配套的StateFactory
public class LocationDBFactory implements StateFactory {
   public State makeState(Map conf, int partitionIndex, int numPartitions) {
      return new LocationDB();
   } 
}</pre>
<p>为了支持状态的查询，Trident提供了QueryFunction接口。 为了支持状态的更新，Trident提供了StateUpdater接口。状态和状态的支持的操作被解耦，如此设计是因为Trident难以对特定状态源的工作方式作出合理假设。</p>
<p>沿用上面的例子，QueryFunction的实现示例如下：</p>
<pre class="crayon-plain-tag">// QueryFunction总是针对状态源 + 结果类型的组合（泛型参数）
public class QueryLocation extends BaseQueryFunction&lt;LocationDB, String&gt; {
    // 第一步：以输入元组inputs为查询参数，获得一系列结果
    // 结果的元素和inputs的元素一一对应，输入元组有几个，则输出结果有几个
    public List&lt;String&gt; batchRetrieve(LocationDB state, List&lt;TridentTuple&gt; inputs) {
        List&lt;String&gt; ret = new ArrayList();
        // State可以暴露批量查询的接口，避免类似下面这种细粒度的交互
        for(TridentTuple input: inputs) {
            ret.add(state.getLocation(input.getLong(0)));
        }
        return ret;
    }
    // 第二步：每对输入-输出的组合，被传递给下面的方法。你可以在此释放元组，供拓扑的下一节点处理
    public void execute(TridentTuple tuple, String location, TridentCollector collector) {
        collector.emit(new Values(location));
    }    
}</pre>
<p>使用上述QueryFunction的代码示例：</p>
<pre class="crayon-plain-tag">TridentState locations = topology.newStaticState(new LocationDBFactory());
topology.newStream("spout", spout)
        // 通过QueryFunction对State发起查询，输入元组字段userid，查询结果保存字段location
        .stateQuery(locations, new Fields("userid"), new QueryLocation(), new Fields("location"));</pre>
<p>StateUpdater的实现示例如下：</p>
<pre class="crayon-plain-tag">public class LocationUpdater extends BaseStateUpdater&lt;LocationDB&gt; {
    // 将一批元组tuples更新到状态源state中
    public void updateState(LocationDB state, List&lt;TridentTuple&gt; tuples, TridentCollector collector) {
        List&lt;Long&gt; ids = new ArrayList&lt;Long&gt;();
        List&lt;String&gt; locations = new ArrayList&lt;String&gt;();
        for(TridentTuple t: tuples) {
            ids.add(t.getLong(0));
            locations.add(t.getString(1));
        }
        // 应当实现批量更新
        state.setLocationsBulk(ids, locations);
    }
}

// 你可以调用collector来释放新的元组，这些元组的流可以通过TridentState.newValuesStream()方法获得</pre>
<p>使用上述StateUpdater的代码示例：</p>
<pre class="crayon-plain-tag">TridentState locations = 
    topology.newStream("locations", locationsSpout)
        // 将输入元组的分区入库
        .partitionPersist(new LocationDBFactory(), new Fields("userid", "location"), new LocationUpdater());
// 返回代表将会被更新的状态源</pre>
<p>除了partitionPersist，Trident提供的另外一类可以更新状态源的API是persistentAggregate。此API是建立在partitionPersist上的一层抽象，它知道如何使用Trident聚合器，将聚合后的数据更新到状态源。</p>
<p>你可以针对GroupedStream调用persistentAggregate，参数必须提供MapState接口的实现。此接口的签名如下：</p>
<pre class="crayon-plain-tag">public interface MapState&lt;T&gt; extends State {
    // 以多个元组（keys）作为参数，获取对应的值
    List&lt;T&gt; multiGet(List&lt;List&lt;Object&gt;&gt; keys);
    // 对每个元组，用相应的ValueUpdater更新之
    List&lt;T&gt; multiUpdate(List&lt;List&lt;Object&gt;&gt; keys, List&lt;ValueUpdater&gt; updaters);
    // 存储多个元组及其对应的状态值
    void multiPut(List&lt;List&lt;Object&gt;&gt; keys, List&lt;T&gt; vals);
}</pre>
<p>如果要对非分组流进行（全局性）聚合，State对象必须实现下面的接口：</p>
<pre class="crayon-plain-tag">public interface Snapshottable&lt;T&gt; extends State {
    T get();
    T update(ValueUpdater updater);
    void set(T o);
}</pre>
<p>MemoryMapState、MemcachedState 实现了 MapState、Snapshottable接口。</p>
<p>当实现自己的MapState时，你不需要从零开始。Trident提供了OpaqueMap、TransactionalMap、NonTransactionalMap类，并把容错相关逻辑封装其中。你需要做的是，为这三个类提供IBackingMap实现：</p>
<pre class="crayon-plain-tag">// 你仅仅需要关注如何读取、存储键值
public interface IBackingMap&lt;T&gt; {
    List&lt;T&gt; multiGet(List&lt;List&lt;Object&gt;&gt; keys); 
    // 对于OpaqueMap，T必须是OpaqueMap；对于TransactionalMap，T必须是TransactionalValue；
    // 对于NonTransactionalMap，T就是裸的聚合值
    void multiPut(List&lt;List&lt;Object&gt;&gt; keys, List&lt;T&gt; vals); 
}</pre>
<p>Trident提供了基于LRU缓存算法的IBackingMap实现：CachedMap，继承它以实现具有缓存特性的状态源。</p>
<p>Trident提供了SnapshottableMap，可以把MapState转变为Snapshottable，其实就是全局使用单个key进行分组。</p>
<div class="blog_h2"><span class="graybg">编译和执行</span></div>
<p>Trident拓扑会被尽可能编译为高效的Storm拓扑，仅仅在发生数据重分区（例如洗牌、分组）时，元组才通过网络传输。</p>
<p>一个可能的Trident拓扑和Storm拓扑的对应关系如下图：<img class="aligncenter size-full wp-image-16991" src="https://blog.gmem.cc/wp-content/uploads/2017/07/trident-to-storm.png" alt="trident-to-storm" width="714" height="976" /></p>
<p>可以看到，Storm以分区节点为界，从Trident拓扑划分出多个Bolt。这些Bolt内部可能包含了多个Trident节点，它们可能被分配到一个Worker中执行，避免了不必要的网络数据拷贝。</p>
<div class="blog_h2"><span class="graybg">API概览</span></div>
<p>Trident的核心数据模型是流，流由一系列的元组批次构成。流可能跨越集群的多个节点而分区，应用到流的操作针对每个分区并行执行。</p>
<p>Trident的操作包含五种类型：</p>
<ol>
<li>在分区本地运行，不牵涉到网络流量的</li>
<li>重新分区操作，仅仅对流进行重新分区，不改变流的内容</li>
<li>聚合操作，产生一定的网络流量</li>
<li>针对分组（Grouped）流的操作</li>
<li>合并、连接操作</li>
</ol>
<div class="blog_h3"><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>Function</td>
<td>
<p>函数根据一系列输入字段，经过计算产生0-N个输出字段。这些输出字段被添加到原始的输入元组中，构成新的字段</p>
<p>如果函数没有释放新字段，则对应的元组被过滤掉</p>
<p>要创建函数，考虑继承BaseFunction，要在Trident中使用函数，调用流的each方法</p>
</td>
</tr>
<tr>
<td>Filter </td>
<td>
<p>过滤器以元组为输入，决定它是否被保留</p>
<p>要创建过滤器，考虑继承BaseFilter，要在Trident中使用过滤器，可以调用流的filter方法</p>
</td>
</tr>
<tr>
<td>map </td>
<td>
<p>针对每个元组进行1:1的转换，输出为转换后的元组</p>
<p>要创建Map函数，考虑继承MapFunction，要在Trident中使用它，调用map方法</p>
</td>
</tr>
<tr>
<td>flatMap</td>
<td>
<p>针对每个元组进行1:N的转换，输出为转换后的元组</p>
<p>要创建flatMap函数，考虑继承FlatMapFunction，要在Trident中使用它，调用flatMap方法</p>
</td>
</tr>
<tr>
<td>peek</td>
<td>
<p>用于针对每个元组进行额外的操作，例如查看每个元组如果经过处理管线。可以用于调试目的</p>
<p>peek不会影响流的后续处理</p>
</td>
</tr>
<tr>
<td>project</td>
<td>
<p>仅仅保留选中的输入元组字段，示例：</p>
<pre class="crayon-plain-tag">// 仅仅保留b、d两个字段
stream.project(new Fields("b", "d"))</pre>
</td>
</tr>
<tr>
<td>min / minBy</td>
<td>对一个元组批次的每一个分区，返回该分区中指定字段的值最小的那个元组。你可以自定义比较器函数 </td>
</tr>
<tr>
<td>max / maxBy</td>
<td>对一个元组批次的每一个分区，返回该分区中指定字段的值最大的那个元组。你可以自定义比较器函数</td>
</tr>
<tr>
<td>Windowing</td>
<td><a href="#windowing">后文详述</a></td>
</tr>
<tr>
<td>partitionAggregate</td>
<td>
<p>针对批次的每个分区进行内部的聚合操作，其<span style="background-color: #c0c0c0;">输出替换掉输入元组，</span>示例：
<p><pre class="crayon-plain-tag">stream.partitionAggregate(new Fields("price"), new Sum(), new Fields("sum"));
// 输出元组包含一个字段 </pre>
</td>
</tr>
<tr>
<td>stateQuery</td>
<td>查询状态源</td>
</tr>
<tr>
<td>partitionPersist</td>
<td>更新状态源</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">重新分区操作</span></div>
<p>这类操作运行一个函数，来决定分组如何在不同Task之间分配。重新分区后，分区数量可能改变，你可以调用<pre class="crayon-plain-tag">parallelismHint()</pre>改变<span style="background-color: #c0c0c0;">并行度亦即分区数量</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>shuffle</td>
<td>随机轮回方式将元组均匀的分配给所有分区</td>
</tr>
<tr>
<td>broadcast</td>
<td>将每个元组广播（重复）到所有分区</td>
</tr>
<tr>
<td>partitionBy</td>
<td>根据一系列字段进行语义分区，先根据字段值计算哈希，然后针对分区数量取模，取模结果相同的元组被发给同一分区</td>
</tr>
<tr>
<td>global</td>
<td>所有元组被发送给单个分区，所有批次也都发送给该分区</td>
</tr>
<tr>
<td>batchGlobal</td>
<td>所有元组被发送给单个分区，不同批次可以发送给不同分区</td>
</tr>
<tr>
<td>partition</td>
<td>传入一个重分区函数（实现CustomStreamGrouping接口）进行分区操作</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">聚合操作</span></div>
<p>Trident提供两个全局性（针对所有分区）的聚合操作：</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>aggregate</td>
<td>针对每个批次（的所有分区）进行聚合操作</td>
</tr>
<tr>
<td>persistentAggregate</td>
<td>针对流的所有批次进行聚合操作，并且将结果存放在状态源</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: 20%; text-align: center;">分组流操作</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>groupBy</td>
<td>
<p>该操作产生一个GroupedStream —— 根据某些字段，把字段值相同的元组分到一个区里面</p>
<p>也就是说，该操作依据字段进行重新分区操作</p>
<p>针对GroupedStream执行聚合操作时，聚合在每个分区（组）内部进行，而不是针对整个批次</p>
<p>针对GroupedStream调用persistentAggregate时，结果存储到MapState对象中，其键为分组所依据的字段</p>
<p>类似于普通流，GroupedStream也支持聚合的链式调用</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">合并与连接</span></div>
<p>这些定义在TridentTopology上的API用于把不同的流合并到一起：</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>merge</td>
<td>
<p>将多个流合并为一个，示例：<pre class="crayon-plain-tag">topology.merge(stream1, stream2, stream3);</pre></p>
<p>合并产生的新流，其字段根据第一个输入流的字段决定</p>
</td>
</tr>
<tr>
<td>join</td>
<td>
<p>类似于SQL的连接操作。当连接发生在来自不同Spout的流之间时，这些Spout将被同步 —— 每个批次将包含来自每个Spout的元组</p>
<p>如果要实现窗口化连接（Windowed Join） —— 例如，来自流A的元组仅仅后过去一小时产生的流B的元组进行连接，可以考虑利用partitionPersist、stateQuery，最近一小时的数据以轮换（Rotated）方式存储在状态源中，并且以连接字段为键。这样，stateQuery就很容易查询到最近一小时的数据了</p>
<p>示例：</p>
<pre class="crayon-plain-tag">// userStream包含字段 id, personId, loginName
// personStream包含字段 id, personName
topology.join(
    userStream, new Fields("personId"), personStream, new Fields("id"),
    // 连接后的流的字段，第一个字段对应用于连接的那个字段
    // 后续一次是前一个、后一个流中，不属于连接字段的所有字段，按照顺序逐一对应
    new Fields("personId","userId","loginName","personName")
);</pre>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">聚合器</span></div>
<p>注意：针对全局性聚合使用：
<ol>
<li>ReducerAggregator或者Aggregator时，流首先被重分区到单个分区，然后聚合器在此新分区上执行</li>
<li>CombinerAggregator时，首先进行局部聚合，然后重分区到单个分区，最后完成聚合操作</li>
</ol>
<p>局部聚合可以大大减少网络流量，因此<span style="background-color: #c0c0c0;">应该尽可能使用CombinerAggregator</span>。</p>
<p>partitionAggregate、aggregate、persistentAggregate等操作，在调用时需要传入聚合器。聚合器相关的接口有三种：</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>CombinerAggregator</td>
<td>
<p>接口签名：</p>
<pre class="crayon-plain-tag">public interface CombinerAggregator&lt;T&gt; extends Serializable {
    // 针对每个输入元组，进行初始化，产生一个值
    T init(TridentTuple tuple);
    // 每个元组产生的值，一次被送入下面的函数进行reduce，直到仅剩一个值
    T combine(T val1, T val2);
    // 如果没有任何输入元组，调用此函数
    T zero();
}</pre>
<p>此聚合器返回单个字段构成的单一元组作为输出</p>
<p>进行aggregate（而非partitionAggregate）时，此聚合器接口的优势体现在，Trident能够自动优化，尽可能的进行局部聚合，避免发送网络流量</p>
</td>
</tr>
<tr>
<td>ReducerAggregator</td>
<td>
<p>接口签名：</p>
<pre class="crayon-plain-tag">public interface ReducerAggregator&lt;T&gt; extends Serializable {
    // 初始值
    T init();
    // 针对每个输入元组进行reduce
    T reduce(T curr, TridentTuple tuple);
}</pre>
<p>可以和persistentAggregate一起使用</p>
</td>
</tr>
<tr>
<td>Aggregator</td>
<td>
<p>这个是最一般化的接口：</p>
<pre class="crayon-plain-tag">public interface Aggregator&lt;T&gt; extends Operation {
    // 在处理每个批次之前，调用返回值是一个代表了聚合状态的对象，会被传递给后面两个方法
    T init(Object batchId, TridentCollector collector);
    // 针对批次分区中每个输入元组调用。此方法会更新state，还可以释放元组
    void aggregate(T state, TridentTuple tuple, TridentCollector collector);
    // 当批次分区中所有输入元组都被处理之后调用此方法
    void complete(T state, TridentCollector collector);
}</pre>
</td>
</tr>
</tbody>
</table>
<p>某些情况下，你需要连续执行多个聚合器，参考如下方式：
<pre class="crayon-plain-tag">stream.chainedAgg()  // 开始链式聚合
        .partitionAggregate(new Count(), new Fields("count"))
        .partitionAggregate(new Fields("b"), new Sum(), new Fields("sum"))
        .chainEnd();  // 结束链式聚合
// 上述代码针对流进行计数、求和操作，其结果是两个元素构成的单一元组 ["count", "sum"]</pre>
<div class="blog_h3"><span class="graybg"><a id="windowing"></a>窗口</span></div>
<p>Trident支持窗口化（Windowing），即对元组批次中，处于同一窗口中的那些元组进行处理，并释放聚合后的结果给下一个操作。</p>
<p>两种风格的窗口被支持：</p>
<ol>
<li>滚动窗口（Tumbling window）—— 元组依据单个窗口分组，<span style="background-color: #c0c0c0;">分窗要么基于处理时间要么基于元组计数</span>。<span style="background-color: #c0c0c0;">每个元组只能属于一个窗口</span>（不重叠）：<br />
<pre class="crayon-plain-tag">/**
 * 返回一个新流，新流元组由当前流窗口内元组基于aggregator聚合而成
 * 窗口为滚动窗口，每隔windowCount个元组，新建一个窗口
 */
public Stream tumblingWindow(int windowCount, WindowsStoreFactory windowStoreFactory,
    Fields inputFields, Aggregator aggregator, Fields functionFields);

/**
 * 返回一个新流，新流元组由当前流窗口内元组基于aggregator聚合而成
 * 窗口为滚动窗口，每隔windowDuration这么长时间，新建一个窗口
 */
public Stream tumblingWindow(BaseWindowedBolt.Duration windowDuration, WindowsStoreFactory windowStoreFactory,
    Fields inputFields, Aggregator aggregator, Fields functionFields);</pre>
</li>
<li>滑动窗口（Sliding window）—— 分窗依据和上面类似，但是每个一定的间隔（处理时间或元组计数），窗口就向前滑动。<span style="background-color: #c0c0c0;">元组可以属于多个窗口</span>（重叠）：<br />
<pre class="crayon-plain-tag">/**
 * 滑动窗口，每个窗口包含windowCount个元组，每处理slideCount个元组则窗口向前滑动
 */
public Stream slidingWindow(int windowCount, int slideCount, WindowsStoreFactory windowStoreFactory,
    Fields inputFields, Aggregator aggregator, Fields functionFields);

/**
 * 滑动窗口，每个窗口时长为windowDuration，每经过slidingInterval则窗口向前滑动
 */
public Stream slidingWindow(BaseWindowedBolt.Duration windowDuration, BaseWindowedBolt.Duration slidingInterval,
    WindowsStoreFactory windowStoreFactory, Fields inputFields, Aggregator aggregator, Fields functionFields);</pre>
</li>
</ol>
<p>除了上面两套API外，Trident还支持一个通用的窗口API：</p>
<pre class="crayon-plain-tag">/**
 * 创建一个窗口
 * @param windowConfig 可以是以下之一：
 *        SlidingCountWindow.of(int windowCount, int slidingCount)
 *        SlidingDurationWindow.of(BaseWindowedBolt.Duration windowDuration, BaseWindowedBolt.Duration slidingDuration)
 *        TumblingCountWindow.of(int windowLength)
 *        TumblingDurationWindow.of(BaseWindowedBolt.Duration windowLength)
 * @param windowStoreFactory  用于存储接收到的元组以及聚合结果，HBaseWindowsStoreFactory是基于HBase的一个实现
 * @param inputFields
 * @param aggregator
 * @param functionFields
 * @return
 */
public Stream window( WindowConfig windowConfig, WindowsStoreFactory windowStoreFactory, Fields inputFields,
    Aggregator aggregator, Fields functionFields );</pre>
<div class="blog_h3"><span class="graybg">RAS</span></div>
<p>Trident的资源感知调度器（Resource Aware Scheduler，RAS）API可以用于限制Trident拓扑的资源消耗。此API和基本的Storm RAS API类似，区别只是该API针对Trident流而非Bolts、Spouts调用。示例：</p>
<pre class="crayon-plain-tag">TridentTopology topo = new TridentTopology();
// 默认资源配额，每个Bolt至少获得这么多资源
topo.setResourceDefaults(new DefaultResourceDeclarer();
    .setMemoryLoad(128)
    .setCPULoad(20));
TridentState wordCounts = topology
    .newStream("words", feeder)
    // 针对特定操作步骤（除了grouping、shuffling、shuffling）进行资源配额
    // 由于多个操作可能被合并为单个Bolt，因而结果Bolt的资源是组成它的操作的资源之和
    .parallelismHint(5)
    .setCPULoad(20)
    .setMemoryLoad(512,256)
    .each( new Fields("sentence"),  new Split(), new Fields("word"))
    .setCPULoad(10)
    .setMemoryLoad(512)
    .each(new Fields("word"), new BangAdder(), new Fields("word!"))
    .parallelismHint(10)
    .setCPULoad(50)
    .setMemoryLoad(1024)
    .each(new Fields("word!"), new QMarkAdder(), new Fields("word!?"))
    .groupBy(new Fields("word!"))
    .persistentAggregate(new MemoryMapState.Factory(), new Count(), new Fields("count"))
    .setCPULoad(100)
    .setMemoryLoad(2048);</pre>
<div class="blog_h1"><span class="graybg">集成</span></div>
<div class="blog_h2"><span class="graybg">Kafka</span></div>
<p>&nbsp;</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">开发注意点</span></div>
<ol>
<li>元组处理代码要尽可能轻量，execute方法中的代码要能够尽快执行完毕。当扩容部署时，性能影响会成倍增加</li>
<li>当内存需求较小（1GB以下）时，考虑使用Guava实现Bolt本地缓存</li>
<li>当需要跨越多个Bolt共享数据时，考虑使用HBase、Phoenix、Redis或者Memcached</li>
<li>尽可能将配置信息外部化到属性文件中，可以避免运维期间不必要的代码重编译</li>
</ol>
<div class="blog_h3"><span class="graybg">集成Kafka时</span></div>
<p>要防止拓扑重部署后，消息被重复处理。使用KafkaSpout时，要确保：</p>
<ol>
<li>SpoutConfig的id、zkroot在重部署后要保持不变。Storm使用这这两个属性决定偏移量在ZooKeeper中的存储路径</li>
<li>注意在生产环境中设置forceFromStart为false。如果设置为true，KafkaSpout会忽略ZooKeeper中存储的偏移量信息，从头开始消费。开发环境下经常设置为true，以重复测试一批消息</li>
<li>如果需要部署同一拓扑的多个版本，则应当分配不同的ClientID，Kafka将不同ClientID作为单独Consumer看待，可以独立的跟踪偏移量</li>
</ol>
<div class="blog_h3"><span class="graybg">Trident配置准则</span></div>
<ol>
<li>工作进程的个数，设置为节点（机器）数量的整数倍。并行度设置为工作进程数量的整数倍。kafka分区的数量设置为Spout并行度的整数倍</li>
<li>考虑为每个(拓扑,机器)组合启动单个Worker进程</li>
<li>启动较少较大的聚合组件，这类组件在Worker节点上运行一个实例</li>
<li>每个Worker使用一个Acker，0.9开始这是默认值</li>
<li>监控JVM垃圾回收器活动，如果一切正常，应该很少出现Major GC</li>
<li>设置Trident的批量处理延迟为你的应用的端对端延迟的1/2</li>
<li>将Spout最大未决元组数量的初值设置的很小，对于Trident可以设置为1或者拓扑执行器线程数量。然后逐步增加设置值，直到流运行状况稳定。最终到达的值可能接近于：<span displaypfx="" class="mathjax-container">\[2 \times \frac{throughputInSecs}{sec} \times endToEndLatency \]</span></li>
</ol>
<div class="blog_h3"><span class="graybg">使用多少个Worker</span></div>
<p>Worker进程的总数由Supervisor来设置。每个Supervisor都管理若干JVM Slot。你在拓扑上设置的参数，实际上是声明Slot的数量。</p>
<p>对于每个(拓扑,机器)的组合，<span style="background-color: #c0c0c0;">没有太大必要使用超过1个的Worker</span>。对于一个运行在3个8核节点上的拓扑，并行能力提示是24，每个Bolt在每个机器上配备8个执行器线程。3工作进程 x 8执行器线程的方案，比起24工作进程的方案，有三个优势：</p>
<ol>
<li>数据被重新分区（shuffle/group-bys）时，分发给的执行器如果在同一进程内，不需要进程间传递数据。本机进程间传递数据缓存需要Work发送 - 本地套接字 - Work接收，尽管不需要使用网卡，仍然比进程内直接调用慢</li>
<li>对于执行聚合功能的组件来说3个使用大后备缓存（backing cache）的组件比起24个使用小缓存的组件，具有更小的effect of skew和更高的LRU效能</li>
<li>较少的Worker进程，意味着较少的控制逻辑流量</li>
</ol>
<div class="blog_h2"><span class="graybg">零散问题</span></div>
<div class="blog_h3"><span class="graybg">Worker莫名宕掉</span></div>
<ol>
<li>检查是否对日志目录具有写权限</li>
<li>检查JVM的堆大小</li>
<li>检查是否所需的库都安装到Worker进程</li>
<li>检查ZooKeeper连接配置</li>
<li>是否正确的为每个Worker节点设置了唯一性机器名，并且写到Storm配置文件中</li>
<li>检查防火墙，允许所有Worker节点、Master节点、ZooKeeper之间的双向通信</li>
</ol>
<div class="blog_h3"><span class="graybg">工作进程崩溃</span></div>
<p>典型症状：本地模式下运行正常，但是部属到集群中后工作节点启动即崩溃</p>
<p>可能原因：子网的配置可能有问题，导致节点无法根据hostname相互发现。有时ZeroMQ会因为无法解析主机名而崩溃</p>
<p>解决方案：</p>
<ol>
<li>在hosts文件中配置主机名和IP地址的映射关系</li>
<li>使用内部DNS服务器</li>
</ol>
<div class="blog_h3"><span class="graybg">节点无法相互通信</span></div>
<p>典型症状：</p>
<ol>
<li>所有的元组均处理失败</li>
<li>处理流程不工作</li>
</ol>
<p>解决方案：</p>
<ol>
<li>注意Storm不支持IPv6，可以考虑设置Supervisor的 Child JVM系统属性-Djava.net.preferIPv4Stack=true</li>
<li>子网配置可能有问题，参考上一个问题</li>
</ol>
<div class="blog_h3"><span class="graybg">一段时间后不再处理元组</span></div>
<p>典型症状：</p>
<ol>
<li>一开始处理正常，但是一段时间后，突然停止处理，Spout的元组开始集体fail</li>
</ol>
<p>解决方案：</p>
<ol>
<li>可能和ZeroMQ版本有关，可以从2.1.10降级为2.1.7</li>
</ol>
<div class="blog_h3"><span class="graybg">部分Supervisor不在UI中出现</span></div>
<p>典型症状：</p>
<ol>
<li>某些Supervisor不出现在Storm UI中</li>
<li>Storm UI刷新后，Supervisor列表发生变化</li>
</ol>
<p>解决方案：</p>
<ol>
<li>确保每个Supervisor的本地目录是独立的，不要通过NFS共享本地目录</li>
<li>尝试删除Supervisor的本地目录并重启守护程序。由于Supervisor会为自己创建一个UUID并存放在本地，如果此ID复制到其它节点，Storm集群会出现问题</li>
</ol>
<div class="blog_h3"><span class="graybg">NoSuchMethodError</span></div>
<p>一定要保证构建JAR时使用的Storm版本和集群使用的Storm版本一致</p>
<div class="blog_h3"><span class="graybg">ConcurrentModificationException</span></div>
<p>输出元组中的对象必须实现不变模式。一旦你释放元组到OutputCollector，你就不应该在修改它。</p>
<div class="blog_h3"><span class="graybg">NotSerializableException/IllegalStateException</span></div>
<p>在Storm生命周期中，拓扑会被串行化并存储到ZooKeeper中。这意味着Spout/Bolt必须支持串行化，包括它们的属性。如果某些属性无法支持串行化，考虑在Spout/Bolt的prepare方法中初始化它。这些方法在拓扑组件被分发给Worker进程后才会执行。</p>
<div class="blog_h3"><span class="graybg">Found multiple defaults.yaml resources</span></div>
<p>报错信息：Caused by: java.lang.RuntimeException: java.io.IOException: Found multiple defaults.yaml resources. You're probably bundling the Storm jars with your topology jar. [jar:file:/apache-storm-1.1.1/storm-local/supervisor/stormdist/name-counter-trident-topology-2-1508312849/stormjar.jar!/defaults.yaml, jar:file:/apache-storm-1.1.1/lib/storm-core-1.1.1.jar!/defaults.yaml]</p>
<p>报错原因：打包时把Storm的JAR包也打到拓扑的JAR包中会导致这个情况的发生。</p>
<div class="blog_h3"><span class="graybg">Async loop died：No DRPC servers configured for topology</span></div>
<p>配置拓扑时，需要指明DRPC服务器的位置：</p>
<pre class="crayon-plain-tag">conf.put( Config.DRPC_SERVERS, Arrays.asList( "storm-n1.gmem.cc" ) );</pre>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/apache-storm-study-note">Apache Storm学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/apache-storm-study-note/feed</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>Kafka Streams学习笔记</title>
		<link>https://blog.gmem.cc/kafka-streams-study-note</link>
		<comments>https://blog.gmem.cc/kafka-streams-study-note#comments</comments>
		<pubDate>Thu, 22 Jun 2017 09:28:50 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[BigData]]></category>
		<category><![CDATA[实时处理]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=17170</guid>
		<description><![CDATA[<p>简介 当前比较流行的实时计算框架包括Apache Storm、Apache Spark等。这些框架的功能强大而全面，但是具有以下缺点： 复杂度高，应对某些简单的工作显得笨重 部署Storm、Spark等分布式框架需要预留集群支持，增加开发负担  Kafka Streams是一个实时计算框架，其特点是： 低门槛：你可以很快的编写一段代码，在单机上运行。要扩容支持生产环境只需要简单的启动多个实例。利用Kafka的并行模型，Kafka Streams能够透明的处理多个实例之间的负载均衡。相比之下，Storm虽然也有本地集群，但是其运行环境和远程集群并不一样 简单而轻量：轻松的嵌入到任何Java应用中，轻松的和现有打包、部署、操作工具整合 没有额外的依赖：除了Kafka本身，没有其它依赖 支持容错的本地状态：可以进行快速有效的有状态操作，例如窗口化Join、聚合 支持精确一次性处理语义：不管是Streams客户端还是Kafka代理出现失败，都能保证一次且仅一次的处理 对于单记录的处理，实现毫秒级延迟。支持基于事件时间对一系列记录进行窗口操作 提供必要的流处理原语，同时提供高层的DSL和低级的Processor API Kafka Streams针对来自1-N个主题的输入进行持续计算，并把计算结果输出到0-N个输出主题中。 每个KafkaStreams实例可以包含1-N个线程，线程数量在配置中指定，这些线程用于执行数据处理任务。 KafkaStreams实例与其他具有相同Application ID的实例进行协作，这些实例可能分布在同一JVM、不同JVM进程或者不同的机器上。所有相同Application <a class="read-more" href="https://blog.gmem.cc/kafka-streams-study-note">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/kafka-streams-study-note">Kafka Streams学习笔记</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 Storm、Apache Spark等。这些框架的功能强大而全面，但是具有以下缺点：</p>
<ol>
<li>复杂度高，应对某些简单的工作显得笨重</li>
<li>部署Storm、Spark等分布式框架需要预留集群支持，增加开发负担 </li>
</ol>
<p>Kafka Streams是一个实时计算框架，其特点是：</p>
<ol>
<li>低门槛：你可以很快的编写一段代码，在单机上运行。要扩容支持生产环境只需要简单的启动多个实例。利用Kafka的并行模型，Kafka Streams能够透明的处理多个实例之间的负载均衡。相比之下，Storm虽然也有本地集群，但是其运行环境和远程集群并不一样</li>
<li>简单而轻量：轻松的嵌入到任何Java应用中，轻松的和现有打包、部署、操作工具整合</li>
<li>没有额外的依赖：除了Kafka本身，没有其它依赖</li>
<li>支持容错的本地状态：可以进行快速有效的有状态操作，例如窗口化Join、聚合</li>
<li>支持精确一次性处理语义：不管是Streams客户端还是Kafka代理出现失败，都能保证一次且仅一次的处理</li>
<li>对于单记录的处理，实现毫秒级延迟。支持基于事件时间对一系列记录进行窗口操作</li>
<li>提供必要的流处理原语，同时提供高层的DSL和低级的Processor API</li>
</ol>
<p>Kafka Streams针对来自1-N个主题的输入进行持续计算，并把计算结果输出到0-N个输出主题中。</p>
<p>每个KafkaStreams实例可以包含1-N个线程，线程数量在配置中指定，这些线程用于执行数据处理任务。</p>
<p>KafkaStreams实例与其他具有相同Application ID的实例进行协作，这些实例可能分布在同一JVM、不同JVM进程或者不同的机器上。所有相同Application ID的实例的整体，构成一个流处理程序。通过分配输入主题的分区给不同的实例，实现处理任务的划分。如果某个实例宕机，其它实例会瓜分它持有的分区，进行负载再平衡，同时确保任何分区都被消费。</p>
<div class="blog_h1"><span class="graybg">核心概念</span></div>
<div class="blog_h2"><span class="graybg">流处理拓扑</span></div>
<p>流是Kafka Streams中最重要的一个抽象，它：</p>
<ol>
<li>是一个无边界的、持续更新的数据集</li>
<li>是一个有序的、容错的、不变的数据记录的序列。数据记录以键值对的形式定义</li>
</ol>
<p>流处理程序是指，利用Kafka Streams库，以处理器拓扑（Processor Topologies）的形式定义计算逻辑的应用程序。处理器拓扑是节点（流处理器）的有向图，节点通过流连接。</p>
<p>流处理器，以及拓扑节点，它每次从上游流接受一个输入记录，进行转换、处理，可选的释放一或多个记录到下游流中。有两种特殊的流处理器：</p>
<ol>
<li>Source处理器：它没有上游处理器，通过读取Kafka主题，消费其中的记录，并释放到下游流中</li>
<li>Sink处理器：它没有下游处理器，负责将记录存储到Kafka主题中</li>
</ol>
<p>拓扑中的节点可以调用任何外部系统，因而有机会将记录存储在外部，而非必须存储在Kafka主题中。</p>
<p>KafkaStreams的计算逻辑可通过两种方式来定义：</p>
<ol>
<li>基于 Processor API 定义，生成一个Processor组成的有向无环图（DAG）拓扑。可以访问状态存储</li>
<li>基于StreamsBuilder定义，此类提供一个高层的DSL。支持map、filter、join、aggregation等常见操作</li>
</ol>
<p>处理器拓扑仅仅是逻辑的抽象，在运行时，该逻辑拓扑会被实例化，并在应用中复制，以增强并行处理能力。</p>
<div class="blog_h2"><span class="graybg">时间</span></div>
<p>对于流处理来说时间是个很重要的概念，窗口化等操作依赖时间来确定边界。Kafka Streams中有三种时间：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td>时间</td>
<td>说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>Event Time/事件时间</td>
<td>事件或者数据记录的产生时间。以GPS位置采集记录为例，事件时间应该是GPS传感器捕获到位置的那个时间</td>
</tr>
<tr>
<td>Ingestion Time/吸收时间</td>
<td>数据记录被存入到Kafka分区日志的时间</td>
</tr>
<tr>
<td>Process Time/处理时间</td>
<td>数据记录被Kafka Streams处理完毕的时间</td>
</tr>
</tbody>
</table>
<p>事件时间、吸收时间是二选一的。由Kafka配置参数（在代理或者主题级别）确定，从0.10开始时间戳自动嵌入到Kafka消息中。</p>
<p>接口TimestampExtractor用于抽取时间戳，分配给每个记录，此接口的默认实现简单的从Kafka消息中获取上述嵌入时间戳。抽取到的时间戳被称为Stream Time。TimestampExtractor的实现可以提供不同的Stream Time语义，它可以从记录的任意字段推导出时间戳，或者直接使用墙上时间。</p>
<p>当Sink处理器将记录写入到Kafka主题时，也需要为记录授予时间戳，具体方式取决于上下文：</p>
<ol>
<li>如果输出记录通过处理某些输入记录而得到，例如在process()中调用了context.forward()方法，输出记录的时间戳直接从输入记录继承</li>
<li>如果输出记录由周期性函数，例如Punctuator#punctuate()生成，则时间戳使用当前Task的内部时间，此内部时间通过context.timestamp()获取</li>
<li>对于聚合操作，结果记录的时间戳更新为最后一个参与聚合的输入记录的时间戳</li>
</ol>
<div class="blog_h2"><span class="graybg">状态</span></div>
<p>某些流处理程序需要记录一些状态信息，例如，聚合类的操作需要随时暂存当前的聚合结果。Kafka Streams DSL包含很多状态性的操作。</p>
<p>Kafka Stream引入状态存储（State Store）的概念，流处理程序可以针对其进行状态的读写。每个流处理Task都可以嵌入一个或多个状态存储。转台存储可以实现为持久化键值对存储、内存哈希表或者其他的形式。Kafka Stream为本地状态存储提供容错、自动恢复功能。</p>
<p>交互式查询（Interactive Queries）这一特性，允许进程内部、外部的代码对流处理程序创建的状态存储进行只读的操作。</p>
<div class="blog_h2"><span class="graybg">一次处理保证</span></div>
<p>关于流处理框架的最常见的提问是，该框架能否保证每个记录被处理一次且仅一次，即使在处理过程中出现失败的情况下？</p>
<p>某些业务场景不允许数据丢失或者重复，因此常常引入面向批处理的框架，配合流处理管线，这被称为Lambda架构。</p>
<p>在0.11版本之前，Kafka仅仅提供至少一次性递送保证，因此任何基于Kafka的流处理系统都不能保证端对端的精确一次性。</p>
<p>从0.11版本开始，Kafka支持生产者向不同的分区甚至主题进行事务性、幂等性的消息发送，利用这一特性，Kafka Streams可以支持精确一次性处理：</p>
<ol>
<li>对于任何从源主题读取来的记录，其处理结果在目的主题上精确的体现一次</li>
<li>对于任何从源主题读取来的记录，其处理结果在有状态操作的状态存储上精确的体现一次</li>
</ol>
<p>与其他实时处理框架不同的是，Kafka Streams和底层的Kafka存储机制紧密集成，确保对输入主题偏移量的提交、对状态存储的更新、对输出主题的写入能够原子的完成。</p>
<p>要启用精确一次性保证，需要配置processing.guarantee=exactly_once。默认值是at_least_once。</p>
<div class="blog_h1"><span class="graybg">入门</span></div>
<p>本章引入一个简单的，基于StreamBuilder的流处理程序，作为入门的例子。</p>
<div class="blog_h2"><span class="graybg">流处理程序</span></div>
<p>下面是一个人名个数统计的例子，它能够在生产环境中弹性（动态增加硬件）、可扩容的部署：</p>
<pre class="crayon-plain-tag">package cc.gmem.study.kafka;

import org.apache.kafka.common.serialization.Serdes;
import org.apache.kafka.streams.KafkaStreams;
import org.apache.kafka.streams.StreamsBuilder;
import org.apache.kafka.streams.StreamsConfig;
import org.apache.kafka.streams.kstream.KStream;
import org.apache.kafka.streams.kstream.KTable;
import org.apache.kafka.streams.kstream.Materialized;
import org.apache.kafka.streams.kstream.Produced;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.util.Arrays;
import java.util.Properties;
import java.util.concurrent.TimeUnit;

public class NameCountApplication {

    private static final Logger LOGGER = LogManager.getLogger( NameCountApplication.class );

    public static void main( final String[] args ) throws Exception {
        Properties config = new Properties();
        // 应用的标识符，不同的实例依据此标识符相互发现
        config.put( StreamsConfig.APPLICATION_ID_CONFIG, "names-counter-application" );
        // 启动时使用的Kafka服务器
        config.put( StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka-1.gmem.cc:9092" );
        // 键值串行化类
        config.put( StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String().getClass() );
        config.put( StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.String().getClass() );
        // High Level DSL for building topologies
        StreamsBuilder builder = new StreamsBuilder();
        // KStream是记录（KeyValue）流的抽象
        KStream&lt;String, String&gt; nameBatches = builder.stream( "names" );
        KTable&lt;String, Long&gt; nameCounts = nameBatches
                // 一到多映射，分割字符串
                .flatMapValues( nameBatch -&gt; Arrays.asList( nameBatch.split( "\\W+" ) ) )
                // 根据人名分组
                .groupBy( ( key, name ) -&gt; name )
                // 进行聚合，结果存放到StateStore中
                .count( Materialized.as( "names-count-store" ) );
        // 输出到目标
        nameCounts.toStream().to( "names-count", Produced.with( Serdes.String(), Serdes.Long() ) );
        // 构建流处理程序并启动
        KafkaStreams streams = new KafkaStreams( builder.build(), config );
        LOGGER.trace( "Prepare to start stream processing." );
        streams.start();

        TimeUnit.DAYS.sleep( 1 );  // 阻塞主线程
    }
}</pre>
<div class="blog_h2"><span class="graybg">运行和测试</span></div>
<p>首先，创建输入、输出主题：</p>
<pre class="crayon-plain-tag">kafka-topics.sh --create --zookeeper zk-1.gmem.cc:2181/kafka --replication-factor 1 --partitions 1 --topic names
kafka-topics.sh --create --zookeeper zk-1.gmem.cc:2181/kafka --replication-factor 1 --partitions 1 --topic names-count</pre>
<p>然后，通过命令行准备一批空格分隔的人名。并准备好输出主题的消费者：</p>
<pre class="crayon-plain-tag">topic-consume names-count --formatter kafka.tools.DefaultMessageFormatter
    --property print.key=true --property print.value=true 
    --property key.deserializer=org.apache.kafka.common.serialization.StringDeserializer
    --property value.deserializer=org.apache.kafka.common.serialization.LongDeserializer</pre>
<p>启动上面的流处理程序，上面的消费者开始输出：</p>
<pre class="crayon-plain-tag">Meng	2
Zhen	2
Cai	4
Dang	3
Ya	4
Alex	5
Alex	6  # 计数更新，发布新记录</pre>
<p>每个名字的计数更新后，流处理程序都会在names-count上发布一个记录，其键位人名，值为计数。记录被逐个打印在消费者的标准输出中。 </p>
<div class="blog_h1"><span class="graybg">低级API</span></div>
<p>本章介绍Kafka Streams的底层API。</p>
<div class="blog_h2"><span class="graybg">Topology</span></div>
<p>拓扑是各种处理器和Kafka主题共同构成的网络，要创建拓扑可以参考：</p>
<pre class="crayon-plain-tag">Topology topology = new Topology();
// 指定拓扑的输入，也就是Kafka主题
topology.addSource( "SOURCE", "src-topic" )
        // 添加一个处理器PROCESS1，其上游为拓扑输入（通过名称引用）
        .addProcessor( "PROCESS1", () -&gt; new Processor1(), "SOURCE" )
        // 添加另一个处理器PROCESS2，以PROCESS1为上游
        .addProcessor( "PROCESS2", () -&gt; new Processor2(), "PROCESS1" )
        // 添加另一个处理器PROCESS3，仍然以PROCESS1为上游，注意拓扑分叉了
        .addProcessor( "PROCESS3", () -&gt; new Processor3(), "PROCESS1" )
        // 添加一个输出处理器，输出到sink-topic1，以PROCESS1为上游
        .addSink( "SINK1", "sink-topic1", "PROCESS1" )
        // 添加一个输出处理器，输出到sink-topic2，以PROCESS2为上游
        .addSink( "SINK2", "sink-topic2", "PROCESS2" )
        // 添加一个输出处理器，输出到sink-topic3，以PROCESS3为上游
        .addSink( "SINK3", "sink-topic3", "PROCESS3" );</pre>
<div class="blog_h2"><span class="graybg">Processor</span></div>
<p>该接口用于定义一个流处理器，也就是处理器拓扑中的节点。流处理器以参数化类型的方式限定了其键、值的类型。你可以定义任意数量的流处理器，并且连同它们关联的状态存储一起，组装出拓扑。</p>
<p>Processor.process()方法针对收到的每一个记录进行处理。Processor.init()方法实例化了一个ProcessorContext，流处理器可以调用上下文：</p>
<ol>
<li>context().schedule，调度一个Punctuation函数，周期性执行</li>
<li>context().forward，转发新的或者被修改的键值对给下游处理器</li>
<li>context().commit，提交当前处理进度</li>
</ol>
<p>下面是基于Processor API的人名个数统计处理器示例：</p>
<pre class="crayon-plain-tag">package cc.gmem.study.kafka.streams.low;

import org.apache.kafka.streams.KeyValue;
import org.apache.kafka.streams.processor.Processor;
import org.apache.kafka.streams.processor.ProcessorContext;
import org.apache.kafka.streams.processor.PunctuationType;
import org.apache.kafka.streams.state.KeyValueIterator;
import org.apache.kafka.streams.state.KeyValueStore;

public class NameCounterProcessor implements Processor&lt;String, String&gt; {

    private ProcessorContext context;

    private KeyValueStore&lt;String, Long&gt; kvStore;

    @Override
    public void init( ProcessorContext context ) {
        // 保存引用，类似于Storm的TopologyContext
        this.context = context;
        // 从上下文中取回一个状态存储
        this.kvStore = (KeyValueStore&lt;String, Long&gt;) context.getStateStore( "NameCounts" );
        // 以墙上时间为准，每秒执行Punctuator逻辑
        this.context.schedule( 1000, PunctuationType.WALL_CLOCK_TIME, timestamp -&gt; {
            NameCounterProcessor.this.punctuate( timestamp );
        } );
    }

    /**
     * 接收一个记录（人名列表）并处理
     *
     * @param dummy 记录的键，无用
     * @param line  记录的值
     */
    @Override
    public void process( String dummy, String line ) {
        String[] names = line.toLowerCase().split( " " );

        // 在键值存储中更新人名计数
        for ( String name : names ) {
            Long oldCount = this.kvStore.get( name );

            if ( oldCount == null ) {
                this.kvStore.put( name, 1L );
            } else {
                this.kvStore.put( name, oldCount + 1L );
            }
        }
    }

    @Override
    public void punctuate( long timestamp ) {
        // 获得键值存储的迭代器
        KeyValueIterator&lt;String, Long&gt; iter = this.kvStore.all();

        while ( iter.hasNext() ) {
            KeyValue&lt;String, Long&gt; entry = iter.next();
            // 转发记录给下游处理器
            context.forward( entry.key, entry.value.toString() );
        }

        /**
         * 调用者必须要负责关闭状态存储上的迭代器
         * 否则可能（取决于底层状态存储的实现）导致内存、文件句柄的泄漏
         */
        iter.close();

        // 请求提交当前流状态（消费进度）
        context.commit();
    }

    @Override
    public void close() {
        // 在此关闭所有持有的资源，但是状态存储不需要关闭，由Kafka Stream自己维护其生命周期
    }
}</pre>
<div class="blog_h2"><span class="graybg">StateStore</span></div>
<div class="blog_h3"><span class="graybg">使用状态存储</span></div>
<p>要位拓扑中每个Processor提供状态存储，调用：</p>
<pre class="crayon-plain-tag">Topology topology = new Topology();
topology.addSource("Source", "source-topic")
    .addProcessor("Process", () -&gt; new WordCountProcessor(), "Source")
    // 为处理器Process提供一个状态存储 
    .addStateStore(countStoreSupplier, "Process");</pre>
<div class="blog_h3"><span class="graybg">changelog</span></div>
<p>为了支持容错、支持无数据丢失的状态迁移， 状态存储可以持续不断的、在后台备份到Kafka主题中。上述用于主题被称为状态存储的changelog主题，或者直接叫changelog。</p>
<p>你可以启用或者禁用状态存储的备份特性。</p>
<p>持久性的KV存储是容错的，它备份在一个紧凑格式的changelog主题中。使用紧凑格式的原因是：</p>
<ol>
<li>防止主题无限增长</li>
<li>减少主题占用的存储空间</li>
<li>当状态存储需要通过Changelog恢复时，缩短需要的时间</li>
</ol>
<p>持久性的窗口化存储也是容错的，它基于紧凑格式、支持删除机制的主题备份。窗口化存储的changelog的键的一部分是窗口时间戳，过期的窗口对应的段会被Kafka的日志清理器清理。changelog的默认存留时间是Windows#maintainMs() + 1天。指定StreamsConfig.WINDOW_STORE_CHANGE_LOG_ADDITIONAL_RETENTION_MS_CONFIG可以覆盖之。</p>
<div class="blog_h3"><span class="graybg">监控状态恢复</span></div>
<p>启动应用程序时，状态存储通常不需要根据changelog来恢复，直接加载磁盘上持久化的数据就可以。但以下场景中：</p>
<ol>
<li>宕机导致本地状态丢失</li>
<li>运行在无状态环境下的应用程序重启</li>
</ol>
<p>状态存储需要基于changelog进行完整的恢复。</p>
<p>如果changelog中的数据量很大，则恢复过程可能相当的耗时。在恢复完成之前，处理器拓扑不能处理新的数据。</p>
<p>要监控状态存储的恢复进度，你需要实现org.apache.kafka.streams.processor.StateRestoreListener接口，并调用KafkaStreams#setGlobalStateRestoreListener注册之。监听器示例：</p>
<pre class="crayon-plain-tag">import org.apache.kafka.common.TopicPartition;
import org.apache.kafka.streams.processor.StateRestoreListener;
// 监听器会被所有org.apache.kafka.streams.processor.internals.StreamThread实例共享，并必须线程安全
public class ConsoleGlobalRestoreListerner implements StateRestoreListener {
    // 在恢复过程开始时回调
    public void onRestoreStart(
            final TopicPartition topicPartition,  // 主题分区
            final String storeName,               // 状态存储名称
            final long startingOffset,            // 需要恢复的起点
            final long endingOffset               // 需要恢复的终点
    ) {}

    // 在恢复一批次数据后回调
    public void onBatchRestored( final TopicPartition topicPartition,
            final String storeName,
            final long batchEndOffset,
            final long numRestored 
    ) {}

    // 恢复完成后回调
    public void onRestoreEnd( final TopicPartition topicPartition,
            final String storeName,
            final long totalRestored ) {}
}</pre>
<div class="blog_h3"><span class="graybg">启/禁changelog</span></div>
<p>禁用changelog之后，状态存储失去容错性：</p>
<pre class="crayon-plain-tag">// 启用：StateStoreBuilder#withLoggingEnabled(Map&lt;String, String&gt;);
// 禁用：StateStoreBuilder#withLoggingDisabled();

KeyValueBytesStoreSupplier countStoreSupplier = Stores.inMemoryKeyValueStore("Counts");
StateStoreBuilder builder = Stores
    .keyValueStoreBuilder(countStoreSupplier,Serdes.String(),Serdes.Long())
    .withLoggingDisabled();</pre>
<p>启用changelog时，你可以为changelog主题提供配置信息：</p>
<pre class="crayon-plain-tag">Map&lt;String, String&gt; changelogConfig = new HashMap();
// 覆盖主题参数
changelogConfig.put("min.insyc.replicas", "1");

Stores...withLoggingEnabled(changelogConfig);</pre>
<div class="blog_h3"><span class="graybg">实现状态存储 </span></div>
<p>除了使用Kafka内置的StateStore实现之外，你还可以自定义状态存储。 </p>
<p>状态存储的核心接口是org.apache.kafka.streams.processor.StateStore，一些扩展的接口包括KeyValueStore。</p>
<p>你还需要一个创建状态存储的工厂，其接口是org.apache.kafka.streams.processor.state.StoreSupplier。</p>
<p>你可以提供一个org.apache.kafka.streams.processor.StateRestoreCallback，用于从changelog中恢复状态存储。要注册此回调，实现StateStore的init方法：</p>
<pre class="crayon-plain-tag">public void init(ProcessorContext context, StateStore store) {
    context.register(store, false, stateRestoreCallBackIntance);
}</pre>
<p>你可以实现org.apache.kafka.streams.processor.BatchingStateRestoreCallback，代替StateRestoreCallback。后者每次恢复一条数据，前者支持批量式的数据恢复。</p>
<p>抽象类AbstractNotifyingRestoreCallback、AbstractNotifyingBatchingRestoreCallback分别实现了StateRestoreCallback、BatchingStateRestoreCallback接口，并同时实现了StateRestoreListener接口。</p>
<div class="blog_h1"><span class="graybg">高级API </span></div>
<p>开发人员可以利用StreamsBuilder，基于Kafka Streams的领域特定语言（DSL）来构建拓扑。</p>
<div class="blog_h2"><span class="graybg">流表二元性</span></div>
<p>高级API引入了表的概念，表和流可以看做是相同数据集不同视图：</p>
<ol>
<li>Stream as Table：一个流可以看做是一个表的changelog。流中的每条记录都捕获了表的一次状态变更，通过Replay changelog，流可以转变为表。流记录和表行不一定是1:1对应关系，流记录可能经过聚合，更新到表中的一行</li>
<li>Table as Stream：表可以看做是某个瞬间的、流中每个键的最终值构成的快照。迭代表中的键值对很容易将其转换为流</li>
</ol>
<p>在Kafka Streams中，状态存储的跨机器复制（容错，基于changelog）就利用了流表二元性。</p>
<div class="blog_h3"><span class="graybg">KStream</span></div>
<p>该接口是对记录的流的抽象，每个记录是无边界的数据集中的一个自包含的数据。 </p>
<p>对于发送到KStream的两个记录("Alex", 1)、("Alex", 3)，假设流处理程序进行sum（人名统计）操作，则返回结果是4。</p>
<div class="blog_h3"><span class="graybg">KTable</span></div>
<p>该接口是对changelog流的抽象，此流中的每个记录代表了针对某个特定key的数据更新。 </p>
<p>对于发送到KTable的两个记录("Alex", 1)、("Alex", 3)，相当于对键Alex进行两次更新，返回结果是3。</p>
<div class="blog_h3"><span class="graybg">GlobalKTable</span></div>
<p>类似于KTable，但是跨越所有KafkaStreams实例进行复制。 GlobalKTable支持通过key来查询value，在进行Join操作时，需要使用这种查询。</p>
<div class="blog_h2"><span class="graybg">创建源流 </span></div>
<p>通过读取Kafka主题，即可为Kafka Streams提供输入流。首先你需要实例化一个StreamsBuilder：</p>
<pre class="crayon-plain-tag">StreamsBuilder builder = new StreamsBuilder();</pre>
<div class="blog_h3"><span class="graybg">创建KStream </span></div>
<pre class="crayon-plain-tag">KStream&lt;String, Long&gt; nameCounts = builder.stream( 
    "names-counts-input-topic",  // 输入主题名称
    Consumed.with(Serdes.String(), Serdes.Long()) // 指定键值的串行化器
);</pre>
<p>KStream对应了从输入主题读取的、分区化的记录的流。流处理程序的每个实例，都会消费输入主题的分区的子集，并且在整体上保证所有分区都被消费。</p>
<div class="blog_h3"><span class="graybg">创建KTable </span></div>
<pre class="crayon-plain-tag">KTable&lt;String, Long&gt; nameCounts = builder.table(
    Serdes.String(), /* 键串行化器 */
    Serdes.Long(),   /* 值串行化器 */
    "name-counts-input-topic", /* 输入主题 */
    "name-counts-partitioned-store" /* 表名 */);</pre>
<p>可以把任何主题看做是changelog，并将其读入到KTable。当：</p>
<ol>
<li>记录的键不存在时，相当于执行INSERT操作</li>
<li>记录的键存在，值不为null时，相当于执行UPDATE操作</li>
<li>记录的键存在，值为null时，相当于执行DELETE操作</li>
</ol>
<p>KTable对应了从输入主题读取的、分区化的记录的流。流处理程序的每个实例，都会消费输入主题的分区的子集，并且在整体上保证所有分区都被消费。</p>
<p>你可以为KTable提供一个名称，此名称也是KTable对应的状态存储的名称。只有命名的KTable才支持交互式查询。</p>
<div class="blog_h3"><span class="graybg">创建GlobalKTable</span></div>
<pre class="crayon-plain-tag">GlobalKTable&lt;String, Long&gt; nameCounts = builder.globalTable(
    Serdes.String(), /* 键串行化器 */
    Serdes.Long(),   /* 值串行化器 */
    "name-counts-input-topic", /* 输入主题 */
    "name-counts-global-store" /* 表名 */);</pre>
<div class="blog_h2"><span class="graybg">对流进行转换 </span></div>
<p>KStream和KTable支持一系列的转换操作，这些高层操作都可以被转换为1-N个连接在一起的处理器。由于KStream、KTable都是强类型的，因此这些转换操作都以泛型的方式定义。</p>
<p>某些KStream转换操作可以产生一个（例如filter/map）或多个（例如branch）KStream对象，另外一些则产生一个KTable对象（例如aggregation）。</p>
<p>对于KTable来说，所有的转换操作都只能产生另一个KTable。</p>
<div class="blog_h3"><span class="graybg">无状态转换</span></div>
<p>不依赖于任何状态即可完成转换，不要求流处理器有关联的StateStore。</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>branch</td>
<td>
<p>IO：KStream → KStream</p>
<p>基于给定的断言集分割KStream，将其分割为1-N个KStream实例。断言按照声明的顺序依次估算，每个记录只被转发到第一个匹配的下游流中：</p>
<pre class="crayon-plain-tag">KStream&lt;String, Long&gt; stream = ...;
KStream&lt;String, Long&gt;[] branches = stream.branch(
        (key, value) -&gt; key.startsWith("A"), /* 以A开头的键  */
        (key, value) -&gt; key.startsWith("B"), /* 以B开头的键 */
        (key, value) -&gt; true                 /* 所有其它的记录均发往此流  */
);</pre>
</td>
</tr>
<tr>
<td>filter</td>
<td>
<p>IO：KStream → KStream 或 KTable → KTable
<p>基于给定的断言，针对每个记录进行估算。估算结果为true的记录进入下游流：</p>
<pre class="crayon-plain-tag">// 仅保留正数值
stream.filter((key, value) -&gt; value &gt; 0);
// 针对一个KTable进行过滤，结果物化到一个StageStore中
Materialized m = Materialized.&lt;String, Long, KeyValueStore&lt;Bytes, byte[]&gt;&gt;as("filtered")
table.filter((key, value) -&gt; value != 0, m);</pre>
</td>
</tr>
<tr>
<td>filterNot</td>
<td>与filter类似，仅仅保留不匹配的</td>
</tr>
<tr>
<td>flatMap</td>
<td>
<p>IO：KStream → KStream
<p>基于一个记录，产生0-N个输出记录：</p>
<pre class="crayon-plain-tag">KStream&lt;String, Integer&gt; transformed = stream.flatMap(
    (key, value) -&gt; {
        // 键值对的列表
        List&lt;KeyValue&lt;String, Integer&gt;&gt; result = new LinkedList&lt;&gt;();
        result.add(KeyValue.pair(value.toUpperCase(), 1000));
        result.add(KeyValue.pair(value.toLowerCase(), 9000));
        return result;
    }
);</pre>
</td>
</tr>
<tr>
<td>flatMapValues</td>
<td>类似于flatMap，但是保持键不变，可能产生多个键相同的记录</td>
</tr>
<tr>
<td>foreach</td>
<td>
<p>IO：KStream → void
<p>终结性操作，针对每个记录执行无状态的操作</p>
<p>需要注意：操作的副作用（例如对外部系统的写）无法被Kafka跟踪，也就是说无法获得Kafka的处理语义保证</p>
<p>示例：<pre class="crayon-plain-tag">stream.foreach((key, value) -&gt; System.out.println(key + " =&gt; " + value));</pre> </p>
</td>
</tr>
<tr>
<td>groupByKey</td>
<td>
<p>IO：KStream → KGroupedStream</p>
<p>分组是进行流/表的聚合操作的前提。分组保证了数据被正确的分区，保证后续操作的正常进行</p>
<p>和分组相关的一个操作是窗口化。利用窗口化，可以将分组后的记录二次分组，形成一个个窗口，然后以窗口为单位进行聚合、Join</p>
<p>仅当流被标记用于重新分区，则此操作才会导致重新分区。该操作不允许修改键或者键类型</p>
<p>示例：</p>
<pre class="crayon-plain-tag">KGroupedStream&lt;byte[], String&gt; groupedStream = stream.groupByKey(
    // 如果键值的类型不匹配配置的默认串行化器，则需要明确指定：
    Serialized.with(
         Serdes.ByteArray(),
         Serdes.String())
);</pre>
</td>
</tr>
<tr>
<td>groupBy</td>
<td>
<p>IO：KStream → KGroupedStream 或 KTable → KGroupedTable
<p>实际上是selectKey+groupByKey的组合</p>
<p>基于一个新的键来分组记录，新键的类型可能和记录旧的键类型不同。当对表进行分组时，还可以指定新的值、值类型</p>
<p>该操作总是会导致数据的重新分区，因此在可能的情况下你应该优选groupByKey，后者仅在必要的时候分区</p>
<p>示例：</p>
<pre class="crayon-plain-tag">KGroupedStream&lt;String, String&gt; groupedStream = stream.groupBy(
    (key, value) -&gt; value,  // 产生键值对value:value并依此分组
    Serialize.with(
         Serdes.String(), /* 键的类型发生改变 */
         Serdes.String())  /* value */
); 

KGroupedTable&lt;String, Integer&gt; groupedTable = table.groupBy(
    // 产生键值对  value:length(value)，并依此分组
    (key, value) -&gt; KeyValue.pair(value, value.length()),
    Serialized.with(
        Serdes.String(), /* 键的类发生改变 */
        Serdes.Integer()) /* 值的类型发生改变  */
);</pre>
</td>
</tr>
<tr>
<td>map</td>
<td>
<p>IO：KStream → KStream
<p>根据一个输入记录产生一个输出记录，你可以修改键值的类型</p>
<pre class="crayon-plain-tag">KStream&lt;byte[], String&gt; stream = ...;
KStream&lt;String, Integer&gt; transformed = stream.map(
    (key, value) -&gt; KeyValue.pair(value.toLowerCase(), value.length()));</pre>
</td>
</tr>
<tr>
<td>mapValues</td>
<td>类似上面，但是仅仅映射值，键不变 </td>
</tr>
<tr>
<td>print</td>
<td>
<p>IO：KStream → void
<p>终结操作，打印记录到输出流中。示例：</p>
<pre class="crayon-plain-tag">stream.print(Printed.toFile("stream.out"));</pre>
</td>
</tr>
<tr>
<td>selectKey</td>
<td>
<p>IO：KStream → KStream
<p>对每个记录分配一个新的键，键类型可能改变。</p>
<pre class="crayon-plain-tag">KStream&lt;String, String&gt; rekeyed = stream.selectKey((key, value) -&gt; value.split(" ")[0])</pre>
</td>
</tr>
<tr>
<td>toStream</td>
<td>
<p>IO：KTable → KStream
<p>将表转换为流：<pre class="crayon-plain-tag">table.toStream();</pre></p>
</td>
</tr>
<tr>
<td>WriteAsText</td>
<td>
<p>IO：KStream → void</p>
<p>终结性操作，将流写出到文件</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">有状态转换 </span></div>
<p>这类转换操作需要依赖于某些状态信息。例如在聚合性操作中，会使用窗口化状态存储来保存上一个窗口的聚合结果。在Join操作中，会使用窗口化状态存储到目前为止接收到的、窗口边界内部的所有记录。</p>
<p>状态存储默认支持容错，如果出现失败，则Kafka Streams会首先恢复所有的状态存储，然后再进行后续的处理。</p>
<p>高级的有状态转换操作包括：聚合、Join，以及针对两者的窗口化支持。</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>aggregate</td>
<td>
<p>IO：KGroupedStream → KTable 或 KGroupedTable → KTable</p>
<p>滚动聚合（Rolling Aggregation）操作，根据分组键对非窗口化的记录的值进行聚合</p>
<p>当对已分组流进行聚合时，你需要提供初始化器（确定聚合初值）、聚合器adder。当聚合已分组表时，你需要额外提供聚合器subtractor。代码示例：</p>
<pre class="crayon-plain-tag">KGroupedStream&lt;Bytes, String&gt; groupedStream = null;
KGroupedTable&lt;Bytes, String&gt; groupedTable = null;
// 聚合一个分组流，值类型从字符串变为整数
KTable&lt;Bytes, Long&gt; aggregatedStream = groupedStream.aggregate(
        () -&gt; 0L, /* 初始化器 */
        ( aggKey, newValue, aggValue ) -&gt; aggValue + newValue.length(), /* 累加器 */
        Serdes.Long(), /* 值的串行化器 */
        "aggregated-stream-store" /* 状态存储的名称 */ );

KTable&lt;Bytes, Long&gt; aggregatedTable = groupedTable.aggregate(
        () -&gt; 0L, /* 初始化器 */
        ( aggKey, newValue, aggValue ) -&gt; aggValue + newValue.length(), /* 累加器 */
        ( aggKey, oldValue, aggValue ) -&gt; aggValue - oldValue.length(), /* 减法器 */
        Serdes.Long(), /* 值的串行化器 */
        "aggregated-table-store" /* 状态存储的名称 */ );</pre>
<p>KGroupedStream的聚合操作的行为：</p>
<ol>
<li>值为null的记录被忽略</li>
<li>当首次收到某个新的记录键时，初始化器被调用</li>
<li>每当接收到非null值的记录时，累加器被调用</li>
</ol>
<p>KGroupedTable的聚合操作的行为：</p>
<ol>
<li>值为null的记录被忽略</li>
<li>当首次收到某个新的记录键时，初始化器被调用（在调用累加器/减法器之前）。与KGroupedStream不同，随着时间的推移，针对一个键，可能调用初始化器多次。只要接收到目标键的墓碑记录</li>
<li>当首次收到某个键的非null值时（INSERT操作），调用累加器</li>
<li>当非首次收到某个键的非null值时（UPDATE操作）：
<ol>
<li>调用减法器，传入存储在KTable表中的旧值</li>
<li>调用累加器，传入刚刚接收到的新值</li>
<li>上述两个聚合器的执行顺序未定义</li>
</ol>
</li>
<li>当接收到墓碑记录（DELETE操作）亦即null值的记录时，调用减法器</li>
<li>不论何时，减法器返回null时都会导致相应的键从结果KTable表中删除。遇到相同键的下一个记录时，会执行第3步的行为</li>
</ol>
</td>
</tr>
<tr>
<td>
<p>windowedBy</p>
<p>+</p>
<p>aggregate</p>
</td>
<td>
<p>IO：KGroupedStream → KTable</p>
<p>窗口化聚合：以窗口为单位，根据分组键，对KGroupedStream中的记录进行聚合操作，并把结果存放到窗口化的KTable</p>
<p>你需要提供初始化器、累加器、窗口定义。如果基于会话进行窗口定义，你需要额外提供聚合器——会话合并器</p>
<p>示例代码：</p>
<pre class="crayon-plain-tag">KGroupedStream&lt;String, Long&gt; groupedStream = null;

// 基于时间的窗口化（滚动窗口）
KTable&lt;Windowed&lt;String&gt;, Long&gt; timeWindowedAggregatedStream = groupedStream
        .windowedBy( TimeWindows.of( TimeUnit.MINUTES.toMillis( 5 ) ) )
        .aggregate(
                () -&gt; 0L, /* 初始化器 */
                ( aggKey, newValue, aggValue ) -&gt; aggValue + newValue, /* 累加器 */
                /* 状态存储 */
                Materialized.&lt;String, Long, WindowStore&lt;Bytes, byte[]&gt;&gt;as( "time-windowed-aggregated-stream-store" )
                        .withValueSerde( Serdes.Long() ) );
// 基于会话的窗口化
KTable&lt;Windowed&lt;String&gt;, Long&gt; sessionizedAggregatedStream = groupedStream
        .windowedBy( SessionWindows.with( TimeUnit.MINUTES.toMillis( 5 ) ) ) /* 窗口定义 */
        .aggregate(
                () -&gt; 0L, /* 初始化器 */
                ( aggKey, newValue, aggValue ) -&gt; aggValue + newValue, /* 累加器 */
                ( aggKey, leftAggValue, rightAggValue ) -&gt; leftAggValue + rightAggValue, /* 会话合并器 */
                Materialized.&lt;String, Long, SessionStore&lt;Bytes, byte[]&gt;&gt;as( "sessionized-aggregated-stream-store" ).withValueSerde( Serdes.Long() ) );</pre>
<p>窗口化聚合操作的行为如下：</p>
<ol>
<li>类似于非窗口化的聚合，但是操作应用到每个窗口</li>
<li>键为null的记录被忽略</li>
<li>对于一个给定的窗口，收到某个键的第一个记录时，调用初始化器</li>
<li>对于一个给定的窗口，收到非空值记录时，调用累加器</li>
<li>当使用基于会话的窗口时，每当两个会话被合并时，会话合并器被调用</li>
</ol>
</td>
</tr>
</tbody>
</table>
<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>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/kafka-streams-study-note">Kafka Streams学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/kafka-streams-study-note/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Apache Kafka学习笔记</title>
		<link>https://blog.gmem.cc/apache-kafka-study-note</link>
		<comments>https://blog.gmem.cc/apache-kafka-study-note#comments</comments>
		<pubDate>Tue, 13 Jun 2017 01:56:01 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[BigData]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=16624</guid>
		<description><![CDATA[<p>简介 Apache Kafka（音标/'ka:fka:/）是一个分布式的实时数据处理的基础平台，能够处理每秒百万条数据。它具有三大功能： 订阅/发布：类似于传统MOM的功能，将队列、主题合二为一 流处理：支持编写可扩容的流处理程序，对实时事件做出响应 存储：安全的存储数据流，支持分布式、复制的、容错等特性 Kafka非常适用于以下几类应用场景： 构建能够可靠的在系统或应用程序之间收发实时流的数据管线 构建能够转换、处理流数据的实时应用程序 Kafka天生使用集群架构，由1-N个服务器构成的集群组成。Kafka集群对流记录进行分类存储，每个类别叫做主题（Topic）。每个流记录包含一个键、一个值、一个时间戳。 Kafka提供四类核心API： Producer API：允许应用程序发布流记录到Kafka主题 Consumer API：允许应用程序订阅主题，并处理发布到这些主题的流记录 Streams API：允许一个应用程序作为流处理器（Strem Processor），消费来自1-N个主题的输入流，并向1-N个主题释放输出流 —— 也就是高效的进行流转换 Connector API： <a class="read-more" href="https://blog.gmem.cc/apache-kafka-study-note">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/apache-kafka-study-note">Apache Kafka学习笔记</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 Kafka（音标/'ka:fka:/）是一个分布式的实时数据处理的基础平台，能够处理每秒百万条数据。它具有三大功能：</p>
<ol>
<li>订阅/发布：类似于传统MOM的功能，将队列、主题合二为一</li>
<li>流处理：支持编写可扩容的流处理程序，对实时事件做出响应</li>
<li>存储：安全的存储数据流，支持分布式、复制的、容错等特性</li>
</ol>
<p>Kafka非常适用于以下几类应用场景：</p>
<ol>
<li>构建能够可靠的在系统或应用程序之间收发实时流的数据管线</li>
<li>构建能够转换、处理流数据的实时应用程序</li>
</ol>
<p>Kafka天生使用集群架构，由1-N个服务器构成的集群组成。Kafka集群对流记录进行分类存储，每个类别叫做主题（Topic）。每个流记录包含一个键、一个值、一个时间戳。</p>
<p>Kafka提供四类核心API：</p>
<ol>
<li>Producer API：允许应用程序发布流记录到Kafka主题</li>
<li>Consumer API：允许应用程序订阅主题，并处理发布到这些主题的流记录</li>
<li>Streams API：允许一个应用程序作为流处理器（Strem Processor），消费来自1-N个主题的输入流，并向1-N个主题释放输出流 —— 也就是高效的进行流转换</li>
<li>Connector API： 允许构建可重用的生产者、消费者，把Kafka和既有的应用程序和数据系统连接起来。例如，针对RDBMS的连接器能够捕获针对表的每一个写操作</li>
</ol>
<p>客户端和Kafka的通信协议基于简单、高效、语言无关的方式设计，以TCP协议为基础，支持版本化（向后兼容）。Kafka提供了Java以及其它语言的客户端。</p>
<div class="blog_h2"><span class="graybg">主题和日志</span></div>
<p>主题即记录的流，Kafka中的主题可以具有多个订阅者。对于每个主题，Kafka集群维护分区化（partitioned）存储的日志：</p>
<ol>
<li>每个分区是有序的、不可变的记录的序列，仅仅支持向序列的尾部添加记录</li>
<li>分区中的每个记录被分配一个序列号 —— offset，此序列号在分区中唯一的标识记录</li>
<li>集群在一个可配置的时间段内保留记录，不管记录有没有消费过，这个行为不同于传统MOM，后者不保留消费过的消息。不管配置的保留时间有多长，都不会影响Kafka的性能，这意味着你可以将Kafka作为数据库使用</li>
</ol>
<p>每个消费者需要记录它当前正在消费的记录的偏移量（游标），通常情况下，消费者会线性的向前单步移动偏移量，这和传统MOM消费者的行为对应。但是，Kafka允许消费者任意移动游标，这个特性可用于实现Replay功能。</p>
<div class="blog_h2"><span class="graybg">分布式</span></div>
<p>上述主题日志的分区，可以：</p>
<ol>
<li>跨越Kafka集群中多个服务器存储</li>
<li>每个分区可以具有可配置数量的副本，用于容错</li>
</ol>
<p>在启用复制的情况下，持有同一分区的多个服务器，其中一个被选举为Leader，其它则作为Followers。<span style="background-color: #c0c0c0;">Leader处理所有针对分区的读写请求，Followers则仅仅进行复制</span>。每个服务器都作为一些分区的Leader，另一些分区的Follower，以实现负载均衡。</p>
<div class="blog_h2"><span class="graybg">生产者</span></div>
<p>生产者负责发布记录到主题上，它决定把哪个记录分配到哪个分区上。可选的策略例如循环轮询（round-robin）、某种和应用语义有关的方式（依据记录的某些键）。</p>
<div class="blog_h2"><span class="graybg">消费者</span></div>
<p>Kafka的一个重要设计特点是，不区分主题、队列。这个特点是它灵活的消费模型提供的。</p>
<p>消费者可以将自己标记为属于某个消费者组（consumer group），发布到主题的每个记录会递送给消费者组消费，<span style="background-color: #c0c0c0;">仅仅组中一个成员可以消费某个记录</span>。这意味着：</p>
<ol>
<li>某个主题仅仅包含单个消费者组：这种配置相当于传统MOM的多消费者的队列</li>
<li>某个主题包含多个单成员的消费者组：这种配置相当于传统MOM的主题</li>
</ol>
<p>Kafka中消费行为的实现方式是：根据消费者数量，对日志分区进行划分。在任意时刻，每个消费者都公平的享有某一部分分区，此行为由Kafka协议动态处理。如果新的消费者加入，它将从其它消费者那里接管一部分分区。如果某个消费者宕机，它拥有的分区将由剩余的消费者瓜分。</p>
<p><span style="background-color: #c0c0c0;">Kafka仅仅在分区内部保证顺序性</span>，在大部分情况下，基于记录键的分区+分区内有序是足够满足需要的。如果一定需要全局性顺序保证，你可以设计仅仅包含单个分区的主题，并且对于每个消费者组，仅仅有单个消费者。</p>
<div class="blog_h2"><span class="graybg">保证</span></div>
<p>Kafka提供以下保证：</p>
<ol>
<li>一个生产者发布到某个特定分区的记录，保证按照其发送顺序附加到日志的尾部</li>
<li>消费者看到的记录顺序，和它们在分区中存储的顺序一致</li>
<li>对于具有复制因子N的主题，Kafka允许N-1服务器同时宕机，而不丢失任何已经提交到日志的记录</li>
</ol>
<div class="blog_h2"><span class="graybg">作为...的Kafka</span></div>
<div class="blog_h3"><span class="graybg">消息系统</span></div>
<p>传统的消息系统模型有两种：队列、订阅/发布。队列的优势是容易实现消费者的负载均衡，但是消息一旦被消费就消失不能供多人消费，订阅/发布（主题）的优缺点则刚好相反。</p>
<p>Kafka的创新之处在于，将队列、主题合而为一。</p>
<p>Kafka还提供比传统MOM更强的有序性。尽管传统MOM支持多个消费者，消息也是按序被消费的，但是这些消费者接收消息是异步的，顺序无法保证。Kafka的分区机制能够增强有序性，分区被特定单个消费者消费，不会出现顺序混乱的情况。</p>
<div class="blog_h3"><span class="graybg">存储系统</span></div>
<p>Kafka主题中的记录被消费后，不会消失，这让它可以作为in-flight消息的存储系统。</p>
<p>事实上，Kafka是一个优秀的存储系统，因为：</p>
<ol>
<li>写入的数据被复制，支持容错。Kafka允许生产者等待一个Ack —— 记录被合理复制、持久化后才认为操作成功</li>
<li>磁盘数据结构非常可扩容，不管是存储10KB还是10TB记录，性能不会有显著变化</li>
</ol>
<div class="blog_h3"><span class="graybg">流处理系统</span></div>
<p>Kafka不仅仅能够用来读取、写入、存储数据流，它还能用来进行实时流处理。</p>
<p>在Kafka中，一个流处理器是这样的一个程序：它接受持续不断的数据流输入（从主题），处理后产生持续不断的数据流输出（到主题）。</p>
<p>使用Producer/Consumer可以实现先单的流处理器，更复杂的需求可以基于Streams API实现。Streams API支持聚合、连接等操作，支持乱序数据处理、输入再加工、有状态计算等应用场景。</p>
<p>Streams API在Kafka提供的核心原语上构建，它使用生产者/消费者API作为输入，使用Kafka作为状态存储，使用组机制实现多个流处理器的负载均衡。</p>
<div class="blog_h3"><span class="graybg">小结</span></div>
<p>联用Kafka的存储+低延迟订阅功能，可以让你处理历史、未来数据的方式变得一致 —— 基于Kafka的应用程序可以处理历史数据，当它处理完最后一个记录时，不需要退出，只需要等待未来的数据到达即可继续处理。Kafka的可靠存储功能，让你可以很容易的集成某些需要定期下线维护的系统。</p>
<div class="blog_h2"><span class="graybg">典型应用场景</span></div>
<div class="blog_h3"><span class="graybg">消息传输</span></div>
<p>消息代理可以用于解耦消息生产者和消费者、缓冲待处理消息，Kafka可以作为传统消息代理的替代品。</p>
<p>和大部分消息代理相比，Kafka具有更好的吞吐能力，并内置数据分区、容错等特性。</p>
<div class="blog_h3"><span class="graybg">活动跟踪</span></div>
<p>Kafka的一个原始的应用场景是，辅助重建用户活动跟踪的管线。用户的活动（页面浏览、搜索等）被按照活动类型收集并发布到不同的主题上。订阅这些主题后，可以进行实时处理、实时监控、加载到Hadoop以便离线处理。</p>
<p>活动跟踪的数据量通常情况下比消息传输大的多。</p>
<div class="blog_h3"><span class="graybg">度量记录</span></div>
<p>Kafka可以用记录、处理监控数据。它可以从分布式应用中获取度量信息并进行聚合操作。</p>
<div class="blog_h3"><span class="graybg">日志聚合</span></div>
<p>Kafka可以用来从多个服务器上收集日志，并集中起来（例如存储到HDFS）以处理。Kafka抽象掉日志文件的概念，日志编程一系列消息组成的流。</p>
<div class="blog_h3"><span class="graybg">流处理</span></div>
<p>从0.10开始Kafka内置了一个轻量的流处理框架Kafka Streams，使用此框架可以构建数据处理管线，针对数据进行聚合、转换等操作。</p>
<p>Apache Storm等流处理框架可以和Kafka很好的集成。</p>
<div class="blog_h3"><span class="graybg">提交日志</span></div>
<p>Kafka可以作为分布式系统的外部提交日志（commit-log）。提交日志用于辅助节点之间的数据复制、失败节点的再同步。Kafka的log compaction特性用于支持这一应用场景，在这种场景下Kafka类似于Apache BookKeeper。</p>
<div class="blog_h1"><span class="graybg">起步</span></div>
<div class="blog_h2"><span class="graybg">下载安装</span></div>
<p>到Kafka官网<a href="https://www.apache.org/dyn/closer.cgi?path=/kafka/1.0.0/kafka_2.11-1.0.0.tgz">下载压缩包</a>，并解压。Kafka是基于JVM的应用，因此你需要事先安装好JRE。</p>
<div class="blog_h2"><span class="graybg">启动服务</span></div>
<div class="blog_h3"><span class="graybg">启动ZooKeeper</span></div>
<p>Kafka依赖于ZooKeeper，你可以使用外部的ZooKeeper服务器。或者，利用Kafka提供的脚本，创建一个简单的单节点ZooKeeper实例：</p>
<pre class="crayon-plain-tag">pushd ~/JavaEE/middleware/kafka/ &gt; /dev/null
# 启动ZooKeeper
zookeeper-server-start.sh config/zookeeper.properties</pre>
<div class="blog_h3"><span class="graybg">启动Kafka</span></div>
<pre class="crayon-plain-tag">kafka-server-start.sh config/server.properties</pre>
<div class="blog_h2"><span class="graybg">创建主题</span></div>
<pre class="crayon-plain-tag"># 连接到ZooKeeper，来操控Kafka
# replication-factor 复制因子，也就是数据复制的份数
# partitions 分区数量
kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic test</pre>
<p>除了手工创建主题外，还可以配置，让代理在有客户端尝试发布消息时自动创建不存在的主题。</p>
<p>执行下面的命令可以列出现有的主题：</p>
<pre class="crayon-plain-tag">kafka-topics.sh --list --zookeeper localhost:2181</pre>
<div class="blog_h2"><span class="graybg">发布消息</span></div>
<p>Kafka提供了一个命令，用于从标准输入或者文件中读取信息，并发布到到主题上：</p>
<pre class="crayon-plain-tag">kafka-console-producer.sh --broker-list localhost:9092 --topic test</pre>
<p>默认的每一行输入都作为单独的消息发送。</p>
<div class="blog_h2"><span class="graybg">消费消息</span></div>
<p>Kafka提供了一个命令，用于消费消息并将其内容打印到标准输出：</p>
<pre class="crayon-plain-tag"># 从头开始消费
kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic test --from-beginning</pre>
<p>如果你在两个Terminal同时打开生产者、消费者，然后在生产者那里输入文字并回车，文字会原样的出现在消费者窗口中。</p>
<div class="blog_h2"><span class="graybg">代理集群</span></div>
<p>对于Kafka来说，单个代理也是集群，也就是说它天然是集群的。要配置包含多个代理实例的集群，没有什么特别之处。本节演示如何创建三个实例组成的集群。</p>
<div class="blog_h3"><span class="graybg">拷贝配置文件</span></div>
<pre class="crayon-plain-tag">cp config/server.properties config/server-1.properties
cp config/server.properties config/server-2.properties</pre>
<div class="blog_h3"><span class="graybg">修改配置文件</span></div>
<p>参考下面的示例修改：</p>
<pre class="crayon-plain-tag"># 对于每个集群中的任何一个节点，id必须是唯一的、永久不变的
broker.id=1
# 由于我们准备在一台机器上启动多个代理实例，因此需要定制某些参数：
listeners=PLAINTEXT://:9093
log.dir=/tmp/kafka-logs-1</pre>
<div class="blog_h3"><span class="graybg">启动额外实例</span></div>
<p>执行下面的命令启动两个额外的实例，加上本章最初创建的那个，组成三成员的集群：</p>
<pre class="crayon-plain-tag">kafka-server-start.sh config/server-1.properties
kafka-server-start.sh config/server-2.properties</pre>
<div class="blog_h3"><span class="graybg">复制的主题</span></div>
<pre class="crayon-plain-tag"># 复制因子3，表示数据复制三份
# 分区数量1
kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 3 --partitions 1  --topic reptest</pre>
<div class="blog_h3"><span class="graybg">查看主题信息</span></div>
<p>执行下面的命令，可以看到刚刚创建的主题的相关信息，以及各代理实例和主题的关系：</p>
<pre class="crayon-plain-tag">kafka-topics.sh --describe --zookeeper localhost:2181 --topic reptest
# Topic:reptest	PartitionCount:1	ReplicationFactor:3	Configs:
# 对于每个分区（本主题只有一个分区），列出以下信息
# Leader 负责该分区所有读写操作的节点（代理实例）ID
# Replicas 复制此分区的所有节点列表，不管是否alive，不管是否leader
# Isr 和Leader保持同步（in-sync）的节点，这些节点必然是alive的
# 	Topic: reptest	Partition: 0	Leader: 0	Replicas: 0,1,2	Isr: 0,1,2 </pre>
<div class="blog_h3"><span class="graybg">导入导出数据</span></div>
<p>使用Kafka Connect这一工具，你可以从其它数据源导入数据到kafka，或者将Kafka中的数据导出到其它系统。对于很多系统来说，导入导出不需要手工编写集成代码。</p>
<div class="blog_h3"><span class="graybg">流式数据处理</span></div>
<p>Kafka Streams是一个客户端库，用于构建输入/输出数据存储于Kafka集群的实时应用或者微服务，并满足可扩容、弹性、容错、分布式的质量需求。</p>
<div class="blog_h1"><span class="graybg">API概览 </span></div>
<p>Kafka提供了5类核心API：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">API</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>Producer</td>
<td>发布数据流到Kafka集群的主题上</td>
</tr>
<tr>
<td>Consumer</td>
<td>从Kafka集群的主题上读取数据流</td>
</tr>
<tr>
<td>Streams</td>
<td>在输入主题、输出主题之间进行数据流的转换</td>
</tr>
<tr>
<td>Connect</td>
<td>实现连接器，可以持续的从外部数据源拉拉取数据并发布到主题，或者持续的将主题推送到外部应用或者Sink系统</td>
</tr>
<tr>
<td>AdminClient</td>
<td>管理、查看代理、主题以及其它Kafka对象</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">生产者</span></div>
<p>要使用此API，可以引入Maven依赖：</p>
<pre class="crayon-plain-tag">&lt;dependency&gt;
    &lt;groupId&gt;org.apache.kafka&lt;/groupId&gt;
    &lt;artifactId&gt;kafka-clients&lt;/artifactId&gt;
    &lt;version&gt;1.0.0&lt;/version&gt;
&lt;/dependency&gt;</pre>
<div class="blog_h3"><span class="graybg">KafkaProducer</span></div>
<p>此类是一个Kafka客户端，用于发送记录到Kafka集群。此类是线程安全的，使用单个实例通常性能更好。示例代码：</p>
<pre class="crayon-plain-tag">package cc.gmem.study.kafka;

import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.Producer;
import org.apache.kafka.clients.producer.ProducerRecord;

import java.util.Properties;

/**
 * Entrypoint
 */
public class Application {

    public static void main( String[] args ) {
        // 指定生产者配置
        Properties props = new Properties();
        props.put( "bootstrap.servers", "172.21.3.1:9092,172.21.3.2:9092,172.21.3.3:9092" );
        props.put( "acks", "all" );  // 仅当所有Replica确认后，才认为记录提交成功
        props.put( "retries", 0 );   // 不重试，注意重试可能引起重复消息
        props.put( "batch.size", 16384 ); // 每个分区可以占用的缓冲区大小
        props.put( "linger.ms", 1 ); // 即使缓冲区有空间，批次也可能立即被发送，此配置引入延迟
        props.put( "buffer.memory", 33554432 ); // 最大可用缓冲区
        // 如何把键值转换为字节
        props.put( "key.serializer", "org.apache.kafka.common.serialization.StringSerializer" );
        props.put( "value.serializer", "org.apache.kafka.common.serialization.StringSerializer" );
        /**
         * 创建生产者
         * 生产者主要由一个缓冲池（存放尚未发送到服务器的记录）和一个后台IO线程（负责将记录转换为请求发送）组成
         *
         */
        Producer&lt;String, String&gt; producer = new KafkaProducer&lt;&gt;( props );

        for ( int i = 0; i &lt; 10; i++ ) {
            String topic = "TEST";
            String key = "KEY-" + Integer.toString( i );
            String value = "VALUE-" + Integer.toString( i );
            /**
             * 生产者记录：需要发送到特定Kafka主题的键值对
             *
             * 你可以为记录指定一个分区号，这样记录会发送到该分区中。如果不指定分区号但是给出了键，则键的哈希
             * 用于确定分区号。如果分区号、键都没有指定，则以循环轮流的方式发送到所有可用分区
             *
             * 你还可以为记录指定一个时间戳，如果不指定默认使用当前时间
             * 如果主题配置为TimestampType#CREATE_TIME，则上述时间戳将被代理使用
             * 如果主题配置为TimestampType#LOG_APPEND_TIME，则代理覆盖记录的时间戳，以实际添加到日志时代理的本地时间为准
             * 实际生效的时间戳将通过RecordMetadata返回给客户端
             *
             */
            ProducerRecord&lt;String, String&gt; record = new ProducerRecord&lt;&gt;( topic, key, value );
            /**
             * 发送方法是异步的，它只是把消息存放到缓冲区中然后立即返回
             * 处于效率考虑，生产者可能批量的发送记录
             */
            producer.send( record );
        }
        /**
         * 必须要保证生产者的关闭，否则线程、缓冲区资源无法释放
         */
        producer.close();
    }
}</pre>
<p>从0.11开始KafkaProducer支持两种额外的模式：幂等模式、事务模式。</p>
<div class="blog_h3"><span class="graybg">幂等模式</span></div>
<p>该模式增强了递送语义，将至少一次递送增强为精确的一次递送。注意：</p>
<ol>
<li>这种幂等性仅仅在单个会话内部保证</li>
<li>客户端的retry不会引发记录的重复</li>
</ol>
<p>要启用该模式，需要配置<pre class="crayon-plain-tag">enable.idempotence=true</pre>。执行此配置后，retries默认值为 Integer.MAX_VALUE 且ack默认值为all。</p>
<p>幂等模式下，API的调用方式无变化。</p>
<div class="blog_h3"><span class="graybg">事务模式</span></div>
<p>该模式允许客户端原子的向多个分区、甚至多个主题发送数据。</p>
<p>要启用该模式，需要设置<pre class="crayon-plain-tag">transactional.id</pre>。如果设置了此属性则幂等性会自动开启。参与到事务中的主题应当具有至少3的复制因子且这些主题的 min.insync.replicas至少为2。</p>
<p>为了保证端到端的事务性，消费者应该设置为仅仅读取已提交消息。</p>
<p>transactional.id的目的能够跨越单个生产者实例的多个会话进行事务恢复。此ID通常根据分片的、有状态的应用程序的分片标识符（Shard Identifier）产生。对于一个分片应用程序中的每个生产者实例来说，此ID必须唯一。</p>
<p>事务性的API是阻塞的，并且失败时会抛出异常，示例：</p>
<pre class="crayon-plain-tag">Properties props = new Properties();
props.put( "bootstrap.servers", "localhost:9092" );
// 每个生产者同时只能打开一个事务
props.put( "transactional.id", "my-transactional-id" );
Producer&lt;String, String&gt; producer = new KafkaProducer&lt;&gt;( props, new StringSerializer(), new StringSerializer() );
/**
 * 在事务模式下，必须首先调用下面的方法。此方法的行为如下：
 * 1、确保由先前实例发起的、相同ID的事务已经完成。如果先前实例在进行事务的过程中失败了，其事务在此被中止
 *    如果上一个事务正在提交，此方法会等待其完成
 * 2、获得内部的生产者id、epoch，并在所有事务性消息上使用
 */
producer.initTransactions();

try {
    // 只要启用了事务模式，任何记录都必须在事务中发送
    // 启动一个新事务
    producer.beginTransaction();
    for ( int i = 0; i &lt; 10; i++ ) {
        String topic = "test";
        String key = "KEY-" + Integer.toString( i );
        String value = "VALUE-" + Integer.toString( i );
        // 不需要注册send的回调或者使用get()获得Future对象
        producer.send( new ProducerRecord&lt;&gt;( topic, key, value ) );
    }
    /**
     * 提交事务，此方法会首先刷出所有尚未发送的消息，然后再提交
     * 任何一次send()调用出现不可恢复的错误，该方法会再接收到错误后立即抛出异常，事务无法提交 
     * 为了事务能提交，所有send()必须按序的成功
     */
    producer.commitTransaction();
} catch ( ProducerFencedException | OutOfOrderSequenceException | AuthorizationException e ) {
    // 无法从这些异常中恢复
    producer.close();
} catch ( KafkaException e ) {
    // 对于其他类型的异常，可以中止然后重试
    // 中止后，所有已经成功的写操作会被标记为aborted
    producer.abortTransaction();
}
producer.close(); </pre>
<div class="blog_h2"><span class="graybg">消费者</span></div>
<p>要使用此API，可以引入Maven依赖：</p>
<pre class="crayon-plain-tag">&lt;dependency&gt;
    &lt;groupId&gt;org.apache.kafka&lt;/groupId&gt;
    &lt;artifactId&gt;kafka-clients&lt;/artifactId&gt;
    &lt;version&gt;1.0.0&lt;/version&gt;
&lt;/dependency&gt;</pre>
<div class="blog_h3"><span class="graybg">KafkaConsumer</span></div>
<p>使用该类能够从Kafka集群消费记录。此客户端：</p>
<ol>
<li>能够透明的处理代理实例的失败。也就是说，如果代理失败导致分区迁移到其他代理，不需要手工处理</li>
<li>可以使用消费者组，与代理进行交互，让一组消费者进行负载均衡</li>
</ol>
<p>与生产者不同，消费者不是线程安全的。</p>
<p>消费者需要维持到必要的代理的TCP连接，以抓取数据。不再使用消费者后，必须关闭它，否则这些连接会泄漏。</p>
<div class="blog_h3"><span class="graybg">偏移量和消费位置</span></div>
<p>Kafka为分区中每个记录都维护一个数字的偏移量（Offset），这个偏移量是记录在分区中的唯一标识。</p>
<p>偏移量也用于标记消费者针对分区的当前消费位置（Position），例如消费者当前位置为5则意味着它已经消费过0-4记录并且下一个将被消费的记录是5。</p>
<p>对于消费者来说，“位置”有两层含义：</p>
<ol>
<li>下一个将要读取的记录的偏移量，比消费者消费的最后一个记录的偏移量大1。每当消费者poll(long)了新记录后此位置自动更新</li>
<li>已提交位置（Committed Position），指已经被安全存储的偏移量值。消费者如果失败，重启后将根据此值进行恢复。该位置信息可以自动的定期提交。或者调用commitSync/commitAsync显式的提交</li>
</ol>
<div class="blog_h3"><span class="graybg">消费者组和主题订阅</span></div>
<p>Kafka引入消费者组（Consumer Group）的概念，来允许一池子的消费者划分消费、处理记录的工作。这些消费者可以运行在同一台或者多台机器上允许。任何具有相同<pre class="crayon-plain-tag">group.id</pre>的消费者都属于同一个组。</p>
<p>通过subscribe接口，组中的每个消费者都可以动态的订阅主题。Kafka会把每个消息递送给组中的单个消费者，具体实现方式是，通过负载均衡，让主题的任一分区仅供单个消费者来消费，组中的消费者会被尽可能的分配相同个数的分区。</p>
<p>组中的成员是动态维护的，如果某个消费者宕掉，分配给它的分区会重新分配给组中其它成员。当新消费者加入后，既有消费者持有的分区会转移给它。此所谓再平衡（Rebalancing）。当发生再平衡时，消费者可以通过 ConsumerRebalanceListener得到通知，从而进行状态清理、手工位置提交等操作。</p>
<p>从概念上，你可以把组看做单个逻辑的消费者。这个逻辑消费者不会重复的消费某个消息。</p>
<div class="blog_h3"><span class="graybg">检测消费者失败</span></div>
<p>当订阅1-N个主题后，消费者首次调用poll(long)时会加入到消费者组。poll的实现能够监测消费者是否失败。只要你继续调用poll，消费者就会保持组成员的身份，并且持续的接收消息。在底层，消费者会定期的发送心跳给代理，如果消费者失败且代理在session.timeout.ms内没有收到心跳，就革除消费者的组成员资格并触发再平衡。</p>
<p>某些情况下，消费者虽然没有崩溃，但是它却进入一种livelock的状态。也就是说，尽管消费者仍然在正常发送心跳，但是它已经不能进行任何消息处理了。为了检测这一情况Kafka引入了max.poll.interval.ms配置，如果消费者在此时间内没有发起过poll()调用，代理会主动革除其组成员资格并触发再平衡。如果这种革除操作发生后，消费者进行位置提交时会收到CommitFailedException。为了保持组成员资格，消费者必须不断的poll。</p>
<p>增大max.poll.interval.ms参数，消费者可以用更充足的时间来处理poll()得到的记录。这样做的缺点是，再平衡可能延迟，因为仅在poll()调用时，消费者才能参与到再平衡中。</p>
<p>配置max.poll.records参数可以限制单次poll()操作能够返回的记录数量。通过减小此参数，你就能相应的调低max.poll.interval.ms。</p>
<p>如果消息处理时间无法估算，你可能需要将处理消息的逻辑从poll()线程中转移出去，由独立的线程处理。这样做的话你需要小心的管理提交位置，因为只有前述的独立线程才知道何时消息处理完毕。通常情况下需要禁用自动的位置提交，由独立线程手工的进行提交。</p>
<p>如果消息抓取的快而处理的相对较慢，你可以考虑pause()目标分区，这样poll()操作就不会抓取新的记录。</p>
<div class="blog_h3"><span class="graybg">手工和自动提交</span></div>
<p>使用自动偏移量提交，可以保证最少一次递送语义。前提是，你必须在后续调用（或者关闭消费者）之前，消费上一次poll()返回的全部数据。如果无法保证全部消费，自动提交的偏移量可能比实际成功消费的偏移量大，导致记录遗漏。</p>
<p>自动偏移量提交的消费者代码示例：</p>
<pre class="crayon-plain-tag">Properties props = new Properties();
props.put( "bootstrap.servers", "172.21.3.1:9092,172.21.3.2:9092,172.21.3.3:9092" );
props.put( "group.id", "group0" );
// 启用自动的位置提交
props.put( "enable.auto.commit", "true" );
// 自动提交的间隔
props.put( "auto.commit.interval.ms", "1000" );
props.put( "key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer" );
props.put( "value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer" );
KafkaConsumer&lt;String, String&gt; consumer = new KafkaConsumer&lt;&gt;( props );
// 可以同时订阅多个主题
consumer.subscribe( Arrays.asList( "test", "hello" ) );
while ( true ) {
    // poll并等待100ms
    ConsumerRecords&lt;String, String&gt; records = consumer.poll( 100 );
    for ( ConsumerRecord&lt;String, String&gt; record : records ) {
        String topic = record.topic();
        int partition = record.partition();
        long offset = record.offset();
        String key = record.key();
        String value = record.value();
        LOGGER.debug( "Record from {}/{}, offset: {}, Key: {}, Value: {}", topic, partition, offset, key, value );
    }
}</pre>
<p>手工偏移量提交的消费者代码示例：</p>
<pre class="crayon-plain-tag">// ...
props.put( "enable.auto.commit", "false" );
// ...
final int minBatchSize = 200;
while ( true ) {
    ConsumerRecords&lt;String, String&gt; records = consumer.poll( 100 );
    buffer.add( records );
    if ( buffer.size() &gt;= minBatchSize ) {
        // 在这里进行批量处理，例如持久化
        // 手工提交一次偏移量，这意味着之前收到的消息均被标记为已提交
        consumer.commitSync();
        buffer.clear();
    }
}</pre>
<p>某些情况下，你可能希望对偏移量的提交进行细粒度的控制，例如按分区单独提交：</p>
<pre class="crayon-plain-tag">try {
    while ( running ) {
        ConsumerRecords&lt;String, String&gt; records = consumer.poll( Long.MAX_VALUE );
        // 按分区处理
        for ( TopicPartition partition : records.partitions() ) {
            // 获取当前分区的所有记录
            List&lt;ConsumerRecord&lt;String, String&gt;&gt; partitionRecords = records.records( partition );
            for ( ConsumerRecord&lt;String, String&gt; record : partitionRecords ) {
                // 消费
            }
            // 获取最后一个记录的偏移量
            long lastOffset = partitionRecords.get( partitionRecords.size() - 1 ).offset();
            // 提交当前分区的消费偏移量：必须指向下一个需要被消费的记录
            consumer.commitSync( Collections.singletonMap( partition, new OffsetAndMetadata( lastOffset + 1 ) ) );
        }
    }
} finally {
    consumer.close();
}</pre>
<div class="blog_h3"><span class="graybg">手工分区分配</span></div>
<p>上面的两个例子中，分区由Kafka公平的分配给组中的消费者实例。某些情况下，你可能需要更细粒度、主动的控制，例如：</p>
<ol>
<li>消费者在本地存储了状态，这些状态和特定的分区相关。因此消费者仅应抓取目标分区的记录</li>
<li>消费者本身具有HA特性，例如它被YARN之类的集群管理框架管理、属于Storm等流处理框架的一部分。这种情况下，不需要Kafka进行故障检测，因为消费者失败后会自动的重新启动</li>
</ol>
<p>要手工分配分区，可以使用assign代替subscribe调用：</p>
<pre class="crayon-plain-tag">String topic = "test";
TopicPartition partition0 = new TopicPartition(topic, 0);
TopicPartition partition1 = new TopicPartition(topic, 1);
// 将分区0、1分配给消费者
consumer.assign(Arrays.asList(partition0, partition1));</pre>
<p>执行上述分配后，你就可以进行poll。消费者的配置参数group.id仍然用于偏移量的提交。除非再次调用assign，分区和消费者的映射关系不会发生变化，因为手工分配模式下组协调器不生效，消费者失败不会导致再平衡。</p>
<p>尽管不同消费者实例可以共享组ID，但是它们是独立工作的。为了避免偏移量提交冲突，手工分配模式下，通常为每个消费者指定唯一性的组ID。</p>
<p>注意：自动分区分配（subscribe）和手工分区分配不能一起使用。 </p>
<div class="blog_h3"><span class="graybg">在外部存储偏移量 </span></div>
<p>尽管Kafka内置的消费偏移量的存储机制，但是你并不一定要使用它。</p>
<p>应用程序独立管理消费偏移量的情况很常见，记录处理结果往往需要和消费偏移量原子的写入到同一存储系统，提供更强的精确一次性语义。典型的例子：</p>
<ol>
<li>如果在RDBMS中存储消费后的结果，那么你可以在单个事务中同时提交消费偏移量和消费结果</li>
<li>如果在本地存储消费后的结果，连带存储消费偏移量也能带来好处。假设你希望基于某个分区的数据构建一个索引，索引数据可以和偏移量一起存储。只要能保证索引写和偏移量写原子的进行，那么即使宕机导致数据没有同步到磁盘，后续仍然可以恢复而不丢失数据</li>
</ol>
<p>每个记录都自带了偏移量信息，因此，要外部存储偏移量，你只需要：</p>
<ol>
<li>配置<pre class="crayon-plain-tag">enable.auto.commit=false</pre></li>
<li>手工存储ConsumerRecord提供的偏移量</li>
<li>消费者重启后，调用<pre class="crayon-plain-tag">seek(TopicPartition, long)</pre>恢复上次消费位置</li>
</ol>
<p>上述步骤配合手工分区分配，最为简单。</p>
<p>如果配合自动分区分配，则需要考虑可能的再平衡的影响。你需要调用 subscribe(Collection/Pattern, ConsumerRebalanceListener)并提供一个ConsumerRebalanceListener监听器：</p>
<ol>
<li>实现onPartitionsRevoked(Collection)方法，这样当分区从消费者处拿走时，当前消费者有机会存储偏移量</li>
<li>实现onPartitionsAssigned(Collection)，这样当消费者被授予新分区时，当前消费者能够读取先前存储的偏移量并seek到适当的位置</li>
</ol>
<p>ConsumerRebalanceListener还可以用于，在分区迁移时，刷出应用程序维护的和目标分区相关的任何缓存信息。</p>
<div class="blog_h3"><span class="graybg">消费位置控制</span></div>
<p>大部分情况下，消费者只是简单的从头开始消费，定期的提交消费偏移量。</p>
<p>Kafka也允许消费者任意的指定消费位置：</p>
<ol>
<li>消费者可以跳转到更小的偏移量，对历史数据进行重新消费。某些情况下消费者可能需要在重启后从头消费，已建立本地的缓存状态</li>
<li>消费者可以跳转到更大的偏移量，跳过一部分未消费的数据。在时间敏感的记录处理程序中会这样跳转，当消费者处理速度赶不上时，可能直接跳过一部分记录</li>
</ol>
<p>和跳转相关的消费者方法包括：</p>
<ol>
<li><pre class="crayon-plain-tag">seek(TopicPartition, long)</pre>，跳转到指定偏移量</li>
<li><pre class="crayon-plain-tag">seekToBeginning(Collection)</pre>，跳转到最小的偏移量</li>
<li><pre class="crayon-plain-tag">seekToEnd(Collection)</pre>，跳转到最大的偏移量</li>
</ol>
<div class="blog_h3"><span class="graybg">消费流控制</span></div>
<p>如果给消费者分配了多个分区，它默认会尝试同时读取所有分区中的可用数据，也就是分区的优先级是一样的。</p>
<p>某些情况下，可能需要优先读取某个分区的数据，仅仅在有空闲的时候才消费其它分区的数据。例如：</p>
<ol>
<li>在流处理程序中，你抓取两个主题的数据，并对两个流进行Join操作。如果其中一个流的消费进度远远拉下，则无法有效的Join。这时可能需要把消费快的那个流暂停下来</li>
<li>同时需要抓取历史数据和实时数据时，可能需要优先抓取实时数据</li>
</ol>
<p>调用<pre class="crayon-plain-tag">pause(Collection)</pre>可以暂停消费已分配的分区，调用<pre class="crayon-plain-tag">resume(Collection)</pre>则可以从暂停中恢复。暂停以后，调用poll()不会获取被暂停分区的数据。</p>
<div class="blog_h3"><span class="graybg">读取事务性消息</span></div>
<p>从0.10引入的事务支持，允许生产者原子的写入多个分区、主题的数据。为了配合事务性生产者，消费者必须配置<pre class="crayon-plain-tag">isolation.level=read_committed</pre>。</p>
<p>启用读取已提交这一隔离级别后，消费者仅会读取已经成功提交的事务性消息。对于非事务性消息，读取方式和以前一致。</p>
<p>读取已提交模式下，客户端并没有缓冲机制。这种模式下消费者针对分区的读偏移量，是该分区中第一个属于进行中事务的记录的偏移量。此偏移量被称为最后稳定偏移量（Last Stable Offset，LSO）。一个进行中的事务可以影响多个分区甚至主题的LSO。</p>
<p>读取已提交模式下的消费者，会最多读取到LSO的前一个位置，并且会过滤掉所有Aborted的任何事务性消息。消费者调用seekToEnd(Collection)、endOffsets(Collection)的结果也会执行LSO。度量值抓取延迟（Fetch Lag）也相对于LSO计算。</p>
<p>包含了事务性消息的分区，会包含提交、中止（Abort）标记，用以说明事务的成功与否。这些标记体现为主题日志中的记录，并且这些记录不会返回给客户端。插入在日志中的标记会导致消费者看到消息偏移量之间出现空隙，不管是哪种隔离级别的消费者都会看到这种空隙。对于读取已提交的消费者，空隙还可能由于中止的事务导致。</p>
<div class="blog_h3"><span class="graybg">多线程处理</span></div>
<p>KafkaConsumer是非线程安全的。所有的网络IO发生在调用线程中。多线程访问时的同步必须由调用者保证，未同步访问可能导致<span style="color: #474747;">ConcurrentModificationException。</span></p>
<p>唯一不需要同步的例外是wakeup()调用。此调用通常在其它线程中发起，中断正在进行中的poll()操作以便关闭消费者：</p>
<pre class="crayon-plain-tag">public class KafkaConsumerRunner implements Runnable {
     private final AtomicBoolean closed = new AtomicBoolean(false);
     private final KafkaConsumer consumer;

     public void run() {
         try {
             consumer.subscribe(Arrays.asList("topic"));
             while (!closed.get()) {
                 ConsumerRecords records = consumer.poll(10000);
                 // 处理抓取到的记录
             }
         } catch (WakeupException e) {
             // poll()被中断会导致该异常
             if (!closed.get()) throw e;
         } finally {
             consumer.close();
         }
     }

     // 下面的方法可以被外部线程调用
     public void shutdown() {
         closed.set(true);
         // 中断正在进行中的poll()方法
         consumer.wakeup();
     }
 }</pre>
<p>Kafka没有设计消费者的线程模型，开发者可以按需开发。</p>
<p>每个消费者独占一个线程的方式，其优势为：</p>
<ol>
<li>容易实现</li>
<li>由于不需要跨线程的协调，往往是最快的方式</li>
<li>按照分区保证消息处理顺序很容易实现，线程简单的根据获得消息的顺序消费它们</li>
</ol>
<p>其缺点是：</p>
<ol>
<li>多个消费者之间，无法合并请求来批量发送给服务器处理，降低了吞吐能力</li>
<li>线程的总数受限于分区的总数，因为每个消费者至少需要独占一个分区</li>
</ol>
<p>为了避免上述线程模型的缺点，可以将消息的消费和处理进行解耦。由一个或者多个消费者线程抓取数据，并将数据ConsumerRecords存放到阻塞队列中。由一个处理线程池来处理队列中的记录。该方式的优势是：</p>
<ol>
<li>可以独立的对消费者、处理者线程进行扩容</li>
<li>需要注意数据的处理顺序问题，由于线程调度的随机性，旧记录实际的处理时间可能在新记录的后面。如果不关注顺序性则不是问题</li>
<li>手工的消费偏移量提交变得困难，需要协调多个线程，确认针对分区的消费已经完成</li>
</ol>
<div class="blog_h2"><span class="graybg">流处理器</span></div>
<p>要使用Kafka Streams API，可以引入Maven依赖：</p>
<pre class="crayon-plain-tag">&lt;dependency&gt;
    &lt;groupId&gt;org.apache.kafka&lt;/groupId&gt;
    &lt;artifactId&gt;kafka-streams&lt;/artifactId&gt;
    &lt;version&gt;1.0.0&lt;/version&gt;
&lt;/dependency&gt;</pre>
<p>详细的讨论参考<a href="#streams-api">Streams API</a>一章。 </p>
<div class="blog_h2"><span class="graybg">连接器</span></div>
<p>很多场景下，你不需要直接使用Connect API，只需要使用内置的Connector就可以了。</p>
<p>详细的讨论参考<a href="#connect-api">Connect API</a>一章。</p>
<div class="blog_h2"><span class="graybg">管理客户端</span></div>
<p>要使用此API，可以引入Maven依赖：</p>
<pre class="crayon-plain-tag">&lt;dependency&gt;
    &lt;groupId&gt;org.apache.kafka&lt;/groupId&gt;
    &lt;artifactId&gt;kafka-clients&lt;/artifactId&gt;
    &lt;version&gt;1.0.0&lt;/version&gt;
&lt;/dependency&gt;</pre>
<p>使用AdminClient.create()可以创建管理客户端的实例（KafkaAdminClient）。此管理客户端可以用于管理、查看主题、代理、配置、ACL。代理的版本必须在0.10.0以上。</p>
<div class="blog_h1"><span class="graybg"><a id="streams-api"></a>Kafka Streams</span></div>
<p>参考<a href="/kafka-streams-study-note">Kafka Streams学习笔记</a></p>
<div class="blog_h1"><span class="graybg"><a id="connect-api"></a>Kafka Connect</span></div>
<p>Kafka Connect是用于在Kafka和其它系统之间进行可扩容、可靠的流传输的工具。使用它你可以快速的定义将大量数据输入到Kafka或者从Kafka输出的“连接器”。</p>
<p>使用Kafka Connect可以读取整个数据库，收集来自所有应用程序的度量信息并存入Kafka主题，并让这些数据适合用于实时的流处理。 </p>
<p>使用Kafka Connect可以将主题数据导出到存储系统、查询系统或者批处理系统，便于离线分析。</p>
<div class="blog_h2"><span class="graybg">特性列表</span></div>
<table class="full-width fixed-word-wrap">
<tbody>
<tr>
<td>
<p><strong>通用的连接器框架</strong></p>
<p>Kafka Connect标准化了其它数据系统和Kafka的集成机制，简化了连接器的开发、部署和管理</p>
</td>
</tr>
<tr>
<td>
<p><strong>分布式和独立运行模式</strong></p>
<p>Kafka Connect可以分布式集群运行，也可以缩小到单个实例运行，满足不同规模应用程序的需要</p>
<p>Kafka Connect基于现有的Kafka组管理协议，需要扩容的时候，把新的Worker添加到Kafka Connect集群中即可</p>
</td>
</tr>
<tr>
<td>
<p><strong>REST接口</strong></p>
<p>可以通过REST接口向Kafka Connect Cluster提交连接器，或者管理连接器</p>
</td>
</tr>
<tr>
<td>
<p><strong>自动偏移量管理</strong></p>
<p>只需要连接器提供少量的信息，框架就能够自动管理偏移量提交过程。这样开发者可以从连接器开发的容易出错的部分解放出来</p>
</td>
</tr>
<tr>
<td>
<p><strong>桥接流式和批量处理系统</strong></p>
<p>使用Kafka既有的特性，Kafka Connect提供了桥接流式（实时）处理系统、批量处理系统的解决方案</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">使用连接器</span></div>
<p>本节给出一个简单的例子，说明如何配置、运行、管理单实例的Kafka Connect。</p>
<div class="blog_h3"><span class="graybg">运行Kafka Connect</span></div>
<p>单实例模式下，所有工作都在单个进程（Worker）内部完成。单实例模式可能适用于日志文件收集这样的场景，但是不能利用Kafka Connect的容错特性。</p>
<p>启动单实例连接器的命令格式如下：</p>
<pre class="crayon-plain-tag"># 第一个参数：Worker的配置
# 后续参数：各连接器的配置
connect-standalone.sh config/connect-standalone.properties connector1.properties connector2.properties ...</pre>
<p>分布式模式下，负载均衡被自动处理，允许随时按需扩容。多实例模式提供活动Task、配置信息、偏移量提交数据的容错：</p>
<pre class="crayon-plain-tag"># 不支持通过命令行提供连接器配置
connect-distributed.sh config/connect-distributed.properties</pre>
<p>单实例和分布式模式会执行不同的Java类，这两个类会读取Worker配置并决定在何处存储配置信息、如何分配任务、在何处存储偏移量和任务状态</p>
<div class="blog_h3"><span class="graybg">Worker配置</span></div>
<p>基础的通用配置项：</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>bootstrap.servers</td>
<td>如何连接到Kafka集群</td>
</tr>
<tr>
<td>key.converter<br />value.converter</td>
<td>如何将键值从Java对象形式转换为Kafka串行化格式</td>
</tr>
</tbody>
</table>
<p>单实例Worker专有配置如下：</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>offset.storage.file.filename</td>
<td> 此参数对单实例模式很重要，指明在何处存放偏移量数据</td>
</tr>
</tbody>
</table>
<p>上面的Worker配置供Kafka Connect创建的生产者、消费者使用，以便访问配置、偏移量、状态主题。</p>
<p>对于Kafka Source和Sink任务，上述配置可以前缀consumer.和.producer，来针对任务进行配置。唯一从Worker继承来的配置项是 bootstrap.servers。</p>
<p>分布式模式下，Kafka Connect在Kafka主题中存储偏移量、配置和任务状态。你应当手工创建用于存放这些信息的主题，以便定制分区数量和复制因子。如果Kafka Connect启动时这些主题不存在，将以默认参数自动创建。</p>
<p>分布式Worker专有配置如下：</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>group.id</td>
<td>
<p>集群的唯一名称，默认connect-cluster。用于构成Kafka Connect集群组</p>
<p>此名称不能和Kafka消费者组名冲突</p>
</td>
</tr>
<tr>
<td>config.storage.topic</td>
<td>
<p>用于存放Connector、Task配置的主题的名称，默认connect-configs</p>
<p>应当是单个分区、大复制因子、压缩格式的主题</p>
</td>
</tr>
<tr>
<td>offset.storage.topic</td>
<td>用于存放偏移量。应当是多分区、复制的、压缩的主题</td>
</tr>
<tr>
<td>status.storage.topic</td>
<td>用于存放状态信息。应当是多分区、复制的、压缩的主题</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">连接器配置</span></div>
<p>你可以指定多个连接器配置，但是这些连接器都是在Worker进程内使用不同的线程运行。</p>
<p>注意：分布式模式下，连接器配置不通过命令行参数指定。你需要通过REST API来创建、修改、销毁连接器。</p>
<p>连接器配置项也是键值对形式，具体包含的配置项取决于不同的连接器。公共的配置项包括：</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>name</td>
<td>连接器的唯一性名称，尝试注册重复名称的连接器会导致失败</td>
</tr>
<tr>
<td>connector.class</td>
<td>连接器的Java类，可以指定权限定名或者别名，例如org.apache.kafka.connect.file.FileStreamSinkConnector、FileStreamSink、FileStreamSinkConnector都表示同一个连接器</td>
</tr>
<tr>
<td>tasks.max</td>
<td>最多为此连接器创建的Task数量</td>
</tr>
<tr>
<td>key.converter<br />value.converter</td>
<td>覆盖Worker配置指定的键值转换器</td>
</tr>
<tr>
<td>topics</td>
<td>作为此连接器输入/输出的主题列表</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">REST API</span></div>
<p>Kafka Connect允许通过REST API来管理连接器。Web服务默认暴露在8083端口，目前支持的端点如下表：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">端点</td>
</tr>
</thead>
<tbody>
<tr>
<td>
<p>GET /connectors    返回活动连接器的列表</p>
</td>
</tr>
<tr>
<td>POST /connectors 创建一个新的连接器，请求体必须是JSON格式，包含字段name、config</td>
</tr>
<tr>
<td>GET /connectors/{name} 获取指定连接器的信息</td>
</tr>
<tr>
<td>GET /connectors/{name}/config 获取指定连接器的配置 </td>
</tr>
<tr>
<td>PUT /connectors/{name}/config 设置指定连接器的配置</td>
</tr>
<tr>
<td>
<p>GET /connectors/{name}/status 获取指定连接器的状态，例如</p>
<ol>
<li>连接器是否正在运行、失败、暂停</li>
<li>连接器被分配给了哪个Worker </li>
<li>连接器的所有Task列表</li>
</ol>
</td>
</tr>
<tr>
<td>GET /connectors/{name}/tasks 获取指定连接器正在允许的Task列表 </td>
</tr>
<tr>
<td>GET /connectors/{name}/tasks/{taskid}/status 获取指定Task的状态信息</td>
</tr>
<tr>
<td>PUT /connectors/{name}/pause 暂停连接器及其Tasks。消息处理将被暂停</td>
</tr>
<tr>
<td>PUT /connectors/{name}/resume 恢复一个被暂停的连接器</td>
</tr>
<tr>
<td>POST /connectors/{name}/restart 重新启动一个连接器 </td>
</tr>
<tr>
<td>POST /connectors/{name}/tasks/{taskId}/restart 重新启动某个任务</td>
</tr>
<tr>
<td>DELETE /connectors/{name} 删除连接器，中止所有Task并移除配置</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">转换</span></div>
<p>通过配置，可以让连接器对消息进行修改或者叫转换，转换可以形成一个链条：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 45%; text-align: center;">配置项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>transforms</td>
<td>转换器的列表，配置顺序对应了转换器的执行顺序</td>
</tr>
<tr>
<td>transforms.$alias.type</td>
<td>
<p>$alias为转换器指定一个别名</p>
<p>转换器的全限定类名</p>
</td>
</tr>
<tr>
<td>transforms.$alias.$transformationSpecificConfig</td>
<td>转换器的私有配置项</td>
</tr>
</tbody>
</table>
<p>这里给出一个示例：内置的文件源连接器 + 添加静态字段的转换器。</p>
<p>本示例使用模式自由的JSON数据格式，因此需要修改Worker 配置文件：</p>
<pre class="crayon-plain-tag">key.converter.schemas.enable=false
value.converter.schemas.enable=false</pre>
<p>在本示例中文件源连接器读取文件的每一行，将其包装为Map，然后添加一个字段用于识别数据来源。为完成这些逻辑，我们需要添加两个转换器：</p>
<ol>
<li>HoistField：将输入纳入到一个Map中</li>
<li>InsertField：添加一个字段</li>
</ol>
<p>修改后的连接器配置如下：</p>
<pre class="crayon-plain-tag">name=local-file-source
# 从文件读取数据（文件作为源），输出到Kafka主题
connector.class=FileStreamSource
tasks.max=1
# 连接器专有配置
file=test.txt
topic=connect-test
# 指定转换器列表
transforms=MakeMap, InsertSource
# 将输入消息转换为键line的值
transforms.MakeMap.type=org.apache.kafka.connect.transforms.HoistField$Value
# 转换器的配置项
transforms.MakeMap.field=line
# 添加一个静态键值
transforms.InsertSource.type=org.apache.kafka.connect.transforms.InsertField$Value
transforms.InsertSource.static.field=data_source
transforms.InsertSource.static.value=test-file-source</pre>
<p>应用此连接器后，主题的记录形式如下：</p>
<pre class="crayon-plain-tag">{"line":"hello","data_source":"test-file-source"}
{"line":"world","data_source":"test-file-source"}</pre>
<div class="blog_h3"><span class="graybg">内置转换器 </span></div>
<p>常用的内置转换器如下表：</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><strong>org.apache.kafka.connect.transforms.InsertField</strong></p>
<p>通过静态值或者记录的元数据，产生一个字段，并插入到记录的键或者值。具体子类型InsertField$Key、InsertField$Value分别针对记录的键、值进行转换</p>
</td>
</tr>
<tr>
<td>offset.field</td>
<td>
<p>存放Kafka偏移量的字段名，仅适用于Sink连接器</p>
<p>如果此配置前缀以<pre class="crayon-plain-tag">!</pre>则表示标注此字段为必须，前缀以<pre class="crayon-plain-tag">?</pre>则表示可选</p>
</td>
</tr>
<tr>
<td>partition.field</td>
<td>存放Kafka分区编号的字段名，前缀!/?表示必须/可选</td>
</tr>
<tr>
<td>static.field</td>
<td>一个存放静态数据的字段的名称，前缀!/?表示必须/可选</td>
</tr>
<tr>
<td>static.value</td>
<td>上述静态字段的值</td>
</tr>
<tr>
<td>timestamp.field</td>
<td>存放Kafka记录时间戳的字段名，前缀!/?表示必须/可选</td>
</tr>
<tr>
<td>topic.field</td>
<td>存放Kafka主题名的字段的名字，前缀!/?表示必须/可选</td>
</tr>
<tr>
<td colspan="2">
<p><strong>org.apache.kafka.connect.transforms.ReplaceField</strong></p>
<p>过滤或者重命名现有的字段。具体子类型ReplaceField$Key、ReplaceField$Value分别针对记录的键、值进行转换</p>
</td>
</tr>
<tr>
<td>blacklist </td>
<td>需要过滤掉的字段名，优先级比whitelist高</td>
</tr>
<tr>
<td>renames </td>
<td>字段重命名映射，格式：oldname:newname,oldname2:newname2 </td>
</tr>
<tr>
<td>whitelist </td>
<td>需要包含的字段名，不在列表中的所有字段被过滤掉</td>
</tr>
<tr>
<td colspan="2">
<p><strong>org.apache.kafka.connect.transforms.MaskField</strong></p>
<p>将指定字段的值替换为一个合法（类型相关）的“空值”，例如0、false、空串。具体子类型MaskField$Key、MaskField$Value分别针对记录的键、值进行转换</p>
</td>
</tr>
<tr>
<td>fields</td>
<td>需要被遮掩的字段列表</td>
</tr>
<tr>
<td colspan="2">
<p><strong>org.apache.kafka.connect.transforms.HoistField</strong></p>
<p>当具有限定的Schema时，使用指定的结构来包裹数据；当使用自由Schema时，使用Map包裹数据。HoistField$Key、HoistField$Value分别针对记录的键、值进行转换</p>
</td>
</tr>
<tr>
<td>field</td>
<td>包裹记录数据的字段的名字</td>
</tr>
<tr>
<td colspan="2">
<p><strong>org.apache.kafka.connect.transforms.ExtractField</strong></p>
<p>从结构（限定Schema）或者Map（自由Schema）中抽取一个字段。任何空值不做修改。ExtractField$Key、ExtractField$Value分别针对记录的键、值进行转换</p>
</td>
</tr>
<tr>
<td>field </td>
<td>需要抽取的字段的名字</td>
</tr>
<tr>
<td colspan="2">
<p><strong>org.apache.kafka.connect.transforms.SetSchemaMetadata</strong></p>
<p>针对键（SetSchemaMetadata$Key）或值（SetSchemaMetadata$Value）来设置Schema名称、版本</p>
</td>
</tr>
<tr>
<td>schema.name</td>
<td>string，模式的名称 </td>
</tr>
<tr>
<td>schema.version</td>
<td>int，模式的版本</td>
</tr>
<tr>
<td colspan="2">
<p><strong>org.apache.kafka.connect.transforms.TimestampRouter</strong></p>
<p>根据原始topic、记录时间戳来更新记录的topic字段</p>
<p>主要用于Sink连接器，原因是topic字段常常在Sink连接器中用作确定目标系统中的实体名（例如数据库系统中的表名）</p>
</td>
</tr>
<tr>
<td>timestamp.format</td>
<td>对时间戳进行格式化的Pattern，此Pattern必须兼容java.text.SimpleDateFormat。默认yyyyMMdd</td>
</tr>
<tr>
<td>topic.format</td>
<td>更新后的topic字段的格式，可以使用${topic}、${timestamp}两个占位符</td>
</tr>
<tr>
<td colspan="2">
<p><strong>org.apache.kafka.connect.transforms.RegexRouter</strong></p>
<p>使用正则式匹配替换，来更新记录的topic字段</p>
</td>
</tr>
<tr>
<td>regex</td>
<td>用于匹配的正则式</td>
</tr>
<tr>
<td>replacement</td>
<td>原topic中匹配项被替换为的字符串</td>
</tr>
<tr>
<td colspan="2">
<p><strong>org.apache.kafka.connect.transforms.Flatten</strong></p>
<p>扁平化一个内嵌的数据结构，新产生的字段名以点号导航的形式产生。Flatten$Key、Flatten$Value分别针对记录的键、值进行转换</p>
</td>
</tr>
<tr>
<td>delimiter</td>
<td>字段名分隔符，默认点号</td>
</tr>
<tr>
<td colspan="2">
<p><strong>org.apache.kafka.connect.transforms.Cast</strong></p>
<p>将字段、整个键或者整个值转换为指定的类型。例如可以强制一个int字段为short，仅仅支持基本类型和字符串。Cast$Key、Cast$Value分别针对记录的键、值进行转换</p>
</td>
</tr>
<tr>
<td>spec</td>
<td>
<p>field:type,field2:type2形式的转换说明。支持的类型包括：</p>
<p style="padding-left: 30px;">int8, int16, int32, int64, float32, float64, boolean,string</p>
</td>
</tr>
<tr>
<td colspan="2">
<p><strong>org.apache.kafka.connect.transforms.TimestampConverter</strong></p>
<p>转换时间戳的格式，TimestampConverter$Key、TimestampConverter$Value分别针对记录的键、值进行转换</p>
</td>
</tr>
<tr>
<td>target.type</td>
<td>期望的目标时间戳呈现格式：string、unix、Date、Time、Timestamp</td>
</tr>
<tr>
<td>field</td>
<td>期望被转换的时间戳字段，如果整个键或值是时间戳则置空 </td>
</tr>
<tr>
<td>format</td>
<td>target.type=string时，指定SimpleDateFormat兼容的Pattern </td>
</tr>
</tbody>
</table>
<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>Connector</td>
<td>
<p>为了让Kafka和其它系统进行数据交换，开发人员需要创建一个Connector，连接器有两类：</p>
<ol>
<li>SourceConnector，从其它系统导入数据到Kafka，例如JDBCSourceConnector将关系型数据库导入到Kafka</li>
<li>SinkConnector，将Kafka中的数据导出到其它系统，例如HDFSSinkConnector将Kafka主题导出到HDFS文件</li>
</ol>
<p>Connector本身不负责实际的数据拷贝工作，它只是把工作分配给一系列的Task进行处理</p>
<p>并非所有的Job都是静态的，例如JDBCSourceConnector，可以监控数据库并把每个表分配给一个Task。当数据库中创建了新表的时候，Connector需要发现此表并将其分配给某个Task，这是通过重新配置Connector实现的</p>
</td>
</tr>
<tr>
<td>Task</td>
<td>
<p>负责将数据的一个子集拷贝到Kafka，或者从Kafka拷贝出去。Task可以分配到不同的Worker上运行</p>
<p>数据子集必须能够转换为Schema一致的记录构成的输入或输出流。某些情况下数据子集和流的映射关系是很明显的，例如日志文件集中的每个文件都可以产生流，日志的每一行都对应一个记录。某些情况下Offset如何界定则不明显，例如JDBC连接器能够把表转换为流，但是流中记录的Offset如何界定呢？一个常见的方案是，使用表的时间戳字段作为Offset，并基于此字段构建查询，进行批量数据读取</p>
</td>
</tr>
<tr>
<td>Stream</td>
<td>每个流都是键值对的流水序列。键、值都可以具有复杂的结构，例如数组、嵌套对象</td>
</tr>
<tr>
<td>Record</td>
<td>不管是由Source生成的，还是输出给Sink的记录，都有关联的Stream ID和Offset。框架会定期的进行偏移量提交，这样发生失败后，可以从上一次提交的偏移量处恢复</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">Source连接器</span></div>
<p>开发连接器仅仅需要实现Connector、Task两个接口。</p>
<p>从文件读取记录的源连接器和任务：</p>
<pre class="crayon-plain-tag">package cc.gmem.study.kafka;

import org.apache.kafka.common.config.ConfigDef;
import org.apache.kafka.connect.connector.Task;
import org.apache.kafka.connect.source.SourceConnector;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class FileStreamSourceConnector extends SourceConnector {

    private String filename;

    private String topic;

    private final String FILE_CONFIG = "filename";

    private final String TOPIC_CONFIG = "topic";

    /**
     * 此连接器的任务实现类
     * 需要在Worker中实例化的，实际的从外部读取数据的任务的Java类
     */
    public Class&lt;? extends Task&gt; taskClass() {
        return FileStreamSourceTask.class;
    }

    /**
     * 生命周期回调，启动连接器
     * 该方法仅仅会在"干净"的连接器上面调用，所谓干净，要么是刚刚实例化、初始化的
     * 要么是已经被停止的
     *
     * @param props 传入的配置信息
     */
    public void start( Map&lt;String, String&gt; props ) {
        filename = props.get( FILE_CONFIG );
        topic = props.get( TOPIC_CONFIG );
    }

    /**
     * 声明周期回调，停止连接器
     */
    public void stop() {

    }

    /**
     * 产生最多max个任务的配置信息
     */
    @Override
    public List&lt;Map&lt;String, String&gt;&gt; taskConfigs( int max ) {
        List&lt;Map&lt;String, String&gt;&gt; configs = new ArrayList&lt;&gt;();
        // 仅仅支持单个任务
        Map&lt;String, String&gt; config = new HashMap&lt;&gt;();
        config.put( FILE_CONFIG, filename );
        config.put( TOPIC_CONFIG, topic );
        configs.add( config );
        return configs;
    }

    /**
     * 返回此连接器的版本
     */
    @Override
    public String version() {
        return null;
    }

    /**
     * 定义连接器的配置
     */
    @Override
    public ConfigDef config() {
        return null;
    }

}</pre><br />
<pre class="crayon-plain-tag">package cc.gmem.study.kafka;

import org.apache.commons.io.IOUtils;
import org.apache.kafka.connect.data.Schema;
import org.apache.kafka.connect.source.SourceRecord;
import org.apache.kafka.connect.source.SourceTask;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;

public class FileStreamSourceTask extends SourceTask {

    private String filename;

    private InputStream stream;

    private String topic;

    /**
     * 启动任务，在此处理任务配置，打开需要的资源
     * &lt;p&gt;
     * 实际应用时，还要考虑从Offset恢复的情况
     *
     * @param props 任务配置
     */
    @Override
    public void start( Map&lt;String, String&gt; props ) {
        filename = props.get( FileStreamSourceConnector.FILE_CONFIG );
        stream = openStream( filename );
        topic = props.get( FileStreamSourceConnector.TOPIC_CONFIG );
    }

    /**
     * Task被分配给专用的线程，此线程可能永久的阻塞，需要从Worker的其它线程调用stop方法才能停止
     */
    @Override
    public synchronized void stop() {
        IOUtils.closeQuietly( stream );
    }

    /**
     * 从外部系统拉取数据并构建源记录，如果没有可用的记录此方法应该阻塞
     *
     * @return 源记录的集合
     * @throws InterruptedException 当阻塞时被中断
     */
    @Override
    public List&lt;SourceRecord&gt; poll() throws InterruptedException {
        try {
            List&lt;SourceRecord&gt; records = new ArrayList&lt;&gt;();
            while ( records.isEmpty() ) {
                LineAndOffset line = readToNextLine( stream );
                if ( line != null ) {
                    records.add(
                            // 源记录，主要包含四个元素
                            new SourceRecord(
                                    // 源分区（相当于流的标识符），这里只有一个源分区，也就是唯一的输入文件
                                    Collections.singletonMap( "filename", filename ),
                                    // 源偏移量，这里即文件中的字节偏移量
                                    Collections.singletonMap( "position", streamOffset ),
                                    // 输出主题
                                    topic,
                                    // 输出值及其Schema。这里的Schema提示输出永远是一个字符串
                                    Schema.STRING_SCHEMA, line
                            )
                    );
                } else {
                    Thread.sleep( 1 );
                }
            }
            return records;
        } catch ( IOException e ) {
        }
        return null;
    }

    @Override
    public String version() {
        return null;
    }

    private InputStream openStream( String filename ) {
        try {
            return new FileInputStream( filename );
        } catch ( FileNotFoundException e ) {
            throw new RuntimeException( e.getCause() );
        }
    }
}</pre>
<p>注意上面Task所释放出的源记录，每个记录都关联了流标识符（文件名）和偏移量信息。Connect框架会利用这些信息，进行周期性的偏移量提交。这样，一旦Task出现失败，可以基于偏移量进行恢复，尽可能减少重复处理、重复记录。</p>
<p>偏移量提交完全由框架负责，Kafka Connect暴露了接口供Task读取偏移量：</p>
<pre class="crayon-plain-tag">@Override
public void initialize( SourceTaskContext context ) {
    super.initialize( context );
    stream = new FileInputStream( filename );
    // 获取偏移量阅读器
    OffsetStorageReader offsetStorageReader = context.offsetStorageReader();
    // 读取指定分区的偏移量，偏移量是一个字典而不是简单的数值
    Map&lt;String, Object&gt; offset = offsetStorageReader.offset( Collections.singletonMap( "filename", filename ) );
    if ( offset != null ) {
        Long lastRecordedOffset = (Long) offset.get( "position" );
        if ( lastRecordedOffset != null )
            seekToOffset( stream, lastRecordedOffset );
    }
}</pre>
<div class="blog_h3"><span class="graybg">Sink连接器</span></div>
<p>Sink连接器的接口和Source类似。但是SinkTask和SourceTask则不同，因为后者使用的是拉模式而前者是推模式：</p>
<pre class="crayon-plain-tag">public abstract class SinkTask implements Task {
    public void initialize(SinkTaskContext context) {
        this.context = context;
    }
    // 逻辑集中在此方法中，需要将SinkRecord进行转换，最终输出到外部系统
    // 此方法不一定需要保证数据完全写入到外部系统中，可以提前返回
    // SinkRecord包含的信息基本和SourceRecord一致
    public abstract void put(Collection&lt;SinkRecord&gt; records);
    // 在偏移量提交阶段调用下面的方法，此方法应该刷出未写入到外部系统的数据，并阻塞等待操作完成
    // currentOffsets可以用于实现精确一次性语义（和写入到外部系统的数据原子的一同写入）
    public void flush(Map&lt;TopicPartition, OffsetAndMetadata&gt; currentOffsets) {
    }
}</pre>
<div class="blog_h3"><span class="graybg">动态流</span></div>
<p>Kafka Connect的主要价值是用于定义大批量的数据拷贝Job，例如，Job通常用来拷贝整个数据库而非单张表。这种设计意味着连接器的输入/输出流（表、文件等等）会动态的变化。</p>
<p>在实现SourceConnector时，你应当考虑源系统中的变化，例如表的增加或删除。当检测到变化后应当调用ConnectorContext来通知框架：</p>
<pre class="crayon-plain-tag">if (inputsChanged())
    this.context.requestTaskReconfiguration();</pre>
<p>在上述调用之后，框架会立即请求新的配置，并更新Tasks。在重新配置Task之前，框架允许Task优雅的完成提交操作。</p>
<p>某些情况下，和输入流更新有关的逻辑仅仅存在于Connector中。另外一些情况下，Task可能受到影响，需要适当的异常处理。例如，表的删除可能导致Task在拉取数据时出现错误，并且此错误可能在Connector检测到输入流的变化之前发生。 </p>
<p>SinkConnector通常仅需要处理新增的Kafka流，Kafka Connect使用正则式订阅来监控SinkConnector的输入主题集的变化，并通知SinkConnector。SinkTask可能需要接收新的流，并在外部系统创建资源。如果多个SinkTask接收同一流则可能出现尝试重复创建同一资源（例如同一张表）的情况，需要注意。</p>
<div class="blog_h3"><span class="graybg">验证配置</span></div>
<p>Kafka Connect支持在提交连接器之前，对提供的配置信息进行验证。要使用此功能，你需要实现config()方法：</p>
<pre class="crayon-plain-tag">private static final ConfigDef CONFIG_DEF = new ConfigDef()
    .define(FILE_CONFIG, Type.STRING, Importance.HIGH, "Source filename.")
    .define(TOPIC_CONFIG, Type.STRING, Importance.HIGH, "The topic to publish data to");
 
public ConfigDef config() {
    return CONFIG_DEF;
}</pre>
<p>ConfigDef.define方法的重载版本允许你提供一个Validator，用于针对单配置项自定义验证规则。</p>
<p>此外，覆盖Connector.validate()方法可以提供配置验证逻辑。</p>
<div class="blog_h3"><span class="graybg">使用Schema</span></div>
<p>要处理符合结构定义的数据，你需要使用Kafka Connect的Data API。大部分的结构化记录需要和Schema、Struct这两个类交互。这两个类都用于定义Schema：</p>
<pre class="crayon-plain-tag">Schema schema = SchemaBuilder.struct().name(NAME)
    .field("name", Schema.STRING_SCHEMA)
    .field("age", Schema.INT_SCHEMA)
    .field("admin", new SchemaBuilder.boolean().defaultValue(false).build())
    .build();
 
Struct struct = new Struct(schema)
    .put("name", "Barbara Liskov")
    .put("age", 75);</pre>
<p>在开发SourceConnector时，你需要考虑何时生成Schema。如果Schema是静态的，可以静态块中生成。</p>
<p>但是很多连接器都需要面对动态Schema。例如数据库连接器，这种连接器可能需要处理多张表，就算处理单张表，表的结构也可能随着时间改变。你必须检测这些改变并且做出适当的处理。</p>
<div class="blog_h1"><span class="graybg">配置</span></div>
<p>Kafka使用properties风格的配置文件。在通过命令行启动Kafka时，你可以指定配置文件路径，或者使用配置项的命令行参数版本。</p>
<div class="blog_h2"><span class="graybg">配置项列表</span></div>
<div class="blog_h3"><span class="graybg">代理配置</span></div>
<p>最基本的配置项包括：broker.id、log.dirs、zookeeper.connect。</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 colspan="2"><strong><em>重要配置</em></strong></td>
</tr>
<tr>
<td>zookeeper.connect</td>
<td>
<p>Kafka使用ZooKeeper存储集群的元数据信息，此配置项提供ZooKeeper的链接字符串：</p>
<pre class="crayon-plain-tag"># 支持ZooKeeper的chroot语法，在尾部添加 /kafka表示所有数据存放在此znode下
zookeeper.connect=zk:2181,zk-2:2181,zk-3:2181/kafka</pre>
</td>
</tr>
<tr>
<td>advertised.listeners</td>
<td>发布到ZooKeeper，供客户端使用的监听器列表。如果不指定则同listeners配置项</td>
</tr>
<tr>
<td>auto.create.topics.enable</td>
<td>boolean=true。是否按需自动在服务器上创建主题</td>
</tr>
<tr>
<td>auto.leader.rebalance.enable</td>
<td>
<p>boolean=true。是否启用Leader负载均衡。一个后台线程负责定期检查并触发负载均衡
<p>主题的每个分区都具有一个Leader节点，此节点负责全部读写</p>
</td>
</tr>
<tr>
<td>leader.imbalance.check.interval.seconds</td>
<td>
<p>long=300。再平衡检查的间隔</p>
</td>
</tr>
<tr>
<td>leader.imbalance.per.broker.percentage</td>
<td>
<p>int=10。每个代理允许的Leader不平衡最大比率（百分比），超过此阈值会导致再平衡</p>
</td>
</tr>
<tr>
<td>background.threads</td>
<td>int=10。用于各种后台任务的线程池的大小</td>
</tr>
<tr>
<td>broker.id</td>
<td>
<p>当前服务器的代理标识。如果不设置则自动生成一个唯一性的标识符</p>
<p>自动生成的ID从reserved.broker.max.id + 1开始，以防止和手工指定的ID冲突</p>
</td>
</tr>
<tr>
<td>compression.type</td>
<td>
<p>string=producer。指定主题的最终压缩方式，可选值有：</p>
<ol>
<li>标准的编码器名称，例如gzip、snappy、lz4</li>
<li>uncompressed，不压缩</li>
<li>producer，保留生产者设置的压缩方式</li>
</ol>
</td>
</tr>
<tr>
<td>delete.topic.enable</td>
<td>boolean=true。是否允许删除主题。如果设置为false，通过管理工具执行的删除操作没有任何效果</td>
</tr>
<tr>
<td>listeners</td>
<td>
<p>在其上监听的URI列表，逗号分隔</p>
<p>如果URI不使用安全协议，则listener.security.protocol.map必须配置</p>
<p>如果URI的主机部分设置为0.0.0.0则监听所有网络接口；置空则监听默认网络接口</p>
<p>示例：</p>
<p style="padding-left: 30px;">PLAINTEXT://myhost:9092<br />SSL://:9091 <br />CLIENT://0.0.0.0:9092<br />REPLICATION://localhost:9093</p>
</td>
</tr>
<tr>
<td>log.dir</td>
<td>日志数据存储的目录，作为log.dirs的补充</td>
</tr>
<tr>
<td>log.dirs</td>
<td>日志数据存储的目录，如果不指定则使用log.dir</td>
</tr>
<tr>
<td>log.flush.interval.messages</td>
<td>
<p>long=9223372036854775807。在消息被刷出磁盘之前，日志分区上累积的消息的数量</p>
<p>Kafka不建议设置此参数。为了防止数据丢失应该使用集群而不是定期调用fsync。操作系统底层会自动调度，高效的决定何时刷出</p>
</td>
</tr>
<tr>
<td>log.flush.interval.ms</td>
<td>
<p>在任何主题中任何消息刷出磁盘之前，在内存中最多驻留的时间</p>
<p>如果不指定，使用 log.flush.scheduler.interval.ms</p>
</td>
</tr>
<tr>
<td>log.flush.offset.checkpoint.interval.ms</td>
<td>int=60000。每隔多久更新最后一次刷出（Last flush）的持久化记录，此记录作为日志恢复点</td>
</tr>
<tr>
<td>log.flush.scheduler.interval.ms</td>
<td>long=9223372036854775807。日志刷出器每隔多少ms检查是否存在需要刷出到磁盘的日志</td>
</tr>
<tr>
<td>log.flush.start.offset.checkpoint.interval.ms</td>
<td>int=60000。每隔多久更新日志起始偏移量（Start offset）的持久化记录</td>
</tr>
<tr>
<td>log.retention.bytes</td>
<td>日志的最大保留尺寸</td>
</tr>
<tr>
<td>log.retention.hours<br />log.retention.minutes<br />log.retention.ms</td>
<td>日志的最大保留时间。优先级逐个升高</td>
</tr>
<tr>
<td>log.roll.hours<br />log.roll.ms</td>
<td>最多经过多久，必须滚出（Roll out，产生）新的日志段（Segment）</td>
</tr>
<tr>
<td>log.roll.jitter.hours<br />log.roll.jitter.ms</td>
<td>从上面那个参数减去的最大抖动时间</td>
</tr>
<tr>
<td>log.segment.bytes</td>
<td>日志段的大小</td>
</tr>
<tr>
<td>log.segment.delete.delay.ms</td>
<td>long=60000。在从文件系统删除一个日志文件（段）之前，最多等待的时间</td>
</tr>
<tr>
<td>message.max.bytes</td>
<td>
<p>int=1000012</p>
<p>Kafka允许的最大消息批量的大小（字节）。如果增加此参数并且消费者的版本在0.10.2之前，消费者的抓取尺寸（Fetch Size）也需要相应的增加</p>
<p>在最近版本的消息格式中，出于性能的考虑，记录总是被组装到批次（Batch）中</p>
<p>在以前版本的消息格式中，未压缩记录是不组装批次的。因此该参数针对单条记录</p>
<p>主题可以覆盖此配置</p>
</td>
</tr>
<tr>
<td>min.insync.replicas</td>
<td>
<p>int=1</p>
<p>当生产者将acks设置为all或-1时，该配置的意义是指定最少的已经同步写操作的节点数量。也就是说，只有足够多的节点已经确认（Acknowledge）了写操作，生产者才认为写操作成功</p>
<p>如果无法满足此条件，则生产者抛出NotEnoughReplicas或NotEnoughReplicasAfterAppend</p>
<p>配合使用acks和此选项可以增强持久性保证</p>
</td>
</tr>
<tr>
<td>num.io.threads</td>
<td>int=8。服务器使用的I/O线程数量，这些线程负责磁盘IO等操作</td>
</tr>
<tr>
<td>num.network.threads</td>
<td>int=3。服务器使用的网络线程的数量，这些线程负责接收请求、发送响应</td>
</tr>
<tr>
<td>num.recovery.threads.per.data.dir</td>
<td>int=1。每个数据目录用于：1、在启动时进行日志恢复；2、在关闭时进行日志刷出的线程数量</td>
</tr>
<tr>
<td>num.replica.fetchers</td>
<td>int=1。从源代理抓取数据以复制消息的线程数量。增加此配置可能增强从代理的并行度</td>
</tr>
<tr>
<td>offset.metadata.max.bytes</td>
<td>int=4096。关联到偏移量提交（Offset Commit）的元数据条目的最大尺寸</td>
</tr>
<tr>
<td>offsets.commit.required.acks</td>
<td>short=-1。通常不需要修改，在提交可以被接受之前，需要的Acks数量</td>
</tr>
<tr>
<td>offsets.commit.timeout.ms</td>
<td>int=5000。偏移量提交（Offset Commit）会被推迟，直到所有Replica接收到提交，或者到达此配置指定的延迟</td>
</tr>
<tr>
<td>offsets.load.buffer.size</td>
<td>int=5242880。当加载偏移量到缓存中时，从偏移量段（Offsets Segments）读取数据的批次大小</td>
</tr>
<tr>
<td>offsets.retention.check.interval.ms</td>
<td>long=600000。检查偏移量是否过期（Stale）的间隔</td>
</tr>
<tr>
<td>offsets.retention.minutes</td>
<td>int=1440。超过此驻留时间的偏移量被丢弃</td>
</tr>
<tr>
<td>offsets.topic.compression.codec</td>
<td>int=0。偏移量提交主题（Offsets Commit Topic ）的压缩算法。使用压缩可以实现“原子”提交</td>
</tr>
<tr>
<td>offsets.topic.num.partitions</td>
<td>int=50。偏移量提交主题包含的分区数量。部署后不应再修改</td>
</tr>
<tr>
<td>offsets.topic.replication.factor</td>
<td>short=3。偏移量提交主题的复制因子。在集群成员大小足够前，内部的主题创建会失败</td>
</tr>
<tr>
<td>offsets.topic.segment.bytes</td>
<td>int=104857600。偏移量提交主题的段大小</td>
</tr>
<tr>
<td>queued.max.requests</td>
<td>int=500。在阻塞网络线程（不再接受新请求）之前，排队的请求数量</td>
</tr>
<tr>
<td>replica.fetch.min.bytes</td>
<td>int=1。Follower Replica期望每次抓取请求能接收的响应的最小字节数。如果字节数不够，Follower会等待直到replica.fetch.wait.max.ms</td>
</tr>
<tr>
<td>replica.fetch.wait.max.ms</td>
<td>int=500。Follower Replica发送抓取请求后等待响应的最大时间。此配置应该总是小于replica.lag.time.max.ms，以防止低吞吐量主题的ISR频繁的收缩 </td>
</tr>
<tr>
<td>replica.high.watermark.checkpoint.interval.ms</td>
<td>long=5000。高水位保存到磁盘的频率 </td>
</tr>
<tr>
<td>replica.lag.time.max.ms</td>
<td>long=10000。Follower Replica在此时间内没有发送抓取请求，或者离Leader Replica的终点偏移量（End Offset）超过此时间，则Leader Replica从ISR列表中将Follower移除</td>
</tr>
<tr>
<td>replica.socket.receive.buffer.bytes</td>
<td>int=65536。网络请求的套接字接收缓冲大小</td>
</tr>
<tr>
<td>replica.socket.timeout.ms</td>
<td>int=30000。网络套接字的超时时间，应当大于replica.fetch.wait.max.ms</td>
</tr>
<tr>
<td>request.timeout.ms</td>
<td>int=30000。客户端等待响应的最大时间，超过此时间客户端可能重发请求或者失败（重发次数超过限制）</td>
</tr>
<tr>
<td>socket.receive.buffer.bytes</td>
<td>int=102400。服务器端套接字的SO_RCVBUF配置。如果设置为-1则使用OS默认值</td>
</tr>
<tr>
<td>socket.request.max.bytes</td>
<td>int=104857600。一个套接字请求包含的最大字节数</td>
</tr>
<tr>
<td>socket.send.buffer.bytes</td>
<td>int=102400。服务器端套接字的SO_SNDBUF配置。如果设置为-1则使用OS默认值</td>
</tr>
<tr>
<td>transaction.max.timeout.ms</td>
<td>int=900000。事务的最大超时，如果客户端请求的事务时间超过此配置则代理在InitProducerIdRequest调用中返回一个错误，以阻止客户端使用太大的超时</td>
</tr>
<tr>
<td>transaction.state.log.load.buffer.size</td>
<td>int=5242880。加载生产者ID、事务到缓存中时，读取事务状态日志的批次大小</td>
</tr>
<tr>
<td>transaction.state.log.min.isr</td>
<td>int=2。为事务状态主题覆盖min.insync.replicas </td>
</tr>
<tr>
<td>transaction.state.log.num.partitions</td>
<td>int=50。事务状态主题的分区数量。部署后不应该修改</td>
</tr>
<tr>
<td>transaction.state.log.replication.factor</td>
<td>short=3。事务状态日志的复制因子 </td>
</tr>
<tr>
<td>transaction.state.log.segment.bytes</td>
<td>int=104857600。事务状态日志的段大小，应该设置的相对较小，以保证较快的日志压缩和缓存加载 </td>
</tr>
<tr>
<td>transactional.id.expiration.ms</td>
<td>int=604800000。事务协调器等待此时间后，主动（不需要从生产者接收任何事务状态更新）将生产者的事务ID过期 </td>
</tr>
<tr>
<td>unclean.leader.election.enable</td>
<td>boolean=false。指示是否允许非ISR（同步）的Replica可以被选举为Leader，设置为true可能导致数据丢失</td>
</tr>
<tr>
<td>zookeeper.connection.timeout.ms</td>
<td>创建到ZooKeeper的连接的超时时间，如果不指定使用zookeeper.session.timeout.ms  </td>
</tr>
<tr>
<td>zookeeper.session.timeout.ms</td>
<td>int=6000。ZooKeeper会话过期时间 </td>
</tr>
<tr>
<td>zookeeper.set.acl</td>
<td>boolean=false。是否设置ACL</td>
</tr>
<tr>
<td>broker.rack</td>
<td>代理所在的机柜，用于Rack-aware的复制分配，用于跨数据中心复制</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">主题配置</span></div>
<p>主题可以从服务器默认值继承配置，并且可以覆盖。要指定主题专有配置，可以：</p>
<pre class="crayon-plain-tag">kafka-topics.sh --zookeeper localhost:2181 --create --topic my-topic --partitions 1 --replication-factor 
   # 使用下面的选项来覆盖配置
   --config max.message.bytes=64000
   --config flush.messages=1</pre>
<p>在主题创建之后，你仍然可以修改配置：</p>
<pre class="crayon-plain-tag">kafka-configs.sh --zookeeper localhost:2181 --entity-type topics --entity-name my-topic
    # 修改配置
    --alter --add-config max.message.bytes=128000
    # 移除配置
    --alter --delete-config max.message.bytes</pre>
<p>执行下面的命令，可以查看一个主题的配置覆盖情况：</p>
<pre class="crayon-plain-tag">kafka-configs.sh --zookeeper localhost:2181 --entity-type topics --entity-name my-topic --describe</pre>
<p>重要配置项如下：</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>cleanup.policy</td>
<td>
<p>list=delete。对应服务器默认配置：log.cleanup.policy</p>
<p>老旧消息的驻留策略，可选值delete/compact</p>
</td>
</tr>
<tr>
<td>compression.type</td>
<td>
<p>string=producer。对应服务器默认配置项：compression.type</p>
<p>主题的最终压缩方式</p>
</td>
</tr>
<tr>
<td>delete.retention.ms</td>
<td>
<p>long=86400000。对应服务器默认配置项：log.cleaner.delete.retention.ms</p>
<p>对于log compacted主题，删除墓碑标记的驻留时间。如果消费者从Offset 0 开始读取，它必须在此配置指定的时间内完成，以确保获得有效的快照</p>
</td>
</tr>
<tr>
<td>file.delete.delay.ms</td>
<td>
<p>long=60000。对应服务器默认配置项：log.segment.delete.delay.ms</p>
<p>从文件系统中删除文件之前，等待的时间</p>
</td>
</tr>
<tr>
<td>flush.messages</td>
<td>
<p>long=9223372036854775807。对应服务器默认配置项：log.flush.interval.messages</p>
<p>指定一个消息计数，累计超过此数量的消息强制fsync日志到文件系统</p>
</td>
</tr>
<tr>
<td>flush.ms</td>
<td>
<p>long=9223372036854775807。对应服务器默认配置项：log.flush.interval.ms</p>
<p>指定一个间隔，超过此时间强制fsync日志到文件系统</p>
</td>
</tr>
<tr>
<td>follower.replication.throttled.replicas</td>
<td>
<p>list。对应服务器默认配置项：follower.replication.throttled.replicas</p>
<p>Replica的列表，对于这些Replica日志复制在Follower段限速</p>
<p>取值*表示针对所有Replica限速，可以指定partitionId:brokerId、brokerId、partitionId</p>
</td>
</tr>
<tr>
<td>leader.replication.throttled.replicas</td>
<td>
<p>list。对应服务器默认配置项：leader.replication.throttled.replicas</p>
<p>类似上一条，只是节流在Leader端执行</p>
</td>
</tr>
<tr>
<td>index.interval.bytes</td>
<td>
<p>int=4096。对应服务器默认配置项：log.index.interval.bytes</p>
<p>通常不需要修改此选项。控制Kafka向Offset索引添加条目的频率。更高的频率允许消费者跳转到更精确的位置，但是会导致索引占用空间更大</p>
</td>
</tr>
<tr>
<td>max.message.bytes</td>
<td>int=1000012。对应服务器默认配置项：message.max.bytes</td>
</tr>
<tr>
<td>message.format.version</td>
<td>
<p>string=1.0-IV0。对应服务器默认配置项：log.message.format.version</p>
<p>代理为该主题添加日志时，使用的消息格式版本</p>
</td>
</tr>
<tr>
<td>message.timestamp.difference.max.ms</td>
<td>
<p>long=9223372036854775807。对应服务器默认配置项：log.message.timestamp.difference.max.ms</p>
<p>代理接收到消息时的实际时间和消息中指定的时间戳的最大差距。如果超过此值并且：</p>
<ol>
<li>message.timestamp.type=CreateTime，消息被拒绝</li>
<li>message.timestamp.type=LogAppendTime，该配置被忽略</li>
</ol>
</td>
</tr>
<tr>
<td>message.timestamp.type</td>
<td>
<p>string=CreateTime。对应服务器默认配置项：log.message.timestamp.type</p>
<p>消息时间戳存储消息创建时间（CreateTime）还是附加到日志的时间（LogAppendTime）</p>
<p>&nbsp;</p>
</td>
</tr>
<tr>
<td>min.cleanable.dirty.ratio</td>
<td>
<p>double=0.5。对应服务器默认配置项：log.cleaner.min.cleanable.ratio</p>
<p>当启用日志整理（log compaction）时，该选项控制日志整理器尝试清理日志的频率。默认值是，当50%的消息已经被整理（也就是重复消息不超过50%），则不触发新的整理。取值越大，则整理频率越低也越高效，同时导致日志浪费更多空间</p>
</td>
</tr>
<tr>
<td>min.compaction.lag.ms</td>
<td>
<p>long=0。对应服务器默认配置项：log.cleaner.min.compaction.lag.ms</p>
<p>当启用日志真理时，该选项控制一个消息在被整理之前至少需要等待的时间</p>
</td>
</tr>
<tr>
<td>min.insync.replicas</td>
<td>
<p>int=0。对应服务器默认配置项：min.insync.replicas</p>
</td>
</tr>
<tr>
<td>preallocate</td>
<td>
<p>boolean=false。对应服务器默认配置项：log.preallocate</p>
<p>当创建一个新的日志段时，是否在磁盘上预分配空间</p>
</td>
</tr>
<tr>
<td>retention.bytes</td>
<td>long=-1。对应服务器默认配置项：log.retention.bytes</td>
</tr>
<tr>
<td>retention.ms</td>
<td>long=604800000。对应服务器默认配置项：log.retention.ms</td>
</tr>
<tr>
<td>segment.bytes</td>
<td>int=1073741824。对应服务器默认配置项：log.segment.bytes</td>
</tr>
<tr>
<td>segment.index.bytes</td>
<td>
<p>int=10485760。对应服务器默认配置项：log.index.size.max.bytes</p>
<p>控制索引的大小，索引用于将偏移量映射到文件位置。索引文件被预先创建，仅当日志滚动（创建新段）之后才收缩其大小</p>
</td>
</tr>
<tr>
<td>segment.jitter.ms</td>
<td>long=0。对应服务器默认配置项：log.roll.jitter.ms</td>
</tr>
<tr>
<td>segment.ms</td>
<td>long=604800000。对应服务器默认配置项：log.roll.ms</td>
</tr>
<tr>
<td>unclean.leader.election.enable</td>
<td>boolean=false。对应服务器默认配置项：unclean.leader.election.enable</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">生产者配置</span></div>
<p>下表列出Java生产者可以使用的配置项：</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>bootstrap.servers</td>
<td>
<p>list=host:port,host:port...</p>
<p>指定初始化到Kafka集群连接时使用的服务器地址端口列表。此列表仅仅用于初始化时发现完整集群，不需要指定集群中所有节点</p>
</td>
</tr>
<tr>
<td>key.serializer</td>
<td>指定一个实现了org.apache.kafka.common.serialization.Serializer的类，此类用于键的串行化</td>
</tr>
<tr>
<td>value.serializer</td>
<td>指定一个实现了org.apache.kafka.common.serialization.Serializer的类，此类用于值的串行化</td>
</tr>
<tr>
<td>acks</td>
<td>
<p>string=1。在生产者认定操作已经完成之前，Leader必须接收到Ack的数量：</p>
<ol>
<li>取值0，不等待任何Ack，记录被加入到套接字缓冲并认为已经发送成功。服务器不一定能收到消息，retries配置也不会生效</li>
<li>取值1，Leader将消息写到本地日志之后，立即答复生产者。如果消息来得及复制到Follower并且Leader宕掉，消息可能丢失</li>
<li>取值all或-1，等待所有in-saync的Follower都Ack。具有最强的可靠性</li>
</ol>
</td>
</tr>
<tr>
<td>buffer.memory</td>
<td>
<p>long=33554432。生产者总计可用的缓冲区，此缓冲区用于暂存等待发送到服务器的那些记录</p>
<p>如果消息生产过快超过消息递送速度，而导致缓冲区爆满，生产者会阻塞max.block.ms，此后仍然没有缓解，生产者抛出异常</p>
<p>此参数大概等于生产者总计的内存消耗。生产者还需要额外的小部分内存用于压缩消息、维护in-flight请求</p>
</td>
</tr>
<tr>
<td>compression.type</td>
<td>string=none。生产者产生的任何数据的压缩方式。默认不压缩，可选gzip、snappy、lz4</td>
</tr>
<tr>
<td>retries</td>
<td>
<p>int=0。设置大于0的值，则生产者可以在失败后重试发送消息，以避免暂时性网络错误的影响</p>
<p>如果允许重试，但是没有把max.in.flight.requests.per.connection设置为1，则<span style="background-color: #c0c0c0;">记录的顺序可能改变</span>。例如，两个批次同时发到同一分区，批次1、批次2连续发送，然后1失败2成功，后续重发1</p>
</td>
</tr>
<tr>
<td>batch.size</td>
<td>
<p>int=16384。批量发送的最大字节数，设置为0则禁用批量发送。当多个记录将被发送给同一分区时，生产者会尝试批量发送以减少请求次数。批量发送可以增强服务器、客户端的性能</p>
<p>生产者不会尝试发送大于此配置的记录批次</p>
<p>发送给服务器的请求可以包含多个批次，每个批次针对一个分区</p>
<p>此配置项设置的过小，可能影响吞吐能力；设置的过大，会导致内存浪费，因为这块内存是预先分配的不管是否实际需要</p>
</td>
</tr>
<tr>
<td>client.id</td>
<td>发送请求时，发送给服务器的一个字符串。用途是跟踪请求的源，此ID可以包含在服务器端日志中</td>
</tr>
<tr>
<td>connections.max.idle.ms</td>
<td>long=540000。多久以后关闭空闲的连接</td>
</tr>
<tr>
<td>linger.ms</td>
<td>
<p>long=0。生产者可以将请求发送期间收集的记录组成批次，在下一个请求中一起发送。正常情况下，这种批量发送行为仅在记录产生速度大于发送速度时存在。如果你希望在缓和的负载下也使用批量发送以减小请求次数，可以将该配置设置的大于0。这样，生产者在产生一个消息后，会等待linger.ms，看看有没有下一个消息到来</p>
<p>一旦针对某个分区的记录达到batch.size字节批量发送就会立即发送，而不需要等待linger.ms。linger.ms限定了批量发送引入的<span style="background-color: #c0c0c0;">最大延迟</span></p>
</td>
</tr>
<tr>
<td>max.block.ms</td>
<td>long=60000。配置KafkaProducer.send()和KafkaProducer.partitionsFor()的最大阻塞时间，当生产者发送缓冲爆满或者元数据不可用时，这两个方法会被阻塞</td>
</tr>
<tr>
<td>max.request.size</td>
<td>
<p>int=1048576。单个请求的最大字节数，需要注意服务器端有类似的限制，且取值可能和客户端不一样</p>
<p>该配置对批量发送的大小作出额外限制</p>
</td>
</tr>
<tr>
<td>partitioner.class</td>
<td>
<p>class=org.apache.kafka.clients.producer.internals.DefaultPartitioner</p>
<p>实现org.apache.kafka.clients.producer.Partitioner接口的类</p>
</td>
</tr>
<tr>
<td>receive.buffer.bytes</td>
<td>int=32768。套接字SO_RCVBUF参数，-1则使用OS默认值</td>
</tr>
<tr>
<td>request.timeout.ms</td>
<td>int=30000。客户端等待响应到达的最大时间。如果超时客户端可能重试或失败</td>
</tr>
<tr>
<td>security.protocol</td>
<td>string=PLAINTEXT。用于和服务器通信的协议，可选PLAINTEXT, SSL, SASL_PLAINTEXT, SASL_SSL</td>
</tr>
<tr>
<td>send.buffer.bytes</td>
<td>int=131072。套接字SO_SNDBUF，-1则使用OS默认值</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">消费者配置</span></div>
<p>从0.9.0.0版本开始，Kafka引入新的Java消费者实现，代替原先基于Scala的简单、高层实现。下表的配置可以用于新、老消费者实现：</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>bootstrap.servers</td>
<td rowspan="3">参考生产者配置</td>
</tr>
<tr>
<td>key.deserialize</td>
</tr>
<tr>
<td>value.deserializer</td>
</tr>
<tr>
<td>fetch.min.bytes</td>
<td>int=1。对于一个抓取请求，服务器应该返回的最小数据量。如果数据量不足，在应答之前请求会等待更多数据。设置为较大的数，则服务器会等待更多的数据再应答，这样可以增加吞吐量</td>
</tr>
<tr>
<td>group.id</td>
<td>
<p>唯一性的字符串，用于识别该消费者所属的组。当消费者使用：</p>
<ol>
<li>subscribe(topic)提供的组管理功能</li>
<li>或者基于Kafka的偏移量管理策略</li>
</ol>
<p>时，需要该配置</p>
</td>
</tr>
<tr>
<td>heartbeat.interval.ms</td>
<td>
<p>int=3000。使用Kafka的组管理功能时的心跳间隔。心跳用于确认消费者还活着，因为消费者加入、离开组时需要进行（谁消费哪个分区的）再平衡</p>
<p>必须小于session.timeout.ms，通常不大于session.timeout.ms的1/3。如果需要更敏捷的再平衡可以设置的更低</p>
</td>
</tr>
<tr>
<td>max.partition.fetch.bytes</td>
<td>
<p>int=1048576。服务针对每个分区最多返回的数据量。消费者按批次抓取记录，如果针对第一个非空分区的第一个记录批次超过此配置的限制，批次仍然被返回以便消费者能进一步处理</p>
<p>服务器允许的记录批次的最大尺寸，受到message.max.bytes（代理级/主题级）约束</p>
</td>
</tr>
<tr>
<td>session.timeout.ms</td>
<td>
<p>int=10000。使用Kafka组管理功能时，判断一个消费者宕机的延迟。消费者定期发送心跳，这样代理知道它还活着，如果session.timeout.ms后代理没有收到新的心跳，则认为消费者宕机，代理会把消费者从组中移除并进行再平衡</p>
<p>必须在代理配置group.min.session.timeout.ms和group.max.session.timeout.ms之间取值</p>
</td>
</tr>
<tr>
<td>connections.max.idle.ms</td>
<td>long=540000。关闭空闲连接之前的等待时间</td>
</tr>
<tr>
<td>enable.auto.commit</td>
<td>boolean=true。如果设置为true则消费者的偏移量会在后台定期的提交</td>
</tr>
<tr>
<td>exclude.internal.topics</td>
<td>boolean=true。Kafka内部主题（例如偏移量）是否暴露给消费者</td>
</tr>
<tr>
<td>fetch.max.bytes</td>
<td>
<p>int=52428800。对于一个抓取请求服务器最多返回的数据量。消费者按批次抓取记录，如果针对第一个非空分区的第一个记录批次就超过此配置的限制，批次仍然被返回以便消费者能进一步处理。因此，此该配置不是一个绝对性的限制</p>
<p>服务器允许的记录批次的最大尺寸，受到message.max.bytes（代理级/主题级）约束</p>
</td>
</tr>
<tr>
<td>isolation.level</td>
<td>
<p>string=read_uncommitted。可选值read_committed</p>
<p>决定如何读取事务性写入的消息，如果设置为：</p>
<ol>
<li>read_committed，则consumer.poll()仅仅返回已经提交的事务性消息</li>
<li>read_uncommitted，则consumer.poll()返回所有消息，甚至是已经被Abort的事务性消息</li>
</ol>
<p>对于非事务性消息，该配置没有影响</p>
<p>消息总是按照偏移量顺序返回给消费者，因此read_committed模式下consumer.poll()只能返回最晚到LSO（Last Stable Offset，最新稳定偏移）的消息。LSO即第一个（偏移最靠前）打开事务的偏移量减1。特别的，任何位于进行中事务消息之后的消息都不会返回给消费者。直到这些事务完成。作为结果，read_committed可能导致消费者无法读取到高水位。另外，seekToEnd()在read_committed模式下也仅读到LSO</p>
<p>&nbsp;</p>
</td>
</tr>
<tr>
<td><span style="color: #000000;">max.poll.interval.ms</span></td>
<td>int=300000。使用消费者组管理时，调用poll()的最大间隔。如果消费者在此时间内没有进行poll()以抓取更多数据，则服务器认为消费者已经失败，并执行再平衡（将分区分配给其它消费者）</td>
</tr>
<tr>
<td>max.poll.records</td>
<td>int=500。单次poll()调用返回的记录的最大数量</td>
</tr>
<tr>
<td>partition.assignment.strategy</td>
<td>
<p>list=org.apache.kafka.clients.consumer.RangeAssignor</p>
<p>使用消费者组管理时，客户端所使用的分区分配策略类。客户端使用此类决定如何在消费者实例之间分配分区</p>
</td>
</tr>
<tr>
<td>receive.buffer.bytes</td>
<td>int=65536。套接字参数SO_RCVBUF</td>
</tr>
<tr>
<td>send.buffer.bytes</td>
<td>int=131072。套接字参数SO_SNDBUF</td>
</tr>
<tr>
<td>request.timeout.ms</td>
<td>int=305000。客户端等待请求的响应到达的最大时间，超时后失败或者重发</td>
</tr>
<tr>
<td>security.protocol</td>
<td>string=PLAINTEXT。与代理通信使用的协议，可选PLAINTEXT, SSL, SASL_PLAINTEXT, SASL_SSL</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">Connect配置</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>bootstrap.servers</td>
<td>list=localhost:9092。初始化到Kafka集群连接的服务器列表</td>
</tr>
<tr>
<td>config.storage.topic</td>
<td>存储连接器配置的Kafka主题的名称</td>
</tr>
<tr>
<td>group.id</td>
<td>当前Worker所属的Kafka Connect集群组</td>
</tr>
<tr>
<td>key.converter<br />value.converter</td>
<td>
<p>配置Kafka Connect格式和串行化的存储到Kafka主题的格式之间的转换器类</p>
<p>两个配置项分别指定消息中键、值的转换器</p>
</td>
</tr>
<tr>
<td>offset.storage.topic</td>
<td>存储连接器偏移量的Kafka主题的名称</td>
</tr>
<tr>
<td>status.storage.topic</td>
<td>存储连接器、任务状态的主题的名称</td>
</tr>
<tr>
<td>heartbeat.interval.ms</td>
<td>
<p>int=3000。当使用Kafka的组管理功能时，发送心跳到组协调器（Group Coordinator）的周期</p>
<p>心跳用于确保Worker还活着，并且在有Worker加入/离开组时进行再平衡</p>
<p>必须设置的比session.timeout.ms，典型的小于其1/3</p>
</td>
</tr>
<tr>
<td>rebalance.timeout.ms</td>
<td>int=60000。再平衡开始后，任务Task需要在此配置的时限内刷出数据、提交偏移量。超时后Worker将从组中移除，从而导致偏移量提交失败</td>
</tr>
<tr>
<td>session.timeout.ms</td>
<td>int=10000。用于检测Worker失败的超时。通常情况下Worker周期性的发送心跳给代理，代理因而确认它还活着，如果超过此配置的时间代理没有收到心跳，则认为Worker失败</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">Streams配置</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><span style="color: #000000;">application.id</span></td>
<td>流处理应用程序的标识符，必须在Kafka集群内是唯一的。此标识符将用于：
<ol>
<li><span style="color: #333333; font-family: Ubuntu, 'Times New Roman', 'Bitstream Charter', Times, serif; font-size: small;">client-id前缀</span></li>
<li>组成员关系管理时的group-id</li>
<li>changelog主题名的前缀</li>
</ol>
</td>
</tr>
<tr>
<td><span style="color: #000000;">bootstrap.servers</span></td>
<td>初始化到Kafka集群连接的服务器列表</td>
</tr>
<tr>
<td><span style="color: #000000;">replication.factor</span></td>
<td>流处理应用程序创建的changelog主题、<span style="color: #000000;">repartition主题的复制因子</span></td>
</tr>
<tr>
<td><span style="color: #000000;">state.dir</span></td>
<td>存储状态的目录位置</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">AdminClient配置</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><span style="color: #000000;">bootstrap.servers</span></td>
<td>初始化到Kafka集群连接的服务器列表</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">JVM配置</span></div>
<p>推荐使用Java8的最新版本，例如LinkedIn就使用Java8 + G1回收器，JVM配置如下：</p>
<pre class="crayon-plain-tag">-Xmx6g -Xms6g -XX:MetaspaceSize=96m -XX:+UseG1GC
-XX:MaxGCPauseMillis=20 -XX:InitiatingHeapOccupancyPercent=35 -XX:G1HeapRegionSize=16M
-XX:MinMetaspaceFreeRatio=50 -XX:MaxMetaspaceFreeRatio=80</pre>
<p>LinkedIn最繁忙的集群，由60个代理、5万个分区（复制因子2），输入消息数量80万每秒，输入流量300MB/s、输出流量1GB+ /s。 基于上述JVM配置，90%的GC停顿时间小于21ms，每秒Young GC次数小于1次。</p>
<div class="blog_h2"><span class="graybg">硬件和OS配置</span></div>
<p>LinkedIn使用24GB内存的4核Xeon机器。你需要为读写保留足够的缓冲内存空间。假设需要能够缓冲30s，则需要的大概内存是写吞吐量 * 30</p>
<p>LinkedIn使用8个7200RPM的SATA硬盘阵列，磁盘的吞吐能力很重要，因为这经常是瓶颈的所在。可以配备更多数量的磁盘以增强吞吐能力。如果配置更加频繁的Flush，则更高转速的磁盘会让你受益，当然价格也更贵。</p>
<div class="blog_h3"><span class="graybg">操作系统</span></div>
<p>Kafka可以很好的运行在所有类UNIX系统上，相比之下Windows平台下则有一些问题。</p>
<p>通常不需要进行很多OS级别的调优，主要考虑一下两个参数：</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>文件描述符数量限制</td>
<td>
<p>Kafka使用到文件描述符的地方包括：日志段（每个段对应一个文件）、打开的网络连接</p>
<p>日志段需要的文件描述符总数的计算公式：</p>
<p style="padding-left: 30px;">(number_of_partitions)*(partition_size/segment_size)</p>
</td>
</tr>
<tr>
<td>最大套接字缓冲</td>
<td>对于跨数据中心的复制，加大此缓冲可能提高性能</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">磁盘和文件系统</span></div>
<p>推荐使用多块磁盘，以增强吞吐能力。不建议和应用程序日志、其它OS文件系统活动共享磁盘，因为这会影响Kafka的处理延迟。</p>
<p>将多块磁盘RAID成单个卷，或者分别挂载为不同的目录，都是可以的。由于Kafka天然的容错性，RAID提供的冗余能力可能不需要，其提高单个逻辑磁盘的能力更有意义。</p>
<p>如果为Kafka配置多个数据目录，则分区会循环分配到这些目录，每个分区仅仅会存放在单个目录中。如果数据不是分区均衡的，则磁盘负载可能不均衡。</p>
<p>RAID在底层进行了负载均衡，因此可能（不一定）避免上述问题。RAID的劣势是它通常对写吞吐量有较大不利影响，并且减少了磁盘可用容量。</p>
<p>RAID另一个潜在的优势是，提高磁盘硬件容错。然而，重建RAID阵列非常IO敏感，几乎让服务器不可用，也就是说RAID不能提升可用性。</p>
<div class="blog_h3"><span class="graybg">刷出管理</span></div>
<p>Kafka没有消息的内存缓冲池，它总是立即把数据写入到文件系统。由于OS页缓存（Pagecache）的存在，写入操作可能并没有持久化到硬盘中，具体写入时机，要么是应用程序显示调用fsync，要么是OS自行调度。在2.6.32以后的内核中，一系列pdflush线程负责在后台fsync。</p>
<p>你可以配置一个刷出策略，以便在一定时间后、累计一定消息数量后，fsync数据到磁盘。</p>
<p>需要注意：意外宕机不会导致数据丢失，因为通常主题的复制因子都大于1，缺少的数据可以从其它Replica上同步。因此Kafka的默认刷出策略是，应用程序不会触发fsync。fsync只会由OS或者Kafka后台线程完成。</p>
<p>启用应用程序级别的fsync，其缺点是：</p>
<ol>
<li>比较低效的磁盘使用模式，因为显式fsync让OS不能进行写重排序以优化性能</li>
<li>大部分Linux文件系统中fsync是阻塞的操作</li>
</ol>
<div class="blog_h3"><span class="graybg">文件系统选择</span></div>
<p>Kafka没有对文件系统有硬性依赖，但是XFS比起EXT4更加适合Kafka的负载。</p>
<div class="blog_h1"><span class="graybg">架构</span></div>
<div class="blog_h2"><span class="graybg">设计目标</span></div>
<ol>
<li>必须具有很高的吞吐能力，以支持高容量的事件系统，例如实时日志聚合</li>
<li>必须具有优雅的后备排队（Backlog）机制，以便处理来自离线系统的、周期性的负载</li>
<li>为了实现传统的消息递送语义，必须具有低延迟的特点</li>
<li>必须支持分区和分布式，保证弹性和扩容能力</li>
<li>必须具有容错能力，不会因为硬件故障导致数据丢失或者重复</li>
</ol>
<p>以上目标，导致Kafka更像数据库日志，而非消息队列。</p>
<div class="blog_h2"><span class="graybg">持久化设计</span></div>
<div class="blog_h3"><span class="graybg">磁盘也可以很快</span></div>
<p>Kafka非常依赖于文件系统来存储、缓存消息。很多人觉得磁盘非常慢，无法提供足够的性能。实际上，磁盘可能运行的比想象的更快或更慢，这取决于你如何使用它。良好设计的磁盘数据结构可以让磁盘IO和网络IO一样快，某些特定情况下，磁盘顺序访问可以比内存随机访问更快。</p>
<p>关于磁盘性能的一个重要事实是，最近十年来磁盘的吞吐能力和磁盘寻道的延迟越来越负相关。六块7200RPM磁盘构成的RAID5，其顺序写可以高达600MB/s，但是随机写仅仅100KB/s，这是一个巨大的反差（6000倍）。在所有使用模式下，线性读写的速度都是最可预测的，并且被操作系统很大程度的优化。现代操作系统都提供了预读（Read-ahead）和后写（Write-behind）技术，支持大块的预抓取数据、分组多个小的逻辑写操作为单个物理写操作。</p>
<p>为了补偿随机磁盘IO的低性能，现代操作系统都激进的利用空闲内存，可能所有的空闲内存都被用作磁盘缓存。所有的磁盘读写操作都经由这个缓存层进行处理。除非使用Direct I/O，这种缓存层无法被跳过，因此即使应用程序在内部维护缓存机制，这些缓存很可能在OS的页缓存中被再次缓存。</p>
<p>对于JVM来说，需要注意以下事实：</p>
<ol>
<li>对象形式的数据，其空间占用很长大，常常比串行化格式大两倍。这意味着从存储效率方面来说，内存缓存是比较浪费的</li>
<li>随着对空间的占用，垃圾回收器的行为越发复杂和缓慢</li>
</ol>
<p>因此，使用文件系统进行缓存，依赖于OS的页缓存机制，要比自行在内存中维护缓存更加有效 —— 避免了潜在的重复缓存、避免了JVM中松散的空间结构。在32G内存的机器上，可以有效的缓存28-30G的数据，却可以避免GC带来的性能问题。</p>
<p>使用文件系统缓存的另外一个优势是重启后不需要预热，内存缓存在重启后是空白的，需要重新加载。</p>
<p>使用文件系统缓存，还免去了手工维护内存、磁盘数据的一致性。</p>
<p>当你的磁盘访问风格是顺序读，则操作系统的预读功能将大大的提高读效率。因为预读总是能命中你所需的数据。</p>
<p>Kafka的日志，不在内存中缓存，直接写入到磁盘日志中。</p>
<div class="blog_h3"><span class="graybg">常量时间复杂度</span></div>
<p>消息系统中的持久化数据结构常常是每个消费者一个队列，附带关联的BTree结构，此数据用于随机访问messages的元数据。BTree是消息系统中最广泛使用的数据结构，用于支持大量事务性、非事务性语义。</p>
<p>但是，BTree结构具有较高的成本。尽管BTree结构本身是对数复杂度（可以近似看做常量复杂度），但是应用到磁盘操作时并非如此。每次磁盘寻道操作大概需要10ms级别，每个磁盘同时仅仅支持单个寻道操作，因此并发度受限，少量的磁盘寻道请求就会导致非常高的Overhead。</p>
<p>存储系统需要混合非常快的页面缓存读写操作，以及非常慢的磁盘寻道操作，因此站在观察者的角度，BTree的性能随着数据量的增加而降低。数据量翻倍后性能降低的超过两倍。</p>
<p>如果以日志系统的风格来设计消息队列，也就是说仅仅支持Append方式的写操作，则所有读写操作都能实现常数的时间复杂度，同时读写操作不会相互阻塞。这种存储风格的一个明显优势是，性能和数据量完全解耦。服务器可以使用廉价、低转速磁盘的全部空间。</p>
<p>性能和数据量解耦后，可以实现在典型消息系统中难以看到的特性。例如，在Kafka中，消息不会在消费后立即被删除（以压缩空间，因为数据量和性能负相关），而是被保留一周甚至更久。这种特性让消费者的行为非常灵活，可以Replay历史数据，或者跳转到未来进行消费。</p>
<div class="blog_h2"><span class="graybg">高效性</span></div>
<div class="blog_h3"><span class="graybg">IO优化</span></div>
<p>Kafka的一个主要目标是处理Web活动数据，这种数据的量是非常大的，一次页面访问可能产生数十条数据。为了保证数据的生产和消费都能流畅进行，Kafka必须非常高效。</p>
<p>关于磁盘的效率问题在前面讨论过了。消除磁盘性能问题后，系统中最常见的性能问题通常由于：</p>
<ol>
<li>过多的微小IO操作：C-S之间的IO和服务器内部的持久化都可能有这种问题。Kafka引入了消息集的概念，批量的发送消息，避免过多的网络请求带来Roundtrip开销。相应的，服务器把消息集整个的Append到日志，消费者也采取批量抓取的方式消费。Kafka的这种设计，让系统性能有了数量级的提升，因为批处理产生了更大的网络包、更大的顺序磁盘IO、更大的连续内存块</li>
<li>过多的字节拷贝：低消息生产速率下不是问题，反之则影响重大。为了避免消息拷贝，Kafka设计了统一的二进制消息格式供生产者、代理、消费者使用，这样数据块就可以直接传输，不需要修改。代理维护的分区日志，本身仅仅是目录中的一系列文件，文件中的消息同样使用标准的二进制格式</li>
</ol>
<p>通常情况下，将文件传输到套接字中的步骤是：</p>
<ol>
<li>OS在内核空间，读取文件到页面缓存</li>
<li>应用从内核空间读取页面缓存，拷贝到用户空间</li>
<li>应用将用户空间中的文件回写到内核空间（套接字缓冲）</li>
<li>OS将套接字缓冲拷贝到网络接口（NIC）缓冲。然后网卡负责发送</li>
</ol>
<p>很明显上述步骤较为低效，包含了4次拷贝2次系统调用。现代UNIX系统（Linux通过sendfile系统调用）对页面缓存到套接字的传输高度优化，统一消息格式因而受益。具体来说，免去了不必要的拷贝（Zero-copy），仅仅需要将页面缓存拷贝到NIC缓冲即可，不需要用户空间的参与。</p>
<p>Kafka主题通常有多组消费者，那么，日志一旦拷贝到页面缓存，就可以多次的Zero-copy并发送给消费者。这避免了反复拷贝数据到用户空间，记录的消费速率，仅仅受限于网络连接本身。</p>
<div class="blog_h3"><span class="graybg">端对端压缩</span></div>
<p>某些情况下，系统的瓶颈出现在网络带宽，而非磁盘或者CPU。当数据处理管线需要跨越数据中心进行消息收发时尤为如此。</p>
<p>用户可以自己实现压缩，但是客户端代码很难实现高效的压缩。Kafka采取批量压缩的方式，这样多个消息中反复出现的内容可以被有效的处理。</p>
<p>Kafka代理收到压缩消息后，直接存放到日志中，因此日志的磁盘结构也是紧凑的。除非消费者进行消费，消息不会解压缩。Kafka支持GZip、LZ4、Snappy等压缩协议。</p>
<div class="blog_h2"><span class="graybg">生产者</span></div>
<div class="blog_h3"><span class="graybg">负载均衡</span></div>
<p>生产者直接将消息发送给分区的Leader，Kafka没有引入中介的路由层。为了能让生产者知道向谁发送消息，所有节点都能应答元数据请求，给出服务器状态信息、分区Leader信息。</p>
<p>生产者负责决定将消息发送到哪个分区，可以随机发送、循环轮流发送，或者根据消息内容实现某种语义分区。要使用语义分区，生产者需要给记录一个适当的键，此键的哈希（默认逻辑，分区函数可以被覆盖）决定其被发送到哪个分区。例如，假设记录的键选用UserId，则用户的所有数据都被发送到单个分区。</p>
<div class="blog_h3"><span class="graybg">异步发送</span></div>
<p>批量处理是高效性的一个重大驱动力。Kafka生产者倾向于在内存中累计记录，并通过单个请求发送多个记录构成的批次。你可以配置累计记录时最多消耗的时间，或者累计数据的最大字节数。通过调整配置，你可以在更好吞吐量、更低延迟之间做权衡。</p>
<div class="blog_h2"><span class="graybg">消费者</span></div>
<p>消费者通过向Leader分区发送抓取（Fetch）请求来工作。请求中会包含消费者的消费偏移量，代理依据此偏移量发送一批记录作为响应。消费者对偏移量具有很大的控制能力，只要不超过可用的区间，消费者能够倒回（Rewind）以消费历史数据，或者跳转到“未来”（在跟不上生产者的节奏时）进行消费。</p>
<div class="blog_h3"><span class="graybg">推vs拉</span></div>
<p>消息应该由代理推送给客户端，还是由客户端主动去拉取？Kafka在这方面的设计和典型消息系统是一致的 —— 消息由生产者推送给代理，再由消费者从代理处拉取。</p>
<p>某些以日志为中心的系统，例如Apache Flume，把消息推送给消费者。推和拉各有其优缺点。</p>
<p>当推送消息给消费者时，代理很难针对消费者的特性进行处理，控制数据的推送速率，当生产者速度太快时，消费者很容易过载。拉模式下，消费者可以自由决定拉取速率，并通过某种后备（Backoff）机制缓和生产和消费速率不匹配的问题。拉模式还将将批量抓取的职责转移到消费者端，消费者更知道自己何时能消费大批量数据。</p>
<p>拉模式的缺点是，如果代理上没有可用的消息。消费者的轮询循环会反复进行，浪费CPU。Kafka的解决方式是使用长轮询，你可以配置一个等待时间，如果代理上没有消息可用，则客户端会在套接字上等待，而非立即结束请求。</p>
<div class="blog_h3"><span class="graybg">消费偏移量</span></div>
<p>对于消息系统来说，跟踪哪些消息已经被消费，是对性能有关键影响的地方。</p>
<p>传统消息系统一般都在代理端维护消息的元数据，通过元数据识别哪些消息已经被消费。当客户端抓取消息，或者Ack消息后，代理立即删除此消息。之所有要尽快的删除，是因为传统消息系统的数据结构的扩容性往往较差，倾向于尽可能让此结构更小。</p>
<p>识别消息是否真正被成功消费，并不简单。如果消息被抓取后立即删除，消费者可能在处理消息之前就宕掉。为解决此问题，消息系统通常引入Ack机制。这又引入新的问题，消费者可能在处理消息之后，Ack之前宕掉，并引发重复的消息处理。分布式事务机制可能解决此问题，但是性能很差。</p>
<p>Kafka跟踪消息进度的方式完全不同，它将主题分为若干个消息有序的分区，并引入消费偏移量，标记消费者当前的位置。对于每个消费者和分区的组合，仅仅需要一个整数就可以完成状态记录。</p>
<p>消费偏移量的引入，还让消息没必要立即删除，实际上，只要磁盘空间充足，你可能配置让Kafka在一周或更久后才删除数据。</p>
<div class="blog_h3"><span class="graybg">离线数据负载</span></div>
<p>由于Kafka不会立即删除数据，且数据量的多少和性能无关，因此，那种周期性运行，消费大量数据并存放到离线系统（Hadoop、RDBMS数据仓库）的消费者，可以和Kafka协同工作。</p>
<p>对于Hadoop来说，为每个节点/主题/分区的组合创建一个Map任务，可以很好的实现加载并行化。利用Hadoop提供的任务管理能力，失败的任务可以自动重启而不需要担心数据重复，重启的任务自动从原始的位置开始读取。 </p>
<div class="blog_h1"><span class="graybg">运维</span></div>
<div class="blog_h2"><span class="graybg">操控主题</span></div>
<p>Kafka的bin目录包含很多脚本，可以用于完成日常的管理工作。</p>
<div class="blog_h3"><span class="graybg">添加主题</span></div>
<p>主题可以在第一次被使用时自动创建，你可能需要对默认配置进行定制，以便控制默认窗口过程。</p>
<p>要手工添加主题，参考如下命令：</p>
<pre class="crayon-plain-tag">kafka-topics.sh --zookeeper zk_host:port/chroot --create --topic topic_name
      --partitions 20 --replication-factor 3 --config x=y</pre>
<p>分区日志存放在Kafka的log目录，每个分区都有自己的目录，目录命名规则为topicname-partitionid。</p>
<div class="blog_h3"><span class="graybg">修改主题</span></div>
<p>仍然使用上面的脚本，例如修改分区数可以：</p>
<pre class="crayon-plain-tag">kafka-topics.sh --zookeeper zk_host:port/chroot --alter --topic topic_name
      --partitions 40</pre>
<p>注意：</p>
<ol>
<li>分区可能具有应用语义，添加分区不会自动修改已有分区的数据分布</li>
<li>当前不支持减少分区数量</li>
</ol>
<p>要添加、删除主题的配置项，参考：</p>
<pre class="crayon-plain-tag">kafka-configs.sh --zookeeper zk_host:port/chroot --entity-type topics --entity-name topic_name 
    --alter --add-config x=y
kafka-configs.sh --zookeeper zk_host:port/chroot --entity-type topics --entity-name topic_name 
    --alter --delete-config x</pre>
<div class="blog_h3"><span class="graybg">删除主题 </span></div>
<p><pre class="crayon-plain-tag">kafka-topics.sh --zookeeper zk_host:port/chroot --delete --topic topic_name</pre></p>
<div class="blog_h2"><span class="graybg">优雅关闭</span></div>
<p>Kafka集群会自动检测到宕机、有意被关闭（维护目的）的代理，并在必要时为分区选举新的Leader。</p>
<p>对于有意关闭的情况，你可以优雅的停止代理而不是强行关机。优雅关闭的好处是：</p>
<ol>
<li>会自动同步所有日志到磁盘中，避免在重启时进行日志恢复。日志恢复对日志尾部的所有记录进行校验和操作，需要一定时间才能完成</li>
<li>对于待关闭机器是Leader的分区，执行迁移，将Leader身份迁移给其它代理。这种Leader迁移更快、Leader迁移导致的分区不可用时间仅仅在ms级别</li>
</ol>
<p>优雅关闭时，上述第1条自动发生。要启用第2条，需要配置：<pre class="crayon-plain-tag">controlled.shutdown.enable=true</pre></p>
<div class="blog_h2"><span class="graybg">再平衡</span></div>
<p>代理宕机后，其Leader身份全部被移除。这意味着，代理重启后，它不是任何分区的Leader，因而不会接受任何客户端的读写请求，其计算能力被浪费。</p>
<p>为了避免这种不平衡，Kafka引入优选Replica的概念。如果某个分区的复制集为1,5,9则1是优选节点，因为它在列表的最前面。要在Replica 1宕机重启后，自动恢复其Leader身份，可以执行：</p>
<p><pre class="crayon-plain-tag">kafka-preferred-replica-election.sh --zookeeper zk_host:port/chroot</pre></p>
<p>要自动在需要的时候执行上述命令，可以配置：<pre class="crayon-plain-tag">auto.leader.rebalance.enable=true</pre></p>
<div class="blog_h3"><span class="graybg">跨机柜再平衡 </span></div>
<p>从0.10开始Kafka支持机柜感知（Rack awareness），允许将分区的不同Replica分布到不同的Rack中，防止机柜整体断电断网导致Kafka分区完全不可用，Rack可以在地理上远程分布。 </p>
<p>要指定代理所属的机柜，配置broker.rack属性即可。</p>
<p>当创建、修改主题，或者再平衡Replica时，Kafka会尽可能的把分区的每个Replica分散到不同的Rack中。</p>
<div class="blog_h2"><span class="graybg">跨集群镜像</span></div>
<p>此脚本提供了一种跨越数据中心进行复制的手段。数据会从源集群中读取，并写到目标集群的同名主题中。实际上此镜像工具就是挂钩在一起的消费者-生产者组合。</p>
<p>为了增强吞吐量和容错能力，你可以运行此脚本的任意数量的实例。当某个实例意外宕掉后，其它实例会接管它的负载。</p>
<p>镜像不能作为完全有效的容错措施，因为源、目标集群是完全独立的，它们的偏移量取值是不一样的。</p>
<p>镜像单个主题的示例：</p>
<pre class="crayon-plain-tag">kafka-mirror-maker.sh
      --consumer.config consumer.properties
      --producer.config producer.properties 
      --whitelist topic-name    # 白名单，仅仅复制这些主题，可以使用正则式</pre>
<div class="blog_h2"><span class="graybg">跨数据中心部署</span></div>
<p>某些部署场景中，需要跨越数据中心复制数据。</p>
<div class="blog_h3"><span class="graybg">推荐的方式</span></div>
<p>Kafka推荐的方式是，在数据中心内部创建集群，应用程序仅仅和当前数据中心的Kafka集群交互。对于跨数据中心的数据复制，利用跨集群镜像实现。</p>
<p>这种部署模式的好处时，让数据中心之间解耦，使它们作为独立的实体运作。同时，跨数据中心的数据复制可以被集中管理。当数据中心之间的链路断开后，各中心可以独立运作，落后的数据复制进度可以在链路恢复后赶上。</p>
<p>如果某个应用需要全数据集的视图，使用跨集群镜像机制来聚合所有数据中心的主题，形成一个新数据中心，那些需要全数据集视图的应用，和新数据中心交互。</p>
<div class="blog_h3"><span class="graybg">远程访问主题</span></div>
<p>除了上述推荐方式以外，你也可以让应用通过WAN访问其它数据中心，但是很明显这会引入访问延迟。</p>
<p>不过，即便在高延迟网络环境下，Kafka天然支持的生产者/消费者批处理能力依然能让应用拥有较高吞吐量。不过你可能需要为代理、生产者、消费者调整socket.send.buffer.bytes、socket.receive.buffer.bytes等参数。</p>
<div class="blog_h3"><span class="graybg">跨数据中心集群</span></div>
<p>通常情况下，不要创建跨越数据中心的Kafka集群。特别是链路延迟较大的情况下。这会让ZooKeeper、Kafka的复制延迟都很大，此外，如果链路中断，则部分数据中心的Kafka、ZooKeeper会不可用。</p>
<p>&nbsp;</p>
<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">kafka-consumer-groups.sh --bootstrap-server localhost:9092 --list</pre>
<p>如果你使用老的High-Level消费者并且在ZooKeeper中存储组元数据（offsets.storage=zookeeper），则传递--zookeeper而非--bootstrap-server。</p>
<div class="blog_h3"><span class="graybg">检查消费偏移量</span></div>
<p>你可以检查某个消费者组中，每个分区的消费偏移量：</p>
<pre class="crayon-plain-tag">kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --group groupname
# 还可以使用如下形式（仅仅显示基于ZooKeeper的消费者，不显示使用Java Consumer API的消费者
kafka-consumer-groups.sh --zookeeper localhost:2181 --describe --group groupname
# 输出示例
# TOPIC PARTITION CURRENT-OFFSET LOG-END-OFFSET LAG CONSUMER-ID 
# 主题   分区号     消费偏移量      日志偏移量       延迟  消费者</pre>
<div class="blog_h2"><span class="graybg">集群扩容 </span></div>
<p>要添加新服务器到Kafka集群，你只需要为它分配唯一性的代理ID即可。但是新的代理不会自动被分配数据分区，因此默认情况下除非创建了新的主题，新加入的代理不会有任何作用。</p>
<p>在添加新服务器的同时，你常常会执行迁移，将已有的分区迁移到新服务器中。迁移过程需要手工触发，之后可以自动完成。迁移触发后，Kafka将新服务器作为待迁移分区的Follower节点，当完成数据复制，新服务器成为In-sync节点后，复制集中某个旧节点的数据被清除，成员身份也被解除。</p>
<p>Kafka提供了分区重分配工具，允许你跨越代理移动分区。理想的分区分布，能让所有代理节点具有均匀的数据负载、均匀的数据尺寸。Kafka没有提供自动识别“不均匀”的工具，因而管理员需要手工识别出哪些分区需要移动。</p>
<p>分区重分配工具可以在三个互斥的模式下运行：</p>
<ol>
<li>--generate，给出主题、代理的列表，生成一个候选重分配方案，将列出的主题的所有分区移动到代理中</li>
<li>--execute，执行用户给出的重分配方案，方案通过 --reassignment-json-file参数给出</li>
<li>--verify，显示上次重分配的执行情况，状态可以是成功、失败、进行中</li>
</ol>
<div class="blog_h3"><span class="graybg">自动分区迁移</span></div>
<p>在添加了新的服务器（扩容Kafka集群）之后，使用分区重分配工具，可以将一部分主题迁移到新添加的代理上，迁移整个主题，要比每次迁移一个分区更加容易。</p>
<p>要执行整个主题的迁移，用户需要指定被迁移的主题、接收主题的代理。迁移后，主题的复制因子保持不变，主题的分区被尽可能均匀的分布到目标代理中。</p>
<p>具体步骤如下：</p>
<ol>
<li>以JSON格式提供待迁移主题的列表：<br />
<pre class="crayon-plain-tag">{
    // 待迁移主题
    "topics": [{"topic": "foo1"}, {"topic": "foo2"}],
    "version":1
}</pre>
</li>
<li>使用分区重分配工具，生成一个候选的重分配置文件：<br />
<pre class="crayon-plain-tag">kafka-reassign-partitions.sh --zookeeper localhost:2181 
    --topics-to-move-json-file topics-to-move.json   # 待迁移主题列表文件
    --broker-list "5,6"                              # 目标代理列表（标识符）
    --generate
# 输出的前一部分是当前重分配置文件（略）
# 输出的后一部分是新的重分配置文件，示例：
# {
# "version":1,
#                 # 主题         # 分区编号     # 复制集
# "partitions":[{"topic":"foo1","partition":2,"replicas":[5,6]},
#               {"topic":"foo1","partition":0,"replicas":[5,6]},
#               {"topic":"foo2","partition":2,"replicas":[5,6]},
#               {"topic":"foo2","partition":0,"replicas":[5,6]},
#               {"topic":"foo1","partition":1,"replicas":[5,6]},
#               {"topic":"foo2","partition":1,"replicas":[5,6]}]
# }</pre>
</li>
<li>此时重分配尚未发生，你需要备份当前分配置文件，以便回滚。然后将重分配候选保存到JSON文件中，执行：<br />
<pre class="crayon-plain-tag">kafka-reassign-partitions.sh --zookeeper localhost:2181 
    # 重分配配置文件
    --reassignment-json-file expand-cluster-reassignment.json --execute</pre>
</li>
<li>上述命令会异步执行，你可以随时检查进度：<br />
<pre class="crayon-plain-tag">kafka-reassign-partitions.sh --zookeeper localhost:2181 
    --reassignment-json-file expand-cluster-reassignment.json --verify

# Reassignment of partition [foo1,0] completed successfully
# Reassignment of partition [foo1,1] is in progress </pre>
</li>
</ol>
<div class="blog_h3"><span class="graybg">定制分区迁移</span></div>
<p>除了上节那种自动分配、迁移整个主题的用法之外，分区重分配工具还允许你手工指定迁移特定的分区。如果你明白集群的负载分配情况，并且自动生成的重分配候选不能满足需要，可以进行使用这种定制迁移。</p>
<p>在准备好重分配置文件后，执行上节第3步即可。</p>
<div class="blog_h3"><span class="graybg">增加复制因子</span></div>
<p>增加某个分区的复制因子很简单，只需要在定制的重分配置文件中，为某个分区的replicas列表添加一个代理ID就可以了：</p>
<pre class="crayon-plain-tag">// 为foo的分区0添加一个复制因子，新的副本存放在代理7上
{ "version":1, "partitions": [{"topic":"foo","partition":0,"replicas":[5,6,7]}] }</pre>
<div class="blog_h3"><span class="graybg">限制迁移带宽</span></div>
<p>Kafka支持设置迁移所消耗的带宽的上限，避免集群再平衡、添加/移除代理时Replica在机器之间移动消耗过多的带宽，影响系统性能。</p>
<p>限制带宽的方式有两种：</p>
<ol>
<li>
<pre class="crayon-plain-tag">kafka-reassign-partitions.sh --execute —throttle 50000000  # 单位 Bytes/s </pre></p>
<p>注意：</p>
<ol>
<li>你可以在迁移过程中重新调用上述脚本并指定不同的带宽限制</li>
<li>在迁移完成后，你必须调用--verify来解除带宽限制，否则，正常的数据复制会一直被限流</li>
</ol>
</li>
<li>调用脚本<pre class="crayon-plain-tag">kafka-configs.sh</pre>来验证相关配置。和限流有关的配置有两对：<br />leader.replication.throttled.rate、follower.replication.throttled.rate   限流速率<br />leader.replication.throttled.replicas、follower.replication.throttled.replicas 受到限制的Replica</li>
</ol>
<div class="blog_h2"><span class="graybg">集群缩容</span></div>
<p>当前Kafka没有提供直接可用的集群缩容工具。这意味着，你需要使用分区重分配工具，将准备移除的代理上所有的分区全部迁移走。</p>
<div class="blog_h2"><span class="graybg">配额 </span></div>
<p>Kafka支持在user,client-id、user、client-id级别进行配额，限制用户对集群资源的占用。默认情况下，资源占用不受限制。</p>
<p>要为用户user1、客户端clientA进行配额，可以：</p>
<pre class="crayon-plain-tag">kafka-configs.sh --zookeeper localhost:2181 --alter
    # 配额        生产速率                  消费速率                 
    --add-config 'producer_byte_rate=1024,consumer_byte_rate=2048,request_percentage=200' 
    # 要仅对用户、客户端进行配额，则指定两项之一
    --entity-type users --entity-name user1 
    --entity-type clients --entity-name clientA</pre>
<p>要查看用户user1、客户端clientA的当前配额，可以：</p>
<pre class="crayon-plain-tag">kafka-configs.sh  --zookeeper localhost:2181 --describe --entity-type clients --entity-name clientA</pre>
<p>你可以在代理级别上设置默认的配额，对所有客户端生效：</p>
<pre class="crayon-plain-tag"># 仅仅当没有在ZooKeeper中覆盖时生效
quota.producer.default=10485760
quota.consumer.default=10485760  </pre>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/apache-kafka-study-note">Apache Kafka学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/apache-kafka-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>ZooKeeper学习笔记</title>
		<link>https://blog.gmem.cc/zookeeper-study-note</link>
		<comments>https://blog.gmem.cc/zookeeper-study-note#comments</comments>
		<pubDate>Mon, 08 Aug 2016 09:23:19 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[BigData]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[Hadoop]]></category>
		<category><![CDATA[ZooKeeper]]></category>
		<category><![CDATA[学习笔记]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=15297</guid>
		<description><![CDATA[<p>基础知识 ZooKeeper是Hadoop的子项目，实现高可靠的分布式协调服务。它可以提供分布式的配置、同步、命名、集群服务。ZooKeeper暴露了一系列简单的接口，具有Java、C语言绑定。 为了正确构建复杂的服务，ZooKeeper提供以下保证： 顺序一致性：来自客户端的更新，按照它们被发送的顺序应用 原子性：更新要么成功要么失败 单一系统镜像：不管客户端连接到哪个ZooKeeper实例，它都看到服务的一致性视图 可靠性：一旦更新被写入，即是永久的 Timeliness：客户端看到的系统视图确保在一定时间范围内更新到最新状态 ZooKeeper组件 ZooKeeper由以下组件构成： 服务器：运行在ZooKeeper ensemble节点上的Java服务器 客户端：一个Java类库，用于链接到ZooKeeper集群 Native客户端：基于C实现的客户端 其它可选组件 Native客户端和可选组件仅仅支持Linux。 服务器端模块 除了Request Processor之外，所有ZooKeeper实例持有一模一样的组件： replicated database，一个内存数据库，包含整个名字空间。对此数据库的更新被刷入磁盘，以便重启后恢复 每个ZooKeeper实例都可以接受客户端请求，读请求基于本地的数据库处理，更新服务状态的写请求基于 <a class="read-more" href="https://blog.gmem.cc/zookeeper-study-note">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/zookeeper-study-note">ZooKeeper学习笔记</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>ZooKeeper是Hadoop的子项目，实现高可靠的分布式协调服务。它可以提供<span style="background-color: #c0c0c0;">分布式的配置、同步、命名、集群服务</span>。ZooKeeper暴露了一系列简单的接口，具有Java、C语言绑定。</p>
<p>为了正确构建复杂的服务，ZooKeeper提供以下保证：</p>
<ol>
<li>顺序一致性：来自客户端的更新，按照它们被发送的顺序应用</li>
<li>原子性：更新要么成功要么失败</li>
<li>单一系统镜像：不管客户端连接到哪个ZooKeeper实例，它都看到服务的一致性视图</li>
<li>可靠性：一旦更新被写入，即是永久的</li>
<li>Timeliness：客户端看到的系统视图确保在一定时间范围内更新到最新状态</li>
</ol>
<div class="blog_h2"><span class="graybg">ZooKeeper组件</span></div>
<p>ZooKeeper由以下组件构成：</p>
<ol>
<li>服务器：运行在ZooKeeper ensemble节点上的Java服务器</li>
<li>客户端：一个Java类库，用于链接到ZooKeeper集群</li>
<li>Native客户端：基于C实现的客户端</li>
<li>其它可选组件</li>
</ol>
<p>Native客户端和可选组件仅仅支持Linux。</p>
<div class="blog_h2"><span class="graybg">服务器端模块</span></div>
<p>除了Request Processor之外，所有ZooKeeper实例持有一模一样的组件：</p>
<p><img class="aligncenter size-full wp-image-15303" src="https://blog.gmem.cc/wp-content/uploads/2016/08/zkcomponents.png" alt="zkcomponents" width="611" height="248" /></p>
<ol>
<li>replicated database，一个内存数据库，包含整个名字空间。对此数据库的更新被刷入磁盘，以便重启后恢复</li>
<li>每个ZooKeeper实例都可以接受客户端请求，读请求基于本地的数据库处理，更新服务状态的写请求基于 agreement protocol处理。此协议：
<ol>
<li>将一个实例作为leader，其它实例作为followers</li>
<li>follower从leader接收消息提议，并同意消息递送</li>
<li>消息层负责leader的故障转移、follower与leader的同步</li>
</ol>
</li>
<li>ZooKeeper使用一个定制化的原子消息协议，保证消息层是原子性的。这种原子性保证了本地数据库不会出现数据不一致性 </li>
</ol>
<div class="blog_h2"><span class="graybg">基本特性</span></div>
<div class="blog_h3"><span class="graybg">简单性</span></div>
<p>ZooKeeper很简单，它允许分布式进程通过一个共享的、有层次的名字空间来相互交互。这个名字空间的组织就像文件系统一样，名字空间由数据寄存器（data registers） —— 所谓znodes组成，类似于文件系统的目录/文件，ZooKeeper的数据是驻留内存的，而不是像文件系统那样写在磁盘上。</p>
<div class="blog_h3"><span class="graybg">复制性</span></div>
<p>就像被它管理的分布式进程，ZooKeeper本身也倾向于跨越一组宿主机（所谓ensemble）复制，整体上形成一个ZooKeeper服务。只要大部分节点可用，则ZooKeeper服务可用。</p>
<p>客户端连接到单个ZooKeeper节点，通过一个TCP连接来：</p>
<ol>
<li>发送请求</li>
<li>获得响应</li>
<li>获得监听的事件</li>
<li>发送心跳</li>
</ol>
<p>如果此TCP连接丢失，客户端可能连接到另外一个节点。</p>
<div class="blog_h3"><span class="graybg">有序性</span></div>
<p>ZooKeeper使用数字标注每个update，以反应所有ZooKeeper事务的顺序。后续操作可以使用此数字序号实现高层次的抽象，例如同步原语。</p>
<div class="blog_h3"><span class="graybg">高性能</span></div>
<p>ZooKeeper非常高速，特别是在读为主的应用场景下。在上千台机器上运行的ZooKeeper应用，其性能在读写比10:1左右最好。</p>
<div class="blog_h2"><span class="graybg">名字空间</span></div>
<p>ZooKeeper提供的名字空间非常类似于Linux目录树，由一系列以 / 分隔的路径元素组成，名字空间中的每个节点以完整路径唯一识别。</p>
<div class="blog_h2"><span class="graybg">时间</span></div>
<p>ZooKeeper使用多种方式来追踪时间：</p>
<ol>
<li>Zxid：即ZooKeeper事务ID。每次对ZooKeeper的状态做出修改，都对应一个这样的ID。这个ID是单调递增的</li>
<li>版本号：对节点的修改会导致其某个版本号字段变更</li>
<li>时间单元（Ticks）：在集群中，ZooKeeper使用时间单元来界定状态上传、会话过期、Peer连接过期的最小时间间隔</li>
<li>真实时间：节点的创建、修改时间使用真实时间记录</li>
</ol>
<div class="blog_h2"><span class="graybg">会话</span></div>
<p>客户端利用C/Java语言绑定库建立会话：</p>
<ol>
<li>客户端发起调用时，可以指定连接字符串、会话超时、默认Watcher等参数，连接字符串包括逗号分隔的多个ZooKeeper实例的host:port列表</li>
<li>客户端创建一个到ZooKeeper服务集群的句柄（Handle），句柄被创建后，进入CONNECTING状态。这时，ZooKeeper会：
<ol>
<li>创建一个代表会话ID的64位整数并分配给客户端</li>
<li>同时，发送一个会话密码给客户端，客户端重连时需要用到此密码</li>
<li>根据客户端请求的会话超时ms，确定一个允许的（受限于时间单元，必须在2-20倍之间）会话超时</li>
</ol>
</li>
<li>绑定库尝试建立到某个ZooKeeper实例的TCP连接。连接建立后，句柄进入CONNECTED状态</li>
<li>如果出现不可恢复的错误，包括会话过期、身份验证失败、客户端显式的关闭句柄，则句柄进入CLOSED状态</li>
<li>如果客户端发现连接意外断开，则尝试连接列表中下一个实例，直到重新连接上。客户端重新连接时，会在握手时携带自己的会话ID、密码</li>
<li>客户端重新连接上之后，可能发现会话已经过期</li>
<li>默认Watcher可以在会话状态发生变化（例如连接断开、会话过期）后，通知客户端</li>
</ol>
<p>从3.2开始，连接字符串可以有一个chroot后缀，例如<pre class="crayon-plain-tag">127.0.0.1:3000,127.0.0.1:3001,127.0.0.1:3002/pems/webmgr</pre> ，其意义是，将客户端的根znode设置为/pems/webmgr，所有绝对、相对路径均假设此znode为根。</p>
<p>客户端通过发送PING心跳来保活，这样双方都可以知道连接是否断开。</p>
<p>客户端可以变更连接字符串，也就是变更服务器列表。此变更可能触发一个负载均衡算法，并导致客户端重连到其它服务器。</p>
<div class="blog_h2"><span class="graybg">节点</span></div>
<p>名字空间的每个节点被称为znode，类似于文件系统的目录。znode有以下特点：</p>
<ol>
<li>路径是znode的唯一标识</li>
<li>znode可以具有关联的数据，还可以具有子节点</li>
<li>znode关联的数据通常很小，一般最大KB级别</li>
<li>znode具有一个状态结构（Stat Structure），存放版本号、数据长度、修改时间戳等信息</li>
<li>znode的数据发生变化，版本号就递增</li>
<li>znode同时保存数据的多个版本</li>
<li>读写znode数据的操作都是原子性的，写操作替换掉数据</li>
<li>每个节点包含一个访问控制列表（ACL）用于限制谁能做什么</li>
<li>znode可以被监控（watch），一旦数据修改、子节点变化，即可通常关注此znode的客户端。这是ZooKeeper的核心特性</li>
</ol>
<div class="blog_h3"><span class="graybg">临时节点</span></div>
<p>所谓临时（EPHEMERAL）节点，仅仅在创建它的Session存在期间存在。ZooKeeper的客户端Session基于TCP长连接，通过心跳来保活。</p>
<div class="blog_h3"><span class="graybg">有序节点</span></div>
<p>当创建一个znode时，你可以要求ZooKeeper<span style="background-color: #c0c0c0;">在路径尾部自动附加单调递增</span>的计数器。对于父节点来说，此计数器具有唯一性。计数器十位左侧补零，示例：<pre class="crayon-plain-tag">&lt;path&gt;0000000001</pre></p>
<p>计数器的下一个值，存储在父节点中。如果计数超过2147483647 会出现溢出。</p>
<div class="blog_h3"><span class="graybg">容器节点</span></div>
<p>3.6版本新增。这类节点的所有子节点都消失后，可能在未来的某个时间点被自动删除。</p>
<div class="blog_h3"><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>czxid</td>
<td>创建此节点的zxid </td>
</tr>
<tr>
<td>mzxid</td>
<td>最后一次修改此节点的zxid </td>
</tr>
<tr>
<td>pzxid</td>
<td>最后一次修改子节点的zxid </td>
</tr>
<tr>
<td>ctime</td>
<td>此节点创建的时间</td>
</tr>
<tr>
<td>mtime</td>
<td>此节点被修改的时间</td>
</tr>
<tr>
<td>version</td>
<td>关联数据变更版本号</td>
</tr>
<tr>
<td>cversion</td>
<td>子节点变更版本号</td>
</tr>
<tr>
<td>aversion</td>
<td>ACL版本号</td>
</tr>
<tr>
<td>ephemeralOwner</td>
<td>
<p>如果此节点是临时节点，则此字段存放节点创建者的会话ID</p>
<p>如果不是临时节点，值为0</p>
</td>
</tr>
<tr>
<td>dataLength</td>
<td>关联数据的长度</td>
</tr>
<tr>
<td>numChildren</td>
<td>子节点的数量</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">监控</span></div>
<p>ZooKeeper支持监控（Watch）的概念——<span style="background-color: #c0c0c0;">一次性的事件监听器</span>。它有三个要点：</p>
<ol>
<li>一次性的：当被监控数据发生变化后，事件发送且仅一次。例如<pre class="crayon-plain-tag">getData("/znode1", true)</pre>导致当znode1的关联数据发生变化后客户端得到通知，但是后续znode1的数据再发生变化则不会自动得到通知</li>
<li>发送给客户端：事件被异步的发送给客户端，并且ZooKeeper提供顺序性保证，客户端不可能在收到通知之前就看到目标数据的变化</li>
<li>监控什么数据：getData/exists针对关联数据进行监控，getChildren()则针对子节点进行监控</li>
</ol>
<p>ZooKeeper中所有的读操作，包括<pre class="crayon-plain-tag">getData()</pre>、<pre class="crayon-plain-tag">getChildren()</pre>、<pre class="crayon-plain-tag">exists()</pre>，都可选设置Watch。</p>
<p>Watch仅仅在客户端所连接到的服务器上维护，但如果客户端发生重连，则新服务器负责维护客户端之前注册的Watch。有一种情况下，Watch会遗漏监控：</p>
<ol>
<li>客户端设置了exists监控，然后客户端断开</li>
<li>被监控节点创建，然后又被删除</li>
<li>客户端重连，此时它不会收到Watch</li>
</ol>
<p>客户端可以调用removeWatches来移除监控。</p>
<div class="blog_h3"><span class="graybg">关于监控的注意事项</span></div>
<p>ZooKeeper对Watch提供以下保证：</p>
<ol>
<li>先发生的事件，其Watcher先被通知</li>
<li>针对同一事件先注册的Watcher先被通知</li>
<li>先得到Watch通知，然后才能看到目标znode数据的变化</li>
</ol>
<p>使用Watch时需要注意：</p>
<ol>
<li>一次性，如果需要获得后续变更通知，需要再次注册Watcher</li>
<li>由于再次注册会有网络延迟，这期间目标znode可能已经发生了多次变化，这些变化是捕获不到的</li>
<li>在连接断开期间，接收不到通知</li>
</ol>
<div class="blog_h3"><span class="graybg">监控与API对照表</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">事件类型</td>
<td style="text-align: center;">可监控该事件的API</td>
</tr>
</thead>
<tbody>
<tr>
<td>节点创建</td>
<td>exists</td>
</tr>
<tr>
<td>节点删除</td>
<td>exists / getData / getChildren</td>
</tr>
<tr>
<td>节点数据改变</td>
<td>getData</td>
</tr>
<tr>
<td>子节点的创建、删除、改变</td>
<td>getChildren</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">访问控制</span></div>
<p>ZooKeeper支持基于ACL对znode进行访问控制。其实现有些类似UNIX文件模式，不同的是，ZooKeeper没有user/group/world这三种角色，它允许定义无限数量的角色（ID），并为这些角色授权。此外<span style="background-color: #c0c0c0;">ACL不是递归的</span>，也就是子节点不会继承父节点的授权设置。</p>
<p>ZooKeeper支持可拔插的身份验证方案（scheme）。ID的形式则为<pre class="crayon-plain-tag">scheme:id</pre>，例如ip:172.16.16.1表示IP验证方案下的实体172.16.16.1。</p>
<p>当客户端登录ZooKeeper并对自己进行身份验证时，ZooKeeper将所有对应此客户端的ID都分配给它。当客户端访问znode时，这些ID用来进行ACL验证。</p>
<p>ACL中每个条目的格式为<pre class="crayon-plain-tag">(scheme:expression, perms)</pre>，其中expression取决于scheme，覆盖1-N个ID。例如<pre class="crayon-plain-tag">(ip:172.21.0.0/16, READ)</pre>表示授予172.21网段所有客户端读权限。</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;">Scheme</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>world</td>
<td>此模式仅包含单个id —— anyone，表示任何人</td>
</tr>
<tr>
<td>auth</td>
<td>此模式没有id，表示任何通过身份验证的人</td>
</tr>
<tr>
<td>digest</td>
<td>基于username:password字符串生产id，用在ACL条目中密码显示为Base64编码的SH1摘要</td>
</tr>
<tr>
<td>ip</td>
<td>使用客户端IP地址作为id</td>
</tr>
<tr>
<td>x509</td>
<td>使用数字整数进行验证，在ACL条目中使用X500主体名称作为id</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">ACL权限列表</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 15%; text-align: center;">Perm</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>CREATE</td>
<td>允许创建子节点</td>
</tr>
<tr>
<td>READ</td>
<td>允许读取数据、列出子节点</td>
</tr>
<tr>
<td>WRITE</td>
<td>允许修改节点的关联数据</td>
</tr>
<tr>
<td>DELETE</td>
<td>允许删除子节点</td>
</tr>
<tr>
<td>ADMIN</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>首先保证1.7或者更高版本的JDK已经安装，然后到<a href="https://mirrors.tuna.tsinghua.edu.cn/apache/zookeeper/stable/">稳定频道</a>下载二进制压缩包，解压到合适的目录中。</p>
<div class="blog_h2"><span class="graybg">配置</span></div>
<div class="blog_h3"><span class="graybg">Standalone </span></div>
<p>要启动ZooKeeper服务，需要创建一个配置文件conf/zoo.cfg：</p>
<pre class="crayon-plain-tag"># 基本的时间单元对应的毫秒数，心跳每个时间单元发送一次，会话过期最小时间2时间单元
tickTime=2000
# 内存数据库的快照存放目录
dataDir=/data
# 监听客户端连接的端口
clientPort=2181</pre>
<div class="blog_h3"><span class="graybg">quorum</span></div>
<p>独立运行的ZooKeeper可以用户开发、测试目的。在产品环境下，你需要在复制（Replication）模式下运行ZooKeeper。一组复制的ZookKeeper称为Quorum，它们必须共享一致的配置文件：</p>
<pre class="crayon-plain-tag">tickTime=2000
dataDir=/var/lib/zookeeper
clientPort=2181
# 连接到Leader超时时间单元
initLimit=5
# Follower最多落后Leader多少时间单元
syncLimit=2
quorumListenOnAllIPs=true
# 服务器列表，server.X中的X是实例ID，在运行时，每个实例检查自己数据目录下的myid文件，其中以ASCII写着自己的ID
# 前一个端口供Follower连接到Leader使用；后一个端口用于选举Leader
server.1=172.21.0.1:2888:3888
server.2=172.21.0.2:2888:3888
server.3=172.21.0.3:2888:3888</pre>
<p>每个Quorum最少应该包含3个ZooKeeper实例，并且应当配置奇数个数的实例。</p>
<div class="blog_h3"><span class="graybg">docker</span></div>
<p>官方镜像的配置文件路径为/conf/zoo.cfg，下面的命令创建容器：</p>
<pre class="crayon-plain-tag"># ZOO_MY_ID 用于设置myid文件，此镜像假设数据目录是/data
docker run -e "ZOO_MY_ID=2" --name zookeeper-2 --network local --ip 172.21.0.2 -d docker.gmem.cc/zookeeper
docker run -e "ZOO_MY_ID=3" --name zookeeper-3 --network local --ip 172.21.0.3 -d docker.gmem.cc/zookeeper</pre>
<div class="blog_h3"><span class="graybg">日志配置</span></div>
<p>环境变量<pre class="crayon-plain-tag">ZOO_LOG_DIR</pre>用于指定ZooKeeper运行期间产生的日志的存放目录。包括log4j日志和守护程序的stdout都存放在此目录中。</p>
<p>环境变量<pre class="crayon-plain-tag">ZOO_LOG4J_PROP</pre>用于设置log4j的日志级别、启用的Appender。此变量的默认值是INFO, CONSOLE，也即是将最低INFO级别的日志输出到标准输出。可以设置为<pre class="crayon-plain-tag">ERROR,ROLLINGFILE</pre>，以使用滚动日志。</p>
<div class="blog_h2"><span class="graybg">运行</span></div>
<pre class="crayon-plain-tag"># 在后台启动ZooKeeper服务
zkServer.sh start
# 在前台启动ZooKeeper服务
zkServer.sh start-foreground
# 停止ZooKeeper服务
zkServer.sh stop

# 启动CLI客户端
zkCli -server 127.0.0.1:2181</pre>
<div class="blog_h1"><span class="graybg">开发</span></div>
<div class="blog_h2"><span class="graybg">简单的API</span></div>
<p>ZooKeeper具有非常简单的编程接口，它仅仅提供以下操作：</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>create</td>
<td>在名字空间树中创建一个节点</td>
</tr>
<tr>
<td>delete</td>
<td>删除一个节点</td>
</tr>
<tr>
<td>exists</td>
<td>测试某个路径上的节点是否存在</td>
</tr>
<tr>
<td>get data</td>
<td>读取节点数据</td>
</tr>
<tr>
<td>set data</td>
<td>写入节点数据</td>
</tr>
<tr>
<td>get children</td>
<td>获取子节点列表</td>
</tr>
<tr>
<td>sync</td>
<td>等待数据传播到整个集群</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">Java绑定</span></div>
<div class="blog_h3"><span class="graybg">Maven依赖</span></div>
<pre class="crayon-plain-tag">&lt;dependency&gt;
    &lt;groupId&gt;org.apache.zookeeper&lt;/groupId&gt;
    &lt;artifactId&gt;zookeeper&lt;/artifactId&gt;
    &lt;version&gt;3.4.10&lt;/version&gt;
&lt;/dependency&gt;</pre>
<div class="blog_h3"><span class="graybg">有序节点</span></div>
<p>使用模式EPHEMERAL可以创建有序节点，反复针对同一路径创建有序节点，会自动后缀单调递增的编号。</p>
<pre class="crayon-plain-tag">ArrayList&lt;ACL&gt; acl = ZooDefs.Ids.OPEN_ACL_UNSAFE;
byte[] data = { 0 };
// 临时节点不支持子节点
try {
    // 同步创建
    zk.create( "/tmp", data, acl, CreateMode.PERSISTENT );
} catch ( KeeperException e ) {
    if ( e.code() != KeeperException.Code.NODEEXISTS ) throw e;
}
// 异步创建
zk.create( "/tmp/no", data, acl, CreateMode.EPHEMERAL_SEQUENTIAL, ( rc, path, ctx, name ) -&gt; {
    // rc: OK path: /tmp/no, name: /tmp/no0000000000
    // rc: OK path: /tmp/no, name: /tmp/no0000000001
    // rc: OK path: /tmp/no, name: /tmp/no0000000002
    LOGGER.debug( "rc: {} path: {}, name: {}", new Object[]{ KeeperException.Code.get( rc ), path, name } );
}, this );
TimeUnit.SECONDS.sleep( 1 );</pre>
<div class="blog_h3"><span class="graybg">简单Watch客户端</span></div>
<p>这个例子中，我们使用一个Agent来：</p>
<ol>
<li>管理到ZooKeeper的连接，一旦会话过期即重连</li>
<li>转发ZooKeeper发来的事件通知给客户端</li>
<li>对事件机制进行简单封装，实现了：
<ol>
<li>隔离客户端和ZooKeeper</li>
<li>会话过期后，自动重新注册Watch</li>
</ol>
</li>
</ol>
<p>Agent代码：</p>
<pre class="crayon-plain-tag">package cc.gmem.study;

import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.function.Consumer;

public class ZooKeeperAgent implements Watcher {

    private static final Logger LOGGER = LoggerFactory.getLogger( ZooKeeperAgent.class );


    private ZooKeeper zk;

    private String connectString;

    private int timeout;

    private Map&lt;Event.EventType, Map&lt;String, Queue&lt;Consumer&lt;EventResolver&gt;&gt;&gt;&gt; allListeners;

    // 对WatchedEvent进行简单包装，支持获取znode数据，对客户端屏蔽ZooKeeper
    public class EventResolver {

        private WatchedEvent event;

        public EventResolver( WatchedEvent event ) {
            this.event = event;
        }

        public WatchedEvent getEvent() {
            return event;
        }

        public byte[] getData() {

            return getData( null );
        }

        public byte[] getData( Stat stat ) {
            if ( stat == null ) {
                stat = new Stat();
            }
            try {
                return zk.getData( event.getPath(), false, stat );
            } catch ( Exception e ) {
                throw new RuntimeException( e.getMessage(), e );
            }
        }
    }

    public ZooKeeperAgent( String connectString, int timeout ) {
        this.connectString = connectString;
        this.timeout = timeout;
        allListeners = new ConcurrentHashMap&lt;&gt;();
        allListeners.put( Event.EventType.NodeCreated, new ConcurrentHashMap&lt;&gt;() );
        allListeners.put( Event.EventType.NodeDataChanged, new ConcurrentHashMap&lt;&gt;() );
        allListeners.put( Event.EventType.NodeDeleted, new ConcurrentHashMap&lt;&gt;() );
        allListeners.put( Event.EventType.NodeChildrenChanged, new ConcurrentHashMap&lt;&gt;() );
        allListeners = Collections.unmodifiableMap( allListeners );
        createZooKeeper();
    }

    public void on( Event.EventType eventType, String path, Consumer&lt;EventResolver&gt; listener ) {
        Map&lt;String, Queue&lt;Consumer&lt;EventResolver&gt;&gt;&gt; listenersOfType = this.allListeners.get( eventType );
        Queue&lt;Consumer&lt;EventResolver&gt;&gt; listeners;
        synchronized ( this ) {
            listeners = listenersOfType.get( path );
            if ( listeners == null ) {
                listeners = new ConcurrentLinkedQueue&lt;&gt;();
                listenersOfType.put( path, listeners );
            }
        }
        listeners.add( listener );
        watch( eventType, path );
    }

    private void watch( Event.EventType eventType, String path ) {
        try {
            switch ( eventType ) {
                case NodeCreated:
                    // 这里可以传递一个Watcher类型，也可以传入boolean
                    // 如果为true，使用构造ZooKeeper时提供的Watcher ———— 所谓defaultWatcher
                    zk.exists( path, this );
                    break;
                case NodeDataChanged:
                    Stat stat = new Stat();
                    zk.getData( path, this, stat );
                    break;
                case NodeDeleted:
                    zk.exists( path, this );
                    break;
                case NodeChildrenChanged:
                    zk.getChildren( path, this );
                    break;
            }
        } catch ( Exception e ) {
            LOGGER.error( e.getMessage(), e );
        }
    }

    private void createZooKeeper() {
        try {
            zk = new ZooKeeper( connectString, timeout, this );
            // 新会话，Watch需要重新注册
            allListeners.entrySet().stream().flatMap( entry -&gt; {
                Event.EventType eventType = entry.getKey();
                return entry.getValue().keySet().stream().map( path -&gt; {
                    return new Object[]{ eventType, path };
                } );
            } ).forEach( args -&gt; watch( (Event.EventType) args[0], (String) args[1] ) );
        } catch ( IOException e ) {
            throw new RuntimeException( e.getMessage(), e );
        }
    }

    public void process( WatchedEvent event ) {
        switch ( event.getType() ) {
            case None:
                onKeeperStateEvent( event );
                break;
            default:
                onZnodeEvent( event );
        }
    }

    private void onKeeperStateEvent( WatchedEvent event ) {
        switch ( event.getState() ) {
            case SyncConnected:
                LOGGER.debug( "Connected to server with session id {}", zk.getSessionId() );
                break;
            case Expired:
                LOGGER.debug( "Session expired, recreating" );
                // 一旦会话过期，ZooKeeper对象就废了
                createZooKeeper();
                break;
        }
    }

    private void onZnodeEvent( WatchedEvent event ) {
        Queue&lt;Consumer&lt;EventResolver&gt;&gt; consumers = allListeners.get( event.getType() ).get( event.getPath() );
        if ( consumers != null ) {
            consumers.forEach( consumer -&gt; consumer.accept( new EventResolver( event ) ) );
            // 继续监控
            watch( event.getType(), event.getPath() );
        }
    }

}</pre>
<p>客户端代码：</p>
<pre class="crayon-plain-tag">package cc.gmem.study;

import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.data.Stat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.TimeUnit;

import static org.apache.zookeeper.Watcher.Event.EventType.*;

/**
 * Created by alex on 8/10/16.
 */
public class WatchClient {

    private static final Logger LOGGER = LoggerFactory.getLogger( WatchClient.class );

    public static void main( String[] args ) throws InterruptedException {
        String connectString = "172.21.0.1:2181,172.21.0.2:2181,172.21.0.3:2181";
        ZooKeeperAgent agent = new ZooKeeperAgent( connectString, 2000 * 20 );
        agent.on( NodeCreated, "/user", ( resolver ) -&gt; {
            Stat stat = new Stat();
            LOGGER.debug( "znode craeted with data: {}", new String( resolver.getData( stat ) ) );
        } );
        agent.on( NodeDataChanged, "/user", ( resolver ) -&gt; {
            Stat stat = new Stat();
            LOGGER.debug( "znode data: {}, version: {}", new String( resolver.getData( stat ) ), stat.getVersion() );
        } );
        agent.on( NodeDeleted, "/user", ( resolver ) -&gt; {
            LOGGER.debug( "znode deleted" );
        } );
        TimeUnit.DAYS.sleep( 1 );
    }
}</pre>
<div class="blog_h1"><span class="graybg">应用场景</span></div>
<div class="blog_h2"><span class="graybg">命名服务</span></div>
<p>ZooKeeper名字空间可以直接当做类似于JNDI的命名服务使用，ZooKeeper和JNDI都可以把目录节点关联到特定的资源（但是JNDI的资源是Java对象），ZooKeeper还具有与生俱来的高可用性。</p>
<div class="blog_h2"><span class="graybg">配置管理</span></div>
<p>ZooKeeper可以用来管理分布式系统中的配置项，如果多个应用服务实例需要共享很多系统参数，可以交由ZooKeeper来管理，通过Watch的方式，应用服务可以在配置变更后获得通知。</p>
<div class="blog_h2"><span class="graybg">集群管理</span></div>
<p>ZooKeeper可以管理服务的集群：</p>
<ol>
<li>当新增服务器、删除服务器（服务器宕机）时，客户端、ZooKeeper、集群成员可以得到通知。实现原理是：
<ol>
<li>每个集群成员启动后，都在某个znode下创建临时子节点，这个子节点依赖于TCP长连接保活</li>
<li>一旦集群成员宕机，临时子节点由于TCP连接的断开而自动删除</li>
<li>关注者可以在父节点上调用getChildren( parentPath, true)进行watch，一旦子节点发生增减，此调用即返回</li>
</ol>
</li>
<li>在集群成员中选取Leader（Master）。实现原理是：
<ol>
<li>每个成员创建不但是临时，还是有序（SEQUENTIAL）的子节点</li>
<li>序号最小的成员，总是作为Leader</li>
<li>如果当前Leader宕机，则次小的成员被选作Leader</li>
</ol>
</li>
</ol>
<div class="blog_h2"><span class="graybg">分布式锁</span></div>
<p>要实现独占锁，可以把多个协作者共享的资源抽象为一个znode，然后：</p>
<ol>
<li>需要占用此资源的协作者，在znode 下创建一个临时、有序子节点</li>
<li>协作者判断自己这个节点序号是否最小：
<ol>
<li>如果是，意味着获得锁</li>
<li>如果否，则在父节点上watch。收到通知后，继续执行步骤2</li>
</ol>
</li>
<li> 当需要是否资源时，删除自己创建的子节点即可</li>
</ol>
<div class="blog_h2"><span class="graybg">屏障</span></div>
<p>所谓屏障（Barrier），即协作者到达某个状态后，等待其它协作者都到达此状态，然后一起继续。基于ZooKeeper可以这样实现屏障：</p>
<ol>
<li>每个协作者到达状态后，均创建一个子节点</li>
<li>判断子节点总数是否足够：
<ol>
<li>如果是，跨越屏障继续执行业务逻辑</li>
<li>如果否，则在父节点上watch。收到通知后，继续执行步骤2</li>
</ol>
</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>
<p>报错信息：</p>
<p>2017-08-09 17:53:03,750 [myid:1] - ERROR [/172.21.1.1:3888:QuorumCnxManager$Listener@763] - Exception while listening<br />java.net.BindException: Cannot assign requested address (Bind failed)</p>
<p>解决办法：配置文件添加<pre class="crayon-plain-tag">quorumListenOnAllIPs=true</pre></p>
<div class="blog_h3"><span class="graybg">如何查看集群状态</span></div>
<p>可以通过JMX查看，通过Oracle Mission Controll连接到名为 org.apache.zookeeper.server.quorum.QuorumPeerMain  的JVM，在MBean Browser选项卡中可以看到相关信息。</p>
<div class="blog_h3"><span class="graybg">zookeeper.out</span></div>
<p>此文件是ZooKeeper运行期间的标准输出，要指定它的存放位置，可以设置<pre class="crayon-plain-tag">ZOO_LOG_DIR</pre>这个环境变量。</p>
<p>注意，上述环境变量同时也作为log4j的日志输出目录。</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/zookeeper-study-note">ZooKeeper学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/zookeeper-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>
	</channel>
</rss>
