第37章:微服务,第二部分¶
在上一章中,你了解了微服务的基础知识,以及如何将该架构应用于TIL
应用。在这一章中,你将学习API
网关,以及如何让客户访问微服务。最后,你将学习如何使用Docker
和Docker Compose
来启动整个应用。
API
网关¶
上一章介绍了TIL
应用程序的两个微服务,一个用于缩写,一个用于用户。在一个真实的应用中,你可能会有更多的服务用于你的应用的各个不同方面。客户端很难与一个由如此多的微服务组成的应用程序集成。每个客户端都需要知道每个微服务做什么,以及每个服务的URL
。客户端甚至可能要为每个服务使用不同的认证方法。微服务架构让人很难把一个服务拆成独立的服务。例如,将认证从TIL
应用中的用户服务中移出,就需要对所有客户端进行更新。
这个问题的一个解决方案是API
网关。一个API
网关可以聚合来自客户的请求,并将其分配给所有需要的服务。此外,API
网关可以从多个服务中检索结果,并将其合并为一个响应。
大多数云供应商提供API
网关解决方案来管理大量的微服务,但你也可以轻松地创建自己的。在本章中,你就可以做到这一点。
下载本章的启动项目。TILAppUsers
和TILAppAcronyms
项目与前一章的最终项目相同。还有一个新的TILAppAPI
项目,包含API
网关的骨架。
启动服务¶
在终端,打开三个独立的标签。确保MySQL
、PostgreSQL
和Redis Docker
容器在前一章中运行。在终端中,输入以下内容:
docker ps
这个命令显示了当前正在运行的容器。你应该看到三个正在运行的容器:
接下来,在第一个选项卡中,导航到TILAppUsers
目录并运行以下命令:
swift run
这将启动TILAppUsers
服务。在第二个选项卡中,导航到TILAppAcronyms
并运行以下命令:
swift run
这将启动TILAppAcronyms
服务。最后,在第三个标签中,导航到TILAppAPI
目录,并输入这个命令:
open Package.swift
这将打开Xcode
项目并开始下载依赖项。
转发请求¶
在TILAppAPI
的Xcode
项目中,打开UsersController.swift
。在boot(route:)
下面输入以下内容:
// 1
func getAllHandler(_ req: Request)
-> EventLoopFuture<ClientResponse> {
return req.client.get("\(userServiceURL)/users")
}
// 2
func getHandler(_ req: Request) throws
-> EventLoopFuture<ClientResponse> {
let id = try req.parameters.require("userID", as: UUID.self)
return req.client.get("\(userServiceURL)/users/\(id)")
}
// 3
func createHandler(_ req: Request)
-> EventLoopFuture<ClientResponse> {
return req.client.post("\(userServiceURL)/users") {
createRequest in
// 4
try createRequest.content.encode(
req.content.decode(CreateUserData.self))
}
}
以下是新代码中的情况:
- 创建一个路由处理程序来获取所有的用户。简单地返回来自
TILAppUsers
微服务的/users
路由的响应。 - 创建一个路由处理程序来获取单个用户。从请求的参数中获取用户的
UUID
,并从TILAppUsers
微服务中返回该用户的响应。 - 创建一个路由处理程序来创建一个用户。向
TILAppUsers
服务的users
路由发送一个POST
请求并返回响应。 - 在你发送请求之前,将请求到
API
网关的数据编码到TILAppUsers
服务的请求中。这是创建一个用户所需的数据。
要注册新的路由,请在boot(routes:)
的末尾添加以下内容:
// 1
routeGroup.get(use: getAllHandler)
// 2
routeGroup.get(":userID", use: getHandler)
// 3
routeGroup.post(use: createHandler)
下面是这个的作用:
- 将一个到
/api/users/
的GET
请求路由到`getAllHandler(_:)'。 - 将
/api/users/<USER_ID>
的GET
请求路由到getHandler(_:)
,使用userID
作为动态参数名。 - 将
/api/users/
的POST
请求路由到`createHandler(_:)'。
这些请求不需要任何认证或多重服务。你可以把它们直接转发到TILAppUsers
微服务上。
打开AcronymsController.swift
,为GET
请求做同样的事情。在boot(routes:)
下面添加以下内容:
// 1
func getAllHandler(_ req: Request)
-> EventLoopFuture<ClientResponse> {
return req.client.get("\(acronymsServiceURL)/")
}
// 2
func getHandler(_ req: Request) throws
-> EventLoopFuture<ClientResponse> {
let id =
try req.parameters.require("acronymID", as: UUID.self)
return req.client.get("\(acronymsServiceURL)/\(id)")
}
以下是新代码的作用:
- 创建一个路由处理程序来获取所有的首字母缩写。简单地从
TILAppAcronyms
微服务的/
路由返回响应。 - 创建一个路由处理程序来获取一个首字母缩写。从请求的参数中获取首字母缩写的
UUID
。从TILAppronyms
微服务中返回该首字母缩写的响应。
要注册新的路由,请在boot(routes:)
的末尾添加以下内容:
// 1
acronymsGroup.get(use: getAllHandler)
// 2
acronymsGroup.get(":acronymID", use: getHandler)
下面是这个的作用:
- 将一个
GET
请求路由到/api/acronyms/
的getAllHandler(_:)
。 - 将
GET
请求路由到/api/acronyms/<ACRONYM_ID>
到getHandler(_:)
。
建立并运行应用程序,并启动RESTed。配置一个新的请求,如下所示:
- URL: http://localhost:8080/api/users
- method: GET
点击Send Request
,你会看到TILAppUsers
微服务中的所有用户:
API
认证¶
登录¶
API
网关的认证方式与微服务的认证方式完全相同。首先,你必须允许一个用户登录。
在Xcode
中,打开UsersController.swift
。在createHandler(_:)
下面,添加一个新的路由处理程序来处理登录:
func loginHandler(_ req: Request)
-> EventLoopFuture<ClientResponse> {
// 1
return req.client.post("\(userServiceURL)/auth/login") {
loginRequest in
// 2
guard let authHeader =
req.headers[.authorization].first else {
throw Abort(.unauthorized)
}
// 3
loginRequest.headers.add(
name: .authorization,
value: authHeader)
}
}
以下是新的路径处理程序的作用:
- 发送一个
POST
请求给TILAppUsers
微服务,以登录用户。 - 确保传入的请求包含一个
Authorization
头。否则,返回一个401 Unauthorized
的响应。 - 用传入请求中的授权头对传出请求进行编码。这个头包含用户的
HTTP
基本认证信息。
要注册路由,请在boot(routes:)
的末尾添加以下内容:
routeGroup.post("login", use: loginHandler)
这将一个POST
请求发送到/api/users/login
的路由到loginHandler(_:)
。建立并运行应用程序,并启动RESTed
。配置一个新的请求,如下所示:
- URL: http://localhost:8080/api/users/login
- method: POST
点击Authorization
,输入上一章创建的用户的用户名和密码。勾选Present Before Authentication Challenge
,然后点击OK
。
点击Send Request
,你会收到该用户的令牌。复制令牌的值:
访问受保护的路由¶
回到Xcode
中,打开AcronymsController.swift
。在getHandler(_:)
下面,创建一个新的路由处理程序来创建一个首字母缩写:
func createHandler(_ req: Request)
-> EventLoopFuture<ClientResponse> {
// 1
return req.client.post("\(acronymsServiceURL)/") {
createRequest in
// 2
guard let authHeader =
req.headers[.authorization].first else {
throw Abort(.unauthorized)
}
// 3
createRequest.headers.add(
name: .authorization,
value: authHeader)
// 4
try createRequest.content.encode(
req.content.decode(CreateAcronymData.self))
}
}
以下是代码的作用:
- 向
TILAppAcronyms
微服务发送一个POST
请求,创建一个新的缩写。 - 确保传入的请求包含一个
Authorization
头,否则返回一个401 Unauthorized
的响应。 - 将
Authorization
头添加到传出的请求中,到TILAppAcronyms
微服务。 - 用数据对传出请求的主体进行编码,以创建一个首字母缩写。数据来自于传入的请求。
在boot(routes:)
下面的acronymsGroup.get(":acronymID", use: getHandler)
中注册路由处理器:
acronymsGroup.post(use: createHandler)
这将一个POST
请求路由到/api/acronyms
到createHandler(_:)
。建立并运行应用程序,并返回到RESTed
。点击Authorization
,取消Present Before Authentication Challenge
,以阻止RESTed
在头中发送HTTP Basic Authentication
凭证。
然后,配置一个新的请求,如下所示:
- URL: http://localhost:8080/api/acronyms/
- method: POST
- Parameter encoding: JSON-encoded
添加两个带有名称和值的参数:
- short: IRL
- long: In Real Life
为Authorization
创建一个新的头域,其值为Bearer <TOKEN STRING>
,使用你之前复制的令牌字符串。
点击Send Request
,你会看到通过API
网关在TILAppAcronyms
微服务中创建的缩写:
回到Xcode
中,创建用于更新和删除首字母缩写的路由处理程序。在createHandler(_:)
下面,添加以下内容:
func updateHandler(_ req: Request) throws
-> EventLoopFuture<ClientResponse> {
// 1
let acronymID =
try req.parameters.require("acronymID", as: UUID.self)
// 2
return req.client
.put("\(acronymsServiceURL)/\(acronymID)") {
updateRequest in
// 3
guard let authHeader =
req.headers[.authorization].first else {
throw Abort(.unauthorized)
}
// 4
updateRequest.headers.add(
name: .authorization,
value: authHeader)
// 5
try updateRequest.content.encode(
req.content.decode(CreateAcronymData.self))
}
}
func deleteHandler(_ req: Request) throws
-> EventLoopFuture<ClientResponse> {
// 6
let acronymID =
try req.parameters.require("acronymID", as: UUID.self)
// 7
return req.client
.delete("\(acronymsServiceURL)/\(acronymID)") {
deleteRequest in
// 8
guard let authHeader =
req.headers[.authorization].first else {
throw Abort(.unauthorized)
}
// 9
deleteRequest.headers.add(
name: .authorization,
value: authHeader)
}
}
以下是新代码的作用:
- 从请求的参数中获取首字母缩写的
ID
。 - 向
TILAppAcronyms
微服务发送请求以更新该首字母缩写。返回响应。 - 在发送请求之前,确保传入的请求包含一个
Authorization
头。如果没有,则返回一个401 Unauthorized
的响应。 - 在发出的请求中添加
Authorization
头。 - 用数据对发出的请求的正文进行编码,以更新缩写。这些数据来自传入的请求。
- 从请求的参数中获取首字母缩写的
ID
。 - 向
TILAppAcronyms
微服务发送请求,删除该首字母缩写。返回响应。 - 在发送请求之前,确保传入的请求包含一个
Authorization
头。如果没有,则返回一个401 Unauthorized
的响应。 - 在发出的请求中添加
Authorization
头。
最后,为了注册新的路由,在boot(routes:)
的末尾添加以下内容:
// 1
acronymsGroup.put(":acronymID", use: updateHandler)
// 2
acronymsGroup.delete(":acronymID", use: deleteHandler)
下面是这个的作用:
- 将一个
PUT
请求路由到/api/acronyms/<ID>
到updateHandler(_:)
。 - 将一个
DELETE
请求路由到/api/acronyms/<ID>
到deleteHandler(_:)
。
处理关系¶
在上一章中,你看到了关系如何在微服务中发挥作用。对于微服务架构中的客户端来说,获取不同模型的关系是很困难的。你可以使用API网关来帮助简化这个问题。
获取用户的首字母缩写¶
在Xcode
中,打开UsersController.swift
。在loginHandler(_:)
下面,添加一个新的路由处理程序来获取用户的首字母缩写:
func getAcronyms(_ req: Request) throws
-> EventLoopFuture<ClientResponse> {
// 1
let userID =
try req.parameters.require("userID", as: UUID.self)
// 2
return req.client
.get("\(acronymsServiceURL)/user/\(userID)")
}
以下是发生的情况:
- 从请求的参数中获取用户的
ID
。 - 向
TILAppAcronyms
微服务发送请求,以获得该用户的所有首字母缩写,并返回响应。
要注册新的路由,请在boot(routes:)
的末尾添加以下内容:
routeGroup.get(":userID", "acronyms", use: getAcronyms)
这将一个GET
请求路由到/api/users/<user_ID>/acronyms
到getAcronyms(_:)
。
获取一个首字母缩写的用户¶
获取一个用户的首字母缩写词看起来和微服务中的其他请求一样,因为客户端知道用户的ID
。获取某个特定首字母缩写的用户则比较复杂。打开AcronymsController.swift
,在deleteHandler(_:)
下面添加一个新的路由处理程序来完成这个任务:
func getUserHandler(_ req: Request) throws
-> EventLoopFuture<ClientResponse> {
// 1
let acronymID =
try req.parameters.require("acronymID", as: UUID.self)
// 2
return req
.client
.get("\(acronymsServiceURL)/\(acronymID)")
.flatMapThrowing { response in
// 3
return try response.content.decode(Acronym.self)
// 4
}.flatMap { acronym in
// 5
return req
.client
.get("\(userServiceURL)/users/\(acronym.userID)")
}
}
以下是新的路径处理程序的作用:
- 从请求的参数中获取首字母缩写的
ID
。 - 向
TILAppAcronyms
发出请求,以获得该首字母缩写的详细信息。 - 将响应解码为一个
Acronym
并返回结果。 - 使用
flatMap(_:)
从前一个期货链中获得Acronym
并将其传递到另一个链中。通过期货链,你可以避免在catch
语句中包裹任何try
。 - 使用首字母缩写的用户
ID
向TILAppUsers
发出请求。
这个路由处理程序需要对两个微服务进行请求。API
网关使客户的请求变得简单,就像单体的TIL应用一样。在boot(routes:)
下面的acronymsGroup.delete(":acronymID", use: deleteHandler)
中注册该路由:
acronymsGroup.get(":acronymID", "user", use: getUserHandler)
这就把对/api/acronyms/<ACRONYM_ID>/user
的GET
请求路由到getUserHandler(_:)
。建立并运行应用程序,并启动RESTed
。配置一个新的请求,如下所示:
- URL: http://localhost:8080/api/acronyms/
/user - method: GET
点击Send Request
。API
网关向所有的微服务发出必要的请求,以获得该ID
的缩写的用户。你会看到返回的用户信息:
最后,在Xcode
中停止TILAppAPI
应用程序。
在Docker
中运行一切¶
现在你有三个微服务,组成了你的TIL
应用程序。这些微服务还需要另外三个数据库来工作。如果你正在开发一个客户端应用程序,或另一个微服务,有很多东西需要运行才能开始。你可能还想在Linux
中运行一切,以检查你的服务部署是否正确。就像在第11章"测试"中,你将使用Docker Compose
来运行一切。
注入服务URLs
¶
目前,应用程序将不同微服务的URL硬编码为localhost
。你必须改变这一点以在Docker Compose
中运行它们。回到Xcode
的TILAppAPI
,打开AcronymsController.swift
。将userServiceURL
和acronymsServiceURL
的定义替换为以下内容:
let acronymsServiceURL: String
let userServiceURL: String
init(
acronymsServiceHostname: String,
userServiceHostname: String) {
acronymsServiceURL =
"http://\(acronymsServiceHostname):8082"
userServiceURL = "http://\(userServiceHostname):8081"
}
这样你就可以为不同的服务注入主机名。打开UsersController.swift
,再次将userServiceURL
和acronymsServiceURL
的定义替换为以下内容:
let userServiceURL: String
let acronymsServiceURL: String
init(
userServiceHostname: String,
acronymsServiceHostname: String) {
userServiceURL = "http://\(userServiceHostname):8081"
acronymsServiceURL =
"http://\(acronymsServiceHostname):8082"
}
最后,打开routes.swift
,用以下内容替换routes(_:)
的主体:
let usersHostname: String
let acronymsHostname: String
// 1
if let users = Environment.get("USERS_HOSTNAME") {
usersHostname = users
} else {
usersHostname = "localhost"
}
// 2
if let acronyms = Environment.get("ACRONYMS_HOSTNAME") {
acronymsHostname = acronyms
} else {
acronymsHostname = "localhost"
}
// 3
try app.register(collection: UsersController(
userServiceHostname: usersHostname,
acronymsServiceHostname: acronymsHostname))
try app.register(collection: AcronymsController(
acronymsServiceHostname: acronymsHostname,
userServiceHostname: usersHostname))
以下是变化的内容:
- 如果环境变量存在,使用
USERS_HOSTNAME
作为用户微服务的主机名。否则,默认为localhost
。 - 如果环境变量存在,使用
ACRONYMS_HOSTNAME
作为缩略语微服务的主机名。否则,默认为`localhost'。 - 将
UsersController
和AcronymsController
注册为RouteCollection
,注入主机名。
构建项目以确保所有东西都能编译并关闭Xcode
。现在,用TILAppUsers
打开标签,用Control-C
停止应用程序,因为你不再需要一个独立的实例运行。
接下来,打开TILAppAcronyms
的标签,用Control-C
停止该应用程序。在Xcode
中打开该项目,并打开UserAuthMiddleware.swift
。在respond(to:chainingTo:)
之前添加以下内容:
let authHostname: String
init(authHostname: String) {
self.authHostname = authHostname
}
这允许你传入TILAppUsers
微服务的主机名。接下来,将中间件发出请求的URL
--http://localhost:8081/auth/authenticate
--替换为以下内容:
"http://\(authHostname):8081/auth/authenticate"
这使用了传入的主机名来进行请求。最后,打开AcronymsController.swift
,在boot(routes:)
中,将let authGroup = routes.grouped(UserAuthMiddleware())
改为以下内容:
let authHostname: String
// 1
if let host = Environment.get("AUTH_HOSTNAME") {
authHostname = host
} else {
authHostname = "localhost"
}
// 2
let authGroup = routes.grouped(
UserAuthMiddleware(authHostname: authHostname))
以下是新代码的作用:
- 检查是否有
AUTH_HOSTNAME
环境变量,并使用authHostname
的值。如果环境变量不存在,则默认为localhost
。 - 使用
UserAuthMiddleware
创建一个路由组,并传入authHostname
。
构建项目以确保代码的编译。
Docker Compose
文件¶
在包含所有三个项目的根目录下,创建一个名为docker-compose.yml
的新文件,并在你选择的编辑器中打开它。添加以下内容来定义版本和数据库服务:
# 1
version: '3'
services:
# 2
postgres:
image: "postgres"
environment:
- POSTGRES_DB=vapor_database
- POSTGRES_USER=vapor_username
- POSTGRES_PASSWORD=vapor_password
# 3
mysql:
image: "mysql"
environment:
- MYSQL_USER=vapor_username
- MYSQL_PASSWORD=vapor_password
- MYSQL_DATABASE=vapor_database
- MYSQL_RANDOM_ROOT_PASSWORD=yes
# 4
redis:
image: "redis"
以下是正在发生的事情:
- 设置
Docker Compose
文件的版本号。 - 为
PostgreSQL
数据库定义一个服务。使用postgres
图像和与你的本地Docker
容器相同的环境变量。 - 为
MySQL
数据库定义一个服务。使用mysql
图像和与你的本地Docker
容器相同的环境变量。 - 为
Redis
数据库定义一个服务。使用redis
镜像。
在文件的最后,为TILAppUsers
微服务添加以下内容:
# 1
til-users:
# 2
depends_on:
- postgres
- redis
# 3
build:
context: ./TILAppUsers
dockerfile: Dockerfile
# 4
environment:
- DATABASE_HOST=postgres
- REDIS_HOSTNAME=redis
- PORT=8081
- ENVIRONMENT=production
Note
缩进必须与定义的其他服务相匹配。
下面是新代码的作用:
- 为
TILAppUsers
定义一个服务。 - 告诉
Docker Compose
这个服务依赖于postgres
和redis
容器。Docker Compose
将在TILAppUsers
之前启动这些服务。 - 告诉
Docker Compose
该服务的工作目录和要使用的Docker
文件。默认的Vapor
模板包含一个兼容的Dockerfile
。 - 为服务设置必要的环境变量。这些定义了数据库以及环境和端口所需的变量。
你可能注意到这个服务没有在Docker Compose
之外暴露任何端口。因为你通过API
网关路由一切,没有必要暴露其他微服务。
在文件的最后,添加TILAppAcronyms
的规范:
# 1
til-acronyms:
# 2
depends_on:
- mysql
- til-users
# 3
build:
context: ./TILAppAcronyms
dockerfile: Dockerfile
# 4
environment:
- DATABASE_HOST=mysql
- PORT=8082
- ENVIRONMENT=production
- AUTH_HOSTNAME=til-users
以下是新规范的作用:
- 为
TILAppAcronyms
定义一个服务。 - 告诉
Docker Compose
这个服务依赖于mysql
和til-users
容器。Docker Compose
会在TILAppAcronyms
之前启动这些服务。 - 告诉
Docker Compose
该服务的工作目录和要使用的Docker
文件。默认的Vapor
模板包含一个兼容的Dockerfile
。 - 为服务设置必要的环境变量。这些定义了数据库所需的变量以及环境和端口。这也设置了
AUTH_HOSTNAME
环境变量,所以这个服务可以向TILAppUsers
发送请求。
最后,在文件的最后,添加TILAppAPI
的规范:
# 1
til-api:
# 2
depends_on:
- til-users
- til-acronyms
# 3
ports:
- "8080:8080"
# 4
build:
context: ./TILAppAPI
dockerfile: Dockerfile
# 5
environment:
- USERS_HOSTNAME=til-users
- ACRONYMS_HOSTNAME=til-acronyms
- PORT=8080
- ENVIRONMENT=production
以下是新规范的作用:
- 为
TILAppAPI
定义一个服务。 - 告诉
Docker Compose
这个服务依赖于til-users
和til-acronyms
容器。Docker Compose
会在TILAppAcronyms
之前启动这些服务。 - 将容器的
8080
端口暴露给你的本地机器,端口为8080
。这允许你连接到该容器。 - 告诉
Docker Compose
服务的工作目录和要使用的Docker
文件。默认的Vapor
模板包含一个兼容的Dockerfile
。 - 为服务设置必要的环境变量。这定义了环境和端口。这也设置了
USERS_HOSTNAME
和ACRONYMS_HOSTNAME
环境变量,因此这个服务可以向TILAppUsers
和TILAppAcronyms
发送请求。
修改Dockerfiles
¶
在你可以运行一切之前,你必须修改Dockerfiles
。Docker Compose
按要求的顺序启动不同的容器,但不会等待它们准备好接受连接。如果你的Vapor
应用程序试图在数据库准备好之前连接到数据库,这将导致问题。在TILAppAcronyms
中,打开Dockerfile
并替换:
ENTRYPOINT ["./Run"]
CMD ["serve", "--env", "production", "--hostname", "0.0.0.0", "--port", "8080"]
为下面内容:
ENTRYPOINT sleep 20 && \
./Run serve --env $ENVIRONMENT --hostname 0.0.0.0 --port $PORT
这告诉容器在启动Vapor
应用程序之前要等待20
秒。这应该给数据库足够的时间来启动。在实际应用中,你可能要考虑把这个放在一个脚本中,在启动Vapor
应用之前测试数据库。你也可以参考第33章,"用Docker
部署",以获得更强大的解决方案。
在TILAppUsers
中,打开Dockerfile
,做与上面相同的修改。
运行一切¶
现在你已经准备好在Docker Compose
中启动你的应用程序了。在Terminal
中,在包含docker-compose.yml
的目录中,输入以下内容:
docker-compose up
这将下载和构建docker-compose.yml
中指定的所有容器,并启动它们。请注意,构建所有的微服务可能需要一些时间。
当一切都启动和运行时,你会看到类似的东西:
然后你可以像以前一样打开RESTed
并提出请求。
接下来去哪?¶
在本章中,你学到了如何使用Vapor
来创建一个API
网关。这使得客户与你的不同微服务的交互变得简单。你学会了如何在不同的微服务之间发送请求并返回单一的响应。你还学会了如何使用Docker Compose
来构建和启动所有的微服务,并将它们连接起来。
你现在已经掌握了编写强大的微服务所需的基本知识。你可以用消息队列、协议缓冲区和远程程序性调用来进一步加强。你现在可以构建的应用是没有限制的!