Python3 标准中不允许将byte类型(不可变类型)和str类型串联在一起;任何连接这两种类型的尝试都会引发TypeError。
Python3 中引入的类型暗示支持只是为了在方法和参数的文档化方面提供更高的清晰度,并且没有对操作强制执行任何标准。
除了功能性和非功能性需求之外,s 软件需求规范文档还指定了其他需求,如 UI、性能、业务和市场需求。
各种需求分类如下:
- **必须有需求:**这些需求必须存在于系统内部。如果缺少任何功能,它们的缺失将影响系统中的关键功能。
- **应该有需求:**这些需求如果存在,将增强应用的功能。
- **可能有需求:**这些需求本质上是非关键性的。如果它们丢失,则不会对应用的功能产生任何影响。
- **需求愿望列表:**利益相关者可能希望在应用的未来更新中看到这些需求。
一旦生成了软件需求规范文档,该过程中的下一步包括软件的设计阶段。在设计阶段,决定软件应用的结构,并决定可能用于构建软件的技术堆栈。
Python 内部的责任链模式允许我们构建一个考虑松散耦合的应用。这是通过将接收到的请求通过软件内的对象链传递来实现的。
以下代码片段显示了 Python 中责任链模式的实现:
import abcclass Handler(metaclass=abc.ABCMeta): """Handler provides an interface to build handlers.""" def __init__(self, handler=None): """Initialize the handler. Keyword arguments: handler -- The next handler object to be called """ self._next_handler = handler @abc.abstractmethod def handler(self, data): """The handler abstract method. Keyword arguments: data -- The data to be processed by the handler """ passclass StringHandler(Handler): ...__new__方法是在需要创建对象的新实例时调用的第一个方法,而__init__方法仅在需要初始化新创建的对象实例时才运行。在类实例创建的正常流程中,__new__方法总是首先执行,并且只有当开发人员想要控制新实例的创建时才应该被重写。然后,在该方法之后应该调用 __init__方法,该方法将在创建实例后被调用,并且需要初始化。
使用 ABC 元类很容易定义一个新的抽象类。以下代码片段显示了实现此类行为的示例:
import abcclass Handler(metaclass=abc.ABCMeta): """Handler provides an interface to build handlers.""" def __init__(self, handler=None): """Initialize the handler. Keyword arguments: handler -- The next handler object to be called """ self._next_handler = handler @abc.abstractmethod def handler(self, data): """The handler abstract method. Keyword arguments: data -- The data to be processed by the handler """ passDBMS 内部模式的规范化提供了许多好处,例如:
- 改善关系的整体组织
- 减少冗余数据的存储
- 改进了数据库内数据的一致性
- 更好地索引数据,从而改进对数据的访问
SQLAlchemy 中的延迟加载为开发人员提供了使用选择或加入模式的选项,在这些模式中可以执行延迟加载。当开发人员使用加载数据的选择模式时,数据集的加载是通过发出 SQLSELECT语句来实现的,SQL 语句根据需求加载数据。
使用*JOIN 时,*通过发出 SQLJOIN语句,一次加载所有相关数据集。这种技术也被称为连接式加载。
我们可以通过多种方式在执行数据更新时维护数据的完整性。我们可以实现的最简单的方法之一是使用事务,它允许我们在一个原子事务中进行大量更新,其中要么应用事务中的所有更新,要么不应用任何更新。
如果事务中的一个更新失败,则事务中以前应用的更新也会回滚,从而保持数据库中关系的一致状态。
可以使用数据库实现的不同级别的缓存如下所示:
- **数据库级缓存:**在数据库级缓存时,我们通常利用数据库的内置功能,通过维护查询缓存来缓存经常使用的数据集。
- **块级缓存:**块级缓存发生在应用级,我们将 ORM 层获取的数据缓存到基于内存的数据存储中,以避免每次请求某个结果时都运行数据库查询。
- **用户级缓存:**在用户级缓存时,非安全关键数据通过会话 cookie 或本地存储在客户端缓存。
Python 有两种不同的方式,允许我们构建可以并发处理请求的应用。这些措施如下:
- **多处理:**Python 多处理模块允许开发人员启动多个进程来并行处理工作负载
- **多线程:**Python 多线程模块允许开发人员执行多个线程,可用于处理并发工作负载
当获取锁的线程突然终止时,根据获取锁的方式,可能存在多种情况。
如果锁是通过 Python 中的with语句获得的,那么一旦线程终止,锁就会被释放。
如果锁是在try-except-final方法中获得的,那么当异常传播到 final 语句时,锁将被释放
如果在没有任何安全过程的情况下获取锁,线程的突然终止将导致死锁,因为锁尚未释放。
通常,当主程序接收到终止信号时,该信号也会传播到其线程;否则,可以将线程标记为守护进程线程,以便使用主程序终止其执行。
实现这一点的另一种方法是使用标志,线程可以定期检查标志。如果设置了该标志,则线程从终止开始。
通过使用管道可以实现不同进程之间的状态共享,这有助于进程之间的通信。
在 Python 中,我们有多种创建进程池的方法来分发任务。我们可以手动创建这些池,如本章进程同步部分中的示例所示,也可以利用concurrent.futures库中提供的ProcessPoolExecutor。
为了通过使用多个应用实例来处理请求,我们使用了水平扩展的概念,即在负载平衡器后面启动同一应用的多个实例。然后,负载平衡器负责在应用实例池中分发传入请求。
流程池可以通过使用 Python 中的concurrent.futures库中的ProcessPoolExecutor来实现。如何使用ProcessPoolExecutor在池上分发请求的示例可以在本章使用线程池处理传入连接一节中看到。
完全可以有一个结合使用多处理和多线程的程序。以下代码片段显示了此实现:
import threading
import multiprocessing
def say_hello():
print("Hello")
def start_threads():
thread_pool = []
for _ in range(5):
thread = threading.Thread(target=say_hello)
thread_pool.append(thread)
for thread in thread_pool:
thread.start()
for thread in thread_pool:
thread.join()
def start_process():
process_pool = []
for _ in range(3):
process = multiprocessing.Process(target=start_threads)
process_pool.append(process)
for process in process_pool:
process.start()
for process in process_pool:
process.join()
if __name__ == '__main__':
start_process()前面实现这一点的方法是有效的,并且可以轻松地实现,没有任何问题,尽管您可能会发现它的用例数量有限,并且它的使用将受到 GIL 实现的限制。
本章的使用 AsyncIO实现简单套接字服务器一节中展示了一个实现套接字服务器的简单示例。另一种方法是通过使用 AsyncIO 实现一个功能齐全的 web 服务器,即使用aiohttp框架,该框架提供了一个基于 AIO 的 HTTP 服务器。
除了我们在本章中看到的泛型View类之外,Flask 还提供了另一个预构建的可插入视图类MethodView。
是的,我们可以从用户表中删除角色表的外键约束,并保持关系。但是,无论何时需要存储数据,我们都需要在用户表中手动插入角色对象所需的对象。
Gunicorn 有许多替代方案可用于基于烧瓶的 Python 应用,例如:
- uWSGI
- 扭网
mod_wsgi- 格温特
增加 Gunicorn 工人的数量非常简单。我们需要做的就是在命令中添加-w <worker count>参数来设置 Gunicorn Worker 的数量,如下例所示:
gunicorn -w 8 --bind 0.0.0.0:8000 wsgi:appCDN 的使用确实提高了网页的加载性能。这是因为浏览器缓存来自给定 URL 的内容的方式。有时,当我们使用现有 CDN 服务某些内容时,我们可以获得以下好处:
- 对于一些常见的前端库,当用户访问其他网站(包括 CDN 中的内容)时,这些库可能已经被用户的浏览器缓存。这有助于我们避免重新下载这些库,减少带宽使用并提高页面的加载速度。
- CDN 还可以根据用户地理位置将请求路由到服务器,以便以尽可能少的延迟下载内容,从而提高页面的加载速度。
为了让浏览器使用现有的连接,我们可以利用一个称为KeepAlive的概念。当在一个请求中设置KeepAlive头时,用于发出请求的连接由服务器保持打开一段固定的时间,希望相同的连接可以用于处理另一个请求,从而避免为每个其他请求设置初始连接的成本。
JavaScript API 提供了一种非常方便的方法,称为removeKey(key),可用于从浏览器的本地/会话存储中删除特定密钥。
单元测试和功能测试之间的主要区别在于测试范围,如下所述:
- **单元测试:**单元测试通常侧重于测试软件中可以分解为单个函数或类的方法的单个组件
- **功能测试:**功能测试也称为集成测试,通常测试系统的特定功能,包括多个组件之间的交互以及它们与外部环境(如数据库系统)的交互。
测试套件是需要在特定程序上运行的测试用例的集合。使用 Python 的unittest库编写测试套件非常容易。例如,如果您已经编写了一些测试用例,例如TestTextInput、TestTextUppercase和TestTextEncode,我们可以使用以下代码片段将它们组合成一个测试套件:
import texttest # Module containing our text related test casesimport unittest# Create a test loaderloader = unittest.TestLoader()# Create a test suitesuite = unittest.TestSuite()# Add tests to a suitesuite.addTests(loader.loadTestsFromModule(texttests)Pytest 中 fixture 的目的是提供一个固定和稳定的环境,测试用例可以在这个环境中执行。这些装置负责通过设置执行测试所需的变量或接口来初始化环境。
使用夹具的另一个优点是它的可重用性,这使得相同的夹具可以在没有任何问题的情况下用于多个测试。
Pytest 中的 fixture 作用域描述了调用 fixture 的频率。这些装置有许多不同的作用域可应用于它们,如下所示:
- **功能范围:**夹具每次测试运行一次
- **类范围:**夹具每类运行一次
- **模块范围:**夹具每模块运行一次
- **会话范围:**每个测试会话运行一次夹具
有多个因素可能导致应用内部出现性能瓶颈,包括:
- 运行应用所需的硬件资源规划不足
- 在应用中实现功能的算法选择不当
- 执行不当的数据库关系,存在大量冗余
- 未对频繁访问的数据实施正确的缓存
Python 中方法的时间分析可以帮助我们了解该方法执行所花费的时间。根据需求,我们可以通过几种不同的方式在方法上运行时间配置文件,如下所示:
- 使用
timeit**模块:**模块为我们提供了一项功能,我们可以使用该功能了解脚本或方法执行所需的时间。 - 使用
time**模块:**我们也可以使用time模块来帮助我们测量 Python 中方法的运行时间。我们可以通过创建装饰器来实现这一点,装饰器可以帮助分析方法运行时。 - 使用
cProfile**模块:**模块允许我们分析 Python 程序中不同步骤的运行时。
尽管 Python 是一种垃圾收集语言,无法直接访问内存指针,但通过非法指针操作可能导致的典型内存泄漏几乎不会发生。但是还有另一种方式,Python 程序可以通过这种方式继续消耗越来越多的内存,而无需释放内存。当程序忘记在对象不再使用时取消对它们的引用时,这可能会导致分配新对象,而不会对不再使用的对象进行垃圾收集。
应用的 API 响应可以通过测量 API 在一组固定执行过程中返回响应所用的平均时间来分析。这可以通过多种方式进行测量,包括使用 Python 标准库中的timeit或time模块。
设计模式对应用的性能有着重要的影响,而不正确的设计模式会对应用的性能造成影响。例如,考虑可用于在整个应用中实现日志记录的对象的分配。如果此记录器对象分配需要对每个单独的模块或类分别进行,那么当对象可以在不同的模块之间共享时,我们可能会浪费大量资源来分配对象。
现在有许多问题使得安全性成为应用的难点。这些问题包括:
- 难以缓解的复杂攻击的增加
- 未修补的 0 天漏洞增加
- 越来越多的国家发起的攻击针对系统的多个漏洞,通常难以追踪
- 越来越多的设备在没有适当安全措施的情况下上线,使得它们容易被用于 DDoS 攻击
XSS 或跨站点脚本攻击是指攻击者在可信网站内注入恶意脚本。加载包含恶意脚本的页面时,会导致客户端系统受到攻击者的危害。
攻击者利用 DoS 或拒绝服务攻击,通过向系统发送大量多余的请求,使其用户无法使用服务或资源,从而导致系统将这些请求排队,从而导致服务中断。
可以通过使用在不同级别实施的不同技术来缓解攻击,例如:
- 添加防火墙规则以拒绝来自给定不受信任源的流量
- 使用云安全提供商提供的服务,云安全提供商可以分析传入流量,并在流量到达应用基础设施之前阻止流量,帮助缓解 DoS 攻击
- 正在配置基础结构以将流量汇聚到没有应用运行的节点,或者通过重新路由。。。
有许多可能危及应用安全性的错误,例如:
- 在应用中使用不安全的第三方库,这可能包含安全漏洞
- 不筛选用户向应用提供的输入
- 在应用中存储未加密的安全敏感数据
- 未实施适当的限制以控制对内部基础设施的访问
面向服务的体系结构和微服务体系结构之间的主要区别在于,在面向服务的体系结构中,应用由不同的服务组成,每个服务都提供在组织的一个业务域上工作的功能。这些服务通过使用企业服务总线彼此通信,企业服务总线将消息从一个服务路由到另一个服务,同时还为消息交换提供通用格式。
在微服务的情况下,应用将由许多小型微服务组成,其中每个微服务只负责提供单个功能,这些功能可能无法映射到组织的完整域,可能只是较大问题域的子集。这些微服务通过使用单个微服务公开的 API 或通过使用允许从一个服务传递消息到另一个服务的无状态消息路由器彼此通信。
为了确保基于微服务的应用具有较高的正常运行时间,我们可以使用以下技术:
- 不为所有微服务使用单一存储
- 在负载平衡器后面运行同一微服务的多个实例
- 使用 API 网关在关键服务失败时客户端仍接收响应的服务中提供优雅的降级
使用服务级别协议或 SLA 提供了许多保证,例如:
- 服务 API 稳定性的保证
- 服务正常运行时间的保证
- 对服务的预期响应时间的保证
- 服务实现的请求速率限制的保证
API 网关可以通过使用 service registry 提供的 SDK 或通过使用 service registry 公开的 API 直接与 service registry 通信。这允许 API 网关从服务注册表自动获取给定服务的正确位置。
微服务内部的异步通信可以通过使用无状态消息代理来实现。为了实现异步通信,一些微服务充当生产者并向 MessageBroker 队列发送消息。然后,其他微服务可能会使用该消息,对其进行处理,并将响应发送回发送该消息的微服务。然后,响应由请求的微服务设置的回调进行处理。这就是微服务之间异步通信的建立方式。
微服务集成测试的编写方式与单片应用的编写方式基本相同,但有以下几点不同:
- 如果微服务需要与另一个外部微服务通信,那么集成测试可能需要设置外部服务,以便正确执行测试用例
- 构成单个服务的组件应该为基础设施中的所有微服务设置,例如,测试所需的特定微服务附带的数据库
单片应用的跟踪与基于微服务的应用的跟踪的不同之处在于,单片应用的跟踪涉及了解应用内部从一个组件到另一个组件的请求流。相反,跟踪基于微服务的应用需要了解请求如何不仅在特定微服务内部流动,而且从一个微服务流向另一个微服务。
在 microservice 体系结构中有多个可用于跟踪的工具,如下表所示:
- 杰格
- 斯普肯
- 应用
为了跟踪微服务中的单个组件,我们可以利用 Jaeger 提供的一种功能,称为 spans。如何使用跨距的示例见https://github.com/jaegertracing/jaeger-client-python 。
迁移到无服务器体系结构有许多优点,例如:
- 通过集成第三方服务减少开发工作量
- 操作复杂性降低,因为现在组织不需要管理基础架构
- 提高了安全性,因为各个函数在各自独立的容器中执行,这有助于我们防止不同的函数相互干扰
- 改进了应用的可伸缩性
使用后端即服务(BaaS)有助于通过集成 API 提供通用功能集,从而创建应用。这些服务由第三方提供商托管,从而减少了应用开发人员从头开始在应用中重建这些服务所需的工作量。
无服务器体系结构中的 API 网关将 API 端点映射到后端的函数。当特定事件发生时,客户端可以调用后端函数调用这些 API 端点。
应用无法成功移植到无服务器体系结构有某些原因。这些理由如下:
- 使用无服务器基础结构提供程序可能不支持的技术堆栈
- 需要存储请求处理状态以生成正确结果的应用
- 一个紧密耦合的代码库,很难定义单个方法
- 应用的某些组件需要非常长的时间才能执行
蓝绿部署的使用为我们提供了以下一系列好处:
- 能够立即将应用从一个版本切换到另一个版本
- 能够轻松地将应用从较新版本回滚到较旧版本,以防新版本遇到某些关键功能错误
- 减少与应用升级相关的停机时间
使用金丝雀部署可以通过以下方式帮助测试应用:
- 应用通过一小部分真实请求进行测试,这可能有助于暴露应用中任何未识别的 bug
- Canary 部署使我们能够运行应用的新版本和旧版本,以便比较 API 提供的响应
使用虚拟机运行基于微服务的应用可能会增加运行微服务实例的开销,因为虚拟机会产生更高的要求。此外,虚拟机的使用限制了可以在同一基础设施上共存的服务的数量,因为虚拟机比容器更难运行,容器利用操作系统功能将程序隔离。
混合云模型中的部署可以与公共云或私有云中的部署相同的方式进行处理。当需要缩放应用时,会出现差异。在这种情况下,当使用混合云方法时,组织可以根据扩展需要从公共云中汇集资源,然后可以在公共云中运行应用的某些部分,在私有云中运行其他部分。
企业应用的点到点集成要求为每对需要集成的应用构建一个连接器。这会创建一个复杂的基础架构,如果将新的应用引入到环境中,则很难管理和扩展该基础架构。
企业服务总线负责通过使用消息传递机制帮助基础结构中的不同服务相互连接。ESB 为应用提供连接器,应用可以通过连接器连接到 ESB 并向 ESB 发送消息。
然后,ESB 负责将这些消息路由到它们预期的正确服务,从而促进基础架构内两个服务之间的通信。
促进 EAI 方法的不同类型的模式如下:
- 调解模式
- 联邦模式
不同微服务的点对点集成很难实现,因为基础设施中的特定微服务可能使用不同的技术堆栈。这可能会导致为每对微服务构建单独的连接器,以便将一个微服务的数据格式转换为另一个。
由于这些服务的可伸缩性,出现了另一个瓶颈,因为现在连接器必须连接已部署微服务的每个实例。
随着微服务体系结构的出现,企业服务总线已被无状态消息路由器所取代,在微服务体系结构中,这些路由器可以单独扩展,并为可能在基础设施内运行的大量微服务实现消息路由。
微服务体系结构中的消息代理通过在可能正在运行的消息代理的多个实例之间复制消息队列来提供高可用性。这允许路由器取代故障路由器,并保持基础设施内的通信完好无损。