2024年1月

本文介绍在
Linux Ubuntu
操作系统下,配置
Visual Studio Code
软件与
C++
代码开发环境的方法。

在文章
VMware虚拟机部署Linux Ubuntu系统的方法
中,我们介绍了
Linux Ubuntu
操作系统的下载、安装方法;本文则基于前述基础,继续介绍在
Linux Ubuntu
操作系统中配置
Visual Studio Code
软件与
C++
开发环境的详细方法。需要注意的是,本文所用的
Ubuntu
版本为
Ubuntu 22.04.1 LTS
版本,其他版本的配置方法可能具体有一些小差异,但整体流程都是近似的。


Linux
操作系统中配置代码开发环境,最复杂的一点就在于其不能安装
Visual Studio
这种集成好的
IDE
软件,而是需要分别配置
Visual Studio Code

编辑器
软件、
g++

编译器
模块,同时对很多
配置文件
进行修改后才可以开始代码的开发工作;那么本文就以
C++
这一编程语言为例,来对上述这一复杂的过程进行介绍。

1 Visual Studio Code编辑器配置

1.1 VS Code软件下载与安装

首先,我们需要下载、安装
Visual Studio Code
软件;我们可以在软件官网下载并手动安装,也可以在
Linux Ubuntu
操作系统的
Terminal
中进行下载与安装,同时还可以在
Ubuntu Software
软件中进行下载与安装。这里我们就以第三种为例进行介绍。

首先,在
Linux Ubuntu
操作系统左侧菜单栏中,选择“
Ubuntu Software
”。

如果菜单栏中没有“
Ubuntu Software
”,那么需要在左下角“
Show Applications
”中,找到“
Ubuntu Software
”;如下图所示,如果软件比较多的话,我们可以通过搜索来找到“
Ubuntu Software
”。

随后,在“
Ubuntu Software
”左上角,点击搜索按钮,并输入
code
字样,找到如下图所示的“
code
”软件并打开;这里的“
code
”软件其实就是
Visual Studio Code
软件。

接下来,点击“
Install
”进行
Visual Studio Code
软件的下载与安装。

软件安装开始前,可能需要大家输入一下
Linux Ubuntu
操作系统的账户密码。

随后,即可开始
Visual Studio Code
软件的下载与安装。

Visual Studio Code
软件安装完毕后,我们可以通过“
Show Applications
”选项,找到刚刚安装好的软件。


Visual Studio Code
软件图标处右键,选择“
Add to Favorites
”,即可将其固定在左侧的菜单栏中。

接下来,我们打开
Visual Studio Code
软件,对其进行界面颜色等基本的设置。

1.2 VS Code插件配置

前面我们完成了对
Visual Studio Code
软件的下载与安装,而为了可以在其中撰写、运行
C++
代码或程序,我们还需要配置几个
Visual Studio Code
软件的插件。


Visual Studio Code
软件左侧菜单中,找到“
Extensions
”选项,并在弹出的界面中搜索
C++
字样,找到如下图所示的
C/C++
插件,点击“
Install
”进行安装。

这里需要注意,有些插件(例如前面提到的
C/C++
插件)有发行版与预览版等两个版本,对应了不同的版本号;二者可以相互转换。如下图所示,建议大家选择发行版即可。

1.3 VS Code工程配置

接下来,我们需要准备好
Visual Studio Code
软件的工程文件,用以接下来代码的撰写与运行。

首先,在
Linux Ubuntu
操作系统左侧菜单栏中,选择“
Files
”。

随后,在我们将要存放代码文件的路径下,在空白区域右键并选择“
New Folder
”选项,从而新建一个文件夹。这个文件夹就是之后存放我们工程文件的文件夹。

对该文件夹进行重命名。

接下来,在
Visual Studio Code
软件左侧菜单栏中,选择“
Explorer
”→“
Open Folder
”选项。

找到我们刚刚建立的文件夹,并选择“
Open
”。

如果弹出如下所示的界面,直接选择左侧一项即可。

接下来,在
Visual Studio Code
软件“
EXPLORER
”一栏中,在我们的文件夹下的空白处右键,选择“
New File...
”。

并将其命名为
main.cpp

随后,即可显示出代码撰写界面。

此外,
Visual Studio Code
软件有时还会依据我们的操作需求为我们推荐一些插件,大家可以自行选择是否下载;如上图右下角所示,
Visual Studio Code
软件推荐了
C/C++ Extension Pack
这个
C++
拓展包合集,如果需要的话我们可以直接点击并安装这一合集插件。

接下来,我们便可以在这里进行代码的撰写。例如,我们先写一个简单的“
Hello World
”程序;具体代码如下。

#include <iostream>
using namespace std;

int main() {
    cout << "Hello World!!!!!" << endl;
    return 0;
}

这里需要注意,代码编辑时,需要同时按下
Ctrl
键与
S
键以进行保存;代码完成撰写后,通过
F5
键调试程序,或同时按下
Ctrl
键与
F5
键不调试直接执行程序。

但此时,我们仅仅可以完成代码的撰写工作,并不能实现代码的编译与运行。如果此时我们按下
F5
键或同时按下
Ctrl
键与
F5
键,代码都不会运行,而会出现如下所示的报错内容(报错内容在下图最下方)。

因此,我们需要进一步配置相关的环境。

1.4 VS Code文件配置

接下来,还需要对
Visual Studio Code
软件中的部分
.json
文件进行配置。首先,在
Visual Studio Code
软件左侧菜单栏中,选择“
Run and Debug
”选项,并点击下图所示红色圈内的蓝色字体。

随后,将显示出
launch.json
文件。

其中,原有的
launch.json
文件代码如上图所示;我们需要将其修改为如下所示的代码。

{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "name": "(gdb) Launch",
            "type": "cppdbg",
            "request": "launch",
            "program": "${workspaceFolder}/${fileBasenameNoExtension}.out",
            "args": [],
            "stopAtEntry": false,
            "cwd": "${workspaceFolder}",
            "environment": [],
            "externalConsole": true,
            "MIMode": "gdb",
            "preLaunchTask": "build",
            "setupCommands": [
                {
                "description": "Enable pretty-printing for gdb",
                "text": "-enable-pretty-printing",
                "ignoreFailures": true
                }
            ]
        }
        ]
}

修改后的
launch.json
文件代码如下图所示。

随后,记得用
Ctrl
键与
S
键保存刚刚的修改。

接下来,我们在
Visual Studio Code
软件中,同时按下
Ctrl
键、
Shift
键与
P
键,唤起搜索框,并输入
Tasks: Run Task
,并选择这一项;如下图所示。

随后,点击右侧出现的
齿轮
标志。

随后,将显示出
tasks.json
文件。

我们需要将其修改为如下的代码。

{
    "tasks": [
        {
            "type": "shell",
            "label": "build",
            "command": "g++",
            "args": [
                "-g",
                "${file}",
                "-std=c++11",
                "-o",
                "${fileBasenameNoExtension}.out"
            ]
        }
    ],
    "version": "2.0.0"
}

修改后的
tasks.json
文件如下图所示。

随后,记得用
Ctrl
键与
S
键保存刚刚的修改。

2 其他环境配置

除了前述对
Visual Studio Code
软件的配置外,还需要进行一些额外的环境配置工作。

首先,我们需要对编辑代码用的
Vim
模块进行配置。在
Linux Ubuntu
操作系统中,同时按下
Ctrl
键、
Shift
键与
T
键,唤起终端,并在其中输入以下代码。

sudo apt-get install vim

随后,需要输入
Linux Ubuntu
操作系统的账户密码,需要注意这里是以不显示密码的方式输入的;输入完毕后按下
回车
键即可。接下来,会出现如下图所示的界面。

在这里,我们输入
y
,从而开始
Vim
模块的下载与安装。稍等片刻,
Vim
模块即可下载、安装完毕。

其次,我们还需要对编译、链接
C++
代码的
g++
模块进行配置(这一模块同样可以编译、链接
C
语言)。同样是在终端中,输入如下的代码。

sudo apt install g++

随后,将出现如下所示的界面。

同样,我们输入
y
,从而开始
g++
模块的下载与安装。稍等片刻,
g++
模块即可下载、安装完毕。

随后,回到我们前面写好的代码,通过
F5
键,或同时按下
Ctrl
键与
F5
键,都可以使得代码成功运行;如下图所示。

至此,我们即完成了在
Linux Ubuntu
操作系统下,配置
Visual Studio Code
软件与
C++
代码开发环境的全部流程。

本文只是陈述了一种方法,不必评判谁对谁错谁好谁坏,选择适合自己的即可。

环境

站点名称 vCenter版本 vSAN集群 集群主机 主机版本 磁盘组

vcsa67.lab.com

vCenter 6.7 U3 cluster

esxi-b1.lab.com

esxi-b2.lab.com

esxi-b3.lab.com

esxi-b4.lab.com

ESXi 6.7 U3

每台主机有两个磁盘组

每个磁盘组一个闪存盘

每个磁盘组两个容量盘

vcsa.lab.com

vCenter 7.0 U3 vsan01

esxi-a1.lab.com

esxi-a2.lab.com

esxi-a3.lab.com

esxi-a4.lab.com

ESXi 7.0 U3

每台主机有两个磁盘组

每个磁盘组一个闪存盘

每个磁盘组两个容量盘

内容

本文内容涉及到以下几个部分:

  • 将vcsa67站点上ESXi主机从vSAN集群cluster中撤出,并从vCenter中移除
  • 在vcsa站点上新建集群vsan02,并将esxi-b1到esxi-b4主机加入该集群
  • 将esxi-b1到esxi-b4主机的ESXi版本升级到vcsa站点上的主机版本(ESXi 7.0 U3)
  • 在vsan02集群中启用vsan,并申明esxi-b1到esxi-b4主机的vSAN磁盘

过程

一、将vcsa67站点上ESXi主机从vSAN集群cluster中撤出,并从vCenter中移除

注意:集群中不得存在任何虚拟机、文件,已全部迁走。

1.将主机置于维护模式。SSH登录到主机esxi-b1,不迁移数据。

esxcli system maintenanceMode set -e true -m noAction
esxcli system maintenanceMode get

2.删除主机上的vSAN磁盘组。有两种方式,一是根据磁盘组UUID,二是磁盘组名字。

esxcli vsan storage list | grep "VSAN Disk Group UUID"
esxcli vsan storage list | grep "VSAN Disk Group Name"

esxcli vsan storage remove -u "VSAN Disk Group UUID"
esxcli vsan storage remove -s "VSAN Disk Group Name"

esxcli vsan storage list

注:其实 "VSAN Disk Group UUID"和"VSAN Disk Group Name"就是磁盘组中闪存盘的UUID和名字,删除闪存盘就删除了整个磁盘组。

3.将主机退出vSAN群集。

esxcli vsan cluster leave
esxcli vsan cluster get

4.将主机从vCenter中移除。由于该主机的vSAN网络vmkernel适配器创建在分布式交换机vds上,所以先将vmkernel适配器从主机中删除,再将主机从vds中移除,最后再从vCenter中移除。

第一步:删除vmkernel适配器。通过powershell工具连接到vcsa67的vCenter。

Connect-VIServer vcsa67.lab.com -Username administrator@vsphere.local -Password xxxxxxxx //环境中SSO用户名和密码

使用命令删除用于vsan流量的vmkernel适配器

Get-VMHostNetworkAdapter -VMHost esxi-b1.lab.com -PortGroup vsan | Remove-VMHostNetworkAdapter //环境中分布式端口组名字为vsan

第二步:从分布式交换机vds中移除主机。

Remove-VDSwitchVMHost -VDSwitch vds -VMHost esxi-b1.lab.com //环境中分布式交换机名字为vds

第三步:从vCenter中移除主机。

Get-VMHost -Name esxi-b1.lab.com | Remove-VMHost

此时esxi-b1主机已从vSAN集群及vCenter中移除。

按上述方式完成esxi-b2主机到esxi-b4主机的删除过程。本小节完。

二、在vcsa站点上新建集群vsan02,并将esxi-b1到esxi-b4主机加入该集群

