标签归档:集群

一种分布式系统消息服务器设计

设计一个分布式系统,首先面临的就是如何解决服务间的通信问题,同步还是异步,是采用基于消息总线的事件驱动架构(EDA)还是分布式服务框架,这在很大程度上决定了系统的可扩展性。

消息服务器的可选择性很多,比如早些年的XMPP服务器,传统的JMS服务器,还有目前比较流行的AMQP消息服务器,简单的优缺点对比如下:

类型 优点 缺点
Openfire (XMPP) 1. 成熟,稳定。
2. 适合做聊天服务,
   在IM领域(Gtalk,网易POPO等)应用广泛。
1. 消息可靠性无保障。
2. 路由策略不够灵活。
3. 集群模式不完善。
4. 协议太重。
ActiveMQ (JMS) 1. 成熟,稳定。
2. 与Java应用契合度高。
1. 路由策略不够灵活。
2. 集群模式不稳定。
RabbitMQ (AMQP) 1. 成熟,稳定。
2. 路由策略灵活。
3. 消息可靠传输。
4. 集群方案成熟。
1. 配置项多,学习和运维成本高。

本文分享一种基于RabbitMQ的消息服务器设计方案:

MQ Server

如上图所示,黄色虚线表示的是分布式系统的一个子服务,假设分别为WEB、REPO和CTRL服务。P表示生产者,C表示消费者。生产者把消息发送到一个topic模式的Exchange (X),根据route key和binding key的运算结果,将消息路由到相应队列,再由队列投递给具体的消费者。

我们将请求从服务调用方的角度分成两类:同步和异步。同步(rpc.call)是指需要应答的请求,比如获取依赖服务的状态。异步(rpc.cast)是指无需应答的请求,比如下发一个命令。

1. 对于同步请求,服务调用方将消息publish到Exchange,根据route key路由到相应队列,投递给服务提供方的消费者处理。服务提供方消费者处理完请求后,将响应内容封装成消息格式,指定route key(TYPETYPE.${HOSTNAME}),将处理结果消息返回。

2. 对于异步请求,服务调用方将消息publish到Exchange,根据route key路由到相应队列,投递给服务提供方的消费者处理。服务提供方消费者处理完请求后无需返回。

无论同步还是异步,服务调用方在发出请求(消息)后会立即返回,不会阻塞。如果是同步请求,那么只需提供回调处理函数,等待响应事件驱动。

每个服务启动后会初始化一条AMQP连接(基于TCP),该连接由3个Channel复用:一个Channel负责生产消息,一个Channel负责从TYPE(REPO/CTRL/WEB等)类型的队列消费消息,一个Channel负责从TYPE.${HOSTNAME}类型的队列消费消息。从队列的角度来看,一个TYPE.${HOSTNAME}类型队列只有一个消费者,一个TYPE类型队列可能有多个消费者。

这样的设计满足以下四类需求:

1. 点对点(P2P)或请求有状态服务:消息的route key设置为TYPE.${HOSTNAME}。比如host1上的WEB服务需要向host2上的REPO服务发送同步请求,只需将消息的route key设置为REPO.host2即可。REPO服务处理完请求后,将响应消息的route key设置为WEB.host1,发送回消息服务器。再比如REPO服务是有状态服务,伸缩性不好做,需要WEB服务做Presharding或者一致性哈希来决定调用哪个REPO服务,也跟上面一样,WEB服务根据计算好的值填充REPO.${HOSTNAME},进行点对点模式的消息通信。

2. 请求无状态服务:如果服务提供方是无状态服务,服务调用方不关心由哪个服务进行响应,那么只需将消息的route key设置为TYPE。比如CTRL是无状态服务,host1上的WEB服务只需将消息的route key设置为CTRL即可。CTRL队列会以Round-robin的调度算法将消息投递给其中的一个消费者。视WEB服务是否无状态而定,CTRL可以选择将响应消息的route key设置为WEB(假设WEB无状态)或者WEB.host1(假设WEB有状态)。

