- 引言
- 本书涉及的内容
- 第 1 部分 Python 开发入门
- 第 1 章 Python 入门
- 第 2 章 开发 Web 应用
- 第 3 章 Python 项目的结构与包的创建
- 第 4 章 面向团队开发的工具
- 第 5 章 项目管理与审查
- 第 6 章 用 Mercurial 管理源码
- 第 7 章 完备文档的基础
- 第 8 章 模块分割设计与单元测试
- 第 9 章 Python 封装及其运用
- 第 10 章 用 Jenkins 持续集成
- 第 11 章 环境搭建与部署的自动化
- 第 12 章 应用的性能改善
- 第 13 章 让测试为我们服务
- 第 14 章 轻松使用 Django
- 第 15 章 方便好用的 Python 模块
- 附录 A VirtualBox 的设置
- 附录 B OS(Ubuntu)的设置
11.1 确定所需环境的内容
首先要从所需环境的内容入手。环境一旦开始使用就很难再进行大的改动,所以动手搭建前搞清所需环境的内容是十分重要的。
在熟悉搭建环境的流程之前,建议先摸索着尝试手动搭建。等到熟悉之后,再考虑结构选择和搭建过程自动化的问题。
利用 VirtualBox 和快照功能可以帮助我们快速确定所需环境的内容。感兴趣的读者请同时参考附录 A。
11.1.1 网络结构
许多服务是由多台服务器组合实现的,所以在探讨服务器内部的环境搭建之前,先要确定服务器的组成结构。
可用服务器数、预算、应用性能不同,所需的服务器结构也是千差万别。这里我们以图 11.1 所示的结构为例进行学习。
图 11.1 服务器结构
· LB:负载平衡器(nginx)
· AP1、2:应用服务器(django/gunicorn)
· DB:数据库(mysql)
· gateway:跳板服务器
◉ 服务器编组
确定服务器结构时,要给职责相同的服务器起一个统一的名字。
这里我们将分担各个职责的服务器群称为组。在图 11.1 所示的结构中,有 LB、AP、DB、gateway 这 4 个组。
即便某个职责只由一台服务器完成,我们也认为其是一个组。这样一来能更灵活地应对多服务器结构。
NOTE
按职责划分的服务器群也被称为“角色”(Role)。但这会与我们即将讲到的 Ansible 的 Role 重复,所以这里改用“组”(Group)。
◉ 跳板服务器
允许从外部网络直接 ssh 登录各个服务器会带来很多安全隐患,所以一般我们会阻止外部通过 ssh 访问各服务器。
可是,一旦阻止了对所有服务器的 ssh,我们便无法通过外网登录,这会造成很多不便。所以这里要准备一台用作登录跳板的服务器,形成通过跳板登录各服务器的结构。
11.1.2 服务器搭建内容的结构化
将服务器搭建内容按照图 11.2 所示的结构进行结构化。
图 11.2 服务器搭建内容的结构化
这个结构很好理解。在中间件里定义安装和设置等流程,然后将各个中间件组合起来搭建服务器组。最后把搭建好的多个服务器组整合起来,就形成了一个环境。
这里需要特别注意的是,将搭建流程整合到各个中间件里时,如果只简单地把要做的处理按顺序一件一件写下来,很容易让人搞不清究竟哪个步骤属于哪个中间件的设置。加强对这个结构的理解和注意,有助于提高自动化的效率。关于自动化的知识我们将在 11.2 节进行学习。
根据图 11.2 所示的结构,本章中的服务器可划分为如下图 11.3 所示的结构。
图 11.3 本章中的服务器的搭建内容
这里出现了“环境设置”,它不是中间件,而是我们为了便于理解,将 OS 的设置、用户的创建等不针对特定中间件进行的操作汇总在了一起,并统一称为“环境设置”。
下面我们开始学习具体的搭建流程。
11.1.3 用户的设置
我们已经确定好了服务器的结构,接下来要探讨搭建流程。首先是用户的设置。
初始状态下的 Ubuntu 以 ubuntu 用户为登录用户。但是,如果让默认的管理员用户来做维护或启动应用,会带来一些安全隐患。所以,我们新建 mainte 用户用于搭建和维护,新建一个不能直接登录的普通用户 www 用于执行应用。
# useradd -m mainte # useradd -m www
搭建工作会经常用到 sudo,因此要给 mainte 用户设置 sudo 权限。另外,考虑到自动化的问题,最好让 mainte 不需要密码就可以执行 sudo。
可以通过编辑 /etc/sudoers 或者在 /etc/sudoers.d/ 中添加设置文件修改 sudo 权限。这次我们用后一种方法,创建 /etc/sudoers.d/mainte。
%mainte ALL=(ALL) NOPASSWD:ALL
%mainte 表示给 mainte 组的所有用户设置 sudo 权限。今后如果有其他用户需要用到 sudo,只要将其加入 mainte 组即可。
至于 www 用户,要让它只保有启动应用所需的最小权限,因此不设置 sudo 权限。
最后是登录设置。mainte 用户今后要用来做维护工作,所以需要能从外部登录。
首先用 ssh-keygen 生成密钥文件。
$ sudo su - mainte $ ssh-keygen -b 2048
如果在初始设置状态下执行 ssh-keygen ,会在 /home/mainte/.ssh/ 下生成 id_rsa 和 id_rsa.pub 两个文件。我们将这两个文件下载到本地 PC 保管。
将 id_rsa.pub 的内容设置到 authorized_keys 中,登录设置就完成了。
$ cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys $ chmod 600 ~/.ssh/authorized_keys
经过上面的设置,我们在 mainte 用户的“.ssh”目录下生成了密钥文件,同时完成了对应的 authorized_key 的设置。
这样设置下来之后,就能很轻松地在多个服务器之间切换登录了。
最后查看设置是否正确。
· mainte 用户能用下载到本地 PC 的密钥文件登录服务器
· mainte 用户能通过 sudo 切换为 www 用户
· 可以对 localhost 执行 ssh
$ ssh localhost
11.1.4 选定程序包
接下来安装应用运行所需的程序包。
在 Ubuntu 上运行 Python 应用时,主要通过 apt-get 和 pip 安装程序包。
选择要安装的程序包时请注意以下几点。
① 安装的程序包要尽量少
尽量不要导入与运行应用无关的程序包。
盲目导入程序包会使我们无法准确掌握应用的运行条件,出问题时很难分辨问题出在环境上还是应用本身上。
要导入与运行应用没有直接关系的程序包时,必须先仔细考虑其用途和安装范围,再确定是否要将其加入搭建流程。
下面是一些做判断的例子。
· 应用的目录结构很复杂,排查 Bug 时需要 tree 命令的辅助
任何环境都难免需要排查 Bug,因此需要将 tree 命令加入构建流程。
· 想用自己常用的编辑器 emacs
基本只会在开发环境中用到,所以仅在开发环境中导入。
② 掌握并管理程序包的版本
对开发力度较大的应用 / 库而言,常有从某个版本起发生大幅度规格变更,或是不兼容旧版本设置文件的情况发生。
这种时候如果盲目升级了版本,很容易导致应用无法运行。
要防止这类因版本导致的事故,重点在于把握安装的程序包的版本以及确定更新原则。
原则主要有下面几种,各位可以为每个程序包分别选择合适的原则。
· 完全固定
不考虑版本升级,仅使用指定版本的程序包。
· 固定至次版本号
固定主版本号和次版本号,允许加入安全更新和 Bug 修复。
◉ 通过 apt-get 安装程序包
用 apt-get 或 yum 从各个 Linux 发行版的程序包版本库安装程序包时,首先要确认各个发行版的更新原则。
本书所用的 Ubuntu 原则上只加入安全更新和 Bug 修复,不进行其他版本升级。
也就是说,环境本身就处于上述“固定至次版本号”原则的状态,我们不必多作修改。程序包升级时只会加入重大的 Bug 修复,不会更新主版本号和次版本号。
上述原则只要能满足我们的需求即可,没有什么特别需要注意的地方。现在我们可以通过简单的命令安装程序包,具体如下。
$ sudo apt-get install packagename
如果需要固定版本,则需要在程序包名称后面添加等号以及版本号。具体如下所示。
$ sudo apt-get install packagename=1.2.3-4ubuntu3
包名和版本号都可以用正则表达式。比如,如果只想固定到次版本号,则上述命令可以写成“1.2\..*”的形式。
◉ 通过 pip 安装程序包
用 pip 安装程序包的流程在第 9 章中有详细介绍,不清楚的读者请参考该部分。
根据 11.1.4 节得出的结果编写 requirements.txt,修正版本指定,依照自身情况选择是否分离一部分到 dev-requires.txt 和 tests-require.txt。
完成 requirements.txt 之后,只需用 pip install -r requirements.txt 进行安装即可。
◉ 封闭环境中的安装
为没有网络连接的服务器搭建环境时,apt-get 和 pip 这种通过外部连接安装程序包的方法就不好用了。另外,程序包提供方(apt 版本库、PyPI、GitHub 等)故障或维护时也是同样道理。
为应对这种情况,我们可以将所需的程序包事先保存在版本库中,然后进行离线安装。我们把这些打包在一起的程序包叫作 bundle。
bundle 不但可以让我们离线安装程序包,还能有效固定版本以及削减搭建时间。
○ 通过 apt-get 安装 bundle
apt-get 安装的程序包都是以“.deb”格式发布的。用 apt-get download 命令可以获取我们需要的 deb 程序包。
$ mkdir aptcache && cd aptcache $ sudo apt-get download python3.4 $ ls python3.4_3.4.0-2ubuntu1_amd64.deb
deb 程序包用 dpkg -i 命令安装。
$ sudo dpkg -i python3.4_3.4.0-2ubuntu1_amd64.deb
但是有一个问题,apt-get download 命令只能获取目标程序包,无法同时获取该程序包的依赖包。因此要用 apt-cache depends 命令查看依赖包,然后在通过上述方法获取它们的 deb 程序包。
$ apt-cache depends python3.4 python3.4 Depends: python3.4-minimal Depends: libpython3.4-stdlib Depends: mime-support Suggests: python3.4-doc Suggests: binutils
有时这些依赖包本身又依赖于其他程序包,如此循环下去不知何时是个头,所以我们需要一个方法来一次性获取所有相关的程序包。
实际上,用 apt-get install 命令安装程序包时,目标程序包及其所有依赖包的 deb 文件全都会被下载到缓存目录下。只要通过 -o 选项临时变更缓存目录,就能将目标程序包和其所有依赖包保存到任意目录下了。
$ sudo apt-get install python3.4 -o Dir::Cache::Archives=/home/mainte/aptcache
然而有一点需要注意,那就是这个方法无法获取已经安装的程序包,因此需要在初始状态的环境下(比如刚用 VM 搭建完毕的环境)进行操作。
○ 通过 pip 安装 bundle
旧版本的 pip 可以用 pip bundle 命令进行安装,但这个命令在 pip 1.5 被删除。1.5 之后开始使用 wheel。9.1.4 节详细介绍了 wheel 的相关内容,有需要的读者请参考该部分。
将所需程序包全部转换为 wheel。此时也通过 requirements.txt 指定程序包。
$ pip wheel -r requirments.txt
这个操作会在当前目录的 wheelhouse 目录下生成 wheel。我们将整个 wheelhouse 目录都添加到应用的版本库中。
在各环境下都可以通过下述命令从 wheelhouse 安装程序包。
$ pip install --no-index -f /path/to/my/repository/wheelhouse -r requirements.txt
○ bundle 的维护
更新或添加新的程序包时,需要将程序包重新打包成 bundle。一旦漏掉这个步骤,应用可能出现在某些环境下能运行,在某些环境下却不能运行的情况,问题很难排查。
重新打包 bundle 是一件非常繁琐的工作,对于 deb 程序包来说尤其如此。开发时会频繁出现程序包的添加和更新,因此过早地 bundle 化会带来许多麻烦。毕竟开发环境很少遇到无法连接外部网络的情况,所以开发中建议使用 apt-get 和 pip install 来安装程序包。等到正式环境就绪再考虑 bundle 化也不迟。
程序包管理进入 bundle 阶段后,程序包的 bundle 化也可以交给构建服务器实现自动化。
11.1.5 中间件的设置
mysql、nginx 等中间件要根据使用环境进行设置。
我们往往需要在大量服务器上实施或更新中间件的设置,如果这些全都手动去完成,很容易出现疏漏,导致发生问题。因此,实现中间件设置的自动化才是上上之选。首先搞清需要自动化的内容,选出对象文件以及要修改的项目。本章用作例子的服务器结构中包含了 mysql、nginx 和 gunicorn,这里我们就来探讨一下这 3 个中间件的设置。
◉ 让中间件设置生效的方法
自动化更新中间件的设置文件需要以下步骤。
① 在版本库中管理对象文件
② 用模板语言重新描述可变部分
③ 将模板化的文件翻译过来直接覆盖原设置文件
步骤②和③提到的模板功能是 Ansible 标配的功能,关于 Ansible 的知识会在后面讲到。探讨设置内容的过程中需要我们手动改写设置文件,但最终所有设置都要通过上述步骤反映出来。 Ansible 采用 Jinja2 为模板编辑器,因此本节的模板文件都是以 Jinja2 格式描述的。
下面我们来看看用 Jinja2 格式描述设置文件可变部分的例子。
◉ mysql
MySQL 的设置描述在“/etc/mysql/conf.d/*”中。本例的设置文件为 /etc/mysql/conf.d/myproject.cnf。
讨论所需项目并编写文件。
[mysqld] bind_address = {{ MY_LOCAL_IP }} innodb_file_per_table = yes innodb_buffer_pool_size = {{ MYSQL_INNODB_BUFFER_POOL_SIZE }}
在 mysql 的设置中,有些值会根据环境变化,比如根据服务器 IP 变化的 bind_address,根据服务器内存容量变更最优值的 innodb_buffer_pool_size 等。如果事先将这些值设置为变量,可以在中间件变更所在服务器时轻松完成设置更新。
◉ nginx
nginx 的设置描述在 /etc/nginx/conf.d/ap.conf 中。
nginx 的 conf.d 目录下设置有 default.conf 和 example_ssl.conf 两个文件,它们是用来显示 nginx 默认页的,与我们要搭建环境的服务器无关。因此要在探讨设置之前把这两个文件删除。
LIST 11.1 /etc/nginx/conf.d/ap.conf
upstream app { {% for server in NGINX_UPSTREAMS %} server {{ server }}:8000; {% endfor %} } server { listen {% if SSL %}443{% else %}80{% endif %}; server_name {{ DOMAIN }}; {% if SSL %} ssl on; ssl_certificate {{ SSL.CERTIFICATE }}; ssl_certificate_key {{ SSL.KEY }}; {% endif %} ... }
需要在 upstream 指令中指定多个反向代理对象的服务器地址。这里我们是用变量 NGINX_UPSTREAMS 来存储服务器地址清单,并通过 for 语句来指定服务器地址的。
另外,我们还对 server 指令内是否存在 SSL 的设置进行了判断,并根据情况进行了 listen 端口的切换以及 ssl_certificate 的设置。出于预算原因,我们的项目并不是每个环境都使用 SSL,所以在这里保证了两种设定之间的简单切换。
可见,利用 Jinja2 模板的 if 和 for 能做出复杂的情况分类(LIST 11.1)。但有一点需要注意,过度使用控制语句会降低程序的可读性,增加修改的难度。
◉ gunicorn
让 gunicorn 通过 upstart 启动。创建 /ect/init/myapp.conf 文件用作启动脚本,脚本内容如下。
upstart 的文档
description "myapp" start on (filesystem) stop on runlevel [016] respawn console log setuid www setgid www chdir {{ SOURCE_DIR }}/src exec DJANGO_SETTINGS_MODULE={{ DJANGO_SETTINGS }} {{ SOURCE_DIR }}.venv/bin/gunicorn myapp.wsgi:application --bind={{ MY_LOCAL_IP }}:8000 --workers={{GUNICORN_WORKERS }}
上述例子将许多值替换为了变量。等到自动化的时候,这些地方就会发挥出其效果了。
· SOURCE_DIR
放置源码目录。虽然这个值很少因为环境而变,但换成变量能防止键入错误。
· DJANGO_SETTINGS
Django的settings文件需要根据环境而变。将它设置为变量之后,我们就可以在所有环境中应用同一个模板了。
· MY_LOCAL_IP
自身服务器的本地IP。直接拿mysql设置文件中定义的变量来用。
· GUNICORN_WORKERS
gunicorn的worker进程数。
将中间件的设置文件模板化时,需要考虑以下几个问题。
· 哪个设置值需要用变量替换
因环境而变的值,以及容易输入错的值(比如较长的文件路径)。
· 有多个中间件共用的变量吗
所有环境共享的值要设计成共用一个变量。
11.1.6 部署
部署就是将应用安置到环境中,使其进入可运行状态。
应用源码的安置、中间件设置的反映、中间件的重启等都属于部署工作。
本节将对上述 3 个工作进行说明。
◉ 源码的安置与更新
源码的安置用 Mercurial 的 clone 和 pull 即可轻松完成。利用标签和分支的功能,还能灵活应对回滚等操作。
如果有从各环境均可连接的公共密钥认证的中央版本库,那么最好从该版本库进行 clone 操作。这里我们假设中央版本库放在 myrepository.com。
clone 时用 11.1.3 节创建的 www 用户。访问版本库时用到的公共密钥和私有密钥要事先设置好。
本例中,我们把版本库设置在 /var/www/myproject。
$ cd /var/www/ $ hg clone ssh://myrepository.com/myproject
执行 clone 操作之后就完成了应用源码的安置工作。更新工作只需要在各个服务器上对版本库进行 pull/update 即可。
$ cd /var/www/myproject $ hg pull; hg update default
根据用途和环境不同可以将 update 位置替换为其他分支名或标签名,以灵活应对各种需求。实现自动化时只需将其改为变量,让其能够被替换即可。
专栏 没有可通过公共密钥认证的中央版本库时该怎么办
源码在 Mercurial 上管理时,只要我们能使用 clone 操作,就能将源码部署到环境中。但是如果中央版本库要求密码认证,那么每次 clone 都要输入密码,不利于自动化。
这种时候可以先将主版本库 clone 到 gateway 服务器上,然后各环境服务器再从 gateway服务器上的版本库进行clone,这样就能将输入密码的次数降到最少(图11.4)。
图 11.4 密码认证环境中的源码部署
◉ 中间件设置的反映
如 11.1.5 节所述,现阶段需要手动改写模板部分并上传至各环境,从而反映设置。
改写过程中要时刻注意设置文件有没有放错地方,有没有不小心改写了未加入管理的文件。
◉ 重启中间件
守护进程的启动和停止要使用 service 命令。
$ sudo service myapp restart
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论