├── .gitignore ├── LICENSE ├── README.md ├── SUMMARY.md ├── categories ├── availability.md ├── data-management.md ├── design-and-implementation.md ├── management-and-monitoring.md ├── messaging.md ├── performance-and-scalability.md ├── resiliency.md └── security.md ├── cloud-design-patterns.md ├── cloud-design-patterns.pdf └── patterns ├── ambassador.md ├── anti-corruption-layer.md ├── backends-for-frontends.md ├── bulkhead.md ├── cache-aside.md ├── circuit-breaker.md ├── compensating-transaction.md ├── competing-consumer.md ├── compute-resource-consolidation.md ├── cqrs.md ├── event-sourcing.md ├── external-configuration-store.md ├── federated-identity.md ├── gatekeeper.md ├── gateway-aggregation.md ├── gateway-offloading.md ├── gateway-routing.md ├── health-endpoint-monitoring.md ├── index-table.md ├── leader-election.md ├── materialized-view.md ├── pipes-and-filters.md ├── priority-queue.md ├── queue-based-load-leveling.md ├── retry.md ├── scheduler-agent-supervisor.md ├── sharding.md ├── sidecar.md ├── static-content-hosting.md ├── strangler.md ├── throttling.md └── valet-key.md /.gitignore: -------------------------------------------------------------------------------- 1 | _book/ 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 bowen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 介绍 2 | 3 | 随着技术的快速发展,应用的架构逐渐从单体、分层、SOA逐渐向微服务的方向演进,而基础设施也逐渐从大型机,自建机房,到托管在云平台的各种服务上。所有这一切都是为了使应用(web/mobile)更快、更安全的上线,同时让应用从架构角度和基础设施的角度,拥有高扩展性、伸缩性和自恢复性。 4 | 5 | 微服务让应用从架构的角度拥有扩展性,更适于运行在云平台提供的VM或者容器上,而云平台服务提供的可编程接口让基础设施的自动化变的异常容易,也让DevOps的落地更加轻松。企业上云已经成为一种不可逆转的趋势。 6 | 7 | 然而,因为大量的存量应用系统,企业应用架构不加修改直接移植到云上的可能性比较低,虽然我们可以采用微服务的拆分策略(功能、数据、DDD的原则等)来将拆分应用,使其更适于部署在云上。但是在实际的实施过程中,还是会面临很多问题。 8 | 9 | Azure云计算团队从可用性、数据管理、设计与实现、消息、管理和监控、性能和可扩展性、弹性、安全等角度总结的[云设计模式](https://docs.microsoft.com/en-us/azure/architecture/patterns/),提供了企业应用上云的实用模式以及案例,具有一定的参考价值。虽然文章中的案例都是基于Azure的服务,但是读者完全可以基于其它的云服务提供商(如AWS,GCP,华为云等)应用相同的模式,让上云的过程变的更加顺畅。 10 | 11 | 恰好我和同事目前研究的方向也是CloudNative,所以我们挤出周末的时间,将这32个云设计模式翻译完成。水平和时间有限,只是尽力而为,如果在阅读的过程中发现什么问题,敬请原谅之余,烦劳在github中创建对应的issue或者发起PR,感激不尽。 12 | 13 | > 注: 文章翻译过程中,发现已经有人翻译了一部分云设计模式的内容,但是只有部分模式,而且是3年前的内容,可读性也一般,所以坚持翻译完了。 14 | 15 | ## 反馈和贡献 16 | 17 | 请在github创建相应的[issue](https://github.com/iambowen/cloud-design-patterns/issues)来反馈问题 18 | 19 | ## 贡献者 20 | 21 | * [iambowen@github](https://github.com/iambowen/), [boweniam@twitter](https://twitter.com/boweniam) 22 | * [@tongzh](https://github.com/tongzh) 23 | * [@wldandan](https://github.com/wldandan) 24 | 25 | 26 | ## 在本地启动 27 | 28 | gitbook在国内的访问不是很稳定,我们可以选择从github clone代码: `https://github.com/iambowen/cloud-design-patterns`。在本地安装`gitbook-cli`- `npm install -g gitbook-cli`,然后用命令 `gitbook serve && open localhost:4000`打开该电子书。 29 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | * [引言](README.md) 4 | * [微软Azure架构中心](https://docs.microsoft.com/en-us/azure/architecture/) 5 | * [云设计模式(Cloud Design Patterns)](cloud-design-patterns.md) 6 | * [可用性模式(Availability)](categories/availability.md) 7 | * [数据管理模式(Data management)](categories/data-management.md) 8 | * [设计实现模式(Design and implementation)](categories/design-and-implementation.md) 9 | * [消息模式(Messaging)](categories/messaging.md) 10 | * [管理和监控模式(Management and monitoring)](categories/management-and-monitoring.md) 11 | * [性能和可扩展性模式(Performance and scalability)](categories/performance-and-scalability.md) 12 | * [弹性模式(Resiliency)](categories/resiliency.md) 13 | * [安全模式(Security)](categories/security.md) 14 | * [大使模式(Ambassador)](patterns/ambassador.md) 15 | * [防腐层模式(Anti-corruption Layer)](patterns/anti-corruption-layer.md) 16 | * [前端专属的后端模式(Backends for Frontends)](patterns/backends-for-frontends.md) 17 | * [隔板模式(Bulkhead)](patterns/bulkhead.md) 18 | * [缓存模式(Cache-Aside)](patterns/cache-aside.md) 19 | * [断路器模式(Circuit Breaker)](patterns/circuit-breaker.md) 20 | * [命令和查询责任分离模式(CQRS)](patterns/cqrs.md) 21 | * [事务补偿模式(Compensating Transaction)](patterns/compensating-transaction.md) 22 | * [竞争消费者模式(Competing Consumers)](patterns/competing-consumer.md) 23 | * [计算资源整合模式(Compute Resource Consolidation)](patterns/compute-resource-consolidation.md) 24 | * [事件溯源模式(Event Sourcing)](patterns/event-sourcing.md) 25 | * [外部配置存储(External Configuration Store)](patterns/external-configuration-store.md) 26 | * [联合身份模式(Federated Identity)](patterns/federated-identity.md) 27 | * [看门人模式(Gatekeeper)](patterns/gatekeeper.md) 28 | * [网关聚合模式(Gateway Aggregation)](patterns/gateway-aggregation.md) 29 | * [网关卸载模式(Gateway Offloading)](patterns/gateway-offloading.md) 30 | * [网关路由模式(Gateway Routing)](patterns/gateway-routing.md) 31 | * [健康端点监控模式(Health Endpoint Monitoring)](patterns/health-endpoint-monitoring.md) 32 | * [索引表模式(Index Table)](patterns/index-table.md) 33 | * [领导者选举模式(Leader Election)](patterns/leader-election.md) 34 | * [物化视图模式(Materialized View)](patterns/materialized-view.md) 35 | * [管道和过滤器模式(Pipes and Filters)](patterns/pipes-and-filters.md) 36 | * [优先级队列模式(Priority Queue)](patterns/priority-queue.md) 37 | * [基于队列的负载均衡模式(Queue-Based Load Leveling)](patterns/queue-based-load-leveling.md) 38 | * [重试模式(Retry)](patterns/retry.md) 39 | * [调度者、代理、管理者模式(Scheduler Agent Supervisor)](patterns/scheduler-agent-supervisor.md) 40 | * [分片模式(Sharding)](patterns/sharding.md) 41 | * [挎斗模式(Sidecar)](patterns/sidecar.md) 42 | * [静态内容托管模式(Static Content Hosting)](patterns/static-content-hosting.md) 43 | * [绞杀者模式(Strangler)](patterns/strangler.md) 44 | * [限流模式(Throttling)](patterns/throttling.md) 45 | * [代客密钥模式(Valet Key)](patterns/valet-key.md) -------------------------------------------------------------------------------- /categories/availability.md: -------------------------------------------------------------------------------- 1 | ## 可用性模式 2 | 3 | 可用性是系统正常工作时间的比例,通常以运行时间的百分比来衡量。它会受到系统错误,基础设施问题,恶意攻击和系统负载的影响。云应用程序通常会为用户提供服务等级协议(SLA),因此应用程序必须设计为最大限度地提高可用性。 4 | 5 | | 模式 | 总结 | 6 | |------------------------------------------|----------------------------------| 7 | | 健康端点监控模式(Health Endpoint Monitoring) |在应用程序中执行功能检查,外部工具可以定期通过暴露的端点访问。| 8 | | 基于队列的负载均衡模式(Queue-Based Load Leveling) | 使用一个队列作为任务和服务之间的缓冲区,平滑间歇性重负载。| 9 | | 限流模式(Throttling) | 控制应用程序,个人租户或整个服务的实例消耗的资源。 | -------------------------------------------------------------------------------- /categories/data-management.md: -------------------------------------------------------------------------------- 1 | ### 数据管理模式 2 | 3 | 数据管理是云应用的关键要素,影响了大多数的质量属性。因为性能,可扩展性或可用性等原因,数据通常托管在不同的位置并跨多个服务器,这可能会带来一系列挑战。例如,必须保持数据一致性,并且数据通常需要在不同位置间同步。 4 | 5 | | 模式 | 总结 | 6 | |------------------------------------------|----------------------------------| 7 | | 缓存模式(Cache-Aside) | 按需将数据从数据存储加载到缓存中。| 8 | | 命令和查询责任分离模式(CQRS) | 通过使用单独的接口来分离读取数据和更新数据的操作。| 9 | | 事件溯源模式(Event Sourcing) | 使用仅追加存储去记录描述对域中的数据采取的操作的完整系列事件。| 10 | | 索引表模式(Index Table) |为查询经常引用的数据存储区中的字段创建索引。| 11 | | 物化视图模式(Materialized View) |针对所需的查询操作,当数据没有理想地格式化时,在一个或多个数据存储中的数据上生成预填充视图。| 12 | | 分片模式(Sharding)) | 将数据存储区划分为一组水平分区或分片。 | 13 | | 静态内容托管模式(Static Content Hosting) | 将静态内容部署到基于云的存储服务,可以将它们直接传递给客户端。 | 14 | | 代客密钥模式(Valet Key) |使用向客户端提供对特定资源或服务的有限直接访问权限的令牌或密钥。| 15 | -------------------------------------------------------------------------------- /categories/design-and-implementation.md: -------------------------------------------------------------------------------- 1 | ### 设计和实现模式 2 | 3 | 良好的设计涵盖了如组件设计和部署中的一致性和相关性,简化管理和开发的可维护性,以及允许组件和子系统在其它应用程序和场景中的可重用性等因素。在设计和实施阶段做出的决策对托管在云商的应用程序和服务的质量、总体拥有成本产生巨大的影响。 4 | 5 | 6 | | 模式 | 总结 | 7 | |------------------------------------------|----------------------------------| 8 | | 大使模式(Ambassador) | 创建代表消费者服务或应用程序发送网络请求的帮助服务。 | 9 | | 反腐模式(Anti-corruption Layer) | 在现代应用程序和遗留系统之间实现装饰或适配器层。 | 10 | | 前端专用的后端模式(Backends for Frontends) | 创建单独的后端服务让特定的前端应用程序或接口使用。 | 11 | | 命令和查询责任分离模式(CQRS) | 通过使用单独的接口来分离读取数据和更新数据的操作。| 12 | | 计算资源整合模式(Compute Resource Consolidation) | 将多个任务或操作整合到单个计算单元中。| 13 | | 外部配置存储(External Configuration Store) | 将应用程序部署包中的配置信息移动到中心化的位置。 | 14 | | 网关聚合模式(Gateway Aggregation) |使用网关将多个单独的请求聚合到一个请求中。| 15 | | 网关卸载模式(Gateway Offloading) |卸载共享或特定的服务功能到网关代理。| 16 | | 网关路由模式(Gateway Routing) |使用单个端点将请求路由到多个服务。 | 17 | | 选举模式(Leader Election) | 通过选举一个实例作为负责管理其它实例的负责人,来协调分布式应用程序中的协作任务实例集合执行的操作。| 18 | | 管道和过滤器模式(Pipes and Filters) | 将需要执行复杂处理的任务分解成可以重复使用的一系列单独的元素。 | 19 | | 挎斗模式(Side-Car) | 将应用程序的组件部署到单独的进程或容器中以提供隔离和封装。 | 20 | | 静态内容托管模式(Static Content Hosting) | 将静态内容部署到基于云的存储服务,可以将它们直接传递给客户端。 | 21 | | 绞杀者模式(Strangler) | 通过使用新的应用程序和服务逐渐替换特定功能部件来逐步迁移旧系统。 | -------------------------------------------------------------------------------- /categories/management-and-monitoring.md: -------------------------------------------------------------------------------- 1 | ### 管理和监控 2 | 3 | 云应用程序运行在基础设施甚至操作系统无法完全控制的远程数据中心。管理和监控比本地部署更困难。应用程序必须公开管理员和操作员可以使用的运行时信息来管理和监视系统,以及支持不断变化的业务需求和定制,而不需要停止或重新部署应用程序。 4 | 5 | | 模式 | 总结 | 6 | |------------------------------------------|----------------------------------| 7 | | 大使模式(Ambassador) | 创建代表消费者服务或应用程序发送网络请求的帮助服务。 | 8 | | 反腐模式(Anti-corruption Layer) | 在现代应用程序和遗留系统之间实现装饰或适配器层。 | 9 | | 外部配置存储(External Configuration Store) | 将应用程序部署包中的配置信息移动到中心化的位置。 | 10 | | 网关聚合模式(Gateway Aggregation) |使用网关将多个单独的请求聚合到一个请求中。| 11 | | 网关卸载模式(Gateway Offloading) |卸载共享或特定的服务功能到网关代理。| 12 | | 网关路由模式(Gateway Routing) |使用单个端点将请求路由到多个服务。 | 13 | | 健康端点监控模式(Health Endpoint Monitoring) |在应用程序中执行功能检查,外部工具可以定期通过暴露的端点访问。| 14 | | 挎斗模式(Side-Car) | 将应用程序的组件部署到单独的进程或容器中以提供隔离和封装。 | 15 | | 绞杀者模式(Strangler) | 通过使用新的应用程序和服务逐渐替换特定功能部件来逐步迁移旧系统。 | -------------------------------------------------------------------------------- /categories/messaging.md: -------------------------------------------------------------------------------- 1 | ### 消息模式 2 | 3 | 云应用程序的分布式特性需要一种连接组件和服务的消息传递基础设施,理想情况下以松散耦合的方式,以便最大限度地提高可扩展性。 异步消息系统使用广泛,提供了许多好处,但也带来了诸如消息排序,[毒药消息管理](https://docs.microsoft.com/en-us/dotnet/framework/wcf/feature-details/poison-message-handling),幂等等的挑战。 4 | 5 | | 模式 | 总结 | 6 | |------------------------------------------|----------------------------------| 7 | | 竞争消费者模式(Competing Consumers) |使用多个并发消费者来处理在同一消息通道上接收的消息。| 8 | | 管道和过滤器模式(Pipes and Filters) | 将需要执行复杂处理的任务分解成可以重复使用的一系列单独的元素。 | 9 | | 优先级队列模式(Priority Queue) | 确定发送到服务的请求的优先级,使得具有较高优先级的请求更快地被接收和处理。| 10 | | 基于队列的负载均衡模式(Queue-Based Load Leveling) | 使用一个队列作为任务和服务之间的缓冲区,平滑间歇性重负载。| 11 | | 重试模式(Retry) | 在应用程序尝试连接到服务或网络资源遇到预期的临时故障时,让程序通过透明地重试以前失败的操作来处理。| 12 | | 调度代理主管模式(Scheduler Agent Supervisor) | 在一组分布式服务和其它远程资源之间协调一组操作。| 13 | -------------------------------------------------------------------------------- /categories/performance-and-scalability.md: -------------------------------------------------------------------------------- 1 | ### 性能和可扩展性模式 2 | 3 | 性能是系统在给定时间间隔内执行任何操作的响应能力的指标,而可扩展性是系统处理增加的负载而不影响性能或轻松增加可用资源的能力。 云应用程序工作负载通常是可变的并且存在活动高峰。预测这些,特别是在多租户场景中,几乎是不可能的。 相反,应用程序应能够在限制范围内扩展,以满足峰值需求,并在流量减少时缩小规模。可扩展性不仅涉及计算实例,还涉及数据存储,消息传递基础设施等其它元素。 4 | 5 | 6 | ## 模式目录 7 | | 模式 | 总结 | 8 | |------------------------------------------|----------------------------------| 9 | | 缓存模式(Cache-Aside) | 按需将数据从数据存储加载到缓存中。| 10 | | 命令和查询责任分离模式(CQRS) | 通过使用单独的接口来分离读取数据和更新数据的操作。| 11 | | 事件溯源模式(Event Sourcing) | 使用仅追加存储去记录描述对域中的数据采取的操作的完整系列事件。| 12 | | 索引表模式(Index Table) |为查询经常引用的数据存储区中的字段创建索引。| 13 | | 物化视图模式(Materialized View) |针对所需的查询操作,当数据没有理想地格式化时,在一个或多个数据存储中的数据上生成预填充视图。| 14 | | 优先级队列模式(Priority Queue) | 确定发送到服务的请求的优先级,使得具有较高优先级的请求更快地被接收和处理。| 15 | | 基于队列的负载均衡模式(Queue-Based Load Leveling) | 使用一个队列作为任务和服务之间的缓冲区,平滑间歇性重负载。| 16 | | 分片模式(Sharding)) | 将数据存储区划分为一组水平分区或分片。 | 17 | | 静态内容托管模式(Static Content Hosting) | 将静态内容部署到基于云的存储服务,可以将它们直接传递给客户端。 | 18 | | 限流模式(Throttling) | 控制应用程序,个人租户或整个服务的实例消耗的资源。 | 19 | -------------------------------------------------------------------------------- /categories/resiliency.md: -------------------------------------------------------------------------------- 1 | ### 弹性模式 2 | 3 | 弹性是系统优雅处理和恢复故障的能力。应用程序通常是多租户形式托管在云上,使用共享平台服务,竞争资源和带宽,通过互联网进行通信,在商业硬件上运行意味着出现短暂或永久性故障的可能性增加。检测故障和快速有效地恢复是保持弹性的必要条件。 4 | 5 | 6 | | 模式 | 总结 | 7 | |------------------------------------------|----------------------------------| 8 | | 隔板模式(Bulkhead) | 将应用程序的元素隔离到池中,如果其中一个失败,其它的将继续运行。| 9 | | 断路器模式(Circuit Breaker) | 连接到远程服务或资源时, 处理可能需要花费时间来修复的故障。 | 10 | | 事务补偿模式(Compensating Transaction) |撤消通过一系列步骤执行的工作,它们一起定义最终一致的操作。| 11 | | 健康端点监控模式(Health Endpoint Monitoring) |在应用程序中执行功能检查,外部工具可以定期通过暴露的端点访问。| 12 | | 选举模式(Leader Election) | 通过选举一个实例作为负责管理其它实例的负责人,来协调分布式应用程序中的协作任务实例集合执行的操作。| 13 | | 基于队列的负载均衡模式(Queue-Based Load Leveling) | 使用一个队列作为任务和服务之间的缓冲区,平滑间歇性重负载。| 14 | | 重试模式(Retry) | 在应用程序尝试连接到服务或网络资源遇到预期的临时故障时,让程序通过透明地重试以前失败的操作来处理。| 15 | | 调度代理主管模式(Scheduler Agent Supervisor) | 在一组分布式服务和其它远程资源之间协调一组操作。| -------------------------------------------------------------------------------- /categories/security.md: -------------------------------------------------------------------------------- 1 | ### 安全 2 | 3 | 安全性是系统防止设计用途之外的恶意或意外行为的能力,并防止信息泄露或丢失。云应用程序暴露在互联网外的可信内部边界之外,通常向公众开放,并可能为不受信任的用户服务。 必须设计和部署应用程序,以防止其受到恶意攻击,限制仅对经过批准的用户的访问,并保护敏感数据。 4 | 5 | | 模式 | 总结 | 6 | |------------------------------------------|----------------------------------| 7 | | 联合身份模式(Federated Identity) |将认证委托给外部身份提供者。| 8 | | 看门人模式(Gatekeeper) | 通过使用专用主机实例,充当客户端和应用程序或服务之间的代理,来保护应用程序和服务,该主机实例,对请求进行验证和消毒,并在它们之间传递请求和数据。| 9 | | 代客密钥模式(Valet Key) |使用向客户端提供对特定资源或服务的有限直接访问权限的令牌或密钥。| 10 | -------------------------------------------------------------------------------- /cloud-design-patterns.md: -------------------------------------------------------------------------------- 1 | ### 云设计模式 2 | 3 | 本书提到的设计模式对于在云中构建可靠,可扩展,安全的应用程序非常有用。 4 | 书中的每个模式描述了要解决的问题,使用模式的注意事项主要基于Microsoft Azure的例子。大多数模式包含如何在Azure上实现模式的代码示例或代码段。大多数模式都适用于分布式系统,具有普适性,和在那个云平台托管没有关系。 5 | 6 | ## 开发云上应用面临的挑战 7 | 8 | ### 可用性 9 | 可用性是系统正常工作时间的比例,通常以运行时间的百分比来衡量。它会受到系统错误,基础设施问题,恶意攻击和系统负载的影响。云应用程序通常会为用户提供服务等级协议(SLA),因此应用程序必须设计为最大限度地提高可用性。 10 | 11 | ### 数据管理 12 | 13 | 数据管理是云应用的关键要素,影响了大多数的质量属性。因为性能,可扩展性或可用性等原因,数据通常托管在不同的位置并跨多个服务器,这可能会带来一系列挑战。例如,必须保持数据一致性,并且数据通常需要在不同位置间同步。 14 | 15 | ### 设计和实现 16 | 17 | 良好的设计涵盖了如组件设计和部署中的一致性和相关性,简化管理和开发的可维护性,以及允许组件和子系统在其它应用程序和场景中的可重用性等因素。在设计和实施阶段做出的决策对托管在云上的应用程序和服务的质量、拥有者的总体成本产生巨大的影响。 18 | 19 | ### 消息 20 | 21 | 云应用程序的分布式特性需要一种连接组件和服务的消息基础设施,理想情况下松耦合,以便最大限度地提高可扩展性。异步消息系统使用广泛,提供了许多好处,但也带来了诸如消息排序,[毒药消息管理](https://docs.microsoft.com/en-us/dotnet/framework/wcf/feature-details/poison-message-handling),幂等等的挑战。 22 | 23 | ### 管理和监控 24 | 25 | 云应用程序运行在基础设施甚至操作系统无法完全控制的远程数据中心。管理和监控比本地部署更困难。应用程序必须公开管理员和操作员可以使用的运行时信息来管理和监视系统,以及支持不断变化的业务需求和定制,而不需要停止或重新部署应用程序。 26 | 27 | ### 性能和扩展性 28 | 29 | 性能是系统在给定时间间隔内执行任何操作的响应能力的指标,而可扩展性是系统处理增加的负载而不影响性能或轻松增加可用资源的能力。云应用程序工作负载通常是可变的并且存在活动高峰。预测这些,特别是在多租户场景中,几乎是不可能的。 相反,应用程序应能够在限制范围内扩展,以满足峰值需求,并在流量减少时缩小规模。可扩展性不仅涉及计算实例,还涉及数据存储,消息传递基础设施等其它元素。 30 | 31 | ### 弹性 32 | 33 | 弹性是系统优雅处理和恢复故障的能力。应用程序通常是多租户形式托管在云上,使用共享平台服务,竞争资源和带宽,通过互联网进行通信,在商业硬件上运行意味着出现短暂或永久性故障的可能性增加。检测故障和快速有效地恢复是保持弹性的必要条件。 34 | 35 | ### 安全 36 | 37 | 安全性是系统防止设计用途之外的恶意或意外行为的能力,并防止信息泄露或丢失。云应用程序暴露在互联网外的可信内部边界之外,通常向公众开放,并可能为不受信任的用户服务。 必须设计和部署应用程序,以防止其受到恶意攻击,限制仅对经过批准的用户的访问,并保护敏感数据。 38 | 39 | ## 模式目录 40 | | 模式 | 总结 | 41 | |------------------------------------------|----------------------------------| 42 | | 大使模式(Ambassador) | 创建代表消费者服务或应用程序发送网络请求的帮助服务。 | 43 | | 反腐模式(Anti-corruption Layer) | 在现代应用程序和遗留系统之间实现装饰或适配器层。 | 44 | | 前端专用的后端模式(Backends for Frontends) | 创建单独的后端服务让特定的前端应用程序或接口使用。 | 45 | | 隔板模式(Bulkhead) | 将应用程序的元素隔离到池中,如果其中一个失败,其它的将继续运行。| 46 | | 缓存?模式(Cache-Aside) | 按需将数据从数据存储加载到缓存中。| 47 | | 断路器模式(Circuit Breaker) | 连接到远程服务或资源时, 处理可能需要花费时间来修复的故障。 | 48 | | 命令和查询责任分离模式(CQRS) | 通过使用单独的接口来分离读取数据和更新数据的操作。| 49 | | 补偿交易模式(Compensating Transaction) |撤消通过一系列步骤执行的工作,它们一起定义最终一致的操作。| 50 | | 竞争消费者模式(Competing Consumers) |使用多个并发消费者来处理在同一消息通道上接收的消息。| 51 | | 计算资源整合模式(Compute Resource Consolidation) | 将多个任务或操作整合到单个计算单元中。| 52 | | 事件溯源模式(Event Sourcing) | 使用仅追加存储去记录描述对域中的数据采取的操作的完整系列事件。| 53 | | 外部配置存储(External Configuration Store) | 将应用程序部署包中的配置信息移动到中心化的位置。 | 54 | | 联合身份模式(Federated Identity) |将认证委托给外部身份提供者。| 55 | | 看门人模式(Gatekeeper) | 通过使用专用主机实例,充当客户端和应用程序或服务之间的代理,来保护应用程序和服务,该主机实例,对请求进行验证和消毒,并在它们之间传递请求和数据。| 56 | | 网关聚合模式(Gateway Aggregation) |使用网关将多个单独的请求聚合到一个请求中。| 57 | | 网关卸载模式(Gateway Offloading) |卸载共享或特定的服务功能到网关代理。| 58 | | 网关路由模式(Gateway Routing) |使用单个端点将请求路由到多个服务。 | 59 | | 健康端点监控模式(Health Endpoint Monitoring) |在应用程序中执行功能检查,外部工具可以定期通过暴露的端点访问。| 60 | | 索引表模式(Index Table) |为查询经常引用的数据存储区中的字段创建索引。| 61 | | 选举模式(Leader Election) | 通过选举一个实例作为负责管理其它实例的负责人,来协调分布式应用程序中的协作任务实例集合执行的操作。| 62 | | 物化视图模式(Materialized View) |针对所需的查询操作,当数据没有理想地格式化时,在一个或多个数据存储中的数据上生成预填充视图。| 63 | | 管道和过滤器模式(Pipes and Filters) | 将需要执行复杂处理的任务分解成可以重复使用的一系列单独的元素。 | 64 | | 优先级队列模式(Priority Queue) | 确定发送到服务的请求的优先级,使得具有较高优先级的请求更快地被接收和处理。| 65 | | 基于队列的负载均衡模式(Queue-Based Load Leveling) | 使用一个队列作为任务和服务之间的缓冲区,平滑间歇性重负载。| 66 | | 重试模式(Retry) | 在应用程序尝试连接到服务或网络资源遇到预期的临时故障时,让程序通过透明地重试以前失败的操作来处理。| 67 | | 调度代理主管模式(Scheduler Agent Supervisor) | 在一组分布式服务和其它远程资源之间协调一组操作。| 68 | | 分片模式(Sharding)) | 将数据存储区划分为一组水平分区或分片。 | 69 | | 挎斗模式(Side-Car) | 将应用程序的组件部署到单独的进程或容器中以提供隔离和封装。 | 70 | | 静态内容托管模式(Static Content Hosting) | 将静态内容部署到基于云的存储服务,可以将它们直接传递给客户端。 | 71 | | 绞杀者模式(Strangler) | 通过使用新的应用程序和服务逐渐替换特定功能部件来逐步迁移旧系统。 | 72 | | 限流模式(Throttling) | 控制应用程序,个人租户或整个服务的实例消耗的资源。 | 73 | | 代客密钥模式(Valet Key) |使用向客户端提供对特定资源或服务的有限直接访问权限的令牌或密钥。| 74 | -------------------------------------------------------------------------------- /cloud-design-patterns.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iambowen/cloud-design-patterns/f5a8fe0010828bb2a9b9612fe2c4203b34852fec/cloud-design-patterns.pdf -------------------------------------------------------------------------------- /patterns/ambassador.md: -------------------------------------------------------------------------------- 1 | ## 大使模式(Ambassador) 2 | 3 | 创建代表消费者服务或应用程序发送网络请求的帮助服务。可以认为大使服务是与客户端共处的进程外代理。 4 | 5 | 此模式可用于以语言无关的方式卸载常见的客户端连接任务,如监视,日志记录,路由,安全性(如TLS)和[弹性模式](../categories/resiliency.html)。它常常与传统应用程序或其它难以修改应用程序一起使用,以便扩展其网络功能。也可以让专门的团队实现这些功能。 6 | 7 | 8 | ### 背景和问题 9 | 10 | 基于云的弹性应用程序需要断路器,路由,计量和监控等功能,以及进行网络相关配置更新的功能。遗留应用程序或现有代码库可能难以或不可能添加这些功能,因为代码缺乏维护,或者开发团队无法轻易修改。 11 | 12 | 网络调用也可能需要大量配置来进行连接,认证和授权。如果这些调用在多个使用不同语言和框架构的建应用程序中使用,必须为每个实例配置调用。此外,网络和安全功能可能需要由组织内的中央团队进行管理。对于很大的代码库,该团队更新他们不熟悉的应用程序代码可能会有风险。 13 | 14 | ### 解决方案 15 | 16 | 将客户端框架和库放入外部进程,作为应用程序和外部服务之间的代理。在与应用程序相同的主机环境中部署代理,从而增加路由控制,弹性,安全功能,并避免任何与主机相关的访问限制。还可以使用大使模式来标准化和扩展仪表。代理可以监视性能指标,例如延迟或资源使用情况,以及监控应用程序运行主机环境。 17 | 18 | ![solution](https://docs.microsoft.com/en-us/azure/architecture/patterns/_images/ambassador.png) 19 | 20 | 卸载到大使的功能可以独立于应用程序进行管理。更新和修改大使不会影响应用程序的旧功能。单独的专门小组可以来实现和维护移交给大使的安全性,网络或认证功能。 21 | 22 | 大使服务可以作为挎斗部署,陪伴在消费应用程序或服务的整个生命周期。 或者,如果大使由公共主机上的多个独立进程共享,则可以将其部署为守护程序或Windows服务。如果消费服务是容器化的,那么大使应该被创建为同一个主机上的一个单独的容器,并配置相应的链接进行通信。 23 | 24 | ### 问题和注意事项 25 | 26 | 代理增加了一些延迟开销。考虑应用程序直接调用客户端库的方式是否更好。 27 | 28 | * 考虑在代理中包含泛化特征的可能影响。 例如,大使可以处理重试,但这可能不安全,除非所有操作都是幂等的。 29 | * 考虑一种允许客户端将某些上下文传递给代理的机制,以及返回给客户端。 例如,包括HTTP请求头部以选择不重试或指定重试的最大次数。 30 | * 考虑如何包装和部署代理。 31 | * 考虑是否为所有客户端使用单个共享实例或为每个客户端使用实例。 32 | 33 | ### 何时使用该模式 34 | 35 | 在以下场景适用于该模式: 36 | 37 | * 需要为多种语言或框架构建一套通用的客户端连接功能。 38 | * 需要将交叉客户端连接问题卸载给基础架构开发人员或其他更专业的团队。 39 | * 需要支持遗留应用程序或难以修改的应用程序上云或集群连接。 40 | 41 | 该模式可能不适合以下场景: 42 | 43 | * 当网络请求延迟要求较高。代理会引入一些时延开销,虽然开销不高,但在某些情况下可能会影响应用程序。 44 | * 客户端连接功能由单一语言使用时。 这种情况下更好的选择可能是作为依赖库以包的形式分发给开发团队的客户端。 45 | * 连接功能无法泛化,需要与客户端应用程序进行更深入的集成。 46 | 47 | ### 案例 48 | 49 | 下图显示了通过大使代理向远程服务发出请求的应用程序。大使提供路由,断路和日志记录。它调用远程服务,然后将响应返回给客户端应用程序: 50 | 51 | ![](https://docs.microsoft.com/en-us/azure/architecture/patterns/_images/ambassador-example.png) 52 | 53 | ### 相关指南 54 | 55 | [挎斗模式]() -------------------------------------------------------------------------------- /patterns/anti-corruption-layer.md: -------------------------------------------------------------------------------- 1 | ## 防腐层模式(Anti-corruption Layer) 2 | 3 | 在现代应用程序和依赖于其的遗留系统之间实现装饰层或适配器层。该层转换现代应用程序和遗留系统之间的请求。 使用此模式可确保应用程序的设计不受依赖的旧系统的限制。 4 | 5 | ### 背景和问题 6 | 7 | 大多数应用程序依赖于其他系统的某些数据或功能。例如,遗留应用程序迁移到现代系统时,可能仍然需要现存的遗留资源。新功能必须能够调用遗留系统。对于渐进迁移尤其如此,随着时间的推移,更大的应用程序的不同功能被迁移到现代系统。 8 | 9 | 通常这些遗留系统会受困于令人费解的数据模式或过时的API等质量问题。遗留系统中使用的功能和技术与现代系统的差异很大。为了与遗留系统进行互操作,新应用程序可能需要支持过时的基础设施,协议,数据模型,API或其它不会在现代应用程序中使用的功能。 10 | 11 | 维护新系统和遗留系统之间的连接可能迫使新系统至少遵循一些遗留系统的API或其它语义。当这些遗留功能存在质量问题时,一个设计干净的现代应用程序可能会支持它们“腐败”。 12 | 13 | ### 解决方案 14 | 15 | 通过在遗留系统和现代系统之间使用防腐层来隔离它们。该层转换两个系统之间的通信,允许遗留系统保持不变,同时可以避免损害现代应用程序的设计和技术方法。 16 | 17 | ![](https://docs.microsoft.com/en-us/azure/architecture/patterns/_images/anti-corruption-layer.png) 18 | 19 | 现代应用与防腐层之间的通信始终使用应用程序的数据模型和架构。从防腐层到遗留系统的调用都符合该系统的数据模型或方法。 防腐层包含两个系统之间转换所需的所有逻辑。该层可以作为应用程序中的组件或作为独立服务来实现。 20 | 21 | ### 问题和注意事项 22 | 23 | * 防腐层可能会增加在两个系统之间进行调用的延迟。 24 | * 防腐层添加了必须进行管理和维护的附加服务。 25 | * 考虑防腐层如何伸缩。 26 | * 考虑是否需要多个防腐层。你可能希望使用不同的技术或语言将功能分解成多个服务,或者因为其它原因来分割防腐层。 27 | * 考虑如何根据其它应用程序或服务来管理防腐层。如何将其集成到监控,发布和配置过程中? 28 | * 确保事务和数据的一致性得到维护并可以监控。 29 | * 考虑防腐层是否需要处理遗留和现代系统之间的所有通信,或只是功能的一个子集。 30 | * 考虑防腐层是否是为永久性的,或者是在所有旧功能都迁移后最终退休。 31 | 32 | ### 何时使用该模式 33 | 34 | 在以下场景使用该模式: 35 | 36 | * 计划在多个阶段进行迁移,但需要维护新系统和旧系统之间的集成。 37 | * 新旧系统具有不同的语义,但仍需要通信。 38 | 39 | 如果新系统和遗留系统之间没有明显的语义差异,则该模式可能不合适。 40 | 41 | ### 相关指南 42 | 43 | [绞杀者模式](stangler.html) -------------------------------------------------------------------------------- /patterns/backends-for-frontends.md: -------------------------------------------------------------------------------- 1 | # 前端专属的后端模式(Backends for Frontends) 2 | 3 | 为特定的前端应用或界面创建不同的后端服务。当你需要避免为多种界面定制同一个后端时,该模式十分有用。 4 | 5 | ## 背景与问题 6 | 7 | 应用最初往往是以桌面web UI为目标而开始设计的。通常会同时并行开发一个为该UI提供所需特性的后端服务。随着应用用户数的增长,移动应用会被开发出来,而且必须与同一个后端进行交互。这个后端服务便变成了通用的后端,需要同时满足桌面和移动界面的需求。 8 | 9 | 但是,在屏幕尺寸、性能和显示限制方面,移动设备与桌面浏览器的能力往往是有显著差异的。结果会导致移动应用对后端的需求与桌面web UI也是不一致的。 10 | 11 | 这种差异体现在对后端需求的相互矛盾上。后端需要经常发生显著的变更,以同时满足桌面web UI和移动应用的需求。通常,每种界面会由不同的前端团队来打造,这就导致后端成为开发过程中的瓶颈。互相冲突的升级需求,以及需要保持后端服务同时为两种界面而工作,都会导致花费大量精力在这个唯一的可部署资源上。 12 | 13 | ![](https://docs.microsoft.com/en-us/azure/architecture/patterns/_images/backend-for-frontend.png) 14 | 15 | 随着开发活动不断聚焦于后端服务,有可能会产生一个独立的团队来管理和维护后端。最后,会导致界面和后端开发团队之间的脱节,并为后端团队增加负担,因为需要在不同UI团队的互相矛盾的需求之间作出平衡。当一个界面团队需要后端进行修改时,这些修改必须经过其它界面团队的验证,才能集成到后端服务中去。 16 | 17 | ## 解决方案 18 | 19 | 为每种用户界面创建一个单独的后端。对每个后端的行为和性能进行精心的调整,以最佳匹配对应前端环境的需求,而不用担心会影响到其它前端体验。 20 | 21 | ![](https://docs.microsoft.com/en-us/azure/architecture/patterns/_images/backend-for-frontend-example.png) 22 | 23 | 因为每个后端是为某种界面量身打造的,就可以为该界面进行专属的优化。结果,与试图满足所有种类界面的通用后端相比,专属的后端会变得更小、更简单,而且很可能更快。每个界面团队对于控制自己的后端有自主权,而不用依赖于一个集中式的后端开发团队。这为界面团队在后端服务的语言选择、发布节奏、工作优先级排序、特性集成等方面提供了灵活性。 24 | 25 | ## 问题与注意事项 26 | 27 | * 需要考虑部署多少个后端。 28 | * 如果不同的界面(比如移动客户端)会发送同样的请求,考虑是否有必要为每种界面都实现一个后端,还是一个单独的后端就可以满足。 29 | 30 | * 当实现这种模式时,不同服务之间的代码重复很有可能出现。 31 | * 专属于前端的后端服务应该只包含客户端特定的逻辑与行为。通用逻辑和其它公用特性应该在你的应用的其它某个地方被管理起来。 32 | 33 | * 思考这种模式应该如何在开发团队的职责上体现出来。 34 | * 考虑需要多长时间实现这种模式。在继续支持已存在的通用后端的同时,为打造新的后端投入精力,是否会引入技术债? 35 | 36 | 37 | ## 何时使用该模式 38 | 39 | 在以下场景使用该模式: 40 | 41 | * 需要为维护一个共享的或通用的后端服务而花费大量的开销。 42 | * 想为特定客户端界面的需求对后端进行优化。 43 | 44 | * 为调解多种界面的需求,需要为一个通用的后端进行定制。 45 | * 对于后端另有一种可供选择的语言,更适合另外一种用户界面。 46 | 47 | 这种模式可能不适用于: 48 | 49 | * 当各种界面对后端的请求相同或相似时。 50 | * 当只有一种界面与后端进行交互时。 51 | 52 | ## 相关指南 53 | 54 | * [网关聚合模式](patterns/gateway-aggregation.md) 55 | * [网关卸载模式](patterns/gateway-offloading.md) 56 | * [网关路由模式](patterns/gateway-routing.md) -------------------------------------------------------------------------------- /patterns/bulkhead.md: -------------------------------------------------------------------------------- 1 | ## 隔板模式(Bulkhead) 2 | 3 | 将应用程序的元素隔离到池中,如果其中一个失败,不影响其它继续运行。 4 | 这种模式被命名为“隔板”,因为它类似于船体的剖面划分。 如果一艘船的船体受到损害,只有损坏的部分会充满水,从而防止船舶沉没。 5 | 6 | ### 背景和问题 7 | 8 | 基于云的应用可能包括多个服务,每个服务拥有一个或多个消费者。服务负载过多或故障将影响所有消费者。 9 | 10 | 此外,消费者可以同时向多个服务发送请求,每个请求都消耗资源。当消费者向错误配置或未响应的服务发送请求时,客户端请求使用的资源可能无法及时释放。继续对服务的请求会耗尽这些资源。例如,客户端的连接池可能会耗尽。在这一点上,消费者对其它服务的请求也受到影响。最终,消费者不能再向其它服务发送请求,而不仅仅是原始的无响应服务。 11 | 12 | 资源耗尽问题同样会影响到多个消费者的服务。来自一个客户端的大量请求可能会耗尽服务中的可用资源。其它消费者不再能够使用该服务,导致级联故障效果。 13 | 14 | ### 解决方案 15 | 16 | 根据消费者的负载和可用性要求,将服务实例分成不同的组。这样的设计有助于隔离故障,并允许在故障期间,为某些消费者维护服务功能。 17 | 18 | 消费者还可以划分资源,以确保用于调用一个服务的资源不会影响调用另一个服务的资源。例如,可以为每个服务分配一个调用多个服务的消费者连接池。如果服务开始失败,则它仅影响为该服务分配的连接池,而消费者可以继续使用其它服务。 19 | 20 | 这种模式的好处包括: 21 | 22 | * 隔离消费者和服务级联故障。影响消费者或服务的问题可以在自己的隔板内隔离开来,从而防止整个解决方案发生故障。 23 | * 允许在发生服务故障时保留一些功能。应用程序的其它服务和功能将继续运行。 24 | * 允许为消费应用程序部署提供不同服务质量的服务。高优先级消费者池可以配置为使用高优先级服务。 25 | 26 | 下图显示了围绕呼叫单独服务的连接池构建的隔板。如果服务A失败或导致某些其它问题,则连接池是隔离的,因此只有使用分配给服务A的线程池的工作负载才会受到影响。使用服务B和C的工作负载不受影响,可以继续工作而不中断。 27 | 28 | ![](https://docs.microsoft.com/en-us/azure/architecture/patterns/_images/bulkhead-1.png) 29 | 30 | 下图显示了多个客户端调用单个服务。 每个客户端分配了一个单独的服务实例。客户端1发出了太多的请求并淹没了它的实例。由于每个服务实例与其它实例隔离,其他客户端可以继续调用。 31 | 32 | ![](https://docs.microsoft.com/en-us/azure/architecture/patterns/_images/bulkhead-2.png) 33 | 34 | ### 问题和注意事项 35 | 36 | * 围绕应用程序的业务和技术要求定义分区。 37 | * 当将服务或消费者分为隔板时,请考虑技术提供的隔离级别以及成本,性能和可管理性方面的开销。 38 | * 考虑将舱壁与重试,断路器和节流模式相结合,以提供更复杂的故障处理。 39 | * 将消费者分为隔板时,请考虑使用进程,线程池和信号量。像[Netflix Hystrix](https://github.com/Netflix/Hystrix)和[Polly](https://github.com/App-vNext/Polly)这样的项目提供了创建消费者隔板的框架。 40 | * 将服务分成隔板时,请考虑将其部署到单独的虚拟机,容器或进程中。容器提供资源隔离的良好平衡,开销相当低。 41 | * 使用异步消息进行通信的服务可以通过不同的队列进行隔离。每个队列可以具有处理队列上的消息的一组专用实例,或者使用算法进行出队和调度处理的一组实例。 42 | * 确定隔板的粒度级别。例如,如果要跨分区分配租户,可以将每个租户分成一个单独的分区,将多个租户放入一个分区。 43 | * 监视每个分区的性能和SLA。 44 | 45 | ### 何时使用该模式 46 | 47 | 在以下场景使用该模式: 48 | 49 | * 隔离用于消耗一组后端服务的资源,特别是如果应用程序可以提供一定程度的功能,即使其中一个服务没有响应。 50 | * 隔离标准消费者和关键消费者。 51 | * 保护应用免受级联故障的影响。 52 | 53 | 这种模式可能不适用于: 54 | 55 | * 项目无法接受资源使用效率低的情况。没有必要增加的复杂性。 56 | 57 | ## 案例 58 | 59 | 下面的Kubernetes配置文件创建一个独立的容器来运行单个服务,拥有自己的CPU和内存资源和限制。 60 | 61 | ```yaml 62 | apiVersion: v1 63 | kind: Pod 64 | metadata: 65 | name: drone-management 66 | spec: 67 | containers: 68 | - name: drone-management-container 69 | image: drone-service 70 | resources: 71 | requests: 72 | memory: "64Mi" 73 | cpu: "250m" 74 | limits: 75 | memory: "128Mi" 76 | cpu: "1" 77 | ``` 78 | 79 | ### 相关指南 80 | 81 | * [断路器模式(Circuit Breaker)](patterns/circuit-breaker.md) 82 | * [为Azure设计弹性的应用](https://docs.microsoft.com/en-us/azure/architecture/resiliency/index) 83 | * [重试模式(Retry)](patterns/retry.md) 84 | * [限流模式(Throttling)](patterns/throttling.md) -------------------------------------------------------------------------------- /patterns/cache-aside.md: -------------------------------------------------------------------------------- 1 | ## 缓存辅助模式 2 | 3 | 按需将数据从数据存储加载到缓存中。除了可以提高性能,还有助于保持缓存中保存的数据与底层数据存储中的数据之间的一致性。 4 | 5 | ### 背景和问题 6 | 7 | 应用程序使用缓存来改进对数据存储中保存的信息的重复访问。然而,期望缓存中的数据始终与数据存储中的数据完全一致是不切实际的。应用程序应实施有助于确保缓存中的数据尽可能保持最新状态的策略,但也可以检测并处理缓存中的数据过时的时候出现的情况。 8 | 9 | ### 解决方案 10 | 11 | 许多商业高速缓存系统提供直读和直写/后写操作(需加注释)。在这些系统中,应用程序通过引用缓存来检索数据。如果数据不在缓存中,则从数据存储中检索数据,并将其添加到缓存中。缓存中数据的任何修改也会自动写回数据存储区。 12 | 13 | 对于不提供此功能的缓存,使用缓存的应用程序负责维护数据。 14 | 应用程序可以通过实现缓存辅助策略来模拟通读缓存的功能。此策略将数据按需加载到缓存中。下图说明了使用Cache-Aside模式将数据存储在缓存中。 15 | 16 | ![](https://docs.microsoft.com/en-us/azure/architecture/patterns/_images/cache-aside-diagram.png) 17 | 18 | 如果应用程序更新信息,则可以通过对数据存储进行修改,使缓存中的相应项目失效来执行直写策略。 19 | 当下一个项目需要时,使用缓存辅助策略更新数据存储中检索的数据并将其添加回缓存。 20 | 21 | ### 问题和注意事项 22 | 23 | 在决定如何实现此模式时,请考虑以下几点: 24 | 25 | **缓存数据的生命周期**。 许多缓存都实现了过期策略让数据无效,如果在指定的时间段内没有访问,就将其从缓存中删除。为了使缓存有效,请确保过期策略与使用数据的应用程序的访问模式相匹配。过期期限不能太短,因为这可能导致应用程序不断从数据存储中检索数据并将其添加到缓存。同样,不要将过期期限长到缓存的数据过时。请记住,缓存对于相对静态的数据或频繁读取的数据是最有效的。 26 | 27 | 28 | **驱逐数据**。 与数据来源的数据存储相比,大多数高速缓存的大小都有限,如果有需要它们会驱逐数据。大多数缓存采用最近最少使用的策略来选择要驱逐的缓存项,但也可能是可定制的。配置缓存的全局过期属性和其它属性以及每个缓存项的过期属性,以确保缓存具有成本效益。对每个缓存项应用全局驱逐策略并不总是合适的。例如,如果缓存项从数据存储中检索非常昂贵,将缓存项保持在缓存中更频繁的访问,成本低,收益高。 29 | 30 | **预填缓存**。许多解决方案在缓存启动时会预填应用程序可能需要的缓存数据。如果某些数据过期或被驱逐,Cache-Aside模式仍然有用。 31 | 32 | **一致性**。实现Cache-Aside模式不能保证数据存储和缓存之间的一致性。数据存储中的项目可以随时通过外部进程更改,下次加载项目时,此更改可能不会反映在缓存中。跨数据存储复制数据的系统中,如果频繁进行同步,一致性的问题可能会很严重。 33 | 34 | **本地(内存中)缓存**。应用程序实例的本地可以有本地缓存,存储在内存中。如果应用程序重复访问相同的数据,本地缓存可能很有用。然而,本地缓存是私有的,因此不同的应用程序实例可能具有相同缓存数据的副本。缓存之间的数据可能会很快变的不一致,因此可能需要让保存在私有缓存中的数据过期,并更频繁地刷新数据。这些场景下请考虑调查共享或分布式缓存机制的使用。 35 | 36 | ### 何时使用该模式 37 | 38 | 在以下场景使用该模式: 39 | 40 | * 缓存不提供原生的直读和直写操作。 41 | * 资源需求是不可预知的。该模式使应用程序能够按需加载数据。它不会假设哪些数据应用程序会提前需要。 42 | 43 | 这种模式可能不适用于: 44 | 45 | * 缓存数据集是静态的。如果数据可以放在可用的缓存空间中,在启动时用数据预填缓存,并应用防止数据过期的策略。 46 | * 为Web集群中托管的Web应用程序缓存会话状态信息。在这种环境中,应该避免引入客户端-服务器亲和性的依赖关系。(?如何理解) 47 | 48 | ## 例子 49 | 50 | 在Microsoft Azure云,你可以使用Azure Redis Cache创建由应用程序的多个实例共享的分布式缓存。 51 | 52 | 要连接到Azure Redis Cache的实例,请调用静态`Connect`方法并传入连接字符串。该方法返回一个表示连接的`ConnectionMultiplexer`。在应用程序中共享`ConnectionMultiplexer`实例的一种方法是拥有返回连接实例的静态属性,和下面的例子类似。用线程安全的方式来初始化一个连接的实例。 53 | 54 | ```c# 55 | private static ConnectionMultiplexer Connection; 56 | 57 | // Redis Connection string info 58 | private static Lazy lazyConnection = new Lazy(() => 59 | { 60 | string cacheConnection = ConfigurationManager.AppSettings["CacheConnection"].ToString(); 61 | return ConnectionMultiplexer.Connect(cacheConnection); 62 | }); 63 | 64 | public static ConnectionMultiplexer Connection => lazyConnection.Value; 65 | ``` 66 | 67 | 下面代码中的`GetMyEntityAsync`方法展示了基于Azure Redis Cache的Cache-Aside模式的实现。该方法使用直读方式从缓存中检索对象。 68 | 69 | 通过使用整数`ID`作为key来标识对象。`GetMyEntityAsync`方法尝试从缓存中检索具有该key的项目。如果命中则返回。如果没有命中,`GetMyEntityAsync`方法将从数据存储中检索对象,将其添加到缓存中,然后返回。实际上从数据存储中读取数据的代码没有在这里,因为它数据存储相关。请注意,要配置缓存的项目过期,以防止其在其它地方更新时变得过时。 70 | 71 | ```c# 72 | // Set five minute expiration as a default 73 | private const double DefaultExpirationTimeInMinutes = 5.0; 74 | 75 | public async Task GetMyEntityAsync(int id) 76 | { 77 | // Define a unique key for this method and its parameters. 78 | var key = $"MyEntity:{id}"; 79 | var cache = Connection.GetDatabase(); 80 | 81 | // Try to get the entity from the cache. 82 | var json = await cache.StringGetAsync(key).ConfigureAwait(false); 83 | var value = string.IsNullOrWhiteSpace(json) 84 | ? default(MyEntity) 85 | : JsonConvert.DeserializeObject(json); 86 | 87 | if (value == null) // Cache miss 88 | { 89 | // If there's a cache miss, get the entity from the original store and cache it. 90 | // Code has been omitted because it's data store dependent. 91 | value = ...; 92 | 93 | // Avoid caching a null value. 94 | if (value != null) 95 | { 96 | // Put the item in the cache with a custom expiration time that 97 | // depends on how critical it is to have stale data. 98 | await cache.StringSetAsync(key, JsonConvert.SerializeObject(value)).ConfigureAwait(false); 99 | await cache.KeyExpireAsync(key, TimeSpan.FromMinutes(DefaultExpirationTimeInMinutes)).ConfigureAwait(false); 100 | } 101 | } 102 | 103 | return value; 104 | } 105 | ``` 106 | 107 | > 这些例子使用Azure Redis Cache API来访问存储并从缓存中检索信息。更多内容请参阅[Microsoft Azure Redis Cache](https://docs.microsoft.com/en-us/azure/redis-cache/cache-dotnet-how-to-use-azure-redis-cache)以及[如何使用Redis Cache创建Web应用程序](https://docs.microsoft.com/en-us/azure/redis-cache/cache-web-app-howto)。 108 | 109 | 下面代码中的`UpdateEntityAsync`方法展示了在应用程序更改值时,如何让缓存中的对象无效。这是一个直写方法的例子。代码更新原始数据存储,然后通过调用`KeyDeleteAsync`方法(指定key)从缓存中删除缓存的数据。 110 | 111 | >这个步骤的顺序很重要。如果在缓存更新之前删除数据,在数据存储中的数据更改之前,客户端应用程序有很短的时间来获取数据(因为数据不在高速缓存中),从而导致缓存包含陈旧的数据。 112 | 113 | 114 | ```c# 115 | public async Task UpdateEntityAsync(MyEntity entity) 116 | { 117 | // Invalidate the current cache object 118 | var cache = Connection.GetDatabase(); 119 | var id = entity.Id; 120 | var key = $"MyEntity:{id}"; // Get the correct key for the cached object. 121 | await cache.KeyDeleteAsync(key).ConfigureAwait(false); 122 | 123 | // Update the object in the original data store 124 | await this.store.UpdateEntityAsync(entity).ConfigureAwait(false); 125 | } 126 | ``` 127 | ### 相关指南 128 | 129 | 下面的内容可能和本模式的实现相关: 130 | 131 | [缓存指南](https://docs.microsoft.com/en-us/azure/architecture/best-practices/caching)。介绍了有关如何在云解决方案中缓存数据的额外信息,以及实现缓存时应考虑的问题。 132 | 133 | [数据一致性入门](https://msdn.microsoft.com/library/dn589800.aspx)。云应用程序通常使用分布在数据存储中的数据。 管理和维护数据一致性是系统的关键,特别是可能出现的并发和可用性问题。该入门介绍了有关分布式数据的一致性的问题,并总结了应用程序如何实现最终的一致性来维护数据的可用性。 -------------------------------------------------------------------------------- /patterns/circuit-breaker.md: -------------------------------------------------------------------------------- 1 | ## 断路器模式(Circuit Breaker) 2 | 3 | 连接到远程服务或资源发生故障时,能花一些时间来修复问题。这可以提高应用程序的稳定性和弹性。 4 | 5 | ### 背景和问题 6 | 7 | 在分布式环境中,对远程资源和服务的调用可能会由于瞬态故障而失败,例如网络连接速度慢,超时,资源过剩或暂时不可用。这些故障通常会在短时间内自行修正,鲁棒性好的云应用程序应当使用诸如[重试模式](retry.html)的策略来准备好处理它们。 8 | 9 | 但是,也可能会出现意外事件造成的故障,而且修复可能需要更长时间。这些故障的严重程度可以从部分连接失效到服务完全失效。在这些情况下,应用可能会无意义地重试不太可能成功的操作,而实际上应用应该快速接受操作失败并相应地处理此故障。 10 | 11 | 另外,如果服务非常繁忙,系统一部分的故障可能会导致级联故障。例如,调用服务的操作可以配置为实现超时,如果服务在该时间段内没有响应,则回复失败消息。但是,该策略可能会阻止许多并发请求,直到超时期限到期。这些被阻止的请求可能会持有关键的系统资源,如内存,线程,数据库连接等。因此,这些资源可能会耗尽,导致系统中其它无关的,需要使用相同的资源的部分故障。在这种情况下,最好立即执行操作,只有在可能成功的情况下才尝试调用该服务。请注意,设置较短的超时可能有助于解决此问题,但超时时间不应太短,以至于操作在大多数时候失败,即使对服务的请求最终会成功。 12 | 13 | ## 解决方案 14 | 15 | 断路器模式可以防止应用程序重复尝试执行可能失败的操作。允许它继续,而不必等待故障修复或浪费CPU周期,同时判断故障是否持续。断路器模式让应用程序能够检测故障是否已解决。如果问题已经修复,应用程序可以尝试调用该操作。 16 | 17 | > 断路器模式的目的不同于重试模式。重试模式使应用程序可以重试一个操作,期望它会成功。断路器模式可防止应用程序执行可能失败的操作。应用程序可以将两者结合起来,使用重试模式通过断路器执行操作。然而,重试逻辑应对断路器返回的任何异常敏感,如果断路器指示故障不是瞬态,则放弃重试尝试。 18 | 19 | 断路器作为可能失败的操作的代理。应监视最近发生的故障数,并使用此信息来决定是否允许操作继续,或者立即返回异常。 20 | 21 | 代理可以被实现为具有模拟电路断路器功能的状态机,包含以下的状态: 22 | 23 | **关闭**:应用程序的请求被路由到操作。代理维护最近的故障数量的计数,如果对操作的调用不成功,则代理会增加此计数。如果最近的故障数量在给定时间段内超出了指定的阈值,则代理将被置于打开状态。此时,代理启动一个超时定时器,当该定时器到期时,代理被置于半打开状态。 24 | 25 | > 超时定时器的目的是让系统有时间来解决导致故障的问题,然后允许应用程序再次执行该操作。 26 | 27 | **打开**:应用程序的请求立即失败,返回异常给应用程序。 28 | 29 | **半开**:允许来自应用程序的有限数量的请求通过并调用操作。如果这些请求成功,则假定以前导致故障的问题已经修复,断路器切换到关闭状态(故障计数器被复位)。如果任何请求失败,则断路器假定故障仍然存在,因此它恢复到打开状态并重新启动超时定时器,给系统更多的时间从故障中恢复。 30 | 31 | > 半开状态有助于防止恢复服务突然被请求淹没。随着服务的恢复,它可能能够支持有限数量的请求,直到恢复完成,但是在恢复进行时,大量的工作可能导致服务超时或失败。 32 | 33 | ![](https://docs.microsoft.com/en-us/azure/architecture/patterns/_images/circuit-breaker-diagram.png) 34 | 35 | 在图中,关闭状态使用的故障计数器是基于时间的。它会周期性的自动重置。这有助于防止断路器在偶然发生故障时进入打开状态。断路器跳闸到断开状态的故障阈值仅在指定时间间隔内发生指定数量的故障时达到。半开状态使用的计数器记录调用操作的成功尝试次数。在指定数量的连续操作调用成功后,断路器将恢复为“关闭”状态。如果任何调用失败,断路器立即进入打开状态,并且下一次进入半打开状态时,复位成功的计数器。 36 | 37 | > 超时定时器的目的是让系统有时间来解决导致故障的问题,然后再允许应用程序重新执行该操作。 38 | 39 | 断路器模式提供稳定性,同时系统从故障中恢复,并最大限度地减少对性能的影响。它可以通过快速拒绝对可能失败的操作的请求来帮助维持系统的响应时间,而不是等待操作超时或永不返回。如果断路器在每次改变状态时引发事件,则该信息可用于监视由断路器保护的系统部分的健康状况,或者当断路器跳闸到打开状态时向管理员发出警报。 40 | 41 | 这种模式是可定制的,可以根据故障类型进行调整。例如,可以将断路器的定时器超时时间逐步增加。最初可以将断路器置于打开状态几秒钟,然后如果故障尚未解决会将超时增加到几分钟,依此类推。 在某些情况下,不是从打开状态返回失败并引发异常,返回对应用程序有意义的默认值可能更有用。 42 | 43 | ### 问题和注意事项 44 | 45 | 在决定如何实现此模式时,应该考虑以下几点: 46 | 47 | **异常处理**。如果操作不可用,则必须准备通过断路器调用操作的应用程序来处理引发的异常。处理异常的方式将是具体的应用程序。例如,应用程序可能暂时将功能降级,调用替代操作来尝试执行相同的任务或获取相同的数据,或向用户报告异常,并请他们稍后再试。 48 | 49 | **异常类型**。请求可能因为很多原因失败,其中一些可能表示比其它故障更严重的故障类型。例如,请求可能会失败,因为远程服务已崩溃,并且需要几分钟的时间才能恢复,或者因为服务暂时重载而导致超时。断路器可能会检查发生的异常类型,并根据这些异常的性质调整其策略。例如,与服务完全不可用导致的故障次数相比,可能需要更多的超时异常来将断路器跳闸到打开状态。 50 | 51 | **日志记录**。断路器应记录所有失败的请求(可能成功的请求),以便管理员能够监视操作的健康状况。 52 | 53 | **可恢复性**。应该配置断路器以匹配其保护操作的可能恢复模式。例如,如果断路器长时间保持在打开状态,即使解决了故障原因,也可能引起异常。类似地,如果能很快地从打开状态切换到半开状态,断路器可能会波动并降低应用的响应时间。 54 | 55 | **测试失败的操作**。在打开状态下,断电器不使用定时器来确定何时切换到半开状态,而是可以定期ping远程服务或资源,以确定它是否再次可用。ping可以采取以前尝试调用先前失败的操作的形式,或者使用由远程服务提供的专门用于测试服务运行状况的特殊操作,如健康端点监控模式所述。 56 | 57 | **手动覆盖**。在故障操作的恢复时间不确定的系统中,提供手动复位选项有助于管理员关闭断路器(并重置故障计数器)。类似地,如果由断路器保护的操作暂时不可用,管理员可以强制断路器进入打开状态(并重新启动超时定时器)。 58 | 59 | **并发**。大量并发的应用程序可以访问相同的断路器。该实现不应阻止并发请求,或者每次调用操作会增加额外的开销。 60 | 61 | **资源差异化**。对于一种类型的资源使用单个断路器时,如果存在多个潜在的独立提供者,请小心。 例如,在包含多个分片的数据存储中,一个分片可能完全可访问,而另一个分片遇到临时问题。如果在这些情况下的错误响应合并,应用程序可能尝试访问某些可能失败的分片,而访问其它可用的分片会被阻止。 62 | 63 | **加速断路**。 有时,故障响应可以包含足够的信息,使断路器立即跳闸并保持最短时间的跳闸。 例如,来自重载的共享资源的错误响应可能表示不推荐立即重试,应该在几分钟之内重试。 64 | 65 | > 注意:如果服务不可用,可以返回HTTP响应码429(太多请求),否则返回HTTP响应码503(服务不可用)。响应可以包括附加信息,例如延迟的预期持续时间。 66 | 67 | 68 | **重放失败的请求**。 在打开状态下,断路器可以将每个请求的详细信息记录到日志中,而不是简单地快速失败,并在远程资源或服务可用时安排这些请求重放。 69 | 70 | **外部服务不合适的超时**。断路器可能无法完全保护应用程序免受配置有超长超时时间的外部服务失败的操作。如果超时时间过长,在断路器指示操作失败之前,运行断路器的线程可能会长时间阻塞。这个时候,许多其它应用程序实例也可能尝试通过断路器调用该服务,并在它们全部失败之前绑定大量线程。 71 | 72 | ### 何时使用该模式 73 | 74 | 在以下场景使用该模式: 75 | 76 | 防止应用程序尝试调用远程服务或访问共享资源(如果此操作可能导致失败)。 77 | 78 | 以下场景不建议使用此模式: 79 | 80 | * 处理应用程序中本地私有资源的访问,例如内存中数据结构。在这种环境中,使用断路器会增加系统的开销。 81 | * 作为在应用程序的业务逻辑中处理异常的替代品。 82 | 83 | ### 例子 84 | 85 | 在Web应用程序中,从外部服务检索的数据会填充几个页面。如果系统实现最小的缓存,大多数页面的命中将导致服务的往返。从Web应用程序到服务的连接可以配置超时时间(通常为60秒),如果服务在这段时间内没有响应,则每个网页中的逻辑将假定该服务不可用并且引发异常。 86 | 87 | 但是,如果服务失败并且系统非常繁忙,用户可能会被迫最多等待60秒才能发现异常。最终,诸如内存,连接和线程之类的资源可能会耗尽,从而阻止其他用户连接到系统,即使他们没有访问从服务中检索数据的页面。 88 | 89 | 通过进一步的添加Web服务器和实现负载平衡来扩展系统可能会延迟资源耗尽的时间,但由于用户请求仍然无响应,最终所有Web服务器仍然可能用尽资源,问题依然没有解决。 90 | 91 | 将连接到服务和检索数据的逻辑包裹在断路器中可以帮助解决这个问题,同时更加优雅地处理服务故障。用户请求仍将失败,但是它们失败更快,因此资源将不会被阻塞。 92 | 93 | `CircuitBreaker`类保存有关实现下面代码所示接口`ICircuitBreakerStateStore`的对象的断路器的状态信息。 94 | 95 | ```c# 96 | interface ICircuitBreakerStateStore 97 | { 98 | CircuitBreakerStateEnum State { get; } 99 | Exception LastException { get; } 100 | DateTime LastStateChangedDateUtc { get; } 101 | void Trip(Exception ex); 102 | void Reset(); 103 | void HalfOpen(); 104 | bool IsClosed { get; } 105 | } 106 | ``` 107 | 108 | `State`属性表示断路器的当前状态,将由`CircuitBreakerStateEnum`枚举类定义断路器状态为打开,半开或闭合。断路器闭合时,`IsClosed`属性应为真,如果断路器为打开或半开。`Trip`方法将断路器的状态切换到打开状态,并记录导致状态更改的异常以及异常发生的日期和时间。`LastException`和`LastStateChangedDateUtc`属性返回此信息。`Reset`方法关闭断路器,`HalfOpen`方法将断路器设置为半开。 109 | 110 | 例子中的`InMemoryCircuitBreakerStateStore`类包含接口`ICircuitBreakerStateStore`的实现。`CircuitBreaker`类创建此类的实例以保持断路器的状态。 111 | 112 | The ExecuteAction methodinthe classwrapsanoperation,specifiedasan Action delegate.If thecircuitbreakerisclosed, invokesthe Action delegate.Iftheoperationfails,anexception handler calls , which sets the circuit breaker state to open. The following code example highlights this flow. 113 | `CircuitBreaker`类中的`ExecuteAction`方法将一个操作指定为`Action`代理。如果断路器关闭,`ExecuteAction`将调用`Action`代理。如果操作失败,则异常处理程序将调用`TrackException`,将断路器状态设置为打开。下面的代码展示了这个流程。 114 | 115 | ```c# 116 | public class CircuitBreaker 117 | { 118 | private readonly ICircuitBreakerStateStore stateStore = 119 | CircuitBreakerStateStoreFactory.GetCircuitBreakerStateStore(); 120 | private readonly object halfOpenSyncObject = new object (); 121 | ... 122 | public bool IsClosed { get { return stateStore.IsClosed; } } 123 | public bool IsOpen { get { return !IsClosed; } } 124 | public void ExecuteAction(Action action) 125 | { 126 | ... 127 | if (IsOpen) 128 | { 129 | // The circuit breaker is Open. 130 | ... (see code sample below for details) 131 | } 132 | // The circuit breaker is Closed, execute the action. 133 | try 134 | { 135 | action(); 136 | } 137 | catch (Exception ex) 138 | { 139 | // If an exception still occurs here, simply 140 | // retrip the breaker immediately. 141 | this.TrackException(ex); 142 | // Throw the exception so that the caller can tell 143 | // the type of exception that was thrown. 144 | throw; 145 | } 146 | } 147 | private void TrackException(Exception ex) 148 | { 149 | // For simplicity in this example, open the circuit breaker on the first exception. 150 | // In reality this would be more complex. A certain type of exception, such as one 151 | // that indicates a service is offline, might trip the circuit breaker immediately. 152 | // Alternatively it might count exceptions locally or across multiple instances and 153 | // use this value over time, or the exception/success ratio based on the exception 154 | // types, to open the circuit breaker. 155 | this.stateStore.Trip(ex); 156 | } 157 | } 158 | ``` 159 | 160 | 下面的例子展示了断路器未关闭时执行的代码(上一个例子中省略掉的)。它首先检查断路器是否已经打开比`CircuitBreaker`类中本地`OpenToHalfOpenWaitTime`字段指定的时间长。如果是这种情况,`ExecuteAction`方法将断路器设置为半开,然后尝试执行由`Action`代理指定的操作。 161 | 162 | 如果操作成功,断路器将复位到关闭状态。如果操作失败,它将跳回到打开状态,并更新异常发生的时间,在再次执行操作之前,断路器将等待一段时间。 163 | 164 | 如果断路器只在短时间内打开,小于`OpenToHalfOpenWaitTime`的值,则`ExecuteAction`方法会抛出`CircuitBreakerOpenException`异常并返回导致断路器转换到打开状态的错误。 165 | 166 | 此外,它使用锁来防止断路器在半开状态下尝试对该操作执行并发调用。尝试并发调用该操作将像断路器打开一样被处理,如下所述,它将失败并且抛出异常。 167 | 168 | ```java 169 | ... 170 | if (IsOpen) 171 | { 172 | // The circuit breaker is Open. Check if the Open timeout has expired. 173 | // If it has, set the state to HalfOpen. Another approach might be to 174 | // check for the HalfOpen state that had be set by some other operation. 175 | if (stateStore.LastStateChangedDateUtc + OpenToHalfOpenWaitTime < DateTime.UtcNow) 176 | { 177 | // The Open timeout has expired. Allow one operation to execute. Note that, in 178 | // this example, the circuit breaker is set to HalfOpen after being 179 | // in the Open state for some period of time. An alternative would be to set 180 | // this using some other approach such as a timer, test method, manually, and 181 | // so on, and check the state here to determine how to handle execution 182 | // of the action. 183 | // Limit the number of threads to be executed when the breaker is HalfOpen. 184 | // An alternative would be to use a more complex approach to determine which 185 | // threads or how many are allowed to execute, or to execute a simple test 186 | // method instead. 187 | bool lockTaken = false; 188 | try 189 | { 190 | Monitor.TryEnter(halfOpenSyncObject, ref lockTaken) 191 | if (lockTaken) 192 | { 193 | // Set the circuit breaker state to HalfOpen. 194 | stateStore.HalfOpen(); 195 | // Attempt the operation. 196 | action(); 197 | // If this action succeeds, reset the state and allow other operations. 198 | // In reality, instead of immediately returning to the Closed state, a counter 199 | // here would record the number of successful operations and return the 200 | // circuit breaker to the Closed state only after a specified number succeed. 201 | this.stateStore.Reset(); 202 | return; 203 | } 204 | catch (Exception ex) 205 | { 206 | // If there's still an exception, trip the breaker again immediately. 207 | this.stateStore.Trip(ex); 208 | // Throw the exception so that the caller knows which exception occurred. 209 | throw; 210 | } 211 | finally { 212 | if (lockTaken) 213 | { 214 | Monitor.Exit(halfOpenSyncObject); 215 | } 216 | } 217 | } 218 | } 219 | // The Open timeout hasn't yet expired. Throw a CircuitBreakerOpen exception to 220 | // inform the caller that the call was not actually attempted, 221 | // and return the most recent exception received. 222 | throw new CircuitBreakerOpenException(stateStore.LastException); 223 | } ... 224 | 225 | ``` 226 | 227 | Tousea objecttoprotectanoperation,anapplicationcreatesaninstanceofthe CircuitBreaker class and invokes the method, specifying the operation to be performed as the parameter. The 228 | CircuitBreaker 229 | ExecuteAction 230 | application should be prepared to catch the exception if the operation fails because the circuit breaker is open. The following code shows an example: 231 | 232 | 要使用`CircuitBreaker`对象来保护操作,应用程序将创建`CircuitBreaker`类的实例,并调用`ExecuteAction`方法,指定要作为参数执行的操作。如果由于断路器断开而导致操作失败,应用程序应准备捕获`CircuitBreakerOpenException`异常。下面为示例代码: 233 | 234 | ```c# 235 | var breaker = new CircuitBreaker(); 236 | try { 237 | breaker.ExecuteAction(() => 238 | { 239 | // Operation protected by the circuit breaker. 240 | ... 241 | }); 242 | } 243 | catch (CircuitBreakerOpenException ex) 244 | { 245 | // Perform some different action when the breaker is open. 246 | // Last exception details are in the inner exception. 247 | ... 248 | } 249 | catch (Exception ex) 250 | { 251 | ... 252 | } 253 | ``` 254 | 255 | ### 相关模式和指南 256 | 257 | 实现此模式时,以下模式也可能很有用: 258 | 259 | * [重试模式](retry.html)。描述当应用程序通过透明地重试以前失败的操作尝试连接到服务或网络资源时,应用程序如何处理预期的临时故障。 260 | * [健康端点监控模式](health-endpoint-monitoring.html)。断路器可能能够通过向服务器公开的端点发送请求来测试服务的健康状况。 服务应返回指示其状态的信息。 -------------------------------------------------------------------------------- /patterns/compensating-transaction.md: -------------------------------------------------------------------------------- 1 | # 事务补偿模式 2 | 3 | 如果一起定义最终一致的操作的一系列步骤,在执行过程中有一个或多个步骤失败,则撤销该操作。通常,托管在云上的实现复杂业务流程和工作流程的应用程序会存在遵循最终一致性模型的操作。 4 | 5 | ## 背景和问题 6 | 7 | 在云中运行的应用程序会频繁修改数据。该数据可能会分布在不同地理位置的不同数据源上。为了提高性能和避免分布式环境中的竞争,应用程序不应该提供强事务一致性。相反,应该实现最终的一致性。在这个模型中,典型的业务操作包括一系列单独的步骤。当执行这些步骤时,系统状态的总体视图可能不一致,但是当操作已经完成并且所有步骤都已执行时,系统应该再次变得一致。 8 | > [数据一致性入门](https://msdn.microsoft.com/library/dn589800.aspx)解释了分布式事务为何不能很好地扩展的原因,以及最终一致性模型的原理。 9 | 最终一致性模型中的挑战是如何处理失败的步骤。在这种情况下,可能需要撤消操作中以前步骤完成的所有工作。但是,数据不能简单地回滚,因为应用程序的其它并发实例可能会更改它。即使在数据未被并发实例更改的情况下,撤消步骤可能不仅仅是恢复原始状态的问题。可能需要应用各种特定于业务的规则(请参阅案例部分中介绍的旅游网站)。 10 | 如果实现最终一致性的操作涉及多个异构数据存储,则撤消操作中的步骤将依次访问每个数据存储。必须可靠地撤消在每个数据存储中执行的工作,以防止系统保持不一致。 11 | 并不是所有受实施最终一致性的操作影响的数据都可能保存在数据库中。在面向服务的架构(SOA)环境中,操作可以调用服务中的动作,并引起该服务所持有的状态发生更改。要撤消操作,此状态更改也必须撤消。这可能涉及再次调用该服务并执行另一个反转第一个操作的动作。 12 | 13 | ## 解决方案 14 | 15 | 解决方案是实施事务补偿。补偿事务中的步骤必须撤销原始操作中的步骤的影响。补偿事务可能无法简单地将当前状态替换为系统在操作开始时所处于的状态,因为该方法可能会覆盖应用程序的其它并发实例所做的更改。相反,它必须是一个智能的进程,考虑到并发实例完成的任何工作。该过程通常由具体应用程序,由原始操作执行的工作性质驱动。 16 | 一个常见的方法是使用工作流来实现最终一致的、需要补偿的操作。随着原始操作的进行,系统记录关于每个步骤的信息以及该步骤执行的工作如何撤销。如果操作在任何时候都失败,则工作流通过完成的步骤回滚,并反转每个步骤的工作。请注意,补偿事务可能不必以原始操作的完全相反的顺序撤销工作,并且可以并行执行一些撤销步骤。 17 | > 这种方法类似于Clemens Vasters博客中讨论的[Sagas策略](http://vasters.com/clemensv/2012/09/01/Sagas.aspx)。 18 | 事务补偿是最终一致的操作,也可能失败。系统应该具备在故障点恢复事务补偿,并继续执行的能力。可能需要重复失败的步骤,因此补偿事务中的步骤应定义为幂等命令。更多信息请参阅Jonathan Oliver博客上的[幂等模式](http://blog.jonathanoliver.com/2010/04/idempotency-patterns/)。 19 | 在某些情况下,可能只能通过手动干预才能从失败的步骤中恢复。在这些情况下,系统应该发出警报,并提供尽可能多的关于故障原因的信息。 20 | 21 | ## 问题和注意事项 22 | 23 | 在决定如何实现此模式时,请考虑以下几点: 24 | 确定执行最终一致性的操作中的步骤何时失败可能并不容易。步骤可能不会立即失败,但是可以阻止。可能需要实施某种形式的超时机制。 25 | 补偿逻辑不容易泛化。事务补偿是特定于应用程序的。它依赖于拥有足够信息的应用程序能够在故障操作中撤消每个步骤的影响。 26 | 应该将补偿事务中的步骤定义为幂等命令。如果补偿事务本身失败,则可以重复这些步骤。 27 | 处理原始操作步骤的基础设施和事务补偿必须具有弹性。不能丢失补偿失败步骤所需的信息,并且必须能可靠地监视补偿逻辑的进度。 28 | 事务补偿不一定将系统中的数据返回到原始操作开始时的状态。相反,它会补偿在操作失败之前成功完成的步骤执行的工作。 29 | 事务补偿中的步骤顺序不一定与原始操作中的步骤完全相反。例如,一个数据存储可能对另一个数据存储区的不一致性更敏感,因此补偿事务中撤消对该存储的更改的步骤应首先发生。 30 | 在完成操作所需的每个资源上放置基于超时的短期锁,提前获取这些资源可以帮助增加整体活动成功的可能性。这项工作只有在所有资源获得后才能执行。所有操作必须在锁到期之前完成。 31 | 考虑使用比平常更宽容的重试逻辑来最小化触发补偿事务的故障。如果执行最终一致性的操作步骤失败,请尝试将该失败视为暂时异常并重复该步骤。只有在反复失败或不可恢复的情况下,才停止操作并启动事务补偿。 32 | > 执行事务补偿的许多挑战与实施最终一致性的挑战相同。更多信息请参阅[数据一致性入门](https://msdn.microsoft.com/library/dn589800.aspx)中实现最终一致性的注意事项。 33 | ## 何时使用该模式 34 | 此模式仅适用于失败时必须撤消的操作。如果可能,设计解决方案避免需要事务补偿的复杂性。 35 | 36 | ## 案例 37 | 旅游网站让客人预订行程。单程可能包括一系列航班和酒店。 从西雅图到伦敦,然后到巴黎的客户,创建行程时可以执行以下步骤: 38 | 1. 预订从西雅图到伦敦的F1航班座位。 39 | 2. 预订从伦敦到巴黎F2航班的座位。 40 | 3. 预订从巴黎到西雅图F3班机的座位。 41 | 4. 在伦敦的H1酒店预订房间。 42 | 5. 在巴黎的H2酒店预订客房。 43 | 44 | 这些步骤构成了最终一致的操作,尽管每个步骤都是单独的操作。因此,除了执行这些步骤之外,系统还必须记录必要的计数器操作,以便在客户决定取消行程的情况下撤消每个步骤。执行计数器操作所需的步骤之后可以作为补偿事务运行。 45 | 请注意,事务补偿中的步骤可能与原始步骤完全相反,事务补偿的每个步骤中的逻辑必须考虑到任何特定于业务的规则。例如,在航班上取消预订座位可能无法让客户完全退还所支付的任何费用。下图展示了生成事务补偿以撤销预订旅行行程的长时间运行的交易。 46 | ![](https://docs.microsoft.com/en-us/azure/architecture/patterns/_images/compensating-transaction-diagram.png) 47 | > 根据每个步骤设计补偿逻辑的方式,补偿事务中的步骤可能并行执行。 48 | 在许多业务解决方案中,单步骤的故障并不总是需要通过使用事务补偿来回滚系统。例如,如果在旅游网站场景中预订了F1,F2和F3的航班-客户无法在酒店H1预订房间,比取消航班更好的是在同一城市的不同酒店为客户预定房间。在客户仍然决定取消时(在这种情况下,事务补偿运行并撤销在F1,F2和F3航班上的预订),但决定应由客户而不是由系统做出。 49 | 50 | ## 相关模式和指南 51 | 52 | 以下模式和指南在实现此模式时也可能相关: 53 | * [数据一致性入门](https://msdn.microsoft.com/library/dn589800.aspx)。补偿事务模式通常用于撤消实现最终一致性模型的操作。该文章提供了有关最终一致性的优点和权衡的信息。 54 | * [调度代理主管模式](scheduler-agent-supervisor.html)。介绍如何实现执行使用分布式服务和资源的业务运营的弹性系统。 有时可能需要通过使用事务补偿来撤消由操作执行的工作。 55 | * [重试模式](retry.html)。补偿事务的执行可能很昂贵,并且可以根据重试模式,实现重试失败操作的有效策略,最小化它们的使用。 56 | -------------------------------------------------------------------------------- /patterns/competing-consumer.md: -------------------------------------------------------------------------------- 1 | # 竞争消费者模式 2 | 3 | 启用多个并发消费者来处理在同一个消息通道上收到的消息。这使系统能够同时处理多个消息,以优化吞吐量,提高可扩展性和可用性,并平衡工作负载。 4 | 5 | ## 背景和问题 6 | 7 | 在云上运行的应用程序预计将处理大量的请求。常见的技术是让应用程序通过消息系统传递给以异步方式处理它们的另一个服务(消费者服务),而不是同步处理请求。这个策略有助于确保应用程序中的业务逻辑在处理请求时不被阻塞。 8 | 9 | 由于许多原因,请求的数量可能随着时间而显着变化。来自多个租户的用户活动的聚合请求突然增加,可能会导致无法预测的工作量。在高峰时间,系统可能需要每秒处理数百个请求,而在其它时间,数量可能非常小。另外,为处理这些请求所进行的工作的性质可能是高度可变的。使用消费者服务的单个实例会导致该实例充斥着请求,或者消息系统可能由于来自应用程序的大量消息而过载。为了处理这种波动的工作量,系统可以运行消费者服务的多个实例。但是,必须协调这些消费者,以确保每封邮件只能发送给单个消费者。工作负载还需要在消费者之间进行负载平衡,以防止实例成为瓶颈。 10 | 11 | ## 解决方案 12 | 13 | 使用消息队列来实现应用程序和客户服务实例之间的通信通道。应用程序以消息的形式向队列发送请求,消费者服务实例从队列接收消息并处理消息。这种方法使相同的消费者服务实例池能够处理来自任何应用程序实例的消息。下图说明了使用消息队列将工作分配给服务的实例。 14 | ![](https://docs.microsoft.com/en-us/azure/architecture/patterns/_images/competing-consumers-diagram.png) 15 | 该解决方案具有以下优点: 16 | * 提供了一个负载均衡的系统,可处理由应用程序实例发送的不同数量的请求。队列充当应用程序实例和使用者服务实例之间的缓冲区。这可以帮助最小化应用程序和服务实例的可用性和响应性的影响,如[基于队列的负载均衡模式](queue-based-load-leveling.html)所述。处理需要一些长时间运行才能处理的消息不会阻止消费者服务的其它实例同时处理其它消息。 17 | * 提高了可靠性。如果一个生产者直接与消费者沟通,而不是使用这种模式,但不监督消费者,那么如果消费者失败,消息很可能会丢失或者不能被处理。在这种模式下,消息不会发送到特定的服务实例。失败的服务实例不会阻塞生产者,消息可以被任何工作服务实例处理。 18 | * 不需要消费者之间或生产者与消费者实例之间的复杂协调。消息队列确保每个消息至少被传递一次。 19 | * 可扩展。当消息量波动时,系统可以动态地增加或减少消费者服务的实例的数量。 20 | * 如果消息队列提供事务性读取操作,则可以提高弹性。如果消费者服务实例作为事务操作的一部分来读取和处理消息,并且消费者服务实例失败,则该模式可以确保消息将被返回到队列以被消费者服务的另一实例拾取和处理。 21 | ## 问题和注意事项 22 | 23 | 在决定如何实现该模式时,请考虑以下几点: 24 | * 消息排序。不能保证消费者服务实例接收消息的顺序,也不一定反映消息的创建顺序。设计系统以确保消息处理是幂等的,这将有助于消除对消息处理顺序的依赖。更多相关内容请参阅Jonathon Oliver博客上的[“幂等模式”](http://blog.jonathanoliver.com/idempotency-patterns/)。 25 | >Microsoft Azure服务总线队列可以通过使用消息会话来保证先进先出的消息排序。更多相关信息请参阅[使用会话的消息传送模式](https://msdn.microsoft.com/magazine/jj863132.aspx)。 26 | * 设计弹性服务。如果系统设计为检测并重新启动失败的服务实例,则可能有必要将服务实例执行的处理实现为幂等操作,以最大程度地减少单个消息被检索和处理多次的影响。 27 | * 检测有毒消息。格式错误的消息或需要访问不可用资源的任务可能导致服务实例失败。系统应该阻止这些消息返回到队列中,并且将这些消息的细节捕获并存储在其它地方,以便在需要时可以对其进行分析。 28 | * 处理结果。处理消息的服务实例与生成消息的应用程序逻辑完全分离,并且可能无法直接通信。如果服务实例生成的结果要传递回应用程序逻辑,则必须将此信息存储在两者均可访问的位置。为了防止应用程序逻辑检索不完整的数据,系统必须指示何时处理完成。 29 | >如果使用Azure,则工作进程可以使用专用的消息应答队列将结果传递回应用程序逻辑。应用程序逻辑必须能够将这些结果与原始消息相关联。这个场景在[异步消息入门](https://msdn.microsoft.com/library/dn589781.aspx)中有更详细的介绍。 30 | * 扩展消息传递系统。在一个大规模的解决方案中,单个消息队列可能被海量消息淹没,并成为系统中的一个瓶颈。在这种情况下,考虑将消息传递系统划分为将消息从特定的生产者发送到特定的队列,或使用负载均衡将消息分布到多个消息队列中。 31 | * 确保消息传递系统的可靠性。需要可靠的消息传递系统来保证应用程序排队后不会丢失。这对于确保所有消息至少交付一次至关重要。 32 | 33 | ## 何时使用该模式 34 | 在以下情况使用此模式: 35 | 36 | * 应用程序的工作负载分为可以异步运行的任务。 37 | * 任务是独立的,可以并行运行。 38 | * 工作量非常多变,需要一个可扩展的解决方案。 39 | * 解决方案必须提供高可用性,并且在任务处理失败时具有弹性。 40 | 41 | 以下情况可能不适合该模式: 42 | * 不容易将应用程序的工作量分解成离散任务,或者任务之间存在高度的依赖关系。 43 | * 任务必须同步执行,应用程序逻辑必须等待任务完成才能继续。 44 | * 任务必须按照特定的顺序进行。 45 | >某些消息系统支持会话,使生产者能够将消息分组在一起,并确保它们都由同一个消费者处理。这种机制可以与优先消息(如果支持的话)一起使用,以实现消息顺序的形式,消息按顺序从生产者传递给单个消费者。 46 | 47 | ## 案例 48 | 49 | Azure提供了存储队列和服务总线队列,可以充当实现这种模式的机制。应用程序逻辑可以将消息发布到队列中,并且作为一个或多个角色的任务实现的消费者,可以从这个队列中检索并处理消息。为了保持弹性,服务总线队列使消费者在从队列中检索消息时使用`PeekLock`模式。这种模式实际上并没有删除该消息,而只是将其从其它消费者隐藏。原消费者在完成处理后可以删除该消息。如果消费者失败,查看锁定将超时,消息将再次变为可见,从而允许另一个消费者检索它。 50 | 51 | >有关使用Azure服务总线队列的详细信息,请参阅服[务总线队列,主题和预订](https://msdn.microsoft.com/library/windowsazure/hh367516.aspx)。有关使用Azure存储队列的信息,请参阅[使用.NET上手Azure队列存储](https://azure.microsoft.com/documentation/articles/storage-dotnet-how-to-use-queues/)。 52 | 53 | 下面的代码来自GitHub上的`CompetingConsumers`解决方案中的`QueueManager`类,显示了如何通过在Web或Worker角色的`Start`事件处理程序使用`QueueClient`实例来创建队列。 54 | 55 | ```c# 56 | private string queueName = ...; 57 | private string connectionString = ...; 58 | ... 59 | 60 | public async Task Start() 61 | { 62 | // Check if the queue already exists. 63 | var manager = NamespaceManager.CreateFromConnectionString(this.connectionString); 64 | if (!manager.QueueExists(this.queueName)) 65 | { 66 | var queueDescription = new QueueDescription(this.queueName); 67 | 68 | // Set the maximum delivery count for messages in the queue. A message 69 | // is automatically dead-lettered after this number of deliveries. The 70 | // default value for dead letter count is 10. 71 | queueDescription.MaxDeliveryCount = 3; 72 | 73 | await manager.CreateQueueAsync(queueDescription); 74 | } 75 | ... 76 | 77 | // Create the queue client. By default the PeekLock method is used. 78 | this.client = QueueClient.CreateFromConnectionString( 79 | this.connectionString, this.queueName); 80 | } 81 | ``` 82 | 下面的代码段显示了应用程序如何创建一批消息并将其发送到队列。 83 | ```C# 84 | public async Task SendMessagesAsync() 85 | { 86 | // Simulate sending a batch of messages to the queue. 87 | var messages = new List(); 88 | 89 | for (int i = 0; i < 10; i++) 90 | { 91 | var message = new BrokeredMessage() { MessageId = Guid.NewGuid().ToString() }; 92 | messages.Add(message); 93 | } 94 | await this.client.SendBatchAsync(messages); 95 | } 96 | ``` 97 | 以下代码显示了消费者服务实例如何通过遵循事件驱动方法接收来自队列的消息。`ReceiveMessages`方法的`processMessageTask`参数是一个委托,它引用在收到消息时运行的代码。这段代码是异步运行的。 98 | ```C# 99 | 100 | private ManualResetEvent pauseProcessingEvent; 101 | ... 102 | 103 | public void ReceiveMessages(Func processMessageTask) 104 | { 105 | // Set up the options for the message pump. 106 | var options = new OnMessageOptions(); 107 | 108 | // When AutoComplete is disabled it's necessary to manually 109 | // complete or abandon the messages and handle any errors. 110 | options.AutoComplete = false; 111 | options.MaxConcurrentCalls = 10; 112 | options.ExceptionReceived += this.OptionsOnExceptionReceived; 113 | 114 | // Use of the Service Bus OnMessage message pump. 115 | // The OnMessage method must be called once, otherwise an exception will occur. 116 | this.client.OnMessageAsync( 117 | async (msg) => 118 | { 119 | // Will block the current thread if Stop is called. 120 | this.pauseProcessingEvent.WaitOne(); 121 | 122 | // Execute processing task here. 123 | await processMessageTask(msg); 124 | }, 125 | options); 126 | } 127 | ... 128 | 129 | private void OptionsOnExceptionReceived(object sender, 130 | ExceptionReceivedEventArgs exceptionReceivedEventArgs) 131 | { 132 | ... 133 | } 134 | ``` 135 | 请注意,自动缩放功能(如Azure中提供的功能)可用于在队列长度波动时启动和停止角色实例。更多相关内容,请参阅[自动缩放指南](https://msdn.microsoft.com/library/dn589774.aspx)。而且,角色实例和工作进程之间不需要保持一对一的对应关系 - 单个角色实例可以实现多个工作进程。更多相关信息,请参阅[计算资源合并模式](compute-resource-consolidation.html)。 136 | 137 | ## 相关的模式和指南 138 | 139 | 以下模式和指南在实施这种模式时可能是相关的: 140 | * [异步消息入门](https://msdn.microsoft.com/library/dn589781.aspx)。消息队列是一种异步通信机制。如果消费者服务需要向应用程序发送回复,则可能需要实现某种形式的回复消息。异步消息入门介绍了如何使用消息队列实现请求/回复消息的信息。 141 | * [自动缩放指南](https://msdn.microsoft.com/library/dn589774.aspx)。队列应用程序发布消息的长度各不相同,可能启动和停止消费者服务的实例。自动缩放可以帮助在峰值处理期间保持吞吐量。 142 | * [计算资源合并模式](compute-resource-consolidation.md)。存在将消费者服务的多个实例合并到单个进程中以降低成本和管理开销的可能性。计算资源合并模式介绍了遵循这种方法的好处和权衡。 143 | * [基于队列的负载均衡模式](queue-based-load-leveling.md)。引入消息队列可以为系统增加弹性,使服务实例能够处理来自应用程序实例的大量不同的请求。消息队列充当缓冲区,负载均衡。基于队列的负载均衡模式更详细地描述了这种情况。 144 | * 和该模式相关的一个[示例应用程序](https://github.com/mspnp/cloud-design-patterns/tree/master/competing-consumers)。 -------------------------------------------------------------------------------- /patterns/compute-resource-consolidation.md: -------------------------------------------------------------------------------- 1 | # 计算资源整合模式 2 | 3 | 将多个任务或操作合并到一个计算单元中。这可以提高计算资源利用率,并降低与在云托管应用程序中执行计算处理相关的成本和管理开销。 4 | 5 | ## 背景和问题 6 | 7 | 云应用程序经常实现各种操作。在一些解决方案中,遵循最初关注分离的设计原则是有意义的,并将这些操作划分为单独的计算单元,这些计算单元分别托管和部署(例如,作为单独的App Service Web应用程序,单独的虚拟机或单独的Cloud Service角色)。但是,尽管这种策略可以帮助简化解决方案的逻辑设计,但将大量计算单元作为同一应用程序的一部分来部署会增加运行时托管成本,并使系统管理更为复杂。 8 | 9 | 例如,下图显示了使用多个计算单元实现的云托管解决方案的简化结构。每个计算单元都运行在自己的虚拟环境中。每个功能都是作为一个单独的任务来实现的(标记为任务A到任务E)以自己的计算单位运行。 10 | 11 | ![](https://docs.microsoft.com/en-us/azure/architecture/patterns/_images/compute-resource-consolidation-diagram.png) 12 | 13 | 每个计算单位即使闲置或轻微使用,也会消耗收费的资源。因此,这并不总是最具成本效益的解决方案。 14 | 这种担心适用于Azure的Cloud Service,App Services和虚拟机中的角色。这些项目在自己的虚拟环境中运行。运行一组单独的角色,网站或虚拟机,这些角色,网站或虚拟机旨在执行一组明确定义的操作,但需要作为单一解决方案的一部分进行通信和合作,可能会导致资源的低效使用。 15 | 16 | ## 解决方案 17 | 18 | 为了帮助降低成本,提高利用率,提高通信速度并减少管理,可以将多个任务或操作合并为一个计算单元。 19 | 20 | 可以根据环境提供的功能和与这些功能相关的成本,将任务分组。一种常见的方法是查找具有与其可扩展性,生命周期和处理要求相似的任务。组合在一起使它们成为一个整体。许多云环境提供的弹性使计算单元的其它实例可以根据工作负载来启动和停止。例如,Azure提供了自动伸缩,可以将其应用于Cloud Service,App Services和虚拟机中的角色。更多相关内容,请参阅[自动缩放指南](https://msdn.microsoft.com/library/dn589774.aspx)。 21 | 22 | 作为一个反例来说明如何使用可伸缩性来确定哪些操作不应该组合在一起,请考虑以下两个任务: 23 | 24 | * 任务1轮询发送到队列的偶发的,时间不敏感的消息。 25 | * 任务2处理大量的网络流量突发。 26 | 27 | 第二个任务需要弹性,可能涉及到启动和停止计算单元的大量实例。对第一个任务应用相同的伸缩比例只会导致更多的任务在同一队列上侦听不频繁的消息,并且浪费资源。 28 | 29 | 在许多云环境中,可以根据CPU核心数量,内存,磁盘空间等指定可用于计算单元的资源。一般来说,指定的资源越多,成本越高。为了节省资金,重要的是要使昂贵的计算单元执行的工作最大化,并且不要让它在很长一段时间内变为非活动状态。 30 | 如果有些任务在短时间内需要大量的CPU,请考虑将这些任务合并到一个提供必要能力的计算单元中。然而,需要在昂贵的资源保持忙碌和让它们过载之间保持平衡是非常重要的。例如,长时间运行的计算密集型任务不应共享相同的计算单位。 31 | 32 | ## 问题和注意事项 33 | 34 | 实施此模式时请考虑以下几点: 35 | * **可伸缩性和弹性**。许多云计算解决方案通过启动和停止单元实例来实现计算单元级别的可伸缩性和弹性。避免在相同的计算单元中对具有可伸缩性要求冲突的任务进行分组。 36 | * **生命周期**。云基础架构定期回收承载计算单元的虚拟环境。当一个计算单元内有很多长时间运行的任务时,可能需要配置该单元以防止它在这些任务完成之前被回收。或者,通过使用检查点方法设计任务,使其能够干净地停止,并在计算单元重新启动时在中断处继续。 37 | * **发布节奏**。如果任务的实现或配置频繁更改,则可能需要停止承载更新代码的计算单元,重新配置和部署单元,然后重新启动它。这个过程还要求停止,重新部署和重新启动同一计算单元内的所有其它任务。 38 | * **安全**。同一计算单元中的任务可能共享相同的安全上下文并访问相同的资源。任务之间必须有高度的信任,相信一项任务不会对另一项任务产生不利影响。此外,增加计算单元中运行的任务数量会增加单元的攻击面。每个任务只与具有最多漏洞的任务一样安全。 39 | * **容错**。如果计算单元中的一个任务失败或行为异常,则可能影响同一单元内运行的其它任务。例如,如果某个任务无法正确启动,则可能导致计算单元的整个启动逻辑失败,并阻止同一单元中的其它任务运行。 40 | * **竞争**。避免在同一计算单元中争夺资源的任务之间引入竞争。理想情况下,共享相同计算单元的任务应该表现出不同的资源利用特性。例如,两个计算密集型任务可能不应该驻留在相同的计算单元中,也不应该占用大量内存的两个任务也不应该在一起。但是,将计算密集型任务与需要大量内存的任务混合在一起是可行的组合。 41 | >注意 42 | >考虑整合计算资源只能用于一段时间内已经投入生产的系统,这样运维和开发人员才能监视系统并创建一个热图来标识每个任务如何利用不同的资源。该图可用于确定哪些任务是共享计算资源的合适候选者。 43 | * **复杂性**。将多个任务合并到一个单一的计算单元会增加单元中代码的复杂性,可能使测试,调试和维护变得更加困难。 44 | * **稳定的逻辑架构**。在每个任务中设计并实现代码,使其不需要更改,即使任务运行的物理环境确实发生了变化。 45 | * **其他策略**。整合计算资源只是帮助降低与同时运行多个任务相关的成本的一种方法。这需要仔细的计划和监督,以确保它仍然是一个有效的方法。其它策略可能更合适,这取决于工作的性质以及这些任务运行的用户所在的位置。例如,对工作负载的功能分解(如[计算分区指南](https://msdn.microsoft.com/library/dn589773.aspx)所述)可能是更好的选择。 46 | 47 | ## 何时使用该模式 48 | 49 | 如果这些模式以自己的计算单位运行,那么使用这种模式性价比不高。如果任务大部分时间闲置,那么在专用单元中执行此任务可能会很昂贵。 50 | 这种模式可能不适用于执行关键容错操作的任务,或者处理高度敏感或私有数据并需要其自身安全上下文的任务。这些任务应该在独立的环境中运行,在一个单独的计算单元中运行。 51 | 52 | ## 案例 53 | 54 | 在Azure上构建云服务时,可以将多个任务执行的处理合并为一个角色。通常这是执行后台或异步处理任务的工作者角色。 55 | 56 | > 在某些情况下,可以在Web角色中包含后台或异步处理任务。这种技术有助于降低成本并简化部署,但它会影响Web角色提供的面向公众的接口的可伸缩性和响应能力。[将多个Azure角色组合到Azure Web角色中](http://www.31a2ba2a-b718-11dc-8314-0800200c9a66.com/2012/02/combining-multiple-azure-worker-roles.html)的文章包含了在Web角色中实现后台或异步处理任务的详细说明。 57 | 58 | 角色负责启动和停止任务。当Azure结构控制器加载角色时,会引发该角色的“启动”事件。可以重写`WebRole`或`WorkerRole`类的`OnStart`方法来处理此事件,也许可以初始化此方法中任务所依赖的数据和其它资源。 59 | 60 | 当`OnStartmethod`完成时,角色可以开始响应请求。可以在模式与实践指南中的[“应用程序启动过程”](https://msdn.microsoft.com/library/ff728592.aspx)部分找到有关在角色中使用`OnStart`和`Run`方法的更多内容。 61 | 62 | >保持OnStart方法中的代码尽可能简洁。 Azure对此方法完成所花费的时间没有任何限制,但在此方法完成之前,角色将无法响应发送给它的网络请求。 63 | 64 | 当`OnStart`方法完成时,角色执行`Run`方法。此时,结构控制器可以开始向角色发送请求。 65 | 将实际创建任务的代码放在`Run`方法中。请注意,`Run`方法定义了角色实例的生命周期。当此方法完成时,结构控制器将安排角色关闭。 66 | 当角色关闭或回收时,结构控制器会阻止从负载均衡器接收到更多传入请求,并引发`Stop`事件。可以通过重写角色的`OnStop`方法来捕获此事件,并在角色终止之前执行所需的任何整理。 67 | 68 | >在`OnStop`方法中执行的任何操作都必须在五分钟内完成(如果在本地计算机上使用Azure模拟器,则必须等待30秒)。否则,Azure结构控制器将假定角色已停止并将强制停止。 69 | 70 | Run方法启动任务并等待任务完成的。这些任务实现了云服务的业务逻辑,并可以通过Azure负载均衡器响应发布到角色的消息。下图显示了Azure云服务中角色中任务和资源的生命周期。 71 | 72 | ![](https://docs.microsoft.com/en-us/azure/architecture/patterns/_images/compute-resource-consolidation-lifecycle.png) 73 | 74 | `ComputeResourceConsolidation.Worker`项目中的`WorkerRole.cs`文件展示了一个例子,说明如何在Azure云服务中实现此模式。 75 | 76 | >`ComputeResourceConsolidation.Worker`项目是`ComputeResourceConsolidation`解决方案的一部分,可以从[GitHub下载](https://github.com/mspnp/cloud-design-patterns/tree/master/compute-resource-consolidation)。 77 | 78 | `MyWorkerTask1`和`MyWorkerTask2`方法说明了如何在同一个辅助角色中执行不同的任务。以下代码展示了`MyWorkerTask1`。这是一个简单的任务,休眠30秒,然后输出一个跟踪消息。它重复这个过程直到任务取消。 `MyWorkerTask2`的代码和这个类似。 79 | 80 | ```C# 81 | 82 | // A sample worker role task. 83 | private static async Task MyWorkerTask1(CancellationToken ct) 84 | { 85 | // Fixed interval to wake up and check for work and/or do work. 86 | var interval = TimeSpan.FromSeconds(30); 87 | 88 | try 89 | { 90 | while (!ct.IsCancellationRequested) 91 | { 92 | // Wake up and do some background processing if not canceled. 93 | // TASK PROCESSING CODE HERE 94 | Trace.TraceInformation("Doing Worker Task 1 Work"); 95 | 96 | // Go back to sleep for a period of time unless asked to cancel. 97 | // Task.Delay will throw an OperationCanceledException when canceled. 98 | await Task.Delay(interval, ct); 99 | } 100 | } 101 | catch (OperationCanceledException) 102 | { 103 | // Expect this exception to be thrown in normal circumstances or check 104 | // the cancellation token. If the role instances are shutting down, a 105 | // cancellation request will be signaled. 106 | Trace.TraceInformation("Stopping service, cancellation requested"); 107 | 108 | // Rethrow the exception. 109 | throw; 110 | } 111 | } 112 | ``` 113 | >示例代码显示了后台进程的常见实现。在真实世界的应用程序中,可以遵循相同的结构,只是应该将处理逻辑放在等待取消请求的循环体中。 114 | 115 | 在worker角色初始化所使用的资源之后,Run方法将同时启动两个任务,如下所示。 116 | ```C# 117 | 118 | /// 119 | /// The cancellation token source use to cooperatively cancel running tasks 120 | /// 121 | private readonly CancellationTokenSource cts = new CancellationTokenSource(); 122 | 123 | /// 124 | /// List of running tasks on the role instance 125 | /// 126 | private readonly List tasks = new List(); 127 | 128 | // RoleEntry Run() is called after OnStart(). 129 | // Returning from Run() will cause a role instance to recycle. 130 | public override void Run() 131 | { 132 | // Start worker tasks and add to the task list 133 | tasks.Add(MyWorkerTask1(cts.Token)); 134 | tasks.Add(MyWorkerTask2(cts.Token)); 135 | 136 | foreach (var worker in this.workerTasks) 137 | { 138 | this.tasks.Add(worker); 139 | } 140 | 141 | Trace.TraceInformation("Worker host tasks started"); 142 | // The assumption is that all tasks should remain running and not return, 143 | // similar to role entry Run() behavior. 144 | try 145 | { 146 | Task.WaitAll(tasks.ToArray()); 147 | } 148 | catch (AggregateException ex) 149 | { 150 | Trace.TraceError(ex.Message); 151 | 152 | // If any of the inner exceptions in the aggregate exception 153 | // are not cancellation exceptions then re-throw the exception. 154 | ex.Handle(innerEx => (innerEx is OperationCanceledException)); 155 | } 156 | 157 | // If there wasn't a cancellation request, stop all tasks and return from Run() 158 | // An alternative to canceling and returning when a task exits would be to 159 | // restart the task. 160 | if (!cts.IsCancellationRequested) 161 | { 162 | Trace.TraceInformation("Task returned without cancellation request"); 163 | Stop(TimeSpan.FromMinutes(5)); 164 | } 165 | } 166 | ... 167 | ``` 168 | 在这个例子中,Run方法等待任务完成。如果任务取消,则Run方法假定角色正在关闭,并在完成之前等待剩余的任务取消(终止之前最多等待5分钟)。如果某个任务由于预期的异常而失败,则Run方法将取消该任务。 169 | 170 | >可以在Run方法中实施更全面的监视和异常处理策略,例如重启失败的任务,或包含使角色停止和启动单个任务的代码。 171 | 172 | 当结构控制器关闭角色实例(从`OnStop`方法调用的)时,将调用以下代码中的`Stop`方法。代码通过取消它来优雅地停止每个任务。 如果任何任务完成要超过五分钟,则Stop方法中的取消处理停止等待并且终止角色。 173 | 174 | ```C# 175 | // Stop running tasks and wait for tasks to complete before returning 176 | // unless the timeout expires. 177 | private void Stop(TimeSpan timeout) 178 | { 179 | Trace.TraceInformation("Stop called. Canceling tasks."); 180 | // Cancel running tasks. 181 | cts.Cancel(); 182 | 183 | Trace.TraceInformation("Waiting for canceled tasks to finish and return"); 184 | 185 | // Wait for all the tasks to complete before returning. Note that the 186 | // emulator currently allows 30 seconds and Azure allows five 187 | // minutes for processing to complete. 188 | try 189 | { 190 | Task.WaitAll(tasks.ToArray(), timeout); 191 | } 192 | catch (AggregateException ex) 193 | { 194 | Trace.TraceError(ex.Message); 195 | 196 | // If any of the inner exceptions in the aggregate exception 197 | // are not cancellation exceptions then rethrow the exception. 198 | ex.Handle(innerEx => (innerEx is OperationCanceledException)); 199 | } 200 | } 201 | ``` 202 | 203 | ## 相关的模式和指南 204 | 205 | 以下模式和指南在实施这种模式时可能是相关的: 206 | 207 | * [自动缩放指南](https://msdn.microsoft.com/library/dn589774.aspx)。自动缩放可用于启动和停止服务托管计算资源的实例,具体取决于预期的处理需求。 208 | * [计算分区指南](https://msdn.microsoft.com/library/dn589773.aspx)。本文介绍了如何在云服务中分配服务和组件,以便在保持服务的可伸缩性,性能,可用性和安全性的同时最大限度降低运营成本。 209 | * 该模式的一个[示例应用程序](https://github.com/mspnp/cloud-design-patterns/tree/master/compute-resource-consolidation)。 -------------------------------------------------------------------------------- /patterns/cqrs.md: -------------------------------------------------------------------------------- 1 | # 命令和查询责任分离模式 2 | 3 | 使用不同的接口,分离更新数据的操作和读取数据的操作。可以最大限度地提高性能,可扩展性和安全性。通过更高的灵活性支持系统随时间演进,并防止更新命令在域级别引起合并冲突。 4 | 5 | ## 背景和问题 6 | 7 | 在传统的数据管理系统中,命令(对数据的更新)和查询(对数据的请求)是针对单个数据存储库中的同一组实体执行的。这些实体可以是关系数据库(如SQL Server)中的一个或多个表中的行的子集。 8 | 通常在这些系统中,所有创建,读取,更新和删除(CRUD)操作都应用于实体的相同表示。例如,通过数据访问层(DAL)从数据存储器检索表示客户的数据传输对象(DTO)并在屏幕上显示。用户更新DTO的某些字段(可能通过数据绑定),然后DAL保存DTO到数据存储区。 同样的DTO用于读写操作。下图介绍了传统的CRUD架构。 9 | 10 | ![](https://docs.microsoft.com/en-us/azure/architecture/patterns/_images/command-and-query-responsibility-segregation-cqrs-tradition-crud.png) 11 | 12 | 当只有有限的业务逻辑被应用于数据操作时,传统的CRUD设计才能正常工作。开发工具提供的脚手架可以非常快速地创建数据访问代码,然后可以根据需要定制。 13 | 14 | 然而,传统的CRUD方法有一些缺点: 15 | * 这通常意味着数据的读取和写入表示之间存在不匹配的情况,例如必须正确更新的附加列或属性,即使操作不要求这部分。 16 | * 当记录锁定在协作域中的数据存储中时,数据争用就会发生风险,其中多个角色在同一组数据上并行操作。或者在并发更新使用乐观锁时引起的冲突。这些风险随着系统的复杂性和吞吐量的增加而增加。此外,由于数据存储和数据访问层的负载以及检索信息所需的查询的复杂性,传统的方法可能会对性能产生负面影响。 17 | * 让管理安全性和权限更加复杂,因为每个实体都受读写操作的限制,这可能会将数据暴露在错误的上下文中。 18 | > 为了更深入地了解CRUD方法的限制,请参阅[只有可以承担时使用CRUD](https://blogs.msdn.microsoft.com/maarten_mullender/2004/07/23/crud-only-when-you-can-afford-it-revisited/) 19 | 20 | ## 解决方案 21 | 22 | 命令和查询责任分离(CQRS)是一种模式,它通过使用单独的接口来隔离更新数据(命令)的操和读取数据(查询)的操作。这意味着用于查询和更新的数据模型是不同的。然后,模型可以如下图所示被隔离,尽管这不是绝对要求。 23 | 24 | ![](https://docs.microsoft.com/en-us/azure/architecture/patterns/_images/command-and-query-responsibility-segregation-cqrs-basic.png) 25 | 26 | 与基于CRUD的系统中使用的单一数据模型相比,CQRS的系统中使用单独的查询和更新模型来简化设计和实现。然而,与CRUD设计不同,CQRS代码的缺点是不能使用脚手架自动生成。 27 | 28 | 用于读取数据的查询模型和用于写入数据的更新模型可以访问相同的物理存储,也许通过使用SQL视图或通过快速生成映射。然而,通常我们会将数据分成不同的物理存储,以最大限度地提高性能,可扩展性和安全性,如下图所示。 29 | 30 | ![](https://docs.microsoft.com/en-us/azure/architecture/patterns/_images/command-and-query-responsibility-segregation-cqrs-separate-stores.png) 31 | 32 | 读存储可以是写存储的只读副本,或者读写存储可以具有完全不同的结构。使用读取存储的多个只读副本可以大大提高查询性能和应用程序UI响应,特别是在只读副本靠近应用程序实例分布式场景中。某些数据库系统(SQL Server)提供了诸如故障转移副本的附加功能,以最大限度地提高可用性。 33 | 读写存储器的分离也允许每个存储器适当地伸缩以匹配负载。例如,读存储器通常遇到比写存储器高得多的负载。 34 | 当查询/读模型包含非规范化数据(请参阅* [物化视图模式](materialized-view.md))时,为应用程序中的每个视图读取数据或查询系统中的数据时,性能将最大化。 35 | 36 | ### 问题和注意事项 37 | 38 | 在决定如何实现此模式时,请考虑以下几点: 39 | * 将数据存储分为单独的物理存储以进行读写操作可以提高系统的性能和安全性,但它增加弹性和最终一致性的复杂性。必须更新读模型存储以反映对写模型存储的更改,并且难以检测用户何时发出请求基于陈旧数据的读取请求,这样的操作无法完成。 40 | > 有关最终一致性的描述,请参阅[数据一致性入门](https://msdn.microsoft.com/library/dn589800.aspx)。 41 | * 考虑将CQRS应用于系统最有价值的部分。 42 | * 部署最终一致性的典型方法是将事件溯源与CQRS结合使用,写模型是仅由执行命令驱动的附加事件流。这些事件用于更新充当读模型的物化视图。更多相关信息,请参阅[事件溯源和CQRS](https://msdn.microsoft.com/library/dn568103.aspx#EventSourcingandCQRS)。 43 | 44 | ## 何时使用该模式 45 | 46 | 在以下场景使用此模式: 47 | 48 | * 在相同数据上并行执行多个操作的协作领域。 CQRS允许以适当的粒度定义命令,以最小化领域级别的合并冲突(即使出现任何冲突也可以由命令合并),即使看起来更新的是相同类型的数据。 49 | * 基于任务的用户界面,其中用户通过一系列步骤或复杂领域模型引导复杂的进程。此外,对于已经熟悉域驱动设计(DDD)技术的团队来说,这是有用的。写模型具有完整的命令处理栈,业务逻辑,输入验证和业务验证,以确保写模型中的每个聚合(每个相关对象的集群作为数据更改的单位处理)中的所有内容始终保持一致。读模型没有业务逻辑或验证堆栈,并且仅返回用于视图模型的DTO。读模型最终与写模型一致。 50 | * 数据读取性能必须与数据写入的性能分开进行优化,特别是当读/写比率非常高,以及需要水平缩放时。例如,在许多系统中,读操作的次数是写操作的许多倍。为了适应这一点,请考虑扩展读模型,仅在一个或几个实例上运行写模型。少量的写入模型实例也有助于最小化合并冲突的发生。 51 | * 一个开发团队可以专注于作为写模型一部分的复杂域模型的场景,另一个团队可以专注于读模型和用户界面。 52 | * 系统预期随着时间的推移而演进的场景,可能包含多个版本的模型,或业务规则定期更改的情况。 53 | * 与其它系统集成,特别是与事件溯源相结合,其中某个子系统的临时故障不会影响其它系统的可用性。 54 | 55 | 在以下场景不推荐使用此模式: 56 | * 领域或业务规则简单的地方。 57 | * 简单的CRUD风格的用户接口和相关的数据访问操作就足够了。 58 | * 用于整个系统的实现。CQRS对于整体数据管理方案的特定组件可能是有用的,对于不需要它的地方则增加相当的、不必要的复杂性。 59 | 60 | ## 事件溯源和CQRS 61 | 62 | CQRS模式经常与事件溯源模式一起使用。基于CQRS的系统使用单独的读写数据模型,每个模型针对相关任务进行定制,并且通常位于物理上分开的存储中。与事件溯源模式一起使用时,事件存储是写模型,并且是合法的信息来源。基于CQRS的系统的读模型提供了数据的物化视图,通常是高度非规范化的视图。这些视图是针对应用程序的接口和显示要求量身定制的,有助于最大化显示和查询性能。 63 | 使用事件流作为写存储,而不是某个时间点的实际数据,避免了单个聚合的更新冲突,并最大限度地提高了性能和可扩展性。事件可以以异步的方式生成用于填充读存储数据的物化视图。 64 | 由于事件存储是合法的信息来源,因此可以删除物化视图并重播所有过去的事件,以便在系统演进或者读模型必须更改时,创建当前状态的新表示。物化视图实际上是数据持久的只读缓存。 65 | 66 | 使用CQRS结合事件溯源模式时,请考虑以下几点: 67 | 68 | * 与读写存储分离的任何系统一样,基于此模式的系统都是最终一致的。正在生成的事件和正在更新的数据存储之间会有一些延迟。 69 | * 该模式增加了复杂性,因为必须创建代码来启动和处理事件,并组合或更新查询或读模型所需的适当视图或对象。当采用事件采购模式时,CQRS模式的复杂性导致实现的困难性,同时需要采用不同的方法来设计系统。但是,事件溯源更容易对域进行建模,因为保留了数据更改的意图,更容易地重建视图或创建新的视图,。 70 | * 通过重放和处理特定实体或实体集合的事件,生成用于读模型或数据预测中的物化视图可能需要大量的时间和资源去处理。尤其是需要长时间的总结或分析值情况下,因为所有相关事件可能都需要检查。解决这个问题需要通过按计划的间隔实现数据的快照,例如已经发生的特定操作的数量的总数或实体的当前状态。 71 | 72 | ## 案例 73 | 74 | 下面的例子是从使用不同定义的读和写模型的CQRS实现中提取出的部分代码。模型接口不规定底层数据存储的任何特性,并且它们可以独立进行演进和调优,因为这些接口是分开的。 75 | 以下代码显示了读模型定义。 76 | 77 | ```java 78 | // Query interface 79 | namespace ReadModel 80 | { 81 | public interface ProductsDao 82 | { 83 | ProductDisplay FindById(int productId); 84 | ICollection FindByName(string name); 85 | ICollection FindOutOfStockProducts(); 86 | ICollection FindRelatedProducts(int productId); 87 | } 88 | 89 | public class ProductDisplay 90 | { 91 | public int Id { get; set; } 92 | public string Name { get; set; } 93 | public string Description { get; set; } 94 | public decimal UnitPrice { get; set; } 95 | public bool IsOutOfStock { get; set; } 96 | public double UserRating { get; set; } 97 | } 98 | 99 | public class ProductInventory 100 | { 101 | public int Id { get; set; } 102 | public string Name { get; set; } 103 | public int CurrentStock { get; set; } 104 | } 105 | } 106 | ``` 107 | 该系统允许用户评价产品。应用程序代码使用代码中的`RateProduct`命令执行此操作。 108 | 109 | ```java 110 | public interface ICommand 111 | { 112 | Guid Id { get; } 113 | } 114 | 115 | public class RateProduct : ICommand 116 | { 117 | public RateProduct() 118 | { 119 | this.Id = Guid.NewGuid(); 120 | } 121 | public Guid Id { get; set; } 122 | public int ProductId { get; set; } 123 | public int Rating { get; set; } 124 | public int UserId {get; set; } 125 | } 126 | ``` 127 | 128 | 系统使用`ProductsCommandHandler`类来处理应用程序发送的命令。客户端通常通过诸如队列的消息系统向域发送命令。命令处理程序接受这些命令并调用域接口的方法。每个命令的粒度旨在减少冲突请求的机会。以下代码显示了`ProductsCommandHandler`类的大概内容。 129 | 130 | ```java 131 | public class ProductsCommandHandler : 132 | ICommandHandler, 133 | ICommandHandler, 134 | ICommandHandler, 135 | ICommandHandler, 136 | ICommandHandler 137 | { 138 | private readonly IRepository repository; 139 | 140 | public ProductsCommandHandler (IRepository repository) 141 | { 142 | this.repository = repository; 143 | } 144 | 145 | void Handle (AddNewProduct command) 146 | { 147 | ... 148 | } 149 | 150 | void Handle (RateProduct command) 151 | { 152 | var product = repository.Find(command.ProductId); 153 | if (product != null) 154 | { 155 | product.RateProduct(command.UserId, command.Rating); 156 | repository.Save(product); 157 | } 158 | } 159 | 160 | void Handle (AddToInventory command) 161 | { 162 | ... 163 | } 164 | 165 | void Handle (ConfirmItemsShipped command) 166 | { 167 | ... 168 | } 169 | 170 | void Handle (UpdateStockFromInventoryRecount command) 171 | { 172 | ... 173 | } 174 | } 175 | ``` 176 | 177 | 以下代码展示了写模型中的`IProductsDomain`接口。 178 | ```java 179 | public interface IProductsDomain 180 | { 181 | void AddNewProduct(int id, string name, string description, decimal price); 182 | void RateProduct(int userId, int rating); 183 | void AddToInventory(int productId, int quantity); 184 | void ConfirmItemsShipped(int productId, int quantity); 185 | void UpdateStockFromInventoryRecount(int productId, int updatedQuantity); 186 | } 187 | ``` 188 | 189 | 还要注意`IProductsDomain`接口是如何包含域中含义的方法。通常,在CRUD环境中,这些方法具有通用名称,例如Save或者Update,并将DTO作为唯一的参数。CQRS方法可以设计为满足本组织业务和目录管理系统的需求。 190 | 191 | ## 相关模式和指南 192 | 193 | 以下模式和指南在实现此模式时很有用: 194 | * 对于CQR​​S与其它架构风格的比较,请参见[架构风格]()和[CQRS架构风格](https://docs.microsoft.com/en-us/azure/architecture/guide/architecture-styles/cqrs)。 195 | * [数据一致性入门](https://msdn.microsoft.com/library/dn589800.aspx)介绍了在使用CQRS模式时读写数据存储之间的最终一致性,以及这些问题如何解决,常见问题等。 196 | * [数据分区指南](https://msdn.microsoft.com/library/dn589795.aspx)。介绍如何将CQRS模式中使用的读写数据存储区划分为可以单独管理和访问的分区,以提高可扩展性,减少争用并优化性能。 197 | * [事件溯源模式](event-sourcing.md)。更详细地介绍了如何使用CQRS模式来实现事件溯源,以简化复杂域中的任务,同时提高性能,可扩展性和响应能力。以及如何为事务提供数据一致性,同时保持完整的审计跟踪和历史,实现补偿措施。 198 | * [物化视图模式](materialized-view.md)。CQRS实现的读模型可以包含写模型数据的物化视图,或者可以使用读模型来生成物化视图。 199 | * 模式与实践指南的[CQRS之旅](http://aka.ms/cqrs)。特别介绍了[命令查询责任分离模式](https://msdn.microsoft.com/library/jj591573.aspx),以及这种模式何时有用,[结语:经验教训](https://msdn.microsoft.com/library/jj591568.aspx)部分可帮助你了解使用此模式时出现的一些问题。 200 | * [Martin Fowler关于CQRS的博客](http://martinfowler.com/bliki/CQRS.html),解释了模式的基础知识和与其它有用资源的链接。 201 | * [Greg Young的博客](http://codebetter.com/gregyoung/),探讨了CQRS模式。 -------------------------------------------------------------------------------- /patterns/event-sourcing.md: -------------------------------------------------------------------------------- 1 | # 事件溯源模式 2 | 3 | 使用只可追加的存储来记录对数据所进行的所有操作,而不是存储领域数据的当前状态。该存储作为记录系统可用于实现领域对象。通过避免数据模型与业务领域之间的同步,该模式简化了复杂领域下的工作,同时改善了性能、可扩展性和响应能力。还能够提供事务数据一致性,并维护了全部的审计记录与历史,可用于修正操作。 4 | 5 | ## 背景与问题 6 | 7 | 大部分应用都需要和数据打交道,典型的方法是由应用维护数据的当前状态,在用户使用时对数据进行更新。例如,在传统的增删改查(CRUD)模型下,典型的数据处理过程是,从数据库中读取该数据,做一些修改后再用新值去更新数据的当前状态——通常会使用带锁的事务。 8 | 9 | 这种CRUD方法有一些局限性: 10 | 11 | * CRUD系统在数据库上直接执行更新操作,由于需要开销,会降低系统的性能与响应能力,限制可扩展性。 12 | * 在一个由多个用户并发操作的领域中,数据更新更有可能会起冲突,因为更新操作发生在同一条单独的数据项上。 13 | * 除非在某个单独的日志中存在额外的审计机制来记录每个操作的细节,否则历史记录会丢失。 14 | 15 | > 关于对CRUD方法局限性的更深入了解,见[CRUD, Only When You Can Afford It](https://msdn.microsoft.com/library/ms978509.aspx)。 16 | 17 | ## 解决方案 18 | 19 | 事件溯源模式对于由一系列事件驱动产生的数据定义了一套处理方法,每个事件被记录在一个只可追加的存储中。应用代码发送一系列事件,这些事件命令式地描述了数据上产生的每个操作,并持久化到事件数据库中。每个事件代表对数据的一系列变化(例如`AddedItemToOrder`)。 20 | 21 | 这些事件持久化在一个事件数据库中(可信的数据源),作为记录数据当前状态的系统。事件数据库通常会发布这些事件,以便通知到消费者,并由消费者进行所需的处理。例如,消费者可以在其它系统中利用这些事件来初始化一些操作任务,或者执行完成整个操作所需的其它相关动作。注意,产生事件的应用代码是与订阅事件的系统相解耦的。 22 | 23 | 事件数据库所发布事件的通常用途是在应用改变实体状态时维护实体的物化视图,以及与外部系统集成。例如,一个系统可以维护所有客户订单的物化视图,用于生成用户界面的一部分。当应用中新增订单,添加或删除订单中的商品项,以及添加配送信息时,可以对描述这些变化的事件进行处理并更新这个[物化视图](patterns/materialized-view.md)。 24 | 25 | 此外,任何时间点上应用都可以读取到事件历史,用来物化某个实体的当前状态,只需要重新播放与该实体相关的所有事件即可。这可以发生在需要处理某个实体对象的物化请求时,或者发生在某个计划任务中,将实体的状态以物化视图形式存储起来,以便支持展示层的需要。 26 | 27 | 下图显示了该模式的概览,包括使用事件流的一些方式,比如创建物化视图,通过事件与外部应用和系统进行集成,以及通过重新播放事件来建立特定实体的当前状态。 28 | 29 | ![](https://docs.microsoft.com/en-us/azure/architecture/patterns/_images/event-sourcing-overview.png) 30 | 31 | 事件溯源模式可以提供如下这些优点: 32 | 33 | 事件是不可变的,可通过只可追加的方式进行存储。用户界面、工作流或者初始化事件的过程都可以继续进行,处理事件的任务可以在后台运行。因此,再加上由于在整个事务处理过程中不存在冲突,便可极大地改善应用的性能与可扩展性,特别是对于展示层或用户界面。 34 | 35 | 事件是描述已发生动作的一些简单对象,包含了事件在描述动作时所需的任何相关数据。事件并不直接更新数据库。它们只是被简单地记录下来以便在合适的时间进行处理。这可以简化实现和管理。 36 | 37 | 鉴于对象关系不匹配时会造成复杂数据库表的难于理解,事件对于领域专家通常是有特殊意义的。数据库表是用于表示系统当前状态的人为概念,而不是所有已发生的事件。 38 | 39 | 事件可以帮助避免引起冲突的并发更新,因为它避免了直接更新数据库中对象的需要。尽管如此,领域对象仍然要设计得能够防止造成不一致状态。 40 | 41 | 只可追加的事件存储提供了一种审计记录,可用于监控在数据库上发生的动作,以物化视图方式或者任何时刻重新播放事件的方式重新生成当前状态,有助于系统测试和调试。此外,如果需要通过修正事件来取消某些变更,可以通过对历史变更进行反向操作进行,而如果模型中只是简单存储了当前状态的话就不行了。事件列表还可用来分析应用性能和检测用户行为趋势,或者获得其他有用的业务信息。 42 | 43 | 事件存储产生事件,任务执行操作来响应这些事件。事件与任务的解耦提供了灵活性与可扩展性。任务知道事件类型与事件数据,却并不知道触发这些事件的操作。此外,每个事件可以被多任务处理。这提供了与其它服务和系统集成的方便方法,只需要监听由事件存储产生的新事件就可以了。尽管如此,事件溯源倾向于发生在非常低的层次上,有可能需要再生成一些特定的集成事件。 44 | 45 | > 事件溯源经常与CQRS模式一起使用,执行数据管理任务来响应事件,以及从存储的事件中产生物化视图。 46 | 47 | 48 | ## 问题与注意事项 49 | 50 | 在决定如何使用该模式时需考虑以下几点: 51 | 52 | 当创建物化视图或者通过重播事件产生数据最终状态时,系统只能保证最终一致性。在应用将请求处理的结果作为事件向事件存储中添加时、事件被发布时以及事件消费者进行处理之间,是存在一定延迟的。在这期间,有可能因实体产生更多变化而产生新的事件存入事件存储中。 53 | 54 | > 注: 55 | > 56 | > 关于最终一致性的更多信息参见[Data Consistency Primer](https://msdn.microsoft.com/library/dn589800.aspx)。 57 | 58 | 事件存储是信息的不变来源,所以事件数据永不应该被更新。唯一一种对实体进行撤销操作的方法是往事件存储里增添一个修正事件。如果已持久化的事件格式(而不是数据本身)需要修改,可能会难以将存储中的已有事件与新版本通过迁移进行融合。也许需要遍历所有事件进行修改才能让它们与新格式相兼容,或者对旧事件使用新格式添加生成新事件。考虑对事件结构的每个版本使用一个版本戳,用于同时维护旧事件和新事件格式。 59 | 60 | 多线程应用和多实例应用可能会同时向事件存储中存储事件。事件存储中的事件一致性极为重要,因为事件的顺序会对特定实体造成影响(实体发生变化的顺序会影响其当前状态)。为每一个事件添加时间戳有助于避免这类问题。另外一种常见的实践是为同一请求所产生的每个事件用一个自增的标识符作为标记。如果两个动作尝试为同一个实体在同一时刻添加事件,那么事件存储可以拒绝与已存在的实体标识符相同的那个事件。 61 | 62 | 对于从事件中读取信息,并不存在标准的方法或者类似SQL查询这样现成的机制。唯一能够被提取的数据就是使用事件标识符作为查询条件的一个事件流。事件ID通常与各个独立实体相对应。某个实体的当前状态只能通过重播从实体初始状态开始到现在的所有相关事件来决定。 63 | 64 | 各个事件流的长度会对系统的管理和更新带来影响。如果事件流太长,考虑在特定时间间隔为其创建快照,比如在收集到一定数量的事件之后。当前实体状态可以从快照和对快照时间点之后发生的事件进行重播而获得。关于创建数据快照的更多信息,参见[Martin Fowler的企业应用架构中关于快照的文章](http://martinfowler.com/eaaDev/Snapshot.html)以及[Master-Subordinate Snapshot Replication](https://msdn.microsoft.com/library/ff650012.aspx)。 65 | 66 | 虽然事件溯源可以将数据更新冲突的可能性减小到最低,应用仍然需要能够处理因为最终一致性和缺乏事务机制而导致的不一致性。例如,在事件存储中产生一个库存减小事件的同时下了一个要订购该商品的订单,就需要对这两个操作进行调和,通知用户或者创建一个延期发货订单。 67 | 68 | 事件的发布可能“不止一次”,所以消费者对事件的处理必须是幂等的。如果事件被多次处理,消费者不得对事件重复操作。举个例子,如果消费者有多个实例一起维护某个实体属性的聚合,比如总下单数量,那么当某个下单事件发生时,只能有其中一个实例增加下单总数。然而这并不是事件溯源的关键特性,所以通常由实现方来做决定。 69 | 70 | ## 何时使用该模式 71 | 72 | 在以下场景使用该模式: 73 | 74 | * 当你想从数据中捕获“意图”、“目的”或“原因”时。例如,一个客户实体的变更,可能是由于一系列特定的事件类型所致,比如搬家、账户终止或身故等。 75 | * 当最小化或完全避免数据更新冲突变得非常重要时。 76 | * 当你想记录所发生的事件,并能通过重播事件来存储系统状态、回滚变更或保留历史与审计日志时。例如,当某个任务涉及多个步骤,你可能需要执行某些动作来回滚更新,并且重播某些步骤来让数据回到一致的状态。 77 | * 当对于应用的操作来说使用事件是一种很自然的特性,而且不需要额外的开发或实现工作时。 78 | * 当你需要将数据输入或更新过程与相应的任务相互解耦时。这会有助于增强用户界面性能,或者将事件分发给其它监听事件并需要执行动作的监听器。例如,将工资系统与报销网站集成起来,以便让事件存储中产生因站点数据更新而产生的事件,网站和工资系统都可以消费该事件。 79 | * 当你需要在需求变化时灵活地修改物化模型和实体数据的格式时,或者(在与CQRS模式联合使用)需要采用读模型或试图来暴露数据时。 80 | * 当与CQRS模式联合使用,并且在读模型被更新时最终一致性是可被接受的,或者从事件流中重新融合实体和数据所造成的影响是可接受的。 81 | 82 | 这种模式可能不适用于以下场景: 83 | 84 | * 小的或简单的领域,业务逻辑很少甚至没有的系统,或者非领域系统,用传统的CRUD数据管理机制就能很好地工作。 85 | * 需要强一致性和数据视图实时更新的系统。 86 | * 不需要审计日志、历史和回滚、重播能力的系统。 87 | * 极少出现底层数据更新冲突的系统。例如,系统主要用于增加数据而很少做更新。 88 | 89 | ## 例子 90 | 91 | 某个会议管理系统,需要跟踪记录会议的已预订数量,以便在参会者尝试预订时检查是否还有空座位。该系统可以通过至少两种方法来存储总预订数: 92 | 93 | * 系统可以用负责维护预订信息的数据库中的一个单独实体来存储总预订数。这种方法理论上比较简单,但是如果大量参会者在短时间内涌入预订座位,会产生可扩展性问题。例如,预定期结束前最后一天或者非常临近结束时。 94 | * 系统可以将预订和取消信息作为事件存储在事件存储中。然后通过重播这些事件来计算空余座位数。由于事件的不可变性,这种方法更容易扩展。系统只需要能从事件存储中读取数据,或者向事件存储中追加数据即可。关于预订与取消的事件信息永远不会被修改。 95 | 96 | 下图展示了如何使用事件溯源实现会议管理系统的座位预订子系统。 97 | 98 | 99 | ![](https://docs.microsoft.com/en-us/azure/architecture/patterns/_images/event-sourcing-bounded-context.png) 100 | 101 | 预订两个座位的动作顺序如下: 102 | 103 | 1. 用户界面发出一个命令,要为两个参会者预订座位。一个独立的命令处理器会处理该命令。少部分逻辑会从用户界面中解耦出来,负责处理以命令形式发送的请求。 104 | 105 | 2. 通过查询所有预订与取消事件形成包含所有会议预订信息的一个聚合。这个聚合叫作`SeatAvailability`,由一个领域模型所包含,该模型对外暴露查询与修改聚合中的数据的方法。 106 | 107 | > 可以考虑使用快照做一些优化(这样你就不需要查询和重播所有事件来获得聚合当前状态),并在内存中维护关于该聚合的一份缓存拷贝。 108 | 109 | 3. 命令处理器调用领域模型上暴露出的方法进行预订。 110 | 111 | 4. 聚合`SeatAvailability`记录下一个含有被预订座位数量的事件。下次这个聚合就可以通过所有预订事件来计算还剩下多少座位。 112 | 113 | 5. 系统向事件存储的事件列表中追加这个新事件。 114 | 115 | 如果用户要取消座位,系统遵循类似的过程,只是命令处理器发出的命令产生的是一个座位取消事件并追加到事件存储中。 116 | 117 | 除了能提供在扩展性方面更大的余地之外,使用事件存储还能够提供一个完整的关于会议预订与取消的历史记录或审计记录。事件存储中的事件是精确的记录。并不需要将聚合持久化到其它地方,因为系统可以轻松地重播事件并恢复任何时间点的状态。 118 | 119 | > 你可以从这篇文章[Introducing Event Sourcing](https://msdn.microsoft.com/library/jj591559.aspx)中找到关于该例子的更多信息。 120 | 121 | ## 相关模式与指南 122 | 123 | 在实现该模式时,下述模式和指南可能会与之相关: 124 | 125 | * [命令和查询责任分离(CQRS)模式](patterns/cqrs.md)。为CQRS的实现提供永久信息源的写存储通常是基于事件溯源模式而实现的。该模式描述了如何通过不同的接口将应用中读取数据与更新数据的操作互相分离。 126 | * [物化视图模式](patterns/materialized-view.md)。基于事件溯源的系统中所使用的数据存储通常不适于很好地进行高效查询。一种常见的取代方法是在固定时间间隔或者当数据变化时,预先产生一个关于该数据的视图。该模式展示了是如何做到这点的。 127 | * [事务修正模式](patterns/compensating-transaction.md)。事件溯源存储中已存在的数据是不会被更新的,新事件被源源不断地加入来表示实体状态的最新值。要做一个反向撤销的话,得使用事务修正,因为简单地回退到之前的状态是不可能的。该模式描述了如何撤销之前一个操作所做的工作。 128 | * [数据一致性入门](https://msdn.microsoft.com/library/dn589800.aspx)。在事件溯源中使用单独的读存储或者物化视图时,读到的数据并不是实时一致的,而仅仅是最终一致的。该入门总结了在分布式数据上维护一致性的相关问题。 129 | * [数据分区指南](https://msdn.microsoft.com/library/dn589795.aspx)。在使用事件溯源来提高可扩展性、减少冲突和优化性能时,数据通常会被分区。该指南描述了如何将数据划分为离散的分区,以及可能出现的问题。 130 | * Greg Young的文章[Why use Event Sourcing?](http://codebetter.com/gregyoung/2010/02/20/why-use-event-sourcing/)。 131 | 132 | -------------------------------------------------------------------------------- /patterns/external-configuration-store.md: -------------------------------------------------------------------------------- 1 | # 外部配置存储模式 2 | 3 | 将应用程序部署包中的配置信息放在集中位置。这可以提供更容易管理和控制配置数据以及跨应用程序和应用程序实例共享配置数据的机会。 4 | 5 | ## 背景和问题 6 | 7 | 大多数应用程序运行时环境,包括在文件中保存的配置信息,与应用程序一起部署。在某些情况下,可以通过编辑这些文件,以便在部署后更改应用程序行为。但是,配置的更改需要重新部署应用程序,通常会导致无法接受的停机时间和其它管理开销。 8 | 本地配置文件也将配置限制在单个应用程序中,但有时跨多个应用程序共享配置设置将非常有用。比如数据库连接字符串,UI主题信息或相关应用程序集使用的队列和存储的URL。 9 | 在应用程序的多个运行实例中管理本地配置的更改具有挑战性,特别是托管在云平台的场景中。部署更新时可能导致实例使用不同配置设置。 10 | 此外,应用程序和组件的更新可能需要更改配置schema。许多配置系统不支持不同版本的配置信息。 11 | 12 | ## 解决方案 13 | 14 | 将配置信息存在的外部存储,并且提供快速有效地读取和更新配置设置的接口。外部存储的类型取决于应用程序的托管和运行时环境。在云托管场景中,通常是基于云的存储服务,也可以是托管数据库或其它系统。 15 | 为配置信息选择的后台存储应具有提供一致且易于使用的访问接口。应该以正确类型和结构化的格式暴露信息。实现还可能需要授权用户访问以保护配置数据,并且足够灵活,以允许存储多个版本的配置(例如开发,预生产或生产环境,包括多个发布版本)。 16 | > 许多内置的配置系统在应用程序启动时读取数据,并将数据缓存在内存中,快速访问并最大限度地减少对应用程序性能的影响。根据所使用的备份存储的类型和该存储的延迟,在外部配置存储中实现缓存机制可能会有所帮助。更多相关信息请参阅[缓存指南](https://msdn.microsoft.com/library/dn589802.aspx)。下图概述了具有可使用本地缓存的外部配置存储模式。 17 | ![](https://docs.microsoft.com/en-us/azure/architecture/patterns/_images/external-configuration-store-overview.png) 18 | 19 | ## 问题和注意事项 20 | 21 | 在决定如何实现此模式时,请考虑以下几点: 22 | 23 | 选择一个提供性能可接受,可用性高,鲁棒的备份存储,并可作为应用程序维护和管理过程的一部分进行备份。在云托管应用中,使用云存储机制通常是满足这些要求的好选择。 24 | 25 | 设计后备存储的schema,以允许其容纳信息类型的灵活性。确保它提供所有配置要求,例如类型化数据,设置集合,多个版本的设置以及应用程序所需的任何其它功能。该schema应该易于扩展,以便在需求更改时支持其它设置。 26 | 27 | 考虑备份存储的物理功能,这与存储配置信息的方式以及对性能的影响有关。例如,存储包含配置信息的XML文档将需要配置接口或应用程序来解析文档,以便读取单独的设置。这将使更新设置更加复杂,但是缓存设置可以帮助抵消较慢的读取性能。 28 | 29 | 考虑配置接口如何允许控制配置设置的范围和继承。例如,可能需要在组织,应用程序和机器级别范围内配置设置。可能需要支持授权对不同作用域的访问的控制,并阻止或允许各个应用程序覆盖设置。 30 | 31 | 考虑如何保护配置数据,以便仅允许合法的用户和应用程序访问。这可能是配置存储接口的一个功能,但是还有必要确保备份存储中的数据在没有适当权限的情况下无法直接访问。确保读取和写入配置数据所需的权限之间的严格分离。还要考虑是否需要加密部分或全部配置设置,以及如何在配置存储接口中实现。 32 | 33 | 确保配置接口按照要求的格式(如键入值,集合,键/值对或属性包)公开配置数据。 34 | 35 | 考虑当设置出错时,或者备份存储中不存在时,配置存储接口的行为。可以返回默认设置并记录错误。同时还要考虑诸如区分配置设置键或名称大小写,二进制数据的存储和处理以及处理null或空值的方式等方面。 36 | 37 | 考虑如何保护配置数据,以便仅访问适当的用户和应用程序。这可能是配置存储界面的一个功能,但是还有必要确保后台存储中的数据无法在没有适当权限的情况下直接访问。确保读取和写入配置数据所需的权限之间的严格分隔。还要考虑是否需要加密部分或全部配置设置,以及如何在配置存储界面中实现。 38 | 39 | 在运行时更改应用程序行为的集中存储配置至关重要,应使用与部署应用程序代码相同的机制进行部署,更新和管理。例如,影响多个应用程序的更改必须使用完整的测试和分段部署方法,以确保更改适用于使用此配置的所有应用程序。如果管理员编辑更新一个应用程序的设置,则可能会对使用相同设置的其它应用程序造成不利影响。 40 | 41 | 如果应用程序缓存配置信息,则应用程序需要在配置更改时收到警报。可以在缓存的配置数据上实现一个过期策略,以便定期自动刷新此信息,并获取(并采取行动)的任何更改。 42 | 43 | ## 何时使用该模式 44 | 45 | 该模式适用于以下场景: 46 | 47 | * 在多个应用程序和应用程序实例之间共享的配置设置,或者必须在多个应用程序和应用程序实例之间执行标准配置。 48 | * 不支持所有必需配置设置的标准配置系统,例如存储图像或复杂数据类型。 49 | * 作为应用程序某些设置的补充存储,可能允许应用程序覆盖部分或全部集中存储的设置。 50 | * 作为一种简化多个应用程序管理的方法,并且可选择的通过记录对配置存储的一些或所有类型的访问来监视配置设置的使用。 51 | 52 | ## 案例 53 | 54 | 在Microsoft Azure托管应用程序中,一般使用`Azure Storage`外部存储配置信息。具备弹性同时提供高性能,有三个副本,出现故障时会自动切换以提供高可用性。Azure Table存储提供了键/值存储,可以为值的使用灵活的schema。Azure Blob存储提供了一个分层的基于容器的存储,可以在单独命名的blob中保存任何类型的数据。 55 | 56 | 下面的例子显示如何通过Blob存储实现配置存储的存储和公开配置信息。`BlobSettingsStore`类用于保存配置信息的Blob存储,并实现以下代码中展示的`ISettingsStore`接口。 57 | > ExternalConfigurationStore解决方案的ExternalConfigurationStore.Cloud项目的代码可以在[github](https://github.com/mspnp/cloud-design-patterns/tree/master/external-configuration-store)上找到。 58 | 59 | > 该代码在ExternalConfigurationStore解决方案中的ExternalConfigurationStore.Cloud项目中提供,可从GitHub获得。 60 | 61 | ```java 62 | public interface ISettingsStore 63 | { 64 | Task GetVersionAsync(); 65 | 66 | Task> FindAllAsync(); 67 | } 68 | ``` 69 | 70 | 该接口定义了用于检索和更新配置存储中保存的配置设置的方法,并包括可用于检测是否最近修改了任何配置设置的版本号。 `BlobSettingsStore`类使用blob的`ETag`属性来实现版本控制。每次写入blob时,`ETag`属性都会自动更新。 71 | 72 | > 根据设计,这个简单的解决方案将所有配置设置暴露为字符串而不是类型值。 73 | 74 | `ExternalConfigurationManager`类围绕`BlobSettingsStore`对象提供了一个包装器。应用程序可以使用它来存储和检索配置信息。它使用[`Microsoft Reactive Extensions库`](https://msdn.microsoft.com/library/hh242985.aspx)实现`IObservable`接口暴露对配置所做的任何更改。如果通过调用`SetAppSetting`方法修改设置,则会出发`Changed`事件,并通知事件的所有订阅者。 75 | 76 | 请注意,所有设置也会缓存在`ExternalConfigurationManager`类中的`Dictionary`对象中,以便快速访问。用于检索配置设置的`GetSetting`方法从缓存读取数据。如果缓存中没有找到设置,它将从`BlobSettingsStore`对象中检索。 77 | 78 | `GetSettings`方法调用`CheckForConfigurationChanges`来检测blob存储中的配置信息是否已更改。它通过检查版本号并将其与`ExternalConfigurationManager`对象持有的当前版本号进行比较。如果发生一个或多个更改,则会出发`Changed`事件,并刷新`Dictionary`对象中缓存的配置设置。这是[Cache-Aside模式](cached-aside.md)的一个应用。 79 | 80 | 以下代码示例显示`Changed`事件,`GetSettings`方法和`CheckForConfigurationChanges`方法如何实现的: 81 | 82 | ```java 83 | public class ExternalConfigurationManager : IDisposable 84 | { 85 | // An abstraction of the configuration store. 86 | private readonly ISettingsStore settings; 87 | private readonly ISubject> changed; 88 | ... 89 | private readonly ReaderWriterLockSlim settingsCacheLock = new ReaderWriterLockSlim(); 90 | private readonly SemaphoreSlim syncCacheSemaphore = new SemaphoreSlim(1); 91 | ... 92 | private Dictionary settingsCache; 93 | private string currentVersion; 94 | ... 95 | public ExternalConfigurationManager(ISettingsStore settings, ...) 96 | { 97 | this.settings = settings; 98 | ... 99 | } 100 | ... 101 | public IObservable> Changed => this.changed.AsObservable(); 102 | ... 103 | 104 | public string GetAppSetting(string key) 105 | { 106 | ... 107 | // Try to get the value from the settings cache. 108 | // If there's a cache miss, get the setting from the settings store and refresh the settings cache. 109 | 110 | string value; 111 | try 112 | { 113 | this.settingsCacheLock.EnterReadLock(); 114 | 115 | this.settingsCache.TryGetValue(key, out value); 116 | } 117 | finally 118 | { 119 | this.settingsCacheLock.ExitReadLock(); 120 | } 121 | 122 | return value; 123 | } 124 | ... 125 | private void CheckForConfigurationChanges() 126 | { 127 | try 128 | { 129 | // It is assumed that updates are infrequent. 130 | // To avoid race conditions in refreshing the cache, synchronize access to the in-memory cache. 131 | await this.syncCacheSemaphore.WaitAsync(); 132 | 133 | var latestVersion = await this.settings.GetVersionAsync(); 134 | 135 | // If the versions are the same, nothing has changed in the configuration. 136 | if (this.currentVersion == latestVersion) return; 137 | 138 | // Get the latest settings from the settings store and publish changes. 139 | var latestSettings = await this.settings.FindAllAsync(); 140 | 141 | // Refresh the settings cache. 142 | try 143 | { 144 | this.settingsCacheLock.EnterWriteLock(); 145 | 146 | if (this.settingsCache != null) 147 | { 148 | //Notify settings changed 149 | latestSettings.Except(this.settingsCache).ToList().ForEach(kv => this.changed.OnNext(kv)); 150 | } 151 | this.settingsCache = latestSettings; 152 | } 153 | finally 154 | { 155 | this.settingsCacheLock.ExitWriteLock(); 156 | } 157 | 158 | // Update the current version. 159 | this.currentVersion = latestVersion; 160 | } 161 | catch (Exception ex) 162 | { 163 | this.changed.OnError(ex); 164 | } 165 | finally 166 | { 167 | this.syncCacheSemaphore.Release(); 168 | } 169 | } 170 | } 171 | ``` 172 | 173 | `ExternalConfigurationManager`类还提供了一个名为`Environment`的属性。它支持在不同环境(如分段和生产)中运行的应用程序的不同配置。 174 | 175 | `ExternalConfigurationManager`对象也可以定期查询`BlobSettingsStore`对象的任何更改。在下面的代码中,`StartMonitor`方法会以一定时间间隔调用`CheckForConfigurationChanges`,检测任何更改并引发更改事件,如前所述。 176 | 177 | ```java 178 | public class ExternalConfigurationManager : IDisposable 179 | { 180 | ... 181 | private readonly ISubject> changed; 182 | private Dictionary settingsCache; 183 | private readonly CancellationTokenSource cts = new CancellationTokenSource(); 184 | private Task monitoringTask; 185 | private readonly TimeSpan interval; 186 | 187 | private readonly SemaphoreSlim timerSemaphore = new SemaphoreSlim(1); 188 | ... 189 | public ExternalConfigurationManager(string environment) : this(new BlobSettingsStore(environment), TimeSpan.FromSeconds(15), environment) 190 | { 191 | } 192 | 193 | public ExternalConfigurationManager(ISettingsStore settings, TimeSpan interval, string environment) 194 | { 195 | this.settings = settings; 196 | this.interval = interval; 197 | this.CheckForConfigurationChangesAsync().Wait(); 198 | this.changed = new Subject>(); 199 | this.Environment = environment; 200 | } 201 | ... 202 | /// 203 | /// Check to see if the current instance is monitoring for changes 204 | /// 205 | public bool IsMonitoring => this.monitoringTask != null && !this.monitoringTask.IsCompleted; 206 | 207 | /// 208 | /// Start the background monitoring for configuration changes in the central store 209 | /// 210 | public void StartMonitor() 211 | { 212 | if (this.IsMonitoring) 213 | return; 214 | 215 | try 216 | { 217 | this.timerSemaphore.Wait(); 218 | 219 | // Check again to make sure we are not already running. 220 | if (this.IsMonitoring) 221 | return; 222 | 223 | // Start running our task loop. 224 | this.monitoringTask = ConfigChangeMonitor(); 225 | } 226 | finally 227 | { 228 | this.timerSemaphore.Release(); 229 | } 230 | } 231 | 232 | /// 233 | /// Loop that monitors for configuration changes 234 | /// 235 | /// 236 | public async Task ConfigChangeMonitor() 237 | { 238 | while (!cts.Token.IsCancellationRequested) 239 | { 240 | await this.CheckForConfigurationChangesAsync(); 241 | await Task.Delay(this.interval, cts.Token); 242 | } 243 | } 244 | 245 | /// 246 | /// Stop monitoring for configuration changes 247 | /// 248 | public void StopMonitor() 249 | { 250 | try 251 | { 252 | this.timerSemaphore.Wait(); 253 | 254 | // Signal the task to stop. 255 | this.cts.Cancel(); 256 | 257 | // Wait for the loop to stop. 258 | this.monitoringTask.Wait(); 259 | 260 | this.monitoringTask = null; 261 | } 262 | finally 263 | { 264 | this.timerSemaphore.Release(); 265 | } 266 | } 267 | 268 | public void Dispose() 269 | { 270 | this.cts.Cancel(); 271 | } 272 | ... 273 | } 274 | ``` 275 | 276 | `ExternalConfigurationManager`类由以下所示的`ExternalConfiguration`类实例化为单例实例。 277 | 278 | ```java 279 | public static class ExternalConfiguration 280 | { 281 | private static readonly Lazy configuredInstance = new Lazy( 282 | () => 283 | { 284 | var environment = CloudConfigurationManager.GetSetting("environment"); 285 | return new ExternalConfigurationManager(environment); 286 | }); 287 | 288 | public static ExternalConfigurationManager Instance => configuredInstance.Value; 289 | } 290 | ``` 291 | 292 | 以下代码取自ExternalConfigurationStore.Cloud项目中的WorkerRole类。它显示了应用程序如何使用ExternalConfiguration类来读取设置。 293 | 294 | ```java 295 | public override void Run() 296 | { 297 | // Start monitoring configuration changes. 298 | ExternalConfiguration.Instance.StartMonitor(); 299 | 300 | // Get a setting. 301 | var setting = ExternalConfiguration.Instance.GetAppSetting("setting1"); 302 | Trace.TraceInformation("Worker Role: Get setting1, value: " + setting); 303 | 304 | this.completeEvent.WaitOne(); 305 | } 306 | ``` 307 | 308 | 下面的代码也来自WorkerRole类,显示应用程序如何订阅配置事件。 309 | 310 | ```java 311 | public override bool OnStart() 312 | { 313 | ... 314 | // Subscribe to the event. 315 | ExternalConfiguration.Instance.Changed.Subscribe( 316 | m => Trace.TraceInformation("Configuration has changed. Key:{0} Value:{1}", 317 | m.Key, m.Value), 318 | ex => Trace.TraceError("Error detected: " + ex.Message)); 319 | ... 320 | } 321 | ``` 322 | 323 | ## 相关模式和指南 324 | 325 | * [GitHub](https://github.com/mspnp/cloud-design-patterns/tree/master/external-configuration-store)上有本模式使用的简单例子。 -------------------------------------------------------------------------------- /patterns/federated-identity.md: -------------------------------------------------------------------------------- 1 | # 联合身份模式 2 | 3 | 将身份验证委托给外部身份提供者。这可以简化开发,最大限度地减少用户管理的需求,并改善应用程序的用户体验。 4 | 5 | ## 问题和背景 6 | 7 | 用户通常需要使用与其业务关系的不同组织提供和托管的多个应用程序。这些用户可能需要为每个用户使用特定(和不同的)凭据。 这会导致: 8 | 9 | * 导致用户体验脱节。当用户有很多不同的凭据时,用户经常忘记登录凭据,。 10 | * 暴露安全漏洞。当用户离开公司时,该帐户必须立即禁止使用。大型组织中很容易忽略这一点。 11 | * 复杂的用户管理。管理员必须管理所有用户的凭据,并执行其它任务,例如提供密码更换提醒。 12 | * 用户通常喜欢为所有应用程序使用相同的凭据。 13 | 14 | ## 解决方案 15 | 16 | 实现可以使用联合身份的身份验证机制。将用户认证与应用程序代码分开,并将认证委托给受信任的身份提供者。这可以简化开发,并允许用户使用更广泛的身份提供者(IdP)进行身份验证,同时最大限度地减少管理开销。它还允许明确地将验证与授权解耦。 17 | 18 | 受信任的身份提供者包括企业目录,内部联合服务,商业伙伴提供的其它安全令牌服务(STS)或可以对具有例如Microsoft,Google,Yahoo!或Facebook的用户进行身份验证的社会身份提供者帐户。 19 | 下图说明了客户端应用程序访问需要身份验证的服务时的联合身份模式。与STS协同工作的IdP执行认证。IdP发出安全令牌,提供有关经过身份验证的用户的信息。这些信息,通常作为声明引用,包括用户身份,还可以包括其它信息,例如角色成员资格和更细粒度的访问权限。 20 | 21 | ![](https://docs.microsoft.com/en-us/azure/architecture/patterns/_images/federated-identity-overview.png) 22 | 23 | 这种模式通常被称为基于声明的访问控制。应用程序和服务根据令牌中包含的声明授权访问特性和功能。需要认证的服务必须信任IdP。客户端应用程序联系执行身份验证的IdP。如果身份验证成功,IdP将返回一个包含将用户标识,给STS的声明令牌(请注意,IdP和STS可以是同一个服务)。STS可以在将其返回给客户端之前,根据预定义的规则,在令牌中转换和增加声明。然后,客户端应用程序可以将该令牌作为其身份的证明,传递给服务。 24 | > 在信任链中可能会有额外的STS。例如,在后面描述的情况下,本地STS信任另一个负责访问身份提供商以验证用户的STS。这种方法在具有本地STS和目录的企业场景中很常见。 25 | 联合身份验证为跨多个领域的身份信任提供了基于标准的解决方案,支持单点登录。因为它支持单点登录,而不需要与身份提供商的直接网络连接,所以在所有类型的应用程序,特别是云托管应用程序中变得越来越普遍。用户不必为每个应用程序输入凭据。这增加了安全性,因为它避免在访问许多不同应用程序时创建凭据,还隐藏了除了原始身份提供者之外的用户的凭据。应用程序仅查看令牌内包含的身份验证身份信息。 26 | 联合身份的主要优点还包括身份提供者负责的身份和凭据管理。应用程序或服务不需要提供身份管理功能。此外,在企业场景中,企业目录不需要知道用户是否信任身份提供者。这将删除在目录中管理用户身份的所有管理开销。 27 | ## 问题和注意事项 28 | 29 | 设计实施联合身份验证的应用程序时,请考虑以下事项: 30 | 31 | * 认证可能存在单点故障可能。如果将应用程序部署到多个数据中心,请考虑将身份管理机制部署到同一数据中心,以维护应用程序的可靠性和可用性。 32 | * 身份验证工具可以根据认证令牌中包含的角色声明配置访问控制。这通常被称为基于角色的访问控制(RBAC),并且它可以允许对访问特征和资源的更细粒度的控制。 33 | * 和公司目录不同,使用社会身份提供者的基于声明的身份验证,通常不提供除了电子邮件和姓名之外的用户信息。一些社会身份提供者(如Microsoft帐户)只提供一个唯一的标识符。应用程序通常需要维护注册用户的一些信息,并且能够将该信息与令牌的声明中包含的标识符相匹配。通常,这是通过在用户首次访问应用程序时注册完成的,然后在每个身份验证之后将信息作为附加权利要求注入到令牌中。 34 | * 如果为STS配置了多个身份提供者,则必须检测用户应该将哪个身份提供者重定向到身份验证。这个过程称为家庭领域发现。STS可以根据用户提供的电子邮件地址或用户名,用户正在访问的应用程序的子域,用户的IP地址范围或存储在用户浏览器的cookie的内容自动执行此操作。例如,如果用户在Microsoft域中输入了电子邮件地址,如user@live.com,则STS将将用户重定向到Microsoft帐户登录页面。在稍后的访问中,STS可以使用cookie来表示最后登录的是Microsoft帐户。如果自动发现无法确定家庭领域???,则STS将显示列出受信任身份提供商的家庭领域发现页面,用户必须选择想要使用的域名。 35 | 36 | ## 何时使用该模式 37 | 38 | 本模式适于以下场景: 39 | 40 | * **单一登录企业**。在这种情况下,需要为在公司安全边界外的云中托管的应用程序进行身份验证,无需每次访问应用程序时登录。用户体验与在登录公司网络时使用内部部署应用程序进行身份验证相同,从那时起可以访问所有相关应用程序,无需再次登录。 41 | * **与多个合作伙伴的联合身份**。在这种情况下,需要对在公司目录中没有帐户的公司员工和业务伙伴进行身份验证。这在企业对企业应用程序,与第三方服务集成的应用程序以及合并了不同IT系统的公司或共享资源的公司中是常见的。 42 | * **SaaS应用中的联合身份**。在这种情况下,独立软件供应商为多个客户或租户提供即用型服务。每个租户使用合适的身份提供商进行身份验证。例如,业务用户将使用他们的公司凭证,而租户的消费者和客户将使用他们的社会身份凭证。 43 | 44 | 本模式可能不适于以下场景: 45 | 46 | * 应用程序的所有用户都可以通过一个身份提供者进行身份验证,并且不需要使用任何其他身份提供者进行身份验证。这是在使用企业目录(在应用程序中可访问的)进行身份验证,通过使用VPN或(在云托管场景中)通过本地目录和应用程序之间的虚拟网络连接的业务应用程序中的典型方式。 47 | * 该应用程序最初使用不同的身份验证机制构建,可能有自定义用户存储,或者没有能力处理基于声明的技术使用的协商标准。 将基于声明的身份验证和访问控制应用到现有应用程序可能很复杂,不划算。 48 | 49 | 50 | ## 案例 51 | 52 | 组织在Microsoft Azure中托管多租户软件即服务(SaaS)应用程序。该应用程序包括租户可以使用来管理自己用户的应用程序的网站。当用户通过该组织自己的Active Directory进行身份验证时,应用程序允许租户通过使用由Active Directory联合身份验证服务(ADFS)生成的联合身份来访问该网站。 53 | ![](https://docs.microsoft.com/en-us/azure/architecture/patterns/_images/federated-identity-multitenat.png) 54 | 该图显示租户如何使用自己的身份提供者进行身份验证(步骤1),图中为ADFS。在成功验证租户后,ADFS发出令牌。浏览器将此令牌转发到SaaS应用程序的联合提供者,该提供者信任由租户的ADFS发出的令牌,以便获取对SaaS联合提供者有效的令牌(步骤2)。如果需要,SaaS联合提供者将令牌中的声明转换为应用程序在将新令牌返回到浏览器之前识别的声明(步骤3)。应用程序信任由SaaS联合提供者发出的令牌,并使用令牌中的声明来应用授权规则(步骤4)。 55 | 租户不需要记住访问应用程序的单独凭据,租户公司的管理员可以在自己的ADFS中配置访问应用程序的用户列表。 56 | 57 | ## 相关指南 58 | 59 | * [Microsoft Azure Active Directory](https://azure.microsoft.com/services/active-directory/) 60 | * [Active Directory Domain Services](https://msdn.microsoft.com/library/bb897402.aspx) 61 | * [Active Directory Federation Services](https://msdn.microsoft.com/library/bb897402.aspx) 62 | * [Microsoft Azure的多租户应用的身份管理](https://azure.microsoft.com/documentation/articles/guidance-multitenant-identity/) 63 | * [Azure的多租户应用](https://azure.microsoft.com/documentation/articles/dotnet-develop-multitenant-applications/) 64 | -------------------------------------------------------------------------------- /patterns/gatekeeper.md: -------------------------------------------------------------------------------- 1 | # 看门人模式 2 | 3 | 通过使用专用主机实例来保护应用程序和服务,该主机实例充当客户端和应用程序或服务之间的代理,对请求进行验证和清理,并在它们之间传递请求和数据。这可以提供额外的安全层,并限制系统的攻击面。 4 | 5 | ### 背景和问题 6 | 7 | 应用程序通过接受和处理请求将其功能暴露给客户端。在云托管方案中,应用程序会公开客户端连接的端点,通常包括处理客户端请求的代码。该代码执行认证和验证,部分或全部请求处理,并且可能代表客户端访问存储和其它服务。 8 | 如果恶意用户能够破坏系统并获得对应用程序托管环境的访问权限,那么它所使用的安全机制(如凭据和存储密钥)以及访问的服务和数据将暴露。结果就是恶意用户可以无限制地访问敏感信息和其它服务。 9 | 10 | ## 解决方案 11 | 12 | 为了最小化客户端访问敏感信息和服务的风险,将公开端点的主机或任务与处理请求和访问存储的代码分离。可以通过使用与客户端交互的门面或专用任务来实现此目的,然后将请求(也许通过解耦接口)切换到将处理请求的主机或任务。下图概述了这种模式。 13 | ![](https://docs.microsoft.com/en-us/azure/architecture/patterns/_images/gatekeeper-diagram.png) 14 | 看门人模式可用于简化保护存储,或者可以用作更全面的门面来保护应用程序的所有功能。重要的因素有: 15 | * **受控验证**。看门人验证所有请求,并拒绝那些不符合验证要求的请求。 16 | * **有限的风险和暴露**。看门人无法访问可信主机使用的凭据或密钥来获得存储和服务的访问。如果看门人暴露,攻击者将无法访问这些凭据或密钥。 17 | * **适当的安全**。看门人以有限的特权模式运行,而其余应用程序以访问存储和服务所需的完全信任模式运行。如果看门人受到威胁,则无法直接访问应用程序服务或数据。 18 | 19 | 这种模式像典型的网络拓扑中的防火墙一样。允许看门人检查请求并做出关于是否将请求传递给执行所需任务的受信任主机(有时称为关键主人)的决定。该决定通常要求看门人在将请求内容传递给受信任的主机之前对请求内容进行验证和清理。 20 | 21 | ### 问题和注意事项 22 | 23 | 在决定如何实现此模式时,请考虑以下几点: 24 | * 确保看门人的信任主机通过请求,仅暴露内部或受保护的端点,并仅连接到看门人。受信任的主机不应暴露任何外部端点或接口。 25 | * 看门人必须以有限的特权模式运行。这通常意味着在单独的托管服务或虚拟机中运行看门人和可信主机。 26 | * 看门人不应该执行与应用程序或服务相关的任何处理,或访问任何数据。它的功能纯粹是为了验证和清理请求。受信任的主机可能需要对请求执行其它验证,但核心验证应由看门人执行。 27 | * 在看门人和受信任的主机之间或者可能的任务使用安全通信通道(HTTPS,SSL或TLS)。但是,某些托管环境不支持内部端点上的HTTPS。 28 | * 将额外的层添加到应用程序以实现看门人模式,这可能会对性能有一些影响,因为需要额外的处理和网络通信。 29 | * 看门人实例可能存在单点故障。为了尽量减少故障的影响,请考虑部署其它实例并使用自动伸缩机制来确保维护可用性的能力。 30 | 31 | ## 何时使用该模式 32 | 33 | 在以下场景适用于该模式: 34 | * 处理敏感信息的应用程序,暴露必须高度保护免受恶意攻击的服务,或执行不应中断的关键任务操作。 35 | * 有必要与主要任务分开执行请求验证的分布式应用程序,或集中此验证以简化维护和管理。 36 | 37 | ## 案例 38 | 在云托管场景中,可以通过分离应用程序中受信任的角色和服务来实现看门人角色或虚拟机的解耦。通过使用内部端点,队列或存储作为中间通信机制来执行此操作。下图展示了使用内部端点的方式。 39 | ![](https://docs.microsoft.com/en-us/azure/architecture/patterns/_images/gatekeeper-endpoint.png) 40 | 41 | ## 相关模式 42 | 实现看门人模式时,[Valet Key模式](valet-key.md)也可能是相关的。在Gatekeeper和受信任的角色之间进行通信时,通过使用限制访问资源的权限的密钥或令牌来增强安全性是个好习惯。描述如何使用令牌或密钥提供客户端对特定资源或服务的受限直接访问。 -------------------------------------------------------------------------------- /patterns/gateway-aggregation.md: -------------------------------------------------------------------------------- 1 | # 网关聚合模式 2 | 3 | 使用网关将多个独立请求聚合为一个单独的请求。当客户端必须向不同的后端系统发起多次调用才能完成某个操作时,该模式十分有用。 4 | 5 | ## 背景与问题 6 | 7 | 要执行某个任务时,客户端必须向不同的后端服务发起多次调用。依赖于多个服务的应用在执行任务时,必须为每个请求都扩展资源。当新的特性或服务添加到该应用时,需要添加额外的请求,而且增加了资源需求与网络调用。这些在客户端与后端之间的通信量会对应用的性能和可扩展性造成不利影响。微服务架构下这种情况更为普遍,因为应用是由若干个更小的服务构成的,天然地需要更多数量的跨服务调用。 8 | 9 | 在下图中,客户端向服务发送请求(1,2,3)。每个服务处理这些请求并向应用返回响应(4,5,6)。在高延迟的蜂窝网络中,以这种方式使用独立的请求无疑是低效的,而且可能出现连接断开或未完成的请求。各个请求还有可能并行执行,应用就必须为每个请求发送、等待和处理数据,这些都是在各自独立的连接中的,增加了失败的可能性。 10 | 11 | ![](https://docs.microsoft.com/en-us/azure/architecture/patterns/_images/gateway-aggregation-problem.png) 12 | 13 | ## 解决方案 14 | 15 | 使用网关来减少客户端与服务之间的通信量。网关接收客户请求,将请求分发到不同的后端系统,然后将结果聚合后返回给请求的客户端。 16 | 17 | 这种模式可以减少应用向后端服务发送请求的数量,并且改善应用在高延迟网络中的性能。 18 | 19 | 在下图中,应用向网关发送一个请求(1)。该请求包括了一系列额外请求组成的包。网关将该请求包分解为不同的请求,并发送给各自的相关服务(2)。每个服务向网关返回一个相应(3)。网关将来自各个服务的响应组合为一个响应后返回给应用(4)。应用只需向网关发送一个单独的请求,并从网关得到一个单独的响应。 20 | 21 | ![](https://docs.microsoft.com/en-us/azure/architecture/patterns/_images/gateway-aggregation.png) 22 | 23 | ## 问题与注意事项 24 | 25 | * 网关不应引入后端服务之间的耦合。 26 | * 网关应该位于后端服务附近,以尽可能地减少之间的延迟。 27 | * 网关服务可能引入单点故障。确保网关设计的合理性,以满足你的应用的可用性要求。 28 | * 网关有可能引入瓶颈。确保网关有足够的性能来处理负载,并能够被扩展以满足预期的增长。 29 | * 对网关进行负载测试,以确保你不会为服务引入级联性故障。 30 | * 使用隔板、断路器、重试、超时等技术实现弹性设计。 31 | * 如果一个或多个服务调用占用时间过长,对其超时并只返回一部分数据也是可以接受的。需要考虑你的应用如何处理这种场景。 32 | * 使用异步I/O以确保后端的延时不会引起应用的性能问题。 33 | * 使用相互关联的ID来实现分布式跟踪,以跟踪到各个单独请求。 34 | * 监控请求度量指标和响应大小。 35 | * 考虑将缓存数据作为一种处理故障的故障转移策略。 36 | * 考虑在网关后面放一个聚合服务,而不是将聚合做入到网关里。因为请求聚合与网关中的其他服务相比可能会有不同的资源需求,因此有可能影响到网关的路由和卸载功能。 37 | 38 | ## 何时使用该模式 39 | 40 | 在以下场景使用该模式: 41 | 42 | * 客户端需要与多个后端服务通信才能完成某个操作时。 43 | * 客户端可能使用具有显著延迟的网络,比如蜂窝网络。 44 | 45 | 这种模式可能不适用于: 46 | 47 | * 在完成多个操作时,希望减少某个客户端与某个服务之间的调用数量时。这种场景下,可能更适于为该服务添加一个批量操作。 48 | * 客户端或者应用位于后端服务附近,且网络延迟不是非常显著时。 49 | 50 | 51 | ## 例子 52 | 53 | 以下例子展示了如何在NGINX服务上使用Lua创建一个简单的网关聚合。 54 | 55 | ```lua 56 | worker_processes 4; 57 | 58 | events { 59 | worker_connections 1024; 60 | } 61 | 62 | http { 63 | server { 64 | listen 80; 65 | 66 | location = /batch { 67 | content_by_lua ' 68 | ngx.req.read_body() 69 | 70 | -- read json body content 71 | local cjson = require "cjson" 72 | local batch = cjson.decode(ngx.req.get_body_data())["batch"] 73 | 74 | -- create capture_multi table 75 | local requests = {} 76 | for i, item in ipairs(batch) do 77 | table.insert(requests, {item.relative_url, { method = ngx.HTTP_GET}}) 78 | end 79 | 80 | -- execute batch requests in parallel 81 | local results = {} 82 | local resps = { ngx.location.capture_multi(requests) } 83 | for i, res in ipairs(resps) do 84 | table.insert(results, {status = res.status, body = cjson.decode(res.body), header = res.header}) 85 | end 86 | 87 | ngx.say(cjson.encode({results = results})) 88 | '; 89 | } 90 | 91 | location = /service1 { 92 | default_type application/json; 93 | echo '{"attr1":"val1"}'; 94 | } 95 | 96 | location = /service2 { 97 | default_type application/json; 98 | echo '{"attr2":"val2"}'; 99 | } 100 | } 101 | } 102 | ``` 103 | 104 | ## 相关指南 105 | 106 | * [前端专属的后端模式](patterns/backends-for-frontends.md) 107 | * [网关卸载模式](patterns/gateway-offloading.md) 108 | * [网关路由模式](patterns/gateway-routing.md) -------------------------------------------------------------------------------- /patterns/gateway-offloading.md: -------------------------------------------------------------------------------- 1 | # 网关卸载模式 2 | 3 | 将共享的或专门的服务功能卸载到一个网关代理中去。通过将共享的服务功能从应用其它部分转移到网关里,这种模式可以简化应用的开发,例如SSL证书的使用。 4 | 5 | ## 背景与问题 6 | 7 | 某些特性往往是跨多个服务公用的,这些特性通常需要配置、管理和维护。散布于每个应用部署中的共享的或专门的服务会增加管理开销,并提高部署出错的可能性。任何对于共享特性的更新都必须对所有共享该特性的服务进行部署。 8 | 9 | 正确地处理安全问题(令牌校验、加密、SSL证书管理)和其它复杂任务需要团队成员具有高度的专业技能。例如,某个应用所需的证书必须在所有应用实例上进行配置和部署。每当有新的部署,就必须管理证书以确保其不会过期。在每次应用部署中,任何即将过期的公共证书必须得到更新、测试和验证。 10 | 11 | 在大规模部署下,其它公共服务,比如身份认证、鉴权、日志、监控或限流可能会变得难于实现和管理。最好将这类功能统一起来,以减少开销和出错机会。 12 | 13 | ## 解决方案 14 | 15 | 将某些特性卸载到一个API网关中去,特别是那些横切关注点,比如证书管理、身份认证、SSL终端、监控、协议转译或限流。 16 | 17 | 下图中显示了一个用于终止入站SSL连接的API网关。它代表原始请求者向网关的任意HTTP上游服务器请求数据。 18 | 19 | ![](https://docs.microsoft.com/en-us/azure/architecture/patterns/_images/gateway-offload.png) 20 | 21 | 该模式的好处在于: 22 | 23 | * 简化了服务的开发,去除了所需资源的分发和维护需求,比如对于安全站点所需的web服务器证书和配置。更简化的配置,使得管理和扩展更为容易,并使得服务升级更为容易。 24 | 25 | * 允许专门的团队来实现那些需要专业技能的特性,比如安全。这也让你的核心团队能够专注于应用功能,而将这些专业的横切关注点交给相关专家。 26 | 27 | * 提供请求与响应在日志和监控上的某种一致性。即使某个服务未能被正确安装,仍能够通过配置的网关保证提供最小级别的监控和日志。 28 | 29 | ## 问题与注意事项 30 | 31 | * 确保API网关是高可用的,而且能够从故障中恢复。你的API网关应当运行多个实例以避免单点故障。 32 | * 确保网关在设计时已考虑到应用和终端的容量与扩展需求。确保网关不会成为应用的瓶颈,能够充分扩展。 33 | * 只卸载那些为整个应用所公用的特性,比如安全或数据传输。 34 | * 永远不要把业务逻辑卸载给API网关。 35 | * 如果需要跟踪事务,为记录日志,可以考虑生成相互关联的ID。 36 | 37 | ## 何时使用该模式 38 | 39 | 在以下场景使用该模式: 40 | 41 | * 应用的部署具有共享关注点,比如SSL证书或者加密。 42 | * 在整个应用的不同部分进行部署时,含有共同的特性,却具有不同的资源需求,例如内存资源、存储容量或网络连接。 43 | * 希望将某些问题的职责交给更为专业的团队,比如网络安全、限流或其它网络边界关注点。 44 | 45 | 如果会引入服务间的耦合,则这种模式可能就不太合适。 46 | 47 | ## 例子 48 | 49 | 如下配置将Nginx作为SSL卸载工具,终止一个入站SSL连接,并将连接分发给三个上游HTTP服务器之一。 50 | 51 | ``` 52 | upstream iis { 53 | server 10.3.0.10 max_fails=3 fail_timeout=15s; 54 | server 10.3.0.20 max_fails=3 fail_timeout=15s; 55 | server 10.3.0.30 max_fails=3 fail_timeout=15s; 56 | } 57 | 58 | server { 59 | listen 443; 60 | ssl on; 61 | ssl_certificate /etc/nginx/ssl/domain.cer; 62 | ssl_certificate_key /etc/nginx/ssl/domain.key; 63 | 64 | location / { 65 | set $targ iis; 66 | proxy_pass http://$targ; 67 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 68 | proxy_set_header X-Forwarded-Proto https; 69 | proxy_set_header X-Real-IP $remote_addr; 70 | proxy_set_header Host $host; 71 | } 72 | } 73 | ``` 74 | 75 | ## 相关指南 76 | 77 | * [前端专属的后端模式](patterns/backends-for-frontends.md) 78 | * [网关聚合模式](patterns/gateway-aggregation.md) 79 | * [网关路由模式](patterns/gateway-routing.md) 80 | -------------------------------------------------------------------------------- /patterns/gateway-routing.md: -------------------------------------------------------------------------------- 1 | # 网关路由模式 2 | 3 | 使用一个端点将请求按照路由分发到多个服务上去。该模式用于希望将多个服务通过一个单独的端点暴露出去,并将请求按路由分发给适当的服务时。 4 | 5 | ## 背景与问题 6 | 7 | 当客户端需要消费多个服务时,为每个服务建一个单独的端点并由客户端去管理每个端点是非常有挑战性的。例如,某个电子商务应用可能会提供多个服务,如搜索、评论、购物车、付款和订单历史等。 8 | 9 | 每个服务都有不同的API,客户端需要与之交互,客户端必须知道每个端点才能连接到服务。如果某个端点修改或更新了,客户端也得跟着更新。如果将某个服务重构为两个或多个独立服务,代码就必须同时在服务端和客户端作出修改。 10 | 11 | ## 解决方案 12 | 13 | 在一组应用、服务或部署程序前面放一个网关。使用第7层应用路由将请求按路由发送给适当的实例。 14 | 15 | 使用该模式,客户端应用只需知道一个单独的端点并与之通信。如果服务被合并或分解,客户端并不需要随之更新。客户端仍然可以继续向网关发送请求,只有路由发生了变化。 16 | 17 | 网关还能让你从客户端的角度对后端服务进行抽象,允许你在保持客户端调用简单的情况下同时允许网关后面的后端服务发生变更。客户端调用可以被路由给任何一个或几个需要处理期望的客户端行为的服务,允许你添加、拆分和重新组织网关后面的服务,而不需修改客户端。 18 | 19 | ![](https://docs.microsoft.com/en-us/azure/architecture/patterns/_images/gateway-routing.png) 20 | 21 | 该模式对部署也有帮助,允许你管理更新是如何推出给用户的。当你的服务的某个新版本被部署后,还可以并行地部署旧版本的服务。你可以通过路由控制将哪个版本的服务展示给用户,这给予你使用不同发布策略的灵活性,是否增量、并行或全部发布。新服务部署后所发现的任何问题都可以被快速回滚,只需要在网关上修改配置即可,而不会影响到客户端。 22 | 23 | ## 问题与注意事项 24 | 25 | * 网关服务可能会引入单点故障。确保在设计时就考虑到可用性需求。在实现时考虑可恢复性与故障容错能力。 26 | * 网关服务可能会成为瓶颈。确保网关具有足够的性能以处理负载,并能够容易随着预期增长一致扩展。 27 | * 对网关进行负载测试,以确保你不会为服务引入级联性故障。 28 | * 网关路由位于第7层。可以基于IP、端口、header或URL来实现。 29 | 30 | ## 何时使用该模式 31 | 32 | 在以下场景使用该模式: 33 | 34 | * 一个客户端需要消费可以通过网关访问的多个服务时。 35 | * 希望通过使用单一端点来简化客户端应用。 36 | * 需要将请求通过路由从外部可寻址的端点分发给内部的虚拟端点,比如在虚拟机上暴露出一些端口指向集群的虚拟IP地址。 37 | 38 | 当你的应用程序比较简单,只有一两个服务时,该模式可能并不适用。 39 | 40 | ## 例子 41 | 42 | 将Nginx作为路由器,以下是一个服务器配置文件的简单示例,将属于不同虚拟目录的应用请求路由给不同的后端机器。 43 | 44 | ``` 45 | server { 46 | listen 80; 47 | server_name domain.com; 48 | 49 | location /app1 { 50 | proxy_pass http://10.0.3.10:80; 51 | } 52 | 53 | location /app2 { 54 | proxy_pass http://10.0.3.20:80; 55 | } 56 | 57 | location /app3 { 58 | proxy_pass http://10.0.3.30:80; 59 | } 60 | } 61 | ``` 62 | 63 | ## 相关指南 64 | 65 | * [前端专属的后端模式](patterns/backends-for-frontends.md) 66 | * [网关聚合模式](patterns/gateway-aggregation.md) 67 | * [网关卸载模式](patterns/gateway-offloading.md) 68 | -------------------------------------------------------------------------------- /patterns/health-endpoint-monitoring.md: -------------------------------------------------------------------------------- 1 | # 健康端点监控模式 2 | 3 | 在应用程序内部实现检查的功能,外部工具可以定期通过暴露出来的端点访问。这可以帮助验证应用程序和服务是否正常运行。 4 | 5 | ## 背景和问题 6 | 7 | 监控Web应用程序和后端服务是一个很好的实践,也是业务需求,以确保可用性和正常运行。但是,监控运行在云中的服务比本地服务更为困难。例如,没有对托管环境的完全控制权,并且服务通常依赖于平台供应商和其它服务。 8 | 影响云托管应用程序的因素很多,如网络延迟,底层计算和存储系统的性能和可用性以及它们之间的网络带宽。任何这些因素都可以导致服务完全或部分失败。因此,必须定期验证服务是否正常执行,以确保所需的可用性级别,这可能是你服务水平协议(SLA)的一部分。 9 | 10 | ## 解决方案 11 | 12 | 通过向应用程序的端点发送请求来实现健康监控。应用程序应执行必要的检查,并返回其状态的指示。 13 | 健康监测检查通常结合两个因素: 14 | * 响应于健康验证端点的请求,应用程序或服务执行的检查(如果有)。 15 | * 通过执行健康验证检查的工具或框架分析结果。 16 | 响应码反映应用程序、或者任何组件或者服务的状态。延迟或响应时间检查由监视工具或框架执行。下图概述了该模式。 17 | 18 | ![](https://docs.microsoft.com/en-us/azure/architecture/patterns/_images/health-endpoint-monitoring-pattern.png) 19 | 20 | 应用程序中的健康检查代码可能执行的其它检查包括: 21 | 22 | * 检查云存储或数据库的可用性和响应时间。 23 | * 检查位于应用程序中的其它资源或服务,或应用程序使用的其它资源或服务。 24 | * 相关的服务和工具通过向可配置的端点集发送请求来监视Web应用程序,并根据一组可配置规则评估结果。创建服务端点相对容易,其唯一目的是对系统执行一些功能测试。 25 | 26 | 监控工具可以执行的典型检查包括: 27 | 28 | * 验证响应代码。例如,HTTP响应为200(OK)表示应用程序无错误地响应。监控系统还可以检查其他响应代码,以获得更全面的结果。 29 | * 检查响应的内容以检测错误,即使返回200(OK)状态代码。这可以检测仅影响返回的网页或服务响应的一部分的错误。例如,返回页面的标题或查找指示正确页面的特定短语。 30 | * 测量响应时间,即网络延迟与应用程序执行请求所花费的时间的组合。发现应用程序或网络正在出现问题带来的价值越多。 31 | * 检查位于应用程序外部的资源或服务,例如应用程序用来全局缓存的内容分发网络(CDN)。 32 | * 检查SSL证书是否到期。 33 | * 测量应用程序URL的DNS查询的响应时间,以测量DNS延迟和DNS故障。 34 | * 验证DNS查询返回的URL以确保入口正确。这可以避免通过攻击DNS服务成功引发的器恶意请求重定向。 35 | 36 | 可能的话,也可以从不同的本地或托管服务运行检查来测量和比较响应时间。理想情况下应该从靠近客户的位置监控应用程序,以便准确了解每个位置的性能。除了提供更强大的检查机制之外,还可以帮助决策应用程序的部署位置,以及是否将其部署到多个数据中心。 37 | 还应针对客户使用的所有服务实例运行测试,以确保应用程序正常运行。例如,如果客户的存储分布在多个存储帐户中,监控需要检查所有这些。 38 | 39 | ## 问题和注意事项 40 | 41 | 实现此模式时,请考虑以下几点: 42 | 如何验证响应。例如,一个200(OK)状态代码足以验证应用程序是否正常工作?虽然这提供了最基本的应用程序可用性的测量,并且是此模式的最小实现,但它提供了有关应用程序中的操作,趋势和可能的即将到来的问题的少量信息。 43 | >确保应用程序仅在找到并正确处理目标资源时才返回200(OK)。在某些情况下,例如,使用主页来托管目标网页时,即使没有找到目标内容页面,服务器也会发回200(OK)状态代码而不是404(未找到)代码。 44 | 应用程序公开的端点数。一种方法是至少暴露应用使用的核心服务的一个端点,另一种用于低优先级服务,允许每个监控结果展示不同级别的重要性。还要考虑暴露更多的端点,例如每个核心服务的端点,以获得更多的监控粒度。例如,健康验证检查可能检查应用程序使用的数据库,存储和外部地理编码服务,每个服务需要的正常运行时间等级和响应时间都不相同。如果地理编码服务或其它一些后台任务几分钟不可用,应用程序仍然可以正常运行。 45 | 46 | 是否使用与通用访问相同的端点进行监控,但是是针对通用访问端点上的健康验证检查(例如/HealthCheck/{GUID}/)设计的特定路径。这允许应用程序中的一些功能测试由监控工具运行,例如添加新的用户注册,登录和订单测试,同时还验证一般访问端点是否可用。 47 | 响应监控请求在服务中收集的信息类型,以及如何返回此信息。大多数现有的工具和框架仅查看端点返回的HTTP状态代码。要返回并验证其它信息,可能需要创建自定义监视工具或服务。 48 | 49 | 要收集多少信息。在检查过程中执行过多的处理可能使应用程序过载,并影响其他用户。所需的时间可能超过监控系统的超时时间,因此它将应用程序标记为不可用。大多数应用程序包含了诸如错误处理程序和性能计数器之类的工具,在日志中记录性能和详细的错误信息,比从健康验证检查返回附加信息更加好用。 50 | 51 | 缓存端点状态。频繁运行健康检查成本太高。例如,如果通过仪表板报告运行状况,则不需要仪表板的每个请求来触发运行状况检查。而是定期检查系统运行状况并缓存状态。暴露返回缓存状态的端点。 52 | 53 | 如何配置监控端点的安全性以保护其免受公共访问,这可能会使应用程序暴露于恶意攻击,存在敏感信息的暴露或吸引拒绝服务(DoS)攻击。通常这应该在应用程序配置中完成,以便可以轻松更新,而无需重新启动应用程序。考虑使用以下一种或多种技术: 54 | 55 | * 通过要求身份验证来保护端点。可以通过在请求头中使用身份验证安全密钥或通过传递带有请求的凭据来进行此操作,前提是监控服务或工具支持身份验证。 56 | * 使用模糊或隐藏的端点。例如,将端点暴露于与默认应用程序URL使用的不同IP地址上的端点,在非标准HTTP端口上配置端点或使用测试页面的复杂路径。通常可以在应用程序配置中指定其它端点地址和端口,如果需要,可以将这些端点的条目添加到DNS服务器,以避免直接指定IP地址。 57 | * 在接受诸如键值对或操作模式值的参数的端点上公开一种方法。根据为此参数提供的值,在接收到请求时,代码可以执行特定的测试或一组测试,或者如果未识别参数值,则返回404错误。识别的参数值可以在应用程序配置中设置。 58 | >DoS攻击可能对执行基本功能测试的单独端点的影响较小,而不会影响应用程序的操作。理想情况下,避免使用可能暴露敏感信息的测试。如果您必须返回可能对攻击者有用的信息,请考虑如何保护端点和数据免遭未经授权的访问。在这种情况下,只是依赖于晦涩是不够的。您还应考虑使用HTTPS连接和加密任何敏感数据,尽管这将增加服务器上的负载。 59 | 60 | * 如何访问使用身份验证保护的端点。并不是所有的工具和框架都可以配置为包含身份验证请求的凭据。例如,Microsoft Azure内置的健康验证功能无法提供身份验证凭据。可以使用一些第三方的服务如Pingdom,Panopta,NewRelic和Statuscake。 61 | * 如何确保监控代理正常运行。一种方法是公开一个简单地从应用程序配置返回值的端点或可用于测试代理的随机值。 62 | >还要确保监控系统对自身进行检查,如自检和内置测试,以避免发生假阳性结果。 63 | 64 | ## 何时使用该模式 65 | 66 | 此模式适用于以下场景: 67 | * 监控网站和网络应用程序以验证可用性。 68 | * 监控网站和网络应用程序以检查正确的操作。 69 | * 监控中间层或共享服务,以检测和隔离可能会中断其它应用程序的故障。 70 | * 补充应用程序中的现有仪器,如性能计数器和错误处理程序。健康检查不会取代应用程序中记录和审核的要求。仪器可以为现有框架提供有价值的信息,监控计数器和错误日志可以用来检测故障或其它问题。但是,如果应用程序不可用,则无法提供信息。 71 | 72 | ## 案例 73 | 74 | 下面的例子中`HealthCheckController`类代码(代码可在[GitHub](https://github.com/mspnp/cloud-design-patterns/tree/master/health-endpoint-monitoring)上)展示了一个端点,用于执行一系列健康检查。 75 | `CoreServices`方法对应用程序中使用的服务执行一系列检查。如果所有的测试运行没有错误,方法返回200(OK)状态代码。如果测试引发异常,方法将返回一个500(内部错误)状态代码。如果监控工具或框架能使用该方法,可以选择在发生错误时返回附加信息。 76 | 77 | ```java 78 | public ActionResult CoreServices() 79 | { 80 | try 81 | { 82 | // Run a simple check to ensure the database is available. 83 | DataStore.Instance.CoreHealthCheck(); 84 | 85 | // Run a simple check on our external service. 86 | MyExternalService.Instance.CoreHealthCheck(); 87 | } 88 | catch (Exception ex) 89 | { 90 | Trace.TraceError("Exception in basic health check: {0}", ex.Message); 91 | 92 | // This can optionally return different status codes based on the exception. 93 | // Optionally it could return more details about the exception. 94 | // The additional information could be used by administrators who access the 95 | // endpoint with a browser, or using a ping utility that can display the 96 | // additional information. 97 | return new HttpStatusCodeResult((int)HttpStatusCode.InternalServerError); 98 | } 99 | return new HttpStatusCodeResult((int)HttpStatusCode.OK); 100 | } 101 | ``` 102 | 103 | `ObscurePath`方法从应用程序配置读取路径并将其用作测试的端点。这个C#的例子也显示了如何接受ID作为参数,并用它来检查有效的请求。 104 | ```java 105 | public ActionResult ObscurePath(string id) 106 | { 107 | // The id could be used as a simple way to obscure or hide the endpoint. 108 | // The id to match could be retrieved from configuration and, if matched, 109 | // perform a specific set of tests and return the result. If not matched it 110 | // could return a 404 (Not Found) status. 111 | 112 | // The obscure path can be set through configuration to hide the endpoint. 113 | var hiddenPathKey = CloudConfigurationManager.GetSetting("Test.ObscurePath"); 114 | 115 | // If the value passed does not match that in configuration, return 404 (Not Found). 116 | if (!string.Equals(id, hiddenPathKey)) 117 | { 118 | return new HttpStatusCodeResult((int)HttpStatusCode.NotFound); 119 | } 120 | 121 | // Else continue and run the tests... 122 | // Return results from the core services test. 123 | return this.CoreServices(); 124 | } 125 | ``` 126 | `TestResponseFromConfig`方法显示如何暴露一个检查指定配置设置值的端点。 127 | 128 | ```java 129 | public ActionResult TestResponseFromConfig() 130 | { 131 | // Health check that returns a response code set in configuration for testing. 132 | var returnStatusCodeSetting = CloudConfigurationManager.GetSetting( 133 | "Test.ReturnStatusCode"); 134 | 135 | int returnStatusCode; 136 | 137 | if (!int.TryParse(returnStatusCodeSetting, out returnStatusCode)) 138 | { 139 | returnStatusCode = (int)HttpStatusCode.OK; 140 | } 141 | 142 | return new HttpStatusCodeResult(returnStatusCode); 143 | } 144 | ``` 145 | ## 监控Azure托管应用程序中的端点 146 | 147 | Azure应用程序中监视端点的选项有: 148 | * 使用Azure的内置监控功能。 149 | * 使用第三方服务或框架,如Microsoft System Center Operations Manager。 150 | * 创建一个自定义实用程序或一个在自己的或托管的服务器上运行的服务。 151 | 152 | >即使Azure提供了相当全面的监控选项,也可以使用其它服务和工具来提供额外的信息。Azure管理服务提供内置的警报规则监控机制。 Azure门户中的管理服务页面的告警部分可以为服务配置最多10个警报规则。这些规则包含(如CPU负载)或每秒请求或错误数量指定条件和阈值,并且服务可以自动向每个规则中定义的地址发送电子邮件通知。 153 | 154 | 取决于应用程序的托管机制(如Web站点,云服务,虚拟机或移动服务),监控条件各不相同,但这些都拥有在服务的设置中指定创建使用Web端点的警报规则的功能。端点应及时响应,以使警报系统能够检测到应用程序正常运行。 155 | 阅读更多有关[创建警报](https://azure.microsoft.com/documentation/articles/insights-receive-alert-notifications/)通知的信息。 156 | 157 | 如果你在Azure Cloud Services Web和工作角色或虚拟机中托管应用程序,可以利用Azure中的一种内置服务(称为流量管理器)。流量管理器是路由和负载平衡服务,可以根据一系列规则和设置将请求分发到云服务托管应用程序的特定实例。 158 | 除了路由请求之外,流量管理器会定期ping指定的URL,端口和相对路径,以确定其规则中定义的应用程序的哪些实例是活动的并且响应请求。如果检测到状态代码200(OK),它将应用程序标记为可用。任何其它状态代码都会导致流量管理器将应用程序标记为离线。你可以在流量管理器控制台中查看状态,并配置规则将请求重新路由到正在响应的应用程序的其它实例。 159 | 但是,流量管理器只会等待十秒钟从监控URL接收到响应。因此,应该确保在此时间内执行健康验证码,从而允许从流量管理器往返应用程序的网络延迟。 160 | >阅读更多有关[使用流量管理器监控应用程序](https://azure.microsoft.com/documentation/services/traffic-manager/)的内容。[多数据中心部署指南](https://msdn.microsoft.com/library/dn589779.aspx)中还讨论了流量管理器。 161 | 162 | ## 相关指南 163 | 以下指南在实现此模式时有用: 164 | * [仪表与遥测指南](https://msdn.microsoft.com/library/dn589775.aspx)。检查服务和组件的运行状况通常通过探测来完成,但是也可以使用信息来监控应用程序性能并检测运行时发生的事件。该数据可以作为健康监测的附加信息传回监控工具。仪器和遥测指南探索收集仪器在应用程序中收集的远程诊断信息。 165 | * [接收警报通知](https://azure.microsoft.com/documentation/articles/insights-receive-alert-notifications/)。 166 | * 使用此模式的[示例应用程序下载](https://github.com/mspnp/cloud-design-patterns/tree/master/health-endpoint-monitoring)。 -------------------------------------------------------------------------------- /patterns/index-table.md: -------------------------------------------------------------------------------- 1 | # 索引表模式 2 | 3 | 通过查询经常引用的数据存储区中的字段创建索引。该模式可以通过允许应用程序更快速从数据存储中检索定位数据来提高查询性能。 4 | 5 | ## 背景和问题 6 | 7 | 许多数据存储使用主键来组织实体集合的数据。应用程序可以使用该键来定位和检索数据。下图是存储客户信息的数据存储的例子。主键是Customer ID。该图显示了由主键(Customer ID)组织的客户信息。 8 | 9 | ![](https://docs.microsoft.com/en-us/azure/architecture/patterns/_images/index-table-figure-1.png) 10 | 11 | 虽然主键对于根据该键的值查询获取数据很有价值,但如果需要根据某个其它字段检索数据,应用程序可能无法使用主键。在上面的例子中,只有通过引用某个其它属性(例如客户所在的town)的值来查询数据时,应用程序无法使用Customer ID主键来检索客户。要执行这样的查询,应用程序可能必须获取并检查每个客户记录,这可能是一个缓慢的过程。 12 | 13 | 许多关系数据库管理系统支持二级索引。二级索引是由一个或多个非主要(次要)键字段组织的单独的数据结构,指示存储每个索引值的数据的位置。二级索引中的项目通常按二级主键的值进行排序,以便能够快速查找数据。这些索引通常由数据库管理系统自动维护。 14 | 15 | 你可以创建尽可能多的二级索引,因为需要支持应用程序执行的不同查询。例如,在关系数据库中的Customers表中,Customer ID是主键,如果应用程序经常在其所在的town查找客户,在town字段中添加二级索引会有所帮助。 16 | 17 | 然而,虽然二级索引在关系数据库中很常见,但云应用程序提供的大多数NoSQL数据存储无法提供相同的功能。 18 | 19 | ## 解决方案 20 | 21 | 如果数据存储不支持二级索引,可以通过创建自己的索引表来手动模拟。索引表通过特定的键组织数据。通常使用三种策略来构造索引表,具体取决于所需的二级索引的数量以及应用程序执行的查询的性质。 22 | 23 | 第一个策略是复制每个索引表中的数据,但是通过不同的键进行组织(完全非规范化)。下图显示了通过Town和LastName组织相同客户信息的索引表。 24 | ![](https://docs.microsoft.com/en-us/azure/architecture/patterns/_images/index-table-figure-2.png) 25 | 26 | 该策略适于数据与使用每个键查询的次数相比是相对静态的场景。如果数据更具动态性,则维护每个索引表的处理开销变得太大,不适于使用。此外,如果数据量非常大,则存储重复数据所需的空间量很大。 27 | 第二种策略是创建由不同键组织的归一化索引表,并使用主键而不是复制原始数据,如下图所示。 原始数据称为事实表。 28 | ![](https://docs.microsoft.com/en-us/azure/architecture/patterns/_images/index-table-figure-3.png) 29 | 30 | 这种技术可节省空间并减少维护重复数据的开销。缺点是应用程序必须执行两次查找操作以使用二级键查找数据。必须在索引表中找到数据的主键,然后使用主键查找事实表中的数据。 31 | 第三种策略是创建通过重复频繁检索的字段的不同键组织的部分归一化索引表。参考事实表来访问访问频度较低的字段。下图显示了频繁访问的数据是如何复制到每个索引表的。 32 | 33 | ![](https://docs.microsoft.com/en-us/azure/architecture/patterns/_images/index-table-figure-4.png) 34 | 35 | 这一策略可以在前两种方法之间取得平衡。普通查询的数据可以通过使用单个查找快速检索,而空间和维护开销没有复制整个数据集大。 36 | 37 | 如果应用程序经常通过指定组合值来查询数据(例如,“查找居住在Redmond的所有客户,姓氏为Smith”),则可以将索引表中的项目的键实现为级联的Town属性和LastName属性。下图展示了基于复合键的索引表。键按Town排序,然后按Town值相同的LastName排序。 38 | ![](https://docs.microsoft.com/en-us/azure/architecture/patterns/_images/index-table-figure-5.png) 39 | 40 | 索引表可以加快对分片数据的查询操作,并且在碎片密钥散列时特别有用。下图中的例子分片键是Customer ID的哈希值。索引表可以通过非标记值(Town和LastName)组织数据,并提供散列分片键作为查找数据。如果需要检索落在一个范围内的数据,或者按照非散列键的顺序获取数据取,则可以避免应用程序重复计算散列键(昂贵的操作)。例如,可以通过在索引表中找到匹配的项目,将它们全部存储在连续的块中来快速解决诸如“查找居住在Redmond的所有客户”之类的查询。 然后,使用存储在索引表中的分片键,跟随客户数据的引用。 41 | ![](https://docs.microsoft.com/en-us/azure/architecture/patterns/_images/index-table-figure-6.png) 42 | 43 | ## 问题和注意事项 44 | 45 | 实现此模式时,请考虑以下几点: 46 | * 维持二级索引的开销可能很大。必须分析和了解应用程序使用的查询。只有在经常使用时才能创建索引表。不要创建推测索引表来支持应用程序不执行或仅偶尔执行的查询。 47 | * 在索引表中复制数据可能会增加存储成本巨大开销和维护多份数据副本的工作量。 48 | * 用引用原始数据的规范化结构实现索引表要求应用程序执行两次查找操作找到数据。第一个操作搜索索引表以检索主键,第二个操作使用主键来获取数据。 49 | * 如果系统在非常大的数据集中包含多个索引表,则很难在索引表与原始数据之间保持一致性。可能围绕最终一致性模型设计应用程序。例如,要插入,更新或删除数据,应用程序可以将消息发布到队列,并让单独的任务执行操作并维护异步引用此数据的索引表。有关实现最终一致性的更多信息,请参阅[数据一致性入门](https://msdn.microsoft.com/library/dn589800.aspx)。 50 | > 微软Azure存储表支持对同一分区(又称实体组事务)中保存的数据所做更改的事务更新。如果可以将事实表和一个或多个索引表的数据存储在同一分区中,则可以使用此功能来确保一致性。 51 | * 索引表本身可能分区或分片。 52 | 53 | ## 何时使用该模式 54 | 55 | 当应用程序经常需要使用除主(或分片)键之外的键来检索数据时,使用此模式可以提高查询性能。 56 | 此模式可能不适于以下场景: 57 | 58 | * 数据不稳定。索引表可能很快过时,使其无效或使维护索引表的开销大于使用索引表所节省的成本。 59 | * 索引表选择的二级索引字段无差别,只能具有一小组值(例如,性别)。 60 | * 索引表的二级键选择的字段的数据值的平衡偏差较高。例如,如果90%的记录在字段中包含相同的值,则创建和维护索引表,根据此字段查找数据可能会比依次扫描数据开销更大。但是,如果查询频繁访问的目标值位于剩余的10%中,该索引可能是有用的。应该了解应用程序执行的查询以及执行的频率。 61 | 62 | ## 案例 63 | Azure存储表为在云中运行的应用程序提供了高度可扩展的键/值数据存储。应用程序通过指定键来存储和检索数据值。数据值可以包含多个字段,但数据项的结构对表存储是不透明的,它只是将数据项作为字节数组处理。 64 | 65 | Azure存储表还支持分片。分片键包括两个元素,分区键和行键。具有相同分区键的项目存储在相同的分区(分片)中,并且项目以分片中的行键顺序存储。优化表存储用于执行查询,以获取落在分区内的行键值的连续范围内的数据。如果构建的应用程序正在使用Azure表存储信息,应考虑使用此功能来构建数据。 66 | 67 | 例如,存储有关电影的信息的应用程序。应用程序经常根据类型(动作,纪录片,历史,喜剧,戏剧等)查询电影。可以通过使用类型作为分区键为每个类型创建一个具有分区的Azure表,并将电影名称指定为行键,如下图所示。 68 | 69 | ![](https://docs.microsoft.com/en-us/azure/architecture/patterns/_images/index-table-figure-7.png) 70 | 71 | 如果应用程序还需要通过主演查询电影,使用这种方法效果较差。在这种情况下,可以创建一个独立的Azure表作为索引表。分区键是演员,行键是电影名称。每个演员的数据将存储在不同的分区中。如果电影有超过一个电影明星,同一部电影将出现在多个分区中。 72 | 可以通过采用上面“解决方案”部分中描述的第一种方法,复制每个分区所保存的值中的电影数据。然而,每个电影很可能复制好几次(每个演员一次),所以可能会更有效地将数据反规范化以支持最常见的查询(例如其他演员的名称),并使应用程序通过包括在类型分区中找到完整信息所必需的分区键来检索任何剩余的详细内容。此方法为“解决方案”部分中的第三个策略。下图展示了这种方法。 73 | ![](https://docs.microsoft.com/en-us/azure/architecture/patterns/_images/index-table-figure-8.png) 74 | 75 | ## 相关模式和指南 76 | 77 | 以下模式和指南在实现此模式时也可能相关: 78 | * [数据一致性入门](https://msdn.microsoft.com/library/dn589800.aspx)。索引表必须在数据更改时同步其索引。在云中,将执行更新索引的操作作为修改数据的相同事务不太可能也不合适。在这种情况下,采用最终一致更为合适。文章提供了有关最终一致性问题的相关内容。 79 | 可能作为修改数据的相同事务的部分 80 | * [分片模式](sharding.md)。索引表模式经常与使用分片分割的数据结合使用。分片模式提供了有关如何将数据存储分成一组分片的相关内容。 81 | * [物化视图模式](materialized-view.md)。相比索引数据来支持汇总数据的查询,可能创建数据的物化视图更合适。物化视图模式中介绍了如何通过在数据上生成预填充视图来支持高效的汇总查询。 -------------------------------------------------------------------------------- /patterns/leader-election.md: -------------------------------------------------------------------------------- 1 | # 领导者模式 2 | 通过选择一个实例作为管理其它实例的领导者,协调分布式应用程序中的一系列协作实例所执行的操作。 这可以确保实例不会因为相互冲突,导致争用共享资源,或者无意中干扰其它实例正在执行的工作。 3 | 4 | ## 背景和问题 5 | 典型的云应用程序以协调的方式运行许多任务。这些任务可以是运行相同代码并访问相同资源的实例,也可以并行执行复杂计算的各个部分。 6 | 任务实例大部分时间可能在单独运行,也可能需要协调每个实例的操作,以确保不会发生冲突,导致共享资源的争用,或者意外地干扰其它正在运行的任务实例的工作。 7 | 例如: 8 | * 在实现水平缩放的基于云的系统中,可以同时运行同一任务的多个实例,每个实例服务于不同的用户。如果这些实例写入共享资源,则必须协调其操作以防止每个实例覆盖其它实例所做的更改。 9 | * 如果任务并行执行复杂计算的各个元素,则在完成所有结果时需要汇总结果。 10 | 任务实例都是对等的,所以没有一个自然的领导可以充当协调者或聚合者。 11 | 12 | ## 解决方案 13 | 14 | 应该选择一个任务实例作为领导者,用来协调其它从属任务实例的动作。如果所有任务实例都运行相同的代码,那么它们每个都可以充当领导者。因此,必须认真管理选举过程,防止两个或更多的实例同时充当领导者角色。 15 | 系统必须提供一个强大的选择领导者的机制。这种方法必须处理诸如网络中断或流程失败等事件。在许多解决方案中,下级任务实例通过某种类型的心跳方法或通过轮询来监视领导者。如果指定的领导者意外终止,或者网络故障使得领导者不能从属于任务实例,那么有必要选出一个新的领导者。 16 | 在分布式环境中的一组任务中选择领导者有几种策略,包括: 17 | * 选择排名最低的实例或进程ID的任务实例。 18 | * 竞赛以获得共享的,分布式的互斥量。获得互斥量的第一个任务实例是领导者。然而,系统必须确保领导者终止或与系统的其它部分断开连接时,释放互斥体以允许另一个任务实例成为领导者。 19 | * 实现常见的领导者选举算法之一,比如Bully算法或者Ring算法。这些算法假定选举中的每个候选人都有一个唯一的ID,并且可以可靠地与其它候选人通信。 20 | 21 | ## 问题和注意事项 22 | 在决定如何实现该模式时,请考虑以下几点: 23 | * 选举领导者的过程应该是对瞬态和持续失败具备弹性。 24 | * 必须能够检测领导者何时失败或不可用(例如由于通信故障)。如何快速检测是系统相关的。有些系统可能在没有领导者的情况下短时间工作,在此期间解决瞬时故障。在其它情况下,可能需要立即检测领导者失败并引发新的选举。 25 | * 在实现水平自动缩放的系统中,如果系统收缩并关闭一些计算资源,则可以终止领导者。 26 | * 使用共享的分布式互斥量引入了对提供互斥量的外部服务的依赖。该服务构成单点故障。如果因任何原因无法使用,系统将无法选举领导者。 27 | * 使用专门的流程作为领导是一个直接的方法。但是,如果进程失败,那么在重新启动时可能会有明显的延迟。如果等待领导者协调一个操作,则所产生的延迟会影响其它进程的性能和响应时间。 28 | * 实现一个领导者选举算法手动提供了最大的灵活性来调整和优化代码。 29 | 30 | ## 何时使用该模式 31 | 当分布式应用程序中的任务(如云托管解决方案)需要仔细协调并且没有天生的领导者时,请使用此模式: 32 | * 避免让领导者成为系统中的瓶颈。领导者的目的是协调下属任务的工作,而不一定要参与这项工作本身-尽管如果任务不被指派给领导者就可以了。 33 | 以下情况可能不适合该模式: 34 | * 有一个自然的领导者或专门的过程,总是可以扮演领导者。例如,可以实现协调任务实例的单例过程。如果此过程失败或变得不健康,系统可以关闭并重新启动。 35 | * 可以使用更轻量级的方法来实现任务之间的协调。例如,如果几个任务实例只需要对共享资源进行协调访问,则更好的解决方案是使用乐观或悲观锁定来控制访问。 36 | * 第三方解决方案更合适。例如,Microsoft Azure HDInsight服务(基于Apache Hadoop)使用Apache Zookeeper提供的服务来协调收集和汇总数据的map reduce任务。 37 | 38 | ## 案例 39 | `LeaderElection`解决方案中的`DistributedMutex`项目(此模式的演示代码可以[GitHub](https://github.com/mspnp/cloud-design-patterns/tree/master/leader-election)上找到)显示了如何使用Azure存储区域上的租约来提供实现共享的分布式互斥量的机制。这个互斥量可以用来选择Azure云服务中的一组角色实例中的领导者。获得租赁的第一个角色实例被选举为领导者,直到它解除租约或不能续租为止。其他角色实例可以继续监视blob租约,以防领导者不再可用。 40 | 41 | >blob租约是blob上的独占写入锁。在任何时间点,一个blob可能只是一个租约的主题。角色实例可以通过指定的blob请求租约,如果没有其它角色实例在同一个blob上持有租约,它将被授予租约。否则,请求会抛出异常。 42 | >为避免故障的角色实例无限期地保留租约,请为租约指定一个生命周期。租约到期时将变为可用。但是,角色实例持有租约时,可以要求续租,并且将续租一段时间。角色实例可以不断重复这个过程,如果它想保留租约。有关如何租用Blob的更多信息,请参阅[租赁Blob(REST API)](https://msdn.microsoft.com/library/azure/ee691972.aspx)。 43 | 44 | 下面的C#示例中的`BlobDistributedMutex`类包含的`RunTaskWhenMutexAquired`方法允许角色实例尝试获取指定blob上的租约。创建`BlobDistributedMutex`对象(该对象是包含在示例代码中的简单结构体)时,Blob(名称,容器和存储帐户)的详细信息将传递给`BlobSettings`对象中的构造函数。构造函数还接受一个Task,该Task引用角色实例应该运行的代码,如果它成功地获得blob上的租约并成为领导者。请注意,处理获取租约的底层详细代码是在名为`BlobLeaseManager`的单独的辅助类中实现的。 45 | 46 | ```c# 47 | public class BlobDistributedMutex 48 | { 49 | ... 50 | private readonly BlobSettings blobSettings; 51 | private readonly Func taskToRunWhenLeaseAcquired; 52 | ... 53 | 54 | public BlobDistributedMutex(BlobSettings blobSettings, 55 | Func taskToRunWhenLeaseAquired) 56 | { 57 | this.blobSettings = blobSettings; 58 | this.taskToRunWhenLeaseAquired = taskToRunWhenLeaseAquired; 59 | } 60 | 61 | public async Task RunTaskWhenMutexAcquired(CancellationToken token) 62 | { 63 | var leaseManager = new BlobLeaseManager(blobSettings); 64 | await this.RunTaskWhenBlobLeaseAcquired(leaseManager, token); 65 | } 66 | ... 67 | ``` 68 | 上面代码中的`RunTaskWhenMutexAquired`方法调用以下代码中的`RunTaskWhenBlobLeaseAcquired`方法以获取实际租约。 `RunTaskWhenBlobLeaseAcquired`方法异步运行。如果成功获得租约,角色实例将成为领导者。`taskToRunWhenLeaseAcquired`委托的目的是执行协调其他角色实例的工作。 如果没有获得租约,则选择另一个角色实例作为领导者,而当前角色实例仍然是下属。请注意,`TryAcquireLeaseOrWait`方法是使用`BlobLeaseManager`对象获取租约的帮助方法。 69 | ```c# 70 | private async Task RunTaskWhenBlobLeaseAcquired( 71 | BlobLeaseManager leaseManager, CancellationToken token) 72 | { 73 | while (!token.IsCancellationRequested) 74 | { 75 | // Try to acquire the blob lease. 76 | // Otherwise wait for a short time before trying again. 77 | string leaseId = await this.TryAquireLeaseOrWait(leaseManager, token); 78 | 79 | if (!string.IsNullOrEmpty(leaseId)) 80 | { 81 | // Create a new linked cancellation token source so that if either the 82 | // original token is canceled or the lease can't be renewed, the 83 | // leader task can be canceled. 84 | using (var leaseCts = 85 | CancellationTokenSource.CreateLinkedTokenSource(new[] { token })) 86 | { 87 | // Run the leader task. 88 | var leaderTask = this.taskToRunWhenLeaseAquired.Invoke(leaseCts.Token); 89 | ... 90 | } 91 | } 92 | } 93 | ... 94 | } 95 | ``` 96 | 领导者开始的任务也是异步运行的。在任务正在运行时,以下代码中的`RunTaskWhenBlobLeaseAquired`方法会定期尝试续订租约。 这有助于确保角色实例仍然是领导者。在案例解决方案中,续订请求之间的延迟时间小于租用期间指定的时间,以防止另一个角色实例被选为领导者。 如果由于任何原因续约失败,取消任务。 97 | 如果租约未能续订或任务取消(可能因为角色实例关闭),则租约将被释放。此时,这个或另一个角色实例可能被选为领导者。下面的代码断显示了这部分过程。 98 | ```c# 99 | private async Task RunTaskWhenBlobLeaseAcquired( 100 | BlobLeaseManager leaseManager, CancellationToken token) 101 | { 102 | while (...) 103 | { 104 | ... 105 | if (...) 106 | { 107 | ... 108 | using (var leaseCts = ...) 109 | { 110 | ... 111 | // Keep renewing the lease in regular intervals. 112 | // If the lease can't be renewed, then the task completes. 113 | var renewLeaseTask = 114 | this.KeepRenewingLease(leaseManager, leaseId, leaseCts.Token); 115 | 116 | // When any task completes (either the leader task itself or when it 117 | // couldn't renew the lease) then cancel the other task. 118 | await CancelAllWhenAnyCompletes(leaderTask, renewLeaseTask, leaseCts); 119 | } 120 | } 121 | } 122 | } 123 | ... 124 | } 125 | ``` 126 | `KeepRenewingLease`方法是使用`BlobLeaseManager`对象更新租约的另一个辅助方法。`CancelAllWhenAnyCompletes`方法取消指定为前两个参数的任务。下图说明了如何使用`BlobDistributedMutex`类来选举领导者并运行协调操作的任务。 127 | ![](https://docs.microsoft.com/en-us/azure/architecture/patterns/_images/leader-election-diagram.png) 128 | 以下代码示例展示了如何在辅助角色中使用`BlobDistributedMutex`类。代码通过开发存储中的租约容器中名为`MyLeaderCoordinatorTask`的blob获取租约,如果角色实例被选为领导者,则运行`MyLeaderCoordinatorTask`方法中定义的代码。 129 | ```c# 130 | var settings = new BlobSettings(CloudStorageAccount.DevelopmentStorageAccount, 131 | "leases", "MyLeaderCoordinatorTask"); 132 | var cts = new CancellationTokenSource(); 133 | var mutex = new BlobDistributedMutex(settings, MyLeaderCoordinatorTask); 134 | mutex.RunTaskWhenMutexAcquired(this.cts.Token); 135 | ... 136 | 137 | // Method that runs if the role instance is elected the leader 138 | private static async Task MyLeaderCoordinatorTask(CancellationToken token) 139 | { 140 | ... 141 | } 142 | ``` 143 | 请注意关于案例解决方案中以下几点: 144 | * blob存在单点故障的可能。如果blob服务变得不可用或无法访问,领导者将不能续订租约,并且其他任何角色实例都不能获得租约。在这种情况下,任何角色实例都不能成为领导者。然而,blob服务被设计为具有弹性,所以blob服务的完全失败被不太可能。 145 | * 如果领导者执行的任务失败,可能会继续更新租约,阻止任何其他角色实例获得租约并接管领导角色以协调任务。在现实世界中,应该经常检查领导者的健康状况。 146 | * 选举过程是不确定的。不能假定哪个角色实例会获得blob租约并成为领导者。 147 | * 用作blob租赁目标的blob不应该用于其它任何目的。如果角色实例试图将数据存储在此blob中,则除非角色实例是领导者并拥有blob租约,否则将无法访问此数据。 148 | 149 | ## 相关模式和指南 150 | 151 | 以下模式和指南在实现此模式时可能相关: 152 | * 这种模式有一个可示例应用程序可以在[这里](https://github.com/mspnp/cloud-design-patterns/tree/master/leader-election)下载。 153 | * [自动缩放指南](https://msdn.microsoft.com/library/dn589774.aspx)。应用程序的负载变化时,可以启动和停止任务主机的实例。自动缩放可以在高峰期间帮助保持吞吐量和性能。 154 | * [计算分区指南](https://msdn.microsoft.com/library/dn589773.aspx)。该指南介绍了如何以一种有助于最大限度降低运营成本同时保持服务的可伸缩性,性能,可用性和安全性的方式,将任务分配给云服务中的主机。 155 | * [基于任务的异步模式](https://msdn.microsoft.com/library/hh873175.aspx)。 156 | * [介绍欺负算法的例子](http://www.cs.colostate.edu/~cs551/CourseNotes/Synchronization/BullyExample.html)。 157 | * [介绍环算法的例子](http://www.cs.colostate.edu/~cs551/CourseNotes/Synchronization/RingElectExample.html)。 158 | * Microsoft Open Technologies网站上关于[Apache Zookeeper在Microsoft Azure上使用的文章](https://msopentech.com/opentech-projects/apache-zookeeper-on-windows-azure-2/)。 159 | * [Apache Curator](http://curator.apache.org/)是Apache ZooKeeper的客户端库。 160 | * MSDN上的[Lease Blob(REST API)文章](https://msdn.microsoft.com/library/azure/ee691972.aspx)。 -------------------------------------------------------------------------------- /patterns/materialized-view.md: -------------------------------------------------------------------------------- 1 | # 物化视图模式 2 | 3 | 当数据未针对所需的查询操作进行理想的格式化时,在一个或多个数据存储区中的数据上生成预填充的视图。这可以帮助高效的查询和数据提取,并提高应用程序的性能。 4 | 5 | ## 背景和问题 6 | 7 | 在存储数据时,开发人员和数据管理员的优先级往往集中在如何存储数据,而不是如何读取数据。所选择的存储格式通常与数据格式,管理数据大小和数据完整性的要求以及使用中的存储类型密切相关。例如,使用NoSQL文档存储时,数据通常表示为一系列聚合,每个聚合包含该实体的所有信息。 8 | 9 | 但是,这可能会对查询产生负面影响。 当查询只需要来自某些实体的数据的一个子集,例如几个客户的订单摘要而不是所有的订单细节时,它就必须提取相关实体的所有数据以获得所需的信息。 10 | 11 | ## 解决方案 12 | 13 | 为了支持高效的查询,一个通用的解决方案是事先生成一个视图,以适合所需结果集的格式实现数据。物化视图模式描述了在源数据不适于合适格式的查询,或者由于数据或数据存储的性质而导致查询性能差的情况下,生成数据的预先填充的视图。 14 | 15 | 这些仅包含查询所需数据的物化视图允许应用程序快速获取所需的信息。除了连接表或组合数据实体之外,物化视图还可以包括计算列或数据项的当前值,组合值或对数据项执行转换的结果以及作为查询一部分指定的值。物化视图甚至可以针对单个查询进行优化。 16 | 17 | 关键点是,物化视图及其包含的数据是完全一次性的,因为它可以完全从源数据存储重建。物化视图永远不会被应用程序直接更新,所以它是一个专门的缓存。 18 | 19 | 当视图的源数据更改时,必须更新视图以包含新信息。可以设置为自动更新,或者在系统检测到对原始数据的更改更新。在某些情况下,可能需要手动重新生成视图。下图是如何使用物化视图模式的例子。 20 | ![](https://docs.microsoft.com/en-us/azure/architecture/patterns/_images/materialized-view-pattern-diagram.png) 21 | 22 | ## 问题和注意事项 23 | 24 | 在决定如何实现这种模式时,请考虑以下几点: 25 | 26 | * 如何以及何时更新视图。理想情况下,它将会响应于指示源数据更改的事件而重新生成,但是如果源数据快速更改,这可能会导致过多的开销。或者,考虑使用计划任务,外部触发器或手动操作来重新生成视图。 27 | 28 | * 在某些系统中,例如当使用事件源模式来维护只修改数据的事件时,物化视图是必需的。通过检查所有事件来确定当前状态来预先填充视图可能是从事件存储库获取信息的唯一方法。如果不使用事件溯源,则需要考虑物化视图是否有用。物化视图往往是专门为一个或少量的查询定制的。如果使用了许多查询,物化视图会导致不可接受的存储容量要求和存储成本。 29 | 30 | * 考虑生成视图时的数据一致性以及更新视图(如果按计划进行)时对数据一致性的影响。如果源数据在生成视图时发生变化,则视图中的数据副本将不会与原始数据完全一致。 31 | 32 | * 考虑将在哪里存储视图。该视图不必与原始数据位于同一存储或分区中。它可以是几个不同分区组合的子集。 33 | 34 | * 视图如果丢失可以重建。因此,如果视图是暂时的,并且仅用于通过反映数据的当前状态来提高查询性能,或者为了提高可伸缩性,则可以将其存储在缓存中或不太可靠的位置。 35 | 36 | * 在定义物化视图时,根据现有数据项的计算或转换,查询中传递的值或适当时组合这些值,通过向其添加数据项或列来最大化其值。 37 | 38 | * 在存储机制支持的情况下,考虑索引物化视图以进一步提高性能。大多数关系数据库都支持索引视图,而基于Apache Hadoop的大数据解决方案也是如此。 39 | 40 | ## 何时使用该模式 41 | 42 | 这种模式在以下情况下很有用 43 | 44 | * 对难以直接查询的数据创建物化视图,或者非常复杂的查询才能提取的,以规范化,半结构化或非结构化方式存储的数据。 45 | * 创建可显著提高查询性能的临时视图,或者可以直接作为UI的源视图或数据传输对象,用于报告或展示。 46 | * 在数据存储连接不总是可用时,支持偶尔连接或断开连接的情况。在这种情况下,视图可以缓存在本地。 47 | * 不需要知道源数据格式的方式简化查询和公开数据以进行实验。例如,通过在一个或多个数据库中加入不同的表,或在NoSQL存储中加入一个或多个域,然后格式化数据以适应其最终用途。 48 | * 为了安全或隐私的原因,提供访问源数据的特定子集的访问权限通常不应该是可访问的,可以修改或者完全暴露给用户。 49 | * 连接不同的数据存储,以利用其单独功能。例如,使用写入高效的云存储作为参考数据存储,以及提供良好查询和读取性能来保存物化视图的关系数据库。 50 | 51 | 该模式不适于以下情况: 52 | 53 | * 源数据简单易于查询。 54 | * 源数据变化非常快,或者可以在不使用视图的情况下访问。在这些情况下,应该避免创建视图的处理开销。 55 | * 一致性是重中之重。这些视图可能并不总是与原始数据完全一致。 56 | 57 | ## 案例 58 | 59 | 下图显示了使用物化视图模式生成销售摘要。将Azure存储帐户中单独分区中的`Order`,`OrderItem`和`Customer`表中的数据组合在一起生成一个视图,其中包含`Electronics`类别中每个产品的总销售额以及购买的每一个项目的用户的数量。 60 | 61 | ![](https://docs.microsoft.com/en-us/azure/architecture/patterns/_images/materialized-view-summary-diagram.png) 62 | 63 | 创建这个物化视图需要复杂的查询。 但是,通过将查询结果公开为物化视图,用户可以轻松获得结果并直接使用,或将它们合并到另一个查询中。该视图很可能会在报告系统或仪表板中使用,并且可以按计划进行更新,例如每周更新一次。 64 | 65 | >尽管例子中使用Azure表存储,但许多关系数据库管理系统也原生支持物化视图功能。 66 | 67 | ## 相关模式和指南 68 | 69 | 以下模式和指南在实施这种模式时可能是相关的: 70 | 71 | * [数据一致性入门](https://msdn.microsoft.com/library/dn589800.aspx)。物化视图中的摘要信息必须保留,以便反映基础数据值。随着数据值的变化,实时更新汇总数据可能不太实际,相反,不得不采取最终一致的方法。该文章总结围绕分布式数据保持一致性的问题,并介绍了不同一致性模型的优点和折衷。 72 | * [命令和查询责任分离(CQRS)模式](cqrs.md)。用于通过响应基础数据值更改时发生的事件来更新物化视图中的信息。 73 | * [事件溯源模式](event-sourcing.md)。与CQRS模式结合使用以维护物化视图中的信息。当物化视图所基于的数据值发生更改时,系统可以引发描述这些更改的事件并将其保存在事件存储中。 74 | * [索引表模式](index-table.md)。物化视图中的数据通常由主键组织,但查询可能需要通过检查其它字段中的数据来从该视图中检索信息。该模式用于为不支持本机二级索引的数据存储的数据集创建二级索引。 -------------------------------------------------------------------------------- /patterns/pipes-and-filters.md: -------------------------------------------------------------------------------- 1 | # 管道和过滤器模式 2 | 3 | 将执行复杂处理的任务分解为一系列可重用的单独元素。通过允许执行处理的任务元素独立部署和扩展来提高性能,可伸缩性和可重用性。 4 | 5 | ## 背景和问题 6 | 7 | 应用程序需要在处理信息时执行各种复杂性不同的任务。直接但是不灵活的实现应用程序的方式是采用单体模块执行处理。如果在应用程序的其它地方需要部分相同的处理,这种方式可能会减少重构,优化或重用的机会。 8 | 下图说明了使用单体方式处理数据的问题。应用程序接收和处理的数据有两个来源。每个来源的数据由一个单独的模块处理,该模块在将结果传递给应用程序的业务逻辑之前执行一系列的任务来转换数据。 9 | ![](https://docs.microsoft.com/en-us/azure/architecture/patterns/_images/pipes-and-filters-modules.png) 10 | 11 | 单体模块执行的一些任务在功能上非常相似,但模块是分开设计的。实现这些任务的代码耦合在一个模块中,并且在很少或根本没有考虑重用或可伸缩性的情况下开发。 12 | 但是,每个模块执行的处理任务或每个任务的部署要求可能随着业务需求的更新而改变。一些任务可能是计算密集型的,可以在强大的硬件上运行,而其它任务可能不需要这样昂贵的资源。另外,将来可能需要额外的处理,或者处理所执行的任务的顺序可能会改变。需要解决这些问题的解决方案,并增加了代码重用的可能性。 13 | 14 | ## 解决方案 15 | 16 | 将每个流所需的处理分解成一组单独的组件(或过滤器),每个组件执行单个任务。通过标准化每个组件接收和发送的数据的格式,这些过滤器可以组合在一起成为一个流水线。这有助于避免重复代码,并且在处理需求发生变化时可以轻松地删除,替换或集成其它组件。下图展示了使用管道和过滤器实现的解决方案。 17 | 18 | ![https://docs.microsoft.com/en-us/azure/architecture/patterns/_images/pipes-and-filters-solution.png] 19 | 20 | 处理单个请求所花费的时间取决于流水线中最慢的过滤器的速度。一个或多个过滤器可能是瓶颈,特别是如果大量请求出现在来自特定数据源的流中。流水线结构的一个关键优势是它为慢速过滤器的并行运行提供了机会,分散系统负载并提高吞吐量。 21 | 22 | 组成管道的过滤器可以运行在不同的机器上,使其可以独立扩展,并充分利用许多云环境提供的弹性。计算密集型的过滤器可以在高性能硬件上运行,而其他要求不高的过滤器则可以在更便宜的商品硬件上运行。过滤器甚至不必位于相同的数据中心或地理位置,这使得管道中的每个元素都可以在接近所需资源的环境中运行。下图显示了应用于来自Source 1的数据的管道的示例。 23 | ![](https://docs.microsoft.com/en-us/azure/architecture/patterns/_images/pipes-and-filters-load-balancing.png) 24 | 如果过滤器输入和输出以流的方式构造,那么每个滤波器就可以并行执行处理。流水线中的第一个过滤器可以开始工作并输出结果,在第一个过滤器完成工作之前,结果直接顺序的传递下一个过滤器。 25 | 另一个好处是这个模型可以提供弹性。如果过滤器发生故障或者运行的机器不再可用,则管道可以重新安排过滤器正在执行的工作,并将此工作指向组件的另一个实例。单个过滤器的故障不一定会导致整个管道的故障。 26 | 将管道和过滤器模式与补偿交易模式结合使用是另一种实现分布式事务的方法。分布式事务可以分解为单独的,可补偿的任务,每个任务都可以通过使用实现补偿事务模式的过滤器来实现。流水线中的过滤器可以作为独立的托管任务,在靠近它们维护的数据的地方运行。 27 | 28 | ### 问题和注意事项 29 | 30 | 在决定如何实现这种模式时应该考虑以下几点: 31 | * **复杂性**。这种模式增加了灵活性但同时也带来复杂性,特别是如果管道中的过滤器分布在不同的服务器上。 32 | * **可靠性**。使用可确保流水线中的过滤器之间的数据流不会丢失的基础设施。 33 | * **幂等**。如果管道中的过滤器在收到消息后失败,并且重新安排工作到过滤器的另一个实例,则部分工作可能已经完成。如果这项工作更新了全局状态的某些方面(例如存储在数据库中的信息),则可以重复相同的更新。如果过滤器在将结果发布到管道中的下一个过滤器之后但在指示过程成功完成之前失败,可能会发生类似的问题。在这些情况下,相同的工作可以通过过滤器的另一个实例重复,导致相同的结果发布两次。可能导致流水线中的后续过滤器重复处理相同的数据。因此,管道中的过滤器应设计为幂等。更多相关内容,请参阅Jonathan Oliver的博客上的[幂等模式](http://blog.jonathanoliver.com/idempotency-patterns/)。 34 | * **重复消息**。如果管道中的过滤器在将消息发布到管道的下一个阶段后失败,则可能会运行另一个过滤器实例,将相同消息的副本发布到管道。这可能导致相同消息的两个实例传递到下一个过滤器。为了避免这种情况,管道应检测并消除重复的消息。 35 | > 如果使用消息队列(如Microsoft Azure服务总线队列)来实现管道,则消息队列基础结构可能会提供自动重复消息检测和删除功能。 36 | * **上下文和状态**。在一个流水线中,每个过滤器本质上都是独立运行的,不应该假设如何调用它。这意味着每个过滤器都应有足够的上下文来执行其工作。这个上下文可能包含大量的状态信息。 37 | 38 | ## 何时使用该模式 39 | 40 | 在以下场景使用此模式: 41 | * 应用程序所需的处理可以很容易地分解为一系列独立的步骤。 42 | * 应用程序执行的处理步骤具有不同的可伸缩性要求。 43 | >可以在同一个过程(进程?)中将过滤器分组在一起伸缩。更多相关内容,请参阅[计算资源合并模式](https://docs.microsoft.com/en-us/azure/architecture/patterns/compute-resource-consolidation)。 44 | * 需要重新排序由应用程序执行的处理步骤的灵活性,或者添加和移除步骤的能力。 45 | * 系统可以通过分散跨不同服务器的步骤的处理而受益。 46 | * 需要一个可靠的解决方案,以最大限度地减少数据正在处理中的一个步骤中的故障造成的影响。 47 | 48 | 此模式可能不适用一下场景: 49 | * 应用程序执行的处理步骤不是独立的,或者它们必须作为同一事务的一部分一起执行。 50 | * 步骤所需的上下文或状态信息量使得这种方法效率低下。可以将状态信息持久化到数据库,但是如果数据库上的额外负载导致过度争用,则不要使用此策略。 51 | 52 | ## 案例 53 | 可以使用一系列消息队列来提供实现管道所需的基础架构。初始消息队列接收未处理的消息。做为过滤器任务的组件在此队列上侦听消息,执行工作,然后将已转换的消息发布到序列中的下一个队列中。另一个过滤器任务可以侦听并处理此队列上的消息,将结果发布到另一个队列中,依此类推,直到完全转换的数据出现在队列中的最终消息中。下图说明了如何使用消息队列来实现流水线。 54 | ![](https://docs.microsoft.com/en-us/azure/architecture/patterns/_images/pipes-and-filters-message-queues.png) 55 | 如果在Azure上构建解决方案,则可以使用`Service Bus`队列服务来提供可靠且可扩展的排队机制。下面的C#代码中的`ServiceBusPipeFilter`类展示了如何实现从队列接收输入消息,处理消息并将结果发布到另一个队列的过滤器。 56 | > `ServiceBusPipeFilter`类在GitHub上的[PipesAndFilters.Shared项目](https://github.com/mspnp/cloud-design-patterns/tree/master/pipes-and-filters)中找到。 57 | 58 | ```c# 59 | public class ServiceBusPipeFilter 60 | { 61 | ... 62 | private readonly string inQueuePath; 63 | private readonly string outQueuePath; 64 | ... 65 | private QueueClient inQueue; 66 | private QueueClient outQueue; 67 | ... 68 | 69 | public ServiceBusPipeFilter(..., string inQueuePath, string outQueuePath = null) 70 | { 71 | ... 72 | this.inQueuePath = inQueuePath; 73 | this.outQueuePath = outQueuePath; 74 | } 75 | 76 | public void Start() 77 | { 78 | ... 79 | // Create the outbound filter queue if it doesn't exist. 80 | ... 81 | this.outQueue = QueueClient.CreateFromConnectionString(...); 82 | 83 | ... 84 | // Create the inbound and outbound queue clients. 85 | this.inQueue = QueueClient.CreateFromConnectionString(...); 86 | } 87 | 88 | public void OnPipeFilterMessageAsync( 89 | Func> asyncFilterTask, ...) 90 | { 91 | ... 92 | 93 | this.inQueue.OnMessageAsync( 94 | async (msg) => 95 | { 96 | ... 97 | // Process the filter and send the output to the 98 | // next queue in the pipeline. 99 | var outMessage = await asyncFilterTask(msg); 100 | 101 | // Send the message from the filter processor 102 | // to the next queue in the pipeline. 103 | if (outQueue != null) 104 | { 105 | await outQueue.SendAsync(outMessage); 106 | } 107 | 108 | // Note: There's a chance that the same message could be sent twice 109 | // or that a message gets processed by an upstream or downstream 110 | // filter at the same time. 111 | // This would happen in a situation where processing of a message was 112 | // completed, it was sent to the next pipe/queue, and then failed 113 | // to complete when using the PeekLock method. 114 | // Idempotent message processing and concurrency should be considered 115 | // in a real-world implementation. 116 | }, 117 | options); 118 | } 119 | 120 | public async Task Close(TimeSpan timespan) 121 | { 122 | // Pause the processing threads. 123 | this.pauseProcessingEvent.Reset(); 124 | 125 | // There's no clean approach for waiting for the threads to complete 126 | // the processing. This example simply stops any new processing, waits 127 | // for the existing thread to complete, then closes the message pump 128 | // and finally returns. 129 | Thread.Sleep(timespan); 130 | 131 | this.inQueue.Close(); 132 | ... 133 | } 134 | 135 | ... 136 | } 137 | ``` 138 | The following code shows an Azure worker role named PipeFilterARoleEntry, defined in the PipeFilterA project in the sample solution. 139 | `ServiceBusPipeFilter`类中的`Start`方法连接到一对输入和输出队列,`Close`方法断开与输入队列的连接。`OnPipeFilterMessageAsync`方法实际处理消息,该方法的`asyncFilterTask`参数指定要执行的处理。 `OnPipeFilterMessageAsync`方法等待输入队列中的传入消息,在每个消息到达时运行由`asyncFilterTask`参数指定代码,并将结果发布到输出队列。队列本身是由构造函数指定的。 140 | 案例解决方案使用一组角色实现过滤器。每个辅助角色都可以独立扩展,具体取决于其执行的业务处理的复杂程度或处理所需的资源。另外,可以并行运行每个辅助角色的多个实例以提高吞吐量。 141 | 以下代码显示了示例解决方案中`PipeFilterA`项目定义的名为`PipeFilterARoleEntry`的Azure辅助角色。 142 | 143 | ```c# 144 | public class PipeFilterARoleEntry : RoleEntryPoint 145 | { 146 | ... 147 | private ServiceBusPipeFilter pipeFilterA; 148 | 149 | public override bool OnStart() 150 | { 151 | ... 152 | this.pipeFilterA = new ServiceBusPipeFilter( 153 | ..., 154 | Constants.QueueAPath, 155 | Constants.QueueBPath); 156 | 157 | this.pipeFilterA.Start(); 158 | ... 159 | } 160 | 161 | public override void Run() 162 | { 163 | this.pipeFilterA.OnPipeFilterMessageAsync(async (msg) => 164 | { 165 | // Clone the message and update it. 166 | // Properties set by the broker (Deliver count, enqueue time, ...) 167 | // aren't cloned and must be copied over if required. 168 | var newMsg = msg.Clone(); 169 | 170 | await Task.Delay(500); // DOING WORK 171 | 172 | Trace.TraceInformation("Filter A processed message:{0} at {1}", 173 | msg.MessageId, DateTime.UtcNow); 174 | 175 | newMsg.Properties.Add(Constants.FilterAMessageKey, "Complete"); 176 | 177 | return newMsg; 178 | }); 179 | 180 | ... 181 | } 182 | 183 | ... 184 | } 185 | ``` 186 | 该角色包含一个`ServiceBusPipeFilter`对象。角色中的`OnStart`方法连接到接收输入消息和发布输出消息的队列(队列的名称在`Constants`类中定义)。Run方法调用`OnPipeFilterMessagesAsync`方法对接收到的每条消息执行一些处理(在本例中,处理是通过等待一段时间来模拟的)。处理完成后,构造一个包含结果的新消息(在这种情况下,输入消息具有添加的定制属性),并将此消息发布到输出队列。 187 | 示例代码在`PipeFilterB`项目中包含另一个名为`PipeFilterBRoleEntry`的辅助角色。除了在`Run`方法中执行不同的处理外,该角色与`PipeFilterARoleEntry`相似。示例解决方案中将这两个角色组合起来构建一个管道,`PipeFilterARoleEntry`角色的输出队列是`PipeFilterBRoleEntry`角色的输入队列。 188 | 该示例解决方案还提供了两个名为`InitialSenderRoleEntry`(在`InitialSender`项目中)和`FinalReceiverRoleEntry`(在`FinalReceiver`项目中)的其它角色。`InitialSenderRoleEntry`角色在管道中提供初始消息。`OnStart`方法连接到单个队列,`Run`方法将一个方法发送到此队列。此队列是`PipeFilterARoleEntry`角色使用的输入队列,因此向其发送消息会导致消息被`PipeFilterARoleEntry`角色接收和处理。处理的消息之后通过`PipeFilterBRoleEntry`角色。 189 | `FinalReceiveRoleEntry`角色的输入队列是`PipeFilterBRoleEntry`角色的输出队列。`FinalReceiveRoleEntry`角色中的Run方法(如下所示)接收消息并执行一些最终处理。然后,将管道中的过滤器添加的自定义属性的值写入跟踪输出。 190 | ```c# 191 | public class FinalReceiverRoleEntry : RoleEntryPoint 192 | { 193 | ... 194 | // Final queue/pipe in the pipeline to process data from. 195 | private ServiceBusPipeFilter queueFinal; 196 | 197 | public override bool OnStart() 198 | { 199 | ... 200 | // Set up the queue. 201 | this.queueFinal = new ServiceBusPipeFilter(...,Constants.QueueFinalPath); 202 | this.queueFinal.Start(); 203 | ... 204 | } 205 | 206 | public override void Run() 207 | { 208 | this.queueFinal.OnPipeFilterMessageAsync( 209 | async (msg) => 210 | { 211 | await Task.Delay(500); // DOING WORK 212 | 213 | // The pipeline message was received. 214 | Trace.TraceInformation( 215 | "Pipeline Message Complete - FilterA:{0} FilterB:{1}", 216 | msg.Properties[Constants.FilterAMessageKey], 217 | msg.Properties[Constants.FilterBMessageKey]); 218 | 219 | return null; 220 | }); 221 | ... 222 | } 223 | 224 | ... 225 | } 226 | ``` 227 | 228 | ## 相关模式和指南 229 | 以下模式和指南在实现此模式时很有用: 230 | * 此模式的示例在[GitHub上](https://github.com/mspnp/cloud-design-patterns/tree/master/pipes-and-filters)可以找到。 231 | * [竞争消费者模式](competing-consumers.html)。一个管道可以包含一个或多个过滤器的多个实例。这种方法对于慢速过滤器的并行运行实例非常有用,使系统能够分散负载并提高吞吐量。过滤器的每个实例将与其它实例竞争输入,过滤器的两个实例不应该处理相同的数据。链接中的模式提供这种方法的解释。 232 | * [计算资源合并模式](compute-resource-consolidation.html)。将可以一起放大到相同的过程中的过滤器分组也是可能的。链接提供了更多有关此策略的优点和折衷的信息。 233 | * [补偿交易模式](https://docs.microsoft.com/en-us/azure/architecture/patterns/compensating-transaction)。过滤器可以实现为可以反转的操作,或者具有补偿操作,以在发生故障的情况下将状态恢复到先前的版本。链接中的模式解释如何实现这一点,以保持或实现最终的一致性。 234 | * Jonathan Oliver的博客上介绍的[幂等模式](http://blog.jonathanoliver.com/idempotency-patterns/)。 -------------------------------------------------------------------------------- /patterns/priority-queue.md: -------------------------------------------------------------------------------- 1 | # 优先级队列模式 2 | 3 | 对发送给服务的请求进行优先级排序,以便更快地接收和处理具有更高优先级的请求。这种模式在为各个客户提供不同服务级别保证的应用程序中非常有用。 4 | 5 | ## 问题和背景 6 | 7 | 应用程序可以将特定任务委托给其它服务,例如执行后台处理或与其它应用程序或服务集成。在云中,消息队列通常用于将任务委托给后台处理。在许多情况下,订单请求被服务接收并不重要。 但在某些情况下,有必要优先考虑具体的要求。这些请求应该早于由应用程序先前发送的低优先级请求处理。 8 | 9 | ## 解决方案 10 | 11 | 队列通常是先入先出(FIFO)结构,而消费者通常按照发送到队列的相同顺序接收消息。但是,某些消息队列支持优先消息。发布消息的应用程序可以分配优先级,队列中的消息将自动重新排序,以便先收到优先级更高的消息。下图说明了具有优先消息的队列。 12 | 13 | ![](https://docs.microsoft.com/en-us/azure/architecture/patterns/_images/priority-queue-pattern.png) 14 | 15 | >大多数消息队列实现支持多个消费者(遵循[竞争消费者模式](competing-consumers.html)),并且消费者进程的数量可以根据需求而放大或缩小。 16 | 17 | 18 | 在不支持基于优先级的消息队列的系统中,另一种解决方案是为每个优先级维护一个单独的队列。应用程序负责将消息发布到适当的队列。每个队列可以有一个单独的消费者池。较高优先级的队列可以拥有比较低优先级的队列运行速度更快的硬件。下图说明了为每个优先级使用单独的消息队列。 19 | 20 | ![](https://docs.microsoft.com/en-us/azure/architecture/patterns/_images/priority-queue-separate.png) 21 | 22 | 这种策略的一个变种就是拥有单个消费者池,先检查高优先级队列中的消息,然后才开始从低优先级队列中获取消息。在使用单个消费者进程池的解决方案(具有支持不同优先级消息的单个队列或者多个处理单个优先级消息的队列)之间存在一些语义差异,以及使用多个队列,每个队列都有一个单独的池。 23 | 24 | 在单池方法中,优先级较高的消息总是在较低优先级的消息之前接收和处理。从理论上讲,优先级非常低的消息可能会不断被取代,永远不会被处理。在多池方法中,总会处理较低优先级的消息,只是速度不如较高优先级的消息(取决于池的相对大小和可用资源)。 25 | 26 | 使用优先级排队机制有如下的好处: 27 | * 允许应用程序满足业务要求,要求优先考虑可用性或性能,例如为特定的客户群提供不同级别的服务。 28 | * 这可以帮助降低运维成本。单一队列方法可以根据需要缩减使用者的数量。高优先级的消息仍然会首先处理(尽管可能更慢),低优先级的消息可能会延迟更长的时间。如果已经为每个队列实施了具有不同使用者池的多个消息队列方法,则可以减少较低优先级队列的使用者池,甚至可以暂停处理一些超低优先级队列,方法是停止所有使用者监听这些队列上的消息。 29 | * 多消息队列方法可以通过根据处理需求对消息进行分区来最大化应用程序性能和可伸缩性。例如,重要的任务可以优先处理,由接受者立即运行,而不重要的后台任务可以在不太繁忙的时间运行的接收者处理。 30 | 31 | ## 问题和注意事项 32 | 33 | 在决定如何实现该模式时,请考虑以下几点: 34 | 35 | 在解决方案的上下文中定义优先级。例如,高优先级可能意味着消息应该在十秒内处理。确定处理高优先级项目的要求,以及为满足这些标准而分配的其它资源。 36 | 37 | 确定是否所有高优先级的项目必须在低优先级项目之前处理。如果只有单个消费者池处理消息,则必须提供一种机制,以便在优先级较高的消息可用时抢占并暂停处理低优先级消息的任务。 38 | 39 | 在多队列方法中,使用单个消费者进程池(每个队列监听所有队列而不是专用消费者池)时,消费者必须应用一种算法,以确保其始终为优先处理高优先级队列消息。 40 | 41 | 监控高优先级队列和低优先级队列的处理速度,以确保这些队列中的消息按预期的速率处理。 42 | 43 | 如果需要保证处理低优先级的消息,那么有必要实现具有多个消费者池的多消息队列方法。或者,在支持消息优先级的队列中,可以动态增加排队消息在老化时的优先级。但是,这种方法取决于提供此功能的消息队列。 44 | 45 | 为每个消息使用单独的队列优先级对于具有少量定义明确的优先级的系统来说效果最佳。 46 | 47 | 消息优先级可以由系统逻辑确定。例如,它们可以被指定为“付费客户”或“非付费客户”,而不是具有明确的高优先级和低优先级消息。根据业务模型,系统可以分配更多资源处理来自付费客户的消息。 48 | 49 | 可能存在与检查消息队列相关联的金融和处理成本(一些商业消息传递系统每次发布或检索消息,并且每次查询消息的队列时收取少量费用)。此成本在检查多个队列时会增加。 50 | 可以根据正在服务的队列的长度动态调整消费者池的大小。更多信息请参阅[自动缩放指南](https://msdn.microsoft.com/library/dn589774.aspx)。 51 | 52 | ## 何时使用该模式 53 | 54 | 该模式适用于以下场景: 55 | 56 | * 系统必须处理具有不同优先级的多个任务。 57 | * 不同的用户或租户应该享有不同的优先权。 58 | 59 | ## 案例 60 | 61 | Microsoft Azure不原生支持通过排序自动确定消息的优先级的排队机制。但是,它确实提供了支持提供消息过滤的排队机制的Azure服务总线主题和订阅,以及广泛的灵活功能,使其成为大多数优先级队列实现的理想选择。 62 | 63 | Azure解决方案可以实现应用程序可以将消息发布到的服务总线主题,方式与队列相同。消息可以包含应用程序定义的自定义属性形式的元数据。服务总线订阅可以与该主题相关联,并且这些订阅可以基于消息的属性来过滤消息。当应用程序向主题发送消息时,消息被定向到适当的订阅,消费者可以读取消息。消费者进程可以使用与消息队列相同的语义从订阅中检索消息(订阅是逻辑队列)。下图说明了使用Azure Service Bus主题和订阅实现优先级队列。 64 | 65 | ![](https://docs.microsoft.com/en-us/azure/architecture/patterns/_images/priority-queue-service-bus.png) 66 | 67 | 在上图中,应用程序创建了几个消息,并在每个消息中分配一个名为`Priority`的自定义属性,其值为`High`或`Low`。应用程序将这些消息发布到一个主题。该主题有两个关联的订阅,通过检查`Priority`属性过滤邮件。一个订阅接受`Priority`属性设置为`High`的消息,另一个接受`Priority`属性设置为`Low`的消息。消费者池从每个订阅中读取消息。高优先级的订阅具有更大的池,并且这些消费者可以运行在比低优先级池中的消费者更多的可用资源的更强大的计算机上。 68 | 69 | 请注意,在这个例子中,没有什么特别的地方可以指定高优先级和低优先级的消息。它们只是在每条消息中指定为属性的标签,用于将消息定向到特定的订阅。如果需要额外的优先级,创建更多的订阅和消费者进程池来处理这些优先级相对容易。 70 | 71 | [GitHub上](https://github.com/mspnp/cloud-design-patterns/tree/master/priority-queue)提供的`PriorityQueue`解决方案包含了这种方法的实现。该解决方案包含两个名为`PriorityQueue.High`和`PriorityQueue.Low`的工作角色项目。这些角色继承自`PriorityWorkerRole`类,包含`OnStart`方法中用于连接到指定订阅的功能。 72 | 73 | `PriorityQueue.High`和`PriorityQueue.Low` 工作者角色连接到由其配置定义的不同订阅。管理员可以配置不同数量的角色来运行。通常情况下,`PriorityQueue.High`工作者角色的实例将比`PriorityQueue.Low`工作者角色更多。 74 | 75 | `PriorityWorkerRole`类中的`Run`方法安排在队列中接收的每个消息都运行虚拟`ProcessMessage`方法(也在`PriorityWorkerRole`类中定义)。以下代码显示了`Run`和`ProcessMessage`方法。`QueueManager`类在`PriorityQueue.Shared`项目中定义,它提供了使用Azure服务总线队列的辅助方法。 76 | ```c# 77 | public class PriorityWorkerRole : RoleEntryPoint 78 | { 79 | private QueueManager queueManager; 80 | ... 81 | 82 | public override void Run() 83 | { 84 | // Start listening for messages on the subscription. 85 | var subscriptionName = CloudConfigurationManager.GetSetting("SubscriptionName"); 86 | this.queueManager.ReceiveMessages(subscriptionName, this.ProcessMessage); 87 | ...; 88 | } 89 | ... 90 | 91 | protected virtual async Task ProcessMessage(BrokeredMessage message) 92 | { 93 | // Simulating processing. 94 | await Task.Delay(TimeSpan.FromSeconds(2)); 95 | } 96 | } 97 | ``` 98 | `PriorityQueue.High`和`PriorityQueue.Low`工作角色都覆盖了`ProcessMessage`方法的默认功能。下面的代码显示了`PriorityQueue.High`worker角色的`ProcessMessage`方法。 99 | ```C# 100 | protected override async Task ProcessMessage(BrokeredMessage message) 101 | { 102 | // Simulate message processing for High priority messages. 103 | await base.ProcessMessage(message); 104 | Trace.TraceInformation("High priority message processed by " + 105 | RoleEnvironment.CurrentRoleInstance.Id + " MessageId: " + message.MessageId); 106 | } 107 | ``` 108 | 当应用程序将消息发送到与`PriorityQueue.High`和`PriorityQueue.Low`工作程序角色所使用的预订相关联的主题时,它会使用`Priority`自定义属性指定优先级,如下面的代码所示。代码(在`PriorityQueue.Sender`项目的`WorkerRole`类中实现)使用`QueueManager`类的`SendBatchAsync`帮助器方法批量发送消息到主题。 109 | ```C# 110 | // Send a low priority batch. 111 | var lowMessages = new List(); 112 | 113 | for (int i = 0; i < 10; i++) 114 | { 115 | var message = new BrokeredMessage() { MessageId = Guid.NewGuid().ToString() }; 116 | message.Properties["Priority"] = Priority.Low; 117 | lowMessages.Add(message); 118 | } 119 | 120 | this.queueManager.SendBatchAsync(lowMessages).Wait(); 121 | ... 122 | 123 | // Send a high priority batch. 124 | var highMessages = new List(); 125 | 126 | for (int i = 0; i < 10; i++) 127 | { 128 | var message = new BrokeredMessage() { MessageId = Guid.NewGuid().ToString() }; 129 | message.Properties["Priority"] = Priority.High; 130 | highMessages.Add(message); 131 | } 132 | 133 | this.queueManager.SendBatchAsync(highMessages).Wait(); 134 | ``` 135 | 136 | ## 相关模式和指南 137 | 138 | 以下模式和指南在实现此模式时很有用: 139 | * 此模式的案例代码可以在[GitHub](https://github.com/mspnp/cloud-design-patterns/tree/master/priority-queue)上下载。 140 | * [异步消息入门](https://msdn.microsoft.com/library/dn589781.aspx)。处理请求的消费者服务可能需要向发布请求的应用程序实例发送回复。该指南提供了有关可用于实施请求/响应消息传递的策略的信息。 141 | * [竞争消费者模式](consumer-competing.md)。为了增加队列的吞吐量,可以让多个使用者在同一队列上侦听,并且并行处理任务。这些消费者将争夺消息,但只有一个消费者能够处理每个消息。该指南提供了更多关于实施这种方法的好处和权衡的信息。 142 | * [限流模式](throttling.md)。可以通过使用队列来实现限流。高优先级消息可用于确保来自关键应用程序或高价值客户运行的应用程序的请求优先运行。 143 | * [自动缩放指南](https://msdn.microsoft.com/library/dn589774.aspx)。根据队列的长度,可以扩展处理队列的消费者进程池的大小。此策略可以帮助提高性能,特别是处理高优先级消息的池。 144 | * Abhishek Lal的博客上关于[服务总线的企业集成模式](http://abhishekrlal.com/2013/01/11/enterprise-integration-patterns-with-service-bus-part-2/) 145 | -------------------------------------------------------------------------------- /patterns/queue-based-load-leveling.md: -------------------------------------------------------------------------------- 1 | # 基于队列的负载均衡模式 2 | 3 | 使用一个队列,充当任务和它调用的服务之间的缓冲区,以平滑间歇性的重负载导致的服务失败或任务超时。这可以最大限度地帮助减少需求峰值对任务和服务的可用性和响应性的影响。 4 | 5 | ## 背景和问题 6 | 7 | 云中的许多解决方案都涉及运行调用服务的任务。在这种环境下,如果某个服务遭受间歇性的重负载,可能会导致性能或可靠性问题。 8 | 服务可以与使用它的任务是同一解决方案的一部分,也可以是提供对常用资源(如缓存或存储服务)的访问的第三方服务。 如果同时运行多个任务使用相同的服务,则可能难以随时预测对服务的请求量。 9 | 服务可能会遇到需求高峰导致其过载,无法及时响应请求。如果无法处理这些请求引起的争用,大量并发请求也可能导致服务失败。 10 | 11 | ## 解决方案 12 | 13 | 重构解决方案并在任务和服务之间引入队列。任务和服务异步运行。任务将包含服务所需数据的消息发送到队列。队列充当缓冲区,存储消息,直到被服务检索到。该服务从队列中检索并处理消息。 14 | 以高度可变的速率生成的许多任务的请求,可以通过相同的消息队列传递给服务。下图显示使用队列来平衡服务上的负载。 15 | ![](https://docs.microsoft.com/en-us/azure/architecture/patterns/_images/queue-based-load-leveling-pattern.png) 16 | 17 | 队列将任务从服务中分离出来,服务可以按照自己的速度处理消息,而不管并发任务的请求量如何。此外,如果服务在将消息发送到队列时不可用,则不会有任何延迟。 18 | 19 | 这种模式提供了以下好处: 20 | * 它可以帮助最大限度地提高可用性,因为在服务中产生的延迟不会对应用程序产生即时和直接的影响,即使服务不可用或者当前没有处理消息,应用程序也可以继续发送消息到队列中。 21 | * 它可以帮助最大限度地提高可扩展性,因为队列和服务数量都可以根据需求而变化。 22 | * 它可以帮助控制成本,因为部署的服务实例数量必须足以满足平均负载而不是峰值负载。 23 | > 当需求达到系统可能失败的阈值时,一些服务会引入限流。限流可能会降级可用的功能。可以使用这些服务实现负载均衡,以确保达不到此阈值。 24 | 25 | ## 问题和注意事项 26 | 27 | 在决定如何实现这种模式时,请考虑以下几点: 28 | * 有必要实现应用程序逻辑来控制服务处理消息的速率,以避免超出目标资源。避免将需求传递到系统的下一个阶段。对系统进行负载测试,确保它提供所需的负载水平,并调整队列数量和处理消息的服务实例的数量。 29 | * 消息队列是一种单向通信机制。如果任务期望从服务中得到答复,则可能需要实现服务可以用来发送答复的机制。更多相关内容,请参阅[异步消息入门](https://msdn.microsoft.com/library/dn589781.aspx)。 30 | * 如果将自动伸缩应用于正在侦听队列上的请求的服务,请小心。这可能会导致对这些服务共享的资源争用的增加,并降低使用队列来平衡负载的有效性。 31 | 32 | ## 何时使用该模式 33 | 34 | 这种模式对于任何引发过载服务的应用程序都很有用。 35 | 如果应用程序期望延迟最小的服务响应,则此模式不适用。 36 | 37 | ## 案例 38 | Microsoft Azure Web角色使用单独的存储服务存储数据。如果Web角色的大量实例并发运行,则存储服务可能无法快速响应请求,以防止这些请求超时或失败。下图强调了一个服务被来自Web角色实例的大量并发请求所淹没。 39 | ![](https://docs.microsoft.com/en-us/azure/architecture/patterns/_images/queue-based-load-leveling-overwhelmed.png) 40 | 41 | 要解决这个问题,可以使用队列来平衡Web角色实例和存储服务之间的负载。但是,存储服务旨在接受同步请求,不能轻易修改以读取消息和管理吞吐量。可以引入一个辅助角色作为代理服务,接收来自队列的请求并将其转发给存储服务。 worker角色中的应用程序逻辑可以控制将请求传递到存储服务的速度,以防止存储服务被淹没。下图说明了一个队列和一个工作者角色来平衡Web角色和服务的实例之间的负载。 42 | ![](https://docs.microsoft.com/en-us/azure/architecture/patterns/_images/queue-based-load-leveling-worker-role.png) 43 | 44 | ## 相关的模式和指南 45 | 46 | 以下模式和指南在实施这种模式时可能是相关的: 47 | 48 | * [异步消息入门](https://msdn.microsoft.com/library/dn589781.aspx)。消息队列本质上是异步的。如果从一个服务直接与一个服务通信改为使用一个消息队列,那么可能有必要重新设计一个任务中的应用程序逻辑。同样,可能需要重构服务以接受来自消息队列的请求。或者,也可以如案例中所述实现代理服务。 49 | * [竞争消费者模式](competing-consumers.md)。运行一个服务的多个实例是可能的,每个实例都作为负载均衡队列中的消息使用者。可以使用此方法来调整接收邮件并将其传递给服务的速率。 50 | * [限流模式](throttling.md)。服务实施限流的一个简单方法是使用基于队列的负载均衡,并通过消息队列将所有请求路由到服务。该服务可以确保所需的资源没有用尽的速率来处理请求,并且减少可能发生的争用的量。 51 | * [队列服务概念](https://msdn.microsoft.com/library/azure/dd179353.aspx)。介绍了有关在Azure应用程序中选择消息传递和排队机制的内容。 -------------------------------------------------------------------------------- /patterns/retry.md: -------------------------------------------------------------------------------- 1 | # 重试模式 2 | 3 | 通过透明地重试失败的操作,让应用程序可以在尝试连接到服务或网络资源时处理出现的瞬态故障。这可以提高应用程序的稳定性。 4 | 5 | ## 背景和问题 6 | 7 | 与云中运行的元素进行通信的应用程序必须对环境中可能发生的瞬态故障敏感。故障包括与组件和服务的网络连接的瞬时丢失,服务的暂时不可用或服务繁忙时发生的超时。 8 | 这些故障通常可以自我纠正,如果触发故障的动作在适当的延迟之后重复,则可能会成功。例如,处理大量并发请求的数据库服务可以实现一种限制策略,临时拒绝任何进一步的请求,直到其工作负载松动。尝试访问数据库的应用程序可能无法连接,但如果在延迟后再次尝试可能会成功。 9 | 10 | ## 解决方案 11 | 12 | 在云中,瞬态故障并不罕见,应用程序应应通过优雅而透明地设计处理。最大限度地减少故障对应用程序执行的业务任务的影响。 13 | 14 | 如果应用程序在尝试向远程服务发送请求时检测到故障,则可以使用以下策略来处理该故障: 15 | * **取消**。如果故障不是暂时的,或者即便重复也不太可能成功,应用程序应该取消操作并报告异常。例如,由于无效凭据而导致的身份验证失败无论尝试多少次,都不可能成功。 16 | * **重试**。如果报告的特定故障不常见,可能是由于网络数据包在发送时被破坏的异常情况引起的。在这种情况下,应用程序可以立即重试失败的请求,因为同样的错误不太可能重复,并且请求可能会成功。 17 | * **延迟后重试**。如果故障是由更常见的连接或繁忙导致,则网络或服务可能需要较短的时间,来更正连接问题或清除累积的工作负载。应用程序应等待适当的时间才能重试请求。 18 | 对于更常见的瞬态故障,应该选择重试操作之间的时间段,以便从应用程序的多个实例中均匀地传播请求。这降低了繁忙的服务继续超载的可能。如果应用程序的许多实例不断地覆盖具有重试请求的服务,则会延长服务恢复时间。 19 | 20 | 如果请求依然失败,应用程序可以等待并再次尝试。如有必要,可以在重试尝试之间随着延迟的增加重复该过程,直到达到请求数量的最大值。延迟可以逐渐增加或指数地增加,这取决于故障类型和在此期间将更正的概率。 21 | 下图说明了使用此模式调用托管服务中的操作。如果请求在预定义的尝试次数后失败,应用程序应将故障视为异常并做相应处理。 22 | ![](https://docs.microsoft.com/en-us/azure/architecture/patterns/_images/retry-pattern.png) 23 | 24 | 应用程序应该包含所有尝试访问远程服务的代码,实现与之前列出的策略匹配的重试策略。发送到不同服务的请求可能会受到不同的策略的约束。一些供应商提供实现重试策略的类库,应用程序可以指定最大重试次数,重试尝试之间的时间以及其它参数。 25 | 应用程序应记录故障和故障操作的详细信息。此信息对操作者很有用。如果服务频繁不可用或繁忙,往往是因为服务已耗尽资源。可以通过扩展服务来减少这些故障的频率。例如,如果数据库服务不断重载,对数据库分区并在多个服务器分散负载可能是有帮助。 26 | >[Microsoft Entity Framework](https://docs.microsoft.com/ef/)框架提供了重试数据库操作的功能。此外,大多数Azure服务和客户端SDK都包含重试机制。详细信息请参阅[特定服务的重试指南](https://docs.microsoft.com/en-us/azure/architecture/best-practices/retry-service-specific)。 27 | 28 | ## 问题和注意事项 29 | 30 | 在决定如何实现此模式时应该考虑以下几点。 31 | 根据应用程序的业务需求和故障的性质重新调整重试策略。对于某些非关键操作,最好是快速失败,而不是重试几次,影响应用程序的吞吐量。例如,在访问远程服务的交互式Web应用程序中,最好在延迟时间较短的少量重试后就宣告失败,并向用户显示合适的提示消息(例如,“请稍后再试” )。对于批处理应用程序,可能更适合增加重试次数,并在尝试延迟时间以指数增长。 32 | 激进的重试策略,包含尝试之间的最小延迟和大量重试时间间隔,可能会导致正在靠近或处于容量极限的繁忙服务进一步降级。如果应用程序不断尝试执行失败操作,重试策略也可能会影响应用程序的响应。 33 | 如果一个请求在大量重试后仍然失败,那么应用程序最好防止更多请求使用相同的资源,并且立即报告失败。到期时,应用程序可以暂时允许一个或多个请求,以查看它们是否成功。有关此策略的更多详细信息,请参阅[断路器模式](circuit-breaker.html)。 34 | 考虑操作是否是幂等的。如果是这样,重试本来就是安全的。否则,重试可能导致操作不止一次执行,引发意外的副作用。例如,服务可能会收到请求并成功处理,但无法发送响应。这时,假设没有收到第一个请求,重试逻辑可能会重新发送请求。 35 | 根据故障的性质,对服务的请求可能因各种原因导致不同的异常而失败。一些异常可以快速解决,而其它异常的故障持续时间较长。重试策略可以根据异常的类型调整重试之间的时间。 36 | 考虑如何重试操作,作为事务的一部分将影响事务整体一致性。微调事务操作的重试策略,最大限度地发挥成功的机会,并减少撤销所有事务步骤的需要。 37 | 确保所有重试代码都针对各种故障条件进行了全面测试。检查它不会严重影响应用程序的性能或可靠性,导致服务和资源过载,或产生竞争条件或瓶颈。 38 | 仅在理解失败操作的完整上下文的情况下才实现重试逻辑。例如,如果包含重试策略的任务调用还包含重试策略的另一个任务,则这个额外的重试层增加了处理的延迟时间。最好将低级别任务配置为快速失败,并将失败的原因报告给调用它的任务。更高级别的任务可以根据自己的策略来处理故障。 39 | 记录导致重试的所有连接失败非常重要,方便识别应用程序,服务或资源的基础问题。 40 | 调查服务或资源最有可能发生的故障,以发现其是否持久或终结。如果是这样,最好把错误当作异常。应用程序可以报告或记录异常,然后尝试通过调用替代服务(如果可用)或通过提供降级的功能来继续。有关如何检测和处理长时间故障的更多信息,请参阅[断路器模式](circuit-breaker.html)。 41 | ## 何时使用该模式 42 | 43 | 在应用程序与远程服务交互或访问远程资源时可能会遇到瞬态故障时,使用此模式。 这些故障持续时间不长,并且重复先前失败的请求可能会在以后的尝试中成功。 44 | 45 | 此模式可能不太适用以下场景: 46 | 47 | * 故障比较持久,可能会影响应用程序的响应。重复尝试可能失败的请求,对于应用程序来说是浪费时间和资源。 48 | * 用于处理非瞬态故障导致的失败,例如由应用程序的业务逻辑中的错误引起的内部异常。 49 | * 作为解决系统中可扩展性问题的替代方法。如果应用程序遇到频繁的忙碌故障,这表明应当垂直扩展正在访问的服务或资源。 50 | 51 | ## 案例 52 | 53 | 这个C#的例子代码说明了如何实现重试模式。`OperationWithBasicRetryAsync`方法(如下所示)通过`TransientOperationAsync`方法异步调用外部服务。`TransientOperationAsync`方法的实现细节和服务相关,并从示例代码中省略。 54 | 55 | ```java 56 | private int retryCount = 3; 57 | private readonly TimeSpan delay = TimeSpan.FromSeconds(5); 58 | 59 | public async Task OperationWithBasicRetryAsync() 60 | { 61 | int currentRetry = 0; 62 | 63 | for (;;) 64 | { 65 | try 66 | { 67 | // Call external service. 68 | await TransientOperationAsync(); 69 | 70 | // Return or break. 71 | break; 72 | } 73 | catch (Exception ex) 74 | { 75 | Trace.TraceError("Operation Exception"); 76 | 77 | currentRetry++; 78 | 79 | // Check if the exception thrown was a transient exception 80 | // based on the logic in the error detection strategy. 81 | // Determine whether to retry the operation, as well as how 82 | // long to wait, based on the retry strategy. 83 | if (currentRetry > this.retryCount || !IsTransient(ex)) 84 | { 85 | // If this isn't a transient error or we shouldn't retry, 86 | // rethrow the exception. 87 | throw; 88 | } 89 | } 90 | 91 | // Wait to retry the operation. 92 | // Consider calculating an exponential delay here and 93 | // using a strategy best suited for the operation and fault. 94 | await Task.Delay(delay); 95 | } 96 | } 97 | 98 | // Async method that wraps a call to a remote service (details not shown). 99 | private async Task TransientOperationAsync() 100 | { 101 | ... 102 | } 103 | ``` 104 | 105 | 调用此方法的语句包含在一个包装在for循环中的`try / catch`块中。如果`TransientOperationAsync`方法调用成功,不抛出异常,for循环将退出。如果`TransientOperationAsync`方法失败,catch块将检查失败的原因。如果判定是一个瞬态错误,代码在重试操作之前等待一个短暂的延迟。 106 | for循环还跟踪尝试操作的次数,如果代码失败三次,则认为异常会持久。如果异常不是瞬态的或持久的,捕获处理程序会引发异常。异常退出for循环,被调用`OperationWithBasicRetryAsync`方法的代码捕获。 107 | `IsTransient`方法(如下所示)检查与代码运行的环境相关的一组特定异常。瞬态异常的定义,因正在访问的资源和执行操作的环境而有所不同。 108 | 109 | ```java 110 | private bool IsTransient(Exception ex) 111 | { 112 | // Determine if the exception is transient. 113 | // In some cases this is as simple as checking the exception type, in other 114 | // cases it might be necessary to inspect other properties of the exception. 115 | if (ex is OperationTransientException) 116 | return true; 117 | 118 | var webException = ex as WebException; 119 | if (webException != null) 120 | { 121 | // If the web exception contains one of the following status values 122 | // it might be transient. 123 | return new[] {WebExceptionStatus.ConnectionClosed, 124 | WebExceptionStatus.Timeout, 125 | WebExceptionStatus.RequestCanceled }. 126 | Contains(webException.Status); 127 | } 128 | 129 | // Additional exception checking logic goes here. 130 | return false; 131 | } 132 | ``` 133 | ## 相关模式和指南 134 | 135 | * [断路器模式](circuit-breaker.md)。重试模式对于处理瞬态故障很有用。如果预计故障会持续更长时间,则可能更适合实施断路器模式。重试模式还可以与断路器一起使用,以提供处理故障的综合方法。 136 | * [重试针对具体服务的指南](https://docs.microsoft.com/en-us/azure/architecture/best-practices/retry-service-specific) 137 | * [连接弹性](https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency) -------------------------------------------------------------------------------- /patterns/scheduler-agent-supervisor.md: -------------------------------------------------------------------------------- 1 | # 调度者、代理、管理者模式 2 | 3 | 将一组分布式操作协调为单一的操作。如果有任何操作失败,请尝试透明地处理故障,否则撤销已执行的工作,从而使整个操作成功或失败。这可以为分布式系统增加弹性,使其能够恢复并重试由于瞬时异常,长时间故障和进程故障而失败的操作。 4 | 5 | ## 背景和问题 6 | 7 | 应用程序执行包含多个步骤的任务,其中一些步骤可能会调用远程服务或访问远程资源。 各个步骤可能是彼此独立的,但它们是由执行任务的应用程序逻辑编排的。 8 | 应用程序应确保任务尽可能运行完成,并解决访问远程服务或资源时可能发生的任何故障。故障发生的原因很多。例如,网络可能断开,通信可能中断,远程服务无响应或处于不稳定状态,或者远程资源暂时无法访问(可能是由于资源限制)。很多情况下故障将是暂时的,可以通过使用[重试模式](retry.html)来处理。 9 | 如果应用程序检测到更永久性的故障,不能轻易恢复,必须能够将系统恢复到一致状态,并确保整个操作的完整性。 10 | 11 | ## 解决方案 12 | 13 | 调度者-代理-管理者模式定义了以下角色。这些角色编排作为整体任务一部分执行的步骤。 14 | 15 | 调度者安排构成要执行的任务的步骤并编排其操作。这些步骤可以合并到一个流水线或工作流程中。调度者负责确保以正确的顺序执行此工作流程中的步骤。在执行每个步骤时,计划程序会记录工作流的状态,如“步骤尚未开始”,“步骤运行”或“步骤完成”。状态信息还应该包括该步骤完成所允许的时间的上限,称为完成时间。如果某个步骤需要访问远程服务或资源,则调度程序会调用相应的代理程序,并向其传递要执行的工作的详细信息。调度程序通常使用异步请求/响应消息与代理进行通信。这可以使用队列来实现,但是也可以使用其它分布式消息技术。 16 | 17 | >调度者在[进程管理器模式](http://www.enterpriseintegrationpatterns.com/patterns/messaging/ProcessManager.html)中执行与进程管理器类似的功能。实际的工作流程通常由调度者控制的工作流引擎定义和实现。这种方法将工作流中的业务逻辑从调度者中分离出来。 18 | 19 | * 代理包含封装调用远程服务的逻辑,或者访问由任务中的步骤引用的远程资源。每个代理通常将调用包装为单个服务或资源,实现相应的错误处理和重试逻辑(受到超时限制的限制,稍后会介绍)。如果调度程序运行的工作流中的步骤跨越不同的步骤使用多个服务和资源,则每个步骤都可能引用不同的代理(这是模式的实现细节)。 20 | 21 | * 管理者监视调度者执行的任务中的步骤状态。定期运行(系统指定的频率),并检查调度者维护的步骤的状态。如果检测到任何超时或失败,则会安排相应的代理恢复该步骤或执行适当的补救措施(这可能涉及修改步骤的状态)。请注意,恢复或补救措施由调度者和代理执行。管理者应该简单地要求执行这些动作。 22 | 23 | 调度者、代理和管理者是逻辑组件,其物理实现取决于所使用的技术。例如,几个逻辑代理可能作为为单个Web服务的一部分实现。 24 | 25 | 调度者维护有关任务进度以及持久化数据存储中每个步骤的状态(称为状态存储)的信息。管理者可以使用这些信息来帮助确定步骤是否失败。下图说明了调度者,代理,管理者和状态存储之间的关系。 26 | 27 | ![](https://docs.microsoft.com/en-us/azure/architecture/patterns/_images/scheduler-agent-supervisor-pattern.png) 28 | 29 | >该图是模式的简化版本。在一个真正的实现中,可能有许多的实例同时运行,每个任务都是一个子集。同样,系统可以运行每个代理的多个实例,甚至可以运行多个管理者。在这种情况下,管理者必须认真协调它们的工作,以确保它们不会为了恢复相同的失败步骤和任务而相互竞争。[领导者选举模式](leader-election.html)为这个问题提供了一个可能的解决方案。 30 | 31 | 当应用程序准备好运行任务时,它向调度者提交一个请求。调度者在状态存储区中记录有关任务及其步骤的初始状态信息(例如,步骤尚未开始),然后开始执行由工作流定义的操作。当调度者启动每一步时,它会更新有关状态存储中该步骤状态的信息(例如,步骤在运行中)。 32 | 33 | 如果某个步骤引用了远程服务或资源,则调度者将向相应的代理发送消息。该消息包含代理程序需要传递给服务或访问资源的信息,以及操作的完成时间。如果代理程序成功完成其操作,它将向计划程序返回一个响应。然后调度程序可以更新状态存储中的状态信息(例如,完成步骤)并执行下一步。这个过程一直持续到整个任务完成。 34 | 35 | 代理可以实现任何执行其工作所必需的重试逻辑。但是,如果代理在时间到期之前没有完成工作,则调度者将认为操作失败。在这种情况下,代理应该停止工作,不要试图将任何东西返回给调度者(甚至不会出现错误消息),或尝试任何形式的恢复。这样限制的原因是,在一个步骤超时或失败之后,可能会安排另一个代理实例运行失败步骤(稍后介绍此过程)。 36 | 37 | 如果代理失败,调度者将不会收到响应。这种模式并没有区分已经超时的步骤和真正失败的步骤。 38 | 39 | 如果某个步骤超时或失败,状态存储将包含一个记录,指示该步骤正在运行,但是已经过去了一段时间。管理者寻找这样的步骤,并试图恢复它们。一种可能的策略是管理者更新完整值以延长完成该步骤的可用时间,然后向调度者发送消息,以识别已超时的步骤。调度者然后可以尝试重复此步骤。但是,这种设计要求任务是幂等的。 40 | 41 | 如果监督者连续失败或超时,可能需要防止同一步骤重试。要做到这一点,管理者可以在状态存储中保存每个步骤的重试次数以及状态信息。如果计数超过预定的阈值,则管理者可以采取等待较长时间的策略,然后通知调度者应该重试此步骤,以期在此期间解决故障。或者,管理者可以通过执行[补偿交易模式](compensating-transaction.html)向调度者发送消息以请求撤消整个任务。这种方法将取决于调度者和代理提供实施成功完成的每个步骤的补偿操作的必要信息。 42 | 43 | >管理者的目的不是监控调度者和代理,而是在失败时重新启动它们。系统的这个方面应该由运行这些组件的基础设施来处理。同样,管理者不应该知道调度者正在执行的任务的实际业务操作的内容(包括如果这些任务失败)。这是调度者实现的工作流逻辑的目的。管理者的唯一责任是确定一个步骤是否失败,并安排是重复执行还是将包含失败步骤的整个任务撤消。 44 | 45 | 如果调度者在发生故障后重新启动,或者由调度程序执行的工作流程意外终止,则调度程序应该能够确定在发生故障时正在处理的任何中转任务的状态,并准备从该调度程序中恢复此任务点。这个过程的实现细节可能是系统特定的。如果任务无法恢复,则可能需要撤消该任务已执行的工作。也可能需要实施[补偿性交易](compensating-transaction.html)。 46 | 47 | 这种模式的关键优势在于,系统在发生意外的暂时或不可恢复的故障时具有弹性。该系统可以构建为能自我修复。例如,如果代理或调度程序出现故障,则可以启动新代理,并且管理者可以安排要恢复的任务。如果管理者失败,另一个实例可以启动,并可以从发生故障的地方接管。如果管理者定期运行,则可以在预定义的时间间隔后自动启动新的实例。可以复制状态存储以达到更大程度的弹性。 48 | 49 | ## 问题和注意事项 50 | 在决定如何实现这种模式时,应该考虑以下几点: 51 | 52 | * 这种模式可能很难实现,需要对系统的每种可能的故障模式进行彻底的测试。 53 | * 调度者执行的恢复/重试逻辑非常复杂,取决于状态存储中保存的状态信息。在持久的数据存储中记录实施补偿性交易所需的信息也许是必要的。 54 | * 管理者多久运行一次很重要。它应该经常运行,以防止任何失败的步骤长时间阻塞应用程序,但它不应该过多运行,以至于带来过多开销。 55 | * 代理执行的步骤可以运行多次。实现这些步骤的逻辑应该是幂等的。 56 | 57 | ## 何时使用此模式 58 | 59 | 当在分布式环境(如云)中运行的进程必须对通信故障和/或操作故障具有弹性时,请使用此模式。 60 | 此模式可能不适于不调用远程服务或访问远程资源的任务。 61 | 62 | ## 案例 63 | 64 | 电子商务系统的Web应用程序已部署在Microsoft Azure上。用户可以运行此应用程序来浏览可用产品并下订单。用户界面作为Web角色运行,应用程序的订单处理元素被实现为一组辅助角色。订单处理逻辑的一部分涉及访问远程服务,系统的这一方面可能容易出现瞬态或更持久的故障。为此,设计人员使用调度者-代理-管理者模式来实现系统的订单处理元素。 65 | 66 | 当客户下订单时,应用程序会构造一条描述订单的消息,并将此消息发布到队列中。以工作角色运行的单独提交流程检索消息,将订单详细信息插入订单数据库,并在状态存储中为订单处理创建记录。请注意,插入订单数据库和状态存储是作为相同操作的一部分执行的。提交过程旨在确保两个插入一起完成。 67 | 68 | 提交过程为订单创建的状态信息包括: 69 | 70 | * **订单ID**。订单数据库中订单的ID。 71 | 72 | * **LockedBy**。处理订单的辅助角色的实例标识。运行调度程序的工作角色可能有多个当前实例,但每个订单只能由一个实例处理。 73 | 74 | * **CompleteBy**。订单处理的时间。 75 | 76 | * **ProcessState**。处理订单的任务的当前状态。可能的状态包括: 77 | * **待定**。订单已创建,但尚未开始处理。 78 | * **处理**。订单正在处理中。 79 | * **处理**。订单已成功处理。 80 | * **错误**。订单处理失败。 81 | 82 | * **FailureCount**。已经尝试处理订单的次数。 83 | 84 | 在此状态信息中,`OrderID`字段从新订单的订单ID复制而来。 `LockedBy`和`CompleteBy`字段被设置为null,`ProcessState`字段被设置为Pending,并且`FailureCount`字段被设置为0。 85 | 86 | >在这个例子中,订单处理逻辑相对简单,只有一个调用远程服务的步骤。在更复杂的多步骤场景中,提交过程可能涉及多个步骤,因此状态存储中将创建多个记录,每个记录描述单个步骤的状态。 87 | 88 | 调度者也作为辅助角色的一部分运行,并实现处理订单的业务逻辑。调度者轮询新订单的实例将检查`LockedBy`字段为空且`ProcessState`字段处于挂起状态的记录的状态存储。当调度者发现一个新的订单时,立即用自己的实例ID填充`LockedBy`字段,将`CompleteBy`字段设置为适当的时间,并将`ProcessState`字段设置为处理中。该代码被设计为独占和原子,以确保调度程序的两个并发实例不能尝试同时处理相同的订单。 89 | 90 | 然后,调度程序运行业务工作流以异步处理订单,并将状态存储中的`OrderID`字段中的值传递给它。处理订单的工作流程将从订单数据库中检索订单的详细信息并执行其工作。当订单处理工作流中的某个步骤需要调用远程服务时,它使用一个代理。工作流步骤使用一对充当请求/响应通道的Azure服务总线消息队列与代理进行通信。下图显示了该解决方案的概况。 91 | 92 | ![](https://docs.microsoft.com/en-us/azure/architecture/patterns/_images/scheduler-agent-supervisor-solution.png) 93 | 94 | 从工作流步骤发送到代理的消息描述了订单并包含完成时间。如果代理在完成时间到期之前收到来自远程服务的响应,则会在工作流正在侦听的服务总线队列上发布回复消息。当工作流步骤接收到有效的回复消息时,它完成其处理,并且调度者设置要处理的订单状态的`ProcessState`字段。此时,订单处理已成功完成。 95 | 96 | 如果在代理收到远程服务响应之前完成时间到期,代理将简单地停止并终止处理订单。同样,如果处理订单的工作流程超过完成时间,它也会终止。在这两种情况下,状态存储中的订单状态都保持设置为处理状态,但是完成时间表示处理订单的时间已经过去,并且过程被视为失败。请注意,如果访问远程服务的代理或处理订单(或两者)的工作流程意外终止,则状态存储中的信息将再次保持设置为处理状态,并最终将具有到期的完成时间值。 97 | 98 | 如果代理在尝试联系远程服务时检测到不可恢复的,非暂时性故障,它可以将错误响应发送回工作流。调度者可以将订单的状态设置为错误,并引发提醒操作员的事件。然后操作员可以尝试手动解决失败的原因,然后重新提交失败的处理步骤。 99 | 100 | 管理者定期检查状态存储,查找完成时间过期的订单。如果管理者发现一条记录,则会增加`FailureCount`字段。如果`FailureCount`低于指定的阈值,管理者将`LockedBy`字段重置为空,用新的到期时间更新`CompleteBy`字段,并将`ProcessState`字段设置为未决定。调度者的一个实例可以拿起这个订单并像以前一样执行处理。如果`FailureCount`值超过指定的阈值,则认为失败的原因是非暂时性的。管理者将订单的状态设置为错误,并引发提醒操作员的事件。 101 | 102 | >在这个例子中,管理者是在一个单独的辅助角色中实现的。可以使用各种策略来安排管理者任务运行,包括使用Azure Scheduler服务(不要与此模式中的调度者组件混淆)。关于Azure调度者服务的更多信息,请访问[调度者](https://azure.microsoft.com/services/scheduler/)页面。 103 | 104 | 尽管在本例中没有显示,但是调度者可能需要让提交订单的应用程序知道订单的进度和状态。应用程序和调度程序是相互隔离的,以消除它们之间的依赖关系。应用程序不知道调度者的哪个实例正在处理订单,并且调度者不知道哪个特定的应用程序实例公布了订单。 105 | 106 | 为了能够报告订单状态,应用程序可以使用自己的专用响应队列。这个响应队列的细节将作为发送到提交过程的请求的一部分,包括在状态存储中的信息。调度者然后将消息发送到此队列,指示订单的状态(请求接收,订单完成,订单失败等等)。它应该在这些消息中包含订单ID,以便关联应用程序的原始请求。 107 | 108 | ## 相关的模式和指南 109 | 110 | 以下模式和指南在实施这种模式时可能是相关的: 111 | 112 | * [重试模式](retry.md)。代理可以使用此模式来透明地重试访问以前失败的远程服务或资源的操作。当期望失败的原因是暂时的并且可以纠正时使用。 113 | * [断路器模式](circuit-breaker.md)。代理可以使用此模式处理连接到远程服务或资源时需要花费不同时间的错误。 114 | * [补偿交易模式](compensating-transaction.md)。如果调度者执行的工作流程无法成功完成,则可能需要撤消之前执行的任何工作。补偿交易模式描述了如何实现遵循最终一致性模型的操作。这些类型的操作通常由执行复杂业务流程和工作流程的调度者来实现。 115 | * [异步消息入门](https://msdn.microsoft.com/library/dn589781.aspx)。调度者-代理-管理者模式中的组件通常彼此解耦,并异步通信。本文介绍了一些可用于基于消息队列实现异步通信的方法。 116 | * [领导者选举模式](leader-election.md)。可能需要协调管理者的多个实例的操作,以防止它们尝试恢复相同的失败进程。领导者选举模式介绍了如何做到这一点。 117 | * Clemens Vasters博客上的[云体系结构:调度者-代理-管理者模式](https://blogs.msdn.microsoft.com/clemensv/2010/09/27/cloud-architecture-the-scheduler-agent-supervisor-pattern/) 118 | * [进程管理器模式](http://www.enterpriseintegrationpatterns.com/patterns/messaging/ProcessManager.html) 119 | * [参考6:Saga的传奇](https://msdn.microsoft.com/library/jj591569.aspx)。介绍CQRS模式如何使用流程管理器(CQRS之旅指南的一部分)的案例。 120 | * [Microsoft Azure Scheduler](https://azure.microsoft.com/services/scheduler/) -------------------------------------------------------------------------------- /patterns/sharding.md: -------------------------------------------------------------------------------- 1 | # 分片模式 2 | 3 | 将数据存储区划分为一组水平分区或分片。 这可以提高存储和访问大量数据时的扩展性。 4 | 5 | ### 背景和问题 6 | 7 | 由单个服务器托管的数据存储可能会受到以下限制: 8 | * **储存空间**。预计大规模云应用的数据存储将包含大量可能随时间显著增加的数据。通常服务器仅提供有限数量的磁盘存储,但是可以用较大的磁盘替换现有磁盘,或者随着数据卷的增长,将额外的磁盘添加到服务器。然而,系统最终将达到极限,无法再轻松地给服务器增加存储容量。 9 | * **计算资源**。需要云应用程序来支持大量并发用户,每个并发用户运行查询从数据存储中检索信息。托管数据存储的单个服务器可能无法提供必要的计算能力来支持此负载,从而在尝试存储和检索数据超时的应用程序时,导致更多的响应时间和频繁的故障。可以添加内存或升级处理器,但如果无法进一步增加计算资源,系统将达到极限。 10 | * **网络带宽**。最终,在单个服务器上运行的数据存储的性能受服务器可以接收请求和发送回复的速率的约束。网络流量可能会超过用于连接到服务器的网络的容量,导致请求失败。 11 | * **地理**。因为法律、合规性或者性能、减少数据访问延迟的原因,可能需要为同一区域的特定用户存储生成的数据。如果用户分散在不同的国家或地区,可能无法将应用程序的所有数据存储在单个数据存储中。 12 | 13 | 通过添加更多磁盘容量,处理能力,内存和网络连接来垂直扩展可以延缓其中一些限制的影响,但这些只是临时解决方案。能够支持大量用户和数据的商业云应用程序必须能够无限伸缩,因此垂直扩展不一定是最佳解决方案。 14 | 15 | ## 解决方案 16 | 17 | 将数据存储区划分为水平分区或分片。每个分片具有相同的schema,但拥有自己独特的数据子集。分片是自己的数据存储(可以包含许多不同类型实体的数据),在作为存储节点的服务器上运行。 18 | 这种模式具有以下好处: 19 | * 可以通过添加在其它存储节点上运行的碎片进一步的来扩展系统。 20 | * 系统可以使用现成的硬件,而不是为每个存储节点使用定制和昂贵的服务器。 21 | * 通过平衡分片上的工作负载,可以减少争用并提高性能。 22 | * 在云中碎片可以靠近访问数据的用户。 23 | 24 | 将数据存储区划分成碎片时,决定在每个分片中放置哪些数据。分片通常包含落在由数据的一个或多个属性确定的指定范围内的项目。这些属性形成分片键(有时称为分区键)。分片键应该是静态的。不应该基于可能会改变的数据。 25 | 分片物理组织数据。当应用程序存储和检索数据时,分片逻辑将应用程序引导到适当的碎片。分片逻辑可以用应用程序中的数据访问代码的一部分实现,或者如果数据存储系统透明地支持分片,也可以实现该分片逻辑。 26 | 在分片逻辑中提取数据的物理位置可以高效地控制分片包含哪些数据。如果分片中的数据稍后需要重新分配(例如,如果分片变得不平衡),它还可以使数据在分片之间迁移,而无需重新调整应用程序的业务逻辑。需要权衡每个数据项的检索位置时所需的附加数据访问开销。 27 | 为了确保最佳性能和可扩展性,重要的是以适合应用程序执行的查询类型的方式拆分数据。在许多场景下,分片方案不太可能完全符合每个查询的要求。例如,在多租户系统中,应用程序可能需要使用租户ID检索租户数据,但也可能需要根据某些其它属性(如租户的姓名或位置)查找此数据。要处理这些情况,使用支持最常执行查询的分片键来实施分片策略。 28 | 29 | 如果查询使用属性值的组合定期检索数据,则可以通过将属性链接在一起来定义复合分片键。或者使用诸如[索引表模式](index-table.html)可以基于未被分片键覆盖的属性快速查找数据。 30 | 31 | ## 分片策略 32 | 33 | 通常使用三种策略选择分片键并决定如何在分片间分配数据。请注意,分片和托管它们的服务器之间不一定是一一对应的-单个服务器可以托管多个分片。 这些策略是: 34 | **查询策略**。该策略中,分片逻辑实现了使用分片键将数据请求路由到包含该数据的分片的映射。在多租户应用程序中,租户的所有数据可以使用租户ID作为分片键一起存储在分片中。多个租户可能共享相同的分片,但单个租户的数据不会分散在多个分片之间。下图示出了基于租户ID划分租户数据。 35 | ![](https://docs.microsoft.com/en-us/azure/architecture/patterns/_images/sharding-tenant.png) 36 | 37 | 碎片键和物理存储之间的映射可以基于物理碎片,其中每个碎片键映射到物理分区。或者,更灵活的重新平衡分片的技术是虚拟分区,其中分片键映射到相同数量的虚拟分片,其又映射到更少的物理分区。在这种方法中,应用程序使用指向虚拟分片的分片密钥来定位数据,系统将虚拟分片透明地映射到物理分区。虚拟分片和物理分区之间的映射可以改变,而不需要修改应用程序代码以使用不同的分片键集合。 38 | 39 | **范围策略**。此策略将相关项目组合在同一个分片中,并通过分片键排序-分片键是顺序的。对于使用范围查询(返回一个分配给指定范围内的分片数据项的一组数据项的查询)的应用程序很有用。例如,如果应用程序需要经常查找给定月份中的所有订单,一个月的所有订单以相同的分片的日期和时间顺序存储,可以更快地检索到数据。如果每个订单都存储在不同的分片中,则必须通过执行大量的点查询(返回单个数据项的查询)来单独获取。下图说明了以碎片存储数据的顺序集(范围)。 40 | ![](https://docs.microsoft.com/en-us/azure/architecture/patterns/_images/sharding-sequential-sets.png) 41 | 42 | 在这个例子中,分片键是一个复合键,其中最重要的元素包含订单月份,以及订单日期和时间。订单数据自然会在创建新订单并添加到分片时排序。某些数据存储支持两部分的分片键,其中包含标识分片的分区键元素和唯一标识分片中的项目的行键。数据通常在分片中按行键顺序保存。范围查询且需要分组在一起的项目可以使用分区键具有相同值,同时行键的值唯一的分片键,。 43 | **哈希策略**。该策略的目的是减少热点(接收到不相称的负载的碎片)的机会。它可以在数据分片之间分配数据,以实现每个分片的大小与每个分片将遇到的平均负载之间的平衡。分片逻辑基于数据的一个或多个属性的散列来计算分片以存储项目。使用的散列函数应该将数据均匀地分布在分片上,可能需要在计算中引入一些随机元素。下图说明了基于租户ID散列的租户数据。 44 | 45 | ![](https://docs.microsoft.com/en-us/azure/architecture/patterns/_images/sharding-data-hash.png) 46 | 47 | 要了解哈希策略相比其它分片策略的优势,考虑顺序注册新租户的多租户应用程序,如何将租户分配给数据存储中的分片。当使用范围策略时,租户1到n的数据将全部存储在分片A中,租户n + 1至m的数据将全部存储在分片B中,依此类推。如果最近注册的租户也是最活跃的,大多数数据活动将发生在少数分片中,这可能导致热点。相比之下,哈希策略根据租户ID的哈希值将租户分配给分片。这意味着顺序租户最有可能被分配到不同的分片,这将分散负载。如上图中租户55和56。 48 | 49 | 三个分片策略具有以下优点和考虑: 50 | **查询**。这样可以更好地控制碎片的配置和使用方式。使用虚拟分片减少重新平衡数据时的影响,因为可以添加新的物理分区以使工作负载平衡。可以修改虚拟分片和实现分片的物理分区之间的映射,而不会影响使用分片键来存储和检索数据的应用程序代码。查找碎片位置可能会增加额外的开销。 51 | **范围**。这很容易实现,并且适用于范围查询,因为它们通常可以在单个操作中从单个分片中获取多个数据项。这一策略提供更简单的数据管理,例如,如果同一区域中的用户处于相同的分片,则可以根据本地的负载和需求模式在每个时区中调度更新。但是,这种策略不能在分片之间提供最佳平衡。如果大多数活动用于相邻的分片键,重新平衡分片是困难的,并且可能无法解决负载不均匀的问题。 52 | **哈希**。这种策略提供了更加均匀的数据和负载分配的机会。可以通过使用哈希函数直接完成请求路由。没有必要维护映射。请注意,计算哈希可能会增加额外的开销。此外,重新平衡分片比较困难。 53 | 54 | 最常见的分片系统都使用上述方法之一实现,但还应考虑应用程序的业务需求及其数据使用模式。例如,在多租户应用程序中: 55 | * 可以根据工作负载分割数据。将单独碎片中高度易失性租户的数据隔离。这样其他租户的数据访问速度可能会得到改善。 56 | * 可以根据租户的位置来划分数据。将特定地理区域内的租户的数据在该地区的非高峰时段离线进行备份和维护,而其他地区的租户的数据在工作时间内保持在线状态。 57 | * 高价值租户可以分配自己的私人,高性能,低负载的分片,而低价值租户可能会分享更密集包装、繁忙的分片。 58 | * 需要高度数据隔离和隐私的租户的数据可以存储在完全独立的服务器上。 59 | 60 | ## 扩展和数据移动操作 61 | 62 | 每个分片策略意味着管理收缩,扩展,数据移动和维护状态的不同能力和复杂级别。 63 | 查询策略允许在用户级别执行缩放和数据移动操作,无论是在线还是离线。该技术会暂停某些或所有用户活动(可能在非高峰期),将数据移动到新的虚拟分区或物理分片,更改映射,使保存此数据的任何缓存无效或刷新,然后允许用户活动恢复。通常这种类型的操作可以集中管理。查询策略要求状态高度可缓存和复制友好。 64 | 范围策略对缩放和数据移动操作施加了一些限制,通常必须在数据存储的一部分或全部脱机时执行,因为数据必须在分片之间进行拆分和合并。移动数据平衡分片可能无法解决不均匀加载的问题,如果大多数活动用于相同范围内的相邻分片键或数据标识符。范围策略还可能需要维护一些状态,以将范围映射到物理分区。 65 | 哈希策略使缩放和数据移动操作更复杂,因为分区键是分片键或数据标识符的散列。每个分片的新位置必须从散列函数确定,或修改为提供正确映射的函数。但是,哈希策略不需要维护状态。 66 | 67 | ## 问题和注意事项 68 | 69 | 在决定如何实现此模式时,请考虑以下几点: 70 | 71 | * 分片与其它形式的分区互补,如垂直分区和功能分区。例如,单个分片可以包含已经垂直分区的实体,并且功能分区可以以多个分片的方式实现。有关分区的更多信息,请参阅[数据分区指南](https://msdn.microsoft.com/library/dn589795.aspx)。 72 | * 保持分片平衡,平衡I/O处理的容量。随着数据的插入和删除,有必要定期重新平衡分片,以保证均匀分布,并减少热点的问题。再平衡的成本可能很高。为了减少重新平衡的必要性,通过确保每个分片都包含足够的可用空间来处理预期的变化量来计划增长。如果有必要,还应该开发可用于快速重新平衡碎片的策略和脚本。 73 | * 使用稳定的数据作为分片的键。如果键更改,相应的数据项可能需要在分片之间移动,从而增加更新操作执行的工作量。因此,避免将键置于潜在的易失性信息上。相反,寻找不变量或自然形成关键字的属性。 74 | 75 | * 确保分片键是唯一的。例如,避免使用自增字段作为分片键。某些系统的自增字段无法在分片上进行协调,可能导致不同分片中的项目具有相同的分片键。 76 | > 非分片键的自增字段也可能会导致问题。例如,如果使用自增字段来生成唯一的ID,那么位于不同分段中的两个不同的项目可能会被分配相同的ID。 77 | * 可能无法设计匹配每个可能的数据查询要求的分片键。分割数据以支持最常执行的查询,如有必要可以创建辅助索引表,支持不属于分片键的属性的标准来检索数据的查询。详细信息,请参阅[索引表模式](index-table.html)。 78 | * 仅访问单个分片的查询比从多个分片中检索数据的查询更有效,因此分片系统要避免设计成让应用程序在执行大量join查询时,从多个不同的分片读取数据。单个分片可以包含多种类型的实体的数据。考虑对数据进行非规范化,以便将通常在一起查询的相关实体(例如客户的详细信息和他们所放置的订单)保留在同一个分片中,以减少应用程序执行的独立读取次数。 79 | 80 | > 如果一个分片中的实体引用存储在另一个分片中的实体,则将第二个实体的分片键作为第一个实体的schema的一部分。这可以帮助提高需要在分片上引用相关数据的查询的性能。 81 | 82 | * 如果应用程序必须执行从多个分片中检索数据的查询,可能可以通过并行任务来获取这些数据。比如扇出查询的例子,并行检索来自多个分片的数据,然后聚合成单个结果。然而,这种方法不可避免地为数据访问逻辑增加了一些复杂性。 83 | 84 | * 对于很多应用程序来说,创建大量小碎片可以比拥有少量大碎片更有效,因为它们可以提供更多的负载均衡机会。将碎片从一个物理位置迁移到另一个物理位置的场景也非常适用。移动一个小碎片比移动一个大碎片更快。 85 | 86 | * 确保每个分片存储节点可用的资源足以在数据大小和吞吐量方面满足可扩展性要求。详细信息请参见[数据分区指南](https://msdn.microsoft.com/library/dn589795.aspx)中的“设计可扩展性分区”一节。 87 | 88 | * 考虑将参考数据复制到所有分片。如果从分片中检索数据的操作也引用静态或缓慢移动的数据作为查询的一部分,请将此数据添加到分片。然后应用程序就可以轻松获取查询的所有数据,无需再次单独访问数据存储。 89 | 90 | * 如果保存在多个分片中的参考数据发生更改,则系统必须在所有分片之间同步这些更改。同步发生时,系统可能会遇到一定程度的不一致。如果要这么做,应该设计应用程序具备处理的能力。 91 | 92 | * 维护引用完整性和分片之间的一致性可能很困难,应该最大程度地减少影响多个分片中的数据的操作。如果应用程序必须在分片之间修改数据,请评估是否需要完整的数据一致性。相反,云中的常见做法是实现最终一致性。每个分区中的数据分别更新,应用程序逻辑必须承担责任,确保更新全部成功完成,并处理在最终一致的操作运行时查询数据时可能产生的不一致。有关实现最终一致性的更多内容,请参阅[数据一致性入门](https://msdn.microsoft.com/library/dn589800.aspx)。 93 | 94 | * 配置和管理大量分片可能是一个挑战。监控,备份,检查一致性以及日志记录或审核等任务必须在多个碎片和服务器上完成,可能会保存在多个位置。这些任务可以使用脚本或其它自动化解决方案来实现,但可能无法完全消除额外的管理需求。 95 | 96 | * 碎片可以进行地理位置定位,使其包含的数据与使用它的应用程序的实例接近。这种方法可以显著提高性能,但需要额外考虑对必须访问不同位置的多个分片的任务。 97 | 98 | ### 何时使用该模式 99 | 100 | 当数据存储可能需要扩展超过单个存储节点可用的资源时,或者通过减少数据存储中的资源争夺来提高性能,可以使用此模式。 101 | > 分片的重点是提高系统的性能和可扩展性,其副产品还可以通过将数据分割为单独的分区来提高可用性。 一个分区中的故障不一定妨碍应用程序访问其他分区中保存的数据,并且操作员可以执行维护或恢复一个或多个分区,而不会使应用程序的所有数据无法访问。有关更多信息,请参阅[数据分区指南](https://msdn.microsoft.com/library/dn589795.aspx)。 102 | 103 | ## 案例 104 | 105 | 以下例子中C#代码使用一组作为碎片的SQL Server数据库。每个数据库保存应用程序使用的数据子集。应用程序使用自己的分片逻辑(这里是扇出查询的例子)检索分片在碎片之间的数据。`GetShards`方法返回位于每个分片中的数据的详细信息。该方法返回`ShardInformation`对象的枚举列表,其中`ShardInformation`类型包含每个分片的标识符和应用程序用于连接到分片的SQL Server连接字符串(代码示例中未显示连接字符串)。 106 | 107 | ```c# 108 | private IEnumerable GetShards() 109 | { 110 | // This retrieves the connection information from a shard store 111 | // (commonly a root database). 112 | return new[] 113 | { 114 | new ShardInformation 115 | { 116 | Id = 1, 117 | ConnectionString = ... 118 | }, 119 | new ShardInformation 120 | { 121 | Id = 2, 122 | ConnectionString = ... 123 | } 124 | }; 125 | } 126 | ``` 127 | 128 | 下面的代码显示了应用程序如何使用`ShardInformation`对象列表执行并行从每个分片中获取数据的查询。查询的详细信息未显示,但例子中检索的数据包含一个字符串,如果该分片包含客户的详细信息,它可以保存诸如客户名称等信息。查询结果汇总到`ConcurrentBag`集合中以供应用程序处理。 129 | 130 | ```c# 131 | // Retrieve the shards as a ShardInformation[] instance. 132 | var shards = GetShards(); 133 | 134 | var results = new ConcurrentBag(); 135 | 136 | // Execute the query against each shard in the shard list. 137 | // This list would typically be retrieved from configuration 138 | // or from a root/master shard store. 139 | Parallel.ForEach(shards, shard => 140 | { 141 | // NOTE: Transient fault handling isn't included, 142 | // but should be incorporated when used in a real world application. 143 | using (var con = new SqlConnection(shard.ConnectionString)) 144 | { 145 | con.Open(); 146 | var cmd = new SqlCommand("SELECT ... FROM ...", con); 147 | 148 | Trace.TraceInformation("Executing command against shard: {0}", shard.Id); 149 | 150 | var reader = cmd.ExecuteReader(); 151 | // Read the results in to a thread-safe data structure. 152 | while (reader.Read()) 153 | { 154 | results.Add(reader.GetString(0)); 155 | } 156 | } 157 | }); 158 | 159 | Trace.TraceInformation("Fanout query complete - Record Count: {0}", 160 | results.Count); 161 | ``` 162 | 163 | ### 相关模式和指南 164 | 165 | 以下模式和指南在实现此模式时也可能相关: 166 | 167 | * [数据一致性入门](https://msdn.microsoft.com/library/dn589800.aspx)。需要保持分布在不同分片之间的数据的一致性。这篇文章总结了关于保持分布式数据一致性的问题,并描述不同一致性模型的优点和权衡。 168 | * [数据分区指南](https://msdn.microsoft.com/library/dn589795.aspx)。分割数据存储可能会引入一系列其它问题。这篇文章介绍了在云中分区数据存储的问题,以提高可扩展性,减少争用并优化性能。 169 | * [索引表模式](index-table.html)。有时不可能通过分片键的设计完全支持查询。允许应用程序通过指定除了分片键之外的键来快速从大型数据存储中检索数据。 170 | * [物化视图模式](materialized-view.html)。为了保持某些查询操作的性能,创建物化视图以聚合和汇总数据是非常有用的,特别是如果摘要数据基于分布在分片上的信息。物化视图模式描述了如何生成和填充这些视图。 171 | * Adding Simplicity 网站上的[分片教程](http://www.addsimplicity.com/adding_simplicity_an_engi/2008/08/shard-lessons.html)。 172 | * CodeFutures网站上的[数据库分片教程](http://dbshards.com/database-sharding/)。 173 | * [可扩展性策略入门](http://blog.maxindelicato.com/2008/12/scalability-strategies-primer-database-sharding.html):Max Indelicato博客上的数据库分片教程。 174 | * Dare Obasanjo博客[构建可扩展数据库](http://www.25hoursaday.com/weblog/2009/01/16/BuildingScalableDatabasesProsAndConsOfVariousDatabaseShardingSchemes.aspx):Dare Obasanjo博客中各种数据库分片方案的优缺点。 -------------------------------------------------------------------------------- /patterns/sidecar.md: -------------------------------------------------------------------------------- 1 | ## 挎斗模式 2 | 3 | 将应用程序的组件部署到单独的进程或容器中以提供隔离和封装。这种模式还支持异构组件和技术组成的应用程序。 4 | 5 | 这种模式叫做挎斗模式,因为它类似于三轮摩托车的挎斗。该模式下挎斗附加到父应用程序,并为应用程序提供支持功能。挎斗还拥有与父应用程序相同的生命周期,与父应用程序一起创建和退休。挎斗模式有时被称为助手模式,是一种解耦模式。 6 | 7 | ### 背景和问题 8 | 9 | 应用程序和服务通常需要如监视,日志记录,配置和网络服务等相关功能。这些外围的任务可以作为单独的组件或服务来实现。 10 | 11 | 如果将它们紧密集成到应用程序中,与应用程序在同一进程运行,从而可以有效地利用共享资源。然而,这也意味着它们没有很好的隔离,任何一个组件中断可能会影响其它组件或整个应用程序。而且,它们通常需要使用与父应用程序相同的语言来实现。因此,组件和应用程序之间存在着密切的相互依存关系。 12 | 13 | 如果将应用程序分解为服务,则可以使用不同的语言和技术构建每个服务。虽然这提供了更多的灵活性,但这意味着每个组件都有自己的依赖关系,并且需要特定语言的库来访问底层平台以及与父应用程序共享的任何资源。此外,将这些功能部署为单独的服务可能会增加应用程序的延迟。管理这些特定语言的接口的代码和依赖关系也会增加很大的复杂性,特别是托管,部署和管理方面。 14 | 15 | ## 解决方案 16 | 17 | 将主要应用程序的一组任务集中在一起,将它们运行在自己的进程或容器中,为跨语言的平台服务提供一个同构的接口。 18 | 19 | ![sidecar](https://docs.microsoft.com/en-us/azure/architecture/patterns/_images/sidecar.png) 20 | 21 | 挎斗服务不一定是应用程序的一部分,但与之相关。父应用程序在哪里它就在哪里。挎斗们是支持与主应用程序一起部署的进程或服务。 挎斗依附于摩托车上,每辆摩托车都可以有自己的挎斗。同样的,挎斗服务和父应用程序生死相依。对于应用程序的每个实例,旁边部署和托管了一个挎斗实例。 22 | 23 | 挎斗模式的优点: 24 | 25 | * 挎斗在运行时环境和编程语言方面独立于其主要应用,因此不需要用每种语言开发一个挎斗。 26 | * 挎斗可以访问与主应用程序相同的资源。例如,挎斗可以监视挎斗和主应用程序使用的系统资源。 27 | * 由于与主要应用程序相邻,它们之间的通信没有明显的延迟。 28 | * 即使对于不提供可扩展性机制的应用程序,也可以使用挎斗来扩展功能,将其作为主应用程序作为自己的进程附加在相同的主机或子容器中。 29 | 30 | 挎斗模式通常与容器一起使用,称为挎斗容器或助手容器。 31 | 32 | ### 问题和注意事项 33 | 34 | * 考虑部署服务、进程或者容器的部署和打包格式。容器特别适用于挎斗模式。 35 | * 设计挎斗服务时,请慎重决定进程间通信机制。尝试使用语言或框架无关的技术,除非性能要求。 36 | * 在将功能放入挎斗之前,请考虑它作为单独的服务或更传统的守护进程运行方式更好。 37 | * 还要考虑功能是否能以库或使用传统的扩展机制实现。特定语言的库可能具有更深层次的集成和更少的网络开销。 38 | 39 | ### 何时使用该模式 40 | 41 | 在以下场景使用该模式: 42 | 43 | * 主要应用程序使用异构语言和框架。位于挎斗服务中的组件可以由使用不同框架的不同语言编写的应用程序使用。 44 | * 远程团队或不同组织拥有组件。 45 | * 组件或功能必须与应用程序位于同一主机上 46 | * 需要一个共享主应用程序整体生命周期的服务,该服务可以独立更新。 47 | * 需要对特定资源或组件的资源限制进行细粒度的控制。例如,限制特定组件使用的内存量。可以将组件部署为挎斗,独立于主应用程序管理内存使用。 48 | 49 | 该模式可能不适合以下场景: 50 | 51 | * 当进程间通信需要优化时。父应用程序和挎斗服务之间的通信存在一些开销,特别是调用延迟。这恐怕不是聊天接口的能接受的折衷。 52 | * 对于小型应用程序,为每个实例部署挎斗服务的资源开销超过了隔离的优势。 53 | * 当服务需要与主应用程序独立或者使用不同方式进行扩展时。如果是这样,最好将功能部署为单独的服务。 54 | 55 | ### 案例 56 | 57 | 挎斗模式适用于许多场景。下面是一些常见的例子: 58 | 59 | * 基础设施API。基础设施开发团队创建一个与每个应用程序一起部署的服务,而不是一个特定于语言的客户端库访问基础架构。该服务作为挎斗加载,并为基础架构服务提供了一个共同的层,包括日志记录,环境数据,配置存储,服务发现,健康检查和看门狗服务。挎斗还监视父应用程序的主机环境和进程(或容器),并将信息记录到集中式服务。 60 | * 管理NGINX/HAProxy。用监控环境状态的挎斗服务一起部署NGINX,然后在需要更改状态时更新NGINX配置文件并回收进程。 61 | * 大使挎斗。将[大使服务](patterns/ambassador.html)部署以挎斗形式部署。该应用程序通过大使调用,该大使服务处理日志请求,路由,断路和其它连接相关功能。 62 | * 卸载代理。将NGINX代理部署在node.js服务实例的前面,处理为服务提供的静态文件内容。 63 | 64 | ### 相关指南 65 | 66 | * [大使模式](ambassador.md) -------------------------------------------------------------------------------- /patterns/static-content-hosting.md: -------------------------------------------------------------------------------- 1 | # 静态内容托管模式 2 | 3 | 将静态内容部署到云存储服务,直接返回给浏览器。这可以减少使用计算实例的昂贵成本。 4 | 5 | ## 背景和问题 6 | 7 | Web应用程序通常包含静态内容的一些元素。这些静态内容可能包括HTML页面和浏览器所需的其它资源(如图像和文档),作为HTML页面的一部分(如内联图像,样式表和客户端JavaScript文件)或单独下载( 如PDF文件)。 8 | 9 | 虽然Web服务器通过高效的动态页面代码执行和输出缓存来优化请求,但仍然需要处理下载静态内容的请求。这消耗了能更有效利用的处理循环。 10 | 11 | ## 解决方案 12 | 13 | 在大多数云托管环境中,通过将某些应用程序的资源和静态页面部署在存储服务中,可以最大限度地减少对计算实例的需求(例如,使用较小的实例或更少的实例)。云托管存储的成本通常远少于计算实例的成本。 14 | 15 | 当在存储服务中托管应用程序的某些部分时,主要注意事项与应用程序的部署以及保护匿名用户无法使用的资源相关。 16 | 17 | ## 问题和注意事项 18 | 19 | 在考虑如何实现此模式时,请考虑以下几点: 20 | 21 | * 托管存储服务必须公开一个用户可以访问的HTTP端点来下载静态资源。一些存储服务还支持HTTPS,为需要SSL的存储服务中托管资源。 22 | * 为了获得最佳性能和可用性,请考虑使用CDN将存储容器的内容缓存在世界各地的多个数据中心中。但是使用CDN需要额外的开支。 23 | * 默认情况下,存储帐户经常在不同区域复制,以提供数据中心外更高的可用性。这意味着IP地址可能会更改,但URL将保持不变。 24 | * 当某些内容位于存储帐户而其它内容位于托管的计算实例中时,应用程序部署和更新变得更具挑战。可能需要执行单独的部署,并版本化应用程序和内容以方便管理,尤其是静态内容包含脚本文件或UI组件时。但是如果只更新静态资源,则只需将其上传到存储帐户,而不需要重新部署应用程序。 25 | * 存储服务可能不支持使用自定义域名。在这种情况下,有必要在链接中指定资源的完整URL,因为它们与动态生成的包含链接的内容在不同的域中。 26 | * 必须为存储容器配置公开读取访问权限,但确保不要公开写入权限以防止用户能够上传内容。考虑使用代客钥匙或令牌来控制不对匿名用户可用资源的访问 - 更多相关信息,请参阅[代客钥匙模式](valet-key.html)。 27 | 28 | ## 何时使用该模式 29 | 30 | 在以下场景使用该模式: 31 | 32 | * 最小化包含一些静态资源的网站和应用程序的托管成本。 33 | * 最小化仅由静态内容和资源组成的网站的托管费用。根据托管服务提供商的存储系统的功能,可以在一个存储帐户中完全托管一个完全静态的网站。 34 | * 为运行在其它托管环境或本地服务器的应用程序暴露静态资源和内容。 35 | * 使用内容分发网络在多个地理区域中定位内容,CDN将存储帐户的内容缓存在世界各地的多个数据中心内。 36 | * 监控成本和带宽使用。对于某些或所有静态内容使用单独的存储帐户,可以更容易地将托管成本和运行成本分开。 37 | 38 | 此模式在以下场景用处不大: 39 | 40 | * 应用程序需要在将其传递给客户端之前对静态内容执行一些处理。例如,可能需要向文档添加时间戳。 41 | * 静态内容很小。从单独的存储中检索此内容的开销可能超过将其从计算资源中分离出来的成本。 42 | 43 | ## 案例 44 | 45 | 位于Azure Blob存储中的静态内容可以通过Web浏览器直接访问。Azure提供了可以公开暴露给浏览器的基于HTTP的存储接口。例如,Azure Blob存储容器中的内容使用以下形式的URL: 46 | ``` 47 | http:// [storage-account-name] .blob.core.windows.net / [container-name] / [file-name] 48 | ``` 49 | 上传内容时,需要创建一个或多个blob容器来保存文件和文档。请注意,新容器的默认权限为`Private`,您必须将其更改为`Public`以允许客户端访问内容。如果有必要保护内容免受匿名访问,可以实现[代客钥匙模式](valet-key.html),用户必须提供有效的令牌来下载资源。 50 | > [Blob服务概念](https://msdn.microsoft.com/library/azure/dd179376.aspx)介绍了关于blob存储的信息,以及访问和使用方式。 51 | 52 | 每个页面中的链接将指定资源的URL,客户端将直接从存储服务访问它。该图说明了直接从存储服务提供应用程序的静态部分。 53 | ![](https://docs.microsoft.com/en-us/azure/architecture/patterns/_images/static-content-hosting-pattern.png) 54 | 浏览器页面中的链接必须指定blob容器和资源的完整URL。例如,包含指向公共容器中的图像的链接的页面可能包含以下HTML内容。 55 | ```html 56 | My image 58 | ``` 59 | 60 | > 如果通过使用代客钥匙,如Azure共享访问签名,来保护资源,该签名必须包含在链接中的URL中。 61 | 62 | 名为StaticContentHosting的解决方案,演示了如何使用外部存储进行静态资源,可以在[GitHub](https://github.com/mspnp/cloud-design-patterns/tree/master/static-content-hosting)上获得。 StaticContentHosting.Cloud项目包含指定保存静态内容的存储帐户和容器的配置文件。 63 | 64 | ```xml 65 | 67 | 68 | ``` 69 | StaticContentHosting.Web工程的`Settings.cs`文件中的`Settings`类包含提取这些值并构建包含云存储帐户容器URL的字符串值的方法。 70 | 71 | ```java 72 | public class Settings 73 | { 74 | public static string StaticContentStorageConnectionString { 75 | get 76 | { 77 | return RoleEnvironment.GetConfigurationSettingValue( 78 | "StaticContent.StorageConnectionString"); 79 | } 80 | } 81 | 82 | public static string StaticContentContainer 83 | { 84 | get 85 | { 86 | return RoleEnvironment.GetConfigurationSettingValue("StaticContent.Container"); 87 | } 88 | } 89 | 90 | public static string StaticContentBaseUrl 91 | { 92 | get 93 | { 94 | var account = CloudStorageAccount.Parse(StaticContentStorageConnectionString); 95 | 96 | return string.Format("{0}/{1}", account.BlobEndpoint.ToString().TrimEnd('/'), 97 | StaticContentContainer.TrimStart('/')); 98 | } 99 | } 100 | } 101 | ``` 102 | 103 | StaticContentUrlHtmlHelper.cs文件中的`StaticContentUrlHtmlHelper`类有一个名为`StaticContentUrl`的公有方法,如果传递给它的URL以ASP.NET根路径字符(〜)开头,该方法生成包含云存储帐户路径的URL。 104 | ```java 105 | public static class StaticContentUrlHtmlHelper 106 | { 107 | public static string StaticContentUrl(this HtmlHelper helper, string contentPath) 108 | { 109 | if (contentPath.StartsWith("~")) 110 | { 111 | contentPath = contentPath.Substring(1); 112 | } 113 | 114 | contentPath = string.Format("{0}/{1}", Settings.StaticContentBaseUrl.TrimEnd('/'), 115 | contentPath.TrimStart('/')); 116 | 117 | var url = new UrlHelper(helper.ViewContext.RequestContext); 118 | 119 | return url.Content(contentPath); 120 | } 121 | } 122 | ``` 123 | 124 | Views\Home目录中的`Index.cs`html文件包含一个使用`StaticContentUrl`方法为其src属性创建URL的图像元素。 125 | ```html 126 | Test Image 127 | ``` 128 | 129 | ## 相关模式和指南 130 | 131 | * 在[GitHub](https://github.com/mspnp/cloud-design-patterns/tree/master/static-content-hosting)上可以找到此模式的例子。 132 | * [随从钥匙](valet-key.md)模式。如果目标资源需要对匿名用户不可用,则有必要在保存静态内容的存储上实现安全性。描述如何使用向客户端提供受限直接访问特定资源或服务(如云托管存储服务)的令牌或密钥。 133 | * 在Infosys博客上关于[在Azure上部署静态网站的有效方式](http://www.infosysblogs.com/microsoft/2010/06/an_efficient_way_of_deploying.html)的文章。 134 | * [Blob服务概念](https://msdn.microsoft.com/library/azure/dd179376.aspx) -------------------------------------------------------------------------------- /patterns/strangler.md: -------------------------------------------------------------------------------- 1 | # 绞杀者模式 2 | 3 | 通过使用新的应用程序和服务逐渐替换遗留系统的特定功能部件来逐步迁移。由于替换遗留系统的功能,新系统最终将替代旧系统的所有功能,绞杀并允许停用旧系统。 4 | 5 | ### 背景和问题 6 | 7 | 随着系统老化,开发工具,托管技术甚至系统架构也越来越过时。伴随新特性和功能的增加,这些应用程序的复杂性可能会大大增加,从而使其难以维护或添加新功能。 8 | 完全替代复杂的系统可能是一项巨大的任务。通常需要逐渐迁移到新系统,同时保持旧系统处理尚未迁移的功能。但是,运行两个单独版本的应用程序意味着客户端必须知道特定功能所在的位置。每次迁移功能或服务时,需要更新客户端以指向新位置。 9 | 10 | ## 解决方案 11 | 12 | 逐渐用新的应用程序和服务来替代特定功能。创建一个门面,拦截后端遗留系统的请求。门面将这些请求路由到旧应用程序或新服务。现有功能可以逐渐迁移到新系统,消费者可以继续使用相同的接口,不会感知到迁移的发生。 13 | 14 | ![](https://docs.microsoft.com/en-us/azure/architecture/patterns/_images/strangler.png) 15 | 16 | 这种模式有助于最大限度地减少迁移的风险,并随着时间推移扩大开发工作。通过门面安全地将用户路由到正确的应用程序,可以以任何喜欢的速度向新系统添加功能,同时确保旧应用程序继续运行。随着时间的推移,功能迁移到新系统,遗留系统最终被“绞杀”,不再需要。 一旦这个过程完成,遗留系统就可以安全地退休。 17 | 18 | ## 问题和注意事项 19 | 20 | * 考虑如何处理新系统和旧系统可能使用的服务和数据存储。确保两者都可以并行访问这些资源。 21 | * 构建新的应用程序和服务,以便在未来的绞杀者迁移中轻松拦截和替换它们。 22 | * 在某些时候,迁移完成时,绞杀者门面将会消失,或者演变成遗留客户端的适配器。 23 | * 确保门面跟上迁移。 24 | * 确保门面不会成为单点故障或性能瓶颈。 25 | 26 | ## 何时使用该模式 27 | 28 | 在将后端应用程序渐进迁移到新架构时,可以使用此模式。 29 | 这种模式可能不适于以下场景: 30 | * 当不能拦截后台系统的请求时。 31 | * 完整更换的复杂性较低的小系统。 32 | 33 | ## 相关指南 34 | 35 | * [反腐层模式](anti-corruption-layer.md) 36 | * [网关路由模式](gateway-routing.md) -------------------------------------------------------------------------------- /patterns/throttling.md: -------------------------------------------------------------------------------- 1 | # 限流模式 2 | 3 | 控制应用程序,个人租户或整个服务的实例的资源消耗。这可以使系统继续运行并符合SLA(服务水平协议),即使需求增加对资源造成极大的负担。 4 | 5 | ## 背景和问题 6 | 7 | 云应用程序的负载通常随时间而变化,这取决于活跃用户的数量或者正在执行的活动类型。例如,更多用户可能在工作时间内处于活跃状态,或者系统可能需要在每个月底执行昂贵的分析计算。活动中也可能出现突发和意外的爆发。如果系统的处理要求超过了可用资源的容量,会导致性能降低,甚至失败。如果系统必须达到约定的服务水平,这种失败可能是不可接受的。 8 | 9 | 根据应用程序的业务目标,有很多策略可以处理云中的不同负载。一种策略是在任何给定时间使用自动伸缩功能置备与用户需求相匹配的资源。这有可能始终满足用户需求,同时优化运行成本。然而,虽然自动伸缩可以触发附加资源的置备,但它不是即时的。如果需求快速增长,可能会出现资源短缺的时间窗口。 10 | 11 | ## 解决方案 12 | 13 | 自动伸缩的另一种策略是限制应用程序能使用的资源,在达到限制时限流。系统应该监控资源的使用情况,以便当使用率超过阈值时,可以阻止来自一个或多个用户的请求。这将使系统能够继续运行并满足任何已实施的服务水平协议(SLA)。有关监控资源使用情况的更多信息,请参阅[仪器与遥测指南](https://msdn.microsoft.com/library/dn589775.aspx)。 14 | 系统可以实施多种节流策略,其中包括: 15 | 16 | * 在给定的时间内,拒绝每秒超过n次的请求的速率访问系统API的个人用户。这要求系统计算每个租户或运行应用程序的用户的资源使用情况。相关内容请参阅[服务计量指南](https://msdn.microsoft.com/library/dn589796.aspx)。 17 | * 禁用或降低次要服务的功能,让基本服务有足够的资源不受阻碍地运行。例如,如果应用程序是流视频输出,则可以切换到较低的分辨率。 18 | * 使用负载分级来平滑活动量(关于此方法更多内容在[基于队列的负载均衡模式](queue-based-load-leveling.html)有更详细地介绍)。在多租户环境中,这种方法将降低每个租户的性能。如果系统必须支持具有不同SLA的租户组合,高价值租户的工作可能会立即执行。其它租户的要求可以阻止,在积压减轻时处理。[优先级队列模式](priority-queue.html)可用于帮助实现此方法。 19 | * 延迟执行代表优先级较低的应用程序或租户执行的操作。这些操作可以暂停或限制,产生异常并通知租户系统忙碌并且稍后应重试该操作。 20 | 21 | 该图显示了使用三种特性的应用程序的资源使用的区域图(内存,CPU,带宽和其它因素的组合)与时间的关系。特性是功能区域,例如执行特定任务集的组件,执行复杂计算的代码片段或提供诸如内存中缓存服务的元素。这些特征标记为A,B和C. 22 | 23 | ![](https://docs.microsoft.com/en-us/azure/architecture/patterns/_images/throttling-resource-utilization.png) 24 | 25 | 功能线下方的区域表示应用程序在调用此功能时使用的资源。例如,功能A的线下方区域显示了正在使用功能A的应用程序消耗的资源,而功能A和功能B的线之间的区域表示应用程序调用功能B所使用的资源。汇总区域针对每个功能,显示系统的总资源使用情况。 26 | 27 | 上图说明了延迟操作的效果。就在T1之前,分配给使用这些功能的所有应用程序的总资源达到阈值(资源使用限制)。此时应用程序有可能耗尽可用资源。该系统功能B没有功能A或功能C重要,因此暂时禁用了该功能,并释放了它所使用的资源。在T1和T2之间,使用功能A和功能C的应用程序正常运行。最终,这两个功能的资源利用减少到当时间T2有足够的能力来再次启用功能B的时候。 28 | 自动伸缩和限流方法也可以组合起来,以帮助应用程序将响应保持在SLA内。如果需求预期保持高位,限流在系统水平扩展时提供临时解决方案。此时,可以恢复系统的全部功能。 29 | 下图显示了系统中运行的所有应用程序随时间的整体资源使用情况的区域图,并说明了如何将限流与自动伸缩相结合。 30 | 31 | ![](https://docs.microsoft.com/en-us/azure/architecture/patterns/_images/throttling-autoscaling.png) 32 | 33 | 在时间T1,达到指定资源使用软限制的阈值。此时系统可以开始水平扩展。但是,如果新的资源不能快速可用,那么现有资源可能会耗尽,系统可能会失败。为了防止这种情况发生,系统暂时被限流,如前所述。当自动伸缩完成并且有额外的资源可用时,限流可以放宽。 34 | ## 问题和注意事项 35 | 36 | 在决定如何实现此模式时,您应该考虑以下几点: 37 | 38 | * 应用程序限流和使用的策略的架构决策是影响系统整个设计。在应用程序设计早期就应该考虑限流,因为系统实现后再添加就不太容易了。 39 | * 限流必须快速执行。系统必须能检测活动的增加并相应地作出反应。在负载缓解之后,系统还必须快速恢复到原来的状态。这要求不断捕获和监视适当的性能数据。 40 | * 如果服务需要临时拒绝用户请求,则应返回特定的错误代码,以便客户端应用程序了解拒绝执行操作的原因是由于限流。客户端应用程序可以等待一段时间再重试请求。 41 | * 系统自动伸缩时可以使用限流作为临时措施。在某些情况下,如果发生突发事件,并且延续时间不会很长,那么最好只是限流而不是缩放,因为伸缩会大大增加运行成本。 42 | * 如果系统自动调整时将限流用作临时措施,而资源需求增长非常快,即使在节流模式下运行,系统也可能无法继续运行。如果不能接受,请考虑维持更大的容量储备并配置更积极的自动伸缩。 43 | 44 | ## 何时使用该模式 45 | 46 | 使用此模式: 47 | * 确保系统继续符合服务水平协议(SLA)。 48 | * 防止单一租户垄断应用程序提供的资源。 49 | * 处理突发事件。 50 | * 通过限制保持其运行所需的最大资源水平来帮助优化系统成本。 51 | 52 | ## 案例 53 | 54 | 最后一个数字说明如何在多租户系统中实现限流。来自每个租户企业的用户访问云端托管的应用程序,他们填写并提交调查。该应用程序包含监视这些用户向应用程序提交请求的速率的工具。 55 | 56 | 为了防止一个租户的用户影响所有其它用户的应用程序的响应能力和可用性,对每个租户可以提交的用户每秒的请求数量进行限制。超出限制的请求应用程序会阻止。 57 | 58 | ![](https://docs.microsoft.com/en-us/azure/architecture/patterns/_images/throttling-multi-tenant.png) 59 | 60 | ## 相关模式和指南 61 | 62 | 以下模式和指南在实现此模式时也可能相关: 63 | 64 | * [仪表与遥测指南](https://msdn.microsoft.com/library/dn589775.aspx)。调节取决于收集的关于服务使用量的信息。该指南描述如何生成和捕获自定义监视信息。 65 | * [服务度量指南](https://msdn.microsoft.com/library/dn589796.aspx)。描述如何度量服务的使用,以了解如何使用。该指南可用于确定如何限制服务。 66 | * [自动伸缩指南](https://msdn.microsoft.com/library/dn589774.aspx)。节流可以作为临时措施用于系统自动伸缩,或者不需要系统自动调整。该指南包含有关自动伸缩策略的信息。 67 | * [基于队列的负载均衡模式](queue-based-load-leveling.md)。基于队列的负载均衡是实现限流的常用机制。队列可以充当缓冲区,有助于将应用程序发送的请求的速率均匀地传递到服务。 68 | * [优先级队列模式](priority-queue.md)。系统可以使用优先级排队作为其节流策略的一部分,以维护关键或更高价值应用程序的性能,同时降低次要应用程序的性能。 -------------------------------------------------------------------------------- /patterns/valet-key.md: -------------------------------------------------------------------------------- 1 | # 代客密钥模式 2 | 3 | 使用令牌为客户提供特定资源的受限的直接访问权限,以便从应用程序卸载数据传输。这对于在云上托管存储系统和队列的应用及其有用,并且可以最小化成本并最大化可伸缩性和性能。 4 | 5 | ## 背景和问题 6 | 7 | 客户端程序和Web浏览器经常需要读取和写入应用程序存储中的文件或数据流。通常情况下,应用程序将处理数据的移动-从存储中获取数据并将其传输到客户端,或者通过读取客户端上传的数据流并将其保存在数据存储中。但是,这种方式消耗了宝贵的服务器资源,如计算,内存和带宽。 8 | 9 | 数据存储能够直接处理数据的上传和下载,不需要应用程序执行任何处理来移动这些数据。但是,这通常要求客户端有权访问数据存储的安全凭证。这可能是一个有用的技术,来最大限度地减少数据传输成本和扩展应用程序的要求,并最大限度地提高性能。这同时意味着应用程序不再能够管理数据的安全性。客户端连接到数据存储以进行直接访问后,应用程序不能充当守门人。它不再控制流程,也不能防止后续上传或从数据存储下载。 10 | 11 | 在分布式系统中为不可信的客户端提供服务不是一个现实的方法。相反,应用程序必须能够以细粒度的方式安全地控制对数据的访问,但通过设置此连接,然后允许客户端直接与数据存储进行通信以执行所需的读取或写入操作,仍然可以减少服务器上的负载。 12 | 13 | ## 解决方案 14 | 15 | 需要解决数据存储的访问控制问题,存储无法管理客户端的身份验证和授权。一种典型的解决方案是限制对数据存储的公共连接的访问​​,并为客户端提供数据存储可以验证的密钥或令牌。 16 | 17 | 这个密钥或令牌通常称为代客密钥。它提供对特定资源的限定时间访问,并只允许预定义的操作,如读取和写入存储或队列,或在Web浏览器中上传和下载。应用程序可以快速方便地创建和发布客户端设备和Web浏览器的代客密钥,从而允许客户端执行所需的操作,而无需应用程序直接处理数据传输。这消除了应用程序和服务器的处理开销以及对性能和可伸缩性的影响。 18 | 19 | 客户端使用此令牌在限定时间内访问数据存储中的特定资源,访问权限也有特定的限制,如下图所示。在指定的时间段之后,密钥变无效,并且不允许访问资源。 20 | 21 | ![](https://docs.microsoft.com/en-us/azure/architecture/patterns/_images/valet-key-pattern.png) 22 | 23 | 也可以配置具有其它依赖的密钥,如数据的范围。例如,根据数据存储能力,密钥可以在数据存储中指定一个完整的表,或者只在表中指定特定的行。在云存储系统中,密钥可以指定一个容器,或者只是一个容器中的特定项目。 24 | 25 | 应用程序可以废止密钥。这是一个有用的方法,如果客户端通知服务器数据传输操作完成。服务器可以使该密钥无效以阻止进一步的操作。 26 | 27 | 使用此模式可以简化对资源的访问管理,因为不需要创建和验证用户,授予权限,然后再删除用户。它还可以很容易地限制位置,权限和有效期-只需在运行时生成密钥即可。比较重要的是尽可能严格地限制资源的有效期,尤其是资源的位置,使得接收方只能将其用于预期的目的。 28 | ## 问题和注意事项 29 | 30 | 在决定如何实现这种模式时,请考虑以下几点: 31 | 32 | **管理密钥的有效状态和期限**。如果被泄露或破解,密钥有效地解锁目标项目,并使其在有效期内被恶意使用。通常可以撤销或禁用密钥,具体取决于密钥的发放方式。服务器端策略可以更改,或者可以使其签名的服务器密钥失效。指定一个短的有效期限,以尽可能减少对数据存储进行未经授权的操作的风险。但是,如果有效期太短,客户可能无法在密钥过期之前完成操作。如果需要多次访问受保护资源,允许授权用户在有效期到期前更新密钥。 33 | 34 | **控制密钥将提供的访问级别**。通常,密钥应允许用户仅执行完成操作所必需的操作,例如,如果客户端不能将数据上载到数据存储区,则只读访问。对于文件上传,通常指定提供只写权限的密钥以及位置和有效期限。准确地指定密钥适用的资源或资源集合至关重要。 35 | 36 | **考虑如何控制用户的行为**。实现这种模式意味着某些对用户授权访问的资源的控制权的丧失。可以施加的控制级别受限于可用于服务或目标数据存储的策略和权限的能力。例如,通常不可能创建一个限制要写入存储的数据大小的密钥,或者可以使用该密钥访问文件的次数。这可能会导致数据传输的巨大的意外成本,即使是由预期的客户端使用,也可能是由于导致重复上传或下载的代码错误引起的。要限制文件上传的次数,在可能的情况下强制客户端在一个操作完成时通知应用程序。例如,一些数据存储会引发应用程序代码可以用来监视操作和控制用户行为的事件。但是,在一个租户的所有用户使用相同密钥的多租户方案中,很难为单个用户强制执行配额。 37 | 38 | **验证所有上传的数据并进行选择性清理**。有权访问密钥的恶意用户可以上传危害系统的数据。或者,授权用户可能会上传无效的数据,处理后可能会导致错误或系统故障。为了防止这种情况,请确保所有上传的数据在使用前都经过验证并检查是否有恶意内容。 39 | 40 | **审计所有操作**。许多基于密钥的机制都可以记录上传,下载和失败等操作。这些日志通常可以包含在审计过程中,如果用户根据文件大小或数据量收费,也可以用于计费。使用日志来检测可能由密钥提供者的问题导致的身份验证失败,或者意外删除存储的访问策略。 41 | 42 | **安全地传送密钥**。它可以嵌入到用户在网页中激活的URL中,也可以在服务器重定向操作中使用,以便自动下载。始终使用HTTPS通过安全通道传送密钥。 43 | 44 | **保护传送中的敏感数据**。通过应用程序传递的敏感数据通常会使用SSL或TLS进行传输,并且应通过安全传输加强客户端执行直接访问数据存储。 45 | 46 | 其它需要注意的问题包括: 47 | 48 | * 如果客户端没有或不能通知服务器完成操作,且唯一的限制是密钥的有效期限,则应用程序将不能执行审计操作,例如计数上传或下载,或防止多次上传或下载。 49 | * 密钥生成的策略的灵活性可能是有限的。例如,有些机制只允许使用定时到期。其他人无法指定足够的读/写权限的粒度。 50 | * 如果指定了密钥或令牌有效期的开始时间,请确保它比当前服务器时间稍早,以允许可能略微不同步的客户端时钟。如果未指定,则默认通常是当前服务器时间。 51 | * 包含密钥的URL将被记录在服务器日志文件中。密钥通常会在日志文件用于分析之前过期,但请确保限制访问。如果日志数据传输到监控系统或存储在其它位置,请考虑实施延迟以防止密钥泄漏,直到其有效期到期。 52 | * 如果客户端代码在Web浏览器中运行,则浏览器可能需要支持跨源资源共享(CORS),以便在Web浏览器中执行的代码能够访问与服务页面不同的域中的数据。一些较旧的浏览器和某些数据存储不支持CORS,而在这些浏览器中运行的代码可能能够使用代客密钥来访问不同域中的数据(例如云存储帐户)。 53 | 54 | ## 何时使用该模式 55 | 56 | 该模式适用于以下场景: 57 | * 最大限度地减少资源负载并使性能和可伸缩性最大化。使用代客密钥不需要锁定资源,不需要远程服务器调用,可以发出的代客密钥数量没有限制,并且避免了应用程序代码通过执行数据传输而导致的单点故障。创建代客密钥通常是一个使用密钥对字符串进行签名的简单密码操作。 58 | * 尽量减少运维成本。启用直接访问存储和队列的资源和成本性价比比较高,可以减少网络往返次数,并可能减少所需的计算资源数量。 59 | * 客户定期上传或下载数据时,特别是在卷数量较大或每次操作涉及大型文件的情况下。 60 | * 当应用程序可用计算资源有限时,无论是由于主机限制还是成本考虑。在这种情况下,如果存在许多并发数据上传或下载,则该模式更有帮助,因为它可以减轻应用程序处理数据传输的负担。 61 | * 数据存储在远程数据存储或其它数据中心时。如果要求应用程序充当守门人,在数据中心之间或在客户端与应用程序之间的公共或专用网络之间,然后在应用程序与数据存储区之间需要传输数据的附加带宽。 62 | 63 | 该模式可能不适用于以下场景: 64 | 65 | * 如果应用程序必须在数据存储之前或在将数据发送到客户端之前对数据执行一些任务。例如,如果应用程序需要执行验证,则记录成功访问或对数据执行转换。但是,一些数据存储和客户端能够协商并进行简单的转换,例如压缩和解压缩(例如,网页浏览器通常可以处理GZip格式)。 66 | * 如果现有应用程序的设计难以集成模式。使用这种模式通常需要采用不同的架构方法来发送和接收数据。 67 | * 如果需要维护审计跟踪或控制执行数据传输操作的次数,并且正在使用的代客密钥机制不支持服务器可用来管理这些操作的通知。 68 | * 如果有必要限制数据的大小,特别是在上传时。唯一的解决办法是让应用程序在操作完成后检查数据大小,或者在指定的时间段或按照预定的时间检查上传的大小。 69 | 70 | ## 案例 71 | 72 | Azure支持Azure存储上的共享访问签名,用于对Blob,表和队列中的数据以及Service Bus队列和主题进行细粒度访问控制。共享访问签名令牌可以配置为向特定表提供特定的访问权限,例如读取,写入,更新和删除;表格中的键范围;一个队列;一个blob;或一个blob容器。有效期可以是指定的时间段,也可以是没有时间限制。 73 | 74 | Azure共享访问签名还支持服务器存储的访问策略,这些访问策略可以与特定的资源(如表或Blob)相关联。与应用程序生成的共享访问签名令牌相比,此功能提供了额外的控制和灵活性,应尽可能使用。在服务器存储的策略中定义的设置可以更改并反映在令牌中,不需要发出新的令牌,但是令牌中定义的设置不会在未发出新令牌的情况下更改。这种方法还可以在有效的共享访问签名令牌过期之前撤消。 75 | 76 | > 详细内容请参阅在MSDN上[引入表SAS(共享访问签名),队列SAS和更新Blob](https://blogs.msdn.microsoft.com/windowsazurestorage/2012/06/12/introducing-table-sas-shared-access-signature-queue-sas-and-update-to-blob-sas/),[SAS和使用共享访问签名](https://azure.microsoft.com/documentation/articles/storage-dotnet-shared-access-signature-part-1/)。 77 | 78 | 以下代码显示如何创建有效时间为五分钟的共享访问签名令牌。 `GetSharedAccessReferenceForUpload`方法返回可用于将文件上传到Azure Blob存储的共享访问签名标记。 79 | ```C# 80 | public class ValuesController : ApiController 81 | { 82 | private readonly CloudStorageAccount account; 83 | private readonly string blobContainer; 84 | ... 85 | /// 86 | /// Return a limited access key that allows the caller to upload a file 87 | /// to this specific destination for a defined period of time. 88 | /// 89 | private StorageEntitySas GetSharedAccessReferenceForUpload(string blobName) 90 | { 91 | var blobClient = this.account.CreateCloudBlobClient(); 92 | var container = blobClient.GetContainerReference(this.blobContainer); 93 | 94 | var blob = container.GetBlockBlobReference(blobName); 95 | 96 | var policy = new SharedAccessBlobPolicy 97 | { 98 | Permissions = SharedAccessBlobPermissions.Write, 99 | 100 | // Specify a start time five minutes earlier to allow for client clock skew. 101 | SharedAccessStartTime = DateTime.UtcNow.AddMinutes(-5), 102 | 103 | // Specify a validity period of five minutes starting from now. 104 | SharedAccessExpiryTime = DateTime.UtcNow.AddMinutes(5) 105 | }; 106 | 107 | // Create the signature. 108 | var sas = blob.GetSharedAccessSignature(policy); 109 | 110 | return new StorageEntitySas 111 | { 112 | BlobUri = blob.Uri, 113 | Credentials = sas, 114 | Name = blobName 115 | }; 116 | } 117 | 118 | public struct StorageEntitySas 119 | { 120 | public string Credentials; 121 | public Uri BlobUri; 122 | public string Name; 123 | } 124 | } 125 | ``` 126 | > ValetKey解决方案的完整的示例可从[GitHub]()https://github.com/mspnp/cloud-design-patterns/tree/master/valet-key下载 此解决方案中的ValetKey.Web项目包含上面显示的`ValuesController`类的Web应用程序。 使用此Web应用程序检索共享访问签名密钥并将文件上传到Blob存储的样本客户端应用程序可在`ValetKey.Client`项目中找到。 127 | 128 | ## 相关的模式和指南 129 | 130 | 以下模式和指南在实施这种模式时可能是相关的: 131 | 132 | * 此模式的示例在[GitHub](https://github.com/mspnp/cloud-design-patterns/tree/master/valet-key)上下载。 133 | * [看门人模式](patterns/gatekeeper.md)。此模式可以与代客密钥模式一起使用,通过使用充当客户端与应用程序或服务之间的代理的专用主机实例来保护应用程序和服务。看门人验证和清理请求,并在客户端和应用程序之间传递请求和数据。它可以提供额外的安全层,并减少系统的攻击面。 134 | * [静态内容托管模式](static-content-hosting.md)。介绍如何将静态资源部署到基于云的存储服务,这些存储服务可以将这些资源直接发送给客户端,以减少昂贵计算实例的需求。如果资源不是公开可用的,可以使用代客密钥模式来保护它们。 135 | * [引入表SAS(共享访问签名),队列SAS和更新到Blob SAS](https://blogs.msdn.microsoft.com/windowsazurestorage/2012/06/12/introducing-table-sas-shared-access-signature-queue-sas-and-update-to-blob-sas/) 136 | * [使用共享访问签名](https://azure.microsoft.com/documentation/articles/storage-dotnet-shared-access-signature-part-1/) 137 | * [使用服务总线进行共享访问签名认证](https://azure.microsoft.com/documentation/articles/service-bus-shared-access-signature-authentication/) --------------------------------------------------------------------------------