3. 组播:如果服务调用方需要与某类服务的所有节点通信,可以将消息的route key设置为TYPE.*,Exchange会将消息投递到所有TYPE.${HOSTNAME}队列。比如WEB服务需通知所有CTRL服务更新配置,只需将消息的route key设置为CTRL.*

4. 广播:如果服务调用方需要与所有服务的所有节点通信,也就是说对当前系统内所有节点广播消息,可以将消息的route key设置为*.*

本方案优缺点如下:

优点:
1. 路由策略灵活。
2. 支持负载均衡。
3. 支持高可用部署。
4. 支持消息可靠传输(生产者confirm,消费者ack,消息持久化)。
5. 支持prefetch,流控。

缺点:
1. 存在消息重复投递的可能性。
2. 对于多服务协作的场景支持度有限。比如以下场景:WEB服务发送同步请求给CTRL服务,CTRL本身无法提供该服务,需要调用REPO服务,再将REPO服务的响应结果返回给WEB。这个时候就需要CTRL缓存WEB请求,直至REPO响应。
3. 缺少超时管理,错误处理等。

以上列举缺点需要由业务方考虑解决。

顺便再提供本方案的一个简单SDK:Gear

Gear SDK提供的功能包括:
1. 基础组件(Exchange, Binding, Queue, etc)初始化。
2. 连接复用,断线重连。
3. P2P方式,组播,广播消息发送。
4. 异步消息接收。

--EOF--

『大型网站技术架构:核心原理与案例分析』(四)

『大型网站技术架构:核心原理与案例分析』读书笔记系列:
(一):架构演化、模式、要素
(二):高性能架构
(三):高可用架构
(四):可伸缩架构
(五):可扩展架构
(六):安全性架构


『大型网站技术架构』(四):可伸缩架构

“大型“定义:

  • Facebook: 大量用户及大量访问,10亿用户。
  • 腾讯: 功能复杂,产品众多,1600+种产品。
  • Google:大量服务器,100w台服务器。

一、网站架构的伸缩性设计

  • 不同功能进行物理分离实现伸缩

    单一服务器处理所有服务 -> 数据库从应用服务器分离 -> 缓存从应用服务器分离 -> 静态资源从应用服务器分离

    横向分离(分层后分离)、纵向分离(业务分割后分离)

  • 单一功能通过集群规模实现伸缩

    当一头牛拉不动车的时候,不要去寻找一头更强壮的牛,而是用两头牛来拉车。

    集群伸缩性:应用服务器集群伸缩性、数据服务器集群伸缩性(缓存数据服务器集群和存储数据服务器集群)

二、应用服务器集群的伸缩性设计

负载均衡:实现网站伸缩性,改善网站可用性。

负载均衡类型

1. HTTP重定向负载均衡

通过一台HTTP重定向服务器,返回302实现负载均衡。实践中很少见。

优点:简单
缺点:性能差(2次请求)、伸缩性有限(重定向服务器容易成为瓶颈)、被搜素引擎判为SEO作弊(302请求),

2. DNS域名解析负载均衡

在DNS服务器中配置多个A纪录。大型网站用于进行第一级负载均衡。

优点:支持基于地理位置域名解析,加快用户访问速度。
缺点:DNS多级解析,生效和失效时间久。

3. 反向代理负载均衡

利用反向代理服务器(缓存资源、安全等)进行负载均衡。应用层负载均衡。

优点:反向代理功能和负载均衡功能集成,部署简单。
缺点:反向代理服务器容易成为瓶颈。

4. IP负载均衡

在网络层通过修改请求目标地址进行负载均衡。

  • 负载均衡服务器修改目的IP的同时修改源地址,将数据包源地址设为自身IP。(SNAT)
  • 负载均衡服务器同时作为Real Server的网关服务器。(LVS/NAT模式)

