Python-系统管理高级教程-六-

45 阅读1小时+

Python 系统管理高级教程(六)

原文:Pro Python System Administration

协议:CC BY-NC-SA 4.0

十二、分布式信息处理系统

在前三章中,我们构建了一个基于 XML-RPC 消息协议的分布式监控系统。虽然它工作得很好,但它可能缺少一些功能,如消息优先级和任务调度。我们可以在已经编写的代码中添加额外的功能,但是我将向您展示如何用一个更健壮、功能更全的基于分布式任务队列 Celery 的系统来替换定制的消息传递平台。

消息和任务队列快速介绍

任务队列是一种强大的机制,允许您将工作分割成更小的块,将这些工作块发送到大量的机器,然后收集结果。根据您可以使用的机器数量,您可能会显著增加处理时间。

任务排队系统

从本质上讲,任务排队机制相对简单。主进程生成一个或多个需要处理的任务,然后将指令推入任务队列。一个(或多个)工作进程监视队列,一旦发现新任务,它就从队列中获取它。当任务完成时,结果(如果有)被发送回主进程。该过程如图 12-1 所示。

9781484202180_Fig12-01.jpg

图 12-1 。任务排队

这种机制可用于将任务分配给在单台机器或多台机器上运行的多个进程。

理解任务队列是在多个进程之间分配任务的一种方法是很重要的。它不是特定的实现或产品。这种任务分配可以应用于多个级别。例如,您可以使用一个线程作为主进程,多个线程作为辅助进程来创建应用级任务队列。然后线程可以使用共享变量在它们之间分配任务。另一个例子是主机级任务分发。一台主机可以是主控主机,它生成任务并将任务下推到处理这些任务的工作主机。一个实际的例子是 web 邮件系统,其中前端(主节点)节点接受用户输入并将其发送到邮件处理节点(工作节点),然后邮件处理节点充当邮件中继并将电子邮件发送出去。再者,任务队列甚至根本不需要和电脑有关系!例如,团队领导可以在便利贴上写下一天的任务,贴在白板上,然后团队成员可以在一天中拿起它们,做笔记上写的事情。

如果希望异步执行长时间运行的任务,任务队列非常有用。您需要任务被处理,但不需要马上得到结果。一个很好的例子是从 web 表单发送电子邮件。电子邮件可能需要一段时间才能发送,尤其是在远程邮件服务器不可用并且您需要多次重试发送邮件的情况下。同时,你不希望用户一直等到邮件发出。因此,web 前端从用户那里获取 web 表单数据,将其发送到邮件中继进行进一步处理,并指示用户电子邮件正在发送。

