4889软件园:电脑手机软件下载大全,热门手机游戏免费下载

4889软件园 > 资讯文章 > 我的世界op指令(我的世界基础op指令大全 教你如何做一名合格的OP)

我的世界op指令(我的世界基础op指令大全 教你如何做一名合格的OP)

作者:佚名 来源:4889软件园 时间:2023-02-09 03:12:39

我的世界op指令(我的世界基础op指令大全 教你如何做一名合格的OP)

我的世界op指令文章列表:

我的世界op指令(我的世界基础op指令大全 教你如何做一名合格的OP)

我的世界基础op指令大全 教你如何做一名合格的OP

下面分享是是我的世界中的OP指令哦~那想要做一名好的合格的OP不妨进来看看下面的介绍吧!

基础op指令

首先我想说的是在这个op指令不是全部都会有的只是我这样做op所一直用的而已。也有我自搜索的一些op指令

op [ID]给予某个玩家管理权限 /nj hand -1无限耐久(这个好像一插件)

/deop [ID]消除某个玩家的OP

/gm [0/1/2] 修改自己生存/创造/冒险模式

/whitelist add [ID] 给予某个玩家白名单

/whitelist remove [ID] 解除某个玩家的白名单

/ban [ID] 封禁某个玩家

/banip [ID] 封禁某个玩家所在的ip地址的所有号

/tempban [ID/IP] [time]

封禁某个ID或者IP一段时间.附加:s是秒,m是分钟,h是小时,d是天,w是星期,mo是月,y是年

/mute [ID] [time] 禁言某个玩家一段时间

/unban [ID] 解封玩家ID

/unbanip [IP] 解封 IP

/setworth [物品名字/物品id] [数值] 设定一个东西的系统价格,这个物品名字或者ID都行

/worth [ID] 查看系统价格,不写物品id默认为查看手上的物品价格

/itemdb 查询你手上拿着的物品的名称和ID

/mail [read/clear/send] [ID] [语句] 读取,清除,发送,某个离线玩家的邮件

/msg [语句] 私聊

/near 查看附近的玩家

/reply [语句] 回复最后一个私聊你的玩家

/whois [nickname/ID] 查看某个玩家的用户信息,ID,akf信息,nick名

/seen [ID] 查看玩家最后一次下线前所在的坐标

/sudo [ID] [指令] 让某个玩家执行一段命令,OP不能强制OP执行命令

/tp [ID] 强制传送到某个玩家的身边

/tphere [ID] 请求让某个玩家到你这里

/tpall [ID] 大传送

/tppos [x] [y] [z] 传送到指定坐标

/spawner [怪物的英文名] 改变刷怪笼的刷新物

/money give * [钱数] 给与全部人xxx钱*代表全部人

/give [ID] [物品ID] 给予某人一定数量的物品

/say [语句] 让服务器发一段所有人可见的话,并且是紫色字体的

/gc 查看服务器信息

/rules [数量] 查看服务器第几页的规矩

/essentials [reload/debug] 显示插件版本或者重读

/backup 备份服务器。需要配置备份脚本

/setspawn 在你站着的这个地方设置重生点

/setwarp [名字] 标注一块地区,以后你可以用/warp [名字] 来飞到这个你标注过的地方,相当于tp到领地

/baltop 查看财富排行榜

/compass 显示你当前的方位

/depth [ID] 显示你当前的高度,z轴高度

/getpos [ID] 显示当前的坐标x,y

/help [数量] 查看第几页的帮助

/helpop [语句] 向Op留言求助

/world 切换世界为nether、normal。地狱,END等等,具体的我也不知道-

-我不敢这么玩,我只试过一次,然后人飞到了地狱

/remove droPS 99999 清空掉落物品

/killall mobs 杀光怪物

/vanish或者直接/v 隐身

/fireball 释放地狱轰炸机的火球,从OP的口里吐出来

/shock [玩家ID] 雷击某个玩家,如果直接指令就在准心对着的位置雷击

/nuke [ID] 在某个玩家上方放核弹,其实就是一堆被激活的TNT从天上飞下来

/antioch 在准心所指的地方放一颗炸弹

/butcher 杀死附近的全部怪物

/killall 杀死附近的全部生物,包括村民.不包括玩家

/kill [ID] 杀死某个玩家

/suicide 自杀

/burn [ID] 让某个玩家起火

/ext [ID] 给某个玩家灭火

/jump 直接跳跃到准心所指地方,长途旅行很实用

/fly [ID] 给予某个玩家临时飞行,重新登陆后失效

/up [数量] 把自己升到某高度,最大256,脚下会生成一块玻璃垫着你

/weather [storm/sun] 改变天气

/rain off /on 停雨/降雨

/time set [xx:xx] 设置时间.

/heal [ID] 回复某个玩家的生命

/unlimited [list/item/clear] [ID] 查看,给予,清楚,某个玩家的无限物品

/nick [称号] 给自己换昵称

/powertooltoggle 清除所有的powertoll

/foreSTGen 在自己身旁形成森林

/pumpkins 在自己身旁形成南瓜林

/snow 在自己的身旁成为雪后的样子

/thaw 融雪、冰

/tree [树的形状]

big 大树

ewquoia红木

Tall sequoia 高大的红木

Birch 衫树

Random 随机

/setjail [名字] 设置一个监狱

/togglejail [名字] 把玩家送进xxx监狱,永久封存

/tjail [ID] [监狱名字] [时间] 把玩家扔到指定的监狱多少时间.

/unjail [ID] [监狱名字] [时间] 多久后把玩家解放出来

/deljail [ID] 使某玩家从监狱中释放出来

/invsee 查看某个玩家的背包

/clear [ID]清空某个玩家的背包

/socialspy 开启后可看见任何玩家的私聊

/resadmin 管理员圈地

/delhome 删除家

用Essentials

运用号令

/mangadd 组 来创立一个用户组

/mangaddp 组 essentials.权限 来给组添加某某权限

/manglistp 组 列出某个组的权限

/mangdelp 组 essentials.权限 删除某组的某权限

/manuaddv [ID] prefix 你要的称号 给某人增加称号

/mangaddv 组 prefix 值 给组添加称号

/mangaddi 组1 组2 让组1担当租2的权限,能够为权限设置剩下不少费事

/manuadd 用户 组 将用户添加到组

/manuaddp 用户 权限 给用户权限

/manudelp 用户 权限 删除用户权限

/manudel 用户 把人移出权限组

/mangdel 组 删除权限组

默认权限组名字:default

/skull [ID] 获得玩家的头

/god [ID] 上帝模式

每一个服务器都会有创世神所有我也搜索了许多创世神的指令

【铺满】 //set xxx(放置xxx,xxx是方块ID

【清空】 //set 0 (0=空气,无方块

【围墙】 //walls xxx (xxx是方块IDweeeeshiSB

【空心球体】 //hsphere xxx yyy(xxx是方块ID,yyy是半径【可以用指南针右键方块定位

【实心球体】 //sphere xxx yyy(xxx是方块ID,yyy是半径【可以用指南针右键方块定位

【圆圈】 //hcyl xxx yyy(xxx是物品ID,yyy是半径

【圆形】 //cyl xxx yyy(xxx是方块ID,yyy是半径

【空心圆柱】 //hcyl xxx yyy zzz(xxx是方块ID,yyy是半径,zzz是高度

