2024年7月

VMware Cloud Foundation 管理域部署要求至少准备 4 台 ESXi 主机作为最小计算单元,如果采用整合部署(管理域和 VI 工作负载域合并),还需要根据实际情况适量增加 ESXi 计算主机。但是,对于测试学习来说,我们不需要准备这么多物理主机,可以采用嵌套虚拟化部署的方式来完成实验目的。

虽然嵌套虚拟机的确可以很容易的部署我们所需要的环境,但是要部署一套完整的 VMware Cloud Foundation 解决方案,对嵌套虚拟机所在的物理宿主机的配置和性能要求可不低,这在一定程度上增加了学习和使用的门槛。所以,要是当前没有条件的小伙伴,不妨可以先试试
VMware Hands-on Labs
体验一下官方的模拟环境。

一、准备嵌套 ESXi 主机环境

1)物理 ESXi 主机信息

本次准备用于部署 VCF 嵌套实验环境的物理宿主机的配置信息如下图所示。其实,部署 VCF 环境主要对内存的大小要求比较高,部署完整的管理域相关组件下来差不多就要占用 200 GB左右内存,而对 CPU 和存储的需求可以根据实际情况适当进行分配。

2)物理 ESXi 主机网络配置信息

VCF 部署要求不同的网络流量类型使用不同的 VLAN 进行隔离,这对于有物理交换机的环境来说可以很方便的进行配置,但是如果没有这种环境,那可以参考下面这种方法在 ESXi 主机上创建一个没有连接网卡的标准交换机,然后在这个标准交换机上面再创建端口组来模拟实现相同的效果。

如下图所示,在物理宿主机上面创建了一个标准交换机 vSwitch2,并且没有连接任务物理适配器。在 vSwitch2 下面创建了两个主要的端口组 vcf-mgmt-vmotion 和 vcf-nsx-vsan,之前文章(
VMware Cloud Foundation Part 03:准备 Excel 参数表。
)中了解了 VCF 将按照 Excel 参数表中所选择的 Profile 配置文件,并根据 Profile 配置文件对 ESXi 主机所用于 VCF 网络流量的网卡进行分配,比如 Profile-2 配置文件将 ESXi 主机的 vmnic0 和 vmnic1 网卡用作管理网络和 vMotion 网络,将 vmnic2 和 vmnic3 网卡用于 NSX Overlay 网络和 vSAN 网络,而这里所创建的两个端口组的可以达到分离的目的;另外四个端口组分别用于这几种网络类型的虚拟网关。

关于这个 vSwitch2 标准交换机的设置,请在“安全”配置下将混杂模式、MAC 地址更改以及伪传输的功能调整为“接受”。

网络端口组 vcf-mgmt-vmotion 用于 VCF 管理网络和 vMotion 网络的流量传输,当然除了分配给嵌套 ESXi 主机以外,用于部署 VCF 管理域的 Cloud Builder(vcf-builder) 也连接到这个网络,还有提供 VCF 管理相关组件外部服务的 DNS 和 NTP 服务器(同一虚拟机 vcf-dns)也连接了该端口组,最后还有一个跳板机(vcf-win11)也连接到了该端口组,由于无法从外部访问到该交换机上面的网络,所以需要一个同属于该网络上面的跳板机去访问 Cloud Builder 工具并部署 VCF 管理域以及后续的管理。

关于这个 vcf-mgmt-vmotion 端口组的设置,请一定要将 VLAN ID 配置为“全部(4095)”,并在“安全”配置下将混杂模式、MAC 地址更改以及伪传输的功能调整为“接受”。

网络端口组 vcf-mgmt-vmotion 用于 VCF 的 NSX Overlay 网络和 vSAN 网络,主要分配给嵌套 ESXi 主机使用。

关于这个 vcf-nsx-vsan 端口组的设置,请一定要将 VLAN ID 配置为“全部(4095)”,并在“安全”配置下将混杂模式、MAC 地址更改以及伪传输的功能调整为“接受”。

其他 VMkernel 端口组是用于 VCF 几种网络类型的虚拟网关,这是可选项,没有网关只会提示警告并不会影响 VCF 管理域的部署。

关于这几个 VMkernel 端口组的设置,请根据不同网络类型的设定配置不同 VLAN ID ,并在“安全”配置下将混杂模式、MAC 地址更改以及伪传输的功能调整为“接受”。

3)虚拟机汇总信息

创建了一个 vcf 虚拟机文件夹,在这个文件夹下面创建了 mgmt workload domain 和 vi workload domain 文件夹分别用于放置不同工作负载域的嵌套 ESXi 主机,如下图所示。在 vcf 文件夹下面有用于部署 VCF 管理域的 Cloud Builder 虚拟机(vcf-builder)、DNS 和 NTP 服务器(vcf-dns)以及跳板机(vcf-win11)。在 mgmt workload domain 文件夹下面创建了用于部署 VCF 管理域的嵌套 ESXi 虚拟机(vcf-mgmt01-esxi01~vcf-mgmt01-esxi04),vi workload domain 文件夹后续可能会用于放置部署 VI 管理域的嵌套 ESXi 虚拟机。

4)嵌套 ESXi 虚拟机配置信息

关于创建用于部署 VCF 管理域的嵌套 ESXi 虚拟机(vcf-mgmt01-esxi01~vcf-mgmt01-esxi04)的配置信息,如下图所示。

每个嵌套 ESXi 虚拟机分配了 16 CPU,并开启了硬件虚拟化。

