分布式系统的异步处理流程通常都有哪些设计解决方案

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了分布式系统的异步处理流程通常都有哪些设计解决方案相关的知识,希望对你有一定的参考价值。

参考技术A   开源软件已经成为许多大型网站的基本组成部分,随着这些网站的逐步壮大,他们的网站架构和一些指导原则也出现在开发者们的面前,给予切实有用的指导和帮助。本文旨在介绍一些核心问题以及通过构建模块来制作大型网站,实现最终目标。   这篇文章主要侧重于Web系统,并且也适用于其他分布式系统。   Web分布式系统设计的原则   构建并运营一个可伸缩的Web站点或应用程序到底指的是什么?在最初,仅是通过互联网连接用户和访问远程资源。   和大多数事情一样,当构建一个Web服务时,需要提前抽出时间进行规划。了解大型网站创建背后的注意事项以及权衡可能会给你带来更加明智的决策,当你在创建小网站时。下面是设计大型Web系统时,需要注意的一些核心原则:   1.可用性   2.性能   3.可靠性   4.可扩展   5.易管理   6.成本   上面的这些原则给设计分布式Web架构提供了一定的基础和理论指导。然而,它们也可能彼此相左,例如实现这个目标的代价是牺牲成本。一个简单的例子:选择地址容量,仅通过添加更多的服务器(可伸缩性),这个可能以易管理(你不得不操作额外的服务器)和成本作为代价(服务器价格)。   无论你想设计哪种类型的Web应用程序,这些原则都是非常重要的,甚至这些原则之间也会互相羁绊,做好它们之间的权衡也非常重要。   基础   当涉及到系统架构问题时,这几件事情是必须要考虑清楚的:什么样的模块比较合适?如何把它们组合在一起?如何进行恰当地权衡?在扩大投资之前,它通常需要的并不是一个精明的商业命题,然而,一些深谋远虑的设计可以帮你在未来节省大量的时间和资源。   讨论的重点几乎是构建所有大型Web应用程序的核心:服务、冗余、分区和故障处理能力。这里的每个因素都会涉及到选择和妥协,特别是前面所讨论的那些原则。解释这些核心的最佳办法就是举例子。   图片托管应用程序   有时,你会在线上传图片,而一些大型网站需要托管和传送大量的图片,这对于构建一个具有成本效益、高可用性并具有低延时(快速检索)的架构是一项挑战。   在一个图片系统中,用户可以上传图片到一个中央服务器里,通过网络连接或API对这些图片进行请求,就像Flickr或者Picasa。简单点,我们就假设这个应用程序只包含两个核心部分:上传(写)图片和检索图片。图片上传时最好能够做到高效,传输速度也是我们最关心的,当有人向图片发出请求时(例如是一个Web页面或其他应用程序)。这是非常相似的功能,提供Web服务或内容分发网络(一个CDN服务器可以在许多地方存储内容,所以无论是在地理上还是物理上都更加接近用户,从而导致更快的性能)边缘服务器。   该系统需要考虑的其他重要方面:   1.图片存储的数量是没有限制的,所以存储应具备可伸缩,另外图片计算也需要考虑   2.下载/请求需要做到低延迟   3.用户上传一张图片,那么图片就应该始终在那里(图片数据的可靠性)   4.系统应该易于维护(易管理)   5.由于图片托管不会有太高的利润空间,所以系统需要具备成本效益   图1是个简化的功能图   图1 图片托管系统的简化结构图   在这个例子中,系统必须具备快速、数据存储必须做到可靠和高度可扩展。构建一个小型的应用程序就微不足道了,一台服务器即可实现托管。如果这样,这篇文章就毫无兴趣和吸引力了。假设我们要做的应用程序会逐渐成长成Flickr那么大。   服务   当我们考虑构建可伸缩的系统时,它应有助于解耦功能,系统的每个部分都可以作为自己的服务并且拥有清晰的接口定义。在实践中,这种系统设计被称作面向服务的体系结构(SOA)。对于此类系统,每个服务都有它自己的独特功能,通过一个抽象接口可以与外面的任何内容进行互动,通常是面向公众的另一个服务 API。   把系统分解成一组互补性的服务,在互相解耦这些操作块。这种抽象有助于在服务、基本环境和消费者服务之间建立非常清晰的关系。这种分解可以有效地隔离问题,每个块也可以互相伸缩。这种面向服务的系统设计与面向对象设计非常相似。   在我们的例子中,所有上传和检索请求都在同一台服务器上处理。然而,因为系统需要具备可伸缩性,所以把这两个功能打破并集成到自己的服务中是有意义的。   快进并假设服务正在大量使用;在这种情况下,很容易看到写图片的时间对读图片时间有多大影响(他们两个功能在彼此竞争共享资源)。根据各自体系,这种影响会是巨大的。即使上传和下载速度相同(这是不可能的,对于大多数的IP网络来说,下载速度:上传速度至少是3:1),通常,文件可以从缓存中读取,而写入,最终是写到磁盘中(也许在最终一致的情况下,可以被多写几次)。即使是从缓存或者磁盘(类似SSD)中读取,数据写入都会比读慢(Pole Position,一个开源DB基准的开源工具和结果)。   这种设计的另一个潜在问题是像Apache或者Lighttpd这些Web服务器通常都会有一个并发连接数上限(默认是500,但也可以更多),这可能会花费高流量,写可能会迅速消掉所有。既然读可以异步或利用其他性能优化,比如gzip压缩或分块传输代码,Web服务可以快速切换读取和客户端来服务于更多的请求,超过每秒的最大连接数(Apache的最大连接数设置为500,这种情况并不常见,每秒可以服务几千个读取请求)。另一方面,写通常倾向于保持一个开放的链接进行持续上传,所以,使用家庭网络上传一个1 MB的文件花费的时间可能会超过1秒,所以,这样的服务器只能同时满足500个写请求。   图2:读取分离   规划这种瓶颈的一个非常好的做法是把读和写进行分离,如图2所示。这样我们就可以对它们单独进行扩展(一直以来读都比写多)但也有助于弄明白每个点的意思。这种分离更易于排除故障和解决规模方面问题,如慢读。   这种方法的优点就是我们能够彼此独立解决问题——在同种情况下,无需写入和检索操作。这两种服务仍然利用全球语料库的图像,但是他们可以自由地优化性能和服务方法(例如排队请求或者缓存流行图片——下面会介绍更多)。从维护和成本角度来看,每一个服务都可以根据需要独立进行扩展,但如果把它们进行合并或交织在一起,那么有可能无意中就会对另一个性能产生影响,如上面讨论的情景。   当然,如果你有两个不同的端点,上面的例子可能会运行的很好(事实上,这非常类似于几个云存储供应商之间的实现和内容分发网络)。虽然有很多种方法可以解决这些瓶颈,但每个人都会有不同的权衡,所以采用适合你的方法才是最重要的。   例如,Flickr解决这个读/写问题是通过分发用户跨越不同的碎片,每个碎片只能处理一组用户,但是随着用户数的增加,更多的碎片也会相应的添加到群集里(请参阅Flickr的扩展介绍)。在第一个例子中,它更容易基于硬件的实际用量进行扩展(在整个系统中的读/写数量),而Flickr是基于其用户群进行扩展(but forces the assumption of equal usage across users so there can be extra capacity)。而前面的那个例子,任何一个中断或者问题都会降低整个系统功能(例如任何人都没办法执行写操作),而Flickr的一个中断只会影响到其所在碎片的用户数。在第一个例子中,它更容易通过整个数据集进行操作——例如,更新写服务,包括新的元数据或者通过所有的图片元数据进行搜索——而 Flickr架构的每个碎片都需要被更新或搜索(或者需要创建一个搜索服务来收集元数据——事实上,他们就是这样做的)。   当谈到这些系统时,其实并没有非常正确的答案,但有助于我们回到文章开始处的原则上看问题。确定系统需求(大量的读或写或者两个都进行、级别并发、跨数据查询、范围、种类等等),选择不同的基准、理解系统是如何出错的并且对以后的故障发生情况做些扎实的计划。   冗余   为了可以正确处理错误,一个Web架构的服务和数据必须具备适当的冗余。例如,如果只有一个副本文件存储在这台单独的服务器上,那么如果这台服务器出现问题或丢失,那么该文件也随即一起丢失。丢失数据并不是什么好事情,避免数据丢失的常用方法就是多创建几个文件或副本或冗余。   同样也适用于服务器。如果一个应用程序有个核心功能,应确保有多个副本或版本在同时运行,这样可以避免单节点失败。   在系统中创建冗余,当系统发生危机时,如果需要,可以消除单点故障并提供备份或备用功能。例如,这里有两个相同的服务示例在生产环境中运行,如果其中一个发生故障或者降低,那么该系统容错转移至那个健康的副本上。容错转移可以自动发生也可以手动干预。   服务冗余的另一重要组成部分是创建一个无共享架构。在这种体系结构中,每个节点都能相互独立运行,并且没有所谓的中央“大脑”管理状态或协调活动其他节点。这对系统的可扩展帮助很大,因为新节点在没有特殊要求或知识的前提下被添加。然而,最重要的是,这些系统是没有单点故障的,所以失败的弹性就更大。   例如在我们的图片服务器应用程序中,所有的图片在另一个硬件上都有冗余副本(理想情况下是在不同的地理位置,避免在数据中心发生一些火灾、地震等自然事故),服务去访问图片将被冗余,所有潜在的服务请求。(参见图3:采用负载均衡是实现这点的最好方法,在下面还会介绍更多方法)   图3 图片托管应用程序冗余   分区   数据集有可能非常大,无法安装在一台服务器上。也有可能这样,某操作需要太多的计算资源、性能降低并且有必要增加容量。在这两种情况下,你有两种选择:纵向扩展或横向扩展。   纵向扩展意味着在单个服务器上添加更多的资源。所以,对于一个非常大的数据集来说,这可能意味着添加更多(或更大)的硬件设备,来使一台服务器能容下整个数据集。在计算操作下,这可能意味着移动计算到一个更大的服务器上,拥有更快的CPU或更大的内存。在各种情况下,纵向扩展可以通过提升单个资源的处理能力来完成。   横向扩展在另一方面是添加更多的节点,在大数据集下,这可能会使用第二服务器来存储部分数据集,对于计算资源来说,这意味着分割操作或跨节点加载。为了充分利用横向扩展,它应作为一种内在的系统架构设计原则,否则修改或拆分操作将会非常麻烦。   当谈到横向扩展时,最常见的做法是把服务进行分区或碎片。分区可以被派发,这样每个逻辑组的功能就是独立的。可以通过地理界限或其他标准,如非付费与付费用户来完成分区。这些方案的优点是他们会随着容量的增加提供一个服务或数据存储。   在我们的图片服务器案例中,用来存储图片的单个文件服务器可能被多个文件服务器取代,每个里面都会包含一套自己独特的图像。(见图4)这种架构将允许系统来填充每一个文件/图片服务器,当磁盘填满时会添加额外的服务器。这样的设计需要一个命名方案,用来捆绑图片文件名到其相应的服务器上。图像名字可以形成一个一致的哈希方案并映射到整个服务器上;或者给每张图片分配一个增量ID,当客户端对图片发出请求时,图片检索服务只需要检索映射到每个服务器上(例如索引)的ID。   图4 图片托管应用程序冗余和分区   当然,跨越多个服务器对数据或功能进行分区还是有许多挑战的。其中的关键问题是数据本地化。在分布式系统中,数据操作或计算点越接近,系统性能就会越好。因此,它也可能是个潜在问题,当数据分散在多个服务器上时。有时数据不是在本地,那么就要迫使服务器通过网络来获取所需的信息,这个获取的过程就会设计到成本。   另一潜在问题是不一致。当这里有多个服务对一个共享资源执行读写操作时,潜在可能会有另一个服务器或数据存储参与进来,作为竞选条件——一些数据需要更新,但是读的优先级高于更新——在这种情况下,数据就是不一致的。例如在图片托管方案中,有可能出现的不一致是:如果一个客户端发送更新“狗”图片请求,进行重新命名,把“Dog”改成“Gizmo”,但同时,另一个客户端正在读这张图片。在这种情况下,标题就是不清楚的。“Dog”或“Gizmo” 应该被第二个客户端接收。   当然,在进行数据分区时会产生一些障碍,但是分区允许把每个问题拆分到管理群里——通过数据、负载、使用模式等。这样对可扩展和易管理都是有帮助的,但也不是没有风险的。这里有很多方式来降低风险和故障处理;然而,为了简便起见,并未在本文中详细说明,如果你有兴趣,可以访问我的博客。   总结   以上介绍的都是设计分布式系统需要考虑的核心要素。可用性、性能、可靠性、可扩展、易管理、成本这几个原则非常重要,但在实际应用中可能会以牺牲某个原则来实现另外一个原则,在这个过程中就要做好权衡工作,做到因时制宜。   在下面的构建分布式系统实战中,我们将会深入介绍如何设计可扩展的数据访问,包括负载均衡、代理、全局缓存、分布式缓存等。   英文地址:Dr.Dobb's   文:CSDN

