联犀是如何架构好一个既要又要的系统
约 2203 字大约 7 分钟
2025-03-03
单体和微服务说明
微服务架构
微服务架构通过将应用程序分解为一组小型服务来实现,每个服务在自己的进程中运行,并通过轻量级通信机制(rpc或http)交互。这些服务围绕业务能力构建,支持独立部署。微服务强调服务间的松耦合、高内聚和自治,以提升系统的可伸缩性、可维护性和可重用性。
单体架构
单体架构是传统的软件架构模式,将所有功能打包在一个进程中。这种模式下,代码、数据库、配置和第三方库紧密耦合,形成一个庞大的整体。单体架构的优点在于开发简单、部署方便、易于测试和调试。然而,随着业务的发展和系统规模的扩大,单体架构面临着可扩展性差、维护困难、技术栈僵化等问题。
对比
可伸缩性
- 微服务架构:通过将应用程序拆分为多个独立的服务,每个服务都可以根据实际需求进行独立的扩展,适合处理高并发、大数据量场景。
- 单体架构:扩展时需要整体升级硬件资源,成本较高且不够灵活。
可维护性
- 微服务架构:服务之间松耦合,独立开发、测试和部署,降低了服务间的依赖关系,提高了系统的可维护性。
- 单体架构:出现问题时需要修复整个应用程序,维护成本较高。
技术栈选择
- 微服务架构:允许每个服务采用不同的技术栈,提高系统的性能和稳定性,但也可能导致技术栈多样化和复杂性增加。
- 单体架构:通常使用统一的技术栈,降低了技术复杂性和学习成本。
部署和测试
- 微服务架构:支持独立部署和测试每个服务,加快项目交付速度,提高开发效率,但可能导致服务间依赖关系复杂化。
- 单体架构:部署和测试时需要整体打包和发布,开发流程相对较慢但较为简单。
团队协作与沟通
- 微服务架构:应用程序拆分为多个独立的服务,团队可以按照业务领域进行划分和协作,提高团队协作效率,但可能导致接口定义、数据交换和通信协议等方面的沟通和协调成本增加。
- 单体架构:团队协作和沟通方面相对简单,但可能面临跨部门协作的困难。
架构方案
问题
既然单体和微服务各有各的好,那么是否有一种办法可以结合微服务和单体的优势呢?
传统的单体和微服务其实指的是部署的方式,但是现实情况远比理想情况复杂,比如公司的业务及请求量是不断增长的,但是一开始采用微服务的话成本会太高,一开始用单体的话后续还得重构,但是一开始写成单体的代码,没有做好领域隔离的话重构起来的工作量不比重写一个小.
那么是否可以在业务量小的时候使用单体模式,业务量增加之后使用微服务的方式进行部署呢? 这样无论业务量多大,都可以轻松支持.
解决方案
服务拆分
首先,我们要明确一点,微服务并不是拆分的越细越好,一旦两个服务的数据有高度交叉,如获取的数据需要从两个表连表才能处理,那么他本来就应该是一个服务. 从ddd的领域来说,一个物理上的服务其实可以放多个领域,而我们在实际开发的时候也可以使用该方式将相近的领域聚合到一个服务中,如用户,角色,日志,通知等等其实都是属于一个应用所必须的功能,那么其实就没有必要把他们拆开,像rpc也可以使用service的方式来进行区分.
单体微服务结合
- api网关聚合: 传统的微服务架构约等于一个rpc服务对应一个api网关,或者说BFF(Backend for Front),但是这种方式会带来服务数量的暴增,那么我们是否可以采用一个大的业务或者说模块使用一个api网关来提供 ,如设备管理,设备网关,用户设备本质上都属于物联网的功能,那么就没必要每个都创建一个api网关,集合成一个即可.

