服务端程序运行模型¶
学习目标¶
学完本章后,学习者应该能够:
- 理解一个后端服务从启动到处理请求的基本过程。
- 知道端口、进程、配置、日志、健康检查的作用。
- 理解同步处理和异步处理的区别。
- 初步理解优雅退出为什么重要。
一个服务如何运行起来¶
一个典型 Go HTTP 服务运行过程可以简化为:
flowchart TD
Start["启动进程"] --> LoadConfig["加载配置"]
LoadConfig --> InitDeps["初始化依赖"]
InitDeps --> RegisterRoutes["注册路由"]
RegisterRoutes --> Listen["监听端口"]
Listen --> Accept["接收请求"]
Accept --> Handle["处理请求"]
Handle --> Response["返回响应"]
每一步都有可能失败。
例如:
- 配置文件不存在。
- 数据库连接失败。
- Redis 密码错误。
- 端口被占用。
- 路由注册错误。
- 请求处理 panic。
进程¶
程序运行起来后,在操作系统里表现为一个进程。
进程拥有:
- 进程 ID。
- 内存空间。
- 打开的文件。
- 网络连接。
- 环境变量。
- 当前工作目录。
- 权限信息。
查看进程是排查问题的第一步。
端口监听¶
后端服务通常会监听一个端口,例如 8080。
监听地址很重要:
127.0.0.1:8080:只允许本机访问。0.0.0.0:8080:允许外部通过机器 IP 访问。
新手常见问题是服务只监听 127.0.0.1,在容器或服务器外部访问不到。
Go 示例:
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
_, _ = fmt.Fprintln(w, "ok")
})
log.Println("listening on :8080")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}
这里的 :8080 等价于监听所有网卡的 8080 端口。
配置¶
配置决定服务如何连接外部世界。
常见配置:
- 服务端口。
- 数据库地址。
- Redis 地址。
- 日志级别。
- 第三方 API 地址。
- 超时时间。
- 开关配置。
- 密钥和证书路径。
配置来源可能是:
- 配置文件。
- 环境变量。
- 命令行参数。
- 配置中心。
- Kubernetes ConfigMap / Secret。
原则:
- 配置和代码分离。
- 敏感信息不要写进代码仓库。
- 启动时要校验关键配置。
- 打印配置时要脱敏。
日志¶
日志用于回答“发生了什么”。
好的服务日志至少应该包含:
- 时间。
- 日志级别。
- 请求 ID 或 Trace ID。
- 用户或租户信息。
- 接口路径。
- 错误信息。
- 关键业务字段。
- 耗时。
不好的日志通常有两个问题:
- 太少:出了问题什么都看不到。
- 太多:正常请求刷屏,关键错误被淹没。
Go 后续会使用 slog 或 zap 做结构化日志。
健康检查¶
健康检查用于判断服务是否可以接收流量。
常见接口:
/healthz:进程是否活着。/readyz:是否准备好接收流量。/metrics:暴露指标。
简单示例:
func healthz(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ok"))
}
在 Kubernetes 中,健康检查会影响 Pod 是否被重启、是否接收请求。
同步处理与异步处理¶
同步处理:请求进来后,必须立刻完成所有工作再返回。
适合:
- 查询用户信息。
- 校验登录状态。
- 提交小型表单。
异步处理:请求只触发任务,耗时工作交给后台处理。
适合:
- 发送邮件。
- 生成报表。
- 图片转码。
- Webhook 投递。
- 大批量数据同步。
同步处理简单,但容易让用户等待。异步处理能提升响应速度,但会引入任务状态、重试、幂等和一致性问题。
优雅退出¶
服务不能只会启动,也要能正确退出。
不优雅退出可能导致:
- 正在处理的请求被中断。
- 消息消费到一半。
- 数据库事务未完成。
- 日志还没写完。
- 客户端看到连接重置。
Go 服务在容器或 Kubernetes 中通常会收到 SIGTERM 信号。服务应停止接收新请求,等待已有请求处理完成,然后释放资源并退出。
简化示例:
srv := &http.Server{Addr: ":8080", Handler: mux}
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal(err)
}
}()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
_ = srv.Shutdown(ctx)
完整写法会在 Go Web 和云原生章节继续展开。
常见误区¶
- 误区一:服务能启动就算成功。
还要确认依赖是否可用、端口是否监听、健康检查是否正常、日志是否完整。
- 误区二:所有任务都应该同步处理。
耗时任务同步处理会拖慢请求,也容易引起超时。
- 误区三:直接杀进程没问题。
对生产服务来说,退出过程也属于系统设计的一部分。
实战任务¶
写一个最小 Go HTTP 服务,要求:
- 提供
/healthz接口,返回ok。 - 提供
/hello?name=xxx接口,返回问候语。 - 启动时打印监听端口。
- 使用命令行工具验证端口监听和 HTTP 返回。
可选加分:
- 从环境变量读取端口。
- 增加请求日志。
- 实现优雅退出。
面试题¶
1. 一个后端服务启动时通常会做哪些初始化?¶
参考答案
通常会加载配置、校验必要参数、初始化日志、连接数据库和缓存、初始化消息队列客户端、注册路由和中间件、启动后台任务、暴露健康检查接口,并开始监听端口。
生产服务还应该在启动失败时给出明确错误,而不是静默失败。例如数据库地址缺失、端口被占用、配置格式错误,都应该在启动阶段尽早暴露。
2. 127.0.0.1:8080 和 0.0.0.0:8080 有什么区别?¶
参考答案
127.0.0.1:8080 表示只监听本机回环地址,只能从本机访问。0.0.0.0:8080 表示监听所有网卡地址,外部机器可以通过该机器 IP 访问,前提是网络和防火墙允许。
在 Docker 或服务器部署时,如果服务只监听 127.0.0.1,容器外或机器外可能访问不到。这是新手很常见的部署问题。
3. 健康检查接口有什么作用?¶
参考答案
健康检查接口用于让外部系统判断服务是否存活、是否准备好接收流量。常见接口包括 /healthz 和 /readyz。前者偏向进程是否活着,后者偏向依赖是否就绪、是否可以处理请求。
在 Kubernetes 中,健康检查会影响 Pod 是否被重启、是否进入 Service 后端列表。健康检查设计不好,可能导致服务还没准备好就接流量,或者短暂抖动时被频繁重启。
4. 什么场景适合同步处理,什么场景适合异步处理?¶
参考答案
同步处理适合用户需要立刻知道结果、耗时较短、链路简单的场景,比如查询用户信息、校验登录态、提交小表单。异步处理适合耗时较长、可以稍后完成、需要削峰或重试的场景,比如发送邮件、生成报表、图片处理、Webhook 投递。
异步处理能改善用户响应时间,但会引入任务状态、失败重试、幂等、顺序和一致性问题。因此不能简单地认为异步一定更好,要结合业务需求取舍。
5. 为什么 Kubernetes 中的 Go 服务需要优雅退出?¶
参考答案
Kubernetes 滚动更新、缩容或驱逐 Pod 时,会向容器发送终止信号。如果服务不处理退出过程,正在处理的请求可能被中断,消息可能消费一半,数据库事务可能未完成,客户端会看到连接错误。
优雅退出通常包括停止接收新请求、让 readiness 变为失败、等待已有请求完成、停止后台任务、提交或回滚事务、刷新日志并释放资源。这样可以减少发布和扩缩容期间的用户感知错误。