75% 新项目都可以“无脑”选择单体架构( 二 )


 
另一种解决方案则是负载均衡器 。这时候我们无需直接与服务对话,而是使用中间层、即另一项服务(负载均衡器)来维持各实例的可访问性并代理通信负载 。例如,大家可以将其理解成类似于 DNS 负载均衡的功能,或者是像 AWS ALB 那种负载均衡器服务 。
 
所以很明显,当我们使用 HTTP 进行服务间通信时,就必须通过位置解析才能正确处理服务实例 。这可是有成本的哦,毕竟天底下没有免费的午餐 。
 
如果会话服务可以通过消息进行访问,那在本文的示例中我们就要用到 Kafka,而这又会让问题跃上新的层次 。具体为何,容我细细道来 。
 
理由很简单,因为在使用 Kafka 时,我们可以做到有多少主题分区、就启动多少服务实例 。假定我们有一个 session-service-topic 主题,这个主题明显需要由会话服务进行读取 。Kafka 主题的扩展取决于主题分区的数量,每个分区对应一个消费方 。如果我们希望将会话服务扩展至 4 个实例,那就需要建立 4 个分区来存储各实例所需读取的主题 。
 
这有什么问题吗?我们可以随时增加分区数量呀 。没错,但请大家注意,消息的顺序只能存留在主题分区之内,这可是有很大影响的 。
 
【75% 新项目都可以“无脑”选择单体架构】我们假设会话服务需要处理两种消息类型:
 

  • UserLoggedInEvent
  • UserActivityEvent
 
在收到 UserLoggedInEvent 之后,会话服务将创建一个内部“会话”,这可能涉及相关的数据库表之类 。而在收到 UserActivityEvent 之后,会话服务则须更新现有用户会话的过期时间,这可能涉及相关的数据库条目 。
 
问题在于,假定我们有 2 个会话服务实例,而每个主题又对应 2 个用于消息发送的分区 。主题生产方选择使用循环分区策略,意味着一条消息进入第一分区、接下来第二条消息进入第二分区 。这时候,就会有一项服务接收到 UserLoggedInEvent,而另一项服务则接收到 UserActivityEvent 。
 
在这种情况下,接收 UserActivityEvent 的服务在处理速度上可能快于接收 UserLoggedInEvent 的服务;如果双方共享同一数据库,就有可能引发问题 。因为初始会话记录还没有被相应的服务实例正确写入至数据库 。
75% 新项目都可以“无脑”选择单体架构

文章插图
 
听起来问题不大,但实际应用起来可是相当复杂 。这种情况当然也有解决方案,但它本来可以不必存在,只是因为我们选择了微服务架构、就必须多承担调试压力,有点不划算 。
 
我也见过很多比较复杂的系统,由于很难明确该如何进行数据分区、如何解决排序问题,所以几乎无法使用 Kafka 实现横向扩展 。另外,也有一些系统设计者为了避免这类问题,而选择使用单一 Kafka 主题实现所有服务间通信 。这种方法虽然回避了分区的困扰,但也彻底牺牲掉了扩展的可能性 。而且在大部分场景下,HTTP 其实就完全够用了,着实没必要搞那么麻烦 。
 
我想强调的一点是:千万别以为微服务的横向扩展能力是默认的、“免费的” 。如果不开动脑筋,这种扩展能力根本就实现不了 。不知道大家有没有尝试过在微服务环境下调试排序问题,却发现问题只发生在常规开发/测试环境中,却没法在本地计算机上重现 。这真的很让人头大,不提了 。
技术自由终于进入了我最喜爱的环节 。这是种幸运、也是种不幸,我本人对技术自由这事有着深切的体会 。有时候问题的根源并不是无奈的意外,而是……开发者们实在太有创意了 。
 
所以大家会天然更喜欢微服务架构 。毕竟在单体式架构中,我们总要被编程语言和技术栈的条条框框所束缚,但在微服务里却可以使用不同的编程语言和技术栈编写不同服务 。比方说,我们可以在某一服务中使用 JAVA,在另一服务中使用 Node.js,在第三项服务中使用 Go 等等,完全没有问题 。
 
但这种优势,有时候反而成为最大的弊端 。我当然不反对创新,但我理解的创新是用前所未有的方法解决问题、而不是用前所未有的方法创造问题 。
 
大家可能觉得异构微服务架构没什么问题,但前提是你得有明确的职能划分,保证由专人专门管理特定微服务项目 。只有这样,我们才真正能说“没什么问题” 。


推荐阅读