服务化架构设计:微服务拆分原则与核心服务边界

好,咱们进入第二章。上一章聊了为什么要做服务化改造,这一章直接落地——怎么拆?拆成几个服务?每个服务管什么?

说实话,我见过不少团队一上来就搞十几个微服务,结果运维成本直接爆炸。我个人习惯是:先按职责边界拆,再按业务粒度优化。数据目录这个场景,核心就三个服务——元数据服务、搜索服务、血缘服务。把这三个的边界划清楚,剩下的都是锦上添花。

微服务拆分的基本原则

先说说拆分原则。你想想看,一个数据目录系统,最核心的诉求是什么?是让用户找得到、看得懂、信得过。围绕这个目标,我总结了三条原则:

  • 单一职责:每个服务只做一件事,并且做好。元数据服务只管元数据的增删改查,别往里塞搜索逻辑。
  • 数据自治:每个服务拥有自己的数据库,服务之间通过API通信,不直接访问对方的库。我在项目中遇到过,两个服务共享一个MySQL实例,结果一个慢查询把另一个拖垮了——血的教训。
  • 接口契约化:服务间交互必须定义明确的接口协议,版本管理要跟上。我曾经因为接口字段名改了一个字母,导致下游服务解析失败,排查了一整天。

核心观点:微服务拆分不是越细越好,而是找到「业务内聚」和「独立演进」的平衡点。数据目录的三个核心服务,就是按这个思路拆出来的。

元数据服务:数据目录的「骨架」

元数据服务是整个数据目录的基石。说白了,它就是管理「数据的数据」——表结构、字段定义、分区信息、存储位置、数据量、更新时间等等。

它的职责边界很清晰:

  • 元数据采集:从各种数据源(Hive、MySQL、Kafka、HDFS等)拉取元数据信息
  • 元数据存储:维护元数据的版本历史和变更记录
  • 元数据查询:提供按库、表、字段维度的查询接口
  • 元数据变更通知:当元数据发生变化时,推送事件给其他服务

嗯,这里要注意:元数据服务不要做搜索。搜索是另一个服务的活。我见过有人把ES直接嵌在元数据服务里,结果元数据一更新,ES索引重建把CPU打满,查询接口全挂了。

个人经验:元数据服务的数据模型建议用「图」结构来设计。表和字段之间、表和表之间天然存在关联关系,用图数据库(如Neo4j)或者关系型数据库+关系表都可以。我早期用MySQL硬撑,后来数据量到百万级,关联查询慢得不行,才换成了图存储。

搜索服务:数据目录的「入口」

搜索服务是用户最直接接触的。用户打开数据目录,第一件事就是搜——搜表名、搜字段、搜描述。搜索服务的核心职责就是让用户快速找到想要的数据。

它的职责边界:

  • 索引构建:从元数据服务获取元数据,构建全文索引和倒排索引
  • 搜索查询:支持模糊搜索、精确搜索、标签搜索、高级过滤
  • 搜索排序:根据热度、相关性、更新时间等维度排序结果
  • 搜索建议:输入过程中的自动补全和推荐

为什么要把搜索单独拆出来?原因很简单:搜索的负载模式和元数据完全不同。元数据服务是读写均衡,搜索服务是读多写少,而且搜索的QPS通常比元数据高一个数量级。混在一起,资源没法独立扩缩容。

避坑指南:我曾经把搜索服务的索引更新做成全量重建,每天凌晨跑一次。结果白天新增的表,用户搜不到,被投诉了好几次。后来改成增量更新+准实时同步,才解决问题。记住:搜索的时效性直接影响用户体验。

血缘服务:数据目录的「脉络」

血缘服务是数据目录里最有技术含量的一个。它要回答的问题是:这张表的数据从哪来?经过哪些处理?流向了哪里?

它的职责边界:

  • 血缘解析:解析SQL、ETL脚本、数据管道配置,提取数据流向关系
  • 血缘存储:以有向无环图(DAG)的形式存储血缘关系
  • 血缘查询:支持向上溯源、向下追踪、影响分析
  • 血缘可视化:提供图结构的查询结果,供前端渲染

说实话,血缘服务是最容易和元数据服务「打架」的。两者都涉及表和字段的关系,但视角不同:元数据关注的是「静态结构」,血缘关注的是「动态流向」。我建议把血缘单独拆出来,因为血缘的解析逻辑非常复杂,而且血缘图的查询模式(图遍历)和元数据的查询模式(点查+列表)完全不同。

关键区别:元数据服务回答「是什么」,血缘服务回答「怎么来的」。两者数据模型不同、查询模式不同、性能要求不同,必须分开。

三个服务的协作关系

三个服务不是孤立的,它们之间有明确的协作流程。我画了一张图,帮你理清关系:

数据目录核心服务协作架构 数据源(Hive/MySQL/Kafka) 元数据服务 采集·存储·查询·变更通知 搜索服务 索引构建·搜索查询·排序建议 血缘服务 SQL解析·血缘存储·影响分析 用户/前端应用 元数据采集 SQL/ETL日志 元数据同步 表结构信息 元数据查询 搜索接口 血缘查询 说明:元数据服务是数据源和搜索服务、血缘服务之间的桥梁。搜索服务和血缘服务不直接对接数据源, 所有元数据变更都通过元数据服务统一管理,保证数据一致性。

服务拆分后的接口设计

服务拆完了,接口怎么设计?我列了一个表格,把三个服务的核心接口和调用方说清楚:

服务 核心接口 调用方 协议
元数据服务 getTableInfo, listDatabases, getFieldDetail, subscribeChange 搜索服务、血缘服务、前端 gRPC + Protobuf
搜索服务 search, suggest, getSearchResult, rebuildIndex 前端 RESTful JSON
血缘服务 getLineage, traceUpstream, traceDownstream, analyzeImpact 前端 RESTful JSON

为什么元数据服务用gRPC,搜索和血缘用REST?原因很简单:元数据服务是内部基础设施,调用频率高、数据量大,gRPC的二进制传输和流式处理更高效。搜索和血缘面向用户,REST接口更灵活,前端调起来也方便。

小技巧:接口版本号一定要带上。我习惯在URL或者gRPC的service名里加v1、v2。别问我为什么——有一次我改了元数据服务的返回结构,忘了通知搜索服务,结果搜索索引全乱了。从那以后,接口版本管理成了我的铁律。

总结一下

这一章的核心就三句话:

  • 元数据服务管「静态结构」,是数据目录的骨架
  • 搜索服务管「快速查找」,是数据目录的入口
  • 血缘服务管「数据流向」,是数据目录的脉络