文章目录

  1. 1. 如何拆分服务
  2. 2. 论微服务的数据库管理
  3. 3. 服务多版本指南
  4. 4. 应对微服务的链式调用异常
  5. 5. 是否需要提供外观接口
  6. 6. 如何快速追踪与定位问题
  7. 7. 微服务的安全
  8. 8. 微服务的数据一致性

微服务架构越来越被重视与应用,然而在拥抱微服务的过程中,我们或多或少会遇到一些常见的问题。那么,本节将介绍几种常见的问题以及应对思路。

如何拆分服务

微服务要如何拆分,是否拆分粒度越小越好?一般情况下,对于服务的拆分并非越小越好,甚至极端的案例是把一块功能拆分成一个服务,这种做法是不对的。因此,拆分粒度应该保证微服务具有业务的独立性与完整性,服务的拆分围绕业务模块进行拆分。例如将 VR 资讯系统进行服务拆分,分为资讯系统、话题系统、日报系统、百科系统四个微服务系统。

但是,很多情况下,服务的拆分围绕业务模块进行拆分是一种理想状态下的拆分方法,换句话说,我们在架构设计之初就假定我们可以掌握一切。然而,不同的服务可能由不同的团队开发与维护,实际场景下,微服务的便利性更多的在于团队内部能够产生闭环,换句话说,团队内部可以易于开发与维护,便于沟通与协作,但是对于外部团队就存在很大的沟通成本与协作成本。现在,我们来看一个案例。团队 A 考虑到功能的复用性而开发了一个“互动组件”,其中包括 “评论模块”功能。此时,团队 B 并不知情也开发了一个类似的“互动组件”。而团队 C 也有这个需求,它知道团队 A 有这个“互动组件”,希望可以复用,但是由于这个“互动组件”在设计的时候更多地考虑了团队 A 的当前业务,没有很好的复用性,例如不支持“评论盖楼”功能,而由于团队 A 出于当前其他项目的进度原因无法马上提供支持,团队 B 评估后决定花一周时间自己开发一个符合自己业务需求的“互动组件”。此时,各个项目团队各自维护了一个“互动组件”。此外,我们再来看一个案例。一个 OA 系统拥有“用户管理”、“文件管理”、“公告管理”、“政策管理”、“公文管理”、“任务管理”、“审批管理”等功能,如果按照微服务架构思想可以围绕业务模块进行拆分,但是事实上这个 OA 系统的最终用户只有 30 多人,使用微服务架构可能有点“杀鸡用牛刀”的感觉了。回顾下,第一个案例中,由于团队之间的职责与边界导致了服务的复用存在局限性,甚至造成各自为战的局面,这种情况一般需要公司层面进行规划和统筹。第二案例中,由于用户量不大,系统也不复杂,使用微服务反而带来了不必要的设计和运维难度,同时也带来了一些技术的复杂度。此外,我们还需要考虑服务依赖,链式调用、数据一致性、分布式事务等问题。

总结下,服务的拆分是一个非常有学问的技术活,要围绕业务模块进行拆分,拆分粒度应该保证微服务具有业务的独立性与完整性,尽可能少的存在服务依赖,链式调用。但是,在实际开发过程中,有的时候单体架构更加适合当前的项目。实际上,微服务的设计并不是一蹴而就的,它是一个设计与反馈过程。因此,我们在设计之初可以将服务的粒度设计的大一些,并考虑其可扩展性,随着业务的发展,进行动态地拆分也是一个不错的选择。

论微服务的数据库管理

通常情况下,在每个服务都有自己的缓存和数据库,并且缓存和数据库是相互独立且透明的。因此,共享缓存与共享数据库是不对的。那如果服务 A 需要获取服务 B 的数据怎么办?请读者思考,这种情况下,可以直接在服务 A 创建两个数据源(一个是服务 A 的数据库,一个是服务 B 的数据库)进行数据操作么?事实上,这个方案是不提倡的,因为它破坏了微服务之间的数据独立性。因此,更好的做法是:服务 B 提供一个获取该数据的 API 接口,而服务 A 通过调用该接口进行业务组装。但是,凡事无绝对,有一种特殊的场景可能需要共享数据库,那就是旧的服务过度到新的服务的场景,新的服务复用旧的服务的数据库从而到达功能与数据过度的需求。

服务多版本指南

微服务的 API 接口应该尽量兼容之前的版本,换句话说,微服务可以支持多版本,但是需要尽量确保向下兼容。如果服务无法兼容旧版本,则需要升级版本号。为了解决这个版本不兼容问题,在设计 RESTful API 的一种实用的做法是使用版本号。一般情况下,我们会在 url 中保留版本号,并同时兼容多个版本。

【GET】  /v1/users/{user_id}  // 版本 v1 的查询用户列表的 API 接口
【GET】  /v2/users/{user_id}  // 版本 v2 的查询用户列表的 API 接口

在 Java 语言的 Spring 框架中实现,如下所示。

@RestController
@RequestMapping({"v1/c/users"})
public class SysUserV1Controller {
    @RequestMapping(value={"/{userId:\\d+}"}, method=RequestMethod.GET)
    public SysUser findOne(@PathVariable long userId){  
        // 业务实现
    }
}

@RestController
@RequestMapping({"v2/c/users"}) 
public class SysUserV2Controller {
    @RequestMapping(value={"/{userId:\\d+}"}, method=RequestMethod.GET)
    public SysUser findOne(@PathVariable long userId){  
        // 业务实现
    }
}

此时,客户端的产品的新功能将请求新的服务端的 API 接口地址。

