2024年9月

最近AI搜索很火爆,有Perplexity、秘塔AI、MindSearch、Perplexica、memfree、khoj等等。

在使用大语言模型的过程中,或许你也遇到了这种局限,就是无法获取网上最新的信息,导致回答的内容不是基于最新的信息,为了解决这个问题,可以通过LLM+搜索引擎的方式实现。

以我之前开源的一个简单项目为例,如果直接问一般的大语言模型是不知道的,如下所示:

image-20240920103257679

对比可以联网的回答:

Perplexity

image-20240920103503743

khoj

image-20240920103739835

Kimi

image-20240920103933071

那么我们如何自己实现类似的效果呢?

先来看看自己实现的效果:

image-20240920104451845

源码GitHub地址:
https://github.com/Ming-jiayou/SimpleAISearch

如果对此感兴趣的话,就可以继续往下阅读。

实现思路

本质上就是LLM+搜索引擎。

首先需要能够实现函数调用功能,在之前的文章中已经有所说明。主要介绍一下实现思路,源码已经开源,感兴趣的话可以自己去看下具体代码。

首先在插件中添加调用搜索引擎的代码,我这里搜索引擎选用的是DuckDuckGo。

开始执行时,LLM会判断需要调用这个函数,并且参数是问题:

image-20240920105218166

这个函数如下所示:

image-20240920105254572

搜索引擎会找到相关内容:

image-20240920105409114

让LLM根据获取到的这些信息给出回答:

image-20240920105518735

目前是经过总结之后显示在界面上,也可以修改为不经过总结的。

以上就是实现的一个简单思路。

快速体验

通过源码构建

和之前的LLM项目一样,只需appsettings.example.json修改为appsettings.json选择你使用的平台并填入API Key即可。

直接体验

我已经在github上发布了两个版本一个依赖框架,一个不依赖框架:

image-20240920113656942

下载解压之后,在appsettings中填入你的api key即可使用。

Parquet.Net 是一个用于读取和写入 Apache Parquet 文件的纯 .NET 库,使用MIT协议开源,github仓库:
https://github.com/aloneguid/parquet-dotnet
。Apache Parquet 是一种面向大数据的列式存储格式。Parquet.Net 支持 .NET 4.5 及以上版本和 .NET Standard 1.4 及以上版本,这意味着它也隐式支持所有版本的 .NET Core。这个库可以在所有 Windows、Linux、macOSX 版本上运行,也可以通过 Maui 在移动设备(iOS、Android)和游戏机等支持 .NET Standard 的平台上运行。

image

Parquet.Net 的一个重要特点是它对 Apache Parquet 文件的支持,这使得 .NET 平台在大数据应用中更加完整。由于 Parquet 库主要适用于 Java、C 和 Python,这限制了 .NET/C# 平台在大数据领域的应用。Parquet.Net 的出现填补了这一空白,为 .NET 开发者提供了一个处理 Parquet 文件的强大工具。可以无缝集成到 .NET 生态系统中,帮助开发者高效地处理和存储数据。

Parquet.Net 提供了低级 API 和高级 API,允许用户根据需要进行灵活的操作。此外,它还提供了基于行的 API,使得处理复杂的数据结构更加直观和方便。Parquet.Net 支持动态模式,并且能够自动将 C# 类序列化为 Parquet 文件,无需编写繁琐的代码。Parquet.Net 被全球许多小型和大型组织使用。
官方公开的 NuGet 统计数据
已经表明
Azure 机器学习

ML.NET
正在使用它,这两者都很大,但也有很多其他的用户在用。

Parquet 是一种列式存储格式,旨在提供高效的存储和检索能力,广泛应用于大数据处理框架如 Apache Spark 中。Parquet 支持高级压缩和编码方案,以优化存储空间和提高读取速度。截至
2024
年,Parquet.Net是世界上
最快的 Parquet 库
,不仅在 .NET 运行时,而且与所有平台相比。

Parquet.Net 提供的高级 API 具体包括以下功能:

  1. 列式存储
    :Parquet 是一种列式存储格式,这意味着数据按列存储而不是按行存储。这种存储方式可以显著提高大数据处理和分析的效率。

  2. 高效的数据读取
    :通过列式存储结构,Parquet 实现了高效的数据读取能力,特别是在处理大规模数据集时表现尤为突出。

  3. 低级 API 使用
    :Parquet.Net 还提供了低级 API,这是与 Parquet 数据结构最相似且性能最高的方法。虽然这种方法不如其他高级 API 直观,但它需要用户对 Parquet 数据结构有一定的了解,并且在使用前必须定义模式(schema)

目前,Parquet.Net 的最新版本是 4.25.0,可以通过 NuGet 包管理器在 Visual Studio 中安装。

大家好,我是Edison。

上一篇
,我们了解了什么是AI Agent以及如何用Semantic Kernel手搓一个AI Agent。有朋友留言说,自动函数调用对大模型有较高的要求,比如Azure OpenAI、智谱AI等这些收费的大模型产品就能很好地规划和处理函数调用,而像是一些开源的小参数量的模型例如qwen2-7b-instruct这种可能效果就不太好。刚好,之前在网上看到一位大佬的开源
通用函数调用方案
,于是重构了一下上一篇的Agent应用。

UniversalFunctionCaller

这个项目是一个封装了大模型对话的入口,有点类似我们在ASP.NET中写的Filter,在处理某个真正的请求时,给其设置一些横切面,例如PreHandle,PostHandle之类的方法供用户做自定义处理,最终完成所谓的AoP(面向横切面编程)的效果。这个项目做的事儿其实也就是封装了横切面,在真正将prompt发给LLM前,它会读取一些自定义的优点类似于预训练的prompt来对用户的prompt进行“增强“。例如,下面这个方法 GetAskFromHistory 就会来 设定一个函数调用的背景 以及 给出一些预置的训练提示词供大模型理解,妥妥的一个手动增强版提示词工程:

