第35章:生产关注点和Redis
¶
编程中最令人兴奋的部分之一是与世界分享你所创造的东西。对于网络应用,这通常意味着将你的项目部署到可以通过互联网访问的服务器上。
网络服务器可以是数据中心的专用机器,也可以是云端的容器,甚至可以是放在衣柜里的Raspberry Pi
。只要你的服务器能够运行Swift
,并且能够连接到互联网,你就可以用它来部署Vapor
应用程序。
在本章中,你将了解Vapor
的一些常见部署方法的优缺点。你还将学习如何正确优化、配置和监控你的应用程序,以提高效率和正常运行时间。
使用环境¶
每个Application
的实例都有一个相关的Environment
。每个环境都有一个String
名称。常见的环境包括:production
, development
, 和testing
. 你可以从Application
的environment
属性中获取当前环境。
print(req.application.environment) // "production"
在大多数情况下,环境是供你在配置你的应用程序时随意使用的。
然而,当在发布环境中运行时,Vapor
的某些部分会有不同的表现。一些区别包括在500
错误中隐藏调试信息和减少错误日志的冗长性。
正因为如此,当你在生产中运行你的应用程序时,请确保你使用production
环境。
选择一个环境¶
大多数模板包括代码,用于在应用程序运行时检测当前环境。如果你在项目的Run
模块中打开main.swift
,你会看到与下面类似的内容:
import App
import Vapor
var env = try Environment.detect()
try LoggingSystem.bootstrap(from: &env)
let app = Application(env)
defer { app.shutdown() }
try configure(app)
try app.run()
这段代码调用Environment.detect()
,它解析传递给你的应用程序的命令行参数并返回指定的环境。如果你没有指定环境,Vapor
默认使用development
。你可以使用--env
标志和环境名称来指定环境。
你可以在使用swift run
从命令行运行你的应用程序的可执行文件时这样做。
swift run Run serve --env development
你也可以在Xcode
中使用方案编辑器运行你的应用程序时指定环境。
Vapor
支持prod
表示production
,dev
表示development
的快捷键。它还支持-e
的缩写,即--env
的缩写。
$ swift run Run serve -e prod
用优化的方式进行编译¶
在开发你的应用程序时,你通常会使用Swift
的调试构建模式编译代码。调试构建模式速度快,并且在生成的二进制文件中包含有用的调试信息。Xcode
可以在以后使用这些信息来提供更多关于致命错误和断点调试的信息。
对于生产部署,你应该使用Swift的发布构建模式。在发布模式下构建时,Swift
会花更多时间分析和优化您的程序。虽然这增加了整体的构建时间,但它在运行时的性能改进是非常值得的。Swift
还从生成的二进制文件中删除了调试信息,使其更小。
Vapor
和Swift NIO
在发布构建模式下的行为也可能略有不同。这些软件包的一个常见模式是,在调试模式下,将可恢复的开发者错误转换成致命错误。这有助于开发人员在开发过程中快速追踪常见的错误,而不影响生产中的稳定性。
本节向您展示了如何在Xcode
和直接使用SwiftPM
启用发布构建模式。它还告诉你如何在发布模式下运行你的测试。这对依赖运行时性能的测试很有用。
在Xcode
中构建发布模式¶
您可以使用方案编辑器在Xcode
中启用发布构建模式。要在发布模式下构建,请为您的应用程序的可执行目标编辑方案。然后,在Build Configuration
下选择Release
。
要在发布模式下进行测试,再次为你的应用程序的可执行目标编辑方案。然后,从方案编辑器的左侧选择Test
,将Build Configuration
模式改为Release
。
使用SwiftPM
构建发布版¶
当部署到Linux
时,由于Xcode
不可用,你需要使用SwiftPM
来编译发布可执行文件。默认情况下,SwiftPM
以调试构建模式进行编译。要指定发布模式,请在您的构建命令中添加-c release
。
swift build -c release
当构建完成后,编译器会将生成的可执行文件的路径打印到终端。你可以复制并粘贴该路径来运行你的应用程序。
如果你访问构建文件夹,你可能会注意到与你的可执行二进制文件一起存在的其他文件。这些文件中包括由构建过程产生的任何共享库(macOS
上的.dylib
和Linux
上的.so
)。这些共享库是你的可执行文件运行所需要的。
你也可以用SwiftPM
在发布模式下运行你的测试。
swift test -c release
Note
有些功能,如@testable import
,在发布模式下测试时可能无法使用。
关于测试的说明¶
在类似生产环境中定期构建和测试你的代码,对于尽早发现问题非常重要。你将使用的一些模块,如Foundation
,根据平台的不同有不同的实现。实现上的细微差别会导致你的代码出现错误。有时,一个API
的实现可能在一个平台上还不存在。像Docker
这样的容器环境可以帮助你解决这个问题,使你可以很容易地在不同于主机的平台上测试你的代码,比如在Linux
上测试,而在macOS
上开发。
使用Docker
¶
Docker
是一个测试和部署Vapor
应用程序的伟大工具。部署步骤被编码为Dockerfile
,你可以将其与你的项目一起提交到源代码控制。你可以执行这个Dockerfile
来构建和运行你的应用程序的实例,在本地进行测试,或在你的部署服务器上进行生产。这样做的好处是,可以很容易地测试部署,创建新的部署,并跟踪你的代码部署方式的变化。
参见第33章"使用Docker
进行部署",了解更多信息。
进程监控¶
要运行一个Vapor
应用程序,你只需要启动SwiftPM
生成的可执行文件。
swift build -c release
.build/release/Run serve -e prod
虽然这对测试很有效,但它有一个主要问题:如果你的应用程序崩溃了怎么办?在这种情况下,你将需要登录到你的服务器并手动重启它。幸运的是,进程监控器可以帮助解决这个问题。
Supervisor
¶
Supervisor
,也叫supervisord
,是一个流行的Linux
进程监视器。这个程序允许你注册你想启动和停止的进程。如果其中一个进程崩溃了,Supervisor
会自动为你重新启动它。它还可以方便地将进程的stdout
和stderr
存储在/var/log
中,以便于访问。
Supervisor
通常在Ubuntu
上使用APT
安装,但可能因你的部署方式不同而不同。
apt-get install supervisor
一旦安装完毕,Supervisor
就可以通过Ubuntu
的systemctl
命令来启动。
systemctl restart supervisor
Supervisor
的配置文件存储在/etc/supervisor/conf.d
。在那里创建一个新的文件来管理你的Vapor应用程序,称为my-app.conf
。
// 1
[program:my-app]
command=/path/to/my-app/.build/release/Run serve -e prod
// 2
autostart=true
autorestart=true
// 3
stderr_logfile=/var/log/my-app.err.log
stdout_logfile=/var/log/my-app.out.log
下面是这个配置文件的具体内容:
- 声明一个新的
Supervisor
程序,使用serve
命令和生产环境启动你的应用程序的Run
可执行程序。 - 启用自动启动和自动重启,确保你的应用程序在服务器开启时始终运行。
- 配置
Supervisor
,使你的应用程序的stderr
和stdout
指向日志文件。
现在你已经添加了配置文件,运行以下命令来更新Supervisor
。
supervisorctl reread
supervisorctl update
你的应用程序现在应该正在运行。如果应用程序崩溃了,Supervisor
会注意到这一点,并立即尝试重新启动它。
Systemd
¶
另一个不需要你安装额外软件的选择叫做systemd
。它是Swift
支持的Linux
版本中的一个标准部分。关于如何使用systemd
配置你的应用程序,请参见第34章,"使用AWS
部署"。
反向代理¶
无论你在哪里或如何部署你的Vapor
应用程序,在nginx
这样的反向代理后面托管它通常是一个好主意。nginx
是一个非常快的、经过战斗检验的、易于配置的HTTP
服务器和代理。虽然Vapor
支持直接提供HTTP
请求,但在nginx
后面的代理可以提供更高的性能、安全性和易用性。
安装Nginx
¶
nginx
通常在Ubuntu
上使用APT
安装,但根据你的部署方式,可能会有所不同。
apt-get update
apt-get install nginx
安装完毕后,可以使用Ubuntu
的systemctl
命令来启动nginx
。
systemctl start nginx
systemctl restart nginx
systemctl stop nginx
启动后,可以在/etc/nginx/sites-enabled
创建一个新的站点配置。看一下下面的nginx
配置文件的例子:
server {
## 1
server_name hello.com;
## 2
listen 80;
## 3
root /home/vapor/Hello/Public/;
try_files $uri @proxy;
## 4
location @proxy {
## 5
proxy_pass http://127.0.0.1:8080;
## 6
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
## 7
proxy_connect_timeout 3s;
proxy_read_timeout 10s;
}
}
下面是nginx
配置的每一行的作用:
- 指定此配置用于对
hello.com
的请求。你可以在这里列出多个服务器名称。 - 指定此配置用于对
80
端口的请求,这是默认的HTTP端口。 - 为这个服务器指定一个文件根。任何对
hello.com/*
的请求,如果与这个文件夹中的文件名匹配,将由nginx
直接提供服务,绕过你的Vapor应用程序。 - 指定该服务器应该是一个反向代理。
- 将所有请求传递给
Vapor
程序,该程序绑定在127.0.0.1
端口8080
。 - 指定特殊的头文件,添加到传入的请求中。这些头信息有助于
Vapor
维护有关连接的客户端的信息。 - 为你的服务器指定连接和读取超时。
保存好配置文件后,重启nginx
以启用新的网站。接下来,确保你的Vapor
服务器在配置中指定的主机名和端口运行。现在你应该能够通过nginx
访问你的Vapor
服务器。
记录¶
在开发过程中使用Swift
的print
方法来记录日志是非常好的,甚至可以成为一些生产用例的合适选择。像Supervisor
这样的程序有助于将你的应用程序的打印输出汇总到服务器上的文件中,你可以根据需要访问。
然而,在某些情况下,你可能希望以不同的方式收集你的日志。例如,也许你更喜欢收集日志,并将它们发送到一个远程API
进行存储。你可能还想指定每条日志的重要性,这样你就知道如何对待它。Vapor
使用SwiftLog为你和你用来构建的所有包提供了一个一致的API
。
使用日志很容易;只需import Vapor
并从你的Request
或Application
中访问一个Logger
。
app.get("log-test") { req -> HTTPStatus in
req.logger.info("The route was called")
return .ok
}
记录器有几种可用的日志级别方法:
trace
:记录任何和所有信息。用于追踪特定的问题。debug
:用于调试问题。info
:表示发生了一个不常见的事件。notice
:用于通知应该注意的特定事件或状态,但不作为错误处理。warning
:表明有些事情应该被修复。error
:表明有什么地方出了问题。critical
:致命的错误。必须取消执行。
默认情况下,访问一个Logger
会产生一个ConsoleLogger
,它将你的日志输出到控制台,使用终端颜色来指定日志级别。
不过,SwiftLog
还有其他几种实现方式供你选择,可以在https://github.com/apple/swift-log#selecting-a-logging-backend-implementation-applications-only。
横向可扩展性¶
最后,在设计一个生产就绪的应用程序时,最重要的问题之一是可扩展性。随着你的应用程序的用户群增长和流量增加,你将如何跟上需求?你的瓶颈会是什么?刚开始的时候,一个合理的解决方案是随着流量的增加而增加服务器的资源--增加内存、更好的CPU
、更多的磁盘空间等等。这通常被称为scaling vertically
。
当你的应用程序的要求开始超过单台服务器的能力时,垂直扩展就会失败。最终,如果你的应用程序增长得足够大,你可能需要扩展到多个服务器。这就是所谓的horizontal scaling
。然而,水平扩展不仅在你用尽了垂直扩展的能力时才有用。扩展到多个便宜的服务器可能比单个昂贵的服务器更有成本效益。
负载平衡¶
现在你了解了横向扩展的一些好处,你可能想知道它实际上是如何工作的。这个概念的关键是负载平衡器。负载平衡器是轻量级的、快速的程序,坐在你的应用程序的服务器前面。当一个新的请求进来时,负载平衡器选择你的一个服务器来发送这个请求。
如果其中一个服务器不健康--响应缓慢或返回错误--负载平衡器可以暂时停止向该服务器发送请求。
在上图中,负载均衡器收到来自客户端的消息,并决定将请求转发给应用程序#3
。该应用为该请求生成一个响应,而负载均衡器将该响应送回给客户端。
虽然横向扩展的基础知识很简单,但你应该了解一些常见的陷阱,这些陷阱会阻止你的应用程序以这种方式进行扩展。最常见的是,这些问题与在服务器上本地存储信息有关。
为了更好地理解这一点,以下面这个将图片保存到磁盘的个人资料图片上传端点为例:
当客户A
向API
上传其资料图片时,负载均衡器将请求引导到应用程序#2
。这个应用程序处理请求并将图像保存到服务器的磁盘上。后来,当客户B
试图获取该图像时,负载平衡器将请求引导到应用程序#3
。运行应用程序#3
的服务器不知道该图像,所以它返回一个错误。客户端B
有可能被引导到应用程序#2
,成功获取图像,但那纯粹是运气。
这个问题的其他常见例子是内存会话缓存和SQLite
数据库。解决这个问题的一般方法是为你的应用程序的common
数据使用共享存储。这意味着你的应用程序的任何实例可能需要访问的数据。如果这些数据对服务器来说是私有的--例如,一个API
响应缓存--那么将其存储在本地是没有问题的。
有大量的工具供你使用,以使你的应用程序可扩展。对于文件上传,有像亚马逊网络服务的S3
桶这样的API
,让你从一个单一的远程来源存储和获取文件。你也可以将你的服务器配置成一个共享驱动器来存储文件,如下图所示:
在上面的例子中,客户B
对图像的请求成功了,因为App #2
和App #3
都可以访问同一个共享驱动器。对于数据库和会话,你可以使用非基于文件的数据库,如Redis
、MySQL
、PostgreSQL
、MongoDB
等。这些数据库在一个单独的服务器上运行,你的所有应用程序实例都可以访问。
如果你认为你的应用程序需要处理大量的流量,或者它有可能快速增长,那么在你设计和编写代码时要记住水平扩展性。
使用Redis
的会话¶
为了演示如何在一个应用程序中工作,请下载本章的启动项目。该项目是基于本书第一节中的TIL
应用。在Xcode
中打开该项目并构建应用程序。
Note
和前几章一样,你需要为该项目设置自定义工作目录。
当用户登录到网站时,应用程序将用户的ID
存储在一个相关的会话中。目前,该应用程序将会话存储在内存中。这带来了一些问题。
- 当你重新启动应用程序时,你会失去所有的会话。任何已登录的用户将不得不重新登录。
- 如果你横向扩展你的应用程序,会话是不共享的。如果一个用户登录到服务器
#1
,而该用户的下一个请求到了服务器#2
,它不知道这个会话,所以该用户不能访问任何受保护的路由。登录到2
号服务器会覆盖1
号服务器的会话信息,从而失去该会话。随着你的水平扩展,这导致问题的机会也会增加。
你可以通过将会话转移到数据库中来解决这个问题。Redis
是一个快速的内存数据库,有很多用途,它是这个用例的最佳选择。如果应用程序的所有实例都使用Redis
,它们可以共享会话。
在Xcode
中,打开configure.swift
。启动项目已经将Redis配置为Package.swift
中的一个依赖项。在import Leaf
下面,添加以下内容:
import Redis
这允许你看到Redis
的功能和类型。接下来,在你的应用程序中配置Redis
数据库。在app.databases.use(..)
下面添加以下内容:
// 1
let redisHostname = Environment
.get("REDIS_HOSTNAME") ?? "localhost"
// 2
let redisConfig =
try RedisConfiguration(hostname: redisHostname)
// 3
app.redis.configuration = redisConfig
下面是这个的作用:
- 将主机名设置为
REDIS_HOSTNAME
环境变量,如果它存在的话。否则,默认为localhost
。这允许你为托管方案注入主机名。 - 使用该主机名创建一个
RedisConfiguration
。 - 在应用程序的
Redis
服务上设置RedisConfiguration
。
接下来,在app.views.use(.leaf)
后面添加以下内容:
app.sessions.use(.redis)
这告诉应用程序在存储会话数据时要使用Redis
。
最后,将app.middleware.use(app.session.middleware)
移到app.session.use(.redis)
下面。这可以确保会话中间件使用Redis
会话配置。
这就是在会话中使用Redis
所需要的全部内容!
在终端,输入以下内容来启动数据库:
# 1
docker run --name postgres \
-e POSTGRES_DB=vapor_database \
-e POSTGRES_USER=vapor_username \
-e POSTGRES_PASSWORD=vapor_password \
-p 5432:5432 -d postgres
# 2
docker run --name redis -p 6379:6379 -d redis
下面是这个的作用:
-
启动
PostgreSQL
数据库。 -
运行一个名为
postgres
的新容器。 - 通过环境变量指定数据库的名称、用户名和密码。
- 允许应用程序连接到
PostgreSQL
服务器的默认端口:5432
. - 在后台作为一个守护程序运行服务器。
-
为这个容器使用名为
postgres
的Docker
镜像。如果你的机器上没有这个镜像,Docker
会自动下载它。 -
启动
Redis
数据库: -
运行一个名为
redis
的新容器。 - 允许应用程序连接到Redis服务器的默认端口:
6379
. - 在后台作为一个守护程序运行服务器。
- 为这个容器使用名为
redis
的Docker
镜像。如果你的机器上没有这个镜像,Docker
会自动下载它。
在Xcode
中构建并运行该应用程序。在你的浏览器中,导航到http://localhost:8080/。点击Create An Acronym
,该应用程序会将你重定向到登录页面。用用户名admin
和密码password
登录。点击Create An Acronym
,你就可以查看该页面:
在Xcode
中,停止并启动应用程序,在浏览器中刷新页面。应用程序知道你仍然在登录,因为它将会话存储在Redis
而不是内存中。
接下来去哪?¶
你现在明白了将你的Swift
网络应用程序转移到生产中时要避免的常见陷阱。现在是时候把这里列出的最佳实践和有用的工具用起来了。这里有一些额外的资源,在你继续磨练你的技能时,它们应该被证明是无价的: