Joyber 发布的文章

真的可以 全部在 Nacos 里配置,不用改本地文件、不用重新打包!

Ruoyi 分布式版 本来就是用 Nacos 统一管理配置的!
你之前改本地 application.yml没用的,因为服务优先读 Nacos 配置


1. 为什么你改本地文件不生效?

因为 Ruoyi 微服务架构:

  1. 项目启动 → 优先去 Nacos 拉取配置
  2. 本地配置只做连接 Nacos 用
  3. 真正生效的是:
    Nacos → 配置管理 → application-dev.yml

所以:
你改本地文件 = 白改!不会生效!不需要打包!


2. 正确修复 /actuator 漏洞的方法(1 分钟搞定)

直接在 Nacos 里加配置,不用打包、不用重新编译!

步骤:

  1. 打开 Nacos 控制台
  2. 进入 配置管理 → 配置列表
  3. 找到 application-dev.yml(所有服务共享配置)
  4. 编辑,在最下面添加以下内容:
# 安全关闭 actuator 暴露,只允许健康检查
management:
  endpoints:
    web:
      exposure:
        include: health,info
  server:
    address: 127.0.0.1
  endpoint:
    health:
      show-details: always
  1. 点击 发布
  2. 重启微服务(只重启,不打包)

3. 这样改的好处

  • 不用重新打包
  • 不用本地改代码
  • 所有服务统一生效
  • 不影响前端任何接口
  • /actuator 立刻无法外部访问,漏洞修复完成

4. 最重要的一句提醒

你以后所有配置,都在 Nacos 里改,不要动本地文件!


总结(超简单)

  • Nacos 里的 application-dev.yml全局生效配置
  • 直接在里面加关闭 actuator 的配置
  • 发布 → 重启服务 → 完成
  • 不用打包!不用编译!不影响前端!

你的理解有一定道理,但关键在于 rewrite 指令的 break 标志

在 Nginx 中,rewritebreak 标志会停止当前 location 内的重写处理,但不会重新进行 location 匹配。所以即使 URI 被改成了 /xxx(去掉了 /api/ 前缀),请求仍然停留在当前 location /api/ 块内,继续执行后面的 proxy_pass 等指令,最终将修改后的 URI 代理到后端。

如果误用了 last 标志,则会在重写完成后重新匹配 location,此时 URI 变成 /xxx,就可能匹配到 location / 块,导致代理失效。但这里用的是 break,所以安全

总结

  • rewrite ... break; → 在当前 location 内继续处理,不会重新匹配 location
  • rewrite ... last; → 重写后重新匹配 location,可能跳转到其他 location

因此,你提供的配置中,/api/xxx 的请求会始终走代理,不会落到 location / 里。

池化和归一化是完全不同的概念。


1. 池化(Pooling)

  • 作用:在卷积神经网络(CNN)等模型中,对特征图进行下采样,缩小尺寸、减少参数量,同时保留重要特征。
  • 常见方式:最大池化(Max Pooling)、平均池化(Average Pooling)。
  • 操作:对局部区域取最大值或平均值,不涉及对数值的缩放或归一化。

2. 归一化(Normalization)

  • 作用:将数据缩放到一个特定范围(如 [0,1])或使分布符合某种统计特性(如均值为0、方差为1),以加速模型收敛、提高稳定性。
  • 常见方式

    • Min-Max 归一化:将数据线性缩放到 [0,1]。
    • Z-score 标准化:减去均值后除以标准差,使数据服从标准正态分布。
    • Batch Normalization:在神经网络层间对激活值进行标准化。
  • 操作:对数值进行线性变换或统计调整,不改变数据维度。

3. 主要区别

方面池化归一化
目的降维、特征选择缩放数值范围、稳定分布
操作对象空间或时间邻域单个样本或特征维度
维度变化通常降低尺寸不改变维度
典型应用CNN 中的下采样数据预处理、网络内部层

虽然两者常同时出现在深度学习模型中,但它们服务于不同的目的,不可混淆。

这个计算余弦相似度的MYSQL函数:

    IF JSON_LENGTH(v1) != JSON_LENGTH(v2) THEN
        RETURN NULL;
    END IF;
    WHILE
        i < len
        DO
            SET sim = sim + (JSON_EXTRACT(v1, CONCAT('$[', i, ']')) * JSON_EXTRACT(v2, CONCAT('$[', i, ']')));
            SET i = i + 1;
        END WHILE;
    RETURN sim;
END

然后通过这个SQL语句搜索:

        $normalizedVector = json_encode($normalizedVector);
        $similarity =  new Expression("COSIM(v.normalized_vector, :json)", [
            ':json' => $normalizedVector
        ]);
        $query2->select(["v.id", "v.merchant_id", "v.product_id", "v.qfa_id", 'similarity' => $similarity]);
        $query2->andWhere(['v.id' => $recordIds]);
        $query2->orderBy(['similarity' => SORT_DESC]);
        $query2->limit($n);
        if ($withQfa) {
            $query2->innerJoinWith('qfa as q');
            $query2->andWhere('q.status=1');
        }

