架构导读
简介
MooseFS是一个容错性的文件系统,存储在它上面的文件分布在多个物理服务器上,用户可以像对待类unix文件系统一样操作moosefs文件系统:
- 层级结构(目录树)
- POSIX文件属性(ermissions, last access and modification times)
- 支持特殊文件(块设备,字符设备,管道,套接字)
- 支持符号链接和硬链接
- 支持通过IP地址或者密码对文件系统进行访问控制
MooseFS特性:
- 高可用(多份数据副本分别存储在不同服务器上)
- 可以增加服务器或者磁盘动态扩展容量
- 支持垃圾箱功能,删除的文件可以保持一段时间,以供恢复使用
- Coherent snapshots of files, even while the file is being written/accessed
MooseFS 技术架构
MooseFS由4部分组成:
- 管理服务器 ( master servers ) - master server 管理整个文件系统,存储所有文件的元数据(metadata, 包括普通文件的大小、属性、位置,以及特殊文件的所有属性,如目录、套接字、管道、设备),开源版本master server只能部署一台,高可用通过其他方式实现。 Metadate存储在管理服务器的内存里,并会同步到磁盘,包括一个定时更新的二进制文件和一些增量的log文件,这些文件也会同步到metaloggers服务器上以做备份
- 数据存储服务器 ( chunk servers ) - 存储文件的任意数量服务器,这些服务器互相之间会进行数据同步。
- 元数据备份服务器 ( metalogger server ) - 存储metadata changelogs,定时下载main metadata file,服务器数量不受限制。当master server出现灾难性故障时可以通过metalogger server进行恢复。
- 客户端 - 客户端通过mfsmount与管理服务器和数据存储服务器进行通信,mfsmount是基于FUSE(Filesystem in USErspace)机制的实现,所以moosefs可以工作在任何实现FUSE机制的操作系统上。
- 存储在 moosefs 的文件被分成碎片 ( fragments, chunks ) ,每个碎片最大64MiB,chunk在数据存储服务器上本身也是一个文件。通过对文件设置合适的 goal 值 ( 文件的副本数,这个值跟 chunk server 也数有关,后面会讲到 ) 来实现高可用。
MooseFS 如何工作
MooseFS通过客户端挂载到系统上,用户所有的操作就像操作系统本地文件系统一样。系统内核会把所有的操作交给FUSE来处理,FUSE与mfsmount进程进行交互,mfsmount进程再通过网络与master server和chunk server交互。整个过程对用户是透明的。
当需要对文件元数据进行操作时,mfsmount会跟管理服务器进行通信:
- 创建文件
- 删除文件
- 读取目录
- 读取和更改属性
- 更改文件大小
- 开始读或者写数据时
- 访问MFSMETA特殊文件
mfsmount直接连接要访问的文件所在的数据服务器 ( chunk server ) ,当写入一个文件时,完成写入过程后,管理服务器会从 mfsmount 接收信息以更新文件的长度和最后修改时间。
同时,数据服务器 ( chunk server ) 之间会互相通信进行数据复制,以便达到预设的副本数。
故障容错
管理员通过命令行接口设置文件或者目录的副本数,即 goal ,当服务器数量大于 1 台以上时,goal 设置大于 1 将提供容错功能,容许单台服务器短暂的断开。
如果 goal 设置为 1 ,不管服务器有多少台,文件副本只有一个,这种情况下是提供不了容错功能的。
通常情况下,可用副本数的设置应比无法访问或无效服务器的预期数量多一。如果单台数据服务器出现故障或者网络分区,则其中存储的至少具有两个副本的文件将仍可以从另外一台数据服务器访问,而副本数小于 goal 值的数据将被复制到另外一台可访问的数据服务器上,以满足所设置数量的副本数。
值得注意的是,如果数据服务器的数量小于给文件设置的 goal 值时,则无法存储所有的副本,所以当 goal 值与数据服务器数量相同时,如果一台数据服务器宕机,则集群无法再复制一份失联服务器的副本,这种情况下要尽快新增数据服务器,以保证存储正确数量的文件副本数。
新的数据服务器可以随时加入集群中,新增的容量立即生效,可以用于存储新文件或者保存其它数据服务器上的文件副本。
存储在 chunk 的数据分片具有版本号,当故障的数据服务器重新连接加入集群后,旧的数据不会导致文件出现问题,这台服务器会同步集群中当前最新的文件版本,原来旧的数据会释放,存放同步过来的新的 chunk 。
单机部署
集群部署大同小异,只需横向扩展chunkserver节点即可。
环境准备
设置 master host
echo "192.168.7.33 mfs-test-master" >> /etc/hosts
echo "192.168.7.33 mfsmaster" >> /etc/hosts
添加 mfsmaster 的 hosts 是为了方便后续 CGI 后台直接链接 master 读取信息。
下载源码包
数据磁盘
/mnt/data1
、/mnt/data2
、/mnt/data3
各100GB分块文件目录 ( 打散后的数据实际存放与此处 )
/mnt/data1/chunk
、/mnt/data2/chunk
、/mnt/data3/chunk
mkdir /mnt/data{1..3}/chunk
master 元数据目录
/data1/mfs
元数据备份目录
/data1/mfsmetadata
注意事项
启动顺序:
matser — metalogger — chunker — client (mount挂载点)关闭顺序:
client (mount挂载点) — chunker — metalogger — masterMetaLogger 最好不要与 master 同一节点运行,并且集群内可以运行多个 metalogger 节点,做到多点备份。
安装与配置
全量安装
yum install gcc make libpcap-devel zlib-devel fuse3-devel pkgconfig -y
wget https://github.com/moosefs/moosefs/archive/refs/tags/v3.0.115.tar.gz
tar zxvf v3.0.115.tar.gz
cd moosefs-3.0.115
./configure --prefix=/usr/local/mfs-3.0.115
make && make install
ln -s /usr/local/mfs-3.0.115 /usr/local/mfs
配置
MasterServer
进入配置目录
cd /usr/local/mfs/etc/mfs
创建 master 配置文件
此处只填写必须配置,如需其他配置可参考 mfsmaster.cfg.sample 内容
cat > mfsmaster.cfg << EOF
WORKING_USER = root
WORKING_GROUP = root
DATA_PATH = /data1/mfs
#FILE_UMASK = 002
EOF
创建挂载权限配置文件
此处只填写必须配置,如需其他配置可参考 mfsexports.cfg.sample 内容
cat > mfsexports.cfg << EOF
192.168.7.31-192.168.7.33 / rw,alldirs,admin,maproot=root
192.168.7.34 /test rw,alldirs,admin,maproot=root
192.168.7.35 / ro,alldirs,admin,maproot=root
EOF
准备初始元数据
cp /usr/local/mfs-3.0.115/var/mfs/metadata.mfs.empty /data1/mfs/metadata.mfs
启动方式
/usr/local/mfs/sbin/mfsmaster start
ChunkServer
创建 chunkserver 配置文件
此处只填写必须配置,如需其他配置可参考 mfschunkserver.cfg.sample 内容
cat > mfschunkserver.cfg << EOF
WORKING_USER = root
WORKING_GROUP = root
# 3.0 以上版本可用,用于机器分组
LABELS = A
# Master地址,可以是IP、域名或者hosts主机名
MASTER_HOST = mfs-test-master
EOF
创建数据目录配置文件
此处只填写必须配置,如需其他配置可参考 mfshdd.cfg.sample 内容
cat > mfshdd.cfg << EOF
/mnt/data1/chunk
#/mnt/data2/chunk
#/mnt/data3/chunk
EOF
暂时只添加一块磁盘
启动方式
/usr/local/mfs/sbin/mfschunkserver start
CGIServer
后台Web界面。
无需特别配置,默认启动即可
启动方式
/usr/local/mfs/sbin/mfscgiserv start
如需 nginx 反向代理,可参考配置:
upstream mfs_cgi_server
{
#ip_hash;
server 192.168.7.33:9425;
}
server {
listen 80;
server_name mfscgiserver.nestealin.com ;
rewrite ^(.*)$ https://$host$1 permanent;
}
server {
listen 443 ssl;
server_name mfscgiserver.nestealin.com;
charset utf-8;
access_log logs/$http_host.access.main.log main;
error_log logs/mfscgiserver.nestealin.com.error.log error;
ssl_certificate /data/keys/server.cer;
ssl_certificate_key /data/keys/server.key;
ssl_session_timeout 5m;
ssl_prefer_server_ciphers on;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
if ($document_uri = / ) {
rewrite ^(.*)$ https://$http_host/mfs.cgi redirect;
}
client_header_timeout 600;
client_body_timeout 600;
proxy_connect_timeout 600;
proxy_read_timeout 600;
proxy_send_timeout 600;
location / {
#allow 1.2.3.4;
include firewall.conf;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-Port $remote_port;
proxy_pass http://mfs_cgi_server/;
}
}
MetaLogger
创建配置文件
cat > mfsmetalogger.cfg << EOF
WORKING_USER = root
WORKING_GROUP = root
MASTER_HOST = mfs-test-master
# 表示源数据备份下载请求频率,这里设置为1小时。默认为24小时,即每隔一天从元数据服务器(MASTER)下载一个metadata.mfs.back 文件。
META_DOWNLOAD_FREQ = 1
DATA_PATH = /data1/mfsmetadata
EOF
启动方式
/usr/local/mfs/sbin/mfsmetalogger start
验证访问
打开浏览器访问
http://192.168.7.33:9425
由于初始集群尚无数据,所以主要检查 Servers 下的 chunkserver 与 metalogger 是否正常运行。
以及 Disks 中,数据磁盘是否无误上线。
至此,单机版 MFS 已经搭建完毕,如果后续需要改为集群模式,只需不断添加 chunkserver 即可。
最佳实践
目录挂载
挂载格式
# 默认挂载根目录,更多详情 -h 参数查看
/usr/local/mfs/bin/mfsmount ${本地挂载点} -H ${Master域名/IP} -o mfssubfolder=${子目录}
例如
# 全路径挂载 /usr/local/mfs/bin/mfsmount /mnt/mfs_dev -H 192.168.7.33 # 指定子目录挂载 /usr/local/mfs/bin/mfsmount /mnt/mfs_dev -H mfsmaster.homelab.com -o mfssubfolder=/server_data
磁盘下线 / Chunker节点驱逐
- 将磁盘需要驱逐的磁盘标记为
*
(与挂载点无需保持空格)vim /usr/local/mfs/etc/mfs/mfshdd2.cfg
- 将
/mnt/data5
磁盘标记为备份并驱逐/mnt/data4/chunk2 */mnt/data5/chunk2 /mnt/data6/chunk2
reload chunkserver 使配置生效
/usr/local/mfs/sbin/mfschunkserver -c /usr/local/mfs/etc/mfs/mfschunkserver2.cfg reload
后台观察驱逐状态
观察挂载点的 chunks 数,会逐渐下降
待状态出现 ready 后,就可以继续下线
主界面确认副本数:
- 没有
missing
、endangered
或undergoal
数量 - 存在
overgoal
的 chunks 数
证明数据已冗余,可以放心驱逐。
- 注释需要下线的磁盘
vim /usr/local/mfs/etc/mfs/mfshdd2.cfg
- 将
/mnt/data5
磁盘标记为注释,以示下线/mnt/data4/chunk2 #/mnt/data5/chunk2 /mnt/data6/chunk2
reload chunkserver 使配置生效,完成下线
/usr/local/mfs/sbin/mfschunkserver reload
确认磁盘已移除
速度参考
不完全的简单速度对比
- 单线程随机、顺序、混合随机(读7写3)、混合顺序(读7写3)测试
- 文件系统均为fuse方式挂载
测试脚本
#!/usr/bin/python3 # -*- coding: UTF-8 -*- # Author: nestealin # Created: 2021/03/21 import time import os import sys import platform import shlex import subprocess import json from prettytable import PrettyTable rwResult = dict() def bash(cmd): """ run a bash shell command 执行bash命令,只返回执行状态吗 """ return shlex.os.system(cmd) # 创建项目文件夹 def project_detect(output_path, output_filename): list_dir = os.listdir(output_path) if output_filename in list_dir: print('%s输出文件夹已存在,不重复创建。' % output_filename) # sys.exit(1) pass else: print('%s输出文件夹不存在,开始自动创建...' % output_filename) os.mkdir(output_path + output_filename) def save_to_file(file_name, contents): fh = open(file_name, 'w') fh.write(contents) fh.close() def color_print(msg, color='red', exits=False): """ Print colorful string. 颜色打印字符或者退出 """ color_msg = {'blue': '\033[1;36m%s\033[0m', 'green': '\033[1;32m%s\033[0m', 'yellow': '\033[1;33m%s\033[0m', 'red': '\033[1;31m%s\033[0m', 'title': '\033[30;42m%s\033[0m', 'info': '\033[32m%s\033[0m'} msg = color_msg.get(color, 'red') % msg print(msg) if exits: time.sleep(2) sys.exit() return msg def printResult(): table = PrettyTable(["测试项目", "写IOPS", "写带宽", "读IOPS", "读带宽"]) for k, v in rwResult.items(): list = [ k, v["write_iops"], v["write_bw"], v["read_iops"], v["read_bw"]] table.add_row(list) color_print(table) def output_markdown_table(header_name_list, data_dict): markdown_output_list = list() markdown_table_header = """\n### %s (%s) \n| 测试名称<br>(读写方式-块大小-线程数) | 读取iops | 读取带宽<br>(单位 MB/s) | 写入iops | 写入带宽<br>(单位 MB/s) | 备注 |\n| :--: |----| ---- | ---- | ---- | ---- |""" markdown_table_row = """| {KEY} | {read_iops} | {read_bw} | {write_iops} | {write_bw} | |""" markdown_output_list.append( markdown_table_header % (header_name_list[0], header_name_list[1])) for key, value in data_dict.items(): row_data = markdown_table_row.format(KEY=key, read_iops=value['read_iops'], read_bw=value['read_bw'], write_iops=value['read_iops'], write_bw=value['write_bw'] ) markdown_output_list.append(row_data) if markdown_output_list: return '\n'.join(markdown_output_list) else: return '输出列表为空' def Usage(): print('''用法: python3 fio_test.py" 参数: bs/thread bs:进行固定线程,不同块大小读写测试;thread:进行固定块大小,不同线程读写测试。 /dev/sdh 目标测试的盘符路径,最好选取空盘测试,否则会对现有数据产生影响。 ''') class PreSetup(object): def __init__(self): self.dist = platform.platform().lower() @property def _is_redhat(self): if "centos" in self.dist or self.dist.startswith( "red") or self.dist == "fedora" or self.dist == "amazon linux ami": return True @property def _is_ubuntu(self): if self.dist == "ubuntu" or self.dist == "debian" or self.dist == "deepin": return True def check_platform(self): if not (self._is_redhat or self._is_ubuntu): print( u"支持的平台: CentOS, RedHat, Fedora, Debian, Ubuntu, Amazon Linux, 暂不支持其他平台安装.") exit() @staticmethod def check_bash_return(ret_code, error_msg): if ret_code != 0: color_print(error_msg, 'red') exit() def _set_env(self): # color_print('开始关闭防火墙和selinux', 'green') if self._is_redhat: os.system("export LANG='en_US.UTF-8'") # if self._is_centos7 or self._is_fedora_new: # cmd1 = "systemctl status firewalld 2> /dev/null 1> /dev/null" # cmd2 = "systemctl stop firewalld" # cmd3 = "systemctl disable firewalld" # bash('%s && %s && %s' % (cmd1, cmd2, cmd3)) # bash('localectl set-locale LANG=en_US.UTF-8') # bash('which setenforce 2> /dev/null 1> /dev/null && setenforce 0') # else: # bash("sed -i 's/LANG=.*/LANG=en_US.UTF-8/g' /etc/sysconfig/i18n") # bash('service iptables stop && chkconfig iptables off && setenforce 0') if self._is_ubuntu: os.system("export LANG='en_US.UTF-8'") # bash("which iptables && iptables -F") # bash('which setenforce && setenforce 0') def _rpm_repo(self): if self._is_redhat: color_print('开始安装epel源', 'green') bash('yum -y fio libaio-devel python-pip') def _depend_rpm(self): color_print('开始安装依赖包', 'green') if self._is_redhat: cmd = 'yum -y install epel-release' ret_code = bash(cmd) self.check_bash_return(ret_code, "安装依赖失败, 请检查安装源是否更新或手动安装!") if self._is_ubuntu: cmd = "apt-get -y --force-yes install fio python-pip" ret_code = bash(cmd) self.check_bash_return(ret_code, "安装依赖失败, 请检查安装源是否更新或手动安装!") def _require_pip(self): color_print('开始安装依赖pip包', 'green') ret_code = bash('pip install prettytable') self.check_bash_return(ret_code, "安装依赖的python库失败!") def start(self): color_print('检测到系统没有/usr/bin/fio命令,准备开始安装相关依赖...') time.sleep(3) self.check_platform() self._rpm_repo() self._depend_rpm() self._require_pip() self._set_env() class FioTest(object): """ 默认配置: 1. 混合读写比7:3 (rwmixwrite=30) 2. 线程数1 (numjobs=1) 3. io深度1 (iodepth=1) 4. io引擎libaio (ioengine="libaio") 5. 不使用磁盘缓存 (direct=1) """ def __init__( self, name, filename, rw, bs, size, numjobs=1, direct=1, iodepth=1, ioengine="libaio", runtime=60, rwmixwrite=30): self.name = name self.filename = filename self.direct = direct self.rw = rw self.bs = bs self.size = size self.numjobs = numjobs self.iodepth = iodepth self.ioengine = ioengine self.runtime = runtime self.rwmixwrite = rwmixwrite def exprCmd(self): if self.rw == "randrw": cmd = "fio --name=" + self.name + " --numjobs=" + str(self.numjobs) + " --rw=" + self.rw + " --rwmixwrite=" + str( self.rwmixwrite) + " --iodepth=" + str(self.iodepth) + " --ioengine=" + self.ioengine + \ " --thread --direct=" + str(self.direct) + " --norandommap --bs=" + self.bs + " --size=" + \ self.size + " --runtime=" + str(self.runtime) + " --filename=" + self.filename + \ " --minimal --output-format=json" else: cmd = "fio --name=" + self.name + " --numjobs=" + str(self.numjobs) + " --rw=" + self.rw + " --iodepth=" + str( self.iodepth) + " --ioengine=" + self.ioengine + " --thread --direct=" + str( self.direct) + " --norandommap --bs=" + self.bs + " --size=" + self.size + " --runtime=" + str( self.runtime) + " --filename=" + self.filename + " --minimal --output-format=json" # print(cmd) return cmd def runCmd(self, cmd): result_str = '' process = subprocess.Popen( cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) out = process.stdout errors = process.stderr err = errors.read() if err: color_print("执行错误") color_print(err) sys.exit(1) result_str = out.read().strip() if out: out.close() if errors: errors.close() return str(result_str, encoding='utf-8').strip('\n') def explain(self, result): if self.rw == "read" or self.rw == "randread": r = json.loads(result) iops = r["jobs"][0]["read"]["iops"] bw = r["jobs"][0]["read"]["bw"] return iops, bw # else: elif self.rw == "write" or self.rw == "randwrite": r = json.loads(result) iops = r["jobs"][0]["write"]["iops"] bw = r["jobs"][0]["write"]["bw"] return iops, bw else: if self.rw == "randrw": r = json.loads(result) read_iops = r["jobs"][0]["read"]["iops"] read_bw = r["jobs"][0]["read"]["bw"] write_iops = r["jobs"][0]["write"]["iops"] write_bw = r["jobs"][0]["write"]["bw"] return read_iops, read_bw, write_iops, write_bw def expResult(self, iops, bw, randmix='read'): name = "" rw_iops = "" rw_bw = "" rw = self.rw if rw == "write" or rw == "read": name = "seq-" + self.bs + "-" + str(self.numjobs) rw_iops = rw + "_iops" rw_bw = rw + "_bw" elif rw == "randwrite" or rw == "randread": name = "rand-" + self.bs + "-" + str(self.numjobs) rw_iops = rw[4:] + "_iops" rw_bw = rw[4:] + "_bw" elif rw == "randrw": if randmix == 'read': name = "randmixrw-" + self.bs + "-" + str(self.numjobs) rw_iops = "read_iops" rw_bw = "read_bw" else: if randmix == 'write': name = "randmixrw-" + self.bs + "-" + str(self.numjobs) rw_iops = "write_iops" rw_bw = "write_bw" else: name = "NotDefine" rw_iops = rw + "_iops" rw_bw = rw + "_bw" if name in rwResult.keys(): rwResult[name][rw_iops] = round(iops, 2) rwResult[name][rw_bw] = round(bw / 1024, 2) else: rwResult[name] = dict() rwResult[name][rw_iops] = round(iops, 2) rwResult[name][rw_bw] = round(bw / 1024, 2) def saveResult(self): cmd = self.exprCmd() result = self.runCmd(cmd) fio_result = self.explain(result) if len(fio_result) > 2: self.expResult(fio_result[0], fio_result[1], randmix='read') self.expResult(fio_result[2], fio_result[3], randmix='write') else: self.expResult(fio_result[0], fio_result[1]) if __name__ == '__main__': # if len(sys.argv) != 3: # Usage() # exit(1) Usage() test_type = input("请输入测试类型(bs/thread):") filename = input("请输入测试分区(/dev/sdh):") test_size = '10G' block_size_list = ['4', '128', '512', '1024', '4096'] numjobs_list = [1, 24, 64] if not os.path.isfile("/usr/bin/fio"): pre_setup = PreSetup() pre_setup.start() if len(filename) < 8: print("请输入正确的分区路径。") Usage() sys.exit(1) if test_type == "bs": for bs in block_size_list: print("正在进行固定%sk块大小,随机读取测试..." % bs) cmd1 = FioTest( name="randread-%sk" % bs, rw="randread", iodepth=1, ioengine="libaio", direct=1, bs="%sk" % bs, size=test_size, runtime=60, filename=filename) cmd1.saveResult() print("正在进行固定%sk块大小,随机写入测试..." % bs) cmd2 = FioTest( name="randwrite-%sk" % bs, rw="randwrite", iodepth=1, ioengine="libaio", direct=1, bs="%sk" % bs, size=test_size, runtime=60, filename=filename) cmd2.saveResult() print("正在进行固定%sk块大小,顺序读取测试..." % bs) cmd3 = FioTest( name="seq-read-%sk" % bs, rw="read", iodepth=1, ioengine="libaio", direct=1, bs="%sk" % bs, size=test_size, runtime=60, filename=filename) cmd3.saveResult() print("正在进行固定%sk块大小,顺序写入测试..." % bs) cmd4 = FioTest( name="seq-write-%sk" % bs, rw="write", iodepth=1, ioengine="libaio", direct=1, bs="%sk" % bs, size=test_size, runtime=60, filename=filename) cmd4.saveResult() print("正在进行固定%sk块大小,混合读写测试(读写比例7:3)..." % bs) cmd5 = FioTest( name="rand-mixrw-%sk" % bs, rw="randrw", iodepth=1, ioengine="libaio", direct=1, bs="%sk" % bs, size=test_size, runtime=60, filename=filename) cmd5.saveResult() elif test_type == "thread": for thread_num in numjobs_list: bs = "512" print("正在进行固定%sk块大小,%s线程数,随机读取测试..." % (bs, thread_num)) cmd1 = FioTest( name="randread-%sk" % bs, rw="randread", iodepth=1, ioengine="libaio", direct=1, bs="%sk" % bs, size=test_size, runtime=60, numjobs=thread_num, filename=filename) cmd1.saveResult() print("正在进行固定%sk块大小,%s线程数,随机写入测试..." % (bs, thread_num)) cmd2 = FioTest( name="randwrite-%sk" % bs, rw="randwrite", iodepth=1, ioengine="libaio", direct=1, bs="%sk" % bs, size=test_size, runtime=60, numjobs=thread_num, filename=filename) cmd2.saveResult() print("正在进行固定%sk块大小,%s线程数,顺序读取测试..." % (bs, thread_num)) cmd3 = FioTest( name="seq-read-%sk" % bs, rw="read", iodepth=1, ioengine="libaio", direct=1, bs="%sk" % bs, size=test_size, runtime=60, numjobs=thread_num, filename=filename) cmd3.saveResult() print("正在进行固定%sk块大小,%s线程数,顺序写入测试..." % (bs, thread_num)) cmd4 = FioTest( name="seq-write-%sk" % bs, rw="write", iodepth=1, ioengine="libaio", direct=1, bs="%sk" % bs, size=test_size, runtime=60, numjobs=thread_num, filename=filename) cmd4.saveResult() print("正在进行固定%sk块大小,%s线程数,混合读写测试(读写比例7:3)..." % (bs, thread_num)) cmd5 = FioTest( name="rand-mixrw-%sk" % bs, rw="randrw", iodepth=1, ioengine="libaio", direct=1, bs="%sk" % bs, size=test_size, runtime=60, numjobs=thread_num, filename=filename) cmd5.saveResult() else: print("不是有效的测试类型,请重新选择。") Usage() sys.exit(1) header_name_list = ['单盘性能'] get_test_device_model_name_cmd = "smartctl -a %s | grep -e \"Device Model\" | awk -F ':' '{print $NF}'" % filename test_device_model_name = ''.join(os.popen(get_test_device_model_name_cmd).readlines()).strip() header_name_list.append(test_device_model_name) output_markdown_table_contents = output_markdown_table(header_name_list, rwResult) print(output_markdown_table_contents) printResult() output_path = "/root/fio_test_result/" output_file_name = "%s%s/%s.md" % (output_path, test_device_model_name, test_device_model_name) project_detect(output_path, test_device_model_name) save_to_file(output_file_name, output_markdown_table_contents) print("已输出结果到文件:%s" % output_file_name)
相关文档
官方Readme–安装文档
https://github.com/moosefs/moosefs#getting-started
官方用户手册
https://moosefs.com/Content/Downloads/moosefs-3-0-users-manual.pdf
简介文档
https://xiaoz.co/2019/12/01/MooseFS%E4%BD%BF%E7%94%A8%E6%8C%87%E5%8D%97-Part1/