译者 | 刘汪洋
审校 | 重楼
API 在我们的数字世界中发挥着关键的作用,使各种不同的应用能够相互通信。然而,这些 API 的可靠性是保证依赖它们的应用程序功能正常、性能稳定的关键因素。本文,我们将探讨提高 API 可靠性的五种主要策略。
1.全面测试
要确保 API 的可靠性,第一步是进行全面的测试。需要进行的测试包括:功能测试以验证 API 的正确运行,集成测试以确保 API 能与其他系统正常协同,以及负载测试以理解 API 在大规模使用下的表现。
自动化测试能在开发周期的早期发现问题,回归测试能保证新的修改不会对现有功能造成破坏。使用虚拟化或模拟技术可以模拟 API 依赖,进行更深度的测试。此外,为了确保 API 的提供者和消费者都能满足约定的接口,契约测试非常重要。
下面我们将使用 Go 的内置 testing 包,通过一个简单的例子对一个假设的 API 端点(用于访问 API 的 URI)进行测试。
假设我们有一个端点 GET /users/{id} ,用于返回用户的详细信息。
下面是我们可能编写的测试代码:
package main
import (
"net/http"
"net/http/httptest"
"testing"
)
// 这是一个简化的实际处理器函数示例
func UserHandler(w http.ResponseWriter, r *http.Request) {
// ... 处理器逻辑
}
func TestUserHandler(t *testing.T) {
req, err := http.NewRequest("GET", "/users/1", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
handler := http.HandlerFunc(UserHandler)
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v",
status, http.StatusOK)
}
// 你还可以检查响应体是否符合预期的输出
expected := `{"id": "1", "name": "John Doe"}`
if rr.Body.String() != expected {
t.Errorf("handler returned unexpected body: got %v want %v",
rr.Body.String(), expected)
}
}
这个测试创建了一个新的 HTTP 请求,模拟了对我们的 /users/{id} 端点的调用,然后把请求传递给了处理器函数。测试会检查响应状态是否为 200 OK(即我们期望的成功请求应返回的结果)以及响应体是否与预期的输出一致。
这个例子只是一个简单的示例,在实际应用中,你可能会面临更复杂的场景,包括测试各种边界条件、错误路径等。此外,net/http/httptest 包提供了许多用于测试 HTTP 客户端和服务器的工具。
总之,你可以结合单元测试、性能测试和持续的集成测试,为你的 API 构建一个全面的测试套件。
单元测试的目的是确保你的 API 中每个组件的正确性。它通过验证每个部分的功能和隔离它们,使得你可以在早期发现并纠正问题。单元测试通常通过模拟依赖项并独立测试函数来完成。在 Go 语言中,可以利用诸如 testify 等包来达成这一目标。
性能测试则是为了在高流量的情况下对 API 进行压力测试。这种测试有助于你确定系统在高负载情况下的表现,识别瓶颈,并确保 API 能处理真实世界的使用情况。性能测试可以使用 JMeter 或Gatling 等工具进行。
最后,持续集成测试则通过模拟用户或客户端对 API 进行一系列连续操作,来测试系统的工作流程。这类测试能够提供对端到端工作流程、潜在的障碍或延迟,以及整体用户体验的深入理解。这个过程可以自动化并集成到你的 CI/CD 流程中,使得你可以持续监控并及时反馈任何代码更改的影响。
通过实施包括功能测试、单元测试、性能测试和持续合成测试在内的全面的测试策略,你可以确保你的 API 不仅稳定且高性能,还能为使用者提供无缝的体验。而在问题出现时,这种多元化的测试方法可以帮助你快速定位并解决问题的根源。
2.版本控制
在维护软件系统的稳定性方面,API 版本管理扮演了核心角色。随着时间推移,API 可能会随着需求变化和优化,如果没有适当的版本管理,可能会对现有的客户端应用造成破坏。这就是 API 版本管理的关键所在。通过维护 API 的各个版本,你可以在引入新功能和优化的同时,确保不影响使用旧版本 API 的应用。
这种策略提升了系统的稳定性,因为即使 API 经过改动和优化,客户端应用依然可以稳定运行。它让开发者能够部署 API 更新,而不需要担心这些变化会对正在运行的应用造成破坏,保障了系统的稳定性和正常运行。
保持向后兼容性是实现 API 稳定性的关键一环,也就是说,新系统应能与旧版 API 兼容。即使新的版本发布,使用旧 API 版本的应用依然可以正常运行。这避免破坏用户体验,并给了开发者足够的时间,让他们可以按照自己的节奏更新应用以适应新的 API ,而不是为了防止应用出错而被迫升级。这样做,有助于创建一个整体上更稳定、更强大和更具有弹性的系统。
示例
在 Go 语言中,我们可以使用多种方式来进行 API 的版本管理。
下面这个例子展示了如何通过在 URL 中嵌入 API 版本实现版本管理,这种方法通常被称为"路径版本控制"。
package main
import (
"fmt"
"net/http"
)
func handleRequest(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/v1/users":
fmt.Fprintf(w, "You've hit the version 1 of the users API!")
case "/v2/users":
fmt.Fprintf(w, "You've hit the version 2 of the users API!")
default:
http.NotFound(w, r)
}
}
func main() {
http.HandleFunc("/", handleRequest)
http.ListenAndServe(":8080", nil)
}
在这个例子中,我们定义了一个处理函数,它根据请求的 URL 路径来匹配响应的代码。当访问 "/v1/users" 路径时,我们认为这是对我们 API 第一版本的请求。同样地,"/v2/users" 则对应我们 API 的第二个版本。通过添加更多的分支,你可以轻松地扩展这种模式以适应更多版本和端点。
此外,你也可以通过自定义头部或媒体类型版本管理(也称为"内容协商")来实现版本管理。
不论你选择何种版本管理策略,都应为 API 的每个版本维护清晰且最新的文档,这是一种良好的实践。
但是,我们也需要谨慎使用版本管理。我们应尽可能地保持向后兼容性,并提供清晰的文档。文档应详细说明每个新版本中的变化,并提供废弃旧版本的合理时间表。
3. 面向失败设计
在理想情况下,API 始终能够正确运行。然而在实际操作中,出现失败的情况并不罕见。在设计 API 的过程中,我们需要考虑其容错能力,这可能涉及到诸如优雅降级(即系统继续运行但是功能有所缩减)和故障转移机制(即出现故障时,系统自动切换到备份系统)等策略。
将明确的错误消息和代码纳入 API,能有助于应用程序更好地理解问题所在以及采取应对策略。我们可以通过重试逻辑、速率限制和断路器,让系统从临时性问题中恢复,避免故障级联。
下图显示了应对各种故障类型的操作方法:
示例:断路器模式
在断路器模式中,Go 语言有一个叫 go-hystrix 的热门库,该库专注于延迟和容错处理。它主要是在服务停止时,通过快速失败阻止故障级联。以下是一个基本示例:
package main
import (
"github.com/afex/hystrix-go/hystrix"
"log"
"net/http"
"errors"
)
func main() {
hystrix.ConfigureCommand("my_command", hystrix.CommandConfig{
Timeout: 1000,
MaxConcurrentRequests: 100,
ErrorPercentThreshold: 25,
})
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
err := hystrix.Do("my_command", func() error {
// 调用其他服务
return nil
}, nil)
if err != nil {
log.Printf("Failed to talk to other services: %v", err)
http.Error(w, "Failed to talk to other services", http.StatusInternalServerError)
}
})
log.Fatal(http.ListenAndServe(":1234", nil))
}
在上述示例中,我们将一个命令封装在 hystrix.Do() 中。如果基于我们设置的参数,传入 Do() 的函数失败或超时,断路器就会被触发,后续的调用将会立即失败,而不再调用该函数。
请注意,这只是一个基本的示例,实际应用场景将涉及更多复杂的用法,需要针对这个库以及其他的弹性实用程序库涉及的各种参数进行细致调整。请务必阅读各种库的文档,深入理解如何在你的代码中有效地使用它们。
4. 监控与分析
实时监控与及时分析对于保证 API 的稳定性至关重要。执行一套全面的 API 监控策略可以包括对运行时间、性能以及错误的检测,这有助于我们在问题扩散影响用户前,及时发现并处理。
同时,深入分析 API 的使用模式可以让我们得到重要的洞察。了解到高峰负载时段、最常使用的端点以及其他使用详情后,您就可以主动地找出可能存在的弱点,并据此进行 API 优化。
选择正确的指标去追踪,对于了解你的 API 的健康状态和性能至关重要。以下是一些需要考虑的关键指标:
1.吞吐量:您的 API 单位时间内处理的请求数量,可以进一步分为端点、HTTP 方法(如 GET、POST、PUT、DELETE 等)或响应状态码。
2.错误率:单位时间内的错误响应数量,通常是指含有 4xx 或 5xx 状态码的响应。同吞吐量一样,这个指标也可以按端点、HTTP 方法或具体的状态码进行细分。
3.延迟:处理一个请求所需的时间,通常以一系列百分位数(如第 50、95 和 99 百分位)来追踪,这可以帮助您了解典型和极端情况下的性能表现。您可能需要针对不同的端点或 HTTP 方法单独追踪此项。
4.流量:发送和接收的数据量,可以按端点、HTTP 方法或响应状态码进行细分。
5.可用性:您的 API 正常运行并能够处理请求的时间占比,可以作为一个整体进行测量,或者针对每一个单独的端点进行测量。
6.饱和度:系统达到最大容量的程度,这可以通过测量 CPU 使用率、内存使用率、磁盘 I/O 或其他可能限制系统处理更多负载的资源来了解。
7.断路器触发:如果您使用断路器模式处理故障,您可能需要追踪断路器被触发的频率,这可以帮助您了解 API 或其依赖项失败的频率。
请记住,根据你的 API 特性和应用需求,选择追踪的具体指标可能会有所不同。关键是要选择那些能为你提供有意义的 API 健康状况和性能洞察力的指标。
以 Prometheus 为例:
Prometheus 是一款内建客户端库的开源系统监控和警告工具包,它支持用各种语言度量您的服务。下面就是一个示例,说明如何使用 Go 客户端库在 HTTP 端点上展示指标。
我们将使用 Prometheus 的 Go 客户端 来展示和创建这些指标。
package main
import (
"net/http"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
var (
httpRequestsTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "http_requests_total",
Help: "Number of HTTP requests",
},
[]string{"path"},
)
httpRequestDuration = prometheus.NewSummaryVec(
prometheus.SummaryOpts{
Name: "http_request_duration_seconds",
Help: "Duration of HTTP requests in seconds",
},
[]string{"path"},
)
)
func init() {
// Register the metrics.
prometheus.MustRegister(httpRequestsTotal)
prometheus.MustRegister(httpRequestDuration)
}
func handler(w http.ResponseWriter, r *http.Request) {
// Increment the counter for the received requests.
httpRequestsTotal.WithLabelValues(r.URL.Path).Inc()
// Measure the time it took to serve the request.
timer := prometheus.NewTimer(httpRequestDuration.WithLabelValues(r.URL.Path))
defer timer.ObserveDuration()
// Handle the request.
w.Write([]byte("Hello, world!"))
}
func main() {
http.HandleFunc("/", handler)
// Expose the registered metrics via HTTP.
http.Handle("/metrics", promhttp.Handler())
http.ListenAndServe(":8080", nil)
}
在这个例子中,我们创建并注册了两个指标:http_requests_total 和 http_request_duration_seconds。前者是一个计数器,每接收到一个请求就增加一次计数,后者是一个汇总指标,用于记录处理每个请求所花费的时间。
然后,我们创建了一个 HTTP 处理器,每处理一个请求,就会增加计数器并测量请求的执行时长。我们利用 promhttp.Handler() 在 /metrics 端点上展示这些指标。
现在,只要你启动了服务器并向其发送请求,就可以通过访问 http://localhost:8080/metrics 或者使用工具如 curl 来查看这些指标。
这只是一个基础的示例,在实际应用中,你可能会希望追踪更多的指标,并基于其他维度(如 HTTP 方法、响应状态码等)对它们进行细分。
5. 利用 API 网关
API 网关是一种强大的工具,能有效提升 API 的健壮性。作为系统的统一入口,API 网关能够处理诸如路由、负载均衡、认证、限流等多项功能。通过将这些问题从 API 本体中抽离,你能更专注于业务逻辑,而非基础设施。
另外,API 网关还可提供额外的弹性特性,如自动故障转移、为提高性能而对响应进行缓存,以及在高负载时对请求进行缓冲或排队。
下面列出了 API 网关能提供的部分功能,帮助你为技术栈选择适合的 API 网关:
以下是一些知名的开源 API 网关:
总的来说,提升 API 的可靠性不是一项一次性任务,而是需要持续投入的工作。这包括严格的测试、精确的版本控制、遵循好的设计原则,智能地使用如 API 网关这样的工具,以及持续的监控和分析。有了这些策略,你就能构建出能经受住时间考验并为你的应用程序提供可靠基础的 API。
译者介绍
刘汪洋,51CTO社区编辑,昵称:明明如月,一个拥有 5 年开发经验的某大厂高级 Java 工程师,拥有多个主流技术博客平台博客专家称号。
原文标题:5 Ways to Improve Your API Reliability,作者:CodeReliant 社区