分布式
kafka定义
kafka是一个分布式的基于发布/订阅模式的消息队列,主要应用于大数据处理领域。
图1 Kafka架构图
- Producer :消息生产者,向kafka broker 发消息的客户端;
- Consumer :消息消费者,从kafka broker 取消息的客户端;
- Consumer Group(CG):消费者组,由多个consumer 组成。消费者组内每个消费者负责消费不同分区的数据,一个分区只能由一个组内消费者消费,消费者组之间互不影响。所有的消费者都属于某个消费者组,即消费者组是逻辑上的一个订阅者。
- Broker :一台kafka 服务器就是一个broker,一个集群由多个broker 组成,一个broker可以容纳多个topic。
- Topic :可以理解为一个队列,生产者和消费者面向的都是一个topic;
- Partition:为了实现扩展性,一个非常大的topic可以分布到多个broker(即服务器)上,一个topic 可以分为多个partition,每个partition 是一个有序的队列;
- Replica:副本,为保证集群中的某个节点发生故障时,该节点上的partition 数据不丢失,且kafka仍然能够继续工作, kafka提供了副本机制,一个 topic的每个分区都有若干个副本,一个 leader和若干个 follower。
- Leader:每个分区多个副本的"主",生产者发送数据的对象,以及消费者消费数据的对象都是 leader。
- Follower:每个分区多个副本中的"从",实时从 leader中同步数据,保持和 leader数据的同步。 leader发生故障时,某个 follower会成为新的 follower。
使用消息队列的好处
1)解耦
允许你独立的扩展或修改两边的处理过程,只要确保它们遵守同样的接口约束。
2)可恢复性
系统的一部分组件失效时,不会影响到整个系统。消息队列降低了进程间的耦合度,所以即使一个处理消息的进程挂掉,加入队列中的消息仍然可以在系统恢复后被处理。
3)缓冲
有助于控制和优化数据流经过系统的速度,解决生产消息和消费消息的处理速度不一致的情况。
4)灵活性 & 峰值处理能力
在访问量剧增的情况下,应用仍然需要继续发挥作用,但是这样的突发流量并不常见。如果为以能处理这类峰值访问为标准来投入资源随时待命无疑是巨大的浪费。使用消息队列能够使关键组件顶住突发的访问压力,而不会因为突发的超负荷的请求而完全崩溃。
5)异步通信
很多时候,用户不想也不需要立即处理消息。消息队列提供了异步处理机制,允许用户把一个消息放入队列,但并不立即处理它。想向队列中放入多少消息就放多少,然后在需要的时候再去处理它们。
消息队列的两种模式
- 点对点模式(一对一)
- 发布/订阅模式(一对多)
- 消费者主动拉取(kafka):消费得速度由消费者决定,维护长轮询
- 队列主动推送
Zookeeper优缺点
- zookeeper不是为高可用性设计的
- 由于要跨机房容灾,很多系统实际上是需要跨机房部署的。出于性价比的考虑我们通常会让多个机房同时工作,而不会搭建N倍的冗余。也就是说单个机房肯定撑不住全流量。由于zookeeper集群只能有一个master,因此一旦机房之间连接出现故障,zookeeper master就只能照顾一个机房,其他机房运行的业务模块由于没有master都只能停掉。于是所有流量集中到有master的那个机房,于是系统crash。
- 即使是在同一个机房里面,由于网段的不同,在调整机房交换机的时候偶尔也会发生网段隔离的情况。实际上机房每个月基本上都会发生短暂的网络隔离之类的子网段调整。在那个时刻zookeeper将处于不可用状态。如果整个业务系统基于zookeeper(比如要求每个业务请求都先去zookeeper获取业务系统的master地址),则系统的可用性将非常脆弱。
- 由于zookeeper对于网络隔离的极度敏感,导致zookeeper对于网络的任何风吹草动都会做出激烈反应。这使得zookeeper的'不可用'时间比较多,我们不能让zookeeper的'不可用',变成系统的不可用。
- zookeeper的选举过程速度很慢
这是一个很难从理论分析上看到的弱点,但是你一旦遇到就会痛不欲生,网络实际上常常是会出现隔离等不完整状态的,而zookeeper对那种情况非常敏感。一旦出现网络隔离,zookeeper就要发起选举流程。zookeeper的选举流程通常耗时30到120秒,期间zookeeper由于没有master,都是不可用的。对于网络里面偶尔出现的,比如半秒一秒的网络隔离,zookeeper会由于选举过程,而把不可用时间放大几十倍。
- zookeeper的性能是有限的
- 典型的zookeeper的tps大概是一万多,无法覆盖系统内部每天动辄几十亿次的调用。因此每次请求都去zookeeper获取业务系统master信息是不可能的。
- 因此zookeeper的client必须自己缓存业务系统的master地址。
- 因此zookeeper提供的'强一致性'实际上是不可用的。如果我们需要强一致性,还需要其他机制来进行保障:比如用自动化脚本把业务系统的old master给kill掉,但是那会有很多陷阱。
- zookeeper无法进行有效的权限控制
在大型的复杂系统里面,使用zookeeper必须自己再额外的开发一套权限控制系统,通过那套权限控制系统再访问zookeeper,额外的权限控制系统不但增加了系统复杂性和维护成本,而且降低了系统的总体性能
- 有了zookeeper也很难避免业务系统的数据不一致
- 由于zookeeper的性能限制,我们无法让每次系统内部调用都走zookeeper,因此总有某些时刻,业务系统会存在两个master(业务系统client那边缓存的业务系统master信息是定时从zookeeper更新的,因此会有更新不同步的问题)。
- 如果要在业务系统client的master信息不一致的情况下,仍要保持系统的数据一致性,唯一的方法是"先kill掉老master,再在zookeeper上更新master信息"。但是在是否要kill current master这个问题上,程序是无法完全自动决定的(因为网络隔离的时候zookeeper已经不可用了,自动脚本没有全局信息,不管怎么做都可能是错的,什么都不做也可能是错的。当网络故障的时候,只有运维人员才有全局信息,程序是无法接电话得知其他机房的情况的)。因此系统无法自动的保障数据一致性,必须要人工介入。而人工介入的典型时间是半个小时以上,我们不能让系统这么长时间不可用。因此我们必须在某个方向上进行妥协,最常见的妥协方式是放弃'强一致性',而接受'最终一致性'。
- 如果我们需要人工介入才能保证'可靠的强一致性',那么zookeeper的价值就大打折扣。
- 我们能做什么
- 我们或者选择人工介入的强一致性,或者选择程序自动化进行的弱一致性。需要进行取舍。
- 最终一致性甚至未必是程序来做的,有时候人工修正数据反而在灵活、可靠、低成本上有优势。这需要权衡。
- 不要迷信zookeeper,有时候不妨考虑一下主备数据库。数据库自带权限控制,用起来比zookeeper方便多了。
- zookeeper比较有价值的东西也许是内容变化的时候,可以阻塞回调的方式通知所有在线的client实时更新信息,但这个功能用处不大。因为php这样的模块你很难说它是在线还是离线,每次都是新发起的。一旦这个功能无法支持php,就无法覆盖整个系统,那么就无法保证强一致性了。
Dubbo
Dubbo运行流程:先用容器Container(以Docker为代表的应用级虚拟化技术)启动我们的服务Provider。然后将其IP(192.169.0.1)注册到注册中心Registry去(通常我们会采用ZooKeeper这样的分布式应用服务协调程序)。接着我的服务调用方Comsumer就会去注册中心去订阅Subscribe相关服务。注册中心会异步的方式将你消费者需要的服务的地址列表推送Notify给你。消费者就会将整个地址列表缓存在本地,当业务需求到来时,就会使用地址列表里的地址去请求相关的服务程序。至于Monitor顾名思义就是监视器的含义,消费者和提供者会定时将服务的调用次数和被调用的次数发送给监视器。
- Provider: 暴露服务的服务提供方
- Consumer: 调用远程服务的服务消费方
- Registry: 服务注册与发现的注册中心
- Monitor: 统计服务的调用次数和调用时间的监控中心
- Container: 服务运行容器
- Dubbo模块
- dubbo-config:配置层。解析dubbo中的xml文件生成config对象。
- dubbo-proxy:服务代理层。负责生产消费端的代理对象。
- dubbo-registry:注册中心层。负责服务注册与查询服务,以及注册服务的本地缓存。
- dubbo-cluster:路由层。负责消费端的负载均衡策略,以及在访问某个节点服务失败以后的调整策略。
- dubbo-monitor:监控层。RPC服务端被调用次数还有消费方调用时间的监控。
- dubbo-rpc:远程调用层。封将RPC调用、支持RMI 、Hessian、Http、Webservice、thrift等rpc调用方式。
- dubbo-remoting:信息交换层。比较底层的模块,是封装请求和响应,处理各种通信协议,支持netty,mina,http。默认采用netty通信。
- dubbo-common:序列化层。Common就是复用度很高的模块,主要是数据序列化和序列化线程池之类的工具。
JSF
JSF相比于Dubbo而言多了一个注册中心寻址服务,主要是因为2015年双十一时候注册中心挂了以后,后端容器服务重启以后之前缓存的服务地址列表丢失,服务无法调用。而且以Zookeeper为注册中心的Dubbo,会受制于Zookeeper的缺点:在Zookeeper主节点挂了以后,新的主节点被选出来之前,Zookeeper集群不可用。而且Zookeeper没有动态水平扩展的能力。由此可见注册中心是瓶颈,所以京东在这一块做了改进,抛弃Zookeeper集群,自己取长补短的去实现一个服务治理。
- 引入了index概念,将注册中心根据不同的业务分片,不同的业务模块访问不同的注册中心以实现负载均衡,而不像以前的注册中心逻辑上就是一个整体。因此也需要注册中心寻址服务,在一开始定位到自己的注册中心。
- 因为没了Zookeeper的主从架构来保证分布式的数据一致性,所以就得自己去实现数据的一致性,京东则是采用的数据库来实现数据的一致性。同时为了保障高可用,注册中心内有服务列表的缓存,所以即便数据库挂了,注册中心依旧可用
- 注册中心服务既然通过数据库解决了数据的一致性问题,自然可以实现水平扩容,监控到压力大即可进行动态水平扩展
- Zookeeper优点就是其有着订阅推送的功能来实现服务列表更新的时候会推送给客户端,京东采用的是CallBack的方式,只要注册中心发现出现变化,调用消费者的回调函数,消费者就可以知道服务列表的更新。
- 如果注册中心寻址服务挂了,注册中心也就成了断线纸鸢,所以只要在消费端本地再缓存一个注册中心地址即可。
JMQ
JMQ是京东自主研发的一款消息中间件系统,具有高可用、数据高可靠等特性。广泛应用于公司内部系统,包括订单、支付、库房等场景。整体结构包括服务端、客户端、管理端与其他支撑模块。
- 服务端
服务端提供了配置信息分发、重试消息管理和消息存储与分发这三大类功能。每个服务端实例都具备这三类功能的服务能力,但是在实际部署上这三类功能对应三个不同的集群,对应每一个实例功能不叠加。在测试环境和库房等资源有限的环境下,这三类功能由同一个服务端实例提供服务。
- 配置信息分发:负责客户端参数变更时与消息分配的服务端实例变更时通知客户端。
- 重试消息管理:主要用于对业务系统临时处理不了的消息进行存放,然后再按照一定的策略投递给客户端处理。可以提供错误原因、错误处理次数等查询。
- 消息存储与分发:接收生产者投递的消息,把消息存放在本地磁盘上,消费者从该服务上拉取消息进行消费。
- 客户端
目前只提供了JAVA语言的SDK和支持HTTP协议的proxy,非JAVA语言通过proxy接入。
- 管理端
主要功能有:接入申请、消息元数据管理、重试消息信息查询、消息发送和消费日志查询、服务端状态信息管理查看、客户端连接信息管理查看等。
- 支撑模块
主要有报警模块、任务模块、归档模块、信息采集模块等。
JMQ特点
- 数据可靠性
针对公司的业务特点,消息服务主要应用于订单、支付、物流等环节。服务端采用MASTER-SLAVE结构,消息在正常情况下会同时存放两份,其中一份会强制持久化到磁盘,磁盘做RAID-5。默认情况下客户端采用同步发送,每条消息到达服务端MASTER后会强制刷入磁盘同时并行推送一份到SLAVE上,SLAVE写入文件系统后不等待强制刷盘就反馈给MASTER。根据不同的场景为了提高服务的可用性,普通级别的消息SLAVE断开后,该组服务可以正常使用,当SLAVE连接上后又会自动切换为保存两份。当然对数据可靠级别高的消息是强制要求数据必须写两份才算成功的。
- 服务高可用
每类消息一般都会分配3组及以上的服务组,每组服务包括一个MASTER和一个SLAVE,当然如果有需要也可以挂载多个SLAVE。客户端发送消息时,如果其中一组出现故障会重试发送给其他的组。虽然MASTER-SLAVE支持切换,提高服务的可用性,但是在实际生产中MASTER出现故障时,会优先采用通过其他服务组自动接替生产服务的方式,本组服务只提供从SLAVE读取的方式,而不是让SLAVE接替MASTER的写入,避免临界状态下丢失消息。对要求严格顺序的消息,不能通过简单的切换服务组实现,具体实现方式参考《高可用保证消息绝对顺序消费的BROKER设计方案》。
- 消费模型
由于公司以前有使用基于ACTIVEMQ二次开发的服务,服务端会存放客户端的消费位置,因此在自主研发JMQ时也延续了这种方式(可以兼容ACTIVEMQ的客户端)。但是ACTIVEMQ生产和消费都会操作索引文件,影响性能,JMQ吸取了这个经验教训。消费者在消费时按照索引分区顺序的消费,消费确认时只需要变更最后确认位置的值,不需要操作索引文件,而且多个消费者共用一个索引文件,各自保存自己的消费偏移位置就可以了。当然在实践过程中,由于一些特殊场景需要,会允许一定范围内不完全按照顺序消费,但是服务端会记录已经消费的索引区间。
JimDB
- 支持大容量缓存
采用"pre-sharding"技术,将缓存数据分摊到多个分片(每个分片上具有相同的构成,比如:都是一主一从两个节点)上,从而可以创建出大容量的缓存。将来会利用集群技术创建大容量的缓存。
- 支持2种存储类型
- Jimdb M(纯内存)
- Jimdb S(内存+SSD两级存储),可以存储10倍于内存容量的数据,并且提供更加可靠的持久性。
- 缓存数据的高可用性
和mysql相同,采用的也是异步复制,目前可以达到等同于mysql级别的数据可用性。Jimdb S版支持命令级的同步复制,超过mysql级别的数据可用性。可设置为自动的failover
- 支持多种I/O策略
支持读写分离、双写等I/O策略;针对读操作可分为"主优先"、"从优先"、"随机挑选"等方式;针对写操作可分为"同步写"和"异步写"。不同的I/O策略,对数据一致性的影响也不同,应用可以根据自身对数据一致性的需求,选择不同的I/O策略。
- 支持动态扩容
动态扩容有两种支持两种形式的动态扩容。第一种形式,通过在单个节点上预留内存,然后需要扩容时直接使用预留内存的方法达到扩容的目的;第二种形式,通过增加分片数并结合数据迁移来达到扩容的目的。第一种形式和第二种形式可以结合使用。第一种方式对应用的影响最小,但扩容效果有限;第二种方式扩容效果好,但由于会引发数据迁移,从而挤占网络带宽,会对应用有一定的影响。
- 支持特殊的命令及应用(spring-cache等)
目前已经支持了spring-cache与缓存的结合使用。将来会增加若干其他的特殊命令和应用。
浙公网安备 33010602011771号