一、为什么中大型项目要重视可扩展性?

很多做过中大型项目的开发者应该都有过这样的体会:项目刚上线时,几个服务就能搞定所有业务,跑起来顺得很;但过个半年一年,业务需求越来越多,比如电商项目要加会员体系、积分体系、多仓库存,教育项目要加直播、题库、学情分析,原来的服务慢慢就撑不住了——要么改代码改得牵一发动全身,要么加新功能得大动架构,甚至原来的功能还会跟着出bug。

可扩展性说白了,就是项目能“轻松加功能、轻松扛压力”的能力。对于中大型微服务项目来说,这种能力不是“加分项”,是“必选项”:一方面,业务发展快,新需求层出不穷,要是每次加功能都要重构,不仅开发效率低,还会耽误业务上线;另一方面,用户量可能突然爆发(比如电商大促、教育机构搞活动),要是架构扛不住,直接就是宕机、损失用户。

Kratos是国内团队开发的一套微服务框架,专门给中大型项目设计的,它的可扩展性设计不是空架子,是真的能解决实际问题的。接下来咱们就拆解开说。

二、Kratos可扩展性的核心设计思路

Kratos的可扩展性不是随便凑的功能,而是围绕“业务和底层解耦、模块能替换、资源能动态调”这三个核心思路来做的。简单说就是:业务代码不用管底层的事(比如怎么连数据库、怎么调别的服务),底层模块想换就换,资源不够能随时加。

2.1 业务与底层的解耦设计

解耦是可扩展性的基础,要是业务代码和底层技术绑死了,换个数据库都得改所有业务逻辑,那根本没法扩展。Kratos用的是“分层架构”,把整个项目分成三层:业务层、中间件层、基础设施层。

举个例子,咱们做一个电商的订单服务,业务层只需要写“创建订单要扣库存、减余额、生成订单号”这些业务逻辑,不用管“库存存在MySQL还是Redis”“余额存在支付宝还是微信”“怎么调用库存服务”这些事;中间件层负责把业务层的请求转成能被基础设施理解的格式;基础设施层才是真的连数据库、调服务的地方。

为了让大家更清楚,咱们写个具体的例子,先说明这个例子用的技术栈:Go语言(因为Kratos是Go写的,所有示例都用Go)。

首先是业务层的代码,只关心订单的核心逻辑:

// 业务层:订单服务的核心逻辑,只处理业务规则
package order

import (
	"context"
	"errors"
)

// 定义订单业务需要的接口(解耦的关键:只定义要什么,不定义怎么实现)
type OrderService interface {
	CreateOrder(ctx context.Context, userID int64, productID int64, amount float64) error
}

// 具体的订单业务逻辑实现
type orderServiceImpl struct {
	// 依赖的外部服务,用接口类型,不用具体实现
	stockService StockService
	payService   PayService
}

// 新建订单业务实例
func NewOrderService(stock StockService, pay PayService) OrderService {
	return &orderServiceImpl{
		stockService: stock,
		payService:   pay,
	}
}

// 创建订单的核心逻辑
func (o *orderServiceImpl) CreateOrder(ctx context.Context, userID int64, productID int64, amount float64) error {
	// 第一步:扣库存,不用管库存服务怎么实现
	if err := o.stockService.DeductStock(ctx, productID, 1); err != nil {
		return errors.New("扣库存失败:" + err.Error())
	}

	// 第二步:扣余额,不用管支付服务怎么实现
	if err := o.payService.DeductBalance(ctx, userID, amount); err != nil {
		return errors.New("扣余额失败:" + err.Error())
	}

	// 第三步:生成订单号(这里简化,实际会更复杂)
	orderID := generateOrderID()
	_ = orderID // 这里可以存订单,先省略

	return nil
}

// 生成订单号的工具函数
func generateOrderID() string {
	return "ORD" + string(time.Now().UnixMilli())
}

然后是中间件层和基础设施层的代码,这两层可以随时换,不影响业务层:

// 中间件层:定义外部服务的接口,和业务层的接口对应
package middleware

// 库存服务接口,业务层依赖这个接口
type StockService interface {
	DeductStock(ctx context.Context, productID int64, num int) error
}

// 支付服务接口,业务层依赖这个接口
type PayService interface {
	DeductBalance(ctx context.Context, userID int64, amount float64) error
}
// 基础设施层:库存服务的具体实现(比如用MySQL)
package infra

import (
	"context"
	"errors"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

// MySQL库存服务实现
type MysqlStockService struct {
	db *gorm.DB
}

// 新建MySQL库存服务
func NewMysqlStockService(dsn string) (*MysqlStockService, error) {
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		return nil, err
	}
	return &MysqlStockService{db: db}, nil
}

