webman_socket_send()不能在主线程批量调用,因其为阻塞式IO,单次卡顿可达几十毫秒,拖垮整个事件循环;正确做法是移至异步线程或无锁队列处理,并配合Redis Pub/Sub实现多实例消息同步。

Webman 能撑起高并发智能客服机器人,但直接照搬 HTTP 服务写法会在线上崩得很快——核心瓶颈不在连接数,而在消息广播、状态同步和 NLU 调用的阻塞点。
webman_socket_send() 为什么不能在主线程里批量调用
这是最常被踩的坑:开发者看到 webman_get_all_sockets() 返回了所有连接,就直接 foreach 调用 webman_socket_send() 推送消息。但 webman_socket_send() 是阻塞式 IO,一旦某个客户端 TCP 窗口满或网络抖动,单次调用可能卡住几十毫秒,整个事件循环就被拖死。
- 连接数超 2000 后,广播延迟会从毫秒级跳到数百毫秒,用户明显感知“机器人卡顿”
- 该函数不区分目标用户,全量遍历 + 阻塞发送,CPU 和内核 send buffer 双重压力
- 无法优雅降级:当部分连接异常时,没有失败重试或熔断机制
正确做法是把发送逻辑移出主线程:用 ev_async 触发异步线程批量处理,或用无锁环形队列(如 ringbuffer)暂存待发消息,由独立 worker 进程消费。
Redis Pub/Sub 是多实例部署的唯一可行方案
单机 Webman 实例靠内存广播能跑通 demo,但只要部署两个及以上实例,websocket_dispatch 收到的消息就只推给本机连接——用户 A 在实例 1 发问,用户 B 在实例 2 就收不到回复,这就是典型的“消息不一致”。
- 必须用
Redis PUB/SUB或Redis Stream做消息总线,所有实例订阅同一 channel - 每个实例收到消息后,只向本机已连接的目标用户推送,避免跨进程通信开销
- 用户在线状态不能存内存,要用
Redis SETEX存心跳,配合KEYSPACE通知自动清理离线态
别碰 Redis LIST + BRPOP 拉模式,它在高并发下会竞争锁、产生延迟毛刺。
Webman 2.2.0版本强化了 TCP/UDP 服务支持,优化路由组管理,并增强异步任务处理能力。结合协程与连接池技术,Webman 能轻松应对高并发场景,适用于网站、接口服务、即时通讯、物联网及游戏开发,兼具高性能、灵活扩展与稳定可靠,是多场景 PHP 服务开发的理想选择。
意图识别调用必须异步化,且带超时与降级
智能客服真正的性能杀手不是 WebSocket 连接,而是每次用户提问都同步调用外部 NLU 服务(比如 Rasa、自研 BERT API)。HTTP 请求本身就有 DNS 解析、TLS 握手、网络往返等不可控延迟,P99 超过 800ms 就会让对话体验断裂。
- 所有 NLU 调用必须走
Workerman\Connection\AsyncTcpConnection或 cURL 异步扩展,禁止file_get_contents()或curl_exec() - 必须设硬性超时(建议 ≤ 600ms),超时后返回兜底规则响应(如“正在理解您的问题,请稍候”),而不是让连接空等
- 高频低风险意图(如“你好”“再见”)可本地缓存识别结果,用
Redis SETEX cache:nlu:hello 300 "greeting"减少外部依赖
别在 websocket_dispatch 回调里做任何 DB 查询或外部调用——只做协议解析 + 入队,这是 Webman 高性能的铁律。
对话状态持久化不能只靠 Session
Webman 的 session 默认基于文件或 Redis,但它只是键值对存储,无法表达多轮对话的状态机流转。更危险的是,在实例重启或横向扩缩容时,内存 Session 会丢失,用户正在填的退货表单、刚选的物流方式全归零。
- 必须用结构化方式存状态:推荐
Redis HASH,key 为dialog:session_id,field 包含state、last_intent、slots(JSON 字符串)、updated_at - 状态更新要原子:用
Redis HSET+EXPIRE组合,避免先HSET再EXPIRE中间被中断导致永不过期 - 敏感字段(如用户手机号、订单号)不要明文落库,至少 AES 加密后再存
Redis
真正难的不是存状态,而是怎么在用户网络中断又重连时,精准恢复到“正在确认收货地址”的那个节点——这需要前端传回 last_message_id,并和服务端存储的 dialog_version 做比对校验。


