高度分布式任务队列的一个例子是 Google 的 Appengine 任务队列。你可以在developers . Google . com/app engine/docs/python/task queue上阅读关于实现的更多信息。任务队列的其他例子有 Resque(【github.com/resque/resq… Ruby 应用的任务队列库;jesque(【github.com/gresrun/jes… Java 语言实现的 Resque 芹菜(www.celeryproject.org),我们将在本章中讨论。

理解任务队列是多个协同工作的组件的整体是非常重要的:主进程,它通常结合多个子系统,例如任务执行调度器;消息队列,用于通信目的;和实现任务执行算法的工作进程。

消息队列系统

任务队列中的核心组件之一是消息队列。消息队列是一种在进程和系统之间共享信息的机制。任务队列使用消息队列在任务队列系统的不同组件之间进行通信。例如,当主进程需要向一个工作进程发送任务时,它使用消息队列来传递消息。在前面的手动任务队列示例中,白板扮演消息队列的角色。团队领导(主流程)使用白板(消息队列)向团队成员(工作流程)发送消息(在便利贴上手写的文本)。

有时您会发现消息队列系统被称为消息代理。有许多不同的消息队列;一些流行的例子是:

  • ActiveMQ(activemq.apache.org)
  • rabbitq(http://www . rabbitq . com
  • zeromq(??)} http://zerocq . org 的缩写形式的缩写形式为 zerocq(零、零、零、零、零、零、零、零、零、零、零、零、零、零、零、零、零、零、零、零、零、零、零、零、零、零

通常,消息队列和任务队列是非常松散耦合的。例如,Celery 可以使用以下消息代理之一:

如您所见,您并不局限于专用的消息队列;您可以从各种各样的专用工具和通用数据库中进行选择。

设置 Celery 服务器和客户端

在这一节中,我们看看如何安装和配置 Celery 及其所有要求。我们还会看到一些基本的任务队列使用模式。

安装和设置 RabbitMQ

我将使用推荐的消息队列 RabbitMQ,它是受支持平台中功能最全、最稳定的。除非您有非常好的理由使用不同的平台,否则也要在部署中使用 RabbitMQ。如果您正在构建大型系统,请小心,因为用许多不同的队列扩展 RabbitMQ 可能会有问题;您可能需要先做一些性能测试。

RabbitMQ 在大多数流行的 Linux 发行版上都可以作为一个包获得。如果您使用的是基于 RedHat 的系统,可以使用以下命令安装 RabbitMQ:

$ sudo yum install rabbitmq-server

安装好软件包后,启动它并检查它是否正常运行:

$ sudo systemctl start rabbitmq-server
$ sudo systemctl status rabbitmq-server
rabbitmq-server.service - RabbitMQ broker
   Loaded: loaded (/usr/lib/systemd/system/rabbitmq-server.service; disabled)
   Active: active (running) since Sun 2014-07-20 12:30:22 BST; 25s ago
  Process: 304 ExecStartPost=/usr/lib/rabbitmq/bin/rabbitmqctl wait /var/run/rabbitmq/pid (code=exited, status=0/SUCCESS)
  Process: 32746 ExecStartPre=/bin/sh -c /usr/lib/rabbitmq/bin/rabbitmqctl status > /dev/null 2>&1 (code=exited, status=2)
 Main PID: 303 (beam)
   CGroup: /system.slice/rabbitmq-server.service
           ├─303 /usr/lib64/erlang/erts-5.10.4/bin/beam -W w -K true -A30 -P 1048576 -- -root /usr/lib64/erlang -progname erl -- -home /var/lib/rabbitmq -- -pa /usr/lib/rabbitmq/lib/rabbitmq_server-3.1.5/sbin/../ebin -noshell -noinput -s rabbit boot -sname rabbit@fedora -boot start...
           ├─334 /usr/lib64/erlang/erts-5.10.4/bin/epmd -daemon
           ├─403 inet_gethost 4
           └─404 inet_gethost 4

Jul 20 12:30:19 fedora.local rabbitmq-server[303]: RabbitMQ 3.1.5\. Copyright (C) 2007-2013 GoPivotal, Inc.
Jul 20 12:30:19 fedora.local rabbitmq-server[303]: ##  ##  Licensed under the MPL.  See http://www.rabbitmq.com/
Jul 20 12:30:19 fedora.local rabbitmq-server[303]: ##  ##
Jul 20 12:30:19 fedora.local rabbitmq-server[303]: ##########  Logs: /var/log/rabbitmq/rabbit@fedora.log
Jul 20 12:30:19 fedora.local rabbitmq-server[303]: ######  ##  /var/log/rabbitmq/rabbit@fedora-sasl.log
Jul 20 12:30:19 fedora.local rabbitmq-server[303]: ##########
Jul 20 12:30:22 fedora.local rabbitmq-server[303]: Starting broker... completed with 0 plugins.
Jul 20 12:30:22 fedora.local rabbitmqctl[304]: ...done.
Jul 20 12:30:22 fedora.local systemd[1]: Started RabbitMQ broker.
Jul 20 12:30:46 fedora.local systemd[1]: Started RabbitMQ broker.

如果您没有看到任何错误消息,这意味着 RabbitMQ 已经成功安装并启动。真的就这么简单!如果您需要对服务器配置进行任何更改(例如,更改服务器绑定的端口),您可以创建一个名为/etc/rabbitmq/rabbitmq.conf 的配置文件。有关配置参数的更多详细信息,可以在位于www.rabbitmq.com/configure.html的 rabbitmq 官方文档中找到。

安装和设置芹菜

一旦 RabbitMQ 服务器安装在主节点和工作节点上,就可以继续进行 Celery 的安装和配置了。请记住,主服务器和辅助服务器不一定需要位于不同的主机上;出于测试目的,两者都可以设置在同一台主机上。

大多数流行的 Linux 发行版上都有 Celery 软件包。在基于 RedHat 的系统上,您可以通过运行以下命令来安装 Celery:

$ sudo yum install python-celery

在基于 Debian 的系统上,包名是相同的:

$ sudo apt-get install python-celery

但是,我建议从 PyPI 存储库中安装 Celery 包,因为它将包含最新的稳定版本:

$ sudo pip install celery

创建芹菜系统用户和组

首先,您应该确保在所有工作节点上自动启动 Celery 进程。这不是必需的,您可以在每次需要时手动启动 Celery 进程,但是这种方法不可伸缩。以下示例假设您正在使用基于 RedHat 的系统,并且 systemd 控制系统服务。如果您使用的是不同的 Linux 发行版,那么您必须调整示例。

出于安全原因,不建议您以 root 用户身份运行 Celery。更好的方法是创建一个专用的用户和组,并在这些凭证下运行 Celery 守护程序。

要创建新用户并检查其 UID 和 GID,请运行以下命令:

# useradd --system -s /sbin/nologin celery
# id celery
uid=987(celery) gid=984(celery) groups=984(celery)

创建芹菜项目目录和示例应用

所有 Celery 应用必须位于一个项目目录中,该目录可以按如下方式创建:

# mkdir /opt/celery_project
# chown celery:celery /opt/celery_project

您还需要一个示例应用来测试您的配置。我们将在本章的后面查看应用开发的具体细节,但现在让我们创建一个名为/opt/celery_project/tasks.py 的文件,其内容如下:

from celery import Celery

app = Celery('tasks', broker='amqp://guest@localhost//', backend='amqp')

@app.task
def hello(name='Anonymous'):
    return "Hello, %s" % name

Image 注意如果你用的是比 3.1 更早的芹菜版本,用‘芹菜’对象名代替‘app’;早期版本希望找到这个特定的名称。在 3.1 及更高版本中,名称不再重要。

创建所需的系统目录

需要两个目录:一个用于存储日志文件,另一个用于存储临时 PID 文件。如果使用以下内容创建配置文件/usr/lib/tmpfile.d/celery.conf,systemd 进程会自动创建这些目录:

d /run/celery 0755 celery celery -
d /var/log/celery 0755 celery celery -

创建系统配置文件

systemd 进程管理守护进程需要一个名为/var/lib/systemd/system/celery . service 的系统定义文件,其内容如下:

[Unit]
Description=Celery workers
After=network.target

[Service]
Type=forking
User=celery
Group=celery
EnvironmentFile=-/etc/conf.d/celery
WorkingDirectory="${CELERYD_CHDIR}"
ExecStart=/bin/celery multi start –A "${CELERY_APP}" "${CELERYD_NODES}" \
          --pidfile="${CELERYD_PID_FILE}" \
          --logfile="${CELERYD_LOG_FILE}" --loglevel="${CELERYD_LOG_LEVEL}"
ExecStop=/bin/celery multi stopwait –A "${CELERY_APP}" "${CELERYD_NODES}" \
          --pidfile="${CELERYD_PID_FILE}"
ExecReload=/bin/celery multi restart –A "${CELERY_APP}" "${CELERYD_NODES}" \
           --pidfile="${CELERYD_PID_FILE}" \
           --logfile="${CELERYD_LOG_FILE}" --loglevel="${CELERYD_LOG_LEVEL}"

[Install]
WantedBy=multi-user.target

它需要名为/etc/conf.d/celery 的环境配置文件,包含以下内容:

CELERY_APP="tasks"
CELERYD_NODES="worker"
CELERY_BIN="/bin/celery"
CELERYD_PID_FILE="/run/celery/%n.pid"
CELERYD_LOG_FILE="/var/log/celery/%n.log"
CELERYD_LOG_LEVEL="DEBUG"
CELERYD_USER="celery"
CELERYD_GROUP="celery"

创建这些文件后,您可以使用以下命令启用并启动该服务:

# systemctl enable celery
# systemctl start celery

如果一切正常,您应该在日志文件/var/log/celery/celery.log 中看到以下输出:

[2014-07-20 19:00:52,594: WARNING/MainProcess] /usr/lib/python2.7/site-packages/celery/apps/worker.py:161: CDeprecationWarning:
Starting from version 3.2 Celery will refuse to accept pickle by default.

The pickle serializer is a security concern as it may give attackers
the ability to execute any command.  It's important to secure
your broker from unauthorized access when using pickle, so we think
that enabling pickle should require a deliberate action and not be
the default choice.

If you depend on pickle then you should set a setting to disable this
warning and to be sure that everything will continue working
when you upgrade to Celery 3.2::

    CELERY_ACCEPT_CONTENT = ['pickle', 'json', 'msgpack', 'yaml']

You must only enable the serializers that you will actually use.

  warnings.warn(CDeprecationWarning(W_PICKLE_DEPRECATED))
[2014-07-20 19:00:52,612: INFO/MainProcess] Connected to amqp://guest:**@127.0.0.1:5672//
[2014-07-20 19:00:52,620: INFO/MainProcess] mingle: searching for neighbors
[2014-07-20 19:00:53,628: INFO/MainProcess] mingle: all alone
[2014-07-20 19:00:53,638: WARNING/MainProcess] worker@fedora.local ready.

有一个警告消息,我们将很快解决,但除此之外,启动过程看起来很好。您还可以使用命令行工具来检查系统的运行状况:

# celery status
worker@fedora.local: OK

1 node online.
# celery inspect ping
-> worker@fedora.local: OK
        pong

测试对芹菜服务器的访问

在我们继续之前,让我们通过从一个简单的 Python 应用连接到 Celery 服务器来确保一切正常。您将导入我们之前编写的应用代码,因此请确保在/opt/celery_project/目录中运行以下命令:

# cd /opt/celery_project/
# python
Python 2.7.5 (default, Feb 19 2014, 13:47:28)
[GCC 4.8.2 20131212 (Red Hat 4.8.2-7)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from tasks import hello
>>> result = hello.delay('World')
>>> result
<AsyncResult: 926aabc8-6b1b-424e-be06-b15bcf92137e>
>>> result.id
'926aabc8-6b1b-424e-be06-b15bcf92137e'
>>> result.ready()
True
>>> result.result
'Hello, World'
>>> result.status
'SUCCESS'
>>>

芹菜基础知识

在这一节中,我们来看看芹菜的基本用法。

典型芹菜应用的布局

事实上,每个基于 Celery 的应用都是一个至少由两个组件组成的系统:一个生成工作并消耗结果的主进程,以及一个执行工作请求的工作进程。通常情况下,您会有多个工作进程,但您至少需要一个。正如您在前面发现的,这些进程之间的通信是通过消息队列完成的,因此这些进程不需要驻留在同一个物理操作系统上。

理解主进程和工作进程都需要访问正在执行的代码是很重要的。例如,如果您编写一个 web 邮件处理系统,您将编写一个处理发送邮件的库。相同的库需要在主机器和所有工作机器上可用。只有工作机器将执行代码,但是主机器需要能够检查代码,以便它能够通过发送任务名称和正确的参数来适当地构造工作请求。

这些模块的分发完全由您决定。芹菜不提供这样的功能。您可以使用 Celery 来分发这些模块,就像我们对定制的 XML-RPC 系统所做的那样,但是通常这是不可取的。如果你想自动化一个模块部署过程,最好看看配置管理工具,比如 ansi ble(www.ansible.com/)、salt stack(【www.saltstack.com)、Puppet()或者 Chef(www.getchef.com)。

创建任务模块

在上一节中,当我们对 Celery 设置进行基本测试时,我们创建了一个名为 tasks.py 的文件,其中包含一个方法。当您编写更大的应用时,您将有不止一个任务方法可供主流程使用。您可以将所有任务保存在一个文件中,但是该文件可能很快变得无法管理。因此,建议创建一个专用模块来封装所有任务。

工作进程和主进程应用文件

首先,删除前面创建的 tasks.py 文件,然后在/opt/celery_project 中创建以下目录结构。现在创建空文件,因为您将在本例中填充它们。这里,您创建了一个简单的 Celery 模块,它有两组任务——一组用于算术运算,一组用于几何运算:

# pwd
/opt/celery_project
# tree
.
├── calculator.py
└── celery_app
    ├── celeryconfig.py
    ├── celery.py
    ├── __init__.py
    └── tasks
        ├── arithmetics.py
        ├── geometry.py
        └── __init__.py

2 directories, 7 files

让我们讨论一下如何使用每个文件和目录:

  • 第一个文件 calculator.py 是实际运行的应用,它提交任务进行处理。这是主流程。
  • 名为 celery_app/的目录是一个 Python 模块,它将包含与 celery 后台任务处理相关的所有文件。
  • Celery.py 是 Celery 应用的主文件,它初始化 Celery 应用并设置其配置。
  • Celeryconfig.py 是一个配置文件。是芹菜. py 进口使用的。
  • init。py 是一个空文件,它的唯一目的是表明这个目录是一个 Python 模块。
  • 子目录 tasks/是包含按公共属性分组的特定子模块的子模块,例如,所有算术运算都放在 arithmetics.py 子模块中。

这可能看起来像是一个不必要的复杂布局,但实际上它并不难设置,如果您的应用变得更大,它会提供很大的灵活性。

芹菜配置文件概述

通常,Celery 应用不需要太多的配置。您必须告诉它在哪里寻找新的作业(这通常是运行在相同主机上的消息队列),在哪里存储结果(通常是相同的消息队列),并且可能提供一些特定于环境的设置。管理 Celery 配置文件最简单的方法是将所有配置项放在一个单独的文件中,并从主应用导入它。因此,在我们的例子中,配置文件名为 celeryconfig.py,它包含清单 12-1 中的设置。

清单 12-1 。芹菜配置设置

CELERY_TASK_SERIALIZER = 'json'         # Only allow object serialization using JSON
CELERY_RESULT_SERIALIZER = 'json'       # Previously the default was Python pickle objects,
CELERY_ACCEPT_CONTENT = [ 'json', ]     #   but they are not secure, and will be discontinued
BROKER_URL = 'amqp://guest@localhost//' # Where to look for new jobs
CELERY_RESULT_BACKEND ='amqp'           # Where to send job results
CELERY_IMPORTS = ('celery_app.tasks.geometry',     # Modules that contain Celery tasks
                  'celery_app.tasks.arithmetics',) #
CELERY_TASK_RESULT_EXPIRES=3600         # How long keep tasks results before purging them

你可以在官方的芹菜文档网页上找到配置选项的完整列表,docs.celeryproject.org/en/latest/configuration.html;然而,最常用的项目在表 12-1 中列出。

表 12-1 。一些最常用的芹菜配置物品

|

配置项目

|

描述

| | --- | --- | | 芹菜 _ 时区 | 默认情况下,芹菜采用 UTC 时区;如果需要在邮件时间戳中设置特定于位置的时区,可以修改此设置。要获得最新的时区名称列表,请查看en.wikipedia.org/wiki/List_of_tz_database_time_zones。 | | CELERYD _ 并发 | 该设置允许您指定允许 Celery workers 运行多少个并发进程或线程。默认情况下,该设置被设置为可用 CPU 的数量;然而,这是非常保守的。除非您正在进行大量的计算,并且这些进程确实受到 CPU 的限制,否则您至少应该将这个数字增加一倍。如果您的进程主要是 I/O 绑定的,那么您通常可以比 CPU 的数量多 5 到 10 倍。 | | 芹菜 _ 结果 _ 后端 | 默认情况下,Celery 不使用任何后端来存储任务结果。在大多数情况下,这可能是可以接受的行为。例如,如果您正在运行发送电子邮件的后台任务,您可能希望工作进程更新数据库中指示任务状态等的多个表。您可以将主流程视为分派任务的调度程序,并不真正关心任务发生了什么。更新系统取决于任务流程。这是一种有效的方法,尤其是当工作进程需要更新运行系统的许多方面时。然而,另一种方法是由主进程处理数据。在这种情况下,主进程向长时间运行的工作进程发出指令,然后获取结果,或者自己更新系统状态,或者将任务交给另一个工作进程。您选择哪个选项取决于系统和您的偏好。我的建议是将任务明确分为两类:一类是与外部系统(邮件服务器、文件服务器、web 服务器等)交互的任务。)和另一个处理内部系统状态(更新内部数据库、增加计数器等)。).所选择的后端系统不需要与用于通信的平台相同。因此,您可以使用 RabbitMQ(设置值为' amqp ')进行消息排队,并将结果存储在 redis 数据库中(设置值为' Redis ')。 | | 芹菜 _ 结果 _ 序列化程序 | 当结果由工作进程生成时,它们需要存储在您使用以前的设置选择的任何后端媒体中。默认是 Python 的 pickle 序列化方法。简而言之,pickle 使用字节码序列化对象,然后在接收端评估这个字节码,而不首先验证它的安全性。因此,如果有人设法发送恶意数据,假装来自工作进程,您的主进程可能会执行收到的代码;这可以用来侵入你的系统。因此,默认值将会改变,如果不指定不同的序列化方法,Celery 会在启动时警告您。最方便和安全的选择之一是使用 JSON 数据结构。 | | 芹菜 _ 接受 _ 内容 | 此设置是允许的序列化程序列表。如您所知,主进程为远程执行准备数据,并将其发送给工作进程。为此,主进程使用由 CELERY_TASK_SERIALIZER 设置指定的 serialize 来序列化数据。当 worker 节点完成处理数据时,结果(如果有)在被发回之前被序列化。如何序列化由 CELERY_RESULT_SERIALIZER 设置定义。CELERY_ACCEPT_CONTENT 没有说如何序列化任务参数或者结果;它只列出允许的序列化程序。这允许您让一些 worker 节点在 JSON 中生成结果,让一些 worker 节点在 YAML 中生成结果;如果您在这里列出这两种方法,它们将被接受。 | | 芹菜 _ 任务 _ 结果 _ 过期 | 如果您存储结果,该设置会告诉 Celery 在删除之前要保存多长时间(以秒为单位)。默认设置是保留一天的所有结果;如果将其设置为零,则不会删除结果。 | | 芹菜 _ 任务 _ 序列化程序 | 类似于 CELERY_RESULT_SERIALIZER,但是该设置指示在向远程工作进程发送数据时使用何种序列化方法。 | | 芹菜 _ 进口 | 这是芹菜工启动时需要导入的模块列表。Celery worker 将搜索 Celery 任务兼容函数(由 Celery.task decorator 修饰)。 |

主芹菜申请文件

该文件用于初始化 Celery 应用,并从配置模块加载配置设置。文件的内容非常简单明了,如清单 12-2 所示。

清单 12-2 。主芹菜申请文件

from __future__ import absolute_import

from celery import Celery
from celery_app import celeryconfig

app = Celery()
app.config_from_object(celeryconfig)

if __name__ == '__main__':
    app.start()

您可能想知道第一个 import 语句是关于什么的。这是必要的,因为我们将我们的模块命名为 celery.py,但是还有一个系统范围的包具有相同的名称。因此,当 Python 解释器看到您想要“导入芹菜”时,它会感到困惑你要本地文件还是要官方包?为了解决这种不确定性,您告诉 Python 解释器,每当出现名称冲突时,优先考虑通过 sys.path 提供的模块。这允许您为您的 Celery 应用取一个方便的名称,并且仍然导入正式的包。

芹菜任务

正如您已经从目录结构中看到的,我们将 Celery 将要管理的所有后台任务移到了一个单独的子模块目录中。在那个模块中,我们有两个文件用于不同的任务集,算术运算,如清单 12-3 所示。

清单 12-3 。算术运算任务文件

from __future__ import absolute_import

from celery_app.celery import app

@app.task
def add(a, b):
    return a + b

@app.task
def sub(a, b):
    return a - b

和几何运算,如清单 12-4 所示。

清单 12-4 。几何运算任务文件

from __future__ import absolute_import

from celery_app.celery import app

@app.task
def rect_area(h, w):
    return h * w

@app.task
def circle_area(r):
    import math
    return math.pi * r

系统配置

我们需要调整 systemd 配置文件,以便它们与我们当前的项目布局相匹配。您修改了/etc/conf.d/celery 文件,使其指向您的新任务模块:

CELERY_APP="celery_app.celery"

现在,当您使用以下命令重新启动 Celery 守护程序时,它应该会获得新的 Celery 应用文件:

# systemctl restart celery

芹菜主应用

最后,创建 Celery master 应用,并测试 Celery 进程是否正在运行,以及任务是否可用。简单的测试应用代码在 calculator.py 文件中:

#!/usr/bin/env python

from celery_app import tasks

def test_tasks():
    print 'Submitting job...'
    r = tasks.geometry.rect_area.delay(2, 2)
    print r.info
    print 'Job completed'

if __name__ == '__main__':
    test_tasks()

如果运行它,您应该会看到以下结果:

# ./calculator.py
Submitting job...
4
Job completed
#

路由任务

消息队列系统提供的关键特性之一是路由发送到队列的消息的能力。由于任务队列系统(如 Celery)通常基于消息队列系统(如 RabbitMQ ),所以它们继承了相同的功能。

在较小和较简单的系统中,单个队列通常就足够了,但是对于较大的系统,您需要能够将任务分组到特定的工作人员组,这就是队列的设计目的。

在消息队列系统内部

图 12-2 是一个基于 AMQP 的典型消息队列系统如何工作的高级概述。芹菜隐藏了大部分的复杂性,但是在考虑消息路由之前,至少有一个总体的概念是很好的。

9781484202180_Fig12-02.jpg

图 12-2 。典型的消息队列系统架构

图 12-2 展示了消息分发机制中的主要参与者。

这是一个简化的工作流程:

  • 您的应用(生产者)调用一个后台任务(在我们的示例应用中,它是:tasks . geometry . rect _ area . delay(2,2)。
  • 任务细节,如名称、参数和目标队列(如果指定的话),被序列化并提交给交换。
  • 然后,作为 RabbitMQ 一部分的 Exchange 将邮件转发到一个可用的队列。该标准定义了四种不同类型的交换类型:直接(如果匹配消息的路由关键字,则将任务发送到一个队列)、扇出(将任务发送到绑定到它的所有队列)、主题(将消息发送到具有匹配路由关键字模式的所有队列)和头(基于消息头分发消息)。路由关键字是一个标记,每个消息都可以用它来标记。当队列被绑定到交换机时,它们也会被分配一个路由关键字(或路由关键字模式)。这允许 exchange 相应地路由邮件。
  • 消息到达消费者进程(工作进程),在那里消息被处理并从队列中删除。

如果你对《AMQP 议定书》的细节感兴趣,你可以在官方的 AMQP 车型描述页面找到更多信息:www.openamq.org/tutorial:the-amq-model

深入讨论消息队列系统超出了本书的范围,尤其是这个主题如此广泛。如果你对消息队列系统感兴趣,我推荐 Alvaro Videla 和 Jason J. W. Williams 的rabbit MQ in Action:Distributed Messaging for every one

将工作节点绑定到特定队列

对于基本的应用,您需要做两件事来有效地使用队列:首先,您需要指示 worker 节点绑定到特定的队列;然后,您需要标记任务,以便它们被正确地路由。

您在启动时将 worker 节点绑定到特定的队列。默认情况下,所有未标记的任务都被发送到名为(标记为)“celery”的默认队列中如果未指定任何要绑定到的队列,则工作进程将自动绑定到该队列。让我们创建一个新队列,并将其命名为“calc ”,以便所有与计算相关的任务都只发送给绑定到该队列的工作线程。

首先,您需要向/etc/conf.d/celery 添加新的设置:

CELERY_QUEUES="calc"

然后,确保当芹菜守护进程启动时,它使用这个参数。您需要修改系统服务定义文件/usr/lib/systemd/system/celery . service:

ExecStart=/bin/celery multi start "${CELERYD_NODES}" -A "${CELERY_APP}" -Q "${CELERY_QUEUES}" --pidfile="${CELERYD_PID_FILE}" --logfile="${CELERYD_LOG_FILE}" --loglevel="${CELERYD_LOG_LEVEL}"

如果您现在重新启动 Celery 进程,您将看到该进程只绑定到新队列:

# ps auxww | grep celery
celery    4760  0.7  1.0 246404 21472 ?        S    14:27   0:00 /usr/bin/python -m celery worker -n worker@fedora.local -A celery_app.celery --loglevel=INFO -Q calc --logfile=/var/log/celery/worker.log --pidfile=/run/celery/worker.pid
celery    4773  0.0  0.8 245600 17680 ?        S    14:27   0:00 /usr/bin/python -m celery worker -n worker@fedora.local -A celery_app.celery --loglevel=INFO -Q calc --logfile=/var/log/celery/worker.log --pidfile=/run/celery/worker.pid

那么,既然 worker 不再绑定到默认队列,那么如果您运行 calculator.py 应用 会发生什么情况呢?记住,默认情况下任务是没有标记的,因此它们都进入默认的“芹菜”队列,但是现在没有任何东西监听它。让我们试着运行几次:

# ./calculator.py
Submitting job...
None
Job completed
#
# ./calculator.py
Submitting job...
None
Job completed
#

没什么好惊讶的,是吧?没有工人在“芹菜”队列上工作,所以任务没有被处理。但是提交的任务实际上发生了什么呢?需要使用 rabbitmqctl 命令直接询问 rabbitmqctl】

# rabbitmqctl list_queues
Listing queues ...
1159cf27f68247da9885495e63c7dd1c        0
calc    0
celery  2
celeryev.601d558c-6354-4265-9704-a225948bb052   0
e289f4c20f754489944f75e1ee7c8ac6        0
worker@fedora.local.celery.pidbox       0
...done.

您可以看到有两个队列,一个名为“celery”,一个名为“calc”“calc”队列中没有消息,但是“celery”队列中有两条消息。

为了确保你的请求不会被发送到黑洞,你需要给它们做标记。这就像指定要将任务发送到的队列名称一样简单。您修改了 calculator.py 文件,以便任务调用在其中有一个队列名称(不幸的是,我们不能使用“延迟”快捷方式):

r = tasks.geometry.rect_area.apply_async((2, 2), queue="calc")

如果您再次运行 calculator.py,您将会看到任务现在已经得到处理,正如预期的那样:

# ./calculator.py
Submitting job...
4
Job completed
#

指定队列的另一种方法是在 Celery 应用文件中(在我们的示例中,这是 celeryconfig.py):

from kombu import Queue
CELERY_QUEUES = ( Queue("calc"), )

这样,您可以在所有处理机器上保持相同的 systemd 配置文件,即使它们绑定到不同的队列。

发送广播消息

默认的消息队列行为是一条消息只能到达一个收件人。这对于发送电子邮件(您希望只发送一封电子邮件!)或执行计算(不需要在所有可用的服务器上计算相同的东西)。但是,有时您需要向所有可用的服务器发送消息,监控系统就是一个例子。您希望告诉所有服务器运行它们的检查并相应地更新状态。

为了实现这个目标,芹菜有一个发送广播消息的机制。这意味着发送到队列的消息将被路由到所有监听该队列的工作线程。

同样,这可以在 **celeryconfig.py 文件中定义;**在示例中,您定义了两个队列,一个用于“正常”计算,一个用于“广播”计算:

from kombu import Queue
from kombu.common import Broadcast

CELERY_QUEUES = ( Queue("calc"),
                  Broadcast("broadcast_calc"), )

还要修改 calculator.py 应用,以便将任务提交到两个不同的队列:

def test_tasks():
    print "Submitting job..."
    r = tasks.geometry.rect_area.apply_async((2, 2), queue="calc")
    print r.info
    print "Job completed"
    print "Submitting broadcast job..."
    r = tasks.arithmetics.add.apply_async((1, 1), queue="broadcast_calc")
    print r.info
    print "Job completed"

如果再次运行示例代码,应该会得到两个结果:

# ./calculator.py
Submitting job...
4
Job completed
Submitting broadcast job...
2
Job completed

好的,这是我们所期望的,但是让我们想一想:如果您提交一个任务,那么队列中只有一个任务,但是它被转发给所有的工人。当工人回复他们的结果时,他们会发送一条消息,用简单的英语可以翻译成这样:“ID 为 A 的任务的结果是 XYZ。”如果您有多个任务 ID,这很好,因为您可以将任务结果与任务 ID 号相关联,但是如果您广播,那么将会有多个结果,但是只有一个任务 ID!

没有简单的方法来解决这个问题,除了忽略提交方(主进程)的结果,并确保工作进程在某个中心位置提交它们的结果——例如,在共享数据库上。

摘要

在本章中,我们简要地讨论了任务和消息队列系统。您可以使用这些知识来重写我们在前三章中编写的分布式监控应用。

  • 任务队列系统用于将任务分配给工作节点,以便它们可以在后台处理。
  • 任务队列系统通常使用底层消息队列系统在工作节点之间分发消息。
  • Celery 结合 RabbitMQ 可以用来调用远程 Python 函数。
  • 任务可以被路由到专用队列,工作进程可以监听一组预定义的队列。这允许您拥有专门化的工作进程,并根据这种专门化对它们进行分组。
  • 可以创建广播队列,让所有用户都能收到相同的消息。

十三、MySQL 数据库自动性能调优

在本章中,我们将扩展我们在第六章中构建的插件框架。您可能还记得,插件框架允许我们通过在主应用代码之外实现新方法来扩展应用的功能。新的框架将允许插件生成数据并将其提交回应用,因此其他插件也能够使用它。基于新的框架,我们将构建一个应用来检查 MySQL 数据库配置和实时统计数据,并提出性能调整建议。我们将查看一些调优参数,并编写一些插件。

需求规格和设计

作为一名系统管理员,您可能被要求提高 MySQL 数据库服务器的性能。这是一项富有创造性和挑战性的任务,但同时也可能令人望而生畏。数据库软件本身是一个复杂的软件,您还必须考虑外部因素,如运行环境 CPU 内核的数量和内存量。除此之外,实际的表布局和 SQL 语句结构扮演着非常重要的角色。

对于如何处理这个问题,你可能已经有了自己的策略。我提到“您自己的策略”的原因是,不幸的是,没有调优 MySQL 数据库的通用解决方案。每个安装都是独特的,需要单独的方法。有各种解决方案可以帮助您确定数据库中最常见的问题,包括商业选项,如 MySQL Enterprise Monitor(mysql.com/products/enterprise/monitor.html)和开源工具,如 MySQL tuner(blog.mysqltuner.com/)。这些工具的主要目的是通过提供对系统配置和行为的深入了解来自动化调优过程。

假设 SQL 语句调优是软件开发人员的工作,作为系统管理员,您实际上是在处理两个参数:数据库配置和操作环境配置。反馈以内部数据库计数器的形式提供,例如慢速查询的数量或连接的数量。

从这个角度来看,MySQL 社区服务器 5.6.19 有 443 个状态变量和 602 个配置变量。我甚至不考虑列出操作环境变量,因为这几乎是不可能的。人类不可能将所有变量联系起来,并在更大范围内进行有意义的观察。

可用的工具试图检查配置,并根据观察到的状态变量,提出一些如何改进配置的建议。这对于基本的调优很有效,但是随着您的深入研究,您可能会发现您需要修改该工具,以便根据您的需要进行调优,而不是基于一些一般性的观察。这就是你需要一个可扩展且易于调整的工具的地方。

基本应用要求

在第六章中,我们讨论了基于插件的架构的优势。在这种架构中,主(主机)应用向插件提供一些通用服务,这些插件或者扩展主应用的功能,或者实际上提供服务。从用户的角度来看,系统就像一个实体。

这就把我们带到了本章将要构建的应用的基本需求列表:

  • 应用应该易于扩展、修改和增加新功能。
  • 应用应该关注从 MySQL 数据库收集和处理性能观察。
  • 性能调整规则应该易于在应用的不同实例之间转移和交换。

系统设计

作为应用的基础,我们将使用我们在第六章中创建的插件框架。我们可以照原样使用,用 MySQL 数据收集函数替换日志行读取部分,并开始编写消耗数据的插件模块。这种方法在短期内会很好地为我们服务,但从长远来看,它可能不是最具可扩展性的解决方案。问题是,尽管我们可以立即识别 MySQL 配置参数和状态变量,但我们会与操作系统状态参数发生冲突。这是因为这一信息没有明确的来源。每个系统都不同,可能需要不同的工具来报告状态。

这个问题的解决方案是将产生信息的任务从主机应用转移到插件模块。换句话说,一些插件将产生数据,其他插件将依赖这些数据进行计算,并最终提出性能改进建议。在这种情况下,主机应用仅充当调度程序,它提供的唯一服务是到数据库服务器的连接。其余的功能由插件提供。图 13-1 显示了生产者/消费者插件架构的示意图。

9781484202180_Fig13-01.jpg

图 13-1 。生产者/消费者插件框架

正如您所看到的,主机应用仍然通过插件管理器对象发出命令。结果也通过插件管理器传递回来,但是为了清楚起见,图中显示了返回到主机应用的直接链接。一旦从生产者插件模块中收集到数据,它就会被传递回消费者模块。因此,主机应用负责向插件提供连接细节,并维护生产者优先、消费者最后调用的正确顺序。

除了插件框架的这些变化,我们还将提供三个基本的生产者插件:

  • 提供 MySQL 系统变量的插件
  • 提供配置细节的插件
  • 一个插件,提供系统上可用的物理和虚拟内存的详细信息,以及 CPU 核心的数量

这将是我们构建顾问插件的基本信息集。advisor 插件将根据收到的结果执行一些计算,并提供如何提高服务器性能的建议。

Image 注意 MySQL 调优是一个非常宽泛的话题。如果您想了解更多,我推荐您从 MySQL 性能博客(mysqlperformanceblog.com/)开始,它包含了大量的性能调优技巧和文章。其他有用的资源有dev . MySQL . com/doc/ref man/5.7/en/server-parameters . htmlwww.mysql.com/why-mysql/performance/

修改插件框架

不同组件之间的信息共享会很快变得复杂。以下是您可能需要解决的一些潜在问题:

  • 哪些插件可以访问哪些信息?您可能想要隐藏某些插件的某些信息。
  • 生产者插件也是消费者怎么办?一些插件可能需要其他插件产生的信息来完成它们的任务。
  • 如何在插件之间共享大量数据?例如,当产生的数据量不适合物理内存而需要存储在磁盘上时。

为了简单起见,我们将有一个平面访问模型,其中消费者模块可以访问生产者插件生成的所有信息。我们不会实现分层生产者布局,我们将假设生产者是自给自足的。

对宿主应用的更改

宿主应用的职责限于以下三个任务:

  • 从配置文件中读取 MySQL 数据库凭证
  • 建立与服务器的初始连接
  • 分三个阶段运行插件模块:运行生产者并收集数据,运行生产者的过程方法,然后运行生产者的报告模块

我们将使用 Python 的 ConfigParser 库从 Windows INI 风格的配置文件中访问配置,该配置文件包含以下内容(显然,您需要调整设置以匹配您的数据库细节):

[main]
user=root
passwd=password
host=localhost

清单 13-1 显示了主机应用的完整列表。如您所见,代码很简单。它在逻辑上分为三个主要阶段和三个插件处理阶段。注意,我们使用关键字来区分生产者和消费者模块。

清单 13-1 。宿主应用

#!/usr/bin/env python

import re
import os, sys
from ConfigParser import SafeConfigParser
import MySQLdb
from plugin_manager import PluginManager

def main():
    cfg = SafeConfigParser()
    cfg.read('mysql_db.cfg')

    plugin_manager = PluginManager()
    connection = MySQLdb.connect(user=cfg.get('main', 'user'),
                                 passwd=cfg.get('main', 'passwd'),
                                 host=cfg.get('main', 'host'))

    env_vars = plugin_manager.call_method('generate', keywords=['provider'],
                                          args={'connection': connection})
    plugin_manager.call_method('process', keywords=['consumer'],
                              args={'connection': connection, 'env_vars': env_vars})
    plugin_manager.call_method('report')

if __name__ == '__main__':
    main()

如果您将这个清单与第六章中的例子进行比较,您会注意到这次我们实际上期望从 call_method 函数中得到一些东西。该函数返回生产者插件模块生成的结果,并将它们存储在一个变量中。然后,这个变量作为名为 env_vars 的关键字参数被传递给消费者插件。消费者插件希望这个参数存在。我们将在下一节研究这个变量的结构。

修改插件管理器

主机应用只处理对 call_method 函数的一次调用,因为它不知道——也不需要知道——插件的确切数量和名称。将请求路由到适当的插件模块是插件管理器的责任。然而,这种方法带来了一个问题:如果对一个函数的单次调用实际上产生了来自多个函数的多个答案,我们如何存储结果呢?

更复杂的是,我们不知道插件将返回什么。它可能是一个字典、一个列表,甚至是一个自定义对象。我们不需要知道这些。由消费者来解密这些信息。编写生产者插件的人应该提供关于他们的模块所产生的数据结构的大量文档。

在我们的例子中,插件管理器组件将以非常简单的方式处理结果。它会将它们作为单独的条目存储在字典中。字典键将是插件类名,键值将是插件模块调用返回的任何对象。然后,这个字典作为参数传递给消费者插件调用。这将产生一个平面信息存储,其中所有信息都可以被所有插件访问。这可能会带来一些安全问题,但是对于像我们在这里构建的这样一个简单的应用,简单性起着重要的作用。

对插件管理器代码的唯一修改是 call_method()函数,如清单 13-2 所示。

清单 13-2 。插件管理器方法调度程序函数

def call_method(self, method, args={}, keywords=[]):
    result = {}
    for plugin in self.plugins:
        if not keywords or (set(keywords) & set(self.plugins[plugin])):
            try:
                name_space = plugin.__class__.__name__
                result[name_space] = getattr(plugin, method)(**args)
            except AttributeError:
                pass
    return result

我们现在有了一个插件框架,它能够在模块之间传递信息。

如果你真的需要多级生产者架构,仅仅是几个级别,你可以使用关键字来实现它。例如,您可能有关键字生产者 1、生产者 2 和生产者 3。然后,您可以调用 generate()方法三次,每次传递不同的关键字,并将中间结果提供给 producer2 和 producer3 实例。

编写生成器插件

我们需要为顾问插件生成一些数据。我们将从查询 MySQL 内部状态和配置表开始。首先,让我们看看如何从 Python 应用访问 MySQL 数据库。

从 Python 应用访问 MySQL 数据库

MySQL 数据库的支持是由 MySQLdb Python 模块提供的,该模块是大多数 Linux 发行版上的预构建包。例如,在 Fedora 系统上,可以用下面的命令安装这个模块:

$ sudo yum install MySQL-python

或者,您可以从项目主页的sourceforge.net/projects/mysql-python/下载最新的源代码包,并从源代码构建这个库。请记住,MySQLdb 使用用 C 编写的模块,因此您还需要安装一个 C 编译器(通常称为 gcc 的包)、MySQL 开发头(通常包名为 mysql-devel)和 Python 开发头(通常包名为 python-devel)。

一旦安装了库,请检查它是否正确加载:

$ python
Python 2.6.2 (r262:71600, Jan 25 2010, 18:46:45)
[GCC 4.4.2 20091222 (Red Hat 4.4.2-20)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import MySQLdb
>>> MySQLdb.__version__
'1.2.3c1'
>>>

MySQLdb 库与 Python DB-API 规范版本 2 兼容。该规范定义了兼容库必须实现的接口、对象、变量和错误处理规则。这是统一所有数据库访问模块接口的一种尝试。这种统一的好处是,作为开发人员,您不需要太担心数据库模块调用的细节,因为它们非常相似。您编写的用于连接 SQLite 3 的代码应该可以与 MySQL 数据库一起工作,而无需进行大的修改。这两个库之间的主要区别可能是 connect()方法,它用于连接数据库,因此对于您正在使用的数据库软件来说是非常特定的。

不管您使用哪个数据库模块,您将调用的第一个方法通常是 connect() 。此方法返回 connect 对象的一个实例,您将使用它来访问数据库。这些参数是特定于数据库的。因为我们在本章中讨论的是 MySQL 数据库,所以下面是如何建立到数据库服务器的连接:

>>> connection = MySQLdb.connect( host='localhost',
...                               user='root',
...                               passwd='password',
...                               db='test')
>>>

这四个参数——主机名、用户名、密码和数据库名——是您会发现自己大部分时间都在使用的参数。但是,MySQL 服务器也支持多个连接选项,您可能需要修改这些选项。表 13-1 列出了最重要的几个。有关参数的完整列表,请参考 MySQLdb 文档(【mysql-python.sourceforge.net/MySQLdb.htm…

表 13-1 。常用的 MySQL 连接选项

|

参数

|

描述

| | --- | --- | | 圣体 | 要连接的主机的名称—主机的完全限定域名或 IP 地址。 | | 用户 | 用于向数据库服务器进行身份验证的用户名。 | | 修改密码 | 您用于验证的密码。 | | 分贝 | 您正在连接的数据库的名称。如果省略,将不会选择默认数据库,您将需要使用 USE DATABASE SQL 命令来连接数据库。 | | 港口 | 运行 MySQL 服务器的端口号。默认值为 3306。 | | unix_socket | MySQL 服务器实例的 UNIX 套接字的位置。默认位置因发行版而异,但通常是/var/lib/mysql/mysql.sock。 | | 压缩 | 指示应该启用还是禁用协议压缩的标志。默认情况下,它是禁用的。 | | 连接超时 | 等待连接操作完成的秒数。如果未在指定的时间范围内完成,操作将引发错误。 | | 初始化命令 | 建立连接后,服务器必须立即执行的初始化命令。 | | 使用 unicode) | 如果此标志设置为 true,则 CHAR、VARCHAR 和 TEXT 字段将作为 Unicode 字符串返回。否则,返回的结果只是普通的字符串。不管该设置如何,您总是可以以 Unicode 格式写入数据库。 | | 字符集 | 连接字符集将被设置为指定为此参数值的字符集。 |

返回的 connect 对象实现了管理连接状态的四种基本方法。这些方法在表 13-2 中列出。

表 13-2 。连接对象方法

|

方法名称

|

描述

| | --- | --- | | 。关闭() | 关闭已建立的连接,该连接从调用此方法时起就不可用。从此连接派生的所有游标对象也将不可用。请记住,如果您没有先提交更改就关闭连接,所有事务或更改都将回滚。 | | 。提交() | 强制数据库引擎提交所有未完成的事务。 | | 。回滚() | 如果您使用的是支持事务的 MySQL 数据库引擎(如 InnoDB ),则回滚最后一个未提交的事务。 | | 。光标() | 返回一个 cursor 对象,您将使用它来执行 SQL 命令并读取结果。MySQL 数据库不支持游标,但是 MySQLdb 库提供了这个包装器对象,它模拟游标功能。 |

数据库中的实际工作是使用游标对象完成的。游标对象充当查询执行的上下文,更重要的是,充当数据提取操作的上下文。一个连接对象可以创建多个游标。任何游标所做的更改都会被其他游标立即看到,只要它们属于同一个连接。表 13-3 列出了最常用的光标方法。表中示例中使用的连接上下文创建如下:

>>> connection = MySQLdb.connect( host='localhost',
...                               user='root',
...                               passwd='password',
...                               db='zm' )
>>>
>>> cursor = connection.cursor()

表 13-3 。常用的数据库光标方法

|

方法

|

描述

|

例子

| | --- | --- | --- | | 。执行() | 准备并执行 SQL 查询。它接受两个参数:需要执行的 SQL 语句(必需的)和可选的参数列表。SQL 字符串中的变量仅使用%s 字符串指定。第二个可选参数必须是元组,即使它只是单个值。 | 以下两个查询在功能上是相同的:> > > cursor.execute("选择类型shift-enter.jpg来自 id=1 的区域预设”)1L> > > cursor.execute("选择类型shift-enter.jpg来自区域预设,其中 id=%s ",(1,))1L>>> | | 。executemany() | 类似于。execute()方法;接受选项列表并遍历它们。使用游标数据提取方法可以组合和访问结果。列表元素必须是元组,即使它们只包含一个值。 | 以下示例在一个命令中运行两个选择查询:>>> cursor.executemany("""SELECT type shift-enter.jpg从区域预设位置shift-enter.jpgid=%s,类型=%s " ", [ (1,'活动'),(2,'活动')])2L>>> | | .rowcount | 一个只读属性(不是方法),指示最后一个。execute()语句已生成。 |   | | 。费特乔内() | 返回结果集中的下一行。如果没有更多的数据可用。它将返回 None 对象。结果总是一个元组。元素的顺序与查询集指定的顺序相同。 | > > > cursor.execute("选择 id,shift-enter.jpg从 ZonePresets 键入”)6L>>游标. fetchone()(1L,“活跃”)[...]>>游标. fetchone()(6L,“活动”)>>游标. fetchone()>>> | | 。fetchall() | 以元组的形式返回查询返回的所有行。 | > > > cursor.execute("选择 id,shift-enter.jpg从 ZonePresets 键入”)6L> > > cursor.fetchall()((1L,“主动”),(2L,“主动”),shift-enter.jpg(3L,'主动'),(4L,'主动'),shift-enter.jpg(5L,'活动'),(6L,'活动'))>>> | | 。费奇曼尼() | 返回由其参数指定的行数。如果没有提供参数,则读取的行数取决于。数组大小设置。 | > > > cursor.execute("选择 id,键入shift-enter.jpg来自区域预设”)6L>>> cursor.fetchmany(2)((1L,“主动”),(2L,“主动”))>>> | | 。像素 | 一个读/写属性,它控制。fetchmany()方法必须返回。 | > > > cursor.execute("选择 id,键入shift-enter.jpg来自区域预设”)6L>>游标. arraysizeone>>游标. arraysize=3>>> cursor.fetchmany()((1L,“主动”),(2L,“主动”),shift-enter.jpg(3L,“活跃”))>>> |

查询配置变量

如果您想要检索服务器配置或系统状态变量,您实际上不需要连接到任何数据库。建立到数据库服务器的连接就足够了。

要获取 MySQL 变量,我们需要使用 MySQL SHOW 语句。它的语法类似于 SELECT 语句,允许使用 LIKE 和 where 修饰符来限制查询集。(记住有 287 个配置设置和 291 个状态变量!)

我们将从配置变量开始。这些变量表明服务器是如何配置的。有三种方法可以改变这些变量:

  • 使用命令行参数在服务器启动时设置它们。
  • 使用选项文件(通常是 my.cnf)在服务器启动时设置它们。
  • 在服务器运行时使用 MySQL SET 语句设置它们。

Image 你可以在dev . MySQL . com/doc/ref man/5.7/en/server-system-variables . html的 MySQL 官方文档中找到所有 MySQL 变量以及它们如何影响服务器功能的详细描述。

该命令的基本语法是 SHOW VARIABLES。该命令的默认行为是显示应用于当前会话的设置,等效于同一命令的扩展语法:SHOW LOCAL VARIABLES。如果您想知道哪些设置将应用于新连接,您需要使用 SHOW GLOBAL VARIABLES 命令。可以使用 LIKE 和 WHERE 子句进一步修改结果集,如下例所示:

>>> connection = MySQLdb.connect( host='localhost',
...                               user='root',
...                               passwd='password' )
>>> cursor = connection.cursor()
>>> cursor.execute("SHOW GLOBAL VARIABLES LIKE '%innodb%'")
37L
>>> forin cursor.fetchmany(10): print r
...
('have_innodb', 'YES')
('ignore_builtin_innodb', 'OFF')
('innodb_adaptive_hash_index', 'ON')
('innodb_additional_mem_pool_size', '1048576')
('innodb_autoextend_increment', '8')
('innodb_autoinc_lock_mode', '1')
('innodb_buffer_pool_size', '8388608')
('innodb_checksums', 'ON')
('innodb_commit_concurrency', '0')
('innodb_concurrency_tickets', '500')
>>>
>>> cursor.execute("SHOW GLOBAL VARIABLES WHERE variable_name LIKE '%innodb%'
 AND value > 0")
18L
>>> forin cursor.fetchmany(10): print r
...
('innodb_additional_mem_pool_size', '1048576')
('innodb_autoextend_increment', '8')
('innodb_autoinc_lock_mode', '1')
('innodb_buffer_pool_size', '8388608')
('innodb_concurrency_tickets', '500')
('innodb_fast_shutdown', '1')
('innodb_file_io_threads', '4')
('innodb_flush_log_at_trx_commit', '1')
('innodb_lock_wait_timeout', '50')
('innodb_log_buffer_size', '1048576')
>>>

Image 提示系统配置表的列名为变量名。您可以在 SHOW 命令中使用这些名称以及 LIKE 和 WHERE 语句。

让我们编写一个插件类,它从数据库中检索所有变量,并将数据返回给插件管理器。如你所知,默认情况下,结果是一个元组的元组。为了使它更有用,我们将把它转换成 dictionary 对象,其中变量名是字典键,变量值是字典值,如清单 13-3 所示。

清单 13-3 。用于检索 MySQL 服务器变量的插件

class ServerSystemVariables(Plugin):

    def __init__(self, **kwargs):
        self.keywords = ['provider']
        print self.__class__.__name__, 'initialising...'

    def generate(self, **kwargs):
        cursor = kwargs['connection'].cursor()
        cursor.execute('SHOW GLOBAL VARIABLES')
        result = {}
        for k, v in cursor.fetchall():
            result[k] = v
        cursor.close()
        return result

查询服务器状态变量

服务器状态变量通过显示内部计数器来提供对服务器操作的洞察。所有变量都是只读的,不能修改。

Image 注意你可以在 MySQL 文档中找到关于每个 MySQL 服务器状态变量的详细信息,可以在dev . MySQL . com/doc/ref man/5.7/en/server-status-variables . html上找到。

SHOW 命令语法是 SHOW STATUS。类似于不带修饰符的 SHOW VARIABLES 命令,该命令返回适用于当前会话的状态,等效于 SHOW LOCAL STATUS 命令。如果要检索服务器范围的状态,请使用 SHOW GLOBAL STATUS 命令。

此行为仅适用于 MySQL 服务器的 5.0 及更高版本。此版本之前的版本具有相反的行为,其中 SHOW STATUS 假定为全局状态,如果您想要检索特定于会话的计数器,则需要显式运行 SHOW LOCAL STATUS。如果您正在开发一个可能在不同版本的 MySQL 服务器上执行的插件,这可能会带来一个问题。不过,这个问题有一个简单的解决方案:在 SHOW 语句中指定版本选择器。以下查询正确地使用了适当的命令修饰符,并且可以跨所有版本的 MySQL server 使用:

SHOW /*!50000 GLOBAL */ STATUS

您也可以在此命令中使用 LIKE 和 WHERE 数据集修饰符,如下例所示:

>>> cursor.execute("SHOW GLOBAL STATUS WHERE variable_name LIKE '%innodb%' AND value > 0")
16L
>>> forin cursor.fetchmany(10): print r
...
('Innodb_buffer_pool_pages_data', '19')
('Innodb_buffer_pool_pages_free', '493')
('Innodb_buffer_pool_pages_total', '512')
('Innodb_buffer_pool_read_ahead_rnd', '1')
('Innodb_buffer_pool_read_requests', '77')
('Innodb_buffer_pool_reads', '12')
('Innodb_data_fsyncs', '3')
('Innodb_data_read', '2494464')
('Innodb_data_reads', '25')
('Innodb_data_writes', '3')
>>>

清单 13-4 显示了检索系统状态变量的插件。这个插件类类似于查询系统配置设置的插件类。

清单 13-4 。用于检索系统状态变量的插件

class ServerStatusVariables(Plugin):

    def __init__(self, **kwargs):
        self.keywords = ['provider']
        print self.__class__.__name__, 'initialising...'

    def generate(self, **kwargs):
        cursor = kwargs['connection'].cursor()
        cursor.execute('SHOW /*!50000 GLOBAL */ STATUS')
        result = {}
        for k, v in cursor.fetchall():
            result[k] = v
        cursor.close()
        return result

收集主机配置数据

我们能够检索 MySQL 配置和状态数据,这很好,但是我们仍然需要将这些数据放到操作环境的上下文中,以便实际使用它们。

我们以系统配置列表中的 key_buffer_size 变量为例。此变量设置专用于 MyISAM 表索引的内存量。该设置会对 MySQL 服务器的性能产生重大影响。如果设置得太小,索引将不会缓存在内存中,并且对于每次查找,服务器都将执行磁盘读取操作,这比从内存读取操作要慢得多。

如果给这个缓冲区分配太多的内存,就会限制可用于其他操作的内存,比如文件系统缓存。如果文件系统缓存太小,所有读写操作都不会被缓存,因此磁盘 I/O 会受到负面影响。

这个缓冲区变量的标准建议是使用服务器上可用总内存的 30%到 40%。所以,要做这个推论,你实际上需要知道系统上的物理内存量!

您必须考虑许多不同的方面,但是最重要的是物理内存的数量、虚拟内存的数量(或者 Linux 系统上的交换空间大小)以及 CPU 内核的数量。

我们将使用 psutil 库,它提供了查询系统内存读数的 API。该库旨在获取关于正在运行的进程的信息,并执行一些基本的进程操作。它不包含在基本的 Python 模块集中,但是它在大多数 Linux 发行版上都有。例如,在 Fedora 系统上,可以使用以下命令安装这个库:

$ sudo yum install python-psutil

源代码和完整的文档可以在项目网站github.com/giampaolo/psutil获得。

不幸的是,这个库不提供关于可用 CPU 内核数量的信息。我们需要查询 Linux /proc/ file 系统来获得关于可用 CPU 的报告。这很容易做到。我们只需要计算/proc/cpuinfo 文件中以关键字 processor 开头的行数。

清单 13-5 显示了收集系统内存读数和 CPU 信息的插件代码。

清单 13-5 。用于检索系统信息的插件

import psutil

[...]

class HostProperties(Plugin):

    def __init__(self, **kwargs):
        self.keywords = ['provider']
        print self.__class__.__name__, 'initialising...'

    def _get_total_cores(self):
        f = open('/proc/cpuinfo', 'r')
        c_cpus = 0
        for line in f.readlines():
            if line.startswith('processor'):
                c_cpus += 1
        f.close()
        return c_cpus

    def generate(self, **kwargs):
        result = { 'mem_phys_total': psutil.TOTAL_PHYMEM,
                   'mem_phys_avail': psutil.avail_phymem(),
                   'mem_phys_used' : psutil.used_phymem(),
                   'mem_virt_total': psutil.total_virtmem(),
                   'mem_virt_avail': psutil.avail_virtmem(),
                   'mem_virt_used' : psutil.used_virtmem(),
                   'cpu_cores'     : self._get_total_cores(),
                 }
        return result

编写消费者插件

现在我们已经准备好开始编写顾问插件了。这些插件将基于它们从信息生产者模块接收的信息提出建议。到目前为止,我们已经收集了关于数据库设置和状态的基本信息,以及关于物理硬件和操作系统的一些信息。尽管信息集并不详尽,但它包括了做出一些有根据的结论所需的关键细节。在这里,我们将看三个例子,它们应该足以让您快速上手,这样您就可以开始开发自己的 advisor 插件了。

检查 MySQL 版本

您可能需要执行的第一个检查是 MySQL 版本号。让您的服务器安装保持最新非常重要。每个新版本都会修复服务器软件错误,并可能带来性能改进。

检查当前 MySQL 版本的插件基于最新的公开发行(GA)版本号做出决定,该版本号可从位于mysql.com/downloads/mysql/的 MySQL 下载页面获得。为了从网页中提取这些信息,我们将使用漂亮的 Soup HTML 解析库。页面结构相对简单,我们需要的数据包含在最后出现的< h1 >标签:中

[...]
<div id="page" class="sidebar" >
    <h1 class="page_header">Download MySQL Community Server</h1>
[...]
<div dojoType="dijit.layout.ContentPane" title="Generally Available (GA) Releases"
 id="current_pane" selected="true">

<h1>MySQL Community Server 5.6.19/h1>

<div id="current_os_selection">
[...]

插件代码将提取这些信息,并将其与 ServerSystemVariables 模块报告的信息进行比较。可以报告四种状态:

  • 如果主要版本号不匹配,这可能是一个严重的问题,因此被标记为关键。
  • 如果当前主要版本与最新版本匹配,但当前次要版本号低于最新版本号,则该问题被标记为警告。
  • 如果主版本和次版本都是最新的,只需注意补丁可能是有益的。
  • 如果以上都不是,我们将断定当前安装是最新的。

Image 注意另一个可能的检查是检查比当前 GA 版本更新的版本,这可能会导致潜在的问题,因为开发版本不能被彻底测试。为了简化代码,我们将在示例中排除这种情况。这种额外的检查应该相对容易包含在模块中。

检查当前 MySQL 版本的插件的完整列表如清单 13-6 所示。

清单 13-6 。用于根据最新 GA 版本检查当前版本的模块

class MySQLVersionAdvisor(Plugin):

    def __init__(self, **kwargs):
        self.keywords = ['consumer']
        self.advices = []
        self.installed_release = None
        self.latest_release = None

    def _check_latest_ga_release(self):
        html = urllib2.urlopen('http://www.mysql.com/downloads/mysql/')
        soup = BeautifulSoup(html)
        tags = soup.findAll('h1')
        version_str = tags[1].string.split()[-1]
        (major, minor, release) = [int(i) forin version_str.split('.')]
        return (major, minor, release)

    def process(self, **kwargs):
        version = kwargs['env_vars']['ServerSystemVariables']['version'].split('-')[0]
        (major, minor, release) = [int(i) forin version.split('.')]
        latest_major, latest_minor, latest_rel = self._check_latest_ga_release()
        self.installed_release = (major, minor, release)
        self.latest_release = (latest_major, latest_minor, latest_rel)
        if major < latest_major:
            self.advices.append(('CRITICAL',
                   'There is a newer major release available, you should upgrade'))
        elif major == latest_major and minor < latest_minor:
            self.advices.append(('WARNING',
                   'There is a newer minor release available, consider an upgrade'))
        elif major == latest_major and minor == latest_minor and release < latest_rel:
            self.advices.append(('NOTE',
                   'There is a newer update release available, consider a patch'))
        else:
            self.advices.append(('OK', 'Your installation is up to date'))

    def report(self, **kwargs):
        print self.__class__.__name__, 'reporting...'
        print "The running server version is: %d.%d.%d" % self.installed_release
        print "The latest available GA release is: %d.%d.%d" % self.latest_release
        for rec in self.advices:
            print "%10s: %s" % (rec[0], rec[1])

以下是在运行比当前版本稍旧的服务器的系统上执行的报告功能的输出:

MySQLVersionAdvisor reporting...
The running server version is: 5.6.19
The latest available GA release is: 5.6.19
      NOTE: There is a newer update release available, consider a patch

检查密钥缓冲区大小设置

我们已经讨论了 key_buffer_size 配置参数的含义以及该设置对 MySQL 数据库服务器性能的影响。如清单 13-7 中的所示,插件模块假设最佳设置是总可用物理内存的 40%。

清单 13-7 。检查密钥缓冲区大小的最佳设置

class KeyBufferSizeAdvisor(Plugin):

    def __init__(self, **kwargs):
        self.keywords = ['consumer']
        self.physical_mem = 0
        self.key_buffer = 0
        self.ratio = 0.0
        self.recommended_buffer = 0
        self.recommended_ratio = 0.4

    def process(self, **kwargs):
        self.key_buffer = \
                    int(kwargs['env_vars']['ServerSystemVariables']['key_buffer_size'])
        self.physical_mem = int(kwargs['env_vars']['HostProperties']['mem_phys_total'])
        self.ratio = float(self.key_buffer) / self.physical_mem
        self.recommended_buffer = int(self.physical_mem * self.recommended_ratio)

    def report(self, **kwargs):
        print self.__class__.__name__, 'reporting...'
        print "The key buffer size currently is %d" % self.key_buffer
        if self.ratio < self.recommended_ratio:
            print "This setting seems to be too small for the amount of memory \
                                             installed: %d" % self.physical_mem
        else:
            print "You may have allocated too much memory for the key buffer"
            print "You currently have %d, you must free up some memory"
        print "Consider setting key_buffer_size to %d, if the difference is \
                                               too high" % self.recommended_buffer

以下是报告的输出示例:

KeyBufferSizeAdvisor reporting...
The key buffer size currently is 8384512
This setting seems to be too small for the amount of memory installed: 1051463680
Consider setting key_buffer_size to 420585472, if the difference is too high

检查慢速查询计数器

由于各种原因,一些 SQL 查询可能需要很长时间才能执行。如果您有一个大型数据集,大多数查询需要相当长的时间来完成可能是完全正常的。在这种情况下,您可能需要增加 long_query_time 设置。另一种可能是您的表没有被正确索引。在这种情况下,您应该重新考虑表的结构和设置。

我们的最后一个插件模块读取两个状态变量:数据库服务器收到的请求总数和执行时间比 long_query_time 指定的时间长的查询总数。如果该比率大于 0.0001%(百万分之一以上的查询为慢速查询),报告会将其指示为问题。显然,您可能需要调整这个值以适应您的特定数据库环境。

MySQL 服务器上默认情况下不启用慢速查询跟踪,因此在执行插件代码之前,需要将 MySQL 属性文件/etc/my.cnf 中的 log_slow_queries 变量设置为 on。完整的模块代码如清单 13-8 中的所示。

清单 13-8 。检查慢速查询比率的插件

class SlowQueriesAdvisor(Plugin):

    def __init__(self, **kwargs):
        self.keywords = ['consumer']
        self.log_slow = False
        self.long_query_time = 0
        self.total_slow_queries = 0
        self.total_requests = 0
        self.long_qry_ratio = 0.0 # in %
        self.threshold = 0.0001   # in %
        self.advise = ''

    def process(self, **kwargs):
        if kwargs['env_vars']['ServerSystemVariables']['log_slow_queries'] == 'ON':
               self.log_slow = True
        self.long_query_time = \

float(kwargs['env_vars']['ServerSystemVariables']['long_query_time'])
        self.total_slow_queries = \
                   int(kwargs['env_vars']['ServerStatusVariables']['Slow_queries'])
        self.total_requests = \
                   int(kwargs['env_vars']['ServerStatusVariables']['Questions'])
        self.long_qry_ratio = (100\. * self.total_slow_queries) / self.total_requests

    def report(self, **kwargs):
        print self.__class__.__name__, 'reporting...'
        if self.log_slow:
            print "There are %d slow requests out of total %d, which is %f%%" % \
                                                       (self.total_slow_queries,
                                                        self.total_requests,
                                                        self.long_qry_ratio)
            print "Currently all queries taking longer than %f are considered \
                                                        slow" % self.long_query_time
            if self.long_qry_ratio < self.threshold:
                print 'The current slow queries ratio seems to be reasonable'
            else:
                print 'You seem to have lots of slow queries, investigate them and \
                                               possibly increase long_query_time'
        else:
            print 'The slow queries are not logged, set log_slow_queries to ON for tracking'

以下是该模块的输出示例:

SlowQueriesAdvisor reporting...
There are 0 slow requests out of total 15, which is 0.000000%
Currently all queries taking longer than 10.000000 are considered slow
The current slow queries ratio seems to be reasonable

摘要

在本章中,我们已经讨论了如何检查 MySQL 数据库的设置和当前运行状态。我们还修改了我们在第六章中创建的插件框架,以便它允许不同插件模块之间的信息交换。

  • 可以使用 SHOW GLOBAL VARIABLES 查询来查询 MySQL 服务器配置项。
  • 可以使用 SHOW GLOBAL STATUS 命令检查数据库状态变量。
  • 您可以使用 psutil 模块来获取有关可用系统内存的信息。

十四、使用亚马逊 EC2/S3 作为数据仓库解决方案

虚拟计算或云计算正变得越来越流行。有各种各样的原因,但主要是节约成本。许多大型供应商都提供云计算服务,如亚马逊、IBM、惠普、谷歌、微软和 VMWare。这些服务中的大多数都提供了 API 接口,允许控制虚拟机实例和虚拟存储设备。在本章中,我们将研究如何从您的 Python 应用中控制亚马逊弹性计算云(EC2)和亚马逊简单存储系统(S3)。

指定问题和解决方案

首先,我们需要了解这个解决方案在什么情况下适用。尽管按需计算是一种方便的方法,可以节省大量成本,但它并不适用所有情况。在本节中,我们将简要讨论按需计算可以成功使用的情况。

问题

让我们想象一个典型的小型网络创业公司。该公司在互联网上提供一些服务。用户群相对较小但在稳步增长,而且在地理上分布均匀,这意味着系统一天 24 小时都很忙。

该系统是典型的两层设计,由两个应用节点和两个数据库节点组成。应用服务器运行的是部署在 Apache Tomcat 应用服务器上的内部构建的 Java 应用,并使用 MySQL 数据库来存储数据。web 应用和数据库服务器相当繁忙,因此被认为不适合在虚拟化平台上运行。所有四台服务器都是从服务器托管公司租赁的,托管在远程数据中心。

现在,这个设置满足了当前的大部分需求,并且考虑到缓慢的用户基础增长,它应该在相当长的时间内保持不变。该公司的扩展策略是根据需要添加更多的应用和数据库节点。应用设计允许近乎线性的水平可伸缩性。

然而,随着公司的发展,所有者决定在市场研究上投入更多。为了更好地了解用户行为,更有针对性地进行销售,公司需要分析存储在数据库中的数据。然而,正如我们已经知道的,数据库服务器已经非常繁忙,运行额外的查询肯定会降低整个应用的速度。仅仅为了数据分析任务而添加新的数据库服务器是不划算的,因为这需要相当大的初始投资,并且会增加每月的维护成本。此外,分析很少执行,新系统大部分时间都处于闲置状态。

我们创业公司面临的第二个问题是缺乏备份策略。目前,所有数据都存储在数据库服务器上,尽管服务器是冗余的,但它们仍然位于同一场所。这些数据肯定应该在远程位置进行备份。

我们的解决方案

一种策略是使用按需计算解决方案,比如 Amazon EC2。由于该公司只是偶尔需要处理能力,因此可以在必要时创建虚拟服务器来执行计算。当计算完成后,公司可以安全地销毁虚拟服务器。在这种情况下,公司只为服务器处于活动状态的时间付费。在撰写本文时,这些虚拟实例的成本从每小时 0.02 美元到 6.82 美元不等,具体取决于使用的内存和分配的虚拟 CPU 数量。

如果每周进行一次数据分析,每次花费 8 小时,每月总成本不会超过 10 美元(假设一个超大高内存实例目前定价为每小时 0.28 美元)。这比公司决定租赁一台普通服务器的成本要低得多。

但是请记住,初创公司面临的第二个问题是缺少远程备份。Amazon 提供了一个高可用性和可伸缩性的存储解决方案:它的简单存储系统。与 EC2 类似,您只需为使用的内容付费,在 S3 上可以存储的内容没有限制。在撰写本文时,S3 的基本定价是每月每千兆字节 0.03 美元。如果你将数据上传到 S3,数据传输是免费的,但数据传出去(例如,如果你想从备份恢复)将花费你每 GB 0.12 美元。

这就是你要小心的地方,因为总价加起来可能相当大。1tb 的信息会让你每月花费 30 美元。考虑到当前的存储价格,这听起来可能是一大笔钱(1TB 外部 USB 价格为 60 美元,这是一次性费用),但请记住,您不仅获得了存储设备,还获得了数据保护。目前,标准的亚马逊 S3 提供“给定年份内 99.999999999%的持久性和 99.99%的可用性”(aws.amazon.com/s3/)。

设计规范

为了适应我们之前提出的所有需求和约束,我们将构建一个应用,该应用将在 EC2 中创建一个新的虚拟机实例。虚拟机将运行一个 MySQL 数据库服务器实例,并可用于接受外部连接。数据库文件将存储在一个单独的、高度可用的卷上,即弹性块存储卷。

应用将分三个阶段运行:初始化、处理和取消初始化。在初始化阶段,应用创建一个虚拟机,将卷设备连接到该虚拟机,并启动 MySQL 服务器。处理阶段取决于您的处理要求;它通常包含数据传输和数据处理任务。我们不打算详细讨论这个阶段,因为它确实取决于您自己的要求。最后,在取消初始化阶段,我们关闭远程 MySQL 实例,分离卷,创建快照,并销毁虚拟机。

创建快照的原因是为了在需要检查特定时间点的数据状态时,有一个可以恢复的参考点。你可以把它看作一个版本控制系统。显然,每个快照都会增加数据使用量,从而增加成本,因此您必须手动控制想要维护的快照映像的数量。

亚马逊 EC2 和 S3 速成班

在写这篇文章的时候,没有多少关于亚马逊 EC2 和 S3 的最新书籍。原因是这两种技术(尤其是 EC2)都在快速发展,这使它们成为快速移动的目标。有一些好书,可惜已经略显过时。

关于亚马逊网络服务的一个很好的手册是 James Murty 的《编程亚马逊网络服务:S3、EC2、SQS、FPS 和 simple db 》( O ' Reilly Media,2008)。这本书很好地概述了这些技术以及详细的 API 规范。另一个更加关注运营方面的文本是乔治·里斯(O'Reilly Media,2009)的云应用架构:在云中构建应用和基础设施

您还可以在每个 web 服务的文档页面上找到大量信息:

很难在一章中包含所有关于这些 web 服务的必要信息,所以我将描述一些基本概念。话虽如此,本章将为您提供足够的信息来开始使用 Amazon EC2 和 S3 web 服务,并且您可以在熟悉基本原则的基础上进行更多的探索。

理解这两个系统(EC2 和 S3)主要是 web 服务,并且被设计成使用标准的 web 服务协议(如 SOAP 和 REST)来控制,这一点很重要。许多工具为这些服务提供了用户友好的界面,但是它们都使用上述协议与 AWS (Amazon Web Services)进行交互。

如果你想使用这些服务,你必须在 aws.amazon.com/注册。您不必为每个服务…

身份验证和安全性

当您使用 EC2 和 S3 服务时,您必须向 AWS 系统验证您自己。有不同的方法可以做到这一点,不同的服务需要您提供略有不同的信息。有时,这可能会造成混乱,不知道应该在哪里使用哪种方法,更重要的是,不知道从哪里获取这些信息。因此,在研究每个单独的服务之前,我将提供关于 AWS 中使用的安全性和认证机制的基本信息。

帐户标识符

每个账户都有一个唯一的 AWS 账户 ID 号,由 12 位数字组成,看起来像 1234-5678-9012。每个帐户还有一个指定的规范用户 ID ,它是一个包含 64 个字母数字字符的字符串。

AWS 帐号用于在不同帐户之间共享对象。例如,如果您想授权其他人访问您的虚拟机映像,您必须知道该人的 AWS 帐户 ID。该 ID 用于除 S3 之外的所有 AWS 服务。

规范用户 ID 仅在 S3 服务中使用。与 AWS 帐户 ID 类似,它的主要目的是控制访问。

你可以通过进入aws.amazon.com/account/,点击“安全证书”,并向右滚动到网页底部来获取这些信息。包含所需信息的部分称为“帐户标识符”

访问凭据

每个 REST API 调用都会用到访问凭证。这些键也在亚马逊 S3 SOAP 调用中使用。

访问凭证分为两部分。第一部分是访问密钥 ID ,用于识别请求者身份。第二部分是秘密访问密钥,它用于创建一个签名,该签名随每个 API 请求一起发送。当 AWS 收到请求时,它使用相应的秘密访问密钥(只有 AWS 知道)来验证签名。只有有效的秘密访问密钥才能创建能够被 AWS 秘密密钥对应方验证的签名。这确保了请求是从有效的请求者发出的。

这两个密钥都是长字母数字字符串,可以在“访问密钥”选项卡的访问凭据部分找到。建议定期轮换钥匙。此外,确保不要向任何人透露密钥。

最佳实践是创建新用户并生成访问密钥 id 和相应的秘密访问密钥,而不是依赖根访问密钥对。这可以通过导航到用户帐户管理控制台console.aws.amazon.com/iam/home?#users来完成。

X.509 证书

X.509 证书主要用于 SOAP API 请求。该证书由两个文件组成。第一个文件是 X.509 证书,它包含您的公钥和相关元数据。公钥与请求正文一起发送,用于解密请求中包含的签名信息。

证书的第二部分是私钥文件。该文件用于创建数字签名,它包含在每个 SOAP 请求中。你必须保守这个密钥的秘密。

当您生成 X.509 证书时,会为您提供这两个文件。然而,密匙并不存储在亚马逊系统上;因此,如果您丢失了私钥,您必须重新生成 X.509 证书。与访问凭据密钥一样,定期轮换证书是一种很好的做法。

您可以在 X.509 证书选项卡下的“访问凭据”部分生成新闻证书或上传您自己的证书。

EC2 密钥对

EC2 密钥对允许您登录到新的虚拟机实例。每个密钥对由三部分组成。

第一部分是密钥对名称。创建新实例时,通过选择适当的密钥对名称来选择要在该实例上使用的密钥对。

第二部分是私钥文件。该文件用于建立到新虚拟机实例的 SSH(安全 Shell)会话。这个密钥在任何时候都要保密和安全,这一点非常重要。拥有此密钥的任何人都可以访问您的任何虚拟机。

最后一部分是公钥,保存在 AWS 系统上。您不能下载此密钥。当虚拟机实例启动时,AWS 会将这个密钥复制到正在运行的系统,这允许您使用您的私有密钥文件连接到它。

您可以生成任意多的密钥对。与其他凭证不同,EC2 密钥只能从 EC2 管理控制台访问,该控制台位于console.aws.amazon.com/ec2/home

简单的存储系统概念

从用户的角度来看,在 S3 架构中只有两个实体:数据对象和存储桶。

最重要的实体是数据对象。数据对象是实际存储在 S3 基础设施上的东西。从技术上讲,每个数据对象都由两部分组成:元数据和数据负载。元数据部分描述对象,由键值对组成。作为开发人员,您可以定义任意数量的键值对。该元数据在请求的 HTTP 头中发送。第二部分是数据有效载荷,它是您实际想要存储在 S3 上的内容。数据有效负载大小可以是 1 字节到 5 千兆字节。您可以为对象指定任何名称,只要它符合 URI 命名标准。基本上,如果您将名称限制为字母数字字符、点、正斜杠和连字符,应该没问题。

第二个实体是 bucket 对象。是包含数据对象的实体。这些存储桶不能包含其他存储桶。对象名称空间在每个桶内;但是,bucket 名称在全局名称空间中。这意味着一个存储桶中的对象必须有唯一的名称,但是在不同的存储桶中可以有两个同名的对象。在 S3 系统中,存储桶必须具有唯一的名称,因此您可能会尝试使用其他人已经使用的存储桶名称。

每个帐户有 100 个存储桶的限制,但是每个存储桶中存储的对象的大小没有限制。

图 14-1 展示了桶和对象之间的关系,以及每个对象的一些示例名称。

9781484202180_Fig14-01.jpg

图 14-1 。亚马逊 S3 桶和物品

可以使用以下命名方案将这些名称映射到亚马逊 S3 资源 URL:

http://<bucket name>.s3.amazonaws.com/<object name>

因此,图 14-1 中的对象可以通过以下 URL 访问(假设启用了公共访问权限):

我有意在第二个桶中显示真实的 web URLs。当您导航到任何网站时,您的浏览器都会使用 HTTP GET 请求来获取页面。这些请求与用于访问 S3 系统对象的 REST 请求相同,因此您可以在 S3 上托管完整的网站(或动态站点的静态部分)。

弹性计算云概念

Amazon EC2 WS 是一个复杂的系统,它与其他服务(如 Amazon S3)交互,为您提供完整的按需计算解决方案。如果您熟悉任何虚拟化平台,如 Xen、KVM 或 VMWare,您会发现这里描述的大多数概念都非常相似。

Amazon 机器映像和实例

亚马逊机器映像(AMI) 是可以启动的操作系统的映像。该映像包含运行您的系统所需的所有包。你需要多少阿米就有多少。例如,如果您想要复制我们前面描述的两层 web 系统,您将创建两种类型的 AMI:web 服务器 AMI 和数据库 AMI。web 服务器 AMI 安装了 Apache web 服务器和 Apache Tomcat 应用服务器包。数据库 AMI 会安装一个 MySQL 实例。

公开提供了许多不同的 ami。有几个是亚马逊和其他公司提供的。一些 ami 是免费的,但是如果你想使用它们,也有一些 ami 是需要付费的。创建您自己的 AMI 的最简单的方法是克隆一个现有的 AMI 并进行您自己的修改。确保您使用来自可信来源的 AMI!

Image 注意尽量不要将你的操作建立在公开可用的 AMIs 上。当此类 AMI 的创建者决定销毁该 AMI 时,您将无法再次使用它。如果你发现一个你认为合适的 AMI,就把它复制一份,创建一个私有 AMI。即使你不打算对它做任何修改,也要这样做。这确保了每当您需要使用 AMI 时,您总是能够找到相同的 AMI。S3 上典型的 Linux AMI 大小不到 1GB。假设标准费用为每月 0.03 美元,维护自己的 AMI 每年只需花费 0.36 美元。

您不能运行 AMI 本身;您必须创建一个想要运行的 AMI 实例。该实例是运行 AMI 中安装的软件的实际虚拟机。类似的可以是 Python 类和类实例(或对象)。该类定义了方法和属性(或操作系统术语中的软件包)。当您想要运行已定义的方法时,您可以创建该特定类的对象。类似地,AMI 是虚拟机的内容,而实例是实际运行的虚拟机。

您有两种选择来存储 ami:在亚马逊 S3 存储上或者在亚马逊弹性块存储快照上(我们将在下一节讨论)。存储 AMI 的方法决定了它是如何创建的,并影响它的行为。

表 14-1 总结了这两种储存 ami 方法之间的差异。

表 14-1 。比较 S3 和 EBS 支持的非盟特派团

|

方面

|

EBS 支持的 AMI

|

S3 支持的 AMI

| | --- | --- | --- | | 尺寸限制 | EBS 卷限制为 1TB。这对于大型安装非常方便。 | S3 支持的根分区最大可达 10GB。如果您的根分区需要比这个大,您就不能使用这个方法。 | | 停止正在运行的实例 | 您可以停止实例,这意味着虚拟机没有运行,您也没有被收费,但是根分区没有被释放,仍然作为 EBS 卷存在。然后,您可以从同一个实例卷重新启动该实例。 | 您不能停止实例。如果您停止实例,它将被终止,根分区也被销毁;因此,存储在该分区上的所有信息都将丢失。 | | 数据持久性 | 本地数据存储是附加的,可用于存储临时数据。停止实例时,不会分离根分区,但会丢失本地存储。您可以连接任意数量的 EBS 卷来永久存储数据。 | 本地数据存储是附加的,可用于存储临时数据。终止实例时,根分区和本地存储中的数据都会丢失。您可以连接任意数量的 EBS 卷来永久存储数据。 | | 启动时间 | 引导时间更快,因为根分区上的数据在 EBS 卷上立即可用。但是,虚拟机在开始时会运行得较慢,因为数据是逐渐从快照中获取的。 | 启动时间较慢,因为所有数据都需要在部署到根分区之前从 S3 中检索。 | | 创建新的图像 | 单个 API 调用将现有运行的 AMI 克隆到一个新卷。 | 您必须创建一个包含所有必需包的操作系统映像,然后创建一个映像包并将其上传到 S3。然后用捆绑的映像注册 AMI。 | | 充电 | 将收取以下费用:

  • 卷快照(完整卷大小)的费用
  • 实例处于停止状态时使用的卷空间的费用(完整卷大小)
  • 对正在运行的实例收费

| 将收取以下费用:

  • 存储 AMI 图像的 S3 费用(压缩)
  • 对正在运行的实例收费

|

下图显示了 S3 支持的和 EBS 支持的实例的生命周期。

图 14-2 显示了一个典型的 S3 支持的实例的生命周期。该实例是从存储在 S3 上的 AMI 映像创建的。当实例终止时,卷将被销毁,所有数据都将丢失。您只需支付 S3 商店和虚拟机的运行成本。

9781484202180_Fig14-02.jpg

图 14-2 。S3 支持的实例生命周期

图 14-3 显示了 EBS 支持的实例的典型生命周期。初始启动时,根卷是从 EBS 快照创建的。实例可以有两种不同的状态:运行和停止。当实例运行时,您需要为处理能力和 EBS 容量付费。当实例停止时,您只需为 EBS 卷付费。如果您恢复实例,它将保留其根卷中的所有数据;所以,你买单。最后,如果您选择销毁实例,卷也会被销毁,并且您不再为卷付费。

9781484202180_Fig14-03.jpg

图 14-3 。EBS 支持的实例生命周期

从图中可以看出,无论实例类型如何,它们都有一个本地存储。这个存储称为临时存储,它的生命周期受限于实例处于运行状态的时间。它还可以在操作系统重新启动后继续存在(有意或无意),但是一旦您停止实例,临时设备上的所有数据都会丢失。

弹性块存储

弹性块存储(EBS) 是一个块级设备,可用于 EC2 实例。卷完全独立于实例,当实例被终止和销毁时,数据不会丢失。EBS 卷是高度可用和可靠的存储设备。

每个 EBS 卷的大小从 1GB 到 1TB 不等。您可以将多个卷附加到一个正在运行的 EC2 实例。如果需要大于 1TB 的卷,可以使用 LVM(逻辑卷管理器)等操作系统工具将多个 EBS 卷合并成一个更大的卷。

正如我提到的,EBS 卷是块设备,所以在使用它们之前,您必须在其上创建一个文件系统。或者,您可以在支持原始设备访问的应用中将这些设备用作原始设备。

Amazon WS 还提供了获取卷快照的功能。A ) 卷快照是卷内容的时间点副本。拷贝将备份到 S3 存储。您可以根据需要创建任意数量的快照。第一个快照是卷的完整副本,但顺序快照仅记录最后一个快照和当前卷状态之间的差异。

拍摄卷快照的操作可以颠倒过来,您可以从现有快照创建卷。如果您必须向多个 EC2 实例提供相同的数据,这将非常有用。您还可以在 Amazon WS 帐户之间共享快照。

其他重要且有用的 EBS 功能包括:

  • 调配的 IOPS,允许您预定义特定级别的 I/O 性能。
  • EBS 卷加密,可用于加密卷和保护敏感数据。
  • 性能指标监控,可通过 AWS 管理控制台获得。

安全组

使用安全组控制对实例的网络访问。安全组是一组网络访问规则,类似于 IPTables 规则集。您可以定义目的网络地址、端口号和通信协议,例如 TCP 或 UDP。

当您启动一个新实例时,您可以为其分配一个或多个安全组。例如,您可以有一个允许 TCP 访问端口 3306 (MySQL 服务端口)的数据库安全组。当您创建一个新的数据库实例时,您选择这个安全组,它允许外部访问您的 MySQL 服务器。

确保您允许管理 SSH 访问您的实例;否则,您将无法连接和管理它们。

弹性 IPs 和负载平衡器

默认情况下,每个实例都会收到一个动态分配的公共 IP 地址。显然,这不适合服务于 web 内容或提供其他公共可用服务的服务器。每次重新启动实例时,您可能会获得不同的 IP 地址。

您可以请求一个弹性 IP 地址,它总是附属于一个 EC2 实例。这允许您为您的服务器创建一个 DNS 条目,并且该条目不需要随着时间而改变。弹性 IP 的额外好处是您可以为它分配一个故障转移实例。这意味着,如果主实例出现故障,IP 将被重新定位到另一个能够处理请求的实例。这种方法允许您实现简单的活动-备用系统配置。

您还可以使用 Amazon EC2 负载平衡功能,将传入的请求分布在两个或更多实例之间。虚拟负载平衡器的作用类似于传统的硬件负载平衡器,如 Cisco、Citrix Netscaler 或 A10 range 负载平衡器。

创建新的负载平衡器实例相对简单。您必须选择外部可用的服务端口—例如,用于 HTTP 流量的端口 80。然后,在实例上选择服务端口。例如,假设您在 EC2 实例的端口 8080 上运行一个 Tomcat 实例,但是您想通过标准的 HTTP 端口 80 提供这个服务。在这种情况下,外部服务端口 80 将被映射到内部服务端口 8080。最后,将 EC2 实例分配给负载平衡器。

用户界面

您可以通过位于console.aws.amazon.com/console/home的 AWM 管理控制台管理所有 AWS 服务。

创建自定义 EC2 图像

现在,您已经对 EC2 和 S3 服务有了基本的了解,让我们将这些知识付诸实践。正如您已经知道的,我们需要创建一个 AMI,用于启动我们的实例。我将向您展示如何基于现有的图像创建自定义 AMI。我们将创建一个 S3 支持的 AMI 映像,因为在我们的实例中,它将更加经济高效,并且我们不需要实例停止功能。当数据被传输和处理后,我们可以销毁实例。

重用现有图像

让我们从可用图像列表中选择一个现有图像开始。在本练习中,我将使用标准的 Amazon AWS 管理控制台。

  1. 我们首先从主仪表板中选择 EC2 管理控制台。我们将看到您所有 EC2 服务的概述。

  2. 在左侧菜单中,我们选择“图像”部分下的“AMIs”。

  3. The default filter is to show all images that we own. We will need to select “Public Images” from the dropdown menu. For this exercise, I am going to use a CentOS 6.5 image created by a company called RightScale. This is a well-known company, which specializes in deploying mission-critical systems in the cloud environment; therefore, the images produced by them can be trusted. The AMIs ID we are looking for is ami-2e32c646. We can find it by using a search field in the filter.

    图 14-4 是选择了 AMI 的 AWS 管理控制台的屏幕截图。

    9781484202180_Fig14-04.jpg

    图 14-4 。选择要克隆的 AMI

  4. 找到 AMI 后,我们右键单击它并选择“Launch”来启动实例启动过程。

Image 注意确保您已经创建了安全组,并启用了端口 3306 (MySQL)和 22 (SSH)以便从所有 IP 进行访问。您还需要生成一个密钥对,并将私钥下载到您的本地机器上。安全地保存私钥,并记下它的名称。在本文中,我们将这个文件称为<密钥对名> .pem

我们可以通过单击导航菜单中的 Instances 链接来监视 EC2 实例的状态。一旦实例处于运行状态,我们就可以使用 SSH 来连接它。单击实例名称,详细信息将显示在单独的窗口中。记下实例的公共 DNS 名称。

我们使用以下命令连接到实例:

$ ssh -i <key-pair name>.pem root@<instance public DNS>

进行修改

我们现在准备对图像进行修改。正如您所记得的,我们的目标是使这个映像成为一个 MySQL 数据库实例,将所有文件存储在一个专用的持久 EBS 卷上。

安装附加软件包

首先,我们需要安装额外的包——特别是 MySQL 服务器。我们首先执行这一步的原因是,在挂载新的文件系统时,我们需要 MySQL 用户帐户,它是由我们现在要安装的包创建的。

我们使用 Yum 安装程序来安装附加的包:

# yum install mysql mysql-server

该命令将从操作系统存储库中安装默认的 MySQL 包版本。在 CentOS 6.5 的情况下,这将是 MySQL 5.1 版本。(如果需要使用更高版本的 MySQL server,就必须使用 Oracle 提供的 MySQL 存储库。)要添加存储库配置,我们在安装 MySQL 包之前运行以下命令:

# yum install http://repo.mysql.com/mysql-community-release-el6-5.noarch.rpm

Image 注意如果你决定从源代码构建 MySQL,确保你设置好了,这样当你的机器启动时它会自动启动。如果你安装的是标准的操作系统包,你就不需要担心这个问题。

创建和设置弹性块存储卷

接下来,我们将设置一个新的 EBS 卷。我们返回并单击“Create Volume ”,然后使用弹出窗口指定新卷所需的选项。

我们确保为数据分配足够的空间。可用性区域必须与正在运行的实例的可用性区域相匹配。我们可以通过单击 Instances 部分中的实例名称来找出实例的可用性区域。

根据卷的大小,卷可能需要一段时间才能变得可用。当卷变得可用时(如卷状态列所示),我们可以将其附加到正在运行的 EC2 实例。我们右键单击卷名并选择“附加卷”菜单项。然后,我们将看到可用的运行 EC2 实例的列表。我们选择之前创建的实例。我们还将被要求为新卷指定本地设备名称。在选择设备名称(如/dev/sdf)时,我们确保该名称没有被任何其他设备使用。

当设备可用时(在实例的文件系统上创建了设备文件/dev/xvdf),我们可以使用以下命令在其上创建文件系统:

# mke2fs -F -j /dev/xvdf
...
# e2label /dev/xvdf mysqlvol

Image 注意你可能想知道为什么我们创建了/dev/sdf 设备,却在实际的操作系统上使用/dev/xvdf。这是因为如果设备是虚拟磁盘,较新的内核会对其进行重命名。

现在,我们创建一个新目录,它将用于挂载新创建的文件系统并更改所有权,以便 MySQL 进程能够写入卷:

# mkdir /mysql-db
# chown mysql.mysql /mysql-db
# mount LABEL=mysqlvol /mysql-db

配置 MySQL 实例

接下来,我们将配置 MySQL 实例。我们必须更改 MySQL 配置文件(位于/etc/my.cnf 中)的内容,以便 socket 文件和所有数据文件都存储在 EBS 卷上。这确保了数据在系统重新启动时不会丢失。MySQL 配置文件的新内容在清单 14-1 中给出。

清单 14-1 。将 MySQL 数据库指向新位置

[mysqld]
datadir=/mysql-db
socket=/mysql-db/mysql.sock
user=mysql
symbolic-links=0

[mysqld_safe]
log-error=/var/log/mysqld.log
pid-file=/var/run/mysqld/mysqld.pid

现在,让我们启动 MySQL 守护进程并设置默认密码。

Image 注意显然,你必须使用比我在这个例子中使用的更安全和不可预测的东西。

# chkconfig --levels 235 mysqld on
# service mysqld start
# mysqladmin -u root -S /mysql-db/mysql.sock password 'password'
# mysql -p -S /mysql-db/mysql.sock
[...]
mysql> grant all privileges on *.* to 'root'@'%' identified by 'password' with grant option;
Query OK, 0 rows affected (0.00 sec)

mysql> flush privileges;
Query OK, 0 rows affected (0.00 sec)

最后,我们关闭 MySQL 守护进程并卸载卷:

# service mysqld stop
# umount /mysql-db

捆绑新 AMI

一旦我们完成了所有的修改并且对运行的实例感到满意,我们就可以通过捆绑它来创建一个新的 AMI。

首先,我们必须在 Linux 实例上安装 AMI 工具。这些说明可能会随着时间的推移而略有变化,因此我们遵循这里描述的安装说明:docs . AWS . Amazon . com/AWS C2/latest/command line reference/set-up-ami-tools . html安装 AMI 工具包,以及docs . AWS . Amazon . com/AWS C2/latest/command line reference/set-up-ec2-cli-Linux . html安装 CLI 工具。

我们检查安装是否成功:

# ec2-ami-tools-version
1.5.3 20071010

Copyright 2008-2014 Amazon.com, Inc. or its affiliates.  All Rights Reserved.
Licensed under the Amazon Software License (the "License").  You may not use
this file except in compliance with the License. A copy of the License is
located at http://aws.amazon.com/asl or in the "license" file accompanying this
file.  This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
CONDITIONS OF ANY KIND, either express or implied. See the License for the
specific language governing permissions and limitations under the License.

# ec2-describe-regions
REGION  eu-west-1       ec2.eu-west-1.amazonaws.com
REGION  sa-east-1       ec2.sa-east-1.amazonaws.com
REGION  us-east-1       ec2.us-east-1.amazonaws.com
REGION  ap-northeast-1  ec2.ap-northeast-1.amazonaws.com
REGION  us-west-2       ec2.us-west-2.amazonaws.com
REGION  us-west-1       ec2.us-west-1.amazonaws.com
REGION  ap-southeast-1  ec2.ap-southeast-1.amazonaws.com
REGION  ap-southeast-2  ec2.ap-southeast-2.amazonaws.com

然后,我们必须准备我们的 X.509 证书文件,并用访问凭证设置一些环境变量。这些将在捆绑命令中使用,因此我们确保事先做好准备,以避免在运行命令时出现任何问题。从账户管理控制台(console.aws.amazon.com/iam/home?#security_credential ,我们创建一个新的 X.509 证书文件(证书文件和私钥)。我们在本地将下载的文件保存为 pk.pem(私钥)和 cert.pem(证书)。当我们拥有这两个文件时,我们将它们复制到正在运行的实例的/mnt/目录中。

我们返回到正在运行的 EC2 实例上的 shell 提示符,并将以下环境变量设置为适当的值,这些值可以从帐户管理 web 页面获得:

export AWS_USER=<12 digit account ID>
export AWS_ACCESS_KEY=<REST access key>
export AWS_SECRET_KEY=<REST secret access key>

我们现在准备捆绑正在运行的实例。我们发出下面的命令,并等待它完成。这是一个相当漫长的过程,可能需要 10 分钟:

# ec2-bundle-vol -u $AWS_USER -k /mnt/pk.pem -c /mnt/cert.crt -p CentOS-6.5-x86_64-mysql -r x86_64
Setting partition type to bundle "/" with...
Auto-detecting partition type for "/"
Partition label detected using parted: "loop"
Using partition type "none"
Copying / into the image file /tmp/CentOS-6.5-x86_64-mysql...
Excluding:
         /proc/sys/fs/binfmt_misc
         /sys
         /proc
         /dev/pts
         /dev
         /media
         /mnt
         /proc
         /sys
         /tmp/CentOS-6.5-x86_64-mysql
         /mnt/img-mnt

Image 注意 rsync 似乎成功了,但退出时出现错误代码 23。这可能意味着您的 rsync 版本是针对定义了 HAVE_LUTIMES 的内核构建的,尽管当前的内核没有启用该选项。因此,捆绑过程将忽略该错误并继续捆绑。如果捆绑成功完成,您的图像应该完全可用。然而,我们建议您安装一个版本的 rsync 来更好地处理这种情况。

Image file created: /tmp/CentOS-6.5-x86_64-mysql
Volume cloning done.
Bundling image file...
Splitting /tmp/CentOS-6.5-x86_64-mysql.tar.gz.enc...
Created CentOS-6.5-x86_64-mysql.part.00
Created CentOS-6.5-x86_64-mysql.part.01
[ . . . ]
Created CentOS-6.5-x86_64-mysql.part.35
Generating digests for each part...
Digests generated.
Unable to read instance meta-data for ancestor-ami-ids
Unable to read instance meta-data for ramdisk-id
Unable to read instance meta-data for product-codes
Creating bundle manifest...
Bundle manifest is /tmp/CentOS-6.5-x86_64-mysql.manifest.xml
ec2-bundle-vol complete.

一旦图像捆绑完成,我们必须上传到 S3 存储。以下命令中的–b 选项表示存储桶名称。如您所知,桶名在整个 S3 系统中必须是唯一的,所以我们要小心选择。我们不需要事先创建桶;如果存储桶不存在,将会为您创建一个存储桶。上传过程比捆绑过程稍微快一点,但是我们预计它也会花费相当多的时间:

# ec2-upload-bundle -b pro-python-system-administration -m /tmp/CentOS-6.5-x86_64-mysql.manifest.xml -a "$AWS_ACCESS_KEY" -s "$AWS_SECRET_KEY"
Uploading bundled image parts to the S3 bucket pro-python-system-administration ...
Uploaded CentOS-6.5-x86_64-mysql.part.00
[ . . . ]
Uploaded CentOS-6.5-x86_64-mysql.part.35
Uploading manifest ...
Uploaded manifest.
Manifest uploaded to: pro-python-system-administration/CentOS-6.5-x86_64-mysql.manifest.xml
Bundle upload completed.

最后,我们必须注册新创建的 AMI。一旦命令执行完毕,我们将得到 AMI ID 字符串提示。我们还将在您的私人 AMI 选择屏幕中看到新 AMI:

# ec2-register --name 'pro-python-system-administration/CentOS-6.5-x86_64-mysql' \
               pro-python-system-administration/CentOS-6.5-x86_64-mysql.manifest.xml \
               -K /mnt/pk.pem

IMAGE   ami-2a58a342

使用 Boto Python 模块控制 EC2

我们最终进入了创建代码来自动管理 EC2 实例的阶段。您可以使用 SOAP 或 REST API 来访问这些服务,但是您不必亲自动手,因为有许多不同的库可供使用。尽管缺少印刷文档,但该主题在互联网上有很好的文档记录,并且大多数流行的编程语言都可以使用这些库,如 Java、Ruby、C#、Perl,当然还有 Python。

用于访问 Amazon web 服务的最流行的 Python 库之一是 Boto 库。这个库提供了到以下 AWS 的接口:

  • 简单存储服务(S3)
  • 简单队列服务(SQS)
  • 弹性计算云(EC2)
  • 机械土耳其人
  • 简单数据库(SDB)
  • 内容推送服务
  • 虚拟专用云(VPC)

大多数 Linux 发行版上都有这个库。例如,在 Fedora 系统上,可以使用以下命令安装库:

$ sudo yum install python-boto

您也可以从项目主页github.com/boto/boto下载源代码。官方文件可在 docs.pythonboto.org/en/latest/[…](docs.pythonboto.org/en/latest/)

设置配置变量

将有两种类型的配置数据。特定于帐户的配置(REST API 访问键)并不特定于我们的应用,可以存储在名为。用户目录中的 boto。

该配置文件包含访问 ID 密钥和秘密访问密钥:

[Credentials]
aws_access_key_id = <Access key>
aws_secret_access_key = <Secret access key>

我们将在 backup.cfg 文件中存储特定于应用的配置,并通过使用 ConfigParser 库来访问它。文件的内容在下面的代码中描述:

[main]
volume_id=vol-7556353c           # the EBS volume ID which we mount to the EC2 DB instances
vol_device=/dev/sdf              # the name of the device of the attached volume
mount_dir=/mysql-db              # the name of the mount directory
image_id=ami-2a58a342            # the name of the custom created AMI image
key_name=<private key>           # the name of the key pair (and the pem file)
key_location=/home/rytis/EC2/    # the location of the key pair file
security_grp=database            # the name of the security group (with SSH and MySQL ports)

以编程方式初始化 EC2 实例

首先,让我们创建应用的框架结构。在清单 14-2 中,我们从创建 BackupManager 类开始。这个类将实现管理自定义 EC2 实例的方法。我们还设置了一个 logger 对象,用于记录应用的状态。

清单 14-2 。应用的结构

#!/usr/bin/env python

import sys
import logging
import time
import subprocess
import boto
import boto.ec2
from ConfigParser import SafeConfigParser
import MySQLdb
from datetime import datetime

CFG_FILE = 'backup.cfg'

class BackupManager:

    def __init__(self, cfg_file=CFG_FILE, logger=None):
        self.logger = logger
        self.config = SafeConfigParser()
        self.config.read(cfg_file)
        self.aws_access_key = boto.config.get('Credentials', 'aws_access_key_id')
        self.aws_secret_key = boto.config.get('Credentials',
                                              'aws_secret_access_key')
        self.ec2conn = boto.ec2.connection.EC2Connection(self.aws_access_key,
                                                         self.aws_secret_key)
        self.image = self.ec2conn.get_image(self.config.get('main', 'image_id'))
        self.volume = self.ec2conn.get_all_volumes([self.config.get('main',
                                                                'volume_id')])[0]
        self.reservation = None
        self.ssh_cmd = []
[...]
def main():
    console = logging.StreamHandler()
    logger = logging.getLogger('DB_Backup')
    logger.addHandler(console)
    logger.setLevel(logging.DEBUG)
    bck = BackupManager(logger=logger)

if __name__ == '__main__':
    main()

如您所见,在初始化过程中我们已经建立了与 AWS 的连接。EC2Connection()调用返回的结果是连接对象,我们将使用它来访问 AWS 系统。

self.ec2conn = boto.ec2.connection.EC2Connection(self.aws_access_key,
                                                 self.aws_secret_key)

例如,以下两个调用返回 AMI 映像和卷对象:

self.image = self.ec2conn.get_image(self.config.get('main', 'image_id'))
self.volume = self.ec2conn.get_all_volumes([self.config.get('main', 'volume_id')])[0]

这些对象中的每一个都公开了可用于控制它们的方法。例如,volume 对象实现 attach 方法,该方法可用于将特定卷附加到 EC2 实例。我们将在下面几节中发现最常用的方法。

启动 EC2 实例

我们的第一个任务是启动实例。这可以通过 run()方法来完成,该方法在我们前面创建的 image 对象中可用。

这个调用的结果是一个 reservation 对象,它列出了从这个调用开始的所有实例。目前,我们只启动了一个实例,但是您可以从同一个 AMI 映像启动多个实例。

run()方法需要设置两个参数:密钥对名称和安全组。我还指定了可选的 placement zone 参数,它指示实例需要在哪个 EC2 zone 中启动。我们并不真正关心该分区是什么,只要它是创建卷的同一个分区。您不能从不同的区域附加卷,因此实例必须在同一区域中运行。您可以通过检查卷对象的区域属性来发现卷的区域。

如您所知,该实例不会立即可用;因此,我们必须实现一个简单的循环,定期检查实例的状态,并等待直到它将状态更改为“正在运行”(见清单 14-3 )。

清单 14-3 。启动 EC2 实例

def _start_instance(self):
    self.logger.debug('Starting new instance...')
    self.reservation = self.image.run(key_name=self.config.get('main', 'key_name'),
                           security_groups=[self.config.get('main', 'security_grp')],
                           placement=self.volume.zone)
    instance = self.reservation.instances[0]
    while instance.state != u'running':
        time.sleep(60)
        instance.update()
        self.logger.debug("instance state: %s" % instance.state)
    self.logger.debug("Instance %s is running and available at %s" % (instance.id,
instance.public_dns_name))

连接 EBS 卷

实例运行后,我们可以将卷连接到它。如清单 14-4 所示,只需一个方法调用就可以附加这个卷。然而,有一个警告。即使您等待卷更改其状态以指示它已成功“连接”,您仍可能会发现设备未准备好。我发现通常多等 5 秒钟就足够了,但是为了安全起见,我们会再等 10 秒钟。

清单 14-4 。连接 EBS 卷

def _attach_volume(self, volume=None):
    if not volume:
        volume_to_attach = self.volume
    else:
        volume_to_attach = volume
    instance_id = self.reservation.instances[0].id
    self.logger.debug("Attaching volume %s to instance %s as %s" %
                                              (volume_to_attach.id,
                                              instance_id,
                                              self.config.get('main', 'vol_device')))
    volume_to_attach.attach(instance_id, self.config.get('main', 'vol_device'))
    while volume_to_attach.attachment_state() != u'attached':
        time.sleep(20)
        volume_to_attach.update()
        self.logger.debug("volume status: %s", volume_to_attach.attachment_state())
    time.sleep(10) # give it some extra time
                   # aws sometimes is mis-reporting the volume state
    self.logger.debug("Finished attaching volume")

安装 EBS 设备

卷已连接,但文件系统对操作系统尚不可见。不幸的是,没有 API 调用来挂载文件系统,因为这是操作系统的功能,Amazon WS 对此无能为力。

因此,我们必须使用 ssh 命令远程发出 mount 命令。建立远程通信链接的 ssh 命令总是相同的,所以我们使用清单 14-5 中的方法构建一个,并且我们将在每次需要在远程系统上发出操作系统命令时重用它。

清单 14-5 。构造 ssh 命令参数

def _init_remote_cmd_args(self):
    key_file = "%s/%s.pem" % (self.config.get('main', 'key_location'),
                              self.config.get('main', 'key_name'))
    remote_user = 'root'
    remote_host = self.reservation.instances[0].public_dns_name
    remote_resource = "%s@%s" % (remote_user, remote_host)
    self.ssh_cmd = ['ssh',
                    '-o', 'StrictHostKeyChecking=no',
                    '-i', key_file,
                    remote_resource]

我们必须使用 OpenSSH 选项 StrictHostKeyChecking=no,因为我们将连接到新主机,默认情况下,OpenSSH 会警告您它接收的主机密钥以前从未出现过。它还会要求确认是否接受远程键——这是您不希望在自动化系统中看到的行为。

一旦构造了默认的 ssh 参数字符串,我们就可以向正在运行的实例发出远程卷挂载命令,如清单 14-6 所示。

清单 14-6 。在远程主机上挂载文件系统

def _mount_volume(self):
    self.logger.debug("Mounting %s on %s" % (self.config.get('main', 'vol_device'),
                                             self.config.get('main', 'mount_dir')))
    remote_command = "mount %(dev)s %(mp)s && df -h %(mp)s" %    \
                                      {'dev': self.config.get('main', 'vol_device'),
                                       'mp': self.config.get('main', 'mount_dir')}
    rc = subprocess.call(self.ssh_cmd + [remote_command])
    self.logger.debug('done')

启动 MySQL 实例

正如我们对 mount 命令所做的那样,我们将使用相同的机制来启动和停止远程服务器上的 MySQL 守护进程。我们将使用标准的 Red Hat distribution/sbin/service 命令来运行初始化脚本,如清单 14-7 所示。

清单 14-7 。远程启动和停止 MySQL 守护进程

def _control_mysql(self, command):
    self.logger.debug("Sending MySQL DB daemon command to: %s" % command)
    remote_command = "/sbin/service mysqld %s; pgrep mysqld" % command
    rc = subprocess.call(self.ssh_cmd + [remote_command])
    self.logger.debug('done')

传输数据

此时,我们已经让远程系统准备好接受 MySQL 数据库连接。正如我们之前讨论过的,实际的数据传输和处理是特定的任务,没有通用的方法。通常,所涉及的步骤如下:

5.建立到本地数据库的连接。

6.建立到运行在 EC2 实例上的远程数据库的连接。

7.找出哪些本地数据还不存在于远程数据库中。

8.从本地数据库中读取记录集,并相应地更新远程数据库。

9.如果不需要,从本地数据库中删除旧数据。

10.通过在远程 EC2 实例上使用复杂的 SQL 查询或函数来执行任何统计计算。

不过话说回来,这个过程很大程度上取决于您的需求,所以我将把这个任务的实现留给您。在我们的示例应用中,我们将使用一个只等待一小段时间的虚拟函数:

def _copy_db(self):
    self.logger.debug('Backing up the DB...')
    time.sleep(60)

以编程方式销毁 EC2 实例

当我们完成远程数据库的更新和所有数据处理任务之后,我们就可以开始销毁 EC2 实例了。该实例将被销毁,但数据库卷将与其上的数据文件一起保留。作为辅助安全措施,我们还将创建该卷的快照。

关闭 MySQL 实例

我们从关闭 MySQL 数据库服务器开始。您已经熟悉了代码,如清单 14-7 中的所示。唯一的不同是,这一次,我们将把“stop”参数传递给方法调用。

卸载文件系统

当 MySQL 服务器没有运行时,我们可以安全地卸载文件系统。同样,我们将通过使用 ssh 连接机制发出 OS 命令来做到这一点,如清单 14-8 中的所示。

清单 14-8 。卸载文件系统

def _unmount_volume(self):
    self.logger.debug("Unmounting %s" % self.config.get('main', 'mount_dir'))
    remote_command = "sync; sync; umount %(mp)s; df -h %(mp)s" % \
                             {'mp':self.config.get('main', 'mount_dir')}
    rc = subprocess.call(self.ssh_cmd + [remote_command])
    self.logger.debug('done')

分离 EBS 卷

从技术上讲,此时不需要分离卷;一旦 EC2 实例终止,它就会自动分离。但是,我建议您首先分离这个卷(如清单 14-9 所示),因为如果 EC2 WS 行为改变,假设默认行为可能会在将来导致不必要的问题。

清单 14-9 。分离卷

def _detach_volume(self, volume=None):
    if not volume:
        volume_to_detach = self.volume
    else:
        volume_to_detach = volume
    self.logger.debug("Detaching volume %s" % volume_to_detach.id)
    volume_to_detach.detach()
    while volume_to_detach.attachment_state() == u'attached':
        time.sleep(20)
        volume_to_detach.update()
        self.logger.debug("volume status: %s", volume_to_detach.attachment_state())
    self.logger.debug('done')

拍摄卷的快照

分离卷后,我们将拍摄当前状态的快照。同样,它只是一个单一的方法调用。我们还将使用拍摄快照时的当前时间戳填充描述字段;参见清单 14-10 。

清单 14-10 。拍摄卷快照

def _create_snapshot(self, volume=None):
    if not volume:
        volume_to_snapshot = self.volume
    else:
        volume_to_snapshot = volume
    self.logger.debug("Taking a snapshot of %s" % volume_to_snapshot.id)
    volume_to_snapshot.create_snapshot(description="Snapshot created on %s" % \
                                                  datetime.isoformat(datetime.now()))
    self.logger.debug('done')

关闭实例

最后,我们将终止 EC2 实例。虽然没有必要,但我们将等待实例完全终止后再继续,如清单 14-11 所示。

清单 14-11 。终止正在运行的实例

def _terminate_instance(self):
        instance = self.reservation.instances[0]
        self.logger.debug("Terminating instance %s" % instance.id)
        instance.terminate()
        while instance.state != u'terminated':
            time.sleep(60)
            instance.update()
            self.logger.debug("instance state: %s" % instance.state)
        self.logger.debug('done')

控制序列

虽然我按照调用方法的顺序描述了这些方法,但是为了方便起见,下面是从主应用函数执行的方法调用的顺序:

def main():
    console = logging.StreamHandler()
    logger = logging.getLogger('DB_Backup')
    logger.addHandler(console)
    logger.setLevel(logging.DEBUG)
    bck = BackupManager(logger=logger)
    bck._start_instance()
    bck._init_remote_cmd_args()
    bck._attach_volume()
    bck._mount_volume()
    bck._control_mysql('start')
    bck._copy_db()
    bck._control_mysql('stop')
    bck._unmount_volume()
    bck._detach_volume()
    bck._create_snapshot()
    bck._terminate_instance()

正在运行的应用的示例输出如下。请注意,第二个 df 命令的输出显示了不同的装载点和不同的设备,因为 EBS 卷上的文件系统已被成功卸载。

# ./db_backup.py
Starting new instance...
instance state: running
Instance i-02139929 is running and available at ec2-54-90-194-188.compute-1.amazonaws.com
Attaching volume vol-7556353c to instance i-02139929 as /dev/xvdf
volume status: attached
Finished attaching volume
Mounting /dev/xvdf on /mysql-db
Warning: Permanently added 'ec2-54-90-194-188.compute-1.amazonaws.com,54.90.194.188' (RSA) to the list of known hosts.
Filesystem      Size  Used Avail Use% Mounted on
/dev/xvdf       9.9G  172M  9.2G   2% /mysql-db
done
Sending MySQL DB daemon command to: start
Starting mysqld:  [  OK  ]
1063
1165
done
Backing up the DB...
Sending MySQL DB daemon command to: stop
Stopping mysqld:  [  OK  ]
done
Unmounting /mysql-db
Filesystem      Size  Used Avail Use% Mounted on
/dev/xvda       9.9G  1.3G  8.2G  13% /
done
Detaching volume vol-7556353c
volume status: None
done
Taking a snapshot of vol-7556353c
done
Terminating instance i-02139929
instance state: terminated
done

摘要

在这一章中,我们研究了亚马逊网络服务(AWS ),以及如何使用简单存储系统(S3)和弹性计算云(EC2)来执行临时计算任务。除了按需计算任务之外,您还了解了如何对重要数据执行远程备份。我们在本章中构建的简单应用可以作为在虚拟计算云上构建您自己的数据仓库的基础。记住本章的这些要点:

  • EC2 和 S3 主要是被设计成可编程控制的 web 服务。
  • 主要的 S3 组件是数据对象和包含它们的存储桶。
  • Amazon 机器映像(ami)被用作启动 EC2 实例的模板。
  • EC2 实例是实际运行的虚拟机。
  • 您可以使用 Python Boto 库来控制大多数 AWS 服务。