Nanos Unikernel入门

2024-04-21

1. Unikernel 概述

1.1. 什么是 Unikernel

Unikernel 是一种专为特定应用构建的轻量级操作系统,将应用程序与操作系统功能紧密集成在一起,形成单一的可执行文件。这种设计可以显著提高性能和安全性,同时减少资源消耗。

  1. 定义与构成:Unikernel 是从传统的全功能操作系统中剥离出来的,它仅包含运行特定应用程序所必需的最小操作系统组件。这些组件与应用程序代码一起编译,形成一个高度优化和专用的系统映像。

  2. 性能优化:由于 Unikernel 仅包含必要的操作系统功能,它能够在启动时间、运行效率和响应速度等方面实现显著的性能优势。例如,Unikernel 可以在几毫秒内启动,非常适合需要快速启动和高效运行的环境。

  3. 安全性提高:Unikernel 的攻击面相对较小,因为它剥离了不必要的代码和服务,从而减少了潜在的安全漏洞。此外,由于每个 Unikernel 都是为特定应用定制的,因此可以实现更细粒度的安全控制。

  4. 应用场景:

    • 云计算:在云平台上,Unikernels 可以提供轻量级、高性能的虚拟机选项,特别适合微服务架构和弹性计算需求。

    • 物联网(IoT):对于资源受限的物联网设备,Unikernel 提供了一种有效的方法,可以在保持较低资源消耗的同时,确保设备的响应速度和安全性。

    • 边缘计算:在边缘计算场景中,Unikernel 由于其快速启动和低资源消耗的特性,非常适合处理离用户更近的数据处理任务。

  5. 挑战与局限性:

    • 兼容性问题:由于 Unikernel 高度专用化,它可能不支持标准的 POSIX 接口,这可能会限制某些应用程序的移植。

    • 开发与维护难度:Unikernel 需要针对每个应用定制开发,这可能增加开发和维护的复杂性和成本。

1.2. 虚拟机 VS Linux 容器 VS Unikernel

技术

优点

缺点

虚拟机

  • 允许在单一主机上部署不同操作系统

  • 与主机完全隔离

  • 有编排解决方案可用

  • 需要与实例数量成比例的计算能力

  • 需要大型基础设施

  • 每个实例都需要加载完整操作系统

容器

  • 轻量级虚拟化

  • 快速启动时间

  • 有编排解决方案可用

  • 动态资源分配

  • 主机和客户端之间的隔离性降低,因为内核是共享的

  • 灵活性较差(依赖于主机内核)

  • 网络灵活性较差

Unikernel

  • 轻量级镜像

  • 专业化应用

  • 与主机完全隔离

  • 对缺失功能(例如:远程命令执行)有更高的安全性

  • 尚不够成熟,不适合生产环境

  • 需要从头开始开发应用程序

  • 部署可能性有限

  • 缺乏完整的 IDE 支持

  • 静态资源分配

  • 缺乏编排工具

2024-04-21-ucsvrpvh.png

2. Unikernel 现有解决方案及对比

下表是一些仍保持活跃开发的 Unikernel 解决方案

解决方案

支持的语言

平台兼容性

特点

地址

Unikraft

C, C++, Go, Python

KVM, XEN, Linux

易于使用,支持多种编程语言

https://github.com/unikraft/unikraft

Nanos Unikernel

C, C++, Go, Rust

QEMU/KVM, XEN, ESXi, Amazon EC2, Google Cloud, HyperV

专为云应用设计,支持多种云平台

https://github.com/nanovms/nanos

RustyHermit

Rust

Xen, Uhyve

专注于 Rust 语言,提供良好的安全性和现代语言特性支持

https://github.com/hermit-os/hermit-rs

MirageOS

OCaml

KVM, Xen, RTOS/MCU

https://github.com/mirage/mirage

3. Nanos Unikernel

Nanos 是一个 Unikernel 的解决方案,支持较多的编程语言和平台,其主要使用 ops 工具对 Nanos 镜像和实例进行管理和编排,同时还支持 Kibs 插件对其进行扩展。

ops 是一个使用 Go 语言编写的 CLI 工具,地址在 https://github.com/nanovms/ops

3.1. 安装

官方提供了一键安装脚本

curl https://ops.city/get.sh -sSfL | sh
ops version

3.2. 一个最简单的 Demo

这里使用 Node.js 作为编程语言实现

var http = require('http');
http.createServer(function (req, res) {
    res.writeHead(200, {'Content-Type': 'text/plain'});
    res.end('Hello World\n');
}).listen(8083, "0.0.0.0");
console.log('Server running at http://127.0.0.1:8083/');

将上述代码保存为 hi.js ,然后运行

ops pkg load eyberg/node:20.5.0 -p 8083 -f -n -a hi.js

上述命令中各参数含义如下

  • pkg load eyberg/node:20.5.0 : 用于将 eyberg/node:20.5.0 作为基础的镜像,可以理解为 Docker 中的基础镜像,

  • -p : 用于映射端口号

  • -f : 强制从远程拉取最新的镜像

  • -n : 使用 nightly 通道进行构建

  • -a : 传递给程序的参数

运行命令后输出:

running local instance
booting /root/.ops/images/node ...
en1: assigned 10.0.2.15
Server running at http://127.0.0.1:8083/
en1: assigned FE80::84D8:EFFF:FEDC:6C5D

3.3. 原理

上述的 Demo 运行过程发生了什么?

首先,我们先将 eyberg/node:20.5.0 这个基础镜像拉取到了本地,它在本地的路径位于 ~/.ops/packages/eyberg/node_20.5.0 ,可以查看一下这个目录下有什么内容

# tree
.
├── node
├── package.manifest
├── README.md
└── sysroot
    ├── lib
    │   └── x86_64-linux-gnu
    │       ├── libc.so.6
    │       ├── libdl.so.2
    │       ├── libgcc_s.so.1
    │       ├── libm.so.6
    │       ├── libpthread.so.0
    │       └── libstdc++.so.6
    └── lib64
        └── ld-linux-x86-64.so.2

4 directories, 10 files

可以看到目录下包含了 node 的二进制文件和其所需要的动态依赖库,其中 sysroot 目录在镜像打包时,会映射到系统的根目录。

而我们上一步运行的程序也会打包成一个二进制文件,使用 ops image list 查看

+------+------------------------+----------+---------------+
| NAME |          PATH          |   SIZE   |   CREATEDAT   |
+------+------------------------+----------+---------------+
| node | /root/.ops/images/node | 114.8 MB | 2 seconds ago |
+------+------------------------+----------+---------------+

可以看到镜像位于 /root/.ops/images/node