// 实现扣库存的具体逻辑
func (m *MysqlStockService) DeductStock(ctx context.Context, productID int64, num int) error {
	// 这里是具体的MySQL操作,业务层完全不用管
	result := m.db.WithContext(ctx).Model(&Stock{}).Where("product_id = ?", productID).Update("stock", gorm.Expr("stock - ?", num))
	if result.Error != nil {
		return result.Error
	}
	if result.RowsAffected == 0 {
		return errors.New("库存不足")
	}
	return nil
}

// 库存表的模型
type Stock struct {
	ProductID int64 `gorm:"column:product_id"`
	Stock     int   `gorm:"column:stock"`
}

要是哪天业务发展了,库存要换成Redis来存(比如秒杀场景,Redis的性能更高),只需要改基础设施层的代码,写一个Redis的库存服务,业务层的代码完全不用动:

// 基础设施层:Redis库存服务实现(新增的,不用改原来的代码)
package infra

import (
	"context"
	"errors"
	"github.com/redis/go-redis/v9"
)

// Redis库存服务实现
type RedisStockService struct {
	client *redis.Client
}

// 新建Redis库存服务
func NewRedisStockService(addr string) *RedisStockService {
	client := redis.NewClient(&redis.Options{Addr: addr})
	return &RedisStockService{client: client}
}

// 实现扣库存的具体逻辑,和MySQL的接口完全一样
func (r *RedisStockService) DeductStock(ctx context.Context, productID int64, num int) error {
	// 这里是具体的Redis操作,业务层还是不用管
	key := "stock:" + string(productID)
	// 先判断库存够不够
	stock, err := r.client.Get(ctx, key).Int()
	if err != nil {
		return errors.New("库存不存在")
	}
	if stock < num {
		return errors.New("库存不足")
	}
	// 扣库存
	_, err = r.client.DecrBy(ctx, key, int64(num)).Result()
	return err
}

最后在主函数里,只需要把依赖的服务换成Redis的,业务层的CreateOrder逻辑完全不用改:

func main() {
	// 原来用MySQL库存服务的写法
	// stockService, err := infra.NewMysqlStockService("root:123456@tcp(localhost:3306)/order")
	// if err != nil {
	// 	panic(err)
	// }

	// 现在换成Redis库存服务,只改这一行
	stockService := infra.NewRedisStockService("localhost:6379")

	payService := infra.NewMysqlPayService("root:123456@tcp(localhost:3306)/order")
	orderService := order.NewOrderService(stockService, payService)

	// 调用创建订单的逻辑,完全不用改
	ctx := context.Background()
	err := orderService.CreateOrder(ctx, 1001, 2001, 99.9)
	if err != nil {
		panic(err)
	}
}

这个例子就能看出来解耦的好处:业务层的核心逻辑稳定,底层技术想换就换,不用动业务代码,扩展起来特别轻松。

2.2 模块的可替换设计

Kratos的所有模块都是“插件化”的,就像手机的APP,不用的可以卸载,需要的可以安装。比如日志模块、监控模块、服务发现模块,都是独立的,想换哪个就换哪个。

比如原来用的是ELK做日志系统,后来换成了Loki,只需要把Kratos的日志插件换成Loki的,其他代码不用动。再比如服务发现,原来用的是Consul,后来换成了Nacos,也是只换插件就行。

Kratos的模块替换是有规范的,每个模块都要实现Kratos定义的接口,比如日志模块要实现Logger接口,服务发现模块要实现Discovery接口。只要符合接口规范,自己写的模块也能插进去。

举个服务发现的例子,Kratos默认的服务发现是Consul,要是想换成Nacos,只需要写一个实现Discovery接口的Nacos服务发现模块:

// 实现Kratos的服务发现接口
package discovery

import (
	"context"
	"github.com/go-kratos/kratos/v2/registry"
	"github.com/nacos-group/nacos-sdk-go/clients"
	"github.com/nacos-group/nacos-sdk-go/clients/naming_client"
	"github.com/nacos-group/nacos-sdk-go/vo"
)

// Nacos服务发现模块
type NacosDiscovery struct {
	client naming_client.INamingClient
}

// 新建Nacos服务发现实例
func NewNacosDiscovery(addr string) (*NacosDiscovery, error) {
	client, err := clients.NewNamingClient(vo.NacosClientParam{
		ClientConfig: &vo.ClientConfig{ServerAddr: addr},
	})
	if err != nil {
		return nil, err
	}
	return &NacosDiscovery{client: client}, nil
}

