微服务遵循领域驱动设计(ddd),与开发平台无关。python 微服务也不例外。python3 的面向对象特性使得按照 ddd 对服务进行建模变得更加容易。
微服务架构的强大之处在于它的多语言性。企业将其功能分解为一组微服务,每个团队自由选择一个平台。
我们的用户管理系统已经分解为四个微服务,分别是添加、查找、搜索和日志服务。添加服务在 java 平台上开发并部署在 kubernetes 集群上,以实现弹性和可扩展性。这并不意味着其余的服务也要使用 java 开发,我们可以自由选择适合个人服务的平台。
让我们选择 python 作为开发查找服务的平台。查找服务的模型已经设计好了(参考 2022 年 3 月份的文章),我们只需要将这个模型转换为代码和配置。
pythonic 方法python 是一种通用编程语言,已经存在了大约 30 年。早期,它是自动化脚本的首选。然而,随着 django 和 flask 等框架的出现,它的受欢迎程度越来越高,现在各种领域中都在应用它,如企业应用程序开发。数据科学和机器学习进一步推动了它的发展,python 现在是三大编程语言之一。
许多人将 python 的成功归功于它容易编码。这只是一部分原因。只要你的目标是开发小型脚本,python 就像一个玩具,你会非常喜欢它。然而,当你进入严肃的大规模应用程序开发领域时,你将不得不处理大量的 if 和 else,python 变得与任何其他平台一样好或一样坏。例如,采用一种面向对象的方法!许多 python 开发人员甚至可能没意识到 python 支持类、继承等功能。python 确实支持成熟的面向对象开发,但是有它自己的方式 -- pythonic!让我们探索一下!
领域模型addservice 通过将数据保存到一个 mysql 数据库中来将用户添加到系统中。findservice 的目标是提供一个 rest api 按用户名查找用户。域模型如图 1 所示。它主要由一些值对象组成,如 user 实体的name、phonenumber 以及 userrepository。
图 1: 查找服务的域模型
让我们从 name 开始。由于它是一个值对象,因此必须在创建时进行验证,并且必须保持不可变。基本结构如所示:
class name:value: strdef __post_init__(self):if self.value is none or len(self.value.strip()) 32:raise valueerror(invalid name)
如你所见,name 包含一个字符串类型的值。作为后期初始化的一部分,我们会验证它。
python 3.7 提供了 @dataclass 装饰器,它提供了许多开箱即用的数据承载类的功能,如构造函数、比较运算符等。如下是装饰后的 name 类:
from dataclasses import dataclass@dataclassclass name:value: strdef __post_init__(self):if self.value is none or len(self.value.strip()) 32:raise valueerror(invalid name)
以下代码可以创建一个 name 对象:
name = name(krishna)
value 属性可以按照如下方式读取或写入:
name.value = mohanprint(name.value)
可以很容易地与另一个 name 对象比较,如下所示:
other = name(mohan)if name == other:print(same)
如你所见,对象比较的是值而不是引用。这一切都是开箱即用的。我们还可以通过冻结对象使对象不可变。这是 name 值对象的最终版本:
from dataclasses import dataclass@dataclass(frozen=true)class name:value: strdef __post_init__(self):if self.value is none or len(self.value.strip()) 32:raise valueerror(invalid name)
phonenumber 也遵循类似的方法,因为它也是一个值对象:
@dataclass(frozen=true)class phonenumber:value: intdef __post_init__(self):if self.value name:return self._name@propertydef phone(self) -> phonenumber:return self._phone@propertydef since(self) -> datetime.datetime:return self._since
phone 字段的 setter 可以使用 @.setter 来装饰:
@phone.setterdef phone(self, phone: phonenumber) -> none:if phone is none:raise valueerror(invalid phone)self._phone = phone
通过重写 __str__() 函数,也可以为 user 提供一个简单的打印方法:
def __str__(self):return self.name.value + [ + str(self.phone.value) + ] since + str(self.since)
这样,域模型的实体和值对象就准备好了。创建异常类如下所示:
class usernotfoundexception(exception):pass
域模型现在只剩下 userrepository 了。python 提供了一个名为 abc 的有用模块来创建抽象方法和抽象类。因为 userrepository 只是一个接口,所以我们可以使用 abc 模块。
任何继承自 abc.abc 的类都将变为抽象类,任何带有 @abc.abstractmethod 装饰器的函数都会变为一个抽象函数。下面是 userrepository 的结构:
from abc import abc, abstractmethodclass userrepository(abc):@abstractmethoddef fetch(self, name:name) -> user:pass
userrepository 遵循仓储模式。换句话说,它在 user 实体上提供适当的 crud 操作,而不会暴露底层数据存储语义。在本例中,我们只需要 fetch() 操作,因为 findservice 只查找用户。
因为 userrepository 是一个抽象类,我们不能从抽象类创建实例对象。创建对象必须依赖于一个具体类实现这个抽象类。数据层 userrepositoryimpl 提供了 userrepository 的具体实现:
class userrepositoryimpl(userrepository):def fetch(self, name:name) -> user:pass
由于 addservice 将用户数据存储在一个 mysql 数据库中,因此 userrepositoryimpl 也必须连接到相同的数据库去检索数据。下面是连接到数据库的代码。注意,我们正在使用 mysql 的连接库。
from mysql.connector import connect, errorclass userrepositoryimpl(userrepository):def fetch(self, name:name) -> user:try:with connect(host=mysqldb,user=root,password=admin,database=glarimy,) as connection:with connection.cursor() as cursor:cursor.execute(select * from ums_users where name=%s, (name.value,))row = cursor.fetchone()if cursor.rowcount == -1:raise usernotfoundexception()else:return user(name(row[0]), phonenumber(row[1]), row[2])except error as e:raise e
在上面的片段中,我们使用用户 root / 密码 admin 连接到一个名为 mysqldb 的数据库服务器,使用名为 glarimy 的数据库(模式)。在演示代码中是可以包含这些信息的,但在生产中不建议这么做,因为这会暴露敏感信息。
fetch() 操作的逻辑非常直观,它对 ums_users 表执行 select 查询。回想一下,addservice 正在将用户数据写入同一个表中。如果 select 查询没有返回记录,fetch() 函数将抛出 usernotfoundexception 异常。否则,它会从记录中构造 user 实体并将其返回给调用者。这没有什么特殊的。
应用层最终,我们需要创建应用层。此模型如图 2 所示。它只包含两个类:控制器和一个 dto。
图 2: 添加服务的应用层
众所周知,一个 dto 只是一个没有任何业务逻辑的数据容器。它主要用于在 findservice 和外部之间传输数据。我们只是提供了在 rest 层中将 userrecord 转换为字典以便用于 json 传输:
class userrecord:def tojson(self):return {name: self.name,phone: self.phone,since: self.since}
控制器的工作是将 dto 转换为用于域服务的域对象,反之亦然。可以从 find() 操作中观察到这一点。
class usercontroller:def __init__(self):self._repo = userrepositoryimpl()def find(self, name: str):try:user: user = self._repo.fetch(name(name))record: userrecord = userrecord()record.name = user.name.valuerecord.phone = user.phone.valuerecord.since = user.sincereturn recordexcept usernotfoundexception as e:return none
find() 操作接收一个字符串作为用户名,然后将其转换为 name 对象,并调用 userrepository 获取相应的 user 对象。如果找到了,则使用检索到的 user`` 对象创建userrecord`。回想一下,将域对象转换为 dto 是很有必要的,这样可以对外部服务隐藏域模型。
usercontroller 不需要有多个实例,它也可以是单例的。通过重写 __new__,可以将其建模为一个单例。
class usercontroller:def __new__(self):if not hasattr(self, ‘instance’):self.instance = super().__new__(self)return self.instancedef __init__(self):self._repo = userrepositoryimpl()def find(self, name: str):try:user: user = self._repo.fetch(name(name))record: userrecord = userrecord()record.name = user.name.getvalue()record.phone = user.phone.getvalue()record.since = user.sincereturn recordexcept usernotfoundexception as e:return none
我们已经完全实现了 findservice 的模型,剩下的唯一任务是将其作为 rest 服务公开。
rest apifindservice 只提供一个 api,那就是通过用户名查找用户。显然 uri 如下所示:
get /user/{name}
此 api 希望根据提供的用户名查找用户,并以 json 格式返回用户的电话号码等详细信息。如果没有找到用户,api 将返回一个 404 状态码。
我们可以使用 flask 框架来构建 rest api,它最初的目的是使用 python 开发 web 应用程序。除了 html 视图,它还进一步扩展到支持 rest 视图。我们选择这个框架是因为它足够简单。 创建一个 flask 应用程序:
from flask import flaskapp = flask(__name__)
然后为 flask 应用程序定义路由,就像函数一样简单:
@app.route('/user/')def get(name):pass
注意 @app.route 映射到 api /user/,与之对应的函数的 get()。
如你所见,每次用户访问 api 如 http://server:port/user/krishna 时,都将调用这个 get() 函数。flask 足够智能,可以从 url 中提取 krishna 作为用户名,并将其传递给 get() 函数。
get() 函数很简单。它要求控制器找到该用户,并将其与通常的 http 头一起打包为 json 格式后返回。如果控制器返回 none,则 get() 函数返回合适的 http 状态码。
from flask import jsonify, abortcontroller = usercontroller()record = controller.find(name)if record is none:abort(404)else:resp = jsonify(record.tojson())resp.status_code = 200return resp
最后,我们需要 flask 应用程序提供服务,可以使用 waitress 服务:
from waitress import serveserve(app, host=0.0.0.0, port=8080)
在上面的片段中,应用程序在本地主机的 8080 端口上提供服务。最终代码如下所示:
from flask import flask, jsonify, abortfrom waitress import serveapp = flask(__name__)@app.route('/user/')def get(name):controller = usercontroller()record = controller.find(name)if record is none:abort(404)else:resp = jsonify(record.tojson())resp.status_code = 200return respserve(app, host=0.0.0.0, port=8080)
部署findservice 的代码已经准备完毕。除了 rest api 之外,它还有域模型、数据层和应用程序层。下一步是构建此服务,将其容器化,然后部署到 kubernetes 上。此过程与部署其他服务妹有任何区别,但有一些 python 特有的步骤。
在继续前进之前,让我们来看下文件夹和文件结构:
+ ums-find-service+ ums- domain.py- data.py- app.py- dockerfile- requirements.txt- kube-find-deployment.yml
如你所见,整个工作文件夹都位于 ums-find-service 下,它包含了 ums 文件夹中的代码和一些配置文件,例如 dockerfile、requirements.txt 和 kube-find-deployment.yml。
domain.py 包含域模型,data.py 包含 userrepositoryimpl,app.py 包含剩余代码。我们已经阅读过代码了,现在我们来看看配置文件。
第一个是 requirements.txt,它声明了 python 系统需要下载和安装的外部依赖项。我们需要用查找服务中用到的每个外部 python 模块来填充它。如你所见,我们使用了 mysql 连接器、flask 和 waitress 模块。因此,下面是 requirements.txt 的内容。
flask==2.1.1flask_restfulmysql-connector-pythonwaitress
第二步是在 dockerfile 中声明 docker 相关的清单,如下:
from python:3.8-slim-busterworkdir /umsadd ums /umsadd requirements.txt requirements.txtrun pip3 install -r requirements.txtexpose 8080entrypoint [python]cmd [/ums/app.py]
总的来说,我们使用 python 3.8 作为基线,除了移动 requirements.txt 之外,我们还将代码从 ums 文件夹移动到 docker 容器中对应的文件夹中。然后,我们指示容器运行 pip3 install 命令安装对应模块。最后,我们向外暴露 8080 端口(因为 waitress 运行在此端口上)。
为了运行此服务,我们指示容器使用使用以下命令:
python /ums/app.py
一旦 dockerfile 准备完成,在 ums-find-service 文件夹中运行以下命令,创建 docker 镜像:
docker build -t glarimy/ums-find-service
它会创建 docker 镜像,可以使用以下命令查找镜像:
docker images
尝试将镜像推送到 docker hub,你也可以登录到 docker。
docker logindocker push glarimy/ums-find-service
最后一步是为 kubernetes 部署构建清单。
在之前的文章中,我们已经介绍了如何建立 kubernetes 集群、部署和使用服务的方法。我假设仍然使用之前文章中的清单文件来部署添加服务、mysql、kafka 和 zookeeper。我们只需要将以下内容添加到 kube-find-deployment.yml 文件中:
apiversion: apps/v1kind: deploymentmetadata:name: ums-find-servicelabels:app: ums-find-servicespec:replicas: 3selector:matchlabels:app: ums-find-servicetemplate:metadata:labels:app: ums-find-servicespec:containers:- name: ums-find-serviceimage: glarimy/ums-find-serviceports:- containerport: 8080---apiversion: v1kind: servicemetadata:name: ums-find-servicelabels:name: ums-find-servicespec:type: loadbalancerports:- port: 8080selector:app: ums-find-service
上面清单文件的第一部分声明了 glarimy/ums-find-service 镜像的 findservice,它包含三个副本。它还暴露 8080 端口。清单的后半部分声明了一个 kubernetes 服务作为 findservice 部署的前端。请记住,在之前文章中,mysqldb 服务已经是上述清单的一部分了。
运行以下命令在 kubernetes 集群上部署清单文件:
kubectl create -f kube-find-deployment.yml
部署完成后,可以使用以下命令验证容器组和服务:
kubectl get services
输出如图 3 所示:
图 3: kubernetes 服务
它会列出集群上运行的所有服务。注意查找服务的外部 ip,使用 curl 调用此服务:
curl http://10.98.45.187:8080/user/krishnamohan
注意:10.98.45.187 对应查找服务,如图 3 所示。
如果我们使用 addservice 创建一个名为 krishnamohan 的用户,那么上面的 curl 命令看起来如图 4 所示:
图 4: 查找服务
用户管理系统(ums)的体系结构包含 addservice 和 findservice,以及存储和消息传递所需的后端服务,如图 5 所示。可以看到终端用户使用 ums-add-service 的 ip 地址添加新用户,使用 ums-find-service 的 ip 地址查找已有用户。每个 kubernetes 服务都由三个对应容器的节点支持。还要注意:同样的 mysqldb 服务用于存储和检索用户数据。
图 5: ums 的添加服务和查找服务
其他服务ums 系统还包含两个服务:searchservice 和 journalservice。在本系列的下一部分中,我们将在 node 平台上设计这些服务,并将它们部署到同一个 kubernetes 集群,以演示多语言微服务架构的真正魅力。最后,我们将观察一些与微服务相关的设计模式。
以上就是在 kubernetes 上使用 flask 搭建 python 微服务的详细内容。