public classUniversalFunctionCaller
{
......
public async Task<string>RunAsync(ChatHistory askHistory)
{
var ask = awaitGetAskFromHistory(askHistory);return awaitRunAsync(ask);
}
private async Task<string>GetAskFromHistory(ChatHistory askHistory)
{
var sb = newStringBuilder();var userAndAssistantMessages = askHistory.Where(h => h.Role == AuthorRole.Assistant || h.Role ==AuthorRole.User);foreach (var message inuserAndAssistantMessages)
sb.AppendLine($
"{message.Role.ToString()}: {message.Content}");var extractAskFromHistoryPrompt = $@"阅读这段用户与助手之间的对话。
总结用户在最后一句话中希望助手做什么
##对话开始##
{sb.ToString()}
##对话结束##
";var extractAskResult = await_chatCompletion.GetChatMessageContentAsync(extractAskFromHistoryPrompt);var ask =extractAskResult.Content;returnask;
}
  ......
}

然后,它会初始化一个ChatHistory,提供一些示范性的对话,让大模型知道是否该进行函数调用 以及 如何调用:

private ChatHistory InitializeChatHistory(stringask)
{
var history = newChatHistory();
history.Add(
new ChatMessageContent(AuthorRole.User, "New task: 启动飞船"));
history.Add(
new ChatMessageContent(AuthorRole.Assistant, "GetMySpaceshipName()"));
history.Add(
new ChatMessageContent(AuthorRole.User, "长征七号"));
history.Add(
new ChatMessageContent(AuthorRole.Assistant, "StartSpaceship(ship_name: \"长征七号\")"));
history.Add(
new ChatMessageContent(AuthorRole.User, "飞船启动"));
history.Add(
new ChatMessageContent(AuthorRole.Assistant, "Finished(finalmessage: \"'长征七号'飞船启动 \")"));returnhistory;
}

而示范用的函数则将其封装到了一个预置的Plugin,我们暂且叫它 PreTrainingPlugin,它是一个internal访问的class,只用于对prompt进行增强即给出示例:

internal classPreTrainingPlugin
{
[KernelFunction, Description(
"当工作流程完成,没有更多的函数需要调用时,调用这个函数")]public string Finished([Description("总结已完成的工作和结果,尽量简洁明了。")] stringfinalmessage)
{
return string.Empty;//no actual implementation, for internal routing only }

[KernelFunction, Description(
"获取用户飞船的名称")]public stringGetMySpaceshipName()
{
return "长征七号";
}

[KernelFunction, Description(
"启动飞船")]public void StartSpaceship([Description("启动的飞船的名字")] stringship_name)
{
//no actual implementation, for internal routing only }
}

同时,它会将你定义的Functions总结为一个string列表,然后作为可用的Function list 放到prompt中告诉大模型:

然后,就开始根据用户的prompt进行函数调用了,直到它认为不会再需要函数调用时就结束,这个方法的全部代码如下所示:

public async Task<string> RunAsync(stringtask)
{
//Initialize plugins var plugins =_kernel.Plugins;var internalPlugin = _kernel.Plugins.AddFromType<PreTrainingPlugin>();//Convert plugins to text var pluginsAsText =GetTemplatesAsTextPrompt3000(plugins);//Initialize function call and chat history var nextFunctionCall = new FunctionCall { Name =ConfigConstants.FunctionCallStatus.Start };var chatHistory =InitializeChatHistory(task);//Add new task to chat history chatHistory.Add(new ChatMessageContent(AuthorRole.User, $"New task: {task}"));//Process function calls for (int iteration = 0; iteration < 10 && nextFunctionCall.Name != ConfigConstants.FunctionCallStatus.Finished; iteration++)
{
nextFunctionCall
= awaitGetNextFunctionCallAsync(chatHistory, pluginsAsText);if (nextFunctionCall == null)throw new Exception("The LLM is not compatible with this approach!");//Add function call to chat history var nextFunctionCallText =GetCallAsTextPrompt3000(nextFunctionCall);
chatHistory.AddAssistantMessage(nextFunctionCallText);
//Invoke plugin and add response to chat history var pluginResponse = awaitInvokePluginAsync(nextFunctionCall);
chatHistory.AddUserMessage(pluginResponse);
}
//Remove internal plugin _kernel.Plugins.Remove(internalPlugin);//Check if task was completed successfully if (nextFunctionCall.Name ==ConfigConstants.FunctionCallStatus.Finished)
{
var finalMessage = nextFunctionCall.Parameters[0].Value.ToString();returnfinalMessage;
}
throw new Exception("LLM could not finish workflow within 10 steps. Please consider increasing the number of steps!");
}

需要特别注意的是,不建议在一个prompt中涉及超过10次函数调用,这样效果不太好,处理速度也慢,验证也不太方便。

此外,在方法内部进行函数调用的分析时,自动加了一个如下所示的SystemMessage,用于设定一些通用的规则给到大模型进行理解:

private string GetLoopSystemMessage(stringpluginsAsTextPrompt3000)
{
var systemPrompt = $@"你是一个计算机系统。
你只能使用TextPrompt3000指令,让用户调用对应的函数,而用户将作为另一个回答这些函数的计算机系统。
以下是您所需实现的目标,以及用户可以使用的函数列表。
您需要找出用户到达目标的下一步,并推荐一个TextPrompt3000函数调用。
您还会得到一个TextPrompt3000 Schema格式的函数列表。
TextPrompt3000格式的定义如下所示:
{GetTextPrompt300Explanation()}
##可用函数列表开始##
{pluginsAsTextPrompt3000}
##可用函数列表结束##

以下规则非常重要:
1) 你只能推荐一个函数及其参数,而不是多个函数
2) 你可以推荐的函数只存在于可用函数列表中
3) 你需要为该函数提供所有参数。不要在函数名或参数名中转义特殊字符,直接使用(如只写aaa_bbb,不要写成aaa\_bbb)
4) 你推荐的历史记录与函数需要对更接近目标有重要作用
5) 不要将函数相互嵌套。 遵循列表中的函数,这不是一个数学问题。 不要使用占位符。
我们只需要一个函数,下一个所需的函数。举个例子, 如果 function A() 需要在 function B()中当参数使用, 不要使用 B(A())。 而是,
如果A还没有被调用, 先调用 A()。返回的结果将在下一次迭代中在B中使用。
6) 不要推荐一个最近已经调用过的函数。 使用输出代替。 不要将占位符或函数作为其他函数的参数使用。
7) 只写出一个函数调用,不解释原因,不提供理由。您只能写出一个函数调用!
8) 当所有必需的函数都被调用,且计算机系统呈现了结果,调用Finished函数并展示结果。
9) 请使用中文回答。

