基于Libvirt实现轻量级的虚拟机管理工具

程序实现了虚拟机的所有基本操作。包括虚拟机的启动、暂停、关闭、重启、强制关机、强制重启、保存等功能。
程序源码地址:Github
程序编译:支持g++和clang(Apple LLVM 8.0)编译。
libvirt_1
编译命令:clang libvirtctl.cpp -o libvirtctl -lvirt
程序执行方法:./libvirtctl DomainName Operation
DomainName : 虚拟主机域名
Operation : 虚拟机执行操作
操作示例:开机:./libvirtctl CentOS6.4 start
libvirt_2
libvirt_3
示例:./libvirtctl CentOS6.4 shutdown
libvirt_4

在程序编译过程中,若clang提示错误:

#include
^
clang error: libvirt.h not found
解决方案:sudo apt-get install libvirt-dev

源码:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <libvirt/libvirt.h>
#include <libvirt/virterror.h>

typedef int DOMAIN_INFO_STATUS;
typedef int DOMAIN_INFO_LOOKUPID;

static virConnectPtr connection = NULL;
static virDomainPtr domainInfoPtr = NULL;
static DOMAIN_INFO_STATUS domainStart(const char * guestname);
static DOMAIN_INFO_STATUS domainShutdown(const char * guestname);
static DOMAIN_INFO_STATUS domainForcedShutdown(const char * guestname);
//static DOMAIN_INFO_STATUS GetDomainInformation(DOMAIN_INFO_LOOKUPID id);
static DOMAIN_INFO_STATUS domainReboot(const char * guestname);
static DOMAIN_INFO_STATUS domainReset(const char * guestname);
static DOMAIN_INFO_STATUS domainResume(const char * guestname);
static DOMAIN_INFO_STATUS GetDomainInformation(const char * guestname);

/*
virConnectPtr getConnection() {
    connection = virConnectOpenReadOnly(NULL);
    if(connection == NULL)
    {
        printf("error!\n");
        exit(1);
    }
    return connection;
}

DOMAIN_INFO_STATUS GetDomainInformation(DOMAIN_INFO_LOOKUPID id) {
    virDomainInfo domainInfo;
    connection = getConnection();
    domainInfoPtr = virDomainLookupByID(connection, id);
    if(domainInfoPtr == NULL)
    {
        printf("Can not find Domain!\n");
        virConnectClose(connection);
        exit(1);
    }
    if( virDomainGetInfo(domainInfoPtr, &domainInfo) < 0 )
    {
        printf("Can not get Information!\n");
        virDomainFree(domainInfoPtr);
        exit(1);
    }

    printf("The Domain Status is : %d.\n", domainInfo.state);
    printf("The Domain Allowed Max Memory is : %ld.\n", domainInfo.maxMem);
    printf("The Domain Memory is : %ld. \n", domainInfo.memory);
    printf("The Domain Virtual CPU is : %d.\n", domainInfo.nrVirtCpu);

    if(domainInfoPtr != NULL)
    {
        virDomainFree(domainInfoPtr);
    }
    if(connection != NULL)
    {
        virConnectClose(connection);
    }
}*/

int main(int argc, const char * argv[])
{
    if (argc != 3) {
        fprintf(stderr, "Usage: ./libvirtctl guestName(domainName) start(START)/shutdown(SHUTDOWN)/forcedShutdown(FORCEDSHUTDOWN)/reboot(REBOOT)/reset(RESET)/resume(RESUME)/status(STATUS).\n");
        return -1;
    }
    connection = virConnectOpen("qemu:///system");
    if (connection == NULL) {
        fprintf(stderr, "Failed to open connectionn to qemu:///system.\n");
        return -1;
    }
    domainInfoPtr = virDomainLookupByName(connection, argv[1]);
    if (domainInfoPtr == NULL)
    {
        fprintf(stderr, "virDomainLookupByName failed.\n");
        virConnectClose(connection);
        return -1;
    }

    if (strcmp(argv[2], "start") == 0 || strcmp(argv[2], "START") == 0 ) {
        if (domainStart(argv[1]) != 0 ) {
            fprintf(stderr, "Start Failed.\n");
            virDomainFree(domainInfoPtr);
            return -1;
        }
    }

    if (strcmp(argv[2], "shutdown") == 0 || strcmp(argv[2], "SHUTDOWN") == 0 ) {
        if (domainShutdown(argv[1]) != 0 ) {
            fprintf(stderr, "Shutdown Failed.\n");
            virConnectClose(connection);
            virDomainFree(domainInfoPtr);
            return -1;
        }
    }

    if(strcmp(argv[2], "forcedShutdown") == 0 || strcmp(argv[2], "FORCEDSHUTDOWN") == 0 ) {
        if(domainForcedShutdown(argv[1]) != 0) {
            fprintf(stderr, "Forced Shutdown Failed.\n");
            virConnectClose(connection);
            virDomainFree(domainInfoPtr);
            return -1;
        }
    }

    if(strcmp(argv[2], "reboot") == 0 || strcmp(argv[2], "REBOOT") == 0) {
        if(domainReboot(argv[1]) != 0) {
            fprintf(stderr, "Reboot Failed.\n");
            virConnectClose(connection);
            virDomainFree(domainInfoPtr);
            return -1;
        }
    }

    if(strcmp(argv[2], "reset") == 0 || strcmp(argv[2], "RESET") == 0) {
        if(domainReset(argv[1]) != 0) {
            fprintf(stderr, "Reset Failed.\n");
            virConnectClose(connection);
            virDomainFree(domainInfoPtr);
            return -1;
        }
    }

    if(strcmp(argv[2], "resume") == 0 || strcmp(argv[2], "RESUME") == 0) {
        if(domainResume(argv[1]) != 0) {
            fprintf(stderr, "Resume Failed.\n");
            virConnectClose(connection);
            virDomainFree(domainInfoPtr);
            return -1;
        }
    }

    if (strcmp(argv[2], "status") == 0 || strcmp(argv[2], "STATUS") == 0 ) {
        if (GetDomainInformation(argv[1]) != 0 ) {
            fprintf(stderr, "Get Status Failed.\n");
            virDomainFree(domainInfoPtr);
            virConnectClose(connection);
            return -1;
        }
    }

/*    if(strcmp(argv[2], "start") != 0 || strcmp(argv[2], "shutdown") !=0 || strcmp(argv[2], "status") !=0 || strcmp(argv[2], "START") != 0 || strcmp(argv[2], "SHUTDOWN") != 0 || strcmp(argv[2], "STATUS") != 0)
    {
        fprintf(stderr, "Usage: ./libvirtctl guestName(domainName) start(START)/shutdown(SHUTDOWN)/status(STATUS).\n");
        return -1;
    }*/

    if (domainInfoPtr != NULL)
    {
        virDomainFree(domainInfoPtr);
    }
    if (connection != NULL)
    {
        virConnectClose(connection);
    }

    return EXIT_SUCCESS;
}

DOMAIN_INFO_STATUS domainStart(const char * guestname)
{
    int flag = -1;
    flag = virDomainCreate(domainInfoPtr);
    if (flag != 0)
    {
        virErrorPtr error = virGetLastError();
        fprintf(stderr, "virDomainCreate failed: %s.\n", error->message);
        virFreeError(error);
        return -1;
    }
    else
    {
        fprintf(stdout, "libvirtctl: %s domain starting was succeed.\n", guestname);
    }
    return 0;
}

DOMAIN_INFO_STATUS domainShutdown(const char * guestname)
{
    int flag = -1;
    flag = virDomainShutdown(domainInfoPtr);
    if (flag != 0)
    {
        virErrorPtr error = virGetLastError();
        fprintf(stderr, "virDomainShutdown failed: %s.\n", error->message);
        virFreeError(error);
        return -1;
    }
    else
    {
        fprintf(stdout, "libvirtctl: %s domain shutdown was succeed.\n", guestname);

    }
    return 0;
}

DOMAIN_INFO_STATUS domainForcedShutdown(const char * guestname)
{
    int flag = -1;
    domainShutdown(guestname);
    flag = virDomainDestroy(domainInfoPtr);
    if(flag != 0)
    {
        virErrorPtr error = virGetLastError();
        fprintf(stderr, "virDomainForcedShutdown failed: %s.\n", error->message);
        virFreeError(error);
        return -1;
    }
    else
    {
        fprintf(stdout, "libvirtctl: %s domain forced shutdown was succeed.\n", guestname);
    }
    return 0;
}

DOMAIN_INFO_STATUS domainReboot(const char * guestname)
{
    int flag = -1;
    flag = virDomainReboot(domainInfoPtr, VIR_DOMAIN_REBOOT_ACPI_POWER_BTN);
    if(flag != 0) 
    {
        virErrorPtr error = virGetLastError();
        fprintf(stderr, "virDomainReboot failed: %s.\n", error->message);
        virFreeError(error);
        return -1;
    }
    else
    {
        fprintf(stdout, "libvirtctl: %s domain reboot was succeed.\n", guestname);
    }
    return 0;
}