分布式一致性的一种方案

背景


对于服务化的系统,一个请求涉及多个服务的调用是司空见惯的。如果是涉及到多个服务写的请求,如何保证多个服务写状态的一致性一直是服务化系统面对的一个技术难题。本文会介绍笔者之前参与设计的一种解决该问题的方案-异步任务框架。

框架思想

框架主要思想是异步+足够次数的重试+补偿。

异步:指请求处理流程异步化。多服务之间的调用都是通过,网络调用可能超时,由超时到终态(成功或失败)这个转换过程的时间并不能得到保证。而每次请求时长都是有限制的,同步的话请求时长不能得到保证了。

足够次数的重试:出现超时时,一般我们会设置重试次数以求得到确定的结果。其实这儿是有风险的,一般我们重试是针对网络超时的,还有一种是上游服务代码性能很差(也许超时时间不合理)或者系统资源不足导致服务端响应不及时,这个时候如果系统没有设置服务降级和隔离策略,重试会导致系统负载更高,最终导致雪崩。本框架假设业务开发人员已掌握相关知识。本框架的重试策略也是针对网络超时,并且乐观的认定重试足够次数(默认无限次)肯定可以得到一个终态。网络超时短时间内重试是没有意义的,所以重试间隔是指数级增长。

补偿:一个请求会涉及到多个服务的写操作。如果当前一步执行失败了,之前的操作就需要回滚。称之为补偿。