如果你违反了任何这些规定,那么会有一只小猫死去。
";returnsystemPrompt;
}

综上所示,这就是提示词工程的魔力所在!

更新后的AI Agent效果

这里我们快速对原来的WorkOrder Agent重构了一下,增加了 Use Function Planner 的 checkbox选项,如果你勾选了它,就会使用上面介绍的 UniversalFunctionCaller 进行prompt的包裹和预处理,然后再发给大模型 以及 进行函数调用。

这里我修改了使用的模型和平台信息,这里我们基于SiliconCloud来使用一个通义千问的小参数文本生成模型Qwen2-7B-Instruct来试试:

{"LLM_API_PROVIDER": "QwenAI","LLM_API_MODEL": "Qwen/Qwen2-7B-Instruct","LLM_API_BASE_URL": "https://api.siliconflow.cn","LLM_API_KEY": "sk-**************" //Update this value to yours
}

具体效果如下图所示:

(1)没有使用Function Planner的效果

(2)使用了Function Planner的效果

可以看到,我的需求其实包含3个步骤:第一步是更新工单的Quantity,第二步是更新工单的状态,第三步是查询更新后的工单信息。而这几个步骤我们假设其实都是需要去调用MES WorkOrderService API才能获得的,这里我们的Agent理解到了要点,并分别调用了两个function实现了任务。

这个示例代码的结构如下所示:

我这里将UniversalFunctionCaller放到了解决方案中的Shared类库中了,源码来自
Jenscaasen
大佬的开源项目,中文翻译的prompt来自国内的
mingupupu
大佬的介绍。

小结

本文简单介绍了一种面向小参数量模型的通用函数调用方案,基于这个方案,我们可以在这类大模型上进行准确的函数调用,以便实现更可靠的AI Agent。

参考内容

国外的
Jenscaasen
大佬开源的这个项目 :
https://github.com/Jenscaasen/UniversalLLMFunctionCaller

国内的
mingupupu
大佬的介绍和翻译:
https://www.cnblogs.com/mingupupu/p/18385798

示例源码

GitHub:
https://github.com/Coder-EdisonZhou/EDT.Agent.Demos

推荐学习

Microsoft Learn, 《
Semantic Kernel 学习之路

VMware Cloud Foundation 5.2 发布并引入了一个新的功能,借助 VCF Import Tool 工具可以将现有 vSphere 环境直接转换(Convert)为管理工作负载域或者导入(Import)为 VI 工作负载域。通过这种能力,客户无需购买新硬件和进行复杂的部署和迁移工作,即可将已有的环境快速转变为由 VMware Cloud Foundation 解决方案驱动的 SDDC 软件定义数据中心。使用这种方式,不会对现有的业务产生任何影响,即能够在不中断工作负载的情况下完成所有转换过程。

VCF Import Tool 提供两种方式来帮助客户将现有的 vSphere 环境转变为 VMware Cloud Foundation,分别是转换(Convert)和导入(Import)。如果客户之前从来没有用过 VMware Cloud Foundation 解决方案,也就是说现在可能是 vSphere 环境或者 vSphere+vSAN 环境,那可以使用 VCF Import Tool 提供的转换(Convert)功能直接将现有环境转换为管理工作负载域;如果客户环境中已经在用 VMware Cloud Foundation 解决方案,也就是说现在已经有了管理工作负载域,可以使用 VCF Import Tool 提供的导入(Import)功能直接将现有环境导入为 VI 工作负载域。总体来看,VMware Cloud Foundation 解决方案现在提供了三种方式来构建工作负载域,分别是部署(Deploy)、转换(Convert)和导入(Import),VMware Cloud Builder 方式用于 VCF 管理域初始部署(Deploy)。

注意,使用这两种方式所执行的工作流有所不同,如果是转换(Convert),那第一步需要在现有 vSphere 环境上部署 SDDC Manager,然后上传 VCF Import Tool 相关软件和工具并执行转换(Convert)过程;如果是导入(Import),那将 VCF Import Tool 相关软件和工具上传至 SDDC Manager 后即可执行导入(Import)过程。有关更多内容和细节请查看
《Converting or Importing Existing vSphere Environments into VMware Cloud Foundation》
产品文档。

一、使用要求和限制

如果使用 VCF Import Tool 工具将现有 vShere 环境转换(Convert)为管理工作负载域或者导入(Import)为 VI 工作负载域,会具有许多
要求和限制
,需要先满足这些条件才能执行对应的操作。注,由于 VCF Import Tool 工具目前发布的还是初始版,随着后续工具的逐渐完善,肯定会支持越来越多的场景,比如目前版本还不支持具有 NSX 的环境,而在 VMware Explore 2024 大会上发布的
VMware Cloud Foundation 9
未来版本中将支持转换(Convert)或导入(Import)具有 NSX 解决方案的环境。

1)基本要求

转换为管理域:

  • 现有 vSphere 环境必须运行 vSphere 8 U3 及更高版本(VCF 5.2 BOM),包括 vCenter 和 ESXi 主机。
  • 现有 vSphere 环境中的 vCenter Server 虚拟机必须属于同一集群,也被称为“共存”。

导入为 VI 域:

  • 现有 vSphere 环境必须运行 vSphere 7 U3 及更高版本(VCF 4.5 BOM),包括 vCenter 和 ESXi 主机。
  • 现有 vSphere 环境中的 vCenter Server 虚拟机必须属于同一集群或者在管理域中运行。

2)通用要求

  • 现有 vSphere 集群中的所有主机都必须是同构的。即集群中的所有主机在容量、存储类型和配置(pNIC、VDS 等)方面都需要相同。
  • 现有 vSphere 集群必须配置 DRS 为全自动模式。
  • 现有 vSphere 集群只能使用三种受支持的存储类型之一:vSAN、NFS 或 FC SAN(VMFS)。
  • 现有 vSphere 集群如果使用了 vSAN 存储,至少需要 4 台 ESXi 主机,使用其他类型的存储则至少需要 2 台 ESXi 主机,若要部署 NSX,则至少需要 3 台 ESXi 主机。
  • 现有 vSphere 集群 vCenter Server 不能配置为增强型链接模式(EAM),使用转换或导入后,现有 vSphere 集群都只能属于自己的 SSO 域当中。
  • 现有 vSphere 集群中的所有主机必须配置专用的 vMotion 网络,每台主机所配置的网卡流量有且只能有一个,并且网卡配置的 IP 地址必须是静态固定的。
  • 现有 vSphere 环境中,vCenter Server 清单中的所有集群都必须配置了一个或多个专用 VDS 分布式交换机,VDS 交换机不能被多个集群共享,集群内不能存在 VSS 标准交换机。
  • 现有 vSphere 环境中,vCenter Server 清单中不能有独立的主机,独立主机通常是指位于数据中心和主机文件夹而不属于任何集群的主机,若有则需要将主机移入到某个集群当中。

3)支持限制

  • 不支持配置了 LACP 链路聚合的 vSphere 环境。
  • 不支持配置了 VDS 交换机共享的 vSphere 环境。
  • 不支持配置了 vSAN 延伸集群的 vSphere 环境。
  • 不支持配置了 vSAN 集群“仅压缩”功能的 vSphere 环境。
  • 不支持配置了 NSX 的 vSphere 环境。
  • 不支持配置了 AVI Load Balancer 的 vSphere 环境。
  • 不支持配置了 IaaS Control Plane 的 vSphere 环境。
  • 不支持 VxRail 环境。

值得一提的是,使用 VCF Import Tool 方式将现有 vSphere 环境转换为管理工作负载域,可以绕过使用 Cloud Builder 部署 VCF 时只能使用 vSAN 的要求。有关使用要求和限制也可以查看这篇(
Introduction to the VMware Cloud Foundation (VCF) Import Tool
)文章。

二、现有 vSphere 环境

使用 VCF Import Tool 工具有各种限制和注意事项,针对这些情况,下面让我们确认一下现有 vSphere 环境中的信息,以确保能满足转换要求。由于我这边是通过嵌套虚拟化搭建准备的环境,这跟实际环境肯定会有所区别,如果你也想搭建这样的测试环境,可以参考这篇(
一次性说清楚 vCenter Server 的 CLI 部署方式。
)文章中的方法,部署单节点 vSAN ESA 集群后再添加并配置其他 ESXi 主机。

首先,现有 vSphere 环境中的 vCenter Server 必须是 VCF 5.2 BOM 物料清单中的版本,如果实际环境中不是这个版本,需要将 vCenter Server 升级到该版本(或更高)。

现有 vSphere 环境中用于转换为管理域集群内的主机必须是 VCF 5.2 BOM 物料清单中的版本,如果实际环境中不是这个版本,需要将 ESXi 主机升级到该版本(或更高)。由于集群使用了 vSAN 存储,所以集群内的主机要求至少具有 4 台。当前 vSphere 环境中不支持位于数据中心或主机文件夹的独立主机,如果有独立主机,则必须移至某个集群内。

集群内每个 ESXi 主机的 VMkernel 网卡所启用的服务流量类型必须只有一个,也就是说每种流量类型的服务只能有一个 VMkernel 网卡,vmk0 不能既启用了管理流量,同时又启用了 vMotion 流量,必须分开。这些 VMkernel 网卡所分配的 IP 地址不能是 DHCP 获取的,必须是静态配置并且固定的。

集群内每个 ESXi 主机都应该配置了 NTP 时钟同步。同样,vCenter Server 也应该配置。

现有 vSphere 环境中的集群 DRS 配置必须是全自动的。

现有 vSphere 集群如果为 vSAN 集群,只能是 vSAN HCI 标准/单站点集群,目前还不支持 vSAN HCI 延伸集群。如果使用了 OSA 架构的 vSAN 集群,不支持仅开了压缩功能,重删和压缩需要都开启。

现有 vSphere 集群使用了基于 vLCM 的生命周期管理方式。

现有 vSphere 集群中当前有 vCenter Server 虚拟机和 vCLS 集群服务虚拟机,如果转换为管理域,则 vCenter Server 虚拟机必须存在于于这个集群之中,不能位于其他位置,如果有多个集群并且用于转换的集群中没有 vCenter Server 虚拟机,则需要先将虚拟机迁移到管理域集群。

现有 vSphere 集群所使用的 vSAN 存储。

现有 vSphere 集群所使用的 VDS 分布式交换机,转换为管理域的集群不能存在 VSS 标准交换机,如果有标准交换机,则需要将虚拟机迁移至分布式交换机并将标准交换机删除。这个 VDS 交换机不能启用 LACP 链路聚合功能,同时集群内的 ESXi 主机至少有两张网卡(10G)连接到这个 VDS 交换机的上行链路当中。这个 VDS 交换机只能专用于这个集群,不能有多个集群共享同一个 VDS 交换机。

现有 vSphere 集群的 vCenter Server 不能配置增强型链接模式(ELM),否则当前 VCF Import Tool 版本不支持。

现有 vSphere 集群的 vCenter Server 不能注册到任何 NSX 解决方案当中,当前还不支持转换具有 NSX 注册的 vCenter Server。

现有 vSphere 集群没有启用工作负载管理(IaaS Control Plane,以前叫 vSphere with Tanzu)。其他的,还有就是现有 vSphere 环境不能是 VxRail 环境,不能部署了 AVI Load Balancer 等其他解决方案。

三、准备 VCF Import Tool

使用 VCF Import Tool 工具对现有 vSphere 环境进行转换(Convert)或导入(Import),需要提前准备相关软件和工具,如下图所示。主要有三个文件,首先第一个是 VCF Import Tool,用于执行转换(Convert)或导入(Import)的命令行工具;第二个是 VCF-SDDC-Manager-Appliance,这是 VCF 中 SDDC Manager 组件的独立部署设备,执行转换(Convert)或导入(Import)过程需要在 SDDC Manager 虚拟机中运行;第三个是 VMware Software Install Bundle - NSX_T_MANAGER 4.2.0.0,这是 NSX 解决方案的 NSX Manager 安装包,在执行转换(Convert)或导入(Import)的过程中或者 Day 2,用于部署 NSX 解决方案。

如果你有账号,可以登录
Broadcom 支持门户
(BSP)并在上图中的地方进行下载,如果你嫌麻烦,下面的百度网盘链接也可以保存。

文件名称 MD5 百度网盘
VCF-SDDC-Manager-Appliance-5.2.0.0-24108943.ova 1944511a2aaff3598d644059fbfc2c19 https://pan.baidu.com/s/1lUbrN0zjLUUC1oB8L7ZRAg?pwd=lvx9
vcf-brownfield-import-5.2.0.0-24108578.tar.gz 22e66def7acdaa60fb2f057326fec1fd
bundle-124941.zip dabf98d48d9b295bced0a5911ed7ff24

四、检查 vSphere 环境

准备了相关软件和工具后,可以使用 VCF Import Tool 工具先对当前的 vSphere 环境进行验证一下,看看现有 vSphere 环境是否有那些地方不符合 VCF Import Tool 转换(Convert)或导入(Import)的要求。通过 SSH 以 root 用户连接到 vCenter Server 并进入 Shell 命令行,运行 chsh 命令将 vCenter Server 的默认终端命令行从 API 改成 Shell,然后创建一个临时目录(vcfimport),将 VCF Import Tool 工具上传到这个目录。

chsh
/bin/bash
mkdir /tmp/vcfimport
ls /tmp/vcfimport

进入到工具上传的目录,使用 tar 命令解压文件,再进入到 vcf-brownfield-toolset 目录。

cd /tmp/vcfimport/
tar -xf /tmp/vcfimport/vcf-brownfield-import-5.2.0.0-24108578.tar.gz
cd vcf-brownfield-import-5.2.0.0-24108578/vcf-brownfield-toolset/

需要使用工具包内的
vcf_brownfield.py
脚本来执行现有 vSphere 环境的检查,命令如下所示。

python3 vcf_brownfield.py precheck --vcenter vcf-mgmt01-vcsa01.mulab.local --sso-user administrator@vsphere.local --sso-password Vcf520@password

检查结果通过,将 VCF Import Tool 工具包从 vCenter Server 中删除。

cd ~
rm -rf /tmp/vcfimport/

五、部署 SDDC Manager

SDDC Manager 是 VCF 解决方案的关键核心组件,如果通过 Cloud Builder 工具构建管理域则将自动部署 SDDC Manager 虚拟机,如果使用 VCF Import Tool 方式进行转换(Convert)则需要手动将 SDDC Manager 设备部署到现有 vSphere 环境当中。

导航到 vCenter Server(vSphere Client)->数据中心->集群,右击选择“部署 OVF 模板”。

选择从本地文件上载 SDDC Manager OVA 设备,点击下一页。

配置 SDDC Manager 虚拟机的名称并选择存放的位置,点击下一页。

选择 SDDC Manager 虚拟机所使用的计算资源,点击下一页。

检查 SDDC Manager OVA 设备的摘要信息,点击下一页。

接受 SDDC Manager 安装许可协议,点击下一页。

选择 SDDC Manager 虚拟机所使用的存储,点击下一页。

选择 SDDC Manager 虚拟机所使用的网络端口组,点击下一页。

配置 SDDC Manager 虚拟机各类用户的密码以及地址等信息,点击下一页。

检查所有配置,点击完成并开始部署。

部署成功后,可以对 SDDC Manager 虚拟机创建一个快照。

右击 SDDC Manager 虚拟机,点击打开电源。

此时,如果访问 SDDC Manager UI,可以看到正在初始化中,现在不用管它,只需要能通过 SSH 访问 SDDC Manager 的 Shell 即可。

六、执行转换前预检查

后续我们需要通过 SDDC Manager 执行现有 vSphere 环境的转换过程,但是在正式执行转换前,还需要在 SDDC Manager 上再执行一次预检查,确定当前 vSphere 环境是否满足转换为管理域的要求。通过 SSH 以 vcf 用户连接到 SDDC Manager 的命令行,使用以下命令创建一个新的目录(vcfimport),然后将 VCF Import Tool 文件上传到 SDDC Manager 的这个目录,再将文件解压后并进入到 vcf-brownfield-toolset 目录。

