微服务是什么?
围绕业务功能构建的,服务关注单一业务,服务间采用轻量级的通信机制,可以全自动独立部署,可以使用不同的编程语言和数据存储技术。
那么微服务可以带来那些好处,又有哪些缺点呢?
1 优点
- 服务拆分后比较小,BUG 少,容易测试和维护,也容易扩展
- 原子服务,一个服务只做一件事情,并且这个属于这个服务的也不应该拆分到其他服务去
- 独立进程,一个服务只有一个独立进程,可以很好的和当前的容器化进行结合,无状态的服务可以很容易的享受到,k8s 上的故障转移,自动重启等好处
- 隔离部署,每个服务之间独立部署,可以避免相互影响,并且和按需进行分配资源,节省成本
- 去中心化服务治理
- 数据去中心化,每个服务独享数据库,缓存等设施,也有个别情况多个服务共享数据库,例如面向用户的管理后台和面向管理员的管理后台
- 治理去中心化
- 技术去中心化,每个服务可以使用适合自己的技术进行实施,但是注意如果技术栈过于发散对于企业或者团队本身也是不利的
2 缺点
- 服务之间的依赖关系复杂,成千上万个服务相互依赖就像一团乱麻一样,剪不断理还乱。
- 常见的解决方案:全链路追踪,例如, opentracing
- 微服务本身是分布式系统,需要使用 RPC 或者 消息进行通信,此外,必须要写代码来处理消息传递中速度过慢或者服务不可用等局部失效问题
- 例子:服务调用流量会容易被放大,如果 服务 A -> B ->C 如果 A 有一个循环调用 B,B 也有一个循环调用 C,那么一个请求到达 C 之后就被放大了 100 倍甚至上千倍。这是扛不住的
- 常见解决方案:粗粒度的进程间通信(batch 接口,批量请求,避免 n+1 问题),隔离,超时保护,负载保护,熔断、限流、降级、重试,负载均衡
- 会有分布式事务问题,因为现在每个微服务之间都会有一个独立的数据库,事务在单体应用中很好处理,但是在跨服务时会变得很麻烦
- 常见解决方案:两阶段提交、TCC 等
- 小米信息部技术团队: 分布式事务,这一篇就够了(opens new window)
- 测试会非常复杂,由于依赖多,无法得知是因为功能异常还是依赖的某个服务发版出现问题
- 常见解决方案:独立测试环境,后面会有一个解决方案
- 服务模块间的依赖,应用的升级有可能会波及多个服务模块的修改。
- 切记,在服务需要变更时我们要特别小心,服务提供者的变更可能引发服务消费者的兼容性破坏,时刻谨记保持服务契约(接口)的兼容性
- 发送时要保守,接收时要开放。按照伯斯塔尔法则的思想来设计和实现服务时,发送的数据要更保守,意味着最小化的传送必要的信息,接收时更开放意味着要最大限度的容忍冗余数据,保证兼容性。
- 对基础建设的要求很高,基础设施需要自动化,日志采集,监控数据采集,告警,CICD,K8s 等
- 常见解决方案:上云
3 微服务常见解决方案
3.1 负载
解决方案:粗粒度的进程间通信(batch 接口,批量请求,避免 n+1 问题),隔离,超时保护,负载保护,熔断、限流、降级、重试,负载均衡
- 粗粒度的进程间通信:指在进行进程间通信时,将多个请求合并为一个批量请求来减少通信开销。相对于细粒度的通信,粗粒度通信可以提高效率和性能。
- 批量请求:将多个请求打包成一个请求进行发送的方式。通过批量请求,可以减少单独发起每个请求的开销,提高系统的吞吐量。
- n+1 问题:在进行大量请求时,如果每次请求都涉及到额外的一次查询或计算,会导致系统性能下降。n+1 问题指的是每个请求都需要额外发起一次查询或计算的情况,加大了系统的负载。
- 隔离:在分布式系统中,隔离指的是通过限制资源的使用和控制不同组件之间的影响,来确保一个组件的问题不会影响到整个系统的稳定性。常见的隔离方式包括进程隔离、容器隔离和虚拟化隔离等。
- 超时保护:在进行网络通信时,为了防止请求过久未响应而造成系统资源浪费,可以设置超时时间。超过设定的超时时间后,系统会主动中断请求或采取相应的处理措施。
- 负载保护:在高负载情况下,为了保护系统的稳定性和可用性,可以采取一些措施来限制进入系统的负载。常见的负载保护方式包括限流、熔断和降级等。
- 熔断:当服务出现故障或异常时,为了避免错误的扩散和影响到其他服务,可以暂时中断对该服务的请求。熔断机制可以快速检测到故障,并将请求快速失败,以减少对故障服务的访问压力。
- 限流:为了控制系统的负载,限制并发请求数量或单位时间内的请求速率。限流机制可以防止系统过载,保持系统的稳定性和性能。
- 降级:在系统遇到高负载或故障情况时,为了保证核心功能的可用性,可以临时关闭或减少一些非核心功能,降低系统的复杂度和负载。
- 重试:在网络通信中,由于网络不稳定或其他原因,请求可能会失败。重试机制可以重新发送失败的请求,以增加请求成功的概率,提高系统的可靠性。
- 负载均衡:将请求均匀地分发到多个服务器上,以实现负载的平衡。负载均衡可以提高系统的吞吐量和可扩展性,并减少单个服务器的压力。
这些解决方案和技术在构建高可用、高性能的分布式系统中非常重要,能够帮助系统处理高并发和大规模负载,并提供稳定和可靠的服务。
3.2 监控
全链路追踪的一些关键概念和步骤:
- 仪表盘:通过仪表盘可以可视化、监控和分析请求的追踪数据。仪表盘可以展示请求的整体性能指标、延迟、调用次数以及不同服务之间的调用关系等信息。
- 追踪数据:追踪数据是指请求在系统中经过的不同节点和服务的记录。每个节点或服务都会生成追踪数据,并将其传递给下一个节点或服务。追踪数据通常包括请求的唯一标识符、开始时间、结束时间、耗时、所属服务等信息。
- 上下文传递:为了确保追踪数据的连贯性,需要在请求的各个节点和服务之间传递上下文信息。上下文信息包括请求的唯一标识符和其他相关的元数据,用于将不同节点或服务产生的追踪数据关联起来。
- 集成支持:为了实现全链路追踪,系统的各个组件和服务需要进行集成和支持。通过对应用程序、框架和基础设施的改动,可以将追踪功能集成到系统中,并收集关键的追踪数据。
全链路追踪可以帮助开发人员和运维人员更好地理解系统的性能特征和请求流程,从而优化系统的性能、提高用户体验,并进行故障排查和问题定位。OpenTracing 是一个通用的标准,广泛应用于各种编程语言和框架中,方便开发者进行全链路追踪的实践。
3.3 事务
分布式事务解决方案
两阶段提交(Two-Phase Commit,2PC)和 TCC(Try-Confirm-Cancel,尝试-确认-取消)都是常见的分布式事务解决方案。
- 两阶段提交:两阶段提交是一种保证分布式事务一致性的协议。它涉及到一个协调者和多个参与者之间的交互。在第一阶段,协调者向所有参与者发送预提交请求,参与者执行事务并将准备好的结果返回给协调者。如果所有参与者都准备好了,协调者会发送提交请求给所有参与者,否则会发送中止请求。在第二阶段,参与者根据协调者的请求进行最终提交或中止操作。两阶段提交保证了分布式事务的原子性和一致性,但存在一些问题,如阻塞、单点故障和长时间超时等。(跑步比赛,所有选手听裁判枪响开始比赛,裁判观察所有选手全部准备好才开枪)
- TCC:TCC 是一种通过明确的 Try、Confirm 和 Cancel 阶段来实现分布式事务的解决方案。在 TCC 中,每个参与者定义了三个方法:Try 用于预留资源和执行业务逻辑,Confirm 用于确认提交事务,Cancel 用于撤销预留的资源。当执行分布式事务时,首先执行 Try 阶段,如果所有参与者的 Try 阶段都成功,再依次执行 Confirm 阶段。如果任何一个 Confirm 阶段失败,会执行 Cancel 阶段来撤销已经预留的资源。TCC 的优势在于灵活性,每个参与者可以自定义 Try、Confirm 和 Cancel 的逻辑,但需要开发人员显式地编写这些逻辑。
无论是两阶段提交还是 TCC,它们都是为了解决分布式环境下的事务一致性问题而设计的。同时,也应该考虑到它们可能带来的一些问题,如性能开销、可扩展性和复杂性等。
两阶段提交
// 协调者
func coordinator() {
// 向参与者发送预提交请求
prepareRequest := sendPrepareRequest()
// 检查参与者的准备情况
if checkPrepareResponse(prepareRequest) {
// 向参与者发送提交请求
sendCommitRequest()
} else {
// 向参与者发送中止请求
sendAbortRequest()
}
}
// 参与者
func participant() {
// 接收协调者的预提交请求
prepareRequest := receivePrepareRequest()
// 执行事务逻辑
if executeTransaction() {
// 准备成功,返回成功响应给协调者
sendPrepareResponse(true)
} else {
// 准备失败,返回失败响应给协调者
sendPrepareResponse(false)
}
// 接收协调者的提交或中止请求,并执行相应操作
commitOrAbortRequest := receiveCommitOrAbortRequest()
if commitOrAbortRequest == Commit {
commitTransaction()
} else {
abortTransaction()
}
}
模拟两阶段提交
通过 var wg sync.WaitGroup 加锁的方式实现等待参与者全部已就绪
package main
import (
"fmt"
"sync"
)
type Participant struct {
id int
vote bool
}
func main() {
// 定义参与者列表
participants := []Participant{
{id: 1, vote: true},
{id: 2, vote: false}, // 模拟一个未准备好的参与者
}
// 定义信号量,用于在不同阶段之间进行同步
var wg sync.WaitGroup
// 协调阶段
wg.Add(len(participants))
for _, p := range participants {
go func(p Participant) {
fmt.Printf("Participant %d: Received Prepare request\n", p.id)
prepare(p)
wg.Done()
}(p)
}
wg.Wait()
// 检查是否有未准备好的参与者
for _, p := range participants {
if !p.vote {
fmt.Printf("Participant %d is not prepared. Terminating the transaction.\n", p.id)
return
}
}
// 投票阶段
var agree int
wg.Add(len(participants))
for _, p := range participants {
go func(p Participant) {
fmt.Printf("Participant %d: Received CanCommit request\n", p.id)
vote(p)
if p.vote {
agree++
}
wg.Done()
}(p)
}
wg.Wait()
// 提交阶段
wg.Add(len(participants))
for _, p := range participants {
go func(p Participant) {
fmt.Printf("Participant %d: Received DoCommit request\n", p.id)
doCommit(p)
wg.Done()
}(p)
}
wg.Wait()
// 协议执行完毕,输出最终结果
if agree == len(participants) {
fmt.Println("All participants agreed to commit")
} else {
fmt.Println("Some participants refused to commit")
}
}
// 处理 Prepare 请求
func prepare(p Participant) {
// 模拟未准备好的参与者
if p.id == 2 {
fmt.Printf("Participant %d is not prepared\n", p.id)
p.vote = false
return
}
// 这里省略本地预处理逻辑
fmt.Printf("Participant %d: Prepared\n", p.id)
}
// 处理 CanCommit 请求
func vote(p Participant) {
// 如果之前已经拒绝提交,则直接返回 No 响应
if !p.vote {
fmt.Printf("Participant %d: Already voted No\n", p.id)
return
}
// 模拟随机结果,50% 概率投票 Yes,50% 概率投票 No
if randBool() {
fmt.Printf("Participant %d: Voted Yes\n", p.id)
} else {
fmt.Printf("Participant %d: Voted No\n", p.id)
p.vote = false
}
}
// 处理 DoCommit 请求
func doCommit(p Participant) {
// 如果之前已经拒绝提交,则直接返回 Aborted 响应
if !p.vote {
fmt.Printf("Participant %d: Refused to commit\n", p.id)
return
}
// 这里省略本地提交逻辑
fmt.Printf("Participant %d: Committed\n", p.id)
}
// 随机生成一个布尔值
func randBool() bool {
// 省略随机逻辑,这里直接返回 true 或 false
return true
}
TCC
// Try 阶段
func tryPhase() error {
// 预留资源和执行业务逻辑
if reserveResource() && executeBusinessLogic() {
return nil
}
// Try 阶段失败,抛出错误,触发 Cancel 阶段
return errors.New("Try phase failed")
}
// Confirm 阶段
func confirmPhase() error {
// 确认提交事务
if confirmTransaction() {
return nil
}
// Confirm 阶段失败,抛出错误
return errors.New("Confirm phase failed")
}
// Cancel 阶段
func cancelPhase() {
// 撤销预留的资源
cancelResource()
// 执行撤销操作
rollbackTransaction()
}
// 分布式事务处理
func distributedTransaction() {
// 执行 Try 阶段
if err := tryPhase(); err == nil {
// Try 阶段成功,执行 Confirm 阶段
if confirmErr := confirmPhase(); confirmErr != nil {
// Confirm 阶段失败,执行 Cancel 阶段
cancelPhase()
}
} else {
// Try 阶段失败,执行 Cancel 阶段
cancelPhase()
}
}
两个服务模拟TCC
1.订单服务
// Try 阶段:预留库存、创建订单
func (s *OrderService) Try(ctx context.Context, order *Order) error {
// 调用库存服务,预留库存
err := s.InventoryClient.Reserve(ctx, order.ProductID, order.Quantity)
if err != nil {
return err
}
// 创建订单
_, err = s.DB.ExecContext(ctx, "INSERT INTO orders (product_id, quantity, amount) VALUES (?, ?, ?)", order.ProductID, order.Quantity, order.Amount)
if err != nil {
return err
}
// Try 阶段执行成功,返回 nil 表示可以进入 Confirm 阶段
return nil
}
// Confirm 阶段:提交订单
func (s *OrderService) Confirm(ctx context.Context, order *Order) error {
// 提交订单
_, err := s.DB.ExecContext(ctx, "UPDATE orders SET status = ? WHERE id = ?", OrderStatusPaid, order.ID)
if err != nil {
return err
}
// Confirm 阶段执行成功,返回 nil 表示事务提交成功
return nil
}
// Cancel 阶段:撤销预留、删除订单
func (s *OrderService) Cancel(ctx context.Context, order *Order) error {
// 调用库存服务,撤销库存预留
err := s.InventoryClient.Cancel(ctx, order.ProductID, order.Quantity)
if err != nil {
return err
}
// 删除订单
_, err = s.DB.ExecContext(ctx, "DELETE FROM orders WHERE id = ?", order.ID)
if err != nil {
return err
}
// Cancel 阶段执行成功,返回 nil 表示事务回滚成功
return nil
}
2.库存服务
// Try 阶段:减少库存
func (s *InventoryService) Try(ctx context.Context, productID int64, quantity int64) error {
// 查询库存数量
var count int64
err := s.DB.QueryRowContext(ctx, "SELECT count FROM inventories WHERE product_id = ?", productID).Scan(&count)
if err != nil {
return err
}
// 判断库存是否充足
if count < quantity {
return errors.New("insufficient inventory")
}
// 减少库存
_, err = s.DB.ExecContext(ctx, "UPDATE inventories SET count = count - ? WHERE product_id = ?", quantity, productID)
if err != nil {
return err
}
// Try 阶段执行成功,返回 nil 表示可以进入 Confirm 阶段
return nil
}
// Confirm 阶段:不需要实现任何操作,直接返回 nil 表示事务提交成功
func (s *InventoryService) Confirm(ctx context.Context, productID int64, quantity int64) error {
return nil
}
// Cancel 阶段:恢复库存
func (s *InventoryService) Cancel(ctx context.Context, productID int64, quantity int64) error {
// 恢复库存
_, err := s.DB.ExecContext(ctx, "UPDATE inventories SET count = count + ? WHERE product_id = ?", quantity, productID)
if err != nil {
return err
}
// Cancel 阶段执行成功,返回 nil 表示事务回滚成功
return nil
}
3.4 依赖
- 切记,在服务需要变更时我们要特别小心,服务提供者的变更可能引发服务消费者的兼容性破坏,时刻谨记保持服务契约(接口)的兼容性
- 发送时要保守,接收时要开放。按照伯斯塔尔法则的思想来设计和实现服务时,发送的数据要更保守,意味着最小化的传送必要的信息,接收时更开放意味着要最大限度的容忍冗余数据,保证兼容性。
评论区