1.远程SSH连接到vcsa的vCenter,并登录RVC命令行控制台。

rvc localhost  //运行后输入vCenter的sso用户名和密码

2.使用RVC命令cluster.create创建集群vsan02。

cluster.create /localhost/datacenter/computers/vsan02

3.使用RVC命令cluster.add_host将esxi-b1到esxi-b4主机添加到集群vsan02中,主机默认进入维护模式。

cluster.add_host /localhost/datacenter/computers/vsan02/ esxi-b1.lab.com -u root -p xxxxxxxx //ESXi用户名和密码
cluster.add_host /localhost/datacenter/computers/vsan02/ esxi-b2.lab.com -u root -p xxxxxxxx
cluster.add_host /localhost/datacenter/computers/vsan02/ esxi-b3.lab.com -u root -p xxxxxxxx
cluster.add_host /localhost/datacenter/computers/vsan02/ esxi-b4.lab.com -u root -p xxxxxxxx

4.查看所有主机都已经加入集群vsan02成功。本小节完。

ls /localhost/datacenter/computers/vsan02/

三、将esxi-b1到esxi-b4主机的ESXi版本升级到vcsa站点上的主机版本(ESXi 7.0 U3)

ESXi主机升级的方式有多种,比如只有几台或少数主机的情况下,使用esxcli software命令一台一台的升级,如果主机较多的话推荐使用VUM基于基准的方式进行升级,当然仅在6.x及7.x版本中可用,在8.x以后的版本中仅支持vLCM基于单个映像的方式进行主机升级。本环境只有四台主机,所以就采用esxcli software命令进行升级。

VMware ESXi产品升级路径:

1.将升级包上传至esxi-b1主机的本地存储。

2.查看补丁的映像配置文件。

esxcli software sources profile list --depot=/vmfs/volumes/65a744d5-442a4976-59cb-00505687d430/VMware-ESXi-7.0U3o-22348816-depot.zip

3.安装更新ESXi7 U3o。由于环境中的硬件CPU不受支持,所以在命令中加了-f强制更新,--no-hardware-warning忽略硬件警告。(生产环境No!!!)

esxcli software profile update --profile=ESXi-7.0U3o-22348816-standard --depot=/vmfs/volumes/65a744d5-442a4976-59cb-00505687d430/VMware-ESXi-7.0U3o-22348816-depot.zip -f --no-hardware-warning

4.重启主机后查看版本,已更新到VMware ESXi 7.0 U3。

5.按上述方式完成esxi-b2~esxi-b4主机的更新操作。本小节完。

四、在vsan02集群中启用vsan,并申明esxi-b1到esxi-b4主机的vSAN磁盘

1.SSH连接到vCenter,进入RVC交互式管理控制台。

rvc localhost

2.使用vds.create_vds命令创建一个新的分布式交换机vds2。

vds.create_vds localhost/datacenter/networks/vds2

3.使用vds.add_hosts命令将主机加入到vds2分布式交换机,并指定uplink端口为vmnic1。

vds.add_hosts --vmnic=vmnic1 /localhost/datacenter/networks/vds2/ /localhost/datacenter/computers/vsan02/hosts/esxi-b1.lab.com/
vds.add_hosts --vmnic=vmnic1 /localhost/datacenter/networks/vds2/ /localhost/datacenter/computers/vsan02/hosts/esxi-b2.lab.com/
vds.add_hosts --vmnic=vmnic1 /localhost/datacenter/networks/vds2/ /localhost/datacenter/computers/vsan02/hosts/esxi-b3.lab.com/
vds.add_hosts --vmnic=vmnic1 /localhost/datacenter/networks/vds2/ /localhost/datacenter/computers/vsan02/hosts/esxi-b4.lab.com/


4.使用vds.create_portgroup命令创建分布式端口组vsan-pg。

vds.create_portgroup /localhost/datacenter/networks/vds2/ vsan-pg

5.使用vds.create_vmknic命令在分布式端口组vsan-pg中给每台主机创建一个vmkernel端口vmk1,用于主机的vSAN流量端口。

vds.create_vmknic /localhost/datacenter/networks/vds2/portgroups/vsan-pg/ /localhost/datacenter/computers/vsan02/hosts/esxi-b1.lab.com/
vds.create_vmknic /localhost/datacenter/networks/vds2/portgroups/vsan-pg/ /localhost/datacenter/computers/vsan02/hosts/esxi-b2.lab.com/
vds.create_vmknic /localhost/datacenter/networks/vds2/portgroups/vsan-pg/ /localhost/datacenter/computers/vsan02/hosts/esxi-b3.lab.com/
vds.create_vmknic /localhost/datacenter/networks/vds2/portgroups/vsan-pg/ /localhost/datacenter/computers/vsan02/hosts/esxi-b4.lab.com/

6.为了方面,给每台主机对象分别打一个标记。

mark esxib1 /localhost/datacenter/computers/vsan02/hosts/esxi-b1.lab.com/
mark esxib2 /localhost/datacenter/computers/vsan02/hosts/esxi-b2.lab.com/
mark esxib3 /localhost/datacenter/computers/vsan02/hosts/esxi-b3.lab.com/
mark esxib4 /localhost/datacenter/computers/vsan02/hosts/esxi-b4.lab.com/

7.使用rvc esxcli命令查看主机esxib1的接口地址信息。

esxcli ~esxib1 network ip interface ipv4 get

8.使用rvc esxcli命令给所有主机的vmkernel网卡vmk1配置一个静态地址,并查看是否已配置成功。

esxcli ~esxib1 network ip interface ipv4 set -i vmk1 -t static -p 11.1.1.1 -n 255.255.255.0
esxcli ~esxib2 network ip interface ipv4 set -i vmk1 -t static -p 11.1.1.2 -n 255.255.255.0
esxcli ~esxib3 network ip interface ipv4 set -i vmk1 -t static -p 11.1.1.3 -n 255.255.255.0
esxcli ~esxib4 network ip interface ipv4 set -i vmk1 -t static -p 11.1.1.4 -n 255.255.255.0
esxcli ~esxib1 network ip interface ipv4 get
esxcli ~esxib2 network ip interface ipv4 get
esxcli ~esxib3 network ip interface ipv4 get
esxcli ~esxib4 network ip interface ipv4 get


