微服务开发规范

一. 项目说明

XXXX的所有微服务都基于springboot(1.5.7.RELEASE),提供dubbo的微服务基于dubbo(2.5.9)进行开发。为了保证这些第三方jar的版本统一,提供pom依赖,请不要直接引用springboot及dubbo的pom。

使用springboot的微服务,请配置pom的parent为如下:

<parent>

    <groupId>XXXXgroupId>

    <artifactId>infra-microservice-parentartifactId>

    <version>1.0-SNAPSHOTversion>

parent>

是用dubbo的微服务,请引用该pom:

<dependency>

    <groupId>XXXXgroupId>

    <artifactId>dubbo-springboot-starterartifactId>

    <version>2.0-SNAPSHOTversion>

dependency>

二. 微服务项目结构

1.微服务划分原则

把一个大系统划分为多个微服务,使模块间结构更加清晰,模块间更低耦合高内聚,有更好的扩展性和稳定性。微服务划分请按以下原则:
按功能模块划分微服务,尽量做到一个功能模块一个微服务。
微服务之间减少互相调用,做到低耦合高内聚。

2.微服务模块划分

针对单个微服务,采用maven模块化(module)拆分,具体拆分如下:

微服务名称

微服务说明

xxxx-xxxx 单个微服务的总目录,仅包含一个pom,pom中有module的描述,包括该微服务的所有module,微服务名称中间使用【-】分割
xxxx-xxxx-parent

其余所有微服务pom的parent,使得子POM可以获得 parent 中的各项配置,可以对子pom进行统一的配置和依赖管理。

※该pom的parent统一使用infra-microservice-parent,这样可以保证springboot版本的统一

xxxx-xxxx-api

接口层,包括对外暴露的所有接口以及接口使用的model

※理论上来说该pom没有任何其他api或者jar的引用

xxxx-xxxx(-dubbo)-client

其他dubbo consumer引用的dubbo的client,包括dubbo reference的xml

※理论上来说该pom只引用同一个微服务下面的api

xxxx-xxxx-service 微服务实现层,包括所有接口的实现
xxxx-xxxx-dubbo

实际微服务的springboot工程,将api暴露为dubbo服务。

※需要引用dubbo-springboot-starter

3. 聚合 VS 父POM

发现很多同事对maven 的parent(父Pom)和module(聚合)的概念混淆,导致pom的引用十分混乱。

父POM是为了抽取统一的配置信息和依赖版本控制,方便子POM直接引用,简化子POM的配置。

※其中relativePath元素不是必须的,指定后会优先从指定的位置查找父POM。

聚合(多模块)则是为了方便一组项目进行统一的操作而作为一个大的整体。

※在列出模块时,不需要自己考虑模块间依赖关系,即POM给出的模块排序并不重要。Maven将对模块进行拓扑排序,使得依赖关系始终在依赖模块之前构建。

所以要真正根据这两者不同的作用来使用,不必为了聚合而继承同一个父POM,也不必为了继承父POM而设计成多模块。

4.微服务gateway

因为微服务基于dubbo,所以需要将各个微服务的api以及client deploy到maven私服上以便其他微服务的dubbo consumer可以引用,另基于目前git的结构,会有若干个微服务放置到同一个git仓库中,所以需要一个额外的pom聚合,在微服务的git目录增加xxxx-xxxx-gateway module,用来做该git需要deploy的jar的聚合。例:

xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0"

         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

 

    <modelVersion>4.0.0modelVersion>

    <groupId>XXXXgroupId>

    <artifactId>platform-XXXX-gatewayartifactId>

    <version>1.0-SNAPSHOTversion>

    <packaging>pompackaging>

    <modules>

        <module>../platform-XXXX/platform-XXXX-parentmodule>

        <module>../platform-XXXX/platform-XXXX-apimodule>

        <module>../platform-XXXX/platform-XXXX-clientmodule>

    modules>

 

    <distributionManagement>

        <repository>

            <id>XXXXid>

            <url>http://XXXX/nexus/content/repositories/supplychainurl>

        repository>

    distributionManagement>

project>

※为了便于管理,只在xxxx-xxxx-gateway的pom中添加distributionManagement的描述

三. 微服务配置文件设置

为了减少环境变化时,外置yml文件每个环境都要修改的问题,现在采用通过设置环境变量的方式设置yml参数。

具体参照:微服务采用通过设置环境变量的方式设置yml参数

统一的逻辑为,所有应用根据环境不同,配置同一个setenv.sh文件,应用内部的配置文件都采用读取环境变量的方式。

这样应用在打包的时候与环境profile无关,安装程序时只要维护一个setenv.sh即可。具体参照:环境变量维护

 

四. 微服务端口号设置规范

为了避免同一台机器启动若干个微服务导致的接口冲突,需要统一分配微服务的端口号。

具体参照:微服务端口号分配

五. 微服务接口版本号设置规范

1.  dubbo协议接口约束

  • 参数及返回值需实现 Serializable 接口
  • 参数及返回值不能自定义实现 ListMapNumberDateCalendar 等接口,只能用 JDK 自带的实现,因为 hessian 会做特殊处理,自定义实现类中的属性值都会丢失。
  • Hessian 序列化,只传成员属性值和值的类型,不传方法或静态变量,兼容情况 :

