TFS问题思考
配置参数
在工程实践中,我们通常把一些需要测量才能确定最佳取值的参数以可配置的形式处理,但实际上大部分的参数可能只是一个可行值,而不是一个最佳值。
1. DS与NS之间的心跳间隔设置:NS通过心跳来确定DS的状态,心跳间隔太短,NS容易出现误判,有时短暂的网络断开也会被NS认定为DS宕机;而心跳间隔过长,NS就不能及时发现宕机的DS,导致的问题是,NS复制丢失block的时间点会被延迟,影响系统的可靠性;同时很多请求仍然被引导到已经宕机的DS上,最终会导致客户端的重试,影响系统的请求响应时间。线上这个值配置为2s,为什么是2s,不是1s,3s,甚至5s、10s,或是根据系统负载状态动态调整。
2. 块大小:64M的block大小是业界的一个经典配置,为什么是64M我也不得而知,可能是因为开山鼻祖GFS是64M吧。后来由于“历史原因”,TFS块大小变成72M,72M的block运行起来也很好,block大小会影响到哪些点呢? block越大,block的元数据相对就越少,一个block内能存放的文件就越多,复制block的时间就会变长。
3. 扩展块大小及数量:扩展block主要用于解决文件更新问题,目前配置扩展块大小为4M,数量是主块的2倍,平均一个主块分配2个扩展块,但实际上线上扩展块使用很少,主要是因为业务更新需求很少,绝大部分的写是新建,而不是更新,也就是说大部分的扩展块都浪费了,是不是应该预留更少的扩展块,万一预留的块太少,当业务需要更新时又满足不了怎么办。 出现上述问题的主要还是设计不够好的原因,如果任意block都能即当主块又能当扩展块使用,问题就迎仍而解了。
4. 集群平均文件大小设置:这个配置决定block中平均能存多少个文件,然后合理为index分配hash桶数,但我发现这个配置在所有集群中、都是40K,包括大文件集群(实际平均文件大小接近2M)。
5. 均衡因子: NS根据DS占用的存储空间比例来做负载均衡,个人认为这样是合理的,对于一台正常的server来说(不是计算密集型或是IO密集型),其各个部件的配置应该是相互协调的,所以存储空间从很大程度上反映了server的计算处理能力,那么通过存储来反映其服务能力也是合理的。目前线上的均衡因子为1%,当某一台server的存储使用率与所有server平均存储使用率之差超过1%时,就要进行block迁移,最终达到一个高度平均的状态,因为条件比较苛刻,所以数据迁移操作很频繁,个人认为设置在5%-10%间更加合理。
6. 复制等待时间:当NS通过心跳机制发现某DS宕机后,会等待一段时间开始复制这个DS上的block,配置等待时间为4min,为什么是4min? google有篇关于可靠性研究的论文之处,GFS大大部分的故障恢复时间在15min之内。TFS为什么要配置成4min,我的理解是DS的重启时间加上一定的预留时间。
集群同步
TFS采用主备集群的方式来实现异地机房容灾,具体实现是在主集群所有的DS(dataserer)上配置备集群NS(nameserver)的信息,主集群上的DS在写、删除、更新文件的操作成功后会写binlog,DS会在后台重放binlog,把这些操作应用到备集群上,从而实现主备集群的数据同步,由于binlog重放是异步的,所以主备集群的数据不能保证实时一致。同步是线上经常出问题的点,而且经常需要人工介入处理。
1. 最初我们按操作重放,某个操作如果同步不成功,会一直重试,整个同步队列会阻塞知道该操作同步成功,这样设计的主要原因在于,很多失败只是暂时性,比如备集群上的NS短暂性繁忙,或是系统升级等,这些问题会在短时间内恢复,所以只要同步一直重试,大部分的操作最终是会成功的,不需要人工介入处理;但显然这个设计存在很多不合理的地方, 比如某个同步操作阻塞后,后续的同步将一直累积,当某一时刻同步恢复后,累积的许多操作就会迅速压到备集群上,影响到备集群的稳定性。
2. 针对上述问题,重试是为了避免临时性失败,那么只要控制重试次数和时间即可,于是把同步的策略改成重试有限次数,并且在重试时把同步文件操作改为同步文件最终状态,进一步提高了同步的成功率。
由于磁盘本身是不可靠的,如果在binlog重放前,存放binlog的磁盘坏了,那么binlog就会丢失,所有未重放的操作也就丢失了;另外对于上面提到重试有限次数仍失败的文件,能否避免每次人工介入处理。为了应付这些异常情况,我们开发了一系列的辅助同步工具,包括集群全量同步工具,集群增量同步工具,block同步工具,文件同步工具,分别在不同的场景运用。比如通过每日的增量同步任务,就能解决那些binlog丢失或是同步失败的情况。
假设binlog不会丢失,有哪些原因会导致同步失败?
1. 主或备集群本身出问题,对同步的影响千奇百怪,但出现的频度不高。
2. 集群内副本不一致,比如某个文件先后进行了写、删除操作,在写操作时,备集群上只有一个副本active,另一个故障或是正在升级等,这时写操作只会应用到一个副本上,当应用删除操作时,另一个副本已经启动并成为主副本,那么接下来应用删除操作时,因为文件在主副本上不存在,所以删除会失败。出现这个的主要原因是对于更新操作,为了提高成功率,即使副本数不足时,也允许更新。
一致性与负载均衡
TFS采用W=N, R=1的强一致性算法,但实际实现的时候,使用了取巧的办法,比如有3个副本,我们只写成功了1个,这时返回给客户端是失败的,但系统不会对写成功的那个副本就行回滚,但因为返回客户端是失败的,客户端也就得不到文件名,也不会访问到这个文件,所以这里的一致只是逻辑上的一致;另外,在删除和更新时,也可能出现部分副本更新成功,部分副本更新失败,最终文件的多个副本是不一致的,但因为系统大部分的文件都是写一次就不会再更改,即更新操作很少,更新失败就更少了,而且上层的业务能接受这种不一致,也没有在这块做更多的工作。后续为了减少不一致带来的不确定性,将会在发现这种不一致时,复制最高版本的block,删除低版本的副本,最终使得所有的副本处于一致的状态。
当读取时,客户端会随机选择一个副本读取文件数据,关于这点,有两种不同意见,有人认为这样是合理的,客户端的读请求会分散到多个副本;有人认为读请求应该始终向主副本请求,首先主副本上的数据一定是最新的,同时又能充分利用linux OS的page cache以减少磁盘IO,因为block的主副本是均衡分布在各个DS上的,所以总体看来各个DS间的负载也是均衡的。
运维
个人认为在大的互联网,运维规范化是至关重要的,最近线上问题中有跟运维相关的问题,因为发现比较及时,没造成什么影响。一是在DS升级的过程中,有一个机器的安装新程序没有成功,导致这台机器只是简单的重启了dataserver,而没有升级程序;另外一个是在添加备集群时,遗漏了port信息,导致上线后,往备集群的同步一直失败。前者可通过在升级时加强检查避免,比如先计算待升级的DS的md5sum,在升级时检查启动的DS md5是否匹配,如果不匹配则给出告警信息;后者则只能通过上线前进行充分的测试来避免。
监控
监控对系统更好的运行是必不可少的,通过监控发现问题,开发解决问题,升级上线,如此往复,使系统不断趋于一个稳定运行的状态。最开始为了满足监控需求,我们开发了admin server,admin server会可以监控ns,ds的运行状态,当发现ds进程crash时(通常是由于程序bug导致进程coredump或是磁盘坏掉等),会自动把进程重启起来,因为如果不自动重启,等人工介入处理,我们需要等待故障发现时间,处理时间,等DS恢复后,可能NS早已把该DS上的block都复制了一份,这时DS再加入已经没有任何意义了。使用这种方法,我们应该对线上的coredump文件进行监控,因为进程coredump后会立即被admin server启动起来工作,监控可能根本发现不了DS出过问题,于是要想开发尽快发现问题,对于新产生的core文件,要能及时通知到开发人员进行调试分析。最近由于不知名的原因,admin server都下线了,甚是怀念,很多重启能解决的问题现在必须人工介入。
工程实现
系统设计和最终工程实现出来的产品,可能多少有些差距,最初的开发人员留下一堆TODO就直接上线了,后面的开发人员不断移除这些TODO,加入自己的代码,而开发经常遇到的一个问题就是,由于之前实现遗留的历史原因,自己不得不加入一些很ugly的代码,这样当下一个开发人员接手时,情况就变得更糟,形成恶性循环;这种问题只能通过统一编码风格,加强开发人员间code review等方式来适当缓解,很难做到完全避免。