redis 替代 mqtt 和自定义协议 作为物联网设备通讯的 可行性

本文只针对笔者自己的项目情况,不使用redis,而且使用更专业的消息队列服务器的存在有更充分的理由。

# 选择的原因

# 性能天花板

从树莓派到志强,redis是所有mq服务器的天花板。
自己手撸的简单hash map查询 业务添加一些 都比不过redis,而且在大量数据的情况下,查询和操作速度更是直线下滑。想做到极致的性能优化需要大量繁重工作,不如直接拿来就用

# 简单灵活的高可用性集群

哨兵模式和主从模式也不是不能用。
cluster 虽然必须要最少3主3从才能实现高可用性,但是很容易满足。3台云服务器即可 互为主从 并集群,可以在线扩容,在线收缩,简单灵活可靠。

# 低流量消耗的轻量化协议

redis 是基于tcp的简单文本协议,没有协议头,没有校验码,没有封装,每一个字节都是有效必须的数据,大大节省服务器带宽资源

# 活跃的老项目良好的向上兼容

redis至今依旧维持较为频繁的功能和性能更新

# 免中间件

1、工作量小 服务器上不需要再运行太多程序,开发工作小很多(在大并发环境一个中间件 真的很需要时间调试优化)
2、节省服务器开支

# 性能下限高

团队里面的实习生,只要不让他碰redis扩展module的代码,再烂的代码也不会影响整体运行

# 不用更专业的mq的原因

现有的主流mq服务, 性能都低于redis,在集群扩展上也都远远不如redis灵活和高效。
部分mq自带的消息持久化储存 性能更是一般,外部储存方式 多了一层转换。很多mq可能已经很轻量,但是相对 redis 还是差了一些。 Kafka RocketMQ Apollo ZeroMQ ActiveMQ RabbitMQ emqx Mosquitto 情况差不多,不展开细说
集群方面,要么死板复杂不能热扩容收缩,要么集群后单机性能会掉

# 不使用 loTDB的原因

没有看到loTDB的压测数据,自己简单测试过后性能不理想

# 不自己开发私有协议的原因

私有协议 可以灵活使用 tcp/udp 包括上层websocket。但是集群部署 还有大量map的操作 开发工作量比较大。
另外 golang在 大并发情况下 资源占用要比C高很多,引入epoll可以降低一些,但是开发工作量还是不低。

# 物联网设备通讯要求

# 基本

包括多数物联网设备的的要求 我手上的项目情况有以下要求:

  • 低成本、可快速扩展、高可用性集群
  • 部分功能要求消息必须送达:例如 报警信息
  • 部分消息 要求 只可以送达1次,不可重复送达:例如 提醒消息
  • 部分数据允许少量丢失: 例如设备实时采集的数据
  • 设备24小时开机,不停上报数据。对服务器处理和记录能力有一定要求(最主要是服务器成本要低)
  • 设备离线通知和报警
  • 设备和用户app可以直接进行通讯,当然 这里指的还是服务器转发消息
  • qps的严格限制,防止服务器被拉垮
  • 部分功能需要 tls加密通讯
  • 消息需要选择性的持久化和转存

# tcp和udp

国内环境下 部分地区部分时段udp 丢包严重。另外udp每次都需要带一定字数的token信息 虽然减少了握手环节,但是对于 少量频繁的数据通讯 就会明显增加服务器流量开支
ws协议,有更好,没有可以接受,毕竟物联网设备直接tcp udp是没问题的。

# 自定义协议和现有协议的选择

自定义协议tcp/udp 自然是更好,但是之前的项目开发经验让我明白 还是要权衡利弊一下。

# 数据的序列化和反序列化

考虑到服务器资源的占用,json 肯定是最不可取的。msgpack反序列化性能还可以,但是比json并不能降低太多带宽。 所以开发阶段用json或者msgpack,后期还是要换成protobuf。

# 用redis实现上面的功能的逐一说明

因为redis 本身不适合作为面向用户的消息队列服务,所以有一些问题需要额外处理。

  • 使用redis6.0 以后的tls 解决加密通讯
  • 务必使用redis7.0以后版本的 acl 精确控制到key和指令
  • 使用stream 处理 必到达的消息 和 之发送一次的消息
  • 使用sub 处理一次性不需要储存 可以容许丢失的消息
  • redis开启数据持久化,但是后端需要定期把历史消息转储存到sql,防止redis内的历史消息占用内存多大
  • 集群 cluster 解决高可用性和灵活扩展性
  • redis 集群客户端 在物联网设备 和 app端 socket 手撸。使用{key}标识符,把单一个物联网设备的数据固定到一个节点上即可,然后客户端自动从任意一个节点得到负责的节点地址。
  • 设备离线通知: 后端程序 连到redis 定时扫描 客户端连接信息,找出应在线但是未连接的客户端

