各位老铁们,大家好,今天由我来为大家分享2144小游戏网站源码分享,以及2408小游戏网页的相关问题知识,希望对大家有所帮助。如果可以帮助到大家,还望关注收藏下本站,您的支持是我们最大的动力,谢谢大家了哈,下面我们开始吧!
背景
随着企业SDK在多条产品线的广泛使用,随着SDK开发人员的增长,每日往SDK提交的补丁量与日俱增,自动化提交代码检查的压力已经明显超过了通用服务器的负载。于是向公司申请了一台专用服务器,用于SDK构建检查。
$cat/proc/cpuinfo|grep^proccessor|wc-l\n48\n$free-h\ntotalusedfreesharedbufferscached\nMem:47G45G1.6G20M7.7G25G\n-/+buffers/cache:12G35G\nSwap:0B0B0B\n$df\n文件系统容量已用可用已用%挂载点\n……\n/dev/sda198G14G81G15%/\n/dev/vda12.9T1.8T986G65%/home\n
这是KVM虚拟的服务器,提供了CPU48线程,实际可用47G内存,磁盘空间约达到3TB。
由于独享服务器所有资源,设置了十来个worker并行编译,从提交补丁到发送编译结果的速度杠杠的。但是在补丁提交非常多的时候,速度瞬间就慢了下去,一次提交触发的编译甚至要1个多小时。通过top看到CPU负载并不高,难道是IO瓶颈?找IT要到了root权限,干起来!
由于认知的局限性,如有考虑不周的地方,希望一起交流学习
整体认识IO栈
如果有完整的IO栈的认识,无疑有助于更细腻的优化IO。循着IO栈从上往下的顺序,我们逐层分析可优化的地方。
在网上有Linux完整的IO栈结构图,但太过完整反而不容易理解。按我的认识,简化过后的IO栈应该是下图的模样。
用户空间:除了用户自己的APP之外,也隐含了所有的库,例如常见的C库。我们常用的IO函数,例如open()/read()/write()是系统调用,由内核直接提供功能实现,而fopen()/fread()/fwrite()则是C库实现的函数,通过封装系统调用实现更高级的功能。虚拟文件系统:屏蔽具体文件系统的差异,向用户空间提供统一的入口。具体的文件系统通过register_filesystem()向虚拟文件系统注册挂载钩子,在用户挂载具体的文件系统时,通过回调挂载钩子实现文件系统的初始化。虚拟文件系统提供了inode来记录文件的元数据,dentry记录了目录项。对用户空间,虚拟文件系统注册了系统调用,例如SYSCALL_DEFINE3(open,constchar__user*,filename,int,flags,umode_t,mode)注册了open()的系统调用。具体的文件系统:文件系统要实现存储空间的管理,换句话说,其规划了哪些空间存储了哪些文件的数据,就像一个个收纳盒,A文件保存在这个块,B文件则放在哪个块。不同的管理策略以及其提供的不同功能,造就了各式各样的文件系统。除了类似于vfat、ext4、btrfs等常见的块设备文件系统之外,还有sysfs、procfs、pstorefs、tempfs等构建在内存上的文件系统,也有yaffs,ubifs等构建在Flash上的文件系统。页缓存:可以简单理解为一片存储着磁盘数据的内存,不过其内部是以页为管理单元,常见的页大小是4K。这片内存的大小不是固定的,每有一笔新的数据,则申请一个新的内存页。由于内存的性能远大于磁盘,为了提高IO性能,我们就可以把IO数据缓存在内存,这样就可以在内存中获取要的数据,不需要经过磁盘读写的漫长的等待。申请内存来缓存数据简单,如何管理所有的页缓存以及如何及时回收缓存页才是精髓。通用块层:通用块层也可以细分为bio层和request层。页缓存以页为管理单位,而bio则记录了磁盘块与页之间的关系,一个磁盘块可以关联到多个不同的内存页中,通过submit_bio()提交bio到request层。一个request可以理解为多个bio的集合,把多个地址连续的bio合并成一个request。多个request经过IO调度算法的合并和排序,有序地往下层提交IO请求。设备驱动与块设备:不同块设备有不同的使用协议,而特定的设备驱动则是实现了特定设备需要的协议以正常驱使设备。对块设备而言,块设备驱动需要把request解析成一个个设备操作指令,在协议的规范下与块设备通信来交换数据。
形象点来说,发起一次IO读请求的过程是怎么样的呢?
用户空间通过虚拟文件系统提供的统一的IO系统调用,从用户态切到内核态。虚拟文件系统通过调用具体文件系统注册的回调,把需求传递到具体的文件系统中。紧接着具体的文件系统根据自己的管理逻辑,换算到具体的磁盘块地址,从页缓存寻找块设备的缓存数据。读操作一般是同步的,如果在页缓存没有缓存数据,则向通用块层发起一次磁盘读。通用块层合并和排序所有进程产生的的IO请求,经过设备驱动从块设备读取真正的数据。最后是逐层返回。读取的数据既拷贝到用户空间的buffer中,也会在页缓存中保留一份副本,以便下次快速访问。
如果页缓存没命中,同步都会一路通到块设备,而对于异步写,则是把数据放到页缓存后返回,由内核回刷进程在合适时候回刷到块设备。
根据这个流程,考虑到我没要到KVMhost的权限,我只能着手从Guest端的IO栈做优化,具体包括以下几个方面:
交换分区(swap)文件系统(ext4)页缓存(PageCache)Request层(IO调度算法)
由于源码以及编译的临时文件都不大但数量极其多,对随机IO的要求非常高。要提高随机IO的性能,在不改变硬件的情况下,需要缓存更多数据,以实现合并更多的IO请求。
咨询ITer得知,服务器都有备用电源,能确保不会掉电停机。出于这样的情况,我们可以尽可能优化速度,而不用担心掉电导致数据丢失问题。
总的来说,优化的核心思路是尽可能多的使用内存缓存数据,尽可能减小不必要的开销,例如文件系统为了保证数据一致性使用日志造成的开销。
交换分区
交换分区的存在,可以让内核在内存压力大时,把内核认为一些不常用的内存置换到交换分区,以此腾出更多的内存给系统。在物理内存容量不足且运行吃内存的应用时,交换分区的作用效果是非常明显的。
然而本次优化的服务器反而不应该使用交换分区。为什么呢?服务器总内存达到47G,且服务器除了Jenkinsslave进程外没有大量吃内存的进程。从内存的使用情况来看,绝大部分内存都是被cache/buffer占用,是可丢弃的文件缓存,因此内存是充足的,不需要通过交换分区扩大虚拟内存。
cat/proc/swaps\nFilenameTypeSizeUsedPriority\nmount\n…\n/dev/sda1onon/typeext4(rw)\n/dev/vda1on/hometypeext4(rw)\n…\n
此服务器主要有两个块设备,分别是sda和vda。sda是常见的SCSI/IDE设备,我们个人PC上如果使用的机械硬盘,往往就会是sda设备节点。vda是virtio磁盘设备。由于本服务器是KVM提供的虚拟机,不管是sda还是vda,其实都是虚拟设备,差别在于前者是完全虚拟化的块设备,后者是半虚拟化的块设备。从网上找到的资料来看,使用半虚拟化的设备,可以实现Host与Guest更高效的协作,从而实现更高的性能。在此例子中,sda作为根文件系统使用,vda则是用于存储用户数据,在编译时,主要看的是vda分区的IO情况。
vda使用ext4文件系统。ext4是目前常见的Linux上使用的稳定的文件系统,查看其超级块信息:
tune2fs-O^has_journal/dev/vda1\ntune2fs1.42.9(4-Feb-2014)\nThehas_journalfeaturemayonlybeclearedwhenthefilesystemis\nunmountedormountedread-only.\n
可惜失败了。由于时刻有任务在执行,不太好直接umount或者-oremount,ro,无法在挂载时取消日志。既然取消不了,咱们就让日志减少损耗,就需要修改挂载参数了。
ext4挂载参数:data
ext4有3种日志模式,分别是ordered,writeback,journal。他们的差别网上有很多资料,我简单介绍下:
jorunal:把元数据与数据一并写入到日志块。性能差不多折半,因为数据写了两次,但最安全writeback:把元数据写入日志块,数据不写入日志块,但不保证数据先落盘。性能最高,但由于不保证元数据与数据的顺序,也是掉电最不安全的ordered:与writeback相似,但会保证数据先落盘,再是元数据。这种性能以保证足够的安全,这是大多数PC上推荐的默认的模式
在不需要担心掉电的服务器环境,我们完全可以使用writeback的日志模式,以获取最高的性能。
dmesg\n[235737.532630]EXT4-fs(vda1):Cannotchangedatamodeonremount\n
沮丧,又是不能动态改,干脆写入到/etc/config,只能寄希望于下次重启了。
mount-o…noatime…/home\n
ext4挂载参数:nobarrier
这主要是决定在日志代码中是否使用写屏障(writebarrier),对日志提交进行正确的磁盘排序,使易失性磁盘写缓存可以安全使用,但会带来一些性能损失。从功能来看,跟writeback和ordered日志模式非常相似。没研究过这方面的源码,说不定就是一回事。不管怎么样,禁用写屏障毫无疑问能提高写性能。
mount-o…delalloc…/home\n
ext4挂载参数:inode_readahead_blks
ext4从inode表中预读的indoeblock最大数量。访问文件必须经过inode获取文件信息、数据块地址。如果需要访问的inode都在内存中命中,就不需要从磁盘中读取,毫无疑问能提高读性能。其默认值是32,表示最大预读32×block_size即64K的inode数据,在内存充足的情况下,我们毫无疑问可以进一步扩大,让其预读更多。
mount-o…journal_async_commit…/home\n
ext4挂载参数:commit
ext4一次缓存多少秒的数据。默认值是5,表示如果此时掉电,你最多丢失5s的数据量。设置更大的数据,就可以缓存更多的数据,相对的掉电也有可能丢失更多的数据。在此服务器不怕掉电的情况,把数值加大可以提高性能。
cat/etc/fstab\nUUID=…/homeext4defaults,rw,noatime,nobarrier,delalloc,inode_readahead_blks=4096,journal_async_commit,commit=1800,data=writeback00\n…\n
页缓存
页缓存在FS与通用块层之间,其实也可以归到通用块层中。为了提高IO性能,减少真实的从磁盘读写的次数,Linux内核设计了一层内存缓存,把磁盘数据缓存到内存中。由于内存以4K大小的页为单位管理,磁盘数据也以页为单位缓存,因此也称为页缓存。在每个缓存页中,都包含了部分磁盘信息的副本。
如果因为之前读写过或者被预读加载进来,要读取数据刚好在缓存中命中,就可以直接从缓存中读取,不需要深入到磁盘。不管是同步写还是异步写,都会把数据copy到缓存,差别在于异步写只是copy且把页标识脏后直接返回,而同步写还会调用类似fsync()的操作等待回写,详细可以看内核函数generic_file_write_iter()。异步写产生的脏数据会在“合适”的时候被内核工作队列writeback进程回刷。
那么,什么时候是合适的时候呢?最多能缓存多少数据呢?对此次优化的服务器而言,毫无疑问延迟回刷可以在频繁的删改文件中减少写磁盘次数,缓存更多的数据可以更容易合并随机IO请求,有助于提升性能。
在/proc/sys/vm中有以下文件与回刷脏数据密切相关:
配置文件功能默认值dirty_background_ratio触发回刷的脏数据占可用内存的百分比0dirty_background_bytes触发回刷的脏数据量10dirty_bytes触发同步写的脏数据量0dirty_ratio触发同步写的脏数据占可用内存的百分比20dirty_expire_centisecs脏数据超时回刷时间(单位:1/100s)3000dirty_writeback_centisecs回刷进程定时唤醒时间(单位:1/100s)500
对上述的配置文件,有几点要补充的:
XXX_ratio和XXX_bytes是同一个配置属性的不同计算方法,优先级XXX_bytes>XXX_ratio可用内存并不是系统所有内存,而是freepages+reclaimablepages脏数据超时表示内存中数据标识脏一定时间后,下次回刷进程工作时就必须回刷回刷进程既会定时唤醒,也会在脏数据过多时被动唤醒。
dirty_background_XXX与dirty_XXX的差别在于前者只是唤醒回刷进程,此时应用依然可以异步写数据到Cache,当脏数据比例继续增加,触发dirty_XXX的条件,不再支持应用异步写。
更完整的功能介绍,可以看内核文档Documentation/sysctl/vm.txt,也可看我写的一篇总结博客《Linux脏数据回刷参数与调优》
对当前的案例而言,我的配置如下:
dirty_background_ratio=60\ndirty_ratio=80\ndirty_writeback_centisecs=6000\ndirty_expire_centisecs=12000\n
这样的配置有以下特点:
当脏数据达到可用内存的60%时唤醒回刷进程当脏数据达到可用内存的80%时,应用每一笔数据都必须同步等待每隔60s唤醒一次回刷进程内存中脏数据存在时间超过120s则在下一次唤醒时回刷
当然,为了避免重启后丢失优化结果,我们在/etc/sysctl.conf中写入:
cat/sys/block/vda/queue/scheduler\nnone\nuname-r\n3.13.0-170-generic\n
查看Linux内核git提交记录,发现在3.13.0的内核版本上还没有实现适用于多队列的IO调度算法,且此时还没完全切到多队列架构,因此使用单队列的sda设备依然存在传统的noop,deadline和cfq调度算法,而使用多队列的vda设备(virtio)的IO调度算法只有none。为了使用mq-deadline调度算法把内核升级的风险似乎很大。因此IO调度算法方面没太多可优化的。
但Request层优化只能这样了?既然IO调度算法无法优化,我们是否可以修改queue相关的参数?例如加大Request队列的长度,加大预读的数据量。
在/sys/block/vda/queue中有两个可写的文件nr_requests和read_ahead_kb,前者是配置块层最大可以申请的request数量,后者是预读最大的数据量。默认情况下,
nr_request=128\nread_ahead_kb=128\n
我扩大为
nr_request=1024\nread_ahead_kb=512\n
优化效果
优化后,在满负荷的情况下,查看内存使用情况:
#cat/proc/meminfo\nMemTotal:49459060kB\nMemFree:1233512kB\nBuffers:12643752kB\nCached:21447280kB\nActive:19860928kB\nInactive:16930904kB\nActive(anon):2704008kB\nInactive(anon):19004kB\nActive(file):17156920kB\nInactive(file):16911900kB\n…\nDirty:7437540kB\nWriteback:1456kB\n
可以看到,文件相关内存(Active(file)+Inactive(file))达到了32.49GB,脏数据达到7.09GB。脏数据量比预期要少,远没达到dirty_background_ratio和dirty_ratio设置的阈值。因此,如果需要缓存更多的写数据,只能延长定时唤醒回刷的时间dirty_writeback_centisecs。这个服务器主要用于编译SDK,读的需求远大于写,因此缓存更多的脏数据没太大意义。
我还发现Buffers达到了12G,应该是ext4的inode占用了大量的缓存。如上分析的,此服务器的ext4有大量富余的inode,在缓存的元数据里,无效的inode不知道占比多少。减少inode数量,提高inode利用率,说不定可以提高inode预读的命中率。
优化后,一次使能8个SDK并行编译,走完一次完整的编译流程(包括更新代码,抓取提交,编译内核,编译SDK等),在没有进入错误处理流程的情况下,用时大概13分钟。
这次的优化就到这里结束了,等后期使用过程如果还有问题再做调整。
本文来自公众号:Linux阅码场,作者:廖威雄,就职于珠海全志科技股份有限公司,负责LinuxIO全栈研发、性能优化、开源社区开发交流、Linux内核开源社区pstore/blk,mtdpstore模块的作者(与maintainer交流中)、大客户存储技术支持、全志首个UBI存储方案主导人、全志首个RTOSNFTL主导人。
如果你还想了解更多这方面的信息,记得收藏关注本站。