- api网关集成微服务和单体模式: 如果我们可以借助代码生成生成rpc的访问方式和代码直接调用的两种调用方式,那么我们在生产部署的时候就可以根据我们的需要去进行部署,单体模式也好,集群模式也好,微服务模式也好,按需选用.
那么,如何实现以上方式呢?
- 代码生成: 首先我们需要借助gozero的代码生成工具(goctl)来实现同时支持的能力,goctl在生成rpc的代码的时候会生成封装好的客户端请求代码,服务端的处理代码.联犀通过修改生成的模版,同时生成rpc请求代码和直连的代码,由于对外都是相同的interface,所以只有初始化的时候有差异,在业务内使用是完全一致的.生成的代码示例:
type (
Rule interface {
// 场景
SceneInfoCreate(ctx context.Context, in *SceneInfo, opts ...grpc.CallOption) (*WithID, error)
SceneInfoUpdate(ctx context.Context, in *SceneInfo, opts ...grpc.CallOption) (*Empty, error)
SceneInfoDelete(ctx context.Context, in *WithID, opts ...grpc.CallOption) (*Empty, error)
SceneInfoIndex(ctx context.Context, in *SceneInfoIndexReq, opts ...grpc.CallOption) (*SceneInfoIndexResp, error)
SceneInfoRead(ctx context.Context, in *WithID, opts ...grpc.CallOption) (*SceneInfo, error)
SceneManuallyTrigger(ctx context.Context, in *WithID, opts ...grpc.CallOption) (*Empty, error)
SceneLogIndex(ctx context.Context, in *SceneLogIndexReq, opts ...grpc.CallOption) (*SceneLogIndexResp, error)
}
defaultRule struct {
cli zrpc.Client
}
directRule struct {
svcCtx *svc.ServiceContext
svr ud.RuleServer
}
)
//rpc模式初始化代码
func NewRule(cli zrpc.Client) Rule {
return &defaultRule{
cli: cli,
}
}
//直连模式初始化代码
func NewDirectRule(svcCtx *svc.ServiceContext, svr ud.RuleServer) Rule {
return &directRule{
svr: svr,
svcCtx: svcCtx,
}
}
// 场景 rpc模式请求代码
func (m *defaultRule) SceneInfoCreate(ctx context.Context, in *SceneInfo, opts ...grpc.CallOption) (*WithID, error) {
client := ud.NewRuleClient(m.cli.Conn())
return client.SceneInfoCreate(ctx, in, opts...)
}
// 场景 直连模式请求代码
func (d *directRule) SceneInfoCreate(ctx context.Context, in *SceneInfo, opts ...grpc.CallOption) (*WithID, error) {
return d.svr.SceneInfoCreate(ctx, in)
}
- 初始化:可以根据配置来选择使用微服务模式(grpc访问)或直连模式(代码直接调用)来实现灵活的部署配置:
if c.UdRpc.Enable {
if c.UdRpc.Mode == conf.ClientModeGrpc {//grpc模式初始化
Rule = rule.NewRule(zrpc.MustNewClient(c.UdRpc.Conf))
} else {//直连模式初始化
Rule = uddirect.NewRule(c.UdRpc.RunProxy)
}
}
- 单服务聚合多个领域:rpc服务使用service来同时支持多个领域,具体可以参考(https://go-zero.dev/docs/tutorials/proto/services/group)

- 降低领域模型转换的代码量: 在微服务开发中有个工作量最大的事情就是领域模型的转换,api结构体转换成rpc结构体转换成orm结构体.枯燥乏味而且容易出错.联犀基于jinzhu的
//结构体转换
utils.Copy[relationDB.SysOpsFeedback](in)
//切片转换
utils.CopySlice[sys.DictDetail](in)
//map转换
utils.CopyMap[relationDB.SharePerm](in.SchemaPerm)
//函数定义
//指针类型结构体转换
func Copy[toT any](fromValue any) *toT
//非指针类型结构体转换
func Copy2[toT any](fromValue any) toT
//切片转换
func CopySlice[toT any, fromT any](fromValue []*fromT) []*toT
//数组转换
func CopyMap[toT any, fromT any, keyT comparable](fromValue map[keyT]*fromT) map[keyT]*toT
//包含转换错误的通用转换
func CopyE(toValue interface{}, fromValue interface{}) (err error)
代码位置: share/utils/copier.go
总结
借助以下几个法宝:
- 合理的服务拆分
- 单体和微服务架构的统一
- 便捷的领域转换工具
联犀可以高效的实现以下既要又要的能力,同时又不失优雅.
- 既要快速开发的能力又要高度的拓展能力
- 既要可以小规模部署,也可以大规模部署
- 既要微服务的灵活性,又要单体的便捷性
最终实现以下联犀的架构图:
物联网模块架构设计
SaaS中台架构设计