每个嵌套 ESXi 虚拟机分配了 96 GB 内存,VCF 对 ESXi 主机内存的需求比较大,如果配置较低一点的内存应该也能部署成功,不过如果后面要部署 VI 工作负载域并在上面运行 VI 工作负载域管理相关虚拟机,建议分配高一点。

每个嵌套 ESXi 虚拟机分配了一块 60 GB 的硬盘用于安装 ESXi 系统,类型是厚置备延迟置零,并配置单独了 NVMe 控制器。

每个嵌套 ESXi 虚拟机分配了两块 500 GB 的硬盘用于 vSAN ESA 存储,类型是精简置备,并配置单独了 NVMe 控制器。

每个嵌套 ESXi 虚拟机分配了两个 NVMe 控制器分别用于 ESXi 系统和 vSAN 存储,建议将两种类型的存储分开。

每个嵌套 ESXi 虚拟机分配了四张网卡,前面两张网卡用于管理网络和 vMotion 网络,后面两张网卡用于 NSX 网络和 vSAN 网络。

二、规划嵌套 ESXi 主机地址

规划嵌套 ESXi 主机的管理网络,DNS 和 NTP 服务器为同一个,请一定提前在 DNS 服务器上配置好嵌套 ESXi 主机的正反向域名解析。

主机名 IP 地址 子网掩码 网关 DNS/NTP 服务器
vcf-mgmt01-esxi01.mulab.local 192.168.32.61 255.255.255.0 192.168.32.254 192.168.32.3
vcf-mgmt01-esxi02.mulab.local 192.168.32.62 255.255.255.0 192.168.32.254 192.168.32.3
vcf-mgmt01-esxi03.mulab.local 192.168.32.63 255.255.255.0 192.168.32.254 192.168.32.3
vcf-mgmt01-esxi04.mulab.local 192.168.32.64 255.255.255.0 192.168.32.254 192.168.32.3

三、配置嵌套 ESXi 主机信息

关于嵌套 ESXi 主机系统的安装过程,直接就跳过了,有需要可以查阅
官方文档
,所以从 ESXi 主机的配置开始,下面将以一台主机的配置为例,请根据这些步骤执行相同的操作,这里就不再赘述。

1)配置 ESXi 地址

安装完 ESXi 系统后,通过 DCUI 配置 ESXi 主机的地址信息。按 F2 输入安装过程设置的 Root 密码进入配置界面。

选择“Configure Management Network”选项,配置管理网络。

网卡和 VLAN 默认即可,选择“IPv4 Configuration”选项配置 ESXi 主机管理地址。

选择静态 IPv4 地址并输入之前规划好的 ESXi 主机 IP 地址、子网掩码和网关。

选择“DNS Configuration”选项配置 ESXi 主机的 DNS 服务器和主机名。

选择“Custom DNS Suffixes”选项配置 ESXi 主机的 DNS 后缀。

按 ESC 退出并选择 YES 保存配置。

2)配置 NTP 服务

通过 ESXi 主机的管理地址访问并登录到 ESXi Host Client 管理界面。

导航到主机->管理->系统->时间和日期,点击“编辑 NTP 设置”,输入 NTP 服务器的地址并选择随主机启动和停止。

导航到主机->管理->服务,找到 ntpd 服务,点击“启动”服务。

3)配置 SSL 证书

ESXi 主机安装完系统后,导航到主机->管理->安全和用户->证书,可以看到主机 SSL 证书默认是以 localhost.localdomain 域名进行签发的,需要将 SSL 证书重新生成我们配置的主机名。

导航到主机->管理->服务,找到 SSH 服务,点击“启动”服务。

通过 SSH 以 Root 用户登录到 ESXi 主机的命令行,使用以下命令重新生成新的 SSL 证书。

/sbin/generate-certificates
/etc/init.d/hostd restart && /etc/init.d/vpxa restart

如果你不想一个一个 SSH 登录到 ESXi 主机,也可以运行下面的 Powershell 脚本(管理员身份)来重新生成新的 SSL 证书。

# Install the Posh-SSH module if not already installed
Install-Module -Name Posh-SSH -Force

# Define the ESXi host details
$esxiHost = "192.168.32.62"
$username = "root"
$password = "Vcf5@password"

# Convert the password to a secure string
$securePassword = ConvertTo-SecureString $password -AsPlainText -Force

# Create the credential object
$credential = New-Object System.Management.Automation.PSCredential ($username, $securePassword)

# Establish the SSH session
$session = New-SSHSession -ComputerName $esxiHost -Credential $credential

# Run the command to regenerate certificates
Invoke-SSHCommand -SessionId $session.SessionId -Command '/sbin/generate-certificates'

# Restart the hostd service
Invoke-SSHCommand -SessionId $session.SessionId -Command '/etc/init.d/hostd restart && /etc/init.d/vpxa restart'

# Close the SSH session
Remove-SSHSession -SessionId $session.SessionId

执行上述步骤后,重新登录到 ESXi ,导航到主机->管理->安全和用户->证书,可以看到主机 SSL 证书已经变成主机名。

本文分享自华为云社区
《【华为云MySQL技术专栏】GaussDB(for MySQL)与MySQL的COUNT查询并行优化策略》
,作者:GaussDB 数据库。

1.背景介绍

统计表的行数(COUNT)是客户应用和DBA运维常用的操作。MySQL虽是业界广泛使用的OLTP数据库,但大表执行COUNT操作非常耗时,原因在于:

(1) COUNT操作需要遍历表的全量数据来获取精确的行数,当表数据量较大或部分数据不在Buffer Pool时,查询操作很耗时。

(2) MySQL 8.0.14之前的版本无并行查询技术,只能串行执行SQL语句,无法利用多核技术进行加速。

