微服务架构设计实战:从单体到分布式
大家好,我是小张,一名985毕业的全栈工程师,平时在掘金写一些入门教程。今天想和大家分享一个我当初学后端开发时特别头疼的话题——微服务架构。
为什么写这篇教程?因为我在带实习生时发现,很多人一听到“微服务”就立刻想到 Kubernetes、Docker、Service Mesh 这些高大上的词,结果连最基础的“为什么要拆分服务”都没搞明白。其实,微服务的核心不是技术堆砌,而是解决复杂业务下的可维护性问题。
今天,我们就用 Go 语言,从零开始,把一个简单的单体应用一步步演进成微服务架构。过程中还会结合一个轻量级爬虫项目,让你真正理解“拆”与“不拆”的区别。
一、什么是微服务?它能解决什么问题?
简单来说:
- 单体应用(Monolith):所有功能(用户、订单、商品、爬虫等)都写在一个项目里,部署成一个进程。
- 微服务(Microservices):把不同功能拆成多个独立的服务,每个服务单独开发、部署、扩展。
🌰 举个例子:
你原来开一家“全能小店”,一个人既收银、又做饭、还打扫卫生。生意一忙,你就手忙脚乱。
微服务就像你请了三个员工:收银员、厨师、保洁员,各干各的,互不影响。
适用场景:
- 团队规模变大,多人协作困难
- 某个模块(比如爬虫)需要高频更新或独立扩展
- 系统越来越复杂,改一处怕崩全局
二、环境准备:搭建 Go 开发环境
我们使用 Go 1.20+(语法简洁、性能强、适合微服务)。安装步骤如下:
1. 安装 Go
# macOS (使用 Homebrew)
brew install go
# Windows / Linux
# 访问 https://go.dev/dl/ 下载安装包
验证安装:
go version
# 输出类似:go version go1.22.0 darwin/arm64
2. 创建项目目录
mkdir microservice-demo && cd microservice-demo
go mod init microservice-demo
3. 安装必要依赖
我们会用到:
gin:轻量级 Web 框架goquery:HTML 解析库(用于爬虫)
go get -u github.com/gin-gonic/gin
go get -u github.com/PuerkitoBio/goquery
三、核心概念:单体 vs 微服务
| 对比维度 | 单体应用 | 微服务 |
|---|---|---|
| 部署方式 | 一个可执行文件 | 多个独立服务 |
| 技术栈 | 统一语言/框架 | 各服务可选不同技术 |
| 扩展性 | 整体扩容 | 按需扩容(如只扩爬虫服务) |
| 开发效率 | 初期快 | 需要协调接口 |
| 调试难度 | 简单 | 需要链路追踪 |
| 通信方式 | 函数调用 | HTTP/gRPC/RPC |
💡 我当初学的时候以为“微服务一定更好”,后来踩坑才发现:小项目用微服务反而增加复杂度!建议:日活 < 1万 的项目先别急着拆。
四、实战项目:从单体爬虫到微服务
我们做一个“新闻聚合系统”:
- 功能1:爬取某个新闻网站标题
- 功能2:提供 API 接口返回最新标题
第一步:单体实现(all-in-one)
创建 main.go:
// main.go
package main
import (
"github.com/gin-gonic/gin"
"github.com/PuerkitoBio/goquery"
"log"
"net/http"
)
// 爬取 Hacker News 前10个标题
func scrapeTitles() []string {
resp, err := http.Get("https://news.ycombinator.com")
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
doc, err := goquery.NewDocumentFromReader(resp.Body)
if err != nil {
log.Fatal(err)
}
var titles []string
doc.Find(".titleline > a").Each(func(i int, s *goquery.Selection) {
if i < 10 {
titles = append(titles, s.Text())
}
})
return titles
}
func main() {
r := gin.Default()
r.GET("/news", func(c *gin.Context) {
titles := scrapeTitles()
c.JSON(200, gin.H{"news": titles})
})
r.Run(":8080")
}
运行:
go run main.go
访问 http://localhost:8080/news,你会看到 JSON 格式的新闻标题。
✅ 优点:代码简单,启动快
❌ 缺点:每次请求都爬一次!而且爬虫逻辑和 API 耦合在一起
第二步:拆分成两个微服务
我们将系统拆为:
- crawler-service:专门负责爬虫,定时抓取并存储
- api-service:提供 HTTP 接口,从存储中读数据
1. 创建 crawler-service
新建目录 crawler/,创建 main.go:
// crawler/main.go
package main
import (
"encoding/json"
"io/ioutil"
"log"
"net/http"
"time"
"github.com/PuerkitoBio/goquery"
)
type NewsItem struct {
Title string `json:"title"`
}
func scrapeAndSave() {
resp, err := http.Get("https://news.ycombinator.com")
if err != nil {
log.Println("Scrape failed:", err)
return
}
defer resp.Body.Close()
doc, err := goquery.NewDocumentFromReader(resp.Body)
if err != nil {
log.Println("Parse failed:", err)
return
}
var news []NewsItem
doc.Find(".titleline > a").Each(func(i int, s *goquery.Selection) {
if i < 10 {
news = append(news, NewsItem{Title: s.Text()})
}
})
data, _ := json.Marshal(news)
ioutil.WriteFile("latest_news.json", data, 0644)
log.Println("News saved!")
}
func main() {
// 启动定时任务:每5分钟爬一次
ticker := time.NewTicker(5 * time.Minute)
scrapeAndSave() // 立即执行一次
for range ticker.C {
scrapeAndSave()
}
}
2. 创建 api-service
新建目录 api/,创建 main.go:
// api/main.go
package main
import (
"encoding/json"
"github.com/gin-gonic/gin"
"io/ioutil"
)
type NewsResponse struct {
News []NewsItem `json:"news"`
}
type NewsItem struct {
Title string `json:"title"`
}
func main() {
r := gin.Default()
r.GET("/news", func(c *gin.Context) {
data, err := ioutil.ReadFile("../crawler/latest_news.json")
if err != nil {
c.JSON(500, gin.H{"error": "Data not ready"})
return
}
var news []NewsItem
json.Unmarshal(data, &news)
c.JSON(200, NewsResponse{News: news})
})
r.Run(":8081")
}
3. 启动两个服务
终端1:
cd crawler && go run main.go
终端2:
cd api && go run main.go
现在访问 http://localhost:8081/news,数据来自本地文件,不再实时爬取,性能更高!
🔁 注意:这里用文件共享是简化方案。生产环境应使用 Redis、MySQL 或消息队列。
五、新手常见问题解答
Q1:微服务之间怎么通信?
- 同步通信:HTTP(REST)、gRPC(高性能)
- 异步通信:Kafka、RabbitMQ(解耦、削峰)
我们上面用“文件”只是教学演示,实际项目建议用 Redis:
// crawler 写入 redisClient.Set(ctx, "latest_news", jsonData, 0) // api 读取 val, _ := redisClient.Get(ctx, "latest_news").Result()
Q2:Go 适合做微服务吗?
非常适合!原因:
- 编译成单文件,部署简单
- 并发模型(goroutine)天然支持高并发
- 生态丰富(gin、echo、go-kit)
Q3:微服务是不是越多越好?
绝对不是!
服务拆得太细会导致:
- 网络延迟累积
- 事务一致性难保证(需 Saga 模式)
- 运维成本飙升
建议初期拆 2~3 个核心服务即可。
六、学习建议与避坑指南
下一步学什么?
- 服务发现:用 Consul 或 etcd 让服务自动找到彼此
- API 网关:用 KrakenD 或自研网关统一入口
- 配置中心:避免 hardcode 配置
- 链路追踪:Jaeger / Zipkin 查看请求路径
避坑指南(血泪经验):
- ❌ 不要为了“微服务”而微服务
- ✅ 先做好单体,再识别可拆模块
- ✅ 接口契约先行(用 Swagger 定义 API)
- ✅ 日志 + 监控 必须跟上(Prometheus + Grafana)
结语
今天我们用不到 100 行 Go 代码,完成了从单体到微服务的初步演进。虽然只是一个玩具项目,但背后的思路是真实的:按业务边界拆分,降低耦合,提升可维护性。
记住:架构不是一蹴而就的,而是随着业务增长逐步演化的。我当初也是从“一个 main.go 打天下”开始的,慢慢才理解分布式系统的取舍。
希望这篇 技术分享 能帮你少走弯路。如果你对 Go 微服务、爬虫或其他后端技术感兴趣,欢迎关注我的掘金主页,我会持续更新实战教程!
📌 最后提醒:代码已上传 GitHub(模拟地址:
github.com/yourname/microservice-demo),欢迎 Star & 提 Issue!
字数统计:2996 字

评论 0