DOMAIN_INFO_STATUS domainReset(const char * guestname)
{
    int flag = -1;
    flag = virDomainReset(domainInfoPtr, 0);
    if(flag != 0)
    {
        virErrorPtr error = virGetLastError();
        fprintf(stderr, "virDomainReset failed: %s.\n", guestname);
        virFreeError(error);
        return -1;
    }
    else
    {
        fprintf(stdout, "libvirtctl: %s domain reset was succeed.\n", guestname);
    }
    return 0;
}

DOMAIN_INFO_STATUS domainResume(const char * guestname)
{
    int flag = -1;
    flag = virDomainResume(domainInfoPtr);
    if(flag != 0)
    {
        virErrorPtr error = virGetLastError();
        fprintf(stderr, "virDomainResume failed: %s.\n", guestname);
        virFreeError(error);
        return -1;
    }
    else
    {
        fprintf(stdout, "libvirtctl: %s domain resume was succeed.\n", guestname);
    }
    return 0;
}

DOMAIN_INFO_STATUS GetDomainInformation(const char * guestname)
{
    char * status = NULL;
    virErrorPtr error = NULL;
    int vcpus = 0;
    unsigned long long node_free_memory = 0;
    int id = 0;
    const char * name = NULL;
    virNodeInfo nodeInfo;
    virDomainInfo domainInfo;

    fprintf(stdout, "****************************************************\n");
    status = virConnectGetCapabilities(connection);
    if (status == NULL)
    {
        error = virGetLastError();
        fprintf(stderr, "virConnectGetCapabilities failed: %s.\n", error->message);
        virFreeError(error);
        return -1;
    }
    free(status);
    status = NULL;

    status = virConnectGetHostname(connection);
    if (status == NULL)
    {
        error = virGetLastError();
        fprintf(stderr, "virConnectGetHostname failed: %s.\n", error->message);
        virFreeError(error);
        return -1;
    }
    fprintf(stdout, "Connection Hostname:\t%s\n", status);
    free(status);
    status = NULL;

    vcpus = virConnectGetMaxVcpus(connection, NULL);
    if (vcpus < 0)
    {
        error = virGetLastError();
        fprintf(stderr, "virConnectGetMaxVcpus faild: %s.\n", error->message);
        virFreeError(error);
        return -1;
    }
    fprintf(stdout, "Maximum number of cpus supported on connection:\t%d\n", vcpus);

    node_free_memory = virNodeGetFreeMemory(connection);
    if (node_free_memory == 0) {
        error = virGetLastError();
        fprintf(stderr, "virNodeGetFreeMemory failed: %s.\n", error->message);
        virFreeError(error);
        return -1;
    }
    fprintf(stdout, "Node Free Memory:\t%llu\n", node_free_memory);

    if (virNodeGetInfo(connection, &nodeInfo) < 0) {
        error = virGetLastError();
        fprintf(stderr, "virNodeGetInfo failed: %s.\n", error->message);
        virFreeError(error);
        return -1;
        }

    fprintf(stdout, "------------------------------------------\n");
    fprintf(stdout, "Node Information From Connection : \n");
    fprintf(stdout, "Model:\t%s\n", nodeInfo.model);
    fprintf(stdout, "Memory Size:\t%luKiB\n", nodeInfo.memory);
    fprintf(stdout, "Number of CPUs:\t%u\n", nodeInfo.cpus);
    fprintf(stdout, "MHz of CPUs:\t%u\n", nodeInfo.mhz);
    fprintf(stdout, "Number of NUMA Nodes:\t%u\n", nodeInfo.nodes);
    fprintf(stdout, "Number of CPU Sockets:\t%u\n", nodeInfo.sockets);
    fprintf(stdout, "Number of CPU Cores Per Socket:\t%u\n", nodeInfo.cores);
    fprintf(stdout, "Number of CPU Threads Per Core:\t%u\n", nodeInfo.threads);
    fprintf(stdout, "****************************************************\n");
    fprintf(stdout, "ID\t名称\t\t状态\n");
    fprintf(stdout, "------------------------------------------\n");
    id = virDomainGetID(domainInfoPtr);
    name = virDomainGetName(domainInfoPtr);
    if (virDomainGetInfo(domainInfoPtr, &domainInfo) < 0) {
        error = virGetLastError();
        fprintf(stderr, "virDomainGetInfo failed: %s.\n", error->message);
        virFreeError(error);
        return -1;
    }
    fprintf(stdout, "%d\t%s\t\t%d\n", id, name, domainInfo.state);
    fprintf(stdout, "****************************************************\n");
    return 0;
}

KVM on KVM 嵌套虚拟化的实现

本实验系统环境为:Mac OS X El Capitan 10.11.6 15G1217
使用的虚拟机系统为:Parallels Desktop 12
虚拟化技术:Nested 虚拟化技术 + PMU虚拟化技术
L0: Ubuntu 16.04.1 Xenial LTS Desktop
L1: CentOS 6.4 Desktop
L2: Cirros Linux 0.3.5
首先,若要准备L2级虚拟机的系统镜像,可参考Ubuntu官网提供的cloud-images,链接为:https://cloud-images.ubuntu.com/?_ga=1.222879259.1831544656.1487263184
首先可以在物理机使用scp命令将L2级虚拟机所需img镜像文件传输至L0,命令及效果图如图所示:

scp xenial-server-cloudimg-amd64-disk1.img fa1c0n@10.211.55.6:~/

kvmonkvm_1
若当前虚拟机不存在网桥,则需要配置网桥才可使虚拟机联网。由于之前的实验已完成配置,此处只写出相关命令:

#root@master:~# brctl addbr br0        #增加一个虚拟网桥br0
#root@master:~# brctl addif br0 enp0s5    #在br0中添加一个接口eth0
#root@master:~# brctl stp br0 on        #打开STP协议,否则可能造成环路
#root@master:~# ifconfig enp0s5 0        #将eth0的IP设置为0
#root@master:~# dhclient br0          #设置动态给br0配置ip、route等
#root@master:~# route                #显示路由表信息
#root@master:~# brctl show                #检查br0状态

效果图如下:
kvmonkvm_2
同样,由于已完成之前的实验,qemu_ifup启动脚本为在启动时创建和打开指定的TAP接口以供虚拟机连接使用。启动脚本见主要算法和程序清单。
接下来,即可开启L1虚拟机,命令如下:

qemu-system-x86_64 –cpu qemu64,+vmx -m 1024 -smp 4 -boot order=d -hda rhel-6.4.img -net nic -net tap

启动虚拟机后,如图所示:
如图可以看到已成功在L0上运行L1.
kvmonkvm_3
首先开始安装qemu,kvm,libvirt,libvirt-python,命令如下:

yum install qemu-kvm libvirt libvirt-python virt-manager python-virtinst libvirt-client

安装完成后,运行如下命令确认kvm是否安装成功:

lsmod | grep kvm && stat /dev/kvm

kvmonkvm_4
如上图所示,即为安装成功。安装成功后,需要配置L1的网桥才可使L1创建的L2虚拟机可上网。命令同上:

#root@master:~# brctl addbr br0        #增加一个虚拟网桥br0
#root@master:~# brctl addif br0 enp0s5    #在br0中添加一个接口eth0
#root@master:~# brctl stp br0 on        #打开STP协议,否则可能造成环路
#root@master:~# ifconfig enp0s5 0        #将eth0的IP设置为0
#root@master:~# dhclient br0          #设置动态给br0配置ip、route等
#root@master:~# route                #显示路由表信息
#root@master:~# brctl show                #检查br0状态

kvmonkvm_5
接下来,将镜像文件从L0拷贝至L1中,使用命令:

scp cirros-0.3.5-x86_64-disk.img root@10.211.55.9:~/

传输完成后如图所示:
kvmonkvm_6
接下来即可在L1中启动L2虚拟机。命令如下:

qemu-system-x86_64 -smp 4 -m 256 -boot order=d -hda cirros-0.3.5-x86_64-disk.img -net nic -net tap -enable-kvm

kvmonkvm_7
kvmonkvm_8
启动成功后,可以看到已成功启动L2虚拟机。且L2虚拟机可ping通百度,可以上网。至此,KVM on KVM嵌套虚拟化的实现已完成。

在CentOS 6.4下,启动虚拟机时,遇到libdevmapper库错误问题,错误提示如下:
libvirtd: relocation error: libvirtd: symbol dm_task_get_info_with_deferred_remove, version Base not defined in file libdevmapper.so.1.02 with link time reference
解决方案:yum -y upgrade device-mapper-libs
若启动虚拟机时遇到如下问题,问题描述为:
error: internal error: unable to execute QEMU command ‘cont’: Resetting the Virtual Machine is required
检查系统是否有vmx:cat /proc/cpuinfo | grep vmx
若没有vmx,则需要开启-enable-kvm选项。
若有vmx,则重新开启虚拟机即可。

#! /bin/sh
# Script to bring a network (tap) device for qemu up.
# The idea is to add the tap device to the same bridge
# as we have default routing to.

# in order to be able to find brctl

switch=br0
PATH=$PATH:/sbin:/usr/sbin
ip=$(which ip)