// 实现Kratos要求的服务发现接口:获取服务实例
func (n *NacosDiscovery) GetService(ctx context.Context, serviceName string) ([]*registry.ServiceInstance, error) {
	// 从Nacos获取服务实例
	instances, err := n.client.SelectOneHealthyInstance(vo.SelectOneHealthyInstanceParam{
		ServiceName: serviceName,
	})
	if err != nil {
		return nil, err
	}
	// 转成Kratos要求的格式
	return []*registry.ServiceInstance{
		{
			ID:       instances.InstanceId,
			Name:     serviceName,
			Endpoint: instances.Ip + ":" + string(instances.Port),
		},
	}, nil
}

// 实现Kratos要求的服务发现接口:监听服务变化
func (n *NacosDiscovery) Watch(ctx context.Context, serviceName string) (registry.Watcher, error) {
	// 监听服务变化的逻辑,这里简化
	return &nacosWatcher{}, nil
}

// 监听接口的实现
type nacosWatcher struct{}

func (w *nacosWatcher) Next() ([]*registry.ServiceInstance, error) {
	// 具体的监听逻辑
	return nil, nil
}

func (w *nacosWatcher) Stop() error {
	return nil
}

然后在Kratos的主配置里,把服务发现换成Nacos的就行:

func main() {
	// 原来的Consul服务发现
	// reg := consul.New("localhost:8500")

	// 现在换成Nacos服务发现
	reg, err := discovery.NewNacosDiscovery("localhost:8848")
	if err != nil {
		panic(err)
	}

	// 启动Kratos服务,配置服务发现
	srv := kratos.New(
		kratos.Name("order-service"),
		kratos.Registrar(reg),
		kratos.Discovery(reg),
	)
	if err := srv.Run(); err != nil {
		panic(err)
	}
}

这个例子就能看出来,模块替换是完全不影响业务逻辑的,只需要改配置和插件,扩展起来特别灵活。

2.3 资源的动态扩展设计

可扩展性还有一个很重要的点,就是资源能动态调整,比如某个服务的压力大了,能随时加实例;压力小了,能随时减实例。Kratos的动态扩展是和容器编排(比如Kubernetes)结合的,服务本身是无状态的,所以能轻松伸缩。

无状态服务就是说,服务本身不存任何状态(比如用户的登录状态、订单的临时数据),所有状态都存在外部的存储里(比如Redis、MySQL)。这样的话,一个服务的多个实例是完全一样的,加新实例的时候,不用做任何额外的配置,直接启动就行。

比如电商的订单服务,要是大促的时候用户量突然涨了10倍,订单服务的压力大了,只需要在Kubernetes里把订单服务的副本数从1改成10,10个实例就能一起扛压力,完全不用改代码;大促结束后,再把副本数改回1就行。

Kratos的服务本身就是无状态的,它的配置是存在配置中心的,比如Nacos或者Consul,服务启动的时候从配置中心拉取配置,所以多个实例的配置是完全一样的,不用每个实例单独配置。

举个Kratos服务配置的例子,配置中心用Nacos,配置的内容是订单服务的数据库地址、端口、超时时间:

// Nacos里的配置内容(key是order-service/config)
{
  "server": {
    "addr": ":8080",
    "timeout": "10s"
  },
  "database": {
    "dsn": "root:123456@tcp(localhost:3306)/order",
    "max_open_conns": 100,
    "max_idle_conns": 10
  }
}

然后Kratos服务启动的时候,从Nacos拉取配置:

func main() {
	// 从Nacos拉取配置
	config, err := config.NewNacosConfig("localhost:8848", "order-service/config")
	if err != nil {
		panic(err)
	}

	// 启动服务,配置的内容都是从Nacos拉取的,多个实例的配置完全一样
	srv := kratos.New(
		kratos.Name("order-service"),
		kratos.Config(config),
	)
	if err := srv.Run(); err != nil {
		panic(err)
	}
}

这样的话,加新实例的时候,只需要启动一个新的Kratos服务,它会自动从Nacos拉取和原来实例一样的配置,不用做任何额外的工作,动态扩展特别轻松。

三、Kratos可扩展性的应用场景

Kratos的可扩展性设计,特别适合中大型微服务项目,具体的应用场景主要有这几个:

第一个是业务快速迭代的场景,比如互联网公司的电商、社交、教育项目,新需求层出不穷,需要频繁加功能、改逻辑。Kratos的解耦设计,能让业务逻辑和底层技术分开,改底层不影响业务,改业务也不影响底层,开发效率特别高。

第二个是用户量爆发的场景,比如电商大促、教育机构搞活动、社交平台的热点事件,用户量突然涨很多,需要架构能快速扛住压力。Kratos的动态扩展设计,能让服务轻松加实例,扛住压力。