9.使用rvc esxcli命令将所有主机的vmkernel端口vmk1配置为vSAN流量。

esxcli ~esxib1 vsan network ip add -i vmk1
esxcli ~esxib2 vsan network ip add -i vmk1
esxcli ~esxib3 vsan network ip add -i vmk1
esxcli ~esxib4 vsan network ip add -i vmk1


10.使用rvc esxcli命令查看所有主机的vSAN网络。

esxcli ~esxib1 vsan network list
esxcli ~esxib2 vsan network list
esxcli ~esxib3 vsan network list
esxcli ~esxib4 vsan network list


11.使用vsan.enable_vsan_on_cluster命令开启集群vsan02的vSAN功能,并关闭磁盘自动申明。

vsan.enable_vsan_on_cluster /localhost/datacenter/computers/vsan02/ --disable-storage-auto-claim

12.SSH登录到主机esxi-b1上,使用vdq命令查看本机的所有磁盘。准备将T1~T6磁盘用于vSAN磁盘组,T1、T3、T4为一个磁盘组,T2、T5、T6为一个磁盘组,共两个磁盘组。

vdq -q | grep "Name\|IsSSD\|Size"

13.由于主机上全是SSD磁盘,所以在创建全闪磁盘组之前,需要先将用于容量盘的磁盘标记为容量闪存盘。

esxcli vsan storage tag add -d mpx.vmhba0:C0:T3:L0 -t capacityFlash
esxcli vsan storage tag add -d mpx.vmhba0:C0:T4:L0 -t capacityFlash
esxcli vsan storage tag add -d mpx.vmhba0:C0:T5:L0 -t capacityFlash
esxcli vsan storage tag add -d mpx.vmhba0:C0:T6:L0 -t capacityFlash

14.在主机上创建两个磁盘组,T1和T2作为闪存盘,T3~T6作为容量闪存盘。

esxcli vsan storage add -s mpx.vmhba0:C0:T1:L0 -d mpx.vmhba0:C0:T3:L0 -d mpx.vmhba0:C0:T4:L0
esxcli vsan storage add -s mpx.vmhba0:C0:T2:L0 -d mpx.vmhba0:C0:T5:L0 -d mpx.vmhba0:C0:T6:L0

15.主机上查看刚刚已创建好的所有vSAN存储设备。

esxcli vsan storage list | grep -A 5 "Device"

使用上述方式,完成esxi-b2~esxi-b4主机vSAN磁盘组的创建。

16.主机上查看vSAN集群状态。

esxcli vsan cluster list

17.主机上查看vSAN集群中其他主机的单播代理状态。

esxcli vsan cluster unicastagent list

18.主机上查看vSAN集群健康状态。

esxcli vsan health cluster

19.在vCenter上使用RVC命令host.exit_maintenance_mode将所有主机退出维护模式。

host.exit_maintenance_mode ~esxib1
host.exit_maintenance_mode ~esxib2
host.exit_maintenance_mode ~esxib3
host.exit_maintenance_mode ~esxib4

20.在vCenter上使用RVC命令vsan.cluster_info查看vSAN集群vsan02的状态。本小节完。

> vsan.cluster_info localhost/datacenter/computers/vsan02
2024-01-26 11:54:11 +0800: Fetching host info from esxi-b3.lab.com (may take a moment) ...
2024-01-26 11:54:11 +0800: Fetching host info from esxi-b1.lab.com (may take a moment) ...
2024-01-26 11:54:11 +0800: Fetching host info from esxi-b2.lab.com (may take a moment) ...
2024-01-26 11:54:11 +0800: Fetching host info from esxi-b4.lab.com (may take a moment) ...
Host: esxi-b1.lab.com
  Product: VMware ESXi 7.0.3 build-22348816
  vSAN enabled: yes
  Cluster info:
    Cluster role: agent
    Cluster UUID: 52783a01-cbdd-3691-ae7d-e1460dcb601e
    Node UUID: 65a743cf-35f2-1ea3-5b85-00505687d430
    Member UUIDs: ["65a743f9-1aad-f462-d1a8-005056875b25", "65a74405-7719-a1c0-5677-00505687cc0d", "65a743cf-35f2-1ea3-5b85-00505687d430", "65a74408-270e-ee81-2892-00505687df8b"] (4)
  Node evacuated: no
  Storage info:
    Auto claim: no
    Disk Mappings:
      Cache Tier: Local VMware Disk (mpx.vmhba0:C0:T2:L0) - 10 GB, v15
      Capacity Tier: Local VMware Disk (mpx.vmhba0:C0:T6:L0) - 20 GB, v15
      Capacity Tier: Local VMware Disk (mpx.vmhba0:C0:T5:L0) - 20 GB, v15
      Cache Tier: Local VMware Disk (mpx.vmhba0:C0:T1:L0) - 10 GB, v15
      Capacity Tier: Local VMware Disk (mpx.vmhba0:C0:T4:L0) - 20 GB, v15
      Capacity Tier: Local VMware Disk (mpx.vmhba0:C0:T3:L0) - 20 GB, v15
  FaultDomainInfo:
    Not configured
  NetworkInfo:
    Adapter: vmk1 (11.1.1.1)
  Data efficiency enabled: no
  Encryption enabled: no