if [ -n "$ip" ]; then
   ip link set "$1" up
else
   brctl=$(which brctl)
   if [ ! "$ip" -o ! "$brctl" ]; then
     echo "W: $0: not doing any bridge processing: neither ip nor brctl utility not found">&2
     exit 0
   fi
   ifconfig "$1" 0.0.0.0 up
fi

switch=$(ip route ls | \
    awk '/^default / {
          for(i=0;i<NF;i++) { if ($i == "dev") { print $(i+1); next; } }
         }'
        )

# only add the interface to default-route bridge if we
# have such interface (with default route) and if that
# interface is actually a bridge.
# It is possible to have several default routes too
for br in $switch; do
    if [ -d /sys/class/net/$br/bridge/. ]; then
        if [ -n "$ip" ]; then
          ip link set "$1" master "$br"
        else
          brctl addif $br "$1"
        fi
        exit    # exit with status of the previous command
    fi
done

VirtIO半虚拟化驱动的使用

virtio 是对半虚拟化 hypervisor 中的一组通用模拟设备的抽象。该设置还允许 hypervisor 导出一组通用的模拟设备,并通过一个通用的应用编程接口(API)让它们变得可用。下图展示了为什么这很重要。有了半虚拟化 hypervisor 之后,来宾操作系统能够实现一组通用的接口,在一组后端驱动程序之后采用特定的设备模拟。后端驱动程序不需要是通用的,因为它们只实现前端所需的行为。除了前端驱动程序(在来宾操作系统中实现)和后端驱动程序(在 hypervisor 中实现)之外,virtio 还定义了两个层来支持来宾操作系统到 hypervisor 的通信。在顶级(称为 virtio)的是虚拟队列接口,它在概念上将前 端驱动程序附加到后端驱动程序。驱动程序可以使用 0 个或多个队列,具体数量取决于需求。例如,virtio 网络驱动程序使用两个虚拟队列(一个用于接收,另一个用于发送),而 virtio 块驱动程序仅使用一个虚拟队列。虚拟队列实际上被实现为跨越来宾操作系统和 hypervisor 的衔接点。但这可以通过任意方式实现,前提是来宾操作系统和 hypervisor 以相同的方式实现它。
KVM是必须使用硬件虚拟化辅助技术(如Intel VT-x、AMD-V)的hypervisor,在CPU运行效率方面有硬件支持,其效率是比较高的;在有Intel EPT特性支持的平台上,内存虚拟化的效率也较高。QEMU/KVM提供了全虚拟化环境,可以让客户机不经过任何修改就能运行在KVM环境中。不过,KVM在I/O虚拟化方面,传统的方式是使用QEMU纯软件的方式来模拟I/O设备(如第4章中提到模拟的网卡、磁盘、显卡等等),其效率并不非常高。在KVM中,可以在客户机中使用半虚拟化驱动(Paravirtualized Drivers,PV Drivers)来提高客户机的性能(特别是I/O性能)。目前,KVM中实现半虚拟化驱动的方式是采用了virtio这个Linux上的设备驱动标准框架。
使用QEMU模拟I/O的情况下,当客户机中的设备驱动程序(device driver)发起I/O操作请求之时,KVM模块中的I/O操作捕获代码会拦截这次I/O请求,然后经过处理后将本次I/O请求的信息存放到I/O共享页,并通知用户控件的QEMU程序。QEMU模拟程序获得I/O操作的具体信息之后,交由硬件模拟代码来模拟出本次的I/O操作,完成之后,将结果放回到I/O共享页,并通知KVM模块中的I/O操作捕获代码。最后,由KVM模块中的捕获代码读取I/O共享页中的操作结果,并把结果返回到客户机中。当然,这个操作过程中客户机作为一个QEMU进程在等待I/O时也可能被阻塞。另外,当客户机通过DMA(Direct Memory Access)访问大块I/O之时,QEMU模拟程序将不会把操作结果放到I/O共享页中,而是通过内存映射的方式将结果直接写到客户机的内存中去,然后通过KVM模块告诉客户机DMA操作已经完成。
QEMU模拟I/O设备的方式,其优点是可以通过软件模拟出各种各样的硬件设备,包括一些不常用的或者很老很经典的设备(如RTL8139的网卡),而且它不用修改客户机操作系统,就可以实现模拟设备在客户机中正常工作。在KVM客户机中使用这种方式,对于解决手上没有足够设备的软件开发及调试有非常大的好处。而它的缺点是,每次I/O操作的路径比较长,有较多的VMEntry、VMExit发生,需要多次上下文切换(context switch),也需要多次数据复制,所以它的性能较差。
Virtio最初由澳大利亚的一个天才级程序员Rusty Russell编写,是一个在hypervisor之上的抽象API接口,让客户机知道自己运行在虚拟化环境中,从而与hypervisor根据 virtio 标准协作,从而在客户机中达到更好的性能(特别是I/O性能)。目前,有不少虚拟机都采用了virtio半虚拟化驱动来提高性能,如KVM和Lguest。
其中前端驱动(frondend,如virtio-blk、virtio-net等)是在客户机中存在的驱动程序模块,而后端处理程序(backend)是在QEMU中实现的。在这前后端驱动之间,还定义了两层来支持客户机与QEMU之间的通信。其中,“virtio”这一层是虚拟队列接口,它在概念上将前端驱动程序附加到后端处理程序。一个前端驱动程序可以使用0个或多个队列,具体数量取决于需求。例如,virtio-net网络驱动程序使用两个虚拟队列(一个用于接收,另一个用于发送),而virtio-blk块驱动程序仅使用一个虚拟队列。虚拟队列实际上被实现为跨越客户机操作系统和hypervisor的衔接点,但它可以通过任意方式实现,前提是客户机操作系统和virtio后端程序都遵循一定的标准,以相互匹配的方式实现它。而virtio-ring实现了环形缓冲区(ring buffer),用于保存前端驱动和后端处理程序执行的信息,并且它可以一次性保存前端驱动的多次I/O请求,并且交由后端去动去批量处理,最后实际调用宿主机中设备驱动实现物理上的I/O操作,这样做就可以根据约定实现批量处理而不是客户机中每次I/O请求都需要处理一次,从而提高客户机与hypervisor信息交换的效率。
Virtio半虚拟化驱动的方式,可以获得很好的I/O性能,其性能几乎可以达到和native(即:非虚拟化环境中的原生系统)差不多的I/O性能。所以,在使用KVM之时,如果宿主机内核和客户机都支持virtio的情况下,一般推荐使用virtio达到更好的性能。当然,virtio的也是有缺点的,它必须要客户机安装特定的Virtio驱动使其知道是运行在虚拟化环境中,且按照Virtio的规定格式进行数据传输,不过客户机中可能有一些老的Linux系统不支持virtio和主流的Windows系统需要安装特定的驱动才支持Virtio。不过,较新的一些Linux发行版(如RHEL 6.3、Fedora 17等)默认都将virtio相关驱动编译为模块,可直接作为客户机使用virtio,而且对于主流Windows系统都有对应的virtio驱动程序可供下载使用。
首先,搭建kvm+qemu基础环境。检查CPU是否支持虚拟化技术,命令如下:

cat /proc/cpuinfo | grep vmx

接下来安装kvm以及qemu组件等:

sudo apt-get install kvm qemu libvirt-bin virtinst virt-manager virt-viewer

效果如图所示:
virtio1
实验使用的系统为Ubuntu Linux 16.04.1 LTS系统,内核版本4.4.0支持virtio。
首先使用qemu-img命令创建镜像文件:

sudo qemu-img create -f qcow2 rhel-6.4.img 20G

此命令创建了文件名为rhel-6.4.img的qcow2格式的20GB大小的镜像文件。
接下来使用rhel-server-6.4-x86_64-dvd.iso文件安装操作系统。
若要执行系统安装步骤,需要使用qemu-system-x86_64指令启动虚拟机执行系统安装。命令如下:

sudo qemu-system-x86_64 -enable-kvm -m 1024 -smp 4 -boot order=cd -hda rhel-6.4.img -cdrom ~/rhel-server-6.4-x86_64-dvd.iso

效果如图所示:
由于系统安装并非重点,故此处略去安装过程截图。
virtio2
在上面的命令中,-enable-kvm代表使用Linux内核内置的KVM硬件加速虚拟化技术,而非使用QEMU效率低的纯软件虚拟化。-m 1024代表分配1GB内存给虚拟机。-smp 4代表分配4个vCPU给虚拟机。-boot order=cd代表变更虚拟机启动顺序为光驱优先。-hda rhel-6.4.img代表虚拟机使用的磁盘镜像文件。-cdrom代表虚拟机需要使用光盘驱动器,此选项可直接指定物理光驱也可指定ISO文件,此处选择ISO镜像。
安装系统完成后,变更启动命令行为:

sudo qemu-system-x86_64 -enable-kvm -m 1024 -smp 4 -hda rhel-6.4.img -balloon virtio -vga virtio -net nic,model=virtio -device virtio-balloon-pci,id=balloon0,bus=pci.0,addr=0x05