mkdir /home/vcf/vcfimport
ls /home/vcf/vcfimport
cd /home/vcf/vcfimport
tar -xf vcf-brownfield-import-5.2.0.0-24108578.tar.gz
cd vcf-brownfield-import-5.2.0.0-24108578/vcf-brownfield-toolset/

进入 VCF Import Tool 目录后,使用以下命令在 SDDC Manager 上执行环境检查。总共有 98 个内容,成功检查 97 个,失败 1 个。

python3 vcf_brownfield.py check --vcenter vcf-mgmt01-vcsa01.mulab.local --sso-user administrator@vsphere.local --sso-password Vcf520@password

可以通过输出的结果(JSON 文件和 CSV 表格文件),查看具体失败的内容,还可以通过 All guardrails CSV 文件查看所有检查的内容。

根据上面所查看的 JSON 文件,有一个检查失败的原因是由于当前 vSphere 环境的 vLCM 配置有一项与 SDDC Manager 中的默认 vLCM 配置不一致导致的。其实,我们可以通过
ESX Upgrade Policy Guardrail Failure
查看 SDDC Manager 中 vLCM 的默认配置,检查当前 vSphere 环境的 vLCM 配置,然后将这些配置调整为 SDDC Manager 中 vLCM 的默认配置即可。当然,如果你不处理,这个失败的检查应该不会影响现有 vSphere 环境转换为管理域。根据错误的信息,将当前 vSphere 环境的 vLCM 配置修改为 SDDC Manager 中 vLCM 的默认配置,如下图所示。

修改 vLCM 配置后,重新执行一遍检查,现在所有检查都已成功。

七、准备 NSX Manager

在将现有 vSphere 环境转换为管理域时,我们可以同时执行 NSX 的部署,由于当前还不支持将现有 NSX 的环境转换为管理域,所以这一步算是对这一解决方案的补充。不过,如果在执行转换过程部署 NSX 解决方案,这只会为 ESXi 主机配置只有安全功能的 NSX,如果想实现完整的 NSX Overlay 网络功能,比如支持微分段、T0/T1 网关等,需要在 NSX 部署完以后,单独去配置 TEP 网络和其他设置。这一步骤是可选操作,你可以在执行转换过程同时执行 NSX 的部署,也可以在执行转换结束之后,在其他时间再执行 NSX 的部署。

使用 VCF Import Tool 执行现有 vSphere 环境的转换并同时执行 NSX 的部署需要准备一个
JSON 配置文件
,如下所示。这个配置文件中定义了 NSX Manager 的部署大小,NSX 集群的 VIP 地址以及三个 NSX Manager 设备的地址信息,请一定提前配置好这些地址的正反向域名解析,还有一个重点要注意的是,NSX 所部署的安装包路径,这个保持默认即可。

{
  "license_key": "AAAAA-BBBBB-CCCCC-DDDDD-EEEEE",
  "form_factor": "medium",
  "admin_password": "Vcf520@password",
  "install_bundle_path": "/nfs/vmware/vcf/nfs-mount/bundle/bundle-124941.zip",
  "cluster_ip": "192.168.32.66",
  "cluster_fqdn": "vcf-mgmt01-nsx01.mulab.local",
  "manager_specs": [{
    "fqdn": "vcf-mgmt01-nsx01a.mulab.local",
    "name": "vcf-mgmt01-nsx01a",
    "ip_address": "192.168.32.67",
    "gateway": "192.168.32.254",
    "subnet_mask": "255.255.255.0"
  },
  {
    "fqdn": "vcf-mgmt01-nsx01b.mulab.local",
    "name": "vcf-mgmt01-nsx01b",
    "ip_address": "192.168.32.68",
    "gateway": "192.168.32.254",
    "subnet_mask": "255.255.255.0"
  },
  {
    "fqdn": "vcf-mgmt01-nsx01c.mulab.local",
    "name": "vcf-mgmt01-nsx01c",
    "ip_address": "192.168.32.69",
    "gateway": "192.168.32.254",
    "subnet_mask": "255.255.255.0"
  }]
}

将 NSX 部署的 JSON 配置文件以及 NSX 的安装包上传到 SDDC Manager 中,需要记住这个配置文件上传的路径,后面需要用到。

ls /home/vcf/vcfimport/
ls /nfs/vmware/vcf/nfs-mount/bundle/

八、正式执行转换过程

通过 vcf 用户登录到 SDDC Manager 命令行,进入到 vcf-brownfield-toolset 目录后,使用以下命令执行 vSphere 环境转换过程。运行命令后,输入 SDDC Manager 的 admin 和 backup 用户的密码以及 vCenter Server 的 root 密码进行验证。

cd /home/vcf/vcfimport/vcf-brownfield-import-5.2.0.0-24108578/vcf-brownfield-toolset/
python3 vcf_brownfield.py convert --vcenter vcf-mgmt01-vcsa01.mulab.local --sso-user administrator@vsphere.local --sso-password Vcf520@password --nsx-deployment-spec-path /home/vcf/vcfimport/vcf520-import-nsx.json

此时,可以登录 SDDC Manager UI 查看任务执行的状态。

转换任务执行一段时间后,居然失败了!

但是,通过 SDDC Manager UI 查看,任务已经成功了。

也能看到 vSphere 环境已经转换成管理域。

使用下面命令查看了一下输出结果,原因是部署 NSX 任务失败了,意思是说上传的 NSX 安装包不是一个有效的 ZIP 文件。

再次查看了一下上传的 NSX 安装包文件,发现确实是上传的包有问题,大小只有 3 个多 G,额确实是自己疏忽了。将文件删除后,重新使用 FTP 工具将 NSX 安装包上传上去,查看大小为 12 G,这次没问题了。正常情况下,你应该不会遇到上面这个问题。

