随着开源物联网中间件 EMQX 的使用的普及,各种社交软件上有关 EMQX 的问题也越来越多。这些问题繁杂、琐碎,但都没有系统和详尽的解答。因此,我准备写一个 《EMQX 常见问题》的系列文章,愿对您有所帮助。
本文是 EMQX 该系列的第一篇,讲解在 MQTT 客户端登录时发生的一个常见的错误:
Failed to open session due to: client_id_unavailable
上面的日志是说 MQTT 客户端尝试建立连接但失败了,错误原因是 client_id_unavailable。注意这个错误只会在多个客户端、使用相同的 ClientID 同时登录的时候发生。要详细了解此错误的原因,首先要了解 EMQX 对于会话的处理。
MQTT 中的会话
MQTT 协议规定,服务端将为相同的 ClientID 维护全局唯一的会话。会话保存了该客户端的订阅信息、消息队列等等。这个会话可以是持久保存的,也可以是临时的。
如果两个相同 ClientID 的客户端先后登录,后面一个客户端将登录成功,而第一个客户端会被服务端断开。换句话说,后登录的客户端把老的客户端”踢下线“了。但如果它们是同时登录的,难以确认谁先谁后,则必须用某种方式来决定让哪个客户端连上来。
EMQX 的会话处理逻辑
在 EMQX 中,与客户端的 TCP 连接建立完成之后,会为其分配一个 Erlang 进程,然后开始做认证,认证完成之后会为此客户端创建会话。为了实现整个集群内会话的唯一性,在该进程创建会话之前需要申请一个分布式的互斥锁。而 client_id_unavailable 错误,正是因为申请互斥锁失败了。
申请互斥锁的过程,是 RPC 到每一个节点去申请锁,如果至少有一个节点返回失败则失败,EMQX 将以 0x85(Client Identifier not valid)为错误码发送 CONNACK,并断开与客户端的 TCP 连接。
所以,如果你看到了 client_id_unavailable 错误,可能的原因是:
- 在使用相同的 ClientID 做压力测试或攻击,多个连接以非常快的速度重连。
- 创建会话的过程太慢了。这也是最常见的情况。
正常情况下 EMQX 创建会话是非常快的,它首先把系统中现有的已登录的 ClientID 踢出,然后初始化对应的 Session 对象和 ETS 表等。
对于非持久的会话 (clean session = true) 来说,踢出老的连接需要去对应的节点上,通知老的进程断开 TCP 连接。对于持久会话 (clean session = false) 来说,还需从老的进程复制整个会话到本进程,这个过程叫做会话接管(Session Takeover)。通知老进程断开连接和接管会话都是同步的过程,如果老进程位于远程节点还需要 RPC,这是整个会话创建中最费时的一步,此任务过慢会导致新的进程获取了锁却长时间不释放,从而导致该客户端的后续重连失败。
出现该问题的最常见的原因是老的进程僵死无法响应请求了。可能是因为老进程正在执行一个阻塞的操作,比如正在调用写入数据库的钩子,或者使用同步模式发送 TCP 报文等等。
僵死的进程无法主动释放锁,就只能等待系统周期性检查并释放已经过期的锁。锁的过期时间大概在 2 分钟之内,不同的 emqx 版本实现不同。
调查与调试
- 使用 emqx_ctl listeners 命令来查看错误统计。可见大量的 client_id_unavailable 原因码统计。
- 使用 observer_cli 调查老进程阻塞原因。
emqx eval 'observer_cli:start()'
进入 observe_cli 界面之后,输入 mq 回车,查看有无进程邮箱堵塞(MsgQueue > 0)的进程。输入阻塞的进程的序号,比如 1,回车,然后输入 C 并回车查看此进程的 current-stacktrace,可以看到进程正在执行的函数调用,可快速定位到问题所在。
任何技术支持、问题咨询等请直接联系我。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。