71·云原生进阶

微服务架构基础

microservicearchitecturedesign

第 71 课 - 微服务架构基础

学习目标

完成本课后,你将能够:

  1. 理解单体应用(Monolithic)与微服务架构的核心区别与优劣。
  2. 掌握设计和划分微服务边界的基本原则(单一职责)。
  3. 使用 Go 语言实现一个简单的、包含两个服务的微服务原型。
  4. 了解服务间通信(HTTP/JSON)在 Go 中的基础实践。
  5. 认识到在微服务架构中,如何让并发机制(如 Goroutine)服务于请求处理。

核心概念

1. 从单体到微服务:一次“拆解”

想象一下一个庞大的电商单体应用:用户管理、商品目录、订单处理、支付等功能全部打包成一个单独的部署单元(如一个巨大的二进制文件或WAR包)。

  • 问题:一个模块的 bug 可能导致整个系统崩溃。技术栈升级像换发动机——需要停掉整辆车。团队协作时,所有人都在修改同一个代码库,冲突不断。
  • 微服务的思路:将这个“庞然大物”拆解成多个小而专的“服务”。
    • 用户服务:只负责用户注册、登录、信息查询。
    • 商品服务:只负责商品的增删改查。
    • 订单服务:只负责订单的创建、状态管理。 每个服务独立开发、独立部署、独立扩展(例如,商品查询压力大,就多部署几个商品服务实例)。

2. 微服务的核心特征

  • 单一职责:每个服务只关注并做好一件事。这是服务划分的“黄金法则”。
  • 服务自治:每个服务拥有自己的进程、数据库(数据库按服务隔离,而非共享)。这避免了数据紧耦合。
  • 通过 API 通信:服务之间通常通过轻量级的 HTTP API(如 RESTful)或 gRPC(下下节课学习)进行通信,而不是直接的函数调用。
  • 容错设计:一个服务的失败不应导致整个系统雪崩。这需要引入断路器、重试等模式(更高级主题)。

3. 为什么 Go 适合微服务?

  • 编译为单个二进制:部署时只需要复制一个文件,非常方便容器化(Docker)。
  • 天生高性能:对于高并发的网络请求处理非常高效。
  • 并发原语:Goroutine 和 Channel 是构建高性能、非阻塞 API 网关或服务内部并发处理的利器。
  • 丰富的标准库net/http 包足以构建生产级的 HTTP 服务器。

代码示例

下面,我们用 Go 创建一个包含 User ServiceOrder 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()
}

如何运行与测试:

  1. 运行 go run main.go,你会看到两个服务分别在 8081 和 8082 端口启动。
  2. 使用 curl 或浏览器测试:
    • 直接访问用户服务:curl http://localhost:8081/users/u1
    • 通过订单服务间接获取用户信息:curl http://localhost:8082/orders/o1

实践练习

  1. 基础:服务划分练习 考虑一个博客系统,包含“文章发布”、“评论管理”、“用户认证”三个功能。请按照单一职责原则,为这三个功能分别设计一个微服务,并列出每个服务的核心API端点(至少2个)和所需的数据模型。

  2. 进阶:实现服务间调用 基于上面的代码示例,为“商品服务”和“订单服务”增加功能。商品服务(端口8083)管理商品信息。当订单服务创建订单时,除了校验用户,还需要向商品服务查询商品信息(例如商品名称、价格),并将订单和商品信息一起返回。

    • 提示:在订单服务中,你需要再发起一个到商品服务的 HTTP GET 请求。
  3. 挑战:引入并发与错误处理 修改订单服务的 /orders/ 处理函数。如果订单中涉及多个商品(假设订单结构变为 Products []string),你需要并发地调用商品服务获取每个商品的信息。使用 sync.WaitGrouperrgroupgolang.org/x/sync/errgroup)来管理并发,并聚合结果。同时,实现一个简单的超时控制(例如使用 context.WithTimeout)。

常见错误

  1. 过度拆分:将微服务拆分得过细(如一个“获取用户名”的操作就成一个服务),导致服务间调用频繁、网络开销巨大,系统复杂度激增。应根据业务边界和团队结构合理划分。
  2. 忽视服务发现:在示例中,服务地址是硬编码的(localhost:8081)。在生产环境中,服务实例的IP和端口是动态的,必须使用服务发现机制(如Consul, etcd, 或 Kubernetes Service),否则服务无法找到彼此。
  3. 共享数据库:虽然示例中使用了模拟的map,但绝不应该让两个微服务直接读写同一个物理数据库。这违背了自治原则,会导致数据耦合和迁移困难。
  4. 滥用并发:在单个服务内部处理请求时,使用Goroutine并发处理多个任务(如并行查询)是好的。但盲目地用Goroutine调用其他微服务而不加控制(如限制并发数、设置超时),可能拖垮下游服务。

小结

  • 微服务是一种将单体应用拆解为一组小型、自治、围绕业务能力构建的服务的架构风格。
  • 单一职责服务自治是划分微服务的核心指导思想。
  • 服务间通常通过轻量级通信协议(如 HTTP/JSON)进行交互。
  • Go 语言凭借其编译特性、高性能和并发原语,是实现微服务的绝佳选择。
  • 本课通过一个简单的Go代码示例,展示了如何定义服务、暴露API以及服务间进行最基本的HTTP调用。这为学习更高级的服务治理(服务发现、负载均衡、熔断)打下了基础。

练习编辑器

go
Loading...

继续学习

完成本课后,建议继续学习下一课「gRPC 与 Protocol Buffers」 以巩固所学知识。