Feed架构介绍
最后更新于:2022-04-01 03:15:32
> 原文出处:http://weibo.com/p/1001643877261186063112
> 作者:胡忠想[@古月中心相心](http://weibo.com/n/%E5%8F%A4%E6%9C%88%E4%B8%AD%E5%BF%83%E7%9B%B8%E5%BF%83)
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f66aee426e8.jpg)
微博作为当今中国最大的社交媒体平台,每天都有上亿人访问。而Feed作为微博最为核心的功能,每天有高达数十亿的请求,高峰期每秒的请求量上万。如何设计高可用的Feed系统,来应对如此高并发的访问,极具挑战。
下面,我们将具体探讨微博Feed系统的架构,希望通过这次讲解,大家能够掌握微博Feed系统架构的基本知识。
# 大纲
[TOC=2,2]
## 一、初探Feed-What is feed?
从广义上讲,Feed是指为满足用户以某种形式持续获得更新信息的需求而提供的格式标准的信息出口。
### **1、****PC Feed**
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f66aee61bb9.jpg)
### **2、mobile feed**
微博体系中,Feed是指用户通过关注关系,聚合好友最新微博以供自己消费的信息服务,其中也包括分享微博。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f66aeea07c3.jpg)
### **分享**
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f66aeed427f.jpg)
### **倾听**
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f66aeef39da.jpg)
### **看新闻**
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f66aef28f88.jpg)
### **围观**
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f66aef5327a.jpg)
## 二、Feed业务体系
### **1、微博**
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f66af488e56.jpg)
### **2、page**
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f66af589121.jpg)
### **3、Card**
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f66afb307ca.jpg)
### **4、计数器**
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f66afbc03af.jpg)
### **5、未读数、话题、分组、转评赞、广告和趋势**
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f66afcbd8b5.jpg)
## 三、Feed系统架构设计
### **1\. 如何设计一个高可用的Feed系统?**
* 如何存储用户发表的微博
* 如何聚合关注用户发表的微博
### 2\. Feed存储
**Feed特征**
* 每天发博量大:上亿条
* 发博峰值高:几万/s
* 并发访问量大:几万/s
* 访问热点集中:最近三天占99%
**存储选型**
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f66afd3917f.jpg)
**存储特点**
* 持久化:MySQL、Redis、HBase
* 并发读QPS:几百万/s:Memcached、Redis
* 最近三天热点数据:Memcached
**实现方案 Memcached+MySQL**
Memcached存储最近三天热点数据、MySQL存储全量数据
### 3、Feed存储-MySQL
**分库分表**
* 按访问用户维度hash来分库
* 按访问时间维度来分表
**部署形式**
* 一主多从
* 读写分离
**HA**
* 主从机制、自动切换
* backup离线灾备
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f66b025f692.jpg)
### 4、Feed存储-Memcached
**高可用性架构**
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f66b028e58b.jpg)
**业务架构**
* mvd:存储用户最近15天50条。
* mvl:存储用户最近200条。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f66b02b3842.jpg)
### 5、Feed聚合
如何聚合关注用户的微博?
**拉模型**
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f66b02d228b.jpg)
**优缺点**
* 优点:架构简单、一致性好、写入过程开销小
* 缺点:写少读多,网络开销大,资源读取量大
**推模型**
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f66b0307877.jpg)
**优缺点**
* 优点:下行网络开销小,资源成本低
* 缺点:写入成本较高,资源写瓶颈高
## 四、未来
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f66b032bf32.jpg)
**业务发展**
* 分发控制
* 智能Feed
* 实时反馈
**架构发展**
* 存储架构
* 消息架构
* 可插拔
* cell模型
## 作业
设计一个简化的Feed系统。
– 10亿用户,每天发表1亿条微博。
– 平均每秒500次feed请求,平均每个人关注10个用户。
– 平均每秒20000次feed请求,平均每个人关注200个用户。
– 针对上面两种情况,分别设计feed系统,要求给出存储选型和对应的机器数量。
**------------------新兵训练营简介**------------------
微博平台新兵训练营活动是微博平台内部组织的针对新入职同学的团队融入培训课程,目标是团队融入,包括人的融入,氛围融入,技术融入。当前已经进行4期活动,很多学员迅速成长为平台技术骨干。
微博平台是非常注重团队成员融入与成长的团队,在这里有人帮你融入,有人和你一起成长,也欢迎小伙伴们加入微博平台,欢迎私信咨询。
**------------------讲师简介**------------------
胡忠想,微博名[@古月中心相心](http://weibo.com/n/%E5%8F%A4%E6%9C%88%E4%B8%AD%E5%BF%83%E7%9B%B8%E5%BF%83),目前任职于新浪微博平台研发及大数据部,主要负责微博 Feed 策略相关工作,曾先后参与微博 Feed 存储、微博计数器、微博阅读数等重大业务产品的开发。2012年3月份毕业于北京航空航天大学计算系,同年4月份,加入新浪微博并工作至今。业余爱好户 外,曾徒步过贡嘎、雨崩,攀登过四姑娘三峰。新兵训练营第一期学员。
平台RPC框架介绍
最后更新于:2022-04-01 03:15:30
原文出处:http://weibo.com/p/1001643875439147097368
作者:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f66a0c949fa.jpg)
**课程大纲**
[TOC]
## 一、RPC简介
RPC协议具有语言无关、通信协议无关在大中型分布式系统中,远程服务调用是十分关键的环节。RPC由于具有语言无关、简单高效等特点,在远程服务调用中有着广泛应用。本文将简单介绍一下RPC的基本概念,以及微博平台RPC服务框架的基本原理和使用方式。
### 1、什么是RPC?
RPC(Remote Procedure Call)指远程过程调用,是一种通过网络调用远程过程(或方法)的协议。RPC是基于Client/Server模式,Client端携带必要参数调用Server端的方法,并获取Server端返回的方法执行结果。
、高效可靠等特点,非常适合作为分布式计算、分布式服务等远程交互的基础协议。RPC协议一般包括协议消息处理、网络传输两个部分,一个简单的rpc调用过程如下图:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f66a0d085ed.jpg)
当Client调用远程的方法时,先将要调用的方法名、参数等信息按RPC协议转换为RPC消息,然后再通过某种传输协议(TCP、HTTP等)将RPC消息传输到Server端。Server端接到请求后将RPC消息按协议转换为调用请求,并执行方法,将执行后的结果按类似的过程返回给Client端,完成一次RPC调用。
### 2、RPC与HTTP、RMI等远程通信方式比较
1)、RMI与RPC:
RMI(Remote Method Invocation)是指java语言中的远程方法调用,RMI中的每个方法都具有方法签名,RMI客户端和服务器端通过方法签名进行远程方法调用。RMI只能在java语言中使用,可以把RMI看作面向对象的java RPC。
2)、HTTP与RPC
HTTP(HyperText Transfer Protocol)是应用层通信协议,使用标准语义访问指定资源(图片、接口等),网络中的中转服务器能识别协议内容。HTTP协议是一种资源访问协议,通过HTTP协议也可以完成远程请求并返回请求结果。HTTP的优点是简单、易用,可理解性好、并且与语言无关;HTTP协议的缺点是协议头比较重,一般请求到具体服务器的链路较长(可能会有dns解析、nginx代理等)。
相对于HTTP来说,通信层只是RPC协议的一部分,RPC可以使用TCP或者HTTP等作为通信协议。除了通信协议,RPC还包括请求序列化协议,一般RPC框架还会包括一些调度管理能力。RPC相对于HTTP的优点是能够提供调度、管理能力,使用RPC服务的自动化程度比较高,性能也十分优秀。缺点就是相对复杂,学习成本稍高。
下面就以平台的RPC框架Motan为例,介绍一下RPC服务的基本实现及使用方式。
## 二、Motan RPC服务框架
### 1、RPC服务框架
一般RPC协议中仅仅包含了client与server之间点对点的调用,在实际的使用当中,还需要考虑服务的发现与服务注销、提供服务的多台server的负载均衡、服务的高可用等等一系列的问题。
目前业界比较成熟的RPC框架有阿里开源的Dubbo、Facebook开源的Thrift,以及Avro、Hetty、Grpc等等,其中Thrift、Avro、Hetty、Grpc等都是性能优秀的RPC框架,偏重于跨语言序列化传输、在序列化方式、协议扩展、同步异步调用等方面有各自不同的优点,但在RPC服务管理方面功能较少,在分布式服务应用时还需要进行一些辅助开发。Dubbo则是一个比较完备的服务治理框架,能够提供高性能、透明化的RPC调用,并且拥有强大的服务管理功能。Dubbo虽然功能强大,但是整个框架有些过重,应用起来略微复杂。
考虑到微服务化、动态服务治理等需求,并结合自身业务高并发、服务性能、稳定性要求较高等特点,微博平台开发了自己的Motan RPC框架。Motan偏重于简洁实用的服务治理功能和优秀的RPC协议扩展能力,既可以提供高效的RPC远程调用,又能提供服务发现、服务高可用(High Available)、负载均衡、服务监控、管理等服务治理功能。通过SPI机制提供强大的扩展能力,可以支持不同的RPC协议、传输协议。Motan能够无缝支持Spring配置方式使用RPC服务,通过简单、灵活的配置就可以提供或使用RPC服务。通过使用Motan框架,可以十分方便的进行服务拆分、分布式服务部署。
### 2、MotanRPC框架中的三个角色
在Motan框架中,RPC服务调用是由三个角色的交互完成的。首先需要一个服务的提供方Service(也可以叫做Provider),Service对外提供各种可以远程调用的方法(Method);另外一个角色叫Client(也可以叫做Referer或Consumer),Client需要使用Service提供的远程服务。如果仅有这两个角色,还不能实现自动化的RPC服务,还需要第三个角色--注册中心Registry,Registry是用来提供服务发现功能的。
当Service准备对外提供一个RPC服务时,首先需要跟Registry进行服务注册。当Client需要使用RPC服务时,则需要跟Registry订阅RPC服务,Registry会把对应服务信息给Client,并且在服务信息变更时,及时通知Client。Client根据Registry提供的服务信息,请求具体的一台能够提供指定RPC服务的服务器,完成远程调用。Service、Client、Registry这三个角色的交互如下图:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f66a0da8d8d.jpg)
例如,Server1这台服务器跟Registry注册了“S”这个RPC服务,Registry就保存了“Server1提供S服务”这样一条信息。Server2也可以提供“S”服务,也向Register注册了“S”服务。这时Register中的信息变成了“Server1和Server2都提供S服务”。当Client1需要使用S服务了,它向Register订阅了S服务,Register告诉Client1:“Server1和Server2都提供S服务”,Client1根据某个策略选择了调用Server1的S服务完成了RPC调用。后来,Server1不在提供S服务了,Register把这个变化告诉给了Client1,Client1就不再使用Server1的S服务,而是使用Server2的S服务完成RPC调用。
java语言在使用Motan时,Client与Service通过java接口类来进行RPC调用。例如Service端实现了com.weibo.demo.TestService接口, Client可以通过Motan来使用这个接口类中声明的方法。
### 3、Motan RPC调用流程
Motan框架中Registry、Service、Client三个角色包含的模块以及各模块的交互流程如下:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f66a12da23c.jpg)
其中register模块用来和注册中心进行交互,包括注册服务、订阅服务、服务变更通知、服务心跳发送等功能;transport模块用来进行远程通信;serialize模块将rpc请求中的参数、结果等对象进行序列化与反序列化,即进行对象与字节流的互相转换;protocol模块用来进行RPC服务的描述和RPC服务的配置管理;cluster模块包含一组可以提供RPC服务的具体Server,实际请求时会根据不同的HA与负载均衡策略在一组Server中选择一个可用的发起远程调用。各模块的详细情况将在下一节进行介绍。
## 三、Motan中各角色介绍
### 1、Registry
Motan可以支持多种Registry模块,例如zookeeper、consul等都可以作为注册中心使用,默认的Registry模块使用平台开发的Vintage配置服务。Vintage是一个基于redis的轻量级KV存储系统,能够提供命名空间服务、服务注册、服务订阅等功能。Vintage中的服务是以分组(Group)保存的,一般一个分组以机房+业务线进行命名,如yf-user-rpc。一个分组中包含若干的Service,一个Service即表示java中的一个接口类,能够提供其中的多个方法。
每个Service下有一组能够提供对应服务的Server,每个Server在向Registry注册后,会定时发送心跳,向Registry汇报健康状态。Server通过心跳开关的开启与关闭,可以控制是否对外提供RPC服务。
Client端订阅Service后,会从Registry中得到能够提供对应Service的一组Server,Client把这一组Server看作一个提供服务的cluster。当cluster中的Server发生变更时,Client端的register模块会通知Client进行更新。
### 2、Motan Service
Service端在启动RPC服务时的主要步骤如下:
1)、首先通过配置文件生成对应的protocol信息,包括对外提供服务的端口、服务名称、工作线程数等等
2)、向Vintage(即Registry)注册RPC服务,包括提供服务的protocol信息、Server的ip、提供服务的端口等。
3)、打开并监听声明的RPC服务端口。此时服务处于已经注册,但未正式提供服务的状态。
4)、服务验证通过后,打开心跳开关,Server会定时向Vintage注册中心汇报健康状态,此时就能正式对外提供RPC服务了。
在Service端提供服务后,由transport模块负责监听rpc服务的端口,并接收client端发送过来的RPC请求。一次完整的Service端处理RPC请求流程如下:
1)、监听RPC服务端口,由transport模块接收RPC request,transport模块默认是使用Netty nio通过TCP长链接方式与Client进行通信。transport模块收到请求后将请求转交给serialize模块进行解析。
2)、serialize模块默认使用Hession对RPC请求进行解码,将请求由字节流转化为java对象。
3)、fliter模块会根据请求的参数做一下过滤策略,例如调用统计、日志、请求限制等。
4)、protocol负责解析通过filter后的请求对象,根据协议分析出请求想要调用的方法及参数,然后找到提供对应服务的具体对象。
5)、具体服务对象通过反射机制完成服务中方法的调用并返回处理结果。
6)、返回结果通过filter后,在通过serialize模块将结果对象转换为字节流,由transport模块发送给Client端,完成一次RPC请求的处理。
### 3、Motan Client
Motan Client端的初始化流程如下:
1)、与Service端一样,首先会通过配置文件生成对应的protocol信息。
2)、向Registry订阅所需的RPC服务,获取提供对应服务的全部Server,这一组Server和HA策略模块、负载均衡模块一起组成了一个cluster。
3)、对每个Server建立初始链接。
在Client完成了初始化流程后,在调用RPC服务时,会有一个选择Server的过程,当Server选定后的处理过程跟Server端类似。Client调用RPC的流程如下图:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f66a131980c.jpg)
1)、当进行RPC请求时,会首先通过HA模块选择一种高可用策略,例如快速失败、失败选择其他Server重试等策略,各种策略的参数可以根据需要进行设置。
2)、然后会通过负载均衡模块从一组提供服务的Server中选择一个可用的Server。负载均衡的策略包括一致性哈希、轮询、并发度、权重等等。
3)、选出Server后,就对这个Server进行RPC请求,图中的netty client是用来进行点对点的RPC请求,包含了protocol、serialize、transport模块,请求的处理流程与Service端的流程基本一致,也是通过serialize模块转换请求对象,transport模块来进行通信。
## 四、Motan实战
### 1、Motan RPC基本使用方式
Motan RPC是按接口提供和调用服务,以java服务为例,一般由服务提供方声明服务的接口类,并同时提供一个默认的RPC Client调用配置,然后将接口类与默认的Client配置xml文件一起封装成jar包。服务提供方和服务使用方共同引入这个jar包,服务提供方对接口类进行实现并对外提供RPC服务;服务使用方加载默认Client端配置文件,直接使用RPC服务。
除了xml配置方式外,Motan也支持API方式,因为xml配置方式比较灵活,并且与spring配置加载完全兼容,一般推荐使用xml方式配置Motan服务。下面就以xml配置方式分别介绍使用Motan提供、调用RPC服务的具体步骤。
### 2、使用Motan提供RPC服务
使用Motan提供RPC服务大致需要如下几步:
1)、声明RPC接口类,并实现该接口类。
2)、引入Motan相关jar包。
3)、配置服务端xml配置。
4)、部署、启动服务,验证服务。
5)、打开心跳开关,正式提供RPC服务
一份基础的Service端xml配置如下:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f66a1343ad6.jpg)
Motan使用了自定义的xml schema,在xml中需要引入对应的schema。Motan的xml配置中包括registry、protocol、service、referer几个部分,其中registry与protocol是Service端与Client端都要使用的。
registry配置了注册中心相关的属性,包括注册中心ip、prot、超时等;protocol配置了RPC服务相关的属性,如工作线程数、请求超时、消息长度等;basicService配置了不同服务的公共属性,当一个工程对外提供多个RPC Service的时候,可以把这些Service的公共属性配置在basicService中;service则配置了具体的服务属性,包括对外提供服务的端口、对外提供服务的具体服务实现类bean等。
除了这些基础属性外,每个配置标签下还支持大量的高级属性,如访问控制、filter策略等等,这里就不在一一讲解了。
### 3、使用Motan调用RPC服务
使用Motan RPC服务就相对简单一些,一般服务提供方已经给出了默认的client配置,只需要在项目中加载xml配置,就可以像使用本地的spring bean一样直接使用RPC服务了。步骤如下:
1)、引入Motan相关jar包及RPC服务接口jar包。
2)、加载RPC接口jar包中的默认xml配置
3)、直接使用xml中声明的refer bean。
Client端基本配置如下:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f66a13b73b1.jpg)
其中registry、protocol部分与Service端的配置一致。basicRefer配置是多个RPC服务时的公共配置,例如请求超时控制、服务端异常是否抛出等;referer表示对服务实现类的远程引用,配置了使用的服务接口类等属性。
如果想使用自定义的Client端配置,也可以按schema中的属性单独配置xml,加载后就可以生效。另外在Client端还可以进行一些更细化的配置包括对一个接口类中不同方法的详细配置,可以根据具体的业务场景进行配置。
## 五、总结
RPC服务在分布式系统中有着越来越广泛的应用,本文简单介绍了RPC的基本概念、平台RPC框架Motan的基本结构、Motan中的三个角色的交互流程以及基本的使用方式。通过简单配置就能够使用RPC服务了。当然,如果需要对RPC服务进行更精细的控制,可以通过schema中的特定属性进行配置或者使用不同扩展模块来实现。
## 六、课后思考
1、远程服务的不同调用方式、RPC与HTTP的区别以及适用的场景。
2、Motan RPC框架中的三个角色及各角色之间的交互过程。
3、RPC请求的调用过程。
------------------新兵训练营简介------------------
微博平台新兵训练营活动是微博平台内部组织的针对新入职同学的团队融入培训课程,目标是团队融入,包括人的融入,氛围融入,技术融入。当前已经进行4期活动,很多学员迅速成长为平台技术骨干。
微博平台是非常注重团队成员融入与成长的团队,在这里有人帮你融入,有人和你一起成长,也欢迎小伙伴们加入微博平台,欢迎私信咨询。
------------------讲师简介------------------
张雷,[@rayzhang0603](http://weibo.com/n/rayzhang0603) 微博平台及大数据部——平台研发高级系统研发工程师,2012年7月毕业于太原科技大学,13年加入微博平台,先后负责开放平台、粉服开发者、MotanRPC等后端服务架构工作,有着丰富的系统的架构设计与优化经验。新兵训练营第二期学员。
一次服务上线
最后更新于:2022-04-01 03:15:28
> 原文出处:http://weibo.com/p/1001643879307398512716
> 赖佳俊[@LierD](http://weibo.com/n/LierD)
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f669011d98d.jpg)
## 引言:
前面几节课程中,我们已经对常见的互联网技术,代码编写规范有了一定的学习和了解。这些技术以及规范在我们日常的开发中都是经常被使用到的。
这些技术的学习最终都是以应用为目标的,即需要把这些技术通过coding、软件系统的组合(资源、服务)等实现方式最终应用到线上环境当中。
对于小规模的开发团队,因服务规模比较小,服务的部署可能是很简单的——手动部署代码成本也可控。但对于数十人上百人的开发团队,大规模的分布式系统来说,原来简单的方式就难以运转了。我们急需一套符合软件工程原理的流程体系,来保证我们研发效率、上线服务质量。
今天我将结合微博平台目前的上线流程以及个人的一些体会,来介绍一下研发上线流程体系。
培训大纲:
[TOC]
## 工程师的日常工作内容
对于一个刚进入公司的新同学,肯定有好多问题:
* 作为一个工程师,日常工作内容都包括哪些呢?我是否只要写代码就可以了?
* 日常工作中,都需要与哪些同事打交道呢?
* 日常工作中,是否有一些流程需要了解?如何推进项目、事情的进展?
作为一个系统研发工程师,我对日常工作的一个总结如下:
主线任务:完成需求开发,推进上线
业务需求:完成各个业务部门产品需求
改造需求:来源于部门内部技术方案、整体架构升级、改造需求
支线任务:提供其他的技术支持
线上系统问题跟踪
产品数据分析支持
我们可以看到,一个工程师日常最主要的任务即是完成各类需求研发工作。这部分的工作实际上包括两方面的内容:
(1)功能研发,也即通常所说的写代码
(2)推动上线部署
## 上线:
上线指的是将工程师开发的功能推动到线上生产环境中,为系统面向的用户所使用。
在一些中小企业中,因面向的用户总量不大,所需的服务器也无需太大,上线的过程可能会比较简单——开发工程师完成代码的编写后,编译,测试,手动部署可发布包到服务器上重启即可。
但在大型的互联网环境中,上述简单的做法面临着很多问题:
(1)分布式系统环境复杂,上线所涉及到的读写顺序、操作流程更复杂
(2)同一个系统可能由多个工程师维护,代码的变更涉及到多个功能、多个团队
(3)系统代码总量大,生命周期长,每一次的变更都有可能带来潜在bug以及严重的故障
微博平台历史上也曾发生过多次故障。通过对故障的分析和总结,我们发现约80%的故障都来源于系统变更。即每次上线变更其实都是隐含着很大的风险性的。
但是,作为互联网工程师,我们并不可能因为上线变更所可能带来的风险,就不进行新功能的研发,停止不前。相反,我们根据软件工程的开发流程原理以及业界的一些成熟的经验,结合我们的业务特点,以及自身历史上发生过的几次故障的经验,总结设计出了我们自己的一套的开发、上线流程。总的来说,我们期望能够:
(1)按时、高质量交付需求、功能实现到生产环境
(2)研发过程明确清晰,每一步都是经过充分review的
(3)上线过程步骤明确,较少运维误操作
(4)出事可操作,always have plan B,即使出问题了,我们也可以快速恢复
(5)对研发效率的影响尽可能小
## 研发上线流程说明:
我们的研发上线流程由多个步骤组成,需要一步一步完成。对于我们的研发工程师来说,他需要关注每一步的:
(1)该步的检查内容
(2)需要提供的明确的产出物
(3)该步相应的审查人员
如图1所示为研发上线流程的一个示意图:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f6690652dd8.jpg)
图1 研发上线流程示意图
从图1中可以看到,整个研发流程由多个步骤组成,每个步骤都涉及到一个或多个人员。
因本节课更多的关注在上线部分的推动上,所以我们将从开发完成后开始详细介绍:
## 代码审查(Code Review):
即代码评审,意指待交付的代码经过除开发工程师以外的人员进行评审检查。
Code review的意义在于:
(1)帮助梳理逻辑,从而发现更好的实现方式
(2)帮助规范代码,保证线上代码的规范、质量
(3)减少因开发人员个人经验、疏忽所可能带来的bug
开发人员在提交codereview时候需要明确写出相关代码变更的需求来源、实现方案以及其他的能够帮助评审人员快速了解这次变更内容的文档、资料。
当然code review本身也是需要一定的规范制度的:
1. 明确reivew点。reviewer需要从多个层次进行reivew:代码本身质量(编码风格,防御性编程等),代码功能逻辑实现,现有系统的影响(侵入、耦合、数据兼容性等)。需要避免单一角度review。对于上述的每一个层次的review,也需要有更细致的review点。
2. 开发人员需要预留充足的review时间,避免在临上线前才提code review。在没有充足的review时间的保证下,很难对上面说的代码功能逻辑实现层次以及现有系统的影响进行充分细致的评估,code review非常容易沦为一个流程过场。我们的做法为上线前一天下班前为code review的最晚提交时间点。如果没有在这个时间点前完成code review的评定,则上线延迟。
## 测试(QA)
为了保证待发布代码的质量,我们在实际的工作中,会将测试交由单独的测试部门同事负责检测。测试的方式和手段也根据带发布的变更不同而不同。总的来说,对于日常功能变更,只需进行新功能测试已经原有功能回归即可。对于系统架构级变更,则会通过压测等衡量系统的负载情况。
对于功能测试来说,测试是对某个特性分支进行测试的,即对每个测试提案单独测试。这样做的目的是避免待上线功能相互影响,提高测试效率。
引入测试环节的意义在于:
1. 由测试部门提供专业、全面的测试,从而保证测试的覆盖度,以及测试的准确度
2. 减轻开发工程师的负担(单测还是要的)
上线申请以及汇总:
上线申请值得是在代码完成了cr以及通过了测试部门的检验后,需要向上级主管以及部门上线总调度人发送上线申请通知。
上线申请内容主要包括本次上线变更内容,代码涉及的功能模块,设计方案,需要上线的服务器集群,回滚方案等。开发人员需要明确提供上述的各类信息,以帮助上级主管、总调度人知悉本次上线功能以及可能造成的影响。
Code review以及测试更多的关注在代码本身的质量上,而业务主管的关注点更多的在于上线方案、变更可能造成的服务的影响点等问题上。
上线申请以及汇总的意义在于:
(1)上线功能的内容经过更高一级的review,从而保证上线服务的方案经过更全面的review
(2)协调跨个人、跨团队的上线。如上所述,我们每次上线都不是单独某个人代码变更,可能涉及到的团队、功能是多个以上的。代码的提交顺序,模块的打包顺序,回滚方案等,都需要经过统一的评估和协调。避免出现各自为政,代码冲突无人负责等问题的出现。
(3)上线变更在部门范围内透明。如果出现某个功能问题,可以做到更大范围的支持帮助。
## 提交代码,打包
当上线被业务主管以及上线总调度人通过后,研发人员可以将自己的特性分支合并到主分支上。并且交由运维同学打包部署。
对于线上代码主分支的修改,我们是统一在上线调度人发出上线汇总邮件后,才进行。目的也是保证线上主干分支在任意时刻都是production级的。避免出现从主干分支获取代码后,出现不可知bug。
当研发人员将代码提交完成后,由运维同学负责打出可发布包(docker镜像)。
## 预览验证
运维使用没有线上真实请求流量的线上机器,部署包含新代码的发布包,完成操作后由测试同学负责在该环境上执行测试用例。
在前面的课程中也已经提到了,在线上系统中,应用服务是由web服务器集群来提供服务的,同时由nginx来提供主要的负载均衡以及健康管理。
如图2所示,线上nginx服务器会定时的向它所维护的web服务器发起健康探测请求,当实际的web服务器返回200响应时,则nginx认为该web服务器正常工作;相反,如果web服务器返回的是503响应时,则nginx认为该web服务器无法正常提供服务,nginx将会把该web服务器从服务列表中摘除,后续请求将不会打到该web服务器上。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f66906d0cd4.jpg)
利用这个特性,我们可以将线上服务器从线上环境中摘除,即让该服务器没有线上请求。运维同学将部署新的代码,重启tomcat服务进程。启动成功后,由测试对其进行功能测试,包括本次上线的所有的功能的测试,以及原测试用例的回归。
预览验证的意义在于:
(1)使用线上服务器环境配置进行测试,所用数据都是真实的线上数据,避免测试环境不一造成的bug遗漏
(2)合并后代码的测试,避免合并代码过程中造成的冲突修改引入不可知bug
## 放量
预览测试通过后,运维同学会将线上流量重新导到预览机器上。此时,预览机器部署的新代码、配置,将经受线上真实流量的检验。
线上流量是最好的检验环境。qa受限于测试用例覆盖度的影响,可能无法做到所有代码分支都检测到,所以我们在线上服务器中引入真实流量用以检验代码的功能的正确性以及对原有服务是否有明显的影响。
我们通常通过对访问日志统计、错误日志以及监控系统的观察来判断变更是否符合要求。
## 全量上线
完成上述的上线步骤后,运维可以通过发布系统,将待发布的代码推送到所有线上应用服务器上,完成本次系统的变更操作。
在推送的过程中,由于启动服务进程本身需要一定的时间,所以运维发布系统会按一定的步长进行操作,即,将线上机器分批重启,一批完成变更后,再进行另外一批。
得益于目前微博平台的双机房架构,我们在核心服务的上线是按机房进行上线的。即先上tc机房服务,完成整个机房的变更后,由测试在tc的线上环境中进行测试用例回归。通过后,再进行yf机房的上线。采取这样的分机房上线的意义在于,如果tc发生问题,我们可以迅速的将流量导流到yf中,避免服务完全不可用。
## 回滚
故障是无法被避免的,无论我们的code review做的多细致,测试覆盖的多全面,上线流程的审查多么严格,最终在变更的过程中依然可能会有因疏漏、bug导致的故障。
如果代码中有预留相应的防御性措施,如开关等,那么可以通过采取相应的措施进行规避。
对于无法通过防御性编程进行错误恢复的变更,我们则需要进行回滚。即将线上代码回滚到上一个稳定版本中,取消变更。
在大型分布系统中,回滚也不是一件简单的事情。即在很多时候,单纯的回退代码可能引入更为严重的问题:
(1)上线过程中写入的数据无法做到向前兼容,回退代码,会造成这部分数据丢失,甚至影响系统功能。
(2)代码、配置回滚顺序。系统采用了代码、配置分离的管理策略。代码的回滚可能依赖于配置的先回滚,单纯回滚代码可能会造成服务无法启动的问题。
(3)队列机、前端应用服务器回退顺序。在线上系统中,我们采用前端应用服务器加队列服务器的架构,即由队列服务负责对资源进行更新,前端服务器只对资源进行下行获取。当涉及到资源的上线回退的时候,资源的读写顺序回退的错乱可能会造成线上数据不一致。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f669072e042.jpg)
图3 异步消息处理架构
所以我们在上线申请过程中,就需要明确回退步骤,包括:
(1)代码配置回退顺序
(2)前端机、处理机回退顺序
(3)数据尽量做到向前兼容,无法向前兼容的需要明确指出
当线上真的发生了问题时,我们将马上按照上线定好的回滚方案进行代码、配置回滚。从而保证线上服务稳定。
## 总结:
本文主要结合微博平台的开发、上线流程进行说明。如文中所说的,上线是一件具有风险的操作,每次新加入的功能点、改造变更,都可能给线上系统引入不可知的问题与故障。为了尽量减少类似风险,以及在类似问题出现后,可以快速的进行恢复,我们根据自身的业务特点总结设计出了一套研发上线流程。
引入流程并不是为了限制研发人员的研发效率,而是希望借助对每个流程环节的把控,从而保证我们每次变革的质量,尽量减少bug、故障的发生。
当然,我们也不认为我们的流程能够完全将故障排除。借助流程中的上线前梳理、代码的防御性编程,回滚方案的准备等措施,以及上线后的监控、发布系统的调度等措施来做到出事可干预,有问题可以快速回滚,从而减少故障对线上服务的影响。
## 展望:
目前我们也在积极的推动持续集成、微服务、docker化,希望借助于这些新的技术来提升我们开发、上线的效率,本文因篇幅所限虽未就这些方面进行展开描述,但希望各位关注@微博平台架构 后续的微博、课程分享、讨论。如果你有好的资料和建议,也可以随时反馈给我们。
------------------新兵训练营简介------------------
微博平台新兵训练营活动是微博平台内部组织的针对新入职同学的团队融入培训课程,目标是团队融入,包括人的融入,氛围融入,技术融入。当前已经进行4期活动,很多学员迅速成长为平台技术骨干。具体课程包括《环境与工具》《分布式缓存介绍》《海量数据存储基础》《平台RPC框架介绍》《平台Web框架》《编写优雅代码》《一次服务上线》《Feed架构介绍》《unread架构介绍》。微博平台是非常注重团队成员融入与成长的团队,在这里有人帮你融入,有人和你一起成长,也欢迎小伙伴们加入微博平台,欢迎私信咨询
------------------讲师简介------------------
赖佳俊(微博昵称:[@LierD](http://weibo.com/n/LierD) ),2013年7月毕业于哈尔滨工业大学后,加入新浪微博工作至今,担任系统研发工程师。曾先后参与微博Feed流优化、后端服务稳定性建设等项目。关注jvm调优,微服务,分布式存储系统。微博平台新兵训练营二期学员.
业余爱好三国杀,dota,喜欢自己做饭下厨~~希望能在微博的平台上认识到更多优秀的小伙伴,一起交流,一起成长~~
Unread架构介绍
最后更新于:2022-04-01 03:15:25
> 原文出处:http://weibo.com/p/1001643880172431480781
> 作者:唐扬,[@唐扬TY](http://weibo.com/n/%E5%94%90%E6%89%ACTY)
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f6685f4e461.jpg)
未读提醒功能在各种社交平台服务中较为常见,在微博中这些功能由Unread服务来提供。看似简单的功能,当请求量级达到一定规模后,成本、性能、稳定性的平衡将是架构设计的重点。
**大纲**
[TOC]
## 一 微博中Unread服务业务场景
在以timeline为核心的微博业务中, 未读数场景出现的频率较高,它可以是这样的…
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f6686235481.jpg)
也可以是这样的…
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f66862b2ad7.jpg)
## 二 从架构角度对各种业务场景的抽象
通过分析和比较这些未读场景,我们抽象了unread服务中设计到的三种主要操作:
1. incr:增加未读数
2. reset:未读数清零
3. get:获取未读数
我们发现unread服务中get操作是典型的无触发操作,即不需要用户执行任何操作都会对服务器造成请求。正是这个特点给unread服务带来如下问题:
1. 高并发:高峰期单一业务的qps达到10万+
2. 性能要求高:接口4个9的响应时间在10ms
为了解决上述问题,unread架构针对不同的业务场景设计了不同的方案,保证了服务的高性能、高可用和可扩展。本文主要针对三种典型的未读数场景介绍微博平台是如何设计解决方案的。
### 场景一. 一对一行为未读提醒场景
在这种场景下,用户的某一个操作只会影响一个用户的未读数字。典型的场景有:@未读提醒、评论未读提醒、赞评论提醒等等。
针对这种场景,我们采用最简单的解决方案:为每一个用户存储一份未读数字,如下图
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f668633120e.jpg)
在设计的实现中,由于存储容量可控,我们采用redis存储未读数。相比于通常使用的mysql+mc的存储解决方案,redis有以下的优势:
1. 存储一体化,避免了缓存和持久化存储之间一致性的问题
2. 快速恢复
这个设计方案主要基于如下的考虑:
1. 简单直观
2. 性能能够达到SLA,每次操作只需要访问一次资源
### 场景二. 全量打点场景
打点主要指微博官方客户端中的一些弱提醒功能,见下图中的红点
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f66863b3125.jpg)
而全量打点指对全量用户都增加未读提醒红点。考虑到目前微博的用户量,如果采用第一种场景的方案,打点过程会存在很大的延迟。因此我们采用了基于tag的解决方案。
1. 存储公共的时间戳global
2. 每一个用户存储一个时间戳
3. 打点时,更新global时间戳为当前时间
4. 消点时,更新用户的时间戳为global时间戳
5. 如果用户时间戳小于global时间戳,则有点;否则没有点
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f66863d2132.jpg)
这个方案适用于用户间存在共享存储,且共享存储有限的场景。在这种场景下,我们为每一个用户存储一个tag用来记录用户在共享存储中的已读位置,这样就可以通过比较这个已读位置获得用户的未读数。
在实际的应用过程中,我们通常会使用本地缓存来解决访问共享存储的极端峰值。这种基于已读位置的解决方案虽然能很好的解决全量打点的问题,但是面对访问量最大的微博未读数场景却是无能为力,原因有二:
1. 用户的feed是无限的,不存在共享存储,全部存储下来的成本很高
2. 在高并发下获取未读数操作性能衰减严重
我们采用了下面这种方案来解决微博未读数问题。
### 场景三. 微博未读数场景
众所周知,微博未读数就是微博主feed未读数,当我关注的人发表一条微博,我的微博未读数提醒就会加一。
对于微博微博数场景,我们采用了基于snapshot的解决方案,具体如图所示:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f6686440c28.jpg)
1. 我们会存储每一个用户发微博数,用户发微博时只会增加自身的微博数
2. 用户reset时会触发一次快照操作,存储当时用户的关注关系以及每一个关注者的微博数。
3. 用户的未读数由当前用户所有关注人的微博数和快照中所有关注人的微博数的差值组成
基于快照的方案近乎完美的解决了微博未读数的问题,目前微博平台单机qps接近5万,平均响应时间小于10ms
在基于快照的方案中,我们采用空间换时间的方式,额外存储了一份snapshot来换取性能上达到SLA要求。因此这个方案主要的适用场景是快照的存储有限或者是允许丢失。
在微博未读数实现中,我们采用memcache存储快照,使用本地内存存储用户的微博数,采用这种方式是基于以下考虑:
1. 存储用户微博数的空间有限,使用本地内存可以减少一次网络开销
2. 快照中需要存储关系,理论上需要N*N的空间消耗(N为用户数),利用memcache的LRU机制可以淘汰不活跃用户的快照,节省空间开销
memcache的容量评估也是这个方案实现中需要考虑的重要点。我们主要从两个方面来考虑:
1. 容量:根据存储的单个用户快照大小和MAU评估
2\. QPS:依据前端QPS和每次调用带来的对memcache的调用次数估算出memcache的QPS,以此QPS估算需要多少个memcache实例
以上就是我对于微博平台不同的未读场景设计的未读架构的理解。在设计的过程中,我们没有拷贝相似场景的方案,而是权衡需求、性能、成本和扩展性等多方面的因素,设计出最合适的架构。希望通过以上介绍能给大家以后工作有所帮助和启发。
**------------------新兵训练营简介**------------------
微博平台新兵训练营活动是微博平台内部组织的针对新入职同学的团队融入培训课程,目标是团队融入,包括人的融入,氛围融入,技术融入。当前已经进行4期活动,很多学员迅速成长为平台技术骨干。
微博平台是非常注重团队成员融入与成长的团队,在这里有人帮你融入,有人和你一起成长,也欢迎小伙伴们加入微博平台,欢迎私信咨询。
**------------------讲师简介**------------------
唐扬,[@唐扬TY](http://weibo.com/n/%E5%94%90%E6%89%ACTY),微博平台及大数据部技术专家,负责微博平台架构及基础架构研发工作,在高并发可扩展架构设计方面有丰富的实战经验。新兵训练营第一期学员。
编写优雅代码
最后更新于:2022-04-01 03:15:23
> 原文出处:http://weibo.com/p/1001643877361430185536
> 作者:秦迪,[@蛋疼的AXB](http://weibo.com/n/%E8%9B%8B%E7%96%BC%E7%9A%84AXB)
![document/2015-09-14/55f667c23be38](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/document_2015-09-14_55f667c23be38.png)
**课程大纲**
[TOC=2,2]
## 什么是好代码
对于代码质量的定义需要于从两个维度分析:主观的,被人类理解的部分;还有客观的,在计算机里运行的状况。
我把代码质量分为五个层次,依次为:
* 完成功能的代码
* 高性能的代码
* 易读的代码
* 可测试的代码
* 可扩展的代码
## 如何编写可读的代码
在很多跟代码质量有关的书里都强调了一个观点:程序首先是给人看的,其次才是能被机器执行。
### 逐字翻译
在评价一段代码能不能让人看懂的时候,可以自己把这段代码**逐字**翻译成中文,试着组成句子,之后把中文句子读给另一个人没有看过这段代码的人听,如果另一个人能听懂,那么这段代码的可读性基本就合格了。
而实际阅读代码时,读者也会一个词一个词的阅读,推断这句话的意思,如果仅靠句子无法理解,那么就需要联系上下文理解这句代码,如果简单的联系上下文也理解不了,可能还要掌握更多其它部分的细节来帮助推断。大部分情况下,理解一句代码**在做什么**需要联系的上下文越多,意味着代码的质量越差。
逐字翻译的好处是能让作者能轻易的发现那些只有自己知道的、没有体现在代码里的假设和可读性陷阱。无法从字面意义上翻译出原本意思的代码大多都是烂代码,比如“ms代表messageService“,或者“ms.proc()是发消息“,或者“tmp代表当前的文件”。
### 遵循约定
约定包括代码和文档如何组织,注释如何编写,编码风格的约定等等,这对于代码未来的维护很重要。
大家刚开始工作时,一般需要与部门的约定保持一致,包括一些强制的规定,如代码的格式化设置文件;或者一些非强制的约定,如工程的命名等。
从更大的范围考虑,整个行业的约定和规则也在不断的更新。所以在工作中也要对行业动向和开源项目保持关注,如果发现部门中哪一项约定已经过时了,那么可以随时提出来,由平台的架构师小组review之后推进平台更新这些规则。
### 文档和注释
对于文档的标准很简单,能找到、能读懂就可以了,一般一个工程至少要包含以下几类文档:
1. 对于项目的介绍,包括项目功能、作者、目录结构等,读者应该能3分钟内大致理解这个工程是做什么的。
2. 针对新人的QuickStart,读者按照文档说明应该能在1小时内完成代码构建和简单使用。
3. 针对使用者的详细说明文档,比如接口定义、参数含义、设计等,读者能通过文档了解这些功能(或接口)的使用方法。
有一部分注释实际是文档,比如javadoc。这样能把源码和注释放在一起,对于读者更清晰,也能简化不少文档的维护的工作。
还有一类注释并不作为文档的一部分,比如函数内部的注释,这类注释的职责是说明一些代码本身无法表达的作者在编码时的思考,比如“为什么这里没有做XX”,或者“这里要注意XX问题”。
一般来说函数内部注释的数量应该不会有很多,也不会完全没有,一般滚动几屏幕看到一两处左右比较正常。过多的话可能意味着代码本身的可读性有问题,而如果一点都没有可能意味着有些隐藏的逻辑没有说明,需要考虑适当的增加一点注释了。
其次也需要考虑注释的质量:在代码可读性合格的基础上,注释应该提供比代码更多的信息。文档和注释并不是越多越好,它们可能会导致维护成本增加。
## 如何编写可发布的代码
刚开始接触高并发线上系统的新同学经常容易出现一个问题:写的代码在发布之后才发现很多考虑不到的地方,比如说测试的时候没问题,项目发布之后发现有很多意料之外的状况;或者出了问题之后不知道从哪下手排查,等等。
这里介绍一下从代码编写完成到发布前需要注意的一些地方。
### 处理异常
新手程序员普遍没有处理异常的意识,但代码的实际运行环境中充满了异常:服务器会死机,网络会超时,用户会胡乱操作,不怀好意的人会恶意攻击你的系统。
对一段代码异常处理能力的第一印象应该来自于单元测试的覆盖率。大部分异常难以在开发或者测试环境里复现,依靠测试团队也很难在集成测试环境中模拟所有的异常情况。
而单元测试可以比较简单的模拟各种异常情况,如果一个模块的单元测试覆盖率连50%都不到,很难想象这些代码考虑了异常情况下的处理,即使考虑了,这些异常处理的分支都没有被验证过,怎么指望实际运行环境中出现问题时表现良好呢?
### 处理并发
而是否高质量的实现并发编程的关键并不是是否应用了某种同步策略,而是看代码中是否保护了共享资源:
* 局部变量之外的内存访问都有并发风险(比如访问对象的属性,访问静态变量等)
* 访问共享资源也会有并发风险(比如缓存、数据库等)。
* 被调用方如果不是声明为线程安全的,那么很有可能存在并发问题(比如java的hashmap)。
* 所有依赖时序的操作,即使每一步操作都是线程安全的,还是存在并发问题(比如先删除一条记录,然后把记录数减一)。
前三种情况能够比较简单的通过代码本身分辨出来,只要简单培养一下自己对于共享资源调用的敏感度就可以了。
但是对于最后一种情况,往往很难简单的通过看代码的方式看出来,甚至出现并发问题的两处调用并不是在同一个程序里(比如两个系统同时读写一个数据库,或者并发的调用了一个程序的不同模块等)。但是,只要是代码里出现了不加锁的,访问共享资源的“先做A,再做B”之类的逻辑,可能就需要提高警惕了。
### 优化性能
性能是评价程序员能力的一个重要指标,但要评价程序的性能往往要借助于一些性能测试工具,或者在实际环境中执行才能有结果。
如果仅从代码的角度考虑,有两个评价执行效率的办法:
* 算法的时间复杂度,时间复杂度高的程序运行效率必然会低。
* 单步操作耗时,单步耗时高的操作尽量少做,比如访问数据库,访问io等,这里需要建立对各种操作的耗时的概念。
而实际工作中,也会见到一些同学过于热衷优化效率,相对的会带来程序易读性的降低、复杂度提高、或者增加工期等等。所以在优化之前最好多想想这段程序的瓶颈在哪里,为什么会有这个瓶颈,以及优化带来的收益。
再强调一下,无论是优化不足还是优化过度,判断性能指标最好的办法是用数据说话,而不是单纯看代码。
### 日志
日志代表了程序在出现问题时排查的难易程度,对于日志的评价标准有三个:
* 日志是否足够,所有异常、外部调用都需要有日志,而一条调用链路上的入口、出口和路径关键点上也需要有日志。
* 日志的表达是否清晰,包括是否能读懂,风格是否统一等。这个的评价标准跟代码的可读性一样,不重复了。
* 日志是否包含了足够的信息,这里包括了调用的上下文、外部的返回值,用于查询的关键字等,便于分析信息。
对于线上系统来说,一般可以通过调整日志级别来控制日志的数量,所以打印日志的代码只要不对阅读造成障碍,基本上都是可以接受的。
## 如何编写可维护的代码
可维护性要对应的是未来的情况,但是一般新人很难想象现在的一些做法会对未来造成什么影响。
### 避免重复
代码重复分为两种:模块内重复和模块间重复。两种重复都在一定程度上说明了编码的水平有问题。现代的IDE都提供了检查重复代码的工具,只需点几下鼠标就可以判断了。
除了代码重复之外,还会出现另一类重复:信息重复。
比方说每行代码前面都写一句注释,一段时间之后维护的同学忘了更新注释,导致注释的内容和代码本身变得不一致了。此时过多的注释反而成了鸡肋,看之无用,删之可惜。
随着项目的演进,无用的信息会越积越多,最终甚至让人无法分辨哪些信息是有效的,哪些是无效的。
如果在项目中发现好几个东西都在做同一件事情,比如通过注释描述代码在做什么,或者依靠注释替代版本管理的功能,这些都是需要避免的。
### 划分模块
模块内高内聚与模块间低耦合是大部分设计遵循的标准,通过合理的模块划分能够把复杂的功能拆分为更易于维护的更小的功能点。
一般来说可以从代码长度上初步评价一个模块划分的是否合理,一个类的长度大于2000行,或者一个函数的长度大于两屏幕都是比较危险的信号。
另一个能够体现模块划分水平的地方是依赖。如果一个模块依赖特别多,甚至出现了循环依赖,那么也可以反映出作者对模块的规划比较差,今后在维护这个工程的时候很有可能出现牵一发而动全身的情况。
有不少工具能提供依赖分析,比如IDEA中提供的Dependencies Analysis功能,学会这些工具的使用对于评价代码质量会有很大的帮助。
值得一提的是,绝大部分情况下,不恰当的模块划分也会伴随着极低的单元测试覆盖率:复杂模块的单元测试非常难写的,甚至是不可能完成的任务。
### 简洁与抽象
只要提到代码质量,必然会提到简洁、优雅之类的形容词。简洁这个词实际涵盖了很多东西,代码避免重复是简洁、设计足够抽象是简洁,一切对于提高可维护性的尝试实际都是在试图做减法。
编程经验不足的程序员往往不能意识到简洁的重要性,乐于捣鼓一些复杂的玩意并乐此不疲。但复杂是代码可维护性的天敌,也是程序员能力的一道门槛。
跨过门槛的程序员应该有能力控制逐渐增长的复杂度,总结和抽象出事物的本质,并体现到自己设计和编码中。一个程序的生命周期也是在由简入繁到化繁为简中不断迭代的过程。
简洁与抽象更像是一种思维方式,除了要理解、还需要练习。多看、多想、多交流,很多时候可以简化的东西会大大超出原先的预计。
## 如何做出优雅的设计
当程序的功能越来越多时,编程就不再只是写代码,而会涉及到模块的划分、和模块之间的交互等内容。对于新同学来说,一开始很难写出优雅的设计。
这一节会讨论一下如何能让自己编写的代码有更强的“设计感”。
### 参考设计模式
最容易快速上手的提升自己代码设计水平的方式就是参考其他人的设计,这些前人总结的面对常见场景时如何进行模块划分和交互的方式被称作设计模式。
### 设计模式的分类
* 创建型模式主要用于创建对象。
* 结构型模式主要用于处理类或对象的组合。
* 行为型模式主要用于描述对类或对象怎样交互和怎样分配职责。
由于篇幅有限,这里不再展开每一种设计模式的用途。这部分资料和书籍已经比较全了,可以课下学习。
### 编写单元测试
### 单元测试是什么
维基百科上的词条说明:
> 单元测试是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。
所以,我眼中的“合格的”单元测试需要满足几个条件:
1. 测试的是一个代码单元内部的逻辑,而不是各模块之间的交互。
2. 无依赖,不需要实际运行环境就可以测试代码。
3. 运行效率高,可以随时执行。
### 单元测试的目的
了解了单元测试是什么之后,第二个问题就是:单元测试是用来做什么的?
很多人第一反应是“看看程序有没有问题”。但是,只是使用单元测试来“看看程序有没有问题”的话,效率反而不如把程序运行起来直接查看结果。原因有两个:
1. 单元测试要写额外的代码,而不写单元测试,直接运行程序也可以测试程序有没有问题。
2. 即使通过了单元测试,程序在实际运行的时候仍然有可能出问题。
在这里我总结了一下几个比较常见的单元测试的几个典型场景:
1. 开发前写单元测试,通过测试描述需求,由测试驱动开发。
2. 在开发过程中及时得到反馈,提前发现问题。
3. 应用于自动化构建或持续集成流程,对每次代码修改做回归测试。
4. 作为重构的基础,验证重构是否可靠。
还有最重要的一点:编写单元测试的难度和代码设计的好坏息息相关,单元测试测的三分是代码,七分是设计,能写出单元测试的代码基本上可以说明这段代码的设计是比较合理的。能写出和写不出单元测试之间体现了编程能力上的巨大的鸿沟,无论是什么样的程序员,坚持编写一段时间的单元测试之后,都会明显感受到代码设计能力的巨大提升。
### 如何编写单元测试
测试代码不像普通的应用程序一样有着很明确的作为“值”的输入和输出。举个例子,假如一个普通的函数要做下面这件事情:
* 接收一个user对象作为参数
* 调用dao层的update方法更新用户属性
* 返回true/false结果
那么,只需要在函数中声明一个参数、做一次调用、返回一个布尔值就可以了。但如果要对这个函数做一个“纯粹的”单元测试,那么它的输入和输出会有很多情况,比如其中一个测试是这样:
* 假设调用dao层的update方法会返回true。
* 程序去调用service层的update方法。
* 验证一下service是不是也返回了true。
而具体的测试内容可以依赖单元测试框架提供的功能来完成。
### 单元测试框架
运行框架:
* jUnit
* TestNG
* Spock
Mock框架:
* Mockito
* EasyMock
* PowerMock
* Spock
由于篇幅限制,这里不再展开具体的框架用法了,有兴趣的同学可以自行搜索相关文章。
## 如何规划合理的架构
很多新同学的规划都是未来成为架构师,要做架构师就免不了设计架构。而在微博平台工作也会经常跟架构打交道,由于后面有独立的架构课程,这里只是简单介绍一下常见的架构模式。
### 常见的架构模式
### 分层架构
分层架构是应用很普遍架构模式,它能降低模块之间的耦合,便于测试开发,它也是程序员需要掌握的基础。
典型的分层架构模式如下:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f666c340fe1.jpg)
上图中是一个4层的架构,在现实场景中,层数不是一个定值,而是需要根据业务场景的复杂度决定。使用分层模型中需要注意,一般来说不能跨层、同层或反向调用,否则会让整个层次模型由单一的树形结构变为网状结构,失去了分层的意义。
但随着程序复杂度的逐渐提升,要严格的按照分层模型逐级调用的话,会产生很多无用的空层,它们的作用只是传递请求,这也违背了软件设计尽量简洁的方向。所以实际场景中可以对各个层次规定“开放”或“关闭”属性,对于“开放”的层次,上层可以越过这层,直接访问下层。
对层次定义“开放”或“关闭”可以帮助程序员更好的理解各层次之间的交互,这类约定需要记录在文档中,并且确保团队中的每个人都了解这些约定,否则会得到一个复杂的、难以维护的工程。
### 事件驱动架构
事件驱动架构能比较好的解耦请求方和处理方,经常被用在写入请求量变化较大,或者是请求方不关心处理逻辑的场景中,它有两种主要的实现方式:
### Mediator
在mediator方式中,存在一个中介者角色,它接收写入方的请求,并把事件分配到对应的处理方(图中的channel),每个处理方只需要关心自己的channel,而不需要与写入方直接通信。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f666c3c2c99.jpg)
在微博前些年应用比较多的应用服务-队列-消息处理服务可以认为是属于这种模式。
### Broker
在broker方式中不存在中介者的角色,取而代之的是消息流在轻量的processor中流转,形成一个消息处理的链路,如图:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f666c45bae5.jpg)
前一段时间开始推广的storm流式处理属于这种模式,对于较长的处理流程,用broker方式可以避免实现Mediator的复杂性,相对的,管理整个流程变得复杂了。
### 微内核架构(Microkernel)
微内核架构相对于普通架构最主要的区别是多了“内核”的概念,在编写程序时把基础功能和扩展功能分离:内核中不再实现具体功能,而是定义“扩展点”,增加功能时不再修改主逻辑,而是通过“扩展点”挂接到内核中,如图:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f666ca344f1.jpg)
之前介绍的motan RPC框架应用了这种设计,这让motan可以通过不同的插件支持更多的功能,比如增加传输协议、定义新的服务发现规则等。
### 微服务架构
近年来微服务架构的概念一直比较火,它可以解决服务逐渐增长之后造成的难以测试及部署、资源浪费等问题,但也带来了服务调度和服务发现层面的复杂度,如图:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f666ca53098.jpg)
微博底层实际包含了很多业务逻辑,这些业务逻辑被抽象成一个个服务和模块,不同模块之间通过motan rpc、grpc或http rest api进行通信,通过docker和之上的调度服务进行调度和部署,最终成为一个完整的系统。
微服务隔离了各服务之间的耦合,能够有效提升开发效率;除此之外,当微博面对巨大的流量峰值时,可以进行更精细的资源调配和更有效率的部署。
### 单元化架构
传统的分层架构往往会存在一些中心节点,如数据库、缓存等,这些节点往往容易成为性能瓶颈,并且存在扩容比较复杂的问题。
在面临对扩展性和性能有极端要求的场景时,可以考虑使用单元化架构:对数据进行切分,并将每一部分数据及相关的逻辑部署在同一个节点中,如图:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f666ca7cde5.jpg)
在单元化架构中,每个“单元”都可以独立部署,单元中包括独立的计算和存储模块;计算模块只与单元内的存储模块交互,不再需要分库分表等逻辑;而数据与存储更近也降低了网络消耗,进而提高了效率。
微博平台通过对群发服务的单元化改造,实现了百万级每秒的私信推送服务。
## 如何处理遗留代码
对于一个不断发展的系统,必然有一些遗留下来的历史问题。当遇到了遗留下来的烂代码时,除了理解和修改代码,更重要的是如何让代码朝着好的方向发展,而不是放任不管。
重构是处理遗留代码的比较重要的手段,这一节主要讨论一下重构的相关话题。
### 何时重构
新同学往往对重构抱有恐惧感,认为重构应该找一个比较长的时间专门去做。这种愿望很好,但是现实中一般很难找出一段相对稳定的时间。
另一方面,重构是比较考验编程水平的一项工作。代码写的不好的人,即使做了重构也只是把不好的代码变了个形式。要达到比较高的水平往往需要不断的练习,几个月做一次重构很难得到锻炼,重构效果也会打折。
所以,重构最好是能够作为一项日常工作,在开发时对刚写完的代码做重构往往单位时间的收益是最大的。
### 如何重构
一般来说,重构可以抽象成四个方面:
#### 理解现状
如果对当前程序的理解是错的,那么重构之后的正确性也就无从谈起。所以在重构之前需要理解待重构的代码做了什么,这个过程中可以伴随一些小的、基本无风险的重构,例如重命名变量、提取内部方法等,以帮助我们理解程序。
#### 理解目标
在理解了程序做了什么事情之后,第二个需要做的事情就是需要提前想好重构之后的代码是什么样的。
改变代码结构比较复杂,并且往往伴随着风险和不可控的问题。所以在实际动手之前,需要从更高的层次考虑重构之后的模块如何划分,交互是如何控制等等,在这个过程中实际与写代码要做的事情是一致的。
#### 划分范围
烂代码往往模块的划分有一些问题,在重构时牵一发而动全身,改的越多问题越多,导致重构过程不可控。所以在动手重构前需要想办法减少重构改动的范围,一般来说可以只改动相邻层次的几个类,并且只改动一个功能相关的代码,不求一次性全部改完。
为了能够划分范围,在重构时需要采用一些方法解除掉依赖链。比如增加适配器等等,这些类可能只是临时的,在完整的重构完成之后就可以删除掉,看起来是增加了工作量,但是换来的是更可控的影响范围。
#### 确保正确
为了能保证重构的正确性,需要一些测试来验证重构是否安全。最有效的是单元测试,它能提供比集成测试更高的覆盖率,也能验证重构之后的代码设计是否是合理的。
在做一次重构之前需要整理模块的单元测试。遗留代码有可能测试不全,并且难以编写单元测试,此时可以适当的牺牲待重构代码的优雅性,比如提高私有方法的可见性,满足测试的需求。在重构的过程中,这部分代码会被逐渐替换掉。
## 总结
今天跟大家讨论了一下关于编程的各个方面,关于编程的话题看似很基础,想要做好却并不容易。新同学比较容易急于求成,往往过多的关注架构或者某些新技术,却忽视了基本功的修炼,而在后续的工作过程中,基本功不扎实的人做事往往会事倍功半,难以有更一步的发展。
勿在浮沙筑高台,与各位共勉。
**新兵训练营简介**
微博平台新兵训练营活动是微博平台内部组织的针对新入职同学的团队融入培训课程,目标是团队融入,包括人的融入,氛围融入,技术融入。当前已经进行4期活动,很多学员迅速成长为平台技术骨干。
具体课程包括《环境与工具》《分布式缓存介绍》《海量数据存储基础》《平台RPC框架介绍》《平台Web框架》《编写优雅代码》《一次服务上线》《Feed架构介绍》《unread架构介绍》
微博平台是非常注重团队成员融入与成长的团队,在这里有人帮你融入,有人和你一起成长,也欢迎小伙伴们加入微博平台,欢迎私信咨询。
**讲师简介**
秦迪,[@蛋疼的AXB](http://weibo.com/n/%E8%9B%8B%E7%96%BC%E7%9A%84AXB) 微博平台及大数据部技术专家
个人介绍:http://blog.2baxb.me/about-me
平台服务部署及Web框架
最后更新于:2022-04-01 03:15:21
> 原文出处:http://weibo.com/p/1001643875679132642345
> 作者:苏星宇,[@zhuidawugui](http://weibo.com/n/zhuidawugui)
![document/2015-09-14/55f668043ad17](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/document_2015-09-14_55f668043ad17.png)
**大纲**
微博平台主要负责微博基础功能。接下来将会介绍
* 平台的作用,以及服务提供的形式
* 平台Web服务的部署
* 平台Web框架简介
## 背景
目前整体架构大体上分为三层
* 展现层:手机端,主站和第三方应用,承担相关业务的前端展示
* 适配层:负责服务端和多个展示端的接口适配
* 服务层:提供基础功能服务,包括Feed服务,用户关系,开放平台和消息箱等
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f66614c8d5b.jpg)
平台作为整个微博架构的基础功能服务层,对外以Http接口的方式提供服务。接口遵守RESTful规范。接口示例如下:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f66614e833d.jpg)
关于RESTful,与其说是规范,其实更像是一种架构设计风格。它主要是提供了一组设计原则和约束条件,广泛应用于C/S或者B/S架构中。要想理解什么是RESTful,可以从它的全称入手--Representational State Transfer,翻译成中文是表现层状态转化。这段晦涩的文字省略了主语,"表现层"其实指的是"资源"(Resources)的"表现层"。核心概念包括
* 资源是由URI来指定。
* 对资源的操作包括获取、创建、修改和删除资源,这些操作正好对应HTTP协议提供的GET、POST、PUT和DELETE方法。
* 通过操作资源的表现形式来操作资源。
概括起来,平台对外提供服务的形式就是通过HTTP接口对基础资源进行存取。
## 平台服务部署
对平台的定位和服务形式有所了解后,我们看下平台的Web服务部署结构。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f666150e7e6.jpg)
平台的服务部署在多个机房中。以北京为例,就有AX、BX和CX三个机房。自建的DNS服务会将用户的流量根据不同的运营商切换到不同的机房。
用户请求到达服务端后,首先会经过反向代理服务器。反向代理(Reverse Proxy)方式是指以代理服务器来接受公网上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给公网上请求连接的客户端,此时代理服务器对外就表现为一个反向代理服务器。平台目前使用的反向代理有LVS和Nginx。
* LVS:Linux Virtual Server,基于IP的负载均衡和反向代理技术
* Nginx:基于HTTP的负载均衡和反向代理服务器
关于Nginx,除了以上提到的之外,还负责集群的健康检查。这个主要是通过Nginx自带的健康检查模块实现的。Nginx server会轮询后端集群的index.jsp页面,如果返回200则认为服务器正常,请求会正常被转发到该服务器;返回503则进行服务器摘除,请求将不会再到达该服务器。
经过反向代理转发后,请求会到达部署Web应用的应用服务器。平台目前主要使用Tomcat作为应用容器。之后,请求会被统一的Web框架解析并处理。稍后会详细讲述Web框架的内容。
对于上行和下行不同的请求,请求处理的链路也不同。
以微博核心业务Feed流为例。应用服务器在收到下行请求(如查询一条微博的内容)时,会直接访问缓存资源,如果命中则直接返回结果给客户端,否则继续查询DB,将结果返回客户端。
而收到上行请求(如发微博)时,应用会将上行请求写入一个消息队列中。由另一个单独的处理应用读取消息队列,执行上行请求的资源操作,比如写入缓存、更新DB等等。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f666153c8ba.jpg)
这种队列加处理机的上行请求模式被平台广泛使用,主要有以下优点:
* 解除前端应用和后端资源的耦合
* 削峰填谷:在请求量很大时,队列可以作为缓冲,缓解后端资源的压力
由于请求被分配到不同机房,因此多机房之间的数据也需要同步。目前我们使用虚拟消息管道WMB来同步机房之间的数据:所有的上行请求在到达某个机房后都会通过WMB重放到其他机房,从而保证机房后端资源一致。除此之外,为了容灾,后端资源如缓存,DB的主从集群会分布在不同机房。彼此之间通过应用自身(Redis、MySql)或者客户端(Memcached)来同步主从数据。
## 平台Web框架
下面给大家简单介绍下我们使用的Web框架。前面我们提到,在请求到达应用容器后,首先会被统一Web框架进行处理。用户请求在应用容器中的整个处理链路如下。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f666155bcf2.jpg)
Web框架的处理主要是将Http形式的请求转换成应用运行环境(如JVM)理解的请求,包括接口路由、参数处理和参数校验等等。平台目前使用Credus作为统一的Web框架,它是一个基于Jersey改造的自研框架。
Jersey是JAX-RS(JSR311)开源参考实现用于构建RESTful Web service。特性比较丰富,包括
* 接口路由
* 功能丰富的Filter
* Http参数校验
* 文档生成
此外Jersey还提供一些额外的API和扩展机制,所以开发人员能够按照自己的需要对Jersey进行扩展。
在Jersey提供的扩展机制上,我们开发了Credus,主要功能有
* 封装Jersey框架
* 定制内容
* Wiki模板
在Jersey提供的Filter机制上,Credus框架定制了一系列接口通用策略和功能。包括用户认证、接口频次限制、接口信息统计和返回接口JsonP封装。另外,还进一步扩展了Jersey原有的参数校验,增加了更多了参数校验方式。Web请求在Credus框架中的处理过程如下
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f666157b84c.jpg)
## 总结
这次和大家分享了微博平台的相关知识,包括平台功能、平台服务部署以及平台Web框架介绍。希望通过本次分享,大家能够对微博平台有所了解,同时对服务结构有一个整体的认识,对以后的工作有所帮助。
由于篇幅和主题的限制,还有很多内容没有具体展开,有兴趣可以关注[@平台技术沙龙](http://weibo.com/n/%E5%B9%B3%E5%8F%B0%E6%8A%80%E6%9C%AF%E6%B2%99%E9%BE%99)。
------------------**新兵训练营简介**------------------
微博平台新兵训练营活动是微博平台内部组织的针对新入职同学的团队融入培训课程,目标是团队融入,包括人的融入,氛围融入,技术融入。当前已经进行4期活动,很多学员迅速成长为平台技术骨干。
微博平台是非常注重团队成员融入与成长的团队,在这里有人帮你融入,有人和你一起成长,也欢迎小伙伴们加入微博平台,欢迎私信咨询。
------------------**讲师简介**------------------
苏星宇,[@zhuidawugui](http://weibo.com/n/zhuidawugui),微博平台及大数据部——通讯系统研发工程师,2014年1月毕业于中国科学技术大学,校招进入微博。先后负责粉服平台、群聊、微博相机消息箱等业务后端设计和研发工作。关注分布式系统设计和服务保障,Storm流式处理。新兵训练营第三期学员
海量数据存储基础
最后更新于:2022-04-01 03:15:18
> 原文出处:http://weibo.com/p/1001643874615465508614
> 作者:毕建坤[@bijiankun](http://weibo.com/n/bijiankun)
![document/2015-09-14/55f667eec97d8](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/document_2015-09-14_55f667eec97d8.png)
微博平台研发作为微博的底层数据及业务支撑部门,已经经历了5年的发展历程。伴随着从数据及业务暴发式增长,我们在海量数据存储方面遭遇了诸多挑战,与此同时也伴随着丰富经验的积累。
本次新兵训练营,受众在于应届毕业生,目的在于让新同学系统化并且有针对性的了解平台的核心技术及核心业务,以使新同学在新兵训练营结束后,能够对平台的底层架构与业务有一定的了解。
本文主要面向新同学介绍平台的核心技术之一——海量数据存储,主要介绍在海量数据存储在大规模分布式系统下的架构变迁与设计。
**课程大纲:**
[TOC]
## 一、课程目标
1\. 了解存储服务概况,以及RDBMS及NoSQL的差异
2\. 理解MySQL、Redis、HBase基本实现机制、特性、适用场景
3\. 理解几种存储产品的大规模分布式服务方案
4\. 学会使用平台的MySQL、Redis client组件
5\. 理解对于MySQL、Redis分布式系统设计想要注意的问题
6\. 了解平台几种典型案例
7\. 理解几种存储产品在平台的定制修改与名词术语
## 二、存储服务概述
1\. 关系型数据库是基于实体关系模型(Entity-Relationship Model)的数据服务,具备以下特点。
* 适合存储结构化数据
* 查询语言SQL,insert delete update select
* 主流关系型数据库多是持久化存储系统,系统性能与机器性能相关性较大
* 几类主流的 关系型数据库
* * MySQL
* Oracle
* DB2
* SQL Server
* 性能
* * 局限于服务器性能,与其是磁盘性能
* 局限于数据复杂度
* 常见的SSD磁盘服务器,单机读取性能可达万级/s
大型互联网服务大多采用MySQL进行作为关系型数据库,微博平台的核心业务(如微博内容用户微博列表)也同样如此
本次培训也会着重介绍MySQL及其分布式架构方案。
2\. NoSQL(Not only SQL)数据库,泛指非关系型的数据库,兴起的契机在于传统关系型数据库应对大规模、高并发的能力有限,而NoSQL的普遍性能优势能够弥补关系型数据库在这方面的不足
* 存储非结构化数据、半结构化数据
* 性能
业界使用的NoSQL多为内存集中型服务,受限于I/O及网络,通常请求响应时间在毫秒级别,单机QPS在10万级别(与数据大小及存储复杂度相关)
* 常见的几类NoSQL产品
* K-V(Memcached、Redis),这类NoSQL产品在互联网业内应用范围最广。Memcached提供具备LRU淘汰策略的K-V内存存储;而Redis提供支持复杂结构(List、Hash等)的内存及持久化存储
* Column(HBase、Cassandra),HBase是基于列式存储的分布式数据库集群系统
* Document(MongoDb)
* Graph(Neo4J),最庞大、最复杂的Graph模型是人的关系,理论上用图描述并且用Graph数据库存储最合适不过,不过目前的数据规模、系统性能仍然有待优化
web2.0时代,NoSQL产品在互联网行业中的重要性随着互联网及移动互联网的发展而与日剧增 大型互联网应用,为应对大规模、高并发访问,大多都引入了NoSQL产品,其中Memcached、Redis以其高成熟度、高性能、高稳定性而被广泛使用。微博平台也具备千台规模的NoSQL集群,微博核心的Feed业务、关系业务也都依赖Memcached及Redis提供高性能服务
本次培训,会着重介绍Redis及其分布式架构
## 三、MySQL与MySQL分布式架构设计
微博平台核心业务的数据都存储在MySQL上,目前具备千台规模的集群,单个核心业务数据突破千亿级,单个核心业务QPS峰值可达10万级每秒,写入也是万级每秒。
在海量数据并且数据量持续增长的景下,在如何设计满足 高并发(w/r)、低延时(10ms级别)、高可用性(99.99%)的分布式MySQL系统方面,我们已经具备充足的经验并且依然在持续攻坚这一问题,而我们的课程也会着重介绍海量数据存储之MySQL。
1\. MySQL简介
* MySQL是一个关系型数据库系统RDBMS
* 使用SQL作为查询语言
* 开源
* 存储引擎
* Innodb 支持事务、行锁,写入性能稍差
* MyIsam 不支持事务,读写性能略好
* 满足ACID特性
* 主键、唯一键、外键(大规模系统一般不用)
* Transaction,事务即一系列操作,要么完全地执行,要么完全地不执行
* 服务、端口、实例,都是指 服务端启动的一个MySQL数据库
* 性能
* ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f664752dc1f.jpg)
* 随着磁盘性能升高,读写性能也逐步升高,但成本也随之增加
* 数据库的写入性能:写入tps随着并发量增加而增加,但上升到一定瓶颈,增速放缓至并发数临界点后 tps会急剧下滑
*
思考:如果对性能有更高(超出上述三种存储介质并发量级)的要求怎么办?
* 定制存储:针对服务特点,定制存储,定制更适合自己业务场景的存储产品。然而一般业界成熟产品为考虑通用性而会牺牲部分性能
* 引入NoSQL
2\. 从单机到集群的架构变迁
* 业务上线初期,web服务规模较小,一般具备以下特点
* * 服务原型时期,用户基数小,多种业务公用资源,日均写入百万级别,读取千万级别
* 数据规模小,单机性能能够满足需求
* 用户规模小,开发重心偏向迭代速度
考虑到上述小型业务特点,为节约资源成本及开发成本,可以采用多个业务混合部署形式
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f66475c4a4f.jpg)
* 当用户增多,数据量、访问量升高(2倍以下),数据库压力较大,怎样在有限程度提高MySQL吞吐量呢?
* * SQL优化
* 硬件升级
压力还在有限的范围内增长,通过简单、低成本优化,可以一定成都上提高有限的服务性能
* 业务持续发展,读取性能出现瓶颈&&各业务互相影响,多个业务出 现资源抢占,如何快速解决业务抢占问题以提高服务性能?
* * 垂直拆分——按业务进行数据拆分
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f6647654371.jpg)
按业务进行拆分,以使业务隔离,timeline的压力增加,不会影响content数据库服务性能;进行拆分后,资源增加,服务性能也相应提升。
* 随着业务的继续发展,读取性能出现瓶颈,读写互相影响,如何确保读请求量的增加,不要影响写入性能?相反写入请求量增加如何确保不影响读取性能?(写入性能出现问题会造成数据丢失)
* 读写分离,写入仅写master,master与slave自动同步;读取仅以slave作为来源
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f66476962fe.jpg)
读写分离后,slave仅专注于承担读请求,读取性能得到优化;同里独立的master服务的写入性能也得到优化。
* 一台/一对M-S服务器性能终归是很有限的,当单实例服务性能无法承载线上的请求量时,如何进行优化?
* * 升级为一主多从架构
* 一个master承载所有写入请求,理论上master性能不变
* 多个slave分担读取请求,读取性能提升n倍
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f66476c56e3.jpg)
* 随着业务量的增长,服务出现如下变化:
* 数据量增长,意味着原本的存储空间不足
* 写入量增长,意味着master写入性能存在瓶颈
* 读取量增长,意味着slave读取性能也存在瓶颈,但通过扩充slave是有限制的:一方面M-S replication性能有风险;另一方面扩充slave的成本较高
如何优化以解决上述问题?
* 水平拆分
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f66476ee863.jpg)
业务经历数据量的增长、读写请求量的增长,数据库服务已经演进为分布式架构,一个业务的数据,怎样合理的分布到上述复杂的分布式数据库是下一个需要解决的问题
3\. 如何基于上述演进到最后的架构进行数据库设计呢?
* 分布式数据库设计
* hash拆分方式,既按hash规则,将数据读写请求分散到多个实例上,见上述水平拆分示意图
* 时间拆分方式,基于确定好的时间划分规则,将数据按时间段分散存储再多个实例中
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f664771a14a.jpg)
数据分布到一个分布式数据库内,一个实例存储1/n的数据,一个实例只需要一个数据库就能够满足功能需求。
经历几年的发展,数据规模会成倍增长,当需要再次水平扩容(4太→8台),需要通过程序,将数据一分为二,数据迁移成本较高,需要开发人员介入。
如果在数据库设计时,一个就预先建好2个数据库 ,每个数据库存储1/n/2的数据,需要水平扩容时,即可完整迁移一个数据库,而无需开发人员干预。
在一个数据库实例上,建立的多个数据库,称为逻辑库。
* 逻辑库设计
* * 逻辑库是相对与物理库而言的概念:物理库只数据库服务的实例;逻辑库指在一个数据库实例上创建的多个database
* 定义逻辑库的目的是便于扩容。假如4台数据库服务器,每台上的物理库包含8个逻辑库,当系统出现容量、写入量瓶颈时,可以新增一倍即4台服务器,直接以同步方式同步数据库,而不需要单独编写应用程序利用进行导入
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f6647739616.jpg)
4\. 基于上述分布式数据库下的表拆分设计方式
* hash拆分方式:按hash规则将一个数据库的数据,分散hash到多张表中
* ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f6647753720.jpg)
* 适合数据规模有限的数据集
* 适合增长速度可控的数据集
结合数据库的hash模型如图:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f6647772153.jpg)
* 根据uid hash到uid所在到数据库,然后再hash到数据库_1下的tb_5表
* 按时间拆分方式,按时间规则将同一时段的数据存储在一张表,多个时段时间存储在多张表。例如按月划分,每个月表存储一个月的数据,如果需要获取全部数据需要跨越多个月表
* ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f6647786f94.jpg)
* 适合存储增速较快的数据集
* 但查询数据需要跨越多个时间段的表
结合数据库的hash模型如图:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f664779bbfb.jpg)
* 根据uid hash到所在的数据库db_1,然后再查找201507 201506获取两个月的数据
思考一个问题:如何能够快速定位,一个人的第1000条到1100条数据呢?
* 二级索引快速定位(一级)索引位置
* * 描述数据在以及索引中的分布状况
* 用于快速定位/缩小查询范围
* 一般情况字段列表:uid, date_time, min_id, count
* ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f66477b6380.jpg)
5\. 当一台服务器宕机怎么办?
* Slave(一主多从)宕机?
* 剩余健康Slave无风险,则无需紧急操作,例行修复
* 切换流量到容灾机房(如果具备容灾机房)
* 紧急扩容[优先]、重启、替换
* 有损降级部分请求
* Master宕机?
* 由于master数据的唯一性,致使master出现异常会直接造成数据写入失败
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f6647810036.jpg)
* * 快速下线master
* 下线一台salve的读服务(如果slave性能有风险,则同时快速扩容)
* 提升slave为master
* 生效新master与slave的同步机制
6\. 如此复杂的分布式数据库+数据库拆分+数据表拆分,client端如何便捷操作呢?
多数使用分布式数据库服务的团队,都有各自实现的数据库Client组件,微博平台采用如下几个层级的组建来进行分布式数据库操作
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f6647829d9e.jpg)
* 获取TableContainer,获取所有表定义规则
* 通过table name从TableContainer中获取指定的TableItem
* TableItem关联多个JdbcTeplate-DataSource
* 通过TableItem结合uid、id、date获取经过hash计算得到正确的JdbcTemplate及SQL
* 使用JdbcTemplate进行SQL操作
7\. 注意事项
* MySQL设计应该注意的问题
* 表字符集选择UTF8
* 存储引擎使用InnoDB
* 使用Varchar/Varbinary存储变长字符串
* 不在数据库中存储图片、文件等
* 每张表数据量控制在20000W以下
* 提前对业务做好垂直拆分
* MySQL查询应该遇到的问题
* * 避免使用存储过程、触发器、函数等
* * 让数据库做最擅长的事
* 降低业务耦合度避开服务端BUG
* 避免使用大表的JOIN
* * MySQL最擅长的是单表的主键/索引查询
* JOIN消耗较多内存,产生临时表
* 避免在数据库中进行数学运算
* * MySQL不擅长数学运算
* 无法使用索引
* 减少与数据库的交互次数
* * select条件查询要利用索引
* 同一字段的条件判定要用in而不要用or
8\. MySQL练习题
* 设计一个每秒2000qps,1亿条数据的用户基本信息存储数据库。完成数据库设计,数据库搭建,web写入查询服务搭建。
* 定义用户信息结构:uid,name,age,gender
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f664788799b.jpg)
* 给定2个mysql实例,每个实例创建2个数据库
* 每个数据库创建2长表
* 编写代码,以hash形式,实现对数据库、表的数据操作
## 四、Redis与Redis分布式架构设计
微博作为web2.0时代具备代表性的SNS服务,具备庞大的用户群体和亿级的活跃用户,同时也承担着高并发、低延迟的服务性能压力。
Redis作为NoSQL系列的一个典型应用,以其高成熟度、高可用性、高性能而被用来解决web2.0时代关系型数据库性能瓶颈问题。例如微博的计数服务的请求量以达到百万级/s,数以百计的关系型数据库才能应对如此高的QPS,而且请求耗时较高且波动较大;然而使用Redis这种NoSQL产品,仅仅需要10台级别的集群即可应对,平均请求耗时5ms以下。
这一章节,为大家介绍Redis以及其大规模集群架构。
1\. Redis简介
* Redis是一个支持内存存储及持久化存储的K-V存储系统
* 支持复杂数据结构,相比与Memcached仅支持简单的key-value存储,Redis原生支持几类常用的存储结构,例如
* * hash:存储哈希结构数据
* list:存储列表数据
* 单线程
* 高性能,避免过多考虑并发、锁、上下文切换
* 数据一致性好,例如对一个计数的并发操作,不会有‘读者写者’问题
* 单线程无法利用多核,单可以通过启动多个实例方式,充分利用多核
* 原生支持Master-Slave
* 过期机制
* 被动过期——client访问key时,判断过期时间选择是否过期
* 主动过期——默认使用valatile-lru
* * volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰
* volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰
* volatile-random:从已设置过期时间的数据集中任意选择数据淘汰
* allkeys-lru:从全部数据集中挑选最近最少使用的数据淘汰
* allkeys-random:从全部数据集中任意选择数据淘汰no-enviction(驱逐):禁止驱逐数据
* Redis的字典表结构
* * ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f66478a6a3b.jpg)
* Key字典表hash table结构,有hash结构就意味着需要按需进行rehash,rehash的时间段内,对内存是有成倍开销的
* Value结构,存储Key对应的value
* Expire表结构,存储key的过期时间
* 额外开销60B+
* 持久化方式
* * AOF
* Snapshot——RDB文件快照
* 与MC的差异
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f664793f6e8.jpg)
* 平台的定制CounterService
* * 修改hash table为,增量扩展式的hash tables,例如每1亿个key存储在一个table中,数据超过1亿(或者一个临界比例)则开辟下一个1亿空间的table
* 废弃expire,Redis的主动过期策略无法像MC的LRU策略确保热数据留存在内存中,冷数据从缓存剔除,我们多数场景需要控制Redis中的数据量不突破内存限制
2\. Redis的主要数据结构
* String (key-value)
* Hash (key-field-value)
* List(key-values)
* Set(key-members)
* SortedSet(key-member-score)
3\. Redis的分布式部署方案是怎样的?与MySQL有什么异同
* Reids由于其M-S特性与MySQL类似,因此分布式部署方案同MySQL相当
* 单实例——小型业务 or 业务初期
* 主从——HA、读写分离
* 一主多从——读取性能出现瓶颈
* 数据水平拆分——容量不足|写入性能瓶颈
* 常用的分布式部署方案
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f6647953eec.jpg)
4\. 分布式Redis架构如何实现高可用(HA)?
* 采用M-S高可用方案,原因也式由于其Master-Slave的特性
* 服务域名化是必要的,目前大型的Redis集群应用大多采用域名方式
5\. 基本容量规划
* 空间=key数量*单条占用(K-V占用+额外空间) 用户空间=5亿用户*200B(平均)=100G 微博计数器=(500亿+预期2年新增300亿)*10B=800G
* 访问量=服务访问量*单次访问对资源的hit量 微博计数器Feed访问量=10000/s * 20 = 20万/s
6\. CounterService
微博具备庞大的数据基数,因此所需要存储的数据量级也极其庞大
例如微博计数器,具有百亿条纪录,全部存储在Redis中,需要T级别的空间,成本过高
因此我们对Redis进行定制化改造,以使其适合多数数据小,大小有固定限制的数据
* 优化存储空间
* 采用分段哈希桶的形式,进行存储,避免rehash (分段存储要求key为递增序)
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f6647ec3d08.jpg)
* 空间占用优化效果
* key:8B
* value:自定义
7\. 如何支持上述分布式架构下的client访问?(redis3.0+支持Redis Cluster)
* Reids具有多个开源的client支持,我们所使用的是Jedis
* Jedis除了提供client外,还提供了操作封装以及M-S组件
* 我们所使用的Redis系列组件如下:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f664843a8bc.jpg)
8\. Redis练习题
* 使用Redis,实现用户受到的赞列表及赞计数功能
* 使用测试环境,启动两个Redis实例
* 使用Redis存储用户受到的赞列表[{uid, time}..]及赞计数uid-count
* 完成赞操作业务逻辑,包括赞、取消赞、查看赞列表、查看赞计数
## 五、思考与讨论
1\. Memcache当容量到达瓶颈会 截取LRU链以释放空间。上文介绍过Redis的key过期机制,思考以下几个问题:
* Redis满了会发生什么?如何避免发生上述问题呢?
* 为什么我们的定制Redis会废弃expire表?
2\. MySQL与Redis各自适合什么样的场景?
* 数据冷热?
* 数据大小?
* 数据量级?
* 数据增长速度?
* 是否持久化?
* 访问量(read/write)?
* 请求性能要求?
* * * * *
**新兵训练营简介**
微博平台新兵训练营活动是微博平台内部组织的针对新入职同学的团队融入培训课程,目标是团队融入,包括人的融入,氛围融入,技术融入。当前已经进行4期活动,很多学员迅速成长为平台技术骨干。
微博平台是非常注重团队成员融入与成长的团队,在这里有人帮你融入,有人和你一起成长,也欢迎小伙伴们加入微博平台,欢迎私信咨询。
**讲师简介**
毕建坤,[@bijiankun](http://weibo.com/n/bijiankun) 微博平台及大数据部——平台研发系统研发工程师,2012年7月毕业于哈尔滨理工大学,校招入职微博工作至今,先后负责微博Feed、赞、评论等底层服务研发以及方案评审等工作。聚焦大规模系统的架构设计与优化,以及大规模系统下的服务稳定性保障。新兵训练营第一期学员。
分布式缓存介绍
最后更新于:2022-04-01 03:15:16
> 原文出处:http://weibo.com/p/1001643872772421209951
> 作者:赖佳俊[@LierD](http://weibo.com/n/LierD)
![document/2015-09-14/55f6683a6535c](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/document_2015-09-14_55f6683a6535c.png)
微博作为目前最大的中文社交媒体平台,拥有着上亿的日活用户。我们每天都会面临各种非常具有挑战性的业界技术难题。其中最具挑战性的几类问题是:
1. 海量数据存储。微博总量已经超过千亿数据。海量数据的存取是一个非常大的技术挑战
2. 巨大的并发请求。主feed接口的设计需要面对4倍于日常请求峰值的情况。
3. SLA要求高。核心系统需要保证至少4个9的稳定性,核心接口平均响应时间1s内需要达到4个9
我们在解决上述的几类挑战中,结合自身的业务场景以及实际请求情况,设计出了高容量、可扩展、具有稳定性保障等特点的服务架构。本文主要结合微博平台的业务场景以及个人对分布式缓存的使用经验和体会来做说明介绍。
文章主要从以下几个方面来展开:
(1)缓存介绍,包括缓存的引入,缓存内存分配策略,以及淘汰算法等基本要点说明
(2)分布式缓存架构,主要结合平台缓存使用中碰到的问题以及一些考虑点,说明缓存系统的变迁。
[TOC]
## 1. 缓存介绍
### 1.1 为什么引入缓存
在传统的后端架构中,由于请求量以及响应时间要求不高,我们经常采用单一的db的结构。如下图1 所示,应用服务器直接存取DB。这种架构简单,但也存在着如图中所描述的问题,即DB存在性能瓶颈,随着请求量的增加,单DB无法继续稳定提供服务。
对于请求量不大的场景,我们可以通过对DB进行读写分离、一主多从、硬件升级(SSD)等方式提升系统的承载能力以及冗余能力,但这几种提升方式存在着以下几个缺陷:
(1)性能提升有限。很难得到量级上的提升,而大部分互联网产品直接面对着千万级用户访问,单一使用db的结构,难以达到性能要求
(2)成本高昂,为了达到N倍的承载能力的提升,需要至少N倍以上DB服务器
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f66333e8532.jpg)
图1 传统服务架构
那么我们是否有更好的方式来做呢?
相信大家在学习操作系统的时候,一定看过如图2 类似的一组数据:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f6633470ee5.jpg)
图2 各类存储介质的访问速度
从图2中可以看到,一次内存寻址的时间大概在100ns,顺序从内存中获取1MB数据的时间大概在250000ns。对应的,我们可以看到一次磁盘寻址以及顺序读取磁盘的速度大概在千万ns级别。这里我们可以得出一个结论,也即内存数据的获取速度大概在磁盘的获取速度的两个数量级。也因此我们可以通过引入缓存中间件,来提高系统整体的承载能力,原有的单层db结构也可以变为如图3所示的缓存+db结构。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f66335148f4.jpg)
图3 DB+缓存
通过在应用服务与DB中间引入缓存层,我们可以得到如下三个好处:
(1)读取速度得到提升
(2)系统扩展能力得到大幅增强。我们可以通过加缓存,来让系统的承载能力提升
(3)总成本下降,单台缓存即可承担原来的多台DB的请求量,大大节省了机器成本
常见的缓存服务有本地缓存,memcached,redis等。他们各有自己的特点,本文以下内容主要是结合微博对于memcached一些使用来做讲解
### 1.2 Memcached
1.2.1 Memcached介绍
Memcached 是一个高性能的分布式内存对象缓存系统,广泛应用于动态Web应用,以减轻数据库负载。它通过在内存中缓存数据和对象来减少读取数据库的次数,从而提高网站的访问速度。
Memcached都有哪些特点呢?
1. 本身是一个kv存储系统。存储的时候,需要指定key以及对应的value;获取的时候,只能通过key获取
2. 协议简单,只支持简单的命令如:set、get、cas、delete、stats、incr、decr等,而无类似于mysql的select、join等复杂的sql命令。所有的命令以及数据都是以文本的形式传递,这也就意味着mc的协议无需特殊的客户端解析工具。我们可以通过在终端telnet进行命令传递以及数据获取。
3. 支持设定过期时间
4. 支持LRU等淘汰算法
1.2.2 Memcached的安装以及使用
Memcached的安装可以通过在官网上下载源码的方式安装:http://memcached.org/
相关安装教程可以参考官网安装步骤,亲测有效~:http://code.google.com/p/memcached/wiki/NewInstallFromSource
安装完成后,可以在linux终端执行以下命令启动 ./memcached -l 11211
新启一个终端,敲入命令telnet localhost 11211,即进入命令行模式
下面我们一起来试试Memcached hello world调用:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f663358b4b3.jpg)
图4 memcached helloworld
因本文篇幅有限,具体命令不再展开描述,大家如果感兴趣可以通过官网的链接:http://code.google.com/p/memcached/wiki/NewCommands 了解。
1.2.3 Memcached内存分配原理介绍
掌握Memcached的安装、使用命令,其实对大部分的同学来说已经足以开展相关开发工作了。但当碰到一些线上问题的时候,单纯的会用Memcached是无法快速、合理的分析问题所在的。所以接下来我们将介绍Memcached的内存分配管理原理。
Memcached默认情况下采用了名为Slab Allocator的机制分配、管理内存。Slab Allocator的基本原理是按照预先规定的大小,将分配的内存分割成特定长度的块, 以完全解决内存碎片问题。
在介绍memcached的内存分配原理之前,需要跟大家说明以下几个关键的名词的概念:
item
一个待存储的元素,按字节计算大小,可以理解为一个物品
Chunk
用于缓存item的内存空间。可以理解为一个储物格
Slab Class
特定大小的chunk的组。可以理解为储物格按大小进行分类,如80B作为一类,96B作为一类....
Page
分配给Slab class的内存空间,默认是1MB。分配给Slab之后根据slab class的大小切分成chunk。可以理解为一个page是一个固定大小的柜子,上面可以按slab class进行分割,一个柜子只能按一个slab class进行分割。柜子上的格子数为柜子大小/ 储物格的大小
介绍完上述的几个基本概念后,我们可以来看看mc在分配内存的时候是怎么处理的。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f66335d9f5b.jpg)
图5 memcached 初始化示意图
如图5为一个memcached示例在启动的时候,可以指定的一些参数,初始大小为slab class的起始大小,增长因子为下一个slab class是初始因子的倍数。如图中所示,初始大小为80B,增长因子为1.5。则mc在启动后,会按下图生成slab class表。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f6633604cd2.jpg)
图6 slab分布图
完成初始后,当某一个请求到来的时候——如图中所示由一个123B大小的元素希望找到存储空间,memcached会通过slab class表找到最合适的slab class:比元素大的最少的那个,在图中场景下为180B,即使所需的空间只要123B。
此时Memcached示例并没有分配任何的空间给180B的slab进行管理。所以为了能让请求的元素能存储上,Memcached实例会分配1 个page给180这个slab(在默认情况下为1MB实际内存)。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f6633b59b6b.jpg)
图 7 page分配图
180B slab class在获取到1MB的空间后,会按照自己的大小对page进行分隔,也即1MB/180=5828个具体的存储空间(chunk)。此时,123B的请求就可以被存储起来了。
随着时间的慢慢推移,memcached的内存空间会逐步被分配完,如下图8所示:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f6633bdbe63.jpg)
图 8 内存slab分配图
我们可以看到,memcached划分给每个slab的page数是不均等的,存在部分的slab是可能一个page都分配不到的。
假设所有的内存都分配完,同时每个slab内部的chunk也都分配完了。此时又来了一个新的元素123B,那么就会触发memcached的淘汰机制了。
memcached首先会查看180B的slab是否存在过期的元素,如果存在,则先清理部分,预留空位。如果180B这个slab的数据都比较热(没有过期),则按LRU进行淘汰。需要注意的是,淘汰是在slab内部进行的,也即在上面的场景中只有180Bslab内部进行淘汰剔除,对于其他的slab,是没有受到影响的。memcached也不会回收比较空余的其他slab的page。也即一个page被分配给某个slab后,他将一直被这个slab所占用,永远无法被mc回收,直到memcached重启。
这个特性被称为Memcached的钙化问题:Memcached在线上跑了一段时间后,内存按原始访问模式分配内存。当访问模式变更后,原有的分配模式可能导致缓存频繁出现数据剔除问题。最典型的场景即为内存尚有空余,但一直有数据被剔除,命中率一直上不去。对于这种情况,解决方法为重启缓存。
1.2.4 小结
上面两节中我们主体介绍了Memcached的使用以及内部的一些内存分布原理。相信大家在自己动手操作过后,会有一个较深的影响。在实际的使用中,除了关注缓存本身的一个使用外,我们还会有其他一些关注点。如高可用、扩展性等,这些关注点的实现并不是单纯依靠单一服务节点即可。为了能够满足上述的几个关注点,我们需要引入分布式缓存结构。
## 2\. 分布式缓存
### 2.1 分布式?
考虑之前在缓存引入小节中所描述的,我们在原有的单层db结构中引入了缓存memcached:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f6633c360ac.jpg)
图 9 DB+缓存
在这种单实例缓存架构下,随着业务规模的不断增长,我们发现存在如下几个问题:
(1)容量问题
单一服务节点无法突破单机内存上限。目前微博数据已经超过千亿数据,虽然无需所有数据都放入缓存当中,但在保证一定的命中率的情况下(注:微博核心缓存命中率需要达到99%)缓存部分数据,也需要以百GB甚至是TB为单位的内存容量。而单台服务器的内存总有上限(目前微博使用的常用服务器内存上限128G),也即单实例缓存无法满足缓存所有数据的需要
(2)服务高可用(HA)
在单实例场景下,缓存如果因为网络波动、机器故障等原因不可用,原来由缓存承担的请求会全部落到DB上,最终系统会如同雪崩般,全部crash。
(3)扩展问题
单一服务节点无法突破单实例请求峰值上限。微博业务是存在热点突发事件的,也即随时会有可能出现数倍于日常请求峰值的情况。虽然我们在做资源的评估的时候,会确保资源的冗余度足够满足请求翻倍(一般为2~4倍)的情况。但我们也需要后端资源是可以线性扩展的
下面我们也会针对上面的几个问题,一步一步说明相应的解决方案。
### 2.2 分布式缓存实现
微博线上服务都是由多个服务端节点存储数据。由于memcached本身不支持分布式,所以需要客户端或者中间层代理实现分布式节点获取。在今天的描述中,为了简化问题,会主要以客户端实现分布式为主,即应用服务器(客户端)根据内部的算法,选择特定的缓存节点,存取数据。
2.2.1 数据分片(Sharding)
前面提到单实例memcached主要会有以下几个问题:
(1)请求量请求量超过单端口可承受极限
(2)数据容量超过单实例内存容量。
为了解决以上两个问题,我们的做法是从单端口实例扩展为一组实例。一组memcached由多个memcached实例组成,每个实例上只缓存数据总量的一部分数据,客户端在请求的时候,需要决定从哪个memcached中获取数据(hash)。
常见的hash算法可以使用:
(1)取模hash
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f6633c6a17b.jpg)
图 10 取模hash
如图10所示,客户端根据请求的key,做取模,最终根据结果选择对应的memcached节点。这种方式实现简单。易于理解,缺点在于加减节点的时候,会造成较大的震荡:每加、减一个节点,hash方式全部改变,整体命中率会下降的非常快。
(1)一致性hash
一致性哈希的算法描述可以参考
https://zh.wikipedia.org/wiki/%E4%B8%80%E8%87%B4%E5%93%88%E5%B8%8C
一致性哈希算法的主要优点在于加减节点,对于服务整体的震荡较小。并且在某个节点crash后,可以将后续请求,转移至另一个节点中,具备一定的防单点特性。
2.2.2 主从双层结构
从单台实例增加到一组缓存后,我们可以解决单端口容量、访问量不足的问题,但是如果出现某一台缓存挂了的情况。请求依然会落到后端的DB上。
上面也提到了一致性hash的算法,可以通过一致性hash的方式,来减少损失。
但基于一致性哈希策略的分布式实现在微博业务场景下也存在一些问题:
(1)微博线上业务对缓存命中率要求高。某台缓存挂了,会导致缓存整体命中率下降(即使一致性hash会在一定时间后将数据重新种到另一个节点上),对于命中率要求在99%以上的Feed流核心业务场景来说,命中率的下降是难以接受的
(2)一致性hash存在请求漂移的情况,假设某一段时间服务因网络因素访问某个服务节点失败,则在这时候,会将数据的更新、获取都迁移到下一个节点上。后续网络恢复后,应用服务探测到服务节点可用,则继续从原服务节点中获取数据,这就导致了在故障期间所做的更新操作,对于原服务节点不可见了
目前我们对于这种单点问题主要是通过引入主从缓存结构来解决的。主从结构示意图如下图11所示:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f6633c7ef4a.jpg)
图 11 主从双层结构缓存
服务端在上行逻辑中,进行双写操作——由应用服务负责更新master、slave数据。
下行获取数据,先获取master数据,当master返回空,或者无法取到数据的时候,访问slave。
在这种模式下,为了避免两份数据带来的不一致问题,需要以master数据为准。即如果有更新数据操作,需要从master中获取数据,再对master进行cas更新。更新成功后,才更新slave。如果cas多次后都失败,则对master、slave进行delete操作,后续让请求穿透回种即可。
2.2.3 横向线性扩展
在双层结构下,我们可以很好的解决单点问题,即某一个节点如果crash了,请求可以被slave承接住,请求不会直接落在DB上。
但这种架构仍然存在一些问题:
(1)带宽问题。由于存在热点访问的情况,线上经常出现单个服务节点的带宽跑满的情况。
(2)请求量问题。随着业务的不断发展,并发请求数超过了单个节点的机器上限。数据分片、双层结构都不能解决这种问题。
上面的两个问题,其实总结起来是如何快速横向扩展系统的支撑能力。对于这个问题,我们的解决思路为增加数据的副本数。即让数据副本存在于多个节点中,从而平摊原本落在一个节点的请求。
从我们经验来看,对于线性扩展,可以在原来的master上引入一层L1层缓存。整体示意图如12所示:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f6633cd2375.jpg)
图12 采用L1的缓存架构
上行操作需要对L1进行多写。写缓存的顺序为master-slave-L1(所有),写失败则进行delete操作,后续由穿透请求进行回种。
L1可以由多组缓存组成,每组缓存相互独立。应用服务在获取数据的时候,先从L1中选取一组资源,然后再进行hash选取特定节点。对于multiget的场景也是先选取一组缓存,然后才对这组缓存进行multiget操作。如果L1获取不到数据,再依次获取master、slave数据。获取成功,则回种到L1中。
在采用L1的模式中,数据也是以master中数据为主的。即如果有更新数据的需要,需要从master中获取数据原本,再进行cas更新。如果cas更新成功,才同时更新slave、L1资源。如果对master的操作失败,则进行delete all操作,让后续请求穿透回种。
当线上流量、请求量达到一个水位的时候,我们会进行L1的扩容——增加一组、或几组L1缓存,从而提升系统整体的承载能力。此时系统的整体响应请求量是可以做到线性扩展的。
可以看到,双层结构下,slave作为主的备份存在。假设线上master缓存命中率为99%,则落在slave上的请求只有1%,并且这1%的请求都是很偏、很少人访问到的。可以想象,在这种情况下,如果master真的出现问题,请求全部落在slave上,slave也是没有任何数据可供访问的。Slave作为防单点措施是失败的。
引入L1后,slave过冷并没有被解决,同时,由于master被放置到L1之下,也遇到了slave的问题,master的数据也存在过冷的风险。为了解决上面的问题,我们在线上配置的时候,会将整组slave做为L1的一组资源进行配置,让slave以L1的身份承担部分的热请求。同时为了解决master过冷的问题,我们也会让应用服务在选择L1的时候有一定的概率落空,从而让master作为L1逻辑分组,去承担部分热请求。整体结构图如图13所示:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-14_55f6633d3b78b.jpg)
图13 slave、master同时作为L1架构
## 结尾:
本文主要结合目前微博平台的线上业务场景,根据个人对memcached的使用的经验,以及分布式架构情况做了介绍。行文仓促,肯定有很多细节需要继续完善,如果有问题或者建议,可以微博私信 @微博平台架构 或讲师 [@LierD](http://weibo.com/n/LierD) 一起继续探讨。
随着微博业务的蓬勃发展,相信还会有更多的技术挑战在等着我们去解决,去征服。我们也希望看到本文的小伙伴以及有志于做一些不一样的互联网产品的小伙伴们,能够加入我们,跟我们一起去攀登技术高峰~
* * * * *
**新兵训练营简介**
微博平台新兵训练营活动是微博平台内部组织的针对新入职同学的团队融入培训课程,目标是团队融入,包括人的融入,氛围融入,技术融入。当前已经进行4期活动,很多学员迅速成长为平台技术骨干。
具体课程包括《环境与工具》《分布式缓存介绍》《海量数据存储基础》《平台RPC框架介绍》《平台Web框架》《编写优雅代码》《一次服务上线》《Feed架构介绍》《unread架构介绍》
微博平台是非常注重团队成员融入与成长的团队,在这里有人帮你融入,有人和你一起成长,也欢迎小伙伴们加入微博平台,欢迎私信咨询。
**讲师简介**
赖佳俊(微博昵称:[@LierD](http://weibo.com/n/LierD) ),2013年7月毕业于哈尔滨工业大学后,加入新浪微博工作至今,担任系统研发工程师。曾先后参与微博Feed流优化、后端服务稳定性建设等项目。关注jvm调优,微服务,分布式存储系统。微博平台新兵训练营二期学员.
业余爱好三国杀,dota,喜欢自己做饭下厨~~希望能在微博的平台上认识到更多优秀的小伙伴,一起交流,一起成长~~