Host: esxi-b2.lab.com
  Product: VMware ESXi 7.0.3 build-22348816
  vSAN enabled: yes
  Cluster info:
    Cluster role: master
    Cluster UUID: 52783a01-cbdd-3691-ae7d-e1460dcb601e
    Node UUID: 65a743f9-1aad-f462-d1a8-005056875b25
    Member UUIDs: ["65a743f9-1aad-f462-d1a8-005056875b25", "65a74405-7719-a1c0-5677-00505687cc0d", "65a743cf-35f2-1ea3-5b85-00505687d430", "65a74408-270e-ee81-2892-00505687df8b"] (4)
  Node evacuated: no
  Storage info:
    Auto claim: no
    Disk Mappings:
      Cache Tier: Local VMware Disk (mpx.vmhba0:C0:T2:L0) - 10 GB, v15
      Capacity Tier: Local VMware Disk (mpx.vmhba0:C0:T6:L0) - 20 GB, v15
      Capacity Tier: Local VMware Disk (mpx.vmhba0:C0:T5:L0) - 20 GB, v15
      Cache Tier: Local VMware Disk (mpx.vmhba0:C0:T1:L0) - 10 GB, v15
      Capacity Tier: Local VMware Disk (mpx.vmhba0:C0:T4:L0) - 20 GB, v15
      Capacity Tier: Local VMware Disk (mpx.vmhba0:C0:T3:L0) - 20 GB, v15
  FaultDomainInfo:
    Not configured
  NetworkInfo:
    Adapter: vmk1 (11.1.1.2)
  Data efficiency enabled: no
  Encryption enabled: no

Host: esxi-b3.lab.com
  Product: VMware ESXi 7.0.3 build-22348816
  vSAN enabled: yes
  Cluster info:
    Cluster role: agent
    Cluster UUID: 52783a01-cbdd-3691-ae7d-e1460dcb601e
    Node UUID: 65a74408-270e-ee81-2892-00505687df8b
    Member UUIDs: ["65a743f9-1aad-f462-d1a8-005056875b25", "65a74405-7719-a1c0-5677-00505687cc0d", "65a743cf-35f2-1ea3-5b85-00505687d430", "65a74408-270e-ee81-2892-00505687df8b"] (4)
  Node evacuated: no
  Storage info:
    Auto claim: no
    Disk Mappings:
      Cache Tier: Local VMware Disk (mpx.vmhba0:C0:T2:L0) - 10 GB, v15
      Capacity Tier: Local VMware Disk (mpx.vmhba0:C0:T6:L0) - 20 GB, v15
      Capacity Tier: Local VMware Disk (mpx.vmhba0:C0:T5:L0) - 20 GB, v15
      Cache Tier: Local VMware Disk (mpx.vmhba0:C0:T1:L0) - 10 GB, v15
      Capacity Tier: Local VMware Disk (mpx.vmhba0:C0:T4:L0) - 20 GB, v15
      Capacity Tier: Local VMware Disk (mpx.vmhba0:C0:T3:L0) - 20 GB, v15
  FaultDomainInfo:
    Not configured
  NetworkInfo:
    Adapter: vmk1 (11.1.1.3)
  Data efficiency enabled: no
  Encryption enabled: no

Host: esxi-b4.lab.com
  Product: VMware ESXi 7.0.3 build-22348816
  vSAN enabled: yes
  Cluster info:
    Cluster role: backup
    Cluster UUID: 52783a01-cbdd-3691-ae7d-e1460dcb601e
    Node UUID: 65a74405-7719-a1c0-5677-00505687cc0d
    Member UUIDs: ["65a743f9-1aad-f462-d1a8-005056875b25", "65a74405-7719-a1c0-5677-00505687cc0d", "65a743cf-35f2-1ea3-5b85-00505687d430", "65a74408-270e-ee81-2892-00505687df8b"] (4)
  Node evacuated: no
  Storage info:
    Auto claim: no
    Disk Mappings:
      Cache Tier: Local VMware Disk (mpx.vmhba0:C0:T2:L0) - 10 GB, v15
      Capacity Tier: Local VMware Disk (mpx.vmhba0:C0:T6:L0) - 20 GB, v15
      Capacity Tier: Local VMware Disk (mpx.vmhba0:C0:T5:L0) - 20 GB, v15
      Cache Tier: Local VMware Disk (mpx.vmhba0:C0:T1:L0) - 10 GB, v15
      Capacity Tier: Local VMware Disk (mpx.vmhba0:C0:T4:L0) - 20 GB, v15
      Capacity Tier: Local VMware Disk (mpx.vmhba0:C0:T3:L0) - 20 GB, v15
  FaultDomainInfo:
    Not configured
  NetworkInfo:
    Adapter: vmk1 (11.1.1.4)
  Data efficiency enabled: no
  Encryption enabled: no


No Fault Domains configured in this cluster
> 

后续

主机升级到VMware ESXi 7.0以后,License Key将不再可用,在主机上使用vim-cmd命令配置主机的序列号。

vim-cmd vimsvc/license --set xxxxx-xxxxx-xxxxx-xxxxx-xxxxx(License Key)

新的vSAN集群也需要vSAN License Key,在vCenter上使用RVC命令vsan.apply_license_to_cluster配置vSAN集群的序列号。

vsan.apply_license_to_cluster /localhost/datacenter/computers/vsan02/ -k xxxxx-xxxxx-xxxxx-xxxxx-xxxxx(License Key)

开启vSAN集群vsan02的DRS和HA功能。配置DRS模式为半自动,配置HA时需要注意主机网卡上已启用vMotion。

cluster.configure_drs /localhost/datacenter/computers/vsan02/ -m partiallyAutomated
cluster.configure_ha /localhost/datacenter/computers/vsan02/

先看效果图,这是一个比较典型的逻辑推理问题,以下是本地运行的模型和openai gpt3.5的推理对比

本地运行Mixtral-8x7B大模型:

chatgpt3.5的回答:

关于Mixtral 8x7B这个就不多介绍了,但凡在关注开源大模型的朋友应该知道这个模型是目前市场上最具竞争力的开源大型模型之一,其能力水平在多项任务中都有可以和gpt3.5打的有来有回,今天主要聊聊使用c#如何本地化部署实践

整个模型部署其实相对已经比较简单了,其核心是采用了llama.cpp这个项目,这个项目是用 ggml(一个用 c++ 开发的一个机器学习的张量库)写的一个推理 LLaMA 的 demo,随着项目持续火爆吸引了大量没有昂贵GPU 的玩家,成为了在消费级硬件上玩大模型的首选。而今天我们要用到的项目就是依赖llama.cpp的c#封装实现的nuget包LLamaSharp,地址(https://github.com/SciSharp/LLamaSharp)。基于它对llama.cpp的c#封装从而完成本机纯c#部署大模型的实现。通过LLamaSharp既可以使用cpu进行推理,如果你有30系以上的N卡,也可以使用它的LLamaSharp.Backend.Cuda11或者Cuda12进行推理,当然其效果肯定相比纯CPU推理速度更快。

