本文只针对笔者自己的项目情况,不使用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一般直接限制了最大连接数)