用分布式系统思路解决资源受限问题

通过最近的一个小示例,介绍一下分布式系统的特点,和简单实现方式,目的在于使用高级的思路解决实际的问题。

背景

有个任务,将csv格式的数据读入,加工处理,最后输出一个或者多个excel文件。

问题

为了方便使用,将系统迁移至VPS,由于VPS新能问题(配置1核,1G,别问我为啥不升级配置),处理时大文件时,占用内存过多,程序会崩溃

目标

在资源受限的情况下,让程序正常使用

分析

  1. 发现程序崩溃是由于生成大的Excel文件引起的
  2. 单独生成一个大文件不会出现崩溃,但连续生成3个以上的文件会崩溃
  3. 发现系统崩溃时,程序内存占比达到37%,小于37%可以正常运行
  4. 单独执行有大概2~4分钟,内存占比才会下降
  5. 处理此类任务每月不超过2次
  6. 用户对任务的结果的期待不能超过24小时

方案

根据分析,可以采用时间换新能的方式,另外还可以主动释放内存。
具体方案是:将生成多个大Excel文件的过程分解为多个一次只生成一个Excel文件的过程,即子过程,没执行一个子过程,主动调用垃圾回收方法,而要执行一个子过程时,先看内存占比是否小于37%,是执行,否放弃执行,等待下次执行。
于是打算采用异步、分布式处理的方式。

过程

  • 基本功能:
    1. 主任务拆分(生产者)
      主任务要具有可拆分性,就是可以拆分成简单子任务,子任务之间的关联度要尽量小,否则就不能并行执行了,比如任务A,拆分成a,b,c三个子任务,b依赖于a的结果,c依赖于b的结果,那么子任务只能按a、b、c的顺来执行,无法并行,分布式反而会让处理更复杂更慢;还有后续任务需要明确知道前序任务以及前序任务的结果,所以后续任务执行前需要更多的时间和资源创建执行环境。对于只想让拆分任务,分步执行的,子任务间的关联度大小影响不大。
      处理中,使用了循环,生成多个Excel文件,所以很容易拆分成子任务,且子任务之间没有关联。
    2. 子任务队列(产品和篮子)
      队列是分布式方案中很常用的数据结构,方便先进先出,同时方便调度算法的实现。队列的实现方式很多,其实很简单,这里用库表作为存储,每次获取id最小的记录,实现先进先出的效果。 队列中存放什么是关键,需要充分考虑子任务执行中用到的数据,执行方法,尤其是环境变量,很容易遗漏。程序中,处理完所有的子任务,需要将结果打包成zip文件,zip文件名是个环境变量,也需要保存在记录中,见4结果合成。
    3. 队列调度(消费者)
      简单说就是从队列中获取记录的方法,比如一次获取几个,要不要考虑优先级,怎样才能避免一个任务分配给多个执行者等等,就简单的就是每次只取一条。调度算法将拿到的记录分配给执行者。一般对于队列来说,除了队列就从队列中删除了,我采用了库表做存储,并且想作为执行日志来用,所以采用标记的方式,即获取到的记录打上已分配的标签,表示这个记录已经被取走,调度程序就不会再分配它了。
    4. 结果组合
      分布式执行必须要有这一步,1为表示主任务完成了,2 将子任务执行的结果组合起来作为主任务的结果。
      由于分布式异步执行的原因,不能预测主任务的完成时间,也无法预知最后一个子任务是谁,执行者执行完一个子任务后,需要看一下自己是否是最后一个完成的,如果是就调用结果组合方法。
      程序中,合成方法会将所有子任务产生的文件,打成zip包,最后一个子任务负责打包,由于不知道谁是最后一个,所以每个子任务都会zip包的名字。好傻哈哈,如果任务分配后还需要变更某些变量时,就得更新每个子任务记录了,更优雅的做法是设计一个主任务记录,将zip包文件名,以及和主任务相关的数据记录在这里,当子任务需要时从这里拿就行,不过为了方便实现,没有过多优雅。
  • 还需考虑:
    1. 持久化 —— 断点续传
      断点续传的概念来自于迅雷下载,下载一个大文件,到一半,电脑关机了,下次打开,还可以从关机时下载到的地方继续下载,不必重新开始。分布式处理可能时间长,环境复杂,服务器宕机、重启,服务崩溃等情况不可避免,所以需要记录执行的位置,以便系统恢复后不必重新开始。
      在恢复执行中,有个有意思的地方,先看子任务处理状态图:

      当状态是已分配,系统崩溃了,在执行时,调度程序就会认为这个记录正在被执行,但实际上执行者早随系统死了。于是这个主任务就永远执行不完。其实解决方法很简单:当系统启动时,确认没有任务被执行了,需要对状态为已分配的记录设置状态为未分配,即
    2. 进度反馈
      这个其实挺有个必要的,因为不知道任务什么时候能结束,有个进度提示会人性化很多,当然实现很简单,已完成的子任务数/子任务总数就可以,这里要注意什么叫已完成的子任务数,应该是状态不是未分配的任务个数,这个好处在于不用区分已分配,已完成和存在错误者三个状态了,即将他们归于一类,这样方便些,可能有些不精确的地方,但这里只是显示个大概,所以越简单越好。
    3. 执行中出了问题
      不能保证所有的任务都按预想的那样执行,所以问题总会有的,出了问题需要记录下原因,以便查看和修正,至于是否终止掉整个任务,就看具体场景来定,在我的例子中,子任务之间没有关联,所以就没必要终止任务的执行。无论才有什么策略,都需要在主任务上有所返回,这样就能让使用者及时发现问题。当然更好的做法是有个运维接口,出现问题及时报告给运维人员。
      我的实现中,会见错误信息记录在子任务记录上,并且设置记录的状态为出错,相当于同时充当了日志记录。

概括下要点:

  1. 任务可拆分
  2. 队列调度
  3. 断点恢复
  4. 结果组合
  5. 进度反馈
  6. 错误处理