15 个开源的顶级人工智能工具

<ruby> 人工智能 <rp>  （ </rp> <rt>  artificial intelligence，AI </rt> <rp>  ） </rp></ruby>是科技研究中最热门的方向之一。像 IBM、谷歌、微软、Facebook 和亚马逊等公司都在研发上投入大量的资金、或者收购那些在机器学习、神经网络、自然语言和图像处理等领域取得了进展的初创公司。考虑到人们对此感兴趣的程度，我们将不会惊讶于斯坦福的专家在[人工智能报告](https://ai100.stanford.edu/sites/default/files/ai_100_report_0906fnlc_single.pdf)中得出的结论：“越来越强大的人工智能应用，可能会对我们的社会和经济产生深远的积极影响，这将出现在从现在到 2030 年的时间段里。”

在最近的一篇[文章](http://www.datamation.com/applications/artificial-intelligence-software-45-ai-projects-to-watch-1.html)中，我们概述了 45 个十分有趣或有前途的人工智能项目。在本文中，我们将聚焦于开源的人工智能工具，详细的了解下最著名的 15 个开源人工智能项目。

![](/data/attachment/album/201610/02/232929w6n5lf9fo8od2ze2.jpg)

*开源人工智能*

以下这些开源人工智能应用都处于人工智能研究的最前沿。

### 1. Caffe

![](/data/attachment/album/201610/02/232931rxx6z6lxdzw0ewd8.jpg)

它是由[贾扬清](http://daggerfs.com/)在加州大学伯克利分校的读博时创造的，[Caffe](http://caffe.berkeleyvision.org/) 是一个基于表达体系结构和可扩展代码的深度学习框架。使它声名鹊起的是它的速度，这让它受到研究人员和企业用户的欢迎。根据其网站所言，它可以在一天之内只用一个 NVIDIA K40 GPU 处理 6000 万多个图像。它是由伯克利视野和学习中心（BVLC）管理的，并且由 NVIDIA 和亚马逊等公司资助来支持它的发展。

### 2. CNTK

![](/data/attachment/album/201610/02/232934fefle555651e4u1j.jpg)

它是<ruby> 计算网络工具包 <rp>  （ </rp> <rt>  Computational Network Toolkit </rt> <rp>  ） </rp></ruby>的缩写，[CNTK](https://www.cntk.ai/) 是一个微软的开源人工智能工具。不论是在单个 CPU、单个 GPU、多个 GPU 或是拥有多个 GPU 的多台机器上它都有优异的表现。微软主要用它做语音识别的研究，但是它在机器翻译、图像识别、图像字幕、文本处理、语言理解和语言建模方面都有着良好的应用。

### 3. Deeplearning4j

![](/data/attachment/album/201610/02/232937tiuiq50l6pcpcenu.jpg)

[Deeplearning4j](http://deeplearning4j.org/) 是一个 java 虚拟机（JVM）的开源深度学习库。它运行在分布式环境并且集成在 Hadoop 和 Apache Spark 中。这使它可以配置深度神经网络，并且它与 Java、Scala 和 其他 JVM 语言兼容。

这个项目是由一个叫做 Skymind 的商业公司管理的，它为这个项目提供支持、培训和一个企业的发行版。

### 4. DMTK

![](/data/attachment/album/201610/02/232943wyfbtt08f0py1jf4.jpg)

[DMTK](http://www.dmtk.io/) 是<ruby> 分布式机器学习工具 <rp>  （ </rp> <rt>  Distributed Machine Learning Toolkit </rt> <rp>  ） </rp></ruby>的缩写，和 CNTK 一样，是微软的开源人工智能工具。作为设计用于大数据的应用程序，它的目标是更快的训练人工智能系统。它包括三个主要组件：DMTK 框架、LightLDA 主题模型算法和分布式（多义）字嵌入算法。为了证明它的速度，微软声称在一个八集群的机器上，它能够“用 100 万个主题和 1000 万个单词的词汇表（总共 10 万亿参数）训练一个主题模型，在一个文档中收集 1000 亿个符号，”。这一成绩是别的工具无法比拟的。

### 5. H20

![](/data/attachment/album/201610/02/232945wj97q71141wv16e5.jpg)

相比起科研，[H2O](http://www.h2o.ai/) 更注重将 AI 服务于企业用户，因此 H2O 有着大量的公司客户，比如第一资本金融公司、思科、Nielsen Catalina、PayPal 和泛美都是它的用户。它声称任何人都可以利用机器学习和预测分析的力量来解决业务难题。它可以用于预测建模、风险和欺诈分析、保险分析、广告技术、医疗保健和客户情报。

它有两种开源版本：标准版 H2O 和 Sparking Water 版，它被集成在 Apache Spark 中。也有付费的企业用户支持。

### 6. Mahout

![](/data/attachment/album/201610/02/232948kft14qxvnyr6tkf6.jpg)

它是 Apache 基金会项目，[Mahout](http://mahout.apache.org/) 是一个开源机器学习框架。根据它的网站所言，它有着三个主要的特性：一个构建可扩展算法的编程环境、像 Spark 和 H2O 一样的预制算法工具和一个叫 Samsara 的矢量数学实验环境。使用 Mahout 的公司有 Adobe、埃森哲咨询公司、Foursquare、英特尔、领英、Twitter、雅虎和其他许多公司。其网站列了出第三方的专业支持。

### 7. MLlib

![](/data/attachment/album/201610/02/232950oyfphwhhq6py8dpe.jpg)

由于其速度，Apache Spark 成为一个最流行的大数据处理工具。[MLlib](https://spark.apache.org/mllib/) 是 Spark 的可扩展机器学习库。它集成了 Hadoop 并可以与 NumPy 和 R 进行交互操作。它包括了许多机器学习算法如分类、回归、决策树、推荐、集群、主题建模、功能转换、模型评价、ML 管道架构、ML 持久、生存分析、频繁项集和序列模式挖掘、分布式线性代数和统计。

### 8. NuPIC

![](/data/attachment/album/201610/02/232952gptjzh7tn1v9iyjh.jpg)

由 [Numenta](http://numenta.com/) 公司管理的 [NuPIC](http://numenta.org/) 是一个基于<ruby> 分层暂时记忆 <rp>  （ </rp> <rt>  Hierarchical Temporal Memory，HTM </rt> <rp>  ） </rp></ruby>理论的开源人工智能项目。从本质上讲，HTM 试图创建一个计算机系统来模仿人类大脑皮层。他们的目标是创造一个 “在许多认知任务上接近或者超越人类认知能力” 的机器。

除了开源许可，Numenta 还提供 NuPic 的商业许可协议，并且它还提供技术专利的许可证。

### 9. OpenNN

![](/data/attachment/album/201610/02/232954adpafa0d4rrcrca1.jpg)

作为一个为开发者和科研人员设计的具有高级理解力的人工智能，[OpenNN](http://www.opennn.net/) 是一个实现神经网络算法的 c++ 编程库。它的关键特性包括深度的架构和快速的性能。其网站上可以查到丰富的文档，包括一个解释了神经网络的基本知识的入门教程。OpenNN 的付费支持由一家从事预测分析的西班牙公司 [Artelnics](https://www.artelnics.com/) 提供。

### 10. OpenCyc

![](/data/attachment/album/201610/02/232959bmmszflxs1ilfhqo.jpg)

由 Cycorp 公司开发的 [OpenCyc](http://www.cyc.com/platform/opencyc/) 提供了对 Cyc 知识库的访问和常识推理引擎。它拥有超过 239,000 个条目，大约 2,093,000 个三元组和大约 69,000 owl：这是一种类似于链接到外部语义库的命名空间。它在富领域模型、语义数据集成、文本理解、特殊领域的专家系统和游戏 AI 中有着良好的应用。该公司还提供另外两个版本的 Cyc：一个可免费的用于科研但是不开源，和一个提供给企业的但是需要付费。

### 11. Oryx 2

![](/data/attachment/album/201610/02/233002cg2uf5uzuo33uogu.jpg)

构建在 Apache Spark 和 Kafka 之上的 [Oryx 2](http://oryx.io/) 是一个专门针对大规模机器学习的应用程序开发框架。它采用一个独特的三层 λ 架构。开发者可以使用 Orys 2 创建新的应用程序，另外它还拥有一些预先构建的应用程序可以用于常见的大数据任务比如协同过滤、分类、回归和聚类。大数据工具供应商 Cloudera 创造了最初的 Oryx 1 项目并且一直积极参与持续发展。

### 12. PredictionIO

![](/data/attachment/album/201610/02/233004ohc6ecc6cjgw6cj6.jpg)

今年的二月，Salesforce 收购了 [PredictionIO](https://prediction.io/)，接着在七月，它将该平台和商标贡献给 Apache 基金会，Apache 基金会将其列为孵育计划。所以当 Salesforce 利用 PredictionIO 技术来提升它的机器学习能力时，成效将会同步出现在开源版本中。它可以帮助用户创建带有机器学习功能的预测引擎，这可用于部署能够实时动态查询的 Web 服务。

### 13. SystemML

![](/data/attachment/album/201610/02/233007l5ew61rtr4cgkrt6.jpg)

最初由 IBM 开发， [SystemML](http://systemml.apache.org/) 现在是一个 Apache 大数据项目。它提供了一个高度可伸缩的平台，可以实现高等数学运算，并且它的算法用 R 或一种类似 python 的语法写成。企业已经在使用它来跟踪汽车维修客户服务、规划机场交通和连接社会媒体数据与银行客户。它可以在 Spark 或 Hadoop 上运行。

### 14. TensorFlow

![](/data/attachment/album/201610/02/233010x4auie4wcxyuya5c.jpg)

[TensorFlow](https://www.tensorflow.org/) 是一个谷歌的开源人工智能工具。它提供了一个使用数据流图进行数值计算的库。它可以运行在多种不同的有着单或多 CPU 和 GPU 的系统，甚至可以在移动设备上运行。它拥有深厚的灵活性、真正的可移植性、自动微分功能，并且支持 Python 和 c++。它的网站拥有十分详细的教程列表来帮助开发者和研究人员沉浸于使用或扩展他的功能。

### 15. Torch

![](/data/attachment/album/201610/02/233012dbvar6ar7oc7lag6.jpg)

[Torch](http://torch.ch/) 将自己描述为：“一个优先使用 GPU 的拥有机器学习算法广泛支持的科学计算框架”，它的特点是灵活性和速度。此外，它可以很容易的通过软件包用于机器学习、计算机视觉、信号处理、并行处理、图像、视频、音频和网络等方面。它依赖一个叫做 LuaJIT 的脚本语言，而 LuaJIT 是基于 Lua 的。

---

via: <http://www.datamation.com/open-source/slideshows/15-top-open-source-artificial-intelligence-tools.html>

作者：[Cynthia Harvey](http://www.datamation.com/author/Cynthia-Harvey-6460.html) 译者：[Chao-zhi](https://github.com/Chao-zhi) 校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


一个漂亮的 Linux 桌面 REST 客户端：Insomnia 3.0

Summary: 正在为 Linux 桌面端找一个免费的 REST 客户端？ 别睡不着觉了！试试 Insomnia。

![](/data/attachment/album/201610/03/230115csifmat28rmzdr09.png)

正在为 Linux 桌面端找一个免费的 REST 客户端？ 别睡不着觉了！试试 [Insomnia](http://insomnia.rest/)。

这个应用是跨平台的，可以工作在 Linux、macOS、Windows。开发者 Gregory Schier 告诉我们他创造这个应用是为了“帮助开发者处理和 [REST API](https://en.wikipedia.org/wiki/Representational_state_transfer) 的通信”

他还说，Insomnia 已经有大约10000 个活跃用户，9% 使用着 Linux.

“目前来说，Linux用户的反馈是非常积极的，因为类似的应用（反正不怎么样）通常不支持 Linux。”

Insomnia 的目标是“加速你的 API 测试工作流”，通过一个简洁的接口让你组织、运行、调试 HTTP 请求。

这款应用还包含一些其他的高级功能比如 Cookie 管理、全局环境、SSL 验证和代码段生成。

由于我不是一个开发者，没有办法第一时间的评价这款应用，也没办法告诉你的它的特性或指出任何比较重大的不足之处。

但是，我将这款应用告诉你，让你自己决定它，如果你正在寻找一个有着顺滑的用户界面的替代命令行工具，比如HTTPie，它可能是值得一试的。

### 下载 Linux 版 Insomnia 3.0

Insomnia 3.0 现在可以用在 Windows、macOS、Linux 上（不要和只能在 Chrome 上使用的 Insomnia v2.0 混淆）。

* [下载 Insomnia 3.0](https://insomnia.rest/download/)

对于 Ubuntu 14.04 LTS 或更高版本，有一个安装包，它是一个跨发行版的安装包：

* [下载 Insomnia 3.0 (.AppImage)](https://builds.insomnia.rest/downloads/linux/latest)

如果你想跟进这个应用的步伐，你可以在 [Twitter](https://twitter.com/GetInsomnia) 上关注它。

---

via: <http://www.omgubuntu.co.uk/2016/09/insomnia-3-is-free-rest-client-for-linux>

作者：[JOEY-ELIJAH SNEDDON](https://plus.google.com/117485690627814051450/?rel=author) 译者：[Bestony](https://github.com/Bestony) 校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


如何运用 Python 建立你的第一个 Slack 聊天机器人？

Summary: 聊天机器人（Bot） 是一种像 Slack 一样的实用的互动聊天服务方式。如果你之前从来没有建立过聊天机器人，那么这篇文章提供了一个简单的入门指南，告诉你如何用 Python 结合 Slack API 建立你第一个聊天机器人。

不太看得懂，按照这个步骤走了，但是发消息后，确没有收到回复，不知道问题出在哪里

[聊天机器人（Bot）](https://www.fullstackpython.com/bots.html) 是一种像 [Slack](https://slack.com/) 一样的实用的互动聊天服务方式。如果你之前从来没有建立过聊天机器人，那么这篇文章提供了一个简单的入门指南，告诉你如何用 Python 结合 [Slack API](https://api.slack.com/) 建立你第一个聊天机器人。

我们通过搭建你的开发环境, 获得一个 Slack API 的聊天机器人令牌，并用 Pyhon 开发一个简单聊天机器人。

![](/data/attachment/album/201610/04/075901vg9gcch17v3ghvg4.png)

### 我们所需的工具

我们的聊天机器人我们将它称作为“StarterBot”，它需要 Python 和 Slack API。要运行我们的 Python 代码，我们需要：

* [Python 2 或者 Python 3](https://www.fullstackpython.com/python-2-or-3.html)
* [pip](https://pip.pypa.io/en/stable/) 和 [virtualenv](https://virtualenv.pypa.io/en/stable/) 来处理 Python [应用程序依赖关系](https://www.fullstackpython.com/application-dependencies.html)
* 一个可以访问 API 的[免费 Slack 账号](https://slack.com/)，或者你可以注册一个 [Slack Developer Hangout team](http://dev4slack.xoxco.com/)。
* 通过 Slack 团队建立的官方 Python [Slack 客户端](https://github.com/slackhq/python-slackclient)代码库
* [Slack API 测试令牌](https://api.slack.com/tokens)

当你在本教程中进行构建时，[Slack API 文档](https://api.slack.com/) 是很有用的。

本教程中所有的代码都放在 [slack-starterbot](https://github.com/mattmakai/slack-starterbot) 公共库里，并以 MIT 许可证开源。

### 搭建我们的环境

我们现在已经知道我们的项目需要什么样的工具，因此让我们来搭建我们所的开发环境吧。首先到终端上（或者 Windows 上的命令提示符）并且切换到你想要存储这个项目的目录。在那个目录里，创建一个新的 virtualenv 以便和其他的 Python 项目相隔离我们的应用程序依赖关系。

```
virtualenv starterbot

```

激活 virtualenv：

```
source starterbot/bin/activate
```

你的提示符现在应该看起来如截图：

![已经激活的 starterbot 的 virtualenv的命令提示符](/data/attachment/album/201610/04/075915hjmmhg3mgn407ajj.png)

这个官方的 slack 客户端 API 帮助库是由 Slack 建立的，它可以通过 Slack 通道发送和接收消息。通过这个 `pip` 命令安装 slackclient 库：

```
pip install slackclient

```

当 `pip` 命令完成时，你应该看到类似这样的输出，并返回提示符。

![在已经激活的 virtualenv 用 pip 安装 slackclient 的输出](/data/attachment/album/201610/04/075916edc4570t15ry811o.png)

我们也需要为我们的 Slack 项目获得一个访问令牌，以便我们的聊天机器人可以用它来连接到 Slack API。

### Slack 实时消息传递（RTM）API

Slack 允许程序通过一个 [Web API](https://www.fullstackpython.com/application-programming-interfaces.html) 来访问他们的消息传递通道。去这个 [Slack Web API 页面](https://api.slack.com/) 注册建立你自己的 Slack 项目。你也可以登录一个你拥有管理权限的已有账号。

![使用 Web API页面的右上角登录按钮](/data/attachment/album/201610/04/075919ubkbh0ddscggvh8g.png)

登录后你会到达 [聊天机器人用户页面](https://api.slack.com/bot-users)。

![定制聊天机器人用户页面](/data/attachment/album/201610/04/075920oosiasao55i6l258.png)

给你的聊天机器人起名为“starterbot”然后点击 “Add bot integration” 按钮。

![添加一个bot integration 并起名为“starterbot”](/data/attachment/album/201610/04/075921uiicc9skl4yzng0i.jpg)

这个页面将重新加载，你将看到一个新生成的访问令牌。你还可以将标志改成你自己设计的。例如我给的这个“Full Stack Python”标志。

![为你的新 Slack 聊天机器人复制和粘贴访问令牌](/data/attachment/album/201610/04/075924ei6rqzoo0li1jz0k.png)

在页面底部点击“Save Integration”按钮。你的聊天机器人现在已经准备好连接 Slack API。

Python 开发人员的一个常见的做法是以环境变量输出秘密令牌。输出的 Slack 令牌名字为`SLACK_BOT_TOKEN`：

```
export SLACK_BOT_TOKEN='你的 slack 令牌粘帖在这里'

```

好了，我们现在得到了将这个 Slack API 用作聊天机器人的授权。

我们建立聊天机器人还需要更多信息：我们的聊天机器人的 ID。接下来我们将会写一个简短的脚本，从 Slack API 获得该 ID。

### 获得我们聊天机器人的 ID

这是最后写一些 Python 代码的时候了! 我们编写一个简短的 Python 脚本获得 StarterBot 的 ID 来热身一下。这个 ID 基于 Slack 项目而不同。

我们需要该 ID，当解析从 Slack RTM 上发给 StarterBot 的消息时，它用于对我们的应用验明正身。我们的脚本也会测试我们 `SLACK_BOT_TOKEN` 环境变量是否设置正确。

建立一个命名为 print*bot*id.py 的新文件，并且填入下面的代码：

```
import os
from slackclient import SlackClient

BOT_NAME = 'starterbot'

slack_client = SlackClient(os.environ.get('SLACK_BOT_TOKEN'))

if __name__ == "__main__":
    api_call = slack_client.api_call("users.list")
    if api_call.get('ok'):
        # retrieve all users so we can find our bot
        users = api_call.get('members')
        for user in users:
            if 'name' in user and user.get('name') == BOT_NAME:
                print("Bot ID for '" + user['name'] + "' is " + user.get('id'))
    else:
        print("could not find bot user with the name " + BOT_NAME)

```

我们的代码导入 SlackClient，并用我们设置的环境变量 `SLACK_BOT_TOKEN` 实例化它。 当该脚本通过 python 命令执行时，我们通过会访问 Slack API 列出所有的 Slack 用户并且获得匹配一个名字为“satrterbot”的 ID。

这个获得聊天机器人的 ID 的脚本我们仅需要运行一次。

```
python print_bot_id.py

```

当它运行为我们提供了聊天机器人的 ID 时，脚本会打印出简单的一行输出。

![在你的 Slack 项目中用 Python 脚本打印 Slack 聊天机器人的 ID](/data/attachment/album/201610/04/075926oyi5jz3whikq23gd.png)

复制这个脚本打印出的唯一 ID。并将该 ID 作为一个环境变量 `BOT_ID` 输出。

```
(starterbot)$ export BOT_ID='bot id returned by script'
```

这个脚本仅仅需要运行一次来获得聊天机器人的 ID。 我们现在可以在我们的运行 StarterBot 的 Python应用程序中使用这个 ID 。

### 编码我们的 StarterBot

现在我们拥有了写我们的 StarterBot 代码所需的一切。 创建一个新文件命名为 starterbot.py ，它包括以下代码。

```
import os
import time
from slackclient import SlackClient
```

对 `os` 和 `SlackClient` 的导入我们看起来很熟悉，因为我们已经在 theprint*bot*id.py 中用过它们了。

通过我们导入的依赖包，我们可以使用它们获得环境变量值，并实例化 Slack 客户端。

```
# starterbot 的 ID 作为一个环境变量
BOT_ID = os.environ.get("BOT_ID")

# 常量
AT_BOT = "<@" + BOT_ID + ">:"
EXAMPLE_COMMAND = "do"

# 实例化 Slack 和 Twilio 客户端
slack_client = SlackClient(os.environ.get('SLACK_BOT_TOKEN'))
```

该代码通过我们以输出的环境变量 `SLACK_BOT_TOKEN 实例化`SlackClient` 客户端。

```
if __name__ == "__main__":
    READ_WEBSOCKET_DELAY = 1 # 1 从 firehose 读取延迟 1 秒
    if slack_client.rtm_connect():
        print("StarterBot connected and running!")
        while True:
            command, channel = parse_slack_output(slack_client.rtm_read())
            if command and channel:
                handle_command(command, channel)
            time.sleep(READ_WEBSOCKET_DELAY)
    else:
        print("Connection failed. Invalid Slack token or bot ID?")

```

Slack 客户端会连接到 Slack RTM API WebSocket，然后当解析来自 firehose 的消息时会不断循环。如果有任何发给 StarterBot 的消息，那么一个被称作 `handle_command` 的函数会决定做什么。

接下来添加两个函数来解析 Slack 的输出并处理命令。

```
def handle_command(command, channel):
    """
        Receives commands directed at the bot and determines if they
        are valid commands. If so, then acts on the commands. If not,
        returns back what it needs for clarification.
    """
    response = "Not sure what you mean. Use the *" + EXAMPLE_COMMAND + \
               "* command with numbers, delimited by spaces."
    if command.startswith(EXAMPLE_COMMAND):
        response = "Sure...write some more code then I can do that!"
    slack_client.api_call("chat.postMessage", channel=channel,
                          text=response, as_user=True)

def parse_slack_output(slack_rtm_output):
    """
        The Slack Real Time Messaging API is an events firehose.
        this parsing function returns None unless a message is
        directed at the Bot, based on its ID.
    """
    output_list = slack_rtm_output
    if output_list and len(output_list) > 0:
        for output in output_list:
            if output and 'text' in output and AT_BOT in output['text']:
                # 返回 @ 之后的文本，删除空格
                return output['text'].split(AT_BOT)[1].strip().lower(), \
                       output['channel']
    return None, None

```

`parse_slack_output` 函数从 Slack 接受信息，并且如果它们是发给我们的 StarterBot 时会作出判断。消息以一个给我们的聊天机器人 ID 的直接命令开始，然后交由我们的代码处理。目前只是通过 Slack 管道发布一个消息回去告诉用户去多写一些 Python 代码!

这是整个程序组合在一起的样子 (你也可以 [在 GitHub 中查看该文件](https://github.com/mattmakai/slack-starterbot/blob/master/starterbot.py)）：

```
import os
import time
from slackclient import SlackClient

# starterbot 的 ID 作为一个环境变量
BOT_ID = os.environ.get("BOT_ID")

# 常量
AT_BOT = "<@" + BOT_ID + ">:"
EXAMPLE_COMMAND = "do"

# 实例化 Slack 和 Twilio 客户端
slack_client = SlackClient(os.environ.get('SLACK_BOT_TOKEN'))

def handle_command(command, channel):
    """
        Receives commands directed at the bot and determines if they
        are valid commands. If so, then acts on the commands. If not,
        returns back what it needs for clarification.
    """
    response = "Not sure what you mean. Use the *" + EXAMPLE_COMMAND + \
               "* command with numbers, delimited by spaces."
    if command.startswith(EXAMPLE_COMMAND):
        response = "Sure...write some more code then I can do that!"
    slack_client.api_call("chat.postMessage", channel=channel,
                          text=response, as_user=True)

def parse_slack_output(slack_rtm_output):
    """
        The Slack Real Time Messaging API is an events firehose.
        this parsing function returns None unless a message is
        directed at the Bot, based on its ID.
    """
    output_list = slack_rtm_output
    if output_list and len(output_list) > 0:
        for output in output_list:
            if output and 'text' in output and AT_BOT in output['text']:
                # 返回 @ 之后的文本，删除空格
                return output['text'].split(AT_BOT)[1].strip().lower(), \
                       output['channel']
    return None, None

if __name__ == "__main__":
    READ_WEBSOCKET_DELAY = 1 # 1 second delay between reading from firehose
    if slack_client.rtm_connect():
        print("StarterBot connected and running!")
        while True:
            command, channel = parse_slack_output(slack_client.rtm_read())
            if command and channel:
                handle_command(command, channel)
            time.sleep(READ_WEBSOCKET_DELAY)
    else:
        print("Connection failed. Invalid Slack token or bot ID?")

```

现在我们的代码已经有了，我们可以通过 `python starterbot.py` 来运行我们 StarterBot 的代码了。

![当 StarterBot 开始运行而且连接到 API 的输出通道](/data/attachment/album/201610/04/075927mppqceff2jjetfpk.png)

在 Slack 中创建新通道，并且把 StarterBot 邀请进来，或者把 StarterBot 邀请进一个已经存在的通道中。

![在 Slack 界面创建一个新通道并且邀请 StarterBot](/data/attachment/album/201610/04/075928dja2nacjwszccwc2.png)

现在在你的通道中给 StarterBot 发命令。

![在你的 Slack 通道里给你的 StarterBot 发命令](/data/attachment/album/201610/04/075929wv5ioayzbbvydibx.png)

如果你从聊天机器人得到的响应中遇见问题，你可能需要做一个修改。正如上面所写的这个教程，其中一行 `AT_BOT = "<@" + BOT_ID + ">:"`，在“@starter”（你给你自己的聊天机器人起的名字）后需要一个冒号。从 `AT_BOT` 字符串后面移除`:`。Slack 似乎需要在`@` 一个人名后加一个冒号，但这好像是有些不协调的。

### 结束

好吧，你现在已经获得一个简易的聊天机器人，你可以在代码中很多地方加入你想要创建的任何特性。

我们能够使用 Slack RTM API 和 Python 完成很多功能。看看通过这些文章你还可以学习到什么：

* 附加一个持久的[关系数据库](https://www.fullstackpython.com/databases.html) 或者 [NoSQL 后端](https://www.fullstackpython.com/no-sql-datastore.html) 比如 [PostgreSQL](https://www.fullstackpython.com/postgresql.html)、[MySQL](https://www.fullstackpython.com/mysql.html) 或者 [SQLite](https://www.fullstackpython.com/sqlite.html) ，来保存和检索用户数据
* 添加另外一个与聊天机器人互动的通道，比如 [短信](https://www.twilio.com/blog/2016/05/build-sms-slack-bot-python.html) 或者[电话呼叫](https://www.twilio.com/blog/2016/05/add-phone-calling-slack-python.html)
* [集成其它的 web API](https://www.fullstackpython.com/api-integration.html)，比如 [GitHub](https://developer.github.com/v3/)、[Twilio](https://www.twilio.com/docs) 或者 [api.ai](https://docs.api.ai/)

有问题? 通过 Twitter 联系我 [@fullstackpython](https://twitter.com/fullstackpython) 或 [@mattmakai](https://twitter.com/mattmakai)。 我在 GitHub 上的用户名是 [mattmakai](https://github.com/mattmakai)。

这篇文章感兴趣? Fork 这个 [GitHub 上的页面](https://github.com/mattmakai/fullstackpython.com/blob/gh-pages/source/content/posts/160604-build-first-slack-bot-python.markdown)吧。

---

via: <https://www.fullstackpython.com/blog/build-first-slack-bot-python.html>

作者：[Matt Makai](https://www.fullstackpython.com/about-author.html) 译者：[jiajia9llinuxer](https://github.com/jiajia9linuxer) 校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出aa


使用 Elasticsearch 和 cAdvisor 监控 Docker 容器

Summary: 如果你正在运行 Swarm 模式的集群，或者只运行单台 Docker，你都会有下面的疑问：我如何才能监控到它们都在干些什么？

您好，现在我们已经做到开始收集容器及应用日志，因为日志量过大现在还没有一个方案，您那边这两个日志收集是怎么做的

如果你正在运行 Swarm 模式的集群，或者只运行单台 Docker，你都会有下面的疑问：

>
> 我如何才能监控到它们都在干些什么？
>
>
>

这个问题的答案是“很不容易”。

![](/data/attachment/album/201610/05/211233tabbmh8bjt5jjpw5.png)

你需要监控下面的参数：

1. 容器的数量和状态。
2. 一台容器是否已经移到另一个节点了，如果是，那是在什么时候，移动到哪个节点？
3. 给定节点上运行着的容器数量。
4. 一段时间内的通信峰值。
5. 孤儿卷和网络（LCTT 译注：孤儿卷就是当你删除容器时忘记删除它的卷，这个卷就不会再被使用，但会一直占用资源）。
6. 可用磁盘空间、可用 inode 数。
7. 容器数量与连接在 `docker0` 和 `docker_gwbridge` 上的虚拟网卡数量不一致（LCTT 译注：当 docker 启动时，它会在宿主机器上创建一个名为 docker0 的虚拟网络接口）。
8. 开启和关闭 Swarm 节点。
9. 收集并集中处理日志。

本文的目标是介绍 [Elasticsearch](https://github.com/elastic/elasticsearch) + [Kibana](https://github.com/elastic/kibana) + [cAdvisor](https://github.com/google/cadvisor) 的用法，使用它们来收集 Docker 容器的参数，分析数据并产生可视化报表。

阅读本文后你可以发现有一个监控仪表盘能够部分解决上述列出的问题。但如果只是使用 cAdvisor，有些参数就无法显示出来，比如 Swarm 模式的节点。

如果你有一些 cAdvisor 或其他工具无法解决的特殊需求，我建议你开发自己的数据收集器和数据处理器（比如 [Beats](https://github.com/elastic/beats)），请注意我不会演示如何使用 Elasticsearch 来集中收集 Docker 容器的日志。

>
> [“你要如何才能监控到 Swarm 模式集群里面发生了什么事情？要做到这点很不容易。” —— @fntlnz](https://twitter.com/share?text=%22How+do+you+keep+track+of+all+that%27s+happening+in+a+Swarm+Mode+cluster%3F+Not+easily.%22+via+%40fntlnz&url=https://blog.codeship.com/monitoring-docker-containers-with-elasticsearch-and-cadvisor/)
>
>
>

### 我们为什么要监控容器？

想象一下这个经典场景：你在管理一台或多台虚拟机，你把 tmux 工具用得很溜，用各种 session 事先设定好了所有基础的东西，包括监控。然后生产环境出问题了，你使用 `top`、`htop`、`iotop`、`jnettop` 各种 top 来排查，然后你准备好修复故障。

现在重新想象一下你有 3 个节点，包含 50 台容器，你需要在一个地方查看整洁的历史数据，这样你知道问题出在哪个地方，而不是把你的生命浪费在那些字符界面来赌你可以找到问题点。

### 什么是 Elastic Stack ？

Elastic Stack 就一个工具集，包括以下工具：

* Elasticsearch
* Kibana
* Logstash
* Beats

我们会使用其中一部分工具，比如使用 Elasticsearch 来分析基于 JSON 格式的文本，以及使用 Kibana 来可视化数据并产生报表。

另一个重要的工具是 [Beats](https://github.com/elastic/beats)，但在本文中我们还是把精力放在容器上，官方的 Beats 工具不支持 Docker，所以我们选择原生兼容 Elasticsearch 的 cAdvisor。

[cAdvisor](https://github.com/google/cadvisor) 工具负责收集、整合正在运行的容器数据，并导出报表。在本文中，这些报表被到入到 Elasticsearch 中。

cAdvisor 有两个比较酷的特性：

* 它不只局限于 Docker 容器。
* 它有自己的 Web 服务器，可以简单地显示当前节点的可视化报表。

### 设置测试集群，或搭建自己的基础架构

和我[以前的文章](https://blog.codeship.com/nginx-reverse-proxy-docker-swarm-clusters/)一样，我习惯提供一个简单的脚本，让读者不用花很多时间就能部署好和我一样的测试环境。你可以使用以下（非生产环境使用的）脚本来搭建一个 Swarm 模式的集群，其中一个容器运行着 Elasticsearch。

>
> 如果你有充足的时间和经验，你可以<ruby> 搭建自己的基础架构 <rp>  （ </rp> <rt>  Bring Your Own Infrastructure，BYOI </rt> <rp>  ） </rp></ruby>。
>
>
>

如果要继续阅读本文，你需要：

* 运行 Docker 进程的一个或多个节点（docker 版本号大于等于 1.12）。
* 至少有一个独立运行的 Elasticsearch 节点（版本号 2.4.X）。

重申一下，此 Elasticsearch 集群环境不能放在生产环境中使用。生产环境也不推荐使用单节点集群，所以如果你计划安装一个生产环境，请参考 [Elastic 指南](https://www.elastic.co/guide/en/elasticsearch/guide/2.x/deploy.html)。

### 对喜欢尝鲜的用户的友情提示

我就是一个喜欢尝鲜的人（当然我也已经在生产环境中使用了最新的 alpha 版本），但是在本文中，我不会使用最新的 Elasticsearch 5.0.0 alpha 版本，我还不是很清楚这个版本的功能，所以我不想成为那个引导你们出错的关键。

所以本文中涉及的 Elasticsearch 版本为最新稳定版 2.4.0。

### 测试集群部署脚本

前面已经说过，我提供这个脚本给你们，让你们不必费神去部署 Swarm 集群和 Elasticsearch，当然你也可以跳过这一步，用你自己的 Swarm 模式引擎和你自己的 Elasticserch 节点。

执行这段脚本之前，你需要：

* [Docker Machine](https://docs.docker.com/machine/install-machine/) – 最终版：在 DigitalOcean 中提供 Docker 引擎。
* [DigitalOcean API Token](https://cloud.digitalocean.com/settings/api/tokens/new): 让 docker 机器按照你的意思来启动节点。

![](/data/attachment/album/201610/05/211255f31k8ir3t81p4w3n.png)

### 创建集群的脚本

现在万事俱备，你可以把下面的代码拷到 create-cluster.sh 文件中：

```
#!/usr/bin/env bash
#
# Create a Swarm Mode cluster with a single master and a configurable number of workers

workers=${WORKERS:-"worker1 worker2"}

#######################################
# Creates a machine on Digital Ocean
# Globals:
#   DO_ACCESS_TOKEN The token needed to access DigitalOcean's API
# Arguments:
#   $1 the actual name to give to the machine
#######################################
create_machine() {
  docker-machine create \
    -d digitalocean \
    --digitalocean-access-token=$DO_ACCESS_TOKEN \
    --digitalocean-size 2gb \
    $1
}

#######################################
# Executes a command on the specified machine
# Arguments:
#   $1     The machine on which to run the command
#   $2..$n The command to execute on that machine
#######################################
machine_do() {
  docker-machine ssh $@
}

main() {

  if [ -z "$DO_ACCESS_TOKEN" ]; then
    echo "Please export a DigitalOcean Access token: https://cloud.digitalocean.com/settings/api/tokens/new"
    echo "export DO_ACCESS_TOKEN=<yourtokenhere>"
    exit 1
  fi

  if [ -z "$WORKERS" ]; then
    echo "You haven't provided your workers by setting the \$WORKERS environment variable, using the default ones: $workers"
  fi

  # Create the first and only master
  echo "Creating the master"

  create_machine master1

  master_ip=$(docker-machine ip master1)

  # Initialize the swarm mode on it
  echo "Initializing the swarm mode"
  machine_do master1 docker swarm init --advertise-addr $master_ip

  # Obtain the token to allow workers to join
  worker_tkn=$(machine_do master1 docker swarm join-token -q worker)
  echo "Worker token: ${worker_tkn}"

  # Create and join the workers
  for worker in $workers; do
    echo "Creating worker ${worker}"
    create_machine $worker
    machine_do $worker docker swarm join --token $worker_tkn $master_ip:2377
  done
}

main $@

```

赋予它可执行权限：

```
chmod +x create-cluster.sh

```

### 创建集群

如文件名所示，我们可以用它来创建集群。默认情况下这个脚本会创建一个 master 和两个 worker，如果你想修改 worker 个数，可以设置环境变量 WORKERS。

现在就来创建集群吧。

```
./create-cluster.sh

```

你可以出去喝杯咖啡，因为这需要花点时间。

最后集群部署好了。

![](/data/attachment/album/201610/05/211258k1j9q19nj6p5u716.png)

现在为了验证 Swarm 模式集群已经正常运行，我们可以通过 ssh 登录进 master：

```
docker-machine ssh master1

```

然后列出集群的节点：

```
docker node ls

```

```
ID                           HOSTNAME  STATUS  AVAILABILITY  MANAGER STATUS
26fi3wiqr8lsidkjy69k031w2 *  master1   Ready   Active        Leader
dyluxpq8sztj7kmwlzs51u4id    worker2   Ready   Active
epglndegvixag0jztarn2lte8    worker1   Ready   Active

```

### 安装 Elasticsearch 和 Kibana

>
> 注意，从现在开始所有的命令都运行在主节点 master1 上。
>
>
>

在生产环境中，你可能会把 Elasticsearch 和 Kibana 安装在一个单独的、[大小合适](https://www.elastic.co/blog/found-sizing-elasticsearch)的实例集合中。但是在我们的实验中，我们还是把它们和 Swarm 模式集群安装在一起。

为了将 Elasticsearch 和 cAdvisor 连通，我们需要创建一个自定义的网络，因为我们使用了集群，并且容器可能会分布在不同的节点上，我们需要使用 [overlay](https://docs.docker.com/engine/userguide/networking/get-started-overlay/) 网络（LCTT 译注：overlay 网络是指在不改变现有网络基础设施的前提下，通过某种约定通信协议，把二层报文封装在 IP 报文之上的新的数据格式，是目前最主流的容器跨节点数据传输和路由方案）。

也许你会问，“为什么还要网络？我们不是可以用 link 吗？” 请考虑一下，自从引入*用户定义网络*后，link 机制就已经过时了。

以下内容摘自 [Docker 文档](https://docs.docker.com/engine/userguide/networking/default_network/dockerlinks/)：

>
> 在 Docker network 特性出来以前，你可以使用 Docker link 特性实现容器互相发现、安全通信。而在 network 特性出来以后，你还可以使用 link，但是当容器处于默认桥接网络或用户自定义网络时，它们的表现是不一样的。
>
>
>

现在创建 overlay 网络，名称为 monitoring：

```
docker network create monitoring -d overlay

```

### Elasticsearch 容器

```
docker service create --network=monitoring \
  --mount type=volume,target=/usr/share/elasticsearch/data \
  --constraint node.hostname==worker1 \
  --name elasticsearch elasticsearch:2.4.0

```

注意 Elasticsearch 容器被限定在 worker1 节点，这是因为它运行时需要依赖 worker1 节点上挂载的卷。

### Kibana 容器

```
docker service create --network=monitoring --name kibana -e ELASTICSEARCH_URL="http://elasticsearch:9200" -p 5601:5601 kibana:4.6.0

```

如你所见，我们启动这两个容器时，都让它们加入 monitoring 网络，这样一来它们可以通过名称（如 Kibana）被相同网络的其他服务访问。

现在，通过 [routing mesh](https://docs.docker.com/engine/swarm/ingress/) 机制，我们可以使用浏览器访问服务器的 IP 地址来查看 Kibana 报表界面。

获取 master1 实例的公共 IP 地址：

```
docker-machine ip master1

```

打开浏览器输入地址：`http://[master1 的 ip 地址]:5601/status`

所有项目都应该是绿色：

![](/data/attachment/album/201610/05/211300g6m67fwcp3fndx5m.png)

让我们接下来开始收集数据！

### 收集容器的运行数据

收集数据之前，我们需要创建一个服务，以全局模式运行 cAdvisor，为每个有效节点设置一个定时任务。

这个服务与 Elasticsearch 处于相同的网络，以便于 cAdvisor 可以推送数据给 Elasticsearch。

```
docker service create --network=monitoring --mode global --name cadvisor \
  --mount type=bind,source=/,target=/rootfs,readonly=true \
  --mount type=bind,source=/var/run,target=/var/run,readonly=false \
  --mount type=bind,source=/sys,target=/sys,readonly=true \
  --mount type=bind,source=/var/lib/docker/,target=/var/lib/docker,readonly=true \
  google/cadvisor:latest \
  -storage_driver=elasticsearch \
  -storage_driver_es_host="http://elasticsearch:9200"

```

>
> 注意：如果你想配置 cAdvisor 选项，参考[这里](https://github.com/google/cadvisor/blob/master/docs/runtime_options.md)。
>
>
>

现在 cAdvisor 在发送数据给 Elasticsearch，我们通过定义一个索引模型来检索 Kibana 中的数据。有两种方式可以做到这一点：通过 Kibana 或者通过 API。在这里我们使用 API 方式实现。

我们需要在一个连接到 monitoring 网络的正在运行的容器中运行索引创建命令，你可以在 cAdvisor 容器中拿到 shell，不幸的是 Swarm 模式在开启服务时会在容器名称后面附加一个唯一的 ID 号，所以你需要手动指定 cAdvisor 容器的名称。

拿到 shell：

```
docker exec -ti <cadvisor-container-name> sh

```

创建索引：

```
curl -XPUT http://elasticsearch:9200/.kibana/index-pattern/cadvisor -d '{"title" : "cadvisor*",  "timeFieldName": "container_stats.timestamp"}'

```

如果你够懒，可以只执行下面这一句：

```
docker exec $(docker ps | grep cadvisor | awk '{print $1}' | head -1) curl -XPUT http://elasticsearch:9200/.kibana/index-pattern/cadvisor -d '{"title" : "cadvisor*",  "timeFieldName": "container_stats.timestamp"}'

```

### 把数据汇总成报表

你现在可以使用 Kibana 来创建一份美观的报表了。但是不要着急，我为你们建了一份报表和一些图形界面来方便你们入门。

![](/data/attachment/album/201610/05/211302zopaa394j1pnoa43.png)

访问 Kibana 界面 => Setting => Objects => Import，然后选择包含以下内容的 JSON 文件，就可以导入我的配置信息了：

```
[
  {
    "_id": "cAdvisor",
    "_type": "dashboard",
    "_source": {
      "title": "cAdvisor",
      "hits": 0,
      "description": "",
      "panelsJSON": "[{\"id\":\"Filesystem-usage\",\"type\":\"visualization\",\"panelIndex\":1,\"size_x\":6,\"size_y\":3,\"col\":1,\"row\":1},{\"id\":\"Memory-[Node-equal->Container]\",\"type\":\"visualization\",\"panelIndex\":2,\"size_x\":6,\"size_y\":4,\"col\":7,\"row\":4},{\"id\":\"memory-usage-by-machine\",\"type\":\"visualization\",\"panelIndex\":3,\"size_x\":6,\"size_y\":6,\"col\":1,\"row\":4},{\"id\":\"CPU-Total-Usage\",\"type\":\"visualization\",\"panelIndex\":4,\"size_x\":6,\"size_y\":5,\"col\":7,\"row\":8},{\"id\":\"Network-RX-TX\",\"type\":\"visualization\",\"panelIndex\":5,\"size_x\":6,\"size_y\":3,\"col\":7,\"row\":1}]",
      "optionsJSON": "{\"darkTheme\":false}",
      "uiStateJSON": "{}",
      "version": 1,
      "timeRestore": false,
      "kibanaSavedObjectMeta": {
        "searchSourceJSON": "{\"filter\":[{\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true}}}]}"
      }
    }
  },
  {
    "_id": "Network",
    "_type": "search",
    "_source": {
      "title": "Network",
      "description": "",
      "hits": 0,
      "columns": [
        "machine_name",
        "container_Name",
        "container_stats.network.name",
        "container_stats.network.interfaces",
        "container_stats.network.rx_bytes",
        "container_stats.network.rx_packets",
        "container_stats.network.rx_dropped",
        "container_stats.network.rx_errors",
        "container_stats.network.tx_packets",
        "container_stats.network.tx_bytes",
        "container_stats.network.tx_dropped",
        "container_stats.network.tx_errors"
      ],
      "sort": [
        "container_stats.timestamp",
        "desc"
      ],
      "version": 1,
      "kibanaSavedObjectMeta": {
        "searchSourceJSON": "{\"index\":\"cadvisor*\",\"query\":{\"query_string\":{\"analyze_wildcard\":true,\"query\":\"*\"}},\"highlight\":{\"pre_tags\":[\"@kibana-highlighted-field@\"],\"post_tags\":[\"@/kibana-highlighted-field@\"],\"fields\":{\"*\":{}},\"fragment_size\":2147483647},\"filter\":[]}"
      }
    }
  },
  {
    "_id": "Filesystem-usage",
    "_type": "visualization",
    "_source": {
      "title": "Filesystem usage",
      "visState": "{\"title\":\"Filesystem usage\",\"type\":\"histogram\",\"params\":{\"addLegend\":true,\"addTimeMarker\":false,\"addTooltip\":true,\"defaultYExtents\":false,\"mode\":\"stacked\",\"scale\":\"linear\",\"setYExtents\":false,\"shareYAxis\":true,\"times\":[],\"yAxis\":{}},\"aggs\":[{\"id\":\"1\",\"type\":\"avg\",\"schema\":\"metric\",\"params\":{\"field\":\"container_stats.filesystem.usage\",\"customLabel\":\"USED\"}},{\"id\":\"2\",\"type\":\"terms\",\"schema\":\"split\",\"params\":{\"field\":\"machine_name\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\",\"row\":false}},{\"id\":\"3\",\"type\":\"avg\",\"schema\":\"metric\",\"params\":{\"field\":\"container_stats.filesystem.capacity\",\"customLabel\":\"AVAIL\"}},{\"id\":\"4\",\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"container_stats.filesystem.device\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\"}}],\"listeners\":{}}",
      "uiStateJSON": "{\"vis\":{\"colors\":{\"Average container_stats.filesystem.available\":\"#E24D42\",\"Average container_stats.filesystem.base_usage\":\"#890F02\",\"Average container_stats.filesystem.capacity\":\"#3F6833\",\"Average container_stats.filesystem.usage\":\"#E24D42\",\"USED\":\"#BF1B00\",\"AVAIL\":\"#508642\"}}}",
      "description": "",
      "version": 1,
      "kibanaSavedObjectMeta": {
        "searchSourceJSON": "{\"index\":\"cadvisor*\",\"query\":{\"query_string\":{\"analyze_wildcard\":true,\"query\":\"*\"}},\"filter\":[]}"
      }
    }
  },
  {
    "_id": "CPU-Total-Usage",
    "_type": "visualization",
    "_source": {
      "title": "CPU Total Usage",
      "visState": "{\"title\":\"CPU Total Usage\",\"type\":\"area\",\"params\":{\"shareYAxis\":true,\"addTooltip\":true,\"addLegend\":true,\"smoothLines\":false,\"scale\":\"linear\",\"interpolate\":\"linear\",\"mode\":\"stacked\",\"times\":[],\"addTimeMarker\":false,\"defaultYExtents\":false,\"setYExtents\":false,\"yAxis\":{}},\"aggs\":[{\"id\":\"1\",\"type\":\"avg\",\"schema\":\"metric\",\"params\":{\"field\":\"container_stats.cpu.usage.total\"}},{\"id\":\"2\",\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"container_stats.timestamp\",\"interval\":\"auto\",\"customInterval\":\"2h\",\"min_doc_count\":1,\"extended_bounds\":{}}},{\"id\":\"3\",\"type\":\"terms\",\"schema\":\"group\",\"params\":{\"field\":\"container_Name\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\"}},{\"id\":\"4\",\"type\":\"terms\",\"schema\":\"split\",\"params\":{\"field\":\"machine_name\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\",\"row\":true}}],\"listeners\":{}}",
      "uiStateJSON": "{}",
      "description": "",
      "version": 1,
      "kibanaSavedObjectMeta": {
        "searchSourceJSON": "{\"index\":\"cadvisor*\",\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true}},\"filter\":[]}"
      }
    }
  },
  {
    "_id": "memory-usage-by-machine",
    "_type": "visualization",
    "_source": {
      "title": "Memory [Node]",
      "visState": "{\"title\":\"Memory [Node]\",\"type\":\"area\",\"params\":{\"shareYAxis\":true,\"addTooltip\":true,\"addLegend\":true,\"smoothLines\":false,\"scale\":\"linear\",\"interpolate\":\"linear\",\"mode\":\"stacked\",\"times\":[],\"addTimeMarker\":false,\"defaultYExtents\":false,\"setYExtents\":false,\"yAxis\":{}},\"aggs\":[{\"id\":\"1\",\"type\":\"avg\",\"schema\":\"metric\",\"params\":{\"field\":\"container_stats.memory.usage\"}},{\"id\":\"2\",\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"container_stats.timestamp\",\"interval\":\"auto\",\"customInterval\":\"2h\",\"min_doc_count\":1,\"extended_bounds\":{}}},{\"id\":\"3\",\"type\":\"terms\",\"schema\":\"group\",\"params\":{\"field\":\"machine_name\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\"}}],\"listeners\":{}}",
      "uiStateJSON": "{}",
      "description": "",
      "version": 1,
      "kibanaSavedObjectMeta": {
        "searchSourceJSON": "{\"index\":\"cadvisor*\",\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true}},\"filter\":[]}"
      }
    }
  },
  {
    "_id": "Network-RX-TX",
    "_type": "visualization",
    "_source": {
      "title": "Network RX TX",
      "visState": "{\"title\":\"Network RX TX\",\"type\":\"histogram\",\"params\":{\"addLegend\":true,\"addTimeMarker\":true,\"addTooltip\":true,\"defaultYExtents\":false,\"mode\":\"stacked\",\"scale\":\"linear\",\"setYExtents\":false,\"shareYAxis\":true,\"times\":[],\"yAxis\":{}},\"aggs\":[{\"id\":\"1\",\"type\":\"avg\",\"schema\":\"metric\",\"params\":{\"field\":\"container_stats.network.rx_bytes\",\"customLabel\":\"RX\"}},{\"id\":\"2\",\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"container_stats.timestamp\",\"interval\":\"s\",\"customInterval\":\"2h\",\"min_doc_count\":1,\"extended_bounds\":{}}},{\"id\":\"3\",\"type\":\"avg\",\"schema\":\"metric\",\"params\":{\"field\":\"container_stats.network.tx_bytes\",\"customLabel\":\"TX\"}}],\"listeners\":{}}",
      "uiStateJSON": "{\"vis\":{\"colors\":{\"RX\":\"#EAB839\",\"TX\":\"#BF1B00\"}}}",
      "description": "",
      "savedSearchId": "Network",
      "version": 1,
      "kibanaSavedObjectMeta": {
        "searchSourceJSON": "{\"filter\":[]}"
      }
    }
  },
  {
    "_id": "Memory-[Node-equal->Container]",
    "_type": "visualization",
    "_source": {
      "title": "Memory [Node=>Container]",
      "visState": "{\"title\":\"Memory [Node=>Container]\",\"type\":\"area\",\"params\":{\"shareYAxis\":true,\"addTooltip\":true,\"addLegend\":true,\"smoothLines\":false,\"scale\":\"linear\",\"interpolate\":\"linear\",\"mode\":\"stacked\",\"times\":[],\"addTimeMarker\":false,\"defaultYExtents\":false,\"setYExtents\":false,\"yAxis\":{}},\"aggs\":[{\"id\":\"1\",\"type\":\"avg\",\"schema\":\"metric\",\"params\":{\"field\":\"container_stats.memory.usage\"}},{\"id\":\"2\",\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"container_stats.timestamp\",\"interval\":\"auto\",\"customInterval\":\"2h\",\"min_doc_count\":1,\"extended_bounds\":{}}},{\"id\":\"3\",\"type\":\"terms\",\"schema\":\"group\",\"params\":{\"field\":\"container_Name\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\"}},{\"id\":\"4\",\"type\":\"terms\",\"schema\":\"split\",\"params\":{\"field\":\"machine_name\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\",\"row\":true}}],\"listeners\":{}}",
      "uiStateJSON": "{}",
      "description": "",
      "version": 1,
      "kibanaSavedObjectMeta": {
        "searchSourceJSON": "{\"index\":\"cadvisor*\",\"query\":{\"query_string\":{\"query\":\"* NOT container_Name.raw: \\\\\\\"/\\\\\\\" AND NOT container_Name.raw: \\\\\\\"/docker\\\\\\\"\",\"analyze_wildcard\":true}},\"filter\":[]}"
      }
    }
  }
]

```

这里还有很多东西可以玩，你也许想自定义报表界面，比如添加内存页错误状态，或者收发包的丢包数。如果你能实现开头列表处我没能实现的项目，那也是很好的。

### 总结

正确监控需要大量时间和精力，容器的 CPU、内存、IO、网络和磁盘，监控的这些参数还只是整个监控项目中的沧海一粟而已。

我不知道你做到了哪一阶段，但接下来的任务也许是：

* 收集运行中的容器的日志
* 收集应用的日志
* 监控应用的性能
* 报警
* 监控健康状态

如果你有意见或建议，请留言。祝你玩得开心。

现在你可以关掉这些测试系统了：

```
docker-machine rm master1 worker{1,2}

```

---

via: <https://blog.codeship.com/monitoring-docker-containers-with-elasticsearch-and-cadvisor/>

作者：[Lorenzo Fontana](https://blog.codeship.com/author/lorenzofontana/) 译者：[bazz2](https://github.com/bazz2) 校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


KDE 庆祝 20 岁生日，发布了首个 LTS 版本： KDE 5.8

good

就等KDE5毕业了

archlinux中的plasma 5 的breeze主题还是不太完善，对gtk的支持不够好。

KDE 5.8 还在 Testing 里

默认的 Fedora24 KDE 版本用起来简直就是灾难，忘了版本号是多少了，如果中文化跟稳定性完美，那还能接受。

非常好用

英文显示非常清晰好看，中文就模模糊糊，不知道中文渲染怎么回事

10 月 4 日，KDE 项目发布了其最新的桌面环境 KDE 5.8，它是面向 GNU/Linux 发行版的流行的现代桌面环境。

经过几个月的开发和几个版本的迭代，KDE 5.8 带着若干新功能闪亮登场，取代了之前的 5.7 系列——虽然 5.7 才于 9 月 13 日发布了其第五个维护版本。KDE 5.8 是一个长期支持版本（LTS），它会一直支持到 2018 年。

![](/data/attachment/album/201610/06/102155dnwnvg5kvjna74ks.jpg)

“**今天，KDE 发布了其旗舰产品 Plasma 的首个 LTS 版本**”，[公告](https://www.kde.org/announcements/plasma-5.8.0.php)中称，“这标志着它的开发人员和设计师们认为它已经可以推荐给普罗大众了。如果你以前体验过 KDE，但是由于种种原因弃之而去，那么现在是你该重新考虑回来的时候了。Plasma 是一个初看简单，实则强大的桌面环境！”

*（注，虽然 KDE 3.5 发布于 2005 年 11 月 29 日，而下一个版本 4.0 在 2008 年 1 月 11 日才发布，但是 KDE Plasma 5.8 是官方认可的第一个 LTS 版本）*

### KDE 主要功能

新版本的 KDE 的主要功能包括：支持桌面小工具；新的系统级搜索功能可以让用户在 KDE 桌面里面搜索任何东西，包括应用、音乐、视频、文件、目录等等；此外还有很多新的东西，包括桌面墙纸、小工具、桌面效果、窗口风格，以及丰富的定制能力。

#### 桌面小工具

![](/data/attachment/album/201610/06/104538mdhts6hofstegztd.jpg)

*桌面小工具*

放在你桌面上的有用小工具可以让你及时关注每天的天气，休息的时候可以欣赏漫画，或者简单的用来做做计算。

#### 各种新鲜热辣的装饰素材

![](/data/attachment/album/201610/06/104435hndnwn000ywdkrni.jpg)

*各种新鲜热辣的装饰素材*

用下载的墙纸、窗口风格、小工具、桌面效果和大量的其它装饰素材打扮你的桌面。我们提供了一个 [KDE 商店](http://store.kde.org/)，可供你在海量的素材中进行选择。

#### 桌面搜索

![](/data/attachment/album/201610/06/104359bjb2knjv0zv7rrzt.jpg)

*桌面搜索*

Plasma 可以让你在桌面上搜索应用、文件夹、音乐、视频、文件等等你任何想找的东西。

#### 统一的外观

![](/data/attachment/album/201610/06/104318uzppy3y5t1kyj2hj.jpg)

*统一的外观*

Plasma 的默认 Breeze 主题将各种可视组件的外观打造为一致的风格，这包括用 Qt 4、Qt 5、GTK 2、GTK 3 等等开发的应用，甚至还包括 LibreOffice。

#### 与手机集成

![](/data/attachment/album/201610/06/104951qm24rr6mwx2xrzvz.jpg)

*与手机集成*

使用 KDE Connect 你可以在桌面上接到短信提醒、可以轻松传输文件、在接到电话时自动静默音乐，甚至将你的手机当成一个遥控器。

#### 无尽的定制可能性

![](/data/attachment/album/201610/06/105203bobajamogqy7g36u.jpg)

*无尽的定制可能性*

Plasma 以简洁为本，但是你可以用各种小工具、面板、屏幕和风格来定制它。

### KDE 5.8 中的新变化

KDE 完美打造了从开机到关机的整个过程、支持从右向左文字、简化了全局快捷键、改进了许多小应用、修复了多屏支持的问题、更好的支持了坞站和投影仪、增强了 KWin 窗口管理器、新的默认字体、Kirigami 界面更舒适，以及更好的 Wayland 支持！

#### 从启动到关机的一致体验

![](/data/attachment/album/201610/06/105636l35hugdg2ud7693z.jpg)

*从启动到关机的一致体验*

这个版本带来了全新的登录屏设计，从开机到关闭，给你一个完整的 Breeze 体验。布局经过精细打磨，更适合作为团体或公司办公用的计算机使用。它更加顺滑，也支持更多的定制能力，比如说，所有的 Plasma 插件，比如幻灯片小工具或动画墙纸小工具现在都可以放在锁屏上了。

#### 支持从右向左的语言

![](/data/attachment/album/201610/06/110638l88l831d8l2lc963.jpg)

*支持从右向左的语言*

对希伯来语和阿拉伯语这样从右向左书写的语言的支持进行了极大改进。面板内容、桌面和配置对话框会如镜面般反射过来。Plasma 的边栏，比如小工具栏、窗口切换器、活动管理群都放在了屏幕右侧。

#### 改进的应用

![](/data/attachment/album/201610/06/111335z7kksegv6p6madvo.png)

*媒体播放器的上下文菜单*

重写了虚拟桌面切换器（即 Pager）和窗口列表应用，使用了我们在 5.7 中引入的新任务管理器后端，这可以让它们使用同任务管理器一样的数据，从而降低了内存占用并提升了性能。在多屏幕时，虚拟桌面切换器现在也可以设置为仅显示在当前屏幕上，现在它与活动切换器应用大部分代码都相同。

任务管理器在这个版本中有了更多的用处。以前仅在任务管理器提示菜单中可用的媒体控制器现在也可以在上下文菜单中访问了。此外，在拖放操作中可以将窗口拖到前端，将文件拖入任务管理器中会以其关联的应用打开。分组窗口的弹窗现在可以使用键盘进行导航，并且其标签的文字渲染也进行了改进。

#### 简化的全局快捷键

![](/data/attachment/album/201610/06/112940s0yka8fe0ftff7ia.jpg)

*全局快捷键设置*

全局快捷键的配置现在针对应用启动后最常用的任务进行了简化。跳转功能是在前一个版本中增加的，全局快捷键现在可以配置跳转到一个应用内的特定功能上。

由于我们在 Wayland 方面的突破，现在可以提供称之为“<ruby> 修饰快捷键 <rp>  （ </rp> <rt>  modifier-only shortcuts </rt> <rp>  ） </rp></ruby>”的功能，它可以让你只需要按下 Alt 键（或 Meta 键）就可以打开应用菜单。由于这个功能很受欢迎，所以被移植回 X11 会话中了。

#### 其他改进

![](/data/attachment/album/201610/06/113150q2mssy9smtvvs93v.jpg)

*Plasma Discover 的新界面*

这个版本修复了多屏支持方面许多问题，并在 Qt 5.6.1 的支持下显著提升了坞站和投影仪的体验。

Plasma 的窗口管理器 KWin 现在通过 llvmpipe 进行<ruby> 合成 <rp>  （ </rp> <rt>  compositing </rt> <rp>  ） </rp></ruby>，更易于用在少见的硬件和嵌入式设备上。对于要求关闭合成的应用现在有了标准化而广泛使用的界面，移除了“<ruby> 非重定向全屏 <rp>  （ </rp> <rt>  Unredirect Fullscreen </rt> <rp>  ） </rp></ruby>”选项，它通常会导致一些稳定性问题，因为很多驱动已经不支持它了。

我们的跨平台 UI 组件 [Kirigami](https://dot.kde.org/2016/08/10/kdes-kirigami-ui-framework-gets-its-first-public-release) 现在已经发布，你可以试试看我们用它重新打造的 Plasma Discover。

我们也换了新的默认字体，来自 Google 的 Noto 字体包括了 Unicode 标准中的所有字符，而我们的等宽字体修饰非常适合于程序员和面向终端的用户。

#### Wayland 支持

![](/data/attachment/album/201610/06/114732zpp57kn4ni873nz3.jpg)

*Wayland 之上的 Plasma 现在支持 GTK+*

在过去几个月当中，在 Wayland 上运行 Plasma 经历了漫长的工作。虽然我们的长期支持的范围并不包括快速发展的 Wayland 方面，不过我们认为它可以经受更多的测试了。虽然仍然有一些小问题和功能缺失，但是现在我们希望你可以试试它并给我们反馈。这个版本中部分改进如下：

* 支持 xdg-shell，比如现在支持 GTK+ 应用了
* 对触屏支持做了很多改进
* 支持触屏手势，虽然现在还没有任何手势
* 现在支持滑动弹窗效果
* 在 X11 和 Wayland 应用之间同步剪贴板内容

更多的详细变更内容请参见[变更日志](https://www.kde.org/announcements/plasma-5.7.5-5.8.0-changelog.php)。

### 支持期直到 2018 年 4 月

如上面所提到的，KDE Plasma 5.8 LTS 会支持到 2018 年，并将会发布至少 9 个维护版本。而第一个维护版本 KDE Plasma 5.8.1 在下周的 10 月 11 日就要发布了！按照[开发计划](https://community.kde.org/Schedules/Plasma_5)，开始的维护版本发布会很频繁，简直让人目不暇接：之后再过一周会发布 5.8.2，11 月 1 日发布 5.8.3，11 月 22 日发布 5.8.4。

### 下载与使用

#### 体验镜像

体验 KDE 5.8 最容易的方式是将体验镜像放到可启动的 USB 棒中，你可用在 KDE 社区维基中找到 [Plasma 5 的体验镜像列表](https://community.kde.org/Plasma/LiveImages)。

#### 下载软件包

软件包如下：

* [软件包下载](https://community.kde.org/Plasma/Packages)

#### 源代码

你也可以从源代码直接安装 Plasma 5。KDE 社区维基有如何[编译的指导](http://community.kde.org/Frameworks/Building)。注意，Plasma 5 不能与 Plasma 4 共存，你需要卸载旧版本或将其安装到不同的前缀目录下。

* [源代码](https://www.kde.org/info/plasma-5.8.0.php)

### KDE 20 周年纪念北京庆祝沙龙

2016 年 10 月 15 日，由普华基础软件与“Linux 中国”开源社区共同联合主办的 [KDE 20 周年庆祝沙龙在北京召开](http://s.wcd.im/v/158vrZ3a/)，欢迎各位 KDE 用户和开源爱好者们参加，沙龙不但由社区骨灰级爱好者进行讲演，而且还提供了琳琅满目的独有纪念品。报名人数名额限定，希望参加者需尽快[报名](http://s.wcd.im/v/158vrZ3a/)。

这是 KDE 在国内的第一次大型活动。该活动得到了 KDE 的官方认证，可以通过[官方活动入口](https://community.kde.org/Promo/Events/Parties/KDE_20_Anniversary)或[这里](http://s.wcd.im/v/158vrZ3a/)直接报名。

在此沙龙上，我们还会宣布成立 KDE 中国用户组 China KUG，您将成为 China KUG 的第一批成员。

![](/data/attachment/album/201609/28/211044i570h1np0r1phzmd.jpg)

*KDE 20 周年纪念沙龙*

来吧！和我们一起吃蛋糕、合影，分享 KDE 体会，还有机会获取 iSOFTLinux V4.0 U 盘、KDE 二十周年 T 恤等多种奖品。我们在这里真诚期待每一个 KDE 爱好者及开源软件爱好者的到来！

![](/data/attachment/album/201609/28/211056fqpl4zs4t44zwbbk.jpg)

*KDE 20 周年纪念专属定制限量 T 恤*

#### 沙龙日程

|  |  |
| --- | --- |
| 13:30–14:00 | 签到 |
| 14:00-14:30 | KDE 20 年发展历史 （KDE 成员 Leslie Zhai 讲演） |
| 14:30-14:50 | 国际及国内 KDE KOL 对本次北京庆祝活动的祝贺视频 |
| 14:50–15:20 | KDE 组件打包 |
| 15:20-15:50 | KDE-China 翻译组介绍 |
| 15:50-16:10 | 20 周年庆典（蛋糕） |
| 16:20-16:40 | China KUG 成立仪式 |
| 16:40-17:00 | 游戏及合影 |

#### 沙龙地点

北京海淀区农大南路 1 号硅谷亮城 8 号楼 1 层布谷森林咖啡厅

请务必提前报名，好给您预留座位。


Fedora 24 的用户，千万不要在桌面里运行 `dnf update`

Summary: 如果你正在使用 Fedora 24 操作系统，当你在使用 KDE、GNOME、Xfce 等桌面环境时，千万不要执行 dnf update 命令。截止到现在，该 bug 仍无修复完成的通知。

tty 终端在 Fedora 从F2 开始

囧

尝试了下，没有崩溃阿。。。。

Gentoo用戶前來看戲...

ubuntu用户默默路过……

来自117.136.99.17的 Sina Weibo 6.8|Android 6.0 用户

原文有这么一句：“但是这与用户的硬件配置和安装的软件有关。”

Arch表示不慌

囧，刚输了

我就好奇，然后呢？

我也好奇，结果怎么样？

瞎说，我这几天每天都有更新，怎么没事？你是谁派来的？老实交待

是最近的包有问题吗？一直都是在图形化界面更新的，没有出现过问题哦。

是习惯了吗？哈哈！不过还是习惯用Arch

Thinkpad E450 赛扬芯，KDE版的试过了，确实有kernel updates not working的报错，但是不会崩。。。。已经手贱亲测。。。。。。

这玩意不光fedora吧！centos的gnome也挂过，后来就摸黑又更新到新的内核又好了。

为何我的 update 没事？

部分情况下有，我见到有人反馈遇到了。

摸黑更新内核——笑哭了。

连发三条——难道也是摸黑发的嘛。哈哈哈~

真是嘴贱啊我。。。

昨天再次 update 一下，今早 Gnome 就崩溃了。现在只有 Gnome on Wayland 可以用了，什么鬼。。。

哈哈哈哈，我能说我忍不住笑吗。

Linux Mint用户：看戏

前几天，Fedora 项目组的 Adam Williamson [发布一则服务公告（PSA）](https://lists.fedoraproject.org/archives/list/devel@lists.fedoraproject.org/thread/7ULAG243UNGTOSL6URGNG23GC4B6X5GB/)，提醒 Fedora 社区在内部更新过程中出现了严重问题！

许多 Fedora 24 的用户报告称，从 4 日起，当他们在桌面中运行 `dnf update` 命令，通过官方软件仓库更新软件时会遇到 “duplicated packages” 和 “kernel updates not working” 等错误。

![](/data/attachment/album/201610/06/205757x4a43qqavau64a4p.jpeg)

经过一些调查，Williamson 得出结论在 dnf update 过程中存在一个 bug，会导致图形界面，比如 GNOME 或 KDE，甚至整个 X Window 系统（X11）崩溃，但是这与用户的硬件配置和安装的软件有关。

“当崩溃发生时，更新进程会被杀死以至于不能完整结束，这就是为什么会得到 ‘duplicated packages’ 或其它的奇怪错误的原因”，Adam Williamson 说，“我在努力和报告者配合调查原因，希望能够解决它，但是，此刻，**不要在桌面里面运行 `dnf update` !**”

所以，如果你正在使用 Fedora 24 操作系统，当你在使用 KDE、GNOME、Xfce 等桌面环境时，千万不要执行 `dnf update` 命令。你可以通过按下 Ctrl+Alt+F3 切换到文本模式来执行它，或者使用离线更新系统。

截止到现在，该 bug 仍无修复完成的通知。


一周开源新闻：Linux 4.8 内核发布

Summary:
更多：
- Ubuntu 16.10 可选 Unity 8
- dnf 2.0 里程碑版本发布
- KDE 5.8 发布

### Linux 4.8 内核发布

![](/data/attachment/album/201610/06/212122x2vk9wz8z29t3yp3.jpg)

*Linux 4.8 内核发布*

10 月 2 日，经过了两个月的开发，发布了 8 个 RC 版本后，Linus Torvalds 宣布 Linux 4.8 内核正式发布。Linux 4.8 内核会很快随着各个发行版的更新推送给 Linux 用户们，而即将发布的 Ubuntu 16.10 应该是第一个使用最新的 4.8 内核的主流发行版。

此外随着 4.8 的发布，同时也开启了 4.9 的合并窗口——宣告了 4.9 的开发开始。

不过，两天后，Linus 为新发布的 4.8 内核中包含的 bug 进行了[道歉](http://lkml.iu.edu/hypermail/linux/kernel/1610.0/00878.html)。该 bug 由内核开发者 Andrew Morton 引入到 4.8 发布前的最后一个 RC 版本当中，当编译这部分代码时会导致问题，从而编译出来的是一个死内核。显然， 为了应对这个问题，4.8.1 会很快发布。

### Ubuntu 16.10 终于支持可选 Unity 8 会话了

![](/data/attachment/album/201610/06/221814y82too383gf895d6.jpg)

*可选的 Unity 8 会话*

即将在下周发布的 Ubuntu 16.10 发布了最后一个 Beta 版本，在这个版本中，Canonical 之前就说的要把 Unity 8 会话作为可选会话的承诺终于实现了。不过目前 Unity 8 仍然是一个试验性的会话，所以，仅供体验尝鲜。

此外，如上面说的，在这个最终 Beta 版本中，也使用了 Linux 4.8 内核。

### DNF 达到 2.0 里程碑

Fedora Linux 的 dnf 软件包管理器[发布](http://dnf.baseurl.org/2016/10/03/dnf-2-0-0-and-dnf-plugins-core-1-0-0-released/)了 2.0.0 版本，它出现在了 Fedora 的开发版本 Fedora Rawhide 之中，结束了 dnf 1.1 系列。

你可以看看 [dnf 2.0.0 的发布公告](http://dnf.readthedocs.org/en/latest/release_notes.html#release-notes)，但是由于和 1.x 的部分[不兼容性](http://dnf.readthedocs.io/en/latest/dnf-1_vs_dnf-2.html)，所以看起来不大可能会出现在 Fedora 25 之中。

此外，最近两天 Fedora 24 的 dnf 更新出现了严重错误，在该问题解决前，请勿在桌面环境中执行 `dnf update` 命令。详见：[Fedora 24 的用户，千万不要在桌面里运行 `dnf update`](/article-7836-1.html "Fedora 24 的用户，千万不要在桌面里运行 `dnf update`") 。

### KDE 5.8 LTS 发布

在 KDE 赢来 20 周年之际，KDE 开发团队宣布了 KDE 5.8 LTS 正式发布，这是官方认可的第一个长期支持版本，会一直支持到 2018 年。详见： [KDE 庆祝 20 岁生日，发布了首个 LTS 版本： KDE 5.8](/article-7835-1.html "KDE 庆祝 20 岁生日，发布了首个 LTS 版本： KDE 5.8") 。


揭秘 Twitter 背后的基础设施：效率与优化篇

过去我们曾经发布过一些关于 [Finagle](https://twitter.github.io/finagle/) 、[Manhattan](https://blog.twitter.com/2014/manhattan-our-real-time-multi-tenant-distributed-database-for-twitter-scale) 这些项目的文章，还写过一些针对大型事件活动的[架构优化](https://blog.twitter.com/2013/new-tweets-per-second-record-and-how)的文章，例如天空之城、超级碗、2014 世界杯、全球新年夜庆祝活动等。在这篇基础设施系列文章中，我主要聚焦于 Twitter 的一些关键设施和组件。我也会写一些我们在系统的扩展性、可靠性、效率方面的做过的改进，例如我们基础设施的历史，遇到过的挑战，学到的教训，做过的升级，以及我们现在前进的方向等等。

![](/data/attachment/album/201610/08/122706q4t3mysaeyzsmy8m.jpg)

>
> 天空之城：2013 年 8 月 2 日，宫崎骏的《<ruby> 天空之城 <rp>  （ </rp> <rt>  Castle in the Sky </rt> <rp>  ） </rp></ruby>》在 NTV 迎来其第 14 次电视重播，剧情发展到高潮之时，Twitter 的 TPS（Tweets Per Second）也被推上了新的高度——143,199 TPS，是平均值的 25 倍，这个记录保持至今。-- LCTT 译注
>
>
>

### 数据中心的效率优化

#### 历史

当前 Twitter 硬件和数据中心的规模已经超过大多数公司。但达到这样的规模不是一蹴而就的，系统是随着软硬件的升级优化一步步成熟起来的，过程中我们也曾经犯过很多错误。

有个一时期我们的系统故障不断。软件问题、硬件问题，甚至底层设备问题不断爆发，常常导致系统运营中断。出现故障的地方存在于各个方面，必须综合考虑才能确定其风险和受到影响的服务。随着 Twitter 在客户、服务、媒体上的影响力不断扩大，构建一个高效、可靠的系统来提供服务成为我们的战略诉求。

>
> Twitter系统故障的界面被称为<ruby> 失败鲸 <rp>  （ </rp> <rt>  Fail Whale </rt> <rp>  ） </rp></ruby>，如下图 -- LCTT 译注
>
>
> ![Fail Whale](/data/attachment/album/201610/08/122721betey39309i7revk.png)
>
>
>

#### 挑战

一开始，我们的软件是直接安装在服务器，这意味着软件可靠性依赖硬件，电源、网络以及其他的环境因素都是威胁。这种情况下，如果要增加容错能力，就需要统筹考虑这些互不关联的物理设备因素及在上面运行的服务。

最早采购数据中心方案的时候，我们都还是菜鸟，对于站点选择、运营和设计都非常不专业。我们先直接托管主机，业务增长后我们改用租赁机房。早期遇到的问题主要是因为设备故障、数据中心设计问题、维护问题以及人为操作失误。我们也在持续迭代我们的硬件设计，从而增强硬件和数据中心的容错性。

服务中断的原因有很多，其中硬件故障常发生在服务器、机架交换机、核心交换机这地方。举一个我们曾经犯过的错误，硬件团队最初在设计服务器的时候，认为双路电源对减少供电问题的意义不大 -- 他们真的就移除了一块电源。然而数据中心一般给机架提供两路供电来提高冗余性，防止电网故障传导到服务器，而这需要两块电源。最终我们不得不在机架上增加了一个 ATS 单元（<ruby> 交流切换开关 <rp>  （ </rp> <rt>  AC transfer switch </rt> <rp>  ） </rp></ruby>）来接入第二路供电。

提高系统的可靠性靠的就是这样的改进，给网络、供电甚至机房增加冗余，从而将影响控制到最小范围。

#### 我们学到的教训以及技术的升级、迁移和选型

我们学到的第一个教训就是要先建模，将可能出故障的地方（例如建筑的供电和冷却系统、硬件、光纤网络等）和运行在上面的服务之间的依赖关系弄清楚，这样才能更好地分析，从而优化设计提升容错能力。

我们增加了更多的数据中心提升地理容灾能力，减少自然灾害的影响。而且这种站点隔离也降低了软件的风险，减少了例如软件部署升级和系统故障的风险。这种多活的数据中心架构提供了<ruby> 代码灰度发布 <rp>  （ </rp> <rt>  staged code deployment </rt> <rp>  ） </rp></ruby>的能力，减少代码首次上线时候的影响。

我们设计新硬件使之能够在更高温度下正常运行，数据中心的能源效率因此有所提升。

#### 下一步工作

随着公司的战略发展和运营增长，我们在不影响我们的最终用户的前提下，持续不断改进我们的数据中心。下一步工作主要是在当前能耗和硬件的基础上，通过维护和优化来提升效率。

### 硬件的效率优化

#### 历史和挑战

我们的硬件工程师团队刚成立的时候只能测试市面上现有硬件，而现在我们能自己定制硬件以节省成本并提升效率。

Twitter 是一个很大的公司，它对硬件的要求对任何团队来说都是一个不小的挑战。为了满足整个公司的需求，我们的首要工作是能检测并保证购买的硬件的品质。团队重点关注的是性能和可靠性这两部分。对于硬件我们会做系统性的测试来保证其性能可预测，保证尽量不引入新的问题。

随着我们一些关键组件的负荷越来越大（如 Mesos、Hadoop、Manhattan、MySQL 等），市面上的产品已经无法满足我们的需求。同时供应商提供的一些高级服务器功能，例如 Raid 管理或者电源热切换等，可靠性提升很小，反而会拖累系统性能而且价格高昂，例如一些 Raid 控制器价格高达系统总报价的三分之一，还拖累了 SSD 的性能。

那时，我们也是 MySQL 数据库的一个大型用户。SAS（<ruby> 串行连接 SCSI <rp>  （ </rp> <rt>  Serial Attached SCSI </rt> <rp>  ） </rp></ruby>）设备的供应和性能都有很大的问题。我们大量使用 1U 规格的服务器，它的磁盘和回写缓存一起也只能支撑每秒 2000 次的顺序 IO。为了获得更好的效果，我们只得不断增加 CPU 核心数并加强磁盘能力。我们那时候找不到更节省成本的方案。

后来随着我们对硬件需求越来越大，我们成立了一个硬件团队，从而自己来设计更便宜更高效的硬件。

#### 关键技术变更与选择

我们不断的优化硬件相关的技术，下面是我们采用的新技术和自研平台的时间轴。

* 2012 - 采用 SSD 作为我们 MySQL 和 Key-Value 数据库的主要存储。
* 2013 - 我们开发了第一个定制版 Hadoop 工作站，它现在是我们主要的大容量存储方案。
* 2013 - 我们定制的解决方案应用在 Mesos、TFE（ Twitter Front-End ）以及缓存设备上。
* 2014 - 我们定制的 SSD Key-Value 服务器完成开发。
* 2015 - 我们定制的数据库解决方案完成开发。
* 2016 - 我们开发了一个 GPU 系统来做模糊推理和训练机器学习。

#### 学到的教训

硬件团队的工作本质是通过做取舍来优化 TCO（总体拥有成本），最终达到达到降低 CAPEX（资本支出）和 OPEX（运营支出）的目的。概括来说，服务器降成本就是：

1. 删除无用的功能和组件
2. 提升利用率

Twitter 的设备总体来说有这四大类：存储设备、计算设备、数据库和 GPU 。 Twitter 对每一类都定义了详细的需求，让硬件工程师更针对性地设计产品，从而优化掉那些用不到或者极少用的冗余部分。例如，我们的存储设备就专门为 Hadoop 优化过，设备的购买和运营成本相比于 OEM 产品降低了 20% 。同时，这样做减法还提高了设备的性能和可靠性。同样的，对于计算设备，硬件工程师们也通过移除无用的特性获得了效率提升。

一个服务器可以移除的组件总是有限的，我们很快就把能移除的都扔掉了。于是我们想出了其他办法，例如在存储设备里，我们认为降低成本最好的办法是用一个节点替换多个节点，并通过 Aurora/Mesos 来管理任务负载。这就是我们现在正在做的东西。

对于这个我们自己新设计的服务器，首先要通过一系列的标准测试，然后会再做一系列负载测试，我们的目标是一台新设备至少能替换两台旧设备。最大的性能提升来自增加 CPU 的线程数，我们的测试结果表示新 CPU 的 单线程能力提高了 20~50% 。同时由于整个服务器的线程数增加，我们看到单线程能效提升了 25%。

这个新设备首次部署的时候，监控发现新设备只能替换 1.5 台旧设备，这比我们的目标低了很多。对性能数据检查后发现，我们之前对负载特性的一些假定是有问题的，而这正是我们在做性能测试需要发现的问题。

对此我们硬件团队开发了一个模型，用来预测在不同的硬件配置下当前 Aurora 任务的填充效率。这个模型正确的预测了新旧硬件的性能比例。模型还指出了我们一开始没有考虑到的存储需求，并因此建议我们增加 CPU 核心数。另外，它还预测，如果我们修改内存的配置，那系统的性能还会有较大提高。

硬件配置的改变都需要花时间去操作，所以我们的硬件工程师们就首先找出几个关键痛点。例如我们和 SRE（<ruby> 网站可靠性工程师 <rp>  （ </rp> <rt>  Site Reliability Engineer </rt> <rp>  ） </rp></ruby>）团队一起调整任务顺序来降低存储需求，这种修改很简单也很有效，新设备可以代替 1.85 个旧设备了。

为了更好的优化效率，我们对新硬件的配置做了修改，只是扩大了内存和磁盘容量就将 CPU 利用率提高了20% ，而这只增加了非常小的成本。同时我们的硬件工程师也和合作生产厂商一起为那些服务器的最初出货调整了物料清单。后续的观察发现我们的自己的新设备实际上可以代替 2.4 台旧设备，这个超出了预定的目标。

### 从裸设备迁移到 mesos 集群

直到 2012 年为止，软件团队在 Twitter 开通一个新服务还需要自己操心硬件：配置硬件的规格需求，研究机架尺寸，开发部署脚本以及处理硬件故障。同时，系统中没有所谓的“服务发现”机制，当一个服务需要调用一个另一个服务时候，需要读取一个 YAML 配置文件，这个配置文件中有目标服务对应的主机 IP 和端口信息（预留的端口信息是由一个公共 wiki 页面维护的）。随着硬件的替换和更新，YAML 配置文件里的内容也会不断的编辑更新。在缓存层做修改意味着我们可以按小时或按天做很多次部署，每次添加少量主机并按阶段部署。我们经常遇到在部署过程中 cache 不一致导致的问题，因为有的主机在使用旧的配置有的主机在用新的。有时候一台主机的异常（例如在部署过程中它临时宕机了）会导致整个站点都无法正常工作。

在 2012/2013 年的时候，Twitter 开始尝试两个新事物：服务发现（来自 ZooKeeper 集群和 [Finagle](https://twitter.github.io/finagle/) 核心模块中的一个库）和 [Mesos](http://mesos.apache.org/)（包括基于 Mesos 的一个自研的计划任务框架 Aurora ，它现在也是 Apache 基金会的一个项目）。

服务发现功能意味着不需要再维护一个静态 YAML 主机列表了。服务或者在启动后主动注册，或者自动被 mesos 接入到一个“服务集”（就是一个 ZooKeeper 中的 znode 列表，包含角色、环境和服务名信息）中。任何想要访问这个服务的组件都只需要监控这个路径就可以实时获取到一个正在工作的服务列表。

现在我们通过 Mesos/Aurora ，而不是使用脚本（我们曾经是 [Capistrano](https://github.com/capistrano/capistrano) 的重度用户）来获取一个主机列表、分发代码并规划重启任务。现在软件团队如果想部署一个新服务，只需要将软件包上传到一个叫 Packer 的工具上（它是一个基于 HDFS 的服务），再在 Aurora 配置上描述文件（需要多少 CPU ，多少内存，多少个实例，启动的命令行代码），然后 Aurora 就会自动完成整个部署过程。 Aurora 先找到可用的主机，从 Packer 下载代码，注册到“服务发现”，最后启动这个服务。如果整个过程中遇到失败（硬件故障、网络中断等等）， Mesos/Aurora 会自动重选一个新主机并将服务部署上去。

#### Twitter 的私有 PaaS 云平台

Mesos/Aurora 和服务发现这两个功能给我们带了革命性的变化。虽然在接下来几年里，我们碰到了无数 bug ，伤透了无数脑筋，学到了分布式系统里的无数教训，但是这套架还是非常赞的。以前大家一直忙于处理硬件搭配和管理，而现在，大家只需要考虑如何优化业务以及需要多少系统能力就可以了。同时，我们也从根本上解决了 Twitter 之前经历过的 CPU 利用率低的问题，以前服务直接安装在服务器上，这种方式无法充分利用服务器资源，任务协调能力也很差。现在 Mesos 允许我们把多个服务打包成一个服务包，增加一个新服务只需要修改配额，再改一行配置就可以了。

在两年时间里，多数“无状态”服务迁移到了 Mesos 平台。一些大型且重要的服务（包括我们的用户服务和广告服务系统）是最先迁移上去的。因为它们的体量巨大，所以它们从这些服务里获得的好处也最多，这也降低了它们的服务压力。

我们一直在不断追求效率提升和架构优化的最佳实践。我们会定期去测试公有云的产品，和我们自己产品的 TCO 以及性能做对比。我们也拥抱公有云的服务，事实上我们现在正在使用公有云产品。最后，这个系列的下一篇将会主要聚焦于我们基础设施的体量方面。

特别感谢 [Jennifer Fraser](https://twitter.com/jenniferfraser)、[David Barr](https://twitter.com/davebarr)、[Geoff Papilion](https://twitter.com/gpapilion)、 [Matt Singer](https://twitter.com/mattbytes)、[Lam Dong](https://twitter.com/lamdong) 对这篇文章的贡献。

---

via: <https://blog.twitter.com/2016/the-infrastructure-behind-twitter-efficiency-and-optimization>

作者：[mazdakh](https://twitter.com/intent/user?screen_name=mazdakh) 译者：[eriwoon](https://github.com/eriwoon) 校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


Ryver：你应该使用它替代 Slack

Summary: 貌似每个人都听说过 Slack，它是一款跨平台的，可以使你时刻保持与他人同步的团队沟通应用。它革新了用户讨论和规划项目的方式，显而易见，它升级了 email 的沟通功能。

![](/data/attachment/album/201610/08/143220okd66i822m0zz0ze.jpg)

貌似每个人都听说过 [Slack](https://www.maketecheasier.com/new-slack-features/)，它是一款跨平台的，可以使你时刻保持与他人同步的团队沟通应用。它革新了用户讨论和规划项目的方式，显而易见，它升级了 email 的沟通功能。

我在一个非常小的写作团队工作，不管是通过手机还是电脑，我从未在使用 Slack 过程中遇到过沟通问题。若想与任何规模的团队保持同步，继续使用 Slack 仍然不失为不错的方式。

既然如此，为什么我们还要讨论今天的话题？Ryver 被人们认为是下一个热点，相比 Slack，Ryver 提供了升级版的服务。Ryver 完全免费，它的团队正在奋力争取更大的市场份额。

是否 Ryver 已经强大到可以扮演 Slack 杀手的角色？这两种旗鼓相当的消息应用究竟有何不同？

欲知详情，请阅读下文。

### 为什么用 Ryver ?

![](/data/attachment/album/201610/08/143252tb1zlrjmkm1r000h.jpg)

既然 Slack 能用为什么还要折腾呢？Ryver 的开发者对 Slack 的功能滚瓜烂熟，他们希望 Ryver 改进的服务足以让你移情别恋。他们承诺 Ryver 提供完全免费的团队沟通服务，并且不会在任何一个环节隐形收费。

谢天谢地，他们用高质量产品兑现了自己的承诺。

额外的内容是关键所在，他们承诺去掉一些你在 Slack 免费账号上面遇到的限制。无限的存储空间是一个加分点，除此之外，在许多其他方面 Ryver 也更加开放。如果存储空间限制对你来说是个痛点，不防试试 Ryver。

这是一个简单易用的系统，所有的功能都可以一键搞定。这种设计哲学使 Apple 大获成功。当你开始使用它之后，也不会遭遇成长的烦恼。

![](/data/attachment/album/201610/08/143304qssnkd5rcln56auf.png)

会话分为私聊和公示，这意味着团队平台和私人用途有明确的界限。它应该有助于避免将任何尴尬的广而告之给你的同事，这些问题我在使用 Slack 期间都遇到过。

Ryver 支持与大量现成的 App 的集成，并在大多数平台上有原生应用程序。

在需要时，你可以添加访客而无需增加费用，如果你经常和外部客户打交道，这将是一个非常有用的功能。访客可以增加更多的访客，这种流动性的元素是无法从其他更流行的消息应用中看到的。

考虑到 Ryver 是一个为迎合不同需求而产生的完全不同的服务。如果你需要一个账户来处理几个客户，Ryver 值得一试。

问题是它是如何做到免费的呢？ 简单的答案是高级用户将为你的使用付了费。 就像 Spotify 和其他应用一样，有一小部分人为我们其他人支付了费用。 这里有一个[直接链接](http://www.ryver.com/downloads/)到他们的下载页面的地址，如果有兴趣就去试一试吧。

### 你应该切换到 Ryver 吗？

![](/data/attachment/album/201610/08/143406sy9j8h6hchcd0yrs.jpg)

像我一样在小团队使用 Slack 的体验还是非常棒，但是 Ryver 可以给予的更多。一个完全免费的团队沟通应用的想法不可谓不宏伟，更何况它工作的十分完美。

同时使用这两种消息应用也无可厚非，但是如果你不愿意为一个白金 Slack 账户付费，一定要尝试一下竞争对手的服务。你可能会发现，两者各擅胜场，这取决于你需要什么。

最重要的是，Ryver 是一个极棒的免费替代品，它不仅仅是一个 Slack 克隆。他们清楚地知道他们想要实现什么，他们有一个可以在拥挤不堪的市场提供不同的东西的不错的产品。

但是，如果将来持续缺乏资金，Ryver 有可能消失。 它可能会让你的团队和讨论陷入混乱。 目前一切还好，但是如果你计划把更大的业务委托给这个新贵还是需要三思而行。

如果你厌倦了 Slack 对免费帐户的限制，你会对 Ryver 印象深刻。 要了解更多，请访问其网站以获取有关服务的信息。

---

via: <https://www.maketecheasier.com/why-use-ryver-instead-of-slack/>

作者：[James Milin-Ashmore](https://www.maketecheasier.com/author/james-ashmore/) 译者：[firstadream](https://github.com/firstadream) 校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


12 个最佳的免费学习编程的游戏网站

貌似  很难打开

玩了第一个，E文不过关，不知道要做什么

codecombat  可以用 国人搭建的服务器 。http://www.icodegame.com/

这能提高编程水平吗

只有第二个打开了，前几关免费，后面付费

CodinGame怎么登陆呀

CodinGame怎么登陆呀

学习编程虽然对有些人来说是件乐事，但是对大多数人来说仍然是一件比较枯燥困难的事情。当然，面临这样困惑的人，并不是只有你一个，所以，这世界上就出现了许多寓教于乐的通过游戏的形式来教你编程的网站。

在这篇文章中，我们对 200 多个编程游戏网站的各个方面进行了评估，包括是否免费、是否自由开源、是面对菜鸟还是有经验的程序员、支持的编程语言等等，然后遴选出这 12 个上佳的免费的编程游戏网站，希望能让你或你的朋友（或者你的孩子）真正喜欢上编程。

![](/data/attachment/album/201610/09/003833jz9r579r1l557oml.jpg)

### CodinGame

这是一个需要编程一个 AI 机器人，然后由它来解决各种面临的问题的有趣游戏。支持各种编程语言。

网站： <https://www.codingame.com/>

![](/data/attachment/album/201610/09/003950eknoffej5gqkqnjg.jpg)

### Code Combat

CodeCombat 是一个在真正的游戏中教学生们学习计算机编程的平台。这是一个社区项目，有上百位玩家贡献者们创建了各种关卡。支持 Java、JavaScript、Python、Lua、CoffeeScript 等语言。

网站：<https://codecombat.com/play>

![](/data/attachment/album/201610/09/004023iri3rmcciibjmcxi.jpg)

### Screeps

在游戏中学习 JavaScript 编程。该游戏号称它是世界上第一个面向程序员的大型多人在线（MMO）战略沙盒游戏。

网站： <https://screeps.com/>

![](/data/attachment/album/201610/09/004104zrriyo2n5abbnoyg.jpg)

### Check iO

Check iO 是一个基于浏览器的游戏，你需要使用 Python 或 JavaScript 来解决问题才能将游戏进行下去（需要登录）。

网站：<https://checkio.org/>

![](/data/attachment/album/201610/09/004154z7i3ep4q78d8a858.jpg)

### Vim Adventures

通过游戏来学习 Vim。

网站： <http://vim-adventures.com/>

![](/data/attachment/album/201610/09/004221inpten19r1s1zemr.jpg)

### Cyber DoJo

Cyber-dojo 是一个程序员实战编程的地方。支持 JavaScript、Java、Python、PHP、Ruby 等语言。

网站： <http://www.cyber-dojo.org/>

源代码：<https://github.com/JonJagger/cyber-dojo>

![](/data/attachment/album/201610/09/004250w8d88l8beojz58md.jpg)

### Code Monkey

通过尝试捕获香蕉来学习编程。可以用来教孩子们学习编程（付费的）。

网站： <https://www.playcodemonkey.com/>

![](/data/attachment/album/201610/09/004325ykjjm445655u57c8.jpg)

### Elevator Saga

电梯编程游戏，需要一步步解决问题过关。比如说第一关是在 60 秒内运输 15 个人。

网站： <http://play.elevatorsaga.com/>

源代码：<https://github.com/magwo/elevatorsaga>

![](/data/attachment/album/201610/09/004358ltn6fyo6oz64fngt.jpg)

### Codewars

通过挑战来精湛编程技艺。在真实的编程挑战中提升技能。支持 JavaScript、Python、C#、Java、Python 等语言。

网站： <http://www.codewars.com/>

![](/data/attachment/album/201610/09/004418d6cltjjjmmlo6tis.jpg)

### Ruby Quiz

Ruby Quiz 是一个面向 Ruby 程序员的每周编程挑战，当前已经有 156 个竞赛挑战。

网站： <http://rubyquiz.com/>

![](/data/attachment/album/201610/09/004528jvnzpokl9qtpzzc8.jpg)

### Git Games

git-game 是一个基于终端的游戏，它用来教授 git 中的那些非常酷的功能。游戏的主题内容是搜寻，通过 git 命令来找到线索，解开谜题。

网站： <http://www.git-game.com/>

![](/data/attachment/album/201610/09/004654nhbdz3udg52mgbb0.jpg)

### Hacker Org

hacker.org 挑战赛是一系列的拼图、敲砖块、测试和智力题，用来测试你的黑客能力。要通过这一系列的考验，你需要破解加密、写出精巧的代码，揭开重重迷雾。

网站： <http://www.hacker.org/>

![](/data/attachment/album/201610/09/004746sh0pvnrymbpmlwvv.jpg)

好了，以上就是我们挑选出来的 12 个编程游戏，何不试试？


搭个 Web 服务器（三）

感谢分享！

楼主，想问一下mac是不是无法查看僵尸进程？我在mac下运行下面这段代码：

#include <unistd.h>

int main(void)

{

        int i;

        for (i = 0; i < 10; ++i) {

                if (!fork()) {

                        return 0;

                }

        }

        sleep(100);

        return 0;

}

编译成main,然后ps -ef | grep 'main'之后根本无法查看到<defunct>的进程啊。

看 STATE 字段里面 Z 的进程，这是僵尸、

>
> “只有在创造中才能够学到更多。” ——皮亚杰
>
>
>

在本系列的[第二部分](/article-7685-1.html)中，你创造了一个可以处理基本 HTTP GET 请求的、朴素的 WSGI 服务器。当时我问了一个问题：“你该如何让你的服务器在同一时间处理多个请求呢？”在这篇文章中，你会找到答案。系好安全带，我们要认真起来，全速前进了！你将会体验到一段非常快速的旅程。准备好你的 Linux、Mac OS X（或者其他 \*nix 系统），还有你的 Python。本文中所有源代码均可在 [GitHub](https://github.com/rspivak/lsbaws/blob/master/part3/) 上找到。

![](/data/attachment/album/201610/09/144117s2djox1sesdiviid.png)

### 服务器的基本结构及如何处理请求

首先，我们来回顾一下 Web 服务器的基本结构，以及服务器处理来自客户端的请求时，所需的必要步骤。你在[第一部分](/article-7662-1.html)及[第二部分](/article-7685-1.html)中创建的轮询服务器只能够一次处理一个请求。在处理完当前请求之前，它不能够接受新的客户端连接。所有请求为了等待服务都需要排队，在服务繁忙时，这个队伍可能会排的很长，一些客户端可能会感到不开心。

![](/data/attachment/album/201610/09/144221rqrdi1vhdzbf0r3g.png)

这是轮询服务器 [webserver3a.py](https://github.com/rspivak/lsbaws/blob/master/part3/webserver3a.py) 的代码：

```
#####################################################################
# 轮询服务器 - webserver3a.py                                       #
#                                                                   #
# 使用 Python 2.7.9 或 3.4                                          #
# 在 Ubuntu 14.04 及 Mac OS X 环境下测试通过                        #
#####################################################################
import socket

SERVER_ADDRESS = (HOST, PORT) = '', 8888
REQUEST_QUEUE_SIZE = 5

def handle_request(client_connection):
    request = client_connection.recv(1024)
    print(request.decode())
    http_response = b"""\
HTTP/1.1 200 OK

Hello, World!
"""
    client_connection.sendall(http_response)

def serve_forever():
    listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    listen_socket.bind(SERVER_ADDRESS)
    listen_socket.listen(REQUEST_QUEUE_SIZE)
    print('Serving HTTP on port {port} ...'.format(port=PORT))

    while True:
        client_connection, client_address = listen_socket.accept()
        handle_request(client_connection)
        client_connection.close()

if __name__ == '__main__':
    serve_forever()

```

为了观察到你的服务器在同一时间只能处理一个请求的行为，我们对服务器的代码做一点点修改：在将响应发送至客户端之后，将程序阻塞 60 秒。这个修改只需要一行代码，来告诉服务器进程暂停 60 秒钟。

![](/data/attachment/album/201610/09/144221gpbfsxuquizfl2lq.png)

这是我们更改后的代码，包含暂停语句的服务器 [webserver3b.py](https://github.com/rspivak/lsbaws/blob/master/part3/webserver3b.py)：

```
######################################################################
# 轮询服务器 - webserver3b.py                                         #
#                                                                    #
# 使用 Python 2.7.9 或 3.4                                            #
# 在 Ubuntu 14.04 及 Mac OS X 环境下测试通过                           #
#                                                                    #
# - 服务器向客户端发送响应之后，会阻塞 60 秒                             #
######################################################################
import socket
import time

SERVER_ADDRESS = (HOST, PORT) = '', 8888
REQUEST_QUEUE_SIZE = 5

def handle_request(client_connection):
    request = client_connection.recv(1024)
    print(request.decode())
    http_response = b"""\
HTTP/1.1 200 OK

Hello, World!
"""
    client_connection.sendall(http_response)
    time.sleep(60)  ### 睡眠语句，阻塞该进程 60 秒

def serve_forever():
    listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    listen_socket.bind(SERVER_ADDRESS)
    listen_socket.listen(REQUEST_QUEUE_SIZE)
    print('Serving HTTP on port {port} ...'.format(port=PORT))

    while True:
        client_connection, client_address = listen_socket.accept()
        handle_request(client_connection)
        client_connection.close()

if __name__ == '__main__':
    serve_forever()

```

用以下命令启动服务器：

```
$ python webserver3b.py

```

现在，打开一个新的命令行窗口，然后运行 `curl` 语句。你应该可以立刻看到屏幕上显示的字符串“Hello, World!”：

```
$ curl http://localhost:8888/hello
Hello, World!

```

然后，立刻打开第二个命令行窗口，运行相同的 `curl` 命令：

```
$ curl http://localhost:8888/hello

```

如果你在 60 秒之内完成了以上步骤，你会看到第二条 `curl` 指令不会立刻产生任何输出，而只是挂在了哪里。同样，服务器也不会在标准输出流中输出新的请求内容。这是这个过程在我的 Mac 电脑上的运行结果（在右下角用黄色框标注出来的窗口中，我们能看到第二个 `curl` 指令被挂起，正在等待连接被服务器接受）：

![](/data/attachment/album/201610/09/144223uo5qz8qok8wb099h.png)

当你等待足够长的时间（60 秒以上）后，你会看到第一个 `curl` 程序完成，而第二个 `curl` 在屏幕上输出了“Hello, World!”，然后休眠 60 秒，进而终止。

![](/data/attachment/album/201610/09/144224k01m1zo1ef710fmr.png)

这样运行的原因是因为在服务器在处理完第一个来自 `curl` 的请求之后，只有等待 60 秒才能开始处理第二个请求。这个处理请求的过程按顺序进行（也可以说，迭代进行），一步一步进行，在我们刚刚给出的例子中，在同一时间内只能处理一个请求。

现在，我们来简单讨论一下客户端与服务器的交流过程。为了让两个程序在网络中互相交流，它们必须使用套接字。你应当在本系列的前两部分中见过它几次了。但是，套接字是什么？

![](/data/attachment/album/201610/09/144225dabrkbpphuubp5e5.png)

<ruby> 套接字 <rp>  （ </rp> <rt>  socket </rt> <rp>  ） </rp></ruby>是一个通讯通道<ruby> 端点 <rp>  （ </rp> <rt>  endpoint </rt> <rp>  ） </rp></ruby>的抽象描述，它可以让你的程序通过文件描述符来与其它程序进行交流。在这篇文章中，我只会单独讨论 Linux 或 Mac OS X 中的 TCP/IP 套接字。这里有一个重点概念需要你去理解：TCP <ruby> 套接字对 <rp>  （ </rp> <rt>  socket pair </rt> <rp>  ） </rp></ruby>。

>
> TCP 连接使用的套接字对是一个由 4 个元素组成的元组，它确定了 TCP 连接的两端：本地 IP 地址、本地端口、远端 IP 地址及远端端口。一个套接字对唯一地确定了网络中的每一个 TCP 连接。在连接一端的两个值：一个 IP 地址和一个端口，通常被称作一个套接字。（引自[《UNIX 网络编程 卷1：套接字联网 API （第3版）》](http://www.epubit.com.cn/book/details/1692)）
>
>
>

![](/data/attachment/album/201610/09/144226b648t2futy2vyt5v.png)

所以，元组 `{10.10.10.2:49152, 12.12.12.3:8888}` 就是一个能够在客户端确定 TCP 连接两端的套接字对，而元组 `{12.12.12.3:8888, 10.10.10.2:49152}` 则是在服务端确定 TCP 连接两端的套接字对。在这个例子中，确定 TCP 服务端的两个值（IP 地址 `12.12.12.3` 及端口 `8888`），代表一个套接字；另外两个值则代表客户端的套接字。

一个服务器创建一个套接字并开始建立连接的基本工作流程如下：

![](/data/attachment/album/201610/09/144227ozv3rynoay6a76my.png)

1. 服务器创建一个 TCP/IP 套接字。我们可以用这条 Python 语句来创建：

```
listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

```
2. 服务器可能会设定一些套接字选项（这个步骤是可选的，但是你可以看到上面的服务器代码做了设定，这样才能够在重启服务器时多次复用同一地址）：

```
listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

```
3. 然后，服务器绑定一个地址。绑定函数 `bind` 可以将一个本地协议地址赋给套接字。若使用 TCP 协议，调用绑定函数 `bind` 时，需要指定一个端口号，一个 IP 地址，或两者兼有，或两者全无。（引自[《UNIX网络编程 卷1：套接字联网 API （第3版）》](http://www.epubit.com.cn/book/details/1692)）

```
listen_socket.bind(SERVER_ADDRESS)

```
4. 然后，服务器开启套接字的监听模式。

```
listen_socket.listen(REQUEST_QUEUE_SIZE)

```

监听函数 `listen` 只应在服务端调用。它会通知操作系统内核，表明它会接受所有向该套接字发送的入站连接请求。

以上四步完成后，服务器将循环接收来自客户端的连接，一次循环处理一条。当有连接可用时，接受请求函数 `accept` 将会返回一个已连接的客户端套接字。然后，服务器从这个已连接的客户端套接字中读取请求数据，将数据在其标准输出流中输出出来，并向客户端回送一条消息。然后，服务器会关闭这个客户端连接，并准备接收一个新的客户端连接。

这是客户端使用 TCP/IP 协议与服务器通信的必要步骤：

![](/data/attachment/album/201610/09/144228yg1j0wt0l0t40i0g.png)

下面是一段示例代码，使用这段代码，客户端可以连接你的服务器，发送一个请求，并输出响应内容：

```
import socket

### 创建一个套接字，并连接值服务器
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('localhost', 8888))

### 发送一段数据，并接收响应数据
sock.sendall(b'test')
data = sock.recv(1024)
print(data.decode())

```

在创建套接字后，客户端需要连接至服务器。我们可以调用连接函数 `connect` 来完成这个操作：

```
sock.connect(('localhost', 8888))

```

客户端只需提供待连接的远程服务器的 IP 地址（或主机名），及端口号，即可连接至远端服务器。

你可能已经注意到了，客户端不需要调用 `bind` 及 `accept` 函数，就可以与服务器建立连接。客户端不需要调用 `bind` 函数是因为客户端不需要关注本地 IP 地址及端口号。操作系统内核中的 TCP/IP 协议栈会在客户端调用 `connect` 函数时，自动为套接字分配本地 IP 地址及本地端口号。这个本地端口被称为<ruby> 临时端口 <rp>  （ </rp> <rt>  ephemeral port </rt> <rp>  ） </rp></ruby>，即一个短暂开放的端口。

![](/data/attachment/album/201610/09/144229pge5dm0ixbw9i6tj.png)

服务器中有一些端口被用于承载一些众所周知的服务，它们被称作<ruby> 通用 <rp>  （ </rp> <rt>  well-known </rt> <rp>  ） </rp></ruby>端口：如 80 端口用于 HTTP 服务，22 端口用于 SSH 服务。打开你的 Python shell，与你在本地运行的服务器建立一个连接，来看看内核给你的客户端套接字分配了哪个临时端口（在尝试这个例子之前，你需要运行服务器程序 `webserver3a.py` 或 `webserver3b.py`）：

```
>>> import socket
>>> sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
>>> sock.connect(('localhost', 8888))
>>> host, port = sock.getsockname()[:2]
>>> host, port
('127.0.0.1', 60589)

```

在上面的例子中，内核将临时端口 60589 分配给了你的套接字。

在我开始回答我在第二部分中提出的问题之前，我还需要快速讲解一些概念。你很快就会明白这些概念为什么非常重要。这两个概念，一个是进程，另外一个是文件描述符。

什么是进程？进程就是一个程序执行的实体。举个例子：当你的服务器代码被执行时，它会被载入内存，而内存中表现此次程序运行的实体就叫做进程。内核记录了进程的一系列有关信息——比如进程 ID——来追踪它的运行情况。当你在执行轮询服务器 `webserver3a.py` 或 `webserver3b.py` 时，你其实只是启动了一个进程。

![](/data/attachment/album/201610/09/144229pbe9pahr8m2e2r4t.png)

我们在终端窗口中运行 `webserver3b.py`：

```
$ python webserver3b.py

```

在另一个终端窗口中，我们可以使用 `ps` 命令获取该进程的相关信息：

```
$ ps | grep webserver3b | grep -v grep
7182 ttys003    0:00.04 python webserver3b.py

```

`ps` 命令显示，我们刚刚只运行了一个 Python 进程 `webserver3b.py`。当一个进程被创建时，内核会为其分配一个进程 ID，也就是 PID。在 UNIX 中，所有用户进程都有一个父进程；当然，这个父进程也有进程 ID，叫做父进程 ID，缩写为 PPID。假设你默认使用 BASH shell，那当你启动服务器时，就会启动一个新的进程，同时被赋予一个 PID，而它的父进程 PID 会被设为 BASH shell 的 PID。

![](/data/attachment/album/201610/09/144229dnhwgqoo67oanscq.png)

自己尝试一下，看看这一切都是如何工作的。重新开启你的 Python shell，它会创建一个新进程，然后在其中使用系统调用 `os.getpid()` 及 `os.getppid()` 来获取 Python shell 进程的 PID 及其父进程 PID（也就是你的 BASH shell 的 PID）。然后，在另一个终端窗口中运行 `ps` 命令，然后用 `grep` 来查找 PPID（父进程 ID，在我的例子中是 3148）。在下面的屏幕截图中，你可以看到一个我的 Mac OS X 系统中关于进程父子关系的例子，在这个例子中，子进程是我的 Python shell 进程，而父进程是 BASH shell 进程：

![](/data/attachment/album/201610/09/144231mgczk5ap7of0akjx.png)

另外一个需要了解的概念，就是文件描述符。什么是文件描述符？文件描述符是一个非负整数，当进程打开一个现有文件、创建新文件或创建一个新的套接字时，内核会将这个数返回给进程。你以前可能听说过，在 UNIX 中，一切皆是文件。内核会按文件描述符来找到一个进程所打开的文件。当你需要读取文件或向文件写入时，我们同样通过文件描述符来定位这个文件。Python 提供了高层次的操作文件（或套接字）的对象，所以你不需要直接通过文件描述符来定位文件。但是，在高层对象之下，我们就是用它来在 UNIX 中定位文件及套接字，通过这个整数的文件描述符。

![](/data/attachment/album/201610/09/144232r6o3vrgtom63pz4y.png)

一般情况下，UNIX shell 会将一个进程的标准输入流（STDIN）的文件描述符设为 0，标准输出流（STDOUT）设为 1，而标准错误打印（STDERR）的文件描述符会被设为 2。

![](/data/attachment/album/201610/09/144233sdabhm76haaddwa0.png)

我之前提到过，即使 Python 提供了高层次的文件对象或类文件对象来供你操作，你仍然可以在对象上使用 `fileno()` 方法，来获取与该文件相关联的文件描述符。回到 Python shell 中，我们来看看你该怎么做到这一点：

```
>>> import sys
>>> sys.stdin
<open file '<stdin>', mode 'r' at 0x102beb0c0>
>>> sys.stdin.fileno()
0
>>> sys.stdout.fileno()
1
>>> sys.stderr.fileno()
2

```

当你在 Python 中操作文件及套接字时，你可能会使用高层次的文件/套接字对象，但是你仍然有可能会直接使用文件描述符。下面有一个例子，来演示如何用文件描述符做参数来进行一次写入的系统调用：

```
>>> import sys
>>> import os
>>> res = os.write(sys.stdout.fileno(), 'hello\n')
hello

```

下面是比较有趣的部分——不过你可能不会为此感到惊讶，因为你已经知道在 Unix 中，一切皆为文件——你的套接字对象同样有一个相关联的文件描述符。和刚才操纵文件时一样，当你在 Python 中创建一个套接字时，你会得到一个对象而不是一个非负整数，但你永远可以用我之前提到过的 `fileno()` 方法获取套接字对象的文件描述符，并可以通过这个文件描述符来直接操纵套接字。

```
>>> import socket
>>> sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
>>> sock.fileno()
3

```

我还想再提一件事：不知道你有没有注意到，在我们的第二个轮询服务器 `webserver3b.py` 中，当你的服务器休眠 60 秒的过程中，你仍然可以通过第二个 `curl` 命令连接至服务器。当然 `curl` 命令并没有立刻输出任何内容而是挂在哪里，但是既然服务器没有接受连接，那它为什么不立即拒绝掉连接，而让它还能够继续与服务器建立连接呢？这个问题的答案是：当我在调用套接字对象的 `listen` 方法时，我为该方法提供了一个 `BACKLOG` 参数，在代码中用 `REQUEST_QUEUE_SIZE` 常量来表示。`BACKLOG` 参数决定了在内核中为存放即将到来的连接请求所创建的队列的大小。当服务器 `webserver3b.py` 在睡眠的时候，你运行的第二个 `curl` 命令依然能够连接至服务器，因为内核中用来存放即将接收的连接请求的队列依然拥有足够大的可用空间。

尽管增大 `BACKLOG` 参数并不能神奇地使你的服务器同时处理多个请求，但当你的服务器很繁忙时，将它设置为一个较大的值还是相当重要的。这样，在你的服务器调用 `accept` 方法时，不需要再等待一个新的连接建立，而可以立刻直接抓取队列中的第一个客户端连接，并不加停顿地立刻处理它。

欧耶！现在你已经了解了一大块内容。我们来快速回顾一下我们刚刚讲解的知识（当然，如果这些对你来说都是基础知识的话，那我们就当复习好啦）。

![](/data/attachment/album/201610/09/144233qv878ax8rlkb3sso.png)

* 轮询服务器
* 服务端套接字创建流程（创建套接字，绑定，监听及接受）
* 客户端连接创建流程（创建套接字，连接）
* 套接字对
* 套接字
* 临时端口及通用端口
* 进程
* 进程 ID（PID），父进程 ID（PPID），以及进程父子关系
* 文件描述符
* 套接字的 `listen` 方法中，`BACKLOG` 参数的含义

### 如何并发处理多个请求

现在，我可以开始回答第二部分中的那个问题了：“你该如何让你的服务器在同一时间处理多个请求呢？”或者换一种说法：“如何编写一个并发服务器？”

![](/data/attachment/album/201610/09/144234oxoss2xo3ux3sysj.png)

在 UNIX 系统中编写一个并发服务器最简单的方法，就是使用系统调用 `fork()`。

![](/data/attachment/album/201610/09/144234wtjonkwkkt0b2d02.png)

下面是全新出炉的并发服务器 [webserver3c.py](https://github.com/rspivak/lsbaws/blob/master/part3/webserver3c.py) 的代码，它可以同时处理多个请求（和我们之前的例子 [webserver3b.py](https://github.com/rspivak/lsbaws/blob/master/part3/webserver3b.py) 一样，每个子进程都会休眠 60 秒）：

![](/data/attachment/album/201610/09/144221gpbfsxuquizfl2lq.png)

```
#######################################################
# 并发服务器 - webserver3c.py                          #
#                                                     #
# 使用 Python 2.7.9 或 3.4                             #
# 在 Ubuntu 14.04 及 Mac OS X 环境下测试通过            #
#                                                     #
# - 完成客户端请求处理之后，子进程会休眠 60 秒             #
# - 父子进程会关闭重复的描述符                           #
#                                                     #
#######################################################
import os
import socket
import time

SERVER_ADDRESS = (HOST, PORT) = '', 8888
REQUEST_QUEUE_SIZE = 5

def handle_request(client_connection):
    request = client_connection.recv(1024)
    print(
        'Child PID: {pid}. Parent PID {ppid}'.format(
            pid=os.getpid(),
            ppid=os.getppid(),
        )
    )
    print(request.decode())
    http_response = b"""\
HTTP/1.1 200 OK

Hello, World!
"""
    client_connection.sendall(http_response)
    time.sleep(60)

def serve_forever():
    listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    listen_socket.bind(SERVER_ADDRESS)
    listen_socket.listen(REQUEST_QUEUE_SIZE)
    print('Serving HTTP on port {port} ...'.format(port=PORT))
    print('Parent PID (PPID): {pid}\n'.format(pid=os.getpid()))

    while True:
        client_connection, client_address = listen_socket.accept()
        pid = os.fork()
        if pid == 0:  ### 子进程
            listen_socket.close()  ### 关闭子进程中复制的套接字对象
            handle_request(client_connection)
            client_connection.close()
            os._exit(0)  ### 子进程在这里退出
        else:  ### 父进程
            client_connection.close()  ### 关闭父进程中的客户端连接对象，并循环执行

if __name__ == '__main__':
    serve_forever()

```

在深入研究代码、讨论 `fork` 如何工作之前，先尝试运行它，自己看一看这个服务器是否真的可以同时处理多个客户端请求，而不是像轮询服务器 `webserver3a.py` 和 `webserver3b.py` 一样。在命令行中使用如下命令启动服务器：

```
$ python webserver3c.py

```

然后，像我们之前测试轮询服务器那样，运行两个 `curl` 命令，来看看这次的效果。现在你可以看到，即使子进程在处理客户端请求后会休眠 60 秒，但它并不会影响其它客户端连接，因为他们都是由完全独立的进程来处理的。你应该看到你的 `curl` 命令立即输出了“Hello, World!”然后挂起 60 秒。你可以按照你的想法运行尽可能多的 `curl` 命令（好吧，并不能运行特别特别多 `^_^`），所有的命令都会立刻输出来自服务器的响应 “Hello, World!”，并不会出现任何可被察觉到的延迟行为。试试看吧。

如果你要理解 `fork()`，那最重要的一点是：**你调用了它一次，但是它会返回两次** —— 一次在父进程中，另一次是在子进程中。当你创建了一个新进程，那么 `fork()` 在子进程中的返回值是 0。如果是在父进程中，那 `fork()` 函数会返回子进程的 PID。

![](/data/attachment/album/201610/09/144235a1xp89vzdq6n9n90.png)

我依然记得在第一次看到它并尝试使用 `fork()` 的时候，我是多么的入迷。它在我眼里就像是魔法一样。这就好像我在读一段顺序执行的代码，然后“砰！”地一声，代码变成了两份，然后出现了两个实体，同时并行地运行相同的代码。讲真，那个时候我觉得它真的跟魔法一样神奇。

当父进程创建出一个新的子进程时，子进程会复制从父进程中复制一份文件描述符：

![](/data/attachment/album/201610/09/144237dlqkiiqo4j4r9oit.png)

你可能注意到，在上面的代码中，父进程关闭了客户端连接：

```
else:  ### 父进程
    client_connection.close()  # 关闭父进程的副本并循环

```

不过，既然父进程关闭了这个套接字，那为什么子进程仍然能够从来自客户端的套接字中读取数据呢？答案就在上面的图片中。内核会使用描述符引用计数器来决定是否要关闭一个套接字。当你的服务器创建一个子进程时，子进程会复制父进程的所有文件描述符，内核中该描述符的引用计数也会增加。如果只有一个父进程及一个子进程，那客户端套接字的文件描述符引用数应为 2；当父进程关闭客户端连接的套接字时，内核只会减少它的引用计数，将其变为 1，但这仍然不会使内核关闭该套接字。子进程也关闭了父进程中 `listen_socket` 的复制实体，因为子进程不需要关注新的客户端连接，而只需要处理已建立的客户端连接中的请求。

```
listen_socket.close()  ### 关闭子进程中的复制实体

```

我们将会在后文中讨论，如果你不关闭那些重复的描述符，会发生什么。

你可以从你的并发服务器源码中看到，父进程的主要职责为：接受一个新的客户端连接，复制出一个子进程来处理这个连接，然后继续循环来接受另外的客户端连接，仅此而已。服务器父进程并不会处理客户端连接——子进程才会做这件事。

打个岔：当我们说两个事件并发执行时，我们所要表达的意思是什么？

![](/data/attachment/album/201610/09/144238qs0uzarwo5rua2v2.png)

当我们说“两个事件并发执行”时，它通常意味着这两个事件同时发生。简单来讲，这个定义没问题，但你应该记住它的严格定义：

>
> 如果你不能在代码中判断两个事件的发生顺序，那这两个事件就是并发执行的。（引自[《信号系统简明手册 (第二版): 并发控制深入浅出及常见错误》](http://www.amazon.com/gp/product/1441418687/ref=as_li_tl?ie=UTF8&camp=1789&creative=9325&creativeASIN=1441418687&linkCode=as2&tag=russblo0b-20&linkId=QFOAWARN62OWTWUG)）
>
>
>

好的，现在你又该回顾一下你刚刚学过的知识点了。

![](/data/attachment/album/201610/09/144233qv878ax8rlkb3sso.png)

* 在 Unix 中，编写一个并发服务器的最简单的方式——使用 `fork()` 系统调用；
* 当一个进程分叉（`fork`）出另一个进程时，它会变成刚刚分叉出的进程的父进程；
* 在进行 `fork` 调用后，父进程和子进程共享相同的文件描述符；
* 系统内核通过描述符的引用计数来决定是否要关闭该描述符对应的文件或套接字；
* 服务器父进程的主要职责：现在它做的只是从客户端接受一个新的连接，分叉出子进程来处理这个客户端连接，然后开始下一轮循环，去接收新的客户端连接。

### 进程分叉后不关闭重复的套接字会发生什么？

我们来看看，如果我们不在父进程与子进程中关闭重复的套接字描述符会发生什么。下面是刚才的并发服务器代码的修改版本，这段代码（[webserver3d.py](https://github.com/rspivak/lsbaws/blob/master/part3/webserver3d.py) 中，服务器不会关闭重复的描述符）：

```
#######################################################
# 并发服务器 - webserver3d.py                          #
#                                                     #
# 使用 Python 2.7.9 或 3.4                             #
# 在 Ubuntu 14.04 及 Mac OS X 环境下测试通过            #
#######################################################
import os
import socket

SERVER_ADDRESS = (HOST, PORT) = '', 8888
REQUEST_QUEUE_SIZE = 5

def handle_request(client_connection):
    request = client_connection.recv(1024)
    http_response = b"""\
HTTP/1.1 200 OK

Hello, World!
"""
    client_connection.sendall(http_response)

def serve_forever():
    listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    listen_socket.bind(SERVER_ADDRESS)
    listen_socket.listen(REQUEST_QUEUE_SIZE)
    print('Serving HTTP on port {port} ...'.format(port=PORT))

    clients = []
    while True:
        client_connection, client_address = listen_socket.accept()
        ### 将引用存储起来，否则在下一轮循环时，他们会被垃圾回收机制销毁
        clients.append(client_connection)
        pid = os.fork()
        if pid == 0:  ### 子进程
            listen_socket.close()  ### 关闭子进程中多余的套接字
            handle_request(client_connection)
            client_connection.close()
            os._exit(0)  ### 子进程在这里结束
        else:  ### 父进程
            # client_connection.close()
            print(len(clients))

if __name__ == '__main__':
    serve_forever()

```

用以下命令来启动服务器：

```
$ python webserver3d.py

```

用 `curl` 命令连接服务器：

```
$ curl http://localhost:8888/hello
Hello, World!

```

好，`curl` 命令输出了来自并发服务器的响应内容，但程序并没有退出，而是仍然挂起。到底发生了什么？这个服务器并不会挂起 60 秒：子进程只处理客户端连接，关闭连接然后退出，但客户端的 `curl` 命令并没有终止。

![](/data/attachment/album/201610/09/144239j2iv2esj9svdivjs.png)

所以，为什么 `curl` 不终止呢？原因就在于文件描述符的副本。当子进程关闭客户端连接时，系统内核会减少客户端套接字的引用计数，将其变为 1。服务器子进程退出了，但客户端套接字并没有被内核关闭，因为该套接字的描述符引用计数并没有变为 0，所以，这就导致了连接终止包（在 TCP/IP 协议中称作 `FIN`）不会被发送到客户端，所以客户端会一直保持连接。这里也会出现另一个问题：如果你的服务器长时间运行，并且不关闭文件描述符的副本，那么可用的文件描述符会被消耗殆尽：

![](/data/attachment/album/201610/09/144239bxen0lvyjreinl6g.png)

使用 `Control-C` 关闭服务器 `webserver3d.py`，然后在 shell 中使用内置命令 `ulimit` 来查看系统默认为你的服务器进程分配的可用资源数：

```
$ ulimit -a
core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 3842
max locked memory       (kbytes, -l) 64
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1024
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) 3842
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited

```

你可以从上面的结果看到，在我的 Ubuntu 机器中，系统为我的服务器进程分配的最大可用文件描述符（文件打开）数为 1024。

现在我们来看一看，如果你的服务器不关闭重复的描述符，它会如何消耗可用的文件描述符。在一个已有的或新建的终端窗口中，将你的服务器进程的最大可用文件描述符设为 256：

```
$ ulimit -n 256

```

在你刚刚运行 `ulimit -n 256` 的终端窗口中运行服务器 [webserver3d.py](https://github.com/rspivak/lsbaws/blob/master/part3/webserver3d.py)：

```
$ python webserver3d.py

```

然后使用下面的客户端 [client3.py](https://github.com/rspivak/lsbaws/blob/master/part3/client3.py) 来测试你的服务器。

```
#######################################################
# 测试客户端 - client3.py                              #
#                                                     #
# 使用 Python 2.7.9 或 3.4                             #
# 在 Ubuntu 14.04 及 Mac OS X 环境下测试通过            #
#######################################################
import argparse
import errno
import os
import socket

SERVER_ADDRESS = 'localhost', 8888
REQUEST = b"""\
GET /hello HTTP/1.1
Host: localhost:8888

"""

def main(max_clients, max_conns):
    socks = []
    for client_num in range(max_clients):
        pid = os.fork()
        if pid == 0:
            for connection_num in range(max_conns):
                sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                sock.connect(SERVER_ADDRESS)
                sock.sendall(REQUEST)
                socks.append(sock)
                print(connection_num)
                os._exit(0)

if __name__ == '__main__':
    parser = argparse.ArgumentParser(
        description='Test client for LSBAWS.',
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
    )
    parser.add_argument(
        '--max-conns',
        type=int,
        default=1024,
        help='Maximum number of connections per client.'
    )
    parser.add_argument(
        '--max-clients',
        type=int,
        default=1,
        help='Maximum number of clients.'
    )
    args = parser.parse_args()
    main(args.max_clients, args.max_conns)

```

在一个新建的终端窗口中，运行 `client3.py` 然后让它与服务器同步创建 300 个连接：

```
$ python client3.py --max-clients=300

```

过一会，你的服务器进程就该爆了。这是我的环境中出现的异常截图：

![](/data/attachment/album/201610/09/144240p2xiaofi2cxhoo2m.png)

这个例子很明显——你的服务器应该关闭描述符副本。

#### 僵尸进程

但是，即使你关闭了描述符副本，你依然没有摆脱险境，因为你的服务器还有一个问题，这个问题在于“<ruby> 僵尸 <rp>  （ </rp> <rt>  zombies </rt> <rp>  ） </rp></ruby>”！

![](/data/attachment/album/201610/09/144241uwx1agi4xljgwgir.png)

没错，这个服务器代码确实在制造僵尸进程。我们来看看怎么回事。重新运行你的服务器：

```
$ python webserver3d.py

```

在另一个终端窗口中运行以下 `curl` 命令：

```
$ curl http://localhost:8888/hello

```

现在，运行 `ps` 环境，来查看正在运行的 Python 进程。下面是我的环境中 `ps` 的运行结果：

```
$ ps auxw | grep -i python | grep -v grep
vagrant   9099  0.0  1.2  31804  6256 pts/0    S+   16:33   0:00 python webserver3d.py
vagrant   9102  0.0  0.0      0     0 pts/0    Z+   16:33   0:00 [python] <defunct>

```

你看到第二行中，pid 为 9102，状态为 `Z+`，名字里面有个 `<defunct>` 的进程了吗？那就是我们的僵尸进程。这个僵尸进程的问题在于：你无法将它杀掉！

![](/data/attachment/album/201610/09/144242rnzg3zh0d1qmnm0h.png)

就算你尝试使用 `kill -9` 来杀死僵尸进程，它们仍旧会存活。自己试试看，看看结果。

这个僵尸到底是什么，为什么我们的服务器会造出它们呢？一个<ruby> 僵尸进程 <rp>  （ </rp> <rt>  zombie </rt> <rp>  ） </rp></ruby>是一个已经结束的进程，但它的父进程并没有等待（`waited`）它结束，并且也没有收到它的终结状态。如果一个进程在父进程退出之前退出，系统内核会把它变为一个僵尸进程，存储它的部分信息，以便父进程读取。内核保存的进程信息通常包括进程 ID、进程终止状态，以及进程的资源占用情况。OK，所以僵尸进程确实有存在的意义，但如果服务器不管这些僵尸进程，你的系统将会被壅塞。我们来看看这个会如何发生。首先，关闭你运行的服务器；然后，在一个新的终端窗口中，使用 `ulimit` 命令将最大用户进程数设为 400（同时，要确保你的最大可用描述符数大于这个数字，我们在这里设为 500）：

```
$ ulimit -u 400
$ ulimit -n 500

```

在你刚刚运行 `ulimit -u 400` 命令的终端中，运行服务器 `webserver3d.py`：

```
$ python webserver3d.py

```

在一个新的终端窗口中，运行 `client3.py`，并且让它与服务器同时创建 500 个连接：

```
$ python client3.py --max-clients=500

```

然后，过一会，你的服务器进程应该会再次爆了，它会在创建新进程时抛出一个 `OSError: 资源暂时不可用` 的异常。但它并没有达到系统允许的最大进程数。这是我的环境中输出的异常信息截图：

![](/data/attachment/album/201610/09/144242xwf457nwft574404.png)

你可以看到，如果服务器不管僵尸进程，它们会引发问题。接下来我会简单探讨一下僵尸进程问题的解决方案。

我们来回顾一下你刚刚掌握的知识点：

![](/data/attachment/album/201610/09/144233qv878ax8rlkb3sso.png)

* 如果你不关闭文件描述符副本，客户端就不会在请求处理完成后终止，因为客户端连接没有被关闭；
* 如果你不关闭文件描述符副本，长久运行的服务器最终会把可用的文件描述符（最大文件打开数）消耗殆尽；
* 当你创建一个新进程，而父进程不等待（`wait`）子进程，也不在子进程结束后收集它的终止状态，它会变为一个僵尸进程；
* 僵尸通常都会吃东西，在我们的例子中，僵尸进程会吃掉资源。如果你的服务器不管僵尸进程，它最终会消耗掉所有的可用进程（最大用户进程数）；
* 你不能杀死（`kill`）僵尸进程，你需要等待（`wait`）它。

### 如何处理僵尸进程？

所以，你需要做什么来处理僵尸进程呢？你需要修改你的服务器代码，来等待（`wait`）僵尸进程，并收集它们的终止信息。你可以在代码中使用系统调用 `wait` 来完成这个任务。不幸的是，这个方法离理想目标还很远，因为在没有终止的子进程存在的情况下调用 `wait` 会导致服务器进程阻塞，这会阻碍你的服务器处理新的客户端连接请求。那么，我们有其他选择吗？嗯，有的，其中一个解决方案需要结合信号处理以及 `wait` 系统调用。

![](/data/attachment/album/201610/09/144243orgiz9gzzotl1fo7.png)

这是它的工作流程。当一个子进程退出时，内核会发送 `SIGCHLD` 信号。父进程可以设置一个信号处理器，它可以异步响应 `SIGCHLD` 信号，并在信号响应函数中等待（`wait`）子进程收集终止信息，从而阻止了僵尸进程的存在。

![](/data/attachment/album/201610/09/144244n844sw233eab5s58.png)

顺便说一下，异步事件意味着父进程无法提前知道事件的发生时间。

修改你的服务器代码，设置一个 `SIGCHLD` 信号处理器，在信号处理器中等待（`wait`）终止的子进程。修改后的代码如下（[webserver3e.py](https://github.com/rspivak/lsbaws/blob/master/part3/webserver3e.py)）：

```
#######################################################
# 并发服务器 - webserver3e.py                          #
#                                                     #
# 使用 Python 2.7.9 或 3.4                             #
# 在 Ubuntu 14.04 及 Mac OS X 环境下测试通过            #
#######################################################
import os
import signal
import socket
import time

SERVER_ADDRESS = (HOST, PORT) = '', 8888
REQUEST_QUEUE_SIZE = 5

def grim_reaper(signum, frame):
    pid, status = os.wait()
    print(
        'Child {pid} terminated with status {status}'
        '\n'.format(pid=pid, status=status)
    )

def handle_request(client_connection):
    request = client_connection.recv(1024)
    print(request.decode())
    http_response = b"""\
HTTP/1.1 200 OK

Hello, World!
"""
    client_connection.sendall(http_response)
    ### 挂起进程，来允许父进程完成循环，并在 "accept" 处阻塞
    time.sleep(3)

def serve_forever():
    listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    listen_socket.bind(SERVER_ADDRESS)
    listen_socket.listen(REQUEST_QUEUE_SIZE)
    print('Serving HTTP on port {port} ...'.format(port=PORT))

    signal.signal(signal.SIGCHLD, grim_reaper)

    while True:
        client_connection, client_address = listen_socket.accept()
        pid = os.fork()
        if pid == 0:  ### 子进程
            listen_socket.close()  ### 关闭子进程中多余的套接字
            handle_request(client_connection)
            client_connection.close()
            os._exit(0)
        else:  ### 父进程
            client_connection.close()

if __name__ == '__main__':
    serve_forever()

```

运行服务器：

```
$ python webserver3e.py

```

使用你的老朋友——`curl` 命令来向修改后的并发服务器发送一个请求：

```
$ curl http://localhost:8888/hello

```

再来看看服务器：

![](/data/attachment/album/201610/09/144245lantztjl3tlalitl.png)

刚刚发生了什么？`accept` 调用失败了，错误信息为 `EINTR`。

![](/data/attachment/album/201610/09/144246txay743rab5zura7.png)

当子进程退出并触发 `SIGCHLD` 事件时，父进程的 `accept` 调用被阻塞了，系统转去运行信号处理器，当信号处理函数完成时，`accept` 系统调用被打断：

![](/data/attachment/album/201610/09/144246fea64024jb14hda1.png)

别担心，这个问题很好解决。你只需要重新运行 `accept` 系统调用即可。这是修改后的服务器代码 [webserver3f.py](https://github.com/rspivak/lsbaws/blob/master/part3/webserver3f.py)，它可以解决这个问题：

```
#######################################################
# 并发服务器 - webserver3f.py                          #
#                                                     #
# 使用 Python 2.7.9 或 3.4                             #
# 在 Ubuntu 14.04 及 Mac OS X 环境下测试通过            #
#######################################################
import errno
import os
import signal
import socket

SERVER_ADDRESS = (HOST, PORT) = '', 8888
REQUEST_QUEUE_SIZE = 1024

def grim_reaper(signum, frame):
    pid, status = os.wait()

def handle_request(client_connection):
    request = client_connection.recv(1024)
    print(request.decode())
    http_response = b"""\
HTTP/1.1 200 OK

Hello, World!
"""
    client_connection.sendall(http_response)

def serve_forever():
    listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    listen_socket.bind(SERVER_ADDRESS)
    listen_socket.listen(REQUEST_QUEUE_SIZE)
    print('Serving HTTP on port {port} ...'.format(port=PORT))

    signal.signal(signal.SIGCHLD, grim_reaper)

    while True:
        try:
            client_connection, client_address = listen_socket.accept()
        except IOError as e:
            code, msg = e.args
            ### 若 'accept' 被打断，那么重启它
            if code == errno.EINTR:
                continue
            else:
                raise

        pid = os.fork()
        if pid == 0:  ### 子进程
            listen_socket.close()  ### 关闭子进程中多余的描述符
            handle_request(client_connection)
            client_connection.close()
            os._exit(0)
        else:  ### 父进程
            client_connection.close()  ### 关闭父进程中多余的描述符，继续下一轮循环

if __name__ == '__main__':
    serve_forever()

```

运行更新后的服务器 `webserver3f.py`：

```
$ python webserver3f.py

```

用 `curl` 来向更新后的并发服务器发送一个请求：

```
$ curl http://localhost:8888/hello

```

看到了吗？没有 EINTR 异常出现了。现在检查一下，确保没有僵尸进程存活，调用 `wait` 函数的 `SIGCHLD` 信号处理器能够正常处理被终止的子进程。我们只需使用 `ps` 命令，然后看看现在没有处于 `Z+` 状态（或名字包含 `<defunct>` ）的 Python 进程就好了。很棒！僵尸进程没有了，我们很安心。

![](/data/attachment/album/201610/09/144233qv878ax8rlkb3sso.png)

* 如果你创建了一个子进程，但是不等待它，它就会变成一个僵尸进程；
* 使用 `SIGCHLD` 信号处理器可以异步地等待子进程终止，并收集其终止状态；
* 当使用事件处理器时，你需要牢记，系统调用可能会被打断，所以你需要处理这种情况发生时带来的异常。

#### 正确处理 SIGCHLD 信号

好的，一切顺利。是不是没问题了？额，几乎是。重新尝试运行 `webserver3f.py` 但我们这次不会只发送一个请求，而是同步创建 128 个连接：

```
$ python client3.py --max-clients 128

```

现在再次运行 `ps` 命令：

```
$ ps auxw | grep -i python | grep -v grep

```

看到了吗？天啊，僵尸进程又出来了！

![](/data/attachment/album/201610/09/144247j00e0g8unel00ynx.png)

这回怎么回事？当你同时运行 128 个客户端，建立 128 个连接时，服务器的子进程几乎会在同一时间处理好你的请求，然后退出。这会导致非常多的 `SIGCHLD` 信号被发送到父进程。问题在于，这些信号不会存储在队列中，所以你的服务器进程会错过很多信号，这也就导致了几个僵尸进程处于无主状态：

![](/data/attachment/album/201610/09/144248d4q49tpglnnnntpz.png)

这个问题的解决方案依然是设置 `SIGCHLD` 事件处理器。但我们这次将会用 `WNOHANG` 参数循环调用 `waitpid` 来替代 `wait`，以保证所有处于终止状态的子进程都会被处理。下面是修改后的代码，[webserver3g.py](https://github.com/rspivak/lsbaws/blob/master/part3/webserver3g.py)：

```
#######################################################
# 并发服务器 - webserver3g.py                          #
#                                                     #
# 使用 Python 2.7.9 或 3.4                             #
# 在 Ubuntu 14.04 及 Mac OS X 环境下测试通过            #
#######################################################
import errno
import os
import signal
import socket

SERVER_ADDRESS = (HOST, PORT) = '', 8888
REQUEST_QUEUE_SIZE = 1024

def grim_reaper(signum, frame):
    while True:
        try:
            pid, status = os.waitpid(
                -1,          ### 等待所有子进程
                 os.WNOHANG  ### 无终止进程时，不阻塞进程，并抛出 EWOULDBLOCK 错误
            )
        except OSError:
            return

        if pid == 0:  ### 没有僵尸进程存在了
            return

def handle_request(client_connection):
    request = client_connection.recv(1024)
    print(request.decode())
    http_response = b"""\
HTTP/1.1 200 OK

Hello, World!
"""
    client_connection.sendall(http_response)

def serve_forever():
    listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    listen_socket.bind(SERVER_ADDRESS)
    listen_socket.listen(REQUEST_QUEUE_SIZE)
    print('Serving HTTP on port {port} ...'.format(port=PORT))

    signal.signal(signal.SIGCHLD, grim_reaper)

    while True:
        try:
            client_connection, client_address = listen_socket.accept()
        except IOError as e:
            code, msg = e.args
            ### 若 'accept' 被打断，那么重启它
            if code == errno.EINTR:
                continue
            else:
                raise

        pid = os.fork()
        if pid == 0:  ### 子进程
            listen_socket.close()  ### 关闭子进程中多余的描述符
            handle_request(client_connection)
            client_connection.close()
            os._exit(0)
        else:  ### 父进程
            client_connection.close()  ### 关闭父进程中多余的描述符，继续下一轮循环

if __name__ == '__main__':
    serve_forever()

```

运行服务器：

```
$ python webserver3g.py

```

使用测试客户端 `client3.py`：

```
$ python client3.py --max-clients 128

```

现在来查看一下，确保没有僵尸进程存在。耶！没有僵尸的生活真美好 `^_^`。

![](/data/attachment/album/201610/09/144249sxqgjqoj5cvo5on5.png)

### 大功告成

恭喜！你刚刚经历了一段很长的旅程，我希望你能够喜欢它。现在你拥有了自己的简易并发服务器，并且这段代码能够为你在继续研究生产级 Web 服务器的路上奠定基础。

我将会留一个作业：你需要将第二部分中的 WSGI 服务器升级，将它改造为一个并发服务器。你可以在[这里](https://github.com/rspivak/lsbaws/blob/master/part3/webserver3h.py)找到更改后的代码。但是，当你实现了自己的版本之后，你才应该来看我的代码。你已经拥有了实现这个服务器所需的所有信息。所以，快去实现它吧 `^_^`。

然后要做什么呢？乔希·比林斯说过：

>
> “就像一枚邮票一样——专注于一件事，不达目的不罢休。”
>
>
>

开始学习基本知识。回顾你已经学过的知识。然后一步一步深入。

![](/data/attachment/album/201610/09/144250vs6pqlc7jzpbs57b.png)

>
> “如果你只学会了方法，你将会被这些方法所困。但如果你学会了原理，那你就能发明出新的方法。”——拉尔夫·沃尔多·爱默生
>
>
> “有道无术，术尚可求也，有术无道，止于术”——中国古代也有这样的话，LCTT 译注
>
>
>

下面是一份书单，我从这些书中提炼出了这篇文章所需的素材。他们能助你在我刚刚所述的几个方面中发掘出兼具深度和广度的知识。我极力推荐你们去搞到这几本书看看：从你的朋友那里借，在当地的图书馆中阅读，或者直接在亚马逊上把它买回来。下面是我的典藏秘籍：

1. [《UNIX 网络编程 卷1：套接字联网 API （第3版）》](http://www.epubit.com.cn/book/details/1692)
2. [《UNIX 环境高级编程（第3版）》](http://www.epubit.com.cn/book/details/1625)
3. [《Linux/UNIX 系统编程手册》](http://www.epubit.com.cn/book/details/1432)
4. [《TCP/IP 详解 卷1：协议（第2版）](http://www.epubit.com.cn/book/details/4232)
5. [《信号系统简明手册 (第二版): 并发控制深入浅出及常见错误》](http://www.amazon.com/gp/product/1441418687/ref=as_li_tl?ie=UTF8&camp=1789&creative=9325&creativeASIN=1441418687&linkCode=as2&tag=russblo0b-20&linkId=QFOAWARN62OWTWUG)，这本书也可以从[作者的个人网站](http://greenteapress.com/semaphores/)中免费下载到。

顺便，我在撰写一本名为《搭个 Web 服务器：从头开始》的书。这本书讲解了如何从头开始编写一个基本的 Web 服务器，里面包含本文中没有的更多细节。订阅[原文下方的邮件列表](https://ruslanspivak.com/lsbaws-part1/)，你就可以获取到这本书的最新进展，以及发布日期。

---

via: <https://ruslanspivak.com/lsbaws-part3/>

作者：[Ruslan](https://github.com/rspivak/) 译者：[StdioA](https://github.com/StdioA) 校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


地下城与码农：点亮你的技能树

Summary: 作为专业人员，我们不少人喜欢玩角色扮演游戏，比如 D&D 这样的，那么有没有想过把你在 Web 前端方面的技能、经验也像游戏里面一样的技能树一样点亮呢？记得回来分享你的链接。

第一层全部点满，第二层点不到。我适合干什么

适合继续学习。

内容貌似为web端，移动端的怎么办？

并没有考虑移动端，这东西又不是给一般人用的，使用频率也低，Web端就可以了

如果想web端进军的话，倒是不错的学习之路。你这么看？

作为专业人员，我们不少人喜欢玩角色扮演游戏，比如 D&D 这样的，那么有没有想过把你在 Web 前端方面的技能、经验也像游戏里面一样的技能树一样点亮呢？

今天给大家介绍的就是这样的一个有趣的网站：[地下城与码农](http://www.dungeonsanddevelopers.com/)（Dungeons aNd Developers）。

题图就是老王自己点亮的技能树。

![](/data/attachment/album/201610/10/091023vxxlmd757ff00950.jpg)

啊哈，老王自己都不知道已经懂这么多了。是不是也想试试？

那么就点此链接吧： <http://www.dungeonsanddevelopers.com/> ，会看到如下界面：

![](/data/attachment/album/201610/10/091239attqfwrrzm95flrf.jpg)

点击“Open the talent tree »”，即可看到一个空白的技能树，虽然是英文的，不过这应该难不倒你。

英雄！去点亮你的技能树吧，你会回来分享你的链接的。

当然，作为专业人员，只是这样点一点，似乎并不满足，那么我们来探究一下背后所用的技术吧。

这个网站是 352 公司在内部竞赛中产生的，除了精美的仿游戏界面外，采用了 [jQuery](http://www.jquery.com/) 和 [Knockout](http://www.knockoutjs.com/) 来构建的。jQuery 大家已经熟知了，而 [Knockout](http://www.knockoutjs.com/) 可能有的人不太了解，其实它也不太新了，但是它的特性正好用在这个项目上：

>
> Knockout 是一款很优秀的 JavaScript 库，它可以帮助你仅使用一个清晰整洁的底层数据模型，即可创建一个富文本且具有良好的显示和编辑功能的用户界面。任何时候你的局部 UI 内容需要自动更新（比如：依赖于用户行为的改变或者外部的数据源发生变化），Knockout 都可以很简单的帮你实现，并且非常易于维护。
>
>
>

是不是也想自己搭建一个呢？


内容安全策略（CSP），防御 XSS 攻击的好助手

Summary: CSP 对你的用户来说是尤其重要的：他们在你的网站上不再需要遭受任何的未经请求的脚本，内容或 XSS 的威胁了。对于网站维护者来说 CSP 最重要的优势就是可感知。如果你对图片来源设置了严格的规则，这时一个脚本小子尝试在你的网站上插入一张未授权来源的图片，那么这张图片就会被禁止，并且你会在第一时间收到提醒。

很久之前，我的个人网站被攻击了。我不知道它是如何发生的，但它确实发生了。幸运的是，攻击带来的破坏是很小的：一小段 JavaScript 被注入到了某些页面的底部。我更新了 FTP 和其它的口令，清理了一些文件，事情就这样结束了。

有一点使我很恼火：在当时，还没有一种简便的方案能够使我知道那里有问题，更重要的是能够保护网站的访客不被这段恼人的代码所扰。

现在有一种方案出现了，这种技术在上述两方面都十分的成功。它就是<ruby> 内容安全策略 <rp>  （ </rp> <rt>  content security policy </rt> <rp>  ） </rp></ruby>（CSP）。

![](/data/attachment/album/201610/10/145649v0q1bm2a406wmkwq.png)

### 什么是 CSP?

其核心思想十分简单：网站通过发送一个 CSP 头部，来告诉浏览器什么是被授权执行的与什么是需要被禁止的。

这里有一个 PHP 的例子：

```
<?php
    header("Content-Security-Policy: <your directives>");
?>

```

#### 一些指令

你可以定义一些全局规则或者定义一些涉及某一类资源的规则：

```
default-src 'self' ;
     # self = 同端口，同域名，同协议 => 允许

```

基础参数是 `default-src`：如果没有为某一类资源设置指令规则，那么浏览器就会使用这个默认参数值。

```
script-src 'self' www.google-analytics.com ;
     # 来自这些域名的 JS 文件 => 允许

```

在这个例子中，我们已经授权了 www.google-analytics.com 这个域名来源的 JavaScript 文件使用到我们的网站上。我们也添加了 `'self'` 这个关键词；如果我们通过 `script-src` 来重新设置其它的规则指令，它将会覆盖 `default-src` 规则。

如果没有指明协议（scheme）或端口，它就会强制选择与当前页面相同的协议或端口。这样做防止了混合内容（LCTT 译注：混合内容指 HTTPS 页面中也有非 HTTPS 资源，可参见： <https://developer.mozilla.org/zh-CN/docs/Security/MixedContent> ）。如果页面是 https://example.com，那么你将无法加载 http://www.google-analytics.com/file.js 因为它已经被禁止了（协议不匹配）。然而，有一个例外就是协议的提升是被允许的。如果 http://example.com 尝试加载 https://www.google-analytics.com/file.js，接着协议或端口允许被更改以便协议的提升。

```
style-src 'self' data: ;
     # Data-Uri 嵌入 CSS => 允许

```

在这个例子中，关键词 `data:` 授权了在 CSS 文件中 data 内嵌内容。

在 CSP 1 规范下，你也可以设置如下规则：

* `img-src` 有效的图片来源
* `connect-src` 应用于 XMLHttpRequest（AJAX），WebSocket 或 EventSource
* `font-src` 有效的字体来源
* `object-src` 有效的插件来源（例如，`<object>`，`<embed>`，`<applet>`）
* `media-src` 有效的 `<audio>` 和 `<video>` 来源

CSP 2 规范包含了如下规则：

* `child-src` 有效的 web workers 和 元素来源，如 `<frame>` 和 `<iframe>` （这个指令用来替代 CSP 1 中废弃了的 `frame-src` 指令）
* `form-action` 可以作为 HTML `<form>` 的 action 的有效来源
* `frame-ancestors` 使用 `<frame>`，`<iframe>`，`<object>`，`<embed>` 或 `<applet>` 内嵌资源的有效来源
* `upgrade-insecure-requests` 命令用户代理来重写 URL 协议，将 HTTP 改到 HTTPS （为一些需要重写大量陈旧 URL 的网站提供了方便）。

为了更好的向后兼容一些废弃的属性，你可以简单的复制当前指令的内容同时为那个废弃的指令创建一个相同的副本。例如，你可以复制 `child-src` 的内容同时在 `frame-src` 中添加一份相同的副本。

CSP 2 允许你添加路径到白名单中（CSP 1 只允许域名被添加到白名单中）。因此，相较于将整个 www.foo.com 域添加到白名单，你可以通过添加 www.foo.com/some/folder 这样的路径到白名单中来作更多的限制。这个需要浏览器中 CSP 2 的支持，但它很明显更安全。

#### 一个例子

我为 Web 2015 巴黎大会上我的演讲 “[CSP in Action](https://rocssti.net/en/example-csp-paris-web2015)”制作了一个简单的例子。

在没有 CSP 的情况下，页面展示如下图所示：

![](/data/attachment/album/201610/10/145806lyy9osomrvtnvoc4.jpg)

不是十分优美。要是我们启用了如下的 CSP 指令又会怎样呢？

```
<?php
    header("Content-Security-Policy:
      default-src 'self' ;
      script-src 'self' www.google-analytics.com stats.g.doubleclick.net ;
      style-src 'self' data: ;
      img-src 'self' www.google-analytics.com stats.g.doubleclick.net data: ;
      frame-src 'self' ;");
?>

```

浏览器将会作什么呢？它会（非常严格的）在 CSP 基础规则之下应用这些指令，这意味着**任何没有在 CSP 指令中被授权允许的都将会被禁止**（“blocked” 指的是不被执行、不被显示并且不被使用在网站中）。

在 CSP 的默认设置中，内联脚本和样式是不被授权的，意味着每一个 `<script>`，`onclick` 事件属性或 `style` 属性都将会被禁止。你可以使用 `style-src 'unsafe-inline' ;` 指令来授权使用内联 CSS。

在一个支持 CSP 的现代浏览器中，上述示例看起来如下图：

![](/data/attachment/album/201610/10/145807tmhp7qg7uqploghz.jpg)

发生了什么？浏览器应用了指令并且拒绝了所有没有被授权的内容。它在浏览器调试终端中发送了这些通知：

![](/data/attachment/album/201610/10/145807liay2sf2f0ujtb9z.jpg)

如果你依然不确定 CSP 的价值，请看一下 Aaron Gustafson 文章 “[More Proof We Don't Control Our Web Pages](https://www.aaron-gustafson.com/notebook/more-proof-we-dont-control-our-web-pages/)”。

当然，你可以使用比我们在示例中提供的更严格的指令：

* 设置 `default-src` 为 'none'
* 为每条规则指定你的设置
* 为请求的文件指定它的绝对路径
* 等

### 更多关于 CSP 的信息

#### 支持

CSP 不是一个需要复杂的配置才能正常工作的每日构建特性。CSP 1 和 2 是候选推荐标准！[浏览器可以非常完美的支持 CSP 1](http://caniuse.com/#feat=contentsecuritypolicy)。

![](/data/attachment/album/201610/10/145808l5hjg2ghdd9h5a5h.jpg)

[CSP 2 是较新的规范](http://caniuse.com/#feat=contentsecuritypolicy2)，因此对它的支持会少那么一点。

![](/data/attachment/album/201610/10/145808kp7f73sk786jh7rk.jpg)

现在 CSP 3 还是一个早期草案，因此还没有被支持，但是你依然可以使用 CSP 1 和 2 来做一些重大的事。

#### 其他需要考虑的因素

CSP 被设计用来降低跨站脚本攻击（XSS）的风险，这就是不建议开启内联脚本和 `script-src` 指令的原因。Firefox 对这个问题做了很好的说明：在浏览器中，敲击 `Shift + F2` 并且键入 `security csp`，它就会向你展示指令和对应的建议。这里有一个在 Twitter 网站中应用的例子：

![](/data/attachment/album/201610/10/145809jnfau25o34fo7nag.jpg)

如果你确实需要使用内联脚本和样式的话，另一种可能就是生成一份散列值。例如，我们假定你需要使用如下的内联脚本：

```
<script>alert('Hello, world.');</script>

```

你应该在 `script-src` 指令中添加 `sha256-qznLcsROx4GACP2dm0UCKCzCG-HiZ1guq6ZZDob_Tng=` 作为有效来源。这个散列值用下面的 PHP 脚本执行获得的结果：

```
<?php
    echo base64_encode(hash('sha256', "alert('Hello, world.');", true));
?>

```

我在前文中说过 CSP 被设计用来降低 XSS 风险，我还得加上“……与降低未经请求内容的风险。”伴随着 CSP 的使用，你必须**知道你内容的来源是哪里**与**它们在你的前端都作了些什么**（内联样式，等）。CSP 同时可以帮助你让贡献者、开发人员和其他人员来遵循你内容来源的规则！

现在你的问题就只是，“不错，这很好，但是我们如何在生产环境中使用它呢？”

### 如何在现实世界中使用它

想要在第一次使用 CSP 之后就失望透顶的方法就是在生产环境中测试。不要想当然的认为，“这会很简单。我的代码是完美并且相当清晰的。”不要这样作。我这样干过。相信我，这相当的蠢。

正如我之前说明的，CSP 指令由 CSP 头部来激活，这中间没有过渡阶段。你恰恰就是其中的薄弱环节。你可能会忘记授权某些东西或者遗忘了你网站中的一小段代码。CSP 不会饶恕你的疏忽。然而，CSP 的两个特性将这个问题变得相当的简单。

#### report-uri

还记得 CSP 发送到终端中的那些通知么？`report-uri` 指令可以被用来告诉浏览器发送那些通知到指定的地址。报告以 JSON 格式送出。

```
report-uri /csp-parser.php ;

```

因此，我们可以在 csp-parser.php 文件中处理有浏览器送出的数据。这里有一个由 PHP 实现的最基础的例子：

```
$data = file_get_contents('php://input');

    if ($data = json_decode($data, true)) {
     $data = json_encode(
      $data,
      JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES
      );
     mail(EMAIL, SUBJECT, $data);
    }

```

这个通知将会被转换成为一封邮件。在开发过程中，你可能不会需要比这更复杂的其它东西。

对于一个生产环境（或者是一个有较多访问的开发环境），你应该使用一种比邮件更好的收集信息的方式，因为这种方式在节点上没有验证和速率限制，并且 CSP 可能变得乱哄哄的。只需想像一个会产生 100 个 CSP 通知（例如，一个从未授权来源展示图片的脚本）并且每天会被浏览 100 次的页面，那你就会每天收到 10000 个通知啊！

例如 [report-uri.io](https://report-uri.io/) 这样的服务可以用来简化你的通知管理。你也可以在 GitHub上看一些另外的使用 `report-uri` （与数据库搭配，添加一些优化，等）的简单例子。

### report-only

正如我们所见的，最大的问题就是在使用和不使用 CSP 之间没有中间地带。然而，一个名为 `report-only` 的特性会发送一个稍有不同的头部：

```
<?php
    header("Content-Security-Policy-Report-Only: <your directives>");
?>

```

总的来说，这个头部就是告诉浏览器，“表现得似乎所有的 CSP 指令都被应用了，但是不禁止任何东西。只是发送通知给自己。”这是一种相当棒的测试指令的方式，避免了任何有价值的东西被禁止的风险。

在 `report-only` 和 `report-uri` 的帮助下你可以毫无风险的测试 CSP 指令，并且可以实时的监控网站上一切与 CSP 相关的内容。这两个特性对部署和维护 CSP 来说真是相当的有用！

### 结论

#### 为什么 CSP 很酷

CSP 对你的用户来说是尤其重要的：他们在你的网站上不再需要遭受任何的未经请求的脚本，内容或 XSS 的威胁了。

对于网站维护者来说 CSP 最重要的优势就是可感知。如果你对图片来源设置了严格的规则，这时一个脚本小子尝试在你的网站上插入一张未授权来源的图片，那么这张图片就会被禁止，并且你会在第一时间收到提醒。

开发者也需要确切的知道他们的前端代码都在做些什么，CSP 可以帮助他们掌控一切。会促使他们去重构他们代码中的某些部分（避免内联函数和样式，等）并且促使他们遵循最佳实践。

#### 如何让 CSP 变得更酷

讽刺的是，CSP 在一些浏览器中过分的高效了，在和书签栏小程序一起使用时会产生一些 bug。因此，不要更新你的 CSP 指令来允许书签栏小程序。我们无法单独的责备任何一个浏览器；它们都有些问题：

* Firefox
* Chrome (Blink)
* WebKit

大多数情况下，这些 bug 都是禁止通知中的误报。所有的浏览器提供者都在努力解决这些问题，因此我们可以期待很快就会被解决。无论怎样，这都不会成为你使用 CSP 的绊脚石。

---

via: <https://www.smashingmagazine.com/2016/09/content-security-policy-your-future-best-friend/>

作者：[Nicolas Hoffmann](https://www.smashingmagazine.com/author/nicolashoffmann/) 译者：[wcnnbdk1](https://github.com/wcnnbdk1) 校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出