优点:相比应用层负载均衡(反向代理)有更好的性能。
缺点:进出流量走负载均衡服务器,依然存在瓶颈。

5. 数据链路层负载均衡

在通信协议的数据链路层修改mac地址进行负载均衡(LVS/DR)。数据三角传输模式,流量从用户->负载均衡服务器->Real Server->用户。

目前使用最广泛。

负载均衡算法

  • 轮询(Round Robin, RR)
  • 加权轮询(Weighted Round Robin, WRR)
  • 随机(Random)
  • 最少连接(Least Connections)
  • 源地址散列(Source Hashing)

三、分布式缓存集群的伸缩性设计

目标:必须让新上线/下线的缓存服务器对整个分布式缓存集群影响最小,也就是说经过调整使整个缓存服务器集群中已经缓存的数据尽可能还被访问到。

一致性Hash:解决集群扩减容时过多节点缓存失效问题。
使用虚拟节点的一致性Hash环:避免集群扩减容造成的节点负载不均问题,通过增加一层虚拟节点与物理节点的映射来使节点增删带来的影响平均到所有节点。

四、数据存储服务器集群的伸缩性设计

数据存储服务器必须保证数据可靠存储、可用性、正确性,伸缩性设计原则与缓存不同。

1. 关系数据库集群的伸缩性设计

  • 主从读写分离
  • 业务分隔、数据分库。跨库不能Join操作、避免事务
  • 数据分片:通过分布式关系数据库访问代理,比如Amoeba、Cobar(Ali)。伸缩性:一致性Hash + 数据迁移

2. NoSQL数据库的伸缩性设计

HBase伸缩性: 依赖可分裂的HRegion及可伸缩的分布式文件系统HDFS实现。

--EOF--

『大型网站技术架构:核心原理与案例分析』(一)

『大型网站技术架构:核心原理与案例分析』读书笔记系列:
(一):架构演化、模式、要素
(二):高性能架构
(三):高可用架构
(四):可伸缩架构
(五):可扩展架构
(六):安全性架构


『大型网站技术架构』(一):架构演化、模式、要素

一、大型网站架构演化

1. 架构演化

  • 应用程序、数据库、文件服务器部署在同一台机器
  • 应用程序、数据库、文件服务器独立部署
  • 使用缓存,加速数据读取
  • 应用程序集群化,负载均衡
  • 数据库读写分离
  • CDN加速
  • 分布式文件系统、分布式数据库
  • NoSQL + 搜索引擎
  • 业务拆分,数据库共享
  • 服务化,业务分库

2. 演化价值观

  • 逐步发展、灵活应对
  • 业务驱动技术发展:业务成就技术、事业成就人

3. 误区

  • 追随大公司解决方案:"taobao/facebook就是这么搞的"
  • 为了技术而技术:一味追求新技术,误入崎岖道路
  • 企图用技术解决所有问题:考虑通过业务手段解决业务问题(12306排队机制、调整放票时段,不搞秒杀)

二、大型网站架构模式

面临高并发访问、海量数据处理、高可靠运行等挑战,总结出许多解决方案和可重复的模式,以实现高性能、高可用、易伸缩、可扩展、安全等各种技术架构目标。

1. 分层

定义:将系统在横向维度上切分成几个部分,每个部分负责一部分相对比较单一的职责,然后通过上层对下层的依赖和调用组成一个完整系统。

举例: OSI 7层协议,计算机架构(硬件、OS、应用软件),网站系统(应用层、服务层、数据层), MVC。

注意事项:

  • 合理规划层次边界和接口
  • 避免跨层调用
  • 避免逆向调用

2. 分割

定义: 在纵向对软件进行切分。

举例:网站业务拆分(购物、论坛、搜索、广告等),购物业务拆分(酒店业务、3C业务、小商品业务等)

3. 分布式

目标: 分层和分割后的模块拥有更小的粒度,目的是分布式部署,便于使用更多资源(CPU、内存、存储等),提供更多服务。

注意事项:

  • 服务间调用通过网络,造成性能影响。
  • 服务器越多,宕机概率越大,服务可用性降低。
  • 数据一致性问题。
  • 依赖复杂。
  • 开发管理苦难。
  • 切莫为了分布式而分布式。

分布式方案:

  • 分布式应用和服务:分层和分割后的应用和服务模块分布式部署。
  • 分布式静态资源:静态资源(JS、CSS、图片等)分布式部署,动静分离。
  • 分布式数据和存储:海量数据存储,分布式NoSQL。
  • 分布式计算:实时计算,搜索引擎、MapReduce分布式计算框架。
  • 分布式配置:服务器配置实时更新。
  • 分布式锁:分布式环境下的并发和协同。
  • 分布式文件系统:云存储。

4. 集群

定义: 多台服务器部署相同应用或模块,通过负载均衡对外提供服务。提高并发性和可用性。

5. 缓存

定义:将数据存放在距离计算最近的位置以加快处理速度。

举例:

  • CDN: 缓存静态资源。
  • 反向代理:缓存静态资源。
  • 本地缓存: 数据缓存在本机内存,减少访问数据库。
  • 分布式缓存: 可以缓存更多地数据。

缓存使用前提:

  • 数据访问热点不均衡。
  • 数据有一段有效期,不会很快过期,否则很容易脏读。

6. 异步

目标:降低系统间耦合性。

类型:

  • 单一服务:多线程共享内存队列。
  • 分布式系统:分布式消息队列。

异步消息队列特性:

  • 提高系统可用性:消费者故障不影响生产者继续处理业务请求。
  • 加快网站响应速度: 生产者处理业务请求后将数据写入队列,无需等待消费者处理完毕,减少响应延迟。
  • 消除并发访问高峰:突发事件、促销活动、热点事件。

7. 冗余

目标:数据冗余备份,当某台服务器宕机,可以将其上的服务和数据访问转移到其他机器,提高服务可用性。

类型:

  • 冷备份: 定期备份,存档保存。
  • 热备份: 主从分离,主从同步。
  • 灾备数据中心:实时同步网站程序和数据到多个数据中心,抵抗地震、海啸等不可抗力因素。

8. 自动化

目标:一切自动化。

类型:

  • 自动化代码管理:针对开发,代码版本控制、代码分支创建合并自动化。
  • 自动化测试:自动部署到测试环境,运行测试用例,发送测试报告,反馈测试结果。
  • 自动化安全检查:代码静态安全扫描,安全攻击测试。
  • 自动化部署:自动化部署到生产环境。
  • 自动化监控
  • 自动化报警
  • 自动化失效转移:将失效服务器从集群中隔离。
  • 自动化失效恢复:故障消除后重启服务,同步数据。
  • 自动化降级:遇到访问高峰时拒绝部分请求及关闭部分不重要服务,将系统负载降至安全水平。
  • 自动化分配资源:将空闲资源分配给重要服务,扩大其部署规模。

9. 安全

安全手段:

  • 用密码和手机校验码进行身份认证。
  • 加密通信。
  • 验证码机制防止机器人。
  • 进行编码转换等防止XSS、SQL注入。
  • 垃圾信息、敏感信息过滤。
  • 交易信息进行风险控制。

三、大型网站架构要素

架构:最高层次的规划,难以改变的决定。

1. 性能

手段

  • 浏览器端: 浏览器缓存、页面压缩、页面布局、减少Cookie传输、CDN等;
  • 应用服务器端:本地缓存、分布式缓存、消息队列、集群,多线程、改善内存管理;
  • 数据库服务器端:索引、缓存、SQL优化,NoSQL(优化数据模型、存储结构、伸缩性);

指标

  • 响应时间
  • TPS
  • 系统性能计数器

2. 可用性