整个项目我已经放到github上了,有兴趣的同学可以自取:https://github.com/sd797994/LocalChatForLlama

另外关于模型格式说一下,当前使用llama.cpp主要通过gguf格式加载,这是一种专门面向llama.cpp的格式,可以通过huggingface搜索:https://huggingface.co/models?search=gguf。而我目前使用的是Nous-Hermes基于Mixtral-8x7B微调的版本,它的repo如下:https://huggingface.co/TheBloke/Nous-Hermes-2-Mixtral-8x7B-DPO-GGUF/tree/main 据说Nous-Hermes微调版本的性能略好于Mixtral-8x7B。唯一需要注意的是如果不是使用我推荐的模型,index.cshtml这里需要根据模型的实际输出硬编码成对应的字段:

最后的Tips:由于模型确实比较大,在纯CPU模式下如果内存不太够(一般16G)的情况下推理很缓慢,一分钟可能也就能输出几个字。建议上较大内存的纯CPU推理或者使用NVIDIA的显卡安装对应的CUDA环境后基于CUDA推理,整个效果会快很多。

作者:张富春(ahfuzhang),转载时请注明作者和引用链接,谢谢!


1.背景

在可观测领域的 metrics 解决方案中,VictoriaMetrics 整个产品体系的性能非常高。两年前,我们团队用这个组件来代替 kafka+druid 实现的旧版监控系统。其中一个群集扛住了鹅厂内部一个达到 9000 万/s datapoint 的业务所产生的 metrics 数据,vm 部分用了不到 1000 核,成本相比 kafka+druid 低了很多。
美中不足是 VictoriaMetrics 并未提供类似历史群集的解决方案,虽然提供了免费的 vmbackup 和 vmrestore 工具,但是数据的备份和恢复与历史群集相比仍然是不够的。

历史群集的难题是这样的:

  1. 与历史群集相对的实时群集,默认情况下实时群集仅保存最近 30 天的数据;如果需要读取 30 一天,以及更长周期的数据,就需要把备份数据恢复出来形成历史群集;

  2. 实时群集一般会部署多个 sharding 节点:


    • 如果 vm-insert 上的
      -replicationFactor=1
      ,则数据不重复,每个 sharding 上的数据完全重复,每个节点是全量数据的 n 分之一。
    • 如果 vm-insert 上的
      -replicationFactor=2
      , 则各个 sharding 上的数据有一部分与其他 sharding 节点是重复的。
  3. vm-backup 的备份功能是针对 vm-storage 的 sharding 节点的,如果 vm-storage 群集有 n 个节点,则每个节点都需要独立使用 vm-backup 来备份。


    • vm-backup 的原理是先使用 http 协议访问 vm-storage 的 http 服务,访问
      /snapshot/create
      来创建一个所有磁盘上数据文件的快照。
    • 数据文件的快照是文件系统的 hardlink,意味着只是在 vfs 上增加对数据文件的引用计数,不会产生拷贝。这一步非常快。
    • vm-backup 将数据文件备份到 s3 中;
    • 备份完成后,调用 vm-storage http 中的
      /snapshot/delete?snapshot=<id>
      来删除快照
  4. 备份完成后:


    • s3 上每个 sharding 对应着一个数据的文件夹

    • 每个数据文件夹的数据包含从创建快照开始,倒数 30 天的数据;(如果默认的数据存储周期是 30 天的话)

    • 如果需要每天都全新备份,则还需要产生以日期命名的文件夹

    • 下面是一个 s3 上备份目录的例子:

          s3://bucket_name:
              metrics_data/
                 daily/
                    2024-01-26/
                       sharding-0/
                       sharding-1/
                       sharding-2/
      
      
  5. 如果需要直接根据历史的备份数据来启动历史群集,则:


    • 有多少个 sharding, 就要启动多少个 vm-storage 实例;
    • 假设从昨天的备份数据开始启动 vm-storage,则这一组 vm-storage 支持 -1 天到 -31 天的历史数据查询;
    • 当过去了 24 小时,之前的 vm-storage 群集支持的数据变成了 -2 天到 -32 天。如果需要始终从-1 天开始,则需要在每天半夜重新下载最新的备份,然后重启 vm-storage;
    • 如果需要支持更长周期,例如 -31 天到 -61 天,则需要从 -31 天开始的备份数据里,再启动一组 vm-storage 群集。假设需要支持过去 300天的历史数据查询,则需要部署十组 vm-storage.

通过以上的描述,应该可以了解到通过目前的工具,支持历史群集很麻烦。
实际上,我已经通过已有的工具做出来了对应的部署脚本,请看:
https://github.com/ahfuzhang/deploy_VictoriaMetrics_cluster/tree/main/terraform/historical_cluster

2.vmfile

从官方的文档(
https://docs.victoriametrics.com/vmbackupmanager.html
)看,vmbackupmanager 对备份数据有更强大的功能,不过也仍然没有提如何部署历史群集。
收费购买企业版套件,有可能让部署历史群集变得简单。

如果有一个工具,能够从多个 sharding 中导出 metrics 数据,然后再导入到一个包含所有数据的文件夹中,那么部署历史群集就会非常简单了。
因此,我花了两周时间写出了这个可能节约 2 万美元的小工具:vmfile,对离线的 vm-storage 的数据文件进行处理。

vmfile 的相关链接是:

下面介绍一下这个工具的几个基本功能:

2.1 count_index 统计数据文件的索引信息

  • 通过
    -storageDataPath
    来指向某个 vm-storage 的数据(或者通过 vm-restore 恢复的)文件夹。
vmfile \
	  -action=count_index \
	  -storageDataPath=/Users/fuchunzhang/xxx/data/realtime-cluster/sharding-0/ \
	  -fs.disableMmap=false

2.2 count_data 统计数据文件的数据信息

vmfile \
	  -action=count_data \
	  -storageDataPath=/Users/fuchunzhang/xxx/data/realtime-cluster/sharding-0/ \
	  -fs.disableMmap=false

