<?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; 学习笔记</title>
	<atom:link href="https://blog.gmem.cc/tag/%e5%ad%a6%e4%b9%a0%e7%ac%94%e8%ae%b0/feed" rel="self" type="application/rss+xml" />
	<link>https://blog.gmem.cc</link>
	<description></description>
	<lastBuildDate>Tue, 21 Apr 2026 10:40:56 +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>OpenStack学习笔记</title>
		<link>https://blog.gmem.cc/openstack-study-note</link>
		<comments>https://blog.gmem.cc/openstack-study-note#comments</comments>
		<pubDate>Fri, 19 Jan 2018 09:00:57 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[IaaS]]></category>
		<category><![CDATA[学习笔记]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=13588</guid>
		<description><![CDATA[<p>简介 OpenStack是一个开源的IaaS解决方案，使用它，你可以通过仪表盘或者利用OpenStack API控制/Provision大规模的计算、存储、网络资源池。 通过“驱动”，OpenStack支持大量商业、开源的计算、存储、网络相关技术框架，从而能够管理各种各样的基础设施。不管是裸金属机器、虚拟机、还是容器，都可以基于OpenStack进行管理，并共享网络、存储等底层资源： Kubernetes、CloudFoundry等PaaS平台可以构建在OpenStack之上。 项目组成 OpenStack由若干子项目组成，它们围绕着计算、存储、网络这三个核心概念组织： 计算：提供并管理网络中大量的虚拟机，主要由Nova子项目负责 存储：供服务器、应用程序使用的对象存储、块存储，分别由Swift、Cinder子项目负责 网络：可拔插、可扩容、API驱动的网络和IP管理 这三类子项目还具有一些共享的服务：identity、镜像管理（image management）、一个基于Web的UI接口。 总览图 OpenStack项目的总览图如下，其中粗体标出了它的核心子项目，包括Horizon、Heat、Nova、Neutron、Swift、Cinder等： 服务交互 以Bigdata as Service场景为例，各子项目的交互关系如下图： 简单的说明一下： Keystone提供身份验证服务 Ceilometer提供监控服务 Horizon提供一个管理UI <a class="read-more" href="https://blog.gmem.cc/openstack-study-note">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/openstack-study-note">OpenStack学习笔记</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>OpenStack是一个开源的IaaS解决方案，使用它，你可以通过仪表盘或者利用OpenStack API控制/Provision大规模的<span style="background-color: #c0c0c0;">计算、存储、网络资源池</span>。</p>
<p>通过“驱动”，OpenStack支持大量商业、开源的计算、存储、网络相关技术框架，从而能够管理各种各样的基础设施。不管是裸金属机器、虚拟机、还是容器，都可以基于OpenStack进行管理，并共享网络、存储等底层资源：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2019/04/overview-diagram-new.svg"><img class="aligncenter" style="width: 90%;" src="https://blog.gmem.cc/wp-content/uploads/2019/04/overview-diagram-new.svg" alt="" /></a>Kubernetes、CloudFoundry等PaaS平台可以构建在OpenStack之上。</p>
<div class="blog_h2"><span class="graybg">项目组成</span></div>
<p>OpenStack由若干子项目组成，它们围绕着计算、存储、网络这三个核心概念组织：</p>
<ol>
<li>计算：提供并管理网络中大量的虚拟机，主要由Nova子项目负责</li>
<li>存储：供服务器、应用程序使用的对象存储、块存储，分别由Swift、Cinder子项目负责</li>
<li>网络：可拔插、可扩容、API驱动的网络和IP管理</li>
</ol>
<p>这三类子项目还具有一些共享的服务：identity、镜像管理（image management）、一个基于Web的UI接口。</p>
<div class="blog_h3"><span class="graybg">总览图</span></div>
<p>OpenStack项目的总览图如下，其中粗体标出了它的核心子项目，包括<span style="background-color: #c0c0c0;">Horizon、Heat、Nova、Neutron、Swift、Cinder</span>等：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2019/04/openstack-map-v20190402.svg"><img style="width: 100%;" src="https://blog.gmem.cc/wp-content/uploads/2019/04/openstack-map-v20190402.svg" alt="" /></a></p>
<div class="blog_h2"><span class="graybg">服务交互</span></div>
<p>以Bigdata as Service场景为例，各子项目的交互关系如下图：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2019/04/openstack_kilo_conceptual_arch.png"><img style="width: 100%;" src="https://blog.gmem.cc/wp-content/uploads/2019/04/openstack_kilo_conceptual_arch.png" alt="" /></a></p>
<p>简单的说明一下：</p>
<ol>
<li>Keystone提供身份验证服务</li>
<li>Ceilometer提供监控服务</li>
<li>Horizon提供一个管理UI</li>
<li>Nova负责分配虚拟机</li>
<li>Glance提供镜像服务，镜像文件存放在Swift中</li>
<li>Cinder为虚拟机提供块存储卷</li>
<li>Cinder将卷备份到Swift中</li>
<li>Neuron为虚拟机提供网络连接</li>
</ol>
<p>每个子项目，或者叫OpenStack服务，都通过公共的Identity Service进行身份验证，<span style="background-color: #c0c0c0;">服务之间通过公共API进行交互</span>。每个<span style="background-color: #c0c0c0;">服务至少包含一个API进程</span>，此进程监听API请求，进行预处理然后转交给服务的其它部分进行处理。</p>
<p>每个<span style="background-color: #c0c0c0;">服务可以有多个进程</span>，这些<span style="background-color: #c0c0c0;">进程之间的通信方式通常是AMQP</span>。<span style="background-color: #c0c0c0;">服务的状态持久化在数据库中</span>。多种消息代理、RDBMS被支持，例如RabbitMQ、MySQL、MariaDB。</p>
<div class="blog_h2"><span class="graybg">客户端访问</span></div>
<p>用户访问OpenStack的方式有几种：</p>
<ol>
<li>通过Horizon提供的Web仪表盘</li>
<li>提供CLI客户端</li>
<li>提供SDK进行编程</li>
</ol>
<p>不管是何种方式，在底层都会向不同的OpenStack服务发送REST请求。</p>
<div class="blog_h2"><span class="graybg">组件简介</span></div>
<div class="blog_h3"><span class="graybg">Nova</span></div>
<p>Nova是OpenStack云中的计算组织控制器。支持OpenStack云中实例（instances）生命周期的所有活动都由Nova处理。这样使得Nova成为一个负责管理计算资源、网络、认证、所需可扩展性的平台。</p>
<div class="blog_h3"><span class="graybg">Neutron</span></div>
<p>Neutron是openstack核心项目之一，提供云计算环境下的虚拟网络功能。OpenStack网络（neutron）管理OpenStack环境中所有虚拟网络基础设施（VNI），物理网络基础设施（PNI）的接入层。</p>
<div class="blog_h3"><span class="graybg">Cinder</span></div>
<p>Cinder接口提供了一些标准功能，允许创建和附加块设备到虚拟机，如“创建卷”，“删除卷”和“附加卷”。还有更多高级的功能，支持扩展容量的能力，快照和创建虚拟机镜像克隆。</p>
<div class="blog_h3"><span class="graybg">Octavia</span></div>
<p>Octavia 是 openstack lbaas的支持的一种后台程序，提供为虚拟机流量的负载均衡。实质是类似于trove，调用 nova 以及neutron的api生成一台安装好haproxy和keepalived软件的虚拟机，并连接到目标网路。</p>
<div class="blog_h3"><span class="graybg">Swift</span></div>
<p>Swift 不是文件系统或者实时的数据存储系统，而是对象存储，用于长期存储永久类型的静态数据。这些数据可以检索、调整和必要时进行更新。Swift最适合虚拟机镜像、图片、邮件和存档备份这类数据的存储。</p>
<div class="blog_h3"><span class="graybg">Glance</span></div>
<p>Glance（OpenStack Image Service）是一个提供发现，注册，和下载镜像的服务。Glance提供了虚拟机镜像的集中存储。通过 Glance 的 RESTful API，可以查询镜像元数据、下载镜像。虚拟机的镜像可以很方便的存储在各种地方，从简单的文件系统到对象存储系统（比如 OpenStack Swift）。</p>
<div class="blog_h3"><span class="graybg">Horizon</span></div>
<p>Horizon 为 Openstack 提供一个 WEB 前端的管理界面 (UI 服务 )通过 Horizon 所提供的 DashBoard 服务 , 管理员可以使用通过 WEB UI 对 Openstack 整体云环境进行管理 , 并可直观看到各种操作结果与运行状态。</p>
<div class="blog_h3"><span class="graybg">Ironic</span></div>
<p>Ironic包含一个API和多个插件，用于安全性和容错性地提供物理服务器。它可以和nova结合被使用为hypervisor驱动，或者用bifrost使用为独立服务。默认情况下，它会使用PXE和IPMI去与裸金属机器去交互。Ironic也支持使用供应商的插件而实现额外的功能。</p>
<div class="blog_h3"><span class="graybg">Cyborg</span></div>
<p>Cyborg（以前称为Nomad）旨在为加速资源（即FPGA,GPU,SoC, NVMe SSD,DPDK/SPDK,eBPF/XDP …）提供通用管理框架。</p>
<div class="blog_h3"><span class="graybg">Kolla</span></div>
<p>kolla 的使命是为 openstack 云平台提供生产级别的、开箱即用的交付能力。kolla 的基本思想是一切皆容器，将所有服务基于 Docker 运行，并且保证一个容器只跑一个服务（进程），做到最小粒度的运行 docker。</p>
<div class="blog_h3"><span class="graybg">Kuryr</span></div>
<p>Kubernetes Kuryr是 OpenStack Neutron 的子项目，其主要目标是通过该项目来整合 OpenStack 与Kubernetes 的网络。该项目在 Kubernetes 中实现了原生 Neutron-based 的网络，因此<span style="background-color: #c0c0c0;">使用 Kuryr-Kubernetes 可以让你的 OpenStack VM 与 Kubernetes Pods 能够选择在同一个子网上运作</span>，并且能够使用 Neutron 的 L3 与 Security Group 来对网络进行路由，以及阻挡特定来源 Port。</p>
<div class="blog_h3"><span class="graybg">Manila</span></div>
<p>Manila项目全称是File Share Service，文件共享即服务，用来提供云上的文件共享，支持CIFS协议和NFS协议。</p>
<div class="blog_h3"><span class="graybg">Tacker</span></div>
<p>Tacker是一个在OpenStack内部孵化的项目, 他的作用是NVF管理器，用于管理NVF的生命周期。Tacker的重点是配置VNF, 并监视他们。如果需要，还可重启和/或扩展（自动修复）NVF。整个进程贯穿ETSIMANO所描述的整个生命周期。</p>
<div class="blog_h1"><span class="graybg">命令</span></div>
<div class="blog_h2"><span class="graybg">openstack</span></div>
<div class="blog_h3"><span class="graybg">configuration</span></div>
<p>显示详细配置信息：</p>
<pre class="crayon-plain-tag"># --unmask表示明文显示密码
openstack configuration show [--mask | --unmask]</pre>
<div class="blog_h3"><span class="graybg">domain</span></div>
<p>一个Domain，是用户、组、项目的集合。任何组、项目仅仅属于单个Domain。</p>
<pre class="crayon-plain-tag">openstack domain create
    [--description &lt;description&gt;]
    [--enable | --disable]
    [--or-show]
    # 禁止删除或修改，除非去掉此标记
    [--immutable | --no-immutable]
    &lt;domain-name&gt;

openstack domain delete &lt;domain&gt; [&lt;domain&gt; ...]

openstack domain list
    [--sort-column SORT_COLUMN]
    [--name &lt;name&gt;]
    [--enabled]

openstack domain set
    [--name &lt;name&gt;]
    [--description &lt;description&gt;]
    [--enable | --disable]
    [--immutable | --no-immutable]
    &lt;domain&gt;

openstack domain show &lt;domain&gt;</pre>
<div class="blog_h3"><span class="graybg">project</span></div>
<p>管理项目</p>
<pre class="crayon-plain-tag">openstack project create
    [--domain &lt;domain&gt;]
    [--parent &lt;project&gt;]
    [--description &lt;description&gt;]
    [--enable | --disable]
    [--property &lt;key=value&gt;]
    [--or-show]
    [--immutable | --no-immutable]
    [--tag &lt;tag&gt;]
    &lt;project-name&gt;

openstack project delete [--domain &lt;domain&gt;] &lt;project&gt; [&lt;project&gt; ...]

openstack project list
    [--sort-column SORT_COLUMN]
    [--domain &lt;domain&gt;]
    [--parent &lt;parent&gt;]
    [--user &lt;user&gt;]
    [--my-projects]
    [--long]
    [--sort &lt;key&gt;[:&lt;direction&gt;]]
    [--tags &lt;tag&gt;[,&lt;tag&gt;,...]]
    [--tags-any &lt;tag&gt;[,&lt;tag&gt;,...]]
    [--not-tags &lt;tag&gt;[,&lt;tag&gt;,...]]
    [--not-tags-any &lt;tag&gt;[,&lt;tag&gt;,...]]

openstack project set
    [--name &lt;name&gt;]
    [--domain &lt;domain&gt;]
    [--description &lt;description&gt;]
    [--enable | --disable]
    [--property &lt;key=value&gt;]
    [--immutable | --no-immutable]
    [--tag &lt;tag&gt;]
    [--clear-tags]
    [--remove-tag &lt;tag&gt;]
    &lt;project&gt;

openstack project show
    [--domain &lt;domain&gt;]
    [--parents]
    [--children]
    &lt;project&gt; </pre>
<div class="blog_h3"><span class="graybg">project purge</span></div>
<p>清除和指定项目关联的资源</p>
<pre class="crayon-plain-tag">openstack project purge
    [--dry-run]
    [--keep-project]
    (--auth-project | --project &lt;project&gt;)
    [--project-domain &lt;project-domain&gt;]</pre>
<div class="blog_h3"><span class="graybg">group</span></div>
<p>用户的组。</p>
<pre class="crayon-plain-tag"># 添加用户到组
openstack group add user
    [--group-domain &lt;group-domain&gt;]
    [--user-domain &lt;user-domain&gt;]
    &lt;group&gt;
    &lt;user&gt;
    [&lt;user&gt; ...]
openstack group remove user
    [--group-domain &lt;group-domain&gt;]
    [--user-domain &lt;user-domain&gt;]
    &lt;group&gt;
    &lt;user&gt;
    [&lt;user&gt; ...]

# 检查组是否包含用户
openstack group contains user
    [--group-domain &lt;group-domain&gt;]
    [--user-domain &lt;user-domain&gt;]
    &lt;group&gt;
    &lt;user&gt;

# 创建组
openstack group create
    [--domain &lt;domain&gt;]
    [--description &lt;description&gt;]
    [--or-show]
    &lt;group-name&gt;


openstack group delete [--domain &lt;domain&gt;] &lt;group&gt; [&lt;group&gt; ...]

openstack group list
    [--sort-column SORT_COLUMN]
    [--domain &lt;domain&gt;]
    [--user &lt;user&gt;]
    [--user-domain &lt;user-domain&gt;]
    [--long]
openstack group show [--domain &lt;domain&gt;] &lt;group&gt;

openstack group set
    [--domain &lt;domain&gt;]
    [--name &lt;name&gt;]
    [--description &lt;description&gt;]
    &lt;group&gt; </pre>
<div class="blog_h3"><span class="graybg">user</span></div>
<p>用户管理</p>
<pre class="crayon-plain-tag">openstack user create
    # 用户的默认domain
    [--domain &lt;domain&gt;]
    # 用户的默认project
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    # 指定密码
    [--password &lt;password&gt;]
    # 交互的输入密码
    [--password-prompt]
    [--email &lt;email-address&gt;]
    [--description &lt;description&gt;]
    # 禁止连续身份验证失败后，锁定用户
    [--ignore-lockout-failure-attempts]
    [--no-ignore-lockout-failure-attempts]
    # 禁止密码过期
    [--ignore-password-expiry]
    [--no-ignore-password-expiry]
    # 禁止首次使用必须修改密码
    [--ignore-change-password-upon-first-use]
    [--no-ignore-change-password-upon-first-use]
    # 锁定密码，不给修改
    [--enable-lock-password]
    [--disable-lock-password]
    # 启用多因子身份验证
    [--enable-multi-factor-auth]
    [--disable-multi-factor-auth]
    [--multi-factor-auth-rule &lt;rule&gt;]
    # 启用/禁用
    [--enable | --disable]
    # 显示已有的用户
    [--or-show]
    &lt;name&gt;
openstack user delete [--domain &lt;domain&gt;] &lt;user&gt; [&lt;user&gt; ...]

openstack user list
    [--sort-column SORT_COLUMN]
    [--domain &lt;domain&gt;]
    [--group &lt;group&gt; | --project &lt;project&gt;]
    [--long]
openstack user show [--domain &lt;domain&gt;] &lt;user&gt;

# 修改密码
openstack user password set
    [--password &lt;new-password&gt;]
    [--original-password &lt;original-password&gt;]


openstack user set
    [--name &lt;name&gt;]
    [--domain &lt;domain&gt;]
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    [--password &lt;password&gt;]
    [--password-prompt]
    [--email &lt;email-address&gt;]
    [--description &lt;description&gt;]
    [--ignore-lockout-failure-attempts]
    [--no-ignore-lockout-failure-attempts]
    [--ignore-password-expiry]
    [--no-ignore-password-expiry]
    [--ignore-change-password-upon-first-use]
    [--no-ignore-change-password-upon-first-use]
    [--enable-lock-password]
    [--disable-lock-password]
    [--enable-multi-factor-auth]
    [--disable-multi-factor-auth]
    [--multi-factor-auth-rule &lt;rule&gt;]
    [--enable | --disable]
    &lt;user&gt; </pre>
<div class="blog_h3"><span class="graybg">role</span></div>
<p>可以创建角色，将角色映射给用户或组。</p>
<pre class="crayon-plain-tag"># 将角色赋予组或用户
openstack role add
    [--system &lt;system&gt; | --domain &lt;domain&gt; | --project &lt;project&gt;]
    [--user &lt;user&gt; | --group &lt;group&gt;]
    [--group-domain &lt;group-domain&gt;]
    [--project-domain &lt;project-domain&gt;]
    [--user-domain &lt;user-domain&gt;]
    [--inherited]
    [--role-domain &lt;role-domain&gt;]
    &lt;role&gt;
openstack role remove
    [--system &lt;system&gt; | --domain &lt;domain&gt; | --project &lt;project&gt;]
    [--user &lt;user&gt; | --group &lt;group&gt;]
    [--group-domain &lt;group-domain&gt;]
    [--project-domain &lt;project-domain&gt;]
    [--user-domain &lt;user-domain&gt;]
    [--inherited]
    [--role-domain &lt;role-domain&gt;]
    &lt;role&gt;

# 创建角色
openstack role create
    [--description &lt;description&gt;]
    [--domain &lt;domain&gt;]
    [--or-show]
    [--immutable | --no-immutable]
    &lt;role-name&gt;

openstack role delete [--domain &lt;domain&gt;] &lt;role&gt; [&lt;role&gt; ...]

openstack role list [--sort-column SORT_COLUMN] [--domain &lt;domain&gt;]

openstack role set
    [--description &lt;description&gt;]
    [--domain &lt;domain&gt;]
    [--name &lt;name&gt;]
    [--immutable | --no-immutable]
    &lt;role&gt;

openstack role show [--domain &lt;domain&gt;] &lt;role&gt; </pre>
<div class="blog_h3"><span class="graybg">role assignment</span></div>
<p>角色和用户的映射关系。</p>
<pre class="crayon-plain-tag">openstack role assignment list
    [--role &lt;role&gt;]
    [--role-domain &lt;role-domain&gt;]
    [--user &lt;user&gt;]
    [--user-domain &lt;user-domain&gt;]
    [--group &lt;group&gt;]
    [--group-domain &lt;group-domain&gt;]
    [--domain &lt;domain&gt;]
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    [--effective]
    [--inherited]
    [--names] </pre>
<div class="blog_h3"><span class="graybg">implied role</span></div>
<p>指定角色之间的包含关系。</p>
<pre class="crayon-plain-tag">#                             被隐含的角色           目标角色
openstack implied role create --implied-role &lt;role&gt; &lt;role&gt;

openstack implied role delete --implied-role &lt;role&gt; &lt;role&gt;

openstack implied role list [--sort-column SORT_COLUMN] </pre>
<div class="blog_h3"><span class="graybg">trust</span></div>
<p>提供特定项目中，用户之间的角色代理，支持可选的替身机制（impersonation）。需要 OS-TRUST扩展。</p>
<div class="blog_h3"><span class="graybg">consumer</span></div>
<p>在Identity服务的OS-OAUTH1扩展中使用，用于创建request token 、access token，仅仅支持Identity v3。</p>
<pre class="crayon-plain-tag">openstack consumer create [--description &lt;description&gt;]
openstack consumer delete &lt;consumer&gt; [&lt;consumer&gt; ...]
openstack consumer list [--sort-column SORT_COLUMN]
openstack consumer set [--description &lt;description&gt;] &lt;consumer&gt;
openstack consumer show &lt;consumer&gt;</pre>
<div class="blog_h3"><span class="graybg">credential</span></div>
<p>管理凭证：</p>
<pre class="crayon-plain-tag">openstack credential create
    [--type &lt;type&gt;]
    [--project &lt;project&gt;]
    &lt;user&gt;
    &lt;data&gt;

openstack credential delete &lt;credential-id&gt; [&lt;credential-id&gt; ...]

openstack credential list
    [--sort-column SORT_COLUMN]
    [--user &lt;user&gt;]
    [--user-domain &lt;user-domain&gt;]
    [--type &lt;type&gt;]

openstack credential set
    --user &lt;user&gt;
    --type &lt;type&gt;
    --data &lt;data&gt;
    [--project &lt;project&gt;]
    &lt;credential-id&gt;

openstack credential show &lt;credential-id&gt; </pre>
<div class="blog_h3"><span class="graybg">application credential</span></div>
<p>使用应用凭证，用户可以给自己的应用程序授予对云资源的有限访问权限。</p>
<pre class="crayon-plain-tag"># 创建新的凭证
openstack application credential create
    [--secret &lt;secret&gt;]
    [--role &lt;role&gt;]
    [--expiration &lt;expiration&gt;]
    [--description &lt;description&gt;]
    [--unrestricted]
    [--restricted]
    [--access-rules &lt;access-rules&gt;]
    &lt;name&gt;

openstack application credential delete
    &lt;application-credential&gt;
    [&lt;application-credential&gt; ...]

openstack application credential list
    [--sort-column SORT_COLUMN]
    [--user &lt;user&gt;]
    [--user-domain &lt;user-domain&gt;]

openstack application credential show &lt;application-credential&gt;</pre>
<div class="blog_h3"><span class="graybg">access rule</span></div>
<p>对应用程序凭证的权限进行细粒度控制。每个访问规则包含一下要素：</p>
<p>服务类型 + 请求路径 + 请求方法</p>
<pre class="crayon-plain-tag">openstack access rule delete &lt;access-rule&gt; [&lt;access-rule&gt; ...]

openstack access rule list
    [--user &lt;user&gt;]
    [--user-domain &lt;user-domain&gt;]

openstack access rule show &lt;access-rule&gt;</pre>
<div class="blog_h3"><span class="graybg">token</span></div>
<p>创建或吊销一个令牌</p>
<pre class="crayon-plain-tag">openstack token issue

openstack token revoke &lt;token&gt; </pre>
<div class="blog_h3"><span class="graybg">access token</span></div>
<p>Identity服务的OS-OAUTH1扩展使用访问令牌。consumer可以代表被授权用户来获得新的Identity API token。</p>
<pre class="crayon-plain-tag">openstack access token create
    # consumer的键/密钥
    --consumer-key &lt;consumer-key&gt;
    --consumer-secret &lt;consumer-secret&gt;
    --request-key &lt;request-key&gt;
    --request-secret &lt;request-secret&gt;
    --verifier &lt;verifier&gt;</pre>
<div class="blog_h3"><span class="graybg">request token</span></div>
<p>Identity服务的OS-OAUTH1扩展使用请求令牌。consumer使用此令牌来请求access token</p>
<pre class="crayon-plain-tag"># 验证请求令牌
openstack request token authorize
    --request-key &lt;request-key&gt;
    --role &lt;role&gt;

# 创建请求令牌
openstack request token create
    --consumer-key &lt;consumer-key&gt;
    --consumer-secret &lt;consumer-secret&gt;
    --project &lt;project&gt;
    [--domain &lt;domain&gt;]</pre>
<div class="blog_h3"><span class="graybg">policy</span></div>
<p>策略是一组规则，可以被远程服务消费。</p>
<pre class="crayon-plain-tag">openstack policy create [--type &lt;type&gt;] &lt;filename&gt;

openstack policy delete &lt;policy&gt; [&lt;policy&gt; ...]

openstack policy list [--sort-column SORT_COLUMN] [--long]

openstack policy set [--type &lt;type&gt;] [--rules &lt;filename&gt;] &lt;policy&gt;

openstack policy show &lt;policy&gt; </pre>
<div class="blog_h3"><span class="graybg">network rbac</span></div>
<p>基于RBAC的、针对网络资源的授权控制策略。让用户获得某个项目的网络资源的访问权限</p>
<pre class="crayon-plain-tag">openstack network rbac create
    --type &lt;type&gt;
    --action &lt;action&gt;
    (--target-project &lt;target-project&gt; | --target-all-projects)
    [--target-project-domain &lt;target-project-domain&gt;]
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    &lt;rbac-object&gt;

openstack network rbac delete &lt;rbac-policy&gt; [&lt;rbac-policy&gt; ...]

openstack network rbac list
    [--sort-column SORT_COLUMN]
    [--type &lt;type&gt;]
    [--action &lt;action&gt;]
    [--long]

openstack network rbac set
    [--target-project &lt;target-project&gt;]
    [--target-project-domain &lt;target-project-domain&gt;]
    &lt;rbac-policy&gt;

openstack network rbac show &lt;rbac-policy&gt; </pre>
<div class="blog_h3"><span class="graybg">quota</span></div>
<p>很多API都支持资源配额</p>
<pre class="crayon-plain-tag">openstack quota set
    # 为指定的class设置配额
    [--class]
    # 核心数配额
    [--cores &lt;cores&gt;]
    # 固定IP数量配额
    [--fixed-ips &lt;fixed-ips&gt;]
    [--injected-file-size &lt;injected-file-size&gt;]
    [--injected-path-size &lt;injected-path-size&gt;]
    [--injected-files &lt;injected-files&gt;]
    [--instances &lt;instances&gt;]
    [--key-pairs &lt;key-pairs&gt;]
    [--properties &lt;properties&gt;]
    [--ram &lt;ram&gt;]
    [--server-groups &lt;server-groups&gt;]
    [--server-group-members &lt;server-group-members&gt;]
    [--backups &lt;backups&gt;]
    [--backup-gigabytes &lt;backup-gigabytes&gt;]
    [--gigabytes &lt;gigabytes&gt;]
    [--per-volume-gigabytes &lt;per-volume-gigabytes&gt;]
    [--snapshots &lt;snapshots&gt;]
    [--volumes &lt;volumes&gt;]
    [--floating-ips &lt;floating-ips&gt;]
    [--secgroup-rules &lt;secgroup-rules&gt;]
    [--secgroups &lt;secgroups&gt;]
    [--networks &lt;networks&gt;]
    [--subnets &lt;subnets&gt;]
    [--ports &lt;ports&gt;]
    [--routers &lt;routers&gt;]
    [--rbac-policies &lt;rbac-policies&gt;]
    [--subnetpools &lt;subnetpools&gt;]
    [--volume-type &lt;volume-type&gt;]
    [--force]
    # 此配额针对的项目或者class
    &lt;project/class&gt;

openstack quota list
    [--sort-column SORT_COLUMN]
    [--project &lt;project&gt;]
    [--detail]
    (--compute | --volume | --network)

openstack quota show [--class | --default] [&lt;project/class&gt;]

# 显示针对所有项目的默认配额
openstack quota show --default

# 增大默认安装下admin项目的资源配额
openstack quota set --cores 32 admin
openstack quota set --ram 131072 admin
openstack quota set --gigabytes 8192 admin
openstack quota set --volumes 32 admin
openstack quota set --snapshots 32 admin
openstack quota set --instances 16 admin</pre>
<div class="blog_h3"><span class="graybg">limit</span></div>
<p>用于在项目级别进行资源配额。</p>
<pre class="crayon-plain-tag"># 创建一个配额
openstack limit create
    [--description &lt;description&gt;]
    # 此配额影响的区域
    [--region &lt;region&gt;]
    # 此配额针对的项目
    --project &lt;project&gt;
    # 负责资源的服务
    --service &lt;service&gt;
    # 资源额度
    --resource-limit &lt;resource-limit&gt;
    &lt;resource-name&gt;

openstack limit delete &lt;limit-id&gt; [&lt;limit-id&gt; ...]

openstack limit list
    [--sort-column SORT_COLUMN]
    [--service &lt;service&gt;]
    [--resource-name &lt;resource-name&gt;]
    [--region &lt;region&gt;]
    [--project &lt;project&gt;]

openstack limit set
    [--description &lt;description&gt;]
    [--resource-limit &lt;resource-limit&gt;]
    &lt;limit-id&gt;

openstack limit show &lt;limit-id&gt;</pre>
<div class="blog_h3"><span class="graybg">registered limit</span></div>
<p>用于定义OpenStack部署中的默认资源限制</p>
<pre class="crayon-plain-tag">openstack registered limit create
    [--description &lt;description&gt;]
    [--region &lt;region&gt;]
    --service &lt;service&gt;
    --default-limit &lt;default-limit&gt;
    &lt;resource-name&gt;

openstack registered limit delete
    &lt;registered-limit-id&gt;
    [&lt;registered-limit-id&gt; ...]

openstack registered limit list
    [--sort-column SORT_COLUMN]
    [--service &lt;service&gt;]
    [--resource-name &lt;resource-name&gt;]
    [--region &lt;region&gt;]

openstack registered limit set
    [--service &lt;service&gt;]
    [--resource-name &lt;resource-name&gt;]
    [--default-limit &lt;default-limit&gt;]
    [--description &lt;description&gt;]
    [--region &lt;region&gt;]
    &lt;registered-limit-id&gt;

openstack registered limit show &lt;registered-limit-id&gt; </pre>
<div class="blog_h3"><span class="graybg">limits</span></div>
<p>显示计算、存储资源用量的限制</p>
<pre class="crayon-plain-tag">openstack limits show
    [--sort-column SORT_COLUMN]
    (--absolute | --rate)
    [--reserved]
    [--project &lt;project&gt;]
    [--domain &lt;domain&gt;]</pre>
<div class="blog_h3"><span class="graybg">usage</span></div>
<p>显示项目的资源用量</p>
<pre class="crayon-plain-tag">openstack usage list
    [--sort-column SORT_COLUMN]
    # 用量统计的起始时间，默认4周前
    [--start &lt;start&gt;]
    # 用量统计的结束日期，默认明天
    [--end &lt;end&gt;]

openstack usage show
    [--project &lt;project&gt;]
    [--start &lt;start&gt;]
    [--end &lt;end&gt;] </pre>
<div class="blog_h3"><span class="graybg">region</span></div>
<p>区域是OpenStack部署中的最大的分区。你可以配置多个sub-region，甚至形成树形结构。</p>
<pre class="crayon-plain-tag">openstack region create
    [--parent-region &lt;region-id&gt;]
    [--description &lt;description&gt;]
    &lt;region-id&gt;

openstack region delete &lt;region-id&gt; [&lt;region-id&gt; ...]

openstack region list
    [--sort-column SORT_COLUMN]
    [--parent-region &lt;region-id&gt;]

openstack region set
    [--parent-region &lt;region-id&gt;]
    [--description &lt;description&gt;]
    &lt;region-id&gt;

openstack region show &lt;region-id&gt; </pre>
<div class="blog_h3"><span class="graybg">availability zone</span></div>
<p>可用区是云存储、计算、网络服务的逻辑分区。对等的AZ具有构成HA的效果，这和Region不同。</p>
<pre class="crayon-plain-tag"># 列出可用区
openstack availability zone list
    [--sort-column SORT_COLUMN]
    [--compute]
    [--network]
    [--volume]
    [--long] </pre>
<div class="blog_h3"><span class="graybg">aggregate</span></div>
<p>聚合是一组分组host的机制：</p>
<pre class="crayon-plain-tag"># 添加/删除主机到聚合中
openstack aggregate add host &lt;aggregate&gt; &lt;host&gt;
openstack aggregate remove host &lt;aggregate&gt; &lt;host&gt;

# 为聚合请求缓存镜像
openstack aggregate cache image &lt;aggregate&gt; &lt;image&gt; [&lt;image&gt; ...]

# 创建一个聚合，可以看到聚合是在某个AZ内部的
openstack aggregate create
    [--zone &lt;availability-zone&gt;]
    [--property &lt;key=value&gt;]
    &lt;name&gt;

openstack aggregate delete &lt;aggregate&gt; [&lt;aggregate&gt; ...]

# 为聚合设置元数据（键值对），然后为Flavor设置scope为aggregate_instance_extra_specs的
# 额外规格，规格键值和元数据一致，可以将Flavor映射到聚合。从Flavor创建的实例将位于聚合的主机中
openstack aggregate set
    [--name &lt;name&gt;]
    [--zone &lt;availability-zone&gt;]
    [--property &lt;key=value&gt;]
    [--no-property]
    &lt;aggregate&gt;
openstack aggregate unset [--property &lt;key&gt;] &lt;aggregate&gt;

openstack aggregate show &lt;aggregate&gt;
openstack aggregate list [--sort-column SORT_COLUMN] [--long]</pre>
<div class="blog_h3"><span class="graybg">host</span></div>
<p>运行Hypervisor的物理机器。</p>
<pre class="crayon-plain-tag">openstack host list [--sort-column SORT_COLUMN] [--zone &lt;zone&gt;]

openstack host set
    [--enable | --disable]
    [--enable-maintenance | --disable-maintenance]
    &lt;host&gt;

openstack host show [--sort-column SORT_COLUMN] &lt;host&gt;</pre>
<div class="blog_h3"><span class="graybg">hypervisor</span></div>
<pre class="crayon-plain-tag">openstack hypervisor list
    [--sort-column SORT_COLUMN]
    [--matching &lt;hostname&gt;]
    [--long]

openstack hypervisor show &lt;hypervisor&gt;</pre>
<div class="blog_h3"><span class="graybg">hypervisor stats </span> </div>
<pre class="crayon-plain-tag">openstack hypervisor stats show </pre>
<div class="blog_h3"><span class="graybg">keypair</span></div>
<p>OpenSSH公钥管理，用于访问创建的server（虚拟机）。</p>
<pre class="crayon-plain-tag"># 创建公钥
# 如果什么参数都不指定，则生成新的公钥
openstack keypair create
    [--public-key &lt;file&gt; | --private-key &lt;file&gt;]
    [--type &lt;type&gt;]
    [--user &lt;user&gt;]
    [--user-domain &lt;user-domain&gt;]
    &lt;name&gt;

openstack keypair delete
    [--user &lt;user&gt;]
    [--user-domain &lt;user-domain&gt;]
    &lt;key&gt;
    [&lt;key&gt; ...]

openstack keypair list
    [--sort-column SORT_COLUMN]
    [--user &lt;user&gt;]
    [--user-domain &lt;user-domain&gt;]
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]

openstack keypair show
    [--public-key]
    [--user &lt;user&gt;]
    [--user-domain &lt;user-domain&gt;]
    &lt;key&gt;</pre>
<div class="blog_h3"><span class="graybg">versions show</span></div>
<p>显示所有服务的版本、端点、是否弃用之类的信息</p>
<pre class="crayon-plain-tag">openstack versions show </pre>
<div class="blog_h3"><span class="graybg">catalog</span></div>
<p>显示服务的类型、名称、端点列表：</p>
<pre class="crayon-plain-tag">openstack catalog list [--sort-column SORT_COLUMN]
openstack catalog show &lt;service&gt;

openstack catalog list
# +-----------+-----------+-----------------------------------------+
# | Name      | Type      | Endpoints                               |
# +-----------+-----------+-----------------------------------------+
# | glance    | image     | zircon                                  |
# |           |           |   public: http://os.gmem.cc:9292        |
# |           |           |                                         |
# | keystone  | identity  | zircon                                  |
# |           |           |   internal: http://os.gmem.cc:5000/v3/  |
# |           |           | zircon                                  |
# |           |           |   public: http://os.gmem.cc:5000/v3/    |
# |           |           | zircon                                  |
# |           |           |   admin: http://os.gmem.cc:5000/v3/     |
# |           |           |                                         |
# | nova      | compute   | zircon                                  |
# |           |           |   internal: http://os.gmem.cc:8774/v2.1 |
# |           |           | zircon                                  |
# |           |           |   public: http://os.gmem.cc:8774/v2.1   |
# |           |           | zircon                                  |
# |           |           |   admin: http://os.gmem.cc:8774/v2.1    |
# |           |           |                                         |
# | placement | placement | zircon                                  |
# |           |           |   public: http://os.gmem.cc:8778        |
# |           |           | zircon                                  |
# |           |           |   admin: http://os.gmem.cc:8778         |
# |           |           | zircon                                  |
# |           |           |   internal: http://os.gmem.cc:8778      |
# |           |           |                                         |
# +-----------+-----------+-----------------------------------------+</pre>
<div class="blog_h3"><span class="graybg">extension</span></div>
<p>很多OpenStack API包含API扩展，这些扩展提供额外的功能。</p>
<pre class="crayon-plain-tag"># 列出API扩展
openstack extension list
    [--sort-column SORT_COLUMN]
    [--compute]
    [--identity]
    [--network]
    [--volume]
    [--long]

# 显示API扩展
openstack extension show &lt;extension&gt;</pre>
<div class="blog_h3"><span class="graybg">endpoint</span></div>
<p>管理服务的API端点</p>
<pre class="crayon-plain-tag"># 关联项目到端点
openstack endpoint add project
    [--project-domain &lt;project-domain&gt;]
    &lt;endpoint&gt;
    &lt;project&gt;
openstack endpoint remove project
    [--project-domain &lt;project-domain&gt;]
    &lt;endpoint&gt;
    &lt;project&gt;

# 创建新的端点
openstack endpoint create
    # 所属的区域
    [--region &lt;region-id&gt;]
    [--enable | --disable]
    # 端点所属的服务
    &lt;service&gt;
    # admin, public 还是 internal
    &lt;interface&gt;
    &lt;url&gt;

openstack endpoint delete &lt;endpoint-id&gt; [&lt;endpoint-id&gt; ...]

openstack endpoint list
    [--sort-column SORT_COLUMN]
    [--service &lt;service&gt;]
    [--interface &lt;interface&gt;]
    [--region &lt;region-id&gt;]
    [--endpoint &lt;endpoint-group&gt; | --project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]

openstack endpoint set
    [--region &lt;region-id&gt;]
    [--interface &lt;interface&gt;]
    [--url &lt;url&gt;]
    [--service &lt;service&gt;]
    [--enable | --disable]
    &lt;endpoint-id&gt;

openstack endpoint show &lt;endpoint&gt;</pre>
<div class="blog_h3"><span class="graybg">endpoint group</span></div>
<p>一组端点，可以一起关联到项目。</p>
<pre class="crayon-plain-tag"># 关联端点组到项目
openstack endpoint group add project
    [--project-domain &lt;project-domain&gt;]
    &lt;endpoint-group&gt;
    &lt;project&gt;
openstack endpoint group remove project
    [--project-domain &lt;project-domain&gt;]
    &lt;endpoint-group&gt;
    &lt;project&gt;


# 创建端点组
openstack endpoint group create
    [--description DESCRIPTION]
    &lt;name&gt;
    &lt;filename&gt;
openstack endpoint group delete &lt;endpoint-group&gt; [&lt;endpoint-group&gt; ...]
openstack endpoint group set
    [--name &lt;name&gt;]
    [--filters &lt;filename&gt;]
    [--description &lt;description&gt;]
    &lt;endpoint-group&gt;
openstack endpoint group list
    [--sort-column SORT_COLUMN]
    [--endpointgroup &lt;endpoint-group&gt; | --project &lt;project&gt;]
    [--domain &lt;domain&gt;]
openstack endpoint group show &lt;endpointgroup&gt; </pre>
<div class="blog_h3"><span class="graybg">flavor</span></div>
<p>表示一种虚拟机的规格。</p>
<pre class="crayon-plain-tag">openstack flavor create
    [--id &lt;id&gt;]
    # 内存大小，MB
    [--ram &lt;size-mb&gt;]
    # 磁盘大小，GB
    [--disk &lt;size-gb&gt;]
    # 临时磁盘大小
    [--ephemeral &lt;size-gb&gt;]
    # 交换分区大小
    [--swap &lt;size-mb&gt;]
    # VCPU数量，默认1
    [--vcpus &lt;vcpus&gt;]
    # RX/TX 因子，默认1.0
    [--rxtx-factor &lt;factor&gt;]
    # 是否可以被其它项目使用
    [--public | --private]
    [--property &lt;key=value&gt;]
    # 所属项目
    [--project &lt;project&gt;]
    [--description &lt;description&gt;]
    [--project-domain &lt;project-domain&gt;]
    # 传统的名字是  XX.SIZE_NAME 格式，现在已经没有要求。不排除某些工具依赖于这种名称格式
    &lt;flavor-name&gt;

openstack flavor list
    [--sort-column SORT_COLUMN]
    [--public | --private | --all]
    [--long]
    [--marker &lt;flavor-id&gt;]
    [--limit &lt;num-flavors&gt;]

openstack flavor set
    [--no-property]
    [--property &lt;key=value&gt;]
    [--project &lt;project&gt;]
    [--description &lt;description&gt;]
    [--project-domain &lt;project-domain&gt;]
    &lt;flavor&gt;
openstack flavor unset
    [--property &lt;key&gt;]
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    &lt;flavor&gt;

openstack flavor show &lt;flavor&gt;


# 创建一个Flavor
openstack flavor create FLAVOR_NAME --id FLAVOR_ID \
    --ram RAM_IN_MB --disk ROOT_DISK_IN_GB --vcpus NUMBER_OF_VCPUS

# 创建Flavor并分配给一个租户
openstack flavor create --public m1.extra_tiny --id auto \
    --ram 256 --disk 0 --vcpus 1
openstack flavor set --project PROJECT_ID m1.extra_tiny



# 列出所有flavor
openstack flavor create --id 0 --vcpus 1 --ram 64 --disk 1 m1.nano
openstack flavor create --id 1 --vcpus 1 --ram 512 --disk 8 m1.tiny
openstack flavor create --id 2 --vcpus 1 --ram 2048 --disk 32 m1.small
openstack flavor create --id 3 --vcpus 2 --ram 4096 --disk 64 m1.medium
openstack flavor create --id 4 --vcpus 4 --ram 8192 --disk 128 m1.large
openstack flavor create --id 5 --vcpus 8 --ram 16384 --disk 256 m1.xlarge

openstack flavor list
# +----+-----------+-------+------+-----------+-------+-----------+
# | ID | Name      |   RAM | Disk | Ephemeral | VCPUs | Is Public |
# +----+-----------+-------+------+-----------+-------+-----------+
# | 0  | m1.nano   |    64 |    1 |         0 |     1 | True      |
# | 1  | m1.tiny   |   512 |    8 |         0 |     1 | True      |
# | 2  | m1.small  |  2048 |   32 |         0 |     1 | True      |
# | 3  | m1.medium |  4096 |   64 |         0 |     2 | True      |
# | 4  | m1.large  |  8192 |  128 |         0 |     4 | True      |
# | 5  | m1.xlarge | 16384 |  256 |         0 |     8 | True      |
# +----+-----------+-------+------+-----------+-------+-----------+</pre>
<div class="blog_h3"><span class="graybg">image</span></div>
<p>管理镜像。</p>
<pre class="crayon-plain-tag"># 允许项目访问镜像
openstack image add project
    [--project-domain &lt;project-domain&gt;]
    &lt;image&gt;
    &lt;project&gt;
openstack image remove project
    [--project-domain &lt;project-domain&gt;]
    &lt;image&gt;
    &lt;project&gt;

# 创建镜像
openstack image create
    [--id &lt;id&gt;]
    # 镜像容器格式：ami, ari, aki, bare, docker, ova, ovf。默认bare
    [--container-format &lt;container-format&gt;]
    # 镜像磁盘格式：ami, ari, aki, vhd, vmdk, raw, qcow2, vhdx, vdi, iso, ploop。默认raw
    [--disk-format &lt;disk-format&gt;]
    # 启动镜像需要的最小磁盘尺寸
    [--min-disk &lt;disk-gb&gt;]
    # 启动镜像需要的最小内存
    [--min-ram &lt;ram-mb&gt;]
    # 镜像文件       从卷生成镜像
    [--file &lt;file&gt; | --volume &lt;volume&gt;]
    # 从卷生成镜像时，即便卷正在使用，也强制生成镜像
    [--force]
    # 使用本地私钥签名镜像
    [--sign-key-path &lt;sign-key-path&gt;]
    # 用于镜像签名校验的，位于key manager中的certificate的UUID
    [--sign-cert-id &lt;sign-cert-id&gt;]
    # 保护镜像防止被删除
    [--protected | --unprotected]
    # 公共      项目私有    可被社区使用    共享
    [--public | --private | --community | --shared]
    [--property &lt;key=value&gt;]
    [--tag &lt;tag&gt;]
    [--project &lt;project&gt;]
    [--import]
    [--project-domain &lt;project-domain&gt;]
    &lt;image-name&gt;

# 设置镜像属性
openstack image set
    [--name &lt;name&gt;]
    [--min-disk &lt;disk-gb&gt;]
    [--min-ram &lt;ram-mb&gt;]
    [--container-format &lt;container-format&gt;]
    [--disk-format &lt;disk-format&gt;]
    [--protected | --unprotected]
    [--public | --private | --community | --shared]
    [--property &lt;key=value&gt;]
    [--tag &lt;tag&gt;]
    [--architecture &lt;architecture&gt;]
    [--instance-id &lt;instance-id&gt;]
    [--kernel-id &lt;kernel-id&gt;]
    [--os-distro &lt;os-distro&gt;]
    [--os-version &lt;os-version&gt;]
    [--ramdisk-id &lt;ramdisk-id&gt;]
    [--deactivate | --activate]
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    [--accept | --reject | --pending]
    &lt;image&gt;
openstack image unset [--tag &lt;tag&gt;] [--property &lt;property-key&gt;] &lt;image&gt;

# 删除镜像
openstack image delete &lt;image&gt; [&lt;image&gt; ...]

# 将镜像保存到本地
openstack image save [--file &lt;filename&gt;] &lt;image&gt;

# 列出镜像
openstack image member list
    [--sort-column SORT_COLUMN]
    [--project-domain &lt;project-domain&gt;]
    &lt;image&gt;

# 显示镜像信息
openstack image show [--human-readable] &lt;image&gt;


# 示例
# 上传镜像
openstack image create --public --disk-format qcow2 --container-format bare \
  --file cirros-0.5.1-x86_64-disk.img cirros-0.5.1-amd64
# 将卷上传为镜像，上传的QCOW2镜像，会自动shrink
# 注意，默认情况下只有Available状态的卷能够上传为镜像，In-use的用--force也不行
# Force upload to image is disabled, Force option will be ignored.
openstack image create --volume 840e9e25-192c-401b-83f5-898fd82839c4 --force centos8-amd64-prepared</pre>
<div class="blog_h3"><span class="graybg">compute agent</span></div>
<p>计算代理是和Hypervisor相关的，且仅仅被 XenAPI hypervisor driver支持。</p>
<div class="blog_h3"><span class="graybg">compute service</span></div>
<p>Nova相关的服务。</p>
<pre class="crayon-plain-tag">openstack compute service delete &lt;service&gt; [&lt;service&gt; ...]

openstack compute service list
    [--sort-column SORT_COLUMN]
    [--host &lt;host&gt;]
    [--service &lt;service&gt;]
    [--long]

openstack compute service set
    [--enable | --disable]
    [--disable-reason &lt;reason&gt;]
    [--up | --down]
    &lt;host&gt;
    &lt;service&gt;</pre>
<div class="blog_h3"><span class="graybg">console log</span></div>
<p>显示虚拟机的控制台日志：</p>
<pre class="crayon-plain-tag">openstack console log show [--lines &lt;num-lines&gt;] &lt;server&gt;</pre>
<div class="blog_h3"><span class="graybg">console url</span></div>
<p>打印各种类型的控制台URL：</p>
<pre class="crayon-plain-tag">openstack console url show
    [--novnc | --xvpvnc | --spice | --rdp | --serial | --mks]
    &lt;server&gt; </pre>
<div class="blog_h3"><span class="graybg">server group</span></div>
<p>基于某种策略对服务器进行分组。</p>
<pre class="crayon-plain-tag">openstack server group create [--policy &lt;policy&gt;] &lt;name&gt;

openstack server group delete &lt;server-group&gt; [&lt;server-group&gt; ...]

openstack server group list
    [--sort-column SORT_COLUMN]
    [--all-projects]
    [--long]

openstack server group show &lt;server-group&gt;</pre>
<div class="blog_h3"><span class="graybg"><a id="server-create"></a>server create</span></div>
<p>创建一个实例（虚拟机，也叫服务器server） </p>
<pre class="crayon-plain-tag">openstack server create
    # --image 从镜像创建服务器的启动磁盘
    # --volume 将卷作为服务器的启动磁盘，会自动创建一个块设备，映射boot index为0
    #          在很多Hypervisor（例如libvirt/kvm）这个磁盘将是vda
    #          不要使用 –block-device-mapping  选项来为此磁盘创建重复的映射
    (--image &lt;image&gt; | --image-property &lt;key=value&gt; | --volume &lt;volume&gt;)
    # 服务器的密码
    [--password &lt;password&gt;]
    # 服务器的风格
    --flavor &lt;flavor&gt;
    # 加入安全组，可以指定多个
    [--security-group &lt;security-group&gt;]
    # 注入到此服务器的OpenSSH公钥
    [--key-name &lt;key-name&gt;]
    # 设置属性
    [--property &lt;key=value&gt;]
    # 在启动之前，注入到镜像中的文件。可以指定多个
    [--file &lt;dest-filename=source-filename&gt;]
    # 从metadata服务器来serve的用户数据文件。用于配置新实例
    [--user-data &lt;user-data&gt;]
    [--description &lt;description&gt;]
    # 在哪个AZ中创建此服务器，可以指定：
    #   &lt;zone-name&gt;:&lt;host-name&gt;:&lt;node-name&gt;
    #   &lt;zone-name&gt;::&lt;node-name&gt;
    #   &lt;zone-name&gt;:&lt;host-name&gt;
    #   &lt;zone-name&gt;
    [--availability-zone &lt;zone-name&gt;]
    # 指定在某个宿主机上创建实例
    [--host &lt;host&gt;]
    [--hypervisor-hostname &lt;hypervisor-hostname&gt;]
    # 和 --image 或 --image-property 一起使用时，该选项自动创建boot index 为0的块设备
    # 并且告知计算服务，从镜像创建卷+卷的大小（GB）。此卷在实例销毁后不会删除
    # 不能和 --volume 联用
    [--boot-from-volume &lt;volume-size&gt;]
    # 在服务器上创建额外的块设备，格式：
    #   &lt;dev-name&gt;=&lt;id&gt;:&lt;type&gt;:&lt;size(GB)&gt;:&lt;delete-on-terminate&gt;
    #     dev-name 为块设备名称，例如vdb xvdc
    #     id 为卷、卷快照、镜像的名字或ID
    #     type：volume, snapshot 或 image。默认volume
    #     size：卷的大小
    #     delete-on-terminate：true或false
    [--block-device-mapping &lt;dev-name=mapping&gt;]
    # 在服务器上创建NIC。要创建多个NIC，则指定多次
    #     net-id和port-id互斥，不能同时指定
    #     v4-fixed-ip 此NIC的IPv4固定IP地址
    #     v6-fixed-ip 此NIC的IPv6固定IP地址
    #     auto 由计算服务自动分配一个网络。不能和其它参数一起使用
    #     none 不连接到网络。不能和其它参数一起使用
    [--nic &lt;net-id=net-uuid,v4-fixed-ip=ip-addr,v6-fixed-ip=ip-addr,port-id=port-uuid,auto,none&gt;]
    # 创建一个NIC，并且连接到该网络。可以指定多次，以创建多个NIC并连接到多个网络
    [--network &lt;network&gt;]
    # 创建一个NIC，并且连接到该端口。可以指定多次，以创建多个NIC并连接到多个端口
    [--port &lt;port&gt;]
    # 提供给nova-scheduler的提示信息
    [--hint &lt;key=value&gt;]
    # 启用config drive
    [--use-config-drive | --no-config-drive | --config-drive &lt;config-drive-volume&gt;|True]
    # 启动的实例数量
    [--min &lt;count&gt;]
    [--max &lt;count&gt;]
    # 等待实例构建完成
    [--wait]
    [--tag &lt;tag&gt;]
    # 实例的名字
    &lt;server-name&gt;


# 从镜像启动虚拟机，并挂载一个非启动磁盘
## 创建非启动磁盘
openstack volume create --size 8 my-volume
## 创建虚拟机         从镜像创建
nova boot --flavor 2 --image 98901246-af91-43d8-b5e6-a4506aa8f369 \
# 添加块设备      源是卷        卷的ID                                   挂载为卷    虚拟机删除后保留卷  
  --block-device source=volume,id=d620d971-b160-4c4e-8652-2513d74e2080,dest=volume,shutdown=preserve \
  myInstanceWithVolume


# 从SOURCE创建启动卷，并从该卷启动虚拟机
# SOURCE： volume, snapshot, image, 或者 blank
# DEST：volume或local
nova boot --flavor FLAVOR --block-device \
  source=SOURCE,id=ID,dest=DEST,size=SIZE,shutdown=PRESERVE,bootindex=INDEX  NAME

# 从镜像创建一个可启动卷。如果指定--image参数，则Cinder自动将卷标记为可启动的
openstack volume create --image IMAGE_ID --size SIZE_IN_GB bootable_volume


# 挂载Swap或者临时磁盘到虚拟机
nova boot --flavor FLAVOR --image IMAGE_ID --swap 512  --ephemeral size=2 NAME</pre>
<div class="blog_h3"><span class="graybg">server add fixed ip</span></div>
<p>为服务器添加固定IP地址：</p>
<pre class="crayon-plain-tag">openstack server add fixed ip
    # 请求的固定IP地址
    [--fixed-ip-address &lt;ip-address&gt;]
    [--tag &lt;tag&gt;]
    &lt;server&gt;
    &lt;network&gt;

openstack server remove fixed ip &lt;server&gt; &lt;ip-address&gt;</pre>
<div class="blog_h3"><span class="graybg">server add floating ip</span></div>
<p>为服务器添加浮动IP地址：</p>
<pre class="crayon-plain-tag">openstack server add floating ip
    # 和此浮动IP地址关联的固定IP地址，使用第一个具有此固定IP地址的、此服务器上的port
    [--fixed-ip-address &lt;ip-address&gt;]
    &lt;server&gt;
    # 分配给上述第一个服务器port的浮动IP地址
    &lt;ip-address&gt;

openstack server remove floating ip &lt;server&gt; &lt;ip-address&gt;</pre>
<div class="blog_h3"><span class="graybg">server add network</span></div>
<p>将服务器连接到某个网络 </p>
<pre class="crayon-plain-tag">openstack server add network [--tag &lt;tag&gt;] &lt;server&gt; &lt;network&gt;

openstack server remove network &lt;server&gt; &lt;network&gt;</pre>
<div class="blog_h3"><span class="graybg">server add port </span></div>
<p>将某个端口连接到服务器</p>
<pre class="crayon-plain-tag">openstack server add port [--tag &lt;tag&gt;] &lt;server&gt; &lt;port&gt;

openstack server remove port &lt;server&gt; &lt;port&gt;</pre>
<div class="blog_h3"><span class="graybg">server add security group</span></div>
<p>将服务器添加到安全组</p>
<pre class="crayon-plain-tag">openstack server add security group &lt;server&gt; &lt;group&gt;

openstack server remove security group &lt;server&gt; &lt;group&gt;</pre>
<div class="blog_h3"><span class="graybg">server add volume </span></div>
<p>挂载（Attach）一个卷给服务器</p>
<pre class="crayon-plain-tag">openstack server add volume
    # 服务器上的内部设备名
    [--device &lt;device&gt;]
    [--tag &lt;tag&gt;]
    # 如果服务器被销毁，此卷是否被删除
    [--enable-delete-on-termination | --disable-delete-on-termination]
    &lt;server&gt;
    &lt;volume&gt;

openstack server remove volume &lt;server&gt; &lt;volume&gt;

# 示例
openstack server add volume cirros-amd64 cirros-amd64-diskb</pre>
<div class="blog_h3"><span class="graybg">server migration list</span></div>
<p>服务器迁移，就是将一台宿主机上的实例，转移到另外一台上运行。</p>
<p>OpenStack支持四种迁移模式：热迁移、冷迁移、升降配（resize）、重建（evacuation）</p>
<pre class="crayon-plain-tag"># 显示迁移历史的列表
openstack server migration list
    [--sort-column SORT_COLUMN]
    [--server &lt;server&gt;]
    [--host &lt;host&gt;]
    [--status &lt;status&gt;]
    [--type &lt;type&gt;]
    [--marker &lt;marker&gt;]
    [--limit &lt;limit&gt;]
    [--changes-since &lt;changes-since&gt;]
    [--changes-before &lt;changes-before&gt;]
    [--project &lt;project&gt;]
    [--user &lt;user&gt;]</pre>
<div class="blog_h3"><span class="graybg">server resize</span></div>
<p>扩/缩容服务器为新的flavor。实现方式是：</p>
<ol>
<li>创就一个新的服务器</li>
<li>复制文件到新服务器</li>
</ol>
<p>扩/缩容操作分为两步完成：第一步迁移，第二步确认</p>
<pre class="crayon-plain-tag">openstack server resize
    [--flavor &lt;flavor&gt; | --confirm | --revert]
    [--wait]
    &lt;server&gt;</pre>
<div class="blog_h3"><span class="graybg">server resize confirm</span></div>
<pre class="crayon-plain-tag">openstack server resize confirm &lt;server&gt;</pre>
<div class="blog_h3"><span class="graybg">server resize revert</span></div>
<pre class="crayon-plain-tag">openstack server resize revert &lt;server&gt;</pre>
<div class="blog_h3"><span class="graybg">server migrate</span></div>
<p>将服务器迁移到另外一个宿主机上。</p>
<p>迁移操作是基于resize操作实现的：</p>
<ol>
<li>创建一个新的实例，使用相同的flavor</li>
<li>从原始磁盘上拷贝内容到新磁盘</li>
</ol>
<p>和resize一样，迁移操作是分两步完成的：</p>
<ol>
<li>执行上述两步的迁移操作</li>
<li>让用户确认，迁移是否成功并移除酒实例，还是执行revert操作 —— 删除新实例并重启老的</li>
</ol>
<pre class="crayon-plain-tag">openstack server migrate
    # 不宕机迁移
    [--live-migration]
    # 目标主机 
    [ --host &lt;hostname&gt;]
    [--shared-migration | --block-migration]
    [--disk-overcommit | --no-disk-overcommit]
    [--wait]
    &lt;server&gt;</pre>
<div class="blog_h3"><span class="graybg">server migrate confirm</span></div>
<p>确认迁移</p>
<pre class="crayon-plain-tag">openstack server migrate confirm &lt;server&gt;</pre>
<div class="blog_h3"><span class="graybg">server migrate revert </span></div>
<p>撤销迁移 </p>
<pre class="crayon-plain-tag">openstack server migrate revert &lt;server&gt;</pre>
<div class="blog_h3"><span class="graybg">server evacuate</span></div>
<p>将服务器在另外一个宿主机上重建。这个命令的使用场景是：<span style="background-color: #c0c0c0;">实例已经运行，但是后来它所在的宿主机宕掉了</span>。 也就是说，仅当管理此实例的compute service宕机了，才可以使用此命令。</p>
<p>如果服务器实例使用临时的（ephemeral）root磁盘，此<span style="background-color: #c0c0c0;">磁盘位于非共享存储上，则使用原始的glance镜像重建服务器。连接到原实例的port、挂载的卷被保留</span>。</p>
<p><span style="background-color: #c0c0c0;">如果服务器实例从volume启动，或者跟磁盘位于共享存储上，则新建实例会重用</span>此启动盘。</p>
<div class="blog_h3"><span class="graybg">server pause</span></div>
<p>暂停服务器，状态保存在内存中</p>
<pre class="crayon-plain-tag">openstack server pause &lt;server&gt; [&lt;server&gt; ...]</pre>
<div class="blog_h3"><span class="graybg">server unpause</span></div>
<p>取消暂停服务器</p>
<pre class="crayon-plain-tag">openstack server unpause &lt;server&gt; [&lt;server&gt; ...]</pre>
<div class="blog_h3"><span class="graybg">server suspend</span></div>
<p>暂停服务器，状态保存在磁盘中</p>
<pre class="crayon-plain-tag">openstack server suspend &lt;server&gt; [&lt;server&gt; ...]</pre>
<div class="blog_h3"><span class="graybg">server resume</span></div>
<p>从暂停中恢复</p>
<pre class="crayon-plain-tag">openstack server resume &lt;server&gt; [&lt;server&gt; ...]</pre>
<div class="blog_h3"><span class="graybg">server restore</span></div>
<p>回退状态为软删除的服务器</p>
<pre class="crayon-plain-tag">openstack server restore &lt;server&gt; [&lt;server&gt; ...] </pre>
<div class="blog_h3"><span class="graybg">server reboot</span></div>
<p>重启服务器</p>
<pre class="crayon-plain-tag">#                       强行立即重启
openstack server reboot [--hard | --soft] [--wait] &lt;server&gt;</pre>
<div class="blog_h3"><span class="graybg">server start</span></div>
<p>启动服务器</p>
<pre class="crayon-plain-tag">openstack server start &lt;server&gt; [&lt;server&gt; ...]</pre>
<div class="blog_h3"><span class="graybg">server stop</span></div>
<p>停止服务器</p>
<pre class="crayon-plain-tag">openstack server stop &lt;server&gt; [&lt;server&gt; ...]</pre>
<div class="blog_h3"><span class="graybg">server rebuild</span></div>
<p>重建服务器</p>
<pre class="crayon-plain-tag">openstack server rebuild
    [--image &lt;image&gt;]
    [--password &lt;password&gt;]
    [--property &lt;key=value&gt;]
    [--description &lt;description&gt;]
    [--key-name &lt;key-name&gt; | --key-unset]
    [--wait]
    &lt;server&gt; </pre>
<div class="blog_h3"><span class="graybg">server rescue</span></div>
<p>让服务器进入rescue模式</p>
<pre class="crayon-plain-tag">openstack server rescue
    [--image &lt;image&gt;]
    [--password &lt;password&gt;]
    &lt;server&gt; </pre>
<div class="blog_h3"><span class="graybg">server unrescue</span></div>
<p>从rescue模式恢复：</p>
<pre class="crayon-plain-tag">openstack server unrescue &lt;server&gt;</pre>
<div class="blog_h3"><span class="graybg">server shelve</span></div>
<p>将服务器实例作为镜像，保存在glance中，然后在宿主机上删除此服务器 </p>
<pre class="crayon-plain-tag">openstack server shelve &lt;server&gt; [&lt;server&gt; ...] </pre>
<div class="blog_h3"><span class="graybg">server unshelve</span></div>
<p>将shelve的实例恢复</p>
<pre class="crayon-plain-tag">openstack server unshelve
    [--availability-zone AVAILABILITY_ZONE]
    &lt;server&gt;
    [&lt;server&gt; ...] </pre>
<div class="blog_h3"><span class="graybg">server ssh</span></div>
<p>通过SSH连接到服务器</p>
<pre class="crayon-plain-tag">openstack server ssh
    [--login &lt;login-name&gt;]
    [--port &lt;port&gt;]
    [--identity &lt;keyfile&gt;]
    [--option &lt;config-options&gt;]
    [-4 | -6]
    [--public | --private | --address-type &lt;address-type&gt;]
    &lt;server&gt;


# 自动使用当前用户的SSH key
openstack server ssh --private -4 --login cirros cirros-amd64-0</pre>
<div class="blog_h3"><span class="graybg">server dump create</span></div>
<p>创建服务器的Dump文件。这会触发一个crash dump（例如Linux的kdump） </p>
<pre class="crayon-plain-tag">openstack server dump create &lt;server&gt; [&lt;server&gt; ...]</pre>
<div class="blog_h3"><span class="graybg">server delete</span></div>
<p>（软）删除服务器</p>
<pre class="crayon-plain-tag">openstack server delete [--wait] &lt;server&gt; [&lt;server&gt; ...]</pre>
<div class="blog_h3"><span class="graybg">server set</span></div>
<p>设置服务器属性</p>
<pre class="crayon-plain-tag">openstack server set
    [--name &lt;new-name&gt;]
    [--root-password]
    [--property &lt;key=value&gt;]
    [--state &lt;state&gt;]
    [--description &lt;description&gt;]
    [--tag &lt;tag&gt;]
    &lt;server&gt;

openstack server unset
    [--property &lt;key&gt;]
    [--description]
    [--tag &lt;tag&gt;]
    &lt;server&gt;</pre>
<div class="blog_h3"><span class="graybg">server lock</span></div>
<p>锁定实例，这样非admin用户就不能对它进行任何操作。</p>
<pre class="crayon-plain-tag">openstack server lock [--reason &lt;reason&gt;] &lt;server&gt; [&lt;server&gt; ...]</pre>
<div class="blog_h3"><span class="graybg">server unlock</span></div>
<p>解锁服务器</p>
<pre class="crayon-plain-tag">openstack server unpause &lt;server&gt; [&lt;server&gt; ...] </pre>
<div class="blog_h3"><span class="graybg">server list</span></div>
<pre class="crayon-plain-tag">openstack server list
    [--sort-column SORT_COLUMN]
    [--availability-zone &lt;availability-zone&gt;]
    [--reservation-id &lt;reservation-id&gt;]
    [--ip &lt;ip-address-regex&gt;]
    [--ip6 &lt;ip-address-regex&gt;]
    [--name &lt;name-regex&gt;]
    [--instance-name &lt;server-name&gt;]
    [--status &lt;status&gt;]
    [--flavor &lt;flavor&gt;]
    [--image &lt;image&gt;]
    [--host &lt;hostname&gt;]
    [--all-projects]
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    [--user &lt;user&gt;]
    [--user-domain &lt;user-domain&gt;]
    [--long]
    [-n | --name-lookup-one-by-one]
    [--marker &lt;server&gt;]
    [--limit &lt;num-servers&gt;]
    [--deleted]
    [--changes-before &lt;changes-before&gt;]
    [--changes-since &lt;changes-since&gt;]
    [--locked | --unlocked]
    [--tags &lt;tag&gt;]
    [--not-tags &lt;tag&gt;]</pre>
<div class="blog_h3"><span class="graybg">server show</span></div>
<pre class="crayon-plain-tag">#                     显示诊断信息
openstack server show [--diagnostics] &lt;server&gt;</pre>
<div class="blog_h3"><span class="graybg">server backup</span></div>
<p>备份一个运行中的服务器实例，将其磁盘保存为镜像，存放在Glance中。</p>
<pre class="crayon-plain-tag"># 创建备份
openstack server backup create
    # 备份镜像的名字，默认为服务器名
    [--name &lt;image-name&gt;]
    # 填充镜像的backup_type字段
    [--type &lt;backup-type&gt;]
    # 保存的备份数量
    [--rotate &lt;count&gt;]
    [--wait]
    &lt;server&gt;</pre>
<div class="blog_h3"><span class="graybg">server image</span></div>
<p>从运行中的实例创建磁盘镜像，并存放到Glance中。</p>
<pre class="crayon-plain-tag">openstack server image create [--name &lt;image-name&gt;] [--wait] &lt;server&gt; </pre>
<div class="blog_h3"><span class="graybg">server event</span></div>
<p>服务器事件，记录了针对服务器的各种操作。事件由操作类型（create, delete, reboot ...）+ 操作结果（success, error） + 开始/结束时间组成。</p>
<pre class="crayon-plain-tag"># 事件列表
openstack server event list
    [--sort-column SORT_COLUMN]
    [--long]
    &lt;server&gt;

# 显示事件
openstack server event show &lt;server&gt; &lt;request-id&gt;</pre>
<div class="blog_h3"><span class="graybg">network service provider</span></div>
<p>一个网络服务提供者，表示一个特定的、实现了网络服务的驱动：</p>
<pre class="crayon-plain-tag">openstack network service provider list [--sort-column SORT_COLUMN]</pre>
<div class="blog_h3"><span class="graybg">network</span></div>
<p>所谓网络，是指一个独立的（isolated）的L2网段。OpenStack具有两种类型的网络：</p>
<ol>
<li>project：完全隔离的、不和其它项目共享的网络。自服务网络</li>
<li>provider：映射到现有的、数据中心中的物理网络，为server或其它资源提供外部网络访问</li>
</ol>
<p>仅仅管理员可以创建provider网络</p>
<pre class="crayon-plain-tag">openstack network create
    # 是否允许跨项目共享
    [--share | --no-share]
    # 是否启用网络
    [--enable | --disable]
    # 所属项目
    [--project &lt;project&gt;]
    [--description &lt;description&gt;]
    # MTU设置
    [--mtu &lt;mtu&gt;]
    [--project-domain &lt;project-domain&gt;]
    # 在什么AZ中创建此网络。需要Network Availability Zone扩展
    # 此选项可以指定多次，表示网络跨越多个AZ
    [--availability-zone-hint &lt;availability-zone&gt;]
    # 是否启用端口安全，如果指定，则此网络上创建的端口自动应用安全设置
    [--enable-port-security | --disable-port-security]
    # 是否外部网络，如果是，则需要external-net扩展
    [--external | --internal]
    # 是否作为默认外部网络
    [--default | --no-default]
    # 应用到此网络的QoS策略
    [--qos-policy &lt;qos-policy&gt;]
    # 指定此网络是VLAN透明的
    [--transparent-vlan | --no-transparent-vlan]
    # 此虚拟网络所基于其实现的物理机制（physical mechanism）
    #   例如  flat, geneve, gre, local, vlan, vxlan
    [--provider-network-type &lt;provider-network-type&gt;]
    # 此虚拟网络所基于的物理网络的名字
    [--provider-physical-network &lt;provider-physical-network&gt;]
    # 对于VLAN网络，指定VLAN ID
    # 对于GENEVE/GRE/VXLAN，指定Tunnel ID
    [--provider-segment &lt;provider-segment&gt;]
    # 此网络的DNS domain，需要DNS integration扩展
    [--dns-domain &lt;dns-domain&gt;]
    [--tag &lt;tag&gt; | --no-tag]
    # IPv4的CIDR
    --subnet &lt;subnet&gt;
    &lt;name&gt;

openstack network delete &lt;network&gt; [&lt;network&gt; ...]

openstack network list
    [--sort-column SORT_COLUMN]
    [--external | --internal]
    [--long]
    [--name &lt;name&gt;]
    [--enable | --disable]
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    [--share | --no-share]
    [--status &lt;status&gt;]
    [--provider-network-type &lt;provider-network-type&gt;]
    [--provider-physical-network &lt;provider-physical-network&gt;]
    [--provider-segment &lt;provider-segment&gt;]
    [--agent &lt;agent-id&gt;]
    [--tags &lt;tag&gt;[,&lt;tag&gt;,...]]
    [--any-tags &lt;tag&gt;[,&lt;tag&gt;,...]]
    [--not-tags &lt;tag&gt;[,&lt;tag&gt;,...]]
    [--not-any-tags &lt;tag&gt;[,&lt;tag&gt;,...]]

openstack network set
    [--name &lt;name&gt;]
    [--enable | --disable]
    [--share | --no-share]
    [--description &lt;description]
    [--mtu &lt;mtu]
    [--enable-port-security | --disable-port-security]
    [--external | --internal]
    [--default | --no-default]
    [--qos-policy &lt;qos-policy&gt; | --no-qos-policy]
    [--tag &lt;tag&gt;]
    [--no-tag]
    [--provider-network-type &lt;provider-network-type&gt;]
    [--provider-physical-network &lt;provider-physical-network&gt;]
    [--provider-segment &lt;provider-segment&gt;]
    [--dns-domain &lt;dns-domain&gt;]
    &lt;network&gt;
openstack network unset [--tag &lt;tag&gt; | --all-tag] &lt;network&gt;

openstack network show &lt;network&gt;</pre>
<div class="blog_h3"><span class="graybg">subnet</span></div>
<p>子网是一段IP地址以及关联的配置状态。当新的Port接入到网络中时，子网用于向Port分配IP地址。</p>
<pre class="crayon-plain-tag">openstack subnet create
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    # --subnet-pool 该子网从此池中获得自己的CIDR
    # --use-default-subnet-pool 使用默认的子网池
    [--subnet-pool &lt;subnet-pool&gt; | --use-prefix-delegation USE_PREFIX_DELEGATION | --use-default-subnet-pool]
    # 从子网池中分配子网时，前缀的长度
    [--prefix-length &lt;prefix-length&gt;]
    # CIDR格式的子网IP地址范围
    [--subnet-range &lt;subnet-range&gt;]
    # 是否启用DHCP
    [--dhcp | --no-dhcp]
    # 是否在DNS中发布固定IP
    [--dns-publish-fixed-ip | --no-dns-publish-fixed-ip]
    # 指定子网的网关。三种形式：
    #   &lt;ip-address&gt; 将指定的IP地址作为网关
    #   auto 自动在子网内部选择网关地址，默认
    #   none 不使用网关
    [--gateway &lt;gateway&gt;]
    # 子网的IP版本，如果使用了subnet pool，则IP版本取决于子网池，该选项被忽略
    [--ip-version {4,6}]
    # IPv6 RA（Router Advertisement）模式
    [--ipv6-ra-mode {dhcpv6-stateful,dhcpv6-stateless,slaac}]
    # IPv6地址模式
    [--ipv6-address-mode {dhcpv6-stateful,dhcpv6-stateless,slaac}]
    # 关联到此子网的网段，网段属于某个网络
    [--network-segment &lt;network-segment&gt;]
    # 此子网所属的网络
    --network &lt;network&gt;
    [--description &lt;description&gt;]
    # 此子网的DHCP自动分配IP地址的范围。可以指定多个
    [--allocation-pool start=&lt;ip-address&gt;,end=&lt;ip-address&gt;]
    # 此子网使用的DNS服务器
    [--dns-nameserver &lt;dns-nameserver&gt;]
    # 为子网添加额外的路由，示例 destination=10.10.0.0/16,gateway=192.168.71.254 网关为下一跳地址
    [--host-route destination=&lt;subnet&gt;,gateway=&lt;ip-address&gt;]
    # 子网的服务类型，必须指定为有效的、某个网络端口的device owner，例如network:floatingip_agent_gateway
    # 可以指定多个，以支持多个服务类型
    [--service-type &lt;service-type&gt;]
    [--tag &lt;tag&gt; | --no-tag]
    &lt;name&gt;

openstack subnet delete &lt;subnet&gt; [&lt;subnet&gt; ...]

openstack subnet list
    [--sort-column SORT_COLUMN]
    [--long]
    [--ip-version &lt;ip-version&gt;]
    [--dhcp | --no-dhcp]
    [--service-type &lt;service-type&gt;]
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    [--network &lt;network&gt;]
    [--gateway &lt;gateway&gt;]
    [--name &lt;name&gt;]
    [--subnet-range &lt;subnet-range&gt;]
    [--tags &lt;tag&gt;[,&lt;tag&gt;,...]]
    [--any-tags &lt;tag&gt;[,&lt;tag&gt;,...]]
    [--not-tags &lt;tag&gt;[,&lt;tag&gt;,...]]
    [--not-any-tags &lt;tag&gt;[,&lt;tag&gt;,...]]
openstack subnet show &lt;subnet&gt;

openstack subnet set
    [--name &lt;name&gt;]
    [--dhcp | --no-dhcp]
    [--dns-publish-fixed-ip | --no-dns-publish-fixed-ip]
    [--gateway &lt;gateway&gt;]
    [--network-segment &lt;network-segment&gt;]
    [--description &lt;description&gt;]
    [--tag &lt;tag&gt;]
    [--no-tag]
    [--allocation-pool start=&lt;ip-address&gt;,end=&lt;ip-address&gt;]
    [--no-allocation-pool]
    [--dns-nameserver &lt;dns-nameserver&gt;]
    [--no-dns-nameservers]
    [--host-route destination=&lt;subnet&gt;,gateway=&lt;ip-address&gt;]
    [--no-host-route]
    [--service-type &lt;service-type&gt;]
    &lt;subnet&gt;
openstack subnet unset
    [--allocation-pool start=&lt;ip-address&gt;,end=&lt;ip-address&gt;]
    [--dns-nameserver &lt;dns-nameserver&gt;]
    [--host-route destination=&lt;subnet&gt;,gateway=&lt;ip-address&gt;]
    [--service-type &lt;service-type&gt;]
    [--tag &lt;tag&gt; | --all-tag]
    &lt;subnet&gt;</pre>
<div class="blog_h3"><span class="graybg">subnet pool</span></div>
<p>子网池中包含若干CIDR格式的子网前缀，这些前缀用于分配给子网。</p>
<pre class="crayon-plain-tag"># 创建一个子网池
openstack subnet pool create
    # 子网池的前缀
    --pool-prefix &lt;pool-prefix&gt;
    # 子网池默认前缀长度
    [--default-prefix-length &lt;default-prefix-length&gt;]
    # 最小/最大前缀长度
    [--min-prefix-length &lt;min-prefix-length&gt;]
    [--max-prefix-length &lt;max-prefix-length&gt;]
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    # 关联到此池的address scope对象的名字或ID
    [--address-scope &lt;address-scope&gt;]
    # 是否作为默认子网池
    [--default | --no-default]
    [--share | --no-share]
    [--description &lt;description&gt;]
    # 默认的，每个项目的配额 —— 可以从池中分配的IP数量
    [--default-quota &lt;num-ip-addresses&gt;]
    [--tag &lt;tag&gt; | --no-tag]
    &lt;name&gt;

openstack subnet pool delete &lt;subnet-pool&gt; [&lt;subnet-pool&gt; ...]

openstack subnet pool list
    [--sort-column SORT_COLUMN]
    [--long]
    [--share | --no-share]
    [--default | --no-default]
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    [--name &lt;name&gt;]
    [--address-scope &lt;address-scope&gt;]
    [--tags &lt;tag&gt;[,&lt;tag&gt;,...]]
    [--any-tags &lt;tag&gt;[,&lt;tag&gt;,...]]
    [--not-tags &lt;tag&gt;[,&lt;tag&gt;,...]]
    [--not-any-tags &lt;tag&gt;[,&lt;tag&gt;,...]]
openstack subnet pool show &lt;subnet-pool&gt;

openstack subnet pool set
    [--name &lt;name&gt;]
    [--pool-prefix &lt;pool-prefix&gt;]
    [--default-prefix-length &lt;default-prefix-length&gt;]
    [--min-prefix-length &lt;min-prefix-length&gt;]
    [--max-prefix-length &lt;max-prefix-length&gt;]
    [--address-scope &lt;address-scope&gt; | --no-address-scope]
    [--default | --no-default]
    [--description &lt;description&gt;]
    [--default-quota &lt;num-ip-addresses&gt;]
    [--tag &lt;tag&gt;]
    [--no-tag]
    &lt;subnet-pool&gt;
openstack subnet pool unset [--tag &lt;tag&gt; | --all-tag] &lt;subnet-pool&gt;</pre>
<div class="blog_h3"><span class="graybg">address scope</span></div>
<p>表示IPv4或IPv6的地址范围，属于某个特定项目，可以被多个项目共享。</p>
<pre class="crayon-plain-tag"># 创建地址范围
openstack address scope create
    [--ip-version {4,6}]
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    # 是否可以在项目之间共享
    [--share | --no-share]
    &lt;name&gt;

openstack address scope delete &lt;address-scope&gt; [&lt;address-scope&gt; ...]

openstack address scope list
    [--sort-column SORT_COLUMN]
    [--name &lt;name&gt;]
    [--ip-version &lt;ip-version&gt;]
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    [--share | --no-share]

openstack address scope set
    [--name &lt;name&gt;]
    [--share | --no-share]
    &lt;address-scope&gt;

openstack address scope show &lt;address-scope&gt; </pre>
<div class="blog_h3"><span class="graybg">router</span></div>
<p>虚拟路由器是一个逻辑组件，能够在不同网络之间分发数据包。虚拟路由器也提供L3和NAT转发功能，让虚拟网络中的服务器能够访问外部流量。</p>
<pre class="crayon-plain-tag"># 将端口添加到路由器
openstack router add port &lt;router&gt; &lt;port&gt;
openstack router remove port &lt;router&gt; &lt;port&gt;

# 将子网连接到路由器
openstack router add subnet &lt;router&gt; &lt;subnet&gt;
openstack router remove subnet &lt;router&gt; &lt;subnet&gt;

# 在路由器的路由表中添加一个静态路由
openstack router add route
    [--route destination=&lt;subnet&gt;,gateway=&lt;ip-address&gt;]
    &lt;router&gt;
openstack router remove route
    [--route destination=&lt;subnet&gt;,gateway=&lt;ip-address&gt;]
    &lt;router&gt;

# 创建路由器
openstack router create
    [--enable | --disable]
    # 集中还是分布式的
    [--distributed | --centralized]
    # 是否高可用
    [--ha | --no-ha]
    [--description &lt;description&gt;]
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    [--availability-zone-hint &lt;availability-zone&gt;]
    [--tag &lt;tag&gt; | --no-tag]
    &lt;name&gt;

# 删除路由器
openstack router delete &lt;router&gt; [&lt;router&gt; ...]

# 列出路由器
openstack router list
    [--sort-column SORT_COLUMN]
    [--name &lt;name&gt;]
    [--enable | --disable]
    [--long]
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    [--agent &lt;agent-id&gt;]
    [--tags &lt;tag&gt;[,&lt;tag&gt;,...]]
    [--any-tags &lt;tag&gt;[,&lt;tag&gt;,...]]
    [--not-tags &lt;tag&gt;[,&lt;tag&gt;,...]]
    [--not-any-tags &lt;tag&gt;[,&lt;tag&gt;,...]]

# 设置属性
openstack router set
    [--name &lt;name&gt;]
    [--description &lt;description&gt;]
    [--enable | --disable]
    [--distributed | --centralized]
    [--route destination=&lt;subnet&gt;,gateway=&lt;ip-address&gt;]
    [--no-route]
    [--ha | --no-ha]
    [--external-gateway &lt;network&gt;]
    [--fixed-ip subnet=&lt;subnet&gt;,ip-address=&lt;ip-address&gt;]
    [--enable-snat | --disable-snat]
    [--qos-policy &lt;qos-policy&gt; | --no-qos-policy]
    [--tag &lt;tag&gt;]
    [--no-tag]
    &lt;router&gt;
openstack router unset
    [--route destination=&lt;subnet&gt;,gateway=&lt;ip-address&gt;]
    [--external-gateway]
    [--qos-policy]
    [--tag &lt;tag&gt; | --all-tag]
    &lt;router&gt;

# 显示路由器信息
openstack router show &lt;router&gt; </pre>
<div class="blog_h3"><span class="graybg">port</span></div>
<p>端口是网络上的接入点，它可以将单个设备（例如server上的NIC）连接到网络。端口也描述了其关联的网络配置，例如MAC地址、IP地址</p>
<pre class="crayon-plain-tag">openstack port create
    # 端口所属的网络
    --network &lt;network&gt;
    [--description &lt;description&gt;]
    # 端口的设备ID
    [--device &lt;device-id&gt;]
    # 端口的MAC地址
    [--mac-address &lt;mac-address&gt;]
    # 使用端口的实体，例如network:dhcp
    [--device-owner &lt;device-owner&gt;]
    # 端口的VNIC类型。默认normal
    # direct | direct-physical | macvtap | normal | baremetal | virtio-forwarder
    [--vnic-type &lt;vnic-type&gt;]
    # 在宿主机上分配端口
    [--host &lt;host-id&gt;]
    # 此端口所属的DNS域
    [--dns-domain dns-domain]
    # 此端口的DNS名称
    [--dns-name &lt;dns-name&gt;]
    # 调度此端口所需的NUMA亲和性策略
    [--numa-policy-required | --numa-policy-preferred | --numa-policy-legacy]
    # 此端口期望的IP和/或子网。可以指定多次
    [--fixed-ip subnet=&lt;subnet&gt;,ip-address=&lt;ip-address&gt; | --no-fixed-ip]
    [--binding-profile &lt;binding-profile&gt;]
    # 启用/禁用端口
    [--enable | --disable]
    # 启用uplink状态传播
    [--enable-uplink-status-propagation | --disable-uplink-status-propagation]
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    # 额外分配给此端口的DHCP选项
    [--extra-dhcp-option name=&lt;name&gt;[,value=&lt;value&gt;,ip-version={4,6}]]
    # 关联到此端口的安全组
    [--security-group &lt;security-group&gt; | --no-security-group]
    # 关联到此端口的QoS策略
    [--qos-policy &lt;qos-policy&gt;]
    # 是否启用端口安全
    [--enable-port-security | --disable-port-security]
    # 添加允许的IP/MAC地址。可以指定多次
    # 不被允许的IP地址，不能作为发往此端口的IP目的地址
    [--allowed-address ip-address=&lt;ip-address&gt;[,mac-address=&lt;mac-address&gt;]]
    [--tag &lt;tag&gt; | --no-tag]
    &lt;name&gt;

openstack port delete &lt;port&gt; [&lt;port&gt; ...]

openstack port list
    [--sort-column SORT_COLUMN]
    [--device-owner &lt;device-owner&gt;]
    [--host &lt;host-id&gt;]
    [--network &lt;network&gt;]
    [--router &lt;router&gt; | --server &lt;server&gt; | --device-id &lt;device-id&gt;]
    [--mac-address &lt;mac-address&gt;]
    [--long]
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    [--fixed-ip subnet=&lt;subnet&gt;,ip-address=&lt;ip-address&gt;,ip-substring=&lt;ip-substring&gt;]
    [--tags &lt;tag&gt;[,&lt;tag&gt;,...]]
    [--any-tags &lt;tag&gt;[,&lt;tag&gt;,...]]
    [--not-tags &lt;tag&gt;[,&lt;tag&gt;,...]]
    [--not-any-tags &lt;tag&gt;[,&lt;tag&gt;,...]]

openstack port set
    [--description &lt;description&gt;]
    [--device &lt;device-id&gt;]
    [--mac-address &lt;mac-address&gt;]
    [--device-owner &lt;device-owner&gt;]
    [--vnic-type &lt;vnic-type&gt;]
    [--host &lt;host-id&gt;]
    [--dns-domain dns-domain]
    [--dns-name &lt;dns-name&gt;]
    [--numa-policy-required | --numa-policy-preferred | --numa-policy-legacy]
    [--enable | --disable]
    [--name &lt;name&gt;]
    [--fixed-ip subnet=&lt;subnet&gt;,ip-address=&lt;ip-address&gt;]
    [--no-fixed-ip]
    [--binding-profile &lt;binding-profile&gt;]
    [--no-binding-profile]
    [--qos-policy &lt;qos-policy&gt;]
    [--security-group &lt;security-group&gt;]
    [--no-security-group]
    [--enable-port-security | --disable-port-security]
    [--allowed-address ip-address=&lt;ip-address&gt;[,mac-address=&lt;mac-address&gt;]]
    [--no-allowed-address]
    [--data-plane-status &lt;status&gt;]
    [--tag &lt;tag&gt;]
    [--no-tag]
    &lt;port&gt;
openstack port unset
    [--fixed-ip subnet=&lt;subnet&gt;,ip-address=&lt;ip-address&gt;]
    [--binding-profile &lt;binding-profile-key&gt;]
    [--security-group &lt;security-group&gt;]
    [--allowed-address ip-address=&lt;ip-address&gt;[,mac-address=&lt;mac-address&gt;]]
    [--qos-policy]
    [--data-plane-status]
    [--numa-policy]
    [--tag &lt;tag&gt; | --all-tag]
    &lt;port&gt;

openstack port show &lt;port&gt; </pre>
<div class="blog_h3"><span class="graybg">security group</span></div>
<p>安全组是虚拟的网络防火墙，网络中的服务器、端口等资源可以受其影响。</p>
<p>安全组是安全组规则的容器。</p>
<pre class="crayon-plain-tag"># 创建安全组
openstack security group create
    [--description &lt;description&gt;]
    [--project &lt;project&gt;]
    # 是否五状态
    [--stateful | --stateless]
    [--project-domain &lt;project-domain&gt;]
    [--tag &lt;tag&gt; | --no-tag]
    &lt;name&gt;

openstack security group delete &lt;group&gt; [&lt;group&gt; ...]

openstack security group list
    [--sort-column SORT_COLUMN]
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    [--tags &lt;tag&gt;[,&lt;tag&gt;,...]]
    [--any-tags &lt;tag&gt;[,&lt;tag&gt;,...]]
    [--not-tags &lt;tag&gt;[,&lt;tag&gt;,...]]
    [--not-any-tags &lt;tag&gt;[,&lt;tag&gt;,...]]
    [--all-projects]

openstack security group set
    [--name &lt;new-name&gt;]
    [--description &lt;description&gt;]
    [--stateful | --stateless]
    [--tag &lt;tag&gt;]
    [--no-tag]
    &lt;group&gt;
openstack security group unset [--tag &lt;tag&gt; | --all-tag] &lt;group&gt;

openstack security group show &lt;group&gt;</pre>
<div class="blog_h3"><span class="graybg">security group rule</span></div>
<p>安全组中的一条规则。</p>
<pre class="crayon-plain-tag"># 创建一条规则
openstack security group rule create
    # 此规则针对的远程IP，可以使用CIDR
    #   0.0.0.0/0 表示默认IPv4规则
    #   ::/0 表示默认IPv6规则
    [--remote-ip &lt;ip-address&gt; | --remote-group &lt;group&gt;]
    # 目标（远程）端口。可以使用端口范围，例如  137:139
    # 对于TCP/UDP必须，对于ICMP忽略此字段
    [--dst-port &lt;port-range&gt;]
    # 协议： 默认any表示任何协议
    #   ah, dccp, egp, esp, gre, icmp, igmp, ipv6-encap, ipv6-frag, ipv6-icmp, ipv6-nonxt, 
    #   ipv6-opts, ipv6-route, ospf, pgm, rsvp, sctp, tcp, udp, udplite, vrrp 
    [--protocol &lt;protocol&gt;]
    [--description &lt;description&gt;]
    # 针对特定的ICMP类型
    [--icmp-type &lt;icmp-type&gt;]
    [--icmp-code &lt;icmp-code&gt;]
    # 此规则针对入站还是出站流量，默认ingress
    [--ingress | --egress]
    # 以太网上的流量类型  IPv4, IPv6
    [--ethertype &lt;ethertype&gt;]
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    # 所属的组
    &lt;group&gt;

openstack security group rule delete &lt;rule&gt; [&lt;rule&gt; ...]

openstack security group rule list
    [--sort-column SORT_COLUMN]
    [--protocol &lt;protocol&gt;]
    [--ethertype &lt;ethertype&gt;]
    [--ingress | --egress]
    [--long]
    [--all-projects]
    [&lt;group&gt;]

openstack security group rule show &lt;rule&gt;


# 完全开放默认安全组
openstack security group rule create --remote-ip 0.0.0.0/0  --protocol any --ingress --ethertype IPv4 default
openstack security group rule create --remote-ip ::/0  --protocol any --ingress --ethertype IPv6 default
openstack security group rule create --remote-ip 0.0.0.0/0  --protocol any --egress --ethertype IPv4 default
openstack security group rule create --remote-ip ::/0  --protocol any --egress --ethertype IPv6 default</pre>
<div class="blog_h3"><span class="graybg">network auto allocated topology</span></div>
<p>可以让管理员快速的设置某个项目的外部连接性。每个项目只能有一个此对象。</p>
<pre class="crayon-plain-tag">openstack network auto allocated topology create
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    [--check-resources]
    [--or-show]

openstack network auto allocated topology delete
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]</pre>
<div class="blog_h3"><span class="graybg">network flavor</span></div>
<p>扩展network flavor允许用户在创建资源时，选择管理员配置的“网络风格”</p>
<pre class="crayon-plain-tag"># 添加一个service profile到network flavor
openstack network flavor add profile &lt;flavor&gt; &lt;service-profile&gt;
openstack network flavor remove profile &lt;flavor&gt; &lt;service-profile&gt;

# 创建network flavor
openstack network flavor create
    # 此flavor应用到的网络服务类型，例如VPN
    # 执行 openstack network service provider list  获得网络服务类型列表
    --service-type &lt;service-type&gt;
    [--description DESCRIPTION]
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    [--enable | --disable]
    &lt;name&gt;

openstack network flavor delete &lt;flavor&gt; [&lt;flavor&gt; ...]

openstack network flavor list [--sort-column SORT_COLUMN]

openstack network flavor set
    [--description DESCRIPTION]
    [--disable | --enable]
    [--name &lt;name&gt;]
    &lt;flavor&gt;

openstack network flavor show &lt;flavor&gt;</pre>
<div class="blog_h3"><span class="graybg">network flavor profile</span></div>
<p>用于管理员创建/删除/列出/显示网络服务的profile。</p>
<pre class="crayon-plain-tag">openstack network flavor profile create
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    [--description &lt;description&gt;]
    [--enable | --disable]
    [--driver DRIVER]
    [--metainfo METAINFO]

openstack network flavor profile delete
    &lt;flavor-profile&gt;
    [&lt;flavor-profile&gt; ...]

openstack network flavor profile list [--sort-column SORT_COLUMN]

openstack network flavor profile set
    [--project-domain &lt;project-domain&gt;]
    [--description &lt;description&gt;]
    [--enable | --disable]
    [--driver DRIVER]
    [--metainfo METAINFO]
    &lt;flavor-profile&gt;

openstack network flavor profile show &lt;flavor-profile&gt;</pre>
<div class="blog_h3"><span class="graybg">network meter </span></div>
<p>允许管理员来度量某个IP范围的流量。需要L3 metering extension</p>
<pre class="crayon-plain-tag">openstack network meter create
    [--description &lt;description&gt;]
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    [--share | --no-share]
    &lt;name&gt;

openstack network meter delete &lt;meter&gt; [&lt;meter&gt; ...]

openstack network meter list [--sort-column SORT_COLUMN]

openstack network meter show &lt;meter&gt;</pre>
<div class="blog_h3"><span class="graybg">network meter rule</span></div>
<p>为某个meter设置度量网络流量的规则。需要L3 metering extension</p>
<pre class="crayon-plain-tag">openstack network meter rule create
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    [--exclude | --include]
    [--ingress | --egress]
    [--remote-ip-prefix &lt;remote-ip-prefix&gt;]
    [--source-ip-prefix &lt;remote-ip-prefix&gt;]
    [--destination-ip-prefix &lt;remote-ip-prefix&gt;]
    &lt;meter&gt;

openstack network meter rule delete
    &lt;meter-rule-id&gt;
    [&lt;meter-rule-id&gt; ...]

openstack network meter rule list [--sort-column SORT_COLUMN]

openstack network meter rule show &lt;meter-rule-id&gt;</pre>
<div class="blog_h3"><span class="graybg">network qos policy</span></div>
<p>将一组网络QoS规则组合到一起，可以应用到一个网络或端口。</p>
<pre class="crayon-plain-tag">openstack network qos policy create
    [--description &lt;description&gt;]
    [--share | --no-share]
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    [--default | --no-default]
    &lt;name&gt;

openstack network qos policy delete &lt;qos-policy&gt; [&lt;qos-policy&gt; ...]

openstack network qos policy list
    [--sort-column SORT_COLUMN]
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    [--share | --no-share]

openstack network qos policy set
    [--name &lt;name&gt;]
    [--description &lt;description&gt;]
    [--share | --no-share]
    [--default | --no-default]
    &lt;qos-policy&gt;

openstack network qos policy show &lt;qos-policy&gt;</pre>
<div class="blog_h3"><span class="graybg">network qos rule</span></div>
<p>上述policy中的一个规则</p>
<pre class="crayon-plain-tag">openstack network qos rule create
    # QoS规则类型
    # minimum-bandwidth, dscp-marking, bandwidth-limit
    [--type &lt;type&gt;]
    # 最大带宽
    [--max-kbps &lt;max-kbps&gt;]
    # 最大突发带宽。如果不指定或者设置为0表示自动，为80%的最大带宽，适合于典型的TCP流量
    [--max-burst-kbits &lt;max-burst-kbits&gt;]
    # DSCP标记，可以是0，或者8-56之间的偶数（42 44 50 52 54不可以）
    [--dscp-mark &lt;dscp-mark&gt;]
    # 最小保障的带宽
    [--min-kbps &lt;min-kbps&gt;]
    # 此规则是用于入站还是出站的流量（从当前项目的角度）
    [--ingress | --egress]
    # 此规则加到哪个策略中
    &lt;qos-policy&gt;

openstack network qos rule delete &lt;qos-policy&gt; &lt;rule-id&gt;

openstack network qos rule list
    [--sort-column SORT_COLUMN]
    &lt;qos-policy&gt;

openstack network qos rule set
    [--max-kbps &lt;max-kbps&gt;]
    [--max-burst-kbits &lt;max-burst-kbits&gt;]
    [--dscp-mark &lt;dscp-mark&gt;]
    [--min-kbps &lt;min-kbps&gt;]
    [--ingress | --egress]
    &lt;qos-policy&gt;
    &lt;rule-id&gt;

openstack network qos rule show &lt;qos-policy&gt; &lt;rule-id&gt;</pre>
<div class="blog_h3"><span class="graybg">network segment</span></div>
<p>表示一个网络中的隔离的L2的段。一个（虚拟）网络可以包含多个段，同一个网络中的段的L2通信不被保证。</p>
<pre class="crayon-plain-tag"># 创建一个网络段
openstack network segment create
    [--description &lt;description&gt;]
    # 物理网络的名字
    [--physical-network &lt;physical-network&gt;]
    # 段的名字
    [--segment &lt;segment&gt;]
    # 此段属于的虚拟网络的名字
    --network &lt;network&gt;
    # 此段的网络类型：flat, geneve, gre, local, vlan, vxlan
    --network-type &lt;network-type&gt;
    &lt;name&gt;

openstack network segment delete
    &lt;network-segment&gt;
    [&lt;network-segment&gt; ...]

openstack network segment list
    [--sort-column SORT_COLUMN]
    [--long]
    [--network &lt;network&gt;]

openstack network segment set
    [--description &lt;description&gt;]
    [--name &lt;name&gt;]
    &lt;network-segment&gt;

openstack network segment show &lt;network-segment&gt;</pre>
<div class="blog_h3"><span class="graybg">network segment range</span></div>
<p>用于多租户下的网络段分配。可以让管理员全局的，或者基于用户的，来控制网络段范围。</p>
<pre class="crayon-plain-tag">openstack network segment range create
    [--private | --shared]
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    --network-type &lt;network-type&gt;
    [--physical-network &lt;physical-network-name&gt;]
    --minimum &lt;minimum-segmentation-id&gt;
    --maximum &lt;maximum-segmentation-id&gt;
    &lt;name&gt;

openstack network segment range delete
    &lt;network-segment-range&gt;
    [&lt;network-segment-range&gt; ...]

openstack network segment range list
    [--sort-column SORT_COLUMN]
    [--long]
    [--used | --unused]
    [--available | --unavailable]

openstack network segment range set
    [--name &lt;name&gt;]
    [--minimum &lt;minimum-segmentation-id&gt;]
    [--maximum &lt;maximum-segmentation-id&gt;]
    &lt;network-segment-range&gt;

openstack network segment range show &lt;network-segment-range&gt; </pre>
<div class="blog_h3"><span class="graybg">network agent</span></div>
<p>所谓网络代理，负责（在节点上）处理各种任务，以实现虚拟网络。网络代理包括：</p>
<ol>
<li>neutron-dhcp-agent，负责提供DHCP服务给虚拟机</li>
<li>neutron-l3-agent，负责在自服务网络中提供路由</li>
<li>neutron-metering-agent</li>
<li>neutron-lbaas-agent</li>
</ol>
<pre class="crayon-plain-tag"># 添加网络到代理
openstack network agent add network [--dhcp] &lt;agent-id&gt; &lt;network&gt;
openstack network agent remove network [--dhcp] &lt;agent-id&gt; &lt;network&gt;

# 添加路由器到代理
openstack network agent add router [--l3] &lt;agent-id&gt; &lt;router&gt;
openstack network agent remove router [--l3] &lt;agent-id&gt; &lt;router&gt;

# 删除代理
openstack network agent delete &lt;network-agent&gt; [&lt;network-agent&gt; ...]

# 设置代理属性
openstack network agent set
    [--description &lt;description&gt;]
    [--enable | --disable]
    &lt;network-agent&gt;

# 列出代理
openstack network agent list
    [--sort-column SORT_COLUMN]
    [--agent-type &lt;agent-type&gt;]
    [--host &lt;host&gt;]
    [--network &lt;network&gt; | --router &lt;router&gt;]
    [--long]

# 显示代理详细信息
openstack network agent show &lt;network-agent&gt; </pre>
<div class="blog_h3"><span class="graybg">ip availability</span></div>
<p>显示网络可用的IP地址</p>
<pre class="crayon-plain-tag"># IP可用数量
openstack ip availability list

# 显示详细信息
openstack ip availability show &lt;network&gt;</pre>
<div class="blog_h3"><span class="graybg">floating ip</span></div>
<p>管理浮动IP</p>
<pre class="crayon-plain-tag"># 创建一个浮动IP
openstack floating ip create
    # 在哪个子网上创建浮动IP
    [--subnet &lt;subnet&gt;]
    # 浮动IP授予哪个端口
    [--port &lt;port&gt;]
    # 浮动IP的值
    [--floating-ip-address &lt;ip-address&gt;]
    # 映射到浮动IP的固定IP
    [--fixed-ip-address &lt;ip-address&gt;]
    # 浮动IP的QoS策略
    [--qos-policy &lt;qos-policy&gt;]
    [--description &lt;description&gt;]
    [--project &lt;project&gt;]
    # 浮动IP的DNS名
    [--dns-domain &lt;dns-domain&gt;]
    [--dns-name &lt;dns-name&gt;]
    [--project-domain &lt;project-domain&gt;]
    # 添加标记
    [--tag &lt;tag&gt; | --no-tag]
    # 从什么网络来分配浮动IP
    &lt;network&gt;
openstack floating ip unset
    [--port]
    [--qos-policy]
    [--tag &lt;tag&gt; | --all-tag]
    &lt;floating-ip&gt;

openstack floating ip delete &lt;floating-ip&gt; [&lt;floating-ip&gt; ...]
openstack floating ip set
    [--port &lt;port&gt;]
    [--fixed-ip-address &lt;ip-address&gt;]
    [--description &lt;description&gt;]
    [--qos-policy &lt;qos-policy&gt; | --no-qos-policy]
    [--tag &lt;tag&gt;]
    [--no-tag]
    &lt;floating-ip&gt;

openstack floating ip show &lt;floating-ip&gt;
openstack floating ip list
    [--sort-column SORT_COLUMN]
    [--network &lt;network&gt;]
    [--port &lt;port&gt;]
    [--fixed-ip-address &lt;ip-address&gt;]
    [--floating-ip-address &lt;ip-address&gt;]
    [--long]
    [--status &lt;status&gt;]
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    [--router &lt;router&gt;]
    [--tags &lt;tag&gt;[,&lt;tag&gt;,...]]
    [--any-tags &lt;tag&gt;[,&lt;tag&gt;,...]]
    [--not-tags &lt;tag&gt;[,&lt;tag&gt;,...]]
    [--not-any-tags &lt;tag&gt;[,&lt;tag&gt;,...]]</pre>
<div class="blog_h3"><span class="graybg">floating ip pool</span></div>
<p>浮动IP池管理</p>
<pre class="crayon-plain-tag">openstack floating ip pool list [--sort-column SORT_COLUMN]</pre>
<div class="blog_h3"><span class="graybg">floating ip port forwarding </span></div>
<p>创建浮动IP端口转发规则</p>
<pre class="crayon-plain-tag">openstack floating ip port forwarding create
    # 端口上的固定IPv4地址，浮动IP将其作为转发目标
    --internal-ip-address &lt;internal-ip-address&gt;
    # 转发到的端口
    --port &lt;port&gt;
    # 固定地址上的端口
    --internal-protocol-port &lt;port-number&gt;
    # 浮动IP上的端口
    --external-protocol-port &lt;port-number&gt;
    # 协议
    --protocol &lt;protocol&gt;
    [--description &lt;description&gt;]
    # 此转发规则针对的浮动IP（的IP或ID）
    &lt;floating-ip&gt;</pre>
<div class="blog_h3"><span class="graybg">volume service</span></div>
<p>管理卷服务</p>
<pre class="crayon-plain-tag">openstack volume service list
    [--host &lt;host&gt;]
    [--service &lt;service&gt;]
    [--long]

openstack volume service set
    [--enable | --disable]
    [--disable-reason &lt;reason&gt;]
    &lt;host&gt;
    &lt;service&gt;</pre>
<div class="blog_h3"><span class="graybg">volume</span></div>
<p>管理卷</p>
<pre class="crayon-plain-tag"># 创建卷
openstack volume create
    # 卷的大小，单位GB
    [--size &lt;size&gt;]
    # 卷的类型
    [--type &lt;volume-type&gt;]
    # 将镜像、快照或者另外一个卷，作为新卷的数据来源
    [--image &lt;image&gt; | --snapshot &lt;snapshot&gt; | --source &lt;volume&gt; ]
    [--description &lt;description&gt;]
    # 指定一个alternate用户、项目
    [--user &lt;user&gt;]
    [--project &lt;project&gt;]
    # 在指定可用区中创建卷
    [--availability-zone &lt;availability-zone&gt;]
    # 将卷加入到一致性组
    [--consistency-group &lt;consistency-group&gt;]
    [--property &lt;key=value&gt; [...] ]
    # 提供给卷调度器的提示信息
    [--hint &lt;key=value&gt; [...] ]
    # 卷是否需要支持多重挂载
    [--multi-attach]
    # 是否将卷标记为可启动磁盘
    [--bootable | --non-bootable]
    # 是否只读卷
    [--read-only | --read-write]
    &lt;name&gt;
# 删除卷
openstack volume delete
    [--force | --purge]
    &lt;volume&gt; [&lt;volume&gt; ...]

# 列出卷
openstack volume list
    [--project &lt;project&gt; [--project-domain &lt;project-domain&gt;]]
    [--user &lt;user&gt; [--user-domain &lt;user-domain&gt;]]
    [--name &lt;name&gt;]
    [--status &lt;status&gt;]
    [--all-projects]
    [--long]
    [--limit &lt;num-volumes&gt;]
    [--marker &lt;volume&gt;]
# 显示卷的详细信息
openstack volume show
    &lt;volume&gt;

# 设置卷属性
openstack volume set
    [--name &lt;name&gt;]
    [--size &lt;size&gt;]
    [--description &lt;description&gt;]
    [--no-property]
    [--property &lt;key=value&gt; [...] ]
    [--image-property &lt;key=value&gt; [...] ]
    [--state &lt;state&gt;]
    [--attached | --detached ]
    [--type &lt;volume-type&gt;]
    [--retype-policy &lt;retype-policy&gt;]
    [--bootable | --non-bootable]
    [--read-only | --read-write]
    &lt;volume&gt;
openstack volume unset
    [--property &lt;key&gt;]
    [--image-property &lt;key&gt;]
    &lt;volume&gt;</pre>
<div class="blog_h3"><span class="graybg">volume type</span></div>
<p>管理卷类型</p>
<pre class="crayon-plain-tag">openstack volume type create
    [--description &lt;description&gt;]
    [--public | --private]
    [--property &lt;key=value&gt; [...] ]
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    [--encryption-provider &lt;provider&gt;]
    [--encryption-cipher &lt;cipher&gt;]
    [--encryption-key-size &lt;key-size&gt;]
    [--encryption-control-location &lt;control-location&gt;]
    &lt;name&gt;
openstack volume type delete
    &lt;volume-type&gt; [&lt;volume-type&gt; ...]

openstack volume type list
    [--long]
    [--default | --public | --private]
    [--encryption-type]
openstack volume type show
    [--encryption-type]
    &lt;volume-type&gt;


openstack volume type set
    [--name &lt;name&gt;]
    [--description &lt;description&gt;]
    [--property &lt;key=value&gt; [...] ]
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    [--encryption-provider &lt;provider&gt;]
    [--encryption-cipher &lt;cipher&gt;]
    [--encryption-key-size &lt;key-size&gt;]
    [--encryption-control-location &lt;control-location&gt;]
    &lt;volume-type&gt;
openstack volume type unset
    [--property &lt;key&gt; [...] ]
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    [--encryption-type]
    &lt;volume-type&gt;</pre>
<div class="blog_h3"><span class="graybg">volume backend</span></div>
<pre class="crayon-plain-tag"># 显示卷后端特性
openstack volume backend capability show
    [--sort-column SORT_COLUMN]
    &lt;host&gt;

# 列出后端池
openstack volume backend pool list [--sort-column SORT_COLUMN] [--long] </pre>
<div class="blog_h3"><span class="graybg">volume migrate</span></div>
<p>将卷迁移到一个新的宿主机 </p>
<pre class="crayon-plain-tag">openstack volume migrate
    # 目标主机
    --host &lt;host&gt;
    # 使用一般性的，基于复制的迁移。跳过存储驱动可能的优化
    [--force-host-copy]
    # 锁定卷，防止迁移被其它操作中止
    [--lock-volume]
    &lt;volume&gt;</pre>
<div class="blog_h3"><span class="graybg">volume snapshot</span></div>
<p>管理卷的快照</p>
<pre class="crayon-plain-tag"># 创建一个卷快照
openstack volume snapshot create
    # 目标卷
    [--volume &lt;volume&gt;]
    [--description &lt;description&gt;]
    # 即使在使用中，也创建快照
    [--force]
    [--property &lt;key=value&gt; [...] ]
    [--remote-source &lt;key=value&gt; [...]]
    &lt;snapshot-name&gt;
openstack volume snapshot delete
    [--force]
    &lt;snapshot&gt; [&lt;snapshot&gt; ...]


# 列出和显示
openstack volume snapshot list
    [--all-projects]
    [--project &lt;project&gt; [--project-domain &lt;project-domain&gt;]]
    [--long]
    [--limit &lt;num-snapshots&gt;]
    [--marker &lt;snapshot&gt;]
    [--name &lt;name&gt;]
    [--status &lt;status&gt;]
    [--volume &lt;volume&gt;]
openstack volume snapshot show
    &lt;snapshot&gt;

# 设置属性
openstack volume snapshot set
    [--name &lt;name&gt;]
    [--description &lt;description&gt;]
    [--no-property]
    [--property &lt;key=value&gt; [...] ]
    [--state &lt;state&gt;]
    &lt;snapshot&gt;
openstack volume snapshot unset
    [--property &lt;key&gt;]
    &lt;snapshot&gt;</pre>
<div class="blog_h3"><span class="graybg">volume backup</span></div>
<p>管理卷备份</p>
<pre class="crayon-plain-tag"># 创建卷备份
openstack volume backup create
    [--name &lt;name&gt;]
    [--description &lt;description&gt;]
    # 保存到哪个对象容器
    [--container &lt;container&gt;]
    # 备份快照
    [--snapshot &lt;snapshot&gt;]
    # 允许备份使用中的卷
    [--force]
    # 进行增量备份
    [--incremental]
    &lt;volume&gt;

# 删除卷备份
openstack volume backup delete [--force] &lt;backup&gt; [&lt;backup&gt; ...]

# 列出卷备份
openstack volume backup list
    [--sort-column SORT_COLUMN]
    [--long]
    [--name &lt;name&gt;]
    [--status &lt;status&gt;]
    [--volume &lt;volume&gt;]
    [--marker &lt;volume-backup&gt;]
    [--limit &lt;num-backups&gt;]
    [--all-projects]

# 显示备份详细信息
openstack volume backup show &lt;backup&gt;

# 导出卷备份详细信息
openstack volume backup record export &lt;backup&gt;

# 导入卷备份详细信息
openstack volume backup record import
    &lt;backup_service&gt;
    &lt;backup_metadata&gt;

# 设置属性
openstack volume backup set
    [--name &lt;name&gt;]
    [--description &lt;description&gt;]
    [--state &lt;state&gt;]
    &lt;backup&gt;</pre>
<div class="blog_h3"><span class="graybg">volume backup restore</span></div>
<p>从备份中恢复卷</p>
<pre class="crayon-plain-tag">openstack volume backup restore &lt;backup&gt; &lt;volume&gt;</pre>
<div class="blog_h3"><span class="graybg">volume qos</span></div>
<p>管理卷关联的QoS规格，将QoS规格关联到卷类型</p>
<pre class="crayon-plain-tag"># 关联QoS规格到一个卷类型
openstack volume qos associate
    &lt;qos-spec&gt;
    &lt;volume-type&gt;
volume qos disassociate

# 创建一个QoS规格
openstack volume qos create
    [--consumer &lt;consumer&gt;]
    [--property &lt;key=value&gt; [...] ]
    &lt;name&gt;

# 删除一个QoS规格
volume qos delete

# 列出和显示
volume qos list
openstack volume qos show
    &lt;qos-spec&gt;

# 设置属性
openstack volume qos set
    [--property &lt;key=value&gt; [...] ]
    &lt;qos-spec&gt;
openstack volume qos unset
    [--property &lt;key&gt; [...] ]
    &lt;qos-spec&gt; </pre>
<div class="blog_h3"><span class="graybg">consistency group</span></div>
<p>可以让一组卷同时进行快照，以保证数据一致性</p>
<pre class="crayon-plain-tag"># 将卷加入/移除一致性组
openstack consistency group add volume
    &lt;consistency-group&gt;
    &lt;volume&gt; [&lt;volume&gt; ...]
openstack consistency group remove volume
    &lt;consistency-group&gt;
    &lt;volume&gt; [&lt;volume&gt; ...]


# 创建一致性组
openstack consistency group create
    --volume-type &lt;volume-type&gt; | --consistency-group-source &lt;consistency-group&gt; | --consistency-group-snapshot &lt;consistency-group-snapshot&gt;
    [--description &lt;description&gt;]
    [--availability-zone &lt;availability-zone&gt;]
    [&lt;name&gt;]

# 删除一致性组
openstack consistency group delete
    # 即使出错也强制删除
    [--force]
    &lt;consistency-group&gt; [&lt;consistency-group&gt; ...]



openstack consistency group list
    [--all-projects]
    [--long]
openstack consistency group set
    [--name &lt;name&gt;]
    [--description &lt;description&gt;]
    &lt;consistency-group&gt;
openstack consistency group show
    &lt;consistency-group&gt;</pre>
<div class="blog_h3"><span class="graybg">container</span></div>
<p>定义Object Storage V1中的一个命名空间。</p>
<div class="blog_h3"><span class="graybg">object</span></div>
<p>管理对象存储中的对象。</p>
<pre class="crayon-plain-tag"># 上传对象到container
openstack object create
    [--sort-column SORT_COLUMN]
    [--name &lt;name&gt;]
    &lt;container&gt;
    &lt;filename&gt;
    [&lt;filename&gt; ...]

# 删除对象
openstack object delete &lt;container&gt; &lt;object&gt; [&lt;object&gt; ...]

# 列出对象
openstack object list
    [--sort-column SORT_COLUMN]
    [--delimiter &lt;delimiter&gt;]
    [--marker &lt;marker&gt;]
    [--end-marker &lt;end-marker&gt;]
    [--limit &lt;num-objects&gt;]
    [--long]
    [--all]
    &lt;container&gt;

# 下载对象到本地
openstack object save [--file &lt;filename&gt;] &lt;container&gt; &lt;object&gt;

# 设置对象属性
openstack object set --property &lt;key =value&gt; &lt;container&gt; &lt;object&gt;
openstack object unset --property &lt;key&gt; &lt;container&gt; &lt;object&gt;

# 显示对象信息
openstack object show &lt;container&gt; &lt;object&gt;</pre>
<div class="blog_h3"><span class="graybg">object store account</span></div>
<p>账户是container - objects树结构的最根部。</p>
<pre class="crayon-plain-tag">openstack object store account set --property &lt;key =value&gt;
openstack object store account unset --property &lt;key&gt;

openstack object store account show </pre>
<div class="blog_h2"><span class="graybg">nova</span></div>
<p>很多功能和openstack命令重复，建议使用openstack命令，仅仅在使用某些高级特性时，才需要底层的nova命令。</p>
<div class="blog_h3"><span class="graybg">环境变量</span></div>
<pre class="crayon-plain-tag">OS_USERNAME
OS_PASSWORD
OS_PROJECT_NAME
OS_PROJECT_ID
OS_PROJECT_DOMAIN_NAME
OS_PROJECT_DOMAIN_ID
OS_USER_DOMAIN_NAME
OS_USER_DOMAIN_ID
# Keystone端点URL
OS_AUTH_URL
OS_COMPUTE_API_VERSION
OS_REGION_NAME
# 逗号分隔的，受信任的镜像证书ID
OS_TRUSTED_IMAGE_CERTIFICATE_IDS</pre>
<div class="blog_h3"><span class="graybg">add-secgroup</span></div>
<p>为虚拟机添加安全组</p>
<div class="blog_h3"><span class="graybg">list-secgroup</span></div>
<p>列出指定虚拟机的安全组</p>
<div class="blog_h3"><span class="graybg">remove-secgroup</span></div>
<p>将虚拟机从安全组移除</p>
<div class="blog_h3"><span class="graybg">agent-create</span></div>
<p>创建agent build， 类似的agent-delete、agent-list、agent-modify命令完成相关CRUD操作</p>
<div class="blog_h3"><span class="graybg">aggregate-create</span></div>
<p>管理服务器聚合， 类似的aggregate-delete、aggregate-list、aggregate-update、aggregate-show命令完成相关CRUD操作</p>
<div class="blog_h3"><span class="graybg">aggregate-add-host</span></div>
<p>添加虚拟机到聚合中。类似的aggregate-remove-host用于移除虚拟机</p>
<div class="blog_h3"><span class="graybg">aggregate-cache-images</span></div>
<p>缓存镜像到聚合的所有虚拟机中</p>
<div class="blog_h3"><span class="graybg">aggregate-set-metadata</span></div>
<p>更新和聚合关联的元数据</p>
<div class="blog_h3"><span class="graybg">server-group-create</span></div>
<p>创建（基于策略的）虚拟机分组。 类似的server-group-delete、server-group-list、server-group-get命令完成相关CRUD操作</p>
<div class="blog_h3"><span class="graybg">availability-zone-list</span></div>
<p>列出可用区</p>
<div class="blog_h3"><span class="graybg">list</span></div>
<p>列出虚拟机</p>
<div class="blog_h3"><span class="graybg">update</span></div>
<p>修改虚拟机的名字或描述</p>
<div class="blog_h3"><span class="graybg">show</span></div>
<p>显示单个虚拟机的详细信息</p>
<div class="blog_h3"><span class="graybg">ssh</span></div>
<p>SSH到虚拟机</p>
<div class="blog_h3"><span class="graybg">start</span></div>
<p>启动虚拟机</p>
<div class="blog_h3"><span class="graybg">stop</span></div>
<p>停止虚拟机</p>
<div class="blog_h3"><span class="graybg">backup</span></div>
<p>通过创建backup类型的快照，来备份一个虚拟机</p>
<div class="blog_h3"><span class="graybg">boot</span></div>
<p>启动一个新的虚拟机</p>
<div class="blog_h3"><span class="graybg">clear-password</span></div>
<p>从元数据服务器上清除某个虚拟机的管理密码，不会改变实例的密码</p>
<div class="blog_h3"><span class="graybg">get-password</span></div>
<p>获取虚拟机的管理密码，调用元数据服务器，而不是虚拟机自身</p>
<div class="blog_h3"><span class="graybg">set-password</span></div>
<p>设置虚拟机密码</p>
<div class="blog_h3"><span class="graybg">console-log</span></div>
<p>获取虚拟机控制台日志</p>
<div class="blog_h3"><span class="graybg">reset-state</span></div>
<p>重置虚拟机状态</p>
<div class="blog_h3"><span class="graybg">lock</span></div>
<p>锁定虚拟机，非管理员将无法对虚拟机进行操作</p>
<div class="blog_h3"><span class="graybg">unlock</span></div>
<p>解锁虚拟机</p>
<div class="blog_h3"><span class="graybg">pause</span></div>
<p>在内存中暂停虚拟机</p>
<div class="blog_h3"><span class="graybg">unpause</span></div>
<p>解除内存中暂停的虚拟机</p>
<div class="blog_h3"><span class="graybg">suspend</span></div>
<p>暂停虚拟机到磁盘</p>
<div class="blog_h3"><span class="graybg">resume</span></div>
<p>恢复暂停到磁盘的虚拟机</p>
<div class="blog_h3"><span class="graybg">reboot</span></div>
<p>重启虚拟机</p>
<div class="blog_h3"><span class="graybg">rescue</span></div>
<p>重启虚拟机进入救援模式 —— 从虚拟机的初始镜像或另外一个特定镜像启动虚拟机，将当前book disk挂载为非boot disk</p>
<div class="blog_h3"><span class="graybg">unrescue</span></div>
<p>重启虚拟机，进入正常模式</p>
<div class="blog_h3"><span class="graybg">trigger-crash-dump</span></div>
<p>触发虚拟机crash dump</p>
<div class="blog_h3"><span class="graybg">delete</span></div>
<p>立即关机，同时删除实例</p>
<div class="blog_h3"><span class="graybg">restore</span></div>
<p>恢复一个软删除的实例</p>
<div class="blog_h3"><span class="graybg">force-delete</span></div>
<p>强制删除一个虚拟机</p>
<div class="blog_h3"><span class="graybg">rebuild</span></div>
<p>重建（关机、re-image、启动）虚拟机</p>
<div class="blog_h3"><span class="graybg">shelve</span></div>
<p>保存虚拟机为镜像</p>
<div class="blog_h3"><span class="graybg">unshelve</span></div>
<p>将镜像化的虚拟机恢复</p>
<div class="blog_h3"><span class="graybg">shelve-offload</span></div>
<p>将shelved的虚拟机从宿主机上删除</p>
<div class="blog_h3"><span class="graybg">meta</span></div>
<p>设置/删除虚拟机的元数据</p>
<div class="blog_h3"><span class="graybg">diagnostics</span></div>
<p>获取虚拟机诊断信息</p>
<div class="blog_h3"><span class="graybg">evacuate</span></div>
<p>重建失败宿主机上的某个虚拟机</p>
<div class="blog_h3"><span class="graybg">migrate</span></div>
<p>迁移虚拟机</p>
<div class="blog_h3"><span class="graybg">resize</span></div>
<p>修改虚拟机规格，即flavor</p>
<div class="blog_h3"><span class="graybg">resize-confirm</span></div>
<p>确认修改规格操作</p>
<div class="blog_h3"><span class="graybg">resize-revert</span></div>
<p>撤销尚未确认的resize操作，虚拟机恢复原状</p>
<div class="blog_h3"><span class="graybg">live-migration</span></div>
<p>对指定的虚拟机进行在线迁移</p>
<div class="blog_h3"><span class="graybg">live-migration-abort</span></div>
<p>中止正在进行的在线迁移。需要Nova API版本2.24+</p>
<div class="blog_h3"><span class="graybg">live-migration-force-complete</span></div>
<p>强制结束正在进行的在线迁移</p>
<div class="blog_h3"><span class="graybg">server-migration-show</span></div>
<p>显示某次虚拟机迁移的详细信息</p>
<div class="blog_h3"><span class="graybg">server-migration-list</span></div>
<p>列出指定虚拟机的迁移</p>
<div class="blog_h3"><span class="graybg">migration-list</span></div>
<p>列出所有迁移</p>
<div class="blog_h3"><span class="graybg">server-tag-add</span></div>
<p>添加一个或多个tag到虚拟机，类似的server-tag-delete、server-tag-delete-all、server-tag-list、server-tag-set完成相应CRUD操作</p>
<div class="blog_h3"><span class="graybg">flavor-access-add</span></div>
<p>为某个租户增加某个flavor的权限。类似的flavor-access-remove移除某个租户访问某个flavor的权限</p>
<div class="blog_h3"><span class="graybg">flavor-access-list</span></div>
<p>查看某个flavor的访问权限列表</p>
<div class="blog_h3">
<div class="blog_h3"><span class="graybg">flavor-create</span></div>
</div>
<p>创建一个flavor。类似的 flavor-delete、flavor-list、flavor-update、flavor-show命令完成相关CRUD操作。</p>
<div class="blog_h3"><span class="graybg">flavor-key</span></div>
<p>为某个flavor设置或清除extra_spec。</p>
<div class="blog_h3"><span class="graybg">get-rdp-console</span></div>
<p>达到虚拟机的RDP控制台</p>
<div class="blog_h3"><span class="graybg">get-serial-console</span></div>
<p>得到寻机的串口控制台</p>
<div class="blog_h3"><span class="graybg">get-spice-console</span></div>
<p>得到虚拟机的Spice控制台</p>
<div class="blog_h3"><span class="graybg">get-vnc-console</span></div>
<p>得到虚拟机的VNC控制台</p>
<div class="blog_h3"><span class="graybg">host-evacuate</span></div>
<p>重建失败宿主机上的所有实例</p>
<div class="blog_h3"><span class="graybg">host-evacuate-live</span></div>
<p>对指定宿主机上所有虚拟机执行在线迁移操作</p>
<div class="blog_h3"><span class="graybg">host-servers-migrate</span></div>
<p>对指定宿主机上所有虚拟机执行迁移操作</p>
<div class="blog_h3"><span class="graybg">host-meta</span></div>
<p>设置/删除宿主机上所有的虚拟机的元数据</p>
<div class="blog_h3"><span class="graybg">hypervisor-list</span></div>
<p>列出可用的Hypervisor</p>
<div class="blog_h3"><span class="graybg">hypervisor-servers</span></div>
<p>列出基于指定Hypervisor的虚拟机</p>
<div class="blog_h3"><span class="graybg">hypervisor-show</span></div>
<p>查看Hypervisor的详细信息</p>
<div class="blog_h3"><span class="graybg">hypervisor-stats</span></div>
<p>显示所有Hypervisor的总和统计信息</p>
<div class="blog_h3"><span class="graybg">hypervisor-uptime</span></div>
<p>显示指定Hypervisor的已启动时间</p>
<div class="blog_h3"><span class="graybg">image-create</span></div>
<p>通过获取虚拟机快照，来创建新的镜像</p>
<div class="blog_h3"><span class="graybg">instance-action-list</span></div>
<p>列出针对指定虚拟机的操作历史</p>
<div class="blog_h3"><span class="graybg">interface-attach</span></div>
<p>为虚拟机添加一个网络接口（Port）</p>
<div class="blog_h3"><span class="graybg">interface-list</span></div>
<p>列出连接到虚拟机的Port</p>
<div class="blog_h3"><span class="graybg">refresh-network</span></div>
<p>刷新虚拟机的网络信息</p>
<div class="blog_h3"><span class="graybg">reset-network</span></div>
<p>重置虚拟机的网络</p>
<div class="blog_h3"><span class="graybg">volume-attach</span></div>
<p>添加一个卷给虚拟机</p>
<div class="blog_h3"><span class="graybg">volume-detach</span></div>
<p>将某个卷从虚拟机移除</p>
<div class="blog_h3"><span class="graybg">volume-attachments</span></div>
<p>列出所有添加到虚拟机的卷</p>
<div class="blog_h3"><span class="graybg">volume-update</span></div>
<p>将指定的、已经添加到虚拟机的卷的数据，拷贝到另外一个可用（没有被其它虚拟机使用）的卷上，然后将当前挂载的卷换成新的（接收数据拷贝的哪个）</p>
<div class="blog_h3"><span class="graybg">keypair-add</span></div>
<p>添加访问虚拟机的密钥。类似的 keypair-delete、keypair-list、keypair-show命令完成相关CRUD操作。</p>
<div class="blog_h3"><span class="graybg">quota-class-show</span></div>
<p>显示一个quota class的配额信息</p>
<div class="blog_h3"><span class="graybg">quota-class-update</span></div>
<p>更新quota class的配额值</p>
<div class="blog_h3"><span class="graybg">quota-defaults</span></div>
<p>列出租户的默认配额</p>
<div class="blog_h3"><span class="graybg">quota-delete</span></div>
<p>为一个用户/租户删除配额，配额值恢复为默认</p>
<div class="blog_h3"><span class="graybg">quota-show</span></div>
<p>显示指定用户/租户的配额</p>
<div class="blog_h3"><span class="graybg">quota-update</span></div>
<p>为指定用户/租户更新配额</p>
<div class="blog_h3"><span class="graybg">usage</span></div>
<p>显示单个租户的用量信息</p>
<div class="blog_h3"><span class="graybg">usage-list</span></div>
<p>列出所有租户的用量信息</p>
<div class="blog_h3"><span class="graybg">version-list</span></div>
<p>列出所有API 版本</p>
<div class="blog_h3"><span class="graybg">service-delete</span></div>
<p>删除服务</p>
<div class="blog_h3"><span class="graybg">service-disable</span></div>
<p>禁用服务</p>
<div class="blog_h3"><span class="graybg">service-enable</span></div>
<p>启用服务</p>
<div class="blog_h3"><span class="graybg">service-force-down</span></div>
<p>强制停止服务</p>
<div class="blog_h3"><span class="graybg">service-list</span></div>
<p>列出运行中的服务</p>
<div class="blog_h3"><span class="graybg">bash-completion</span></div>
<p>用于bash自动补全。脚本文件位于：/etc/bash_completion.d/nova，可以直接拷贝到其它机器使用</p>
<div class="blog_h2"><span class="graybg">cinder</span></div>
<div class="blog_h3"><span class="graybg">absolute-limits</span></div>
<p>列出针对某个用户的，存储（总计、备份、快照、卷等）用量的硬限制</p>
<div class="blog_h3"><span class="graybg">backup-create</span></div>
<p>创建一个卷的备份</p>
<div class="blog_h3"><span class="graybg">backup-delete</span></div>
<p>删除一个卷备份</p>
<div class="blog_h3"><span class="graybg">backup-export</span></div>
<p>导出备份元数据</p>
<div class="blog_h3"><span class="graybg">backup-import</span></div>
<p>导入备份元数据</p>
<div class="blog_h3"><span class="graybg">backup-list</span></div>
<p>列出所有备份</p>
<div class="blog_h3"><span class="graybg">backup-reset-state</span></div>
<p>显式的更新备份状态</p>
<div class="blog_h3"><span class="graybg">backup-restore</span></div>
<p>从一个备份恢复</p>
<div class="blog_h3"><span class="graybg">backup-show</span></div>
<p>显示备份详细信息</p>
<div class="blog_h3"><span class="graybg">backup-update</span></div>
<p>更新一个备份</p>
<div class="blog_h3"><span class="graybg">create</span></div>
<p>创建一个卷</p>
<div class="blog_h3"><span class="graybg">delete</span></div>
<p>删除一个或多个卷</p>
<div class="blog_h3"><span class="graybg">extend</span></div>
<p>尝试扩展一个卷的尺寸</p>
<div class="blog_h3"><span class="graybg">failover-host</span></div>
<p>进行故障转移，要求卷是replicated</p>
<div class="blog_h3"><span class="graybg">force-delete</span></div>
<p>强制删除卷，不管其状态如何</p>
<div class="blog_h3"><span class="graybg">freeze-host</span></div>
<p>冻结并且禁用指定的卷主机</p>
<div class="blog_h3"><span class="graybg">get-capabilities</span></div>
<p>显示卷的后端的统计信息、属性</p>
<div class="blog_h3"><span class="graybg">get-pools</span></div>
<p>显示卷的池信息：</p>
<pre class="crayon-plain-tag">cinder get-pools 

+----------+---------------------+
| Property | Value               |
+----------+---------------------+
| name     | openstack-3@lvm#LVM |
+----------+---------------------+
+----------+---------------------+
| Property | Value               |
+----------+---------------------+
| name     | openstack-4@lvm#LVM |
+----------+---------------------+
+----------+---------------------+
| Property | Value               |
+----------+---------------------+
| name     | openstack-2@lvm#LVM |
+----------+---------------------+
+----------+---------------------+
| Property | Value               |
+----------+---------------------+
| name     | openstack-2@nfs#nfs |
+----------+---------------------+</pre>
<p> 每个主机上的lvm后端，独立作为一个池。但是在各主机上都配置了的、指向同一NFS export的，只显示了一个池</p>
<div class="blog_h3"><span class="graybg">list</span></div>
<p>列出所有卷</p>
<div class="blog_h3"><span class="graybg">migrate</span></div>
<p>将卷迁移到一个新的主机上</p>
<div class="blog_h3"><span class="graybg">qos-associate</span></div>
<p>为指定的卷类型设置QoS规格</p>
<div class="blog_h3"><span class="graybg">qos-create</span></div>
<p>创建一个QoS规格</p>
<div class="blog_h3"><span class="graybg">qos-delete</span></div>
<p>删除一个QoS规格</p>
<div class="blog_h3"><span class="graybg">qos-disassociate</span></div>
<p>解除一个QoS规格和一个卷类型的关联</p>
<div class="blog_h3"><span class="graybg">qos-disassociate-all</span></div>
<p>解除一个QoS规格的所有关联</p>
<div class="blog_h3"><span class="graybg">qos-get-association</span></div>
<p>列出QoS规格的关联</p>
<div class="blog_h3"><span class="graybg">qos-key</span></div>
<p>设置/删除QoS规格的某个属性</p>
<div class="blog_h3"><span class="graybg">qos-list</span></div>
<p>列出所有的QoS规格</p>
<div class="blog_h3"><span class="graybg"> quota-class-show </span></div>
<p>列出一个配额类（quota class）的所有配额属性</p>
<div class="blog_h3"><span class="graybg">quota-class-update </span></div>
<p>更新配额类的属性</p>
<div class="blog_h3"><span class="graybg">quota-defaults</span></div>
<p>列出租户的默认配额</p>
<div class="blog_h3"><span class="graybg"> quota-delete</span></div>
<p>为一个租户删除配额</p>
<div class="blog_h3"><span class="graybg">quota-show </span></div>
<p>显示某个租户的当前配额</p>
<div class="blog_h3"><span class="graybg">quota-update</span></div>
<p>更新一个租户的配额</p>
<div class="blog_h3"><span class="graybg"> quota-usage</span></div>
<p>列出一个租户的配额使用情况</p>
<div class="blog_h3"><span class="graybg"> rate-limits</span></div>
<p>列出一个租户的速率限制</p>
<div class="blog_h3"><span class="graybg">rename</span></div>
<p>重命名卷</p>
<div class="blog_h3"><span class="graybg">reset-state</span></div>
<p>在Cinder数据库中显式的重置卷的状态</p>
<div class="blog_h3"><span class="graybg">retype</span></div>
<p>修改卷的类型</p>
<div class="blog_h3"><span class="graybg">revert-to-snapshot</span></div>
<p>将某个卷回退到某个快照</p>
<div class="blog_h3"><span class="graybg">service-disable</span></div>
<p>禁用一个卷服务：</p>
<pre class="crayon-plain-tag">cinder service-disable  openstack-4@nfs  cinder-volume</pre>
<p>要删除卷服务，需要：</p>
<pre class="crayon-plain-tag">cinder-manage service remove  cinder-volume openstack-4@nfs </pre>
<div class="blog_h3"><span class="graybg"> snapshot-create</span></div>
<p>创建卷快照</p>
<div class="blog_h3"><span class="graybg">snapshot-delete </span></div>
<p>删除卷快照</p>
<div class="blog_h3"><span class="graybg">snapshot-list</span></div>
<p>列出卷快照</p>
<div class="blog_h3"><span class="graybg">snapshot-manage</span></div>
<p>管理卷快照</p>
<div class="blog_h3"><span class="graybg">snapshot-metadata</span></div>
<p>设置或删除快照元数据</p>
<div class="blog_h3"><span class="graybg">snapshot-metadata-show</span></div>
<p>查看快照元数据</p>
<div class="blog_h3"><span class="graybg">snapshot-rename</span></div>
<p>重命名快照</p>
<div class="blog_h3"><span class="graybg">snapshot-reset-state</span></div>
<p>显式的重置快照状态</p>
<div class="blog_h3"><span class="graybg">snapshot-show</span></div>
<p>显示卷快照的信息</p>
<div class="blog_h3"><span class="graybg">snapshot-unmanage</span></div>
<p>停止管理卷快照</p>
<div class="blog_h3"><span class="graybg">transfer-accept</span></div>
<p>接受一个卷转移（volume transfer）</p>
<div class="blog_h3"><span class="graybg">transfer-create</span></div>
<p>创建一个卷转移</p>
<div class="blog_h3"><span class="graybg">transfer-delete</span></div>
<p>撤销一个卷转移</p>
<div class="blog_h3"><span class="graybg">transfer-list</span></div>
<p>列出所有卷转移</p>
<div class="blog_h3"><span class="graybg">transfer-show</span></div>
<p>显示卷转移的信息</p>
<div class="blog_h3"><span class="graybg">type-access-add </span></div>
<p>授予指定项目（租户）访问某个卷类型的权限</p>
<div class="blog_h3"><span class="graybg">type-access-list </span></div>
<p>列出卷类型的访问权限</p>
<div class="blog_h3"><span class="graybg">type-access-remove</span></div>
<p>移除卷类型的访问权限</p>
<div class="blog_h3"><span class="graybg">type-create </span></div>
<p>配置好一个卷后端后，需要使用该命令，创建对应的卷类型，并且将类型关联到后端：</p>
<pre class="crayon-plain-tag">cinder type-create nfs-fast
cinder type-key nfs-fast set volume_backend_name=nfs-fast

cinder type-create nfs-slow
cinder type-key nfs-slow set volume_backend_name=nfs-slow</pre>
<div class="blog_h3"><span class="graybg"> type-default</span></div>
<p>显示默认使用的卷类型：</p>
<pre class="crayon-plain-tag">cinder type-default
+--------------------------------------+-------------+---------------------+-----------+
| ID                                   | Name        | Description         | Is_Public |
+--------------------------------------+-------------+---------------------+-----------+
| 464dc192-cc63-4aab-8466-6d4f41cd0fb4 | __DEFAULT__ | Default Volume Type | True      |
+--------------------------------------+-------------+---------------------+-----------+</pre>
<p>通过type-update修改默认卷类型的名字，会导致出错。你必须同步修改控制节点的cinder.conf：</p>
<pre class="crayon-plain-tag">default_volume_type = lvm</pre>
<div class="blog_h3"><span class="graybg">type-delete</span></div>
<p>删除卷类型</p>
<div class="blog_h3"><span class="graybg">type-key</span></div>
<p>设置/删除卷类型的额外规格（extra_spec）</p>
<div class="blog_h3"><span class="graybg">type-list </span></div>
<p>列出所有卷类型</p>
<div class="blog_h3"><span class="graybg">type-show</span></div>
<p>显示一个卷类型的详细信息</p>
<div class="blog_h3"><span class="graybg">type-update </span></div>
<p>根据ID来更新一个卷类型的名字、描述、是否公开：</p>
<pre class="crayon-plain-tag">cinder type-update --name __DEFAULT__ 464dc192-cc63-4aab-8466-6d4f41cd0fb4</pre>
<div class="blog_h3"><span class="graybg">unmanage</span></div>
<p>停止管理卷</p>
<div class="blog_h3"><span class="graybg">upload-to-image</span></div>
<p>将卷上传到镜像服务，作为镜像使用</p>
<div class="blog_h1"><span class="graybg">样例环境</span></div>
<p>本章按照官网的样例架构，搭建最小化的OpenStack集群。此集群和生产环境架构的不同之处是：</p>
<ol>
<li>网络代理（networking agents）部署在控制器节点上，而非专用的网络节点</li>
<li>自服务网络（self-service networks）的隧道（Overlay）流量，通过管理网络（management network）而非专用网络传递</li>
</ol>
<div class="blog_h2"><span class="graybg">环境需求</span></div>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2020/12/hwreqs.png"><img class="size-full wp-image-34681 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2020/12/hwreqs.png" alt="hwreqs" width="714" height="630" /></a></p>
<div class="blog_h3"><span class="graybg">节点角色</span></div>
<p>此集群包含如下角色的节点：</p>
<ol>
<li>Controller：要求2张NIC
<ol>
<li>部署Identity Service、Image Service、计算服务的管理部分、网络服务的管理部分、Web仪表盘，以及多种网络代理</li>
<li>部署支持性服务，包括SQL数据库、消息代理、NTP</li>
<li>可选的，部署块存储服务、对象存储服务、编排服务、遥测服务的部分组件</li>
</ol>
</li>
<li>Compute：要求2张NIC
<ol>
<li>部署计算服务的Hypervisor部分，能够操控Instance。默认Hypervisor是KVM</li>
<li>部署一个网络代理，用于<span style="background-color: #c0c0c0;">将Instance连接到虚拟网络</span>，并通过安全组为Instance提供防火墙服务</li>
</ol>
</li>
<li>Block Storage：可选的，为Instance提供块存储、共享文件系统服务。在本示例环境中，计算节点和块存储节点之间的流量通过管理网络传输，生产环境下应该有独立的存储网络</li>
<li>Object Storage：可选的，用于对象存储的服务</li>
</ol>
<div class="blog_h2"><span class="graybg">网络布局</span></div>
<p>推荐使用两套物理网络：</p>
<ol>
<li>管理网络（management network，10.1.0.0/16）：通过NAT连接到互联网。绝大部分情况下，节点需要连接到外网（例如安装软件包）时，都应该通过管理网</li>
<li>提供者网络（provider network，10.0.0.0/16）：这是虚拟机的工作负载流量所使用的网络，在：
<ol>
<li>网络选项一（提供者网络）下，虚拟机直接连接到此网络</li>
<li>网络选项二（自服务网络）下，虚拟机连接到自服务网络，然后NAT到此网络以获得外部连接</li>
</ol>
</li>
</ol>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2020/12/networklayout.png"><img class="size-full wp-image-34961 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2020/12/networklayout.png" alt="networklayout" width="693" height="567" /></a></p>
<div class="blog_h2"><span class="graybg">网络选项</span></div>
<p>此集群可以选用两种虚拟网络之一。</p>
<div class="blog_h3"><span class="graybg">提供者网络</span></div>
<p>Provider networks，也就是外部网络。以最简单的方式部署OpenStack网络服务，通常基于L2（桥接/交换）服务和网络VLAN分段实现。这种选项将<span style="background-color: #c0c0c0;">虚拟网络桥接到物理网络</span>，依赖于物理网络基础设施完成L3服务。</p>
<p>此外，一个DHCP服务用于为Instance提供IP地址。</p>
<p>这种选项不支持一些高级特性，例如LBaaS、FWaaS。</p>
<div class="blog_h3"><span class="graybg">自服务网络</span></div>
<p>所谓自服务，是指非特权账户在不需要管理员介入的情况下，管理虚拟化基础设施 —— 例如网络的能力。</p>
<p>这种选项通过提供L3（路由）服务来增强提供者网络，使用的是类似VXLAN之类的overlay segmentation技术。<span style="background-color: #c0c0c0;">虚拟网络到物理网络的路由通过NAT</span>实现。</p>
<p>OpenStack用户可以在不了解数据网络（data network）底层基础设施的情况下，创建虚拟网络。包括VLAN网络（如果L2插件被相应的配置）。 </p>
<div class="blog_h2"><span class="graybg">客户端安装</span></div>
<div class="blog_h3"><span class="graybg">Ubuntu</span></div>
<p>安装软件：</p>
<pre class="crayon-plain-tag">sudo -H pip install python-openstackclient  --ignore-installed PyYAML
# placement插件
sudo -H pip install osc-placement

# 修复错误，将下面两个文件开头的import queue 改为 import Queue as queue
sudo vim /usr/local/lib/python2.7/dist-packages/openstack/utils.py
sudo vim /usr/local/lib/python2.7/dist-packages/openstack/cloud/openstackcloud.py</pre>
<p>配置Shell自动完成： </p>
<pre class="crayon-plain-tag">openstack complete | sudo tee /etc/bash_completion.d/osc.bash_completion &gt; /dev/null</pre>
<div class="blog_h2"><span class="graybg">组件安装</span></div>
<p>OpenStack由一系列独立安装的、相互协作的组件构成。</p>
<div class="blog_h3"><span class="graybg">源配置</span></div>
<pre class="crayon-plain-tag">dnf -y install centos-release-openstack-ussuri
yum config-manager --set-enabled powertools
dnf install https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm
dnf -y upgrade</pre>
<div class="blog_h3"><span class="graybg">基础软件</span></div>
<pre class="crayon-plain-tag">dnf -y install telnet htop bridge-utils xterm</pre>
<div class="blog_h2"><span class="graybg">安装时钟同步服务</span></div>
<p>略，参考<a href="/ntp-under-ubuntu">Ubuntu的时钟同步</a></p>
<div class="blog_h2"><span class="graybg">安装数据库</span></div>
<p>Keystone为OpenStack提供身份（Identity）服务。Keystone可以提供四种Token（代表请求者身份），样例环境使用Fernet Token，同时基于Apache HTTP Server来处理请求。</p>
<p>首先，你需要安装一个数据库： </p>
<pre class="crayon-plain-tag"># 安装mariadb模块
dnf -y install mariadb mariadb-server python2-PyMySQL</pre>
<p>创建并修改配置文件：</p>
<pre class="crayon-plain-tag">[mysqld]
bind-address = 10.1.0.10

default-storage-engine = innodb
innodb_file_per_table = on
max_connections = 4096
collation-server = utf8_general_ci
character-set-server = utf8</pre>
<p>启用服务：</p>
<pre class="crayon-plain-tag">systemctl enable mariadb.service
systemctl start mariadb.service</pre>
<p>启用安全配置：</p>
<pre class="crayon-plain-tag">mysql_secure_installation </pre>
<div class="blog_h2"><span class="graybg">安装消息队列</span></div>
<p>安装RabbitMQ：</p>
<pre class="crayon-plain-tag">dnf -y install rabbitmq-server

systemctl enable rabbitmq-server.service
systemctl start rabbitmq-server.service</pre>
<p>创建一个RabbitMQ用户：</p>
<pre class="crayon-plain-tag">rabbitmqctl add_user openstack openstack</pre>
<p>为用户openstack授予配置、读写权限：</p>
<pre class="crayon-plain-tag">rabbitmqctl set_permissions openstack ".*" ".*" ".*"</pre>
<div class="blog_h2"><span class="graybg">安装Memcached</span></div>
<p>Identity Service使用Memcached来缓存Tokens。</p>
<p>安装软件：</p>
<pre class="crayon-plain-tag">dnf -y install memcached python3-memcached</pre>
<p> 修改配置：</p>
<pre class="crayon-plain-tag">OPTIONS="-l 0.0.0.0"</pre>
<p>启动服务：</p>
<pre class="crayon-plain-tag">systemctl enable memcached.service
systemctl start memcached.service</pre>
<div class="blog_h2"><span class="graybg">安装Etcd</span></div>
<p>OpenStack组件可能需要使用Etcd来实现分布式键锁定、配置存储、跟踪服务是否存活。</p>
<p>安装软件：</p>
<pre class="crayon-plain-tag">dnf -y install etcd</pre>
<p>修改配置文件：</p>
<pre class="crayon-plain-tag">#[Member]
ETCD_DATA_DIR="/var/lib/etcd/default.etcd"
ETCD_LISTEN_PEER_URLS="http://10.1.0.10:2380"
ETCD_LISTEN_CLIENT_URLS="http://10.1.0.10:2379"
ETCD_NAME="openstack"
#[Clustering]
ETCD_INITIAL_ADVERTISE_PEER_URLS="http://10.1.0.10:2380"
ETCD_ADVERTISE_CLIENT_URLS="http://10.1.0.10:2379"
ETCD_INITIAL_CLUSTER="openstack=http://10.1.0.10:2380"
ETCD_INITIAL_CLUSTER_TOKEN="etcd-cluster-01"
ETCD_INITIAL_CLUSTER_STATE="new"</pre>
<p>启动服务：</p>
<pre class="crayon-plain-tag">systemctl enable etcd
systemctl start etcd</pre>
<div class="blog_h2"><span class="graybg">安装Keystone</span></div>
<p>为Keystone创建数据库和用户：</p>
<pre class="crayon-plain-tag">CREATE DATABASE IF NOT EXISTS keystone;

GRANT ALL PRIVILEGES ON keystone.* TO 'keystone'@'localhost' IDENTIFIED BY 'keystone';

GRANT ALL PRIVILEGES ON keystone.* TO 'keystone'@'%' IDENTIFIED BY 'keystone';</pre>
<p>然后，安装以下包： </p>
<pre class="crayon-plain-tag">dnf -y install openstack-keystone httpd
dnf -y install python3-mod_wsgi</pre>
<p>编辑Keystone配置文件：</p>
<pre class="crayon-plain-tag">[database]
# ...
connection = mysql+pymysql://keystone:keystone@os.gmem.cc/keystone


[token]
provider = fernet</pre>
<p>初始化数据库：</p>
<pre class="crayon-plain-tag">su -s /bin/sh -c "keystone-manage db_sync" keystone</pre>
<p>初始化Fernet密钥存储库：</p>
<pre class="crayon-plain-tag">#                            运行keystone的操作系统用户和组
keystone-manage fernet_setup --keystone-user keystone --keystone-group keystone
keystone-manage credential_setup --keystone-user keystone --keystone-group keystone</pre>
<p>启动Identity服务：</p>
<pre class="crayon-plain-tag">keystone-manage bootstrap --bootstrap-password keystone \
  --bootstrap-admin-url http://os.gmem.cc:5000/v3/ \
  --bootstrap-internal-url http://os.gmem.cc:5000/v3/ \
  --bootstrap-public-url http://os.gmem.cc:5000/v3/ \
  --bootstrap-region-id china</pre>
<p>编辑Apache配置文件： </p>
<pre class="crayon-plain-tag">ServerName os.gmem.cc</pre>
<p>将Keystone的配置文件链接到Apache配置目录：</p>
<pre class="crayon-plain-tag">ln -s /usr/share/keystone/wsgi-keystone.conf /etc/httpd/conf.d/</pre>
<p>启动Apache服务：</p>
<pre class="crayon-plain-tag">systemctl enable httpd.service
systemctl start httpd.service</pre>
<p>为了后续使用OpenStack工具时进行身份验证，你需要设置环境变量（密码来自上面的 keystone-manage bootstrap 步骤）：</p>
<pre class="crayon-plain-tag">export OS_USERNAME=admin
export OS_PASSWORD=keystone
export OS_PROJECT_NAME=admin
export OS_USER_DOMAIN_NAME=Default
export OS_PROJECT_DOMAIN_NAME=Default
export OS_AUTH_URL=http://os.gmem.cc:5000/v3
export OS_IDENTITY_API_VERSION=3 </pre>
<p>现在，我们需要需要创建一些OpenStack对象：</p>
<pre class="crayon-plain-tag"># 创建一个域
# openstack domain create --description "An Example Domain" example

# 创建一个项目
# 示例环境为每个服务创建一个用户，这些用户在此项目中获得授权
openstack project create --domain default --description "Service Project" service


# 常规操作（非管理）应该使用非特权的项目和用户进行
openstack project create --domain default --description "Gmem Project" gmem
openstack user create --domain default --password gmem gmem
openstack role create gmem
openstack role add --project gmem --user gmem gmem </pre>
<div class="blog_h2"><span class="graybg">安装Glance</span></div>
<div class="blog_h3"><span class="graybg">安装组件</span></div>
<p>Glance为OpenStack提供（虚拟机）镜像服务，Glance支持多种后端存储，本样例环境下我们直接存放在文件系统中。</p>
<p>首先，需要为Glance创建数据库：</p>
<pre class="crayon-plain-tag">CREATE DATABASE IF NOT EXISTS glance;

GRANT ALL PRIVILEGES ON glance.* TO 'glance'@'localhost' IDENTIFIED BY 'glance';
GRANT ALL PRIVILEGES ON glance.* TO 'glance'@'%' IDENTIFIED BY 'glance';</pre>
<p>使用OpenStack命令行来创建Glance的凭证信息，注意需要进行上述环境变量设置： </p>
<pre class="crayon-plain-tag">openstack user create --domain default --password glance glance</pre>
<p>为service项目中的glance用户添加admin角色：</p>
<pre class="crayon-plain-tag">openstack role add --project service --user glance admin</pre>
<p>创建Glance服务： </p>
<pre class="crayon-plain-tag"># 服务的类型
openstack service create --name glance --description "OpenStack Image" image</pre>
<p>为Glance服务添加一个端点：</p>
<pre class="crayon-plain-tag">openstack endpoint create --region china image public http://os.gmem.cc:9292</pre>
<p>下面，需要安装和配置Glance组件。安装软件包：</p>
<pre class="crayon-plain-tag">dnf -y install openstack-glance</pre>
<p>修改配置文件：</p>
<pre class="crayon-plain-tag">[database]
connection = mysql+pymysql://glance:glance@os.gmem.cc/glance


[keystone_authtoken]
www_authenticate_uri  = http://os.gmem.cc:5000
auth_url = http://os.gmem.cc:5000
memcached_servers = os.gmem.cc:11211
auth_type = password
project_domain_name = Default
user_domain_name = Default
project_name = service
username = glance
password = glance


[paste_deploy]
flavor = keystone


[glance_store]
stores = file,http
default_store = file
filesystem_store_datadir = /var/lib/glance/images/</pre>
<p>初始化Glance数据库：</p>
<pre class="crayon-plain-tag">su -s /bin/sh -c "glance-manage db_sync" glance</pre>
<p>启动服务：</p>
<pre class="crayon-plain-tag">systemctl enable openstack-glance-api.service
systemctl start openstack-glance-api.service</pre>
<div class="blog_h3"><span class="graybg">下载镜像</span></div>
<p>为了测试OpenStack，建议使用<a href="http://download.cirros-cloud.net/0.5.1/">CirrOS镜像</a>。</p>
<pre class="crayon-plain-tag">wget http://download.cirros-cloud.net/0.4.0/cirros-0.4.0-x86_64-disk.img

openstack image create --public --disk-format qcow2 --container-format bare \
  --file cirros-0.4.0-x86_64-disk.img cirros-0.4.0-amd64 </pre>
<div class="blog_h2"><span class="graybg">安装Placement</span></div>
<p>在Stein版本之前，placement 的代码在Nova中，服务在compute REST API（nova-api）中。</p>
<p>Placement提供了WSGI脚本placement-api，可以在Apache/Nginx之类的WSGI-capable服务器中使用。取决于你的安装方式，该脚本可能位于/usr/bin或/usr/local/bin下面。</p>
<p>为Placement创建数据库：</p>
<pre class="crayon-plain-tag">CREATE DATABASE IF NOT EXISTS placement;
GRANT ALL PRIVILEGES ON placement.* TO 'placement'@'localhost' IDENTIFIED BY 'placement';
GRANT ALL PRIVILEGES ON placement.* TO 'placement'@'%' IDENTIFIED BY 'placement';</pre>
<p>创建用户和端点：</p>
<pre class="crayon-plain-tag">openstack user create --domain default --password placement placement

openstack role add --project service --user placement admin

openstack service create --name placement --description "Placement API" placement

openstack endpoint create --region china placement public  http://os.gmem.cc:8778
openstack endpoint create --region china placement internal http://os.gmem.cc:8778
openstack endpoint create --region china placement admin http://os.gmem.cc:8778</pre>
<p>安装和配置Placement组件：</p>
<pre class="crayon-plain-tag">dnf -y install openstack-placement-api</pre>
<p>修改配置文件：</p>
<pre class="crayon-plain-tag">[placement_database]
connection = mysql+pymysql://placement:placement@os.gmem.cc/placement


[api]
auth_strategy = keystone


[keystone_authtoken]
auth_url = http://os.gmem.cc:5000/v3
memcached_servers = os.gmem.cc:11211
auth_type = password
project_domain_name = Default
user_domain_name = Default
project_name = service
username = placement
password = placement</pre>
<p>初始化数据库：</p>
<pre class="crayon-plain-tag">su -s /bin/sh -c "placement-manage db sync" placement</pre>
<p>修改Apache配置文件（否则可能计算节点nova报 You don't have permission to access this resource）：</p>
<pre class="crayon-plain-tag">&lt;Directory /usr/bin&gt;
  Require all granted
&lt;/Directory&gt;</pre>
<p>重启Apache服务：</p>
<pre class="crayon-plain-tag">systemctl restart httpd</pre>
<div class="blog_h2"><span class="graybg">安装Nova</span></div>
<div class="blog_h3"><span class="graybg">控制节点</span></div>
<p>创建数据库：</p>
<pre class="crayon-plain-tag">CREATE DATABASE IF NOT EXISTS nova_api;
CREATE DATABASE IF NOT EXISTS nova;
CREATE DATABASE IF NOT EXISTS nova_cell0;

GRANT ALL PRIVILEGES ON nova_api.* TO 'nova'@'localhost' IDENTIFIED BY 'nova';
GRANT ALL PRIVILEGES ON nova_api.* TO 'nova'@'%' IDENTIFIED BY 'nova';

GRANT ALL PRIVILEGES ON nova.* TO 'nova'@'localhost' IDENTIFIED BY 'nova';
GRANT ALL PRIVILEGES ON nova.* TO 'nova'@'%' IDENTIFIED BY 'nova';

GRANT ALL PRIVILEGES ON nova_cell0.* TO 'nova'@'localhost' IDENTIFIED BY 'nova';
GRANT ALL PRIVILEGES ON nova_cell0.* TO 'nova'@'%' IDENTIFIED BY 'nova';</pre>
<p>创建用户和端点：</p>
<pre class="crayon-plain-tag">openstack user create --domain default --password nova nova
openstack role add --project service --user nova admin
openstack service create --name nova  --description "OpenStack Compute" compute

openstack endpoint create --region china compute public   http://os.gmem.cc:8774/v2.1
openstack endpoint create --region china compute internal http://os.gmem.cc:8774/v2.1
openstack endpoint create --region china compute admin    http://os.gmem.cc:8774/v2.1</pre>
<p>安装组件：</p>
<pre class="crayon-plain-tag">dnf -y install openstack-nova-api openstack-nova-conductor openstack-nova-novncproxy openstack-nova-scheduler</pre>
<p>修改配置文件： </p>
<pre class="crayon-plain-tag">[DEFAULT]
enabled_apis = osapi_compute,metadata
transport_url = rabbit://openstack:openstack@os.gmem.cc:5672/
; 管理网络的IP地址
my_ip = 10.1.0.10

[api_database]
connection = mysql+pymysql://nova:nova@os.gmem.cc/nova_api

[database]
connection = mysql+pymysql://nova:nova@os.gmem.cc/nova

[api]
auth_strategy = keystone

[keystone_authtoken]
www_authenticate_uri = http://os.gmem.cc:5000/
auth_url = http://os.gmem.cc:5000/
memcached_servers = os.gmem.cc:11211
auth_type = password
project_domain_name = Default
user_domain_name = Default
project_name = service
username = nova
password = nova

[vnc]
enabled = true
server_listen = $my_ip
server_proxyclient_address = $my_ip

[glance]
api_servers = http://os.gmem.cc:9292

[oslo_concurrency]
lock_path = /var/lib/nova/tmp

[placement]
region_name = china
project_domain_name = Default
project_name = service
auth_type = password
user_domain_name = Default
auth_url = http://os.gmem.cc:5000/v3
username = placement
password = placement

[neutron]
; 参考：安装Neutron

[cinder]
; 参考：安装Cinder</pre>
<p>初始化数据库：</p>
<pre class="crayon-plain-tag">su -s /bin/sh -c "nova-manage api_db sync" nova

su -s /bin/sh -c "nova-manage cell_v2 map_cell0" nova

su -s /bin/sh -c "nova-manage cell_v2 create_cell --name=cell1 --verbose" nova

su -s /bin/sh -c "nova-manage db sync" nova</pre>
<p>校验一下，确保cell0和cell1正确的注册了： </p>
<pre class="crayon-plain-tag">su -s /bin/sh -c "nova-manage cell_v2 list_cells" nova</pre>
<p>启动服务：</p>
<pre class="crayon-plain-tag">systemctl enable \
    openstack-nova-api.service \
    openstack-nova-scheduler.service \
    openstack-nova-conductor.service \
    openstack-nova-novncproxy.service

systemctl start \
    openstack-nova-api.service \
    openstack-nova-scheduler.service \
    openstack-nova-conductor.service \
    openstack-nova-novncproxy.service</pre>
<div class="blog_h3"><span class="graybg">计算节点</span></div>
<p>安装软件：</p>
<pre class="crayon-plain-tag">dnf -y install openstack-nova-compute</pre>
<p>修改配置文件：</p>
<pre class="crayon-plain-tag">[DEFAULT]
enabled_apis = osapi_compute,metadata
; 替换为该计算节点上，管理网络的IP
my_ip = 10.1.0.10

[DEFAULT]
transport_url = rabbit://openstack:openstack@os.gmem.cc

[api]
auth_strategy = keystone

[keystone_authtoken]
www_authenticate_uri = http://os.gmem.cc:5000/
auth_url = http://os.gmem.cc:5000/
memcached_servers = os.gmem.cc:11211
auth_type = password
project_domain_name = Default
user_domain_name = Default
project_name = service
username = nova
password = nova

[vnc]
enabled = true
server_listen = 0.0.0.0
server_proxyclient_address = $my_ip
novncproxy_base_url = http://os.gmem.cc:6080/vnc_auto.html

[glance]
api_servers = http://os.gmem.cc:9292

[oslo_concurrency]
lock_path = /var/lib/nova/tmp

[placement]
region_name = china
project_domain_name = Default
project_name = service
auth_type = password
user_domain_name = Default
auth_url = http://os.gmem.cc:5000/v3
username = placement
password = placement

[libvirt]
virt_type = kvm

[neutron]
; 参考：安装Neutron

[cinder]
; 参考：安装Cinder</pre>
<p>启动服务：</p>
<pre class="crayon-plain-tag">systemctl enable libvirtd.service openstack-nova-compute.service
systemctl start libvirtd.service openstack-nova-compute.service</pre>
<p>如果nova-compute启动失败，检查日志： /var/log/nova/nova-compute.log。</p>
<p>最后，将该节点加入到cell数据库中：</p>
<pre class="crayon-plain-tag"># 检查计算服务列表
openstack compute service list --service nova-compute

# 发现计算服务主机
# 需要在控制节点上执行
su -s /bin/sh -c "nova-manage cell_v2 discover_hosts --verbose" nova

# 如果希望自动发现新的主机，可以配置控制节点的/etc/nova/nova.conf：
# [scheduler]
# discover_hosts_in_cells_interval = 300</pre>
<div class="blog_h2"><span class="graybg">安装Neutron</span></div>
<div class="blog_h3"><span class="graybg">控制节点</span></div>
<p>创建数据库：</p>
<pre class="crayon-plain-tag">CREATE DATABASE IF NOT EXISTS neutron;

GRANT ALL PRIVILEGES ON neutron.* TO 'neutron'@'localhost'  IDENTIFIED BY 'neutron';
GRANT ALL PRIVILEGES ON neutron.* TO 'neutron'@'%' IDENTIFIED BY 'neutron';</pre>
<p>创建用户和端点：</p>
<pre class="crayon-plain-tag">openstack user create --domain default --password neutron neutron

openstack role add --project service --user neutron admin

openstack service create --name neutron --description "OpenStack Networking" network

openstack endpoint create --region china network public   http://os.gmem.cc:9696
openstack endpoint create --region china network internal http://os.gmem.cc:9696
openstack endpoint create --region china network admin    http://os.gmem.cc:9696</pre>
<p>如果使用提供者网络，也就是直接将VM添加到外部网络，不提供自服务网络（以及路由器、浮动IP等），则需要使用admin或其它特权账户。步骤如下：</p>
<ol>
<li>安装软件：<br />
<pre class="crayon-plain-tag">dnf -y install openstack-neutron openstack-neutron-ml2 openstack-neutron-linuxbridge ebtables </pre>
</li>
<li>配置Neutron：<br />
<pre class="crayon-plain-tag">[database]
connection = mysql+pymysql://neutron:neutron@os.gmem.cc/neutron

[DEFAULT]
core_plugin = ml2
service_plugins =

transport_url = rabbit://openstack:openstack@os.gmem.cc

auth_strategy = keystone

notify_nova_on_port_status_changes = true
notify_nova_on_port_data_changes = true

[keystone_authtoken]
www_authenticate_uri = http://os.gmem.cc:5000
auth_url = http://os.gmem.cc:5000
memcached_servers = os.gmem.cc:11211
auth_type = password
project_domain_name = default
user_domain_name = default
project_name = service
username = neutron
password = neutron

[nova]
auth_url = http://os.gmem.cc:5000
auth_type = password
project_domain_name = default
user_domain_name = default
region_name = china
project_name = service
username = nova
password = nova

[oslo_concurrency]
lock_path = /var/lib/neutron/tmp</pre>
</li>
<li>
<p>配置Modular Layer2（ML2）插件。此插件使用Linux bridge来为虚拟机构建L2 VNI：</p>
<pre class="crayon-plain-tag">[ml2]
; 启用Flat/VLAN网络
type_drivers = flat,vlan
; 禁用自服务网络
tenant_network_types =
; 启用 Linux bridge mechanism 
mechanism_drivers = linuxbridge
; 启用端口安全扩展驱动
extension_drivers = port_security

[ml2_type_flat]
; 配置提供者虚拟网络为Flat网络
flat_networks = provider

[securitygroup]
; 增强安全组规则的性能
enable_ipset = true </pre>
</li>
<li>
<p> 配置Linux Bridge agent，此Agent为VM构建L2 VNI，并处理安全组：
<pre class="crayon-plain-tag">[linux_bridge]
; 这里填写底层的提供者网络的设备名
physical_interface_mappings = provider:eth0

[vxlan]
; 禁用VXLAN
enable_vxlan = false

[securitygroup]
; 启用安全组，配置基于iptables的防火墙驱动
enable_security_group = true
firewall_driver = neutron.agent.linux.iptables_firewall.IptablesFirewallDriver</pre>
</li>
<li>
<p>确保宿主机内核支持Network bridge filters：
<pre class="crayon-plain-tag"># 二层的网桥在转发包时也会被iptables的FORWARD规则所过滤
net.bridge.bridge-nf-call-iptables  1
net.bridge.bridge-nf-call-ip6tables 1</pre>
<p>此外，为了支持网桥，需要加载内核模块<pre class="crayon-plain-tag">br_netfilter</pre></p>
</li>
<li>
<p> 配置DHCP代理，此代理为虚拟网络提供DHCP服务：</p>
<pre class="crayon-plain-tag">[DEFAULT]
interface_driver = linuxbridge
dhcp_driver = neutron.agent.linux.dhcp.Dnsmasq
enable_isolated_metadata = true</pre>
</li>
</ol>
<p>如果使用自服务网络，不需要特权用户就可以管理网络（包括路由）并在自服务网络和提供者网络之间创建连接，也可以为VM提供浮动IP，以便从外部访问VM。步骤如下：
<ol>
<li>安装软件，同上</li>
<li>配置Neutron，基本同上：<br />
<pre class="crayon-plain-tag">[database]
connection = mysql+pymysql://neutron:neutron@os.gmem.cc/neutron

[DEFAULT]
; 同样使用ML2插件
core_plugin = ml2
; 启用路由服务，允许IP重叠
service_plugins = router
allow_overlapping_ips = true

transport_url = rabbit://openstack:openstack@os.gmem.cc

auth_strategy = keystone

notify_nova_on_port_status_changes = true
notify_nova_on_port_data_changes = true

[keystone_authtoken]
www_authenticate_uri = http://os.gmem.cc:5000
auth_url = http://os.gmem.cc:5000
memcached_servers = os.gmem.cc:11211
auth_type = password
project_domain_name = default
user_domain_name = default
project_name = service
username = neutron
password = neutron

[nova]
auth_url = http://os.gmem.cc:5000
auth_type = password
project_domain_name = default
user_domain_name = default
region_name = china
project_name = service
username = nova
password = nova

[oslo_concurrency]
lock_path = /var/lib/neutron/tmp</pre>
</li>
<li>
<p> 配置Modular Layer2（ML2）插件：</p>
<pre class="crayon-plain-tag">[ml2]
; 启用Flat/VLAN/VXLAN
type_drivers = flat,vlan,vxlan
; 启用自服务网络，基于VXLAN
tenant_network_types = vxlan
; 启用Linux网桥，以及Layer-2 Population
mechanism_drivers = linuxbridge,l2population
; 启用端口安全扩展驱动
extension_drivers = port_security

[ml2_type_flat]
; 配置提供者虚拟网络为Flat网络
flat_networks = provider

[ml2_type_vxlan]
; 设置VXLAN网络标识符的范围
vni_ranges = 1:1000

[securitygroup]
; 增强安全组规则的性能
enable_ipset = true</pre>
</li>
<li>配置Linux Bridge agent：<br />
<pre class="crayon-plain-tag">[linux_bridge]
; 这里填写底层的提供者网络的设备名
physical_interface_mappings = provider:eth0

[vxlan]
; 启用VXLAN
enable_vxlan = true
; 用于处理Overlay网络的底层网络的本机IP地址
local_ip = 10.1.0.10
l2_population = true

[securitygroup]
; 启用安全组，配置基于iptables的防火墙驱动
enable_security_group = true
firewall_driver = neutron.agent.linux.iptables_firewall.IptablesFirewallDriver </pre>
</li>
<li>
<p>确保宿主机内核支持Network bridge filters，同上
</li>
<li>
<p> 配置DHCP，同上</p>
</li>
<li>配置L3代理，此代理为自服务VNI提供路由、NAT：<br />
<pre class="crayon-plain-tag">[DEFAULT]
interface_driver = linuxbridge</pre>
</li>
</ol>
<p>自服务网络是overlay网络，使用VXLAN之类的协议，这些协议具有额外的头，导致实际可能负载减小，如果VM不知道此VNI的特征，会自动设置过大的MTU 1500。Neutron提供的DHCP能自动给VM提供正确的MTU。但是，某些云镜像不使用DHCP，或者忽略DHCP的MTU选项，需要注意。</p>
<p>执行完上述两种网络选项之一后，继续配置元数据代理（metadata agent） ，<span style="background-color: #c0c0c0;">元数据代理代替虚拟机（附加Instance ID、Tenant ID等请求头）访问Nova metadata API</span>，获取虚拟机镜像的配置信息：</p>
<pre class="crayon-plain-tag">[DEFAULT]
nova_metadata_host = os.gmem.cc
; 设置适当的共享密钥
metadata_proxy_shared_secret = openstack</pre>
<p> 配置计算服务（Nova）来使用网络服务：</p>
<pre class="crayon-plain-tag">[neutron]
auth_url = http://os.gmem.cc:5000
auth_type = password
project_domain_name = default
user_domain_name = default
region_name = china
project_name = service
username = neutron
password = neutron
service_metadata_proxy = true
metadata_proxy_shared_secret = openstack</pre>
<p>将ML2配置链接为Neutron插件主配置文件：</p>
<pre class="crayon-plain-tag">ln -s /etc/neutron/plugins/ml2/ml2_conf.ini /etc/neutron/plugin.ini</pre>
<p>初始化数据库： </p>
<pre class="crayon-plain-tag">su -s /bin/sh -c "neutron-db-manage --config-file /etc/neutron/neutron.conf \
  --config-file /etc/neutron/plugins/ml2/ml2_conf.ini upgrade head" neutron</pre>
<p>重启Nova： </p>
<pre class="crayon-plain-tag">systemctl restart openstack-nova-api.service</pre>
<p>启动Neutron：  </p>
<pre class="crayon-plain-tag">systemctl enable neutron-server.service \
  neutron-linuxbridge-agent.service neutron-dhcp-agent.service \
  neutron-metadata-agent.service
systemctl start neutron-server.service \
  neutron-linuxbridge-agent.service neutron-dhcp-agent.service \
  neutron-metadata-agent.service</pre>
<p>如果使用自服务网络选项，还需要启用L3服务： </p>
<pre class="crayon-plain-tag">systemctl enable neutron-l3-agent.service
systemctl start neutron-l3-agent.service</pre>
<div class="blog_h3"><span class="graybg">计算节点</span></div>
<p>安装软件： </p>
<pre class="crayon-plain-tag">dnf -y install openstack-neutron-linuxbridge ebtables ipset</pre>
<p>配置身份验证、消息队列： </p>
<pre class="crayon-plain-tag">[DEFAULT]
transport_url = rabbit://openstack:openstack@os.gmem.cc

auth_strategy = keystone


[keystone_authtoken]
www_authenticate_uri = http://os.gmem.cc:5000
auth_url = http://os.gmem.cc:5000
memcached_servers = os.gmem.cc:11211
auth_type = password
project_domain_name = default
user_domain_name = default
project_name = service
username = neutron
password = neutron

[oslo_concurrency]
lock_path = /var/lib/neutron/tmp</pre>
<p>配置网络选项。如果使用提供者网络：</p>
<ol>
<li> 配置Linux bridge Agent：<br />
<pre class="crayon-plain-tag">[linux_bridge]
physical_interface_mappings = provider:eth0

[vxlan]
enable_vxlan = false

[securitygroup]
enable_security_group = true
firewall_driver = neutron.agent.linux.iptables_firewall.IptablesFirewallDriver</pre>
</li>
<li>
<p>确保内核参数（通常需要保证内核模块br_netfilter已加载 ）：</p>
<pre class="crayon-plain-tag">net.bridge.bridge-nf-call-iptables 1
net.bridge.bridge-nf-call-ip6tables 1</pre>
</li>
</ol>
<p>如果使用自服务网络：
<ol>
<li> 配置Linux bridge Agent：<br />
<pre class="crayon-plain-tag">[linux_bridge]
physical_interface_mappings = provider:eth0

[vxlan]
enable_vxlan = true
local_ip = 10.1.0.11
l2_population = true

[securitygroup]
enable_security_group = true
firewall_driver = neutron.agent.linux.iptables_firewall.IptablesFirewallDriver</pre>
</li>
<li>
<p>同上 </p>
</li>
</ol>
<p>配置计算服务，让它使用网络服务：</p>
<pre class="crayon-plain-tag">[neutron]
auth_url = http://os.gmem.cc:5000
auth_type = password
project_domain_name = default
user_domain_name = default
region_name = china
project_name = service
username = neutron
password = neutron</pre>
<p>重启Nova：<pre class="crayon-plain-tag">systemctl restart openstack-nova-compute.service</pre></p>
<p>启动Linux bridge Agent：</p>
<pre class="crayon-plain-tag">systemctl enable neutron-linuxbridge-agent.service
systemctl start neutron-linuxbridge-agent.service</pre>
<div class="blog_h3"><span class="graybg">操作校验</span></div>
<p>列出已加载的扩展列表，确保Neutron相关进程启动：</p>
<pre class="crayon-plain-tag">openstack extension list --network</pre>
<p>校验Neutron代理都已经启动： </p>
<pre class="crayon-plain-tag">openstack network agent list</pre>
<p>应该启动的代理包括：</p>
<ol>
<li>控制节点的元数据代理、DHCP代理、Linux bridge代理</li>
<li>计算节点的Linux bridge代理</li>
<li>如果使用自服务网络，控制节点还有L3代理</li>
</ol>
<div class="blog_h2"><span class="graybg">安装Cinder</span></div>
<p>样例环境中，使用存储节点上的空白磁盘/dev/sdb ，基于LVM划分初逻辑卷，然后通过iSCSI协议暴露给虚拟机。</p>
<div class="blog_h3"><span class="graybg">控制节点</span></div>
<pre class="crayon-plain-tag">CREATE DATABASE IF NOT EXISTS cinder;

GRANT ALL PRIVILEGES ON cinder.* TO 'cinder'@'localhost' IDENTIFIED BY 'cinder';
GRANT ALL PRIVILEGES ON cinder.* TO 'cinder'@'%' IDENTIFIED BY 'cinder';</pre>
<p>创建用户和端点：</p>
<pre class="crayon-plain-tag">openstack user create --domain default --password cinder cinder

openstack role add --project service --user cinder admin

openstack service create --name cinderv2 --description "OpenStack Block Storage" volumev2
openstack service create --name cinderv3 --description "OpenStack Block Storage" volumev3

openstack endpoint create --region china volumev2 public http://os.gmem.cc:8776/v2/%\(project_id\)s
openstack endpoint create --region china volumev2 internal http://os.gmem.cc:8776/v2/%\(project_id\)s
openstack endpoint create --region china volumev2 admin http://os.gmem.cc:8776/v2/%\(project_id\)s

openstack endpoint create --region china volumev3 public http://os.gmem.cc:8776/v3/%\(project_id\)s
openstack endpoint create --region china volumev3 internal http://os.gmem.cc:8776/v3/%\(project_id\)s
openstack endpoint create --region china volumev3 admin http://os.gmem.cc:8776/v3/%\(project_id\)s</pre>
<p><span class="graybg">安装组件：</span></p>
<pre class="crayon-plain-tag">dnf -y install openstack-cinder</pre>
<p>修改配置文件：</p>
<pre class="crayon-plain-tag">[DEFAULT]
transport_url = rabbit://openstack:openstack@os.gmem.cc
auth_strategy = keystone
; 管理网络接口的IP地址
my_ip = 10.1.0.10

[database]
connection = mysql+pymysql://cinder:cinder@os.gmem.cc/cinder

[keystone_authtoken]
www_authenticate_uri = http://os.gmem.cc:5000
auth_url = http://os.gmem.cc:5000
memcached_servers = os.gmem.cc:11211
auth_type = password
project_domain_name = default
user_domain_name = default
project_name = service
username = cinder
password = cinder

[oslo_concurrency]
lock_path = /var/lib/cinder/tmp</pre>
<p>初始化数据库：</p>
<pre class="crayon-plain-tag">su -s /bin/sh -c "cinder-manage db sync" cinder</pre>
<p>配置Nova来使用存储服务： </p>
<pre class="crayon-plain-tag">[cinder]
os_region_name = china</pre>
<p>重新启动Nova：</p>
<pre class="crayon-plain-tag">systemctl restart openstack-nova-api.service</pre>
<p>启动Cinder服务： </p>
<pre class="crayon-plain-tag">systemctl enable openstack-cinder-api.service openstack-cinder-scheduler.service
systemctl start openstack-cinder-api.service openstack-cinder-scheduler.service</pre>
<div class="blog_h3"><span class="graybg">存储节点</span></div>
<p>在本样例环境下，需要LVM支持：</p>
<pre class="crayon-plain-tag">yum install lvm2 device-mapper-persistent-data

systemctl enable lvm2-lvmetad.service
systemctl start lvm2-lvmetad.service</pre>
<p>在/dev/sdb上创建LVM物理卷，并创建名为cinder-volumes的卷组：</p>
<pre class="crayon-plain-tag">pvcreate /dev/sdb

vgcreate cinder-volumes /dev/sdb</pre>
<p>只有虚拟机实例能够访问块设备卷。但是，存储节点的底层OS负责管理卷关联的块设备。默认情况下，LVM卷扫描工具会扫描/dev/目录来寻找包含卷的块设备。如果某个OpenStack项目使用基于LVM的卷，扫描工具会扫描卷并缓存结果，这可能导致很多问题。因此，你必须重新配置LVM，让它仅仅扫描包含cinder-volumes卷组的设备：</p>
<pre class="crayon-plain-tag">devices {
# a表示允许允许使用的卷，r表示拒绝使用的卷     
#                       拒绝所有其它的卷    
filter = [ "a/sdb/", "r/.*/"]</pre>
<p>安装组件： </p>
<pre class="crayon-plain-tag">dnf -y install openstack-cinder targetcli python3-keystone</pre>
<p>修改配置文件： </p>
<pre class="crayon-plain-tag">[DEFAULT]
transport_url = rabbit://openstack:openstack@os.gmem.cc
auth_strategy = keystone
; 此节点启用的存储后端
enabled_backends = lvm
; 配置镜像服务的API地址
glance_api_servers = http://os.gmem.cc:9292
; 管理网络接口的IP地址
my_ip = 10.1.0.11

[database]
connection = mysql+pymysql://cinder:cinder@os.gmem.cc/cinder

[keystone_authtoken]
www_authenticate_uri = http://os.gmem.cc:5000
auth_url = http://os.gmem.cc:5000
memcached_servers = os.gmem.cc:11211
auth_type = password
project_domain_name = default
user_domain_name = default
project_name = service
username = cinder
password = cinder

[oslo_concurrency]
lock_path = /var/lib/cinder/tmp

[lvm]
volume_driver = cinder.volume.drivers.lvm.LVMVolumeDriver
volume_group = cinder-volumes
target_protocol = iscsi
target_helper = lioadm</pre>
<p>启动服务：</p>
<pre class="crayon-plain-tag">systemctl enable openstack-cinder-volume.service target.service
systemctl start openstack-cinder-volume.service target.service</pre>
<div class="blog_h2"><span class="graybg">安装Horizon</span></div>
<p>OpenStack Dashboard组件，即Horizon，此组件仅仅依赖于Identity。</p>
<p>安装软件：</p>
<pre class="crayon-plain-tag">dnf -y install openstack-dashboard</pre>
<p>修改配置文件：</p>
<pre class="crayon-plain-tag">OPENSTACK_HOST = "os.gmem.cc"

WEBROOT = '/dashboard/'

# 允许哪些主机访问仪表盘
ALLOWED_HOSTS = ['os.gmem.cc']


# 配置基于Memcache的分布式会话存储
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
CACHES = {
    'default': {
         'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
         'LOCATION': 'os.gmem.cc:11211',
    }
}

# 启用Identity API v3
OPENSTACK_KEYSTONE_URL = "http://%s/identity/v3" % OPENSTACK_HOST

# 确保Domain支持
OPENSTACK_KEYSTONE_MULTIDOMAIN_SUPPORT = True

# 配置API版本
OPENSTACK_API_VERSIONS = {
    "identity": 3,
    "image": 2,
    "volume": 3,
}

# 默认访问的Domain
OPENSTACK_KEYSTONE_DEFAULT_DOMAIN = "Default"

# 通过仪表盘创建的用户的默认角色
OPENSTACK_KEYSTONE_DEFAULT_ROLE = "user"

# 配置网络
OPENSTACK_NEUTRON_NETWORK = {
    # 如果使用提供者网络，需要禁用router
    'enable_router': False,
    'enable_quotas': False,
    'enable_distributed_router': False,
    'enable_ha_router': False,
    'enable_lb': False,
    'enable_firewall': False,
    'enable_vpn': False,
    'enable_fip_topology_check': False,
}

TIME_ZONE = "Asia/Shanghai"</pre>
<p>修改配置文件：</p>
<pre class="crayon-plain-tag">WSGIDaemonProcess dashboard
WSGIProcessGroup dashboard
WSGISocketPrefix run/wsgi

WSGIApplicationGroup %{GLOBAL}

WSGIScriptAlias /dashboard /usr/share/openstack-dashboard/openstack_dashboard/wsgi/django.wsgi
Alias /dashboard/static /usr/share/openstack-dashboard/static

&lt;Directory /usr/share/openstack-dashboard/openstack_dashboard/wsgi&gt;
  Options All
  AllowOverride All
  Require all granted
&lt;/Directory&gt;

&lt;Directory /usr/share/openstack-dashboard/static&gt;
  Options All
  AllowOverride All
  Require all granted
&lt;/Directory&gt;</pre>
<p>重启服务：</p>
<pre class="crayon-plain-tag">systemctl restart httpd.service memcached.service</pre>
<p>此后你应该可以通过：http://os.gmem.cc访问仪表盘。</p>
<div class="blog_h3"><span class="graybg">启用HTTPS</span></div>
<p>修改horizon主配置文件：</p>
<pre class="crayon-plain-tag"># 添加以下配置
USE_SSL = True
CSRF_COOKIE_SECURE = True
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True</pre>
<p>修改Dashboard的httpd配置：</p>
<pre class="crayon-plain-tag">WSGISocketPrefix run/wsgi

&lt;VirtualHost *:443&gt;
  ServerName os.gmem.cc

  SSLEngine On
  SSLCertificateFile /etc/httpd/ssl/os.gmem.cc.crt
  SSLCACertificateFile /etc/httpd/ssl/os.gmem.cc.crt
  SSLCertificateKeyFile /etc/httpd/ssl/os.gmem.cc.key
  SetEnvIf User-Agent ".*MSIE.*" nokeepalive ssl-unclean-shutdown

  Header add Strict-Transport-Security "max-age=15768000"

  WSGIDaemonProcess dashboard
  WSGIProcessGroup dashboard

  WSGIApplicationGroup %{GLOBAL}

  WSGIScriptAlias /dashboard /usr/share/openstack-dashboard/openstack_dashboard/wsgi/django.wsgi
  Alias /dashboard/static /usr/share/openstack-dashboard/static

  &lt;Directory /usr/share/openstack-dashboard/openstack_dashboard/wsgi&gt;
    Options All
    AllowOverride All
    Require all granted
  &lt;/Directory&gt;

  &lt;Directory /usr/share/openstack-dashboard/static&gt;
    Options All
    AllowOverride All
    Require all granted
  &lt;/Directory&gt;
&lt;/VirtualHost&gt;</pre>
<p>安装必要的httpd模块：</p>
<pre class="crayon-plain-tag">dnf -y install mod_ssl </pre>
<div class="blog_h2"><span class="graybg">创建网络</span></div>
<div class="blog_h3"><span class="graybg">创建提供者网络</span></div>
<p>在启动实例之前，你需要创建必要的VNI。如果使用网络选项一，则虚拟机通过Provider（External）网络连接到PNI（基于bridging/switching）。</p>
<p>网络架构如下图：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2020/12/network1-overview.png"><img class="size-full wp-image-34955 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2020/12/network1-overview.png" alt="network1-overview" width="630" height="558" /></a></p>
<p>&nbsp;</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2020/12/network1-connectivity.png"><img class="size-full wp-image-34953 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2020/12/network1-connectivity.png" alt="network1-connectivity" width="817" height="451" /></a></p>
<p>使用下面的命令在OpenStack中创建一个名为provider的网络：</p>
<pre class="crayon-plain-tag"># share 表示允许所有project使用此虚拟网络
# external 表示网络是外部的，默认值internal
openstack network create  --share --external \
# 此虚拟网络所基于的物理网络
# 此名字对应的物理网络接口在linuxbridge_agent.ini中定义
#   physical_interface_mappings = provider:eth0
  --provider-physical-network provider \
# 此虚拟网络的物理实现机制（physical mechanism）
  --provider-network-type flat \
# 网络的名字
  provider</pre>
<p>为上述网络创建一个子网：</p>
<pre class="crayon-plain-tag">openstack subnet create --network provider \
# 子网中，用于分配给虚拟机实例的IP地址范围，此IP地址范围由DHCP agent管理
  --allocation-pool start=10.0.100.1,end=10.0.1.255 \
# DNS服务器地址              底层物理网络的网关地址
  --dns-nameserver 10.0.0.1 --gateway 10.0.0.1 \
# 底层物理网络的CIDR
  --subnet-range 10.0.0.0/16 provider</pre>
<div class="blog_h3"><span class="graybg">创建自服务网络</span></div>
<p>前面我们提到过两个网络选项，如果使用选项1，则参考上一小节的内容创建虚拟网络。如果使用选项2，则<span style="background-color: #c0c0c0;">在创建上述provider网络之后，还需要</span>参考本节内容。</p>
<p>网络架构如下：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2020/12/network2-overview.png"><img class="size-full wp-image-34951 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2020/12/network2-overview.png" alt="network2-overview" width="774" height="726" /></a></p>
<p>&nbsp;</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2020/12/network2-connectivity.png"><img class="size-full wp-image-34949 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2020/12/network2-connectivity.png" alt="network2-connectivity" width="753" height="898" /></a></p>
<p>使用选项2时，需要创建一个自服务（私有）的虚拟网络，并通过NAT连接到物理网络。此虚拟网络提供DHCP服务器，并且分配IP地址给实例。在这种私有网络中的实例可以直接访问外部网络（NAT），但是外部网络不能直接访问这些实例，除非给实例分配浮动IP。</p>
<p>修改环境变量，使用用户gmem来操作。</p>
<p>创建名为gmem的自服务网络：</p>
<pre class="crayon-plain-tag">openstack network create gmem-net</pre>
<p>非特权用户通常无法为上述命令指定额外的参数。OpenStack会依据下面的配置文件来自动选择参数：</p>
<pre class="crayon-plain-tag">[ml2]
tenant_network_types = vxlan

[ml2_type_vxlan]
vni_ranges = 1:1000</pre>
<p>创建子网： </p>
<pre class="crayon-plain-tag">openstack subnet create --network gmem-net \
#                           子网的网关地址，不是物理网络上的网关地址
  --dns-nameserver 10.1.0.1 --gateway 192.168.100.1 \
# 子网的CIDR
  --subnet-range 192.168.100.0/24 gmem-subnet</pre>
<p>自服务网络通过一个虚拟路由器，连接到提供者网络：</p>
<pre class="crayon-plain-tag">openstack router create gmem-router</pre>
<p>将上述子网添加为此虚拟路由器的一个接口（interface）： </p>
<pre class="crayon-plain-tag">openstack router add subnet gmem-router gmem-subnet</pre>
<p>将在提供者网络上的生成一个“网关“，连接到路由器：</p>
<pre class="crayon-plain-tag">openstack router set gmem-router --external-gateway provider</pre>
<p>到控制节点上进行校验，确保虚拟网络正常工作：</p>
<pre class="crayon-plain-tag"># 检查网络命名空间，应该至少看到：
#   一个qrouter开头的
#   二个qdhcp开头的
ip netns


# 列出路由器的端口，确定提供者网络上的网关地址
openstack port list --router router


# 确认可以从控制节点，或者物理网络上任何节点来访问上述网关地址  </pre>
<div class="blog_h2"><span class="graybg">创建Flavor</span></div>
<p>所谓Flavor就是虚拟机的规格，例如内存多大，CPU几个，磁盘几个，等等。最小的默认Flavor消耗512MB内存，这里我们创建一个仅消耗64MB内存的Flavor：</p>
<pre class="crayon-plain-tag">openstack flavor create --id 0 --vcpus 1 --ram 64 --disk 1 m1.nano</pre>
<div class="blog_h2"><span class="graybg">创建密钥</span></div>
<p>大部分的云镜像支持基于PKI的身份验证，而不是密码。在启动实例之前，我们需要为计算服务创建密钥： </p>
<pre class="crayon-plain-tag">openstack keypair create --public-key /home/alex/Documents/puTTY/gmem.crt default

openstack keypair list</pre>
<div class="blog_h2"><span class="graybg">添加安全组规则</span></div>
<p><span style="background-color: #c0c0c0;">默认的安全组应用到所有的实例， 此安全组禁止对实例的所有远程访问</span>。对于Linux镜像例如CirrOS，我们至少应该允许ICMP（ping）以及SSH：</p>
<pre class="crayon-plain-tag">openstack security group rule create --proto icmp default

openstack security group rule create --proto tcp --dst-port 22 default</pre>
<div class="blog_h2"><span class="graybg">创建虚拟机实例</span></div>
<p>列出可用的基础资源：</p>
<pre class="crayon-plain-tag">openstack flavor list
# +----+---------+-----+------+-----------+-------+-----------+
# | ID | Name    | RAM | Disk | Ephemeral | VCPUs | Is Public |
# +----+---------+-----+------+-----------+-------+-----------+
# | 0  | m1.nano |  64 |    1 |         0 |     1 | True      |
# +----+---------+-----+------+-----------+-------+-----------+

openstack image list
# +--------------------------------------+--------------------+--------+
# | ID                                   | Name               | Status |
# +--------------------------------------+--------------------+--------+
# | 5b337b93-96c6-4803-b0e2-bc0bce0afde9 | cirros-0.5.1-amd64 | active |
# +--------------------------------------+--------------------+--------+

openstack network list
# +--------------------------------------+----------+--------------------------------------+
# | ID                                   | Name     | Subnets                              |
# +--------------------------------------+----------+--------------------------------------+
# | 500cd78a-a05c-4b93-b399-06b26cc108de | provider | 9be1305c-a641-40f0-bd1b-6617e96025e6 |
# +--------------------------------------+----------+--------------------------------------+

openstack security group list
# +--------------------------------------+---------+------------------------+----------------------------------+------+
# | ID                                   | Name    | Description            | Project                          | Tags |
# +--------------------------------------+---------+------------------------+----------------------------------+------+
# | f5b0e967-5903-4b6b-9b9d-8e39a07b52da | default | Default security group | e1c4a3403e1b46cd969e4d626b5cf799 | []   |
# +--------------------------------------+---------+------------------------+----------------------------------+------+</pre>
<div class="blog_h3"><span class="graybg">在提供者网络上</span></div>
<pre class="crayon-plain-tag">openstack server create --flavor m1.nano --image cirros-0.5.1-amd64 \
  --nic net-id=500cd78a-a05c-4b93-b399-06b26cc108de --security-group default \
  --key-name default cirros-amd64-0</pre>
<p>当虚拟机构建完毕后，其状态会从BUILD变为ACTIVE：</p>
<pre class="crayon-plain-tag">openstack server list </pre>
<p>CirrOS的默认用户密码是cirros / gocubsgo，确认可以登陆。</p>
<p>在虚拟机的宿主机上，可以看到OpenStack创建了一个网桥，连接了提供者物理网络和虚拟机（的tap设备）：</p>
<pre class="crayon-plain-tag">brctl show
# bridge name     bridge id               STP enabled     interfaces
# brq1305444e-cf          8000.100000000012       no              eth0
#                                                         tapa87f2880-fc </pre>
<div class="blog_h1"><span class="graybg">Keystone</span></div>
<div class="blog_h2"><span class="graybg">简介</span></div>
<p>Keystone是OpenStack的Identity服务，它负责身份<span style="background-color: #c0c0c0;">验证、授权，以及一系列其它服务</span>。Keystone可以集成到外部的用户管理系统，例如LDAP。</p>
<p>Keystone通常是用户首先与之交互的服务，随后用户使用他的Identity来访问其它OpenStack服务。OpenStack组件也需要访问Keystone来验证用户声明的Identity是否合法。</p>
<p>用户或者服务，可以利用服务目录来得到其它服务（Openstack组件）的位置，<span style="background-color: #c0c0c0;">服务目录（Service catalog）也是Keystone提供</span>的服务之一。</p>
<p>每个服务可以提供1-N个端点，每个端点可以是admin/internal（可能仅限于OpenStack所在主机访问）/public（可能允许Internet访问）三种类型之一。在生产环境中，这些不同类型的端点可能出于安全方面的考虑，存放在不同的网络中，供不同类型的用户访问。</p>
<div class="blog_h2"><span class="graybg">权限模型</span></div>
<div class="blog_h3"><span class="graybg">User</span></div>
<p>代表单个API消费者：</p>
<ol>
<li>用户就是一个有身份验证信息的API消费实体</li>
<li>用户可以属于多个项目/角色</li>
</ol>
<div class="blog_h3"><span class="graybg">Group</span></div>
<p>代表一组User的集合。</p>
<div class="blog_h3"><span class="graybg">Project</span></div>
<p>代表OpenStack中基本的所有权单元 —— OpenStack中各种<span style="background-color: #c0c0c0;">计算资源都是归属于某个特定项目</span>的。而Project则是归属于某个Domain的。</p>
<p><span style="background-color: #c0c0c0;">租户（Tenant）在OpenStack中，就是项目，其目的就是隔离计算资源</span>。</p>
<div class="blog_h3"><span class="graybg">Domin</span></div>
<p>是Project、User、Role、Group的高层次容器，后面三者仅仅属于唯一一个domain。Keystone默认提供一个名为Defulat的domain。</p>
<div class="blog_h2"><span class="graybg">架构</span></div>
<p>Keystone由三类组件构成。</p>
<div class="blog_h3"><span class="graybg">Server</span></div>
<p>中心化的HTTP服务器，对外提供鉴权的RESTful接口</p>
<div class="blog_h3"><span class="graybg">Drivers</span></div>
<p>集成在Server里面，用于访问存放在OpenStack外部（例如LDAP或MySQL数据库）的身份信息。</p>
<div class="blog_h3"><span class="graybg">Modules</span></div>
<p>运行在使用Identity service的那些OpenStack组件中，负责拦截针对这些组件的请求，抽取用户凭证信息，发送到上述Server执行鉴权。</p>
<p>这些Modules基于Python Web Server Gateway Interface和OpenStack组件集成。</p>
<div class="blog_h2"><span class="graybg">Token选型</span></div>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;"><strong>Token类型</strong></td>
<td style="text-align: center;"><strong>UUID</strong></td>
<td style="text-align: center;"><strong>PKI</strong></td>
<td style="text-align: center;"><strong>PKIZ</strong></td>
<td style="text-align: center;"><strong>Fernet</strong></td>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left;">大小</td>
<td style="text-align: left;">32 Byte</td>
<td style="text-align: left;">KB 级别</td>
<td style="text-align: left;">KB 级别</td>
<td style="text-align: left;">约 255 Byte</td>
</tr>
<tr>
<td style="text-align: left;">支持本地认证</td>
<td style="text-align: left;">不支持</td>
<td style="text-align: left;">支持</td>
<td style="text-align: left;">支持</td>
<td style="text-align: left;">不支持</td>
</tr>
<tr>
<td style="text-align: left;">Keystone 负载</td>
<td style="text-align: left;">大</td>
<td style="text-align: left;">小</td>
<td style="text-align: left;">小</td>
<td style="text-align: left;">大</td>
</tr>
<tr>
<td style="text-align: left;">存储于数据库</td>
<td style="text-align: left;">是</td>
<td style="text-align: left;">是</td>
<td style="text-align: left;">是</td>
<td style="text-align: left;">否</td>
</tr>
<tr>
<td style="text-align: left;">携带信息</td>
<td style="text-align: left;">无</td>
<td style="text-align: left;">user, catalog 等</td>
<td style="text-align: left;">user, catalog 等</td>
<td style="text-align: left;">user 等</td>
</tr>
<tr>
<td style="text-align: left;">涉及加密方式</td>
<td style="text-align: left;">无</td>
<td style="text-align: left;">非对称加密</td>
<td style="text-align: left;">非对称加密</td>
<td style="text-align: left;">对称加密(AES)</td>
</tr>
<tr>
<td style="text-align: left;">是否压缩</td>
<td style="text-align: left;">否</td>
<td style="text-align: left;">否</td>
<td style="text-align: left;">是</td>
<td style="text-align: left;">否</td>
</tr>
<tr>
<td style="text-align: left;">版本支持</td>
<td style="text-align: left;">D</td>
<td style="text-align: left;">G</td>
<td style="text-align: left;">J</td>
<td style="text-align: left;">K</td>
</tr>
</tbody>
</table>
<p>K版本之后，通常选择Fernet Token。</p>
<div class="blog_h1"><span class="graybg">Nova</span></div>
<p>Nova，即OpenStack Compute组件，负责host和管理云主机，是IaaS系统的核心组成部分。可以管理的云主机类型包括虚拟机、物理机（依赖ironic），并对容器提供有限的支持。</p>
<p>Nova和Keystone交互进行身份验证。<span style="background-color: #c0c0c0;">Placement负责计算资源的跟踪和选择</span>（作为实例的宿主），Glance提供虚拟机镜像，这些组件配合就能让实例运行起来。</p>
<p>OpenStack自身不提供虚拟化软件，而是通过“驱动”和底层的Hypervisor进行交互。交互工作主要由Nova完成。</p>
<div class="blog_h2"><span class="graybg">架构</span></div>
<p>Nova由一系列分布式的组件构成，每种组件具有自己的职责。面向用户的是一个REST API。内部组件主要通过RPC消息传递机制通信。</p>
<p>API组件监听到请求后，<span style="background-color: #c0c0c0;">通常会进行数据库读写操作，可选的，会将RPC消息发送给其它Nova组件，然后将REST响应发给客户端</span>。RPC消息是通过 <span style="background-color: #c0c0c0;">oslo.messaging 库完成的，这个库是基于消息队列的抽象</span>。</p>
<p><span style="background-color: #c0c0c0;">大部分组件可以运行在多台宿主机上，并且其中具有一个manager负责监听RPC消息、执行一些周期性工作</span>。一个主要的例外是nova-compute，此组件和它管理的Hypervisor（除了VMware或Ironic驱动）对应，每个Hypervisor对应一个nova-compute。</p>
<p>Nova具有一个逻辑的、中心化的、被所有组件共享的数据库。为了辅助OpenStack升级，<span style="background-color: #c0c0c0;">对数据库的访问基于一个对象层</span>，此对象层确保一个升级后的控制平面，仍然能和低版本的nova-compute进行交互。具体实现上，nova-compute通过中心化的nova-conductor间接的访问数据库，后者提供基于RPC的接口。</p>
<p><img src="https://blog.gmem.cc/wp-content/uploads/2021/01/nova-arch.svg" alt="" width="1038" height="860" /></p>
<div class="blog_h3"><span class="graybg">nova-api服务</span></div>
<p>安装在控制节点上。提供OpenStack Compute API，负责确保一些策略，<span style="background-color: #c0c0c0;">发起大部分编排活动（例如运行实例）</span>。</p>
<div class="blog_h3"><span class="graybg">nova-api-metadata服务</span></div>
<p>安装在控制节点上。处理处理获取实例元数据的请求。</p>
<div class="blog_h3"><span class="graybg">nova-compute服务</span></div>
<p>安装在计算节点上。Worker守护进程，负责调用Hypervisor API，在计算节点上创建、终结虚拟机实例。支持的Hypervisor API包括：</p>
<ol>
<li>XenAPI for XenServer/XCP</li>
<li>libvirt for KVM or QEMU</li>
<li>VMwareAPI for VMware</li>
</ol>
<p>该组件的大概工作流程是：</p>
<ol>
<li>从消息队列里接受Action</li>
<li>调用一系列的系统命令，启动虚拟机</li>
<li>在数据库中更新实例状态</li>
</ol>
<div class="blog_h3"><span class="graybg">nova-scheduler服务</span></div>
<p>从队列中取出虚拟机实例的请求，然后决定将其调度在哪台计算节点上。</p>
<div class="blog_h3"><span class="graybg">nova-conductor模块</span></div>
<p>协调nova-compute s服务和数据库之间的交互。避免nova-compute直接访问数据库。该模块支持水平扩容，<span style="background-color: #c0c0c0;">避免将其部署在nova-compute运行的节点</span>。</p>
<div class="blog_h3"><span class="graybg">nova-novncproxy进程</span></div>
<p>提供一个代理，用于通过VNC协议连接到运行中的实例。</p>
<div class="blog_h3"><span class="graybg">nova-spicehtml5proxy进程</span></div>
<p>提供一个代理，用于通过SPICE协议连接到运行中的实例，支持HTML5客户端。</p>
<div class="blog_h3"><span class="graybg">placenment服务</span></div>
<p>这个服务目前已经独立出去，它负责跟踪资源库存、使用情况。</p>
<div class="blog_h3"><span class="graybg">消息队列</span></div>
<p>这是一个中心化的Hub，用于在不同组件之间传递消息。通常使用RabbitMQ。</p>
<div class="blog_h3"><span class="graybg">数据库</span></div>
<p>存储云基础设施的构建时、运行时状态，包括：</p>
<ol>
<li>可用的实例类型</li>
<li>使用着哦个的实例</li>
<li>可用的网络</li>
<li>项目</li>
<li>……</li>
</ol>
<p>任何SQLAlchemy支持的RDBMS都可以。</p>
<div class="blog_h2"><span class="graybg">Cell简介</span></div>
<p>为了支持Nova部署的水平扩展，Nova引入了分片机制，每个分片叫<a href="https://docs.openstack.org/nova/latest/user/cells.html">Cell</a>。利用Cell：</p>
<ol>
<li>可以在<span style="background-color: #c0c0c0;">单个Region中将计算节点的数量扩容到成千上万</span>。每个Cell具有自己的数据库、消息队列，这是可扩容的关键</li>
<li>实现故障隔离的特性（一个Cell故障，其它Cell还可以正常工作）</li>
<li>作为一种分组机制，可以将类似的硬件放在同一Cell中</li>
</ol>
<div class="blog_h3"><span class="graybg">Cell V1</span></div>
<p>Cell V1的特性：</p>
<ol>
<li>捕获并中继消息给Cell</li>
<li>处理竞态条件</li>
<li>两级调度架构</li>
</ol>
<p>Cell V1的缺点：</p>
<ol>
<li>不支持安全组、主机聚合、可用区等特性</li>
<li>顶级调度功能很弱</li>
</ol>
<div class="blog_h3"><span class="graybg">Cell V2</span></div>
<p>当Nova API接收到针对实例的前请求后，实例的信息将从数据库读取，其中包含实例的宿主机名字。如果需要针对实例进行其它操作（通常都需要），那么宿主机名字将用来计算出消息队列的名字，RPC消息随后被写入消息队列，可以到达正确的计算节点。</p>
<p>引入Cell后，上述逻辑将变成：</p>
<ol>
<li>查找实例的三元组：宿主机名称、数据库连接信息、消息队列连接信息</li>
<li>连接到数据库，获取实例记录</li>
<li>连接到消息队列，并根据宿主机名称，选额消息队列，发送RPC消息</li>
</ol>
<p>引入Cell V2后，不存在没有Cell的部署架构。Cell V2的优势包括：</p>
<ol>
<li>数据库、消息队列的分片，成为Nova的一等特性</li>
<li>不需要在顶级复制Cell数据库，Nova API需要自己的数据库，存放例如实例索引的信息</li>
<li>在gloabl和local数据元素之间划分好了界限。Flavor、Keypair之类的全局性质对象，仅仅需要存储在顶级。这样计算节点更加无状态化，不会被全局数据的修改所干扰</li>
</ol>
<div class="blog_h2"><span class="graybg">Cell部署</span></div>
<p>所有Nova部署中，都需要一个名为API的数据库，一个<span style="background-color: #c0c0c0;">特殊的Cell数据库cell0</span>，1-N个cell的数据库。高层次的跟踪信息存放在API数据库中，哪些<span style="background-color: #c0c0c0;">从未调度成功的实例，存放在cell0</span>。所有成功调度的/运行的实例，都放在其它cell数据库中。</p>
<p>你需要将API数据库的信息配置在nova.conf：</p>
<pre class="crayon-plain-tag">[api_database]
connection = mysql+pymysql://nova:nova@os.gmem.cc/nova_api?charset=utf8</pre>
<div class="blog_h3"><span class="graybg">cell0</span></div>
<p>由于cell数据库数量不定，此外任何部署都至少有cell0和cell1（唯一的Cell使用），因此这些Cell的连接信息，是写在API数据库中（而不是静态编写在文件）的。</p>
<pre class="crayon-plain-tag"># 后续命令需要读写API数据库，因此首先执行api_db sync子命令来初始化schema
su -s /bin/sh -c "nova-manage api_db sync" nova

# 为cell0的数据库创建记录
nova-manage cell_v2 map_cell0 --database_connection \
  mysql+pymysql://nova:nova@os.gmem.cc/nova_cell0
# 如果不指定--database_connection，就像样例环境那样，则自动使用[database]/connection字段
# 中的连接串，但是在结尾添加_cell0后缀</pre>
<p>由于cell0中不会存在任何宿主机，不需要对它进行进一步配置。</p>
<div class="blog_h3"><span class="graybg">常规cell</span></div>
<p>现在，你需要创建第一个常规cell： </p>
<pre class="crayon-plain-tag"># 如果不指定 --database_connection、--transport-url，就像样例环境那样，
# 则自动使用 [database]/connection 和 [DEFAULT]/transport_url
su -s /bin/sh -c "nova-manage cell_v2 create_cell --name=cell1 --verbose" nova

nova-manage cell_v2 create_cell --verbose --name cell1 \
  --database_connection mysql+pymysql://nova:nova@os.gmem.cc/nova
  --transport-url rabbit://openstack:openstack@os.gmem.cc</pre>
<p>如果为cell1准备的数据库是空白的，你需要同步数据库schema：</p>
<pre class="crayon-plain-tag">su -s /bin/sh -c "nova-manage db sync" nova</pre>
<p>现在，cell1中没有任何宿主机，因此nova-scheduler不会把实例调度到此cell中。</p>
<p>使用下面的命令，可以扫描数据库中计算节点的记录，并将其添加到刚刚创建的cell中。执行命令之前<span style="background-color: #c0c0c0;">，至少需要安装一个计算节点并<strong><span style="background-color: #99cc00;">添加到cell</span></strong></span>。</p>
<pre class="crayon-plain-tag">nova-manage cell_v2 discover_hosts</pre>
<p>上述命令会连接到所有cell数据库，扫描将自己注册到cell的宿主机，然后在API数据库中映射这些宿主机，这样nova-scheduler就可以进行调度了。</p>
<p>任何时候，你加入新的宿主机到cell，都需要调用此命令（或者启用自动发现）。</p>
<div class="blog_h3"><span class="graybg">添加宿主机到cell</span></div>
<p>我们知道计算节点通过消息队列和控制平面通信，也不会直接访问数据库。实际上，计算节点属于哪个cell，就看它通过哪个消息队列连接。你只需要配置计算节点的nova.conf：</p>
<pre class="crayon-plain-tag">[DEFAULT]
; 设置为某个cell的--transport-url 
transport_url = rabbit://openstack:openstack@os.gmem.cc</pre>
<p>然后再启动nova-compute服务，最后验证、确保节点在下面命令的输出中： </p>
<pre class="crayon-plain-tag">nova service-list --binary nova-compute</pre>
<p>就将计算节点<strong><span style="background-color: #99cc00;">添加到cell</span></strong>中了。 </p>
<div class="blog_h3"><span class="graybg">自动发现</span></div>
<p>要实现自动发现添加到cell的计算节点，并通过到API数据库，可以配置所有nova-scheduler节点的nova.conf：</p>
<pre class="crayon-plain-tag">[scheduler]
; 300秒执行一次发现
discover_hosts_in_cells_interval = 300 </pre>
<div class="blog_h2"><span class="graybg">多Cell陷阱</span></div>
<div class="blog_h3"><span class="graybg">跨Cell迁移实例</span></div>
<p>到目前为止（V版），还不支持跨Cell迁移实例。影响的操作包括resize/evacuate/migrate等。</p>
<div class="blog_h3"><span class="graybg">Quota计算</span></div>
<p>如果Cell不可达，那么针对租户的用量统计信息可能不准确。</p>
<p>从T版开始，可以配置在Placement服务+API数据库上进行Quota统计，这样宕掉/性能很差的Cell不会导致用量统计不准确。</p>
<div class="blog_h3"><span class="graybg">列出实例</span></div>
<p>多Cell环境下，列出实例的结果可能没有排序、分页可能不正确。</p>
<div class="blog_h3"><span class="graybg">元数据服务</span></div>
<p>从S版开始，元数据服务可以运行为两种模式之一：全局/PerCell。使用api.local_metadata_per_cell配置项</p>
<div class="blog_h2"><span class="graybg">Metadata</span></div>
<p>Nova将它启动的<span style="background-color: #c0c0c0;">实例的配置信息呈现为元数据</span>。cloud-init之类的助手会在虚拟机初始化时，利用元数据进行配置工作，例如设置虚拟机root密码。</p>
<p>通过元数据服务、或者config drive，元数据变的可（被实例使）用。你还可以通过nova api的user data特性来定制实例的元数据。</p>
<div class="blog_h3"><span class="graybg">元数据类别</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 150px; text-align: center;">类别</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>用户提供</td>
<td>
<p>创建实例的用户可以通过多种方式，将元数据传递给实例：</p>
<ol>
<li>实例nova api的keypairs功能，可以设置宿主机的登陆密钥</li>
<li>使用nova api的<a href="https://docs.openstack.org/nova/latest/user/metadata.html#metadata-userdata">user data</a>特性，可以传递一小块opaque blob</li>
</ol>
</td>
</tr>
<tr>
<td>Nova提供</td>
<td>
<p>Nova自身会添加一些元数据，例如实例所在宿主机名称、实例所在AZ。</p>
<p>Nova提供OpenStack metadata API，以及EC2-compatible API。两者都是以日期来版本化的</p>
</td>
</tr>
<tr>
<td>部署者提供</td>
<td>
<p>对于创建实例的用户来说未知，由OpenStack的部署者提供。通过<a href="https://docs.openstack.org/nova/latest/user/metadata.html#metadata-vendordata">vendordata</a>特性可以实现。用于实现在实例创建后，自动加入AD这样的网络管理类功能</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">元数据代理</span></div>
<p>控制节点上的Neutron元数据代理服务，负责代替虚拟机Nova metadata API。并自动设置正确的Instance ID、Tenant ID等请求头。</p>
<div class="blog_h3"><span class="graybg">元数据服务</span></div>
<p>元数据服务<span style="background-color: #c0c0c0;">为实例提供了一种获取自身元数据</span>的REST API。实例可以通过169.254.169.254或者fe80::a9fe:a9fe访问此REST API，如此奇怪的地址是兼容Amazon EC2的考虑。在Openstack里面，这两个IP通过iptables映射到控制节点。</p>
<p>所有上述三类元数据，都可以通过此REST API访问：</p>
<pre class="crayon-plain-tag"># OpenStack metadata API
curl http://169.254.169.254/openstack
# EC2-compatible API
curl http://169.254.169.254
# 都会显示若干目录（版本信息）
# 2012-08-10
# 2013-04-04
# 2013-10-17
# 2015-10-15
# 2016-06-30
# 2016-10-06
# 2017-02-22
# 2018-08-27
# latest</pre>
<div class="blog_h3"><span class="graybg">config drive</span></div>
<p>Config drive是一种特殊的drive，在实例boot时添加。实例可以挂载此drive，读取其中的文件，从而获取（通常应该从metadata service获取的）信息。</p>
<p>下面的命令示意了如何使用在创建实例时使用config drive：</p>
<pre class="crayon-plain-tag">#                       使用config drive
openstack server create --config-drive true --image my-image-name \
#                               传递一个user data文件
    --flavor 1 --key-name mykey --user-data ./my-user-data.txt \
#   传递两个元数据键值对
    --property role=webservers --property essential=false MYINSTANCE</pre>
<p>如果客户机操作系统支持udev，则可以这样挂载config drive： </p>
<pre class="crayon-plain-tag">mkdir -p /mnt/config
mount /dev/disk/by-label/config-2 /mnt/config</pre>
<p>否则，这样识别config drive对应的块设备：</p>
<pre class="crayon-plain-tag">blkid -t LABEL="config-2" -odevice
# /dev/vdb</pre>
<p>config drive中的文件目录结构，和metadata service的URL结构对应：</p>
<pre class="crayon-plain-tag">cd /mnt/config
find . -maxdepth 2
# .
# EC2兼容的元数据放在这里
# ./ec2
# ./ec2/2009-04-04
# ./ec2/latest
# OpenStack元数据放在这里
# ./openstack
# ./openstack/2012-08-10
# ./openstack/2013-04-04
# ./openstack/2013-10-17
# ./openstack/2015-10-15
# ./openstack/2016-06-30
# ./openstack/2016-10-06
# ./openstack/2017-02-22
# ./openstack/latest</pre>
<div class="blog_h3"><span class="graybg">OpenStack元数据格式 </span></div>
<p>OpenStack元数据基于JSON格式分发： </p>
<ol>
<li>meta_data.json：提供Nova相关的信息</li>
<li> network_data.json：提供从Neutron获取的，网络相关信息</li>
</ol>
<p>示例：</p>
<pre class="crayon-plain-tag">// curl http://169.254.169.254/openstack/2018-08-27/meta_data.json
{
   "random_seed": "yu5ZnkqF2CqnDZVAfZgarG...",
   "availability_zone": "nova",
   "keys": [
       {
         "data": "ssh-rsa AAAAB3NzaC1y...== Generated by Nova\n",
         "type": "ssh",
         "name": "mykey"
       }
   ],
   "hostname": "test.novalocal",
   "launch_index": 0,
   "meta": {
      "priority": "low",
      "role": "webserver"
   },
   "devices": [
       {
         "type": "nic",
         "bus": "pci",
         "address": "0000:00:02.0",
         "mac": "00:11:22:33:44:55",
         "tags": ["trusted"]
       },
       {
         "type": "disk",
         "bus": "ide",
         "address": "0:0",
         "serial": "disk-vol-2352423",
         "path": "/dev/sda",
         "tags": ["baz"]
       }
   ],
   "project_id": "f7ac731cc11f40efbc03a9f9e1d1d21f",
   "public_keys": {
       "mykey": "ssh-rsa AAAAB3NzaC1y...== Generated by Nova\n"
   },
   "name": "test"
}


// curl http://169.254.169.254/openstack/2018-08-27/network_data.json
{
    "links": [
        {
            "ethernet_mac_address": "fa:16:3e:9c:bf:3d",
            "id": "tapcd9f6d46-4a",
            "mtu": null,
            "type": "bridge",
            "vif_id": "cd9f6d46-4a3a-43ab-a466-994af9db96fc"
        }
    ],
    "networks": [
        {
            "id": "network0",
            "link": "tapcd9f6d46-4a",
            "network_id": "99e88329-f20d-4741-9593-25bf07847b16",
            "type": "ipv4_dhcp"
        }
    ],
    "services": [
        {
            "address": "8.8.8.8",
            "type": "dns"
        }
    ]
}</pre>
<div class="blog_h3"><span class="graybg">EC2兼容元数据格式</span></div>
<p>兼容Amazon EC2 metadata service 2009-04-04版本。这意味着，为EC2设计的虚拟机镜像，可以和OpenStack一起工作。</p>
<p>EC2 API为每个元数据暴露了独立的URL：</p>
<pre class="crayon-plain-tag"># curl http://169.254.169.254/2009-04-04/meta-data/
ami-id
ami-launch-index
ami-manifest-path
block-device-mapping/
hostname
instance-action
instance-id
instance-type
kernel-id
local-hostname
local-ipv4
placement/
public-hostname
public-ipv4
public-keys/
ramdisk-id
reservation-id
security-groups

# curl http://169.254.169.254/2009-04-04/meta-data/block-device-mapping/
ami

# curl http://169.254.169.254/2009-04-04/meta-data/placement/
availability-zone

# curl http://169.254.169.254/2009-04-04/meta-data/public-keys/
0=mykey

# curl http://169.254.169.254/2009-04-04/meta-data/public-keys/0/openssh-key
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDYVEprvtYJXVOBN0XNKVVRNCRX6BlnNbI+US\
LGais1sUWPwtSg7z9K9vhbYAPUZcq8c/s5S9dg5vTHbsiyPCIDOKyeHba4MUJq8Oh5b2i71/3B\
ISpyxTBH/uZDHdslW2a+SrPDCeuMMoss9NFhBdKtDkdG9zyi0ibmCP6yMdEX8Q== Generated\
by Nova</pre>
<div class="blog_h3"><span class="graybg">user-data</span></div>
<p>就是一小段blob，OpenStack不知道它内容的意义。传递user data： </p>
<pre class="crayon-plain-tag">openstack server create --image ubuntu-cloudimage --flavor 1 \
    --user-data mydata.file TEST</pre>
<p>在客户机里面访问user data： </p>
<pre class="crayon-plain-tag"># OpenStack
http://169.254.169.254/openstack/{version}/user_data
# EC2
http://169.254.169.254/{version}/user-data</pre>
<p>支持cloud-init的镜像，可以利用user data，定制实例的初始化过程。</p>
<div class="blog_h3"><span class="graybg">vendor-data</span></div>
<p>这类数据可以通过metadata service或config drive读取。对于前者： </p>
<pre class="crayon-plain-tag">// curl http://169.254.169.254/openstack/2018-08-27/vendor_data2.json
{
    "testing": {
        "value1": 1
    }
}</pre>
<div class="blog_h2"><span class="graybg">cloud-init</span></div>
<p>cloud-init是一个在主要Linux发行版中都支持的包，用于在云环境（例如OpenStack）中初始化虚拟机实例。cloud-init在虚拟机第一次运行时执行。</p>
<div class="blog_h3"><span class="graybg">启动过程</span></div>
<p>cloud-init集成到系统启动的五个Stage，以发挥作用：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 120px; text-align: center;">Stage</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>Generator</td>
<td>
<p>基于Systemd启动时，此阶段中有一个Generator用于确认cloud-init.target十分需要包含在Boot Goals中。默认情况下此Generator会启用cloud-init，以下情形之一，则不启用：</p>
<ol>
<li>文件<pre class="crayon-plain-tag">/etc/cloud/cloud-init.disabled</pre>存在</li>
<li>内核命令行参数<pre class="crayon-plain-tag">/proc/cmdline</pre>包含<pre class="crayon-plain-tag">cloud-init=disabled</pre></li>
</ol>
</td>
</tr>
<tr>
<td>Local</td>
<td>
<p>此Stage运行一个Systemd服务cloud-init-local.service。该服务在 / 挂载为读写后立即执行，block尽可能多的Systemd启动单元，必须block网络</p>
<p>该Stage的意图包括：</p>
<ol>
<li>定位到“local”数据源</li>
<li>应用网络配置到系统（包含Fallback）。网络配置的来源可能包括：
<ol>
<li>datasource，云环境通过元数据（Metadata）提供的网络配置信息</li>
<li>fallback，cloud-init的备用网络配置，等价于dhcp on eth0</li>
<li>none，如果/etc/cloud/cloud.cfg包含配置<pre class="crayon-plain-tag">network: {config: disabled}</pre></li>
</ol>
</li>
</ol>
</td>
</tr>
<tr>
<td>Network</td>
<td>
<p>此Stage运行一个Systemd服务cloud-init.service。该服务在Local Stage之后，所有网络启动后运行。包含的模块定义在/etc/cloud/cloud.cfg中</p>
<p>此Stage会处理所有user-data，所谓处理指：</p>
<ol>
<li>递归的读取<pre class="crayon-plain-tag">#include</pre>或<pre class="crayon-plain-tag">#include-once</pre></li>
<li>展开所有压缩内容</li>
<li>运行发现的所有<pre class="crayon-plain-tag">part-handler</pre></li>
</ol>
<p>此Stage会运行disk_setup、mounts模块，从而进行分区、格式化、配置挂载点（/etc/fstab）。这些模块不能运行的更早，韵味可能依赖于需要从网络才能得到的配置输入，例如用户提供的位于网络资源中的user-data</p>
</td>
</tr>
<tr>
<td>Config</td>
<td>
<p>此Stage运行一个Systemd服务cloud-config.service。包含的模块定义在/etc/cloud/cloud.cfg中</p>
<p>对于其他Boot Stage不产生影响的模块运行在此Stage</p>
</td>
</tr>
<tr>
<td>Final</td>
<td>
<p>此Stage运行一个Systemd服务cloud-final.service。它的运行时机相当于rc.local，也就是在启动的最后阶段。可以做的事情包括：</p>
<ol>
<li>安装软件包</li>
<li>执行用户定义的、通过user-data传递的脚本</li>
</ol>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">判断首次启动</span></div>
<p>cloud-init需要判断实例是否第一次启动。在第一次启动时，会运行<pre class="crayon-plain-tag">per-instance</pre>的配置；在后续启动时，仅运行<pre class="crayon-plain-tag">per-boot</pre>的配置。</p>
<p>在运行的时候，cloud-init会将内部状态缓存起来，共后续boot读取。缓存存在，意味着两种情况之一：</p>
<ol>
<li>实例不是第一次启动</li>
<li>文件系统被挂载给一个新实例，该实例是第一次启动。将一个OpenStack卷上传为镜像，然后从此镜像启动新实例，就会导致这种情况</li>
</ol>
<p>默认情况下，cloud-init检查缓存中的实例ID，和运行时获取的实例ID，来判断是上述两种情况的哪一种。</p>
<p>使用命令<pre class="crayon-plain-tag">cloud-init clean</pre>可以清空缓存。</p>
<div class="blog_h3"><span class="graybg">配置</span></div>
<p>cloud-init的主配置文件位于<pre class="crayon-plain-tag">/etc/cloud/cloud.cfg</pre>，<pre class="crayon-plain-tag">/etc/cloud/cloud.cfg.d</pre>目录中的所有其它文件都会被合并。CentOS 8云镜像的配置文件如下：</p>
<pre class="crayon-plain-tag"># 需要添加到系统中的用户，特殊值default特指system_info.default_user
users:
 - default

# 禁止root登陆
disable_root: 1
# 禁用密码登陆
ssh_pwauth:   0

mount_default_fields: [~, ~, 'auto', 'defaults,nofail,x-systemd.requires=cloud-init.service', '0', '2']
resize_rootfs_tmp: /dev
ssh_deletekeys:   1
ssh_genkeytypes:  ~
syslog_fix_perms: ~
disable_vmware_customization: false

# 每个Stage都包含一系列的模块，可以启用/禁用
cloud_init_modules:
 - disk_setup
 - migrator
 - bootcmd
 - write-files
 - growpart
 - resizefs
 - set_hostname
 - update_hostname
 - update_etc_hosts
 - rsyslog
 - users-groups
 - ssh

cloud_config_modules:
 - mounts
 - locale
 - set-passwords
 - rh_subscription
 - yum-add-repo
 - package-update-upgrade-install
 - timezone
 - puppet
 - chef
 - salt-minion
 - mcollective
 - disable-ec2-metadata
 - runcmd

cloud_final_modules:
 - rightscale_userdata
 - scripts-per-once
 - scripts-per-boot
 - scripts-per-instance
 - scripts-user
 - ssh-authkey-fingerprints
 - keys-to-console
 - phone-home
 - final-message
 - power-state-change

system_info:
  # 默认用户信息
  default_user:
    name: centos
    lock_passwd: true
    gecos: Cloud User
    groups: [adm, systemd-journal]
    sudo: ["ALL=(ALL) NOPASSWD:ALL"]
    shell: /bin/bash
  distro: rhel
  paths:
    cloud_dir: /var/lib/cloud
    templates_dir: /etc/cloud/templates
  ssh_svcname: sshd

# vim:syntax=yaml</pre>
<div class="blog_h3"><span class="graybg">user-data</span></div>
<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>Gzip压缩内容</td>
<td>任何user-data都可以压缩为gzip格式，cloud-init会解压它，然后再处理</td>
</tr>
<tr>
<td>MIME Multi Part Archive</td>
<td>使用此格式，用户可以指定多种类型的数据，例如同时指定一个user-data script和cloud-config。支持的类型包括：<br />
<pre class="crayon-plain-tag"># cloud-init devel make-mime --list-types
cloud-boothook
cloud-config
cloud-config-archive
cloud-config-jsonp
jinja2
part-handler
upstart-job
x-include-once-url
x-include-url
x-shellscript</pre></p>
<p>使用make-mime子命令可以生成MIME multi-part文件：</p>
<p><pre class="crayon-plain-tag">cloud-init devel make-mime -a config.yaml:cloud-config \
                           -a script.sh:x-shellscript &gt; user-data</pre>
</td>
</tr>
<tr>
<td>User-Data Script </td>
<td>就是一段脚本，需要以<pre class="crayon-plain-tag">#!</pre>开头，包含在MIME Multi Part Archive中时使用Content-Type：text/x-shellscript</td>
</tr>
<tr>
<td>Include File</td>
<td>
<p>文件包含一系列URL，每个URL一行，这些URL会被读取，从中获取Gzip压缩内容、MIME Multi Part Archive，或者普通文本</p>
<p>需要以<pre class="crayon-plain-tag">#include</pre>开头，包含在MIME Multi Part Archive中时使用Content-Type： text/x-include-url</p>
</td>
</tr>
<tr>
<td>Cloud Config Data</td>
<td>
<p>这是最简单的通过user-data来实现实例定制的方式。就是提供一个YAML配置文件</p>
<p>需要以<pre class="crayon-plain-tag">#cloud-config</pre>开头，包含在MIME Multi Part Archive中时使用Content-Type：text/cloud-config</p>
<p>定制用户、组的例子：</p>
<pre class="crayon-plain-tag">#cloud-config
# Add groups to the system
# The following example adds the ubuntu group with members 'root' and 'sys'
# and the empty group cloud-users.
groups:
  - ubuntu: [root,sys]
  - cloud-users

# Add users to the system. Users are added after groups are added.
# Note: Most of these configuration options will not be honored if the user
#       already exists. Following options are the exceptions and they are
#       applicable on already-existing users:
#       - 'plain_text_passwd', 'hashed_passwd', 'lock_passwd', 'sudo',
#         'ssh_authorized_keys', 'ssh_redirect_user'.
users:
  - default
  - name: foobar
    gecos: Foo B. Bar
    primary_group: foobar
    groups: users
    selinux_user: staff_u
    expiredate: '2012-09-01'
    ssh_import_id: foobar
    lock_passwd: false
    passwd: $6$j212wezy$7H/1LT4f9/N3wpgNunhsIqtMj62OKiS3nyNwuizouQc3u7MbYCarYeAHWYPYb2FT.lbioDm2RrkJPb9BZMN1O/
  - name: barfoo
    gecos: Bar B. Foo
    sudo: ALL=(ALL) NOPASSWD:ALL
    groups: users, admin
    ssh_import_id: None
    lock_passwd: true
    ssh_authorized_keys:
      - &lt;ssh pub key 1&gt;
      - &lt;ssh pub key 2&gt;
  - name: cloudy
    gecos: Magic Cloud App Daemon User
    inactive: '5'
    system: true
  - name: fizzbuzz
    sudo: False
    ssh_authorized_keys:
      - &lt;ssh pub key 1&gt;
      - &lt;ssh pub key 2&gt;
  - snapuser: joe@joeuser.io
  - name: nosshlogins
    ssh_redirect_user: true

# Valid Values:
#   name: The user's login name
#   expiredate: Date on which the user's account will be disabled.
#   gecos: The user name's real name, i.e. "Bob B. Smith"
#   homedir: Optional. Set to the local path you want to use. Defaults to
#           /home/&lt;username&gt;
#   primary_group: define the primary group. Defaults to a new group created
#           named after the user.
#   groups:  Optional. Additional groups to add the user to. Defaults to none
#   selinux_user:  Optional. The SELinux user for the user's login, such as
#           "staff_u". When this is omitted the system will select the default
#           SELinux user.
#   lock_passwd: Defaults to true. Lock the password to disable password login
#   inactive: Number of days after password expires until account is disabled
#   passwd: The hash -- not the password itself -- of the password you want
#           to use for this user. You can generate a safe hash via:
#               mkpasswd --method=SHA-512 --rounds=4096
#           (the above command would create from stdin an SHA-512 password hash
#           with 4096 salt rounds)
#
#           Please note: while the use of a hashed password is better than
#               plain text, the use of this feature is not ideal. Also,
#               using a high number of salting rounds will help, but it should
#               not be relied upon.
#
#               To highlight this risk, running John the Ripper against the
#               example hash above, with a readily available wordlist, revealed
#               the true password in 12 seconds on a i7-2620QM.
#
#               In other words, this feature is a potential security risk and is
#               provided for your convenience only. If you do not fully trust the
#               medium over which your cloud-config will be transmitted, then you
#               should use SSH authentication only.
#
#               You have thus been warned.
#   no_create_home: When set to true, do not create home directory.
#   no_user_group: When set to true, do not create a group named after the user.
#   no_log_init: When set to true, do not initialize lastlog and faillog database.
#   ssh_import_id: Optional. Import SSH ids
#   ssh_authorized_keys: Optional. [list] Add keys to user's authorized keys file
#   ssh_redirect_user: Optional. [bool] Set true to block ssh logins for cloud
#       ssh public keys and emit a message redirecting logins to
#       use &lt;default_username&gt; instead. This option only disables cloud
#       provided public-keys. An error will be raised if ssh_authorized_keys
#       or ssh_import_id is provided for the same user.
#
#       ssh_authorized_keys.
#   sudo: Defaults to none. Accepts a sudo rule string, a list of sudo rule
#         strings or False to explicitly deny sudo usage. Examples:
#
#         Allow a user unrestricted sudo access.
#             sudo:  ALL=(ALL) NOPASSWD:ALL
#
#         Adding multiple sudo rule strings.
#             sudo:
#               - ALL=(ALL) NOPASSWD:/bin/mysql
#               - ALL=(ALL) ALL
#
#         Prevent sudo access for a user.
#             sudo: False
#
#         Note: Please double check your syntax and make sure it is valid.
#               cloud-init does not parse/check the syntax of the sudo
#               directive.
#   system: Create the user as a system user. This means no home directory.
#   snapuser: Create a Snappy (Ubuntu-Core) user via the snap create-user
#             command available on Ubuntu systems.  If the user has an account
#             on the Ubuntu SSO, specifying the email will allow snap to
#             request a username and any public ssh keys and will import
#             these into the system with username specifed by SSO account.
#             If 'username' is not set in SSO, then username will be the
#             shortname before the email domain.
#

# Default user creation:
#
# Unless you define users, you will get a 'ubuntu' user on ubuntu systems with the
# legacy permission (no password sudo, locked user, etc). If however, you want
# to have the 'ubuntu' user in addition to other users, you need to instruct
# cloud-init that you also want the default user. To do this use the following
# syntax:
#   users:
#     - default
#     - bob
#     - ....
#  foobar: ...
#
# users[0] (the first user in users) overrides the user directive.
#
# The 'default' user above references the distro's config:
# system_info:
#   default_user:
#     name: Ubuntu
#     plain_text_passwd: 'ubuntu'
#     home: /home/ubuntu
#     shell: /bin/bash
#     lock_passwd: True
#     gecos: Ubuntu
#     groups: [adm, audio, cdrom, dialout, floppy, video, plugdev, dip, netdev]</pre>
<p>写入到文件系统的例子：</p>
<pre class="crayon-plain-tag">#cloud-config
# vim: syntax=yaml
#
# This is the configuration syntax that the write_files module
# will know how to understand. encoding can be given b64 or gzip or (gz+b64).
# The content will be decoded accordingly and then written to the path that is
# provided. 
#
# Note: Content strings here are truncated for example purposes.
write_files:
- encoding: b64
  content: CiMgVGhpcyBmaWxlIGNvbnRyb2xzIHRoZSBzdGF0ZSBvZiBTRUxpbnV4...
  owner: root:root
  path: /etc/sysconfig/selinux
  permissions: '0644'
- content: |
    # My new /etc/sysconfig/samba file

    SMBDOPTIONS="-D"
  path: /etc/sysconfig/samba
- content: !!binary |
    f0VMRgIBAQAAAAAAAAAAAAIAPgABAAAAwARAAAAAAABAAAAAAAAAAJAVAAAAAAAAAAAAAEAAOAAI
    AEAAHgAdAAYAAAAFAAAAQAAAAAAAAABAAEAAAAAAAEAAQAAAAAAAwAEAAAAAAADAAQAAAAAAAAgA
    AAAAAAAAAwAAAAQAAAAAAgAAAAAAAAACQAAAAAAAAAJAAAAAAAAcAAAAAAAAABwAAAAAAAAAAQAA
    ....
  path: /bin/arch
  permissions: '0555'
- encoding: gzip
  content: !!binary |
    H4sIAIDb/U8C/1NW1E/KzNMvzuBKTc7IV8hIzcnJVyjPL8pJ4QIA6N+MVxsAAAA=
  path: /usr/bin/hello
  permissions: '0755'</pre>
<p>添加YUM源：</p>
<pre class="crayon-plain-tag">#cloud-config
# vim: syntax=yaml
#
# Add yum repository configuration to the system
#
# The following example adds the file /etc/yum.repos.d/epel_testing.repo
# which can then subsequently be used by yum for later operations.
yum_repos:
  # The name of the repository
  epel-testing:
    # Any repository configuration options
    # See: man yum.conf
    #
    # This one is required!
    baseurl: http://download.fedoraproject.org/pub/epel/testing/5/$basearch
    enabled: false
    failovermethod: priority
    gpgcheck: true
    gpgkey: file:///etc/pki/rpm-gpg/RPM-GPG-KEY-EPEL
    name: Extra Packages for Enterprise Linux 5 - Testing</pre>
<p>添加APT源：</p>
<pre class="crayon-plain-tag">#cloud-config

# Add primary apt repositories
#
# To add 3rd party repositories, see cloud-config-apt.txt or the
# Additional apt configuration and repositories section.
#
#
# Default: auto select based on cloud metadata
#  in ec2, the default is &lt;region&gt;.archive.ubuntu.com
# apt:
#   primary:
#     - arches [default]
#       uri:
#     use the provided mirror
#       search:
#     search the list for the first mirror.
#     this is currently very limited, only verifying that
#     the mirror is dns resolvable or an IP address
#
# if neither mirror is set (the default)
# then use the mirror provided by the DataSource found.
# In EC2, that means using &lt;region&gt;.ec2.archive.ubuntu.com
#
# if no mirror is provided by the DataSource, but 'search_dns' is
# true, then search for dns names '&lt;distro&gt;-mirror' in each of
# - fqdn of this host per cloud metadata
# - localdomain
# - no domain (which would search domains listed in /etc/resolv.conf)
# If there is a dns entry for &lt;distro&gt;-mirror, then it is assumed that there
# is a distro mirror at http://&lt;distro&gt;-mirror.&lt;domain&gt;/&lt;distro&gt;
#
# That gives the cloud provider the opportunity to set mirrors of a distro
# up and expose them only by creating dns entries.
#
# if none of that is found, then the default distro mirror is used
apt:
  primary:
    - arches: [default]
      uri: http://us.archive.ubuntu.com/ubuntu/
# or
apt:
  primary:
    - arches: [default]
      search:
        - http://local-mirror.mydomain
        - http://archive.ubuntu.com
# or
apt:
  primary:
    - arches: [default]
      search_dns: True</pre>
<p>在首次启动时更新APT数据库：</p>
<pre class="crayon-plain-tag">#cloud-config
# Update apt database on first boot (run 'apt-get update').
# Note, if packages are given, or package_upgrade is true, then
# update will be done independent of this setting.
#
# Default: false
# Aliases: apt_update
package_update: true</pre>
<p>执行YUM/APT Upgrade：</p>
<pre class="crayon-plain-tag">#cloud-config

# Upgrade the instance on first boot
# (ie run apt-get upgrade)
#
# Default: false
# Aliases: apt_upgrade
package_upgrade: true</pre>
<p>配置实例的受信任CA：</p>
<pre class="crayon-plain-tag">#cloud-config
#
# This is an example file to configure an instance's trusted CA certificates
# system-wide for SSL/TLS trust establishment when the instance boots for the
# first time.
#
# Make sure that this file is valid yaml before starting instances.
# It should be passed as user-data when starting the instance.

ca-certs:
  # If present and set to True, the 'remove-defaults' parameter will remove
  # all the default trusted CA certificates that are normally shipped with
  # Ubuntu.
  # This is mainly for paranoid admins - most users will not need this
  # functionality.
  remove-defaults: true

  # If present, the 'trusted' parameter should contain a certificate (or list
  # of certificates) to add to the system as trusted CA certificates.
  # Pay close attention to the YAML multiline list syntax.  The example shown
  # here is for a list of multiline certificates.
  trusted: 
  - |
   -----BEGIN CERTIFICATE-----
   YOUR-ORGS-TRUSTED-CA-CERT-HERE
   -----END CERTIFICATE-----
  - |
   -----BEGIN CERTIFICATE-----
   YOUR-ORGS-TRUSTED-CA-CERT-HERE
   -----END CERTIFICATE-----</pre>
<p>配置DNS：</p>
<pre class="crayon-plain-tag">#cloud-config
#
# This is an example file to automatically configure resolv.conf when the
# instance boots for the first time.
#
# Ensure that your yaml is valid and pass this as user-data when starting
# the instance. Also be sure that your cloud.cfg file includes this
# configuration module in the appropriate section.
#
manage_resolv_conf: true

resolv_conf:
  nameservers: ['8.8.4.4', '8.8.8.8']
  searchdomains:
    - foo.example.com
    - bar.example.com
  domain: example.com
  options:
    rotate: true
    timeout: 1</pre>
<p>在第一次启动时执行命令：</p>
<pre class="crayon-plain-tag">#cloud-config

# boot commands
# default: none
# this is very similar to runcmd, but commands run very early
# in the boot process, only slightly after a 'boothook' would run.
# bootcmd should really only be used for things that could not be
# done later in the boot process.  bootcmd is very much like
# boothook, but possibly with more friendly.
# - bootcmd will run on every boot
# - the INSTANCE_ID variable will be set to the current instance id.
# - you can use 'cloud-init-per' command to help only run once
bootcmd:
  - echo 192.168.1.130 us.archive.ubuntu.com &gt;&gt; /etc/hosts
  - [ cloud-init-per, once, mymkfs, mkfs, /dev/vdb ]</pre><br />
<pre class="crayon-plain-tag">#cloud-config

# run commands
# default: none
# runcmd contains a list of either lists or a string
# each item will be executed in order at rc.local like level with
# output to the console
# - runcmd only runs during the first boot
# - if the item is a list, the items will be properly executed as if
#   passed to execve(3) (with the first arg as the command).
# - if the item is a string, it will be simply written to the file and
#   will be interpreted by 'sh'
#
# Note, that the list has to be proper yaml, so you have to quote
# any characters yaml would eat (':' can be problematic)
runcmd:
 - [ ls, -l, / ]
 - [ sh, -xc, "echo $(date) ': hello world!'" ]
 - [ sh, -c, echo "=========hello world'=========" ]
 - ls -l /root
 # Note: Don't write files to /tmp from cloud-init use /run/somedir instead.
 # Early boot environments can race systemd-tmpfiles-clean LP: #1707222.
 - mkdir /run/mydir
 - [ wget, "http://slashdot.org", -O, /run/mydir/index.html ]</pre>
<p>安装软件包：</p>
<pre class="crayon-plain-tag">#cloud-config

# Install additional packages on first boot
#
# Default: none
#
# if packages are specified, this apt_update will be set to true
#
# packages may be supplied as a single package name or as a list
# with the format [&lt;package&gt;, &lt;version&gt;] wherein the specifc
# package version will be installed.
packages:
 - pwgen
 - pastebinit
 - [libpython2.7, 2.7.3-0ubuntu3.1]</pre>
<p>调整挂载点：</p>
<pre class="crayon-plain-tag">#cloud-config

# set up mount points
# 'mounts' contains a list of lists
#  the inner list are entries for an /etc/fstab line
#  ie : [ fs_spec, fs_file, fs_vfstype, fs_mntops, fs-freq, fs_passno ]
#
# default:
# mounts:
#  - [ ephemeral0, /mnt ]
#  - [ swap, none, swap, sw, 0, 0 ]
#
# in order to remove a previously listed mount (ie, one from defaults)
# list only the fs_spec.  For example, to override the default, of
# mounting swap:
# - [ swap ]
# or
# - [ swap, null ]
#
# - if a device does not exist at the time, an entry will still be
#   written to /etc/fstab.
# - '/dev' can be ommitted for device names that begin with: xvd, sd, hd, vd
# - if an entry does not have all 6 fields, they will be filled in
#   with values from 'mount_default_fields' below.
#
# Note, that you should set 'nofail' (see man fstab) for volumes that may not
# be attached at instance boot (or reboot).
#
mounts:
 - [ ephemeral0, /mnt, auto, "defaults,noexec" ]
 - [ sdc, /opt/data ]
 - [ xvdh, /opt/data, "auto", "defaults,nofail", "0", "0" ]
 - [ dd, /dev/zero ]

# mount_default_fields
# These values are used to fill in any entries in 'mounts' that are not
# complete.  This must be an array, and must have 6 fields.
mount_default_fields: [ None, None, "auto", "defaults,nofail", "0", "2" ]


# swap can also be set up by the 'mounts' module
# default is to not create any swap files, because 'size' is set to 0
swap:
  filename: /swap.img
  size: "auto" # or size in bytes
  maxsize: size in bytes</pre>
<p>cloud-init完毕后重启/关机：</p>
<pre class="crayon-plain-tag">#cloud-config

## poweroff or reboot system after finished
# default: none
#
# power_state can be used to make the system shutdown, reboot or
# halt after boot is finished.  This same thing can be acheived by
# user-data scripts or by runcmd by simply invoking 'shutdown'.
# 
# Doing it this way ensures that cloud-init is entirely finished with
# modules that would be executed, and avoids any error/log messages
# that may go to the console as a result of system services like
# syslog being taken down while cloud-init is running.
#
# If you delay '+5' (5 minutes) and have a timeout of
# 120 (2 minutes), then the max time until shutdown will be 7 minutes.
# cloud-init will invoke 'shutdown +5' after the process finishes, or
# when 'timeout' seconds have elapsed.
#
# delay: form accepted by shutdown.  default is 'now'. other format
#        accepted is '+m' (m in minutes)
# mode: required. must be one of 'poweroff', 'halt', 'reboot'
# message: provided as the message argument to 'shutdown'. default is none.
# timeout: the amount of time to give the cloud-init process to finish
#          before executing shutdown.
# condition: apply state change only if condition is met.
#            May be boolean True (always met), or False (never met),
#            or a command string or list to be executed.
#            command's exit code indicates:
#               0: condition met
#               1: condition not met
#            other exit codes will result in 'not met', but are reserved
#            for future use.
#
power_state:
  delay: "+30"
  mode: poweroff
  message: Bye Bye
  timeout: 30
  condition: True</pre>
<p>配置实例的SSH Keys：</p>
<pre class="crayon-plain-tag">#cloud-config

# add each entry to ~/.ssh/authorized_keys for the configured user or the
# first user defined in the user definition directive.
ssh_authorized_keys:
  - ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAGEA3FSyQwBI6Z+nCSjUUk8EEAnnkhXlukKoUPND/RRClWz2s5TCzIkd3Ou5+Cyz71X0XmazM3l5WgeErvtIwQMyT1KjNoMhoJMrJnWqQPOt5Q8zWd9qG7PBl9+eiH5qV7NZ mykey@host
  - ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA3I7VUf2l5gSn5uavROsc5HRDpZdQueUq5ozemNSj8T7enqKHOEaFoU2VoPgGEWC9RyzSQVeyD6s7APMcE82EtmW4skVEgEGSbDc1pvxzxtchBj78hJP6Cf5TCMFSXw+Fz5rF1dR23QDbN1mkHs7adr8GW4kSWqU7Q7NDwfIrJJtO7Hi42GyXtvEONHbiRPOe8stqUly7MvUoN+5kfjBM8Qqpfl2+FNhTYWpMfYdPUnE7u536WqzFmsaqJctz3gBxH9Ex7dFtrxR4qiqEr9Qtlu3xGn7Bw07/+i1D+ey3ONkZLN+LQ714cgj8fRS4Hj29SCmXp5Kt5/82cD/VN3NtHw== smoser@brickies

# Send pre-generated SSH private keys to the server
# If these are present, they will be written to /etc/ssh and
# new random keys will not be generated
#  in addition to 'rsa' and 'dsa' as shown below, 'ecdsa' is also supported
ssh_keys:
  rsa_private: |
    -----BEGIN RSA PRIVATE KEY-----
    MIIBxwIBAAJhAKD0YSHy73nUgysO13XsJmd4fHiFyQ+00R7VVu2iV9Qcon2LZS/x
    1cydPZ4pQpfjEha6WxZ6o8ci/Ea/w0n+0HGPwaxlEG2Z9inNtj3pgFrYcRztfECb
    1j6HCibZbAzYtwIBIwJgO8h72WjcmvcpZ8OvHSvTwAguO2TkR6mPgHsgSaKy6GJo
    PUJnaZRWuba/HX0KGyhz19nPzLpzG5f0fYahlMJAyc13FV7K6kMBPXTRR6FxgHEg
    L0MPC7cdqAwOVNcPY6A7AjEA1bNaIjOzFN2sfZX0j7OMhQuc4zP7r80zaGc5oy6W
    p58hRAncFKEvnEq2CeL3vtuZAjEAwNBHpbNsBYTRPCHM7rZuG/iBtwp8Rxhc9I5w
    ixvzMgi+HpGLWzUIBS+P/XhekIjPAjA285rVmEP+DR255Ls65QbgYhJmTzIXQ2T9
    luLvcmFBC6l35Uc4gTgg4ALsmXLn71MCMGMpSWspEvuGInayTCL+vEjmNBT+FAdO
    W7D4zCpI43jRS9U06JVOeSc9CDk2lwiA3wIwCTB/6uc8Cq85D9YqpM10FuHjKpnP
    REPPOyrAspdeOAV+6VKRavstea7+2DZmSUgE
    -----END RSA PRIVATE KEY-----

  rsa_public: ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAGEAoPRhIfLvedSDKw7XdewmZ3h8eIXJD7TRHtVW7aJX1ByifYtlL/HVzJ09nilCl+MSFrpbFnqjxyL8Rr/DSf7QcY/BrGUQbZn2Kc22PemAWthxHO18QJvWPocKJtlsDNi3 smoser@localhost

  dsa_private: |
    -----BEGIN DSA PRIVATE KEY-----
    MIIBuwIBAAKBgQDP2HLu7pTExL89USyM0264RCyWX/CMLmukxX0Jdbm29ax8FBJT
    pLrO8TIXVY5rPAJm1dTHnpuyJhOvU9G7M8tPUABtzSJh4GVSHlwaCfycwcpLv9TX
    DgWIpSj+6EiHCyaRlB1/CBp9RiaB+10QcFbm+lapuET+/Au6vSDp9IRtlQIVAIMR
    8KucvUYbOEI+yv+5LW9u3z/BAoGBAI0q6JP+JvJmwZFaeCMMVxXUbqiSko/P1lsa
    LNNBHZ5/8MOUIm8rB2FC6ziidfueJpqTMqeQmSAlEBCwnwreUnGfRrKoJpyPNENY
    d15MG6N5J+z81sEcHFeprryZ+D3Ge9VjPq3Tf3NhKKwCDQ0240aPezbnjPeFm4mH
    bYxxcZ9GAoGAXmLIFSQgiAPu459rCKxT46tHJtM0QfnNiEnQLbFluefZ/yiI4DI3
    8UzTCOXLhUA7ybmZha+D/csj15Y9/BNFuO7unzVhikCQV9DTeXX46pG4s1o23JKC
    /QaYWNMZ7kTRv+wWow9MhGiVdML4ZN4XnifuO5krqAybngIy66PMEoQCFEIsKKWv
    99iziAH0KBMVbxy03Trz
    -----END DSA PRIVATE KEY-----

  dsa_public: ssh-dss AAAAB3NzaC1kc3MAAACBAM/Ycu7ulMTEvz1RLIzTbrhELJZf8Iwua6TFfQl1ubb1rHwUElOkus7xMhdVjms8AmbV1Meem7ImE69T0bszy09QAG3NImHgZVIeXBoJ/JzByku/1NcOBYilKP7oSIcLJpGUHX8IGn1GJoH7XRBwVub6Vqm4RP78C7q9IOn0hG2VAAAAFQCDEfCrnL1GGzhCPsr/uS1vbt8/wQAAAIEAjSrok/4m8mbBkVp4IwxXFdRuqJKSj8/WWxos00Ednn/ww5QibysHYULrOKJ1+54mmpMyp5CZICUQELCfCt5ScZ9GsqgmnI80Q1h3Xkwbo3kn7PzWwRwcV6muvJn4PcZ71WM+rdN/c2EorAINDTbjRo97NueM94WbiYdtjHFxn0YAAACAXmLIFSQgiAPu459rCKxT46tHJtM0QfnNiEnQLbFluefZ/yiI4DI38UzTCOXLhUA7ybmZha+D/csj15Y9/BNFuO7unzVhikCQV9DTeXX46pG4s1o23JKC/QaYWNMZ7kTRv+wWow9MhGiVdML4ZN4XnifuO5krqAybngIy66PMEoQ= smoser@localhost

# By default, the fingerprints of the authorized keys for the users
# cloud-init adds are printed to the console. Setting
# no_ssh_fingerprints to true suppresses this output.
no_ssh_fingerprints: false

# By default, (most) ssh host keys are printed to the console. Setting
# emit_keys_to_console to false suppresses this output.
ssh:
  emit_keys_to_console: false</pre>
<p>初始化磁盘：</p>
<pre class="crayon-plain-tag">#cloud-config
# Cloud-init supports the creation of simple partition tables and file systems
# on devices.

# Default disk definitions for AWS
# --------------------------------
# (Not implemented yet, but provided for future documentation)

disk_setup:
  ephmeral0:
    table_type: 'mbr'
    layout: True
    overwrite: False

fs_setup:
  - label: None,
    filesystem: ext3
    device: ephemeral0
    partition: auto

# Default disk definitions for Microsoft Azure
# ------------------------------------------

device_aliases: {'ephemeral0': '/dev/sdb'}
disk_setup:
  ephemeral0:
    table_type: mbr
    layout: True
    overwrite: False

fs_setup:
  - label: ephemeral0
    filesystem: ext4
    device: ephemeral0.1
    replace_fs: ntfs


# Data disks definitions for Microsoft Azure
# ------------------------------------------

disk_setup:
  /dev/disk/azure/scsi1/lun0:
    table_type: gpt
    layout: True
    overwrite: True

fs_setup:
  - device: /dev/disk/azure/scsi1/lun0
    partition: 1
    filesystem: ext4


# Default disk definitions for SmartOS
# ------------------------------------

device_aliases: {'ephemeral0': '/dev/vdb'}
disk_setup:
  ephemeral0:
    table_type: mbr
    layout: False
    overwrite: False

fs_setup:
  - label: ephemeral0
    filesystem: ext4
    device: ephemeral0.0

# Caveat for SmartOS: if ephemeral disk is not defined, then the disk will
#    not be automatically added to the mounts.


# The default definition is used to make sure that the ephemeral storage is
# setup properly.

# "disk_setup": disk partitioning
# --------------------------------

# The disk_setup directive instructs Cloud-init to partition a disk. The format is:

disk_setup:
  ephmeral0:
    table_type: 'mbr'
    layout: 'auto'
  /dev/xvdh:
    table_type: 'mbr'
    layout:
      - 33
      - [33, 82]
      - 33
    overwrite: True

# The format is a list of dicts of dicts. The first value is the name of the
# device and the subsequent values define how to create and layout the
# partition.
# The general format is:
#   disk_setup:
#     &lt;DEVICE&gt;:
#       table_type: 'mbr'
#       layout: &lt;LAYOUT|BOOL&gt;
#       overwrite: &lt;BOOL&gt;
#
# Where:
#   &lt;DEVICE&gt;: The name of the device. 'ephemeralX' and 'swap' are special
#               values which are specific to the cloud. For these devices
#               Cloud-init will look up what the real devices is and then
#               use it.
#
#               For other devices, the kernel device name is used. At this
#               time only simply kernel devices are supported, meaning
#               that device mapper and other targets may not work.
#
#               Note: At this time, there is no handling or setup of
#               device mapper targets.
#
#   table_type=&lt;TYPE&gt;: Currently the following are supported:
#                   'mbr': default and setups a MS-DOS partition table
#                   'gpt': setups a GPT partition table
#
#               Note: At this time only 'mbr' and 'gpt' partition tables
#                   are allowed. It is anticipated in the future that
#                   we'll also have "RAID" to create a mdadm RAID.
#
#   layout={...}: The device layout. This is a list of values, with the
#               percentage of disk that partition will take.
#               Valid options are:
#                   [&lt;SIZE&gt;, [&lt;SIZE&gt;, &lt;PART_TYPE]]
#
#               Where &lt;SIZE&gt; is the _percentage_ of the disk to use, while
#               &lt;PART_TYPE&gt; is the numerical value of the partition type.
#
#               The following setups two partitions, with the first
#               partition having a swap label, taking 1/3 of the disk space
#               and the remainder being used as the second partition.
#                 /dev/xvdh':
#                   table_type: 'mbr'
#                   layout:
#                     - [33,82]
#                     - 66
#                   overwrite: True
#
#               When layout is "true" it means single partition the entire
#               device.
#
#               When layout is "false" it means don't partition or ignore
#               existing partitioning.
#
#               If layout is set to "true" and overwrite is set to "false",
#               it will skip partitioning the device without a failure.
#
#   overwrite=&lt;BOOL&gt;: This describes whether to ride with saftey's on and
#               everything holstered.
#
#               'false' is the default, which means that:
#                   1. The device will be checked for a partition table
#                   2. The device will be checked for a file system
#                   3. If either a partition of file system is found, then
#                       the operation will be _skipped_.
#
#               'true' is cowboy mode. There are no checks and things are
#                   done blindly. USE with caution, you can do things you
#                   really, really don't want to do.
#
#
# fs_setup: Setup the file system
# -------------------------------
#
# fs_setup describes the how the file systems are supposed to look.

fs_setup:
  - label: ephemeral0
    filesystem: 'ext3'
    device: 'ephemeral0'
    partition: 'auto'
  - label: mylabl2
    filesystem: 'ext4'
    device: '/dev/xvda1'
  - cmd: mkfs -t %(filesystem)s -L %(label)s %(device)s
    label: mylabl3
    filesystem: 'btrfs'
    device: '/dev/xvdh'

# The general format is:
#   fs_setup:
#     - label: &lt;LABEL&gt;
#       filesystem: &lt;FS_TYPE&gt;
#       device: &lt;DEVICE&gt;
#       partition: &lt;PART_VALUE&gt;
#       overwrite: &lt;OVERWRITE&gt;
#       replace_fs: &lt;FS_TYPE&gt;
#
# Where:
#   &lt;LABEL&gt;: The file system label to be used. If set to None, no label is
#     used.
#
#   &lt;FS_TYPE&gt;: The file system type. It is assumed that the there
#     will be a "mkfs.&lt;FS_TYPE&gt;" that behaves likes "mkfs". On a standard
#     Ubuntu Cloud Image, this means that you have the option of ext{2,3,4},
#     and vfat by default.
#
#   &lt;DEVICE&gt;: The device name. Special names of 'ephemeralX' or 'swap'
#     are allowed and the actual device is acquired from the cloud datasource.
#     When using 'ephemeralX' (i.e. ephemeral0), make sure to leave the
#     label as 'ephemeralX' otherwise there may be issues with the mounting
#     of the ephemeral storage layer.
#
#     If you define the device as 'ephemeralX.Y' then Y will be interpetted
#     as a partition value. However, ephermalX.0 is the _same_ as ephemeralX.
#
#   &lt;PART_VALUE&gt;:
#     Partition definitions are overwriten if you use the '&lt;DEVICE&gt;.Y' notation.
#
#     The valid options are:
#     "auto|any": tell cloud-init not to care whether there is a partition
#       or not. Auto will use the first partition that does not contain a
#       file system already. In the absence of a partition table, it will
#       put it directly on the disk.
#
#       "auto": If a file system that matches the specification in terms of
#       label, type and device, then cloud-init will skip the creation of
#       the file system.
#
#       "any": If a file system that matches the file system type and device,
#       then cloud-init will skip the creation of the file system.
#
#       Devices are selected based on first-detected, starting with partitions
#       and then the raw disk. Consider the following:
#           NAME     FSTYPE LABEL
#           xvdb
#           |-xvdb1  ext4
#           |-xvdb2
#           |-xvdb3  btrfs  test
#           \-xvdb4  ext4   test
#
#         If you ask for 'auto', label of 'test, and file system of 'ext4'
#         then cloud-init will select the 2nd partition, even though there
#         is a partition match at the 4th partition.
#
#         If you ask for 'any' and a label of 'test', then cloud-init will
#         select the 1st partition.
#
#         If you ask for 'auto' and don't define label, then cloud-init will
#         select the 1st partition.
#
#         In general, if you have a specific partition configuration in mind,
#         you should define either the device or the partition number. 'auto'
#         and 'any' are specifically intended for formating ephemeral storage or
#         for simple schemes.
#
#       "none": Put the file system directly on the device.
#
#       &lt;NUM&gt;: where NUM is the actual partition number.
#
#   &lt;OVERWRITE&gt;: Defines whether or not to overwrite any existing
#     filesystem.
#
#     "true": Indiscriminately destroy any pre-existing file system. Use at
#         your own peril.
#
#     "false": If an existing file system exists, skip the creation.
#
#   &lt;REPLACE_FS&gt;: This is a special directive, used for Microsoft Azure that
#     instructs cloud-init to replace a file system of &lt;FS_TYPE&gt;. NOTE:
#     unless you define a label, this requires the use of the 'any' partition
#     directive.
#
# Behavior Caveat: The default behavior is to _check_ if the file system exists.
#   If a file system matches the specification, then the operation is a no-op.</pre>
<p>自动增长分区大小：</p>
<pre class="crayon-plain-tag">#cloud-config
#
# growpart entry is a dict, if it is not present at all
# in config, then the default is used ({'mode': 'auto', 'devices': ['/']})
#
#  mode:
#    values:
#     * auto: use any option possible (any available)
#             if none are available, do not warn, but debug.
#     * growpart: use growpart to grow partitions
#             if growpart is not available, this is an error.
#     * off, false
#
# devices:
#   a list of things to resize.
#   items can be filesystem paths or devices (in /dev)
#   examples:
#     devices: [/, /dev/vdb1]
#
# ignore_growroot_disabled:
#   a boolean, default is false.
#   if the file /etc/growroot-disabled exists, then cloud-init will not grow
#   the root partition.  This is to allow a single file to disable both
#   cloud-initramfs-growroot and cloud-init's growroot support.
#
#   true indicates that /etc/growroot-disabled should be ignored
#
growpart:
  mode: auto
  devices: ['/']
  ignore_growroot_disabled: false</pre>
</td>
</tr>
<tr>
<td>Upstart Job</td>
<td>
<p>内容存放为/etc/init/下的一个文件，从而被Upstart调用
<p>需要以<pre class="crayon-plain-tag">#upstart-job</pre>开头，包含在MIME Multi Part Archive中时使用Content-Type：text/upstart-job</p>
</td>
</tr>
<tr>
<td>Cloud Boothook</td>
<td>
<p>存放在/var/lib/cloud并立即执行，没有任何机制保证钩子仅仅执行一次</p>
<p>需要以<pre class="crayon-plain-tag">#cloud-boothook</pre>开头，包含在MIME Multi Part Archive中时使用Content-Type：text/cloud-boothook</p>
</td>
</tr>
<tr>
<td>Part Handler</td>
<td>
<p>一段代码，用于处理新的MIME类型，或者覆盖既有的MIME类型的处理器。以Python编写，包含函数：</p>
<ol>
<li>list_types：本Handler支持的MIME类型列表</li>
<li>handle_part：执行处理</li>
</ol>
<p>示例：</p>
<pre class="crayon-plain-tag">#part-handler

def list_types():
    # return a list of mime-types that are handled by this module
    return(["text/plain", "text/go-cubs-go"])

def handle_part(data,ctype,filename,payload):
    # data: the cloudinit object
    # ctype: '__begin__', '__end__', or the specific mime-type of the part
    # filename: the filename for the part, or dynamically generated part if
    #           no filename is given attribute is present
    # payload: the content of the part (empty for begin or end)
    if ctype == "__begin__":
       print "my handler is beginning"
       return
    if ctype == "__end__":
       print "my handler is ending"
       return

    print "==== received ctype=%s filename=%s ====" % (ctype,filename)
    print payload
    print "==== end ctype=%s filename=%s" % (ctype, filename)</pre>
<p>需要以<pre class="crayon-plain-tag">#part-handler</pre>开头，包含在MIME Multi Part Archive中时使用Content-Type：text/part-handler</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">instance-data</span></div>
<p>所谓实例数据，是指cloud-init用来配置实例的所有数据的集合。数据来源包括：</p>
<ol>
<li>云环境的元数据服务（metadata）</li>
<li>用户定制的，提供给实例的config-drive</li>
<li>镜像中的cloud-config seed files</li>
<li>提供文件/元数据服务提供的vendor-data</li>
<li>创建实例时提供的user-data</li>
</ol>
<p>也就是说，上节的user-data是instance-data的一部分。</p>
<div class="blog_h2"><span class="graybg">Flavor</span></div>
<p>在OpenStack中，Flavor定义了实例的规格 —— 计算、内存、存储资源的硬件规格。Flavor也可以用来定义实例可以在哪些宿主机上启动。</p>
<div class="blog_h3"><span class="graybg">规格参数</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 150px; text-align: center;">命令行标记</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>--vcpus</td>
<td>虚拟CPU数量，必须</td>
</tr>
<tr>
<td>--ram</td>
<td>内存/MB，必须</td>
</tr>
<tr>
<td>--disk</td>
<td>
<p>根磁盘大小/GB，必须</p>
<p>从镜像创建虚拟机时，根磁盘是一个临时（ephemeral）磁盘，虚拟机的镜像会拷贝到该磁盘上。如果从持久化的卷来启动虚拟机，则不会使用这种磁盘</p>
<p>设置为0，其含义是，使用虚拟机镜像的大小作为临时磁盘大小。但是这种情况下会导致filter scheduler不能基于镜像尺寸来选择适当的宿主机。因此，你应该仅仅在从卷启动虚拟机、或者出于测试目的的时候，才将此参数设置为0</p>
<p>要强制必须从0根磁盘大小的Flavor来创建基于卷的虚拟机，设置策略规则：os_compute_api:servers:create:zero_disk_flavor</p>
</td>
</tr>
<tr>
<td>--ephemeral</td>
<td>
<p>额外的临时磁盘/GB，默认0</p>
<p>该参数为虚拟机提供额外的临时分去，虚拟机销毁后，此磁盘消失</p>
</td>
</tr>
<tr>
<td>--swap</td>
<td>交换分区/MB，默认0</td>
</tr>
<tr>
<td>--public<br />--private</td>
<td>
<p>公共标记，默认True</p>
<p>指示该Flavor是不是可以被除了Flavor所在项目（租户）的其它租户使用</p>
</td>
</tr>
<tr>
<td>--property</td>
<td>
<p>额外规格说明，可以指定多次，键值对。高级配置下，用作scheduler的提示信息</p>
<p>&nbsp;</p>
<p>以quota:开头的属性，用于对Flavor进行配额限制，示例：</p>
<pre class="crayon-plain-tag"># 确保虚拟机只能消耗50%的物理CPU能力
openstack flavor set FLAVOR-NAME \
    --property quota:cpu_quota=10000 \
    --property quota:cpu_period=20000

# 限制每秒最多写入10MB到磁盘
openstack flavor set FLAVOR-NAME \
    --property quota:disk_write_bytes_sec=10485760</pre>
<p>所有quota属性列表（前缀省略）：<br />cpu_shares，相对于domain下其它虚拟机的、使用CPU时间片的权重值<br />cpu_shares_level，仅仅用于VMware，custom, high, normal, low<br />cpu_period，设置QEMU/LXC的enforcement interval，在周期内，不得消耗大于cpu_quota的带宽<br />cpu_limit，设置VMware的CPU频率，单位MHZ<br />cpu_reservation，设置VMware可以确保给虚拟机的CPU频率，MHZ<br />cpu_quota，设置最大允许带宽，单位微秒。负数表示无限制<br />memory_limit，内存上限，单位MB<br />memory_reservation，设置VMware最小保证内存，指定数量的内存一定会分配给虚拟机<br />disk_io_limit，设置VMware下每秒磁盘IO的上限<br />disk_io_reservation，设置VMware下保证的IOPS<br />disk_read_bytes_sec，限制读流量<br />disk_read_iops_sec，限制读IOPS<br />disk_write_bytes_sec，限制写流量<br />disk_write_iops_sec，限制写IOPS<br />disk_total_bytes_sec，限制流量<br />disk_total_iops_sec，限制IOPS<br />vif_inbound_average，入站流量平均速度，单位kb<br />vif_inbound_burst，入站流量以peak速度最多连续接收多少kb<br />vif_inbound_peak，入站流量的的最大速度，单位kb<br />vif_outbound_average，出站流量平均速度，单位kb<br />vif_outbound_burst，出站流量以peak速度最多连续接收多少kb<br />vif_outbound_peak，出站流量的的最大速度，单位kb</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">Quotas</span></div>
<p>允许使用的资源的配额可以针对项目（租户）或者用户进行设置。</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>cores</td>
<td>每个项目总计允许分配的核心数</td>
</tr>
<tr>
<td>instances</td>
<td>每个项目总计允许启动的实例数</td>
</tr>
<tr>
<td>key_pairs</td>
<td>每个用户允许的密钥对数量</td>
</tr>
<tr>
<td>metadata_items</td>
<td>每个实例允许的元数据数量</td>
</tr>
<tr>
<td>ram</td>
<td>每个项目总计允许分配的内存MB</td>
</tr>
<tr>
<td>server_groups</td>
<td>每个项目允许的服务器组数量</td>
</tr>
<tr>
<td>server_group_members</td>
<td>每个服务器组中成员的数量</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">默认配额</span></div>
<p>使用下面的命令查看默认配额值：</p>
<pre class="crayon-plain-tag">openstack quota show --default</pre>
<div class="blog_h3"><span class="graybg">用户配额</span></div>
<p>要查看某个用户的配额，执行：</p>
<pre class="crayon-plain-tag">nova quota-show --user USER --tenant PROJECT</pre>
<div class="blog_h2"><span class="graybg">Host Aggregate</span></div>
<p>主机聚合是一种对宿主机进行分区的机制，聚合中的主机常常具有类似的硬件/性能特征。<span style="background-color: #c0c0c0;">一个主机可以属于多个聚合</span>。</p>
<p>引入主机聚合最初是为了使用Xen资源池，但是现在作为一种机制，允许管理员将一系列键值对（属性）同时分配到多台主机。</p>
<p>主机聚合不直接对用户暴露，管理员可以将Flavor映射到主机聚合 —— 只需要<span style="background-color: #c0c0c0;">为聚合设置匹配Flavor的额外规格说明的元数据</span>，即可完成映射。</p>
<p>管理员也可以将一组主机划分为AZ，与主机聚合不同，AZ是面向用户的概念，而且主机仅能属于一个AZ。</p>
<div class="blog_h3"><span class="graybg">启用支持</span></div>
<p>要让Nova调度器支持主机聚合，需要配置：</p>
<pre class="crayon-plain-tag">[filter_scheduler]
enabled_filters=...,AggregateInstanceExtraSpecsFilter</pre>
<div class="blog_h3"><span class="graybg">使用示例</span></div>
<pre class="crayon-plain-tag"># 在nova可用区中创建一个SSD磁盘的主机的聚合（后面假设聚合的ID为1）
openstack aggregate create --zone nova fast-io

# 为聚合设置元数据
openstack aggregate set --property ssd=true 1

# 添加主机到聚合
openstack aggregate add host 1 node1
openstack aggregate add host 1 node2


# 创建一个Flavor
openstack flavor create --id 6 --ram 8192 --disk 80 --vcpus 4 ssd.large

# 映射Flavor到聚合
openstack flavor set \
# 设置scope为aggregate_instance_extra_specs的额外规格，
#                                             键值和聚合元数据一致
    --property aggregate_instance_extra_specs:ssd=true ssd.large</pre>
<div class="blog_h3"><span class="graybg">Placement中的聚合</span></div>
<p>在Placement中，Aggregate表示相关的资源提供者（resource provider）的分组。<span style="background-color: #c0c0c0;">在Placement中，Nova的计算节点就是资源提供者</span>。因此，节点在Placement中也可以被加入到聚合。</p>
<p>使用下面的命令，可以查询计算节点的UUID，并将其加入到Placement聚合中：</p>
<pre class="crayon-plain-tag">openstack --os-compute-api-version=2.53 hypervisor list
# +--------------------------------------+---------------------+-----------------+-----------------+-------+
# | ID                                   | Hypervisor Hostname | Hypervisor Type | Host IP         | State |
# +--------------------------------------+---------------------+-----------------+-----------------+-------+
# | 815a5634-86fb-4e1e-8824-8a631fee3e06 | node1               | QEMU            | 192.168.1.123   | up    |
# +--------------------------------------+---------------------+-----------------+-----------------+-------+

openstack --os-placement-api-version=1.2 resource provider aggregate set \
    --aggregate df4c74f3-d2c4-4991-b461-f1a678e1d161 \
    815a5634-86fb-4e1e-8824-8a631fee3e06</pre>
<p>从Nova 18.0.0开始，添加主机到Host Aggregate中后，会自动修改对应的Placement聚合。不需要手工操作。删除时类似。</p>
<div class="blog_h3"><span class="graybg">基于Placement的租户隔离</span></div>
<p>为了使用Placement来进行租户隔离，必须存在和Host Aggregate在UUID+成员关系上匹配的Placement Aggregate。调度过滤器AggregateMultiTenancyIsolation会使用聚合元数据。</p>
<p>需要设置 scheduler.limit_tenants_to_placement_aggregate  = True才能启用此特性。</p>
<p>配置示例：</p>
<pre class="crayon-plain-tag"># 创建主机聚合
openstack --os-compute-api-version=2.53 aggregate create myagg
# +-------------------+--------------------------------------+
# | Field             | Value                                |
# +-------------------+--------------------------------------+
# | availability_zone | None                                 |
# | created_at        | 2018-03-29T16:22:23.175884           |
# | deleted           | False                                |
# | deleted_at        | None                                 |
# | id                | 4                                    |
# | name              | myagg                                |
# | updated_at        | None                                 |
# | uuid              | 019e2189-31b3-49e1-aff2-b220ebd91c24 |
# +-------------------+--------------------------------------+

# 添加节点到主机聚合
openstack --os-compute-api-version=2.53 aggregate add host myagg node1

# 获取租户ID
openstack project list -f value | grep 'demo'
9691591f913949818a514f95286a6b90 demo

# 设置此聚合仅仅给租户使用
openstack aggregate set --property filter_tenant_id=9691591f913949818a514f95286a6b90 myagg

# 将主机加入到聚合
openstack --os-placement-api-version=1.2 resource provider aggregate set \
    --aggregate 019e2189-31b3-49e1-aff2-b220ebd91c24 \
    815a5634-86fb-4e1e-8824-8a631fee3e06</pre>
<div class="blog_h3"><span class="graybg">为聚合缓存镜像</span> </div>
<pre class="crayon-plain-tag">nova aggregate-cache-images my-aggregate image1 image2</pre>
<div class="blog_h2"><span class="graybg">调度器</span></div>
<p>Nova组件nova-scheduler负责决定在哪台计算节点上创建虚拟机。 在调度的场景下，术语host表示运行了nova-compute服务的哪些节点。</p>
<p>调度器的基本配置项是：</p>
<pre class="crayon-plain-tag">[scheduler]
driver = filter_scheduler

[filter_scheduler]
available_filters = nova.scheduler.filters.all_filters
enabled_filters = AvailabilityZoneFilter, ComputeFilter, ComputeCapabilitiesFilter, ImagePropertiesFilter, ServerGroupAntiAffinityFilter, ServerGroupAffinityFilter</pre>
<p>默认的调度器驱动是 filter_scheduler，在默认配置下，该调度器选择满足以下所有条件的宿主机：</p>
<ol>
<li>AvailabilityZoneFilter：位于请求的AZ中</li>
<li>ComputeFilter：能够服务请求</li>
<li>ComputeCapabilitiesFilter：满足实例类型的extra_specs（来自Flavor）</li>
<li>ImagePropertiesFilter：满足实例镜像属性中的架构、Hypervisor类型、虚拟机模式属性</li>
<li>ServerGroupAntiAffinityFilter：满足服务器组的反亲和设置——和组中的其它虚拟机不再同一宿主机上</li>
<li>ServerGroupAffinityFilter：满足服务器组亲和设置</li>
</ol>
<p>当执行nova evacuate命令重建虚拟机时，调度器服务遵循管理员给出的目标宿主机，如果管理员没有指定目标宿主机，则由调度器来选择适当的宿主机。</p>
<div class="blog_h3"><span class="graybg">预过滤</span></div>
<p>从R版开始，调度器包含一个前置的过滤步骤，其目的时提升效率，<span style="background-color: #c0c0c0;">减少候选的主机</span>。</p>
<p>下面是一些常用的预过滤器：</p>
<table class="full-width fixed-word-wrap">
<tbody>
<tr>
<td>
<p>镜像类型支持过滤器：<strong> [scheduler]/query_placement_for_image_type_support=True</strong></p>
<p>过滤掉那些不支持用于启动虚拟机的镜像的格式的计算节点。例如，对于libvrit驱动，当使用Ceph作为临时存储后端时，不支持qcow2镜像格式。在混合使用基于Ceph、不使用Ceph作为存储后端的计算节点时，可以启用此过滤器</p>
</td>
</tr>
<tr>
<td>
<p>禁用状态节点过滤器：强制启用</p>
<p>从T版本开始，此过滤器会排除禁用状态的节点（类似于ComputeFilter）。具有trait COMPUTE_STATUS_DISABLED的（计算节点）资源提供者，将被排除，不作为调度候选</p>
<p>Trait由nova-compute服务管理，应该mirror位于os-services中的计算服务记录的disabled状态。例如，如果<span style="background-color: #c0c0c0;">计算服务的状态是disabled，那么它关联的计算节点资源提供者对象应当具有COMPUTE_STATUS_DISABLED这一trait</span>；当计算服务状态为enabled，对应资源提供者的此trait应该被移除</p>
<p>如果状态改变时计算服务宕了，那么trait将在它重启后同步。如果尝试给对应资源提供者添加/删除trait时出错，则update_available_resource这一定时任务负责重新同步。<strong>[DEFAULT]/update_resources_interval</strong>负责此同步操作的间隔</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">Filter scheduler</span></div>
<p>前面我们提到过，nova.scheduler.filter_scheduler.FilterScheduler是默认的调度器（驱动）。它使用过滤器、权重来选择宿主机。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2021/01/filtering-workflow-1.png"><img class="size-full wp-image-35277 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2021/01/filtering-workflow-1.png" alt="filtering-workflow-1" width="550" height="400" /></a></p>
<p>&nbsp;</p>
<p>配置项<strong> [filter_scheduler]/available_filters</strong> 列出调度器可以使用的过滤器集合，此配置项可以指定多次：</p>
<pre class="crayon-plain-tag">[filter_scheduler]
; 所有自带过滤器
available_filters = nova.scheduler.filters.all_filters
; 加上这个自己编写的过滤器
available_filters = myfilter.MyFilter</pre>
<p>配置项 <strong>[filter_scheduler]/filter_scheduler.enabled_filters</strong> 列出当前nova-scheduler启用的过滤器。</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>AggregateImagePropertiesIsolation</td>
<td>
<p>从L版开始，Nova仅会传递标准元数据给此过滤器。如果需要使用所有元数据，考虑过滤器AggregateInstanceExtraSpecsFilter</p>
<p>该过滤器对 <span style="background-color: #c0c0c0;">镜像元数据 和 聚合元数据 进行匹配</span>：</p>
<ol>
<li>如果主机属于聚合，且<span style="background-color: #c0c0c0;">聚合</span>定义了1-N个元数据，这些<span style="background-color: #c0c0c0;">元数据匹配镜像的属性</span>，则主机作为<span style="background-color: #c0c0c0;">从镜像启动的虚拟机</span>的候选宿主</li>
<li>如果宿主机不属于任何聚合，通过此过滤器（即不会被过滤掉）</li>
</ol>
</td>
</tr>
<tr>
<td>AggregateInstanceExtraSpecsFilter</td>
<td>
<p>该过滤器对 实例类型（Flavor）的、Scope为aggregate_instance_extra_specs的extra_specs 和 聚合属性进行匹配</p>
<p>为了向后兼容，没有Scope的Specs也可以用来匹配，但是不推荐 —— 当同时使用ComputeCapabilitiesFilter时会出现冲突</p>
<p>使用此过滤器，可以实现将<span style="background-color: #c0c0c0;">Flavor调度到特定的主机集合</span>中</p>
</td>
</tr>
<tr>
<td>AggregateMultiTenancyIsolation</td>
<td>
<p><span style="background-color: #c0c0c0;">确保租户隔离</span>（tenant-isolated）的主机聚合（即设置了filter_tenant_id的主机聚合）仅仅能被相关的租户（项目）所使用</p>
<p>filter_tenant_id可以用逗号分隔多个租户</p>
<p>如果某个主机属于设置了filter_tenant_id的聚合，那么某个不属于相应租户的用户发起创建虚拟机的请求时，虚拟机不会在此聚合的某个宿主机上创建</p>
<p>主机可以不属于任何设置了filter_tenant_id的聚合，通过此过滤器</p>
</td>
</tr>
<tr>
<td>AggregateNumInstancesFilter</td>
<td>
<p>用于<span style="background-color: #c0c0c0;">限制宿主机上实例的最大数量</span></p>
<p>对于一个聚合，如果设置了max_instances_per_host，那么其中的宿主机上的实例不会超过特定数量</p>
<p>如果主机属于多个设置了max_instances_per_host的聚合，验证此主机实例数量是否到达上限时，使用最小的max_instances_per_host值</p>
</td>
</tr>
<tr>
<td>AggregateTypeAffinityFilter</td>
<td>
<p>用于实现<span style="background-color: #c0c0c0;">实例类型（Flavor）亲和性</span></p>
<p>此过滤器pass（通过）没有设置instance_type的主机，或者所在聚合的元数据instance_type（逗号分隔）包含正在请求创建的虚拟机的instance_type的主机</p>
</td>
</tr>
<tr>
<td>AllHostsFilter</td>
<td>通过所有主机</td>
</tr>
<tr>
<td>AvailabilityZoneFilter</td>
<td>满足调度请求中关于<span style="background-color: #c0c0c0;">可用区的需求</span></td>
</tr>
<tr>
<td>ComputeCapabilitiesFilter</td>
<td>
<p>对Flavor的extra_specs中的属性 和 compute capabilities进行匹配</p>
<p>如果Extra Spec中包含冒号，则：之前的看作命名空间，之后的看作需要匹配的key。如果命名空间不是capabilities，则忽略此Spec</p>
<p>为了向后兼容，没有Namespace的Specs也可以用来匹配，但是不推荐 —— 当同时使用AggregateInstanceExtraSpecsFilter时会出现冲突</p>
<p>某些虚拟化驱动支持报告CPU的trait给placement服务，这种情况下，应该在Flavor中使用trait，而不是使用此过滤器。因为trait提供了CPU特性的一致性命名，而且查询trait的效率更高</p>
</td>
</tr>
<tr>
<td>ComputeFilter</td>
<td>此过滤器pass（通过）所有启用的、可以工作的计算服务（节点）</td>
</tr>
<tr>
<td>DifferentHostFilter</td>
<td>
<p>调度到其它宿主机，排除的宿主机由请求时给出的different_host来确定，该字段是一个实例的列表，排除的是这些实例所在的宿主机：</p>
<pre class="crayon-plain-tag">{
    "server": {
        "name": "server-1",
        "imageRef": "cedef40a-ed67-4d10-800e-17455edce175",
        "flavorRef": "1"
    },
    "os:scheduler_hints": {
        "different_host": [
            "a0cf03a5-d921-4877-bb5c-86d26cf818e1",
            "8c19174f-4220-44f0-824a-cd1eeef10287"
        ]
    }
}</pre>
<p> 使用命令：</p>
<pre class="crayon-plain-tag">openstack server create --image cedef40a-ed67-4d10-800e-17455edce175 \
  --flavor 1 --hint different_host=a0cf03a5-d921-4877-bb5c-86d26cf818e1 \
  # 调度提示
  --hint different_host=8c19174f-4220-44f0-824a-cd1eeef10287 server-1</pre>
</td>
</tr>
<tr>
<td>ImagePropertiesFilter</td>
<td>
<p>根据实例的<span style="background-color: #c0c0c0;">镜像的属性来过滤宿主机</span>，仅仅通过那些支持镜像属性的宿主机
<p>属性包括：<span style="background-color: #c0c0c0;">架构、Hypervisor类型/版本、虚拟机模式：</span></p>
<p style="padding-left: 30px;">hw_architecture，架构：i686, x86_64, arm, ppc64 ...<br />img_hv_type，Hypervisor类型：qemu,hyperv ...<br />img_hv_requested_version，Hypervisor版本</p>
<p>对于QEMU、KVM，Hypervisor类型都是qemu</p>
<pre class="crayon-plain-tag">openstack image set --architecture arm --property img_hv_type=qemu img-uuid</pre>
</td>
</tr>
<tr>
<td>IsolatedHostsFilter</td>
<td>
<p>允许定义一个<span style="background-color: #c0c0c0;">隔离镜像集、隔离宿主机集，两者必须在一起</span>
<p>restrict_isolated_hosts_to_isolated_images 用于限制隔离主机仅仅运行隔离镜像。取值True意味着卷后备的虚拟机不能调度到隔离主机集；取值False则没有任何限制（对比镜像后备的虚拟机）</p>
<p>镜像集、宿主机集合必须配置在nova.conf：</p>
<pre class="crayon-plain-tag">[filter_scheduler]
isolated_hosts = server1, server2
isolated_images = 342b492c-128f-4a42-8d3a-c5088cf27d13, ebd267a6-ca86-4d6c-9a0e-bd132d6b7d09 </pre>
</td>
</tr>
<tr>
<td>IoOpsFilter</td>
<td>
<p><span style="background-color: #c0c0c0;">过滤掉具有太多并发IO实例的宿主机</span>
<p>max_io_ops_per_host指定 单个宿主机上IO敏感的实例的最大数量</p>
</td>
</tr>
<tr>
<td>JsonFilter</td>
<td>
<p>此过滤器默认没有启用，且没有广泛测试</p>
<p>允许用户为调度器提供一个JSON格式的提示。在提示中：</p>
<ol>
<li>支持操作符 = &lt; &gt; in &lt;= &gt;= not or and</li>
<li>支持判断属性   $free_ram_mb  $free_disk_mb  $hypervisor_hostname  $total_usable_ram_mb  $vcpus_total $vcpus_used 等任何<a href="https://opendev.org/openstack/nova/src/branch/master/nova/scheduler/host_manager.py">HostState</a></li>
</ol>
<p>示例：</p>
<pre class="crayon-plain-tag">openstack server create --image 827d564a-e636-4fc4-a376-d36f7ebe1747 \
  --flavor 1 --hint query='["&gt;=","$free_ram_mb",1024]' server1</pre>
</td>
</tr>
<tr>
<td>PciPassthroughFilter</td>
<td>仅仅通过匹配Flavor的extra_specs中设备请求的宿主机</td>
</tr>
<tr>
<td>SameHostFilter</td>
<td>
<p>调度到和<span style="background-color: #c0c0c0;">指定实例（集）相同的宿主机（之一）</span>
<p>实例（集）由提示给出：</p>
<pre class="crayon-plain-tag">openstack server create --image cedef40a-ed67-4d10-800e-17455edce175 \
  --flavor 1 --hint same_host=a0cf03a5-d921-4877-bb5c-86d26cf818e1 \
  --hint same_host=8c19174f-4220-44f0-824a-cd1eeef10287 server-1</pre>
</td>
</tr>
<tr>
<td>ServerGroupAffinityFilter</td>
<td>
<p>确保<span style="background-color: #c0c0c0;">调度到指定的服务器组</span>中，服务器组由提示给出
<pre class="crayon-plain-tag">openstack server group create --policy affinity group-1
openstack server create --image IMAGE_ID --flavor 1 \
  --hint group=SERVER_GROUP_UUID server-1</pre>
</td>
</tr>
<tr>
<td>ServerGroupAntiAffinityFilter</td>
<td>
<p>确保<span style="background-color: #c0c0c0;">不调度到指定的服务器组</span>中，服务器组由提示给出
</td>
</tr>
<tr>
<td>SimpleCIDRAffinityFilter</td>
<td>
<p>根据<span style="background-color: #c0c0c0;">宿主机的IP地址CIDR</span>进行过滤，指定两个提示：</p>
<p style="padding-left: 30px;">build_near_host_ip  CIDR的第一个IP <br />cidr  CIDR的掩码部分</p>
<pre class="crayon-plain-tag">openstack server create --image cedef40a-ed67-4d10-800e-17455edce175 \
  --flavor 1 --hint build_near_host_ip=192.168.1.1 --hint cidr=/24 server-1</pre>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">Weights</span></div>
<p>经过过滤后，可能仍然有多个宿主机满足需求。那么，到底选择哪一个？这时需要基于权重来确定。
<p>一个宿主机的<span style="background-color: #c0c0c0;">最初权重由它拥有的硬件资源来确定</span>，<span style="background-color: #c0c0c0;">每当调度新的实例到它上面，宿主机的权重值就变小</span>。 </p>
<p>权重计算算法的参数，配置在nova.conf中：</p>
<pre class="crayon-plain-tag">[DEFAULT]

; 注意：主机聚合可以覆盖这里的权重因子设置
; 内存权重因子
ram_weight_multiplier
; 磁盘权重因子
disk_weight_multiplier
; 处理器权重因子
cpu_weight_multiplier
; IO负载权重因子，负数表示倾向于选择轻负载的宿主机
io_ops_weight_multiplier

; 进行权重判断后，会返回N个最适合主机，然后在随机从中取一个作为最终宿主机
; 该选项决定N的大小
scheduler_host_subset_size

; 默认权重值最大的获胜
scheduler_weight_classes = nova.scheduler.weights.all_weighers

[filter_scheduler]
; 对于主机组进行软亲和时时候
soft_affinity_weight_multiplier
; 对于主机组进行软反亲和时时候
soft_anti_affinity_weight_multiplier
; 对于最近发生创建虚拟机失败的宿主机，设置权重因子
; 设置为负数，则最近失败的主机更加少的机会被选择
build_failure_weight_multiplier
; 在跨Cell移动实例时，使用该权重因子
cross_cell_move_weight_multiplier </pre>
<div class="blog_h3"><span class="graybg">Compute capabilities作为Trait</span></div>
<p>从S版开始，nova-compute会基于计算驱动的capabilities报告为COMPUTE_开头的（资源提供者的）Trait（特性）。</p>
<p>通过配置Flavor，可以指定实例要求、禁止哪些Trait。例如，某个主机聚合支持multi-attach卷，你可以限制某个Flavor仅仅调度到这个主机聚合：</p>
<ol>
<li>为Flavor设置extra_specs：<pre class="crayon-plain-tag">trait:COMPUTE_VOLUME_MULTI_ATTACH=required</pre></li>
<li>按照常规方式限制Flavor到主机聚合</li>
</ol>
<pre class="crayon-plain-tag">openstack --os-compute-api-version=2.53 hypervisor list
# 列出trait
openstack --os-placement-api-version 1.6 resource provider trait list 8fa133f5-a41e-4ef6-a485-cb0d6e167860 </pre>
<p>关于基于计算驱动capabilities定义的Trait，需要注意：</p>
<ol>
<li>计算服务拥有这些COMPUTE_开头的Trait的控制权，nova-compute服务启动后，或者update_available_resource定时任务执行后，会自动添加/擅长Traits</li>
<li>用户自定义的Trait，不会被删除。除非定义的Trait以COMPUTE_开头</li>
<li>如果用户通过命令：<pre class="crayon-plain-tag">openstack resource provider trait delete</pre> 删除某个资源提供者的COMPUTE_*，计算服务会在重启时再次添加</li>
</ol>
<div class="blog_h2"><span class="graybg">可用区</span></div>
<p>在OpenStack中，可用区是一个用户可见的逻辑的云分区。在创建主机时，用户可以指定，创建到哪个可用区。</p>
<p><span style="background-color: #c0c0c0;">可用区没有在数据库中建模，而是定义为主机聚合的元数据</span>。为主机聚合添加特定的元数据，即可将其中的主机加入到某个可用区。</p>
<p>需要注意主机聚合和AZ的不同：</p>
<ol>
<li>宿主机可以属于多个主机聚合，但是只能属于一个AZ</li>
<li><span style="background-color: #c0c0c0;">默认情况下，主机是默认AZ的成员</span>，即使它不属于任何主机聚合</li>
</ol>
<p>其它OpenStack服务，例如网络、块存储服务，也有可用区的概念，但是实现方式各不相同。</p>
<div class="blog_h3"><span class="graybg">默认可用区</span></div>
<p>默认AZ的名字，可以在nova.conf中配置：</p>
<pre class="crayon-plain-tag">[DEFAULT]
default_availability_zone = nova</pre>
<p>该配置项指定计算服务（nova-compute组件）的默认可用区的名字。 </p>
<div class="blog_h3"><span class="graybg">配置可用区</span></div>
<p>可用区是在主机聚合上设置的：</p>
<pre class="crayon-plain-tag"># --os-compute-api-version=2.53
openstack aggregate create zircon
openstack aggregate add host zircon centos-11
openstack aggregate add host zircon centos-12
openstack aggregate add host zircon centos-13

# 设置聚合的可用区
openstack aggregate set --property availability_zone=zircon zircon</pre>
<p>将主机聚合关联到可用区的操作，需要提前规划。聚合中任何主机上已经实例，则无法设置可用区。</p>
<div class="blog_h3"><span class="graybg">对迁移的影响</span></div>
<p>导致实例所在宿主机改变的操作包括 evacuate, resize, cold migrate, live migrate 以及 unshelve。其中<span style="background-color: #c0c0c0;">只有evacuate和live migrate可以绕过调度器，强制指定目标主机</span>。</p>
<p>如果满足以下条件之一，迁移后的实例限定在特定的AZ中：</p>
<ol>
<li>创建实例时，指定了availability_zone参数，即指明在特地AZ中创建实例</li>
<li>虽然没有指定availability_zone，但是API service配置了 default_schedule_zone</li>
<li>2.77版本之后，Unshelve实例时，指定了availability_zone</li>
<li>cinder.cross_az_attach设置为False，default_schedule_zone也没有设置，但是实例使用了卷，这样会调度到卷所在的AZ</li>
</ol>
<p>如果实例没有在特定AZ内创建，则它可以被自由的移动到其它AZ， AvailabilityZoneFilter不做任何事情。</p>
<p>需要注意，如果实例在某个AZ内创建的情况下，通过evacuate或者live migrate将其<span style="background-color: #c0c0c0;">移动到另外一个AZ的主机上，是个危险的操作</span>。因为假设后续你又resize实例，调度器会将其转移到原先的AZ。</p>
<div class="blog_h3"><span class="graybg">资源亲和</span></div>
<p>Noava的配置项cinder.cross_az_attach，用于限制实例和它使用的卷在相同的AZ中。如果设置为False，则计算和存储资源会位于相同AZ，如果无法满足，则请求会失败：</p>
<ol>
<li>创建实例时，将一个已存在的卷挂载给它，那么实例将创建到卷所在的AZ</li>
<li>创建实例时，需要创建一个新卷挂载给它，那么Nova将会在实例所在的AZ中创建卷</li>
</ol>
<div class="blog_h2"><span class="graybg">管理卷</span></div>
<p>关于在Nova中使用、创建、管理卷，参考<a href="#server-create">openstack server create</a>。</p>
<div class="blog_h3"><span class="graybg">multi-attach</span></div>
<p>Nova从Q版开始支持Cinder的多重挂载。前提条件：</p>
<ol>
<li>Compute API最小版本是2.6</li>
<li>底层的Hypervisor驱动必须支持将卷挂载到多个客户机。使用libvirt驱动时，<span style="background-color: #c0c0c0;">libvirt必须大于3.10，qemu必须大于2.10</span></li>
<li>不支持swap一个正在使用的multiattach卷</li>
</ol>
<div class="blog_h2"><span class="graybg">远程访问</span></div>
<p>OpenStack支持多种控制台来连接到客户机，包括VNC、SPICE、Serial、RDP、MKS（VMware vSphere）。推荐仅仅部署一种类型的控制台支持，此外需要注意某些Hypervisor不支持某些控制台类型。</p>
<p>为了连接到虚拟机控制台，<span style="background-color: #c0c0c0;">计算节点的5900-5999端口必须开启</span>。</p>
<div class="blog_h3"><span class="graybg">控制台代理</span></div>
<p>不管使用哪种控制台，都需要部署console proxy服务。该服务负责：</p>
<ol>
<li>提供用户所在的公共网络和虚拟机所在的私有网络之间的桥梁</li>
<li>中介Token验证</li>
<li>屏蔽Hypervisor相关的连接细节，给用户一致的体验</li>
</ol>
<p>对于<span style="background-color: #c0c0c0;">某些Hypervisor + Console驱动的组合，控制台代理是Hypervisor/其它外部服务提供</span>的。其它的则由Nova提供控制台代理服务。Nova控制台代理的工作方式如下（以基于noVNC的VNC控制台为例）：</p>
<ol>
<li>用户访问OpenStack API，获取控制台访问URL，例如：http://ip:port/?path=%3Ftoken%3Dxyz</li>
<li>用户在浏览器打开控制台</li>
<li>浏览器连接到代理</li>
<li>代理校验用户的Token，映射Token到私有网络中的、实例的VNC服务器的地址:端口</li>
<li>计算节点在vnc.server_proxyclient_address中指定代理应该如何连接到本机的VNC服务器，代理通过此地址连接到VNC服务器</li>
</ol>
<p>要启用基于noVNC的VNC控制台，OpenStack需要部署以下额外组件：</p>
<ol>
<li>一个或多个 nova-novncproxy服务，以支持<span style="background-color: #c0c0c0;">基于浏览器的noVNC客户端</span>。在简单部署场景中，此服务运行在nova-api所在机器上，因为它是公共、私有网络之间的桥梁</li>
</ol>
<div class="blog_h3"><span class="graybg">基于noVNC的VNC控制台</span></div>
<p>VNC是很多Hypervisor和客户端支持的图形化控制台。noVNC支持通过浏览器访问VNC。</p>
<p>配置nova-novncproxy服务：</p>
<pre class="crayon-plain-tag">[vnc]
novncproxy_host = 0.0.0.0
novncproxy_port = 6082</pre>
<p>配置nova-compute服务： </p>
<pre class="crayon-plain-tag">[vnc]
enabled = True
novncproxy_base_url = http://os.gmem.cc:6082/vnc_auto.html
server_listen = 127.0.0.1
server_proxyclient_address = 127.0.0.1</pre>
<div class="blog_h3"><span class="graybg">串口控制台</span></div>
<p>使用串口控制台（serial console）可以检查虚拟机的内核输出，查看其它虚拟消息。串口控制台在虚拟机的网络连接不可用时特别有效。</p>
<p>从J版开始，OpenStack支持可读写的串口控制台。你需要在计算节点上配置：</p>
<pre class="crayon-plain-tag">[serial_console]
; ...
enabled = true
base_url = ws://os.gmem.cc:6083/
; 监听虚拟控制台请求的地址
listen = 0.0.0.0
; 控制台代理连接到哪个网络接口，通常管理网
proxyclient_address = MANAGEMENT_INTERFACE_IP_ADDRESS</pre>
<p>使用下面的命令后的串口控制台的WS地址：</p>
<pre class="crayon-plain-tag">nova get-serial-proxy INSTANCE_NAME</pre>
<div class="blog_h2"><span class="graybg">注入密码</span></div>
<p>Nova支持注入密码到虚拟机的管理员用户，密码会打印在openstack server create命令的输出中。</p>
<p>默认情况下，仪表盘会显示管理员密码并允许修改。如果希望禁用此特性：</p>
<pre class="crayon-plain-tag">PENSTACK_HYPERVISOR_FEATURES = {
...
    'can_set_password': False,
}</pre>
<p>对于使用libvirt后端的Hypervisor（KVM/QEMU/LXC），管理员密码注入默认禁用，要启用需要修改：</p>
<pre class="crayon-plain-tag">[libvirt]
inject_password=true</pre>
<div class="blog_h2"><span class="graybg">配置文件</span></div>
<pre class="crayon-plain-tag">[libvirt]
; 使用KVM以提升性能
virt_type = kvm
; 创建的实例的CPU模式，参考
; https://blog.gmem.cc/libvirt-study-note#cpu-mode
cpu_mode=host-passthrough</pre>
<div class="blog_h1"><span class="graybg">Placement</span></div>
<div class="blog_h2"><span class="graybg">osc-placement</span></div>
<p>默认情况下，很多placement相关的OpenStackt命令不可用，需要安装osc-placement插件。</p>
<p>默认情况下，使用的Placement API版本是1.0。要使用其它版本，需要指定<pre class="crayon-plain-tag">--os-placement-api-version</pre>命令行标记，或者设置环境变量：</p>
<pre class="crayon-plain-tag">export OS_PLACEMENT_API_VERSION=1.6 </pre>
<div class="blog_h1"><span class="graybg">Neutron</span></div>
<div class="blog_h2"><span class="graybg">简介</span></div>
<p>OpenStack Networking组件，即Neutron，允许你创建网络接口设备、将网络接口设备Attach到虚拟网络。Neutron基于<span style="background-color: #c0c0c0;">插件化机制</span>设计，用以支持不同的网络设备以及软件。</p>
<p>Neutron负责管理虚拟网络基础设施（Virtual Networking Infrastructure，VNI）的方方面面、以及物理网络基础设施（Physical Networking Infrastructure，PNI）的访问/接入层面。</p>
<p>Neutron支持<span style="background-color: #c0c0c0;">防火墙、VPN等高级网络</span>特性。</p>
<p>Neutron提供<span style="background-color: #c0c0c0;">网络、子网、路由器</span>的抽象，这些抽象模拟相应物理实体的特性。例如，网络包含子网，路由器负责再不同子网/网络之间进行封包路由。</p>
<p>任何给定的网络方案（set up），<span style="background-color: #c0c0c0;">至少包含一个外部网络</span>（External network）。与其它网络不同，外部网络不仅仅是虚拟网络，他是真实物理网络（可以访问OpenStack外部）的一种视图。外部网络上的IP地址，可以从OpenStack外部直接访问到。</p>
<p>除了外部网络之外，任何网络方案<span style="background-color: #c0c0c0;">至少包含一个内部网络</span>（Internal network），内部网络是虚拟（软件定义的）网络，<span style="background-color: #c0c0c0;">直接将VM连接在一起。</span></p>
<p>为了从<span style="background-color: #c0c0c0;">外部访问VM（或者反之），需要添加虚拟路由器</span>。每个路由器<span style="background-color: #c0c0c0;">包含一个网关，通向外部网络；包含1-N个接口，通往内部网络</span>。和物理路由器类似，连接到同一个路由器的<span style="background-color: #c0c0c0;">子网之间可以相互访问</span>。VM可以<span style="background-color: #c0c0c0;">通过路由器的网关访问外部网络</span>。</p>
<p>当有什么连接到一个子网时，那个<span style="background-color: #c0c0c0;">连接点（connection）就称为端口（port）</span>。你将外部网络的IP地址分配到内部网络的（由于VM连接到子网而产生的）端口上，这样，外部实体就能直接访问VM。</p>
<p>Neutron支持安全组（security groups），<span style="background-color: #c0c0c0;">安全组让管理员可以按组来定义防火墙规则</span>。一个VM可以归属1-N个安全组，Neutron根据安全组中的规则，来阻止VM访问端口、端口范围。</p>
<div class="blog_h2"><span class="graybg">组件</span></div>
<p>一个典型的Netron部署，包含了多个服务（service）和代理（agent），这些组件可能运行在一个或多个节点上。</p>
<div class="blog_h3"><span class="graybg">neutron-server</span></div>
<p>接受API请求，并路由给适当的OpenStack Networking plug-in进行处理。作为访问数据库的中心点。</p>
<div class="blog_h3"><span class="graybg">插件</span></div>
<p>Neutron使用插件化的架构，各种可拔插功能依赖于plugin和agent实现。</p>
<div class="blog_h3"><span class="graybg">L2代理</span></div>
<p>使用通用/厂商特定的技术，来提供网络分段（segmentation）和隔离（isolation），也就是划分出子网（<span style="background-color: #c0c0c0;">network number相同的主机位于同一个子网，子网是个以太网</span>）。</p>
<p>L2代理应当运行在任何需要网络连接、提供虚拟接口安全性的节点上，包括计算、网络节点。</p>
<p>OpenStack自带了Cisco 虚拟/物理交换机、NEC OpenFlow产品、Open vSwitch、Linux Bridging、VMware NSX产品的代理。</p>
<div class="blog_h3"><span class="graybg">L3代理</span></div>
<p>运行在网络节点上，提供东西向、南北向的路由能力，并提供FWaaS、VPNaaS之类的高级特性。</p>
<div class="blog_h3"><span class="graybg">消息队列</span></div>
<p>用于再Neutron服务器和各种代理之间进行消息交换，也用作某些特定插件存储网络状态的数据库。</p>
<div class="blog_h2"><span class="graybg">配置概述</span></div>
<p>主配置文件neutron.conf，neutron-server和各种Agent都会读取。该文件包含用于Neutron内部RPC的oslo.messaging配置，并且会包含一些和主机相关的信息。此配置文件还包括database、nova、keystone的凭证信息。</p>
<p>此外neutron-server可能会加载plugin特定的配置文件。而Agent则不会加载。原因是插件配置主要是全局范围的选项。</p>
<p>每个不同的Agent可能有自己的配置文件，他们应当在主配置之后加载。因此其中的配置项优先级更高。代理配置文件中，可能包含主机特定的配置，例如local_ip。</p>
<div class="blog_h2"><span class="graybg">ML2</span></div>
<p>模块化L2（Modular Layer 2 ，ML2）插件是Neutron的L2框架，它允许你在一个部署中使用多种L2网络技术。ML2的扩展点是两种不同类型的驱动</p>
<div class="blog_h3"><span class="graybg">Type驱动</span></div>
<p>网络类型驱动，定义了OpenStack网络的技术分类，例如VxLAN、flat。</p>
<p>每种实现技术都对应了一个ML2 Type驱动。这些驱动会维护任何需要的、和网络类型相关的状态。它会验证Provider网络上和网络类型有关的信息，并且负责在项目的网络中分配一个空闲的段。</p>
<div class="blog_h3"><span class="graybg">Mechanism驱动</span></div>
<p>网络机制驱动，定义了OpenStack网络的实现技术，例如flat网络可以利用Linux bridge或者OVS来实现。</p>
<p>Mechanism驱动会利用Type驱动所产生的信息，并且确保这些信息并应用。</p>
<p>Mechanism驱动能通过RPC利用L2代理，也能够直接和外部控制器/设备进行交互。</p>
<div class="blog_h3"><span class="graybg">驱动兼容矩阵</span></div>
<p>Mechanism驱动和Type驱动的搭配，不是任意的：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">Mech\Type</td>
<td style="text-align: center;">Flat</td>
<td style="text-align: center;">VLAN</td>
<td style="text-align: center;">VxLAN</td>
<td style="text-align: center;">GRE</td>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Open vSwitch</strong></td>
<td>Y</td>
<td>Y</td>
<td>Y</td>
<td>Y</td>
</tr>
<tr>
<td><strong>Linx Bridge</strong></td>
<td>Y</td>
<td>Y</td>
<td>Y</td>
<td>N</td>
</tr>
<tr>
<td><strong>SRIOV</strong></td>
<td>Y</td>
<td>Y</td>
<td>N</td>
<td>N</td>
</tr>
<tr>
<td><strong>MacVTap</strong></td>
<td>Y</td>
<td>N</td>
<td>N</td>
<td>N</td>
</tr>
<tr>
<td><strong>L2 population</strong></td>
<td>N</td>
<td>N</td>
<td>Y</td>
<td>Y</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">Type驱动配置</span></div>
<p>在ML2的主配置文件/etc/neutron/plugins/ml2/ml2_conf.ini中配置：</p>
<pre class="crayon-plain-tag">[ml2]
; 支持的Type驱动列表
type_drivers = flat,vlan,vxlan,gre


; flat相关配置
[ml2_type_flat]
; 可以从中创建flat网络的物理网络（physical_network）的名称
; 设置为*则允许使用任何物理网络名
; 设置为空则禁用flat网络
flat_networks = provider


; vlan相关配置
[ml2_type_vlan]
; 格式 &lt;physical_network&gt;:&lt;vlan_min&gt;:&lt;vlan_max&gt; 或者 &lt;physical_network&gt;
; 指定可用作VLAN provider以及租户网络的物理网络名称，以及可分配的VLAN Tag范围
network_vlan_ranges = provider:0:200


; vxlan相关配置
[ml2_type_vxlan]
; 格式 &lt;vni_min&gt;:&lt;vni_max&gt;,&lt;vni_min&gt;:&lt;vni_max&gt;... 列出VxLAN VNI ID范围
vni_ranges = 
; VxLAN的多播组，如果配置，所有广播流量发送到此组；如果不配置，禁用multicast VxLAN模式
vxlan_group =</pre>
<div class="blog_h3"><span class="graybg">Mech驱动</span></div>
<p>在ML2的主配置文件/etc/neutron/plugins/ml2/ml2_conf.ini中配置：</p>
<pre class="crayon-plain-tag">[ml2]
;支持的Mech驱动列表
mechanism_drivers = ovs,l2pop</pre>
<p>更多的配置查看相关Agent的配置文件。</p>
<div class="blog_h2"><span class="graybg">L2代理</span></div>
<div class="blog_h3"><span class="graybg">Linux Bridge</span></div>
<p>该代理通过配置Linux Bridge来为OpenStack资源实现L2网络。配置文件路径 /etc/neutron/plugins/ml2/linuxbridge_agent.ini</p>
<pre class="crayon-plain-tag">[DEFAULT]
; 是否打开DEBUG级别的日志，默认INFO
debug = False

[agent]
; 每隔多少秒，Agent来轮询本地设备的变化情况
polling_interval = 2
; 封装为外层IP报文时，设置的DSCP值。用于overlay网络。0-63之间的整数
dscp = 
; 如果设置为True，则从内层封包取得DSCP，设置到外层封包上
dscp_inherit = False
; 使用的扩展列表
extensions = 

[linux_bridge]
; 格式：&lt;physical_network&gt;:&lt;physical_interface&gt;,&lt;physical_network&gt;:&lt;physical_interface&gt;...
; 将物理网络名称映射到Agent节点上的物理网络接口名。这样flat和VLAN网络才能利用这些网络
physical_interface_mappings = provider:eth0
; 格式：&lt;physical_network&gt;:&lt;physical_bridge&gt;
bridge_mappings

[securitygroup]
; 此L2代理使用的安全组防火墙驱动
firewall_driver = 
; 是否启用neutron security group API，如果不使用安全组，或者使用nova security group API，则禁用
enable_security_group = True
; 十分启用IPSet提升iptables的性能
enable_ipset = True
; 逗号分隔的，允许的ethertypes。0x开头的16进制形式
permitted_ethertypes = 

[vxlan]
; 此Agent是否支持VxLAN
enable_vxlan = Tre
; VxLAN接口协议包的TTL
ttl =
; VxLAN接口协议包的TOS，已经废弃，使用agent段的dscp选项代替
tos = 
; VxLAN接口的多播组
; 如果指定组地址的范围，必须使用CIDR格式
vxlan_group = 224.0.0.1
; 本地的Overlay(Tunnel)网络端点的IP地址，使用一个IPv4/IPv6的、位于宿主机网络接口上的地址
local_ip = 
; VxLAN包的UDP源地址范围
udp_srcport_min = 
udp_srcport_max = 
; VxLAN的UDP目的地址
udp_dstport = 
; 使用Local ARP responder，它提供本地响应，而非在overlay范围内进行ARP广播
; Local ARP responder和allowed-address-pairs扩展存在兼容性问题
arp_responder = 
; 描述组播地址和VLAN（VNI ID）之间的映射关系
; &lt;multicast address&gt;:&lt;vni_min&gt;:&lt;vni_max&gt; 逗号分隔
multicast_ranges</pre>
<div class="blog_h2"><span class="graybg">QoS</span></div>
<p>参考<a href="#neutron-qos">配额和限速 - Neutron QoS</a></p>
<div class="blog_h2"><span class="graybg">内部DNS</span></div>
<p>OpenStack支持对以下资源配置DNS：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">资源</td>
<td style="text-align: center;">DNS名</td>
<td style="text-align: center;">DNS域</td>
</tr>
</thead>
<tbody>
<tr>
<td>端口/Ports</td>
<td>是</td>
<td>否</td>
</tr>
<tr>
<td>网络/Networks</td>
<td>否</td>
<td>是</td>
</tr>
<tr>
<td>浮动IP</td>
<td>是</td>
<td>是</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">配置内置解析</span></div>
<p>要更改默认的域名后缀openstacklocal，需要修改neutron-server节点的：</p>
<pre class="crayon-plain-tag">[default]
dns_domain = os.gmem.cc
; router插件需要启用，否则报错Extensions not found: ['auto-allocated-topology', 'dns-integration'].
service_plugins = router</pre><br />
<pre class="crayon-plain-tag">[ml2]
# dns即DNS Integration。是dns_domain_ports的子集
# dns_domain_ports，额外允许设置port的dns_domain属性
extension_drivers = port_security,dns</pre>
<p>重启服务：</p>
<pre class="crayon-plain-tag">systemctl restart neutron-server.service</pre>
<div class="blog_h3"><span class="graybg">配置DNS相关属性</span></div>
<p>你可以为一个Port配置DNS名：</p>
<pre class="crayon-plain-tag">neutron port-create my-net --dns_name my-port</pre>
<p>当Port分配给实例时，实例的主机名+域名后缀，会自动成为Port的FQDN，例如centos8-amd64.os.gmem.cc</p>
<div class="blog_h1"><span class="graybg">designate</span></div>
<p>OpenStack DNS Service，即Designate，是一个支持多租户的OpenStack的DNSaaS服务。它提供集成了keystone身份验证的REST API，能够基于Nova/Neutron动作自动生成DNS记录。Designate支持多种DNS服务器，包括Bind9和PowerDNS 4</p>
<div class="blog_h2"><span class="graybg">组件</span></div>
<div class="blog_h3"><span class="graybg">designate-api </span></div>
<p>提供REST API接口。</p>
<div class="blog_h3"><span class="graybg">designate-central</span></div>
<p>编排Zones和RecordSet的创建、删除和更新。</p>
<div class="blog_h3"><span class="graybg">designate-producer</span></div>
<p>编排周期性任务。</p>
<div class="blog_h3"><span class="graybg">designate-worker</span></div>
<p>运行任务，例如Zone的创建/删除/更新，以及来自designate-producer的周期性任务。</p>
<div class="blog_h3"><span class="graybg">designate-mdns</span></div>
<p>一个小的DNS服务器，负责推送DNS Zone信息到面向客户的DNS服务器，也能够拉取Designate基础设施之外的DNS Zone信息。</p>
<div class="blog_h3"><span class="graybg">designate-agent</span></div>
<p>某些DNS服务要求在本地执行命令，此代理负责对接到这些DNS服务。</p>
<div class="blog_h3"><span class="graybg">面向客户的DNS服务</span></div>
<p>为客户提供DNS服务，由designate-worker管理其记录。Bind9、Power DNS 4被支持的很好。</p>
<div class="blog_h2"><span class="graybg">安装配置</span></div>
<div class="blog_h3"><span class="graybg">客户端安装</span></div>
<pre class="crayon-plain-tag">sudo apt install python3-designateclient</pre>
<div class="blog_h3"><span class="graybg">安装软件</span></div>
<p>安装openstack-designate以及相关依赖：</p>
<pre class="crayon-plain-tag">dnf install openstack-designate-* bind bind-utils -y</pre>
<div class="blog_h3"><span class="graybg">创建数据库</span></div>
<pre class="crayon-plain-tag">CREATE DATABASE IF NOT EXISTS designate CHARACTER SET utf8 COLLATE utf8_general_ci;
GRANT ALL PRIVILEGES ON designate.* TO 'designate'@'localhost' IDENTIFIED BY 'designate';
GRANT ALL PRIVILEGES ON designate.* TO 'designate'@'%' IDENTIFIED BY 'designate'; </pre>
<div class="blog_h3"><span class="graybg">创建OS对象</span></div>
<pre class="crayon-plain-tag">openstack user create --domain default --password designate designate
openstack role add --project service --user designate admin
openstack service create --name designate --description "DNS" dns
openstack endpoint create --region china dns public http://openstack.gmem.cc:9001/</pre>
<p>创建一个RNDC key：</p>
<pre class="crayon-plain-tag">rndc-confgen -a -k designate -c /etc/designate/rndc.key -r /dev/urandom
chown named:named /etc/designate/rndc.key
chmod 644 /etc/rndc.key</pre>
<div class="blog_h3"><span class="graybg">修改named配置文件</span></div>
<pre class="crayon-plain-tag">...
include "/etc/designate/rndc.key";

options {
    ...
    allow-new-zones yes;
    request-ixfr no;
    listen-on port 53 { any; };
    recursion no;
    allow-query { any; };
};

controls {
  inet 127.0.0.1 port 953
    allow { 127.0.0.1; } keys { "designate"; };
};</pre>
<p>改完重启服务：</p>
<pre class="crayon-plain-tag">systemctl enable named.service
systemctl start named.service</pre>
<div class="blog_h3"><span class="graybg">配置OpenStack</span></div>
<pre class="crayon-plain-tag">[DEFAULT]
transport_url = rabbit://openstack:openstack@openstack.gmem.cc

[service:api]
listen = 0.0.0.0:9001
auth_strategy = keystone
enable_api_v2 = True
enable_api_admin = True
enable_host_header = True
enabled_extensions_admin = quotas, reports

[keystone_authtoken]
auth_type = password
username = designate
password = designate
project_name = service
project_domain_name = Default
user_domain_name = Default
www_authenticate_uri = http://openstack.gmem.cc:5000/
auth_url = http://openstack.gmem.cc:5000/
memcached_servers = openstack.gmem.cc:11211

[storage:sqlalchemy]
connection = mysql+pymysql://designate:designate@openstack.gmem.cc/designate</pre>
<div class="blog_h3"><span class="graybg">初始化数据库</span></div>
<pre class="crayon-plain-tag">su -s /bin/sh -c "designate-manage database sync" designate</pre>
<div class="blog_h3"><span class="graybg">创建池</span></div>
<pre class="crayon-plain-tag">- name: default
  # 池的名字，创建后不可改变。除非删除（连同关联的Zones）并重建
  description: default

  attributes: {}

  # 该池负责管理的Zone的NS记录
  # 记录应当在Designate之外创建, 指向控制节点的公共IP
  ns_records:
    - hostname: ns.openstack.gmem.cc.
      priority: 1

  # 该池使用的DNS服务器列表，也就是Bind服务器的地址
  nameservers:
    - host: 127.0.0.1
      port: 53

  # 该池的目标列表，对于Bind来说，每个BIND服务器对应一个条目
  # designate会在每个Bind服务器上运行rndc命令
  targets:
    - type: bind9
      description: bind9 on openstack-1
      # 列出designate-mdns服务地址，Bind服务器向其发送zone transfers (AXFRs)请求
      # 应当是控制节点的IP
      masters:
        - host: 127.0.0.1
          port: 5354

      options:
        host: 127.0.0.1
        port: 53
        rndc_host: 127.0.0.1
        rndc_port: 953
        rndc_key_file: /etc/designate/rndc.key</pre>
<p>执行下面的命令创建池：</p>
<pre class="crayon-plain-tag">su -s /bin/sh -c "designate-manage pool update" designate</pre>
<div class="blog_h3"><span class="graybg">启用服务 </span></div>
<pre class="crayon-plain-tag">systemctl start designate-central designate-api
systemctl enable designate-central designate-api

systemctl start designate-worker designate-producer designate-mdns
systemctl enable designate-worker designate-producer designate-mdns</pre>
<div class="blog_h3"><span class="graybg">验证 </span></div>
<p>查看DNS服务列表：</p>
<pre class="crayon-plain-tag">openstack dns service list
+--------------------------------------+-------------+--------------+--------+-------+--------------+
| id                                   | hostname    | service_name | status | stats | capabilities |
+--------------------------------------+-------------+--------------+--------+-------+--------------+
| a071880d-a2d4-468a-a452-da4e7856a63c | openstack-1 | api          | UP     | -     | -            |
| e7b8a2dd-c92e-41fb-bd23-c342c76de154 | openstack-1 | central      | UP     | -     | -            |
| 502c5b1f-1036-4a84-9b3e-14c7255b20eb | openstack-1 | mdns         | UP     | -     | -            |
| a2f0d9ad-366b-4ec2-8b92-d2f0765f2333 | openstack-1 | producer     | UP     | -     | -            |
| d64fa762-3598-4477-b7bc-a4394b326177 | openstack-1 | worker       | UP     | -     | -            |
+--------------------------------------+-------------+--------------+--------+-------+--------------+</pre>
<p>创建一个DNS Zone：</p>
<pre class="crayon-plain-tag">openstack zone create --email admin@gmem.cc os.gmem.cc.</pre>
<p>确认其状态到达ACTIVE：</p>
<pre class="crayon-plain-tag">openstack zone list
+--------------------------------------+-------------+---------+------------+--------+--------+
| id                                   | name        | type    |     serial | status | action |
+--------------------------------------+-------------+---------+------------+--------+--------+
| 2093169d-15d5-4e9c-8b94-a6569ffac7b8 | os.gmem.cc. | PRIMARY | 1618994404 | ACTIVE | NONE   |
+--------------------------------------+-------------+---------+------------+--------+--------+</pre>
<p>创建一个记录集（RecordSet）：</p>
<pre class="crayon-plain-tag">openstack recordset create --record '10.1.1.1' --type A os.gmem.cc. horizon</pre>
<div class="blog_h2"><span class="graybg"> 外部DNS</span></div>
<p>Neutron支持将域名通过designate暴露给外部。</p>
<div class="blog_h3"><span class="graybg">配置neutron-server</span></div>
<pre class="crayon-plain-tag">[default]
# 决定了配置了dns_domain的port默认所在Zone
dns_domain = os.gmem.cc.
external_dns_driver = designate

[designate]
url = http://openstack.gmem.cc:9001/v2
admin_auth_url = http://openstack.gmem.cc:35357/v2.0
admin_username = neutron
admin_password = neutron
admin_tenant_name = service
allow_reverse_dns_lookup = True
ipv4_ptr_zone_prefix_size = 16
ipv6_ptr_zone_prefix_size = 116</pre><br />
<pre class="crayon-plain-tag">extension_drivers=port_security,dns</pre>
<div class="blog_h3"><span class="graybg">UC1：在外部DNS中发布Port</span></div>
<p>用例说明：用户创建在一个可被外部访问的网络上创建实例，并且希望从集群外部通过域名来访问实例。</p>
<p>参考步骤：</p>
<ol>
<li>前提条件：
<ol>
<li><span style="background-color: #c0c0c0;">不支持使用 --external 创建的网络</span></li>
<li>支持FLAT, VLAN, GRE, VXLAN, GENEVE类型的网络</li>
<li>对于FLAT, VLAN, GRE, VXLAN, GENEVE，其segmentation ID必须位于分配给project networks的范围之外</li>
</ol>
</li>
<li>给网络的dns_domain属性分配一个合法的值：<br />
<pre class="crayon-plain-tag"># neutron net-update 5fdf474c-533a-403d-b7dd-cc7d7e1dfa23 --dns_domain os.gmem.cc.
openstack network set --dns-domain os.gmem.cc. provider</pre>
</li>
<li>
<p>查看此Zone中的记录：</p>
<pre class="crayon-plain-tag">openstack recordset list os.gmem.cc.
+--------------------------------------+-------------+------+---------------------------------------------------------------------+--------+--------+
| id                                   | name        | type | records                                                             | status | action |
+--------------------------------------+-------------+------+---------------------------------------------------------------------+--------+--------+
| 22422e08-0c17-4106-a222-35c3e535d37a | os.gmem.cc. | NS   | ns.openstack.gmem.cc.                                               | ACTIVE | NONE   |
| e4683c9e-70c0-4d3b-83e1-bb5530059311 | os.gmem.cc. | SOA  | ns.openstack.gmem.cc. admin.gmem.cc. 1618997152 3586 600 86400 3600 | ACTIVE | NONE   |
+--------------------------------------+-------------+------+---------------------------------------------------------------------+--------+--------+</pre>
</li>
<li>
<p> 创建一个端口，配置DNS名：
<pre class="crayon-plain-tag">openstack port create --dns-name t3m1 --network provider \
    --fixed-ip subnet=provider,ip-address=10.2.0.111 --disable-port-security tcnp3-master-1
# | dns_assignment          | fqdn='t3m1.os.gmem.cc.', hostname='t3m1', ip_address='10.2.0.111'         |
 </pre>
</li>
<li>再次查看Zone，应该发现名为t3m1.os.gmem.cc.的新A记录：<br />
<pre class="crayon-plain-tag">openstack recordset list os.gmem.cc.
# | f81bc3c2-2272-4f21-93d3-2c4fc9e6435d | t3m1.os.gmem.cc. | A    | 10.2.0.111   | ACTIVE | NONE   |</pre>
</li>
</ol>
<p>当把端口授予实例后，<span style="background-color: #c0c0c0;">原先的A记录被替换，前缀改为实例的主机名</span>。
<div class="blog_h1"><span class="graybg">Cinder</span></div>
<p>OpenStack Block Storage service，即Cinder为VM提供块设备。块设备如何产生、如何被消费，取决于<span style="background-color: #c0c0c0;">块设备驱动</span>。如果使用多后端配置，则取决于多个驱动。Cinder具有多种驱动，包括NAS/SAN, NFS, iSCSI, Ceph，等等。</p>
<p>Block Storage API组件和Scheduler服务通常运行在控制节点上。取决于使用的驱动，Volume服务可能运行在控制节点、计算节点，或者独立的存储节点上。</p>
<p>Cinder和Nova交互，从而将卷提供给虚拟机实例。</p>
<div class="blog_h2"><span class="graybg">组件</span></div>
<div class="blog_h3"><span class="graybg">cinder-api</span></div>
<p>接受API请求，路由给cinder-volume处理。</p>
<div class="blog_h3"><span class="graybg">cinder-volume</span></div>
<p>直接（或者通过消息队列）和后端存储服务交互、cinder-scheduler这样的组件交互。</p>
<p>cinder-volume会响应针对后端存储服务的读写请求，从而维持状态。它是通过驱动架构与后端交互的。</p>
<div class="blog_h3"><span class="graybg">cinder-scheduler</span></div>
<p>这是一个守护程序，负责选取一个最优化的、能够提供卷的节点，并由节点创建卷。</p>
<div class="blog_h3"><span class="graybg">cinder-backup</span></div>
<p>这是一个守护程序，负责备份任意类型的卷到某个Backup storage provider。它是通过驱动架构与后端交互的。</p>
<div class="blog_h3"><span class="graybg">消息队列</span></div>
<p>在上述进程之间交换信息。</p>
<div class="blog_h2"><span class="graybg">备份后端配置</span></div>
<div class="blog_h3"><span class="graybg">nfs</span></div>
<pre class="crayon-plain-tag">[DEFAULT]
backup_driver = cinder.backup.drivers.nfs.NFSBackupDriver
backup_share = 10.0.0.1:/slow</pre>
<div class="blog_h2"><span class="graybg">卷后端配置</span></div>
<div class="blog_h3"><span class="graybg">lvm</span></div>
<p>参考上文的样例环境。</p>
<p>需要注意的是，LVM后端可以映射为远程宿主机的磁盘。当卷从位于LVM后端上的快照上创建时，它必须和快照在一个存储节点上，这种情况下，远程映射可能是必须的，需要注意性能问题。不能假设LVM后端是本地磁盘。</p>
<div class="blog_h3"><span class="graybg">nfs</span></div>
<p>添加到已启用后端列表：</p>
<pre class="crayon-plain-tag">[DEFAULT]
enabled_backends = lvm,nfs

[nfs]
volume_driver = cinder.volume.drivers.nfs.NfsDriver
nfs_shares_config = /etc/cinder/nfs_shares
nfs_mount_point_base = /var/lib/cinder/mnt
volume_backend_name = nfs</pre>
<p>配置NFS服务地址，以及使用的NFS导出目录的绝对路径：</p>
<pre class="crayon-plain-tag">10.0.0.1:/cinder</pre>
<p>并修改该配置文件的访问权限：</p>
<pre class="crayon-plain-tag">chown root:cinder /etc/cinder/nfsshares
chmod 0640 /etc/cinder/nfsshares</pre>
<p>重新启动cinder-volume服务：</p>
<pre class="crayon-plain-tag">systemctl restart openstack-cinder-volume.service</pre>
<p>查看cinder服务列表：</p>
<pre class="crayon-plain-tag">cinder service-list

| cinder-volume    | openstack-2@nfs | nova | enabled | up    | 2021-04-15T13:59:30.000000</pre>
<p>创建对应的volume type：</p>
<pre class="crayon-plain-tag">cinder type-create nfs
cinder type-key nfs set volume_backend_name=nfs

openstack volume type show nfs
+--------------------+--------------------------------------+
| Field              | Value                                |
+--------------------+--------------------------------------+
| access_project_ids | None                                 |
| description        | None                                 |
| id                 | c31ec381-c1f7-46c3-9119-bf699413f6b1 |
| is_public          | True                                 |
| name               | nfs                                  |
| properties         | volume_backend_name='nfs'            |
| qos_specs_id       | None                                 |
+--------------------+--------------------------------------+</pre>
<div class="blog_h3"><span class="graybg">ceph</span></div>
<p>参考<a href="#ceph">集成Ceph</a>。</p>
<div class="blog_h1"><span class="graybg">Glance</span></div>
<div class="blog_h2"><span class="graybg">多后端</span></div>
<div class="blog_h3"><span class="graybg">启用RBD后端</span></div>
<p>参考如下配置：</p>
<pre class="crayon-plain-tag">[DEFAULT]
# 增加             后端key:后端类型
enabled_backends = rbd:rbd, file:file

[glance_store]
# 增加
default_backend=file
# 注释掉
# stores = file,http,rbd
# default_store = file
filesystem_store_datadir = /var/lib/glance/images/

# 增加
[rbd]
rbd_store_pool = images
rbd_store_user = glance
rbd_store_ceph_conf = /etc/ceph/ceph.conf
rbd_store_chunk_size = 8</pre>
<p>重启服务：</p>
<pre class="crayon-plain-tag">systemctl restart  openstack-glance-api.service</pre>
<p>查看后端列表：</p>
<pre class="crayon-plain-tag">glance stores-info

+----------+----------------------------------------------------+
| Property | Value                                              |
+----------+----------------------------------------------------+
| stores   | [{"id": "rbd"}, {"id": "file", "default": "true"}] |
+----------+----------------------------------------------------+</pre>
<div class="blog_h3"><span class="graybg">向特定后端上传镜像</span></div>
<p>首先创建一个镜像记录：</p>
<pre class="crayon-plain-tag">glance image-create --disk-format raw --container-format bare --visibility public --name bionic-amd64 </pre>
<p>然后向此镜像记录上传镜像：</p>
<pre class="crayon-plain-tag">#                                                           指定后端     镜像记录的ID
glance image-upload --file bionic-server-cloudimg-amd64.img --store rbd d828f193-830f-4e1f-ad28-4fc607474368</pre>
<p>通过Ceph命令可以看到，存储池中出现一个以镜像ID为名字的RBD镜像：</p>
<pre class="crayon-plain-tag">rbd -p images ls
d828f193-830f-4e1f-ad28-4fc607474368</pre>
<div class="blog_h1"><span class="graybg"><a id="ceph"></a>集成Ceph</span></div>
<p>通过libvirt（底层是配置QEMU使用librbd），你可以将Ceph的RBD镜像挂载（Attach）给Openstack实例。</p>
<p>OpenStack有三部分可以用到Ceph存储：</p>
<ol>
<li>镜像：Glance管理的虚拟机镜像（不可变的）可以存放在Ceph中。Openstack将其作为blob并下载使用</li>
<li>卷：虚拟机启动使用的卷，或者后续挂载的额外卷</li>
<li>客户机磁盘：虚拟机启动时使用的系统盘<span style="background-color: #c0c0c0;">，默认情况下，此系统盘表现为Hypervisor的文件系统中的一个文件</span>，通常位于/var/lib/nova/instances/&lt;uuid&gt;/。在Havana版本之前，唯一启动系统盘位于Ceph中的VM方法是，使用boot-from-volume特性。现在，则可以直接启动存储在Ceph中的虚拟机，而<span style="background-color: #c0c0c0;">不需要使用Cinder</span>。这样，在虚拟机热迁移过程中，可以方便的执行维护操作。另外一个好处是，如果Hypervisor宕机，你可以通过<pre class="crayon-plain-tag">nova evacuate</pre>几乎无缝的重新实例化VM，OpenStack会在系统盘对应的RBD镜像上加独占锁以防多个计算节点并发访问之</li>
</ol>
<p>Glance 能够将镜像存储为一个 Ceph 块设备。通过 Cinder使用COW克隆镜像来启动虚拟机。<span style="background-color: #c0c0c0;">Ceph不支持 QCOW2 格式的镜像，必须使用RAW格式的镜像</span>。</p>
<div class="blog_h2"><span class="graybg">准备存储池</span></div>
<pre class="crayon-plain-tag">ceph osd pool create volumes 32; rbd pool init volumes
ceph osd pool create backups 32; rbd pool init backups
ceph osd pool create images 32;  rbd pool init images
ceph osd pool create vms 32;     rbd pool init vms

for i in volumes backups images vms; do ceph osd pool application enable $i rbd; done</pre>
<div class="blog_h2"><span class="graybg">安装软件</span></div>
<p>需要在nova-compute, cinder-backup 和 cinder-volume节点上安装Ceph客户端软件：</p>
<pre class="crayon-plain-tag">dnf install -y ceph-common python-rbd</pre>
<p>glance-api节点只需要安装 python-rbd。</p>
<div class="blog_h2"><span class="graybg">配置Ceph客户端</span></div>
<p>运行glance-api, cinder-volume, nova-compute, cinder-backup的节点，是Ceph客户端，因此首先把/etc/ceph/ceph.conf复制到这些节点。</p>
<p>然后，创建以下Ceph账户：</p>
<pre class="crayon-plain-tag">ceph auth get-or-create client.glance mon 'profile rbd' osd 'profile rbd pool=images' mgr 'profile rbd pool=images'
ceph auth get-or-create client.cinder mon 'profile rbd' osd 'profile rbd pool=volumes, profile rbd pool=vms, profile rbd-read-only pool=images' mgr 'profile rbd pool=volumes, profile rbd pool=vms'
ceph auth get-or-create client.cinder-backup mon 'profile rbd' osd 'profile rbd pool=backups' mgr 'profile rbd pool=backups'</pre>
<p>并且把Keyring分发到相应节点：</p>
<pre class="crayon-plain-tag"># 镜像服务使用client-glance
ceph auth get-or-create client.glance | ssh root@openstack-1 tee /etc/ceph/ceph.client.glance.keyring
ssh root@openstack-1 chown glance:glance /etc/ceph/ceph.client.glance.keyring

# 卷服务、计算节点使用client-cinder
ceph auth get-or-create client.cinder | ssh root@openstack-2 tee /etc/ceph/ceph.client.cinder.keyring
ssh root@openstack-2 chown cinder:cinder /etc/ceph/ceph.client.cinder.keyring
ceph auth get-key client.cinder | ssh root@openstack-2 tee /tmp/client.cinder.key
ceph auth get-or-create client.cinder | ssh root@openstack-3 tee /etc/ceph/ceph.client.cinder.keyring
ssh root@openstack-3 chown cinder:cinder /etc/ceph/ceph.client.cinder.keyring
ceph auth get-key client.cinder | ssh root@openstack-3 tee /tmp/client.cinder.key
ceph auth get-or-create client.cinder | ssh root@openstack-4 tee /etc/ceph/ceph.client.cinder.keyring
ssh root@openstack-4 chown cinder:cinder /etc/ceph/ceph.client.cinder.keyring
ceph auth get-key client.cinder | ssh root@openstack-4 tee /tmp/client.cinder.key

# 备份服务使用client.cinder-backup
ceph auth get-or-create client.cinder-backup | ssh root@openstack-1 tee /etc/ceph/ceph.client.cinder-backup.keyring
ssh root@openstack-1 chown cinder:cinder /etc/ceph/ceph.client.cinder-backup.keyring</pre>
<p>准备一个UUID，推荐所有节点使用同一UUID：</p>
<pre class="crayon-plain-tag">uuidgen
b3a20e8b-a27a-4623-8ea1-39eb7e34da4c</pre>
<p>在每个节点上执行：</p>
<pre class="crayon-plain-tag">cat &gt; secret.xml &lt;&lt;EOF
&lt;secret ephemeral='no' private='no'&gt;
  &lt;uuid&gt;b3a20e8b-a27a-4623-8ea1-39eb7e34da4c&lt;/uuid&gt;
  &lt;usage type='ceph'&gt;
    &lt;name&gt;client.cinder secret&lt;/name&gt;
  &lt;/usage&gt;
&lt;/secret&gt;
EOF

virsh secret-define --file secret.xml

virsh secret-set-value --secret b3a20e8b-a27a-4623-8ea1-39eb7e34da4c --base64 $(cat /tmp/client.cinder.key)
rm -f secret.xml /tmp/client.cinder.key</pre>
<div class="blog_h2"><span class="graybg">作为镜像存储</span></div>
<p>编辑配置文件：</p>
<pre class="crayon-plain-tag">[glance_store]
stores = file,http,rbd
default_store = file
filesystem_store_datadir = /var/lib/glance/images/
rbd_store_pool = images
rbd_store_user = glance
rbd_store_ceph_conf = /etc/ceph/ceph.conf
rbd_store_chunk_size = 8</pre>
<p>注意，glance支持多个存储后端，上面的配置增加了一个rbd后端。</p>
<p>要启用copy-on-write的镜像克隆，增加配置：</p>
<pre class="crayon-plain-tag">[DEFAULT]
show_image_direct_url = True</pre>
<p>为了防止glance在/var/lib/glance/image-cache/下缓存镜像，配置：</p>
<pre class="crayon-plain-tag">[paste_deploy]
flavor = keystone</pre>
<div class="blog_h2"><span class="graybg">作为备份存储</span></div>
<p>修改备份节点的Cinder配置文件：</p>
<pre class="crayon-plain-tag">backup_driver = cinder.backup.drivers.ceph
backup_ceph_conf = /etc/ceph/ceph.conf
backup_ceph_user = cinder-backup
backup_ceph_chunk_size = 134217728
backup_ceph_pool = backups
backup_ceph_stripe_unit = 0
backup_ceph_stripe_count = 0
restore_discard_excess_bytes = true</pre>
<div class="blog_h2"><span class="graybg">作为卷存储</span></div>
<div class="blog_h3"><span class="graybg">配置Cinder后端</span></div>
<p>配置cinder.conf：</p>
<pre class="crayon-plain-tag">[DEFAULT]
# 增加一个ceph的后端
enabled_backends = lvm,nfs-fast,nfs-slow,ceph
# 要支持多个Cinder后端，需要配置：
glance_api_version = 2

# 后端配置
[ceph]
volume_driver = cinder.volume.drivers.rbd.RBDDriver
volume_backend_name = ceph
rbd_pool = volumes
rbd_ceph_conf = /etc/ceph/ceph.conf
rbd_flatten_volume_from_snapshot = false
rbd_max_clone_depth = 5
rbd_store_chunk_size = 4
rados_connect_timeout = -1
# 如果启用了cephx身份验证
rbd_user = cinder
rbd_secret_uuid = b3a20e8b-a27a-4623-8ea1-39eb7e34da4c</pre>
<p>创建卷类型并关联到新建的后端：</p>
<pre class="crayon-plain-tag">cinder type-create ceph
cinder type-key ceph set volume_backend_name=ceph</pre>
<p>到这一步，可以创建Ceph卷了，但是还不能挂载给虚拟机。</p>
<div class="blog_h3"><span class="graybg">配置Nova</span></div>
<p>libvirt进程需要利用client.cinder的keyring来挂载Ceph卷（不管是作为普通块设备还是启动盘）：</p>
<pre class="crayon-plain-tag">[libvirt]
rbd_user = cinder
rbd_secret_uuid = b3a20e8b-a27a-4623-8ea1-39eb7e34da4c</pre>
<p>为了支持直接从Ceph卷启动虚拟机，需要为Nova配置ephemeral后端。</p>
<p>建议在ceph.conf中启用RBD缓存（从Giant版本默认开启）。此外，可以开启client admin socket，以收集指标和辅助故障诊断：</p>
<pre class="crayon-plain-tag">[client]
    rbd cache = true
    rbd cache writethrough until flush = true
    admin socket = /var/run/ceph/guests/$cluster-$type.$id.$pid.$cctid.asok
    log file = /var/log/qemu/qemu-guest-$pid.log
    rbd concurrent management ops = 20</pre><br />
<pre class="crayon-plain-tag">mkdir -p /var/run/ceph/guests/ /var/log/qemu/
chown qemu:libvirt /var/run/ceph/guests /var/log/qemu/</pre>
<div class="blog_h2"><span class="graybg">从Ceph卷启动</span></div>
<p>使用下面的命令可以将镜像转换为raw格式：</p>
<pre class="crayon-plain-tag">qemu-img convert -f {source-format} -O {output-format} {source-filename} {output-filename}
qemu-img convert -f qcow2 -O raw precise-cloudimg.img precise-cloudimg.raw</pre>
<p>从raw格式的、存放在Ceph后端的镜像创建卷：</p>
<pre class="crayon-plain-tag">cinder create --image-id {id of image} --display-name {name of volume} {size of volume}</pre>
<p>当Glance和Cinder同时使用Ceph后端时，镜像使用copy-on-write方式克隆，因此新卷的很快。</p>
<p>如果通过OpenStack Dashboard操作，参考步骤：</p>
<ol>
<li>新建一个实例</li>
<li>选择一个关联到copy-on-write克隆的镜像的卷。也就是存放在Ceph中的，创建自位于Ceph中的镜像的卷</li>
<li>选择从卷启动，并使用上一步的卷</li>
</ol>
<div class="blog_h1"><span class="graybg">配额和限速</span></div>
<div class="blog_h2"><span class="graybg">配额</span></div>
<p>所谓配额，是指限制某个项目（租户）或者类（class）能够使用的各种云资源的量。例如CPU核心个数、固定IP个数、实例个数、密钥对个数、内存量、存储量、备份存储量、备份个数、每个卷最大存储量、网络数、子网数，等等。</p>
<p>默认的admin项目的配额比较小，可以用<pre class="crayon-plain-tag">openstack quota set</pre>修改配额。要查看配额，可以：</p>
<pre class="crayon-plain-tag"># 显示项目的各种配额
openstack limits show --absolute --project admin

# 显示计算配额
openstack quota list --compute --project admin</pre>
<div class="blog_h2"><span class="graybg">Flavor限速</span></div>
<p>每个Flavor都可以被分配额外的属性（Extra Specs），来限制它的CPU、IO、网络的速度。</p>
<p>CPU限速基于cgroups实现。</p>
<p>IO限流由QEMU处理（通过libvirt的blkdeviotune），尽管libvirt提供了基于cgroups的blkiotune特性，但是Nova并没有使用它。</p>
<p>流量塑形（Traffic shaping）也就是出入站带宽限制，基于tc实现。<span style="background-color: #c0c0c0;">Libvirt以网络接口为级别进行流量塑形，如果它基于cgroups进行塑形，则客户机的所有网络接口被一起限制，因为cgroups运行在进程级别</span>。</p>
<div class="blog_h3"><span class="graybg">IO限速</span></div>
<p>可以使用以下Extra Specs键：</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>disk_write_bytes_sec</td>
<td>限制字节数/秒，写</td>
</tr>
<tr>
<td>disk_read_bytes_sec</td>
<td>限制字节数/秒，读</td>
</tr>
<tr>
<td>disk_read_iops_sec</td>
<td>限制IOPS，读</td>
</tr>
<tr>
<td>disk_write_iops_sec</td>
<td>限制IOPS，写</td>
</tr>
<tr>
<td>disk_total_bytes_sec</td>
<td>限制字节数/秒，读写</td>
</tr>
<tr>
<td>disk_total_iops_sec</td>
<td>限制IOPS，读写</td>
</tr>
</tbody>
</table>
<p>配置示例：</p>
<pre class="crayon-plain-tag"># 通过nova-manage命令
nova-manage flavor set_key --name m1.small  --key quota:disk_read_bytes_sec --value 10240000

# 通过nova命令，需要admin凭证
nova flavor-key m1.small  set quota:disk_read_bytes_sec=10240000

# 通过openstack命令，property可以指定多个
openstack flavor set m1.small --property quota:disk_read_bytes_sec=10000</pre>
<div class="blog_h3"><span class="graybg">CPU限速</span></div>
<p>可以使用以下Extra Specs键：</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>cpu_shares</td>
<td>占用CPU时间的权重，不是绝对值。一个设置了2048的VM，会比1024的多用一倍的CPU时间</td>
</tr>
<tr>
<td>cpu_period</td>
<td rowspan="2">单位微秒，限制在指定周期（period）内占用CPU带宽（时间）最大值（quota）</td>
</tr>
<tr>
<td>cpu_quota</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">网络限速</span></div>
<p>可以使用以下Extra Specs键：</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>vif_inbound_average</td>
<td rowspan="6">
<p>inbound表示入站流量限制，outbound表示出站流量限制</p>
<p>average: 期望的平均带宽KB/s<br />peak：最大的峰值带宽KB/s<br />burst：以峰值带宽最多连续发送多少KB</p>
</td>
</tr>
<tr>
<td>vif_outbound_average</td>
</tr>
<tr>
<td>vif_inbound_peak</td>
</tr>
<tr>
<td>vif_outbound_peak</td>
</tr>
<tr>
<td>vif_inbound_burst</td>
</tr>
<tr>
<td>vif_outbound_burst</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg"><a id="neutron-qos"></a>Neutron QoS</span></div>
<p>所谓<span style="background-color: #c0c0c0;">QoS</span>，是指能够确保一定的网络需求的能力，这些网络需求包括<span style="background-color: #c0c0c0;">带宽、延迟、抖动、可靠性</span>。</p>
<p>交换机、路由器之类的网络设备，能够给流量打标记，并以更高的优先级来处理这些流量，以满足应用提供者和终端用户之间的SLA（Service Level Agreement ）。</p>
<p>在Neutron中，qos是一个高级的插件，它将QoS从OpenStaack网络代码的其他部分解耦出来，它从多个网络级别进行QoS控制，可以通过ml2扩展驱动的方式来使用。目前qos仅仅支持ml2（的SR-IOV, Open vSwitch, linuxbridge驱动）</p>
<div class="blog_h3"><span class="graybg">配置</span></div>
<p>在控制节点，配置Neutron：</p>
<pre class="crayon-plain-tag">service_plugins = router,metering,qos</pre><br />
<pre class="crayon-plain-tag">[ml2]
extension_drivers = port_security,qos</pre>
<p>修改你所使用的网络代理的配置，配置文件位于 /etc/neutron/plugins/ml2/&lt;agent_name&gt;_agent.ini。例如：</p>
<pre class="crayon-plain-tag">[agent]
extensions = qos</pre>
<p>在计算/网络节点，修改网络代理配置：</p>
<pre class="crayon-plain-tag">[agent]
extensions = qos</pre>
<p>如果希望给浮动IP配置QoS，需要配置L3代理：</p>
<pre class="crayon-plain-tag">[agent]
;                    如果需要支持路由器网关地址的QoS
extensions = fip_qos,gateway_ip_qos

; 使用Open vSwitch时，由于rate limit对于OVS内部端口不工作，作为变通方法，添加配置
ovs_use_veth = True</pre>
<p>Differentiated Services（DS，DiffServ）是一种在IP网络中分类、管理网络流量并提供QoS的机制。DS在保障关键流量（例如VoIP、视频流）低延迟的同时，为非关键服务（例如Web流量）提供best-effort服务。</p>
<p>DS在8bit的IP头字段differentiated services（DS）字段（用于替换过时的IPv4 TOS字段）中，写入6bit的DSCP（Differentiated Services Code Point）。<a href="https://tools.ietf.org/html/rfc4594">RFC 4594</a>对各个DSCP值由规定，例如广播食品CS3的DSCP为24。</p>
<p>当使用VxLAN之类的Overlay网络时，DHCP标记仅仅应用在内层IP头上。在封装期间，DSCP标记不会自动拷贝到外层包头。为了自动在外层包头设置DSCP，需要配置网络代理：</p>
<pre class="crayon-plain-tag">[agent]
dscp = 8
dscp_inherit = true</pre>
<div class="blog_h3"><span class="graybg">实现</span></div>
<p>OpenStack会将QoS规则映射为底层代理的配置。如果使用Linux Bridge作为网络代理：</p>
<pre class="crayon-plain-tag"># 入站
tc qdisc show dev tap2e939f9e-9e
# qdisc tbf 8002: root refcnt 2 rate 512Kbit burst 16Kb lat 50.0ms

# 出站
tc filter show dev tap2e939f9e-9e parent ffff:
# filter protocol all pref 49 basic chain 0
# filter protocol all pref 49 basic chain 0 handle 0x1
#   police 0x1 rate 1024Kbit burst 32Kb mtu 64Kb action drop overhead 0b
#     ref 1 bind 1

# DSCP
iptables -t mangle -nL neutron-linuxbri-qos-o2e939f
Chain neutron-linuxbri-qos-o2e939f (1 references)
target prot opt source destination
DSCP all -- 0.0.0.0/0 0.0.0.0/0 DSCP set 0x10</pre>
<div class="blog_h3"><span class="graybg">使用</span></div>
<p>使用默认的policy.json时，仅仅管理员能够创建QoS策略。 下面是个例子：</p>
<pre class="crayon-plain-tag"># 创建一个规则
openstack network qos policy create bw-limiter
# 设置出站带宽限制
openstack network qos rule create --type bandwidth-limit --max-kbps 3000
  --max-burst-kbits 2400 --egress bw-limiter


# 创建一个DSCP标记规则
openstack network qos policy create dscp-marking
# 设置DSCP值
openstack network qos rule create --type dscp-marking --dscp-mark 26 \
    dscp-marking</pre>
<p>注意：对于OVS和Linux Bridge来说，QoS实现需要burst值才能确保正确的带宽限制行为。设置合理的burst值非常重要，如果仅设置bandwidth-limit，即使它的值是合理的，带宽也会被throttled。对于TCP流量来说，<span style="background-color: #c0c0c0;">推荐burst为bandwidth-limit的80%</span>，如果burst设置的过低，则实际获得的带宽可能小于预期。</p>
<p>为某个网络端口配置/解除QoS策略：</p>
<pre class="crayon-plain-tag"># 设置
openstack port set --qos-policy bw-limiter 88101e57-76fa-4d12-b0e0-4fc7634b874a
# 解除
openstack port unset --qos-policy 88101e57-76fa-4d12-b0e0-4fc7634b874a

# 创建端口时可以直接指定QoS策略
openstack port create --qos-policy bw-limiter --network private port1</pre>
<p>你也可以将浮动IP、网络关联到QoS策略：</p>
<pre class="crayon-plain-tag"># 关联浮动IP到QoS策略
openstack floating ip create --qos-policy bw-limiter public
openstack floating ip set --qos-policy bw-limiter d0ed7491-3eb7-4c4f-a0f0-df04f10a067c
# 解除关联
openstack floating ip set --no-qos-policy d0ed7491-3eb7-4c4f-a0f0-df04f10a067c
openstack floating ip unset --qos-policy d0ed7491-3eb7-4c4f-a0f0-df04f10a067c
# 关联整个网络到QoS策略
openstack network set --qos-policy bw-limiter private</pre>
<p>关联到浮动IP的QoS策略，将在浮动IP挂载到某个端口的时候生效。</p>
<p>每个项目可以配置一个默认QoS策略：</p>
<pre class="crayon-plain-tag">openstack network qos policy create --default bw-limiter</pre>
<p>你可以在<span style="background-color: #c0c0c0;">运行时动态修改QoS策略的规则，这些修改会传播到每个Attach到策略的端口</span>：</p>
<pre class="crayon-plain-tag"># 修改规则
openstack network qos rule set --max-kbps 2000 --max-burst-kbits 1600 \
  --ingress bw-limiter 92ceb52f-170f-49d0-9528-976e2fee2d6f</pre>
<div class="blog_h1"><span class="graybg">诊断和调试</span></div>
<div class="blog_h2"><span class="graybg">日志级别</span></div>
<p>要调整Nova日志级别，修改配置：</p>
<pre class="crayon-plain-tag">log_config_append=/etc/nova/logging.conf</pre>
<p>在/etc/nova/logging.conf中配置日志级别： </p>
<pre class="crayon-plain-tag">[logger_nova]
level = INFO
handlers = stderr
qualname = nova</pre>
<div class="blog_h1"><span class="graybg">新特性</span></div>
<div class="blog_h2"><span class="graybg">12.Queens</span></div>
<div class="blog_h3"><span class="graybg">Cinder Multi-Attach</span></div>
<p>此功能使用户能够将单个块存储卷挂载到多个服务器，以及从多个服务器访问单个块存储卷。此功能的用例包括 active-active 和 hot-standby 场景——有多台服务器需要访问卷上的数据，以在出现故障时快速恢复或能够处理系统中增加的负载。在 Queens 发行版中，有三个驱动程序支持 multi-attach ：LVM、NetApp / SolidFire 和 Oracle ZFSSA</p>
<div class="blog_h3"><span class="graybg">vGPU支持</span></div>
<p>在 Nova 中，对 vGPU 的支持让云管理员能够定义 flavor 以请求 vGPU 的特定资源和分辨率。最终用户可以启动具有 vGPU 的虚拟机，这对于图像密集型工作负载以及人工智能和机器学习工作负载来说是一项重要的功能</p>
<div class="blog_h3"><span class="graybg">Cyborg</span></div>
<p>Cyborg是用于管理硬件和软件加速资源（如 GPU，FPGA，CryptoCards 和 DPDK/SPDK）的架构，对NFV工作负载的电信公司而言，加速是一项必备的功能。通过Cyborg，运维人员可以列出、识别和发现加速器，将加速器连接到实例并将其分离、安装和卸载驱动器。它可以单独使用，或与 Nova 或 Ironic 结合使用</p>
<div class="blog_h3"><span class="graybg"> Ironic 救援模式</span></div>
<p>之前在 Nova 中可以实现虚拟机实例修复，现在 Ironic 中可以实现裸机实例修复。运维人员可以对错误配置的裸机节点进行故障排除，或从诸如丢失的SSH密钥等问题中恢复</p>
<div class="blog_h3"><span class="graybg">Kuryr CNI 守护进程</span></div>
<p>OpenStack是在私有云中部署容器的首选平台，Queens版本扩展了微服务功能。Kuryr 增加了一个 CNI 守护进程来增加 Kubernetes 操作的可扩展性。为了支持高可用（HA），CNI 守护进程能够监控 Pod 事件，不需要为每个事件等待 Kubernetes API。即便控制器宕机了，也可以创建 Pod。</p>
<div class="blog_h3"><span class="graybg">Zun容器服务</span></div>
<p>Zun 是一个新 的OpenStack 项目，它允许用户无需管理服务器或集群即可快速启动和运行容器。它通过与 Neutron，Cinder，Keystone 和其他核心 OpenStack 服务集成，无缝地将先进的企业网络、存储和身份验证功能添加到容器中</p>
<div class="blog_h3"><span class="graybg"> OpenStack-Helm</span></div>
<p>提供了一系列 Helm 图表和工具，用于在 Kubernetes之 上管理 OpenStack的生命周期，并将 OpenStack 作为独立服务运行</p>
<div class="blog_h3"><span class="graybg">LOCI</span></div>
<p>OCI 生成兼容 Open Container Initiative 的 OpenStack 服务镜像，可以放入像 OpenStack-Helm 这样的重量级部署工具，或者单独使用来交付像 Cinder 块存储这样的独立服务。 LOCI 提供了现有 OpenStack Kolla 项目的一种替代方案（为每个容器镜像提供一个更完整的打包方法）。LOCI 采取的方法更符合 Kubernetes 运行镜像的方式，其中容器本身非常小，管理位于容器外部</p>
<div class="blog_h2"><span class="graybg">14.Stein</span></div>
<div class="blog_h3"><span class="graybg">容器功能的强化</span></div>
<p>提供运行容器所需的裸机和网络功能：</p>
<ol>
<li>OpenStack Magnum，经过认证的Kubernetes安装程序，显著提升了Kubernetes集群的启动时间—无论节点数量多少，每个节点从10-12分钟降至5分钟</li>
<li>通过OpenStack云供应商，您现在可以在Manila、Cinder和Keystone服务的支持下启动完全集成的Kubernetes集群，从而充分利用其底层的OpenStack云平台</li>
<li>Neutron，OpenStack网络服务，针对在组中创建端口的容器用例，更快速的创建批量端口</li>
<li>Ironic，裸机配置服务，持续改进部署模板，以便于独立用户请求分配裸机节点并提交配置数据，而不需要预先配置驱动器</li>
</ol>
<div class="blog_h3"><span class="graybg">网络功能强化</span></div>
<ol>
<li>
<p>Neutron，网段范围管理，云管理员可通过新的扩展API动态管理网段范围，而不是采用之前编辑配置文件的方法。StarlingX和边缘用例将得益于此，更易于管理</p>
</li>
<li>
<p>对于网络密集型应用程序，拥有最小可用网络带宽至关重要。在Rocky周期中开始工作，提供基于最小带宽需求的调度，该功能已在Stein中交付。作为强化功能的一部分，Neutron将带宽视为一种资源，并与OpenStack Nova计算服务协作，将实例调度到满足其带宽需求的主机上</p>
</li>
<li>
<p>对API的改进增加了OpenStack体系结构和部署的灵活性，增加了对服务质量（QoS）策略规则aliases的支持，使调用者能够更高效地执行删除、显示和更新QoS规则等请求</p>
</li>
</ol>
<div class="blog_h3"><span class="graybg">增强资源管理</span></div>
<ol>
<li>
<p>Blazar，资源预留服务，引进了新的资源分配API，运营商可查询其云计算资源的保留状态</p>
</li>
<li>
<p>Placement是引入Stein版本的一个新项目，是从Nova项目中分离出来的。可定位候选资源供应商，简化了为工作负载迁移指定主机的任务。对于常见的调度操作，API性能提升了50%。Train版本中将删除Nova中的Placement服务，其后安装Nova需要使用单独的Placement服务</p>
</li>
</ol>
<div class="blog_h2"><span class="graybg">15.Train</span></div>
<div class="blog_h3"><span class="graybg">增强安全性</span></div>
<ol>
<li>支持软件RAID：具有Ironic 裸机服务可保护服务免受磁盘故障的影响。欧洲核研究组织欧洲核子研究组织（CERN）领导了此功能的上游开发，并且已经将该功能在超过1000个节点上投入生产</li>
<li>基于硬件的加密：Nova 是OpenStack的计算功能， 其新框架支持对guest存储器进行基于硬件的加密，以保护用户免遭攻击者或流氓管理员在使用libvirt计算驱动程序时窥探其工作负载。此功能对于多租户环境和具有可公开访问的硬件的环境很有用</li>
<li>数据保护流程：Karbor 为检查，还原，计划和触发操作添加了事件通知。此功能允许用户使用位于根磁盘上的新添加的数据备份映像引导服务器</li>
</ol>
<div class="blog_h2"><span class="graybg">16.Ussuri</span></div>
<p>要求CentOS 8</p>
<div class="blog_h3"><span class="graybg">增强AI支持</span></div>
<p>完成了Nova Cyborg Interaction功能，使两者在某些方面进行紧密的联系，用于启动和管理具有GPU等加速器的实例</p>
<div class="blog_h3"><span class="graybg">增强安全性</span></div>
<ol>
<li>
<p>Nova API策略引入了具有scope_type功能的新默认角色</p>
</li>
<li>
<p>Ironic及其远程代理之间的交互身份验证得到了补充</p>
</li>
<li>
<p>Kolla 添加了对后端API服务的TLS加密的初始支持</p>
</li>
</ol>
<div class="blog_h3"><span class="graybg">优化用户体验</span></div>
<ol>
<li>
<p>Glance 优化了对Multiple Strores的操作，单次操作，后台同步</p>
</li>
<li>
<p>Keystone对创建应用程序凭据和信任关系的用户体验进行了极大改善</p>
</li>
</ol>
<div class="blog_h3"><span class="graybg">Cinder增强</span></div>
<ol>
<li>
<p>为volume-type设置最大和最小的Size</p>
</li>
<li>
<p>使用时间比较运算符过滤卷列表的能力</p>
</li>
<li>
<p>将卷上载到Image Service时，支持Glance multistore和镜像数据托管</p>
</li>
<li>
<p>添加了一些新的后端驱动程序，并且许多当前的驱动程序都增加了对更多功能的支持</p>
</li>
</ol>
<div class="blog_h3"><span class="graybg">Cyborg增强</span></div>
<ol>
<li>
<p>随着Nova-Cyborg集成的完成，用户现在可以使用由Cyborg管理的加速器启动实例</p>
</li>
<li>
<p>实现了新的API，用以列出由Cyborg管理的设备，可以查看和管理加速器的列表</p>
</li>
<li>
<p>Cyborg通过在v2 API中采用microversions的模式，旨在将来版本中提供向后兼容的办法</p>
</li>
<li>
<p>Cyborg客户端现在基于OpenStack SDK，并支持大多数Version 2 API</p>
</li>
<li>
<p>通过增加更多的单元/功能测试并减少技术负担来提高总体质量</p>
</li>
</ol>
<div class="blog_h3"><span class="graybg">Glance增强</span></div>
<ol>
<li>
<p>增强了Multiple Stores功能，用户现在可以向多个Stores导入单个镜像，在多个Stores中复制现有的imgae，并从单个Store中删除镜像</p>
</li>
<li>
<p>新导入了插件以解压镜像</p>
</li>
<li>
<p>再次为glance-store引入了S3 driver</p>
</li>
</ol>
<div class="blog_h3"><span class="graybg">Horizon增强</span></div>
<ol>
<li>
<p>支持范围内省的规则，该规则允许每个节点子集具有（并保留）规则，例如不同的硬件交付</p>
</li>
<li>
<p>支持硬件退役工作流程，以实现托管云中硬件退役的自动化</p>
</li>
<li>
<p>非管理员使用Ironic可以使用多租户概念和其他策略选项</p>
</li>
<li>
<p>Ironic及其远程代理之间的交互身份验证得到了补充，从而可以在不受信任的网络上进行部署</p>
</li>
<li>
<p>UEFI和设备选择现在可用于软件RAID</p>
</li>
</ol>
<div class="blog_h3"><span class="graybg">Keystone增强</span></div>
<ol>
<li>
<p>使用联合身份验证方法时，用于创建应用程序凭据和信任关系的用户体验已得到极大改善。角色分配来自映射的组成员身份的联盟用户将在令牌过期后将这些组成员身份保留为可配置的TTL，在此期间其应用程序凭证将保持有效</p>
</li>
<li>
<p>现在，可以通过在Keystone中直接创建联盟用户并将其链接到其身份提供者，而无需依赖于映射API，就可以为联盟用户指定具体的角色分配</p>
</li>
<li>
<p>当引导新的Keystone部署时，管理员角色现在默认设置为“ immutable”选项，这可以防止意外删除或修改它，除非有意删除了“ immutable”选项</p>
</li>
<li>
<p>Keystonemiddleware不再支持Identity v2.0 API，该身份在先前的发行周期中已从keystone中删除</p>
</li>
<li>
<p>恢复资源驱动程序的可配置性，因此，如果内置sql驱动程序不满足业务要求，现在可以创建自定义资源驱动程序</p>
</li>
</ol>
<div class="blog_h3"><span class="graybg">Kolla/容器化部署</span></div>
<ol>
<li>
<p>所有镜像，脚本和Ansible剧本都使用Python 3，并且对Python 2的支持也已删除</p>
</li>
<li>
<p>添加了对CentOS 8主机和镜像的支持</p>
</li>
<li>
<p>添加了对后端API服务的TLS加密的初始支持，从而提供了API流量的端到端加密。目前支持Keystone</p>
</li>
<li>
<p>增加了对开放虚拟网络（OVN）部署以及与Neutron集成的支持</p>
</li>
<li>
<p>增加了对部署Zun CNI（容器网络接口）组件的支持，从而使带有容器的Docker可以支持Zun capsules(pods)</p>
</li>
<li>
<p>添加了对Elasticsearch Curator的支持，以帮助管理集群日志数据</p>
</li>
<li>
<p>添加了将Mellanox网络设备与Neutron一起使用所必需的组件</p>
</li>
<li>
<p>简化了外部Ceph集成的配置，可以轻松地从Ceph-Ansible部署的Ceph集群过渡到在OpenStack中启用它</p>
</li>
</ol>
<div class="blog_h3"><span class="graybg">Kuryr连接到容器网络</span></div>
<ol>
<li>
<p>支持IPv6</p>
</li>
<li>
<p>DPDK支持嵌套设置以及其他各种与DPDK和SR-IOV相关功能的改进</p>
</li>
<li>
<p>与NetworkPolicy支持相关的多个修复程序</p>
</li>
</ol>
<div class="blog_h3"><span class="graybg">Manila共享文件系统</span></div>
<ol>
<li>
<p>共享组已从试验性功能逐渐发展成熟。从API版本2.55开始，不再需要X-OpenStack-Manila-API-Experimental标头来创建/更新/删除共享组类型，组规范，组配额和共享组本身</p>
</li>
<li>
<p>兼容时，可以从跨存储池的快照创建共享。这项新功能可以通过分散先前局限于托管快照的后端的工作负载来更好地利用后端资源</p>
</li>
<li>
<p>引入了新的配额控制机制，以限制项目及其用户可创建的共享副本的数量和大小</p>
</li>
<li>
<p>现在可以按时间间隔查询异步用户消息</p>
</li>
</ol>
<div class="blog_h3"><span class="graybg">Neutron增强</span></div>
<ol>
<li>
<p>OVN驱动程序现在已合并到Neutron存储库中，并且是核心 Neutron ML2 drivers之一，例如linuxbridge或openvswitch。与openvswitch驱动程序相比，OVN驱动程序的优点包括具有分布式SNAT流量的DVR，分布式DHCP以及无需网络节点即可运行的可能性。当然其他ML2驱动程序仍然受到完全支持。当前默认代理还是openvswitch，但计划是使用OVN驱动程序成为将来的默认选择</p>
</li>
<li>
<p>添加了对无状态安全组的支持。用户现在可以将安全组集创建为无状态，这意味着conntrack将不会用于该组中的任何规则。一个端口只能使用无状态或有状态安全组。在某些用例中，无状态安全组将允许操作员选择优化的数据路径性能，而有状态安全组会在系统上施加额外的处理</p>
</li>
<li>
<p>已添加用于地址范围和子网池的基于角色的访问控制（RBAC）。地址范围和子通常由运营商定义并向用户公开。此更改使操作员可以在地址范围和子网池上使用更精细的访问控制</p>
</li>
<li>
<p>Neutron API中添加了对创建过程中标记资源的支持。用户现在可以设置资源标签，例如直接在POST请求中移植端口。这将大大提高kubernetes网络操作的性能。API调用的数量，例如Kuryr已发送给Neutron的邮件大大减少</p>
</li>
</ol>
<div class="blog_h3"><span class="graybg">Nova增强</span></div>
<ol>
<li>
<p>Nova不再支持Python 2，Python 3.6和3.7则受支持</p>
</li>
<li>
<p>支持在Nova cells间进行冷迁移和重新调整虚拟机大小</p>
</li>
<li>
<p>支持precaching glance image到计算节点</p>
</li>
<li>
<p>支持在创建虚拟机时通过Cyborg来附加加速设备</p>
</li>
<li>
<p>进一步支持QOS最小的带宽功能(拓展了以下操作evacuate、live migrate、unshelve)</p>
</li>
<li>
<p>支持nova-manage placement auditCLI，以查找和清理孤立的资源分配</p>
</li>
<li>
<p>Nova API策略引入了具有scope_type功能的新默认角色。这些新更改提高了安全级别和可管理性。在处理具有“读取”和“写入”角色的系统和项目级别令牌的访问权限方面，新策略更加丰富</p>
</li>
<li>
<p>从卷启动的虚拟机能够使用Rescue操作，允许将稳定的磁盘设备连接到救援实例</p>
</li>
<li>
<p>计算节点支持多种虚拟GPU类型</p>
</li>
<li>
<p>移除os-consoles和os-networksREST APIs</p>
</li>
<li>
<p>移除nova-dhcpbridge、nova-console、nova-xvpvncproxy服务</p>
</li>
</ol>
<div class="blog_h3"><span class="graybg">Octavia增强</span></div>
<p>Octavia提供负载均衡服务</p>
<ol>
<li>
<p>Octavia现在支持在特定可用性区域中部署负载平衡器。这允许将负载平衡功能部署到边缘环境</p>
</li>
<li>
<p>Octavia amphora驱动程序已添加了一项技术预览功能，可以改善控制平面的弹性。如果控制平面主机在负载均衡器配置操作期间发生故障，备用控制器可以恢复进行中的配置并完成请求</p>
</li>
<li>
<p>用户现在可以指定侦听器和池可接受的TLS密码。这允许负载平衡器强制执行安全合规性要求</p>
</li>
</ol>
<div class="blog_h3"><span class="graybg">Placement增强</span></div>
<p>Placement 放置服务，通过使分配重试计数可配置，提高了常见的并发分配写入次数（例如繁忙的群集管理程序）情况下的鲁棒性</p>
<div class="blog_h3"><span class="graybg">Swift增强</span></div>
<ol>
<li>
<p>为Swift容器和对象添加了新的系统命名空间</p>
</li>
<li>
<p>使用新的名称空间添加了新的Swift对象版本API</p>
</li>
<li>
<p>添加了对使用新API的S3版本控制的支持</p>
</li>
<li>
<p>添加了使用SIGUSR1执行“无缝”重载的功能，其中WSGI服务器套接字从不停止接受连接</p>
</li>
</ol>
<div class="blog_h2"><span class="graybg">17.Victoria</span></div>
<div class="blog_h3"><span class="graybg">Cinder增强</span></div>
<ol>
<li>改进了对配置的默认卷类型的处理，并使用microversion 3.62添加了新的块存储API调用，可以为单个项目设置项目级别的默认卷类型</li>
<li>添加了一些新的后端驱动程序，同时当前的大部分驱动程序都添加了对更多功能的支持。例如，NFS驱动程序现在支持卷加密</li>
<li>使用流行的Zstandard压缩算法，增加了对cinder备份的支持</li>
</ol>
<div class="blog_h3"><span class="graybg">Cyborg增强</span></div>
<ol>
<li>自Ussuri发行以来，用户可以使用由Cyborg管理的加速器启动实例，该发行版还支持两项操作Rebuild and Evacuate</li>
<li>Cyborg支持新的加速器驱动程序（Intel QAT和Inspur FPGA），并达成协议，希望实施新驱动程序的供应商至少应提供完整的驱动程序报告结果</li>
<li>支持Program API，现在，用户可以在给定预加载的bitstream的情况下对FPGA进行编程</li>
<li>在此版本中，部分实施了针对cyborg的策略刷新（带有作用域的RBAC）（设备配置文件API），我们在基本策略和device_profile策略中实现了新的默认规则，并为所有策略添加了基本测试框架。对于向后兼容性，将旧规则保留为不推荐使用的规则，并使用与当前相同的默认值，以便现有部署将保持原样运行。实施所有功能后，我们将为用户提供两个周期的过渡期</li>
</ol>
<div class="blog_h3"><span class="graybg">Glance增强</span></div>
<ol>
<li>增强了多个商店功能，管理员现在可以设置策略以允许用户复制其他租户拥有的镜像</li>
<li>概览允许配置多cinder存储</li>
<li>一目了然的RBD和Filesystem驱动程序现在支持稀疏镜像上传</li>
<li>增强了RBD驱动程序块上传镜像</li>
</ol>
<div class="blog_h3"><span class="graybg">Ironic增强</span></div>
<ol>
<li>部署步骤工作将基本部署操作分解为多个步骤，现在还可以包括部署时支持的RAID和BIOS接口的步骤</li>
<li>一个agent电源接口支持在没有基板管理控制器的情况下进行资源调配操作</li>
<li>现在可以将Ironic配置为进行HTTP Basic身份验证，而无需其他服务</li>
<li>添加了对Redfish虚拟介质的基于DHCP的部署的初始支持</li>
</ol>
<div class="blog_h3"><span class="graybg">Kolla增强</span></div>
<ol>
<li>添加了对Ubuntu Focal 20.04的支持</li>
<li>添加了对后端API服务的TLS加密的附加支持，从而提供了API流量的端到端加密</li>
<li>增加了对核心OpenStack服务的容器健康检查的支持</li>
<li>添加了对etcd的TLS加密的支持</li>
<li>改善了Ansible Playbook的性能和可伸缩性</li>
<li>增加了对将Neutron与Mellanox InfiniBand集成的支持</li>
<li>为Kayobe添加了对在neutron上部署自定义容器的支持</li>
</ol>
<div class="blog_h3"><span class="graybg">Kuryr增强</span></div>
<ol>
<li>Kuryr将不再使用注释在k8s api中存储关于OpenStack对象的数据。而是创建了一个相应的CRD，即KuryrPort、KuryrLoadBalancer和KuryrNetworkPolicy</li>
<li>增加了在嵌套设置中自动检测虚拟机桥接接口的支持</li>
</ol>
<div class="blog_h3"><span class="graybg">Manila增强</span></div>
<ol>
<li>租户驱动的共享复制，数据保护，灾难恢复和高可用性的自助服务现已普遍可用并得到完全支持</li>
<li>共享服务器迁移现在作为一个实验性功能提供。共享服务器通过隔离网络路径中的共享文件系统来提供多租户保证。在这个版本中，云管理员能够将共享服务器移动到不同的后端或共享网络</li>
</ol>
<div class="blog_h3"><span class="graybg">Neutron增强</span></div>
<ol>
<li>现在可以通过IPv6使用元数据服务。用户现在可以在仅IPv6的网络中使用不带配置驱动器的元数据服务。</li>
<li>flat已为分布式虚拟路由器（DVR）添加了对网络的支持。</li>
<li>OVN后端增加了对浮动IP端口转发的支持。现在，在Neutron中使用OVN后端时，用户可以为浮动IP创建端口转发。</li>
<li>在OVN中增加了对路由器可用区域的支持。OVN驱动程序现在可以从路由器的Availability_zone_hints字段中读取，并使用给定的可用区域相应地调度路由器端口</li>
</ol>
<div class="blog_h3"><span class="graybg">Nova增强</span></div>
<ol>
<li>Nova支持在同一nova服务器中混合使用绑定和浮动CPU</li>
<li>Nova支持通过提供程序配置文件来自定义计算节点的放置资源清单</li>
<li>即使使用Glance多存储 配置，Nova也支持 从Ceph RBD群集快速克隆Glance镜像 </li>
<li>Nova支持使用虚拟TPM设备创建服务器</li>
</ol>
<div class="blog_h3"><span class="graybg">Octavia增强</span></div>
<ol>
<li>用户现在可以指定侦听器和池接受的TLS版本。用户现在还可以设置其部署可接受的最低TLS版本</li>
<li>Octavia现在使用新的侦听器应用程序层协议协商（ALPN）配置选项来支持TLS上的HTTP/2</li>
<li>现在可以将负载均衡器统计信息同时报告给多个统计信息驱动程序，并支持增量指标。这样可以更轻松地集成到外部度量系统中，例如时间序列数据库</li>
<li>用于amphora驱动程序的Octavia flavors现在支持将glance image标记指定为flavor的一部分。这允许用户定义Octavia flavor来引导备用的amphora镜像</li>
<li>负载平衡器池现在支持PROXY协议的版本2。使用TCP协议时，这允许将客户端信息传递到成员服务器。PROXYV2提高了使用PROXY协议与成员服务器建立新连接的性能，尤其是在侦听器使用IPv6的情况下</li>
</ol>
<div class="blog_h3"><span class="graybg">Swift增强</span></div>
<ol>
<li>改进了读取纠错码数据时的第一字节延迟</li>
<li>当使用单独的复制网络运行时，后台守护程序和代理服务器之间的隔离度增加</li>
<li>我们开始看到生产集群从python2下运行Swift过渡到python3 </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">The remote metadata server responded with Forbidden</span></div>
<p>Neutron元数据代理日志（/var/log/neutron/metadata-agent.log）报错：The remote metadata server responded with Forbidden. This response usually occurs when shared secrets do not match.</p>
<p>可能原因：元数据代理配置文件metadata_agent.ini中的metadata_proxy_shared_secret和nova.conf中配置的不一样。</p>
<div class="blog_h2"><span class="graybg">仪表盘相关</span></div>
<div class="blog_h3"><span class="graybg"> You have offline compression enabled but</span></div>
<p>报错示例：compressor.exceptions.OfflineGenerationError: You have offline compression enabled but key "xxx" is missing from offline manifest. You may need to run "python manage.py compress". Here is the original content</p>
<p>解决办法：</p>
<pre class="crayon-plain-tag">COMPRESS_OFFLINE = False</pre>
<p>然后重启httpd</p>
<div class="blog_h2"><span class="graybg">网络相关</span></div>
<div class="blog_h3"><span class="graybg">pyroute2.netlink.exceptions.NetlinkError: (13, 'Permission denied')</span></div>
<p>禁用IPv6可以消除此错误：</p>
<pre class="crayon-plain-tag">net.ipv6.conf.all.disable_ipv6 = 1</pre>
<p>linux-bridge从设计上禁用了IPv6，如果物理网卡上配置了IPv6地址，则neutron/root没有权限将IPv6地址从物理网卡移动到linux-bridge上。</p>
<p>尽管如此，linux-bridge仍然会将IPv6的L2帧转发，你仍然可以在客户机上使用IPv6。</p>
<div class="blog_h3"><span class="graybg">实例的真实IP和Nova中的不一致</span></div>
<p>使用provider网络时，底层网络中存在DHCP服务器时出现此情况。实例向底层网络中的DHCP请求了IP地址，而非DHCP Agent。</p>
<p>建议：使用底层网络，但是分配独立的网段。</p>
<div class="blog_h2"><span class="graybg">镜像相关</span></div>
<div class="blog_h3"><span class="graybg">Image virtual size is 128GB and doesn't fit in a volume of size 12GB</span></div>
<p>需要修改镜像虚拟尺寸，首先检查一下镜像信息：</p>
<pre class="crayon-plain-tag"># 进入镜像存储目录
/var/lib/glance/images/


# 得到镜像ID
openstack image list
# | 379caead-7878-4d82-847d-502feea5b8ed | centos8-amd64-prepared | active |


# 查看镜像信息
qemu-img info 379caead-7878-4d82-847d-502feea5b8ed
# image: 379caead-7878-4d82-847d-502feea5b8ed
# file format: qcow2
# virtual size: 128 GiB (137438953472 bytes)  虚拟尺寸
# disk size: 1.42 GiB
# cluster_size: 65536
# Format specific information:
#     compat: 1.1
#     compression type: zlib
#     lazy refcounts: false
#     refcount bits: 16
#     corrupt: false

# 挂载到虚拟文件系统
export LIBGUESTFS_BACKEND=direct
virt-filesystems --long -h --all -a 379caead-7878-4d82-847d-502feea5b8ed
# Name       Type        VFS  Label  MBR  Size  Parent
# /dev/sda1  filesystem  xfs  -      -    128G  -
# /dev/sda1  partition   -    -      83   128G  /dev/sda
# /dev/sda   device      -    -      -    128G  
# 检查实际用量
virt-df  379caead-7878-4d82-847d-502feea5b8ed 
# Filesystem                           1K-blocks       Used  Available  Use%
# 379caead-7878-4d82-847d-502feea5b8ed:/dev/sda1
#                                      134206444    1678740  132527704    2%</pre>
<p>可以看到这是一个128G的XFS分区，由于XFS分区不支持Shrink，我们可以考虑将其备份，然后还原到一个较小的卷上。</p>
<pre class="crayon-plain-tag"># 从上述镜像创建一个卷，附到/dev/vdb
openstack volume create --size 130 --image centos8-amd64-prepared centos8-amd64 
# 创建一个新的2GB的目标卷，附到/dev/vdc

# 挂载
mount -t xfs /dev/vdb1 /tmp/vdb1                                                                                                                                                      
# mount: /tmp/vdb1: wrong fs type, bad option, bad superblock on /dev/vdb1, 
# missing codepage or helper program, or other error.
# 上述报错的原因是 vda vdb的UUID重复
uuidgen
xfs_admin -U 9e58a8e1-6962-4342-af59-ba10c53626cc /dev/vdb1
mount -t xfs /dev/vdb1 /tmp/vdb1     

# 导出
xfsdump -l 0 -f vdb /dev/vdb1 

# 导入
mkfs.xfs /dev/vdc1
mount /dev/vdc1 /tmp/vdc1
xfsrestore -f vdb /tmp/vdc1</pre>
<p>把这个2G的卷保存为镜像即可。</p>
<div class="blog_h3"><span class="graybg">IOError: 32 Corrupt image download</span></div>
<p>镜像被外部程序修改，建议重新上传镜像。</p>
<div class="blog_h3"><span class="graybg">Force upload to image is disabled, Force option will be ignored.</span></div>
<p>上传卷为镜像时报此错误：Invalid volume: Volume 8d2747cf-b50e-4d36-a90a-33dd7b56cad4 status must be available</p>
<p>解决方案，配置Cinder：</p>
<pre class="crayon-plain-tag">enable_force_upload = true</pre>
<p>重启控制节点服务：</p>
<pre class="crayon-plain-tag">systemctl restart openstack-cinder-volume
systemctl restart openstack-cinder-api
systemctl restart openstack-cinder-scheduler</pre>
<div class="blog_h3"><span class="graybg">Glance metadata cannot be updated, key signature_verified exists for volume</span></div>
<p>从镜像创建卷失败，报如上错误。移除镜像的signature_verified属性即可解决：</p>
<pre class="crayon-plain-tag">openstack image unset --property signature_verified centos8-amd64-prepared </pre>
<div class="blog_h2"><span class="graybg">存储相关</span></div>
<div class="blog_h3"><span class="graybg">Update driver status failed: (config name lvm) is uninitialized.</span></div>
<p><pre class="crayon-plain-tag">openstack volume service list</pre>报告某些节点的cinder-volume处于down状态：</p>
<pre class="crayon-plain-tag">openstack volume service list 
# +------------------+---------------+------+---------+-------+----------------------------+
# | Binary           | Host          | Zone | Status  | State | Updated At                 |
# +------------------+---------------+------+---------+-------+----------------------------+
# | cinder-volume    | centos-11@lvm | nova | enabled | down  | 2021-01-14T10:44:12.000000 |
# | cinder-volume    | centos-13@lvm | nova | enabled | down  | 2021-01-14T10:44:26.000000 |
# | cinder-volume    | centos-12@lvm | nova | enabled | down  | 2021-01-14T10:44:23.000000 |
# | cinder-scheduler | centos-10     | nova | enabled | up    | 2021-01-16T10:14:23.000000 |
# +------------------+---------------+------+---------+-------+----------------------------+</pre>
<p>查看对应节点的/var/log/cinder/volume.log，出现上述报错。</p>
<p>原因：LVM卷组cinder-volumes没有初始化。 </p>
<div class="blog_h2"><span class="graybg">快照相关</span></div>
<div class="blog_h3"><span class="graybg">卡在error_deleting如何强制删除</span></div>
<p>首先重置状态：</p>
<pre class="crayon-plain-tag">cinder snapshot-reset-state --state error  7f5154ca-9413-4071-b99f-ec60626a9efe</pre>
<p>然后登陆数据库删除记录：</p>
<pre class="crayon-plain-tag">use cinder;

update snapshots set deleted=1,status='deleted',deleted_at=now(),updated_at=now() where deleted=0 and id='7f5154ca-9413-4071-b99f-ec60626a9efe';</pre>
<div class="blog_h2"><span class="graybg">Flaver相关</span></div>
<div class="blog_h3"><span class="graybg">设置Flaver限速导致新实例报错 Illegal "rate"</span></div>
<p>报错内容：libvirt.libvirtError: internal error: Child process (tc filter add dev tapaf7dae14-ef parent ffff: protocol all u32 match u32 0 0 police rate 10485760kbps burst 10485760kb mtu 64kb drop flowid :1) unexpected exit status 1: Illegal "rate"</p>
<p>可能原因：限速的值太大了。</p>
<div class="blog_h2"><span class="graybg">环境变量相关</span></div>
<div class="blog_h3"><span class="graybg">nova命令环境变量</span></div>
<pre class="crayon-plain-tag">export OS_USERNAME=admin
export OS_PASSWORD=111111
export OS_TENANT_NAME=admin
export OS_AUTH_URL=http://192.168.101.250:35357/v3
export OS_USER_DOMAIN_NAME=Default                                                                                                                                                  
export OS_PROJECT_DOMAIN_NAME=Default</pre>
<div class="blog_h3"><span class="graybg">openstack命令环境变量</span></div>
<p>如果遇到报错：</p>
<p style="padding-left: 30px;">Ignoring domain related config project_domain_name because identity API version is 2.0</p>
<p style="padding-left: 30px;">Expecting to find domain in project. The server could not comply with the request since it is either malformed or otherwise incorrect. The client is assumed to be in error. (HTTP 400) (Request-ID: req-047e4968-2c06-4501-8635-0dd27093d5d8)</p>
<p>需要增加环境变量：</p>
<pre class="crayon-plain-tag">export OS_IDENTITY_API_VERSION=3</pre>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/openstack-study-note">OpenStack学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/openstack-study-note/feed</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>H.264学习笔记</title>
		<link>https://blog.gmem.cc/h264-study-note</link>
		<comments>https://blog.gmem.cc/h264-study-note#comments</comments>
		<pubDate>Sat, 16 Sep 2017 07:02:53 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Graphic]]></category>
		<category><![CDATA[Multimedia]]></category>
		<category><![CDATA[学习笔记]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=16164</guid>
		<description><![CDATA[<p>简介 MPEG MPEG是动态图像专家组（Moving Picture Experts Group）的简称，它可以指： 一个成立于1988年的，研究视频和音频编码标准的组织 一系列音视频编码标准，包括MPEG-1、MPEG-2、MPEG-3、MPEG-4、MPEG-7以及正在制定中的MPEG-21 MPEG-1 MPEG发布的第一个视频和音频有损压缩标准，它采用了块方式的运动补偿、离散余弦变换（DCT）、量化等技术，并为1.2Mbps传输速率进行了优化。其主要是为光盘类介质制定，随后被作为VCD（352×240，1.15mbps比特率）的核心技术。 MPEG-1标准由五个部分组成，其中第二部分、第三部分规定了视频、音频编码标准。 MPEG-1音频编码标准分为三代，逐代提升了压缩比。其中最著名的第三代协议被称为MPEG-1 Layer 3，简称MP3。 一个MPEG-1视频序列，包含多个图像群组（Group Of Pictures，GOP），每个GOP包含多个帧，每个帧包含多个slice。GOP由两个I帧之间的帧构成。 帧是MPEG-1的一个重要基本元素，一个帧就是一个完整的显示图像。帧的种类有四种： I帧（Intra Frame）即帧内帧，也叫关键帧。这类帧能够被独立的解码，可以看做是基线Profile的JPEG图像 P帧（Predicted Frame）即预测帧，也叫向前预测帧（ Forward-predicted Frames）。P帧利用视频中的时域冗余（ <a class="read-more" href="https://blog.gmem.cc/h264-study-note">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/h264-study-note">H.264学习笔记</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>
<div class="blog_h2"><span class="graybg">MPEG</span></div>
<p>MPEG是动态图像专家组（Moving Picture Experts Group）的简称，它可以指：</p>
<ol>
<li>一个成立于1988年的，研究视频和音频编码标准的组织</li>
<li>一系列音视频编码标准，包括MPEG-1、MPEG-2、MPEG-3、MPEG-4、MPEG-7以及正在制定中的MPEG-21</li>
</ol>
<div class="blog_h3"><span class="graybg">MPEG-1</span></div>
<p>MPEG发布的第一个视频和音频有损压缩标准，它采用了块方式的运动补偿、离散余弦变换（DCT）、量化等技术，并为1.2Mbps传输速率进行了优化。其主要是为光盘类介质制定，随后被作为VCD（352×240，1.15mbps比特率）的核心技术。</p>
<p>MPEG-1标准由五个部分组成，其中第二部分、第三部分规定了视频、音频编码标准。</p>
<p>MPEG-1音频编码标准分为三代，逐代提升了压缩比。其中最著名的第三代协议被称为<span style="background-color: #c0c0c0;">MPEG-1 Layer 3，简称MP3</span>。</p>
<p>一个MPEG-1视频序列，<span style="background-color: #c0c0c0;">包含多个图像群组（Group Of Pictures，GOP），每个GOP包含多个帧，每个帧包含多个slice</span>。<span style="background-color: #c0c0c0;">GOP由两个I帧之间的帧构成</span>。</p>
<p>帧是MPEG-1的一个重要基本元素，<span style="background-color: #c0c0c0;">一个帧就是一个完整的显示图像</span>。帧的种类有四种：</p>
<ol>
<li>I帧（Intra Frame）即帧内帧，也叫关键帧。这类帧能够被独立的解码，可以看做是基线Profile的JPEG图像</li>
<li>P帧（Predicted Frame）即预测帧，也叫向前预测帧（ Forward-predicted Frames）。P帧利用视频中的时域冗余（ Temporal Redundancy）来提高压缩比。P帧仅仅存储相对于它前面的那一帧的图像的差异（基于运动补偿和运动估计算法）部分</li>
<li>B帧（Bidirectional Frame）即双向预测帧，也叫向后预测帧（Backwards-predicted Frames）。B帧类似于P帧，但是它同时可以基于前一帧、后一帧进行预测。由于B帧可能依赖于“未来”的帧来解码，它会引入额外的编解码延迟</li>
<li>D帧（Direct Frame）即指示帧。仅仅由DC转换系数编码而成，因此其质量较低。D帧不会被I/P/B帧引用，仅仅在快速预览时有用。D帧实际应用的很少，后续标准也没有包含它</li>
</ol>
<div class="blog_h3"><span class="graybg">MPEG-2</span></div>
<p>通常用来为广播信号提供视频和音频编码，包括卫星电视、有线电视等。MPEG-2经过少量修改后，成为DVD产品的核心技术。</p>
<p>MPEG-2的视频部分，提供了对隔行扫描（广泛应用在广播电视领域，在CRT类显示器上，比相同帧率的逐行扫描更加不会引起视觉闪烁）视频显示模式的支持。</p>
<p>高级音频编码（Advanced Audio Coding，AAC）在MPEG-4发布前，作为MPEG-2的附加内容发布。</p>
<p>MPEG-2定义了两种复合信息流：传送流（TS）和节目流（PS：Program Stream）。TS流与PS流的区别在于TS流的包结构是固定长度的，而PS流的包结构是可变长度的。 PS包与TS包在结构上的这种差异，导致了它们对传输误码具有不同的抵抗能力，因而应用的环境也有所不同。TS码流由于采用了固定长度的包结构，当传输误码破坏了某一个TS包的同步信息时，接收机可在固定的位置检测它后面包中的同步信息，从而恢复同步，避免了信息丢失。而PS包由于长度是变化的，一旦某一PS包的同步信息丢失，接收机无法确定下一包的同步位置，就会造成失步，导致严重的信息丢失。因此，在信道环境较为恶劣，传输误码较高时，一般采用TS码流；而在信道环境较好，传输误码较低时，一般采用PS码流。由于TS码流具有较强的抵抗传输误码的能力，因此目前在传输媒体中进行传输的MPEG-2码流基本上都采用了TS码流。</p>
<div class="blog_h3"><span class="graybg">MPEG-3</span></div>
<p>本来的目标是为HDTV提供20-40Mbps视频压缩技术。在标准制定的过程中，委员会很快发现MPEG-2技术足以获取类似的效果，因此将其合并到MPEG-2，成为MPEG-2的延伸。</p>
<div class="blog_h3"><span class="graybg">MPEG-4</span></div>
<p>主要用途在于网上流、光盘、语音发送，以及电视广播。 MPEG-4吸收了MPEG-1、MPEG-2以及其它相关标准的很多特性。</p>
<p>MPEG-4仍然在进化之中，其关键组成部分是：</p>
<ol>
<li>第二部分：定义了一个编码器标准，DivX、Xvid都是该标准的实现</li>
<li>第十部分：即MPEG-4 AVC（高级视频编码， Advanced Video Coding），也称H.264。开源编码器x264、Quick Time7以及蓝光都遵循此标准</li>
</ol>
<div class="blog_h2"><span class="graybg">H.264</span></div>
<p>目前H.264已经成为高精度视频录制、压缩和发布的最常用格式之一。它不是单个标准，而是由<span style="background-color: #c0c0c0;">多个配置（Profile）构成的标准家族</span>。每个编码器至少需要支持一种H.264配置。</p>
<p>H.264能够在低带宽情况下提供优质视频，同等视频质量下，它仅仅需要MPEG-2/H.263/MPEG-4 Part2的一半甚至更少的带宽。</p>
<div class="blog_h1"><span class="graybg">格式与质量</span></div>
<p>视频编码是压缩、解压缩数字视频信号的处理过程。数字视频是真实世界中视觉影像的基于<span style="background-color: #c0c0c0;">空间、时间的采样</span>。</p>
<p>通常情况下，在<span style="background-color: #c0c0c0;">某一特定时刻对整个场景采样，形成帧（Frame）</span>，或者，对场景进行<span style="background-color: #c0c0c0;">隔行采样，所谓场（Field）</span>。采样总是按照一定的时间间隔进行，例如每1/25秒一次采样，这样连续的采样就形成了动态的视频信号，每秒钟采样的次数叫做<span style="background-color: #c0c0c0;">帧率（Frame Rate）</span>。为了表示彩色的场景，通常需要三个分量（Component）或者一系列的采样。</p>
<p>程序需要度量场景还原的精度，这样才能评估自身的性能。评估逻辑比较困难，原因是场景的质量很大程度上要考虑人的视觉心理特征，不同的人的视觉心理特征是不同的。</p>
<div class="blog_h2"><span class="graybg">自然的视觉场景</span></div>
<p>真实世界中的视觉场景，通常由多个物体构成。这些物体有各自的形状、深度（景深）、纹理、照度。自然视觉场景的颜色、亮度呈现出平滑变化的特征。</p>
<p>对自然视觉场景进行数字化处理时，程序需要关注两个维度：</p>
<ol>
<li>空间特征：单个场景内部纹理的变化特征、物体的数量和形状、颜色</li>
<li>时间特征：物体移动、明度变化、镜头/视点的切换</li>
</ol>
<div class="blog_h2"><span class="graybg">捕获</span></div>
<p>自然视觉场景在空间、时间上都是连续的。要以数字化的方式呈现这种场景，需要：</p>
<ol>
<li>空间采样：通常在场景的图像平面上设立矩形网格（Grid），采集离散的点（分辨率，帧大小），这些点分布在Grid的交叉处</li>
<li>时间采样：按照一定的间隔对帧或者帧的分量进行采样（帧率）。对于低质量的视频通信来说了，采样率通常在10-20FPS之间，这种级别的FPS下急速移动的物体不容易平滑显示；25-30FPS是典型的电视电影采样率；高达50-60FPS的时间采样下移动物体会非常平滑，代价是过高的数据率（Data Rate）</li>
</ol>
<p>时空采样的示意图如下：</p>
<p><img class="aligncenter size-large wp-image-16298" src="https://blog.gmem.cc/wp-content/uploads/2017/09/st-sample-1024x670.png" alt="st-sample" width="710" height="464" /></p>
<p>每个时空采样点 —— 叫做图像元素（Picture Element）或者像素（Pixel）——采用1-N个数字来表示。这些数字包含了亮度（Brightness）/照度（Luminance）、颜色信息。注意，从视频采集设备（如CCD）直接获得的采样阵列是模拟视频电信号。经过处理后才能变成像素表示的数字信号。</p>
<div class="blog_h3"><span class="graybg">亮度/照度</span></div>
<p>这两个概念是对同一事物的不同表述。</p>
<p>照度是一个客观性的概念，即以特定角度射向指定区域的光照强度。单位是坎德拉（Candela）/平方厘米（cd/cm2）。通过调整，不同显示器可以达到相同的光照强度。</p>
<p>亮度则是一个主观性的和光照相关的概念。显示器可以调整亮度，但是不好度量，只能自己感觉。</p>
<div class="blog_h3"><span class="graybg">帧和场</span></div>
<p>视频信号采样可以由：</p>
<ol>
<li>一系列连续的完整的帧构成，所谓逐行采样（Progressive Sampling） </li>
<li>一系列交错的场构成，所谓隔行采样（Interlaced Sampling）</li>
</ol>
<p>隔行采样时，通常在每个时间采样间隔中，两个场（分别由奇数行、偶数行构成）都进行采样。奇数行构成的场叫做Top Field，偶数行构成的场叫做Bottom Field。</p>
<p>隔行采样的优势是，提供两倍的帧率。根据人类的视觉停留的特点，可以避免产生画面抖动。</p>
<div class="blog_h2"><span class="graybg">色彩空间</span></div>
<p>相关文章：<a href="/image-processing-faq#color-space">图像处理知识集锦</a></p>
<p>大部分数字视频程序依赖于显示彩色图像，因此，需要一种机制来捕获、呈现颜色信息。单色图像仅仅需要一个数字来表示像素点的亮度/明度。彩色图像则需要至少三个数字来表示一个像素。</p>
<p>所谓色彩空间，就是用来描述<span style="background-color: #c0c0c0;">亮度/照度</span>（Brightness, Luminance or Luma）、<span style="background-color: #c0c0c0;">颜色</span>信息的方法。</p>
<div class="blog_h3"><span class="graybg">RGB</span></div>
<p>这种色彩空间中，一个采样点利用三个数字表示，分别代表红色、绿色、蓝色的<span style="background-color: #c0c0c0;">相对</span>比例。由于RGB是三原色，因而它们的组合可以形成任何颜色。RGB的各分量取值越大，则亮度越高。</p>
<div class="blog_h3"><span class="graybg">YCrCb</span></div>
<p>RGB色彩空间中，颜色信息、亮度信息是融合在一起的。然而，人类视觉系统（HVS）的特点是，对于亮度比颜色更加敏感。为了节省空间，可以把亮度信息分离出来，然后以较低的分辨率存储颜色信息，较高的分辨率存储亮度信息。</p>
<p>YCrCb（也叫YUV）就是一种分离亮度的色彩空间。其中Y是明度分量，Y根据根据RGB的权重计算出来：</p>
<p style="padding-left: 60px;"><em>Y = kr R + kgG + kbB        k*是颜色分量的权重因子</em></p>
<p>颜色信息则可以利用色差（Chrominance/Chroma）分量表示，每个色差分量即RGB与Y的差值：</p>
<p style="padding-left: 60px;"><em>Cr = R − Y</em><br /><em>Cb = B − Y</em><br /><em>Cg = G − Y</em></p>
<p>由于Cr+Cb+Cg求和是常量，因此，实际上仅仅需要记录两个色差信息就足够了。在YCrCb空间中，仅仅明度、红色差、蓝色差信号被传输。</p>
<p>YCrCb可以使用较低分辨率来描述Cr、Cb，由于HVS的特点，这样做图像质量不会受到太大影响。</p>
<div class="blog_h1"><span class="graybg">视频编码原理</span></div>
<p>视频编码的目的是实现视频压缩，这样视频信号更加容易存储和传输。没有压缩过的原始视频需要很高的比特率，对于标清视频来说，大概256pbps。</p>
<p>压缩总是要和解压缩配对使用，因此视频编码器通常包含压缩、解压缩两套算法。</p>
<p>某些类型的数据包含统计冗余（Statistical Redundancy），可以被无损的压缩/解压缩。不幸的是，要实现无损的图像、视频压缩，则压缩比会很低，因而在这些领域常常使用有损压缩。</p>
<p>视频的有损压缩原则是基于主观冗余（Subjective Redundancy），即在不太影响观察者的主观感受的前提下，信息可以被删除。大部分视频编码器同时关注空间、时间上的冗余：</p>
<ol>
<li>在时域上，相邻的场景总是有很大的相似性，特别是在高帧率的情况下</li>
<li>在空域上，类似于静态图片的压缩算法</li>
</ol>
<p>H.264和其它流行视频压缩算法——例如MPEG-2、MPEG-4、H.263——共享了一系列通用的特性。例如预测、基于块的运动补偿（Motion Compensation）。</p>
<div class="blog_h2"><span class="graybg">一般流程</span></div>
<p>编码器的一般工作流程如下图所示：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2017/09/encoder-procss.png"><img class="aligncenter size-full wp-image-16306" src="https://blog.gmem.cc/wp-content/uploads/2017/09/encoder-procss.png" alt="encoder-procss" width="100%" /></a></p>
<p>&nbsp;</p>
<p>编码器使用一个模型（Model）来表示视频源，一个搞笑的编码算法允许解码器尽可能保真的还原视频流。理想情况下，编码后的视频应该占据尽可能少的比特，同时尽可能的保真，当然这两个目标通常是冲突的。</p>
<p>编码器主要包括三个功能单元：预测模型、空闲模型、熵（Entropy，信息论中的熵是信息量的度量，熵越高包含的信息量越大）编码器：</p>
<ol>
<li>预测模型的输入时原始视频序列。预测模型利用邻近的视频帧/图像采样之间的相似性，来降低信息冗余。典型的做法是构造当前帧/视频数据块的预测（Prediction）。对于H.264来说，预测可以是：
<ol>
<li>帧内预测（Intra Prediction）：通过根据当前帧内的邻近的图像采样进行空间推断（Spatial Extrapolation），构造出预测</li>
<li>帧间预测（Inter Prediction）或者叫运动补偿预测（Motion Compensated Prediction）：通过补偿不同帧之间的差异构造出预测</li>
</ol>
</li>
<li>预测模型的输出是帧残余（Residual Frame）—— 从当前帧中减去预测，附加上说明帧间/帧内预测如何进行的模型参数</li>
<li>残余帧输入到空间模型，后者利用残余帧中的采样之间的相似性，降低空间冗余。H.264的做法是对残余帧进行转换并对结果进行量化。转换后的残余帧变为量化转换系数（Quantized Transform Coefficients）表示——量化移除了采样中不重要的数据以实现对残余帧的进一步压缩</li>
<li>预测模型的参数：帧内预测模式、帧间预测模式、运动向量（Motion Vectors），以及空间模型的参数，一起被熵编码器进一步压缩，移除统计学冗余数据。例如，反复出现的向量、系数被替换为简短的二进制代码。熵编码器产生容易传输的比特流或者文件</li>
<li>压缩完成后的视频序列，包括编码后的预测参数、编码后的残余系数，外加头信息</li>
</ol>
<div class="blog_h2"><span class="graybg">预测模型</span></div>
<p>预测模型处理的对象是当前帧/场中的一系列图像采样，其目标是减少数据冗余，其手段是构建一个预测，并将其中当前数据中减去。预测可能从先前已经编码好的帧中推导，此所谓时域预测；预测也可能从当前帧/场中已经编码好的图像采样中推导，此所谓空域预测。</p>
<p>预测模型的输出是<span style="background-color: #c0c0c0;">一系列残余/差异样本</span>。预测处理越精确，则残余样板中包含的Energy（信息量）越少。</p>
<div class="blog_h2"><span class="graybg">时域预测</span></div>
<p>被预测的帧的产生依赖于参考帧（Reference Frames），参考帧可以是过去或者未来的帧。帧预测的精度通常可以通过运动补偿——补偿当前帧和参考帧中由于物体移动产生的差异——的方式提高。</p>
<div class="blog_h3"><span class="graybg">简单预测</span></div>
<p>最简单的时域预测，是使用前一个帧（预测器，Predictor）来预测当前帧，<span style="background-color: #c0c0c0;">从当前帧中减去预测帧，直接得到帧残余</span>：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2017/09/simple-prediction.png"><img class="size-large wp-image-16312 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2017/09/simple-prediction-1024x323.png" alt="simple-prediction" width="710" height="223" /></a></p>
<p>这种预测方法的缺点是，残余的信息量很大。这些残余很大程度上都是因为物体运动导致的，因而更好的时域预测算法能够通过自动补偿，减少不必要的信息量。</p>
<div class="blog_h3"><span class="graybg">运动导致的差异</span></div>
<p>帧之间的差异，主要原因包括：<span style="background-color: #c0c0c0;">物体运动、未覆盖（Uncovered）的区域、光照变化</span>。</p>
<p>物体运动的类型包括：</p>
<ol>
<li>死板的平移，例如汽车运动</li>
<li>变形运动，例如人说话时脸部的运动</li>
<li>镜头运动，例如平移、倾斜、缩放、旋转</li>
</ol>
<p>未覆盖区域的类型包括：</p>
<ol>
<li>由于物体移动而显露出来的背景区域</li>
</ol>
<p>除了未覆盖区域、光照变化之外的其他帧间差异，都属于<span style="background-color: #c0c0c0;">帧间像素移动</span>。估算每个像素在帧间的移动轨迹（Trajectory）是可能的，<span style="background-color: #c0c0c0;">像素移动轨迹构成的场被称为光流（Optical Flow）</span>：</p>
<p><img class="size-full wp-image-16314 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2017/09/optical-flow.png" alt="optical-flow" width="636" height="434" /></p>
<p>上图是前面前帧、后帧之间的光流的示意图。</p>
<p>如果得到了精确的光流场，那么就可以构造当前帧的绝大部分像素的精确预测，只需要将参考帧中的每一个像素沿着它的光流向量（Optical Flow Vector）移动即可。然而，精确的光流场需要大量的计算资源才能获得。</p>
<div class="blog_h3"><span class="graybg">基于块的运动估算和补偿</span></div>
<p>实践中经常使用的一种运动补偿方是，针对块（当前帧中一个矩形区域）进行运动补偿，这种方法避免了逐像素光流计算的资源消耗。基于块的运动补偿的流程如下（针对当前帧中每一个MxN大小的采样块）：</p>
<ol>
<li>搜索过去或者未来的参考帧中的一个相似的MxN采样块。具体的做法可能是，将当前帧的MxN块和搜索区域中所有可能的MxN块进行比较，从中选取最匹配的块。一个流行的判断“匹配”的准则是，将两个块进行相减得到残余，残余的Energy越低匹配度越高。寻找<span style="background-color: #c0c0c0;">最佳</span>匹配的过程被称为移动估算（Motion Estimation）</li>
<li>最佳匹配的块被作为当前MxN块的预测器（Predictor），预测器和当前块求差后，形成一个MxN的残余块 —— 运动补偿（Motion Compensation）</li>
<li>编码后的残余块，外加预测器和当前块之间的位置偏移（运动向量，Motion Vector），被一起发送</li>
</ol>
<p>解码器利用运动向量重新定位预测器区域，解码残余块，将预测器 + 残余块即可还原当前块。</p>
<p>基于块的运动补偿之所以流行，有如下几个原因：</p>
<ol>
<li>计算资源的消耗相对较小，而且算法比较直观</li>
<li>帧本身都是矩形的，和块运动补偿很适配</li>
<li>基于块的图像转换算法（例如离散余弦变换，Discrete Cosine Transform，DCT）和块运动补偿很适配</li>
<li>对于很多视频序列来说，块运动补偿能提供高效的时域模型（Temporal Model）</li>
</ol>
<p>但是这种运动补偿也有缺陷：</p>
<ol>
<li>真实物体很少具有能匹配矩形区域的边界</li>
<li>物体的帧间移动距离，常常不是整数个像素</li>
<li>很多类型的对象运动很难通过基于块的方式补偿 —— 例如变形、旋转，以及类似云或者烟雾那样复杂的运动</li>
</ol>
<p>尽管如此，当前所有视频编码标准均将基于块的运动补偿作为时域预测模型的基础。</p>
<div class="blog_h3"><span class="graybg">宏块的运动补偿预测</span></div>
<p>宏块（Macroblock）是帧中16x16大小的区域，它是包括MPEG-1、MPEG-2、MPEG-4 Visual、H.261、H.262、H,264在内的很多视频编码标准的运动补偿预测的基本单元。</p>
<p>在常见的YUV 4:2:0图像编码格式中，一个宏块由：</p>
<ol>
<li>256个照度采样构成，这些采样组成4个8x8的采样块</li>
<li>64个红色色差采样构成，这些采样组成1个8x8的采样块</li>
<li>64个蓝色色差采样构成，这些采样组成1个8x8的采样块</li>
</ol>
<p>即一共6个采样块，示意图如下：</p>
<p><img class="size-full wp-image-16320 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2017/09/macroblock-420.png" alt="macroblock-420" width="100%" /></p>
<p>宏块的运动估计，主要是寻找参考帧中和当前宏块匹配的16x16采样区域。参考帧是先前就编码好的一个帧，在时间维上，参考帧可以在过去或者未来。参考帧中以当前宏块为中心的区域被搜索，寻找最佳匹配。</p>
<p>最佳匹配的照度、色差采样，被从当前宏块中减去，这样就产生了一个残余宏块。<span style="background-color: #c0c0c0;">残余宏块</span>与标示了最佳匹配区域和当前宏块的相对位移的<span style="background-color: #c0c0c0;">移动向量</span>一起编码并传输。</p>
<p>在上述基本的运动估计、运动补偿的基础上，有很多变体的算法：</p>
<ol>
<li>如果使用了未来的帧作为参考帧，则未来的帧必须在当前帧之前编码，也就是帧的编码必须是乱序的</li>
<li>当参考帧和当前帧的差异非常大时，不使用运动补偿可能更加高效，编码器可能选择使用帧内预测</li>
<li>视频中的移动物体很少能恰恰匹配16x16的边缘，因此使用可变大小的块往往更加高效</li>
<li>物体移动的距离可能不是整像素，例如物体可能在水平方向移动3.83像素的距离。因此一个好的预测算法会在搜索最佳匹配之前在参考帧中，在次像素级别进行插值</li>
</ol>
<div class="blog_h3"><span class="graybg">宏块的尺寸</span></div>
<p>宏块的尺寸越小，则残余帧的Energy越低，预测越精准。但是相应的，计算复杂度越高。</p>
<p>为此，一个折衷的方式是：对于扁平、均匀的区域选择大的宏块尺寸；对于高度细节、复杂的移动区域选择小的宏块尺寸。</p>
<div class="blog_h3"><span class="graybg">次像素运动补偿</span></div>
<p>某些情况下，从参考帧的插值后（非整数像素）的采样位置进行预测可能获得更佳的效果。例如下图中，参考帧区域中像素被插值到半像素级别，这样匹配位置的精度可以提高一倍，通过搜索插值采样，可能获得更好的匹配。</p>
<p><img class="size-large wp-image-16328 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2017/09/half-pixel-interp-1024x481.png" alt="half-pixel-interp" width="710" height="333" /></p>
<p>通常来说，更加细粒度的插值可以提供更好的运动补偿效果，得到更低Engery的残余，代价是更高的复杂性。 但是这种效果的提升不是线性的，插值越精细，效果进一步的提升越小。</p>
<div class="blog_h2"><span class="graybg">空间预测</span></div>
<p>对当前块的空间预测，是基于当前帧中其它先前编码过的采样进行的。假设帧中的块以光栅扫描（Raster-scan） 顺序逐个编码，则所有左上方向的块都可以用于当前块的帧内预测。由于左上方向的块已经编码并存放到输出流，解码器很自然的可以用它们进行预测的重建。</p>
<p>帧内预测的具体算法有很多，H.264使用的是空间外推法（Spatial Extrapolation）。一个/多个预测由当前块上侧或左侧的外推采样构成。通常最靠近的采样最可能和当前块中的采用具有相关性，因而仅仅沿着上侧/左侧边缘的那些像素才会用来创建预测块。一旦预测块被创建，会被用来产生残余块，具体方式和帧间预测类似。</p>
<div class="blog_h2"><span class="graybg">图像模型</span></div>
<p>自然的视频帧是一系列采样构成的Grid，这种图片的原始格式很难被压缩，因为邻近的采样具有高相关性。下图左侧是某个自然视频帧的2D自相关（Autocorrelation）函数的曲面，高度表现了图片与其空间偏移之后的副本的相关性，底面的两个维度表示了空间偏移的方向。缓和的坡度提示了邻近样本的高度相关性。</p>
<p><img class="alignnone size-full wp-image-16332" src="https://blog.gmem.cc/wp-content/uploads/2017/09/autocorrelation.png" alt="autocorrelation" width="100%" /></p>
<p>而经过运动补偿的残余图像的自相关性函数如上图右侧所示，可以看到随着空间偏移的增大，相关性急剧的降低。这提示了邻近采样的若相关性。有效的运动补偿/帧间预测降低了残余图像的本地相关性，让其比原始的视频帧更加容易被压缩。</p>
<p>图像模型的功能是，进一步的对残余图像进行去相关（Decorrelate），让它能够更有效的被熵编码器所压缩。图像模型通常有三个处理阶段：</p>
<ol>
<li>转换（Transformation）：对图片进行去相关、让数据更加紧凑（Compact）</li>
<li>量化（Quantization）：降低转换后数据的精度</li>
<li>重排（Reordering）：对数据进行重新排序，让关键数值（Significant Values）分组在一起</li>
</ol>
<div class="blog_h3"><span class="graybg">预测性图像编码</span></div>
<p>运动补偿是预测性编码的一个例子，编码器基于过去/未来的某个帧创建当前帧中某个区域的预测，然后把预测从当前区域中减去，得到一个残余。如果预测成功的话，残余的Energy会比原始区域小，需要更少的比特来表示。</p>
<p>预测性编码是早期的视频编码器的基础，也是H.264的帧内编码的重要组件。空间预测需要基于先前传输的、当前帧已经编码好的区域的样本，这种预测方法有时被称为差分脉冲编码调制（Differential Pulse Code Modulation）。</p>
<p>下面的公式示意像素X的编码过程，我们假设帧基于光栅顺序处理。A、B、C是对于编解码器都可用的参考（相邻）像素，这些像素应该在X之前被编解码：</p>
<p style="padding-left: 60px;"><em>P(X) = (2A + B + C)/4</em><br /><em>R(X) = X − P(X)</em></p>
<p> 解码器根据A、B、C可以重新构造出预测，然后<em>X = R(X) + P(X)</em>即解码出像素X。四个像素的位置关系如下图：</p>
<p><img class="size-full wp-image-16338 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2017/09/spatil-prediction.png" alt="spatil-prediction" width="550" height="339" /></p>
<p>如果编码过程是有损的，也就是说残余被量化过，那么解码得到的A'/B'/C'并不和A/B/C完全一致，而依据A'/B'/C'推导出来的X'则会与X有更多的误差。这会导致编码器和解码器之间的累积性的误差，或者叫漂移（Drift）。为了避免这种漂移，编码器可以使用解码后的参考像素来构建残余，即：</p>
<p style="padding-left: 60px;"><em>P(X) = (2A’ + B'+ C‘</em><em>) / 4</em></p>
<p>这样编码器、解码器使用相同的P(X)，也就避免了漂移。</p>
<p>上面介绍的预测性编码算法的效率取决于P(X)的精度，如果P(X)和X很接近，则残余的Engery很低，预测效率就高。但是，很难为复杂图像的所有区域选取一个通用的预测器。为了得到高性能，通常需要基于图像的本地统计信息进行自适应性的预测器选取。例如，对于图片中的扁平纹理、强竖直纹理、强水平纹理区域选取不同的预测器。由于你需要使用额外的bit来告知解码器使用了哪些预测器，因此要注意预测性能和流量消耗之间的权衡。</p>
<div class="blog_h3"><span class="graybg">转换</span></div>
<p>图片或者视频编码器的转换阶段的意图是，将图片或者运动补偿残余数据转换到转换域（Transform Domain）。选择转换算法取决于一系列准则：</p>
<ol>
<li>转换域中的数据应该是：
<ol>
<li>去相关的，也就是说，这些数据应该分离到最小相关性的分量中</li>
<li>紧凑的，大部分的Energy应该集中到数据的一小部分数值中</li>
</ol>
</li>
<li>转换必须是可逆的</li>
<li>转换对计算资源的需求必须是可容忍的，包括内存、CPU</li>
</ol>
<p>流行的图像/视频转换转发基本上分为两大类：</p>
<ol>
<li>基于块的：包括KLT、SVD、DCT。这些算法以NxN的采样为操作单元，其优点是内存用量小，适合压缩基于块的运动补偿残余。这类算法的缺点是块效应（Blockiness）明显</li>
<li>基于图像的：在整个图像/帧上，或者大块的区域（所谓Tile）上操作。包括离散小波变换（Discrete Wavelet Transform，DWT）。这类算法对于静态图像的压缩处理由于上一类，但是需要更高的内存</li>
</ol>
<div class="blog_h3"><span class="graybg">量化</span></div>
<p>量化器（Quantizer）将信号值范围X映射到一个较小的值范围Y。主要有两类量化器：</p>
<ol>
<li>标量量化器：将输入信号中的一个采样映射为一个量化的输出值</li>
<li>向量量化器：将输入信号中的一组采样映射为一组量化值</li>
</ol>
<div class="blog_h3"><span class="graybg">重排和零编码</span></div>
<p>对于一个基于转换的图像/视频编码器，量化器的输出是一个稀疏的数组。其中包含少量的非零系数，以及大量的零值系数。</p>
<p>重排阶段的工作就是把非零系数排列在一起，然后标识出这些系数在数组中的索引，实现压缩。</p>
<div class="blog_h2"><span class="graybg">熵编码器</span></div>
<p>熵编码器把一系列表示视频序列的元素转换成适合传输和存储的压缩比特流。输入符号包括量化后的转换系数、整/次像素级别的移动向量、标记性编码、宏块头、图像头等。</p>
<p>在信息论中，熵编码属于无损压缩，且压缩不受媒介的特质影响。熵编码器可以把定长的输入符号替换为相应的可变长度的代号（C<span style="color: #222222;">odeword），从而实现压缩。代号的长度和出现几率的负对数正相关，因而大部分公共符号具有最短的代号。</span></p>
<div class="blog_h1"><span class="graybg">H.264简介</span></div>
<div class="blog_h2"><span class="graybg">H.264是什么</span></div>
<p>从不同的视角看，H.264可以有不同的含义：</p>
<ol>
<li>它是一个工业标准，定义了一种压缩视频格式</li>
<li>一种流行的视频格式</li>
<li>一套用于视频压缩的工具 </li>
</ol>
<p>编码是视频类应用的基础技术，因为原始视频格式太大，难以传输或者存储。对视频编码进行标准化，可以让不同厂商开发的编码器、解码器、媒体存储能够方便的互操作。</p>
<p>典型的H.264应用，例如远程视频监控，视频从摄像头采集出来后被编码为H.264比特流，通过网络传输。终端应用解码比特流并获得原始视频：</p>
<p><img class="alignnone size-full wp-image-16343" src="https://blog.gmem.cc/wp-content/uploads/2017/09/h264-process.png" alt="h264-process" width="100%" /></p>
<p>H.264标准首次发布于2003年，之后经历了数次修订和更新。它基于早先的视频编码标准的设计理念，进一步提高了压缩视频的质量，在压缩、传输、存储方面有更大的灵活性。</p>
<p>H.264描述了一组用于压缩的工具/方法，规定了基于这些工具编码的视频如何呈现和解码。视频编码器可以选择一个工具，应用一些约束，然后处理视频流。H.264兼容的解码器必须能够使用工具组的<span style="background-color: #c0c0c0;">某个子集 —— 所谓配置（Profile）</span>。</p>
<div class="blog_h2"><span class="graybg">H.264如何工作</span></div>
<p>通过预测、转换、编码等处理过程，H.264编码器生成一个H.264比特流。解码器则进行逆向处理——解码、反向转换、重构——以生成原始（Raw）视频序列。</p>
<p>每个视频帧/场都需要被编码器处理，帧/场被编码后，可能被放到已编码图像缓冲中（Coded Picture Buffer，CPB）。在编码后续帧时，编码器可以使用CPB。类似的，解码器在解码出一个帧后，将其放到已解码图像缓冲中（Decded Picture Buffer，DPB），在解码后续帧时可以使用DPB。</p>
<p><img class="alignnone  wp-image-16347" src="https://blog.gmem.cc/wp-content/uploads/2017/09/h264-codec-high-lv-view.png" alt="h264-codec-high-lv-view" width="908" height="269" /></p>
<div class="blog_h3"><span class="graybg">编解码流程总览</span></div>
<p>H.264的数据处理单元是16x16大小的宏块（Macroblock） 。</p>
<p>在编码器中，预测宏块从当前宏块中减去，得到一个残余宏块。残余宏块被转换、量化并编码。在此同时，量化后的数据被重新扫描、反向转换并加上预测宏块，得到一个编码后的帧版本，然后存储起来用于后续的预测：</p>
<p><img class="alignnone  wp-image-16351" src="https://blog.gmem.cc/wp-content/uploads/2017/09/typical-h264-encoder.png" alt="typical-h264-encoder" width="911" height="372" /></p>
<p>在解码器中，宏块被解码、重新扫描、反向转换，得到一个编码过的残余宏块。解码器生成预测宏块后加上残余宏块，产生解码后的宏块：</p>
<p><img class="alignnone  wp-image-16353" src="https://blog.gmem.cc/wp-content/uploads/2017/09/typical-h264-decoder.png" alt="typical-h264-decoder" width="908" height="367" /></p>
<div class="blog_h3"><span class="graybg">编码流程</span></div>
<p>预测阶段，包括帧间预测和帧内预测。H.264支持的预测方法很灵活，从而实现更精确的预测。帧内预测使用16x16或者4x4的块大小，从当前宏块的四周进行预测。帧间预测的块大小可以在16x16 - 4x4之间自由变动，参考帧可以来自过去或者未来。</p>
<p>预测阶段产生的残余采样，使用4x4或者8x8的整数变换（Integer Transform）——离散余弦变换的近似形式——进行转换，转换的输出是一组系数。<span style="background-color: #c0c0c0;">系数的每个成员是一种标准化基本图式（Standard Basis Patterns）的权重值</span>。通过系数可以重新创建出残余采样：</p>
<p><img class="size-full wp-image-16359 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2017/09/forward-transform.png" alt="forward-transform" width="883" height="617" /></p>
<p>转换的结果进一步被量化，也就是，每个系数除以一个整数。量化后的转换系数精度降低：</p>
<p><img class="size-full wp-image-16360 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2017/09/quantization.png" alt="quantization" width="845" height="695" />视频编码最终产生的是一系列需要编码组成压缩比特流的数值，这些数值包括：</p>
<ol>
<li>量化后的转换系数</li>
<li>供解码器重建预测的信息</li>
<li>压缩数据结构相关信息</li>
<li>和完整视频序列有关的信息 </li>
</ol>
<p>这些数值和参数，以及语法元素（Syntax Elements），被可变长度编码/算术编码算法转换为二进制代码。</p>
<div class="blog_h3"><span class="graybg">解码流程</span></div>
<p>首先要进行的是对二进制比特流进行解码，解码语法元素并抽取上节所述的数值和参数。</p>
<p>然后是重扫描，每个系数乘以一个整数以近似的还原其原始值：</p>
<p><img class="aligncenter size-full wp-image-16362" src="https://blog.gmem.cc/wp-content/uploads/2017/09/rescaling.png" alt="rescaling" width="897" height="295" /></p>
<p>重扫描后的图式权重系数，加上标准化基础图式，经过反向离散余弦变换/整数变换可以重新创建出采样的残余数据：</p>
<p><img class="aligncenter size-full wp-image-16364" src="https://blog.gmem.cc/wp-content/uploads/2017/09/inverse-transform.png" alt="inverse-transform" width="795" height="607" /></p>
<p>得到采样残余后，解码器使用和编码器一样的预测，加上残余即得到原始图像。</p>
<div class="blog_h2"><span class="graybg">H.264语法</span></div>
<p>H.264规范清晰的定义了一套格式，或者叫语法。用于呈现压缩视频及其相关信息。这套语法的总体结构图如下：</p>
<p><img class="aligncenter  wp-image-16368" src="https://blog.gmem.cc/wp-content/uploads/2017/09/h264-syntax.png" alt="h264-syntax" width="902" height="924" /></p>
<p>在最顶层， 一个H.264序列由一系列的包（Packet），或者叫网络抽象层单元（Network Abstraction Layer Unit，NALU）构成。NAL可以包含解码器需要用到的关键参数集，这些参数集指示解码器如何正确的解码帧（Frame）或切片（Slice）。所谓切片，是指被分解后的帧的一部分，帧可以仅仅包含一个切片</p>
<p>在下一层，切片由一系列编码过的宏块组成。每个宏块对应帧中16x16大小的块。</p>
<p>在最底层，宏块包含描述自己如何被编码的信息 —— 编码的具体方法、预测信息、残余采样等。</p>
<div class="blog_h1"><span class="graybg">H.264语法</span></div>
<p>所谓H.264视频，是一种遵循特定规范——H.264/AVC语法——的视频序列。 此语法是H.264规范的一部分，它以语法元素的形式精确的描述了H.264视频序列结构的不同层面。</p>
<p>此语法是层次性的，它描述了最顶层的视频序列，以及下层的帧/场、切片，直到底层的宏块。控制参数可以：</p>
<ol>
<li>以独立的语法区段存储，例如参数集（Parameter Sets）</li>
<li>嵌入为其它区段（宏块层）的一部分</li>
</ol>
<div class="blog_h2"><span class="graybg">概要</span></div>
<p>H.264语法的层次性组织如下图所示：</p>
<p><img class="aligncenter size-large wp-image-16372" src="https://blog.gmem.cc/wp-content/uploads/2017/09/syntax-overview-966x1024.png" alt="syntax-overview" width="710" height="752" />说明如下：</p>
<ol>
<li>网络抽象层：由一系列的NAL单元组成：
<ol>
<li>SPS、PPS是特殊的NAL单元，作为解码器特定通用控制参数变更的信号</li>
<li>编码后的视频数据对应视频编码层（Video Coding Layer）NAL单元，也被称为切片（Slice）。每个访问单元（Access Unit），即编码后的帧/场，可以由1-N个切片构成</li>
</ol>
</li>
<li>切片层：每个切片包括切片头、切片数据两部分。切片数据是一系列编码后的宏块，外加可能的跳过提示符。跳过提示符用于指示特定的宏块位置没有数据</li>
<li>宏块层：每个编码后的宏块包括如下语法元素：
<ol>
<li>MB类型：
<ol>
<li>I：帧内编码</li>
<li>P：基于一个参考帧进行帧间编码</li>
<li>B：基于1-2个参考帧进行帧间编码</li>
</ol>
</li>
<li>预测信息：I宏块的预测模式，P/B宏块的参考帧和移动向量</li>
<li>编码块图式（Coded Block Pattern CBP）：提示哪些明度块、色差块包含非零残余系数</li>
<li>量化参数（Quantization Parameter QP）：仅仅CBP非零的宏块具有此元素</li>
<li>残余数据：仅仅CBP非零的宏块具有此元素</li>
</ol>
</li>
</ol>
<p>编码后的视频序列总是以即时解码器刷新（Instantaneous Decoder Refresh，IDR）访问单元开始，其包括若干个IDR切片。IDR切片是一种特殊的帧内编码切片。IDR访问单元后面跟着很多普通的访问单元序列。当一个新的视频序列到达时，需要提前再次发送IDR切片。此外传输结束时也发送IDR切片。</p>
<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="width: 20%; text-align: center;">包含区段</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>NAL unit</td>
<td>RBSP</td>
<td>
<p>网络抽象层单元，包含原始字节序列载荷（Raw Byte Sequence Payload，RBSP）。RBSP是包含了H.264语法元素的字节序列。H.264元素的长度是以位计算的可变长度，因此RBSP的总长度不一定是整数字节。因此RBSP尾部会补零（Trailing Bits）确保匹配整数字节</p>
<p>一个RBSP语法元素可以在单独的包中发送。某些语法端包含子段</p>
</td>
</tr>
<tr>
<td>SPS</td>
<td>Scaling List<br />VUI Parameters<br />Trailing bits</td>
<td>
<p>此区段是RBSP</p>
<p>序列参数集（Sequence Parameter Set），对于视频序列通用的参数</p>
</td>
</tr>
<tr>
<td>Scaling List </td>
<td> </td>
<td>编码器提供的用于反向量化处理的缩放矩阵 </td>
</tr>
<tr>
<td>SPS Extension</td>
<td>Trailing bits</td>
<td>此区段是RBSP。包含用于阿尔法混合（Alpha Blending，混合多个透明图片）的辅助图片信息</td>
</tr>
<tr>
<td>SEI</td>
<td>SEI Message<br />Trailing bits</td>
<td>
<p>此区段是RBSP</p>
<p>辅助增强信息（Supplement Enhancement Information），SEI消息容器</p>
</td>
</tr>
<tr>
<td>SEI Message </td>
<td>SEI payload</td>
<td>此区段是RBSPSEI消息可以用于辅助解码或显示，但是不影响解码帧的构建 </td>
</tr>
<tr>
<td>AUD</td>
<td>Trailing bits</td>
<td>
<p>此可选区段是RBSP</p>
<p>访问单元定界符（Access Unit Delimiter），可选的定界符，用于指示下一个编码图片的切片类型</p>
</td>
</tr>
<tr>
<td>End of Sequence</td>
<td> </td>
<td>
<p>此可选区段是RBSP。指示下一个切片是IDR </p>
</td>
</tr>
<tr>
<td>End of Stream</td>
<td> </td>
<td>此可选区段是RBSP。指示视频流的结束</td>
</tr>
<tr>
<td>Filler Data</td>
<td>Trailing bits </td>
<td>此可选区段是RBSP。填充字节序列</td>
</tr>
<tr>
<td>Slice layer</td>
<td>Slice header<br />Slice data<br />Trailing bits</td>
<td>此区段是RBSP。编码后的切片，分为几个类别：
<ol>
<li><span style="color: #333333; font-family: Ubuntu, 'Times New Roman', 'Bitstream Charter', Times, serif;"><span style="font-size: 13px; line-height: 22px;">Slice layer without partitioning，不适用分区的切片</span></span></li>
<li><span style="color: #333333; font-family: Ubuntu, 'Times New Roman', 'Bitstream Charter', Times, serif;"><span style="font-size: 13px; line-height: 22px;">Slice data partition A layer，分区切片的分区A</span></span></li>
<li>Slice data partition B layer，分区切片的分区B</li>
<li>Slice data partition C layer，分区切片的分区C</li>
</ol>
</td>
</tr>
<tr>
<td>Slice header</td>
<td>RPLR<br />PWT<br />DRPR</td>
<td>对于切片的通用参数 </td>
</tr>
<tr>
<td>RPLR</td>
<td> </td>
<td>引用图片列表重排（Reference Picture List Reordering） ，一系列用于修改默认引用图片列表顺序的命令</td>
</tr>
<tr>
<td>PWT</td>
<td> </td>
<td>预测权重表格（Prediction Weight Table） ，明度、色差权重偏移量，用于影响运动补偿预测的效果</td>
</tr>
<tr>
<td>DRPR</td>
<td> </td>
<td>解码后引用图片标记（Decoded Reference Picture Marking），一系列用于标记引用图片为长期引用的命令</td>
</tr>
<tr>
<td>Slice data</td>
<td>MB layer</td>
<td>包含一系列编码后的宏块</td>
</tr>
<tr>
<td>MB layer</td>
<td>MB prediction<br />Sub-MB prediction<br />Residual data</td>
<td>PCM头、宏块头、 预测、转换系数</td>
</tr>
<tr>
<td>MB prediction</td>
<td> </td>
<td>帧内预测模式，或者引用索引+移动向量</td>
</tr>
<tr>
<td>Sub-MB prediction </td>
<td> </td>
<td>引用索引+移动向量</td>
</tr>
<tr>
<td>Residual data </td>
<td>RB CAVLC<br />RB CABAC</td>
<td>包含一系列残余块，具体内容取决于CBP</td>
</tr>
<tr>
<td>RB CAVLC </td>
<td> </td>
<td>基于CAVLC编码的转换系数块 </td>
</tr>
<tr>
<td>RB CABAC</td>
<td> </td>
<td>基于CABAC编码的转换系数块</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">帧/场/图片</span></div>
<p>H.264将帧定义为一组明度采样数组，外加两组对应的色差采样数组。场分为Top field、Bottom field，共同构成帧，两个场可以同时扫描或者交错扫描。术语图片（Picture）作为帧/场的通称。</p>
<p>帧/场被解码，形成解码后图片并被放置到解码后图片缓冲（Decoded Picture Buffer，DPB）。此缓冲中的图片可以用于：</p>
<ol>
<li>支持后续的帧间预测</li>
<li>输出到显示组件</li>
</ol>
<p>区分以下三个顺序很重要：</p>
<ol>
<li>解码顺序：图片从比特流中被解码的顺序</li>
<li>显示顺序：图片输出到显示组件的顺序</li>
<li>参考顺序：图片如何被排列以供其它图片进行帧间预测</li>
</ol>
<div class="blog_h3"><span class="graybg">解码顺序</span></div>
<p>帧和帧之间可能存在引用（时域预测）关系，因此它们的解码顺序必须是确定的。</p>
<p>解码序（Decoding Order）确定了编码后的帧/场的解码顺序，由切片头参数frame_num确定。一般情况下，当前帧的frame_num为先前参考帧的frame_num+1。</p>
<div class="blog_h3"><span class="graybg">显示顺序</span></div>
<p>显示序（Display Order）即帧/场的播放顺序。由参数图像顺序计数器（POC，Picture Order Count）确定，POC参数包括TopFieldOrderCount、BottomFieldOrderCount。这两个参数也来自切片头，获取方法有三种：</p>
<ol>
<li>类型0：在每个切片头中，都包含了POC的最低有效位（Least Significant Bits），这种方式提供了灵活性但是占据更多字节。Type0示意如下图，箭头（从被参考帧发起）表示帧引用关系：<img class="aligncenter  wp-image-16451" src="https://blog.gmem.cc/wp-content/uploads/2017/09/doc-type0.png" alt="doc-type0" width="100%" /></li>
<li>类型1：在SPS中设立一个循环的POC计数器，POC依据此计数器循环计数，除非切片头使用Delta Offset</li>
<li>类型2：直接从frame_num获得，解码序和显示序一致</li>
</ol>
<div class="blog_h3"><span class="graybg">参考顺序</span></div>
<p>图片编码后，如果允许被其它图片参考，则进入已解码图片缓冲（Decoded Picture Buffer，DPB），并被标记为以下两种之一：</p>
<ol>
<li>短期参考图片，以frame_num或者POC进行索引。把这类图片从DPB移除的方法有：
<ol>
<li>通过比特流中明确的命令移除</li>
<li>如果启用了DPB自动处理模式，并且DPB已满，自动移除最旧的图片</li>
</ol>
</li>
<li>长期参考图片，以LongTermPicNum进行索引，此数字基于图片被标记为长期参考帧时设置的参数LongTermFrameIdx推导。这类图片需要通过比特流中明确的命令移除</li>
</ol>
<p>短期参考图片后续可以被赋予LongTermFrameIdx，导致它变为长期参考图片。</p>
<div class="blog_h3"><span class="graybg">默认参考图像列表顺序</span></div>
<p>参考图像列表（Reference Picture List） 是存放参考图片引用的列表。对于P切片来说，使用单个列表list0；对于B切片来说，使用两个列表list0、list1。</p>
<p>在每个列表中，短期参考图片排在前面，短期参考图片的排列规则：</p>
<ol>
<li>如果当前切片是P，依赖于解码序</li>
<li>如果当前切片是B，依赖于显示序</li>
</ol>
<p>长期参考图片排在短期参考图片后面，且按照LongTermPicNum升序排列。</p>
<p>列表元素的排序细节很重要，因为要引用列表中前面的项需要的比特数更少。因此默认排序规则让“接近”当前图像的参考图像排在列表前面，这些参考图像中存在最佳预测匹配的几率更大：</p>
<ol>
<li>P切片的list0：默认顺序是PicNum的降序，frame_num对MaxFrameNum取模得到PicNum</li>
<li>B切片的list0：默认顺序是：
<ol>
<li>如果参考图片的POC比当前图像早，则按POC降序</li>
<li>如果参考图片的POC比当前图片晚，则按POC升序</li>
</ol>
</li>
<li>B切片的list1：默认顺序是：
<ol>
<li>如果参考图片的POC比当前图像早，则按POC升序</li>
<li>如果参考图片的POC比当前图片晚，则按POC降序</li>
</ol>
</li>
</ol>
<div class="blog_h2"><span class="graybg">NAL单元</span></div>
<p>编码后的H.264数据以NAL单元这种数据包在网络中发送。每个NAL单元包含1字节的NALU头，后面跟着包含控制参数或者视频数据的比特流。</p>
<p>NALU头包含信息：</p>
<ol>
<li>NALU的类型<br />
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 70px; text-align: center;">值</td>
<td style="text-align: center;">NALU类型</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td>未使用</td>
<td> </td>
</tr>
<tr>
<td>1</td>
<td>Coded slice, non-IDR</td>
<td>典型的切片</td>
</tr>
<tr>
<td>2</td>
<td>Coded slice data partition A</td>
<td>数据分区切片，分区A</td>
</tr>
<tr>
<td>3</td>
<td>Coded slice data partition B</td>
<td>激活数据分区切片，分区B</td>
</tr>
<tr>
<td>4</td>
<td>Coded slice data partition C</td>
<td>数据分区切片，分区C</td>
</tr>
<tr>
<td>5</td>
<td>Coded slice, IDR</td>
<td>作为视频序列起点</td>
</tr>
<tr>
<td>6</td>
<td>SEI</td>
<td>补充增强信息</td>
</tr>
<tr>
<td>7</td>
<td>SPS</td>
<td>序列参数集，每序列一个</td>
</tr>
<tr>
<td>8</td>
<td>PPS</td>
<td>图像参数集</td>
</tr>
<tr>
<td>9</td>
<td>Access unit delimiter</td>
<td>提示下一个编码图片的切片类型</td>
</tr>
<tr>
<td>10</td>
<td>End of sequence</td>
<td>提示下一个NALU是IDR</td>
</tr>
<tr>
<td>11</td>
<td>End of stream</td>
<td>提示视频序列结束</td>
</tr>
<tr>
<td>12</td>
<td>Filler</td>
<td>填充字节</td>
</tr>
<tr>
<td>13-23</td>
<td>保留</td>
<td> </td>
</tr>
<tr>
<td>24-31</td>
<td>不保留，RTP打包用到</td>
<td> </td>
</tr>
</tbody>
</table>
</li>
<li>NALU的重要程度</li>
</ol>
<div class="blog_h3"><span class="graybg">NALU头结构</span></div>
<p>NALU头固定为1字节长，其结构示意图如下：</p>
<p style="padding-left: 60px;"><span class="monospace">+---------------+</span><br /><span class="monospace">|0|1|2|3|4|5|6|7|</span><br /><span class="monospace">+-+-+-+-+-+-+-+-+</span><br /><span class="monospace">|F|NRI| Type    |</span><br /><span class="monospace">+---------------+</span></p>
<p>其中：</p>
<ol>
<li>forbidden_zero_bit，第1位，必须为0</li>
<li>nal_ref_idc，第2-3位，重要程度。值越大越重要，当解码器过载时可以考虑把值为0的NALU丢弃。在RTP中使用，NRI还指示了传输的相对优先级</li>
<li>nal_unit_type，最后5位。类型6/9/10/11/12对应的NRI应该为00，类型7/8对应第NRI应该为11</li>
</ol>
<div class="blog_h2"><span class="graybg">参数集</span></div>
<p>参数集是携带了解码参数的NALU，这些参数对于后续若干切片是公用的，独立于切片发送参数集可以提高效率。 这些参数对于正确解码非常重要，在不可靠信道上传输视频流时，参数集可能丢失，可以考虑用更高的QoS发送参数集。</p>
<p>序列参数集（SPS）包含对整个视频序列有效的参数，例如Profile和Level、帧尺寸、某些解码器约束（例如参考帧最大数量）。SPS示例：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 35%; text-align: center;">参数</td>
<td style="width: 12%; text-align: center;">取值</td>
<td style="width: 8%; text-align: center;">符号</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>profile_idc</td>
<td>1000010    </td>
<td>66</td>
<td>使用Profile Baseline</td>
</tr>
<tr>
<td>constrained_set0_flag</td>
<td>0</td>
<td>0</td>
<td>比特流可能不遵从Baseline的所有约束</td>
</tr>
<tr>
<td>constrained_set1_flag</td>
<td>0</td>
<td>0</td>
<td>比特流可能不遵从Main的所有约束</td>
</tr>
<tr>
<td>constrained_set2_flag</td>
<td>0</td>
<td>0</td>
<td>比特流可能不遵从Extended的所有约束</td>
</tr>
<tr>
<td>reserved_zero_4bits</td>
<td>0</td>
<td>0</td>
<td>保留的4bit</td>
</tr>
<tr>
<td>level_idc</td>
<td>11110</td>
<td>30 </td>
<td>级别3</td>
</tr>
<tr>
<td>seq_parameter_set_id</td>
<td>1</td>
<td>0 </td>
<td>SPS标识符</td>
</tr>
<tr>
<td>log2_max_frame_num_minus4</td>
<td>1</td>
<td>0 </td>
<td>frame_num不大于16 </td>
</tr>
<tr>
<td>pic_order_cnt_type</td>
<td>1 </td>
<td>0 </td>
<td>默认POC </td>
</tr>
<tr>
<td>log2_max_pic_order_cnt_lsb_minus4</td>
<td>1</td>
<td>0 </td>
<td>POC的LSB不大于16 </td>
</tr>
<tr>
<td>num_ref_frames</td>
<td>1011 </td>
<td>10 </td>
<td>最多10个参考帧 </td>
</tr>
<tr>
<td>gaps_in_frame_num_value_allowed_flag</td>
<td>0</td>
<td>0 </td>
<td>frame_num中没有gap</td>
</tr>
<tr>
<td>pic_width_in_mbs_minus1</td>
<td>1011</td>
<td>10</td>
<td>11宏块宽 = QCIF</td>
</tr>
<tr>
<td>pic_height_in_map_units_minus1</td>
<td>1001</td>
<td>8</td>
<td>9宏块高 = QCIF </td>
</tr>
<tr>
<td>frame_mbs_only_flag</td>
<td>1</td>
<td>1</td>
<td>没有场切片或者场宏块</td>
</tr>
<tr>
<td>direct_8_×_8_inference_flag</td>
<td>1</td>
<td>1 </td>
<td>指定B宏块的移动向量如何得出 </td>
</tr>
<tr>
<td>frame_cropping_flag</td>
<td>0 </td>
<td>0</td>
<td>帧没有被裁剪</td>
</tr>
<tr>
<td>vui_parameters_present_flag</td>
<td>0</td>
<td>0</td>
<td>VUI参数不存在 </td>
</tr>
</tbody>
</table>
<p>图像参数集（PPS）包含应用到一部分帧的参数，例如熵编码类型、活动参考图片数量、初始化参数。PPS继承特定SPS的参数。PPS示例：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 35%; text-align: center;">参数</td>
<td style="width: 12%; text-align: center;">取值</td>
<td style="width: 8%; text-align: center;">符号</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td style="width: 35%;">pic_parameter_set_id</td>
<td style="width: 12%;">1 </td>
<td style="width: 8%;">0</td>
<td>PPS标识符</td>
</tr>
<tr>
<td>seq_parameter_set_id</td>
<td>1</td>
<td>0</td>
<td>继承自的SPS</td>
</tr>
<tr>
<td>entropy_coding_mode_flag</td>
<td>0</td>
<td>0</td>
<td>基于CAVLC进行熵编码</td>
</tr>
<tr>
<td>pic_order_present_flag</td>
<td>0</td>
<td>0</td>
<td>POC未设置</td>
</tr>
<tr>
<td>num_slice_groups_minus1</td>
<td>1</td>
<td>0</td>
<td>一个Slice组</td>
</tr>
<tr>
<td>num_ref_idx_l0_active_minus1</td>
<td>1010</td>
<td>9</td>
<td>第一个列表中有10个参考图像</td>
</tr>
<tr>
<td>num_ref_idx_l1_active_minus1</td>
<td>1010</td>
<td>9</td>
<td>第二个列表中有10个参考图像</td>
</tr>
<tr>
<td>weighted_pred_flag</td>
<td>0</td>
<td>0</td>
<td>没有使用权重预测</td>
</tr>
<tr>
<td>weighted_bipred_idc</td>
<td>0</td>
<td>0</td>
<td>没有使用双向权重预测</td>
</tr>
<tr>
<td>pic_init_qp_minus26</td>
<td>1</td>
<td>0</td>
<td>初始明度量化参数为26</td>
</tr>
<tr>
<td>pic_init_qs_minus26</td>
<td>1</td>
<td>0</td>
<td>初始SI/SP 量化参数为26</td>
</tr>
<tr>
<td>chroma_qp_index_offset</td>
<td>1</td>
<td>0</td>
<td>色差量化参数没有设置</td>
</tr>
<tr>
<td>deblocking_filter_control_present_flag</td>
<td>0</td>
<td>0</td>
<td>使用默认过滤器参数</td>
</tr>
<tr>
<td>constrained_intra_pred_flag</td>
<td>0</td>
<td>0</td>
<td>帧内预测不受限</td>
</tr>
<tr>
<td>redundant_pic_cnt_present_flag</td>
<td>0</td>
<td>0</td>
<td>没有使用荣誉图像计数参数</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">参数集的激活 </span></div>
<p>直到切片头引用PPS之前，PPS没有被激活，也就是编码器不使用它。一旦应用，则一直有效，直到另一个PPS被激活。</p>
<p>SPS仅仅在引用它PPS激活时，才被激活。单一的SPS之后对整个流有效，而流以IDR切片开始，因而通常由IDR激活SPS。</p>
<div class="blog_h2"><span class="graybg">切片层</span></div>
<p>每个编码后的帧/场都由1-N个切片组成。切片以切片头开始，后面跟着1-N个宏块，宏块的数量可以不固定。</p>
<p>切片大小的选择方式有：</p>
<ol>
<ol>
<ol>
<li>每个帧一个切片，很多H.264编码器选择这种方式</li>
<li>每个帧分为N个切片，每个切片分为M个宏块。切片的比特数量随着运动量的变大而便多</li>
<li>每个帧分为N个切片，每个切片包含的宏块数量不一定。这种方式可以让切片的比特数大致一致，用于固定长度的网络包</li>
</ol>
</ol>
</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="width: 20%; text-align: center;">内部宏块类型</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>I（包括IDR）</td>
<td>仅I</td>
<td>仅帧内预测</td>
</tr>
<tr>
<td>P</td>
<td>I或P</td>
<td>帧内预测、每个宏块分区基于一个参考帧预测</td>
</tr>
<tr>
<td>B</td>
<td>I、P或B</td>
<td>帧内预测、每个宏块分区基于1-2个参考帧预测</td>
</tr>
<tr>
<td>SP</td>
<td>P或I</td>
<td>用于切换到不同的流</td>
</tr>
<tr>
<td>SI</td>
<td>SI</td>
<td>用于切换到不同的流</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">切片头</span></div>
<p>切片头携带了对于所有宏块通用的信息，例如：</p>
<ol>
<li>切片类型，限制了宏块可能的类型</li>
<li>帧编号（Frame Number），此切片所属的帧</li>
</ol>
<p>一个示例切片头如下（IDR/Intra，Frame0）：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 35%; text-align: center;">参数</td>
<td style="width: 12%; text-align: center;">取值</td>
<td style="width: 8%; text-align: center;">符号</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td style="width: 35%;">first_mb_in_slice</td>
<td style="width: 12%;">1</td>
<td style="width: 8%;">0</td>
<td>第一个宏块位于位置0 —— 当前切片的左上角</td>
</tr>
<tr>
<td>slice_type</td>
<td>1000</td>
<td>7</td>
<td>这是一个I切片</td>
</tr>
<tr>
<td>pic_parameter_set_id</td>
<td>1</td>
<td>0</td>
<td>使用PPS 0</td>
</tr>
<tr>
<td>frame_num</td>
<td>0</td>
<td>0</td>
<td>此切片属于帧 0</td>
</tr>
<tr>
<td>idr_pic_id</td>
<td>1</td>
<td>0</td>
<td>仅出现在IDR切片，IRD #0</td>
</tr>
<tr>
<td>pic_order_cnt_lsb</td>
<td>0</td>
<td>0</td>
<td>POC = 0</td>
</tr>
<tr>
<td>no_output_of_prior_pics_flag</td>
<td>0</td>
<td>0</td>
<td>未使用</td>
</tr>
<tr>
<td>long_term_reference_flag</td>
<td>0</td>
<td>0</td>
<td>未使用长期参考帧</td>
</tr>
<tr>
<td>slice_qp_delta</td>
<td>1000</td>
<td>4</td>
<td>量化参数偏移量 = initial QP + 4 = 30</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">切片数据</span></div>
<p>切片数据为若干宏块的集合。有一种没有数据的宏块 —— Skip Macroblock，在很多编码序列中会出现。和熵编码器有关。</p>
<div class="blog_h2"><span class="graybg">宏块层 </span></div>
<p>宏块层中包含解码一个宏块所需要的所有语法元素：￼</p>
<p><img class="aligncenter size-full wp-image-16386" src="https://blog.gmem.cc/wp-content/uploads/2017/09/macroblock-syntax.png" alt="macroblock-syntax" width="445" height="1002" /></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>mb_type</td>
<td>指示当前宏块的类型，可以是I、SI、P、B，并且可以包含额外的信息说明宏块如何编码和预测</td>
</tr>
<tr>
<td>transform_size_8_×_8_flag</td>
<td>仅仅出现在High Profile中，此元素可以出现在两个位置之一，这取决于宏块的类型。此元素不会出现在16x16的帧内预测宏块</td>
</tr>
<tr>
<td>mb_pred</td>
<td>除了8x8分区大小的P/B宏块之外，指示帧内或者帧间预测类型</td>
</tr>
<tr>
<td>sub_mb_pred</td>
<td>8x8分区大小的P/B宏块，指示帧内或者帧间预测类型</td>
</tr>
<tr>
<td>coded_block_pattern</td>
<td>除了16x16帧内预测块，取值范围0-47之间</td>
</tr>
<tr>
<td>delta_qp</td>
<td>指示量化参数的变化</td>
</tr>
<tr>
<td>residual_data</td>
<td>残余块</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">配置和级别</span></div>
<p>前文提到过，H.264支持一组工具 —— 处理编解码的算法和过程。这些工具中有些很基础，任何编解码器实现都需要用到，例如4x4的变换算法。另外一些工具是可选的，例如CABAC/CAVLC熵编码器。</p>
<p>通过按需选择标准中定义的工具，编码器的实现可以非常的灵活，编码器可以仅仅使用工具的某些子集。</p>
<div class="blog_h2"><span class="graybg">Profile</span></div>
<p>H.264配置（Profile）规范了工具子集的定义。任何H.264比特流必须遵从Profile规范，使用子集中部分或者全部工具实现编码。一个Profile兼容的解码器，必须能够解码使用子集中任何工具编码的H.264比特流。</p>
<p>当前最广泛使用的Profile是Main，其包含的工具很好的在压缩性能和计算复杂度之间进行权衡。Constrained Baseline这个Profile是Main的一个子集，在低复杂度、低延迟的应用程序中非常流行，例如移动视频电话。</p>
<div class="blog_h3"><span class="graybg">Main及其子集</span></div>
<p>Extended、Main、Baseline、Constrained Baseline之间的关系如下图：</p>
<p><img class="aligncenter  wp-image-16388" src="https://blog.gmem.cc/wp-content/uploads/2017/09/main-profiles.png" alt="main-profiles" width="851" height="764" /></p>
<p>说明如下：</p>
<ol>
<ol>
<ol>
<li>Baseline设计用于低复杂度、低延迟应用程序，例如移动视频电话。它支持：
<ol>
<li>I帧、P帧</li>
<li>允许帧内预测、基于单个参考帧的运动补偿</li>
<li>使用基本的4x4整数变换</li>
<li>使用CAVLC熵编码</li>
<li>支持FMO、ASO、冗余切片，这些技术用于提高传输效率</li>
</ol>
</li>
<li>上面提到的最后三个工具并不流行，很多实现不能完整支持。排除了这三者的Profile就是Constrained Baseline</li>
<li>Extended是Baseline的超集，添加了某些提高网络流传输效率的工具</li>
<li>Main是Constrained Baseline的超级，支持CABAC编码和B帧</li>
</ol>
</ol>
</ol>
<div class="blog_h3"><span class="graybg">High</span></div>
<p>高配置分为4个级别，添加了一些编码工具，满足高质量应用程序（高分辨率、扩展比特深度、高色彩深度）的需要。High是Main的超集，添加特性：</p>
<ol>
<ol>
<ol>
<li>8x8变换</li>
<li>8x8帧间预测，这提高了编码性能，特别是高空间分辨率情况下</li>
<li>支持频率相关的量化器权重</li>
<li>为Cr、Cb分开设置量化器参数</li>
<li>支持单色差视频（4:0:0格式）</li>
</ol>
</ol>
</ol>
<p>不同级别High和Main的关系如下图：</p>
<p><img class="aligncenter  wp-image-16389" src="https://blog.gmem.cc/wp-content/uploads/2017/09/high-profiles.png" alt="high-profiles" width="895" height="555" /></p>
<div class="blog_h2"><span class="graybg">Level</span></div>
<p>级别规定了帧尺寸、处理速度（每秒能够解码的帧或者块数量）、工作内存的最大需求量。Profile + Level共同构成了对解码器的约束条件。</p>
<p>照度分辨率、帧率和级别的关系如下表：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">分辨率</td>
<td style="text-align: center;">最大帧率</td>
<td style="text-align: center;">级别</td>
</tr>
</thead>
<tbody>
<tr>
<td rowspan="2">QCIF (176x144)</td>
<td>15</td>
<td>1, 1b</td>
</tr>
<tr>
<td>30</td>
<td>1.1</td>
</tr>
<tr>
<td rowspan="2">CIF (352x288)</td>
<td>15</td>
<td>1.2</td>
</tr>
<tr>
<td>30</td>
<td>1.3, 2</td>
</tr>
<tr>
<td>525 SD (720x480)</td>
<td>30</td>
<td>3</td>
</tr>
<tr>
<td>625 SD (720x576)</td>
<td>25</td>
<td>3</td>
</tr>
<tr>
<td>720p HD (1280x720)</td>
<td>30</td>
<td>3.1</td>
</tr>
<tr>
<td rowspan="2">1080p HD (1920x1080)</td>
<td>30</td>
<td>4, 4.1</td>
</tr>
<tr>
<td>60</td>
<td>4.2</td>
</tr>
<tr>
<td>4Kx2K (4096x2048)</td>
<td>30</td>
<td>5.1</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">H.264的传输</span></div>
<div class="blog_h2"><span class="graybg">传输支持工具</span></div>
<p>鉴于大部分H.264应用程序都牵涉到传输、存储比特流。H.264标准引入了一系列工具和特性，让传输过程更加高效和健壮。</p>
<p>需要注意：大部分商业编解码器没有支持这些特性。</p>
<div class="blog_h3"><span class="graybg">冗余切片</span></div>
<p>如果包含某个帧部分或者全部的重复信息，一个切片可以被标记为冗余的。解码器通常基于非冗余切片重构帧，如果非冗余切片损坏或者丢失，则使用冗余切片。</p>
<p>冗余切片增强了健壮性，代价是更高的比特率。</p>
<div class="blog_h3"><span class="graybg">任意切片顺序</span></div>
<p>任意切片顺序（Arbitrary Slice Order，ASO）允许帧中的切片以任意（非光栅序）的解码顺序排列。可以用于辅助解码错误的隐藏。</p>
<div class="blog_h3"><span class="graybg">切片组/灵活宏块排序</span></div>
<p>灵活宏块排序（Flexible Macroblock Ordering，FMO）让帧中的宏块被分配到一个或者多个切片组（Slice Groups）中。每个切片组包含1-N个切片。在切片组内部，宏块以光栅序编码，但是这些宏块在帧中的位置不一定相邻。宏块和切片组的对应关系由宏块分配映射（Macroblock Allocation Map）指定。</p>
<p>FMO可以增加容错性，因为每个切片组可以独立的解码。如果在使用交错排序的情况下，一个切片或者切片组丢失，其影响可以利用空间插值屏蔽掉。</p>
<div class="blog_h3"><span class="graybg">SP/SI切片</span></div>
<p>SP和SI切片的用途是：</p>
<ol>
<ol>
<ol>
<li>允许高效的在不同视频流之间切换</li>
<li>允许解码器进行高效的随机访问</li>
</ol>
</ol>
</ol>
<p>例如，同一视频源使用不同码率在网络中传输，解码器可以在正常情况下使用高码率，并且在网络拥塞的时候切换到低码率。</p>
<div class="blog_h3"><span class="graybg">数据分区切片</span></div>
<p>该特性将切片分为三个区：NAL头</p>
<ol>
<ol>
<ol>
<li>A分区：包含切片头、每个宏块的头</li>
<li>B分区：包含帧内预测的残余数据、SI切片宏块</li>
<li>C分区：包含帧间预测的残余数据、SP切片宏块</li>
</ol>
</ol>
</ol>
<p>每个分区都是独立的NAL单元。A、B、C分区的容错度依次增高，传输时可以应用不同的QoS。</p>
<div class="blog_h2"><span class="graybg">RBSP/NALU/Packet封装</span></div>
<p>这三者的逐层封装关系如下图：</p>
<p><img class="aligncenter size-full wp-image-16394" src="https://blog.gmem.cc/wp-content/uploads/2017/09/h264-encap.png" alt="h264-encap" width="868" height="672" /></p>
<p>说明：</p>
<ol>
<ol>
<ol>
<li>H.264语法元素被封装为RBSP，后者被封装为NALU。由于H.264语法元素是可变长度的，为了将RBSP字节对齐，尾部可能需要补零</li>
<li>RBSP封装进NALU的方式如下：
<ol>
<li>添加一字节的NALU头</li>
<li>按序添加模拟预防（Emulation Prevention）字节。为了防止起始码（Start Code）出现在NALU内部，每当出现和起始码前缀一致的3字节图式时，就插入一个模拟预防字节（二进制00000011）。解码器能够检测到模拟预防字节，进而知道其相邻的字节不是起始码</li>
</ol>
</li>
<li>NALU可以作为传输协议的载荷。每个NALU都前缀一个起始码，起始码为三字节的特殊序列。解码器依赖起始码来判断NALU的边界 </li>
</ol>
</ol>
</ol>
<div class="blog_h2"><span class="graybg">RTP传输</span></div>
<p>相关文章：<a href="/realtime-communication-protocols">实时通信协议族</a>。</p>
<p>H.264对传输协议没有任何规定，常用的传输协议是RTP。RTP是一种常见的打包协议，一般在UDP基础上运行。RTP Payload Formats为很多标准的音视频编码格式定义了标准。</p>
<p>本节主要讨论<a href="https://tools.ietf.org/html/rfc6184">RFC 6184</a>：RTP Payload Format for H.264 Video。</p>
<div class="blog_h3"><span class="graybg">RTP载荷结构</span></div>
<p>用于H.264传输时，RTP支持三种载荷结构。接收方可以根据载荷的首字节来识别载荷结构。这个直接也作为RTP载荷头，某些情况下还作为载荷的组成部分（第一字节）。</p>
<p>此首字节的格式和NALU头格式一致。其中NALU类型字段指明了载荷结构是哪一种：</p>
<ol>
<li>单NALU包：载荷中仅仅包含单个NALU，NALU类型取值范围在1-23之间</li>
<li>聚合包：载荷中包含多个NALU。这种载荷结构具有4种子类型：
<ol>
<li>STAP-A：单一时间聚合包，类型A。NALU类型为24</li>
<li>STAP-B：单一时间聚合包，类型B。NALU类型为25</li>
<li>MTAP16：多时间聚合包，使用16bit偏移。NALU类型为26</li>
<li>MTAP24：多时间聚合包，使用24bit偏移。NALU类型为27</li>
</ol>
</li>
<li>片断单元：其中仅仅包含NALU的一部分，这种方式允许NALU拆分到多个RTP包中传输。这种载荷结构具有2种子类型：
<ol>
<li>FU-A。NALU类型为28</li>
<li>FU-B。NALU类型为29</li>
</ol>
</li>
</ol>
<p>此首字节的NRI字段，00表示可丢弃，这个语义和H.264规范是一致的，解码器不关心任何非零NRI的具体值。RFC6184对非零值的含义进行了延伸，用于表示传输相对优先级。MANE可以对高优先级的包进行更强的保护，防止丢包，11是最高优先级。</p>
<div class="blog_h3"><span class="graybg">打包模式</span></div>
<p>对NALU的打包模式有三种：</p>
<ol>
<li>单NALU模式，用于遵从H.241规范的系统</li>
<li>非交错模式，用于可能不遵循H.241规定的系统。在此模式下，NALU的按解码序传输</li>
<li>交错模式，用于不需要非常低的端对端延迟低系统。此模式允许NALU不按解码序传输</li>
</ol>
<p>打包模式可以从SDP的packetization-mode字段获得：</p>
<ol>
<li>0或者没有该字段对应单NALU模式</li>
<li>1对应非交错模式</li>
<li>2对应交错模式</li>
</ol>
<p>打包模式和载荷结构类型的兼容性表格如下：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">载荷类型</td>
<td style="text-align: center;">单NALU模式</td>
<td style="text-align: center;">非交错模式</td>
<td style="text-align: center;">交错模式</td>
</tr>
</thead>
<tbody>
<tr>
<td>reserved</td>
<td>忽略</td>
<td>忽略</td>
<td>忽略</td>
</tr>
<tr>
<td>单NALU</td>
<td>是</td>
<td>是</td>
<td>否</td>
</tr>
<tr>
<td>STAP-A</td>
<td>否</td>
<td>是</td>
<td>否</td>
</tr>
<tr>
<td>STAP-B</td>
<td>否</td>
<td>否</td>
<td>是</td>
</tr>
<tr>
<td>MTAP16</td>
<td>否</td>
<td>否</td>
<td>是</td>
</tr>
<tr>
<td>MTAP24</td>
<td>否</td>
<td>否</td>
<td>是</td>
</tr>
<tr>
<td>FU-A</td>
<td>否</td>
<td>是</td>
<td>是</td>
</tr>
<tr>
<td>FU-B</td>
<td>否</td>
<td>否</td>
<td>是</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">单NALU包</span></div>
<p>此类型的载荷仅仅包含一个NALU，并且，<span style="background-color: #c0c0c0;">NALU头即为RTP载荷头</span>： </p>
<p><img class="aligncenter size-full wp-image-16408" src="https://blog.gmem.cc/wp-content/uploads/2017/09/rpt-single-nalu.png" alt="rpt-single-nalu" width="767" height="256" /></p>
<p>NALU由NALU头、NALU载荷组成，<span style="background-color: #c0c0c0;">前缀的开始码（00 00 00 01或者00 00 01）被清除</span>后再打包。</p>
<div class="blog_h3"><span class="graybg">聚合包</span> </div>
<p>引入聚合包的原因是不同网络的MTU不同：</p>
<ol>
<li>有线IP网络的MTU主要受限于以太网MTU，大概1500字节左右</li>
<li>IP/非IP无线网络的MTU首先的传输单元较小，大概254字节或者更少 </li>
</ol>
<p>由于RTP通常基于UDP，UDP包的尺寸则受限于MTU。因此聚合包能够最高效率的使用MTU。</p>
<p>聚合包分为两大类，分别为单时间（STAP）、多时间（MTAP）聚合包。前者的NALU时间只有一个值。后者包含低NALU可能对应不同的时间。聚合包中的每个NALU都基于聚合单元打包：</p>
<p><img class="aligncenter size-full wp-image-16409" src="https://blog.gmem.cc/wp-content/uploads/2017/09/rtp-aggr-pkg.png" alt="rtp-aggr-pkg" width="766" height="252" /></p>
<p>STAP和MTAP共享以下打包规则：</p>
<ol>
<li>RTP时间戳必须设置为包内所有NALU的最早的那个NALU-time</li>
<li>NALU类型必须正确设置</li>
<li>如果所有NALU的F位均为0，F位必须清零</li>
<li>NRI必须设置为所有NALU中NRI的最大值</li>
</ol>
<p>STAP-A载荷结构如下：</p>
<p><img class="aligncenter size-full wp-image-16412" src="https://blog.gmem.cc/wp-content/uploads/2017/09/rtp-stap.png" alt="rtp-stap" width="762" height="250" />STAP-B载荷结构如下：</p>
<p><img class="aligncenter size-full wp-image-16414" src="https://blog.gmem.cc/wp-content/uploads/2017/09/rtp-mtap.png" alt="rtp-mtap" width="766" height="253" />两者的主要区别是STAP-B有一个DON字段，它以传输序指定了第一个NALU的位置，后续NALU的DON = (第一个NALU的DON + 1) %65536。</p>
<p>STAP-A包含1-N个聚合单元，聚合单元的结构如下：</p>
<p><img class="aligncenter size-full wp-image-16415" src="https://blog.gmem.cc/wp-content/uploads/2017/09/rtp-stau.png" alt="rtp-stau" width="767" height="253" /></p>
<p>STAP-A、STAP-B的聚合单元的头部是16bit的网络序无符号整数，指示后续NALU的长度（字节）。聚合单元在RTP包内是字节对齐的。</p>
<p>MTAP载荷由一个16bit网络序无符号整数的解码序号基数（Decoding Order Number Base）和1-N个聚合单元组成。DONB的值为当前包中以NALU解码序计第一个NALU的DON值。</p>
<p>MTAP的聚合单元MTAP16的结构如下：</p>
<p><img class="aligncenter size-full wp-image-16417" src="https://blog.gmem.cc/wp-content/uploads/2017/09/rtp-mtap16.png" alt="rtp-mtap16" width="768" height="253" /></p>
<p>MTAP24的结构如下：</p>
<p><img class="aligncenter size-full wp-image-16418" src="https://blog.gmem.cc/wp-content/uploads/2017/09/rtp-mtap24.png" alt="rtp-mtap24" width="779" height="268" /></p>
<p>两种聚合单元都有如下字段：</p>
<ol>
<li>16bit的NALU大小</li>
<li>8bit的解码序号差异（Decoding Order Number Difference，DOND）</li>
<li>Nbit的时间戳偏移，对于MTAP16 N = 16，对于MTAP24 N = 24</li>
</ol>
<div class="blog_h3"><span class="graybg">分段包</span></div>
<p>当NALU长度超过MTU后，可以使用分段包，让一个NALU分散在多个RTP包中。FU有两个子类，其中FU-A的结构如下：</p>
<p><img class="aligncenter size-full wp-image-16420" src="https://blog.gmem.cc/wp-content/uploads/2017/09/rtp-fu.png" alt="rtp-fu" width="765" height="250" /></p>
<p>FU-B必须配合交错打包模式使用。其结构如下：</p>
<p><img class="aligncenter size-full wp-image-16421" src="https://blog.gmem.cc/wp-content/uploads/2017/09/rtp-fu-b.png" alt="rtp-fu-b" width="770" height="251" /></p>
<p>FU indicator结构与NALU头一致，NALU类型取值28或29。</p>
<p>FU header结构如下：</p>
<p style="padding-left: 60px;"><span class="monospace">+---------------+</span><br /><span class="monospace">|0|1|2|3|4|5|6|7|</span><br /><span class="monospace">+-+-+-+-+-+-+-+-+</span><br /><span class="monospace">|S|E|R| Type    |</span><br /><span class="monospace">+---------------+</span></p>
<p>其中：</p>
<ol>
<li>S：1bit，如果设置此位，表示此FU是第一个NALU分片</li>
<li>E：1bit，如果设置此位，表示此FU是最后一个NALU分片</li>
<li>R：1bit，取值0</li>
<li>Type：NALU载荷类型</li>
</ol>
<div class="blog_h1"><span class="graybg">X264</span></div>
<p>x264是VideoLAN开源的H.264编码器，特性包括：</p>
<ol>
<li>8x8和4x4自适应空域变换</li>
<li>自适应B帧置入</li>
<li>B帧作为参考帧</li>
<li>任意帧顺序</li>
<li>支持CAVLC、CABAC熵编码</li>
<li>自定义量化矩阵</li>
<li>帧内预测：16x16, 8x8, 4x4等任意宏块大小</li>
<li>帧间预测：所有分区大小，从16x16到4x4</li>
<li>帧间双向预测：分区大小支持从16x16到8x8</li>
<li>交错扫描—— 宏块级帧场自适应（Macro-block Adaptive Field Frame，MBAFF）</li>
<li>多参考帧</li>
<li>速率控制：常量量化器、常量质量、单步/多步ABR、可选VBV</li>
<li>场景切换（Scenecut）检测</li>
<li>B帧中的空域/时域直接模式，自适应模式选择</li>
<li>使用多个CPU并行编码</li>
<li>预测性无损模式</li>
</ol>
<div class="blog_h2"><span class="graybg">构建</span></div>
<pre class="crayon-plain-tag">git clone http://git.videolan.org/git/x264.git
cd x264
./configure --enable-debug --enable-static --enable-shared --prefix=/home/alex/CPP/lib/x264
make &amp;&amp; make install &amp;&amp; make clean</pre>
<div class="blog_h2"><span class="graybg">HelloWorld</span></div>
<p>下面是一个编码QCIF尺寸的YUV序列的示例。CMake项目配置：</p>
<pre class="crayon-plain-tag">cmake_minimum_required(VERSION 3.6)
project(x264 C)

set(X264_HOME /home/alex/CPP/lib/x264)
include_directories(${X264_HOME}/include)

set(SRC_ENCODER encoder.c)
add_executable(encoder ${SRC_ENCODER})
target_link_libraries(
        encoder
        ${X264_HOME}/lib/libx264.so
)</pre>
<p> 编码器源码：</p>
<pre class="crayon-plain-tag">#include &lt;stdint.h&gt;
#include &lt;stdio.h&gt;
#include &lt;x264.h&gt;

int main( int argc, char **argv ) {
    int width = 176, height = 144;    //QCIF
    x264_param_t param;
    x264_picture_t pic;
    x264_picture_t pic_out;
    x264_t *h;
    int i_frame = 0;
    int i_frame_size;
    x264_nal_t *nal;
    int i_nal;

    /* 应用编码器预设 */
    if ( x264_param_default_preset( &amp;param, "fast", "zerolatency" ) &lt; 0 ) goto fail;

    /* 在预设的基础上定制 */
    param.i_csp = X264_CSP_I420;      // 色彩空间：yuv 4:2:0 planar（三个数组）
    param.i_width = width;            // 帧尺寸
    param.i_height = height;
    param.b_vfr_input = 0;            // 可变帧率的输入：否
    param.b_repeat_headers = 1;       // 在每个I帧之前插入SPS/PPS
    param.b_annexb = 1;               // 在NALU之前插入4字节起始码

    /* 设置Profile，x264仅仅提供了Baseline选项，但是由于不支持某些特性，编码实际使用的是Constrained Baseline */
    if ( x264_param_apply_profile( &amp;param, "baseline" ) &lt; 0 ) goto fail;
    if ( x264_picture_alloc( &amp;pic, param.i_csp, param.i_width, param.i_height ) &lt; 0 ) goto fail;
#undef fail
#define fail fail2
    // 创建句柄
    h = x264_encoder_open( &amp;param );
    if ( !h ) goto fail;
#undef fail
#define fail fail3

    int luma_size = width * height;    // 单帧包含的明度元素个数
    int chroma_size = luma_size / 4;   // 单帧包括的色差元素个数
    /* 循环编码所有帧  */
    FILE *in = fopen( "/home/alex/CPP/projects/clion/x264/qcif.yuv", "r" );
    FILE *out = fopen( "/home/alex/CPP/projects/clion/x264/qcif.h264", "w" );
    for ( ;; i_frame++ ) {
        /* 读取输入帧 */
        if ( fread( pic.img.plane[ 0 ], 1, luma_size, in ) != luma_size )
            break;
        if ( fread( pic.img.plane[ 1 ], 1, chroma_size, in ) != chroma_size )
            break;
        if ( fread( pic.img.plane[ 2 ], 1, chroma_size, in ) != chroma_size )
            break;

        pic.i_pts = i_frame;  // PTS，展现时间戳
        // 编码当前帧，nal是编码后的第一个NALU的指针，i_nal是NALU个数。返回编码后NALU的总字节数
        i_frame_size = x264_encoder_encode( h, &amp;nal, &amp;i_nal, &amp;pic, &amp;pic_out );
        if ( i_frame_size &lt; 0 ) goto fail;
            // 将NALU的载荷写入到输出流
        else if ( !fwrite( nal-&gt;p_payload, i_frame_size, 1, out ))goto fail;
    }
    /* 写出延迟帧 */
    while ( x264_encoder_delayed_frames( h )) {
        i_frame_size = x264_encoder_encode( h, &amp;nal, &amp;i_nal, NULL, &amp;pic_out );
        if ( i_frame_size &lt; 0 ) goto fail;
        else if ( i_frame_size ) if ( !fwrite( nal-&gt;p_payload, i_frame_size, 1, out )) goto fail;
    }
    // 关闭编码器
    x264_encoder_close( h );
    // 清理内存
    x264_picture_clean( &amp;pic );
    return 0;

#undef fail
    fail3:
    x264_encoder_close( h );
    fail2:
    x264_picture_clean( &amp;pic );
    fail:
    return -1;
}</pre>
<p>编码后原始的5.7MB的YUV序列产生了88.3KB的H.264 NALU序列，缩小了接近80倍。 以HEX打开输出文件：</p>
<p style="padding-left: 60px;"><span class="monospace">0000 0001 6742 c00b db0b 13a1 0000 0300</span><br /><span class="monospace">0100 0003 0032 8f14 2ae0 0000 0001 68ca</span><br /><span class="monospace">83cb 2000 0001 0605 ffff 66dc 45e9 bde6</span><br /><span class="monospace">d948 b796 2cd8 20d9 23ee ef78 3236 3420 ...</span></p>
<p>可以看到：</p>
<ol>
<li>NALU前缀了4字节的起始码HEX：0000 0001</li>
<li>第1个NALU头为BIN：01100111，此单元是一个SPS</li>
<li>第2个NALU是PPS</li>
<li>第3个NALU是一个典型切片</li>
</ol>
<p>&nbsp;</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/h264-study-note">H.264学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/h264-study-note/feed</wfw:commentRss>
		<slash:comments>5</slash:comments>
		</item>
		<item>
		<title>Spring Boot学习笔记</title>
		<link>https://blog.gmem.cc/spring-boot-study-note</link>
		<comments>https://blog.gmem.cc/spring-boot-study-note#comments</comments>
		<pubDate>Tue, 05 Sep 2017 08:27:38 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Java]]></category>
		<category><![CDATA[Spring]]></category>
		<category><![CDATA[学习笔记]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=15876</guid>
		<description><![CDATA[<p>简介 Spring Boot是Spring的一个子项目，它让创建独立运行（通过java -jar）的、产品级别的Spring应用变得简单： 支持创建独立运行于JVM中的Spring程序 内嵌Tomcat、Jetty或者Undertow，不需要部署War包 简化Maven的POM配置 在任何可能的情况下，自动配置Spring 提供产品级特性，包括性能度量、健康检测、外部化配置 不需要任何代码生成或者XML配置 新特性 2.0 支持Java 9，至少要求Java 8 基于 Spring 5 构建，Spring 的新特性均可以在 Spring Boot <a class="read-more" href="https://blog.gmem.cc/spring-boot-study-note">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/spring-boot-study-note">Spring Boot学习笔记</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>Spring Boot是Spring的一个子项目，它让创建独立运行（通过java -jar）的、产品级别的Spring应用变得简单：</p>
<ol>
<li>支持创建独立运行于JVM中的Spring程序</li>
<li>内嵌Tomcat、Jetty或者Undertow，不需要部署War包</li>
<li>简化Maven的POM配置</li>
<li>在任何可能的情况下，自动配置Spring</li>
<li>提供产品级特性，包括性能度量、健康检测、外部化配置</li>
<li>不需要任何代码生成或者XML配置</li>
</ol>
<div class="blog_h2"><span class="graybg">新特性</span></div>
<div class="blog_h3"><span class="graybg">2.0</span></div>
<ol>
<li>支持Java 9，至少要求Java 8</li>
<li>基于 Spring 5 构建，Spring 的新特性均可以在 Spring Boot 2.0 中使用</li>
<li>为各种组件的响应式编程提供了自动化配置，如：Reactive Spring Data、Reactive Spring Security 等</li>
<li>支持 Spring MVC 的非阻塞式替代方案 WebFlux 以及嵌入式 Netty Server</li>
<li>对 HTTP/2 的支持</li>
<li>更灵活的属性绑定API，不通过<pre class="crayon-plain-tag">@ConfigurationProperties</pre>注解就能实现配置内容读取和使用</li>
<li>Spring Security 整合的简化</li>
<li>Gradle 插件增强，要求Gradle版本4.4+</li>
<li>Starter整合的第三方组件版本升级：
<ol>
<li>Tomcat 升级至 9.0，Servlet版本4.0</li>
<li>Jetty 升级至 9.4，Servlet版本3.1</li>
<li>Flyway 升级至 5</li>
<li>Hibernate 升级至 5.2</li>
<li>Thymeleaf 升级至 3</li>
</ol>
</li>
</ol>
<div class="blog_h1"><span class="graybg">起步</span></div>
<p>本章我们创建一个简单的基于Spring Boot的Web应用。</p>
<div class="blog_h2"><span class="graybg">Maven依赖</span></div>
<p>首先设置父项目：</p>
<pre class="crayon-plain-tag">&lt;parent&gt;
    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
    &lt;artifactId&gt;spring-boot-starter-parent&lt;/artifactId&gt;
    &lt;version&gt;2.1.6.RELEASE&lt;/version&gt;
&lt;/parent&gt; </pre>
<p>然后添加Web的Starter依赖：</p>
<pre class="crayon-plain-tag">&lt;dependency&gt;
    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
    &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;
    &lt;version&gt;2.1.6.RELEASE&lt;/version&gt;
&lt;/dependency&gt;</pre>
<p>只需要这一个依赖就可以了，开发Spring MVC项目所需要的繁杂依赖项不需要手工指定，均使用spring-boot-starter-web的传递依赖即可。</p>
<p>最后配置一下Maven插件，打包为可执行JAR：</p>
<pre class="crayon-plain-tag">&lt;build&gt;
	&lt;plugins&gt;
		&lt;plugin&gt;
			&lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
			&lt;artifactId&gt;spring-boot-maven-plugin&lt;/artifactId&gt;
		&lt;/plugin&gt;
	&lt;/plugins&gt;
&lt;/build&gt; </pre>
<div class="blog_h2"><span class="graybg">HelloWorld</span></div>
<pre class="crayon-plain-tag">package cc.gmem.study.spring;


import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
// 让Spring Boot猜测应该怎么样配置Spring（基于添加的依赖）
@EnableAutoConfiguration
public class SampleController {

    @RequestMapping( "/" )
    @ResponseBody
    String home() {
        return "Hello World!";
    }

    public static void main( String[] args ) throws Exception {
        System.getProperties().put( "server.port", 9090 );
        SpringApplication.run( SampleController.class, args );
    }
}</pre>
<p>运行此程序，然后打开浏览器访问http://localhost:9090，即可访问。</p>
<div class="blog_h1"><span class="graybg">安装</span></div>
<div class="blog_h2"><span class="graybg">基于Maven</span></div>
<pre class="crayon-plain-tag">&lt;project&gt;
    &lt;!-- 
        继承基础配置：
        1、使用JDK 1.6作为默认编译级别
        2、使用UTF-8作为源码的编码方式
    --&gt;
    &lt;parent&gt;
        &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
        &lt;artifactId&gt;spring-boot-starter-parent&lt;/artifactId&gt;
        &lt;version&gt;1.5.6.RELEASE&lt;/version&gt;
    &lt;/parent&gt;
    &lt;!-- 可以定制的配置 --&gt;
    &lt;properties&gt;
        &lt;java.version&gt;1.8&lt;/java.version&gt;
        &lt;!-- 如果有多个main函数入口，需要指定一个start-class才能进行可执行JAR打包 --&gt;
        &lt;start-class&gt;cc.gmem.study.spring.boot.HelloApp&lt;/start-class&gt;
    &lt;/properties&gt;
    &lt;!-- Web应用典型依赖，包括了对Spring MVC和Tomcat的依赖 --&gt;
    &lt;dependencies&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
            &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;
        &lt;/dependency&gt;
    &lt;/dependencies&gt;
 
    &lt;!-- 打包为可执行的JAR --&gt;
    &lt;build&gt;
        &lt;plugins&gt;
            &lt;plugin&gt;
                &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
                &lt;artifactId&gt;spring-boot-maven-plugin&lt;/artifactId&gt;
            &lt;/plugin&gt;
        &lt;/plugins&gt;
    &lt;/build&gt;
 
&lt;/project&gt;</pre>
<p>如果parent构件已经存在，可以这样引入基础配置：</p>
<pre class="crayon-plain-tag">&lt;dependencyManagement&gt;
     &lt;dependencies&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
            &lt;artifactId&gt;spring-boot-dependencies&lt;/artifactId&gt;
            &lt;version&gt;1.5.6.RELEASE&lt;/version&gt;
            &lt;type&gt;pom&lt;/type&gt;
            &lt;scope&gt;import&lt;/scope&gt;
        &lt;/dependency&gt;
    &lt;/dependencies&gt;
&lt;/dependencyManagement&gt;</pre>
<div class="blog_h2"><span class="graybg">基于Gradle</span></div>
<pre class="crayon-plain-tag">plugins {
    id 'org.springframework.boot' version '1.5.6.RELEASE'
    id 'java'
}


jar {
    baseName = 'myproject'
    version =  '0.0.1-SNAPSHOT'
}

repositories {
    jcenter()
}

dependencies {
    compile("org.springframework.boot:spring-boot-starter-web")
    testCompile("org.springframework.boot:spring-boot-starter-test")
}</pre>
<div class="blog_h2"><span class="graybg">Starters</span></div>
<p>所谓Starter，就是一个依赖描述符，你可以包含它们，避免手工逐个配置依赖项。上面我们已经使用了两种Starter。常用的Starter如下表：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 35%; text-align: center;">Starter</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>spring-boot-starter</td>
<td>核心Starter，包含了自动配置支持、日志、YAML</td>
</tr>
<tr>
<td>spring-boot-starter-activemq</td>
<td>支持基于ActiveMQ的JMS应用</td>
</tr>
<tr>
<td>spring-boot-starter-amqp</td>
<td>支持Spring AMQP、Rabbit MQ</td>
</tr>
<tr>
<td>spring-boot-starter-aop</td>
<td>支持Spring AOP和AspectJ</td>
</tr>
<tr>
<td>spring-boot-starter-cache</td>
<td>支持Spring缓存机制</td>
</tr>
<tr>
<td>spring-boot-starter-data-jpa</td>
<td>支持JPA + Hibernate</td>
</tr>
<tr>
<td>spring-boot-starter-data-mongodb</td>
<td>支持MongoDB</td>
</tr>
<tr>
<td>spring-boot-starter-data-redis</td>
<td>支持Redis</td>
</tr>
<tr>
<td>spring-boot-starter-jdbc</td>
<td>支持基于Tomcat的JDBC连接池</td>
</tr>
<tr>
<td>spring-boot-starter-jersey</td>
<td>基于JAX-RS / Jersey构建RESTful的Web应用</td>
</tr>
<tr>
<td>spring-boot-starter-test</td>
<td>支持JUnit、Hamcrest、Mockito</td>
</tr>
<tr>
<td>spring-boot-starter-web</td>
<td>支持Web应用，包括RESTful，使用Tomcat作为默认内嵌容器</td>
</tr>
<tr>
<td>spring-boot-starter-web-services</td>
<td>支持Spring WebService</td>
</tr>
<tr>
<td>spring-boot-starter-websocket</td>
<td>支持WebSocket</td>
</tr>
<tr>
<td>spring-boot-starter-jetty<br />spring-boot-starter-tomcat<br />spring-boot-starter-undertow</td>
<td>内嵌容器依赖</td>
</tr>
<tr>
<td>spring-boot-starter-log4j2<br />spring-boot-starter-logging</td>
<td>日志依赖</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">使用Spring Boot</span></div>
<div class="blog_h2"><span class="graybg">配置类</span></div>
<p>尽管你可以通过<pre class="crayon-plain-tag">SpringApplication.run()</pre>传递XML源，但是Spring Boot倾向于使用基于Java的配置。 即主要配置源应该是一个注解了<pre class="crayon-plain-tag">@Configuration</pre>的Java类，此类通常定义了main方法。</p>
<div class="blog_h3"><span class="graybg">额外配置类</span></div>
<p>你可以使用<pre class="crayon-plain-tag">@Import</pre>注解引入额外的配置类。</p>
<p>你也可以使用<pre class="crayon-plain-tag">@ComponentScan</pre>自动扫描任何Spring组件，包括@ComponentScan本身。</p>
<p>如果要引入额外的XML配置，可以使用<pre class="crayon-plain-tag">@ImportResource</pre>注解。</p>
<div class="blog_h2"><span class="graybg">自动配置</span></div>
<p>Spring Boot的自动配置机制，尝试根据当前项目的依赖，自动判断如何配置Spring上下文、运行容器、数据库。例如，如果HSQLDB位于类路径下，Spring Boot会自动配置一个内存数据库。</p>
<p>要启用自动配置，可以在主配置类上添加<pre class="crayon-plain-tag">@EnableAutoConfiguration</pre>或者<pre class="crayon-plain-tag">@SpringBootApplication</pre>注解。</p>
<p>如果要查看哪些自动配置被启用，为何被启用，可以为应用程序传入参数<pre class="crayon-plain-tag">--debug</pre>。</p>
<p>你可以明确的禁用某种自动配置：</p>
<pre class="crayon-plain-tag">@Configuration
@EnableAutoConfiguration(exclude={DataSourceAutoConfiguration.class})
public class Configuration {
}</pre>
<p>注意：自动配置可以<span style="background-color: #c0c0c0;">被渐进的替代</span>。例如，当你手工配置了数据源后，Spring Boot就不会自动配置HSQLDB。</p>
<div class="blog_h3"><span class="graybg">@SpringBootApplication</span></div>
<p>此注解等价于@Configuration, @EnableAutoConfiguration,@ComponentScan的组合。其中@ComponentScan为：</p>
<pre class="crayon-plain-tag">@ComponentScan(excludeFilters = {
		@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })</pre>
<p>由于没有为@ComponentScan指定basePackage，<span style="background-color: #c0c0c0;">因此默认扫描当前类所在包及其子包</span>。</p>
<div class="blog_h2"><span class="graybg">Bean和注入</span></div>
<p>可以通过@ComponentScan来查找Bean，或者在配置类的方法中使用@Bean。@Component, @Service, @Repository, @Controller等注解都是支持的。</p>
<p>要为Bean注入字段，可以使用@Autowired,@Inject等注解。</p>
<div class="blog_h2"><span class="graybg">运行</span></div>
<p>很多IDE，包括Eclipse、IDEA都提供了对Spring Boot应用程序的支持。</p>
<p>通过命令行运行的示例：</p>
<pre class="crayon-plain-tag">java -Xdebug -Xrunjdwp:server=y,transport=dt_socket,address=8000,suspend=n -jar boot-study.jar</pre>
<p>通过Maven运行的示例：</p>
<pre class="crayon-plain-tag">export MAVEN_OPTS=-Xmx1024m -XX:MaxPermSize=128M
mvn spring-boot:run</pre>
<p>通过Gradle运行的示例：</p>
<pre class="crayon-plain-tag">gradle bootRun</pre>
<div class="blog_h2"><span class="graybg">热部署</span></div>
<p>Spring Boot仅仅是简单的Java应用程序，JVM的热交换机制直接可以使用。但是JVM热交换有所限制，更加完整的解决方案是JRebel或者Spring Load。模块spring-boot-devtools也提供了快速的应用重启机制。</p>
<div class="blog_h3"><span class="graybg">静态内容重新载入</span></div>
<p>有多种实现方式，推荐使用spring-boot-devtools模块，因为它提供了额外的开发时特性——快速重启、LiveReload。DevTools的工作方式是监控类路径变化，这意味着你需要触发项目构建，在IDEA中可以调用Make Project触发（或者开启自动构建）。<span style="background-color: #c0c0c0;">静态内容的变化不会导致应用重启</span>，但是会导致LiveReload。</p>
<div class="blog_h3"><span class="graybg">模板重新载入</span></div>
<p>Spring Boot支持的模板技术大部分支持禁用缓存的功能，这样模板被修改后可以立即被感知到。使用spring-boot-devtools的情况下，模板自动重新载入。</p>
<div class="blog_h3"><span class="graybg">Java类重新载入</span></div>
<p>大部分IDE支持代码热替换，因此你对类进行非结构性的改变时，构建后Java类会自动更新。</p>
<p>Spring Loaded允许方法签名改变后的类仍然能够重新载入。</p>
<div class="blog_h3"><span class="graybg">快速重启</span></div>
<p>spring-boot-devtools提供了自动的重启功能，其速度比JRebel、Spring Loaded慢，但是比冷启动快的多。</p>
<p>spring-boot-devtools的快速重启功能依赖于ClassLoader，打包好的第三方库，使用Base加载器加载，而正在开发的那些类则使用restart加载器加载。当快速重启发生后，之前的restart加载器被丢弃，新的restart加载器被创建。</p>
<div class="blog_h2"><span class="graybg">DevTools</span></div>
<p>此模块提供一些开发时特性，要启用DevTools，引入：</p>
<pre class="crayon-plain-tag">&lt;dependency&gt;
    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
    &lt;artifactId&gt;spring-boot-devtools&lt;/artifactId&gt;
    &lt;version&gt;1.5.6.RELEASE&lt;/version&gt;
    &lt;optional&gt;true&lt;/optional&gt;
&lt;/dependency&gt;</pre>
<p>DevTools在生产环境下会自动禁用，生产环境的判断依据是：</p>
<ol>
<li>以java -jar启动Spring Boot应用</li>
<li>或者，使用了特殊的ClassLoader </li>
</ol>
<div class="blog_h3"><span class="graybg">默认属性</span></div>
<p>模板引擎具有缓存功能、Spring MVC也在HTTP头中添加了缓存字段。DevTools通过配置属性，禁用了这些功能。</p>
<div class="blog_h3"><span class="graybg">自动重启</span></div>
<p>默认情况下，使用了DevTools的应用会在类路径发生变化时自动重启，注意模板、静态资源不会触发重启。具体来说，下列位置的资源不会触发重启：</p>
<p style="padding-left: 30px;">/META-INF/maven, /META-INF/resources ,/resources ,/static ,/public和/templates</p>
<p>如果需要显式指定那些资源不会触发重启，配置下面的属性：</p>
<pre class="crayon-plain-tag"># 自己指定不触发的资源
spring.devtools.restart.exclude=static/**,public/**
# 在默认不触发资源目录列表的基础上，额外增加
spring.devtools.restart.additional-exclude=</pre>
<p>如果需要额外指定会触发重启的资源，配置下面的属性：</p>
<pre class="crayon-plain-tag">spring.devtools.restart.additional-exclude=</pre>
<p>如果你使用某种持续的自动编译、产生输出的IDE，并且期望在这些输出发生变化时，触发重启，配置下面的属性：</p>
<pre class="crayon-plain-tag">spring.devtools.restart.trigger-file= </pre>
<p>要禁用DevTools的重启功能，配置下面的属性：</p>
<pre class="crayon-plain-tag">spring.devtools.restart.enabled=false</pre>
<div class="blog_h3"><span class="graybg">restart加载器</span></div>
<p>配置下面的属性以确定哪些JAR包由restart加载器加载：</p>
<pre class="crayon-plain-tag"># 下面的JAR由Base加载器加载
restart.exclude.companycommonlibs=/mycorp-common-[\\w-]+\.jar
# 下面的JAR由restart加载器加载
restart.include.projectcommon=/mycorp-myproj-[\\w-]+\.jar</pre>
<div class="blog_h3"><span class="graybg">LiveReload</span></div>
<p>DevTools包含了一个内嵌的LiveReload服务器，可以用来在资源发生变化时触发浏览器刷新。各大浏览器均有<a href="http://livereload.com/extensions/">LiveReload</a>扩展。</p>
<p>如果要禁用LiveReload服务器，配置：</p>
<pre class="crayon-plain-tag">spring.devtools.livereload.enabled=false </pre>
<div class="blog_h1"><span class="graybg">Spring Boot特性</span></div>
<div class="blog_h2"><span class="graybg">SpringApplication</span></div>
<p>此类提供了一些便捷方法，用于Spring应用的自举。最简单的用法：</p>
<pre class="crayon-plain-tag">SpringApplication.run(MyConfiguration.class, args);</pre>
<p>可以使用链式调用进行定制化：</p>
<pre class="crayon-plain-tag">new SpringApplicationBuilder()
        .sources(Parent.class)
        .child(Application.class)
        .bannerMode(Banner.Mode.OFF)
        .run(args); </pre>
<div class="blog_h3"><span class="graybg">启动失败</span></div>
<p>启动后，控制台默认打印INFO级别的日志。</p>
<p>如果应用自举失败，已经注册的FailureAnalyzer会打印相关的信息，Spring Boot提供了很多FailureAnalyzer实现。 </p>
<p>如果FailureAnalyzer没有提供有价值的信息，你可以设置应用程序参数<pre class="crayon-plain-tag">--debug</pre>或者设置org.springframework.boot.autoconfigure.logging.AutoConfigurationReportLoggingInitializer的日志级别。</p>
<div class="blog_h3"><span class="graybg">定制Banner</span></div>
<p>你可以在类路径下放置一个banner.txt文件，或者设置banner.location来指定任意文件。你还可以在类路径下添加banner.png|gif|jpg等图片，这些图片会被转换为ASCII（透明的PNG背景被转换为黑色），打印在文本Banner之前。</p>
<div class="blog_h3"><span class="graybg">应用事件</span></div>
<p>除了ContextRefreshedEvent，SpringApplication会发布很多额外的事件：</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>ApplicationStartingEvent</td>
<td>开始启动时发布，此时仅仅注册了listeners、initializers，尚未进行其它处理</td>
</tr>
<tr>
<td>ApplicationEnvironmentPreparedEvent</td>
<td>Spring context需要的Environment准备好之后调用，此时Context尚未创建</td>
</tr>
<tr>
<td>ApplicationPreparedEvent</td>
<td>Bean定义已经全部加载，即将刷新Context</td>
</tr>
<tr>
<td>ApplicationReadyEvent</td>
<td>Context刷新完毕，相关的回调已经被调用</td>
</tr>
<tr>
<td>ApplicationFailedEvent</td>
<td>启动失败后发布</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">Web环境</span></div>
<p>SpringApplication会自动尝试创建合适的ApplicationContext实现类。默认的：</p>
<ol>
<li>如果当前应用是Web应用，创建AnnotationConfigEmbeddedWebApplicationContext</li>
<li>否则创建AnnotationConfigApplicationContext</li>
</ol>
<p>你可以调用setWebEnvironment()来强制指定是否Web应用。</p>
<div class="blog_h3"><span class="graybg">ApplicationArguments</span></div>
<p>你可以注入此类型，以访问传递给SpringApplication.run的参数。</p>
<div class="blog_h2"><span class="graybg">配置外部化</span></div>
<p>Environment对象是配置信息的容器。</p>
<p>Spring Boot允许你把配置信息外部化，这样同一套代码就可以很容易的在不同环境下运行。</p>
<p>你可以使用YAML 、环境变量、命令行参数来指定配置。</p>
<p>使用<pre class="crayon-plain-tag">@Value</pre>注解可以直接注入配置参数，也可以通过Environment来访问，或者通过@ConfigurationProperties绑定到结构化对象。例如：</p>
<pre class="crayon-plain-tag">@Component
public class Bean {
    @Value("${name}")
    private String name;

}</pre>
<p>如果类路径下的application.properties文件中有name这个键，或者命令行指定了--name参数，则会被注入到上述Bean的属性中。</p>
<div class="blog_h3"><span class="graybg">配置优先级</span></div>
<p>Spring使用PropertySource来定位配置属性，它会从以下位置寻找（优先级从高到低）：</p>
<ol>
<li>当使用DevTools时，DevTools全局设置，位于~/.spring-boot-devtools.properties</li>
<li>位于测试用例上的@TestPropertySource注解</li>
<li>位于测试用例上的@SpringBootTest#properties注解</li>
<li>命令行参数。例如 --server.port=9090</li>
<li>来自SPRING_APPLICATION_JSON中的属性，这是内联在环境变量或者系统属性中的一个JSON</li>
<li>ServletConfig初始化参数</li>
<li>ServletContext初始化参数</li>
<li>来自java:comp/env的JNDI属性</li>
<li>系统属性System.getProperties()</li>
<li>OS环境变量</li>
<li>RandomValuePropertySource中的random.*属性</li>
<li>位于应用JAR包外部的application-{profile}.properties文件（或者YAML），针对特定profile</li>
<li>位于应用JAR包内部的application-{profile}.properties文件（或者YAML），针对特定profile</li>
<li>位于应用JAR包外部的application.properties文件（或者YAML）</li>
<li>位于应用JAR包内部的application.properties文件（或者YAML）</li>
<li>@Configuration类上的@PropertySource注解</li>
<li>缺省属性，来自SpringApplication.setDefaultProperties</li>
</ol>
<p>其中application.properties的搜索位置包括（优先级从高到低）：</p>
<ol>
<li>当前目录的/config子目录</li>
<li>当前目录</li>
<li>类路径下的/config包</li>
<li>类路径根目录</li>
</ol>
<p>如果你不想使用上述默认属性文件名称，可以指定命令行参数：</p>
<pre class="crayon-plain-tag">--spring.config.location=classpath:/default.properties,classpath:/override.properties</pre>
<div class="blog_h3"><span class="graybg">针对特定Profile的属性 </span></div>
<p>你可以针对不同运行环境，为Spring Boot应用定义多个Profile。Environment包含了若干内置Profile，且默认激活（如果你没有激活其它）default这个Profile。</p>
<p>针对特定Profile的属性定义在application-{profile}.properties文件中。</p>
<p>通过设置spring.profiles.active属性，可以改变当前活动Profile。例如：</p>
<pre class="crayon-plain-tag">spring.profiles.active=dev,hsqldb</pre>
<div class="blog_h3"><span class="graybg">属性占位符</span></div>
<p>在属性文件中，你可以通过占位符引用先前定义的属性：</p>
<pre class="crayon-plain-tag">app.name=MyApp
app.description=${app.name} is a Spring Boot application</pre>
<div class="blog_h3"><span class="graybg">Maven变量展开</span></div>
<p>你可以在属性文件中引用Maven项目属性：</p>
<pre class="crayon-plain-tag">spring.profiles.active=@environment@
app.encoding=@project.build.sourceEncoding@
app.java.version=@java.version@</pre>
<p>配合Maven Profile：</p>
<pre class="crayon-plain-tag">&lt;profile&gt;
    &lt;id&gt;pro&lt;/id&gt;
    &lt;properties&gt;
        &lt;environment&gt;pro&lt;/environment&gt;
    &lt;/properties&gt;
&lt;/profile&gt;</pre>
<p>则运行<pre class="crayon-plain-tag">mvn package -Ppro</pre>后属性文件中的@environment@自动替换为pro</p>
<div class="blog_h2"><span class="graybg">日志</span></div>
<p>Spring Boot支持使用Java Util Logging、Log4J2、Logback这几种日志实现。如果你使用Starters，则默认使用Logback。要定制日志配置，使用以下配置文件：</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>Logback</td>
<td>logback-spring.xml, logback-spring.groovy, logback.xml 或者logback.groovy</td>
</tr>
<tr>
<td>Log4j2</td>
<td>log4j2-spring.xml 或者 log4j2.xml</td>
</tr>
<tr>
<td>Java Util Logging</td>
<td>logging.properties</td>
</tr>
</tbody>
</table>
<p>你可以使用Slf4J提供的日志API。</p>
<div class="blog_h3"><span class="graybg">日志配置</span></div>
<p>可以在application.properties中配置对应的属性：</p>
<pre class="crayon-plain-tag"># 设置根日志级别
logging.level.root=DEBUG
# 设置日志级别
logging.level.cc.gmem=DEBUG
# 配置基于文件的日志
logging.file=${java.io.tmpdir}/application.log
# 配置日志输出格式
logging.pattern.console= "%d{yyyy-MM-dd HH:mm:ss} - %msg%n"
logging.pattern.file= "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"</pre>
<div class="blog_h2"><span class="graybg">Web应用支持</span></div>
<p>Spring MVC框架允许你创建特殊的@Controller、@RestController以处理入站的HTTP请求，具体处理逻辑由注解了@RequestMapping的方法负责。</p>
<div class="blog_h3"><span class="graybg">自动配置</span></div>
<p>执行以下自动配置：</p>
<ol>
<li>包含ContentNegotiatingViewResolver 、BeanNameViewResolver 这两个Bean</li>
<li>支持包括WebJars在内的静态资源</li>
<li>自动注册Converter、GenericConverter、Formatter</li>
<li>支持HttpMessageConverters</li>
<li>自动注册MessageCodesResolver</li>
<li>支持静态index.html</li>
<li>支持定制Favicon</li>
<li>自动使用ConfigurableWebBindingInitializer </li>
</ol>
<p>如果你仅仅想添加额外的拦截器、formatters、view controllers，可以在WebMvcConfigurerAdapter的子类上加@Configuration注解，但是不使用@EnableWebMvc注解。</p>
<p>如果你希望使用自己实现的RequestMappingHandlerMapping、RequestMappingHandlerAdapter 、ExceptionHandlerExceptionResolver ，声明一个WebMvcRegistrationsAdapter，提供这些组件。</p>
<p>如果你希望对Spring MVC进行完全的控制，在主配置类上添加@EnableWebMvc注解。</p>
<div class="blog_h3"><span class="graybg">HttpMessageConverters</span></div>
<p>如果需要自定义消息转换器，声明HttpMessageConverters类型的Bean即可：</p>
<pre class="crayon-plain-tag">import org.springframework.boot.autoconfigure.web.HttpMessageConverters;
// ...
@Bean
public HttpMessageConverters customConverters() {
    HttpMessageConverter&lt;?&gt; additional = ...
    HttpMessageConverter&lt;?&gt; another = ...
    return new HttpMessageConverters(additional, another);
}</pre>
<div class="blog_h3"><span class="graybg">定制JSON编解码器</span></div>
<p>当基于JacksonJSON来编解码JSON时，你可能需要定制JsonSerializer、JsonDeserializer。</p>
<p>结合Spring Boot使用时，如果要注册定制的编解码器，需要在编解码器类（或者其外部类）上声明注解：</p>
<pre class="crayon-plain-tag">@JsonComponent
public class JsonCodecs {
    public static class Serializer extends JsonSerializer {}
    public static class Deserializer extends JsonDeserializer {}
}</pre>
<div class="blog_h3"><span class="graybg">静态内容</span></div>
<p>默认情况下，Spring Boot从类路径、ServletContext的根目录下寻找：</p>
<ol>
<li>/static</li>
<li>/public</li>
<li>/resources</li>
<li>/META-INF/resources </li>
</ol>
<p>这几个目录，并将其作为静态内容看待，你可以设置spring.resources.static-locations以修改静态目录。 这些静态内容默认映射URL为/**，你可以设置下面的属性以修改：</p>
<pre class="crayon-plain-tag"># 重新定位URL
spring.mvc.static-path-pattern=/resources/** </pre>
<p>除了以目录方式组织的静态内容，Spring Boot还支持<a href="http://www.webjars.org/">WebJars</a>。任何/webjars/**目录下的JAR，如果符合WebJar格式，将会被作为静态资源使用。</p>
<div class="blog_h3"><span class="graybg">Favicon</span></div>
<p>你可以在静态文件根目录中放置一个favicon.ico文件。</p>
<div class="blog_h3"><span class="graybg">模板引擎支持</span></div>
<p>Spring Boot支持<a href="http://freemarker.org/docs/">FreeMarker</a>、<a href="http://docs.groovy-lang.org/docs/next/html/documentation/template-engines.html#_the_markuptemplateengine">Groovy</a>、<a href="http://www.thymeleaf.org/">Thymeleaf</a>、<a href="https://mustache.github.io/">Mustache</a>这几种模板引擎的自动配置。</p>
<p>JSP也可以使用。</p>
<div class="blog_h3"><span class="graybg">错误处理</span></div>
<p>Spring Boot默认提供了一个/error映射，来处理所有错误，并且注册为Servlet容器的全局错误页面。此错误页面的默认行为：</p>
<ol>
<li>对于浏览器客户端：以HTML格式显示一个页面。要定制此页面，可以配置一个映射到/error的View</li>
<li>对于机器客户端：返回一个JSON响应，包含错误码、详细错误信息。此JSON响应对应的Java类为ErrorAttributes，你可以注册一个ErrorAttributes的子类型Bean，改变响应内容<br />你还可以针对特定的控制器、异常类型，定制此JSON：<br />
<pre class="crayon-plain-tag">// 针对和FooController在同一包中的所有控制器
@ControllerAdvice(basePackageClasses = FooController.class)
public class FooControllerAdvice extends ResponseEntityExceptionHandler {
    // 针对YourException
    @ExceptionHandler(YourException.class)
    @ResponseBody
    ResponseEntity&lt;?&gt; handleControllerException(HttpServletRequest request, Throwable ex) {
        HttpStatus status = getStatus(request);
        // CustomErrorType的JSON展示会代替ErrorAttributes写到响应中
        return new ResponseEntity&lt;&gt;(new CustomErrorType(status.value(), ex.getMessage()), status);
    }

    private HttpStatus getStatus(HttpServletRequest request) {
        Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code");
        if (statusCode == null) {
            return HttpStatus.INTERNAL_SERVER_ERROR;Media Source
        }
        return HttpStatus.valueOf(statusCode);
    }

}Environment</pre>
</li>
</ol>
<p>要完成替换上述默认行为，可以注册一个ErrorController类型的Bean。</p>
<div class="blog_h3"><span class="graybg">定制错误页</span></div>
<p>如果希望针对不同的HTTP状态码定制错误页面，添加一个文件到/error目录下。错误页面可以是静态HTML或者某种被支持的模板。 </p>
<p>例如，要对404定制错误页面，可以创建src/main/resources/public/error/404.html。要对所有5xx定制错误页面，可以在前面的目录中创建5xx.ftl。</p>
<div class="blog_h3"><span class="graybg">CORS支持</span></div>
<p>跨源资源共享（Cross-origin resource sharing）是被浏览器广泛实现的W3C规范，允许你指定一个灵活的策略，允许哪些跨Domain请求被发送。使用CORS可以避免使用IFRAME、JSONP等变通技术。</p>
<p>Spring MVC 4.2开始对CORS进行了开箱即用的支持。你可以在控制器方法上使用@CrossOrigin注解。或者配置全局性的CORS策略：</p>
<pre class="crayon-plain-tag">@Configuration
public class MyConfiguration {
    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurerAdapter() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/api/**");
            }
        };
    }
}</pre>
<div class="blog_h3"><span class="graybg">JAX-RS和Jersey</span></div>
<p>如果你喜欢基于JAX-RS编程模型创建REST端点，你可以使用Jersey、 Apache CXF等框架。要集成到Spring你只需要把它们的Servlet、Filter注册为@Bean。</p>
<p>Jersey 2.x对Spring集成提供了支持，Spring Boot可以进行自动配置，你需要依赖spring-boot-starter-jersey这个Starter并且注册ResourceConfig类型的Bean：</p>
<pre class="crayon-plain-tag">@Component
@ApplicationPath("/api")  // 默认情况下Jersey Servlet被映射到/*，此注解用于定制映射路径
public class JerseyConfig extends ResourceConfig {
    public JerseyConfig() {
        register(Endpoint.class);
    }
}
// 所有注册的Endpoint，必须是@Component，且包含HTTP资源注解（例如GET)
@Component
@Path("/hello")
public class Endpoint {
    @GET
    public String message() {
        return "Hello";
    }
}</pre>
<p>要进行更定制化的配置，实现任意数量的ResourceConfigCustomizer类型的Bean。</p>
<p>默认情况下，Jersey在一个ServletRegistrationBean类型的@Bean中初始化其Servlet。此Servlet默认是延迟加载的，你可以配置属性spring.jersey.servlet.load-on-startup让其立即加载。 你也可以配置spring.jersey.type=filter，这样一个ServletFilter会代替上述Servlet。</p>
<div class="blog_h3"><span class="graybg">JAX-RS和CXF</span></div>
<p>添加Maven依赖：</p>
<pre class="crayon-plain-tag">&lt;dependencies&gt;
  &lt;dependency&gt;
    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
    &lt;artifactId&gt;spring-boot-starter&lt;/artifactId&gt;
  &lt;/dependency&gt;
  &lt;dependency&gt;
    &lt;groupId&gt;org.apache.cxf&lt;/groupId&gt;
    &lt;artifactId&gt;cxf-spring-boot-starter-jaxrs&lt;/artifactId&gt;
    &lt;version&gt;3.3.1&lt;/version&gt;
  &lt;/dependency&gt;
  &lt;dependency&gt;
    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
    &lt;artifactId&gt;spring-boot-starter-test&lt;/artifactId&gt;
    &lt;scope&gt;test&lt;/scope&gt;
  &lt;/dependency&gt;
  &lt;!-- 对JSON的支持 --&gt;
  &lt;dependency&gt;
    &lt;groupId&gt;org.codehaus.jackson&lt;/groupId&gt;
    &lt;artifactId&gt;jackson-jaxrs&lt;/artifactId&gt;
    &lt;version&gt;1.9.13&lt;/version&gt;
  &lt;/dependency&gt;
  &lt;dependency&gt;
    &lt;groupId&gt;org.codehaus.jackson&lt;/groupId&gt;
    &lt;artifactId&gt;jackson-xc&lt;/artifactId&gt;
    &lt;version&gt;1.9.13&lt;/version&gt;
  &lt;/dependency&gt; 
&lt;/dependencies&gt;</pre>
<p>RESTful资源接口示例，一般直接将Service层作为RESTful资源的接口：</p>
<pre class="crayon-plain-tag">import java.util.Collection;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

import com.javacodegeeks.examples.jaxrs.model.Student;

// 将此类标识为根资源
@Path("students")
// 指定请求、响应的MIME类型
@Consumes("application/json;charset=UTF-8")
@Produces(MediaType.APPLICATION_JSON)
public interface StudentService {
  
  // 动词，此外支持@POST, @UPDATE，@DELETE
  @GET
  public Collection&lt;Student&gt; getAllStudents();
  
  @Path("{id}")
  @GET
  // 返回值类型javax.ws.rs.core.Response，是HTTP响应的抽象
  public Response getById(@PathParam("id") Long id);

}</pre>
<p>在Spring属性中配置CXF：</p>
<pre class="crayon-plain-tag"># 默认/services
cxf.path=/studentservice
# 让CXF自动扫描带有JAX-RS注解的类
cxf.jaxrs.classes-scan=true
cxf.jaxrs.classes-scan-packages=cc.gmem.springboot.jaxrs</pre>
<p>现在你可以通过URL：http://localhost:8080/studentservice/students来访问 StudentService暴露的RESTful服务了。</p>
<div class="blog_h3"><span class="graybg">内嵌Servlet容器</span></div>
<p>Spring Boot支持内嵌的Tomcat、Jetty或者Undertow。通过合适的Starter你就可以得到自动化的配置。内嵌服务器默认监听端口8080。</p>
<p>当使用内嵌Servlet容器时，标准的Servlet组件（Servlet、Filter、Listener）既可以注册为Spring Beans，也可以自动被扫描。要启用自动扫描，使用@ServletComponentScan注解，@WebServlet、@WebFilter、@WebListener可以被自动扫描。</p>
<p>如果应用中仅仅有一个Servlet，它会默认被映射到 /，如果有多个Servlet，其Bean名称被作为URL前缀。Filter默认映射到/*。</p>
<p>如果自动化配置不能满足需要，你可以实现自己的ServletRegistrationBean、FilterRegistrationBean或者ServletListenerRegistrationBean。</p>
<p>要定制Servlet容器的运行参数，可以使用配置属性，通常定义在application.properties文件中。通用的配置属性包括：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 30%; text-align: center;">Servlet容器属性</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>server.port</td>
<td>监听端口</td>
</tr>
<tr>
<td>server.address</td>
<td>监听地址</td>
</tr>
<tr>
<td>server.ssl</td>
<td>
<p>SSL相关配置，示例：</p>
<pre class="crayon-plain-tag"># 密钥数据库的位置
server.ssl.key-store:classpath:keystore.jks
# 密钥数据库的密码
server.ssl.key-store-password:kurento
# 密钥数据库的类型
server.ssl.keyStoreType:JKS
# 使用的密钥
server.ssl.keyAlias:kurento-selfsigned</pre>
</td>
</tr>
<tr>
<td>server.session.persistence</td>
<td>是否进行会话的持久化</td>
</tr>
<tr>
<td>server.session.timeout</td>
<td>会话过期时间</td>
</tr>
<tr>
<td>server.session.store-dir</td>
<td>会话存放位置</td>
</tr>
<tr>
<td>server.session.cookie.*</td>
<td>Cookie相关配置</td>
</tr>
<tr>
<td>server.error.path</td>
<td>错误页面配置</td>
</tr>
<tr>
<td>server.tomcat<br />server.undertow</td>
<td>容器特定的配置属性，<a href="https://github.com/spring-projects/spring-boot/blob/v1.5.6.RELEASE/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java">这个页面</a>包含所有可用属性</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">EmbeddedWebApplicationContext</span></div>
<p>这是一种特化的WebApplicationContext，在自举时它会寻找一个EmbeddedServletContainerFactory实现类，通常是TomcatEmbeddedServletContainerFactory、JettyEmbeddedServletContainerFactory或者UndertowEmbeddedServletContainerFactory，以创建Servlet容器。
<div class="blog_h2"><span class="graybg">WebSockets支持</span></div>
<p>Spring Boot支持基于内嵌容器的自动化WebSockets配置，包含对spring-boot-starter-websocket的依赖即可。 </p>
<div class="blog_h2"><span class="graybg">任务调度支持</span></div>
<p>要启用任务调度支持，只需要在应用程序入口添加<pre class="crayon-plain-tag">@EnableScheduling</pre>注解即可，之后你就可以在任何服务的方法上使用注解了：</p>
<pre class="crayon-plain-tag">@EnableScheduling
public class App {}


// 每5秒调度执行一次
@Scheduled( fixedRate = 5000 )
@Scheduled( cron = "*/5 * * * * * *" )
// 每次执行完毕后，延迟5秒再次执行
@Scheduled( fixedDelay = 5000 )
public synchronized void cleanup() {
}</pre>
<div class="blog_h2"><span class="graybg">缓存支持</span></div>
<p>引入依赖：</p>
<pre class="crayon-plain-tag">&lt;dependency&gt;
    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
    &lt;artifactId&gt;spring-boot-starter-cache&lt;/artifactId&gt;
&lt;/dependency&gt;</pre>
<p>在主程序上使用注解来启用缓存支持：</p>
<pre class="crayon-plain-tag">import org.springframework.cache.annotation.EnableCaching;

@SpringBootApplication
@EnableCaching
public class CacheApplication {} </pre>
<div class="blog_h1"><span class="graybg">常用样例</span></div>
<div class="blog_h2"><span class="graybg">JPA和缓存</span></div>
<div class="blog_h3"><span class="graybg">POM配置</span></div>
<pre class="crayon-plain-tag">&lt;?xml version="1.0" encoding="UTF-8"?&gt;
&lt;project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"&gt;
    &lt;dependencies&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;mysql&lt;/groupId&gt;
            &lt;artifactId&gt;mysql-connector-java&lt;/artifactId&gt;
            &lt;version&gt;6.0.6&lt;/version&gt;
        &lt;/dependency&gt;

        &lt;!-- 使用Log4j2 --&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
            &lt;artifactId&gt;spring-boot-starter&lt;/artifactId&gt;
            &lt;version&gt;RELEASE&lt;/version&gt;
            &lt;exclusions&gt;
                &lt;exclusion&gt;
                    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
                    &lt;artifactId&gt;spring-boot-starter-logging&lt;/artifactId&gt;
                &lt;/exclusion&gt;
            &lt;/exclusions&gt;
        &lt;/dependency&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
            &lt;artifactId&gt;spring-boot-starter-log4j2&lt;/artifactId&gt;
            &lt;version&gt;RELEASE&lt;/version&gt;
        &lt;/dependency&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;com.fasterxml.jackson.dataformat&lt;/groupId&gt;
            &lt;artifactId&gt;jackson-dataformat-yaml&lt;/artifactId&gt;
            &lt;version&gt;2.5.0&lt;/version&gt;
        &lt;/dependency&gt;

        &lt;!-- Spring Boot 缓存支持 --&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
            &lt;artifactId&gt;spring-boot-starter-cache&lt;/artifactId&gt;
            &lt;version&gt;RELEASE&lt;/version&gt;
        &lt;/dependency&gt;
        &lt;!-- Spring Boot data-jpa支持 --&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
            &lt;artifactId&gt;spring-boot-starter-data-jpa&lt;/artifactId&gt;
            &lt;version&gt;RELEASE&lt;/version&gt;
        &lt;/dependency&gt;
    &lt;/dependencies&gt;
&lt;/project&gt;</pre>
<div class="blog_h3"><span class="graybg">application.properties</span></div>
<pre class="crayon-plain-tag">spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=none

spring.datasource.url=jdbc:mysql://10.255.223.241:3306/digital
spring.datasource.username=digital
spring.datasource.password=digital
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver</pre>
<div class="blog_h3"><span class="graybg">实体类</span></div>
<pre class="crayon-plain-tag">package cc.gmem.study.sb.entity;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table( name = "media" )
public class Media {

    @Id
    private Long mediaId;

    @Column
    private Long saleId;

    @Column
    private String title;

    @Column( name = "is_bn" )
    private String isbn;

    @Column( name = "shelf_status" )
    private boolean onShelf;
}</pre>
<div class="blog_h3"><span class="graybg">存储层</span></div>
<pre class="crayon-plain-tag">package cc.gmem.study.sb.repo;


import cc.gmem.study.sb.entity.Media;
import org.springframework.cache.annotation.*;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

/**
 * JpaRepository&lt;EntityClass,IdType&gt;，继承了：
 *      PagingAndSortingRepository 支持分页和排序
 *          CrudRepository 支持常规增删改查
 *              Repository 标识接口
 */

/* 类级别的缓存公共设置 */
@CacheConfig( cacheNames = "media" )
public interface MediaRepository extends JpaRepository&lt;Media, Long&gt; {

    // 指定一个JPA查询
    @Query( "SELECT m FROM Media m WHERE m.isbn=:isbn AND m.onShelf=true" )
    /**
     * 将函数调用结果放入缓存，优先从缓存中查找，如果找不到才真正调用函数
     * key 指定SpEL表达式作为缓存键，p0表示第一个入参，result表示返回值
     * keyGenerator 不能和key同时使用，指定一个org.springframework.cache.interceptor.KeyGenerator实现类
     * sync 多个线程同时调用此函数时，是否串行化。当底层操作非常耗时时可以设置为true
     * condition 指定SpEL，仅当（在函数调用前的）求值结果为true时才会缓存
     * unless 指定SpEL，仅当（在函数调用后的）求值结果为false时才会缓存，可以引用result变量
     * cacheManager 指定使用的缓存管理器，@EnableCaching会自动注册可用的缓存管理器，查找缓存实现的顺序为：
     *      GenericJCache EhCache 2.x Hazelcast Infinispan Redis Guava Simple
     *      可以使用spring.cache.type强制指定
     * cacheResolver 指定使用的缓存解析器
     */
    @Cacheable( key = "#p0", sync = false, condition = "#p0.length()==13", unless = "#result == null" )
    Media findByIsbn( @Param( "isbn" ) String isbn );

    @Override
    /**
     * 调用此函数时导致缓存清除
     * allEntries 清除所有缓存
     * beforeInvocation 在调用函数前还是后执行清除
     */
    @CacheEvict( key = "#p0", allEntries = false, beforeInvocation = false )
    void deleteById( Long mediaId );

    @Override
    // 下面的注解总是调用函数并缓存其结果
    @CachePut( key = "#p0.isbn" )
    // 可以同时存储多个缓存
    @Caching(
        put = {
            // 可以引用result
            @CachePut(value = "media", key = "#result.username", condition = "#result != null"),
            @CachePut(value = "media", key = "#result.id", condition = "#result != null")
        }
    )
    Media save( Media media );
}</pre>
<div class="blog_h3"><span class="graybg">服务层</span></div>
<pre class="crayon-plain-tag">package cc.gmem.study.sb.service;


import cc.gmem.study.sb.entity.Media;
import cc.gmem.study.sb.repo.MediaRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.transaction.Transactional;

@Service
@Transactional
public class MediaService {

    @Autowired
    private MediaRepository repo;

    public Media findUserByIsbn( String isbn ) {
        return repo.findByIsbn( isbn );
    }
}</pre>
<div class="blog_h3"><span class="graybg">入口点</span></div>
<pre class="crayon-plain-tag">package cc.gmem.study.sb;

import cc.gmem.study.sb.entity.Media;
import cc.gmem.study.sb.service.MediaService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.ApplicationContext;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

@SpringBootApplication
// 启用Spring基于注解的缓存管理
@EnableCaching
// 从何处扫描JPA实体类
@EntityScan( basePackages = { "cc.gmem.study.sb.entity" } )
// 从何处查找Repository接口的子接口，并自动生成实现类
@EnableJpaRepositories( basePackages = "cc.gmem.study.sb.repo" )
public class JPACacheApplication {

    private static final Logger LOGGER = LoggerFactory.getLogger( JPACacheApplication.class );

    public static void main( String[] args ) {
        ApplicationContext ctx = SpringApplication.run( JPACacheApplication.class, args );
        MediaService ms = ctx.getBean( MediaService.class );
        Media m = ms.findUserByIsbn( "9787547227961" );
        LOGGER.debug( m.getTitle() );
        m = ms.findUserByIsbn( "9787547227961" );
        LOGGER.debug( m.getTitle() );
    }
}</pre>
<div class="blog_h1"><span class="graybg">常见问题</span></div>
<div class="blog_h2"><span class="graybg">关于日志</span></div>
<p>org.springframework.boot.diagnostics应该调整到debug级别， org.springframework应该调整到warn级别，这样可以看到更多有价值的日志。</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/spring-boot-study-note">Spring Boot学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/spring-boot-study-note/feed</wfw:commentRss>
		<slash:comments>0</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-69e8384d5aba7582732397-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>Three.js学习笔记</title>
		<link>https://blog.gmem.cc/three-js-study-note</link>
		<comments>https://blog.gmem.cc/three-js-study-note#comments</comments>
		<pubDate>Sun, 01 Jan 2017 04:16:22 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[JavaScript]]></category>
		<category><![CDATA[WebGL]]></category>
		<category><![CDATA[学习笔记]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=14391</guid>
		<description><![CDATA[<p>简介 关于WebGL Web图形库（Web Graphics Library）简称WebGL，是在浏览器环境下进行3D/2D图像渲染的技术。你不需要额外的插件，就可以在HTML5的Canvas上绘制复杂的、可交互的图形。 大部分现代浏览器支持WebGL技术，IE从11开始支持，老版本的IE可以通过第三方插件支持，例如IEWebGL。 WebGL基于OpenGL ES 2.0提供3D图形接口。后者是OpenGL的一个子集，主要针对手机、PDA之类的嵌入式设备。 关于Three.js 直接使用WebGL编程难度较高，需要了解WebGL的细节、学习复杂的着色器（Shader）语言。Three.js对WebGL的底层细节进行了封装，让你更加容易的、仅利用JavaScript语言创建3D图形，你可以： 创建简单/复杂的3D几何图形 在3D场景中动画、移动对象 给对象应用纹理、材质 从3D模型软件中加载对象 Three.js基本概念 术语 说明 场景Scene 存储并跟踪所有待渲染对象的容器。场景被渲染器渲染到一个HTML5画布中 镜头Camera 定义查看场景的视角，有多种实现： <a class="read-more" href="https://blog.gmem.cc/three-js-study-note">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/three-js-study-note">Three.js学习笔记</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>
<div class="blog_h2"><span class="graybg">关于WebGL</span></div>
<p>Web图形库（Web Graphics Library）简称WebGL，是在浏览器环境下进行3D/2D图像渲染的技术。你不需要额外的插件，就可以在HTML5的Canvas上绘制复杂的、可交互的图形。</p>
<p>大部分现代浏览器支持WebGL技术，IE从11开始支持，老版本的IE可以通过第三方插件支持，例如IEWebGL。</p>
<p>WebGL基于OpenGL ES 2.0提供3D图形接口。后者是OpenGL的一个子集，主要针对手机、PDA之类的嵌入式设备。</p>
<div class="blog_h2"><span class="graybg">关于Three.js</span></div>
<p>直接使用WebGL编程难度较高，需要了解WebGL的细节、学习复杂的着色器（Shader）语言。Three.js对WebGL的底层细节进行了封装，让你更加容易的、仅利用JavaScript语言创建3D图形，你可以：</p>
<ol>
<li>创建简单/复杂的3D几何图形</li>
<li>在3D场景中动画、移动对象</li>
<li>给对象应用纹理、材质</li>
<li>从3D模型软件中加载对象</li>
</ol>
<div class="blog_h2"><span class="graybg">Three.js基本概念</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>场景<br />Scene</td>
<td>存储并跟踪所有待渲染对象的容器。场景被渲染器渲染到一个HTML5画布中</td>
</tr>
<tr>
<td>镜头<br />Camera</td>
<td>
<p>定义查看场景的视角，有多种实现：</p>
<ol>
<li>PerspectiveCamera，基于透视投影（perspective projection）的镜头。透视投影模拟人的视觉效果（近大远小），从某个投射中心（人眼）将物体投射到单一投影面（画面）之上。是最常使用的投影模式</li>
</ol>
</td>
</tr>
<tr>
<td>视截锥<a id="ViewFrustum"></a><br />View Frustum</td>
<td>
<p>在3D计算机图形学中，视截锥是指被建模世界空间的一个区域，该区域可以出现在屏幕中，视截锥定义了概念相机的视界（Field of view，FOV）</p>
<p>使用两个平行的平面，对视野金字塔（pyramid of vision）进行截断操作，即得到视截锥。视截锥的精确形状取决于期望模拟的相机棱镜的形状，典型情况下为六面体，其中远近平面为同长宽比的矩形，如下图：</p>
<p><img class="aligncenter size-full wp-image-14439" src="https://blog.gmem.cc/wp-content/uploads/2017/01/viewfrustum-02.png" alt="viewfrustum-02" width="100%" /> </p>
<p>所谓远、近平面，是指六面体中与视觉方向正交的那两个平面。近平面即上图中标为黄色的那一面。比近平面更近、远平面更远的区域中的对象，不会被绘制。某些情况下远平面被放置到无限远处</p>
<p>视截锥选择（View frustum culling）是指从渲染过程中移除完全位于视截锥之外的对象的处理步骤</p>
</td>
</tr>
<tr>
<td>视界<br />FOV</td>
<td>
<p>在第一人称游戏中，所谓视界（ field of view, field of vision）是指某一时刻游戏世界中显示在屏幕中的（矩形）范围（extent）。视界通常用角度（angle）来描述，但是此角度可能指FOV在垂直、水平、对角线（diagonal）方向的分量</p>
<p>在一定的分辨率下，FOV会依据屏幕纵横比（aspect ratio）而变化，通常FOV在宽屏上更大</p>
<p>我们常以FOV在水平/垂直方向的角度、结合纵横比来描述FOV。它们之间的换算公式如下：</p>
<p style="padding-left: 60px;"><em>r  = w / h = tan(H/2) / tan(V/2)</em></p>
<p>其中r为屏幕纵横比，w/h为屏幕宽高度，H/V为水平、垂直方向的FOV分量</p>
<p>在Three.js中，PerspectiveCamera的fov参数为FOV的垂直分量，这意味着取值从0 ~ 180之间时变化时，如果r保持不变，则视界越大，场景中的目标显得越小</p>
</td>
</tr>
<tr>
<td>渲染器<br />Renderer</td>
<td>负责计算在指定的Camera之下，Scene长得什么样子</td>
</tr>
<tr>
<td>多边形网格<br />Polygon mesh</td>
<td>多边形网格是一系列顶点（vertex）、边线（edge）、面（face）的几何。它在3D计算机图形学中定义了一个多面体的轮廓。网格中的面通常由三角形、四边形或者其它简单的凸面多边形（ convex polygons）构成，以简化渲染的计算量。下面是一个由三角形网格构成的海豚模型示例：<a href="https://blog.gmem.cc/wp-content/uploads/2017/01/Dolphin_triangle_mesh.png"><img class="aligncenter size-full wp-image-14413" src="https://blog.gmem.cc/wp-content/uploads/2017/01/Dolphin_triangle_mesh.png" alt="dolphin_triangle_mesh" width="95%" /></a> </td>
</tr>
<tr>
<td>混合模式<br />Blend mode / <br />Mixing mode</td>
<td>
<p>图像处理中的概念。用于确定两层图像如何叠加到一起。大部分应用中默认的叠加模式就是让顶层（top layer ）直接覆盖较低的层（lower layers ）。由于每个像素的色彩都是基于数字来表示的，因此基于数学运算的大量混合模式可用</p>
<p>大部分图像处理软件，例如Photoshop、GIMP，都支持用户修改混合模式。</p>
<p>参考：<a href="https://en.wikipedia.org/wiki/Blend_modes">https://en.wikipedia.org/wiki/Blend_modes</a></p>
</td>
</tr>
<tr>
<td>粒子，精灵<br />Particles, Sprite</td>
<td>
<p>指存在于3D场景中的二维图形或者动画</p>
</td>
</tr>
<tr>
<td>右手系<br />right-handed system</td>
<td>
<p>Three.js默认使用右手坐标系，因为这是OpenGL默认的坐标系</p>
<p>所谓右手系，是指：</p>
<ol>
<li>伸出右手，伸直拇指，让它与另外四指垂直</li>
<li>弯曲中、无名、小指，让它们与食指垂直</li>
<li>以拇指指向为X轴正向、食指指向为Y轴正向、其它手指指向为Z轴正向的坐标系，即右手系</li>
</ol>
<p>图示如下：</p>
<p><img class="aligncenter size-full wp-image-14527" src="https://blog.gmem.cc/wp-content/uploads/2017/01/right-handed-system.png" alt="right-handed-system" width="220" height="165" /></p>
<p>Blender等3D建模软件，使用Z轴向上（上图右手系沿X轴正向逆时针旋转90度）的右手系。主要原因是大部分CAD软件均使用这样的坐标系</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">第一个3D场景</span></div>
<div class="blog_h2"><span class="graybg">渲染并查看3D对象</span></div>
<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>Plane</td>
<td>平面，二维的矩形，作为“地面”在场景的中央展示</td>
</tr>
<tr>
<td>Cube</td>
<td>三维盒子，展示为红色</td>
</tr>
<tr>
<td>Sphere</td>
<td>三维球体，展示为蓝色</td>
</tr>
<tr>
<td>Camera</td>
<td>镜头，决定你看到的场景是什么样子</td>
</tr>
<tr>
<td>Axes</td>
<td>X/Y/Z轴，辅助的调试工具，方便查看对象在哪里渲染</td>
</tr>
</tbody>
</table>
<p>代码及注释：</p>
<pre class="crayon-plain-tag">&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;title&gt;Three.js Study&lt;/title&gt;
    &lt;script src="https://code.jquery.com/jquery-1.12.4.js"&gt;&lt;/script&gt;
    &lt;script src="three.js"&gt;&lt;/script&gt;
    &lt;style&gt;
        body {
            margin: 0;
            overflow: hidden;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;div id="WebGL"&gt;&lt;/div&gt;
&lt;script type="text/javascript"&gt;
    $( function () {
        // 场景
        var scene = new THREE.Scene();

        var aspect = window.innerWidth / window.innerHeight;
        /**
         * 定义一个透视镜头，参数：
         * fov FOV垂直分量，镜头到视截锥近平面上下边之间的夹角
         * aspect 视截锥的纵横比
         * near 近平面离镜头多远
         * far 远平面离镜头多远
         */
        var camera = new THREE.PerspectiveCamera( 45, aspect, 0.1, 1000 );

        // WebGL渲染器，使用显卡来渲染场景。尽管还存在其它渲染器实现，但是处于性能、特性的考虑，不推荐使用
        var renderer = new THREE.WebGLRenderer();
        // 设置背景色，第二参数为透明度
        renderer.setClearColor( 0xEEEEEE, 1 );
        // 设置场景的大小
        renderer.setSize( window.innerWidth, window.innerHeight );

        // 创建一个调试用途的坐标轴，X轴红色、Y轴绿色、Z轴蓝色。20表示轴线长度
        var axes = new THREE.AxisHelper( 20 );
        // 把对象添加到场景中
        scene.add( axes );

        // 创建一个平面几何图形，宽60高20，在宽、高方向上分段数为1（不切分）
        var planeGeometry = new THREE.PlaneGeometry( 60, 20, 1, 1 );
        // 材质，定义颜色、透明度、反光效果之类的属性
        // MeshBasicMaterial是一种简单材质，它不受光线影响，使用纯色或者网格（wireframe）渲染几何图形
        var planeMaterial = new THREE.MeshBasicMaterial( { color: 0xcccccc } );
        // Mesh表示一类基于三角形网格（triangular polygon mesh）的对象
        var plane = new THREE.Mesh( planeGeometry, planeMaterial );
        // 默认的，平面的对称中心位于原点，width与X轴平行，height与Y轴平行
        // 在X轴方向逆时针（从原点往X轴正向看）旋转90度
        plane.rotation.x = -0.5 * Math.PI; // 圆周长2PI，PI代表180度，
        // 在X轴方向偏移15
        plane.position.x = 15;
        plane.position.y = 0;
        plane.position.z = 0;
        scene.add( plane );

        // 类似的，创建一个立方体，类似的，其对称中心也是默认位于原点
        var cubeGeometry = new THREE.CubeGeometry( 4, 4, 4 );
        // wireframe表示绘制网格线
        var cubeMaterial = new THREE.MeshBasicMaterial( { color: 0xff0000, wireframe: true } );
        var cube = new THREE.Mesh( cubeGeometry, cubeMaterial );
        cube.position.x = -4;
        cube.position.y = 3;
        cube.position.z = 0;
        scene.add( cube );

        // 绘制一个球体
        var sphereGeometry = new THREE.SphereGeometry( 4, 20, 20 );
        var sphereMaterial = new THREE.MeshBasicMaterial( { color: 0x7777ff, wireframe: true } );
        var sphere = new THREE.Mesh( sphereGeometry, sphereMaterial );
        sphere.position.x = 20;
        sphere.position.y = 4;
        sphere.position.z = 2;
        scene.add( sphere );

        // 移动镜头
        camera.position.x = -30;
        camera.position.y = 40;
        camera.position.z = 30;
        // 将镜头指向场景的中心
        camera.lookAt( scene.position );
        
        // 渲染
        $( "#WebGL" ).append( renderer.domElement );
        renderer.render( scene, camera );
    } );
&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</pre>
<p>渲染效果：<img class="aligncenter size-full wp-image-14416" src="https://blog.gmem.cc/wp-content/uploads/2017/01/first-threejs-scene-01.png" alt="first-threejs-scene-01" width="100%" /></p>
<div class="blog_h2"><span class="graybg">添加材质/光影效果</span></div>
<p>本节我们改进一下上面的例子，修改材质，并添加光线、阴影效果。</p>
<p>首先，为场景添加一个光源：</p>
<pre class="crayon-plain-tag">// 聚光灯效果的白色光源
var spotLight = new THREE.SpotLight( 0xffffff );
// 设置聚光灯的位置
spotLight.position.set( -40, 60, -10 );
scene.add( spotLight );</pre>
<p>添加这段代码后，渲染效果不会有任何改变。原因我们已经在前面的代码注释中提到过， MeshBasicMaterial这种材质不会对光线作出反应。我们替换一下材质：</p>
<pre class="crayon-plain-tag">var planeMaterial = new THREE.MeshLambertMaterial( { color: 0xcccccc } );
// ...
var cubeMaterial = new THREE.MeshLambertMaterial( { color: 0xff0000 } );
// ... 
var sphereMaterial = new THREE.MeshLambertMaterial( { color: 0x7777ff} );</pre>
<p>除了MeshLambertMaterial之外，MeshPhongMaterial也会对光源作出反应。</p>
<p>现在刷新一下页面，可以看到如下渲染效果：<img class="aligncenter size-full wp-image-14419" src="https://blog.gmem.cc/wp-content/uploads/2017/01/first-threejs-scene-02.png" alt="first-threejs-scene-02" width="100%" /></p>
<p>比上一幅截图好看多了，但是还有点不自然，因为没有阴影效果。</p>
<p>由于渲染阴影比较消耗资源，因此默认情况下Three.js关闭了阴影。要启用阴影其实很简单：</p>
<pre class="crayon-plain-tag">// 启用阴影效果
renderer.shadowMapEnabled = true;</pre>
<p>此外，你还需要定义什么对象产生（cast）阴影，什么对象接收阴影：</p>
<pre class="crayon-plain-tag">// 阴影由平面接收
plane.receiveShadow = true;
// ...
cube.castShadow = true;
// ...
sphere.castShadow = true;


// 设置产生阴影的光源
spotLight.castShadow = true;
// 提高阴影质量
spotLight.shadowMapWidth = spotLight.shadowMapHeight = 1024 * 4;</pre>
<div class="blog_h2"><span class="graybg">添加动画效果</span></div>
<p>要想为场景添加动画效果，我们需要找到定期重渲染场景的方法。setInterval()这种定时器是不适合的，因为它与渲染行为不是同步的，会导致严重性能问题。</p>
<p><pre class="crayon-plain-tag">requestAnimationFrame()</pre> 是现代浏览器支持的、避免两setInterval()缺点的函数。你可以为它提供一个回调，此回调会定期（间隔由浏览器定义）的被调用。在回调中你可以指定任何渲染逻辑，浏览器负责尽可能平滑、高效的绘制。示例代码：</p>
<pre class="crayon-plain-tag">function renderScene() {
    requestAnimationFrame( renderScene );
    renderer.render( scene, camera );
}</pre>
<p>上面的函数把自己传递给requestAnimationFrame，从而导致函数的逻辑被反复调用，从而可以产生动画效果。</p>
<div class="blog_h3"><span class="graybg">FPS统计</span></div>
<p>为了显示动画帧率信息，我们引入一个助手库<a href="https://github.com/mrdoob/stats.js/">stats.js</a>：</p>
<pre class="crayon-plain-tag">&lt;script src="stat.js"&gt;&lt;/script&gt;
&lt;script type="text/javascript"&gt;
    var stats = new Stats();
    stats.showPanel( 0 ); // 0: fps, 1: ms, 2: mb, 3+: custom
    document.body.appendChild( stats.dom );
    function renderScene() {
        // 开始统计
        stats.begin();
        // 这里编写被监控的代码
        stats.end();
        requestAnimationFrame( animate );
    }
    requestAnimationFrame( renderScene );
&lt;/script&gt;</pre>
<div class="blog_h3"><span class="graybg">添加动画</span></div>
<p>下面我们为立方体添加一个翻滚效果，为球体添加一个弹跳效果：</p>
<pre class="crayon-plain-tag">var step = 0;

function animate() {
    stats.begin();
    cube.rotation.x += 0.02;
    cube.rotation.y += 0.02;
    cube.rotation.z += 0.02;
    step += 0.04; // 定义弹跳速度
    sphere.position.x = 20 + ( 10 * (Math.cos( step )));
    sphere.position.y = 2 + ( 10 * Math.abs( Math.sin( step ) ));
    renderer.render( scene, camera );  // 反复渲染
    stats.end();
    requestAnimationFrame( animate );
}

requestAnimationFrame( animate );</pre>
<p>刷新浏览器，可以查看动画效果，注意左上角的帧率窗口。</p>
<div class="blog_h1"><span class="graybg">使用基本组件</span></div>
<p>上一章的学习中，我们创建了由几个对象构成的简单场景，并制作了简单的动画效果。现在我们来更深入的了解一下构成Three.js场景的组件。 </p>
<div class="blog_h2"><span class="graybg">场景的内容物</span></div>
<p>之前我们调用<pre class="crayon-plain-tag">new THREE.Scene()</pre> 创建了一个场景。场景是一个容器，它内部可以包含三类东西：</p>
<ol>
<li>镜头（Camera）：决定了查看场景的角度和方式。在渲染场景的时候镜头可以自动创建，但是你也可以手工指定其参数</li>
<li>光源（Lights）：影响材质的渲染效果、阴影</li>
<li>物体（Objects）：场景中渲染的主要东西。包括各种几何形状、导入的模型</li>
</ol>
<div class="blog_h2"><span class="graybg">场景的基本API</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>children</td>
<td>所有对象组成的数组</td>
</tr>
<tr>
<td>getChildByName(name)</td>
<td>根据名称来查找对象</td>
</tr>
<tr>
<td>remove(obj)</td>
<td>从场景中移除一个对象</td>
</tr>
<tr>
<td>traverse(callback)</td>
<td>指定一个回调，针对场景中所有对象调用之</td>
</tr>
<tr>
<td>fog</td>
<td>添加烟雾效果，这样越远的物体显示越模糊：<br />
<pre class="crayon-plain-tag">// 白色雾，从near=0.015开始出现，far=100表示雾变浓厚的速率
scene.fog = new THREE.Fog( 0xffffff, 0.015, 100 );
// 指定颜色、浓度
scene.fog = new THREE.FogExp2( 0xffffff, 0.01 );</pre>
</td>
</tr>
<tr>
<td>overrideMaterial</td>
<td>覆盖场景中所有物体的材质设置：<br />
<pre class="crayon-plain-tag">scene.overrideMaterial = new THREE.MeshLambertMaterial({color: 0xffffff});</pre>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">Geometry/Mesh的基本API</span></div>
<p>Three.js提供了大量开箱即用的Geometry， Geometry用于定义物体的形状，材质则定义其外观。</p>
<p>在大部分3D图形库中，Geometry基本上都是三维空间中一系列点、以及连接这些点的面的集合。以立方体为例：</p>
<ol>
<li>每个立方体包含8个角，这些角可以由三维空间中的一个点来确定。这些点称为顶点（vertices）</li>
<li>每个立方体包含6个面，这些面的每个角都对应一个顶点。这些面称为face</li>
</ol>
<p>当使用Three.js自带的Geometry时你不需要逐个定义所有点、面。例如对于Cube，你只需要定义长宽高即可，Three.js会利用你提供的这些信息创建所有必须的点、面。</p>
<p>Three.js允许自定义点、面，然后构成一个几何图形。下面是手工构建Cube的例子：</p>
<pre class="crayon-plain-tag">// 所有顶点
var vertices = [
    new THREE.Vector3( 1, 3, 1 ),
    new THREE.Vector3( 1, 3, -1 ),
    new THREE.Vector3( 1, -1, 1 ),
    new THREE.Vector3( 1, -1, -1 ),
    new THREE.Vector3( -1, 3, -1 ),
    new THREE.Vector3( -1, 3, 1 ),
    new THREE.Vector3( -1, -1, -1 ),
    new THREE.Vector3( -1, -1, 1 )
];
// 所有三角形的面，数字为从0开始的顶点序号
var faces = [
    new THREE.Face3( 0, 2, 1 ),
    new THREE.Face3( 2, 3, 1 ),
    new THREE.Face3( 4, 6, 5 ),
    new THREE.Face3( 6, 7, 5 ),
    new THREE.Face3( 4, 5, 1 ),
    new THREE.Face3( 5, 0, 1 ),
    new THREE.Face3( 7, 6, 2 ),
    new THREE.Face3( 6, 3, 2 ),
    new THREE.Face3( 5, 7, 0 ),
    new THREE.Face3( 7, 2, 0 ),
    new THREE.Face3( 1, 3, 4 ),
    new THREE.Face3( 3, 6, 4 ),
];
var geom = new THREE.Geometry();
geom.vertices = vertices;
geom.faces = faces;
// 在重新设置顶点数组后，提示需要更新。这是因为Three.js默认假设Mesh的Geometry的形状在生命周期内保持不变
geom.verticesNeedUpdate = true;
// 根据顶点重新计算面法线
geom.computeFaceNormals();</pre>
<p>在以前版本的Three.js中，允许使用四边形来定义面。四边形在建模时比较受欢迎，原因是很容易被增强、平滑。三角形在渲染、游戏引擎中比较受欢迎，原因是比较简单。 </p>
<p>有了Geometry后，加上材质就可以构成简单的3D物体——Mesh了：</p>
<pre class="crayon-plain-tag">// 材质数组
var materials = [
    new THREE.MeshLambertMaterial( { opacity: 0.6, color: 0x44ff44, transparent: true } ),
    new THREE.MeshBasicMaterial( { color: 0x666666, wireframe: true } )
];
// Mesh组
var mesh = THREE.SceneUtils.createMultiMaterialObject( geom, materials );
scene.add( mesh );</pre>
<p>Three.js允许给Geometry应用多个材质，上例中的Cube既有颜色填充，也显示了线条，这是两种材质的混合效果。从实现角度来说，<span style="background-color: #c0c0c0;">Three.js创建了两个THREE.Mesh实例</span>，每个材质对应一个实例，这<span style="background-color: #c0c0c0;">两个实例被放置到一个组里面</span>。添加组到场景的方式，与添加Mesh一致。</p>
<p>我们可以调用组的forEach，对其中所有Mesh进行操作：</p>
<pre class="crayon-plain-tag">mesh.children.forEach( function ( e ) {
    e.castShadow = true
} );</pre>
<div class="blog_h3"><span class="graybg">Geometry的基本API</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>vertices</td>
<td>构成此Geometry的顶点坐标数组</td>
</tr>
<tr>
<td>faces</td>
<td>构成Geometry的三角形面数组</td>
</tr>
<tr>
<td>verticesNeedUpdate</td>
<td>修改顶点数组后，提示Three.js需要更新顶点</td>
</tr>
<tr>
<td>computeFaceNormals()</td>
<td>重新根据顶点来计算面</td>
</tr>
<tr>
<td>clone()</td>
<td>克隆一个Geometry</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">Mesh的基本API</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>position.x|y|z<br />position.set(x,y,z)</td>
<td>此物体相对于父对象的位置，大部分物体的父对象是THREE.Scene对象，对于组中的Mesh，其父对象是组。示例代码：<br />
<pre class="crayon-plain-tag">// 方法一：
cube.position.x=10;
cube.position.y=3;
cube.position.z=1;
// 方法二：
cube.position.set(10,3,1);
// 方法三：
cube.postion=new THREE.Vector3(10,3,1)</pre>
</td>
</tr>
<tr>
<td>rotation.x|y|z</td>
<td>让物体围绕自己的轴（而不是场景的）旋转一定角度。与position类似，具有三种设置方法</td>
</tr>
<tr>
<td>scale.x|y|z</td>
<td>让物体在其轴方向缩放。与position类似，具有三种设置方法</td>
</tr>
<tr>
<td>translateX(amount)</td>
<td rowspan="3">
<p>将物体沿着X/Y/Z轴方向移动</p>
<p>这些方法指定的是相对位移，而position指定的是绝对值</p>
</td>
</tr>
<tr>
<td>translateY(amount)</td>
</tr>
<tr>
<td>translateZ(amount)</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">使用镜头</span></div>
<p>Three.js支持两种类型的镜头：正交（orthographic）镜头、透视（perspective）镜头。到目前为止我们还没有使用过正交镜头。</p>
<p>正交镜头的特点是，物品的渲染尺寸与它距离镜头的远近无关。也就是说在场景中移动一个物体，其大小不会变化。正交镜头适合2D游戏。</p>
<p>透视镜头则是模拟人眼的视觉特点，距离远的物体显得更小。透视镜头通常更适合3D渲染。</p>
<div class="blog_h3"><span class="graybg">THREE.PerspectiveCamera的API</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>fov</td>
<td>
<p>视界，从镜头可以看到的场景的部分。其值为镜头到近平面上下边的夹角</p>
<p>人眼的FOV接近180度，某些鸟类的FOV打到360度。但是计算机屏幕做不到覆盖视野，通常3D游戏的FOV取值在60-90度之间</p>
<p>较好的默认值为45</p>
</td>
</tr>
<tr>
<td>aspect</td>
<td>渲染区域的纵横比。较好的默认值为window.innerWidth/window.innerHeight</td>
</tr>
<tr>
<td>near</td>
<td>近平面离镜头的距离。较好的默认值为0.1</td>
</tr>
<tr>
<td>far</td>
<td>远平面离镜头的距离。较好的默认值为1000</td>
</tr>
</tbody>
</table>
<p>关于这些参数的形象化描述，请参考<a href="#ViewFrustum">术语视截锥</a>中的截图。</p>
<div class="blog_h3"><span class="graybg">THREE.OrthographicCamera的API</span></div>
<p>正交镜头不关心FOV、纵横比这些概念。其构造函数实际上是指定了一个Cube，落在其中的物体会被渲染：</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>left</td>
<td>相机截锥左平面位置，如果你将其设置为-100，则位置在其更左边的物体将不可见</td>
</tr>
<tr>
<td>right</td>
<td>相机截锥右平面位置</td>
</tr>
<tr>
<td>top</td>
<td>相机截锥上平面位置</td>
</tr>
<tr>
<td>bottom</td>
<td>相机截锥下平面位置</td>
</tr>
<tr>
<td>near</td>
<td>近平面的位置</td>
</tr>
<tr>
<td>far</td>
<td>远平面的位置</td>
</tr>
</tbody>
</table>
<p>关于这些参数的形象化描述，参考下图：</p>
<div class="blog_h2"><img class="aligncenter size-full wp-image-14445" src="https://blog.gmem.cc/wp-content/uploads/2017/01/orthographic-camera.png" alt="orthographic-camera" width="100%" /><span class="graybg">镜头聚焦</span></div>
<p>创建镜头后，还需要将其移动、然后对准物体积聚的场景中心位置，才能确保物体的渲染。移动镜头，通过设置其position属性来实现：</p>
<pre class="crayon-plain-tag">camera.position.x = 120;
camera.position.y = 60;
camera.position.z = 180;</pre>
<p>聚焦，则是调用下面的方法实现：</p>
<pre class="crayon-plain-tag">camera.lookAt( new THREE.Vector3( x, 10, 0 ) );</pre>
<div class="blog_h2"><span class="graybg">HUD</span></div>
<p>所谓HUD（head-up display，平视显示），是指在屏幕（挡风玻璃）上显示一些辅助信息（例如飞机、汽车的仪表信息），避免驾驶员低头分散注意力。HUD的特点是其显示内容的大小、位置与镜头无关。</p>
<p>要实现HUD效果，可以同时渲染两套场景，其中HUD场景使用OrthographicCamera镜头：</p>
<pre class="crayon-plain-tag">var scene = new THREE.Scene();
var sceneOrtho = new THREE.Scene();

var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 250);
// 正交镜头的近平面的大小，和浏览器窗口大小一致
var cameraOrtho = new THREE.OrthographicCamera(0, window.innerWidth, window.innerHeight, 0, -10, 10);

var webGLRenderer = new THREE.WebGLRenderer();

webGLRenderer.render(scene, camera);
// 防止在下一次render时，自动清屏
webGLRenderer.autoClear = false;
webGLRenderer.render(sceneOrtho, cameraOrtho);</pre>
<div class="blog_h1"><span class="graybg">使用光源</span></div>
<p>在Three.js中光源很重要。不设置光源你就看不到被渲染的物体。Three.js内置了多种光源以满足特定场景的需要。</p>
<div class="blog_h2"><span class="graybg">光源分类</span></div>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">光源</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>AmbientLight</td>
<td>环境光，其颜色均匀的应用到场景及其所有对象上</td>
</tr>
<tr>
<td>PointLight</td>
<td>3D空间中的一个点光源，向所有方向发出光线</td>
</tr>
<tr>
<td>SpotLight</td>
<td>产生圆锥形光柱的聚光灯，台灯、天花板射灯通常都属于这类光源</td>
</tr>
<tr>
<td>DirectionalLight</td>
<td>也就无限光，光线是平行的。典型的例子是日光</td>
</tr>
<tr>
<td>HemisphereLight</td>
<td>特殊光源，用于创建户外自然的光线效果，此光源模拟物体表面反光效果、微弱发光的天空</td>
</tr>
<tr>
<td>AreaLight</td>
<td>面光源，指定一个发光的区域</td>
</tr>
<tr>
<td>LensFlare</td>
<td>不是光源，用于给光源添加镜头光晕效果</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">基本光源</span></div>
<div class="blog_h3"><span class="graybg">AmbientLight</span></div>
<p>这种光源为场景添加全局的环境光。这种光没有特定的方向，不会产生阴影。通常不会把AmbientLight作为唯一的光源，而是和SpotLight、DirectionalLight等光源结合使用，从而达到<span style="background-color: #c0c0c0;">柔化阴影、添加全局色调</span>的效果。</p>
<p>指定颜色时要相对保守，例如#0c0c0c。设置太亮的颜色会导致整个画面过度饱和，什么都看不清：</p>
<pre class="crayon-plain-tag">var ambiColor = "#0c0c0c";
var ambientLight = new THREE.AmbientLight(ambiColor);
scene.add(ambientLight);</pre>
<div class="blog_h3"><span class="graybg">PointLight</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>color</td>
<td>光线的颜色</td>
</tr>
<tr>
<td>intensity</td>
<td>光线的强度，默认1，浮点数</td>
</tr>
<tr>
<td>distance</td>
<td>光线能照耀的距离</td>
</tr>
<tr>
<td>position</td>
<td>光源的位置</td>
</tr>
<tr>
<td>visible</td>
<td>设置为true则打开光源</td>
</tr>
</tbody>
</table>
<p>示例代码：</p>
<pre class="crayon-plain-tag">var pointColor = "#ccffcc";
var pointLight = new THREE.PointLight( pointColor );
pointLight.distance = 100;
scene.add( pointLight );
// 设置强度
pointLight.intensity = 2.4;</pre>
<div class="blog_h3"><span class="graybg">SpotLight</span></div>
<p>这种光源的使用场景最多，特别是在你需要阴影效果的时候。PointLight的所有属性对于SpotLight可用，前者还包括以下属性：</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>castShadow</td>
<td>
<p>此光源是否可以导致物体产生阴影</p>
<p>注意：目标物体需要设置receiveShadow</p>
</td>
</tr>
<tr>
<td>shadowCameraNear</td>
<td>从距离光源多远的地方开始创建阴影</td>
</tr>
<tr>
<td>shadowCameraFar</td>
<td>到距离光源多远的地方不再创建阴影</td>
</tr>
<tr>
<td>shadowCameraFov</td>
<td>阴影的FOV</td>
</tr>
<tr>
<td>target</td>
<td>此光源指向的目标。光线从光源照向该目标：<br />
<pre class="crayon-plain-tag">var targetObject = new THREE.Object3D();
scene.add(targetObject);
// 聚光灯将跟踪三维空间中的一个点
light.target = targetObject;</pre>
</td>
</tr>
<tr>
<td>shadowBias</td>
<td>设置阴影的位置偏移</td>
</tr>
<tr>
<td>angle</td>
<td>光锥的夹角，默认Math.PI/3</td>
</tr>
<tr>
<td>exponent</td>
<td>衰减指数，即随着与光源距离的增加，光线衰减的速度</td>
</tr>
<tr>
<td>onlyShadow</td>
<td>如果设置为true，仅仅产生阴影，而不照亮物体</td>
</tr>
<tr>
<td>shadowCameraVisible</td>
<td>如果设置为true，你将看到光源如何、从何处产生阴影（显示截锥）。用于调试目的</td>
</tr>
<tr>
<td>shadowDarkness</td>
<td>阴影的暗度，默认0.5。一旦场景被创建此参数即不可修改</td>
</tr>
<tr>
<td>shadowMapWidth<br />shadowMapHeight</td>
<td>
<p>有多少像素用于创建阴影，如果阴影出现锯齿效果，可以增加此参数。一旦场景被创建此参数即不可修改</p>
<p>另一种减轻阴影锯齿的方法是，让阴影相机截锥尽可能小</p>
</td>
</tr>
</tbody>
</table>
<p>示例代码：</p>
<pre class="crayon-plain-tag">var spotLight = new THREE.SpotLight(pointColor);
spotLight.position.set(-40, 60, -10);
spotLight.castShadow = true;
spotLight.shadowCameraNear = 2;
spotLight.shadowCameraFar = 200;
spotLight.shadowCameraFov = 30;
spotLight.target = plane;   // 跟踪目标
spotLight.distance = 0;
spotLight.angle = 0.4;

scene.add(spotLight);</pre>
<p>光锥的宽、高可以基于以下代码求出：</p>
<pre class="crayon-plain-tag">var coneLength = light.distance ? light.distance : 10000;
var coneWidth = coneLength * Math.tan( light.angle * 0.5 ) * 2;</pre>
<div class="blog_h3"><span class="graybg">DirectionalLight</span></div>
<p>用于模拟遥远的，类似太阳那样的光源。该光源与SpotLight的主要区别是，它不会随着距离而变暗，所有被照耀的地方获得相同的光照强度。</p>
<p>DirectionalLight具有大部分SpotLight的属性。示例代码：</p>
<pre class="crayon-plain-tag">var directionalLight = new THREE.DirectionalLight( pointColor );
directionalLight.position.set( -40, 60, -10 );
directionalLight.castShadow = true;
directionalLight.shadowCameraNear = 2;
directionalLight.shadowCameraFar = 200;
directionalLight.shadowCameraLeft = -50;
directionalLight.shadowCameraRight = 50;
directionalLight.shadowCameraTop = 50;
directionalLight.shadowCameraBottom = -50;

directionalLight.distance = 0;
directionalLight.intensity = 0.5;
directionalLight.shadowMapHeight = 1024;
directionalLight.shadowMapWidth = 1024;

scene.add( directionalLight );</pre>
<div class="blog_h2"><span class="graybg">高级光源</span></div>
<div class="blog_h3"><span class="graybg">HemisphereLight</span></div>
<p>模拟穹顶（半球）的微弱发光效果，让户外场景更加逼真。使用DirectionalLight + AmbientLight可以在某种程度上来模拟户外光线，但是不够真实，因为无法体现大气层的散射效果、地面或物体的反射效果。常用属性：</p>
<table class=" fixed-word-wrap full-width">
<thead>
<tr>
<td style="width: 25%; text-align: center;">属性</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>color</td>
<td>天空散射的光线颜色</td>
</tr>
<tr>
<td>groundColor</td>
<td>地面散射的光线颜色</td>
</tr>
<tr>
<td>intensity</td>
<td>光线强度</td>
</tr>
</tbody>
</table>
<p>示例代码：</p>
<pre class="crayon-plain-tag">// 三个参数分别对应天空颜色、地面颜色、强度
var hemiLight = new THREE.HemisphereLight(0x0000ff, 0x00ff00, 0.6);
hemiLight.position.set(0, 500, 0);
scene.add(hemiLight);</pre>
<div class="blog_h3"><span class="graybg">AreaLight</span></div>
<p>用于定义一个发光的矩形区域，该光源属于Three.js扩展。</p>
<p>THREE.WebGLRenderer这个渲染器不能和AreaLight一起使用，原因是THREE.AreaLight是一种复杂的光源，与WebGLRenderer一起使用会导致严重的性能问题。</p>
<p>渲染器THREE.WebGLDeferredRenderer使用不同的途径来渲染场景，它将渲染拆分为几个步骤。它能够处理复杂的光源或者数量众多的光源。</p>
<div class="blog_h1"><span class="graybg">使用材质</span></div>
<p>通过前面章节的学习，我们已经知道材质 + Geometry可以构成Mesh——可以添加到3D场景中的物体。</p>
<p>Geometry就好像是骨架，材质则类似于皮肤，它定义了Geometry的外观——是否有金属质感、是否透明、是否显示为线框（wireframe）。</p>
<div class="blog_h2"><span class="graybg">材质分类</span></div>
<table class=" fixed-word-wrap full-width">
<thead>
<tr>
<td style="width: 25%; text-align: center;">材质</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>MeshBasicMaterial</td>
<td>基本的材质，显示为简单的颜色或者显示为线框。不考虑光线的影响</td>
</tr>
<tr>
<td>MeshDepthMaterial</td>
<td>使用简单的颜色，但是颜色深度和距离相机的远近有关</td>
</tr>
<tr>
<td>MeshNormalMaterial</td>
<td>基于面Geometry的法线（normals）数组来给面着色</td>
</tr>
<tr>
<td>MeshFacematerial</td>
<td>容器，允许为Geometry的每一个面指定一个材质</td>
</tr>
<tr>
<td>MeshLambertMaterial</td>
<td>考虑光线的影响，哑光材质</td>
</tr>
<tr>
<td>MeshPhongMaterial</td>
<td>考虑光线的影响，光泽材质</td>
</tr>
<tr>
<td>ShaderMaterial</td>
<td>允许使用自己的着色器来控制顶点如何被放置、像素如何被着色</td>
</tr>
<tr>
<td>LineBasicMaterial</td>
<td>用于THREE.Line对象，创建彩色线条</td>
</tr>
<tr>
<td>LineDashMaterial</td>
<td>用于THREE.Line对象，创建虚线条</td>
</tr>
<tr>
<td>RawShaderMaterial</td>
<td>仅和THREE.BufferedGeometry联用，优化静态Geometry（顶点、面不变）的渲染</td>
</tr>
<tr>
<td>SpriteCanvasMaterial</td>
<td rowspan="3">在针对单独的点进行渲染时用到</td>
</tr>
<tr>
<td>SpriteMaterial</td>
</tr>
<tr>
<td>PointCloudMaterial</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">公共属性</span></div>
<p>作为所有材质的基类，THREE.Material提供了以下属性：</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"><em><strong>基本属性</strong></em><br />最常用的属性，用于控制对象的透明度、是否可见、如何被引用（基于ID还是名称）</td>
</tr>
<tr>
<td>id</td>
<td>当你创建一个材质的时候自动分配，作为材质实例的标识，从0开始自动计数</td>
</tr>
<tr>
<td>uuid</td>
<td>全局唯一标识，内部使用</td>
</tr>
<tr>
<td>name</td>
<td>给材质分配一个名称，调试用</td>
</tr>
<tr>
<td>opacity</td>
<td>
<p>设定材质的透明度，和transparent联用，范围0～1</p>
</td>
</tr>
<tr>
<td>transparent</td>
<td>
<p>如果设置为true，则Three.js考虑opacity的设置。对于具有Alpha通道的纹理，该属性也需要设置为true</p>
</td>
</tr>
<tr>
<td>overdraw</td>
<td>使用THREE.CanvasRenderer渲染器时多边形会比预期的绘制的大一些。如果使用该渲染器时你法线Gaps可设置为true </td>
</tr>
<tr>
<td>visible</td>
<td>材质是否可见，如果设置为false则物体看不见</td>
</tr>
<tr>
<td>side</td>
<td>
<p>材质应用到目标Geomotry的哪一面。默认THREE.Frontside表示应用在外面，可选值THREE.BackSide应用在里面、THREE.DoubleSide应用到两面</p>
<p>对于不封闭空间的Geomotry，例如平面，此属性重要</p>
</td>
</tr>
<tr>
<td>needsUpdate</td>
<td>改变材质的某些属性后，你可以设置该属性为true，这样Three.js就会丢弃缓存，重新渲染材质</td>
</tr>
<tr>
<td colspan="2"><em><strong>混合（Blending）属性</strong></em><br />定义对象如何与其背景混合，或者说我们渲染的颜色如何与其背后的颜色交互</td>
</tr>
<tr>
<td>blending </td>
<td>决定材质如何与背景混合，默认值THREE.NormalBlending，表示仅仅显示顶层颜色</td>
</tr>
<tr>
<td>blendsrc</td>
<td>定义物体（源）如何混合到背景（目标）中，默认THREE.SrcAlphaFactor表示基于物体的Alpha通道进行混合</td>
</tr>
<tr>
<td>blenddst</td>
<td>定义在混合时，背景（目标）如何渲染，默认THREE.OneMinusSrcAlphaFactor表示基于物体的Alpha通道进行混合</td>
</tr>
<tr>
<td> blendequation</td>
<td>定义blendsrc、blenddst如何使用，默认将它们相加（AddEquation）</td>
</tr>
<tr>
<td colspan="2"><strong><em>高级属性</em></strong><br />控制低级别的WebGL上下文如何渲染对象，大部分情况下不需要使用</td>
</tr>
<tr>
<td>depthTest</td>
<td>
<p>如果关闭depthTest，意味着同时关闭reading/testing/writing</p>
<p>到底什么是深度测试（depthTest）呢？假设由两个完全一样的形状，位于你的正前方。真实世界中，你仅能看到里你近的那一个。但是在3D渲染过程中：</p>
<ol>
<li>如果远的物体先被绘制，那么没有问题，效果和真实世界一致</li>
<li>如果近的物体先被绘制，远物体后被绘制，就会有问题，远物体可以被看见</li>
</ol>
<p>所谓深度测试，是现代GPU中内置的一个工具，能够让渲染输出总是符合预期，而不管对象的输出先后顺序。具体实现机制是：当绘制一个像素时，会查看此像素位置原先的depth（即离相机的远近）值，<span style="background-color: #c0c0c0;">如果新的像素depth值较小，则执行绘制</span>，否则保留原来的值</p>
<p>由于深度测试的实现机制，和透明度（混合）在一起工作时可能出现问题，有时候需要禁用</p>
</td>
</tr>
</tbody>
</table>
<p>本章跳过了所有和纹理（textures）、映射（maps）、动画 有关的属性。</p>
<div class="blog_h2"><span class="graybg">简单Mesh材质</span></div>
<p>你可以把属性组成一个对象，作为构造函数的入参：</p>
<pre class="crayon-plain-tag">var material = new THREE.MeshBasicMaterial( {
    color: 0xff0000,
    name: 'material-1',
    opacity: 0.5,
    transparency: true
} );</pre>
<p>或者逐个的设置属性：</p>
<pre class="crayon-plain-tag">var material = new THREE.MeshBasicMaterial();
material.color = new THREE.Color( 0xff0000 );  // 这种方式必须提供Color对象
material.name = 'material-1';
material.opacity = 0.5;
material.transparency = true;</pre>
<div class="blog_h3"><span class="graybg">THREE.MeshBasicMaterial</span></div>
<p>该材质不考虑场景中的光源，目标物体被渲染成简单的、扁平（Flat）的形状。可选的，你可以显示物体的线框（Wireframe），线框由所有面的边构成。</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>color</td>
<td>材质的颜色</td>
</tr>
<tr>
<td>wireframe</td>
<td>是否显示线框。显示线框对于调试有帮助</td>
</tr>
<tr>
<td>Wireframelinewidth</td>
<td>线框的线条宽度</td>
</tr>
<tr>
<td>shading</td>
<td>
<p>定义如何着色，可选值THREE.SmoothShading、THREE.NoShading、THREE.FlatShadin</p>
<p>默认值THREE.SmoothShading，导致渲染平滑的渲染——例如平滑过渡颜色</p>
</td>
</tr>
<tr>
<td>vertexColors</td>
<td>可以定义各个顶点的颜色，默认值THREE.NoColors。你可以设置为THREE.VertexColors，这样渲染器会考虑Geometry.colors属性</td>
</tr>
<tr>
<td>fog</td>
<td>该材质是否被全局迷雾效果影响</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">THREE.MeshDepthMaterial</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>wireframe</td>
<td>是否显示线框</td>
</tr>
<tr>
<td>wireframeLineWidth</td>
<td>线框的宽度</td>
</tr>
</tbody>
</table>
<p>设置相机的near、far属性，可以决定使用此材质的物体的淡出速度 。如果far - near很大，则物体的淡出速度非常慢。</p>
<div class="blog_h3"><span class="graybg">THREE.MeshNormalMaterial</span></div>
<p>每个面显示特定的颜色，其颜色取决于该面的法线（垂直于面的向量）。当物体旋转时，其固定角度的颜色保持不变。</p>
<p>法线在Three.js中被大量使用，它被用来确定光线反射效果、帮助映射纹理到3D模型，并且为如何照亮、shade、染色（color）一个表面上的像素点。</p>
<p>为了查看法线的方向，我们可以使用THREE.ArrowHelper：</p>
<pre class="crayon-plain-tag">//遍历球体的所有面
for ( var f = 0, fl = sphere.geometry.faces.length; f &lt; fl; f++ ) {
    var face = sphere.geometry.faces[ f ];
    // 计算面的中心点：把面的3个顶点依次加到三维向量中，然后除以3
    var centroid = new THREE.Vector3( 0, 0, 0 );
    centroid.add( sphere.geometry.vertices[ face.a ] );
    centroid.add( sphere.geometry.vertices[ face.b ] );
    centroid.add( sphere.geometry.vertices[ face.c ] );
    centroid.divideScalar( 3 );
    // 创建一个箭头助手
    var arrow = new THREE.ArrowHelper(
        face.normal,  // 法线矢量（箭头方向）
        centroid,   // 中心点 （箭头起点）
        2, // 长度
        0x3333FF, // 颜色
        0.5,  //箭头长度
        0.5 //箭头宽度
    );
    sphere.add( arrow );
}</pre>
<p>该材质的额外属性包括：wireframe、wireframeLineWidth、shading。 使用FlatShading、SmoothShading的效果分别如下图：</p>
<p><img class="aligncenter size-full wp-image-14459" src="https://blog.gmem.cc/wp-content/uploads/2017/01/shading-diff.png" alt="shading-diff" width="582" height="297" /></p>
<div class="blog_h3"><span class="graybg">THREE.MeshFaceMaterial</span></div>
<p>这不是一个单独的材质，而是一个容器。使用它，你可以为每个面指定材质。例如，对于具有12个面（Three.js仅支持三角形面）的Cube，你可以指定具有12个元素的MeshFaceMaterial：</p>
<pre class="crayon-plain-tag">var mats = [];
mats.push(new THREE.MeshBasicMaterial({color: 0x009e60}));
mats.push(new THREE.MeshBasicMaterial({color: 0x009e60}));
mats.push(new THREE.MeshBasicMaterial({color: 0x0051ba}));
mats.push(new THREE.MeshBasicMaterial({color: 0x0051ba}));
mats.push(new THREE.MeshBasicMaterial({color: 0xffd500}));
mats.push(new THREE.MeshBasicMaterial({color: 0xffd500}));
mats.push(new THREE.MeshBasicMaterial({color: 0xff5800}));
mats.push(new THREE.MeshBasicMaterial({color: 0xff5800}));
mats.push(new THREE.MeshBasicMaterial({color: 0xC41E3A}));
mats.push(new THREE.MeshBasicMaterial({color: 0xC41E3A}));
mats.push(new THREE.MeshBasicMaterial({color: 0xffffff}));
mats.push(new THREE.MeshBasicMaterial({color: 0xffffff}));

var faceMaterial = new THREE.MeshFaceMaterial(mats);

var cubeGeom = new THREE.BoxGeometry( 2.9, 2.9, 2.9 );
var cube = new THREE.Mesh( cubeGeom, faceMaterial );</pre>
<p>你可以设置<pre class="crayon-plain-tag">geometry.faces[*].materialIndex</pre>  来指名某个面使用MeshFaceMaterial中的哪个元素来渲染。</p>
<div class="blog_h2"><span class="graybg">联合多个材质</span></div>
<p>像MeshDepthMaterial这样的材质，不能设置颜色或者纹理，基本不能单独使用。</p>
<p>Three.js允许联合使用多个材质，以产生新的特效。<span style="background-color: #c0c0c0;">材质联合也使混合（blending）有意义</span>。示例：</p>
<pre class="crayon-plain-tag">var cubeMaterial = new THREE.MeshDepthMaterial();
var colorMaterial = new THREE.MeshBasicMaterial( {
    // 绿色材质
    color: 0x00ff00,
    // 允许透明度
    transparent: true,
    // 决定如何与背景（即使用了MeshDepthMaterial的那个内部盒子）进行交互
    // MultiplyBlending将前景、背景色进行乘积运算（正片叠底）
    blending: THREE.MultiplyBlending
} );
// 创建两个物体构成的组
var cube = new THREE.SceneUtils.createMultiMaterialObject( cubeGeometry, [ colorMaterial, cubeMaterial ] );
// 避免两个相同大小的重叠物体产生闪烁
cube.children[ 1 ].scale.set( 0.99, 0.99, 0.99 );</pre>
<div class="blog_h2"><span class="graybg">高级材质</span></div>
<div class="blog_h3"><span class="graybg">THREE.MeshLambertMaterial </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>ambient</td>
<td>材质的阴影色，与AmbientLight配合。AmbientLight的颜色与该颜色进行乘积混合（正片叠底），默认白色</td>
</tr>
<tr>
<td>emissive</td>
<td>材质发出的光线的颜色，注意这不会让材质成为光源，只是一个不会被其它光源影响的颜色而已，默认黑色 </td>
</tr>
<tr>
<td>wrapAround</td>
<td>
<p>设置为true则启用半环境光（half-lambert lighting）技术——光线的减弱行为更加微妙。如果你的Mesh具有尖锐、黑暗的区域，设置为true可以柔化阴影、更均匀的分散（distribute）光线</p>
</td>
</tr>
<tr>
<td>wrapRGB</td>
<td>当wrapAround设置为true时，使用一个THREE.Vector3来控制光线减弱（drop off）的速度，可以用来微调物体的色泽</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">THREE.MeshPhongMaterial</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>ambient</td>
<td>材质的阴影色，与AmbientLight配合。AmbientLight的颜色与该颜色进行乘积混合（正片叠底），默认白色</td>
</tr>
<tr>
<td>emissive</td>
<td>材质发出的光线的颜色，注意这不会让材质成为光源，只是一个不会被其它光源影响的颜色而已，默认黑色</td>
</tr>
<tr>
<td>specular</td>
<td>
<p>材质的高光色，即反光的颜色。如果将其设置：</p>
<ol>
<li>和color属性相同，可以得到金属质感（metallic-looking）的材质</li>
<li>为灰色，可以得到塑料质感（plastic-looking）的材质</li>
</ol>
</td>
</tr>
<tr>
<td>shininess</td>
<td>高光色的亮度，默认30</td>
</tr>
<tr>
<td>metal</td>
<td>设置为true，则Three.js更改算法，让材质更加像金属</td>
</tr>
<tr>
<td>wrapAround</td>
<td>
<p>设置为true则启用半环境光（half-lambert lighting）技术——光线的减弱行为更加微妙。如果你的Mesh具有尖锐、黑暗的区域，设置为true可以柔化阴影、更均匀的分散（distribute）光线</p>
</td>
</tr>
<tr>
<td>wrapRGB</td>
<td>当wrapAround设置为true时，使用一个THREE.Vector3来控制光线减弱（drop off）的速度，可以用来微调物体的色泽</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">THREE.ShaderMaterial</span></div>
<p>基于这种材质，可以应用自己开发的着色器。通过定制着色器，你可以精确的定义物体如何被渲染，或者修改Threee.js的默认渲染行为。</p>
<p>ShaderMaterial支持wireframe、Wireframelinewidth、linewidth、shading、vertexColors、fog以及以下额外属性：</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>fragmentShader</td>
<td>
<p>使用的片断着色器程序的名称</p>
<p>片断着色器，也叫像素着色器（pixel shader）。用于定义顶点之间每个点如何渲染</p>
</td>
</tr>
<tr>
<td>vertexShader</td>
<td>
<p>使用的顶点着色器程序的名称</p>
<p>顶点着色器，可以操控顶点的属性（例如改变顶点位置）</p>
<p>如果你像让多边形为全红色，可以基于此着色器，指定所有顶点为红色（此颜色信息会传递给片断着色器）。反之，如果你想在顶点之间产生渐变效果，则需要基于片断着色器</p>
<p>顶点着色器位于图形管线（graphic pipeline）的早期，在模型坐标转换、多边形修剪（clipping）之前，此时实际渲染工作并为开始</p>
</td>
</tr>
<tr>
<td>uniforms</td>
<td>用于向着色器程序发送信息，相同的信息被传递给每个vertex、fragment</td>
</tr>
<tr>
<td>defines </td>
<td>转换为#define代码片断，设置一些全局变量供着色器程序使用</td>
</tr>
<tr>
<td>attributes</td>
<td>用于传递位置性的、法线相关的信息。如果使用该属性，必须为每个顶点提供</td>
</tr>
<tr>
<td>lights</td>
<td>是否把光照数据传入着色器，默认false</td>
</tr>
</tbody>
</table>
<p>对于前面已经讨论过的其它材质，Three.js已经提供了它们的片断着色器、顶点着色器。</p>
<div class="blog_h3"><span class="graybg">GSGL</span></div>
<p>着色器不是基于JavaScript语言编写的，它的专用语言是GSGL，即OpenGL ES着色器语言的WebGL支持。这种语言的语法风格类似于C语言。</p>
<div class="blog_h3"><span class="graybg">示例一：动画材质</span></div>
<p>在本节，我们编写：</p>
<ol>
<li>一个简单顶点着色器。该着色器能够修改Cube顶点的坐标值</li>
<li>多个借用自<a href="http://glslsandbox.com/">glslsandbox</a>代码的片断着色器，创建具有动画效果的材质</li>
</ol>
<p>顶点着色器代码：</p>
<pre class="crayon-plain-tag">&lt;script id="vertex-shader" type="x-shader/x-vertex"&gt;
    // 外部传入的时间
    uniform float time;
    varying vec2 vUv;


    void main()
    {
        // 计算变换后的位置
        vec3 posChanged = position;
        posChanged.x = posChanged.x*(abs(sin(time*1.0)));
        posChanged.y = posChanged.y*(abs(cos(time*1.0)));
        posChanged.z = posChanged.z*(abs(sin(time*1.0)));
        gl_Position = projectionMatrix * modelViewMatrix * vec4(posChanged,1.0);
    }

&lt;/script&gt;</pre>
<p>为了JavaScript与着色器之间的通信，我们使用所谓uniforms。上面的例子中定义了一个uniform，传递外部的时间，根据此时间来变换顶点的位置。</p>
<p><pre class="crayon-plain-tag">gl_Position</pre> 是一个特殊变量，用于将顶点位置信息传回JavaScript。</p>
<p>其中一个片断着色器代码：</p>
<pre class="crayon-plain-tag">&lt;script id="fragment-shader-6" type="x-shader/x-fragment"&gt;


    uniform float time;
    uniform vec2 resolution;


    void main( void )
    {
        vec2 uPos = ( gl_FragCoord.xy / resolution.xy );

        uPos.x -= 1.0;
        uPos.y -= 0.5;

        vec3 color = vec3(0.0);
        float vertColor = 2.0;
        for( float i = 0.0; i &lt; 15.0; ++i )
        {
        float t = time * (0.9);

        uPos.y += sin( uPos.x*i + t+i/2.0 ) * 0.1;
        float fTemp = abs(1.0 / uPos.y / 100.0);
        vertColor += fTemp;
        color += vec3( fTemp*(10.0-i)/10.0, fTemp*i/10.0, pow(fTemp,1.5)*1.5 );
        }

        vec4 color_final = vec4(color, 1.0);
        // 把颜色传递回JavaScript
        gl_FragColor = color_final;
    }

&lt;/script&gt; </pre>
<p>材质的创建，可以基于以下助手函数：</p>
<pre class="crayon-plain-tag">function createMaterial( vertexShader, fragmentShader ) {
    // 从HTML标签中读取着色器源码vertexShader、fragmentShader为脚本标签的ID
    var vertShader = document.getElementById( vertexShader ).innerHTML;
    var fragShader = document.getElementById( fragmentShader ).innerHTML;

    var attributes = {};
    // 向着色器传递变量
    var uniforms = {
        time: { type: 'f', value: 0.2 },
        scale: { type: 'f', value: 0.2 },
        alpha: { type: 'f', value: 0.6 },
        resolution: { type: "v2", value: new THREE.Vector2() }
    };

    uniforms.resolution.value.x = window.innerWidth;
    uniforms.resolution.value.y = window.innerHeight;

    // 创建一个ShaderMaterial材质
    var meshMaterial = new THREE.ShaderMaterial( {
        uniforms: uniforms,
        attributes: attributes,
        vertexShader: vertShader,
        fragmentShader: fragShader,
        transparent: true

    } );

    return meshMaterial;
}</pre>
<p>渲染循环中，我们需要改变uniform，从而导致着色器绘制结果发生变化，进而产生动画效果：</p>
<pre class="crayon-plain-tag">function render() {
    // 递增time
    cube.material.materials.forEach( function ( e ) {
        e.uniforms.time.value += 0.01;
    } );

    requestAnimationFrame( render );
    renderer.render( scene, camera );
}</pre>
<div class="blog_h2"><span class="graybg">线形几何图形的材质</span></div>
<p>有两类仅仅支持用在线条（THREE.Line）的材质。线条这种特殊的Geometry仅仅具有顶点，而没有面。</p>
<div class="blog_h3"><span class="graybg">THREE.LineBasicMaterial</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>color</td>
<td>线条的颜色</td>
</tr>
<tr>
<td>linewidth</td>
<td>线条的宽度</td>
</tr>
<tr>
<td>vertexColors</td>
<td>设置各个顶点的颜色为THREE.VertexColors类型。覆盖color属性</td>
</tr>
<tr>
<td>fog</td>
<td>是否受到全局迷雾的影响</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">THREE.LineDashedMaterial</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>scale</td>
<td>虚线条、线条间隔的缩放比例</td>
</tr>
<tr>
<td>dashSize</td>
<td>虚线条的大小</td>
</tr>
<tr>
<td>gapSize</td>
<td>线条间隔的大小</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">使用几何图形</span></div>
<p>Three.js内置了大量的Geometry，可以开箱即用。 本章介绍其中的二维、三维Geometry，线条类不再介绍。</p>
<div class="blog_h2"><span class="graybg">二维几何图形</span></div>
<p>二维图形的初始摆放位置是X-Y平面，但是很多情况下需要需要将它们（特别是PlaneGeometry）放置到“地面”上，也就是X-Z平面上。此时可以让它绕着X轴逆时针旋转90度：</p>
<pre class="crayon-plain-tag">mesh.rotation.x =- Math.PI/2; </pre>
<div class="blog_h3"><span class="graybg">THREE.PlaneGeometry</span></div>
<p>外观上是一个矩形。示例：</p>
<pre class="crayon-plain-tag">new THREE.PlaneGeometry(width, height, widthSegments, heightSegments);</pre>
<p>可用属性：</p>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">属性</td>
<td style="width: 28px; text-align: center;">必</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td> width</td>
<td style="text-align: center;">Y</td>
<td>矩形的宽度</td>
</tr>
<tr>
<td> height</td>
<td style="text-align: center;">Y </td>
<td>矩形的高度 </td>
</tr>
<tr>
<td> widthSegments</td>
<td style="text-align: center;">N </td>
<td>宽方向上分段的数量，默认1 </td>
</tr>
<tr>
<td> heightSegments</td>
<td style="text-align: center;">N </td>
<td>高方向上分段的数量，默认1 </td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">THREE.CircleGeometry</span></div>
<p>外观上是一个圆形或者扇形。示例：</p>
<pre class="crayon-plain-tag">// 半径为3的圆
new THREE.CircleGeometry(3, 12);
// 半径为3的半圆
new THREE.CircleGeometry(3, 12, 0, Math.PI);</pre>
<p>可用属性：</p>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">属性</td>
<td style="width: 28px; text-align: center;">必</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>radius</td>
<td style="text-align: center;">N</td>
<td>圆的半径，默认50</td>
</tr>
<tr>
<td>segments</td>
<td style="text-align: center;">N</td>
<td>分段数，定义了构成圆的面的数量，最小值3，默认值8。更大的面数意味着更平滑的边缘</td>
</tr>
<tr>
<td>thetaStart</td>
<td style="text-align: center;">N</td>
<td>从什么角度绘制起始扇边，默认0，支持范围0 ～ 2 * PI</td>
</tr>
<tr>
<td>thetaLength</td>
<td style="text-align: center;">N</td>
<td>从什么角度绘制终止扇边，默认 2 * PI，支持范围0 ～ 2 * PI</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">THREE.RingGeometry</span></div>
<p>外观上是一个圆环或者扇环。示例：</p>
<pre class="crayon-plain-tag">Var ring = new THREE.RingGeometry();</pre>
<p>可用属性： </p>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">属性</td>
<td style="width: 28px; text-align: center;">必</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>innerRadius</td>
<td style="text-align: center;">N</td>
<td>内半径，默认0</td>
</tr>
<tr>
<td>outerRadius</td>
<td style="text-align: center;">N</td>
<td>外半径，默认50</td>
</tr>
<tr>
<td>thetaSegments</td>
<td style="text-align: center;">N</td>
<td>分段数，定义了构成环的面的数量。影响圆弧的平滑度</td>
</tr>
<tr>
<td>phiSegments</td>
<td style="text-align: center;">N</td>
<td>不影响圆环的平滑度，但是可以增加其构成面的数量</td>
</tr>
<tr>
<td>thetaStart</td>
<td style="text-align: center;">N</td>
<td>从什么角度绘制起始扇边，默认0，支持范围0 ～ 2 * PI</td>
</tr>
<tr>
<td>thetaLength</td>
<td style="text-align: center;">N</td>
<td>从什么角度绘制终止扇边，默认 2 * PI，支持范围0 ～ 2 * PI</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">THREE.ShapeGeometry</span></div>
<p>该形状允许你创建自定义的二维图形，其操作方式类似于SVG/Canvas中的画布。 示例：</p>
<pre class="crayon-plain-tag">var shape = new THREE.Shape();

// 移动画笔到指定的点
shape.moveTo( 10, 10 );

// 向Y方向画30像素的线段
shape.lineTo( 10, 40 );

// 绘制贝塞尔曲线
shape.bezierCurveTo( 15, 25, 25, 25, 30, 40 );

// 绘制拟合曲线
shape.splineThru( [
    new THREE.Vector2( 32, 30 ),
    new THREE.Vector2( 28, 20 ),
    new THREE.Vector2( 30, 10 ),
] );

// 绘制二次方曲线
shape.quadraticCurveTo( 20, 15, 10, 10 );

// 添加一个路径到形状中，挖洞
var hole1 = new THREE.Path();
hole1.absellipse( 16, 24, 2, 3, 0, Math.PI * 2, true );
shape.holes.push( hole1 );
// 再挖一个洞
var hole2 = new THREE.Path();
hole2.absellipse( 23, 24, 2, 3, 0, Math.PI * 2, true );
shape.holes.push( hole2 );
// 再一个挖洞
var hole3 = new THREE.Path();
hole3.absarc( 20, 16, 2, 0, Math.PI, true );
shape.holes.push( hole3 );

// 基于上述形状创建Geometry对象
new THREE.ShapeGeometry( shape );</pre>
<p>运行结果示意图：</p>
<p><img class="aligncenter size-full wp-image-14474" src="https://blog.gmem.cc/wp-content/uploads/2017/01/ShapeGeometry.png" alt="shapegeometry" width="406" height="431" />ShapeGeometry支持以下属性：</p>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">属性</td>
<td style="width: 28px; text-align: center;">必</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>shapes</td>
<td style="text-align: center;">Y</td>
<td>构成此Geometry的一个或者多个THREE.Shape对象，可以传入数组</td>
</tr>
<tr>
<td>options</td>
<td style="text-align: center;">N</td>
<td>
<p>应用到所有THREE.Shape的选项：</p>
<ol>
<li>curveSegments，决定了曲线的平滑程度，默认12</li>
<li>material，使用MeshFaceMaterial时，指定该形状使用的materialIndex</li>
<li>UVGenerator，当为材质指定纹理时，指定UV Mapping——决定纹理的哪个部分给哪个面使用。默认THREE.ExtrudeGeometry.WorldUVGenerator</li>
</ol>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">THREE.Shape</span></div>
<p>该类型是THREE.ShapeGeometry的最重要的部分，允许你创建自定义的形状。它提供以下方法和属性：</p>
<table class=" full-width fixed-word-wrap">
<tbody>
<tr>
<td><strong>moveTo(x,y)  </strong>移动画笔到指定的位置</td>
</tr>
<tr>
<td><strong>lineTo(x,y)</strong>  从当前位置向(x,y)绘制直线</td>
</tr>
<tr>
<td>
<p><strong>quadraticCurveTo(aCPx, aCPy, x, y)</strong><br /><strong>bezierCurveTo(aCPx1, aCPy1, aCPx2, aCPy2, x, y)</strong></p>
<p>你可以使用两种方式绘制曲线：二次曲线、贝塞尔曲线。两种方式的不同之处在于如何指定曲线的曲率（curvature）。下图显示这两种曲线的差别：</p>
<p><img class="aligncenter size-full wp-image-14478" src="https://blog.gmem.cc/wp-content/uploads/2017/01/curve.png" alt="curve" width="430" height="154" /></p>
<p>除了曲线的两个端点以外，对于：</p>
<ol>
<li>二次曲线，你需要提供额外的一个点(aCPx, aCPy)，这个点决定了曲线的曲率</li>
<li>三次曲线（贝塞尔曲线），你需要提供额外的两个点(aCPx1, aCPy1, aCPx2, aCPy2)</li>
</ol>
<p>注意：起点都是画笔当前位置，不需要在参数中指定</p>
</td>
</tr>
<tr>
<td><strong>splineThru(pts)</strong><br />在一系列点之间绘制流线型（拟合）的曲线，参数必须是THREE.Vector2对象的数组</td>
</tr>
<tr>
<td><strong>arc(aX, aY, aRadius, aStartAngle, aEndAngle, aClockwise)</strong><br />绘制一个圆圈或者圆弧。(aX, aY)指定离开当前画笔位置的偏移量，aRadius表示半径，(aStartAngle, aEndAngle)表示起始、终止角度，aClockwise为布尔值，表示是否顺时针绘制</td>
</tr>
<tr>
<td><strong>absArc(aX, aY, aRadius, aStartAngle, aEndAngle,AClockwise)</strong><br />在绝对位置上绘制圆弧</td>
</tr>
<tr>
<td><strong>ellipse(aX, aY, xRadius, yRadius, aStartAngle, aEndAngle, aClockwise)</strong><br />绘制椭圆或者部分椭圆</td>
</tr>
<tr>
<td><strong>absellipse(aX, aY, xRadius, yRadius, aStartAngle, aEndAngle, aClockwise)</strong><br />在绝对位置上绘制椭圆或者部分椭圆</td>
</tr>
<tr>
<td><strong>fromPoints(vectors)</strong><br />根据THREE.Vector2或者THREE.Vector3数组绘制路径</td>
</tr>
<tr>
<td><strong>holes</strong><br />THREE.Shape对象的数组，表示在当前形状上挖去的洞</td>
</tr>
<tr>
<td><strong>makeGeometry(options)<br /></strong>基于此形状生成一个THREE.ShapeGeometry对象<strong><br /></strong></td>
</tr>
<tr>
<td><strong>createPointsGeometry(divisions)<br /></strong>把形状转换为一系列采样点的数组，divisions指定点的数量。你可以基于这些点生成一个线条对象：<br />
<pre class="crayon-plain-tag">new THREE.Line( 
    shape.createPointsGeometry(10), new
    THREE.LineBasicMaterial( { color: 0xff3333, linewidth: 2 } ) 
);</pre>
</td>
</tr>
<tr>
<td><strong>createSpacedPointsGeometry(divisions)</strong> <br />与上面类似，但是生成一个Path对象</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">三维几何图形</span></div>
<div class="blog_h3"><span class="graybg">THREE.BoxGeometry</span></div>
<p>这是一个非常简单的三维图形，具有长宽高的盒子：</p>
<pre class="crayon-plain-tag">new THREE.BoxGeometry(10,10,10);</pre>
<p>可用属性：</p>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">属性</td>
<td style="width: 28px; text-align: center;">必</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>width</td>
<td style="text-align: center;">Y</td>
<td>宽度，沿着X轴</td>
</tr>
<tr>
<td>height</td>
<td style="text-align: center;">Y</td>
<td>高度，沿着Y轴</td>
</tr>
<tr>
<td>depth</td>
<td style="text-align: center;">Y</td>
<td>长度，沿着Z轴</td>
</tr>
<tr>
<td>widthSegments</td>
<td style="text-align: center;">N</td>
<td rowspan="3">在三个方向上的分段数</td>
</tr>
<tr>
<td>heightSegments</td>
<td style="text-align: center;">N</td>
</tr>
<tr>
<td>depthSegments</td>
<td style="text-align: center;">N</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">THREE.SphereGeometry</span></div>
<p>基于此类型，你可以绘制三维球体、不完整球体：</p>
<p><img class="aligncenter size-full wp-image-14482" src="https://blog.gmem.cc/wp-content/uploads/2017/01/sphere.png" alt="sphere" width="676" height="430" />可以看到，你可以截取球体经度、纬度方向的任意片断。</p>
<p>代码示例：</p>
<pre class="crayon-plain-tag">new THREE.SphereGeometry(radius,widthSegments,heightSegments,phiStart,phiLength,thetaStart,thetaLength) </pre>
<p>该类型提供以下属性：</p>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">属性</td>
<td style="width: 28px; text-align: center;">必</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>radius</td>
<td style="text-align: center;">N</td>
<td>球体的半径，默认50</td>
</tr>
<tr>
<td>widthSegments</td>
<td style="text-align: center;">N</td>
<td>垂直方向的分段数，默认8</td>
</tr>
<tr>
<td>heightSegments</td>
<td style="text-align: center;">N</td>
<td>水平方向的分段数，默认8</td>
</tr>
<tr>
<td>phiStart</td>
<td style="text-align: center;">N</td>
<td>在经度方向上，绘制球体的起点，向东绘制。范围0 ~ 2*PI</td>
</tr>
<tr>
<td>phiLength</td>
<td style="text-align: center;">N</td>
<td>在经度方向上，绘制的长度。范围0 ~ 2*PI</td>
</tr>
<tr>
<td>thetaStart</td>
<td style="text-align: center;">N</td>
<td>在纬度方向上，绘制球体的起点，向南绘制。范围0 ~ 2*PI</td>
</tr>
<tr>
<td>thetaLength</td>
<td style="text-align: center;">N</td>
<td>在纬度方向上，绘制的长度。范围0 ~ 2*PI</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">THREE.CylinderGeometry</span></div>
<p>可以绘制圆柱、圆筒、圆锥或者截锥。代码示例：</p>
<pre class="crayon-plain-tag">new THREE.CylinderGeometry(radiusTop,radiusBottom,height,radialSegments,heightSegments,openEnded)</pre>
<p>可用属性：</p>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">属性</td>
<td style="width: 28px; text-align: center;">必</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>radiusTop</td>
<td style="text-align: center;">N</td>
<td>上半径</td>
</tr>
<tr>
<td>radiusBottom</td>
<td style="text-align: center;">N</td>
<td>下半截</td>
</tr>
<tr>
<td>height</td>
<td style="text-align: center;">N</td>
<td>高度</td>
</tr>
<tr>
<td>radialSegments</td>
<td style="text-align: center;">N</td>
<td>在上下底方向上的分段数，决定光滑度</td>
</tr>
<tr>
<td>heightSegments</td>
<td style="text-align: center;">N</td>
<td>在高度方向上的分段数</td>
</tr>
<tr>
<td>openEnded</td>
<td style="text-align: center;">N</td>
<td>是否上下底开放，默认false</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">THREE.TorusGeometry</span></div>
<p>类似于甜甜圈的圆环面。代码示例：</p>
<pre class="crayon-plain-tag">new THREE.TorusGeometry(radius, tube, radialSegments,tubularSegments,arc)</pre>
<p> 可用属性：</p>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">属性</td>
<td style="width: 28px; text-align: center;">必</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>radius</td>
<td style="text-align: center;">N</td>
<td>外半径</td>
</tr>
<tr>
<td>tube</td>
<td style="text-align: center;">N</td>
<td>甜甜圈管道的半径</td>
</tr>
<tr>
<td>radialSegments</td>
<td style="text-align: center;">N</td>
<td rowspan="2">分段数</td>
</tr>
<tr>
<td>tubularSegments</td>
<td style="text-align: center;">N</td>
</tr>
<tr>
<td>arc</td>
<td style="text-align: center;">N</td>
<td>弧度，决定是不是绘制完整的甜甜圈，最大值2 * PI</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">高级图形及二进制操作</span></div>
<div class="blog_h2"><span class="graybg">THREE.ConvexGeometry</span></div>
<p>创建基于若干点的最小化凸面体。该形状不是Three.js核心库的组成部分。</p>
<div class="blog_h2"><span class="graybg">THREE.LatheGeometry</span></div>
<p>允许你基于一个光滑曲线来创建形状。此曲线由一系列的点（Knots）指定，通常是拟合曲线。曲线围绕对象的中心Z轴转动，可以产生类似于花瓶、钟之类的形状。示例：</p>
<p><img class="aligncenter size-full wp-image-14484" src="https://blog.gmem.cc/wp-content/uploads/2017/01/lathe.png" alt="lathe" width="566" height="560" /></p>
<p>可用属性：</p>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">属性</td>
<td style="width: 28px; text-align: center;">必</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>points</td>
<td style="text-align: center;">Y</td>
<td>绘制曲线的基准点</td>
</tr>
<tr>
<td>segments</td>
<td style="text-align: center;">N</td>
<td>分段数，数字越大则形状越光滑</td>
</tr>
<tr>
<td>phiStart</td>
<td style="text-align: center;">N</td>
<td>开始弧度</td>
</tr>
<tr>
<td>phiLength</td>
<td style="text-align: center;">N</td>
<td>绘制弧长</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">THREE.ExtrudeGeometry</span></div>
<p>可以把2D图形凸起、抬高为3D图形。比如我们可以把上面章节中的ShapeGeometry抬高：</p>
<pre class="crayon-plain-tag">var options = {
    amount: 10,
    bevelThickness: 2,
    bevelSize: 1,
    bevelSegments: 3,
    bevelEnabled: true,
    curveSegments: 12,
    steps: 1
};
new THREE.ExtrudeGeometry( drawShape(), options );</pre>
<p> 运行效果图如下：</p>
<p><img class="aligncenter size-full wp-image-14485" src="https://blog.gmem.cc/wp-content/uploads/2017/01/extrude-geometry.png" alt="extrude-geometry" width="95%" /></p>
<p>可用属性：</p>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">属性</td>
<td style="width: 28px; text-align: center;">必</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>shapes</td>
<td style="text-align: center;">Y</td>
<td>基于其进行凸起、抬高的THREE.Shape或者THREE.Shape数组</td>
</tr>
<tr>
<td>amount</td>
<td style="text-align: center;">N</td>
<td>抬高的高度，默认100</td>
</tr>
<tr>
<td>bevelThickness</td>
<td style="text-align: center;">N</td>
<td>
<p>在形状前面、后面，以及抬起的哪个侧面之间，创建一个斜坡</p>
<p>此厚度，即为圆滑斜坡给侧面“增加”的厚度的1/2</p>
</td>
</tr>
<tr>
<td>bevelSize</td>
<td style="text-align: center;">N</td>
<td>斜坡的高度，此高度导致从前/后面看形状，其面积变大</td>
</tr>
<tr>
<td>bevelSegments</td>
<td style="text-align: center;">N</td>
<td>斜坡分段数，让斜坡光滑</td>
</tr>
<tr>
<td>bevelEnabled</td>
<td style="text-align: center;">N</td>
<td>是否启用斜坡，默认启用</td>
</tr>
<tr>
<td>curveSegments</td>
<td style="text-align: center;">N</td>
<td>让曲线光滑</td>
</tr>
<tr>
<td>steps</td>
<td style="text-align: center;">N</td>
<td>凸起生成的面的分段数，默认1</td>
</tr>
<tr>
<td>extrudePath</td>
<td style="text-align: center;">N</td>
<td>沿着什么路径执行凸起，默认沿着Z轴，可以指定任意的路径</td>
</tr>
<tr>
<td>material</td>
<td style="text-align: center;">N</td>
<td>用作前后面的材质的索引，如果希望前后面使用不同材质可以调用THREE.SceneUtils.createMultiMaterialObject()</td>
</tr>
<tr>
<td>extrudeMaterial</td>
<td style="text-align: center;">N</td>
<td>凸起面和斜坡使用的材质的索引</td>
</tr>
<tr>
<td>uvGenerator</td>
<td style="text-align: center;">N</td>
<td>UVGenerator，当为材质指定纹理时，指定UV Mapping——决定纹理的哪个部分给哪个面使用。默认THREE.ExtrudeGeometry.WorldUVGenerator</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">THREE.TubeGeometry</span></div>
<p>与ExtrudeGeometry类似，这个类也是用于“凸起”的，只是它凸起的目标是3D的拟合曲线，而非2D图形。可用属性：</p>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">属性</td>
<td style="width: 28px; text-align: center;">必</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>path</td>
<td style="text-align: center;">Y</td>
<td>凸起的目标，一个THREE.SplineCurve3对象</td>
</tr>
<tr>
<td>segments</td>
<td style="text-align: center;">N</td>
<td>分段数，路径越长，该值应该越大，默认64</td>
</tr>
<tr>
<td>radius</td>
<td style="text-align: center;">N</td>
<td>管道的半径，默认1</td>
</tr>
<tr>
<td>radiusSegments</td>
<td style="text-align: center;">N</td>
<td>管道截面分段数，默认8</td>
</tr>
<tr>
<td>closed</td>
<td style="text-align: center;">N</td>
<td>是否闭合管道，默认false</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">THREE.ParametricGeometry</span></div>
<p>基于一个函数来生成几何图形。此函数有两个入参u、v，其返回值是一个三维向量，此向量作为几何图形的顶点，本质上是二维平面到三维空间的映射。</p>
<p>可用属性：</p>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">属性</td>
<td style="width: 28px; text-align: center;">必</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>function</td>
<td style="text-align: center;">Y</td>
<td>生成器函数，其返回值作为结果Geometry的顶点</td>
</tr>
<tr>
<td>slices</td>
<td style="text-align: center;">Y</td>
<td>u值被划分为多少子值，u的取值范围是0～1</td>
</tr>
<tr>
<td>stacks</td>
<td style="text-align: center;">Y</td>
<td>v值被划分为多少子值，v的取值范围是0～1</td>
</tr>
</tbody>
</table>
<p>示例：</p>
<pre class="crayon-plain-tag">var radialWave = function ( u, v ) {
    var r = 50;

    var x = Math.sin( u ) * r;
    var z = Math.sin( v / 2 ) * 2 * r;
    var y = (Math.sin( u * 4 * Math.PI ) + Math.cos( v * 2 * Math.PI )) * 2.8;

    return new THREE.Vector3( x, y, z );
};

var mesh = createMesh( new THREE.ParametricGeometry( radialWave, 120, 120, false ) );</pre>
<p>运行效果图：</p>
<p><img class="aligncenter size-full wp-image-14498" src="https://blog.gmem.cc/wp-content/uploads/2017/01/radialWave.png" alt="radialwave" width="400" height="294" /> </p>
<div class="blog_h2"><span class="graybg">THREE.TextGeometry</span></div>
<p>该类型用于创建凸起、抬升的3D文本。可用属性：</p>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">属性</td>
<td style="width: 28px; text-align: center;">必</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>size</td>
<td style="text-align: center;">N</td>
<td>文本的尺寸，默认100</td>
</tr>
<tr>
<td>height</td>
<td style="text-align: center;">N</td>
<td>凸起的高度</td>
</tr>
<tr>
<td>weight</td>
<td style="text-align: center;">N</td>
<td>粗体设置，可选值bold、normal</td>
</tr>
<tr>
<td>font</td>
<td style="text-align: center;">N</td>
<td>字体名称，默认helvetiker</td>
</tr>
<tr>
<td>style</td>
<td style="text-align: center;">N</td>
<td>字体样式，可选值normal、italic</td>
</tr>
<tr>
<td>bevelThickness</td>
<td style="text-align: center;">N</td>
<td rowspan="4">斜坡设置，默认不启用斜坡</td>
</tr>
<tr>
<td>bevelSize</td>
<td style="text-align: center;">N</td>
</tr>
<tr>
<td>bevelSegments</td>
<td style="text-align: center;">N</td>
</tr>
<tr>
<td>bevelEnabled</td>
<td style="text-align: center;">N</td>
</tr>
<tr>
<td>curveSegments</td>
<td style="text-align: center;">N</td>
<td>让曲线光滑</td>
</tr>
<tr>
<td>steps</td>
<td style="text-align: center;">N</td>
<td rowspan="5">参考ExtrudeGeometry</td>
</tr>
<tr>
<td>extrudePath</td>
<td style="text-align: center;">N</td>
</tr>
<tr>
<td>material</td>
<td style="text-align: center;">N</td>
</tr>
<tr>
<td>extrudeMaterial</td>
<td style="text-align: center;">N</td>
</tr>
<tr>
<td>uvGenerator</td>
<td style="text-align: center;">N</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">二进制操作</span></div>
<p>你可以把Three.js的标准几何图形联合起来，形成复杂的新图形，这种技术叫做CSG（Constructive Solid Geometry，构造实体几何）。</p>
<p>为了支持CSG，我们需要使用到Three.js扩展<a href="https://github.com/skalnik/ThreeBSP">ThreeBSP</a>。该库提供了以下函数：</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>intersect</td>
<td>基于两个既有Geometry的空间交叉部分（intersection）来生成新的Geometry</td>
</tr>
<tr>
<td>union</td>
<td>联合两个既有Geometry的空间，生成新的Geometry </td>
</tr>
<tr>
<td>subtract</td>
<td>从一个Geometry中挖去与另外一个Geometry重叠的部分，形成新的Geometry</td>
</tr>
</tbody>
</table>
<p>注意：这三个函数都基于Mesh的绝对位置执行计算。因此，如果你对组（或者应用多重材质）进行操作，可能得到意外的结果。</p>
<p>下面的代码示例了如何对两个球体进行二进制操作：</p>
<pre class="crayon-plain-tag">// 创建BSP对象
var sphere1BSP = new ThreeBSP( sphere1 );
var sphere2BSP = new ThreeBSP( sphere2 );

var resultBSP;

switch ( controls.actionSphere ) {
    case "subtract":
        resultBSP = sphere1BSP.subtract( sphere2BSP );
        break;
    case "intersect":
        resultBSP = sphere1BSP.intersect( sphere2BSP );
        break;
    case "union":
        resultBSP = sphere1BSP.union( sphere2BSP );
        break;
    case "none": // noop;
}
// 转换为Mesh并添加到场景
result = resultBSP.toMesh();
result.geometry.computeFaceNormals();
result.geometry.computeVertexNormals();
scene.add(result);</pre>
<div class="blog_h1"><span class="graybg">粒子和点云</span></div>
<p>在前面的章节中，我们以及了解了Three.js的大部分重要组件：场景、镜头、灯光、图形、材质。本章主要研究一个重要的，但是迄今为止尚未提及的重要概念——粒子。</p>
<p>粒子（particles）某些时候也称为精灵（sprites），是场景中的小物体。这些物体很容易被大量的创建，以模拟雨、雪、烟以及其它多种有趣的特效。</p>
<p>需要注意，在较近版本的Three.js中，与粒子相关的物体的类型名称从THREE.ParticleSystem变为THREE.PointCloud。粒子本身的类型名称从THREE.Particle变为THREE.Sprite。</p>
<div class="blog_h2"><span class="graybg">理解粒子</span></div>
<p>粒子是<span style="background-color: #c0c0c0;">2D的平面，该平面总是正向面对镜头</span>。下面的代码创建了100个粒子：</p>
<pre class="crayon-plain-tag">// 粒子的材质
var material = new THREE.SpriteMaterial();
for ( var x = -5; x &lt; 5; x++ ) {
    for ( var y = -5; y &lt; 5; y++ ) {
        // 创建粒子
        var sprite = new THREE.Sprite( material );
        // 设置其位置
        sprite.position.set( x * 10, y * 10, 0 );
        // 添加到场景
        scene.add( sprite );
    }
}</pre>
<p>当你不指定任何属性的时候，粒子被渲染为白色二维小方块。 所以，上面的代码会在场景中展示10 x 10的小方块阵列。</p>
<p>粒子接受的材质类型只有：THREE.SpriteCanvasMaterial、THREE.SpriteMaterial。</p>
<p>与Three.Mesh类似，THREE.Sprite也继承自THREE.Object3D。这意味着THREE.Mesh的很多属性/方法对于粒子也是可用的，你可以使用scale属性对其缩放、使用position让其移动。</p>
<div class="blog_h2"><span class="graybg">理解点云</span></div>
<p>虽然创建并移动粒子很简单，但是如果操控的粒子数量很大，你很快就会遇到性能问题。为此，Three.js提供了THREE.PointCloud用来统一处理大量的粒子。基于PointCloud的、与上面等效的代码如下：</p>
<pre class="crayon-plain-tag">var geom = new THREE.Geometry();
// 点云材质
var material = new THREE.PointCloudMaterial( {
    size: 4,
    vertexColors: true, color: 0xffffff
} );
for ( var x = -5; x &lt; 5; x++ ) {
    for ( var y = -5; y &lt; 5; y++ ) {
        // 每个粒子是三维空间中的一个点
        var particle = new THREE.Vector3( x * 10, y * 10, 0 );
        geom.vertices.push( particle );
        geom.colors.push( new THREE.Color( Math.random() * 0x00ffff ) );
    }
}
// 点的集合，点云
var cloud = new THREE.PointCloud( geom, material );
scene.add( cloud );</pre>
<p>要创建点云，需要两个参数：</p>
<ol>
<li>材质，使用颜色或者纹理来装饰粒子</li>
<li>Geometry， 指定所有粒子的位置</li>
</ol>
<p>下面再举一个例子：创建15000个随机亮度的绿色粒子构成的点云：</p>
<pre class="crayon-plain-tag">var geom = new THREE.Geometry();
var material = new THREE.PointCloudMaterial( {
    size: size,
    transparent: transparent,
    opacity: opacity,
    vertexColors: vertexColors,
    sizeAttenuation: sizeAttenuation,
    color: color
} );


var range = 500;
for ( var i = 0; i &lt; 15000; i++ ) {
    // 位置随机的粒子
    var particle = new THREE.Vector3( 
        Math.random() * range - range / 2, 
        Math.random() * range - range / 2, 
        Math.random() * range - range / 2 
    );
    geom.vertices.push( particle );
    var color = new THREE.Color( 0x00ff00 );
    // 以色相、饱和度、亮度的方式设置颜色。随机亮度的绿色
    color.setHSL( color.getHSL().h, color.getHSL().s, Math.random() * color.getHSL().l );
    // 顶点颜色数组
    geom.colors.push( color );

}</pre>
<p>当自动旋转点云时，你可以看到粒子满天飞舞的效果。</p>
<div class="blog_h3"><span class="graybg">THREE.PointCloudMaterial</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>color</td>
<td>点云（粒子系统）中所有粒子的颜色，如果vertexColors设置为true，并且设置了Geometry的colors属性，则该属性被覆盖</td>
</tr>
<tr>
<td>map</td>
<td>指定该属性，你可以为粒子设置纹理。使用该属性，你可以让粒子看起来更像真实世界中的粒子，例如雪花 </td>
</tr>
<tr>
<td>size</td>
<td>粒子的尺寸，默认1</td>
</tr>
<tr>
<td>sizeAnnutation</td>
<td>如果false，则所有粒子的大小一样。否则，其尺寸取决于粒子距离镜头的远近</td>
</tr>
<tr>
<td>vertexColors</td>
<td>默认情况下，点云中所有粒子的颜色一致，设置该属性为THREE.VertexColors则Geometry的colors属性被用来指定粒子的颜色。默认值THREE.NoColors </td>
</tr>
<tr>
<td>opacity</td>
<td>与transparent联用，设置粒子的透明度</td>
</tr>
<tr>
<td>transparent</td>
<td>默认false，如果设置为true，允许粒子具有透明度 </td>
</tr>
<tr>
<td>blending</td>
<td>渲染粒子时使用的混合模式 </td>
</tr>
<tr>
<td>fog </td>
<td>默认true，粒子是否被全局迷雾影响</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">基于HTML5画布来装饰粒子</span></div>
<p>你可以使用三种方式来基于HTML画布装饰（Style）粒子：</p>
<ol>
<li>如果使用THREE.CanvasRenderer，你可以直接通过THREE.SpriteCanvasMaterial引用HTML5画布对象</li>
<li>如果使用THREE.WebGLRenderer，你需要一些额外的步骤来使用HTML5画布</li>
</ol>
<div class="blog_h3"><span class="graybg">使用THREE.CanvasRenderer</span></div>
<p>在使用该渲染器时，你可以使用THREE.SpriteCanvasMaterial，直接把画布的输出作为粒子的纹理使用。SpriteCanvasMaterial这个材质是专门为CanvasRenderer准备的，支持以下属性：</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>color</td>
<td>粒子的颜色，依据混合模式的设置，该颜色会和画布中图片进行混合</td>
</tr>
<tr>
<td>program</td>
<td>一个函数，以画布上下文作为入参。在粒子渲染时该函数被调用，函数的输出被绘制为粒子</td>
</tr>
<tr>
<td>opacity</td>
<td>粒子的透明度</td>
</tr>
<tr>
<td>transparent</td>
<td>是否允许粒子透明</td>
</tr>
<tr>
<td>blending</td>
<td>使用的混合模式</td>
</tr>
<tr>
<td>rotation</td>
<td>用于旋转画布的内容</td>
</tr>
</tbody>
</table>
<p>示例：</p>
<pre class="crayon-plain-tag">var canvasRenderer = new THREE.CanvasRenderer();
// ...

// 抽取纹理的程序
var getTexture = function ( ctx ) {

    // the body
    ctx.translate( -81, -84 );

    ctx.fillStyle = "orange";
    ctx.beginPath();
    // ...
    ctx.fill();

};

// 粒子材质
var material = new THREE.SpriteCanvasMaterial( {
        program: getTexture,
        color: 0xffffff
    }
);
// 旋转
material.rotation = Math.PI;
// 创建粒子
var range = 500;
for ( var i = 0; i &lt; 1500; i++ ) {
    var sprite = new THREE.Sprite( material );
    sprite.position.set( /* random */ );
    sprite.scale.set( 0.1, 0.1, 0.1 );
    scene.add( sprite );
}</pre>
<div class="blog_h3"><span class="graybg">使用WebGLRenderer</span></div>
<p>使用该渲染器时， 你需要手工在内存中创建画布对象，完成2D图形绘制，并返回一个纹理对象：</p>
<pre class="crayon-plain-tag">var getTexture = function () {
    var canvas = document.createElement( 'canvas' );
    canvas.width = 32;
    canvas.height = 32;

    var ctx = canvas.getContext( '2d' );
    // ...

    // 返回一个纹理对象
    var texture = new THREE.Texture( canvas );
    texture.needsUpdate = true;
    return texture;
};

var geom = new THREE.Geometry();


var material = new THREE.PointCloudMaterial( {
    size: size,
    transparent: transparent,
    opacity: opacity,
    // 指定使用的THREE.Texture对象
    map: getTexture(),
    sizeAttenuation: sizeAttenuation,
    color: color
} );


var range = 500;
for ( var i = 0; i &lt; 5000; i++ ) {
    var particle = new THREE.Vector3( /* random */ );
    geom.vertices.push( particle );
}

cloud = new THREE.PointCloud( geom, material );</pre>
<div class="blog_h3"><span class="graybg">径向渐变的例子</span></div>
<p>下面的代码演示了如何使用Canvas绘制一个径向渐变的光球：</p>
<pre class="crayon-plain-tag">var canvas = document.createElement('canvas');
canvas.width = 16;
canvas.height = 16;

var context = canvas.getContext('2d');
var gradient = context.createRadialGradient(
    canvas.width / 2, canvas.height / 2, 0, canvas.width / 2, canvas.height / 2, canvas.width / 2
);
gradient.addColorStop(0, 'rgba(255,255,255,1)');
gradient.addColorStop(0.2, 'rgba(0,255,255,1)');
gradient.addColorStop(0.4, 'rgba(0,0,64,1)');
gradient.addColorStop(1, 'rgba(0,0,0,1)');

context.fillStyle = gradient;
context.fillRect(0, 0, canvas.width, canvas.height);

var texture = new THREE.Texture(canvas);
texture.needsUpdate = true;
return texture;</pre>
<p>可以使用此光球来装饰粒子，产生荧光那样的效果。 </p>
<div class="blog_h2"><span class="graybg">使用纹理装饰粒子</span></div>
<p>上一个粒子中，我们已经使用了纹理，纹理的图像从画布中抓取。</p>
<p>实际上，我们可以把任何图片作为纹理使用：</p>
<pre class="crayon-plain-tag">var texture = THREE.ImageUtils.loadTexture( "../assets/textures/particles/raindrop-3.png" );</pre>
<p>注意：作为纹理的图片，大小必须是2的N次方，必须是正方形。</p>
<div class="blog_h3"><span class="graybg">下雨的例子</span></div>
<p>下面使用该纹理模拟下雨效果：</p>
<pre class="crayon-plain-tag">var geom = new THREE.Geometry();

var material = new THREE.ParticleBasicMaterial( {
    size: size,
    transparent: transparent,
    opacity: opacity,
    map: texture,
    // 设置混合模式为相加，意味着雨滴图片中黑色背景部分（000000）不会被绘制——背景色 + 0 仍然是背景色
    // 使用透明背景的纹理是不支持的
    blending: THREE.AdditiveBlending,
    sizeAttenuation: sizeAttenuation,
    color: color
} );


var range = 40;
for ( var i = 0; i &lt; 1500; i++ ) {
    var particle = new THREE.Vector3(
        Math.random() * range - range / 2,
        Math.random() * range * 1.5,
        Math.random() * range - range / 2 
    );
    // 设置随机的速度属性，备用
    particle.velocityY = 0.1 + Math.random() / 5;
    particle.velocityX = (Math.random() - 0.5) / 3;
    geom.vertices.push( particle );
}

cloud = new THREE.ParticleSystem( geom, material );
cloud.sortParticles = true;

scene.add( cloud );


function render() {
    scene.children.forEach( function ( child ) {
        if ( child instanceof THREE.PointCloud ) {
            var vertices = child.geometry.vertices;
            // 在渲染循环中遍历处理所有粒子，根据速度设置其位置
            vertices.forEach( function ( v ) {
                v.y = v.y - (v.velocityY);
                v.x = v.x - (v.velocityX);
                // 如果粒子超出显示范围，则重置其位置
                if ( v.y &lt;= 0 ) v.y = 60;
                if ( v.x &lt;= -20 || v.x &gt;= 20 ) v.velocityX = v.velocityX * -1;
            } );
        }
    } );
    requestAnimationFrame( render );
    webGLRenderer.render( scene, camera );
}</pre>
<div class="blog_h3"><span class="graybg">下雪的例子</span></div>
<p>我们改进一下上面的例子，模拟更真实的下雪效果：</p>
<ol>
<li>建立多个点云，来模拟不同大小的雪花</li>
<li>在Z轴方向改变雪花的位置，模拟三维空间中雪花的飘舞</li>
</ol>
<p>代码如下：</p>
<pre class="crayon-plain-tag">function createPointCloud( name, texture, size, transparent, opacity, sizeAttenuation, color ) {
    var geom = new THREE.Geometry();

    var color = new THREE.Color( color );
    // 随机的改变亮度
    color.setHSL( color.getHSL().h, color.getHSL().s, (Math.random()) * color.getHSL().l );

    var material = new THREE.PointCloudMaterial( {
        size: size,
        transparent: transparent,
        opacity: opacity,
        map: texture,
        blending: THREE.AdditiveBlending,
        // 设置为false，表示对象不影响WebGL的depth buffer，这样不同的粒子系统就不会相互干扰
        depthWrite: false,
        sizeAttenuation: sizeAttenuation,
        color: color
    } );

    var range = 40;
    for ( var i = 0; i &lt; 50; i++ ) {
        var particle = new THREE.Vector3(
            Math.random() * range - range / 2,
            Math.random() * range * 1.5,
            Math.random() * range - range / 2 );
        // 雪花在三个轴的方向上都具有速度
        particle.velocityY = 0.1 + Math.random() / 5;
        particle.velocityX = (Math.random() - 0.5) / 3;
        particle.velocityZ = (Math.random() - 0.5) / 3;
        geom.vertices.push( particle );
    }

    var system = new THREE.PointCloud( geom, material );
    system.name = name;
    system.sortParticles = true;
    return system;
}
// 创建多个粒子系统
function createPointClouds( size, transparent, opacity, sizeAttenuation, color ) {

    var texture1 = THREE.ImageUtils.loadTexture( "../assets/textures/particles/snowflake1.png" );
    var texture2 = THREE.ImageUtils.loadTexture( "../assets/textures/particles/snowflake2.png" );
    var texture3 = THREE.ImageUtils.loadTexture( "../assets/textures/particles/snowflake3.png" );
    var texture4 = THREE.ImageUtils.loadTexture( "../assets/textures/particles/snowflake5.png" );

    scene.add( createPointCloud( "system1", texture1, size, transparent, opacity, sizeAttenuation, color ) );
    scene.add( createPointCloud( "system2", texture2, size, transparent, opacity, sizeAttenuation, color ) );
    scene.add( createPointCloud( "system3", texture3, size, transparent, opacity, sizeAttenuation, color ) );
    scene.add( createPointCloud( "system4", texture4, size, transparent, opacity, sizeAttenuation, color ) );
}

createPointClouds( controls.size, controls.transparent, controls.opacity, controls.sizeAttenuation, controls.color );

function render() {

    scene.children.forEach( function ( child ) {
        if ( child instanceof THREE.PointCloud ) {
            var vertices = child.geometry.vertices;
            vertices.forEach( function ( v ) {
                // 模拟三维飘落效果
                v.y = v.y - (v.velocityY);
                v.x = v.x - (v.velocityX);
                v.z = v.z - (v.velocityZ);

                if ( v.y &lt;= 0 ) v.y = 60;
                if ( v.x &lt;= -20 || v.x &gt;= 20 ) v.velocityX = v.velocityX * -1;
                if ( v.z &lt;= -20 || v.z &gt;= 20 ) v.velocityZ = v.velocityZ * -1;
            } );
        }
    } );

    requestAnimationFrame( render );
    webGLRenderer.render( scene, camera );
}</pre>
<div class="blog_h2"><span class="graybg">Sprite Map</span></div>
<p>我们可以把多个Sprite放在一个图片中，然后通过偏移量来加载、使用。就像CSS Sprite那样：</p>
<pre class="crayon-plain-tag">var spriteMaterial = new THREE.SpriteMaterial({
        opacity: opacity,
        color: color,
        transparent: transparent,
        map: getTexture() // 这个纹理中包含五个横向排列的小图片，我们只需要其中一个
    }
);
// 纹理图片中，X轴（u）\Y轴（v）方向的偏移量
// 如果spriteNumber取2，表示需要第三个小图片，u-offset = 0.2 * 2 = 0.4，即u偏移为0.4
// 注意u、v的最大值都是1，表示整个图片的大小，因此每个图片的大小是0.2
spriteMaterial.map.offset = new THREE.Vector2(0.2 * spriteNumber, 0);
// 如果不设置repeat，那么3、4、5几个小图都称为Sprite的一部分。而我们只需要第三个图片
// 1/5表示在u方向仅需要1/5长度，恰好是一个小图的大小
spriteMaterial.map.repeat = new THREE.Vector2(1 / 5, 1);
spriteMaterial.depthTest = false;

spriteMaterial.blending = THREE.AdditiveBlending;

var sprite = new THREE.Sprite(spriteMaterial);</pre>
<div class="blog_h2"><span class="graybg">从高级形状创建点云</span></div>
<p>点云基于你所提供的Geometry的顶点来渲染粒子。这意味着我们可以向它传递前面所学过的任何几何图形。</p>
<div class="blog_h1"><span class="graybg">创建、加载高级Mesh和Geometry</span></div>
<p>前面的章节我们了解到，可以通过ThreeBSP这个插件来创建复合的Mesh。本章我们将学习另外两种创建高级Geometry/Mesh的机制：</p>
<ol>
<li>分组、合并：Three.js支持内置的分组/合并机制，允许基于现存的对象来创建Mesh/Geometry</li>
<li>加载模型：Three.js支持从多种外部格式来加载Mesh/Geometry</li>
</ol>
<div class="blog_h2"><span class="graybg">分组多个Mesh</span></div>
<p>这个机制我们已经使用过，当为Geometry应用多个材质时，实际上Three.js就创建了组。</p>
<p>创建组非常容易，任何Mesh都可以包含子元素，你可以通过<pre class="crayon-plain-tag">add()</pre> 方法随时添加子元素：</p>
<pre class="crayon-plain-tag">sphere = createMesh(new THREE.SphereGeometry(5, 10, 10));
cube = createMesh(new THREE.BoxGeometry(6, 6, 6));

// 任何3D对象可以作为组的容器，Object3D是Mesh、Scene的超类
group = new THREE.Object3D();   // 最近版本的Three.js引入THREE.Group，专门用作组容器
// 向容器中添加其它Mesh
group.add(sphere);
group.add(cube);

scene.add(group); </pre>
<p>当你针对组中的父对象进行移动、缩放、旋转等操作时，所有子对象将会被应用相同的操作。需要强调的是旋转操作，执行旋转的时候，是整个组围绕组的中心进行旋转，而不是每个元素绕着各自的中心旋转。</p>
<p>使用组时，你依然可以对单个元素进行移动、缩放、旋转等操作。但是需要注意，这些操作都是相对于父对象进行的。</p>
<div class="blog_h2"><span class="graybg">合并多个Geometry</span></div>
<p>大部分情况下，使用分组可以让你方便的操控大量的Mesh。但是性能问题也可能出现，因为使用分组时，每个对象依然需要被单独的处理、渲染。</p>
<p>使用<pre class="crayon-plain-tag">THREE.Geometry.merge()</pre> 你可以合并多个几何图形，然后创建单个Mesh：</p>
<pre class="crayon-plain-tag">var geometry = new THREE.Geometry();
for ( var i = 0; i &lt; controls.numberOfObjects; i++ ) {
    var cubeMesh = createCube();
    cubeMesh.updateMatrix();
    // 提供被合并geometry的转换矩阵，确保geometry被正确的置位、旋转
    geometry.merge( cubeMesh.geometry, cubeMesh.matrix );
}
scene.add( new THREE.Mesh( geometry, cubeMaterial ) );</pre>
<div class="blog_h2"><span class="graybg">加载外部模型</span></div>
<p>使用编程方式来模拟真实世界中复杂的形状是困难的，Three.js允许加载3D建模软件设计的Geometry/Mesh。</p>
<div class="blog_h3"><span class="graybg">加载器</span></div>
<p>加载外部模型，是通过Three.js加载器（Loader）实现的。加载器把文本/二进制的模型文件转化为Three.js对象结构。</p>
<p>每个加载器理解某种特定的文件格式。</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>JSON</td>
<td>
<p>Three.js自定义的、基于JSON的格式。可以声明式的定义一个Geometry或者Scene</p>
<p>利用该格式，你可以方便的重用复杂的Geometry或Scene</p>
</td>
</tr>
<tr>
<td>OBJ / MTL</td>
<td>
<p>OBJ是Wavefront开发的一种简单3D格式，此格式被广泛的支持，用于定义Geometry</p>
<p>MTL用于配合OBJ，它指定OBJ使用的材质</p>
<p>Three.js提供了OBJExporter.js，使用它可以把Three.js模型导出为OBJ格式</p>
</td>
</tr>
<tr>
<td>Collada</td>
<td>基于XML的格式，被大量3D应用程序、渲染引擎支持</td>
</tr>
<tr>
<td>STL</td>
<td>
<p>STereoLithography的简写，在快速原型领域被广泛使用。3D打印模型通常使用该格式定义</p>
<p>Three.js提供了STLExporter.js，使用它可以把Three.js模型导出为STL格式</p>
</td>
</tr>
<tr>
<td>CTM</td>
<td>openCTM定义的格式，以紧凑的格式存储基于三角形的Mesh</td>
</tr>
<tr>
<td>VTK</td>
<td>Visualization Toolkit定义的格式，用于声明顶点和面。此格式有二进制/ASCII两种变体，Three.js仅支持ASCII变体</td>
</tr>
<tr>
<td>AWD</td>
<td>3D场景的二进制格式，主要被away3d引擎使用，Three.js不支持AWD压缩格式</td>
</tr>
<tr>
<td>Assimp</td>
<td>开放资产导入库（Open asset import library）是导入多种3D模型的标准方式。使用该Loader你可以导入多种多样的3D模型格式</td>
</tr>
<tr>
<td>VRML</td>
<td>
<p>虚拟现实建模语言（Virtual Reality Modeling Language）是一种基于文本的格式，现已经被X3D格式取代</p>
<p>尽管Three.js不直接支持X3D，但是后者很容易被转换为其它格式</p>
</td>
</tr>
<tr>
<td>Babylon</td>
<td>
<p>游戏引擎Babylon的私有格式</p>
</td>
</tr>
<tr>
<td>PLY</td>
<td>
<p>常用于存储来自3D扫描仪的信息</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">保存/加载JSON格式</span></div>
<p>使用Three.js的JSON格式，你可以保存/加载一个Mesh或者整个场景。</p>
<div class="blog_h3"><span class="graybg">保存/加载Mesh</span></div>
<pre class="crayon-plain-tag">knot = createMesh(new THREE.TorusKnotGeometry());
scene.add(knot);

// 保存
var result = knot.toJSON();
localStorage.setItem("json", JSON.stringify(result));

// 加载
var json = localStorage.getItem("json");
if (json) {
    var loadedGeometry = JSON.parse(json);
    var loader = new THREE.ObjectLoader();
    // 将JSON解析为Mesh
    loadedMesh = loader.parse(loadedGeometry);
    loadedMesh.position.x -= 50;
    scene.add(loadedMesh);
}</pre>
<div class="blog_h3"><span class="graybg">保存/加载场景</span></div>
<p>首先引入必要的脚本：</p>
<pre class="crayon-plain-tag">&lt;script type="text/javascript" src="../libs/SceneLoader.js"&gt;&lt;/script&gt;
&lt;script type="text/javascript" src="../libs/SceneExporter.js"&gt;&lt;/script&gt;</pre>
<p>代码示例：</p>
<pre class="crayon-plain-tag">var exporter = new THREE.SceneExporter();
var sceneJson = JSON.stringify(exporter.parse(scene));
localStorage.setItem('scene', sceneJson);


var json = (localStorage.getItem('scene'));
var sceneLoader = new THREE.SceneLoader();
// 最后一个参数 . 定义了相对URL，加载纹理时需要
sceneLoader.parse(JSON.parse(json), function(e) {
    scene = e.scene;
}, '.');</pre>
<div class="blog_h2"><span class="graybg">与Blender一起使用</span></div>
<p>市场上有大量3D建模软件，用来设计复杂的Mesh。开源领域比较流行的是Blender。</p>
<p>Three.js提供了针对Blender、Maya、3D Studio Max等流行软件的Exporter，可以把基于这些软件设计的模型直接导出为Three.js的JSON格式。 </p>
<p>Exporter并非Three.js支持Blender的唯一途径，因为Three.js本身理解多种3D格式，而Blender也支持保存为这些格式。</p>
<div class="blog_h3"><span class="graybg">安装Blender加载项</span></div>
<ol>
<li>复制/home/alex/JavaScript/three.js/utils/exporters/blender/addons目录到/home/alex/Applications/blender/2.78/scripts/addons</li>
<li>打开Blender，点击菜单栏File ⇨ User Preferences，选择Addons选项卡，搜索Three.js，勾选以启用：<a href="https://blog.gmem.cc/wp-content/uploads/2017/01/Blender-User-Preferences_001.png"><img class="aligncenter size-full wp-image-14519" src="https://blog.gmem.cc/wp-content/uploads/2017/01/Blender-User-Preferences_001.png" alt="blender-user-preferences_001" width="95%" /></a></li>
<li>点击File ⇨ Export，在弹出的菜单中应该可以看到Three.js项</li>
</ol>
<div class="blog_h3"><span class="graybg">从Blender导出</span></div>
<p>点击File ⇨ Open，你可以打开既有模型文件并编辑。点击File ⇨ Export ⇨ Three.js(json)，选择目标路径即可导出。</p>
<p>在导出对话框中，可以修改设置：</p>
<p><img class="aligncenter size-full wp-image-14522" src="https://blog.gmem.cc/wp-content/uploads/2017/01/BlenderExportSettings.png" alt="blenderexportsettings" width="369" height="98" /></p>
<p>这样，导出的JSON会包含材质的声明，并且模型使用的纹理自动导出为图片。</p>
<div class="blog_h3"><span class="graybg">导入到Three.js场景</span></div>
<pre class="crayon-plain-tag">var loader = new THREE.JSONLoader();
// 设置纹理的加载路径，JSON中仅仅包含纹理图片文件的名称，不包括目录前缀
loader.setTexturePath( './assets/' ); // 注意结尾的 /
// 异步的加载操作，回调函数提供两个入参：加载的Geometry、加载到的材质的数组
loader.load( './assets/chair.json', function ( geometry, materials ) {
    // 回调参数是THREE.Geometry，THREE.Material[]
    var material = new THREE.MultiMaterial( materials );
    var mesh = new THREE.Mesh( geometry, material );
    // 放大以便看清
    mesh.scale.x = 15;
    mesh.scale.y = 15;
    mesh.scale.z = 15;
    scene.add( mesh );

} ); </pre>
<div class="blog_h2"><span class="graybg">加载OBJ/MTL格式</span></div>
<p>此格式被Blender原生支持、Three.js也提供了相应的加载器。</p>
<p>首先引入必要的脚本：</p>
<pre class="crayon-plain-tag">&lt;script type="text/javascript" src="../libs/OBJLoader.js"&gt;&lt;/script&gt;
&lt;!-- 下面两个用于加载MTL --&gt;
&lt;script type="text/javascript" src="../libs/MTLLoader.js"&gt;&lt;/script&gt;
&lt;script type="text/javascript" src="../libs/OBJMTLLoader.js"&gt;&lt;/script&gt;</pre>
<div class="blog_h3"><span class="graybg">仅加载OBJ</span></div>
<pre class="crayon-plain-tag">var loader = new THREE.OBJLoader();
loader.load( '../assets/models/pinecone.obj', function ( loadedMesh ) {
    // 回调参数是一个THREE.Object3D对象
    var material = new THREE.MeshLambertMaterial( { color: 0x5C3A21 } );
    loadedMesh.children.forEach( function ( child ) {
        child.material = material;
        child.geometry.computeFaceNormals();
        child.geometry.computeVertexNormals();
    } );

    scene.add( loadedMesh );
} );</pre>
<p>一个好的实践是，在回调中打印加载对象的结构。通常，加载的Geometry/Mesh表现为层次化的Group。理解此Group的结构，以便正确的应用材质，并执行额外的处理步骤。</p>
<p>此外，注意查看顶点的位置信息，然后估算是否需要进行缩放、如何放置镜头。</p>
<p>对Geometry调用computeFaceNormals、computeVertexNormals，以确保材质被正确的渲染。</p>
<div class="blog_h3"><span class="graybg">同时加载MTL</span></div>
<p>如果你需要通过OBJ/MTL来加载模型，首先检查MTL的内容，确保它以相对路径来引用纹理图片。</p>
<p>下面的例子加载一个蝴蝶模型，需要注意，某些时候需要对材质进行微调：</p>
<pre class="crayon-plain-tag">var loader = new THREE.OBJMTLLoader();

loader.load( '../assets/models/butterfly.obj', '../assets/models/butterfly.mtl', function ( object ) {
    // 回调参数是一个THREE.Group对象

    var wing2 = object.children[ 5 ].children[ 0 ];
    var wing1 = object.children[ 4 ].children[ 0 ];

    // 模型源文件中，蝴蝶翅膀的透明度设置有误导致看不见，这里手工调整一下材质
    wing1.material.opacity = 0.6;
    wing1.material.transparent = true;
    // 禁用深度测试，避免渲染错误（不指定下面的代码来运行示例，可以看到翅膀中部分像素不停抖动）
    wing1.material.depthTest = false;
    // 默认情况下，Three.js仅仅会渲染一个面
    wing1.material.side = THREE.DoubleSide;

    wing2.material.opacity = 0.6;
    wing2.material.depthTest = false;
    wing2.material.transparent = true;
    wing2.material.side = THREE.DoubleSide;

    object.scale.set( 140, 140, 140 );
    mesh = object;
    scene.add( mesh );

    object.rotation.x = 0.2;
    object.rotation.y = -1.3;
} );</pre>
<div class="blog_h2"><span class="graybg">加载Collada格式</span></div>
<p>此格式的默认扩展名为.dae，也被广泛的使用。此格式用来定义场景、模型，甚至是动画。一个Collada模型同时包含了Geometry、材质的定义。</p>
<p>不意外的，要加载Collada格式同样需要引入Loader脚本：</p>
<pre class="crayon-plain-tag">&lt;script type="text/javascript" src="../libs/ColladaLoader.js"&gt;&lt;/script&gt;</pre>
<p>下面的代码，从Collada模型中导入一个卡车模型：</p>
<pre class="crayon-plain-tag">var loader = new THREE.ColladaLoader();

var mesh;
loader.load( "../assets/models/dae/Truck_dae.dae", function ( result ) {
    // 从模型场景中克隆出一个对象
    mesh = result.scene.children[ 0 ].children[ 0 ].clone();
    mesh.scale.set( 4, 4, 4 );
    // 添加到当前场景中
    scene.add( mesh );
} );</pre>
<p>需要注意的是，Collada加载器回调参数是如下结构：</p>
<pre class="crayon-plain-tag">var result = {
    scene: scene,  // 场景对象，THREE.Scene，包括所有模型对象，都在其中
    morphs: morphs,
    skins: skins,
    animations: animData,
    dae: {}
};</pre>
<p>本章仅关心scene属性中的对象。需要注意，纹理可能基于WebGL不支持的格式（例如.tga），你可能需要将其转换为.png格式，并编辑Collada文件。</p>
<div class="blog_h1"><span class="graybg">动画和镜头控制</span></div>
<div class="blog_h2"><span class="graybg">基本动画</span></div>
<p>我们之前的动画，都是基于渲染循环来实现——通知Three.js尽快的重新渲染。实现代码都是如下的模式：</p>
<pre class="crayon-plain-tag">render();
function render() {
    // 在此，可以修改模型属性
    /* ... */
    // 执行渲染
    renderer.render( scene, camera );
    // 调度下依次渲染
    requestAnimationFrame( render );
}</pre>
<p>我们只需要手工触发一次render()调用，之后它就会被定期（通常是每秒60次）递归调用了。 </p>
<p>基于这种方式，我们可以修改模型的各种属性——otation,scale, position, material, vertices, faces从而产生简单的动画效果。</p>
<div class="blog_h2"><span class="graybg">选择对象</span></div>
<p>尽管用鼠标选择对象和动画没有直接关系，但是为了深入理解镜头和动画，我们需要用到这一功能。</p>
<p>Three.js没有直接提供“点击”功能，但是我们可以基于THREE.Projector、THREE.Raycaster来判断鼠标当前对应到哪个物体：</p>
<pre class="crayon-plain-tag">document.addEventListener( 'mousedown', onDocumentMouseDown, false );

var projector = new THREE.Projector();

function onDocumentMouseDown( event ) {
    // 基于鼠标当前位置，创建一个3D向量
    var x = ( event.clientX / window.innerWidth ) * 2 - 1;
    var y = -( event.clientY / window.innerHeight ) * 2 + 1;
    var z = 0.5;
    var vector = new THREE.Vector3( x, y, z );
    // 把鼠标当前位置转换为Three.js场景中的坐标 —— 把2D屏幕坐标unproject为3D世界坐标
    vector = vector.unproject( camera );
    // 从相机所在位置发出一条射线，射到鼠标位置
    var raycaster = new THREE.Raycaster( camera.position, vector.sub( camera.position ).normalize() );
    // 检查此射线穿过哪些物体
    var intersects = raycaster.intersectObjects( [ sphere, cylinder, cube ] );

    if ( intersects.length &gt; 0 ) {

        console.log( intersects[ 0 ] );

        intersects[ 0 ].object.material.transparent = true;
        intersects[ 0 ].object.material.opacity = 0.1;
    }
}</pre>
<div class="blog_h2"><span class="graybg">基于Tween.js的动画</span></div>
<p><a href="https://github.com/tweenjs/tween.js/blob/master/docs/user_guide.md">Tween.js</a>是一个简单的JS库，可以基于给定的初值、终值自动计算所有中间值。这个中间值计算过程一般叫做tweening。示例代码：</p>
<pre class="crayon-plain-tag">var coords = { x: 0, y: 0 };
var tween = new TWEEN.Tween( coords )
    .to( { x: 100, y: 100 }, 1000 ) // 在1秒内完成变换
    .onUpdate( function () {  // 每当值变化时，执行的回调
        console.log( this.x, this.y );
    } )
    .start();

requestAnimationFrame( animate );
// requestAnimationFrame会自动把一个高精度的、从DOM加载到当前流逝的时间传递给回调
function animate( time ) {
    requestAnimationFrame( animate );
    TWEEN.update( time );
}</pre>
<p>我们可以创建一个改变物体位置的循环动画：</p>
<pre class="crayon-plain-tag">var posSrc = { pos: 1 };
// 创建两个Tween并链接，形成循环动画
var tween = new TWEEN.Tween( posSrc ).to( { pos: 0 }, 5000 );
tween.easing( TWEEN.Easing.Sinusoidal.InOut );

var tweenBack = new TWEEN.Tween( posSrc ).to( { pos: 1 }, 5000 );
tweenBack.easing( TWEEN.Easing.Sinusoidal.InOut );

tween.chain( tweenBack );
tweenBack.chain( tween );
// 当值变化时，改变物体的顶点位置
var onUpdate = function () {
    var count = 0;
    var pos = this.pos;

    loadedGeometry.vertices.forEach( function ( e ) {
        var newY = ((e.y + 3.22544) * pos) - 3.22544;
        pointCloud.geometry.vertices[ count++ ].set( e.x, newY, e.z );
    } );

    pointCloud.sortParticles = true;
};

tween.onUpdate( onUpdate );
tweenBack.onUpdate( onUpdate );

function render() {
    TWEEN.update();
    // 以16.7次/秒的频率，更新Tween，然后重渲染场景
    requestAnimationFrame( render );
    webGLRenderer.render( scene, camera );
}</pre>
<div class="blog_h2"><span class="graybg">镜头控制</span></div>
<div class="blog_h3"><span class="graybg">TrackballControls</span></div>
<p>跟踪球控制，允许你使用鼠标进行镜头的平移（鼠标左键）、缩放（鼠标中键）、旋转操作（鼠标右键）。</p>
<p>使用该控制方式，需要引入：</p>
<pre class="crayon-plain-tag">&lt;script type="text/javascript" src="../libs/TrackballControls.js"&gt;&lt;/script&gt;</pre>
<p>然后，创建控制器对象：</p>
<pre class="crayon-plain-tag">// 关联到镜头
var trackballControls = new THREE.TrackballControls( camera );
// 设置速度
trackballControls.rotateSpeed = 1.0;
trackballControls.zoomSpeed = 1.0;
trackballControls.panSpeed = 1.0;


function render() {
    // 获取上一次调用getDelta到现在流逝的时间
    var delta = clock.getDelta();
    // 传递此时间增量，控制器会根据移动速度来计算距离
    trackballControls.update( delta );
    requestAnimationFrame( render );
    webGLRenderer.render( scene, camera )
}</pre>
<div class="blog_h3"><span class="graybg">FlyControls</span></div>
<p>飞行控制，好像你在驾驶一架飞机，在场景中穿梭。</p>
<p>使用该控制方式，需要引入：</p>
<pre class="crayon-plain-tag">&lt;script type="text/javascript" src="../libs/FlyControls.js"&gt;&lt;/script&gt;</pre>
<p>示例代码：</p>
<pre class="crayon-plain-tag">var flyControls = new THREE.FlyControls(camera);
flyControls.movementSpeed = 25;
// 需要指向渲染场景的DOM元素
flyControls.domElement = document.querySelector('#WebGL');
flyControls.rollSpeed = Math.PI / 24;
flyControls.autoForward = true;
flyControls.dragToLook = false;</pre>
<div class="blog_h3"><span class="graybg">FirstPersonControls</span></div>
<p>第一人称视角。示例代码：</p>
<pre class="crayon-plain-tag">var camControls = new THREE.FirstPersonControls(camera);
camControls.lookSpeed = 0.1;
camControls.movementSpeed = 20;
camControls.noFly = true;
camControls.lookVertical = true;
camControls.constrainVertical = true;
camControls.verticalMin = 1.0;
camControls.verticalMax = 2.0;
// 场景最初渲染时，镜头的位置
camControls.lon = -150;
camControls.lat = 120; </pre>
<div class="blog_h3"><span class="graybg">PointerLockControls</span></div>
<p>与上一个类似，但是提供鼠标锁定功能。避免镜头一直移动导致晃眼。具体查看<a href="https://threejs.org/examples/misc_controls_pointerlock.html">这个示例</a>。</p>
<div class="blog_h3"><span class="graybg">OrbitControl</span></div>
<p>这种方式可以很方便的旋转、平移、缩放位于场景中心位置的物体。例如太空场景中的星球。示例代码：</p>
<pre class="crayon-plain-tag">var orbitControls = new THREE.OrbitControls(camera);
orbitControls.autoRotate = true;
var clock = new THREE.Clock();
...
var delta = clock.getDelta();
orbitControls.update(delta);</pre>
<div class="blog_h2"><span class="graybg">变形与骨骼动画</span></div>
<p>当利用3D建模软件创建动画时，通常有两种机制—— 变形目标、骨骼动画。</p>
<div class="blog_h3"><span class="graybg">Morph targets</span></div>
<p>使用变形目标（Morph targets），你可以定义模型的变形（deformed）版本——Mesh的一个关键位置（key position）。对于此变形版本，所有顶点的位置被记录下来。根据原始版本、变形版本的顶点位置的变化，可以方便的创建变化。其本质就是移动顶点的位置。</p>
<p>变形目标是定义动画最直接的方式，其缺点是对于大的Mesh和大的动画，模型文件会边的庞大。</p>
<p>Three.js支持手工的从一个关键位置移动到另一个，但是手工控制比较麻烦，你需要跟踪当前位置、需要变形到的目标位置。THREE.MorphAnimMesh把这些细节封装起来，我们通常直接使用该类。下面的代码示例了如何加载内置了变形目标的模型：</p>
<pre class="crayon-plain-tag">var loader = new THREE.JSONLoader();
loader.load( '../assets/models/horse.js', function ( geometry, mat ) {

    var mat = new THREE.MeshLambertMaterial({
        morphTargets: true, // 一定要设置材质的morphTargets为true，否则不支持动画
        vertexColors: THREE.FaceColors
    } );

    geometry.computeVertexNormals();
    geometry.computeFaceNormals();
    // 在创建MorphAnimMesh之前，要调用computeMorphNormals确保变形目标的所有法向量被正确的计算
    // 此操作对于正确的灯光、阴影效果是必须的
    geometry.computeMorphNormals();
    if ( geometry.morphColors &amp;&amp; geometry.morphColors.length ) {
        // 你可以为某个特定的变形目标的面定制颜色
        var colorMap = geometry.morphColors[ 0 ];
        for ( var i = 0; i &lt; colorMap.colors.length; i++ ) {
            geometry.faces[ i ].color = colorMap.colors[ i ];
            geometry.faces[ i ].color.offsetHSL( 0, 0.3, 0 );
        }
    }
    meshAnim = new THREE.MorphAnimMesh( geometry, mat );
    meshAnim.duration = 1000;
    meshAnim.position.x = 200;
    meshAnim.position.z = 0;
    
    scene.add( meshAnim );
}, '../assets/models' );

function render() {
    var delta = clock.getDelta();
    webGLRenderer.clear();
    // 推进动画
    meshAnim.updateAnimation( delta * 1000 );
    requestAnimationFrame( render );
    webGLRenderer.render( scene, camera );
}</pre>
<p>Three.js的默认行为是一下子运行所有动画，如果为单个Geometry定义了多个动画，则可以通过<pre class="crayon-plain-tag">parseAnimations()</pre> 和<pre class="crayon-plain-tag">playAnimation(name,fps)</pre> 来运行其中一个动画。</p>
<div class="blog_h3"><span class="graybg">Skeletal animation</span></div>
<p>这种方式允许你为模型定义骨骼，并且把顶点附着在骨骼上。当你移动骨骼的时候，所有相连的骨骼也跟随移动，并导致顶点移动，产生变形。</p>
<p>变形动画比较简单，Three.js只需要转换顶点位置就可以了。骨骼动画则要复杂一些，当你移动骨骼时，Three.js需要知道如何计算附着其上的皮肤（Mesh顶点）的位置。</p>
<p>下面这个例子是手工执行骨骼动画的代码：</p>
<pre class="crayon-plain-tag">var clock = new THREE.Clock();

var loader = new THREE.JSONLoader();
loader.load( '../assets/models/hand-1.js', function ( geometry, mat ) {
    // 注意设置skinning为true，否则不会看到任何骨骼移动的效果
    var mat = new THREE.MeshLambertMaterial( { color: 0xF0C8C9, skinning: true } );
    // 专门针对骨骼/皮肤几何图形的Mesh
    mesh = new THREE.SkinnedMesh( geometry, mat );
    scene.add( mesh );
    // 开始动画
    tween.start();

}, '../assets/models' );


var onUpdate = function () {
    var pos = this.pos;
    // 转动手指
    mesh.skeleton.bones[ 5 ].rotation.set( 0, 0, pos );
    mesh.skeleton.bones[ 6 ].rotation.set( 0, 0, pos );
    mesh.skeleton.bones[ 10 ].rotation.set( 0, 0, pos );
    mesh.skeleton.bones[ 11 ].rotation.set( 0, 0, pos );
    mesh.skeleton.bones[ 15 ].rotation.set( 0, 0, pos );
    mesh.skeleton.bones[ 16 ].rotation.set( 0, 0, pos );
    mesh.skeleton.bones[ 20 ].rotation.set( 0, 0, pos );
    mesh.skeleton.bones[ 21 ].rotation.set( 0, 0, pos );
    // 转动手腕
    mesh.skeleton.bones[ 1 ].rotation.set( pos, 0, 0 );
};
var tween = new TWEEN.Tween( { pos: -1 } )
    .to( { pos: 0 }, 3000 )
    .easing( TWEEN.Easing.Cubic.InOut )
    .yoyo( true ) // 下一次反向执行
    .repeat( Infinity ) // 无限执行
    .onUpdate( onUpdate );

render();

function render() {
    TWEEN.update();
    requestAnimationFrame( render );
    webGLRenderer.render( scene, camera );
} </pre>
<div class="blog_h2"><span class="graybg">从外部模型创建动画</span></div>
<p>前面我们讨论过，Three.js支持多种外部模型格式。这些格式中的一部分，支持动画：</p>
<ol>
<li>对于JSON格式，可以使用Blender with the JSON exporter导出</li>
<li>Collada，该格式支持动画</li>
</ol>
<div class="blog_h3"><span class="graybg">导入Blender骨骼动画</span></div>
<p>基于Blender创建骨骼动画时，要注意以下几点：</p>
<ol>
<li>模型的所有顶点，至少分配到一个顶点组（vertex group）中</li>
<li>顶点组的名称必须和控制它的骨骼的名称一致，这样Three.js才知道，移动骨骼时，需要修改哪些顶点</li>
<li>注意仅仅第一个Action被导出，因此要确保你需要导出的动画时第一个</li>
<li>创建关键帧时，最好选取所有骨头，即使某些不变化</li>
<li>导出模型时，需要保证模型处于rest pose，否则动画可能变形严重</li>
<li>导出时，注意勾选：Vertices、Faces、Normals、Skinning、UVs、Colors、Materials、Flip YZ、Skeletal animation</li>
</ol>
<p>这样，骨骼的移动路径会被一同导出，在Three.js中可以简单的进行回放：</p>
<pre class="crayon-plain-tag">var loader = new THREE.JSONLoader();
loader.load( '../assets/models/hand-2.js', function ( model, mat ) {

    var mat = new THREE.MeshLambertMaterial( { color: 0xF0C8C9, skinning: true } );
    mesh = new THREE.SkinnedMesh( model, mat );

    var animation = new THREE.Animation( mesh, model.animation );

    mesh.rotation.x = 0.5 * Math.PI;
    mesh.rotation.z = 0.7 * Math.PI;
    scene.add( mesh );
    // 此助手用于查看骨骼如何变化
    helper = new THREE.SkeletonHelper( mesh );
    helper.material.linewidth = 2;
    helper.visible = false;
    scene.add( helper );

    // 开始播放动画
    animation.play();

}, '../assets/models' );

render();

function render() {
    var delta = clock.getDelta();
    if ( mesh ) {
        helper.update();
        THREE.AnimationHandler.update( delta );
    }
    requestAnimationFrame( render );
    webGLRenderer.render( scene, camera );
}</pre>
<div class="blog_h3"><span class="graybg">导入Collada动画</span></div>
<p>和从JSON格式导入动画的方式差不多，但是要注意Collada可以存储整个场景，包括镜头、灯光、动画，因此你需要找到需要使用的那个附带动画的Mesh：</p>
<pre class="crayon-plain-tag">var child = collada.skins[0];  // THREE.SkinnedMesh
scene.add(child);
var animation = new THREE.Animation(child, child.geometry.animation);
animation.play(); </pre>
<div class="blog_h1"><span class="graybg">使用纹理</span></div>
<p>纹理在Three.js中有多种不同的使用方式，你可以用纹理来定义Mesh的颜色，或者定义发光效果、凹凸（bump）、反射效果。</p>
<div class="blog_h2"><span class="graybg">在材质中使用纹理</span></div>
<p>最基本的用例是加载纹理，并将其作为材质上的一个map。当你基于此材质创建Mesh时，Mesh被纹理着色：</p>
<pre class="crayon-plain-tag">var texture = THREE.ImageUtils.loadTexture("../assets/textures/general/" + imageFile)
var mat = new THREE.MeshPhongMaterial();
mat.map = texture;
var mesh = new THREE.Mesh(geom, mat);
return mesh;</pre>
<p>作为纹理的图片，可以是PNG、GIF或者JPG格式，且大小必须是2的N次方。 需要注意，纹理图片的加载是异步的，如果你希望纹理加载完毕之后再进行渲染，可以：</p>
<pre class="crayon-plain-tag">texture = THREE.ImageUtils.loadTexture('texture.png', {},function() { renderer.render(scene); });</pre>
<p>由于纹理图片的像素一般不能和面的像素一一对应，纹理需要放大或者缩小后使用。WebGL/Three.js提供了几个不同的选项。你可以设置纹理的magFilter、minFilter属性，来声明它将如何被缩放 。两个基本的取值为：</p>
<table class=" full-width fixed-word-wrap">
<tbody>
<tr>
<td style="width: 35%;">THREE.NearestFilter</td>
<td>使用最临近的像素。当放大时，出现色块；当缩小时，丢失细节</td>
</tr>
<tr>
<td>THREE.LinearFilter</td>
<td>基于周围四个像素的值，来决定一个正确的颜色。缩小时仍然会丢失细节，但是放大时会更加平滑，不会出现色块</td>
</tr>
</tbody>
</table>
<p>除了这两个基本的取值之外，我们还可以使用mipmap——一系列纹理图片的集合，后者是前者的一半大小。mipmap可以在加载纹理时自动创建，结合以下filter取值使用：</p>
<table class=" full-width fixed-word-wrap">
<tbody>
<tr>
<td style="width: 35%;">THREE.NearestMipMapNearestFilter</td>
<td>选取最匹配分辨率的mipmap，并应用NearestFilter规则。放大时仍然出现色块，但是看起来好很多</td>
</tr>
<tr>
<td>THREE.NearestMipMapLinearFilter</td>
<td>选取最接近的两个mipmap级别，在两个级别上分别应用NearestFilter规则，得到中间结果。这两个中间结果随之传递给LinearFilter获得最终结果</td>
</tr>
<tr>
<td>THREE.LinearMipMapNearestFilter</td>
<td> </td>
</tr>
<tr>
<td>THREE.LinearMipMapLinearFilter </td>
<td> </td>
</tr>
</tbody>
</table>
<p>如果不明确指定，magFilter默认取值THREE.LinearFilter，minFilter默认取值THREE.LinearMipMapLinearFilter。</p>
<p>纹理本身是方形的，但是Three.js可以确保不管对于什么形状，材质都能正确的覆盖（wrap around），此保证由UV mapping实现。</p>
<div class="blog_h2"><span class="graybg">创建凹凸效果</span></div>
<div class="blog_h3"><span class="graybg">基于bump map</span></div>
<p>所谓bump map，是一幅额外的纹理，用于在材质上添加更多的深度效果：</p>
<pre class="crayon-plain-tag">var texture = THREE.ImageUtils.loadTexture( "../assets/textures/general / " + imageFile)
var mat = new THREE.MeshPhongMaterial();
mat.map = texture;
var bump = THREE.ImageUtils.loadTexture("../assets/textures/general/" + bump )
mat.bumpMap = bump;
mat.bumpScale = 0.2;  // 设置凸起的高度（负值则表示凹下的深度）
var mesh = new THREE.Mesh( geom, mat );
return mesh;</pre>
<p>bump map通常都是灰度图，像素的密度代表了凸起的（相对）高度 。</p>
<div class="blog_h3"><span class="graybg">基于normal map</span></div>
<p>对于normal map来说，高度信息没有被保存，但是法线的方向被保存了。使用normal map你可以在仅使用很少点、面的情况下创建具有复杂细节的模型：</p>
<pre class="crayon-plain-tag">var t = THREE.ImageUtils.loadTexture("../assets/textures/general/" + imageFile);
var m = THREE.ImageUtils.loadTexture("../assets/textures/general/" + normal);
var mat2 = new THREE.MeshPhongMaterial();
mat2.map = t;
mat2.normalMap = m;
mat.normalScale.set(1,1); // 设置凸起的高度（负值则表示凹下的深度）
var mesh = new THREE.Mesh(geom, mat2);
return mesh;</pre>
<p>normal map的缺点是不容易创建，需要使用Blender/Photoshop之类的特殊工具。 </p>
<div class="blog_h2"><span class="graybg">创建假反射</span></div>
<p>计算环境反射效果是非常消耗资源的操作。在Three.js中你可以模拟这种效果。步骤如下：</p>
<ol>
<li>创建一个CubeMap对象，CubeMap是六个纹理的集合，可以被应用到Cube的六个面</li>
<li>使用CubeMap创建一个Box，此Box作为场景的环境，当你转动镜头时，看到的是此Box的内侧</li>
<li>把上述模拟环境的CubMap应用到需要反射效果的Mesh上面，Three.js可以确保其看起来就像是环境的反射</li>
</ol>
<div class="blog_h3"><span class="graybg">创建CubeMap</span></div>
<p>准备好<a href="http://www.humus.name/index.php?page=Textures">纹理图片</a>后，创建CubeMap非常容易。你需要的是能够组成完整环境的六幅图片：向前看时的图片（posz）、向后看时的图片（negz）、向上看的图片（posy）、向下看的图片（negy）、向右看的图片（posx）、向左看的图片（negx）。示例代码：</p>
<pre class="crayon-plain-tag">var path = "../assets/textures/cubemap/parliament/";
var format = '.jpg';
var urls = [
    path + 'posx' + format, path + 'negx' + format,
    path + 'posy' + format, path + 'negy' + format,
    path + 'posz' + format, path + 'negz' + format
];
var textureCube = THREE.ImageUtils.loadTextureCube( urls );</pre>
<p>如果你已经获得360度全景图片，可以利用<a href="//gonchar.me/%20panorama/">工具</a>将其切割为上面的六幅图。 或者直接让Three.js处理切割过程：</p>
<pre class="crayon-plain-tag">var textureCube = THREE.ImageUtils.loadTexture("360-degrees.png", new THREE.UVMapping());</pre>
<div class="blog_h3"><span class="graybg">创建Skybox</span></div>
<p>Three.js提供了一个特殊的着色器，用来基于CubeMap来创建Skybox（环境）：</p>
<pre class="crayon-plain-tag">var shader = THREE.ShaderLib[ "cube" ];
shader.uniforms[ "tCube" ].value = textureCube;

var material = new THREE.ShaderMaterial( {

    fragmentShader: shader.fragmentShader,
    vertexShader: shader.vertexShader,
    uniforms: shader.uniforms,
    depthWrite: false,
    side: THREE.DoubleSide

} );

var skybox = new THREE.Mesh( new THREE.BoxGeometry( 10000, 10000, 10000 ), material );
scene.add( skybox );</pre>
<div class="blog_h3"><span class="graybg">创建反射物体</span></div>
<pre class="crayon-plain-tag">// 此镜头看到的景象，将用于球体的动态反射效果
cubeCamera = new THREE.CubeCamera( 0.1, 20000, 256 );
scene.add( cubeCamera );

// 动态反射，不仅仅CubeMap出现在反射图像中，Mesh也是
// 两个动态反射的物体不支持相互反射
var dynamicEnvMaterial = new THREE.MeshBasicMaterial( { envMap: cubeCamera.renderTarget, side: THREE.DoubleSide } );
sphere = new THREE.Mesh( sphereGeometry, dynamicEnvMaterial );
scene.add( sphere );

// 静态反射
// 注意材质可以设置反射率
var envMaterial = new THREE.MeshBasicMaterial( { envMap: textureCube, side: THREE.DoubleSide， reflection: 1 } );
var cylinder = new THREE.Mesh( cylinderGeometry, envMaterial );
scene.add( cylinder );


function render() {
    // 此镜头看到的内容需要更新，否则动态反射物体漆黑一片
    cubeCamera.updateCubeMap( renderer, scene );
    requestAnimationFrame( render );
}</pre>
<p>材质的envMap属性可以设置为一个CubeMap对象，这样Mesh就可以反射CubeMap代表的环境。</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/three-js-study-note">Three.js学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/three-js-study-note/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>PostCSS学习笔记</title>
		<link>https://blog.gmem.cc/postcss-study-note</link>
		<comments>https://blog.gmem.cc/postcss-study-note#comments</comments>
		<pubDate>Thu, 22 Dec 2016 08:03:47 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[CSS]]></category>
		<category><![CDATA[学习笔记]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=14147</guid>
		<description><![CDATA[<p>简介 由于CSS语言本身的表达能力较差，Web开发人员常常使用SASS、LESS之类的CSS预处理器语言来编写样式，然后再编译为普通的CSS代码。 PostCSS是一个类似的、较晚出现的CSS处理器，它基于JavaScript语言编写。PostCSS使用插件式的架构，而不是像以前的CSS预处理器那样内置所有特性。目前PostCSS的插件数量已经有数百个。 尽管名字中带有Post，PostCSS并不是所谓后处理器。它可以执行其它预处理器能够完成的任务。PostCSS本身只做两件事情： 将输入代码转换为抽象语法树（AST） 调用插件处理AST 插件机制 PostCSS依赖于插件完成实际的工作，例如Lint、变量和混入的支持、未来CSS特性支持（transpile）、内联图片，等等。本节列出常用的插件。 解决全局CSS问题 插件 说明 postcss-use 允许你在CSS中明确声明使用PostCSS特性，这样可以仅针对某些文件使用PostCSS postcss-modules 可以让你在任何地方使用CSS Modules——自动进行CSS选择器的命名隔离，不仅仅限于客户端 react-css-modules 针对React扩展CSS Modules postcss-autoreset 条件式的自动重置 postcss-initial <a class="read-more" href="https://blog.gmem.cc/postcss-study-note">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/postcss-study-note">PostCSS学习笔记</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>由于CSS语言本身的表达能力较差，Web开发人员常常使用SASS、LESS之类的CSS预处理器语言来编写样式，然后再编译为普通的CSS代码。</p>
<p>PostCSS是一个类似的、较晚出现的CSS处理器，它基于JavaScript语言编写。PostCSS使用插件式的架构，而不是像以前的CSS预处理器那样内置所有特性。目前PostCSS的插件数量已经有数百个。</p>
<p>尽管名字中带有Post，PostCSS并不是所谓后处理器。它可以执行其它预处理器能够完成的任务。PostCSS本身只做两件事情：</p>
<ol>
<li>将输入代码转换为抽象语法树（AST）</li>
<li>调用插件处理AST</li>
</ol>
<div class="blog_h2"><span class="graybg">插件机制</span></div>
<p>PostCSS依赖于插件完成实际的工作，例如Lint、变量和混入的支持、未来CSS特性支持（transpile）、内联图片，等等。本节列出常用的插件。</p>
<div class="blog_h3"><span class="graybg">解决全局CSS问题</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>postcss-use</td>
<td>
<p>允许你在CSS中明确声明使用PostCSS特性，这样可以仅针对某些文件使用PostCSS</p>
</td>
</tr>
<tr>
<td>postcss-modules</td>
<td>可以让你在任何地方使用CSS Modules——自动进行CSS选择器的命名隔离，不仅仅限于客户端</td>
</tr>
<tr>
<td>react-css-modules</td>
<td>针对React扩展CSS Modules</td>
</tr>
<tr>
<td>postcss-autoreset</td>
<td>条件式的自动重置</td>
</tr>
<tr>
<td>postcss-initial</td>
<td>支持<pre class="crayon-plain-tag">all: initial</pre> 规则，以便重置所有继承得到的样式</td>
</tr>
<tr>
<td>cq-prolyfill</td>
<td>添加容器查询（container query）支持，允许根据父元素的宽度来决定样式</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">使用未来的CSS特性</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>autoprefixer</td>
<td>自动添加厂商相关的规则前缀</td>
</tr>
<tr>
<td>postcss-cssnext</td>
<td>支持未来的CSS特性</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">CSS可读性</span></div>
<table class=" fixed-word-wrap full-width">
<thead>
<tr>
<td style="width: 20%; text-align: center;">插件</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>precss</td>
<td>
<p>提供SASS风格的特性，例如变量、嵌套、混入</p>
</td>
</tr>
<tr>
<td>postcss-sorting</td>
<td>对规则的内容进行排序</td>
</tr>
<tr>
<td>postcss-utilities</td>
<td>包含很多常用的助手工具</td>
</tr>
<tr>
<td>short</td>
<td>支持某些属性简写</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">图像和字体</span></div>
<table class=" fixed-word-wrap full-width">
<thead>
<tr>
<td style="width: 20%; text-align: center;">插件</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>postcss-assets</td>
<td>管理资产，把样式表和环境改变隔离。可以自动寻找图片的URL、自动生成图片的尺寸</td>
</tr>
<tr>
<td>postcss-sprites</td>
<td>生成图片sprites，自动合并多张小图片</td>
</tr>
<tr>
<td>font-magician</td>
<td>生成CSS中所有需要的@font-face规则</td>
</tr>
<tr>
<td>postcss-inline-svg</td>
<td>支持内联SVG图片并设置样式</td>
</tr>
<tr>
<td>postcss-write-svg</td>
<td>支持在CSS中直接写入简单的SVG</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">语法相关</span></div>
<p>PostCSS可以转换基于任何语法的样式，不仅仅是CSS</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>sugarss</td>
<td>类似于SASS的基于缩进的语法</td>
</tr>
<tr>
<td>postcss-scss</td>
<td>与SCSS语言一起工作，但是不进行SCSS编译</td>
</tr>
<tr>
<td>postcss-less</td>
<td>与LESS语言一起工作，但是不进行LESS编译</td>
</tr>
<tr>
<td>postcss-less-engine</td>
<td>与LESS语言一起工作，并且把LESS编译为CSS</td>
</tr>
<tr>
<td>postcss-js</td>
<td>
<p>支持在JS中编写样式，或者把转换React内联样式、Radium、JSS</p>
</td>
</tr>
<tr>
<td>postcss-safe-parser</td>
<td>查找并修复CSS语法错误</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">其它</span></div>
<table class=" fixed-word-wrap full-width">
<thead>
<tr>
<td style="width: 20%; text-align: center;">插件</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>cssnano</td>
<td>模块化的CSS压缩器</td>
</tr>
<tr>
<td>lost</td>
<td>基于calc()的特性丰富的网格系统</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">使用PostCSS</span></div>
<div class="blog_h2"><span class="graybg">Webpack</span></div>
<p>基于postcss-loader：</p>
<pre class="crayon-plain-tag">module.exports = {
    module: {
        loaders: [
            {
                test: /\.css$/,
                // 需要在style、css等加载器之前执行
                loader: "style-loader!css-loader!postcss-loader"
            }
        ]
    },
    postcss: function () {
        // 列出启用的PostCSS插件
        return [ require( 'autoprefixer' ), require( 'precss' ) ];
    }
}</pre>
<div class="blog_h2"><span class="graybg">命令行</span></div>
<pre class="crayon-plain-tag">postcss --use autoprefixer -c options.json -o main.css css/*.css</pre>
<div class="blog_h2"><span class="graybg">Webstorm</span></div>
<p>你需要安装<a href="https://plugins.jetbrains.com/plugin/8578">PostCSS support</a>插件，以获得以下特性：</p>
<ol>
<li>自动识别.pcss文件</li>
<li>支持PostCSS语法高亮</li>
<li>智能的代码完成</li>
<li>可配置的代码样式和自动格式化</li>
<li>代码导航支持</li>
<li>对自定义选择器、自定义媒体查询的支持</li>
</ol>
<div class="blog_h3"><span class="graybg">设置</span></div>
<p>如果要对任意.css文件启用PostCSS支持，你可以在Preferences ⇨ Languages &amp; Frameworks ⇨ Stylesheets ⇨ Dialects 把方言切换为PostCSS。</p>
<div class="blog_h1"><span class="graybg">常用插件</span></div>
<div class="blog_h2"><span class="graybg">Autoprefixer</span></div>
<p>这是一个必备的插件，可以让你脱离编写厂商特定的规则前缀的苦海。启用该插件后，如果需要编写弹性和子布局，你仅需要：</p>
<pre class="crayon-plain-tag">#content {
    display: flex;
}</pre>
<p>该插件会将其转换为：</p>
<pre class="crayon-plain-tag">#content {
    display: -webkit-box;
    display: -webkit-flex;
    display: -ms-flexbox;
    display: flex;
}</pre>
<p>你可以配置需要支持的浏览器：</p>
<pre class="crayon-plain-tag">require( 'autoprefixer' )( {
    // 仅仅支持支持主流浏览器的最近两个版本
    browsers: [ 'last 2 versions' ]
    // ie 6-8 表示支持IE6-8
    // &gt; 1% 表示支持全球占用率超过1%的浏览器
} )</pre>
<div class="blog_h2"><span class="graybg">postcss-cssnext</span></div>
<p>该插件允许你使用未来版本CSS可能纳入标准的特性，它会将其编译为当前浏览器能够理解的CSS规则。</p>
<p>cssnext已经使用了Autoprefixer，因此不需要明确声明后者。</p>
<div class="blog_h3"><span class="graybg">自定义属性和变量</span></div>
<p>CSS 的层叠变量的自定义属性规范（CSS Custom Properties for Cascading Variables）允许在CSS中定义属性，并在样式规则中将其作为变量引用。自定义属性以<pre class="crayon-plain-tag">--</pre> 开头，你可以利用<pre class="crayon-plain-tag">var()</pre> 引用这些属性：</p>
<pre class="crayon-plain-tag">:root {
    /* 自定义属性 */
    --text-color: black;
}

body {
    /* 引用自定义属性 */
    color: var(--text-color);
}</pre>
<p>你还可以定义一个属性集，然后通过@apply来应用：</p>
<pre class="crayon-plain-tag">:root {
    /* 自定义属性集 */
    --danger-theme: {
        color: white;
        background-color: red;
    };
}

.danger {
    /* 应用属性集，新增两条样式规则 */
    @apply --danger-theme;
}</pre>
<p>你可以在calc()函数中引用属性：</p>
<pre class="crayon-plain-tag">:root {
    --fontSize: 1rem;
}

h1 {
    font-size: calc(var(--fontSize) * 2);
}</pre>
<div class="blog_h3"><span class="graybg">自定义媒体查询</span> </div>
<pre class="crayon-plain-tag">@custom-media --small-viewport (max-width: 30em);

@media (--small-viewport) {
    /* 对于小的viewport应用样式 */
}</pre>
<div class="blog_h3"><span class="graybg">媒体查询范围</span></div>
<p>使用操作符代替min- / max-，让语法更易读：</p>
<pre class="crayon-plain-tag">@media (width &gt;= 500px) and (width &lt;= 1200px) {
    /* 样式规则 */
}

/* 也可以和自定义媒体查询联用 */
@custom-media --only-medium-screen (width &gt;= 500px) and (width &lt;= 1200px);
@media (--only-medium-screen) {
    /* 样式规则 */
}</pre>
<div class="blog_h3"><span class="graybg">自定义选择器</span></div>
<p>CSS 扩展规范（CSS Extensions）允许自定义选择器，你可以创建一个选择器来引用多个既有选择器：</p>
<pre class="crayon-plain-tag">@custom-selector :--heading h1, h2, h3, h4, h5, h6;

:--heading {
    font-weight: bold;
}</pre>
<p>转换后的结果为：</p>
<pre class="crayon-plain-tag">h1, h2, h3, h4, h5, h6 {
 font-weight: bold;
}</pre>
<div class="blog_h3"><span class="graybg">样式规则嵌套</span></div>
<p>样式规则嵌套是非常实用的特性，可以减少重复的选择器声明。它是SASS、LESS等CSS预处理器能够流行的重要原因。</p>
<p>CSS 嵌套模块规范（CSS Nesting Module）中定义了标准的样式规则嵌套方式，cssnext支持这些规范。</p>
<p>规范引入了一种新的嵌套选择器，使用<pre class="crayon-plain-tag">&amp;</pre> 来表示。在嵌套样式规则（nested style rule）中使用该选择器时，它代表当前被匹配的父规则；在其它地方使用该选择器不代表任何东西。</p>
<p>嵌套样式规则具有两种语法：</p>
<ol>
<li>直接嵌套，直接编写在其它样式的内部，且规则的组合选择器（compound selector）的第一个必须是嵌套选择器：<br />
<pre class="crayon-plain-tag">.foo {
    color: blue;
    &amp; &gt; .bar { color: red; }
}
/* 转换后 */
.foo { color: blue; }
.foo &gt; .bar { color: red; }


.foo {
    color: blue;
    &amp;.bar { color: red; }
}
/* 转换后 */
.foo { color: blue; }
.foo.bar { color: red; }

.foo, .bar {
    color: blue;
    &amp; + .baz, &amp;.qux {
        color: red;
    }
}

/* 等价于 */
.foo, .bar { color: blue; }
:matches(.foo, .bar) + .baz, :matches(.foo, .bar).qux { color: red; }</pre>
</li>
<li>嵌套@Rule：这种方式更加灵活，没有嵌套选择器的那些限制：<br />
<pre class="crayon-plain-tag">.foo {
    color: red;
    @nest &amp; &gt; .bar { color: blue; }
}
/* 等价于 */
.foo { color: red; }
.foo &gt; .bar { color: blue; }


.foo {
    color: red;
    /* &amp; 仍然表示父规则，但是位置随意了 */
    @nest .parent &amp; { color: blue; }
}
/* 等价于 */
.foo { color: red; }
.parent .foo { color: blue; }

.foo {
    color: red;
    @nest :not(&amp;) { color: blue; }
}
/* 等价于 */
.foo { color: red; }
:not(.foo) { color: blue; }</pre>
</li>
</ol>
<div class="blog_h3"><span class="graybg">颜色操控</span></div>
<p>color函数可以让颜色操控更加方便，该函数会被编译为rgba：</p>
<pre class="crayon-plain-tag">a {
    color: color(red alpha(-10%));
}

a:hover {
    color: color(red blackness(80%));
}</pre>
<p>更多的颜色修饰符参考<a href="https://github.com/postcss/postcss-color-function#list-of-color-adjuster">官方</a>。</p>
<p>hwb函数类似于hsl，但是更加易于人类理解，该函数会被编译为rgba：</p>
<pre class="crayon-plain-tag">body {
    color: hwb(90, 0%, 0%, 0.5);
}</pre>
<p>gray函数用于指定灰度颜色 ，该函数会被编译为rgba：</p>
<pre class="crayon-plain-tag">.foo {
    color: gray(85);
}

.bar {
    color: gray(10%, 50%);
}</pre>
<p>四位或者八位的<pre class="crayon-plain-tag">#rrggbbaa</pre> 格式的颜色代码被支持：</p>
<pre class="crayon-plain-tag">body {
    background: #9d9c;
}</pre>
<div class="blog_h3"><span class="graybg">initial值</span></div>
<p>cssnext允许任何样式属性取值initial，这个值不是浏览器默认值，而是规定的某个属性的默认值。例如display的默认值总是inline。</p>
<p>你可以使用</p>
<pre class="crayon-plain-tag">div {
    all: initial;
}</pre>
<p>把所有属性设置为默认值。</p>
<div class="blog_h3"><span class="graybg">:any-link伪类</span></div>
<p>该伪类匹配任何具有href属性的a/area/link元素：</p>
<pre class="crayon-plain-tag">nav :any-link {
    background-color: yellow;
}</pre>
<div class="blog_h3"><span class="graybg">:matches伪类</span></div>
<p>该伪类：</p>
<pre class="crayon-plain-tag">p:matches(:first-child, .special) {
    color: red;
}</pre>
<div class="blog_h3"><span class="graybg">:not伪类</span></div>
<p>允许该伪类指定多个选择器：</p>
<pre class="crayon-plain-tag">p:not(:first-child, .special) {
  color: red;
}</pre>
<div class="blog_h3"><span class="graybg">::伪语法</span></div>
<p>对于不支持该语法的旧浏览器，自动转换为单个冒号：</p>
<pre class="crayon-plain-tag">a::before {  }</pre>
<div class="blog_h2"><span class="graybg">postcss-use</span></div>
<p>在某个样式类文件中启用PostCSS插件。例如清除CSS代码中的注释：</p>
<pre class="crayon-plain-tag">/* 标准语法 */
@use postcss-discard-comments(removeAll: true);
/* 备选语法 */
@use postcss-discard-comments {
    removeAll: true
}

/* 这里的注释会被清除 */
h1 {
    color: red
}</pre>
<div class="blog_h3"><span class="graybg">short</span></div>
<p>为某些样式属性提供简写：</p>
<pre class="crayon-plain-tag">/* 简写 */
.icon {
    size: 48px;
}
/* 等价于 */
.icon {
    width: 48px;
    height: 48px;
}


/* 简写 */
.frame {
    /* 第一个是下上，第二个是左右 */
    margin: * auto;
}
/* 等价于 */
.frame {
    margin-right: auto;
    margin-left: auto;
}


/* 简写 */
.banner {
    position: fixed 0 0 *;
}
/* 等价于 */
.banner {
    position: fixed;
    top: 0;
    right: 0;
    left: 0;
}

/* 简写 */
.canvas {
    color: #abccfc #212231;
}
/* 等价于 */
.canvas {
    color: #abccfc;
    background-color: #212231;
}</pre>
<p>目前IDE对这些简写的支持不太好，可能误判为语法错误。</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/postcss-study-note">PostCSS学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/postcss-study-note/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>CSS Modules学习笔记</title>
		<link>https://blog.gmem.cc/css-modules-study-note</link>
		<comments>https://blog.gmem.cc/css-modules-study-note#comments</comments>
		<pubDate>Thu, 22 Dec 2016 05:25:07 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[CSS]]></category>
		<category><![CDATA[学习笔记]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=14132</guid>
		<description><![CDATA[<p>CSS Modules 简介 CSS Modules是一个开源项目，它是一个简单的CSS模块化规范，主要完成两件事情： 样式类名、动画名的作用域支持。这可以避免命名冲突 模块化支持，允许CSS文件之间的依赖关系 与Less、SASS、PostCSS不同，CM并不尝试把CSS变得像一门编程语言（比如支持控制结构、变量），它仅仅解决模块化的基本问题——作用域和模块依赖。 使用CM时所有[crayon-69e8384d5e0ca318506804-i/] 和[crayon-69e8384d5e0ce195423327-i/] 所操作的URL均为模块请求格式（module request format）： [crayon-69e8384d5e0d0402122895-i/]和[crayon-69e8384d5e0d2123837825-i/] 这样的URL表示想对路径 [crayon-69e8384d5e0d4397838501-i/]和[crayon-69e8384d5e0d6065970496-i/] 这样的URL表示目标位于模块目录（例如node_modules）内部 作用域 使用CM时，样式类名、动画名默认仅具有局部作用域。CM会把CSS文件编译为一个低级别ICSS（Interoperable CSS）格式，不过你编写的时候仍然使用普通CSS语法： [crayon-69e8384d5e0d8945525449/] ICSS会导出一个对象，其键是局部名称（即你在CSS中声明的样式类名），其值则是编译后的全局名称。全局名称正是运行时使用的真实CSS样式类名，默认情况下全局名称是依据局部名称生成的哈希串。 从JS模块引入一个CSS模块时，你自然获得上述导出对象，可以通过键引用CSS类名： [crayon-69e8384d5e0da243692726/] 命名 CM建议局部名称一律使用驼峰式大小写，单这不是必须的。 <a class="read-more" href="https://blog.gmem.cc/css-modules-study-note">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/css-modules-study-note">CSS Modules学习笔记</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">CSS Modules</span></div>
<div class="blog_h2"><span class="graybg">简介</span></div>
<p><a href="https://github.com/css-modules/css-modules">CSS Modules</a>是一个开源项目，它是一个简单的CSS模块化规范，主要完成两件事情：</p>
<ol>
<li>样式类名、动画名的作用域支持。这可以避免命名冲突</li>
<li>模块化支持，允许CSS文件之间的依赖关系</li>
</ol>
<p>与Less、SASS、PostCSS不同，CM并不尝试把CSS变得像一门编程语言（比如支持控制结构、变量），它仅仅解决模块化的基本问题——作用域和模块依赖。</p>
<p>使用CM时所有<pre class="crayon-plain-tag">url(...)</pre> 和<pre class="crayon-plain-tag">import</pre> 所操作的URL均为模块请求格式（module request format）：</p>
<ol>
<li><pre class="crayon-plain-tag">./xxx</pre>和<pre class="crayon-plain-tag">../xxx</pre> 这样的URL表示想对路径</li>
<li><pre class="crayon-plain-tag">xxx</pre>和<pre class="crayon-plain-tag">xxx/yyy</pre> 这样的URL表示目标位于模块目录（例如node_modules）内部</li>
</ol>
<div class="blog_h2"><span class="graybg">作用域</span></div>
<p>使用CM时，样式类名、动画名默认仅具有局部作用域。CM会把CSS文件编译为一个低级别ICSS（Interoperable CSS）格式，不过你编写的时候仍然使用普通CSS语法：</p>
<pre class="crayon-plain-tag">/* style.css */
.className {
    color: green;
}</pre>
<p>ICSS会导出一个对象，其<span style="background-color: #c0c0c0;">键是局部名称（即你在CSS中声明的样式类名），其值则是编译后的全局名称</span>。全局名称正是运行时使用的真实CSS样式类名，默认情况下全局名称是依据局部名称生成的哈希串。</p>
<p>从JS模块引入一个CSS模块时，你自然获得上述导出对象，可以通过键引用CSS类名：</p>
<pre class="crayon-plain-tag">// 导入全部映射
import styles from "./style.css";
// 导入需要的映射
import { className } from "./style.css";

// 基于键引用全局样式类名
element.innerHTML = '&lt;div class="' + styles.className + '"&gt;';</pre>
<div class="blog_h3"><span class="graybg">命名</span></div>
<p>CM建议局部名称一律使用驼峰式大小写，单这不是必须的。</p>
<div class="blog_h3"><span class="graybg">全局作用域</span></div>
<p>使用特殊伪类<pre class="crayon-plain-tag">:global</pre> 可以声明一个全局作用域：</p>
<pre class="crayon-plain-tag">:global(.className) {
    color: green;
}

@keyframes :global(animeName){
}</pre>
<p>全局名称不会被编译成哈希串，在JS中可以直接使用： </p>
<pre class="crayon-plain-tag">element.innerHTML = '&lt;div class="className"&gt;';</pre>
<p>尽管通常情况下没有必要，你可以显式的声明局部作用域： </p>
<pre class="crayon-plain-tag">:local(.className) {
    color: green;
}</pre>
<div class="blog_h2"><span class="graybg">组合</span></div>
<p>CM支持让一个选择器compose另一个选择器定义的样式规则，并称其为组合（Composition）。例如：</p>
<pre class="crayon-plain-tag">.className {
    color: green;
    background: red;
}

.otherClassName {
    composes: className;
    color: yellow;
}

/* 编译结果 */
.global_name_className{
   color: green;
   background: red;
}
.global_name_otherClassName{
    color: yellow;
}</pre>
<p>引用otherClassName的JS代码，会被编译为分别引用上面两个选择器的形式：</p>
<pre class="crayon-plain-tag">`&lt;div class="${otherClassName}"&gt;`
&lt;!-- 编译结果 --&gt;
&lt;div class="global_name_className global_name_otherClassName"&gt;</pre>
<p>注意：</p>
<ol>
<li>你可以在单个composes规则中指定多个目标类名，例如<pre class="crayon-plain-tag">composes: classNameA classNameB;</pre></li>
<li>你可以指定多次composes规则，但是必须位于其它规则的前面</li>
<li>组合仅仅支持局部选择器，并且选择器必须是单个样式类名</li>
</ol>
<div class="blog_h2"><span class="graybg">依赖</span></div>
<p>使用组合时，你可以compose来自其它CSS模块的样式类：</p>
<pre class="crayon-plain-tag">.title {
    composes: className from './another.css';
    color: red;
}</pre>
<p>注意： compose其它CSS文件中定义的样式类时，其应用顺序是不确定的，因此你不能假设同名规则的覆盖情况。</p>
<div class="blog_h2"><span class="graybg">在Webpack中使用</span></div>
<p>CM为流行的构建工具提供了插件支持。使用Webpack时，你可以通过css-loader来支持CM。配置示例：</p>
<pre class="crayon-plain-tag">// Webpack 1.x配置
module.exports = {
    module: {
        loaders: [
            {
                test: /\.css$/,
                // modules参数导致css-loader工作在module模式下
                loader: "style-loader!css-loader?modules"
                // 你可以使用localIdentName参数来定制全局名生成规则，默认规则为[hash:base64]
                loader: "style-loader!css-loader?modules&amp;localIdentName=[path][name]-[local]-[hash:base64:5]"
            },
        ]
    }
};</pre>
<p>当工作在modules模式下时，css-loader会把所有局部样式类名编译成唯一的全局名。 </p>
<div class="blog_h2"><span class="graybg">联用CSS预处理器</span></div>
<p>CM产生的ICSS文件与SASS/SCSS/LESS之类的CSS预处理器是兼容的。你可以把预处理器的loader添加到加载器链中：</p>
<pre class="crayon-plain-tag">{
    test: /\.scss$/,
    loaders: [
        // 注意链条是从右向左（从下向上）执行的
        'style',
        'css?modules&amp;importLoaders=1&amp;localIdentName=[path]___[name]__[local]___[hash:base64:5]',
        'resolve-url',
        'sass'
    ]
} </pre>
<div class="blog_h1"><span class="graybg">React CSS Modules</span></div>
<p>在开发React应用程序时，可以考虑使用PostCSS插件React CSS Modules来代替CM，其优势是：</p>
<ol>
<li>不要求你使用驼峰式大小写的类名。使用CM时，Webpack的css-loader强制要求驼峰式大小写</li>
<li>不需要在代码中到处引用styles对象中的属性</li>
<li>全局CSS与CSS模块很容易区分：<br />
<pre class="crayon-plain-tag">// RCS使用className引用全局CSS类名，styleName引用局部CSS类名
&lt;div className='global-css' styleName='local-module'&gt;&lt;/div&gt; </pre>
</li>
<li>如果styleName属性引用一个未定义的CSS模块，你可以得到一个警告而非错误</li>
</ol>
<div class="blog_h2"><span class="graybg">实现机制</span></div>
<p>RCS扩展了目标React组件的render方法，依据输出元素上的styleName属性的值来寻找styles对象中的CSS模块，然后把找到的全局样式类名<span style="background-color: #c0c0c0;">附加</span>到元素的className后面。</p>
<p>这意味着，组件必须要被RCS装饰。</p>
<div class="blog_h3">
<div class="blog_h3"><span class="graybg" style="color: #007755;">在Webpack中使用</span></div>
</div>
<div class="blog_h3"><span class="graybg">开发环境</span></div>
<p>在开发环境下，你可能希望启用Sourcemaps和热模块替换（Hot Module Replacement） 。加载器style-loader已经支持HMR，因此HMR是开箱即用的。</p>
<p>参考下面的内容配置加载器：</p>
<pre class="crayon-plain-tag">// 需要预先安装style-loader、css-loader
{
    test: /\.css$/,
    loaders: [
        'style?sourceMap',
        'css?modules&amp;importLoaders=1&amp;localIdentName=[path]___[name]__[local]___[hash:base64:5]'
    ]
}</pre>
<div class="blog_h3"><span class="graybg">生产环境</span></div>
<p>在生产环境下，你可能希望把CSS块合并到单个文件中：</p>
<pre class="crayon-plain-tag">// 需要预先安装style-loader、css-loader
// extract-text-webpack-plugin 用于把CSS块合并到单个文件
{
    module: {
        loaders: [
            // ExtractTextPlugin v2x
            {
                test: /\.css$/,
                loader: ExtractTextPlugin.extract( {
                    notExtractLoader: 'style-loader',
                    loader: 'css?modules&amp;importLoaders=1&amp;localIdentName=[path]___[name]__[local]___[hash:base64:5]!resolve-url!postcss',
                } ),
            }
        ]
    },
    plugins: [
        new ExtractTextPlugin( {
            filename: 'app.css',
            allChunks: true
        } )
    ]
}</pre>
<div class="blog_h2"><span class="graybg">联用React</span></div>
<p>要使用RCM，你的React组件必须被CSSModules装饰：</p>
<pre class="crayon-plain-tag">import React from 'react';
import CSSModules from 'react-css-modules';
import styles from './table.css';

// 可以使用ES7装饰器语法
@CSSModules(styles, options)
class Table extends React.Component {
    render() {
        return &lt;div styleName='table'&gt;
            &lt;div styleName='row'&gt;
                &lt;div styleName='cell'&gt;A0&lt;/div&gt;
            &lt;/div&gt;
        &lt;/div&gt;;
    }
}

// 必须装饰React组件，否则无法使用styleName属性
export default CSSModules( Table, styles, options);</pre>
<p>其中options支持以下选项：</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>allowMultiple</td>
<td>默认false。是否允许多个CSS模块名，如果设置为false，以下代码会导致错误：<br />
<pre class="crayon-plain-tag">&lt;div styleName='foo bar' /&gt; </pre>
</td>
</tr>
<tr>
<td>errorWhenNotFound</td>
<td>默认true。如果styleName指定了无法在styles对象中找到的CSS模块，是否报错</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">styles属性</span></div>
<p>被装饰的React组件，会获得<pre class="crayon-plain-tag">this.props.styles</pre> 属性，它的值与CSSModules调用的第2参数是一个对象：</p>
<pre class="crayon-plain-tag">&lt;div&gt;
   // 这两种写法等价
    &lt;p styleName='foo'&gt;&lt;/p&gt;
    &lt;p className={this.props.styles.foo}&gt;&lt;/p&gt;
&lt;/div&gt;;</pre>
<div class="blog_h3"><span class="graybg">子组件</span></div>
<p>默认的，你不能在子组件中的输出元素中使用styleName属性，因为子组件没有被CSSModules装饰。你可以：</p>
<ol>
<li>使用装饰过的子组件</li>
<li>使用从被装饰过的父组件中继承得到的<pre class="crayon-plain-tag">this.props.styles</pre></li>
</ol>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/css-modules-study-note">CSS Modules学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/css-modules-study-note/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>React Router学习笔记</title>
		<link>https://blog.gmem.cc/react-router-study-note</link>
		<comments>https://blog.gmem.cc/react-router-study-note#comments</comments>
		<pubDate>Tue, 20 Dec 2016 09:24:50 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[JavaScript]]></category>
		<category><![CDATA[学习笔记]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=14091</guid>
		<description><![CDATA[<p>简介 React Router（本文后续简写为RR）是一个专门服务于React应用的强大的路由库。利用它你可以轻松的建立URL和UI之间的对应关系、在浏览器历史记录中自由导航。 本章先手工实现一个简单的路由机制，然后利用RR进行改造，以了解RR的优势和基本功能。 手工实现路由 [crayon-69e8384d5e8fc269532064/] 通过RR实现路由 [crayon-69e8384d5e900278446386/] 可以看到，RR知道如何（根据配置信息）构建嵌套的UI，你不需要手工的读取URL并找到对应的Child组件， App与其内部的子组件实现了解耦。 添加更多的UI 现在，我们在inbox这个路径下面再嵌套一级子路由：  [crayon-69e8384d5e903746228102/] 这样，当你访问/inbox/messages/123时，RR将会构建如下组件层次： [crayon-69e8384d5e905444778701/] 当你访问/inbox时，则构建如下组件层次： [crayon-69e8384d5e907274556933/] 总之，URL的层次和组件的层次具有对应关系，上层组件的this.props.children，等于匹配的下层路由的component属性所指向的组件。注意这种对应关系不一定是“严格”的： URL中的一个“层次”，可以跨越多个以斜杠[crayon-69e8384d5e90a007638680-i/] 划分的片断 组件中的一个“层次”，可以包含多级React元素。层次的结果取决于你在何处声明this.props.children 这种对应关系很自然，但是如果要手工实现的话需要编写不少罗嗦的代码。 获取路径变量和URL参数 <a class="read-more" href="https://blog.gmem.cc/react-router-study-note">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/react-router-study-note">React Router学习笔记</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>React Router（本文后续简写为RR）是一个专门服务于React应用的强大的路由库。利用它你可以轻松的建立URL和UI之间的对应关系、在浏览器历史记录中自由导航。</p>
<p>本章先手工实现一个简单的路由机制，然后利用RR进行改造，以了解RR的优势和基本功能。</p>
<div class="blog_h2"><span class="graybg">手工实现路由</span></div>
<pre class="crayon-plain-tag">import React from 'react'
import { render } from 'react-dom'

const About = React.createClass( { /*...*/ } )
const Inbox = React.createClass( { /*...*/ } )
const Home = React.createClass( { /*...*/ } )

const App = React.createClass( {
    getInitialState() {
        return {
            // 使用Hash部分进行路由
            route: window.location.hash.substr( 1 )
        }
    },

    componentDidMount() {
        // 当Hash部分变化后，需要改变React状态进行重渲染
        window.addEventListener( 'hashchange', () =&gt; {
            this.setState( {
                route: window.location.hash.substr( 1 )
            } )
        } )
    },

    render() {
        // 根据状态，也就是URL的Hash的不同，决定渲染哪个子组件
        let Child
        switch ( this.state.route ) {
            case '/about': Child = About; break;
            case '/inbox': Child = Inbox; break;
            default: Child = Home;
        }

        return (
            &lt;div&gt;
                &lt;h1&gt;App&lt;/h1&gt;
                &lt;ul&gt;
                    &lt;li&gt;&lt;a href="#/about"&gt;About&lt;/a&gt;&lt;/li&gt;
                    &lt;li&gt;&lt;a href="#/inbox"&gt;Inbox&lt;/a&gt;&lt;/li&gt;
                &lt;/ul&gt;
                &lt;Child/&gt;
            &lt;/div&gt;
        )
    }
} )

render( &lt;App /&gt;, document.body )</pre>
<div class="blog_h2"><span class="graybg">通过RR实现路由</span></div>
<pre class="crayon-plain-tag">import React from 'react'
import ReactDom from 'react-dom'

import { Router, Route, IndexRoute, Link, hashHistory } from 'react-router'

const App = React.createClass( {
    render() {
        return (
            &lt;div&gt;
                &lt;h1&gt;App&lt;/h1&gt; 
                /* 将a替换为RR提供的Link组件 */
                &lt;ul&gt;
                    &lt;li&gt;&lt;Link to="/about" &gt;About&lt;/Link&gt;&lt;/li&gt;
                    &lt;li&gt;&lt;Link to='/inbox' &gt;Inbox&lt;/Link&gt;&lt;/li&gt;
                &lt;/ul&gt;
                /* 这里不需要手工判断渲染哪个子路由组件，RR知道 */
                {this.props.children}
            &lt;/div&gt;
        )
    }
} )

/**
 * 渲染的是由Router/Route定义的路由规则，而非UI组件
 */
ReactDom.render( (
    /* 使用hashHistory，表示路由的判断依据是URL的Hash部分 */
    &lt;Router history={hashHistory}&gt;
        /* path指定路由基准URL */
        &lt;Route path="/" component={App}&gt;
            /* 路由的嵌套层次，与组件的嵌套层次对应 */
            &lt;IndexRoute component={Home}/&gt;  /* 默认UI */
            &lt;Route path="about" component={About}/&gt;  /* Hash部分是About时，例如/#/about */
            &lt;Route path="inbox" component={Inbox}/&gt;  /* Hash部分是Inbox时 */
        &lt;/Route&gt;
    &lt;/Router&gt;
), document.body )</pre>
<p>可以看到，RR知道如何（根据配置信息）构建嵌套的UI，你不需要手工的读取URL并找到对应的Child组件， App与其内部的子组件实现了解耦。</p>
<div class="blog_h3"><span class="graybg">添加更多的UI</span></div>
<p>现在，我们在inbox这个路径下面再嵌套一级子路由： </p>
<pre class="crayon-plain-tag">const Message = React.createClass( {
    render() {
        return &lt;h3&gt;Message&lt;/h3&gt;
    }
} )

const Inbox = React.createClass( {
    render() {
        return (
            &lt;div&gt;
                &lt;h2&gt;Inbox&lt;/h2&gt;
                /* 渲染下一级子路由组件 */
                {this.props.children}
            &lt;/div&gt;
        )
    }
} )

ReactDom.render( (
    &lt;Router history={hashHistory}&gt;
        &lt;Route path="/" component={App}&gt;
            &lt;IndexRoute component={Home}/&gt;
            &lt;Route path="about" component={About}/&gt;
            &lt;Route path="inbox" component={Inbox}&gt;
                /* 嵌套路由 */
                &lt;IndexRoute component={InboxStats}/&gt; /* 嵌套路由的默认UI */
                /* 在类似/inbox/messages/123这样的URL下渲染Message组件 */
                &lt;Route path="messages/:id" component={Message}/&gt;
            &lt;/Route&gt;
        &lt;/Route&gt;
    &lt;/Router&gt;
), document.body )</pre>
<p>这样，当你访问/inbox/messages/123时，RR将会构建如下组件层次：</p>
<pre class="crayon-plain-tag">&lt;App&gt;
    &lt;Inbox&gt;
        &lt;Message params={{ id: 'Jkei3c32' }}/&gt;
    &lt;/Inbox&gt;
&lt;/App&gt;</pre>
<p>当你访问/inbox时，则构建如下组件层次：</p>
<pre class="crayon-plain-tag">&lt;App&gt;
    &lt;Inbox&gt;
        &lt;InboxStats/&gt;
    &lt;/Inbox&gt;
&lt;/App&gt;</pre>
<p>总之，URL的层次和组件的层次具有对应关系，<span style="background-color: #c0c0c0;">上层组件的this.props.children，等于匹配的下层路由的component属性所指向的组件</span>。注意这种对应关系不一定是“严格”的：</p>
<ol>
<li>URL中的一个“层次”，可以跨越多个以斜杠<pre class="crayon-plain-tag">/</pre> 划分的片断</li>
<li>组件中的一个“层次”，可以包含多级React元素。层次的结果取决于你在何处声明this.props.children</li>
</ol>
<p>这种对应关系很自然，但是如果要手工实现的话需要编写不少罗嗦的代码。</p>
<div class="blog_h3"><span class="graybg">获取路径变量和URL参数</span></div>
<p>React组件会被RR自动注入路径变量，例如messages/:id中的id，可以通过<pre class="crayon-plain-tag">props.params.id</pre> 读取到：</p>
<pre class="crayon-plain-tag">const Message = React.createClass( {
    componentDidMount() {
        const id = this.props.params.id
        fetchMessage( id, function ( err, message ) {
            this.setState( { message: message } )
        } )
    }
} )</pre>
<p>URL中附带的查询参数也被注入，例如/user?name=alex中的name，可以通过<pre class="crayon-plain-tag">props.location.query.name</pre> 读取到。</p>
<div class="blog_h1"><span class="graybg">基础</span></div>
<div class="blog_h2"><span class="graybg">Router组件</span></div>
<p>该组件用于将RR引入到React应用程序中，渲染时一般将其作为JSX根元素：</p>
<pre class="crayon-plain-tag">ReactDom.render( (
    &lt;Router history={hashHistory}&gt;
       /* ... */
    &lt;/Router&gt;
), document.body )</pre>
<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>history</td>
<td>该router需要监听的history对象，通常是browserHistory或hashHistory</td>
</tr>
<tr>
<td>children</td>
<td>一个或者多个Route、PlainRoute组件。指定路由的规则集</td>
</tr>
<tr>
<td>routes</td>
<td>children的别名</td>
</tr>
<tr>
<td>createElement</td>
<td>当router准备渲染一个组件树分支时，调用此函数进行React元素的创建。你可以用该方法控制创建过程：<br />
<pre class="crayon-plain-tag">&lt;Router createElement={createElement} /&gt;

// 默认行为
function createElement(Component, props) {
    return &lt;Component {...props} /&gt;
}</pre>
</td>
</tr>
<tr>
<td>onError</td>
<td>
<p>方法签名：<pre class="crayon-plain-tag">onError(error)</pre> </p>
<p>进行路由匹配的时候可能出现错误，这些错误通常来自于那些异步的特性，例如route.getComponents、route.getIndexRoute、route.getChildRoutes等。你可以基于此方法进行捕获和处理</p>
</td>
</tr>
<tr>
<td>onUpdate</td>
<td>
<p>一旦router改变其状态以对URL变更进行响应，即调用此方法</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">Route组件</span></div>
<p>这是一个配置组件（Configuration Components），定义一个路由规则，该规则将一个URL片断和一个React组件对应起来。路由规则可以嵌套，并与URL嵌套、组件嵌套对应。</p>
<p>JSX中的Route元素实际上等价于Route的子类型PlainRoute。</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>path</td>
<td>此规则匹配的URL片断，可以使用绝对路径（以/开头），也可以使用相对于父Route元素的想对路径</td>
</tr>
<tr>
<td>component</td>
<td>对于根元素，指定渲染什么React组件，对于非根元素，指定上级Route元素的<pre class="crayon-plain-tag">props.children</pre> 对应什么组件</td>
</tr>
<tr>
<td>components</td>
<td>以对象形式指定多个<a id="named-components"></a>命名组件，父路由组件可以通过<pre class="crayon-plain-tag">props[name]</pre> 来访问这些组件。举例：<br />
<pre class="crayon-plain-tag">&lt;Route path="groups" components={{main: Groups, sidebar: GroupsSidebar}} /&gt;</pre></p>
<p> 在父路由组件中，你可以这些引用这两个子路由组件：</p>
<p><pre class="crayon-plain-tag">const {main, sidebar} = this.props
return (
    &lt;div&gt;
        &lt;div className="Main"&gt;
            {main}
        &lt;/div&gt;
        &lt;div className="Sidebar"&gt;
            {sidebar}
        &lt;/div&gt;
    &lt;/div&gt;
)</pre>
</td>
</tr>
<tr>
<td>getComponent</td>
<td>
<p>方法签名：<pre class="crayon-plain-tag">getComponent(nextState, callback)</pre> 。callback的签名：<pre class="crayon-plain-tag">cb(err, component)</pre> </p>
<p>类似于component，但是用于动态路由。RR会在需要的时候调用此函数，加载需要的组件。此方法的实现示例：</p>
<pre class="crayon-plain-tag">(nextState, cb) =&gt; {
    // 异步的查找、加载组件
    // 执行RR提供的回调
    cb(null, Course)
}</pre>
</td>
</tr>
<tr>
<td>onEnter</td>
<td rowspan="2">
<p>方法签名：
<pre class="crayon-plain-tag">// nextState 下一个（即将进入的）路由状态对象
// replace 函数，调用它可以重定向到另外一个位置
// callback(err) 如果提供此参数，则onEnter被异步调用，路由切换在callback调用前一直阻塞
// 函数中的this指向当前Route对象
onEnter(nextState, replace, callback?)

// 前一个（正要离开的）路由状态对象
onLeave(prevState)</pre>
<p>路由切换被确认时执行的钩子函数。使用这些钩子可以做很多事情，例如：</p>
<ol>
<li>Enter路由之前执行身份验证</li>
<li>Leave路由之前保存数据</li>
</ol>
<p>在路由切换时：</p>
<ol>
<li>首先在<span style="background-color: #c0c0c0;">旧叶子Route</span>上执行<pre class="crayon-plain-tag">onLeave</pre> 钩子，并向上逐级执行直到遇到<span style="background-color: #c0c0c0;">新旧URL公用的祖先路由（此祖先路由的onLeave不执行）为止</span></li>
<li>然后在<span style="background-color: #c0c0c0;">最顶级的新旧URL不同的祖先路由</span>上执行<pre class="crayon-plain-tag">onEnter</pre> 钩子，并向下逐级执行到<span style="background-color: #c0c0c0;">新叶子路由</span>为止</li>
</ol>
<p>示例：</p>
<p><pre class="crayon-plain-tag">const userIsInATeam = (nextState, replace, callback) =&gt; {
    // 路由切换前需要准备数据
    fetch(/**/) .then(response = response.json()) .then(userTeams =&gt; {
            // 某些条件下，可以重定向路由到其它位置
            if (userTeams.length === 0) {
                replace(`/users/${nextState.params.userId}/teams/new`)
            }
            // 调用RR提供的回调，以完成路由切换
            callback();
        }) .catch(error =&gt; {
            // 在此执行错误处理
            callback(error);
        })
}

&lt;Route path="/users/:userId/teams" onEnter={userIsInATeam}/&gt;</pre>
</td>
</tr>
<tr>
<td>onLeave</td>
</tr>
<tr>
<td>onChange</td>
<td>
<p>方法签名：<pre class="crayon-plain-tag">onChange(prevState, nextState, replace, callback?)</pre> ，参数作用类似于onEnter</p>
<p>当浏览器的地址发生改变，但是当前路由对象既不Enter也不Leave的情况下触发的钩子方法。触发时机举例：</p>
<ol>
<li>当前路由的子路由发生变化</li>
<li>URL查询参数部分发生变化</li>
</ol>
</td>
</tr>
<tr>
<td colspan="2">
<p><em><strong>以下属性为PlainRoute专有</strong></em></p>
</td>
</tr>
<tr>
<td>childRoutes</td>
<td>
<p>子路由集合，对应JSX中Route的子元素</p>
</td>
</tr>
<tr>
<td>getChildRoutes</td>
<td>
<p>方法签名：</p>
<pre class="crayon-plain-tag">// partialNextState 部分解析的下一状态，因为子路由尚未加载，因而路由匹配尚未完成
// callback(err, routesArray) 
getChildRoutes(partialNextState, callback)</pre>
<p>类似于childRoutes，但是用于动态路由。RR会在需要的时候调用此函数，以完成路由的匹配。示例：</p>
<pre class="crayon-plain-tag">// 静态路由，子路由代码需要同步加载
let myRoute = {
    path: 'course/:courseId',
    childRoutes: [
        announcementsRoute,
        gradesRoute,
        assignmentsRoute
    ]
}

// 异步子路由，仅仅在匹配 course/**/**这样的URL时才异步的加载
let myRoute = {
    path: 'course/:courseId',
    getChildRoutes(location, cb) {
        // 在此加载子路由
        // 然后调用RR提供的回调
        cb(null, [announcementsRoute, gradesRoute, assignmentsRoute])
    }
}


// 导航依赖的子路由，可以链接到某些状态
&lt;Link to="/picture/123" state={{ fromDashboard: true }}/&gt;

let myRoute = {
    path: 'picture/:id',
    getChildRoutes(partialNextState, cb) {
        let {state} = partialNextState

        // 根据条件，加载不同的子路由
        if (state &amp;&amp; state.fromDashboard) {
            cb(null, [dashboardPictureRoute])
        } else {
            cb(null, [pictureRoute])
        }
    }
}</pre>
</td>
</tr>
<tr>
<td>indexRoute</td>
<td>
<p>参考IndexRoute子元素
</td>
</tr>
<tr>
<td>getIndexRoute</td>
<td>
<p>方法签名：<pre class="crayon-plain-tag">getIndexRoute(partialNextState, callback)</pre> </p>
<p>异步的加载IndexRoute</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">IndexRoute组件</span></div>
<p>这是一个配置组件，属于特殊的路由规则，当（下层）URL片断为空时，匹配此规则。例如：</p>
<pre class="crayon-plain-tag">&lt;Route path="/" component={App}&gt;
    &lt;IndexRoute component={Dashboard} /&gt;
&lt;/Route&gt;</pre>
<p>当你访问URL / 时，会渲染App组件，且其children为一个Dashboard组件。</p>
<div class="blog_h2"><span class="graybg">Redirect组件</span></div>
<p>这是一个配置组件，用于URL的重定向，例如：</p>
<pre class="crayon-plain-tag">&lt;Redirect from="messages/:id" to="/messages/:id" /&gt;</pre>
<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>from</td>
<td>需要重定向的URL，包含路径变量部分</td>
</tr>
<tr>
<td>to</td>
<td>重定向到的目标</td>
</tr>
<tr>
<td>query</td>
<td>查询参数部分，默认情况下自动把from的查询参数带过去</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">IndexRedirect组件</span></div>
<p>这是一个配置组件，指定指定默认使用的下层URL。例如：</p>
<pre class="crayon-plain-tag">&lt;Route path="/" component={App}&gt;
    &lt;IndexRedirect to="/welcome" /&gt;
    // 访问 / 时自动重定向到 /welcome
    &lt;Route path="welcome" component={Welcome} /&gt;
&lt;/Route&gt;</pre>
<div class="blog_h2"><span class="graybg">Link组件</span></div>
<p>该组件用于触发路由切换，以便在应用程序中导航，该组件渲染为a标签。</p>
<p>如果Link指向的路由恰恰是应用中的当前路由（URL匹配），则RR自动给Link添加activeClassName样式类，并且其activeStyle指定的样式被启用。</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>to</td>
<td>
<p>一个位置描述符（location descriptor）对象，或者一个字符串。如果不指定该属性，生成一个没有href的a标签</p>
<p>使用字符串时，指定需要链接到的绝对路径。使用位置描述符时，可以指定以下属性：</p>
<ol>
<li>pathname  URL的路径部分</li>
<li>query  一个对象，指定URL查询参数部分</li>
<li>hash  URL的#部分，例如#a-hash</li>
<li>state 需要持久化到location的状态</li>
</ol>
<p>示例：</p>
<p><pre class="crayon-plain-tag">// 字符串
&lt;Link to="/hello"&gt;Hello&lt;/Link&gt;
// 位置描述符
&lt;Link to={{ pathname: '/hello', query: { name: 'ryan' } }}&gt;Hello&lt;/Link&gt;
// 返回位置描述符的函数
&lt;Link to={location =&gt; ({ ...location, query: { name: 'ryan' } })}&gt;Hello&lt;/Link&gt; </pre>
</td>
</tr>
<tr>
<td>activeClassName</td>
<td>to指定的路由是当前路由时，启用的样式类</td>
</tr>
<tr>
<td>activeStyle</td>
<td>to指定的路由是当前路由时，启用的样式</td>
</tr>
<tr>
<td>onClick</td>
<td>点击时执行的函数，在此函数调用e.preventDefault()可以阻止路由切换</td>
</tr>
<tr>
<td>onlyActiveOnIndex</td>
<td>
<p>仅当to与当前路由精确匹配时，才认为是Active。等价于<pre class="crayon-plain-tag">&lt;IndexLink&gt;</pre> 组件</p>
<p>如果不指定该属性，那么<pre class="crayon-plain-tag">&lt;Link to="/"&gt;Home&lt;/Link&gt;</pre> 这个链接总是Active，因为URL总是以/开头。此时可以使用<pre class="crayon-plain-tag">&lt;IndexLink to="/"&gt;Home&lt;/IndexLink&gt;</pre> 代替，这样仅当URL为/时才匹配</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">规则匹配算法</span></div>
<p>同级别的Route，<span style="background-color: #c0c0c0;">先声明的具有更高的优先级</span>。</p>
<p>URL语法和匹配规则如下：</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>:paramName</td>
<td>
<p>匹配一个URL片断，直到遇到 <pre class="crayon-plain-tag">/</pre> 、<pre class="crayon-plain-tag">？</pre> 或者<pre class="crayon-plain-tag">#</pre> ，组件自动获得<pre class="crayon-plain-tag">params.paramName</pre> 属性。举例：</p>
<p><pre class="crayon-plain-tag">// 匹配：/hello/alex  /hello/wong
&lt;Route path="/hello/:name"&gt;</pre>
</td>
</tr>
<tr>
<td>()</td>
<td>包围URL的一部分，表示该部分是可选的。举例：<br />
<pre class="crayon-plain-tag">// 匹配：/hello/alex  /hello
&lt;Route path="/hello(/:name)"&gt;</pre>
</td>
</tr>
<tr>
<td>*</td>
<td>非贪婪的通配，遇到此通配符后面指定的那个字符之前一直匹配。捕获到的匹配项会存入<pre class="crayon-plain-tag">params.splat</pre> 属性中。举例：<br />
<pre class="crayon-plain-tag">// 匹配：/files/hello.jpg  /files/hello.html
&lt;Route path="/files/*.*"&gt;</pre>
</td>
</tr>
<tr>
<td>**</td>
<td>贪婪的通配，直到遇到 <pre class="crayon-plain-tag">/</pre> 、<pre class="crayon-plain-tag">？</pre> 或者<pre class="crayon-plain-tag">#</pre> 。捕获到的匹配项会存入<pre class="crayon-plain-tag">params.splat</pre> 属性中。举例：<br />
<pre class="crayon-plain-tag">// 匹配：/files/hello.jpg   /files/path/to/file.jpg
&lt;Route path="/**/*.jpg"&gt;  </pre>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">Histories</span></div>
<p>RR在<a href="https://github.com/mjackson/history">history</a>库的基础上构建，一个history对象可以监听浏览器的URL的改变，并把URL解析为一个location对象。RR使用此location对象来匹配路由规则并渲染正确的组件树。</p>
<p>缺省可用的history实现包括三种：</p>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">History实现</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>browserHistory</td>
<td>
<p>对于运行在浏览器中的应用，这是推荐的实现。它使用浏览器内置的<a href="https://developer.mozilla.org/en-US/docs/Web/API/History">History</a> API来操控URL，能够创建“真实的”URL，例如 gmem.cc/some/path</p>
<p><strong>服务器配置</strong></p>
<p>要在所有浏览器中使用这种History实现，需要服务器的支持。你可能需要将<span style="background-color: #c0c0c0;">某个通配的路径映射到同一个HTML文件</span>，例如gmem.cc/**总是映射到gmem.cc/index.html。</p>
<p>使用Express作为服务器时，可以参考如下代码：</p>
<pre class="crayon-plain-tag">app.get('*', function (request, response){
    response.sendFile(path.resolve(__dirname, 'public', 'index.html'))
})</pre>
<p>使用Nginx时，可以使用try_files指令：</p>
<pre class="crayon-plain-tag">server {
    location / {
        try_files $uri /index.html;
    }
}</pre>
<p>使用Apache时可以使用rewrite模块：</p>
<pre class="crayon-plain-tag">RewriteBase /
RewriteRule ^index\.html$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]</pre>
<p><strong>IE8,IE9支持</strong></p>
<p>RR会自动检测浏览器是否支持History API，如果不支持，所有URL转换都会导致完全的页面reload</p>
</td>
</tr>
<tr>
<td>hashHistory</td>
<td>
<p>仅仅使用URL的哈希（#）部分，生成gmem.cc/#/some/path风格的URL</p>
<p>该实现的优点是不需要配置服务器。缺点是URL比较难堪而且不支持服务器端渲染</p>
</td>
</tr>
<tr>
<td>createMemoryHistory</td>
<td>不会操控浏览器地址栏，使用RR进行服务器端渲染时使用，也可以用于测试React Native等其它渲染环境</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">withRouter函数</span></div>
<p>用于包装其它React组件，为其提供<pre class="crayon-plain-tag">props.router</pre> 属性。函数签名：</p>
<pre class="crayon-plain-tag">/**
 * Component 被包装的组件
 * options 选项：
 *      withRef  如果为true，则包装后的组件的getWrappedInstance()返回被包装组件
 */
withRouter(Component, [options])</pre>
<p>props.router属性与context.router是同一种对象。</p>
<div class="blog_h2"><span class="graybg">RouterContext</span></div>
<p>依据给定的路由状态，渲染对应的组件树。Router组件使用了该组件，在React组件的上下文对象上添加一个<pre class="crayon-plain-tag">this.context.router</pre> 属性。</p>
<div class="blog_h3"><span class="graybg"><a id="context.router"></a>context.router</span></div>
<p>该对象提供与路由有关的方法和数据，你可以使用该对象进行编程式的路由控制：</p>
<table class=" fixed-word-wrap full-width">
<thead>
<tr>
<td style="width: 35%; text-align: center;">属性/方法</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td><pre class="crayon-plain-tag">push(pathOrLoc)</pre> </td>
<td>切换路由到一个新的URL，并在浏览器历史中压入条目。示例：<br />
<pre class="crayon-plain-tag">// 使用字符串
router.push( '/users/alex' )
// 使用位置描述符对象
router.push( {
    pathname: '/users/alex',
    query: { modal: true },
    state: { fromDashboard: true }
} ) </pre>
</td>
</tr>
<tr>
<td><pre class="crayon-plain-tag">replace(pathOrLoc)</pre> </td>
<td>类似于push，但是替换浏览器历史的当前条目</td>
</tr>
<tr>
<td><pre class="crayon-plain-tag">go(n)</pre> </td>
<td>在浏览器历史中前进或者后退</td>
</tr>
<tr>
<td><pre class="crayon-plain-tag">goBack()</pre> </td>
<td>在浏览器历史中后退一步</td>
</tr>
<tr>
<td><pre class="crayon-plain-tag">goForward()</pre> </td>
<td>在浏览器历史中前进一步</td>
</tr>
<tr>
<td><pre class="crayon-plain-tag">setRouteLeaveHook(route, hook)</pre> </td>
<td>注册一个钩子，在离开route这个路由时调用，用于导航确认</td>
</tr>
<tr>
<td><pre class="crayon-plain-tag">createPath(pathOrLoc, query)</pre> </td>
<td>根据router的配置，把查询参数对象转换为URL路径名（不包括协议、域名部分）</td>
</tr>
<tr>
<td><pre class="crayon-plain-tag">createHref(pathOrLoc, query)</pre> </td>
<td>
<p>创建一个URL，如果使用hashHitory，会自动在URL路径名前面添加#/</p>
</td>
</tr>
<tr>
<td><pre class="crayon-plain-tag">isActive(pathOrLoc, indexOnly)</pre> </td>
<td>判断pathOrLoc是否对应当前路由。如果匹配路由R，则同样匹配R的祖先路由，那么在R及其祖先路由对应的Component中调用该方法，均返回true</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">路由组件</span></div>
<p>所谓路由组件，是指路由规则（Route）关联的组件，当路由规则被匹配、且父路由组件输出了当前路由组件时，路由组件被自动渲染。</p>
<p>路由组件被渲染时，RR自动为其注入一些属性。</p>
<div class="blog_h3"><span class="graybg">注入的属性</span></div>
<table class=" fixed-word-wrap full-width">
<thead>
<tr>
<td style="width: 20%; text-align: center;">属性</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>location</td>
<td>当前<a href="https://github.com/mjackson/history/blob/v2.x/docs/Location.md">location</a>对象</td>
</tr>
<tr>
<td>params</td>
<td>URL中的动态部分，包括路径变量捕获</td>
</tr>
<tr>
<td>route</td>
<td>导致此组件被渲染的路由对象</td>
</tr>
<tr>
<td>router</td>
<td>与<a href="#context.router">context.router</a>相同</td>
</tr>
<tr>
<td>routeParams</td>
<td>
<p>捕获到的、在Route.path中直接声明的路径变量。如果Route.path为users/:userId 而当前URL为/users/123/portfolios/345。那么：</p>
<ol>
<li>this.props.routeParams为<pre class="crayon-plain-tag">{userId: '123'}</pre> </li>
<li>this.props.params为<pre class="crayon-plain-tag">{userId: '123', portfolioId: '345'}</pre> </li>
</ol>
</td>
</tr>
<tr>
<td>children</td>
<td>匹配的、将被渲染的子路由组件。如果当前路由使用<a href="#named-components">命名组件</a>，则该属性为undefined。各命名组件将作为this.props的直接属性</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">高级主题</span></div>
<div class="blog_h2"><span class="graybg">动态路由</span></div>
<p>对于大型应用来说，下载尽可能少的JavaScript文件以启动应用很重要，最好仅下载与当前的UI相关的JS。如果不这样做，用户将忍受过长的加载时间。生产环境下我们通常使用模块化，配合代码分割（Code Splitting）技术来满足尽快启动的需求，然后随着用户的操作不断加载用到的JS。</p>
<p>路由定义了UI的样子，很自然的可以作为代码分割点。</p>
<p>RR支持异步的读取路由规则、异步的加载组件。在最初的捆绑文件（Bundle，即启动应用的那个代码分割块）中，你只需要提供一个路由规则，其它规则可以后续按需加载。</p>
<p>Route组件可以定义getChildRoutes、getIndexRoute、getComponents方法，这些方法仅在需要的时候才会被调用以完成规则匹配和组件渲染。RR称这种方式为渐进匹配（gradual  matching）——逐步的匹配URL片段且仅仅加载必要的信息。</p>
<p>下面是一个动态路由的示例：</p>
<pre class="crayon-plain-tag">const CourseRoute = {
    path: 'course/:courseId',
    // 当尝试导航到course/**/**时，下面的方法被调用
    getChildRoutes(partialNextState, callback) {
        // 这里使用Webpack的CommonJS扩展
        require.ensure([], function (require) {
            callback(null, [
                // 同步加载子路由定义模块
                require('./routes/Announcements'),
                require('./routes/Assignments'),
                require('./routes/Grades'),
            ])
        })
    },
    // 当尝试导航到course/**时，下面的方法被调用
    getIndexRoute(partialNextState, callback) {
        require.ensure([], function (require) {
            callback(null, {
                component: require('./components/Index'),
            })
        })
    },
    // 当尝试导航到course/**时，该方法被调用，加载对应的组件
    getComponents(nextState, callback) {
        require.ensure([], function (require) {
            callback(null, require('./components/Course'))
        })
    }
}</pre>
<div class="blog_h2"><span class="graybg">导航确认</span></div>
<p>你可以调用router的<pre class="crayon-plain-tag">setRouteLeaveHook</pre> 方法，设置一个钩子，当离开某个路由时，该钩子会被执行，你可以利用此钩子：</p>
<ol>
<li>向用户做出提示</li>
<li>阻止导航的发生</li>
</ol>
<p> 示例：</p>
<pre class="crayon-plain-tag">// v2.4.0引入的withRouter可以向组件注入当前router对象
const Home = withRouter(
    React.createClass({

        componentDidMount() {
            // 为当前组件对应的route对象设置钩子
            this.props.router.setRouteLeaveHook(this.props.route, this.routerWillLeave)
        },

        routerWillLeave(nextLocation) {
            //  返回false禁止导航
            //  返回字符串则让用户确认是否导航
            if (!this.state.isSaved)
                return 'Your work is not saved! Are you sure you want to leave?'
        },

    })
)</pre>
<div class="blog_h2"><span class="graybg">组件外导航</span></div>
<p>你可以使用<pre class="crayon-plain-tag">withRouter</pre> 包装一个组件，从而通过<pre class="crayon-plain-tag">this.props.router</pre> 获得当前router对象的引用。有了router对象后你就可以随意导航（触发路由切换）了。</p>
<p>在React组件外部，例如Redux中间件或者Flux Action的代码中，你可以通过history对象进行导航：</p>
<pre class="crayon-plain-tag">import {browserHistory} from 'react-router'

// 导航到 /some/path.
browserHistory.push('/some/path')

// 后退到前一个URL
browserHistory.goBack()</pre>
<div class="blog_h2"><span class="graybg">最小化Bundle的尺寸</span></div>
<p>为了简便，RR通过顶级模块react-router暴露了完整的API。这导致整个RR库及其依赖被包含到入口点Bundle中，从而增加了Bundle的大小。为了避免此问题，可以从react-router/lib的子模块进行导入：</p>
<pre class="crayon-plain-tag">import { Link, Route, Router } from 'react-router'
// 可以改写为：
import Link from 'react-router/lib/Link'
import Route from 'react-router/lib/Route'
import Router from 'react-router/lib/Router'</pre>
<p>&nbsp;</p>
<p>&nbsp;</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/react-router-study-note">React Router学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/react-router-study-note/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Redux学习笔记</title>
		<link>https://blog.gmem.cc/redux-study-note</link>
		<comments>https://blog.gmem.cc/redux-study-note#comments</comments>
		<pubDate>Thu, 15 Dec 2016 03:34:04 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[JavaScript]]></category>
		<category><![CDATA[学习笔记]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=14012</guid>
		<description><![CDATA[<p>简介 Redux是一个存储JavaScript应用状态的容器，它是更早出现的Flux架构的一种变体，Redux弱化了Dispatcher并将其职责转移到全局唯一的Store身上。官网称Redux为可预测的（predictable）——一个操作（Action）会引发应用状态怎么样改变是确定的，这应该是声明在自己对MVVM流派的反对态度。 导致Redux之类的框架出现的原因，主要是越来越复杂的单页面JavaScript应用。开发人员需要维护越来越多的“状态”，这些状态包括： 业务数据：服务器响应、缓存数据、客户端新创建的数据 UI状态：当前路由、选中的Tab页… 手工管理这些状态是困难的。模型更新模型、视图更新模型、模型更新视图…会让你无法判断系统为什么处于现在的状态，进而导致BUG难以重现、调试困难。新的前端开发需求则加剧了这种困难，这些需求包括服务器端渲染、路由前数据抓取、优化的数据更新。 通过限制状态何时、如何被更新，Redux让应用状态的变更变得可预测。这些限制体现在Redux的三个理念中。 Redux提供的API非常少，实质上Redux并非单一的框架，它更是一套约定。这套约定规定了函数的规格、函数之间应该如何交互。使用Redux时大部分时间你都在编写函数。 Redux支持Web客户端、Web服务器、甚至Native环境，易于测试。尽管Redux经常和React一起使用，但是配合其它视图层的库也是可以的。 何时使用Redux 下面列出可以引入Redux的应用场景： 视图需要从多个数据源获得数据 不同视图需要共享状态 大量服务器交互，使用Websocket 对比Flux Redux没有Flux架构中的Dispatcher角色 Redux只有唯一的Store，而Flux可以具有很多个。当应用程序规模增大时，你不能增加Store，而应把Reducer切分为多个小的Reducer，这些小的Reducer独立的操控状态树的一部分 理念 Redux遵循三个基本的理念： Single source of <a class="read-more" href="https://blog.gmem.cc/redux-study-note">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/redux-study-note">Redux学习笔记</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>Redux是一个存储JavaScript应用状态的容器，它是更早出现的Flux架构的一种变体，Redux弱化了Dispatcher并将其职责转移到全局唯一的Store身上。官网称Redux为可预测的（predictable）——一个操作（Action）会引发应用状态怎么样改变是确定的，这应该是声明在自己对MVVM流派的反对态度。</p>
<p>导致Redux之类的框架出现的原因，主要是越来越复杂的单页面JavaScript应用。开发人员需要维护越来越多的“状态”，这些状态包括：</p>
<ol>
<li>业务数据：服务器响应、缓存数据、客户端新创建的数据</li>
<li>UI状态：当前路由、选中的Tab页…</li>
</ol>
<p>手工管理这些状态是困难的。模型更新模型、视图更新模型、模型更新视图…会让你无法判断系统为什么处于现在的状态，进而导致BUG难以重现、调试困难。新的前端开发需求则加剧了这种困难，这些需求包括服务器端渲染、路由前数据抓取、优化的数据更新。</p>
<p>通过限制状态何时、如何被更新，Redux让应用状态的变更变得可预测。这些限制体现在Redux的三个理念中。</p>
<p>Redux提供的API非常少，实质上Redux并非单一的框架，它更是一套约定。这套约定规定了函数的规格、函数之间应该如何交互。使用Redux时大部分时间你都在编写函数。</p>
<p>Redux支持Web客户端、Web服务器、甚至Native环境，易于测试。尽管Redux经常和React一起使用，但是配合其它视图层的库也是可以的。</p>
<div class="blog_h2"><span class="graybg">何时使用Redux</span></div>
<p>下面列出可以引入Redux的应用场景：</p>
<ol>
<li>视图需要从多个数据源获得数据</li>
<li>不同视图需要共享状态</li>
<li>大量服务器交互，使用Websocket</li>
</ol>
<div class="blog_h2"><span class="graybg">对比Flux</span></div>
<ol>
<li>Redux没有Flux架构中的Dispatcher角色</li>
<li>Redux只有唯一的Store，而Flux可以具有很多个。当应用程序规模增大时，你不能增加Store，而应把Reducer切分为多个小的Reducer，<span style="background-color: #c0c0c0;">这些小的Reducer独立的操控状态树的一部分</span></li>
</ol>
<div class="blog_h2"><span class="graybg">理念</span></div>
<p>Redux遵循三个基本的理念：</p>
<ol>
<li>Single source of truth：整个应用程序的状态，并存放在单个Store持有的对象树中</li>
<li>State is readonly：修改对象树的唯一办法是触发一个Action，Action是描述所发生事情（用户、定时器操作或者服务器数据到达）的简单对象</li>
<li>Changes are made with pure function：为了基于Action改变状态树，你必须编写纯函数风格的Reducers。Reducer可以接收当前状态 + Action，并返回一个新状态</li>
</ol>
<pre class="crayon-plain-tag">import {createStore} from 'redux'

// 这是一个Reducer函数，从当前状态、动作推导新状态，它必须是纯函数
function counter( state = 0, action ) {
    switch ( action.type ) {
        case 'INCREMENT':
            return state + 1
        case 'DECREMENT':
            return state - 1
        default:
            return state
    }
}
/**
 * 创建Store时，需要指定其用什么Reducer来处理Action
 * 尽管没有硬性限制，你还是应当仅创建一个Store
 */
let store = createStore( counter );

/**
 * 你可以使用调用Store的subscribe()方法来注册监听器，这样状态树变更后你可以得到通知，以更新UI或者把当前状态
 * 存放到LocalStorage
 *
 * 通常，你会使用某种View层的绑定，例如React绑定，因而不需要直接调用该方法
 */
store.subscribe( () =&gt;
    console.log( store.getState() )
);
// 唯一改变Store内部状态的方法：
store.dispatch( { type: 'INCREMENT' } ); </pre>
<div class="blog_h2"><span class="graybg">安装</span></div>
<p>执行下面的命令，为当前工程添加Redux支持：</p>
<pre class="crayon-plain-tag"># Redux
npm install --save redux

# Redux的React绑定
npm install --save react-redux

# Redux开发者工具
npm install --save-dev redux-devtools</pre>
<p>与Redux不同，其生态系统中的很多包不提供UMD构建，因此建议使用CommonJS模块捆绑器，例如Webpack或者Browserify。</p>
<div class="blog_h1"><span class="graybg">基础</span></div>
<div class="blog_h2"><span class="graybg">Actions</span></div>
<p>Action是应用程序发送给Store的数据的载荷（Payloads）。对于Store来说，它是唯一的信息来源。要将Action送给Store处理，需要调用<pre class="crayon-plain-tag">store.dispatch()</pre> 方法。</p>
<p>Action是简单JS对象，必须具有一个type属性，表示Action的类型。type属性通常都是字符串常量，应用程序规模扩大后，你应当把这些type常量独立到模块中：</p>
<pre class="crayon-plain-tag">import { ADD_TODO, REMOVE_TODO } from '../actionTypes'</pre>
<p>除了type以外Action还需要什么属性，完全取决于你的需求。</p>
<div class="blog_h3"><span class="graybg">规范化Action</span></div>
<p>Redux推荐遵循<a href="https://github.com/acdlite/flux-standard-action">Flux标准Action</a>（FSA）写法：</p>
<pre class="crayon-plain-tag">// 一个普通的FSA
var action = {
    type: 'ADD_TODO',
    payload: {
        text: 'Do something.'
    }
};
// 一个代表错误的FSA
var err = {
    type: 'ADD_TODO',
    payload: new Error(),
    error: true
};</pre>
<p>FSA规定：</p>
<ol>
<li>Action必须是简单JS对象，且必须具有type属性</li>
<li>Action可以具有error、payload、meta属性</li>
<li> 其它未提及的属性均不允许出现</li>
</ol>
<div class="blog_h3"><span class="graybg">Action Creators</span></div>
<p>这是一类用于创建Action的助手类函数，让Action的发出者免于关注Action的类型：</p>
<pre class="crayon-plain-tag">function addTodo( text ) {
    return {
        type: ADD_TODO,
        text
    }
}</pre>
<p>在典型的Flux架构中，Action Creator负责触发dispatch()的调用。Redux却不是这样，你可以创建bound action creator来执行dispatch：</p>
<pre class="crayon-plain-tag">const boundAddTodo = (text) =&gt; dispatch(addTodo(text))</pre>
<p>你可以调用<pre class="crayon-plain-tag">bindActionCreators()</pre> 将多个Action Creator绑定到dispatch()调用。</p>
<div class="blog_h2"><span class="graybg">Reducers</span></div>
<p>Action描述发生了什么，而Reducer则描述应用状态应该如何依据Action而改变，它根据前一个状态 + Action推导出下一个状态：</p>
<pre class="crayon-plain-tag">(previousState, action) =&gt; newState</pre>
<p>之所以叫做Reducer，是因为这种函数可以传递给数组的reduce函数：</p>
<pre class="crayon-plain-tag">Array.prototype.reduce(reducer, ?initialValue);</pre>
<p>Reducer<span style="background-color: rgb(192, 192, 192);">必须基于函数式编程范式编写，必须实现为纯函数</span>：</p>
<ol>
<li>immutable：不要修改参数</li>
<li>不要调用任何具有边际效应（side effects ）的API，例如路由转换</li>
<li>不要调用任何非纯函数，例如Date.now()</li>
<li>对于相同的输入，总是返回相同的结果</li>
</ol>
<p>提醒事项应用中最初的Reducer实现如下：</p>
<pre class="crayon-plain-tag">// 初始状态
const initialState = {
    visibilityFilter: VisibilityFilters.SHOW_ALL,
    todos: []
}
// Reducer最初从初始状态开始处理，这里使用ES6默认参数
function todoApp(state = initialState, action) {
    return state; // 暂时不进行处理，仅仅返回原始状态
}</pre>
<p>现在，添加代码，让它能够处理SET_VISIBILITY_FILTER、ADD_TODO这两个Action：</p>
<pre class="crayon-plain-tag">function todoApp( state = initialState, action ) {
    switch ( action.type ) {
        // 设置显示过滤器
        case SET_VISIBILITY_FILTER:
            // 不能改变状态，因此这里调用assign复制对象
            return Object.assign( {}, state, {
                visibilityFilter: action.filter
            } )
        // 添加一个提醒事项
        case ADD_TODO:
            return Object.assign( {}, state, {
                // 也不能改变状态的任何子属性
                todos: [
                    ...state.todos,
                    {
                        text: action.text,
                        completed: false
                    }
                ]
            } )
        // 改变一个提醒事项的完成状态
        case TOGGLE_TODO:
            return Object.assign( {}, state, {
                // 映射出一个新的数组，因为原数组不能被改变
                todos: state.todos.map( ( todo, index ) =&gt; {
                    if ( index === action.index ) {
                        // 这个提醒事项需要被改变，因此必须在副本上执行修改
                        return Object.assign( {}, todo, {
                            completed: !todo.completed
                        } )
                    }
                    // 你可以使用原状态树中的节点，只要不改变它
                    return todo
                } )
            } )
        default:
            // 对于未知的Action，应该总是返回前一个状态
            return state
    }
}</pre>
<p>要处理更多的Action，可以继续增加case子句。从上面的代码可以看到，为了保证纯函数的特征，需要编写很多数据拷贝代码。你可以使用</p>
<ol>
<li><a href="https://github.com/kolodny/immutability-helper">immutability-helper</a>：改变对象的拷贝，保持源对象不变</li>
<li><a href="https://github.com/substantial/updeep">updeep</a>：以声明式/不变性的方法，更新嵌套的冻结对象、数组</li>
<li><a href="http://facebook.github.io/immutable-js/">Immutable</a>：不变的JavaScript集合</li>
</ol>
<p>之类支持深层更新（deep update）的库，以降低工作量同时提供安全性（防止意外操作导致纯函数特征破坏）。</p>
<div class="blog_h3"><span class="graybg">分割Reducer</span></div>
<p>Case语句越来越多以后，上面的Reducer会变得又长又臭，难以读懂。而且，尽管SET_VISIBILITY_FILTER和其它Action操作的数据完全没有交集，它们都被迫处理整个状态树。</p>
<p>我们可以对上面的代码进行重构：</p>
<pre class="crayon-plain-tag">// 这个子Reducer仅仅处理状态树的visibilityFilter子树
function visibilityFilter( state = SHOW_ALL, action ) {
    switch ( action.type ) {
        case SET_VISIBILITY_FILTER:
            return action.filter
        default:
            return state
    }
}
// 这个子Reducer仅仅处理状态树的todos子树
function todos( state = [], action ) {
    switch ( action.type ) {
        case ADD_TODO:
            return [
                ...state,
                {
                    text: action.text,
                    completed: false
                }
            ]
        case TOGGLE_TODO:
            return state.map( ( todo, index ) =&gt; {
                if ( index === action.index ) {
                    return Object.assign( {}, todo, {
                        completed: !todo.completed
                    } )
                }
                return todo
            } )
        default:
            // 不是我关心的Action，一定要返回原来的状态
            return state
    }
}
// 父Reducer不再负责提供初始状态，由管理状态子树的子Reducer负责
function todoApp( state = {}, action ) {
    switch ( action.type ) {
        case SET_VISIBILITY_FILTER:
            return Object.assign( {}, state, {
                visibilityFilter: visibilityFilter( state.visibilityFilter, action )
            } )
        case ADD_TODO:
        case TOGGLE_TODO:
            // 父Reducer中的框架代码负责状态子树的抽取、拼回
            return Object.assign( {}, state, {
                todos: todos( state.todos, action )
            } )
        default:
            return state
    }
}</pre>
<p>重构以后的代码，具有以下特点：</p>
<ol>
<li>原先Reducer的体积变小了，部分工作委托给与Reducer行为类似的子Reducer处理</li>
<li>子Reducer仅仅处理它关注的状态子树</li>
<li>父Reducer负责把状态子树抽取出来供子Reducer处理，并把后者返回的新状态子树拼接回去</li>
</ol>
<p>这段重构，蕴含了Redux应用的一个<span style="background-color: #c0c0c0;">基础模式：Reducer组合（composition）</span> 。</p>
<p>由于子Reducer仅仅处理它关注的那个子树，上面的父Reducer中switch语句可以安全的移除：</p>
<pre class="crayon-plain-tag">function todoApp( state = {}, action ) {
    // 简单的返回新状态
    return {
        // visibilityFilter这个状态子树由visibilityFilter这个子Reducer处理
        visibilityFilter: visibilityFilter( state.visibilityFilter, action ),
        // todos这个状态子树由todos这个子Reducer处理
        todos: todos( state.todos, action )
    }
}</pre>
<p>这样，尽管任意Action需要交给所有子Reducer处理，但是子Reducer<span style="background-color: #c0c0c0;">遇到其不关心的Action会立即返回未经改变的状态</span>，因此不会引入太多的性能损耗。 </p>
<p>Redux提供了一个工具函数<pre class="crayon-plain-tag">combineReducers()</pre> ，可以更进一步简化上一段代码：</p>
<pre class="crayon-plain-tag">import { combineReducers } from 'redux';

// 如果状态子树的属性名，与子Reducer的函数名一致：
const todoApp = combineReducers({
    visibilityFilter,
    todos
});
// 如果不一致：
const todoApp = combineReducers({
    visibilityFilter : vf
    todos : tds
});

// 如果你使用ES6，可以把所有相关的子Reducer编写在一个模块中并全部export，然后：
import * as reducers from './reducers';
const todoApp = combineReducers(reducers);</pre>
<div class="blog_h2"><span class="graybg">Store</span></div>
<p>通过Store，Action才可以传递给Reducer。Store具有以下职责：</p>
<ol>
<li>持有应用程序的状态</li>
<li>允许代码通过<pre class="crayon-plain-tag">getState()</pre> 方法访问状态</li>
<li>允许代码通过<pre class="crayon-plain-tag">dispatch(action)</pre> 派发新的Action，并交由Reducer处理</li>
<li>允许代码通过<pre class="crayon-plain-tag">subscribe(listener)</pre> 注册监听器，该方法的返回值用于解除监听器。监听器在状态改变后自动调用</li>
</ol>
<div class="blog_h3"><span class="graybg">创建Store</span></div>
<p>记住：整个应用程序（精确来说是页面，如果一个系统由多个页面组成，就对应这里的多个应用程序）只有一个Store。下面的代码示例了如何创建Store：</p>
<pre class="crayon-plain-tag">import { createStore } from 'redux';
import todoApp from './reducers';
let store = createStore(todoApp);</pre>
<p>你可以提供第二个可选参数，为状态赋初值：</p>
<pre class="crayon-plain-tag">let store = createStore(todoApp, window.STATE_FROM_SERVER);</pre>
<div class="blog_h3"><span class="graybg">状态的组织</span></div>
<p>在编写代码之前应该好好考虑Store中存储的应用程序状态的结构，以“提醒事项”应用为例，状态包括两类不同的东西：</p>
<ol>
<li>UI状态：过滤规则，是否显示已经完成的提醒事项</li>
<li>数据：真正的提醒事项的列表</li>
</ol>
<p>你需要把这两类东西都放在Store中，但是注意将它们分开：</p>
<pre class="crayon-plain-tag">{
    visibilityFilter: 'SHOW_ALL',
    todos: [
        {
            text: 'Consider using Redux',
            completed: true,
        },
        {
            text: 'Keep all state in a single tree',
            completed: false
        }
    ]
}</pre>
<p>对于大部分复杂的应用程序，Redux建议尽量规范化（normalized）的存储数据，避免嵌套，就像数据库那样：</p>
<ol>
<li>使用ID作为key来引用单条数据，例如：<pre class="crayon-plain-tag">todosById: { id -&gt; todo }</pre> </li>
<li>使用IDs来引用数据的列表，例如：<pre class="crayon-plain-tag">todos: array&lt;id&gt;</pre> </li>
<li>数据之间有关联时，通过ID/IDs引用，而不是嵌套关联对象</li>
</ol>
<div class="blog_h3"><span class="graybg">分发Action</span></div>
<p>调用<pre class="crayon-plain-tag">store.dispatch()</pre> 即可分发一个Action，甚至不需要View层的参与。这让Redux应用容易自动化的测试。</p>
<pre class="crayon-plain-tag">import { addTodo, toggleTodo, setVisibilityFilter, VisibilityFilters } from './actions'
// 打印初始状态
console.log(store.getState())

// 注册监听器，当状态改变后，打印新的状态
let unsubscribe = store.subscribe(() =&gt;
    console.log(store.getState())
)

// 发布一些事件
store.dispatch(addTodo('Learn about actions'))
store.dispatch(addTodo('Learn about reducers'))
store.dispatch(addTodo('Learn about store'))
store.dispatch(toggleTodo(0))
store.dispatch(toggleTodo(1))
store.dispatch(setVisibilityFilter(VisibilityFilters.SHOW_COMPLETED))

// 解除监听器
unsubscribe() </pre>
<p>尽管你可以直接调用dispatch方法，但配合React时最好使用react-redux的<pre class="crayon-plain-tag">connect()</pre> 方法。</p>
<div class="blog_h2"><span class="graybg">数据流</span></div>
<p>Redux架构是围绕构建<span style="background-color: #c0c0c0;">严格的单向数据流</span>来构建的。 </p>
<p>这意味着，应用程序中的所有数据遵循相同的生命周期模式。这让你的代码逻辑易于理解、行为可预测。Redux同样鼓励数据规范化（normalization）——状态组织类似于遵循范式的数据库，以便减少<span style="background-color: #c0c0c0;">相互不感知（由不同子Reducer处理）的、实际上是相同数据的状态子树</span>。</p>
<p>如上面章节讨论的那样，Redux中数据的生命周期通常是：</p>
<ol>
<li><pre class="crayon-plain-tag">store.dispatch(action)</pre>  被调用，新数据作为Action的载荷被发送</li>
<li>Store调用Reducer函数，处理Action</li>
<li>根Reducer可以合并多个子Reducer的输出，形成完整的状态树</li>
<li>根Reducer返回状态树给Store，由后者保存</li>
<li>Store发布事件，所有订阅者获得最新状态的通知</li>
</ol>
<div class="blog_h1"><span class="graybg">联用React</span></div>
<p>Redux可以与很多其它框架/库联用，包括React、Angular、Ember、jQuery。对于React这样的库来说，Redux特别适用，因为React将UI表示为关于状态的函数，而Redux正好能够很好的管理状态。</p>
<p>本章以提醒事项应用为例来阐述Redux和React的整合。</p>
<div class="blog_h2"><span class="graybg">安装react-redux</span></div>
<p>Redux的React绑定是独立的项目，可以执行下面的命令添加到当前工程中：</p>
<pre class="crayon-plain-tag">npm install --save react-redux</pre>
<div class="blog_h2"><span class="graybg">展示/容器组件</span></div>
<p>React-Redux将React组件分为展示（Presentational）组件和容器（Container）组件：</p>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;"> </td>
<td style="text-align: center;">展示组件</td>
<td style="text-align: center;">容器组件</td>
</tr>
</thead>
<tbody>
<tr>
<td><strong>目的</strong></td>
<td>利用HTML标签、样式表，来确定页面长什么样</td>
<td>进行数据获取、状态更新</td>
</tr>
<tr>
<td><strong>是否感知Redux</strong></td>
<td>否</td>
<td>是</td>
</tr>
<tr>
<td><strong>能否读取数据</strong></td>
<td>从props读取数据</td>
<td>订阅Redux管理的状态</td>
</tr>
<tr>
<td><strong>能否改变数据</strong></td>
<td>从props中执行回调</td>
<td>派发Redux Action</td>
</tr>
<tr>
<td><strong>来源</strong></td>
<td>手工编写</td>
<td>通常由react-redux自动生成</td>
</tr>
</tbody>
</table>
<p>我们编写的绝大部分组件都是展示组件，只有少数用来对接到Redux的容器组件。</p>
<p>尽管你可以调用store.subscribe()，来创建自己的容器组件，但是这样做并不被推荐。React-Redux提供了<pre class="crayon-plain-tag">connect()</pre> 函数，调用它就可以生成容器组件了。</p>
<div class="blog_h2"><span class="graybg">设计组件层次</span></div>
<div class="blog_h3"><span class="graybg">展示组件</span></div>
<p>React组件层次往往和Redux状态树多少有些对应关系。还是以提醒事项为例，展示组件可以包括：</p>
<ol>
<li>TodoList 显示所有可见的提醒事项的列表</li>
<li>Todo 单个提醒事项条目</li>
<li>Link 带有回调的链接，点击后其onClick被调用</li>
<li>Footer 切换可以看到全部还是仅未完成的提醒事项</li>
<li>App 根组件</li>
</ol>
<div class="blog_h3"><span class="graybg">容器组件</span></div>
<p>为了把展示组件连接到Redux，我们需要一些容器组件：</p>
<ol>
<li>VisibleTodoList：根据当前的过滤器设置，过滤不显示的提醒事项，并渲染TodoList</li>
<li>FilterLink：根据当前过滤器设置，设置Link的样式和点击事件处理函数</li>
</ol>
<div class="blog_h3"><span class="graybg">其它组件</span></div>
<p>某些组件很难严格的划分到上面两类中。例如，某些时候表单字段和功能是紧耦合在一起的：</p>
<ol>
<li>AddTodo，一个附带了按钮的输入框</li>
</ol>
<p>我们可以把AddTodo拆分为两个组件，但是由于此组件非常小，混合展示、逻辑在其中也无可厚非。如果该组件以后变大了可以考虑重构。</p>
<div class="blog_h2"><span class="graybg">组件实现</span></div>
<div class="blog_h3"><span class="graybg">展示组件</span></div>
<pre class="crayon-plain-tag">import React from 'react'
import Footer from './Footer'
import AddTodo from '../containers/AddTodo'
import VisibleTodoList from '../containers/VisibleTodoList'

// 顶级展示组件
const App = () =&gt; (
    &lt;div&gt;
        // 添加提醒事项条目的输入框和按钮
        &lt;AddTodo /&gt;
        // 容器，可见的提醒事项列表
        &lt;VisibleTodoList /&gt;
        // 尾部链接区，切换哪些事项可见
        &lt;Footer /&gt;
    &lt;/div&gt;
)

export default App</pre><br />
<pre class="crayon-plain-tag">import React from 'react'
import FilterLink from '../containers/FilterLink'

const Footer = () =&gt; (
    &lt;p&gt;
        Show:
        {" "}
        // 每个链接都使用容器包装
        &lt;FilterLink filter="SHOW_ALL"&gt;
            All  // 作为Link的children属性传入
        &lt;/FilterLink&gt;
        {", "}
        &lt;FilterLink filter="SHOW_ACTIVE"&gt;
            Active
        &lt;/FilterLink&gt;
        {", "}
        &lt;FilterLink filter="SHOW_COMPLETED"&gt;
            Completed
        &lt;/FilterLink&gt;
    &lt;/p&gt;
)

export default Footer</pre><br />
<pre class="crayon-plain-tag">import React, { PropTypes } from 'react'

const Link = ({ active, children, onClick }) =&gt; {
    if (active) return &lt;span&gt;{children}&lt;/span&gt;
    else return &lt;a href="#" onClick={e =&gt; { e.preventDefault(); onClick() }} &gt;{children}&lt;/a&gt;
}

Link.propTypes = {
    active: PropTypes.bool.isRequired,
    children: PropTypes.node.isRequired,
    onClick: PropTypes.func.isRequired
}

export default Link</pre><br />
<pre class="crayon-plain-tag">import React, {PropTypes} from 'react'
// 提醒事项条目，所有状态、配置、包括事件处理函数，从外部传入
const Todo = ({onClick, completed, text}) =&gt; {
    // 样式取决于状态
    let todoStyle = {
        textDecoration: completed ? 'line-through' : 'none'
    }
    return &lt;li onClick={onClick} style={todoStyle}&gt; {text}&lt;/li&gt;
}

Todo.propTypes = {
    onClick: PropTypes.func.isRequired,
    completed: PropTypes.bool.isRequired,
    text: PropTypes.string.isRequired
}

export default Todo</pre><br />
<pre class="crayon-plain-tag">import React, { PropTypes } from 'react'
// 引入依赖的组件
import Todo from './Todo'
// 提醒事项列表，所有状态、配置、包括事件处理函数，仍然从外部传入
const TodoList = ({ todos, onTodoClick }) =&gt; (
    // 注意展开操作符，可以简化React元素属性传入，注意onClick的绑定
    &lt;ul&gt;
        {todos.map(todo =&gt; &lt;Todo key={todo.id} {...todo} onClick={() =&gt; onTodoClick(todo.id)} /&gt; )}
    &lt;/ul&gt;
)

TodoList.propTypes = {
    todos: PropTypes.arrayOf(PropTypes.shape({
        id: PropTypes.number.isRequired,
        completed: PropTypes.bool.isRequired,
        text: PropTypes.string.isRequired
    }).isRequired).isRequired,
    onTodoClick: PropTypes.func.isRequired
}

export default TodoList</pre>
<div class="blog_h3"><span class="graybg">容器组件</span></div>
<p>上面的展示组件已经引用了一些容器组件，容器组件用来把某些展示层组件挂钩到Redux。严格的说，容器组件仅仅是通过<pre class="crayon-plain-tag">store.subscribe()</pre> 来读取Redux一部分状态树、并且为其内部的展示组件提供props的普通React组件。</p>
<p>编写容器组件时，一般使用React-Redux提供的<pre class="crayon-plain-tag">connect()</pre> 函数。此函数提供了必要的优化，可以避免不必要的重渲染。</p>
<p>使用connect函数时，你需要定义：</p>
<ol>
<li>一个名为<pre class="crayon-plain-tag">mapStateToProps</pre> 函数。<span style="background-color: #c0c0c0;">该函数指明如何将当前Redux状态转换为当前容器包装的展示组件的props</span>。该函数接收两个参数，第一个是当前Redux Store的状态，第二个是传递给容器（而非它包装的展示组件）的属性集</li>
<li>一个名为<pre class="crayon-plain-tag">mapDispatchToProps</pre> 的函数，该函数接受Store.dispatch、展示组件的props作为入参，返回<span style="background-color: #c0c0c0;">展示层组件所需要的那些事件处理函数</span>。这些事件处理函数作为props的组成部分</li>
</ol>
<p>准备好这两个函数之后，将其作为入参传递给connect函数，即可获得自动创建的容器组件。</p>
<p>链接的容器：</p>
<pre class="crayon-plain-tag">import { connect } from 'react-redux'
import { setVisibilityFilter } from '../actions'
import Link from '../components/Link'

// 将Redux状态转换为容器包裹的展示组件的props
const mapStateToProps = (state, ownProps) =&gt; {
    return {
        active: ownProps.filter === state.visibilityFilter
    }
}
// 指定展示组件中的事件处理函数，dispatch为Store的派发函数，ownProps则为事件发生时容器组件的属性
const mapDispatchToProps = (dispatch, ownProps) =&gt; {
    return {
        onClick: () =&gt; {
            dispatch(setVisibilityFilter(ownProps.filter))
        }
    }
}
// 将上面两个函数传递给connect函数，生成容器
const FilterLink = connect(
    mapStateToProps,
    mapDispatchToProps
)(Link)

export default FilterLink</pre>
<p><a id="VisibleTodoList"></a>提醒事项列表的容器：</p>
<pre class="crayon-plain-tag">import {connect} from 'react-redux'
import {toggleTodo} from '../actions'
import TodoList from '../components/TodoList'

const getVisibleTodos = (todos, filter) =&gt; {
    switch (filter) {
        case 'SHOW_ALL':
            return todos
        case 'SHOW_COMPLETED':
            // 系统状态中，提醒事项只有一个列表。每次展示时，基于此列表返回一个临时的副本
            return todos.filter(t =&gt; t.completed)
        case 'SHOW_ACTIVE':
            return todos.filter(t =&gt; !t.completed)
    }
}

// 根据过滤器判断展示哪些提醒事项条目
const mapStateToProps = (state) =&gt; {
    return {
        todos: getVisibleTodos(state.todos, state.visibilityFilter)
    }
}

// 指定事件处理函数
const mapDispatchToProps = (dispatch) =&gt; {
    return {
        onTodoClick: (id) =&gt; {
            dispatch(toggleTodo(id))
        }
    }
}
const VisibleTodoList = connect(
    mapStateToProps,
    mapDispatchToProps
)(TodoList)

export default VisibleTodoList</pre>
<div class="blog_h3"><span class="graybg">其它组件</span></div>
<p>上面几个是纯粹的容器组件，它们不包含任何展示性的代码。实现这类组件的关键是基于两个回调提供，将展示组件连接到Redux的全部必要信息。</p>
<p>某些情况下展示和逻辑是自然耦合在一起的，并且组件规模很小，展示、容器组件可以合二为一：</p>
<pre class="crayon-plain-tag">import React from 'react'
import { connect } from 'react-redux'
import { addTodo } from '../actions'

// Store.dispatch方法作为唯一传入的props，“展示组件”自己负责Action的分发
let AddTodo = ({ dispatch }) =&gt; {
    let input

    return (
        &lt;div&gt;
            &lt;form onSubmit={e =&gt; {
                e.preventDefault()
                if (!input.value.trim()) {
                    return
                }
                dispatch(addTodo(input.value))
                input.value = ''
            }}&gt;
                // ref回调会在组件挂载时执行，传递当前元素
                &lt;input ref={node =&gt; {
                    input = node
                }} /&gt;
                &lt;button type="submit"&gt;
                    Add Todo
                &lt;/button&gt;
            &lt;/form&gt;
        &lt;/div&gt;
    )
}

// 调用connect时不提供回调入参
// 创建React组件实例时，dispatch由Redux传入
AddTodo = connect()(AddTodo)

export default AddTodo</pre>
<div class="blog_h3"><span class="graybg">传入Store</span></div>
<p>上节中AddTodo组件会在运行时自动被Redux注入dispatch方法，这意味着你必须把Store实例告诉给Redux。这可以通过Provider组件完成：</p>
<pre class="crayon-plain-tag">import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import todoApp from './reducers'
import App from './components/App'

let store = createStore(todoApp)

render(
    &lt;Provider store={store}&gt;
        &lt;App /&gt;
    &lt;/Provider&gt;,
    document.getElementById('root')
)</pre>
<div class="blog_h3"><span class="graybg">Redux相关代码</span></div>
<p>以下代码仅仅和Redux相关。Action Creators：</p>
<pre class="crayon-plain-tag">let nextTodoId = 0
export const addTodo = (text) =&gt; {
    return {
        type: 'ADD_TODO',
        id: nextTodoId++,
        text
    }
}

export const setVisibilityFilter = (filter) =&gt; {
    return {
        type: 'SET_VISIBILITY_FILTER',
        filter
    }
}

export const toggleTodo = (id) =&gt; {
    return {
        type: 'TOGGLE_TODO',
        id
    }
}</pre>
<p> Reducers：</p>
<pre class="crayon-plain-tag">const todo = (state = {}, action) =&gt; {
    switch (action.type) {
        case 'ADD_TODO':
            return {
                id: action.id,
                text: action.text,
                completed: false
            }
        case 'TOGGLE_TODO':
            if (state.id !== action.id) {
                return state
            }

            return Object.assign({}, state, {
                completed: !state.completed
            })

        default:
            return state
    }
}

const todos = (state = [], action) =&gt; {
    switch (action.type) {
        case 'ADD_TODO':
            return [
                ...state,
                todo(undefined, action)
            ]
        case 'TOGGLE_TODO':
            return state.map(t =&gt;
                todo(t, action)
            )
        default:
            return state
    }
}

export default todos</pre><br />
<pre class="crayon-plain-tag">const visibilityFilter = (state = 'SHOW_ALL', action) =&gt; {
    switch (action.type) {
        case 'SET_VISIBILITY_FILTER':
            return action.filter
        default:
            return state
    }
}

export default visibilityFilter</pre><br />
<pre class="crayon-plain-tag">import { combineReducers } from 'redux'
import todos from './todos'
import visibilityFilter from './visibilityFilter'

const todoApp = combineReducers({
    todos,
    visibilityFilter
})

export default todoApp</pre>
<div class="blog_h1"><span class="graybg">联用React Router</span></div>
<p><a href="https://github.com/reactjs/react-router">React-Router</a>为React库添加了路由功能，联合Redux使用该React Router时，Redux作为应用状态、数据的唯一依据（Source of truth），而React Router则是URL的唯一依据。大部分时间里，Redux和React Router可以相互隔离不相关，除非你希望时间旅行（ time travel）、rewind触发URL改变的Action。</p>
<div class="blog_h2"><span class="graybg">配置Fallback URL</span></div>
<p>在集成React Router之前，我们需要配置开发服务器。开发服务器可能对React Router的路由配置一无所知。例如当你访问/todos时，服务器应该知道要访问的页面是index.html——因为这是单页面应用程序。</p>
<p>如果你使用create-react-app，Fallback URL不需要手工配置。</p>
<div class="blog_h3"><span class="graybg">配置Express</span></div>
<p>如果你使用Express作为服务器，可以这样配置：</p>
<pre class="crayon-plain-tag">app.get( '/*', ( req, res ) =&gt; {
    res.sendfile( path.join( __dirname, 'index.html' ) )
} )</pre>
<div class="blog_h3"><span class="graybg">配置Webpack开发服务器</span></div>
<p>如果你使用Webpack提供的开发服务器，可以这样配置：</p>
<pre class="crayon-plain-tag">devServer: {
    historyApiFallback: true,
}</pre>
<div class="blog_h2"><span class="graybg">连接React Router到Redux</span></div>
<p>根元素内容如下：</p>
<pre class="crayon-plain-tag">import { Router, Route, browserHistory } from 'react-router';
import { Provider } from 'react-redux';

const Root = ( { store } ) =&gt; (
    // 将Router包含在Provider内部，这样Route处理器就可以访问Redux的Store
    &lt;Provider store={store}&gt;
        &lt;Router&gt;
            // Route声明式的映射路由（URL）到应用程序的组件层次中
            &lt;Route path="/" component={App}/&gt;
            // 我们需要读取URL路径变量，因此该写为：
            &lt;Route path="/(:filter)" component={App} /&gt;
        &lt;/Router&gt;
    &lt;/Provider&gt;
);</pre>
<p>如果需要移除URL中的#字符（例如<pre class="crayon-plain-tag">http://localhost:3000/#/...</pre> ） ，你需要引入browserHistory。同时为Router组件传入history属性：</p>
<pre class="crayon-plain-tag">&lt;Router history={browserHistory}&gt;...&lt;/Router&gt;</pre>
<p>除非需要支持IE9之类的老旧浏览器，你总是应该使用browserHistory。</p>
<div class="blog_h2"><span class="graybg">基于React Router导航</span></div>
<p>React Router内置了<pre class="crayon-plain-tag">&lt;Link/&gt;</pre> 组件，使用它可以在应用内自由导航。在提醒实现的例子中，我们可以用此组件代替自己实现的Link：</p>
<pre class="crayon-plain-tag">import React from 'react';
import { Link } from 'react-router';

const FilterLink = ( { filter, children } ) =&gt; {
    let activeStyle = {
        textDecoration: 'none',
        color: 'black'
    };
    return (
        // to指定目标URL
        &lt;Link to={filter === 'all' ? '' : filter} activeStyle={activeStyle}&gt;
            {children}
        &lt;/Link&gt;
    );
};

export default FilterLink;</pre><br />
<pre class="crayon-plain-tag">import React from 'react'
import FilterLink from '../containers/FilterLink'

const Footer = () =&gt; (
    &lt;p&gt;
        Show:
        {" "}
        &lt;FilterLink filter="all"&gt;
            All
        &lt;/FilterLink&gt;
        {", "}
        &lt;FilterLink filter="active"&gt;
            Active
        &lt;/FilterLink&gt;
        {", "}
        &lt;FilterLink filter="completed"&gt;
            Completed
        &lt;/FilterLink&gt;
    &lt;/p&gt;
);</pre>
<p>现在，当你依次点击三个FilterLink时，浏览器的URL会在/complete、/active、/之间切换。使用后退键在历史记录中导航也是支持的。</p>
<div class="blog_h2"><span class="graybg">从URL中读取状态</span></div>
<p>仅仅改变URL是不够的，容器组件的代码也要一并修改，这样UI才能与URL同步更新。首先修改：</p>
<pre class="crayon-plain-tag">const mapStateToProps = ( state, ownProps ) =&gt; {
    return {
        // 原来是getVisibleTodos(state.todos, state.visibilityFilter)
        todos: getVisibleTodos( state.todos, ownProps.filter )
    };
};</pre>
<p>目前，我们尚未传递任何东西到&lt;App/&gt; 中，因此ownProps是一个空对象。为了依据URL来过滤提醒事项，我们需要把URL路径变量传递给&lt;VisibleTodoList /&gt;组件。</p>
<p>之前声明的路由规则：<pre class="crayon-plain-tag">&lt;Route path="/(:filter)" component={App} /&gt;</pre> 导致App内具有一个params属性，该属性是一个对象，其属性与路径变量对应。例如，对于URLlocalhost:3000/completed，params的值为<pre class="crayon-plain-tag">{ filter: 'completed' }</pre> 。</p>
<p>下面修改App组件：</p>
<pre class="crayon-plain-tag">// 使用ES解构操作符读取props
const App = ( { params } ) =&gt; {
    return (
        &lt;div&gt;
            &lt;AddTodo /&gt;
            &lt;VisibleTodoList filter={params.filter || 'all'} /&gt;
            &lt;Footer /&gt;
        &lt;/div&gt;
    );
};</pre>
<p>路由发生后，Redux驱动React进行重新渲染， 此时App的params属性改变，因而渲染的内容也跟着改变。</p>
<div class="blog_h1"><span class="graybg">Reselect </span></div>
<p>Reselect是一个小巧的库，用于创建可备忘的（memoized）、可组合（composable）的选择器函数。这些选择器可以用来有效的从Redux Store中计算衍生数据（ derived data）。</p>
<div class="blog_h2"><span class="graybg">动机</span></div>
<p>回顾一下提醒事项的例子，容器<a href="#VisibleTodoList">VisibleTodoList</a>的mapStateToProps函数调用助手函数getVisibleTodos，来计算需要渲染的todos。这种方法可行，但是有一个缺点：组件每次被更新时，todos都需要重新计算。如果状态树非常大、或者算法本身复杂，那么这种反复计算可能引起性能问题。</p>
<p>Reselect可以帮助避免不必要的重新计算。</p>
<div class="blog_h2"><span class="graybg">创建备忘选择器</span></div>
<p>我们可以使用一个备忘选择器，来替换提醒事项中的getVisibleTodos函数。此选择器会仅仅在state.todos、state.visibilityFilter之一发生改变时，才触发重新计算。而不是在应用的任何无关部分发生变化时都去反复的计算。</p>
<p>要创建备忘选择器，可以调用createSelector函数。该函数接受一个输入选择器的数组、一个转换函数作为入参。如果Redux状态改变导致输入选择器的值改变，则转换函数会被自动调用并返回计算结果；反之，仅仅返回先前计算好的结果。</p>
<pre class="crayon-plain-tag">import { createSelector } from 'reselect'

const getVisibilityFilter = ( state ) =&gt; state.visibilityFilter
const getTodos = ( state ) =&gt; state.todos

export const getVisibleTodos = createSelector(
    // 输入选择器，就是Redux的状态子树的获取函数
    [ getVisibilityFilter, getTodos ],
    // 转换函数，以输入选择器的调用结果为入参
    ( visibilityFilter, todos ) =&gt; {
        switch ( visibilityFilter ) {
            case 'SHOW_ALL':
                return todos
            case 'SHOW_COMPLETED':
                return todos.filter( t =&gt; t.completed )
            case 'SHOW_ACTIVE':
                return todos.filter( t =&gt; !t.completed )
        }
    }
)</pre>
<div class="blog_h2"><span class="graybg">组合选择器</span></div>
<p>从上面的例子中可以看到，所谓选择器就是一个普通函数。 createSelector的返回值则是<span style="background-color: #c0c0c0;">与其输入选择器相同规格的函数</span>，而createSelector又具有依据多个输入选择器生成新选择器的能力。</p>
<p>因此利用createSelector可以实现复杂的备忘选择器——仅当某个子选择器的值改变，其父选择器才重新计算，并且递归的触发祖代选择器的重新计算。</p>
<pre class="crayon-plain-tag">// 为提醒事项添加按关键字过滤的功能
const getKeyword = ( state ) =&gt; state.keyword
// 组合选择器，在根据可见性过滤的基础上，添加关键字过滤。这样，无论是切换所有/未完成/已完成可见，还是改变过滤关键字，都会
// 触发重新计算
const getVisibleTodosFilteredByKeyword = createSelector(
    [ getVisibleTodos, getKeyword ],
    ( visibleTodos, keyword ) =&gt; visibleTodos.filter(
        todo =&gt; todo.text.indexOf( keyword ) &gt; -1
    )
)</pre>
<div class="blog_h2"><span class="graybg">在React中使用</span></div>
<p>如果你使用React-Redux，则可以在mapStateToProps函数中直接调用选择器：</p>
<pre class="crayon-plain-tag">const mapStateToProps = (state) =&gt; {
  return {
    // 像普通函数那样调用
    todos: getVisibleTodos(state)
  }
}</pre>
<div class="blog_h2"><span class="graybg">访问React组件属性</span></div>
<p>前面我们实现的选择器，入参是Redux状态。实际上你可以声明任意个参数：</p>
<pre class="crayon-plain-tag">const mapStateToProps = ( state, props ) =&gt; {
    return {
        todos: getVisibleTodos( state, props )
    }
}</pre>
<div class="blog_h2"><span class="graybg">跨组件共享选择器</span></div>
<p>我们延伸一下提醒事项的需求，现在需要展示三个独立的列表。 代码修改如下：</p>
<pre class="crayon-plain-tag">const App = () =&gt; (
    &lt;div&gt;
        &lt;VisibleTodoList listId="1"/&gt;
        &lt;VisibleTodoList listId="2"/&gt;
        &lt;VisibleTodoList listId="3"/&gt;
    &lt;/div&gt;
)</pre><br />
<pre class="crayon-plain-tag">import { createSelector } from 'reselect'

const getVisibilityFilter = ( state, props ) =&gt; state.todoLists[ props.listId ].visibilityFilter

const getTodos = ( state, props ) =&gt; state.todoLists[ props.listId ].todos

const getVisibleTodos = createSelector(
    [ getVisibilityFilter, getTodos ],
    ( visibilityFilter, todos ) =&gt; {
        switch ( visibilityFilter ) {
            case 'SHOW_COMPLETED':
                return todos.filter( todo =&gt; todo.completed )
            case 'SHOW_ACTIVE':
                return todos.filter( todo =&gt; !todo.completed )
            default:
                return todos
        }
    }
)

export default getVisibleTodos</pre>
<p>上段代码中的选择器需要两个参数：state、props，使用第二个属性的原因是，需要读取容器组件的listId属性以确定使用哪个提醒事项列表。</p>
<pre class="crayon-plain-tag">const mapStateToProps = ( state, props ) =&gt; {
    return {
        // 警告：下面的备忘选择器不能正常工作
        todos: getVisibleTodos( state, props )
    }
}

const mapDispatchToProps = ( dispatch ) =&gt; {/**/}

const VisibleTodoList = connect( mapStateToProps, mapDispatchToProps )( TodoList )

export default VisibleTodoList</pre>
<p>基于以上代码，无法实现“备忘”目的，因为：</p>
<ol>
<li>三个 VisibleTodoList组件共享了同一选择器getVisibleTodos</li>
<li>每当应用程序状态改变后，getVisibleTodos函数的第二参数会以1、2、3依次调用一遍。这导致输入选择器getTodos结果总是变化，因而getVisibleTodos总是需要重新计算</li>
</ol>
<p>那么，如何让同一组件的多个实例使用不同的选择器实例，以便缓存不会相互干扰呢？</p>
<div class="blog_h3"><span class="graybg">mapStateToProps的返回值</span></div>
<p>通常mapStateToProps函数的返回值应该是一个对象，该对象作为展示组件的属性使用。但是从React-Redux 4.3.0开始，此函数可以返回一个函数。</p>
<p>如果mapStateToProps返回一个函数，那么在Redux状态变化时，对于<span style="background-color: #c0c0c0;">每一个容器组件实例，该函数会被调用一次，以计算展示组件的属性</span>。</p>
<p>这样我们就获得创建不同选择器实例的机会了：</p>
<pre class="crayon-plain-tag">const makeMapStateToProps = () =&gt; {
    const getVisibleTodos = createSelector( /**/ )
    // 返回一个箭头函数
    return ( state, props ) =&gt; {
        return {
            todos: getVisibleTodos( state, props )
        }
    }
}

const VisibleTodoList = connect(
    makeMapStateToProps, // 返回函数
    mapDispatchToProps
)( TodoList ) </pre>
<p>&nbsp;</p>
<div class="blog_h1"><span class="graybg">异步Action</span></div>
<p>上面的提醒事项应用，其Action都是同步的——一旦Action被派发，Store立即调用Reducer处理它，应用状态也就立即更新。实际应用中Action常需要触发服务器请求，因而应用状态必须异步更新。</p>
<p>调用异步API时，有两个关键时间点：发起API调用的那一刻；获得API调用结果的（或者超时的）那一刻。通常，这两个时刻都需要改变应用的状态。例如，发起异步调用时可能显示一个遮罩“正在获取数据”，获得调用结果时则需要显示处理后的结果。要基于Redux来处理这种异步调用，你需要<span style="background-color: #c0c0c0;">派发多个被Reducer同步处理的普通的Action</span>：</p>
<ol>
<li>通知Redux请求已经开始的Action。Reducer通常会在Store中设置isFetching标记，以便UI组件显示一个“正在加载”的提示（Spinner）</li>
<li>通知Redux请求处理成功的Action。Reducer可能把新获得的数据合并到Store中并重置isFetching标记</li>
<li>通知Redux请求处理失败的Action。Reducer通常重置isFetching标记，有可能把失败原因存储到Store中供UI组件使用</li>
</ol>
<p> Action类型和载荷属性命名，通常使用如下风格：</p>
<pre class="crayon-plain-tag">// 风格一：
{ type: 'FETCH_POSTS' }
{ type: 'FETCH_POSTS', status: 'error', error: 'Oops' }
{ type: 'FETCH_POSTS', status: 'success', response: { ... } }
// 风格二：
{ type: 'FETCH_POSTS_REQUEST' }
{ type: 'FETCH_POSTS_FAILURE', error: 'Oops' }
{ type: 'FETCH_POSTS_SUCCESS', response: { ... } }</pre>
<div class="blog_h2"><span class="graybg">示例：新闻订阅</span></div>
<p>本节我们实现一个新闻订阅的例子，牵涉到网络通信和异步Action。</p>
<div class="blog_h3"><span class="graybg">同步Action Creators</span></div>
<p>由用户操作触发的Action：</p>
<pre class="crayon-plain-tag">// 选择新闻栏目
export const SELECT_SUBREDDIT = 'SELECT_SUBREDDIT'

export function selectSubreddit( subreddit ) {
    return {
        type: SELECT_SUBREDDIT,
        subreddit
    }
}

// 点击刷新按钮后触发，更新新闻列表
export const INVALIDATE_SUBREDDIT = 'INVALIDATE_SUBREDDIT'

export function invalidateSubreddit( subreddit ) {
    return {
        type: INVALIDATE_SUBREDDIT,
        subreddit
    }
}</pre>
<p>除了用户操作以外，网络请求也可以触发Action：</p>
<pre class="crayon-plain-tag">// 为指定的栏目请求新数据
export const REQUEST_POSTS = 'REQUEST_POSTS'

export function requestPosts( subreddit ) {
    return {
        type: REQUEST_POSTS,
        subreddit
    }
}</pre>
<p>尽管用户选择一个栏目后应该自动触发更新，但是我们并没有把REQUEST_POSTS与SELECT_SUBREDDIT或者合并INVALIDATE_SUBREDDIT为单个Action。这很重要，因为随着应用需求的变化，请求新数据的操作可能不依赖于用户操作而发生——例如预读取流行栏目、自动定期更新新为列表。</p>
<p>最后，当网络响应到达后触发的Action：</p>
<pre class="crayon-plain-tag">export const RECEIVE_POSTS = 'RECEIVE_POSTS'

export function receivePosts( subreddit, json ) {
    return {
        type: RECEIVE_POSTS,
        subreddit,
        posts: json.data.children.map( child =&gt; child.data ),
        receivedAt: Date.now()
    }
}</pre>
<p>这里我们省略了处理请求错误的Action，真实应用中这通常是需要的。</p>
<div class="blog_h3"><span class="graybg">设计状态树</span></div>
<p>前面已经提到过，在动手编写代码之前规划好应用程序的状态很重要。对于异步代码来说，你需要维护更多的状态。</p>
<p>新闻订阅的状态树结构如下：</p>
<pre class="crayon-plain-tag">{
    selectedSubreddit: 'frontend',
    postsBySubreddit: {
        frontend: {
            isFetching: true,
            didInvalidate: false,
            items: []
        },
        reactjs: {
            isFetching: false,
            didInvalidate: false,
            lastUpdated: 1439478405547,
            items: [
                {
                    id: 42,
                    title: 'Confusion about Flux and Relay'
                },
                {
                    id: 500,
                    title: 'Creating a Simple Application Using React JS and Flux Architecture'
                }
            ]
        }
    }
}</pre>
<p>isFetching指示是否正在请求数据，以便UI显示一个提示；didInvalidate用于提示数据是否过期；lastUpdated可以让用户知道数据更新时间；items存放新闻列表。真实应用中可能还需要fetchedPageCount、nextPageUrl之类的状态，用于分页（pagination）。</p>
<p>我们将每个栏目的信息单独存放。这样当用户切换栏目时，UI可以立即更新而不需要计算或者请求服务器，请求可以仅在需要的时候才进行。</p>
<p>上面的状态树比较简单，不牵涉实体之间的引用（假设新闻与栏目是ManyToOne关系），也就不存在嵌套实体（Nested Entities）的问题。前面的章节我们已经提到过一个<span style="background-color: #c0c0c0;">设计原则——使用范式化的（ normalized）的状态树</span>，避免嵌套实体。嵌套实体带来的数据冗余让状态同步变得很困难，应该从设计的初期就避免。</p>
<p>如果引入新闻作者等额外实体，或者新闻可以归属于多个栏目，特别是应用需要修改作者、新闻等实体，可以修改状态树为：</p>
<pre class="crayon-plain-tag">{
    selectedSubreddit: 'frontend',
    entities: {
        users: {
            2: {
                id: 2,
                name: 'Andrew'
            }
        },
        posts: {
            42: {
                id: 42,
                title: 'Confusion about Flux and Relay',
                author: 2
            },
            100: {
                id: 100,
                title: 'Creating a Simple Application Using React JS and Flux Architecture',
                author: 2
            }
        }
    },
    postsBySubreddit: {
        frontend: {
            isFetching: true,
            didInvalidate: false,
            items: []
        },
        reactjs: {
            isFetching: false,
            didInvalidate: false,
            lastUpdated: 1439478405547,
            items: [
                42,
                100
            ]
        }
    }
}</pre>
<p>这是个典型的范式化的状态树设计，就像数据库那样，总是通过“键”引用其它实体。 为了简化代码以突出本章的主题，新闻订阅的状态树不使用范式化状态树设计。</p>
<div class="blog_h3"><span class="graybg">处理Action</span></div>
<p>在深入如何随着网络请求派发Action的细节之前，我们先编写处理上述Action的Reducers。</p>
<pre class="crayon-plain-tag">import { combineReducers } from 'redux'
import { SELECT_SUBREDDIT, INVALIDATE_SUBREDDIT, REQUEST_POSTS, RECEIVE_POSTS } from '../actions'

// 处理切换栏目的动作
function selectedSubreddit( state = 'reactjs', action ) {
    switch ( action.type ) {
        case SELECT_SUBREDDIT:
            return action.subreddit
        default:
            return state
    }
}

/**
 * 这个函数也是（更次级的）Reducer组合，如何细分Reducer取决于实现的需要
 * 
 * 处理多种Action，一个Reducer可以处理1-N种Action。combineReducers并不能阻止多个Reducer处理同一种Action，尽管比较古怪
 */
function posts( state = { isFetching: false, didInvalidate: false, items: [] }, action ) {
    switch ( action.type ) {
        // 点击刷新按钮，设置数据为过期
        case INVALIDATE_SUBREDDIT:
            return Object.assign( {}, state, {
                didInvalidate: true
            } )
        // 发起网络请求，设置正在请求标记。同时取消数据过期标记
        case REQUEST_POSTS:
            return Object.assign( {}, state, {
                isFetching: true,
                didInvalidate: false
            } )
        // 收到网络响应，取消正在请求标记、数据过期标记。并把新闻条目合并到状态树
        case RECEIVE_POSTS:
            return Object.assign( {}, state, {
                isFetching: false,
                didInvalidate: false,
                items: action.posts,
                lastUpdated: action.receivedAt
            } )
        default:
            return state
    }
}

function postsBySubreddit( state = {}, action ) {
    switch ( action.type ) {
        case INVALIDATE_SUBREDDIT:
        case RECEIVE_POSTS:
        case REQUEST_POSTS:
            return Object.assign( {}, state, {
                // 这种ES6语法让代码简洁
                [action.subreddit]: posts( state[ action.subreddit ], action )
            } )
        default:
            return state
    }
}

const rootReducer = combineReducers( {
    postsBySubreddit,
    selectedSubreddit
} )

export default rootReducer</pre>
<div class="blog_h3"><span class="graybg">异步Action Creators</span></div>
<p>如何让前面创建的同步Action与网络请求一起工作呢？Redux提供的标准解决方案是Thunk这个中间件（Middleware）。关于中间件的细节后面的章节会讨论，这里仅需要知道：使用中间件后，Action Creator可以返回一个函数，而不是Action对象本身。执行下面的命令把Thunk添加为当前工程的依赖：</p>
<pre class="crayon-plain-tag">npm install redux-thunk -save</pre>
<p>如果Action Creator返回一个函数，则该函数会被Redux-Thunk这个中间件执行。 该函数：</p>
<ol>
<li>不需要是纯函数，可以具有边际效应，例如执行异步API</li>
<li>可以调用dispatch派发其它Action</li>
</ol>
<pre class="crayon-plain-tag">/**
 * 第一个Thunk Action Creator
 * 尽管和我们以前编写的Action Creator很不一样，其返回值仍然可以这样派发：
 *     store.dispatch(fetchPosts('reactjs'))
 */
export function fetchPosts(subreddit) {

    // Thunk将dispatch方法作为入参传递给Creator返回的函数
    // 此函数本身因而具有派发Action的能力

    return function (dispatch) {

        // 第一次派发，应用状态改变，提示正在请求数据
        dispatch(requestPosts(subreddit))

        // 此函数可以具有返回值，此返回值将作为store.dispatch(fetchPosts(*))的返回值

        // 执行异步请求，这里，我们返回一个Promise
        // 此返回值不是Thunk限定的，可以根据自己的需要返回某个值、或者不返回值
        return fetch(`http://www.reddit.com/r/${subreddit}.json`)
            .then(response =&gt; response.json()) // 返回一个立即解析的对象
            .then(json =&gt;
                // 第二次派发，根据服务器响应更新State
                // 你可以进行任意次数的派发
                dispatch(receivePosts(subreddit, json))
            )
        // 真实应用中，你可能需要对Promise做异常处理
    }
}</pre>
<p>上面的代码使用了fetch API，这是一个尚未被广泛支持的、用于代替XMLHttpRequest的API。可以通过垫片库获得此API：</p>
<pre class="crayon-plain-tag">import fetch from 'isomorphic-fetch'</pre>
<p>isomorphic-fetch在客户端环境会调用whatwg-fetch，在服务器端环境则会调用node-fetch，因此使用它可以方便universal应用的编写。</p>
<p>isomorphic-fetch假设Promise垫片已经存在，某些浏览器尚不能支持Promise，如果你使用Babel，最简单的、启用Promise支持的方法是在入口点代码开头处添加：</p>
<pre class="crayon-plain-tag">import 'babel-polyfill'</pre>
<p>注意Thunk也支持为函数式Action提供第二个参数——Store.getState方法。这样，你可以根据状态，进行不同的派发操作：</p>
<pre class="crayon-plain-tag">function shouldFetchPosts( state, subreddit ) {
    const posts = state.postsBySubreddit[ subreddit ]
    if ( !posts ) {
        return true
    }
    else if ( posts.isFetching ) {
        return false
    }
    else {
        return posts.didInvalidate
    }
}

export function fetchPostsIfNeeded( subreddit ) {
    // 在缓存数据可用的情况下避免网络访问
    return ( dispatch, getState ) =&gt; {
        if ( shouldFetchPosts( getState(), subreddit ) ) {
            // 在thunk中派发另外一个thunk
            return dispatch( fetchPosts( subreddit ) )
        }
        else {
            // 仍然返回一个Promise，统一处理接口
            return Promise.resolve()
        }
    }
}

// 测试代码：
store.dispatch( fetchPostsIfNeeded( 'reactjs' ) ).then( () =&gt;
    console.log( store.getState() )
)</pre>
<div class="blog_h3"><span class="graybg">启用中间件</span></div>
<p>调用applyMiddleware()可以为Store添加中间件：</p>
<pre class="crayon-plain-tag">import thunkMiddleware from 'redux-thunk'
import createLogger from 'redux-logger'
import { createStore, applyMiddleware } from 'redux'
import { selectSubreddit, fetchPosts } from './actions'
import rootReducer from './reducers'

const loggerMiddleware = createLogger()

const store = createStore(
    rootReducer,
    // 中间件链条
    applyMiddleware(
        thunkMiddleware, // 支持dispath()一个函数而非对象
        loggerMiddleware // 用于记录Action
    )
)

// 测试派发
store.dispatch( selectSubreddit( 'reactjs' ) )
store.dispatch( fetchPosts( 'reactjs' ) ).then( () =&gt;
    console.log( store.getState() )
)</pre>
<div class="blog_h2"><span class="graybg">服务器渲染</span></div>
<p>异步Action Creator在服务器端渲染时特别便利。你可以创建一个Store，然后派发单个异步Action，此异步Action可以派发其它的异步Actions以获取所有需要的数据。然后，仅仅在Promise返回、完成后（此时Store状态已经更新完毕）才执行渲染。</p>
<div class="blog_h2"><span class="graybg">其它中间件</span></div>
<p>Thunk并不是唯一支持异步Action的中间件。其它备选方案包括：</p>
<ol>
<li>redux-promise、 redux-promise-middleware：支持派发Promise而非Function</li>
<li>redux-observable支持派发可监听对象（Observables）</li>
<li>redux-saga，用于构建复杂的异步Action</li>
</ol>
<div class="blog_h2"><span class="graybg">连接UI</span></div>
<p>使用同步、还是异步Action，对如何连接Redux到UI没有影响。</p>
<div class="blog_h1"><span class="graybg">异步数据流</span></div>
<p>不使用中间件的情况下，Redux仅仅支持简单的同步数据流（synchronous data flow）。当调用applyMiddleware应用一个中间件链条后，情况发生改变。</p>
<p>Redux-Thunk、Redux-Promise之类的异步中间件，可以<span style="background-color: #c0c0c0;">包装dispatch方法</span>，以允许你派发其它东西——函数、Promise等。不同中间件能够理解你dispatch的不同东西，并将其转换为另外一种形式。例如Promise中间件能够理解你派发的Promise，并将其转换为两个异步的Begin/End Actions传递给下一个中间件。</p>
<p>链条中的最后一个中间件，必须派发简单JS对象，因为Redux本身仅仅理解这种对象，Reducers仅能对这种对象进行处理，以更新应用状态。</p>
<div class="blog_h1"><span class="graybg">中间件</span></div>
<p>在异步Action一章，我们已经使用过一个中间件，它可以增强dispatch方法的功能，使其支持特定规格的函数的派发。</p>
<p>中间件有点类似AOP编程领域的切面。如果你使用过Express、Koa等服务器端库，你可能已经对中间件的概念有所了解。这两个库中，中间件是插入到接收请求框架、生成响应框架之间的代码。这些代码可以完成：添加CORS头、日志记录、压缩等各种工作。中间件最优秀的特性是它们可以形成一个链条，这样在一个工程中你就可以使用多个第三方开发的中间件。</p>
<p>Redux中间件要解决的问题与Express、Koa不同，但是在设计理念和工作方式上是类似的。Redux中间件机制提供一个<span style="background-color: #c0c0c0;">位于派发Action和Reducer处理Action之间的第三方扩展点</span>。你可以使用中间件机制实现错误报告、日志记录、调用异步API、路由等多种功能。</p>
<p>使用中间件，可以在增强Redux的功能的同时，保持接口不变。</p>
<div class="blog_h2"><span class="graybg">问题：记录日志</span></div>
<p>我们以老生常谈的日志记录需求为切入点，来讲解中间件是如何引入到Redux中的。</p>
<p>Redux的一个优势是状态变更的可预测性和透明性。每当一个同步的Action被派发，相应的新状态就会被计算和保存。如果能记录应用中所有派发的Action连同其引发的状态改变，错误诊断的效率会提高不少。我们如何实现日志记录呢？</p>
<div class="blog_h3"><span class="graybg">手工记录</span></div>
<p>最简单的方法就是在每次调用store.dispatch时，手工记录Action和结果状态：</p>
<pre class="crayon-plain-tag">console.log('dispatching', action)
store.dispatch(action)
console.log('next state', store.getState())</pre>
<p>这种方法的确有效，但是你需要写很多无价值的代码。</p>
<div class="blog_h3"><span class="graybg">封装dispatch</span></div>
<p>为了避免每次派发都要写一遍日志记录语句，我们可以对dispatch方法进行简单的封装：</p>
<pre class="crayon-plain-tag">function dispatchAndLog(store, action) {
    console.log('dispatching', action)
    store.dispatch(action)
    console.log('next state', store.getState())
}</pre>
<p>封装了代码量减少了不少，但是每次都需要导入dispatchAndLog函数也比较麻烦</p>
<div class="blog_h3"><span class="graybg">Monkeypatching </span></div>
<p>Monkey patch这个术语表示在程序运行时对支持系统/软件/框架进行扩展或者修改，并且仅仅影响到正在运行中的程序的补丁。</p>
<p>由于Redux中的Store仅仅是具有几个方法的简单JS对象，我们很容易对其进行Monkey patch：</p>
<pre class="crayon-plain-tag">let next = store.dispatch
store.dispatch = function dispatchAndLog(action) {
    console.log('dispatching', action)
    let result = next(action)
    console.log('next state', store.getState())
    return result
}</pre>
<p>到这一步为止，我们已经实现应用透明的日志记录功能了。</p>
<div class="blog_h2"><span class="graybg">问题：错误报告</span></div>
<p>如果除了日志记录，我们需要增加“横切”功能，该怎么办？</p>
<p>例如，应用出现问题时的错误报告也是一种横切功能，我们希望它能够自动的进行而非到处try-catch。监听window.onerror虽然可以进行全局的错误捕获，但是该事件在某些老浏览器上不能提供调用栈信息，不利于问题诊断。如果任何时候，因派发Action而导致错误抛出时，能够收集当前Action、当前状态、调用栈信息，则问题诊断、场景重新将变得很容易。</p>
<p>我们可以像实现日志记录时那样，继续进行Monkeypatching。但是，分离日志记录、错误报告的代码很重要，它们是完全不相关的模块，糅合在一起明显违反SRP原则。</p>
<div class="blog_h3"><span class="graybg">模块化Monkeypatching</span></div>
<p>为了职责分离，我们简单的把给store打补丁的代码分离到各自的模块中：</p>
<pre class="crayon-plain-tag">// 模块一
function patchStoreToAddLogging(store) {
    store.dispatch = function dispatchAndLog(action) {/**/}

}

// 模块二
function patchStoreToAddCrashReporting(store) {
    let next = store.dispatch
    store.dispatch = function dispatchAndReportErrors(action) {
        try {
            return next(action)
        } catch (err) {/**/}
    }
}

// 使用
patchStoreToAddLogging(store)
patchStoreToAddCrashReporting(store)</pre>
<div class="blog_h3"><span class="graybg">隐藏Monkeypatching</span></div>
<p>Monkeypatching属于一种Hack手段，替换你想替换的方法——store.dispatch。如果把Monkeypatching隐藏到Redux框架内部，上面两个模块至少可以不用牵涉到如何Hack的细节：</p>
<pre class="crayon-plain-tag">// 模块一
function logger(store) {
    return function dispatchAndLog(action) {/**/}
}

// Redux框架内代码
function applyMiddlewareByMonkeypatching(store, middlewares) {
    middlewares = middlewares.slice()
    middlewares.reverse()

    middlewares.forEach(middleware =&gt;
        store.dispatch = middleware(store)
    )
}
// 使用
applyMiddlewareByMonkeypatching(store, [ logger, crashReporter ])</pre>
<p>在应用中间件时，<span style="background-color: #c0c0c0;">后面的中间件总是调用被前一个中间件装饰过的dispatch方法</span>，这是chaining的关键。</p>
<div class="blog_h3"><span class="graybg">移除Monkeypatching</span></div>
<p>chaining可以通过读写store.dispatch字段来实现，也可以使用另外一种方式——传参：</p>
<pre class="crayon-plain-tag">function logger( store ) {
    // 可能已经被装饰过的store.dispatch方法，通过入参传递
    return function wrapDispatchToAddLogging( next ) {
        return function dispatchAndLog( action ) {
            let result = next( action )
            return result
        }
    }
}</pre>
<p>这种嵌套的函数可能让你眼晕，可以该写为等价的ES6箭头函数：</p>
<pre class="crayon-plain-tag">const logger = store =&gt; next =&gt; action =&gt; {
    console.log( 'dispatching', action )
    let result = next( action )
    console.log( 'next state', store.getState() )
    return result
}
const crashReporter = store =&gt; next =&gt; action =&gt; {
    try {
        return next( action )
    }
    catch ( err ) {
        console.error( 'Caught an exception!', err )
        Raven.captureException( err, {
            extra: { action, state: store.getState() }
        } )
        throw err
    }
}</pre>
<p>这种通过参数来使用装饰后的dispatch方法的chaining，与Redux中间件的实际实现很类似了：</p>
<ol>
<li>中间件以next()这一派发函数作为入参，返回一个新的派发函数</li>
<li>新生成的派发函数，作为后一个中间件的入参传入</li>
<li>为了方便访问store.getState等方法，因此store示例作为最外层函数的入参传入</li>
</ol>
<div class="blog_h3"><span class="graybg">实现applyMiddleware</span></div>
<p>下面的函数是对Redux中间件机制的applyMiddleware函数的简化实现：</p>
<pre class="crayon-plain-tag">function applyMiddleware( store, middlewares ) {
    middlewares = middlewares.slice()
    middlewares.reverse()

    let dispatch = store.dispatch
    middlewares.forEach( middleware =&gt;
        // 以传参方式进行Chaining
        dispatch = middleware( store )( dispatch )
    )

    return Object.assign( {}, store, { dispatch } )
}</pre>
<p>注意，Redux实现的applyMiddleware虽然与上面的代码类似，但是它具有以下改进：</p>
<ol>
<li>仅仅Store的部分API暴露给中间件：dispatch、getState</li>
<li>使用了一些编程技巧，确保你的中间件代码执行一个新的Action派发时，调用的时原始的store.dispatch(action)而非装饰过的next(action)。这意味着<span style="background-color: #c0c0c0;">新派发的Action会重新遍历中间件链条，包括当前中间件</span>。该逻辑对异步中间件非常重要</li>
<li>为了防止同一中间件被多次apply，applyMiddleware不直接操控store，而是将其返回值作为参数传递给createStore处理</li>
</ol>
<div class="blog_h2"><span class="graybg">中间件实例</span></div>
<pre class="crayon-plain-tag">// thunk，添加派发函数的能力
const thunk = store =&gt; next =&gt; action =&gt;
    typeof action === 'function' ?  action( store.dispatch, store.getState ) : next( action )


// 支持派发带有promise属性的Action，派发两次：立即派发，promise resolve后，再次派发
const readyStatePromise = store =&gt; next =&gt; action =&gt; {
    if ( !action.promise ) {
        return next( action )
    }

    function makeAction( ready, data ) {
        let newAction = Object.assign( {}, action, { ready }, data )
        delete newAction.promise
        return newAction
    }
    // 立即派发，UI可能因此显示一个spin
    next( makeAction( false ) )
    // resolve后，再次派发，处理异步响应
    return action.promise.then(
        result =&gt; next( makeAction( true, { result } ) ),
        error =&gt; next( makeAction( true, { error } ) )
    )
} </pre>
<div class="blog_h1"><span class="graybg">最佳实践</span></div>
<div class="blog_h2"><span class="graybg">使用对象展开操作符</span></div>
<p>Redux的信条是永远不要修改状态，而是创建一个对象替换它。因此，本文的代码大量使用了<pre class="crayon-plain-tag">Object.assign</pre> 调用。该调用语法非常啰嗦，推荐使用ES6的对象展开操作符（<pre class="crayon-plain-tag">...</pre> ）代替之：</p>
<pre class="crayon-plain-tag">// Object.assign调用
return Object.assign( {}, state, { visibilityFilter: action.filter } )
// 等价的展开操作符语法
return { ...state, visibilityFilter: action.filter }</pre>
<p>对象展开操作符仍然属于ECMAScript的Stage 2提议，为了得到运行环境的支持，你可以使用Babel之类的编译器。首先安装babel-plugin-transform-object-rest-spread插件，然后修改Babel配置文件：</p>
<pre class="crayon-plain-tag">{
    "presets": ["es2015"],
    "plugins": ["transform-object-rest-spread"]
}</pre>
<div class="blog_h2"><span class="graybg">隔离子应用</span></div>
<p>考虑一个大型的应用，例如那种传统的管理信息系统：几十个菜单项的左侧菜单，右侧窗口显示一个“当前”模块。子模块之间完全相互独立，不共享任何状态或者Action。如果用React实现这种应用，可以基于多个隔离的Redux子应用：</p>
<pre class="crayon-plain-tag">import React, { Component } from 'react'
import SubApp from './subapp'

class BigApp extends Component {
    render() {
        return (
            &lt;div&gt;
                &lt;SubApp /&gt;
                &lt;SubApp /&gt;
                &lt;SubApp /&gt;
            &lt;/div&gt;
        )
    }
}</pre>
<p>SubApp对应上述大型应用的“模块”，它们之间相互独立，因而也<span style="background-color: #c0c0c0;">不会共享同一个Redux Store实例</span>。</p>
<p>除了大型MIS系统，某些仪表盘、产品门户之类的应用，也可以考虑用隔离子应用的方式开发。从团队角度来说，如果依据产品或者特性划分开发小组，小组甚至可以使用不同的技术栈，也可以考虑隔离子应用的方式。</p>
<p>对于普通的面向大众的Web应用，最好不要使用隔离子应用，而是利用Redux Reducer组合。</p>
<div class="blog_h3"><span class="graybg">隐藏子应用细节</span></div>
<p>如果仅仅某些子应用是基于Redux实现的，而且希望将Redux作为它的实现细节来隐藏，可以创建React组件，并把Store的创建、连接封装在其中：</p>
<pre class="crayon-plain-tag">import React, { Component } from 'react'
import { Provider } from 'react-redux'
import reducer from './reducers'
import App from './App'

class SubApp extends Component {
    constructor( props ) {
        super( props )
        // 在组件内部创建Store
        this.store = createStore( reducer )
    }

    render() {
        return (
            // Provider让组件子树关联一个Redux Store
            &lt;Provider store={this.store}&gt;
                &lt;App /&gt;
            &lt;/Provider&gt;
        )
    }
} </pre>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/redux-study-note">Redux学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/redux-study-note/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
