Skip to content

Latest commit

 

History

History
834 lines (592 loc) · 66 KB

File metadata and controls

834 lines (592 loc) · 66 KB

六、部署代码

即使是完美的代码(如果存在的话)如果不运行也没有用。因此,为了达到某种目的,我们的代码需要安装在目标机器(计算机)上并执行。使应用程序或服务的特定版本可供最终用户使用的过程称为部署。

对于桌面应用程序,这似乎很简单。如果需要,您的工作将结束于提供一个带有可选安装程序的可下载包。用户有责任在其环境中下载并安装它。您的责任是使此过程尽可能简单和方便。正确的包装仍然不是一项简单的任务,但是一些工具已经在前一章中解释过了。

令人惊讶的是,当代码本身不是产品时,事情会变得更加复杂。如果您的应用程序只提供向用户出售的服务,那么您有责任在自己的基础架构上运行它。此场景是 web 应用程序或任何“X 即服务”产品的典型场景。在这种情况下,部署代码是为了启动开发人员通常很难物理访问的远程机器。如果您已经是云计算服务的用户,如亚马逊网络****服务AWS或 Heroku),则尤其如此。

在本章中,我们将集中讨论远程主机的代码部署方面,因为 Python 在构建各种 web 相关服务和产品方面非常流行。尽管这种语言具有很高的可移植性,但它没有使代码易于部署的特定质量。最重要的是如何构建应用程序以及使用什么流程将其部署到目标环境。因此,本章将重点讨论以下主题:

  • 将代码部署到远程环境的主要挑战是什么
  • 如何用 Python 构建易于部署的应用程序
  • 如何在不停机的情况下重新加载 web 服务
  • 如何在代码部署中利用 Python 打包生态系统
  • 如何正确监控和检测远程运行的代码

十二要素 App

无痛部署的主要要求是以一种确保此过程尽可能简单和精简的方式构建应用程序。这主要是为了消除障碍和鼓励行之有效的做法。在只有特定人员负责开发(开发人员团队或开发人员简称)和不同人员负责部署和维护执行环境(操作团队或 Ops 简称)的组织中,遵循这些常见做法尤其重要。

所有与服务器维护、监视、部署、配置等相关的任务通常都放在一个称为操作的包中。即使在没有独立团队执行操作任务的组织中,通常也只有一些开发人员有权执行部署任务和维护远程服务器。这种职位的通用名称是 DevOps。另外,开发团队的每个成员都负责操作,这并不是一种不寻常的情况,所以这样一个团队中的每个人都可以称为 DevOps。无论如何,无论您的组织结构如何,每个开发人员的职责是什么,每个人都应该知道操作是如何工作的,以及代码是如何部署到远程服务器的,因为最终,执行环境及其配置是您正在构建的产品的一个隐藏部分。

以下常见做法和惯例之所以重要,主要原因如下:

  • 在每一家公司,人们都会辞职,并雇用新员工。通过使用最佳方法,您可以使新的团队成员更容易地加入项目。您永远无法确定新员工是否已经熟悉系统配置和以可靠方式运行应用程序的常见做法,但您至少让他们更容易快速适应。
  • 在只有一些人负责部署的组织中,它只是减少了操作和开发团队之间的摩擦。

鼓励构建易于部署的应用程序的此类实践的一个良好来源是一份名为十二要素应用程序的宣言。它是构建软件即服务应用程序的通用语言不可知方法。其目的之一是使应用程序更易于部署,但它也强调了其他主题,如可维护性和使应用程序更易于扩展。

顾名思义,十二要素应用程序由 12 条规则组成:

  • 代码库:版本控制中跟踪一个代码库,多个部署
  • 依赖项:显式声明和隔离依赖项
  • 配置:在环境中存储配置
  • 支持服务:将支持服务视为附加资源
  • 构建、发布、运行:严格区分构建和运行阶段
  • 进程:将应用程序作为一个或多个无状态进程执行
  • 端口绑定:通过端口绑定导出服务
  • 并发:通过流程模型向外扩展
  • 可处置性:最大化鲁棒性,快速启动,优雅关闭
  • 开发/生产对等:保持开发、阶段和生产尽可能相似
  • 日志:将日志视为事件流
  • 管理流程:将管理/管理任务作为一次性流程运行