(3) MySQL 8.0.14及后续版本InnoDB存储引擎支持并行扫描主键,但不支持并行扫描二级索引,在主键很大、二级索引较小的场景下,相比老版本(MySQL 5.7)串行扫描二级索引,社区版本并行扫描可能出现性能劣化,并且不支持关闭并行扫描主键特性。

GaussDB(for MySQL)通过自研并行查询(PQ)和计算下推(NDP)特性,解决了大表COUNT慢的问题,典型场景下,相比MySQL并行扫描主键性能可提升超过80倍。

2. MySQL COUNT并行介绍

MySQL8.0.14版本InnoDB存储引擎支持并行扫描主键,这样可以利用并行的能力对COUNT操作进行加速,特性说明参见图1。

1.PNG

图1 MySQL 8.0 InnoDB存储引擎并行扫描主键特性

2.1原理介绍

MySQL COUNT并行在InnoDB存储引擎层实现的框架图参见图2。优化器决策走COUNT并行后,生成COUNT并行算子“UnqualifiedCountIterator”, 调用handler API接口“handler::ha_records”,InnoDB层在函数“Parallel_reader::parallel_read”中调度worker线程进行拆分、扫描、计数汇总。

2.png

图2 InnoDB 并行扫描调度逻辑

下面基于MySQL 8.0.14源码,介绍COUNT并行在SQL引擎和InnoDB存储引擎中的实现。

2.1.1 COUNT并行在SQL引擎中的实现

(1)SQL引擎层在优化阶段判断SQL是否为简单的COUNT,记录在变量“JOIN:: select_count”中,变量的定义参见下方代码。

/*When join->select_count is set, tables will not be optimized away.
The call to records() will be delayed until the execution phase and the counting will be done on an index of Optimizer's choice.
The index will be decided in find_shortest_key(), called from
optimize_aggregated_query().
*/ bool JOIN::select_count{false};

(2)SQL引擎层在生成执行计划阶段,判断变量“JOIN::select_count”的值,如果变量值为TRUE,则生成并行COUNT算子“UnqualifiedCountIterator”,用户可以通过“EXPLAIN FORMAT=TREE”或“EXPLAIN ANALYZE”命令查看执行计划,如果包含“Count rows”关键字说明 COUNT 并行生效,参见下面的执行计划。

mysql> explain format=tree select  count(*) fromlineitem\G*************************** 1. row  ***************************EXPLAIN:-> Count rows in lineitem
2.1.2 COUNT并行在InnoDB 存储引擎中的实现

(1) SQL引擎调用handler API 接口“handler::ha_records”,传递优化器选择的索引给InnoDB存储引擎,获取COUNT结果。

(2) InnoDB存储引擎只支持主键的并行扫描,函数“ha_innobase::records_from_index”忽略索引信息,强制选择主键进行并行扫描。

(3) InnoDB存储引擎在函数“Parallel_reader::parallel_read”中对主键索引进行初步分片,并调度 worker 线程对分片进一步拆分、扫描、计数。

(4) 我们把InnoDB中响应“handler::ha_records”接口并调度worker进行工作的的线程称为leader线程,leader线程调用堆栈信息如下:

UnqualifiedCountIterator::Read
get_exact_record_count
handler::ha_records
ha_innobase::records_from_index
ha_innobase::records
row_scan_index_for_mysql
row_mysql_parallel_select_count_star
Parallel_reader::run
Parallel_reader::parallel_read

(5) 我们把InnoDB中响应“Parallel_reader::worker”接口并进行扫描、计数工作的线程称为worker线程,worker线程的并发度可以通过参数“ innodb_parallel_read_threads”控制,worker线程调用堆栈信息如下:

Parallel_reader::worker
Parallel_reader::Ctx::traverse
Parallel_reader::Ctx::traverse_recs

2.2 性能提升效果

我们使用4U16G规格ECS实例,部署MySQL Community 8.0.14版本,innodb_buffer_pool_size设置为8GB。采用TPC-H测试模型,Scale Factor(Gigabytes)为20,lineitem表主键大小约17.4GB,二级索引i_l_orderkey大小约2.3GB,二级索引i_l_partkey_suppkey大小约3.3GB,表结构如下:

mysql>show create table lineitem\G*************************** 1. row  ***************************Table: lineitemCreate Table: 
CREATE TABLE `lineitem` (
`L_ORDERKEY` bigint NOT NULL,
`L_PARTKEY`
intNOT NULL,
`L_SUPPKEY`
intNOT NULL,
`L_LINENUMBER`
intNOT NULL,
`L_QUANTITY`
decimal(15,2) NOT NULL,
`L_EXTENDEDPRICE`
decimal(15,2) NOT NULL,
`L_DISCOUNT`
decimal(15,2) NOT NULL,
`L_TAX`
decimal(15,2) NOT NULL,
`L_RETURNFLAG`
char(1) NOT NULL,
`L_LINESTATUS`
char(1) NOT NULL,
`L_SHIPDATE` date NOT NULL,
`L_COMMITDATE` date NOT NULL,
`L_RECEIPTDATE` date NOT NULL,
`L_SHIPINSTRUCT`
char(25) NOT NULL,
`L_SHIPMODE`
char(10) NOT NULL,
`L_COMMENT` varchar(
44) NOT NULL,
PRIMARY KEY (`L_ORDERKEY`,`L_LINENUMBER`),
KEY `i_l_orderkey` (`L_ORDERKEY`),
KEY `i_l_partkey_suppkey` (`L_PARTKEY`,`L_SUPPKEY`)
) ENGINE
=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