这个COSIM的函数,只是将两个json向量数组对应位置的两个数相乘后累加起来返回,为什么能达到语意搜索的效果?

COSIM 函数实际上只计算了两个 JSON 数组的点积(对应位置元素相乘后累加),而不是严格的余弦相似度(余弦相似度还需要除以两个向量的模长乘积)。但它依然能在你的场景中实现语义搜索,原因如下:


1. 语义搜索的原理:向量嵌入(Embedding)

现代语义搜索通常基于 文本嵌入(如 Word2Vec、BERT 等模型),将一段文本转换成一个高维稠密向量。这些向量的特点是:语义相近的文本,其向量在空间中的方向也相近。因此,我们可以用向量间的夹角余弦来度量相似度,即:

[
\text{cosine\_similarity}(A, B) = \frac{A \cdot B}{|A| |B|}
]

  • 当两个向量方向完全一致时,余弦相似度为 1;
  • 当它们正交时,为 0;
  • 当方向相反时,为 -1(但嵌入向量通常非负,所以范围在 [0,1])。

2. 为什么点积能代替余弦相似度?

在代码中,查询向量使用了 $normalizedVector 这一变量名,暗示查询向量已经做过归一化处理(即 (|B| = 1))。如果存储的向量 v.normalized_vector 同样也是归一化的((|A| = 1)),那么:

[
A \cdot B = \frac{A \cdot B}{1 \times 1} = \text{cosine\_similarity}(A, B)
]

此时点积就等于余弦相似度。即使存储的向量没有归一化,只要查询向量归一化,点积仍然与余弦相似度成正比(相差一个常数因子 (|A|)),排序结果不变。因此,在你的场景下,直接计算点积已经足够得到语义相似度的排序。


3. 函数 COSIM 的作用

COSIM 函数虽然名称是余弦相似度,但实现的是点积。它逐一取出两个 JSON 数组对应位置的值相乘并累加。这个过程正是向量相似度计算的核心运算。

  • 为什么能实现语义搜索?
    因为该函数计算出的点积数值越大,代表两个向量在方向上越接近,从而语义上越相关。配合 ORDER BY similarity DESC,就能将最相关的记录排在前面。

4. 注意事项

  • 如果向量没有归一化,点积会受到向量长度影响,可能导致长度大的向量获得更高的分数,从而影响排序。此时应改用完整的余弦相似度公式:
    [
    \frac{A \cdot B}{\sqrt{\sum A_i^2} \times \sqrt{\sum B_i^2}}
    ]
    其中分母部分可以通过预先存储向量的模长(或计算平方和)来优化。
  • 你的函数未考虑向量长度不等的情况(已返回 NULL),这保证了输入向量的维度一致。
  • 性能方面:该函数使用循环逐元素相乘,当向量维度较高(如 512 维或更高)时,每次查询都会对每条记录执行循环,效率较低。在生产环境中,建议:

    • 使用数据库的向量扩展(如 MySQL 8.0.31 后的 VECTOR 类型 + 向量函数);
    • 或将向量存储到专门支持向量检索的数据库(如 Milvus、Pgvector、Elasticsearch 等);
    • 或利用近似最近邻(ANN)算法加速检索。

总结

COSIM 函数虽然只计算了点积,但因为查询向量已经归一化(且假设存储向量也归一化),它等价于余弦相似度,因此能够正确反映两个文本向量的语义相关性。这正是语义搜索能够“按意思”排序的核心数学基础。

Swoole WebSocket 并发架构与实战:从原理到部署

引言

使用 Swoole 开发 WebSocket 服务时,很多从传统 PHP-FPM 转过来的开发者会有一个疑问:每个 WebSocket 连接是否对应一个独立的进程或线程? 答案是否定的。Swoole 采用“多进程 + 协程”的架构,能在一个极小的资源开销下支撑数十万乃至百万级并发连接。本文将深入解析 Swoole 的并发模型,并通过一个完整的多人聊天室示例,带你从零开始构建高性能 WebSocket 服务。最后,我们将探讨在生产环境中如何调整系统限制,以确保服务器能承载预期的连接数。


一、Swoole 的并发模型:进程、线程与协程

1.1 传统 PHP 的局限

在 PHP-FPM 模式下,每个请求都会启动一个进程,处理完即销毁。这种方式无法维持长连接,更不可能同时处理数万连接,因为进程创建和内存开销极大。

1.2 Swoole 的多进程 + 协程架构