框架核心设计

架构图,略了,要现画,还要洗澡睡觉觉呢。

该框架也是以SDK的方式实现,需要业务方建一张辅助表,表结构参照下方AsyncTaskDTO的属性。接下来以下单请求为例,介绍一下本框架。

一个简单的下单流程如下:更新限购(1,1)-> 预占库存(1,2)->订单数据落地(1,3)->通知支付(1,4)。


本框架把这一个请求抽象成一个流程,一个流程的多个写步骤抽象成流程的一个任务,就如上面小括号标记的。每一步需要实现框架定义的任务处理器接口:

/** * 异步任务处理器接口 * * @author jacky * @created 2017/5/9 */public interface IAsyncTaskProcessor { /**     * 处理对应的任务, 也就是业务方这一步具体要做的事情,该方法要能保证幂等 *     * @param task * @return */    public AsyncTaskRunResult process(AsyncTaskParam task);

/**     * 当前任务信息 * * @return */    public TaskTaskDTO currentTaskInfo();

/** * 获取运行的线程池, 返回空时使用默认的 * * @return */ public default Executor getExecutor() { return null; }}

process方法参数AsyncTaskParam:

 // 关联的业务数据,建议只存储id private String bizData = StringUtils.EMPTY;    // 流程id    private int flowId;    // 任务id    private int taskId; // 可执行时间 private Date executeTime = new Date();

