《企业级容器云架构开发指南》—2.2.2 微服务的特性
498
2022-05-29
2.4 如何从单体架构迁移到微服务
虽然微服务是近年很热门的架构选择,但什么时候该选择微服务架构是有一定前提的。
图2-27是马丁·福勒所绘制的,其中黑线指的是单体,灰线是微服务架构。实际上在最初各方面效能、效率、开发、迭代的成果都比较好,不过随着单体越来越臃肿,各方面效能降低,这时候微服务的优势才得以体现。任何时候都是单体优先,只有单体结构变得越来越庞大,效能降低,并满足以下4个条件的时候才考虑进行微服务化:
图2-27 单体架构和微服务架构在不同系统复杂度下不同的生产力
要有快速迭代的能力。
要有基本的监控。
要有快速的集成。
要有一个DevOps文化。
图2-27表明了复杂度和生产率拐点的存在,但并没有量化复杂度的拐点到底是多少,或者换种说法,系统或代码库的规模具体达到多大才适合开始进行微服务化的拆分。在一篇有趣的文章《程序员职业生涯中的 Norris 常数》中提到,大部分普通程序员成长生涯的瓶颈在两万行代码左右。每一个瓶颈点的突破意味着需要新的技能和技巧,微服务合适的拆分拐点可能就在两万行代码规模附近,而每个微服务的规模大小最好能控制在一个普通程序员的舒适维护区范围内。一个受过职业训练的普通程序员就像一个拿到驾照的司机,一般司机都能轻松驾驭100公里左右的时速,但很少有能轻松驾驭200公里或以上时速的司机,即使能够驾驶,风险也是很高的,而能开“喷气式飞机的飞行员级别”的程序员恐怕在大部分的团队里一个也没有。
另外一个实施前提是基础设施的自动化,把1个应用进程部署到1台主机,部署复杂度是 1×1 = 1,若应用规模需要部署200台主机,那么部署复杂度是1×200 = 200。把1个应用进程拆分成50个微服务进程,则部署复杂度变成了50×200 = 10 000,缺乏自动化设施,仅部署就是一件“疯狂”的事情。所以前面微服务的特征才有基础设施自动化,这与规模有关,也是因为其运维复杂度呈乘数级飙升,从开发之后的构建、测试、部署都需要一个高度自动化的环境来支撑,这样才能有效降低边际成本。
微服务化的原则是:要渐进改革,不要推倒重来。不要大规模重写代码,重写代码听起来很不错,但实际上充满了风险,最终可能会失败,就如马丁·福勒所说:“the only thing a Big Bang rewrite guarantees is a Big Bang!”
相反,应该采取逐步迁移单体式应用的策略,通过逐步生成微服务新应用,与旧的单体式应用集成,随着时间的推移,单体式应用在整个架构中的比例逐渐下降,直到消失或者成为微服务架构的一部分。
最好从一个新的需求开始微服务化,而这个需求又比较适合微服务化。例如,稽核模块和其他模块没有关联,通信比较简单,业务逻辑比较独立,在这个时候我们可把它做成一个微服务。除了新服务和传统应用,还要新增两个模块。一个是请求路由器,负责处理入口(http)请求,有点像之前提到的API网关。路由器将新功能请求发送给新开发的服务,而将传统请求仍发给单体式应用。另外一个是胶水代码(glue code),它将微服务和单体应用集成起来,微服务很少能独立存在,经常会访问单体应用的数据。胶水代码可能在单体应用或者微服务或者两者兼而有之,负责数据整合。微服务通过胶水代码从单体应用中读写数据。
微服务可以通过3种方式访问单体应用数据:
访问单体应用提供的远程API。
直接访问单体应用数据库。
自己维护一份从单体应用中同步的数据。
胶水代码也被称为容灾层(anti-corruption layer),这是因为胶水代码保护微服务全新域模型免受传统单体应用域模型的污染。胶水代码在这两种模型间提供翻译功能。开发容灾层可能不是很重要,但却是避免单体式泥潭的必要部分。
将新功能以轻量级微服务方式实现有很多优点,如可以阻止单体应用变得更加无法管理。微服务本身可以开发、部署和独立扩展。采用微服务架构会给开发者带来不同的切身感受。然而,这种方法并不解决任何单体式本身问题,为了解决单体式本身问题必须深入单体应用做出改变。下面我们来看看这么做的策略。
(1)排序模块转成微服务
一个巨大的复杂单体应用由上百个模块构成,每个都是被抽取对象。决定第一个被抽取模块一般极具挑战,最好是从最容易抽取的模块开始,这会让开发者积累足够的经验,这些经验可以为后续模块化工作带来巨大的好处。转换模块成为微服务一般很耗费时间,可以根据获益程度来排序,一般从经常变化的模块开始会获益最大。一旦转换一个模块为微服务,就可以将其开发部署成独立模块,从而加速开发进程。
将资源消耗大户先抽取出来也是排序标准之一。例如,将内存数据库抽取出来成为一个微服务会非常有用,可以将其部署在大内存主机上。同样地,将对计算资源很敏感的算法应用抽取出来也是非常有益的,这种服务可以被部署在有很多CPU的主机上。通过将资源消耗模块转换成微服务,可以使得应用易于扩展。
查找现有粗粒度边界来决定哪个模块应该被抽取,也是很有益的,这使得移植工作更容易和简单。例如,只与其他应用异步消息同步的模块就是一个明显的边界,可以很简单、很容易地将其转换为微服务。
(2)从哪里开始拆分:接缝
从接缝处可以抽取相对独立的一部分代码,对这部分代码的修改不会影响系统的其他部分,这些接缝就可以作为服务的边界。那么如何识别出接缝呢?我们可以使用前面所提到的限界上下文,可以通过程序中的命名空间,也可以通过工具来帮助我们,如利用Structure 101这样的工具来可视化包之间的依赖。
(3)如何抽取模块
抽取模块的第一步就是定义好模块和单体应用之间的粗粒度接口,由于单体应用需要微服务的数据,反之亦然,因此这更像是一个双向API。因为必须在负责依赖关系和细粒度接口模式之间做好平衡,因此开发这种API具有挑战性,尤其对使用域模型模式的业务逻辑层来说更具有挑战。因此经常需要改变代码来解决依赖性问题,一旦完成粗粒度接口,也就将此模块转换成独立微服务了。为了实现,必须写代码使得单体应用和微服务之间通过使用进程间通信(IPC)机制的API来交换信息。第二步迁移就是将模块转换成独立服务。内部和外部接口都使用基于IPC机制的代码,抽取完模块,也就可以开发、部署和扩展另外一个服务了,此服务独立于单体应用和其他服务。
可以从头写代码实现服务,在这种情况下,将服务和单体应用进行整合的API代码成为容灾层,在两种域模型之间进行翻译工作。每抽取一个服务,就朝着微服务方向前进一步。随着时间的推移,单体应用将会越来越简单,用户就可以增加更多独立的微服务了。
(4)杂乱依赖的根源:数据库
为什么这么说呢?因为在通常情况下,我们在业务层的代码已经通过分层组织到相应的包中了,但是只有数据库是共用的,数据库对所有的代码都允许访问,是一个巨大的API。
对于同一张表被多个限界上下文使用的场景,我们应该如何处理?以下是一些处理的步骤和原则:
1)分清代码中对数据库进行读写的部分。我们需要厘清代码是如何访问数据库的,在什么地方读,在什么地方写,它们分别位于什么样的上下文中。
2)打破外键关系。对于表与表之间的外键关系,如果这两张表需要被拆分至两个微服务中,我们可能需要放弃外键关系,同时把这个约束关系放到代码中实现,可能还需要实现跨服务的一致性检查,或者实现周期性触发清理数据的任务。我们可以通过类似于SchemeSpy这样的工具来分析数据库表之间的依赖关系。
3)共享静态数据。例如,国家、部门之类的数据都是各个微服务之间经常使用的,这些数据的特征是不会经常变化,而且通用性高。这些数据在微服务划分之后该如何处理呢?
方法一:我们可以为每个微服务复制一份这样的数据,但这会导致数据的一致性问题。
方法二:把共享的数据放入代码之中,如放在属性文件中,或者简单地放在一个枚举中,但数据一致性问题仍然存在。
方法三:把这些静态数据放在一个单独的服务中。
4)共享数据。如果不同的微服务都使用了同一张表,在这种情况下该如何分享?其实这种情况很常见:领域概念不是在代码中建模,相反是在数据库中隐式地进行建模。这里缺失的领域概念是客户,因而我们需要提供一个新的服务。
5)共享表。与共享数据不同的是,不同的微服务也会使用同一张表,但两者修改的部分不一样,在这样的情况下,我们可以把这张表拆分成两张表,分别供两个微服务使用。
6)实施拆分。通常,我们推荐先分离数据库结构,然后对代码进行拆分的方法。表结构分离之后,对于原先的某个动作而言,对数据库的访问次数可能会变多。这也是我们需要考虑的问题,这里涉及分布式事务的相关问题。
另外,先拆分数据库但不分离代码的好处在于,可以随时选择回退这些修改或是继续,而不影响服务的任何消费者。
总之,将现有应用迁移成微服务架构的现代化应用,不应该通过从头重写代码的方式实现;相反,应该通过逐步迁移的方式实现。在拆分的同时,需要同期配置服务治理平台,完成服务发现、配置管理、日志管理、监控等内容。平台和应用是可以考虑分开的,由团队专门负责。
OpenStack 云计算
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。