# 关于权限控制和防恶意用户

  • key访问权限和指令限制:使用redis7.0以后版本的 acl 限制 物联网客户端可用指令和可写key
  • 后端程序 通过扫描redis客户端信息,拉黑频繁多次连接的用户
  • key 内容大小限制:简单的方案 依旧是后端程序定期扫描bigkey。我的方案,自己开发一个简单的redis 模块 来限制,比如 用 lyh.get 限制客户端请求太大的字符串 key lyh.set限制 写入太大的数据
  • 客户端qps限制:1是后端定时扫描 hot key 找到异常用户 拉黑 2、自定义模块 创建全局hash map记录用户操作频率 进行限制

# 其他安全措施

毕竟redis 不是设计为了面向非可信用户的mq服务器。 一些防患措施还是是要做一下。

  • 配置 maxmemory-clients 并经常监控
  • tls和明文tcp 都使用,优先使用tls,在负载过大情况下,临时切换到明文,并紧急扩容集群。
  • 为了避免可能的安全隐患,客户端应该预留一组redis服务器地址: 这组服务器 应该由可以过滤转发tcp指令的程序反代redis 在redis出现未知漏洞的情况下使用
  • 关键性指令 重命名 或者禁用掉
  • redis 使用普通无ssh权限的用户运行
  • iptable,限制单ip连接频率和连接数
  • 如果内容很重要,记得定时备份 redis的持久化文件
  • 核心数据如果使用redis储存,需要和客户端可以访问的redis 做隔离。

# 优点

1、高性能
2、高可用性
3、高扩展性
4、没有中间件,只有后台整理和维护件,后端工作量很少
5、低服务器成本,cpu、内存、带宽占用都不会太高

# 最后说一下缺点

  • 在嵌入式设备上 因为没有支持redis集群的客户端(甚至部分环境也没有可用的redis客户端),需要自己实现 ,不过因为我们允许这部分客户端能使用指令很少,所以并不复杂。知道redis 就是 命令+换行 来通讯的就够了
  • 需要redis自定义模块 进行高阶防护 不过真的不难,尤其是对熟悉C的嵌入式开发人员,当然其他语言也可以
  • 云厂商的redis通常不可以导入自定义模块
  • redis目前不支持websocket 官方说后续有可能会支持,在确实需要的情况下可以起一个后端程序转发ws到redis
    golang实现的:https://dev.leiyanhui.com/golang/ws-redis/
    官方进展关注: https://github.com/orgs/redis/projects/4/views/1?filterQuery=http&pane=issue&itemId=24785018
  • redis目前不支持auth验证次数限制 8年了还没添加的功能 ,8年后的今天我们依旧需要自己在防火墙限制过滤auth指令
    官方进展关注:https://github.com/redis/redis/pull/1241
  • redis目前不能避免单一用户多次连接,需要自己后端扫描,官方没有支持计划,暂时可以用iptable略微临时限制 和后台扫描。我已经自己修改完成redis的自动离线重复登陆的用户 目前运行良好。
  • redis目前不能尽快自动踢出 没有进行 auth 的空连接 ,暂时只能自己在iptable限制,不过根据我的使用 多数mq服务器 都不支持。
  • redis acl的持久化在rdb/aof文件中,目前是可以手动保持到acl文本文件的,不过这里有一个pull 是 等待批准 状态 https://github.com/redis/redis/pull/10369
  • redis auth 没有防止穷举的措施 目前暂时没有解决办法,客户端可以在一秒内发起几万次auth请求,拖垮redis,只能等待官方更新 https://github.com/redis/redis/pull/11554

# 权衡后

因为现有app和小程序端,依赖http/ws协议,不能直接连接redis 所以app端是无法直接引入,通过服务器端口搭建ws转发到redis又再次引入了中间件
auth 指令没有使用限制,以及 auth之前不能通过自定义模块的方式来替代auth,所以导致此问题 除了修改redis源码和等待官方更新之外,可以通过防火墙适当防止一下,然后暂时无解(云厂商的公网redis一般直接限制了最大连接数)

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计