Django 微服务设计指南(二)
五、从单体到微服务
现在,我们已经了解了我们在服务中的目标是什么,以及我们如何将它们相互联系起来,是时候让我们更仔细地了解实际技术了,这将有助于我们从单一应用迁移到微服务架构。请注意,这里描述的技术不是灵丹妙药,您肯定需要根据自己的使用情况对它们进行修改,但是,经验表明,这些通用方法为成功的迁移提供了一个极好的起点。请记住,本章中描述的一些步骤是并行的,所以如果有更多的人来帮助你,那么可以加快一点速度。
开始之前
到目前为止,您可能已经理解了将您的单片系统迁移到微服务并不只是在公园里散步。将会有严重的人力和财力成本。甚至估计交付给你的涉众也可能是一个巨大的困难(至少在开始的时候),所以让我们来看看你需要计算的基本成本。
人力成本
自然,我们谈论的主要是重构代码库的成本。在项目的早期阶段,您将需要比迁移多个组件时多得多的努力。一开始对你的估计要非常保守,在你准备好工具之后,对你自己和你的团队要更严格一点,我们将在第五章和稍后的第六章中讨论。
根据我的经验,有两个领域的迁移可能会非常困难,并且可能会显著增加您的迁移的编码成本:
-
运营相关——当迁移到一种新的架构类型时,如何部署和扩展您的新服务始终是一个关键且有分歧的问题。monolith 的部署可能是一个人运行几个脚本将数据同步到远程服务器,然后重新启动应用,监控(当您如此大规模地改变您的基础架构时,这是一个非常重要的方面)可能是在同一条船上。当您转向微服务时,从长远来看,这可能不会奏效。至少,您需要收集这些可执行文件,并以一种可用的方式组织它们,以便公司中的其他人也可以访问它们。我们将在下一章讨论更多与操作相关的主题。
-
代码相关——你知道什么时候代码像一碗意大利面条一样凌乱,而不是像一片切片的比萨饼一样整齐有序吗?在一个不断推进交付的高速环境中,保持代码库的整洁是一个巨大的挑战。当您想要迁移时,混乱的代码可能是另一个巨大的成本。
根据您的公司和单一应用的规模,最好有一个专门的团队来为其他团队处理工具、文档、指南和最佳实践,这些团队拥有组件迁移的领域知识。如果你和数百名工程师一起操作数百万行代码,这几乎是必须的。如果你的规模稍小,这可能是一个方便。
当一个关键组件由于弹性或其他问题需要迁移时,一些公司喜欢实施应急或“老虎”团队。这可能是将大量软件转移到不同系统的好方法,但是,强烈建议关注代码的移交,并在迁移和维护团队之间实施密集的知识共享会议。
现在,让我们看看我们需要实施的硬件和基础架构的成本。
基础设施成本
转移到微服务可能会有另一个昂贵的影响,即拥有足够的机器来运行新系统(以及一段时间内旧系统)的成本。这到底是什么意思?让我们考虑以下场景:
我们的 tizza 应用运行在两个 10 核机器上,内存为 128。在迁移规划期间,我们已经确定了 6 个系统,我们可以在逻辑上将应用分成 6 个系统。现在,让我们来计算一下:
根据系统的负载,我们需要单核或双核机器来提供新服务。处理认证等的系统可能需要两个内核和 8gb 的 ram,而披萨元数据存储可能只需要一个内核和 4gb 的 RAM。我们可以将整个集群的 CPU 数量平均为 8,总内存成本为 32gb。因为,我们曾经用 2 台机器来处理 monolith,我们也应该提高这里的数字,我们毕竟不想降低弹性。
当您试图将您的系统缩减为更小但更高效的部分时,缩小您的集群规模并低估安全运行您的软件所需的原始功率是一种非常人性化的反应。在创建新的微服务器时,我喜欢遵循的一般经验法则是在不同的(虚拟或物理)机器上运行服务的 3 个副本,以实现高可用性。
对于自信的人来说,有了优秀的云提供商、超轻量级的应用和配置良好的自动伸缩系统,就可以消除上述说法。
注意
自动扩展是指定义关于您希望在集群中运行的服务器数量的规则。该规则可以是内存或 CPU 使用量、集群的实时连接数、一天中的时间或云提供商可能允许您使用的其他值的函数。
正如您所看到的,我们将系统中的内核总数从 20 个增加到了 24 个,内存保持在 128 左右,总计为 96 个。您将很快注意到,在现实生活环境中,这些数字往往会比预期增长得更快,并且取决于您的提供商,这可能会给您的业务带来毁灭性的成本。
我的建议是,为了安全起见,在开始的时候过犹不及,并不时地重新审视你的应用,以确保硬件不会对软件造成过度破坏。
我打了电话,接下来呢?
到目前为止,在阅读本书时,你脑海中出现的最大问题可能是如何说服你的公司,这对他们来说是一项值得的投资,而不仅仅是一个有趣的重构。这是一场旷日持久的辩论,没有灵丹妙药。我会试着给你一些建议,这样你就可以开始了:
-
作为一等项目公民的技术债务:通常人们认为这种类型的变化将需要大项目,其中多人以宏大的规模合作。从本章你会看到,事实并非如此。我能给出的第一个建议是将技术债务和重构转移到你需要交付给公司的特性项目中。确保你在这件事上是透明和合理的,这样你将来也可以这样做。此外,如果你收到一个“不”,那也没关系,只是要坚持下去,这都是关于谈话的。将与技术债务相关的任务放入特性项目中,可以使工程师在这两个方面都更有效率。上下文切换更少,使得特性开发和技术债务工作更有效。工程师们也会更开心,因为他们会把更高质量的工作抛在身后。
-
衡量你的结果:如果你能够在这里或那里进行一些重构,向你的同事展示使用你的新数据库界面有多容易,或者用你提取的功能交付新功能有多快,并确保告诉你的产品所有者或经理。如果你有指标来证明你的工作是值得的,那就更好了。这些指标通常很难找到和提出,其中一些可能与应用速度有关,一些可能与交付速度有关(例如,由于我们在服务中进行了这种和这种技术更改,新功能发布的速度有多快),甚至是发给您团队的 bug 单的数量。
-
坦诚:确保你衡量了成本并向所有利益相关者解释了成本,而且你诚实地做到了。这将是一个大项目,没有必要让它看起来很小。人们确实需要理解,特性开发将会放缓一段时间,并且将来围绕它的过程会有所不同。
-
有时候没有也没关系:完全有可能你的公司还没有准备好像这样的大规模迁移。在这种情况下,确保你和你的公司尽可能成功,这样你就可以在不久的将来遇到弹性问题。在介绍性章节中,我们已经看到了流应用的灾难场景。像这样的冲击可以导致一家公司改变他们的思维模式,然而,你需要首先在业务上达到那个规模。如果你收到太多的“不”,那么可能是时候重新思考你自己的范围了,把它缩小到你能做到的最小范围,向公司展示这个过程是什么样的,它的价值是什么。
正如你所看到的,从很多方面来说,打这样的电话对一家公司来说是非常困难的。最好的策略通常是保持耐心,把你的挫折抛在脑后,使用你从本书中学到的重构技术和工具,它们在未来会派上用场。
既然我们已经了解了任务的成本,那么是时候开始迁移我们的应用了,首先,通过准备数据。
数据准备
在我们进入重构应用的有趣部分之前,我们需要确保我们想要传输的数据是可传输的,这意味着它很容易从一个地方复制到另一个地方,并且不会与系统中的其他数据域耦合太多。除此之外,我们需要找到从领域和业务角度看似乎生活在一起的数据集群。
域分片
如您所见,我们可以识别出以下共存的数据块:
-
用户相关信息
-
披萨和餐厅信息
-
喜欢和匹配组
-
事件和事件相关集成
领域规定了上述内容,然而,在上述内容的某些部分之间仍然有许多硬耦合。让我们来看看比萨店的模式:
class Pizzeria(models.Model):
owner = models.ForeignKey(UserProfile, on_delete=models.CASCADE)
address = models.CharField(max_length=512)
phone = models.CharField(max_length=40)
如您所见,我们在用户配置文件模型的 owner 字段上有一个硬外键规则。您需要做的第一件事是确保这些外键将指向虚拟对象,其中外部对象可以被认为是这样的引用:
class Pizzeria(models.Model):
owner_user_profile_id = models.PositiveIntegerField()
address = models.CharField(max_length=512)
phone = models.CharField(max_length=40)
为什么这是有益的?现在,对象之间的耦合性降低了,并且更加基于信任。比萨饼将信任系统,有一个用户具有给定的用户配置文件 id,并可以生活在他们自己的独立环境中。不再有硬编码的数据库规则将比萨饼和用户资料绑定在一起,这非常解放,但同时也非常可怕。
我们失去了什么?
-
级联删除不见了。您需要手动删除链接的对象。
-
Django ORM 提供的一些方便的方法,比如 select_related,不再可用。
自然,您可以(也应该)保持驻留在同一个数据库中的模型之间的耦合,这样您就保留了方便的方法,并为您的查询提供了一些速度和可靠性。
如果您不是数据库专家,这似乎是一项艰巨的任务。然而,你可能记得我们在第二章中了解到的一个强大的工具,叫做迁移。您可以非常容易地创建一个新的迁移,用标识符替换外键。清单 5-1 为比萨店提供了一个范例。
def set_defaults(apps, schema_editor):
Pizzeria = apps.get_model('pizza', 'pizzeria')
for pizzeria in Pizzeria.objects.all().iterator():
pizzeria.owner_user_profile_id = pizzeria.owner.id
pizzeria.save()
def reverse(apps, schema_editor):
pass
class Migration(migrations.Migration):
dependencies = [
('pizza', '0002_pizzeria'),
]
operations = [
migrations.AddField(
model_name='pizzeria',
name='owner_user_profile_id',
field=models.PositiveIntegerField(null=True),
preserve_default=False,
),
migrations.RunPython(set_defaults, reverse),
migrations.AlterField(
model_name='pizzeria',
name='owner_user_profile_id',
field=models.PositiveIntegerField(),
),
migrations.RemoveField(
model_name='pizzeria',
name='owner',
),
]
Listing 5-1Example migration from model to id
让我们仔细看看这段代码。在我们更改了模型并运行了 makemigrations 命令后,系统会提示我们为我们创建的新字段提供一个默认值,这里我们可以给 0,这不会有太大影响。为了确保所有的值都设置正确,我们将以上述方式修改迁移代码。逻辑如下:
-
我们向表中添加一个名为 owner_user_profile_id 的新字段。我们将它设置为可空,因此迁移可以毫无问题地创建它。
-
我们运行一组 Python 代码,这些代码将相应地为我们设置值:
-
set_defaults 函数从已经创建的 pizzerias 中获取所有值,并将它们添加到新字段中。正是我们需要的。
-
如果我们真的需要,我们可以为这个函数指定一个反函数。现在不需要它。
-
-
我们将 owner_user_profile_id 字段改为不可空。
-
我们将永久删除所有者字段。
你可以使用上面的模板来迁移几乎所有的文件。对于行数较多的表(即将整个数据库加载到内存中是很危险的),强烈建议将 set_defaults 函数中的查询改为批量操作。或者,对于非常大的表(我们这里讨论的是数百万个业务关键行),您可能希望让数据库专家来帮助迁移。
您可能会有这样的预感,如果您运行这个迁移,一切都会崩溃。嗯,这完全是真的。所有 pizzeria 对象上的 owner 字段将从那里开始破坏您代码库中的代码,这可能会引起一些麻烦。理想情况下,您将更改代码库中的所有代码,以使用为获取所有者对象而创建的新字段,然而,有一些方法可以保护我们不被破坏,例如使用 Python 属性,请参见下面的清单 5-2 。
class Pizzeria(models.Model):
owner_user_profile_id = models.PositiveIntegerField()
address = models.CharField(max_length=512)
phone = models.CharField(max_length=40)
@property
def owner(self):
return UserProfile.objects.get(id=self.owner_user_profile_id)
Listing 5-2Using properties as model fields
以上述方式使用属性可以大大加快迁移过程,但是,从长远来看,它可能会导致问题,特别是性能方面的问题,因为我们刚刚从一个非常高效的数据库连接操作转移到另一个要执行的查询。但是,您稍后会注意到,这并不是我们将获得的最大速度提升。让我们看一下迁移的后续步骤,我们将确保新旧系统都可以访问数据。
数据库复制
在决定了要迁移应用的哪一部分并相应地修改了数据库之后,就该在数据库级别上建立迁移计划了。也就是说,是时候准备您的新数据库来托管您的模型了。
也许最简单的开始方法是建立主数据库的副本。我们的想法是将所有写入内容拷贝到复制副本,该复制副本将用作只读。不要太担心只为特定的表设置复制,大多数时候这只会带来麻烦和额外的工作。通常更简单的方法是设置一个完整的复制,并在迁移准备就绪时,从新数据库中删除不需要的表。
注意
您还可以在两个数据库之间设置主-主复制,但是,这种技术需要大量的数据库专业知识,并且为发布后的错误提供了更多的空间。
根据数据库的大小和类型,复制可能需要几分钟到几天的时间,因此在与您的直线经理和团队通信时,请确保将这一点添加到您的估计中。首先,您可能想看看 Amazon RDS 是如何进行数据复制的。如果你想更深入地了解这项技术,dev.mysql.com 网站上有关于如何为 MySQL 设置复制的很好的文档,Postgres 维基百科上有关于 Postgres 的文档。
测试和覆盖范围
我们已经做了一些准备。现在是时候复制所有代码了…开个玩笑。理想情况下,这是您确保在将代码从一个系统迁移到另一个系统时应用不会中断的地方。
要做到这一点,你可以使用的最有用的工具就是测试。Django 自带内置的测试框架,你可以很容易地测试数据库级别的测试,包括内存数据库,然而,任何单元测试框架都可以完成这项工作,比如单元测试 2 、 pytest 或 nose 。
当谈到如何衡量你在测试方面做得好不好时,许多团队和工程师推荐使用像 coverage 这样的工具,用它你可以衡量你在应用中测试过的代码行数。然而,这个度量并不总是测量您的测试的真实价值。建议您覆盖视图和模型的核心业务功能。理想情况下,如果您在运行后端应用时暴露了一些外部通信方法,那么您还可以实现集成测试,测试整个端点或消费者提供的功能。如果你有人员,那么你也可以实施验收测试,这通常是非常高水平的测试,自动机器人点击通过你的网站,检查基本用户流是否成功。这些系统通常非常脆弱,维护起来也很昂贵,但是,在一个关键的 bug 投入生产之前,它们可以作为最后一道防线拯救生命。cucumber 是一个优秀的验收测试框架,你可以在 cucumber.io 上了解更多。
既然我们已经用测试覆盖了代码,是时候开始使用一些工具了,这样我们就可以将代码库从一个地方迁移到另一个地方。
移动服务
到目前为止,我们对想要迁移的模型做了一些工作,并准备了一个新的数据库。是时候开始实际迁移代码了。
远程模型
在我们能够复制想要在独立系统中运行的代码库部分之前,我们需要确保两个代码库之间的依赖关系是可管理的。到目前为止,我们已经了解到 Django 和 Python 是构建和维护服务的非常灵活的工具,然而,我们也了解到对模型形式的数据有很大的依赖性。考虑清单 5-3 中的代码片段,我们希望将它迁移到一个单独的服务中:
from pizza.models import Like
from user.models import UserProfile
def get_fullname_and_like_count(user_profile_id):
user_profile = UserProfile.objects.get(id=user_profile_id)
full_name = user_profile.first_name + ' ' + user_profile.last_name
likes = Likes.objects.count()
return full_name, likes
Listing 5-3Problematic function to extract
无论我们想从哪个服务中提取上面的代码,我们都会面临一个两难的境地。函数中存在对模型的交叉引用,这可能很难解决。如果我们想避免数据重复和清理领域,我们需要确保喜欢和用户配置文件不驻留在单独的服务和数据库中。为此,我们可以做一个重构技术,我们称之为远程模型。
远程模型是我在职业生涯中多次遇到的一个概念,它们是真正的救星。这个想法是,如果你的 API 是统一的,你可以很容易地用远程调用替换你的数据库模型调用,在你的代码库中使用简单的搜索和替换(至少在大多数情况下)。参见清单 5-4 中的远程模型实现示例。
注意
我们将看到的代码可能不完全符合您的需求,但它是一个很好的起点,可以让您开始用远程模型思考问题。
import requests
import urllib.parse
from settings import ENTITY_BASE_URL_MAP
class RemoteModel:
def __init__(self, request, entity, version):
self.request = request
self.entity = entity
self.version = version
self.url = f'{ENTITY_BASE_URL_MAP.get(entity)}/api/{version}/{entity}'
def _headers(self, override_headers=None):
base_headers = {'content-type': 'application/json'}
override_headers = override_headers or {}
return {
**request.META,
**base_headers,
**override_headers,
}
def _cookies(self, override_cookies=None):
override_cookies = override_cookies or {}
return {
**self.request.COOKIES,
**override_cookies,
}
def get(self, entity_id):
return requests.get(
f'{self.url}/{entity_id}',
headers=self._headers(),
cookies=self._cookies())
def filter(self, **conditions):
params = f'?{urllib.parse.urlencode(conditions)}' if conditions else "
return requests.get(
f'{self.url}/{params}',
headers=self._headers(),
cookies=self._cookies())
def delete(self, entity_id):
return requests.delete(
f'{self.url}/{entity_id}',
headers=self._headers(),
cookies=self._cookies())
def create(self, entity_id, entity_data):
return requests.put(
f'{self.url}/',
data=json.dumps(entity_data),
headers=self._headers(),
cookies=self._cookies())
def update(self, entity_id, entity_data):
return requests.post(
f'{self.url}/{entity_id}'
data=json.dumps(entity_data),
headers=self._headers(),
cookies=self._cookies())
Listing 5-4The basic remote model
代码太多了。让我们仔细看看。您可能注意到的第一件事是,remote modelclass’接口公开了 Django 模型和标准的混合,这是我们在探索 REST 框架的过程中建立的。为了简单的重构和熟悉领域,get、filter、delete、create、update 方法公开了一个类似 Django 模型的接口,然而,实现本身包含了很多我们在研究 REST 范例时遇到的词汇。
ENTITY_BASE_URL_MAP 是一个方便的映射,您可以在设置文件中创建它来为您正在处理的每个实体指定唯一的 URL 基础。
到目前为止,所有这些都很简单。那么诀窍在哪里呢?您可能已经注意到,在创建远程模型的实例时,请求对象是一个必需的参数。这是为什么?简单地说,我们使用请求对象来传播我们在请求本身中收到的头。这样,如果您使用头或 cookies 进行身份验证,所有内容都将顺利传播。
在此之后,这些模型的使用应该是相当容易的。为了方便起见,您可以根据您的特定需求对 RemoteModel 进行子类化,就像我们在清单 5-5 中所做的那样:
class RemotePizza(RemoteModel):
def __init__(self, request):
super().__init__(request, 'pizza', 'v1')
Listing 5-5Simple remote pizza
然后,您可以在视图函数中执行以下操作,如清单 5-6 所示:
pizza = RemotePizza(request).get(1)
pizzas = RemotePizza(request).filter(title__startswith='Marg')
RemotePizza(request).delete(1)
Listing 5-6Examples of remote pizza usage
注意
过滤器函数需要在服务器端进行额外的实现,因为 Django REST 框架默认不支持它们。
远程模型的缺点:
-
远程模型可能会很慢——取决于网络、实现、硬件,有时还取决于星的排列,远程模型可能会比它们的数据库对应物慢得多。当您开始在您的体系结构上“链接”远程方法时,通过调用调用其他系统的其他系统的系统,这种缓慢也会升级。
-
它更脆弱——一般来说,远程模型比常规模型脆弱得多。与数据库的连接比通过 HTTP 进行的连接更加健壮和持久。
-
需要彻底检查批量操作和循环——有时在迁移过程中会复制不理想的代码,比方说,通过模型调用数据库的 for 循环变成了通过远程模型的 HTTP 调用。由于第一点,如果我们查询大量的模型,这可能是毁灭性的。
-
没有序列化——如果您使用这个简单的模型,您肯定会失去序列化的能力,这意味着您只会收到一个作为响应返回的 dict,而不一定是您所期望的一个模型或多个模型。这不是一个无法解决的问题,你可以研究一下 Python dataclass es 和类似英安岩的模块。
远程模型实现过程中出现的另一个好话题是缓存。缓存是一个很难解决的问题,所以我建议您不要在第一次迭代中实现它。我多年来注意到的一个简单而巨大的成功是在您的服务中实现请求级缓存。这意味着,每个远程调用的结果都以某种方式存储在请求中,不需要再次从远程服务中获取。这允许您在一个视图函数中从您的服务对同一个资源进行多个远程模型调用,而不实际使用网络来获取资源。这可以节省大量的网络流量,即使在开始。
让我们看一下练习 5-1、5-2 和 5-3,它们将帮助我们更多地使用远程模型。
练习 5-1:服务对服务的远程模型
上面的模型很好地解决了头和 cookie 传播的问题,因此我们可以使用认证方法(如我们在前面章节中看到的会话或认证头)从系统的各个点访问数据。然而,如果我们想在没有用户认证的情况下调用服务时使用不同的令牌,这可能会导致问题。在本练习中,鼓励您为 RemoteModel 设计一个扩展,通过它我们可以正确地分配覆盖身份验证令牌。上面已经有一些代码可供您使用。
练习 5-2:远程或非远程
远程模型看起来已经是一个非常强大的工具了,但是我们能让它们更加强大吗?当模型在本地数据库中可用时,尝试扩展 RemoteModel 类,以便能够处理数据库调用。进行这一更改可以让您在未来加快迁移速度。
练习 5-3:请求级缓存
我们之前提到过请求级缓存,现在是时候测试我们的 Python 和 Django 知识并实现它了。每次调用远程模型动作时,确保将响应存储在与请求本身相关的缓存中。为此,您可以使用各种缓存库,如 cachetools。
在我们的工具上工作是非常有趣的,是代码迁移的时间。
代码迁移
这可能是整个迁移过程中最不激动人心的部分。您需要复制您希望其他系统拥有的代码库。您需要为这些应用创建一个新的 Django 项目,找到设置和实用程序,并复制所有内容。当我处于迁移的这个阶段时,我喜欢遵循以下几个提示:
-
保持简单——在这一点上,不需要太担心服务之间的代码重复(除非您已经有了一些工具来实现这一点)。只要确保您的应用尽快启动并运行即可。无论如何,我们都要删除 monolith 中的代码。
-
遵循领域——就像数据分片一样,领域在这里也是关键。确保您想要移出的模块尽可能与系统隔离。你想要的目标,只是把一个应用从一个代码库复制到另一个代码库。
-
测试是关键——您创建的一些微服务本身就是怪物。例如,您可能有一个支付服务,它有一个内部状态机和对各种支付提供商的多个集成,您已经决定将整个域提取出来。确保您的测试在适当的位置,运行并最终不会中断。手工测试如此庞大的系统几乎是不可能的。如果您遗漏了一些代码或功能,测试也可以帮助您进行迁移。
-
接受你首先会降低速度的事实——迁移需要一段时间是一回事,但是应用通常在生命周期的早期会变得更慢。这是由我们用远程模型检查的上述负面因素造成的。你会注意到,从长远来看,所有者团队会像他们领域中最有知识的工程师一样,很好地维护他们的应用并实现各种速度增强特性。
释放;排放;发布
代码被复制,所有的测试都通过了,是时候发布了。
战略
新微服务的首次部署总是有点混乱,并且需要事先就策略和方法进行大量讨论。正如本书中的大多数地方一样,发布过程没有灵丹妙药,但是,有一些方法可以选择,这取决于您的准备、工具和您的团队是否愿意早点醒来。
先读,后写——这种策略意味着微服务将首先只以只读模式运行,也就是说其上的流量不会修改它所拥有的数据。这是我最喜欢的策略之一,它允许你同时使用 monolith 和新的微服务来访问数据。如果您选择设置新数据库的读取复制,那么使用新服务提供的读取功能的 API 应该是相当安全的,例如获取 pizza 元数据。这样,您可以确保您的应用在生产环境中运行,并且只有在您确信您的基础架构能够处理它时,才开始在其中写入数据。
滚动部署 -基本上意味着你将把总流量的一部分发送到新的微服务,而将其余部分留在 monolith 上,缓慢但稳定地让所有流量由新系统处理。借助现代负载平衡器和服务网格,这可以很容易地建立起来。如果您选择创建一个读取副本,这不是一个选项,因为在新的微服务数据库上发生的写入不会在 monolith 的数据库中注册。
全流量变化——可能是最容易实现也是恢复最快的。当您确信您的服务工作正常时,您可以将给定 url 上的流量切换到新服务。这个过程应该简单且容易逆转,例如改变网站或文件的配置。
注意
当然,我们可以在这里讨论许多其他的发布策略。主要的想法是围绕你所拥有的关于风险、难度和准备时间的选项有一个背景,这样你就可以就你想要如何解决这个问题做出一个有根据的决定。
既然我们已经知道了发布我们的服务应该采用什么样的策略,那么让我们来看看当事情不可避免地发生时,我们该如何应对。
处理停机
根据我的经验,当发布新的微服务时,总会有一些预期的停机时间。好消息是,如果您事先做了几个小的准备步骤,这种停机时间可以最小化:
-
为恢复创建一个剧本——这可能是你能做的最重要的事情。确保你有一个一步一步的指南,让工程师将流量恢复到单一的应用。起初这看起来似乎微不足道,但在实际环境中,事情可能会变得非常糟糕,特别是在关键任务服务中,比如披萨元数据。确保也练习剧本,并让其他团队参与审查。
-
日志记录和监控应该到位——您的日志记录和指标应该到位,并在发布期间得到适当的监控,包括 monolith、新服务和数据库。
-
选择时间和地点——理想情况下,这样的发布应该发生在流量低的时候,你最了解你的应用,所以相应地选择时间。一般来说,周一早上或周六早上是这种迁移的好选择。如果有机会,让所有者团队和平台团队(如果有的话)的人员在现场进行有效的通信。
-
关于阶段化的实践——许多团队忘记的是,他们的系统通常有一个预生产或阶段化的环境。您可以利用这个空间来练习几次发布,因为这里最好没有真实的客户数据。
-
让公司的其他人知道-这是至关重要的一步,确保公共关系和客户服务团队了解即将进行的维护以及对客户可能产生的影响。他们知道的越多,如果事情变糟,他们就能更有效地通信。
-
不要忘记数据——确保你也有一个数据回填的计划,因为在一个有问题的版本中,monolith 和 microservice 数据库之间可能会有数据差异。
这里有一个在遭受攻击的情况下恢复 tizza 应用的示例剧本。我们的目标是做发布的人不需要考虑任何事情,只需要按照说明去做。
-
先决条件:
-
确保您连接到了 VPN 。
-
确保您可以访问
http://ci.tizza.io。 -
确保您可以通过 ssh 访问所请求的机器。
-
在你的机器上克隆最新的
https://github.com/tizza/tizza。
-
-
在 #alerts 频道用 @here 宣布你的版本有问题,需要恢复。
-
选择您想要部署的应用版本,然后点击绿色按钮。
-
如果部署工具报告失败,请继续。
-
ssh 进入主机,可以使用 ssh -A <主机 ip >
-
运行以下命令:
-
【sudo su-
-
bash-x ./maintenance/set-application-version . sh<应用版本>
-
supervisor TL 重启保证-应用粉笔-engine
-
-
如果服务仍然没有响应,请拨打+36123456789
这个剧本非常简单,但是,它为开发人员提供了多种解决方案。它包括一个先决条件部分,因此运行这些命令的开发人员可以确保他们能够完成剧本要求的所有事情。它还包括一个灾难情况解决方案,其中提供了一个电话号码,该号码很可能与该领域中有经验的开发人员相关联。
第二步还有一个针对公司其他部门的通信计划。这是绝对重要的,因为如果出了什么差错,你公司的其他人也会感兴趣。
我们做到了!应用已经迁移,但是我们还没有完全准备好。最有趣的部分还在后面。让我们谈谈如何确保我们不会留下一个巨大的烂摊子。
清除
图表和日志看起来很棒。客户没有抱怨任何新的问题,系统是稳定的。恭喜你,你刚刚发布了一个新的微服务!现在最有趣的事情来了:收拾我们留下的烂摊子。
就像你处理你的厨房一样,确保你不会在旧代码库中留下不需要的东西。您可以慢慢来,事实上,将旧代码保留 2-3 周通常是一个好主意,因此如果有一些问题,您仍然可以使用您创建的行动手册恢复到旧逻辑。
在您的新服务成熟一段时间后,请确保完成以下清理清单:
-
关闭 monolith 和微服务数据库之间的复制——如果您还没有这样做,现在可以关闭两个数据库之间的数据复制。
-
从新服务中删除未使用的表——如果您进行了简单的完整数据库复制,现在可以从微服务的数据库中删除域中不涉及的表。这将释放大量存储空间。
-
从 monolith 中移除不用的代码——移除不用的模块。确保进行一次彻底的清理,利用像 pycodestyle 这样的工具来找到可以删除的未使用的代码。
-
从 monolith 中删除未使用的表——现在您已经确定没有代码访问已经迁移到新服务的表,您可以安全地删除它们了。将这些数据存档并存储一段时间也是一个好主意,花费不多。
结论
在这一章中,我们学到了很多可以用来加速微服务迁移的小技巧。与此同时,我们的新系统也搞得一团糟。有许多重复的代码,仍然不清楚谁拥有应用的哪些部分。在下一章中,我们将更深入地探讨这一话题,并确保我们不仅能增加我们拥有的服务数量,还能扩展我们的组织和开发,以利用这些系统实现最佳效率。
六、规模化发展
到目前为止,我们已经花了很多时间讨论设计和构建微服务的方法和原因。然而,有一个古老的原则,许多行业专业人士多年来创造了这个原则,即“代码读得多,写得少。”基础设施和系统也是如此。如果您想要确保您的系统可伸缩,以便人们一眼就能理解它们,并且能够快速地转移到软件工程的“编写”部分,您需要在您的组织中花费一些时间在工具和文化上工作。在本章中,我们将探索其中的一些领域。
使用 Python 包
我已经多次提到的一件事是,在我们上一章所做的假设迁移中,我们一直在处理大量的代码重复。每个软件工程师都知道 DRY 原则(如果你不知道,现在就去查一下),所以希望你们中的一些人对复制这么大量的代码感到不舒服。
在第一部分中,我们将探讨重用代码的另一种方式。通过使用 Python 包。
重复使用什么
您可能会问自己的第一个问题是,您应该为什么创建一个新的包?答案通常是:无论两个或更多微服务中存在什么代码,都应该迁移到一个单独的包中。有时也有例外,例如,如果您预计在短期或长期内重复的代码会有严重的代码漂移,那么您应该将它们保存在单独的服务中,让它们各自漂移。我们在本书中使用的一些例子应该放在不同的包中:
-
RabbitMQ 发布者和消费者——它们的基本代码在每个服务中都应该相同,因此它们应该有自己的包。
-
rest 框架的无记名令牌认证方法——在第四章中,我们也看到了 Django REST 框架的无记名令牌认证选项。这也应该在一个包中分发,因为如果它在一个地方改变,它应该在所有地方都改变。
-
速率限制中间件——在第三章中,我们做了一个创建中间件的练习,这个中间件可以根据 IP 地址和一段时间内的呼叫量来限制呼叫。
-
远程模型及其实例——第五章中描述的模型也是一个很好的包。如果系统中的模型发生了变化,所有者团队只需要相应地更新客户端并重新分发包。
当然,还有其他的例子。如果您的系统有一个定制的日期模块,那么您可能也想把它作为一个包来分发。如果您有 Django 基础模板,并希望在服务中分发,那么包是最适合您的。
创建新的包
在您决定了首先将哪个模块移入包中之后,您可以通过在您最喜欢的代码管理工具中创建一个新的存储库来开始迁移过程。但是,在此之前,强烈建议检查您想要移出的模块的内部和外部依赖关系。从长远来看,依赖于包而不处理向后兼容性的包通常会带来麻烦,应该避免(特别是如果依赖是循环的,在这种情况下,无论如何都要避免)。
如果您已经隔离了想要移出的代码,那么您可以创建一个新的存储库,并将您想要的代码迁移到一个单独的包中。确保也移动测试,就像您在迁移服务时所做的那样。迁移后,您应该拥有如图 6-1 所示的目录结构。
图 6-1
基本包目录结构
让我们一个文件一个文件地检查一下:
tizza -我们想要保存包源代码的目录。您可能想知道当有多个包时会发生什么?答案是 Python 模块系统很好地处理了同名模块,并如您所料加载了它们。一般来说,用一个像你的公司名称这样的前缀作为你的包的前缀是一个好主意,因为它可以是你的导入中的一个很好的指示器,不管一个特定的方法是否来自于一个包,而且,如果你将来开源这些包,它可以是一个营销你的公司的好方法。
tests——我们保存测试源代码的目录。
setup . py——这个包文件包含了关于包本身的元信息。它还描述了如何使用该包以及对它有什么要求。这些文件非常强大,可以围绕你的包做很多操作。我强烈推荐在 https://docs.python.org/3.7/distutils/setupscript.html 查阅文档。清单 6-1 是一个示例 setup.py 文件:
from setuptools import setup, find_packages
VERSION = "0.0.1"
setup(
name="auth-client-python",
version=VERSION,
description="Package containing authentication and authorization tools",
author_email='akos@tizza.com',
install_requires=[
'djangorestframework==3.9.3',
],
packages=find_packages()
)
Listing 6-1An example setup.py file for a package
如你所见,这很简单。name 属性包含包本身的名称。这个版本是这个包的当前版本,它作为一个变量被移出,所以更容易修改它。这里有一个简短的描述,还有软件包所需的依赖项。在本例中,是 Django REST 框架的 3.9.3 版本。
使用一个新的包并不比创建一个更难。由于 pip 可以从各种代码托管站点下载包,比如 Github,我们可以简单地将下面一行插入到 requirements.txt 文件中:
git+git://github.com/tizza/auth-client-python.git#egg=auth-client-python
运行pip install-r requirements . txt现在将按照预期安装软件包。
这里我们可以提到的另一件事是关于包的版本控制。在本书的前面,我们已经提到,固定的依赖关系(有固定版本的依赖关系)通常比非固定的依赖关系更好,因为开发人员可以控制他们的系统。现在,在这里,你可以看到,我们总是拉最新版本的代码库,这违背了这个原则。幸运的是,pip 支持特定的包版本,即使它们来自代码版本控制系统,而不是“真正的”包存储库。允许使用以下引脚:标记、分支、提交和各种引用,如拉请求。
pip install git+git://github.com/tizza/auth-client-python.git@master#egg=auth-client-python
幸运的是,发布一个新标签非常容易,您可以用清单 6-2 中的 bash 脚本来完成:
#!/bin/sh
VERSION=`grep VERSION setup.py | head -1 | sed 's/.*"\(.*\)".*/\1/'`
git tag $VERSION
git push origin $VERSION
Listing 6-2example bash script for publishing tags
这个脚本从您的 setup.py 文件中获取版本信息,并在您所在的存储库中创建一个新的标签,假设其结构与上面的文件相同。因此,在运行脚本之后,您可以在您的需求文件中使用以下内容:
git+git://github.com/tizza/auth-client-python.git@0.0.1#egg=auth-client-python
当我们想要处理固定的包时,这是非常方便的。
注意
标记是管理包版本的一个很好的工具,但是,在理想的情况下,您不希望您的开发人员手动处理这个问题。如果您有资源,您应该将标记逻辑添加到您正在使用的构建系统的管道中。
我们已经设置了我们的包,是时候确保它在测试中得到很好的维护了。
测试包
在理想的情况下,测试您的包应该简单而优雅,运行一个测试命令,这很可能是您在您的单片应用中一直使用的命令,您的代码最初驻留在那里,然而,有时生活只是稍微困难一点。如果您的包需要在依赖关系互不相同的环境中使用,会发生什么情况?如果需要支持多个 Python 版本会怎么样?幸运的是,对于这些问题,我们在 Python 社区中有一个简单的答案: tox 。
tox 是一个简单的测试编排工具,旨在概括如何在 Python 中进行测试。这个概念围绕着一个名为 tox.ini 的配置文件。清单 6-3 向我们展示了一个简单的例子:
[tox]
envlist = py27,py36,py37
[testenv]
deps = pytest
commands =
pytest
Listing 6-3Simple tox file
这个文件的意思是,我们希望使用命令 pytest 针对 Python 版本 2.7、3.6 和 3.7 运行我们的测试。该命令可以用您正在使用的任何测试工具来替换,甚至可以用您编写的定制脚本来替换。
您只需在终端中说: tox 就可以运行 tox。
这到底是什么意思?当你在一家在整个生态系统中使用多个 Python 版本的公司开发软件时,你可以确保你正在开发的包在所有版本中都可以工作。
即使从这一小段中,您也可以看出 tox 是维护您的包装的一个很好的工具。如果你想了解更多关于 tox 的信息,我推荐你登录它的网站 https://tox.readthedocs.io/ 。
现在我们已经了解了包,它们应该如何被构造和测试,让我们看看如何存储关于我们的服务的元信息,这样公司的开发人员可以更容易地以最快的方式找到他们需要的信息。
服务存储库
随着您的系统随着越来越多的微服务而增长,您将面临与单一应用不同的问题。其中一个挑战是数据的可发现性,这意味着人们将很难找到系统中的某些数据。良好的命名惯例在短期内有助于这一点,例如,将存储披萨信息的服务命名为食品服务或烹饪服务可能比命名为戈登或冰箱更好(然而,我确实同意后两者更有趣)。从长远来看,您可能想要创建某种元服务,它将托管关于您的生态系统中的服务的信息。
设计服务存储库
服务存储库总是需要为给定的公司量身定制,但是,在默认情况下,您可以在模型设计中涉及一些东西,如清单 6-4 所示。
from django.db import models
class Team(models.Model):
name = models.CharField(max_length=128)
email = models.EmailField()
slack_channel = models.CharField(max_length=128)
class Owner(models.Model):
name = models.CharField(max_length=128)
team = models.ForeignKey(Team)
email = models.EmailField()
class Service(models.Model):
name = models.CharField(max_length=128)
owners = models.ManyToManyField(Team)
repository = models.URLField()
healthcheck_url = models.URLField()
Listing 6-4Basic service repository models
我们把它保持得很简单。我们的目标是让团队和工程师能够互相交流。我们创建了一个描述系统中的工程师或所有者的模型,我们还创建了一个描述团队的模型。一般来说,最好将团队视为所有者,它鼓励这些单位在团队内部共享知识。团队也有一个 slack 频道,理想情况下,它应该是一个单击连接,任何工程师都可以获得关于服务的信息。
您可以看到,对于服务模型,我们添加了几个基本字段。我们已经将 owners 设置为多对多字段,因为多个团队可能使用相同的服务。这在较小的公司和单一应用中很常见。我们还添加了一个简单的存储库 url 字段,因此可以立即访问服务代码。此外,我们还添加了一个健康检查 url,因此当有人对该服务是否正常工作感兴趣时,他们只需简单地点击一下即可完成。
拥有关于我们服务的基本元信息固然很好,但现在是时候添加更多日常使用的内容了。
寻找数据
现在,我们已经开始了这一部分,工程师可以寻找的最有趣的元数据之一是系统中存在的特定实体的位置以及如何访问它。为了在这个维度上扩展我们的服务存储库,我们还需要扩展现有服务的代码库。
记录通信
你可以要求你的团队做的第一件事是开始记录他们的通信方法。这意味着对于每个团队的每个服务,应该有某种形式的文档来描述给定服务存在什么实体、端点和消息。作为初学者,您可以要求您的团队在其服务的自述文件中包含这一点,但在这里,我们将了解更多选项。
Swagger 工具链
对于 API 文档,互联网上有很多工具可供参考,我们将深入研究的工具叫做 Swagger。你可以在 http://swagger.io 找到更多关于 Swagger 的信息。
Swagger API 项目由 Tony Tam 于 2011 年启动,目的是为各种项目生成文档和客户端 SDK。该工具已经发展成为当今业界正在使用的较大的 RESTful API 工具之一。
Swagger 生态系统的核心是一个 yaml 文件,它描述了您想要使用的 API。让我们看看清单 6-5 中的一个示例文件:
swagger: "2.0"
info:
description: "Service to host culinary information for the tizza ecosystem: pizza metadata"
version: "0.0.1"
title: "culinary-service"
contact:
email: "team-culinary@tizza.com"
host: "tizza.com"
basePath: "/api/v1"
tags:
- name: "pizza"
description: "Pizza metadata"
schemes:
- "https"
paths:
/pizza:
post:
tags:
- "pizza"
summary: "Create a new pizza"
operationId: "createPizza"
consumes:
- "application/json"
produces:
- "application/json"
parameters:
- in: "body"
name: "body"
description: "Pizza object to be created"
required: true
schema:
$ref: "#/definitions/Pizza"
responses:
405:
description: "Invalid input"
/pizza/{id}:
get:
tags:
- "pizza"
produces:
- "application/json"
parameters:
- in: "path"
name: "id"
required: true
type: "integer"
responses:
404:
description: "Pizza not found"
definitions:
Pizza:
type: "object"
required:
- "name"
- "photoUrls"
properties:
id:
type: "integer"
format: "int64"
title:
type: "string"
example: "Salami Pikante"
description:
type: "string"
example: "Very spicy pizza with meat"
Listing 6-5Swagger file example for the pizza entity
因此,该文件包含一些关于服务和服务所有者的元信息。之后,我们定义了客户端可以使用的端点,我们甚至提到了基本 URL 和应该使用的路径。
每条路径都被分解为方法,您可以看到我们为 pizza/端点分配了一个 POST 方法来创建 pizza。我们还描述了可能的响应以及它们的含义,包括文件末尾的 pizza 对象的结构。这些定义还包括接受什么类型的数据,以及可以从某些端点返回什么类型的数据。在这种情况下,我们所有的端点只返回 application/json 作为响应。
乍一看,这个文件只是看起来像一些不可读的废话。然而,当你将它与 Swagger 生态系统的其他部分配对时,你会得到一些不可思议的东西。首先,图 6-2 中由可视化编辑器创建的文件可以在 https://editor.swagger.io 找到。
图 6-2
傲慢的编辑
Swagger 编辑器是一个动态工具,使用它创建 Swagger 文件非常容易和有趣。它还将文件验证为 Swagger 格式,这样可以确保 API 描述符文件保留在生态系统中。
您还可以在自己的系统中利用 Swagger UI(上图中的右边面板),如果愿意,您可以下载源代码并将其托管在服务存储库旁边,这样您就可以对 API 描述符和想要了解它的人拥有最终的控制权。
您可能会问自己的第一件事是,是否有任何方法可以从这些定义中生成客户端代码。答案是肯定的。Swagger 有一个代码生成器模块,涵盖了多种编程语言的客户端代码的生成,但是,我们不会在本书中讨论这些选项。如果你想了解更多这些工具,我推荐你在 https://github.com/swagger-api/swagger-codegen 阅读代码和用户手册。
Swagger 对于同步 API 来说绝对是不可思议的,但是它不支持许多异步特性。在下一节中,我们将了解另一个可以帮助我们做到这一点的工具。
AsyncAPI 工具链
与您的同步 API 类似,您也可以(并且应该)记录您的异步 API。不幸的是,Swagger 不支持像 AMQP 这样的协议定义,然而,我们有另一个优秀的工具来处理这个问题, AsyncAPI 。
AsyncAPI 构建在与 Swagger 类似的 yaml 文件上。清单 6-6 展示了一个我们已经在做的烹饪服务的简单例子:
asyncapi: '2.0.0-rc1'
id: 'urn:com:tizza:culinary-service:server'
info:
title: culinary-service
version: '0.0.1'
description: |
AMQP messages published and consumed by the culinary service
defaultContentType: application/json
channels:
user.deleted.1.0.0:
subscribe:
message:
summary: 'User deleted'
description: 'A notification that a certain user has been removed from the system'
payload:
type: 'object'
properties:
user_id:
type: "string"
pizza.deleted.1.0.0:
publish:
message:
summary: 'Pizza deleted'
description: 'A notification that a certain pizza has been removed from the system'
payload:
type: 'object'
properties:
pizza_id:
type: "string"
Listing 6-6AsyncAPI example descriptor file
这里的规范非常简单。我们有两个路由键,一个用于删除用户,这是我们消费的,另一个用于删除披萨,这是我们生产的。消息本身描述了消息的结构,但是,我们也可以创建类似于 Swagger 描述文件中的对象。
就像在同步世界中一样,我们有一个很好的编辑器 UI(图 6-3 ,我们也可以在这里工作,在 https://playground.asyncapi.io 找到。
图 6-3
AsyncAPI 编辑器
注意
正如您可能已经看到的,我们没有声明消息将被发布到哪个交换或从哪个交换消费。 AsyncAPI 规范没有描述这一点的固有方式,但是,对于这种情况,您总是可以添加一个 x 属性。现在,我们可以称之为 x-exchange 。这是规范所接受的。
是时候把我们新的闪亮的 API 描述符放到我们的服务存储库中了。
把它绑在一起
在这些文件就位之后,我们可以开始将它们链接到我们的服务存储库中。清单 6-7 向您展示了如何将它们作为额外字段添加到服务模型中。
class Service(models.Model):
...
swagger_file_location = models.URLField()
asyncapi_file_location = models.URLField()
Listing 6-7Updated service model
这些新链接将使我们能够即时访问所有服务中的 API 用户界面。如果我们愿意,我们还可以扩展模型来包含并定期加载 URL 的内容,我们可以在用户界面上对这些内容进行索引以便进行搜索。
其他有用的 Tields
现在,如果您的服务存储库中有这么多可用的数据,那么您在行业标准方面已经做得很好了,并且为您的工程师提供了非常先进的工具,以便在您复杂的微服务架构中导航。您可能希望在将来添加几个字段,所以我会在这里为您留下一些想法,您可以在将来改进:
图表、警报和跟踪 -向服务存储库添加图表、警报和服务跟踪信息是一个简单的方法。这些通常是简单的 URL,但是,如果你想变得更有趣,你总是可以将图形嵌入到一些 UI 元素中,这样开发服务的开发者一眼就能了解服务的状态。
日志 -维护和使用日志对于每个公司都是不同的。但是,有时很难发现给定服务的日志。您可能希望将文档、链接甚至服务的日志流(如果可能的话)包含到存储库中。对于那些试图找出服务是否有问题,但又不太熟悉的工程师来说,这可能会加快速度。
依赖关系健康 -自从 2016 年 JavaScript 生态系统的巨大丑闻以来,当一半的互联网因为一个依赖关系(左键盘)在节点包管理器中被禁用而崩溃时,人们非常重视依赖关系,你可能也想搭上火车。您可以使用一些工具来确定服务中依赖项的最新程度和安全性。例如,您可以使用安全来实现这一点。
构建健康状况(Build health)——有时,如果服务的构建管道健康与否,这可能是有用的信息。如果需要,这也可以显示在服务存储库 UI 上。
如您所见,服务存储库是非常强大的工具,不仅可以发现服务,还可以很好地概述生态系统的健康状况和整体性能。
在最后一节中,我们将快速看一下如何利用脚手架的力量加速开发新服务。
脚手架
我们已经在如何扩展我们的应用开发方面取得了很大进展。在结束本书之前,我们可能还想做一小步,那就是搭建服务。
当设计微服务架构时,您可以瞄准的最终目标之一是使团队能够尽快交付业务逻辑,而不中断技术领域。这意味着建立一项新的服务(如果业务需要)应该是几分钟的事,而不是几天的事。
搭建服务的想法并不新鲜。有许多工具可以让您编写尽可能少的代码,并尽可能少地点击您的云界面,以构建您的开发人员可以使用的新服务。让我们从前者开始,因为它与我们一直在研究的内容非常接近。
搭建代码库
当我们谈论脚手架代码时,我们谈论的是拥有一个包含各种占位符的目录结构,并用与服务相关的模板变量替换占位符。为您的服务设计一个基础模板一点也不困难。你想要实现的主要目标是保持尽可能的简约,同时保持每个服务中应该存在的技术特定需求。让我们来看看这些可能是什么:
-
需求文件(Requirements files)——大多数服务喜欢维护它们自己的需求,所以在每个服务中分别维护这些需求通常是个好主意。有些团队喜欢在服务的基础映像中维护他们的需求,这也是一个解决方案。
-
测试 -团队应该以相同的方式编写测试,这意味着单元测试、集成测试和验收测试的文件夹结构和执行应该在任何地方都是相同的。这是必需的,这样开发人员可以尽快跟上服务的速度。这里没有妥协。
-
服务的基础结构*——服务的 API 和模板在每个服务中都应该是一样的。公司中使用给定语言的每个人都应该对文件夹结构感到熟悉,在浏览代码库时应该不会感到意外。这个结构本身也应该可以工作,并且应该包含一些在模板完成后就可以工作的东西。*
** 基本依赖关系 -可能是由需求文件暗示的,但是,我想强调一下基本依赖关系。这些依赖的主要目标是保持公共代码的完整性,而不是被公司的多个团队重写。您之前提取的包应该包含在基本依赖项中,如果不需要它们,可以从长远来看删除它们。
* readmes 和基本文档 -基本模板还应该包括 Readmes 和其他文档文件的位置,例如默认情况下的 Swagger 和 AsyncAPI。如果可能的话,用于模板更新的脚本也应该鼓励以某种方式填写这些信息。
* *健康检查和潜在图表* -服务的构架还应该包括如何访问服务以及如何检查服务是否工作。如果您正在使用 Grafana 这样的工具,这些工具使用 JSON 描述符文件构建服务图,那么您也可以在这里生成它们,这样所有服务的基础图看起来和感觉起来都是一样的。
* Dockerfiles 和 Docker compose files——我们在本书中没有过多地讨论 Docker 和围绕它的生态系统,但是,如果你正在使用这些工具,你一定要确保你正在搭建的服务默认包含这些文件。*
*每个人都应该可以接触到基础脚手架。我建议在你最喜欢的代码版本系统中创建一个新的存储库,用模板和脚本填充其中的模板,并让公司的所有开发人员都可以访问它。如果您愿意,也可以留下一个示例服务。
对于脚手架本身来说。我推荐使用非常简单的工具,比如 Python cookiecutter 模块。
我想在这里指出的一点是,脚手架会在短期内加快你的速度,然而,从长远来看,它会导致另一系列的问题。确保我们生成的所有这些文件在整个微服务生态系统中保持统一和可互换几乎是不可能的。在这一点上,如果您想使用一个健康的、可维护的基础设施,建议让专门的人来处理您的系统的统一性和可操作性。这种最近蓬勃发展的工程文化被称为“开发人员体验”我建议对它进行研究,评估适应对你和你的公司是否值得。
搭建基础设施
搭建代码库是一件事,另一件事是确保你的云提供商有资源来托管你的系统。以我的经验来看,在公司生命周期的每个周期,每个公司的这个领域都是极其不同的。因此,为了方便起见,我将提供一些指导并提到一些您可以在这里使用的工具。
HashiCorp 的 Terraform 是一个非常强大的工具,用于维护基础设施代码。基本思想是,Terraform 定义了各种提供者,如 Amazon Web Services、DigitalOcean 或 Google Cloud Platform,在这些提供者中,所有资源都以类似 JSON 的语法描述。参见清单 6-8 中的地形文件示例:
provider "aws" {
profile = "default"
region = "us-east-1"
}
resource "aws_instance" "example" {
ami = "ami-2757f631"
instance_type = "t2.micro"
}
Listing 6-8A simple terraform file
上面的例子直接来自 Terraform 网站。它展示了几行代码,您可以用它们在 Amazon 上创建一个简单的实例。很整洁,是吧?如需了解更多关于搭建整个基础设施的 terraform 和工具的信息,您可以前往 http://terraform.io 查看教程。
过一段时间,你会发现不仅管理你的代码和服务会变得困难,而且管理你的秘密、密码、用户名、密钥对,一般来说,所有你不想与业务之外的人分享的东西也会变得困难。Vault 是 HashiCorp 开发的一个工具,也是为了让这方面的事情变得更容易。它提供了一个简单的接口,并与云生态系统的其余部分很好地集成。围绕它的 API 既简单又安全。
Chef-Chef 是最受欢迎的代码解决方案基础设施之一,被全球数百家公司(如脸书)用于增强其基础设施。Chef 使用 Ruby 编程语言的强大功能来扩展。
结论
在这一章中,我们已经了解了如何使用 Python 包,以及如何使用它们来确保开发人员不会在每次创建新服务时都重新发明轮子。我们还学习了服务存储库,以及如何通过创建同步和异步消息传递系统的详细文档来帮助自己。我们还了解了脚手架服务,以及我们希望工程师在创建新服务时使用的模板的最低要求。
我希望这一章已经为您提供了关于如何在您的组织中扩展微服务开发的有用信息。*