在我们以task为粒度的分布式系统中,需要对数据做checkpoint,以保证task在重启的时候内存中未被消费完的数据可以保存到文件系统中以防止丢失。
在第一个版本中,我们的task默认使用了task的名字作为任务的ID,同时在做checkpoint时使用diskQueue也是使用相同的方式来生成。直到我们碰到一个需要更新task配置的需求后产生了问题。
为了解决任务更新的问题,我们task的做了使用时间戳作为版本管理,简单就是说:在更新一个任务的配置后,version会替换,这时候会生成一个新的task(旧的任务在消费完数据后会被cleanTask的任务给删除掉)来继续。
在过渡期的时候,即新旧task同时在运行的时候,我们做了重启操作,此时两个任务同时执行了checkpoint操作。而bug的原因是在于,task的ID和diskQueue的ID是不相同的!也就是说两个任务虽然new出两个diskQ,但是两个diskQ会同时向一个文件中写数据,这就导致了文件的损坏。
这里的反思是:对于一个Task任务,如果需要和外部的文件或者其他资源交互时,一定需要保证外部的依赖对于每一个task任务都是唯一的。这里以fileSystem为例子,一个task保证对应的是一个file or dir。两种方式:1.使用一个xxx.lock的方式,一个系统如果已经决定对该资源做write/read操作时,就建立一个lock。该系统内部的进程想要同时做操作时可以避免因为上述简单的ID BUG而造成的问题。同时其他系统可以辨识到该文件可能被其他应用程序使用中,他可以针对这种情况做一些预期内的操作。
- Golang中突然想起一种方案,对于需要写文件或者其他资源访问时,使用一个
channel
来做串行处理。比如当多个不可预知的任务可能同时做一个写入文件操作时,任务可以将此次操作的metadata以一种特定的数据格式交给上层系统(我们定义的channel)来统一处理,因为channel的并发写入是绝对安全的。当然如果是需要对多个文件做写入操作时,我们可以使用这样一种方式: 一个channel对应下游有多个channel(而不是file对象),每个channel都定义一个唯一ID,作为suffix。每个channel写完的文件都有ID,比如A,B,C三个channel现在得到的metadata是:向task.txt中写入数据,如果直接同时写,需要自己控制锁,我们使用task.txt.A task.txt.B task.txt.C的方式来写入,即channel A写入task.txt.A 中,B->B中。 再任务读取时,生成这样的3个channel并发读取到内存的总线channel。
Comments
Post a Comment