作者选择女性工程师协会接受捐赠,作为“为捐赠而写”计划的一部分。
介绍
当您在具有多个 CPU 的系统上运行Node.js程序时,它会创建一个默认情况下仅使用单个 CPU 执行的进程。由于 Node.js 使用单个线程来执行 JavaScript 代码,因此对应用程序的所有请求都必须由在单个 CPU 上运行的线程来处理。如果应用程序有 CPU 密集型任务,操作系统必须安排它们共享单个 CPU,直到完成。如果单个进程收到太多请求,可能会导致其不堪重负,从而降低性能。如果进程崩溃,用户将无法访问您的应用程序。
作为解决方案,Node.js 引入了该cluster
模块,该模块在同一台计算机上创建同一应用程序的多个副本并让它们同时运行。它还配备了一个负载平衡器,可以使用循环算法在进程之间均匀分配负载。如果单个实例崩溃,用户可以由仍在运行的剩余进程提供服务。应用程序的性能显着提高,因为负载在多个进程之间均匀共享,从而防止单个实例不堪重负。
cluster
在本教程中,您将在具有四个或更多 CPU 的计算机上使用该模块扩展 Node.js 应用程序。您将创建一个不使用集群的应用程序,然后修改该应用程序以使用集群。您还将使用该pm2
模块跨多个 CPU 扩展应用程序。您将使用负载测试工具来比较使用集群的应用程序和未使用集群的应用程序的性能,并评估模块pm2
。
先决条件
要学习本教程,您将需要以下内容:
- 具有四个或更多 CPU 的系统。
- 在您的开发环境中设置 Node.js。如果您运行的是 Ubuntu 22.04,请按照如何在 Ubuntu 22.04 上安装 Node.js的选项 3 进行操作。对于其他系统,请访问如何安装 Node.js 和创建本地开发环境。
- 使用Express的基本知识,您可以通过如何开始使用 Node.js 和 Express 来刷新这些知识。
第 1 步 - 设置项目目录
在此步骤中,您将为项目创建目录,并下载本教程稍后将构建的应用程序的依赖项。在步骤 2中,您将使用 Express 构建应用程序。然后,您将在步骤 3中将其扩展到具有内置node-cluster
模块的多个 CPU,您将loadtest
在步骤 4中使用软件包进行测量。从那里,您将使用包装缩放它,并在步骤 5pm2
中再次测量。
首先,创建一个目录。您可以将其命名为cluster_demo
您喜欢的目录名称或任何目录名称:
接下来,进入目录:
然后,初始化项目,这也会创建一个package.json
文件:
该-y
选项告诉 NPM 接受所有默认选项。
当命令运行时,它将产生与以下内容匹配的输出:
Wrote to /home/sammy/cluster_demo/package.json:
{
"name": "cluster_demo",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
请注意这些与您的特定项目一致的属性:
name
:npm 包的名称。version
:你的包的版本号。main
:项目的入口点。
要了解有关其他属性的更多信息,您可以查看package.json
NPM 文档 的 部分。
接下来,使用package.json
您喜欢的编辑器打开文件(本教程将使用nano
):
在您的package.json
文件中,添加突出显示的文本以在导入包时启用对ES 模块的支持:
保存并关闭文件CTRL+X
。
接下来,您将下载以下软件包:
express
:用于在 Node.js 中构建 Web 应用程序的框架。loadtest
:一种负载测试工具,可用于生成应用程序的流量以测量其性能。pm2
:一种自动将应用程序扩展到多个 CPU 的工具。
执行以下命令下载Express包:
接下来,运行命令来全局下载loadtest
和pm2
包:
现在您已经安装了必要的依赖项,您将创建一个不使用集群的应用程序。
第 2 步 — 创建不使用集群的应用程序
在此步骤中,您将创建一个包含单个路由的示例程序,该路由将在每个用户访问时启动 CPU 密集型任务。该程序不会使用该cluster
模块,因此您可以了解在一个 CPU 上运行应用程序的单个实例的性能影响。cluster
您将在本教程后面将此方法与模块的性能进行比较。
使用nano
或您最喜欢的文本编辑器创建index.js
文件:
在您的index.js
文件中,添加以下行以导入并实例化 Express:
在第一行中,导入express
包。在第二行中,将port
变量设置为 port 3000
,应用程序的服务器将侦听该端口。接下来,将该app
变量设置为 Express 的实例。之后,您可以使用内置模块在控制台中记录应用程序进程的进程process
ID。
接下来,添加这些行来定义路由/heavy
,其中将包含一个受 CPU 限制的循环:
在该/heavy
路由中,您定义一个循环,将total
变量递增 500 万次。total
然后,您可以使用该方法发送包含变量中的值的响应res.send()
。虽然 CPU 密集型任务的示例是任意的,但它在不增加复杂性的情况下演示了 CPU 密集型任务。您还可以为路由使用其他名称,但本教程用于/heavy
指示繁重的性能任务。
接下来,调用listen()
Express 模块的方法让服务器侦听存储3000
在port
变量中的端口:
完整的文件将匹配以下内容:
添加完代码后,保存并退出文件。然后使用以下命令运行该文件node
:
运行该命令时,输出将匹配以下内容:
worker pid=11023
App listening on port 3000
输出显示正在运行的进程的进程 ID 以及确认服务器正在侦听 port 的消息3000
。
要测试应用程序是否正常工作,请打开另一个终端并运行以下命令:
注意:如果您在远程服务器上学习本教程,请打开另一个终端,然后输入以下命令:
连接后,输入以下命令以使用以下命令向应用程序发送请求curl
:
输出将匹配以下内容:
The result of the CPU intensive task is 5000000
输出提供 CPU 密集型计算的结果。
此时,您可以使用 停止服务器CTRL+C
。
当您使用命令运行该index.js
文件时node
,操作系统 (OS) 会创建一个进程。进程是操作系统为正在运行的程序所做的抽象。操作系统为程序分配内存,并在包含所有操作系统进程的进程列表中创建一个条目。该条目是一个进程 ID。
然后找到程序二进制文件并将其加载到分配给进程的内存中。从那里开始执行。当它运行时,它不知道系统中的其他进程,并且该进程中发生的任何事情都不会影响其他进程。
由于 Node.js 应用程序有一个进程运行在具有多个 CPU 的服务器上,因此它将接收并处理所有传入请求。在此图中,所有传入请求都定向到在单个 CPU 上运行的进程,而其他 CPU 保持空闲状态:
现在您已经在不使用该cluster
模块的情况下创建了一个应用程序,接下来您将使用该cluster
模块来扩展应用程序以使用多个 CPU。
第 3 步 — 对应用程序进行集群
在此步骤中,您将添加cluster
模块来创建同一程序的多个实例,以处理更多负载并提高性能。当您使用该cluster
模块运行进程时,您可以在计算机上的每个 CPU 上运行多个进程:
在此图中,请求经过主进程中的负载均衡器,然后使用循环算法在进程之间分配请求。
您现在将添加该cluster
模块。在您的终端中,创建primary.js
文件:
在您的primary.js
文件中,添加以下行以导入依赖项:
在前两行中,导入cluster
和os
模块。在以下两行中,您导入dirname
和fileURLToPath
,用于将__dirname
变量值设置为文件正在执行的目录的绝对路径index.js
。这些导入是必要的,因为在使用 ES 模块时未定义 ,并且仅在CommonJS 模块__dirname
中默认定义。
接下来,添加以下代码来引用该index.js
文件:
首先,将变量设置cpuCount
为计算机中的 CPU 数量,该数量应该是 4 个或更多。接下来,您在控制台中记录 CPU 的数量。然后,您记录主进程(将接收所有请求的进程 ID),并使用负载均衡器在工作进程之间分配它们。
接下来,您使用模块的方法引用该index.js
文件,以便它将在生成的每个工作进程中执行。setupPrimary()
cluster
接下来,添加以下代码来创建进程:
循环会迭代与 中的值一样多的次数,并在每次迭代期间cpuCount
调用模块fork()
的方法。cluster
您可以exit
使用模块on()
的方法附加事件cluster
,以便在进程发出exit
事件时进行监听,这通常是在进程终止时。当该exit
事件被触发时,您记录已死亡的工作进程的进程 ID,然后调用该fork()
方法来创建新的工作进程来替换已死亡的进程。
您的完整代码现在将匹配以下内容:
添加完行后,保存并退出文件。
接下来,运行该文件:
输出将与以下内容紧密匹配(您的进程 ID 和信息顺序可能不同):
The total number of CPUs is 4
Primary pid=7341
worker pid=7353
worker pid=7354
worker pid=7360
App listening on port 3000
App listening on port 3000
App listening on port 3000
worker pid=7352
App listening on port 3000
输出将指示四个 CPU、一个包含负载均衡器的主进程和四个正在侦听端口 的工作进程3000
。
接下来,返回第二个终端,然后向路由发送请求/heavy
:
输出确认程序正在运行:
The result of the CPU intensive task is 5000000
您现在可以停止服务器。
此时,您计算机上的所有 CPU 上将运行四个进程:
将集群添加到应用程序后,您可以比较使用该cluster
模块和不使用该cluster
模块的程序性能。
第 4 步 — 使用负载测试工具比较性能
在此步骤中,您将使用该loadtest
包针对您构建的两个程序生成流量。primary.js
您将比较使用cluster
该模块的程序与index.js
不使用集群的程序的性能。您会注意到,使用该cluster
模块的程序比不使用集群的程序执行速度更快,并且可以在特定时间内处理更多请求。
首先,您将测量文件的性能index.js
,该文件不使用cluster
模块并且仅在单个实例上运行。
在第一个终端中,运行该index.js
文件来启动服务器:
您将收到应用程序正在运行的输出:
worker pid=7731
App listening on port 3000
接下来,返回到第二个终端以使用该loadtest
包向服务器发送请求:
该-n
选项接受包应发送的请求数,即此处的1200
requests。该-c
选项接受应同时发送到服务器的请求数。
发送请求后,包将返回类似于以下内容的输出:
Requests: 0 (0%), requests per second: 0, mean latency: 0 ms
Requests: 430 (36%), requests per second: 87, mean latency: 1815.1 ms
Requests: 879 (73%), requests per second: 90, mean latency: 2230.5 ms
Target URL: http://localhost:3000/heavy
Max requests: 1200
Concurrency level: 200
Agent: keepalive
Completed requests: 1200
Total errors: 0
Total time: 13.712728601 s
Requests per second: 88
Mean latency: 2085.1 ms
Percentage of the requests served within a certain time
50% 2234 ms
90% 2340 ms
95% 2385 ms
99% 2406 ms
100% 2413 ms (longest request)
从此输出中,记下以下指标:
Total time
衡量处理所有请求所花费的时间。在此输出中,服务所有1200
请求仅花费了 13 秒多一点的时间。Requests per second
测量服务器每秒可以处理的请求数。在此输出中,服务器88
每秒处理的请求数。Mean latency
测量发送请求和获取响应所花费的时间,该时间位于2085.1 ms
示例输出中。
这些指标将根据您的网络或处理器速度而有所不同,但它们将接近这些示例。
现在您已经测量了文件的性能index.js
,您可以停止服务器了。
primary.js
接下来,您将测量使用该模块的文件的性能cluster
。
为此,请返回第一个终端并重新运行该primary.js
文件:
您将收到包含与之前相同信息的回复:
The total number of CPUs is 4
Primary pid=7841
worker pid=7852
App listening on port 3000
worker pid=7854
App listening on port 3000
worker pid=7853
worker pid=7860
App listening on port 3000
App listening on port 3000
在第二个终端中,loadtest
再次运行命令:
完成后,您将收到类似的输出(它可能会根据系统上 CPU 的数量而有所不同):
Requests: 0 (0%), requests per second: 0, mean latency: 0 ms
Target URL: http://localhost:3000/heavy
Max requests: 1200
Concurrency level: 200
Agent: keepalive
Completed requests: 1200
Total errors: 0
Total time: 3.412741962 s
Requests per second: 352
Mean latency: 514.2 ms
Percentage of the requests served within a certain time
50% 194 ms
90% 2521 ms
95% 2699 ms
99% 2710 ms
100% 2759 ms (longest request)
primary.js
与模块一起运行的应用程序的输出cluster
表明,在不使用集群的程序中,总时间从 13 秒减少到 3 秒。352
服务器每秒可以处理的请求数量比以前增加了两倍88
,这意味着您的服务器可以承受巨大的负载。另一个重要指标是平均延迟,它已从 显着下降2085.1 ms
到514.2 ms
。
此响应确认扩展已生效,并且您的应用程序可以在短时间内处理更多请求而不会出现延迟。如果您升级计算机以拥有更多 CPU,应用程序将自动扩展到 CPU 数量并进一步提高性能。
提醒一下,终端输出中的指标会因网络和处理器速度的不同而有所不同。总时间和平均延迟会大幅下降,总时间会快速增加。
现在您已经进行了比较并注意到应用程序在使用该cluster
模块时性能更好,您可以停止服务器。在下一步中,您将使用该模块pm2
来代替该cluster
模块。
第 5 步 — 用于pm2
聚类
到目前为止,您已经使用该cluster
模块根据机器上的 CPU 数量创建工作进程。您还添加了在工作进程终止时重新启动它的功能。在此步骤中,您将设置一个替代方案,通过使用pm2
基于该cluster
模块构建的流程管理器来自动扩展应用程序。该进程管理器包含一个负载平衡器,可以自动创建与计算机上的 CPU 数量一样多的工作进程。它还允许您监视进程,并在一个进程死亡时自动生成一个新的工作进程。
要使用它,您需要运行pm2
包含需要缩放的文件的包,这就是index.js
本教程中的文件。
在初始终端中,pm2
使用以下命令启动集群:
该选项接受您要创建的-i
工作进程的数量。pm2
如果传递参数0
,pm2
将自动创建与计算机上的 CPU 数量一样多的工作进程。
运行命令后,pm2
将显示有关工作进程的更多详细信息:
...
[PM2] Spawning PM2 daemon with pm2_home=/home/sammy/.pm2
[PM2] PM2 Successfully daemonized
[PM2] Starting /home/sammy/cluster_demo/index.js in cluster_mode (0 instance)
[PM2] Done.
┌─────┬──────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id │ name │ namespace │ version │ mode │ pid │ uptime │ ↺ │ status │ cpu │ mem │ user │ watching │
├─────┼──────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 0 │ index │ default │ 1.0.0 │ cluster │ 7932 │ 0s │ 0 │ online │ 0% │ 54.5mb │ nod… │ disabled │
│ 1 │ index │ default │ 1.0.0 │ cluster │ 7939 │ 0s │ 0 │ online │ 0% │ 50.9mb │ nod… │ disabled │
│ 2 │ index │ default │ 1.0.0 │ cluster │ 7946 │ 0s │ 0 │ online │ 0% │ 51.3mb │ nod… │ disabled │
│ 3 │ index │ default │ 1.0.0 │ cluster │ 7953 │ 0s │ 0 │ online │ 0% │ 47.4mb │ nod… │ disabled │
└─────┴──────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘
该表包含每个工作线程的进程 ID、状态、CPU 利用率和内存消耗,您可以使用这些信息来了解进程的行为。
使用 启动集群时pm2
,该软件包在后台运行,即使您重新启动系统也会自动重新启动。
如果要读取工作进程的日志,可以使用以下命令:
您将收到日志的输出:
[TAILING] Tailing last 15 lines for [all] processes (change the value with --lines option)
/home/sammy/.pm2/pm2.log last 15 lines:
...
PM2 | 2022-12-25T17:48:37: PM2 log: App [index:3] starting in -cluster mode-
PM2 | 2022-12-25T17:48:37: PM2 log: App [index:3] online
/home/sammy/.pm2/logs/index-error.log last 15 lines:
/home/sammy/.pm2/logs/index-out.log last 15 lines:
0|index | worker pid=7932
0|index | App listening on port 3000
0|index | worker pid=7953
0|index | App listening on port 3000
0|index | worker pid=7946
0|index | worker pid=7939
0|index | App listening on port 3000
0|index | App listening on port 3000
在最后八行中,日志提供了四个正在运行的工作进程中每个进程的输出,包括进程 ID 和端口号3000
。此输出确认所有进程都在运行。
您还可以使用以下命令检查进程的状态:
输出将与下表匹配:
┌─────┬──────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id │ name │ namespace │ version │ mode │ pid │ uptime │ ↺ │ status │ cpu │ mem │ user │ watching │
├─────┼──────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 0 │ index │ default │ 1.0.0 │ cluster │ 7932 │ 5m │ 0 │ online │ 0% │ 56.6mb │ nod… │ disabled │
│ 1 │ index │ default │ 1.0.0 │ cluster │ 7939 │ 5m │ 0 │ online │ 0% │ 55.7mb │ nod… │ disabled │
│ 2 │ index │ default │ 1.0.0 │ cluster │ 7946 │ 5m │ 0 │ online │ 0% │ 56.5mb │ nod… │ disabled │
│ 3 │ index │ default │ 1.0.0 │ cluster │ 7953 │ 5m │ 0 │ online │ 0% │ 55.9mb │ nod… │ disabled │
└────┴────────────────────┴──────────┴──────┴───────────┴──────────┴──────────┘
现在集群正在运行,在同一终端中输入以下命令来测试其性能:
输出将与以下内容紧密匹配:
Requests: 0 (0%), requests per second: 0, mean latency: 0 ms
Target URL: http://localhost:3000/heavy
Max requests: 1200
Concurrency level: 200
Agent: keepalive
Completed requests: 1200
Total errors: 0
Total time: 3.771868785 s
Requests per second: 318
Mean latency: 574.4 ms
Percentage of the requests served within a certain time
50% 216 ms
90% 2859 ms
95% 3016 ms
99% 3028 ms
100% 3084 ms (longest request)
、Total time
、Requests per second
和Mean latency
接近您使用该cluster
模块时生成的指标。这种对齐方式表明pm2
缩放的工作原理类似。
要改进您的工作流程pm2
,您可以生成配置文件来传递应用程序的配置设置。这种方法将允许您在不传递选项的情况下启动或重新启动集群。
要使用配置文件,请删除当前集群:
您将收到它已消失的回复:
[PM2] Applying action deleteProcessId on app [index.js](ids: [ 0, 1, 2, 3 ])
[PM2] [index](2) ✓
[PM2] [index](1) ✓
[PM2] [index](0) ✓
[PM2] [index](3) ✓
┌────┬────────────────────┬──────────┬──────┬───────────┬──────────┬──────────┐
│ id │ name │ mode │ ↺ │ status │ cpu │ memory │
└────┴────────────────────┴──────────┴──────┴───────────┴──────────┴──────────┘
接下来,生成配置文件:
输出将确认文件已生成:
File /home/sammy/cluster_demo/ecosystem.config.js generated
重命名.js
为.cjs
以启用对 ES 模块的支持:
使用编辑器打开配置文件:
在您的ecosystem.config.cjs
文件中,添加下面突出显示的代码:
module.exports = {
apps : [{
script: 'index.js',
watch: '.',name: "cluster_app",instances: 0,exec_mode: "cluster",
}, {
script: './service-worker/',
watch: ['./service-worker']
}],
deploy : {
production : {
user : 'SSH_USERNAME',
host : 'SSH_HOSTMACHINE',
ref : 'origin/master',
repo : 'GIT_REPOSITORY',
path : 'DESTINATION_PATH',
'pre-deploy-local': '',
'post-deploy' : 'npm install && pm2 reload ecosystem.config.cjs --env production',
'pre-setup': ''
}
}
};
该script
选项接受将在包将生成的每个进程中运行的文件pm2
。该name
属性接受任何可以标识集群的名称,这在您需要停止、重新启动或执行其他操作时会有所帮助。该instances
属性接受您想要的实例数。设置instances
为0
将使pm2
产生与 CPU 一样多的进程。接受exec_mode
该cluster
选项,该选项指示pm2
在集群中运行。
完成后,保存并关闭文件。
要启动集群,请运行以下命令:
您将收到以下回复:
[PM2][WARN] Applications cluster_app, service-worker not running, starting...
[PM2][ERROR] Error: Script not found: /home/node-user/cluster_demo/service-worker
[PM2] App [cluster_app] launched (4 instances)
最后一行确认4
进程正在运行。由于您尚未在本教程中创建脚本,因此您可以忽略有关未找到的service-worker
错误。service-worker
要确认集群正在运行,请检查状态:
您将收到确认四个进程正在运行的响应:
┌────┬────────────────────┬──────────┬──────┬───────────┬──────────┬──────────┐
│ id │ name │ mode │ ↺ │ status │ cpu │ memory │
├────┼────────────────────┼──────────┼──────┼───────────┼──────────┼──────────┤
│ 0 │ cluster_app │ cluster │ 0 │ online │ 0% │ 56.9mb │
│ 1 │ cluster_app │ cluster │ 0 │ online │ 0% │ 57.6mb │
│ 2 │ cluster_app │ cluster │ 0 │ online │ 0% │ 55.9mb │
│ 3 │ cluster_app │ cluster │ 0 │ online │ 0% │ 55.9mb │
└────┴────────────────────┴──────────┴──────┴───────────┴──────────┴──────────┘
如果您想重新启动集群,可以使用您在文件中定义的应用程序名称ecosystem.config.cjs
,在本例中为cluster_app
:
集群将重新启动:
Use --update-env to update environment variables
[PM2] Applying action restartProcessId on app [cluster_app](ids: [ 0, 1, 2, 3 ])
[PM2] [cluster_app](0) ✓
[PM2] [cluster_app](1) ✓
[PM2] [cluster_app](2) ✓
[PM2] [cluster_app](3) ✓
┌────┬────────────────────┬──────────┬──────┬───────────┬──────────┬──────────┐
│ id │ name │ mode │ ↺ │ status │ cpu │ memory │
├────┼────────────────────┼──────────┼──────┼───────────┼──────────┼──────────┤
│ 0 │ cluster_app │ cluster │ 1 │ online │ 0% │ 48.0mb │
│ 1 │ cluster_app │ cluster │ 1 │ online │ 0% │ 47.9mb │
│ 2 │ cluster_app │ cluster │ 1 │ online │ 0% │ 38.8mb │
│ 3 │ cluster_app │ cluster │ 1 │ online │ 0% │ 31.5mb │
└────┴────────────────────┴──────────┴──────┴───────────┴──────────┴──────────┘
要继续管理集群,您可以运行以下命令:
命令 | 描述 |
---|---|
pm2 start app_name |
启动集群 |
pm2 restart app_name |
杀死集群并重新启动它 |
pm2 reload app_name |
无需停机即可重新启动集群 |
pm2 stop app_name |
停止集群 |
pm2 delete app_name |
删除集群 |
您现在可以使用pm2
模块和cluster
模块来扩展您的应用程序。
结论
在本教程中,您使用该模块扩展了应用程序cluster
。首先,您创建了一个不使用该cluster
模块的程序。然后,您创建了一个程序,使用该cluster
模块将应用程序扩展到计算机上的多个 CPU。之后,您比较了使用该cluster
模块的应用程序和未使用该模块的应用程序之间的性能。最后,您使用该pm2
包作为cluster
模块的替代方案来跨多个 CPU 扩展应用程序。
要进一步了解,您可以访问cluster
模块文档页面以了解有关该模块的更多信息。
如果您想继续使用pm2
,可以参考PM2-流程管理文档。您还可以尝试使用我们的教程pm2
如何在 Ubuntu 22.04 上设置用于生产的 Node.js 应用程序。
Node.js 还附带了一个worker_threads
模块,允许您在工作线程之间拆分 CPU 密集型任务,以便它们可以快速完成。尝试我们的如何在 Node.js 中使用多线程的教程。您还可以使用专用 Web Worker 优化前端中的 CPU 密集型任务,您可以按照如何使用 Web Workers 处理 CPU 密集型任务教程来完成此任务。如果您想了解如何避免 CPU 密集型任务影响应用程序的请求/响应周期,请查看如何使用 Node.js 和 BullMQ 处理异步任务。
via https://www.digitalocean.com/community/tutorials/how-to-scale-node-js-applications-with-clustering