第三个是技术栈升级的场景,比如原来用的技术栈比较老,需要换成新的技术栈,比如从MySQL换成Redis,从Consul换成Nacos,从ELK换成Loki。Kratos的模块可替换设计,能让技术栈升级的时候,不用改业务逻辑,只换模块就行,风险特别小。

第四个是多环境部署的场景,比如项目需要部署在不同的环境,比如开发环境、测试环境、生产环境,不同环境的配置不一样,甚至技术栈也不一样。Kratos的配置中心和模块可替换设计,能让不同环境的部署特别轻松,不用改代码。

四、Kratos可扩展性的优缺点

任何架构设计都有优缺点,Kratos的可扩展性设计也不例外,咱们得客观看待:

4.1 优点

第一个是解耦彻底,业务逻辑和底层技术分开,改底层不影响业务,改业务也不影响底层,开发效率高,维护成本低。比如原来的库存服务从MySQL换成Redis,只改基础设施层的代码,业务层完全不用动,不会影响原来的业务逻辑,风险特别小。

第二个是模块灵活,所有模块都是插件化的,想换哪个就换哪个,不用改核心代码。比如服务发现从Consul换成Nacos,只换插件就行,不用改业务逻辑,技术栈升级特别轻松。

第三个是动态扩展方便,服务是无状态的,能轻松加实例、减实例,扛住压力。比如大促的时候,订单服务的压力大了,只需要把副本数从1改成10,10个实例就能一起扛压力,完全不用改代码。

第四个是适合中大型项目,Kratos的可扩展性设计是专门为中大型项目设计的,能解决中大型项目的痛点,比如业务迭代快、用户量爆发、技术栈升级、多环境部署等。

4.2 缺点

第一个是学习成本高,Kratos的架构比较复杂,涉及到很多概念,比如分层架构、插件化、配置中心、服务发现、无状态服务等,新手需要花时间学习,才能掌握。

第二个是初期开发慢,因为要做解耦、要写接口、要配置模块,初期的开发量比普通的单体项目或者简单的微服务项目要大,比如写一个订单服务,要写业务层、中间件层、基础设施层,还要配置服务发现、配置中心,初期的开发时间会比普通项目长。

第三个是依赖多,Kratos的可扩展性设计依赖很多外部的技术,比如配置中心、服务发现、容器编排、数据库、缓存等,要是其中一个技术出问题,整个架构都会受影响,比如配置中心挂了,所有服务都没法启动。

第四个是不适合小型项目,Kratos的可扩展性设计是为中大型项目设计的,要是项目很小,比如只有几个服务,用户量也不大,用Kratos反而会增加复杂度,开发效率低,维护成本高。

五、使用Kratos可扩展性设计的注意事项

要是想用Kratos的可扩展性设计,得注意这几个问题:

第一个是不要过度设计,解耦和插件化是为了扩展,不是为了解耦而解耦,要是某个模块肯定不会换,就不用做太复杂的解耦,比如项目确定用MySQL做数据库,就不用写Redis的库存服务接口,增加开发量。

第二个是要规范接口,所有模块的接口都要符合Kratos的规范,比如日志模块要实现Logger接口,服务发现模块要实现Discovery接口,要是接口不规范,模块就没法替换,扩展起来特别麻烦。

第三个是要做好配置管理,配置中心的配置要规范,不同环境的配置要分开,比如开发环境的数据库地址和生产环境的数据库地址要分开,配置要加密,比如数据库的密码不能明文存在配置中心,要加密存储。

第四个是要做好监控,动态扩展的时候,要监控服务的压力,比如CPU使用率、内存使用率、请求量、响应时间等,要是服务的压力大了,及时加实例;要是服务的压力小了,及时减实例,避免浪费资源。

第五个是要测试,每次换模块或者加实例的时候,都要做测试,比如把库存服务从MySQL换成Redis,要测试扣库存的逻辑是不是正常,会不会有数据不一致的问题;加实例的时候,要测试多个实例是不是能正常工作,会不会有负载不均衡的问题。

六、总结

Kratos的可扩展性设计,是为中大型微服务项目量身定做的,核心思路是业务与底层解耦、模块可替换、资源动态调,能解决中大型项目的痛点,比如业务迭代快、用户量爆发、技术栈升级、多环境部署等。

它的优点是解耦彻底、模块灵活、动态扩展方便、适合中大型项目;缺点是学习成本高、初期开发慢、依赖多、不适合小型项目。

要是想用Kratos的可扩展性设计,得注意不要过度设计、规范接口、做好配置管理、做好监控、做好测试。

对于中大型微服务项目来说,Kratos的可扩展性设计是一个很好的选择,能让项目轻松加功能、轻松扛压力,支撑业务的快速发展。