2.3 export_metrics 导出 metrics 数据到文本文件

  • 通过
    output
    来指定导出的位置
vmfile \
	  -action=export_metrics \
	  -storageDataPath=/Users/fuchunzhang/xxx/data/realtime-cluster/sharding-0/ \
      -output=./metrics.txt \
	  -fs.disableMmap=false

(后续会优化这个功能,例如:可以把所有 metrics 数据的 name, value, timestamp 都导出,然后可以再导入到 clickhouse 做复杂分析)

2.4 simple_merge 简单合并多个 sharding 数据

把多个 sharding 文件夹的数据,把索引和数据两部分分别拷贝到目的文件夹,并最终成为一个可以正常启动 vm-storage 的数据文件夹。

  • simple_merge_from
    : 多个逗号分隔的路径
  • simple_merge_to
    : merge 后的目的文件夹
vmfile \
	  -action=simple_merge \
	  -simple_merge_from=/Users/fuchunzhang/xxx/data/2024-01-02/sharding-0/,/Users/fuchunzhang/xxx/data/2024-01-02/sharding-1/,/Users/fuchunzhang/xxx/data/2024-01-02/sharding-2/ \
	  -simple_merge_to=/Users/fuchunzhang/Documents/temp/2024/simple_merge

2.5 merge 重建索引和数据方式的merge

  • mergev2_from
    : 多个逗号分隔的路径
  • mergev2_to
    : merge 后的目的文件夹
vmfile \
	  -action=merge_v2 \
	  -mergev2_from=/Users/fuchunzhang/xxx/sharding-0/,/Users/fuchunzhang/xxx/sharding-1/,/Users/fuchunzhang/xxx/sharding-2/ \
	  -mergev2_to=/Users/fuchunzhang/Documents/temp/2024/2024-01-22/merge_v2/ \
	  -dedup.minScrapeInterval=0s \
	  -cpu=1

2.5.1 降采样(downsample)功能


-dedup.minScrapeInterval=0s
的时候,数据部分不会丢失任何数据。
可以使用这个参数指定一个时间窗口,在时间窗口内,同一个 metric 仅保留一个 data point。

下面的例子,每 5 分钟仅保留一个 data point,数据部分的体积会缩小很多:

vmfile \
	  -action=merge_v2 \
	  -mergev2_from=/Users/fuchunzhang/xxx/sharding-0/,/Users/fuchunzhang/xxx/sharding-1/,/Users/fuchunzhang/xxx/sharding-2/ \
	  -mergev2_to=/Users/fuchunzhang/Documents/temp/2024/2024-01-22/merge_v2/ \
	  -dedup.minScrapeInterval=5m \
	  -cpu=1

使用降采样方式的 merge,我的测试数据如下:

  • 降采样时间窗口 0s: 数据部分膨胀 1.1 倍,merge 后记录数不变
  • 降采样时间窗口 1m: 数据部分膨胀 1.005 倍, merge 后记录数减少 0.004%
  • 降采样时间窗口 5m: 数据部分减少 2.919 倍, merge 后记录数减少 79.97%

2.5.2 索引和数据并行merge


-cpu=
参数大于 1 时,索引部分和数据部分会并行 merge。目前最多只支持两个核。

vmfile \
	  -action=merge_v2 \
	  -mergev2_from=/Users/fuchunzhang/xxx/sharding-0/,/Users/fuchunzhang/xxx/sharding-1/,/Users/fuchunzhang/xxx/sharding-2/ \
	  -mergev2_to=/Users/fuchunzhang/Documents/temp/2024/2024-01-22/merge_v2/ \
	  -dedup.minScrapeInterval=5m \
	  -cpu=2

3.历史群集部署

有了 vmfile 这个工具后,部署历史群集就简单很多了。
每天只需要把最新的备份数据不断 merge 到一个最终的数据文件就行了。(当然,这里仍然是离线操作的)

每天自动拉取最新的各个 sharding,并开始 merge,然后停掉旧的 vm-storage,启动新的 vm-storage……这一系列的动作我都通过一个 shell 脚本来实现了。