lineitem表的主键约17GB,无法全部加载到Buffer Pool中,每次COUNT执行触发的磁盘IO基本相同(约82万次)。在这个场景下,提升InnoDB并行扫描并发度(innodb_parallel_read_threads),COUNT性能可以线性提升,1并发执行时间约585秒,2并发执行时间约300秒,4并发执行时间约145秒,数据参见图3。

3.png

图3 MySQL 8.0 COUNT并行提升效果

2.3 约束限制

(1) 社区MySQL COUNT并行在InnoDB存储引擎实现,只支持主键的并行扫描,忽略了优化器选择的最佳索引。当一个表主键很大、二级索引较小,相比老版本(MySQL 5.7)串行扫描二级索引,社区并行无优化效果。

(2) 社区MySQL COUNT并行只支持无WHERE条件的COUNT,原因在于InnoDB存储无法进行过滤计算。

(3) 当扫描主键数据量很大时,可能会淘汰Buffer Pool中的热数据,导致后续的性能波动。

(4) 社区MySQL COUNT并行强制生效,无法关闭,当遇到(1)中的性能问题时,无法回退至串行扫描二级索引。

使用2.2节相同的测试环境和测试模型,执行“SELECT COUNT(*) FROM lineitem” SQL语句,对比MySQL 5.7.44版本与MySQL 8.0.14版本执行时间,数据参见表1。

表1 MySQL 5.7.44与8.0.14版本COUNT执行时间对比

1.PNG

在这个场景下,MySQL 8.0版本使用4并发扫描主键,但是由于扫描的数据量较大,触发大量的磁盘IO,导致性能差于MySQL 5.7串行扫描二级索引。

3. GaussDB(for MySQL) COUNT 优化

针对MySQL COUNT并行存在的问题,GaussDB(for MySQL)进行了针对性优化,通过自研的并行查询(PQ)和计算下推(NDP)特性,实现了三层并行,加快COUNT执行。框架图参见图4。

  • 第一层并行: SQL引擎层,通过自研并行查询,利用多核计算加速;

  • 第二层并行:InnoDB存储引擎层,通过自研计算下推特性,触发批量读请求,SAL层将批量读的Page组装、打包,并发将读请求发送至分布式存储(Page Store);

  • 第三层并行:Page Store接受到读请求后,每个Page Store内部并发响应读请求,待页面扫描、过滤、聚合操作完成后,将结果返回至计算层。

    5.png

图4 GaussDB(for MySQL) COUNT并行优化

3.1 原理介绍

下面介绍下GaussDB(for MySQL) COUNT优化细节。

3.1.1 支持动态关闭社区MySQL COUNT并行

当遇到2.3节的性能问题时,可以通过调整参数“innodb_parallel_select_count”动态关闭或开启MySQL COUNT并行功能,使用方法如下:

mysql> SET  innodb_parallel_select_count=OFF;mysql> EXPLAIN FORMAT=TREE SELECT  COUNT(*) FROM lineitem\G*************************** 1. row ***************************EXPLAIN:-> Aggregate: count(0)-> Index scan on lineitem using i_l_orderkey  (cost=12902405.32 rows=118641035)

3.1.2 GaussDB(for MySQL)并行查询特性

GaussDB(for MySQL)支持并行查询(PQ)[1],用以降低分析型查询场景的处理时间,满足企业级应用对查询低时延的要求。相比社区MySQL并行查询的诸多限制,GaussDB(for MySQL)自研的并行查询支持主键、二级索引多种扫描方式,适用于大部分SELECT语句。

针对COUNT操作,可以利用PQ特性,并行扫描二级索引,提升查询性能。

用户可以通过Hint的方式开启PQ,当执行计划中出现Parallel、Gather关键字时,说明PQ特性生效。使用方法如下:

mysql>  EXPLAIN FORMAT=TREE SELECT/*+ PQ()*/ COUNT(*) FROM lineitem\G***************************  1. row ***************************EXPLAIN:-> Aggregate: count(`<temporary>`.`0`)-> Gather: 4workers, parallel scan on  lineitem-> Aggregate: count(`<temporary>`.`0`)-> Parallel index scan on lineitem using i_l_orderkey  (cost=4004327.70 rows=29660259)

3.1.3 GaussDB(for MySQL)计算下推特性

计算下推(Near Data Processing)[2]是GaussDB(for MySQL)提高数据复杂查询效率的解决方案。针对数据密集型查询,将列投影、聚合运算、条件过滤等操作从计算节点向下推送给分布式存储层的多个节点,并行执行。通过计算下推方法,提升了并行处理能力,减少网络流量和计算节点的压力,提高了查询处理执行效率。

针对COUNT操作,可以利用NDP特性,将聚合操作下推至分布式存储,减少网络流量,提升查询性能。

用户可以通过Hint的方式开启NDP,执行计划中出现NDP 关键字时,说明此特性生效。使用方法如下:

mysql> EXPLAIN FORMAT=TREE SELECT/*+  PQ() NDP_PUSHDOWN()*/ COUNT(*) FROM lineitem\G*************************** 1. row  ***************************EXPLAIN:-> Aggregate:  count(`<temporary>`.`0`)-> Gather: 4workers, parallel scan on lineitem-> Aggregate:  count(`<temporary>`.`0`)-> Parallel index scan  on lineitem using i_l_orderkey Using  pushed NDP (aggregate)   (cost=4046562.45 rows=29047384)

3.2性能优化效果