在这里扩展这些规则都有点毫无意义,因为十二因素应用程序方法学的官方页面(http://12factor.net/ 包含每个应用程序因素的广泛理由,以及针对不同框架和环境的工具示例。

本章试图与上述宣言保持一致,因此我们将在必要时详细讨论其中一些宣言。所介绍的技术和示例有时可能与这 12 个因素略有不同,但请记住,这些规则并非一成不变的。只要他们能达到目的,他们就是伟大的。最后,重要的是工作应用程序(产品)与某些任意方法不兼容。

使用 Fabric 实现部署自动化

对于非常小的项目,可以“手动”部署代码,也就是说,通过远程 shell 手动键入安装新版本代码并在远程 shell 上执行所需的命令序列。无论如何,即使对于一个一般规模的项目来说,这也是一个容易出错、乏味的过程,应该被认为是浪费了你拥有的最宝贵的资源,你自己的时间。

解决这个问题的办法是自动化。简单的经验法则是,如果您需要手动执行同一任务至少两次,您应该将其自动化,这样您就不需要第三次执行。有多种工具可以让您自动化不同的事情:

  • 远程执行工具(如 Fabric)用于在多个远程主机上按需自动执行代码。
  • 配置管理工具(如 Chef、Puppet、CFEngine、Salt 和 Ansible)是为远程主机(执行环境)的自动化配置而设计的。它们可用于设置备份服务(数据库、缓存等)、系统权限、用户等。它们中的大多数还可以用作远程执行的工具,如 Fabric,但根据它们的体系结构,这可能或多或少比较容易。

配置管理解决方案是一个复杂的主题,值得另写一本书。事实是,最简单的远程执行框架具有最低的入门门槛,并且是最流行的选择,至少对于小型项目来说是如此。事实上,每个提供声明式指定机器配置方法的配置管理工具都在其内部深处实现了一个远程执行层。

此外,根据某些工具的设计,它可能并不最适合实际的自动化代码部署。Puppet 就是这样一个例子,它实际上不鼓励显式运行任何 shell 命令。这就是为什么许多人选择使用这两种解决方案来相互补充:用于设置系统级环境的配置管理和用于应用程序部署的按需远程执行。

织物(http://www.fabfile.org/ 是迄今为止 Python 开发人员用于自动化远程执行的最流行的解决方案。它是一个 Python 库和命令行工具,用于简化应用程序部署或系统管理任务中 SSH 的使用。我们将重点关注它,因为它相对容易启动。请注意,根据您的需要,它可能不是解决您问题的最佳方案。无论如何,这是一个很好的实用程序示例,它可以为您的操作添加一些自动化,如果您还没有。

提示

织物与蟒蛇 3

本书鼓励您只使用 Python3(如果可能的话)进行开发,并对较旧的语法特性和兼容性注意事项进行说明,以使最终的版本切换更加轻松。不幸的是,在撰写本书时,Fabric 还没有正式移植到 Python3。该工具的爱好者们至少在几年前就被告知,正在进行的 Fabric 2 开发将带来兼容性更新。据说这是一次完全的重写,有很多新特性,但 Fabric 2 没有正式的开放存储库,几乎没有人看过它的代码。在本项目的当前开发分支中,核心结构开发人员不接受任何关于 Python3 兼容性的请求,并关闭每个特性请求。这种开发流行开源项目的方法充其量只是令人不安。这个问题的历史并没有给我们很快看到 Fabric 2 正式发布的机会。这种新面料发布的秘密开发引发了许多问题。

不管任何人的意见如何,这一事实都不会降低织物在当前状态下的实用性。因此,如果您已经决定坚持使用 Python3,那么有两种选择:使用完全兼容且独立的 fork(https://github.com/mathiasertl/fabric/ )或用 Python 3 编写应用程序,并用 Python 2 维护结构脚本。最好的方法是在一个单独的代码库中完成。

当然,您可以仅使用 Bash 脚本自动化所有工作,但这非常繁琐且容易出错。Python 有更方便的字符串处理方法,并鼓励代码模块化。Fabric 实际上只是一个通过 SSH 粘合命令执行的工具,因此仍然需要一些关于命令行界面及其实用程序在您的环境中如何工作的知识。

要开始使用 Fabric,您需要安装fabric包(使用pip,并创建一个名为fabfile.py的脚本,该脚本通常位于项目的根目录中。请注意,fabfile可以被视为项目配置的部分。因此,如果您想严格遵循十二要素应用程序方法,您应该不要在已部署应用程序的源代码树中维护其代码。事实上,复杂的项目通常是从作为独立代码库维护的各种组件构建的,因此,为所有项目组件配置和结构脚本创建一个单独的存储库是一个好方法的另一个原因。这使得不同服务的部署更加一致,并鼓励良好的代码重用。

定义简单部署过程的示例fabfile如下所示:

# -*- coding: utf-8 -*-
import os

from fabric.api import *  # noqa
from fabric.contrib.files import exists

# Let's assume we have private package repository created
# using 'devpi' project
PYPI_URL = 'http://devpi.webxample.example.com'

# This is arbitrary location for storing installed releases.
# Each release is a separate virtual environment directory
# which is named after project version. There is also a
# symbolic link 'current' that points to recently deployed
# version. This symlink is an actual path that will be used
# for configuring the process supervision tool e.g.:
# .
# ├── 0.0.1
# ├── 0.0.2
# ├── 0.0.3
# ├── 0.1.0
# └── current -> 0.1.0/

REMOTE_PROJECT_LOCATION = "/var/projects/webxample"

env.project_location = REMOTE_PROJECT_LOCATION

# roledefs map out environment types (staging/production)
env.roledefs = {
    'staging': [
        'staging.webxample.example.com',
    ],
    'production': [
        'prod1.webxample.example.com',
        'prod2.webxample.example.com',
    ],
}

def prepare_release():
    """ Prepare a new release by creating source distribution and uploading to out private package repository
    """
    local('python setup.py build sdist upload -r {}'.format(
        PYPI_URL
    ))

def get_version():
    """ Get current project version from setuptools """
    return local(
        'python setup.py --version', capture=True
    ).stdout.strip()

def switch_versions(version):
    """ Switch versions by replacing symlinks atomically """
    new_version_path = os.path.join(REMOTE_PROJECT_LOCATION, version)
    temporary = os.path.join(REMOTE_PROJECT_LOCATION, 'next')
    desired = os.path.join(REMOTE_PROJECT_LOCATION, 'current')

    # force symlink (-f) since probably there is a one already
    run(
        "ln -fsT {target} {symlink}"
        "".format(target=new_version_path, symlink=temporary)
    )
    # mv -T ensures atomicity of this operation
    run("mv -Tf {source} {destination}"
        "".format(source=temporary, destination=desired))

@task
def uptime():
    """
    Run uptime command on remote host - for testing connection.
    """
    run("uptime")

@task
def deploy():
    """ Deploy application with packaging in mind """
    version = get_version()
    pip_path = os.path.join(
        REMOTE_PROJECT_LOCATION, version, 'bin', 'pip'
    )

    prepare_release()

    if not exists(REMOTE_PROJECT_LOCATION):
        # it may not exist for initial deployment on fresh host
        run("mkdir -p {}".format(REMOTE_PROJECT_LOCATION))

    with cd(REMOTE_PROJECT_LOCATION):
        # create new virtual environment using venv
        run('python3 -m venv {}'.format(version))

        run("{} install webxample=={} --index-url {}".format(
            pip_path, version, PYPI_URL
        ))

    switch_versions(version)
    # let's assume that Circus is our process supervision tool
    # of choice.
    run('circusctl restart webxample')

每个用@task修饰的函数都被视为包提供的fab实用程序的可用子命令。您可以使用-l--list开关列出所有可用的子命令:

$ fab --list
Available commands:

 deploy  Deploy application with packaging in mind
 uptime  Run uptime command on remote host - for testing connection.

现在,只需一个 shell 命令即可将应用程序部署到给定的环境类型:

$ fabR production deploy

注意,前面的fabfile仅用于说明目的。在您自己的代码中,您可能希望提供广泛的故障处理,并尝试在不需要重新启动 web worker 进程的情况下重新加载应用程序。此外,这里介绍的一些技术现在可能很明显,但将在本章后面进行解释。这些是:

  • 使用专用包存储库部署应用程序
  • 在远程主机上使用 Circus 进行进程监视

您自己的包装索引或索引镜像

您可能希望运行自己的 Python 包索引有三个主要原因:

  • 官方的 Python 包索引没有任何可用性保证。它由 Python 软件基金会运行,由于大量捐赠。正因为如此,这往往意味着这个网站可以关闭。由于 PYPI 中断,您不希望在中间停止部署或打包过程。

  • 即使对于永远不会公开发布的封闭源代码,用 Python 编写的可重用组件也要进行适当的打包。它简化了代码库,因为跨公司用于不同项目的包不需要出售。您只需从存储库安装它们。这简化了对这些共享代码的维护,并且如果公司有许多团队在不同的项目上工作,可能会降低整个公司的开发成本。

  • It is very good practice to have your entire project packaged using setuptools. Then, deployment of the new application version is often as simple as running pip install --update my-application.

    提示

    代码销售

    代码销售是一种实践,将外部包的源代码包括在其他项目的源代码(存储库)中。当项目的代码依赖于某个外部包的特定版本时,通常会执行此操作,而其他包可能也需要此版本(并且是完全不同的版本)。例如,流行的requests包供应商在其源代码树中提供了urllib3的某些版本,因为它与之紧密耦合,也不太可能与urllib3的任何其他版本一起工作。six是一个特别经常被他人出售的模块示例。它可以在众多流行项目的来源中找到,如 Django(django.utils.six)、Boto(boto.vedored.six)或 Matplotlib(matplotlib.externals.six)。

    尽管一些大型成功的开源项目也在进行销售,但如果可能的话应该避免。这只有在某些情况下才有合理的用途,不应被视为包依赖关系管理的替代品。

PyPI 镜像

PyPI 中断的问题可以通过允许安装工具从其中一个镜像下载包来缓解。事实上,官方 Python 包索引已经通过CDN内容交付网络)提供,因此它本质上是镜像的。这并没有改变这样一个事实,即当任何下载包的尝试失败时,似乎有时会有一些糟糕的日子。使用非官方镜像在这里不是一个解决方案,因为它可能会引起一些安全问题。

最好的解决方案是拥有您自己的 PyPI 镜像,它将包含您需要的所有包。唯一会使用它的一方是您,因此确保适当的可用性要容易得多。另一个优点是,每当这项服务出现问题时,您不需要依赖其他人来解决。PyPA 维护和推荐的镜像工具为bandersnatchhttps://pypi.python.org/pypi/bandersnatch )。它允许您镜像 Python 包索引的全部内容,并且可以作为.pypirc文件中存储库部分的index-url选项提供(如前一章所述)。此镜像不接受上载,并且没有 PyPI 的 web 部件。无论如何,当心!一个完整的镜像可能需要数百 GB 的存储空间,并且随着时间的推移,其大小将继续增长。

但是,既然我们有更好的选择,为什么还要停在一面简单的镜子上呢?有一个非常小的机会,你将需要一个镜像的整个包索引。即使一个项目有数百个依赖项,它也只是所有可用包的一小部分。此外,不能上传你自己的私人软件包是这样一个简单镜像的巨大限制。使用 bandersnatch 的附加值似乎很低,但价格却很高。这在大多数情况下都是正确的。如果包镜像仅针对少数项目中的单个项目进行维护,则更好的方法是使用devpi)http://doc.devpi.net/ )。它是一个与 PyPI 兼容的包索引实现,提供以下两种功能:

  • 用于上载非公共包的专用索引
  • 索引镜像

与 bandersnatch 相比,devpi 的主要优势在于它如何处理镜像。当然,它可以像 bandersnatch 那样对其他索引进行完全的通用镜像,但这不是它的默认行为。它没有对整个存储库进行相当昂贵的备份,而是为客户端已经请求的包维护镜像。因此,每当安装工具(pipsetuptoolseasyinstall请求包时,如果本地镜像中不存在该包,则 devpi 服务器将尝试从镜像索引(通常为 PyPI)下载该包并提供服务。下载包后,devpi 将定期检查其更新,以保持镜像的新状态。

当您请求尚未镜像的新包且上游包索引中断时,镜像方法会留下轻微的失败风险。无论如何,由于在大多数部署中,您将只依赖于已在索引中镜像的包,因此这种风险降低了。已请求的包的镜像状态最终与 PyPI 一致,新版本将自动下载。这似乎是一个非常合理的权衡。

使用包部署

现代 web 应用程序有很多依赖项,通常需要很多步骤才能在远程主机上正确安装。例如,远程主机上应用程序新版本的典型引导过程包括以下步骤:

  • 为隔离创建新的虚拟环境
  • 将项目代码移动到执行环境
  • 安装最新的项目要求(通常来自requirements.txt文件)
  • 同步或迁移数据库架构
  • 将项目源和外部包中的静态文件收集到所需位置
  • 为不同语言的应用程序编译本地化文件

对于更复杂的站点,可能会有很多其他任务,主要与前端代码有关:

  • 使用诸如 SASS 或更少的预处理器生成 CSS 文件
  • 执行静态文件(JavaScript 和 CSS 文件)的缩小、模糊和/或连接
  • 将用 JavaScript 超集语言(CoffeeScript、TypeScript 等)编写的代码编译为原生 JS
  • 预处理响应模板文件(缩小、样式内联等)

所有这些步骤都可以使用 Bash、Fabric 或 Ansible 等工具轻松实现自动化,但在安装应用程序的远程主机上执行所有操作并不是一个好主意。原因如下:

  • 处理静态资产的一些流行工具可能是 CPU 密集型或内存密集型。在生产环境中运行它们可能会影响应用程序的执行。
  • 这些工具通常需要额外的系统依赖性,而这些系统依赖性可能不是项目正常运行所必需的。这些主要是额外的运行时环境,如 JVM、Node 或 Ruby。这增加了配置管理的复杂性,并增加了总体维护成本。
  • 如果要将应用程序部署到多个服务器(十分之一、百分之一百、数千),只需重复许多可以一次性完成的工作即可。如果您有自己的基础架构,那么您可能不会经历成本的大幅增加,尤其是在流量较低的时期执行部署时。但是,如果您在定价模型中运行云计算服务,而这种定价模型会因负载峰值或执行时间而向您收取额外费用,那么在适当的范围内,这种额外的成本可能是巨大的。
  • 这些步骤中的大多数只需要花费大量的时间。您正在远程服务器上安装代码,因此,您最不希望的事情是由于某些网络问题而中断连接。通过保持部署过程的快速,可以降低部署中断的可能性。

由于明显的原因,上述部署步骤的结果不能包含在应用程序代码存储库中。简单地说,每一次发布都必须做一些事情,你不能改变这一点。这显然是一个实现适当自动化的地方,但关键是在正确的时间和地点进行自动化。

大多数事情(如静态收集和代码/资产预处理)都可以在本地或专用环境中完成,因此部署到远程服务器的实际代码只需要少量的现场处理。在构建分发版或安装包的过程中,最值得注意的部署步骤包括:

  • 安装 Python 依赖项和将静态资产(CSS 文件和 JavaScript)传输到所需位置可以作为setup.py脚本的install命令的一部分来处理
  • 预处理代码(处理 JavaScript 超集、缩小/混淆/连接资产、运行 SASS 或更少)和本地化文本编译(例如,Django 中的compilemessages)等可以是setup.py脚本的sdist/bdist命令的一部分

使用适当的MANIFEST.in文件可以轻松处理 Python 以外的预处理代码。依赖项当然最好作为来自setuptools包的setup()函数调用的install_requires参数提供。

打包整个应用程序当然需要您做一些额外的工作,比如提供您自己的自定义setuptools命令或覆盖现有的命令,但这会给您带来很多好处,并使项目部署更快、更可靠。

让我们以一个基于 Django 的项目(在 Django 1.9 版本中)为例。我之所以选择这个框架,是因为它似乎是这种类型中最流行的 Python 项目,所以很有可能您已经对它有所了解。此类项目中的典型文件结构可能如下所示:

$ tree . -I __pycache__ --dirsfirst
.
├── webxample
│   ├── conf
│   │   ├── __init__.py
│   │   ├── settings.py
│   │   ├── urls.py
│   │   └── wsgi.py
│   ├── locale
│   │   ├── de
│   │   │   └── LC_MESSAGES
│   │   │       └── django.po
│   │   ├── en
│   │   │   └── LC_MESSAGES
│   │   │       └── django.po
│   │   └── pl
│   │       └── LC_MESSAGES
│   │           └── django.po
│   ├── myapp
│   │   ├── migrations
│   │   │   └── __init__.py
│   │   ├── static
│   │   │   ├── js
│   │   │   │   └── myapp.js
│   │   │   └── sass
│   │   │       └── myapp.scss
│   │   ├── templates
│   │   │   ├── index.html
│   │   │   └── some_view.html
│   │   ├── __init__.py
│   │   ├── admin.py
│   │   ├── apps.py
│   │   ├── models.py
│   │   ├── tests.py
│   │   └── views.py
│   ├── __init__.py
│   └── manage.py
├── MANIFEST.in
├── README.md
└── setup.py

15 directories, 23 files

请注意,这与通常的 Django 项目模板略有不同。默认情况下,包含 WSGI 应用程序、设置模块和 URL 配置的包与项目具有相同的名称。因为我们决定采用包装方式,所以将其命名为webxample。这可能会造成一些混乱,因此最好将其重命名为conf

在不深入了解可能的实施细节的情况下,我们只做几个简单的假设:

  • 我们的示例应用程序具有一些外部依赖项。在这里,它将是两个流行的 Django 包:djangorestframeworkdjango-allauth,再加上一个非 Django 包:gunicorn
  • djangorestframeworkdjango-allauthwebexample.webexample.settings模块中作为INSTALLED_APPS提供。
  • 该应用程序使用三种语言(德语、英语和波兰语)进行本地化,但我们不希望将编译后的gettext消息存储在存储库中。
  • 我们已经厌倦了普通的 CSS 语法,所以我们决定使用更强大的 SCSS 语言,并使用 SASS 将其转换为 CSS。

了解项目的结构,我们可以用一种让setuptools处理的方式编写setup.py脚本:

  • 编制webxample/myapp/static/scss下的 SCSS 文件
  • webexample/localegettext报文由.po格式编译为.mo格式
  • 安装要求
  • 一个新的脚本提供了一个包的入口点,因此我们将使用自定义命令而不是manage.py脚本

我们在这里有点运气。SASS 引擎的 C/C++端口libsass的 Python 绑定提供了与setuptoolsdistutils的少量集成。只需很少的配置,它就提供了一个自定义的setup.py命令来运行 SASS 编译:

from setuptools import setup

setup(
    name='webxample',
    setup_requires=['libsass >= 0.6.0'],
    sass_manifests={
        'webxample.myapp': ('static/sass', 'static/css')
    },
)

因此,我们不必手动运行sass命令,也不必在setup.py脚本中执行子流程,而可以键入python setup.py build_scss并将我们的 SCSS 文件编译为 CSS。这还不够。它使我们的生活变得更轻松,但我们希望整个发行版完全自动化,因此创建新发行版只有一个步骤。为了实现这一目标,我们不得不覆盖一些现有的setuptools分发命令。

通过打包处理一些项目准备步骤的示例setup.py文件可能看起来像:

import os

from setuptools import setup
from setuptools import find_packages
from distutils.cmd import Command
from distutils.command.build import build as _build

try:
    from django.core.management.commands.compilemessages \
        import Command as CompileCommand
except ImportError:
    # note: during installation django may not be available
    CompileCommand = None

# this environment is requires
os.environ.setdefault(
    "DJANGO_SETTINGS_MODULE", "webxample.conf.settings"
)

class build_messages(Command):
    """ Custom command for building gettext messages in Django
    """
    description = """compile gettext messages"""
    user_options = []

    def initialize_options(self):
        pass

    def finalize_options(self):

        pass

    def run(self):
        if CompileCommand:
            CompileCommand().handle(
                verbosity=2, locales=[], exclude=[]
            )
        else:
            raise RuntimeError("could not build translations")

class build(_build):
    """ Overriden build command that adds additional build steps
    """
    sub_commands = [
        ('build_messages', None),
        ('build_sass', None),
    ] + _build.sub_commands

setup(
    name='webxample',
    setup_requires=[
        'libsass >= 0.6.0',
        'django >= 1.9.2',
    ],
    install_requires=[
        'django >= 1.9.2',
        'gunicorn == 19.4.5',
        'djangorestframework == 3.3.2',
        'django-allauth == 0.24.1',
    ],
    packages=find_packages('.'),
    sass_manifests={
        'webxample.myapp': ('static/sass', 'static/css')
    },
    cmdclass={
        'build_messages': build_messages,
        'build': build,
    },
    entry_points={
        'console_scripts': {
            'webxample = webxample.manage:main',
        }
    }
)

通过这种实现,我们可以使用以下单终端命令为webxample项目构建所有资产并创建包的源分发:

$ python setup.py build sdist

如果您已经拥有自己的包索引(使用devpi创建),您可以添加install子命令或使用twine,以便此包可以在您的组织中与pip一起安装。如果我们查看使用setup.py脚本创建的源代码分发结构,我们可以看到它包含编译的gettext消息和从 SCSS 文件生成的 CSS 样式表:

$ tar -xvzf dist/webxample-0.0.0.tar.gz 2> /dev/null
$ tree webxample-0.0.0/ -I __pycache__ --dirsfirst
webxample-0.0.0/
├── webxample
│   ├── conf
│   │   ├── __init__.py
│   │   ├── settings.py
│   │   ├── urls.py
│   │   └── wsgi.py
│   ├── locale
│   │   ├── de
│   │   │   └── LC_MESSAGES
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── en
│   │   │   └── LC_MESSAGES
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   └── pl
│   │       └── LC_MESSAGES
│   │           ├── django.mo
│   │           └── django.po
│   ├── myapp
│   │   ├── migrations
│   │   │   └── __init__.py
│   │   ├── static
│   │   │   ├── css
│   │   │   │   └── myapp.scss.css
│   │   │   └── js
│   │   │       └── myapp.js
│   │   ├── templates
│   │   │   ├── index.html
│   │   │   └── some_view.html
│   │   ├── __init__.py
│   │   ├── admin.py
│   │   ├── apps.py
│   │   ├── models.py
│   │   ├── tests.py
│   │   └── views.py
│   ├── __init__.py
│   └── manage.py
├── webxample.egg-info
│   ├── PKG-INFO
│   ├── SOURCES.txt
│   ├── dependency_links.txt
│   ├── requires.txt
│   └── top_level.txt
├── MANIFEST.in
├── PKG-INFO
├── README.md
├── setup.cfg
└── setup.py

16 directories, 33 files

使用这种方法的另一个好处是,我们能够为项目提供自己的入口点,而不是 Django 的默认manage.py脚本。现在,我们可以使用此入口点运行任何 Django 管理命令,例如:

$ webxample migrate
$ webxample collectstatic
$ webxample runserver

这需要对manage.py脚本稍作修改,以与setup()中的entry_points参数兼容,因此其代码的主要部分用main()函数调用包装:

#!/usr/bin/env python3
import os
import sys

def main():
    os.environ.setdefault(
        "DJANGO_SETTINGS_MODULE", "webxample.conf.settings"
    )

    from django.core.management import execute_from_command_line

    execute_from_command_line(sys.argv)

if __name__ == "__main__":
    main()

不幸的是,很多框架(包括 Django)在设计时都没有考虑到以这种方式打包项目。这意味着,根据应用程序的进展,将其转换为软件包可能需要进行大量更改。在 Django 中,这通常意味着重写许多隐式导入,并更新设置文件中的许多配置变量。

另一个问题是使用 Python 打包创建的版本的一致性。如果不同的团队成员被授权创建应用程序分发,那么在相同的可复制环境中进行此过程是至关重要的,尤其是当您进行大量资产预处理时;在两个不同环境中创建的包可能看起来不一样,即使是从相同的代码库创建的。这可能是由于构建过程中使用的工具版本不同。最佳实践是将分销责任转移到持续集成/交付系统,如 Jenkins 或 Buildbot。另外一个优点是,您可以断言包在进入发行版之前通过了所有必需的测试。您甚至可以将自动化部署作为此类连续交付系统的一部分。

尽管如此,使用setuptools以 Python 包的形式分发代码并不是简单和轻松的;它将大大简化您的部署,因此绝对值得尝试。请注意,这也符合十二要素应用程序中第六条规则的详细建议:将应用程序作为一个或多个无状态进程执行(http://12factor.net/processes )。

共同惯例和惯例

有一套常见的部署约定和实践,并非每个开发人员都知道,但对于在他们的生活中做过一些操作的人来说,这是显而易见的。正如简介一章中所解释的,即使您不负责代码部署和操作,也必须至少了解其中的一些,因为这将允许您在开发过程中做出更好的设计决策。

文件系统层次结构

您可能想到的最明显的约定可能是关于文件系统层次结构和用户命名的。如果你在这里寻找这样的建议,那么你会失望的。当然有一个文件系统层次结构标准,它定义了 Unix 和类 Unix 操作系统中的目录结构和目录内容,但很难找到一个真正的操作系统发行版完全符合 FHS。如果系统设计人员和程序员不能遵守这些标准,那么很难期望其管理员遵守这些标准。根据我的经验,我看到应用程序代码几乎部署在任何可能的地方,包括根文件系统级别的非标准自定义目录。几乎一直以来,这些决策背后的人都有很强的理由支持这样做。关于这件事,我能给你的唯一建议如下:

  • 明智地选择,避免意外
  • 在项目的所有可用基础架构中保持一致
  • 在整个组织(你工作的公司)内保持一致

真正有帮助的是记录项目的约定。请记住,确保每个感兴趣的团队成员都可以访问此文档,并且每个人都知道这样的文档存在。

隔离

隔离的原因以及推荐的工具已经在第 1 章Python 的当前状态中讨论过。对于部署,只有一件重要的事情需要添加。您应该始终隔离应用程序每个版本的项目依赖关系。实际上,这意味着无论何时部署应用程序的新版本,都应该为此版本创建一个新的隔离环境(使用virtualenvvenv。旧环境应该在主机上保留一段时间,以便在出现问题时可以轻松地回滚到应用程序的旧版本之一。

为每个版本创建新的环境有助于管理它们的干净状态,并符合提供的依赖项列表。所谓新环境,我们指的是在文件系统中创建一个新的目录树,而不是更新已有的文件。不幸的是,这可能会使执行诸如优雅地重新加载服务之类的操作变得有点困难,如果环境更新到位,这将更容易实现。

使用过程监控工具

远程服务器上的应用程序通常不会退出。如果是 web 应用程序,则其 HTTP 服务器进程将无限期地等待新的连接和请求,并且只有在出现无法恢复的错误时才会退出。

当然,不可能在 shell 中手动运行并拥有一个永无止境的 SSH 连接。使用nohupscreentmux半守护进程不是一个选项。这样做就像设计服务失败一样。

您需要的是有一些流程监控工具,可以启动和管理您的应用程序流程。在选择正确的选项之前,您需要确保:

  • 如果服务退出,则重新启动该服务
  • 可靠地跟踪其状态
  • 捕获其stdout/stderr流以用于日志记录
  • 使用特定的用户/组权限运行进程
  • 配置系统环境变量

大多数 Unix 和 Linux 发行版都有一些用于过程监控的内置工具/子系统,例如initd脚本、upstartrunit。不幸的是,在大多数情况下,它们并不适合运行用户级应用程序代码,并且很难维护。尤其是编写可靠的init.d脚本是一个真正的挑战,因为它需要大量的 Bash 脚本,而这些脚本很难正确完成。一些 Linux 发行版(如 Gentoo)对init.d脚本采用了重新设计的方法,因此编写它们要容易得多。无论如何,仅仅为了一个进程监控工具的目的而将自己锁定到一个特定的 OS 发行版并不是一个好主意。

Python 社区中管理应用程序流程的两个流行工具是 Supervisor(http://supervisord.org 和马戏团https://circus.readthedocs.org/en/latest/ )。它们在配置和使用上都非常相似。Circus 比 Supervisor 年轻一点,因为它是为了解决后者的一些弱点而创建的。它们都可以用简单的类似 INI 的配置格式进行配置。它们不仅限于运行 Python 进程,还可以配置为管理任何应用程序。很难说哪一个更好,因为它们都提供非常相似的功能。

无论如何,Supervisor 并没有在 Python3 上运行,所以它并没有得到我们的许可。虽然在主管的控制下运行 Python3 进程不是问题,但我将以此为借口,仅以 Circus 配置为例。

假设我们希望在 Circus 控制下使用gunicornWeb 服务器运行 webxample 应用程序(本章前面介绍)。在生产中,我们可能会在适用的系统级过程监控工具(initdupstartrunit下运行 Circus,特别是如果它是从系统包存储库安装的。为了简单起见,我们将在虚拟环境中本地运行它。允许我们在 Circus 中运行应用程序的最低配置文件(此处命名为circus.ini)如下所示:

[watcher:webxample]
cmd = /path/to/venv/dir/bin/gunicorn webxample.conf.wsgi:application
numprocesses = 1

现在,可以使用此配置文件作为执行参数来运行circus流程:

$ circusd circus.ini
2016-02-15 08:34:34 circus[1776] [INFO] Starting master on pid 1776
2016-02-15 08:34:34 circus[1776] [INFO] Arbiter now waiting for commands
2016-02-15 08:34:34 circus[1776] [INFO] webxample started
[2016-02-15 08:34:34 +0100] [1778] [INFO] Starting gunicorn 19.4.5
[2016-02-15 08:34:34 +0100] [1778] [INFO] Listening at: http://127.0.0.1:8000 (1778)
[2016-02-15 08:34:34 +0100] [1778] [INFO] Using worker: sync
[2016-02-15 08:34:34 +0100] [1781] [INFO] Booting worker with pid: 1781

现在,您可以使用circusctl命令运行交互式会话,并使用简单的命令控制所有托管进程。以下是此类会议的一个示例:

$ circusctl
circusctl 0.13.0
webxample: active
(circusctl) stop webxample
ok
(circusctl) status
webxample: stopped
(circusctl) start webxample
ok
(circusctl) status
webxample: active

当然,上述两种工具都有更多的可用功能。所有这些都在他们的文档中进行了解释,所以在做出选择之前,您应该仔细阅读它们。

应用程序代码应在用户空间中运行

您的应用程序代码应始终在用户空间中运行。这意味着它不能在超级用户权限下执行。如果您按照十二要素应用程序设计应用程序,则可以在几乎没有权限的用户下运行应用程序。没有文件且不在特权组中的用户的常规名称为nobody,无论如何,实际建议是为每个应用程序守护进程创建一个单独的用户。原因是系统安全。这是为了限制恶意用户在控制您的应用程序进程时可能造成的损害。在 Linux 中,同一用户的进程可以相互交互,因此在用户级别将不同的应用程序分开是很重要的。

使用反向 HTTP 代理

多个符合 PythonWSGI 的 web 服务器可以轻松地单独为 HTTP 流量提供服务,而不需要在它们之上安装任何其他 web 服务器。出于各种原因,将它们隐藏在反向代理(如 Nginx)后面仍然很常见:

  • TLS/SSL 终止通常由顶级 web 服务器(如 Nginx 和 Apache)更好地处理。Python 应用程序只能使用简单的 HTTP 协议(而不是 HTTPS),因此安全通信通道的复杂性和配置留给反向代理。
  • 非特权用户不能绑定低端端口(在 0-1000 范围内),但应在端口 80 上向用户提供 HTTP 协议,并在端口 443 上提供 HTTPS。为此,必须以超级用户权限运行该进程。通常,让您的应用程序在高端口或 Unix 域套接字上运行,并将其用作在特权更高的用户下运行的反向代理的上游,会更安全。
  • 通常,Nginx 可以比 Python 代码更有效地服务于静态资产(图像、JS、CSS 和其他媒体)。如果您将其配置为反向代理,那么只需再进行几行配置即可通过它提供静态文件。
  • 当单个主机需要为来自不同域的多个应用程序提供服务时,Apache 或 Nginx 对于为同一端口上服务的不同域创建虚拟主机是必不可少的。
  • 反向代理可以通过添加额外的缓存层来提高性能,也可以配置为简单的负载平衡器。

实际上,有些 web 服务器建议在代理(如 Nginx)后面运行。例如,gunicorn是一个非常健壮的基于 WSGI 的服务器,如果它的客户端速度也很快,它可以提供优异的性能。另一方面,它不能很好地处理慢速客户端,因此很容易受到基于慢速客户端连接的拒绝服务攻击。使用能够缓冲慢速客户端的代理服务器是解决此问题的最佳方法。

重新加载过程优雅

十二因素应用程序方法论的第九条规则涉及流程可处置性,并指出您应该通过快速启动时间和优雅关闭来最大限度地提高稳健性。虽然快速启动时间是不言自明的,但优雅的关闭需要一些额外的讨论。

在 web 应用程序的范围内,如果以非竞争方式终止服务器进程,它将立即退出,没有时间完成处理请求并以正确的响应答复连接的客户端。在最好的情况下,如果您使用某种反向代理,那么代理可能会向连接的客户端回复一些一般错误响应(例如,502 坏网关),即使这不是通知用户您已重新启动应用程序并已部署新版本的正确方式。

根据十二要素应用程序,web 服务进程应该能够在收到 UnixSIGTERM信号(例如kill -TERM <process-id>信号)时优雅地退出。这意味着服务器应该停止接受新连接,完成处理所有挂起的请求,然后在没有其他事情要做时使用一些退出代码退出。

显然,当所有服务进程退出或启动其关闭过程时,您将无法再处理新请求。这意味着您的服务仍将经历中断,因此您需要执行另外一个步骤启动新工作人员,以便在旧工作人员正常退出时能够接受新连接。各种符合 Python WSGI 的 web 服务器实现允许在不停机的情况下优雅地重新加载服务。最受欢迎的是 Gunicorn 和 uWSGI:

  • Gunicorn 的主进程在收到SIGHUP信号(kill -HUP <process-pid>后,将启动新的工人(使用新代码和配置),并尝试在旧工人上正常关闭。
  • uWSGI 至少有三个独立的方案用于进行优雅的重新加载。其中每一项都过于复杂,无法简单解释,但其官方文件提供了所有可能选项的完整信息。

如今,优雅的重新加载是部署 web 应用程序的标准。Gunicorn 似乎有一种最容易使用的方法,但也让您的灵活性最低。另一方面,uWSGI 中优雅的重新加载允许更好地控制重新加载,但需要更多的努力来实现自动化和设置。此外,在自动化部署中如何处理优雅的重新加载也会受到您使用的监控工具及其配置方式的影响。例如,在 Gunicorn 中,优雅的重新加载非常简单:

kill -HUP <gunicorn-master-process-pid>

但是,如果您希望通过为每个版本分离虚拟环境来正确地隔离项目分布,并使用符号链接配置过程监控(如前面的fabfile示例所示),您很快就会注意到这并没有按预期工作。对于更复杂的部署,仍然没有现成的解决方案。您将始终需要进行一些黑客攻击,有时这将需要大量关于低级系统实现细节的知识。

规范仪表与监控

我们的工作不会以编写应用程序并将其部署到目标执行环境而结束。可以编写一个在部署后不需要进一步维护的应用程序,尽管这种可能性很小。实际上,我们需要确保正确地观察错误和性能。

为了确保我们的产品按预期工作,我们需要正确处理应用程序日志并监控必要的应用程序指标。这通常包括:

  • 监视各种 HTTP 状态代码的 web 应用程序访问日志
  • 进程日志的集合,其中可能包含有关运行时错误和各种警告的信息
  • 监控运行应用程序的远程主机上的系统资源(CPU 负载、内存和网络流量)
  • 监控作为业务绩效指标的应用程序级性能和指标(客户获取、收入等)

幸运的是,有很多免费工具可用于检测代码和监控其性能。它们中的大多数都很容易集成。

记录错误–哨兵/乌鸦

不管你的应用程序被测试得多么精确,事实是痛苦的。您的代码最终会在某个时候失败。这可能是任何意外的异常、资源耗尽、某些支持服务崩溃、网络中断,或者只是外部库中的问题。一些可能的问题,比如资源耗尽,可以通过适当的监控来预测和预防,但是无论你怎么努力,总会有一些东西通过你的防御。

您所能做的是为这些场景做好充分准备,并确保没有任何错误未被注意到。在大多数情况下,任何意外的故障场景都会导致由应用程序引发并通过日志系统记录的异常。这可以是stdoutsderr、文件或您为日志记录配置的任何输出。根据您的实现,这可能会导致应用程序退出,也可能不会导致应用程序退出某些系统退出代码。

当然,您可以仅依靠存储在文件中的日志来查找和监视应用程序错误。不幸的是,在文本日志中观察错误是非常痛苦的,并且不能扩展到比在开发中运行代码更复杂的范围。您最终将被迫使用一些为日志收集和分析而设计的服务。由于其他原因,正确的日志处理非常重要,稍后将解释这些原因,但是对于跟踪和调试生产错误并不起作用。原因很简单。最常见的错误日志形式就是 Python 堆栈跟踪。如果您仅停留在这一点上,您很快就会意识到,仅找到问题的根本原因是不够的,尤其是在未知模式或某些负载条件下出现错误时。

您真正需要的是关于错误发生的尽可能多的上下文信息。拥有生产环境中发生的错误的完整历史记录也非常有用,您可以通过某种方便的方式进行浏览和搜索。提供这种功能的最常用工具之一是 Sentry(https://getsentry.com 。这是一项经过战斗测试的服务,用于跟踪异常和收集崩溃报告。它是开源的,是用 Python 编写的,最初是作为后端 web 开发人员的工具。现在,它已经超越了最初的雄心壮志,支持更多的语言,包括 PHP、Ruby 和 JavaScript,但仍然是大多数 Python web 开发人员选择的最流行的工具。

提示

web 应用程序中的异常堆栈回溯

web 应用程序通常不会在未处理的异常情况下退出,因为如果发生任何服务器错误,HTTP 服务器必须返回带有 5XX 组状态代码的错误响应。默认情况下,大多数 pythonweb 框架都会做这样的事情。在这种情况下,异常实际上是在较低的框架级别上处理的。无论如何,在大多数情况下,这仍然会导致打印异常堆栈跟踪(通常在标准输出上)。

Sentry 以付费软件即服务模式提供,但它是开源的,因此可以在您自己的基础设施上免费托管。提供与 Sentry 集成的库是raven(可在 PyPI 上获得)。如果您还没有使用过它,想要测试它,但无法访问您自己的 Sentry 服务器,那么您可以轻松注册 Sentry 的内部部署服务站点,免费试用。一旦您访问了 Sentry 服务器并创建了一个新项目,您将获得一个名为 DSN 的字符串或数据源名称。此 DSN 字符串是将应用程序与 sentry 集成所需的最低配置设置。它包含协议、凭据、服务器位置和您的组织/项目标识符,格式如下:

'{PROTOCOL}://{PUBLIC_KEY}:{SECRET_KEY}@{HOST}/{PATH}{PROJECT_ID}'

一旦有了 DSN,集成就非常简单:

from raven import Client

client = Client('https://<key>:<secret>@app.getsentry.com/<project>')

try:
    1 / 0
except ZeroDivisionError:
    client.captureException()

Raven 与最流行的 Python 框架(如 Django、Flask、Cellery 和 Pyramid)进行了大量集成,以简化集成。这些集成将自动提供特定于给定框架的附加上下文。如果您选择的 web 框架没有专用支持,raven包提供通用 WSGI 中间件,使其与任何基于 WSGI 的 web 服务器兼容:

from raven import Client
from raven.middleware import Sentry

# note: application is some WSGI application object defined earlier
application = Sentry(
    application,
    Client('https://<key>:<secret>@app.getsentry.com/<project>')
)

另一个值得注意的集成是跟踪通过 Python 内置的logging模块记录的消息的能力。启用此类支持只需要几行额外的代码:

from raven.handlers.logging import SentryHandler
from raven.conf import setup_logging

client = Client('https://<key>:<secret>@app.getsentry.com/<project>')
handler = SentryHandler(client)
setup_logging(handler)

捕获logging消息可能有一些不明显的警告,因此如果您对此类功能感兴趣,请务必阅读有关该主题的官方文档。这将使你免于不愉快的意外。

最后一点是关于经营自己的哨兵以节省一些钱。“没有免费的午餐。”你最终将支付额外的基础设施费用,哨兵将只是另一项需要维护的服务。维护=额外工作=成本!随着应用程序的增长,异常的数量也会增加,因此在扩展产品时,您将被迫扩展 Sentry。幸运的是,这是一个非常健壮的项目,但如果负载过重,它不会给您带来任何价值。此外,让哨兵为灾难性的故障场景做好准备,在这种情况下,每秒可以发送数千份碰撞报告,这是一个真正的挑战。因此,你必须决定哪种选择对你来说更便宜,以及你是否有足够的资源和智慧独自完成这一切。如果您的组织中的安全策略拒绝向第三方发送任何数据,那么当然不会出现这种困境。如果是这样,只需在您自己的基础设施上托管它即可。当然也有成本,但绝对值得付出。

监控系统及应用指标

当涉及到监控性能时,可供选择的工具数量可能是巨大的。如果您有很高的期望值,那么您可能需要同时使用其中的一些。

穆宁http://munin-monitoring.org 是许多组织使用的流行选择之一,无论使用的是哪种技术。它是分析资源趋势的一个很好的工具,即使在没有额外配置的默认安装情况下,它也提供了很多有用的信息。其安装包括两个主要部件:

  • 从其他节点收集度量并为度量图提供服务的 Munin 主节点
  • 安装在受监控主机上的 Munin 节点,用于收集本地度量并将其发送给 Munin 主机

Master、node 和大多数插件都是用 Perl 编写的。还有其他语言的节点实现:munin-node-c是用 C(编写的 https://github.com/munin-monitoring/munin-cmunin-node-python是用 Python 编写的(https://github.com/agroszer/munin-node-python 。Munin 在其contrib存储库中提供了大量插件。这意味着它为大多数流行的数据库和系统服务提供开箱即用的支持。甚至还有用于监视流行 Python web 服务器的插件,如 uWSGI 和 Gunicorn。

Munin 的主要缺点是它将图形作为静态图像使用,而实际的绘图配置包含在特定的插件配置中。这无助于创建灵活的监控仪表板,也无助于在同一图表上比较来自不同来源的度量值。但这是我们需要为简单的安装和多功能性付出的代价。编写自己的插件非常简单。有munin-python包(http://python-munin.readthedocs.org/en/latest/ )这有助于用 Python 编写 Munin 插件。

不幸的是,Munin 的体系结构假设负责收集度量的每个主机上都有一个单独的监控守护进程,这可能不是监控自定义应用程序性能度量的最佳解决方案。编写自己的 Munin 插件确实很容易,但前提是监控过程已经可以以某种方式报告其性能统计数据。如果您想收集一些自定义应用程序级别的指标,可能需要将它们聚合并存储在一些临时存储中,直到向自定义 Munin 插件报告为止。它使得自定义的度量度量变得更加复杂,因此您可能需要考虑其他解决方案。

另一个流行的解决方案是 StatsD(,它使收集定制度量变得特别容易 https://github.com/etsy/statsd )。它是一个用 Node.js 编写的网络守护进程,用于侦听各种统计信息,如计数器、计时器和仪表。由于基于 UDP 的简单协议,它很容易集成。使用名为statsd的 Python 包向 StatsD 守护进程发送度量也很容易:

>>> import statsd
>>> c = statsd.StatsClient('localhost', 8125)
>>> c.incr('foo')  # Increment the 'foo' counter.
>>> c.timing('stats.timed', 320)  # Record a 320ms 'stats.timed'.

由于 UDP 是无连接的,因此它在应用程序代码上的性能开销非常低,因此非常适合跟踪和测量应用程序代码中的自定义事件。

不幸的是,StatsD 是唯一的度量集合守护程序,因此它不提供任何报告功能。您需要能够处理来自 StatsD 的数据的其他流程,以便查看实际的度量图。最受欢迎的选择是石墨(http://graphite.readthedocs.org 。它主要做两件事:

  • 存储数字时间序列数据
  • 按需呈现此数据的图形

Graphite 为您提供了保存高度可定制的图形预设的功能。您还可以将许多图形分组到主题仪表板中。与 Munin 类似,图形被渲染为静态图像,但也有 JSON API 允许其他前端读取图形数据并通过其他方式渲染。与 Graphite 集成的一个伟大的仪表板插件是 Grafana(http://grafana.org 。它确实值得一试,因为它比普通石墨仪表板有更好的可用性。Grafana 中提供的图形是完全交互式的,更易于管理。

不幸的是,石墨是一个有点复杂的项目。它不是单一服务,由三个独立的组件组成:

  • Carbon:这是一个使用 Twisted 编写的守护进程,用于侦听时间序列数据
  • whisper:这是一个简单的数据库库,用于存储时间序列数据
  • graphite webapp:这是一款 Django web 应用程序,可按需将图形呈现为静态图像(使用 Cairo 库)或 JSON 数据

当与 StatsD 项目一起使用时,statsd守护进程将其数据发送给carbon守护进程。这使得完整的解决方案成为各种应用程序的相当复杂的堆栈,其中每个应用程序都是使用完全不同的技术编写的。此外,没有预配置的图形、插件和仪表板,因此您需要自己配置所有内容。这是一开始的大量工作,很容易错过一些重要的事情。这就是为什么使用 Munin 作为监控备份可能是一个好主意的原因,即使您决定使用 Graphite 作为您的核心监控服务。

处理应用日志

虽然像 Sentry 这样的解决方案通常比存储在文件中的普通文本输出更强大,但日志永远不会消失。将一些信息写入标准输出或文件是应用程序可以做的最简单的事情之一,这一点永远不能低估。乌鸦发送给哨兵的信息可能无法送达。网络可能会失败。哨兵的仓库可能会耗尽,或者可能无法处理传入的负载。您的应用程序可能在发送任何消息之前崩溃(例如,存在分段错误)。这些只是一些可能的情况。不太可能的是,您的应用程序无法记录将要写入文件系统的消息。这仍然是可能的,但老实说。如果您面临这样一种情况,日志记录失败,那么您可能会遇到比丢失一些日志消息更严重的问题。

请记住,日志不仅仅是关于错误的。许多开发人员过去只将日志视为调试问题时有用的数据源和/或可用于执行某种取证的数据源。当然,很少有人尝试将其用作生成应用程序度量或进行统计分析的源。但日志可能比这有用得多。它们甚至可以成为产品实现的核心。使用日志构建产品的一个很好的例子是 Amazon 的文章,它为实时投标服务提供了一个示例架构,其中所有内容都围绕着访问日志的收集和处理。参见https://aws.amazon.com/blogs/aws/real-time-ad-impression-bids-using-dynamodb/

基本低级日志实践

12 因素应用程序表示,日志应被视为事件流。因此,日志文件本身不是日志,而只是一种输出格式。它们是流这一事实意味着它们代表按时间顺序排列的事件。在 raw 中,它们通常是文本格式,每个事件一行,尽管在某些情况下它们可能跨越多行。这对于与运行时错误相关的任何回溯都是典型的。

根据十二因素应用程序方法,应用程序永远不应该知道日志的存储格式。这意味着,应用程序代码永远不应维护对文件的写入、日志循环和保留。这些是运行应用程序的环境的职责。这可能会令人困惑,因为许多框架提供了用于管理日志文件的函数和类,以及旋转、压缩和保留实用程序。使用它们很有诱惑力,因为所有内容都可以包含在应用程序代码库中,但实际上,这是一种应该避免的反模式。

处理日志的最佳约定可以通过以下几条规则来完成:

  • 应用程序应始终写入未缓冲到标准输出的日志(stdout
  • 执行环境应负责收集日志并将其路由到最终目的地

上述执行环境的主要部分通常是某种过程监控工具。流行的 Python 解决方案,如 Supervisor 或 Circus,是第一个负责处理日志收集和路由的解决方案。如果日志要存储在本地文件系统中,那么只有它们才应该写入实际的日志文件。

监督者和马戏团 Apple T2A2 也能够处理日志轮转和托管进程的保留,但您应该真正考虑这是否是一条要走的路径。成功的操作主要是简单性和一致性。您自己的应用程序的日志可能不是唯一需要处理和归档的日志。如果使用 Apache 或 Nginx 作为反向代理,则可能需要收集它们的访问日志。您可能还希望存储和处理缓存和数据库的日志。如果您正在运行一些流行的 Linux 发行版,那么这些服务中的每一个都有自己的日志文件被名为logrotate的流行实用程序处理(旋转、压缩等)的可能性非常高。为了与其他系统服务保持一致,我强烈建议您忘记 Supervisor 和 Circus 的日志轮换功能。logrotate更具可配置性,还支持压缩。

提示

logrotate 和主管/马戏团

当与主管或马戏团一起使用logrotate时,有一件重要的事情需要知道。当流程主管仍然有一个开放的描述符来描述已旋转的日志时,日志的旋转将始终发生。如果您没有采取适当的对策,那么新事件仍将写入已被logrotate删除的文件描述符。因此,文件系统中将不再存储任何内容。这个问题的解决办法很简单。使用copytruncate选项为主管或 Circus 管理的流程的日志文件配置logrotate。它不会在旋转后移动日志文件,而是复制日志文件并将原始文件截断为零大小。这种方法不会使任何现有的文件描述符无效,并且已经运行的进程可以不间断地写入日志文件。主管还可以接受SIGUSR2信号,使其重新打开所有文件描述符。它可以作为postrotate脚本包含在logrotate配置中。第二种方法在 I/O 操作方面更经济,但也不太可靠,更难维护。

日志处理工具

如果您没有使用大量日志的经验,那么在使用负载较大的产品时,您最终会获得这种经验。您很快就会注意到,仅使用一种简单的方法,将它们存储在文件中,并在一些持久性存储中备份以供以后检索是不够的。如果没有合适的工具,这将变得粗糙和昂贵。像logrotate这样的简单实用程序只会帮助您确保硬盘不会因不断增加的新事件而溢出,但拆分和压缩日志文件只会有助于数据归档过程,但不会使数据检索或分析变得更简单。

当使用跨多个节点的分布式系统时,最好有一个中心点,从中可以检索和分析所有日志。这需要一个日志处理流程,它不仅仅是简单的压缩和备份。幸运的是,这是一个众所周知的问题,因此有许多工具可用于解决它。

在许多开发者中,一个流行的选择是Logstash。这是一个日志收集守护进程,它可以观察活动日志文件,解析日志条目,并以结构化形式将它们发送给支持服务。背衬的选择几乎总是一样的-Elasticsearch。Elasticsearch 是建立在 Lucene 之上的搜索引擎。在文本搜索功能中,它有一个独特的数据聚合框架,非常适合日志分析的目的。

这对工具的另一个补充是Kibana。它是一个用于 Elasticsearch 的多功能监控、分析和可视化平台。这三种工具是如何相互补充的,这就是为什么它们几乎总是作为日志处理的单个堆栈一起使用的原因。

现有服务与 Logstash 的集成非常简单,因为它只需在日志配置中进行最小的更改,就可以侦听现有日志文件的新事件更改。它以文本形式解析日志,并对一些流行的日志格式(如 Apache/Nginx 访问日志)提供预配置支持。Logstash 唯一的问题是它不能很好地处理日志旋转,这有点令人惊讶。通过发送一个已定义的 Unix 信号(通常为SIGHUPSIGUSR1)来强制进程重新打开其文件描述符是一种非常成熟的模式。似乎每个(专门)处理日志的应用程序都应该知道这一点,并且能够处理各种日志文件轮换场景。遗憾的是,Logstash 不是其中之一,所以如果您想使用logrotate实用程序管理日志保留,请记住要严重依赖其copytruncate选项。Logstash 进程无法处理原始日志文件被移动或删除的情况,因此如果没有copytruncate选项,它将无法在日志旋转后接收新事件。Logstash 当然可以处理日志流的不同输入,如 UDP 数据包、TCP 连接或 HTTP 请求。

另一个似乎可以填补一些后勤缺口的解决方案是 Fluentd。它是一个可选的日志收集守护程序,可以与上述日志监视堆栈中的 Logstash 互换使用。它还可以选择直接在日志文件中侦听和解析日志事件,因此最小的集成只需要一点努力。与 Logstash 不同,它可以很好地处理重新加载,并且在日志文件被旋转时甚至不需要发出信号。无论如何,最大的优势来自使用它的一个可选日志收集选项,这将需要对应用程序中的日志配置进行一些实质性的更改。

Fluentd 确实将日志视为事件流(正如 12 因素应用程序所建议的那样)。基于文件的集成仍然是可能的,但对于主要将日志视为文件的遗留应用程序来说,这只是一种向后兼容性。每个日志条目都是一个事件,应该对其进行结构化。Fluentd 可以解析文本日志,并有多个插件选项可供处理:

  • 通用格式(Apache、Nginx 和 syslog)
  • 使用正则表达式指定或使用自定义解析插件处理的任意格式
  • 结构化消息(如 JSON)的通用格式

Fluentd 的最佳事件格式是 JSON,因为它增加的开销最少。JSON 中的消息也可以在几乎不改变支持服务(如 Elasticsearch 或数据库)的情况下传递。

Fluentd 的另一个非常有用的功能是能够使用传输而不是写入磁盘的日志文件来传递事件流。最著名的内置输入插件有:

  • in_udp:使用此插件,每个日志事件都作为 UDP 数据包发送
  • in_tcp:此插件通过 TCP 连接发送事件
  • in_unix:此插件通过 Unix 域套接字(名称套接字)发送事件
  • in_http:此插件将事件作为 HTTP POST 请求发送
  • in_exec:使用此插件,Fluentd 进程定期执行外部命令,以提取 JSON 或 MessagePack 格式的事件
  • in_tail:使用此插件,Fluentd 进程将侦听文本文件中的事件

在需要处理机器存储的 I/O 性能差的情况下,日志事件的替代传输可能特别有用。在云计算服务上,默认磁盘存储的IOPS每秒输入输出操作)非常低,您需要支付大量资金才能获得更好的磁盘性能。如果应用程序输出大量日志消息,即使数据大小不是很高,也很容易使 I/O 功能饱和。使用备用传输,您可以更有效地使用硬件,因为您将数据缓冲的责任仅留给单个进程日志收集器。当配置为在内存而不是磁盘中缓冲消息时,您甚至可以完全消除日志的磁盘写入,尽管这可能会大大降低收集日志的一致性保证。

使用不同的运输方式似乎有点违背了 12 因素 App 方法的第 11 条规则。当详细解释时,将日志视为事件流表明应用程序应始终仅通过单个标准输出流(stdout进行日志记录。在不违反此规则的情况下,仍然可以使用备用传输。写入stdout并不一定意味着必须将该流写入文件。您可以让您的应用程序以这种方式记录日志,并使用一个外部进程将其包装起来,该进程将捕获此流并将其直接传递给 Logstash 或 Fluentd,而无需使用文件系统。这是一种高级模式,可能不适合于每个项目。它有一个明显的缺点是更高的复杂性,所以你需要自己考虑它是否真的值得去做。

总结

代码部署不是一个简单的主题,在阅读本章之后,您应该已经知道了这一点。对这个问题的广泛讨论可能需要几本书。尽管我们仅限于 web 应用程序,但我们几乎没有触及表面。本章以十二因素应用程序法为基础。我们只详细讨论了其中的几个:日志处理、管理依赖项和分离构建/运行阶段。

阅读本章后,您应该知道如何适当地自动化部署过程,并考虑到最佳实践,并且能够为在远程主机上运行的代码添加适当的检测和监视。