当我们从客户端开发转向服务端开发,面对的不再是一台确定的机器、一个确定的用户,而是不确定的流量、不确定的状态、不确定的故障。服务端开发的核心命题可以归结为一句话:
如何在分布式环境下,对海量用户的请求进行可靠、高效、可扩展的处理?
许式伟指出,服务端程序的本质差异在于:它不再服务于单一用户,而是服务于海量用户。这一根本差异催生了服务端所有的技术挑战——从流量调度到状态管理,从存储中间件到容错设计。
客户端程序与服务端程序的差异,不是"运行在哪里"这么简单,而是问题域的根本不同:
| 维度 | 客户端程序 | 服务端程序 |
|---|---|---|
| 用户模型 | 单用户,状态确定 | 海量用户,状态不确定 |
| 并发模型 | 主线程驱动,关注响应 | 多协程/多线程,关注吞吐 |
| 故障模型 | 本地崩溃可重启 | 集群局部故障必须容灾 |
| 状态管理 | 本地存储为主 | 分布式状态,强/弱一致性 |
| 扩展方式 | 升级单机硬件 | 水平扩容,弹性伸缩 |
| 生命周期 | 用户启停 | 7x24 常驻 |
服务端架构经历了从单体到微服务的演进,但演进的本质驱动力始终是对规模与复杂度的响应:
关键洞察:架构演进的每一步,都是在用更高的系统复杂度换取更低的业务耦合度。这不是免费的午餐,而是权衡的结果。
许式伟将服务端开发抽象为三层:
接入层解决"请求怎么进来"的问题;逻辑层解决"业务怎么处理"的问题;数据层解决"状态怎么持久"的问题。三层之间的边界清晰,职责分明,但层与层之间的交互模式决定了系统的上限。
服务端架构最核心的设计原则之一是无状态计算与有状态存储的分离:
为什么这个分离如此重要? 因为无状态节点的失败是廉价的(重启或替换即可),而有状态节点的失败是昂贵的(数据可能丢失或不一致)。将两者分离,使得我们可以用不同的策略应对不同类型的故障。
| Trade-off | 一端 | 另一端 | 决策依据 |
|---|---|---|---|
| 一致性 vs 可用性 | 强一致(CP) | 高可用(AP) | 业务语义:金融选CP,社交选AP |
| 性能 vs 正确性 | 异步最终一致 | 同步强一致 | 数据重要性:关键数据同步,日志异步 |
| 简单性 vs 可扩展性 | 单体 | 微服务 | 团队规模与业务复杂度 |
| 延迟 vs 吞吐 | 低延迟响应 | 高吞吐批处理 | 业务场景:在线交互 vs 离线分析 |
| 资源利用率 vs 隔离性 | 共享资源池 | 独立资源 | 故障爆炸半径 vs 成本 |
// 反模式:将用户会话存在本地内存
var sessions = make(map[string]*Session)
func handleRequest(w http.ResponseWriter, r *http.Request) {
session := sessions[sessionID] // 本地内存状态
// 请求路由到其他实例时,session 丢失!
}正确做法:将 session 存入 Redis 等外部存储,计算节点保持无状态。
没有熔断、限流、超时控制的调用链,一个下游慢节点可以拖垮整个系统。这是服务端最常见的雪崩场景。
一个设计良好的服务端系统应具备:
相关章节:35丨流量调度与负载均衡 将深入接入层的流量调度机制;36丨业务状态与存储中间件 将深入数据层的状态管理