使用2.2节相同的测试环境和测试模型,执行“SELECT COUNT(*) FROM lineitem” SQL语句,对比GaussDB(for MySQL)开启PQ特性与开启PQ+NDP特性的执行时间,参见表2。

表2 GaussDB(for MySQL) COUNT操作执行时间

2222.PNG

从测试结果看:只开启PQ特性,并行查询并发度设置为4,磁盘IO约13万次,查询耗时约31秒;

同时开启PQ和NDP特性,并行查询并发度设置为4,NDP通过IO合并和计算下推,大幅减少了磁盘IO,查询耗时只有1.7秒,相比社区MySQL 8.0.22 执行耗时145秒,COUNT性能提升超过80倍。

7.png

图5 GaussDB(for MySQL) COUNT优化提升效果

4.总结

社区MySQL 8.0引入了并行扫描主键功能,但不支持并行扫描二级索引,导致在大表或冷数据场景(表页面数据不在Buffer Pool)反而出现劣化,GaussDB(for MySQL)通过并行查询(PQ)和计算下推(NDP)特性,解决了大表COUNT慢的问题,典型场景下相比社区并行,性能提升超过80倍,为用户提供更加极致的体验。

5.相关参考

[1] 并行查询(PQ)https://support.huaweicloud.com/usermanual-gaussdbformysql/gaussdbformysql_05_0150.html

[2] 算子下推(NDP)https://support.huaweicloud.com/usermanual-gaussdbformysql/gaussdbformysql_05_0129.html

点击关注,第一时间了解华为云新鲜技术~

前言

vue3的响应式API大家应该都特别熟悉,比如
ref

watch

watchEffect
等。平时大家都是在
vue-cli
或者
vite
创建的vue项目里面使用的这些响应式API,今天欧阳给大家带来一些不一样的。
脱离vue项目,在
node.js
项目中使用vue的响应式API

关注公众号:【前端欧阳】,给自己一个进阶vue的机会

直接上代码

话不多说,直接上代码。这个是我在本地新建的一个最简单的
node.js
项目,如下图:
node

从上图可以看到我们的
node.js
项目依赖只有一个:
vue
。并且提供了一个名为dev的脚本命令,这个脚本命令实际是在node环境内执行
index.js
文件。

我们来看
index.js
文件,代码如下:

const { ref, watch, watchEffect } = require("vue");

const count = ref(0);

// 模拟count变量的值修改
setInterval(() => {
  count.value++;
}, 1000);

watch(count, (newVal) => {
  console.log("触发watch", newVal);
});

watchEffect(
  () => {
    console.log("触发watchEffect", count.value);
  },
  {
    flush: "sync",
  }
);

为了标明
index.js
文件是在
node.js
环境中运行的,所以这里我特地使用
require
去导入vue导出的
ref

watch

watchEffect
这三个响应式API。

并且我们还模拟了修改
count
响应式变量值的操作,使用
setInterval
每隔一秒让
count
的值
+1

在vue项目中一样使用
watch

watchEffect
去监听
count
变量的值。

在终端执行
yarn dev
,也就是执行
node index.js
,如下图:
run

从上图中可以看到在
node.js
中的执行结果和预期是一模一样的。

为什么可以这样写呢?

前面的那个例子是一个
node.js
项目,项目中我们并没有像vue项目那样去创建一个vue组件,然后在组件里面去使用响应式API。而是直接在一个普通的
node.js
文件中使用vue暴露出来的响应式API,并且
watch

watchEffect
在监听的值改变后同样触发了对应的watch回调,那么这个又是怎么做到的呢?

这得益于vue3优秀的模块化设计,他将核心功能拆分为多个独立的模块,如下图:
packages

比如
reactivity
模块中就是响应式的核心代码、
compiler-core
模块就是编译相关的核心代码。

并且这些模块还被单独当作npm包进行发布,命名规则是
@vue+模块名
。比如
reactivity
模块对应的npm包就是
@vue/reactivity
。如下图:
npm

得益于模块化的设计,响应式相关的API和vue组件并没有强关联的关系,所以我们可以在
node.js
应用中去直接使用响应式API。

这里使用到了三个响应式API,分别是:
ref

watch

watchEffect
。在vue组件中的响应式的实现原理大家多多少少都有所听闻,其实在
node.js
项目中实现原理也是一样的,接下来我们讲讲是如何实现响应式的。

在我们这个demo中
count
是一个ref的响应式变量,当我们对
count
变量进行读操作时会触发
get
拦截。当我们对
count
变量进行写操作时会触发
set
拦截。

在我们这里使用
watch

watchEffect
的代码是下面这样的:

watch(count, (newVal) => {
  console.log("触发watch", newVal);
});

watchEffect(
  () => {
    console.log("触发watchEffect", count.value);
  },
  {
    flush: "sync",
  }
);

当代码首次执行到
watch

watchEffect
时都会对
count
变量进行读操作,并且
watch

watchEffect
都传入了一个回调函数。

由于对
count
变量进行读操作了,所以就会触发
get
拦截。在
get
拦截中会将当前
watch的回调函数
作为依赖收集到
count
变量中。收集的方式也很简单,因为
count
变量是一个对象,所以使用对象的
dep
属性进行依赖收集。因为
dep
属性是一个集合,所以可以收集多个依赖。

在我们这里
watch

watchEffect
都触发了
count
变量的get拦截,所以
watch

watchEffect
的回调函数都被
count
变量进行了依赖收集。

当修改
count
变量的值时会触发
set
拦截,在
set
拦截中做的事情也很简单。将
count
变量收集到的依赖全部取出来,然后执行一遍。这里收集的依赖是
watch

