大多数浏览器和
Developer App 均支持流媒体播放。
-
探索 Swift on Server 生态系统
Swift 这种语言非常适合用来编写服务器端应用程序代码,而且为 Apple 各款云端产品的关键服务提供了强大支持。我们将探索相关工具、深入研究 Swift 服务器软件包生态系统,还将展示如何与数据库交互,以及为应用程序增加可观测性。
章节
- 0:00 - Introduction
- 0:13 - Agenda
- 0:27 - Meet Swift on Server
- 2:30 - Build a service
- 3:46 - Swift OpenAPI generator
- 5:42 - Database drivers
- 10:53 - Observability
- 15:19 - Explore the ecosystem
- 16:12 - Wrap up
资源
相关视频
WWDC23
-
下载
大家好 我叫 Franz 来自 Apple Swift on Server 团队 今天 我们将探讨 Swift on Server 生态系统 首先 我们将讨论 Swift 为什么是 开发服务器应用程序的绝佳语言 然后 我们将使用生态系统中的 一些流行软件包来构建一项服务
最后 我们将探讨 生态系统的运作方式 以及从哪里了解更多信息
首先 我们来了解 Swift 为什么非常适合 进行服务器应用程序开发
Swift 采用自动引用计数 而不是垃圾回收 因此可让你 实现类似 C 语言的性能 同时占用较少的内存 这使得 Swift 非常适合 那些需要可预测资源消耗 和快速启动时间的 现代云服务
Swift 是一种表现力强 且安全的语言 在编译时可消除一系列错误 让开发者编写出强大且 可靠的分布式系统 强类型、可选类型和 内存安全等特性 使 Swift 服务不易出现崩溃 和安全漏洞
云服务经常需要处理 高并发的工作负载 Swift 一流的并发功能 让开发者能够编写 可缩放且响应迅速的服务器应用程序 同时消除数据争用导致的常见错误
所有这些特性使 Swift 成为 编写服务器应用程序的绝佳选择 事实上 Swift 为 iCloud 钥匙串、 “照片”和“备忘录”等 Apple 云服务的许多 关键功能提供支持 其他用例包括 App Store 处理管道 和同播共享文件共享 最后 我们全新的 Private Cloud Compute 服务 是使用 Swift on Server 构建的
在 Apple 的各项服务中 使用 Swift on Server 的 应用程序每秒 可处理数百万个请求
在 Apple 平台之外 Server 生态系统 是 Swift 的最早用户之一 事实上 Swift Server 工作组 成立于 2016 年 比 Swift 实现开源 只晚了一年时间 这使它成为历史最悠久的工作组
这个工作组由 使用 Swift on Server 的 成员公司以及生态系统的 个人贡献者组成 致力于推广使用 Swift 来开发和部署服务器应用程序 这个工作组的职责包括 定义并优先完成满足 服务器社区需求的工作 运行软件包孵化流程 以减少重复工作 提高兼容性并推广最佳实践
工作组还将服务器生态系统的反馈 传达给 Swift 项目的其他小组 现在 让我们使用 服务器生态系统中的 一些流行软件包来 构建一项服务 我和我的同事今年 计划参加很多活动 为确保有条不紊 我们想实现一项活动服务 用于追踪谁参加哪项活动 这个服务应支持两项操作 一是列出所有活动 以显示谁计划参加什么活动 既然我不能错过慕尼黑啤酒节 我们就需要另一项操作 来创建新活动
要处理 Swift 软件包 你可以使用不同的编辑器 如 Xcode、VS Code、Neovim 或任何其他支持语言 服务器协议的编辑器 在今天的演示中 我们将使用 VS Code 来处理软件包 在演示过程中 我们将使用 VS Code 底部的内置终端 来查看服务的输出 并向服务发送请求 我已经准备了一个软件包 供我们开始实现活动服务 我们来看看吧
这个软件包依赖于 OpenAPI 生成器 并使用 Vapor 作为 OpenAPI 的服务器传输
软件包有两个目标
一个是 EventAPI 目标 这个目标已配置 OpenAPIGenerator 插件 另一个目标
是 EventService executableTarget 这个目标包含我们服务的实现
通过 Swift OpenAPI Generator 我们可以用 YAML 记录服务 并为服务器和客户端生成代码 如果你是新手或想查看 OpenAPI 请观看去年的讲座 “认识 Swift OpenAPI Generator” 我们来看看 OpenAPI 文档
这个文档定义了我们活动 路径中的两项操作
第一项操作是一个名为 listEvents 的 get 方法
这个操作会返回一个包含 活动数组的成功响应
第二项操作是一个名为 createEvent 的 post 方法
这个操作会接收活动的 JSON 主体
并根据创建是否成功 返回 201 或 400 状态代码
我们的服务包含主要入口点
首先创建一个 Vapor 应用程序
然后 在应用程序中创建 OpenAPI VaporTransport
接下来 创建一个服务实例 并在传输系统上注册
最后 执行 Vapor 应用程序 这将启动 HTTP 服务器 并监听传入连接
我们的服务还实现 生成的 APIProtocol
listEvents 方法 返回一个硬编码的活动数组
createEvent 方法 目前返回未实现的状态代码
让我们继续启动我们的服务
这将构建我们的服务 并为它附加调试器 在终端底部 可以看到服务器已启动
现在 可以在另一个终端中 使用 curl 查询我们的服务 从而列出所有活动
很好 我们的服务返回了一个 JSON 数组 其中包含硬编码的活动列表
不过 我们希望动态添加新活动 并将这些活动持久保存在数据库中 因此让我们来看看数据库驱动程序 开源生态系统中有许多 不同的数据库驱动程序 如 PostgreSQL、MySQL、 Cassandra、MongoDB 等
今天 我们将使用 Postgres 数据库进行持久化 PostgresNIO 是 Postgres 的 开源数据库驱动程序 由 Vapor 和 Apple 维护 PostgresNIO 1.21 新增了 PostgresClient PostgresClient 提供 一个全新的异步接口 并随附一个利用结构化并发 功能的内置连接池 从而使它能够抵御 数据库间歇性联网故障 此外 连接池通过将查询分配到 多个连接并预热连接来提高吞吐量 从而加快查询执行速度
让我们继续使用 PostgresNIO 将 EventService 连接到数据库
首先 要在软件包中添加 PostgresNIO 的依赖项 并在服务中导入这个依赖项 然后 在 listEvents 方法中 使用 PostgresClient 来 查询数据库 最后 我们将实现 createEvent 方法 把新活动插入数据库 首先 让我们在软件包清单中 添加 PostgresNIO 的 依赖项
然后 我们可以为 EventService 目标添加依赖项
现在 我们可以在服务中 导入 PostgresNIO
接下来 要在服务中添加一个 PostgresClient 属性
我们将使用客户端 在 listEvents 方法中查询数据库
查询方法会返回 行的 AsyncSequence 为替换硬编码的活动列表 我们将遍历行 解码字段 并为每一行创建一项活动
查询方法返回的 AsyncSequence 将自动从数据库中预取行 以提升性能
要再次运行我们的服务 必须创建一个 PostgresClient 并将它传递给我们的服务
首先 我们创建一个 PostgresClient 以连接到 我已经在本地启动的数据库
接下来 要把 PostgresClient 传给我们的服务
要启动客户端 需要调用它的运行方法 这个方法将接管当前任务 直到任务完成 由于要同时运行 Vapor 应用程序 和 PostgresClient 因此我们将使用一个任务组 我们将创建一个丢弃任务组 并添加一个运行 PostgresClient 的子任务
然后 将 Vapor 应用程序执行 移到一个单独的子任务中
让我们再次运行服务
重启按钮将停止当前进程 重建服务并再次启动
底部的终端显示它正在运行 让我们再次列出所有活动
数据库似乎是空的 要在数据库中添加新活动 接下来要实现 createEvent 方法
首先 必须切换输入 并提取 JSON 活动
然后 查询数据库以插入新活动
最后 必须返回一个响应 以表明已创建活动
看到这段代码 有些人可能会警觉起来 因为在其他语言中 这是 SQL 注入漏洞的常见载体 尽管这看起来像一个字符串 但它并不是字符串 而是使用 Swift 的 字符串插值功能 将字符串查询转换为 带值绑定的参数化查询 这使得它完全安全 不会受到 SQL 注入攻击 这个例子很好地证明了 Swift 在保证代码安全的同时 还致力于符合人体工程学
我们将重启服务
服务再次运行后 我们将使用 curl 创建两项活动
看起来活动创建成功了 让我们再次列出所有活动 以检查数据库中 是否已存储这些活动
很好 所有活动 都已保存在数据库中 Gus 刚刚给我发了一条信息 说他想带一个朋友来 问我能不能在他的名下 再添加一个活动条目 让我们继续 为 Gus 创建另一项活动
尝试在 Gus 名下添加 另一个活动条目时好像出了点问题 在底部的终端中 我们可以看到 一条很长的错误信息 但是错误信息并没有告诉我们 到底是什么出了问题 我们看到的唯一信息是 操作无法完成 以及抛出的错误 属于 PSQLError 类型 PSQLError 的描述 有意省略了详细信息 以防止意外泄漏数据库信息 比如表的架构 在这种情况下 为服务添加额外的可观测性 有助于故障诊断 可观测性包括三大要素: 日志记录、指标和跟踪 日志记录可帮助你准确了解 服务的运行情况 让你在对问题进行排除诊断时 能够深入了解细节 指标可以让你一目了然地获得 服务健康状况的简要概览 日志和指标可以帮助你了解 单个服务的运行情况 而现代云系统通常是分布式 系统的集合 这时 跟踪可以帮助你了解 单个请求在系统中所采用的路径
Swift 生态系统为这三大要素 都提供了 API 软件包 允许代码发送可观测性事件
让我们来看看如何利用 listEvents 方法
首先 当我们开始处理 新的 listEvents 请求时 可以使用 swift-log 发送日志 swift-log 支持结构化日志记录 可在日志信息中添加元数据 从而在对问题进行故障诊断时 提供更多的上下文
接下来 可以从 swift-metrics 添加一个计数器 计数器在每次请求时会递增 以跟踪我们的服务 处理了多少个请求
最后 可以添加 swift-distributed-tracing 来围绕数据库查询创建一个跨度 这有助于通过我们的系统对请求 进行端到端故障诊断 要进一步了解 Swift 中分布式跟踪的工作原理 请观看去年的讲座 “超越结构化并发的基础” 我们刚刚在 listEvents 方法中使用了 日志记录、指标和跟踪工具 我们使用的 API 与可观测性后端无关 服务创作者可自行选择 向何处发送数据 Swift on Server 生态系统 包含很多不同的后端 用于日志记录、指标 和分布式跟踪 选择后端 可通过调用三个库的引导方法 来完成 引导只能在可执行文件中进行 并应尽早完成 以确保 不会丢失可观测性事件 此外 建议首先引导 LoggingSystem 然后引导 MetricsSystem 最后引导 InstrumentationSystem 这是因为指标系统和仪表化系统 可能希望发送有关自身状态的日志
只需几行代码 我们就能向终端发送日志 向 Prometheus 发送指标 向 Open Telemetry 发送跟踪 让我们在 createEvent 方法中 添加日志记录 来了解当尝试在 Gus 名下添加 另一项活动时 究竟哪里出现问题
首先 必须在软件包和 EventService 目标中 添加 swift-log 作为依赖项
然后 就可以在服务中导入 日志记录模块
接下来 我们将捕获查询方法 抛出的错误
如果在执行查询时出现问题 查询方法就会抛出 PSQLError
让我们创建一个日志记录器 这样就能发送 包含 Postgres 服务器发送的 错误信息的日志事件
接下来 提取错误信息并发送日志 PSQLError 在 serverInfo 属性中包含 出错点的详细信息
最后 我们将返回 badRequest 响应 指出在将活动添加到 数据库时出现问题
让我们重启服务 看看能否获得关于错误的 更多详细信息
默认情况下 swift-log 会将日志发送到终端 这非常适合调试我们的应用程序 我们运行相同的 curl 命令 再次创建活动
这次我们没有再遇到同样的错误 因为我们返回了 badRequest 状态代码
因此 让我们查看一下服务日志 看看哪里出了问题
在底部的终端可以看到日志信息 错误信息元数据字段告诉我们 错误是由于重复键违规导致的 我们的数据库表只允许姓名、 日期和参与者组合成一个条目
添加日志帮助我们对具体问题 进行了故障诊断 我稍后会让我的同事修复这个错误
以上只是对 Swift on Server 生态系统中 一些库的简单介绍 你可以用这些库来构建服务 还有很多库适用于各种用例 如联网、 数据库驱动程序、 可观测性、 消息流 等等 如果你希望查找更多库 请访问 swift.org 软件包部分 并探索服务器类别 你还可以使用 swift 软件包索引 查找更多服务器库
另一个查找软件包的重要资源是 Swift Server 工作组的 孵化列表 工作组运行软件包孵化流程 来创建强大而稳定的生态系统
孵化流程中的软件包 从 Sandbox 到 Incubating 再到 Graduated 实现成熟度级别转换
每个级别都有与软件包的 生产就绪度和使用情况 相一致的不同要求 你可以在 swift.org 上找到 已孵化软件包的列表
希望本讲座能激发你对 Swift on Server 生态系统的热情 我们讨论了 Swift 为何 是服务器应用程序的 绝佳语言 以及它如何为 Apple 云服务的 许多关键功能 提供助力与支持 我们还探讨了一些软件包 以及 Swift Server 工作组 如何帮助生态系统健康成长 感谢大家观看! 啤酒节上见!
-
-
3:23 - EventService Package.swift
// swift-tools-version:5.9 import PackageDescription let package = Package( name: "EventService", platforms: [.macOS(.v14)], dependencies: [ .package( url: "https://github.com/apple/swift-openapi-generator", from: "1.2.1" ), .package( url: "https://github.com/apple/swift-openapi-runtime", from: "1.4.0" ), .package( url: "https://github.com/vapor/vapor", from: "4.99.2" ), .package( url: "https://github.com/swift-server/swift-openapi-vapor", from: "1.0.1" ), ], targets: [ .target( name: "EventAPI", dependencies: [ .product( name: "OpenAPIRuntime", package: "swift-openapi-runtime" ), ], plugins: [ .plugin( name: "OpenAPIGenerator", package: "swift-openapi-generator" ) ] ), .executableTarget( name: "EventService", dependencies: [ "EventAPI", .product( name: "OpenAPIRuntime", package: "swift-openapi-runtime" ), .product( name: "OpenAPIVapor", package: "swift-openapi-vapor" ), .product( name: "Vapor", package: "vapor" ), ] ), ] )
-
4:05 - EventService openapi.yaml
openapi: "3.1.0" info: title: "EventService" version: "1.0.0" servers: - url: "https://localhost:8080/api" description: "Example service deployment." paths: /events: get: operationId: "listEvents" responses: "200": description: "A success response with all events." content: application/json: schema: type: "array" items: $ref: "#/components/schemas/Event" post: operationId: "createEvent" requestBody: description: "The event to create." required: true content: application/json: schema: $ref: '#/components/schemas/Event' responses: '201': description: "A success indicating the event was created." '400': description: "A failure indicating the event wasn't created." components: schemas: Event: type: "object" description: "An event." properties: name: type: "string" description: "The event's name." date: type: "string" format: "date" description: "The day of the event." attendee: type: "string" description: "The name of the person attending the event." required: - "name" - "date" - "attendee"
-
4:35 - EventService initial implementation
import OpenAPIRuntime import OpenAPIVapor import Vapor import EventAPI @main struct Service { static func main() async throws { let application = try await Vapor.Application.make() let transport = VaporTransport(routesBuilder: application) let service = Service() try service.registerHandlers( on: transport, serverURL: URL(string: "/api")! ) try await application.execute() } } extension Service: APIProtocol { func listEvents( _ input: Operations.listEvents.Input ) async throws -> Operations.listEvents.Output { let events: [Components.Schemas.Event] = [ .init(name: "Server-Side Swift Conference", date: "26.09.2024", attendee: "Gus"), .init(name: "Oktoberfest", date: "21.09.2024", attendee: "Werner"), ] return .ok(.init(body: .json(events))) } func createEvent( _ input: Operations.createEvent.Input ) async throws -> Operations.createEvent.Output { return .undocumented(statusCode: 501, .init()) } }
-
6:56 - EventService Package.swift
// swift-tools-version:5.9 import PackageDescription let package = Package( name: "EventService", platforms: [.macOS(.v14)], dependencies: [ .package( url: "https://github.com/apple/swift-openapi-generator", from: "1.2.1" ), .package( url: "https://github.com/apple/swift-openapi-runtime", from: "1.4.0" ), .package( url: "https://github.com/vapor/vapor", from: "4.99.2" ), .package( url: "https://github.com/swift-server/swift-openapi-vapor", from: "1.0.1" ), .package( url: "https://github.com/vapor/postgres-nio", from: "1.19.1" ), ], targets: [ .target( name: "EventAPI", dependencies: [ .product( name: "OpenAPIRuntime", package: "swift-openapi-runtime" ), ], plugins: [ .plugin( name: "OpenAPIGenerator", package: "swift-openapi-generator" ) ] ), .executableTarget( name: "EventService", dependencies: [ "EventAPI", .product( name: "OpenAPIRuntime", package: "swift-openapi-runtime" ), .product( name: "OpenAPIVapor", package: "swift-openapi-vapor" ), .product( name: "Vapor", package: "vapor" ), .product( name: "PostgresNIO", package: "postgres-nio" ), ] ), ] )
-
7:08 - Implementing the listEvents method
import OpenAPIRuntime import OpenAPIVapor import Vapor import EventAPI import PostgresNIO @main struct Service { let postgresClient: PostgresClient static func main() async throws { let application = try await Vapor.Application.make() let transport = VaporTransport(routesBuilder: application) let postgresClient = PostgresClient( configuration: .init( host: "localhost", username: "postgres", password: nil, database: nil, tls: .disable ) ) let service = Service(postgresClient: postgresClient) try service.registerHandlers( on: transport, serverURL: URL(string: "/api")! ) try await withThrowingDiscardingTaskGroup { group in group.addTask { await postgresClient.run() } group.addTask { try await application.execute() } } } } extension Service: APIProtocol { func listEvents( _ input: Operations.listEvents.Input ) async throws -> Operations.listEvents.Output { let rows = try await self.postgresClient.query("SELECT name, date, attendee FROM events") var events = [Components.Schemas.Event]() for try await (name, date, attendee) in rows.decode((String, String, String).self) { events.append(.init(name: name, date: date, attendee: attendee)) } return .ok(.init(body: .json(events))) } func createEvent( _ input: Operations.createEvent.Input ) async throws -> Operations.createEvent.Output { return .undocumented(statusCode: 501, .init()) } }
-
9:02 - Implementing the createEvent method
func createEvent( _ input: Operations.createEvent.Input ) async throws -> Operations.createEvent.Output { switch input.body { case .json(let event): try await self.postgresClient.query( """ INSERT INTO events (name, date, attendee) VALUES (\(event.name), \(event.date), \(event.attendee)) """ ) return .created(.init()) } }
-
11:34 - Instrumenting the listEvents method
func listEvents( _ input: Operations.listEvents.Input ) async throws -> Operations.listEvents.Output { let logger = Logger(label: "ListEvents") logger.info("Handling request", metadata: ["operation": "\(Operations.listEvents.id)"]) Counter(label: "list.events.counter").increment() return try await withSpan("database query") { span in let rows = try await postgresClient.query("SELECT name, date, attendee FROM events") return try await .ok(.init(body: .json(decodeEvents(rows)))) } }
-
13:14 - EventService Package.swift
// swift-tools-version:5.9 import PackageDescription let package = Package( name: "EventService", platforms: [.macOS(.v14)], dependencies: [ .package( url: "https://github.com/apple/swift-openapi-generator", from: "1.2.1" ), .package( url: "https://github.com/apple/swift-openapi-runtime", from: "1.4.0" ), .package( url: "https://github.com/vapor/vapor", from: "4.99.2" ), .package( url: "https://github.com/swift-server/swift-openapi-vapor", from: "1.0.1" ), .package( url: "https://github.com/vapor/postgres-nio", from: "1.19.1" ), .package( url: "https://github.com/apple/swift-log", from: "1.5.4" ), ], targets: [ .target( name: "EventAPI", dependencies: [ .product( name: "OpenAPIRuntime", package: "swift-openapi-runtime" ), ], plugins: [ .plugin( name: "OpenAPIGenerator", package: "swift-openapi-generator" ) ] ), .executableTarget( name: "EventService", dependencies: [ "EventAPI", .product( name: "OpenAPIRuntime", package: "swift-openapi-runtime" ), .product( name: "OpenAPIVapor", package: "swift-openapi-vapor" ), .product( name: "Vapor", package: "vapor" ), .product( name: "PostgresNIO", package: "postgres-nio" ), .product( name: "Logging", package: "swift-log" ), ] ), ] )
-
13:38 - Adding logging to the createEvent method
func createEvent( _ input: Operations.createEvent.Input ) async throws -> Operations.createEvent.Output { switch input.body { case .json(let event): do { try await self.postgresClient.query( """ INSERT INTO events (name, date, attendee) VALUES (\(event.name), \(event.date), \(event.attendee)) """ ) return .created(.init()) } catch let error as PSQLError { let logger = Logger(label: "CreateEvent") if let message = error.serverInfo?[.message] { logger.info( "Failed to create event", metadata: ["error.message": "\(message)"] ) } return .badRequest(.init()) } } }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。