Django微服务设计指南全绝不原创的飞龙

早在农业革命之前,服务就已经存在于人类之中。每当我们走进商店或餐馆,我们都在使用一种服务。有些服务比其他服务更复杂,提供给我们的商品更符合我们的口味,或者需要更少的工作,而有些服务更专业,专注于一项任务,并且做得非常好。让我们看一个简单的例子。

市场和餐馆都可以算作服务,尽管它们提供给我们不同的东西,但它们都给我们一般的食物。当你进入一个市场,你可以从各种各样的卖家那里买到各种各样的食材。之后,你可以在家里将所有这些材料组合成你喜欢的各种菜肴。然而,一家豪华的餐馆会为你提供现场制作的美味佳肴。在餐馆的后台有许多流程和系统在工作,其中大部分你都不知道,但是在你的请求被处理后,你的饭菜就被送到了。

当我们谈论服务时,餐馆/市场摊位的类比只是我们可以提出的众多类比之一。从概念上讲,它们的工作方式与软件中的服务相同。

在过去的几年中,围绕服务设计、软件即服务、微服务、单片、面向服务的体系结构等等有很多讨论。在下图中,可以清楚地看到微服务越来越受欢迎。图1-1提供了过去五年谷歌搜索“微服务”的图形视图。

图1-1

2014年以来“微服务”一词的流行,谷歌

在接下来的几节中,我们将试着弄清楚其中每一个的含义,以及当你在这样的系统中工作时需要记住的重要术语。请记住,这些术语不是刻在石头上的。

术语“软件即服务”(或SaaS)主要描述一种许可模式,在这种模式下,你可以为某种在线服务的使用付费。这些软件中的大部分都存在于云中,并为最终用户提供各种方式来修改和查询他们系统中的数据。Spotify就是一个例子,这是一个在线音乐流媒体软件,最终用户可以用它来听音乐和创建自己的播放列表。此外,Spotify有一个广泛的软件界面和软件包,工程师可以使用它们以编程方式获取和修改Spotify云中的数据。

面向服务的架构(或SoA)可能是业界最受欢迎的术语之一。简单地说,这种风格的架构设计比任何东西都更支持服务。正如我们在上面所了解的,服务需要服务于某种业务需求,并且需要围绕现实世界的需求进行建模。服务需要是自包含的,并且有一个干净的接口,通过它可以进行通信。它们是独立部署和开发的,在抽象层次上代表一个功能单元。该架构还涉及这些服务之间使用的通信协议。一些软件即服务公司使用面向服务的架构向他们的最终用户交付高质量的产品。

对软件工程师来说,这是十年来最可怕的词汇之一。单一的应用和单一的服务是单一的服务,它们增长得太大了,以至于无法进行推理。“太大”到底意味着什么,将是本书的核心话题之一。我们还会看到,这个可怕的词不一定意味着不好的事情。

软件工程师十年来的另一个可怕的词(尽管原因不同)。简而言之,微服务是一种存在于面向服务的架构中的服务,并且易于推理。这些是松散耦合的轻量级组件,具有定义良好的接口、单一用途,并且易于创建和处理。由于细粒度的性质,更多的人可以并行处理它们,特性的所有权成为组织要解决的一个更干净的问题。

现在,我们已经看了一下高层次的定义,让我们深入研究一下独石。

正如我们所了解到的,monoliths是一个开发人员甚至一个开发团队都难以理解的庞大的代码库,其复杂性已经达到了一个程度,即使只更改几行代码也会在其他部分产生意想不到的未知后果。

首先,我们需要在这里打下一个基础,那就是独石本身并不坏。有许多公司已经在单片应用上建立了成功的IT业务。

事实上,独石实际上对发展你的业务很有帮助。只需很少的管理费用,您就可以为您的产品添加更多的功能。在IDE中只需几个组合键就可以轻松导入模块,这使得开发变得轻而易举,并让您对向雇主交付大量代码(无论是高质量还是低质量)充满信心。就像生活中的大多数成长过程一样,公司和软件需要快速启动并经历快速迭代。软件需要快速增长,这样企业才能得到更多的钱,这样软件才能增长,这样企业才能得到更多的钱。你会看到它在哪里生长。所以下次你参加一个会议,听到一个演讲者说独石是旧设计的系统,一定要半信半疑。顺便提一句,我想说的是,如果你的初创公司正在努力维护你的遗留单片应用,这通常是一个健康业务的标志。

图1-2简单展示了该视频流公司在应用中发生的事情。

图1-2

由紧密耦合的损坏组件导致的全面停机

现在,在这一部分的结尾不提及独石的好处是不公平的。抛开上面所有的恐怖故事和消极因素,建议是利用单一模型来发展你的业务和工程。只要确保紧紧抓住方向盘,每个人都在技术债务上保持一致。

现在我们已经了解了monolith的外观和行为,我们将看看它在本书中的对应部分,微服务。

简单回顾一下:微服务是一个单一用途的软件应用,驻留在web上的某个地方,是一个小型的代码库,即使是一个工程师也可以很容易地对其进行推理。

就像我们了解到独石不是撒旦的后代一样,我们将了解到微服务也不是银弹。

图1-3

松散耦合的组件可能只会导致部分停机

就这样吗?嗯,不。在上面的故事中,我们公司有一个运行聊天系统的服务,但核心业务仍然最有可能驻留在monolith中。如果聊天系统在那里,这是完全可能的营销活动也将是一场灾难,导致该公司真正的结束。总之,在关键情况下,在系统中构建或提取哪怕是很小的模块都会带来巨大的成功。

请记住,像这样的转换是一种很好的方式,可以让您组织中的工程师成长,并测试他们的知识,不仅仅是技术方面,还有组织方面。这是一个很好的机会,可以留住贵公司的高级工程师,并使正式员工和初级员工能够探索他们还不需要接触的新领域。

在这一点上,用微服务开始你的entre架构似乎是一个好主意。让我们在下一节探讨这个选项。

从经验来看,如今公司可能犯的最大错误之一是开始使用微服务构建他们的架构。尽管今天的工具远远优于5年前的工具。我们有令人惊叹的协调器,如Kubernetes,容器化工具,如Docker,AWS上的各种服务,如EBS,它们帮助我们构建和部署服务。然而,尽管有惊人的工具,开发可能会很快陷入困境。

我自己工作的系统从一开始就被设计成面向“微服务”的。理论上,一切看起来都很棒。这些服务有清晰的接口定义,并且都有所有者。这些服务被编译在一起,并通过一个异步的、基于事件的系统相互通信。构建过程使用一个非常成熟的构建协调器作为基础系统,并经过优化以帮助所有工程师取得成功。受到如此多的赞扬后,你可以预料结果是负面的。是的,它是。理论上一切看起来都很完美,但实际上大多数事情都失败了。系统过于分散,依赖性成了负担,而不是促进因素。工程师们需要的是速度,而不是一个当他们更新另一个组件时需要升级每个组件版本的系统。不久之后,维护接口定义变成了一场噩梦,因为很难理解组件之间的交叉依赖和版本控制。工程师们更多地是在寻找定制的、不受监管的捷径,而不是目前的工作方式。构建系统,虽然非常专业,但被认为是为时过早,减缓了开发人员的速度,而不是使他们能够加速开发。

后来,该公司决定将微服务架构合并到一个整体应用中,此后速度开始加快。最终证明该系统是成功的。最终,代码库变得太大,团队开始考虑再次拆分的方法。

从微服务开始是个坏主意吗?当工程规范要求你应该转向另一个方向时,转向一个整体是个好主意吗?

至于第二个问题,不管你问谁,答案都会是响亮的“是”。在这个故事中,一个很好的例子是能够回头看,并后退两步,从长远来看,向前迈出巨大的一步。

这个故事的寓意之一是,为了实现这一飞跃,你不仅需要了解行业中的技术和最佳实践,还需要了解你的业务、组织以及在你公司工作的人。可能适用于其他公司(在某些情况下,甚至只是你的工程组织中的其他团队)的东西对你来说可能是一个巨大的失误。

在本书中,我们将致力于尽可能贴近现实生活的问题。因此,我们将根据业务需求开展工作,并尝试遵循整个产品的服务设计流程,而不仅仅是其中的一小部分。以下是我们的问题空间:

提扎。Tizza是一个移动优先应用,用户可以通过照片和对披萨的描述来决定他们是否喜欢披萨。喜欢一个地方后,用户将收到使用该应用组织到给定比萨饼店的团体参观的通知。最终用户有一个朋友列表,他们应该能够管理。用户可以设置私人(仅朋友)和公共(不仅仅是朋友)事件。

在我们深入研究服务设计之前,我们将在终端中运行几个命令来开始我们的Django项目。

强烈建议将您的代码和依赖项组织到虚拟环境或容器中。如果您不完全熟悉这些概念,我强烈推荐您使用virtualenv或Docker查看Python开发。

要安装Django,您只需在终端中运行以下代码:

pipinstalldjango当我们看到Django应用作为一个整体运行时,我们称之为项目。要创建Django项目,只需在终端中执行以下命令:

django-adminstartprojecttizza这样,一个简单的Django应用将被创建在一个名为tizza的文件夹中,如图2-1所示。

图2-1

裸露django文件夹结构

如果我告诉你,在这一点上,你已经有一个功能正常的网站?在您的终端中键入以下内容:

您应该会看到图2-2中的屏幕。

图2-2

Django已成功安装

恭喜你!现在真正的工作开始了。

Django在其通用文件夹结构中有第二层逻辑。这些被称为应用。命名可能会有点混乱,因为它们与智能手机上运行的软件无关。应用通常用作业务案例和/或应用功能之间的逻辑分隔符。你可以认为项目能够在不同的应用之间进行编排,而一个应用只能属于一个项目。要创建新的应用,我们只需运行以下命令:

pythonmanage.pystartapppizza现在我们的目录结构已经更新,如图2-3所示。

图2-3

带app的裸django文件夹结构

每当您收到一个新功能或开始开发一个全新的产品或应用时,您通常需要考虑的第一件事就是驱动它的数据。一旦数据搞清楚了,借助Django和ORMs的力量,我们可以马上开始。

ORM(或对象关系映射)充当数据库和应用之间的抽象层。您不希望每次在数据库上运行原始查询时都创建封送和解封代码。我们不仅在讨论漏洞问题,还在讨论这些未受保护的系统可能存在的巨大安全问题。

相反,我们将使用ORM,大多数语言和web框架都有类似的东西。对于不使用Django(或者不需要整个web框架)的Python开发人员来说,有SQLAlchemy。对于PHP开发人员来说,这是一个信条,对于Ruby爱好者来说,这就是ActiveRecord。简而言之,我强烈建议您开始使用并习惯ORM,如果您还没有的话,因为它们会让您和您公司的其他开发人员的生活更加简单。

为了让您对ORMs有更多的了解,请想象以下情况:您是一名刚从学校毕业的工程师,渴望完成您的第一份工作,即维护和扩展web应用。您得到的第一个任务是从数据库中获取并显示一些数据。幸运的是,你上过数据库和网络课程,所以你对数据应该如何流动有一个模糊的概念。您编写的第一段代码如清单2-1所示。

importdatabasedefget_pizzas(pid):cursor=database.cursor()returncursor.execute(f"""SELECT*FROMpizzasWHEREid={pid};""")Listing2-1Simplequerytogetapizza代码本身并不可怕,但是有几个非常严重的问题,有经验的工程师可能已经注意到了:

总而言之,像上面这样写代码是危险的,会给数据完整性和工程寿命带来不必要的风险。当然,有些问题是无法用我们将要学习的方法解决的,在这种情况下,你总是可以使用原始的SQL查询,只是要特别注意你输入的内容。

在Django中,我们需要在应用中创建一个models.py文件来开始使用ORM。该文件应该如下所示:

#pizza/models.pyfromdjango.dbimportmodelsclassPizza(models.Model):title=models.CharField(max_length=120)description=models.CharField(max_length=240)Listing2-2Databasemodelforourpizza您在上面看到的是一个数据库表作为Django模型的表现形式。Pizza类继承了Django提供的Model类。这将通知系统有一个我们想要使用的新数据类型,以及我们在接下来的几行中列出的字段。在我们的例子中,标题和描述都是字符字段。为了简单起见,我们将在数据库中创建我们的表。为此我们将利用移民的力量。

迁移就是从您的模型中生成脚本,您可以使用这些脚本自动搭建您的数据库,而无需运行手动CREATETABLE等操作。迁移是一个非常强大的工具,我推荐阅读更多关于它的内容,就本书而言,我们只使用最基本的内容。

pythonmanage.pymakemigtations在您的项目目录中运行上面的命令将导致Django收集每个应用中的模型,这些模型在insalled_apps下的设置文件中注册,然后为它们创建一个迁移计划。迁移计划本质上是包含数据库操作的Python文件,这些操作按照将在数据库上运行的执行顺序排列。也就是说,如果您运行以下命令:

pythonmanage.pymigrate现在,您的表应该已经准备好了,该是我们探索数据库中的内容的时候了。

因此,您只需创建一个模型,在shell中运行2个命令,突然您就有了一个数据库设置,其中包含了您想要处理的所有表。刚刚发生了什么?生活怎么这么神奇?

Django做的事情实际上非常不可思议。当您运行makemigrations命令时,Django收集在您的应用中创建的所有模型(每个应用),在内存中注册它们,并解析它们的元数据,例如其中需要的列、索引和序列。之后,它运行一个代码生成模块,该模块将在数据库元信息的先前状态和当前状态之间创建一个差异,并在migrations文件夹中呈现一个文件。这个文件是一个简单的Python文件,可能看起来像这样:

fromdjango.dbimportmigrations,modelsclassMigration(migrations.Migration):initial=Truedependencies=[]operations=[migrations.CreateModel(name='Pizza',fields=[('id',models.AutoField(auto_created=True,primary_key=True,serialize=False,verbose_name="ID")),('title',models.CharField(max_length=120)),('description',models.CharField(max_length=240)),],),]Listing2-3Migrationfilefortheinitialpizza您可以查看并根据需要进行修改。请务必查看在pizza应用的migrations文件夹中创建的迁移文件。尝试向模型中添加一个新字段,再次运行makemigrations,并查看所创建的两个文件之间的差异。

当您使用migrate命令应用迁移时,您只需使用数据库引擎按顺序执行这些Python文件。这里需要注意的一点是,根据表的大小、分配的资源以及迁移的复杂性,在实时数据库上运行迁移可能是一项开销很大的操作。我总是建议在实际环境中执行迁移时要小心!

访问项目运行时的行为方式非常简单。只需执行:

pythonmanage.pyshell这将启动一个交互式PythonREPL,它具有从您的Django应用加载的所有设置,并且行为与您的应用在当前上下文中的行为完全一样。

>>>frompizza.modelsimportPizza>>>Pizza.objects.all()我们可以看到数据库中目前没有比萨饼,所以让我们创建一个:

>>>Pizza.objects.create(title="PepperoniandCheese",description="Bestpizzaever,clearly")>>>Pizza.objects.all()|]>>>>pizza=Pizza.objects.get(id=1)>>>pizza.title'PepperoniandCheese'在我们的数据存储中创建一个新对象就是这么简单。更新现有的也很简单。

>>>pizza.description'Bestpizzaever,clearly'>>>pizza.description="Actuallythebestpizzaever.">>>pizza.save()>>>pizza2=Pizza.objects.get(id=1)>>>pizza2.description'Actuallythebestpizzaever.'对于这个例子,我们很幸运,但是,我们还没有真正满足任何业务需求,所以让我们继续添加几个模型,以便在我们享受巨大乐趣的同时至少满足一些需求:

现在我们已经创建了许多模型,我们可以在练习2-1中更多地使用shell。

在外壳中创建几个新的比萨饼,并将它们放在新创建的比萨饼店下面。试着去买一家披萨店的所有披萨。为比萨饼创造几个赞。尝试访问所有喜欢的比萨饼店!这些都是很棒的特性,有一天会成为我们应用的一部分。请随意探索外壳和模型层。

公开我们数据的主要方式是使用Django的视图。视图本质上是端点,您可以利用它向客户返回各种类型的数据,包括他们浏览器中的HTML页面。

为了设置视图,我们将在披萨应用中创建一个名为views.py的文件。

#pizza/urls.pyfromdjango.urlsimportinclude,pathfrom.viewsimportindexurlpatterns=[path('',index,name="pizza"),]#tizza/urls.pyfromdjango.contribimportadminfromdjango.urlsimportinclude,pathurlpatterns=[path('admin/',admin.site.urls),path('pizzas/',include('pizza.urls')),]Listing2-6Theeditedurlsfilessowecanaccesstheresources太棒了!现在是我们开始利用互联网的力量获取数据的时候了。根据您使用的工具类型,您可以远程呼叫pizzas端点。这里有一个可以从终端运行的curl:

如果您能看到我们几分钟前刚刚创建的比萨饼的id、描述和标题,我有一个好消息要告诉您:您已经成功地通过互联网查询了数据库!如果您有问题,我建议检查您的服务器是否正在运行,如果没有,在重试之前运行以下命令:

pythonmanage.pyrunserver现在,让我们尝试以下内容:

{"status":"error","message":"pizzanotfound"}我们的视图功能看起来很棒,但我们需要一些额外的功能来满足业务需求(练习2-3)。

让我们创建一个端点,它将从数据库中返回一个随机的比萨饼。端点应具有以下路径:

/pizzas/random让我们确保在这样做的时候不会弄乱其他端点!

完成后,我们还可以向客户端返回15个随机的披萨(这样我们就不会对后端服务器进行那么多远程调用)。我们还应该确保,如果用户看到了一个比萨饼,他们不应该再次收到相同的。你可以使用Likes模型来实现。

您可能已经注意到,我们已经在url模式中添加了一个名为admin的url。默认情况下,Django为整个应用提供了一个非常方便的管理视图。如果您需要手动管理数据库中的对象,这可能非常有用。要访问管理面板,首先您需要在数据库中创建一个超级用户。

在这一点上,除了用户管理,你可能看不到很多有用的模块。如果您想在这里使用pizza资源,您需要将下面的代码添加到tizza/pizza/admin.py中,这将告诉管理面板注册pizza模型以进行管理。

fromdjango.contribimportadminfrom.modelsimportPizzaclassPizzaAdmin(admin.ModelAdmin):passadmin.site.register(Pizza,PizzaAdmin)此时,我们可以访问Django管理面板,查看我们的模型在UI上的行为(图2-4)。

图2-4

Django管理面板

在这个屏幕上,您可以访问所有用户信息,所有在Django中注册的模型,包括用户组和权限。总的来说非常方便,是快速帮助客户的一个非常强大的工具。

当您检查应用是否正常工作时,它也是一个非常好的测试工具。只需点击几下鼠标,创建用户就变成了一项非技术性的任务。

管理面板也是一个高度可配置的界面。您可以向已经实现的各种模型添加自定义字段和自定义操作。如果您想了解更多关于Djangoadmin的知识,我建议您查看文档。这里还有很多值得探索的地方。

如果您希望以这种方式访问实时数据库,您需要为每个环境创建一个超级用户。

为了确保我们熟悉管理面板,让我们在练习2-4中创建几个用户和几个比萨饼。

让我们试着从我们创建的API访问比萨饼。

只是为了实践管理应用的多功能性,让我们为pizza添加一个新的字段,通过它我们可以选择pizza是肉、素食还是纯素食。提示:对于数据库模型,您应该检查models.ChoiceField。添加它之后,运行迁移并尝试在管理面板中创建新的pizza。有什么变化?

Django提供了一个非常强大的工具集来为用户实现一个简单的注册表单。

首先,我们将创建一个注册视图。但是我们应该把它放在哪里呢?我猜用户应用现在应该没问题。我们将使用Django表单来解决这个问题,因为auth应用默认为注册用户提供了一个表单。

fromdjango.contrib.authimportlogin,authenticate在这一点上,这是一个将这些功能粘合在一起的问题。为了使我们的系统更加灵活,我们将使用基于类的视图,也是由Django提供的:

#user/views.pyfromdjango.contrib.authimportlogin,authenticatefromdjango.contrib.auth.formsimportUserCreationFormfromdjango.shortcutsimportrender,redirectfromdjango.viewsimportViewclassSignupView(View):template_name='signup.html'defpost(self,request):ifform.is_valid():form.save()username=form.cleaned_data.get('username')password=form.cleaned_data.get('password1')user=authenticate(username=username,password=password)login(request,user)returnredirect('/')defget(self,request):returnrender(request,self.template_name,{'form':UserCreationForm()})Listing2-7Classbasedviewforsigningup正如您所看到的,基于类的视图是一种更简洁的方式来描述在您的端点上会发生什么,这取决于您在其上使用的操作。我们现在只需要一个屏幕,向用户显示这些内容。

fromdjango.urlsimportpathfromtizza.user.viewsimportSignupViewurlpatterns=[path(r'^register/—',SignupView.as_view()),|]太好了。使用as_view方法,我们可以很容易地将基于类的视图转换成我们在本章前面遇到的常规Django视图。您可以在图2-5中看到我们创建的注册页面。

图2-5

注册页面

正如我们之前看到的,Django提供的auth包附带了许多内置功能。这些也包括客户端身份验证端点。

其中包括以下内容:

将这些端点添加到您的系统中并不是最难的事情,我们需要做的只是更改项目目录中的urls.py文件。

#tizza/urls.pyfromdjango.contribimportadminfromdjango.urlsimportinclude,pathfromdjango.contrib.authimportviewsasauth_viewsurlpatterns=[path('admin/',admin.site.urls),path('pizzas/',include('pizza.urls')),path('login/',auth_views.login,name="login"),path('logout/',auth_views.logout,name="logout"),]Listing2-9Urlsforlogginginandloggingout默认情况下,Django会尝试呈现registration/login.html模板。让我们用下面的格式创建这个文件。,这与注册页面非常相似:

现在让我们将pizza的端点再扩展一点,因为我们需要能够与它们进行交互。

首先,我们将为拥有比萨店的用户添加一个比萨创建端点,这可以通过多种方式完成,这次我们将使用HTTP动词来区分我们希望在实体上执行的各种操作。

您还可以注意到一件微妙的事情,我可以用login_required装饰器的力量来做这件事。这不亚于使用由认证中间件填充的请求用户。等等,什么是中间件?

中间件是Django的核心概念之一。就像食人魔和洋葱一样,Django也有层次,当请求和响应进入和退出应用时,它们会经过这些层次。这个分层系统的核心是视图功能和基于类的视图本身。考虑图2-6。

图2-6

Django请求响应周期一览

在这里您可以看到,当一个请求进入您的应用时,它进入各种中间件层,这些中间件层可以做很多事情。中间件的几个例子:

commonmiddleware——提供一些基本的功能,这些功能的实现很简单。例如发送禁止的用户代理,并确保URL以斜杠结尾。

正如你所看到的,中间件有各种各样的用途,但是要小心你放在中间件代码库中的东西。由于服务中的所有请求都将进入该中间件,计算密集型和网络操作可能会显著降低应用的速度。

现在我们已经熟悉了Django的工作方式,并且我们已经创建了几个可以通过浏览器访问的简单页面,让我们更深入地了解一下,在我们公司请得起设计师之前,我们如何让用户体验变得更容易接受。