![](https://www.fa1c0n.cn/img/posts/virtio/3.png)  

启动系统后效果如图所示。进入虚拟机后,输入命令以查看virtio分配设备:

sudo lsmod | grep virtio

效果如图所示:

从图片中以及命令行中可以看到,virtio_balloon,virtio_net,virtio_pci,virtio_ring,virtio均已开启。实现了通过QEMU命令行参数启动带有Virtio网卡驱动、Virtio磁盘驱动、Virtio Balloon设备驱动的虚拟机。
在上面执行的命令中,由于命令行中加入了-net nic,model=virtio命令行配置网络,则必须配置网络启动脚本/etc/qemu_ifup。在虚拟机启动时创建和打开指定的tap接口,并将接口添加到虚拟网桥中。qemu_ifup的配置代码见主要算法和程序清单部分。
至此Linux下的VirtIO半虚拟化驱动的使用实验完成。
对于Windows操作系统非开源,微软没有开发相应的virtio驱动,所以需要安装virtio驱动程序才能支持virtio。virtio驱动下载地址:https://launchpad.net/kvm-guest-drivers-windows/+download
此次实验选用Windows XP系统为实验对象。首先制作WinXP的镜像文件,命令如下:

sudo qemu-img create -f qcow2 winxp.img 50G

接下来启动镜像文件开始安装操作系统,安装过程截图不是重点同样略过。
安装系统启动虚拟机使用命令:

sudo qemu-system-x86_64 -smp 4 -m 1024 -cdrom zh-hans_windows_xp_professional_with_service_pack_3_x86_cd_vl_x14-74070.iso -drive file=winxp.img,cache=writeback,if=virtio,boot=on -boot order=cd -fda virtio-win-0.1.96_x86.vfd -usbdevice tablet -device virtio-balloon-pci,id=balloon0 -enable-kvm -net nic,model=virtio

![](https://www.fa1c0n.cn/img/posts/virtio/5.png)  

安装过程中,直接加载硬盘驱动器的VirtIO驱动(Red Hat VirtIO Block Disk Device WinXP/32bit)安装后的操作系统直接支持VirtIO磁盘驱动。

如图所示,在安装过程中,会提示安装Red Hat VirtIO SCSI Controller驱动程序,代表此虚拟机已加载virtio驱动程序。
系统安装完毕后,加载virtio-win-0.1.96.iso文件到光驱安装PCI设备驱动(VirtIO Balloon Driver)和网卡驱动(Red Hat VirtIO Ethernet Adapter),如图所示:

安装完成所有驱动后,打开任务管理器,效果如图所示:

如上图所示,任务管理器中成功安装了SCSI驱动、网卡驱动、Balloon驱动。实现了在Windows上通过QEMU命令行参数启动带有Virtio网卡驱动、Virtio磁盘驱动、Virtio Balloon设备驱动的虚拟机。

注意:使用qemu-system-x86_64命令启动虚拟机时肯能会出现以下错误:
qemu-system-x86_64: drive with bus=0, unit=0 (index=0) exists
解决方案如下:
1)大部分的原因是参数不对,可能少了“-”符号之类的;
2)参数是正确的,但是还是会出现这种情况,可以把参数重新用手动输入的方式敲,不要直接复制,因为复制过程中可能复制了一些我们没注意到的字符导致程序不识别;
3)可以切换至root权限下操作。

网络启动脚本/etc/qemu_ifup源码

#! /bin/sh
# Script to bring a network (tap) device for qemu up.
# The idea is to add the tap device to the same bridge
# as we have default routing to.

# in order to be able to find brctl
PATH=$PATH:/sbin:/usr/sbin
ip=$(which ip)

if [ -n "$ip" ]; then
    ip link set "$1" up
else
brctl=$(which brctl)
if [ ! "$ip" -o ! "$brctl" ]; then
    echo "W: $0: not doing any bridge processing: neither ip nor brctl utility not found" >&2
    exit 0
fi
ifconfig "$1" 0.0.0.0 up
fi

switch=$(ip route ls | \
awk '/^default / {
      for(i=0;i<NF;i++) { if ($i == "dev") { print $(i+1); next; } }
     }'
    )

# only add the interface to default-route bridge if we
# have such interface (with default route) and if that
# interface is actually a bridge.
# It is possible to have several default routes too
for br in $switch; do
    if [ -d /sys/class/net/$br/bridge/. ]; then
        if [ -n "$ip" ]; then
        ip link set "$1" master "$br"
        else
         brctl addif $br "$1"
        fi
        exit    # exit with status of the previous command
    fi
done

KVM虚拟机在物理主机之间迁移的实现

迁移(migration)包括系统整体的迁移和某个工作负载的迁移。
系统的整体迁移,是将系统上的所有软件(也包括操作系统)完全复制到另外一台物理硬件机器上。而工作负载的迁移是将系统撒谎嗯某个工作负载起那一另外一台物理机器上运行。
服务器系统迁移的作用在于简化了系统的维护管理,提高了系统的负载均衡,增强了负载的容错性,并优化了系统的电源管理。
虚拟化环境中的静态迁移也可以分为两种,第一种是关闭客户机后,将其硬盘镜像复制到另外一台宿主即机中然后恢复启动起来。这种迁移不能够保证客户机中运行的工作负载。另外一种是两台宿主机共享存储系统,只需要暂停客户机,复制其内存镜像到另外一台宿主机中恢复启动,这种迁移可以保证客户机迁移前的内存状态和系统运行的工作负载
动态迁移:是指在保证客户机上应用服务正常运行的同时,让客户机在不同的宿主机之间进行迁移,其逻辑步骤和前面的静态迁移几乎一致,有硬盘存储和内存都复制的动态迁移,也有仅内存镜像迁移的。不同的是,为了保证迁移过程中客户机服务的可用性,迁移过程仅有非常短暂的停机时间。动态迁移允许系统管理员将客户机在不同的宿主机上进行迁移,同时不会断开访问客户机中服务的客户端或者应用程序的连接。
一个成功的动态迁移,需要保证客户机的内存、硬盘存存储、网络连接在迁移到目的主机后依然保持不变,而且迁移过程的服务暂停时间较短。
动态迁移的应用场景:负载均衡、解除硬件依赖、节约能源、实现客户机地理位置的远程迁移。
动态迁移的条件:源宿主机和目的宿主机共享存储系统,只需要通过网络发送客户机的vCPU执行状态,内存中的内容,虚拟设备的状态到目的主机上。否则就需要将客户机的磁盘存储发送到目的主机上。
虚拟机迁移技术为服务器虚拟化提供了便捷的方法。而目前流行的虚拟化工具如 VMware,Xen,HyperV,KVM 都提供了各自的迁移组件。尽管商业的虚拟软件功能比较强大,但是开源虚拟机如 Linux 内核虚拟机 KVM 和 XEN 发展迅速,迁移技术日趋完善。虚拟机迁移有三种方式,分别是P2V、V2V 和 V2P,不同的方式又存在许多不同的解决方案。
在虚拟化环境中的迁移又分为动态迁移和静态迁移。(也有人称作冷迁移,热迁移,或者离线迁移,在线迁移)。静态迁移和动态迁移的最大的区别在于静态迁移有明显的一段时间客户机的服务不能用,而动态迁移则没有明显的服务暂停。
静态迁移是指在虚拟机关闭或暂停的情况下,将源宿主机上虚拟机的磁盘文件和配置文件拷贝到目标宿主机上。这种方式需要显式的停止虚拟机运行,对服务可用性要求高的需求不合适。
动态迁移无需拷贝虚拟机配置文件和磁盘文件,但是需要迁移的主机之间有相同的目录结构放置虚拟机磁盘文件,可以通过多种方式实现,本例采用基于共享存储动态迁移,通过NFS来实现。
静态迁移也叫做常规迁移、离线迁移(Offline Migration)。是在虚拟机关机或暂停的情况下,拷贝虚拟机磁盘文件与配置文件到目标虚拟主机中,实现的从一台物理机到另一台物理机的迁移。因为虚拟机的文件系统建立在虚拟机镜像文件上面,所以在虚拟机关机的情况下,只需要简单的迁移虚拟机镜像和相应的配置文件到另外一台物理主机上即可。如果需要保存虚拟机迁移之前的状态,那么应该在迁移之前将虚拟机暂停,然后拷贝状态至目的主机,最后在目的主机重建虚拟机状态,恢复执行。这种方式的迁移过程需要显式的停止虚拟机的运行。从用户角度看,有明确的一段停机时间,虚拟机上的服务不可用。这种迁移方式简单易行,适用于对服务可用性要求不严格的场合。
KVM虚拟机迁移原理:虚拟机迁移过程中数据的传输(磁盘镜像和内存数据),通常有两种常用的数据传输方式:
基于hypervisor的传输机制,即通过host之间连接来进行数据传输
基于libvirtd的传输机制,即两个libvirtd进程之间的数据传输

基于hypervisor的数据传输
这种传输方式具有最低的overload,因为传输的是裸数据,不支持数据的加密。另外,因为依赖于hypervisor的网络,所以需要对hypervisor networks进行一些特定的配置,比如打开某些端口。
hypervisor_based_datatransfer

