1 创建微服务
1.1 定义服务
采用领域驱动设计原则
在领域驱动设计中,一个domain表示一个特定的知识领域或活动。一个model代表领域某个重要方面的抽象,用于理解领域的变化。这个模型用于构建解决方案,跨团队交流。
Bounded Context允许领域被分为多个独立的子系统。每个Bounded Context可以拥有自己的模型来表示Domain中的概念。
将领域元素转换为服务
应用和服务结构
单体架构,通常将应用分为客户端、Web、业务层、数据层,微服务架构也采用类似的方式进行划分,但是每层的元素由独立的服务提供,并且运行在独立的进程中,如上图2所示。
- BFF(backend for frontend)用于支持特定的前端特定的行为,例如优化设备或提供Web应用额外的能力
- 每个服务拥有和维护各自的数据存储层,数据存储也是独立的服务
然而,不是所有的微服务应用都需要BFF层,例如使用JSP或JSF等与前端Javascript相关的应用,可以看做是BFF层。而单页面应用,是一些静态资源,来自独立的服务,它们运行在浏览器中,直接调用后台API,这种方式是不需要BFF的。
微服务内部结构
微服务架构中每个服务的内部情况,如上图所示。调用外部服务应该与领域逻辑进行分离,也应该与后台数据服务进行分离。
图中包括以下几个元素:
- Resources 向外部客户端提供资源,这层对请求进行基本的验证,然后将信息传递到Domain逻辑层
- Domain logic,领域逻辑层,一般有很多形式。在Boundary-Entity-Control模式中,领域逻辑表示entity,参数校验,状态变化逻辑等
- Repository (可选),用于提供核心Domain Logic与数据存储之间的抽象。这个配置允许后台数据存储层的改变或替换不会影响Domain Logic层
-Service connectors 类似于Repository 抽象,封装了与其他服务之间的通讯。这层的功能是作为门面,保护Domain logic不会受外部资源API改变而产生影响。也提供API 格式向内部Domain model结构转换功能。
这个架构要求我们在创建类Classes时,需要遵守一些规则,例如每个用于实现服务的class需要执行至少一项任务:
1.执行Domain Logic
2.显示Resources
3.调用其他服务
4.调用数据存储层
一般而言,建议代码结构不需要严格按照上述进行创建。最重要的是,减少变化带来的风险。例如,如果我们需要改变数据存储(data store),我们可能需要更新Repository层。我们不需要搜索所有的类,来找到那些方法调用了数据存储层。如果外部API服务发生变化,我们可以通过改变service connector中来进行调整。
共享库或新建服务
根据DRY(Don’t repeat yourself)原则,重复的代码最好封装成公共的方法或库。在微服务架构中,由于服务是相互隔离的进程,这使得获取共享代码实现变得很困难,因而,如何处理共用的代码呢?
- 接受微服务架构中存在冗余的代码
- 将共用代码封装成共享的、版本控制的代码库
- 新建独立的服务
根据代码的本质,最好接受冗余的代码,因为,这样可以保持每个服务的独立演进,便于后续服务的更新这部分初始化共用的代码。
虽然将Data Transfer Objects创建为共享的库比较方便,并且这些DTO也是简单的类,通常用于生成JSON格式,但是这样会丢失对DTO实现细节,并且会增加耦合性。
客户端库最好避免代码冗余,最好让客户端库容易消费APIs。这个技术在数据存储中使用比较常见,包括NoSQL数据存储,提供基于REST的APIs。客户端库同样能为其他服务使用,特别是一些使用二进制的协议。然而,客户端库会引入服务消费与提供者的耦合性。这个服务可能会很难被平台消费,或者很难被没有库支持的语言消费。
共享库也可以用于确保复杂算法过程的一致性,
什么时候讲共享库创建为独立的服务呢?一个独立的服务,可以使用独立的语言开发,可以进行分离的扩展,可以快速的更新服务而不需要同步更新消费者。一个独立的服务也可以添加扩展的进程,从属于服务之间交流。
1.2 创建RESTAPIs
如何设计微服务的API呢? 在微服务架构中设计有效和可用的API至关重要,API需要提供文档说明,版本控制,便于消费者理解和使用这些API。
从上到下或从下到上
API的设计有两种思路,一种是先设计API然后开展代码实现,一种是先实现服务然后提取API。原则上,第一种方式较好。微服务不是独立进行操作的,服务可能调用其他的服务也可能被其他服务调用。服务之间的调用需要提交定义清除,并且形成文档。
API 文档
使用工具生成APIs说明文档,规范的API文档有利于精确和正确的使用这些API,也可以用于和API消费者讨论API。这些文档也可以用于消费者驱动的测试。
Open API Initiative (OAI)是一个标准化RESTful APIs描述的组织。OpenAPI 说明是基于Swagger的,它定义了表示RESTful API结构和元数据格式的创建方法。这中定义由一个可移动的文件描述(swagger.json,也有swagger.yml)。可以使用可视化编辑器或注解的方式在应用中定义swagger。在后续开发中,可以用于生成客户端和server stubs。
使用正确的HTTP动词
REST APIs应该使用标准的HTTP方法来表示创建、查询、更新、删除和操作,以及幂等操作。
- POST 表示资源的创建,POST不是幂等操作。例如,多次发起POST请求,每个请求会生成唯一的资源
- GET 表示查询资源,是幂等性操作,多次请求不会产生副作用。GET请求的查询参数不应该用于改变或更新信息。
- PUT 表示更新资源,PUT操作是幂等性的,PUT 操作的请求体,通常包含整个需要更新的资源信息。
- PATCH 表示更新部分资源,根据业务情况可以是幂等操作也可以不是。例如,如果一个PATCH操作,表示一个数值需要从A变到B,这种是幂等操作。多次请求也不会产生其他影响,当数值已经改成B了。
- DELETE 表示删除资源,删除操作是幂等性的,资源只能删除一次。第一次调用时。响应码为200表示操作成功,后续再调用时,响应码为204表示资源未找到。
创建机器友好、可描述的结果
一个具有表现力的REST API应用谨慎的考虑返回的结果。由于API的调用一般是使用软件而不是用户,因此,需要与注意调用者的交流尽可能的有效和高效。
例如,比较常见的实践,当我们使用200返回码来HTML解释错误信息。虽然,技术上没有什么问题,用户可以看到错误的页面信息,但是对于机器来说,会认为这次请求成功了。
HTTP状态码应该是有用的并且有相关性的。当操作正常时使用200(OK)。当没有内容返回时,使用204(NO CONTENT)。201(CREATED)应该用于POST请求,表明创建资源的结果,无论是否有响应体。当并发更新冲突时使用409(CONFLICT),当请求参数类型不正确时使用400(BAD REQUEST)。
资源URI和版本
RESTful 资源 URI有很多可选的地方。一般而言,资源应该以名词进行描述,而不是动词,节点应该以复数的形式表示。例如:
URI | 说明 |
POST /accounts | Create a new item |
GET /accounts | Retrieve a list of items |
GET /accounts/16 | Retrieve a specific item |
PUT /accounts/16 | Update a specific item |
PATCH /accounts/16 | Update a specific item |
DELETE /accounts/16 | Delete a specific item |
URI可以是层级嵌套的方式组织
版本控制
微服务最大的好处是,每个服务都可以独立开发演进。服务之间的调用保持独立的前提是,这些API不能轻易更改。
稳健性原则,服务作为发送方要保守一些,作为接收方可以随意一些,在API改变之前一直要保持这种原则。当仍然需要修改API时,可以选择构建一个不同的整个服务,并逐渐替换原始的服务,也许领域模型的演进和更好的抽象能让这件事情更有意义。
如果不需要改变现有的API服务,那么如何管理这些改变呢:
- 服务是否处理所有版本的API?
- 是否需要维护服务的版本来支持多个版本的API?
- 服务是否只支持最新版的API,并且依赖其他的适配层来转换和来自旧版的API?
我们很难抉择,最本质的问题是,我们如何映射API的版本。这有三种方式来处理REST资源的版本:
- 在URI中设置版本
- 使用自定义的版本请求头
- 将版本设置在HTTP的Accept请求头,并依赖内容协调
URI中添加版本信息
这种是最简单的方法来指定版本,有以下几个优点:
- 容易理解
- 容易实现
- 容易使用API浏览工具(例如 Swagger) 和 命令行工具(例如 curl)
如果决定采用URI中版本进行控制,那么版本号需要设置到整个应用上,例如 api/v1/accounts
,而不是api/accounts/v1
.
如果决定使用URI中版本,我们可以采用不同的方式进行管理,部分取决于请求在系统中如何路由。
当然也有反对这种版本控制方式,URL是HTML标准中一个严格解释的,一条URL应该代表一个实体并且如果实体不改变,那么URL也不改变。
其他也指出,将版本放到URI中需要消费者更新他们的URI引用。这种问题可以通过将旧版URI映射到新的请求中,然而,当最新的版本改变时,这样的方法会出现异常行为。
添加自定义的请求头
我们也可以添加自定义的请求头来指明API的版本。自定义的请求头可以被路由器或其他基础设施来路由到特定的后台实例。然而,这种机制不容易使用。此外,这种自定义的头只能在我们自己的应用中使用,也就是消费者需要学习如何使用。
修改Accept Header添加版本
Accept Header是一个比较明显的地方用于定义版本,但是很难测试。URIs 很容易指定并替换,但是指定HTTPHeader需要更多API的信息以及命令行调用。
设计API时,有以下几点建议:
1.从消费者的角度设计APIs
2.考虑API改变的策略
3.在整个应用中使用一致的版本控制技术