我通过 terraform 来实现了 k8s 上的部署代码,通过阅读源码,修改成别的部署方式也很容易。
历史群集的部署流程如下:

  1. 实时群集需要创建 crontab,每天半夜使用 vmbackup 把各个 sharding 部署到 s3 上;


  2. 构造一个新的 docker 镜像:


  3. 创建一个 k8s 上的 deployment,启动命令是上面提到的 shell

        container {
          name              = "${local.vm-storage-name-with-merge}-${count.index}"
          image             = "ahfuzhang/vm-historical:v1.95.1-vmfile"  # 包含了多种工具和 shell 的镜像
          image_pull_policy = "Always" #"IfNotPresent"
          command           = ["/bin/sh"]
          args = [
            "/daily_with_vmfile.sh",  # 从镜像中的 shell 脚本启动
          ]
          ......

部署的代码请看:
https://github.com/ahfuzhang/deploy_VictoriaMetrics_cluster/blob/main/terraform/historical_cluster/vm-storage-with-merge.tf

shell 的控制参数通过容器的环境变量来传入,重要的参数有这些:

  • storage_base_path: 本地的存储路径
  • s3_storage_base_path: s3 上的备份文件的路径
  • sharding_count: sharding 的个数
  • n_days_before : 历史群集从几天以前开始,填 1 就是昨天。历史群集会在第二天半夜拉取最新备份,始终维持在当前时间的 n 天以前。
  1. shell 的执行逻辑:


    • 检查是否已经存在 merge 后的文件夹,如果不存在,就使用 vmrestore 来下载多个 sharding
    • sharding 文件夹下载好后,使用 vmfile 的 merge 功能来重建索引和数据。
    • 在 merge 好后的文件夹上启动 vm-storage 来提供历史数据的查询服务;
    • 脚本进入循环睡眠,等到第二天半夜。
    • 根据当前日期,下载 n_days_before 指定的历史文件夹到本地
    • 使用 vmfile merge 数据
    • 启动新的 vm-storage
    • 停掉旧的 vm-storage

    具体代码请看:
    https://github.com/ahfuzhang/deploy_VictoriaMetrics_cluster/blob/main/terraform/historical_cluster/docker_image/daily_with_vmfile.sh

The End

  • vmfile 的功能还会继续优化,如果你们的团队也在使用 VictoriaMetrics ,欢迎关注我的后续进展;
  • vmfile 的实现代码很简单,但是原理却相对复杂,设计很多存储引擎的结构设计。如果对 vm-storage 的 tsdb 感兴趣,欢迎查看我分享的源码分析电子书:
    《VictoriaMetrics存储引擎分析.pdf》


Have fun. 希望对你有用。

本篇是mygin的第六篇,参照gin框架,感兴趣的可以从
Mygin第一篇
开始看,Mygin从零开始完全手写,在实现的同时,带你一窥gin框架的核心原理实现。

目的

  • 实现中间件Middleware
    在上一篇
    Mygin实现分组路由Group
    中,实现了路由分组,且同一分组的执行,会先执行Group,有一点点中间件的雏形了。但是中间件不完全还应该提供中断功能,比如一个Group组中添加了auth鉴权中间件,只有auth认证通过才可以通过,因此需要对上篇中的内容进行一些修改。
    在实现之前,先分析gin中是怎样去实现的这一功能的
func (c *Context) Next() {
	c.index++
    //遍历handlers
	for c.index < int8(len(c.handlers)) {
        //真正调用执行handler方法
		c.handlers[c.index](c)
		c.index++
	}
}

这个时候就有疑问了,从上述方法中看不到中间件执行失败的中断方法,那又是怎么实现中断。
在揭晓答案之前,先看看int8(len(c.handlers) 为什么要写个int8,原因在于gin中规定的handlers最多63个,相信实际的应用请求中,没有超过63个那么多变态的执行链。在gin中如果某一中间件执行失败,就把c.index赋值为63,上述for循环就不满足条件,因此就跳出for循环,不再继续执行后面的代码。gin中对应的代码也很简单。

const abortIndex int8 = math.MaxInt8 >> 1
//中间件执行失败,中断方法
func (c *Context) Abort() {
	c.index = abortIndex
}

因此只需在mygin/content.go中新加Next方法和Abort方法

上下文

content.go中的代码不多,索性加上注释全部贴出来。

  • mygin/content.go
package mygin

import (
	"encoding/json"
	"math"
	"net/http"
)

// 定义 表示最大和上下文应中止时的索引值
const abortIndex int8 = math.MaxInt8 >> 1

// Context 封装了一个HTTP请求的上下文
type Context struct {
	Request *http.Request
	Writer  http.ResponseWriter
	Params  Params
	index   int8
}

// Next 执行链中的剩余处理程序。
func (c *Context) Next(handlers HandlersChain) {
	//遍历handlers
	for c.index < int8(len(handlers)) {
		//真正调用执行handler方法
		handlers[c.index](c)
		c.index++
	}
}

// Abort 中断链中剩余处理程序的执行。
func (c *Context) Abort() {
	c.index = abortIndex
}

// IsAborted 如果当前上下文被中止,则返回true。
func (c *Context) IsAborted() bool {
	return c.index >= abortIndex
}

// writeContentType 如果尚未设置,则设置Content-Type标头。
func writeContentType(w http.ResponseWriter, value []string) {
	header := w.Header()
	if val := header["Content-Type"]; len(val) == 0 {
		header["Content-Type"] = value
	}

}

// Status 设置HTTP响应状态码。
func (c *Context) Status(code int) {
	c.Writer.WriteHeader(code)
}

// JSON 将值序列化为JSON并将其写入响应。
func (c *Context) JSON(v interface{}) error {
	writeContentType(c.Writer, []string{"application/json; charset=utf-8"})
	encoder := json.NewEncoder(c.Writer)
	err := encoder.Encode(v)
	if err != nil {
		c.Status(http.StatusInternalServerError)
	}
	c.Status(http.StatusOK)
	return err
}

// Html 将字符串以HTML形式写入响应。
func (c *Context) Html(v string) error {
	writeContentType(c.Writer, []string{"text/html; charset=utf-8"})
	c.Status(http.StatusOK)
	_, err := c.Writer.Write([]byte(v))
	return err
}

// String 将字符串写入响应
func (c *Context) String(v string) error {
	writeContentType(c.Writer, []string{"text/plain; charset=utf-8"})
	c.Status(http.StatusOK)
	_, err := c.Writer.Write([]byte(v))
	return err
}

接下来就是调用handles的修改了,原来的解决方法是直接循环调用,对应的代码如下:

for _, handler := range handlers {
		handler(&Context{
			Request: r,
			Writer:  w,
			Params:  params,
		})
	}

引擎

  • mygin/engine.go
    现在找到engine.go文件中将上面的代码替换为:
	//实例化一个下上文
	c := &Context{
		Request: r,
		Writer:  w,
		Params:  params,
	}
	// 执行处理函数链
	c.Next(handlers)

测试代码

package main

import (
	"gophp/mygin"
	"path"
)

func main() {
	// 创建一个默认的 mygin 实例
	r := mygin.Default()

	//测试Abort
	group := r.Group("/api", func(context *mygin.Context) {
		//todo....
		context.String("api Group 中间件失败了....\n")
		context.Abort()
	})
	//这个回调不会执行
	group.GET("/hello/:name", func(context *mygin.Context) {
		name := context.Params.ByName("name")
		context.String(path.Join("hello ", name, "!"))
	})

	//测试没有发生Abort
	group2 := r.Group("/api2", func(context *mygin.Context) {
		//todo....
		context.String("api Group 中间件成功了....\n")
	})
	
	//这个回调会执行
	group2.GET("/hello2/:name", func(context *mygin.Context) {
		name := context.Params.ByName("name")
		context.String(path.Join("hello2 ", name, "!\n"))
	})

	// 启动服务器并监听端口
	r.Run(":8088")
}

curl测试

 curl http://127.0.0.1:8088/api/hello/scott
api Group 中间件失败了....
~ curl http://127.0.0.1:8088/api2/hello2/scott
api Group 中间件成功了....
hello2 /scott/!

看到上诉输出,即为成功。