虽然服务端会同时兼容多个版本,但是同时维护太多版本对于服务端而言是个不小的负担,因为服务端要维护多套代码。这种情况下,常见的做法不是维护所有的兼容版本,而是只维护最新的几个兼容版本,例如维护最新的三个兼容版本。在一段时间后,当绝大多数用户升级到较新的版本后,废弃一些使用量较少的服务端的老版本API 接口版本,并要求使用产品的非常旧的版本的用户强制升级。

此外,在微服务的多版本并存的场景下,这些服务的版本可以在同一个服务中定义,这种情况一般出现在服务的内容变更不是很大的场景。另一种方式,服务的版本可以在不同的服务中定义,例如“资讯v1服务”、“资讯v2服务”, 这种情况一般出现在服务框架重构中,它将抽离出一个新的工程来提供新的服务接口。

应对微服务的链式调用异常

一般情况下,每个微服务之间是独立的,如果某个服务宕机,只会影响到当前服务,而不会对整个业务系统产生影响。但是,服务端可能会在多个微服务之间产生一条链式调用,并把整合后的信息返回给客户端。在调用过程中,如果某个服务宕机或者网络不稳定可能造成整个请求失败。因此,为了应对微服务的链式调用异常,我们需要在设计微服务调用链时不宜过长,以免客户端长时间等待,以及中间环节出现错误造成整个请求失败。此外,可以
考虑使用消息队列进行业务解耦,并且使用缓存避免微服务的链式调用从而提高该接口的可用性。

是否需要提供外观接口

外观接口,指的是将多个服务的接口进行业务封装与整合并提供一个简单的调用接口给客户端使用。这种设计的好处在于,客户端不再需要知道那么多服务的接口,只需要调用这个外观接口即可。但是,坏处也是显而易见的,即增加了服务端的业务复杂度,接口性能不高,并且复用性不高。因此,笔者的建议是服务端的接口设计尽可能保证职责单一,而在客户端进行“乐高式”组装。如果存在 SEO 优化的产品,需要被类似于百度这样的搜索引擎收录,可以当首屏的时候,通过服务端渲染生成 HTML,使之让搜索引擎收录,若不是首屏的时候,可以通过客户端调用服务端 RESTful API 接口进行页面渲染。

如何快速追踪与定位问题

在微服务复杂的链式调用中,我们会比单体架构更难以追踪与定位问题。因此,在设计的时候,需要特别注意。一种比较好的方案是,当 RESTful API 接口出现非 2xx 的 HTTP 错误码响应时,采用全局的异常结构响应信息。其中,code 字段用来表示某类错误的错误码,在微服务中应该加上“{biz_name}/”前缀以便于定位错误发生在哪个业务系统上。我们来看一个案例,假设“用户中心”某个接口没有权限获取资源而出现错误,我们的业务系统可以响应“UC/AUTH_DENIED”,并且通过自动生成的 UUID 值的 request_id 字段,在日志系统中获得错误的详细信息。

HTTP/1.1 400 Bad Request
Content-Type: application/json
{
    "code": "INVALID_ARGUMENT",
    "message": "{error message}",
    "cause": "{cause message}",
    "request_id": "01234567-89ab-cdef-0123-456789abcdef",
    "host_id": "{server identity}",
    "server_time": "2014-01-01T12:00:00Z"
}

此外,我们需要在记录日志时,标记出错误来源以及错误详情便于更好地分析与定位问题。

微服务的安全

OAuth 是一个关于授权的开放网络标准,它允许第三方网站在用户授权的前提下访问用户在服务商那里存储的各种信息。实际上,OAuth 2.0 允许用户提供一个令牌给第三方网站,一个令牌对应一个特定的第三方网站,同时该令牌只能在特定的时间内访问特定的资源。用户在客户端使用用户名和密码在用户中心获得授权,然后客户端在访问应用是附上 Token 令牌。此时,应用接收到客户端的 Token 令牌到用户中心进行认证。

一般情况下,access token 会添加到 HTTP Header 的 Authorization 参数中使用,其中经常使用到的是 Bearer Token 与 Mac Token。其中,Bearer Token 适用于安全的网络下 API 授权。MAC Token 适用于不安全的网络下 API 授权。

微服务的数据一致性

如何保证多个微服务的数据的一致性是一个必须面对的问题。例如,“话题系统”的数据变更的同时要保证“用户动态系统”的数据级联更新。如果,这个时候“用户动态系统”发生宕机,或者网络连接异常、网络超时,就会导致数据的不一致。目前,分布式事务并没有很好的解决方案,难以满足数据强一致性,一般情况下,保证系统经过一段较短的时间的自我恢复和修正,数据最终达到一致。这个自我恢复和修正的方式,可以在每次更新的时候进行修复,或者采取周期性的进行校验操作来保证。

我们还可以引入可靠的消息队列,只要保证当前“话题系统”的可靠事件投递并且消息中间件确保事件传递至少一次,那么订阅这个事件的消费者(用户动态系统)保证事件能够在自己的业务内被消费即可。在消费者(用户动态系统)处理的过程中出现异常,可以将事件放入重试队列并根据具体的策略进行失败重试,如果多次重试失败可以写入错误日志并主动通知开发人员进行手工介入。注意的是,这个过程要保证可靠事件投递与避免重复消费,其中尤其重要是接口要保证幂等性,例如支付系统不能因为重复收到支付事件而导致多次支付。

此外,我们可以采用业务补偿等方式保证数据的一致性。

(完)

微信公众号

文章目录

  1. 1. 如何拆分服务
  2. 2. 论微服务的数据库管理
  3. 3. 服务多版本指南
  4. 4. 应对微服务的链式调用异常
  5. 5. 是否需要提供外观接口
  6. 6. 如何快速追踪与定位问题
  7. 7. 微服务的安全
  8. 8. 微服务的数据一致性