Swoole 的 WebSocket 服务器在启动时会创建固定数量的 Worker 进程(例如 8 个),这些进程常驻内存,负责处理所有连接的业务逻辑。每个连接不是独占一个进程,而是通过 协程(Coroutine)来实现并发。

核心组件:

  • Master 进程:负责监听端口、管理 Reactor 线程和 Worker 进程。
  • Reactor 线程池:负责网络 I/O 的读写,将数据分发给 Worker 进程。
  • Worker 进程池:固定的进程数,处理业务逻辑(解析消息、数据库操作、广播等)。
  • 协程:在每个 Worker 进程内,每个连接对应一个协程。协程的切换开销远小于线程或进程,几 KB 的内存即可承载一个协程,从而让单个 Worker 进程可同时处理数千连接。

下面这张图清晰地展示了客户端连接如何在 Swoole 内部流转:

graph TD
    subgraph Client [客户端]
        C1[客户端 1]
        C2[客户端 2]
        C3[客户端 3]
        C4[客户端 ...]
    end

    subgraph SwooleServer [Swoole WebSocket 服务器]
        direction LR
        Master[主进程<br>Master Process<br>负责创建和管理子进程]
        
        subgraph Reactor [Reactor 线程池]
            R1[Reactor 线程]
            R2[Reactor 线程]
            R3[Reactor 线程]
        end
        
        subgraph Workers [Worker 进程池]
            direction TB
            W1[Worker 进程 1<br>协程1 协程2 ...]
            W2[Worker 进程 2<br>协程1 协程2 ...]
            W3[Worker 进程 N<br>协程1 协程2 ...]
        end
    end

    C1 ---> R1
    C2 ---> R2
    C3 ---> R3
    C4 ---> R1
    
    R1 --> W1
    R2 --> W2
    R3 --> W3

当某个连接需要等待 I/O(如数据库查询)时,Swoole 会自动挂起该协程,Worker 进程转而去处理其他协程的任务,待 I/O 完成后再恢复执行。这种机制使得 CPU 利用率极高,且无需开发者手动处理异步回调。

1.3 对比总结

特性误解:每个连接一个线程/进程实际:Swoole 架构
连接与进程关系1 个连接 = 1 个进程/线程多个连接共享固定数量 Worker 进程
并发实现方式通过大量进程/线程通过少量进程 + 大量协程
内存开销极高(每个线程几 MB)极低(每个协程几 KB)
最大连接数受进程/线程数限制(通常几千)可达数十万甚至百万
典型代表PHP-FPM, ApacheSwoole, Nginx, Node.js

二、实战:用 Swoole 开发一个多人聊天室

下面我们构建一个简单的聊天室,所有连接的用户都能看到彼此发送的消息。

2.1 服务端代码 chat_server.php

<?php
use Swoole\WebSocket\Server;
use Swoole\Http\Request;
use Swoole\WebSocket\Frame;

// 创建 WebSocket 服务器,监听 0.0.0.0:9501
$server = new Server("0.0.0.0", 9501);

// 使用 Swoole\Table 存储所有客户端连接的文件描述符(fd),以便多进程共享
$connections = new Swoole\Table(1024);
$connections->column('fd', Swoole\Table::TYPE_INT);
$connections->create();

// 监听 WebSocket 连接打开事件
$server->on('open', function (Server $server, Request $request) use ($connections) {
    $fd = $request->fd;
    $connections->set((string)$fd, ['fd' => $fd]);
    echo "新连接: fd={$fd}\n";

    // 欢迎消息
    $server->push($fd, "欢迎加入聊天室!");
    // 通知其他人
    foreach ($connections as $conn) {
        if ($conn['fd'] != $fd) {
            $server->push($conn['fd'], "用户 {$fd} 进入房间");
        }
    }
});

// 监听 WebSocket 消息事件
$server->on('message', function (Server $server, Frame $frame) use ($connections) {
    $fd = $frame->fd;
    $data = $frame->data;
    echo "收到消息: fd={$fd}, data={$data}\n";

    // 广播给所有客户端
    foreach ($connections as $conn) {
        $server->push($conn['fd'], "用户 {$fd} 说: {$data}");
    }
});

// 监听 WebSocket 连接关闭事件
$server->on('close', function (Server $server, int $fd) use ($connections) {
    $connections->del((string)$fd);
    echo "连接关闭: fd={$fd}\n";

    // 广播离开消息
    foreach ($connections as $conn) {
        $server->push($conn['fd'], "用户 {$fd} 离开了聊天室");
    }
});

// 启动服务器
$server->start();

关键点

  • 使用 Swoole\Table 存储连接的 fd,这是共享内存结构,可在多个 Worker 进程间共享。
  • 广播时遍历所有连接,向每个客户端推送消息。如果连接已断开,push 可能会失败,可根据实际情况忽略错误。