watchEffect
的回调函数,所以当
count
变量的值改变时会导致
watch

watchEffect
的回调函数重新执行。

这个是整个流程图:
progress

从流程图可以看到响应式的实现原来完全不依赖vue组件,所以我们可以在
node.js
项目中使用vue的响应式API,这也是vue的设计奇妙之处。

总结

这篇文章讲了我们可以脱离vue项目,直接在
node.js
项目中使用vue的响应式API。接着讲了响应式的实现原理其实就是依靠
get
拦截进行依赖收集,
set
拦截进行依赖触发。

搞清楚响应式原理后,我们发现响应式完全不依赖vue组件,所以我们可以在
node.js
项目中使用vue的响应式API,这也是vue的设计奇妙之处。

关注公众号:【前端欧阳】,给自己一个进阶vue的机会

大家好,我是码农先森。

从 PHP 转到 Go 的朋友,常常会因为没有便捷的工具函数而感到苦恼。PHP 写的多了就会形成路径依赖,在写 Go 的时候时不时就会想到 PHP 强大的数组函数。当然写 Go 的 PHPer 也想拥有这么强大的工具函数,在这个开源的编程世界总有一些伟大的程序员做着贡献,正是有了他们的存在,因此便有了 lancet 这样一个强大的 Go 语言工具函数库,下面我介绍一些常用的工具函数「文末附上了手册地址」。

ContainChinese
验证字符串是否包含中文字符。

import (
    "fmt"
    "github.com/duke-git/lancet/v2/validator"
)

func main() {
    result1 := validator.ContainChinese("你好")
    result2 := validator.ContainChinese("你好hello")
    result3 := validator.ContainChinese("hello")

    fmt.Println(result1)
    fmt.Println(result2)
    fmt.Println(result3)

    // Output:
    // true
    // true
    // false
}

IsChineseMobile
验证字符串是否是中国手机号码。

import (
    "fmt"
    "github.com/duke-git/lancet/v2/validator"
)

func main() {
    result1 := validator.IsChineseMobile("13263527980")
    result2 := validator.IsChineseMobile("434324324")

    fmt.Println(result1)
    fmt.Println(result2)

    // Output:
    // true
    // false
}

IsChineseIdNum
验证字符串是否是中国身份证号码。

import (
    "fmt"
    "github.com/duke-git/lancet/v2/validator"
)

func main() {
    result1 := validator.IsChineseIdNum("210911192105130715")
    result2 := validator.IsChineseIdNum("123456")

    fmt.Println(result1)
    fmt.Println(result2)

    // Output:
    // true
    // false
}

IsChinesePhone
验证字符串是否是中国电话座机号码。

import (
    "fmt"
    "github.com/duke-git/lancet/v2/validator"
)

func main() {
    result1 := validator.IsChinesePhone("010-32116675")
    result2 := validator.IsChinesePhone("123-87562")

    fmt.Println(result1)
    fmt.Println(result2)

    // Output:
    // true
    // false
}

IsCreditCard
验证字符串是否是信用卡号码。

import (
    "fmt"
    "github.com/duke-git/lancet/v2/validator"
)

func main() {
    result1 := validator.IsCreditCard("4111111111111111")
    result2 := validator.IsCreditCard("123456")

    fmt.Println(result1)
    fmt.Println(result2)

    // Output:
    // true
    // false
}

IsEmail
验证字符串是否是有效电子邮件地址。

import (
    "fmt"
    "github.com/duke-git/lancet/v2/validator"
)

func main() {
    result1 := validator.IsEmail("abc@xyz.com")
    result2 := validator.IsEmail("a.b@@com")

    fmt.Println(result1)
    fmt.Println(result2)

    // Output:
    // true
    // false
}

Contain
判断 slice 是否包含 value。

import (
    "fmt"
    "github.com/duke-git/lancet/v2/slice"
)

func main() {
    result1 := slice.Contain([]string{"a", "b", "c"}, "a")
    result2 := slice.Contain([]int{1, 2, 3}, 4)

    fmt.Println(result1)
    fmt.Println(result2)

    // Output:
    // true
    // false
}

Chunk
按照 size 参数均分 slice。

import (
    "fmt"
    "github.com/duke-git/lancet/v2/slice"
)

func main() {
    arr := []string{"a", "b", "c", "d", "e"}

    result1 := slice.Chunk(arr, 1)
    result2 := slice.Chunk(arr, 2)

    fmt.Println(result1)
    fmt.Println(result2)

    // Output:
    // [[a] [b] [c] [d] [e]]
    // [[a b] [c d] [e]]
}

Equal
检查两个切片是否相等,相等条件:切片长度相同,元素顺序和值都相同。

import (
    "fmt"
    "github.com/duke-git/lancet/v2/slice"
)

func main() {
    s1 := []int{1, 2, 3}
    s2 := []int{1, 2, 3}
    s3 := []int{1, 3, 2}

    result1 := slice.Equal(s1, s2)
    result2 := slice.Equal(s1, s3)

    fmt.Println(result1)
    fmt.Println(result2)

    // Output:
    // true
    // false
}

Comma
用逗号每隔3位分割数字/字符串,支持前缀添加符号。参数 value 必须是数字或者可以转为数字的字符串, 否则返回空字符串。

package main

import (
    "fmt"
    "github.com/duke-git/lancet/v2/formatter"
)

func main() {
    result1 := formatter.Comma("123", "")
    result2 := formatter.Comma("12345", "$")
    result3 := formatter.Comma(1234567, "¥")

    fmt.Println(result1)
    fmt.Println(result2)
    fmt.Println(result3)

    // Output:
    // 123
    // $12,345
    // ¥1,234,567
}

