跳转至

服务端程序运行模型

学习目标

学完本章后,学习者应该能够:

  1. 理解一个后端服务从启动到处理请求的基本过程。
  2. 知道端口、进程、配置、日志、健康检查的作用。
  3. 理解同步处理和异步处理的区别。
  4. 初步理解优雅退出为什么重要。

一个服务如何运行起来

一个典型 Go HTTP 服务运行过程可以简化为:

flowchart TD
    Start["启动进程"] --> LoadConfig["加载配置"]
    LoadConfig --> InitDeps["初始化依赖"]
    InitDeps --> RegisterRoutes["注册路由"]
    RegisterRoutes --> Listen["监听端口"]
    Listen --> Accept["接收请求"]
    Accept --> Handle["处理请求"]
    Handle --> Response["返回响应"]

每一步都有可能失败。

例如:

  • 配置文件不存在。
  • 数据库连接失败。
  • Redis 密码错误。
  • 端口被占用。
  • 路由注册错误。
  • 请求处理 panic。

进程

程序运行起来后,在操作系统里表现为一个进程。

进程拥有:

  • 进程 ID。
  • 内存空间。
  • 打开的文件。
  • 网络连接。
  • 环境变量。
  • 当前工作目录。
  • 权限信息。

查看进程是排查问题的第一步。

Get-Process
ps aux

端口监听

后端服务通常会监听一个端口,例如 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 后续会使用 slogzap 做结构化日志。

健康检查

健康检查用于判断服务是否可以接收流量。

常见接口:

  • /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 服务,要求:

  1. 提供 /healthz 接口,返回 ok
  2. 提供 /hello?name=xxx 接口,返回问候语。
  3. 启动时打印监听端口。
  4. 使用命令行工具验证端口监听和 HTTP 返回。

可选加分:

  1. 从环境变量读取端口。
  2. 增加请求日志。
  3. 实现优雅退出。

面试题

1. 一个后端服务启动时通常会做哪些初始化?

参考答案

通常会加载配置、校验必要参数、初始化日志、连接数据库和缓存、初始化消息队列客户端、注册路由和中间件、启动后台任务、暴露健康检查接口,并开始监听端口。

生产服务还应该在启动失败时给出明确错误,而不是静默失败。例如数据库地址缺失、端口被占用、配置格式错误,都应该在启动阶段尽早暴露。

2. 127.0.0.1:80800.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 变为失败、等待已有请求完成、停止后台任务、提交或回滚事务、刷新日志并释放资源。这样可以减少发布和扩缩容期间的用户感知错误。