2.2 客户端代码 index.html

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>Swoole 多人聊天室</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 20px; }
        #messages { list-style: none; padding: 0; height: 300px; overflow-y: auto; border: 1px solid #ccc; margin-bottom: 10px; }
        #messages li { padding: 5px; border-bottom: 1px solid #eee; }
        #form { display: flex; }
        #input { flex: 1; padding: 8px; }
        button { padding: 8px 15px; }
    </style>
</head>
<body>
    <h2>Swoole 多人聊天室</h2>
    <ul id="messages"></ul>
    <form id="form" action="">
        <input id="input" autocomplete="off" />
        <button>发送</button>
    </form>

    <script>
        const ws = new WebSocket("ws://你的服务器IP:9501");
        const messages = document.getElementById('messages');
        const input = document.getElementById('input');
        const form = document.getElementById('form');

        ws.onmessage = function(event) {
            const li = document.createElement('li');
            li.textContent = event.data;
            messages.appendChild(li);
            messages.scrollTop = messages.scrollHeight;
        };

        form.addEventListener('submit', function(e) {
            e.preventDefault();
            if (input.value) {
                ws.send(input.value);
                input.value = '';
            }
        });
    </script>
</body>
</html>
注意:将 ws://你的服务器IP:9501 替换为实际服务器的 IP 和端口。

2.3 运行测试

  1. 启动服务端:php chat_server.php
  2. 启动一个简易 HTTP 服务器(用于托管 index.html):php -S 0.0.0.0:8080
  3. 用多个浏览器标签页或不同设备访问 http://服务器IP:8080,即可模拟多人聊天。

三、系统限制:文件描述符与 ulimit

当连接数增长时,我们需要确保操作系统允许打开足够多的文件描述符(每个网络连接占用一个)。默认的 ulimit -n 通常为 1024,对于高并发服务远远不够。

3.1 查看当前使用情况

  • 系统整体已分配的文件描述符数

    cat /proc/sys/fs/file-nr

    输出示例:3120 0 9223372036854775807

    • 第一个数字 = 当前已分配数量
    • 第二个 = 未使用但已分配的数量(通常为 0)
    • 第三个 = 系统最大限制(fs.file-max
  • 查看某个进程(如 Swoole 服务)的文件描述符使用数

    ps aux | grep swoole          # 获取进程 PID
    ls /proc/<PID>/fd | wc -l     # 统计 fd 目录下的文件数
  • 统计当前 TCP 连接数(ESTABLISHED)

    ss -tan | grep ESTAB | wc -l

3.2 临时修改(重启后失效)

ulimit -n 1000000

该命令只影响当前 shell 会话,关闭终端即恢复。

3.3 永久修改

3.3.1 修改 /etc/security/limits.conf

* soft nofile 1000000
* hard nofile 1000000
root soft nofile 1000000
root hard nofile 1000000

修改后需重新登录才能生效。

3.3.2 针对 systemd 管理的服务(如 PHP-FPM、Swoole 守护进程)

在服务单元文件(如 /etc/systemd/system/swoole.service)中添加:

[Service]
LimitNOFILE=1000000

然后执行 systemctl daemon-reload 并重启服务。

3.3.3 WSL(Windows Subsystem for Linux)特殊配置

WSL 中 /etc/security/limits.conf 可能不生效,推荐使用以下方法:

  • 创建或编辑 /etc/wsl.conf

    [boot]
    command="ulimit -n 1000000"
  • 或者修改 Windows 宿主目录下的 .wslconfig 文件(位于用户目录),增加:

    [wsl2]
    kernelCommandLine = "sysctl fs.file-max=1000000"

    修改后需完全重启 WSL:wsl --shutdown,再重新启动。

3.4 系统上限 fs.file-max

fs.file-max 是系统级的总文件描述符上限。可通过以下命令查看:

cat /proc/sys/fs/file-max

临时修改:

sysctl -w fs.file-max=1000000

永久修改:在 /etc/sysctl.conf 中添加 fs.file-max=1000000,然后执行 sysctl -p

注意:即使将 ulimit -nfs.file-max 设置得非常大,实际能支撑的连接数仍受服务器内存限制(每个连接约几 KB 内存开销)。

四、总结

Swoole 通过 多进程 + 协程 的架构,打破了传统 PHP 的并发瓶颈。它利用少量常驻进程和轻量级协程,高效处理海量网络连接,是构建 WebSocket 服务、即时通讯、游戏服务器的理想选择。开发时,我们只需按事件驱动的模式编写业务逻辑,无需关心底层并发细节。部署时,合理调整系统文件描述符限制,就能让服务平滑支撑数万甚至数十万并发连接。

希望本文能帮助你从原理到实践全面掌握 Swoole WebSocket 的开发与运维。如果你想进一步探索房间管理、用户认证、消息持久化等高级功能,可以在现有代码基础上轻松扩展。