ForEach
对 map 中的每对 key和 value 执行 iteratee 函数。

package main

import (
    "fmt"
    "github.com/duke-git/lancet/v2/maputil"
)

func main() {
    m := map[string]int{
        "a": 1,
        "b": 2,
        "c": 3,
        "d": 4,
    }

    var sum int

    maputil.ForEach(m, func(_ string, value int) {
        sum += value
    })

    fmt.Println(sum)

    // Output:
    // 10
}

Keys
返回 map 中所有 key 的切片。

package main

import (
    "fmt"
    "github.com/duke-git/lancet/v2/maputil"
)

func main() {
    m := map[int]string{
        1: "a",
        2: "a",
        3: "b",
        4: "c",
        5: "d",
    }

    keys := maputil.Keys(m)
    sort.Ints(keys)

    fmt.Println(keys)

    // Output:
    // [1 2 3 4 5]
}

Merge
合并多个 maps 相同的 key 会被后来的 key 覆盖。

package main

import (
    "fmt"
    "github.com/duke-git/lancet/v2/maputil"
)

func main() {
    m1 := map[int]string{
        1: "a",
        2: "b",
    }
    m2 := map[int]string{
        1: "1",
        3: "2",
    }

    result := maputil.Merge(m1, m2)

    fmt.Println(result)

    // Output:
    // map[1:c 2:b 3:d]
}

RandInt
生成随机int, 范围[min, max)。

package main

import (
    "fmt"
    "github.com/duke-git/lancet/v2/random"
)

func main() {
    rInt := random.RandInt(1, 10)
    fmt.Println(rInt)
}

RandString
生成给定长度的随机字符串,只包含字母(a-zA-Z)。

package main

import (
    "fmt"
    "github.com/duke-git/lancet/v2/random"
)

func main() {
    randStr := random.RandString(6)
    fmt.Println(randStr) //pGWsze
}

以上这些只是部分的工具函数,如果想查看所有内容可以访问
https://www.golancet.cn/
该网站。

再强大的函数工具库也不可能涵盖所有的场景,随着现在 AI 的盛行,AI 的编程辅助工具也应运而生。在这里我再分享一个免费的 PHP 代码直接转换 Go 代码工具,这个工具简直是 PHP 程序员的福音,直接将 PHP 代码贴上便自动生成了 Go 代码。

最后附上这个 AI 工具的地址
https://www.codeconvert.ai/php-to-golang-converter
赶快去使用吧。


欢迎关注、分享、点赞、收藏、在看,我是微信公众号「码农先森」作者。

什么是端到端的深度学习?

深度学习中最令人振奋的最新动态之一就是端到端深度学习的兴起,那么端到端学习到底是什么呢?简而言之,以前有一些数据处理系统或者学习系统,它们需要多个阶段的处理。那么端到端深度学习就是忽略所有这些不同的阶段,用单个神经网络代替它。

来看一些例子,以语音识别为例,目标是输入
\(x\)
,比如说一段音频,然后把它映射到一个输出
\(y\)
,就是这段音频的听写文本。所以传统上,语音识别需要很多阶段的处理。首先会提取一些特征,一些手工设计的音频特征,也许听过
MFCC
,这种算法是用来从音频中提取一组特定的人工设计的特征。在提取出一些低层次特征之后,可以应用机器学习算法在音频片段中找到音位,所以音位是声音的基本单位,比如说“
Cat
”这个词是三个音节构成的,
Cu-

Ah-

Tu-
,算法就把这三个音位提取出来,然后将音位串在一起构成独立的词,然后将词串起来构成音频片段的听写文本。

所以和这种有很多阶段的流水线相比,端到端深度学习做的是,训练一个巨大的神经网络,输入就是一段音频,输出直接是听写文本。
AI
的其中一个有趣的社会学效应是,随着端到端深度学习系统表现开始更好,有一些花了大量时间或者整个事业生涯设计出流水线各个步骤的研究员,还有其他领域的研究员,不只是语言识别领域的,也许是计算机视觉,还有其他领域,他们花了大量的时间,写了很多论文,有些甚至整个职业生涯的一大部分都投入到开发这个流水线的功能或者其他构件上去了。而端到端深度学习就只需要把训练集拿过来,直接学到了
\(x\)

\(y\)
之间的函数映射,直接绕过了其中很多步骤。对一些学科里的人来说,这点相当难以接受,他们无法接受这样构建
AI
系统,因为有些情况,端到端方法完全取代了旧系统,某些投入了多年研究的中间组件也许已经过时了。

事实证明,端到端深度学习的挑战之一是,可能需要大量数据才能让系统表现良好,比如,只有3000小时数据去训练语音识别系统,那么传统的流水线效果真的很好。但当拥有非常大的数据集时,比如10,000小时数据或者100,000小时数据,这样端到端方法突然开始很厉害了。所以当数据集较小的时候,传统流水线方法其实效果也不错,通常做得更好。需要大数据集才能让端到端方法真正发出耀眼光芒。如果数据量适中,那么也可以用中间件方法,可能输入还是音频,然后绕过特征提取,直接尝试从神经网络输出音位,然后也可以在其他阶段用,所以这是往端到端学习迈出的一小步,但还没有到那里。

这张图上是一个研究员做的人脸识别门禁,是百度的林元庆研究员做的。这是一个相机,它会拍下接近门禁的人,如果它认出了那个人,门禁系统就自动打开,让他通过,所以不需要刷一个
RFID
工卡就能进入这个设施。系统部署在越来越多的办公室,可以接近门禁,如果它认出脸,它就直接让通过,不需要带
RFID
工卡。