返回值AsyncTaskRunResult:

 // 结果    private AsyncTaskRunResultEnum result; // 失败或重试的中止原因 private String terminateReason;    // 后续步骤    private List<AsyncTaskDTO> nextTask;

AsyncTaskRunResultEnum:

SUC(1, "成功"), FAIL(2, "失败"), RETRY(3, "重试");

表结构参照AsyncTaskDTO:


// 任务表id private long id; // 业务参数,建议只存储id private String bizData = StringUtils.EMPTY;
// 流程id private int flowId; // 任务id private int taskId;
// 尝试次数 private int tryNum = 0;
// 执行时间 private Date executeTime = new Date();
// 状态    private int status = AsyncTaskStatusEnum.WAITING.getValue(); // 终止原因 private String terminateReason = StringUtils.EMPTY;
// 处理节点,目前是ip private String node = StringUtils.EMPTY; // 机器分组,目前无用 private String group = StringUtils.EMPTY; // 扩展信息 private String extInfo = StringUtils.EMPTY;
// 添加时间 private Date addTime; // 修改时间 private Date modTime;

AsyncTaskStatusEnum:

WAITING(1"待处理"), PROCESSING(2"处理中"), SUC(3"处理成功"), FAIL(4"处理失败");

当服务启动时,异步任务框架会扫描该服务中的业务任务处理器Bean。详细点就是通过Spring的ApplicationContext,拿到IAsyncTaskProcessor类型的所有Bean。然后借助currentTaskInfo方法,将bean缓存。外层key是flowId,内层key是taskId,value是这一步对应的processor。

 /**     * 任务处理器map */    private Map<Integer, Map<Integer, IAsyncTaskProcessor>> processorMap = Maps.newConcurrentMap();

