虚拟机和容器常常被用来进行比较,因为它们有着相似的功能,都可以提供独立环境来运行软件服务。实际上,两者都是虚拟化技术的一种,工作在不同的抽象层级。但为了区分,有人将两种技术分别称为虚拟化技术和容器化技术。由于抽象层级的不同,它们各自的运行原理和隔离技术也不相同。
系统级虚拟化之硬件虚拟
系统级虚拟化的目标是虚拟出一台完整的计算机,其拥有底层的物理硬件、操作系统和应用程序执行的完整环境。HyperVisor在这个过程中做了大量的工作,让虚拟机像是在真实物理机器上运行。
CPU虚拟化
在x86架构中CPU为了保护特权指令的运行,提供了指令的4个不同Privilege特权级别,术语称为Ring,从Ring 0~Ring 3。Ring 0的优先级最高,可运行所有特权指令,Ring 3最低。而对于一个OS来说它必须是运行在Ring0上的,当然对于GuestOS来说也是如此,因此对于Hypervisor就必须模拟让GuestOS认为自己是运行在Ring0上。
面临的问题
- 所有OS设计时都认为Kernel是可以控制所有硬件,并可运行CPU的特权指令,即Kernel运行于CPU的Ring0上。
- 多个OS无法同时直接运行于硬件层之上,它们必须运行在Hypervisor层(下文称:Host)上,那问题来了,若GuestOS必须运行在CPU的Ring0上,Host运行在哪里?
- OS设计时它认为自己是可以控制所有硬件资源的,那GuestOS之间不就可以相互影响了吗?Guest1要关机,若它直接执行CPU的特权指令关机,那它应该可以关闭整个物理机器,这不是虚拟化所希望的。
这些问题给CPU虚拟化带来了诸多问题,但实际上Host一定是真正能执行CPU的特权指令的,Guest运行起来后,它实际控制的CPU是通过软件模拟的CPU,实际上任何物理硬件都是通过集成电路进行运算,通过微代码向外提供输出结果的接口,只有通过软件模拟出这些接口就可以模拟硬件。
BinaryTranslation技术
即二进制翻译技术,后文简称BT,BT可以让Guest在发起特权指令时,直接将其翻译为Host的系统调用,以便加速Guest的执行性能。也就是说它的作用是将GuestOS发出的特权指令,转换为非特权指令,再交由宿主OS执行。它最早是由VMware采用的一种动态翻译技术,hypervisor将捕获的GuestOS发出的特权指令直接在其进程内部进行转换翻译,最终达到Hypervisor所允许执行的指令,再交由HostOS执行,这使得GuestOS的执行性能不高。
硬件辅助
由于Hypervisor并不是所有特权指令都可捕获,且有些特权指令执行失败后,并不会返回给GuestOS任何信息,还有一些其他问题Hypervisor都不能很好的解决,因此,后来Intel和AMD分别通过硬件实现了直接由CPU自身来实现虚拟化,即VT-X(intel) 和 AMD-V。
这两种解决方案的原理是一样的,新一代CPU在原先的Ring0-Ring3四种工作状态之下,再引入了一个叫工作模式的概念,有VMX root operation和VMX non-root operation两种模式,每种模式都具有完整的Ring0-Ring3四种工作状态。前者是Hypervisor运行的模式,后者是虚拟机中的OS运行的模式。Hypervisor运行的层次,也可以将其称为Ring -1。
简单来说,在原先4个CPU等级执行环的基础上新增量一个Ring-1的环,将所有CPU的特权指令放到Ring-1中,并让宿主机的Kernel可运行于Ring-1中,这就将Ring0空闲出来,接着让GuestOS的Kernel可运行在Ring0中,GuestOS执行的特性指令直接由CPU进行翻译并交给Ring-1中执行。同时Hypervisor可以通过CPU提供的编程接口,配置对哪些指令的劫持和捕获,从而实现对虚拟机操作系统的掌控。
换句话说,原先的Hypervisor为了能够掌控虚拟机中代码的执行,采用“中间人”来进行翻译执行。现在新的CPU告诉Hypervisor:不用那么麻烦了,你提前告诉我你对哪些指令哪些事件感兴趣,我在执行这些指令和发生这些事件的时候就通知你,你就可以实现掌控了。完全由硬件层面提供支持,性能自然高了不少。
VMX non-root operation是一个相对受限的执行环境,为了适应虚拟化而专门做了一定的修改,在GuestOS执行的一些特殊的敏感指令或者一些异常会触发“VM Exit”退到虚拟机监控器中,从而运行在VMX根模式。正是这样的限制,让Hypervisor保持了对处理器资源的控制。有了硬件辅助虚拟化的加持,KVM等技术也相继出现。
内存虚拟化
无内存虚拟化
内存在没有虚拟化时,GuestOS要请求内存空间时,它需要将请求的线性地址(虚拟地址:Virtual Address),经MMU(内存管理单位,负责虚拟地址与物理地址之间的转化、内存保护、权限控制等事务)转为物理地址(Physical Address)Hypervisor维护了GuestOS的PA与Host的VA的映射,最后由Hypervisor将GuestOS的PA转交给Host的MMU,并由Host的MMU转为Host的PA,最后将数据存入真机的物理内存空间中,过程漫长效率低。
影子页表
影子页表由Hypervisor维护,影子页表是指在上述的基础上,将一路的映射的头尾记录到表中,即GuestOS的VA与Host的PA的映射,这个映射表就是影子页表,也可以称为虚拟的MMU,直接将GuestOS的VA转为Host的PA。
影子页表实现非常复杂,且需要为每个Guest中的每个进程的页表都维护一个对应的影子页表。当发生缺页异常时,会陷入到Hypervisor进行对应的处理。
扩展页表
扩展页表技术即虚拟MMU的硬件虚拟化解决方案。Intel推出了一个EPT(扩展页表技术),而AMD推出了一个NPT(嵌套页表技术)。实现原理大致相同,这里介绍一下EPT的过程。EPT技术由以下两部分组成,这两部分都是由硬件自动完成的:
- 客户机原有的客户机虚拟地址(GVA)到客户机物理地址(GPA)的转换,借助Guest的MMU;
- EPT页表实现从Host物理地址(GPA)到Guest物理地址(HPA)的转换,借助EPT,EPT 页表被载入专门的 EPT 页表指针寄存器 EPTP,进而由MMU执行GPA到HPA的转换。
这里值得说明的是,EPT维护的是虚拟机物理地址(GPA)到宿主机物理地址(HPA)之间的映射关系,所以只需要为每一个虚拟机维护一套EPT页表。且客户机内部的缺页异常不会致使客户机退出陷入Hypervisor中,提高了客户机运行的性能。
I/O虚拟化
| 分类 | 流程示意图 | 优点 | 缺点 |
|---|---|---|---|
| 设备模拟 | 暂时无法在飞书文档外展示此内容 | 兼容性好,不需要额外驱动 | 性能较差 |
| 前后端接口 | 暂时无法在飞书文档外展示此内容 | 性能有所提升virtio | 兼容性较差;依赖客户机中安装特定驱动 |
| I/O透传 | 暂时无法在飞书文档外展示此内容 | 性能非常好Intel VT-d | 需要硬件设备特性支持;单个设备只能分配一个客户机;很难支持动态迁移,Host并不知道设备的运行的内部状态,状态无法迁移或恢复,已有一定程度的解决(vhost-user、vDPA) |
| 设备共享 | 暂时无法在飞书文档外展示此内容 | 性能非常好;设备可共享Intel VT-cVFIO-mdev | 需要硬件设备特性支持;很难支持动态迁移 |
针对I/O透传的热迁移解决方案,一直都在探索和进步。字节跳动提出 KVM 内核热升级方案,效率提升 5.25 倍
网络虚拟化
| 分类 | 示意图 | 说明 |
|---|---|---|
| Bridge 桥接 | 将Host的网卡当成交换机来使用,Host再虚拟出来一块桥接设备,用来接收真正发往本机的数据包,其MAC地址和物理网卡的MAC一致。Guest系统如同有自己的网卡,直接连上网络,Guest系统对于外部直接可见。 | |
| Host-Only 仅主机 | 在Host上虚拟出来一个交换机,让各个Guest都连接到这同一个交换机。为了让Host也能和虚拟机通信,还需要在Host上虚拟出一块网卡,该虚拟网卡也要连接到之前虚拟的交换机上。仅虚拟机之间进行通信,及虚拟机和本机通信,与外部网络隔离。 | |
| NAT网络 | Host虚拟出一个NAT服务,虚拟机的源IP地址(也就是虚拟机的网卡地址)经NAT服务修改为物理网卡地址,再将数据发送出去。目的端无法得知真正的虚拟机的IP。 |
这三种是比较经典常用的方法,除此之外还有在这三种基础上进行小修改的方法,此处不再赘述。
硬盘虚拟化
虚拟机如何使用硬盘的空间:Guest在物理硬盘上划分一个文件来使用。此文件是在Host通过文件系统将磁盘分区,并创建文件系统后分给Guest。在Host上表现的是一个文件,而在Guest上表现的就是一块硬盘,所以还需要hypervisor模拟一个适配器,将此文件模拟成硬盘。
操作系统级虚拟化之OS隔离
操作系统级虚拟化,也是虚拟化的一种,更准确来说,容器是一种对进程进行隔离的运行环境。实际上主要是利用Cgroups与Chroot、Namespace的功能,来提供一个独立的操作系统运行环境,容器本质上是一种特殊的进程。
Cgroups(即Control Groups) 是Linux内核提供的一种可以限制、记录、隔离进程组所使用的物理资源的机制,简单概括就是对资源使用的限制和隔离。而Namespace则负责隔离的控制,在创建容器进程时,指定了一组Namespace参数,这样容器内只能看到当前Namespace所限定的资源、文件、设备、状态等,而Host相关资源是看不到的。
Chroot
在介绍Namespace之前,先介绍一下Chroot,它出现的比较早,是 Linux 内核提供的一个系统调用,从安全性的角度考虑,用于限定用户使用的根目录。Chroot 的逻辑其实非常简单,首先是搜索当前容器进程获得path,把当前进程的文件系统的 root 设置为 path。Chroot 仅仅是在访问文件系统目录的时候,限定了用户的根目录,Chroot内部的文件系统无法访问外部的内容。这是一种障眼法,并不是真正的虚拟化技术,可以发挥的作用也非常有限。Linux Namespace在此基础之上,又提供了对UTS、mount、IPC、PID、User和Net等的隔离机制。
Namespace
Namespace 对 Linux 非常重要,用于实现容器之类的隔离的重要技术,这样为每个容器创建的进程就可以运行在一个独立的命名空间之中。隔离后每个 namespace 看上去就像一个单独的 Linux 系统。
下面先来了解一下进程和Namespace 之间的关系。一个进程可以属于多个Namesapce,在进程创建的时候可以指定Namespace,假如不指定,那么默认所有进程在创建的时候,都会指定一个默认的Namespace。
Namespace类型
这里,我们首先来介绍Linux中的Namespace的几种类型:
| 类型 | 功能概括 | 功能说明 |
|---|---|---|
| UTS Namespace | 提供对主机名的隔离能力 | 使得子进程有独立的主机名和域名。从用户角度来看,多个容器就是多台可以独立使用的主机。那么从系统角度来看,就要对这多个容器进行区分隔离,它们就需要拥有自己独一无二的名字。 |
| Mount Namespace | 提供对磁盘挂载点和文件系统的隔离能力 | 让容器看上去拥有整个文件系统,有/、/bin、/sbin、/etc等目录。Mount Namespace就是隔离文件系统的挂载点,这样进程就只能看到自己的Mount Namespace中的文件系统挂载点。因此,在容器中对文件系统进行的相关操作,并不能影响到host和其他容器的文件系统。 |
| IPC Namespace | 提供对进程间通信的隔离能力 | IPC有共享内存、信号量、消息队列等方法。每个用户空间都有自己的IPC,所以,为了保证同一用户空间进程之间可以通信而跨用户空间的进程之间无法通信,也需要对IPC进行隔离。IPC需要有一个全局的ID,既然是全局的,Namespace需要对这个ID进行隔离,不能让别的Namespace的进程看到。 |
| PID Namespace | 提供对进程的隔离能力 | 在所有用户空间中,都有一个init进程,为进程树的起点。但一个实际的系统中,只存在一个init进程,于是为每一个用户空间创建的所谓的“init”进程,对于该用户空间来说是一个特殊的进程,但对于系统来说,并非init进程。当该特殊进程结束时,那么该用户空间也将消失。 |
| User Namespace | 提供对用户的隔离能力 | 一个系统只应存在一个root用户,而每一个用户空间也需要一个“root”用户,这也就意味着对于用户空间而言的“root”用户对于系统而言只是一个普通用户,而对于所处用户空间而言,可以将该用户ID伪装为ID为0的用户,该用户只能在自身用户空间之内为所欲为。 |
| Network Namespace | 提供对网络的隔离能力 | 让容器拥有自己独立的网卡、IP、路由等资源,因而,两个容器既可以通过网络通信。 |
| Control group (cgroup) Namespace | 隔离 Cgroups 根目录 | 4.6版本加入,是进程的 cgroups 的虚拟化视图,通过 /proc/[pid]/cgroup 和 /proc/[pid]/mountinfo 展示。 |
| Time Namespace | 隔离系统时间 | 5.6版本加入 |
举例:PID Namespace对进程的隔离
准备一台主机,已经安装docker,且已有容器实例,可以通过如下命令对已有实例进行操作
- 查看所有容器:docker ps
- 启动一个容器:docker start id/name
- 查看运行中的容器: docker container ls
- 进入容器中:docker attach id/name
容器 1024c 当中已经安装压力测试工具stress,如需新建容器并安装,以ubuntu为例步骤如下
- apt-get update(当直接安装报错,先对apt-get更新)
- apt-get install stress
下面我们通过一个例子来更直观地看到Namespace对容器的隔离,首先新建并启动进入容器。
docker run -t -i ubuntu:18.04 /bin/bash
在容器内通过ps命令,可以看到容器内的进程,pid为1的进程即为该容器内的init进程。该进程只是在容器中看起来是“init”进程,但在Host上有真实的pid。
可以在容器内通过ps -p 1 -o pid,pidns查看该进程的namespace。
拿到这个namespace,再去Host当中通过lsns查找这个namespace下的进程。
可以看到,namespace为4026532454的空间下的init进程实际上在Host当中的pid为3058。分别在Host和容器中通过pid查看该容器init进程所属的所有namespace,可以看到都属于同一个namespace下。
Cgroup
Linux Cgroup是Linux内核中用来为进程设置资源限制的重要功能,但是我们为什么要做资源限制呢?我们通过Namespace创建的容器通过障眼法的把PID在容器看到的是1号进程,但是在Host上的实际进程是100号,但是100号进程与Host其他所有进程是平等竞争的关系。这就意味着容器里的1号进程可以把所有Host资源吃光。显然这样的设计是不合理的。所以我们通过Cgroup来限制进程对资源的使用情况,主要作用就是限制一个进程组能够使用的资源上限,例如:CPU、内存、磁盘、网络带宽等。
Cgroup子系统
Cgroup是 Control Groups 的缩写,提供了例如I/O 或内存的分配控制等具体的资源管理功能。这些具体的资源管理功能称为 cgroups 子系统,Cgroup共有V1和V2两个版本。由于V1有一些问题,从 Linux 4.5 内核开始 Cgroup V2 接口开始正式发布,目前两个版本都可以使用,但Cgroup V2 是社区趋势,社区已经不接受 Cgroup V1 新的 feature,新 feature 只支持 Cgroup V2。最新Linux内核查看
| V1 | 功能 | V2 | 功能 |
|---|---|---|---|
| cpu | 用来限制cgroup的CPU使用率。 | cpu | V1的cpu和cpuacct的继任者,Linux 4.15 |
| cpuacct | 产生 cgroup 任务的 cpu 资源报告。 | ||
| cpuset | 如果是多核心的 cpu,这个子系统会为 cgroup 任务分配单独的 cpu 和内存。 | cpuset | V1的cpuset的继任者,Linux 5.0 |
| memory | 设置每个 cgroup 的内存限制以及产生内存资源报告。 | memory | V1的memory的继任者,Linux 4.5 |
| devices | 允许或拒绝 cgroup 任务对设备的访问。 | ||
| freezer | 暂停和恢复 cgroup 任务。 | freezer | V1的freezer的继任者,Linux 5.2 |
| net_cls | 标记每个网络包以供 cgroup 方便使用。 | ||
| blkio | 设置限制每个块设备的输入输出控制。例如:磁盘,光盘以及 usb 等等。 | io | V1的blkio的继任者,Linux 4.5 |
| perf_event | 增加了对每 group 的监测跟踪的能力,可以监测属于某个特定的 group 的所有线程以及运行在特定CPU上的线程。 | perf_event | 与V1中的perf_event是同一个,Linux 4.11 |
| net_prio | 针对每个网络接口设置cgroup的访问优先级。 | ||
| hugetlb | 限制cgroup的huge pages的使用量。 | hugetlb | V1的hugetlb的继任者,Linux 5.6 |
| pids | 限制一个cgroup及其子孙cgroup中的总进程数。 | pids | 与V1中的pids是同一个,Linux 4.5 |
| rdma | 限制进程对RDMA/IB资源的使用。 | rdma | 与V1中的rdma是同一个,Linux 4.11 |
这里面每一个子系统都需要与内核的其他模块配合来完成资源的控制,比如对 cpu 资源的限制是通过进程调度模块根据 cpu 子系统的配置来完成的;对内存资源的限制则是内存模块根据 memory 子系统的配置来完成的。
子系统并不是一成不变,随着Linux内核更新修订或者增加,docker等容器实现也会随之渐进式支持。
举例:CPU限制功能
下面我们通过一组实例来验证一下CPU的限制功能。在Linux中,Cgroup给用户暴露出来的操作接口是文件系统,即它以文件和目录的方式组织操作系统 /sys/fs/cgroup 路径下。
可以看到在/sys/fs/cgroup下面有cpu、memory、blkio等这样的子目录,这都是能被Cgroup进行资源限制的资源类型,而这些子目录下就是这些类型的限制方法,例如:对CPU子目录来说,我们可以看到如下几个配置文件。
我们可以用cpu.cfs_quota_us和cpu.rt_period_us来配合使用限制CPU使用情况,可以用来限制进程在长度为cpu.rt_period的一段时间,只能被分配到总量为cpu.cfs_quota的CPU时间。我们通过下面例子展示。首先我们需要在对应的子目录下创建一个目录,我们进入/sys/fs/cgroup/cpu,执行如下指令:
cd /sys/fs/cgroup/cpu
$ mkdir container
$ ls container/
我们可以看到在创建的container目录下会自动生成该cpu子目录下的资源限制文件。
下面我们执行一个死循环脚本,让他把CPU资源吃到100%,根据输出,我们可以看到这个脚本的PID是3080。
while : ; do : ; done &
通过top命令查看cpu资源使用情况,可以看到3080的CPU使用率接近于100%。
我们看一下container目录下的cpu.cfs_quota_us和cpu.rt_period_us的值
$ cd container/
$ cat cpu.cfs_quota_us
$ cat cpu.cfs_period_us
接下来我们通过修改这两个文件的内容来设置CPU限制。
向container目录下的cpu.cfs_quota_us文件中写入20ms (20000 us)
echo 20000 > cpu.cfs_quota_us
上述命令意味着在每100ms的时间里,被该控制组限制的进程只能使用20ms的CPU时间,也就是说这个进程只能使用到20% 的CPU带宽。
接下来,我们把被限制的进程的PID写入container目录下tasks文件,上面设置就对该PID生效。
echo 3080 > /sys/fs/cgroup/cpu/container/tasks
我们用top指令看一下,该进程的CPU占用率降到了20%。
Docker中如何使用CPU限制功能
Docker 是一个开源的应用容器引擎,主要是提供一个抽象层,向用户提供一个管理容器的机制。让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的 Linux 机器上。下面的操作是在平时我们用的MacOS的机器上执行,感兴趣可以直接在自己本机上安装docker进行实验。
-
--cpu-shares
- 默认所有的容器对于 CPU 的利用占比都是一样的,
-c或者--cpu-shares可以设置 CPU 利用率权重值,默认为 1024。如果设置选项为 0,则系统会忽略该选项并且使用默认值 1024。通过以上设置,只会在 CPU 密集(繁忙)型运行进程时体现出来。当一个 container 空闲时,其它容器都是可以占用 CPU 的。cpu-shares 值为一个相对值,实际 CPU 利用率则取决于系统上运行容器的数量。 - 为容器设置可用的CPU的权重,假如一个 1core 的主机运行 3 个 container,其中一个 cpu-shares 设置为 1024,而其它 cpu-shares 被设置成 512。当 3 个容器中的进程尝试使用 100% CPU 的时候,则设置 1024 的容器会占用 50% 的 CPU 时间。如果只有一个容器,那么此时它无论设置 512 或者 1024,CPU 利用率都将是 100%。
- 在机器上实验,测试主机为6core。
-
先执行 docker run -t -i --cpu-shares 1024 --name 1024c ubuntu:18.04 /bin/bash 再打开一个窗口执行 docker run -t -i --cpu-shares 512 --name 1024c ubuntu:18.04 /bin/bash 进入容器分别都执行 stress -c 6(stress是一个linux下的压力测试工具) 在打开一个窗口执行 docker stats 可以查看容器情况
- 默认所有的容器对于 CPU 的利用占比都是一样的,
-
--cpu-period & --cpu-quota
- 默认的 CPU CFS「Completely Fair Scheduler」period 是 100ms。我们可以通过
--cpu-period值限制容器的 CPU 使用。一般--cpu-period配合--cpu-quota一起使用。设置 cpu-period 为 100ms,cpu-quota 为 200ms,表示最多可以使用 2 个 cpu。 -
docker run -t -i --cpu-period=100000 --cpu-quota=200000 --name quota2 ubuntu:18.04 /bin/bash - 通过以上测试可以得知,
--cpu-period结合--cpu-quota配置最多能使用的CPU是固定的,无论 CPU 是闲还是繁忙,如上配置,容器最多只能使用 2 个 CPU 到 100%。
- 默认的 CPU CFS「Completely Fair Scheduler」period 是 100ms。我们可以通过
-
--cpuset-cpus
-
docker run -t -i --cpuset-cpus="1,3" --name cpu13 ubuntu:18.04 /bin/bash
-
容器运用场景
-
混和部署
- 根据近五年的一些研究数据,从全球范围来看,服务器利用率 仅达到 6%~12%。即使通过服务聚合技术进行优化,服务器的利用率仍然只有 7%~17%,数据中心硬件成本占据了云服务商总成本的一半以上。为了提升硬件效率,主流云服务厂商逐步采用在线服务与离线作业的混合部署技术。Google是最先尝试混部技术,且K8s其实也是源于Google的混部场景。从目前公开的数据,Google的数据中心已经达到了60%的CPU资源利用率,阿里云和百度则都是40%左右。
-
CDN 场景(边缘容器)
- 容器基于轻量级、安全性、秒级启动等优秀特性,适合支撑CDN场景下如实时视频处理、语音识别、直播等实时性较强的应用管理场景,十分适合边缘计算场景。
-
微服务架构
- 将单体式应用从不同维度拆分成多个微服务,每个微服务的内容使用一个 docker 镜像管理。
-
持续集成与持续交付
-
等等
语言虚拟机及使用场景
我们都知道,我们平时接触最多的Javascript语言运行的背后有引擎,这个引擎也可以被理解为一个语言虚拟机,它模拟实际计算机各种功能来实现代码的编译和执行。我们知道编译型语言如Java,编写的代码编译后因为可以直接执行,但与平台相关;解释型语言如JS,需要运行时对程序一行行解释转为字节码,运行比较慢但是可以跨域平台。解释型语言之所以可以跨平台就是因为中间有一层虚拟机(或者称之为解释器)。语言虚拟机除了提供跨平台能力,通常还有内存分配、垃圾回收等能力。
这里关于虚拟机和解释器的说法,有人认为虚拟机是和语言无关的,JVM为例,除了Java之外,Scala,Clojure,甚至Python借助于Jython工具,也可以运行在JVM上,而没听说除Python以外有什么语言能被Python解释器解释执行;有人认为解释器是一个历史遗留术语,现代语言中虚拟机和解释器的分界已经很模糊甚至不存在。本人倾向于后者。
JVM
JVM是一个Java语言虚拟机,Java语言作为编译型语言可以做到平台无关,JVM是实现这一特点的关键。一般的高级语言如果要在不同的平台上运行,至少需要编译成不同的目标代码。而有了JVM后,Java语言在不同平台上运行时不需要重新编译。Java语言使用JVM屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在JVM运行的目标代码),就可以在多种平台上不加修改地运行。这就是Java的能够“一次编译,到处运行”的原因。
为了支持与硬件平台无关的执行模式,JVM提供了字节码(byte-code)这一中间表达形式,Java代码首先会被翻译为字节码,然后就可以在任意JVM上执行。JVM提供了基于栈的执行模式,通过构建Java栈依次解释执行字节码,从而完成对Java应用的执行。由于解释执行效率较低,JVM提供了即时编译(just-in-time compilation)功能,会在运行时将频繁使用的代码进行编译优化,从而提高Java应用的整体性能。同时,JVM也提供了自动内存管理(即垃圾回收)以及安全检查功能,进一步提高了易用性和可靠性。
目前,在JVM上运行的大数据系统有很多,其中既包含Hadoop、Hive等完全使用Java语言编写的系统,也包含Spark、Flink等使用JVM语言编写的系统。可以说,JVM在目前的大数据环境中是不可或缺的。
WebAssembly
WebAssembly,用自己一句话来解释,就是可以通过编译器将C、C++和 Rust 等低级源语言编译出产物,且这个产物可以被支持WebAssembly的Javascript引擎执行,例如V8,同时暴露出一些API可以与JavaScript 协同合作,使得Web可以获得巨大性能优势和新特性。
WebAssembly的由来,其实就是Javascript的性能瓶颈,例如当JavaScript 应用到诸如 3D 游戏、虚拟现实、增强现实、计算机视觉、图像/视频编辑等领域时。
WebAssembly的应用
-
AutoCAD
- 这是一个用于画图的软件,在很长的一段时间是没有Web的版本的,原因有两个,其一,是Web的性能的确不能满足他们的需求。其二,在WebAssembly没有面世之前,AutoCAD是用C++实现的,要将其搬到Web上,就意味着要重写他们所有的代码,这代价十分的巨大。而在WebAssembly面世之后,AutoCAD得以利用编译器,将其沉淀了30多年的代码直接编译成WebAssembly,同时性能基于之前的普通Web应用得到了很大的提升。
-
Google Earth
- Google Earth也就是谷歌地球,因为需要展示很多3D的图像,对性能要求十分高,所以采取了一些Native的技术。最初的时候就连Chrome浏览器都不支持Web的版本,需要单独下载Google Earth的Destop应用。而在WebAssembly之后,谷歌地球推出了Web的版本。
-
Unity和Unreal游戏引擎
WebAssembly的开发工具
WebAssembly2015年问世,现在已有较多的开发工具。
-
AssemblyScript:支持直接将TypeScript编译成WebAssembly。这对于很多前端同学来说,比较好入门。
-
Emscripten:是WebAssembly的灵魂工具,这个就是前面提到的编译器,将其他的高级语言,编译成WebAssembly。
-
WABT:是个将WebAssembly在字节码和文本格式相互转换的一个工具,方便开发者去理解这个wasm到底是在做什么事。
总结
-
系统级虚拟化
- 将整个计算机虚拟化到硬件层,客户机拥有自己的操作系统,通过Hypervisor运行在宿主机操作系统(或宿主机硬件设备)上,完全模拟 CPU、磁盘和网络设备等硬件设备,来管理分配资源及执行操作。实现过程主要是对硬件的虚拟化。
-
操作系统级虚拟化
- 容器只虚拟化操作系统级别以上的软件层。通过Linux提供的Chroot、Namespace来进行用户、进程、文件系统、IO等的隔离,使得容器中只能看到自己的资源而看不到宿主机和其他容器的资源。通过Cgroup来进行物理资源限制、资源优先级分配、资源统计和进程控制。
-
语言级虚拟化
- 语言虚拟机为语言提供了与平台无关的执行环境,简化了应用的开发和部署。通常都是提供一个环境,在该环境中执行编译型语言编译后的字节码,而对解释型语言会先将其解释为字节码再执行。