【实心圆柱】 //cyl xxx yyy zzz(xxx是方块ID,yyy是半径,zzz是高度

【还原】 //undo

【撤销/撤销还原】//redo

【复制】 //copy (站在哪复制,等一下黏贴也会是在相同的位置出现

【黏贴】 //paste (注意站好位置

【改变复制物的方向】//rotate xxx (xxx是度数,只能打90的倍数。顺时针旋转复制物

【替换】 //replace xxx yyy (xxx是原来方块,yyy是替换方块

这个上面都是基础的希望对一些新的op有帮助,最后我在说一个生物生成的办法

【/summon 名字 】 召唤出生物

Chicken (鸡)

Cow(牛)

Horse(马)

Ocelot(豺猫)

Pig(猪)

Sheep (羊)

rabbit(兔子)

Bat(蝙蝠)

Mooshroom (哞菇)

Squid (鱿鱼)

Villager (村民)

Cave Spider (洞穴蜘蛛)

Enderman(末影人)

Spider(蜘蛛)

Wolf(狼)

Zombie Pigman (僵尸猪人)

Blaze (烈焰人)

Creeper(爬行者)

Ghast (恶魂)

Magma Cube (岩浆怪)

Silverfish (蠹虫)

Skeleton(骷髅射手)

Slime (史莱姆)

Spider Jockey (蜘蛛骑士)

Witch(女巫)

Wither Skeleton(凋零骷髅)

Zombie(僵尸)

Endermite(末影螨)

Zombie Villager(僵尸村民)

Guardian( 守卫者)

Elder Guardian(远古守卫者)

Chicken Jockey (鸡骑士)

Snow Golem(雪傀儡)*

Iron Golem(铁傀儡)*

Ender Dragon(末影龙)

Wither(凋零)*

以下通过命令方块生成:

Giant (巨人;巨大僵尸)

Undead Horse(僵尸马 )

Skeleton Horse(骷髅马)

我的世界中用指令给装备、武器附魔,这个比较难,一定要理解

大家好,我是海底蛟龙4解说,今天向大家分享一下我的世界高手教学的内容,就给运用指令给武器和装备附魔,这个需要很高的智商、理解能力和记忆能力,所以对大家的能力是有很高的要求的。

其中,可以用于附魔的指令一共有两种:一是enchant指令, 二是give指令。需要管理员op权限。

enchant指令

用法:

/enchant <玩家> <魔咒ID> [等级]

这个指令是给自己手上的物品附魔,如果手上没有物品,则这条指令失效并且告诉你:“目标没有拿任何物品”。虽然这条指令非常简单,但是却只能普通附魔,比如说生存模式下保护这个魔咒最高能弄到4级, 那么在输入[等级]这个值的时候,你就不能超过4级。

如果超过了,则这条指令失效并且告诉你:“你输入的数字(x)太大了,它最高只能为4”。(x>4)。而且魔咒只能附在相应的物品上,如: 保护、火焰保护、爆炸保护只可以附在防具上, 锋利、亡灵杀手、节肢杀手只可以附在剑上。

give指令

用法:

/give <玩家> <物品> [数量] [数据值] [数据标签]

这个指令可以自定义附魔。自定义附魔即附魔等级可以超过普通附魔极限的附魔(最高32767) 需要注意的是/give指令在我的世界手游中 不能使用ench定义 ,一样只能用于普通附魔。

魔咒ID

0 保护 效果:减少伤害

1 火焰保护 效果:减少烧伤伤害

2 摔落保护 效果:减少掉落伤害

3 爆炸保护 效果:减少爆炸伤害

4 弹射物保护 效果:减少弹射物伤害

5 水下呼吸 效果:憋气更久

6 水下加速 效果:让你挖矿不受水的阻力

上面的魔咒ID是一些魔咒的示例,除了这几种,还有其他很多种魔咒没有列举,好了,以上就是关于指令附魔的本期内容,喜欢的话千万不要忘记点个关注哦。

CPU虚拟化:陷入和模拟

导读:本文摘自于王柏生、谢广军撰写的《深度探索Linux系统虚拟化:原理与实现》一书,重点讨论了虚拟CPU在Guest模式下运行时,由于运行敏感指令而触发虚拟机退出的典型情况。

作者:王柏生、谢广军

来源:华章科技

虚拟机进入Guest模式后,并不会永远处于Guest模式。从Host的角度来说,VM就是Host的一个进程,一个Host上的多个VM与Host共享系统的资源。因此,当访问系统资源时,就需要退出到Host模式,由Host作为统一的管理者代为完成资源访问。

比如当虚拟机进行I/O访问时,首先需要陷入Host,VMM中的虚拟磁盘收到I/O请求后,如果虚拟机磁盘镜像存储在本地文件,那么就代为读写本地文件,如果是存储在远端集群,那么就通过网络发送到远端存储集群。再比如访问设备I/O内存映射的地址空间,当访问这些地址时,将触发页面异常,但是这些地址对应的不是内存,而是模拟设备的I/O空间,因此需要KVM介入,调用相应的模拟设备处理I/O。通常虚拟机并不会呈现Host的CPU信息,而是呈现一个指定的CPU型号,在这种情况下,显然cpuid指令也不能在Guest模式执行,需要KVM介入对cpuid指令进行模拟。

当然,除了Guest主动触发的陷入,还有一些陷入是被动触发的,比如外部时钟中断、外设的中断等。对于外部中断,一般都不是来自Guest的诉求,而只是需要Guest将CPU资源让给Host。

01 访问外设

前文中提到,虚拟化的3个条件之一是资源控制,即由VMM控制和协调宿主机资源给各个虚拟机,而不能由虚拟机控制宿主机的资源。以虚拟机的不同处理器之间发送核间中断为例,核间中断是由一个CPU通过其对应的LAPIC发送中断信号到目标CPU对应的LAPIC,如果不加限制地任由Guest 访问CPU的物理LAPIC芯片,那么这个中断信号就可能被发送到其他物理CPU了。而对于虚拟化而言,不同的CPU只是不同的线程,核间中断本质上是在同一个进程的不同线程之间发送中断信号。当Guest的一个CPU(线程)发送核间中断时,应该陷入VMM中,由虚拟的LAPIC找到目标CPU(线程),向目标CPU(线程)注入中断。

常用的访问外设方式包括PIO(programmed I/O)和MMIO(memory-mapped I/O)。在这一节,我们重点探讨MMIO,然后简单地介绍一下PIO,更多内容将在“设备虚拟化”一章中讨论。

1. MMIO

MMIO是PCI规范的一部分,I/O设备被映射到内存地址空间而不是I/O空间。从处理器的角度来看,I/O映射到内存地址空间后,访问外设与访问内存一样,简化了程序设计。以MMIO方式访问外设时不使用专用的访问外设的指令(out、outs、in、ins),是一种隐式的I/O访问,但是因为这些映射的地址空间是留给外设的,因此CPU将产生页面异常,从而触发虚拟机退出,陷入VMM中。以LAPIC为例,其使用一个4KB大小的设备内存保存各个寄存器的值,内核将这个4KB大小的页面映射到地址空间中:

linux-1.3.31/arch/i386/kernel/smp.cvoid smp_boot_cpus(void){ … apic_reg = vremap(0xFEE00000,4096); …}linux-1.3.31/include/asm-i386/i82489.h#define APIC_ICR 0x300linux-1.3.31/include/asm-i386/smp.hextern __inline void apic_write(unsigned long reg, unsigned long v){ *((unsigned long *)(apic_reg reg))=v;}