数据通讯

情况

结果

A调用B 类A多一种 属性(或者说类B少一种 属性) 不抛异常,A多的那 个属性的值,B没有, 其他正常
A调用B 枚举A多一种 枚举(或者说B少一种 枚举),A使用多 出来的枚举进行传输 抛异常
A调用B 枚举A多一种 枚举(或者说B少一种 枚举),A不使用 多出来的枚举进行传输 不抛异常,B正常接 收数据
A调用B A和B的属性 名相同,但类型不相同 抛异常
A调用B serialId 不相同 正常传输

接口增加方法,对客户端无影响,如果该方法不是客户端需要的,客户端不需要重新部署。输入参数和结果集中增加属性,对客户端无影响,如果客户端并不需要新属性,不用重新部署。

输入参数和结果集属性名变化,对客户端序列化无影响,但是如果客户端不重新部署,不管输入还是输出,属性名变化的属性值是获取不到的。

总结:服务器端和客户端对领域对象并不需要完全一致,而是按照最大匹配原则。会抛异常的情况:枚举值一边多一种,一边少一种,正好使用了差别的那种,或者属性名相同,类型不同。

2. 接口版本号设置规范

按照1.接口约束的说明,暂定接口版本号设置规范如下:

  • 当接口无变化,接口实现有变化时,微服务api及client不用重新deploy,版本号不用升级。客户端不需要重新部署。
  • 当接口有变化,且增加方法或者增加接口时,微服务api及client需要重新deploy,版本号固定为本次release版本号。使用接口新方法的客户端需要更新pom,修改对应api的版本。未使用新接口或方法的客户端无需更新pom。
  • 当接口有变化,且修改接口,或者删除接口时,微服务api及client需要重新deploy,版本号固定为本次release版本号。所有客户端需要更新pom,修改对应api的版本。

3.dubbo服务化最佳实践

分包

建议将服务接口、服务模型、服务异常等均放在 API 包中,因为服务模型和异常也是 API 的一部分,这样做也符合分包原则:重用发布等价原则(REP),共同重用原则(CRP)。

如果需要,也可以考虑在 API 包中放置一份 Spring 的引用配置,这样使用方只需在 Spring 加载过程中引用此配置即可。配置建议放在模块的包目录下,以免冲突,如:com/alibaba/china/xxx/dubbo-reference.xml

粒度

服务接口尽可能大粒度,每个服务方法应代表一个功能,而不是某功能的一个步骤,否则将面临分布式事务问题,Dubbo 暂未提供分布式事务支持。

服务接口建议以业务场景为单位划分,并对相近业务做抽象,防止接口数量爆炸。

不建议使用过于抽象的通用接口,如:Map query(Map),这样的接口没有明确语义,会给后期维护带来不便。

版本

每个接口都应定义版本号,为后续不兼容升级提供可能,如: 

建议使用两位版本号,因为第三位版本号通常表示兼容升级,只有不兼容时才需要变更服务版本。

当不兼容时,先升级一半提供者为新版本,再将消费者全部升为新版本,然后将剩下的一半提供者升为新版本。

兼容性

服务接口增加方法,或服务模型增加字段,可向后兼容,删除方法或删除字段,将不兼容,枚举类型新增字段也不兼容,需通过变更版本号升级。

各协议的兼容性不同,参见:服务协议

枚举值

如果是完备集,可以用 Enum,比如:ENABLEDISABLE

如果是业务种类,以后明显会有类型增加,不建议用 Enum,可以用 String 代替。

如果是在返回值中用了 Enum,并新增了 Enum 值,建议先升级服务消费方,这样服务提供方不会返回新值。

如果是在传入参数中用了 Enum,并新增了 Enum 值,建议先升级服务提供方,这样服务消费方不会传入新值。

序列化

服务参数及返回值建议使用 POJO 对象,即通过 settergetter 方法表示属性的对象。

服务参数及返回值不建议使用接口,因为数据模型抽象的意义不大,并且序列化需要接口实现类的元信息,并不能起到隐藏实现的意图。

服务参数及返回值都必需是传值调用,而不能是传引用调用,消费方和提供方的参数或返回值引用并不是同一个,只是值相同,Dubbo 不支持引用远程对象。

异常

建议使用异常汇报错误,而不是返回错误码,异常信息能携带更多信息,并且语义更友好。

如果担心性能问题,在必要时,可以通过 override 掉异常类的 fillInStackTrace() 方法为空方法,使其不拷贝栈信息。

查询方法不建议抛出 checked 异常,否则调用方在查询时将过多的 try...catch,并且不能进行有效处理。

服务提供方不应将 DAO 或 SQL 等异常抛给消费方,应在服务实现中对消费方不关心的异常进行包装,否则可能出现消费方无法反序列化相应异常。

调用

不要只是因为是 Dubbo 调用,而把调用 try...catch 起来。try...catch 应该加上合适的回滚边界上。

Provider 端需要对输入参数进行校验。如有性能上的考虑,服务实现者可以考虑在 API 包上加上服务 Stub 类来完成检验。

 

六.微服务日志输出规范

(待续)

你可能感兴趣的:(架构)