基于libvirtd的数据传输
这种传输方式支持加密,是通过libvirt内建的RPC协议来进行数据的传输的,但是缺点是除了传输裸数据外,还需要传输一些额外的数据,这对镜像尺寸很大的虚拟机来说是个大问题。优点是由于不依赖与hypervisor network,所以不需要hypervisor对network做过多的配置,仅仅打开某个指定的port即可。
libvirtd_datatransfer

虚拟机迁移过程中的控制流
带有管理端的直接迁移:这种迁移方式是由一个管理客户端发起,管理客户端完全控制整个迁移流程,所以它必须能够且有权限访问源主机和目的主机上libvirtd的权限,因为外加一个管理客户端,所以不需要源libvirtd和目的libvirtd之间进行直接的交流,只需要按照管理客户端的指示来办事就好了。
control_stream_datatransfer

带有管理端的点对点的迁移
这种迁移方式下,管理客户端至于源libvirtd交互,然后源libvirtd完全控制整个迁移过程。优点是,即使管理客户端挂掉了,迁移还是能正常完成的。
p2p_migration

静态迁移过程较为简单,故作者开发了脚本程序来实现虚拟机静态迁移。
源码:Github
虚拟机静态迁移效果图:

上面的图为执行虚拟机转移端的脚本执行图。
下面的图为迁移到另外一台虚拟机的效果图。

KVM虚拟机动态迁移无需拷贝虚拟机配置文件和磁盘文件,但是需要迁移的主机之间有相同的目录结构放置虚拟机磁盘文件(本例为“/home/kvm”目录),这里的动态迁移是基于共享存储动态迁移,通过NFS来实现,需要qemu-kvm-0.12.2以上版本支持。
在NFS服务器上,下载安装NFS,kernel-server相当于server端,common是client端,使用命令

# sudo apt-get install nfs-kernel-server nfs-common rpcbind

安装NFS,在客户端机上使用命令

# sudo apt-get install nfs-common

安装NFS客户端。
若要实现虚拟机动态迁移,则至少需要三台虚拟机。以10.211.55.7为IP地址的虚拟机为NFS服务器。以10.211.55.6为IP地址的虚拟机为源虚拟机,以10.211.55.3为IP地址的虚拟机为目标虚拟机。
在NFS服务器上,可随时使用如下命令查看服务器共享目录:

# showmount -e 10.211.55.7

在NFS服务器上配置共享目录,首先使用命令

# sudo mkdir /root/share

创建该目录,然后使用命令

# sudo chmod -R 777 /root/share

修改该目录权限,接下来使用vim修改“/etc/exports”文件添加共享目录,在该文件中添加“/root/share (rw,sync)”即可。(rw,sync)是命令参数,表示包括读写权限。
“/etc/exports”文件修改后,使用命令

# sudo exportfs –r

刷新。然后启动NFS服务,命令如下:

# sudo /etc/init.d/rpcbindr restart
# sudo /etc/init.d/nfs-kernel-server restart

效果如图所示:

在客户机上可使用如下命令挂载并查看NFS目录:

# mount -t nfs 10.211.55.7://root/share /root/share-disk -o rw
# df && ll /root/share-disk/

如下图所示,可以看到另外一台虚拟机已经成功挂载nfs磁盘镜像:

接下来,在源宿主机上建立WinXP的XML描述文件,可使用多种方法。
在成功配置WinXP后,可以看到可以ping通内网外网。

从图中的Terminal可以看到,使用

# virsh list —all

命令可以看到虚拟机winxp正在运行,准备执行虚拟机迁移操作:

接下来执行虚拟机动态迁移。为了保证动态迁移的准确性,我们选择在NFS服务器上ping WinXP虚拟机以查看是否断开:

# ping 10.211.55.8

在源主机上输入以下命令以执行虚拟机迁移操作:

# virsh migrate winxp —live qemu+ssh://10.211.55.3/system tcp://10.211.55.3/system —unsafe

在命令执行前即开始ping操作,一直到结束停止ping操作。NFS服务器ping虚拟机效果图:

上图为迁移后的源宿主机。
下图为目标机。可以看到虚拟机启动后的状态仍为迁移前的状态未动。

至此,虚拟机的动态迁移操作完成。

在虚拟机迁移过程中,遇到过提示错误:
无法在 ‘br0’获取接口MTU:没有那个设备
解决方案:源主机与宿主机同时建立br0网桥,可使用如下命令:

#root@master:~# brctl addbr br0        #增加一个虚拟网桥br0
#root@master:~# brctl addif br0 eth0    #在br0中添加一个接口eth0
#root@master:~# brctl stp br0 on        #打开STP协议,否则可能造成环路
#root@master:~# ifconfig eth0 0        #将eth0的IP设置为0
#root@master:~# dhclient br0          #设置动态给br0配置ip、route等
#root@master:~# route                #显示路由表信息
#root@master:~# brctl show    #显示网桥信息

若需要持久化br0网桥,可直接写入/etc/network/interfaces文件解决。

虚拟机静态迁移工具源码:

#!/bin/bash
read -p "Please Input Domain Name to Migrate: " -s domainName
cd ~/
echo Preparing VMs basic files, please wait...
mkdir $domainName > /dev/null
virsh dumpxml $domainName >  ~/$domainName/$domainName.xml

qcow2=$(virsh dumpxml $domainName | grep qcow2 | grep file | sed "s/'/#/g" | sed "s/'/#/g" | sed -r 's/.*#(.*)#.*/\1/')

if [ $qcow2=='' ]
then
     img=$(virsh dumpxml $domainName | grep img | grep file | sed "s/'/#/g" | sed "s/'/#/g" | sed -r 's/.*#(.*)#.*/\1/')
fi

if [ $qcow2=='' ]
then
     raw=$(virsh dumpxml $domainName | grep raw | grep file | sed "s/'/#/g" | sed "s/'/#/g" | sed -r 's/.*#(.*)#.*/\1/')
fi

if [ -n "$qcow2" ]
then
        cp $qcow2 ~/$domainName/
elif [ -n "$img" ]
then
        cp $img ~/$domainName/
elif [ -n "$raw" ]
then
        cp $raw ~/$domainName/
else
        echo $domainName is not exist. Please check again.