代码中地址0xFEE00000是32位x86架构为LAPIC的4KB的设备内存分配的总线地址,映射到地址空间中的逻辑地址为apic_reg。LAPIC各个寄存器都存储在这个4KB设备内存中,各个寄存器可以使用相对于4KB内存的偏移寻址。比如,icr寄存器的低32位的偏移为0x300,因此icr寄存器的逻辑地址为apic_reg 0x300,此时访问icr寄存器就像访问普通内存一样了,写icr寄存器的代码如下所示:

linux-1.3.31/arch/i386/kernel/smp.cvoid smp_boot_cpus(void){ … apic_write(APIC_ICR, cfg); /* Kick the second */ …}

当Guest执行这条指令时,由于这是为LAPIC保留的地址空间,因此将触发Guest发生页面异常,进入KVM模块:

commit 97222cc8316328965851ed28d23f6b64b4c912d2KVM: Emulate local APIC in kernellinux.git/drivers/kvm/vmx.cstatic int handle_exception(struct kvm_vcpu *vcpu, …){ … if (is_page_fault(intr_info)) { … r = kvm_mmu_page_fault(vcpu, cr2, error_code); … if (!r) { … return 1; } er = emulate_instruction(vcpu, kvm_run, cr2, error_code); … } …}

显然对于这种页面异常,缺页异常处理函数是没法处理的,因为这个地址范围根本就不是留给内存的,所以,最后逻辑就到了函数emulate_instruction。后面我们会看到,为了提高效率和简化实现,Intel VMX增加了一种原因为apic access的虚拟机退出,我们会在“中断虚拟化”一章中讨论。可以毫不夸张地说,MMIO的模拟是KVM指令模拟中较为复杂的,代码非常晦涩难懂。要理解MMIO的模拟,需要对x86指令有所了解。我们首先来看一下x86指令的格式,如图1所示。

图1 x86指令格式

首先是指令前缀(instruction prefixes),典型的比如lock前缀,其对应常用的原子操作。当指令前面添加了lock前缀,后面的操作将锁内存总线,排他地进行该次内存读写,高性能编程领域经常使用原子操作。此外,还有常用于mov系列指令之前的rep前缀等。

每一个指令都包含操作码(opcode),opcode就是这个指令的索引,占用1~3字节。opcode是指令编码中最重要的部分,所有的指令都必须有opcode,而其他的5个域都是可选的。

与操作码不同,操作数并不都是嵌在指令中的。操作码指定了寄存器以及嵌入在指令中的立即数,至于是在哪个寄存器、在内存的哪个位置、使用哪个寄存器索引内存位置,则由ModR/M和SIB通过编码查表的方式确定。

displacement表示偏移,immediate表示立即数。

我们以下面的代码片段为例,看一下编译器将MMIO访问翻译的汇编指令:

// test.cchar *icr_reg;void write() {    *((unsigned long *)icr_reg) = 123;

我们将上述代码片段编译为汇编指令:

gcc -S test.c

核心汇编指令如下:

// test.s movq icr_reg(%rip), %rax    movq    $123, (%rax)

可见,这段MMIO访问被编译器翻译为mov指令,源操作数是立即数,目的操作数icr_reg(%rip)相当于icr寄存器映射到内存地址空间中的内存地址。因为这个地址是一段特殊的地址,所以当Guest访问这个地址,即上述第2行代码时,将产生页面异常,触发虚拟机退出,进入KVM模块。

KVM中模拟指令的入口函数是emulate_instruction,其核心部分在函数x86_emulate_memop中,结合这个函数我们来讨论一下MMIO指令的模拟:

