第 71 课 - 微服务架构基础
学习目标
完成本课后,你将能够:
- 理解单体应用(Monolithic)与微服务架构的核心区别与优劣。
- 掌握设计和划分微服务边界的基本原则(单一职责)。
- 使用 Go 语言实现一个简单的、包含两个服务的微服务原型。
- 了解服务间通信(HTTP/JSON)在 Go 中的基础实践。
- 认识到在微服务架构中,如何让并发机制(如 Goroutine)服务于请求处理。
核心概念
1. 从单体到微服务:一次“拆解”
想象一下一个庞大的电商单体应用:用户管理、商品目录、订单处理、支付等功能全部打包成一个单独的部署单元(如一个巨大的二进制文件或WAR包)。
- 问题:一个模块的 bug 可能导致整个系统崩溃。技术栈升级像换发动机——需要停掉整辆车。团队协作时,所有人都在修改同一个代码库,冲突不断。
- 微服务的思路:将这个“庞然大物”拆解成多个小而专的“服务”。
- 用户服务:只负责用户注册、登录、信息查询。
- 商品服务:只负责商品的增删改查。
- 订单服务:只负责订单的创建、状态管理。 每个服务独立开发、独立部署、独立扩展(例如,商品查询压力大,就多部署几个商品服务实例)。
2. 微服务的核心特征
- 单一职责:每个服务只关注并做好一件事。这是服务划分的“黄金法则”。
- 服务自治:每个服务拥有自己的进程、数据库(数据库按服务隔离,而非共享)。这避免了数据紧耦合。
- 通过 API 通信:服务之间通常通过轻量级的 HTTP API(如 RESTful)或 gRPC(下下节课学习)进行通信,而不是直接的函数调用。
- 容错设计:一个服务的失败不应导致整个系统雪崩。这需要引入断路器、重试等模式(更高级主题)。
3. 为什么 Go 适合微服务?
- 编译为单个二进制:部署时只需要复制一个文件,非常方便容器化(Docker)。
- 天生高性能:对于高并发的网络请求处理非常高效。
- 并发原语:Goroutine 和 Channel 是构建高性能、非阻塞 API 网关或服务内部并发处理的利器。
- 丰富的标准库:
net/http包足以构建生产级的 HTTP 服务器。
代码示例
下面,我们用 Go 创建一个包含 User Service 和 Order Service 的简化版原型。Order Service 需要从 User Service 获取用户信息,来校验用户是否存在。
// main.go - 我们将在单个文件中模拟两个服务,以方便演示。实际中它们是两个独立程序。
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"sync"
)
// ===================== 共享数据模型 =====================
type User struct {
ID string `json:"id"`
Name string `json:"name"`
}
type Order struct {
ID string `json:"id"`
UserID string `json:"user_id"`
Product string `json:"product"`
}
// ===================== 用户服务 (独立进程/端口) =====================
var userStore = map[string]User{ // 模拟数据库
"u1": {ID: "u1", Name: "Alice"},
"u2": {ID: "u2", Name: "Bob"},
}
// 用户服务的处理器:根据ID获取用户
func userServiceHandler(w http.ResponseWriter, r *http.Request) {
// 从URL路径中提取用户ID,例如 /users/u1 -> u1
userID := r.URL.Path[len("/users/"):]
user, exists := userStore[userID]
if !exists {
http.Error(w, "User not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}
// ===================== 订单服务 (独立进程/端口) =====================
var orderStore = map[string]Order{ // 模拟数据库
"o1": {ID: "o1", UserID: "u1", Product: "Go并发编程实战"},
}
// 订单服务的处理器:根据ID获取订单,需要校验用户是否存在
func orderServiceHandler(w http.ResponseWriter, r *http.Request) {
orderID := r.URL.Path[len("/orders/"):]
order, exists := orderStore[orderID]
if !exists {
http.Error(w, "Order not found", http.StatusNotFound)
return
}
// 关键点:订单服务通过HTTP API调用用户服务,获取用户信息进行校验
// 在真实微服务中,这里应调用另一个服务的地址(如 http://user-service:8081/users/u1)
userServiceURL := fmt.Sprintf("http://localhost:8081/users/%s", order.UserID)
resp, err := http.Get(userServiceURL)
if err != nil {
// 简单的错误处理,真实场景需要更复杂的重试/熔断逻辑
http.Error(w, "Failed to call user service", http.StatusInternalServerError)
log.Printf("Error calling user service: %v", err)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
http.Error(w, "User validation failed", http.StatusBadRequest)
return
}
// 解码用户信息
var user User
json.NewDecoder(resp.Body).Decode(&user)
// 组合信息返回
response := struct {
Order Order `json:"order"`
User User `json:"user"`
}{
Order: order,
User: user,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
func main() {
var wg sync.WaitGroup
// 启动用户服务 (端口 8081)
wg.Add(1)
go func() {
defer wg.Done()
http.HandleFunc("/users/", userServiceHandler)
fmt.Println("User Service listening on :8081")
log.Fatal(http.ListenAndServe(":8081", nil))
}()
// 启动订单服务 (端口 8082)
wg.Add(1)
go func() {
defer wg.Done()
http.HandleFunc("/orders/", orderServiceHandler)
fmt.Println("Order Service listening on :8082")
log.Fatal(http.ListenAndServe(":8082", nil))
}()
// 等待两个服务(理论上会永久运行,但程序会在这里阻塞)
wg.Wait()
}
如何运行与测试:
- 运行
go run main.go,你会看到两个服务分别在 8081 和 8082 端口启动。 - 使用
curl或浏览器测试:- 直接访问用户服务:
curl http://localhost:8081/users/u1 - 通过订单服务间接获取用户信息:
curl http://localhost:8082/orders/o1
- 直接访问用户服务:
实践练习
-
基础:服务划分练习 考虑一个博客系统,包含“文章发布”、“评论管理”、“用户认证”三个功能。请按照单一职责原则,为这三个功能分别设计一个微服务,并列出每个服务的核心API端点(至少2个)和所需的数据模型。
-
进阶:实现服务间调用 基于上面的代码示例,为“商品服务”和“订单服务”增加功能。商品服务(端口8083)管理商品信息。当订单服务创建订单时,除了校验用户,还需要向商品服务查询商品信息(例如商品名称、价格),并将订单和商品信息一起返回。
- 提示:在订单服务中,你需要再发起一个到商品服务的 HTTP GET 请求。
-
挑战:引入并发与错误处理 修改订单服务的
/orders/处理函数。如果订单中涉及多个商品(假设订单结构变为Products []string),你需要并发地调用商品服务获取每个商品的信息。使用sync.WaitGroup或errgroup(golang.org/x/sync/errgroup)来管理并发,并聚合结果。同时,实现一个简单的超时控制(例如使用context.WithTimeout)。
常见错误
- 过度拆分:将微服务拆分得过细(如一个“获取用户名”的操作就成一个服务),导致服务间调用频繁、网络开销巨大,系统复杂度激增。应根据业务边界和团队结构合理划分。
- 忽视服务发现:在示例中,服务地址是硬编码的(
localhost:8081)。在生产环境中,服务实例的IP和端口是动态的,必须使用服务发现机制(如Consul, etcd, 或 Kubernetes Service),否则服务无法找到彼此。 - 共享数据库:虽然示例中使用了模拟的map,但绝不应该让两个微服务直接读写同一个物理数据库。这违背了自治原则,会导致数据耦合和迁移困难。
- 滥用并发:在单个服务内部处理请求时,使用Goroutine并发处理多个任务(如并行查询)是好的。但盲目地用Goroutine调用其他微服务而不加控制(如限制并发数、设置超时),可能拖垮下游服务。
小结
- 微服务是一种将单体应用拆解为一组小型、自治、围绕业务能力构建的服务的架构风格。
- 单一职责和服务自治是划分微服务的核心指导思想。
- 服务间通常通过轻量级通信协议(如 HTTP/JSON)进行交互。
- Go 语言凭借其编译特性、高性能和并发原语,是实现微服务的绝佳选择。
- 本课通过一个简单的Go代码示例,展示了如何定义服务、暴露API以及服务间进行最基本的HTTP调用。这为学习更高级的服务治理(服务发现、负载均衡、熔断)打下了基础。
练习编辑器
go
Loading...