任务的触发分为两种。一种是业务方主动调用。一种是定时扫描异步任务表执行。

业务方主动调用,框架根据业务方传入的flowId和taskId找到相应的bean执行其process方法。process方法的返回值要告诉框架两个信息:

  • 这一步任务的执行结果。失败,成功还是重试。重试的话,定时任务1ms后会再次调起该处理器。

  • 下一步要执行哪个流程的哪一个任务。

服务启动时也会扫描表里待执行和未执行的任务,触发相应的处理器。

execute核心代码如下:

 /** * 提交一个任务至线程池中执行 *     * @param taskId */    public void execute(long taskId) { executorPool.execute(new Runnable() { @Override public void run() {                AsyncTaskDTO task = asyncTaskService.queryById(taskId); if (null == task) { return; }
// 未到可执行时间 if (null != task.getExecuteTime() && new Date().before(task.getExecuteTime())) { return; }
// 查找任务对应的处理器bean                IAsyncTaskProcessor processor = queryTaskProcessor(task); if (null == processor) { LOG.error("任务查找不到对应的处理器,task id={}", taskId); return; }
// 优先使用执行器里指定的线程池 if (null == processor.getExecutor()) {                    startAndRunTask(processor, task); } else { processor.getExecutor().execute(new Runnable() { @Override public void run() { startAndRunTask(processor, task); } }); } } }); }

异步任务表的数据会定期删除。

回顾

可能有读者会问为啥不用现成的分布式事务框架呢?

根据当时的测试异步任务框架性能要比当时猫眼用的分布式任务框架性能要高,而且据说和业务代码耦合也很严重。

这个异步任务框架依然很粗糙,就实践来讲存在如下问题:

  • 随着业务迭代,会存在processor爆炸的问题

  • 每个任务和机器是绑定的。如果涉及到迁机器,相应的需要迁移数据。如果服务长时间宕机,绑定该服务节点的任务无法执行。

这个框架本可以做的更好,但是由于时间和精力问题一直没有迭代和维护。其实挺遗憾的。


以上是关于分布式系统的异步处理流程通常都有哪些设计解决方案的主要内容,如果未能解决你的问题,请参考以下文章

分布式事务详解

SpringSecurity整合JWT

分布式一致性的一种方案

聊聊数据库分布式事务

一文让你搞懂分布式事务

常用的大数据工具都有哪些?