手段:冗余

  • 应用服务器: 负载均衡 + 流量切换;
  • 存储服务器: 实时备份 + 访问转移 + 数据恢复

3. 伸缩性

定义: 通过不断向集群中加入服务器的手段来缓解不断上升的用户并发访问压力和不断增长的数据存储需求。

  • 应用服务器: 无状态;
  • 缓存服务器: 改进缓存路由算法。注意严重依赖缓存场景下,缓存失效导致整个网站崩溃;
  • 关系数据库: 需在数据库外实现伸缩性,通过路由分区等手段将多个数据库组成集群;
  • NoSQL:先天为海量数据而生,线性伸缩。

4. 扩展性

标准:

  • 网站快速发展,功能不断扩展,系统架构能够使其快速响应需求变化;
  • 新增业务,对现有产品透明无影响;
  • 产品间很少耦合。

手段:

  • 事件驱动架构:通过消息队列实现,用户请求和其他业务事件是生产者,消息处理者是消费者,可以透明增加新的消息生产者任务或者消费者任务;
  • 分布式服务:将业务和可复用服务分离开来,通过分布式服务框架调用。新增产品可以通过调用可复用的服务实现自身业务逻辑。可复用服务升级变更时,通过多版本方式对应用实现透明升级。

5. 安全性

标准: 针对现存和潜在的攻击和窃密手段,是否有可靠的应对策略。

--EOF--

如何用Spring实现集群环境下的定时任务

定时任务的实现方式有多种,例如JDK自带的Timer+TimerTask方式,Spring 3.0以后的调度任务(Scheduled Task),Quartz等。

Timer+TimerTask是最基本的解决方案,但是比较远古了,这里不再讨论。Spring自带的Scheduled Task是一个轻量级的定时任务调度器,支持固定时间(支持cron表达式)和固定时间间隔调度任务,支持线程池管理。以上两种方式有一个共同的缺点,那就是应用服务器集群下会出现任务多次被调度执行的情况,因为集群的节点之间是不会共享任务信息的,每个节点上的任务都会按时执行。Quartz是一个功能完善的任务调度框架,特别牛叉的是它支持集群环境下的任务调度,当然代价也很大,需要将任务调度状态序列化到数据库。Quartz框架需要10多张表协同,配置繁多,令人望而却步...

经过折中考虑,还是选择了Spring的Scheduled Task来实现定时任务。如下:

1. Spring配置文件application-context.xml中添加task命名空间和描述。

1
2
3
4
5
6
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:task="http://www.springframework.org/schema/task"
	xsi:schemaLocation="http://www.springframework.org/schema/beans
	http://www.springframework.org/schema/beans/spring-beans.xsd
	http://www.springframework.org/schema/task
	http://www.springframework.org/schema/task/spring-task.xsd">

2. 添加调度器和线程池声明。

1
2
<task:executor id="taskExecutor" pool-size="10" />
<task:annotation-driven executor="taskExecutor" />

3. 实现调度方法。基本结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
package com.netease.yx.service;
 
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
 
@Service
public class ScheduledService {
    @Scheduled(cron = "0 0 5 * * *")
    public void build() {
       System.out.println("Scheduled Task");
    }
}

@Scheduled注解支持秒级的cron表达式,上述声明表示每天5点执行build任务。

前文已经提过,这种方式在单台应用服务器上运行没有问题,但是在集群环境下,会造成build任务在5点的时候运行多次,遗憾的是,Scheduled Task在框架层面没有相应的解决方案,只能靠程序员在应用级别进行控制。

如何控制?

1. 无非是一个任务互斥访问的问题,声明一把全局的“锁”作为互斥量,哪个应用服务器拿到这把“锁”,就有执行任务的权利,未拿到“锁”的应用服务器不进行任何任务相关的操作。
2.这把“锁”最好还能在下次任务执行时间点前失效。

在项目中我将这个互斥量放在了redis缓存里,1小时过期,这个过期时间是由任务调度的间隔时间决定的,只要小于两次任务执行时间差,大于集群间应用服务器的时间差即可。