fi
echo VM Files Preparing finished.
read -p "Please Input The Remote Host IP: " -s remotehostIP
read -p "Please Input Remote Host Username: " -s remotehostUsername
echo If needed, you need to input remoteHost user password.
dircreate="mkdir ~\/"$domainName
softinstall="sudo apt-get install libvirt-bin kvm qemu virtinst virt-manager virt-viewer"
vmregister="virsh define ~\/"$domainName"\/"$domainName".xml"
ssh -t -p 22 $remotehostUsername@$remotehostIP $dircreate
scp ~/$domainName/* $remotehostUsername@$remotehostIP:~/$domainName
echo Register Remote Host VMs...
ssh -t -p 22 $remotehostUsername@$remotehostIP $softinstall
ssh -t -p 22 $remotehostUsername@$remotehostIP $vmregister
echo VMs Static Migration Finished.

CocoaPods 默态框架下载失败及过慢的解决方案

在刚刚安装好CocoaPods后,为项目配置框架时,执行pod install会出现如下问题:

sh-3.2# pod install
Updating local specs repositories
Analyzing dependencies
[!] Unable to find a specification for `SDWebImage`

即为:错误:[!] Unable to find a specification for ‘$randomFrameworkName’
$randomFrameworkName可替换为任意框架名称,如SDWebImage。

经研究,得出解决方案为:更换CocoaPods的默认pod-repolist

更换repolist-spec镜像方案:
spec镜像方案源链接

pod repo remove master
pod repo add master https://gitcafe.com/akuandev/Specs.git
pod repo update

如果想用oschina的镜像也可以把第二条命令 换成 http://git.oschina.net/akuandev/Specs.git 即可


更换操作记录如下:

sh-3.2# pod repo list
master
- Type: git (master)
- URL:      https://github.com/CocoaPods/Specs.git
- Path: /Users/Fa1c0n/.cocoapods/repos/master
1 repo

sh-3.2# pod repo remove master
Removing spec repo `master`

sh-3.2# pod repo add master https://gitcafe.com/akuandev/Specs.git
Cloning spec repo `master` from `https://gitcafe.com/akuandev/Specs.git`

sh-3.2# pod repo update
Updating spec repo `master`
Already up-to-date.

sh-3.2# pod install

Creating shallow clone of spec repo `master-1` from `https://github.com/CocoaPods/Specs.git`
Updating local specs repositories
Analyzing dependencies
Downloading dependencies
Installing AFNetworking (2.6.0)
Installing AFOnoResponseSerializer (1.0.0)
Installing DateTools (1.7.0)
Installing GPUImage (0.1.7)
Installing GRMustache (7.3.2)
Installing JRSwizzle (1.0)
Installing MBProgressHUD (0.9.1)
Installing MJRefresh (2.4.12)
Installing Ono (1.2.2)
Installing RESideMenu (4.0.7)
Installing Reachability (3.2)
Installing ReactiveCocoa (2.5)
Installing SDWebImage (3.7.3)
Installing SSKeychain (1.2.3)
Installing TBXML (1.5)
Installing TOWebViewController (2.0.19)
Installing TTTAttributedLabel (1.13.4)
Generating Pods project
Integrating client project
Pod installation complete! There are 14 dependencies from the Podfile and 17 total pods installed.

在执行pod install和pod update过程中,由于CocoaPods会自动升级spec库,导致执行过程缓慢,可使用以下参数忽略升级spec库:

pod install --verbose --no-repo-update
pod update --verbose --no-repo-update

由于pod版本更新,会导致重复克隆pod文件,解决方案:在Podfile的第一行中加入:

source ‘https://gitcafe.com/akuandev/Specs.git‘

即可解决

Mac下NTFS格式外存器不能写入问题的解决方案

很多人在用Mac的时候,使用的U盘,移动硬盘大多是NTFS格式的。插入Mac后,默认情况下Mac不能读写NTFS格式的外存,普遍的解决方案是通过安装第三方软件如Paragon NTFS For Mac等实现NTFS的读写,但Paragon是商业软件,需要购买使用。

大部分情况下,外存设备插入Mac后不能写入,有人认为Mac根本不支持NTFS格式系统的写入,事实是,Mac本身对于NTFS的读写支持的非常优秀,只是检测到外存设备时,Mac并没有默认打开读写的功能而已。开启的方法如下:

方案一:适用于本机硬盘或固定移动硬盘

在终端中输入如下命令:

diskutil list | grep NTFS

在输出的结果中的TYPE列为Windows_NTFS的即为NTFS分区,需要记录一下卷标。然后键入:

sudo vifs

后,会进入fstab的vi编辑模式,然后输入以下内容:

LABEL=FA1C0N-EXT-DISK none ntfs rw,nobrowse,noowners,noatime,nosuid

注意,FA1C0N-EXT-DISK是刚才记录的卷标名,按下Esc,输入:wq后退出以后每次开机都会自动挂载为可读写格式。

方案二:适用于移动硬盘或U盘等设备

在终端中输入如下命令:

mount | grep ntfs

本地输出结果如下:

/dev/disk3s8 on /Volumes/FA1C0N-EXT (ntfs, local, nodev, nosuid, read-only, noowners)

得到以上信息后,可以根据此信息卸载当前的挂载点,如当前的挂载点为:/dev/disk3s8,则执行下面的命令:

umount /dev/disk3s8

注:若当前挂载点有文件被打开或程序占用,则会出现卸载失败。解决方法:退出当前所有可能占用该挂载点中文件的程序即可。

执行下面的命令创建一个挂载目录:

mkdir -p /Volumes/FA1C0N-EXT-DISK

执行以下命令实现读写挂载:

mount_ntfs -o rw,auto,nobrowse,noowners,noatime  /dev/disk3s8 /Volumes/FA1C0N-EXT-DISK

以上命令的 rw 选项添加了读写权限,到这里完成一个磁盘的挂载,其它的用同样的方法。如果是移动硬盘,在-o后再加一个nodev选项。挂载成功的磁盘并不会在Finder边栏中出现,需要访问到/Volumes/FA1C0N-EXT-DISK目录即可访问该磁盘。

文章中第二种方法虽然方便,但每次手动输入命令,时间长了或许会忘记,故作者写了一个小脚本,在执行完成后自动打开当前挂载的NTFS分区,源码如下:

#!/bin/bash

#
# Author: Fa1c0n
# Copyright (c) https://blog.fa1c0n.com
# Version: 1.0
#

mount | grep ntfs
echo "请输入要重新挂载的分区标识(on左侧):"
read PARTITION
umount $PARTITION
mkdir -p /Volumes/FA1C0N-EXT-DISK
mount_ntfs -o rw,auto,nobrowse,noowners,noatime $PARTITION /Volumes/FA1C0N-EXT-DISK
echo "挂载成功!感谢使用!"
open /Volumes/FA1C0N-EXT-DISK

创建该脚本,可以点击此处下载或在终端中进入指定目录后执行:

touch ntfsRemount.sh
vi ntfsRemount.sh

在进入vi后,按下i键即可进入编辑模式,将上面的源码粘贴到文本区域,按下Esc键,输入:wq后即可保存。保存后输入下列指令为当前脚本程序增加可执行权限:

sudo chmod +x ./ntfsRemount.sh

执行完成后,每次需要挂载时,只需要输入:

sudo sh ntfsRemount.sh

即可挂载可读写的NTFS分区。

Hadoop分布式开发环境搭建教程

配置外部环境:
Mac OS X El Capitan 10.11.4
选用虚拟机:VMware Fusion 8.1 Professional
下载并安装VMware虚拟机,由于本文的重点不是安装虚拟机,故虚拟机软件安装部分不再描述。
本次虚拟机系统采用最新版本的Ubuntu 15.10 Wily Werewolf,点击此处可下载镜像文件
Ubuntu ISO Image

下载完成后,即可打开VMware开始创建虚拟机并安装,只是在创建虚拟机的过程中,建议不要勾选Linux快捷安装。如图所示:
Disable QuickSetup Image
在虚拟机创建完成后,先不要急着开机启动系统安装,先打开设置页面,选择处理器和内存,根据当前宿主机器配置虚拟机可用CPU处理器核心数量和合适的内存大小,本机选取了4个处理器核心,2GB的内存,在高级选项中推荐打开虚拟化管理程序和代码分析应用程序,有助于提升虚拟机中部分程序的运行效率,如图所示:
CPUSettings Image
点击显示全部后,选择网络适配器,配置Hadoop务必要将网络适配器模式选择为桥接模式,可根据宿主机器的网络选择,如图所示:
BridgeNetwork Image
返回后进入磁盘模块,可以适当增加磁盘容量,hadoop是分布式存储系统,建议的磁盘容量是40GB。完成后,即可开启虚拟机自动加载镜像,并开始安装Ubuntu,如图所示:
UbuntuSetupWelcome Image
注:在磁盘分区模块,不推荐选择加密Ubuntu,会给后面的操作带来麻烦。
UbuntuDiskSetting Image
Ubuntu系统的安装过程本文不再描述。
UbuntuSetup Image
如下图所示,Ubuntu已安装完成:
UbuntuFinish Image
UbuntuDesktop Image
在初次完成安装Ubuntu后,打开终端,需要先执行以下两条命令:

sudo apt-get update
sudo apt-get dist-upgrade

DistUpgrade Image
刚刚安装好的Ubuntu执行dist-upgrade后建议重启一次,此次更新了包括linux-kernel-image、linux-firmware等内核级软件,如图所示:
DistUpgradeReboot Image
由于此分布式开发环境在移动PC端搭建,性能不足,故只搭建3个主机的集群。
众所周知,Hadoop是基于Java编写,Hadoop、MapReduce运行需要JDK,因此在安装Hadoop之前,必须安装和配置JDK。点击此处跳转到Oracle官方下载链接下载。
下载完成后,笔者将JDK放在了/usr/local/jdk目录下,读者可根据自己的情况适当调整。
jdkUncompressPath Image
jdkFolder Image
在配置完成jdk后,需要为jdk设置环境变量,在终端下输入:

sudo vi /etc/profile

然后增加如下几条命令:

export JAVA_HOME=/usr/local/jdk
export PATH=$JAVA_HOME/bin:$PATH
export CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar

如图所示:
jdkProfileSettings Image
若要让环境变量生效,可以选择重启或执行以下命令并如图所示:

source /etc/profile
java -version

jdksourceProfile Image
在执行java -version成功出现java版本信息时,此时jdk配置完毕。

接下来,到Hadoop官网下载Hadoop的执行包,而非源码包,一定要选择binary下载,而非source。(否则需要配置环境编译Hadoop,编译Hadoop将在另外一篇文章中详细描述,此处不再详叙。)链接点击此处。本文笔者下载最新版本的Hadoop2.6.4的安装包,如图所示:
DownloadHadoopBinary Image
在下载完成后,首先我们先开启Ubuntu的root用户登陆,Ubuntu安装完成后默认不能够使用root用户登陆,开启root登录,需要执行以下指令:

vi /usr/share/lightdm/lightdm.conf.d/50-ubuntu.conf

并在行末添加:

greeter-show-manual-login=true

如图所示:
enableRootLogin Image
由于刚刚开启了root账户的登录权限,故需要为root账户设置密码,输入如下指令:

sudo passwd root

输入两次root账户要设置的密码,如图所示:
passwdRoot Image
完成上述命令后,重新启动Ubuntu,可以看到多用户登录界面,选择root用户,输入密码登录,如图所示:
rootLogin Image
接下来需要配置ssh免密码登录,输入以下命令安装ssh:

sudo apt-get install ssh

如图所示:
sshInstall Image
在安装完成后,输入以下命令检查ssh服务是否启动,如图所示:

ps -e | grep ssh

grepSSHInstall Image
安装完成后,打开ssh配置文件修改远程登录访问权限:

vi /etc/ssh/sshd_config

修改行内容如下:

//PermitRootLogin without-password
PermitRootLogin yes

permitRootLoginSSHConfig Image
生成ssh密钥的过程需要在三台中进行,故ssh密钥配置稍后进行。
现在将刚才下载好的Hadoop进行解压,如图所示:
hadoopUncompress Image
或使用以下命令解压:

tar -zxvf /home/fa1c0n/download/hadoop-2.6.4.tar.gz

进行解压。
配置Hadoop需要配置以下文件,参加文件列表:

core-site.xml
hadoop-env.sh
hdfs-site.xml
mapred-site.xml
slaves
yarn-env.sh
yarn-site.xml

由mapred-site.xml不存在,故打开终端后,使用下列命令创建,如图所示:

cd /root/hadoop-2.6.4/
cd etc/hadoop/
cp mapred-site.xml.template mapred-site.xml

createmapred-sitexml Image
首先,执行下面的命令修改core-site.xml:

vi /root/hadoop-2.6.4/etc/hadoop/core-site.xml

如图所示:
coresitexml Image
core-site.xml文件内容如下,如果怕敲错可以直接粘贴上去:

<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>
<!--
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. See accompanying LICENSE file.
-->

<!-- Put site-specific property overrides in this file. -->

<configuration>
  <property>
    <name>fs.default.name</name>
    <value>hdfs://master:9000</value>
  </property>
  <property>
    <name>hadoop.tmp.dir</name>
    <value>/home/hdfs_all/tmp</value>
  </property>
</configuration>

接下来修改hadoop-env.sh,指令同上:

vi /root/hadoop-2.6.4/etc/hadoop/hadoop-env.sh

如图所示:
hadoopenvsh Image
同样,hadoop-env.sh文件内容如下:

# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE     file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# Set Hadoop-specific environment variables here.

# The only required environment variable is JAVA_HOME.  All others are
# optional.  When running a distributed configuration it is best to
# set JAVA_HOME in this file, so that it is correctly defined     on
# remote nodes.

# The java implementation to use.
# export JAVA_HOME=${JAVA_HOME}
export JAVA_HOME=/usr/local/jdk
# The jsvc implementation to use. Jsvc is required to run secure datanodes
# that bind to privileged ports to provide authentication of data transfer
# protocol.  Jsvc is not required if SASL is configured for authentication of
# data transfer protocol using non-privileged ports.
#export JSVC_HOME=${JSVC_HOME}

export HADOOP_CONF_DIR=${HADOOP_CONF_DIR:-"/etc/hadoop"}

# Extra Java CLASSPATH elements.  Automatically insert capacity-scheduler.
for f in $HADOOP_HOME/contrib/capacity-scheduler/*.jar; do
  if [ "$HADOOP_CLASSPATH" ]; then
    export HADOOP_CLASSPATH=$HADOOP_CLASSPATH:$f
  else
    export HADOOP_CLASSPATH=$f
  fi
done

# The maximum amount of heap to use, in MB. Default is 1000.
#export HADOOP_HEAPSIZE=
#export HADOOP_NAMENODE_INIT_HEAPSIZE=""

# Extra Java runtime options.  Empty by default.
export HADOOP_OPTS="$HADOOP_OPTS -Djava.net.preferIPv4Stack=true"

# Command specific options appended to HADOOP_OPTS when specified
export HADOOP_NAMENODE_OPTS="-Dhadoop.security.logger=${HADOOP_SECURITY_LOGGER:-INFO,RFAS} -Dhdfs.audit.logger=${HDFS_AUDIT_LOGGER:-INFO,NullAppender} $HADOOP_NAMENODE_OPTS"
export HADOOP_DATANODE_OPTS="-Dhadoop.security.logger=ERROR,RFAS $HADOOP_DATANODE_OPTS"

export HADOOP_SECONDARYNAMENODE_OPTS="-Dhadoop.security.logger=${HADOOP_SECURITY_LOGGER:-INFO,RFAS} -Dhdfs.audit.logger=${HDFS_AUDIT_LOGGER:-INFO,NullAppender} $HADOOP_SECONDARYNAMENODE_OPTS"

export HADOOP_NFS3_OPTS="$HADOOP_NFS3_OPTS"
export HADOOP_PORTMAP_OPTS="-Xmx512m $HADOOP_PORTMAP_OPTS"

# The following applies to multiple commands (fs, dfs, fsck, distcp etc)
export HADOOP_CLIENT_OPTS="-Xmx512m $HADOOP_CLIENT_OPTS"
#HADOOP_JAVA_PLATFORM_OPTS="-XX:-UsePerfData $HADOOP_JAVA_PLATFORM_OPTS"

# On secure datanodes, user to run the datanode as after dropping privileges.
# This **MUST** be uncommented to enable secure HDFS if using privileged ports
# to provide authentication of data transfer protocol.  This **MUST NOT** be
# defined if SASL is configured for authentication of data transfer protocol
# using non-privileged ports.
export HADOOP_SECURE_DN_USER=${HADOOP_SECURE_DN_USER}

# Where log files are stored.  $HADOOP_HOME/logs by default.
#export HADOOP_LOG_DIR=${HADOOP_LOG_DIR}/$USER

# Where log files are stored in the secure data environment.
export HADOOP_SECURE_DN_LOG_DIR=${HADOOP_LOG_DIR}/${HADOOP_HDFS_USER}

###
# HDFS Mover specific parameters
###
# Specify the JVM options to be used when starting the HDFS Mover.
# These options will be appended to the options specified as HADOOP_OPTS
# and therefore may override any similar flags set in     HADOOP_OPTS
#
# export HADOOP_MOVER_OPTS=""

###
# Advanced Users Only!
###

# The directory where pid files are stored. /tmp by default.
# NOTE: this should be set to a directory that can only be written to by
#       the user that will run the hadoop daemons.  Otherwise there is the
#       potential for a symlink attack.
export HADOOP_PID_DIR=${HADOOP_PID_DIR}
export HADOOP_SECURE_DN_PID_DIR=${HADOOP_PID_DIR}

# A string representing this instance of hadoop. $USER by default.
export HADOOP_IDENT_STRING=$USER

接下来修改hdfs-site.xml,指令同上:

vi /root/hadoop-2.6.4/etc/hadoop/hdfs-site.xml

如图所示:
hdfs-sitexml Image
同样,hdfs-site.xml文件内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>
<!--
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. See accompanying LICENSE file.
-->

<!-- Put site-specific property overrides in this file. -->

<configuration>
    <property>
            <name>dfs.replication</name>
        <value>2</value>
    </property>
    <property>
<name>dfs.namenode.name.dir</name>
<value>file:/home/hdfs_all/dfs/name</value>
    </property>
    <property>
      <name>dfs.namenode.data.dir</name>
  <value>file:/home/hfds_all/dfs/data</value>
    </property>
</configuration>

接下来修改mapred-site.xml,指令同上:

vi /root/hadoop-2.6.4/etc/hadoop/mapred-site.xml

如图所示:
mapred-sitexml Image
同样,mapred-site.xml文件内容如下:

<?xml version="1.0"?>
<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>
<!--
  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
  You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License. See accompanying LICENSE file.
-->

<!-- Put site-specific property overrides in this file. -->

<configuration>
    <property>
        <name>mapreduce.framework.name</name>
        <value>yarn</value>
    </property>
    <property>
        <name>mapreduce.jobhistory.address</name>
        <value>master:10020</value>
    </property>
    <property>
        <name>mapreduce.jobhistory.webapp.address</name>
        <value>master:19888</value>
    </property>
</configuration>

接下来修改slaves,指令同上:

vi /root/hadoop-2.6.4/etc/hadoop/slaves

如图所示:
slaves Image
同样,slaves文件内容如下:

slave1
slave2

注:该文件自带的localhost须去掉,原因:此处填写的是DataNode,而非NameNode。

接下来修改yarn-env.sh,指令同上:

vi /root/hadoop-2.6.4/etc/hadoop/yarn-env.sh

如图所示:
yarn-envsh Image
同样,yarn-env.sh文件内容如下:

# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License.  You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# User for YARN daemons
export HADOOP_YARN_USER=${HADOOP_YARN_USER:-yarn}

# resolve links - $0 may be a softlink
export YARN_CONF_DIR="${YARN_CONF_DIR:-$HADOOP_YARN_HOME/conf}"

# some Java parameters
# export JAVA_HOME=/home/y/libexec/jdk1.6.0/
export JAVA_HOME=/usr/local/jdk
if [ "$JAVA_HOME" != "" ]; then
  #echo "run java in $JAVA_HOME"
  JAVA_HOME=$JAVA_HOME
fi

if [ "$JAVA_HOME" = "" ]; then
  echo "Error: JAVA_HOME is not set."
  exit 1
fi

JAVA=$JAVA_HOME/bin/java
JAVA_HEAP_MAX=-Xmx1000m

# For setting YARN specific HEAP sizes please use this
# Parameter and set appropriately
# YARN_HEAPSIZE=1000

# check envvars which might override default args
if [ "$YARN_HEAPSIZE" != "" ]; then
  JAVA_HEAP_MAX="-Xmx""$YARN_HEAPSIZE""m"
fi

# Resource Manager specific parameters

# Specify the max Heapsize for the ResourceManager using a numerical value
# in the scale of MB. For example, to specify an jvm option of -Xmx1000m, set
# the value to 1000.
# This value will be overridden by an Xmx setting specified in either YARN_OPTS
# and/or YARN_RESOURCEMANAGER_OPTS.
# If not specified, the default value will be picked from either YARN_HEAPMAX
# or JAVA_HEAP_MAX with YARN_HEAPMAX as the preferred option of the two.
#export YARN_RESOURCEMANAGER_HEAPSIZE=1000

# Specify the max Heapsize for the timeline server using a numerical value
# in the scale of MB. For example, to specify an jvm option of -Xmx1000m, set
# the value to 1000.
# This value will be overridden by an Xmx setting specified in either YARN_OPTS
# and/or YARN_TIMELINESERVER_OPTS.
# If not specified, the default value will be picked from either YARN_HEAPMAX
# or JAVA_HEAP_MAX with YARN_HEAPMAX as the preferred option of the two.
#export YARN_TIMELINESERVER_HEAPSIZE=1000

# Specify the JVM options to be used when starting the ResourceManager.
# These options will be appended to the options specified as YARN_OPTS
# and therefore may override any similar flags set in YARN_OPTS
#export YARN_RESOURCEMANAGER_OPTS=

# Node Manager specific parameters

# Specify the max Heapsize for the NodeManager using a numerical value
# in the scale of MB. For example, to specify an jvm option of -Xmx1000m, set
# the value to 1000.
# This value will be overridden by an Xmx setting specified in either YARN_OPTS
# and/or YARN_NODEMANAGER_OPTS.
# If not specified, the default value will be picked from either YARN_HEAPMAX
# or JAVA_HEAP_MAX with YARN_HEAPMAX as the preferred option of the two.
#export YARN_NODEMANAGER_HEAPSIZE=1000

# Specify the JVM options to be used when starting the NodeManager.
# These options will be appended to the options specified as YARN_OPTS
# and therefore may override any similar flags set in YARN_OPTS
#export YARN_NODEMANAGER_OPTS=

# so that filenames w/ spaces are handled correctly in loops below
IFS=


# default log directory & file
if [ "$YARN_LOG_DIR" = "" ]; then
  YARN_LOG_DIR="$HADOOP_YARN_HOME/logs"
fi
if [ "$YARN_LOGFILE" = "" ]; then
  YARN_LOGFILE='yarn.log'
fi

# default policy file for service-level authorization
if [ "$YARN_POLICYFILE" = "" ]; then
  YARN_POLICYFILE="hadoop-policy.xml"
fi

# restore ordinary behaviour
unset IFS


YARN_OPTS="$YARN_OPTS -Dhadoop.log.dir=$YARN_LOG_DIR"
YARN_OPTS="$YARN_OPTS -Dyarn.log.dir=$YARN_LOG_DIR"
YARN_OPTS="$YARN_OPTS -Dhadoop.log.file=$YARN_LOGFILE"
YARN_OPTS="$YARN_OPTS -Dyarn.log.file=$YARN_LOGFILE"
YARN_OPTS="$YARN_OPTS -Dyarn.home.dir=$YARN_COMMON_HOME"
YARN_OPTS="$YARN_OPTS -Dyarn.id.str=$YARN_IDENT_STRING"
YARN_OPTS="$YARN_OPTS -Dhadoop.root.logger=${YARN_ROOT_LOGGER:-INFO,console}"
YARN_OPTS="$YARN_OPTS -Dyarn.root.logger=${YARN_ROOT_LOGGER:-INFO,console}"
if [ "x$JAVA_LIBRARY_PATH" != "x" ]; then
  YARN_OPTS="$YARN_OPTS -Djava.library.path=$JAVA_LIBRARY_PATH"
fi
YARN_OPTS="$YARN_OPTS -Dyarn.policy.file=$YARN_POLICYFILE"

接下来修改yarn-site.xml,指令同上:

vi /root/hadoop-2.6.4/etc/hadoop/yarn-site.xml

如图所示:
yarn-sitexml Image
同样,yarn-site.xml文件内容如下:

<?xml version="1.0"?>
<!--
  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
  You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License. See accompanying LICENSE file.
-->
<configuration>

<!-- Site specific YARN configuration properties -->

    <property>
        <name>yarn.nodemanager.aux-services</name>
        <value>mapreduce_shuffle</value>
    </property>
    <property>
        <name>yarn.nodemanager.aux-services.mapreduce.shuffle.class</name>
        <value>org.apache.hadoop.mapred.ShuffleHandler</value>
    </property>
    <property>
        <name>yarn.resourcemanager.address</name>
        <value>master:8032</value>
    </property>
    <property>
        <name>yarn.resourcemanager.scheduler.address</name>
        <value>master:8030</value>
    </property>
    <property>
        <name>yarn.resourcemanager.resource-tracker.address</name>
        <value>master:8031</value>
    </property>
    <property>
        <name>yarn.resourcemanager.admin.address</name>
        <value>master:8033</value>
    </property>
    <property>
        <name>yarn.resourcemanager.webapp.address</name>
        <value>master:8088</value>
    </property>
</configuration>

至此,Hadoop的配置文件配置完毕,为了防止读者在粘贴过程中配置出错,可以点击此处下载配置文件包,直接解压所有文件到

/root/hadoop-2.6.4/etc/hadoop/

目录即可。
下一步是配置环境变量,执行以下命令打开环境变量配置文件:

vi /etc/environment

如下图所示:
pathenvironmentsetting Image

PATH="/root/hadoop-2.6.4/bin:/root/hadoop-2.6.4/sbin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games"

注意增加的hadoop路径,然后输入以下命令使当前配置的环境变量生效,若仍没有生效,请尝试重启:

source /etc/environment

如下图所示:
sourcetcenvironment Image

到此时为止,配置Hadoop在一台机器需要完成的工作结束,输入poweroff关机。回到VMware Fusion 8中,对此虚拟机进行二次克隆。
点击Finder边栏上的虚拟机,选择创建完整克隆,输入克隆后的虚拟机,如图所示:
ubuntucloning Image
待克隆完成后,VMware的虚拟机资源列表应如下图所示:

clonefinishvmwarelist Image
接下来需要打开三台虚拟机进行操作了,同时启动三台虚拟机,如图所示:
ubuntuVMx3 Image
分别打开三台虚拟机的终端,输入ifconfig命令分别查看三台IP地址,按照虚拟机名分配master、slave,并填写在/etc/hosts文件中,如图所示:
etchostsettings Image
完成后保存hosts文件,并分别在三台主机上配置SSH密钥,在master上执行以下命令:

ufw disable
ssh-keygen -t dsa
cat ~/.ssh/id_dsa.pub >> ~/.ssh/authorized_keys
ls .ssh/
scp authorized_keys slave1:~/.ssh/

如下图所示:
masterSSHKeygenSettings Image
在slave1上执行如下命令:

ufw disable
ssh-keygen -t dsa
cat ~/.ssh/id_dsa.pub >> ~/.ssh/authorized_keys
ls .ssh/
scp authorized_keys slave2:~/.ssh/

如下图所示:
slave1SSHKeygenSettings Image
在slave2上执行如下命令:

ufw disable
ssh-keygen -t dsa
cat ~/.ssh/id_dsa.pub >> ~/.ssh/authorized_keys
ls .ssh/
scp authorized_keys master:~/.ssh/
scp authorized_keys slave1:~/.ssh/

如下图所示:
slave2SSHKeygenSettings_1 Image
slave2SSHKeygenSettings_2 Image

至此,SSH密钥配置完毕,接下来在master节点上执行以下命令格式化namenode节点:

hadoop namenode -format

如下图所示:
hadoopnamenodeformat1 Image
hadoopnamenodeformat2 Image
hadoopnamenodeformat3 Image
hadoopnamenodeformat4 Image
hadoopnamenodeformat5 Image
hadoopnamenodeformat6 Image

此时Hadoop已经配置完毕,输入以下命令在master节点上启动Hadoop:

start-all.sh

如下图所示:
startallsh Image
如上图,Hadoop已经配置成功,为了核实服务是否开启,在三台机器上分别运行jps,在master上运行jps如图所示:
masterjps Image
在slave1和slave2上运行jps如图所示:
slave1jps Image
slave2jps Image
若读者运行的jps和图片上一致,说明配置成功,运行下面的命令查看集群的状态:

hadoop dfsadmin -report

如下图所示:
dfsadminreport1 Image
dfsadminreport2 Image
有此图可以看到,Hadoop已配置成功,打开网页查看如下:
hadoopcluster Image
hadoopoverview1 Image
hadoopoverview2 Image
hadoopoverview3 Image
至此,Hadoop分布式集群开发环境搭建完毕,若需要停止Hadoop运行,则执行以下命令:

stop-all.sh

如图所示:
stopallsh Image

,