那么,怎么搭建这样的系统呢?可以做的第一件事是,看看相机拍到的照片,对吧?想画的不太好,但也许这是相机照片,知道,有人接近门禁了,所以这可能是相机拍到的图像
\(x\)
。有件事可以做,就是尝试直接学习图像
\(x\)
到人物
\(y\)
身份的函数映射,事实证明这不是最好的方法。其中一个问题是,人可以从很多不同的角度接近门禁,他们可能在绿色位置,可能在蓝色位置。有时他们更靠近相机,所以他们看起来更大,有时候他们非常接近相机,那照片中脸就很大了。在实际研制这些门禁系统时,他不是直接将原始照片喂到一个神经网络,试图找出一个人的身份。

相反,迄今为止最好的方法似乎是一个多步方法,首先,运行一个软件来检测人脸,所以第一个检测器找的是人脸位置,检测到人脸,然后放大图像的那部分,并裁剪图像,使人脸居中显示,然后就是这里红线框起来的照片,再喂到神经网络里,让网络去学习,或估计那人的身份。

研究人员发现,比起一步到位,一步学习,把这个问题分解成两个更简单的步骤。首先,是弄清楚脸在哪里。第二步是看着脸,弄清楚这是谁。这第二种方法让学习算法,或者说两个学习算法分别解决两个更简单的任务,并在整体上得到更好的表现。

顺便说一句,如果想知道第二步实际是怎么工作的,这里其实省略了很多。训练第二步的方式,训练网络的方式就是输入两张图片,然后网络做的就是将输入的两张图比较一下,判断是否是同一个人。比如记录了10,000个员工
ID
,可以把红色框起来的图像快速比较……也许是全部10,000个员工记录在案的
ID
,看看这张红线内的照片,是不是那10000个员工之一,来判断是否应该允许其进入这个设施或者进入这个办公楼。这是一个门禁系统,允许员工进入工作场所的门禁。

为什么两步法更好呢?实际上有两个原因。一是,解决的两个问题,每个问题实际上要简单得多。但第二,两个子任务的训练数据都很多。具体来说,有很多数据可以用于人脸识别训练,对于这里的任务1来说,任务就是观察一张图,找出人脸所在的位置,把人脸图像框出来,所以有很多数据,有很多标签数据
\((x,y)\)
,其中
\(x\)
是图片,
\(y\)
是表示人脸的位置,可以建立一个神经网络,可以很好地处理任务1。然后任务2,也有很多数据可用,今天,业界领先的公司拥有,比如说数百万张人脸照片,所以输入一张裁剪得很紧凑的照片,比如这张红色照片,下面这个,今天业界领先的人脸识别团队有至少数亿的图像,他们可以用来观察两张图片,并试图判断照片里人的身份,确定是否同一个人,所以任务2还有很多数据。相比之下,如果想一步到位,这样
\((x,y)\)
的数据对就少得多,其中
\(x\)
是门禁系统拍摄的图像,
\(y\)
是那人的身份,因为没有足够多的数据去解决这个端到端学习问题,但却有足够多的数据来解决子问题1和子问题2。

实际上,把这个分成两个子问题,比纯粹的端到端深度学习方法,达到更好的表现。不过如果有足够多的数据来做端到端学习,也许端到端方法效果更好。但在今天的实践中,并不是最好的方法。

再来看几个例子,比如机器翻译。传统上,机器翻译系统也有一个很复杂的流水线,比如英语机翻得到文本,然后做文本分析,基本上要从文本中提取一些特征之类的,经过很多步骤,最后会将英文文本翻译成法文。因为对于机器翻译来说的确有很多(英文,法文)的数据对,端到端深度学习在机器翻译领域非常好用,那是因为在今天可以收集
\(x-y\)
对的大数据集,就是英文句子和对应的法语翻译。所以在这个例子中,端到端深度学习效果很好。

最后一个例子,比如说希望观察一个孩子手部的X光照片,并估计一个孩子的年龄。知道,当第一次听到这个问题的时候,以为这是一个非常酷的犯罪现场调查任务,可能悲剧的发现了一个孩子的骨架,想弄清楚孩子在生时是怎么样的。事实证明,这个问题的典型应用,从X射线图估计孩子的年龄,是想太多了,没有想象的犯罪现场调查脑洞那么大,结果这是儿科医生用来判断一个孩子的发育是否正常。

处理这个例子的一个非端到端方法,就是照一张图,然后分割出每一块骨头,所以就是分辨出那段骨头应该在哪里,那段骨头在哪里,那段骨头在哪里,等等。然后,知道不同骨骼的长度,可以去查表,查到儿童手中骨头的平均长度,然后用它来估计孩子的年龄,所以这种方法实际上很好。

相比之下,如果直接从图像去判断孩子的年龄,那么需要大量的数据去直接训练。据所知,这种做法今天还是不行的,因为没有足够的数据来用端到端的方式来训练这个任务。

可以想象一下如何将这个问题分解成两个步骤,第一步是一个比较简单的问题,也许不需要那么多数据,也许不需要许多X射线图像来切分骨骼。而任务二,收集儿童手部的骨头长度的统计数据,不需要太多数据也能做出相当准确的估计,所以这个多步方法看起来很有希望,也许比端对端方法更有希望,至少直到能获得更多端到端学习的数据之前。

所以端到端深度学习系统是可行的,它表现可以很好,也可以简化系统架构,让不需要搭建那么多手工设计的单独组件,但它也不是灵丹妙药,并不是每次都能成功。