完整定时任务类如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.netease.yx.service;
 
import javax.annotation.Resource;
import org.apache.commons.lang3.time.DateUtils;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import com.netease.yx.service.ICacheService;
 
@Service
public class ScheduledService {
    @Resource
    private ICacheService cache = null;
 
    private static String CACHE_LOCK = "cache_lock";
 
    private static int EXPIRE_PERIOD = (int)DateUtils.MILLIS_PER_HOUR / 1000;
 
    @Scheduled(cron = "0 0 5 * * *")
    public void build() {
        if (cache.get(CACHE_LOCK) == null) {
            cache.set(CACHE_LOCK, true, EXPIRE_PERIOD);
            doJob();
        }
    }
}

--EOF--

一种"无状态"的竞猜游戏设计

场景如下:某个电视节目,共6个选手,每期节目有一个或多个选手为获胜者。现要给这个节目开发一个与节目同步的竞猜游戏,观众参与竞猜谁是节目的获胜者,答对的有红包奖励。节目每周一期,周六21点播出,22:30结束。周四10点至周六22点为观众竞猜时间,周六22点至22:30为等待开奖时间,周六22:30至下个周四10点为领奖时间。然后下个节目周期开始,依此循环。

上述节目有三个时间点,开始竞猜时间(结束领奖时间,周四10点),停止竞猜时间(周六22点),开奖时间(周六22:30),这三个时间点前后会有较大的访问量,其余时间则较为平缓。一种设计方法是给每个用户保存一个状态,定时器在上述三个时间点修改用户的状态,这就是一种有状态的实现方式,优点是直接,用户访问页面后查一次数据库即可知道状态,显示对应的视图即可。缺点是三个时间点附近有大量的数据库读写,特别是开奖时间前后会涌入大量用户,应用服务器一边要等待数据库IO,处理缓存更新,一边要处理用户请求,会给响应时间带来一定影响。而且定时更新状态的任务在应用服务器集群下还要附加额外逻辑,避免不同服务器更新多次用户状态。

另外有一种无状态的设计方式,这里的无状态是指不用将用户的当前状态存储到数据库中。在用户访问应用之前,他的状态是未知的,当用户访问页面后,服务器根据一些参照值实时运算出该用户的当前状态。这种实现方式避免了第一种实现方式的两个缺点,首先三个时间点上不再有涉及全表的数据库操作,它将集中的状态更新操作平摊到每次用户访问页面时,其次应用支持简单的水平扩展。由于需要实时运算用户状态,这种实现方式会额外消耗CPU时间。从业务场景的特点来看,这种交换是值得的。

主要的实现方式比较简单:

1. 定义状态类型,定义以下8种状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 领奖期间的状态。周六22:30至下个周四10:00。
public static final int STATUS_PRIZE_NO_PARTI = 0; // 未参与
public static final int STATUS_PRIZE_PARTI_WIN_DRAW = 1; // 参与,压中,已领奖
public static final int STATUS_PRIZE_PARTI_WIN_NO_DRAW = 2; // 参与,压中,未领奖
public static final int STATUS_PRIZE_PARTI_NO_WIN = 3; // 参与,未压中
 
// 竞猜阶段的状态。周四10:00至周六22:00。
public static final int STATUS_GUESSING_CHOOSE = 4; // 已选择
public static final int STATUS_GUESSING_NO_CHOOSE = 5; // 未选择
 
// 节目播放阶段的状态。周六22:00至周六22:30。
public static final int STATUS_BROADCAST_CHOOSE = 6; // 已选择
public static final int STATUS_BROADCAST_NO_CHOOSE = 7; // 未选择

2. 用户访问页面后,以当期节目的播出时间为参照点,根据相对时间进行运算,确定3个时间点。确定当前时间所在的区间,再根据其他信息确定当前用户的状态,返回即可。

--EOF--