由于已经完成 vSphere 环境的转换,现在不能再重新使用上面的 Convert 命令了,就是等于在转换的时候没有执行 NSX 的部署,所以这就当作是在 Day 2 执行这个步骤,我们需要使用另外一个命令来
单独执行 NSX 的部署
工作流,如下所示。

python3 vcf_brownfield.py deploy-nsx --vcenter vcf-mgmt01-vcsa01.mulab.local --nsx-deployment-spec-path /home/vcf/vcfimport/vcf520-import-nsx.json

现在可以看到,NSX 设备压缩包已经可以正常解压了,输入 yes 开始 NSX Manager 的部署。

NSX 部署成功。

通过 SDDC Manager UI 查看任务状态,结果也是成功。

部署成功后,切换到 root 用户,重新启动 SDDC Manager 的所有服务,并等待 UI 重新初始化。

九、验证已转换的管理域

导航到 SDDC Manager UI->清单->工作负载域,可以看到现有 vSphere 环境已经被转换成管理域,管理域的名称为 domain-vcf-mgmt01-vcsa01。

点击进入管理域,查看该工作负载域的摘要信息,提示当前管理域中的产品缺少许可证,需要点击“添加许可证”为该域中的产品分配许可证密钥。

在主机和集群选项卡中,可以看到属于 vSphere 环境中的 ESXi 主机和集群配置信息。

导航到 SDDC Manager UI->管理->网络设置,可以看到 vSphere 环境中 ESXi 主机的 VMkernel 网卡,用于 vMotion 服务和 vSAN 服务的静态 IP 地址已经被创建为网络池。

导航到 SDDC Manager UI->管理->许可,由于当前管理域中产品缺少许可证,可以点击“许可证密钥”为该域中的产品分配许可证密钥。

可以对管理域执行一下预检查,检查各个组件和配置是否正常。

查看检查结果,可以看到有些错误和警告,这些都可以忽略,因为当前环境中确实还没有进行配置。

登录 vCenter Server(vSphere Client),可以看到三个 NSX Manager 设备已被部署到管理域集群当中,并且创建了虚拟机/主机关联性规则,三个虚拟机必须在不同的主机上运行,这就是为什么如果使用转换(Convert)将现有 vSphere 环境转换为管理域,如果没有使用 vSAN 解决方案,使用了 NFS 或者 FC SAN,如果要部署 NSX 的话就需要三台主机,如果不部署 NSX 的话可以只需要两台主机。

登录 NSX Manager UI(VIP),查看 NSX 系统配置概览。

NSX 集群配置,一个由三节点 NSX Manager 所组成的 NSX 管理集群。

管理域 vCenter Server 已作为计算管理器被添加到 NSX 当中。

管理域集群中的主机已配置了分布式虚拟端口组(DVPG)的NSX,也就是 NSX-Security only 仅安全功能,也就是说可以将 NSX 的安全功能应用于管理域 vCenter Server 上连接至虚拟端口组的管理组件虚拟机。

注意,在制定 NSX 安全策略之前,请确保将关键管理虚拟机列入白名单或进行排除,以避免出现锁定情况,你也可以为不是管理组件的虚拟机单独创建 DFW 排除列表。

今天在编码的时候遇到了一个问题,需要对数组变量添加新元素和删除元素,因为数组是固定大小的,因此对新增和删除并不友好,但有时候又会用到,因此想针对数组封装两个扩展方法:新增元素与删除元素,并能到达以下三个目标:

1、性能优异;

2、兼容性好;

3、方便使用;

这三个目标最麻烦的应该就是性能优异了,比较后面两个可以通过泛型方法,扩展方法,按引用传递等语法实现,性能优异却要在十来种实现方法中选出两个最优的实现。那关于数组新增和删除元素你能想到多少种实现呢?下面我们来一起看看那个性能最好。

01
、新增元素实现方法对比

1、通过List方法实现

通过转为List,再用AddRange方法添加元素,最后再转为数组返回。代码实现如下:

public static int[] AddByList(int[] source, int[] added)
{
    var list = source.ToList();
    list.AddRange(added);
    return list.ToArray();
}

2、通过IEnumerable方法实现

因为数组实现了IEnumerable接口,所以可以直接调用Concat方法实现两个数组拼接。代码实现如下:

public static int[] AddByConcat(int[] source, int[] added)
{
    return source.Concat(added).ToArray();
}

3、通过Array方法实现

Array有个Copy静态方法可以实现把数组复制到目标数组中,因此我们可以先构建一个大数组,然后用Copy方法把两个数组都复制到大数组中。代码实现如下:

public static int[] AddByCopy(int[] source, int[] added)
 {
     var size = source.Length + added.Length;
     var array = new int[size];
     // 复制原数组  
     Array.Copy(source, array, source.Length);
     // 添加新元素  
     Array.Copy(added, 0, array, source.Length, added.Length);
     return array;
 }

4、通过Span方法实现

Span也有一个类似Array的Copy方法,功能也类似,就是CopyTo方法。代码实现如下:

public static int[] AddBySpan(int[] source, int[] added)
{
    Span<int> sourceSpan = source;
    Span<int> addedSpan = added;
    Span<int> span = new int[source.Length + added.Length];
    // 复制原数组
    sourceSpan.CopyTo(span);
    // 添加新元素
    addedSpan.CopyTo(span.Slice(sourceSpan.Length)); 
    return span.ToArray();
}

我想到了4种方法来实现,如果你有不同的方法希望可以给我留言,不吝赐教。那么那种方法效率最高呢?按我理解作为现在.net core性能中的一等公民Span应该性能是最好的。

我们也不瞎猜了,直接来一组基准测试对比。我们对4个方法,分三组测试,每组分别随机生成两个100、1000、10000个元素的数组,然后每组再进行10000次测试。

测试结果如下:

整体排名:AddByCopy > AddByConcat > AddBySpan > AddByList。

可以发现性能最好的竟然是Array的Copy方法,不但速度最优,而且内存使用方面也是最优的。

而我认为性能最好的Span整体表现还不如IEnumerable的Concat方法。

最终Array的Copy方法完胜。

02
、删除元素实现方法对比

1、通过List方法实现

还是先把数组转为List,然后再用RemoveAll进行删除,最后把结果转为数组返回。代码实现如下:

public static int[] RemoveByList(int[] source, int[] added)
{
    var list = source.ToList();
    list.RemoveAll(x => added.Contains(x));
    return list.ToArray();
}

2、通过IEnumerable方法实现

因为数组实现了IEnumerable接口,所以可以直接调用Where方法进行过滤。代码实现如下:

public static int[] RemoveByWhere(int[] source, int[] added)
{
     return source.Where(x => !added.Contains(x)).ToArray();
}

3、通过Array方法实现

Array有个FindAll静态方法可以实现根据条件查找数组。代码实现如下:

public static int[] RemoveByArray(int[] source, int[] added)
{
    return Array.FindAll(source, x => !added.Contains(x));
}

4、通过For+List方式实现

直接遍历原数组,把满足条件的元素放入List中,然后转为数组返回。代码实现如下:

public static int[] RemoveByForList(int[] source, int[] added)
{
    var list = new List<int>();
    foreach (int item in source)
    {
        if (!added.Contains(item))
        {
            list.Add(item);
        }
    }
    return list.ToArray();
}

5、通过For+标记+Copy方式实现

还是直接遍历原数组,但是我们不创建新集合,直接把满足的元素放在原数组中,因为从原数组第一个元素迭代,如果元素满足则放入第一个元素其索引自动加1,如果不满足则等下一个满足的元素放入其索引保持不变,以此类推,直至所有元素处理完成,最后再把原数组中满足要求的数组复制到新数据中返回。代码实现如下:

public static int[] RemoveByForMarkCopy(int[] source, int[] added)
{
    var idx = 0;
    foreach (var item in source)
    {
        if (!added.Contains(item))
        {
            // 标记有效元素
            source[idx++] = item; 
        }
    }
    // 创建新数组并复制有效元素
    var array = new int[idx];
    Array.Copy(source, array, idx);
    return array;
}

6、通过For+标记+Resize方式实现

这个方法和上一个方法实现基本一致,主要差别在最后一步,这个方法是直接通过Array的Resize静态方法把原数组调整为我们要的并返回。代码实现如下:

public static int[] RemoveByForMarkResize(int[] source, int[] added)
{
    var idx = 0;
    foreach (var item in source)
    {
        if (!added.Contains(item))
        {
            //标记有效元素
            source[idx++] = item; 
        }
    }
    //调整数组大小
    Array.Resize(ref source, idx); 
    return source;
}

同样的我们再做一组基准测试对比,结果如下:

可以发现最后两个方法随着数组元素增加性能越来越差,而其他四种方法相差不大。既然如此我们就选择Array原生方法FindAll。

03
、实现封装方法

新增删除的两个方法已经确定,我们第一个目标就解决了。

既然要封装为公共的方法,那么就必要要有良好的兼容性,我们示例虽然都是用的int类型数组,但是实际使用中不知道会碰到什么类型,因此最好方式是选择泛型方法。这样第二个目标就解决了。

那么第三个目标方便使用要怎么办呢?第一想法既然做成公共方法了,直接做一个帮助类,比如ArrayHelper,然后把两个实现方法直接以静态方法放进去。

但是我更偏向使用扩展方法,原因有二,其一可以利用编辑器直接智能提示出该方法,其二代码更简洁。形如下面两种形式,你更喜欢那种?

//扩展方法
var result = source.Add(added);
//静态帮助类方法
var result = ArrayHelper.Add(source, added);

现在还有一个问题,这个方法是以返回值的方式返回最后的结果呢?还是直接修改原数组呢?两种方式各有优点,返回新数组,则原数组不变便于链式调用也避免一些副作用,直接修改原数组内存效率高。

我们的两个方法是新增元素和删除元素,其语义更贴合对原始数据进行操作其结果也作用在自身。因此我更倾向无返回值的方式。

那现在有个尴尬的问题,不知道你还记得我们上一章节《C#|.net core 基础 - 值传递 vs 引用传递》讲的值传递和引用传递,这里就有个这样的问题,如果我们现在想用扩展方法并且无返回值直接修改原数组,那么需要对扩展方法第一个参数使用ref修饰符,但是扩展方法对此有限制要求【第一个参数必须是struct 或是被约束为结构的泛型类型】,显示泛型数组不满足这个限制。因此无法做到我心目中最理想的封装方式了,下面看看扩展方法和帮助类的代码实现,可以按需使用吧。

public static class ArrayExtensions
{
    public static T[] AddRange<T>(this T[] source, T[] added)
    {
        var size = source.Length + added.Length;
        var array = new T[size];
        Array.Copy(source, array, source.Length);
        Array.Copy(added, 0, array, source.Length, added.Length);
        return array;
    }
    public static T[] RemoveAll<T>(this T[] source, Predicate<T> match)
    {
        return Array.FindAll(source, a => !match(a));
    }
}
public static class ArrayHelper
{
    public static void AddRange<T>(ref T[] source, T[] added)
    {
        var size = source.Length + added.Length;
        var array = new T[size];
        Array.Copy(source, array, source.Length);
        Array.Copy(added, 0, array, source.Length, added.Length);
        source = array;
    }
    public static void RemoveAll<T>(ref T[] source, Predicate<T> match)
    {
        source = Array.FindAll(source, a => !match(a));
    }
}


:测试方法代码以及示例源码都已经上传至代码库,有兴趣的可以看看。
https://gitee.com/hugogoos/Planner