目录
个人项目
一、一站到底
1、如何设计排行榜
2、如何解决重复答题
3、一个题目被多个人抢答
4、如何管理昵称重复
5、如何管理出题定时任务
6:如何解决客户端断连
二、秒杀项目
技术选型
1、如何解决超卖?
2、如何解决重复下单?
3、如何防刷?
4、热key问题如何解决?
5、应对高并发的读请求
6、应对高并发的写请求
7、如何保证数据一致性
8、可靠性如何保障**
9、秒杀系统瓶颈-日志
10、池化技术
三、即时通信
1、单聊消息可靠传输
2、群聊消息如何保证不丢不重
3、如何保证消息的时序性
4:推拉结合
5、好友推荐
四、智慧社区
物联网架构
DCM系统架构
三要素
云 / 边 / 端协同
物联网平台接入
上行数据链路
下行指令链路
门锁接入
各种协议
HTTP协议(CS用户上网)
RESTAPI(松耦合调用)
CoAP协议(无线传感)
MQTT协议(低带宽)
AMQP协议(互操作性)
XMPP协议(即时通信)
JMS (Java消息服务)
Zigbee协议
IOT流量洪峰
上下行拆分
海量Topic下性能
实时消息优先处理
连接、计算、存储分离
消息策略-推拉结合
如何解决海量Topic
社区直播带货
产品的背景
面临的挑战
协议的比较
整体流程
直播流程
播放流程
直播高可用方案
性能优化方案
方法论
七、架构设计
1、社区系统的架构
2、商城系统-亿级商品如何存储
3、对账系统-分布式事务一致性
4、用户系统-多线程数据割接
5、秒杀系统场景设计
6、统计系统-海量计数
7、系统设计 - 微软
1、需求收集
2、顶层设计
3、系统核心指标
4、数据存储
7、如何设计一个微博
八、领域模型落地
1、拆分微服务
2、关联微服务
3、微服务的落地
4、领域模型的意义
5、战略建模
6、相关名词
采用SpringBoot构建项目,主要通过分布式缓存、队列、限流保证系统高可用,Netty、缓存、反向代理保证高并发。
双人对战答题、公司对战抢答
性能优化过程
第一条需求很简单,使用了Redis的Zset实现不过这里总得分采用了基于分数、时间、次数和正确率的混合加权。考虑到数据的持久化,以及关系数据库和缓存的一致性导致的设计的复杂性,使用了谷歌开源的JamsRanking
优点是可以直接使用现成的setScores和getRanking接口封装了Redis和Mysql和消息队列的完成事务和一致性的使用细节。缺点是并发比较低使用Jmeter进行压测,单机只有20左右的TPS**
后来看了下源码,主要是它针对每一次设置都进行了分布式事务处理,并且会返回事务提交或回滚的结果。了解了底层实现以后就去谷歌的开源社区去查阅了相关的解决方案,当时官方对这个问题并没有通过配置能直接解决问题的快捷方式,不过推荐了使用者自身如果对响应时间不高的场景下可以采用批量合并事务的方式进行优化。基于这个思路,我们把写操作进行了封装并放入了队列,然后在消费者端批量取得数据后进行事务的批量处理,压测环境下整体性能达到了500TPS。已经基本满足了线上更新的需求,但是当时压测的过程中,队列偶尔的吞吐量会大范围波动,经常会持续数十秒,然后业务一次性处理完再响应,导致局部响应时间大幅度增长
后来也是在官网上查询,了解到谷歌开源组件使用的队列服务底层是使用BigTable作为持久层,但是当BigTable分片过大时,会触发再分片的过程,再分片的过程中,是不会进行任务分发的,所以就会导致先前的问题。针对这个问题,谷歌官方的建议是提前配置队列的数量、负载策略和最大容量等信息,保证所有队列不同时触发再分片
进行两次优化后,压测环境已经基本可以满足预期了,在实际生产环境的部署中,发现对于事务更新失败时,JamsRanking会对失败的事务进行切分和重试,整个过程对于研发人员是透明的,不利于线上问题排查,所以我们当时特地写了一个watchdog的工具,监控事务回滚达到十次以上的事务,查明原因后通过后台管理系统进行相应补偿,保证最终一致性
最终结果:
针对这个缺点谷歌官方也是给出了使用分片树和近似排名的解决方案,当然复杂的方案有更高的运维成本,所以我们优化工作也就到此为止
方案优化过程
方案1:每日一个滚动榜,当日汇聚(费时间)
首先记录每天的排行榜和一个滚动榜,加分时同时写入这两个榜单,每日零点后跑工具将前几天数据累加写入当日滚动榜,该方案缺点是时间复杂度高,7天榜还好,只需要读过去6天数据,如果是100天榜,该方案需要读过去99天榜,显然不可接受
方案2:全局N个滚动榜同时写(费空间)
要做到每日零点后榜单实时生效,而不需要等待离线作业的完成,一种方案是预写未来的榜单。可以写当天的滚动榜的同时,写往后N-1天的滚动榜一起写入该方案不仅能脱离离线作业做到实时更新,且可以省略每天的日榜。但缺点也不难看出,对于7天滚动榜,每次写操作需要更新7个榜单,但是对于百日榜,空间消耗无法接受,1000万榜单大约消耗1G内存
方案3:实时更新,常数次写操作
有不有办法做到既能实时更新,写榜数量也不随N的增加而增加呢?
仍然是记录每天的排行榜和一个滚动榜,加分操作也还是同时操作当日榜和全局榜,但每日零点的离线作业改为从全局榜中减去之前过期的数据,从而实现先滚动更新。 此方案每次只需读取一个日榜做减法,时间复杂度为O(1);但是无法做到实时更新。 这个方案的优点是在十二点前提前准备好差分榜,到了十二点直接加上当天数据就是滚动榜内容 ,这样就在常数次写操作的前提下,实现了滚动榜的实时更新
利用setnx防止重复答题
分布式锁是控制分布式系统之间同步访问共享资源的一种方式。 利用Redis的单线程特性对共享资源进行串行化处理
// 获取锁推荐使用set的方式String result = jedis.set(lockKey, requestId, "NX", "EX", expireTime);
// 推荐使用redis+lua脚本String lua = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";Object result = jedis.eval(lua, Collections.singletonList(lockKey)
利用redis来实现乐观锁(抢答),好处是答错的人不影响状态,第一个秒杀答对的人才能得分。
1、利用redis的watch功能,监控这个 Corp:Activ:Qust: 的状态值
2、获取Corp:Activ:Qust: 的值,创建redis事务,给这个key的值-1
3、执行这个事务,如果key的值被修改过则回滚,key不变
使用布隆过滤器:
它实际上是一个很长的二进制矢量数组和 K 个哈希函数。当一个昵称加入布隆过滤器中的时候,会进行如下操作:
用户新增昵称时需要首先计算K个哈希值,如果K个哈希值有一个不为0则通过,否则不通过,不通过时通过加随机字符串再次检验,检测通过后返回给前端,帮助用户自动填写。
布隆过滤器的好处是它可以用来判断一个元素是否在一个集合中。它的优势是只需要占用很小的内存空间以及有着高效的查询效率。对于布隆过滤器而言,它的本质是一个位数组:位数组就是数组的每个元素都只占用 1 bit ,并且每个元素只能是 0 或者 1。
BloomFilter 的优势是,全内存操作,性能很高。另外空间效率非常高,要达到 1% 的误判率,平均单条记录占用 1.2 字节即可。而且,平均单条记录每增加 0.6 字节,还可让误判率继续变为之前的 1/10,即平均单条记录占用 1.8 字节,误判率可以达到 1/1000;平均单条记录占用 2.4 字节,误判率可以到 1/10000,以此类推。这里的误判率是指,BloomFilter 判断某个 key 存在,但它实际不存在的概率,因为它存的是 key 的 Hash 值,而非 key 的值,所以有概率存在这样的 key,它们内容不同,但多次 Hash 后的 Hash 值都相同。对于 BloomFilter 判断不存在的 key ,则是 100% 不存在的,反证法,如果这个 key 存在,那它每次 Hash 后对应的 Hash 值位置肯定是 1,而不会是 0
压测环境中服务器通过Netty的主从Reactor多路复用NIO事件模型,单机可以轻松应对十万长连接,但是每个业务中,由于每个用户登录系统后需要按照指定顺序答题,例如一共要答十道,那么服务器针对这一个用户就会产生十个定时任务,所以对于系统来说,定时器的数量就是百万级别的。
通过压测结果发现:JDK自带的Timer,在大概三万并发时性能就急剧下降了。也是此时根据业务场景的需要,将定时任务改成了Netty自带的HashedWheelTimer时间轮方案,通过压测单机在50万级别下依然能够平滑的执行。
也是这个强烈的反差,使我在强烈的好奇心促使下,阅读源码了解到常规的JDK 的Timer 和 DelayedQueue 等工具类,可实现简单的定时任务,单底层用的是堆数据结构,存取复杂度都是 O(NlogN),无法支撑海量定时任务。Netty经典的时间轮方案,正是通过将任务存取及取消操作时间复杂度降为 O(1),而广泛应用在定时任务量大、性能要求高的场景中。
基于Netty的Websocket底层,服务器端维护一个高效批量管理定时任务的调度模型。时间轮一般会实现成一个环形数组结构,类似一个时钟,分为很多槽,一个槽代表一个时间间隔,每个槽使用双向链表存储定时任务。指针周期性地跳动,跳动到一个槽位,就执行该槽位的定时任务。
单层时间轮的容量和精度都是有限的,对于精度要求特别高、时间跨度特别大或是海量定时任务需要调度的场景,可以考虑使用多级时间轮以及持久化存储与时间轮结合的方案。时间轮的定时任务处理逻辑如下:
使用Netty的重连检测狗ConnectionWatchdog
服务端定义refreshTime,当我们从channel中read到了服务端发来的心跳响应消息的话,就刷新refreshTime为当前时间
客户端在state是WRITER_IDLE的时候每隔一秒就发送一个心跳包到sever端,告诉server端我还活着。
当重连成功时,会触发channelActive方法,在这里我们开启了一个定时任务去判断refreshTime和当前时间的时间差,超过5秒说明断线了,要进行重连,最后计算重连次数,尝试连接2次以上连不上就会修改header信息强制重连去连另一个服务器。
秒杀用到的基础组件,主要有框架、KV 存储、关系型数据库、MQ。
框架主要有 Web 框架和 RPC 框架。
其中,Web 框架主要用于提供 HTTP 接口给浏览器访问,所以 Web 框架的选型在秒杀服务中非常重要。在这里,我推荐Gin,它的性能和易用性都不错,在 GitHub 上的 Star 达到了 44k。对比性能最好的 fasthttp,虽然 fasthttp 在请求延迟低于 10ms 时性能优势明显,但其底层使用的对象池容易让人踩坑,导致其易用性较差,所以没必要过于追求性能而忽略了稳定性
至于 RPC 框架,我推荐选用 gRPC,因为它的扩展性和性能都非常不错。在秒杀系统中,Redis 中的数据主要是给秒杀接口服务使用,以便将配置从管理后台同步到 Redis 缓存中。
KV 存储方面,秒杀系统中主要是用 Redis 缓存活动配置,用 etcd 存储集群信息。
关系型数据库中,MySQL 技术成熟且稳定可靠,秒杀系统用它存储活动配置数据很合适。主要 原因还是秒杀活动信息和库存数据都缓存在 Redis 中,活动过程中秒杀服务不操作数据库, 使用 MySQL 完全能够满足需求。
MQ 有很多种,其中 Kafka 在业界认可度最高,技术也非常成熟,性能很不错,非常适合用在秒杀系统中。Kafka 支持自动创建队列,秒杀服务各个节点可以用它自动创建属于自己的队列
方案设计
背景
现象
秒杀系统设计
首先,要尽力将请求拦截在系统上游,层层设阻拦截,过滤掉无效或超量的请求。因为访问量远远大于商品数量,所有的请求打到后端服务的最后一步,其实并没有必要,反而会严重拖慢真正能成交的请求,降低用户体验。
秒杀系统专为秒杀活动服务,售卖商品确定,因此可以在设计秒杀商品页面时,将商品信息提前设计为静态信息,将静态的商品信息以及常规的 CSS、JS、宣传图片等静态资源,一起独立存放到 CDN 节点,加速访问,且降低系统访问压力,在访问前端也可以制定种种限制策略,比如活动没开始时,抢购按钮置灰,避免抢先访问,用户抢购一次后,也将按钮置灰,让用户排队等待,避免反复刷新。
其次,要充分利用缓存,提升系统的性能和可用性。
用户所有的请求进入秒杀系统前,通过负载均衡策略均匀分发到不同 Web 服务器,避免节点过载。在 Web 服务器中,首先检查用户的访问权限,识别并发刷订单的行为。如果发现售出数量已经达到秒杀数量,则直接返回结束,要将秒杀业务系统和其他业务系统进行功能分拆,尽量将秒杀系统及依赖服务独立分拆部署,避免影响其他核心业务系统。
秒杀系统需要构建访问记录缓存,记录访问 IP、用户的访问行为,发现异常访问,提前进行阻断及返回。同时还需要构建用户缓存,并针对历史数据分析,提前缓存僵尸强刷专业户,方便在秒杀期间对其进行策略限制。这些访问记录、用户数据,通过缓存进行存储,可以加速访问,另外,对用户数据还进行缓存预热,避免活动期间大量穿透。
mysql乐观锁+redis预减库存+redis缓存卖完标记
第一是基于数据库乐观锁的方式保证数据并发扣减的强一致性;
第二是基于数据库的事务实现批量扣减部分失败时的数据回滚。
在扣减指定数量前应先做一次前置数量校验的读请求(参考读写分离 + 全缓存方案)
纯数据库乐观锁+事务的方式性能比较差,但是如果不计成本和考虑场景的话也完全够用,因为任何没有机器配置的指标,都是耍流氓。如果我采用 Oracle 的数据库、100 多核的刀锋服务器、SSD 的硬盘,即使是纯数据库的扣减方案,也是可以达到单机上万的 TPS 的。
单线程Redis 的 lua 脚本实现批量扣减
当用户调用扣减接口时,将扣减的 对应数量 + 脚本标示传递至 Redis 即可,所有的扣减判断逻辑均在 Redis 中的 lua 脚本中执行,lua 脚本执行完成之后返还是否成功给客户端。
Redis 中的 lua 脚本执行时,首先会使用 get 命令查询 uuid 进行查重。当防重通过后,会批量获取对应的剩余库存状态并进行判断,如果一个扣减的数量大于剩余数量,则返回错误并提示数量不足。
Redis 的单线程模型,确保不会出现当所有扣减数量在判断均满足后,在实际扣减时却数量不够。同时,单线程保证判断数量的步骤和后续扣减步骤之间,没有其他任何线程出现并发的执行。
当 Redis 扣减成功后,扣减接口会异步的将此次扣减内容保存至数据库。异步保存数据库的目的是防止出现极端情况—— Redis 宕机后数据未持久化到磁盘,此时我们可以使用数据库恢复或者校准数据
最后,运营后台直连数据库,是运营和商家修改库存的入口。商家在运营后台进货物进行补充。同时,运营后台的实现需要将此数量同步的增加至 Redis,因为当前方案的所有实际扣减都在 Redis 中
纯缓存方案虽不会导致超卖,但因缓存不具备事务特性,极端情况下会存在缓存里的数据无法回滚,导致出现少卖的情况。且架构中的异步写库,也可能发生失败,导致多扣的数据丢失
可以借助顺序写的特性,将扣减任务同步插入任务表,发现异常时,将任务表作为undolog进行回滚
可以解决由于网络不通、调用缓存扣减超时、在扣减到一半时缓存突然宕机(故障 failover)了。针对上述请求,都有相应的异常抛出,根据异常进行数据库回滚即可,最终任务库里的数据都是准的
更进一步:由于任务库是无状态的,可以进行水平分库,提升整体性能
mysql唯一索引+分布式锁
IP限流 | 验证码 | 单用户 | 单设备 | IMEI | 源IP |均设置规则
redis集群+本地缓存+限流+key加随机值分布在多个实例中
1、缓存集群可以单节点进行主从复制和垂直扩容
2、利用应用内的前置缓存,但是需注意需要设置上限
3、延迟不敏感,定时刷新,实时感知用主动刷新
4、和缓存穿透一样,限制逃逸流量,单请求进行数据回源并刷新前置
5、无论如何设计,最后都要写一个兜底逻辑,千万级流量说来就来
使用缓存策略将请求挡在上层中的缓存中
使用CDN,能静态化的数据尽量做到静态化,
加入限流(比如对短时间之内来自某一个用户,某一个IP、某个设备的重复请求做丢弃处理)
资源隔离限流会将对应的资源按照指定的类型进行隔离,比如线程池和信号量。
流量控制效果从好到差依次是:漏桶限流 > 令牌桶限流 > 滑动窗口限流 > 计数器限流
其中,只有漏桶算法真正实现了恒定速度处理请求,能够绝对防止突发流量超过下游系统承载能力。
不过,漏桶限流也有个不足,就是需要分配内存资源缓存请求,这会增加内存的使用率。而令牌桶限流算法中的“桶”可以用一个整数表示,资源占用相对较小,这也让它成为最常用的限流算法。正是因为这些特点,漏桶限流和令牌桶限流经常在一些大流量系统中结合使用。
对于单用户多次点击、单设备、IMEI、源IP均设置规则
可以集群限流,但单机限流更加简洁和稳定
对于一段时间内的秒杀活动,需要保证写成功,我们可以使用 消息队列。
削去秒杀场景下的峰值写流量
通过异步处理简化秒杀请求中的业务流程
先处理主要的业务,异步处理次要的业务。
解耦
实现秒杀系统模块之间松耦合将秒杀数据同步给数据团队,有两种思路:
CacheAside旁路缓存读请求不命中查询数据库,查询完成写入缓存,写请求更新数据库后删除缓存数据。
// 延迟双删,用以保证最终一致性,防止小概率旧数据读请求在第一次删除后更新数据库public void write(String key,Object data){ redis.delKey(key); db.updateData(data); Thread.sleep(1000); redis.delKey(key);}
为防缓存失效这一信息丢失,可用消息队列确保。
订阅binlog程序在mysql中有现成的中间件叫canal,重试机制,主要采用的是消息队列的方式。
终极方案:请求串行化
真正靠谱非秒杀的方案:将访问操作串行化
需要解决的问题:
由一个或多个sentinel实例组成sentinel集群可以监视一个或多个主服务器和多个从服务器。哨兵模式适合读请求远多于写请求的业务场景,比如在秒杀系统中用来缓存活动信息。 如果写请求较多,当集群 Slave 节点数量多了后,Master 节点同步数据的压力会非常大。
当主服务器进入下线状态时,sentinel可以将该主服务器下的某一从服务器升级为主服务器继续提供服务,从而保证redis的高可用性。
秒杀服务单节点需要处理的请求 QPS 可能达到 10 万以上。一个请求从进入秒杀服务到处理失败或者成功,至少会产生两条日志。也就是说,高峰期间,一个秒杀节点每秒产生的日志可能达到 30 万条以上
一块性能比较好的固态硬盘,每秒写的IOPS 大概在 3 万左右。也就是说,一个秒杀节点的每秒日志条数是固态硬盘 IOPS 的 10 倍,磁盘都扛不住,更别说通过网络写入到监控系统中。
解决方案
通常可以采用循环队列来保存空闲连接。使用的时候,可以从队列头部取出连接,用完后将空闲连接放到队列尾部。Netty中利用带缓冲区的 channel 来充当队列。
TCP保证消息可靠传输三板斧:超时、重传、确认。服务端和客户端通信MSG和ACK的共计6个报文
在线消息流程:
A 消息请求 MSG:R => S 消息应答 MSG:A => S 消息通知B MSG:N
S 确认通知 ACK:N <= S 确认应答 ACK:A <= B确认请求S ACK:R
超时与重传、确认和去重:
A发出了 MSG:R ,收到了MSG:A之后,在一个期待的时间内,如果没有收到ACK:N,A会尝试将 MSG:R 重发。可能A同时发出了很多消息,所以A需要在本地维护一个等待ack队列,并配合timer超时机制,来记录哪些消息没有收到ACK:N,定时重发。确认ACK保证必达,去重保证唯一
离线消息流程
原方案:根据离线好友的标识,交互拉取指定的消息
优化的方案:
在线的群友能第一时间收到消息;
离线的群友能在登陆后收到消息。
对于同一份群消息的内容,多个离线用户存储了很多份。假设群中有200个用户离线,离线消息则冗余了200份,这极大的增加了数据库的存储压力
方案:
优化:
历史方案:
“消息风暴扩散系数”是指一个消息发出时,变成N个消息的扩散系数,这个系数与业务及数据相关,一定程度上它的大小决定了技术采用推送还是拉取。
优化方案:
Neo4j 图谱数据库
18年初,针对我们Dubbo框架的智慧楼宇项目的单体服务显得十分笨重,需要采用微服务的形式进行架构的重新设计,当时,我阅读了Eric Evans 写的《领域驱动设计:软件核心复杂性应对之道》和Martin fowler的《微服务架构:Microservice》两本重量级书籍,书中了解到转型微服务的重要原因之一就是利用分治的思想减少系统的复杂性,是一种针对复杂问题的宏观设计,来应对系统后来规模越来越大,维护越来越困难的问题。然而,拆分成微服务以后,并不意味着每个微服务都是各自独立地运行,而是彼此协作地组织在一起。这就好像一个团队,规模越大越需要一些方法来组织,这正是我们需要DDD模型为我们的架构设计提供理论并实践的方法。
当时每次版本更新迭代动辄十几个微服务同时修改,有时一个简单的数据库字段变更,也需要同时变更多个微服务,引起了团队的反思:微服务化看上去并没有减少我们的工作量。《企业架构设计》中对于微服务的定义是小而专,但在起初的设计时,我们只片面的理解了小却忽视了专,此时我们才意识到拆分的关键是要保证微服务内高内聚,微服务间低耦合。
物联网是互联网的外延。将用户端延伸和扩展到物与人的连接。物联网模式中,所有物品与网络连接,并进行通信和场景联动。互联网通过电脑、移动终端等设备将参与者联系起来,形成的一种全新的信息互换方式
云端计算、终端计算和边缘计算是一个协同的系统,根据用户场景、资源约束程度、业务实时性等进行动态调 配,形成可靠、低成本的应用方案。从过去几年的发展积累来看,AI 已在物联网多个层面进行融合,比我们合作的海康威视、旷视宇视、商汤科技等纷纷发布了物联网AI相关平台和产品,和移动和小区进行了紧密的融合。
向下连接海量设备,支撑设备数据采集上云;
向上通过调用云端API将指令下发至设备端,实现远程控制。
WIFI门锁:非保活 平常处于断电休眠状态,需要MCU 唤醒才能传输和发送数据
蓝牙门锁:MCU串口对接和SDK对接,近距离单点登录和远距离网关登录
Zigbee门锁:非保活 但是保持心跳,MCU对接,Zigbee协议控制。
NB-Iot门锁:可以通过公网连接,把门禁变成SAAS服务,MCU
名词 |
解释 |
SaaS |
Software-as-a-Service ,提供给客户的服务是运营商运行在云计算基础设施上的应用程序。用户可以在各种设备上通过客户端界面访问应用,例如计算机浏览器。用户不需要管理或控制任何云计算基础设施,包括网络、服务器、操作系统、存储等资源,一切由 SaaS 提供商管理和运维。 |
PaaS |
Platform-as-a-Service,表示平台即服务理念,客户不需要管理或控制底层的云基础设施,包括网络、服务器、操作系统、存储等,但客户能控制部署的应用程序,也可能控制运行应用程序的托管环境配置。 |
IaaS |
Infrastructure-as-a-Service ,表示基础设施即服务理念,提供的服务是对所有计算基础设施的利用,包括 CPU、内存、存储、网络等其它计算资源。用户能够部署和运行任意软件,包括操作系统和应用程序。 |
HTTP协议是典型的CS通讯模式,由客户端主动发起连接,向服务器请求XML或JSON数据。该协议最早是为了适用web浏览器的上网浏览场景和设计的,目前在PC、手机、pad等终端上都应用广泛,但并不适用于物联网场景
REST/HTTP主要为了简化互联网中的系统架构,快速实现客户端和服务器之间交互的松耦合,降低了客户端和服务器之间的交互延迟。因此适合在物联网的应用层面,通过REST开放物联网中资源,实现服务被其他应用所调用。
简化了HTTP协议的RESTful API,它适用于在资源受限的通信的IP网络。
MQTT协议采用发布/订阅模式,物联网终端都通过TCP连接到云端,云端通过主题的方式管理各个设备关注的通讯内容,负责将设备与设备之间消息的转发
适用范围:在低带宽、不可靠的集中星型网络架构(hub-and-spoke),不适用设备与设备之间通信,设备控制能力弱,另外实时性较差,一般都在秒级。协议要足够轻量,方便嵌入式设备去快速地解析和响应。具备足够的灵活性,使其足以为 IoT 设备和服务的多样化提供支持。应该设计为异步消息协议,这么做是因为大多数 IoT 设备的网络延迟很可能非常不稳定,若使用同步消息协议,IoT 设备需要等待服务器的响应,必须是双向通信,服务器和客户端应该可以互相发送消息。
用于业务系统例如PLM,ERP,MES等进行数据交换。
适用范围:最早应用于金融系统之间的交易消息传递,在物联网应用中,主要适用于移动手持设备与后台数据中心的通信和分析。
开源形式组织产生的网络即时通信协议。被IETF国际标准组织完成了标准化工作
适用范围:即时通信的应用程序,还能用在协同工具、游戏等。
XMPP在通讯的业务流程上是更适合物联网系统的,开发者不用花太多心思去解决设备通讯时的业务通讯流程,相对开发成本会更低。但是HTTP协议中的安全性以及计算资源消耗的硬伤并没有得到本质的解决。
Java消息服务(Java Message Service)应用程序接口,是一个Java平台中关于面向消息中间件(MOM)的API,用于在两个应用程序之间,或分布式系统中发送消息,进行异步通信。
低功耗,它保持IEEE 802.15.4(2003)标准
智慧社区IOT领域,不管是嵌入式芯片还是应用服务器都需要传递消息,常见上行的消息有:人脸识别开门、烟感雾感告警、共享充电桩充电,下行的广告下发、NB门禁开门指令、超级门板显示等,由于物联网设备时不时会故障和断网导致大量的流量洪峰,传统消息队列需要针对性优化。
上行消息特征:并发量高、可靠性和时延性要求低
下行消息特征:并发量低、控制指令的成功率要求高
Kafka海量Topic性能会急剧下降,Zookeeper协调也有瓶颈
多泳道消息队列可以实现IoT消息队列的故障隔离
NB门禁实时产生的开门指令必须第一优先级处理,堆积的消息降级
设计成无序、不持久化的,并与传统的FIFO队列隔离
Broker只做流转分发,实现无状态和水平扩展
计算交给Flink,存储交给nosqlDB,实现高吞吐写
MQTT针对电池类物联网设备,AMQP针对安全性较高的门禁设备
消费端离线时存到queue,在线时将实时消息和从queue中拉取的消息一起推送
首先要做的就是分区、分组等水平拆分的方式,接下来考虑单实例如何处理更多Topic,传统消息队列在海量Topic下顺序写会退化成随机写,性能大幅下降
基于这个思路,使用Kafka Streams或者Hbase列存储来聚合
针对单个Topic海量订阅的问题,可以在上层封装广播组件来协调批量发送
使用端 / 边 / 云三级架构,客户端加密传输,边缘节点转发、云侧转码并持久化
上线时间,从调研到正式上线用了 3个月时间,上线后一个月内就要经历双十二挑战。在这么紧的上线时间要求下,需要用到公司提供的所有优势,包括cdn网络,直播牌照等
协议 |
上线时间 |
网络兼容 |
端对端延迟 |
应用大小 |
问题 |
WebRTC |
✗ |
Webrtc 基于 UDP,和社区应用的网络架构不兼容 |
|||
HTTP Upload |
✗ |
会导致网络高延迟 |
|||
Custom Protocol |
✗ |
工程师需要实现自己的客户端与服务端的库,无法按时上线 |
|||
Proprietary |
✗ |
协议就需要几兆的空间,超出额度 |
|||
RTMPS |
✔ |
✔ |
✔ |
✔ |
TCP实时传输消息协议,更安全更可靠 |
RTMPS:基于TCP实时传输消息协议,更安全更可靠
MPEG-DASH:是一种基于HTTP协议自适应比特率流媒体技术,应对复杂的环境
云端转码主要有两种分辨率400x400 和 720x720.
如果用户网络不好**MPEG-DASH**会自动转换成低分辨率
收获
网络可靠性:
惊群效应:
数据库优化: 数据库是最容易成为瓶颈的组件,考虑从 SQL 优化或者数据库本身去提高它的性能。如果瓶颈依然存在,则会考虑分库分表将数据打散,如果这样也没能解决问题,则可能会选择缓存组件进行优化
集群最优:存储节点的问题解决后,计算节点也有可能发生问题。一个集群系统如果获得了水平扩容的能力,就会给下层的优化提供非常大的时间空间,由最初的 3 个节点,扩容到最后的 200 多个节点,但由于人力问题,服务又没有什么新的需求,下层的优化就一直被搁置着。
硬件升级:水平扩容不总是有效的,原因在于单节点的计算量比较集中,或者 JVM 对内存的使用超出了宿主机的承载范围。在动手进行代码优化之前,我们会对节点的硬件配置进行升级。
代码优化:代码优化是提高性能最有效的方式,但需要收集一些数据,这个过程可能是服务治理,也有可能是代码流程优化。比如JavaAgent 技术,会无侵入的收集一些 profile 信息,供我们进行决策。
并行优化:并行优化是针对速度慢的接口进行并行调用。所以我们通常使用 ContDownLatch 对需要获取的数据进行并行处理,效果非常不错,比如在 200ms 内返回对 50 个耗时 100ms 的下层接口的调用。
JVM 优化: JVM 发生问题时,优化会获得巨大的性能提升。但在 JVM 不发生问题时,它的优化效果有限。但在代码优化、并行优化、JVM 优化的过程中,JVM 的知识却起到了关键性的作用
操作系统优化:操作系统优化是解决问题的杀手锏,比如像 HugePage、SWAP、“CPU 亲和性”这种比较底层的优化。但就计算节点来说,对操作系统进行优化并不是很常见。运维在背后会做一些诸如文件句柄的调整、网络参数的修改,这对于我们来说就已经够用了
流量回放自动化测试
系统级的重构,测试回归的工作量至少都是以月为单位,对于人力的消耗巨大。一种应对方案是,先不改造,到系统实在扛不住了再想办法。另一种应对方案是,先暂停需求,全力进行改造。但在实际工作场景中,上述应对策略往往很难实现。
场景:
1、读服务均是查询,它是无状态的。
2、不管是架构升级还是日常需求,读服务对外接口的出入参格式是没有变化的
Spring 里的 Interceptor 、Servlet 里的 Filter 过滤器,对所有请求的入参和出参进行记录,并通过 MQ 发送出去。(注意错峰、过滤写、去重等)
离线回放:只调用新服务,将返回的数据和日志里的出参进行比较,日志比较大
实时回放:去实时调用线上系统和被测系统,并存储实时返回回放的结果信息,线上有负担
并行回放:新版本不即时上线,每次调用老版本接口时概率实时回放新版本接口,耗时间周期
采用文本对比,可以直观地看到哪个字段数据有差异,从而更快定位到问题。正常情况下,只要存在差异的数据,均可认为是 Bug,是需要进行修复的。
Discovery
考虑企业战略,分析客户需求,制定产品目标
由外到内:竞争对手的方案,为什么做,以后怎么发展,如何去优化。
自上而下:基于公司的战略,考虑自身能力和所处环境。
自下而上:从资源、历史问题、优先级出发,形成一套可行性实施方法。
Define
基于收集的信息,综合跨业务线的抽象能力和服务,先做什么后做什么,怎么做
设计新的架构,重点设计解决痛点问题。
拆分业务领域,重点划分工作临界上下文。
Design
详细的业务设计,功能设计,交付计划,考核计划
产品愿景,产品形态,相关竞品方案对比,价值、优势、收益
梳理业务范围,要知道电商领域四大流(信息流、商流、资金流、物流)
MVP最小可用比,让客户和老大看到结果,最后通编写story把故事编圆
Delivery
交付阶段,根据反馈及时调整中台战略,减少损失和增大收益
合理制定每个阶段的绩效考核目标:
40%稳定+25%业务创新+20%服务接入+15%用户满意度
系统拆分
通过DDD领域模型,对服务进行拆分,将一个系统拆分为多个子系统,做成SpringCloud的微服务。微服务设计时要尽可能做到少扇出,多扇入,根据服务器的承载,进行客户端负载均衡,通过对核心服务的上游服务进行限流和降级改造。
一个服务的代码不要太多,1 万行左右,两三万撑死了吧。
大部分的系统,是要进行多轮拆分的,第一次拆分,可能就是将以前的多个模块该拆分开来了,比如说将电商系统拆分成订单系统、商品系统、采购系统、仓储系统、用户系统等等吧。
但是后面可能每个系统又变得越来越复杂了,比如说采购系统里面又分成了供应商管理系统、采购单管理系统,订单系统又拆分成了购物车系统、价格系统、订单管理系统。
CDN、Nginx静态缓存、JVM缓存
利用Java的模板thymeleaf可以将页面和数据动态渲染好,然后通过Nginx直接返回。动态数据可以从redis中获取。其中redis里的数据由一个缓存服务来进行消费指定的变更服务。
商品数据,每条数据是 10kb。100 条数据是 1mb,10 万条数据是 1g。常驻内存的是 200 万条商品数据,占用内存是 20g,仅仅不到总内存的 50%。目前高峰期每秒就是 3500 左右的请求量。
缓存
Redis cluster,10 台机器,5主5从,5 个节点对外提供读写服务,每个节点的读写高峰 QPS 可能可以达到每秒 5 万,5 台机器最多是 25 万读写请求每秒。
32G 内存+ 8 核 CPU + 1T 磁盘,但是分配给 Redis 进程的是 10g 内存,一般线上生产环境,Redis 的内存尽量不要超过 10g,超过 10g 可能会有问题。
因为每个主实例都挂了一个从实例,所以是高可用的,任何一个主实例宕机,都会自动故障迁移,Redis 从实例会自动变成主实例继续提供读写服务。
MQ
可以通过消息队列对微服务系统进行解耦,异步调用的更适合微服务的扩展
同时可以应对秒杀活动中[应对高并发写请求](# 6、应对高并发的写请求),比如kafka在毫秒延迟基础上可以实现10w级吞吐量
针对IOT流量洪峰做了一些特殊的优化,保证消息的及时性
同时可以使用消息队列保证分布式系统最终一致性
分库分表
分库分表,可能到了最后数据库层面还是免不了抗高并发的要求,好吧,那么就 将一个数据库拆分为多个库,多个库来扛更高的并发;然后将一个表拆分为多个 表,每个表的数据量保持少一点,提高 sql 跑的性能。在通讯录、订单和商城商品模块超过千万级别都应及时考虑分表分库
读写分离
读写分离,这个就是说大部分时候数据库可能也是读多写少,没必要所有请求都 集中在一个库上吧,可以搞个主从架构,主库写入,从库读取,搞一个读写分离。 读流量太多的时候,还可以加更多的从库。比如统计监控类的微服务通过读写分离,只需访问从库就可以完成统计,例如ES
ElasticSearch
Elasticsearch,简称 es。es 是分布式的,可以随便扩容,分布式天然就可以支撑高并发,因为动不动就可以扩容加机器来扛更高的并发。那么一些比较简单的查询、统计类的操作,比如运营平台上的各地市的汇聚统计,还有一些全文搜索类的操作,比如通讯录和订单的查询。
基于 Hash 取模、一致性 Hash 实现分库分表
高并发读可以通过多级缓存应对
大促销热key读的问题通过redis集群+本地缓存+限流+key加随机值分布在多个实例中
高并发写的问题通过基于 Hash 取模、一致性 Hash 实现分库分表均匀落盘
业务分配不均导致的热key读写问题,可以根据业务场景进行range分片,将热点范围下的子key打散
具体实现:预先设定主键的生成规则,根据规则进行数据的分片路由,但这种方式会侵入商品各条线主数据的业务规则,更好的方式是基于分片元数据服务器(即每次访问分片前先询问分片元服务器在路由到实际分片)不过会带来复杂性,比如保证元数据服务器的一致性和可用性。
尽量避免分布式事务,单进程用数据库事务,跨进程用消息队列
主流实现分布式系统事务一致性的方案:
理论上也可以使用(2PC两阶段提交、3PC三阶段提交、TCC短事务、SAGA长事务方案),但是这些方案工业上落地代价很大,不适合互联网的业界场景。针对金融支付等需要强一致性的场景可以通过前两种方案实现。(展开说的话参考分布式事务)
本地数据库事务原理:undo log(原子性) + redo log(持久性) + 数据库锁(原子性&隔离性) + MVCC(隔离性)
分布式事务原理:全局事务协调器(原子性) + 全局锁(隔离性) + DB本地事务(原子性、持久性)
一、我们公司账单系统和第三方支付系统对账时,就采用“自研补偿/MQ方案 + 人工介入”方式
落地的话:方案最“轻”,性能损失最少。可掌控性好,简单易懂,易维护。
考虑到分布式事务问题是小概率事件,留有补救余地就行,性能的损失可是实打实的反应在线上每一个请求上
二、也了解到业界比如阿里成熟Seata AT模式,平均性能会降低35%以上
我觉得不是特殊的场景不推荐
三、RocketMQ事务消息
听起来挺好挺简单的方案,但它比较挑业务场景,同步性强的处理链路不适合。
【重要】要求下游MQ消费方一定能成功消费消息。否则转人工介入处理。
【重要】千万记得实现幂等性。
由于项目需要进行数据割接,保证用户多平台使用用户感知的一致,将广东项目的几百万用户及业务数据按照一定的逻辑灌到社区云平台上,由于依赖了第三方统一认证和省侧crm系统,按照之前系统内割接的方法,通过数据库将用户的唯一标识查出来然后使用多线程向省侧crm系统获取结果。
但是测试的过程中,发现每个线程请求的数据发生了错乱,导致每个请求处理的数据有重复,于是立即停止了脚本,当时怀疑是多线程对资源并发访问导致的,于是把ArrayList 改成了CopyOnWriteArrayList,但是折腾了一晚上,不管怎么修改,线程之间一直有重复数据,叫了一起加班的同事也没看出问题来,和同事估算了一下不使用多线程,大概30-40个小时能跑完,想了下也能接受,本来已经准备放弃了。
不过回到家,我还是用多线程仔细单步模拟了下,整个处理的过程,发现在起线程的时候,有些子线程并没有把分配给他的全部id的list处理完,导致最终状态没更新,新线程又去执行了一遍,然后我尝试通过修改在线程外深拷贝一个List再作为参数传入到子线程里,(后续clear的时候也是clear老的List)果然,整个测试过程中再也没出现过重复处理的情况。
事后,我也深究了下原因:
if(arrayBuffer.length == 99) {
val asList = arrayBuffer.toList
exec.execute ( openIdInsertMethod(asList) )
arrayBuffer.clear
}
在一个线程中开启另外一个新线程,则新开线程称为该线程的子线程,子线程初始优先级与父线程相同。不过主线程先启动占用了cpu资源,因此主线程总是优于子线程。然而,即使设置了优先级,也无法保障线程的执行次序。只不过,优先级高的线程获取CPU资源的概率较大,优先级低的并非没机会执行。
所以主线程上的clear操作有可能先执行,那么子线程中未处理完的数据就变成一个空的数组,所以就出现了多个线程出现了重复数据的原因,所以我们要保证的是子线程每次执行完后再进行clear即可。而不是一开始定位的保证ArrayList的安全性。所以将赋值(buffer->list)操作放在外面去执行后,多线程数据就正常了。
[见秒杀项目方案设计](# 二、秒杀项目)
中小规模的计数服务(万级)
最常见的计数方案是采用缓存 + DB 的存储方案。当计数变更时,先变更计数 DB,计数加 1,然后再变更计数缓存,修改计数存储的 Memcached 或 Redis。这种方案比较通用且成熟,但在高并发访问场景,支持不够友好。在互联网社交系统中,有些业务的计数变更特别频繁,比如微博 feed 的阅读数,计数的变更次数和访问次数相当,每秒十万到百万级以上的更新量,如果用 DB 存储,会给 DB 带来巨大的压力,DB 就会成为整个计数服务的瓶颈所在。即便采用聚合延迟更新 DB 的方案,由于总量特别大,同时请求均衡分散在大量不同的业务端,巨大的写压力仍然是 DB 的不可承受之重。
大型互联网场景(百万级)
直接把计数全部存储在 Redis 中,通过 hash 分拆的方式,可以大幅提升计数服务在 Redis 集群的写性能,通过主从复制,在 master 后挂载多个从库,利用读写分离,可以大幅提升计数服务在 Redis 集群的读性能。而且 Redis 有持久化机制,不会丢数据
一方面 Redis 作为通用型存储来存储计数,内存存储效率低。以存储一个 key 为 long 型 id、value 为 4 字节的计数为例,Redis 至少需要 65 个字节左右,不同版本略有差异。但这个计数理论只需要占用 12 个字节即可。内存有效负荷只有 12/65=18.5%。如果再考虑一个 long 型 id 需要存 4 个不同类型的 4 字节计数,内存有效负荷只有 (8+16)/(65*4)= 9.2%。
另一方面,Redis 所有数据均存在内存,单存储历史千亿级记录,单份数据拷贝需要 10T 以上,要考虑核心业务上 1 主 3 从,需要 40T 以上的内存,再考虑多 IDC 部署,轻松占用上百 T 内存。就按单机 100G 内存来算,计数服务就要占用上千台大内存服务器。存储成本太高。
微博、微信、抖音(亿级)
定制数据结构,共享key 紧凑存储,提升计数有效负荷率
超过阈值后数据保存到SSD硬盘,内存里存索引
冷key从SSD硬盘中读取后,放入到LRU队列中
自定义主从复制的方式,海量冷数据异步多线程并发复制
确认使用的对象(ToC:高并发,ToB:高可用)
系统的服务场景(即时通信:低延迟,游戏:高性能,购物:秒杀-一致性)
用户量级(万级:双机、百万:集群、亿级:弹性分布式、容器化编排架构)
百万读:3主6从,每个节点的读写高峰 QPS 可能可以达到每秒 5 万,可以实现15万,30万读性能
亿级读,通过CDN、静态缓存、JVM缓存等多级缓存来提高读并发
百万写,通过消息队列削峰填谷,通过hash分拆,水平扩展分布式缓存
亿级写,redis可以定制数据结构、SSD+内存LRU、冷数据异步多线程复制
持久化,(Mysql)承受量约为 1K的QPS,读写分离提升读并发,分库分表提升写并发
核心功能包括什么:
写功能:发送微博
读功能:热点资讯
交互:点赞、关注
键值存储 : Redis ( 热点资讯 )
文档存储 : MongoDB ( 微博文档分类)
分词倒排:Elasticsearch(搜索)
列型存储:Hbase、BigTable(大数据)
图形存储:Neo4j (社交及推荐)
多媒体:FastDfs(图文视频微博)
实现哪些功能:
筛选出核心功能(Post a Tweet,Timeline,News Feed,Follow/Unfollow a user,Register/Login)
承担多大QPS:
QPS = 100,那么用我的笔记本作Web服务器就好了
QPS = 1K,一台好点的Web 服务器也能应付,需要考虑单点故障;
QPS = 1m,则需要建设一个1000台Web服务器的集群,考虑动态扩容、负载分担、故障转移
一台 SQL Database (Mysql)承受量约为 1K的QPS;
一台 NoSQL Database (Redis) 约承受量是 20k 的 QPS;
一台 NoSQL Database (Memcache) 约承受量是 200k 的 QPS;
微服务战略拆分
针对不同服务选择不同存储
设计数据表的结构
基本差不多就形成了一个解决方案,但是并不是完美的,仍然需要小步快跑的不断的针对消息队列、缓存、分布式事务、分表分库、大数据、监控、可伸缩方面进行优化
微服务内高内聚,微服务间低耦合
微服务内高内聚即单一职责原则
每个微服务中的代码变化都是同一类原因。因这类原因而需要变更的代码都在这个微服务中,与其他微服务无关,那么就可以将代码修改的范围缩小到这个微服务内。把这个微服务修改好了,独立修改、独立发布,该需求就实现了。这样,微服务的优势才能发挥出来。
微服务间低耦合开放封闭原则
就是说在微服务实现自身业务的过程中,如果需要执行的某些过程不是自己的职责,就应当将这些过程交给其他微服务去实现,你只需要对它的接口进行调用。这样,微服务之间的调用就实现了解耦。
领域建模就是将一个系统划分成了多个子域,每个子域都是一个独立的业务场景,每个子域的边界就是“限界上下文”。该业务场景会涉及许多领域对象,但分析建模始终需要围绕着业务场景的上下文进行。
领域事件通知机制最有效的方式就是通过消息队列,实现领域事件在微服务间的通知。
“核心通讯录”微服务只负责发送变更消息到消息队列,不管谁会接收并处理这些消息;
“门禁管理”微服务只负责接收照片变更消息,不管谁发送的这个消息。
解决物业多品牌、多系统应用造成的信息孤岛,数据混乱的问题
通过图像视频识别、传感数据采集,实现报警联动和风险预警
围绕业主需求,逐步引入社区医疗、社区养老、社区团购、社区家政等服务
实现跨品牌的产品体验,支持基于matrix引擎的智能生活场景裂变能力
- 智能充电 | 云广播 | 出入提醒 | 定向投放
通过合理的微服务设计,尽量让每次的需求变更都交给某个小团队独立完成,让需求变更落到某个微服务上进行变更。唯有这样,每次变更只需独立地修改这个微服务,独立打包、独立升级,新需求独立实现,才能发挥微服务的优势。
贫血模型、充血模型、策略模式、装饰者模式只是DDD实现的方式,而DDD的真谛是领域建模。
做事不能仅凭一腔热血,一定要符合自然规律。其实软件的设计开发过程也是这样。对业务理解不深刻全局架构设计往往是过度设计,这时候应该抓主要流程,开始领域建模。
保持软件设计不退化的关键在于每次需求变更的设计,只有保证每次需求变更时做出正确的设计,才能保证软件以一种良性循环的方式不断维护下去。
有没有一种方法,让我们在第十次变更、第二十次变更、第三十次变更时,依然能够找到正确的设计呢?有,那就是领域驱动设计
那么在每次需求变更时,将变更还原到真实世界中,看看真实世界是什么样子的,根据真实世界进行变更。
领域和子域(Domain/Subdomain)
在上下文地图构建的领域中,对应模块,使用限界上下文划分领域,对应微服务
限界上下文(Bounded Context)
在一个领域/子域中,有概念上的领域边界,任何领域对象在该边界内部的有不依赖外部的确切含义。
领域对象
服务、实体与值对象是领域驱动设计的领域对象,可以通过贫血模型和充血模型转换为程序设计
实体和值对象
通过一个唯一标识字段来区分真实世界中的每一个个体的领域对象,称为实体。真实世界中那些一成不变的、本质性的事物的领域对象,称为值对象。 可变性是实体的特点,而不变性则是值对象的本质。
贫血模型与充血模型
POJO对象中只保存get/set方法,没有任何业务逻辑,这样的设计被称为贫血模型
充血模型是封装和继承思想的体现,门禁设备实体中,包含特征值下发、广告下发、通行记录回调等方法,不同厂商的实体针对多态进行聚合,并通过工厂或仓库对外提供服务。在充血模型中, Service 只干一件非常简单的事,就是直接去调用对象中的工厂方法生成不同产品,其他的什么都不干。
聚合
聚合体现的是一种整体与部分的关系。正是因为有这样的关系,在操作整体的时候,整体就封装了对部分的操作。如何正确理解是否存在聚合的关系:就是当整体不存在时,部分就变得没有了意义。部分是整体的一个部分,与整体有相同的生命周期。
工厂
通过装配,创建领域对象,是领域对象生命周期的起点。譬如,系统要通过 ID 装载一个访客申请:
仓库
如果服务器是一个非常强大的服务器,那么我们不需要任何数据库。系统创建的所有领域对象都放在仓库中,当需要这些对象时,通过 ID 到仓库中去获取。