都说架构是演化出来的,而不是设计出来的,有一定道理。
本文介绍一个服务系统如何从一开始的简单结构,逐步演化成为能同时服务上百
万人的系统。这个系统是一个通用物联网设备接入平台,设备可以接入平台,将
数据发往平台,或者将数据发给设备,也可以再平台内按照预定规则处理设备数
据。整个系统中,边缘网络和终端设备的工作很繁杂,但本文的重点是服务端的
演化过程。比较失败的设计就不多说了,能写一本书。有一定参考的价值,但如
果你想要做出一个一样的物联网平台,我可以给你一个忠告:这东西过气了。
验证
我们当然可以从头就设计一个一开始就能支持上百万并发的系统,但并没有这么
做,原因和成本以及时间有关。如果一开始我们就计划让10个富有经验的
序员开发半年以上,烧掉五百万才能见到第一个用户,那么创业第一年就死了,
因为根本拿不到投资。最开始,我们需要的是 "quick and dirty",能够验证有
足够多的用户对这种服务感兴趣并愿意花钱的东西。比如说,美团外卖从立项到
推出使用花了一周,就做了个订餐页面直接转客服。这个阶段做的东西只是为了
验证 idea 的商业价值,如果不靠谱,可以花很少的成本转向和关掉。所以,能
人工的就别写代码,有现成东西的就直接用现成的,能多简单就多简单。这个阶
段甚至很多公司根本不需要写代码,比如垂直电商,完全可以先用微信朋友圈和
excel 表格人工客服,如果靠谱能挣钱,再写代码也不迟。
我们的物联网平台,做了个最简单的能用的功能,就是 Rabbitmq+mqtt插件 套
个页面。这个阶段的东西不靠谱,但是能用。设计如图所示。
- Web 服务器用来给用户注册申请开通使用。
- 开通后 rabbitmq 上创建一个用户和命名空间给他,设备和手机可以使用
mqtt 连接 rabbitmq 端口,实现数据互通。 - Web 也会连接 rabbitmq 接口,这样用户就能看到通过 rabbitmq 的数据了。
这个 Web 后台操作其实包括很多人工。实际使用中,这个 Web 只是用来注册和
配置,并发要求很低,在后续内容中,会将其忽略。上述架构只保留内核功能的
话,就只有:
简化之后的系统其实类似于即时通信,可以群聊或单聊,能查看历史数据,能看
设备列表。
这个阶段的服务有很多漏洞,但我们小范围验证后,马上就开始开发,抛
弃这个架构了。
MVP
MVP (Minimum Viable Product, 最小可行产品),是 Eric Ries 在
精益创业 里提出的一种软件产品开发迭代方法。简单地说,就是先开发一个满
足用户需求的最小产品,然后获取用户反馈,并持续迭代。它的目的是避免 "窝
在家里做没人要的产品,却自以为很有市场" 的问题。 MVP 过程中,必须每个版本
都是能用的,常有人误解为每个迭代版本做出一部分功能,最后才可用。
在这个物联网系统中,我们要求的最核心的功能是:
- 设备低成本快速接入互通。
- 隐私和数据安全。
- 开放和自由。
只要做到这几点,用户就能用,其他功能只能说是优化。核心功能先做出来,能
用就行,其他要求都不做,或者优先级调低,后续再做。这个阶段小需求变化非常
快,并发量还不在考虑范围内。
我们选用了市面上最常用的设备通信协议MQTT,因为几乎所有模组都有这个协议
的开源适配,我们通过几条规则,做好身份认证和加密即可。 Rabbitmq 虽然优
秀,但实际使用时并没有满足我们功能要求,所以自己开发了 MQTT Brocker。
如上图所示,我们开发了自己的设备接入服务。MySQL 用于存储设备列表、认证
信息、事件、设备数据。还有其它服务用于外部系统对接,满足"开放和自
由"这个要求,但这不在本文重点讨论的范围中,就先忽略了。
这个简单架构持续了很多个功能版本,直到我们推广阶段实在没办法满足并发要
求。
纵向扩展和横向扩展
这里,纵向扩展 (vertical scaling, scale up),指的给服务器是加 CPU,加
内存。横向扩展 (hozizontal scaling, scale out),指的是添加更多服务器。
早期并发不多的时候,纵向扩展是个不错的选择,但纵向扩展是有极限的:
- 一个服务器能使用的CPU和内存数量是有限的。
- 这一个服务器挂了,就全完了。
如果想要上百万并发,只能考虑横向扩展。
负载均衡
前面设备服务程序是单个服务器上的,早期迭代速度非常快,每天一个版本,更
新时经常重启这个程序,重启这段时间所有设备连接都会断开,连不上服务器,
直到重启完成。同时,这个服务程序承担了太多计算任务,包括加密解密、身份
认证、数据转发、数据转换规则匹配等,很快就将服务器的 CPU 消耗殆尽。于
是我们打算使用负载均衡器横向扩展它。
如图所示,负载均衡器会把连接分配到多个服务器上,这样就分担了单个服务器
的压力。当一个服务器重启时,负载均衡器也会将这个服务器的连接分配到另一
个服务器上。当两个服务器压力还是太大,也可以再添加一个服务器。
设备服务经常需要将一个设备的数据发送到另一个设备上,或者将一个设备的数
据发送到另一个设备服务以完成预定规则的匹配计算。这要求增加一个所有设备
服务都共享的信息:一个设备在哪个设备服务器上连接。我们使用 redis 来记
录这个信息。
使用时序数据库
时序数据库非常适合用来存储设备上传的数据。我们当时使用的 MySQL 并不适
合。当时一万个设备,一天就能上传上亿条数据了,放在 MySQL 里面索引都建
不起来。根据我们的使用习惯和扩展要求,我们使用 timescaledb 来保存设备
上传的数据。
如你所见,我们使用 timescaledb 来存储设备上传的数据点,系统运行中记录
的事件(离线重连升级等)。MySQL 用来存储设备列表、设备关系、用户关系、认
证信息。Redis 用来记录设备目前在哪个设备服务上连接。
数据库横向扩展
系统不久遇到了数据库瓶颈。
分布式数据库是个不错的选择,奶飞 选用 casandra 作为数据库系统,看起来
很稳定并发也没什么问题。对于大多数公司而言,MySQL一主多从能满足要求。
再这个物联网平台中,MySQL数据库时写少读多的场景,同时考虑到迁移成本,
我们决定使用 MySQL 主从读写分离,只需要加个 mysql 代理。
MySQL 可以通过binlog实现主从同步,再使用一个 mysql 代理,将 SELECT
请
求都分配到从数据库,将 UPDATE
请求都分配到主数据库。
后续如果设备数量增多,增长到接近亿时,就需要考虑分表 (Charding),这个
也可以使用 mysql 代理实现。但最终我们的系统并没有达到这个量级,没有分
表的必要。
使用缓存
数据库读写分离以后,我们发现一些请求非常频繁,导致数据库压力还是很大。
这些请求的数据通常变化较少,但是查询频繁,如用户和设备的绑定关系、设备
之间的通信关系等。我们决定将这些请求的数据写入到缓存中,以减少数据库压
力。
我们使用 Redis 作为缓存,每条数据都设置有效期。每次查询请求时,先查询
redis,如果有数据就直接返回;如果 redis 里查不到数据,就去 mysql 查询,
查到以后再将数据写回 redis,再返回。修改数据库同时,也修改 redis 内容。
这有几个问题:
- 一致性。一个修改请求,改动 redis。同时另读请求,redis没有数据,查
询mysql写回。这样就可能造成 redis 里面存储旧数据的情况。只能等到数
据缓存过期后才会同步。触发频率和影响范围在我们可接受的范围内。 - 缓存穿透。查询的数据不存在,会一直查询数据库。我们通过无效的key存储
"null" 值解决了这个问题。
使用 CDN
我们接入方案里面包括了设备在线升级功能,设备可能再一段时间内大量请求下
载升级固件文件,占满了服务器流量资源。于是我们决定使用 CDN。
CDN 负责接受固件下载请求,如果文件在CDN服务器上,就直接返回,如果不在
服务器上,就去后端真正的服务器上下载后,再返回给设备。使用 CDN 减轻了
固件下载时我们服务器的压力,同时 CDN 也加快了文件的下载速度。
CDN 是三方服务器,如果请求太频繁,像物联网这样的场景,那么多设
备同时下载,可能会被当成 DDoS 攻击,还是要适当分配下载时间。对文件
的数据签名也是必要的,被人偷摸修改过的固件运行在那么多设备上,是个很麻
烦的事情,很可能需要跑到客户厂区跪着人工把设备拆开,再把正确的固件刷到
设备上。
拆分服务
原本设备服务包括了几乎所有的平台功能,随着功能增多,很多小的需求变化都
需要更新整个程序,部门间合作,以及更新程序时造成了很多麻烦。我们决定将
其拆分,首先拆分为三个部分:身份服务、接入服务、分析服务。
身份服务负责确定设备、用户之间的关系和权限;接入服务负责设备接入和即时
通信转发;分析服务负责即时或离线的数据分析。虽然这样划分是为了协调方便,
对性能和效率并没有多少提升。
横跨机房
承诺全球可用的服务,总要提供跨机房的方案,这是个不小的挑战。
我们使用 DNS 服务分配IP地址,设备开机会先请求DNS,从域名获取一个 IP 地
址。DNS 根据设备所在地区,返回一个最近机房的 IP 给设备,设备通过这个
IP 地址连接。
跨机房通信通常比较慢,经常涉及到跨国通信的延迟,所以不能像单个机房中一
样同步所有的数据,只能同步一部分。我们将设备认证信息、历史数据分配、绑
定关系等存储到 NoSQL 数据库中,并开启多实例同步。当设备转移了地区,或者
一个机房被炸了,这个设备就可以接入到其它机房。这样的转换有可能会丢失部
分数据,这部分不影响基本共功能使用,也可以花时间离线同步过去。