{#user/templates/signup.html#}{%extends'base.html'%}{%blockcontent%}

Signup

{%csrf_token%}{{form.as_p}}Signup{%endblock%}Listing2-11Reminderontemplates第一行是注释,如果你正在读这本书,你可能已经熟悉了注释。

第二行是扩展语句。这基本上意味着该模板将使用注册为base.html的模板中的所有块,然后扩展并覆盖其中指定的属性。这意味着,我们可以为我们的应用构建一个基础模板,其中我们需要指定网站中只需要在任何地方出现一次的部分。让我们看一个简单的例子:

{%blockcss%}{%endblock%}{%blockheader%}

Thisistheheaderofthewebsite,thedesignerswillprobablywantittobestickyandweneedtoaddaloginbuttontotherightifthecustomerisloggedouroralogoutbuttoniftheyareloggedin
{%endblock%}{%blockcontent%}{%endblock%}{%blockfooter%}
Thiswillbeourfooterwherewewillputcolumnsaboutourcompanyvaluesandthejobopenings.
{%endblock%}{%blockjavascript%}{%endblock%}Listing2-12Simplebasetemplate我知道,上面的东西是很多代码,但是让我们运行一个快速分析和思想实验,看看每一行是如何有意义的:

作为一个简单的例子,我们将创建一个视图和一个模板,显示我们在视图中返回的比萨饼的信息。

fromdjango.shortcutsimportrenderfromdjango.viewsimportViewfrompizza.modelsimportPizzaclassGetTenPizzasView(View):template_name='ten_pizzas.html'defget(self,request)pizzas=Pizza.objects.order_by('')[:10]returnrender(request,self.template_name,{'pizzas':pizzas})Listing2-13Pizzashufflingendpoint上面的代码有点笨拙,但是现在它将为我们完成返回10个随机比萨饼的工作。让我们来看看我们将要构建的模板:

关于模板的进一步阅读,我建议查阅文档。

当你编写一个应用时,你总是需要确保实体只能被有权限的用户查看、编辑和删除。同样,我们很幸运选择Django作为我们构建tizza的工具,因为我们已经有了一个内置的许可系统。如果您已经熟悉基于UNIX的权限,您可能会跳过接下来的几段,直接进入代码示例,对于我们其他人来说,这里有一个术语入门:

用户对象有以下两个字段,在使用权限和组时会很方便:

让我们来看一个快速的权限示例:

#pizza/views.pyfromdjango.contrib.auth.modelsimportPermissiondefindex(request,pid):#...elifrequest.method=='DELETE':if'can_delete'inrequest.user.user_permissions:pizza=Pizza.objects.get(id=pid)pizza.delete()returnHttpResponse(content={'id':pizza.id,})else:returnHttpResponse(status_code=404)Listing2-15Permissionsexample在上面的简单例子中,我们检查用户是否有权限删除给定的比萨饼。当他们创建了这个披萨,或者如果他们加入了有权处理这个披萨的组,我们可以授予这个权限。

到目前为止,我们在tizza应用上已经做了很多工作,但要完成产品还有很长的路要走。现在我们将把它留在这里。我为渴望的人增加了练习2-7到2-9。如果您只想看到项目的运行,您可以访问下面的存储库,克隆代码库,并试用应用:

尽管代码库仍然只有几千行,但我们已经可以看到项目中的某些区域可能会更好。在下一章中,我们将探索将项目分割成小块的选择,并熟悉我们将在本书其余部分遵循的原则。

很高兴我们已经为正在使用的模型创建了API,然而,我们的大多数用户并不完全熟悉curl的神奇之处。对于这些人,让我们创建一个页面,在那里我们随机获取比萨饼,一个接一个地展示它们,并提供给他们喜欢或不喜欢的比萨饼。当我们卖完比萨饼时(或者更好:快卖完了),让我们去拿下一批。

如果您以前使用过电子商务应用,您会知道有两种类型的用户。买的人(客户)和卖的人(商家)。到目前为止,我们主要是迎合客户的用例,让我们确保商家也得到一些爱。对于拥有比萨饼店的用户来说,他们应该会收到一个仪表板页面,在那里他们可以管理所有他们想在我们的系统中显示的比萨饼(创建新的,更新和删除现有的)。

我们需要开始赚钱。创建一个新的应用,将从商家方面模拟我们的支付,所以他们可以从他们的管理页面“提高”他们的产品的可见性。

现在,我们对微服务的鸟瞰图有了一个模糊的概念,是时候放大并仔细观察各种服务以及它们如何在内部相互交互了。为了更容易理解我们的架构,我们将把重点放在3个主要类别上,因此在讨论整个系统时,推理会更容易一些

首先,我们将查看特定服务在您的体系结构中的位置。我们将检查3种类型:

先从用户看不到的说起。你会明白为什么。

每个系统都有用户不直接与之交互的组件,只是通过许多抽象层。例如,您可以想象一个状态机,它计算用户在当前系统中是如何表示的。他们是以前的客户,还是目前的客户?也许他们以前从未在你的网站上使用过付费功能,但出于营销目的,存储他们的数据是很重要的。

后端服务为您的应用提供主干。其中大多数封装了公司核心业务逻辑的功能。他们经常提供数据或者与数据转换有关。他们更可能是其他服务的数据提供者,而不是消费者。

设计纯粹的后端应用有时感觉像是一个微不足道的挑战,我们将从我们的pizza应用中查看几个例子,以确保我们理解这意味着什么。在图3-1中,您可以看到连接到数据存储的pizza微服务,该数据存储包含我们在前一章中定义的pizzaDjango应用下的模型。

图3-1

想象中的tizza应用后端系统

这里值得一提的是,这些服务的设计是由它们托管的数据和它们工作的领域共同驱动的。在构建微服务时,创建仅托管单一特定数据类型的服务是一个非常常见的错误。当不同类型的数据位于相似的业务领域时,它们应该在物理上和逻辑上彼此靠近。然而,这总是一个艰难的决定。这里有几个案例,你可以在午休时和同事一起哀叹:

以上所有问题都有多个好答案。我的建议是这样考虑:如果数据不是耦合的或者是松散耦合的,那么您可以安全地将其分解。如果耦合紧密,试着测量耦合有多紧密。例如,您可以随时测量从数据存储中一起查询不同资源的次数,这可能有助于您做出决定。

这个演讲可能会给你一个在不同的存储器中进行数据复制的想法。让我们绕过这个话题快速说一下。

既然我们已经谈了很多关于数据服务如何工作的内容,我想稍微绕一下,谈谈当您迁移到微服务时,您将使用的不同数据存储之间的数据复制。

使用微服务时,数据复制会变得非常自然。毕竟,您的服务中确实需要电子邮件地址,对吗?为什么不在创建用户时存储它,这样您就可以确信这些数据在任何时候都是可用的。

这样的想法可能很有欺骗性。当你使用微服务(实际上,软件中的任何东西)时,你总是想减少的一件事就是维护工作。当您在服务中引入一个新的数据库、表甚至只是一个字段时,您正在为自己创建所有权和维护工作。在上面提到的电子邮件示例中,您需要确保电子邮件始终保持最新,这意味着您必须确保如果您的用户在auth服务中更改了它,您也需要在自己的服务中更改它!当客户想要使用他们被遗忘的权利时,您也需要确保删除或匿名化您数据存储中的电子邮件地址。从长远来看,这会导致很多不一致和令人头痛的问题。

在许多系统中保持数据一致是一个非常困难的问题。几十年来,数据库工程师一直在与上限定理作斗争,创造了像暗示移交或草率仲裁这样的算法,最终实现了各种数据库副本之间的一致性。在您的应用中实现这样复杂的一致性算法值得吗?

如您所知,我不太喜欢数据复制。当然,有些情况是你无法避免的,但是,我通常会推荐以下替代方案:

在开始从其他服务复制数据之前,请考虑替代方案。从长远来看,这可能会让你付出昂贵的代价。

既然我们已经很好地理解了数据将存储在哪里以及如何存储,那么让我们来看看将消费它们的服务类型。

前端服务的存在是为了将呈现在用户机器上的前端应用容器化。乍一看,它们的存在可能没有多大意义,但是,有几个原因可以解释为什么设计(几乎)完全前端的服务对您和您的团队来说可能有意义:

前端服务可以直接使用来自后端服务和系统的数据,后端服务和系统可以将后端服务提供的数据集成为由特定业务逻辑定义的更易于理解的格式。让我们来看看混合服务。

按照SoA的理念,有时我们需要系统只为我们的业务做一件事,而且它做得很好。没有前端或后端专业知识的工程师需要负责这些服务。也完全有可能这些业务组件没有严格地绑定到工程部门。这本书的主要焦点将围绕后端和混合服务。

如果所有权或者缺乏维护系统的人,我们完全可以考虑系统,我喜欢称之为“混合服务”。在野外,它们有时被称为后端到前端服务,或BFF。

混合服务将大量前端和后端组件连接在一起,以实现简单的业务目标。在开始编写代码之前,让我们先看一个例子:

让我们想象一个世界,在遥远的未来,我们成为tizza中最重要的团队之一的技术领导,这就是tizza-admin团队。我们的使命是确保所有的披萨创建者可以轻松管理他们的披萨,并可以在应用中推广营销活动。他们需要一个单页应用来使体验更加流畅。阅读规范后,可能会出现以下问题:

所有这些都是每个全栈(和非全栈)在构建具有多个数据源的单页面应用时应该问自己的有效问题。我们不想做的第一件事是连接到现有的数据库(我们将在本章的后面有更多的推理),所以我们将限制自己调用API。在这里,我们可以选择从单独的数据源或从单个数据源调用数据的端点(在这种情况下,例如,我们需要比萨饼的列表、许可、活动选项和支付细节等)。借助事件循环和线程的力量,我们可以轻松地运行第一个选项,同时并行获取所有信息,但是,我们正在消耗大量网络带宽。

更改当前已有的端点来支持部分响应负载可能有点麻烦,因此出现了一个服务的想法,该服务将为我们聚合数据并以紧凑的响应进行响应。弊端?我们已经向BFF引入了额外的呼叫。

随着独立服务而来的是另一个美好的东西,那就是所有权。BFF通常是系统中拥有最多业务逻辑的部分,这使得它成为产品团队所有权的完美候选。

现在,我们已经熟悉了如何对微服务进行分类的基本概念,我们将深入了解服务的高级架构应该是什么样子。

我们将看看像SOLIDprinciples这样的方法——最初用于整体服务来管理代码复杂性——以及它们如何提供一种考虑服务的有用方式。我们还将看看在服务设计过程中出现的一些常见的设计模式。

请记住,我们在这一部分要看的例子应该有所保留和思考。这些模式并不能解决设计服务时的所有困难。在实现过程中保持开放的心态,在将这些原则集成到您的系统中时专注于您的业务问题。

你们中的一些人可能听说过传奇软件工程师制定的坚实原则,如SandiMetz和RobertC.Martin,如果没有,这可能是一个令人大开眼界的小片段。

defpizzas(request):ifrequest.method!='GET':#wearepost(Iguess)returnupdate_pizzas(request)else:returnget_pizzas(request)Listing3-1Notconformingtoopen-close向上面的代码中添加一个新的方法类型需要进行大量的修改

defpizzas(request):ifrequest.method!='GET'andrequest.method!='PUT':#stillpost!(Iguess)returnupdate_pizzas(request)elifrequest.method=='PUT':returncreate_pizzas(request)else:returnget_pizzas(request)Listing3-2Stillnotconformingtoopen-close相反,考虑下面的(仍然不是最好的,但是足够了):

假设你是一名后端开发人员。您的工作是编写原始的、多用途的API,供数百个内部和外部客户每分钟使用。这些年来,您的界面已经成长为巨大的怪物,其中一些没有限制它们返回的关于客户的数据量。从名字到他们去过的餐馆数量,每次访问的朋友列表都会在响应中返回。现在,这可能对你来说很容易,数据库就在你的下面,在你的MySQL集群上有了聪明的查询,你就能够保持API的高速运行。然而,移动团队突然开始抱怨。他们说,你不可能指望客户每次打开应用时下载数百千字节的数据!的确,大规模的API被分割成较小的API肯定会更好。这样,查询的数据更加具体,这种后端服务的重构和扩展将会更快。构建API的时候,一定要从客户端开始!

微服务——应该——都是关于依赖性反转原则的。在理想情况下,系统使用契约(如API定义)进行通信,以确保每个服务都在相同的页面上提供和使用哪种数据。可悲的是,现实世界并不总是充满阳光和幸福,但我们将看看如何实现这一目标的方法。

关于微服务设计,人们经常忘记的一件事是,它不允许你编写糟糕的代码,并在底层遵循糟糕的设计模式。确保你为你在低层和高层抽象上设计的系统感到自豪,并且这个服务不仅仅是可替换的,而且是可维护的。

更流行的服务设计方法之一是遵循12因素应用的规则。最初由AdamWiggins编写,后来由Heroku负责维护,12因素应用是一个微服务设计方法集,它为我们提供了构建可扩展和可维护的服务时应该遵循的12点。现在,这些方法涵盖的范围比这个厨师所能深入涵盖的要广得多,所以我建议在12factor.net多读一些。

1。版本控制系统中应该有一个被跟踪的代码库,被多次部署

我认为现在没有太多的代码库没有被各种修订系统跟踪,例如Git或Subversion。如果你是一个还没有采用这些技术的人,我强烈建议你检查一下,并把它们集成到你的工作流程中。一个应用应该由一个代码库和一个或多个部署组成。在面向对象的术语中,你可以把你的代码库想象成一个类,而部署则是你的类的一个实例,带有各种参数,使它能够在生产、开发或测试环境中运行。

您的代码库在不同的部署中可以有不同的版本。例如,当您构建应用时,您的本地开发部署可以在不同版本的代码库上运行。

正如我们将从本书的后面部分了解到的,依赖性管理是构建微服务的最大和最困难的问题之一。12条规则中的第二条可以给我们一些经验法则,我们可以遵循这些法则来开始。

在python世界中,我们通常使用pip结合需求或设置文件作为依赖管理器。这条规则规定所有的依赖项都应该有固定的版本。这是什么意思?想象一下下面的情况:您在您的应用中使用带有一个非固定版本的包A。一切都进行得很顺利,直到在包中发现了一个关键的安全性,而您却从未得到通知。此外,该项目的唯一维护者已经在8个月前失踪,导致您的所有用户数据被盗。现在,这听起来像是一个极端的情况,但是如果你曾经和依赖管理器如npm和版本指示器如^和~一起工作过,你就会知道我在说什么。为了安全起见,使用==作为依赖项。

3。店铺配置环境

为了遵守规则#1,我们需要将依赖于部署的配置与部署本身分开存储。依赖于部署的配置可以是多种多样的,它们通常是您的应用运行所必需的。我们指的是以下变量:

4。将外部服务视为资源

5。非开发部署创建应支持构建-发布-运行周期

一个包含12个因素的应用将部署创建分为3个独立的阶段

6。12因子app分别是无状态流程

12因素应用假定没有东西会长期存储在主应用旁边的磁盘或内存中。同样,这样做的原因是能够推理出应用以及它将来可能会出现的错误。当然,这并不意味着您不能使用内存,建议将它视为单个请求缓存。如果您在内存中存储了许多东西,并且您的流程由于某种原因(例如新的部署)而重新启动,您将丢失所有这些数据,这可能对您的业务没有好处。

7。使用端口绑定导出您的服务

更具体一点的web开发(但是,嘿,这本书的大部分内容都是关于web开发的),这个规则规定应用应该是完全自包含的,不应该依赖于web服务器的运行时注入,而是通过绑定到一个端口并通过该端口服务请求来导出它的接口。

在我们的情况下,Django会处理所有的事情。

8。使用流程向外扩展

这并不意味着你的进程运行时不鼓励线程,在Python的情况下,你的应用完全可以利用“线程”库,或“asyncio”。另一方面,您的应用需要能够扩展为在相同或多个物理和/或虚拟机上运行的多个进程。

确保不要在操作系统层面上使事情过于复杂,只需使用标准的工具来管理您的流程,如“systemd”或“supervisor”。

第6点实现了这一点。

9。流程应该易于启动和处理

不要依赖你的12因素应用的流程,因为它们应该很容易摆脱,也很容易创建。一接到通知。不过,这有几个要求。

10。保持尽可能接近

**确保生产环境中运行的代码尽可能地接近开发机器上运行的代码。为了避免误解,我们所说的接近是指运行的应用版本之间的差异。根据12因素应用,要实现这一目标,您需要努力缩小3个“差距”:

你可能会认为这些大多说起来容易做起来难。十年前,如果运营人员不在几分钟内到位,几乎无法想象服务的部署。大多数持续开发和部署系统都是使用从运营人员那里收集的各种脚本手工构建的,这些人厌倦了每次有人更改代码库时运行“rsync”。如今,整个行业和技术分支都在发展,以使部署体验更快、更简单、更不容易出错。有些系统可以直接连接到您的git存储库,并为您的集群提供自动化部署,如AWSCodePipeline、CircleCI或Jenkins。

如果您不熟悉持续集成(CI)或持续部署(CD)管道,我建议您阅读一下。在devops.com上可以找到极好的资源。

关于工具,今天,在容器化的时代,您和您的开发人员可以使用多种工具来简化它。在我们浏览它们之前,让我们先来看看为什么这很重要:

想象一下下面的情况:您的一个开发人员正在处理一个非常复杂的查询,而您的系统的ORM无法处理这个查询,所以您决定使用一个原始查询作为解决方案。开发人员启动他们的本地系统,开始在本地SQLite数据库上构建查询。几天后,数百行查询就完成了,覆盖了自动和手动测试,一切都运行得很好。开发人员在他们的拉式请求上获得批准,并且在部署之后,您的监控系统提醒团队该特性不可操作。经过一些调试后,开发人员得出结论,他的本地SQLite数据库和生产环境中运行的Postgres之间存在语法差异,这是他所不知道的。

在过去,在您的本地开发部署上运行轻量级支持服务是有意义的,因为您的机器上的资源通常是有限且昂贵的。今天,有了我们使用的巨型开发机器,这不再是一个问题。另一个问题可能是后端服务类型的可用性。在您的本地机器上维护Postgres集群可能看起来很乏味,如果您没有工具备份的话,这是很乏味的,因为现在虚拟化,尤其是容器化的力量已经提供了工具备份。

如今,在本地机器上设置Postgres数据库就像编写一个Dockercompose文件一样简单,如下所示:

version:'3'services:postgres:image:postgres:11.6.1ports:-"5432:5432"Listing3-4SampleyamltospinupadatabasewithDockerCompose再也没有借口了!确保在您的所有部署中使用类似的生态系统,以减少上面详述的错误类型。

11。日志应该由别的东西管理

这一点很简单。一个12因素的应用不应该关心管理和写入各种日志行,而应该把所有的日志作为一个事件流写入“stdout”。这使得开发非常容易,因为在本地机器上,开发人员可以看到他们的应用中发生了什么事件,从而加快了调试过程。

在登台和生产环境中,流由执行环境收集,然后发送以供查看和/或存档。这些目的地不应由12因素应用配置。

如今,有几十种优秀的日志解决方案供您使用。如果你不确定从哪里开始,我建议查看Logstash、Graylog或Flume。

12。运行您的行政流程作为一次性流程

出于维护目的,开发人员经常需要在12因素应用上运行手动流程/脚本。一些例子包括:

现在我们已经了解了12因素应用的规则,我们可能对高性能微服务有一个模糊的概念。理想情况下,你已经听说过这些要点中的大部分,并认为它们是值得添加到你的设计服务库中的东西。在本书的某些部分,我们将会观察到,由于发展或业务的限制,这些规则是如何被打破的。无论我们在哪里打破12因素的规则,我都会让你知道,你可以评估自己是否值得。

我们采纳了一些高层次的设计理念,从鸟瞰的角度来看,我们的服务应该是什么样的。现在,我们将放大图片,了解它们应该如何相互交流。**

因此,我们已经对微服务的外观以及如何从概念上开始创建微服务有了一个很好的基本概念。在这一章中,我们将更深入地探讨这些服务如何能够、应该以及将要彼此交互的主题。我们将讨论同步和异步通信、安全性和测试等主题。系好安全带,让我们直接进入休息的基础。

回到英雄时代,对于互联网上的交流没有真正高层次的定义。您可能还记得参加网络课程,学习TCP和UDP协议,所有关于ACK和NACK循环的知识,以及各种握手,以确保您可以连接到不同的系统。见鬼,你甚至可能用C语言编写了一个远程计算器,在那里你使用套接字与你机器上不同的开放端口通信。哦,那些日子!

REST代表代表性状态转移。这是一个协议,最初是在2000年罗伊·托马斯·菲尔丁的传奇论文中提出的,叫做架构风格和基于网络的软件架构的设计。没有进入太多关于这篇论文的细节,它描述了90年代系统设计的方法和术语,为今天仍然存在的最佳实践打下了坚实的基础。对于每一个想认真做系统设计的人来说,这绝对是必读书。REST协议在论文的第五章中有详细介绍。

如前所述,REST是一种消息传递协议,旨在允许web上各种服务之间的无状态通信,无状态通信意味着接收者接收的消息与之前的消息无关。遵循REST原则的服务允许通过请求中的文本表示来修改资源和实体。

如果这听起来有点不正常,看看清单4-1中的tizzaAPI。

defpizzas(request,pid):ifrequest.method=='PUT':data=request.json()pizza=Pizza.objects.create(**data)returnHttpResponse(status_code=201,data={'id':pizza.id,})else:returnHttpResponse(status_code=405)Listing4-1ExamplerestfulpizzaAPI在上面的例子中,我们可以看到一个视图函数,它要么创建一个比萨饼,要么向API的调用者返回一个奇怪的状态代码。您可以看到我们使用了一个HTTP动词,PUT来检查操作。这是REST给我们的标准之一。根据您使用的动词,您应该在应用中执行某些操作,这样API的调用者就可以知道会发生什么。我们在响应中使用的状态代码是201,代表“已创建”。状态代码类似于动词。如果我们看到一个201,我们知道作为一个来电者会期待什么。405代表不支持的方法。在清单4-2中,您可以看到一个未找到资源的HTTP响应的示例表示。

$curl-v-XGETlocalhost:8000/pizzas/101Note:Unnecessaryuseof-Xor--request,GETisalreadyinferred.*Trying127.0.0.1...*TCP_NODELAYset*Connectedtolocalhost(127.0.0.1)port8000(#0)>GET/pizzas/101HTTP/1.1>Host:localhost:8000>User-Agent:curl/7.54.0>Accept:*/*>

让我们浏览一下HTTP动词列表,以便我们对我所说的HTTP动词的意思有一个共同的理解:

GET可能是当今互联网上最常用的HTTP动词,是你在浏览器上访问网站时使用的默认动词。它所做的就是,从指定的端点获取资源。如果您正在开发一个使用GET动词为端点提供服务的API,那么您的被调用者的一个期望就是充当端点的服务不会修改应用的服务器端状态(比如写入它的数据库),这意味着GET应该总是等幂的。这里有一个我们希望避免的例子:

$curl-XGETlocalhost:8000/pizzas/1{"id":1,"title":"PepperoniandCheese","description":"Yumm"}$curl-XGETlocalhost:8000/pizzas/1{"id":1,"title":"SalamiPicante","description":"AlsoYUMM"}这里发生了什么?我们调用端点两次,它没有返回相同的响应。现在,在某些情况下可能会有争议,这可能是预期的结果,例如在端点上返回一个随机响应,或者在请求之间修改资源,但是,如果GET端点不像清单4-3中那样:

在上面的curl中,我使用了-XGET标志,通常在使用curl进行GET请求时,这是不需要的。

PUT——另一个重要的HTTP动词之一。PUT表示在请求的有效负载中发送的对象的替换或创建。对象标识符通常在请求本身的URI中表示,主体包含需要在系统中覆盖的成员。PUT请求本质上应该是幂等的。意思是:

$curl-i-XPUTlocalhost:8000/pizzas/1-d'{"title":"Diavola","description":"Spicy!"}'HTTP/1.1201Created...{"id":1,"title":"Diavola","description":"Spicy!"}$curl-i-XPUTlocalhost:8000/pizzas/1-d'{"title":"Pikante","description":"Spicy!"}'HTTP/1.1200OK...{"id":1,"title":"Pikante","description":"Spicy!"}所以,不管发生了什么,我们总是得到相同的对象,有相同的标识符。

POST-经常与PATCH混淆,POST是不幂等的对应词。这意味着每当您向资源的端点发送POST请求时,您应该总是期望在那里创建一个新的资源。

$curl-i-XPOSTlocalhost:8000/pizzas/-d'{"title":"Diavola","description":"Spicy!"}'HTTP/1.1201Created...{"id":1,"title":"Diavola","description":"Spicy!"}$curl-i-XPOSTlocalhost:8000/pizzas/-d'{"title":"Diavola","description":"Spicy!"}'HTTP/1.1200OK...{"id":2,"title":"Diavola","description":"Spicy!"}你还可以看到,在上面的曲线中,我们没有指定想要处理的对象的标识符。

$curl-i-XPATCHlocalhost:8000/pizzas/2-d'{"title":"Diavola","description":"Spicy!"}'HTTP/1.1404NotFound...$curl-i-XPOSTlocalhost:8000/pizzas/1-d'{"title":"Diavola","description":"Spicy!"}'HTTP/1.1200OK...{"id":1,"title":"Diavola","description":"Spicy!"}删除——一个比较简单的动词,它是为了说明我们想要从系统中删除一个特定的资源。

有几个不太常用的HTTP动词,我们将快速浏览一下:

HEAD——这个动词用于在发出适当的get请求之前只获取请求的头部。如果您不确定需要处理的响应的内容类型或大小,这可能会派上用场,它可以为您的系统提供一个有根据的决定,决定是否发出请求。

选项——使用这个HTTP动词,您可以确定在给定的资源上还接受哪些HTTP动词。

看完HTTP动词之后,我们还将快速浏览一下最流行的响应代码。响应代码本质上是REST如何传达请求在我们发送给它的系统中是如何处理的。让我们来看看上面的一些例子。

$curl-i-XPOSTlocalhost:8000/pizzas/-d'{"title":"Diavola","description":"Spicy!"}'HTTP/1.1201Created通过这个POST请求,我们希望确保有一个Diavola披萨的描述是“辣的!”在tizza后端。响应可以按以下方式分解:

HTTP/1.1-HTTP版本,告诉我们请求-响应周期本身使用的HTTP版本。通常我们不需要关心这个,但是,有一些旧的系统不支持1.1版本,也有一些新的系统已经支持2.0版本。大多数情况下我们不需要担心这个。

201-响应代码,这是一个数字,表示我们向其发送请求的系统中发生了什么。通常你需要根据响应代码编写逻辑来处理外部系统的响应,参见下面的清单4-4。

importrequests...response=requests.get('pizza/1')if404==response.status_code:#Wecouldn'tfindthepizza,panic!......Listing4-4Exampleresponsestatuscodehandling注意上面,我们使用requests库从一个系统向另一个系统发出HTTP请求。对于初学Python的用户,我强烈推荐阅读关于3.python-requests.org的文档。另外,看看Github上的源代码,因为它是目前写得最好的Python包之一。

创建了-HTTP状态动词。这基本上是状态代码的书面形式。从程序上来说,处理起来有点麻烦,因此我们通常在处理响应时忽略它,而依赖于状态码。

2xx状态代码通常类似于接受和成功。

200OK-希望是您遇到的最常见的响应状态代码,通常它表示您对外部系统的意向已成功处理。这可能意味着从资源到资源的任何东西都是从数据存储中获取的。

201创建-通常我认为区分不同的200响应有点浪费。然而,有时,让处理客户端看到外部系统中发生了什么会很有帮助。对我来说,201和202是这些信息性消息中的一部分,如果需要的话,应该对它们进行处理。201表示在外部系统中创建了新的资源。如果您还记得前几页,我们检查了PUTHTTP动词,在这里可以创建或更新资源。在这种情况下,201对客户来说是一个很大的优势。

202Accepted-Accepted关键字以后会派上用场。基本上它表明请求已经被记录在被调用的系统中,但是,还没有响应。这个响应代码通常意味着请求已经进入了一个排队系统,该系统最终会处理它。

3xx响应代码通常表示资源已被移动到不同的位置。

301永久移动-此状态代码表示请求的资源已被移动到不同的URI。通常,它自身也会带来重定向。在这种情况下,新的URI应该位于位置头参数中。默认情况下,许多浏览器使用该标题进行重定向。

304未修改3xx系列中的例外。这个响应代码由服务器指示请求的资源没有被修改,因此客户机可以使用其上的任何缓存数据。只有当客户端指示请求是有条件的时,这才是真的,这意味着它已经在本地存储了所提到的数据。

4xx响应表示客户端在访问所需资源时出现错误。在这些情况下,客户端应该在再次发送请求之前重新考虑修改请求。

400错误请求——可能是4xx系列中最常见的响应代码。指示请求本身存在错误。在这一点上,这可能意味着数据验证(例如,比萨饼的名称太长)或者只是一个格式错误的请求设置,比如不受支持的内容类型。

404Notfound——表示在被调用的系统中找不到某个资源。如果我们想要隐藏我们想要访问的资源的存在,使其更加安全,但是对于客户端工程师来说更加混乱,那么这个响应代码通常被用作401和403的替代代码。

5xx响应表示服务器端出现故障。如果您是资源持有者,您应该会收到警报,并测量系统中这些响应的数量。在客户端重试这些是合理的。

500服务器错误5xx系列中最常见的形式。指示服务器端存在未处理(或已处理,但未说明)的异常。对于资源所有者来说,这些异常应该被记录和解释。

502错误网关和503服务不可用-服务器之前的网关或代理从服务器本身收到无效响应,并且在满足请求时出现问题。可能是因为应用服务器没有响应,在这种情况下,请确保所有进程都在服务器端正常运行。

504网关超时-应用服务器的网关或代理没有及时收到响应。这可能表示服务器端的各种故障,从不堪重负的CPU和/或内存到失效的数据库连接池。

这些是我喜欢在应用中使用的基本HTTP响应和动词。如果您在客户端和服务器端都遵循并尊重这些,我可以保证在开发软件时,您的工程团队和微服务之间的摩擦会更少。让我们来看一个tizza服务器的练习。

RESTful后端通常是由人类手工编写的,这意味着涉及到很大的错误率。大多数人对REST应该如何工作有自己的理解。对上面的部分有所保留(也许还有一些牛至),并确保当你在与外部系统一起工作时,你非常精通它是如何操作的。

我们已经讨论了很多关于状态代码和HTTP动词的内容。我想请你现在回到你在第二章写的代码,并根据我们在本章学到的知识重新评估它。哪些端点和资源遵循REST原则,哪些不遵循?

现在我们已经熟悉了REST,让我们看看Django为我们提供的关于这项技术的一些工具。

我知道你在想什么。我们已经了解了关于响应的所有这些事情,并确保我们的服务可以通过HTTP以简洁的方式相互通信,尽管这看起来像是一个可怕的大量工作。幸运的是,Django有一个插件解决方案,可以让您的服务器端代码立刻变得RESTful。让我向您介绍DjangoREST框架。

首先,我们需要安装框架本身。在您的虚拟环境中,运行以下代码:

pipinstalldjangorestframeworkDjangoREST框架需要注册为Django项目的应用。为此,使用以下内容扩展您的settings.py文件:

INSTALLED_APPS=(...#HerearetheDjangobuiltins'rest_framework',...#Hereareyourcustomapps)瞧啊。现在您可以随意使用DjangoREST框架了。让我们把它挂在外面。

首先,我们将创建一个序列化程序。序列化器的存在使得我们可以将驻留在数据存储中的模型转换成对REST更友好且可由其他系统处理的东西。我这么说是什么意思?当不同的系统相互交流时,它们需要对如何表示正在传输的数据有一个共同的理解。也可以传输来自数据库的原始模型,但是,消费者应用不太可能理解这些数据的实际含义。为此,我们需要将数据转换成一种通用格式,在当今世界中通常是JSON。在这个演示中,我们将使用框架提供的默认序列化程序,但是您可以轻松地编写自己的序列化程序,或者使用Python包索引中的序列化程序。

让我们在清单4-5中创建一个名为serializer.py的文件。

我们的下一步是创建我们称之为视图集的东西。这基本上描述了当我们试图访问资源本身时应该运行什么类型的查询。让我们在pizza应用中创建一个viewsets.py文件,参见清单4-6。

fromrest_frameworkimportviewsetsfrompizza.modelsimportPizzafrompizza.serializersimportPizzaSerializerclassPizzaViewSet(viewsets.ModelViewSet):queryset=Pizza.objects.all()serializer_class=PizzaSerializerListing4-6Thepizzaviewset代码看起来很简单,但是它包含了很多功能。有了这个简单的视图集,我们将能够通过一个请求查询数据库中的所有比萨饼,以及通过标识符查询资源。

我们已经非常接近REST框架的工作解决方案了。我们需要做的最后一步是将路由添加到应用本身。首先,让我们在我们的pizza应用中创建一个routes.py文件,参见清单4-7。

fromrest_frameworkimportroutersfrompizza.viewswetsimportPizzaViewSetrouter=routers.DefaultRouter()router.register(r'api/v1/pizzas',PizzaViewSet)Listing4-7Pizzarouter路由器是将RESTful资源映射到一组标准化URL的工具,同时简化了它们的定义。在这里,我们只使用默认路由器,但是,您可以利用各种路由器,为您提供不同的功能,如自动前缀。

现在我们已经添加了路由器,我们将简单地将它链接到tizza模块中的urls.py文件,如清单4-8中所述。

frompizza.routersimportrouter...urlpatterns=[....url(r'^',include(router.urls)),url(r'^api-auth/',include('rest_framework.urls',namespace="rest_framework"))]Listing4-8PizzaURLconfigsadded是时候尝试我们的新功能了。首先,让我们尝试获取带有第一个id的披萨。

图4-1

DjangoREST框架提供的管理接口

我们还收到了一个完整的用户界面,在这里我们可以使用这些资源。当有多个团队维护多个服务并且资源无处不在时,这个功能非常有用。这个用户界面为API的消费者提供了一种与资源交互的方式,而无需阅读大量的文档。

一些更熟悉web服务和REST的人可能熟悉分页的概念。几个请求之前,我们已经从服务中查询了所有的比萨饼,这是一个有用的功能,但是,当我们的用户创建了数百甚至数千个资源,每次他们需要信息时,我们都会返回给客户,这将导致一个巨大的问题。当人们使用只有蜂窝数据的移动设备时,这可能会特别痛苦。这是分页概念出现的原因之一。本质上,这个想法是客户端给出一个偏移量和一个批处理大小,并接收资源,这些资源以某种方式从偏移量索引到偏移量+批处理大小。这个概念非常简单,尽管通常需要在客户端实现。为了使用DjangoREST框架实现分页,我们需要做的就是将清单4-9中的以下几行添加到我们的settings.py文件中。

REST_FRAMEWORK={'DEFAULT_PAGINATION_CLASS':'rest_framework.pagination.LimitOffsetPagination','PAGE_SIZE':5}Listing4-9BasicpaginationsetupfortheRESTframework在图4-2中,你可以看到现在实现的分页的第一页是什么样子:

图4-2

数据库中前五个比萨饼的列表

正如你所看到的,我们的数据库中总共有6个比萨饼,端点返回给我们其中的5个,并给了我们以后可以使用的下一页的URL。在查询了那个之后,你可以在图4-3中看到,我们在第二页上只收到了1个披萨。

图4-3

页码的第二页

我们已经阅读了很多关于DjangoREST框架的内容,是时候将它付诸实践了。使用这个框架,为我们在第二章中创建的所有资源类型创建RESTfulAPIs。

首先,让我们找到一个应该被保护的资源。我认为“喜欢”是关于客户的非常敏感的信息,所以让我们开始吧。在清单4-10中,您可以看到我们将创建的视图集的原始代码:

importbase64fromrest_frameworkimportauthentication,exceptionsfromtizza.settingsimportCLIENT_TOKENSclassBearerTokenAuthentication(authentication.BaseAuthentication):defauthenticate(self,request):try:authorization_header=request.META['HTTP_AUTHORIZATION']_,token_base64=authorization_header.split('')token=base64.b64decode(token_base64.encode())client,password=token.split(':')ifCLIENT_TOKENS[client]==password:returnNone,Noneelse:raiseexceptions.AuthenticationFailed("Invalidauthenticationtoken")exceptKeyError:raiseexceptions.AuthenticationFailed("Missingauthenticationheader")exceptIndexError:|raiseexceptions.AuthenticationFailed("Invalidauthenticationheader")Listing4-11BearertokenauthorizationfortheRESTframework让我们快速浏览一下代码,这样我们就都在同一页上了。第一件看起来奇怪的事情是,我们从应用的设置中加载了这个名为CLIENT_TOKENS的常量。这应该是一个用os模块填充的字典,其中包含所有启用的客户端标识符及其各自的令牌。这里有一个例子:

要进行试验,您需要用base64对密码进行编码,并发出如下请求:

我们已经谈了很多关于同步世界的问题。正如你所看到的,REST和同步通信有很多好处,但是,如果你有太多的服务调用太多的其他服务,就很容易使你的系统变得迟钝。业界提出的解决这个问题的一个解决方案是异步通信和排队系统。

系统之间的同步提供了清晰的通信和一种简单的方法来推理应用。每当你请求一个操作,请求被处理,完成,然后当你收到一个响应,你可以肯定你想要的是成功或失败,但它已经完成。然而,有时如果我们只坚持同步通信,我们会在应用设计中遇到各种各样的困难。考虑以下示例:

我们在tizza应用上进展顺利,人们正在使用它,架构也在不断发展,现在有多个团队拥有和开发多个微服务。这是2018年2月的一个阳光明媚的日子,我们的安全主管告诉我们,GDPR即将到来。为了与GDPR兼容,我们需要确保所有用户上传的信息,包括喜欢和参加的活动,都需要被删除。现在,团队坐下来头脑风暴手头的问题,并提出了图4-4中的架构计划。

图4-4

GDPR的简单解决方案

正如您所看到的,解决方案非常简单:当用户从auth数据库中删除时,会有一个对每个保存用户虚拟值的服务的远程调用。作为第一个版本,这可能是一个好主意,但是,这带来了各种问题:

如你所见,这种方法有很多问题。幸运的是,有解决方案可以减少耦合,使系统中的所有权更清晰。我们称这些系统为队列。

你可能记得在高中或大学学过排队。现在,想象同样的概念,只是在建筑层面上。系统中的应用将消息发布到代理或主题,然后代理或主题将消息放到队列中,供工作人员使用。图4-5简单概述了上面的例子是如何工作的。

图4-5

GDPR的队列概念

如您所见,auth服务发布了一条消息,表明用户已从系统中删除。消息被发布到代理,代理将消息推送到三个单独的队列。一个删除赞,一个删除出席率,一个删除上传的披萨。

这样有什么好处?

不幸的是,Django没有为这些队列编写消费者的超级方便的支持。因此,对于这本书的这一部分,我们将把Djangoverse留给几个段落,并研究Python中针对异步问题的框架不可知的解决方案。

我们要看的第一个工具是RabbitMQ。RabbitMQ是在2000年代中期基于高级消息排队协议构建的。它是目前在大型系统中用于异步通信的最流行的工具之一。像Reddit、9GAG、HelloFresh这样的公司,甚至股票交易所,每天都在使用这种优秀工具的力量在他们的系统中产生和消费数百万条消息。如果你想了解更多关于替代品的信息,我推荐你去亚马逊SQS或者阿帕奇卡夫卡看看。出于Python工具的目的,我们将使用为RabbitMQ创建的pika包。

pipinstallpika让我们来看看它是如何工作的核心概念。

生产者是RabbitMQ的一部分,他们将组装和发布我们希望异步消费的消息。在大多数情况下,这些代码将存在于您的Django服务中。让我们继续我们之前介绍的用户删除和GDPR问题。在我们的用户视图集中,我们将通过API发布一条关于用户被删除的消息。

首先,在清单4-12中,我们将创建一个小的助手类,这样我们可以用一种简单的方式进行制作。

#pizza/utils/producer.pyimportjsonimportpikaimportsysfromdjango.confimportsettingsclassProducer:def__init__(self,host,username,password)self.connection=pika.BlockingConnection(pika.URLParameters(f'amqp://{username}:{password}@{host}:5672'))self.channel=connection.channel()self.exchanges=[]defproduce(exchange,body,routing_key="):ifexchangenotinself.exchanges:channel.declare_exchange(exchange=exchange)self.exchanges.append(exchange)self.channel.basic_publish(exchange=exchange,routing_key=routing_key,body=json.dumps(body))producer=Producer(host=os.environ.get('RABBITMQ_HOST'),username=os.environ.get('RABBITMQ_USERNAME'),password=os.environ.get('RABBITMQ_PASSWORD'),)Listing4-12BasicpublisherforRabbitMQ这里需要解释几件事:

我们可以使用上面的代码,如清单4-13所示:

frompizza.utils.producerimportproducer...classUsersViewSet(viewsets.ModelViewSet):queryset=User.objects.all()serializer_class=UserSerializerdefdestroy(self,request,*args,**kwargs):user=self.get_object()response=super().destroy(request,*args,**kwargs)producer.produce(exchange='user',body={'user_id':user.id},routing_key='user.deleted')returnresponseListing4-13Usingthebasicpublishertopublishuserdeletedinformation现在,每当一个用户对象通过我们的API被删除时,我们将向用户交换发送一个用户被删除的消息。

现在,对该消息感兴趣的系统可以创建一个队列,并用给定的路由键将它绑定到交换机。清单4-14中有一个简单消费者的实现:

importjsonfromdjango.core.management.baseimportBaseCommand,CommandErrorfrompizza.modelsimportLikesfrompizza.utils.consumerimportconsumerclassConsumeUserDeleted(BaseCommand):help="ConsumesuserdeletedmessagesfromRabbitMQ"def_callback(channel,method,properties,body):payload=json.loads(body)user_id=payload.get('user_id')ifuser_idisnotNone:likes=Likes.objects.filter(user_id=user_id)likes.delete()defhandle(self,*args,**options):consumer.consume(exchange='users',queue='users-deleted',routing_key='user.deleted',callback=self._callback,)Listing4-15Basicconsumerusage现在我们已经创建了一个简单的分布式消息传递系统。有趣的是,消费者和生产者可以生活在不同的机器上,甚至不同的云提供商。例如,您可以在自己的机器上设置一个系统,使用来自exchange的消息进行调试。尝试通过API删除用户,看看系统的其余部分如何优雅地处理请求。在下一节中,我们将研究一些在您将异步排队系统引入您的架构时值得遵循的最佳实践。

到目前为止,我们看到的所有这些东西似乎都很简单。然而,当你开始大规模工作时,可能会有点混乱。我们将介绍一些在构建这些异步组件时应该考虑的最佳实践。

最困难的事情之一是确保当消息生产者改变消息有效负载时,消费者不会感到困惑并遇到异常。为此,对您的有效负载进行版本化可能是个好主意,就像我们对RESTAPIs进行版本化一样。

有多种方法可以对您的有效负载进行版本控制。最常见的方法是将消息的版本添加到路由关键字中。一些论坛建议对消息版本使用语义版本化,这使得路由关键字看起来像这样:

user.deleted.1.2.3分解:

现在,如果你想让事情变得简单,你可以只保留1个数字,上面列表中的主要数字。那样的话,工作量会少一点。

让我们做一个简短的练习,其中有两个团队:auth团队和likes团队。出于安全原因,auth团队决定将用户标识符从数字改为uuid。这意味着消息也需要更新。那么,在这种情况下,版本迁移是什么样的呢?

我知道这听起来很理想化,但是,这是您可以确保在突破性特性的实现过程中不会出现中断的方法。

排队系统的一个问题是代理中断。基本上,当中央排队组件停止工作时,可能会出现无数问题:

为了避免这样的灾难,您需要做的第一件事就是在您的代理集群上建立适当的监控和警报系统。您越早知道停机,就能越快做出反应。如果你有资源,一个更好的解决方案可能是将你的经纪人托管业务外包给专业人士。如果这不是你的核心能力,你可能不应该试图去设计它。

另一个可以在内部实现的解决方案是使用各种设计模式来提高集群的弹性。您需要保护的最重要的事情是数据完整性,为此,有一种称为发件箱的模式。

你们中的一些人可能熟悉发件箱模式,对于那些不熟悉的人,这里有一个复习:发件箱是一个软件工程模式,主要用在发送同步或异步消息的应用中,其中所有的消息都存储在一个存储中,然后由一个外部进程从给定的存储中调度。调度程序通常被称为发件箱工作程序。图4-6架构的快速概览。

图4-6

发件箱架构

正如您所看到的,异步通信是一个非常强大的工具,可以最大限度地提高速度、效率并进一步解耦您的系统。在我们进入下一章之前,我想提一下异步通信的缺点。

如果您的团队决定采用代表上述描述的问题的解决方案,我建议为最依赖数据的服务(在本例中为C)构建一个强大的重新排队逻辑。如果C能够处理来自其他服务的错误,您就可以开始了。然而,在现实生活中,竞争条件的复杂性可能比这里解释的要高得多。我还建议始终监控连接、未被确认的消息、异常和队列吞吐量,以确保数据不会丢失。

我们已经讨论了很多关于系统中各种服务之间的通信。所有的交流方式都有其优点和缺点。每一种情况都要求你重新考虑你要用什么工具来解决给定的问题。在构建微服务时,固定通信层可能是最大的挑战。有了这些工具,你肯定不会犯巨大的错误。

现在,我们已经了解了我们在服务中的目标是什么,以及我们如何将它们相互联系起来,是时候让我们更仔细地了解实际技术了,这将有助于我们从单一应用迁移到微服务架构。请注意,这里描述的技术不是灵丹妙药,您肯定需要根据自己的使用情况对它们进行修改,但是,经验表明,这些通用方法为成功的迁移提供了一个极好的起点。请记住,本章中描述的一些步骤是并行的,所以如果有更多的人来帮助你,那么可以加快一点速度。

到目前为止,您可能已经理解了将您的单片系统迁移到微服务并不只是在公园里散步。将会有严重的人力和财力成本。甚至估计交付给你的涉众也可能是一个巨大的困难(至少在开始的时候),所以让我们来看看你需要计算的基本成本。

自然,我们谈论的主要是重构代码库的成本。在项目的早期阶段,您将需要比迁移多个组件时多得多的努力。一开始对你的估计要非常保守,在你准备好工具之后,对你自己和你的团队要更严格一点,我们将在第五章和稍后的第六章中讨论。

根据我的经验,有两个领域的迁移可能会非常困难,并且可能会显著增加您的迁移的编码成本:

根据您的公司和单一应用的规模,最好有一个专门的团队来为其他团队处理工具、文档、指南和最佳实践,这些团队拥有组件迁移的领域知识。如果你和数百名工程师一起操作数百万行代码,这几乎是必须的。如果你的规模稍小,这可能是一个方便。

现在,让我们看看我们需要实施的硬件和基础架构的成本。

我们的tizza应用运行在两个10核机器上,内存为128。在迁移规划期间,我们已经确定了6个系统,我们可以在逻辑上将应用分成6个系统。现在,让我们来计算一下:

根据系统的负载,我们需要单核或双核机器来提供新服务。处理认证等的系统可能需要两个内核和8gb的ram,而披萨元数据存储可能只需要一个内核和4gb的RAM。我们可以将整个集群的CPU数量平均为8,总内存成本为32gb。因为,我们曾经用2台机器来处理monolith,我们也应该提高这里的数字,我们毕竟不想降低弹性。

当您试图将您的系统缩减为更小但更高效的部分时,缩小您的集群规模并低估安全运行您的软件所需的原始功率是一种非常人性化的反应。在创建新的微服务器时,我喜欢遵循的一般经验法则是在不同的(虚拟或物理)机器上运行服务的3个副本,以实现高可用性。

对于自信的人来说,有了优秀的云提供商、超轻量级的应用和配置良好的自动伸缩系统,就可以消除上述说法。

正如您所看到的,我们将系统中的内核总数从20个增加到了24个,内存保持在128左右,总计为96个。您将很快注意到,在现实生活环境中,这些数字往往会比预期增长得更快,并且取决于您的提供商,这可能会给您的业务带来毁灭性的成本。

我的建议是,为了安全起见,在开始的时候过犹不及,并不时地重新审视你的应用,以确保硬件不会对软件造成过度破坏。

到目前为止,在阅读本书时,你脑海中出现的最大问题可能是如何说服你的公司,这对他们来说是一项值得的投资,而不仅仅是一个有趣的重构。这是一场旷日持久的辩论,没有灵丹妙药。我会试着给你一些建议,这样你就可以开始了:

既然我们已经了解了任务的成本,那么是时候开始迁移我们的应用了,首先,通过准备数据。

在我们进入重构应用的有趣部分之前,我们需要确保我们想要传输的数据是可传输的,这意味着它很容易从一个地方复制到另一个地方,并且不会与系统中的其他数据域耦合太多。除此之外,我们需要找到从领域和业务角度看似乎生活在一起的数据集群。

如您所见,我们可以识别出以下共存的数据块:

领域规定了上述内容,然而,在上述内容的某些部分之间仍然有许多硬耦合。让我们来看看比萨店的模式:

classPizzeria(models.Model):owner=models.ForeignKey(UserProfile,on_delete=models.CASCADE)address=models.CharField(max_length=512)phone=models.CharField(max_length=40)如您所见,我们在用户配置文件模型的owner字段上有一个硬外键规则。您需要做的第一件事是确保这些外键将指向虚拟对象,其中外部对象可以被认为是这样的引用:

classPizzeria(models.Model):owner_user_profile_id=models.PositiveIntegerField()address=models.CharField(max_length=512)phone=models.CharField(max_length=40)为什么这是有益的?现在,对象之间的耦合性降低了,并且更加基于信任。比萨饼将信任系统,有一个用户具有给定的用户配置文件id,并可以生活在他们自己的独立环境中。不再有硬编码的数据库规则将比萨饼和用户资料绑定在一起,这非常解放,但同时也非常可怕。

我们失去了什么?

自然,您可以(也应该)保持驻留在同一个数据库中的模型之间的耦合,这样您就保留了方便的方法,并为您的查询提供了一些速度和可靠性。

如果您不是数据库专家,这似乎是一项艰巨的任务。然而,你可能记得我们在第二章中了解到的一个强大的工具,叫做迁移。您可以非常容易地创建一个新的迁移,用标识符替换外键。清单5-1为比萨店提供了一个范例。

defset_defaults(apps,schema_editor):Pizzeria=apps.get_model('pizza','pizzeria')forpizzeriainPizzeria.objects.all().iterator():pizzeria.owner_user_profile_id=pizzeria.owner.idpizzeria.save()defreverse(apps,schema_editor):passclassMigration(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',),]Listing5-1Examplemigrationfrommodeltoid让我们仔细看看这段代码。在我们更改了模型并运行了makemigrations命令后,系统会提示我们为我们创建的新字段提供一个默认值,这里我们可以给0,这不会有太大影响。为了确保所有的值都设置正确,我们将以上述方式修改迁移代码。逻辑如下:

你可以使用上面的模板来迁移几乎所有的文件。对于行数较多的表(即将整个数据库加载到内存中是很危险的),强烈建议将set_defaults函数中的查询改为批量操作。或者,对于非常大的表(我们这里讨论的是数百万个业务关键行),您可能希望让数据库专家来帮助迁移。

您可能会有这样的预感,如果您运行这个迁移,一切都会崩溃。嗯,这完全是真的。所有pizzeria对象上的owner字段将从那里开始破坏您代码库中的代码,这可能会引起一些麻烦。理想情况下,您将更改代码库中的所有代码,以使用为获取所有者对象而创建的新字段,然而,有一些方法可以保护我们不被破坏,例如使用Python属性,请参见下面的清单5-2。

classPizzeria(models.Model):owner_user_profile_id=models.PositiveIntegerField()address=models.CharField(max_length=512)phone=models.CharField(max_length=40)@propertydefowner(self):returnUserProfile.objects.get(id=self.owner_user_profile_id)Listing5-2Usingpropertiesasmodelfields以上述方式使用属性可以大大加快迁移过程,但是,从长远来看,它可能会导致问题,特别是性能方面的问题,因为我们刚刚从一个非常高效的数据库连接操作转移到另一个要执行的查询。但是,您稍后会注意到,这并不是我们将获得的最大速度提升。让我们看一下迁移的后续步骤,我们将确保新旧系统都可以访问数据。

在决定了要迁移应用的哪一部分并相应地修改了数据库之后,就该在数据库级别上建立迁移计划了。也就是说,是时候准备您的新数据库来托管您的模型了。

也许最简单的开始方法是建立主数据库的副本。我们的想法是将所有写入内容拷贝到复制副本,该复制副本将用作只读。不要太担心只为特定的表设置复制,大多数时候这只会带来麻烦和额外的工作。通常更简单的方法是设置一个完整的复制,并在迁移准备就绪时,从新数据库中删除不需要的表。

您还可以在两个数据库之间设置主-主复制,但是,这种技术需要大量的数据库专业知识,并且为发布后的错误提供了更多的空间。

我们已经做了一些准备。现在是时候复制所有代码了…开个玩笑。理想情况下,这是您确保在将代码从一个系统迁移到另一个系统时应用不会中断的地方。

要做到这一点,你可以使用的最有用的工具就是测试。Django自带内置的测试框架,你可以很容易地测试数据库级别的测试,包括内存数据库,然而,任何单元测试框架都可以完成这项工作,比如单元测试2、pytest或nose。

当谈到如何衡量你在测试方面做得好不好时,许多团队和工程师推荐使用像coverage这样的工具,用它你可以衡量你在应用中测试过的代码行数。然而,这个度量并不总是测量您的测试的真实价值。建议您覆盖视图和模型的核心业务功能。理想情况下,如果您在运行后端应用时暴露了一些外部通信方法,那么您还可以实现集成测试,测试整个端点或消费者提供的功能。如果你有人员,那么你也可以实施验收测试,这通常是非常高水平的测试,自动机器人点击通过你的网站,检查基本用户流是否成功。这些系统通常非常脆弱,维护起来也很昂贵,但是,在一个关键的bug投入生产之前,它们可以作为最后一道防线拯救生命。cucumber是一个优秀的验收测试框架,你可以在cucumber.io上了解更多。

既然我们已经用测试覆盖了代码,是时候开始使用一些工具了,这样我们就可以将代码库从一个地方迁移到另一个地方。

到目前为止,我们对想要迁移的模型做了一些工作,并准备了一个新的数据库。是时候开始实际迁移代码了。

在我们能够复制想要在独立系统中运行的代码库部分之前,我们需要确保两个代码库之间的依赖关系是可管理的。到目前为止,我们已经了解到Django和Python是构建和维护服务的非常灵活的工具,然而,我们也了解到对模型形式的数据有很大的依赖性。考虑清单5-3中的代码片段,我们希望将它迁移到一个单独的服务中:

frompizza.modelsimportLikefromuser.modelsimportUserProfiledefget_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_namelikes=Likes.objects.count()returnfull_name,likesListing5-3Problematicfunctiontoextract无论我们想从哪个服务中提取上面的代码,我们都会面临一个两难的境地。函数中存在对模型的交叉引用,这可能很难解决。如果我们想避免数据重复和清理领域,我们需要确保喜欢和用户配置文件不驻留在单独的服务和数据库中。为此,我们可以做一个重构技术,我们称之为远程模型。

远程模型是我在职业生涯中多次遇到的一个概念,它们是真正的救星。这个想法是,如果你的API是统一的,你可以很容易地用远程调用替换你的数据库模型调用,在你的代码库中使用简单的搜索和替换(至少在大多数情况下)。参见清单5-4中的远程模型实现示例。

我们将看到的代码可能不完全符合您的需求,但它是一个很好的起点,可以让您开始用远程模型思考问题。

ENTITY_BASE_URL_MAP是一个方便的映射,您可以在设置文件中创建它来为您正在处理的每个实体指定唯一的URL基础。

到目前为止,所有这些都很简单。那么诀窍在哪里呢?您可能已经注意到,在创建远程模型的实例时,请求对象是一个必需的参数。这是为什么?简单地说,我们使用请求对象来传播我们在请求本身中收到的头。这样,如果您使用头或cookies进行身份验证,所有内容都将顺利传播。

在此之后,这些模型的使用应该是相当容易的。为了方便起见,您可以根据您的特定需求对RemoteModel进行子类化,就像我们在清单5-5中所做的那样:

classRemotePizza(RemoteModel):def__init__(self,request):super().__init__(request,'pizza','v1')Listing5-5Simpleremotepizza然后,您可以在视图函数中执行以下操作,如清单5-6所示:

pizza=RemotePizza(request).get(1)pizzas=RemotePizza(request).filter(title__startswith='Marg')RemotePizza(request).delete(1)Listing5-6Examplesofremotepizzausage注意过滤器函数需要在服务器端进行额外的实现,因为DjangoREST框架默认不支持它们。

远程模型的缺点:

远程模型实现过程中出现的另一个好话题是缓存。缓存是一个很难解决的问题,所以我建议您不要在第一次迭代中实现它。我多年来注意到的一个简单而巨大的成功是在您的服务中实现请求级缓存。这意味着,每个远程调用的结果都以某种方式存储在请求中,不需要再次从远程服务中获取。这允许您在一个视图函数中从您的服务对同一个资源进行多个远程模型调用,而不实际使用网络来获取资源。这可以节省大量的网络流量,即使在开始。

让我们看一下练习5-1、5-2和5-3,它们将帮助我们更多地使用远程模型。

上面的模型很好地解决了头和cookie传播的问题,因此我们可以使用认证方法(如我们在前面章节中看到的会话或认证头)从系统的各个点访问数据。然而,如果我们想在没有用户认证的情况下调用服务时使用不同的令牌,这可能会导致问题。在本练习中,鼓励您为RemoteModel设计一个扩展,通过它我们可以正确地分配覆盖身份验证令牌。上面已经有一些代码可供您使用。

远程模型看起来已经是一个非常强大的工具了,但是我们能让它们更加强大吗?当模型在本地数据库中可用时,尝试扩展RemoteModel类,以便能够处理数据库调用。进行这一更改可以让您在未来加快迁移速度。

这可能是整个迁移过程中最不激动人心的部分。您需要复制您希望其他系统拥有的代码库。您需要为这些应用创建一个新的Django项目,找到设置和实用程序,并复制所有内容。当我处于迁移的这个阶段时,我喜欢遵循以下几个提示:

代码被复制,所有的测试都通过了,是时候发布了。

新微服务的首次部署总是有点混乱,并且需要事先就策略和方法进行大量讨论。正如本书中的大多数地方一样,发布过程没有灵丹妙药,但是,有一些方法可以选择,这取决于您的准备、工具和您的团队是否愿意早点醒来。

先读,后写——这种策略意味着微服务将首先只以只读模式运行,也就是说其上的流量不会修改它所拥有的数据。这是我最喜欢的策略之一,它允许你同时使用monolith和新的微服务来访问数据。如果您选择设置新数据库的读取复制,那么使用新服务提供的读取功能的API应该是相当安全的,例如获取pizza元数据。这样,您可以确保您的应用在生产环境中运行,并且只有在您确信您的基础架构能够处理它时,才开始在其中写入数据。

滚动部署-基本上意味着你将把总流量的一部分发送到新的微服务,而将其余部分留在monolith上,缓慢但稳定地让所有流量由新系统处理。借助现代负载平衡器和服务网格,这可以很容易地建立起来。如果您选择创建一个读取副本,这不是一个选项,因为在新的微服务数据库上发生的写入不会在monolith的数据库中注册。

全流量变化——可能是最容易实现也是恢复最快的。当您确信您的服务工作正常时,您可以将给定url上的流量切换到新服务。这个过程应该简单且容易逆转,例如改变网站或文件的配置。

既然我们已经知道了发布我们的服务应该采用什么样的策略,那么让我们来看看当事情不可避免地发生时,我们该如何应对。

这里有一个在遭受攻击的情况下恢复tizza应用的示例剧本。我们的目标是做发布的人不需要考虑任何事情,只需要按照说明去做。

第二步还有一个针对公司其他部门的通信计划。这是绝对重要的,因为如果出了什么差错,你公司的其他人也会感兴趣。

我们做到了!应用已经迁移,但是我们还没有完全准备好。最有趣的部分还在后面。让我们谈谈如何确保我们不会留下一个巨大的烂摊子。

图表和日志看起来很棒。客户没有抱怨任何新的问题,系统是稳定的。恭喜你,你刚刚发布了一个新的微服务!现在最有趣的事情来了:收拾我们留下的烂摊子。

就像你处理你的厨房一样,确保你不会在旧代码库中留下不需要的东西。您可以慢慢来,事实上,将旧代码保留2-3周通常是一个好主意,因此如果有一些问题,您仍然可以使用您创建的行动手册恢复到旧逻辑。

在这一章中,我们学到了很多可以用来加速微服务迁移的小技巧。与此同时,我们的新系统也搞得一团糟。有许多重复的代码,仍然不清楚谁拥有应用的哪些部分。在下一章中,我们将更深入地探讨这一话题,并确保我们不仅能增加我们拥有的服务数量,还能扩展我们的组织和开发,以利用这些系统实现最佳效率。

我已经多次提到的一件事是,在我们上一章所做的假设迁移中,我们一直在处理大量的代码重复。每个软件工程师都知道DRY原则(如果你不知道,现在就去查一下),所以希望你们中的一些人对复制这么大量的代码感到不舒服。

在第一部分中,我们将探讨重用代码的另一种方式。通过使用Python包。

您可能会问自己的第一个问题是,您应该为什么创建一个新的包?答案通常是:无论两个或更多微服务中存在什么代码,都应该迁移到一个单独的包中。有时也有例外,例如,如果您预计在短期或长期内重复的代码会有严重的代码漂移,那么您应该将它们保存在单独的服务中,让它们各自漂移。我们在本书中使用的一些例子应该放在不同的包中:

当然,还有其他的例子。如果您的系统有一个定制的日期模块,那么您可能也想把它作为一个包来分发。如果您有Django基础模板,并希望在服务中分发,那么包是最适合您的。

在您决定了首先将哪个模块移入包中之后,您可以通过在您最喜欢的代码管理工具中创建一个新的存储库来开始迁移过程。但是,在此之前,强烈建议检查您想要移出的模块的内部和外部依赖关系。从长远来看,依赖于包而不处理向后兼容性的包通常会带来麻烦,应该避免(特别是如果依赖是循环的,在这种情况下,无论如何都要避免)。

如果您已经隔离了想要移出的代码,那么您可以创建一个新的存储库,并将您想要的代码迁移到一个单独的包中。确保也移动测试,就像您在迁移服务时所做的那样。迁移后,您应该拥有如图6-1所示的目录结构。

图6-1

基本包目录结构

让我们一个文件一个文件地检查一下:

tizza-我们想要保存包源代码的目录。您可能想知道当有多个包时会发生什么?答案是Python模块系统很好地处理了同名模块,并如您所料加载了它们。一般来说,用一个像你的公司名称这样的前缀作为你的包的前缀是一个好主意,因为它可以是你的导入中的一个很好的指示器,不管一个特定的方法是否来自于一个包,而且,如果你将来开源这些包,它可以是一个营销你的公司的好方法。

tests——我们保存测试源代码的目录。

fromsetuptoolsimportsetup,find_packagesVERSION="0.0.1"setup(name="auth-client-python",version=VERSION,description="Packagecontainingauthenticationandauthorizationtools",author_email='akos@tizza.com',install_requires=['djangorestframework==3.9.3',],packages=find_packages())Listing6-1Anexamplesetup.pyfileforapackage如你所见,这很简单。name属性包含包本身的名称。这个版本是这个包的当前版本,它作为一个变量被移出,所以更容易修改它。这里有一个简短的描述,还有软件包所需的依赖项。在本例中,是DjangoREST框架的3.9.3版本。

使用一个新的包并不比创建一个更难。由于pip可以从各种代码托管站点下载包,比如Github,我们可以简单地将下面一行插入到requirements.txt文件中:

git+git://github.com/tizza/auth-client-python.git#egg=auth-client-python运行pipinstall-rrequirements.txt现在将按照预期安装软件包。

这里我们可以提到的另一件事是关于包的版本控制。在本书的前面,我们已经提到,固定的依赖关系(有固定版本的依赖关系)通常比非固定的依赖关系更好,因为开发人员可以控制他们的系统。现在,在这里,你可以看到,我们总是拉最新版本的代码库,这违背了这个原则。幸运的是,pip支持特定的包版本,即使它们来自代码版本控制系统,而不是“真正的”包存储库。允许使用以下引脚:标记、分支、提交和各种引用,如拉请求。

pipinstallgit+git://github.com/tizza/auth-client-python.git@master#egg=auth-client-python幸运的是,发布一个新标签非常容易,您可以用清单6-2中的bash脚本来完成:

#!/bin/shVERSION=`grepVERSIONsetup.py|head-1|sed's/.*"\(.*\)".*/\1/'`gittag$VERSIONgitpushorigin$VERSIONListing6-2examplebashscriptforpublishingtags这个脚本从您的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=pytestcommands=pytestListing6-3Simpletoxfile这个文件的意思是,我们希望使用命令pytest针对Python版本2.7、3.6和3.7运行我们的测试。该命令可以用您正在使用的任何测试工具来替换,甚至可以用您编写的定制脚本来替换。

您只需在终端中说:tox就可以运行tox。

这到底是什么意思?当你在一家在整个生态系统中使用多个Python版本的公司开发软件时,你可以确保你正在开发的包在所有版本中都可以工作。

现在我们已经了解了包,它们应该如何被构造和测试,让我们看看如何存储关于我们的服务的元信息,这样公司的开发人员可以更容易地以最快的方式找到他们需要的信息。

随着您的系统随着越来越多的微服务而增长,您将面临与单一应用不同的问题。其中一个挑战是数据的可发现性,这意味着人们将很难找到系统中的某些数据。良好的命名惯例在短期内有助于这一点,例如,将存储披萨信息的服务命名为食品服务或烹饪服务可能比命名为戈登或冰箱更好(然而,我确实同意后两者更有趣)。从长远来看,您可能想要创建某种元服务,它将托管关于您的生态系统中的服务的信息。

服务存储库总是需要为给定的公司量身定制,但是,在默认情况下,您可以在模型设计中涉及一些东西,如清单6-4所示。

fromdjango.dbimportmodelsclassTeam(models.Model):name=models.CharField(max_length=128)email=models.EmailField()slack_channel=models.CharField(max_length=128)classOwner(models.Model):name=models.CharField(max_length=128)team=models.ForeignKey(Team)email=models.EmailField()classService(models.Model):name=models.CharField(max_length=128)owners=models.ManyToManyField(Team)repository=models.URLField()healthcheck_url=models.URLField()Listing6-4Basicservicerepositorymodels我们把它保持得很简单。我们的目标是让团队和工程师能够互相交流。我们创建了一个描述系统中的工程师或所有者的模型,我们还创建了一个描述团队的模型。一般来说,最好将团队视为所有者,它鼓励这些单位在团队内部共享知识。团队也有一个slack频道,理想情况下,它应该是一个单击连接,任何工程师都可以获得关于服务的信息。

您可以看到,对于服务模型,我们添加了几个基本字段。我们已经将owners设置为多对多字段,因为多个团队可能使用相同的服务。这在较小的公司和单一应用中很常见。我们还添加了一个简单的存储库url字段,因此可以立即访问服务代码。此外,我们还添加了一个健康检查url,因此当有人对该服务是否正常工作感兴趣时,他们只需简单地点击一下即可完成。

拥有关于我们服务的基本元信息固然很好,但现在是时候添加更多日常使用的内容了。

现在,我们已经开始了这一部分,工程师可以寻找的最有趣的元数据之一是系统中存在的特定实体的位置以及如何访问它。为了在这个维度上扩展我们的服务存储库,我们还需要扩展现有服务的代码库。

你可以要求你的团队做的第一件事是开始记录他们的通信方法。这意味着对于每个团队的每个服务,应该有某种形式的文档来描述给定服务存在什么实体、端点和消息。作为初学者,您可以要求您的团队在其服务的自述文件中包含这一点,但在这里,我们将了解更多选项。

SwaggerAPI项目由TonyTam于2011年启动,目的是为各种项目生成文档和客户端SDK。该工具已经发展成为当今业界正在使用的较大的RESTfulAPI工具之一。

Swagger生态系统的核心是一个yaml文件,它描述了您想要使用的API。让我们看看清单6-5中的一个示例文件:

每条路径都被分解为方法,您可以看到我们为pizza/端点分配了一个POST方法来创建pizza。我们还描述了可能的响应以及它们的含义,包括文件末尾的pizza对象的结构。这些定义还包括接受什么类型的数据,以及可以从某些端点返回什么类型的数据。在这种情况下,我们所有的端点只返回application/json作为响应。

图6-2

傲慢的编辑

Swagger编辑器是一个动态工具,使用它创建Swagger文件非常容易和有趣。它还将文件验证为Swagger格式,这样可以确保API描述符文件保留在生态系统中。

您还可以在自己的系统中利用SwaggerUI(上图中的右边面板),如果愿意,您可以下载源代码并将其托管在服务存储库旁边,这样您就可以对API描述符和想要了解它的人拥有最终的控制权。

Swagger对于同步API来说绝对是不可思议的,但是它不支持许多异步特性。在下一节中,我们将了解另一个可以帮助我们做到这一点的工具。

与您的同步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-serviceversion:'0.0.1'description:|AMQPmessagespublishedandconsumedbytheculinaryservicedefaultContentType:application/jsonchannels:user.deleted.1.0.0:subscribe:message:summary:'Userdeleted'description:'Anotificationthatacertainuserhasbeenremovedfromthesystem'payload:type:'object'properties:user_id:type:"string"pizza.deleted.1.0.0:publish:message:summary:'Pizzadeleted'description:'Anotificationthatacertainpizzahasbeenremovedfromthesystem'payload:type:'object'properties:pizza_id:type:"string"Listing6-6AsyncAPIexampledescriptorfile这里的规范非常简单。我们有两个路由键,一个用于删除用户,这是我们消费的,另一个用于删除披萨,这是我们生产的。消息本身描述了消息的结构,但是,我们也可以创建类似于Swagger描述文件中的对象。

图6-3

AsyncAPI编辑器

是时候把我们新的闪亮的API描述符放到我们的服务存储库中了。

在这些文件就位之后,我们可以开始将它们链接到我们的服务存储库中。清单6-7向您展示了如何将它们作为额外字段添加到服务模型中。

classService(models.Model):...swagger_file_location=models.URLField()asyncapi_file_location=models.URLField()Listing6-7Updatedservicemodel这些新链接将使我们能够即时访问所有服务中的API用户界面。如果我们愿意,我们还可以扩展模型来包含并定期加载URL的内容,我们可以在用户界面上对这些内容进行索引以便进行搜索。

现在,如果您的服务存储库中有这么多可用的数据,那么您在行业标准方面已经做得很好了,并且为您的工程师提供了非常先进的工具,以便在您复杂的微服务架构中导航。您可能希望在将来添加几个字段,所以我会在这里为您留下一些想法,您可以在将来改进:

图表、警报和跟踪-向服务存储库添加图表、警报和服务跟踪信息是一个简单的方法。这些通常是简单的URL,但是,如果你想变得更有趣,你总是可以将图形嵌入到一些UI元素中,这样开发服务的开发者一眼就能了解服务的状态。

日志-维护和使用日志对于每个公司都是不同的。但是,有时很难发现给定服务的日志。您可能希望将文档、链接甚至服务的日志流(如果可能的话)包含到存储库中。对于那些试图找出服务是否有问题,但又不太熟悉的工程师来说,这可能会加快速度。

依赖关系健康-自从2016年JavaScript生态系统的巨大丑闻以来,当一半的互联网因为一个依赖关系(左键盘)在节点包管理器中被禁用而崩溃时,人们非常重视依赖关系,你可能也想搭上火车。您可以使用一些工具来确定服务中依赖项的最新程度和安全性。例如,您可以使用安全来实现这一点。

构建健康状况(Buildhealth)——有时,如果服务的构建管道健康与否,这可能是有用的信息。如果需要,这也可以显示在服务存储库UI上。

如您所见,服务存储库是非常强大的工具,不仅可以发现服务,还可以很好地概述生态系统的健康状况和整体性能。

在最后一节中,我们将快速看一下如何利用脚手架的力量加速开发新服务。

我们已经在如何扩展我们的应用开发方面取得了很大进展。在结束本书之前,我们可能还想做一小步,那就是搭建服务。

当设计微服务架构时,您可以瞄准的最终目标之一是使团队能够尽快交付业务逻辑,而不中断技术领域。这意味着建立一项新的服务(如果业务需要)应该是几分钟的事,而不是几天的事。

搭建服务的想法并不新鲜。有许多工具可以让您编写尽可能少的代码,并尽可能少地点击您的云界面,以构建您的开发人员可以使用的新服务。让我们从前者开始,因为它与我们一直在研究的内容非常接近。

**基本依赖关系-可能是由需求文件暗示的,但是,我想强调一下基本依赖关系。这些依赖的主要目标是保持公共代码的完整性,而不是被公司的多个团队重写。您之前提取的包应该包含在基本依赖项中,如果不需要它们,可以从长远来看删除它们。

*readmes和基本文档-基本模板还应该包括Readmes和其他文档文件的位置,例如默认情况下的Swagger和AsyncAPI。如果可能的话,用于模板更新的脚本也应该鼓励以某种方式填写这些信息。**健康检查和潜在图表*-服务的构架还应该包括如何访问服务以及如何检查服务是否工作。如果您正在使用Grafana这样的工具,这些工具使用JSON描述符文件构建服务图,那么您也可以在这里生成它们,这样所有服务的基础图看起来和感觉起来都是一样的。*Dockerfiles和Dockercomposefiles——我们在本书中没有过多地讨论Docker和围绕它的生态系统,但是,如果你正在使用这些工具,你一定要确保你正在搭建的服务默认包含这些文件。**每个人都应该可以接触到基础脚手架。我建议在你最喜欢的代码版本系统中创建一个新的存储库,用模板和脚本填充其中的模板,并让公司的所有开发人员都可以访问它。如果您愿意,也可以留下一个示例服务。

对于脚手架本身来说。我推荐使用非常简单的工具,比如Pythoncookiecutter模块。

我想在这里指出的一点是,脚手架会在短期内加快你的速度,然而,从长远来看,它会导致另一系列的问题。确保我们生成的所有这些文件在整个微服务生态系统中保持统一和可互换几乎是不可能的。在这一点上,如果您想使用一个健康的、可维护的基础设施,建议让专门的人来处理您的系统的统一性和可操作性。这种最近蓬勃发展的工程文化被称为“开发人员体验”我建议对它进行研究,评估适应对你和你的公司是否值得。

搭建代码库是一件事,另一件事是确保你的云提供商有资源来托管你的系统。以我的经验来看,在公司生命周期的每个周期,每个公司的这个领域都是极其不同的。因此,为了方便起见,我将提供一些指导并提到一些您可以在这里使用的工具。

HashiCorp的Terraform是一个非常强大的工具,用于维护基础设施代码。基本思想是,Terraform定义了各种提供者,如AmazonWebServices、DigitalOcean或GoogleCloudPlatform,在这些提供者中,所有资源都以类似JSON的语法描述。参见清单6-8中的地形文件示例:

Chef-Chef是最受欢迎的代码解决方案基础设施之一,被全球数百家公司(如脸书)用于增强其基础设施。Chef使用Ruby编程语言的强大功能来扩展。

在这一章中,我们已经了解了如何使用Python包,以及如何使用它们来确保开发人员不会在每次创建新服务时都重新发明轮子。我们还学习了服务存储库,以及如何通过创建同步和异步消息传递系统的详细文档来帮助自己。我们还了解了脚手架服务,以及我们希望工程师在创建新服务时使用的模板的最低要求。

我希望这一章已经为您提供了关于如何在您的组织中扩展微服务开发的有用信息。*

THE END
1.菜单设计制作菜单模板图片这是一个关于菜单设计的专题页面,包含了菜单设计的基本原则、技巧,以及相关的在线设计工具、设计模板等内容。通过本专题,您可以学习如何快速创建专业的菜单设计,并打造与众不同的独特菜单设计。无论您是餐厅、咖啡馆还是其他行业,都能从中获取灵感并提升您的菜单设计水https://www.gaoding.com/features/4
2.饭店菜单模板设计图片大全简单好看饭店菜单模板设计图片大全简单好看 熊猫办公精心为用户挑选81张高清精美饭店菜单图片、支持专业级饭店菜单设计素材下载,更多风格的饭店菜单,免抠元素,卡通手绘素材图片、图标图案、免抠矢量图,尽在熊猫办公。相关搜索:饭店菜单图片素材|饭店菜单大全图片电子版|饭店海报图片素材|饭店宣传图片素材|小饭店菜谱大全家常菜图片|https://www.tukuppt.com/speciall/fandiancaidan4455.html
3.家常菜点餐基于小程序的家庭大厨家常菜点餐系统设计与实现(源码+数据四、数据库设计 (1)管理员信息的实体属性图如下: 图4.12 管理员信息实体属性图 (2)客服实体属性图如图4.13所示: 图4.13 客服实体属性图 (3)菜品分类信息实体属性图如图4.14所示: 图4.14 菜品分类信息实体属性图 表4.1 菜品分类 表4.2 菜品信息 表4.3 在线客服 https://blog.csdn.net/m0_66468899/article/details/144337136
4.餐厅菜单设计免费餐厅菜单设计模板在线餐厅菜单制作设计软件MAKA是最受欢迎的免费在线餐厅菜单设计软件、餐厅菜单制作工具,提供精美的餐厅菜单设计模板素材,5分钟完成在线设计制作餐厅菜单。https://www.maka.im/zhinan/cantingcaidna
5.菜单在线制作图怪兽菜单在线制作专题为您精选菜单在线制作模板,包含菜单在线制作的图片素材等可根据您的需求选择,不同图片尺寸进行在线替换文字制作,即可一键生成一张正版可商用模板图片免费下载。https://818ps.com/shejiimg/12093-2.html
6.菜单菜谱设计模版图片ai平面设计模版素材免费下载详情页投诉 分享 爱给网提供海量的模板大全资源素材免费下载, 本次作品为ai 格式的菜单菜谱设计模版图片, 本站编号37300600, 该模板大全素材大小为2m, 更多精彩模板大全素材,尽在爱给网。 浏览本次作品的您可能还对 菜谱画册(封面)菜单画册按形式 感兴趣。https://www.aigei.com/item/cai_dan_cai_pu_23.html
7.高端菜谱素材高端菜谱图片高端菜谱模板在线制作魔力设图片素材库提供高端菜谱图片下载和在线设计,包括高端菜谱图片、高端菜谱素材、高端菜谱模板。即可下载PS源文件素材,也可以直接在线编辑制作,无需PS,1分钟快速出图。http://www.51mo.com/search/109964.html
8.墙上菜谱设计专题模板墙上菜谱设计图片素材下载我图网墙上菜谱设计专题为您整理了4162个原创高质量墙上菜谱设计图片素材供您在线下载,PSD/JPG/PNG格式墙上菜谱设计模板下载、高清墙上菜谱设计图片大全等,下载图片素材就上我图网。https://m.ooopic.com/sousuo/22269669/
9.食谱设计在线食谱制作食谱图片模板在线设计平台Canva可画提供了海量的食谱设计模板,覆盖绝大多数食谱设计场景,只需选择喜欢的模板进行制作,即可轻松在线设计出精美的食谱。https://www.canva.cn/create/recipe/
10.20多屏餐厅电子菜单销售点单系统网页用户界面设计ui套件模板颜格视觉提供精品网页界面设计下载,当前预览的商品为20多屏餐厅电子菜单销售点单系统网页用户界面设计ui套件模板,源文件格式:fig,容量:128 MB ,分辨率:矢量https://www.youngem.com/detail/14078
11.餐饮网站模板,美食网站模板网页设计分享一款餐饮网站模板,美食网站模板网页设计,可以作为餐厅网站,在线展示菜谱美食,也可作为美食网站,高质量的代码,很容易编辑。https://www.17sucai.com/pins/45038.html
12.菜谱折页模板制作教程(一)视频教程当前小节: 11-3 菜谱折页模板制作教程(一) 平面设计零基础就业班课程 免费试学 ¥99.50 贵族价 ¥199.00 课程咨询 开通贵族 购买课程 溜溜送你3天自学贵族,免费领取噢!购课立享贵族价~ 新用户首单立减10元 溜溜送你3天自学贵族,免费领取噢!购课立享贵族价~ 在看人数:66998 入群交流 ¥199.00 购https://zixue.3d66.com/course/60_25552.html
13.菜谱网站模版菜谱网站源码Service)是一款帮助您搭建网站的华为云服务。无需代码,自由拖拽,快速生成中小企业网站及网店、微信网店等。您可使用网站模板快速搭建网站,也可根据需要自行设计编辑网站,并负责网站最终的展示内容和效果。 立即购买 华为云网站搭建四种方式 您可以选择华为云搭建您的网站,华为云提供丰富的建站资源和建站方式。 https://www.huaweicloud.com/theme/713118-2-C-undefined
14.创意邀请函设计模板素材创意邀请函设计模板素材电子版创意邀请创意邀请函设计模板素材 创意邀请函设计模板素材电子版 创意邀请函设计模板素材制作软件 创意邀请函设计模板素材微信免费相关模板列表,快易邀请函您提供大量创意邀请函设计模板素材 h5邀请函、电子请柬、电子请帖模板。https://www.taozhuo.com/hots/509980.html
15.大图网大图网提供精品设计图片素材下载,内容包括高清图片素材,PSD素材,淘宝素材,影楼模板素材,矢量素材,免扣素材和中英文字体,致力于打造最好的免费素材共享平台http://www.daimg.com/
16.平面设计个人简历模板下载word格式在线制作简历 下载简历模板 个人简历 简小历女23岁2年150***8160880123229@qq.com平面设计一句话向HR介绍自己 教育背景 2012-09 到 2016-06 简历本财经大学 本科 工作经历 2017-02 到 2017-07 北京健力源餐饮管理有限公司 平面设计师 负责公司新项目开业的餐厅软装饰以及周年庆活动。公司其他宣传物料的设计。配https://www.jianliben.com/muban/detail/13214
17.美食天下美食天下是活跃的中文美食网站与厨艺交流社区,拥有海量的优质原创美食菜谱,聚集超千万美食家。我所有的朋友都是吃货,欢迎您加入!https://www.meishichina.com/
18.彩页,在线彩页设计,彩页模板下载,宣传单模板,飞印网彩页在线彩页设计,彩页模板下载,宣传单模板,飞印网提供在线名片设计、在线彩页设计,名片、彩页、画册等设计印刷服务,飞印网最专业的设计印刷服务网站https://www.92mp.com/dmlist
19.面馆菜单设计模板满座菜谱337号菜谱设计 ?陕西面食小食 点击看整套菜谱设计大图片 382号菜谱设计 ?陕西特色粥面店 点击看整套菜谱设计大图片 404号菜谱设计 ?秦美小食面馆 点击看整套菜谱设计大图片 406号菜谱设计 ?陕西面馆 点击看整套菜谱设计大图片 567号菜谱设计 ?醉香餐馆/面馆 点击看整套菜谱设计大图片 403号菜谱设计 https://www.manzuocaipu.com/mb43.htm
20.菜谱设计模板素材网站图片免费下载泸州菜谱设计模板,菜谱设计制作公司,捷达菜谱 6 捷达菜谱品牌设计 菜谱设计模板分享,乐山美食摄影图片分享,捷达菜谱 1 捷达菜单设计公司 菜谱制作设计模板,专业菜谱制作,菜谱制作供应商 2 捷达菜谱品牌设计 成都专业菜谱制作公司,菜谱制作模板参考,成都菜单设计 30 捷达菜谱品牌设计 成都酒水吧菜谱设计制作案例图片-菜谱https://www.zcool.com.cn/tag/ZNTM0MTY5Mg==.html
21.菜谱菜单设计菜谱菜单模板菜谱菜单图片觅知网为您找到960个原创菜谱菜单设计图片,包括菜谱菜单图片,菜谱菜单素材,菜谱菜单海报,菜谱菜单背景,菜谱菜单模板源文件下载服务,包含PSD、PNG、JPG、AI、CDR等格式素材,更多关于菜谱菜单素材、图片、海报、背景、插画、配图、矢量、UI、PS、免抠,模板、艺术字、Phttps://m.51miz.com/so-sucai/3057076.html
22.菜谱模板图片免费下载菜谱模板素材菜谱模板模板千图网为您找到2142张菜谱模板相关素材,千图网还提供菜谱模板图片,菜谱模板素材, 菜谱模板模板等免费下载服务,千图网是国内专业创意营销服务交易平台,一站式解决企业营销数字化、协同化,实现营销转化效果增长!https://www.58pic.com/tupian/caipumoban.html
23.m.redocn.com/psd/2394447.html空白红色中国风菜谱模板设计 高端中餐粤菜菜谱模板 西餐时尚菜单模板 经典西餐牛扒套餐菜牌模板设计 简约时尚自助餐画册模板设计 快餐汉堡三折页菜单菜谱 石锅鱼美食海报 西餐菜单设计模板 年夜饭菜单 铁板丁骨牛排套餐海报设计 铁板香辣牛排套餐海报设计 设计定制 红动网——专业在线设计服务平台 红动创办于2005年,老https://m.redocn.com/psd/2394447.html