commit 97222cc8316328965851ed28d23f6b64b4c912d2KVM: Emulate local APIC in kernellinux.git/drivers/kvm/x86_emulate.c int x86_emulate_memop(struct x86_emulate_ctxt *ctxt, …) { unsigned d; u8 b, sib, twobyte = 0, rex_prefix = 0; …     for (i = 0; i < 8; i ) {         switch (b = insn_fetch(u8, 1, _eip)) {     …     d = opcode_table[b];     …     if (d & ModRM) {         modrm = insn_fetch(u8, 1, _eip);         modrm_mod |= (modrm & 0xc0) >> 6;         …     }     …     switch (d & SrcMask) {     …     case SrcImm:         src.type = OP_IMM;         src.ptr = (unsigned long *)_eip;         src.bytes = (d & ByteOp) ? 1 : op_bytes;         …         switch (src.bytes) {         case 1:             src.val = insn_fetch(s8, 1, _eip);             break;         …     }     … switch (d & DstMask) { … case DstMem: dst.type = OP_MEM; dst.ptr = (unsigned long *)cr2; dst.bytes = (d & ByteOp) ? 1 : op_bytes; … } … switch (b) { … case 0x88 ... 0x8b: /* mov */ case 0xc6 ... 0xc7: /* mov (sole member of Grp11) */ dst.val = src.val;         break;     …     } writeback:     if (!no_wb) {         switch (dst.type) {         …         case OP_MEM:             … rc = ops->write_emulated((unsigned long)dst.ptr, &dst.val, dst.bytes, ctxt->vcpu); … ctxt->vcpu->rip = _eip; … }

函数x86_emulate_memop首先解析代码的前缀,即代码第6~8行。在处理完指令前缀后,变量b通过函数insn_fetch读入的是操作码(opcode),然后需要根据操作码判断指令操作数的寻址方式,该方式记录在一个数组opcode_table中,以操作码为索引就可以读出寻址方式,见第9行代码。如果使用了ModR/M和SIB寻址操作数,则解码ModR/M和SIB部分见第11~15行代码。

第17~29行代码解析源操作数,对于以MMIO方式写APIC的寄存器来说,源操作数是立即数,所以进入第19行代码所在的分支。因为立即数直接嵌在指令编码里,所以根据立即数占据的字节数,调用insn_fetch从指令编码中读取立即数,见第25~27行代码。为了减少代码的篇幅,这里只列出了立即数为1字节的情况。

第31~38行代码解析目的操作数,对于以MMIO方式写APIC的寄存器来说,其目的操作数是内存,所以进入第33行代码所在的分支。本质上,这条指令是因为向目的操作数指定的地址写入时引发页面异常,而引起异常的地址记录在cr2寄存器中,所以目的操作数的地址就是cr2寄存器中的地址,见第35行代码。

确定好了源操作数和目的操作数后,接下来就要模拟操作码所对应的操作了,即第40~47行代码。对于以MMIO方式写APIC的寄存器来说,其操作是mov,所以进入第42、43行代码所在分支。这里模拟了mov指令的逻辑,将源操作数的值写入目的操作数指定的地址,见第44行代码。

指令模拟完成后,需要更新指令指针,跳过已经模拟完的指令,否则会形成死循环,见第60行代码。

对于一个设备而言,仅仅简单地把源操作数赋值给目的操作数指向的地址还不够,因为写寄存器的操作可能会伴随一些副作用,需要设备做些额外的操作。比如,对于APIC而言,写icr寄存器可能需要LAPIC向另外一个处理器发出IPI中断,因此还需要调用设备的相应处理函数,这就是第56~58行代码的目的,函数指针write_emulated指向的函数为emulator_write_emulated:

commit c5ec153402b6d276fe20029da1059ba42a4b55e5KVM: enable in-kernel APIC INIT/SIPI handlinglinux.git/drivers/kvm/kvm_main.cint emulator_write_emulated(unsigned long addr, const void *val,…){ … return emulator_write_emulated_onepage(addr, val, …);}static int emulator_write_emulated_onepage(unsigned long addr,…){ … mmio_dev = vcpu_find_mmio_dev(vcpu, gpa); if (mmio_dev) { kvm_iodevice_write(mmio_dev, gpa, bytes, val); return X86EMUL_CONTINUE; } …}

函数emulator_write_emulated_onepage根据目的操作数的地址找到MMIO设备,然后kvm_iodevice_write调用具体MMIO设备的处理函数。对于LAPIC模拟设备,这个函数是apic_mmio_write。如果Guest内核写的是icr寄存器,可以清楚地看到伴随着这个“写icr寄存器”的动作,LAPIC还有另一个副作用,即向其他CPU发送IPI:

commit c5ec153402b6d276fe20029da1059ba42a4b55e5KVM: enable in-kernel APIC INIT/SIPI handlinglinux.git/drivers/kvm/lapic.cstatic void apic_mmio_write(struct kvm_io_device *this, …){ … case APIC_ICR: … apic_send_ipi(apic); …}

鉴于LAPIC的寄存器的访问非常频繁,所以Intel从硬件层面做了很多支持,比如为访问LAPIC的寄存器增加了专门退出的原因,这样就不必首先进入缺页异常函数来尝试处理,当缺页异常函数无法处理后再进入指令模拟函数,而是直接进入LAPIC的处理函数:

commit f78e0e2ee498e8f847500b565792c7d7634dcf54KVM: VMX: Enable memory mapped TPR shadow (FlexPriority)linux.git/drivers/kvm/vmx.cstatic int (*kvm_vmx_exit_handlers[])(…) = { … [EXIT_REASON_APIC_ACCESS] = handle_apic_access,};static int handle_apic_access(struct kvm_vcpu *vcpu, …){ … er = emulate_instruction(vcpu, kvm_run, 0, 0, 0); …}

2. PIO

PIO使用专用的I/O指令(out、outs、in、ins)访问外设,当Guest通过这些专门的I/O

指令访问外设时,处于Guest模式的CPU将主动发生陷入,进入VMM。Intel PIO指令支持两种模式,一种是普通的I/O,另一种是string I/O。普通的I/O指令一次传递1个值,对应于x86架构的指令out、in?;string I/O指令一次传递多个值,对应于x86架构的指令outs、ins。因此,对于普通的I/O,只需要记录下val,而对于string I/O,则需要记录下I/O值所在的地址。

我们以向块设备写数据为例,对于普通的I/O,其使用的是out指令,格式如表1所示。

表1 out指令格式

我们可以看到,无论哪种格式,out指令的源操作数都是寄存器al、ax、eax系列。因此,当陷入KVM模块时,KVM模块可以从Guest的rax寄存器的值中取出Guest准备写给外设的值,KVM将这个值存储到结构体kvm_run中。对于string类型的I/O,需要记录的是数据所在的内存地址,这个地址在陷入KVM前,CPU会将其记录在VMCS的字段GUEST_LINEAR_ADDRESS中,KVM将这个值从VMCS中读出来,存储到结构体kvm_run中:

commit 6aa8b732ca01c3d7a54e93f4d701b8aabbe60fb7[PATCH] kvm: userspace interfacelinux.git/drivers/kvm/vmx.cstatic int handle_io(struct kvm_vcpu *vcpu, …){ … if (kvm_run->io.string) { … kvm_run->io.address = vmcs_readl(GUEST_LINEAR_ADDRESS); } else kvm_run->io.value = vcpu->regs[VCPU_REGS_RAX]; /* rax */ return 0;}

然后,程序的执行流程流转到I/O模拟设备,模拟设备将从结构体kvm_run中取出I/O相关的值,存储到本地文件镜像或通过网络发给存储集群。I/O模拟的更多细节我们将在“设备虚拟化”一章讨论。

02 特殊指令

有一些指令从机制上可以直接在Guest模式下本地运行,但是其在虚拟化上下文的语义与非虚拟化下完全不同。比如cpuid指令,在虚拟化上下文运行这条指令时,其本质上并不是获取物理CPU的特性,而是获取VCPU的特性;再比如hlt指令,在虚拟化上下文运行这条指令时,其本质上并不是停止物理CPU的运行,而是停止VCPU的运行。所以,这种指令需要陷入KVM进行模拟,而不能在Guest模式下本地运行。在这一节,我们以这两个指令为例,讨论这两个指令的模拟。

1. cpuid指令模拟

cpuid指令会返回CPU的特性信息,如果直接在Guest模式下运行,获取的将是宿主机物理CPU的各种特性,但是实际上,通过一个线程模拟的CPU的特性与物理CPU可能会有很大差别。比如,因为KVM在指令、设备层面通过软件方式进行了模拟,所以这个模拟的CPU可能要比物理CPU支持更多的特性。再比如,对于虚拟机而言,其可能在不同宿主机、不同集群之间迁移,因此也需要从虚拟化层面给出一个一致的CPU特性,所以cpuid指令需要陷入VMM特殊处理。

Intel手册中对cpuid指令的描述如表2所示。

表2 cpuid指令

cpuid指令使用eax寄存器作为输入参数,有些情况也需要使用ecx寄存器作为输入参数。比如,当eax为0时,在执行完cpuid指令后,eax中包含的是支持最大的功能(function)号,ebx、ecx、edx中是CPU制造商的ID;当eax值为2时,执行cpuid指令后,将在寄存器eax、ebx、ecx、edx中返回包括TLB、Cache、Prefetch的信息;再比如,当eax值为7,ecx值为0时,将在寄存器eax、ebx、ecx、edx中返回处理器扩展特性。

起初,KVM的用户空间通过cpuid指令获取Host的CPU特征,加上用户空间的配置,定义好VCPU支持的CPU特性,传递给KVM内核模块。KVM模块在内核中定义了接收来自用户空间定义的CPU特性的结构体:

commit 06465c5a3aa9948a7b00af49cd22ed8f235cdb0fKVM: Handle cpuid in the kernel instead of punting to userspacelinux.git/include/linux/kvm.hstruct kvm_cpuid_entry { __u32 function; __u32 eax; __u32 ebx; __u32 ecx; __u32 edx; __u32 padding;};

用户空间按照如下结构体kvm_cpuid的格式组织好CPU特性后,通过如下KVM模块提供的接口传递给KVM内核模块:

commit 06465c5a3aa9948a7b00af49cd22ed8f235cdb0fKVM: Handle cpuid in the kernel instead of punting to userspacelinux.git/include/linux/kvm.h/* for KVM_SET_CPUID */struct kvm_cpuid { __u32 nent; __u32 padding; struct kvm_cpuid_entry entries[0];};linux.git/drivers/kvm/kvm_main.cstatic long kvm_vcpu_ioctl(struct file *filp, unsigned int ioctl, unsigned long arg){ … case KVM_SET_CPUID: { struct kvm_cpuid __user *cpuid_arg = argp; struct kvm_cpuid cpuid; … if (copy_from_user(&cpuid, cpuid_arg, sizeof cpuid)) goto out; r = kvm_vcpu_ioctl_set_cpuid(vcpu, &cpuid, cpuid_arg->entries); …}static int kvm_vcpu_ioctl_set_cpuid(struct kvm_vcpu *vcpu, struct kvm_cpuid *cpuid, struct kvm_cpuid_entry __user *entries){ … if (copy_from_user(&vcpu->cpuid_entries, entries, cpuid->nent * sizeof(struct kvm_cpuid_entry))) …}

KVM内核模块将用户空间组织的结构体kvm_cpuid复制到内核的结构体kvm_cpuid_entry 实例中。首次读取时并不确定entry的数量,所以第1次读取结构体kvm_cpuid,其中的字段nent包含了entry的数量,类似读消息头。获取了entry的数量后,再读结构体中包含的entry。所以从用户空间到内核空间的复制执行了两次。

事实上,除了硬件支持的CPU特性外,KVM内核模块还提供了一些软件方式模拟的特性,所以用户空间仅从硬件CPU读取特性是不够的。为此,KVM后来实现了2.0版本的cpuid指令的模拟,即cpuid2,在这个版本中,KVM内核模块为用户空间提供了接口,用户空间可以通过这个接口获取KVM可以支持的CPU特性,其中包括硬件CPU本身支持的特性,也包括KVM内核模块通过软件方式模拟的特性,用户空间基于这个信息构造VCPU的特征。具体内容我们就不展开介绍了。

在Guest执行cpuid指令发生VM exit时,KVM会根据eax中的功能号以及ecx中的子功能号,从kvm_cpuid_entry实例中索引到相应的entry,使用entry中的eax、ebx、ecx、edx覆盖结构体vcpu中的数组regs中相应的字段。当再次切入Guest时,KVM会将它们加载到物理CPU的通用寄存器,这样在进入Guest后,Guest就可以从这几个寄存器读取CPU相关信息和特性。相关代码如下:

commit 06465c5a3aa9948a7b00af49cd22ed8f235cdb0fKVM: Handle cpuid in the kernel instead of punting to userspacevoid kvm_emulate_cpuid(struct kvm_vcpu *vcpu){ int i; u32 function; struct kvm_cpuid_entry *e, *best; … function = vcpu->regs[VCPU_REGS_RAX]; … for (i = 0; i < vcpu->cpuid_nent; i) { e = &vcpu->cpuid_entries[i]; if (e->function == function) { best = e; break; } … } if (best) { vcpu->regs[VCPU_REGS_RAX] = best->eax; vcpu->regs[VCPU_REGS_RBX] = best->ebx; vcpu->regs[VCPU_REGS_RCX] = best->ecx; vcpu->regs[VCPU_REGS_RDX] = best->edx; } … kvm_arch_ops->skip_emulated_instruction(vcpu);}

最后,我们以一段用户空间处理cpuid的过程为例结束本节。假设我们虚拟机所在的集群由小部分支持AVX2的和大部分不支持AVX2的机器混合组成,为了可以在不同类型的Host之间迁移虚拟机,我们计划CPU的特征不支持AVX2指令。我们首先从KVM内核模块获取其可以支持的CPU特征,然后清除AVX2指令的支持,代码大致如下:

struct kvm_cpuid2 *kvm_cpuid;kvm_cpuid = (struct kvm_cpuid2 *)malloc(sizeof(*kvm_cpuid) CPUID_ENTRIES * sizeof(*kvm_cpuid->entries));kvm_cpuid->nent = CPUID_ENTRIES;ioctl(vcpu_fd, KVM_GET_SUPPORTED_CPUID, kvm_cpuid);for (i = 0; i < kvm_cpuid->nent; i ) { struct kvm_cpuid_entry2 *entry = &kvm_cpuid->entries[i]; if (entry->function == 7) { /* Clear AVX2 */ entry->ebx &= ~(1 << 6); break; };}ioctl(vcpu_fd, KVM_SET_CPUID2, kvm_cpuid);

2. hlt指令模拟

当处理器执行hlt指令后,将处于停机状态(Halt)。对于开启了超线程的处理器,hlt指令是停止的逻辑核。之后如果收到NMI、SMI中断,或者reset信号等,则恢复运行。但是,对于虚拟机而言,如果任凭Guest的某个核本地执行hlt,将导致物理CPU停止运行,然而我们需要停止的只是Host中用于模拟CPU的线程。因此,Guest执行hlt指令时需要陷入KVM中,由KVM挂起VCPU对应的线程,而不是停止物理CPU:

commit b6958ce44a11a9e9425d2b67a653b1ca2a27796fKVM: Emulate hlt in the kernellinux.git/drivers/kvm/vmx.cstatic int handle_halt(struct kvm_vcpu *vcpu, …){ skip_emulated_instruction(vcpu); return kvm_emulate_halt(vcpu);}linux.git/drivers/kvm/kvm_main.cint kvm_emulate_halt(struct kvm_vcpu *vcpu){ … kvm_vcpu_kernel_halt(vcpu); …}static void kvm_vcpu_kernel_halt(struct kvm_vcpu *vcpu){ … while(!(irqchip_in_kernel(vcpu->kvm) && kvm_cpu_has_interrupt(vcpu)) && !vcpu->irq_summary && !signal_pending(current)) { set_current_state(TASK_INTERRUPTIBLE); … schedule(); … } … set_current_state(TASK_RUNNING);}

VCPU对应的线程将自己设置为可被中断的状态(TASK_INTERRUPTIBLE),然后主动调用内核的调度函数schedule()将自己挂起,让物理处理器运行其他就绪任务。当挂起的VCPU线程被其他任务唤醒后,将从schedule()后面的一条语句继续运行。当准备进入下一次循环时,因为有中断需要处理,则跳出循环,将自己设置为就绪状态,接下来VCPU线程则再次进入Guest模式。

03 访问具有副作用的寄存器

Guest在访问CPU的很多寄存器时,除了读写寄存器的内容外,一些访问会产生副作用。对于这些具有副作用的访问,CPU也需要从Guest陷入VMM,由VMM进行模拟,也就是完成副作用。

典型的比如前面提到的核间中断,对于LAPIC而言,写中断控制寄存器可能需要LAPIC向另外一个处理器发送核间中断,发送核间中断就是写中断控制寄存器这个操作的副作用。因此,当Guest访问LAPIC的中断控制寄存器时,CPU需要陷入KVM中,由KVM调用虚拟LAPIC芯片提供的函数向目标CPU发送核间中断。

再比如地址翻译,每当Guest内切换进程,Guest的内核将设置cr3寄存器指向即将运行的进程的页表。而当使用影子页表机制完成虚拟机地址(GVA)到宿主机物理地址(HPA)的映射时,我们期望物理CPU的cr3寄存器指向KVM为Guest中即将投入运行的进程准备的影子页表,因此当Guest切换进程时,CPU需要从Guest陷入KVM中,让KVM将cr3寄存器设置为指向影子页表。因此,当使用影子页表机制时,KVM需要设置VMCS中的Processor-Based VM-Execution Controls的第15位CR3-load exiting,当设置了CR3-load exiting后,每当Guest访问物理CPU的cr3寄存器时,都将触发物理CPU陷入KVM,KVM调用函数handle_cr设置cr3寄存器指向影子页表,如下代码所示。

commit 6aa8b732ca01c3d7a54e93f4d701b8aabbe60fb7[PATCH] kvm: userspace interfacelinux.git/drivers/kvm/vmx.cstatic int handle_cr(struct kvm_vcpu *vcpu, …){ u64 exit_qualification; int cr; int reg; exit_qualification = vmcs_read64(EXIT_QUALIFICATION); cr = exit_qualification & 15; reg = (exit_qualification >> 8) & 15; switch ((exit_qualification >> 4) & 3) { case 0: /* mov to cr */ switch (cr) { … case 3: vcpu_load_rsp_rip(vcpu); set_cr3(vcpu, vcpu->regs[reg]); skip_emulated_instruction(vcpu); return 1; …}

关于作者:王柏生,资深技术专家,先后就职于中科院软件所、红旗Linux和百度,现任百度主任架构师。在操作系统、虚拟化技术、分布式系统、云计算、自动驾驶等相关领域耕耘多年,有着丰富的实践经验。著有畅销书《深度探索Linux操作系统》(2013年出版)。

谢广军,计算机专业博士,毕业于南开大学计算机系。资深技术专家,有多年的IT行业工作经验。现担任百度智能云副总经理,负责云计算相关产品的研发。多年来一直从事操作系统、虚拟化技术、分布式系统、大数据、云计算等相关领域的研发工作,实践经验丰富。

*本文经出版社授权发布,更多关于虚拟化技术的内容推荐阅读《深度探索Linux系统虚拟化:原理与实现》。

为啥固态硬盘越用越慢?还可以拯救一下!

你是否感觉固态硬盘越用越慢?这是咋回事呢?

江湖上关于固态硬盘传说那么多,哪些是真的呢?

01

固态硬盘为啥越用越慢?

要想回答这个问题,就不得不提到固态硬盘的擦写过程(P/E)。

为了便于理解,我们用这些小格子代表闪存的单位存储空间,其中白色的格子表示空白的存储空间,蓝色的格子表示已存有数据的存储空间。用小格子组成的灰色区域表示一块储存区域。

当你删除了某个文件,硬盘并不会立刻擦除对应位置的数据,而是会给它打上一个标记:“空”。

当你要存储某个文件时,硬盘也不会在标记的位置写入新的数据,而是会把数据存进空白的区域,利用更多的存储空间。

随着读写次数的增加,硬盘里的空白区域很快就被用的差不多了,此时如果还要写入新的数据,就需要把这些带有标记的位置腾出来,也就是擦除带标记的数据。

但要擦除数据,不能只清除对应的格子,而是必须把一整块区域清空,才能写入新的数据。

这个过程被称作写入放大(WAF),它意味着更复杂的步骤、更长的耗时、以及更多的擦写次数。

我们举一个具体的例子:

当要写入一个的数据“7”时,最坏的情况是一个块里已经没有干净的格子了,但是其中无效的数据(标记为“空”)能够擦除,所以要把全部的数据都读到缓存,擦除区域内里的所有数据,再把新数据写进去。

那么这个操作带来的写入放大就是:原本只需要写入一个格子,实际上却造成了整个块的写入操作。

同一时间还造成原本仅仅需要简单一步写入数据的操作变成了:

缓存读取整个区域-缓存修改数据-擦除清空整个区域-写入所有数据,共四步操作,与直接写入相比延迟将大大增加。

所以说:固态硬盘越用越慢?还不怪你塞的太满~

知识丰富的小伙伴都知道,对于计算机领域来说256和512这样的数才是“整数”,为什么市面上的固态硬盘经常是240G或者480G呢?这是通过预留空间,强行阻止大家把硬盘塞满,从而延长硬盘的使用时间。

把磁盘塞得太满虽说会影响固态硬盘的性能,但是真正影响寿命的其实是擦写次数,当擦写次数到极限时,固态硬盘就会损坏,里面的数据也会丢失,那么该如何判断固态硬盘可以“活”多久呢?

02

如何计算固态硬盘寿命


固态硬盘寿命计算非常简单:

举例说明一下:

如果土豪的你买了一块2TB的固态硬盘,总擦写次数为1000,假设每天向这块固态硬盘写入100GB的大容量数据,那么根据计算,可以得到:

使用寿命:(2048*1000)/(100*365)=56年。

一块好的固态硬盘,好好使用它,它可以一直陪着你到退休~~

03

如何保养固态硬盘?江湖传言都是真的吗?

读到这里,其实你已经知道不要把硬盘塞得太满,但是江湖上流传着很多固态硬盘保养的经验!那些都是真的吗?让小编悄悄告诉你~

分区会影响固态硬盘使用性能—假!

固态硬盘分区基本不会影响硬盘性能。

有人可能会说“数据存储在C盘,读取速度最快”,“每分一个区,系统都会预留一定的缓存,造成一定的容量浪费。”“擦写次数是有限制的,长期对系统C盘读写,可能会导致缩短寿命。”其实对于固态硬盘来说,这些担心都是多余的!

机械硬盘主轴是以恒定的角速度转动,这样扫过的磁盘扇区面积,靠外圈的永远大于靠内圈的,C盘位于外圈,单位时间内,磁头扫过的扇区面积最大,所以系统装进C盘速度最快,之后的D、E、F盘速度则递减。

而固态硬盘主要由主控和闪存颗粒等部件组成,没有机械盘的机械结构。数据无论存在固态的哪一块,速度都一样。

而且随着固态硬盘的不断发展,目前市面上的固态硬盘主控也在不断升级,在垃圾回收、预留OP空间、自动分配、擦写均衡等功能方面日益成熟。不用担心分区会造成容量浪费或者某一位置的过度擦写。

所以,小编建议固态硬盘按需要分区就好,如果经费有限,配的是256G以下的固态硬盘,小编建议就不要分区了。

磁盘碎片整理功能可以优化性能—假!

在机械硬盘中,文件不是连续地保存在磁盘连续的簇中,而是哪有空存在哪里,由于文件被分散保存到整个磁盘的不同地方,就产生了磁盘碎片。在存新删旧的过程中,磁盘碎片就变得散乱不堪。在这种情况下,读取就增加了硬盘寻道时间,也增加了能耗。

因此磁盘碎片整理功能就是通过整理磁盘碎片文件,让碎片文件合成一体,加速了磁盘的寻道时间,从而整体上加快了速度。

那么为什么固态硬盘不需要磁盘整理呢?

固态硬盘寻道时间几乎为零:固态硬盘的存储单元,是基于闪存颗粒的电子存储,因而在寻道时间上理论上是永恒不变的,其性能则主要取决于主控芯片的性能和闪存颗粒的工艺。

固态硬盘有读写次数限制:一次磁盘碎片整理就相当于一次全盘读写,相当于擦写次数减1,可见磁盘整理多么损耗固态硬盘寿命。

其实,系统识别到安装的是固态硬盘后,都会默认关闭这个功能。所以我们不要因为看起来可以“节约硬盘空间”,在使用固态硬盘时把它打开。

要更新固态硬盘固件—真!

如果我们把固态硬盘比作一台小型的电脑,那么固件可以理解为其操作系统。固件控制固态硬盘一切的内部操作,可以直接影响固态硬盘的性能、稳定性以及寿命。

优秀的固件能减少固态硬盘不必要的写入,从而在提升固态硬盘性能的同时延长固态硬盘的寿命。所以我们要及时更新官方发布的最新固件。

开启Trim可以提升硬盘性能?—真!

Trim指令也叫做disable delete notify(禁用删除通知)。之前我们说过,固态硬盘删除一个数据是先给数据打上标记,等到系统要求在标记的地方写入数据的时候才会把不用的数据擦除,这样其实无法在最适当的时机做出最好的优化。

Trim就可以很好的解决这个问题,开启Trim之后,固态硬盘可以立即将需要删除的内容删除,而避免了等到要写入数据时候才删除数据的尴尬。

Trim没有改变原本要做的事情,只是提前做完了。所以在大家使用的时候,速度就上去了~Trim可以有效地降低写入放大,从而获得更高的吞吐量,增加固态硬盘的耐久度。

但是!!开启Trim后,对于误删的数据,想要恢复,基本就不可能了……

总结来说,固态硬盘越用越慢的原因是由于固态硬盘机制基于写入放大原理。所以为了保证固态硬盘可以一直高速运行,我们要:

合理使用固态硬盘容量

及时更新固件

开启Trim模式

不要开启磁盘碎片整理功能

最后,希望各位小伙伴的固态硬盘都可以“健康长寿,运行如飞”~~~

转载内容仅代表作者观点

不代表中科院物理所立场

来源:中兴文档

编辑:fiufiu

为君作磐石——人人都能搭建大规模推荐系统

前言

什么是个性化推荐?简单说,就是给用户推荐他喜欢的物品。近 10 年,移动互联网高速发展,个性化推荐扮演了很重要的角色。以运营一款内容类产品为例:用户增长团队通过广告投放等手段为产品拉新,提升 DAU;产品技术团队为用户分发感兴趣的内容,提升留存及停留时长;商业化团队分发用户可能感兴趣的广告,提升单位流量变现效率;商业化收入又用于用户增长,形成正向循环。个性化推荐技术贯穿每个环节,成为了很多公司的高速增长引擎。

怎么做个性化推荐?通常,对一项业务来说,首先会定义出多个优化目标(例如视频的播放时长、点赞、分享,电商的点击、加购、购买等),之后构建一个或多个模型来预估这些目标,最后融合多个目标的预估分来完成排序。对推荐系统来说,最核心的工作,便是构建精准的预估模型。这些年,业界的推荐模型一直朝着大规模、实时化、精细化的趋势不断演进。大规模是指数据量和模型非常大,训练样本达到百亿甚至数万亿,单个模型达到 TB 甚至 10TB 以上;实时化是指特征、模型、候选实时更新;精细化则在特征工程、模型结构、优化方法等多方面有所体现,各种创新思路层出不穷。

大规模推荐系统的落地,工程挑战很大。本文选择大家最关心的 Training 和 Serving 系统,介绍搭建过程中会遇到哪些挑战,我们做了哪些工作。对任何一家公司来说,从 0 搭建这样一套系统都绝非易事,投入非常大。在字节跳动内部,我们也经过了多年的探索与沉淀,有上千名工程师,不断迭代和优化推荐系统。那么,搭建推荐系统一般会遇到哪些问题?我们先来看一个故事:

A公司的故事

A是一家电商公司,他们的产品有300万DAU,有一个10人的算法团队,他们在搭建推荐系统的过程中,遇到了不少麻烦,我们具体来看看。

A公司想训练一个点击率模型,每天有1亿次曝光,100万次点击,他们想用3个月的数据训练模型,样本量级达到90亿。他们设计了200个特征,包含用户ID、商品ID、用户的点击序列等,想为每个特征分配16维的向量来表征,粗略计算下来模型大小为500G。分析之后,他们发现要做分布式训练和模型存储,于是调研了一些开源方案:

TensorFlow:Google开源的机器学习系统,可以使用Partitioned Variable来分布式地存储Embedding,从而实现大规模训练。但由于table size固定,有hash冲突风险。

PyTorch:Facebook开源的机器学习系统,使用Ring All Reduce同步参数,要求单机能容纳所有参数,难以训练超大模型。

XDL:国内开源的机器学习系统,自研PS系统,用TF作为训练引擎,并且内置了一些开箱即用的推荐模型。功能上可以实现大规模训练,但是这套系统开源支持较弱,使用在生产中有风险。

Angel:国内开源的机器学习系统,其特点是与大数据系统Spark紧密结合,使用Spark完成数据预处理与特征工程。自研Parameter Server,内嵌Pytorch为训练引擎,可以训练超大模型。但是Angel的在线离线特征难以保证一致性,只适合做离线训练平台。

经过对比,A公司选择了Tensorflow来做分布式训练。但是,训练模型的时候发现速度非常慢,即使投入大量资源依然需要5天才能训完3个月的数据。他们花了很多时间研究Tensorflow,profiling训练过程,发现了一些问题:

TensorFlow 的分布式runtime 性能不好, 对于每个特征都单独产生了一对send/recv op来连接worker 和 PS,这样单个worker 就跟 PS 产生了200个send/recv,造成了TensorFlow Runtime的调度困难,降低了分布式训练的速度。

训练过程中CPU的使用率非常不稳定,看起来CPU并没有被充分利用起来。

有些算子运算的特别慢,推测可能和内存带宽有关。

虽然网络带宽并没有满载,但是增加更多的机器不能够再提升训练速度了。

浏览TF官方网站的时候发现TF最近推出了各种不同的分布式策略,它们又分别对应着训练集群不同的拓扑结构。他们非常疑惑,不知道应该选择哪一种。

虽然发现了不少性能问题,但优化起来并不十分容易。经过一段时间的努力,他们优化了部分问题,将训练时间从5天压缩到了3天,勉强可以接受。但是,当训练进行到第40小时的时候,因为一台机器OOM,训练任务挂了。他们多尝试了几次,发现训练成功率比较低, 分析之后发现主要原因是:

TF 基于静态拓扑配置来构建 Cluster,不支持动态组网,这就意味着当某个 ps 或者 worker 挂掉重启之后,如果 ip 或者端口发生变化(例如机器 crash),训练将无法继续。

TF 的 checkpoint 只包含 PS 存储的参数信息,不包含 worker 端的状态,不是全局一致性的 checkpoint,无法实现 Exactly-Once 语义。

做好容错挑战不小,他们只能先隔离一个独立的集群,让训练尽量稳定一些。不能和其他任务混合调度,资源利用率自然也要低不少。

几经波折,勉强训好了一个500G的模型,他们想把模型推到线上去Serving,于是考虑在线系统的设计。经过一番讨论,他们认为Serving系统必须满足如下要求:

分布式:推荐模型的特点是有大量的Embedding,模型很容易达到TB级,考虑未来的模型迭代,必须支持分布式Serving。

低时延:单次预估的延时要尽量低,精排模型一般要控制在80ms内。复杂的深度模型,可能需要GPU来Serving,并做一系列的性能优化。

高可用:少部分节点挂掉不影响在线稳定性,一般通过多副本解决,需要调度系统的支持。

少抖动:模型更新、上线、下线等操作,不会造成延时抖动。

AB测试:推荐系统迭代很快,算法工程师会开展很多AB实验,实验组的流量会动态调整,在线系统需要能支持模型、服务的动态调度。

目前,没有开源系统能满足上述要求,各大公司都是自研,实际做起来投入也不小。A公司人力有限,经验也不足,只能先通过一些模型压缩的手段,让单机可以Serving,模型也不能做得太复杂。

模型上线之后,A公司又遇到一个新的问题:如何更新模型。定期全量重训成本很高,如果线上有多个同时ABTest的模型,更是会雪上加霜。所以,至少要做到天级的增量更新,实时更新自然更好。但增量/实时更新,实现起来也不太容易。其实,未来还有更多的问题等着A公司,比如:如何保证线上线下特征的一致性;上游数据流不稳定怎么办;如何解决模型越来越大的问题;如何做好多场景数据的混合训练;如何应对大规模候选的问题;如何解决转化事件大幅延迟的问题等等。

我们的工作

通过A公司的故事,大家能看到,开发一套大规模推荐系统,难度确实不小,成本也很高。那么,有没有一款产品可以直接覆盖数据校验、特征工程、模型开发、线上服务、AB测试等全流程,让业务轻松搭建一套一流的推荐系统,不再遭遇A公司的头疼问题呢?有。

字节跳动成立火山引擎之后,我们一直在努力,将字节的推荐技术开放给外部客户。如今,我们已经可以通过火山引擎的智能推荐平台,来帮助大家解决这些难点和痛点。目前这套平台也开放了部分名额供企业免费使用,具体信息可以在文末进行了解。

接下来,再展开介绍一下,智能推荐平台中的大规模Training和Serving方案,我们把它命名为Monolith(磐石),希望它能成为大家做推荐系统的坚实基础,如下是架构图:

从图中可以看出,Monolith是PS架构,下面看看这套架构是怎样运行的:

批量/增量训练

Worker/PS启动时会向ZK注册,信息包括(server_type,index)。然后Worker向ZK请求注册信息,生成Cluster信息,实现动态组网,动态组网是容错的基础。

训练开始后,Worker会从标准输入或文件中获取数据,同时从PS拉取参数,然后进行forward/backward计算,得到梯度,并将其Push给PS。

PS获得梯度后,一方面,利用优化器更新内部weight,另一方面,会记录哪些数据更新了。在PS上起一个TF Session,它会定时将更新的参数发送到Online PS,从而实现实时增量更新。此外,特征过滤,特征淘汰等也在PS上进行。

在训练过程中或训练结束时,会写checkpoint。为了加速checkpoint,Monolith 没有延用TF 中的saveable,而是利用estimator saving listener,流式多线程地存取,性能大副提升。为了减少checkpoint体积,会将过期特征淘汰。

在线推理

加载saved_model。Entry本质上是TF Serving,它会从HDFS上加载非Embedding部分,同时向ZK注册,以便上层做负载均衡。Online PS也会先向ZK注册,然后从HDFS中加载参数,并在加载过程中去除优化器辅助参数,将fp32转换成fp16,量化压缩等。

对于一次请求,Entry会随机选择一组Online PS,从中获取Embedding,完成预测。Entry/Online PS 是多副本的,只要有一个副本存在,服务就可用。Online PS是多分片的,可以Serving超大模型。可以在一台机器上部署多个分片,也可以Entry/OnlinePS混部。

对于一些对模型实时性较高的系统,Training PS会直接通过RPC的方式与Online PS进行通讯,从而将样本反馈到线上模型的时间间隔缩短到分钟级。

Training PS可以与Online PS通讯,接受Training PS的参数更新;Entry可以自动从HDFS上读取更新参数,从而实现分钟级参数增量更新。

综上所述,Monolith 包括了 Training/Serving/Parameter Sync等,是一套完整的系统。

与业界其它系统相比,Monolith成功应对了多方面的挑战,有如下特色:

解决了TensorFlow PS 通信瓶颈

在工业级的推荐模型中,我们常会使用几百甚至数千类特征,每类特征都需要创建哈希表去存储特征embeddings。直接为每类特征生成一张哈希表,同时对几百张表进行查找会导致两个问题:

    PS和Worker连接会产生过多的 send/recv op,大大影响分布式 runtime 的运行效率。

    这些 ops 导致模型图节点过多,模型图过大,训练初始化时间过长。

针对如上问题,我们在框架层面做了优化:对于配置同构的哈希表(dim 相同、优化器参数相同),在python API 层面合并哈希表来减少表的数量,同时monolith会对通信op进行进一步的合并,从而极大地减少了send/recv ops,解决了原生TensorFlow PS 的通信问题。

针对异步训练,monolith还开发了变量与embedding预取以及梯度异步更新的功能,对于多数模型,能够更加有效的利用带宽与CPU,从而提高训练速度,优化资源利用率。

全方位容错

在服务发现的基础上,无论是Worker还是PS发生错误,都能得到快速恢复。对于Worker,Monolith不同worker节点之间并不直接进行通信,所以一个worker的失败并不会对别的worker产生影响;同时,worker会存储输入的进度,当worker因为意外原因失败时,输入的进度并不会丢失;当PS shard 节点失败,根据离线/在线任务的性质不同,支持部分恢复和全量恢复不同的模式,在正确性以及恢复速度上做一定的取舍。

分布式Serving

Monolith补齐了开源软件在分布式Serving方面的空白,提供了TB级模型的推理服务。支持多副本、高可用,Training PS在训练过程中,分钟级别将刚刚更新过的Embedding同步给Serving PS,从而实现近实时参数更新,提升了产品的推荐效果。

性能优化

除了上面提到的解决 TensorFlow PS 通信瓶颈之外,Monolith 在 Parameter Server 架构、底层 Hash Table 设计、网络传输、多线程加速、OP Fusion、指令集加速等方向也进行了非常细致的优化并取得了可观的性能收益。以异步训练为例,训练时整个过程示意如下:

网络通讯优化:通过 embedding prefetch, gradients postpush 将网络 IO 与图的前向/后向计算异步起来,同时支持控制流与数据流分离、压缩传输等优化;

内存优化:通过支持特征过滤、特征压缩、特征淘汰等手段,可以极大地节省 training/serving 阶段内存使用;

计算优化:Hot spot code 采用 AVX 指令集优化、耗时 Op 精细调优、手工 Op Fusion 等手段加速前向/后向计算过程;

其它方面:多线程优化、细粒度锁设计、IO与计算异步起来等。

目前,Monolith已通过推荐平台,成功应用在电商、社区、视频等多个行业的场景上,效果、稳定性、性能均得到了充足的验证。未来,我们也将继续保持高速迭代,不断优化用户体验和平台功能。

一份礼物

谢谢大家看到这里。目前,字节跳动的这款智能推荐平台已通过火山引擎开放给企业伙伴使用。如果你的企业希望应用推荐算法来帮助业务增长,却也为搭建一套推荐系统而头疼,不妨试试火山引擎智能推荐平台。更详细的信息可以点击文档中心-火山引擎了解:

值得一提的是,当前智能推荐平台开放了30个名额供企业伙伴免费使用,免费时间截止至2021年11月30日。希望领取名额的同学,还请尽快扫描下方二维码报名:

写在最后

最后,介绍一下,我们是火山引擎-智能推荐团队,致力于让全球范围内的企业,都能拥有顶尖的推荐系统。非常欢迎机器学习系统、推荐架构、推荐算法方向的同学加入我们,base地:北京、深圳、杭州、新加坡,简历投递邮箱:ai-coop@bytedance.com,邮件标题:姓名 - 工作年限 - 火山引擎智能推荐 - 职位方向,期待与各位的合作!